diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..7dff644e5f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,59 @@ +name: Bug report +description: A title, homebrew or test stops, crashes or renders wrongly. +labels: [bug] +body: + - type: markdown + attributes: + value: | + Search open and closed issues and pull requests for the error text first, and comment there if it is already reported. + Game bring-up progress belongs in [Discussions](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up), one thread per title. + A slow scene or a change in speed goes into the [Performance report](https://github.com/boykopovar/AnyPS5/issues/new?template=performance.yml) form, which asks for a [performance trace](https://github.com/boykopovar/AnyPS5/blob/main/docs/dev/BUILD.md#performance-trace) and its `tools/trace_digest.py` digest. + - type: input + id: summary + attributes: + label: What happens + description: One sentence, with the exact error text if there is one. + validations: + required: true + - type: input + id: title + attributes: + label: Title, test or build step + description: Title ID and content version (for example PPSAxxxxx 01.000.000), a homebrew with its source, the ctest name, or the build step that fails. + validations: + required: true + - type: input + id: commit + attributes: + label: AnyPS5 commit + description: The `main` commit that was built, or the release that was used, and any pull requests merged on top. + validations: + required: true + - type: input + id: system + attributes: + label: System + description: OS, GPU and driver version, and the compiler if you built it yourself. + placeholder: Arch Linux, GCC 16.2.1, RTX 5080 (NVIDIA 615.78.08) + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: The commands that were run, environment variables included, and how long it takes to stop. + validations: + required: true + - type: textarea + id: log + attributes: + label: Log + description: The lines around the stop. + render: text + validations: + required: true + - type: textarea + id: notes + attributes: + label: What was checked + description: Other devices or commits tried, and whether it is the same every run. Attach the full log and screenshots here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..789cbc7d74 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Game bring-up + url: https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up + about: Progress of one title, one thread per title ID + - name: Question + url: https://github.com/boykopovar/AnyPS5/discussions/categories/q-a + about: Building, converting and running diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000000..672c3a2662 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,3 @@ +self-hosted-runner: + labels: + - anyps5 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 991703ac12..c084976f5c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -13,6 +13,7 @@ - [ ] New third-party code is a submodule built from source - [ ] Generic behaviour, not specific to one title - [ ] Depends on: +- [ ] Fixes: - [ ] Unblocks: - [ ] GPUs tested: - [ ] AI-assisted: diff --git a/.github/workflows/area-labels.yml b/.github/workflows/area-labels.yml index bd3d6a370d..f76171b143 100644 --- a/.github/workflows/area-labels.yml +++ b/.github/workflows/area-labels.yml @@ -26,7 +26,7 @@ jobs: state=$RUNNER_TEMP/area-state.json [ -s "$state" ] || echo '{}' > "$state" config=$(gh api "repos/$GITHUB_REPOSITORY/contents/.github/labeler.yml" --jq .sha) - gh pr list -R "$GITHUB_REPOSITORY" --base "${{ github.event.repository.default_branch }}" -L 200 --json number,headRefOid \ + gh pr list -R "$GITHUB_REPOSITORY" --base "${{ github.event.repository.default_branch }}" -L 1000 --json number,headRefOid \ --jq "map({key: (.number | tostring), value: (.headRefOid + \"-$config\")}) | from_entries" > "$RUNNER_TEMP/area-open.json" jq -r --slurpfile old "$state" 'to_entries[] | select($old[0][.key] != .value) | .key' "$RUNNER_TEMP/area-open.json" | head -n 40 > "$RUNNER_TEMP/area-todo" jq -c --slurpfile open "$RUNNER_TEMP/area-open.json" --rawfile todo "$RUNNER_TEMP/area-todo" \ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a851258417..cab59565e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: inputs: pr: - description: Pull request numbers to build together, separated by spaces or commas (up to 8) + description: Pull request numbers to build together, separated by spaces or commas (up to 40) required: true type: string linux: @@ -17,13 +17,23 @@ on: required: false default: true type: boolean + full: + description: Run the full Linux and Windows builds (off runs only the relinker builds and tests on Linux, Windows and macOS) + 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: >- @@ -47,29 +57,129 @@ jobs: register-context: ${{ steps.gate.outputs.register-context }} write-watch: ${{ steps.gate.outputs.write-watch }} full: ${{ steps.gate.outputs.full }} + relinker-hosts: ${{ steps.gate.outputs.relinker-hosts }} steps: - uses: actions/github-script@v8 id: gate env: PR_NUMBER: ${{ inputs.pr }} + LINUX_BUILD: ${{ inputs.linux }} + FULL_BUILD: ${{ inputs.full }} with: script: | + 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: 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' + ), + 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') { throw new Error('Automatic full builds only run on main'); } + let paths = []; + let all = true; + if (!/^0+$/.test(context.payload.before)) { + try { + const { data } = await github.rest.repos.compareCommitsWithBasehead({ + owner, repo, basehead: `${context.payload.before}...${context.sha}` + }); + 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}`); + } + } + const run = select(paths, all); core.setOutput('sha', context.sha); core.setOutput('pr', ''); core.setOutput('head', ''); - core.setOutput('relinker', true); - core.setOutput('register-context', true); - core.setOutput('write-watch', true); + core.setOutput('relinker', run.relinker); + core.setOutput('register-context', run.registerContext); + core.setOutput('write-watch', run.writeWatch); core.setOutput('full', true); - await core.summary.addRaw(`Building main at ${context.sha}: all builds and tests enabled`).write(); + core.setOutput('relinker-hosts', relinkerHosts(true, true)); + await core.summary.addRaw( + `Building main at ${context.sha}: full builds enabled, ` + + `macOS relinker ${run.relinker ? 'enabled' : 'skipped'}, ` + + `register context tests ${run.registerContext ? 'enabled' : 'skipped'}, write watch tests ${run.writeWatch ? 'enabled' : 'skipped'}` + ).write(); return; } - const { owner, repo } = context.repo; const manual = context.eventName === 'workflow_dispatch'; + const linux = process.env.LINUX_BUILD !== 'false'; + const full = manual && process.env.FULL_BUILD !== 'false'; if (manual) { for (const username of new Set([context.actor, process.env.GITHUB_TRIGGERING_ACTOR])) { const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username }); @@ -77,8 +187,8 @@ jobs: throw new Error('Only collaborators with write access can start or rerun builds'); } } - if (!/^[1-9][0-9]*([ ,]+[1-9][0-9]*){0,7}$/.test(process.env.PR_NUMBER.trim())) { - throw new Error('Enter up to 8 positive pull request numbers separated by spaces or commas'); + if (!/^[1-9][0-9]*([ ,]+[1-9][0-9]*){0,39}$/.test(process.env.PR_NUMBER.trim())) { + throw new Error('Enter up to 40 positive pull request numbers separated by spaces or commas'); } } const numbers = manual @@ -90,7 +200,6 @@ jobs: const pulls = []; const paths = []; let truncated = false; - let relinker = manual; for (const pull_number of numbers) { let { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number }); for (let attempt = 0; pr.mergeable === null && attempt < 10; ++attempt) { @@ -110,28 +219,37 @@ jobs: if (pr.mergeable === false) { throw new Error(`Pull request #${pull_number} does not merge cleanly into main; rebase it first`); } - if (!manual) { - const { data: commits } = await github.rest.repos.listCommits({ - owner, repo, sha: 'main', author: pr.user.login, per_page: 1 - }); - relinker = commits.length > 0; - } const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 }); truncated ||= files.length >= 3000; paths.push(...files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean))); + await readWorkflow(files, pr.head.sha); pulls.push({ number: pull_number, head: pr.head.sha }); } - const touches = (...prefixes) => truncated || - paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix)); - const workflow = '.github/workflows/build.yml'; - const libc = 'core/libs/prx/libc/'; - relinker = 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 - ); - const registerContext = touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc, workflow); - const writeWatch = touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc, workflow); + const run = select(paths, truncated); + let relinker = run.relinker; + if (relinker && !manual) { + const author = context.payload.pull_request.user.login; + const { data: commits } = await github.rest.repos.listCommits({ + owner, repo, sha: 'main', author, per_page: 1 + }); + relinker = commits.length > 0; + if (!relinker) { + try { + const { data: mergedPRs } = await github.rest.search.issuesAndPullRequests({ + q: `repo:${owner}/${repo} is:pr is:merged base:main author:${author}`, + per_page: 1 + }); + if (mergedPRs.incomplete_results) { + throw new Error('the search results are incomplete'); + } + relinker = mergedPRs.total_count > 0; + } catch (error) { + core.warning(`Skipping the relinker: could not search the merged pull requests of ${author}: ${error.message}`); + } + } + } + const registerContext = run.registerContext; + const writeWatch = run.writeWatch; const { data: main } = await github.rest.repos.getBranch({ owner, repo, branch: 'main' }); core.setOutput('sha', main.commit.sha); core.setOutput('pr', pulls.map((pull) => pull.number).join(' ')); @@ -139,12 +257,13 @@ jobs: core.setOutput('relinker', relinker); core.setOutput('register-context', registerContext); core.setOutput('write-watch', writeWatch); - core.setOutput('full', manual); + core.setOutput('full', full); + core.setOutput('relinker-hosts', relinkerHosts(full, linux)); const merged = pulls.map((pull) => `PR #${pull.number} at ${pull.head} merged into main at ${main.commit.sha}`); merged.forEach((line) => core.info(line)); await core.summary.addRaw( - `${merged.join(', then ')}: full builds ${manual ? 'enabled' : 'disabled'}, ` + - `relinker ${relinker ? 'enabled' : 'skipped: no relinker file changed or the author has no commit in main'}, ` + + `${merged.join(', then ')}: full builds ${full ? 'enabled' : 'disabled'}, ` + + `relinker ${relinker ? (full ? (linux ? 'enabled on macOS (the full builds run the Linux and Windows relinker tests)' : 'enabled on Linux and macOS (the full Windows build runs the Windows relinker tests)') : 'enabled') : 'skipped: no relinker file changed, or no commit or merged pull request in main was found for the author'}, ` + `register context tests ${registerContext ? 'enabled' : 'skipped'}, write watch tests ${writeWatch ? 'enabled' : 'skipped'}` ).write(); @@ -156,13 +275,7 @@ jobs: strategy: fail-fast: false matrix: - include: - - os: ubuntu-latest - compiler: g++ - - os: windows-latest - compiler: g++ - - os: macos-latest - compiler: clang++ + include: ${{ fromJSON(needs.eligibility.outputs.relinker-hosts) }} env: WINLIBS_RELEASE: 15.2.0posix-14.0.0-ucrt-r7 WINLIBS_ZIP: winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip @@ -184,7 +297,7 @@ jobs: 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 + 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 @@ -202,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 @@ -218,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 @@ -233,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 @@ -240,8 +422,14 @@ jobs: linux: needs: eligibility if: ${{ (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.linux)) && needs.eligibility.outputs.full == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 30 + runs-on: [self-hosted, linux, x64, anyps5] + timeout-minutes: 60 + env: + CCACHE_DIR: /var/cache/anyps5/ccache + CCACHE_MAXSIZE: 30G + CCACHE_COMPILERCHECK: content + MESA_SHADER_CACHE_DIR: /var/cache/anyps5/mesa + MESA_SHADER_CACHE_MAX_SIZE: 4G steps: - uses: actions/checkout@v7 with: @@ -258,46 +446,107 @@ jobs: 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 number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done + fetch_options=() + if [ "$(git rev-parse --is-shallow-repository)" = true ]; then fetch_options+=(--unshallow); fi + git fetch --no-tags "${fetch_options[@]}" origin "${refspecs[@]}" for i in "${!numbers[@]}"; do if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi done git submodule update --init --recursive - - uses: hendrikmuhs/ccache-action@v1.2.24 - with: - key: linux - save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} - evict-old-files: job - - name: Install lavapipe and the SDL audio, XRandR and Wayland development files - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev libwayland-dev libxkbcommon-dev libegl-dev libdecor-0-dev - - id: mesa + - name: Prepare persistent caches run: | - echo "version=$(dpkg-query -W -f='${Version}' mesa-vulkan-drivers)" >> "$GITHUB_OUTPUT" + mkdir -p "$CCACHE_DIR" "$MESA_SHADER_CACHE_DIR" + test -w "$CCACHE_DIR" && test -w "$MESA_SHADER_CACHE_DIR" + ccache --zero-stats + ccache --show-config + dpkg-query -W mesa-vulkan-drivers grep -m1 '^model name' /proc/cpuinfo - echo "cpu=$(grep -E '^(vendor_id|cpu family|model|flags)[[:space:]]*:' /proc/cpuinfo | sort -u | sha256sum | cut -c1-12)" >> "$GITHUB_OUTPUT" - - uses: actions/cache/restore@v6 - with: - path: .mesa-shader-cache - key: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-${{ github.sha }} - restore-keys: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}- - - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON + - name: 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 --parallel 4 --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 - MESA_SHADER_CACHE_DIR: ${{ github.workspace }}/.mesa-shader-cache - - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && !cancelled() }} + - 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: .mesa-shader-cache - key: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-${{ github.sha }} - - run: python3 tools/check_skips.py build/ctest.xml + path: test-graph.json + key: test-graph-${{ runner.os }}-${{ github.sha }} + - name: Report compiler cache statistics + if: ${{ !cancelled() }} + run: ccache --show-stats - name: Report tests slower than 30 s if: ${{ !cancelled() }} shell: bash @@ -314,7 +563,6 @@ jobs: env: WINLIBS_RELEASE: 15.2.0posix-14.0.0-ucrt-r7 WINLIBS_ZIP: winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip - SSL_CERT_FILE: C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt defaults: run: shell: bash @@ -323,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 @@ -334,13 +581,41 @@ jobs: 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 + for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done git fetch --no-tags --unshallow origin "${refspecs[@]}" for i in "${!numbers[@]}"; do if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi done - git submodule update --init --recursive + - id: 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 + bundle="C:/Program Files/Git/$prefix/etc/ssl/certs/ca-bundle.crt" + if [ -f "$bundle" ]; then echo "SSL_CERT_FILE=$bundle" >> "$GITHUB_ENV"; exit 0; fi + done + echo "::error::No CA bundle found in Git for Windows"; exit 1 - id: toolchain uses: actions/cache@v6 with: @@ -353,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 @@ -375,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/conventions.yml b/.github/workflows/conventions.yml index 2a14c1c2a3..d09210cfd0 100644 --- a/.github/workflows/conventions.yml +++ b/.github/workflows/conventions.yml @@ -18,15 +18,14 @@ jobs: steps: - uses: actions/checkout@v7 with: - ref: ${{ github.event.pull_request.base.sha }} + ref: ${{ github.event.pull_request.base.ref }} fetch-depth: 0 persist-credentials: false - name: Fetch pull request objects without checking out its code env: HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: git fetch --no-tags origin "$HEAD_SHA" - - name: Check conventions with the trusted base script + - name: Check conventions with the current base branch script env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: python3 -I tools/check_conventions.py --base "$BASE_SHA" --head "$HEAD_SHA" + run: python3 -I tools/check_conventions.py --base HEAD --head "$HEAD_SHA" 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..299adbcdb8 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -28,6 +28,9 @@ jobs: force_label ci 5319e7 '.github' force_label agc 1d76db 'libSceAgc*' force_label shader 1d76db 'core/shader' + force_label 'unblocks title' 0e8a16 'Names a title ID on the Unblocks line' + force_label perf fef2c0 'Performance change' + force_label 'perf numbers missing' e99695 'perf pull request without a before and after summary line' label relinker 5319e7 'core/relinker' label libc fbca04 'libc' @@ -35,4 +38,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/pages-progress.yml b/.github/workflows/pages-progress.yml new file mode 100644 index 0000000000..88c1f81d5a --- /dev/null +++ b/.github/workflows/pages-progress.yml @@ -0,0 +1,40 @@ +name: Pages progress + +on: + push: + branches: [main] + paths: + - core/libs/prx/** + - core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp + +permissions: + contents: read + actions: write + pages: read + +concurrency: + group: pages-progress + cancel-in-progress: true + +jobs: + progress: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Deploy Pages if the progress changed + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + python3 tools/progress.py "$RUNNER_TEMP/progress" + site=$(gh api "repos/$GH_REPO/pages" --jq .html_url) + if curl -fsSL "${site%/}/progress.json?$GITHUB_SHA" -o "$RUNNER_TEMP/deployed.json" && + cmp -s "$RUNNER_TEMP/deployed.json" "$RUNNER_TEMP/progress/progress.json"; then + echo "Progress unchanged, Pages not deployed" >> "$GITHUB_STEP_SUMMARY" + else + gh workflow run pages.yml --ref main + echo "Progress changed, Pages deployment dispatched" >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index d789d4de62..0dc28284b0 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -3,6 +3,18 @@ name: Pages on: push: branches: [main] + paths: + - .github/workflows/pages.yml + - CMakeLists.txt + - core/relinker/** + - demo/web/** + - tools/build_web.py + - tools/package_release.py + - tools/nid_names.py + - tools/progress.py + - tools/progress_history.py + - tools/rdna_isa.txt + - tools/test_web.py workflow_dispatch: release: types: [published] @@ -31,21 +43,10 @@ jobs: with: ref: main fetch-depth: 0 - - name: Build web converter - shell: bash - run: | - git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$RUNNER_TEMP/emsdk" - "$RUNNER_TEMP/emsdk/emsdk" install 6.0.3 - "$RUNNER_TEMP/emsdk/emsdk" activate 6.0.3 - source "$RUNNER_TEMP/emsdk/emsdk_env.sh" - emcmake cmake -S . -B build-wasm -DANYPS5_BUILD_WASM=ON -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release - cmake --build build-wasm --target relinker_web --parallel 2 - - name: Test web converter - run: python3 tools/test_web.py --build build-wasm - - name: Download latest release libraries + - name: Download matching release converter and libraries env: GITHUB_TOKEN: ${{ github.token }} - run: python3 tools/build_web.py --build build-wasm --libraries + run: python3 tools/build_web.py --build build-wasm --release - run: python3 tools/progress.py build-wasm/web - run: python3 tools/progress_history.py build-wasm/web - uses: actions/upload-pages-artifact@v5 diff --git a/.github/workflows/pr-intake.yml b/.github/workflows/pr-intake.yml index 6ae30f8b57..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 @@ -42,7 +63,10 @@ jobs: const openPRs = await github.paginate(github.rest.pulls.list, { owner, repo, state: 'open', base: 'main', per_page: 100 }); - const open = new Set(openPRs.filter((p) => p.user.login === pr.user.login).map((p) => p.number)); + const reopened = context.payload.action === 'reopened'; + const open = new Set(openPRs + .filter((p) => p.user.login === pr.user.login && (reopened || p.number < pull_number)) + .map((p) => p.number)); open.add(pull_number); if (open.size <= allowedOpen) { return; diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml new file mode 100644 index 0000000000..15a3e8e3dc --- /dev/null +++ b/.github/workflows/pr-labels.yml @@ -0,0 +1,35 @@ +name: PR labels + +on: + pull_request_target: + branches: [main] + types: [opened, edited, reopened, synchronize] + schedule: + - cron: '45 * * * *' + workflow_dispatch: + +permissions: {} + +concurrency: + group: pr-labels-${{ github.event.pull_request.number || 'all' }} + +jobs: + labels: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + pull-requests: write + issues: write + steps: + - uses: actions/checkout@v7 + - env: + GH_TOKEN: ${{ github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + if [ -n "$PR_NUMBER" ]; then + python3 -I tools/pr_labels.py --pr "$PR_NUMBER" + else + python3 -I tools/pr_labels.py --all + fi diff --git a/.github/workflows/pr-overlap.yml b/.github/workflows/pr-overlap.yml index f6d437eebd..40a008035f 100644 --- a/.github/workflows/pr-overlap.yml +++ b/.github/workflows/pr-overlap.yml @@ -38,11 +38,12 @@ jobs: record() { jq --arg pr "$1" --arg h "$2" '.[$pr] = $h' "$state" > "$state.new" && mv "$state.new" "$state"; } shopt -s nullglob marker='' + unpinned() { sed -E 's#/blob/[0-9a-f]{40}/#/blob/main/#g'; } prev= for file in "$out"/*.md; do [ -z "$prev" ] || record "$prev" "$hash" pr=$(basename "$file" .md) - hash=$(sha256sum "$file" | cut -d' ' -f1) + hash=$(unpinned < "$file" | sha256sum | cut -d' ' -f1) prev= [ "$(jq -r --arg pr "$pr" '.[$pr] // empty' "$state")" = "$hash" ] && continue prev=$pr @@ -50,7 +51,7 @@ jobs: --jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$marker\"))) | [.id, .body] | @json" | head -n1) id=$(jq -r '.[0] // empty' <<< "${comment:-null}") if [ -s "$file" ]; then - [ "$(jq -r '.[1]' <<< "${comment:-null}")" = "$(cat "$file")" ] && continue + [ "$(jq -r '.[1]' <<< "${comment:-null}" | unpinned)" = "$(unpinned < "$file")" ] && continue if [ -n "$id" ]; then gh api -X PATCH "repos/$GH_REPO/issues/comments/$id" -F body=@"$file" > /dev/null else diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1cc1d9aa9d..cff3998f8c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,17 @@ jobs: steps: - uses: actions/checkout@v7 with: + ref: ${{ github.sha }} submodules: recursive + - name: Point the WinLibs CMake at the CA bundle of Git for Windows + if: matrix.platform == 'windows' + shell: bash + run: | + for prefix in ucrt64 mingw64; do + bundle="C:/Program Files/Git/$prefix/etc/ssl/certs/ca-bundle.crt" + if [ -f "$bundle" ]; then echo "SSL_CERT_FILE=$bundle" >> "$GITHUB_ENV"; exit 0; fi + done + echo "::error::No CA bundle found in Git for Windows"; exit 1 - id: toolchain if: matrix.platform == 'windows' uses: actions/cache@v6 @@ -49,8 +59,6 @@ jobs: path: C:\winlibs key: ${{ env.WINLIBS_ZIP }} - if: matrix.platform == 'windows' && steps.toolchain.outputs.cache-hit != 'true' - env: - SSL_CERT_FILE: 'C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt' run: | curl -sSfL -o "$RUNNER_TEMP/winlibs.zip" "https://github.com/brechtsanders/winlibs_mingw/releases/download/$WINLIBS_RELEASE/$WINLIBS_ZIP" 7z x "$RUNNER_TEMP/winlibs.zip" -oC:/winlibs @@ -69,8 +77,6 @@ jobs: - name: Configure Windows if: matrix.platform == 'windows' shell: cmd - env: - SSL_CERT_FILE: 'C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt' run: cmake -S . -B build %CONFIGURE% - name: Build project and tests (Linux) if: matrix.platform == 'linux' @@ -81,7 +87,7 @@ jobs: run: cmake --build build - name: Run tests (Linux) if: matrix.platform == 'linux' - run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error + run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error -E '^precise_wait$' env: ANYPS5_REQUIRE_VULKAN: 1 GALLIUM_OVERRIDE_CPU_CAPS: avx @@ -105,9 +111,38 @@ jobs: if-no-files-found: error compression-level: 0 + web: + name: Build and test web converter + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} + - name: Build web converter + shell: bash + run: | + git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$RUNNER_TEMP/emsdk" + "$RUNNER_TEMP/emsdk/emsdk" install 6.0.3 + "$RUNNER_TEMP/emsdk/emsdk" activate 6.0.3 + source "$RUNNER_TEMP/emsdk/emsdk_env.sh" + emcmake cmake -S . -B build-wasm -DANYPS5_BUILD_WASM=ON -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release + cmake --build build-wasm --target relinker_web --parallel 2 + - name: Test web converter + run: python3 tools/test_web.py --build build-wasm + - name: Package web converter + shell: bash + run: python3 tools/package_release.py --platform web --build build-wasm --output dist --version "$RELEASE_TAG" --revision "$(git rev-parse HEAD)" + - uses: actions/upload-artifact@v7 + with: + name: release-web + path: dist/* + if-no-files-found: error + compression-level: 0 + publish: name: Publish release - needs: build + needs: [build, web] runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -117,6 +152,8 @@ jobs: GH_REPO: ${{ github.repository }} steps: - uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} - uses: actions/download-artifact@v8 with: pattern: release-* @@ -125,7 +162,7 @@ jobs: - name: Check release files and collect documentation shell: bash run: | - for file in "prx-windows-$RELEASE_TAG.zip" "prx-windows-$RELEASE_TAG.tar.gz" "prx-linux-$RELEASE_TAG.zip" "prx-linux-$RELEASE_TAG.tar.gz" "relinker-$RELEASE_TAG.exe" "relinker-$RELEASE_TAG"; do + for file in "prx-windows-$RELEASE_TAG.zip" "prx-windows-$RELEASE_TAG.tar.gz" "prx-linux-$RELEASE_TAG.zip" "prx-linux-$RELEASE_TAG.tar.gz" "relinker-$RELEASE_TAG.exe" "relinker-$RELEASE_TAG" "web-$RELEASE_TAG.zip"; do test -s "dist/$file" done python tools/package_release.py --docs docs/user --output dist diff --git a/.github/workflows/stale-conflicts.yml b/.github/workflows/stale-conflicts.yml index 4eee09a40e..00150e1b70 100644 --- a/.github/workflows/stale-conflicts.yml +++ b/.github/workflows/stale-conflicts.yml @@ -14,6 +14,14 @@ jobs: issues: write pull-requests: write steps: + - env: + GH_TOKEN: ${{ github.token }} + run: | + gh pr list -R "$GITHUB_REPOSITORY" --label stale -L 1000 --json number,labels \ + --jq '.[] | select(all(.labels[]; .name != "needs rebase")) | .number' | + while read -r number; do + gh api -X DELETE "repos/$GITHUB_REPOSITORY/issues/$number/labels/stale" --silent + done - uses: actions/stale@v11 with: only-pr-labels: needs rebase diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml new file mode 100644 index 0000000000..8de6e8d91f --- /dev/null +++ b/.github/workflows/web-tests.yml @@ -0,0 +1,39 @@ +name: Web tests + +on: + pull_request: + paths: + - .github/workflows/web-tests.yml + - demo/web/** + - tools/test_web.py + +permissions: + contents: read + +concurrency: + group: web-tests-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + output-directory: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - 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 + node --check demo/web/archive.mjs + node --check demo/web/batch.mjs + node --check demo/web/output-directory.mjs + node --check demo/web/worker.mjs + - name: Test browser converter helpers + run: node --test demo/web/tests/batch.test.mjs demo/web/tests/output-directory.test.mjs diff --git a/.github/workflows/windows-msys2.yml b/.github/workflows/windows-msys2.yml new file mode 100644 index 0000000000..def52370d2 --- /dev/null +++ b/.github/workflows/windows-msys2.yml @@ -0,0 +1,117 @@ +name: Windows MSYS2 + +on: + schedule: + - cron: '23 8 * * *' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: windows-msys2-${{ github.ref }} + cancel-in-progress: true + +env: + CONFIGURE: >- + -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON + -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ + -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + +jobs: + windows-msys2: + runs-on: windows-latest + timeout-minutes: 45 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} + persist-credentials: false + - 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: Use the CA bundle of Git for Windows + run: | + for prefix in ucrt64 mingw64; do + bundle="C:/Program Files/Git/$prefix/etc/ssl/certs/ca-bundle.crt" + if [ -f "$bundle" ]; then echo "SSL_CERT_FILE=$bundle" >> "$GITHUB_ENV"; exit 0; fi + done + echo "::error::No CA bundle found in Git for Windows"; exit 1 + - id: msys2 + uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: true + install: >- + mingw-w64-ucrt-x86_64-gcc + mingw-w64-ucrt-x86_64-cmake + mingw-w64-ucrt-x86_64-ninja + - shell: pwsh + env: + MSYS2_LOCATION: ${{ steps.msys2.outputs.msys2-location }} + run: Add-Content -LiteralPath $env:GITHUB_PATH -Value "$env:MSYS2_LOCATION\ucrt64\bin" + - uses: hendrikmuhs/ccache-action@v1.2.24 + with: + key: windows-msys2 + save: ${{ github.ref == 'refs/heads/main' }} + evict-old-files: job + - id: ffmpeg-cache + uses: actions/cache/restore@v6 + with: + path: build/externals + key: ffmpeg-${{ runner.os }}-${{ steps.submodules.outputs.ffmpeg }} + - shell: cmd + run: cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown" + - 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 + run: cmake --build build --target libs + - shell: cmd + working-directory: build + run: ctest --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: Report tests slower than 30 s + if: ${{ !cancelled() }} + run: | + [ -f build/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"] }' build/ctest.log + - uses: actions/upload-artifact@v7 + if: ${{ !cancelled() }} + with: + name: windows-msys2-tests + path: | + build/ctest.log + build/ctest.xml + if-no-files-found: ignore diff --git a/.gitmodules b/.gitmodules index efcc5aa70b..215534a440 100644 --- a/.gitmodules +++ b/.gitmodules @@ -31,6 +31,9 @@ [submodule "3rdparty/tracy"] path = 3rdparty/tracy url = https://github.com/wolfpld/tracy.git +[submodule "3rdparty/mimalloc"] + path = 3rdparty/mimalloc + url = https://github.com/microsoft/mimalloc.git [submodule "3rdparty/libjpeg-turbo"] path = 3rdparty/libjpeg-turbo url = https://github.com/libjpeg-turbo/libjpeg-turbo.git @@ -40,3 +43,18 @@ [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 +[submodule "3rdparty/vgmstream"] + path = 3rdparty/vgmstream + url = https://github.com/vgmstream/vgmstream.git +[submodule "3rdparty/ogg"] + path = 3rdparty/ogg + url = https://github.com/xiph/ogg.git +[submodule "3rdparty/vorbis"] + path = 3rdparty/vorbis + url = https://github.com/xiph/vorbis.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/mimalloc b/3rdparty/mimalloc new file mode 160000 index 0000000000..f8401befa6 --- /dev/null +++ b/3rdparty/mimalloc @@ -0,0 +1 @@ +Subproject commit f8401befa675adb13b98decababd7fdc59572477 diff --git a/3rdparty/ogg b/3rdparty/ogg new file mode 160000 index 0000000000..be05b13e98 --- /dev/null +++ b/3rdparty/ogg @@ -0,0 +1 @@ +Subproject commit be05b13e98b048f0b5a0f5fa8ce514d56db5f822 diff --git a/3rdparty/vgmstream b/3rdparty/vgmstream new file mode 160000 index 0000000000..7c520e8f9b --- /dev/null +++ b/3rdparty/vgmstream @@ -0,0 +1 @@ +Subproject commit 7c520e8f9b078dfde2f84f1b5bcbad0c6c415806 diff --git a/3rdparty/vorbis b/3rdparty/vorbis new file mode 160000 index 0000000000..0657aee69d --- /dev/null +++ b/3rdparty/vorbis @@ -0,0 +1 @@ +Subproject commit 0657aee69dec8508a0011f47f3b69d7538e9d262 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..7ded821d28 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,15 @@ +# 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. +- AGC, shader recompiler and VideoOut changes: run the same focused tests on `main` and the change with `ANYPS5_REQUIRE_VULKAN=1`, `ANYPS5_NO_SHADER_CACHE=1` and `VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation` using `ctest --test-dir build -R -V` from the same machine and GPU. Confirm neither log contains `was not found but was requested by env var VK_INSTANCE_LAYERS`, then compare the logs and confirm the change adds no new `VUID-` validation errors (`ctest` can report `Passed` when validation errors are logged). diff --git a/CMakeLists.txt b/CMakeLists.txt index a5ec80bd05..9eaa66c8db 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) @@ -33,6 +44,9 @@ if(BUILD_TESTING) get_filename_component(testCompilerDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY) foreach(runtimeName IN ITEMS libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll) if(NOT EXISTS "${testCompilerDirectory}/${runtimeName}") + if(ANYPS5_RELINKER_ONLY) + continue() + endif() message(FATAL_ERROR "Cannot locate MinGW runtime: ${runtimeName}") endif() file(COPY "${testCompilerDirectory}/${runtimeName}" DESTINATION "${ANYPS5_TEST_OUTPUT_DIRECTORY}") @@ -51,6 +65,18 @@ include(cmake/SystemDeps.cmake) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) +include(CheckCXXSourceCompiles) +function(anyps5_check_cxx_links_standard_library) + set(CMAKE_TRY_COMPILE_TARGET_TYPE EXECUTABLE) + check_cxx_source_compiles("#include \nint main(int argc, char**) { try { throw std::string(static_cast(argc), 'x'); } catch (const std::string& text) { return static_cast(text.size()) - argc; } }" ANYPS5_CXX_LINKS_STANDARD_LIBRARY) +endfunction() +anyps5_check_cxx_links_standard_library() +if(NOT ANYPS5_CXX_LINKS_STANDARD_LIBRARY) + message(FATAL_ERROR "The C++ compiler ${CMAKE_CXX_COMPILER} cannot link a program that uses the C++ standard library. " + "Set CMAKE_CXX_COMPILER to the C++ driver (g++ or clang++, not gcc or clang) in a new build directory. " + "Supported toolchains are listed in docs/dev/BUILD.md.") +endif() + if(ANYPS5_ENABLE_ASAN) if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") message(FATAL_ERROR "ANYPS5_ENABLE_ASAN is supported only on Linux") @@ -86,13 +112,38 @@ 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) + add_test_executable(pad_command_file_tests core/libs/prx/libScePad/tests/InputCommandFile.cpp) + target_include_directories(pad_command_file_tests PRIVATE core/libs) + add_test(NAME pad_command_file COMMAND pad_command_file_tests) + set_tests_properties(pad_command_file PROPERTIES TIMEOUT 20) if(Python3_Interpreter_FOUND) + add_test(NAME shader_cache_native_paths COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ShaderCacheVersion.cmake ${CMAKE_COMMAND}) + set_tests_properties(shader_cache_native_paths PROPERTIES TIMEOUT 20) + add_test(NAME shader_cache_runtime_sources COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_shader_cache_runtime_sources.py + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler ${CMAKE_COMMAND}) + set_tests_properties(shader_cache_runtime_sources PROPERTIES TIMEOUT 180) add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_encoding.py) add_test(NAME pr_overlap_dependencies COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_dependencies.py) add_test(NAME pr_overlap_tools COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap.py) + add_test(NAME pr_labels COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_labels.py) add_test(NAME progress_history COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_history.py) add_test(NAME progress_report COMMAND ${Python3_EXECUTABLE} @@ -107,16 +158,50 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_read_encoding.py) add_test(NAME import_audit COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py) - add_test(NAME conventions_function_bodies COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py) + add_test(NAME unwrap_self COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_unwrap_self.py) + 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}/12) + set_tests_properties(${conventions_test_name} PROPERTIES TIMEOUT 30) + endforeach() add_test(NAME conventions_wrapped_declarations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py) add_test(NAME conventions_ci_gate COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_ci_gate.py) + add_test(NAME conventions_git_errors COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_git.py) + add_test(NAME conventions_doc_links COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_doc_links.py) + add_test(NAME conventions_unknown_nids COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_unknown_nids.py) + add_test(NAME conventions_debt_length COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_debt_length.py) add_test(NAME test_registration COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_test_registration.py) + add_test(NAME test_script_registration COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_test_registration.py --scripts) + add_test(NAME build_eligibility COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_eligibility.py) add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE} ${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 ConfigureSeedTests) + add_test(NAME configure_seed_paths COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedPathTests) + add_test(NAME configure_seed_round_trips COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedRoundTripTests) + add_test(NAME configure_seed_parsing COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ChecksParsingTests) add_test(NAME stubgen COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen.py) add_test(NAME debt_split COMMAND ${Python3_EXECUTABLE} @@ -125,27 +210,57 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen_stdout_newlines.py) add_test(NAME hardware_oracle COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_hw_oracle.py) + add_test(NAME hardware_oracle_compare COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_compare_results.py) + set_tests_properties(hardware_oracle_compare PROPERTIES TIMEOUT 120) add_test(NAME pr_overlap_exports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_exports.py) add_test(NAME pr_overlap_wrapped_exports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_wrapped_exports.py) add_test(NAME pr_overlap_own_branch COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_own_branch.py) + add_test(NAME pr_overlap_errors COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_errors.py) add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py) add_test(NAME release_package COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_package_release.py) + add_test(NAME web_release COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_web_release.py) + set_tests_properties(web_release PROPERTIES TIMEOUT 20) + add_test(NAME web_library_assets COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_web.py) + set_tests_properties(web_library_assets PROPERTIES TIMEOUT 20) add_test(NAME pad_input COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_input.py) set_tests_properties(pad_input PROPERTIES TIMEOUT 20) + add_test(NAME pad_timeline COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_timeline.py) + set_tests_properties(pad_timeline PROPERTIES TIMEOUT 20) add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py) + add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_bom_shebang.py) + set_tests_properties(conventions_bom_shebang PROPERTIES TIMEOUT 20) + add_test(NAME env_reference COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_env_reference.py) + set_tests_properties(env_reference PROPERTIES TIMEOUT 20) + add_test(NAME conventions_env_reference COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_env_reference.py) + set_tests_properties(conventions_env_reference PROPERTIES TIMEOUT 60) add_test(NAME conventions_body_bom COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_body_bom.py) + add_test(NAME conventions_blob_reads COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_blob_reads.py) add_test(NAME conventions_agent_files COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_agent_files.py) + add_test(NAME conventions_pr_template COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_pr_template.py) add_test(NAME check_compile COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile.py) + add_test(NAME performance_compare COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_performance_compare.py) + set_tests_properties(performance_compare PROPERTIES TIMEOUT 20) endif() endif() @@ -231,6 +346,24 @@ 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) +if(WIN32) + set(MI_BUILD_SHARED ON CACHE BOOL "" FORCE) + set(MI_BUILD_STATIC OFF CACHE BOOL "" FORCE) + set(MI_BUILD_OBJECT OFF CACHE BOOL "" FORCE) + set(MI_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(MI_OVERRIDE ON CACHE BOOL "" FORCE) + set(MI_WIN_REDIRECT ON CACHE BOOL "" FORCE) + set(MI_FREE_IS_CHECKED SHARED CACHE STRING "" FORCE) + add_subdirectory(3rdparty/mimalloc EXCLUDE_FROM_ALL) + if(MINGW) + target_compile_definitions(mimalloc PRIVATE MI_MINGW_UCRT64=1) + target_link_options(mimalloc PRIVATE -static + LINKER:--wrap=malloc,--wrap=calloc,--wrap=realloc,--wrap=free,--wrap=strdup,--wrap=_strdup) + target_sources(mimalloc PRIVATE ${CMAKE_SOURCE_DIR}/core/libs/allocator/RuntimeHeap.c) + endif() +endif() + add_subdirectory(core/shader/recompiler) add_subdirectory(core/Decoder/Jpeg) add_subdirectory(core/Decoder/Png) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b83c070c09..01783ee784 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,10 +5,11 @@ - Follow the [coding conventions](docs/dev/CONVENTIONS.md): naming, no comments except [technical debt](docs/dev/TechnicalDebt.md), Conventional Commits. - Every function either does exactly what it is supposed to or throws. Unimplemented exports call `NotImplemented_nid_no_patch(__func__)` (see [libSceAudioIn](core/libs/prx/libSceAudioIn/Export.cpp)). - Pull requests that add or change shader instruction semantics say where they come from: measured on hardware (which GPU and what was checked, e.g. with the [hardware oracle](docs/dev/HW_ORACLE.md)) or the exact source (ISA section, LLVM, ACO or Mesa file). Reviewers check the semantics on hardware. Cases the source doesn't settle throw, and behaviour not verified on hardware is recorded in [technical debt](docs/dev/TechnicalDebt.md). +- An export's signature or behaviour taken from a title's call sites or from a library's disassembly follows [reading titles and libraries](docs/dev/REVERSE_ENGINEERING.md): a call site settles the arguments, not what the library does. - A silent stub is allowed only when it unblocks a title and only affects the UI; add it under Silent stubs in its library's [technical debt](docs/dev/debt/) file. - Missing imports fail at startup with a clear error; don't replace them with fallbacks that keep running. - Implement the general behaviour of a function, not what one title happens to need. -- Don't add replacements for modules that titles ship themselves in `sce_module/`, `sce_modules/` or `prx/`: engine or middleware modules (Cohtml, FMOD, GOG Galaxy) and SDK libraries that only wrap other system libraries (NpCppWebApi over NpWebApi2). The relinker converts and loads the title's own module, and a host library with the same name is left out of `DT_NEEDED`. Only system libraries, which reach the kernel or the hardware, are reimplemented in [core/libs/prx](core/libs/prx). +- Don't add replacements for modules that titles ship themselves in `sce_module/`, `sce_modules/` or `prx/`: engine or middleware modules (Cohtml, FMOD, GOG Galaxy) and SDK libraries that only wrap other system libraries. The relinker converts and loads the title's own module, and a host library with the same name is left out of `DT_NEEDED`. Only system libraries, which reach the kernel or the hardware, are reimplemented in [core/libs/prx](core/libs/prx). A host copy may still exist for a wrapped library that some dumps lack entirely, like libSceNpCppWebApi: the title's own module wins whenever one is shipped, so it is only ever consulted by titles that ship nothing. - Avoid non-standard extensions (`__attribute__`, etc.) where standard C++ is enough. Helper symbols that must not become NIDs use the `_nid_no_patch` or `_nid_no_patch_cut` suffix. - The relinker uses only the C++20 standard library. - Third-party code is added as a submodule under `3rdparty/` and built from source, not found on the system. With `ANYPS5_USE_SYSTEM_DEPS=ON` the packages packaged by the distributions (SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools) come from the system instead; that resolution lives in [cmake/SystemDeps.cmake](cmake/SystemDeps.cmake) and the default build never reads the system. @@ -37,9 +38,11 @@ For pull requests, full Linux and Windows builds and tests run only when a colla gh workflow run build.yml --repo boykopovar/AnyPS5 --ref main --field pr=1234 ``` -This manual run also builds and tests the relinker on Linux, Windows and macOS, regardless of the author's previous contributions or the changed files. The relinker matrix also runs automatically when a pull request targeting `main` is opened, updated or reopened, if its author already has a commit in `main` and it changes a file the relinker-only build uses; full builds never run automatically for pull requests. Every job that runs automatically for pull requests runs only when the pull request changes its files: a `paths` filter on the `pull_request` trigger, or `needs: eligibility` with an `if` on the files output it sets in `build.yml`. The Conventions check fails a workflow change that adds a job without one. All build jobs build the selected pull request merged into the current `main` (GitHub's merge commit), so a pull request that no longer merges cleanly must be rebased first. New commits require another manual run for full builds. +This manual run also builds and tests the relinker on Linux, Windows and macOS when the pull request changes a file the relinker-only build uses, regardless of the author's previous contributions. The relinker matrix also runs automatically when a pull request targeting `main` is opened, updated or reopened, if its author already has a commit in `main` and it changes a file the relinker-only build uses; full builds never run automatically for pull requests. Every job that runs automatically for pull requests runs only when the pull request changes its files: a `paths` filter on the `pull_request` trigger, or `needs: eligibility` with an `if` on the files output it sets in `build.yml`. The Conventions check fails a workflow change that adds a job without one. All build jobs build the selected pull request merged into the current `main` (GitHub's merge commit), so a pull request that no longer merges cleanly must be rebased first. New commits require another manual run for full builds. -The Conventions check runs automatically for every contributor, including first-time contributors. It uses the base branch checker and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [technical debt](docs/dev/TechnicalDebt.md), the index or a file in `docs/dev/debt/`, and only UTF-8 text files. Run it locally before pushing: +With `--field full=false`, the manual run skips the full builds and runs only the relinker matrix on Linux, Windows and macOS, if the pull request changes a file the relinker-only build uses, such as the Python tools and their tests in `tools/`. + +The Conventions check runs automatically for every contributor, including first-time contributors. It uses the checker from the current tip of the base branch, so rules added after a pull request was opened apply to its next run, and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [technical debt](docs/dev/TechnicalDebt.md), the index or a file in `docs/dev/debt/`, and only UTF-8 text files. Run it locally before pushing: ``` python3 tools/check_conventions.py --base origin/main @@ -55,15 +58,17 @@ 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 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. +- 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. Such a pull request gets the `unblocks title` label. +- 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). -- A pull request that claims a change in speed shows the summary lines of `main` and of the change, from the same machine and scene, and attaches the traces, captured as the [Performance report](.github/ISSUE_TEMPLATE/performance.yml) issue form describes. A slow scene is reported with the same form. -- Changes to the AGC driver, the shader recompiler or VideoOut name each GPU the GPU tests ran on (vendor, model, driver, OS) on the `GPUs tested` checklist line. CI runs these tests only on lavapipe, and hardware drivers fail where lavapipe and other vendors pass, so a reviewer can ask for a run on another vendor before merging. +- A pull request that claims a change in speed shows the summary lines of `main` and of the change, from the same machine and scene, and attaches the traces, captured as the [Performance report](.github/ISSUE_TEMPLATE/performance.yml) issue form describes. A slow scene is reported with the same form. A pull request titled `perf(...)` or `perf:` is labeled `perf`, and `perf numbers missing` until its description holds two valid summary lines with different commits. +- Changes to the AGC driver, the shader recompiler or VideoOut run the same focused tests on `main` and the change with `VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation`, `ANYPS5_NO_SHADER_CACHE=1` and `ANYPS5_REQUIRE_VULKAN=1` using `ctest -V` from the same machine and GPU, confirm neither log reports `was not found but was requested by env var VK_INSTANCE_LAYERS`, confirm the change introduces no new `VUID-` validation errors compared with `main`, and name each GPU the GPU tests ran on (vendor, model, driver, OS) on the `GPUs tested` checklist line. CI runs these tests only on lavapipe, and hardware drivers fail where lavapipe and other vendors pass, so a reviewer can ask for a run on another vendor before merging. - Investigation notes, reports, screenshots and logs go in the pull request, not in the repository. Images for documentation go in the [gist](https://gist.github.com/boykopovar/0e53f2e1426f29ecd41e3b51540b8a90) comments. - Say whether the change was written with AI assistance. The author of the pull request is responsible for every line of it. ## Documentation -Text in the repository is dry, strict and concise. Record known gaps, unknown NIDs and signatures in [technical debt](docs/dev/TechnicalDebt.md) instead of new files. An entry goes in `docs/dev/debt/.md`, named after the first `core/` path it links (`` for `core/libs/prx//...`, otherwise for `core//...`), under the same `###` sections as the index; entries that link neither stay in the index. A pull request whose `TechnicalDebt.md` change conflicts after the split rebases and runs `python3 tools/debt_split.py resolve` during the stopped rebase, which moves its added, edited and removed entries to their files; `python3 tools/debt_split.py split` moves entries left in the index. +Text in the repository is dry, strict and concise. Record known gaps, unknown NIDs and signatures in [technical debt](docs/dev/TechnicalDebt.md) instead of new files. An entry states the gap first (what differs from the console, what is unverified, what throws) and describes the implemented behaviour only as far as the gap needs it. A pull request that closes a gap removes its entry, or cuts it to what is still open, instead of rewriting it to describe the corrected behaviour. An entry goes in `docs/dev/debt/.md`, named after the first `core/` path it links (`` for `core/libs/prx//...`, otherwise for `core//...`), under the same `###` sections as the index; entries that link neither stay in the index. A pull request whose `TechnicalDebt.md` change conflicts after the split rebases and runs `python3 tools/debt_split.py resolve` during the stopped rebase, which moves its added, edited and removed entries to their files; `python3 tools/debt_split.py split` moves entries left in the index. diff --git a/README.md b/README.md index b07e1abc67..3e7fc3ccf4 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,15 @@ Tool for automatic executables porting to Linux and Windows. +The relinker-only build runs on macOS, including Apple silicon. + [**Web converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device. -![anyps5_collage1](https://github.com/user-attachments/assets/0f8e13e8-1cb7-4c69-8075-39978e138994) - -[Discord](https://discord.gg/BHFztBPUe) +![anyps5_collage1](https://github.com/user-attachments/assets/ed2f0b2d-33d3-46d7-ab88-767ca872fc99) Includes a [relinker](core/relinker) that converts executable to the target system's native format and implementations of [system prx libraries](core/libs/prx) suitable for dynamic linking. No emulation or separate runtime process. -[Usage](docs/user/USAGE.md), [Build instructions](docs/dev/BUILD.md), [Architecture](docs/dev/ARCHITECTURE.md), [Technical debt of the project](docs/dev/TechnicalDebt.md), [code style conventions](docs/dev/CONVENTIONS.md), [contributing](CONTRIBUTING.md) +[**Discord**](https://discord.gg/BHFztBPUe), [Usage](docs/user/USAGE.md), [Build instructions](docs/dev/BUILD.md), [Architecture](docs/dev/ARCHITECTURE.md), [Technical debt of the project](docs/dev/TechnicalDebt.md), [code style conventions](docs/dev/CONVENTIONS.md), [contributing](CONTRIBUTING.md) ## Status @@ -38,6 +38,8 @@ SDL-mapped game controllers are supported, including analog sticks and triggers. This project is intended for interoperability, research, preservation, and compatibility purposes. It does not include, distribute, or require copyrighted software, firmware, cryptographic keys, or proprietary libraries. Users are responsible for ensuring that any binaries used with this project are obtained and used in accordance with applicable laws and their respective license terms. +AnyPS5 is an independent project and is not affiliated with, authorized or endorsed by Sony Interactive Entertainment. "PlayStation" and "PS5" are trademarks of Sony Interactive Entertainment Inc. + ## License This project is licensed under the GNU General Public License version 2 only. 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/Decoder/Jpeg/include/Decoder/Jpeg.hpp b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp index 13c1d8accc..62d09b0698 100644 --- a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp +++ b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp @@ -19,6 +19,10 @@ struct Header { std::uint32_t width; std::uint32_t height; std::uint32_t channels; + std::uint8_t componentIds[4]{}; + std::uint8_t samplingFactors[4]{}; + bool progressive = false; + bool app0BeforeFrame = false; }; enum class Sampling { @@ -35,6 +39,10 @@ std::optional
ParseHeader(std::span jpeg); std::optional Decode(std::span jpeg); +std::uint32_t ScaledDimension(std::uint32_t dimension, std::uint32_t denominator); + +std::optional DecodeScaled(std::span jpeg, std::uint32_t denominator); + } // namespace Decoder::Jpeg #endif // DECODER_JPEG_HPP diff --git a/core/Decoder/Jpeg/src/Jpeg.cpp b/core/Decoder/Jpeg/src/Jpeg.cpp index d446841cc4..96ace625a4 100644 --- a/core/Decoder/Jpeg/src/Jpeg.cpp +++ b/core/Decoder/Jpeg/src/Jpeg.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #define STB_IMAGE_STATIC @@ -19,8 +20,8 @@ constexpr std::uint32_t MAX_DIMENSION = 0xFFFF; class Handle { public: - Handle() : handle(tj3Init(TJINIT_COMPRESS)) { - if (!handle) throw std::runtime_error("Jpeg::Encode: encoder initialization failed"); + explicit Handle(int type) : handle(tj3Init(type)) { + if (!handle) throw std::runtime_error(type == TJINIT_COMPRESS ? "Jpeg::Encode: encoder initialization failed" : "Jpeg::DecodeScaled: decoder initialization failed"); } ~Handle() { tj3Destroy(handle); } Handle(const Handle&) = delete; @@ -63,7 +64,7 @@ std::vector Encode(std::span pixels, std::uint throw std::invalid_argument("Jpeg::Encode: invalid restart interval"); } const int subsampling = toSubsampling(sampling); - Handle handle; + Handle handle(TJINIT_COMPRESS); if (tj3Set(handle, TJPARAM_QUALITY, quality) < 0 || tj3Set(handle, TJPARAM_SUBSAMP, channels == 1 ? TJSAMP_GRAY : subsampling) < 0) { throw std::runtime_error("Jpeg::Encode: encoder configuration failed"); } @@ -80,6 +81,43 @@ std::vector Encode(std::span pixels, std::uint return {buffer.data, buffer.data + size}; } +std::uint32_t ScaledDimension(std::uint32_t dimension, std::uint32_t denominator) { + if (denominator != 1 && denominator != 2 && denominator != 4 && denominator != 8) { + throw std::invalid_argument("Jpeg::ScaledDimension: the scale must be 1/1, 1/2, 1/4 or 1/8"); + } + const tjscalingfactor factor{1, static_cast(denominator)}; + return static_cast(TJSCALED(static_cast(dimension), factor)); +} + +std::optional DecodeScaled(std::span jpeg, std::uint32_t denominator) { + if (denominator != 1 && denominator != 2 && denominator != 4 && denominator != 8) { + throw std::invalid_argument("Jpeg::DecodeScaled: the scale must be 1/1, 1/2, 1/4 or 1/8"); + } + if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt; + + Handle handle(TJINIT_DECOMPRESS); + if (tj3Set(handle, TJPARAM_STOPONWARNING, 1) < 0) throw std::runtime_error("Jpeg::DecodeScaled: decoder configuration failed"); + if (tj3DecompressHeader(handle, jpeg.data(), jpeg.size()) < 0) return std::nullopt; + const int colorspace = tj3Get(handle, TJPARAM_COLORSPACE); + if (colorspace == TJCS_CMYK || colorspace == TJCS_YCCK) { + throw std::runtime_error("Jpeg::DecodeScaled: scaled decoding of a CMYK or YCCK image is not implemented"); + } + const int width = tj3Get(handle, TJPARAM_JPEGWIDTH); + const int height = tj3Get(handle, TJPARAM_JPEGHEIGHT); + if (width <= 0 || height <= 0) return std::nullopt; + if (tj3SetScalingFactor(handle, tjscalingfactor{1, static_cast(denominator)}) < 0) { + throw std::runtime_error("Jpeg::DecodeScaled: the decoder refused the scaling factor"); + } + + const std::uint32_t channels = colorspace == TJCS_GRAY ? 1 : 3; + Image image{ScaledDimension(static_cast(width), denominator), ScaledDimension(static_cast(height), denominator), channels, {}}; + image.pixels.resize(static_cast(image.width) * image.height * channels); + if (tj3Decompress8(handle, jpeg.data(), jpeg.size(), image.pixels.data(), 0, channels == 1 ? TJPF_GRAY : TJPF_RGB) < 0) { + return std::nullopt; + } + return image; +} + std::optional
ParseHeader(std::span jpeg) { if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt; @@ -88,7 +126,33 @@ std::optional
ParseHeader(std::span jpeg) { int channels = 0; if (!stbi_info_from_memory(jpeg.data(), static_cast(jpeg.size()), &width, &height, &channels)) return std::nullopt; if (width <= 0 || height <= 0 || channels <= 0) return std::nullopt; - return Header{static_cast(width), static_cast(height), static_cast(channels)}; + Header header{static_cast(width), static_cast(height), static_cast(channels)}; + std::size_t offset = 2; + while (offset < jpeg.size()) { + if (jpeg[offset++] != 0xFF) return std::nullopt; + while (offset < jpeg.size() && jpeg[offset] == 0xFF) ++offset; + if (offset == jpeg.size()) return std::nullopt; + const std::uint8_t marker = jpeg[offset++]; + if (marker == 0xD9 || marker == 0xDA) return std::nullopt; + if (marker == 0x01 || (marker >= 0xD0 && marker <= 0xD8)) continue; + if (jpeg.size() - offset < 2) return std::nullopt; + const std::size_t length = (static_cast(jpeg[offset]) << 8) | jpeg[offset + 1]; + if (length < 2 || length > jpeg.size() - offset) return std::nullopt; + if (marker == 0xE0) header.app0BeforeFrame = true; + if (marker == 0xC0 || marker == 0xC1 || marker == 0xC2) { + if (length < 8) return std::nullopt; + header.channels = jpeg[offset + 7]; + if (header.channels == 0 || header.channels > 4 || length != 8 + 3 * header.channels) return std::nullopt; + header.progressive = marker == 0xC2; + for (std::uint32_t i = 0; i < header.channels; ++i) { + header.componentIds[i] = jpeg[offset + 8 + 3 * i]; + header.samplingFactors[i] = jpeg[offset + 9 + 3 * i]; + } + return header; + } + offset += length; + } + return std::nullopt; } std::optional Decode(std::span jpeg) { @@ -97,12 +161,12 @@ std::optional Decode(std::span jpeg) { int width = 0; int height = 0; int channels = 0; - stbi_uc* decoded = stbi_load_from_memory(jpeg.data(), static_cast(jpeg.size()), &width, &height, &channels, 0); + const std::unique_ptr decoded( + stbi_load_from_memory(jpeg.data(), static_cast(jpeg.size()), &width, &height, &channels, 0), stbi_image_free); if (!decoded) return std::nullopt; Image image{static_cast(width), static_cast(height), static_cast(channels), {}}; - image.pixels.assign(decoded, decoded + static_cast(image.width) * image.height * image.channels); - stbi_image_free(decoded); + image.pixels.assign(decoded.get(), decoded.get() + static_cast(image.width) * image.height * image.channels); return image; } diff --git a/core/Decoder/Jpeg/tests/Jpeg.cpp b/core/Decoder/Jpeg/tests/Jpeg.cpp index f5f9c309ab..e19be53028 100644 --- a/core/Decoder/Jpeg/tests/Jpeg.cpp +++ b/core/Decoder/Jpeg/tests/Jpeg.cpp @@ -1,5 +1,7 @@ #include "Decoder/Jpeg.hpp" +#include +#include #include #include #include @@ -108,5 +110,71 @@ int main() { const std::vector garbage{1, 2, 3, 4, 5, 6, 7, 8}; Require(!Decoder::Jpeg::Decode(garbage).has_value()); + Require(Decoder::Jpeg::ScaledDimension(7, 1) == 7 && Decoder::Jpeg::ScaledDimension(7, 2) == 4); + Require(Decoder::Jpeg::ScaledDimension(7, 4) == 2 && Decoder::Jpeg::ScaledDimension(7, 8) == 1); + Require(Decoder::Jpeg::ScaledDimension(32, 8) == 4 && Decoder::Jpeg::ScaledDimension(33, 8) == 5 && Decoder::Jpeg::ScaledDimension(1, 8) == 1); + Require(Decoder::Jpeg::ScaledDimension(0xFFFF, 2) == 0x8000); + for (const std::uint32_t denominator : {0u, 3u, 16u}) { + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::ScaledDimension(7, denominator); })); + Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::DecodeScaled(rgbJpeg, denominator); })); + } + + const auto boxError = [](const std::vector& source, std::uint32_t sourceWidth, std::uint32_t sourceHeight, + std::uint32_t channels, const Decoder::Jpeg::Image& scaled, std::uint32_t denominator) { + long error = 0; + for (std::uint32_t y = 0; y < scaled.height; ++y) { + for (std::uint32_t x = 0; x < scaled.width; ++x) { + for (std::uint32_t c = 0; c < channels; ++c) { + long sum = 0; + long count = 0; + for (std::uint32_t sy = y * denominator; sy < std::min(sourceHeight, (y + 1) * denominator); ++sy) { + for (std::uint32_t sx = x * denominator; sx < std::min(sourceWidth, (x + 1) * denominator); ++sx) { + sum += source[(static_cast(sy) * sourceWidth + sx) * channels + c]; + ++count; + } + } + const auto average = static_cast((sum + count / 2) / count); + error += Difference(average, scaled.pixels[(static_cast(y) * scaled.width + x) * channels + c]); + } + } + } + return error / static_cast(static_cast(scaled.width) * scaled.height * channels); + }; + + for (const std::uint32_t denominator : {1u, 2u, 4u, 8u}) { + const auto scaled = Decoder::Jpeg::DecodeScaled(rgbJpeg, denominator); + Require(scaled.has_value() && scaled->channels == 3); + Require(scaled->width == width / denominator && scaled->height == height / denominator); + Require(scaled->pixels.size() == static_cast(scaled->width) * scaled->height * 3); + Require(boxError(rgb, width, height, 3, *scaled, denominator) < 4); + + const auto grayScaled = Decoder::Jpeg::DecodeScaled(grayJpeg, denominator); + Require(grayScaled.has_value() && grayScaled->channels == 1); + Require(grayScaled->width == width / denominator && grayScaled->height == height / denominator); + Require(boxError(gray, width, height, 1, *grayScaled, denominator) < 4); + } + + std::vector odd(31 * 23 * 3); + for (std::uint32_t y = 0; y < 23; ++y) { + for (std::uint32_t x = 0; x < 31; ++x) { + std::uint8_t* pixel = &odd[(y * 31 + x) * 3]; + pixel[0] = static_cast(x * 8); + pixel[1] = static_cast(y * 11); + pixel[2] = static_cast(200 - x * 2); + } + } + const auto oddJpeg = Decoder::Jpeg::Encode(odd, 31, 23, 3, 95, Decoder::Jpeg::Sampling::Yuv420); + for (const std::uint32_t denominator : {2u, 4u, 8u}) { + const auto scaled = Decoder::Jpeg::DecodeScaled(oddJpeg, denominator); + Require(scaled.has_value() && scaled->channels == 3); + Require(scaled->width == Decoder::Jpeg::ScaledDimension(31, denominator) && scaled->height == Decoder::Jpeg::ScaledDimension(23, denominator)); + Require(boxError(odd, 31, 23, 3, *scaled, denominator) < 8); + } + + Require(!Decoder::Jpeg::DecodeScaled({}, 2).has_value()); + Require(!Decoder::Jpeg::DecodeScaled(garbage, 2).has_value()); + const std::vector truncated(rgbJpeg.begin(), rgbJpeg.begin() + static_cast(rgbJpeg.size() / 2)); + Require(!Decoder::Jpeg::DecodeScaled(truncated, 2).has_value()); + return 0; } diff --git a/core/Decoder/Png/src/Png.cpp b/core/Decoder/Png/src/Png.cpp index cb04f25bd5..434a589d6c 100644 --- a/core/Decoder/Png/src/Png.cpp +++ b/core/Decoder/Png/src/Png.cpp @@ -152,12 +152,12 @@ std::optional Decode(std::span png) { int width = 0; int height = 0; int channels = 0; - stbi_uc* decoded = stbi_load_from_memory(png.data(), static_cast(png.size()), &width, &height, &channels, 4); + const std::unique_ptr decoded( + stbi_load_from_memory(png.data(), static_cast(png.size()), &width, &height, &channels, 4), stbi_image_free); if (!decoded) return std::nullopt; Image image{static_cast(width), static_cast(height), {}}; - image.pixels.assign(decoded, decoded + static_cast(image.width) * image.height * 4); - stbi_image_free(decoded); + image.pixels.assign(decoded.get(), decoded.get() + static_cast(image.width) * image.height * 4); return image; } diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 2b49528bdd..34640dd56d 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() @@ -37,9 +39,16 @@ if(MINGW) check_cxx_source_compiles("#ifdef __USING_SJLJ_EXCEPTIONS__\n#error\n#endif\nint main() { return 0; }" ANYPS5_MINGW_DWARF_EXCEPTIONS) unset(CMAKE_REQUIRED_FLAGS) if(NOT ANYPS5_MINGW_DWARF_EXCEPTIONS) - message(FATAL_ERROR "${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION} selects SjLj exceptions with -fno-asynchronous-unwind-tables; the libraries need DWARF unwinding. Use the MinGW-w64 toolchain listed in docs/dev/BUILD.md.") + check_cxx_source_compiles("#if !defined(__SEH__) || defined(__USING_SJLJ_EXCEPTIONS__)\n#error\n#endif\n[[gnu::sysv_abi, gnu::noinline]] void Throw() { throw 1; }\n[[gnu::sysv_abi, gnu::noinline]] int Catch() { try { Throw(); } catch (int value) { return value; } return 0; }\nint main() { return Catch() != 1; }" ANYPS5_MINGW_SYSV_SEH_EXCEPTIONS) + if(NOT ANYPS5_MINGW_SYSV_SEH_EXCEPTIONS) + message(FATAL_ERROR "The Windows libraries require DWARF exceptions or GCC with System V ABI SEH support. Use the toolchains listed in docs/dev/BUILD.md.") + endif() + add_library(windows_seh_personalities OBJECT ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/src/specifics/windows/SehPersonalities.cpp) + target_link_options(windows_seh_personalities INTERFACE LINKER:--undefined=LibcSehPersonalities_nid_no_patch) + link_libraries(windows_seh_personalities) + else() + add_compile_options(-fno-asynchronous-unwind-tables) endif() - add_compile_options(-fno-asynchronous-unwind-tables) endif() function(configure_windows_unwind target) @@ -48,7 +57,7 @@ function(configure_windows_unwind target) message(FATAL_ERROR "Windows DWARF unwinding requires objcopy") endif() add_custom_command(TARGET ${target} POST_BUILD - COMMAND "${CMAKE_OBJCOPY}" --rename-section .eh_frame=.ehfram "$" + COMMAND "${CMAKE_OBJCOPY}" --rename-section .eh_frame=.ehfram --rename-section .eh_fram=.ehfram "$" VERBATIM ) endif() @@ -58,6 +67,21 @@ if(MINGW) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") endforeach() + + if(NOT EXISTS "${CMAKE_RANLIB}") + message(FATAL_ERROR "Stable import libraries require ranlib") + endif() + list(LENGTH CMAKE_CXX_CREATE_SHARED_LIBRARY linkCommandCount) + string(FIND "${CMAKE_CXX_CREATE_SHARED_LIBRARY}" "-Wl,--out-implib, " implibOption) + if(NOT linkCommandCount EQUAL 1 OR implibOption EQUAL -1) + message(FATAL_ERROR "Expected one shared library link command that writes -Wl,--out-implib,: ${CMAKE_CXX_CREATE_SHARED_LIBRARY}") + endif() + string(REPLACE "-Wl,--out-implib, " "-Wl,--out-implib,.new " linkCommand "${CMAKE_CXX_CREATE_SHARED_LIBRARY}") + set(CMAKE_CXX_CREATE_SHARED_LIBRARY + "\"${CMAKE_COMMAND}\" -E env SOURCE_DATE_EPOCH=0 ${linkCommand}" + "\"${CMAKE_RANLIB}\" -D .new" + "\"${CMAKE_COMMAND}\" -E copy_if_different .new " + ) endif() set(LIBC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc) @@ -65,6 +89,9 @@ add_library(libc SHARED EXCLUDE_FROM_ALL) set(LIBS_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) add_subdirectory(${LIBC_DIR} ${CMAKE_CURRENT_BINARY_DIR}/prx/libc) target_include_directories(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +if(WIN32) + target_compile_options(libc PRIVATE "SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/include/HostStandardStreams.hpp") +endif() configure_windows_unwind(libc) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(libc PRIVATE LINKER:-z,now) @@ -87,6 +114,14 @@ set_target_properties(libc PROPERTIES set(LIBC_UNPATCHED ${UNPATCHED_DIR}/libc.prx) set(LIBC_PATCHED ${PATCHED_DIR}/libc.prx) +set(LIBC_EXPORT_LIST ${CMAKE_CURRENT_BINARY_DIR}/libc.exports) + +add_custom_command( + OUTPUT ${LIBC_EXPORT_LIST} + COMMAND $ --write-export-list ${LIBC_UNPATCHED} ${LIBC_EXPORT_LIST} + DEPENDS nid_patcher libc ${LIBC_UNPATCHED} + COMMENT "Listing libc exports" +) add_custom_command( OUTPUT ${LIBC_PATCHED} @@ -114,6 +149,9 @@ foreach(LIB_DIR ${LIBS_DIRS}) configure_windows_unwind(${LIB_NAME}) endif() + if(WIN32) + target_compile_options(${LIB_NAME} PRIVATE "SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/include/HostStandardStreams.hpp") + endif() if(LIB_NAME STREQUAL "libkernel") target_link_libraries(${LIB_NAME} PUBLIC libc) else() @@ -146,8 +184,8 @@ foreach(LIB_DIR ${LIBS_DIRS}) add_custom_command( OUTPUT ${PATCHED_FILE} COMMAND ${CMAKE_COMMAND} -E copy ${UNPATCHED_FILE} ${PATCHED_FILE} - COMMAND $ ${LIB_NAME} --preserve-exports "${LIBC_UNPATCHED}" "${PATCHED_FILE}" - DEPENDS nid_patcher ${LIB_NAME} ${UNPATCHED_FILE} ${LIBC_UNPATCHED} + COMMAND $ ${LIB_NAME} --preserve-export-list "${LIBC_EXPORT_LIST}" "${PATCHED_FILE}" + DEPENDS nid_patcher ${LIB_NAME} ${UNPATCHED_FILE} ${LIBC_EXPORT_LIST} COMMENT "Patching NIDs for ${LIB_NAME}" ) @@ -155,6 +193,8 @@ foreach(LIB_DIR ${LIBS_DIRS}) endif() endforeach() +target_link_libraries(libSceUsbStorageDialog PRIVATE libSceUserService) + if(APS5_ENABLE_TRACY) set_target_properties(TracyClient PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${UNPATCHED_DIR} @@ -173,6 +213,29 @@ if(APS5_ENABLE_TRACY) list(APPEND PATCHED_OUTPUTS ${TRACY_CLIENT_COPIED}) endif() +if(TARGET mimalloc) + add_executable(pe_import_first ${CMAKE_CURRENT_SOURCE_DIR}/allocator/PeImportFirstMain.cpp ${CMAKE_CURRENT_SOURCE_DIR}/allocator/PeImportFirst.cpp) + target_compile_features(pe_import_first PRIVATE cxx_std_20) + if(MINGW) + target_compile_options(pe_import_first PRIVATE -fasynchronous-unwind-tables) + target_link_options(pe_import_first PRIVATE -municode -static -static-libgcc -static-libstdc++) + endif() + add_custom_command( + OUTPUT ${PATCHED_DIR}/mimalloc.dll + COMMAND ${CMAKE_COMMAND} -E copy $ ${PATCHED_DIR}/mimalloc.dll + COMMAND $ ${PATCHED_DIR}/mimalloc.dll mimalloc-redirect.dll + DEPENDS mimalloc pe_import_first + COMMENT "Copying mimalloc.dll beside the libraries, its redirection module imported first" + ) + add_custom_command( + OUTPUT ${PATCHED_DIR}/mimalloc-redirect.dll + COMMAND ${CMAKE_COMMAND} -E copy $/mimalloc-redirect.dll ${PATCHED_DIR}/mimalloc-redirect.dll + DEPENDS mimalloc + COMMENT "Copying mimalloc-redirect.dll beside the libraries" + ) + list(APPEND PATCHED_OUTPUTS ${PATCHED_DIR}/mimalloc.dll ${PATCHED_DIR}/mimalloc-redirect.dll) +endif() + add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) @@ -185,6 +248,15 @@ if(BUILD_TESTING) set_tests_properties(guest_posix_scalar_io PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_kernel_sys_sdk_version_tests tests/GuestKernelSysSdkVersion.cpp) + target_include_directories(guest_kernel_sys_sdk_version_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_sdk_version_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_sdk_version_tests) + add_test(NAME guest_kernel_sys_sdk_version COMMAND guest_kernel_sys_sdk_version_tests) + if(WIN32) + set_tests_properties(guest_kernel_sys_sdk_version PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(nid_buffer_validation_tests nid/tests/BufferValidation.cpp) target_include_directories(nid_buffer_validation_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/include) target_compile_features(nid_buffer_validation_tests PRIVATE cxx_std_20) @@ -194,10 +266,21 @@ if(BUILD_TESTING) add_test(NAME nid_buffer_validation COMMAND nid_buffer_validation_tests) set_tests_properties(nid_buffer_validation PROPERTIES TIMEOUT 10) + add_test_executable(pe_import_first_tests allocator/tests/PeImportFirstTests.cpp allocator/PeImportFirst.cpp) + target_include_directories(pe_import_first_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_compile_features(pe_import_first_tests PRIVATE cxx_std_20) + if(MINGW) + target_compile_options(pe_import_first_tests PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME pe_import_first COMMAND pe_import_first_tests) + set_tests_properties(pe_import_first PROPERTIES TIMEOUT 10) + find_package(Python3 QUIET COMPONENTS Interpreter) if(Python3_Interpreter_FOUND) add_test(NAME nid_gnu_hash COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_gnu_hash.py $) + add_test(NAME nid_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} @@ -215,6 +298,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 $) @@ -224,16 +318,23 @@ if(BUILD_TESTING) target_link_libraries(guest_kernel_sys_tests PRIVATE libkernel_sys libkernel libc) configure_windows_unwind(guest_kernel_sys_tests) add_test(NAME guest_kernel_sys COMMAND guest_kernel_sys_tests) + add_test_executable(guest_kernel_sys_thread_defaults_tests tests/GuestKernelSysThreadDefaults.cpp) + target_include_directories(guest_kernel_sys_thread_defaults_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_thread_defaults_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_thread_defaults_tests) + add_test(NAME guest_kernel_sys_thread_defaults COMMAND guest_kernel_sys_thread_defaults_tests) + set_tests_properties(guest_kernel_sys_thread_defaults PROPERTIES TIMEOUT 20) if(Python3_Interpreter_FOUND) add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $) endif() if(WIN32) - set_tests_properties(guest_kernel_sys PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_kernel_sys guest_kernel_sys_thread_defaults PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() if(APPLE) add_subdirectory(tests/write_watch) + add_subdirectory(tests/guest_arena) endif() if(WIN32) add_test_executable(windows_write_watch_tests tests/WindowsWriteWatch.cpp) @@ -248,15 +349,77 @@ if(BUILD_TESTING) if(WIN32) add_test_executable(undeliverable_host_exception_tests tests/UndeliverableHostException.cpp) + if(MINGW) + target_compile_options(undeliverable_host_exception_tests PRIVATE -fno-asynchronous-unwind-tables -fno-exceptions) + endif() add_dependencies(undeliverable_host_exception_tests libc) add_test(NAME undeliverable_host_exception COMMAND undeliverable_host_exception_tests $) set_tests_properties(undeliverable_host_exception PROPERTIES PASS_REGULAR_EXPRESSION "C\\+\\+ exception St9bad_alloc raised on thread [0-9]+ cannot be delivered: 0x[0-9a-f]+ undeliverable_host_exception_tests\\.exe\\+0x[0-9a-f]+ has no unwind data") endif() + add_test_executable(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_gnm_neo_retail_tests tests/GuestGnmNeoRetail.cpp) + target_include_directories(guest_gnm_neo_retail_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_gnm_neo_retail_tests PRIVATE libSceGnmDriverForNeoMode libc) + configure_windows_unwind(guest_gnm_neo_retail_tests) + add_test(NAME guest_gnm_neo_retail COMMAND guest_gnm_neo_retail_tests) + if(WIN32) + set_tests_properties(guest_gnm_neo_retail PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_move_tests tests/GuestMove.cpp) + target_include_directories(guest_move_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_move_tests PRIVATE libSceMove libc) + configure_windows_unwind(guest_move_tests) + add_test(NAME guest_move COMMAND guest_move_tests) + if(WIN32) + set_tests_properties(guest_move PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_np_party_tests tests/GuestNpParty.cpp) + target_include_directories(guest_np_party_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_party_tests PRIVATE libSceNpParty libc) + configure_windows_unwind(guest_np_party_tests) + add_test(NAME guest_np_party COMMAND guest_np_party_tests) + if(WIN32) + set_tests_properties(guest_np_party PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp) target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc) + target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libSceVideoRecordingP libc) configure_windows_unwind(guest_offline_imports_tests) add_test(NAME guest_offline_imports COMMAND guest_offline_imports_tests) if(Python3_Interpreter_FOUND) @@ -268,6 +431,17 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_entitlements_tests tests/GuestEntitlements.cpp) + target_include_directories(guest_entitlements_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_entitlements_tests PRIVATE libSceNpEntitlementAccess libc) + configure_windows_unwind(guest_entitlements_tests) + add_test(NAME guest_entitlements COMMAND guest_entitlements_tests) + add_test(NAME guest_entitlements_missing COMMAND guest_entitlements_tests missing) + if(WIN32) + set_tests_properties(guest_entitlements guest_entitlements_missing PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_signin_dialog_tests tests/GuestSigninDialog.cpp) target_include_directories(guest_signin_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_signin_dialog_tests PRIVATE libSceSigninDialog libc) @@ -278,6 +452,36 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_usb_storage_dialog_tests tests/GuestUsbStorageDialog.cpp) + target_include_directories(guest_usb_storage_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_usb_storage_dialog_tests PRIVATE libSceUsbStorageDialog libSceUserService libc) + configure_windows_unwind(guest_usb_storage_dialog_tests) + add_test(NAME guest_usb_storage_dialog COMMAND guest_usb_storage_dialog_tests) + if(WIN32) + set_tests_properties(guest_usb_storage_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_game_live_streaming_tests tests/GuestGameLiveStreaming.cpp) + target_include_directories(guest_game_live_streaming_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_game_live_streaming_tests PRIVATE libSceGameLiveStreaming libc) + configure_windows_unwind(guest_game_live_streaming_tests) + add_test(NAME guest_game_live_streaming COMMAND guest_game_live_streaming_tests) + if(WIN32) + set_tests_properties(guest_game_live_streaming PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_netctl_ap_tests tests/GuestNetCtlAp.cpp) + target_include_directories(guest_netctl_ap_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_netctl_ap_tests PRIVATE libSceNetCtlAp libc) + configure_windows_unwind(guest_netctl_ap_tests) + add_test(NAME guest_netctl_ap COMMAND guest_netctl_ap_tests) + if(WIN32) + set_tests_properties(guest_netctl_ap PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_netctl_ap_dialog_tests tests/GuestNetCtlApDialog.cpp) target_include_directories(guest_netctl_ap_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_netctl_ap_dialog_tests PRIVATE libSceNetCtlApDialog libc) @@ -328,6 +532,26 @@ 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_rudp_poll_tests tests/GuestRudpPoll.cpp) + target_include_directories(guest_rudp_poll_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_rudp_poll_tests PRIVATE libSceRudp libc) + configure_windows_unwind(guest_rudp_poll_tests) + add_test(NAME guest_rudp_poll COMMAND guest_rudp_poll_tests) + if(WIN32) + set_tests_properties(guest_rudp_poll PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_memory_pool_stats_tests tests/GuestMemoryPoolStats.cpp) target_include_directories(guest_memory_pool_stats_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_memory_pool_stats_tests PRIVATE libSceSsl libSceHttp2 libSceNet libkernel libc) @@ -378,6 +602,18 @@ if(BUILD_TESTING) set_tests_properties(savedata_memory_growth savedata_memory_growth_read_failure PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(savedata_memory_journal_tests tests/SaveDataMemoryJournal.cpp) + target_include_directories(savedata_memory_journal_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(savedata_memory_journal_tests PRIVATE libSceSaveData.native) + configure_windows_unwind(savedata_memory_journal_tests) + if(MINGW) + target_compile_options(savedata_memory_journal_tests PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME savedata_memory_journal COMMAND savedata_memory_journal_tests) + if(WIN32) + set_tests_properties(savedata_memory_journal PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_library(host_thread_local_test_library SHARED tests/HostThreadLocalLibrary.cpp) set_target_properties(host_thread_local_test_library PROPERTIES RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") target_include_directories(host_thread_local_test_library PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) @@ -395,6 +631,26 @@ if(BUILD_TESTING) set_tests_properties(prx_thread_local_destructors PROPERTIES TIMEOUT 300) endif() + add_test_executable(guest_np_cpp_web_api_intrusive_ptr_tests tests/GuestNpCppWebApiIntrusivePtr.cpp) + target_include_directories(guest_np_cpp_web_api_intrusive_ptr_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_cpp_web_api_intrusive_ptr_tests PRIVATE libSceNpCppWebApi libc) + configure_windows_unwind(guest_np_cpp_web_api_intrusive_ptr_tests) + add_test(NAME guest_np_cpp_web_api_intrusive_ptr COMMAND guest_np_cpp_web_api_intrusive_ptr_tests) + if(WIN32) + set_tests_properties(guest_np_cpp_web_api_intrusive_ptr PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_np_cpp_web_api_signed_out_tests tests/GuestNpCppWebApiSignedOut.cpp) + target_include_directories(guest_np_cpp_web_api_signed_out_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_cpp_web_api_signed_out_tests PRIVATE libSceNpCppWebApi libc) + configure_windows_unwind(guest_np_cpp_web_api_signed_out_tests) + add_test(NAME guest_np_cpp_web_api_signed_out COMMAND guest_np_cpp_web_api_signed_out_tests) + if(WIN32) + set_tests_properties(guest_np_cpp_web_api_signed_out PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(savedata_core_write_replacement_tests tests/SaveDataWriteReplacement.cpp) target_include_directories(savedata_core_write_replacement_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(savedata_core_write_replacement_tests PRIVATE libSceSaveData) @@ -435,16 +691,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) @@ -474,4 +720,36 @@ if(BUILD_TESTING) set_tests_properties(guest_acm PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_voice_chat_tests tests/GuestVoiceChat.cpp) + target_include_directories(guest_voice_chat_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_voice_chat_tests PRIVATE libSceVoiceChat libc) + configure_windows_unwind(guest_voice_chat_tests) + add_test(NAME guest_voice_chat COMMAND guest_voice_chat_tests) + if(WIN32) + set_tests_properties(guest_voice_chat PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_np_game_intent_tests tests/GuestNpGameIntent.cpp) + target_include_directories(guest_np_game_intent_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_game_intent_tests PRIVATE libSceNpGameIntent libc) + configure_windows_unwind(guest_np_game_intent_tests) + add_test(NAME guest_np_game_intent COMMAND guest_np_game_intent_tests) + if(WIN32) + set_tests_properties(guest_np_game_intent PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_vorbis_dec_tests tests/GuestVorbisDec.cpp) + target_include_directories(guest_vorbis_dec_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_compile_definitions(guest_vorbis_dec_tests PRIVATE OV_EXCLUDE_STATIC_CALLBACKS) + target_link_libraries(guest_vorbis_dec_tests PRIVATE libSceVorbisDec vorbisenc vorbisfile vorbis ogg libc) + configure_windows_unwind(guest_vorbis_dec_tests) + add_test(NAME guest_vorbis_dec COMMAND guest_vorbis_dec_tests) + set_tests_properties(guest_vorbis_dec PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_vorbis_dec PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 9ddb8391f4..4a61c38ed7 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -160,18 +160,23 @@ struct VirtualQueryInfo { std::uint64_t offset; std::int32_t protection; std::int32_t memory_type; - std::uint32_t is_flexible : 1; - std::uint32_t is_direct : 1; - std::uint32_t is_stack : 1; - std::uint32_t is_pooled : 1; - std::uint32_t is_committed : 1; - std::uint32_t is_gpu_prt : 1; - std::uint32_t amm_usage : 1; - std::uint32_t reserved : 1; + std::uint8_t is_flexible : 1; + std::uint8_t is_direct : 1; + std::uint8_t is_stack : 1; + std::uint8_t is_pooled : 1; + std::uint8_t is_committed : 1; + std::uint8_t is_gpu_prt : 1; + std::uint8_t amm_usage : 1; + std::uint8_t reserved : 1; char name[32]; std::uint8_t gpu_mask_id; std::uint8_t reserved2; }; +static_assert(sizeof(VirtualQueryInfo) == 72); +static_assert(alignof(VirtualQueryInfo) == 8); +static_assert(offsetof(VirtualQueryInfo, name) == 33); +static_assert(offsetof(VirtualQueryInfo, gpu_mask_id) == 65); +static_assert(offsetof(VirtualQueryInfo, reserved2) == 66); struct KernelSemaPrivate; struct KernelEventFlagPrivate; @@ -328,7 +333,7 @@ struct VaList { }; #pragma pack(pop) -using FiberEntry = void* (*)(std::uint64_t, void*); +using FiberEntry = void (APS5_VABI*)(std::uint64_t argOnInitialize, std::uint64_t argOnRun); struct FiberOptParam { std::uint32_t magic; @@ -427,6 +432,48 @@ struct AcmBatchError { std::uint32_t reserved[8]; }; +struct AcmReverbBuffer { + void* samples; + const void* blockMetadata; +}; + +struct AcmReverbInput { + std::uint32_t blockSize; + std::uint32_t blockCount; + std::uint32_t channelCount; + std::uint32_t sampleFormat; + std::uint32_t currentBlock; + std::uint32_t availableBlocks; + const AcmReverbBuffer* const* history; + const void* const* inputChannels; + std::uint32_t pendingBlocks; +}; + +struct AcmReverbFilter { + std::uint32_t blockSize; + std::uint32_t blockCount; + std::uint32_t channelCount; + std::uint32_t sampleFormat; + const void* blockMetadata; + const AcmReverbBuffer* const* channels; +}; + +struct AcmReverbOutput { + std::uint32_t blockSize; + std::uint32_t channelCount; + void* const* samples; + void* const* workspace; + float gain; + float previousGain; + void* const* previousSamples; +}; + +static_assert(sizeof(AcmReverbBuffer) == 16); +static_assert(offsetof(AcmReverbInput, history) == 24); +static_assert(offsetof(AcmReverbInput, pendingBlocks) == 40); +static_assert(sizeof(AcmReverbFilter) == 32); +static_assert(offsetof(AcmReverbOutput, previousSamples) == 32); + struct AcmBatchInfo { void* buffer; std::size_t offset; @@ -552,6 +599,47 @@ struct AudioOut2MasteringStatesHeader { std::uint32_t states_id; }; +struct AudioOut2MasteringStatesDescriptor { + std::uint32_t id; + std::uint32_t size; +}; + +struct AudioOut2MasteringCompressorStates { + AudioOut2MasteringStatesDescriptor descriptor; + std::uint32_t reserved[2]; + float input_rms[3][8]; + float compression_coeff[3][8]; +}; + +struct AudioOut2MasteringCompressorStatesV2 { + AudioOut2MasteringStatesDescriptor descriptor; + std::uint32_t reserved[2]; + float input_rms[4][8]; + float compression_coeff[4][8]; +}; + +struct AudioOut2MasteringLimiterStates { + AudioOut2MasteringStatesDescriptor descriptor; + std::uint32_t reserved[2]; + float input_peak[8]; + float output_peak[8]; + float gain_peak[8]; +}; + +struct AudioOut2MasteringStates { + AudioOut2MasteringStatesHeader states_header; + std::uint32_t reserved[3]; + AudioOut2MasteringCompressorStates compressor_states; + AudioOut2MasteringLimiterStates limiter_states; +}; + +struct AudioOut2MasteringStatesV2 { + AudioOut2MasteringStatesHeader states_header; + std::uint32_t reserved[3]; + AudioOut2MasteringCompressorStatesV2 compressor_states; + AudioOut2MasteringLimiterStates limiter_states; +}; + struct AudioOutOutputParam { int handle; const void* ptr; @@ -675,10 +763,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]; @@ -884,13 +975,60 @@ struct AudiodecM4aacInfo { std::int32_t i_result; }; +struct VoiceEvent { + std::int32_t type; + std::uint32_t reserved; + void* userData; + union { + struct { + std::uint16_t count; + std::uint16_t ports[7]; + } dataReady; + struct { + std::uint16_t port; + std::uint8_t state; + std::uint8_t reserved[13]; + } deviceState; + }; +}; + +using VoiceEventHandler = void (APS5_VABI*)(const VoiceEvent* event); + +struct VoiceThreadsParams { + std::uint32_t mode; + std::int32_t firstPriority; + std::uint64_t firstAffinity; + std::int32_t secondPriority; + std::uint32_t reserved20; + std::uint64_t secondAffinity; + std::int32_t thirdPriority; + std::uint32_t reserved36; + std::uint64_t thirdAffinity; +}; + +static_assert(sizeof(VoiceEvent) == 32); +static_assert(offsetof(VoiceEvent, userData) == 8); +static_assert(offsetof(VoiceEvent, dataReady) == 16); +static_assert(offsetof(VoiceEvent, deviceState) == 16); +static_assert(sizeof(VoiceThreadsParams) == 48); +static_assert(offsetof(VoiceThreadsParams, firstAffinity) == 8); +static_assert(offsetof(VoiceThreadsParams, secondPriority) == 16); +static_assert(offsetof(VoiceThreadsParams, secondAffinity) == 24); +static_assert(offsetof(VoiceThreadsParams, thirdPriority) == 32); +static_assert(offsetof(VoiceThreadsParams, thirdAffinity) == 40); + struct VoiceInitParam { std::int32_t app_type; - std::uint64_t on_event; + VoiceEventHandler on_event; void* user_data; - std::uint8_t reserved[32 - sizeof(std::int32_t) - sizeof(std::uint64_t) - sizeof(void*)]; + std::uint8_t reserved[16]; }; +static_assert(sizeof(VoiceInitParam) == 40); +static_assert(offsetof(VoiceInitParam, on_event) == 8); +static_assert(offsetof(VoiceInitParam, user_data) == 16); +static_assert(offsetof(VoiceInitParam, reserved) == 24); + struct VoicePortParam { std::int32_t port_type; std::uint16_t threshold; @@ -913,6 +1051,30 @@ struct VoicePortInfo { std::uint16_t reserved; }; +struct NpAuthClientId; + +struct NpAuthAuthorizedAppCodeParameter { + std::uint64_t size; + std::int32_t userId; + const NpAuthClientId* clientId; + const char* scope; + std::uint32_t option; +}; + +static_assert(sizeof(NpAuthAuthorizedAppCodeParameter) == 40); +static_assert(offsetof(NpAuthAuthorizedAppCodeParameter, clientId) == 16); +static_assert(offsetof(NpAuthAuthorizedAppCodeParameter, scope) == 24); +static_assert(offsetof(NpAuthAuthorizedAppCodeParameter, option) == 32); + +struct VoiceResourceInfo { + std::uint16_t maxVoiceInputPorts; + std::uint16_t maxVoiceOutputPorts; + std::uint16_t maxDeviceInputPorts; + std::uint16_t maxDeviceOutputPorts; +}; + +static_assert(sizeof(VoiceResourceInfo) == 8); + struct VoiceStartParam { void* container; std::uint32_t mem_size; @@ -1098,8 +1260,8 @@ struct ImeKeycode { std::uint64_t timestamp; }; -using ImeTextFilter = std::int32_t (*)(char16_t* out_text, std::uint32_t* out_text_length, const char16_t* source_text, std::uint32_t source_text_length); -using ImeExtKeyboardFilter = int (*)(const ImeKeycode* source_keycode, std::uint16_t* out_keycode, std::uint32_t* out_status, void* reserved); +using ImeTextFilter = std::int32_t (APS5_VABI*)(char16_t* outText, std::uint32_t* outTextLength, const char16_t* sourceText, std::uint32_t sourceTextLength); +using ImeExtKeyboardFilter = int (APS5_VABI*)(const ImeKeycode* sourceKeycode, std::uint16_t* outKeycode, std::uint32_t* outStatus, void* reserved); struct ImeExtendedParam { std::uint32_t option; @@ -1261,7 +1423,12 @@ struct NetEpollEvent { NetEpollData data; }; -struct NetCtlNatInfo { std::uint8_t opaque[128]; }; +struct NetCtlNatInfo { + std::uint32_t size; + std::uint8_t opaque[12]; +}; + +static_assert(sizeof(NetCtlNatInfo) == 16); union NetCtlInfo { std::uint8_t opaque[256]; }; @@ -1272,14 +1439,14 @@ struct NetCtlIfStat { std::uint32_t reserved[8]; }; -using NetCtlCallback = void (*)(int, void*); +using NetCtlCallback = void (APS5_VABI *)(int, void*); struct HttpEpoll; using HttpEpollHandle = HttpEpoll*; -using HttpsCallback = int (*)(int, unsigned int, void* const*, int, void*); -using HttpRedirectCallback = int (*)(int, std::int32_t, std::int32_t*, const char*, void*); -using HttpCookieRecvCallback = int (*)(int, const char*, const char*, std::uint64_t, void*); -using HttpAuthInfoCallback = int (*)(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*); +using HttpsCallback = int (APS5_VABI*)(int, unsigned int, void* const*, int, void*); +using HttpRedirectCallback = int (APS5_VABI*)(int, std::int32_t, std::int32_t*, const char*, void*); +using HttpCookieRecvCallback = int (APS5_VABI*)(int, const char*, const char*, std::uint64_t, void*); +using HttpAuthInfoCallback = int (APS5_VABI*)(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*); struct HttpNBEvent { std::uint8_t opaque[64]; }; @@ -1338,6 +1505,24 @@ struct NpUnifiedEntitlementLabel { char padding[3]; }; +struct NpServiceEntitlementLabel { + char data[7]; + std::uint8_t reserved[13]; +}; + +struct NpEntitlementAccessTransactionId { + char data[65]; + std::uint8_t reserved[7]; +}; + +struct NpEntitlementAccessListParameter; +struct NpEntitlementAccessUnifiedInfo; +struct NpEntitlementAccessServiceInfo; + +static_assert(sizeof(NpUnifiedEntitlementLabel) == 20); +static_assert(sizeof(NpServiceEntitlementLabel) == 20); +static_assert(sizeof(NpEntitlementAccessTransactionId) == 72); + struct NpEntitlementAccessInitParam { char reserved[32]; }; struct NpEntitlementAccessBootParam { char reserved[32]; }; @@ -1364,10 +1549,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; @@ -1386,6 +1567,11 @@ struct NpGameIntentData { std::uint8_t padding[7]; }; +struct NpGameIntentInitParam { + std::size_t size; + std::uint8_t reserved[32]; +}; + struct NpGameIntentInfo { std::size_t size; std::int32_t user_id; @@ -1402,6 +1588,26 @@ struct NpWebApi2ResponseInformationOption { std::size_t response_data_size; }; +struct NpWebApi2IntInitializeArgs { + std::int32_t lib_http_ctx_id; + std::int32_t reserved; + std::uint64_t pool_size; + const char* name; + std::uint64_t struct_size; +}; +static_assert(sizeof(NpWebApi2IntInitializeArgs) == 32); + +struct NpWebApi2IntInitialize2Args { + std::int32_t lib_http_ctx_id; + std::int32_t reserved; + std::uint64_t pool_size; + const char* name; + std::uint32_t push_config_group; + std::int32_t reserved2; + std::uint64_t struct_size; +}; +static_assert(sizeof(NpWebApi2IntInitialize2Args) == 40); + struct GameUpdateCheckParam { std::size_t size; std::uint32_t option; @@ -1467,7 +1673,13 @@ struct SaveDataSearchInfo { std::uint8_t reserved[32]; }; static_assert(sizeof(SaveDataSearchInfo) == 48); -struct SaveDataMemoryData { void* buf; std::size_t buf_size; std::size_t offset; }; +struct SaveDataMemoryData { + void* buf; + std::size_t buf_size; + std::size_t offset; + std::uint8_t reserved[40]; +}; +static_assert(sizeof(SaveDataMemoryData) == 64); struct SaveDataMount3 { int user_id; @@ -1606,6 +1818,34 @@ struct AppContentBootParam { struct AppContentMountPoint { char data[16]; }; +using ContentExportMallocCallback = void* (APS5_VABI *)(std::size_t size, void* userData); +using ContentExportFreeCallback = void (APS5_VABI *)(void* memory, void* userData); +using ContentExportDataCallback = std::int32_t (APS5_VABI *)(const void** data, std::size_t* size, void* userData); + +struct ContentExportInitParam { + ContentExportMallocCallback mallocFunc; + ContentExportFreeCallback freeFunc; + void* userData; +}; + +static_assert(sizeof(ContentExportInitParam) == 24); +static_assert(offsetof(ContentExportInitParam, freeFunc) == 8); +static_assert(offsetof(ContentExportInitParam, userData) == 16); + +struct ContentExportSfParam { + std::size_t size; + const char* metadata; + const std::int64_t* contentIds; + std::uint32_t contentCount; + std::uint32_t option; +}; + +static_assert(sizeof(ContentExportSfParam) == 32); +static_assert(offsetof(ContentExportSfParam, metadata) == 8); +static_assert(offsetof(ContentExportSfParam, contentIds) == 16); +static_assert(offsetof(ContentExportSfParam, contentCount) == 24); +static_assert(offsetof(ContentExportSfParam, option) == 28); + struct ContentExportInitParam2 { void* malloc_func; void* free_func; @@ -1750,23 +1990,29 @@ struct JpegEncOutputInfo { }; struct JpegDecCreateParam { - std::uint32_t size; + std::uint32_t this_size; std::uint32_t attribute; + std::uint32_t max_image_width; }; struct JpegDecParseParam { const void* jpeg_mem_addr; std::uint32_t jpeg_mem_size; - std::uint32_t reserved0; + std::uint16_t decode_mode; + std::uint16_t down_scale; }; struct JpegDecDecodeParam { const void* jpeg_mem_addr; void* image_mem_addr; + void* coefficient_mem_addr; std::uint32_t jpeg_mem_size; std::uint32_t image_mem_size; + std::uint32_t coefficient_mem_size; + std::uint16_t decode_mode; + std::uint16_t down_scale; std::uint16_t pixel_format; - std::uint16_t reserved0; + std::uint16_t alpha_value; std::uint32_t image_pitch; }; @@ -1774,8 +2020,13 @@ struct JpegDecImageInfo { std::uint32_t image_width; std::uint32_t image_height; std::uint16_t color_space; - std::uint16_t bit_depth; - std::uint32_t image_flag; + std::uint16_t component_number; + std::uint8_t component_id[4]; + std::uint8_t sampling_factor[4]; + std::uint32_t coefficient_mem_size; + std::uint32_t suitable_csc_attribute; + std::uint32_t output_image_width; + std::uint32_t output_image_height; }; struct PlayGoInitParams { @@ -1802,11 +2053,22 @@ union PlayGoOptionalChunk { }; struct PlayGoDialogResult { + std::int32_t errorCode; std::int32_t result; - std::int32_t reserved[9]; + std::uint8_t reserved[32]; }; -using RudpEventHandler = void (*)(int ctx_id, int event_id, int error_code, void* arg); +struct RudpPollEvent { + std::int32_t contextId; + std::uint16_t events; + std::uint16_t returnedEvents; +}; + +static_assert(sizeof(RudpPollEvent) == 8); +static_assert(offsetof(RudpPollEvent, events) == 4); +static_assert(offsetof(RudpPollEvent, returnedEvents) == 6); + +using RudpEventHandler = void (APS5_VABI*)(int ctxId, int eventId, int errorCode, void* arg); struct SystemServiceStatus { std::int32_t event_num = 0; @@ -1826,6 +2088,15 @@ struct SystemServiceDisplaySafeAreaInfo { std::uint8_t reserved[128]; }; +struct SystemServicePlayerDialogParam { + std::uint64_t size; + std::int32_t mode; + std::int32_t user_id; + std::uint64_t target_account_id; + std::uint8_t reserved[40]; +}; +static_assert(sizeof(SystemServicePlayerDialogParam) == 0x40); + struct SystemServiceHdrToneMapLuminance { float max_full_frame_tone_map_luminance; float max_tone_map_luminance; @@ -1866,6 +2137,13 @@ struct SystemGestureTouchRecognizerInformation { struct SystemGestureTouchEvent { std::uint8_t reserve[168]; }; +struct SystemGestureTouchPadData { + std::int32_t pad_handle; + std::int32_t report_number; + const PadData* pad_data_buffer; + std::uint8_t reserve[8]; +}; + struct UserServiceLoginUserIdList { int user_id[4]; }; struct UserServiceRegisteredUserIdList { int user_id[16]; }; @@ -1887,12 +2165,16 @@ struct UserServiceGamePresets { std::uint32_t audio_language; }; -struct UltMutexOptParam { - std::uint32_t reserved_header[2]; +struct alignas(8) UltMutexOptParam { + std::uint32_t header[3]; + std::uint32_t buildVersion; std::uint32_t attribute; - std::uint32_t reserved0; + std::uint32_t reserved[27]; }; +static_assert(sizeof(UltMutexOptParam) == 128); +static_assert(offsetof(UltMutexOptParam, attribute) == 16); + struct UltUlthreadRuntimeOptParam { std::uint8_t bytes[128]; }; using UltUlthreadEntry = std::int32_t (APS5_VABI *)(std::uint64_t); @@ -1943,6 +2225,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; @@ -1971,6 +2289,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/allocator/PeImportFirst.cpp b/core/libs/allocator/PeImportFirst.cpp new file mode 100644 index 0000000000..aa4b040ba2 --- /dev/null +++ b/core/libs/allocator/PeImportFirst.cpp @@ -0,0 +1,94 @@ +#include "PeImportFirst.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Allocator { + +namespace { + +std::uint32_t read32(const std::vector& bytes, std::size_t offset) { + if (offset + 4 > bytes.size()) throw std::runtime_error("truncated image"); + std::uint32_t value = 0; + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +std::uint16_t read16(const std::vector& bytes, std::size_t offset) { + if (offset + 2 > bytes.size()) throw std::runtime_error("truncated image"); + std::uint16_t value = 0; + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +std::string lower(std::string text) { + std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return text; +} + +} + +void MoveImportFirst(std::vector& bytes, const std::string& dll) { + const auto pe = read32(bytes, 0x3c); + if (read32(bytes, pe) != 0x00004550u) throw std::runtime_error("not a PE image"); + const auto sections = read16(bytes, pe + 6); + const auto optionalSize = read16(bytes, pe + 20); + const auto optional = pe + 24; + if (read16(bytes, optional) != 0x20bu) throw std::runtime_error("not a PE32+ image"); + const auto importRva = read32(bytes, optional + 112 + 8); + if (importRva == 0) throw std::runtime_error("the image has no imports"); + const auto sectionTable = optional + optionalSize; + const auto fileOffset = [&](std::uint32_t rva) -> std::size_t { + for (std::uint16_t index = 0; index < sections; ++index) { + const auto header = sectionTable + index * 40u; + const auto virtualSize = read32(bytes, header + 8); + const auto virtualAddress = read32(bytes, header + 12); + const auto rawSize = read32(bytes, header + 16); + const auto rawOffset = read32(bytes, header + 20); + if (rva >= virtualAddress && rva < virtualAddress + std::max(virtualSize, rawSize)) { + if (rva - virtualAddress >= rawSize) throw std::runtime_error("RVA outside the section's file data"); + return rawOffset + (rva - virtualAddress); + } + } + throw std::runtime_error("RVA outside every section"); + }; + const auto directory = fileOffset(importRva); + constexpr std::size_t DescriptorBytes = 20; + std::vector> descriptors; + std::size_t found = SIZE_MAX; + for (std::size_t offset = directory;; offset += DescriptorBytes) { + if (offset + DescriptorBytes > bytes.size()) throw std::runtime_error("truncated import directory"); + std::array descriptor{}; + std::memcpy(descriptor.data(), bytes.data() + offset, DescriptorBytes); + if (std::all_of(descriptor.begin(), descriptor.end(), [](unsigned char value) { return value == 0; })) break; + const auto nameOffset = fileOffset(read32(bytes, offset + 12)); + std::string name; + for (auto at = nameOffset; at < bytes.size() && bytes[at] != 0; ++at) name.push_back(static_cast(bytes[at])); + if (lower(name) == lower(dll)) found = descriptors.size(); + descriptors.push_back(descriptor); + } + if (found == SIZE_MAX) throw std::runtime_error("the image does not import " + dll); + std::rotate(descriptors.begin(), descriptors.begin() + static_cast(found), descriptors.begin() + static_cast(found) + 1); + for (std::size_t index = 0; index < descriptors.size(); ++index) std::memcpy(bytes.data() + directory + index * DescriptorBytes, descriptors[index].data(), DescriptorBytes); +} + +void MoveImportFirstInFile(const std::filesystem::path& path, const std::string& dll) { + std::vector bytes; + { + std::ifstream input(path, std::ios::binary); + if (!input) throw std::runtime_error("cannot open " + path.string()); + bytes.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); + } + MoveImportFirst(bytes, dll); + std::ofstream output(path, std::ios::binary | std::ios::trunc); + if (!output || !output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) throw std::runtime_error("cannot write " + path.string()); +} + +} diff --git a/core/libs/allocator/PeImportFirst.hpp b/core/libs/allocator/PeImportFirst.hpp new file mode 100644 index 0000000000..54db5101fc --- /dev/null +++ b/core/libs/allocator/PeImportFirst.hpp @@ -0,0 +1,15 @@ +#ifndef CORE_LIBS_ALLOCATOR_PEIMPORTFIRST_HPP +#define CORE_LIBS_ALLOCATOR_PEIMPORTFIRST_HPP + +#include +#include +#include + +namespace Allocator { + +void MoveImportFirst(std::vector& image, const std::string& dll); +void MoveImportFirstInFile(const std::filesystem::path& path, const std::string& dll); + +} + +#endif diff --git a/core/libs/allocator/PeImportFirstMain.cpp b/core/libs/allocator/PeImportFirstMain.cpp new file mode 100644 index 0000000000..359c357674 --- /dev/null +++ b/core/libs/allocator/PeImportFirstMain.cpp @@ -0,0 +1,38 @@ +#include "PeImportFirst.hpp" + +#include +#include +#include +#include + +namespace { + +int Usage() { + std::fprintf(stderr, "usage: pe_import_first \n"); + return 2; +} + +int Run(const std::filesystem::path& file, const std::string& dll) { + try { + Allocator::MoveImportFirstInFile(file, dll); + } catch (const std::exception& error) { + std::fprintf(stderr, "pe_import_first: %s: %s\n", file.string().c_str(), error.what()); + return 1; + } + return 0; +} + +} + +#ifdef _WIN32 +int wmain(int argc, wchar_t** argv) { + if (argc != 3) return Usage(); + const auto dll = std::filesystem::path(argv[2]).u8string(); + return Run(argv[1], std::string(dll.begin(), dll.end())); +} +#else +int main(int argc, char** argv) { + if (argc != 3) return Usage(); + return Run(argv[1], argv[2]); +} +#endif diff --git a/core/libs/allocator/RuntimeHeap.c b/core/libs/allocator/RuntimeHeap.c new file mode 100644 index 0000000000..4cece0d0ed --- /dev/null +++ b/core/libs/allocator/RuntimeHeap.c @@ -0,0 +1,38 @@ +#include + +#include +#include + +void* __wrap_malloc(size_t bytes) { + return HeapAlloc(GetProcessHeap(), 0, bytes != 0 ? bytes : 1); +} + +void* __wrap_calloc(size_t count, size_t size) { + if (size != 0 && count > SIZE_MAX / size) return NULL; + const size_t bytes = count * size; + return HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bytes != 0 ? bytes : 1); +} + +void __wrap_free(void* block) { + if (block != NULL) HeapFree(GetProcessHeap(), 0, block); +} + +void* __wrap_realloc(void* block, size_t bytes) { + if (block == NULL) return __wrap_malloc(bytes); + if (bytes == 0) { + __wrap_free(block); + return NULL; + } + return HeapReAlloc(GetProcessHeap(), 0, block, bytes); +} + +char* __wrap__strdup(const char* text) { + const size_t bytes = strlen(text) + 1; + char* copy = __wrap_malloc(bytes); + if (copy != NULL) memcpy(copy, text, bytes); + return copy; +} + +char* __wrap_strdup(const char* text) { + return __wrap__strdup(text); +} diff --git a/core/libs/allocator/tests/PeImportFirstTests.cpp b/core/libs/allocator/tests/PeImportFirstTests.cpp new file mode 100644 index 0000000000..5d40857cdc --- /dev/null +++ b/core/libs/allocator/tests/PeImportFirstTests.cpp @@ -0,0 +1,157 @@ +#include "allocator/PeImportFirst.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::size_t PeOffset = 0x40; +constexpr std::size_t OptionalOffset = PeOffset + 24; +constexpr std::size_t OptionalBytes = 240; +constexpr std::size_t SectionTable = OptionalOffset + OptionalBytes; +constexpr std::uint32_t SectionRva = 0x1000; +constexpr std::size_t SectionOffset = 0x200; +constexpr std::size_t SectionBytes = 0x200; +constexpr std::size_t DescriptorBytes = 20; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void put16(std::vector& bytes, std::size_t offset, std::uint16_t value) { + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +void put32(std::vector& bytes, std::size_t offset, std::uint32_t value) { + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +std::uint32_t get32(const std::vector& bytes, std::size_t offset) { + std::uint32_t value = 0; + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +std::vector image(const std::vector& dlls, std::uint16_t magic = 0x20b) { + std::vector bytes(SectionOffset + SectionBytes, 0); + bytes[0] = 'M'; + bytes[1] = 'Z'; + put32(bytes, 0x3c, PeOffset); + put32(bytes, PeOffset, 0x00004550u); + put16(bytes, PeOffset + 4, 0x8664); + put16(bytes, PeOffset + 6, 1); + put16(bytes, PeOffset + 20, OptionalBytes); + put16(bytes, OptionalOffset, magic); + put32(bytes, OptionalOffset + 108, 16); + put32(bytes, OptionalOffset + 112 + 8, SectionRva); + put32(bytes, OptionalOffset + 112 + 12, static_cast((dlls.size() + 1) * DescriptorBytes)); + std::memcpy(bytes.data() + SectionTable, ".idata", 6); + put32(bytes, SectionTable + 8, SectionBytes); + put32(bytes, SectionTable + 12, SectionRva); + put32(bytes, SectionTable + 16, SectionBytes); + put32(bytes, SectionTable + 20, SectionOffset); + auto name = (dlls.size() + 1) * DescriptorBytes; + for (std::size_t index = 0; index < dlls.size(); ++index) { + const auto descriptor = SectionOffset + index * DescriptorBytes; + put32(bytes, descriptor, static_cast(SectionRva + 0x100 + index * 0x10)); + put32(bytes, descriptor + 4, static_cast(index + 1)); + put32(bytes, descriptor + 12, static_cast(SectionRva + name)); + put32(bytes, descriptor + 16, static_cast(SectionRva + 0x180 + index * 0x10)); + std::memcpy(bytes.data() + SectionOffset + name, dlls[index].c_str(), dlls[index].size() + 1); + name += dlls[index].size() + 1; + } + return bytes; +} + +std::string importName(const std::vector& bytes, std::size_t index) { + const auto rva = get32(bytes, SectionOffset + index * DescriptorBytes + 12); + return reinterpret_cast(bytes.data() + SectionOffset + (rva - SectionRva)); +} + +std::vector descriptor(const std::vector& bytes, std::size_t index) { + const auto first = bytes.begin() + static_cast(SectionOffset + index * DescriptorBytes); + return {first, first + static_cast(DescriptorBytes)}; +} + +void writeFile(const std::filesystem::path& path, const std::vector& bytes) { + std::ofstream output(path, std::ios::binary | std::ios::trunc); + output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + require(static_cast(output), "cannot write " + path.string()); +} + +std::vector readFile(const std::filesystem::path& path) { + std::ifstream input(path, std::ios::binary); + require(static_cast(input), "cannot read " + path.string()); + return {std::istreambuf_iterator(input), std::istreambuf_iterator()}; +} + +void movesTheNamedImportFirst() { + const auto original = image({"kernel32.dll", "ucrtbase.dll", "mimalloc-redirect.dll", "advapi32.dll"}); + auto moved = original; + Allocator::MoveImportFirst(moved, "MIMALLOC-REDIRECT.dll"); + require(importName(moved, 0) == "mimalloc-redirect.dll" && importName(moved, 1) == "kernel32.dll" && importName(moved, 2) == "ucrtbase.dll" && importName(moved, 3) == "advapi32.dll", + "the named import did not move first, or the others changed their order"); + require(descriptor(moved, 0) == descriptor(original, 2) && descriptor(moved, 1) == descriptor(original, 0) && descriptor(moved, 2) == descriptor(original, 1) && descriptor(moved, 3) == descriptor(original, 3), + "a descriptor did not move whole"); + const auto directoryEnd = SectionOffset + 4 * DescriptorBytes; + require(std::equal(moved.begin(), moved.begin() + SectionOffset, original.begin()) && std::equal(moved.begin() + directoryEnd, moved.end(), original.begin() + directoryEnd), + "bytes outside the import descriptors changed"); + auto unchanged = original; + Allocator::MoveImportFirst(unchanged, "kernel32.dll"); + require(unchanged == original, "moving the first import changed the image"); +} + +void movesTheImportInAFile(const std::filesystem::path& directory) { + const auto path = directory / "moved.dll"; + writeFile(path, image({"kernel32.dll", "mimalloc-redirect.dll"})); + Allocator::MoveImportFirstInFile(path, "mimalloc-redirect.dll"); + const auto moved = readFile(path); + require(importName(moved, 0) == "mimalloc-redirect.dll" && importName(moved, 1) == "kernel32.dll", "the file's import did not move first"); +} + +void failsWithoutTouchingTheFile(const std::filesystem::path& directory, const std::string& what, const std::vector& bytes, const std::string& expected) { + const auto path = directory / (what + ".dll"); + writeFile(path, bytes); + try { + Allocator::MoveImportFirstInFile(path, "mimalloc-redirect.dll"); + } catch (const std::exception& error) { + require(std::string(error.what()).find(expected) != std::string::npos, what + ": failed with another error: " + error.what()); + require(readFile(path) == bytes, what + ": the file changed although the move failed"); + return; + } + throw std::runtime_error(what + ": the move did not fail"); +} + +} + +int main() { + auto directory = std::filesystem::temp_directory_path() / std::filesystem::path(u8"aps5-pe-import-first-a\u00e7\u00e3o-\u65e5\u672c-"); + directory += std::to_string(std::random_device{}()); + try { + std::filesystem::create_directories(directory); + movesTheNamedImportFirst(); + movesTheImportInAFile(directory); + failsWithoutTouchingTheFile(directory, "missing-import", image({"kernel32.dll", "ucrtbase.dll"}), "does not import mimalloc-redirect.dll"); + auto truncated = image({"kernel32.dll", "mimalloc-redirect.dll", "ucrtbase.dll"}); + truncated.resize(SectionOffset + DescriptorBytes + DescriptorBytes / 2); + failsWithoutTouchingTheFile(directory, "truncated-directory", truncated, "truncated import directory"); + failsWithoutTouchingTheFile(directory, "pe32", image({"kernel32.dll", "mimalloc-redirect.dll"}, 0x10b), "not a PE32+ image"); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + std::error_code ignored; + std::filesystem::remove_all(directory, ignored); + return 1; + } + std::filesystem::remove_all(directory); + std::cout << "pe import first tests passed\n"; + return 0; +} diff --git a/core/libs/nid/include/nid/ElfPatcher.hpp b/core/libs/nid/include/nid/ElfPatcher.hpp index a0194d7fae..0415b4eb7f 100644 --- a/core/libs/nid/include/nid/ElfPatcher.hpp +++ b/core/libs/nid/include/nid/ElfPatcher.hpp @@ -91,6 +91,7 @@ inline bool IsDynamicNameTag(std::int64_t tag) { return tag == kDtNeeded || tag == kDtSoname || tag == kDtRpath || tag == kDtRunpath; } constexpr std::uint32_t kShtDynamic = 6u; +constexpr std::uint32_t kShtHash = 5u; constexpr std::uint32_t kShtGnuVerdef = 0x6ffffffdu; constexpr std::uint32_t kShtGnuVerneed = 0x6ffffffeu; constexpr std::uint32_t kShtDynsym = 11u; diff --git a/core/libs/nid/include/nid/ExportExclusions.hpp b/core/libs/nid/include/nid/ExportExclusions.hpp index 93442d156e..ae41e169c5 100644 --- a/core/libs/nid/include/nid/ExportExclusions.hpp +++ b/core/libs/nid/include/nid/ExportExclusions.hpp @@ -8,6 +8,8 @@ namespace Nid { std::string NormalizeExportName(const std::string& name); std::unordered_set ReadExportExclusions(const std::string& path); +std::unordered_set ReadExportList(const std::string& path); +bool WriteExportList(const std::string& referencePath, const std::string& path); } diff --git a/core/libs/nid/src/ElfNidPatcher.cpp b/core/libs/nid/src/ElfNidPatcher.cpp index b5470147d9..089bcd7107 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 @@ -27,6 +28,44 @@ std::uint32_t GnuHash(const std::string& name) { return h; } +std::uint32_t SysvHash(const std::string& name) { + std::uint32_t hash = 0u; + for (const unsigned char byte : name) { + hash = (hash << 4u) + byte; + const auto high = hash & 0xf0000000u; + hash ^= high >> 24u; + hash &= ~high; + } + return hash; +} + +void ValidateSysvHash(const std::vector& elf, std::size_t offset, std::size_t size, std::size_t symCount) { + using namespace Internal; + RequireElfRange(elf, offset, size, "hash"); + if (size < 8u) throw std::runtime_error("invalid ELF hash header"); + const auto buckets = Read(elf, offset); + const auto chains = Read(elf, offset + 4u); + if (!buckets || chains != symCount || 8u + (std::uint64_t{buckets} + chains) * 4u > size) + throw std::runtime_error("invalid ELF hash geometry"); +} + +void RebuildSysvHash(std::vector& elf, std::size_t offset, const std::vector& symbolNames) { + using namespace Internal; + const auto bucketCount = Read(elf, offset); + std::vector buckets(bucketCount, 0u); + std::vector chains(symbolNames.size(), 0u); + for (std::size_t index = 1u; index < symbolNames.size(); ++index) { + if (symbolNames[index].empty()) continue; + const auto bucket = SysvHash(symbolNames[index]) % bucketCount; + chains[index] = buckets[bucket]; + buckets[bucket] = static_cast(index); + } + for (std::size_t index = 0u; index < buckets.size(); ++index) + Write(elf, offset + 8u + index * 4u, buckets[index]); + for (std::size_t index = 0u; index < chains.size(); ++index) + Write(elf, offset + 8u + buckets.size() * 4u + index * 4u, chains[index]); +} + GnuHashLayout ReadGnuHashLayout(const std::vector& elf, std::size_t sectionOffset) { using namespace Internal; @@ -328,6 +367,8 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& std::size_t dynSymOffset = 0u, dynSymSize = 0u; std::size_t gnuHashOffset = 0u, gnuHashSize = 0u; + std::size_t sysvHashOffset = 0u, sysvHashSize = 0u; + std::uint32_t sysvHashLink = 0u; std::size_t versymOffset = 0u; std::uint32_t dynSymSectionIndex = 0u; std::uint32_t gnuHashSymOffset = 0u; @@ -346,6 +387,11 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& gnuHashOffset = static_cast(shdr.sh_offset); gnuHashSize = static_cast(shdr.sh_size); } + if (shdr.sh_type == kShtHash) { + sysvHashOffset = static_cast(shdr.sh_offset); + sysvHashSize = static_cast(shdr.sh_size); + sysvHashLink = shdr.sh_link; + } if (shdr.sh_type == kShtGnuVersym) versymOffset = static_cast(shdr.sh_offset); if (shdr.sh_type == kShtGnuVerdef) @@ -356,6 +402,10 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& const std::size_t symCount = dynSymSize / sizeof(Elf64_Sym); if (symCount == 0u) throw std::runtime_error(".dynsym is empty"); + if (sysvHashOffset != 0u) { + if (sysvHashLink != dynSymSectionIndex) throw std::runtime_error("invalid ELF hash symbol table link"); + ValidateSysvHash(elf, sysvHashOffset, sysvHashSize, symCount); + } if (gnuHashOffset != 0u) { ValidateGnuHash(elf, gnuHashOffset, gnuHashSize, symCount); gnuHashSymOffset = Read(elf, gnuHashOffset + 4u); @@ -408,6 +458,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 +475,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); @@ -510,6 +564,7 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& FixupRelocationSymbolIndices(elf, ehdr, dynSymSectionIndex, oldToNewIndex); RebuildGnuHash(elf, gnuHashOffset, gnuHashSize, symCount, newSymbolNames); } + if (sysvHashOffset != 0u) RebuildSysvHash(elf, sysvHashOffset, newSymbolNames); } } diff --git a/core/libs/nid/src/ExportExclusions.cpp b/core/libs/nid/src/ExportExclusions.cpp index 366e918cb5..273f517d3b 100644 --- a/core/libs/nid/src/ExportExclusions.cpp +++ b/core/libs/nid/src/ExportExclusions.cpp @@ -4,10 +4,13 @@ #include #include #include +#include #include #include #include +#include #include +#include namespace Nid { @@ -113,6 +116,24 @@ std::unordered_set ReadElfExports(const std::vector& return result; } +std::string ReadText(const std::string& path, std::ifstream& input) { + std::string text{std::istreambuf_iterator(input), {}}; + if (input.bad()) throw std::runtime_error("cannot read export list: " + path); + return text; +} + +std::string FormatExportList(const std::unordered_set& exports) { + std::vector names(exports.begin(), exports.end()); + std::ranges::sort(names); + std::string text; + for (const auto& name : names) { + if (name.find('\n') != std::string::npos) throw std::runtime_error("export name contains a line break: " + name); + text += name; + text += '\n'; + } + return text; +} + } std::string NormalizeExportName(const std::string& name) { @@ -153,4 +174,31 @@ std::unordered_set ReadExportExclusions(const std::string& path) { return result; } +std::unordered_set ReadExportList(const std::string& path) { + std::ifstream input(path, std::ios::binary); + if (!input) throw std::runtime_error("cannot open export list: " + path); + const auto text = ReadText(path, input); + if (text.empty() || text.back() != '\n') throw std::runtime_error("malformed export list: " + path); + std::unordered_set result; + for (std::size_t start = 0; start < text.size();) { + const auto end = text.find('\n', start); + if (end == start) throw std::runtime_error("empty name in export list: " + path); + auto name = text.substr(start, end - start); + if (!result.insert(name).second) throw std::runtime_error("duplicate name in export list: " + name); + start = end + 1; + } + return result; +} + +bool WriteExportList(const std::string& referencePath, const std::string& path) { + const auto text = FormatExportList(ReadExportExclusions(referencePath)); + if (std::ifstream existing(path, std::ios::binary); existing && ReadText(path, existing) == text) return false; + std::ofstream output(path, std::ios::binary | std::ios::trunc); + if (!output) throw std::runtime_error("cannot write export list: " + path); + output.write(text.data(), static_cast(text.size())); + output.close(); + if (!output) throw std::runtime_error("cannot write export list: " + path); + return true; +} + } diff --git a/core/libs/nid/src/PeNidPatcher.cpp b/core/libs/nid/src/PeNidPatcher.cpp index 1c0862d63d..58fb8ade0a 100644 --- a/core/libs/nid/src/PeNidPatcher.cpp +++ b/core/libs/nid/src/PeNidPatcher.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace Nid { @@ -116,7 +117,45 @@ void PeNidPatcher::PatchNids(std::vector& pe, const std::string& l if (stringsRegionStart == std::numeric_limits::max()) throw std::runtime_error("could not determine strings region start"); - const std::uint32_t rawLimit = ((edataSection.SizeOfRawData + fileAlignment - 1u) / fileAlignment) * fileAlignment; + std::uint32_t rawLimit = ((edataSection.SizeOfRawData + fileAlignment - 1u) / fileAlignment) * fileAlignment; + const auto dllName = ReadCStr(pe, RvaToOffset(pe, exportTable.Name, peHeaderOffset, numberOfSections, sizeOfOptionalHeader)); + const bool repackDllName = exportTable.Name >= edataSection.VirtualAddress && + exportTable.Name - edataSection.VirtualAddress < rawLimit; + if (repackDllName) stringsRegionStart = std::min(stringsRegionStart, exportTable.Name - edataSection.VirtualAddress); + struct Forwarder { + std::size_t EntryOffset; + std::string Target; + }; + std::vector forwarders; + std::vector directRvas; + const auto functionsArrayOffset = RvaToOffset(pe, exportTable.AddressOfFunctions, peHeaderOffset, numberOfSections, sizeOfOptionalHeader); + for (std::uint32_t index = 0u; index < exportTable.NumberOfFunctions; ++index) { + const auto entryOffset = functionsArrayOffset + index * sizeof(std::uint32_t); + const auto rva = Read(pe, entryOffset); + if (rva < exportDir.VirtualAddress || std::uint64_t{rva} - exportDir.VirtualAddress >= exportDir.Size) { + directRvas.push_back(rva); + continue; + } + const auto target = ReadCStr(pe, RvaToOffset(pe, rva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader)); + if (target.size() + 1u > exportDir.Size - (rva - exportDir.VirtualAddress)) + throw std::runtime_error("forwarder string outside export directory"); + forwarders.push_back({entryOffset, target}); + stringsRegionStart = std::min(stringsRegionStart, rva - edataSection.VirtualAddress); + } + for (const auto rva : directRvas) { + if (rva >= edataSection.VirtualAddress && rva - edataSection.VirtualAddress >= stringsRegionStart) + rawLimit = std::min(rawLimit, rva - edataSection.VirtualAddress); + } + const auto reserveTable = [&](std::uint32_t rva, std::uint64_t size) { + if (rva < edataSection.VirtualAddress || rva - edataSection.VirtualAddress >= rawLimit) return; + const auto end = std::uint64_t{rva - edataSection.VirtualAddress} + size; + if (end > rawLimit) throw std::runtime_error("export table outside edata section"); + stringsRegionStart = std::max(stringsRegionStart, static_cast(end)); + }; + reserveTable(exportDir.VirtualAddress, sizeof(PeExportDirectory)); + reserveTable(exportTable.AddressOfFunctions, std::uint64_t{exportTable.NumberOfFunctions} * 4u); + reserveTable(exportTable.AddressOfNames, std::uint64_t{exportTable.NumberOfNames} * 4u); + reserveTable(exportTable.AddressOfNameOrdinals, std::uint64_t{exportTable.NumberOfNames} * 2u); std::vector finalNames(exportTable.NumberOfNames); for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) { @@ -125,28 +164,48 @@ void PeNidPatcher::PatchNids(std::vector& pe, const std::string& l finalNames[i] = it->second; } - std::uint32_t requiredSize = stringsRegionStart; - for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) - requiredSize += static_cast(finalNames[i].size()) + 1u; - + std::vector strings; + std::unordered_map stringRvas; + const auto storeString = [&](const std::string& name) { + const auto existing = stringRvas.find(name); + if (existing != stringRvas.end()) return existing->second; + const auto rva = edataSection.VirtualAddress + stringsRegionStart + static_cast(strings.size()); + stringRvas.emplace(name, rva); + strings.insert(strings.end(), name.begin(), name.end()); + strings.push_back(0u); + return rva; + }; + std::vector forwarderRvas; + for (const auto& forwarder : forwarders) forwarderRvas.push_back(storeString(forwarder.Target)); + if (!forwarders.empty() && (std::uint64_t{edataSection.VirtualAddress} + stringsRegionStart < exportDir.VirtualAddress || + std::uint64_t{edataSection.VirtualAddress} + stringsRegionStart + strings.size() > std::uint64_t{exportDir.VirtualAddress} + exportDir.Size)) + throw std::runtime_error("not enough export directory space to repack forwarders"); + const auto dllNameRva = repackDllName ? storeString(dllName) : exportTable.Name; + std::vector finalNameRvas; + for (const auto& name : finalNames) finalNameRvas.push_back(storeString(name)); + const auto requiredSize = std::uint64_t{stringsRegionStart} + strings.size(); if (requiredSize > rawLimit) throw std::runtime_error("not enough raw space in edata section to repack export names"); - - std::uint32_t writeOffset = stringsRegionStart; - for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) { - const std::uint32_t newNameRva = edataSection.VirtualAddress + writeOffset; - const std::size_t newNameFileOffset = static_cast(edataSection.PointerToRawData) + writeOffset; - - if (newNameFileOffset + finalNames[i].size() + 1u > pe.size()) - throw std::runtime_error("repack write out of file bounds"); - - std::memcpy(pe.data() + newNameFileOffset, finalNames[i].data(), finalNames[i].size()); - pe[newNameFileOffset + finalNames[i].size()] = 0u; - Write(pe, namesArrayOffset + i * 4u, newNameRva); - writeOffset += static_cast(finalNames[i].size()) + 1u; + const auto directoryEnd = std::max(std::uint64_t{exportDir.VirtualAddress} + exportDir.Size, + std::uint64_t{edataSection.VirtualAddress} + requiredSize); + for (const auto rva : directRvas) { + if (rva != 0u && rva >= exportDir.VirtualAddress && std::uint64_t{rva} < directoryEnd) + throw std::runtime_error("repacked export directory would cover a direct export"); } + const auto directorySize = directoryEnd - exportDir.VirtualAddress; + if (directorySize > std::numeric_limits::max()) throw std::runtime_error("export directory too large"); + const auto stringFileOffset = static_cast(edataSection.PointerToRawData) + stringsRegionStart; + if (stringFileOffset > pe.size() || strings.size() > pe.size() - stringFileOffset) + throw std::runtime_error("repack write out of file bounds"); + std::memcpy(pe.data() + stringFileOffset, strings.data(), strings.size()); + for (std::size_t index = 0u; index < finalNameRvas.size(); ++index) + Write(pe, namesArrayOffset + index * 4u, finalNameRvas[index]); + for (std::size_t index = 0u; index < forwarders.size(); ++index) + Write(pe, forwarders[index].EntryOffset, forwarderRvas[index]); + Write(pe, exportDirOffset + offsetof(PeExportDirectory, Name), dllNameRva); + Write(pe, dataDirectoryOffset + offsetof(PeDataDirectory, Size), static_cast(directorySize)); - const std::uint32_t newVirtualSize = std::max(edataSection.VirtualSize, writeOffset); + const std::uint32_t newVirtualSize = std::max(edataSection.VirtualSize, static_cast(requiredSize)); auto patchedSection = edataSection; patchedSection.VirtualSize = newVirtualSize; Write(pe, edataSectionOffset, patchedSection); diff --git a/core/libs/nid/src/main.cpp b/core/libs/nid/src/main.cpp index c07a4e8bfa..722c765e7c 100644 --- a/core/libs/nid/src/main.cpp +++ b/core/libs/nid/src/main.cpp @@ -19,16 +19,33 @@ void _writeFile(const std::string& path, const std::vector& data) std::ofstream f(Nid::Internal::NativePath(path), std::ios::binary); if (!f) throw std::runtime_error("cannot write: " + path); f.write(reinterpret_cast(data.data()), static_cast(data.size())); + f.close(); + if (!f) throw std::runtime_error("cannot write: " + path); } } int Run(int argc, char* argv[]) { if (argc < 3) { - std::cerr << "Usage: nid_patcher [--preserve-exports ] [file2 ...]\n"; + std::cerr << "Usage: nid_patcher [--preserve-exports | --preserve-export-list ] [file2 ...]\n" + " nid_patcher --write-export-list \n"; return 1; } + if (std::string(argv[1]) == "--write-export-list") { + if (argc != 4) { + std::cerr << "FAIL: --write-export-list requires a reference library and an export list\n"; + return 2; + } + try { + std::cout << (Nid::WriteExportList(argv[2], argv[3]) ? "WROTE: " : "UNCHANGED: ") << argv[3] << "\n"; + } catch (const std::exception& e) { + std::cerr << "FAIL: " << argv[3] << ": " << e.what() << "\n"; + return 2; + } + return 0; + } + const std::string libraryName = argv[1]; std::unordered_set excludedExports; std::vector paths; @@ -36,10 +53,10 @@ int Run(int argc, char* argv[]) { bool hasReference = false; for (int i = 2; i < argc; ++i) { const std::string argument = argv[i]; - if (argument == "--preserve-exports") { - if (hasReference || i + 1 == argc) throw std::runtime_error("--preserve-exports requires exactly one reference library"); + if (argument == "--preserve-exports" || argument == "--preserve-export-list") { + if (hasReference || i + 1 == argc) throw std::runtime_error(argument + " requires exactly one reference"); hasReference = true; - excludedExports = Nid::ReadExportExclusions(argv[++i]); + excludedExports = argument == "--preserve-exports" ? Nid::ReadExportExclusions(argv[++i]) : Nid::ReadExportList(argv[++i]); } else { if (argument.starts_with("--")) throw std::runtime_error("unknown option: " + argument); paths.push_back(argument); 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/nid/tests/test_gnu_hash.py b/core/libs/nid/tests/test_gnu_hash.py index 3b2d27bbce..2e46a70f32 100644 --- a/core/libs/nid/tests/test_gnu_hash.py +++ b/core/libs/nid/tests/test_gnu_hash.py @@ -3,7 +3,32 @@ import sys import unittest from pathlib import Path -from test_symbol_versions import build_image, symbols, patch, HASH_OFFSET, SHOFF, read_dynsym, gnu_hash +from test_symbol_versions import build_image, symbols, patch, HASH_OFFSET, SHOFF, read_dynsym, gnu_hash, sections + + +def sysv_hash(name): + value = 0 + for byte in name.encode("utf-8"): + value = ((value << 4) + byte) & 0xFFFFFFFF + high = value & 0xF0000000 + value = (value ^ (high >> 24)) & ~high + return value + + +def sysv_lookup(image, name): + table = next(section for section in sections(image) if section[1] == 5) + buckets, chains = struct.unpack_from("= chains or index in visited: + raise AssertionError("invalid SysV hash chain") + visited.add(index) + if entries[index][0] == name: + return index + index = struct.unpack_from(" 512: + raise ValueError("fixture exceeds the export section") + image[512:512 + len(data)] = data + return image + + +def forwarding_exports(image, bits): + directory_offset = 112 if bits == 64 else 96 + export_rva, export_size = struct.unpack_from(" +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAbstractStorage/Export.cpp b/core/libs/prx/libSceAbstractStorage/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAbstractStorage/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAbstractTcs/Export.cpp b/core/libs/prx/libSceAbstractTcs/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAbstractTcs/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAbstractTwitter/Export.cpp b/core/libs/prx/libSceAbstractTwitter/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAbstractTwitter/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAbstractYoutube/Export.cpp b/core/libs/prx/libSceAbstractYoutube/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAbstractYoutube/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAc3Enc/Export.cpp b/core/libs/prx/libSceAc3Enc/Export.cpp new file mode 100644 index 0000000000..4ee193119b --- /dev/null +++ b/core/libs/prx/libSceAc3Enc/Export.cpp @@ -0,0 +1,43 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAc3EncCreateEncoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAc3EncDeleteEncoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAc3EncEncode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAc3EncEncodeAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAc3EncFlush() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAc3EncIsEncoding() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAc3EncWaitEncode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAcm/Export.cpp b/core/libs/prx/libSceAcm/Export.cpp index 97dfc19d91..914d474f0b 100644 --- a/core/libs/prx/libSceAcm/Export.cpp +++ b/core/libs/prx/libSceAcm/Export.cpp @@ -1,67 +1,224 @@ #include #include +#include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -#include -// Audio co-processor batches complete immediately without producing output; audio is silent. -static constexpr int SCE_ACM_ERROR_INVALID_PARAMETER = static_cast(0x80E30002); -static std::atomic g_nextId{1}; +namespace { + +constexpr int SCE_ACM_ERROR_BAD_CONTEXT = static_cast(0x81940002); +constexpr int SCE_ACM_ERROR_BAD_POINTER = static_cast(0x81940006); + +std::mutex g_mutex; +std::unordered_set g_contexts; +std::atomic g_nextId{1}; + +} extern "C" { int APS5_VABI sceAcmBatchStartBuffer(AcmContextId context, const void* batch_commands, size_t batch_size, AcmBatchError* batch_error, AcmBatchId* batch) { - (void)context; (void)batch_commands; (void)batch_size; - (void)batch_error; - if (!batch) return SCE_ACM_ERROR_INVALID_PARAMETER; - *batch = g_nextId.fetch_add(1, std::memory_order_relaxed); + if (!batch) return SCE_ACM_ERROR_BAD_POINTER; + if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT; + std::lock_guard lock(g_mutex); + if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT; + AcmBatchId id = g_nextId.fetch_add(1, std::memory_order_relaxed); + while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed); + if (batch_error) std::memset(batch_error, 0, sizeof(*batch_error)); + *batch = id; return 0; } int APS5_VABI sceAcmBatchStartBuffers(AcmContextId context, uint32_t batch_info_count, const AcmBatchInfo* const batch_info[], AcmBatchError* batch_error, AcmBatchId* batch) { - (void)context; (void)batch_info_count; (void)batch_info; - (void)batch_error; - if (!batch) return SCE_ACM_ERROR_INVALID_PARAMETER; - *batch = g_nextId.fetch_add(1, std::memory_order_relaxed); + if (!batch) return SCE_ACM_ERROR_BAD_POINTER; + if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT; + std::lock_guard lock(g_mutex); + if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT; + AcmBatchId id = g_nextId.fetch_add(1, std::memory_order_relaxed); + while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed); + if (batch_error) std::memset(batch_error, 0, sizeof(*batch_error)); + *batch = id; return 0; } int APS5_VABI sceAcmBatchWait(AcmContextId context, AcmBatchId batch, uint32_t timeout) { - (void)context; (void)batch; (void)timeout; + if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT; + std::lock_guard lock(g_mutex); + if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT; return 0; } int APS5_VABI sceAcmContextCreate(AcmContextId* context) { - if (!context) return SCE_ACM_ERROR_INVALID_PARAMETER; - *context = g_nextId.fetch_add(1, std::memory_order_relaxed); + if (!context) return SCE_ACM_ERROR_BAD_POINTER; + AcmContextId id = g_nextId.fetch_add(1, std::memory_order_relaxed); + while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed); + { + std::lock_guard lock(g_mutex); + g_contexts.insert(id); + } + *context = id; return 0; } int APS5_VABI sceAcmContextDestroy(AcmContextId context) { - (void)context; + if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT; + std::lock_guard lock(g_mutex); + auto it = g_contexts.find(context); + if (it == g_contexts.end()) return SCE_ACM_ERROR_BAD_CONTEXT; + g_contexts.erase(it); return 0; } -// Builds a convolution-reverb command into a batch; batches execute as no-ops, so nothing is encoded. -int APS5_VABI sceAcm_ConvReverb_SharedInput(void) { +int APS5_VABI sceAcm_ConvReverb_SharedInput(AcmBatchInfo* batch, std::uint32_t mode, AcmReverbInput* input, std::uint32_t outputCount, const AcmReverbFilter* const* filters, const float* gains, const AcmReverbOutput* const* outputs) { return 0; } -int APS5_VABI sceAcm_FFT(void) { +int APS5_VABI sceAcm_FFT(AcmBatchInfo* batch, std::int32_t transformSize, std::uint32_t transformCount, std::int32_t inputFormat, const void* const* inputs, std::int32_t outputFormat, void* const* outputs, std::int32_t flags) { + if (!batch || transformCount == 0 || transformCount > 0x7FFFFFFF || !inputs || !outputs || (flags & 3) == 3 || inputFormat < 0 || inputFormat > 1 || outputFormat < 0 || outputFormat > 1) return SCE_ACM_ERROR_BAD_POINTER; + if (transformSize != 512 && transformSize != 1024 && transformSize != 2048) return SCE_ACM_ERROR_BAD_POINTER; + const bool realInput = (flags & 1) != 0; + if (transformSize == 2048 && !realInput) return SCE_ACM_ERROR_BAD_POINTER; + const std::uint32_t inputBytes = transformSize * (inputFormat == 1 ? 2 : 4); + const std::uint32_t outputBytes = transformSize * (outputFormat == 1 ? 2 : 4); + const std::uint32_t inputBlocks = (inputBytes >> 9) >> (realInput ? 1 : 0); + const std::uint32_t outputBlocks = outputBytes >> 9; + std::uint32_t transformCommand; + std::uint32_t inputCommand; + std::uint32_t outputCommand; + std::uint32_t setupCommand; + std::uint32_t sizeCode; + if (transformSize == 512) { + sizeCode = 4; + transformCommand = realInput ? 7 : 11; + inputCommand = realInput ? 2 * inputFormat : 9 + inputFormat; + outputCommand = 4 | outputFormat; + setupCommand = realInput ? 6 : 12; + } else if (transformSize == 1024) { + sizeCode = 5; + transformCommand = realInput ? 8 : 13; + inputCommand = realInput ? 2 * inputFormat : 14 | inputFormat; + outputCommand = 5 | (2 * outputFormat); + setupCommand = realInput ? 10 : 12; + } else { + sizeCode = 6; + transformCommand = 6; + inputCommand = 2 * inputFormat; + outputCommand = 4 | outputFormat; + setupCommand = 8; + } + auto* buffer = static_cast(batch->buffer); + auto offset = batch->offset; + const auto capacity = batch->buffer_size; + const auto appendWord = [&](std::uint64_t value) { + if (offset > capacity || capacity - offset < sizeof(value)) return false; + std::memcpy(buffer + offset, &value, sizeof(value)); + offset += sizeof(value); + return true; + }; + std::uint64_t header = static_cast(sizeCode) << 8; + if (!(flags & 8)) { + const auto encodedCount = transformCount < 64000 ? transformCount : 64000; + header |= (std::uint64_t{1} << 16) | (static_cast(transformSize) << 8) | (static_cast(flags & 6) << 19) | (static_cast(flags & 1) << 23) | (std::uint64_t{1} << (inputFormat == 1 ? 24 : 26)) | (std::uint64_t{1} << (outputFormat == 1 ? 25 : 27)) | (static_cast(encodedCount) << 28); + } + constexpr int noSpace = static_cast(0x81940009); + constexpr int badAlignment = static_cast(0x8194000A); + if (!appendWord(header) || !appendWord(10 | (setupCommand << 8))) return noSpace; + for (std::uint32_t index = 0; index < transformCount; ++index) { + const auto inputIndex = static_cast(index) * ((flags & 2) ? 2 : 1); + const auto input = reinterpret_cast(inputs[inputIndex]); + const auto secondInput = (flags & 2) ? reinterpret_cast(inputs[inputIndex + 1]) : 0; + if (!(flags & 2) && !input) return SCE_ACM_ERROR_BAD_POINTER; + if ((input | secondInput) & 127) return badAlignment; + const auto inputRecordSize = (flags & 2) ? 24u : 16u; + if (offset > capacity || capacity - offset < inputRecordSize) return noSpace; + appendWord(((flags & 2) ? 7 : 6) | (static_cast(inputBlocks) << 32) | ((flags & 2) ? static_cast(inputBytes / 2) << 48 : 0)); + appendWord(input); + if (flags & 2) appendWord(secondInput); + if (!appendWord(10 | (inputCommand << 8)) || !appendWord(10 | (transformCommand << 8)) || !appendWord(10 | (outputCommand << 8))) return noSpace; + const auto outputIndex = static_cast(index) * ((flags & 4) ? 2 : 1); + const auto output = reinterpret_cast(outputs[outputIndex]); + const auto secondOutput = (flags & 4) ? reinterpret_cast(outputs[outputIndex + 1]) : 0; + if ((output | secondOutput) & 127) return badAlignment; + const auto outputRecordSize = (flags & 4) ? 24u : 16u; + if (offset > capacity || capacity - offset < outputRecordSize) return noSpace; + appendWord(((flags & 4) ? 9 : 8) | (static_cast(outputBlocks) << 32) | ((flags & 4) ? static_cast(outputBytes / 2) << 48 : 0)); + appendWord(output); + if (flags & 4) appendWord(secondOutput); + } + batch->offset = offset; return 0; } -int APS5_VABI sceAcm_Panner(void) { +int APS5_VABI sceAcm_Panner(AcmBatchInfo* batch, std::uint32_t sourceCount, const void* const* sources, std::uint32_t sourceMode, std::uint32_t outputMode, std::uint32_t channelCount, const void* const* sourceParameters, const void* const* sourceMatrices, const void* const* inputChannels, void* const* outputChannels) { + if (!batch || sourceCount == 0 || sourceCount > 65535 || sourceMode > 2 || outputMode > 1 || channelCount == 0 || channelCount > 36 || !sources || !sourceParameters || !sourceMatrices || !outputChannels) return SCE_ACM_ERROR_BAD_POINTER; + std::uintptr_t alignment = reinterpret_cast(sources) | reinterpret_cast(sourceParameters) | reinterpret_cast(sourceMatrices) | reinterpret_cast(inputChannels) | reinterpret_cast(outputChannels); + for (std::uint32_t index = 0; index < sourceCount; ++index) { + if (!sources[index] || !sourceParameters[index] || !sourceMatrices[index]) return SCE_ACM_ERROR_BAD_POINTER; + std::uint32_t parameterMode; + std::memcpy(¶meterMode, sourceParameters[index], sizeof(parameterMode)); + if (parameterMode > 1) return SCE_ACM_ERROR_BAD_POINTER; + alignment |= reinterpret_cast(sources[index]) | reinterpret_cast(sourceParameters[index]) | reinterpret_cast(sourceMatrices[index]); + } + for (std::uint32_t index = 0; index < channelCount; ++index) { + if (!outputChannels[index]) return SCE_ACM_ERROR_BAD_POINTER; + alignment |= reinterpret_cast(outputChannels[index]); + if (inputChannels) alignment |= reinterpret_cast(inputChannels[index]); + } + if (alignment & 127) return static_cast(0x8194000A); + const auto offset = batch->offset; + const auto capacity = batch->buffer_size; + if (offset > capacity || capacity - offset < 8) return static_cast(0x81940009); + auto* buffer = static_cast(batch->buffer); + const auto writeWord = [buffer](std::size_t position, std::uint64_t value) { std::memcpy(buffer + position, &value, sizeof(value)); }; + const auto encodedCount = sourceCount < 255 ? sourceCount : 255; + writeWord(offset, (static_cast(channelCount) << 24) | (static_cast(encodedCount) << 16) | (std::uint64_t{1} << (sourceMode + 32)) | (std::uint64_t{1} << (outputMode + 35)) | 0x300); + if (capacity < 128 || capacity - offset < 152) return static_cast(0x81940009); + const auto descriptorOffset = capacity - 128; + std::memset(buffer + descriptorOffset, 0, 128); + writeWord(descriptorOffset, sourceCount); + writeWord(descriptorOffset + 8, reinterpret_cast(inputChannels)); + writeWord(descriptorOffset + 16, reinterpret_cast(outputChannels)); + writeWord(descriptorOffset + 24, reinterpret_cast(sources)); + writeWord(descriptorOffset + 32, sourceMode | (static_cast(outputMode) << 32)); + writeWord(descriptorOffset + 40, channelCount); + writeWord(descriptorOffset + 48, reinterpret_cast(sourceParameters)); + writeWord(descriptorOffset + 56, reinterpret_cast(sourceMatrices)); + writeWord(offset + 8, 0x8000000000000D); + writeWord(offset + 16, reinterpret_cast(buffer + descriptorOffset)); + if (capacity - offset < 160) return static_cast(0x81940009); + writeWord(offset + 24, 10); + batch->offset = offset + 32; + batch->buffer_size = descriptorOffset; return 0; } -int APS5_VABI sceAcmBatchJobPriority(void) { +int APS5_VABI sceAcmBatchJobPriority(AcmBatchInfo* batch, std::uint32_t priority) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAcm_ConvReverb_SharedIr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAcm_IFFT() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + + +int APS5_VABI sceAcmBatchJobNotification() { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceAgc/Acb/src/Control.cpp b/core/libs/prx/libSceAgc/Acb/src/Control.cpp index 0eb8d4fd17..567bfd5e18 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Control.cpp @@ -72,13 +72,14 @@ std::uint32_t* APS5_VABI sceAgcAcbWaitUntilSafeForRendering(CommandBuffer* buf, } std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t videoOutHandle, std::int32_t displayBufferIndex, std::uint32_t flipMode, std::int64_t flipArg) { - (void)buf; - (void)videoOutHandle; - (void)displayBufferIndex; - (void)flipMode; - (void)flipArg; - NotImplemented_nid_no_patch(__func__); - return nullptr; + auto* packet = Agc::Command::Allocate(buf, AgcDriver::FlipPacketWords, __func__); + packet[0] = AgcDriver::FlipPacketHeader; + packet[1] = videoOutHandle; + packet[2] = static_cast(displayBufferIndex); + packet[3] = flipMode; + packet[4] = static_cast(static_cast(flipArg)); + packet[5] = static_cast(static_cast(flipArg) >> 32u); + return packet; } std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount) { diff --git a/core/libs/prx/libSceAgc/Acb/src/Memory.cpp b/core/libs/prx/libSceAgc/Acb/src/Memory.cpp index 1a93bdbc22..3fb978fbed 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Memory.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Memory.cpp @@ -43,7 +43,7 @@ std::uint32_t APS5_VABI sceAgcAcbAtomicMemGetSize() { } std::uint32_t APS5_VABI sceAgcAcbAtomicGdsGetSize() { - return 36; + return 44; } std::uint32_t* APS5_VABI sceAgcAcbPrimeUtcl2(CommandBuffer* buf, const volatile void* address, std::uint32_t sizeInBytes) { diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index 0e8338c3da..afff1e8a73 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -35,6 +35,7 @@ add_library(libSceAgc SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/RegisterDefaults.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Semaphore.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/ShaderFusion.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/StaticBuffer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Suspend.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Patch/src/CondExec.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Patch/src/Dma.cpp @@ -81,8 +82,34 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +add_test_executable(agc_atomic_gds_size_tests EXCLUDE_FROM_ALL tests/AtomicGdsSize.cpp) +target_include_directories(agc_atomic_gds_size_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_atomic_gds_size_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_atomic_gds_size_tests) +if(BUILD_TESTING) + set_target_properties(agc_atomic_gds_size_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_atomic_gds_size COMMAND agc_atomic_gds_size_tests) + if(WIN32) + set_tests_properties(agc_atomic_gds_size PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() configure_windows_unwind(libSceAgc) +add_test_executable(agc_static_buffer_tests EXCLUDE_FROM_ALL tests/StaticBuffer.cpp) +target_include_directories(agc_static_buffer_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_static_buffer_tests PRIVATE libSceAgc libSceAgcDriver libc) +configure_windows_unwind(agc_static_buffer_tests) +if(BUILD_TESTING) + set_target_properties(agc_static_buffer_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_static_buffer COMMAND agc_static_buffer_tests) + if(WIN32) + set_tests_properties(agc_static_buffer PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_platform_tests EXCLUDE_FROM_ALL tests/Platform.cpp) target_include_directories(agc_platform_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_platform_tests PRIVATE libSceAgc libc) @@ -240,6 +267,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_instrumentation_query_size_tests EXCLUDE_FROM_ALL tests/InstrumentationQuerySize.cpp) +target_include_directories(agc_instrumentation_query_size_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_instrumentation_query_size_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_instrumentation_query_size_tests) +if(BUILD_TESTING) + set_target_properties(agc_instrumentation_query_size_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_instrumentation_query_size_tests COMMAND agc_instrumentation_query_size_tests) + if(WIN32) + set_tests_properties(agc_instrumentation_query_size_tests PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_context_state_size_tests EXCLUDE_FROM_ALL tests/ContextStateSize.cpp) target_include_directories(agc_context_state_size_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_context_state_size_tests PRIVATE libSceAgc libc) @@ -415,6 +455,7 @@ configure_windows_unwind(agc_shader_helpers_tests) if(BUILD_TESTING) set_target_properties(agc_shader_helpers_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) add_test(NAME agc_shader_helpers COMMAND agc_shader_helpers_tests) + set_tests_properties(agc_shader_helpers PROPERTIES TIMEOUT 30) if(WIN32) set_tests_properties(agc_shader_helpers PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/prx/libSceAgc/Cb/include/Sync.hpp b/core/libs/prx/libSceAgc/Cb/include/Sync.hpp index 4626a7e45c..a070c6a5fc 100644 --- a/core/libs/prx/libSceAgc/Cb/include/Sync.hpp +++ b/core/libs/prx/libSceAgc/Cb/include/Sync.hpp @@ -1,4 +1,13 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGC_CB_INCLUDE_SYNC_HPP #define CORE_LIBS_PRX_LIBSCEAGC_CB_INCLUDE_SYNC_HPP +#include "SceTypes.hpp" +#include + +struct CommandBuffer; + +extern "C" { +std::uint32_t* APS5_VABI sceAgcCbMemSemaphore(CommandBuffer* buf, const volatile void* address, std::uint32_t operation, std::uint32_t modifier, std::uint32_t value); +} + #endif diff --git a/core/libs/prx/libSceAgc/Cb/src/Sync.cpp b/core/libs/prx/libSceAgc/Cb/src/Sync.cpp index ad70ced121..c8c3f80ef3 100644 --- a/core/libs/prx/libSceAgc/Cb/src/Sync.cpp +++ b/core/libs/prx/libSceAgc/Cb/src/Sync.cpp @@ -58,4 +58,13 @@ std::uint32_t* APS5_VABI sceAgcCbReleaseMem(CommandBuffer* buf, std::uint8_t act return packet; } +std::uint32_t* APS5_VABI sceAgcCbMemSemaphore(CommandBuffer* buf, const volatile void* address, std::uint32_t operation, std::uint32_t modifier, std::uint32_t value) { + (void)operation; + (void)modifier; + (void)value; + const auto guestAddress = static_cast(reinterpret_cast(address)); + const auto maskedAddress = guestAddress & ~0xfULL; + return Agc::Command::Emit(buf, 0x39u, {static_cast(maskedAddress), static_cast(maskedAddress >> 32u), 0x80100000u}, __func__); +} + } diff --git a/core/libs/prx/libSceAgc/Command/include/RegisterDefaults.hpp b/core/libs/prx/libSceAgc/Command/include/RegisterDefaults.hpp index a072cf5f5c..5e93283fb1 100644 --- a/core/libs/prx/libSceAgc/Command/include/RegisterDefaults.hpp +++ b/core/libs/prx/libSceAgc/Command/include/RegisterDefaults.hpp @@ -5,7 +5,14 @@ namespace Agc::Command { -void* GetRegisterDefaults(std::uint32_t version, bool internal, const char* function); +enum class RegisterDefaultsTypeLayout { + Compact, + KeyBankPairs, +}; + +void* GetRegisterDefaults(std::uint32_t version, bool internal, const char* function, + RegisterDefaultsTypeLayout typeLayout = RegisterDefaultsTypeLayout::Compact); +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..6ec0e12165 100644 --- a/core/libs/prx/libSceAgc/Command/src/Packet.cpp +++ b/core/libs/prx/libSceAgc/Command/src/Packet.cpp @@ -1,7 +1,9 @@ #include "prx/libSceAgc/Command/include/Packet.hpp" #include +#include #include +#include #include #include #include @@ -96,8 +98,8 @@ std::uint32_t* WriteRegisterRange(CommandBuffer* buffer, std::uint32_t opcode, s Require(count != 0 && count <= 0x3fffu && offset <= 0xffffu && count <= 0x10000u - offset, function, "invalid register range"); std::vector snapshot; if (values != nullptr) { - CheckAddress(reinterpret_cast(values), 4, function); - snapshot.assign(values, values + count); + snapshot.resize(count); + std::memcpy(snapshot.data(), values, static_cast(count) * sizeof(std::uint32_t)); } auto* packet = Allocate(buffer, count + 2u, function); packet[0] = Header(opcode, count + 2u); @@ -107,7 +109,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..8de7d41030 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 @@ -11,13 +12,21 @@ namespace Agc::Command { namespace { +struct TypeIndex { + std::uint32_t key; + std::uint32_t bankIndex; +}; + +static_assert(sizeof(TypeIndex) == 8); + struct RegisterDefaults { ShaderRegister** tables[4]; std::uint32_t registerCounts[4]; - std::uint32_t* types; + void* types; std::uint32_t count; }; +static_assert(offsetof(RegisterDefaults, types) == 0x30); static_assert(offsetof(RegisterDefaults, count) == 0x38); struct CompactBank { @@ -38,19 +47,49 @@ struct CompactRegisterDefaults { struct Storage { RegisterDefaults defaults{}; std::array, 4> pointers; + std::vector types; bool initialized = false; }; std::mutex defaultsMutex; -std::array, 2> storage; +std::array, 2>, 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) { +void* GetRegisterDefaults(std::uint32_t version, bool internal, const char* function, + RegisterDefaultsTypeLayout typeLayout) { Require(version < 14, function, "unsupported register defaults version"); const auto* source = internal ? agcInternalRegDefaultsByVersion[version] : agcPublicRegDefaultsByVersion[version]; std::lock_guard lock(defaultsMutex); - auto& destination = storage[internal ? 1 : 0][version]; + const auto layoutIndex = typeLayout == RegisterDefaultsTypeLayout::KeyBankPairs ? 1 : 0; + auto& destination = storage[internal ? 1 : 0][layoutIndex][version]; if (!destination.initialized) { std::array, 4> pointers; RegisterDefaults defaults{}; @@ -67,13 +106,38 @@ void* GetRegisterDefaults(std::uint32_t version, bool internal, const char* func defaults.registerCounts[bank] = input.registerCount; } Require(source->count == 0 || source->types != nullptr, function, "missing register defaults types"); - defaults.types = source->types; + std::vector types; + if (typeLayout == RegisterDefaultsTypeLayout::KeyBankPairs) { + types.reserve(source->count); + for (std::uint32_t entry = 0; entry < source->count; ++entry) { + const std::uint32_t bankIndex = source->types[entry * 3 + 1] & 0x3ffu; + Require((bankIndex >> 2) < source->banks[bankIndex & 3u].pointerCount, function, "register defaults type exceeds its bank"); + types.push_back({source->types[entry * 3], bankIndex}); + } + } + if (typeLayout == RegisterDefaultsTypeLayout::KeyBankPairs) { + defaults.types = types.empty() ? nullptr : types.data(); + } else { + defaults.types = source->types; + } defaults.count = source->count; destination.pointers = std::move(pointers); + destination.types = std::move(types); destination.defaults = defaults; destination.initialized = true; } 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/DcbDraw/src/IndexBuffer.cpp b/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp index f304439f4c..1d2414ae38 100644 --- a/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp +++ b/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp @@ -42,16 +42,14 @@ std::uint32_t* APS5_VABI sceAgcDcbSetIndexSize_0600(CommandBuffer* buf, std::uin return packet; } -std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint32_t dataOffsetInBytes) { - (void)buf; - (void)dataOffsetInBytes; - NotImplemented_nid_no_patch(__func__); - return nullptr; +std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint64_t attributeAddress, std::uint32_t attributeIndex) { + if (attributeAddress != 0) Agc::Command::CheckAddress(attributeAddress, 16, __func__); + Agc::Command::CheckBits(attributeIndex, 0xffffu, __func__); + return Agc::Command::Emit(buf, 0x91u, {static_cast(attributeAddress), static_cast(attributeAddress >> 32u), attributeIndex}, __func__); } std::uint32_t APS5_VABI sceAgcDcbSetIndexIndirectArgsGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 16; } } diff --git a/core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp b/core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp index b3ae56e199..5a8ebdd44d 100644 --- a/core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp +++ b/core/libs/prx/libSceAgc/DcbDraw/src/Stats.cpp @@ -21,14 +21,13 @@ std::uint32_t* APS5_VABI sceAgcDcbGetLodStats(CommandBuffer* buf, std::uint8_t c return Agc::Command::Emit(buf, 0x10u, {bufferSizeInBytes, static_cast(address), static_cast(address >> 32u), control}, __func__); } -std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize(std::int32_t queryType) { + if (queryType == 1) return 0x120u; + return queryType == 0 ? 0x10u : 0u; } std::uint32_t APS5_VABI sceAgcDcbEndOcclusionQueryGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 0x10u; } } diff --git a/core/libs/prx/libSceAgc/DcbFlow/include/Control.hpp b/core/libs/prx/libSceAgc/DcbFlow/include/Control.hpp index 46c70ed15e..6ded533662 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/include/Control.hpp +++ b/core/libs/prx/libSceAgc/DcbFlow/include/Control.hpp @@ -1,4 +1,12 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGC_DCBFLOW_INCLUDE_CONTROL_HPP #define CORE_LIBS_PRX_LIBSCEAGC_DCBFLOW_INCLUDE_CONTROL_HPP +#include "SceTypes.hpp" +#include + +extern "C" { +std::uint32_t* APS5_VABI sceAgcDcbAtomicGds(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData); +std::uint32_t* APS5_VABI sceAgcDcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData); +} + #endif diff --git a/core/libs/prx/libSceAgc/DcbFlow/include/Memory.hpp b/core/libs/prx/libSceAgc/DcbFlow/include/Memory.hpp index 184131f904..f089c9a83e 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/include/Memory.hpp +++ b/core/libs/prx/libSceAgc/DcbFlow/include/Memory.hpp @@ -2,7 +2,11 @@ #define CORE_LIBS_PRX_LIBSCEAGC_DCBFLOW_INCLUDE_MEMORY_HPP #include "SceTypes.hpp" +#include -extern "C" std::uint32_t* APS5_VABI sceAgcDcbAcquireMem(CommandBuffer* buf, std::uint8_t engine, std::uint32_t cbDbOp, std::uint32_t gcrControl, const volatile void* base, std::uint64_t sizeBytes, std::uint32_t pollCycles); +extern "C" { +std::uint32_t* APS5_VABI sceAgcDcbAcquireMem(CommandBuffer* buf, std::uint8_t engine, std::uint32_t cbDbOp, std::uint32_t gcrControl, const volatile void* base, std::uint64_t sizeBytes, std::uint32_t pollCycles); +std::uint32_t APS5_VABI sceAgcDcbAtomicGdsGetSize(); +} #endif diff --git a/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp b/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp index eefbb3120c..987ef180f3 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp +++ b/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp @@ -12,10 +12,19 @@ extern "C" { -// unknown signature -void* APS5_VABI sceAgcDcbAtomicGds_0900(void) { - NotImplemented_nid_no_patch(__func__); - return nullptr; +std::uint32_t* APS5_VABI sceAgcDcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData) { + const auto operation = (control & 0xffu) | ((control & 0x200u) << 7u) | + ((control & 0x20u) << 13u) | ((control & 0x40u) << 14u) | + ((control & 0x20u) == 0 && mode == 1 ? 0x20000u : 0u); + return Agc::Command::Emit(buf, 0x1du, { + operation, ((gdsOp & 1u) << 8u) | (src & 0x3fu), offset, index, + mask & 0x00ff00ffu, data, static_cast(compareData), + static_cast(compareData >> 32u), static_cast(extraData), + static_cast(extraData >> 32u)}, __func__); +} + +std::uint32_t* APS5_VABI sceAgcDcbAtomicGds(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData) { + return sceAgcDcbAtomicGds_0900(buf, control, mode, gdsOp, src, offset, index, mask, data, compareData, extraData); } diff --git a/core/libs/prx/libSceAgc/DcbFlow/src/Memory.cpp b/core/libs/prx/libSceAgc/DcbFlow/src/Memory.cpp index 18dd28abf7..21d0a2eeb2 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/src/Memory.cpp +++ b/core/libs/prx/libSceAgc/DcbFlow/src/Memory.cpp @@ -53,7 +53,7 @@ std::uint32_t APS5_VABI sceAgcDcbAtomicMemGetSize() { } std::uint32_t APS5_VABI sceAgcDcbAtomicGdsGetSize() { - return 36; + return 44; } std::uint32_t* APS5_VABI sceAgcDcbWriteData(CommandBuffer* buf, std::uint8_t dst, std::uint8_t cachePolicy, std::uint64_t address, const void* data, std::uint32_t numDwords, std::uint8_t increment, std::uint8_t writeConfirm) { diff --git a/core/libs/prx/libSceAgc/DcbState/include/ContextState.hpp b/core/libs/prx/libSceAgc/DcbState/include/ContextState.hpp index 409da75f33..99d37d3c6a 100644 --- a/core/libs/prx/libSceAgc/DcbState/include/ContextState.hpp +++ b/core/libs/prx/libSceAgc/DcbState/include/ContextState.hpp @@ -1,4 +1,17 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGC_DCBSTATE_INCLUDE_CONTEXTSTATE_HPP #define CORE_LIBS_PRX_LIBSCEAGC_DCBSTATE_INCLUDE_CONTEXTSTATE_HPP +#include "SceTypes.hpp" +#include + +struct CommandBuffer; + +extern "C" { + +uint32_t* APS5_VABI sceAgcDcbContextStateOp(CommandBuffer* buf, uint32_t operation); +uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, uint32_t operation); +uint64_t APS5_VABI sceAgcDcbContextStateOpGetSize(uint32_t operation); + +} + #endif diff --git a/core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp b/core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp index ffdf58c810..ad1eddc97a 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp @@ -9,10 +9,7 @@ extern "C" { uint32_t* APS5_VABI sceAgcDcbContextStateOp(CommandBuffer* buf, uint32_t operation) { - (void)buf; - (void)operation; - NotImplemented_nid_no_patch(__func__); - return nullptr; + return sceAgcDcbContextStateOp_0100(buf, operation); } uint64_t APS5_VABI sceAgcDcbContextStateOpGetSize(uint32_t operation) { diff --git a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp index 27f4ec9738..f3acef34c5 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp @@ -7,21 +7,18 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -static constexpr int SCE_AGC_ERROR_INVALID_ARGUMENT = static_cast(0x80890001); - extern "C" { -int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version) { - Agc::Command::CheckAddress(reinterpret_cast(state), alignof(std::uint32_t), __func__); - Agc::Command::Require(version < 14, __func__, "unsupported AGC initialization version"); - return 0; -} - int APS5_VABI sceAgcInit(std::uint32_t version) { - if (version >= 14) return SCE_AGC_ERROR_INVALID_ARGUMENT; + if (version >= 14) return 0; Agc::Command::GetRegisterDefaults(version, false, __func__); Agc::Command::GetRegisterDefaults(version, true, __func__); return 0; } +int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version) { + Agc::Command::CheckAddress(reinterpret_cast(state), alignof(std::uint32_t), __func__); + return sceAgcInit(version); +} + } diff --git a/core/libs/prx/libSceAgc/Export.cpp b/core/libs/prx/libSceAgc/Export.cpp new file mode 100644 index 0000000000..8e933ec1cf --- /dev/null +++ b/core/libs/prx/libSceAgc/Export.cpp @@ -0,0 +1,13 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAgcSuspendPointAndCheckStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAgc/Misc/include/PacketInfo.hpp b/core/libs/prx/libSceAgc/Misc/include/PacketInfo.hpp index 85a76dced4..04a3d07eb6 100644 --- a/core/libs/prx/libSceAgc/Misc/include/PacketInfo.hpp +++ b/core/libs/prx/libSceAgc/Misc/include/PacketInfo.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_PACKETINFO_HPP #define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_PACKETINFO_HPP +#include "SceTypes.hpp" #include struct SceAgcMemoryRange { @@ -8,4 +9,9 @@ struct SceAgcMemoryRange { uint64_t size; }; +extern "C" { +int APS5_VABI sceAgcGetDataPacketPayloadAddress(uint32_t** addr, uint32_t* cmd, int type); +int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(uint32_t** addr, uint32_t* cmd, int type); +} + #endif 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/include/StaticBuffer.hpp b/core/libs/prx/libSceAgc/Misc/include/StaticBuffer.hpp new file mode 100644 index 0000000000..2bd49c82aa --- /dev/null +++ b/core/libs/prx/libSceAgc/Misc/include/StaticBuffer.hpp @@ -0,0 +1,14 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_STATICBUFFER_HPP +#define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_STATICBUFFER_HPP + +#include +#include "SceTypes.hpp" + +extern "C" { +int APS5_VABI sceAgcSetStaticBuffer(std::uint32_t index, const void* buffer); +int APS5_VABI sceAgcGetStaticBuffer(std::uint32_t index, void* buffer); +int APS5_VABI sceAgcSetStaticQword(std::uint32_t index, std::uint64_t value); +int APS5_VABI sceAgcGetStaticQword(std::uint32_t index, std::uint64_t* value); +} + +#endif diff --git a/core/libs/prx/libSceAgc/Misc/src/Debug.cpp b/core/libs/prx/libSceAgc/Misc/src/Debug.cpp index 8a321110ab..a211959181 100644 --- a/core/libs/prx/libSceAgc/Misc/src/Debug.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/Debug.cpp @@ -1,9 +1,16 @@ #include "prx/libSceAgc/Misc/include/Debug.hpp" +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +std::atomic shaderInstrumentation{0}; + +} + extern "C" { int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId) { @@ -11,4 +18,13 @@ int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId) { return 0; } +int APS5_VABI sceAgcSetShaderInstrumentation(std::uint32_t flags) { + shaderInstrumentation.store(flags, std::memory_order_relaxed); + return 0; +} + +std::uint32_t APS5_VABI sceAgcGetShaderInstrumentation() { + return shaderInstrumentation.load(std::memory_order_relaxed); +} + } diff --git a/core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp b/core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp index 655ce3faed..543231fdcb 100644 --- a/core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp @@ -28,6 +28,10 @@ int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(uint32_t** addr, uint32_t* return 0; } +int APS5_VABI sceAgcGetDataPacketPayloadAddress(uint32_t** addr, uint32_t* cmd, int type) { + return sceAgcGetDataPacketPayloadAddress_0090(addr, cmd, type); +} + int APS5_VABI sceAgcGetDataPacketPayloadRange(SceAgcMemoryRange* range, uint32_t* cmd, int type) { Agc::Command::CheckAddress(reinterpret_cast(range), alignof(SceAgcMemoryRange), __func__); Agc::Command::CheckAddress(reinterpret_cast(cmd), alignof(uint32_t), __func__); diff --git a/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp b/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp index d5ff3372f1..8d6c8ee43d 100644 --- a/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp @@ -10,8 +10,7 @@ extern "C" { void* APS5_VABI sceAgcGetRegisterDefaults() { - // The legacy API has no version argument; use the baseline public table. - return Agc::Command::GetRegisterDefaults(0, false, __func__); + return Agc::Command::GetRegisterDefaults(13, false, __func__, Agc::Command::RegisterDefaultsTypeLayout::KeyBankPairs); } void* APS5_VABI sceAgcGetRegisterDefaultsInternal() { @@ -26,4 +25,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/Misc/src/Semaphore.cpp b/core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp index 7a6c802690..3ff95978d3 100644 --- a/core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp @@ -13,20 +13,12 @@ namespace { constexpr std::uint64_t semaphoreMemoryAlignment = 0x4000u; constexpr std::uint64_t semaphoreLabelBytes = 32u; -constexpr std::uint64_t semaphoreCounterBytes = 8u; std::mutex semaphoreMutex; std::uint8_t* semaphoreMemory = nullptr; std::uint64_t semaphoreBytes = 0; -bool hasOnlyZeroLabelTails(const std::uint8_t* memory, std::uint64_t sizeInBytes) { - for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) { - for (std::uint64_t byte = semaphoreCounterBytes; byte < semaphoreLabelBytes; ++byte) { - if (memory[label + byte] != 0) return false; - } - } - return true; -} + } @@ -35,15 +27,12 @@ extern "C" { int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes) { const auto address = reinterpret_cast(memory); std::lock_guard lock(semaphoreMutex); - Agc::Command::Require(semaphoreMemory == nullptr, __func__, "the AMM semaphore memory is already registered; the console's error is unknown"); + if (semaphoreBytes != 0) return static_cast(0x8a6c0048u); + if (((address | sizeInBytes) & (semaphoreMemoryAlignment - 1u)) != 0) return static_cast(0x8a6c0002u); Agc::Command::Require(memory != nullptr && sizeInBytes != 0, __func__, "null or empty AMM semaphore memory; the console's error is unknown"); - Agc::Command::Require(((address | sizeInBytes) & (semaphoreMemoryAlignment - 1u)) == 0, __func__, "AMM semaphore memory or size is not 16 KiB aligned; the console's error is unknown"); Agc::Command::Require(sizeInBytes - 1u <= std::numeric_limits::max() - address, __func__, "AMM semaphore memory wraps the address space"); auto* labels = static_cast(memory); - Agc::Command::Require(hasOnlyZeroLabelTails(labels, sizeInBytes), __func__, "AMM semaphore labels hold data past their first 8 bytes; whether registration clears it is unknown"); - for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) { - std::memset(labels + label, 0, semaphoreCounterBytes); - } + std::memset(labels, 0, sizeInBytes); semaphoreMemory = labels; semaphoreBytes = sizeInBytes; return 0; @@ -51,9 +40,9 @@ int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInByte int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label) { std::lock_guard lock(semaphoreMutex); - Agc::Command::Require(semaphoreMemory != nullptr, __func__, "no AMM semaphore memory is registered; the console's error is unknown"); - Agc::Command::Require(index < semaphoreBytes / semaphoreLabelBytes, __func__, "the semaphore label lies outside the AMM semaphore memory; the console's error is unknown"); - Agc::Command::Require(label != nullptr, __func__, "null semaphore label output; the console's error is unknown"); + if (semaphoreBytes == 0) return static_cast(0x8a6c0049u); + if (index >= semaphoreBytes / semaphoreLabelBytes) return static_cast(0x8a6c000bu); + Agc::Command::Require(label != nullptr, __func__, "null semaphore label output"); *label = semaphoreMemory + static_cast(index) * semaphoreLabelBytes; return 0; } diff --git a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp index 8bae004ff1..a8fdedf664 100644 --- a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp @@ -35,7 +35,7 @@ constexpr std::size_t FusedCodeAlignment = 256; constexpr char TrailerMagic[8] = {'b', 'a', 'r', 'e', 'f', 'o', 'o', 't'}; constexpr std::size_t TrailerCodeBytesOffset = sizeof(TrailerMagic) + 3 * sizeof(std::uint32_t); constexpr std::uint32_t Rsrc1VgprMask = 0x3fu; -constexpr std::uint32_t Rsrc1SgprMask = 0xfu << 6u; +constexpr std::uint32_t Rsrc2OffchipLdsEnable = 1u << 18u; constexpr std::uint32_t Rsrc2UserSgprMask = (0x1fu << 1u) | (1u << 27u); bool ValidHalves(const Shader* front, const Shader* back) { @@ -120,15 +120,19 @@ std::uint32_t FrontProgramBytes(const char* function, const Shader* front) { return static_cast((end - 1) * 4); } -std::uint32_t MergeRsrc1(std::uint32_t back, std::uint32_t front) { - const auto vgprs = std::max(back & Rsrc1VgprMask, front & Rsrc1VgprMask); - const auto sgprs = std::max(back & Rsrc1SgprMask, front & Rsrc1SgprMask); - return (back & ~(Rsrc1VgprMask | Rsrc1SgprMask)) | vgprs | sgprs; -} - -std::uint32_t MergeRsrc2(const char* function, std::uint32_t back, std::uint32_t front) { - if ((front & ~Rsrc2UserSgprMask & ~back) != 0) Fail(function, "front half requires hardware state the back half does not enable"); - return (back & ~Rsrc2UserSgprMask) | (front & Rsrc2UserSgprMask); +std::array mergeGeometryResources(std::uint32_t frontRsrc1, std::uint32_t frontRsrc2, std::uint32_t backRsrc1, std::uint32_t backRsrc2) { + const auto frontVgprs = ((frontRsrc1 & Rsrc1VgprMask) + 1u) * 4u; + const auto backVgprs = ((backRsrc1 & Rsrc1VgprMask) + 1u) * 4u; + const auto frontTotal = frontVgprs + (frontRsrc2 >> 28u) * 8u; + const auto backTotal = backVgprs + (backRsrc2 >> 28u) * 8u; + const auto maxTotal = std::max(frontTotal, backTotal); + const auto shared = std::max(frontVgprs, backVgprs) >= maxTotal ? 0u : (maxTotal - std::min(frontTotal, backTotal) + 7u) / 64u; + const auto vgprs = std::max(frontRsrc1 & Rsrc1VgprMask, backRsrc1 & Rsrc1VgprMask); + const auto rsrc1Field = std::max((frontRsrc1 >> 29u) & 3u, (backRsrc1 >> 29u) & 3u); + const auto rsrc2Field = std::max((frontRsrc2 >> 16u) & 3u, (backRsrc2 >> 16u) & 3u); + const auto rsrc1 = (backRsrc1 & ~(Rsrc1VgprMask | (3u << 29u))) | vgprs | (rsrc1Field << 29u); + const auto rsrc2 = (backRsrc2 & ~(0xf0000000u | (3u << 16u) | Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable)) | ((shared & 15u) << 28u) | (rsrc2Field << 16u) | (frontRsrc2 & (Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable)); + return {rsrc1, rsrc2}; } const ShaderRegister* FindHalfRegister(const Shader* shader, std::uint32_t offset) { @@ -148,10 +152,14 @@ GeometryLayout ComputeGeometryLayout(const char* function, const Shader* front, const auto* frontRsrc1 = FindHalfRegister(front, SPI_SHADER_PGM_RSRC1_GS); const auto* frontRsrc2 = FindHalfRegister(front, SPI_SHADER_PGM_RSRC2_GS); if (frontRsrc1 == nullptr || frontRsrc2 == nullptr) Fail(function, "front half has no GS resource registers"); + const auto* backRsrc1 = FindHalfRegister(back, SPI_SHADER_PGM_RSRC1_GS); + const auto* backRsrc2 = FindHalfRegister(back, SPI_SHADER_PGM_RSRC2_GS); + if (backRsrc1 == nullptr || backRsrc2 == nullptr) Fail(function, "back half has no GS resource registers"); + const auto resources = mergeGeometryResources(frontRsrc1->value, frontRsrc2->value, backRsrc1->value, backRsrc2->value); layout.sh.assign(back->sh_registers, back->sh_registers + back->num_sh_registers); for (auto& reg : layout.sh) { - if (reg.offset == SPI_SHADER_PGM_RSRC1_GS) reg.value = MergeRsrc1(reg.value, frontRsrc1->value); - else if (reg.offset == SPI_SHADER_PGM_RSRC2_GS) reg.value = MergeRsrc2(function, reg.value, frontRsrc2->value); + if (reg.offset == SPI_SHADER_PGM_RSRC1_GS) reg.value = resources[0]; + else if (reg.offset == SPI_SHADER_PGM_RSRC2_GS) reg.value = resources[1]; } for (std::uint32_t i = 0; i < front->num_sh_registers; ++i) { const auto& reg = front->sh_registers[i]; diff --git a/core/libs/prx/libSceAgc/Misc/src/StaticBuffer.cpp b/core/libs/prx/libSceAgc/Misc/src/StaticBuffer.cpp new file mode 100644 index 0000000000..212715e35f --- /dev/null +++ b/core/libs/prx/libSceAgc/Misc/src/StaticBuffer.cpp @@ -0,0 +1,45 @@ +#include "prx/libSceAgc/Misc/include/StaticBuffer.hpp" + +#include +#include +#include "SceTypes.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" + +namespace { + +constexpr int GRAPHICS5_ERROR_INVALID_STATIC_BUFFER = static_cast(0x8a6c000bu); +constexpr std::uint32_t StaticBufferCount = 6; +constexpr std::size_t StaticBufferBytes = 16; +constexpr std::size_t StaticQwordOffset = 16; + +std::uint8_t* StaticTable() { + return static_cast(AgcDriverStaticTable_nid_postfix()); +} + +} + +extern "C" { + +int APS5_VABI sceAgcSetStaticBuffer(std::uint32_t index, const void* buffer) { + if (index >= StaticBufferCount) return GRAPHICS5_ERROR_INVALID_STATIC_BUFFER; + std::memcpy(StaticTable() + std::size_t{index} * StaticBufferBytes, buffer, StaticBufferBytes); + return 0; +} + +int APS5_VABI sceAgcGetStaticBuffer(std::uint32_t index, void* buffer) { + if (index >= StaticBufferCount) return GRAPHICS5_ERROR_INVALID_STATIC_BUFFER; + std::memcpy(buffer, StaticTable() + std::size_t{index} * StaticBufferBytes, StaticBufferBytes); + return 0; +} + +int APS5_VABI sceAgcSetStaticQword(std::uint32_t index, std::uint64_t value) { + std::memcpy(StaticTable() + StaticQwordOffset + std::size_t{index} * sizeof(value), &value, sizeof(value)); + return 0; +} + +int APS5_VABI sceAgcGetStaticQword(std::uint32_t index, std::uint64_t* value) { + std::memcpy(value, StaticTable() + StaticQwordOffset + std::size_t{index} * sizeof(*value), sizeof(*value)); + return 0; +} + +} diff --git a/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp b/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp index 4da4af5783..34ee4fd902 100644 --- a/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp @@ -45,34 +45,29 @@ extern "C" { int APS5_VABI sceAgcWaitRegMemPatchAddress(std::uint32_t* cmd, const volatile void* address) { auto* wait = Agc::Command::ValidateWait(cmd, __func__); const auto guestAddress = reinterpret_cast(address); - const auto alignment = ((wait[0] >> 8u) & 0xffu) == 0x3cu ? 4u : 8u; - Agc::Command::CheckAddress(guestAddress, alignment, __func__); - Agc::Command::CheckBits(guestAddress, 0xffffffffffffull, __func__); - cmd[2] = (cmd[2] & 0xffff0000u) | static_cast(guestAddress >> 32u); + const auto lowMask = ((wait[0] >> 8u) & 0xffu) == 0x3cu ? 3u : 7u; + cmd[2] = (cmd[2] & 0xffff0000u) | (static_cast(guestAddress >> 32u) & 0xffffu); cmd[3] = static_cast(guestAddress); - wait[2] = static_cast(guestAddress); - wait[3] = static_cast(guestAddress >> 32u); + wait[2] = (static_cast(guestAddress) & ~lowMask) | (wait[2] & lowMask); + wait[3] = (static_cast(guestAddress >> 32u) & 0x3ffffu) | (wait[3] & 0xfffc0000u); return 0; } -int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t reference) { +int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint32_t reference) { auto* wait = Agc::Command::ValidateWait(cmd, __func__); - Agc::Command::CheckBits(reference, 0xffffffffu, __func__); wait[4] = static_cast(reference); return 0; } -int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint64_t mask) { +int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint32_t mask) { auto* wait = Agc::Command::ValidateWait(cmd, __func__); - Agc::Command::CheckBits(mask, 0xffffffffu, __func__); wait[((wait[0] >> 8u) & 0xffu) == 0x3cu ? 5 : 6] = static_cast(mask); return 0; } int APS5_VABI sceAgcWaitRegMemPatchCompareFunction(std::uint32_t* cmd, std::uint8_t compareFunction) { auto* wait = Agc::Command::ValidateWait(cmd, __func__); - Agc::Command::Require(compareFunction <= 6, __func__, "invalid wait comparison"); - wait[1] = (wait[1] & ~7u) | compareFunction; + wait[1] = (wait[1] & ~7u) | (compareFunction & 7u); return 0; } diff --git a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp index 608c859ad8..65b337d335 100644 --- a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp @@ -53,12 +53,9 @@ std::uint32_t CreateInterpolantValueSplitF16(std::uint32_t psWord, const ShaderS : CreateInterpolantValue(psWord, gsSemantic); } -std::uint32_t UpdateInterpolantValue(std::uint32_t psWord, const ShaderSemantic* gsSemantic) { - const std::uint32_t f16 = (psWord >> 20u) & 0x3u; - if (f16 != 0 && f16 != InterpolantF16HiOnly && ((psWord >> 28u) & 0x3u) != ((psWord >> 30u) & 0x3u)) { - throw std::runtime_error(std::string(__func__) + ": DEFAULT_VAL of an f16 input with different low and high defaults is unknown"); - } - return CreateInterpolantValueSplitF16(psWord, gsSemantic); +std::uint32_t CreateInterpolantValueFlatDefault(std::uint32_t psWord, const ShaderSemantic* gsSemantic) { + const std::uint32_t value = CreateInterpolantValue(psWord, gsSemantic); + return ((psWord & 0x00700000u) == 0x00400000u) ? (value | 0x00000020u) : value; } int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue, bool fillIdentityTail) { @@ -72,7 +69,7 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* throw std::runtime_error(std::string(fn) + ": input semantic count exceeds 32"); } - AgcDriver::ShaderPreparationScope transaction; + AgcDriver::ShaderPreparationScope transaction({ps != nullptr ? gs : nullptr, ps}); std::array values{}; auto* output = regs; regs = values.data(); @@ -127,14 +124,20 @@ 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) { - return CreateInterpolantMapping(__func__, regs, gs, ps, UpdateInterpolantValue, false); +int APS5_VABI sceAgcUpdateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps) { + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16, false); +} + +int APS5_VABI sceAgcCreateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps) { + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueFlatDefault, true); +} + +int APS5_VABI sceAgcUpdateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps) { + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueFlatDefault, false); } } diff --git a/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp b/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp index 1044b410f4..59a43354c8 100644 --- a/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp @@ -22,7 +22,7 @@ extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister* context, ShaderRegist // The simple vertex/pixel pipeline combines two primitive context registers // and 32 interpolant registers. Publish only after both helpers succeed. - AgcDriver::ShaderPreparationScope transaction; + AgcDriver::ShaderPreparationScope transaction({vertex, pixel}); std::array contextValues{}; std::array primitiveValues{}; sceAgcCreatePrimState(contextValues.data(), primitiveValues.data(), nullptr, vertex, primitiveType); diff --git a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp index 587ae15a32..b62666315c 100644 --- a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp @@ -8,6 +8,7 @@ #include "SceShaders.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp" #include "prx/libSceAgc/Shader/include/ShaderUtils.hpp" #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" @@ -56,7 +57,9 @@ int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_ const std::array stages{hs, gs}; const bool patch = prim_type == static_cast(ShaderRegs::PrimitiveType::Patch); if (patch == ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_HS_BIT) != 0)) { + AgcDriver::ShaderPreparationScope transaction({hs, gs}); AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span(stages).subspan(hs == nullptr ? 1u : 0u), contextValues, primitiveValues); + transaction.Commit(); } if (cx_regs != nullptr) std::copy(contextValues.begin(), contextValues.end(), cx_regs); if (uc_regs != nullptr) std::copy(primitiveValues.begin(), primitiveValues.end(), uc_regs); diff --git a/core/libs/prx/libSceAgc/Shader/src/ShaderUtils.cpp b/core/libs/prx/libSceAgc/Shader/src/ShaderUtils.cpp index 8a783ea261..5a67826f8f 100644 --- a/core/libs/prx/libSceAgc/Shader/src/ShaderUtils.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/ShaderUtils.cpp @@ -1,4 +1,5 @@ #include +#include #include "prx/libSceAgc/Shader/include/ShaderUtils.hpp" using namespace ShaderRegs; diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index dad824f489..d7751bec51 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -5,52 +5,37 @@ extern "C" { -int APS5_VABI sceAgcAcbPushMarkerSpan() { +std::uint32_t* APS5_VABI sceAgcAcbPushMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAgcAcbSetMarkerSpan() { +std::uint32_t* APS5_VABI sceAgcAcbSetMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAgcAcquireMemSetEngine() { +int APS5_VABI sceAgcAcquireMemSetEngine(std::uint32_t* packet, std::uint8_t engine) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAgcDcbPushMarkerSpan() { +std::uint32_t* APS5_VABI sceAgcDcbPushMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAgcDcbSetMarkerSpan() { +std::uint32_t* APS5_VABI sceAgcDcbSetMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAgcGetDefaultCxStateFlat() { +int APS5_VABI sceAgcSetSemaphoreMemory(void* memory, std::uint64_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAgcSetSemaphoreMemory() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcCbMemsetExclusive() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcSetShaderInstrumentation() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcGetShaderInstrumentation() { +std::uint32_t* APS5_VABI sceAgcCbMemsetExclusive(CommandBuffer* buffer, void* destination, const void* pattern, std::uint64_t size) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceAgc/tests/AtomicGdsSize.cpp b/core/libs/prx/libSceAgc/tests/AtomicGdsSize.cpp new file mode 100644 index 0000000000..bbc5416bd3 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/AtomicGdsSize.cpp @@ -0,0 +1,25 @@ +#include "prx/libSceAgc/Command/include/Packet.hpp" + +#include +#include +#include + +extern "C" std::uint32_t APS5_VABI sceAgcAcbAtomicGdsGetSize(); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, + std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, + std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData); + +int main() { + constexpr std::uint32_t expectedSize = 11u * sizeof(std::uint32_t); + if (sceAgcAcbAtomicGdsGetSize() != expectedSize) { + std::fprintf(stderr, "ACB atomic GDS size does not match its 11-dword packet\n"); + return 1; + } + std::array words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + const auto* packet = sceAgcAcbAtomicGds_0900(&buffer, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + if (packet != words.data() || buffer.cursor_up != words.data() + expectedSize / sizeof(std::uint32_t)) { + std::fprintf(stderr, "ACB atomic GDS builder wrote a different size than its size accessor\n"); + return 1; + } +} diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 6fc0d679e7..88badb2189 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -16,17 +16,22 @@ extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std: extern "C" std::uint32_t* APS5_VABI sceAgcDcbClearState(CommandBuffer* buf, std::uint32_t command); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument); extern "C" std::uint32_t* APS5_VABI sceAgcAcbWaitUntilSafeForRendering(CommandBuffer* buf, std::uint32_t handle, std::uint32_t index); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument); extern "C" int APS5_VABI sceAgcSuspendPoint(); extern "C" int APS5_VABI sceAgcInit(std::uint32_t version); extern "C" void* APS5_VABI sceAgcGetRegisterDefaults(); +extern "C" void* APS5_VABI sceAgcGetRegisterDefaults2(std::uint32_t version); 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); extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirectMulti(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint32_t countIndirect, std::uint32_t maxCountOrCount, const volatile void* countAddress, std::uint32_t strideInBytes, std::uint64_t modifier); -extern "C" int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t reference); -extern "C" int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint64_t mask); +extern "C" int APS5_VABI sceAgcWaitRegMemPatchAddress(std::uint32_t* cmd, const volatile void* address); +extern "C" int APS5_VABI sceAgcWaitRegMemPatchCompareFunction(std::uint32_t* cmd, std::uint8_t compareFunction); +extern "C" int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint32_t reference); +extern "C" int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint32_t mask); extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(std::uint32_t** addr, std::uint32_t* cmd, int type); extern "C" std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer* buf, std::uint32_t offset, const std::uint32_t* values, std::uint32_t numValues); extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, std::uint32_t operation); @@ -45,6 +50,8 @@ extern "C" std::uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, cons extern "C" std::uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf); extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexBuffer(CommandBuffer* buf, std::uint64_t indexAddress); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint64_t attributeAddress, std::uint32_t attributeIndex); +extern "C" std::uint32_t APS5_VABI sceAgcDcbSetIndexIndirectArgsGetSize(); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetBaseIndirectArgs(CommandBuffer* buf, std::uint32_t shaderType, const volatile void* indirectBaseAddr); extern "C" std::uint32_t* APS5_VABI sceAgcSetNop(CommandBuffer* buf, std::uint32_t sizeDw); extern "C" int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId); @@ -52,8 +59,6 @@ extern "C" std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, namespace { -constexpr int SCE_AGC_ERROR_INVALID_ARGUMENT = static_cast(0x80890001); - void check(bool condition, const char* message) { if (!condition) { throw std::runtime_error(message); @@ -324,6 +329,20 @@ void testIndexBuffer() { throw std::runtime_error("misaligned index buffer was accepted"); } +void testIndexIndirectArgs() { + Storage storage; + const auto* packet = sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0x1111111122222220u, 0x4444u); + const std::array expected{0xc0029100u, 0x22222220u, 0x11111111u, 0x4444u}; + check(packet == storage.words.data() && std::equal(expected.begin(), expected.end(), packet), "index indirect args packet differs from the console's"); + check(sceAgcDcbSetIndexIndirectArgsGetSize() == expected.size() * sizeof(std::uint32_t), "index indirect args size differs from its packet"); + const auto* cleared = sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0, 0); + check(cleared == packet + expected.size() && cleared[0] == expected[0] && cleared[1] == 0 && cleared[2] == 0 && cleared[3] == 0, "null index attribute base was not written as 0"); + const auto before = storage.words; + expectFailure([&] { sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0x1111111122222228u, 0); }); + expectFailure([&] { sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0x1111111122222220u, 0x10000u); }); + check(storage.words == before && storage.buffer.cursor_up == cleared + expected.size(), "rejected index indirect args modified the buffer"); +} + void testIndirectBase() { Storage storage; alignas(8) std::uint32_t arguments[4]{}; @@ -426,6 +445,16 @@ void testFlip() { full.buffer.cursor_down = full.words.data() + 3; expectFailure([&] { sceAgcAcbWaitUntilSafeForRendering(&full.buffer, 1, 0); }); check(full.buffer.cursor_up == full.words.data(), "failed compute rendering wait advanced cursor"); + Storage acbFlip; + acbFlip.words.fill(0xdeadbeefu); + auto* computeFlip = sceAgcAcbSetFlip(&acbFlip.buffer, 0xfedcba98u, -2, 0x12345678u, -0x123456789abcdefLL); + check(computeFlip == acbFlip.words.data() && std::equal(expected.begin(), expected.end(), computeFlip), "compute flip packet mismatch"); + check(acbFlip.buffer.cursor_up == computeFlip + 6 && computeFlip[6] == 0xdeadbeefu, "compute flip overran allocation"); + expectFailure([] { sceAgcAcbSetFlip(nullptr, 1, 0, 1, 0); }); + Storage fullFlip; + fullFlip.buffer.cursor_down = fullFlip.words.data() + 5; + expectFailure([&] { sceAgcAcbSetFlip(&fullFlip.buffer, 1, 0, 1, 0); }); + check(fullFlip.buffer.cursor_up == fullFlip.words.data(), "failed compute flip advanced cursor"); check(sceAgcSuspendPoint() == 0, "empty suspend failed"); } @@ -463,8 +492,14 @@ void testRegisterRange() { check(packet == storage.words.data() + 6 && storage.buffer.cursor_up == storage.words.data() + 12, "incorrect populated register range allocation"); check(storage.words == expected, "register values were not copied correctly"); const auto* misaligned = reinterpret_cast(reinterpret_cast(values.data()) + 1); - expectFailure([&] { sceAgcCbSetShRegisterRangeDirect(&storage.buffer, 0x8c, misaligned, 4); }); - check(storage.words == expected && storage.buffer.cursor_up == storage.words.data() + 12, "misaligned register values modified command buffer"); + packet = sceAgcCbSetShRegisterRangeDirect(&storage.buffer, 0x8c, misaligned, 3); + expected[12] = 0xc0037600u; + expected[13] = 0x8c; + expected[14] = 0x02000000u; + expected[15] = 0x03000000u; + expected[16] = 0x04000000u; + check(packet == storage.words.data() + 12 && storage.buffer.cursor_up == storage.words.data() + 17, "incorrect misaligned register range allocation"); + check(storage.words == expected, "misaligned register values were not read byte for byte"); } void testPacketPayloadAddress() { @@ -498,18 +533,28 @@ void testMemory() { check(packet[8] == 0x55667788u && packet[9] == 0x11223344u && packet[12] == 2u, "wait reference or poll interval mismatch"); sceAgcWaitRegMemPatchReference(packet, 7); check(packet[8] == 7 && packet[9] == 0x11223344u, "reference patch changed the high word"); - const auto before = storage.words; - expectFailure([&] { sceAgcWaitRegMemPatchReference(packet, 0x100000000ull); }); - check(storage.words == before, "invalid memory operation modified packet memory"); auto* truncated = Agc::Command::WriteWait(&storage.buffer, 0, 3, 0, 0, &value, 0x100000000ull, 0xffffffff00000001ull, 32, __func__); check(truncated[8] == 0u && truncated[9] == 1u, "32-bit wait did not keep the low halves of the reference and mask"); sceAgcWaitRegMemPatchMask(packet, 0x0f0f0f0fu); check(packet[10] == 0x0f0f0f0fu && packet[11] == 0xffffffffu && packet[8] == 7, "64-bit mask patch changed the wrong word"); sceAgcWaitRegMemPatchMask(truncated, 0xff00u); check(truncated[9] == 0xff00u && truncated[8] == 0u && truncated[10] == 2u, "32-bit mask patch changed the wrong word"); + for (auto* waitPacket : {packet, truncated}) { + const auto lowMask = waitPacket == packet ? 7u : 3u; + waitPacket[6] |= lowMask; + waitPacket[7] = 0xa5fc0000u; + const auto tag = waitPacket[2] & 0xffff0000u; + const std::uintptr_t address = 0x1236abcd12345679ull; + check(sceAgcWaitRegMemPatchAddress(waitPacket, reinterpret_cast(address)) == 0, "wait address patch failed"); + check(waitPacket[2] == (tag | 0xabcdu) && waitPacket[3] == 0x12345679u, "wait wrapper address patch mismatch"); + check(waitPacket[6] == ((0x12345679u & ~lowMask) | lowMask) && waitPacket[7] == 0xa5feabcdu, "wait address patch did not preserve control bits"); + for (std::uint32_t comparison = 0; comparison <= 255; ++comparison) { + const auto control = waitPacket[5] & ~7u; + check(sceAgcWaitRegMemPatchCompareFunction(waitPacket, static_cast(comparison)) == 0, "wait comparison patch failed"); + check(waitPacket[5] == (control | (comparison & 7u)), "wait comparison patch changed control bits"); + } + } const auto beforeMask = storage.words; - expectFailure([&] { sceAgcWaitRegMemPatchMask(packet, 0x100000000ull); }); - expectFailure([&] { sceAgcWaitRegMemPatchMask(truncated, 0x100000000ull); }); expectFailure([&] { sceAgcWaitRegMemPatchMask(packet + 4, 1); }); check(storage.words == beforeMask, "invalid mask patch modified packet memory"); } @@ -624,12 +669,12 @@ void testDefaults() { check(sceAgcInit_0090(&state, 8) == 0 && state == 0x12345678, "AGC initialization failed or modified caller state"); check(sceAgcInit_0090(&state, 13) == 0 && state == 0x12345678, "AGC version 13 initialization changed caller state"); expectFailure([] { sceAgcInit_0090(nullptr, 8); }); - expectFailure([&] { sceAgcInit_0090(&state, 14); }); + check(sceAgcInit_0090(&state, 14) == 0 && state == 0x12345678, "out-of-range AGC initialization version changed caller state"); check(sceAgcInit(8) == 0, "AGC version initialization failed"); check(sceAgcInit(13) == 0, "AGC version 13 initialization failed"); - check(sceAgcInit(14) == SCE_AGC_ERROR_INVALID_ARGUMENT, "unsupported AGC version did not return invalid argument"); - check(sceAgcInit(0xffffffffu) == SCE_AGC_ERROR_INVALID_ARGUMENT, "maximum AGC version did not return invalid argument"); - check(sceAgcInit(0xffffc278u) == SCE_AGC_ERROR_INVALID_ARGUMENT, "pointer-valued AGC argument did not return invalid argument"); + check(sceAgcInit(14) == 0, "out-of-range AGC version did not return success"); + check(sceAgcInit(0xffffffffu) == 0, "maximum AGC version did not return success"); + check(sceAgcInit(0xffffc278u) == 0, "pointer-valued AGC argument did not return success"); for (std::uint32_t version = 0; version < 14; ++version) { for (const bool internal : {false, true}) { auto* first = Agc::Command::GetRegisterDefaults(version, internal, __func__); @@ -639,10 +684,79 @@ 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__); }); } +struct DefaultsType { + std::uint32_t key; + std::uint32_t bankIndex; +}; + +std::uint32_t colourTargetBlock(const void* root) { + const auto* bytes = static_cast(root); + ShaderRegister** contextBlocks = nullptr; + const DefaultsType* types = nullptr; + std::uint32_t count = 0; + std::memcpy(&contextBlocks, bytes, sizeof(contextBlocks)); + std::memcpy(&types, bytes + 0x30, sizeof(types)); + std::memcpy(&count, bytes + 0x38, sizeof(count)); + check(contextBlocks != nullptr && types != nullptr && count > 0, "register defaults root has no context blocks or types"); + std::uint32_t contexts = 0; + std::uint32_t matches = 0; + std::uint32_t block = 0; + for (std::uint32_t entry = 0; entry < count; ++entry) { + if ((types[entry].bankIndex & 3u) == 0) + ++contexts; + if (types[entry].key != 0x38e92c91u) + continue; + check((types[entry].bankIndex & 3u) == 0, "MRT0 defaults are not in the context bank"); + block = types[entry].bankIndex >> 2; + ++matches; + } + check(matches == 1 && block < contexts && contextBlocks[block] != nullptr, "MRT0 defaults key is missing, repeated or outside the context blocks"); + constexpr std::array offsets{0x318, 0x31b, 0x31c, 0x31d, 0x31e, 0x31f, 0x321, 0x323, 0x324, 0x325, 0x390, 0x398, 0x3a0, 0x3a8, 0x3b0, 0x3b8}; + for (std::size_t i = 0; i < offsets.size(); ++i) + check(contextBlocks[block][i].offset == offsets[i], "MRT0 defaults block does not hold the colour target registers"); + return block; +} + +void testDefaultsTypes() { + const auto* legacy = sceAgcGetRegisterDefaults(); + check(legacy != sceAgcGetRegisterDefaults2(13), "legacy register defaults reused the compact versioned table"); + check(colourTargetBlock(legacy) == 78, "MRT0 defaults are not block 78 of the 137-entry table"); + + const auto checkCompactTypes = [](const void* root, std::uint32_t count, std::uint32_t key, + std::uint32_t packedType, std::uint32_t extra) { + const auto* bytes = static_cast(root); + const std::uint32_t* types = nullptr; + std::uint32_t actualCount = 0; + std::memcpy(&types, bytes + 0x30, sizeof(types)); + std::memcpy(&actualCount, bytes + 0x38, sizeof(actualCount)); + check(types != nullptr && actualCount == count, "versioned register defaults have an invalid compact type table"); + check(types[0] == key && types[1] == packedType && types[2] == extra, + "versioned register defaults did not preserve their compact type entry"); + }; + checkCompactTypes(sceAgcGetRegisterDefaults2(0), 150, 0xe24f806du, 0x00040400u, 0); + checkCompactTypes(sceAgcGetRegisterDefaults2(10), 128, 0xe24f806du, 0x00040400u, 0); + checkCompactTypes(sceAgcGetRegisterDefaults2Internal(0), 15, 0xf58fea31u, 0x00040400u, 0); +} + void testEventWrite() { Storage storage; const auto* samplePacket = sceAgcDcbEventWrite(&storage.buffer, 0x38, reinterpret_cast(0x1000)); @@ -690,6 +804,7 @@ int main(int argc, char** argv) { try { if (argc == 2 && std::strcmp(argv[1], "defaults") == 0) { testDefaults(); + testDefaultsTypes(); LibcRunShutdown_nid_postfix(); std::puts("AGC initialization tests passed"); return 0; @@ -703,6 +818,7 @@ int main(int argc, char** argv) { testWorkloads(); testWorkloadStreams(); testIndexBuffer(); + testIndexIndirectArgs(); testIndirectBase(); testContextState(); testFlip(); @@ -713,6 +829,7 @@ int main(int argc, char** argv) { testAtomicGds(); testDebugRaiseException(); testDefaults(); + testDefaultsTypes(); LibcRunShutdown_nid_postfix(); std::puts("AGC command tests passed"); return 0; diff --git a/core/libs/prx/libSceAgc/tests/ContextStateSize.cpp b/core/libs/prx/libSceAgc/tests/ContextStateSize.cpp index 415b81e362..3de0a5eda6 100644 --- a/core/libs/prx/libSceAgc/tests/ContextStateSize.cpp +++ b/core/libs/prx/libSceAgc/tests/ContextStateSize.cpp @@ -8,6 +8,7 @@ #include extern "C" std::uint64_t APS5_VABI sceAgcDcbContextStateOpGetSize(std::uint32_t operation); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp(CommandBuffer* buf, std::uint32_t operation); extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, std::uint32_t operation); namespace { @@ -37,6 +38,15 @@ void testSizes() { check(sceAgcDcbContextStateOp_0100(&buffer, operation) == words.data(), "context state op did not start at the cursor"); const auto written = static_cast(buffer.cursor_up - words.data()) * sizeof(std::uint32_t); check(written == sizes[operation], "context state size differs from the written packets"); + + std::array unversionedWords{}; + CommandBuffer unversionedBuffer{unversionedWords.data(), unversionedWords.data() + unversionedWords.size(), + unversionedWords.data(), unversionedWords.data() + unversionedWords.size(), + nullptr, nullptr, 0}; + check(sceAgcDcbContextStateOp(&unversionedBuffer, operation) == unversionedWords.data(), "context state op did not start at the cursor"); + const auto unversionedWritten = static_cast(unversionedBuffer.cursor_up - unversionedWords.data()) * sizeof(std::uint32_t); + check(unversionedWritten == sizes[operation], "context state size differs from the written packets"); + check(words == unversionedWords, "unversioned context state op mismatch with versioned op"); } } diff --git a/core/libs/prx/libSceAgc/tests/EndOfShaderActionSize.cpp b/core/libs/prx/libSceAgc/tests/EndOfShaderActionSize.cpp index df935a0534..cf78ada4a3 100644 --- a/core/libs/prx/libSceAgc/tests/EndOfShaderActionSize.cpp +++ b/core/libs/prx/libSceAgc/tests/EndOfShaderActionSize.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/Shutdown.hpp" +#include #include #include #include @@ -11,6 +12,9 @@ extern "C" { std::uint32_t APS5_VABI sceAgcDcbQueueEndOfShaderActionGetSize(); std::uint32_t APS5_VABI sceAgcAcbQueueEndOfShaderActionGetSize(); std::uint32_t* APS5_VABI sceAgcCbReleaseMem(CommandBuffer*, std::uint8_t, std::uint16_t, std::uint8_t, std::uint8_t, const volatile Label*, std::uint8_t, std::uint64_t, std::uint16_t, std::uint16_t, std::uint8_t, std::uint32_t); +std::uint32_t APS5_VABI sceAgcDcbAtomicGdsGetSize(); +std::uint32_t* APS5_VABI sceAgcDcbAtomicGds_0900(CommandBuffer*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint16_t, std::uint16_t, std::uint32_t, std::uint32_t, std::uint64_t, std::uint64_t); +std::uint32_t* APS5_VABI sceAgcDcbAtomicGds(CommandBuffer*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint16_t, std::uint16_t, std::uint32_t, std::uint32_t, std::uint64_t, std::uint64_t); } namespace { @@ -19,19 +23,43 @@ void check(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); } +void testEndOfShaderActionSize() { + std::array words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + const auto* packet = sceAgcCbReleaseMem(&buffer, 0x2f, 0, 0, 0, nullptr, 0, 0, 0, 0, 0, 0); + const auto releaseBytes = static_cast((buffer.cursor_up - packet) * sizeof(std::uint32_t)); + check(((packet[0] >> 8u) & 0xffu) == 0x49u && releaseBytes == 32, "end-of-shader RELEASE_MEM size mismatch"); + check(sceAgcDcbQueueEndOfShaderActionGetSize() == releaseBytes, "DCB end-of-shader action size differs from its RELEASE_MEM"); + check(sceAgcDcbQueueEndOfShaderActionGetSize() == sceAgcAcbQueueEndOfShaderActionGetSize(), "DCB and ACB end-of-shader action sizes differ"); +} + +void testAtomicGds() { + constexpr std::uint32_t expectedBytes = 11u * sizeof(std::uint32_t); + check(sceAgcDcbAtomicGdsGetSize() == expectedBytes, "DCB atomic GDS size does not match 11-dword packet"); + + std::array words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + const auto* packet = sceAgcDcbAtomicGds_0900(&buffer, 0x261u, 1, 3, 7, 0x100, 0x200, 0x00ff00ffu, 0x12345678u, 0x1122334455667788ull, 0x99aabbccddeeff00ull); + check(packet != nullptr && packet == words.data(), "DCB atomic GDS returned unexpected packet pointer"); + check(buffer.cursor_up == words.data() + 11, "DCB atomic GDS cursor advance does not match 11 dwords"); + check(packet[0] == 0xc0091d00u, "DCB atomic GDS header opcode mismatch"); + + std::array unversionedWords{}; + CommandBuffer unversionedBuffer{unversionedWords.data(), unversionedWords.data() + unversionedWords.size(), unversionedWords.data(), unversionedWords.data() + unversionedWords.size(), nullptr, nullptr, 0}; + const auto* unversionedPacket = sceAgcDcbAtomicGds(&unversionedBuffer, 0x261u, 1, 3, 7, 0x100, 0x200, 0x00ff00ffu, 0x12345678u, 0x1122334455667788ull, 0x99aabbccddeeff00ull); + check(unversionedPacket != nullptr && unversionedPacket == unversionedWords.data(), "unversioned DCB atomic GDS returned unexpected packet pointer"); + check(unversionedBuffer.cursor_up == unversionedWords.data() + 11, "unversioned DCB atomic GDS cursor advance does not match 11 dwords"); + check(std::equal(words.begin(), words.begin() + 11, unversionedWords.begin()), "versioned and unversioned DCB atomic GDS emitted different packets"); +} + } int main() { try { - std::array words{}; - CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; - const auto* packet = sceAgcCbReleaseMem(&buffer, 0x2f, 0, 0, 0, nullptr, 0, 0, 0, 0, 0, 0); - const auto releaseBytes = static_cast((buffer.cursor_up - packet) * sizeof(std::uint32_t)); - check(((packet[0] >> 8u) & 0xffu) == 0x49u && releaseBytes == 32, "end-of-shader RELEASE_MEM size mismatch"); - check(sceAgcDcbQueueEndOfShaderActionGetSize() == releaseBytes, "DCB end-of-shader action size differs from its RELEASE_MEM"); - check(sceAgcDcbQueueEndOfShaderActionGetSize() == sceAgcAcbQueueEndOfShaderActionGetSize(), "DCB and ACB end-of-shader action sizes differ"); + testEndOfShaderActionSize(); + testAtomicGds(); LibcRunShutdown_nid_postfix(); - std::puts("AGC end-of-shader action size tests passed"); + std::puts("AGC end-of-shader action and atomic GDS size tests passed"); return 0; } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); diff --git a/core/libs/prx/libSceAgc/tests/InstrumentationQuerySize.cpp b/core/libs/prx/libSceAgc/tests/InstrumentationQuerySize.cpp new file mode 100644 index 0000000000..7f75026d39 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/InstrumentationQuerySize.cpp @@ -0,0 +1,48 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" + +#include +#include +#include + +extern "C" { +int APS5_VABI sceAgcSetShaderInstrumentation(std::uint32_t); +std::uint32_t APS5_VABI sceAgcGetShaderInstrumentation(); +std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize(std::int32_t); +std::uint32_t APS5_VABI sceAgcDcbEndOcclusionQueryGetSize(); +} + +namespace { + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void testShaderInstrumentation() { + check(sceAgcGetShaderInstrumentation() == 0, "shader instrumentation starts cleared"); + check(sceAgcSetShaderInstrumentation(0x80000005u) == 0, "setting shader instrumentation failed"); + check(sceAgcGetShaderInstrumentation() == 0x80000005u, "shader instrumentation flags were not kept"); + check(sceAgcSetShaderInstrumentation(0) == 0 && sceAgcGetShaderInstrumentation() == 0, "shader instrumentation was not cleared"); +} + +void testOcclusionQuerySizes() { + check(sceAgcDcbBeginOcclusionQueryGetSize(0) == 0x10u, "begin occlusion query size for type 0"); + check(sceAgcDcbBeginOcclusionQueryGetSize(1) == 0x120u, "begin occlusion query size for type 1"); + check(sceAgcDcbBeginOcclusionQueryGetSize(2) == 0, "begin occlusion query size for an unknown type"); + check(sceAgcDcbEndOcclusionQueryGetSize() == 0x10u, "end occlusion query size"); +} + +} + +int main() { + try { + testShaderInstrumentation(); + testOcclusionQuerySizes(); + LibcRunShutdown_nid_postfix(); + std::puts("AGC shader instrumentation and occlusion query size tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp index b176809a48..1b38f7df08 100644 --- a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp @@ -12,8 +12,10 @@ #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); +extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps); +extern "C" int APS5_VABI sceAgcUpdateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps); namespace { @@ -61,14 +63,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 +96,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 +111,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 +144,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,17 +156,50 @@ 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); }); - check(std::memcmp(update.data(), untouched.data(), sizeof(update)) == 0, "rejected update wrote registers"); - inputs[index].default_value = inputs[index].default_value_hi; + inputs[1].default_value = 0; + inputs[3].default_value = 0; + update = filled(); + check(sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps) == 0, "update with split f16 defaults failed"); + check(update[1].value == 0x01380007u && update[3].value == 0x03680409u, "f16 DEFAULT_VAL does not come from the low default"); +} + +void testUnversionedMapping() { + std::array inputs{ + semantic(1, 0, 2), semantic(2, 0, 2), semantic(5, 0, 2), semantic(3, 0, 0), semantic(4, 0, 3)}; + inputs[0].is_flat_shaded = 1; + inputs[0].default_value = 1; + inputs[0].default_value_hi = 3; + inputs[1].default_value_hi = 1; + inputs[2].is_flat_shaded = 1; + inputs[2].default_value_hi = 2; + inputs[3].is_flat_shaded = 1; + inputs[3].default_value = 2; + std::array outputs{semantic(1, 7, 3), semantic(2, 4, 1), semantic(3, 9, 0), semantic(4, 2, 1)}; + Shader gs{}; + gs.output_semantics = outputs.data(); + gs.num_output_semantics = static_cast(outputs.size()); + Shader ps{}; + ps.input_semantics = inputs.data(); + ps.num_input_semantics = static_cast(inputs.size()); + const std::array flatDefault{0x02680527u, 0x02380024u, 0x02580020u, 0x00000629u, 0x03180002u}; + const std::size_t written = inputs.size() * sizeof(ShaderRegister); + const auto untouched = filled(); + + auto create = filled(); + check(sceAgcCreateInterpolantMapping_nid_postfix(create.data(), &gs, &ps) == 0, "unversioned mapping failed"); + for (std::uint32_t i = 0; i < inputs.size(); ++i) { + check(create[i].offset == ShaderRegs::SPI_PS_INPUT_CNTL_0 + i && create[i].value == flatDefault[i], "unversioned mapping changed"); } + checkIdentity(create, static_cast(inputs.size()), "unused interpolants are not the identity"); + + auto update = filled(); + check(sceAgcUpdateInterpolantMapping_nid_postfix(update.data(), &gs, &ps) == 0, "unversioned update failed"); + check(std::memcmp(update.data(), create.data(), written) == 0, "unversioned update differs from its mapping"); + check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "unversioned update wrote past its inputs"); } void testRejections() { @@ -177,15 +212,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); }); } } @@ -208,8 +243,9 @@ int main() { testIdentity(); testMapping(); testUpdateMapping(); + testUnversionedMapping(); testRejections(); - check(mappings == 7u && links == 5u, "mapping preparation call count changed"); + check(mappings == 10u && links == 8u, "mapping preparation call count changed"); LibcRunShutdown_nid_postfix(); std::puts("AGC interpolant mapping tests passed"); return 0; diff --git a/core/libs/prx/libSceAgc/tests/PayloadRange.cpp b/core/libs/prx/libSceAgc/tests/PayloadRange.cpp index 94f33c2ada..f920eb0384 100644 --- a/core/libs/prx/libSceAgc/tests/PayloadRange.cpp +++ b/core/libs/prx/libSceAgc/tests/PayloadRange.cpp @@ -9,6 +9,7 @@ #include extern "C" int APS5_VABI sceAgcGetDataPacketPayloadRange(SceAgcMemoryRange* range, std::uint32_t* cmd, int type); +extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddress(std::uint32_t** addr, std::uint32_t* cmd, int type); extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(std::uint32_t** addr, std::uint32_t* cmd, int type); namespace { @@ -44,6 +45,9 @@ void testRanges() { std::uint32_t* address = nullptr; check(sceAgcGetDataPacketPayloadAddress_0090(&address, words.data(), type) == 0 && address == range.base, "payload range base differs from the payload address"); + std::uint32_t* unversionedAddress = nullptr; + check(sceAgcGetDataPacketPayloadAddress(&unversionedAddress, words.data(), type) == 0 && unversionedAddress == address, + "unversioned payload address differs from versioned payload address"); } } @@ -60,6 +64,10 @@ void testRejections() { misalignedRange = reinterpret_cast(storage.data() + 2); } expectFailure([&] { sceAgcGetDataPacketPayloadRange(misalignedRange, words.data(), 1); }); + std::uint32_t* address = nullptr; + expectFailure([&] { sceAgcGetDataPacketPayloadAddress(nullptr, words.data(), 1); }); + expectFailure([&] { sceAgcGetDataPacketPayloadAddress(&address, nullptr, 1); }); + expectFailure([&] { sceAgcGetDataPacketPayloadAddress(&address, misaligned, 0); }); } } diff --git a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp index a4ac992501..5e6be0c709 100644 --- a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp +++ b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp @@ -8,6 +8,8 @@ extern "C" { std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterDirect(CommandBuffer*, ShaderRegister); std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t); +std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t); +std::uint32_t* APS5_VABI sceAgcCbSetUcRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t); std::uint32_t* APS5_VABI sceAgcDcbSetCxRegisterDirect(CommandBuffer*, ShaderRegister); std::uint32_t* APS5_VABI sceAgcDcbSetShRegisterDirect(CommandBuffer*, ShaderRegister); std::uint32_t* APS5_VABI sceAgcDcbSetUcRegisterDirect(CommandBuffer*, ShaderRegister); @@ -92,11 +94,37 @@ void testConfig() { expectFailure([&] { sceAgcDcbSetCfRegisterDirect(nullptr, {0, 1}); }); expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x100u, values.data(), 0); }); expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0xffffu, values.data(), 2); }); - const auto* misaligned = reinterpret_cast(reinterpret_cast(values.data()) + 1); - expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x100u, misaligned, 1); }); check(storage.words == before && storage.buffer.cursor_up == reserved + 4, "invalid config register write modified buffer"); } +void testRangeFromAnyByte() { + const std::array writers{sceAgcCbSetShRegisterRangeDirect, sceAgcCbSetUcRegisterRangeDirect, sceAgcDcbSetCfRegisterRangeDirect}; + const std::array headers{0xc0047600u, 0xc0047900u, 0xc0046800u}; + alignas(16) std::array bytes{}; + for (std::size_t i = 0; i < bytes.size(); ++i) bytes[i] = static_cast(0x10u + i); + struct Case { + std::size_t offset; + std::array values; + }; + const std::array cases{ + Case{1, {0x14131211u, 0x18171615u, 0x1c1b1a19u, 0x201f1e1du}}, + Case{2, {0x15141312u, 0x19181716u, 0x1d1c1b1au, 0x21201f1eu}}, + Case{3, {0x16151413u, 0x1a191817u, 0x1e1d1c1bu, 0x2221201fu}}, + Case{13, {0x201f1e1du, 0x24232221u, 0x28272625u, 0x2c2b2a29u}}, + }; + for (std::size_t i = 0; i < writers.size(); ++i) { + for (const auto& entry : cases) { + Storage storage; + storage.words.fill(0xabcdef01u); + const auto* values = reinterpret_cast(bytes.data() + entry.offset); + auto* packet = writers[i](&storage.buffer, 0x40u, values, 4); + check(packet == storage.words.data() && storage.buffer.cursor_up == packet + 6, "unaligned register range allocation mismatch"); + check(packet[0] == headers[i] && packet[1] == 0x40u && std::equal(entry.values.begin(), entry.values.end(), packet + 2), "unaligned register values were not read byte for byte"); + check(packet[6] == 0xabcdef01u, "unaligned register range overwrote the following word"); + } + } +} + void testIndirect() { const std::array writers{sceAgcDcbSetCxRegistersIndirect, sceAgcDcbSetShRegistersIndirect, sceAgcDcbSetUcRegistersIndirect}; const std::array sizes{sceAgcDcbSetCxRegistersIndirectGetSize, sceAgcDcbSetShRegistersIndirectGetSize, sceAgcDcbSetUcRegistersIndirectGetSize}; @@ -193,6 +221,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); }); @@ -206,6 +238,7 @@ int main() { try { testDirect(); testConfig(); + testRangeFromAnyByte(); testIndirect(); testIndirectPlaceholder(); testDirectList(); diff --git a/core/libs/prx/libSceAgc/tests/Semaphore.cpp b/core/libs/prx/libSceAgc/tests/Semaphore.cpp index 2e9e5be0e7..789a303ad8 100644 --- a/core/libs/prx/libSceAgc/tests/Semaphore.cpp +++ b/core/libs/prx/libSceAgc/tests/Semaphore.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgc/Command/include/Packet.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/Shutdown.hpp" @@ -13,6 +14,7 @@ extern "C" int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes); extern "C" int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label); +extern "C" std::uint32_t* APS5_VABI sceAgcCbMemSemaphore(CommandBuffer* buf, const volatile void* address, std::uint32_t operation, std::uint32_t modifier, std::uint32_t value); namespace { @@ -68,8 +70,8 @@ void fillStorage() { void testUnregistered() { std::array labels{storage.get(), storage.get(), storage.get()}; - expectFailure([&] { sceAgcGetSemaphoreLabel(0, &labels[1]); }); - expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); }); + check(sceAgcGetSemaphoreLabel(0, &labels[1]) == static_cast(0x8a6c0049u), "unregistered label error mismatch"); + check(sceAgcGetSemaphoreLabel(0, nullptr) == static_cast(0x8a6c0049u), "unregistered null label error mismatch"); check(labels[0] == storage.get() && labels[1] == storage.get() && labels[2] == storage.get(), "a rejected semaphore label changed the output"); } @@ -78,10 +80,10 @@ void testRegistrationRejections() { expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, memoryBytes); }); expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); }); expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), 0); }); - expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + 1, memoryBytes); }); - expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + labelBytes, blockBytes); }); - expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes - 1u); }); - expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes + labelBytes); }); + check(sceAgcSetAmmSemaphoreMemory(memory() + 1, memoryBytes) == static_cast(0x8a6c0002u), "unaligned semaphore memory error mismatch"); + check(sceAgcSetAmmSemaphoreMemory(memory() + labelBytes, blockBytes) == static_cast(0x8a6c0002u), "unaligned semaphore memory error mismatch"); + check(sceAgcSetAmmSemaphoreMemory(memory(), blockBytes - 1u) == static_cast(0x8a6c0002u), "unaligned semaphore memory error mismatch"); + check(sceAgcSetAmmSemaphoreMemory(memory(), blockBytes + labelBytes) == static_cast(0x8a6c0002u), "unaligned semaphore memory error mismatch"); expectFailure([] { const auto top = std::numeric_limits::max() & ~static_cast(blockBytes - 1u); sceAgcSetAmmSemaphoreMemory(reinterpret_cast(top), memoryBytes); @@ -90,18 +92,8 @@ void testRegistrationRejections() { testUnregistered(); } -void testUnsettledClearing() { - for (const std::uint64_t offset : {5u * labelBytes + counterBytes, memoryBytes - 1u}) { - memory()[offset] = 0x77u; - const auto before = snapshot(); - expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); }); - checkUnchanged(before, "a registration with unsettled clearing changed memory"); - testUnregistered(); - memory()[offset] = 0; - } -} - void testRegistration() { + std::fill(memory(), memory() + memoryBytes, std::uint8_t{0x77}); const auto before = snapshot(); check(sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes) == 0, "registering the AMM semaphore memory failed"); check(std::all_of(memory(), memory() + memoryBytes, [](std::uint8_t byte) { return byte == 0; }), "registration did not clear the semaphore counters"); @@ -121,7 +113,7 @@ void testLabels() { void testLabelRejections() { for (const std::uint32_t index : {labelCount, labelCount + 1u, std::numeric_limits::max()}) { void* label = storage.get(); - expectFailure([&] { sceAgcGetSemaphoreLabel(index, &label); }); + check(sceAgcGetSemaphoreLabel(index, &label) == static_cast(0x8a6c000bu), "out-of-range label error mismatch"); check(label == storage.get(), "a rejected semaphore label changed the output"); } expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); }); @@ -130,14 +122,64 @@ void testLabelRejections() { void testRepeatedRegistration() { std::fill(memory(), memory() + memoryBytes, std::uint8_t{0x3c}); const auto before = snapshot(); - expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); }); - expectFailure([] { sceAgcSetAmmSemaphoreMemory(storage.get(), blockBytes); }); - expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); }); + check(sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes) == static_cast(0x8a6c0048u), "repeated registration error mismatch"); + check(sceAgcSetAmmSemaphoreMemory(storage.get(), blockBytes) == static_cast(0x8a6c0048u), "repeated registration error mismatch"); + check(sceAgcSetAmmSemaphoreMemory(nullptr, 0) == static_cast(0x8a6c0048u), "repeated registration error mismatch"); checkUnchanged(before, "a repeated registration changed memory"); void* label = nullptr; check(sceAgcGetSemaphoreLabel(labelCount - 1u, &label) == 0 && label == memory() + memoryBytes - labelBytes, "a repeated registration replaced the semaphore memory"); } +void testMemSemaphore() { + void* label = nullptr; + check(sceAgcGetSemaphoreLabel(0, &label) == 0 && label != nullptr, "failed to get label for memory semaphore test"); + const auto guestAddress = reinterpret_cast(label); + + { + std::array words{}; + words.fill(0xdeadbeefu); + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + const auto* packet = sceAgcCbMemSemaphore(&buffer, reinterpret_cast(0x1111111122222222ull), 0x33333333u, 0x44444444u, 0); + check(packet == words.data(), "packet address mismatch"); + check(packet[0] == 0xc0023900u, "MEM_SEMAPHORE header opcode mismatch"); + check(packet[1] == 0x22222220u, "MEM_SEMAPHORE address low mismatch"); + check(packet[2] == 0x11111111u, "MEM_SEMAPHORE address high mismatch"); + check(packet[3] == 0x80100000u, "MEM_SEMAPHORE control word mismatch"); + check(buffer.cursor_up == words.data() + 4, "cursor did not advance by 4 words"); + check(words[4] == 0xdeadbeefu, "words past packet modified"); + } + + { + std::array words{}; + words.fill(0xdeadbeefu); + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + const auto* packet = sceAgcCbMemSemaphore(&buffer, reinterpret_cast(1ull), 2, 3, 4); + check(packet == words.data(), "packet address mismatch"); + check(packet[0] == 0xc0023900u, "MEM_SEMAPHORE header opcode mismatch"); + check(packet[1] == 0u, "MEM_SEMAPHORE address low mismatch"); + check(packet[2] == 0u, "MEM_SEMAPHORE address high mismatch"); + check(packet[3] == 0x80100000u, "MEM_SEMAPHORE control word mismatch"); + check(buffer.cursor_up == words.data() + 4, "cursor did not advance by 4 words"); + check(words[4] == 0xdeadbeefu, "words past packet modified"); + } + + { + std::array words{}; + words.fill(0xdeadbeefu); + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + const auto* packet = sceAgcCbMemSemaphore(&buffer, label, 0x12345678u, 0x9abcdef0u, 0x55aa55aau); + check(packet == words.data(), "packet address mismatch"); + check(packet[0] == 0xc0023900u, "MEM_SEMAPHORE header opcode mismatch"); + check(packet[1] == static_cast(guestAddress & ~static_cast(0xfu)), "MEM_SEMAPHORE address low mismatch"); + check(packet[2] == static_cast(guestAddress >> 32u), "MEM_SEMAPHORE address high mismatch"); + check(packet[3] == 0x80100000u, "MEM_SEMAPHORE control word mismatch"); + check(buffer.cursor_up == words.data() + 4, "cursor did not advance by 4 words"); + check(words[4] == 0xdeadbeefu, "words past packet modified"); + } + + expectFailure([&] { sceAgcCbMemSemaphore(nullptr, label, 0, 0, 0); }); +} + } int main() { @@ -146,9 +188,9 @@ int main() { fillStorage(); testUnregistered(); testRegistrationRejections(); - testUnsettledClearing(); testRegistration(); testLabels(); + testMemSemaphore(); testLabelRejections(); testRepeatedRegistration(); storage.reset(); diff --git a/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp b/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp index 2c901efc5b..7b66b5e57c 100644 --- a/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp +++ b/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp @@ -147,7 +147,7 @@ void testFusion(std::size_t misalignment, std::uint32_t terminator = SSetpcS6) { const auto* frontLo = find(fused, 0x0C8u); check(backLo != nullptr && backLo->value == static_cast((base + 256) >> 8u), "back program address was not patched"); check(frontLo != nullptr && frontLo->value == static_cast(base >> 8u), "fused program address was not patched"); - check(find(fused, 0x08Au)->value == ((1u << 24u) | (2u << 6u) | 5u), "RSRC1 register counts were not merged"); + check(find(fused, 0x08Au)->value == ((1u << 24u) | (1u << 6u) | 5u), "RSRC1 register counts were not merged"); check(find(fused, 0x08Bu)->value == ((6u << 1u) | (1u << 7u) | (1u << 15u)), "RSRC2 user SGPRs were not taken from the front half"); check(find(fused, 0x0B2u) != nullptr && find(fused, 0x0B2u)->value == 0x55u, "front-only SH register was lost"); check(find(fused, 0x080u) == nullptr, "front program checksum was kept"); @@ -195,6 +195,65 @@ void testScratchReuse() { check(reinterpret_cast(changed.code) != firstCode && word(reinterpret_cast(firstCode), 64) == SMovS0, "changed halves reused the earlier fused program"); } +void testOffchipLdsEnable() { + constexpr std::uint32_t offchip = 1u << 18u; + for (const auto frontEnable : {0u, offchip}) { + for (const auto backEnable : {0u, offchip}) { + Halves halves; + halves.frontSh[2].value |= frontEnable; + halves.backSh[3].value |= backEnable; + SizeAlign size{}; + check(sceAgcGetFusedShaderSize_0080(&size, &halves.front, &halves.back) == 0, "offchip LDS enable rejected by the size query"); + std::vector storage(size.m_size + 512, 0xcd); + Shader fused{}; + check(sceAgcFuseShaderHalves_0200(&fused, &halves.front, &halves.back, aligned(storage, 256)) == 0, "offchip LDS enable rejected by fusion"); + const auto* resource = find(fused, 0x08bu); + check(resource != nullptr && resource->value == ((6u << 1u) | (1u << 7u) | (1u << 15u) | frontEnable), "fusion did not take offchip LDS enable and user SGPRs from the front half"); + } + } + Halves reported; + reported.frontSh[2].value = 0x00070004u; + reported.backSh[3].value = 0x00ab0020u; + SizeAlign size{}; + check(sceAgcGetFusedShaderSize_0080(&size, &reported.front, &reported.back) == 0, "reported shader resources rejected by size query"); + std::vector storage(size.m_size + 512, 0xcd); + Shader fused{}; + check(sceAgcFuseShaderHalves_0200(&fused, &reported.front, &reported.back, aligned(storage, 256)) == 0, "reported shader resources rejected by fusion"); + check(find(fused, 0x08bu)->value == 0x00af0004u, "reported shader resources did not retain the front offchip LDS enable"); +} + +void testResourceMerging() { + struct Case { + std::uint32_t front1; + std::uint32_t front2; + std::uint32_t back1; + std::uint32_t back2; + std::uint32_t expected1; + std::uint32_t expected2; + }; + const Case cases[] = { + {0u, 0x80000000u, 0u, 0u, 0u, 0x10000000u}, + {1u, 0xf0000000u, 3u, 0u, 3u, 0x10000000u}, + {0u, 0xf0000000u, 0u, 0xf0000000u, 0u, 0u}, + {0x400003c1u, 0x0006000cu, 0x61000043u, 0x00018001u, 0x61000043u, 0x0006800du}, + {0x60000007u, 0x00010000u, 0x21000041u, 0x00020000u, 0x61000047u, 0x00020000u} + }; + for (const auto& entry : cases) { + Halves halves; + halves.frontSh[1].value = entry.front1; + halves.frontSh[2].value = entry.front2; + halves.backSh[2].value = entry.back1; + halves.backSh[3].value = entry.back2; + SizeAlign size{}; + check(sceAgcGetFusedShaderSize_0080(&size, &halves.front, &halves.back) == 0, "resource merge size query failed"); + std::vector scratch(size.m_size); + Shader fused{}; + check(sceAgcFuseShaderHalves_0200(&fused, &halves.front, &halves.back, scratch.data()) == 0, "resource merge failed"); + check(find(fused, 0x08au)->value == entry.expected1, "resource register 1 merge mismatch"); + check(find(fused, 0x08bu)->value == entry.expected2, "resource register 2 merge mismatch"); + } +} + void testRejections() { Halves halves; SizeAlign size{}; @@ -222,11 +281,6 @@ void testRejections() { noTrailer.frontCode[5] = 0; expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &noTrailer.front, &noTrailer.back); }); } - { - Halves hardware; - hardware.frontSh[2].value |= 1u << 16u; - expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &hardware.front, &hardware.back); }); - } { Halves constants; constants.back.embedded_constant_buffer_size_dqw = 1; @@ -277,6 +331,8 @@ int main() { testFusion(4); testFusion(0, SSwappcNullS6); testScratchReuse(); + testOffchipLdsEnable(); + testResourceMerging(); testRejections(); testHullHalves(); LibcRunShutdown_nid_postfix(); 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/libSceAgc/tests/StaticBuffer.cpp b/core/libs/prx/libSceAgc/tests/StaticBuffer.cpp new file mode 100644 index 0000000000..c7a3c162d6 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/StaticBuffer.cpp @@ -0,0 +1,64 @@ +#include "prx/libSceAgc/Misc/include/StaticBuffer.hpp" +#include "prx/libc/include/GuestArena.hpp" + +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uintptr_t StaticTable = 0xfe0040000ull + GuestArena::GuestAddressOffset; +constexpr int InvalidStaticBuffer = static_cast(0x8a6c000bu); + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::array slot(std::uint32_t index) { + std::array words{}; + std::memcpy(words.data(), reinterpret_cast(StaticTable + index * 16u), sizeof(words)); + return words; +} + +void testBuffers() { + std::array fetchShaderTable{}; + check(sceAgcGetStaticBuffer(0, fetchShaderTable.data()) == 0, "reading static buffer 0 failed"); + check(fetchShaderTable == slot(0) && fetchShaderTable[0] >> 48u == 16u, "static buffer 0 is not the driver's fetch shader table"); + const std::array written{0x1122334455667788ull, 0x99aabbccddeeff00ull}; + check(sceAgcSetStaticBuffer(5, written.data()) == 0, "writing static buffer 5 failed"); + check(slot(5) == written, "static buffer 5 was not stored at the sixth 16-byte slot"); + std::array read{}; + check(sceAgcGetStaticBuffer(5, read.data()) == 0 && read == written, "static buffer 5 did not read back"); + check(sceAgcSetStaticBuffer(6, written.data()) == InvalidStaticBuffer, "static buffer index 6 was accepted"); + check(sceAgcGetStaticBuffer(6, read.data()) == InvalidStaticBuffer, "static buffer index 6 was read"); + check(slot(0) == fetchShaderTable, "static buffer writes changed slot 0"); +} + +void testQwords() { + check(sceAgcSetStaticQword(0, 0x0123456789abcdefull) == 0, "writing static qword 0 failed"); + check(sceAgcSetStaticQword(3, 0xfedcba9876543210ull) == 0, "writing static qword 3 failed"); + check(slot(1)[0] == 0x0123456789abcdefull && slot(2)[1] == 0xfedcba9876543210ull, "static qwords are not stored after the first 16-byte slot"); + std::uint64_t value = 0; + check(sceAgcGetStaticQword(3, &value) == 0 && value == 0xfedcba9876543210ull, "static qword 3 did not read back"); + std::array buffer{}; + check(sceAgcGetStaticBuffer(1, buffer.data()) == 0 && buffer[0] == 0x0123456789abcdefull, "static qword 0 is not the first half of static buffer 1"); +} + +} + +int main() { +#ifdef ANYPS5_ENABLE_ASAN + std::puts("skipped: the static table lives at the PS5's fixed 0xfe0040000, which an ANYPS5_ENABLE_ASAN build cannot map"); + return 0; +#endif + try { + testBuffers(); + testQwords(); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 3ea2ccf2c8..7902207afd 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -58,6 +58,39 @@ add_custom_command( ) set(agcMeshArgumentsShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/MeshArguments.comp) +foreach(agcReadbackKind IN ITEMS Uint Depth) + string(TOUPPER ${agcReadbackKind} agcReadbackSymbol) + if(agcReadbackKind STREQUAL "Depth") + set(agcReadbackDepth 1) + else() + set(agcReadbackDepth 0) + endif() + set(agcReadbackSpv ${agcTextureDetileGeneratedDir}/MultisampleReadback${agcReadbackKind}.spv) + set(agcReadbackHeader ${agcTextureDetileGeneratedDir}/MultisampleReadback${agcReadbackKind}_spv.h) + add_custom_command( + OUTPUT "${agcReadbackHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -DREAD_DEPTH=${agcReadbackDepth} -o "${agcReadbackSpv}" "${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/MultisampleSampledReadback.comp" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcReadbackSpv} -DOUTPUT=${agcReadbackHeader} + -DSYMBOL=MULTISAMPLE_READBACK_${agcReadbackSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/MultisampleSampledReadback.comp" glslang-standalone + VERBATIM + ) + list(APPEND agcReadbackHeaders ${agcReadbackHeader}) +endforeach() + +set(agcMultisampleUploadSpv ${agcTextureDetileGeneratedDir}/MultisampleUpload.spv) +set(agcMultisampleUploadHeader ${agcTextureDetileGeneratedDir}/MultisampleUpload_spv.h) +add_custom_command( + OUTPUT "${agcMultisampleUploadHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcMultisampleUploadSpv}" "${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/MultisampleUpload.comp" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcMultisampleUploadSpv} -DOUTPUT=${agcMultisampleUploadHeader} + -DSYMBOL=MULTISAMPLE_UPLOAD_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/MultisampleUpload.comp" glslang-standalone + VERBATIM +) + set(agcMeshArgumentsShaderSpv ${agcTextureDetileGeneratedDir}/MeshArguments.spv) set(agcMeshArgumentsShaderHeader ${agcTextureDetileGeneratedDir}/MeshArguments_spv.h) add_custom_command( @@ -83,6 +116,54 @@ add_custom_command( VERBATIM ) +set(agcTextureRepackShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/TextureRepack.comp) +set(agcTextureRepackShaderSpv ${agcTextureDetileGeneratedDir}/TextureRepack.spv) +set(agcTextureRepackShaderHeader ${agcTextureDetileGeneratedDir}/TextureRepack_spv.h) +add_custom_command( + OUTPUT "${agcTextureRepackShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcTextureRepackShaderSpv}" "${agcTextureRepackShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcTextureRepackShaderSpv} -DOUTPUT=${agcTextureRepackShaderHeader} + -DSYMBOL=TEXTURE_REPACK_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcTextureRepackShaderSource}" glslang-standalone + VERBATIM +) + +set(agcDispatchArgumentsShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/DispatchArguments.comp) +set(agcDispatchArgumentsShaderSpv ${agcTextureDetileGeneratedDir}/DispatchArguments.spv) +set(agcDispatchArgumentsShaderHeader ${agcTextureDetileGeneratedDir}/DispatchArguments_spv.h) +add_custom_command( + OUTPUT "${agcDispatchArgumentsShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcDispatchArgumentsShaderSpv}" "${agcDispatchArgumentsShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcDispatchArgumentsShaderSpv} -DOUTPUT=${agcDispatchArgumentsShaderHeader} + -DSYMBOL=DISPATCH_ARGUMENTS_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcDispatchArgumentsShaderSource}" glslang-standalone + VERBATIM +) + +set(agcDepthSampleShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/DepthSample.comp) +foreach(agcDepthSampleKind IN ITEMS Single Multisample) + string(TOUPPER ${agcDepthSampleKind} agcDepthSampleSymbol) + if(agcDepthSampleKind STREQUAL "Multisample") + set(agcDepthSampleMultisampled 1) + else() + set(agcDepthSampleMultisampled 0) + endif() + set(agcDepthSampleShaderSpv ${agcTextureDetileGeneratedDir}/DepthSample${agcDepthSampleKind}.spv) + set(agcDepthSampleShaderHeader ${agcTextureDetileGeneratedDir}/DepthSample${agcDepthSampleKind}_spv.h) + add_custom_command( + OUTPUT "${agcDepthSampleShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -DMULTISAMPLED=${agcDepthSampleMultisampled} -o "${agcDepthSampleShaderSpv}" "${agcDepthSampleShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcDepthSampleShaderSpv} -DOUTPUT=${agcDepthSampleShaderHeader} + -DSYMBOL=DEPTH_SAMPLE_${agcDepthSampleSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcDepthSampleShaderSource}" glslang-standalone + VERBATIM + ) + list(APPEND agcDepthSampleShaderHeaders ${agcDepthSampleShaderHeader}) +endforeach() + set(agcSampleCounterShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/SampleCounter.comp) set(agcSampleCounterShaderSpv ${agcTextureDetileGeneratedDir}/SampleCounter.spv) set(agcSampleCounterShaderHeader ${agcTextureDetileGeneratedDir}/SampleCounter_spv.h) @@ -125,12 +206,49 @@ foreach(agcPresentationStage IN ITEMS Vert Frag) ) list(APPEND agcPresentationShaderHeaders ${agcPresentationShaderHeader}) endforeach() +foreach(agcSampleOrderVariant IN ITEMS "R8;comp;-DTEXEL_FORMAT=r8ui" "R16;comp;-DTEXEL_FORMAT=r16ui" "R32;comp;-DTEXEL_FORMAT=r32ui" "RG32;comp;-DTEXEL_FORMAT=rg32ui" "RGBA32;comp;-DTEXEL_FORMAT=rgba32ui" "Vertex;vert;-DPLAIN=1" "Depth;frag;-DPLAIN=1" "StencilExport;frag;-DSTENCIL_EXPORT=1" "StencilBits;frag;-DSTENCIL_BITS=1") + list(GET agcSampleOrderVariant 0 agcSampleOrderName) + list(GET agcSampleOrderVariant 1 agcSampleOrderExtension) + list(GET agcSampleOrderVariant 2 agcSampleOrderDefine) + string(TOUPPER "${agcSampleOrderName}" agcSampleOrderSymbol) + set(agcSampleOrderShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/SampleOrder.${agcSampleOrderExtension}) + set(agcSampleOrderShaderSpv ${agcTextureDetileGeneratedDir}/SampleOrder${agcSampleOrderName}.spv) + set(agcSampleOrderShaderHeader ${agcTextureDetileGeneratedDir}/SampleOrder${agcSampleOrderName}_spv.h) + add_custom_command( + OUTPUT "${agcSampleOrderShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 ${agcSampleOrderDefine} -o "${agcSampleOrderShaderSpv}" "${agcSampleOrderShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcSampleOrderShaderSpv} -DOUTPUT=${agcSampleOrderShaderHeader} + -DSYMBOL=SAMPLE_ORDER_${agcSampleOrderSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcSampleOrderShaderSource}" glslang-standalone + VERBATIM + ) + list(APPEND agcSampleOrderShaderHeaders ${agcSampleOrderShaderHeader}) +endforeach() +foreach(agcOverlayStage IN ITEMS Vert Frag) + string(TOLOWER "${agcOverlayStage}" agcOverlayExtension) + string(TOUPPER "${agcOverlayStage}" agcOverlaySymbol) + set(agcOverlayShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/PerformanceOverlay.${agcOverlayExtension}) + set(agcOverlayShaderSpv ${agcTextureDetileGeneratedDir}/PerformanceOverlay${agcOverlayStage}.spv) + set(agcOverlayShaderHeader ${agcTextureDetileGeneratedDir}/PerformanceOverlay${agcOverlayStage}_spv.h) + add_custom_command( + OUTPUT "${agcOverlayShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcOverlayShaderSpv}" "${agcOverlayShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcOverlayShaderSpv} -DOUTPUT=${agcOverlayShaderHeader} + -DSYMBOL=PERFORMANCE_OVERLAY_${agcOverlaySymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcOverlayShaderSource}" glslang-standalone + VERBATIM + ) + list(APPEND agcPresentationShaderHeaders ${agcOverlayShaderHeader}) +endforeach() set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ComputePipelineCapture.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/DisplayBuffer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/DisplayFormat.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/HostHdr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/DeviceAccess.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Diagnostics.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Dispatch/DataHitVerification.cpp @@ -144,13 +262,16 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Dispatch/IndirectDispatch.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Dispatch/VariantValidation.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/Draw.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawProgramTrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawCache.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawCacheStatistics.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawCapture.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawDecode.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawLookup.cpp + ${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 @@ -170,6 +291,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 @@ -179,7 +301,9 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Synchronization/WaitMemory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Synchronization/WaitStatistics.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/WorkerSampler.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/HostAllocator.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 @@ -188,10 +312,14 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/SubgroupClock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ShaderDeviceProfile.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Pm4.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/SubmissionCapture.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/VulkanDevice.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/FloatFlushProbe.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/AspectFit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationPass.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceOverlay.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Screenshot.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTarget.cpp @@ -200,6 +328,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 @@ -217,15 +346,26 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureFormat.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DccMetadata.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureTiling.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTextureLayout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureDetiler.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleReadback.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleUpload.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Texture.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PassHazards.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/SampleOrder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/BarrierBatch.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/UnitShadow.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Sampler.cpp + ${agcSampleOrderShaderHeaders} ${agcTextureDetileShaderHeader} + ${agcReadbackHeaders} + ${agcMultisampleUploadHeader} ${agcCmaskClearShaderHeader} + ${agcDepthSampleShaderHeaders} ${agcMeshArgumentsShaderHeader} ${agcTextureDetileImageShaderHeader} + ${agcTextureRepackShaderHeader} + ${agcDispatchArgumentsShaderHeader} ${agcSampleCounterShaderHeader} ${agcSampleDumpsShaderHeader} ${agcPresentationShaderHeaders} @@ -278,6 +418,7 @@ add_shader_recompiler(shader_recompiler_agc_driver libc target_link_libraries(libSceAgcDriver PRIVATE shader_recompiler_agc_driver + decoder_png SDL2-static libc libkernel @@ -287,11 +428,68 @@ target_link_libraries(libSceAgcDriver PRIVATE if(APS5_ENABLE_TRACY) target_link_libraries(libSceAgcDriver PUBLIC TracyClient) endif() +if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(libSceAgcDriver PRIVATE LINKER:--exclude-libs,$) +endif() + +if(BUILD_TESTING) + add_test_executable(agc_driver_unsupported_diagnostics_tests tests/UnsupportedDiagnostics.cpp) + target_include_directories(agc_driver_unsupported_diagnostics_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_driver_unsupported_diagnostics_tests PRIVATE libSceAgcDriver) + configure_windows_unwind(agc_driver_unsupported_diagnostics_tests) + add_test(NAME agc_driver_unsupported_diagnostics COMMAND agc_driver_unsupported_diagnostics_tests) + if(WIN32) + set_tests_properties(agc_driver_unsupported_diagnostics PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(agc_driver_draw_program_trace_tests + tests/DrawProgramTrace.cpp + Execution/src/Driver/Draw/DrawProgramTrace.cpp + ) + target_include_directories(agc_driver_draw_program_trace_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ) + set_target_properties(agc_driver_draw_program_trace_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(agc_driver_draw_program_trace_tests) + add_test(NAME agc_driver_draw_program_trace COMMAND agc_driver_draw_program_trace_tests) + set_tests_properties(agc_driver_draw_program_trace PROPERTIES TIMEOUT 20) + if(MINGW) + target_link_libraries(agc_driver_draw_program_trace_tests PRIVATE libc) + set_tests_properties(agc_driver_draw_program_trace PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() set_target_properties(libSceAgcDriver PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(libSceAgcDriver) +if(WIN32) + target_link_options(libSceAgcDriver PRIVATE LINKER:--exclude-symbols=_Znwy:_Znay:_ZnwyRKSt9nothrow_t:_ZnayRKSt9nothrow_t:_ZdlPv:_ZdaPv:_ZdlPvy:_ZdaPvy:_ZdlPvRKSt9nothrow_t:_ZdaPvRKSt9nothrow_t) +endif() if(BUILD_TESTING) + add_test_executable(agc_driver_nop_data_packet_tests tests/NopDataWritePacket.cpp) + target_include_directories(agc_driver_nop_data_packet_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_driver_nop_data_packet_tests PRIVATE libSceAgcDriver) + configure_windows_unwind(agc_driver_nop_data_packet_tests) + add_test(NAME agc_driver_nop_data_packet COMMAND agc_driver_nop_data_packet_tests) + if(WIN32) + set_tests_properties(agc_driver_nop_data_packet PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + 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) @@ -313,6 +511,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) @@ -334,6 +544,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) @@ -367,6 +588,38 @@ if(BUILD_TESTING) endif() endif() +if(BUILD_TESTING AND WIN32) + add_test_executable(agc_driver_host_allocator_tests tests/HostAllocator.cpp Execution/src/HostAllocator.cpp) + target_link_libraries(agc_driver_host_allocator_tests PRIVATE libc) + configure_windows_unwind(agc_driver_host_allocator_tests) + add_test(NAME agc_driver_host_allocator COMMAND agc_driver_host_allocator_tests) + set_tests_properties(agc_driver_host_allocator PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") +endif() + +add_test_executable(agc_driver_key_miss_tests EXCLUDE_FROM_ALL tests/KeyMiss.cpp) +target_include_directories(agc_driver_key_miss_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_driver_key_miss_tests PRIVATE + libSceAgcDriver + libc + libkernel +) +set_target_properties(agc_driver_key_miss_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_driver_key_miss_tests) +if(BUILD_TESTING) + set_target_properties(agc_driver_key_miss_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_driver_key_miss COMMAND agc_driver_key_miss_tests) + set_tests_properties(agc_driver_key_miss PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_key_miss PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_driver_tests EXCLUDE_FROM_ALL tests/Submit.cpp ) @@ -410,12 +663,50 @@ 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) set_target_properties(agc_shader_memory_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shader_memory_tests) +add_test_executable(agc_vertex_input_tests EXCLUDE_FROM_ALL tests/VertexInput.cpp) +target_include_directories(agc_vertex_input_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include +) +target_link_libraries(agc_vertex_input_tests PRIVATE shader_recompiler_agc_driver libc) +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) @@ -425,6 +716,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) @@ -435,7 +727,11 @@ if(BUILD_TESTING) add_test(NAME agc_write_tracking COMMAND agc_write_tracking_tests) set_tests_properties(agc_write_tracking PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_write_tracking PROPERTIES ENVIRONMENT_MODIFICATION + add_test(NAME agc_write_tracking_metadata_commit COMMAND agc_write_tracking_tests --metadata-commit) + set_tests_properties(agc_write_tracking_metadata_commit PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_resident_direct_remap COMMAND agc_write_tracking_tests --resident-direct-remap) + set_tests_properties(agc_resident_direct_remap PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 20) + set_tests_properties(agc_write_tracking agc_write_tracking_metadata_commit agc_resident_direct_remap PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() @@ -477,6 +773,15 @@ if(BUILD_TESTING) add_test(NAME agc_fragment_barycentric COMMAND agc_fragment_barycentric_tests) endif() +add_test_executable(agc_float32_flush_control_tests EXCLUDE_FROM_ALL tests/Float32FlushControl.cpp) +target_include_directories(agc_float32_flush_control_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +set_target_properties(agc_float32_flush_control_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_float32_flush_control_tests) +if(BUILD_TESTING) + set_target_properties(agc_float32_flush_control_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_float32_flush_control COMMAND agc_float32_flush_control_tests) +endif() + add_test_executable(agc_pipeline_link_optimization_tests EXCLUDE_FROM_ALL tests/PipelineLinkOptimization.cpp) target_include_directories(agc_pipeline_link_optimization_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) set_target_properties(agc_pipeline_link_optimization_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -486,6 +791,44 @@ if(BUILD_TESTING) add_test(NAME agc_pipeline_link_optimization COMMAND agc_pipeline_link_optimization_tests) endif() +add_test_executable(agc_draw_inputs_in_place_driver_tests EXCLUDE_FROM_ALL tests/DrawInputsInPlace.cpp) +target_include_directories(agc_draw_inputs_in_place_driver_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +set_target_properties(agc_draw_inputs_in_place_driver_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_draw_inputs_in_place_driver_tests) +if(BUILD_TESTING) + set_target_properties(agc_draw_inputs_in_place_driver_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_draw_inputs_in_place_driver COMMAND agc_draw_inputs_in_place_driver_tests) +endif() + +add_test_executable(agc_subgroup_barrier_tests EXCLUDE_FROM_ALL tests/SubgroupBarrier.cpp) +target_include_directories(agc_subgroup_barrier_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +target_link_libraries(agc_subgroup_barrier_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_subgroup_barrier_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_subgroup_barrier_tests) +if(BUILD_TESTING) + set_target_properties(agc_subgroup_barrier_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_subgroup_barrier COMMAND agc_subgroup_barrier_tests) + set_tests_properties(agc_subgroup_barrier PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_subgroup_barrier PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(agc_device_fault_tests EXCLUDE_FROM_ALL tests/DeviceFault.cpp) +target_include_directories(agc_device_fault_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +set_target_properties(agc_device_fault_tests PROPERTIES CXX_EXTENSIONS OFF) +target_link_libraries(agc_device_fault_tests PRIVATE libc libkernel) +configure_windows_unwind(agc_device_fault_tests) +if(BUILD_TESTING) + set_target_properties(agc_device_fault_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_device_fault COMMAND agc_device_fault_tests) + if(WIN32) + set_tests_properties(agc_device_fault PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_subgroup_clock_tests EXCLUDE_FROM_ALL tests/SubgroupClock.cpp Execution/src/SubgroupClock.cpp @@ -549,6 +892,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_f32_denorm_preserve_tests EXCLUDE_FROM_ALL tests/F32DenormPreserve.cpp) +target_link_libraries(agc_f32_denorm_preserve_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_f32_denorm_preserve_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_f32_denorm_preserve_tests) +if(BUILD_TESTING) + set_target_properties(agc_f32_denorm_preserve_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_f32_denorm_preserve COMMAND agc_f32_denorm_preserve_tests) + set_tests_properties(agc_f32_denorm_preserve PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_f32_denorm_preserve PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_spirv_branch_targets_tests EXCLUDE_FROM_ALL tests/SpirvBranchTargets.cpp) target_link_libraries(agc_spirv_branch_targets_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_spirv_branch_targets_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -562,6 +918,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_spirv_failure_text_tests EXCLUDE_FROM_ALL tests/SpirvFailureText.cpp) +target_link_libraries(agc_spirv_failure_text_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_spirv_failure_text_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_spirv_failure_text_tests) +if(BUILD_TESTING) + set_target_properties(agc_spirv_failure_text_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_spirv_failure_text COMMAND agc_spirv_failure_text_tests) + set_tests_properties(agc_spirv_failure_text PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_spirv_failure_text PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_spirv_lds_atomic_lock_tests EXCLUDE_FROM_ALL tests/SpirvLdsAtomicLock.cpp) target_link_libraries(agc_spirv_lds_atomic_lock_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_spirv_lds_atomic_lock_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -575,6 +944,32 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_spirv_subvector_loop_begin_tests EXCLUDE_FROM_ALL tests/SpirvSubvectorLoopBegin.cpp) +target_link_libraries(agc_spirv_subvector_loop_begin_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_spirv_subvector_loop_begin_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_spirv_subvector_loop_begin_tests) +if(BUILD_TESTING) + set_target_properties(agc_spirv_subvector_loop_begin_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_spirv_subvector_loop_begin COMMAND agc_spirv_subvector_loop_begin_tests) + set_tests_properties(agc_spirv_subvector_loop_begin PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_spirv_subvector_loop_begin PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(agc_layer_input_tests EXCLUDE_FROM_ALL tests/LayerInput.cpp) +target_link_libraries(agc_layer_input_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_layer_input_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_layer_input_tests) +if(BUILD_TESTING) + set_target_properties(agc_layer_input_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_layer_input COMMAND agc_layer_input_tests) + set_tests_properties(agc_layer_input PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_layer_input PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_dpp_write_tests EXCLUDE_FROM_ALL tests/DppWrite.cpp) target_link_libraries(agc_dpp_write_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_dpp_write_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -610,6 +1005,19 @@ target_link_libraries(agc_scalar_select_mask_tests PRIVATE shader_recompiler_agc set_target_properties(agc_scalar_select_mask_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_scalar_select_mask_tests) +add_test_executable(agc_tessellation_lowering_tests EXCLUDE_FROM_ALL tests/TessellationLowering.cpp) +target_link_libraries(agc_tessellation_lowering_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_tessellation_lowering_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_tessellation_lowering_tests) +if(BUILD_TESTING) + set_target_properties(agc_tessellation_lowering_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_tessellation_lowering COMMAND agc_tessellation_lowering_tests) + if(WIN32) + set_tests_properties(agc_tessellation_lowering PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_wave_control_decode_tests EXCLUDE_FROM_ALL tests/WaveControlDecode.cpp) target_include_directories(agc_wave_control_decode_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_wave_control_decode_tests PRIVATE shader_recompiler_agc_driver libc) @@ -650,10 +1058,27 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_integer_folding_tests EXCLUDE_FROM_ALL tests/IntegerFolding.cpp) +target_link_libraries(agc_integer_folding_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_integer_folding_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_integer_folding_tests) +if(BUILD_TESTING) + set_target_properties(agc_integer_folding_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_integer_folding COMMAND agc_integer_folding_tests) + if(WIN32) + set_tests_properties(agc_integer_folding PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_vop1_control_decode_tests EXCLUDE_FROM_ALL tests/Vop1ControlDecode.cpp) target_link_libraries(agc_vop1_control_decode_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_vop1_control_decode_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_vop1_control_decode_tests) +add_test_executable(agc_vertex_input_validation_tests EXCLUDE_FROM_ALL tests/VertexInputValidation.cpp) +target_link_libraries(agc_vertex_input_validation_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_vertex_input_validation_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_vertex_input_validation_tests) add_test_executable(agc_vop2_encoding_decode_tests EXCLUDE_FROM_ALL tests/Vop2EncodingDecode.cpp) target_link_libraries(agc_vop2_encoding_decode_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_vop2_encoding_decode_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -680,12 +1105,36 @@ 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) set_target_properties(agc_pass_hazards_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_pass_hazards_tests) +add_test_executable(agc_barrier_batch_tests EXCLUDE_FROM_ALL tests/BarrierBatch.cpp Graphics/src/BarrierBatch.cpp) +target_include_directories(agc_barrier_batch_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +target_link_libraries(agc_barrier_batch_tests PRIVATE libc) +set_target_properties(agc_barrier_batch_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_barrier_batch_tests) + add_test_executable(agc_dcc_key_sharing_tests EXCLUDE_FROM_ALL tests/DccKeySharing.cpp) target_include_directories(agc_dcc_key_sharing_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_dcc_key_sharing_tests PRIVATE libc) @@ -701,6 +1150,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) @@ -708,8 +1158,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() @@ -742,6 +1198,20 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_host_hdr_tests EXCLUDE_FROM_ALL tests/HostHdr.cpp) +target_include_directories(agc_host_hdr_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +target_link_libraries(agc_host_hdr_tests PRIVATE libSceAgcDriver libc) +set_target_properties(agc_host_hdr_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_host_hdr_tests) +if(BUILD_TESTING) + set_target_properties(agc_host_hdr_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_host_hdr COMMAND agc_host_hdr_tests) + if(WIN32) + set_tests_properties(agc_host_hdr PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_validate_program_tests EXCLUDE_FROM_ALL tests/ValidateProgram.cpp) target_link_libraries(agc_validate_program_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_validate_program_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -752,6 +1222,16 @@ target_link_libraries(agc_formatted_buffer_once_tests PRIVATE shader_recompiler_ set_target_properties(agc_formatted_buffer_once_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_formatted_buffer_once_tests) +add_test_executable(agc_fma_mix_once_tests EXCLUDE_FROM_ALL tests/FmaMixOnce.cpp) +target_link_libraries(agc_fma_mix_once_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_fma_mix_once_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_fma_mix_once_tests) + +add_test_executable(agc_f16_single_convert_tests EXCLUDE_FROM_ALL tests/F16SingleConvert.cpp) +target_link_libraries(agc_f16_single_convert_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_f16_single_convert_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_f16_single_convert_tests) + add_test_executable(agc_rect_list_position_invariant_tests EXCLUDE_FROM_ALL tests/RectListPositionInvariant.cpp) target_link_libraries(agc_rect_list_position_invariant_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_rect_list_position_invariant_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -770,6 +1250,12 @@ target_link_libraries(agc_formatted_buffer_byte_stores_tests PRIVATE shader_reco set_target_properties(agc_formatted_buffer_byte_stores_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_formatted_buffer_byte_stores_tests) +add_test_executable(agc_mesh_output_budget_tests EXCLUDE_FROM_ALL tests/MeshOutputBudget.cpp) +target_include_directories(agc_mesh_output_budget_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_mesh_output_budget_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_mesh_output_budget_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_mesh_output_budget_tests) + add_test_executable(agc_shader_disk_cache_tests EXCLUDE_FROM_ALL tests/ShaderDiskCache.cpp) target_include_directories(agc_shader_disk_cache_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_shader_disk_cache_tests PRIVATE shader_recompiler_agc_driver libc) @@ -782,6 +1268,12 @@ target_link_libraries(agc_placeholder_images_tests PRIVATE shader_recompiler_agc set_target_properties(agc_placeholder_images_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_placeholder_images_tests) +add_test_executable(agc_screenshot_tests EXCLUDE_FROM_ALL tests/Screenshot.cpp Execution/src/Screenshot.cpp) +target_include_directories(agc_screenshot_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_screenshot_tests PRIVATE decoder_png SDL2-static libc) +set_target_properties(agc_screenshot_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_screenshot_tests) + add_test_executable(agc_shader_device_profile_tests EXCLUDE_FROM_ALL tests/ShaderDeviceProfile.cpp Execution/src/ShaderDeviceProfile.cpp) target_include_directories(agc_shader_device_profile_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_shader_device_profile_tests PRIVATE shader_recompiler_agc_driver libc) @@ -914,6 +1406,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} @@ -940,7 +1433,17 @@ 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) + add_test(NAME agc_gpu_timing_complete COMMAND agc_driver_recorder_tests --gpu-timing-complete-only) + add_test(NAME agc_gpu_timing_incomplete COMMAND agc_driver_recorder_tests --gpu-timing-incomplete-only) + set_tests_properties(agc_gpu_timing_complete agc_gpu_timing_incomplete PROPERTIES + ENVIRONMENT "APS5_PROFILE_GPU=1" TIMEOUT 15 SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_recorder_device_selection agc_gpu_timing_complete agc_gpu_timing_incomplete 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) add_test(NAME agc_pipeline_library_off COMMAND agc_driver_recorder_tests --pipeline-library-only) set_tests_properties(agc_pipeline_library agc_pipeline_library_off PROPERTIES SKIP_RETURN_CODE 77) @@ -951,9 +1454,31 @@ if(BUILD_TESTING) set_tests_properties(agc_pipeline_library agc_pipeline_library_off agc_pipeline_library_fast_only PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - set_tests_properties(agc_pipeline_cache PROPERTIES SKIP_RETURN_CODE 77) + set_tests_properties(agc_pipeline_cache agc_pipeline_cache_file PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_pipeline_cache PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_pipeline_cache agc_pipeline_cache_file PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_import_watch COMMAND agc_driver_recorder_tests --import-watch-only) + set_tests_properties(agc_import_watch PROPERTIES SKIP_RETURN_CODE 77) + 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) + 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) + add_test(NAME agc_import_settle_storage_image COMMAND agc_driver_recorder_tests --settle-storage-image-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 agc_import_settle_storage_image 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 agc_import_settle_storage_image PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + if(WIN32) + add_test(NAME agc_reimport_bench COMMAND agc_driver_recorder_tests --reimport-bench) + add_test(NAME agc_host_import_cut_lifetime COMMAND agc_driver_recorder_tests --host-import-cut-only) + set_tests_properties(agc_reimport_bench agc_host_import_cut_lifetime PROPERTIES SKIP_RETURN_CODE 77) + set_tests_properties(agc_reimport_bench agc_host_import_cut_lifetime PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_unchanged_cpu_stamps COMMAND agc_driver_recorder_tests --unchanged-cpu-stamps-only) @@ -962,7 +1487,43 @@ if(BUILD_TESTING) set_tests_properties(agc_unchanged_cpu_stamps PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + foreach(mode IN ITEMS map ring shared uncached) + add_test(NAME agc_collect_mapping_history_${mode} COMMAND agc_driver_recorder_tests --collect-mapping-history-only) + set_tests_properties(agc_collect_mapping_history_${mode} PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_collect_mapping_history_${mode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + set_tests_properties(agc_collect_mapping_history_ring PROPERTIES ENVIRONMENT "APS5_NO_EPOCH_COLLECT_MAP=1") + set_tests_properties(agc_collect_mapping_history_shared PROPERTIES ENVIRONMENT "APS5_SHARED_COLLECT_MEMO=1") + set_tests_properties(agc_collect_mapping_history_uncached PROPERTIES ENVIRONMENT "APS5_NO_COLLECT_MEMO=1") + if(WIN32) + foreach(mode IN ITEMS cached uncached) + add_test(NAME agc_hint_mapping_history_${mode} COMMAND agc_driver_recorder_tests --hint-mapping-history-only) + set_tests_properties(agc_hint_mapping_history_${mode} PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endforeach() + set_tests_properties(agc_hint_mapping_history_uncached PROPERTIES ENVIRONMENT "APS5_NO_COLLECT_MEMO=1") + endif() + if(WIN32) + add_test(NAME agc_partial_watch_restoration COMMAND agc_driver_recorder_tests --partial-watch-restoration-only) + set_tests_properties(agc_partial_watch_restoration PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_storage_kept_unit_settle COMMAND agc_driver_recorder_tests --kept-unit-settle-only) + set_tests_properties(agc_storage_kept_unit_settle PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_storage_kept_unit_settle PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_storage_baseline_settled COMMAND agc_driver_recorder_tests --settled-baseline-only) + add_test(NAME agc_storage_clear_hand_over COMMAND agc_driver_recorder_tests --clear-hand-over-only) + set_tests_properties(agc_storage_clear_hand_over PROPERTIES SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_storage_clear_hand_over PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_storage_integer_clear COMMAND agc_driver_recorder_tests --integer-clear-only) set_tests_properties(agc_storage_integer_clear PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) @@ -970,6 +1531,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) @@ -981,26 +1548,70 @@ if(BUILD_TESTING) set_tests_properties(agc_storage_baseline_settled PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_read_written_staging COMMAND agc_driver_recorder_tests --read-written-staging-only) + set_tests_properties(agc_read_written_staging PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_read_written_staging PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_storage_written_buffer_order COMMAND agc_driver_recorder_tests --written-buffer-order-only) set_tests_properties(agc_storage_written_buffer_order PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) set_tests_properties(agc_storage_written_buffer_order PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_storage_split_writeback COMMAND agc_driver_recorder_tests --split-writeback-only) + set_tests_properties(agc_storage_split_writeback PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") + if(WIN32) + set_tests_properties(agc_storage_split_writeback PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_storage_write_back COMMAND agc_driver_recorder_tests --storage-write-back-only) + set_tests_properties(agc_storage_write_back PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 120 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") + if(WIN32) + set_tests_properties(agc_storage_write_back PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_copied_direct_ordering COMMAND agc_driver_recorder_tests --copied-direct-only) set_tests_properties(agc_copied_direct_ordering PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) set_tests_properties(agc_copied_direct_ordering PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_storage_writeback_blocks COMMAND agc_driver_recorder_tests --writeback-blocks-only) + set_tests_properties(agc_storage_writeback_blocks PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") + if(WIN32) + set_tests_properties(agc_storage_writeback_blocks PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_recorder_read_list COMMAND agc_driver_recorder_tests --read-list-only) + set_tests_properties(agc_recorder_read_list PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_recorder_deferred_failure_report COMMAND agc_driver_recorder_tests --deferred-failure-report-only) + set_tests_properties(agc_recorder_deferred_failure_report PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_recorder_deferred_failure_report PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_space_pending_writes COMMAND agc_driver_recorder_tests --space-pending-writes-only) + set_tests_properties(agc_space_pending_writes PROPERTIES SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_space_pending_writes PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_host_import_unmap COMMAND agc_driver_recorder_tests --host-import-unmap-only) set_tests_properties(agc_host_import_unmap PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_parked_import_unmap COMMAND agc_driver_recorder_tests --parked-import-unmap-only) set_tests_properties(agc_parked_import_unmap PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") add_test(NAME agc_host_import_retirement COMMAND agc_driver_recorder_tests --host-import-retirement-only) set_tests_properties(agc_host_import_retirement PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_sparse_run_import COMMAND agc_driver_recorder_tests --sparse-run-import-only) + set_tests_properties(agc_sparse_run_import PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_sparse_run_import_copied COMMAND agc_driver_recorder_tests --sparse-run-copied-only) + set_tests_properties(agc_sparse_run_import_copied PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only) set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") + add_test(NAME agc_idle_publish_wait COMMAND agc_driver_recorder_tests --idle-publish-wait-only) + set_tests_properties(agc_idle_publish_wait PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000;APS5_WRITE_WATCH_IMPORTS=watch") if(WIN32) add_test(NAME agc_storage_cpu_snapshot COMMAND agc_driver_recorder_tests --storage-cpu-snapshot-only) set_tests_properties(agc_storage_cpu_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77 @@ -1010,6 +1621,8 @@ if(BUILD_TESTING) set_tests_properties(agc_descriptor_pool_recycling PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_draw_snapshot_cap COMMAND agc_driver_recorder_tests --draw-snapshot-cap-only) set_tests_properties(agc_draw_snapshot_cap PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_draw_snapshot_epoch COMMAND agc_driver_recorder_tests --draw-snapshot-epoch-only) + set_tests_properties(agc_draw_snapshot_epoch PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_multisample_guest_access COMMAND agc_driver_recorder_tests --multisample-only) set_tests_properties(agc_multisample_guest_access PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_draw_inputs_in_place COMMAND agc_driver_recorder_tests --draw-inputs-in-place-only) @@ -1020,15 +1633,19 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() set_tests_properties(agc_draw_inputs_in_place PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_draw_inputs_copied COMMAND agc_driver_recorder_tests --draw-inputs-copied-only) + set_tests_properties(agc_draw_inputs_copied PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_resident_present COMMAND agc_driver_recorder_tests --resident-present-only) set_tests_properties(agc_resident_present PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_queued_write_groups COMMAND agc_driver_recorder_tests --queued-write-groups-only) + add_test(NAME agc_tiled_resolve COMMAND agc_driver_recorder_tests --tiled-resolve-only) + set_tests_properties(agc_tiled_resolve PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_slice_dirty COMMAND agc_driver_recorder_tests --slice-dirty-only) set_tests_properties(agc_slice_dirty PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_metadata_pass COMMAND agc_driver_recorder_tests --metadata-pass-only) set_tests_properties(agc_metadata_pass PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_idle_publish_wait agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_draw_snapshot_epoch agc_multisample_guest_access agc_draw_inputs_in_place agc_draw_inputs_copied agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only) set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION @@ -1038,10 +1655,45 @@ if(BUILD_TESTING) set_tests_properties(agc_pending_key_stores PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_shadowed_key_stores COMMAND agc_driver_recorder_tests --shadowed-key-stores-only) set_tests_properties(agc_shadowed_key_stores PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + 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 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) + add_test(NAME agc_recorder_completion_labels COMMAND agc_driver_recorder_tests --completion-labels-only) + set_tests_properties(agc_recorder_completion_labels PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_recorder_cmask COMMAND agc_driver_recorder_tests --cmask-only) + set_tests_properties(agc_recorder_cmask PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_recorder_cube COMMAND agc_driver_recorder_tests --cube-only) + set_tests_properties(agc_recorder_cube PROPERTIES 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:$") + set_tests_properties(agc_recorder_completion_labels agc_recorder_cmask agc_recorder_cube 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:") + if(WIN32) + add_test(NAME agc_driver_host_import_lifetime_other_family COMMAND agc_driver_recorder_tests --host-import-lifetime-other-family-only) + set_tests_properties(agc_driver_host_import_lifetime_other_family PROPERTIES + ENVIRONMENT "APS5_PIN_WAIT_MS=2000" SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_property(TEST agc_driver_host_import_lifetime_other_family APPEND PROPERTY ENVIRONMENT_MODIFICATION + "APS5_RELEASE_UNDER_LOCK=unset:" "APS5_RELEASE_ON_UNLOCK=unset:") + endif() endif() add_test_executable(agc_recorder_kept_bytes_tests EXCLUDE_FROM_ALL tests/RecorderKeptBytes.cpp) @@ -1058,11 +1710,29 @@ 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) set_target_properties(agc_host_import_retire_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_host_import_retire_tests) +target_sources(agc_host_import_retire_tests PRIVATE tests/HostImportFailure.cpp) if(BUILD_TESTING) set_target_properties(agc_host_import_retire_tests PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME agc_host_import_retire COMMAND agc_host_import_retire_tests) @@ -1115,6 +1785,51 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_performance_overlay_statistics_tests EXCLUDE_FROM_ALL + tests/PerformanceOverlayStatistics.cpp +) +target_include_directories(agc_performance_overlay_statistics_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_performance_overlay_statistics_tests PRIVATE + libSceAgcDriver + libc +) +set_target_properties(agc_performance_overlay_statistics_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_performance_overlay_statistics_tests) + +add_test_executable(agc_performance_overlay_tests EXCLUDE_FROM_ALL + tests/PerformanceOverlay.cpp +) +target_include_directories(agc_performance_overlay_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_performance_overlay_tests PRIVATE + libSceAgcDriver + SDL2-static + libc +) +set_target_properties(agc_performance_overlay_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_performance_overlay_tests) + +if(BUILD_TESTING) + set_target_properties(agc_performance_overlay_statistics_tests agc_performance_overlay_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_performance_overlay_statistics COMMAND agc_performance_overlay_statistics_tests) + add_test(NAME agc_performance_overlay COMMAND agc_performance_overlay_tests) + add_test(NAME agc_performance_overlay_device_selection COMMAND agc_performance_overlay_tests device-selection) + add_test(NAME agc_performance_overlay_device_selection_cpu COMMAND agc_performance_overlay_tests device-selection-cpu) + set_tests_properties(agc_performance_overlay agc_performance_overlay_device_selection PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + set_tests_properties(agc_performance_overlay_device_selection_cpu PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_performance_overlay_statistics agc_performance_overlay agc_performance_overlay_device_selection agc_performance_overlay_device_selection_cpu PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_driver_pm4_tests EXCLUDE_FROM_ALL tests/Pm4.cpp ) @@ -1125,6 +1840,7 @@ target_include_directories(agc_driver_pm4_tests PRIVATE target_link_libraries(agc_driver_pm4_tests PRIVATE libSceAgcDriver + shader_recompiler_agc_driver libc libkernel ) @@ -1146,6 +1862,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} $ @@ -1164,9 +1896,40 @@ target_link_libraries(agc_driver_graphics_tests PRIVATE libkernel ) +if(BUILD_TESTING) + add_test(NAME agc_command_batch_fence_failure COMMAND agc_driver_graphics_tests --command-batch-fence-failure) + set_tests_properties(agc_command_batch_fence_failure PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_command_batch_fence_failure PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + set_target_properties(agc_driver_graphics_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_graphics_tests) +add_test_executable(agc_driver_msaa_layout_tests EXCLUDE_FROM_ALL + tests/MultisampleTextureLayout.cpp + Graphics/src/MultisampleTextureLayout.cpp +) +target_include_directories(agc_driver_msaa_layout_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_driver_msaa_layout_tests PRIVATE libc) +set_target_properties(agc_driver_msaa_layout_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_driver_msaa_layout_tests) +if(BUILD_TESTING) + set_target_properties(agc_driver_msaa_layout_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_driver_msaa_layout COMMAND agc_driver_msaa_layout_tests) + set_tests_properties(agc_driver_msaa_layout PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_driver_msaa_layout PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + foreach(agcVertexRangeShader IN ITEMS VertexRange.vert VertexRangeProducer.frag) string(REPLACE "." "_" agcVertexRangeName "${agcVertexRangeShader}") string(TOUPPER "${agcVertexRangeName}_SPV" agcVertexRangeSymbol) @@ -1201,6 +1964,7 @@ add_test_executable(agc_driver_bda_device_tests EXCLUDE_FROM_ALL tests/BdaShader.cpp tests/BdaExecution.cpp tests/BdaContracts.cpp + tests/HostImportFailure.cpp ) target_include_directories(agc_driver_bda_device_tests PRIVATE @@ -1212,6 +1976,15 @@ target_include_directories(agc_driver_bda_device_tests PRIVATE target_link_libraries(agc_driver_bda_device_tests PRIVATE libSceAgcDriver SDL2-static libc) set_target_properties(agc_driver_bda_device_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_bda_device_tests) +if(BUILD_TESTING) + add_test(NAME agc_driver_host_import_failure_handles COMMAND agc_driver_bda_device_tests --host-import-failures) + set_tests_properties(agc_driver_host_import_failure_handles PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_host_import_failure_handles PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() add_test_executable(agc_driver_mesh_tests EXCLUDE_FROM_ALL tests/Mesh.cpp) target_include_directories(agc_driver_mesh_tests PRIVATE @@ -1246,42 +2019,158 @@ 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(BUILD_TESTING) + add_test(NAME agc_driver_indirect_thread_dimensions_cpu COMMAND agc_driver_indirect_thread_dimensions_tests) + set_tests_properties(agc_driver_indirect_thread_dimensions_cpu PROPERTIES ENVIRONMENT "APS5_NO_GPU_THREAD_DIMENSIONS=1" SKIP_RETURN_CODE 77 TIMEOUT 60) + if(WIN32) + set_tests_properties(agc_driver_indirect_thread_dimensions_cpu PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() +if(ANYPS5_ENABLE_SPIRV_TOOLS) + target_compile_definitions(agc_driver_stencil_null_feedback_tests PRIVATE ANYPS5_ENABLE_SPIRV_TOOLS=1) + target_link_libraries(agc_driver_stencil_null_feedback_tests PRIVATE SPIRV-Tools-static) +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_rect_list_corner_tests PRIVATE shader_recompiler_agc_driver) +target_link_libraries(agc_driver_queue_zero_late_interrupt_tests PRIVATE libkernel) + +target_link_libraries(agc_driver_storage_file_write_tests PRIVATE libkernel) + +target_link_libraries(agc_driver_preparation_scope_settle_tests PRIVATE libSceAgc) + target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $) +target_include_directories(agc_driver_sample_multisample_target_tests PRIVATE $) + +target_include_directories(agc_driver_image_table_cache_bounds_tests PRIVATE $) + +target_include_directories(agc_driver_draw_cache_relocation_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}") + add_test(NAME agc_driver_store_idle_results_wake COMMAND agc_driver_store_idle_results_tests --idle-epoch-wake) + set_tests_properties(agc_driver_store_idle_results_wake 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:$") + set_tests_properties(agc_driver_store_idle_results_wake PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +if(BUILD_TESTING) + add_test(NAME agc_driver_native_fmask COMMAND agc_driver_sample_multisample_target_tests --native-fmask) + set_tests_properties(agc_driver_native_fmask PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_native_fmask PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + 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) - set_tests_properties(agc_driver_undithered_alpha_to_coverage_state PROPERTIES + add_test(NAME agc_driver_depth_bounds_state COMMAND agc_driver_graphics_tests --depth-bounds-state) + add_test(NAME agc_driver_msaa_dcc_state COMMAND agc_driver_graphics_tests --multisample-dcc-state) + add_test(NAME agc_driver_unused_viewport_depth COMMAND agc_driver_graphics_tests --unused-viewport-depth) + set_tests_properties(agc_driver_unused_viewport_depth PROPERTIES TIMEOUT 30) + set_tests_properties(agc_driver_undithered_alpha_to_coverage_state agc_driver_depth_bounds_state PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --alpha-to-coverage-state) set_tests_properties(agc_driver_alpha_to_coverage_state PROPERTIES 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_depth_bounds_state agc_driver_msaa_dcc_state agc_driver_unused_viewport_depth PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") - set_tests_properties(agc_driver_dithered_alpha_to_coverage PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_dithered_alpha_to_coverage ${multisampledDccClearTests} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() +target_include_directories(agc_driver_native_multisample_depth_tests PRIVATE $) + +foreach(agcReadbackTestKind IN ITEMS Vertex Uint Depth) + string(TOUPPER ${agcReadbackTestKind} agcReadbackTestSymbol) + if(agcReadbackTestKind STREQUAL "Vertex") + set(agcReadbackTestStage vert) + else() + set(agcReadbackTestStage frag) + endif() + if(agcReadbackTestKind STREQUAL "Depth") + set(agcReadbackTestDepth 1) + else() + set(agcReadbackTestDepth 0) + endif() + set(agcReadbackTestHeader ${agcTestShaderGeneratedRoot}/MsaaReadback${agcReadbackTestKind}_spv.h) + set(agcReadbackTestSpv ${agcTestShaderGeneratedRoot}/MsaaReadback${agcReadbackTestKind}.spv) + add_custom_command( + OUTPUT "${agcReadbackTestHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}" + COMMAND "$" -V --target-env vulkan1.1 -DREAD_DEPTH=${agcReadbackTestDepth} -o "${agcReadbackTestSpv}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/MultisampleReadback.${agcReadbackTestStage}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcReadbackTestSpv} -DOUTPUT=${agcReadbackTestHeader} + -DSYMBOL=MSAA_READBACK_${agcReadbackTestSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/MultisampleReadback.${agcReadbackTestStage}" glslang-standalone + VERBATIM + ) + target_sources(agc_driver_multisample_readback_tests PRIVATE ${agcReadbackTestHeader}) +endforeach() +target_include_directories(agc_driver_multisample_readback_tests PRIVATE ${agcTestShaderGeneratedRoot}) target_include_directories(agc_driver_dual_source_blend_tests PRIVATE $) +target_include_directories(agc_driver_reversed_order_blend_tests PRIVATE $) + target_include_directories(agc_driver_swap_alt_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 $) + +target_include_directories(agc_driver_depth_range_remap_draw_tests PRIVATE $) + foreach(stage IN ITEMS vert frag) set(agcProvokingShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/ProvokingVertex.${stage}) set(agcProvokingShaderSpv ${agcTestShaderGeneratedRoot}/ProvokingVertex_${stage}.spv) @@ -1300,10 +2189,38 @@ foreach(stage IN ITEMS vert frag) endforeach() target_include_directories(agc_driver_provoking_vertex_tests PRIVATE ${agcTestShaderGeneratedRoot}) +foreach(agcSampleOrderTestVariant IN ITEMS "R8;SampleOrderTexels.comp;-DTEXEL_FORMAT=r8ui" "R16;SampleOrderTexels.comp;-DTEXEL_FORMAT=r16ui" "R32;SampleOrderTexels.comp;-DTEXEL_FORMAT=r32ui" "RG32;SampleOrderTexels.comp;-DTEXEL_FORMAT=rg32ui" "RGBA32;SampleOrderTexels.comp;-DTEXEL_FORMAT=rgba32ui" "FillVertex;SampleOrderFill.vert;-DPLAIN=1" "FillDepth;SampleOrderFill.frag;-DPLAIN=1" "FillStencil;SampleOrderFill.frag;-DSTENCIL=1" "ReadDepth;SampleOrderRead.comp;-DPLAIN=1" "ReadStencil;SampleOrderRead.comp;-DSTENCIL=1" "ReadColor;SampleOrderRead.comp;-DCOLOR=1") + list(GET agcSampleOrderTestVariant 0 agcSampleOrderTestName) + list(GET agcSampleOrderTestVariant 1 agcSampleOrderTestFile) + list(GET agcSampleOrderTestVariant 2 agcSampleOrderTestDefine) + string(TOUPPER "${agcSampleOrderTestName}" agcSampleOrderTestSymbol) + set(agcSampleOrderTestSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/${agcSampleOrderTestFile}) + set(agcSampleOrderTestSpv ${agcTestShaderGeneratedRoot}/SampleOrderTest${agcSampleOrderTestName}.spv) + set(agcSampleOrderTestHeader ${agcTestShaderGeneratedRoot}/SampleOrderTest${agcSampleOrderTestName}_spv.h) + add_custom_command( + OUTPUT "${agcSampleOrderTestHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}" + COMMAND "$" -V --target-env vulkan1.1 ${agcSampleOrderTestDefine} -o "${agcSampleOrderTestSpv}" "${agcSampleOrderTestSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcSampleOrderTestSpv} -DOUTPUT=${agcSampleOrderTestHeader} + -DSYMBOL=SAMPLE_ORDER_TEST_${agcSampleOrderTestSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcSampleOrderTestSource}" glslang-standalone + VERBATIM + ) + target_sources(agc_driver_sample_order_tests PRIVATE ${agcSampleOrderTestHeader}) +endforeach() +target_include_directories(agc_driver_sample_order_tests PRIVATE ${agcTestShaderGeneratedRoot}) + target_include_directories(agc_driver_prepared_exports_tests PRIVATE $) +target_include_directories(agc_driver_pipeline_journal_tests PRIVATE $) + target_include_directories(agc_driver_buffer_descriptor_runtime_tests PRIVATE $) +target_include_directories(agc_driver_shutdown_threads_tests PRIVATE $) + +target_include_directories(agc_driver_image_converted_unorm_tests PRIVATE $) +target_include_directories(agc_driver_failed_capture_dump_tests PRIVATE $) + target_include_directories(agc_driver_prepared_publication_tests PRIVATE $) target_include_directories(agc_driver_prepared_graphics_tests PRIVATE $) @@ -1311,16 +2228,41 @@ 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 $ ) if(BUILD_TESTING) set_target_properties(agc_shader_mode_register_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + foreach(prebuildPolicyMode IN ITEMS unset 0 1) + set(prebuildPolicyTest agc_driver_prepared_graphics_prebuild_${prebuildPolicyMode}) + add_test(NAME ${prebuildPolicyTest} COMMAND agc_driver_prepared_graphics_tests --prebuild-${prebuildPolicyMode}) + set_tests_properties(${prebuildPolicyTest} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" RUN_SERIAL TRUE SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + set(prebuildPolicyEnvironment "APS5_NO_GPL=unset:") + if(prebuildPolicyMode STREQUAL "unset") + list(APPEND prebuildPolicyEnvironment "APS5_NO_LIBRARY_PREBUILD=unset:") + else() + list(APPEND prebuildPolicyEnvironment "APS5_NO_LIBRARY_PREBUILD=set:${prebuildPolicyMode}") + endif() + if(WIN32) + list(APPEND prebuildPolicyEnvironment "PATH=path_list_prepend:$") + endif() + set_tests_properties(${prebuildPolicyTest} PROPERTIES ENVIRONMENT_MODIFICATION "${prebuildPolicyEnvironment}") + endforeach() add_test(NAME agc_driver_readonly_resummarize COMMAND agc_driver_tests --readonly-resummarize) set_tests_properties(agc_driver_readonly_resummarize PROPERTIES TIMEOUT 10) + add_test(NAME agc_driver_eop_interrupts COMMAND agc_driver_tests --eop-interrupts) + set_tests_properties(agc_driver_eop_interrupts PROPERTIES TIMEOUT 60) if(WIN32) - set_tests_properties(agc_driver_readonly_resummarize PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_readonly_resummarize agc_driver_eop_interrupts PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_driver_small_copy_sync COMMAND agc_driver_tests --small-copy-sync) + set_tests_properties(agc_driver_small_copy_sync PROPERTIES TIMEOUT 30 ENVIRONMENT "APS5_COPY_SYNC=1" SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_driver_small_copy_sync PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_shader_mode_register COMMAND agc_shader_mode_register_tests) @@ -1330,9 +2272,30 @@ if(BUILD_TESTING) endif() add_test(NAME agc_shader_bindless COMMAND agc_shader_memory_tests --bindless) set_tests_properties(agc_shader_bindless PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_selected_records_image_limit COMMAND agc_shader_memory_tests --selected-records-image-limit) + add_test(NAME agc_shader_selected_records_pair_limit COMMAND agc_shader_memory_tests --selected-records-pair-limit) + add_test(NAME agc_shader_selected_records_direct_image_limit COMMAND agc_shader_memory_tests --selected-records-direct-image-limit) + add_test(NAME agc_shader_selected_records_direct_pair_limit COMMAND agc_shader_memory_tests --selected-records-direct-pair-limit) + set_tests_properties(agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_srt_read_patching COMMAND agc_shader_memory_tests --srt-read-patching) set_tests_properties(agc_shader_srt_read_patching PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_table_snapshot_reuse COMMAND agc_shader_memory_tests --table-snapshot-reuse) + set_tests_properties(agc_shader_table_snapshot_reuse PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_image_table_vouched_lookups COMMAND agc_driver_image_table_cache_bounds_tests --vouched-lookups) + set_tests_properties(agc_driver_image_table_vouched_lookups PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_WRITE_WATCH_IMPORTS=watch" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 60) + add_test(NAME agc_driver_image_table_vouch_queued_draws COMMAND agc_driver_image_table_cache_bounds_tests --queued-draws) + set_tests_properties(agc_driver_image_table_vouch_queued_draws PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 60) + if(WIN32) + set_tests_properties(agc_shader_table_snapshot_reuse agc_driver_image_table_vouched_lookups agc_driver_image_table_vouch_queued_draws PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_shader_type_zero_image_materialization COMMAND agc_shader_memory_tests --type-zero-image-materialization) + set_tests_properties(agc_shader_type_zero_image_materialization PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_frontend_pair COMMAND agc_shader_memory_tests --frontend-pair) + add_test(NAME agc_shader_quad_mask_no_demote COMMAND agc_shader_memory_tests --quad-mask-no-demote) + set_tests_properties(agc_shader_quad_mask_no_demote PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_shader_quad_mask_no_demote PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_shader_frontend_pair_off COMMAND agc_shader_memory_tests --frontend-pair) set_tests_properties(agc_shader_frontend_pair PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_FRONTEND_PAIR=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") @@ -1340,10 +2303,12 @@ 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_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_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - foreach(agcTest IN ITEMS agc_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}) + foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_tests agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vertex_input_validation_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_tests agc_screenshot_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests agc_f16_single_convert_tests) if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests") continue() endif() @@ -1356,17 +2321,199 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test(NAME agc_driver_guarded_direct_root COMMAND agc_driver_guarded_null_pointer_tests --direct-root-only) + set_tests_properties(agc_driver_guarded_direct_root PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_guarded_direct_root PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_driver_bda_device_selection_cpu COMMAND agc_driver_bda_device_tests --device-selection-cpu) + set_tests_properties(agc_driver_bda_device_selection_cpu PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_bda_device_selection_cpu PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_driver_draw_unsupported_buffer_type_skip_refused COMMAND agc_driver_draw_unsupported_buffer_type_tests --skip-refused) + set_tests_properties(agc_driver_draw_unsupported_buffer_type_skip_refused PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SKIP_REFUSED_DRAWS=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_draw_unsupported_buffer_type_skip_refused PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_driver_sparse_indirect_indices_unreadable COMMAND agc_driver_sparse_indirect_indices_tests --unreadable) + set_tests_properties(agc_driver_sparse_indirect_indices agc_driver_sparse_indirect_indices_unreadable PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_sparse_indirect_indices_unreadable PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + set_tests_properties(agc_driver_pixel_implicit_derivatives PROPERTIES TIMEOUT 30) + set_tests_properties(agc_driver_storage_file_write PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SUSPEND_DRAIN=1") + set_tests_properties(agc_driver_draw_pipeline_trace PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=8;APS5_TRACE_FRAMES=0:1;APS5_TRACE_EVENTS=64" TIMEOUT 30) + set_property(TEST agc_driver_draw_pipeline_trace APPEND PROPERTY ENVIRONMENT_MODIFICATION + "APS5_TRACE_SECONDS=unset:" "APS5_TRACE_SUMMARY_EVERY=unset:") + add_test(NAME agc_driver_draw_pipeline_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_draw_pipeline_trace_disabled PROPERTIES ENVIRONMENT "APS5_PIPELINED_DRAWS=8" SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_draw_pipeline_trace_disabled PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + set_tests_properties(agc_driver_queued_raw_shader PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=8" TIMEOUT 30) add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores) + set_tests_properties(agc_driver_multisample_readback PROPERTIES TIMEOUT 20) + foreach(agcReadbackCase IN ITEMS uint8 uint16 uint64 uint128 depth upload retile depth_retile) + add_test(NAME agc_driver_multisample_readback_${agcReadbackCase} COMMAND agc_driver_multisample_readback_tests ${agcReadbackCase}) + set_tests_properties(agc_driver_multisample_readback_${agcReadbackCase} PROPERTIES TIMEOUT 20 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_multisample_readback_${agcReadbackCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_shader_disk_cache_large_file COMMAND agc_shader_disk_cache_tests --large-file) + if(Python3_Interpreter_FOUND) + set(housekeepingModes plain) + if(WIN32) + list(APPEND housekeepingModes extended extended-long device) + else() + list(APPEND housekeepingModes plain-long) + endif() + foreach(housekeepingMode IN LISTS housekeepingModes) + set(housekeepingTest agc_shader_cache_housekeeping_${housekeepingMode}) + add_test(NAME ${housekeepingTest} COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_shader_cache_housekeeping.py + $ ${housekeepingMode}) + set_tests_properties(${housekeepingTest} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(${housekeepingTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endif() + set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES SKIP_RETURN_CODE 77 RUN_SERIAL TRUE WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 240) + if(WIN32) + set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test(NAME agc_shader_disk_cache_long_paths COMMAND agc_shader_disk_cache_tests --long-host-paths) + add_test(NAME agc_shader_disk_cache_host_stderr COMMAND agc_shader_disk_cache_tests --host-stderr) + set_tests_properties(agc_shader_disk_cache_host_stderr PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_shader_disk_cache_long_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + if(Python3_Interpreter_FOUND) + set(prebuiltDiscoveryModes plain relative plain-long) + if(WIN32) + list(APPEND prebuiltDiscoveryModes extended extended-forward device extended-long) + endif() + foreach(prebuiltDiscoveryMode IN LISTS prebuiltDiscoveryModes) + set(prebuiltDiscoveryTest agc_shader_prebuilt_discovery_${prebuiltDiscoveryMode}) + add_test(NAME ${prebuiltDiscoveryTest} COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_shader_cache_prebuilt_discovery.py + $ ${prebuiltDiscoveryMode}) + set_tests_properties(${prebuiltDiscoveryTest} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(${prebuiltDiscoveryTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endif() + add_test(NAME agc_driver_large_tracked_snapshot COMMAND agc_driver_partially_resident_snapshot_tests --large-tracked) + set_tests_properties(agc_driver_large_tracked_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_large_tracked_snapshot PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + if(WIN32) + add_test(NAME agc_driver_untracked_snapshot_hint COMMAND agc_driver_partially_resident_snapshot_tests --untracked-hint) + 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) + add_test(NAME agc_copy_input_kept_bytes COMMAND agc_driver_graphics_tests --copy-input-kept-bytes) + add_test(NAME agc_depth_copy_kept_bytes COMMAND agc_driver_graphics_tests --depth-copy-kept-bytes) + set_tests_properties(agc_copy_input_kept_bytes agc_depth_copy_kept_bytes PROPERTIES + TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" + ENVIRONMENT_MODIFICATION "APS5_CPU_COPIES=unset:;APS5_GPU_COPY_MAX_KIB=unset:;APS5_CAPTURE_GPU_COPIES=unset:;APS5_BUFFER_POOL_SLOTS=unset:;APS5_BUFFER_POOL_SHARED=unset:;APS5_NO_BUFFER_CLASSES=unset:;APS5_STAGING_POOL_MIB=unset:") + if(WIN32) + set_property(TEST agc_copy_input_kept_bytes APPEND PROPERTY ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_property(TEST agc_depth_copy_kept_bytes APPEND PROPERTY ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + set_tests_properties(agc_driver_native_multisample_depth PROPERTIES TIMEOUT 30) + set_tests_properties(agc_driver_sample_order PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_sample_order_grid_reference COMMAND agc_driver_sample_order_tests --grid-reference) + add_test(NAME agc_driver_sample_order_recipe_depth_locations COMMAND agc_driver_sample_order_tests --recipe-depth-locations) + set_tests_properties(agc_driver_sample_order_grid_reference agc_driver_sample_order_recipe_depth_locations PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_sample_order_grid_reference agc_driver_sample_order_recipe_depth_locations PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_driver_native_depth_retirement COMMAND agc_driver_native_multisample_depth_tests --retirement) + set_tests_properties(agc_driver_native_depth_retirement PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_native_depth_clears COMMAND agc_driver_native_multisample_depth_tests --clears) + add_test(NAME agc_driver_native_depth_clear_refusals COMMAND agc_driver_native_multisample_depth_tests --clear-refusals) + set_tests_properties(agc_driver_native_depth_clears agc_driver_native_depth_clear_refusals PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_native_depth_retirement PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + 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_journal_long_paths COMMAND agc_driver_graphics_tests --pipeline-journal-long-paths) + set_tests_properties(agc_pipeline_journal_long_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_pipeline_library_clear_waits_for_build COMMAND agc_driver_graphics_tests --pipeline-library-clear-waits-for-build) + add_test(NAME agc_pipeline_store_lazy_buffer_pool COMMAND agc_driver_graphics_tests --pipeline-store-lazy-buffer-pool) + set_tests_properties(agc_pipeline_journal PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_pipeline_library_clear_waits_for_build PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_pipeline_store_lazy_buffer_pool PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + add_test(NAME agc_pipeline_journal_extended_paths COMMAND agc_driver_graphics_tests --pipeline-journal-extended-paths) + set_tests_properties(agc_pipeline_journal_extended_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_pipeline_journal_long_paths agc_pipeline_journal_extended_paths PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build agc_pipeline_store_lazy_buffer_pool PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30) if(WIN32) set_tests_properties(agc_sparse_buffer_holes PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_heap_mirror_unwatch COMMAND agc_driver_graphics_tests --heap-mirror-unwatch) + add_test(NAME agc_heap_mirror_unwatch_no_space_cache COMMAND agc_driver_graphics_tests --heap-mirror-unwatch) + add_test(NAME agc_sparse_heap_mirror_unwatch COMMAND agc_driver_graphics_tests --sparse-heap-mirror-unwatch) + add_test(NAME agc_sparse_heap_mirror_unwatch_no_space_cache COMMAND agc_driver_graphics_tests --sparse-heap-mirror-unwatch) + add_test(NAME agc_heap_mirror_run_unwatch COMMAND agc_driver_graphics_tests --heap-mirror-run-unwatch) + add_test(NAME agc_heap_mirror_run_unwatch_no_space_cache COMMAND agc_driver_graphics_tests --heap-mirror-run-unwatch) + set_tests_properties(agc_heap_mirror_unwatch_no_space_cache agc_sparse_heap_mirror_unwatch_no_space_cache agc_heap_mirror_run_unwatch_no_space_cache PROPERTIES ENVIRONMENT "APS5_NO_ADDRESS_SPACE_CACHE=1") + set_tests_properties(agc_heap_mirror_unwatch agc_heap_mirror_unwatch_no_space_cache agc_sparse_heap_mirror_unwatch agc_sparse_heap_mirror_unwatch_no_space_cache agc_heap_mirror_run_unwatch agc_heap_mirror_run_unwatch_no_space_cache PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_heap_mirror_unwatch agc_heap_mirror_unwatch_no_space_cache agc_sparse_heap_mirror_unwatch agc_sparse_heap_mirror_unwatch_no_space_cache agc_heap_mirror_run_unwatch agc_heap_mirror_run_unwatch_no_space_cache PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + else() + add_test(NAME agc_heap_mirror_unwatched_import COMMAND agc_driver_graphics_tests --heap-mirror-unwatched-import) + set_tests_properties(agc_heap_mirror_unwatched_import PROPERTIES TIMEOUT 30) + 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" @@ -1376,6 +2523,18 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_image_srgb_sample_copy_forced COMMAND agc_driver_image_srgb_sample_copy_tests) + set_tests_properties(agc_driver_image_srgb_sample_copy_forced PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SRGB_SHADER_DECODE=copy" + SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_image_srgb_sample_copy_forced PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test(NAME agc_driver_graphics_maps_fallback COMMAND agc_driver_graphics_tests) + set_tests_properties(agc_driver_graphics_maps_fallback PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PROCMAP_QUERY=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + endif() add_test(NAME agc_shared_write_partial_collect COMMAND agc_shared_write_tests --partial-collect) set_tests_properties(agc_shared_write_partial_collect PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) @@ -1447,12 +2606,60 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() set_tests_properties(agc_driver_provoking_vertex PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_dword_pattern_fill_translated COMMAND agc_driver_dword_pattern_fill_tests) + set_tests_properties(agc_driver_dword_pattern_fill_translated PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_FILL_HLE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_dword_pattern_fill_translated PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_uint8x4_color_target_no_resident COMMAND agc_driver_uint8x4_color_target_tests) set_tests_properties(agc_driver_uint8x4_color_target_no_resident PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_RESIDENT_TARGETS=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) set_tests_properties(agc_driver_uint8x4_color_target_no_resident PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + set(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) @@ -1464,14 +2671,32 @@ if(BUILD_TESTING) endif() endforeach() set_tests_properties(agc_driver_storage_cache_budget PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_storage_cache_retained_batch COMMAND agc_driver_storage_cache_budget_tests --retained-batch) + set_tests_properties(agc_driver_storage_cache_retained_batch 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_storage_cache_retained_batch PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_sampled_cache_budget_memory COMMAND agc_driver_sampled_cache_budget_tests memory) set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) + add_test(NAME agc_driver_sampled_cache_budget_without_storage_image_multisample COMMAND agc_driver_sampled_cache_budget_tests without-storage-image-multisample) + set_tests_properties(agc_driver_sampled_cache_budget_without_storage_image_multisample PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_sampled_cache_device_selection COMMAND agc_driver_sampled_cache_budget_tests device-selection-cpu) + set_tests_properties(agc_sampled_cache_device_selection PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_driver_sampled_cache_device_selection COMMAND agc_driver_sampled_cache_budget_tests device-selection) + set_tests_properties(agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_sampled_cache_device_selection agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission) add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized) add_test(NAME agc_driver_resource_cache_storage_clear COMMAND agc_driver_sampled_cache_budget_tests resource-clear) - set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_driver_resource_cache_replaced_texture COMMAND agc_driver_sampled_cache_budget_tests resource-replaced) + add_test(NAME agc_driver_resource_cache_lookup_changed COMMAND agc_driver_sampled_cache_budget_tests resource-lookup-changed) + set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear agc_driver_resource_cache_replaced_texture agc_driver_resource_cache_lookup_changed PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_sampled_cache_budget_without_storage_image_multisample agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear agc_driver_resource_cache_replaced_texture agc_driver_resource_cache_lookup_changed PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() # The driver draw test again with the draw key off: the draw cache then keys on the decoded vertex @@ -1483,12 +2708,40 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_pm4_failure COMMAND agc_driver_pm4_tests failure) - set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) + 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_rewind_trace COMMAND agc_driver_pm4_tests rewind-trace) + set_tests_properties(agc_driver_pm4_rewind_trace PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=8;APS5_TRACE_FRAMES=0:1;APS5_TRACE_DIR=${CMAKE_CURRENT_BINARY_DIR}/pm4-rewind-trace" TIMEOUT 30) + set_property(TEST agc_driver_pm4_rewind_trace APPEND PROPERTY ENVIRONMENT_MODIFICATION + "APS5_TRACE_SECONDS=unset:" "APS5_TRACE_SUMMARY_EVERY=unset:") if(WIN32) - set_tests_properties(agc_driver_pm4_failure agc_descriptor_pool_reset_failure agc_driver_address_space_teardown PROPERTIES ENVIRONMENT_MODIFICATION + set_property(TEST agc_driver_pm4_rewind_trace APPEND PROPERTY ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_driver_pm4_failure_inline COMMAND agc_driver_pm4_tests failure) + set_tests_properties(agc_driver_pm4_failure_inline PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0" SKIP_RETURN_CODE 77) + if(WIN32) + 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) @@ -1497,20 +2750,43 @@ if(BUILD_TESTING) "ANYPS5_NO_SHADER_CACHE=1;APS5_THREAD_PRIORITY=high;DBUS_SYSTEM_BUS_ADDRESS=unix:path=/nonexistent/aps5-no-rtkit" SKIP_RETURN_CODE 77) endif() add_test(NAME agc_driver_present_mode COMMAND agc_driver_flip_tests present-mode) + add_test(NAME agc_driver_worker_failure_log COMMAND agc_driver_flip_tests failure-log) + set_tests_properties(agc_driver_worker_failure_log PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 20) add_test(NAME agc_driver_flip_compute COMMAND agc_driver_flip_tests compute) set_tests_properties(agc_driver_flip_compute PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_driver_flip_compute agc_driver_present_mode PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_flip_compute agc_driver_present_mode agc_driver_worker_failure_log PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_draw_cache_reuse_off COMMAND agc_driver_draw_cache_reuse_tests) set_tests_properties(agc_driver_draw_cache_reuse_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_GPL=1" SKIP_RETURN_CODE 77) - set_tests_properties(agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off PROPERTIES FAIL_REGULAR_EXPRESSION "FATAL:") + add_test(NAME agc_driver_draw_cache_reuse_verify COMMAND agc_driver_draw_cache_reuse_tests) + set_tests_properties(agc_driver_draw_cache_reuse_verify PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_VERIFY_DRAW_DATA_HITS=1" SKIP_RETURN_CODE 77) + add_test(NAME agc_driver_draw_cache_reuse_inline COMMAND agc_driver_draw_cache_reuse_tests) + set_tests_properties(agc_driver_draw_cache_reuse_inline PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0;APS5_VERIFY_DRAW_RECIPE=1;APS5_VERIFY_REGISTER_KEYS=1" SKIP_RETURN_CODE 77) + set_tests_properties(agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline PROPERTIES FAIL_REGULAR_EXPRESSION "FATAL:") + set_property(TEST agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off APPEND PROPERTY ENVIRONMENT "APS5_VERIFY_DRAW_RECIPE=1" "APS5_VERIFY_REGISTER_KEYS=1") + add_test(NAME agc_driver_draw_data_hit_user_words_verify COMMAND agc_driver_draw_data_hit_user_words_tests) + set_tests_properties(agc_driver_draw_data_hit_user_words_verify PROPERTIES ENVIRONMENT "APS5_VERIFY_DRAW_DATA_HITS=1" SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_driver_draw_cache_reuse_off PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline agc_driver_draw_data_hit_user_words_verify PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_stencil_replay_no_gpl COMMAND agc_driver_stencil_replay_tests) + set_tests_properties(agc_driver_stencil_replay_no_gpl PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_GPL=1" TIMEOUT 30 SKIP_RETURN_CODE 77) + if(WIN32) + 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() + set_tests_properties(agc_driver_queued_index_read_order PROPERTIES TIMEOUT 60) + foreach(agcTest IN ITEMS agc_spirv_narrow_constant_store_tests agc_spirv13_image_tests agc_spirv_optimizer_tests agc_driver_constant_store_tests) if(NOT TARGET ${agcTest}) continue() @@ -1541,14 +2817,26 @@ if(BUILD_TESTING) endif() endforeach() endforeach() - foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync) + add_test(NAME agc_driver_array_layer_target_cpu COMMAND agc_driver_array_layer_target_tests) + set_tests_properties(agc_driver_array_layer_target_cpu PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_HOST_IMPORT=1" TIMEOUT 30 SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_driver_array_layer_target_cpu PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync abi_deferred_compute abi_deferred_pixel abi_deferred_stages abi_deferred_rectangle abi_deferred_undecodable_state abi_deferred_registration_failure abi_deferred_vertex_inputs abi_deferred_rectangle_pairs lazy lazy_sync) set(preparedTest "agc_driver_prepared_shaders_${preparedMode}") - if(preparedMode STREQUAL "indirect") + if(preparedMode MATCHES "^abi_deferred_") + string(REPLACE "_" "-" preparedArgument ${preparedMode}) + add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --${preparedArgument}) + elseif(preparedMode STREQUAL "indirect") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --indirect) elseif(preparedMode STREQUAL "deferred") 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") @@ -1558,10 +2846,17 @@ if(BUILD_TESTING) add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests) endif() set_tests_properties(${preparedTest} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") - if(preparedMode STREQUAL "uncached") + if(preparedMode MATCHES "^abi_deferred_") + set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_NO_FAILURE_MEMO=1") + elseif(preparedMode STREQUAL "uncached") 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() @@ -1569,6 +2864,18 @@ 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_queued_prebuilds COMMAND agc_driver_shutdown_threads_tests --queued-prebuilds) + 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_queued_prebuilds 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_queued_prebuilds 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) @@ -1626,6 +2933,7 @@ if(AGC_BUILD_VISUAL_TEST) target_link_libraries(agc_driver_visual_test PRIVATE shader_recompiler_agc_driver + decoder_png SDL2-static libc libkernel @@ -1683,6 +2991,73 @@ 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 +) +target_include_directories(agc_pm4_replay PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_pm4_replay PRIVATE libSceAgcDriver libc) +set_target_properties(agc_pm4_replay PROPERTIES CXX_EXTENSIONS OFF) +if(MINGW) + target_link_options(agc_pm4_replay PRIVATE -municode) +endif() +configure_windows_unwind(agc_pm4_replay) + +if(MINGW) + get_filename_component(replayCompilerDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY) + foreach(replayTool IN ITEMS agc_shader_replay agc_pm4_replay) + add_custom_command(TARGET ${replayTool} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${replayCompilerDirectory}/libgcc_s_seh-1.dll" + "${replayCompilerDirectory}/libstdc++-6.dll" + "${replayCompilerDirectory}/libwinpthread-1.dll" + "$" + VERBATIM + ) + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(agc_driver_pm4_replay_tests tests/Pm4Replay.cpp tools/Pm4Analysis.cpp) + target_include_directories(agc_driver_pm4_replay_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_driver_pm4_replay_tests PRIVATE libSceAgc libSceAgcDriver libkernel libc) + set_target_properties(agc_driver_pm4_replay_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(agc_driver_pm4_replay_tests) + add_dependencies(agc_driver_pm4_replay_tests agc_pm4_replay) + set(agcPm4ReplayCapture "${CMAKE_CURRENT_BINARY_DIR}/pm4-replay-capture") + add_test(NAME agc_driver_pm4_replay COMMAND agc_driver_pm4_replay_tests) + set_tests_properties(agc_driver_pm4_replay PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_DUMP_SUBMITS=${agcPm4ReplayCapture}" + FIXTURES_SETUP agc_pm4_replay_capture TIMEOUT 30) + add_test(NAME agc_driver_pm4_replay_tool COMMAND ${CMAKE_COMMAND} -DTOOL=$ "-DCAPTURE=${agcPm4ReplayCapture}" + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/Pm4ReplayTool.cmake) + set_tests_properties(agc_driver_pm4_replay_tool PROPERTIES + FIXTURES_REQUIRED agc_pm4_replay_capture TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_pm4_replay agc_driver_pm4_replay_tool PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_executable(agc_compute_pipeline_replay EXCLUDE_FROM_ALL tools/ComputePipelineReplay.cpp) target_include_directories(agc_compute_pipeline_replay PRIVATE ${LIBS_INCLUDE_DIR} @@ -1704,12 +3079,26 @@ if(BUILD_TESTING) set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES TIMEOUT 30) add_test(NAME agc_driver_covered_centroid_state COMMAND agc_driver_graphics_tests --covered-centroid-state) set_tests_properties(agc_driver_covered_centroid_state PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_sample_location_grid_fallback COMMAND agc_driver_graphics_tests --sample-location-grid-fallback) + set_tests_properties(agc_driver_sample_location_grid_fallback PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_sample_order_planning COMMAND agc_driver_graphics_tests --sample-order) + set_tests_properties(agc_driver_sample_order_planning PROPERTIES TIMEOUT 30) if(WIN32) set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") set_tests_properties(agc_driver_covered_centroid_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_sample_location_grid_fallback PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_sample_order_planning PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests) set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30) + foreach(mode IN ITEMS entry-cap byte-cap io invalid) + string(REPLACE "-" "_" name "${mode}") + add_test(NAME agc_compute_pipeline_capture_optional_${name} COMMAND agc_compute_pipeline_capture_tests --optional-${mode}) + set_tests_properties(agc_compute_pipeline_capture_optional_${name} PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_compute_pipeline_capture_optional_${name} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() if(WIN32) set_tests_properties(agc_compute_pipeline_capture PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() @@ -1930,6 +3319,14 @@ if(BUILD_TESTING) set_tests_properties(pixel_ancillary PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(interpolation_reference_range_tests ${LIBS_INCLUDE_DIR}/tests/InterpolationReferenceRange.cpp) + target_link_libraries(interpolation_reference_range_tests PRIVATE shader_recompiler_agc_driver libc) + configure_windows_unwind(interpolation_reference_range_tests) + add_test(NAME interpolation_reference_range COMMAND interpolation_reference_range_tests) + if(WIN32) + set_tests_properties(interpolation_reference_range PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_indexed_draw_tests ${LIBS_INCLUDE_DIR}/tests/GuestIndexedDraw.cpp) target_include_directories(guest_indexed_draw_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_indexed_draw_tests PRIVATE libSceAgc libSceAgcDriver libc) @@ -1980,6 +3377,20 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_buffer_resource_vcc_tests EXCLUDE_FROM_ALL tests/BufferResourceVcc.cpp) +target_link_libraries(agc_buffer_resource_vcc_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_buffer_resource_vcc_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_buffer_resource_vcc_tests) +if(BUILD_TESTING) + set_target_properties(agc_buffer_resource_vcc_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_buffer_resource_vcc COMMAND agc_buffer_resource_vcc_tests) + set_tests_properties(agc_buffer_resource_vcc PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_buffer_resource_vcc PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_vop1_sdwa_move_decode_tests EXCLUDE_FROM_ALL tests/Vop1SdwaMoveDecode.cpp) target_link_libraries(agc_vop1_sdwa_move_decode_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_vop1_sdwa_move_decode_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -1993,6 +3404,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_gcn_decode_tests EXCLUDE_FROM_ALL tests/GcnDecode.cpp) +target_link_libraries(agc_gcn_decode_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_gcn_decode_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_gcn_decode_tests) +if(BUILD_TESTING) + set_target_properties(agc_gcn_decode_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_gcn_decode COMMAND agc_gcn_decode_tests) + if(WIN32) + set_tests_properties(agc_gcn_decode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_pixel_position_export_tests EXCLUDE_FROM_ALL tests/PixelPositionExport.cpp) target_link_libraries(agc_pixel_position_export_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_pixel_position_export_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -2007,6 +3431,20 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_depth_range_remap_tests EXCLUDE_FROM_ALL tests/DepthRangeRemap.cpp) +target_link_libraries(agc_depth_range_remap_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_depth_range_remap_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_depth_range_remap_tests) +if(BUILD_TESTING) + set_target_properties(agc_depth_range_remap_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_depth_range_remap COMMAND agc_depth_range_remap_tests) + set_tests_properties(agc_depth_range_remap PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_depth_range_remap PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_position_invariance_tests EXCLUDE_FROM_ALL tests/PositionInvariance.cpp) target_link_libraries(agc_position_invariance_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_position_invariance_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -2103,3 +3541,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(_long_paths|_extended_paths)?|agc_driver_pipeline_journal)$") + set_property(TEST ${agcRegisteredTests} APPEND PROPERTY ENVIRONMENT "APS5_NO_PIPELINE_JOURNAL=1") +endif() diff --git a/core/libs/prx/libSceAgcDriver/Eq/include/Event.hpp b/core/libs/prx/libSceAgcDriver/Eq/include/Event.hpp index 3cc1ebdff5..2b7bcbe4b0 100644 --- a/core/libs/prx/libSceAgcDriver/Eq/include/Event.hpp +++ b/core/libs/prx/libSceAgcDriver/Eq/include/Event.hpp @@ -4,7 +4,7 @@ #include #include "SceTypes.hpp" -void AgcDriverDeliverEopInterrupt(std::uint32_t queue); +bool AgcDriverDeliverEopInterrupt(std::uint32_t queue); extern "C" int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue eq, int id, void* udata); extern "C" int APS5_VABI sceAgcDriverDeleteEqEvent(KernelEqueue eq, int id); diff --git a/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp b/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp index 564ec3806d..80774d7216 100644 --- a/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp +++ b/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp @@ -25,17 +25,20 @@ std::vector g_registrations; } -void AgcDriverDeliverEopInterrupt(std::uint32_t queue) { +bool AgcDriverDeliverEopInterrupt(std::uint32_t queue) { std::vector registrations; { std::lock_guard lock(g_mutex); registrations = g_registrations; } + bool delivered = false; for (const auto& registration : registrations) { if (registration.id == static_cast(queue)) { EqueueTriggerEvent_nid_postfix(registration.eq, static_cast(registration.id), EvfiltGraphicsCore, nullptr); + delivered = true; } } + return delivered; } extern "C" { 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..1e2b68353b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp @@ -0,0 +1,416 @@ +#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()); +} + +void FlushAsyncLog_nid_no_patch(); + +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/ComputePipelineCapture.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp index 270c29c79b..da6858faff 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp @@ -53,6 +53,8 @@ struct ComputePipelineCapture { static ComputePipelineCapture Decode(std::span bytes); static ComputePipelineCapture Read(const std::filesystem::path& path); std::filesystem::path Write(const std::filesystem::path& directory) const; + std::filesystem::path WriteOptional(const std::filesystem::path& directory) const; + static bool OptionalEnabled(); std::string Identity() const; }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/DeviceFault.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/DeviceFault.hpp new file mode 100644 index 0000000000..558a25bde7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DeviceFault.hpp @@ -0,0 +1,79 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DEVICEFAULT_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DEVICEFAULT_HPP + +#ifndef VK_NO_PROTOTYPES +#define VK_NO_PROTOTYPES +#endif +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +inline const char* DeviceFaultAddressKind(VkDeviceFaultAddressTypeEXT type) { + switch (type) { + case VK_DEVICE_FAULT_ADDRESS_TYPE_NONE_EXT: return "none"; + case VK_DEVICE_FAULT_ADDRESS_TYPE_READ_INVALID_EXT: return "read invalid"; + case VK_DEVICE_FAULT_ADDRESS_TYPE_WRITE_INVALID_EXT: return "write invalid"; + case VK_DEVICE_FAULT_ADDRESS_TYPE_EXECUTE_INVALID_EXT: return "execute invalid"; + case VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_UNKNOWN_EXT: return "instruction pointer unknown"; + case VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_INVALID_EXT: return "instruction pointer invalid"; + case VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_FAULT_EXT: return "instruction pointer fault"; + default: return "unknown"; + } +} + +inline std::string DeviceFaultText(const char (&text)[VK_MAX_DESCRIPTION_SIZE]) { + return std::string(text, strnlen(text, VK_MAX_DESCRIPTION_SIZE)); +} + +inline std::vector DeviceFaultReport(VkDevice device, PFN_vkGetDeviceFaultInfoEXT faultInfo, VkQueue queue, PFN_vkGetQueueCheckpointDataNV checkpointData) { + std::vector lines; + char line[VK_MAX_DESCRIPTION_SIZE + 128]; + if (checkpointData != nullptr && queue != VK_NULL_HANDLE) { + std::uint32_t count = 0; + checkpointData(queue, &count, nullptr); + std::vector points(count, VkCheckpointDataNV{VK_STRUCTURE_TYPE_CHECKPOINT_DATA_NV}); + if (count != 0) checkpointData(queue, &count, points.data()); + points.resize(count); + if (points.empty()) lines.emplace_back("[device-fault] no checkpoint reached"); + for (const auto& point : points) { + std::snprintf(line, sizeof(line), "[device-fault] checkpoint: program 0x%llx at stage 0x%x", static_cast(reinterpret_cast(point.pCheckpointMarker)), static_cast(point.stage)); + lines.emplace_back(line); + } + } + if (faultInfo == nullptr || device == VK_NULL_HANDLE) return lines; + VkDeviceFaultCountsEXT counts{VK_STRUCTURE_TYPE_DEVICE_FAULT_COUNTS_EXT}; + if (faultInfo(device, &counts, nullptr) != VK_SUCCESS) { + lines.emplace_back("[device-fault] vkGetDeviceFaultInfoEXT reported nothing"); + return lines; + } + std::vector addresses(counts.addressInfoCount); + std::vector vendors(counts.vendorInfoCount); + counts.vendorBinarySize = 0; + VkDeviceFaultInfoEXT info{VK_STRUCTURE_TYPE_DEVICE_FAULT_INFO_EXT}; + info.pAddressInfos = addresses.empty() ? nullptr : addresses.data(); + info.pVendorInfos = vendors.empty() ? nullptr : vendors.data(); + const auto result = faultInfo(device, &counts, &info); + addresses.resize(std::min(addresses.size(), counts.addressInfoCount)); + vendors.resize(std::min(vendors.size(), counts.vendorInfoCount)); + std::snprintf(line, sizeof(line), "[device-fault] result %d: %s", static_cast(result), DeviceFaultText(info.description).c_str()); + lines.emplace_back(line); + for (const auto& address : addresses) { + std::snprintf(line, sizeof(line), "[device-fault] %s at 0x%llx (precision 0x%llx)", DeviceFaultAddressKind(address.addressType), static_cast(address.reportedAddress), static_cast(address.addressPrecision)); + lines.emplace_back(line); + } + for (const auto& vendor : vendors) { + std::snprintf(line, sizeof(line), "[device-fault] vendor: %s (code 0x%llx, data 0x%llx)", DeviceFaultText(vendor.description).c_str(), static_cast(vendor.vendorFaultCode), static_cast(vendor.vendorFaultData)); + lines.emplace_back(line); + } + return lines; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp index e01a9ff0f6..685976d255 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp @@ -22,12 +22,16 @@ 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); std::size_t DisplayBufferOffset(const DisplayBuffer& buffer, std::uint32_t x, std::uint32_t y); std::vector DecodeDisplayBuffer(const DisplayBuffer& buffer, std::span source); std::vector ReadDisplayBuffer(const DisplayBuffer& buffer); +std::vector DisplayBufferTexels(const DisplayBuffer& buffer, std::span source); +std::vector ReadDisplayBufferTexels(const DisplayBuffer& buffer); +std::array DisplayBufferClearTexel(const DisplayBuffer& buffer, Graphics::DccKeys keys); std::array DisplayBufferClearPixel(const DisplayBuffer& buffer, Graphics::DccKeys keys); std::size_t DisplayBufferKeyBytes(const DisplayBuffer& buffer); Graphics::DccKeys DisplayBufferKeys(const DisplayBuffer& buffer); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp index 6ea0e234ca..ff879da824 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp @@ -3,8 +3,10 @@ #define VK_NO_PROTOTYPES #include +#include #include #include +#include namespace AgcDriver { @@ -17,6 +19,9 @@ std::uint32_t DisplayTexelBytes(std::uint64_t pixelFormat); enum class ResidentPresent : std::uint8_t { None, Blit, Convert }; ResidentPresent ResidentPresentPath(VkFormat storage, std::uint64_t pixelFormat, bool blitSource); +std::optional Hdr10SurfaceFormat(std::span formats, VkFormat preferred); +VkSurfaceFormatKHR DynamicRangeSurfaceFormat(std::span formats, std::uint64_t pixelFormat, bool hdrAvailable); +std::array PqClearColor(std::uint32_t word, bool redLow); std::optional ResidentPresentSlice(std::uint64_t imageAddress, std::uint64_t imageBytes, std::uint32_t slices, std::uint64_t displayAddress, std::uint64_t displayBytes); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/DrawInputsInPlace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/DrawInputsInPlace.hpp new file mode 100644 index 0000000000..e43c70bfff --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DrawInputsInPlace.hpp @@ -0,0 +1,14 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRAWINPUTSINPLACE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRAWINPUTSINPLACE_HPP + +#include + +namespace AgcDriver { + +inline bool DrawInputsInPlaceUsable(VkDriverId driver, bool windows) { + return !windows || driver != VK_DRIVER_ID_NVIDIA_PROPRIETARY; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp index 76b1a346fc..b599b88f95 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_HPP +#include #include #include "SceTypes.hpp" #include "SceShaders.hpp" @@ -16,10 +17,13 @@ extern "C" void AgcDriverRaiseWorkerThreadPriority_nid_postfix(const char* role) extern "C" void AgcDriverLockVulkanLoader_nid_postfix(); extern "C" void AgcDriverUnlockVulkanLoader_nid_postfix(); extern "C" void AgcDriverShutdown_nid_postfix(); +extern "C" void AgcDriverRequestScreenshot_nid_postfix(); extern "C" void AgcDriverSuspendPoint_nid_postfix(); +extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix(); extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader); extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span context, std::span primitive); extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span stages, std::span context, std::span primitive); extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType); +extern "C" void* AgcDriverStaticTable_nid_postfix(); #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp index 910f6ba77d..3bd84a70db 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp @@ -7,6 +7,7 @@ #include #include #include +#include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::DriverDetail { @@ -21,7 +22,7 @@ public: bool operator==(std::nullptr_t) const; private: - std::atomic> pointer; + AtomicSharedPtr pointer; }; class DeviceUseGate { diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp index 3667d7bfd1..39bb6c35c9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp @@ -3,6 +3,8 @@ #include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" #include +#include +#include namespace AgcDriver::DriverDetail { @@ -10,6 +12,16 @@ double TraceMs(); std::uint64_t SkippedShader(); +class RefusedDraw final : public std::runtime_error { +public: + RefusedDraw(std::uint64_t address, const std::string& what) : std::runtime_error(what), address(address) {} + std::uint64_t address; +}; + +bool SkipRefusedDraws(); + +void ReportRefusedDraw(const RefusedDraw& refused); + void require(bool condition, const char* reason); std::uint32_t readRegister(const Registers& registers, std::uint32_t offset); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp index 1591221fdf..5f3df6c538 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp @@ -17,6 +17,7 @@ #include #include #include +#include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::DriverDetail { @@ -26,12 +27,14 @@ struct DispatchVariant { std::vector> runs; std::vector words; + std::vector shared; std::uint64_t forgetSerial = 0; std::shared_ptr compiled; + std::array meshIndexDescriptor{}; std::shared_ptr shader; - std::atomic> recipe; + AtomicSharedPtr recipe; static constexpr std::uint32_t NoFlatBinding = std::numeric_limits::max(); std::vector dataPositions; @@ -39,6 +42,10 @@ struct DispatchVariant { std::uint32_t flatBinding = NoFlatBinding; std::uint32_t pushOffset = 0; + std::vector runBases; + std::uint64_t relocatableWords = 0; + std::vector> shaderDataPatches; + std::uint64_t userDataMask = ~std::uint64_t{0}; std::shared_ptr vertexInfo; std::shared_ptr memory; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index 83797987c1..3e1f59e9f4 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 @@ -10,7 +11,9 @@ #include #include #include +#include #include +#include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::DriverDetail { @@ -24,6 +27,12 @@ struct DrawProgram { std::shared_ptr snapshot; std::size_t codeOffset = 0; std::shared_ptr tailContinuation; + std::uint32_t resourceRegister = 0; + bool nullPixel = false; + bool merged = false; + bool mergedPointerRequired = false; + std::uint32_t mergedPointer = 0; + std::uint32_t hiddenUserWords = 0; }; struct DrawDecode { @@ -33,8 +42,13 @@ struct DrawDecode { std::vector roles; }; +void ReadUserWords(const QueueState& queue, DrawProgram& program, bool staticAbi); +void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32_t pointerBase, bool pointerRequired, bool staticAbi); void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment); +void AppendProgramMemory(std::vector& memory, const std::vector& programs); +std::vector> UserDataWords(const std::vector& programs, const std::vector& roles, std::uint32_t location); + struct PreparedGraphicsStage { std::shared_ptr snapshot; PreparedShaders::Entry entry; @@ -43,7 +57,7 @@ struct PreparedGraphicsStage { std::shared_ptr registration; }; -std::vector PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target); +std::vector PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target, bool deferUnprepared = false); struct DrawRecipeRecord { std::vector> stages; @@ -58,9 +72,17 @@ struct DrawEntry { std::vector>> stages; - std::atomic>> recipes; + AtomicSharedPtr> recipes; std::uint64_t touched = 0; std::list::iterator order; + std::uint64_t shape = 0; +}; + +using DrawDataCandidates = std::vector, std::vector>>>; + +struct StageDataHit { + std::vector words; + std::shared_ptr compiled; }; enum class DrawMiss : std::size_t { FrontDiffering, FragmentDiffering, OtherDiffering, Layout, Gate, Stages, Count }; @@ -73,6 +95,11 @@ struct DrawEntryCounters { std::uint64_t registerKeyLookups = 0, registerKeyHits = 0, decodeSkipped = 0, decodePartial = 0, facadeMismatches = 0, verifyDecodes = 0, verifyDecodeMismatches = 0; double keyUs = 0; + std::uint64_t absentNewRegisters = 0, absentUserWords = 0, absentEvicted = 0, absentNeverInserted = 0, evictions = 0; + std::array registerKeyMissChanges{}; + std::uint64_t differingSameRuns = 0, differingWords = 0, differingRunsChanged = 0; + std::uint64_t dataHits = 0, dataStagesReused = 0, dataStagesCompiled = 0, dataVerified = 0; + std::uint64_t stageDataHits = 0, stageDataWords = 0, stageDataVerified = 0, stageDataInserts = 0, stageDataPositions = 0; std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); }; @@ -93,9 +120,42 @@ enum class DrawVerdict { Drawn, Nothing, Rejected }; struct StageCapture { std::shared_ptr compiled; + std::shared_ptr capture; std::vector regions; + std::vector> supplied; std::uint64_t forgetSerial = 0; std::uint32_t pushOffset = 0; + ShaderRecompiler::SrtUserDataUse userDataUse; + std::uint64_t userDataMask = ~std::uint64_t{0}; +}; + +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 new file mode 100644 index 0000000000..9ca8b03dd5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp @@ -0,0 +1,85 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAW_DRAWPIPELINE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAW_DRAWPIPELINE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::DriverDetail { + +class DrawPipeline { +public: + using Commit = std::function; + using Range = std::pair; + enum class DrainReason : std::uint8_t { Packet, Flush, Labels, Capture, Indirect, Submission, Count }; + + 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 = {}, std::vector reads = {}); + 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); + std::optional PendingLabel(std::uint64_t address, std::size_t bytes); + static bool HeldReads(std::uint64_t address, std::size_t bytes); + void ReleaseRunningReads(); + +private: + struct Item { + Commit commit; + std::vector writes; + std::uint64_t labelAddress = 0; + std::vector labelBytes; + std::vector reads; + }; + DrawPipeline() = default; + void run(); + void rethrowFailure(); + void report(std::chrono::steady_clock::time_point now); + + std::mutex mutex; + std::condition_variable wake; + std::condition_variable idle; + std::deque items; + std::exception_ptr failure; + std::atomic outstanding{0}; + 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; + std::uint64_t commitErrors = 0; + std::uint64_t enqueued = 0; + std::uint64_t fullWaitNs = 0; + 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(); +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawProgramTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawProgramTrace.hpp new file mode 100644 index 0000000000..b5f44eb347 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawProgramTrace.hpp @@ -0,0 +1,22 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAW_DRAWPROGRAMTRACE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAW_DRAWPROGRAMTRACE_HPP + +#include "Recompiler.hpp" +#include +#include +#include + +namespace AgcDriver::DriverDetail { + +struct DrawProgramTraceEntry { + ShaderRecompiler::ShaderStage stage; + ShaderRecompiler::ProgramRole role; + std::uint64_t codeAddress; + std::uint64_t entryAddress; +}; + +std::string FormatDrawProgramTrace(std::uint64_t submission, std::uint32_t queue, std::span programs); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp index 74a562c8b5..cddc9abc90 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp @@ -2,10 +2,18 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_DRIVER_DRAW_DRAWREGISTERKEY_HPP #include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" namespace AgcDriver::DriverDetail { -std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords = false, bool incremental = true); +struct RegisterStateKeyParts { + std::uint64_t stableRegisters = 0; + std::uint64_t userDataRegisters = 0; + std::uint64_t auxiliaryRegisters = 0; +}; + +std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords = false, bool incremental = true, std::uint64_t* shapeKey = nullptr, RegisterStateKeyParts* parts = nullptr); +std::uint64_t DrawShaderSetKey(const QueueState& queue, const ShaderRegistry& registry); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index b3aa361927..0e96bcc002 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -35,7 +35,9 @@ #include #include #include +#include #include +#include #include #include @@ -48,10 +50,13 @@ public: ~Driver(); void Shutdown(); void WaitIdle(); + std::uint64_t DispatchesAdoptedAhead() const; void CheckFailure(); void ReportFailure(std::exception_ptr error); - void Submit(const Packet* packet, std::uint32_t queue); + void Submit(const Packet* packet, std::uint32_t queue, bool cpuFlip = false); + void SubmitFlip(const FlipInfo& info); void SuspendPoint(); + std::uint32_t ReserveEopFlip(const FlipInfo& info); void RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr& output); void UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptr& output); void AttachWindow(const PresentationWindow& window); @@ -61,15 +66,27 @@ public: void ResolveShaderAbi(const Shader* shader, std::span context, std::span primitive); void ResolveGraphicsStagesAbi(std::span stages, std::span context, std::span primitive); void ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType); + void SettleShaderPreparations(std::span stages); static void FoldDrawOffsets(const ShaderRecompiler::RecompileResult& result, const DrawProgram& program, Pm4::DrawParameters& parameters); static std::optional ClassifyIndirectDraw(const ShaderRecompiler::RecompileResult& result, const Graphics::State& graphics, const DrawProgram& frontProgram, const std::shared_ptr& localDevice, Pm4::DrawParameters& drawParameters, bool traceIndirect); - static std::string dumpRequest(std::uint64_t address, const ShaderRecompiler::RecompileRequest& request); + static void tagVariantRuns(DispatchVariant& variant, std::span regions, const ShaderRecompiler::SrtUserDataUse& use, bool eligible, std::uint32_t hiddenWords, std::span> supplied = {}); + static std::string dumpRequest(std::uint64_t address, const ShaderRecompiler::RecompileRequest& request, std::string_view suffix = {}); DrawEntryCounters DrawCacheCounters(); + EntryCounters DispatchCacheCounters(); + struct DispatchCacheFootprint { + std::size_t entries = 0; + std::uint64_t variantBytes = 0; + }; + DispatchCacheFootprint DispatchCacheSize(); + static std::uint64_t DispatchCacheBytes(); + static ShaderMemory::PendingWrite QueryPendingSpan(std::uint64_t address, std::size_t bytes, std::span known); + std::shared_ptr RegisteredShaders(); private: friend class SampledReadScope; friend struct PendingView; friend struct PacketTimer; + friend struct DriverTestAccess; void stop(); Driver(); void rethrowFailure() const; @@ -88,6 +105,12 @@ private: void enqueue(Submission submission); void noteHeldAtSubmit(Submission& submission, std::size_t cursor); static void forgetUnfinishedWrites(QueueWorker& worker, const Submission& submission); + static void forgetUnfinishedWrites(QueueWorker& worker, std::span labelWrites); + 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; bool orderReleased(std::uint32_t queue, std::uint64_t received) const; @@ -96,6 +119,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(); @@ -109,25 +133,41 @@ private: void dispatch(QueueState& queue, std::span packet, const Submission& submission, std::uint64_t indirectArguments = 0); void verifyDataHit(const ShaderSnapshot& snapshot, std::size_t codeOffset, ShaderRecompiler::RecompileRequest request, std::span memory, std::uint64_t address, const DispatchVariant& variant, std::span liveWords, const ShaderRecompiler::RecompileResult& patched); static bool drawEntries(); + static bool drawDataHits(); + static bool verifyDrawDataHits(); + static bool drawStageDataHits(); static bool verifyDrawEntries(); static bool registerKeyEnabled(); static bool verifyDrawRecipe(); static std::size_t drawCacheEntries(); void accountDrawVariant(const DispatchVariant& variant, bool added); - void insertDrawEntry(std::uint64_t key, std::vector>& fresh, std::shared_ptr decode); - bool admitDrawKey(std::uint64_t key); + void insertDrawEntry(std::uint64_t key, std::vector>& fresh, std::shared_ptr decode, std::uint64_t shape = 0); + bool admitDrawKey(std::uint64_t key, std::uint64_t shape); void eraseDrawEntry(std::uint64_t key, const std::shared_ptr& expected); std::shared_ptr findDrawRecipe(std::uint64_t key, const std::vector>& stages); void attachDrawRecipe(std::uint64_t key, const std::vector>& stages, std::shared_ptr recipe); - void reportDrawCache(DrawEntryCounters& counters); - static std::uint64_t drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial); + static bool traceDrawCache(); + static void traceInsert(std::unordered_set& set, std::uint64_t key); + 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, 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 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); - void resolveDrawDecode(const QueueState& queue, const Submission& submission, std::shared_ptr& decode, bool registerKey, std::uint64_t drawKey, bool profile); - void lookupDraw(const Submission& submission, const std::shared_ptr& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector& programs, const std::vector& roles, const std::vector>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr& entry, std::vector>& matched, std::vector>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array& phaseMs); - std::shared_ptr materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected); - void cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional& indirectCpu, const std::vector& programs, const std::vector& stageCaptures, const std::vector>& vertexInfos, const std::vector>& decodeReads, bool verifyHit, const std::vector>& matched, std::vector>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr& decode, DrawPhaseTiming& phaseTiming); + std::shared_ptr decodeDraw(const QueueState& queue, const Submission& submission, const Graphics::Context& context); + void resolveDrawDecode(const QueueState& queue, const Submission& submission, const Graphics::Context& context, std::shared_ptr& decode, bool registerKey, std::uint64_t drawKey, bool profile); + void lookupDraw(const Submission& submission, const std::shared_ptr& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector& programs, const std::vector& roles, const std::vector>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr& entry, std::vector>& matched, std::vector>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array& phaseMs, const std::shared_ptr& dataEntry, DrawDataCandidates& dataCandidates, std::vector& stageData); + std::shared_ptr materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected, const std::optional& meshBarycentrics); + static void recordStageDataPositions(DispatchVariant& variant, const ShaderRecompiler::ResourceCapture& capture, const std::vector& decodeReads); + void cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional& indirectCpu, const std::vector& programs, const std::vector& stageCaptures, const std::vector>& vertexInfos, const std::vector>& decodeReads, bool verifyHit, const std::vector>& matched, std::vector>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr& decode, DrawPhaseTiming& phaseTiming, const std::vector& reused, const std::shared_ptr& entry, std::uint64_t shapeKey); static bool drawPrecheck(); std::optional precheckDraw(const QueueState& queue, const Submission& submission, std::span packet, const Pm4::DrawParameters& drawParameters, std::string& rejected, bool& traceIndirect); static std::uint32_t drawUserWord(const DrawProgram& program, std::int32_t sgpr); @@ -149,6 +189,7 @@ private: static bool fillClearEnabled(); static bool fillClearExactOnly(); static void fillClearCount(const Graphics::StorageTexture::FillCoverage& coverage, std::size_t bytes, std::span pattern, std::size_t discarded, bool cleared, const char* refusal); + void storeBufferFill(std::uint32_t queueId, std::uint64_t base, std::size_t bytes, const std::array& pattern, const std::shared_ptr& localDevice, bool memoryValueFill, bool noteMetadata); bool fillBuffer(QueueState& queue, std::uint32_t queueId, std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::shared_ptr& localDevice); static bool matchesCopyKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); static void countCopy(int path, std::size_t bytes, std::chrono::steady_clock::time_point started, std::chrono::steady_clock::time_point locked, const VulkanDevice::CopyOutcome* outcome = nullptr); @@ -160,7 +201,18 @@ private: static bool traceCopy(); void traceCopyPending(const char* what, std::uint64_t address, std::size_t bytes, std::uint64_t source, std::uint64_t destination, std::size_t copyBytes, const std::shared_ptr& localDevice); static void countIndirect(int path, double readMs); + static int indirectArgumentsPath(std::span packet, std::uint64_t arguments); void dispatchIndirect(QueueState& queue, std::span packet, const Submission& submission); + enum class AheadArguments { None, Matched, Differed }; + struct IndirectSource { + std::uint64_t arguments = 0; + int path = IndirectGpu; + std::uint32_t queue = 0; + AheadArguments ahead = AheadArguments::None; + }; + static IndirectSource& indirectSource(); + static std::string describeIndirectSource(); + std::array readDispatchArguments(std::uint32_t queue, std::uint64_t arguments, std::uint32_t initiator); static bool& sampledRead(); static bool writeEvidenceEnabled(); static bool writerKeyedEvidence(); @@ -170,6 +222,7 @@ private: static std::uint64_t hookWaits(); static ShaderMemory::HookWaitCounter hookWaitCounter(); static bool validateSkipEnabled(); + static bool vouchedValidation(); static bool validateLegacy(); static bool gateRetry(); static bool validateSkipVerify(); @@ -189,6 +242,13 @@ private: static void appendWrittenRanges(const ShaderRecompiler::RecompileResult& compiled, std::vector>& ranges); void noteForeignWriter(std::uint64_t begin, std::uint64_t end, std::uint32_t queue); void noteDrawWriters(std::span stages, std::uint32_t queue); + static std::vector> drawWriteRanges(const Graphics::State& graphics, std::span stages); + void commitDraw(std::shared_ptr localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span stages, std::span snapshots, const std::shared_ptr& recipe, const std::vector>& recipeStages, std::uint64_t drawKey); + bool enqueueLabelPacket(std::span packet, std::uint32_t opcode, std::uint32_t queue); + bool enqueueDmaPacket(std::span packet, std::uint32_t opcode, std::uint32_t queue, const QueueState& state); + void commitDmaStore(std::uint32_t queue, std::uint64_t address, std::span bytes); + void commitLabel(std::uint32_t queue, std::uint64_t address, std::span bytes, bool endOfPipeInterrupt); + void submitDueAfterCommit(VulkanDevice* localDevice); std::optional newestWriterLocked(std::uint64_t begin, std::uint64_t end) const; std::optional newestWriter(std::uint64_t begin, std::uint64_t end); std::string describeWriters(std::uint64_t begin, std::uint64_t end); @@ -197,9 +257,10 @@ private: void traceCapture(const char* what, std::uint64_t program, std::uint32_t queue, std::span regions, double waitedMs); void reportValidation(ValidateCounters& counters); bool captureStable(std::span captured); - bool validateCaptured(std::uint64_t program, std::uint32_t queue, std::span captured, const ShaderRecompiler::RecompileResult& compiled, bool inPlace, const PendingView& view, bool* unmapped = nullptr, std::optional* sampling = nullptr, const DataMask* data = nullptr); + bool validateCaptured(std::uint64_t program, std::uint32_t queue, std::span captured, const ShaderRecompiler::RecompileResult& compiled, bool inPlace, const PendingView& view, bool* unmapped = nullptr, std::optional* sampling = nullptr, const DataMask* data = nullptr, std::span vouched = {}); static void appendEntryRegions(const DispatchVariant& variant, std::vector& regions, const std::vector* words = nullptr); - bool syncPendingRuns(std::uint64_t program, std::uint32_t queue, const ShaderRecompiler::RecompileResult& compiled, std::span regions, std::uint64_t& synced, PendingView& pending, std::optional& sampling); + static std::shared_ptr patchDataWords(const DispatchVariant& variant, const std::vector& words); + bool syncPendingRuns(std::uint64_t program, std::uint32_t queue, const ShaderRecompiler::RecompileResult& compiled, std::span regions, std::uint64_t& synced, PendingView& pending, std::optional& sampling, std::span skipped = {}); EntryOutcome validateVariant(std::uint64_t program, std::uint32_t queue, const DispatchVariant& variant, std::span regions, std::uint64_t& imagesFlushed, std::uint64_t& runsSynced, std::optional& sampling, std::vector>* live = nullptr); static int waitTimeoutMs(); static bool pollReapAll(); @@ -228,7 +289,7 @@ private: std::chrono::steady_clock::time_point resolvedAt{}; }; struct GroupCaptureStats { - std::uint64_t runs = 0, resolved = 0, resolvedHits = 0, adopted = 0, addressMismatches = 0, keyMismatches = 0, failures = 0, stale = 0, labelOverlaps = 0, writtenBefore = 0, deviceRejects = 0; + std::uint64_t runs = 0, resolved = 0, resolvedHits = 0, adopted = 0, addressMismatches = 0, keyMismatches = 0, failures = 0, stale = 0, labelOverlaps = 0, writtenBefore = 0, deviceRejects = 0, argumentMismatches = 0; double resolveMs = 0, resolveMaxMs = 0, ageMs = 0, ageMaxMs = 0; std::chrono::steady_clock::time_point reported = std::chrono::steady_clock::now(); }; @@ -245,6 +306,9 @@ private: static Graphics::Recorder::LateStatistics& lateCountsSeen(); static EpochBumps& epochBumps(); static void bumpEpoch(std::uint64_t EpochBumps::*counter); + static std::chrono::steady_clock::time_point& lastEpochBump(); + static std::deque& releasedTails(); + void noteReleasedTails(const Submission& submission); static bool packetEpoch(); static bool labelTryEachPacket(); static std::chrono::microseconds labelFlushDeadline(); @@ -267,6 +331,8 @@ private: FrameTiming* frameTiming(); FrameTiming* includeTimingSubmission(const Submission& submission, bool firstSegment); void execute(const Submission& submission); + void deliverGuestEopInterrupt(std::uint32_t queue); + void deliverFlipEopInterrupt(std::uint32_t queue); void markCompleted(std::uint64_t serial); static const std::atomic*& workerQueued(); void reapCompletionLabels(); @@ -277,13 +343,16 @@ private: std::condition_variable changed; std::map workers; - std::uint64_t frameSerial = 0; + std::atomic frameSerial{0}; std::shared_ptr pendingFrameTiming; std::atomic flipsCounted{0}; std::atomic flipSerial{0}; std::atomic flipBatchesUnsignaled{0}; + std::mutex eopSelfSignalMutex; + std::unordered_map eopSelfSignalFlips; + alignas(64) std::atomic packetsInFlight{0}; std::atomic packetsDone{0}; @@ -304,14 +373,21 @@ private: std::atomic dataPendingMisses{0}; - using ValueSet = std::pair>, std::vector>; - std::unordered_map> priorValueSets; + std::unordered_map> priorValueSets; std::unordered_map> drawCache; + std::unordered_map drawShapes; + 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; + std::optional drawShapeSightings; DrawEntryCounters drawEntryCounters; @@ -324,6 +400,12 @@ private: std::map queues; std::map> outputs; + struct EopFlip { + std::uint32_t handle; + std::shared_ptr request; + }; + std::map eopFlips; + std::uint32_t nextEopToken = EopFlipTokenBase; DevicePointer device; std::mutex labelStoresMutex; @@ -344,6 +426,7 @@ private: std::uint32_t idleWaiters = 0; std::uint64_t queue0Executing = 0; + std::deque queue0Uncommitted; std::atomic orderHolders{0}; std::atomic runningWorkers{0}; std::atomic idleEpoch{0}; @@ -354,6 +437,7 @@ private: std::atomic evidenceReads{0}; std::atomic evidenceValidations{0}; + std::atomic dispatchesAdoptedAhead{0}; std::mutex writtenBuffersMutex; std::deque writtenBuffers; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp index ad4da0a768..0c72dfd1f1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp @@ -1,9 +1,14 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_BUFFERCOPY_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_BUFFERCOPY_HPP +#include "Recompiler.hpp" +#include #include #include #include +#include +#include +#include namespace AgcDriver::DriverDetail { @@ -13,6 +18,17 @@ extern std::atomic knownValueEntries, knownValueReads, knownValue enum CopyPath { CopyCpu = 0, CopyGpu = 1, CopyNotImported = 2, CopyAliased = 3, CopyRecordPending = 4, CopyShape = 5, CopyOverlapping = 6, CopyInaccessible = 7, CopyOverGpuMax = 8, CopyPaths = 9 }; +extern std::array, CopyPaths> copyPathCounts; + +struct StreamCopy { + std::uint64_t source = 0; + std::uint64_t destination = 0; + std::size_t bytes = 0; +}; + +bool MatchesStreamCopyKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); +std::optional StreamCopyRange(const std::vector& userData, std::uint32_t groups); + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp new file mode 100644 index 0000000000..261fd2f6f5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp @@ -0,0 +1,21 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAWWRITERANGES_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAWWRITERANGES_HPP + +#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include +#include +#include +#include + +namespace AgcDriver::DriverDetail { + +std::vector> DrawWriteRanges(const Graphics::State& graphics, std::span stages, bool exact); +std::vector> DispatchWriteRanges(const ShaderRecompiler::RecompileResult& compiled, bool& bounded); +bool ExactDrawWrites(); +std::vector> QueuedDrawReadRanges(const Pm4::DrawParameters& draw, std::span stages); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp index 86e8d091a9..404207df81 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp @@ -20,8 +20,10 @@ struct DwordPatternFill { }; std::optional MatchDwordPatternFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); -std::optional MatchZeroDwordFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); +std::optional MatchConstantDwordFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); +std::optional MatchClampedGlobalFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); std::optional MatchMemoryValueBufferFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); +std::optional MatchSgprPatternFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); bool IsByteFill(const std::array& pattern); std::optional KeysFillMemo(const std::array& pattern, bool coversStorageKeys, bool memoryValueFill); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp index f64ecfaa7f..e71457e0f3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace AgcDriver::DriverDetail { 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 82c05c7ff9..203940d7c3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -13,10 +13,12 @@ #include #include #include +#include #include #include #include #include +#include #include struct Shader; @@ -32,7 +34,94 @@ class VulkanDevice; namespace AgcDriver::DriverDetail { struct ShaderSnapshot; -using ShaderRegistry = std::map>>; + +class ShaderRegistry { + using Chunk = std::map>>; + using Chunks = std::vector>; + +public: + using key_type = std::uint64_t; + using mapped_type = Chunk::mapped_type; + using value_type = Chunk::value_type; + + class const_iterator { + public: + using iterator_category = std::bidirectional_iterator_tag; + using value_type = ShaderRegistry::value_type; + using difference_type = std::ptrdiff_t; + using pointer = const value_type*; + using reference = const value_type&; + + const_iterator() = default; + reference operator*() const { return *entry; } + pointer operator->() const { return &*entry; } + const_iterator& operator++() { + if (++entry == (*chunk)->end()) { + ++chunk; + entry = chunk == last ? Chunk::const_iterator{} : (*chunk)->cbegin(); + } + return *this; + } + const_iterator operator++(int) { + auto previous = *this; + ++*this; + return previous; + } + const_iterator& operator--() { + if (chunk == last || entry == (*chunk)->cbegin()) { + --chunk; + entry = std::prev((*chunk)->cend()); + } else { + --entry; + } + return *this; + } + const_iterator operator--(int) { + auto previous = *this; + --*this; + return previous; + } + bool operator==(const const_iterator& other) const { return chunk == other.chunk && entry == other.entry; } + + private: + friend class ShaderRegistry; + const_iterator(Chunks::const_iterator chunk, Chunks::const_iterator last, Chunk::const_iterator entry) : chunk(chunk), last(last), entry(entry) {} + Chunks::const_iterator chunk{}; + Chunks::const_iterator last{}; + Chunk::const_iterator entry{}; + }; + using iterator = const_iterator; + + ShaderRegistry() = default; + ShaderRegistry(std::initializer_list entries) { + for (const auto& [address, headers] : entries) emplace(address, headers); + } + + const_iterator begin() const { return chunks.empty() ? end() : const_iterator(chunks.cbegin(), chunks.cend(), chunks.front()->cbegin()); } + const_iterator end() const { return const_iterator(chunks.cend(), chunks.cend(), {}); } + const_iterator find(std::uint64_t address) const; + const_iterator upper_bound(std::uint64_t address) const; + bool contains(std::uint64_t address) const { return find(address) != end(); } + const mapped_type& at(std::uint64_t address) const; + std::size_t size() const { return count; } + bool empty() const { return count == 0; } + mapped_type& operator[](std::uint64_t address); + std::pair emplace(std::uint64_t address, mapped_type headers); + std::size_t erase(std::uint64_t address); + void clear() { + chunks.clear(); + count = 0; + } + +private: + static constexpr std::size_t ChunkEntries = 64; + + std::size_t chunkFor(std::uint64_t address) const; + Chunk& writable(std::size_t index); + + Chunks chunks; + std::size_t count = 0; +}; struct PreparedShaderState { struct Entry { @@ -69,6 +158,7 @@ struct PreparedShaders : PreparedShaderState { std::exception_ptr failure; std::atomic demanded{false}; bool pending = false; + std::vector convertedSamples; }; struct RegisteredShaderState { @@ -87,6 +177,7 @@ struct ShaderSnapshot { std::shared_ptr registeredState; std::shared_ptr> workgroupUse = std::make_shared>(0u); std::shared_ptr> positionVectorUse = std::make_shared>(0u); + std::shared_ptr> rawGraphicsNoted = std::make_shared>(false); std::optional userDataTailRegister; }; @@ -94,8 +185,9 @@ struct ShaderSnapshot { std::shared_ptr ReadRawShader(std::uint64_t address); std::shared_ptr ReadUserDataTailContinuation(std::span code, std::uint32_t userDataBase, std::span userData); std::shared_ptr ProgramSnapshot(const ShaderRegistry& shaders, std::uint64_t address, std::initializer_list types); -void NoteRawGraphicsDraw(); +void NoteRawGraphicsDraw(const ShaderSnapshot& snapshot); bool RawGraphicsDrawn(); +bool RawGraphicsDrawnIn(std::uint64_t begin, std::uint64_t end); bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset); // The PA_CL_VS_OUT_CNTL position export bits a vertex program is compiled with: none for a program // without pos1..pos3 exports, so its artifact does not depend on them. @@ -107,21 +199,26 @@ std::shared_ptr PrepareShaderWithDiagnosti std::uint64_t NullPixelProgramAddress(); ShaderSnapshot PrepareNullPixelProgram(const VulkanDevice& device); std::optional RegisteredFloatMode(const ShaderSnapshot& snapshot); +std::optional DispatchFloatMode(const ShaderSnapshot& snapshot, const Registers& live); std::shared_ptr RegisteredHeader(const ShaderRegistry& registry, const Shader* shader); std::shared_ptr RegisteredProgram(const ShaderRegistry& registry, std::uint64_t address, std::initializer_list types); void PublishRegisteredShader(std::shared_ptr& registry, const std::shared_ptr& snapshot); void SubmitRegistrationPreparation(std::function task, const PreparedShaders* prepared = nullptr); 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); +void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target, bool deferUnprepared = false); ShaderRecompiler::RectListShaders PreparedRectangle(const ShaderSnapshot& snapshot, std::uint64_t vertexId, std::uint64_t fragmentId); ShaderRecompiler::RectListShaders DrawRectangle(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint64_t vertexId, std::uint64_t fragmentId, const ShaderRecompiler::SpirvTarget& target); std::shared_ptr SourceHandleFor(const ShaderSnapshot& snapshot, std::size_t codeOffset, const ShaderRecompiler::RecompileRequest& request); ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& snapshot, std::size_t codeOffset, const ShaderRecompiler::RecompileRequest& request); +std::shared_ptr CaptureInvocation(ShaderMemory& memory, const ShaderSnapshot& snapshot, std::size_t codeOffset, const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::PreparedShaderInvocation& invocation); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelStores.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelStores.hpp new file mode 100644 index 0000000000..dc39feeb1a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelStores.hpp @@ -0,0 +1,18 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_SYNCHRONIZATION_LABELSTORES_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_SYNCHRONIZATION_LABELSTORES_HPP + +#include +#include + +namespace AgcDriver::DriverDetail { + +inline constexpr std::size_t LabelStoreHistory = 4; +inline constexpr std::size_t LabelStoreBytes = 64; + +inline bool KeepsLabelStore(std::uint64_t address, std::size_t bytes) { + return bytes != 0 && bytes <= LabelStoreBytes && bytes % 4 == 0 && address % 4 == 0; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp index 80cbe89d13..d807612d5d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp @@ -30,7 +30,7 @@ enum TrySite { TryLabel = 0, TryFlush, TryReap, TryPoll, TryIdle, TryCapture, Tr extern std::atomic triesFailed[TrySites]; -extern std::atomic storesOnGpu, storesBehindCompletions, storesOnCpu, storesDrained, copiesToHostMemory; +extern std::atomic storesOnGpu, storesBehindCompletions, storesOnCpu, storesDrained, dmaFills, copiesToHostMemory, copiesBehindPendingSources; extern std::map drainCounts; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/WaitMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/WaitMemory.hpp index dc1e3b7890..8157279e9e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/WaitMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/WaitMemory.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_WAITMEMORY_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_WAITMEMORY_HPP +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelStores.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include #include @@ -9,8 +10,6 @@ namespace AgcDriver::DriverDetail { -inline constexpr std::size_t LabelStoreHistory = 4; - inline constexpr const char* LabelRefusalNames[6] = {"none", "trust-off", "queued", "overwritten", "unclosed", "behind-completion"}; inline constexpr std::chrono::microseconds PollTryInterval{1000}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp new file mode 100644 index 0000000000..0d93c2c2a4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FLOAT32FLUSHCONTROL_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FLOAT32FLUSHCONTROL_HPP + +#include + +namespace AgcDriver { + +struct Float32FlushControl { + bool probe = false; + bool declared = false; +}; + +inline Float32FlushControl Float32FlushControlFor(VkDriverId driver, VkBool32 flushToZero, VkShaderFloatControlsIndependence independence) { + const bool declared = flushToZero == VK_TRUE && independence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE; + return {declared || driver == VK_DRIVER_ID_MOLTENVK, declared}; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/FloatFlushProbe.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/FloatFlushProbe.hpp new file mode 100644 index 0000000000..25a021fab4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/FloatFlushProbe.hpp @@ -0,0 +1,21 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FLOATFLUSHPROBE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FLOATFLUSHPROBE_HPP + +#include "Recompiler.hpp" + +#include + +namespace AgcDriver { + +class VulkanDevice; + +struct FloatFlushProbeResult { + std::uint32_t mismatches = 0; + std::uint32_t compared = 0; +}; + +FloatFlushProbeResult ProbeFloat32FlushToZero(VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target); + +} + +#endif 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 638dfe02d8..24e0fc7968 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,8 +61,13 @@ 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)); +#ifdef _WIN32 +using FreshCollectHook = bool (*)(std::uintptr_t address, std::size_t bytes, void (*written)(void*, std::uintptr_t, std::uintptr_t), void* context); +void SetFreshCollectHook(FreshCollectHook hook); +#endif bool Watched(std::uint64_t address, std::size_t bytes); std::uint64_t CollectWrites(std::uint64_t address, std::size_t bytes); bool UnchangedSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation); @@ -78,9 +84,11 @@ bool UnchangedCommittedSince(std::span& store); std::uint64_t StoreOwnBytes(std::uint64_t address, std::span source); bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generation); +bool StoredAllSince_nid_no_patch(std::uint64_t address, std::size_t bytes, std::uint64_t generation); // Collect epoch: within one epoch a range already collected is not walked again, CollectWrites // returns the current generation instead. A guest write landing between two collects of the same // epoch is seen by the next epoch, which is the ordering real hardware gives a CPU write made while a @@ -97,8 +105,11 @@ bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generati // for every collect: nothing is reused without an ordering point. APS5_PACKET_EPOCH=1 makes the // workers bump before every packet as before. CollectEpochBumps counts the bumps ([guestmem] line). void BumpCollectEpoch(); +void AdoptLatestCollectEpoch(); std::uint64_t CollectEpoch(); std::uint64_t CollectEpochBumps(); +std::uint64_t ThreadCollectEpoch(); +std::uint64_t UnwatchSerial(); std::uint64_t CollectWritesUncached(std::uint64_t address, std::size_t bytes); // The tracker's current generation (every collect and MarkWritten bumps it): a stamp taken after // a set of driver stores, for UnchangedSinceCollected to compare against later. Read under the @@ -190,6 +201,7 @@ unsigned long long CodeOffset(const void* address); // calls FlushGpuWrites itself. void SetFlushHook(void (*hook)(std::uint64_t address, std::size_t bytes)); void SetReadGuard(void (*guard)(std::uint64_t address, std::size_t bytes)); +void GuardRead(std::uint64_t address, std::size_t bytes); void FlushGpuWrites(std::uint64_t address, std::size_t bytes); // Moves with every ForgetPages call (the libc invalidator: memory unmapped or re-registered): odd // while the call stores its page states, moved past it after, so a reader that loads an even @@ -243,5 +255,6 @@ std::size_t CaptureCallerOffsets(std::span frames, unsigned } extern "C" void AgcDriverCheckGuestMemory_nid_postfix(const void* pointer, std::size_t bytes, std::size_t alignment, bool writable = false); +extern "C" void AgcDriverStoreGuestLabel_nid_postfix(std::uint64_t* pointer, std::uint64_t value); #endif 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/HostHdr.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/HostHdr.hpp new file mode 100644 index 0000000000..c2cf033cdd --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/HostHdr.hpp @@ -0,0 +1,65 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_HOSTHDR_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_HOSTHDR_HPP + +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" +#include +#include + +namespace AgcDriver { + +struct HostDisplayHdr { + bool hdr = false; + float maxLuminance = 0.0f; + float minLuminance = 0.0f; + float maxFullFrameLuminance = 0.0f; +}; + +struct HostHdrStatus { + bool available = false; + bool surfaceHdr10 = false; + HostDisplayHdr display{}; + std::uint64_t generation = 0; + std::uint64_t displayGeneration = 0; + float reportedToneMapPeak = 0.0f; +}; + +inline constexpr HostDisplayHdr UndescribedHdrDisplay{true, 1000.0f, 0.0f, 1000.0f}; + +inline HostDisplayHdr DisplayHdrFromOutput(bool hdr10ColorSpace, float maxLuminance, float minLuminance, float maxFullFrameLuminance) { + if (!hdr10ColorSpace) return {}; + if (!std::isfinite(maxLuminance) || maxLuminance < 1.0f) return UndescribedHdrDisplay; + HostDisplayHdr display{true, maxLuminance, 0.0f, maxLuminance}; + if (std::isfinite(minLuminance) && minLuminance >= 0.0f && minLuminance < maxLuminance) display.minLuminance = minLuminance; + if (std::isfinite(maxFullFrameLuminance) && maxFullFrameLuminance >= 1.0f && maxFullFrameLuminance <= maxLuminance) display.maxFullFrameLuminance = maxFullFrameLuminance; + return display; +} + +inline float ToneMapPeakNits(const HostHdrStatus& status) { + return status.available ? status.display.maxLuminance : SdrToneMapLuminanceNits; +} + +inline float PqToSdrWhiteNits(const HostHdrStatus& status) { + return status.reportedToneMapPeak >= 1.0f ? status.reportedToneMapPeak : ToneMapPeakNits(status); +} + +void SetPresentingHdr10(bool hdr10); + +} + +extern "C" void AgcDriverSetHostDisplayHdr_nid_postfix(const AgcDriver::HostDisplayHdr& display); +extern "C" void AgcDriverSetSurfaceHdr10_nid_postfix(bool offered); +extern "C" void AgcDriverNoteReportedToneMapPeak_nid_postfix(float nits); +extern "C" void AgcDriverHostHdr_nid_postfix(AgcDriver::HostHdrStatus& status); +extern "C" bool AgcDriverPresentingHdr10_nid_postfix(); + +namespace AgcDriver { + +inline HostHdrStatus CurrentHostHdr() { + HostHdrStatus status; + AgcDriverHostHdr_nid_postfix(status); + return status; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp new file mode 100644 index 0000000000..653ff19e76 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp @@ -0,0 +1,223 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCEOVERLAY_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCEOVERLAY_HPP + +#define VK_NO_PROTOTYPES +#include +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +namespace Graphics { +class Buffer; +} + +enum class OverlayCorner : std::uint8_t { TopLeft, TopRight, BottomLeft, BottomRight }; + +struct OverlaySettings { + bool visible = false; + OverlayCorner corner = OverlayCorner::TopLeft; +}; + +OverlaySettings ParseOverlaySettings(const char* enabled, const char* position); + +class PerformanceCounters { +public: + static void AddGpuNanoseconds(std::uint64_t nanoseconds); + static void AddDriverNanoseconds(std::uint64_t nanoseconds); + static void CountPipeline(); + static std::uint64_t GpuNanoseconds(); + static std::uint64_t DriverNanoseconds(); + static std::uint64_t Pipelines(); + static bool GpuEnabled(); + static bool DriverEnabled(); +}; + +class PacketCounters { +public: + struct Totals { + std::uint64_t draws = 0; + std::uint64_t dispatches = 0; + }; + + static PacketCounters& ThisThread(); + static Totals Sum(); + + void CountDraw() { draws.store(draws.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); } + void CountDispatch() { dispatches.store(dispatches.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); } + +private: + alignas(64) std::atomic draws{0}; + std::atomic dispatches{0}; +}; + +struct OverlayCounters { + std::uint64_t gpuNanoseconds = 0; + std::uint64_t driverNanoseconds = 0; + std::uint64_t draws = 0; + std::uint64_t dispatches = 0; + std::uint64_t pipelines = 0; +}; + +class OverlayStatistics { +public: + using Clock = std::chrono::steady_clock; + static constexpr std::size_t FrameHistory = 120; + static constexpr Clock::duration Window = std::chrono::seconds(1); + + struct Range { + double minimum = 0; + double average = 0; + double maximum = 0; + }; + + void AddPresent(Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds); + void AddPresent(Clock::time_point at, const OverlayCounters& counters); + void Reset(); + double Fps() const; + double LastFrameMs() const; + std::vector FrameTimes() const; + Range FrameTimeRange() const; + double GpuMsPerFrame() const; + double DriverMsPerFrame() const; + double DrawsPerFrame() const; + double DispatchesPerFrame() const; + std::uint64_t PipelinesInWindow() const; + OverlayCounters Latest() const; + +private: + struct Sample { + Clock::time_point at; + OverlayCounters counters; + }; + + double perFrame(std::uint64_t OverlayCounters::*counter) const; + + std::deque window; + std::optional previous; + std::array frameTimes{}; + std::size_t frameCount = 0; + std::size_t frameNext = 0; +}; + +struct OverlayRect { + std::int32_t x = 0; + std::int32_t y = 0; + std::int32_t width = 0; + std::int32_t height = 0; + std::uint32_t color = 0; +}; + +enum class OverlayColor : std::uint32_t { Background = 0, Text = 1, Graph = 2 }; +inline constexpr std::size_t OverlayColorCount = 3; + +struct OverlayContent { + std::vector above; + std::vector frameTimes; + std::vector below; +}; + +struct OverlayMemory { + std::uint64_t used = 0; + std::uint64_t budget = 0; +}; + +struct OverlayDetails { + std::optional vram; + std::optional ram; + VkExtent2D display{}; + VkExtent2D window{}; +}; + +struct OverlayLayout { + OverlayRect bounds; + std::vector rects; + std::int32_t scale = 1; +}; + +struct OverlayMetrics { + static constexpr std::int32_t GlyphWidth = 5; + static constexpr std::int32_t GlyphHeight = 7; + static constexpr std::int32_t Advance = 6; + static constexpr std::int32_t LinePitch = 9; + static constexpr std::int32_t Columns = 27; + static constexpr std::int32_t Padding = 3; + static constexpr std::int32_t Margin = 4; + static constexpr std::int32_t GraphHeight = 20; + static constexpr std::int32_t GraphGap = 2; + static constexpr std::int32_t InnerWidth = Columns * Advance - 1; +}; + +const std::array& OverlayGlyph(char character); +std::string FormatOverlayValue(double value); +std::string FormatOverlayRate(double value); +std::string FormatOverlayCount(double value); +std::string FormatOverlayGigabytes(std::uint64_t bytes); +OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver); +OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver, const OverlayDetails& details); +bool OverlaySampleDue(std::optional sampledAt, OverlayStatistics::Clock::time_point now); +std::optional ReadVramBudget(const Graphics::Context& context); +std::optional ReadProcessPrivateBytes(); +std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content); +OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner); +std::array, OverlayColorCount> OverlayPalette(VkFormat format, VkColorSpaceKHR colorSpace); + +class PerformanceOverlay { +public: + PerformanceOverlay(const Graphics::Context& context, std::size_t slots); + ~PerformanceOverlay(); + PerformanceOverlay(const PerformanceOverlay&) = delete; + PerformanceOverlay& operator=(const PerformanceOverlay&) = delete; + + static bool Visible(); + static void Toggle(); + static OverlayCorner Corner(); + + void NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display); + const OverlayStatistics& Statistics() const { return statistics; } + const OverlayDetails& Details() const { return details; } + OverlayRect Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout); + OverlayRect Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout, const OverlayContent& content, OverlayCorner corner); + void Release(std::size_t slot) noexcept; + +private: + struct Slot { + std::unique_ptr rects; + std::size_t capacity = 0; + std::vector> transient; + }; + + VkRenderPass renderPass(VkFormat format, VkImageLayout initialLayout, VkImageLayout finalLayout); + VkPipeline pipeline(VkFormat format, VkRenderPass compatible); + void releaseTransient(Slot& slot) noexcept; + void release() noexcept; + void restartIfToggled(); + + Graphics::Context context; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkShaderModule vertex = VK_NULL_HANDLE; + VkShaderModule fragment = VK_NULL_HANDLE; + std::map, VkRenderPass> renderPasses; + std::map pipelines; + std::vector slots; + OverlayStatistics statistics; + OverlayDetails details; + std::optional sampledAt; + std::uint64_t toggles = 0; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp index 0cefddf624..e683bc7695 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp @@ -10,6 +10,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -18,9 +22,17 @@ #include #include #include +#include #include #include +namespace AgcDriver { +class TraceThread; +} + +extern "C" bool AgcDriverTraceRecording_nid_postfix(); +extern "C" AgcDriver::TraceThread* AgcDriverTraceThread_nid_postfix(); + namespace AgcDriver { class VulkanDevice; @@ -122,17 +134,32 @@ private: Clock::duration begun{}; }; +inline thread_local std::uint64_t TraceBatchTag = 0; + class TraceThread { public: using Clock = std::chrono::steady_clock; static constexpr std::size_t DefaultCapacity = 262144; + static constexpr std::size_t MaximumCapacity = 4194304; static constexpr Clock::duration MinimumSlice = std::chrono::microseconds(20); + static std::size_t ParseCapacity(const char* events) { + if (events == nullptr) return DefaultCapacity; + const std::string_view text = events; + std::size_t capacity = 0; + const auto end = text.data() + text.size(); + const auto [last, error] = std::from_chars(text.data(), end, capacity); + if (text.empty() || error != std::errc{} || last != end || capacity == 0 || capacity > MaximumCapacity) + throw std::invalid_argument("APS5_TRACE_EVENTS=" + std::string(text) + " is not a whole number from 1 through " + std::to_string(MaximumCapacity)); + return capacity; + } + struct Slice { const char* scope; const char* stage; Clock::time_point begin; Clock::duration length; + std::uint64_t batch = 0; }; struct Total { @@ -147,6 +174,9 @@ public: std::span slices; std::vector totals; bool dropped = false; + std::uint64_t eventsLost = 0; + std::optional eventsFullAt; + bool totalsFull = false; }; inline static const std::atomic AlwaysBuffering{true}; @@ -170,7 +200,7 @@ public: folded = {}; } if (length < MinimumSlice) folded += length; - else append({scope, stage, begin, length}); + else append({scope, stage, begin, length, TraceBatchTag}); } Snapshot Collect() const { @@ -180,7 +210,10 @@ public: if (scope == nullptr) continue; snapshot.totals.push_back({scope, entry.stage.load(std::memory_order_relaxed), Clock::duration(entry.length.load(std::memory_order_relaxed)), entry.count.load(std::memory_order_relaxed)}); } - snapshot.dropped = dropped.load(std::memory_order_relaxed); + snapshot.eventsLost = lost.load(std::memory_order_acquire); + if (snapshot.eventsLost != 0) snapshot.eventsFullAt = Clock::time_point(Clock::duration(firstLost.load(std::memory_order_relaxed))); + snapshot.totalsFull = totalsFull.load(std::memory_order_relaxed); + snapshot.dropped = snapshot.eventsLost != 0 || snapshot.totalsFull; return snapshot; } @@ -195,7 +228,9 @@ private: void append(const Slice& slice) { const auto index = used.load(std::memory_order_relaxed); if (index == capacity) { - dropped.store(true, std::memory_order_relaxed); + const auto count = lost.load(std::memory_order_relaxed); + if (count == 0) firstLost.store(slice.begin.time_since_epoch().count(), std::memory_order_relaxed); + lost.store(count + 1, std::memory_order_release); return; } slices[index] = slice; @@ -218,7 +253,7 @@ private: entry.count.store(entry.count.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); return; } - dropped.store(true, std::memory_order_relaxed); + totalsFull.store(true, std::memory_order_relaxed); } const std::string name; @@ -227,7 +262,9 @@ private: const std::unique_ptr slices; const std::size_t capacity; std::atomic used{0}; - std::atomic dropped{false}; + std::atomic lost{0}; + std::atomic firstLost{0}; + std::atomic totalsFull{false}; std::array entries; bool sampled = false; std::uint64_t sampledPresent = 0; @@ -235,6 +272,18 @@ private: Clock::duration folded{}; }; +struct TraceHost { + std::string cpu; + std::uint32_t threads = 0; + std::uint64_t memoryMiB = 0; +}; + +struct TraceTitle { + std::string id; + std::string name; + std::string contentVersion; +}; + struct TraceSummary { std::string commit; std::optional dirty; @@ -249,12 +298,142 @@ struct TraceSummary { double gpuBusyMs = 0; std::string trace; std::optional> interval; + std::optional host; + std::optional title; +}; + +struct TraceBenchInterval { + double startS = 0; + double endS = 0; + std::uint64_t firstPresent = 0; + std::uint32_t width = 0; + std::uint32_t height = 0; + double gpuBusyMs = 0; + std::vector frameUs; +}; + +using TraceBenchIntervals = std::vector>; + +class TraceBench { +public: + static constexpr std::size_t MaximumIntervals = 240; + + static void Keep(TraceBenchIntervals& kept, std::shared_ptr interval, std::uint64_t& dropped) { + kept.push_back(std::move(interval)); + if (kept.size() <= MaximumIntervals) return; + const auto excess = kept.size() - MaximumIntervals; + kept.erase(kept.begin(), kept.begin() + static_cast(excess)); + dropped += excess; + } + + static bool Parse(const char* bench, bool summaryEvery) { + if (bench == nullptr) return false; + if (std::string_view(bench) != "1") throw std::invalid_argument("APS5_TRACE_BENCH=" + std::string(bench) + " is not 1"); + if (!summaryEvery) throw std::invalid_argument("APS5_TRACE_BENCH=1 needs APS5_TRACE_SUMMARY_EVERY"); + return true; + } +}; + +struct TraceBenchSnapshot { + TraceSummary summary; + TraceBenchIntervals intervals; + std::uint64_t dropped = 0; +}; + +class TraceBenchWriter { +public: + explicit TraceBenchWriter(std::function writer) : writer(std::move(writer)) { + if (!this->writer) throw std::invalid_argument("invalid benchmark writer"); + worker = std::thread([this] { run(); }); + } + + TraceBenchWriter(const TraceBenchWriter&) = delete; + TraceBenchWriter& operator=(const TraceBenchWriter&) = delete; + + ~TraceBenchWriter() { + Stop(); + } + + void Stop() { + std::lock_guard stopLock(stopMutex); + { + std::lock_guard lock(mutex); + stopping = true; + } + changed.notify_one(); + if (worker.joinable()) worker.join(); + } + + void Write(TraceBenchSnapshot snapshot) { + { + std::lock_guard lock(mutex); + if (failure) std::rethrow_exception(failure); + if (stopping) throw std::runtime_error("benchmark writer is stopping"); + if (pending) ++superseded; + pending = std::move(snapshot); + } + changed.notify_one(); + } + + void Flush() { + std::unique_lock lock(mutex); + idle.wait(lock, [&] { return failure || (!pending && !writing); }); + if (failure) std::rethrow_exception(failure); + } + + std::uint64_t Superseded() { + std::lock_guard lock(mutex); + return superseded; + } + +private: + void run() noexcept { + try { + for (;;) { + TraceBenchSnapshot snapshot; + { + std::unique_lock lock(mutex); + changed.wait(lock, [&] { return stopping || pending.has_value(); }); + if (!pending) return; + snapshot = std::move(*pending); + pending.reset(); + writing = true; + } + writer(snapshot); + { + std::lock_guard lock(mutex); + writing = false; + } + idle.notify_all(); + } + } catch (...) { + { + std::lock_guard lock(mutex); + failure = std::current_exception(); + writing = false; + } + idle.notify_all(); + } + } + + std::function writer; + std::mutex stopMutex; + std::mutex mutex; + std::condition_variable changed; + std::condition_variable idle; + std::optional pending; + std::uint64_t superseded = 0; + bool writing = false; + bool stopping = false; + std::exception_ptr failure; + std::thread worker; }; struct TraceGpuRange { const char* name; double beginNs; double endNs; + std::uint64_t batch = 0; }; struct TraceClockSample { @@ -317,16 +496,21 @@ public: std::stable_sort(slices.begin(), slices.end(), [](const auto& a, const auto& b) { return a.begin != b.begin ? a.begin < b.begin : a.length > b.length; }); const auto folded = thread.name + " folded"; for (const auto& slice : slices) { - char text[64]; + char text[112]; if (slice.scope == nullptr) { event(folded, "C", slice.begin, tid); std::snprintf(text, sizeof text, ",\"args\":{\"ms\":%.3f}}", milliseconds(slice.length)); } else { event(Name(slice.scope, slice.stage), "X", slice.begin, tid); - std::snprintf(text, sizeof text, ",\"dur\":%.3f}", microseconds(slice.length)); + if (slice.batch != 0) std::snprintf(text, sizeof text, ",\"dur\":%.3f,\"args\":{\"batch\":%llu}}", microseconds(slice.length), static_cast(slice.batch)); + else std::snprintf(text, sizeof text, ",\"dur\":%.3f}", microseconds(slice.length)); } output += text; } + if (thread.eventsFullAt) { + event("trace buffer full", "i", *thread.eventsFullAt, tid); + output += ",\"s\":\"t\",\"args\":{\"events_lost\":" + std::to_string(thread.eventsLost) + "}}"; + } } for (std::size_t index = 0; index + 1 < presents.size(); ++index) { event("present", "i", presents[index], presenter + 1); @@ -345,7 +529,7 @@ public: const auto begin = map(range.beginNs); const auto end = map(range.endNs); if (end <= begin || begin < origin) continue; - slices.push_back({range.name, nullptr, begin, end - begin}); + slices.push_back({range.name, nullptr, begin, end - begin, range.batch}); } return slices; } @@ -391,6 +575,7 @@ public: AppendString(output, summary.driver); output += ",\"os\":"; AppendString(output, summary.os); + AppendMachine(output, summary); output += ",\"resolution\":"; AppendString(output, std::to_string(summary.width) + "x" + std::to_string(summary.height)); output += ",\"window\":{\"spec\":"; @@ -406,8 +591,22 @@ public: bool firstWorker = true; std::map phases; bool dropped = false; + struct DroppedThread { + std::string name; + std::uint64_t eventsLost = 0; + std::optional eventsFullAt; + bool totalsFull = false; + }; + std::vector droppedList; for (const auto& thread : threads) { dropped = dropped || thread.dropped; + if (thread.eventsLost != 0 || thread.totalsFull) { + auto entry = std::find_if(droppedList.begin(), droppedList.end(), [&](const DroppedThread& candidate) { return candidate.name == thread.name; }); + if (entry == droppedList.end()) entry = droppedList.insert(droppedList.end(), DroppedThread{thread.name}); + entry->eventsLost += thread.eventsLost; + if (thread.eventsLost != 0 && thread.eventsFullAt && (!entry->eventsFullAt || *thread.eventsFullAt < *entry->eventsFullAt)) entry->eventsFullAt = thread.eventsFullAt; + entry->totalsFull = entry->totalsFull || thread.totalsFull; + } Clock::duration busy{}; for (const auto& total : thread.totals) { const std::string_view scope = total.scope; @@ -430,13 +629,91 @@ public: } output += "},\"trace_dropped\":"; output += dropped ? "true" : "false"; + if (!droppedList.empty()) { + std::string droppedThreads; + for (const auto& entry : droppedList) { + if (!droppedThreads.empty()) droppedThreads += ','; + AppendString(droppedThreads, entry.name); + droppedThreads += ":{"; + if (entry.eventsLost != 0) { + if (entry.eventsFullAt && !presents.empty() && *entry.eventsFullAt >= presents.front() && *entry.eventsFullAt <= presents.back()) droppedThreads += "\"events_full_after_s\":" + number(std::chrono::duration(*entry.eventsFullAt - presents.front()).count()) + ","; + droppedThreads += "\"events_lost\":" + std::to_string(entry.eventsLost) + ","; + } + droppedThreads += std::string("\"totals_full\":") + (entry.totalsFull ? "true" : "false") + "}"; + } + output += ",\"trace_dropped_threads\":{" + droppedThreads + "}"; + } output += ",\"trace\":"; AppendString(output, summary.trace); output += "}\n"; return output; } + static std::vector FrameMicroseconds(std::span presents) { + std::vector frames; + for (std::size_t index = 1; index < presents.size(); ++index) { + const auto elapsed = std::chrono::duration_cast(presents[index] - presents[index - 1]).count(); + frames.push_back(static_cast(std::clamp(elapsed, 0, std::numeric_limits::max()))); + } + return frames; + } + + static std::string Bench(const TraceSummary& summary, std::span> intervals, std::uint64_t intervalsDropped) { + std::string output = "{\"format\":\"aps5-bench/1\",\"commit\":"; + AppendString(output, summary.commit); + output += ",\"dirty\":"; + output += summary.dirty.has_value() ? (*summary.dirty ? "true" : "false") : "null"; + output += ",\"env\":{"; + for (std::size_t index = 0; index < summary.environment.size(); ++index) { + if (index != 0) output += ','; + AppendString(output, summary.environment[index].first); + output += ':'; + AppendString(output, summary.environment[index].second); + } + output += "},\"gpu\":"; + AppendString(output, summary.gpu); + output += ",\"driver\":"; + AppendString(output, summary.driver); + output += ",\"os\":"; + AppendString(output, summary.os); + AppendMachine(output, summary); + output += ",\"intervals_dropped\":" + std::to_string(intervalsDropped) + ",\"intervals\":["; + for (std::size_t index = 0; index < intervals.size(); ++index) { + const auto& interval = *intervals[index]; + if (index != 0) output += ','; + output += "\n{\"start_s\":" + number(interval.startS) + ",\"end_s\":" + number(interval.endS); + output += ",\"first_present\":" + std::to_string(interval.firstPresent); + output += ",\"resolution\":"; + AppendString(output, std::to_string(interval.width) + "x" + std::to_string(interval.height)); + output += ",\"gpu_busy_ms\":" + number(interval.gpuBusyMs) + ",\"frame_us\":["; + for (std::size_t frame = 0; frame < interval.frameUs.size(); ++frame) { + if (frame != 0) output += ','; + output += std::to_string(interval.frameUs[frame]); + } + output += "]}"; + } + output += "\n]}\n"; + return output; + } + private: + static void AppendMachine(std::string& output, const TraceSummary& summary) { + if (summary.host) { + output += ",\"cpu\":"; + AppendString(output, summary.host->cpu); + output += ",\"cpu_threads\":" + std::to_string(summary.host->threads); + output += ",\"memory_mib\":" + std::to_string(summary.host->memoryMiB); + } + if (summary.title) { + output += ",\"title_id\":"; + AppendString(output, summary.title->id); + output += ",\"title\":"; + AppendString(output, summary.title->name); + output += ",\"content_version\":"; + AppendString(output, summary.title->contentVersion); + } + } + static double microseconds(Clock::duration elapsed) { return std::chrono::duration(elapsed).count(); } @@ -455,44 +732,47 @@ private: class PerformanceTrace { public: static bool Configured() { - static const bool configured = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr || std::getenv("APS5_TRACE_SUMMARY_EVERY") != nullptr; + static const bool configured = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr || std::getenv("APS5_TRACE_SUMMARY_EVERY") != nullptr || std::getenv("APS5_TRACE_BENCH") != nullptr; return configured; } static void BindThread(const char* name, std::size_t capacity = TraceThread::DefaultCapacity); static void Present(std::uint32_t width, std::uint32_t height, const VulkanDevice* device); - static void GpuRange(const char* name, double beginNs, double endNs) { + static void GpuRange(const char* name, double beginNs, double endNs, std::uint64_t batch = 0) { if (!buffering.load(std::memory_order_relaxed)) return; std::lock_guard lock(gpuMutex); if (gpuRanges.size() == gpuRanges.capacity()) { - gpuDropped = true; + ++gpuLost; return; } - gpuRanges.push_back({name, beginNs, endNs}); + gpuRanges.push_back({name, beginNs, endNs, batch}); } 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 bool gpuDropped = false; + 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 ce6f1638eb..909b403e26 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. @@ -88,7 +89,8 @@ void Validate(std::span packet, std::uint32_t queue); void Execute(std::span packet, QueueState& queue); inline bool IndirectRegisterOpcode(std::uint32_t opcode) { return opcode == 0x63 || opcode == 0x64 || opcode == 0x9f; } std::vector ReadIndirectRegisters(std::span packet); -void ExecuteIndirectRegisters(std::span packet, std::span pairs, QueueState& queue); +bool IndirectRegistersValid(std::span pairs); +void ExecuteIndirectRegisters(std::span packet, std::span pairs, QueueState& queue, bool skipInvalid = false); bool AccessesMemory(std::uint32_t header); // Whether an ACQUIRE_MEM packet asks only for GPU cache actions (no CPU-visible memory // synchronization): such a packet needs a pipeline barrier, not a device drain. @@ -142,11 +144,59 @@ struct MemoryCopy { std::optional DecodeMemoryCopy(std::span packet); std::uint32_t DmaSource(std::span packet); std::uint32_t DmaDestination(std::span packet); +inline constexpr std::uint32_t DmaSelectGds = 1; +inline constexpr std::uint32_t DmaSourceData = 2; +inline constexpr std::uint32_t DmaDestinationNowhere = 2; +inline std::size_t DmaBytes(std::span packet) { return packet[6] & 0x3ffffffu; } +inline constexpr std::uint32_t CopyDataGpuClockSource = 18; +inline std::uint32_t CopyDataSource(std::span packet) { return ((packet[1] & 0xfu) << 1u) | ((packet[1] >> 30u) & 1u); } +inline bool CopyDataImmediate(std::uint32_t source) { return source >= 10 && source != CopyDataGpuClockSource; } +inline bool CopyDataToRegister(std::span packet) { return ((packet[1] >> 8u) & 0xfu) == 0; } +inline std::size_t CopyDataBytes(std::span packet) { return (packet[1] & 0x10000u) != 0 ? 8 : 4; } +inline constexpr std::uint32_t RewindValid = 0x80000000u; +struct WaitFields { + std::uint32_t function; + std::uint64_t address; + std::uint64_t reference; + std::uint64_t mask; + std::uint32_t poll; + std::size_t bytes; +}; +WaitFields DecodeWait(std::span packet); +struct ConditionalWrite { + std::uint64_t compared; + std::uint64_t destination; + std::uint32_t value; +}; +ConditionalWrite DecodeConditionalWrite(std::span packet); +struct ConstantRamDump { + std::uint32_t offset; + std::uint32_t words; + std::uint64_t destination; +}; +ConstantRamDump DecodeConstantRamDump(std::span packet); +inline constexpr std::uint32_t SampleDumpEvent = 0x39; +inline constexpr std::uint64_t SampleDumpBlocks = 16; +inline constexpr std::uint64_t SampleDumpStride = 16; +inline constexpr std::uint64_t SampleDumpBlockBytes = 8; +inline constexpr std::uint64_t SampleDumpBytes = (SampleDumpBlocks - 1) * SampleDumpStride + SampleDumpBlockBytes; +inline bool SampleDump(std::span packet) { return ((packet[0] >> 8u) & 0xffu) == 0x46u && packet.size() >= 2 && (packet[1] & 0x3fu) == SampleDumpEvent; } +inline std::uint64_t SampleDumpAddress(std::span packet) { return packet[2] | (static_cast(packet[3]) << 32u); } +struct PacketStore { + enum class Value : std::uint8_t { Bytes, Pattern, RunTime }; + std::uint64_t address = 0; + std::uint64_t bytes = 0; + Value value = Value::RunTime; + std::vector data; + bool conditional = false; +}; +std::vector DecodeStores(std::span packet); // A DISPATCH_INDIRECT's arguments: the guest address of its three group-count dwords (no memory // access, so the GPU can read them in place: VulkanDevice::DispatchIndirect), the DISPATCH_DIRECT // packet made by reading them there (through the checked guest memory path, which waits for // recorded GPU work that writes them), and both steps in one for the packet. std::uint64_t DispatchArgumentAddress(std::span packet, const QueueState& queue); +void NoteDispatchRegisters(std::span packet, QueueState& queue); std::array ReadDispatchArguments(std::uint64_t arguments, std::uint32_t initiator); std::array PartialGroupThreads(const std::array& groups, const std::array& numThreads); std::array ResolveDispatch(std::span packet, const QueueState& queue); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp index 3f771fbb1a..3cdc977947 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp @@ -29,6 +29,7 @@ extern "C" void AgcDriverAttachWindow_nid_postfix(const AgcDriver::PresentationW extern "C" void AgcDriverPresentClear_nid_postfix(const AgcDriver::PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context); extern "C" void AgcDriverPresentBuffer_nid_postfix(const AgcDriver::PresentationWindow& window, const AgcDriver::DisplayBuffer& buffer, void (*gpuReady)(void*), void* context); extern "C" void AgcDriverReleaseWindow_nid_postfix(void* window); +extern "C" void AgcDriverTogglePerformanceOverlay_nid_postfix(); extern "C" void AgcDriverReportFailure_nid_postfix(std::exception_ptr error); #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp index 3bcf7f2085..337bfb0805 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp @@ -24,7 +24,7 @@ public: PresentationPass& operator=(const PresentationPass&) = delete; void Bind(std::size_t slot, VkImage image, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat, float whiteNits, std::uint32_t layer = 0); - void RecordPresent(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkExtent2D extent, VkImageLayout finalLayout); + void RecordPresent(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout finalLayout); void RecordDump(VkCommandBuffer commands, std::size_t slot, std::uint32_t stride, VkBuffer destination); void Release(std::size_t slot) noexcept; static bool TenBitFormat(VkFormat format); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp index 617a1399e0..533685ed98 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp @@ -42,7 +42,7 @@ public: { std::lock_guard lock(mutex); if (failure) std::rethrow_exception(failure); - if (stopping) throw std::runtime_error("profile output is stopping"); + if (stopping) return false; if (text.empty()) return true; if (text.size() > capacity - bytes) { ++dropped; @@ -69,6 +69,7 @@ public: } else if (static_cast(size) > capacity) { std::lock_guard lock(mutex); if (failure) std::rethrow_exception(failure); + if (stopping) return; ++dropped; changed.notify_one(); } else { diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp index 0d6a6da583..3954c462a3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp @@ -228,7 +228,7 @@ inline Registers BuildInitialContextRegisters() { {0x105, 0}, {0x106, 0}, {0x107, 0}, {0x108, 0}, {0x10b, 0}, {0x10c, 0}, {0x10d, 0}, {0x1b1, 0}, {0x1b3, 0}, {0x1b4, 0}, {0x1b6, 0}, {0x1c3, 0}, {0x1c4, 0}, {0x1c5, 0}, - {0x1ff, 0}, {0x292, 2}, {0x293, 0}, {0x29b, 0}, + {0x1ff, 0}, {0x280, 0x00080008}, {0x281, 0xffff0000}, {0x292, 2}, {0x293, 0}, {0x29b, 0}, {0x2ce, 0}, {0x2d3, 0}, {0x2d5, 0}, {0x2d6, 0}, {0x2db, 0}, {0x2dc, 0xaa00}, {0x2df, 0}, {0x2e4, 0}, {0x2f8, 0}, {0x2f9, 0x2d}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, {0x313, 0x6000}, diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp index 9a4dc16db6..3ab0d7e78d 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; @@ -82,7 +82,7 @@ struct DrawRecipe { std::array pushBytes{}; VkShaderStageFlags pushStages = 0; std::optional masked; - std::set fragmentOutputs; + std::uint32_t fragmentOutputs = 0; VkPipelineStageFlags shaderStages = 0; bool hasDepth = false; }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Screenshot.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Screenshot.hpp new file mode 100644 index 0000000000..2d8c1c8ec2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Screenshot.hpp @@ -0,0 +1,18 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SCREENSHOT_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SCREENSHOT_HPP + +#include +#include +#include +#include +#include + +namespace AgcDriver::Screenshot { + +void Request(); +bool Take(); +std::filesystem::path Write(std::string_view titleId, std::uint32_t width, std::uint32_t height, std::span bgra); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp index 75f597a2de..dacb784275 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERMEMORY_HPP #include "Recompiler.hpp" +#include #include #include #include @@ -15,6 +16,8 @@ namespace ShaderRecompiler { struct SourceHandle; class PreparedShaderInvocation; +struct SrtWriteStamp; +struct SrtGuestWords; } namespace AgcDriver { @@ -55,7 +58,10 @@ public: // by the driver: the [capture] line charges the waits made inside a capture to it. using WaitedMsProvider = double (*)(); static void SetWaitedMsProvider(WaitedMsProvider provider); - explicit ShaderMemory(std::span initial, PendingWriteQuery pendingWrite = nullptr, PendingWriteObserver observe = nullptr, HookWaitCounter hookWaits = nullptr); + explicit ShaderMemory(std::span initial, PendingWriteQuery pendingWrite = nullptr, PendingWriteObserver observe = nullptr, HookWaitCounter hookWaits = nullptr, PendingWriteQuery pendingSpan = nullptr); + ~ShaderMemory(); + ShaderMemory(const ShaderMemory&) = delete; + ShaderMemory& operator=(const ShaderMemory&) = delete; // Returns what the capture resolved (plan, snapshot, specialization) for // ShaderRecompiler::Recompile(request, capture), which then skips its own materialization. One // result per call: the draw path captures several stages on one ShaderMemory. With `handle` @@ -70,31 +76,88 @@ public: [[nodiscard]] std::vector TakeRecentRegions(); // The driver's source handle memo outcomes, for the [capture] line (APS5_PROFILE_DRAW). static void CountHandleMemo(bool hit); + struct SupplyCounts { + std::uint64_t shared, copied, spilled; + }; + static SupplyCounts Supplies(); + [[nodiscard]] std::vector> SuppliedBlocks() const; + struct VouchCounts { + std::uint64_t calls, blocks, refused, words; + }; + static VouchCounts Vouches(); + static bool Vouched(PendingWriteQuery pending, std::uint64_t address, std::uint64_t bytes, const ShaderRecompiler::SrtWriteStamp& stamp); private: std::shared_ptr capture(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle, const ShaderRecompiler::PreparedShaderInvocation* invocation); static constexpr std::size_t PageBytes = 4096; static constexpr std::size_t PageWords = PageBytes / sizeof(std::uint32_t); + class WordMask { + public: + [[nodiscard]] bool Test(std::size_t index) const { return (bits[index / 64u] >> (index % 64u) & 1u) != 0u; } + [[nodiscard]] bool TestBlock(std::size_t index) const { + const auto block = ((std::uint64_t{1} << BlockWords) - 1u) << (index % 64u / BlockWords * BlockWords); + return (bits[index / 64u] & block) == block; + } + void Set(std::size_t index) { bits[index / 64u] |= std::uint64_t{1} << (index % 64u); } + void SetAll() { bits.fill(~std::uint64_t{0}); } + void SetBlock(std::size_t index) { bits[index / 64u] |= ((std::uint64_t{1} << BlockWords) - 1u) << (index % 64u / BlockWords * BlockWords); } + void Reset() { bits.fill(0u); } + void SetRun(std::size_t first, std::size_t count); + [[nodiscard]] bool None() const; + [[nodiscard]] bool All() const { return std::all_of(bits.begin(), bits.end(), [](std::uint64_t word) { return word == ~std::uint64_t{0}; }); } + template + void ForEachRun(TRun&& run) const; + + private: + std::array bits{}; + }; + + static constexpr std::size_t BlockWords = 16; struct Page { - std::array words{}; - std::bitset valid; - std::bitset read; - std::bitset recent; + std::array words; + WordMask valid; + WordMask read; + WordMask recent; bool wordwise = false; + bool lazy = false; + }; + + struct Supplied { + std::shared_ptr words; + std::uint64_t begin = 0; + std::uint64_t end = 0; + bool recent = true; }; static bool read(void* context, std::uint64_t address, std::uint32_t* value); + static bool readWords(void* context, std::uint64_t address, std::uint32_t* values, std::uint64_t count); static bool accessible(void* context, std::uint64_t address, std::uint64_t bytes); + static bool pending(void* context, std::uint64_t address, std::uint64_t bytes); + static ShaderRecompiler::SrtWriteStamp stampWrites(void* context, std::uint64_t address, std::uint64_t bytes); + static bool unchangedSince(void* context, std::uint64_t address, std::uint64_t bytes, const ShaderRecompiler::SrtWriteStamp& stamp); + static bool supplyWords(void* context, const std::shared_ptr& words); Page& page(std::uint64_t base); + [[nodiscard]] std::size_t pageAtOrAfter(std::uint64_t base) const; + Page& addPage(std::size_t position, std::uint64_t base); + [[nodiscard]] bool overlapsInitial(std::uint64_t begin, std::uint64_t end) const; + bool place(std::uint64_t address, const std::uint32_t* values, std::uint64_t count, bool overwrite, bool recent); + void spill(std::size_t index); + void spillTouching(std::uint64_t begin, std::uint64_t end); + [[nodiscard]] std::vector collect(bool recentOnly) const; // Regions given at construction (the registered shader's code and header), referenced as given: // the caller keeps them alive for as long as the capture is used. - std::map> initial; - std::map pages; + std::vector>> initial; + std::vector> pages; + std::uint64_t lastBase = ~std::uint64_t{0}; + Page* lastPage = nullptr; + std::vector supplied; + std::vector> spilled; PendingWriteQuery pendingWrite = nullptr; PendingWriteObserver observe = nullptr; HookWaitCounter hookWaits = nullptr; + PendingWriteQuery pendingSpan = nullptr; }; // The positions, among a dispatch-cache variant's stored words, of the pure flat-SRT leaves a @@ -112,6 +175,25 @@ struct DataWordPositionCounts { }; DataWordPositionCounts DataWordPositions(std::span> runs, std::span> leaves, std::span otherReads, std::span words, std::span flattenedSrt, std::vector& positions, std::vector& slots); +struct SharedWords { + std::size_t first = 0; + std::size_t count = 0; + std::size_t offset = 0; + std::weak_ptr block; + std::size_t region = 0; + std::size_t regions = 0; +}; +void AppendSharedWords(std::vector& shared, std::size_t region, std::size_t first, std::span bytes, std::span> blocks); +bool SameCapturedWords(std::span left, std::span leftShared, std::span right, std::span rightShared, std::size_t* compared = nullptr); +struct VouchedRegions { + std::size_t begin = 0; + std::size_t end = 0; + std::size_t words = 0; + std::uint64_t low = 0; + std::uint64_t high = 0; +}; +std::size_t VouchSharedRegions(std::span shared, std::size_t regions, ShaderMemory::PendingWriteQuery pending, std::vector& vouched); + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp index 0c31480307..30fcf5b8f4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp @@ -1,11 +1,16 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATIONSCOPE_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATIONSCOPE_HPP +#include +#include + +struct Shader; + namespace AgcDriver::DriverDetail { class ShaderPreparationTransaction; } -extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix(); +extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix(const Shader* const* shaders, std::size_t count); extern "C" void AgcDriverCommitShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction); extern "C" void AgcDriverEndShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction) noexcept; @@ -13,7 +18,7 @@ namespace AgcDriver { class ShaderPreparationScope { public: - ShaderPreparationScope() : transaction(AgcDriverBeginShaderPreparation_nid_postfix()) {} + explicit ShaderPreparationScope(std::initializer_list shaders) : transaction(AgcDriverBeginShaderPreparation_nid_postfix(shaders.begin(), shaders.size())) {} ~ShaderPreparationScope() { AgcDriverEndShaderPreparation_nid_postfix(transaction); } ShaderPreparationScope(const ShaderPreparationScope&) = delete; ShaderPreparationScope& operator=(const ShaderPreparationScope&) = delete; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp new file mode 100644 index 0000000000..30f42ba2ab --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp @@ -0,0 +1,14 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SUBGROUPBARRIER_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SUBGROUPBARRIER_HPP + +#include + +namespace AgcDriver { + +inline bool SubgroupControlBarrierUsable(VkDriverId driver) { + return driver != VK_DRIVER_ID_AMD_PROPRIETARY; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/SubmissionCapture.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/SubmissionCapture.hpp new file mode 100644 index 0000000000..1db347dcca --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/SubmissionCapture.hpp @@ -0,0 +1,68 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SUBMISSIONCAPTURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SUBMISSIONCAPTURE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +enum class SubmissionKind : std::uint8_t { Submit, Rewind, Suspend }; + +struct CapturedSubmission { + std::uint64_t ordinal = 0; + std::uint32_t queue = 0; + std::uint64_t serial = 0; + std::uint64_t timeUs = 0; + SubmissionKind kind = SubmissionKind::Submit; + std::size_t dwords = 0; + std::vector commands; + std::vector heldAtSubmit; + std::vector> conditionalRanges; + bool heldKnown = true; + bool conditionalKnown = true; +}; + +class SubmissionCapture { +public: + static constexpr std::string_view IndexName = "index.txt"; + static constexpr std::string_view Magic = "aps5-submissions 1"; + static constexpr std::string_view Columns = "ordinal queue type serial time_us dwords kind file held conditional"; + static constexpr std::string_view ColumnsWithoutHeld = "ordinal queue type serial time_us dwords kind file"; + + explicit SubmissionCapture(const std::filesystem::path& directory); + SubmissionCapture(const SubmissionCapture&) = delete; + SubmissionCapture& operator=(const SubmissionCapture&) = delete; + + static SubmissionCapture* Active(); + static CapturedSubmission Prepare(std::uint32_t queue, SubmissionKind kind, std::span commands, const std::map& conditionalEnds); + void Take(CapturedSubmission& submission, std::uint64_t serial, std::uint64_t timeUs, const std::set& heldAtSubmit) noexcept; + void Write(const CapturedSubmission& submission); + + static std::string FileName(const CapturedSubmission& submission); + static std::string_view KindName(SubmissionKind kind); + static std::string_view QueueType(std::uint32_t queue); + static std::vector ReadIndex(const std::filesystem::path& directory); + static std::vector ReadCommands(const std::filesystem::path& directory, const CapturedSubmission& submission); + static std::vector Read(const std::filesystem::path& directory); + +private: + std::filesystem::path directory; + std::mutex mutex; + std::ofstream index; + std::atomic next{0}; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp index 9174bfbc06..ed7bb2a11d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp @@ -9,6 +9,7 @@ namespace AgcDriver { class TraceFile { public: static void WriteGzip(const std::filesystem::path& path, std::string_view text); + static void ReplaceGzip(const std::filesystem::path& path, std::string_view text); static void AppendLine(const std::filesystem::path& path, std::string_view line); }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp index e26b9c6570..2c7443e9a5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp @@ -10,6 +10,9 @@ namespace AgcDriver { class FrameTiming; +inline constexpr std::uint32_t EopFlipTokenBase = 0x04000000u; +inline constexpr std::uint32_t EopFlipTokenLimit = 0x07ffffffu; + inline constexpr std::uint32_t FlipPacketHeader = 0xc004105cu; inline constexpr std::uint32_t FlipPacketWords = 6; // AnyPS5's internal encoding for the driver-generated rendering wait. @@ -40,6 +43,7 @@ class IVideoOutput { public: virtual ~IVideoOutput() = default; virtual std::shared_ptr Reserve(const FlipInfo& info) = 0; + virtual std::shared_ptr TryReserve(const FlipInfo& info) { return Reserve(info); } virtual void WaitForFlipRoom() {} virtual std::shared_ptr CaptureRenderingWait(std::uint32_t index) { throw std::runtime_error("VideoOut: rendering waits are unsupported by this output"); @@ -49,6 +53,9 @@ public: } +extern "C" std::uint32_t AgcDriverReserveEopFlip_nid_postfix(const AgcDriver::FlipInfo& info); +extern "C" void AgcDriverSubmitFlip_nid_postfix(const AgcDriver::FlipInfo& info); + extern "C" void AgcDriverRegisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr& output); extern "C" void AgcDriverUnregisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr& output); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 5809ae0851..52303ede78 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -11,11 +11,13 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include #include #include #include #include #include +#include namespace AgcDriver { @@ -25,10 +27,13 @@ class StorageTexture; // Stage A of a dispatch's resource build (see VulkanDevice::PrepareDispatch); opaque to callers. struct PreparedDispatch; +struct ComputePipelineObjects; // The unlocked pre-checks of a recipe hit (see VulkanDevice::PrepareRecipe); opaque to callers. 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: @@ -36,17 +41,30 @@ 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(); + void SavePipelineCache(); + // 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; std::string DeviceName() const; std::string DriverInfo() const; + bool SupportsImageFormat(VkFormat format, VkImageUsageFlags usage) const; bool CalibrateClock(double& gpuNs, std::chrono::steady_clock::time_point& cpu) const; ShaderRecompiler::SpirvTarget Target() const; + Graphics::SampleOrder HostSampleOrder(const Graphics::State& graphics) const; std::optional GeometryLimits() const; + std::uint32_t MaxFragmentInputComponents() const; const std::uint32_t* WorkGroupCountLimit() const; 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; } @@ -57,6 +75,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 @@ -89,6 +108,7 @@ public: // Pending-label table lookup and open-batch overlap test for WAIT_REG_MEM (see Recorder). std::optional PendingLabel(std::uint64_t address, std::size_t bytes, std::uint64_t afterStamp, Graphics::Recorder::LabelRefusal* refusal = nullptr) const; bool OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) const; + void SealPassWriteOnly(); bool RecordedWritesSettled(std::uint64_t address, std::size_t bytes) const; bool StoresPendingOver(std::uint64_t address, std::size_t bytes) const; // Fills [address, address + bytes) of host-imported guest memory with a repeating 16-byte pattern, @@ -153,6 +173,7 @@ public: }; using CopyWriterNote = std::function value, std::uint64_t generation)>; CopyOutcome CopyBuffer(std::uint64_t destination, std::uint64_t source, std::size_t bytes, std::size_t cpuMax, std::size_t gpuMax, std::size_t knownMax, std::uint64_t programAddress, std::uint32_t queue, const CopyWriterNote& noteWriter); + bool GpuCopyPossible(std::uint64_t destination, std::uint64_t source, std::size_t bytes) const; // APS5_COPY_VERIFY=1 totals: CPU copies verified, whose source bytes changed over the sync (a // producer the rule missed), whose destination bytes were overwritten (a store the rule // missed), and whose destination an unsignaled batch read in place (the rule's own answer @@ -168,9 +189,18 @@ public: std::uint64_t aliased, created, noSource, shape, refused; }; static CopyAliasing CopyAliasCounts(); + struct ComputePipelineCreation { + 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); + void SelectDynamicRange(const DisplayBuffer* buffer); bool Presentable() const; bool DualSrcBlend() const; bool PrimitiveListRestart() const; @@ -226,6 +256,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 @@ -233,19 +267,19 @@ public: // records. Null when nothing is prepared: the resource cache may serve the dispatch (its // Revalidate stays under the mutex), or APS5_LOCKED_BUILD=1 keeps the whole build under it as // before. `shader` and `snapshots` must outlive the dispatch. - std::shared_ptr PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span snapshots, std::uint64_t dispatchThreads = 0); + std::shared_ptr PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span snapshots, std::uint64_t dispatchThreads = 0, std::uint64_t programAddress = 0); // APS5_PROFILE_DRAW: the parts of a PrepareDispatch in milliseconds, in the order key, find, // precollect, presync, stage A (the driver's 'prepare:' rows). static std::span PreparePhaseMs(const PreparedDispatch& prepared); // `recipe`, when given, receives the Recipe a successful call built for its dispatch-cache // variant (design_cpu_final M4): only when the resource cache served or took the object // (reusable, cacheable) and recipes are on (APS5_NO_DISPATCH_RECIPE=1 builds none); else null. - void Dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::span snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr prepared = nullptr, std::shared_ptr* recipe = nullptr); + void Dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::span snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr prepared = nullptr, std::shared_ptr* recipe = nullptr, std::optional> htileClear = std::nullopt); struct IndirectOutcome { int cpuReason; double argumentReadMs; }; - IndirectOutcome DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr prepared = nullptr, std::shared_ptr* recipe = nullptr); + IndirectOutcome DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr prepared = nullptr, std::shared_ptr* recipe = nullptr, const std::array& threadGroup = {}); // A value-equal dispatch-cache hit with a recipe (design_cpu_final M4), in two steps. // PrepareRecipe, WITHOUT GuestMemory::GpuMutex: the pre-checks (recipes on, the recipe's // device is this one, its template and pipeline objects still alive), the template's @@ -267,7 +301,7 @@ public: // with the recipe's, else the process aborts. `refreshByWords`: the driver's data-only hit // (`shader` carries live flat-SRT words the recipe's hash does not name), so the template's // data refresh is decided by the per-word compare instead of the hash. - RecipeOutcome DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr& hit, IndirectOutcome& outcome, const std::shared_ptr& verify = nullptr, bool refreshByWords = false); + RecipeOutcome DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr& hit, IndirectOutcome& outcome, const std::shared_ptr& verify = nullptr, bool refreshByWords = false, const std::array& threadGroup = {}); // Whether dispatch-cache hits use recipes (APS5_NO_DISPATCH_RECIPE unset) and whether every // hit is verified (APS5_VERIFY_RECIPE=1). static bool DispatchRecipes(); @@ -287,8 +321,10 @@ public: // `recipe`, when given, receives the DrawRecipe a recorded, cacheable, reusable, direct draw // built for its draw-cache entry (design_cpu_final M8); null otherwise. void Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots = {}, std::shared_ptr* recipe = nullptr); + bool DrawInputsInPlace() const; std::optional KnownDrawRejection(const Graphics::State& graphics, std::span shaders) const; void ColorMetadataPass(const Graphics::ColorMetadataPass& pass); + void DepthCopyPass(const Graphics::DepthCopyPass& pass); // A draw-cache hit recorded from its recipe (Graphics::DrawWithRecipe), under the mutex after // the packet's labels; Rebuild when the recipe's device is not this one or DrawWithRecipe // missed (nothing recorded: the caller runs Draw with the hit's stages and re-attaches). @@ -304,25 +340,41 @@ public: void EnqueueDraw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots = {}); bool PrebuildGraphicsLibraries(const Graphics::ShaderStages& stages, std::span shaders); -private: - VulkanDevice(const PresentationWindow* window, bool compileOnly); - friend struct VulkanDeviceTestAccess; +public: // 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); + bool completionStoresOver(std::uint64_t address, std::size_t bytes) const; + bool copyWaitsOnCompletions(std::uint64_t address, std::size_t bytes) const; + +private: + VulkanDevice(const PresentationWindow* window, bool compileOnly); + friend struct VulkanDeviceTestAccess; + VkPipeline splitComputePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, ComputePipelineObjects& objects, const std::array& base); ShaderRecompiler::SpirvTarget buildTarget() const; Graphics::Context buildContext() const; // Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups. - IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe); + IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, const std::array& threadGroup = {}, std::optional> htileClear = std::nullopt); // 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). void decideIndirect(RecordedDispatch& record, IndirectOutcome& outcome, char* groupsText); // The tail of a dispatch's device call from the open batch's command buffer to the completion // registration: keeps, the template data refresh, barriers, bind, push, dispatch, marks. void recordDispatch(RecordedDispatch& record); - bool present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display = nullptr, const std::shared_ptr& resident = nullptr, VkFilter residentFilter = VK_FILTER_LINEAR, bool dumpFrame = false, bool residentConvert = false, const VkClearColorValue* uniform = nullptr, std::uint32_t residentLayer = 0); + bool present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display = nullptr, const std::shared_ptr& resident = nullptr, VkFilter residentFilter = VK_FILTER_LINEAR, bool dumpFrame = false, bool residentConvert = false, const VkClearColorValue* uniform = nullptr, std::uint32_t residentLayer = 0, bool passthrough = false); + bool takeScreenshot(bool dumpFrame); + void recreateSwapchain(std::uint32_t width, std::uint32_t height, bool force); + void refreshSurfaceFormats(); + void setHdrMetadata(); + void ensureHdrScaler(VkFormat source); + void useSurfaceFormat(VkSurfaceFormatKHR format); struct State; std::unique_ptr state; std::uint64_t serial; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp index ec282a171f..4aa7625ba9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp @@ -5,6 +5,7 @@ namespace AgcDriver { // Starts sampling the calling thread when APS5_SAMPLE_WORKER names an output file. void StartWorkerSampler(); +void StartCommitterSampler(); void StopWorkerSampler(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp index a35d3fb193..22865df487 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp @@ -23,27 +23,33 @@ public: if (bytes == 0 || bytes > size || address < base || address - base > size - bytes) return false; const auto first = (address - base) / BlockBytes; const auto last = (address - base + bytes - 1) / BlockBytes; - return std::none_of(excluded.begin() + first, excluded.begin() + last + 1, [](auto generation) { return generation != 0; }); + const auto firstPage = (address - base) % BlockBytes / PageBytes; + const auto lastPage = (address - base + bytes - 1) % BlockBytes / PageBytes; + if (first == last) return (excluded[first] & pageMask(firstPage, lastPage + 1)) == 0; + if ((excluded[first] & pageMask(firstPage, PagesPerBlock)) != 0 || (excluded[last] & pageMask(0, lastPage + 1)) != 0) return false; + return std::none_of(excluded.begin() + first + 1, excluded.begin() + last, [](auto mask) { return mask != 0; }); } + bool ExcludedSpan(std::uint64_t address, std::size_t bytes, std::uint64_t* begin, std::uint64_t* end) const { + std::size_t first = 0; + std::size_t last = 0; + if (!excludedBlocks(address, bytes, first, last)) return false; + *begin = base + first * BlockBytes; + *end = std::min(base + (last + 1) * BlockBytes, base + size); + return true; + } bool Exclude(std::uint64_t address, std::size_t bytes, std::uint64_t generation = 1) { - if (bytes == 0 || size == 0) return false; - if (address < base) { - if (base - address >= bytes) return false; - bytes -= base - address; - address = base; - } - const auto offset = address - base; - if (offset >= size) return false; - bytes = static_cast(std::min(bytes, size - offset)); - const auto first = offset / BlockBytes; - const auto last = (offset + bytes - 1) / BlockBytes; + std::size_t first = 0; + std::size_t last = 0; + if (!excludedBlocks(address, bytes, first, last)) return false; bool changed = false; for (auto block = first; block <= last; ++block) { - changed = changed || excluded[block] == 0; + const auto pages = (std::min(BlockBytes, size - block * BlockBytes) + PageBytes - 1) / PageBytes; + const auto mask = pageMask(0, pages); + changed = changed || excluded[block] != mask; auto& stamp = excludedGenerations[block]; stamp = std::max(stamp, std::max(generation, 1)); - excluded[block] = 1; + excluded[block] = mask; } return changed; } @@ -58,25 +64,48 @@ public: const auto offset = address - base; if (offset >= size) return false; bytes = static_cast(std::min(bytes, size - offset)); - const auto first = offset / BlockBytes + (offset % BlockBytes != 0); + const auto firstPage = offset / PageBytes + (offset % PageBytes != 0); const auto stop = offset + bytes; - const auto end = stop / BlockBytes + (stop == size && stop % BlockBytes != 0); + const auto endPage = stop / PageBytes + (stop == size && stop % PageBytes != 0); + if (firstPage >= endPage) return false; + const auto first = firstPage / PagesPerBlock; + const auto last = (endPage - 1) / PagesPerBlock; bool changed = false; - for (auto block = first; block < end; ++block) { - if (excluded[block] != 0 && excludedGenerations.at(block) < generation) { + for (auto block = first; block <= last; ++block) { + const auto mask = pageMask(block == first ? firstPage % PagesPerBlock : 0, block == last ? (endPage - 1) % PagesPerBlock + 1 : PagesPerBlock); + if ((excluded[block] & mask) != 0 && excludedGenerations.at(block) < generation) { changed = true; - excluded[block] = 0; - excludedGenerations.erase(block); + excluded[block] &= static_cast(~mask); + if (excluded[block] == 0) excludedGenerations.erase(block); } } return changed; } private: + static std::uint16_t pageMask(std::size_t first, std::size_t end) { + return static_cast(((1u << end) - 1u) & ~((1u << first) - 1u)); + } + bool excludedBlocks(std::uint64_t address, std::size_t bytes, std::size_t& first, std::size_t& last) const { + if (bytes == 0 || size == 0) return false; + if (address < base) { + if (base - address >= bytes) return false; + bytes -= base - address; + address = base; + } + const auto offset = address - base; + if (offset >= size) return false; + bytes = static_cast(std::min(bytes, size - offset)); + first = offset / BlockBytes; + last = (offset + bytes - 1) / BlockBytes; + return true; + } static constexpr std::size_t BlockBytes = 65536; + static constexpr std::size_t PageBytes = 4096; + static constexpr std::size_t PagesPerBlock = BlockBytes / PageBytes; std::uint64_t base = 0; std::size_t size = 0; - std::vector excluded; + std::vector excluded; std::unordered_map excludedGenerations; }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp index 6137a82dcc..4009901feb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp @@ -1,5 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp" #include +#include +#include #include #include #include @@ -11,10 +13,52 @@ namespace AgcDriver { namespace { +class CaptureLimit final : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +class CaptureIoFailure final : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +std::atomic optionalDisabled{false}; + +void DisableOptionalCapture(const char* message) { + if (!optionalDisabled.exchange(true, std::memory_order_relaxed)) std::fprintf(stderr, "[compute-capture] optional capture disabled: %s\n", message); +} + void RequireCapture(bool condition, const char* message) { if (!condition) throw std::runtime_error(std::string("compute pipeline capture: ") + message); } +void RequireCaptureIo(bool condition, const char* message) { + if (!condition) throw CaptureIoFailure(std::string("compute pipeline capture: ") + message); +} + +void RequireCaptureCapacity(bool condition, const char* message) { + if (!condition) throw CaptureLimit(std::string("compute pipeline capture: ") + message); +} + +void CheckCaptureIo(const std::error_code& error, const char* message) { + if (error) throw CaptureIoFailure(std::string("compute pipeline capture: ") + message + ": " + error.message()); +} + +std::filesystem::file_status CaptureStatus(const std::filesystem::path& path) { + std::error_code error; + const auto status = std::filesystem::symlink_status(path, error); + if (error != std::errc::no_such_file_or_directory) CheckCaptureIo(error, "cannot inspect capture path"); + return status; +} + +std::uintmax_t CaptureFileSize(const std::filesystem::path& path) { + std::error_code error; + const auto size = std::filesystem::file_size(path, error); + CheckCaptureIo(error, "cannot size capture"); + return size; +} + std::string Hash(std::span bytes) { std::uint64_t hash = 14695981039346656037ull; for (const auto value : bytes) { @@ -65,13 +109,13 @@ private: }; std::vector ReadBytes(const std::filesystem::path& path) { - const auto size = std::filesystem::file_size(path); + const auto size = CaptureFileSize(path); RequireCapture(size <= ComputePipelineCapture::MaxFileBytes, "file exceeds 36 MiB"); std::ifstream input(path, std::ios::binary); - RequireCapture(input.good(), "cannot open capture"); + RequireCaptureIo(input.good(), "cannot open capture"); std::vector bytes(static_cast(size)); input.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); - RequireCapture(input.good() && input.peek() == std::char_traits::eof(), "capture changed while reading"); + RequireCaptureIo(input.good() && input.peek() == std::char_traits::eof(), "capture changed while reading"); return bytes; } @@ -94,7 +138,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; @@ -111,7 +155,7 @@ void ComputePipelineCapture::Validate() const { RequireCapture(binding.stages == VK_SHADER_STAGE_COMPUTE_BIT, "unsupported descriptor stage flags"); constexpr VkDescriptorBindingFlags supported = VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT | VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; RequireCapture((binding.flags & ~supported) == 0, "unsupported descriptor binding flags"); - RequireCapture((binding.flags & VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT) == 0 || (binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER && (layout.flags & VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) != 0), "unsupported update-after-bind descriptor"); + RequireCapture((binding.flags & VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT) == 0 || ((binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER || binding.type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE || binding.type == VK_DESCRIPTOR_TYPE_SAMPLER) && (layout.flags & VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) != 0), "unsupported update-after-bind descriptor"); RequireCapture((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) == 0 || binding.binding == highest, "variable descriptor count must use the highest binding"); } } @@ -234,32 +278,70 @@ std::filesystem::path ComputePipelineCapture::Write(const std::filesystem::path& const auto path = recordDirectory / "capture.aps5cp"; static std::mutex mutex; std::lock_guard lock(mutex); - std::filesystem::create_directories(directory); - RequireCapture(!std::filesystem::is_symlink(std::filesystem::symlink_status(directory)), "capture directory is a symbolic link"); - if (std::filesystem::exists(recordDirectory)) { - RequireCapture(std::filesystem::is_directory(recordDirectory) && !std::filesystem::is_symlink(std::filesystem::symlink_status(recordDirectory)) && !std::filesystem::is_symlink(std::filesystem::symlink_status(path)), "existing capture path is not a regular capture"); + std::error_code error; + std::filesystem::create_directories(directory, error); + CheckCaptureIo(error, "cannot create capture directory"); + RequireCapture(!std::filesystem::is_symlink(CaptureStatus(directory)), "capture directory is a symbolic link"); + const auto recordStatus = CaptureStatus(recordDirectory); + if (std::filesystem::exists(recordStatus)) { + RequireCapture(std::filesystem::is_directory(recordStatus) && !std::filesystem::is_symlink(recordStatus), "existing capture path is not a regular capture"); + const auto captureStatus = CaptureStatus(path); + RequireCapture(std::filesystem::is_regular_file(captureStatus) && !std::filesystem::is_symlink(captureStatus), "existing capture path is not a regular capture"); RequireCapture(ReadBytes(path) == bytes, "existing capture differs or is incomplete"); return path; } std::uintmax_t totalBytes = 0; std::size_t count = 0; - for (const auto& entry : std::filesystem::directory_iterator(directory)) { - RequireCapture(++count < 128, "capture directory reached 128 entries"); - RequireCapture(entry.is_directory() && !entry.is_symlink(), "capture directory contains an unexpected entry"); + auto iterator = std::filesystem::directory_iterator(directory, error); + CheckCaptureIo(error, "cannot enumerate capture directory"); + while (iterator != std::filesystem::directory_iterator{}) { + const auto& entry = *iterator; + const auto entryStatus = CaptureStatus(entry.path()); + if (std::filesystem::is_regular_file(entryStatus) && entry.path().filename() == ".DS_Store") { + iterator.increment(error); + CheckCaptureIo(error, "cannot advance capture directory"); + continue; + } + RequireCaptureCapacity(++count < 128, "capture directory reached 128 entries"); + RequireCapture(std::filesystem::is_directory(entryStatus) && !std::filesystem::is_symlink(entryStatus), "capture directory contains an unexpected entry"); const auto capture = entry.path() / "capture.aps5cp"; - RequireCapture(std::filesystem::is_regular_file(capture) && !std::filesystem::is_symlink(std::filesystem::symlink_status(capture)), "capture directory contains an incomplete record"); - const auto size = std::filesystem::file_size(capture); + const auto captureStatus = CaptureStatus(capture); + RequireCapture(std::filesystem::is_regular_file(captureStatus) && !std::filesystem::is_symlink(captureStatus), "capture directory contains an incomplete record"); + const auto size = CaptureFileSize(capture); RequireCapture(size <= MaxFileBytes, "existing capture exceeds size bound"); totalBytes += size; + iterator.increment(error); + CheckCaptureIo(error, "cannot advance capture directory"); } - RequireCapture(totalBytes + bytes.size() <= 256u * 1024u * 1024u, "capture directory exceeds 256 MiB"); - RequireCapture(std::filesystem::create_directory(recordDirectory), "capture directory was concurrently created"); + RequireCaptureCapacity(totalBytes + bytes.size() <= 256u * 1024u * 1024u, "capture directory exceeds 256 MiB"); + const bool created = std::filesystem::create_directory(recordDirectory, error); + CheckCaptureIo(error, "cannot create capture record directory"); + RequireCapture(created, "capture directory was concurrently created"); std::ofstream output(path, std::ios::binary); - RequireCapture(output.good(), "cannot create capture"); + RequireCaptureIo(output.good(), "cannot create capture"); output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); output.close(); - RequireCapture(output.good(), "cannot finish capture"); + RequireCaptureIo(output.good(), "cannot finish capture"); return path; } +bool ComputePipelineCapture::OptionalEnabled() { + return !optionalDisabled.load(std::memory_order_relaxed); +} + +std::filesystem::path ComputePipelineCapture::WriteOptional(const std::filesystem::path& directory) const { + if (!OptionalEnabled()) { + Validate(); + return {}; + } + try { + return Write(directory); + } catch (const CaptureLimit& error) { + DisableOptionalCapture(error.what()); + } catch (const CaptureIoFailure& error) { + DisableOptionalCapture(error.what()); + } + return {}; +} + } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp index de10849034..6f7dd42ade 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp @@ -2,7 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" -#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" +#include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/HalfFloatScanout.hpp" #include @@ -59,11 +59,11 @@ void decodeHalfFloatTexel(const std::byte* texel, std::byte* pixel, bool redLow) pixel[3] = static_cast(Graphics::HalfFloatScanoutCode(halves[3], true)); } -void decodeTexel(const std::byte* texel, std::byte* pixel, bool rgba, bool tenBit, bool pq) { +void decodeTexel(const std::byte* texel, std::byte* pixel, bool rgba, bool tenBit, bool pq, float whiteNits) { if (pq) { std::uint32_t word = 0; std::memcpy(&word, texel, 4); - const auto converted = PqToSdrPixel(word, rgba, SdrToneMapLuminanceNits); + const auto converted = PqToSdrPixel(word, rgba, whiteNits); for (std::size_t i = 0; i < converted.size(); ++i) pixel[i] = static_cast(converted[i]); return; } @@ -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"); @@ -167,6 +168,7 @@ std::vector DecodeDisplayBuffer(const DisplayBuffer& buffer, std::spa const auto blocksPerRow = (buffer.width + 127u) / 128u; const bool rgba = DisplayRedLow(buffer.pixelFormat); const bool pq = DisplayPq(buffer.pixelFormat); + const float whiteNits = pq ? PqToSdrWhiteNits(AgcDriver::CurrentHostHdr()) : 0.0f; // Bit 56 marks the 10-bit variant: A2B10G10R10 with red in the low bits (the scanout draws render // COLOR_2_10_10_10 into it). Presented as 8-bit BGRA by dropping the low two bits of each channel. const bool tenBit = (buffer.pixelFormat & PixelFormatUnormBit) != 0; @@ -177,14 +179,14 @@ std::vector DecodeDisplayBuffer(const DisplayBuffer& buffer, std::spa const auto tiled = buffer.tilingMode == 1 ? (static_cast(y) * pitch + x) * 4u : (static_cast(y / 128u) * blocksPerRow + x / 128u) * 65536u + tileOffset(x, y); const auto linear = (static_cast(y) * buffer.width + x) * 4; - decodeTexel(source.data() + tiled, pixels.data() + linear, rgba, tenBit, pq); + decodeTexel(source.data() + tiled, pixels.data() + linear, rgba, tenBit, pq, whiteNits); } } timing.Mark("detile_convert"); return pixels; } -std::array DisplayBufferClearPixel(const DisplayBuffer& buffer, Graphics::DccKeys keys) { +std::array DisplayBufferClearTexel(const DisplayBuffer& buffer, Graphics::DccKeys keys) { static_cast(DisplayBufferSize(buffer)); require(buffer.dccAddress != 0, "VideoOut: a display buffer without DCC metadata has no fast-clear value"); std::array texel{}; @@ -201,8 +203,13 @@ std::array DisplayBufferClearPixel(const DisplayBuffer& buffer, Gr std::snprintf(message, sizeof(message), "VideoOut: display buffer 0x%llx (format 0x%016llx) reads %s DCC keys at 0x%llx: only uncompressed keys or a uniform clear code that the display format encodes can be presented", static_cast(buffer.address), static_cast(buffer.pixelFormat), Graphics::DccKeysName(keys), static_cast(buffer.dccAddress)); throw std::runtime_error(message); } + return texel; +} + +std::array DisplayBufferClearPixel(const DisplayBuffer& buffer, Graphics::DccKeys keys) { + const auto texel = DisplayBufferClearTexel(buffer, keys); std::array pixel{}; - decodeTexel(texel.data(), pixel.data(), DisplayRedLow(buffer.pixelFormat) && !DisplayHalfFloat(buffer.pixelFormat), (buffer.pixelFormat & PixelFormatUnormBit) != 0, DisplayPq(buffer.pixelFormat)); + decodeTexel(texel.data(), pixel.data(), DisplayRedLow(buffer.pixelFormat) && !DisplayHalfFloat(buffer.pixelFormat), (buffer.pixelFormat & PixelFormatUnormBit) != 0, DisplayPq(buffer.pixelFormat), PqToSdrWhiteNits(AgcDriver::CurrentHostHdr())); return pixel; } @@ -243,11 +250,28 @@ 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}); } +std::vector DisplayBufferTexels(const DisplayBuffer& buffer, std::span source) { + require(source.size() == DisplayBufferSize(buffer), "VideoOut: invalid display buffer size"); + require(DisplayTexelBytes(buffer.pixelFormat) == 4u, "VideoOut: raw display texels are only read for 4-byte formats"); + std::vector texels(static_cast(buffer.width) * buffer.height * 4); + for (std::uint32_t y = 0; y < buffer.height; ++y) { + for (std::uint32_t x = 0; x < buffer.width; ++x) std::memcpy(texels.data() + (static_cast(y) * buffer.width + x) * 4, source.data() + texelOffset(buffer, x, y, 4u), 4); + } + return texels; +} + +std::vector ReadDisplayBufferTexels(const DisplayBuffer& buffer) { + const auto size = DisplayBufferSize(buffer); + GuestMemory::FlushGpuWrites(buffer.address, size); + GuestMemory::CheckRange(reinterpret_cast(buffer.address), size, buffer.alignment); + return DisplayBufferTexels(buffer, {reinterpret_cast(buffer.address), size}); +} + } extern "C" std::size_t AgcDriverDisplayBufferSize_nid_postfix(const AgcDriver::DisplayBuffer& buffer) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp index 34c6c08530..914f5961e6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp @@ -1,4 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include +#include +#include namespace AgcDriver { namespace { @@ -49,14 +52,40 @@ ResidentPresent ResidentPresentPath(VkFormat storage, std::uint64_t pixelFormat, return tenBit ? ResidentPresent::Convert : ResidentPresent::None; case VK_FORMAT_R8G8B8A8_UNORM: case VK_FORMAT_R8G8B8A8_SRGB: + case VK_FORMAT_R8G8B8A8_UINT: case VK_FORMAT_B8G8R8A8_UNORM: case VK_FORMAT_B8G8R8A8_SRGB: + case VK_FORMAT_B8G8R8A8_UINT: return tenBit ? ResidentPresent::None : ResidentPresent::Convert; default: return ResidentPresent::None; } } +std::optional Hdr10SurfaceFormat(std::span formats, VkFormat preferred) { + const auto offered = [&](VkFormat format) { + return std::any_of(formats.begin(), formats.end(), [&](const VkSurfaceFormatKHR& item) { return item.format == format && item.colorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT; }); + }; + const std::array tenBit{VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}; + if (std::find(tenBit.begin(), tenBit.end(), preferred) != tenBit.end() && offered(preferred)) return VkSurfaceFormatKHR{preferred, VK_COLOR_SPACE_HDR10_ST2084_EXT}; + for (const auto format : tenBit) { + if (offered(format)) return VkSurfaceFormatKHR{format, VK_COLOR_SPACE_HDR10_ST2084_EXT}; + } + return std::nullopt; +} + +VkSurfaceFormatKHR DynamicRangeSurfaceFormat(std::span formats, std::uint64_t pixelFormat, bool hdrAvailable) { + if (!DisplayPq(pixelFormat) || !hdrAvailable) return {VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}; + const auto format = Hdr10SurfaceFormat(formats, DisplayTexelFormat(pixelFormat)); + if (!format) throw std::runtime_error("VideoOut: HDR output is available but the surface offers no 10-bit HDR10 format"); + return *format; +} + +std::array PqClearColor(std::uint32_t word, bool redLow) { + const auto channel = [&](unsigned shift) { return static_cast((word >> shift) & 0x3ffu) / 1023.0f; }; + return redLow ? std::array{channel(0), channel(10), channel(20), 1.0f} : std::array{channel(20), channel(10), channel(0), 1.0f}; +} + std::optional ResidentPresentSlice(std::uint64_t imageAddress, std::uint64_t imageBytes, std::uint32_t slices, std::uint64_t displayAddress, std::uint64_t displayBytes) { if (slices == 0 || displayBytes == 0 || imageBytes % slices != 0 || imageBytes / slices != displayBytes || displayAddress < imageAddress) return std::nullopt; const auto offset = displayAddress - imageAddress; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp index 3e80f40e9b..b94858e431 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp @@ -1,7 +1,11 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include #include +#include +#include #include +#include namespace AgcDriver::DriverDetail { @@ -15,6 +19,23 @@ std::uint64_t SkippedShader() { return skipped; } +bool SkipRefusedDraws() { + static const bool skip = std::getenv("APS5_SKIP_REFUSED_DRAWS") != nullptr; + return skip; +} + +void ReportRefusedDraw(const RefusedDraw& refused) { + static std::mutex reportedMutex; + static std::set reported; + { + std::lock_guard lock(reportedMutex); + if (!reported.insert(refused.address).second) return; + } + const std::string_view what = refused.what(); + const auto line = what.substr(0, what.find('\n')); + std::fprintf(stderr, "[gpu] APS5_SKIP_REFUSED_DRAWS: skipping the draws that use shader 0x%llx: %.*s\n", static_cast(refused.address), static_cast(line.size()), line.data()); +} + void require(bool condition, const char* reason) { if (!condition) { throw std::runtime_error(std::string("AGC driver: ") + reason); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DataHitVerification.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DataHitVerification.cpp index a13acf5c8b..da9dac3de5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DataHitVerification.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DataHitVerification.cpp @@ -12,9 +12,9 @@ void Driver::verifyDataHit(const ShaderSnapshot& snapshot, std::size_t codeOffse std::fflush(stderr); std::abort(); }; - auto verifyMemory = std::make_shared(memory, &queryPendingWrite, &observePendingWrite, hookWaitCounter()); - const auto invocation = InvocationFor(snapshot, codeOffset, request); - const auto capture = verifyMemory->Capture(invocation); + auto verifyMemory = std::make_shared(memory, &queryPendingWrite, &observePendingWrite, hookWaitCounter(), &QueryPendingSpan); + auto invocation = InvocationFor(snapshot, codeOffset, request); + const auto capture = CaptureInvocation(*verifyMemory, snapshot, codeOffset, request, invocation); const auto regions = verifyMemory->Regions(); request.context.memory = regions; const auto result = invocation.Materialize(*capture); 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 7719b40f2d..808000d470 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -1,18 +1,68 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "Optimization/ResourceProgram.hpp" #include #include +#include #include +#include 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); @@ -57,11 +107,10 @@ void Driver::dispatch(QueueState& queue, std::span packet, resolved = ahead.packet; packet = resolved; indirectArguments = ahead.indirectArguments; - } else if (indirectArguments != 0 && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) { - - recordQueuedLabelsBeforeRead(submission.queue); + } else if (indirectArguments != 0 && !resolving && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) { + indirectSource().path = IndirectFillKernel; const auto readStart = std::chrono::steady_clock::now(); - resolved = Pm4::ReadDispatchArguments(indirectArguments, packet[4]); + resolved = readDispatchArguments(submission.queue, indirectArguments, packet[4]); countIndirect(IndirectFillKernel, std::chrono::duration(std::chrono::steady_clock::now() - readStart).count()); packet = resolved; indirectArguments = 0; @@ -70,12 +119,18 @@ 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{}; + std::optional> htileClearCompletion; if (resolving) { + if (indirectArguments == 0 && Graphics::MatchMultisampledSingleClear(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads)) return; if (!partialLastGroup && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; - if (!partialLastGroup && indirectArguments == 0 && matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; + if (!partialLastGroup && indirectArguments == 0 && (matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) || MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute))) return; } else { - if (const auto htile = Graphics::HtileDepthClearAddress(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads); htile != 0) Graphics::NoteHtileDepthClear(htile); + if (indirectArguments == 0 && Graphics::TakeMultisampledSingleClear(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads)) { + pendingDispatchPhases().outcome = DispatchOutcome::FillHle; + return; + } if (indirectArguments == 0 && (packet[4] & 0x20u) == 0) { if (const auto fill = Graphics::MatchMemoryValueFill(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads, {packet[1], packet[2], packet[3]})) Graphics::NoteMemoryValueFill(*fill); } @@ -83,6 +138,13 @@ void Driver::dispatch(QueueState& queue, std::span packet, pendingDispatchPhases().outcome = DispatchOutcome::FillHle; return; } + 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); + const auto bytes = static_cast(threads * 16u); + if (Graphics::NoteHtileDepthClear(htile, bytes, nativeBytes)) htileClearCompletion = std::pair{htile, bytes}; + } if (!partialLastGroup && indirectArguments == 0 && copyBuffer(queue, submission.queue, packet, std::span(snapshot.code).subspan(codeOffset), userData, compute, localDevice, address)) { pendingDispatchPhases().outcome = DispatchOutcome::CopyHle; return; @@ -95,11 +157,16 @@ void Driver::dispatch(QueueState& queue, std::span packet, } } if (partialLastGroup) compute.partialThreads = partialGroups; + std::array threadGroup{}; + if (indirectArguments != 0 && (packet[4] & 0x20u) != 0) { + for (std::uint32_t axis = 0; axis < 3; ++axis) threadGroup[axis] = std::max(readRegister(queue.shader, 0x207 + axis) & 0xffffu, 1u); + compute.partialThreads = {~0u, ~0u, ~0u}; + } ShaderRecompiler::RecompileRequest request{ {ShaderRecompiler::ShaderStage::Compute, address, std::span(snapshot.code).subspan(codeOffset), snapshot.headerAddress, snapshot.header}, - {(packet[4] & 0x8000u) != 0 ? 32u : 64u, 0, userData, compute, std::nullopt, std::nullopt, memory, RegisteredFloatMode(snapshot)}, + {(packet[4] & 0x8000u) != 0 ? 32u : 64u, 0, userData, compute, std::nullopt, std::nullopt, memory, DispatchFloatMode(snapshot, queue.shader)}, localDevice->ComputeTarget((packet[4] & 0x8000u) != 0 ? 32u : 64u), - {0, 0, 0, 128} + {0, 0, 0, threadGroup != std::array{} ? 0u : 128u} }; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; static double captureMs = 0, keyMs = 0, recompileMs = 0, deviceMs = 0; @@ -216,6 +283,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); @@ -228,6 +316,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, cached = ahead.cached; validated = ahead.validated; ++groupStats.adopted; + ++dispatchesAdoptedAhead; const auto age = std::chrono::duration(std::chrono::steady_clock::now() - ahead.resolvedAt).count(); groupStats.ageMs += age; groupStats.ageMaxMs = std::max(groupStats.ageMaxMs, age); @@ -236,23 +325,36 @@ 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(); } else if (!fromAhead) { - shaderMemory = std::make_shared(memory, &queryPendingWrite, &observePendingWrite, hookWaitCounter()); + shaderMemory = std::make_shared(memory, &queryPendingWrite, &observePendingWrite, hookWaitCounter(), &QueryPendingSpan); std::uint64_t forgetAtCapture = 0; static const bool dumpShaders = std::getenv("APS5_DUMP_SHADERS") != nullptr; try { - const auto invocation = InvocationFor(snapshot, codeOffset, request); + auto invocation = InvocationFor(snapshot, codeOffset, request); timing.Mark("prepared_invocation"); const auto waitedBefore = traceCapSync() ? Graphics::Recorder::ThreadWaitedMs() : 0.0; forgetAtCapture = GuestMemory::ForgetSerial(); capture = [&] { const SampledReadScope sampling(evidenceReads); - return shaderMemory->Capture(invocation); + return CaptureInvocation(*shaderMemory, snapshot, codeOffset, request, invocation); }(); captured = shaderMemory->Regions(); request.context.memory = captured; @@ -272,7 +374,13 @@ void Driver::dispatch(QueueState& queue, std::span packet, totalMs += elapsed; if (profile && elapsed > 200) std::fprintf(stderr, "[gpu] compute shader 0x%llx recompile took %.0f ms (%zu SPIR-V words, %zu captured regions, total %.1f s)\n", static_cast(address), elapsed, compiledResult->spirv.size(), captured.size(), totalMs / 1000); } catch (const std::exception& error) { - const auto dump = dumpShaders ? dumpRequest(address, request) : std::string{}; + std::string dump; + if (dumpShaders) { + const auto regions = shaderMemory->Regions(); + auto failed = request; + failed.context.memory = regions; + dump = dumpRequest(address, failed, "_failed"); + } std::string reason = error.what(); if (const auto newline = reason.find('\n'); newline != std::string::npos) reason.resize(newline); @@ -297,8 +405,8 @@ void Driver::dispatch(QueueState& queue, std::span packet, parked.keepVariant = keepVariant; parked.attachVariant = attachVariant; parked.shaderMemory = shaderMemory; - parked.captured = captured; - parked.liveWords = liveWords; + parked.captured = std::move(captured); + parked.liveWords = std::move(liveWords); parked.capture = capture; parked.dataHit = dataHit; parked.cached = cached; @@ -338,7 +446,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, words += text; } } - std::fprintf(stderr, "[dispatch-io] shader 0x%llx%s\n", static_cast(address), words.c_str()); + std::fprintf(stderr, "[dispatch-io] queue=0x%x submission=%llu shader 0x%llx%s\n", submission.queue, static_cast(submission.serial), static_cast(address), words.c_str()); } const auto rethrow = [&](const std::exception& error) { @@ -349,13 +457,71 @@ void Driver::dispatch(QueueState& queue, std::span packet, } char where[64]; std::snprintf(where, sizeof(where), "compute shader 0x%llx: ", static_cast(address)); - throw std::runtime_error(where + std::string(error.what())); + throw std::runtime_error(where + std::string(error.what()) + describeIndirectSource()); }; 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), liveWords = std::move(liveWords), groups, address, queueId, recipe, keepVariant, dataHit, epoch, htileClearCompletion] { + DrawPipeline::FollowEpoch(epoch); + GuestMemory::SetCurrentPacket(0x15, queueId); + const auto& compiledCommit = *result; + const bool noteWrites = writeEvidenceEnabled() || traceCapSync(); + try { + std::shared_ptr recipeHit = htileClearCompletion.has_value() ? nullptr : localDevice->PrepareRecipe(recipe, false); + std::shared_ptr builtRecipe; + bool writersNoted = false; + for (;;) { + std::shared_ptr prepared; + if (recipeHit == nullptr || VulkanDevice::VerifyRecipes() || htileClearCompletion.has_value()) 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), htileClearCompletion.has_value() ? nullptr : &builtRecipe, htileClearCompletion); + } + 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); + recipeHit = htileClearCompletion.has_value() ? nullptr : localDevice->PrepareRecipe(keepVariant->recipe.load(std::memory_order_acquire), indirectArguments != 0); phaseTiming.Phase(PhaseRecipePrecheck); } @@ -363,13 +529,28 @@ void Driver::dispatch(QueueState& queue, std::span packet, auto* const attachTo = stampValidate() ? nullptr : keepVariant != nullptr ? keepVariant.get() : attachVariant.get(); bool writersNoted = false; const bool noteWrites = writeEvidenceEnabled() || traceCapSync(); + const auto keysFill = indirectArguments == 0 && !partialLastGroup ? MatchSgprPatternFill(packet, std::span(snapshot.code).subspan(codeOffset), userData, compute) : std::nullopt; + const auto filledKeys = keysFill ? KeysFillMemo(keysFill->pattern, false, true) : std::nullopt; + + std::optional depthPlaneClear; + if (indirectArguments == 0 && !partialLastGroup) { + 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; if (recipeHit == nullptr || VulkanDevice::VerifyRecipes()) { try { - prepared = localDevice->PrepareDispatch(compiled, snapshots, dispatchThreads); + prepared = localDevice->PrepareDispatch(compiled, snapshots, dispatchThreads, address); } catch (const std::exception& error) { if (rethrow(error)) return; } @@ -401,10 +582,12 @@ void Driver::dispatch(QueueState& queue, std::span packet, writersNoted = true; } phaseTiming.Phase(PhaseNoteWriters); + auto* const keysRecorder = filledKeys ? Graphics::Recorder::Active() : nullptr; + const auto keysNoteBefore = keysRecorder != nullptr ? keysRecorder->LastWriteNote(keysFill->base, keysFill->bytes) : 0; try { if (recipeHit != nullptr) { VulkanDevice::IndirectOutcome outcome{0, 0}; - const auto result = localDevice->DispatchRecipe(compiled, groups[0], groups[1], groups[2], indirectArguments, address, recipeHit, outcome, VulkanDevice::VerifyRecipes() ? prepared : nullptr, dataHit); + const auto result = localDevice->DispatchRecipe(compiled, groups[0], groups[1], groups[2], indirectArguments, address, recipeHit, outcome, VulkanDevice::VerifyRecipes() ? prepared : nullptr, dataHit, threadGroup); if (result == RecipeOutcome::Rebuild) { recipeHit = nullptr; @@ -413,14 +596,18 @@ void Driver::dispatch(QueueState& queue, std::span packet, } if (indirectArguments != 0) countIndirect(outcome.cpuReason, outcome.argumentReadMs); } else if (indirectArguments != 0) { - const auto outcome = localDevice->DispatchIndirect(compiled, indirectArguments, snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr); + const auto outcome = localDevice->DispatchIndirect(compiled, indirectArguments, snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr, threadGroup); countIndirect(outcome.cpuReason, outcome.argumentReadMs); } else { - localDevice->Dispatch(compiled, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr); + localDevice->Dispatch(compiled, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), htileClearCompletion.has_value() ? nullptr : (attachTo != nullptr ? &builtRecipe : nullptr), htileClearCompletion); } } catch (const std::exception& error) { if (rethrow(error)) return; } + if (keysRecorder != nullptr && keysRecorder == Graphics::Recorder::Active()) { + const auto note = keysRecorder->LastWriteNote(keysFill->base, keysFill->bytes); + if (note != 0 && note != keysNoteBefore) Graphics::NoteKeysFillOnGpu(keysFill->base, keysFill->bytes, *filledKeys); + } if (builtRecipe != nullptr) { if (dataHit) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp index 508bbbfcb1..0b6f1f0eb0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp @@ -35,6 +35,10 @@ bool& Driver::gateOpened() { return opened; } +std::uint64_t Driver::DispatchesAdoptedAhead() const { + return dispatchesAdoptedAhead.load(std::memory_order_relaxed); +} + void Driver::clearResolvedAhead() { auto& resolved = resolvedAhead(); if (resolved.empty()) return; @@ -146,7 +150,7 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l const auto argumentsEnd = arguments + 3u * sizeof(std::uint32_t); const bool written = std::any_of(writes.begin(), writes.end(), [&](const auto& range) { return range.first < argumentsEnd && arguments < range.second; }) || std::any_of(labels.begin(), labels.end(), [&](const RunLabel& label) { return label.address < argumentsEnd && arguments < label.address + label.size; }); - if (unknownWrites || written) { + if ((unknownWrites || written) && indirectArgumentsPath(packet, arguments) != IndirectGpu) { ++stats.writtenBefore; break; } @@ -155,6 +159,7 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l try { if (opcode == 0x15) dispatch(scratch, packet, submission); else dispatchIndirect(scratch, packet, submission); + Pm4::NoteDispatchRegisters(packet, scratch); } catch (const std::exception&) { ++stats.failures; break; @@ -202,10 +207,10 @@ void Driver::reportGroupCapture(std::uint32_t queue) { auto& stats = groupCaptureStats(); const auto now = std::chrono::steady_clock::now(); if (now - stats.reported < std::chrono::seconds(10)) return; - AgcDriver::ProfilePrint_nid_no_patch("[groupcap] queue 0x%x (10 s): %llu runs resolved %llu dispatches (%llu cache hits) in %.1f ms (max %.2f ms); %llu adopted %.2f ms after the resolve on average, max %.2f ms; mismatches: address %llu key %llu; %llu resolve failures, %llu stale, %llu label overlaps, %llu read an earlier dispatch's output, %llu rejected by the device\n", + AgcDriver::ProfilePrint_nid_no_patch("[groupcap] queue 0x%x (10 s): %llu runs resolved %llu dispatches (%llu cache hits) in %.1f ms (max %.2f ms); %llu adopted %.2f ms after the resolve on average, max %.2f ms; mismatches: address %llu key %llu; %llu resolve failures, %llu stale, %llu label overlaps, %llu read an earlier dispatch's output, %llu rejected by the device, %llu dropped because the argument re-read differed\n", queue, static_cast(stats.runs), static_cast(stats.resolved), static_cast(stats.resolvedHits), stats.resolveMs, stats.resolveMaxMs, static_cast(stats.adopted), stats.adopted != 0 ? stats.ageMs / static_cast(stats.adopted) : 0.0, stats.ageMaxMs, static_cast(stats.addressMismatches), static_cast(stats.keyMismatches), - static_cast(stats.failures), static_cast(stats.stale), static_cast(stats.labelOverlaps), static_cast(stats.writtenBefore), static_cast(stats.deviceRejects)); + static_cast(stats.failures), static_cast(stats.stale), static_cast(stats.labelOverlaps), static_cast(stats.writtenBefore), static_cast(stats.deviceRejects), static_cast(stats.argumentMismatches)); stats = GroupCaptureStats{}; stats.reported = now; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp index 93a62a8ce2..732d7733b4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp @@ -45,8 +45,27 @@ std::size_t Driver::dispatchCacheEntries() { return entries; } +std::uint64_t Driver::DispatchCacheBytes() { + static const std::uint64_t bytes = [] { + const char* text = std::getenv("APS5_DISPATCH_CACHE_BYTES"); + const auto parsed = text != nullptr ? std::strtoull(text, nullptr, 10) : 0ull; + return parsed != 0 ? static_cast(parsed) : std::uint64_t{256} << 20u; + }(); + return bytes; +} + +EntryCounters Driver::DispatchCacheCounters() { + std::lock_guard lock(dispatchCacheMutex); + return entryCounters; +} + +Driver::DispatchCacheFootprint Driver::DispatchCacheSize() { + std::lock_guard lock(dispatchCacheMutex); + return {dispatchCache.size(), dispatchCacheVariantBytes}; +} + std::uint64_t Driver::variantBytes(const DispatchVariant& variant) { - return sizeof(DispatchVariant) + variant.words.size() * sizeof(std::uint32_t) + variant.runs.size() * sizeof(std::pair) + variant.captured.size() * sizeof(ShaderRecompiler::MemoryRegion) + (variant.vertexInfo != nullptr ? sizeof(ShaderRecompiler::ShaderVertexStageInfo) : 0); + return sizeof(DispatchVariant) + variant.words.size() * sizeof(std::uint32_t) + variant.shared.size() * sizeof(SharedWords) + variant.runs.size() * sizeof(std::pair) + variant.captured.size() * sizeof(ShaderRecompiler::MemoryRegion) + (variant.vertexInfo != nullptr ? sizeof(ShaderRecompiler::ShaderVertexStageInfo) : 0); } void Driver::accountVariant(const DispatchVariant& variant, bool added) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCacheStatistics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCacheStatistics.cpp index dae236316c..5495a8c0aa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCacheStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCacheStatistics.cpp @@ -7,9 +7,24 @@ namespace AgcDriver::DriverDetail { void Driver::classifyDiffering(std::uint64_t program, std::uint64_t key, const DispatchVariant& old, const DispatchVariant& fresh, const ShaderRecompiler::ResourceCapture* capture, EntryCounters& counters) { ++counters.differingClassified; ++counters.differingByProgram[program]; + const auto valueSet = [](const DispatchVariant& variant) { + std::uint64_t hash = 0xcbf29ce484222325ull; + const auto mix = [&](std::uint64_t value) { + hash ^= value; + hash *= 0x100000001b3ull; + }; + mix(variant.runs.size()); + for (const auto& [begin, end] : variant.runs) { + mix(begin); + mix(end); + } + mix(variant.words.size()); + for (const auto word : variant.words) mix(word); + return hash; + }; auto& ring = priorValueSets[key]; - if (std::any_of(ring.begin(), ring.end(), [&](const ValueSet& set) { return set.first == fresh.runs && set.second == fresh.words; })) ++counters.differingMatchedPrior; - ring.emplace_front(old.runs, old.words); + if (std::find(ring.begin(), ring.end(), valueSet(fresh)) != ring.end()) ++counters.differingMatchedPrior; + ring.push_front(valueSet(old)); while (ring.size() > 3) ring.pop_back(); if (priorValueSets.size() > dispatchCacheEntries()) priorValueSets.clear(); if (old.runs != fresh.runs || old.words.size() != fresh.words.size()) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchInsert.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchInsert.cpp index 8478e84a60..e732e518ee 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchInsert.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchInsert.cpp @@ -1,13 +1,17 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "Optimization/ResourceProgram.hpp" +#include "ImageTableAbi.hpp" +#include #include #include namespace AgcDriver::DriverDetail { bool CacheableResult(const ShaderRecompiler::RecompileResult& compiled) { - return compiled.poisonedSrtReads == 0u; + return compiled.poisonedSrtReads == 0u && std::none_of(compiled.imageTablePoison.begin(), compiled.imageTablePoison.end(), [](const ShaderRecompiler::ImageTableEntryPoison& poison) { + return poison.reason == static_cast(ShaderRecompiler::ImageTableAbi::PoisonReason::Unmapped) || poison.reason == static_cast(ShaderRecompiler::ImageTableAbi::PoisonReason::Inactive); + }); } void Driver::insertDispatch(std::uint64_t address, std::uint64_t key, bool noDispatchCache, bool profile, const std::shared_ptr& registeredShader, std::uint64_t forgetAtCapture, std::span memory, const std::shared_ptr& shaderMemory, const std::vector& captured, const std::shared_ptr& capture, const std::shared_ptr& compiledResult, const std::shared_ptr& missedEntry, bool missedDiffering, std::shared_ptr& attachVariant, DispatchPhaseTiming& phaseTiming) { @@ -18,15 +22,23 @@ void Driver::insertDispatch(std::uint64_t address, std::uint64_t key, bool noDis fresh->forgetSerial = forgetAtCapture; std::vector srtRegions; + std::size_t capturedWords = 0; for (const auto& region : captured) { const bool registered = std::any_of(memory.begin(), memory.end(), [&](const auto& known) { return region.guestAddress >= known.guestAddress && region.guestAddress < known.guestAddress + known.bytes.size(); }); if (registered) continue; srtRegions.push_back(region); + capturedWords += region.bytes.size() / sizeof(std::uint32_t); + } + const auto supplied = shaderMemory->SuppliedBlocks(); + fresh->runs.reserve(srtRegions.size()); + fresh->words.reserve(capturedWords); + for (const auto& region : srtRegions) { fresh->runs.emplace_back(region.guestAddress, region.guestAddress + region.bytes.size()); const auto count = region.bytes.size() / sizeof(std::uint32_t); const auto offset = fresh->words.size(); fresh->words.resize(offset + count); std::memcpy(fresh->words.data() + offset, region.bytes.data(), count * sizeof(std::uint32_t)); + AppendSharedWords(fresh->shared, fresh->runs.size() - 1u, offset, region.bytes, supplied); } DataWordPositionCounts dataCounts; @@ -87,7 +99,7 @@ void Driver::insertDispatch(std::uint64_t address, std::uint64_t key, bool noDis dispatchOrder.push_front(key); created->order = dispatchOrder.begin(); dispatchCache.emplace(key, std::move(created)); - } else if (found->second != missedEntry && !found->second->variants.empty() && found->second->variants.front()->runs == fresh->runs && found->second->variants.front()->words == fresh->words) { + } else if (found->second != missedEntry && !found->second->variants.empty() && found->second->variants.front()->runs == fresh->runs && SameCapturedWords(found->second->variants.front()->words, found->second->variants.front()->shared, fresh->words, fresh->shared)) { attachVariant = found->second->variants.front(); } else { @@ -113,10 +125,13 @@ void Driver::insertDispatch(std::uint64_t address, std::uint64_t key, bool noDis } static const bool lru = std::getenv("APS5_NO_DISPATCH_LRU") == nullptr; - if (dispatchCache.size() > dispatchCacheEntries()) { + const auto over = [&] { return dispatchCache.size() > dispatchCacheEntries() || (dispatchCache.size() > 1 && dispatchCacheVariantBytes > DispatchCacheBytes()); }; + if (over()) { if (lru) { - eraseDispatchEntry(dispatchCache.find(dispatchOrder.back())); - ++dispatchCacheEvictions; + while (over()) { + eraseDispatchEntry(dispatchCache.find(dispatchOrder.back())); + ++dispatchCacheEvictions; + } } else { dispatchCacheEvictions += dispatchCache.size(); dispatchCache.clear(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchLookup.cpp index 47f52c0886..b0b37b7900 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchLookup.cpp @@ -123,12 +123,7 @@ void Driver::lookupDispatch(std::uint64_t address, const Submission& submission, for (const auto& [position, value] : liveData) liveWords[position] = value; regions.clear(); appendEntryRegions(*variant, regions, &liveWords); - auto patched = std::make_shared(*variant->compiled); - auto& descriptor = patched->bindings[variant->flatBinding].guestDescriptor; - for (std::size_t k = 0; k < variant->dataPositions.size(); ++k) { - if (variant->dataSlots[k] < descriptor.size()) descriptor[variant->dataSlots[k]] = liveWords[variant->dataPositions[k]]; - } - compiledResult = std::move(patched); + compiledResult = patchDataWords(*variant, liveWords); ++counters.dataHits; counters.dataWordsRefreshed += liveData.size(); ++counters.dataHitsByRank[rank]; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp index 9be8199572..65057babfa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp @@ -1,10 +1,32 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Dispatch/IndirectDispatch.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include #include namespace AgcDriver::DriverDetail { +namespace { + +const char* IndirectPathName(int path) { + switch (path) { + case IndirectGpu: return "gpu-side"; + case IndirectPendingImage: return "pending image results"; + case IndirectCopiedWrite: return "pending label or copied write"; + case IndirectNotImported: return "not imported"; + case IndirectThreadDimensions: return "thread dimensions"; + case IndirectFillKernel: return "fill kernel"; + case IndirectMisaligned: return "misaligned"; + case IndirectDisabled: return "disabled"; + case IndirectWorkgroupMemory: return "device memory LDS"; + default: return "unknown"; + } +} + +} + void Driver::countIndirect(int path, double readMs) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile || path < 0 || path >= IndirectPaths) return; @@ -20,30 +42,68 @@ void Driver::countIndirect(int path, double readMs) { lastReport = now; std::uint64_t cpu = 0; for (int i = 1; i < IndirectPaths; ++i) cpu += counts[i]; - std::fprintf(stderr, "[indirect] gpu-side %llu, cpu-side %llu (thread dimensions %llu, fill kernel %llu, pending image results %llu, pending label or copied write %llu, not imported %llu, misaligned %llu, disabled %llu, device memory LDS %llu); CPU argument reads took %.1f s\n", static_cast(counts[IndirectGpu]), static_cast(cpu), static_cast(counts[IndirectThreadDimensions]), static_cast(counts[IndirectFillKernel]), static_cast(counts[IndirectPendingImage]), static_cast(counts[IndirectCopiedWrite]), static_cast(counts[IndirectNotImported]), static_cast(counts[IndirectMisaligned]), static_cast(counts[IndirectDisabled]), static_cast(counts[IndirectWorkgroupMemory]), readWaitedMs / 1000); + std::fprintf(stderr, "[indirect] (cumulative): gpu-side %llu, cpu-side %llu (thread dimensions %llu, fill kernel %llu, pending image results %llu, pending label or copied write %llu, not imported %llu, misaligned %llu, disabled %llu, device memory LDS %llu); CPU argument reads took %.1f s\n", static_cast(counts[IndirectGpu]), static_cast(cpu), static_cast(counts[IndirectThreadDimensions]), static_cast(counts[IndirectFillKernel]), static_cast(counts[IndirectPendingImage]), static_cast(counts[IndirectCopiedWrite]), static_cast(counts[IndirectNotImported]), static_cast(counts[IndirectMisaligned]), static_cast(counts[IndirectDisabled]), static_cast(counts[IndirectWorkgroupMemory]), readWaitedMs / 1000); +} + +int Driver::indirectArgumentsPath(std::span packet, std::uint64_t arguments) { + static const bool gpuIndirect = std::getenv("APS5_NO_GPU_INDIRECT") == nullptr; + if (!gpuIndirect) return IndirectDisabled; + static const bool gpuThreadDimensions = std::getenv("APS5_NO_GPU_THREAD_DIMENSIONS") == nullptr; + if ((packet.back() & 0x20u) != 0 && !gpuThreadDimensions) return IndirectThreadDimensions; + if (arguments % 4 != 0) return IndirectMisaligned; + return IndirectGpu; +} + +Driver::IndirectSource& Driver::indirectSource() { + static thread_local IndirectSource source; + return source; +} + +std::string Driver::describeIndirectSource() { + const auto& source = indirectSource(); + if (source.arguments == 0) return {}; + const char* ahead = source.ahead == AheadArguments::Matched ? "adopted, its counts matched the re-read" : source.ahead == AheadArguments::Differed ? "dropped, its counts differed from the re-read" : "none"; + char text[256]; + std::snprintf(text, sizeof(text), "; DISPATCH_INDIRECT on queue 0x%x, arguments at 0x%llx, driver path %s, read-ahead %s", source.queue, static_cast(source.arguments), IndirectPathName(source.path), ahead); + return text; +} + +std::array Driver::readDispatchArguments(std::uint32_t queue, std::uint64_t arguments, std::uint32_t initiator) { + recordQueuedLabelsBeforeRead(queue); + if (!resolvingAhead()) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Indirect); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + Graphics::FinishLeaseWork(); + } + return Pm4::ReadDispatchArguments(arguments, initiator); } void Driver::dispatchIndirect(QueueState& queue, std::span packet, const Submission& submission) { - static const bool gpuIndirect = std::getenv("APS5_NO_GPU_INDIRECT") == nullptr; const auto arguments = Pm4::DispatchArgumentAddress(packet, queue); const auto initiator = packet.back(); - int path = IndirectGpu; - if (!gpuIndirect) path = IndirectDisabled; - else if ((initiator & 0x20u) != 0) path = IndirectThreadDimensions; - else if (arguments % 4 != 0) path = IndirectMisaligned; + const int path = indirectArgumentsPath(packet, arguments); + struct Scope { + IndirectSource& source; + IndirectSource previous; + ~Scope() { source = previous; } + } scope{indirectSource(), indirectSource()}; + scope.source = IndirectSource{arguments, path, submission.queue, AheadArguments::None}; if (path != IndirectGpu) { + const auto readStart = std::chrono::steady_clock::now(); + const auto direct = readDispatchArguments(submission.queue, arguments, initiator); + countIndirect(path, std::chrono::duration(std::chrono::steady_clock::now() - readStart).count()); if (!resolvingAhead()) { - const auto& resolved = resolvedAhead(); + auto& resolved = resolvedAhead(); if (const auto found = resolved.find(currentPacketOffset()); found != resolved.end() && found->second.indirectArguments == 0 && found->second.packet[0] == 0xc0031500u) { - const auto direct = found->second.packet; - dispatch(queue, direct, submission); - return; + if (found->second.packet == direct) { + scope.source.ahead = AheadArguments::Matched; + } else { + resolved.erase(found); + ++groupCaptureStats().argumentMismatches; + scope.source.ahead = AheadArguments::Differed; + } } } - recordQueuedLabelsBeforeRead(submission.queue); - const auto readStart = std::chrono::steady_clock::now(); - const auto direct = Pm4::ReadDispatchArguments(arguments, initiator); - countIndirect(path, std::chrono::duration(std::chrono::steady_clock::now() - readStart).count()); dispatch(queue, direct, submission); return; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/VariantValidation.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/VariantValidation.cpp index 6c76ebb1a6..e4136cbebe 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/VariantValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/VariantValidation.cpp @@ -1,6 +1,9 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" +#include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include #include #include @@ -17,10 +20,24 @@ void Driver::appendEntryRegions(const DispatchVariant& variant, std::vector regions, std::uint64_t& synced, PendingView& pending, std::optional& sampling) { +std::shared_ptr Driver::patchDataWords(const DispatchVariant& variant, const std::vector& words) { + auto patched = std::make_shared(*variant.compiled); + auto& descriptor = patched->bindings[variant.flatBinding].guestDescriptor; + for (std::size_t k = 0; k < variant.dataPositions.size(); ++k) { + if (variant.dataSlots[k] < descriptor.size()) descriptor[variant.dataSlots[k]] = words[variant.dataPositions[k]]; + } + return patched; +} + +bool Driver::syncPendingRuns(std::uint64_t program, std::uint32_t queue, const ShaderRecompiler::RecompileResult& compiled, std::span regions, std::uint64_t& synced, PendingView& pending, std::optional& sampling, std::span skipped) { using Policy = ShaderMemory::PendingWrite; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - for (const auto& region : regions) { + for (std::size_t index = 0, range = 0; index < regions.size(); ++index) { + if (range < skipped.size() && index == skipped[range].begin) { + index = skipped[range++].end - 1u; + continue; + } + const auto& region = regions[index]; if (!pending.Overlaps(region.guestAddress, region.bytes.size())) continue; if (!sampling) sampling.emplace(siteSampling() ? evidenceValidations : evidenceReads); std::uint64_t ValidateCounters::*reason = &ValidateCounters::syncedOff; @@ -67,18 +84,47 @@ bool Driver::syncPendingRuns(std::uint64_t program, std::uint32_t queue, const S return true; } +bool Driver::vouchedValidation() { + static const bool enabled = std::getenv("APS5_NO_VOUCHED_VALIDATION") == nullptr; + return enabled; +} + EntryOutcome Driver::validateVariant(std::uint64_t program, std::uint32_t queue, const DispatchVariant& variant, std::span regions, std::uint64_t& imagesFlushed, std::uint64_t& runsSynced, std::optional& sampling, std::vector>* live) { if (live != nullptr) live->clear(); + if (DrawPipeline::Active()) { + auto& pipeline = DrawPipeline::Queue0(); + if (pipeline.Busy() && std::any_of(variant.runs.begin(), variant.runs.end(), [&](const auto& run) { return pipeline.Overlaps(run.first, static_cast(run.second - run.first)); })) pipeline.Drain(DrawPipeline::DrainReason::Capture); + } const bool masked = live != nullptr && !variant.dataPositions.empty(); const DataMask mask{variant.dataPositions, live}; + thread_local std::vector* vouchedSlot = nullptr; + auto& vouched = ShaderRecompiler::ThreadOwned(vouchedSlot); + vouched.clear(); + if (!variant.shared.empty() && vouchedValidation() && !validateLegacy() && variant.runs.size() == regions.size()) VouchSharedRegions(variant.shared, regions.size(), &QueryPendingSpan, vouched); PendingView pending; pending.Load(); - if (!syncPendingRuns(program, queue, *variant.compiled, regions, runsSynced, pending, sampling)) return EntryOutcome::Differing; + std::erase_if(vouched, [&](const VouchedRegions& range) { return pending.Overlaps(range.low, static_cast(range.high - range.low)); }); + thread_local std::vector>* checkedSlot = nullptr; + auto& checked = ShaderRecompiler::ThreadOwned(checkedSlot); + checked.clear(); + if (!vouched.empty()) { + for (std::size_t index = 0, range = 0; index < variant.runs.size(); ++index) { + if (range < vouched.size() && index == vouched[range].begin) { + checked.emplace_back(vouched[range].low, vouched[range].high); + index = vouched[range++].end - 1u; + continue; + } + checked.push_back(variant.runs[index]); + } + } + const std::span> runs = vouched.empty() ? std::span>(variant.runs) : std::span>(checked); + if (!syncPendingRuns(program, queue, *variant.compiled, regions, runsSynced, pending, sampling, vouched)) return EntryOutcome::Differing; auto publish = Graphics::Recorder::PublishGeneration(); auto pendingSerial = Graphics::StorageTexture::PendingSerial(); auto forget = GuestMemory::ForgetSerial(); - if (Graphics::StorageTexture::AnyPendingOverlaps(variant.runs) || Graphics::AnyShadowedOverlaps(variant.runs)) { + if (Graphics::StorageTexture::AnyPendingOverlaps(runs) || Graphics::AnyShadowedOverlaps(runs)) { + vouched.clear(); for (const auto& region : regions) { if (Graphics::StorageTexture::FlushPending(region.guestAddress, region.bytes.size(), nullptr, "dispatch-cache entry")) ++imagesFlushed; } @@ -91,7 +137,7 @@ EntryOutcome Driver::validateVariant(std::uint64_t program, std::uint32_t queue, } if ((forget & 1) != 0) return EntryOutcome::ForgetMoved; for (const auto& label : deferredLabels().labels) { - for (const auto& [begin, end] : variant.runs) { + for (const auto& [begin, end] : runs) { if (label.address < end && begin < label.address + label.size) return EntryOutcome::QueuedLabel; } } @@ -103,7 +149,7 @@ EntryOutcome Driver::validateVariant(std::uint64_t program, std::uint32_t queue, pending.Load(); bool unmapped = false; - if (!validateCaptured(program, queue, regions, *variant.compiled, true, pending, &unmapped, &sampling, masked ? &mask : nullptr)) return unmapped ? EntryOutcome::Inaccessible : EntryOutcome::Differing; + if (!validateCaptured(program, queue, regions, *variant.compiled, true, pending, &unmapped, &sampling, masked ? &mask : nullptr, vouched)) return unmapped ? EntryOutcome::Inaccessible : EntryOutcome::Differing; if (Graphics::Recorder::PublishGeneration() != publish) return EntryOutcome::PublishMoved; if (Graphics::StorageTexture::PendingSerial() != pendingSerial) return EntryOutcome::PendingMoved; if (GuestMemory::ForgetSerial() != forget) return EntryOutcome::ForgetMoved; 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 8858b58f2c..c751d4ee51 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -1,11 +1,18 @@ #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/Draw/DrawProgramTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "Optimization/ResourceProgram.hpp" #include +#include #include +#include +#include namespace AgcDriver::DriverDetail { @@ -29,6 +36,17 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe return DrawVerdict::Drawn; }; auto drawParameters = Pm4::ResolveDraw(packet, queue); + const bool pipelined = DrawPipeline::Active(); + static const bool pipelinedIndirect = std::getenv("APS5_NO_PIPELINED_INDIRECT") == nullptr; + if (pipelined && drawParameters.indirect) { + const auto& indirect = *drawParameters.indirect; + auto& pipeline = DrawPipeline::Queue0(); + const bool overlaps = pipeline.Busy() && (pipeline.Overlaps(indirect.arguments, static_cast(std::max(indirect.RangeBytes(), indirect.recordBytes))) || (indirect.countIndirect && pipeline.Overlaps(indirect.countAddress, 4))); + if (!pipelinedIndirect || overlaps) pipeline.Drain(DrawPipeline::DrainReason::Indirect); + } + const auto drainBeforeInline = [&] { + if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Indirect); + }; bool traceIndirect = false; if (const auto verdict = precheckDraw(queue, submission, packet, drawParameters, rejected, traceIndirect)) return *verdict; phaseTiming.Phase(DrawRowPrecheck); @@ -65,12 +83,31 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const bool useDrawEntries = drawEntries() && !ShaderRecompiler::DebugProbeActive() && dumpTarget == 0 && dumpSlot1 == 0; const bool registerKey = useDrawEntries && registerKeyEnabled(); std::uint64_t drawKey = 0; + std::array drawKeyComponents{}; std::shared_ptr entry; 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()); - 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(); @@ -80,25 +117,85 @@ 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; + else if (traceKeysSeen.contains(drawKey)) ++counters.absentNeverInserted; + else if (traceShapes.contains(shapeKey)) ++counters.absentUserWords; + else ++counters.absentNewRegisters; + } + 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; + } + if (dataEntry != nullptr) ++drawEntryCounters.dataHits; + } } + if (traceDrawCache()) traceDrawKeyLookup(drawKeyComponents, found == drawCache.end()); + if (traceDrawCache()) { + traceInsert(traceKeysSeen, drawKey); + traceInsert(traceShapes, shapeKey); + } + maybeReportDrawCache(profile); } if (decode != nullptr && !DecodeProgramsCurrent(*decode, *submission.shaders)) { + userPatches.clear(); std::lock_guard cacheLock(drawCacheMutex); eraseDrawEntry(drawKey, entry); ++drawEntryCounters.absent; entry = nullptr; decode = nullptr; } + if (dataEntry != nullptr && !DecodeProgramsCurrent(*dataEntry->decode, *submission.shaders)) dataEntry = nullptr; phaseTiming.Phase(DrawRowKeyLookupValidate); - resolveDrawDecode(queue, submission, decode, registerKey, drawKey, profile); + if (dataEntry != nullptr) { + auto refreshed = std::make_shared(*dataEntry->decode); + for (auto& program : refreshed->programs) { + ReadUserWords(queue, program, false); + if (program.merged) InitializeMerged(queue, program, program.mergedPointer, program.mergedPointerRequired, false); + } + if (verifyDrawDataHits()) { + const auto full = decodeDraw(queue, submission, localDevice->graphicsContext()); + if (!sameDecode(*refreshed, *full)) { + std::size_t differing = 0; + while (differing < full->programs.size() && differing < refreshed->programs.size() && full->programs[differing].userData == refreshed->programs[differing].userData) ++differing; + char message[320]; + std::snprintf(message, sizeof(message), "APS5_VERIFY_DRAW_DATA_HITS: a data-only hit's decode differs from the full decode (key 0x%llx, shape 0x%llx, %zu / %zu programs, first with differing user words %zu, path %u / %u, target 0x%llx / 0x%llx)", static_cast(drawKey), static_cast(shapeKey), refreshed->programs.size(), full->programs.size(), differing, static_cast(refreshed->state.stages.path), static_cast(full->state.stages.path), static_cast(refreshed->state.color.address), static_cast(full->state.color.address)); + throw std::runtime_error(message); + } + } + decode = std::move(refreshed); + } + resolveDrawDecode(queue, submission, localDevice->graphicsContext(), decode, registerKey, drawKey, profile); const auto& graphics = decode->state; auto pixel = decode->pixel; - std::vector programs = decode->programs; + const bool writesPrograms = graphics.stages.mesh || drawParameters.indirect || !userPatches.empty(); + std::vector writablePrograms; + if (writesPrograms) writablePrograms = decode->programs; + for (const auto& patch : userPatches) writablePrograms[patch.program].userData[patch.index] = patch.value; + const std::vector& programs = writesPrograms ? writablePrograms : decode->programs; if (const auto skipped = SkippedShader(); skipped != 0 && std::any_of(programs.begin(), programs.end(), [skipped](const DrawProgram& program) { return program.binary.codeAddress == skipped; })) return drawn(); const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) { if (!graphics.stages.mesh) return; - auto& words = programs.front().userData; + auto& words = writablePrograms.front().userData; require(programs.front().firstUserSgpr == 0 && words.size() >= ShaderRecompiler::MeshIndexBufferUserWord + 4, "mesh program lacks the hidden user words"); const auto descriptor = Graphics::MeshIndexBufferDescriptor(parameters); std::copy(descriptor.begin(), descriptor.end(), words.begin() + ShaderRecompiler::MeshIndexBufferUserWord); @@ -106,16 +203,22 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (!drawParameters.indirect) setMeshIndexBuffer(drawParameters); else if (graphics.stages.mesh) setMeshIndexBuffer(Pm4::DrawParameters{drawParameters.indexAddress, std::max(drawParameters.indexCount, 1u), drawParameters.indexSize, 1, 0, drawParameters.indexed}); const std::vector& roles = decode->roles; + static const bool tracePrograms = std::getenv("APS5_TRACE_DRAW_PROGRAMS") != nullptr; + if (tracePrograms) { + std::vector identities; + identities.reserve(programs.size()); + for (std::size_t i = 0; i < programs.size(); ++i) { + identities.push_back({programs[i].binary.stage, roles[i], programs[i].snapshot->codeAddress, programs[i].binary.codeAddress}); + } + const auto line = FormatDrawProgramTrace(submission.serial, submission.queue, identities); + std::fprintf(stderr, "%s\n", line.c_str()); + } phaseTiming.Phase(DrawRowDecode); const auto locate = [&](std::uint32_t location) -> std::optional> { - if (location == 0x280u) return std::nullopt; - for (std::size_t i = 0; i < programs.size(); ++i) { - if (roles[i] == Role::Fragment || roles[i] == Role::GeometryBack || location < programs[i].userDataBase) continue; - const auto word = location - programs[i].userDataBase + (8u - programs[i].firstUserSgpr); - if (word < programs[i].userData.size()) return std::make_pair(i, static_cast(word)); - } - return std::nullopt; + const auto words = UserDataWords(programs, roles, location); + if (words.empty()) return std::nullopt; + return words.front(); }; if (drawParameters.indirect) { auto& indirect = *drawParameters.indirect; @@ -129,12 +232,12 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe indirect.drawIndexSgpr = sgprOf(indirect.drawIndexLocation); } std::vector memory; + memory.reserve(2 * programs.size() + 32); + AppendProgramMemory(memory, programs); std::vector linked; + linked.reserve(programs.size()); for (std::size_t i = 0; i < programs.size(); ++i) { const auto& program = programs[i]; - for (const auto& region : program.memory) { - if (!region.bytes.empty()) memory.push_back(region); - } linked.push_back({roles[i], program.binary, program.userDataBase, program.firstUserSgpr, program.userData}); } timing.Mark("prepare"); @@ -147,13 +250,17 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (program.binary.stage == Stage::Fragment || roles[i] == Role::GeometryBack) return; decodeReads[i].clear(); vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr); - if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207), program.tailContinuation.get()) | graphics.layerExports; + if (vertexInfos[i]) { + vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207), program.tailContinuation.get()) | graphics.layerExports; + vertexInfos[i]->depthRemapMode = graphics.depthRemap.mode; + Graphics::ApplyPointSize(graphics, *vertexInfos[i]); + } }; if (!registerKey) { for (std::size_t i = 0; i < programs.size(); ++i) decodeVertexInfo(i); phaseTiming.Phase(DrawRowDecode); } - ShaderMemory shaderMemory(memory, &queryPendingWrite, &observePendingWrite, hookWaitCounter()); + auto shaderMemory = std::make_unique(memory, &queryPendingWrite, &observePendingWrite, hookWaitCounter(), &QueryPendingSpan); std::vector results; std::vector> compiledStages; compiledStages.reserve(programs.size()); @@ -171,9 +278,35 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe std::vector> fresh(programs.size()); std::vector recompiled(programs.size(), false); + std::vector, std::vector>>> dataCandidates(programs.size()); + std::vector reused(programs.size(), false); + std::vector stageData(programs.size()); + std::uint64_t dataStagesReused = 0, dataStagesCompiled = 0, dataVerified = 0; bool drawHit = false; bool verifyHit = false; - lookupDraw(submission, localDevice, graphics, pixel, programs, roles, vertexInfos, useDrawEntries, registerKey, profile, drawKey, entry, matched, matchedRegions, drawHit, verifyHit, phaseTiming, phaseMs); + 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; if (registerKey) { @@ -182,6 +315,11 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (matched[i]->vertexInfo != nullptr) vertexInfos[i] = *matched[i]->vertexInfo; continue; } + if (!dataCandidates[i].empty() && !verifyDrawDataHits()) { + const auto& info = dataCandidates[i].front().first->vertexInfo; + if (info != nullptr) vertexInfos[i] = *info; + continue; + } decodeVertexInfo(i); if (matched[i] != nullptr && verifyDrawRecipe() && (vertexInfos[i].has_value() != (matched[i]->vertexInfo != nullptr) || (vertexInfos[i] && !sameVertexInfo(*vertexInfos[i], *matched[i]->vertexInfo)))) { static std::atomic reports{0}; @@ -195,8 +333,49 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto fold = [&](const ShaderRecompiler::RecompileResult& main, Pm4::DrawParameters& parameters) { FoldDrawOffsets(main, programs.front(), parameters); }; + const auto freshRuns = [&](std::size_t i, std::vector>& runs, std::vector& words) { + std::vector regions(stageCaptures[i].regions.begin(), stageCaptures[i].regions.end()); + for (const auto& read : decodeReads[i]) regions.push_back({read.address, 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) { + runs.emplace_back(region.guestAddress, region.guestAddress + region.bytes.size()); + const auto count = region.bytes.size() / sizeof(std::uint32_t); + const auto offset = words.size(); + words.resize(offset + count); + std::memcpy(words.data() + offset, region.bytes.data(), count * sizeof(std::uint32_t)); + } + }; + const auto verifyDataStage = [&](std::size_t i, const DispatchVariant& kept) { + std::vector> runs; + std::vector words; + freshRuns(i, runs, words); + const auto& compiled = *stageCaptures[i].compiled; + const char* what = runs != kept.runs ? "runs" : words != kept.words ? "words" : vertexInfos[i].has_value() != (kept.vertexInfo != nullptr) || (vertexInfos[i] && !sameVertexInfo(*vertexInfos[i], *kept.vertexInfo)) ? "vertex stage info" : compiled.variantId != kept.compiled->variantId ? "variant" : compiled.pushConstants != kept.compiled->pushConstants ? "push constants" : nullptr; + ++dataVerified; + if (what == nullptr) return; + char message[320]; + std::snprintf(message, sizeof(message), "APS5_VERIFY_DRAW_DATA_HITS: stage %zu (program 0x%llx) of a data-only hit differs from its full build in its %s (%zu runs / %zu words kept, %zu / %zu fresh; variant 0x%llx / 0x%llx; key 0x%llx, shape 0x%llx)", i, static_cast(programs[i].binary.codeAddress), what, kept.runs.size(), kept.words.size(), runs.size(), words.size(), static_cast(kept.compiled->variantId), static_cast(compiled.variantId), static_cast(drawKey), static_cast(shapeKey)); + throw std::runtime_error(message); + }; + const auto verifyStageData = [&](std::size_t i) { + const auto& hit = stageData[i]; + const auto& kept = *matched[i]; + std::vector> runs; + std::vector words; + freshRuns(i, runs, words); + const auto& fresh = *stageCaptures[i].compiled; + const auto& patched = *hit.compiled; + const bool sameBindings = fresh.bindings.size() == patched.bindings.size() && std::equal(fresh.bindings.begin(), fresh.bindings.end(), patched.bindings.begin(), [](const auto& a, const auto& b) { return a.kind == b.kind && a.role == b.role && a.binding == b.binding && a.count == b.count && a.guestDescriptor == b.guestDescriptor; }); + const char* what = runs != kept.runs ? "runs" : words != hit.words ? "words" : fresh.variantId != patched.variantId ? "variant" : fresh.pushConstants != patched.pushConstants ? "push constants" : !sameBindings ? "bindings" : nullptr; + ++stageDataVerified; + if (what == nullptr) return; + char message[256]; + std::snprintf(message, sizeof(message), "APS5_VERIFY_DRAW_DATA_HITS: stage %zu (program 0x%llx) of a stage data hit differs from its full build in its %s (%zu data positions refreshed)", i, static_cast(programs[i].binary.codeAddress), what, kept.dataPositions.size()); + throw std::runtime_error(message); + }; std::optional indirectCpu; std::vector pushOffsets(programs.size(), 0); + std::optional meshBarycentrics; std::vector resultIndex(programs.size(), 0); for (std::size_t i = 0; i < programs.size(); ++i) { if (roles[i] == Role::GeometryBack) continue; @@ -205,19 +384,33 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe pushOffsets[i] = pushCursorBytes; if (drawHit) { - programResults[i] = matched[i]->compiled.get(); + programResults[i] = stageData[i].compiled != nullptr ? stageData[i].compiled.get() : matched[i]->compiled.get(); memory.insert(memory.end(), matchedRegions[i].begin(), matchedRegions[i].end()); } else { - if (program.binary.stage == ShaderRecompiler::ShaderStage::Fragment) { - for (auto previous = i; previous-- > 0;) { - if (programResults[previous] == nullptr) continue; - pixel = Graphics::DefaultUnexportedInputs(decode->pixel, programResults[previous]->parameterExports); - break; + const auto candidate = std::find_if(dataCandidates[i].begin(), dataCandidates[i].end(), [&](const auto& kept) { return kept.first->pushOffset == pushCursorBytes; }); + if (candidate != dataCandidates[i].end() && !verifyDrawDataHits()) { + matched[i] = candidate->first; + matchedRegions[i] = std::move(candidate->second); + reused[i] = true; + programResults[i] = stageData[i].compiled != nullptr ? stageData[i].compiled.get() : matched[i]->compiled.get(); + memory.insert(memory.end(), matchedRegions[i].begin(), matchedRegions[i].end()); + } else { + if (!dataCandidates[i].empty() && !verifyDrawDataHits()) decodeVertexInfo(i); + if (program.binary.stage == ShaderRecompiler::ShaderStage::Fragment) { + for (auto previous = i; previous-- > 0;) { + if (programResults[previous] == nullptr) continue; + pixel = Graphics::DefaultUnexportedInputs(decode->pixel, programResults[previous]->parameterExports); + break; + } } + compiledStages.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics)); + if (!rejected.empty()) return DrawVerdict::Rejected; + programResults[i] = compiledStages.back().get(); + if (candidate != dataCandidates[i].end()) verifyDataStage(i, *candidate->first); + if (stageData[i].compiled != nullptr && verifyDrawDataHits()) verifyStageData(i); } - compiledStages.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected)); - if (!rejected.empty()) return DrawVerdict::Rejected; - programResults[i] = compiledStages.back().get(); + if (dataEntry != nullptr && reused[i]) ++dataStagesReused; + else if (dataEntry != nullptr) ++dataStagesCompiled; } const auto& result = *programResults[i]; if (i == 0 && drawParameters.indirect) { @@ -231,13 +424,24 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe pushCursorBytes += static_cast(result.pushConstants.size()); } - cacheDrawStages(useDrawEntries, drawHit, drawParameters, indirectCpu, programs, stageCaptures, vertexInfos, decodeReads, verifyHit, matched, fresh, drawKey, registerKey, decode, phaseTiming); + if (stageDataVerified != 0) { + std::lock_guard cacheLock(drawCacheMutex); + drawEntryCounters.stageDataVerified += stageDataVerified; + } + if (dataEntry != nullptr) { + std::lock_guard cacheLock(drawCacheMutex); + drawEntryCounters.dataStagesReused += dataStagesReused; + drawEntryCounters.dataStagesCompiled += dataStagesCompiled; + drawEntryCounters.dataVerified += dataVerified; + } + cacheDrawStages(stageEntries, drawHit, drawParameters, indirectCpu, programs, stageCaptures, vertexInfos, decodeReads, verifyHit, matched, fresh, drawKey, registerKey, decode, phaseTiming, reused, entry, shapeKey); timing.Mark("shader_compile_and_link"); for (const auto& reads : decodeReads) { - for (const auto& read : reads) memory.push_back({read.address, std::as_bytes(std::span(read.bytes))}); + for (const auto& read : reads) memory.push_back({read.address, read.Bytes()}); } + if (pipelined && !deferredLabels().labels.empty()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Labels); if (recordQueuedLabelsAfterCapture(submission.queue, memory)) return draw(queue, packet, submission, rejected); bool rectListBuilt = false; @@ -247,29 +451,55 @@ 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; phaseTiming.Phase(DrawRowRectList); }; if (graphics.rectList) buildRectList(); - if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) { + if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active && graphics.stages.path == Graphics::ShaderPath::Geometry) { + require(localDevice->Target().mesh.has_value(), "a pixel shader on the mesh path reads barycentrics, and this device has neither VK_KHR_fragment_shader_barycentric nor mesh shaders (VK_EXT_mesh_shader), so they cannot be provided or emulated"); + require(!graphics.rectList && graphics.stages.mesh && programs.size() == 2 && programResults[0] != nullptr && programResults[1] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics needs exactly one mesh program before it to emulate them"); + meshBarycentrics = ShaderRecompiler::MeshBarycentricLayoutOf(*programResults[1]); + if (programResults[0]->meshBarycentrics != meshBarycentrics) { + const auto pushBytes = programResults[0]->pushConstants.size(); + const auto exports = programResults[0]->parameterExports; + compiledStages.push_back(materializeDrawStage(0, pushOffsets[0], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics)); + if (!rejected.empty()) return DrawVerdict::Rejected; + programResults[0] = compiledStages.back().get(); + stages[0].program = programResults[0]; + require(programResults[0]->pushConstants.size() == pushBytes && programResults[0]->parameterExports == exports, "pairing the mesh program with its pixel shader changed the push constants or the parameter exports the pixel shader was built for"); + } + require(programResults[0]->meshBarycentrics == meshBarycentrics, "the mesh program was compiled for a different barycentric layout than its pixel shader"); + } else if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) { require(!graphics.rectList && graphics.stages.path == Graphics::ShaderPath::Vertex && programs.size() == 2 && programResults[0] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric needs a vertex shader before it"); - 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.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) && !graphics.primitiveRestart, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric or geometry shaders needs a triangle list or strip without primitive restart"); + auto patches = ShaderRecompiler::BuildBarycentricTessellationShaders(*programResults[0], *programResults[1], localDevice->Target(), localDevice->MaxFragmentInputComponents(), graphics.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT ? 2u : 0u); + const auto first = results.size(); + results.push_back(std::move(patches.control)); + results.push_back(std::move(patches.evaluation)); + stages.insert(stages.begin() + 1, {{Stage::TessellationControl, &results[first], 0}, {Stage::TessellationEvaluation, &results[first + 1], 0}}); + } } std::vector snapshots; const auto snapshot = [&] { snapshots.clear(); + snapshots.reserve(memory.size()); for (const auto& region : memory) snapshots.push_back({region.guestAddress, region.bytes}); }; snapshot(); @@ -309,22 +539,33 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe std::vector records; for (std::uint32_t record = 0; record < count; ++record) records.push_back(Pm4::ReadDrawArguments(indirect, record)); Graphics::CountIndirectDraw(*indirectCpu, std::chrono::duration(std::chrono::steady_clock::now() - readStart).count()); - const auto baseVertexWord = locate(indirect.baseVertexLocation); - const auto startInstanceWord = locate(indirect.startInstanceLocation); - const auto drawIndexWord = indirect.drawIndexEnabled ? locate(indirect.drawIndexLocation) : std::nullopt; + const auto baseVertexWords = UserDataWords(programs, roles, indirect.baseVertexLocation); + const auto startInstanceWords = UserDataWords(programs, roles, indirect.startInstanceLocation); + const auto drawIndexWords = indirect.drawIndexEnabled ? UserDataWords(programs, roles, indirect.drawIndexLocation) : std::vector>{}; + const auto drawable = [&](const Pm4::DrawArguments& arguments) { return arguments.count != 0 && arguments.instances != 0 && (!drawParameters.indexed || arguments.firstVertexOrIndex < drawParameters.indexCount); }; + std::size_t lastDrawable = records.size(); + for (std::size_t record = 0; record < records.size(); ++record) { + if (drawable(records[record])) lastDrawable = record; + } for (std::uint32_t record = 0; record < records.size(); ++record) { const auto& arguments = records[record]; if (traceIndirect) std::fprintf(stderr, "[draw] record %u: count %u instances %u first %u vertexOffset %u startInstance %u\n", record, arguments.count, arguments.instances, arguments.firstVertexOrIndex, arguments.vertexOffset, arguments.firstInstance); if (arguments.count == 0 || arguments.instances == 0) continue; std::set patched; - const auto patch = [&](const std::optional>& word, std::uint32_t value) { - if (!word) return; - programs[word->first].userData[word->second] = value; - patched.insert(word->first); + const auto patch = [&](const std::vector>& words, std::uint32_t value) { + for (const auto& [program, word] : words) { + writablePrograms[program].userData[word] = value; + patched.insert(program); + } }; - patch(baseVertexWord, indirect.recordBytes == 20 ? arguments.vertexOffset : arguments.firstVertexOrIndex); - patch(startInstanceWord, arguments.firstInstance); - patch(drawIndexWord, record); + patch(baseVertexWords, indirect.recordBytes == 20 ? arguments.vertexOffset : arguments.firstVertexOrIndex); + patch(startInstanceWords, arguments.firstInstance); + patch(drawIndexWords, record); + if (graphics.stages.tessellation) { + for (std::size_t i = 0; i < programs.size(); ++i) { + if (programs[i].binary.stage == Stage::TessellationControl || programs[i].binary.stage == Stage::TessellationEvaluation) patched.insert(i); + } + } Pm4::DrawParameters direct{0, arguments.count, 0, arguments.instances, drawParameters.flags, drawParameters.indexed, 0, 0}; if (drawParameters.indexed) { @@ -343,7 +584,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe auto& result = results[resultIndex[programIndex]]; const auto pushBytes = result.pushConstants.size(); decodeVertexInfo(programIndex); - result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected); + result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, programs[programIndex].binary.stage == Stage::TessellationControl || programs[programIndex].binary.stage == Stage::TessellationEvaluation ? direct : drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics); if (!rejected.empty()) return DrawVerdict::Rejected; require(result.pushConstants.size() == pushBytes, "patched program changed its push constant layout"); } @@ -354,6 +595,24 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe rejected = std::move(*known); return DrawVerdict::Rejected; } + if (pipelined && pipelinedIndirect && record == lastDrawable && !gpuLock.owns_lock() && deferredLabels().labels.empty()) { + phaseTiming.Phase(DrawRowVectors); + auto writes = drawWriteRanges(graphics, stages); + const auto commitQueue = submission.queue; + const auto commitPacket = (packet[0] >> 8u) & 0xffu; + const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed); + auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), direct, stages = std::move(stages), snapshots = std::move(snapshots), commitQueue, commitPacket, epoch, programs = std::move(programs), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)] { + DrawPipeline::FollowEpoch(epoch); + GuestMemory::SetCurrentPacket(commitPacket, commitQueue); + commitDraw(localDevice, commitQueue, decode->state, direct, stages, snapshots, nullptr, {}, 0); + }; + auto held = std::make_shared(std::move(commit)); + DrawPipeline::Queue0().Enqueue([held] { (*held)(); }, std::move(writes)); + phaseTiming.Phase(DrawRowLockWait); + timing.Mark("draw_enqueued"); + return drawn(); + } + drainBeforeInline(); lockForDraw(); noteDrawWriters(stages, submission.queue); phaseTiming.Phase(DrawRowVectors); @@ -365,7 +624,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe } std::vector> recipeStages; - if (registerKey && !drawParameters.indirect && Graphics::DrawRecipes()) { + if (registerKey && !drawParameters.indirect && !patchedStages && Graphics::DrawRecipes()) { recipeStages.reserve(programs.size()); for (std::size_t i = 0; i < programs.size(); ++i) recipeStages.push_back(drawHit ? matched[i] : fresh[i]); if (std::all_of(recipeStages.begin(), recipeStages.end(), [](const std::shared_ptr& variant) { return variant == nullptr; })) recipeStages.clear(); @@ -381,6 +640,25 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe return DrawVerdict::Rejected; } } + if (pipelined && !drawParameters.indirect && deferredLabels().labels.empty()) { + phaseTiming.Phase(DrawRowVectors); + auto writes = drawWriteRanges(graphics, stages); + const auto commitQueue = submission.queue; + const auto commitPacket = (packet[0] >> 8u) & 0xffu; + const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed); + auto reads = QueuedDrawReadRanges(drawParameters, stages); + auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), drawParameters, stages = std::move(stages), snapshots = std::move(snapshots), recipe = std::move(recipe), recipeStages = std::move(recipeStages), drawKey, commitQueue, commitPacket, epoch, programs = std::move(programs), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)] { + DrawPipeline::FollowEpoch(epoch); + GuestMemory::SetCurrentPacket(commitPacket, commitQueue); + commitDraw(localDevice, commitQueue, decode->state, drawParameters, stages, snapshots, recipe, recipeStages, drawKey); + }; + auto held = std::make_shared(std::move(commit)); + DrawPipeline::Queue0().Enqueue([held] { (*held)(); }, std::move(writes), 0, {}, std::move(reads)); + phaseTiming.Phase(DrawRowLockWait); + timing.Mark("draw_enqueued"); + return drawn(); + } + if (drawParameters.indirect) drainBeforeInline(); lockForDraw(); noteDrawWriters(stages, submission.queue); phaseTiming.Phase(DrawRowVectors); @@ -401,4 +679,26 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe return drawn(); } +void Driver::commitDraw(std::shared_ptr localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span stages, std::span snapshots, const std::shared_ptr& recipe, const std::vector>& recipeStages, std::uint64_t drawKey) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Draw); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + if (auto current = device.Load(); current != nullptr && current != localDevice) localDevice = std::move(current); + struct SubmitDue { + Driver& driver; + const std::shared_ptr& device; + ~SubmitDue() noexcept(false) { + if (std::uncaught_exceptions() == 0) driver.submitDueAfterCommit(device.get()); + } + } submitDue{*this, localDevice}; + recordLabelsForPacket(localDevice.get(), queue); + noteDrawWriters(stages, queue); + if (recipe != nullptr) { + if (localDevice->DrawFromRecipe(graphics, drawParameters, stages, snapshots, recipe) == RecipeOutcome::Recorded) return; + VulkanDevice::NoteRecipe(VulkanDevice::RecipeEvent::Restart, VulkanDevice::RecipeKind::Draw); + } + std::shared_ptr built; + localDevice->Draw(graphics, drawParameters, stages, snapshots, recipeStages.empty() ? nullptr : &built); + if (built != nullptr) attachDrawRecipe(drawKey, recipeStages, std::move(built)); +} + } 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 bf7cf368bf..1d76a6a338 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp @@ -2,6 +2,9 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp" #include #include +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/WriteEvidence.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include namespace AgcDriver::DriverDetail { @@ -23,6 +26,21 @@ bool Driver::drawEntries() { return entries; } +bool Driver::drawDataHits() { + static const bool hits = std::getenv("APS5_NO_DRAW_DATA_HITS") == nullptr; + return hits; +} + +bool Driver::drawStageDataHits() { + static const bool hits = std::getenv("APS5_NO_DRAW_STAGE_DATA_HITS") == nullptr && !stampValidate(); + return hits; +} + +bool Driver::verifyDrawDataHits() { + static const bool verify = std::getenv("APS5_VERIFY_DRAW_DATA_HITS") != nullptr; + return verify; +} + bool Driver::verifyDrawEntries() { static const bool verify = std::getenv("APS5_VERIFY_DRAW_ENTRIES") != nullptr; return verify; @@ -80,14 +98,32 @@ bool DrawKeySightings::SeenBefore(std::uint64_t key) { return false; } -bool Driver::admitDrawKey(std::uint64_t key) { +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); - return 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); } -void Driver::insertDrawEntry(std::uint64_t key, std::vector>& fresh, std::shared_ptr decode) { +void Driver::insertDrawEntry(std::uint64_t key, std::vector>& fresh, std::shared_ptr decode, std::uint64_t shape) { std::lock_guard cacheLock(drawCacheMutex); auto& counters = drawEntryCounters; ++counters.inserts; @@ -95,6 +131,7 @@ void Driver::insertDrawEntry(std::uint64_t key, std::vector(); replacement->stages.resize(fresh.size()); replacement->decode = std::move(decode); + replacement->shape = shape; if (found != drawCache.end()) { if (found->second->stages.size() == fresh.size()) replacement->stages = found->second->stages; if (found->second->decode != nullptr) replacement->decode = found->second->decode; @@ -103,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 && SameCapturedWords(kept->words, kept->shared, fresh[i]->words, fresh[i]->shared); }); if (present != variants.end()) { ++counters.present; fresh[i] = *present; @@ -119,6 +156,7 @@ void Driver::insertDrawEntry(std::uint64_t key, std::vectortouched = drawCacheHits; + if (replacement->decode != nullptr && shape != 0) drawShapes[shape] = key; if (found == drawCache.end()) { drawOrder.push_front(key); replacement->order = drawOrder.begin(); @@ -133,9 +171,12 @@ void Driver::insertDrawEntry(std::uint64_t key, std::vectorsecond->stages) { for (const auto& variant : variants) accountDrawVariant(*variant, false); } + if (traceDrawCache()) traceInsert(traceKeysEvicted, last->first); + if (const auto shapeOf = drawShapes.find(last->second->shape); shapeOf != drawShapes.end() && shapeOf->second == last->first) drawShapes.erase(shapeOf); drawOrder.erase(last->second->order); drawCache.erase(last); ++drawCacheEvictions; + ++counters.evictions; } } @@ -145,6 +186,7 @@ void Driver::eraseDrawEntry(std::uint64_t key, const std::shared_ptr& for (const auto& variants : found->second->stages) { for (const auto& variant : variants) accountDrawVariant(*variant, false); } + if (const auto shapeOf = drawShapes.find(found->second->shape); shapeOf != drawShapes.end() && shapeOf->second == key) drawShapes.erase(shapeOf); drawOrder.erase(found->second->order); drawCache.erase(found); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp index 94d63e8297..3e1094adfb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp @@ -1,20 +1,43 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include namespace AgcDriver::DriverDetail { -void Driver::reportDrawCache(DrawEntryCounters& counters) { +bool Driver::traceDrawCache() { + static const bool trace = std::getenv("APS5_TRACE_DRAW_CACHE") != nullptr; + return trace; +} + +void Driver::traceInsert(std::unordered_set& set, std::uint64_t key) { + if (set.size() >= (1u << 16u)) set.clear(); + set.insert(key); +} + +void Driver::maybeReportDrawCache(bool profile) { + auto& counters = drawEntryCounters; + if (!profile && !traceDrawCache()) return; + const auto now = std::chrono::steady_clock::now(); + if (now - counters.lastReport < std::chrono::seconds(10)) return; + reportDrawCache(counters, profile); + counters.lastReport = now; +} + +void Driver::reportDrawCache(DrawEntryCounters& counters, bool profile) { const auto count = [](std::uint64_t value) { return static_cast(value); }; - const auto validated = counters.lookups - counters.absent; const auto miss = [&](DrawMiss reason) { return count(counters.misses[static_cast(reason)]); }; - std::string ranks; - for (std::size_t rank = 0; rank < dispatchVariants(); ++rank) { - char text[32]; - std::snprintf(text, sizeof(text), "%s%llu", rank == 0 ? "" : " / ", count(counters.variantHitsByRank[rank])); - ranks += text; + std::string text; + if (traceDrawCache()) { + char line[1536]; + std::uint64_t misses = 0; + for (const auto value : counters.misses) misses += value; + std::snprintf(line, sizeof(line), "[draw-cache] 10 s: %llu lookups, %llu hits (%.1f%%), %llu stage misses, %llu without an entry, %llu data-only hits (stages: %llu reused, %llu compiled; %llu verified); no entry: new registers %llu, user words/pointers only %llu, evicted %llu, seen but never inserted %llu; stage misses: front differing %llu, fragment differing %llu, other differing %llu, layout %llu, gate %llu, stage count %llu; differing captures: same runs %llu (%.1f words changed each), other runs %llu; %zu entries, %llu evicted\n", count(counters.lookups), count(counters.hits), counters.lookups != 0 ? 100.0 * static_cast(counters.hits) / static_cast(counters.lookups) : 0.0, count(misses), count(counters.absent), count(counters.dataHits), count(counters.dataStagesReused), count(counters.dataStagesCompiled), count(counters.dataVerified), count(counters.absentNewRegisters), count(counters.absentUserWords), count(counters.absentEvicted), count(counters.absentNeverInserted), miss(DrawMiss::FrontDiffering), miss(DrawMiss::FragmentDiffering), miss(DrawMiss::OtherDiffering), miss(DrawMiss::Layout), miss(DrawMiss::Gate), miss(DrawMiss::Stages), count(counters.differingSameRuns), counters.differingSameRuns != 0 ? static_cast(counters.differingWords) / static_cast(counters.differingSameRuns) : 0.0, count(counters.differingRunsChanged), drawCache.size(), count(counters.evictions)); + text += line; + std::snprintf(line, sizeof(line), "[draw-cache] register-key miss component changes (10 s): stable registers %llu, user data %llu, auxiliary registers %llu, device %llu, first shader set %llu, unchanged components %llu\n", count(counters.registerKeyMissChanges[0]), count(counters.registerKeyMissChanges[1]), count(counters.registerKeyMissChanges[2]), count(counters.registerKeyMissChanges[3]), count(counters.registerKeyMissChanges[4]), count(counters.registerKeyMissChanges[5])); + text += line; } - AgcDriver::ProfilePrint_nid_no_patch( "[draw-cache] %llu lookups (10 s): %llu no entry, %llu validated in %.1f us each (key, lookup and every stage): %llu hits (every stage equal; stage hits by variant rank 1..k %s), misses by reason: front stage differing %llu, fragment differing %llu, other stage differing %llu, layout (push offset) %llu, gate %llu, stage count %llu; %llu stage validations (%llu equal, %.2f variants compared each); %llu inserts (%llu variants inserted, %llu evicted beyond k, %llu already present, %llu unstable), %zu entries (%llu variants, ~%.1f MiB), %llu evictions in total, %llu LRU moves; verify: %llu hits captured again, %llu stages differed\n", count(counters.lookups), count(counters.absent), count(validated), validated != 0 ? counters.validateUs / static_cast(validated) : 0.0, count(counters.hits), ranks.c_str(), miss(DrawMiss::FrontDiffering), miss(DrawMiss::FragmentDiffering), miss(DrawMiss::OtherDiffering), miss(DrawMiss::Layout), miss(DrawMiss::Gate), miss(DrawMiss::Stages), count(counters.stageValidations), count(counters.stageEqual), counters.stageValidations != 0 ? static_cast(counters.variantsCompared) / static_cast(counters.stageValidations) : 0.0, count(counters.inserts), count(counters.variantsInserted), count(counters.variantsEvicted), count(counters.present), count(counters.unstable), drawCache.size(), count(drawCacheVariants), static_cast(drawCacheVariantBytes) / (1024.0 * 1024.0), count(drawCacheEvictions), count(counters.touches), count(counters.verifyHits), count(counters.verifyMismatches)); - AgcDriver::ProfilePrint_nid_no_patch( "[draw-cache] register key (10 s): %llu lookups, %llu hits, key build %.1f us each; decode skipped %llu, partial (state/pixel/programs from the entry) %llu; facade log != table %llu; verify: %llu decodes compared, %llu differed\n", count(counters.registerKeyLookups), count(counters.registerKeyHits), counters.registerKeyLookups != 0 ? counters.keyUs / static_cast(counters.registerKeyLookups) : 0.0, count(counters.decodeSkipped), count(counters.decodePartial), count(counters.facadeMismatches), count(counters.verifyDecodes), count(counters.verifyDecodeMismatches)); + if (profile) text += profileDrawCacheLines(counters); + AgcDriver::ProfilePrint_nid_no_patch("%s", text.c_str()); counters = DrawEntryCounters{}; } @@ -23,4 +46,26 @@ DrawEntryCounters Driver::DrawCacheCounters() { return drawEntryCounters; } +std::string Driver::profileDrawCacheLines(const DrawEntryCounters& counters) { + std::string text; + const auto append = [&](const char* format, auto... values) { + char line[2048]; + std::snprintf(line, sizeof(line), format, values...); + text += line; + }; + const auto count = [](std::uint64_t value) { return static_cast(value); }; + const auto validated = counters.lookups > counters.absent ? counters.lookups - counters.absent : 0; + const auto miss = [&](DrawMiss reason) { return count(counters.misses[static_cast(reason)]); }; + std::string ranks; + for (std::size_t rank = 0; rank < dispatchVariants(); ++rank) { + char item[32]; + std::snprintf(item, sizeof(item), "%s%llu", rank == 0 ? "" : " / ", count(counters.variantHitsByRank[rank])); + ranks += item; + } + append("[draw-cache] %llu lookups (10 s): %llu no entry, %llu validated in %.1f us each (key, lookup and every stage): %llu hits (every stage equal; stage hits by variant rank 1..k %s), misses by reason: front stage differing %llu, fragment differing %llu, other stage differing %llu, layout (push offset) %llu, gate %llu, stage count %llu; %llu stage validations (%llu equal, %.2f variants compared each); %llu inserts (%llu variants inserted, %llu evicted beyond k, %llu already present, %llu unstable), %zu entries (%llu variants, ~%.1f MiB), %llu evictions in total, %llu LRU moves; verify: %llu hits captured again, %llu stages differed\n", count(counters.lookups), count(counters.absent), count(validated), validated != 0 ? counters.validateUs / static_cast(validated) : 0.0, count(counters.hits), ranks.c_str(), miss(DrawMiss::FrontDiffering), miss(DrawMiss::FragmentDiffering), miss(DrawMiss::OtherDiffering), miss(DrawMiss::Layout), miss(DrawMiss::Gate), miss(DrawMiss::Stages), count(counters.stageValidations), count(counters.stageEqual), counters.stageValidations != 0 ? static_cast(counters.variantsCompared) / static_cast(counters.stageValidations) : 0.0, count(counters.inserts), count(counters.variantsInserted), count(counters.variantsEvicted), count(counters.present), count(counters.unstable), drawCache.size(), count(drawCacheVariants), static_cast(drawCacheVariantBytes) / (1024.0 * 1024.0), count(drawCacheEvictions), count(counters.touches), count(counters.verifyHits), count(counters.verifyMismatches)); + append("[draw-cache] register key (10 s): %llu lookups, %llu hits, key build %.1f us each; decode skipped %llu, partial (state/pixel/programs from the entry) %llu; facade log != table %llu; verify: %llu decodes compared, %llu differed\n", count(counters.registerKeyLookups), count(counters.registerKeyHits), counters.registerKeyLookups != 0 ? counters.keyUs / static_cast(counters.registerKeyLookups) : 0.0, count(counters.decodeSkipped), count(counters.decodePartial), count(counters.facadeMismatches), count(counters.verifyDecodes), count(counters.verifyDecodeMismatches)); + append("[draw-cache] stage data hits (10 s): %llu stages (%llu words refreshed), verified %llu; stage inserts with data positions %llu (%.1f positions each)\n", count(counters.stageDataHits), count(counters.stageDataWords), count(counters.stageDataVerified), count(counters.stageDataInserts), counters.stageDataInserts != 0 ? static_cast(counters.stageDataPositions) / static_cast(counters.stageDataInserts) : 0.0); + return text; +} + } 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 4f3db831ac..c13d57f4a8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -5,12 +5,16 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "Optimization/ResourceProgram.hpp" +#include #include #include +#include +#include +#include namespace AgcDriver::DriverDetail { -std::shared_ptr Driver::materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected) { +std::shared_ptr Driver::materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected, const std::optional& meshBarycentrics) try { PerformanceTimer timing("Shader.DrawStage"); using Stage = ShaderRecompiler::ShaderStage; phaseTiming.Phase(DrawRowVectors); @@ -20,28 +24,48 @@ std::shared_ptr Driver::materializeDraw program.binary, {waveSize, program.firstUserSgpr, program.userData, std::nullopt, program.binary.stage == Stage::Fragment ? std::optional(pixel) : std::nullopt, vertexInfos[i], memory, RegisteredFloatMode(*program.snapshot)}, localDevice->Target(), - {0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : graphics.stages.tessellation && pushOffset < Graphics::PipelinePushSlotBytes ? ShaderRecompiler::TessellationLevelPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes}, - ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}} + {0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : graphics.stages.tessellation && pushOffset < Graphics::PipelinePushSlotBytes ? ShaderRecompiler::TessellationLevelPushOffsetBytes : graphics.depthRemap.mode != 0u ? ShaderRecompiler::DepthRemapPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes}, + ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}, program.binary.stage == Stage::Mesh ? meshBarycentrics : std::nullopt} }; const auto waitedBefore = traceCapSync() || profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0; - const auto invocation = InvocationFor(*program.snapshot, program.codeOffset, request); + auto invocation = InvocationFor(*program.snapshot, program.codeOffset, request); timing.Mark("prepared_invocation"); auto& stageCapture = stageCaptures[i]; stageCapture.forgetSerial = GuestMemory::ForgetSerial(); stageCapture.pushOffset = pushOffset; + const auto named = [&](const std::exception& error, std::span regions) { + static const bool dumpShaders = std::getenv("APS5_DUMP_SHADERS") != nullptr; + std::string dump; + if (dumpShaders) { + try { + auto failed = request; + failed.context.memory = regions; + dump = dumpRequest(program.binary.codeAddress, failed, "_failed"); + } catch (...) {} + } + std::ostringstream where; + where << "shader 0x" << std::hex << program.binary.codeAddress << " stage " << std::dec << static_cast(program.binary.stage); + if (!dump.empty()) where << " (" << dump << ")"; + where << ": "; + return std::runtime_error(where.str() + error.what()); + }; const auto capture = [&] { const SampledReadScope sampling(evidenceReads); - return shaderMemory.Capture(invocation); + try { + return CaptureInvocation(shaderMemory, *program.snapshot, program.codeOffset, request, invocation); + } catch (const std::exception& error) { + throw named(error, shaderMemory.Regions()); + } }(); stageCapture.regions = shaderMemory.TakeRecentRegions(); + stageCapture.supplied = shaderMemory.SuppliedBlocks(); + stageCapture.userDataUse = capture->userDataUse; recompiled[i] = true; memory = shaderMemory.Regions(); - if (drawHit) { - for (std::size_t j = 0; j < programs.size(); ++j) { - if (matched[j] != nullptr && !recompiled[j]) memory.insert(memory.end(), matchedRegions[j].begin(), matchedRegions[j].end()); - } + for (std::size_t j = 0; j < programs.size(); ++j) { + if (matched[j] != nullptr && !recompiled[j] && (drawHit || j < i)) memory.insert(memory.end(), matchedRegions[j].begin(), matchedRegions[j].end()); } request.context.memory = memory; if (traceCapSync()) traceCapture("draw-capture", program.binary.codeAddress, submission.queue, memory, Graphics::Recorder::ThreadWaitedMs() - waitedBefore); @@ -75,37 +99,116 @@ std::shared_ptr Driver::materializeDraw phaseTiming.Phase(DrawRowCapture); timing.Mark("capture_resources"); - stageCapture.compiled = invocation.Materialize(*capture); + try { + stageCapture.compiled = invocation.Materialize(*capture); + } catch (const std::exception& error) { + throw named(error, request.context.memory); + } + stageCapture.capture = capture; timing.Mark("materialize"); + const auto walked = capture->userDataUse.asValue | capture->userDataUse.asBase; + auto mask = walked == 0 ? ~std::uint64_t{0} : walked | stageCapture.compiled->userDataRegisterReads; + if (vertexInfos[i]) { + for (const auto reg : {static_cast(vertexInfos[i]->fetchAttribReg), static_cast(vertexInfos[i]->fetchBufferReg)}) { + mask |= reg + 1u < 64u ? std::uint64_t{3} << reg : ~std::uint64_t{0}; + } + } + stageCapture.userDataMask = mask; phaseTiming.Phase(DrawRowRecompile); return stageCapture.compiled; +} catch (const std::exception& error) { + if (!SkipRefusedDraws()) throw; + throw RefusedDraw(programs[i].binary.codeAddress, error.what()); } -void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional& indirectCpu, const std::vector& programs, const std::vector& stageCaptures, const std::vector>& vertexInfos, const std::vector>& decodeReads, bool verifyHit, const std::vector>& matched, std::vector>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr& decode, DrawPhaseTiming& phaseTiming) { - if (useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu) && (verifyHit || admitDrawKey(drawKey))) { +void Driver::recordStageDataPositions(DispatchVariant& variant, const ShaderRecompiler::ResourceCapture& capture, const std::vector& decodeReads) { + const auto& compiled = *variant.compiled; + for (std::size_t b = 0; b < compiled.bindings.size(); ++b) { + if (compiled.bindings[b].role != ShaderRecompiler::DescriptorRole::FlattenedSrt) continue; + variant.flatBinding = static_cast(b); + static_cast(DataWordPositions(variant.runs, capture.readTrace.leaves, capture.readTrace.otherReads, variant.words, compiled.bindings[b].guestDescriptor, variant.dataPositions, variant.dataSlots)); + break; + } + if (variant.dataPositions.empty() || decodeReads.empty()) return; + std::vector decoded(variant.words.size(), false); + std::size_t offset = 0; + for (const auto& [begin, end] : variant.runs) { + for (const auto& read : decodeReads) { + const auto from = std::max(begin, read.address); + const auto to = std::min(end, read.address + read.size); + if (from >= to) continue; + for (auto word = (from - begin) / sizeof(std::uint32_t); word < (to - begin + sizeof(std::uint32_t) - 1) / sizeof(std::uint32_t); ++word) decoded[offset + static_cast(word)] = true; + } + offset += static_cast((end - begin) / sizeof(std::uint32_t)); + } + std::size_t kept = 0; + for (std::size_t k = 0; k < variant.dataPositions.size(); ++k) { + if (decoded[variant.dataPositions[k]]) continue; + variant.dataPositions[kept] = variant.dataPositions[k]; + variant.dataSlots[kept] = variant.dataSlots[k]; + ++kept; + } + variant.dataPositions.resize(kept); + variant.dataSlots.resize(kept); +} + +void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional& indirectCpu, const std::vector& programs, const std::vector& stageCaptures, const std::vector>& vertexInfos, const std::vector>& decodeReads, bool verifyHit, const std::vector>& matched, std::vector>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr& decode, DrawPhaseTiming& phaseTiming, const std::vector& reused, const std::shared_ptr& entry, std::uint64_t shapeKey) { + if (useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu) && (verifyHit || admitDrawKey(drawKey, registerKey && drawDataHits() ? shapeKey : 0))) { phaseTiming.Phase(DrawRowVectors); - std::uint64_t unstable = 0, mismatches = 0; + std::uint64_t unstable = 0, mismatches = 0, differingSameRuns = 0, differingWords = 0, differingRunsChanged = 0; for (std::size_t i = 0; i < programs.size(); ++i) { const auto& stageCapture = stageCaptures[i]; + if (reused[i]) { + fresh[i] = matched[i]; + continue; + } 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; + variant->userDataMask = stageCapture.userDataMask; if (vertexInfos[i]) variant->vertexInfo = std::make_shared(*vertexInfos[i]); 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))}); + for (const auto& read : decodeReads[i]) regions.push_back({read.address, 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, stageCapture.supplied); + 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]; + const auto front = std::find_if(variants.begin(), variants.end(), [&](const std::shared_ptr& kept) { return kept->pushOffset == variant->pushOffset; }); + if (front != variants.end() && (*front)->runs == variant->runs && (*front)->words.size() == variant->words.size()) { + ++differingSameRuns; + for (std::size_t w = 0; w < variant->words.size(); ++w) differingWords += (*front)->words[w] != variant->words[w] ? 1u : 0u; + } else if (front != variants.end()) { + ++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()); @@ -119,11 +222,22 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP } fresh[i] = std::move(variant); } - insertDrawEntry(drawKey, fresh, registerKey ? decode : nullptr); - if (unstable != 0 || mismatches != 0) { + insertDrawEntry(drawKey, fresh, registerKey ? decode : nullptr, registerKey && drawDataHits() ? shapeKey : 0); + std::uint64_t dataInserts = 0, dataPositions = 0; + for (std::size_t i = 0; i < programs.size(); ++i) { + if (fresh[i] == nullptr || reused[i] || fresh[i]->dataPositions.empty()) continue; + ++dataInserts; + dataPositions += fresh[i]->dataPositions.size(); + } + if (unstable != 0 || mismatches != 0 || differingSameRuns != 0 || differingRunsChanged != 0 || dataInserts != 0) { std::lock_guard cacheLock(drawCacheMutex); + drawEntryCounters.stageDataInserts += dataInserts; + drawEntryCounters.stageDataPositions += dataPositions; drawEntryCounters.unstable += unstable; drawEntryCounters.verifyMismatches += mismatches; + drawEntryCounters.differingSameRuns += differingSameRuns; + drawEntryCounters.differingWords += differingWords; + drawEntryCounters.differingRunsChanged += differingRunsChanged; } phaseTiming.Phase(DrawRowKeyLookupValidate); } 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 1168f3a115..8208e0805d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -2,6 +2,8 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include +#include namespace AgcDriver::DriverDetail { @@ -14,6 +16,64 @@ std::uint32_t ReadGraphicsRegister(const Registers& registers, std::uint32_t off } +void ReadUserWords(const QueueState& queue, DrawProgram& program, bool staticAbi) { + if (!program.nullPixel) Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, program.resourceRegister); + const auto resources = program.nullPixel ? 0u : ReadGraphicsRegister(queue.shader, program.resourceRegister); + const auto userCount = ((resources >> 1u) & 0x1fu) | (((resources >> 27u) & 1u) << 5u); + require(userCount <= 32, "graphics user SGPR count exceeds the register bank"); + program.userData.clear(); + program.userData.reserve(userCount + 8u); + for (std::uint32_t i = 0; i < userCount; ++i) { + Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, program.userDataBase + i); + program.userData.push_back(staticAbi ? ReadGraphicsRegister(queue.shader, program.userDataBase + i) : readUserData(queue.shader, program.userDataBase + i)); + } + if ((program.binary.stage == ShaderRecompiler::ShaderStage::Vertex || program.binary.stage == ShaderRecompiler::ShaderStage::Fragment) && program.snapshot->header.empty() && program.snapshot->userDataTailRegister) program.tailContinuation = ReadUserDataTailContinuation(program.binary.code, program.firstUserSgpr, program.userData); +} + +void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32_t pointerBase, bool pointerRequired, bool staticAbi) { + program.merged = true; + program.mergedPointer = pointerBase; + 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; + if (!pointerRequired && !queue.shader.contains(pointerBase) && !queue.shader.contains(pointerBase + 1)) return; + const auto low = ReadGraphicsRegister(queue.shader, pointerBase); + const auto high = ReadGraphicsRegister(queue.shader, pointerBase + 1); + const auto address = static_cast(low) | (static_cast(high) << 32u); + require(address != 0 || !pointerRequired, "merged shader user-data address is null"); + if (address == 0) return; + if (pointerRequired) GuestMemory::CheckRange(reinterpret_cast(address), 8, 4); + program.userData[0] = low; + program.userData[1] = high; +} + +void AppendProgramMemory(std::vector& memory, const std::vector& programs) { + for (const auto& program : programs) { + for (const auto& region : program.memory) { + if (region.bytes.empty()) continue; + const bool listed = std::any_of(memory.begin(), memory.end(), [&](const ShaderRecompiler::MemoryRegion& known) { return known.guestAddress == region.guestAddress && known.bytes.data() == region.bytes.data() && known.bytes.size() == region.bytes.size(); }); + if (!listed) memory.push_back(region); + } + } +} + +std::vector> UserDataWords(const std::vector& programs, const std::vector& roles, std::uint32_t location) { + using Role = ShaderRecompiler::ProgramRole; + require(roles.size() == programs.size(), "draw program roles are incomplete"); + std::vector> words; + if (location == 0x280u) return words; + for (std::size_t i = 0; i < programs.size(); ++i) { + if (roles[i] == Role::Fragment || roles[i] == Role::GeometryBack || location < programs[i].userDataBase) continue; + const auto word = location - programs[i].userDataBase + (8u - programs[i].firstUserSgpr); + if (word < programs[i].userData.size()) words.emplace_back(i, static_cast(word)); + } + return words; +} + void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment) { using Stage = ShaderRecompiler::ShaderStage; using Role = ShaderRecompiler::ProgramRole; @@ -32,26 +92,20 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const const auto& snapshot = *program; require((address - snapshot.codeAddress) % sizeof(std::uint32_t) == 0, "graphics entry point is not dword aligned"); require(snapshot.header.empty() || snapshot.type == type, "graphics program refers to an incompatible shader binary type"); - if (!nullPixel && snapshot.header.empty()) NoteRawGraphicsDraw(); - if (!nullPixel) Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, rsrc2); - const auto resources = nullPixel ? 0u : ReadGraphicsRegister(queue.shader, rsrc2); - const auto userCount = ((resources >> 1u) & 0x1fu) | (((resources >> 27u) & 1u) << 5u); - require(userCount <= 32, "graphics user SGPR count exceeds the register bank"); + if (!nullPixel && snapshot.header.empty()) NoteRawGraphicsDraw(snapshot); const auto codeOffset = static_cast((address - snapshot.codeAddress) / sizeof(std::uint32_t)); DrawProgram result{ {stage, address, std::span(snapshot.code).subspan(codeOffset), snapshot.headerAddress, snapshot.header}, userDataBase, - 8, + stage == Stage::Fragment ? 0u : 8u, {}, {{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}, {snapshot.headerAddress, snapshot.header}}}, program, codeOffset }; - for (std::uint32_t i = 0; i < userCount; ++i) { - Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, userDataBase + i); - result.userData.push_back((staticAbi ? ReadGraphicsRegister(queue.shader, userDataBase + i) : readUserData(queue.shader, userDataBase + i))); - } - if (stage == Stage::Vertex && snapshot.header.empty() && snapshot.userDataTailRegister) result.tailContinuation = ReadUserDataTailContinuation(result.binary.code, result.firstUserSgpr, result.userData); + result.resourceRegister = rsrc2; + result.nullPixel = nullPixel; + ReadUserWords(queue, result, staticAbi); return result; }; { @@ -64,27 +118,21 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const programs.push_back(prepare(programAddress(base), type, stage, resources, users)); roles.push_back(role); }; - const auto initializeMerged = [&](DrawProgram& program, std::uint32_t pointerBase, bool pointerRequired) { - program.firstUserSgpr = 0; - program.userData.insert(program.userData.begin(), 8, 0); - Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, pointerBase); - Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, pointerBase + 1); - if (staticAbi) return; - if (!pointerRequired && !queue.shader.contains(pointerBase) && !queue.shader.contains(pointerBase + 1)) return; - const auto low = ReadGraphicsRegister(queue.shader, pointerBase); - const auto high = ReadGraphicsRegister(queue.shader, pointerBase + 1); - const auto address = static_cast(low) | (static_cast(high) << 32u); - require(address != 0 || !pointerRequired, "merged shader user-data address is null"); - if (address == 0) return; - GuestMemory::CheckRange(reinterpret_cast(address), 8, 4); - program.userData[0] = low; - program.userData[1] = high; - }; - const auto& graphics = product->state; + auto& graphics = product->state; if (graphics.stages.path == Graphics::ShaderPath::Tessellation) { - if (!staticAbi || programAddress(0x148) != 0) append(0x148, 5, Stage::Local, 0x10b, 0x10c, Role::Local); - append(0x108, 7, Stage::TessellationControl, 0x10b, 0x10c, Role::Hull); - initializeMerged(programs.back(), 0x102, true); + const auto localAddress = programAddress(0x148); + const auto local = !staticAbi || localAddress != 0 ? ProgramSnapshot(registry, localAddress, {3, 5}) : nullptr; + const bool mergedHull = local != nullptr && !local->header.empty() && local->type == 3; + require(graphics.stages.tessellation.has_value(), "tessellation draw has no tessellation configuration"); + graphics.stages.tessellation->mergedHull = mergedHull; + if (mergedHull) { + append(0x148, 3, Stage::Local, 0x10b, 0x10c, Role::Local); + append(0x148, 3, Stage::TessellationControl, 0x10b, 0x10c, Role::Hull); + } else { + if (local != nullptr) append(0x148, 5, Stage::Local, 0x10b, 0x10c, Role::Local); + append(0x108, 7, Stage::TessellationControl, 0x10b, 0x10c, Role::Hull); + } + InitializeMerged(queue, programs.back(), 0x102, !mergedHull, staticAbi); append(0x0c8, 2, Stage::TessellationEvaluation, 0x08b, 0x08c, Role::Domain); } else if (graphics.stages.path == Graphics::ShaderPath::Geometry) { const auto frontAddress = programAddress(0xc8); @@ -92,7 +140,7 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const const auto type = front->header.empty() ? std::uint8_t{2} : front->type; require(type == 2 || type == 4, "invalid geometry front binary type"); append(0xc8, type, Stage::Mesh, 0x8b, 0x8c, Role::Main); - initializeMerged(programs.back(), 0x82, type == 4); + InitializeMerged(queue, programs.back(), 0x82, type == 4, staticAbi); if (type == 4) append(0x88, 6, Stage::Mesh, 0x8b, 0x8c, Role::GeometryBack); } else { append(0xc8, 2, Stage::Vertex, 0x8b, 0x8c, Role::Main); @@ -103,33 +151,45 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const require(rejection.empty(), rejection.c_str()); } append(0x008, 1, Stage::Fragment, 0x00b, 0x00c, Role::Fragment); - programs.back().firstUserSgpr = 0; } } } -std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Submission& submission) { +std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Submission& submission, const Graphics::Context& context) { auto product = std::make_shared(); product->state = Graphics::DecodeState(queue, VertexWorkgroupRequired(queue, *submission.shaders)); DecodeGraphicsPrograms(*product, queue, *submission.shaders, false, true); product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(product->state), Graphics::PixelProgramSkipped(queue)); + Graphics::NormalizeUnusedViewportDepth(product->state, product->pixel); + Graphics::ApplyDepthRemap(context, product->state); product->pixel.targetExportPacking = Graphics::ExportPackings(product->state); product->pixel.dualSourceBlend = product->state.dualSourceBlend; + product->pixel.reversedBlend = product->state.reversedBlend; product->pixel.alphaToCoverage = product->state.alphaToCoverage; + product->pixel.quadPixelMask = Graphics::QuadPixelMask(product->state); + if (product->state.sampleLocationGrid) { + const auto localDevice = device.Load(); + if (localDevice != nullptr) product->pixel.sampleOrder = localDevice->HostSampleOrder(product->state); + } + product->pixel.quadSampleMask = Graphics::QuadSampleMasks(product->state, product->pixel.sampleOrder); + if (product->pixel.stencilRefExportEnable && Graphics::StencilReferenceUnused(product->state)) { + const auto localDevice = device.Load(); + const auto capabilities = localDevice == nullptr ? std::vector{} : localDevice->Target().supportedCapabilities; + if (std::ranges::find(capabilities, static_cast(spv::CapabilityStencilExportEXT)) == capabilities.end()) product->pixel.stencilRefExportEnable = false; + } if (product->pixel.ancillary) product->pixel.rasterPrimitive = Graphics::RasterPrimitiveOf(product->state); if (product->pixel.ancillary) product->pixel.layerExported = product->state.layerExports != 0; product->pixel.ditheredAlphaToCoverage = product->state.ditheredAlphaToCoverage; + product->pixel.iteratesSamples = product->state.perSampleShading; + product->pixel.sampleLocations = Graphics::PixelSampleLocations(product->state, product->pixel); return product; } -void Driver::resolveDrawDecode(const QueueState& queue, const Submission& submission, std::shared_ptr& decode, bool registerKey, std::uint64_t drawKey, bool profile) { +void Driver::resolveDrawDecode(const QueueState& queue, const Submission& submission, const Graphics::Context& context, std::shared_ptr& decode, bool registerKey, std::uint64_t drawKey, bool profile) { if (decode == nullptr || verifyDrawRecipe()) { std::vector readLog; - struct LogScope { - explicit LogScope(std::vector* log) { Graphics::RegisterReadLog() = log; } - ~LogScope() { Graphics::RegisterReadLog() = nullptr; } - } logScope(verifyDrawRecipe() && registerKey ? &readLog : nullptr); - auto fresh = decodeDraw(queue, submission); + const Graphics::RegisterReadLogScope logScope(verifyDrawRecipe() && registerKey ? &readLog : nullptr); + auto fresh = decodeDraw(queue, submission, context); if (verifyDrawRecipe() && registerKey) { std::uint64_t facadeMismatches = 0; for (const auto read : readLog) { 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 40b6eeef9d..5cb9982751 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -1,10 +1,12 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include +#include namespace AgcDriver::DriverDetail { -void Driver::lookupDraw(const Submission& submission, const std::shared_ptr& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector& programs, const std::vector& roles, const std::vector>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr& entry, std::vector>& matched, std::vector>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array& phaseMs) { +void Driver::lookupDraw(const Submission& submission, const std::shared_ptr& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector& programs, const std::vector& roles, const std::vector>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr& entry, std::vector>& matched, std::vector>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array& phaseMs, const std::shared_ptr& dataEntry, DrawDataCandidates& dataCandidates, std::vector& stageData) { using Role = ShaderRecompiler::ProgramRole; if (useDrawEntries) { if (!registerKey) { @@ -21,12 +23,13 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(tess.breakAtInstance)}) mix(value); + mix(tess.mergedHull); } mix(graphics.rectList); mix(programs.size()); @@ -49,6 +52,10 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(vertex.pointSize)); + mix(std::bit_cast(vertex.pointSizeMin)); + mix(std::bit_cast(vertex.pointSizeMax)); for (std::uint32_t r = 0; r < vertex.resourcesNum; ++r) { for (const auto field : vertex.resources[r].fields) mix(field); const auto& destination = vertex.resourcesDst[r]; @@ -62,28 +69,39 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.conservativeZExport)); mix(pixel.orderedPixelShader); mix(pixel.stencilRefExportEnable); + mix(pixel.stencilOperationValueExport); for (const auto value : pixel.targetOutputMode) mix(value); for (const auto value : pixel.targetExportMapping) mix(value); for (const auto value : pixel.targetExportPacking) mix(static_cast(value)); mix(pixel.dualSourceBlend); + mix(pixel.reversedBlend); mix(pixel.alphaToCoverage); mix(static_cast(pixel.rasterPrimitive)); mix(pixel.ditheredAlphaToCoverage); + mix(pixel.quadPixelMask); + for (const auto mask : pixel.quadSampleMask) mix(mask); + mix(pixel.iteratesSamples); + mix(static_cast(pixel.positionLocation)); + for (const auto location : pixel.sampleLocations) mix(location); + for (const auto order : pixel.sampleOrder) mix(order); std::lock_guard cacheLock(drawCacheMutex); ++drawEntryCounters.lookups; const auto found = drawCache.find(drawKey); if (found != drawCache.end()) entry = found->second; else ++drawEntryCounters.absent; + maybeReportDrawCache(profile); } if (entry != nullptr) { const auto waitedBeforeValidate = profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0; std::optional miss; std::vector ranks(programs.size(), 0); - std::uint64_t stageValidations = 0, stageEqual = 0, compared = 0, imagesFlushed = 0, runsSynced = 0; + std::uint64_t stageValidations = 0, stageEqual = 0, compared = 0, imagesFlushed = 0, runsSynced = 0, dataStages = 0, dataWords = 0; + std::vector> liveData; + auto* live = drawStageDataHits() ? &liveData : nullptr; if (entry->stages.size() != programs.size()) miss = DrawMiss::Stages; std::uint32_t cursor = 0; @@ -101,15 +119,30 @@ 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); ++compared; - auto result = validateVariant(programs[i].binary.codeAddress, submission.queue, *variant, regions, imagesFlushed, runsSynced, sampling); - if (gateRetry() && (result == EntryOutcome::PublishMoved || result == EntryOutcome::PendingMoved)) result = validateVariant(programs[i].binary.codeAddress, submission.queue, *variant, regions, imagesFlushed, runsSynced, sampling); + auto result = validateVariant(programs[i].binary.codeAddress, submission.queue, *variant, regions, imagesFlushed, runsSynced, sampling, live); + if (gateRetry() && (result == EntryOutcome::PublishMoved || result == EntryOutcome::PendingMoved)) result = validateVariant(programs[i].binary.codeAddress, submission.queue, *variant, regions, imagesFlushed, runsSynced, sampling, live); if (!anyLayout) outcome = result; anyLayout = true; - if (result != EntryOutcome::Equal) continue; + if (result != EntryOutcome::Equal && result != EntryOutcome::EqualData) continue; + if (result == EntryOutcome::EqualData) { + auto& hit = stageData[i]; + hit.words = variant->words; + for (const auto& [position, value] : liveData) hit.words[position] = value; + regions.clear(); + appendEntryRegions(*variant, regions, &hit.words); + hit.compiled = patchDataWords(*variant, hit.words); + ++dataStages; + dataWords += liveData.size(); + } matched[i] = variant; ranks[i] = rank; ++stageEqual; @@ -130,6 +163,8 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(); rotated->decode = entry->decode; rotated->stages = entry->stages; + rotated->shape = entry->shape; rotated->recipes.store(entry->recipes.load()); for (std::size_t i = 0; i < programs.size(); ++i) { if (ranks[i] == 0) continue; @@ -167,6 +203,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(*miss)]; if (registerKey && entry->decode != nullptr) ++counters.decodePartial; @@ -178,9 +215,28 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr std::chrono::seconds(10)) { - counters.lastReport = std::chrono::steady_clock::now(); - reportDrawCache(counters); + } + } else if (dataEntry != nullptr && graphics.stages.path == Graphics::ShaderPath::Vertex && dataEntry->stages.size() == programs.size() && dataEntry->decode->programs.size() == programs.size()) { + std::uint64_t imagesFlushed = 0, runsSynced = 0; + const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::DrawCache); + std::optional sampling; + for (std::size_t i = 0; i < programs.size(); ++i) { + if (roles[i] == Role::GeometryBack || programs[i].userData.size() != dataEntry->decode->programs[i].userData.size()) continue; + const auto& now = programs[i].userData; + const auto& was = dataEntry->decode->programs[i].userData; + auto& candidates = dataCandidates[i]; + for (const auto& variant : dataEntry->stages[i]) { + bool usedEqual = true; + for (std::size_t w = 0; w < now.size() && usedEqual; ++w) { + if (now[w] != was[w] && (w >= 64u || ((variant->userDataMask >> w) & 1u) != 0)) usedEqual = false; + } + if (!usedEqual) continue; + if ((i == 0 && variant->pushOffset != 0) || std::any_of(candidates.begin(), candidates.end(), [&](const auto& candidate) { return candidate.first->pushOffset == variant->pushOffset; })) continue; + std::vector regions; + appendEntryRegions(*variant, regions); + auto result = validateVariant(programs[i].binary.codeAddress, submission.queue, *variant, regions, imagesFlushed, runsSynced, sampling); + if (gateRetry() && (result == EntryOutcome::PublishMoved || result == EntryOutcome::PendingMoved)) result = validateVariant(programs[i].binary.codeAddress, submission.queue, *variant, regions, imagesFlushed, runsSynced, sampling); + if (result == EntryOutcome::Equal) candidates.emplace_back(variant, std::move(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 new file mode 100644 index 0000000000..9ba7b4a1db --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -0,0 +1,271 @@ +#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 "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp" +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::DriverDetail { + +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()); +} + +std::atomic heldReadRanges{0}; +std::atomic heldReadQueries{0}; +std::atomic heldReadHits{0}; + +} + +DrawPipeline& DrawPipeline::Queue0() { + static DrawPipeline* pipeline = new DrawPipeline(); + return *pipeline; +} + +std::size_t DrawPipeline::Depth() { + static const std::size_t depth = [] { + const char* text = std::getenv("APS5_PIPELINED_DRAWS"); + if (std::getenv("APS5_LOCKED_DRAW_PREPARE") != nullptr || std::getenv("APS5_DRAW_DRAIN") != nullptr || std::getenv("APS5_DRAIN_ALL") != nullptr || std::getenv("APS5_NO_WORDWISE_CAPTURE") != nullptr) return std::size_t{0}; + if (text == nullptr) return std::size_t{8}; + const auto value = std::strtoull(text, nullptr, 10); + return value == 0 ? std::size_t{0} : value == 1 ? std::size_t{8} : static_cast(std::min(value, 256)); + }(); + 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; +} + +std::atomic& DrawPipeline::EpochToken() { + static std::atomic token{1}; + return token; +} + +void DrawPipeline::FollowEpoch(std::uint64_t token) { + static thread_local std::uint64_t followed = 0; + if (followed == token) return; + followed = 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); + std::rethrow_exception(error); +} + +void DrawPipeline::Enqueue(Commit commit, std::vector writes, std::uint64_t labelAddress, std::vector labelBytes, std::vector reads) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + std::unique_lock lock(mutex); + rethrowFailure(); + 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(); + ++idleWaiters; + idle.wait(lock, [&] { return items.size() < Depth(); }); + --idleWaiters; + fullWaitNs += elapsedNs(start); + rethrowFailure(); + } + depthSum += items.size(); + ++enqueued; + heldReadRanges.fetch_add(reads.size(), std::memory_order_relaxed); + items.push_back({std::move(commit), std::move(writes), labelAddress, std::move(labelBytes), std::move(reads)}); + outstanding.fetch_add(1, std::memory_order_release); + if (!thread.joinable()) { + thread = std::thread([this] { run(); }); + 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(); + if (now - lastReport >= std::chrono::seconds(10)) report(now); +} + +void DrawPipeline::report(std::chrono::steady_clock::time_point now) { + lastReport = now; + std::string drainText; + for (std::size_t i = 0; i < drains.size(); ++i) { + char item[64]; + std::snprintf(item, sizeof(item), " %s %llu (%.1f ms)", DrainNames[i], static_cast(drains[i]), static_cast(drainWaitNs[i]) / 1e6); + drainText += item; + } + drainText += "; packet drains by opcode:"; + for (int shown = 0; shown < 5; ++shown) { + const auto top = std::max_element(drainOpcodes.begin(), drainOpcodes.begin() + 0x100); + if (*top == 0) break; + char item[32]; + std::snprintf(item, sizeof(item), " 0x%02x x%llu", static_cast(top - drainOpcodes.begin()), static_cast(*top)); + drainText += item; + *top = 0; + } + drainOpcodes = {}; + const auto perCommit = commits == 0 ? 0.0 : static_cast(commitNs) / 1e3 / static_cast(commits); + const auto perDraw = enqueued == 0 ? 0.0 : static_cast(fullWaitNs) / 1e3 / static_cast(enqueued); + const auto queued = enqueued == 0 ? 0.0 : static_cast(depthSum) / static_cast(enqueued); + char reads[64]; + std::snprintf(reads, sizeof(reads), " %llu read ranges, %llu stores held", static_cast(heldReadRanges.load(std::memory_order_relaxed)), static_cast(heldReadHits.exchange(0, std::memory_order_relaxed))); + drainText += reads; + std::fprintf(stderr, "[draw] pipelined (10 s, depth %zu): %llu commits at %.1f us each on the committer, %llu failed; %llu enqueued behind %.2f on average, waiting %.1f us per draw for room;%s; drains:%s\n", Depth(), static_cast(commits), perCommit, static_cast(commitErrors), static_cast(enqueued), queued, perDraw, reads, drainText.c_str()); + commits = commitNs = commitErrors = enqueued = fullWaitNs = depthSum = 0; + drains = {}; + drainWaitNs = {}; +} + +void DrawPipeline::run() { + PerformanceTrace::BindThread("draw committer"); + StartCommitterSampler(); + 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() || closing; }); + committerWaiting = false; + if (items.empty()) break; + commit = stopped ? nullptr : &items.front().commit; + } + const auto start = std::chrono::steady_clock::now(); + std::exception_ptr fatal; + bool failed = false; + bool shutdown = false; + try { + if (commit != nullptr) (*commit)(); + } catch (const std::exception&) { + failed = true; + fatal = std::current_exception(); + } catch (const ProcessShutdown&) { + shutdown = true; + fatal = std::current_exception(); + } catch (...) { + fatal = std::current_exception(); + } + const auto spent = elapsedNs(start); + if (fatal != nullptr && !stopped) { + stopped = true; + if (!shutdown) Driver::Get().ReportFailure(fatal); + } + std::lock_guard lock(mutex); + items.pop_front(); + ++commits; + commitNs += spent; + if (failed) ++commitErrors; + if (fatal != nullptr && failure == nullptr) failure = fatal; + 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(); }); + --idleWaiters; + const auto index = static_cast(reason); + ++drains[index]; + ++drainOpcodes[std::min(opcode, 0x100)]; + drainWaitNs[index] += elapsedNs(start); + rethrowFailure(); +} + +std::uint64_t DrawPipeline::DrainRequestsByOpcode(std::uint32_t opcode) { + return Queue0().drainRequests[std::min(opcode, 0x100)].load(std::memory_order_relaxed); +} + +bool DrawPipeline::HeldReads(std::uint64_t address, std::size_t bytes) { + if (bytes == 0 || std::getenv("APS5_NO_QUEUED_DRAW_READ_ORDER") != nullptr) return false; + const auto end = address + bytes; + auto& pipeline = Queue0(); + std::lock_guard lock(pipeline.mutex); + heldReadQueries.fetch_add(1, std::memory_order_relaxed); + for (const auto& item : pipeline.items) { + for (const auto& [begin, limit] : item.reads) { + if (address >= limit || begin >= end) continue; + heldReadHits.fetch_add(1, std::memory_order_relaxed); + return true; + } + } + return false; +} + +void DrawPipeline::ReleaseRunningReads() { + std::lock_guard lock(mutex); + if (!items.empty()) items.front().reads.clear(); +} + +std::optional DrawPipeline::PendingLabel(std::uint64_t address, std::size_t bytes) { + if (bytes == 0 || bytes > 8) return std::nullopt; + const auto end = address + bytes; + std::lock_guard lock(mutex); + for (auto item = items.rbegin(); item != items.rend(); ++item) { + const bool overlaps = std::any_of(item->writes.begin(), item->writes.end(), [&](const Range& range) { return address < range.second && range.first < end; }); + if (!overlaps) continue; + if (item->labelBytes.empty() || address < item->labelAddress || end > item->labelAddress + item->labelBytes.size()) return std::nullopt; + std::uint64_t value = 0; + std::memcpy(&value, item->labelBytes.data() + (address - item->labelAddress), bytes); + return value; + } + return std::nullopt; +} + +bool DrawPipeline::Overlaps(std::uint64_t address, std::size_t bytes) { + const auto end = address + bytes; + std::lock_guard lock(mutex); + for (const auto& item : items) { + for (const auto& [begin, limit] : item.writes) { + if (address < limit && begin < end) return true; + } + } + return false; +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp index 267b6cf704..6311d6461f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" @@ -15,6 +16,7 @@ std::optional Driver::precheckDraw(const QueueState& queue, const S if (!drawParameters.indirect && !drawParameters.indexed && (drawParameters.indexCount == 0 || drawParameters.instanceCount == 0)) return DrawVerdict::Nothing; if (const auto pass = Graphics::DecodeColorMetadataPass(queue)) { require(!drawParameters.indirect, "indirect CB metadata passes are unsupported"); + if (DrawPipeline::Active()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, (packet[0] >> 8u) & 0xffu); GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Draw); std::lock_guard gpuLock(GuestMemory::GpuMutex()); if (device == nullptr) device = std::make_shared(); @@ -23,6 +25,17 @@ std::optional Driver::precheckDraw(const QueueState& queue, const S localDevice->ColorMetadataPass(*pass); return DrawVerdict::Drawn; } + if (const auto copy = Graphics::DecodeDepthCopyPass(queue)) { + require(!drawParameters.indirect, "indirect DB depth copies are unsupported"); + if (DrawPipeline::Active()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, (packet[0] >> 8u) & 0xffu); + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Draw); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + if (device == nullptr) device = std::make_shared(); + const std::shared_ptr localDevice = device; + recordLabelsForPacket(localDevice.get(), submission.queue); + localDevice->DepthCopyPass(*copy); + return DrawVerdict::Drawn; + } if (Graphics::DepthResummarizeDrawsNothing(queue)) return DrawVerdict::Nothing; rejected = Graphics::DepthMaintenanceRejection(queue); if (!rejected.empty()) return DrawVerdict::Rejected; @@ -45,7 +58,7 @@ std::optional Driver::precheckDraw(const QueueState& queue, const S const auto primitive = queue.userConfig.find(0x242); const std::uint32_t allOnes = drawParameters.indexSize == 2 ? 0xffffu : 0xffffffffu; const auto type = primitive == queue.userConfig.end() ? 0u : primitive->second & 0x3fu; - const bool strip = type == 3 || type == 5 || type == 6; + const bool strip = type == 3 || type == 5 || type == 6 || type == 21; const bool list = type == 1 || type == 2 || type == 4; const auto restartDevice = device.Load(); if (!strip && !(list && restartDevice != nullptr && restartDevice->PrimitiveListRestart())) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawProgramTrace.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawProgramTrace.cpp new file mode 100644 index 0000000000..7123e8727d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawProgramTrace.cpp @@ -0,0 +1,55 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawProgramTrace.hpp" +#include +#include + +namespace AgcDriver::DriverDetail { +namespace { + +const char* StageName(ShaderRecompiler::ShaderStage stage) { + using ShaderRecompiler::ShaderStage; + switch (stage) { + case ShaderStage::Compute: return "compute"; + case ShaderStage::Vertex: return "vertex"; + case ShaderStage::TessellationControl: return "tess-control"; + case ShaderStage::TessellationEvaluation: return "tess-evaluation"; + case ShaderStage::Geometry: return "geometry"; + case ShaderStage::Fragment: return "fragment"; + case ShaderStage::Local: return "local"; + case ShaderStage::Mesh: return "mesh"; + } + throw std::invalid_argument("unknown shader stage in draw trace"); +} + +const char* RoleName(ShaderRecompiler::ProgramRole role) { + using ShaderRecompiler::ProgramRole; + switch (role) { + case ProgramRole::Main: return "main"; + case ProgramRole::GeometryBack: return "geometry-back"; + case ProgramRole::Local: return "local"; + case ProgramRole::Hull: return "hull"; + case ProgramRole::Domain: return "domain"; + case ProgramRole::Fragment: return "fragment"; + } + throw std::invalid_argument("unknown program role in draw trace"); +} + +} + +std::string FormatDrawProgramTrace(std::uint64_t submission, std::uint32_t queue, std::span programs) { + char prefix[128]; + const auto size = std::snprintf(prefix, sizeof(prefix), "[draw-programs] submission=%llu queue=0x%x count=%zu", + static_cast(submission), queue, programs.size()); + if (size < 0 || static_cast(size) >= sizeof(prefix)) throw std::runtime_error("draw program trace prefix overflow"); + std::string result(prefix, static_cast(size)); + char item[160]; + for (const auto& program : programs) { + const auto itemSize = std::snprintf(item, sizeof(item), " %s/%s=0x%llx(entry=0x%llx)", + StageName(program.stage), RoleName(program.role), + static_cast(program.codeAddress), static_cast(program.entryAddress)); + if (itemSize < 0 || static_cast(itemSize) >= sizeof(item)) throw std::runtime_error("draw program trace entry overflow"); + result.append(item, static_cast(itemSize)); + } + return result; +} + +} 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 af01cae687..288ce32804 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -1,9 +1,11 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include #include #include +#include namespace AgcDriver::DriverDetail { @@ -31,23 +33,10 @@ const auto& registerMasks() { return masks; } -} - -std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool incremental) { - const auto& masks = registerMasks(); - const auto fingerprint = [&](const Registers& registers, Graphics::RegisterBank bank) { - const auto& mask = masks[static_cast(bank)]; - return incremental ? registers.Fingerprint(mask) : registers.RecomputeFingerprint(*mask); - }; - auto shape = (0xcbf29ce484222325ull ^ fingerprint(queue.context, Graphics::RegisterBank::Context)) * 0x100000001b3ull; - shape = (shape ^ fingerprint(queue.shader, Graphics::RegisterBank::Shader)) * 0x100000001b3ull; - shape = (shape ^ fingerprint(queue.userConfig, Graphics::RegisterBank::UserConfig)) * 0x100000001b3ull; - auto exact = shape; +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) { - exact = (exact ^ it->first) * 0x100000001b3ull; - exact = (exact ^ it->second) * 0x100000001b3ull; - } + 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); @@ -56,19 +45,127 @@ std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool } words(0x082u, 2); words(0x102u, 2); +} + +} + +std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool incremental, std::uint64_t* shapeKey, RegisterStateKeyParts* parts) { + const auto& masks = registerMasks(); + const auto fingerprint = [&](const Registers& registers, Graphics::RegisterBank bank) { + const auto& mask = masks[static_cast(bank)]; + return incremental ? registers.Fingerprint(mask) : registers.RecomputeFingerprint(*mask); + }; + auto shape = (0xcbf29ce484222325ull ^ fingerprint(queue.context, Graphics::RegisterBank::Context)) * 0x100000001b3ull; + shape = (shape ^ fingerprint(queue.shader, Graphics::RegisterBank::Shader)) * 0x100000001b3ull; + shape = (shape ^ fingerprint(queue.userConfig, Graphics::RegisterBank::UserConfig)) * 0x100000001b3ull; + if (shapeKey != nullptr) *shapeKey = shape; + auto exact = shape; + std::uint64_t userData = 0xcbf29ce484222325ull; + std::uint64_t auxiliary = 0xcbf29ce484222325ull; + const auto mixPart = [](std::uint64_t& part, std::uint64_t value) { part = (part ^ value) * 0x100000001b3ull; }; + const auto words = [&](std::uint32_t base, std::uint32_t count, std::uint64_t& part) { + for (auto it = queue.shader.lower_bound(base); it != queue.shader.end() && it->first < base + count; ++it) { + exact = (exact ^ it->first) * 0x100000001b3ull; + exact = (exact ^ it->second) * 0x100000001b3ull; + mixPart(part, it->first); + mixPart(part, 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), userData); + } + words(0x082u, 2, auxiliary); + words(0x102u, 2, auxiliary); + if (parts != nullptr) *parts = {shape, userData, auxiliary}; return exact; } -std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial) { - static const bool allUserWords = std::getenv("APS5_DRAW_KEY_ALL_USER_WORDS") != nullptr; - static const bool verify = std::getenv("APS5_VERIFY_REGISTER_KEYS") != nullptr; - auto key = RegisterStateKey(queue, allUserWords); - if (verify && key != RegisterStateKey(queue, allUserWords, false)) throw std::runtime_error("incremental register fingerprint disagrees with full state"); +std::uint64_t DrawShaderSetKey(const QueueState& queue, const ShaderRegistry& registry) { + std::uint64_t key = 0xcbf29ce484222325ull; + const auto mix = [&](std::uint64_t value) { key = (key ^ value) * 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; } + 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; +} + +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; + shapeKey ^= value; + shapeKey *= 0x100000001b3ull; + }; + if (components != nullptr) { + (*components)[0] = parts.stableRegisters; + (*components)[1] = parts.userDataRegisters; + (*components)[2] = parts.auxiliaryRegisters; + (*components)[3] = deviceSerial; + (*components)[4] = DrawShaderSetKey(queue, registry); + } mix(deviceSerial); for (const auto base : {0x008u, 0x088u, 0x0c8u, 0x108u, 0x148u}) { const auto low = queue.shader.find(base); @@ -86,12 +183,38 @@ 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; } +void Driver::traceDrawKeyLookup(const std::array& components, bool cacheMiss) { + if (!traceDrawCache()) return; + struct Previous { std::array parts{}; }; + struct PreviousStorage {}; + auto& previous = HostThreadLocal, PreviousStorage>(); + const auto shaderSet = components[4]; + if (previous.size() >= 2048 && previous.find(shaderSet) == previous.end()) previous.clear(); + const auto [it, inserted] = previous.try_emplace(shaderSet); + if (!cacheMiss) { it->second.parts = {components[0], components[1], components[2], components[3]}; return; } + auto& counters = drawEntryCounters; + if (inserted) ++counters.registerKeyMissChanges[4]; + else { + bool changed = false; + for (std::size_t i = 0; i < 4; ++i) if (it->second.parts[i] != components[i]) { ++counters.registerKeyMissChanges[i]; changed = true; } + if (!changed) ++counters.registerKeyMissChanges[5]; + } + it->second.parts = {components[0], components[1], components[2], components[3]}; +} + bool Driver::sameVertexInfo(const ShaderRecompiler::ShaderVertexStageInfo& a, const ShaderRecompiler::ShaderVertexStageInfo& b) { - if (a.resourcesNum != b.resourcesNum || a.fetchAttribReg != b.fetchAttribReg || a.fetchBufferReg != b.fetchBufferReg || a.fetchEmbedded != b.fetchEmbedded || a.paClVsOutCntl != b.paClVsOutCntl) return false; + if (a.resourcesNum != b.resourcesNum || a.fetchAttribReg != b.fetchAttribReg || a.fetchBufferReg != b.fetchBufferReg || a.fetchEmbedded != b.fetchEmbedded || a.paClVsOutCntl != b.paClVsOutCntl || a.pointList != b.pointList || a.pointSize != b.pointSize || a.pointSizeMin != b.pointSizeMin || a.pointSizeMax != b.pointSizeMax) return false; + if (a.depthRemapMode != b.depthRemapMode) return false; for (std::uint32_t i = 0; i < a.resourcesNum && i < a.resources.size(); ++i) { if (a.resources[i].fields != b.resources[i].fields) return false; const auto& x = a.resourcesDst[i]; @@ -113,12 +236,12 @@ 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; if (!x) return true; - return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup && x->primitiveGroupSize == y->primitiveGroupSize && x->breakAtInstance == y->breakAtInstance; + return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup && x->primitiveGroupSize == y->primitiveGroupSize && x->breakAtInstance == y->breakAtInstance && x->mergedHull == y->mergedHull; }; if (s.stages.path != t.stages.path || s.stages.registerValue != t.stages.registerValue || s.stages.vertexWaveSize != t.stages.vertexWaveSize || s.stages.fragmentWaveSize != t.stages.fragmentWaveSize || !sameMesh(s.stages.mesh, t.stages.mesh) || !sameTess(s.stages.tessellation, t.stages.tessellation)) return false; if (!sameColor(s.color, t.color) || s.colors.size() != t.colors.size() || s.blends.size() != t.blends.size()) return false; @@ -128,18 +251,19 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { for (std::size_t i = 0; i < s.blends.size(); ++i) { if (!sameBlend(s.blends[i], t.blends[i])) return false; } - if (s.hasColorTarget != t.hasColorTarget || s.rectList != t.rectList || s.renderExtent.width != t.renderExtent.width || s.renderExtent.height != t.renderExtent.height || s.topology != t.topology || s.negativeOneToOne != t.negativeOneToOne || s.depthClamp != t.depthClamp || s.conservativeRasterization != t.conservativeRasterization || s.cullMode != t.cullMode || s.frontFace != t.frontFace || !sameBlend(s.blend, t.blend) || s.blendConstants != t.blendConstants) return false; + if (s.hasColorTarget != t.hasColorTarget || s.rectList != t.rectList || s.renderExtent.width != t.renderExtent.width || s.renderExtent.height != t.renderExtent.height || s.topology != t.topology || s.negativeOneToOne != t.negativeOneToOne || s.depthClamp != t.depthClamp || s.conservativeRasterization != t.conservativeRasterization || s.perSampleShading != t.perSampleShading || s.cullMode != t.cullMode || s.polygonMode != t.polygonMode || s.frontFace != t.frontFace || !sameBlend(s.blend, t.blend) || s.blendConstants != t.blendConstants) return false; if (std::memcmp(&s.viewport, &t.viewport, sizeof(VkViewport)) != 0 || std::memcmp(&s.scissor, &t.scissor, sizeof(VkRect2D)) != 0) return false; const auto& p = a.pixel; const auto& q = b.pixel; - if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.stencilRefExportEnable != q.stencilRefExportEnable || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; + if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.frontFaceAllBits != q.frontFaceAllBits || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.stencilRefExportEnable != q.stencilRefExportEnable || p.stencilOperationValueExport != q.stencilOperationValueExport || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; if (a.roles != b.roles || a.programs.size() != b.programs.size()) return false; for (std::size_t i = 0; i < a.programs.size(); ++i) { const auto& x = a.programs[i]; const auto& y = b.programs[i]; if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false; } - if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage) return false; + if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations || p.sampleOrder != q.sampleOrder) return false; + if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.truncatedCoverage != t.truncatedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false; return true; } 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..33665a380c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp @@ -0,0 +1,273 @@ +#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 "prx/libSceAgcDriver/Execution/include/ShaderMemory.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::span> supplied) { + 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 (std::size_t index = 0; index < regions.size(); ++index) { + const auto& region = regions[index]; + 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) { + AppendSharedWords(variant.shared, variant.runs.size(), offset, region.bytes, supplied); + 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; + AppendSharedWords(variant.shared, variant.runs.size(), offset + start, region.bytes.subspan(start * sizeof(std::uint32_t), (word - start) * sizeof(std::uint32_t)), supplied); + 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..d3049765aa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp @@ -1,5 +1,6 @@ #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/Pm4.hpp" #include @@ -28,10 +29,13 @@ std::optional Driver::ClassifyIndirectDraw(const Sha using Path = Graphics::IndirectDrawPath; const auto support = localDevice->DrawIndirectSupport(); const auto fail = [&](Path reason) { if (!indirectCpu) indirectCpu = reason; }; + if (drawParameters.indexed && !GuestMemory::Accessible(reinterpret_cast(drawParameters.indexAddress), static_cast(drawParameters.indexCount) * drawParameters.indexSize)) fail(Path::IndexRange); constexpr std::uint32_t NoLocation = 0x280u; const bool meshPath = graphics.stages.mesh.has_value() && !graphics.stages.tessellation && !graphics.rectList; const bool meshGpuSide = meshPath && drawParameters.indexed && indirect.count == 1 && !indirect.countIndirect && indirect.baseVertexLocation == NoLocation && indirect.startInstanceLocation == NoLocation && (!indirect.drawIndexEnabled || indirect.drawIndexLocation == NoLocation); - if ((graphics.stages.path != Graphics::ShaderPath::Vertex || graphics.rectList) && !meshGpuSide) fail(Path::NonVertexPath); + static const bool rectListOnGpu = std::getenv("APS5_NO_RECT_INDIRECT") == nullptr; + const bool rectListGpuSide = rectListOnGpu && graphics.rectList && !graphics.stages.mesh && !graphics.stages.tessellation; + if ((graphics.stages.path != Graphics::ShaderPath::Vertex || (graphics.rectList && !rectListGpuSide)) && !meshGpuSide) fail(Path::NonVertexPath); if (indirect.drawIndexEnabled && indirect.drawIndexSgpr >= 0) fail(Path::DrawIndex); if (indirect.countIndirect && !support.count) fail(Path::FeatureGap); if (indirect.countIndirect && indirect.count > 1 && !support.multi) fail(Path::FeatureGap); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index 54f524103e..04a1f72a02 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -1,10 +1,13 @@ #include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include namespace AgcDriver { void Submit(const Packet* packet, std::uint32_t queue) try { + KernelNoteGpuSubmit_nid_no_patch(); DriverDetail::Driver::Get().Submit(packet, queue); } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); @@ -83,6 +86,10 @@ extern "C" void AgcDriverShutdown_nid_postfix() { AgcDriver::Shutdown(); } +extern "C" void AgcDriverRequestScreenshot_nid_postfix() { + AgcDriver::Screenshot::Request(); +} + extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader) try { AgcDriver::RegisterShader(shader); } catch (const ProcessShutdown&) { @@ -95,6 +102,20 @@ extern "C" void AgcDriverSuspendPoint_nid_postfix() try { LibcAwaitExit_nid_postfix(); } +extern "C" std::uint32_t AgcDriverReserveEopFlip_nid_postfix(const AgcDriver::FlipInfo& info) { + return AgcDriver::DriverDetail::Driver::Get().ReserveEopFlip(info); +} + +extern "C" void AgcDriverSubmitFlip_nid_postfix(const AgcDriver::FlipInfo& info) try { + AgcDriver::DriverDetail::Driver::Get().SubmitFlip(info); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + +extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix() { + return AgcDriver::DriverDetail::Driver::Get().DispatchesAdoptedAhead(); +} + extern "C" void AgcDriverRegisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr& output) { AgcDriver::RegisterVideoOutput(handle, output); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 7928206823..0557a7af52 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -1,15 +1,25 @@ +#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 "prx/libc/include/GuestArena.hpp" #include #include #include +#include +#include +#include #include #include #ifdef _WIN32 @@ -19,6 +29,7 @@ #include #else #include +#include #endif namespace AgcDriver::DriverDetail { @@ -29,6 +40,57 @@ constexpr std::uintptr_t InlineImportStackBytes = 256 * 1024; std::atomic gpuMapImportThreads{0}; +void PrintFailure(std::exception_ptr error) { + try { + std::rethrow_exception(error); + } catch (const std::exception& cause) { + std::fprintf(stderr, "[gpu] driver failure: %s\n", cause.what()); + } catch (...) { + std::fprintf(stderr, "[gpu] driver failure with a non-standard exception\n"); + } + FlushAsyncLog_nid_no_patch(); + std::fflush(stderr); +} + +constexpr std::uint64_t DriverMemoryAddress = 0xfe0000000ull + GuestArena::GuestAddressOffset; +constexpr std::size_t DriverMemoryBytes = 2u << 20u; +constexpr std::uint64_t FsTableAddress = DriverMemoryAddress + 0x40000ull; +constexpr std::uint64_t FsTableSlotBytes = 16u; +constexpr std::uint64_t FsTableSlots = 6u; +constexpr std::uint64_t FsRoutineRecords = 8u; +constexpr std::uint64_t FsRoutineRecordBytes = 16u; +constexpr std::uint64_t FsRoutineTableBytes = FsRoutineRecords * FsRoutineRecordBytes; +constexpr std::uint64_t FsRoutineTableAddress = FsTableAddress + FsTableSlots * FsTableSlotBytes; +constexpr std::uint64_t FsRoutineAddress = FsRoutineTableAddress + FsRoutineTableBytes; +constexpr std::uint32_t ScalarSetpcB64LinkS14 = 0xbe80200eu; +constexpr std::uint32_t ScalarEndProgram = 0xbf810000u; +constexpr std::uint64_t FsTableDword3 = 0x5204u; + +void MapDriverMemory() { + void* mapped; +#ifdef _WIN32 + mapped = reinterpret_cast(DriverMemoryAddress); + GuestArena::GuestArenaCommit_nid_postfix(mapped, DriverMemoryBytes, PAGE_READWRITE, DriverMemoryBytes); +#else + mapped = mmap(reinterpret_cast(DriverMemoryAddress), DriverMemoryBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); + if (mapped == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map the AGC driver memory"); +#endif + if (reinterpret_cast(mapped) != DriverMemoryAddress) throw std::runtime_error("the AGC driver memory was not mapped at its fixed address"); + auto* routineTable = reinterpret_cast(FsRoutineTableAddress); + for (std::uint64_t record = 0; record < FsRoutineRecords; ++record) { + routineTable[record * 2u] = FsRoutineAddress; + routineTable[record * 2u + 1u] = 0u; + } + auto* routine = reinterpret_cast(FsRoutineAddress); + routine[0] = ScalarSetpcB64LinkS14; + routine[1] = ScalarEndProgram; + auto* slot = reinterpret_cast(FsTableAddress); + slot[0] = (FsRoutineTableAddress & 0xffffffffffffull) | 16ull << 48u; + slot[1] = FsTableDword3 << 32u | FsRoutineRecords; + GuestAllocations::Mutation mutation; + mutation.Add(mapped, DriverMemoryBytes, true, true, true); +} + bool StackHoldsImport() { #ifdef _WIN32 ULONG_PTR low = 0; @@ -54,8 +116,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; } @@ -89,6 +150,7 @@ void Driver::stop() { { std::lock_guard lock(mutex); stopping = true; + eopFlips.clear(); for (auto& [queue, worker] : workers) { worker.pending.clear(); worker.unfinishedWrites.clear(); @@ -99,18 +161,32 @@ 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()); + Graphics::ReportHostImports(); 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 const bool driverMemory = (MapDriverMemory(), true); + static_cast(driverMemory); + LibcSetExitDrain_nid_no_patch([] { + const auto current = Driver::Get().device.Load(); + if (current) current->SavePipelineCache(); + }); static_cast(GuestMemory::TrackerGeneration()); ShaderMemory::SetWaitedMsProvider(&Graphics::Recorder::ThreadWaitedMs); try { @@ -148,13 +224,17 @@ Driver::Driver() { void Driver::WaitIdle() { require(!onWorkerThread(), "worker cannot wait for itself"); - std::unique_lock lock(mutex); - const auto target = accepted; - ++idleWaiters; - changed.wait(lock, [&] { return failure != nullptr || stopping || completed >= target; }); - --idleWaiters; - rethrowFailure(); - checkStopping(); + { + std::unique_lock lock(mutex); + const auto target = accepted; + ++idleWaiters; + changed.wait(lock, [&] { return failure != nullptr || stopping || completed >= target; }); + --idleWaiters; + rethrowFailure(); + checkStopping(); + } + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + if (const auto localDevice = device.Load()) localDevice->WaitIdle(); } void Driver::CheckFailure() { @@ -167,9 +247,11 @@ void Driver::CheckFailure() { void Driver::ReportFailure(std::exception_ptr error) { require(error != nullptr, "null asynchronous failure"); + bool first = false; { std::lock_guard lock(mutex); - if (!failure) failure = error; + first = !failure; + if (first) failure = error; failed.store(true, std::memory_order_release); for (const auto& [handle, output] : outputs) output->Fail(failure); for (auto& [queue, worker] : workers) { @@ -180,6 +262,7 @@ void Driver::ReportFailure(std::exception_ptr error) { worker.unfinishedWrites.clear(); worker.queued.store(0, std::memory_order_release); } + if (first) PrintFailure(error); } changed.notify_all(); } @@ -200,3 +283,20 @@ bool& Driver::onWorkerThread() { } } + +namespace AgcDriver { + +void FlushAsyncLog_nid_no_patch() { + AsyncLog::Get().Flush(); +} + +std::unique_lock HoldAsyncLog() { + return AsyncLog::Get().Hold(); +} + +} + +extern "C" void* AgcDriverStaticTable_nid_postfix() { + static_cast(AgcDriver::DriverDetail::Driver::Get()); + return reinterpret_cast(AgcDriver::DriverDetail::FsTableAddress); +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp index dc96d10391..6a76867635 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include #include #include #include @@ -9,6 +10,7 @@ namespace AgcDriver::DriverDetail { std::atomic knownValueEntries{0}, knownValueReads{0}, knownValueVerified{0}, knownValueMismatches{0}; +std::array, CopyPaths> copyPathCounts{}; bool Driver::matchesCopyKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { static const bool enabled = std::getenv("APS5_NO_COPY_HLE") == nullptr; @@ -27,9 +29,34 @@ bool Driver::matchesCopyKernel(std::span code, const std::v return userData[3] == 0x00014004u && userData[7] == 0x00014004u && userData[11] == 0x0004dfacu && stride(1) == 4 && stride(5) == 4 && stride(9) == 16 && !swizzled(1) && !swizzled(5) && !swizzled(9) && userData[10] >= 1; } +bool MatchesStreamCopyKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { + static const bool enabled = std::getenv("APS5_NO_COPY_HLE") == nullptr; + if (!enabled || userData.size() != 8 || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1 || !compute.groupIdEnable[0]) return false; + static constexpr std::array kernel{0xd7460008u, 0x04010e08u, 0x8f6a8708u, 0xd76d0009u, 0x0401806au, 0xe00c2000u, 0x80000008u, 0xe00c2000u, 0x80000409u, 0xbf8c3f71u, 0xe01c2000u, 0x80010008u, 0xbf8c3f70u, 0xe01c2000u, 0x80010409u, 0xbf810000u}; + if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return false; + const auto stride = [&](std::size_t word) { return (userData[word] >> 16u) & 0x3fffu; }; + const auto swizzled = [&](std::size_t word) { return ((userData[word] >> 31u) & 1u) != 0; }; + const auto dstSel = userData[3] & 0xfffu; + const bool addTid = ((userData[3] >> 23u) & 1u) != 0; + const auto outOfBounds = (userData[3] >> 28u) & 3u; + const auto type = userData[3] >> 30u; + const auto format = (userData[3] >> 12u) & 0x7fu; + return stride(1) == 16 && stride(5) == 16 && !swizzled(1) && !swizzled(5) && userData[3] == userData[7] && type == 0 && !addTid && outOfBounds == 0 && dstSel == 0xfacu && format == 0x4bu; +} + +std::optional StreamCopyRange(const std::vector& userData, std::uint32_t groups) { + const auto base = [&](std::size_t word) { return userData[word] | (static_cast(userData[word + 1] & 0xffffu) << 32u); }; + const auto elements = std::min(std::uint64_t{groups} * 128u, userData[6]); + const StreamCopy copy{base(0), base(4), static_cast(elements) * 16u}; + if (copy.bytes == 0 || copy.bytes > (16u << 20u) || userData[2] < elements || copy.source % 16 != 0 || copy.destination % 16 != 0) return std::nullopt; + return copy; +} + void Driver::countCopy(int path, std::size_t bytes, std::chrono::steady_clock::time_point started, std::chrono::steady_clock::time_point locked, const VulkanDevice::CopyOutcome* outcome) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - if (!profile || path < 0 || path >= CopyPaths) return; + if (path < 0 || path >= CopyPaths) return; + copyPathCounts[path].fetch_add(1, std::memory_order_relaxed); + if (!profile) return; using Outcome = VulkanDevice::CopyOutcome; static std::mutex countsMutex; static std::uint64_t counts[CopyPaths] = {}; @@ -92,7 +119,8 @@ bool Driver::knownValueVerify() { } bool Driver::copyBuffer(QueueState& queue, std::uint32_t queueId, std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::shared_ptr& localDevice, std::uint64_t programAddress) { - if (!matchesCopyKernel(code, userData, compute)) return false; + const bool stream = !matchesCopyKernel(code, userData, compute); + if (stream && !MatchesStreamCopyKernel(code, userData, compute)) return false; const auto started = std::chrono::steady_clock::now(); static const std::size_t cpuMax = [] { if (std::getenv("APS5_NO_CPU_COPY") != nullptr) return std::size_t{0}; @@ -106,7 +134,7 @@ bool Driver::copyBuffer(QueueState& queue, std::uint32_t queueId, std::span(userData[word + 1] & 0xffffu) << 32u); }; const auto source = base(0); const auto destination = base(4); - const auto record = base(8); + const auto record = stream ? std::uint64_t{0} : base(8); std::array groups{packet[1], packet[2], packet[3]}; if ((packet[4] & 0x20u) != 0) { for (std::uint32_t axis = 0; axis < 3; ++axis) { @@ -132,22 +160,32 @@ bool Driver::copyBuffer(QueueState& queue, std::uint32_t queueId, std::span(record), 8); + std::size_t bytes = 0; + if (stream) { + const auto range = StreamCopyRange(userData, groups[0]); + if (!range) { + countCopy(CopyShape, 0, started, locked); + return false; + } + bytes = range->bytes; + } else { + const bool recordAccessible = GuestMemory::Accessible(reinterpret_cast(record), 8); - bool published = false; - if (!recordAccessible || Graphics::Recorder::SnapshotWriteOverlaps(record, 8) || Graphics::StorageTexture::FlushPending(record, 8, nullptr, "buffer copy count", Graphics::PublishScope::Whole, &published) || published || labelPending(record, 8)) { - if (traceCopy()) traceCopyPending(recordAccessible ? "count record" : "inaccessible count record", record, 8, source, destination, 0, localDevice); - countCopy(CopyRecordPending, 0, started, locked); - return false; - } - std::uint32_t count = 0, period = 0; - std::memcpy(&count, reinterpret_cast(record), 4); - std::memcpy(&period, reinterpret_cast(record + 4), 4); - const auto bytes = static_cast(count) * 4u; + bool published = false; + if (!recordAccessible || Graphics::Recorder::SnapshotWriteOverlaps(record, 8) || Graphics::StorageTexture::FlushPending(record, 8, nullptr, "buffer copy count", Graphics::PublishScope::Whole, &published) || published || labelPending(record, 8)) { + if (traceCopy()) traceCopyPending(recordAccessible ? "count record" : "inaccessible count record", record, 8, source, destination, 0, localDevice); + countCopy(CopyRecordPending, 0, started, locked); + return false; + } + std::uint32_t count = 0, period = 0; + std::memcpy(&count, reinterpret_cast(record), 4); + std::memcpy(&period, reinterpret_cast(record + 4), 4); + bytes = static_cast(count) * 4u; - if (count == 0 || period < count || groups[0] != (count + 63u) / 64u || count > userData[2] || count > userData[6] || bytes > (16u << 20u) || source % 4 != 0 || destination % 4 != 0 || (record < destination + bytes && destination < record + 8)) { - countCopy(CopyShape, bytes, started, locked); - return false; + if (count == 0 || period < count || groups[0] != (count + 63u) / 64u || count > userData[2] || count > userData[6] || bytes > (16u << 20u) || source % 4 != 0 || destination % 4 != 0 || (record < destination + bytes && destination < record + 8)) { + countCopy(CopyShape, bytes, started, locked); + return false; + } } if (source < destination + bytes && destination < source + bytes) { countCopy(CopyOverlapping, bytes, started, locked); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp index 62c6153a13..e410cb88d3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp @@ -3,12 +3,14 @@ #include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include #include +#include #include #include @@ -21,6 +23,27 @@ bool matchesBufferPatternFillKernel(std::span code) { return code.size() >= kernel.size() && std::equal(kernel.begin(), kernel.end(), code.begin()); } +bool matchesDwordPatternFillKernel(std::span code) { + static constexpr std::array kernel{ + 0xbfa00003u, 0xd7460002u, 0x04010c0au, 0x7da80408u, 0xbf88003fu, 0x7e000c09u, 0xbf070980u, 0x858a807eu, + 0x7e005700u, 0x100000ffu, 0x4f800000u, 0x7e060f00u, 0xd5766a00u, 0x02020609u, 0x7d8a0280u, 0x4c020080u, + 0x02000101u, 0xd56a0001u, 0x00020700u, 0x4c000303u, 0x4a020303u, 0x02000101u, 0xd56a0000u, 0x00020500u, + 0xd5690001u, 0x00020009u, 0x4c060302u, 0x7d860609u, 0x7d8c02f9u, 0x06068c02u, 0x87ea6a0cu, 0x50000080u, + 0xd5286a00u, 0x003200c1u, 0xd5010000u, 0x002a00c1u, 0xd5690000u, 0x00020009u, 0x4c000102u, 0x7d0a0080u, + 0xbe88246au, 0xbf880015u, 0x7d0a0081u, 0xbe8a246au, 0xbf88000cu, 0x7d0a0082u, 0xbeea246au, 0xbf880003u, + 0x7e000207u, 0xe0102000u, 0x80000002u, 0x8afe7e6au, 0xbf880003u, 0x7e000206u, 0xe0102000u, 0x80000002u, + 0xbefe046au, 0x8afe7e0au, 0xbf880003u, 0x7e000205u, 0xe0102000u, 0x80000002u, 0xbefe040au, 0x8afe7e08u, + 0xbf880003u, 0x7e000204u, 0xe0102000u, 0x80000002u, 0xbf810000u}; + static constexpr std::size_t comparesAt = 27; + static constexpr std::array comparesSwapped{0x7d8c02f9u, 0x06068c02u, 0x7d860609u}; + if (code.size() < kernel.size()) return false; + const auto before = std::span(kernel).first(comparesAt); + const auto after = std::span(kernel).subspan(comparesAt + comparesSwapped.size()); + const auto compares = code.subspan(comparesAt, comparesSwapped.size()); + if (!std::equal(before.begin(), before.end(), code.begin()) || !std::equal(after.begin(), after.end(), code.begin() + comparesAt + comparesSwapped.size())) return false; + return std::equal(compares.begin(), compares.end(), kernel.begin() + comparesAt) || std::equal(compares.begin(), compares.end(), comparesSwapped.begin()); +} + } bool Driver::matchesFillKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { @@ -43,17 +66,7 @@ bool Driver::matchesFillKernel(std::span code, const std::v std::optional MatchDwordPatternFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr; if (!enabled || userData.size() != 10 || packet.size() < 5 || !compute.groupIdEnable[0] || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return std::nullopt; - static constexpr std::array kernel{ - 0xbfa00003u, 0xd7460002u, 0x04010c0au, 0x7da80408u, 0xbf88003fu, 0x7e000c09u, 0xbf070980u, 0x858a807eu, - 0x7e005700u, 0x100000ffu, 0x4f800000u, 0x7e060f00u, 0xd5766a00u, 0x02020609u, 0x7d8a0280u, 0x4c020080u, - 0x02000101u, 0xd56a0001u, 0x00020700u, 0x4c000303u, 0x4a020303u, 0x02000101u, 0xd56a0000u, 0x00020500u, - 0xd5690001u, 0x00020009u, 0x4c060302u, 0x7d860609u, 0x7d8c02f9u, 0x06068c02u, 0x87ea6a0cu, 0x50000080u, - 0xd5286a00u, 0x003200c1u, 0xd5010000u, 0x002a00c1u, 0xd5690000u, 0x00020009u, 0x4c000102u, 0x7d0a0080u, - 0xbe88246au, 0xbf880015u, 0x7d0a0081u, 0xbe8a246au, 0xbf88000cu, 0x7d0a0082u, 0xbeea246au, 0xbf880003u, - 0x7e000207u, 0xe0102000u, 0x80000002u, 0x8afe7e6au, 0xbf880003u, 0x7e000206u, 0xe0102000u, 0x80000002u, - 0xbefe046au, 0x8afe7e0au, 0xbf880003u, 0x7e000205u, 0xe0102000u, 0x80000002u, 0xbefe040au, 0x8afe7e08u, - 0xbf880003u, 0x7e000204u, 0xe0102000u, 0x80000002u, 0xbf810000u}; - if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt; + if (!matchesDwordPatternFillKernel(code)) return std::nullopt; const auto stride = (userData[1] >> 16u) & 0x3fffu; const bool swizzled = ((userData[1] >> 31u) & 1u) != 0; const bool addTid = ((userData[3] >> 23u) & 1u) != 0; @@ -69,11 +82,19 @@ std::optional MatchDwordPatternFill(std::span MatchZeroDwordFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { +std::optional MatchConstantDwordFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr; if (!enabled || userData.size() != 4 || packet.size() < 5 || !compute.groupIdEnable[0] || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return std::nullopt; - static constexpr std::array kernel{0xd7460000u, 0x04010c04u, 0x7e020280u, 0xe0102000u, 0x80000100u, 0xbf810000u}; - if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt; + static constexpr std::array kernel{0xd7460000u, 0x04010c04u, 0x7e020200u, 0xe0102000u, 0x80000100u, 0xbf810000u}; + if (code.size() < kernel.size()) return std::nullopt; + for (std::size_t word = 0; word < kernel.size(); ++word) { + if ((word == 2 ? code[word] & ~0x1ffu : code[word]) != kernel[word]) return std::nullopt; + } + const auto source = code[2] & 0x1ffu; + std::uint32_t value; + if (source >= 0x80u && source <= 0xc0u) value = source - 0x80u; + else if (source >= 0xc1u && source <= 0xd0u) value = 0u - (source - 0xc0u); + else return std::nullopt; const auto stride = (userData[1] >> 16u) & 0x3fffu; const bool swizzled = ((userData[1] >> 31u) & 1u) != 0; const bool addTid = ((userData[3] >> 23u) & 1u) != 0; @@ -84,7 +105,24 @@ std::optional MatchZeroDwordFill(std::span(static_cast(packet[1]) * 64u, userData[2]); const auto base = userData[0] | (static_cast(userData[1] & 0xffffu) << 32u); if (dwords == 0 || dwords % 4u != 0 || base % 16u != 0) return std::nullopt; - return DwordPatternFill{base, static_cast(dwords * 4u), {}}; + DwordPatternFill fill{base, static_cast(dwords * 4u), {}}; + fill.pattern.fill(value); + return fill; +} + +std::optional MatchClampedGlobalFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { + static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr; + if (!enabled || userData.size() != 6 || packet.size() < 5 || !compute.groupIdEnable[0] || compute.numThreads != std::array{64, 1, 1} || compute.threadIdComponentCount != 1) return std::nullopt; + static constexpr std::array kernel{0xd7460000u, 0x04010c06u, 0x7e080205u, 0x7e0a0205u, 0x7e0c0205u, 0x7e0e0205u, 0x34000084u, 0x26000004u, 0xdc788000u, 0x00020400u, 0xbf810000u}; + if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt; + if ((packet[4] & 0x20u) != 0 || packet[2] != 1 || packet[3] != 1) return std::nullopt; + const auto base = userData[2] | (static_cast(userData[3]) << 32u); + const auto maxOffset = userData[4]; + const auto bytes = std::min(static_cast(packet[1]) * 64u * 16u, static_cast(maxOffset) + 16u); + if (bytes == 0 || base % 16u != 0 || maxOffset % 16u != 0) return std::nullopt; + DwordPatternFill fill{base, static_cast(bytes), {}}; + fill.pattern.fill(userData[5]); + return fill; } std::optional MatchMemoryValueBufferFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { @@ -113,6 +151,26 @@ std::optional KeysFillMemo(const std::array } } +std::optional MatchSgprPatternFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { + if (userData.size() != 9 || packet.size() < 5 || !compute.groupIdEnable[0] || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return std::nullopt; + static constexpr std::array kernel{ + 0xbfa00001u, 0xd7460004u, 0x04010c09u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0x7da80808u, + 0xbf88000du, 0x360a0883u, 0xbe84047eu, 0xbe86047eu, 0x7e100505u, 0x7da40a08u, 0xbefc0308u, 0x7e0c8700u, + 0x8a867e06u, 0xbefe0406u, 0xbf85fff9u, 0xbefe0404u, 0xe0102000u, 0x80000604u, 0xbf810000u}; + if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt; + const auto stride = (userData[1] >> 16u) & 0x3fffu; + const bool swizzled = ((userData[1] >> 31u) & 1u) != 0; + const bool addTid = ((userData[3] >> 23u) & 1u) != 0; + const auto outOfBounds = (userData[3] >> 28u) & 3u; + const auto format = (userData[3] >> 12u) & 0x7fu; + const auto type = userData[3] >> 30u; + if (type != 0 || stride != 4 || swizzled || addTid || outOfBounds > 1u || format < 20u || format > 22u || (packet[4] & 0x20u) != 0 || packet[2] != 1 || packet[3] != 1) return std::nullopt; + const auto dwords = std::min({static_cast(packet[1]) * 64u, userData[8], userData[2]}); + if (dwords == 0) return std::nullopt; + const auto base = userData[0] | (static_cast(userData[1] & 0xffffu) << 32u); + return DwordPatternFill{base, static_cast(dwords * 4u), {userData[4], userData[5], userData[6], userData[7]}}; +} + bool Driver::fillClearEnabled() { static const bool enabled = std::getenv("APS5_NO_FILL_CLEAR") == nullptr; return enabled; @@ -177,9 +235,24 @@ void Driver::fillClearCount(const Graphics::StorageTexture::FillCoverage& covera std::fprintf(stderr, "%s\n", line.c_str()); } +namespace { + +void drainPendingInputWrites(std::uint32_t queueId, std::initializer_list> inputs) { + if (queueId != 0 || !DrawPipeline::Active()) return; + auto& pipeline = DrawPipeline::Queue0(); + for (const auto& [address, bytes] : inputs) { + if (!pipeline.Overlaps(address, bytes)) continue; + pipeline.Drain(DrawPipeline::DrainReason::Capture, 0x15); + return; + } +} + +} + bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::shared_ptr& localDevice) { auto dwordFill = MatchDwordPatternFill(packet, code, userData, compute); - if (!dwordFill) dwordFill = MatchZeroDwordFill(packet, code, userData, compute); + if (!dwordFill) dwordFill = MatchConstantDwordFill(packet, code, userData, compute); + if (!dwordFill) dwordFill = MatchClampedGlobalFill(packet, code, userData, compute); std::optional memoryFill; if (!dwordFill) memoryFill = MatchMemoryValueBufferFill(packet, code, userData, compute); if (!dwordFill && !memoryFill && !matchesFillKernel(code, userData, compute)) return false; @@ -192,6 +265,7 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::spanbytes; pattern = dwordFill->pattern; } else if (memoryFill) { + drainPendingInputWrites(queueId, {{memoryFill->valueAddress, 4}}); GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill); inputLock.lock(); recordLabelsForPacket(localDevice.get(), queueId); @@ -204,6 +278,7 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::spaninvocations == 0) return true; + drainPendingInputWrites(queueId, {{range->control, 8}, {range->source, 4}}); GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill); inputLock.lock(); recordLabelsForPacket(localDevice.get(), queueId); @@ -233,6 +308,7 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span(records * 16u); if (matchesBufferPatternFillKernel(code)) { const auto source = userData[4] | (static_cast(userData[5] & 0xffffu) << 32u); + drainPendingInputWrites(queueId, {{source, sizeof(pattern)}}); GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill); inputLock.lock(); recordLabelsForPacket(localDevice.get(), queueId); @@ -242,6 +318,13 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span& pattern, const std::shared_ptr& localDevice, bool memoryValueFill, bool noteMetadata) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; static std::atomic fills{0}, filledBytes{0}, cpuFills{0}; @@ -256,7 +339,8 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span(base), bytes, 16, true); - phase(FillCheck); - const auto coverage = Graphics::StorageTexture::ClassifyFill(base, bytes); - phase(FillClassify); + recordLabelsForPacket(localDevice.get(), queueId); + phase(FillLabels); + GuestMemory::CheckRange(reinterpret_cast(base), bytes, 16, true); + phase(FillCheck); + const auto coverage = Graphics::StorageTexture::ClassifyFill(base, bytes); + phase(FillClassify); - static const bool traceKeys = std::getenv("APS5_TRACE_DCC_KEYS") != nullptr; - if (traceKeys && coverage.cover == Graphics::StorageTexture::FillCover::Keys) std::fprintf(stderr, "[dcc-keys] title fills keys 0x%llx+0x%zx with %08x %08x %08x %08x (queue 0x%x)\n", static_cast(base), bytes, pattern[0], pattern[1], pattern[2], pattern[3], queueId); - const bool uniformKeysFill = coverage.cover == Graphics::StorageTexture::FillCover::Keys && IsByteFill(pattern); - if (uniformKeysFill) { - Graphics::StorageTexture::NoteKeysFill(base, bytes, static_cast(pattern[0])); - Graphics::StorageTexture::ClearByKeysFill(base, bytes, static_cast(pattern[0])); - } + static const bool traceKeys = std::getenv("APS5_TRACE_DCC_KEYS") != nullptr; + if (traceKeys && coverage.cover == Graphics::StorageTexture::FillCover::Keys) std::fprintf(stderr, "[dcc-keys] title fills keys 0x%llx+0x%zx with %08x %08x %08x %08x (queue 0x%x)\n", static_cast(base), bytes, pattern[0], pattern[1], pattern[2], pattern[3], queueId); + const bool uniformKeysFill = coverage.cover == Graphics::StorageTexture::FillCover::Keys && IsByteFill(pattern); + if (uniformKeysFill) { + Graphics::StorageTexture::NoteKeysFill(base, bytes, static_cast(pattern[0])); + Graphics::StorageTexture::ClearByKeysFill(base, bytes, static_cast(pattern[0])); + } - const auto discarded = fillClearEnabled() ? Graphics::StorageTexture::DiscardPendingInside(base, bytes) : 0u; - phase(FillDiscard); - const char* refusal = nullptr; - bool cleared = false; - if (coverage.image != nullptr) { - if (!fillClearEnabled()) { - refusal = "disabled"; - } else if (fillClearExactOnly() && (coverage.cover != Graphics::StorageTexture::FillCover::Exact || coverage.others != 0)) { - refusal = "exact only"; - } else { + const auto discarded = fillClearEnabled() ? Graphics::StorageTexture::DiscardPendingInside(base, bytes) : 0u; + phase(FillDiscard); + const char* refusal = nullptr; + bool cleared = false; + if (coverage.image != nullptr) { + if (!fillClearEnabled()) { + refusal = "disabled"; + } else if (fillClearExactOnly() && (coverage.cover != Graphics::StorageTexture::FillCover::Exact || coverage.others != 0)) { + refusal = "exact only"; + } else { - Graphics::StorageTexture::FlushPending(base, bytes, coverage.image.get(), "buffer fill", Graphics::PublishScope::PartialUnits); - cleared = coverage.image->FillClear(pattern, coverage.layer, refusal); - } - } - phase(FillClearPhase); - fillClearCount(coverage, bytes, pattern, discarded, cleared, refusal); - noteForeignWriter(base, base + bytes, queueId); - - const bool uniform = pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3]; - const bool absorbed = !cleared && uniform && coverage.cover == Graphics::StorageTexture::FillCover::None && Graphics::MultisampledColorFillAbsorbs(base, bytes, pattern[0]); - if (absorbed) { - ++absorbedFills; - absorbedBytes += bytes; - } else if (!cleared) { - ++flushes; - - if (Graphics::StorageTexture::FlushPending(base, bytes, nullptr, "buffer fill", Graphics::PublishScope::PartialUnits)) ++flushed; - } - phase(FillFlush); - const bool stored = cleared || absorbed || localDevice->FillBuffer(base, bytes, pattern); - phase(FillDevice); - if (stored && !cleared && !absorbed) { - if (const auto filled = KeysFillMemo(pattern, uniformKeysFill, memoryFill.has_value())) Graphics::NoteKeysFillOnGpu(base, bytes, *filled); - } - if (profile && stored && !cleared && !absorbed) { - ++(uniform ? uniformFills : patternFills); - (uniform ? uniformBytes : patternBytes) += bytes; - if (!uniform) chainCopies += static_cast(std::bit_width((bytes / 16) - 1)); - } - if (!stored) { - ++cpuFills; - - localDevice->WaitIdle(); - static thread_local std::vector* blockSlot = nullptr; - auto& block = ShaderRecompiler::ThreadOwned(blockSlot); - const auto chunk = std::min(bytes, 1u << 20u); - block.resize(chunk); - for (std::size_t at = 0; at < chunk; at += 16) std::memcpy(block.data() + at, pattern.data(), 16); - for (std::size_t done = 0; done < bytes; done += chunk) GuestMemory::Write(base + done, std::span(block).first(std::min(chunk, bytes - done)), 16); - phase(FillCpu); - } - if (uniform) { - Graphics::NoteDepthMetadataFill(base, bytes, pattern[0]); - Graphics::NoteColorMetadataFill(base, bytes, pattern[0]); - } - if (profile) { - const auto now = std::chrono::steady_clock::now(); - const auto cover = static_cast(coverage.cover); - ++holdByCover[cover]; - holdByCoverMs[cover] += std::chrono::duration(now - holdStart).count(); - if (now - fillReport > std::chrono::seconds(10)) { - fillReport = now; - static constexpr std::array coverNames{"none", "exact", "inside", "around", "straddle", "several", "keys", "layer"}; - std::string line; - char text[64]; - for (std::size_t i = 0; i < FillPhaseCount; ++i) { - std::snprintf(text, sizeof(text), " %s %.0f", fillPhaseNames[i], fillPhaseMs[i]); - line += text; - } - char absorbedText[128]; - std::snprintf(absorbedText, sizeof(absorbedText), "; multisampled clears not stored %llu (%.0f MiB)", static_cast(absorbedFills), static_cast(absorbedBytes) / 1048576.0); - line += absorbedText; - line += "; hold by cover (count/ms):"; - for (std::size_t i = 0; i < coverNames.size(); ++i) { - std::snprintf(text, sizeof(text), " %s %llu/%.0f", coverNames[i], static_cast(holdByCover[i]), holdByCoverMs[i]); - line += text; - } - std::fprintf(stderr, "[fill] %llu buffer fills (%.0f MiB), %llu stored by the CPU; hold phases ms (cumulative):%s; pre-store flushes %llu (%llu stored an image); stored on the GPU: uniform %llu (%.0f MiB), pattern %llu (%.0f MiB), chain copies %llu\n", static_cast(fills.load()), filledBytes.load() / 1048576.0, static_cast(cpuFills.load()), line.c_str(), static_cast(flushes), static_cast(flushed), static_cast(uniformFills), uniformBytes / 1048576.0, static_cast(patternFills), patternBytes / 1048576.0, static_cast(chainCopies)); - } + Graphics::StorageTexture::FlushPending(base, bytes, coverage.image.get(), "buffer fill", Graphics::PublishScope::PartialUnits); + cleared = coverage.image->FillClear(pattern, coverage.layer, refusal); } } - return true; -} + phase(FillClearPhase); + fillClearCount(coverage, bytes, pattern, discarded, cleared, refusal); + noteForeignWriter(base, base + bytes, queueId); + const bool uniform = pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3]; + const bool absorbed = !cleared && uniform && coverage.cover == Graphics::StorageTexture::FillCover::None && Graphics::MultisampledColorFillAbsorbs(base, bytes, pattern[0]); + if (absorbed) { + ++absorbedFills; + absorbedBytes += bytes; + } else if (!cleared) { + ++flushes; + + if (Graphics::StorageTexture::FlushPending(base, bytes, nullptr, "buffer fill", Graphics::PublishScope::PartialUnits)) ++flushed; + } + phase(FillFlush); + const bool stored = cleared || absorbed || localDevice->FillBuffer(base, bytes, pattern); + phase(FillDevice); + if (stored && !cleared && !absorbed) { + if (const auto filled = KeysFillMemo(pattern, uniformKeysFill, memoryValueFill)) Graphics::NoteKeysFillOnGpu(base, bytes, *filled); + } + if (profile && stored && !cleared && !absorbed) { + ++(uniform ? uniformFills : patternFills); + (uniform ? uniformBytes : patternBytes) += bytes; + if (!uniform) chainCopies += static_cast(std::bit_width((bytes / 16) - 1)); + } + if (!stored) { + ++cpuFills; + + localDevice->WaitIdle(); + static thread_local std::vector* blockSlot = nullptr; + auto& block = ShaderRecompiler::ThreadOwned(blockSlot); + const auto chunk = std::min(bytes, 1u << 20u); + block.resize(chunk); + for (std::size_t at = 0; at < chunk; at += 16) std::memcpy(block.data() + at, pattern.data(), 16); + for (std::size_t done = 0; done < bytes; done += chunk) GuestMemory::Write(base + done, std::span(block).first(std::min(chunk, bytes - done)), 16); + phase(FillCpu); + } + if (noteMetadata && uniform) { + Graphics::NoteDepthMetadataFill(base, bytes, pattern[0]); + Graphics::NoteColorMetadataFill(base, bytes, pattern[0]); + } + if (profile) { + const auto now = std::chrono::steady_clock::now(); + const auto cover = static_cast(coverage.cover); + ++holdByCover[cover]; + holdByCoverMs[cover] += std::chrono::duration(now - holdStart).count(); + if (now - fillReport > std::chrono::seconds(10)) { + fillReport = now; + static constexpr std::array coverNames{"none", "exact", "inside", "around", "straddle", "several", "keys", "layer"}; + std::string line; + char text[64]; + for (std::size_t i = 0; i < FillPhaseCount; ++i) { + std::snprintf(text, sizeof(text), " %s %.0f", fillPhaseNames[i], fillPhaseMs[i]); + line += text; + } + char absorbedText[128]; + std::snprintf(absorbedText, sizeof(absorbedText), "; multisampled clears not stored %llu (%.0f MiB)", static_cast(absorbedFills), static_cast(absorbedBytes) / 1048576.0); + line += absorbedText; + line += "; hold by cover (count/ms):"; + for (std::size_t i = 0; i < coverNames.size(); ++i) { + std::snprintf(text, sizeof(text), " %s %llu/%.0f", coverNames[i], static_cast(holdByCover[i]), holdByCoverMs[i]); + line += text; + } + std::fprintf(stderr, "[fill] %llu buffer fills (%.0f MiB), %llu stored by the CPU; hold phases ms (cumulative):%s; pre-store flushes %llu (%llu stored an image); stored on the GPU: uniform %llu (%.0f MiB), pattern %llu (%.0f MiB), chain copies %llu\n", static_cast(fills.load()), filledBytes.load() / 1048576.0, static_cast(cpuFills.load()), line.c_str(), static_cast(flushes), static_cast(flushed), static_cast(uniformFills), uniformBytes / 1048576.0, static_cast(patternFills), patternBytes / 1048576.0, static_cast(chainCopies)); + } + } + }; + static const bool orderedFills = std::getenv("APS5_PIPELINE_DRAIN_FILLS") == nullptr; + if (orderedFills && queueId == 0 && DrawPipeline::Active()) { + DrawPipeline::Queue0().Enqueue([store, queueId] { + GuestMemory::SetCurrentPacket(0x15, queueId); + store(); + }, {{base, base + bytes}}); + return; + } + store(); +} } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/CaptureValidation.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/CaptureValidation.cpp index 2eb194129d..46d3c66047 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/CaptureValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/CaptureValidation.cpp @@ -57,13 +57,23 @@ bool Driver::captureStable(std::span captu return stable; } -bool Driver::validateCaptured(std::uint64_t program, std::uint32_t queue, std::span captured, const ShaderRecompiler::RecompileResult& compiled, bool inPlace, const PendingView& view, bool* unmapped, std::optional* sampling, const DataMask* data) { +bool Driver::validateCaptured(std::uint64_t program, std::uint32_t queue, std::span captured, const ShaderRecompiler::RecompileResult& compiled, bool inPlace, const PendingView& view, bool* unmapped, std::optional* sampling, const DataMask* data, std::span vouched) { using Policy = ShaderMemory::PendingWrite; thread_local std::vector* pendingSlot = nullptr; auto& pending = ShaderRecompiler::ThreadOwned(pendingSlot); pending.clear(); - for (std::size_t i = 0; i < captured.size(); ++i) { + thread_local std::vector* skippedSlot = nullptr; + auto& skipped = ShaderRecompiler::ThreadOwned(skippedSlot); + skipped.clear(); + for (const auto& range : vouched) { + if (range.begin < range.end && range.end <= captured.size() && (skipped.empty() || skipped.back().end <= range.begin) && !view.Overlaps(range.low, static_cast(range.high - range.low))) skipped.push_back(range); + } + for (std::size_t i = 0, range = 0; i < captured.size(); ++i) { + if (range < skipped.size() && i == skipped[range].begin) { + i = skipped[range++].end - 1u; + continue; + } const auto& region = captured[i]; if (view.Overlaps(region.guestAddress, region.bytes.size())) pending.push_back(i); } @@ -107,7 +117,12 @@ bool Driver::validateCaptured(std::uint64_t program, std::uint32_t queue, std::s observable.clear(); reachedPending = 0; if (data != nullptr) data->live->clear(); - for (std::size_t i = 0; i < captured.size(); ++i) { + for (std::size_t i = 0, range = 0; i < captured.size(); ++i) { + if (range < skipped.size() && i == skipped[range].begin) { + first += skipped[range].words; + i = skipped[range++].end - 1u; + continue; + } const auto& region = captured[i]; const bool isPending = next < pending.size() && pending[next] == i; if (isPending) ++next; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp index bf0f118381..596daf0146 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/WriteEvidence.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include @@ -204,6 +205,21 @@ ShaderMemory::PendingWrite Driver::classifyPendingWrite(std::uint64_t address, s } ShaderMemory::PendingWrite Driver::queryPendingWrite(std::uint64_t address, std::size_t bytes, std::span known) { + if (DrawPipeline::Active()) { + auto& pipeline = DrawPipeline::Queue0(); + if (pipeline.Busy() && pipeline.Overlaps(address, bytes)) pipeline.Drain(DrawPipeline::DrainReason::Capture); + } + std::uint64_t ValidateCounters::*reason = nullptr; + PendingView pending; + pending.Load(); + return Get().classifyPendingWrite(address, bytes, reason, pending, known); +} + +ShaderMemory::PendingWrite Driver::QueryPendingSpan(std::uint64_t address, std::size_t bytes, std::span known) { + if (DrawPipeline::Active()) { + auto& pipeline = DrawPipeline::Queue0(); + if (pipeline.Busy() && pipeline.Overlaps(address, bytes)) return ShaderMemory::PendingWrite::Sync; + } std::uint64_t ValidateCounters::*reason = nullptr; PendingView pending; pending.Load(); 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 682b28dca2..58fa8648a3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp @@ -1,10 +1,23 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "Optimization/ShaderStageInputInfo.hpp" +#include +#include +#include +#include +#include namespace AgcDriver::DriverDetail { +namespace { + template -void Driver::forEachWrittenBuffer(const ShaderRecompiler::RecompileResult& compiled, TVisit&& visit) { +void visitWrittenBuffers(const ShaderRecompiler::RecompileResult& compiled, TVisit&& visit) { for (const auto& binding : compiled.bindings) { if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; for (std::uint32_t element = 0; element < binding.count; ++element) { @@ -20,6 +33,13 @@ void Driver::forEachWrittenBuffer(const ShaderRecompiler::RecompileResult& compi } } +} + +template +void Driver::forEachWrittenBuffer(const ShaderRecompiler::RecompileResult& compiled, TVisit&& visit) { + visitWrittenBuffers(compiled, std::forward(visit)); +} + void Driver::appendWrittenRanges(const ShaderRecompiler::RecompileResult& compiled, std::vector>& ranges) { forEachWrittenBuffer(compiled, [&](std::uint32_t, std::uint64_t begin, std::uint64_t end, bool) { ranges.emplace_back(begin, end); }); } @@ -33,6 +53,32 @@ 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 words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8, 8); + if (Graphics::IsNullTextureResource(words)) continue; + const auto resource = Graphics::DecodeTextureResource(words); + const auto surfaceBytes = Graphics::DescribeSurface(resource).guestBytes; + ranges.emplace_back(resource.baseAddress, resource.baseAddress + (surfaceBytes != 0 ? surfaceBytes : UnknownImageBytes)); + if (resource.dccAddress != 0) ranges.emplace_back(resource.dccAddress, resource.dccAddress + (surfaceBytes != 0 ? surfaceBytes / 256 + 1 : UnknownImageBytes)); + } + } + 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); @@ -46,6 +92,109 @@ void Driver::noteDrawWriters(std::span stages, s } } +bool ExactDrawWrites() { + static const bool exact = [] { const char* text = std::getenv("APS5_EXACT_DRAW_WRITES"); return text == nullptr || std::strcmp(text, "0") != 0; }(); + return exact; +} + +std::vector> DrawWriteRanges(const Graphics::State& graphics, std::span stages, bool exact) { + constexpr std::uint64_t Slack = 64 * 1024; + constexpr std::uint64_t UnknownBytes = 64ull * 1024 * 1024; + std::vector> ranges; + const auto add = [&](std::uint64_t begin, std::uint64_t bytes) { + if (begin != 0) ranges.emplace_back(begin, begin + bytes); + }; + for (const auto& stage : stages) { + if (stage.program == nullptr) continue; + visitWrittenBuffers(*stage.program, [&](std::uint32_t, std::uint64_t begin, std::uint64_t end, bool) { ranges.emplace_back(begin, end); }); + for (const auto& binding : stage.program->bindings) { + 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 || binding.guestDescriptor.size() < (static_cast(element) + 1) * 8) continue; + const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8, 8); + if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + const auto resource = Graphics::DecodeTextureResource(words); + if (resource.baseAddress == 0) continue; + const auto surfaceBytes = Graphics::DescribeSurface(resource).guestBytes; + add(resource.baseAddress, surfaceBytes != 0 ? surfaceBytes : UnknownBytes); + add(resource.dccAddress, surfaceBytes != 0 ? surfaceBytes / 256 + 1 : UnknownBytes); + } + } + } + const auto addColor = [&](const Graphics::ColorTarget& color) { + if (exact && color.bytes != 0) { + const auto slices = static_cast(std::max(color.depth, 1)); + const auto bytes = static_cast(color.bytes) * slices; + add(color.address, bytes); + add(color.dccAddress, (bytes + 255) / 256); + add(color.cmaskAddress, static_cast(color.cmaskBytes) * slices); + return; + } + const std::uint64_t bytes = color.bytes != 0 ? static_cast(color.bytes) : UnknownBytes; + const auto base = color.surfaceAddress != 0 ? std::min(color.surfaceAddress, color.address) : color.address; + if (base != 0) ranges.emplace_back(base, color.address + 2 * bytes + Slack); + add(color.dccAddress, bytes / 16 + Slack); + add(color.cmaskAddress, static_cast(color.cmaskBytes) * std::max(color.depth, 1)); + }; + if (graphics.hasColorTarget) addColor(graphics.color); + for (const auto& color : graphics.colors) addColor(color); + if (graphics.depth) { + const auto& depth = *graphics.depth; + if (exact && depth.extent.width != 0 && depth.extent.height != 0) { + const bool d16 = depth.format == VK_FORMAT_D16_UNORM || depth.format == VK_FORMAT_D16_UNORM_S8_UINT; + add(depth.address, Graphics::DepthSliceBytes(depth.extent, d16 ? 2u : 4u)); + add(depth.stencilAddress, Graphics::DepthSliceBytes(depth.extent, 1u)); + } else { + const auto align = [](std::uint64_t value) { return (value + 255) & ~255ull; }; + const auto pixels = align(depth.extent.width) * align(depth.extent.height); + const auto bytes = pixels != 0 ? pixels * 8 + Slack : UnknownBytes; + add(depth.address, bytes); + add(depth.stencilAddress, bytes); + } + } + return ranges; +} + +std::vector> Driver::drawWriteRanges(const Graphics::State& graphics, std::span stages) { + return DrawWriteRanges(graphics, stages, ExactDrawWrites()); +} + +std::vector> QueuedDrawReadRanges(const Pm4::DrawParameters& draw, std::span stages) { + std::vector> ranges; + const auto add = [&](std::uint64_t begin, std::uint64_t bytes) { + if (begin == 0 || bytes == 0 || bytes > std::numeric_limits::max() - begin) return; + ranges.emplace_back(begin, begin + bytes); + }; + if (draw.indexed && draw.indexSize != 0 && draw.indexCount != 0) add(draw.indexAddress, static_cast(draw.indexCount) * draw.indexSize); + if (stages.empty()) return ranges; + for (const auto& stage : stages) { + if (stage.program == nullptr) continue; + for (const auto& binding : stage.program->bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers || binding.guestDescriptor.size() < static_cast(binding.count) * 4u) continue; + for (std::uint32_t element = 0; element < binding.count; ++element) { + const auto* words = binding.guestDescriptor.data() + static_cast(element) * 4u; + const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; + add(descriptor.Base48(), descriptor.GetSize()); + } + } + } + if (stages.empty() || stages.front().program == nullptr) return ranges; + for (const auto& attribute : stages.front().program->vertexAttributes) { + if (Graphics::NullVertexDescriptor(attribute)) continue; + const auto address = attribute.resource.fields[0] | (static_cast(attribute.resource.fields[1] & 0xffffu) << 32u); + std::uint64_t bytes = 0; + try { + if (Graphics::VertexFetchOutOfRange(attribute)) continue; + bytes = Graphics::VertexBufferExtent(attribute); + } catch (const std::runtime_error&) { + continue; + } + add(address, bytes); + } + return ranges; +} + std::optional Driver::newestWriterLocked(std::uint64_t begin, std::uint64_t end) const { for (auto it = writtenBuffers.rbegin(); it != writtenBuffers.rend(); ++it) { if (begin < it->end && it->begin < end) return *it; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 1ecbf36a71..ce9a656cb7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -2,19 +2,110 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "ThreadOwned.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/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Eq/include/Event.hpp" +#include +#include #include #include #include +#include namespace AgcDriver::DriverDetail { +namespace { + +constexpr std::uint32_t eopSelfSignalFlipLimit = 4; + +bool orderedFills() { + static const bool ordered = std::getenv("APS5_PIPELINE_DRAIN_FILLS") == nullptr; + return ordered; +} + +bool pipelineDma() { + static const bool enabled = std::getenv("APS5_PIPELINE_DMA") != nullptr; + return enabled; +} + +bool keepsDrawPipeline(std::span packet, std::uint32_t header, std::uint32_t opcode) { + if (header == FlipPacketHeader || header == RenderingWaitPacketHeader) return false; + if (Pm4::DrawOpcode(opcode)) return true; + switch (opcode) { + case 0x11: case 0x12: case 0x13: case 0x20: case 0x26: case 0x2a: case 0x2f: case 0x42: case 0x58: case 0x59: case 0x69: case 0x76: case 0x79: case 0x7a: case 0x81: return true; + case 0x46: return packet.size() >= 2 && (packet[1] & 0x3fu) != 0x39u; + case 0x15: return orderedFills(); + case 0x63: case 0x64: case 0x9f: + return packet.size() >= 5 && !DrawPipeline::Queue0().Overlaps(packet[1] | (static_cast(packet[2]) << 32u), static_cast(packet[4]) * 8); + case 0x50: { + if (packet.size() < 7) return false; + const auto destination = Pm4::DmaDestination(packet); + if (destination == Pm4::DmaDestinationNowhere) return true; + if (!pipelineDma()) return false; + if (destination == Pm4::DmaSelectGds) return true; + const auto bytes = Pm4::DmaBytes(packet); + if (bytes == 0) return true; + const auto source = Pm4::DmaSource(packet); + if ((source == 0 || source == 3) && DrawPipeline::Queue0().Overlaps(packet[2] | (static_cast(packet[3]) << 32u), bytes)) return false; + const auto address = packet[4] | (static_cast(packet[5]) << 32u); + return !DrawPipeline::Queue0().Overlaps(address, bytes); + } + case 0x3c: case 0x93: { + if (packet.size() < 5) return false; + const std::uint64_t awaited = packet[2] | (static_cast(packet[3]) << 32u); + const auto bytes = Pm4::WaitAwaitedBytes(packet); + if (DrawPipeline::Queue0().Overlaps(awaited, bytes)) return false; + GuestMemory::CheckRange(reinterpret_cast(awaited), bytes, bytes); + return Pm4::WaitSatisfiedUnchecked(packet); + } + case 0x10: { + const auto operation = (header >> 2u) & 0x3fu; + return operation == 0 || operation == 0x09 || operation == 0x0b || operation == 0x0c || operation == 0x1a; + } + default: return false; + } +} + +bool sealsWriteOnly(std::span packet, std::uint32_t opcode) { + if (opcode == 0x58) return true; + if (opcode != 0x46 || packet.size() < 2) return false; + switch (packet[1] & 0x3fu) { + case 0x07: case 0x0f: case 0x10: case 0x16: case 0x2a: case 0x2c: case 0x2e: case 0x31: return true; + default: return false; + } +} + +bool& drawnSinceSeal() { + static thread_local bool drawn = false; + return drawn; +} + +} + +void Driver::deliverGuestEopInterrupt(std::uint32_t queue) { + if (!AgcDriverDeliverEopInterrupt(queue)) return; + std::lock_guard lock(eopSelfSignalMutex); + eopSelfSignalFlips[queue] = 0; +} + +void Driver::deliverFlipEopInterrupt(std::uint32_t queue) { + { + std::lock_guard lock(eopSelfSignalMutex); + const auto latched = eopSelfSignalFlips.find(queue); + if (latched != eopSelfSignalFlips.end()) { + if (++latched->second <= eopSelfSignalFlipLimit) return; + eopSelfSignalFlips.erase(latched); + } + } + AgcDriverDeliverEopInterrupt(queue); +} + template void Driver::timed(double WorkerProfile::*bucket, TWork&& work) { static thread_local WorkerProfile profile; @@ -27,6 +118,7 @@ void Driver::timed(double WorkerProfile::*bucket, TWork&& work) { profile.reported = end; AgcDriver::ProfilePrint_nid_no_patch( "[gpu] worker at %.0f s: dispatch %.1f s, draw %.1f s, wait %.1f s\n", std::chrono::duration(end - profile.start).count(), profile.dispatchMs / 1000, profile.drawMs / 1000, profile.waitMs / 1000); } + if (report && bucket != &WorkerProfile::waitMs) PerformanceCounters::AddDriverNanoseconds(static_cast(std::chrono::duration_cast(end - begin).count())); } void Driver::execute(const Submission& submission) { @@ -41,6 +133,21 @@ void Driver::execute(const Submission& submission) { } ~FlipHold() { Release(); } } flipHold{*this, submission.holdsFlip}; + const bool pipelined = submission.queue == 0 && DrawPipeline::Depth() != 0; + static const bool orderedWaits = std::getenv("APS5_PIPELINE_DRAIN_LABELS") == nullptr; + DrawPipeline::Active() = pipelined; + struct PipelineDrain { + bool pipelined; + ~PipelineDrain() { + if (!pipelined || std::uncaught_exceptions() == 0) return; + try { + DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission); + } catch (...) { + } + } + } pipelineDrain{pipelined}; + static const bool orderedCompletion = std::getenv("APS5_PIPELINE_DRAIN_SUBMISSIONS") == nullptr && orderedWaits; + if (pipelined && (submission.suspend || !orderedCompletion)) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission); if (submission.suspend) { PerformanceContext timingContext(submissionTiming); PerformanceTimer timing("Driver.Suspend"); @@ -107,8 +214,13 @@ void Driver::execute(const Submission& submission) { thread_local PacketProfile* packetProfileSlot = nullptr; auto& packetProfile = ShaderRecompiler::ThreadOwned(packetProfileSlot); ++packetProfile.submissions; + auto& packetCounters = PacketCounters::ThisThread(); - bumpEpoch(&EpochBumps::submissions); + static const bool epochEach = std::getenv("APS5_SUBMISSION_EPOCH_EACH") != nullptr; + if (epochEach || submission.enqueuedAt == std::chrono::steady_clock::time_point{} || submission.enqueuedAt > lastEpochBump()) { + if (!epochEach) noteReleasedTails(submission); + bumpEpoch(&EpochBumps::submissions); + } clearResolvedAhead(); gateOpened() = true; struct ClearAhead { @@ -126,13 +238,42 @@ void Driver::execute(const Submission& submission) { if (APS5_ENABLE_TIMING_LOG && submission.queue == 0 && pendingFrameTiming == nullptr) includeTimingSubmission(submission, false); PerformanceContext timingContext(submission.queue == 0 ? pendingFrameTiming.get() : FrameTiming::Async()); PerformanceTimer timing("Driver.Packet"); + const bool predicated = Pm4::Predicated(header) && queue.predication.operation != 0; + if (pipelined && !predicated && header != FlipPacketHeader && header != RenderingWaitPacketHeader && !submission.flips.contains(cursor) && (enqueueLabelPacket(packet, opcode, submission.queue) || enqueueDmaPacket(packet, opcode, submission.queue, queue))) { + traceLabel(packet, submission.queue); + recent.Record(cursor); + cursor = nextCursor; + continue; + } + if (pipelined && !predicated && (opcode == 0x3c || opcode == 0x93) && header != RenderingWaitPacketHeader && orderedWaits && DrawPipeline::Queue0().Busy()) { + const auto awaitedBytes = Pm4::WaitAwaitedBytes(packet); + const std::uint64_t awaited = (packet[2] & (awaitedBytes == 8 ? ~7u : ~3u)) | (static_cast(packet[3]) << 32u); + const auto value = DrawPipeline::Queue0().PendingLabel(awaited, awaitedBytes); + if (value.has_value() && Pm4::WaitComparesValue(packet, *value)) { + ++waitOutcomes().fromRecorderSameQueue; + gateOpened() = true; + recent.Record(cursor); + cursor = nextCursor; + continue; + } + if (!DrawPipeline::Queue0().Overlaps(awaited, awaitedBytes) && Pm4::WaitSatisfiedUnchecked(packet)) { + bumpEpoch(&EpochBumps::waits); + ++waitOutcomes().atEntry; + gateOpened() = true; + recent.Record(cursor); + cursor = nextCursor; + continue; + } + } + if (pipelined && DrawPipeline::Queue0().Busy() && !keepsDrawPipeline(packet, header, opcode)) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, header == FlipPacketHeader ? 0xffu : opcode); + if (pipelined && predicated && DrawPipeline::Queue0().Busy() && DrawPipeline::Queue0().Overlaps(queue.predication.address, queue.predication.operation == 3 ? 8 : 4)) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, opcode); std::shared_lock deviceUse(deviceReplacement, std::defer_lock); if (opcode != 0x3c && opcode != 0x93 && header != RenderingWaitPacketHeader && header != FlipPacketHeader) deviceUse.lock(); timing.Mark("device_lock"); GuestMemory::SetCurrentPacket(header == FlipPacketHeader ? 0xffffu : opcode, submission.queue); CaptureTrace::Log("packet submission=%llu queue=%x offset=%zu header=%08x words=%zu", static_cast(submission.serial), submission.queue, cursor, header, packet.size()); - if (Pm4::Predicated(header) && queue.predication.operation != 0) { + if (predicated) { landPendingWaits(submission.queue); recordQueuedLabelsBeforeRead(submission.queue); if (!Pm4::PredicationPasses(queue)) { @@ -202,7 +343,15 @@ void Driver::execute(const Submission& submission) { recent.Record(cursor); if (header == RenderingWaitPacketHeader) { timed(&WorkerProfile::waitMs, [&] { submission.renderingWaits.at(cursor)->Wait(); }); - } else if (header == FlipPacketHeader) { + } else if (submission.flips.contains(cursor)) { + if (opcode == 0x49) { + if (!wroteOnGpu) { + Pm4::Execute(packet, queue); + if (const auto label = Pm4::DecodeLabelWrite(packet)) noteLabelStore(label->address, label->Bytes(), ++eventSerial, submission.queue); + } + if (endOfPipeInterrupt && !interruptDeferred) deliverGuestEopInterrupt(submission.queue); + } + if (deviceUse.owns_lock()) deviceUse.unlock(); CheckFailure(); std::uint64_t batchesAtFlip = 0, unsignaledAtFlip = 0; bool flipInterruptDeferred = false; @@ -216,35 +365,41 @@ void Driver::execute(const Submission& submission) { if (localDevice != nullptr) { localDevice->FlipBatches(batchesAtFlip, unsignaledAtFlip); const auto queueId = submission.queue; - flipInterruptDeferred = localDevice->AfterRecordedWork([queueId] { AgcDriverDeliverEopInterrupt(queueId); }, false); + flipInterruptDeferred = localDevice->AfterRecordedWork([this, queueId] { deliverFlipEopInterrupt(queueId); }, false); } } - if (!flipInterruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue); + if (!flipInterruptDeferred) deliverFlipEopInterrupt(submission.queue); ++flipsCounted; if (batchesAtFlip != 0) flipSerial = batchesAtFlip; flipBatchesUnsignaled += unsignaledAtFlip; timing.Mark("flip_flush"); timing.FinishPacket(submission.serial, cursor, 0xffffu, count); - auto frame = APS5_ENABLE_TIMING_LOG ? std::move(pendingFrameTiming) : std::make_shared(++frameSerial); + const bool queueTiming = APS5_ENABLE_TIMING_LOG && submission.queue == 0; + auto frame = queueTiming ? std::move(pendingFrameTiming) : std::make_shared(++frameSerial); require(frame != nullptr, "flip has no frame timing"); const auto now = FrameTiming::Clock::now(); - if (!APS5_ENABLE_TIMING_LOG) frame->IncludeSubmission(submission.serial, now, now, now, true); - frame->SetFlip(submission.serial, cursor, APS5_ENABLE_TIMING_LOG ? submission.receivedAt : now, now); - if (APS5_ENABLE_TIMING_LOG) frame->CollectBackground(); + if (!queueTiming) frame->IncludeSubmission(submission.serial, now, now, now, true); + frame->SetFlip(submission.serial, cursor, queueTiming ? submission.receivedAt : now, now); + if (queueTiming) frame->CollectBackground(); frame->NoteFlipBatches(batchesAtFlip, unsignaledAtFlip); - CaptureTrace::Log("flip frame=%llu submission=%llu offset=%zu batch=%llu unsignaled=%llu", static_cast(frameSerial), static_cast(submission.serial), cursor, static_cast(batchesAtFlip), static_cast(unsignaledAtFlip)); + CaptureTrace::Log("flip frame=%llu submission=%llu offset=%zu batch=%llu unsignaled=%llu", static_cast(frameSerial.load()), static_cast(submission.serial), cursor, static_cast(batchesAtFlip), static_cast(unsignaledAtFlip)); flipHold.Release(); submission.flips.at(cursor)->GpuReady(frame); } else if (opcode == 0x15 || opcode == 0x16) { + packetCounters.CountDispatch(); landPendingWaits(submission.queue); currentPacketOffset() = cursor; if (gateOpened()) { gateOpened() = false; - 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); }); + Pm4::NoteDispatchRegisters(packet, queue); Graphics::Recorder::CountRecordedWork(); finishDispatchPacket(opcode == 0x16); } else if (opcode == 0x3c || opcode == 0x93) { @@ -266,6 +421,8 @@ void Driver::execute(const Submission& submission) { } } else if (drawPacket) { landPendingWaits(submission.queue); + packetCounters.CountDraw(); + drawnSinceSeal() = true; bool drawn = false; timed(&WorkerProfile::drawMs, [&] { static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr; @@ -289,6 +446,10 @@ void Driver::execute(const Submission& submission) { } else if (traceDraws) { std::fprintf(stderr, "[draw] target 0x%llx mask 0x%x ok\n",static_cast(color), readRegister(queue.context, 0x8e)); } + } catch (const RefusedDraw& refused) { + CaptureTrace::Log("draw-refused submission=%llu offset=%zu reason=%.256s", static_cast(submission.serial), cursor, refused.what()); + countSkip(Graphics::DrawSkip::Thrown); + ReportRefusedDraw(refused); } catch (const std::exception& error) { CaptureTrace::Log("draw-error submission=%llu offset=%zu reason=%.256s", static_cast(submission.serial), cursor, error.what()); countSkip(Graphics::DrawSkip::Thrown); @@ -307,16 +468,49 @@ void Driver::execute(const Submission& submission) { recordQueuedLabelsBeforeRead(submission.queue); Pm4::Execute(packet, queue); } else if (sampleDump && !wroteOnGpu) { - dumpSampleCounters(packet[2] | (static_cast(packet[3]) << 32u)); + dumpSampleCounters(Pm4::SampleDumpAddress(packet)); + } else if (submission.queue == 0 && sealsWriteOnly(packet, opcode)) { + if (std::exchange(drawnSinceSeal(), false)) { + const auto seal = [this] { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + if (const auto localDevice = device.Load()) localDevice->SealPassWriteOnly(); + }; + if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Enqueue(seal, {}); + else seal(); + } } else if (opcode != 0x42 && opcode != 0x46 && opcode != 0x58) { if (!wroteOnGpu) { - if (Pm4::IndirectRegisterOpcode(opcode)) Pm4::ExecuteIndirectRegisters(packet, submission.registerLists.at(cursor), queue); - else Pm4::Execute(packet, queue); + if (Pm4::IndirectRegisterOpcode(opcode)) { + const auto& listed = submission.registerLists.at(cursor); + if (Pm4::IndirectRegistersValid(listed)) { + Pm4::ExecuteIndirectRegisters(packet, listed, queue); + } else { + landPendingWaits(submission.queue); + recordQueuedLabelsBeforeRead(submission.queue); + const auto current = Pm4::ReadIndirectRegisters(packet); + static const bool skipInvalid = std::getenv("APS5_SKIP_INVALID_INDIRECT_REGISTERS") != nullptr; + static std::atomic reported{false}; + if (!reported.exchange(true)) { + std::uint32_t invalid = 0; + for (std::size_t i = 0; i < listed.size(); i += 2) { + if (!Pm4::IndirectRegistersValid(std::span(&listed[i], 1))) { + invalid = listed[i]; + break; + } + } + std::fprintf(stderr, "[gpu] queue 0x%x %s at DWORD %zu: register list entry with offset dword 0x%08x read again where the packet executes%s\n", submission.queue, Pm4::Name(packet[0]).c_str(), cursor, invalid, skipInvalid ? "; invalid entries are skipped (APS5_SKIP_INVALID_INDIRECT_REGISTERS)" : ""); + } + Pm4::ExecuteIndirectRegisters(packet, current, queue, skipInvalid); + } + } else { + Pm4::Execute(packet, queue); + } if (opcode == 0x49 || opcode == 0x37) { if (const auto label = Pm4::DecodeLabelWrite(packet)) noteLabelStore(label->address, label->Bytes(), ++eventSerial, submission.queue); } } - if (endOfPipeInterrupt && !interruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue); + if (endOfPipeInterrupt && !interruptDeferred) deliverGuestEopInterrupt(submission.queue); } if (drawPacket || (sampleDump && wroteOnGpu)) Graphics::Recorder::CountRecordedWork(); timing.Mark(header == RenderingWaitPacketHeader ? "rendering_wait" : opcode == 0x3c || opcode == 0x93 ? "memory_wait" : opcode == 0x15 || opcode == 0x16 ? "dispatch" : drawPacket ? "draw" : "execute"); @@ -328,6 +522,22 @@ void Driver::execute(const Submission& submission) { PerformanceContext endContext(submission.queue == 0 ? frameTiming() : FrameTiming::Async()); PerformanceTimer endTiming("Driver.EndSubmission"); static const bool submitAtEnd = std::getenv("APS5_SUBMIT_AT_END") != nullptr; + if (pipelined && orderedCompletion && deferredLabels().labels.empty()) { + if (submission.rewindTail == nullptr) { + deferCompletion(submission); + return; + } + if ((std::atomic_ref(*const_cast(submission.rewindTail - 1)).load(std::memory_order_acquire) & 0x80000000u) != 0) { + DrawPipeline::Queue0().Enqueue([this] { + submitOpenWork(); + wakeQueueZeroForCompletions(); + }, {}); + endTiming.Finish(); + executeRewindTail(submission); + return; + } + } + if (pipelined) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission); if (!deferredLabels().labels.empty() || Graphics::Recorder::PendingLabelSince().has_value() || Graphics::Recorder::RecordedWorkSinceSubmit() != 0) { auto& costs = submissionCosts(submission.queue); if (!submitAtEnd && submission.rewindTail == nullptr && submission.queue == 0 && workerQueued() != nullptr && workerQueued()->load(std::memory_order_acquire) != 0) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketFlush.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketFlush.cpp index 33863d1b2c..410a98b3c8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketFlush.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketFlush.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/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/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -50,6 +51,11 @@ void Driver::flushBetweenPackets(std::uint32_t queue, std::uint32_t header, bool return; } if (reap) lastReapTry = now; + if (DrawPipeline::Active() && (submit || record || capped || recordAtLock)) { + static const bool committerSubmits = std::getenv("APS5_PIPELINE_DRAIN_LABELS") == nullptr; + if (committerSubmits && !record && !recordAtLock && DrawPipeline::Queue0().Busy()) return; + DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Flush); + } std::unique_lock gpuLock(GuestMemory::GpuMutex(), std::defer_lock); if (record) GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Label); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index a2ba8abae2..d65d4bf795 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -1,5 +1,6 @@ #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/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -9,6 +10,9 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" +#include #include #include #include @@ -52,7 +56,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue } if (reason == 0 || reason == 5) { const auto queueId = submission.queue; - interruptDeferred = localDevice->AfterRecordedWork([queueId] { AgcDriverDeliverEopInterrupt(queueId); }, submission.queue == 0); + interruptDeferred = localDevice->AfterRecordedWork([this, queueId] { deliverGuestEopInterrupt(queueId); }, submission.queue == 0); if (interruptDeferred && workOpen) localDevice->SubmitRecorded(submission.queue == 0); wroteOnGpu = true; } else if (reason == 1) { @@ -112,7 +116,29 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue if (!drainAll && !cpuStores && (opcode == 0x40 || opcode == 0x50 || opcode == 0x83)) { constexpr std::size_t gpuStoreLimit = 65536; bool drained = true; - if (const auto store = Pm4::ResolveStore(packet, queue, gpuStoreLimit)) { + const auto copy = opcode == 0x50 ? Pm4::DecodeMemoryCopy(packet) : std::nullopt; + const auto copyBehindPendingSource = [&] { + if (!copy.has_value() || copy->bytes > gpuStoreLimit || !GuestMemory::Accessible(reinterpret_cast(copy->source), copy->bytes) || !GuestMemory::Accessible(reinterpret_cast(copy->destination), copy->bytes, true)) return false; + const auto end = copy->source + copy->bytes; + const std::array, 1> range{{{copy->source, end}}}; + const auto& queued = deferredLabels().labels; + const bool labelQueued = std::any_of(queued.begin(), queued.end(), [&](const DeferredLabel& label) { return label.address < end && copy->source < label.address + label.size; }); + if (!labelQueued && !Graphics::Recorder::SnapshotWriteOverlaps(copy->source, copy->bytes) && !Graphics::StorageTexture::AnyPendingOverlaps(range) && !Graphics::AnyShadowedOverlaps(copy->source, copy->bytes)) return false; + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Copy); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + const auto localDevice = device.Load(); + if (localDevice == nullptr) return false; + recordDeferredLabels(localDevice.get(), submission.queue); + if (!localDevice->StoresPendingOver(copy->source, copy->bytes) || !localDevice->GpuCopyPossible(copy->destination, copy->source, copy->bytes)) return false; + const auto outcome = localDevice->CopyBuffer(copy->destination, copy->source, copy->bytes, 0, std::numeric_limits::max(), 0, 0, submission.queue, [](std::span, std::uint64_t) {}); + if (outcome.path != 1 && outcome.path != 3) throw std::runtime_error("AGC driver: a DMA_DATA copy of up to 64 KiB that GpuCopyPossible accepted took copy path " + std::to_string(outcome.path)); + return true; + }; + if (copyBehindPendingSource()) { + ++copiesBehindPendingSources; + wroteOnGpu = true; + drained = false; + } else if (const auto store = Pm4::ResolveStore(packet, queue, gpuStoreLimit)) { const auto bytes = store->Bytes(); if (bytes.empty()) { @@ -145,7 +171,6 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue } } if (drained && opcode == 0x50) { - const auto copy = Pm4::DecodeMemoryCopy(packet); if (copy.has_value() && copy->bytes > gpuStoreLimit && GuestMemory::Accessible(reinterpret_cast(copy->source), copy->bytes) && GuestMemory::Accessible(reinterpret_cast(copy->destination), copy->bytes, true)) { GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Copy); std::lock_guard gpuLock(GuestMemory::GpuMutex()); @@ -165,21 +190,39 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue } } } + if (drained && opcode == 0x50 && Pm4::DmaSource(packet) == 2 && Pm4::DmaDestination(packet) != 1) { + const auto destination = packet[4] | (static_cast(packet[5]) << 32u); + const std::size_t bytes = packet[6] & 0x3ffffffu; + if (bytes != 0 && destination % 16 == 0 && bytes % 16 == 0 && GuestMemory::Accessible(reinterpret_cast(destination), bytes, true)) { + if (const auto localDevice = device.Load()) { + storeBufferFill(submission.queue, destination, bytes, {packet[2], packet[2], packet[2], packet[2]}, localDevice, false, false); + ++dmaFills; + wroteOnGpu = true; + drained = false; + } + } + } if (drained) ++storesDrained; } static const bool drawDrain = std::getenv("APS5_DRAW_DRAIN") != nullptr; drawPacket = Pm4::DrawOpcode(opcode); - sampleDump = opcode == 0x46 && (packet[1] & 0x3fu) == 0x39u; + sampleDump = Pm4::SampleDump(packet); if (sampleDump && !drainAll) { GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Label); std::lock_guard gpuLock(GuestMemory::GpuMutex()); if (const auto localDevice = device.Load()) { recordDeferredLabels(localDevice.get(), submission.queue); - wroteOnGpu = localDevice->DumpSamplesOnGpu(packet[2] | (static_cast(packet[3]) << 32u)); + wroteOnGpu = localDevice->DumpSamplesOnGpu(Pm4::SampleDumpAddress(packet)); } } + if (header == FlipPacketHeader) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + static_cast(Graphics::StorageTexture::StoreSmallPending("flip")); + } + static const bool syncFlip = std::getenv("APS5_SYNC_FLIP") != nullptr; const bool drains = drainAll ? ((Pm4::AccessesMemory(header) && opcode != 0x16) || opcode == 0x42 || opcode == 0x46 || opcode == 0x58 || header == FlipPacketHeader) : (!wroteOnGpu && !orderedAlready && (opcode == 0x49 || opcode == 0x37 || opcode == 0x40 || opcode == 0x45 || opcode == 0x50 || opcode == 0x83 || sampleDump || (drawPacket && drawDrain) || (header == FlipPacketHeader && syncFlip))); @@ -241,10 +284,173 @@ void Driver::dumpSampleCounters(std::uint64_t address) { samples = recorder->SamplesTotal(); } constexpr std::uint64_t ready = 1ull << 63u; - for (std::uint64_t db = 0; db < 16; ++db) { + for (std::uint64_t db = 0; db < Pm4::SampleDumpBlocks; ++db) { const std::uint64_t value = ready | (db == 0 ? samples : 0u); - GuestMemory::Write(address + db * 16u, std::as_bytes(std::span(&value, 1)), 8); + GuestMemory::Write(address + db * Pm4::SampleDumpStride, std::as_bytes(std::span(&value, 1)).first(Pm4::SampleDumpBlockBytes), 8); } } +bool Driver::enqueueDmaPacket(std::span packet, std::uint32_t opcode, std::uint32_t queue, const QueueState& state) { + static const bool enabled = std::getenv("APS5_PIPELINE_DMA") != nullptr; + if (!enabled || opcode != 0x50 || queue != 0 || packet.size() < 7 || !DrawPipeline::Active() || !deferredLabels().labels.empty()) return false; + const auto source = ((packet[1] >> 29u) & 3u) | ((packet[6] >> 24u) & 4u) | ((packet[6] >> 25u) & 8u); + constexpr std::size_t immediateLimit = std::size_t{16} << 20u; + constexpr std::size_t gpuStoreLimit = 65536; + if (source == 2 && Pm4::DmaBytes(packet) > gpuStoreLimit && Pm4::DmaDestination(packet) != Pm4::DmaSelectGds) { + const auto fillAddress = packet[4] | (static_cast(packet[5]) << 32u); + const auto fillBytes = Pm4::DmaBytes(packet); + const auto pattern = packet[2]; + if (fillAddress % 16 != 0 || fillBytes % 16 != 0 || !GuestMemory::Accessible(reinterpret_cast(fillAddress), fillBytes, true) || !device.Load()) return false; + DrawPipeline::Queue0().Enqueue([this, queue, fillAddress, fillBytes, pattern] { + GuestMemory::SetCurrentPacket(0x50, queue); + if (const auto localDevice = device.Load()) { + storeBufferFill(queue, fillAddress, fillBytes, {pattern, pattern, pattern, pattern}, localDevice, false, false); + ++dmaFills; + } + }, {{fillAddress, fillAddress + fillBytes}}); + return true; + } + if (source == 2) { + const auto store = Pm4::ResolveStore(packet, state, immediateLimit); + if (!store.has_value() || store->Bytes().empty() || !GuestMemory::Accessible(reinterpret_cast(store->address), store->Bytes().size(), true)) return false; + const auto address = store->address; + std::vector bytes(store->Bytes().begin(), store->Bytes().end()); + const auto size = bytes.size(); + const bool metadataFill = Pm4::DmaDestination(packet) != 1; + const auto fillAddress = packet[4] | (static_cast(packet[5]) << 32u); + const auto fillBytes = packet[6] & 0x3ffffffu; + const auto pattern = packet[2]; + DrawPipeline::Queue0().Enqueue([this, queue, address, bytes = std::move(bytes), metadataFill, fillAddress, fillBytes, pattern] { + GuestMemory::SetCurrentPacket(0x50, queue); + if (metadataFill) { + Graphics::NoteDepthMetadataFill(fillAddress, fillBytes, pattern); + Graphics::NoteColorMetadataFill(fillAddress, fillBytes, pattern); + } + commitDmaStore(queue, address, bytes); + }, {{address, address + size}}); + return true; + } + if (Pm4::DmaSource(packet) == 0 || Pm4::DmaSource(packet) == 3) Graphics::RequireNoMultisampleTarget(packet[2] | (static_cast(packet[3]) << 32u), packet[6] & 0x3ffffffu, "a DMA copy"); + const auto copy = Pm4::DecodeMemoryCopy(packet); + if (!copy.has_value() || copy->destination % 4 != 0 || copy->bytes % 4 != 0 || !GuestMemory::Accessible(reinterpret_cast(copy->source), copy->bytes) || !GuestMemory::Accessible(reinterpret_cast(copy->destination), copy->bytes, true)) return false; + DrawPipeline::Queue0().Enqueue([this, queue, copy = *copy] { + DrawPipeline::Queue0().ReleaseRunningReads(); + GuestMemory::SetCurrentPacket(0x50, queue); + constexpr std::size_t gpuStoreLimit = 65536; + if (copy.bytes > gpuStoreLimit) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Copy); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + if (const auto localDevice = device.Load()) { + recordDeferredLabels(localDevice.get(), queue); + const auto outcome = localDevice->CopyBuffer(copy.destination, copy.source, copy.bytes, 0, std::numeric_limits::max(), 0, 0, queue, [](std::span, std::uint64_t) {}); + if (outcome.path == 1 || outcome.path == 3) return; + if (outcome.path == 2 && !Graphics::RegisteredReadableCovers(copy.destination, copy.bytes) && !localDevice->StoresPendingOver(copy.destination, copy.bytes)) ++copiesToHostMemory; + } + } + std::vector bytes(copy.bytes); + GuestMemory::Read(copy.source, bytes, 1); + commitDmaStore(queue, copy.destination, bytes); + }, {{copy->destination, copy->destination + copy->bytes}}, 0, {}, {{copy->source, copy->source + copy->bytes}}); + return true; +} + +void Driver::commitDmaStore(std::uint32_t queue, std::uint64_t address, std::span bytes) { + constexpr std::size_t gpuStoreLimit = 65536; + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Label); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + const auto localDevice = device.Load(); + Graphics::StorageTexture::FlushPending(address, bytes.size(), nullptr, "packet store", Graphics::PublishScope::PartialUnits); + recordDeferredLabels(localDevice.get(), queue); + int reason = 4; + std::uint64_t stamp = 0; + if (bytes.size() <= gpuStoreLimit && localDevice != nullptr) { + stamp = ++eventSerial; + reason = localDevice->WriteLabelOnGpu(address, bytes, stamp, queue); + } + if (reason == 0 || reason == 5 || reason == 6) { + noteLabelStore(address, bytes, stamp, queue); + if (reason == 0) ++storesOnGpu; + else ++storesBehindCompletions; + } else { + if (reason != 1 && localDevice != nullptr) localDevice->WaitIdle(); + GuestMemory::Write(address, bytes, 1); + noteLabelStore(address, bytes, stamp != 0 ? stamp : ++eventSerial, queue); + ++storesOnCpu; + } + Graphics::Recorder::CloseLabelGroup(GuestMemory::TrackerGeneration()); +} + +bool Driver::enqueueLabelPacket(std::span packet, std::uint32_t opcode, std::uint32_t queue) { + static const bool ordered = std::getenv("APS5_PIPELINE_DRAIN_LABELS") == nullptr; + if (!ordered || !DeferLabels() || (opcode != 0x49 && opcode != 0x37) || !deferredLabels().labels.empty()) return false; + const auto interruptSelect = opcode == 0x49 ? (packet[2] >> 24u) & 7u : 0u; + const bool endOfPipeInterrupt = interruptSelect != 0 && interruptSelect != 3; + const auto label = Pm4::DecodeLabelWrite(packet); + if (!label.has_value()) { + if (opcode != 0x49) return false; + const bool storesNothing = (packet[2] >> 29u) == 0 || (packet[3] | (static_cast(packet[4]) << 32u)) == 0; + if (!storesNothing) return false; + if (!endOfPipeInterrupt) { + ++noOpLabels; + return true; + } + } + std::uint64_t address = 0; + std::vector bytes; + std::vector writes; + if (label.has_value()) { + const auto stored = label->Bytes(); + address = label->address; + bytes.assign(stored.begin(), stored.end()); + writes.emplace_back(address, address + bytes.size()); + } + if (endOfPipeInterrupt) bumpEpoch(&EpochBumps::drains); + auto stored = bytes; + DrawPipeline::Queue0().Enqueue([this, queue, opcode, address, bytes = std::move(bytes), endOfPipeInterrupt] { + GuestMemory::SetCurrentPacket(opcode, queue); + commitLabel(queue, address, bytes, endOfPipeInterrupt); + }, std::move(writes), address, std::move(stored)); + return true; +} + +void Driver::commitLabel(std::uint32_t queue, std::uint64_t address, std::span bytes, bool endOfPipeInterrupt) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Label); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + const auto localDevice = device.Load(); + recordDeferredLabels(localDevice.get(), queue); + const bool workOpen = Graphics::Recorder::RecordedWorkSinceSubmit() != 0; + int reason = localDevice != nullptr ? 0 : 1; + if (!bytes.empty()) { + const auto stamp = ++eventSerial; + reason = localDevice != nullptr ? localDevice->WriteLabelOnGpu(address, bytes, stamp, queue) : 4; + if (reason != 0 && reason != 5 && reason != 6) { + if (reason != 1 && localDevice != nullptr) localDevice->WaitIdle(); + GuestMemory::Write(address, bytes, 4); + } + noteLabelStore(address, bytes, stamp, queue); + Graphics::Recorder::CloseLabelGroup(GuestMemory::TrackerGeneration()); + countLabelOutcome(reason); + ++immediateLabels; + } else { + ++noOpLabels; + } + if (endOfPipeInterrupt) { + bool deferred = false; + if (localDevice != nullptr && (reason == 0 || reason == 5 || reason == 6)) { + deferred = localDevice->AfterRecordedWork([this, queue] { deliverGuestEopInterrupt(queue); }, queue == 0); + if (deferred && workOpen) localDevice->SubmitRecorded(queue == 0); + } + if (!deferred) deliverGuestEopInterrupt(queue); + } + submitDueAfterCommit(localDevice.get()); +} + +void Driver::submitDueAfterCommit(VulkanDevice* localDevice) { + if (localDevice == nullptr) return; + const auto pending = Graphics::Recorder::PendingLabelSince(); + const bool due = pending.has_value() && std::chrono::steady_clock::now() - *pending >= labelFlushDeadline(); + const bool capped = batchCap() != 0 && Graphics::Recorder::RecordedWorkSinceSubmit() >= batchCap(); + if (due || capped) localDevice->SubmitRecorded(true); +} + } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp index 55701d83cb..d071d049fc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp @@ -4,11 +4,27 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Queues/WorkerAffinity.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include #include "prx/libSceAgcDriver/Execution/include/TracyZone.hpp" #include namespace AgcDriver::DriverDetail { +std::uint32_t Driver::ReserveEopFlip(const FlipInfo& info) { + std::lock_guard lock(mutex); + rethrowFailure(); + checkStopping(); + require(nextEopToken <= EopFlipTokenLimit, "EOP flip token space exhausted"); + const auto output = outputs.find(info.handle); + require(output != outputs.end(), "EOP flip references an unregistered video output"); + auto request = output->second->TryReserve(info); + if (!request) return 0; + const auto token = nextEopToken; + eopFlips.emplace(token, EopFlip{info.handle, std::move(request)}); + ++nextEopToken; + return token; +} + void Driver::RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr& output) { require(output != nullptr, "null video output"); std::lock_guard lock(mutex); @@ -21,6 +37,7 @@ void Driver::UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptrsecond == output, "video output registration mismatch"); + std::erase_if(eopFlips, [handle](const auto& entry) { return entry.second.handle == handle; }); outputs.erase(it); } @@ -72,6 +89,7 @@ void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buff std::uint32_t drawableHeight = 0; window.getDrawableSize(window.context, &drawableWidth, &drawableHeight); presenting->Resize(drawableWidth, drawableHeight); + presenting->SelectDynamicRange(buffer); timing.Mark("resize"); presentable = presenting->Presentable(); if (buffer != nullptr) require(buffer->width == window.width && buffer->height == window.height, "display buffer extent differs from output"); 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 56db0c1f68..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,5 +1,8 @@ #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" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Queues/WorkerAffinity.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/WaitMemory.hpp" @@ -54,6 +57,61 @@ void Driver::markCompleted(std::uint64_t serial) { } } +bool& Driver::completionDeferred() { + static thread_local bool deferred = false; + return deferred; +} + +void Driver::deferCompletion(const Submission& submission) { + std::uint64_t received = submission.received; + { + std::lock_guard lock(mutex); + if (queue0Executing != 0) received = std::min(received, queue0Executing); + queue0Uncommitted.push_back(received); + } + completionDeferred() = true; + DrawPipeline::Queue0().Enqueue([this, serial = submission.serial, received, labelWrites = submission.labelWrites] { + completeSubmission(serial, received, labelWrites); + }, {}); +} + +void Driver::submitOpenWork() { + if (deferredLabels().labels.empty() && !Graphics::Recorder::PendingLabelSince().has_value() && Graphics::Recorder::RecordedWorkSinceSubmit() == 0) return; + ++submissionCosts(0).endSubmits; + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::End); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + const auto localDevice = device.Load(); + recordDeferredLabels(localDevice.get(), 0); + 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; + std::uint64_t serial, received; + std::span labelWrites; + ~Complete() { + bool notify = true; + { + std::lock_guard lock(driver.mutex); + driver.markCompleted(serial); + 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 || completionsPending(); + } + if (notify) driver.changed.notify_all(); + } + } complete{*this, serial, received, labelWrites}; + submitOpenWork(); +} + const std::atomic*& Driver::workerQueued() { static thread_local const std::atomic* queued = nullptr; return queued; @@ -159,12 +217,12 @@ void Driver::run(std::uint32_t id) noexcept { lock.lock(); if (ready()) break; if (id == 0 && allQueuesIdle() && !completionsPending() - && (!storeIdleResults || idleEpoch.load(std::memory_order_acquire) != idleEpochAtCheck)) continue; + && ((!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; @@ -182,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(); @@ -203,8 +269,10 @@ void Driver::run(std::uint32_t id) noexcept { { std::lock_guard lock(mutex); rethrowFailure(); - markCompleted(submission.serial); - forgetUnfinishedWrites(workers.at(id), submission); + if (!std::exchange(completionDeferred(), false)) { + markCompleted(submission.serial); + forgetUnfinishedWrites(workers.at(id), submission); + } if (id == 0) queue0Executing = 0; notify = idleWaiters != 0 || orderHolders.load(std::memory_order_acquire) != 0; 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 a0db6f839f..13d45c0f36 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -1,17 +1,77 @@ #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" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" +#include "prx/libSceAgcDriver/Execution/include/SubmissionCapture.hpp" #include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp" +#include "prx/libSceAgcDriver/Eq/include/Event.hpp" +#include "ThreadOwned.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include +#include #include #include +#include namespace AgcDriver::DriverDetail { +namespace { + +std::uint64_t captureTime() { + return static_cast(std::llround(TraceMs() * 1000.0)); +} + +void writeCaptured(Driver& driver, SubmissionCapture& capture, const CapturedSubmission& captured) { + try { + capture.Write(captured); + } catch (...) { + driver.ReportFailure(std::current_exception()); + throw; + } +} + +bool EopRelease(std::span words) { + return words.size() >= 8 && (words[0] >> 30u) == 3u && Pm4::PacketWords(words[0]) == 8u && ((words[0] >> 8u) & 0xffu) == 0x49u && ((words[2] >> 24u) & 7u) == 2u; +} + +bool& submitsQueue0() { + struct SubmitsQueue0Storage {}; + return HostThreadLocal(); +} + +} + +std::deque& Driver::releasedTails() { + struct ReleasedTailsStorage {}; + return HostThreadLocal, ReleasedTailsStorage>(); +} + +void Driver::noteReleasedTails(const Submission& submission) { + auto& released = releasedTails(); + released.clear(); + thread_local Submission* scratchSlot = nullptr; + auto& scratch = ShaderRecompiler::ThreadOwned(scratchSlot); + const std::uint32_t* tail = submission.rewindTail; + std::size_t words = submission.rewindWords; + for (std::size_t chunk = 0; chunk < 256 && tail != nullptr; ++chunk) { + if ((std::atomic_ref(*const_cast(tail - 1)).load(std::memory_order_acquire) & 0x80000000u) == 0) break; + released.push_back(tail - 1); + scratch.rewindTail = nullptr; + scratch.rewindWords = 0; + try { + copyCommands(scratch, tail, words); + } catch (const std::exception&) { + break; + } + tail = scratch.rewindTail; + words = scratch.rewindWords; + } +} + void Driver::copyCommands(Submission& submission, const std::uint32_t* guest, std::size_t words) { submission.commands.clear(); submission.conditionalEnds.clear(); @@ -146,10 +206,19 @@ void Driver::noteAwaitingTitle(std::uint32_t queue, std::uint64_t awaited) { void Driver::holdFlipBehindWorker(const Submission& submission) { if (submission.queue != 0 || onWorkerThread()) return; - bool flips = false; - for (std::size_t cursor = 0; cursor < submission.commands.size() && !flips; cursor += Pm4::PacketWords(submission.commands[cursor])) flips = submission.commands[cursor] == FlipPacketHeader; - if (!flips) return; + bool candidate = false; + for (std::size_t cursor = 0; cursor < submission.commands.size() && !candidate; cursor += Pm4::PacketWords(submission.commands[cursor])) { + const auto words = std::span(submission.commands).subspan(cursor); + candidate = words[0] == FlipPacketHeader || EopRelease(words); + } + if (!candidate) return; std::unique_lock lock(mutex); + bool flips = false; + for (std::size_t cursor = 0; cursor < submission.commands.size() && !flips; cursor += Pm4::PacketWords(submission.commands[cursor])) { + const auto words = std::span(submission.commands).subspan(cursor); + flips = words[0] == FlipPacketHeader || (EopRelease(words) && eopFlips.contains(words[7])); + } + if (!flips) return; flipHolders.fetch_add(1, std::memory_order_acq_rel); changed.wait(lock, [&] { return failure != nullptr || stopping.load(std::memory_order_acquire) || shutdownToken.stop_requested() || flipsAhead.load(std::memory_order_acquire) == 0 || queue0AwaitsTitle.load(std::memory_order_acquire); }); flipHolders.fetch_sub(1, std::memory_order_acq_rel); @@ -162,6 +231,7 @@ void Driver::releaseFlipHold() { } void Driver::reserveOutputs(Submission& submission) { + std::set eopTokens; for (std::size_t cursor = 0; cursor < submission.commands.size();) { const auto* words = submission.commands.data() + cursor; if (words[0] == RenderingWaitPacketHeader) { @@ -171,6 +241,15 @@ void Driver::reserveOutputs(Submission& submission) { require(wait != nullptr, "video output returned a null rendering wait"); submission.renderingWaits.emplace(cursor, std::move(wait)); } + if (EopRelease(std::span(submission.commands).subspan(cursor))) { + const auto token = words[7]; + const auto flip = eopFlips.find(token); + if (flip != eopFlips.end()) { + require(submission.queue == 0, "EOP flips on async queues are not implemented"); + require(eopTokens.insert(token).second, "EOP flip token repeated in a submission"); + submission.flips.emplace(cursor, flip->second.request); + } + } noteHeldAtSubmit(submission, cursor); if (words[0] == FlipPacketHeader) { const auto output = outputs.find(words[1]); @@ -182,28 +261,41 @@ void Driver::reserveOutputs(Submission& submission) { } cursor += Pm4::PacketWords(words[0]); } + for (const auto token : eopTokens) eopFlips.erase(token); } 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) & 0x80000000u) == 0) { + if ((control.load(std::memory_order_acquire) & Pm4::RewindValid) == 0) { noteWaitBlocked(stalled.queue, reinterpret_cast(stalled.rewindTail - 1), true); struct BlockedWait { Driver& driver; std::uint32_t queue; ~BlockedWait() { driver.noteWaitBlocked(queue, 0, false); } } blocked{*this, stalled.queue}; - while ((control.load(std::memory_order_acquire) & 0x80000000u) == 0) { + 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(); } } Submission tail{}; tail.queue = stalled.queue; if (APS5_ENABLE_TIMING_LOG) tail.receivedAt = std::chrono::steady_clock::now(); + auto& released = releasedTails(); + if (!released.empty() && released.front() == stalled.rewindTail - 1) { + released.pop_front(); + tail.enqueuedAt = lastEpochBump(); + } else { + released.clear(); + tail.enqueuedAt = std::chrono::steady_clock::now(); + } copyCommands(tail, stalled.rewindTail, stalled.rewindWords); if (APS5_ENABLE_TIMING_LOG) tail.copiedAt = std::chrono::steady_clock::now(); validate(tail, stalled.rewindTail); @@ -211,6 +303,8 @@ void Driver::executeRewindTail(const Submission& stalled) { if (APS5_ENABLE_TIMING_LOG) tail.validatedAt = std::chrono::steady_clock::now(); waitForFlipRoom(tail); if (APS5_ENABLE_TIMING_LOG) tail.roomReadyAt = std::chrono::steady_clock::now(); + auto* const capture = SubmissionCapture::Active(); + auto captured = capture != nullptr ? SubmissionCapture::Prepare(tail.queue, SubmissionKind::Rewind, tail.commands, tail.conditionalEnds) : CapturedSubmission{}; { std::lock_guard lock(mutex); rethrowFailure(); @@ -219,16 +313,31 @@ void Driver::executeRewindTail(const Submission& stalled) { tail.shaders = shaders; 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(); timing.Finish(); execute(tail); } -void Driver::Submit(const Packet* packet, std::uint32_t queue) { +void Driver::SubmitFlip(const FlipInfo& info) { + const auto argument = static_cast(info.argument); + std::array words{FlipPacketHeader, info.handle, static_cast(info.index), info.mode, static_cast(argument), static_cast(argument >> 32u)}; + const Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Submit(&packet, 0, true); +} + +void Driver::Submit(const Packet* packet, std::uint32_t queue, bool cpuFlip) { 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); + static const bool flipsInOrder = std::getenv("APS5_CPU_FLIP_IN_ORDER") != nullptr || std::getenv("APS5_SYNC_FLIP") != nullptr; + const bool unorderedFlip = cpuFlip && !flipsInOrder && !APS5_ENABLE_TIMING_LOG && !submitsQueue0(); + if (queue == 0 && !cpuFlip) submitsQueue0() = true; GuestMemory::CheckRange(packet, sizeof(Packet), alignof(Packet)); const auto descriptor = *packet; require(descriptor.flags == 0, "nonzero submission flags are not implemented"); @@ -250,23 +359,39 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) { if (APS5_ENABLE_TIMING_LOG) submission.validatedAt = std::chrono::steady_clock::now(); waitForFlipRoom(submission); holdFlipBehindWorker(submission); + AwaitRegistrationPreparation(); if (APS5_ENABLE_TIMING_LOG) submission.roomReadyAt = std::chrono::steady_clock::now(); static const bool trace = std::getenv("APS5_TRACE_GPU") != nullptr; if (trace) std::fprintf(stderr, "[gpu] %.1f submit queue=0x%x dwords=%zu at %p\n", TraceMs(), queue, submission.commands.size(), static_cast(descriptor.addr)); const auto validated = profile ? std::chrono::steady_clock::now() : start; + auto* const capture = SubmissionCapture::Active(); + auto captured = capture != nullptr ? SubmissionCapture::Prepare(queue, SubmissionKind::Submit, submission.commands, submission.conditionalEnds) : CapturedSubmission{}; { - std::lock_guard lock(mutex); + std::unique_lock lock(mutex); rethrowFailure(); checkStopping(); require(accepted != std::numeric_limits::max(), "submission serial overflow"); reserveOutputs(submission); + if (unorderedFlip && capture == nullptr && accepted != 0 && flipsAhead.load(std::memory_order_acquire) == 0) { + const auto now = FrameTiming::Clock::now(); + auto frame = std::make_shared(++frameSerial); + frame->IncludeSubmission(accepted, now, now, now, true); + frame->SetFlip(accepted, 0, now, now); + ++flipsCounted; + submission.flips.begin()->second->GpuReady(frame); + lock.unlock(); + AgcDriverDeliverEopInterrupt(0); + return; + } submission.shaders = shaders; submission.serial = accepted + 1; submission.received = ++eventSerial; + 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) { - const auto now = std::chrono::steady_clock::now(); - submission.enqueuedAt = now; ++costs.submissions; costs.validateNs += static_cast(std::chrono::duration_cast(validated - copied).count()); costs.copyNs += static_cast(std::chrono::duration_cast((copied - start) + (now - validated)).count()); @@ -280,11 +405,15 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) { ++accepted; } changed.notify_all(); + if (capture != nullptr) writeCaptured(*this, *capture, captured); } void Driver::SuspendPoint() { const auto receivedAt = APS5_ENABLE_TIMING_LOG ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; require(!onWorkerThread(), "worker cannot suspend itself"); + submitsQueue0() = true; + auto* const capture = SubmissionCapture::Active(); + auto captured = capture != nullptr ? SubmissionCapture::Prepare(0, SubmissionKind::Suspend, {}, {}) : CapturedSubmission{}; std::unique_lock lock(mutex); rethrowFailure(); checkStopping(); @@ -296,10 +425,14 @@ void Driver::SuspendPoint() { boundary.queue = 0; boundary.receivedAt = receivedAt; boundary.enqueuedAt = std::chrono::steady_clock::now(); + if (capture != nullptr) capture->Take(captured, boundary.serial, captureTime(), boundary.heldAtSubmit); enqueue(std::move(boundary)); ++accepted; changed.notify_all(); + if (capture == nullptr) return; + lock.unlock(); + writeCaptured(*this, *capture, captured); } bool Driver::waitFree(const Submission& submission) { @@ -314,6 +447,7 @@ bool Driver::waitFree(const Submission& submission) { bool Driver::queue0Before(std::uint64_t received) const { if (queue0Executing != 0 && queue0Executing < received) return true; + if (!queue0Uncommitted.empty() && queue0Uncommitted.front() < received) return true; const auto worker = workers.find(0); if (worker == workers.end()) return false; for (const auto& pending : worker->second.pending) { @@ -380,8 +514,9 @@ void Driver::noteHeldAtSubmit(Submission& submission, std::size_t cursor) { return; } if ((opcode != 0x3c && opcode != 0x93) || packet.size() < 7 || ((packet[1] >> 4u) & 3u) != 1u) return; - const auto address = packet[2] | (static_cast(packet[3]) << 32u); - const std::size_t bytes = Pm4::WaitAwaitedBytes(packet); + const auto wait = Pm4::DecodeWait(packet); + const auto address = wait.address; + const std::size_t bytes = wait.bytes; if (address % 4 != 0 || !GuestMemory::Accessible(reinterpret_cast(address), bytes)) return; const auto worker = workers.find(submission.queue); for (std::size_t offset = 0; offset < bytes; offset += 4) { @@ -395,7 +530,11 @@ void Driver::noteHeldAtSubmit(Submission& submission, std::size_t cursor) { } void Driver::forgetUnfinishedWrites(QueueWorker& worker, const Submission& submission) { - for (const auto dword : submission.labelWrites) { + forgetUnfinishedWrites(worker, submission.labelWrites); +} + +void Driver::forgetUnfinishedWrites(QueueWorker& worker, std::span labelWrites) { + for (const auto dword : labelWrites) { const auto found = worker.unfinishedWrites.find(dword); if (found == worker.unfinishedWrites.end()) continue; if (--found->second == 0) worker.unfinishedWrites.erase(found); 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/Queues/WorkerAffinity.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp index 0bac6c5eb9..792b920c92 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Queues/WorkerAffinity.hpp" #include "prx/libSceAgcDriver/Execution/include/TracyZone.hpp" #include "prx/libc/include/CpuTopology.hpp" +#include "prx/libc/include/general/ThreadName.hpp" #include namespace AgcDriver::DriverDetail { @@ -22,6 +23,10 @@ std::uint64_t WorkerAffinityMask() { void PinWorkerThread(const char* role) { TracyZone::NameThread(role); +#ifdef _WIN32 + const HRESULT nameResult = ThreadName::SetCurrent(role); + if (FAILED(nameResult)) std::fprintf(stderr, "[thread-name] could not name %s (0x%08lx)\n", role, static_cast(nameResult)); +#endif const auto mask = WorkerAffinityMask(); if (mask == 0) return; static std::once_flag summary; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp index 5fac827511..52f65cc2d0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp @@ -49,16 +49,17 @@ void Driver::reportSkip(const char* kind, const std::string& what) { if (!line.empty()) std::fwrite(line.data(), 1, line.size(), stderr); } -std::string Driver::dumpRequest(std::uint64_t address, const ShaderRecompiler::RecompileRequest& request) { +std::string Driver::dumpRequest(std::uint64_t address, const ShaderRecompiler::RecompileRequest& request, std::string_view suffix) { static std::mutex dumpMutex; - static std::set dumped; - char name[64]; - std::snprintf(name, sizeof(name), "shader_%llx.req", static_cast(address)); + static std::set dumped; + char hex[32]; + std::snprintf(hex, sizeof(hex), "%llx", static_cast(address)); + const std::string name = "shader_" + std::string(hex) + std::string(suffix) + ".req"; std::lock_guard lock(dumpMutex); - if (!dumped.insert(address).second) return name; + if (!dumped.insert(name).second) return name; try { const auto text = ShaderRecompiler::RequestSerializer{}.Serialize(request); - if (std::FILE* file = std::fopen(name, "wb")) { + if (std::FILE* file = std::fopen(name.c_str(), "wb")) { std::fwrite(text.data(), 1, text.size(), file); std::fclose(file); } 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 a57ea0b88c..5f93b08490 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -2,6 +2,7 @@ #include "ControlFlow/GraphBuilder.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -9,24 +10,45 @@ #include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp" #include "CompiledVariant.hpp" +#include "Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" #include +#include #include +#include +#include #include #include #include +#include #include #include #include #include #include #include +#include #include +#include +#include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#endif +#ifdef __GLIBC__ +#include +#endif +#include #include #include #include +#include "Optimization/ShaderStageInputInfo.hpp" +#include #include "RdnaDecoder/RdnaInstructionDecoder.hpp" namespace AgcDriver::DriverDetail { @@ -40,7 +62,7 @@ std::shared_ptr PrepareShaderWithDiagnosti } catch (...) { failure = std::current_exception(); try { - if (std::getenv("APS5_DUMP_SHADERS") != nullptr) dump = Driver::dumpRequest(request.shader.codeAddress, request); + if (std::getenv("APS5_DUMP_SHADERS") != nullptr) dump = Driver::dumpRequest(request.shader.codeAddress, request, ".prepare"); } catch (...) {} } try { @@ -55,6 +77,11 @@ std::shared_ptr PrepareShaderWithDiagnosti } std::shared_ptr ReadRawShader(std::uint64_t address) { + const auto drain = [](std::uint64_t first, std::size_t bytes) { + if (!DrawPipeline::Active()) return; + auto& pipeline = DrawPipeline::Queue0(); + if (pipeline.Busy() && pipeline.Overlaps(first, bytes)) pipeline.Drain(DrawPipeline::DrainReason::Capture); + }; GuestMemory::CheckRange(reinterpret_cast(address), sizeof(std::uint32_t), 256); static std::mutex cacheMutex; static std::list> cache; @@ -67,6 +94,7 @@ std::shared_ptr ReadRawShader(std::uint64_t address) { } if (cached) { const auto code = std::as_bytes(std::span(cached->code)); + drain(address, code.size()); GuestMemory::FlushGpuWrites(address, code.size()); if (GuestMemory::CompareMapped(address, code) == GuestMemory::Compare::Equal) { std::lock_guard lock(cacheMutex); @@ -87,6 +115,7 @@ std::shared_ptr ReadRawShader(std::uint64_t address) { while (snapshot.code.size() < available) { const auto previous = snapshot.code.size(); snapshot.code.resize(std::min(available, std::max(64, previous * 2))); + drain(address + previous * sizeof(std::uint32_t), (snapshot.code.size() - previous) * sizeof(std::uint32_t)); GuestMemory::Read(address + previous * sizeof(std::uint32_t), std::as_writable_bytes(std::span(snapshot.code).subspan(previous)), alignof(std::uint32_t)); try { @@ -126,22 +155,43 @@ std::shared_ptr ReadUserDataTailContinuation(std::span(userData.size())); if (!tail) return nullptr; const auto slot = tail->userDataRegister - userDataBase; - const auto address = userData[slot] | (static_cast(userData[slot + 1u]) << 32u); + const auto high = tail->constantHigh ? *tail->constantHigh : userData[slot + 1u]; + const auto address = userData[slot] | (static_cast(high) << 32u); require(address != 0 && (address & 3u) == 0u, "user-data tail continuation address is null or not dword aligned"); return ReadRawShader(address); } std::shared_ptr ProgramSnapshot(const ShaderRegistry& shaders, std::uint64_t address, std::initializer_list types) { if (auto registered = RegisteredProgram(shaders, address, types)) return registered; - require(shaders.empty(), "graphics program does not belong to a compatible registered shader"); return ReadRawShader(address); } namespace { std::atomic rawGraphicsDrawn{false}; +std::mutex rawGraphicsMutex; +std::map rawGraphicsRanges; } -void NoteRawGraphicsDraw() { +void NoteRawGraphicsDraw(const ShaderSnapshot& snapshot) { + if (snapshot.rawGraphicsNoted->load(std::memory_order_acquire)) return; + auto begin = snapshot.codeAddress; + auto end = snapshot.codeAddress + snapshot.code.size() * sizeof(std::uint32_t); + { + std::lock_guard lock(rawGraphicsMutex); + auto next = rawGraphicsRanges.upper_bound(begin); + if (next != rawGraphicsRanges.begin() && std::prev(next)->second >= begin) { + const auto previous = std::prev(next); + begin = previous->first; + end = std::max(end, previous->second); + rawGraphicsRanges.erase(previous); + } + while (next != rawGraphicsRanges.end() && next->first <= end) { + end = std::max(end, next->second); + next = rawGraphicsRanges.erase(next); + } + rawGraphicsRanges.emplace(begin, end); + } + snapshot.rawGraphicsNoted->store(true, std::memory_order_release); rawGraphicsDrawn.store(true, std::memory_order_relaxed); } @@ -149,6 +199,13 @@ bool RawGraphicsDrawn() { return rawGraphicsDrawn.load(std::memory_order_relaxed); } +bool RawGraphicsDrawnIn(std::uint64_t begin, std::uint64_t end) { + if (begin >= end) return false; + std::lock_guard lock(rawGraphicsMutex); + const auto next = rawGraphicsRanges.lower_bound(end); + return next != rawGraphicsRanges.begin() && std::prev(next)->second > begin; +} + namespace { void AwaitRegisteredPreparation(PreparedShaders& prepared, std::unique_lock& lock) { @@ -189,10 +246,15 @@ std::uint32_t PositionExportLayout(const ShaderSnapshot* snapshot, std::size_t c const auto use = snapshot->positionVectorUse->load(std::memory_order_acquire); if (use != Unknown) return use == Used ? bits : 0; } - const auto decoded = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code); - const bool used = std::any_of(decoded.instructions.begin(), decoded.instructions.end(), [](const auto& instruction) { - return instruction.family == ShaderRecompiler::RdnaInstructionFamily::EXP && instruction.exportTarget >= 0x0du && instruction.exportTarget <= 0x0fu && instruction.exportEnableMask != 0; - }); + bool used = true; + try { + const auto decoded = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code); + used = std::any_of(decoded.instructions.begin(), decoded.instructions.end(), [](const auto& instruction) { + return instruction.family == ShaderRecompiler::RdnaInstructionFamily::EXP && instruction.exportTarget >= 0x0du && instruction.exportTarget <= 0x0fu && instruction.exportEnableMask != 0; + }); + } catch (const std::invalid_argument& error) { + if (std::strstr(error.what(), "opcode is not implemented") == nullptr) throw; + } if (cached) snapshot->positionVectorUse->store(used ? Used : Unused, std::memory_order_release); return used ? bits : 0; } @@ -318,6 +380,7 @@ void ShaderPreparationTransaction::Commit() { destination.rectangleProgress.swap(change.prepared.rectangleProgress); destination.fragments.swap(change.prepared.fragments); std::swap(destination.rectangleRequested, change.prepared.rectangleRequested); + destination.deferred = destination.deferred || change.prepared.deferred; } } committed = true; @@ -362,6 +425,36 @@ static bool RelocatedRegisters(const ShaderSnapshot& snapshot, Shader used, cons return std::memcmp(static_cast(bound), snapshot.header.data() + (address - snapshot.headerAddress), bytes) == 0; } +// A title can relocate a registered header, and the arrays it points at, into memory of its own and resolve the ABI +// with that copy. Every array the copy refers to must still hold the registered bytes: the registered header then +// decodes to the values the copy describes. +static bool ReferredBytesMatch(const ShaderSnapshot& snapshot, const void* pointer, const void* registeredPointer, std::size_t bytes) { + if (bytes == 0 || pointer == registeredPointer) return true; + if (pointer == nullptr || registeredPointer == nullptr) return false; + const auto address = reinterpret_cast(registeredPointer); + if (address < snapshot.headerAddress) return false; + const auto offset = address - snapshot.headerAddress; + if (offset > snapshot.header.size() || snapshot.header.size() - offset < bytes) return false; + if (!GuestMemory::Accessible(pointer, bytes)) return false; + return std::memcmp(static_cast(pointer), snapshot.header.data() + offset, bytes) == 0; +} + +static bool RelocatedCopy(const ShaderSnapshot& snapshot, Shader used, const Shader& registered) { + Shader masked = used; + masked.user_data = registered.user_data; + masked.cx_registers = registered.cx_registers; + masked.sh_registers = registered.sh_registers; + masked.specials = registered.specials; + masked.input_semantics = registered.input_semantics; + masked.output_semantics = registered.output_semantics; + if (std::memcmp(&masked, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) != 0) return false; + return ReferredBytesMatch(snapshot, used.sh_registers, registered.sh_registers, used.num_sh_registers * sizeof(ShaderRegister)) + && ReferredBytesMatch(snapshot, used.cx_registers, registered.cx_registers, used.num_cx_registers * sizeof(ShaderRegister)) + && ReferredBytesMatch(snapshot, used.specials, registered.specials, sizeof(ShaderSpecialRegs)) + && ReferredBytesMatch(snapshot, used.input_semantics, registered.input_semantics, used.num_input_semantics * sizeof(ShaderSemantic)) + && ReferredBytesMatch(snapshot, used.output_semantics, registered.output_semantics, used.num_output_semantics * sizeof(ShaderSemantic)); +} + bool SameHeader(const ShaderSnapshot& snapshot, const Shader* shader) { if (snapshot.headerAddress == reinterpret_cast(shader)) return true; if (snapshot.header.size() < sizeof(Shader)) return false; @@ -371,7 +464,73 @@ bool SameHeader(const ShaderSnapshot& snapshot, const Shader* shader) { std::memcpy(&used, static_cast(shader), sizeof(Shader)); std::memcpy(®istered, snapshot.header.data(), sizeof(Shader)); used.user_data = registered.user_data; - return std::memcmp(&used, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) == 0 || RelocatedRegisters(snapshot, used, registered) || FusedHullHeader(snapshot, used, registered); + return std::memcmp(&used, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) == 0 || RelocatedRegisters(snapshot, used, registered) || RelocatedCopy(snapshot, used, registered) || FusedHullHeader(snapshot, used, registered); +} + +std::size_t ShaderRegistry::chunkFor(std::uint64_t address) const { + const auto above = std::upper_bound(chunks.begin(), chunks.end(), address, [](std::uint64_t key, const std::shared_ptr& chunk) { return key < chunk->begin()->first; }); + return above == chunks.begin() ? 0u : static_cast(above - chunks.begin()) - 1u; +} + +ShaderRegistry::Chunk& ShaderRegistry::writable(std::size_t index) { + auto& chunk = chunks[index]; + if (chunk.use_count() != 1) chunk = std::make_shared(*chunk); + return *chunk; +} + +ShaderRegistry::const_iterator ShaderRegistry::find(std::uint64_t address) const { + if (chunks.empty()) return end(); + const auto index = chunkFor(address); + const auto found = chunks[index]->find(address); + if (found == chunks[index]->end()) return end(); + return const_iterator(chunks.cbegin() + static_cast(index), chunks.cend(), found); +} + +ShaderRegistry::const_iterator ShaderRegistry::upper_bound(std::uint64_t address) const { + if (chunks.empty()) return end(); + const auto index = chunkFor(address); + const auto found = chunks[index]->upper_bound(address); + if (found != chunks[index]->end()) return const_iterator(chunks.cbegin() + static_cast(index), chunks.cend(), found); + if (index + 1u == chunks.size()) return end(); + return const_iterator(chunks.cbegin() + static_cast(index + 1u), chunks.cend(), chunks[index + 1u]->cbegin()); +} + +const ShaderRegistry::mapped_type& ShaderRegistry::at(std::uint64_t address) const { + const auto found = find(address); + if (found == end()) throw std::out_of_range("shader registry has no entry for the code address"); + return found->second; +} + +ShaderRegistry::mapped_type& ShaderRegistry::operator[](std::uint64_t address) { + if (chunks.empty()) chunks.push_back(std::make_shared()); + const auto index = chunks.front()->empty() ? 0u : chunkFor(address); + auto& chunk = writable(index); + const auto [entry, inserted] = chunk.try_emplace(address); + auto& headers = entry->second; + if (!inserted) return headers; + ++count; + if (chunk.size() > ChunkEntries) { + auto upper = std::make_shared(); + for (auto moved = std::next(chunk.begin(), static_cast(chunk.size() / 2u)); moved != chunk.end();) upper->insert(upper->end(), chunk.extract(moved++)); + chunks.insert(chunks.begin() + static_cast(index + 1u), std::move(upper)); + } + return headers; +} + +std::pair ShaderRegistry::emplace(std::uint64_t address, mapped_type headers) { + if (const auto found = find(address); found != end()) return {found, false}; + (*this)[address] = std::move(headers); + return {find(address), true}; +} + +std::size_t ShaderRegistry::erase(std::uint64_t address) { + if (!contains(address)) return 0; + const auto index = chunkFor(address); + auto& chunk = writable(index); + chunk.erase(address); + --count; + if (chunk.empty()) chunks.erase(chunks.begin() + static_cast(index)); + return 1; } std::shared_ptr RegisteredHeader(const ShaderRegistry& registry, const Shader* shader) { @@ -433,6 +592,10 @@ void PublishRegisteredShader(std::shared_ptr& registry, const st namespace { void ReportPreparedAtUse(const ShaderSnapshot& snapshot, const ShaderRecompiler::RecompileRequest& request) { + if (request.context.convertedSamples) { + APS5_LOG_OUT_DEBUG("Shader 0x%llx samples a converted image; preparing its converted-sample variant at use", static_cast(request.shader.codeAddress)); + return; + } if (snapshot.codeAddress == NullPixelProgramAddress() && request.shader.stage == ShaderRecompiler::ShaderStage::Fragment && request.context.waveSize == 32u) { APS5_LOG_ERR("The null pixel program has no wave%u artifact for this draw; preparing it at draw", request.context.waveSize); return; @@ -496,8 +659,9 @@ ShaderRecompiler::RectListShaders DrawRectangle(const ShaderSnapshot& front, con namespace { -std::vector LinkVertexUserDataTail(const ShaderRecompiler::RecompileRequest& request, const ShaderSnapshot& snapshot) { - if (request.shader.stage != ShaderRecompiler::ShaderStage::Vertex || !request.shader.header.empty() || !snapshot.userDataTailRegister) return {}; +std::vector LinkGraphicsUserDataTail(const ShaderRecompiler::RecompileRequest& request, const ShaderSnapshot& snapshot) { + using Stage = ShaderRecompiler::ShaderStage; + if ((request.shader.stage != Stage::Vertex && request.shader.stage != Stage::Fragment) || !request.shader.header.empty() || !snapshot.userDataTailRegister) return {}; const auto program = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(request.shader.code); const auto tail = ShaderRecompiler::FindUserDataTail(program, request.context.userDataBaseRegister, static_cast(request.context.userData.size())); if (!tail) return {}; @@ -531,14 +695,48 @@ std::vector ReadUserDataCallee(std::uint64_t address, std::uint32 throw std::runtime_error("its callee at " + hexText(address) + " does not return within " + std::to_string(limitWords * 4u) + " bytes"); } -std::vector LinkUserDataCallees(const ShaderRecompiler::RecompileRequest& request) { +std::vector ReadGuestBytes(std::uint64_t address, std::uint64_t bytes, const char* what) { + if (address > UINTPTR_MAX - bytes || !GuestMemory::Accessible(reinterpret_cast(static_cast(address)), static_cast(bytes))) { + throw std::runtime_error(std::string("its ") + what + " at " + hexText(address) + " + " + hexText(bytes) + " is not readable"); + } + std::vector data(static_cast(bytes)); + GuestMemory::Read(address, std::as_writable_bytes(std::span(data))); + return data; +} + +std::vector ReadTableCallees(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::TableCall& call) { + constexpr std::uint64_t limitBytes = 64u << 20u; + constexpr std::size_t limitCallees = 64u; + const auto slot = call.userDataRegister - request.context.userDataBaseRegister; + const auto pointer = call.constantPointer ? *call.constantPointer : static_cast(request.context.userData[slot]) | static_cast(request.context.userData[slot + 1u]) << 32u; + const auto words = ReadGuestBytes(pointer + call.descriptorOffset, 16u, "call table descriptor"); + ShaderRecompiler::ShaderBufferResource descriptor{}; + std::memcpy(descriptor.fields.data(), words.data(), words.size()); + const auto bytes = descriptor.GetSize(); + if (bytes > limitBytes) throw std::runtime_error("its call table of " + std::to_string(bytes) + " bytes is larger than the " + std::to_string(limitBytes) + " bytes that are searched for callees"); + const auto table = bytes == 0u ? std::vector{} : ReadGuestBytes(descriptor.Base48(), bytes, "call table"); + std::set targets; + for (std::uint64_t offset = call.entryOffset; offset + 8u <= bytes; offset += call.entryStride) { + std::uint64_t target = 0; + std::memcpy(&target, table.data() + offset, sizeof(target)); + if (target != 0u) targets.insert(target); + if (call.entryStride == 0u) break; + } + if (targets.size() > limitCallees) throw std::runtime_error("its call table holds " + std::to_string(targets.size()) + " distinct targets; at most " + std::to_string(limitCallees) + " are linked"); + std::vector callees; + for (const auto target : targets) callees.push_back({target, ReadUserDataCallee(target, call.linkRegister)}); + return callees; +} + +ShaderRecompiler::LinkedCalls LinkUserDataCallees(const ShaderRecompiler::RecompileRequest& request) { if (request.shader.stage != ShaderRecompiler::ShaderStage::Compute) return {}; const auto code = request.shader.code; if (std::none_of(code.begin(), code.end(), [](std::uint32_t word) { return (word & 0xff80ff00u) == 0xbe802100u; })) return {}; const auto program = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code); - const ShaderRecompiler::SwappcInfo swappc{false, request.context.userDataBaseRegister, static_cast(request.context.userData.size())}; + const ShaderRecompiler::SwappcInfo swappc{false, request.context.userDataBaseRegister, static_cast(request.context.userData.size()), request.context.waveSize}; const auto calls = ShaderRecompiler::FindUserDataCalls(program, swappc); - if (calls.empty()) return {}; + const auto tableCalls = ShaderRecompiler::FindTableCalls(program, swappc); + if (calls.empty() && tableCalls.empty()) return {}; std::vector> callees; for (const auto& call : calls) { const auto slot = call.userDataRegister - swappc.userDataBaseRegister; @@ -550,7 +748,15 @@ std::vector LinkUserDataCallees(const ShaderRecompiler::Recompile std::to_string(call.userDataRegister + 1u) + "] at program counter " + hexText(call.programCounter) + ", but " + error.what()); } } - return ShaderRecompiler::LinkUserDataCalls(program, calls, callees); + std::vector> tableCallees; + for (const auto& call : tableCalls) { + try { + tableCallees.push_back(ReadTableCallees(request, call)); + } catch (const std::exception& error) { + throw std::runtime_error("AGC driver: compute shader " + hexText(request.shader.codeAddress) + " calls a target loaded from a table at program counter " + hexText(call.programCounter) + ", but " + error.what()); + } + } + return ShaderRecompiler::LinkCalls(program, calls, callees, tableCalls, tableCallees); } } @@ -560,19 +766,23 @@ ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& s const auto code = std::span(snapshot.code).subspan(codeOffset); require(request.shader.code.data() == code.data() && request.shader.code.size() == code.size(), "prepared invocation does not refer to registered code"); auto linked = LinkUserDataCallees(request); - if (linked.empty()) linked = LinkVertexUserDataTail(request, snapshot); + if (linked.code.empty()) linked.code = LinkGraphicsUserDataTail(request, snapshot); auto source = request; - if (!linked.empty()) source.shader.code = linked; + if (!linked.code.empty()) { + source.shader.code = linked.code; + source.context.linkedCallFallbacks = linked.fallbacks; + } + std::unique_lock lock(snapshot.prepared->mutex); + AwaitRegisteredPreparation(*snapshot.prepared, lock); + if (std::ranges::find(snapshot.prepared->convertedSamples, codeOffset) != snapshot.prepared->convertedSamples.end()) source.context.convertedSamples = true; auto invocationRequest = source; struct PreparedKeyStorage {}; auto& key = HostThreadLocal, PreparedKeyStorage>(); ShaderRecompiler::BuildPreparedShaderKey(source, key); - std::unique_lock lock(snapshot.prepared->mutex); - AwaitRegisteredPreparation(*snapshot.prepared, lock); for (const auto& entry : snapshot.prepared->entries) { if (entry.codeOffset != codeOffset) continue; invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*entry.handle); - if (!linked.empty() && !std::ranges::equal(invocationRequest.shader.code, linked)) continue; + if (!linked.code.empty() && !std::ranges::equal(invocationRequest.shader.code, linked.code)) continue; if (auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(invocationRequest, entry.handle, key)) return std::move(*invocation); } ReportPreparedAtUse(snapshot, source); @@ -584,23 +794,51 @@ ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& s snapshot.prepared->entries.push_back({codeOffset, std::move(handle)}); return std::move(*invocation); } -std::optional RegisteredFloatMode(const ShaderSnapshot& snapshot) { - if (snapshot.registeredState == nullptr) return std::nullopt; + +std::shared_ptr CaptureInvocation(ShaderMemory& memory, const ShaderSnapshot& snapshot, std::size_t codeOffset, const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::PreparedShaderInvocation& invocation) { + try { + return memory.Capture(invocation); + } catch (const ShaderRecompiler::ConvertedSampleRequired&) { + if (invocation.Request().context.convertedSamples) throw; + } + { + std::lock_guard lock(snapshot.prepared->mutex); + auto& offsets = snapshot.prepared->convertedSamples; + if (std::ranges::find(offsets, codeOffset) == offsets.end()) offsets.push_back(codeOffset); + } + invocation = InvocationFor(snapshot, codeOffset, request); + return memory.Capture(invocation); +} +namespace { + +std::optional DecodeFloatMode(const Registers& registers, std::uint8_t type) { std::uint32_t rsrc1; std::uint32_t fp16OverflowBit; - switch (snapshot.type) { + switch (type) { case 0: rsrc1 = 0x212; fp16OverflowBit = 26; break; case 1: rsrc1 = 0x00a; fp16OverflowBit = 29; break; case 2: case 4: case 6: rsrc1 = 0x08a; fp16OverflowBit = 31; break; - case 5: case 7: rsrc1 = 0x10a; fp16OverflowBit = 30; break; + case 3: case 5: case 7: rsrc1 = 0x10a; fp16OverflowBit = 30; break; default: return std::nullopt; } - const auto found = snapshot.registeredState->shader.find(rsrc1); - if (found == snapshot.registeredState->shader.end()) return std::nullopt; + const auto found = registers.find(rsrc1); + if (found == registers.end()) return std::nullopt; const auto value = found->second; return ShaderRecompiler::ShaderFloatMode{(value >> 12u) & 0xffu, ((value >> 21u) & 1u) != 0u, ((value >> 23u) & 1u) != 0u, ((value >> fp16OverflowBit) & 1u) != 0u}; } +} + +std::optional RegisteredFloatMode(const ShaderSnapshot& snapshot) { + if (snapshot.registeredState == nullptr) return std::nullopt; + return DecodeFloatMode(snapshot.registeredState->shader, snapshot.type); +} + +std::optional DispatchFloatMode(const ShaderSnapshot& snapshot, const Registers& live) { + if (snapshot.registeredState != nullptr) return RegisteredFloatMode(snapshot); + return DecodeFloatMode(live, snapshot.type); +} + namespace { template @@ -657,7 +895,7 @@ void BuildRegisteredAbiKey(const QueueState& state, const VulkanDevice& device, struct RegisteredPreparation { std::size_t codeOffset = 0; - ShaderRecompiler::RdnaProgram decoded; + bool registration = false; ShaderRecompiler::SwappcInfo swappc{}; std::vector userData; std::array memory{}; @@ -678,7 +916,7 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap case 1: stage = Stage::Fragment; programRegister = 0x008; resourceRegister = 0x00b; break; case 2: stage = Stage::Vertex; programRegister = 0x0c8; resourceRegister = 0x08b; firstUser = 8; break; case 4: stage = Stage::Mesh; programRegister = 0x0c8; resourceRegister = 0x08b; break; - case 5: return {}; + case 3: case 5: return {}; case 6: stage = Stage::Mesh; programRegister = 0x088; resourceRegister = 0x08b; break; case 7: stage = Stage::TessellationControl; programRegister = 0x108; resourceRegister = 0x10b; break; default: @@ -702,8 +940,10 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap const auto code = std::span(snapshot.code).subspan(codeOffset); auto plan = std::make_unique(); plan->codeOffset = codeOffset; + ShaderRecompiler::RdnaProgram decoded; + plan->registration = registration; try { - plan->decoded = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code); + decoded = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code); } catch (const ShaderRecompiler::UnsupportedInstructionError& error) { if (deferred == nullptr) throw; return defer(error.what()); @@ -729,11 +969,19 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap const auto count = RegisterValue(state.context, 0x1b6) & 0x3fu; if (registration && count != 0) return nullptr; pixel = Graphics::DecodePixelStageInfo(state.context, {}); + const auto& capabilities = device.Target().supportedCapabilities; + if (pixel->stencilRefExportEnable && std::ranges::find(capabilities, static_cast(spv::CapabilityStencilExportEXT)) == capabilities.end()) return nullptr; wave = pixel->wave32 ? 32u : 64u; } else { const auto routing = RegisterValue(state.context, 0x2d5); wave = (routing & 0x00400000u) != 0 ? 32u : 64u; - if (registration && stage == Stage::Vertex && (routing & 0x02002024u) == 0x2000u && ProgramUsesWorkgroup(snapshot, codeOffset)) return nullptr; + const bool unroutedBarrier = routing == 0 && std::ranges::any_of(decoded.instructions, [](const auto& instruction) { + return instruction.op == ShaderRecompiler::RdnaOpcode::SBarrier; + }); + if (registration && stage == Stage::Vertex && (((routing & 0x02002024u) == 0x2000u && ProgramUsesWorkgroup(snapshot, codeOffset)) || unroutedBarrier)) { + if (unroutedBarrier && deferred != nullptr) return defer("its s_barrier depends on draw routing"); + return nullptr; + } if ((routing & 0x20u) != 0 || stage == Stage::Mesh || (routing & 4u) != 0 || ((routing >> 3u) & 3u) == 1u) { if (registration) return {}; auto stageState = state; @@ -753,7 +1001,7 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap } plan->swappc = {vertex.has_value(), firstUser, userCount}; if (deferred != nullptr) { - if (const auto counter = ShaderRecompiler::UnresolvableSwappcTarget(plan->decoded, &plan->swappc)) { + if (const auto counter = ShaderRecompiler::UnresolvableSwappcTarget(decoded, &plan->swappc)) { char reason[96]; std::snprintf(reason, sizeof(reason), "its s_swappc_b64 at program counter 0x%x calls a computed target", *counter); return defer(reason); @@ -767,8 +1015,16 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap return plan; } +bool LibraryPrebuildDisabled() { + static const bool disabled = [] { + const auto* value = std::getenv("APS5_NO_LIBRARY_PREBUILD"); + return value != nullptr && std::strcmp(value, "1") == 0; + }(); + return disabled; +} + void PrebuildStageLibrary(const std::shared_ptr& device, const Graphics::ShaderStages& stages, ShaderRecompiler::ShaderStage stage, std::shared_ptr result, std::uint32_t pushOffset) { - if (result == nullptr || (stage != ShaderRecompiler::ShaderStage::Vertex && stage != ShaderRecompiler::ShaderStage::Fragment)) return; + if (LibraryPrebuildDisabled() || result == nullptr || (stage != ShaderRecompiler::ShaderStage::Vertex && stage != ShaderRecompiler::ShaderStage::Fragment)) return; SubmitLibraryPrebuild([device, stages, stage, result = std::move(result), pushOffset] { const std::array shaders{{{stage, result.get(), pushOffset}}}; try { @@ -788,7 +1044,7 @@ std::vector PrepareRegistered(RegisteredPreparation& pla entries.push_back({plan.codeOffset, PrepareShaderWithDiagnostics(request, &preparation)}); }; append(); - if (plan.prebuildDevice != nullptr && request.target.registrationModules && request.shader.stage == ShaderRecompiler::ShaderStage::Fragment) { + if (plan.prebuildDevice != nullptr && request.target.registrationModules && request.shader.stage == ShaderRecompiler::ShaderStage::Fragment && !LibraryPrebuildDisabled()) { try { PrebuildStageLibrary(plan.prebuildDevice, {Graphics::ShaderPath::Vertex, 0u, 32u, request.context.waveSize, {}, {}}, request.shader.stage, ShaderRecompiler::RegistrationResult(request, *entries.front().handle), request.layout.pushConstantOffsetBytes); } catch (const std::exception& error) { @@ -802,6 +1058,11 @@ std::vector PrepareRegistered(RegisteredPreparation& pla request.layout.pushConstantSizeBytes = 0; append(); } + if (plan.registration) { + for (const auto& entry : entries) { + if (entry.handle != nullptr) ShaderRecompiler::ReleaseUndrawnResourcePlan(*entry.handle); + } + } return entries; } @@ -810,11 +1071,45 @@ std::vector PrepareRegistered(const ShaderSnapshot& snap return plan != nullptr ? PrepareRegistered(*plan) : std::vector{}; } +void DeferPipelineStage(PreparedShaderState& prepared, const ShaderSnapshot& snapshot, const char* reason) { + if (!prepared.deferred) APS5_LOG_ERR("Shader 0x%llx could not be prepared when the title created its pipeline (%s); preparing it when it is used", static_cast(snapshot.codeAddress), reason); + prepared.deferred = true; +} + +std::optional FailedRegistration(const ShaderSnapshot& snapshot) { + std::unique_lock lock(snapshot.prepared->mutex); + try { + AwaitRegisteredPreparation(*snapshot.prepared, lock); + } catch (const std::exception& error) { + return error.what(); + } + return std::nullopt; +} + +bool DeferFailedRegistration(const ShaderSnapshot& snapshot) { + const auto failure = FailedRegistration(snapshot); + if (!failure) return false; + std::lock_guard lock(snapshot.prepared->mutex); + DeferPipelineStage(*snapshot.prepared, snapshot, failure->c_str()); + return true; +} + +#ifdef __GLIBC__ +void trimFreedMemory() { + static std::atomic last{0}; + const auto now = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); + auto previous = last.load(std::memory_order_relaxed); + if (now - previous < 1000 || !last.compare_exchange_strong(previous, now, std::memory_order_relaxed)) return; + malloc_trim(0); +} +#endif + 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; }(); @@ -828,7 +1123,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; }(); @@ -839,7 +1135,32 @@ 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()}; + } + if (pools[0] != nullptr) pools[0]->Close(false); + if (pools[1] != nullptr) pools[1]->Close(true); + } + 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; @@ -856,8 +1177,10 @@ public: { std::unique_lock lock(mutex); if (threads != 0 && !closed) { - if (prepared != nullptr && prepared->demanded.load()) tasks.push_front({std::move(task), prepared}); - else tasks.push_back({std::move(task), prepared}); + const auto sequence = ++submitted; + if (prepared != nullptr && prepared->demanded.load()) tasks.push_front({std::move(task), prepared, sequence}); + else tasks.push_back({std::move(task), prepared, sequence}); + unfinished.insert(sequence); lock.unlock(); ready.notify_one(); return; @@ -874,27 +1197,85 @@ public: std::rotate(tasks.begin(), found, std::next(found)); } - void Close() { + void AwaitSubmitted() { std::unique_lock lock(mutex); - closed = true; - idle.wait(lock, [&] { return tasks.empty() && running == 0; }); + const auto ticket = submitted; + idle.wait(lock, [&] { return unfinished.empty() || *unfinished.begin() > ticket; }); + } + + void Close(bool dropQueued) { + std::vector joined; + std::deque dropped; + { + std::unique_lock lock(mutex); + closed = true; + if (dropQueued) { + dropped.swap(tasks); + for (const auto& task : dropped) unfinished.erase(task.sequence); + idle.notify_all(); + } + idle.wait(lock, [&] { return tasks.empty() && running == 0; }); + joined.swap(workers); + } + ready.notify_all(); + for (const auto& worker : joined) Join(worker); } private: + static constexpr std::size_t StackBytes = 64u << 20u; + +#ifdef _WIN32 + struct Worker { + HANDLE handle; + unsigned id; + }; + + static void Join(const Worker& worker) { + if (worker.id != GetCurrentThreadId()) WaitForSingleObject(worker.handle, INFINITE); + CloseHandle(worker.handle); + } +#else + using Worker = pthread_t; + + static void Join(Worker worker) { + if (!pthread_equal(worker, pthread_self())) pthread_join(worker, nullptr); + } +#endif + explicit RegistrationPreparePool(unsigned count) { +#ifdef _WIN32 + for (unsigned index = 0; index < count; ++index) { + unsigned id = 0; + const auto thread = _beginthreadex(nullptr, static_cast(StackBytes), &RegistrationPreparePool::WindowsMain, this, STACK_SIZE_PARAM_IS_A_RESERVATION, &id); + if (thread != 0) { + workers.push_back({reinterpret_cast(thread), id}); + ++threads; + liveWorkers.fetch_add(1, std::memory_order_acq_rel); + } + } +#else pthread_attr_t attributes; pthread_attr_init(&attributes); - pthread_attr_setstacksize(&attributes, 64u << 20u); + pthread_attr_setstacksize(&attributes, StackBytes); for (unsigned index = 0; index < count; ++index) { pthread_t thread; if (pthread_create(&thread, &attributes, &RegistrationPreparePool::Main, this) == 0) { - pthread_detach(thread); + workers.push_back(thread); ++threads; + liveWorkers.fetch_add(1, std::memory_order_acq_rel); } } pthread_attr_destroy(&attributes); +#endif } +#ifdef _WIN32 + static unsigned __stdcall WindowsMain(void* argument) { + Main(argument); + return 0; + } +#endif + static void* Main(void* argument) { auto& pool = *static_cast(argument); if (PerformanceTrace::Configured()) PerformanceTrace::BindThread("shader prepare", TraceThread::DefaultCapacity / 8); @@ -902,18 +1283,29 @@ 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; } + const auto sequence = task.sequence; task.function(); task = {}; + bool drained = false; { std::lock_guard lock(pool.mutex); --pool.running; - if (pool.tasks.empty() && pool.running == 0) pool.idle.notify_all(); + pool.unfinished.erase(sequence); + drained = pool.tasks.empty() && pool.running == 0; + pool.idle.notify_all(); } +#ifdef __GLIBC__ + if (drained) trimFreedMemory(); +#endif } return nullptr; } @@ -921,14 +1313,19 @@ private: struct Task { std::function function; const PreparedShaders* prepared = nullptr; + std::uint64_t sequence = 0; }; std::mutex mutex; 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; bool closed = false; }; @@ -974,16 +1371,27 @@ void PrioritizeRegistrationPreparation(PreparedShaders& prepared) { if (auto* pool = RegistrationPreparePool::Existing()) pool->Prioritize(prepared); } +void AwaitRegistrationPreparation() { + if (auto* pool = RegistrationPreparePool::Existing()) pool->AwaitSubmitted(); +} + void SubmitLibraryPrebuild(std::function task) { RegistrationPreparePool::Prebuild().Submit(std::move(task)); } -void CloseRegistrationPreparation() { - RegistrationPreparePool::Instance().Close(); - if (auto* prebuild = RegistrationPreparePool::ExistingPrebuild()) prebuild->Close(); +void AwaitLibraryPrebuilds() { + if (auto* pool = RegistrationPreparePool::ExistingPrebuild()) pool->AwaitSubmitted(); } -std::vector PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target) { +void CloseRegistrationPreparation() { + RegistrationPreparePool::CloseAll(); +} + +unsigned RegistrationPreparationThreads() { + return RegistrationPreparePool::LiveWorkers(); +} + +std::vector PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target, bool deferUnprepared) { ShaderPreparationTransaction transaction; require(decoded.programs.size() == decoded.roles.size(), "graphics ABI program roles are incomplete"); for (const auto& program : decoded.programs) require(program.snapshot != nullptr, "graphics ABI has no registered shader snapshot"); @@ -999,29 +1407,43 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod require(expected == decoded.roles, "graphics ABI linked programs do not match the stage routing"); std::vector linked; std::vector memory; + AppendProgramMemory(memory, decoded.programs); for (std::size_t index = 0; index < decoded.programs.size(); ++index) { const auto& program = decoded.programs[index]; require(program.snapshot != nullptr && program.codeOffset < program.snapshot->code.size(), "graphics ABI has an invalid registered entry point"); const auto code = std::span(program.snapshot->code).subspan(program.codeOffset); require(program.binary.code.data() == code.data() && program.binary.code.size() == code.size() && program.binary.codeAddress == program.snapshot->codeAddress + program.codeOffset * sizeof(std::uint32_t), "graphics ABI entry point differs from its snapshot"); linked.push_back({decoded.roles[index], program.binary, program.userDataBase, program.firstUserSgpr, program.userData}); - memory.insert(memory.end(), program.memory.begin(), program.memory.end()); } std::vector prepared; std::uint32_t pushOffset = 0; const bool fixedSlots = target.fixedPushSlots; - const auto capacity = decoded.state.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : decoded.state.stages.tessellation ? ShaderRecompiler::TessellationLevelPushOffsetBytes : Graphics::PipelinePushSlotBytes; + const auto capacity = decoded.state.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : decoded.state.stages.tessellation ? ShaderRecompiler::TessellationLevelPushOffsetBytes : decoded.state.depthRemap.mode != 0u ? ShaderRecompiler::DepthRemapPushOffsetBytes : Graphics::PipelinePushSlotBytes; for (std::size_t index = 0; index < decoded.programs.size(); ++index) { if (decoded.roles[index] == ShaderRecompiler::ProgramRole::GeometryBack) continue; const auto& program = decoded.programs[index]; const bool fragment = program.binary.stage == ShaderRecompiler::ShaderStage::Fragment; const auto wave = fragment ? decoded.state.stages.fragmentWaveSize : decoded.state.stages.vertexWaveSize; - std::optional vertex; - if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true, 8u - program.firstUserSgpr); - if (vertex) vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl, program.tailContinuation.get()) | decoded.state.layerExports; pushOffset = Graphics::StagePushOffset(pushOffset, program.binary.stage, fixedSlots); const auto offset = pushOffset; const auto room = (fixedSlots && fragment ? Graphics::PipelinePushSlotBytes : capacity) - pushOffset % Graphics::PipelinePushSlotBytes; + if (deferUnprepared && DeferFailedRegistration(*program.snapshot)) { + prepared.push_back({program.snapshot, {program.codeOffset, nullptr}, program.binary.stage, offset, nullptr}); + continue; + } + std::optional vertex; + try { + if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true, 8u - program.firstUserSgpr); + } catch (const std::exception& error) { + if (!deferUnprepared) throw; + DeferPipelineStage(transaction.Edit(*program.snapshot), *program.snapshot, error.what()); + prepared.push_back({program.snapshot, {program.codeOffset, nullptr}, program.binary.stage, offset, nullptr}); + continue; + } + if (vertex) { + vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl, program.tailContinuation.get()) | decoded.state.layerExports; + Graphics::ApplyPointSize(decoded.state, *vertex); + } ShaderRecompiler::RecompileRequest request{program.binary, {wave, program.firstUserSgpr, program.userData, {}, fragment ? std::optional(decoded.pixel) : std::nullopt, vertex, memory, RegisteredFloatMode(*program.snapshot)}, target, {0, 0, offset, room}, ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, decoded.state.stages.mesh, decoded.state.stages.tessellation, {}}}; std::vector key; ShaderRecompiler::BuildPreparedShaderKey(request, key); @@ -1034,12 +1456,19 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod } if (handle == nullptr) { PerformanceTimer timing("Shader.PrepareArtifact"); - handle = PrepareShaderWithDiagnostics(request); + try { + handle = PrepareShaderWithDiagnostics(request); + } catch (const std::exception& error) { + if (!deferUnprepared) throw; + DeferPipelineStage(transaction.Edit(*program.snapshot), *program.snapshot, error.what()); + prepared.push_back({program.snapshot, {program.codeOffset, nullptr}, program.binary.stage, offset, nullptr}); + continue; + } } const auto bytes = handle->artifact->bindings.pushConstantSizeBytes; require(bytes <= room, "prepared graphics stages exceed the push constant block"); std::shared_ptr registration; - if (target.registrationModules) { + if (target.registrationModules && !LibraryPrebuildDisabled()) { try { registration = ShaderRecompiler::RegistrationResult(request, *handle); } catch (const std::exception& error) { @@ -1053,7 +1482,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; @@ -1062,6 +1500,7 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std QueueState state{}; std::shared_ptr registry; std::shared_ptr owner; + std::vector> selectedStages; { std::lock_guard lock(mutex); registry = shaders; @@ -1071,7 +1510,7 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std require(registry != nullptr && registry->contains(address), "graphics ABI refers to an unregistered shader"); const auto selected = RegisteredHeader(*registry, shader); require(selected != nullptr, "graphics ABI refers to a replaced shader header"); - if (owner == nullptr) owner = selected; + selectedStages.push_back(selected); const auto& snapshot = *selected; require(snapshot.registeredState != nullptr, "registered shader state is missing"); const auto registered = DecodeRegisteredState(snapshot, {}); @@ -1089,6 +1528,13 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std for (const auto reg : primitive) state.userConfig.insert_or_assign(reg.offset, reg.value); const auto localDevice = device.Load(); require(localDevice != nullptr, "graphics ABI device is missing"); + for (const auto& selected : selectedStages) { + if (!DeferFailedRegistration(*selected) && owner == nullptr) owner = selected; + } + if (owner == nullptr) { + transaction.Commit(); + return; + } struct GraphicsAbiKeyStorage {}; auto& key = HostThreadLocal, GraphicsAbiKeyStorage>(); BuildRegisteredAbiKey(state, *localDevice, key); @@ -1098,23 +1544,30 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std for (const auto& stage : abi.stages) { const auto snapshot = stage.lock(); require(snapshot != nullptr, "prepared graphics ABI lost a registered stage"); - if (snapshot->type != 1) ResolvePreparedGraphics(*snapshot, {}, primitiveType == state.userConfig.end() ? 0u : primitiveType->second, localDevice->Target()); + if (snapshot->type != 1) ResolvePreparedGraphics(*snapshot, {}, primitiveType == state.userConfig.end() ? 0u : primitiveType->second, localDevice->Target(), true); } transaction.Commit(); return; } DrawDecode decoded{}; - decoded.state.stages = Graphics::DecodeShaderStages(state, VertexWorkgroupRequired(state, *registry)); - DecodeGraphicsPrograms(decoded, state, *registry, true, false); - auto prepared = PrepareGraphicsStages(decoded, localDevice->Target()); + try { + decoded.state.stages = Graphics::DecodeShaderStages(state, VertexWorkgroupRequired(state, *registry)); + DecodeGraphicsPrograms(decoded, state, *registry, true, false); + } catch (const std::exception& error) { + for (const auto& selected : selectedStages) DeferPipelineStage(transaction.Edit(*selected), *selected, error.what()); + transaction.Commit(); + return; + } + auto prepared = PrepareGraphicsStages(decoded, localDevice->Target(), true); for (auto& stage : prepared) { + if (stage.entry.handle == nullptr) continue; const auto& entries = transaction.Read(*stage.snapshot).entries; const auto duplicate = std::ranges::any_of(entries, [&](const auto& existing) { return existing.codeOffset == stage.entry.codeOffset && existing.handle->artifact == stage.entry.handle->artifact; }); if (!duplicate) transaction.Edit(*stage.snapshot).entries.push_back(std::move(stage.entry)); } for (const auto& stage : prepared) { if (stage.snapshot->type == 1) continue; - ResolvePreparedGraphics(*stage.snapshot, {}, primitiveType == state.userConfig.end() ? 0u : primitiveType->second, localDevice->Target()); + ResolvePreparedGraphics(*stage.snapshot, {}, primitiveType == state.userConfig.end() ? 0u : primitiveType->second, localDevice->Target(), true); } if (decoded.state.stages.path == Graphics::ShaderPath::Vertex) { for (const auto& stage : prepared) { @@ -1130,6 +1583,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; @@ -1158,14 +1612,24 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::span, RegisteredAbiKeyStorage>(); BuildRegisteredAbiKey(state, *localDevice, key); + std::optional deferral; const auto prepare = [&] { - const auto plan = PlanRegistered(*snapshot, *localDevice, state, false); - if (plan == nullptr) return std::vector{}; - plan->prebuildDevice = localDevice; - return PrepareRegistered(*plan); + try { + const auto plan = PlanRegistered(*snapshot, *localDevice, state, false); + if (plan == nullptr) return std::vector{}; + plan->prebuildDevice = localDevice; + return PrepareRegistered(*plan); + } catch (const std::exception& error) { + deferral = error.what(); + return std::vector{}; + } }; const bool committedAbi = CommittedAbi(*snapshot, key); auto entries = committedAbi ? std::vector{} : prepare(); @@ -1180,6 +1644,7 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::spanartifact == entry.handle->artifact; }); if (!duplicate) prepared.entries.push_back(std::move(entry)); } + if (deferral) DeferPipelineStage(prepared, *snapshot, deferral->c_str()); prepared.registeredAbis.push_back(key); transaction.Commit(); } @@ -1201,10 +1666,11 @@ 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(); - if (primitiveType != 0 && primitiveType != 7 && primitiveType != 17) return; + if ((primitiveType != 0 && primitiveType != 7 && primitiveType != 17) || pixel == nullptr) return; ShaderPreparationTransaction transaction; require(vertex != nullptr && pixel != nullptr, "rectangle ABI requires vertex and fragment shaders"); std::shared_ptr front; @@ -1224,13 +1690,33 @@ void Driver::ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std:: require(front != fragment, "rectangle stages refer to the same shader"); const auto localDevice = device.Load(); require(localDevice != nullptr, "shader registration device is missing"); - ResolvePreparedGraphics(*front, fragment, primitiveType, localDevice->Target()); + ResolvePreparedGraphics(*front, fragment, primitiveType, localDevice->Target(), true); transaction.Commit(); } -void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target) { +void Driver::SettleShaderPreparations(std::span stages) { + std::vector> snapshots; + { + std::lock_guard lock(mutex); + if (shaders == nullptr) return; + for (const auto* shader : stages) { + if (shader == nullptr) continue; + if (auto snapshot = RegisteredHeader(*shaders, shader)) snapshots.push_back(std::move(snapshot)); + } + } + for (const auto& snapshot : snapshots) static_cast(FailedRegistration(*snapshot)); +} + +void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target, bool deferUnprepared) { ShaderPreparationTransaction transaction; require(fragment.get() != &front, "rectangle stages refer to the same shader"); + if (deferUnprepared) { + const bool frontFailed = DeferFailedRegistration(front); + if ((fragment != nullptr && DeferFailedRegistration(*fragment)) || frontFailed) { + transaction.Commit(); + return; + } + } std::vector> fragments; { const auto& current = transaction.Read(front); @@ -1256,27 +1742,37 @@ void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr< for (const auto& pixel : fragments) { const auto& current = transaction.Read(front); const auto& pixelPrepared = transaction.Read(*pixel); - require(!current.entries.empty() && !pixelPrepared.entries.empty(), "rectangle ABI has missing stage artifacts"); + if ((current.entries.empty() && current.deferred) || (pixelPrepared.entries.empty() && pixelPrepared.deferred)) continue; + if (current.entries.empty() || pixelPrepared.entries.empty()) { + require(deferUnprepared || pixel != fragment, "rectangle ABI has missing stage artifacts"); + continue; + } const auto progress = std::ranges::find_if(current.rectangleProgress, [&](const auto& entry) { return entry.fragment.lock() == pixel; }); const auto vertexCount = progress == current.rectangleProgress.end() ? 0u : progress->vertexCount; const auto fragmentCount = progress == current.rectangleProgress.end() ? 0u : progress->fragmentCount; if (vertexCount == current.entries.size() && fragmentCount == pixelPrepared.entries.size()) continue; auto& prepared = transaction.Edit(front); std::vector rectangles; - for (std::size_t vertexIndex = 0; vertexIndex < prepared.entries.size(); ++vertexIndex) { - const auto& vertexEntry = prepared.entries[vertexIndex]; - for (std::size_t fragmentIndex = vertexIndex < vertexCount ? fragmentCount : 0u; fragmentIndex < pixelPrepared.entries.size(); ++fragmentIndex) { - const auto& fragmentEntry = pixelPrepared.entries[fragmentIndex]; - const auto& vertexArtifact = ShaderRecompiler::GetPreparedArtifact(*vertexEntry.handle); - const auto& fragmentArtifact = ShaderRecompiler::GetPreparedArtifact(*fragmentEntry.handle); - const auto exists = [&](const auto& entry) { return entry.vertexId == vertexArtifact.variantId && entry.fragmentId == fragmentArtifact.variantId; }; - if (std::ranges::any_of(prepared.rectangles, exists) || std::ranges::any_of(rectangles, exists)) continue; - ShaderRecompiler::RecompileResult vertexResult; - ShaderRecompiler::RecompileResult fragmentResult; - static_cast(vertexResult) = vertexArtifact; - static_cast(fragmentResult) = fragmentArtifact; - rectangles.push_back({vertexArtifact.variantId, fragmentArtifact.variantId, std::make_shared(ShaderRecompiler::BuildRectListShaders(vertexResult, fragmentResult, target))}); + try { + for (std::size_t vertexIndex = 0; vertexIndex < prepared.entries.size(); ++vertexIndex) { + const auto& vertexEntry = prepared.entries[vertexIndex]; + for (std::size_t fragmentIndex = vertexIndex < vertexCount ? fragmentCount : 0u; fragmentIndex < pixelPrepared.entries.size(); ++fragmentIndex) { + const auto& fragmentEntry = pixelPrepared.entries[fragmentIndex]; + const auto& vertexArtifact = ShaderRecompiler::GetPreparedArtifact(*vertexEntry.handle); + const auto& fragmentArtifact = ShaderRecompiler::GetPreparedArtifact(*fragmentEntry.handle); + const auto exists = [&](const auto& entry) { return entry.vertexId == vertexArtifact.variantId && entry.fragmentId == fragmentArtifact.variantId; }; + if (std::ranges::any_of(prepared.rectangles, exists) || std::ranges::any_of(rectangles, exists)) continue; + ShaderRecompiler::RecompileResult vertexResult; + ShaderRecompiler::RecompileResult fragmentResult; + static_cast(vertexResult) = vertexArtifact; + static_cast(fragmentResult) = fragmentArtifact; + rectangles.push_back({vertexArtifact.variantId, fragmentArtifact.variantId, std::make_shared(ShaderRecompiler::BuildRectListShaders(vertexResult, fragmentResult, target))}); + } } + } catch (const std::exception& error) { + if (!deferUnprepared && pixel == fragment) throw; + if (deferUnprepared) DeferPipelineStage(transaction.Edit(*pixel), *pixel, error.what()); + continue; } prepared.rectangles.insert(prepared.rectangles.end(), std::make_move_iterator(rectangles.begin()), std::make_move_iterator(rectangles.end())); std::erase_if(prepared.rectangleProgress, [&](const auto& entry) { return entry.fragment.expired() || entry.fragment.lock() == pixel; }); @@ -1305,12 +1801,17 @@ 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; CheckFailure(); - require(!RawGraphicsDrawn(), "a shader was registered after graphics programs were drawn without registration"); GuestMemory::CheckRange(shader, sizeof(Shader), 1); Shader fields; std::memcpy(&fields, static_cast(shader), sizeof(Shader)); @@ -1320,6 +1821,7 @@ void Driver::RegisterShader(const Shader* shader) { GuestMemory::CheckRange(shader, fields.header_size, 1); const auto* code = const_cast(fields.code); GuestMemory::CheckRange(code, fields.shader_size, 256); + require(!RawGraphicsDrawnIn(reinterpret_cast(code), reinterpret_cast(code) + fields.shader_size), "a shader was registered over a graphics program that was drawn without registration"); ShaderSnapshot snapshot{reinterpret_cast(code), reinterpret_cast(shader), fields.type, {}, {}}; snapshot.code.resize(fields.shader_size / sizeof(std::uint32_t)); std::memcpy(snapshot.code.data(), code, fields.shader_size); @@ -1357,7 +1859,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); @@ -1398,7 +1904,8 @@ void Driver::RegisterShader(const Shader* shader) { } -extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix() { +extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix(const Shader* const* shaders, std::size_t count) { + AgcDriver::DriverDetail::Driver::Get().SettleShaderPreparations({shaders, count}); return new AgcDriver::DriverDetail::ShaderPreparationTransaction(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp index 696612f794..2720b91265 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/LabelHistory.cpp @@ -7,7 +7,7 @@ namespace AgcDriver::DriverDetail { void Driver::noteLabelStore(std::uint64_t address, std::span bytes, std::uint64_t stamp, std::uint32_t queue) { - if (bytes.empty() || bytes.size() > 64 || bytes.size() % 4 != 0 || address % 4 != 0) return; + if (!KeepsLabelStore(address, bytes.size())) return; std::lock_guard lock(labelStoresMutex); for (std::size_t offset = 0; offset < bytes.size(); offset += 4) { auto& history = labelStores[address + offset]; @@ -50,7 +50,7 @@ void Driver::traceLabel(std::span packet, std::uint32_t que const char* kind = nullptr; if (opcode == 0x49 && packet.size() >= 7) { target = packet[3] | (static_cast(packet[4]) << 32u); kind = "RELEASE_MEM"; } else if (opcode == 0x37 && packet.size() >= 5) { target = packet[2] | (static_cast(packet[3]) << 32u); kind = "WRITE_DATA"; } - else if ((opcode == 0x3c || opcode == 0x93) && packet.size() >= 7) { target = packet[2] | (static_cast(packet[3]) << 32u); kind = "WAIT_REG_MEM"; } + else if ((opcode == 0x3c || opcode == 0x93) && packet.size() >= 7) { target = (packet[2] & (Pm4::WaitAwaitedBytes(packet) == 8 ? ~7u : ~3u)) | (static_cast(packet[3]) << 32u); kind = "WAIT_REG_MEM"; } else if (opcode == 0x40 && packet.size() >= 6) { target = packet[4] | (static_cast(packet[5]) << 32u); kind = "COPY_DATA"; } std::uint64_t length = 4; if (opcode == 0x50 && packet.size() >= 7) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/SynchronizationStatistics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/SynchronizationStatistics.cpp index 06031abbb3..5b1e22624b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/SynchronizationStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/SynchronizationStatistics.cpp @@ -21,7 +21,7 @@ std::atomic packetLockRecordUs{0}, packetLockSubmitUs{0}; std::atomic triesFailed[TrySites] = {}; -std::atomic storesOnGpu{0}, storesBehindCompletions{0}, storesOnCpu{0}, storesDrained{0}, copiesToHostMemory{0}; +std::atomic storesOnGpu{0}, storesBehindCompletions{0}, storesOnCpu{0}, storesDrained{0}, dmaFills{0}, copiesToHostMemory{0}, copiesBehindPendingSources{0}; std::map drainCounts; @@ -39,7 +39,7 @@ SubmissionCosts& submissionCosts(std::uint32_t queue) { void reportSync() { std::string report; for (const auto& [code, count] : drainCounts) report += " " + (code == 0xffffu ? std::string("flip") : Pm4::Name(code << 8u)) + "=" + std::to_string(count); - AgcDriver::ProfilePrint_nid_no_patch("[sync] %llu device drains by packet:%s (%llu waited without the GPU mutex); %llu labels written on the GPU, %llu deferred behind completions, %llu no-op, fallbacks: idle %llu, completions %llu, not imported %llu, undecodable %llu; %llu labels queued per worker, recorded in %llu groups, %llu recorded at once; stores: %llu recorded on the GPU, %llu behind completions, %llu on the CPU (idle), %llu synced (drained); %llu copies into host memory synced on their source\n", static_cast(drainTotal), report.c_str(), static_cast(unlockedDrains.load()), static_cast(gpuLabels.load()), static_cast(completionLabels.load()), static_cast(noOpLabels.load()), static_cast(labelFallbacks[1].load()), static_cast(labelFallbacks[2].load()), static_cast(labelFallbacks[3].load()), static_cast(labelFallbacks[4].load()), static_cast(queuedLabels.load()), static_cast(labelGroups.load()), static_cast(immediateLabels.load()), static_cast(storesOnGpu.load()), static_cast(storesBehindCompletions.load()), static_cast(storesOnCpu.load()), static_cast(storesDrained.load()), static_cast(copiesToHostMemory.load())); + AgcDriver::ProfilePrint_nid_no_patch("[sync] %llu device drains by packet:%s (%llu waited without the GPU mutex); %llu labels written on the GPU, %llu deferred behind completions, %llu no-op, fallbacks: idle %llu, completions %llu, not imported %llu, undecodable %llu; %llu labels queued per worker, recorded in %llu groups, %llu recorded at once; stores: %llu recorded on the GPU, %llu behind completions, %llu on the CPU (idle), %llu synced (drained), %llu DMA fills stored as buffer fills; %llu copies into host memory synced on their source; %llu copies of up to 64 KiB recorded on the GPU behind their source's pending stores\n", static_cast(drainTotal), report.c_str(), static_cast(unlockedDrains.load()), static_cast(gpuLabels.load()), static_cast(completionLabels.load()), static_cast(noOpLabels.load()), static_cast(labelFallbacks[1].load()), static_cast(labelFallbacks[2].load()), static_cast(labelFallbacks[3].load()), static_cast(labelFallbacks[4].load()), static_cast(queuedLabels.load()), static_cast(labelGroups.load()), static_cast(immediateLabels.load()), static_cast(storesOnGpu.load()), static_cast(storesBehindCompletions.load()), static_cast(storesOnCpu.load()), static_cast(storesDrained.load()), static_cast(dmaFills.load()), static_cast(copiesToHostMemory.load()), static_cast(copiesBehindPendingSources.load())); } void countLabelOutcome(int reason) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp index 3deebcb869..6aead1a2cc 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" @@ -111,11 +112,13 @@ void Driver::waitMemory(std::span packet, std::uint32_t que GuestMemory::TagGpuLockThread(queue); auto& outcomes = waitOutcomes(); - const std::uint64_t awaited = packet[2] | (static_cast(packet[3]) << 32u); + const bool wide = ((packet[0] >> 8u) & 0xffu) == 0x93u; const std::size_t awaitedBytes = Pm4::WaitAwaitedBytes(packet); + const auto alignMask = awaitedBytes == 8 ? ~7ull : ~3ull; + const std::uint64_t awaited = (packet[2] & static_cast(alignMask)) | (static_cast(packet[3]) << 32u); static const bool traceGpu = std::getenv("APS5_TRACE_GPU") != nullptr; - if (traceGpu) std::fprintf(stderr, "[gpu] %.1f queue 0x%x waits 0x%llx == 0x%x (now 0x%x)\n", TraceMs(), queue, static_cast(packet[2] | (static_cast(packet[3]) << 32u)), packet[4], - *reinterpret_cast(packet[2] | (static_cast(packet[3]) << 32u))); + if (traceGpu) std::fprintf(stderr, "[gpu] %.1f queue 0x%x waits 0x%llx == 0x%x (now 0x%x)\n", TraceMs(), queue, static_cast(awaited), packet[4], + *reinterpret_cast(awaited)); struct WaitTrace { bool enabled; std::uint32_t queue; std::uint64_t address; std::chrono::steady_clock::time_point begin; ~WaitTrace() { @@ -123,7 +126,7 @@ void Driver::waitMemory(std::span packet, std::uint32_t que const auto ms = std::chrono::duration(std::chrono::steady_clock::now() - begin).count(); if (ms >= 0.5) std::fprintf(stderr, "[gpu] %.1f queue 0x%x wait on 0x%llx done after %.1f ms\n", TraceMs(), queue, static_cast(address), ms); } - } waitTrace{traceGpu, queue, packet[2] | (static_cast(packet[3]) << 32u), std::chrono::steady_clock::now()}; + } waitTrace{traceGpu, queue, awaited, std::chrono::steady_clock::now()}; bool warned = false; struct EpochPoint { @@ -326,10 +329,12 @@ void Driver::waitMemory(std::span packet, std::uint32_t que ~BlockedWait() { driver.noteWaitBlocked(queue, 0, false); } } blocked{*this, queue}; static const bool pauseSpin = std::getenv("APS5_NO_PAUSE_SPIN") == nullptr; + static const bool storeHeldResults = std::getenv("APS5_NO_QUEUED_DRAW_READ_ORDER") == nullptr; const auto spinLimit = queue == 0 ? std::chrono::microseconds(1500) : std::chrono::microseconds(100); const auto spinStart = std::chrono::steady_clock::now(); bool spinning = pauseSpin; 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); @@ -348,6 +353,7 @@ void Driver::waitMemory(std::span packet, std::uint32_t que } CheckFailure(); if (pollService(!spinning)) return; + if (storeHeldResults && !spinning && (polls & 31u) == 0) GuestMemory::FlushGpuWrites(awaited, awaitedBytes); noteAwaitingTitle(queue, awaited); if (requireMemory && (polls & 15u) == 0) { std::unique_lock settleLock(GuestMemory::GpuMutex(), std::try_to_lock); @@ -358,7 +364,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; @@ -369,15 +377,15 @@ void Driver::waitMemory(std::span packet, std::uint32_t que static std::uint64_t timeouts = 0; const std::lock_guard lock(reportedMutex); if (++timeouts % 20 == 0) std::fprintf(stderr, "[gpu] %llu GPU waits have timed out\n", static_cast(timeouts)); - if (!reported.insert(packet[2] | (static_cast(packet[3]) << 32u)).second) continue; + if (!reported.insert(awaited).second) continue; } const bool wide = ((packet[0] >> 8u) & 0xffu) == 0x93u; const std::uint64_t reference = wide ? packet[4] | (static_cast(packet[5]) << 32u) : packet[4]; const std::uint64_t mask = wide ? packet[6] | (static_cast(packet[7]) << 32u) : packet[5]; - const std::uint64_t current = wide ? *reinterpret_cast(awaited) : *reinterpret_cast(awaited); + const std::uint64_t current = awaitedBytes == 8 ? *reinterpret_cast(awaited) : *reinterpret_cast(awaited); std::fprintf(stderr, "[gpu] queue 0x%x WAIT_REG_MEM%s%s at 0x%llx timed out after %dms (function %u ref 0x%llx mask 0x%llx value 0x%llx)\n", queue, wide ? "_64" : "", requireMemory ? " (landing)" : "", static_cast(awaited), waitTimeoutMs(), packet[1] & 7u, static_cast(reference), static_cast(mask), static_cast(current)); - const auto address = packet[2] | (static_cast(packet[3]) << 32u); + const auto address = awaited; for (const auto& record : writeHistory()) { if (record.length != 0 && record.target <= address && address < record.target + std::max(record.length, 4)) std::fprintf(stderr, "[gpu] earlier write by queue 0x%x opcode 0x%x at 0x%llx+0x%llx\n", record.queue, record.opcode, static_cast(record.target), static_cast(record.length)); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitStatistics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitStatistics.cpp index 99466faac3..28dfef04d2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitStatistics.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include @@ -24,8 +25,15 @@ EpochBumps& Driver::epochBumps() { return bumps; } +std::chrono::steady_clock::time_point& Driver::lastEpochBump() { + static thread_local std::chrono::steady_clock::time_point at{}; + return at; +} + void Driver::bumpEpoch(std::uint64_t EpochBumps::*counter) { + lastEpochBump() = std::chrono::steady_clock::now(); GuestMemory::BumpCollectEpoch(); + if (DrawPipeline::Active()) DrawPipeline::EpochToken().fetch_add(1, std::memory_order_relaxed); ++(epochBumps().*counter); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/FloatFlushProbe.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/FloatFlushProbe.cpp new file mode 100644 index 0000000000..071286d713 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/FloatFlushProbe.cpp @@ -0,0 +1,186 @@ +#include "prx/libSceAgcDriver/Execution/include/FloatFlushProbe.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" + +#include +#include +#include +#include +#include + +namespace AgcDriver { +namespace { + +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 19; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array MixedCode{ + 0x34020084u, 0x160600ffu, 0x0000004cu, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x06140b04u, 0x08160905u, + 0x10180b04u, 0x0e1a0b04u, 0xd54b000eu, 0x041a0b04u, 0x7e1e0306u, 0x561e0b04u, 0x1e200b04u, 0xd5540011u, + 0x041a0b04u, 0xd5570012u, 0x041a0b04u, 0x7e264904u, 0x7e284505u, 0x7e2a8104u, 0x7c020b04u, 0xd5010016u, + 0x01a90280u, 0x7e2e1b04u, 0x7e307f05u, 0xd5080119u, 0x42020b04u, 0xd55f001au, 0x041a0b04u, 0xd550001bu, + 0x041a0b04u, 0xd550001cu, 0x03ca0b04u, 0xe0781000u, 0x80010a03u, 0xe0781010u, 0x80010e03u, 0xe0781020u, + 0x80011203u, 0xe0781030u, 0x80011603u, 0xe07c1040u, 0x80011a03u, 0xbf810000u, +}; + +constexpr std::uint32_t MixedRows[Threads][Inputs] = { + {0x00000001u, 0x3f800000u, 0x00000000u, 0x00000000u}, + {0x80400000u, 0x3f800000u, 0x00800000u, 0x00000000u}, + {0x3f800000u, 0x00012345u, 0x80000001u, 0x00000000u}, + {0x007fffffu, 0x007fffffu, 0x3f800000u, 0x00000000u}, + {0x3f7fffffu, 0x00800000u, 0x00000000u, 0x00000000u}, + {0xbf7fffffu, 0x00800000u, 0x80000000u, 0x00000000u}, + {0x3f7fffffu, 0x00800001u, 0x00000001u, 0x00000000u}, + {0x3f000000u, 0x01000000u, 0x80000001u, 0x00000000u}, + {0x00800000u, 0x80800001u, 0x00800000u, 0x00000000u}, + {0x00800001u, 0x00800000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x80000001u, 0x00000001u, 0x00000000u}, + {0x80000000u, 0x00000001u, 0x807fffffu, 0x00000000u}, + {0x7f800000u, 0x00000001u, 0x3f800000u, 0x00000000u}, + {0x00000001u, 0xff800000u, 0x00000000u, 0x00000000u}, + {0x7fc00000u, 0x00000001u, 0x00000001u, 0x00000000u}, + {0x40000000u, 0x00400000u, 0x80800000u, 0x00000000u}, + {0x3f800000u, 0x3f800000u, 0x80000001u, 0x00000000u}, + {0xbf800000u, 0x00000001u, 0x3f000000u, 0x00000000u}, + {0x34000000u, 0x34000000u, 0x00000000u, 0x00000000u}, + {0x1f800000u, 0x1f800000u, 0x00000000u, 0x00000000u}, + {0x1fffffffu, 0x1fffffffu, 0x00800000u, 0x00000000u}, + {0x20000000u, 0x1f000000u, 0x00000000u, 0x00000000u}, + {0x20000000u, 0x1f7fffffu, 0x00000000u, 0x00000000u}, + {0x20000001u, 0x1f7fffffu, 0x00000000u, 0x00000000u}, + {0xa0000000u, 0x1f7fffffu, 0x80000000u, 0x00000000u}, + {0x3f800000u, 0xbf800000u, 0x00000001u, 0x00000000u}, + {0x00800000u, 0x3f000000u, 0x00000000u, 0x00000000u}, + {0x00800000u, 0x3f7fffffu, 0x00000001u, 0x00000000u}, + {0x41200000u, 0x00000003u, 0x3dcccccdu, 0x00000000u}, + {0xc0490fdbu, 0x80000002u, 0x40490fdbu, 0x00000000u}, + {0x80000001u, 0x80000001u, 0x80000001u, 0x00000000u}, + {0x00000001u, 0x00000001u, 0x00000001u, 0x00000000u} +}; + +alignas(256) constexpr std::array TinyCode{ + 0x34020084u, 0x34040086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x10140b04u, 0x0e160b04u, 0xd54b000cu, + 0x041a0b04u, 0x7e1a0306u, 0x561a0b04u, 0x581c0d04u, 0x3f7ffffeu, 0x5a1e0b04u, 0x80000000u, 0xcc200010u, + 0x041a0b04u, 0xd5080011u, 0x22020b04u, 0xd5400012u, 0x041a0b04u, 0x7e260306u, 0x0c260b04u, 0xe0781000u, + 0x80010a02u, 0xe0781010u, 0x80010e02u, 0xe0741020u, 0x80011202u, 0xbf810000u, +}; + +constexpr std::uint32_t TinyRows[Threads][Inputs] = { + {0x00800001u, 0x3f7ffffeu, 0x00000000u, 0x00000000u}, + {0x80800001u, 0x3f7ffffeu, 0x80000000u, 0x00000000u}, + {0x3f7ffffeu, 0x00800001u, 0x00000000u, 0x00000000u}, + {0x008003e6u, 0x3f7ff834u, 0x00000000u, 0x00000000u}, + {0x00f80000u, 0x3f042108u, 0x80000000u, 0x00000000u}, + {0x80f80000u, 0x3f042108u, 0x00000000u, 0x00000000u}, + {0x00800f95u, 0x3f7fe0d9u, 0x00000000u, 0x00000000u}, + {0x00800001u, 0x3f7ffffdu, 0x00000000u, 0x00000000u}, + {0x00ffffffu, 0x3f000000u, 0x00000000u, 0x00000000u}, + {0x008005a8u, 0x3f7ff4b0u, 0x00000000u, 0x00000000u}, + {0x00f7ffffu, 0x3f042108u, 0x00000000u, 0x00000000u}, + {0x00ffffffu, 0x3effffffu, 0x00000000u, 0x00000000u}, + {0x008005a9u, 0x3f7ff4aeu, 0x00000000u, 0x00000000u}, + {0x00800001u, 0x3f7ffffeu, 0x00000001u, 0x00000000u}, + {0x007fffffu, 0x3f800001u, 0x00000000u, 0x00000000u}, + {0x00800000u, 0x3f7fffffu, 0x00000000u, 0x00000000u}, + {0x80800001u, 0x3f7ffffeu, 0x80000000u, 0x00000000u}, + {0x00800001u, 0x3f7ffffeu, 0x00000000u, 0x00000000u}, + {0xbf7ffffeu, 0x00800001u, 0x80000000u, 0x00000000u}, + {0x808003e6u, 0x3f7ff834u, 0x80000000u, 0x00000000u}, + {0x80f80000u, 0x3f042108u, 0x00000000u, 0x00000000u}, + {0x00f80000u, 0x3f042108u, 0x80000000u, 0x00000000u}, + {0x80800f95u, 0x3f7fe0d9u, 0x80000000u, 0x00000000u}, + {0x80800001u, 0x3f7ffffdu, 0x80000000u, 0x00000000u}, + {0x80ffffffu, 0x3f000000u, 0x80000000u, 0x00000000u}, + {0x808005a8u, 0x3f7ff4b0u, 0x80000000u, 0x00000000u}, + {0x80f7ffffu, 0x3f042108u, 0x80000000u, 0x00000000u}, + {0x80ffffffu, 0x3effffffu, 0x80000000u, 0x00000000u}, + {0x808005a9u, 0x3f7ff4aeu, 0x80000000u, 0x00000000u}, + {0x80800001u, 0x3f7ffffeu, 0x80000001u, 0x00000000u}, + {0x807fffffu, 0x3f800001u, 0x80000000u, 0x00000000u}, + {0x80800000u, 0x3f7fffffu, 0x80000000u, 0x00000000u} +}; + +alignas(256) constexpr std::array MixedTinyCode{ + 0x34020084u, 0x34040086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xcc20000au, 0x141a0b04u, 0xcc20000bu, + 0x0c1a0905u, 0xcc20100cu, 0x141a0b04u, 0xcc20080du, 0x0c1a0905u, 0xcc20400eu, 0x141e0b04u, 0xcc20400fu, + 0x0c1e0905u, 0x7e1002ffu, 0x3f7ffffeu, 0xcc204010u, 0x041e1104u, 0xcc206011u, 0x041e1104u, 0xe0781000u, + 0x80010a02u, 0xe0781010u, 0x80010e02u, 0xbf810000u, +}; + +constexpr std::uint32_t MixedTinyRows[Threads][Inputs] = { + {0x00801002u, 0x00003bffu, 0x00000000u, 0x00000000u}, + {0x80801002u, 0x00003bffu, 0x80000000u, 0x80008000u}, + {0x00801000u, 0x3bff3bffu, 0x00000000u, 0x00000000u}, + {0x00801001u, 0x3bff3bffu, 0x00000000u, 0x00000000u}, + {0x00801002u, 0x3bff3bffu, 0x00000000u, 0x00000000u}, + {0x00801003u, 0x3bff3bffu, 0x00000000u, 0x00000000u}, + {0x80801000u, 0x3bff3bffu, 0x80000000u, 0x80008000u}, + {0x80801001u, 0x3bff3bffu, 0x80000000u, 0x80008000u}, + {0x80801002u, 0x3bff3bffu, 0x80000000u, 0x80008000u}, + {0x80801003u, 0x3bff3bffu, 0x80000000u, 0x80008000u}, + {0x00801000u, 0xbbffbbffu, 0x80000000u, 0x80008000u}, + {0x00801001u, 0xbbffbbffu, 0x80000000u, 0x80008000u}, + {0x00801002u, 0xbbffbbffu, 0x80000000u, 0x80008000u}, + {0x00801003u, 0xbbffbbffu, 0x80000000u, 0x80008000u}, + {0x00800001u, 0x3bff3bffu, 0x00000000u, 0x00000000u}, + {0x80800001u, 0x3bff3bffu, 0x80000000u, 0x80008000u}, +}; + +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}; +} + +bool IsNan(std::uint32_t value) { + return (value & 0x7fffffffu) > 0x7f800000u; +} + +std::vector Run(VulkanDevice& device, ShaderRecompiler::SpirvTarget target, bool hardwareFlush, const std::optional& floatMode, std::span code, const std::uint32_t (*rows)[Inputs]) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy(std::begin(rows[tid]), std::end(rows[tid]), &Input[tid * Inputs]); + Output.fill(0xdeadbeefu); + 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}; + target.float32FlushToZero = hardwareFlush; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + target, + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = floatMode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + return {Output.begin(), Output.end()}; +} + +} + +FloatFlushProbeResult ProbeFloat32FlushToZero(VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + FloatFlushProbeResult result; + const std::array, 3> modes{std::nullopt, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, ShaderRecompiler::ShaderFloatMode{0x00u, false, true, false}}; + const std::array, const std::uint32_t (*)[Inputs]>, 3> programs{{{MixedCode, MixedRows}, {TinyCode, TinyRows}, {MixedTinyCode, MixedTinyRows}}}; + for (const auto& mode : modes) { + for (const auto& [code, rows] : programs) { + const auto software = Run(device, target, false, mode, code, rows); + const auto hardware = Run(device, target, true, mode, code, rows); + for (std::size_t index = 0; index < software.size(); ++index) { + ++result.compared; + if (software[index] != hardware[index] && !(IsNan(software[index]) && IsNan(hardware[index]))) ++result.mismatches; + } + } + } + return result; +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 31152a7138..313498d4ca 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" @@ -34,9 +35,11 @@ #include #include #include +#include #include #include #include +#include #include #include "prx/libc/include/specifics/linux/HostMapping.hpp" #endif @@ -351,8 +354,6 @@ namespace { // remembered, one byte of access flags per 4 KiB page (the table is committed lazily, so only the // pages ever verified cost memory). libc reports every range whose mapping or protection changed // (registry mutations and guest heap decommits) through the invalidator, which forgets those pages. -// Only committed pages are cached; reserved pages are queried each time, so a commit made outside the -// registry (the guest heap grows that way) is seen at once. // A second table covers the main guest image (the PE at 0x7ff6b...): its protections change only // through registry mutations (mprotect replaces the registered range and invalidates through the same // callback), and address-based builds verify every image section on each build, so without the table @@ -360,6 +361,7 @@ namespace { constexpr std::size_t PageBytes = 4096; constexpr std::uint8_t PageReadable = 1; constexpr std::uint8_t PageWritable = 2; +constexpr std::uint8_t PageInaccessible = 4; struct PageSpan { std::uintptr_t base = 0; @@ -385,7 +387,7 @@ struct PageSpan { } std::uint8_t load(std::uintptr_t address) const { - return std::atomic_ref(pages[(address - base) / PageBytes]).load(std::memory_order_relaxed); + return std::atomic_ref(const_cast(pages[(address - base) / PageBytes])).load(std::memory_order_relaxed); } void store(std::uintptr_t address, std::uint8_t value) { @@ -398,11 +400,11 @@ struct PageSpan { const auto stop = std::min((std::min(limit, base + size) - base + PageBytes - 1) / PageBytes, size / PageBytes); const std::uint64_t wide = 0x0101010101010101ull * value; while (index < stop) { - if (index % 8 == 0 && index + 8 <= stop && std::atomic_ref(*reinterpret_cast(pages + index)).load(std::memory_order_relaxed) == wide) { + if (index % 8 == 0 && index + 8 <= stop && std::atomic_ref(*reinterpret_cast(const_cast(pages + index))).load(std::memory_order_relaxed) == wide) { index += 8; continue; } - if (std::atomic_ref(pages[index]).load(std::memory_order_relaxed) != value) break; + if (std::atomic_ref(const_cast(pages[index])).load(std::memory_order_relaxed) != value) break; ++index; } return std::min(limit, base + index * PageBytes); @@ -418,6 +420,7 @@ struct PageSpan { struct PageStates { std::once_flag once; + std::atomic ready{false}; PageSpan arena; PageSpan image; @@ -434,14 +437,26 @@ struct PageStates { image.forget(address, bytes); } - void record(std::uintptr_t base, std::uintptr_t regionEnd, bool readable, bool writable, std::uint64_t generation) { + std::atomic accessSerial{0}; + + void record(std::uintptr_t base, std::uintptr_t regionEnd, bool readable, bool writable, std::uint64_t generation, std::uint64_t access) { for (PageSpan* span : {&arena, &image}) { - if (!readable || span->size == 0 || regionEnd <= span->base || base >= span->base + span->size) continue; - const std::uint8_t value = PageReadable | (writable ? PageWritable : 0u); + if (span->size == 0 || regionEnd <= span->base || base >= span->base + span->size) continue; + const std::uint8_t value = readable ? static_cast(PageReadable | (writable ? PageWritable : 0u)) : PageInaccessible; const auto first = std::max(base, span->base); const auto last = std::min(regionEnd, span->base + span->size); for (auto at = first; at < last; at += PageBytes) span->store(at, value); - if (GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation) span->forget(first, static_cast(last - first)); + if (GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation || accessSerial.load(std::memory_order_acquire) != access) span->forget(first, static_cast(last - first)); + } + } + + void forgetInaccessible(std::uintptr_t address, std::size_t bytes) { + for (PageSpan* span : {&arena, &image}) { + if (span->size == 0 || bytes == 0 || address >= span->base + span->size || address + bytes <= span->base) continue; + const auto last = std::min(address + bytes, span->base + span->size); + for (auto at = std::max(address, span->base) & ~static_cast(PageBytes - 1); at < last; at += PageBytes) { + if (span->load(at) == PageInaccessible) span->store(at, 0); + } } } }; @@ -475,7 +490,33 @@ void ForgetPages(std::uintptr_t address, std::size_t bytes) { forgetSerial.fetch_add(1, std::memory_order_release); } +void ForgetInaccessiblePages(std::uintptr_t address, std::size_t bytes) { + Pages().accessSerial.fetch_add(1, std::memory_order_acq_rel); + Pages().forgetInaccessible(address, bytes); +} + +#ifdef __linux__ +int mainImageExtent(dl_phdr_info* info, std::size_t, void* data) { + constexpr auto mask = static_cast(PageBytes - 1); + auto& span = *static_cast(data); + std::uintptr_t first = std::numeric_limits::max(); + std::uintptr_t last = 0; + for (int index = 0; index < info->dlpi_phnum; ++index) { + const auto& header = info->dlpi_phdr[index]; + if (header.p_type != PT_LOAD || header.p_memsz == 0) continue; + first = std::min(first, static_cast(info->dlpi_addr + header.p_vaddr) & ~mask); + last = std::max(last, static_cast(info->dlpi_addr + header.p_vaddr + header.p_memsz + mask) & ~mask); + } + if (first < last) { + span.base = first; + span.size = static_cast(last - first); + } + return 1; +} +#endif + void PageStates::initialize() { + if (ready.load(std::memory_order_acquire)) return; std::call_once(once, [&] { GuestArena::GuestArenaRange_nid_postfix(&arena.base, &arena.size); const bool arenaCached = arena.allocate(); @@ -496,9 +537,19 @@ void PageStates::initialize() { image.size = cursor - start; imageCached = image.allocate(); } +#elif defined(__linux__) + static const bool noImageCache = std::getenv("APS5_NO_IMAGE_PAGE_CACHE") != nullptr; + if (!noImageCache) { + dl_iterate_phdr(mainImageExtent, &image); + imageCached = image.allocate(); + } #endif - if (arenaCached || imageCached) GuestAllocations::GuestAllocationsSetInvalidator_nid_postfix(&ForgetPages); + if (arenaCached || imageCached) { + GuestAllocations::GuestAllocationsSetInvalidator_nid_postfix(&ForgetPages); + GuestArena::GuestArenaSetAccessObserver_nid_postfix(&ForgetInaccessiblePages); + } }); + ready.store(true, std::memory_order_release); } struct PageRun { @@ -520,9 +571,9 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { while (cursor < end) { if (const auto* span = pages.spanOf(cursor)) { const auto value = span->load(cursor); - if ((value & 3u) != 0) { + if (value != 0) { const auto next = span->runEnd(cursor, value, end); - if (!emit(PageRun{cursor, next, true, (value & PageWritable) != 0})) return true; + if (!emit(PageRun{cursor, next, (value & PageReadable) != 0, (value & PageWritable) != 0})) return true; cursor = next; continue; } @@ -535,6 +586,7 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { // before it invalidates, so a generation change across the query and the store below means // the stored pages may describe the old state and are forgotten again. const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + const auto access = pages.accessSerial.load(std::memory_order_acquire); if (cursor > userAddressLimit()) { static_cast(emit(PageRun{cursor, end, false, false})); return true; @@ -558,15 +610,16 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { const bool committed = memory.State == MEM_COMMIT && (memory.Protect & (PAGE_GUARD | PAGE_NOACCESS)) == 0; const bool readable = committed && readableProtection(protection); const bool writable = readable && writableProtection(protection); - pages.record(base, regionEnd, readable, writable, generation); + if ((memory.Protect & PAGE_GUARD) == 0) pages.record(base, regionEnd, readable, writable, generation, access); const auto next = std::min(end, regionEnd); if (!emit(PageRun{cursor, next, readable, writable})) return true; cursor = next; #else const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + const auto access = pages.accessSerial.load(std::memory_order_acquire); const auto recordRun = [&](std::uintptr_t next, bool readable, bool writable) { constexpr auto mask = static_cast(PageBytes - 1); - pages.record(cursor & ~mask, (next + mask) & ~mask, readable, writable, generation); + pages.record(cursor & ~mask, (next + mask) & ~mask, readable, writable, generation, access); }; if (const int fd = HostMapping::QueryFd(); fd >= 0) { procmap_query query{}; @@ -577,6 +630,7 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { if (query.vma_end <= cursor || query.vma_start >= query.vma_end) return false; if (query.vma_start > cursor) { const auto gapEnd = std::min(end, query.vma_start); + recordRun(gapEnd, false, false); if (!emit(PageRun{cursor, gapEnd, false, false})) return true; cursor = gapEnd; continue; @@ -609,6 +663,7 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { if (last <= cursor) continue; if (first > cursor) { const auto gapEnd = std::min(end, first); + recordRun(gapEnd, false, false); if (!emit(PageRun{cursor, gapEnd, false, false})) return true; cursor = gapEnd; found = true; @@ -657,16 +712,39 @@ 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; } +#ifdef __linux__ +bool pageStateRecorded(std::uintptr_t address, std::size_t bytes) { + auto& pages = Pages(); + pages.initialize(); + for (auto at = address & ~static_cast(PageBytes - 1); at < address + bytes; at += PageBytes) { + if (const auto* span = pages.spanOf(at); span != nullptr && span->load(at) != 0) return true; + } + return false; +} + +bool readableWithoutQuery(std::uintptr_t address, std::size_t bytes) { + constexpr std::size_t limit = 4096; + if (bytes > limit || HostMapping::QueryFd() >= 0 || pageStateRecorded(address, bytes)) return false; + thread_local std::array scratch; + iovec local{scratch.data(), bytes}; + iovec remote{reinterpret_cast(address), bytes}; + static const pid_t self = getpid(); + return ::process_vm_readv(self, &local, 1, &remote, 1, 0) == static_cast(bytes); +} +#endif + // Checks that [address, address + bytes) is mapped with read (and, if asked, write) access. Returns // an empty string when it is, otherwise why it is not. std::string verify(std::uintptr_t address, std::size_t bytes, bool writable) { const TimedAccess timed(CounterVerify, bytes); if (onOwnLiveStack(address, bytes)) return {}; +#ifdef __linux__ + if (!writable && readableWithoutQuery(address, bytes)) return {}; +#endif std::string reason; const bool queried = describePages(address, bytes, [&](const PageRun& run) { if (!run.readable) { @@ -735,12 +813,33 @@ constexpr std::size_t WritePagesPerBlock = WriteBlockBytes / WritePageBytes; enum class StampKind : std::uint8_t { Cpu, Driver, ImportWindow, Fresh }; +std::uint64_t HashPage(const std::byte* page) { + constexpr std::uint64_t prime1 = 0x9E3779B185EBCA87ull; + constexpr std::uint64_t prime2 = 0xC2B2AE3D27D4EB4Full; + constexpr std::uint64_t prime3 = 0x165667B19E3779F9ull; + constexpr std::uint64_t prime4 = 0x85EBCA77C2B2AE63ull; + const auto round = [](std::uint64_t accumulator, std::uint64_t word) { return std::rotl(accumulator + word * prime2, 31) * prime1; }; + std::array lanes{prime1 + prime2, prime2, 0, 0 - prime1}; + for (std::size_t offset = 0; offset < WritePageBytes; offset += 4 * sizeof(std::uint64_t)) { + std::array words; + std::memcpy(words.data(), page + offset, sizeof(words)); + for (std::size_t lane = 0; lane < 4; ++lane) lanes[lane] = round(lanes[lane], words[lane]); + } + std::uint64_t hash = std::rotl(lanes[0], 1) + std::rotl(lanes[1], 7) + std::rotl(lanes[2], 12) + std::rotl(lanes[3], 18); + for (const auto lane : lanes) hash = (hash ^ round(0, lane)) * prime1 + prime4; + hash ^= hash >> 33; + hash *= prime2; + hash ^= hash >> 29; + hash *= prime3; + return hash ^ (hash >> 32); +} + struct WindowCopy { std::uint64_t first; - std::span bytes; + std::span hashes; bool Holds(std::uintptr_t page) const { - return std::memcmp(reinterpret_cast(page), bytes.data() + (page - first), WritePageBytes) == 0; + return HashPage(reinterpret_cast(page)) == hashes[(page - first) / WritePageBytes]; } }; @@ -752,25 +851,23 @@ struct WriteTracker { std::mutex mutex; bool initialized = false; bool watched = false; -#ifdef _WIN32 - std::uintptr_t base = 0; - std::size_t size = 0; - // Generation of the last collected write per 64 KiB block of the arena. - std::vector blocks; - // The same, stamped by collects only (dirty pages = CPU stores), never by MarkWritten: the - // late label rule asks whether the CPU touched a label's block, and the driver's own GPU label - // records in the same block (the title's per-job slots are 0x20 apart) must not count. - std::vector cpuBlocks; - WriteWatchCoverage coverage; - std::vector hintBlocks; -#else + static constexpr std::size_t CoarseBlocks = 64; static constexpr std::size_t LeafBlocks = std::size_t{1} << 16; - static constexpr std::size_t LeafCount = std::size_t{1} << 15; struct Leaf { std::array blocks{}; std::array cpuBlocks{}; + std::array coarseBlocks{}; +#ifdef _WIN32 + std::array hintBlocks{}; +#endif }; std::vector> leaves; +#ifdef _WIN32 + std::uintptr_t base = 0; + std::size_t size = 0; + WriteWatchCoverage coverage; +#else + static constexpr std::size_t LeafCount = std::size_t{1} << 15; #endif // Atomic only so a per-thread memo hit can read it without the mutex (every mutation and every // stamp still happen under it): a hit must return the current value, including MarkWritten @@ -791,6 +888,7 @@ struct WriteTracker { std::uintptr_t end = 0; std::uint64_t epoch = 0; std::uint64_t unwatched = 0; + std::uint64_t mapping = 0; }; std::array memo{}; std::size_t nextMemo = 0; @@ -802,10 +900,8 @@ struct WriteTracker { GuestArena::GuestArenaRange_nid_postfix(&base, &size); watched = size != 0 && GuestArena::GuestArenaWriteWatched_nid_postfix(); if (!watched) return; - blocks.assign(size / WriteBlockBytes + 1, 0); - cpuBlocks.assign(size / WriteBlockBytes + 1, 0); + leaves.resize((size / WriteBlockBytes + LeafBlocks) / LeafBlocks); coverage.Initialize(base, size); - hintBlocks.assign(size / WriteBlockBytes + 1, 0); pages.resize(1u << 16); GuestArena::GuestArenaSetPrivateMappingObserver_nid_postfix(&watchPrivateMapping); #else @@ -845,24 +941,24 @@ struct WriteTracker { if (first >= stop) return; bool started = false; for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) { - if (!coverage.Covers(blockBegin(block), 1)) continue; + if (!coverage.Covers(blockBegin(block), static_cast(std::min(WriteBlockBytes, base + size - blockBegin(block))))) continue; if (!started) ++generation; started = true; - hintBlocks[block] = generation; + stampHint(block); } } bool hinted(std::uint64_t first, std::uint64_t stop) const { if (first >= stop || first < base || stop > base + size) return false; for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) { - if (hintBlocks[block] == 0 || coverage.Covers(blockBegin(block), 1)) return false; + if (hintOf(block) == 0 || coverage.Covers(blockBegin(block), static_cast(std::min(WriteBlockBytes, base + size - blockBegin(block))))) return false; } return !GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(static_cast(first), static_cast(stop - first)); } std::uint32_t newestHint(std::uint64_t first, std::uint64_t stop) const { std::uint32_t newest = 0; - for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) newest = std::max(newest, hintBlocks[block]); + for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) newest = std::max(newest, hintOf(block)); return newest; } #endif @@ -875,8 +971,24 @@ struct WriteTracker { struct DriverPieces { std::uint32_t whole = 0; std::uint32_t dropped = 0; - std::uint32_t next = 0; std::array pieces{}; + std::uint32_t possibleWhole = 0; + std::uint32_t possibleDropped = 0; + std::array possiblePieces{}; + + bool covers(std::uint32_t begin, std::uint32_t end, std::uint64_t generation) const { + if (generation == 0 || begin >= end || dropped > generation) return false; + if (whole > generation) return true; + while (begin < end) { + auto covered = begin; + for (const auto& piece : pieces) { + if (piece.generation > generation && piece.begin <= begin && piece.end > covered) covered = piece.end; + } + if (covered == begin) return false; + begin = covered; + } + return true; + } }; std::unordered_map driverPieces; @@ -884,13 +996,22 @@ struct WriteTracker { std::array pages{}; }; std::unordered_map cpuPages; + std::unordered_map walkPages; void noteCpuStore(std::uint64_t begin, std::uint64_t end, std::uint32_t stampGeneration) { + notePages(cpuPages, begin, end, stampGeneration); + } + + void noteWalkStore(std::uint64_t begin, std::uint64_t end, std::uint32_t stampGeneration) { + notePages(walkPages, begin, end, stampGeneration); + } + + void notePages(std::unordered_map& pages, std::uint64_t begin, std::uint64_t end, std::uint32_t stampGeneration) { for (auto block = blockOf(begin); block <= blockOf(end - 1); ++block) { const auto blockStart = blockBegin(block); const auto from = std::max(begin, blockStart) - blockStart; const auto to = std::min(end, blockStart + WriteBlockBytes) - blockStart; - auto& entry = cpuPages[block]; + auto& entry = pages[block]; for (auto page = from / WritePageBytes; page <= (to - 1) / WritePageBytes; ++page) { auto& stamp = entry.pages[page]; stamp = std::max(stamp, stampGeneration); @@ -898,30 +1019,89 @@ struct WriteTracker { } } - void noteDriverStore(std::uint64_t block, std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration) { + 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, bool complete) { +#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, complete); + } + } + + void noteDriverStore(std::uint64_t block, std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration, bool complete) { const auto begin = blockBegin(block); const auto from = static_cast(std::max(address, begin) - begin); const auto to = static_cast(std::min(end, begin + WriteBlockBytes) - begin); if (from == 0 && to == WriteBlockBytes) { - if (const auto found = driverPieces.find(block); found != driverPieces.end()) { - found->second = DriverPieces{}; - found->second.whole = stampGeneration; + auto& entry = driverPieces[block]; + if (complete) { + entry = DriverPieces{}; + entry.whole = stampGeneration; + } else { + entry.possibleWhole = stampGeneration; + entry.possibleDropped = 0; + entry.possiblePieces = {}; } return; } auto& entry = driverPieces[block]; - auto& slot = entry.pieces[entry.next++ % entry.pieces.size()]; - entry.dropped = std::max(entry.dropped, slot.generation); - slot = {stampGeneration, from, to}; + auto& pieces = complete ? entry.pieces : entry.possiblePieces; + auto& dropped = complete ? entry.dropped : entry.possibleDropped; + DriverPiece* contained = nullptr; + for (auto& piece : pieces) { + if (piece.generation == 0 || piece.begin < from || piece.end > to) continue; + if (contained == nullptr) contained = &piece; + else piece = {}; + } + if (contained != nullptr) { + *contained = {stampGeneration, from, to}; + return; + } + auto* oldest = &pieces.front(); + for (auto& piece : pieces) { + if (piece.generation < oldest->generation) oldest = &piece; + } + dropped = std::max(dropped, oldest->generation); + *oldest = {stampGeneration, from, to}; + } + + Leaf& leafFor(std::uint64_t block) { + auto& leaf = leaves[block / LeafBlocks]; + if (leaf == nullptr) leaf = std::make_unique(); + return *leaf; } - std::uint32_t stampOf(std::uint64_t block) const { #ifdef _WIN32 - return blocks[block]; -#else + void stampHint(std::uint64_t block) { + leafFor(block).hintBlocks[block % LeafBlocks] = generation; + } + + std::uint32_t hintOf(std::uint64_t block) const { + const auto& leaf = leaves[block / LeafBlocks]; + return leaf != nullptr ? leaf->hintBlocks[block % LeafBlocks] : 0; + } +#endif + + std::uint32_t stampOf(std::uint64_t block) const { const auto& leaf = leaves[block / LeafBlocks]; return leaf != nullptr ? leaf->blocks[block % LeafBlocks] : 0; -#endif } static constexpr std::size_t UnstampedSlots = 1u << 16; @@ -958,46 +1138,83 @@ struct WriteTracker { return newest; } + std::uint32_t coarseStampOf(std::uint64_t block) const { + const auto& leaf = leaves[block / LeafBlocks]; + return leaf != nullptr ? leaf->coarseBlocks[(block % LeafBlocks) / CoarseBlocks] : 0; + } + bool unchanged(std::uint64_t first, std::uint64_t last, std::uint64_t since) const { if (first == last) return stampOf(first) <= since; const auto newer = [since](std::uint32_t stamp) { return stamp > since; }; -#ifdef _WIN32 - return std::none_of(blocks.begin() + first, blocks.begin() + last + 1, newer); -#else while (first <= last) { - const auto index = first / LeafBlocks; - const auto end = std::min(last + 1, (index + 1) * LeafBlocks); - if (const auto& leaf = leaves[index]; leaf != nullptr) { + const auto& leaf = leaves[first / LeafBlocks]; + if (leaf == nullptr) { + first = (first / LeafBlocks + 1) * LeafBlocks; + continue; + } + const auto end = std::min(last + 1, (first / CoarseBlocks + 1) * CoarseBlocks); + if (end - first < CoarseBlocks || coarseStampOf(first) > since) { const auto begin = leaf->blocks.begin() + first % LeafBlocks; if (std::any_of(begin, begin + (end - first), newer)) return false; } first = end; } return true; -#endif } std::uint32_t cpuStampOf(std::uint64_t block) const { -#ifdef _WIN32 - return cpuBlocks[block]; -#else const auto& leaf = leaves[block / LeafBlocks]; return leaf != nullptr ? leaf->cpuBlocks[block % LeafBlocks] : 0; -#endif + } + + bool storedOver(std::uint64_t address, std::uint64_t end, std::uint64_t generation, bool walked) const { + for (auto block = blockOf(address); block <= blockOf(end - 1); ++block) { + if (stampOf(block) <= generation) continue; + const auto begin = blockBegin(block); + const auto from = std::max(address, begin) - begin; + const auto to = std::min(end, begin + WriteBlockBytes) - begin; + const auto newerPage = [&](const std::unordered_map& pages, std::uint64_t first, std::uint64_t stop) { + const auto found = pages.find(block); + if (found == pages.end()) return false; + for (auto page = first / WritePageBytes; page <= (stop - 1) / WritePageBytes; ++page) { + if (found->second.pages[page] > generation) return true; + } + return false; + }; + if (cpuPages.find(block) == cpuPages.end() && cpuStampOf(block) > generation) return true; + if (newerPage(cpuPages, from, to)) return true; + if (walked && newerPage(walkPages, from, to)) return true; + const auto found = driverPieces.find(block); + if (found == driverPieces.end()) { + if (!walked && newerPage(walkPages, 0, WriteBlockBytes)) return true; + continue; + } + const auto& entry = found->second; + if (entry.whole > generation || entry.dropped > generation || entry.possibleWhole > generation || entry.possibleDropped > generation) return true; + for (const auto& piece : entry.pieces) { + if (piece.generation > generation && piece.begin < to && from < piece.end) return true; + } + for (const auto& piece : entry.possiblePieces) { + if (piece.generation > generation && piece.begin < to && from < piece.end) return true; + } + } + return false; + } + + bool rangeUnchanged(std::uint64_t address, std::size_t bytes, std::uint64_t since) const { + return unchanged(blockOf(address), blockOf(address + bytes - 1), since) || !storedOver(address, address + bytes, since, true); } void stamp(std::uint64_t block, std::uint32_t stampGeneration, StampKind kind) { const bool cpu = kind == StampKind::Cpu || kind == StampKind::ImportWindow; const bool stored = kind != StampKind::ImportWindow; -#ifdef _WIN32 - if (stored) blocks[block] = stampGeneration; - if (cpu) cpuBlocks[block] = stampGeneration; -#else - auto& leaf = leaves[block / LeafBlocks]; - if (leaf == nullptr) leaf = std::make_unique(); - if (stored) leaf->blocks[block % LeafBlocks] = stampGeneration; - if (cpu) leaf->cpuBlocks[block % LeafBlocks] = stampGeneration; -#endif + auto& leaf = leafFor(block); + if (stored) { + leaf.blocks[block % LeafBlocks] = stampGeneration; + auto& coarse = leaf.coarseBlocks[(block % LeafBlocks) / CoarseBlocks]; + coarse = std::max(coarse, stampGeneration); + } + if (cpu) leaf.cpuBlocks[block % LeafBlocks] = stampGeneration; } }; @@ -1029,7 +1246,8 @@ void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_ ++tracker.generation; tracker.noteCpuStore(address, address + bytes, tracker.generation); for (auto block = tracker.blockOf(address); block <= tracker.blockOf(address + bytes - 1); ++block) { - tracker.driverPieces.erase(block); + if (const auto& leaf = tracker.leaves[block / WriteTracker::LeafBlocks]; leaf != nullptr) leaf->hintBlocks[block % WriteTracker::LeafBlocks] = 0; + if (const auto found = tracker.driverPieces.find(block); found != tracker.driverPieces.end()) found->second.dropped = std::max(found->second.dropped, tracker.generation); tracker.stamp(block, tracker.generation, StampKind::Cpu); } unwatchSerial.fetch_add(1, std::memory_order_release); @@ -1043,6 +1261,7 @@ void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_ // presenter, game threads inside the flush hook), as nothing orders the CPU's writes for it. std::atomic nextCollectEpoch{1}; thread_local std::uint64_t threadCollectEpoch = 0; +std::atomic lastBumpedEpoch{0}; std::uint64_t currentCollectEpoch() { if (threadCollectEpoch != 0) return threadCollectEpoch; @@ -1064,6 +1283,26 @@ struct ThreadCollectMemo { }; thread_local ThreadCollectMemo threadCollectMemo; +struct EpochCollectMap { + std::uint64_t epoch = 0; + std::unordered_map ranges; +}; + +EpochCollectMap& epochCollectMap(std::uint64_t epoch) { + thread_local EpochCollectMap* slot = nullptr; + auto& map = ShaderRecompiler::ThreadOwned(slot); + if (map.epoch != epoch) { + map.ranges.clear(); + map.epoch = epoch; + } + return map; +} + +bool epochCollectMapEnabled() { + static const bool enabled = std::getenv("APS5_NO_EPOCH_COLLECT_MAP") == nullptr; + return enabled; +} + bool sharedCollectMemo() { static const bool shared = std::getenv("APS5_SHARED_COLLECT_MEMO") != nullptr; return shared; @@ -1084,6 +1323,7 @@ void stampWrittenRun(void* context, std::uintptr_t begin, std::uintptr_t end) { ++runs.stamped; const auto generation = tracker.generation.load(std::memory_order_relaxed); if (kind != StampKind::Driver) tracker.noteCpuStore(begin, end, generation); + else tracker.noteWalkStore(begin, end, generation); if (runs.window != nullptr) { for (auto page = begin; page < end; page += WritePageBytes) tracker.stamp(tracker.blockOf(page), generation, runs.window->Holds(page) ? StampKind::ImportWindow : kind); } else { @@ -1095,9 +1335,14 @@ void stampWrittenRun(void* context, std::uintptr_t begin, std::uintptr_t end) { } #ifdef _WIN32 +std::atomic freshCollectHook{nullptr}; + bool collectWindowsWrites(StampRuns& runs, std::uint64_t first, std::uint64_t stop, bool armedOnly) { auto& tracker = runs.tracker; const bool fresh = runs.kind == StampKind::Fresh; + if (fresh) { + if (const auto hook = freshCollectHook.load(std::memory_order_acquire)) return hook(static_cast(first), static_cast(stop - first), &stampWrittenRun, &runs); + } const auto collect = fresh ? &GuestArena::GuestArenaCollectFreshWrites_nid_postfix : armedOnly ? &GuestArena::GuestArenaCollectArmedWrites_nid_postfix : &GuestArena::GuestArenaCollectWrites_nid_postfix; static const bool singlePass = std::getenv("APS5_NO_SINGLE_PASS_COLLECT") == nullptr; const auto walk = [&](bool clear) { @@ -1145,12 +1390,20 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi // must not differ between them. const auto epoch = currentCollectEpoch(); const auto unwatched = unwatchSerial.load(std::memory_order_acquire); + const auto mapping = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); const bool useMemo = memoized && collectMemoEnabled() && bytes != 0; if (useMemo && !sharedCollectMemo()) { // An entry exists only for a completed walk of an in-arena range, so the tracker is // initialized and watched; nothing below the lock needs asking. + if (epochCollectMapEnabled()) { + const auto& map = epochCollectMap(epoch); + if (const auto found = map.ranges.find(first); found != map.ranges.end() && found->second.unwatched == unwatched && found->second.mapping == mapping && stop <= found->second.end) { + collectMemoHits.fetch_add(1, std::memory_order_relaxed); + return tracker.generation.load(std::memory_order_relaxed); + } + } for (const auto& entry : threadCollectMemo.entries) { - if (entry.epoch == epoch && entry.unwatched == unwatched && entry.begin <= first && stop <= entry.end) { + if (entry.epoch == epoch && entry.unwatched == unwatched && entry.mapping == mapping && entry.begin <= first && stop <= entry.end) { // The current generation, not the memoized one: blocks stamped since (MarkWritten, an // overlapping collect) were written before this caller reads, and an older value would // make every later UnchangedSince fail until the epoch ends. @@ -1165,7 +1418,7 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi auto cursor = first; if (useMemo && sharedCollectMemo()) { for (const auto& entry : tracker.memo) { - if (entry.epoch == epoch && entry.unwatched == unwatchSerial.load(std::memory_order_relaxed) && entry.begin <= cursor && stop <= entry.end) { + if (entry.epoch == epoch && entry.unwatched == unwatchSerial.load(std::memory_order_relaxed) && entry.mapping == mapping && entry.begin <= cursor && stop <= entry.end) { collectMemoHits.fetch_add(1, std::memory_order_relaxed); return tracker.generation; } @@ -1186,8 +1439,14 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi // Only a completed walk is remembered; a failed one (uncommitted pages) returned 0 above. if (collectMemoEnabled()) { const auto serial = unwatchSerial.load(std::memory_order_relaxed); - if (sharedCollectMemo()) tracker.memo[tracker.nextMemo++ % tracker.memo.size()] = {first, stop, epoch, serial}; - else threadCollectMemo.entries[threadCollectMemo.next++ % threadCollectMemo.entries.size()] = {first, stop, epoch, serial}; + if (sharedCollectMemo()) tracker.memo[tracker.nextMemo++ % tracker.memo.size()] = {first, stop, epoch, serial, mapping}; + else { + threadCollectMemo.entries[threadCollectMemo.next++ % threadCollectMemo.entries.size()] = {first, stop, epoch, serial, mapping}; + if (epochCollectMapEnabled()) { + auto& entry = epochCollectMap(epoch).ranges[first]; + if (entry.unwatched != serial || entry.mapping != mapping || entry.end < stop) entry = {first, stop, epoch, serial, mapping}; + } + } } return tracker.generation; } @@ -1204,9 +1463,24 @@ std::uint64_t CollectWritesUncached(std::uint64_t address, std::size_t bytes) { void BumpCollectEpoch() { threadCollectEpoch = nextCollectEpoch.fetch_add(1, std::memory_order_relaxed) + 1; + auto last = lastBumpedEpoch.load(std::memory_order_relaxed); + while (last < threadCollectEpoch && !lastBumpedEpoch.compare_exchange_weak(last, threadCollectEpoch, std::memory_order_release, std::memory_order_relaxed)) {} collectEpochBumps.fetch_add(1, std::memory_order_relaxed); } +std::uint64_t ThreadCollectEpoch() { + return collectMemoEnabled() ? threadCollectEpoch : 0; +} + +std::uint64_t UnwatchSerial() { + return unwatchSerial.load(std::memory_order_acquire); +} + +void AdoptLatestCollectEpoch() { + const auto latest = lastBumpedEpoch.load(std::memory_order_acquire); + if (latest > threadCollectEpoch) threadCollectEpoch = latest; +} + std::uint64_t CollectEpochBumps() { return collectEpochBumps.load(std::memory_order_relaxed); } @@ -1220,7 +1494,21 @@ namespace { std::atomic unwatchHook{nullptr}; void beforeUnwatch(std::uint64_t address, std::size_t bytes) { - if (const auto hook = unwatchHook.load(std::memory_order_acquire)) hook(address, bytes); + const auto hook = unwatchHook.load(std::memory_order_acquire); + if (hook == nullptr) return; +#ifdef _WIN32 + std::uint64_t begin = 0; + std::uint64_t end = 0; + { + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + tracker.initialize(); + if (!tracker.watched || !tracker.coverage.ExcludedSpan(address, bytes, &begin, &end)) return; + } + hook(begin, static_cast(end - begin)); +#else + hook(address, bytes); +#endif } void unwatchLocked(WriteTracker& tracker, std::uint64_t address, std::size_t bytes) { @@ -1239,6 +1527,12 @@ void SetUnwatchHook(void (*hook)(std::uint64_t, std::size_t)) { unwatchHook.store(hook, std::memory_order_release); } +#ifdef _WIN32 +void SetFreshCollectHook(FreshCollectHook hook) { + freshCollectHook.store(hook, std::memory_order_release); +} +#endif + void Unwatch(std::uint64_t address, std::size_t bytes) { if (bytes == 0 || bytes > std::numeric_limits::max() - address) return; beforeUnwatch(address, bytes); @@ -1248,6 +1542,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) { @@ -1266,28 +1585,48 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function const auto first = address & ~(page - 1); const auto stop = (address + bytes + page - 1) & ~(page - 1); const bool covered = tracker.covers(first, static_cast(stop - first)); - std::vector saved; - if (covered && staysWatched) { - CheckRange(reinterpret_cast(first), static_cast(stop - first), 1, false); - const auto* source = reinterpret_cast(first); - saved.assign(source, source + (stop - first)); - } - const bool before = covered && walkWrites(tracker, first, stop, StampKind::Cpu, true); - bool imported = false; - { + const auto runImport = [&] { #ifdef __APPLE__ const GuestArena::HostWrite open(reinterpret_cast(first), static_cast(stop - first)); #endif - imported = import(); - } - if (!imported) return false; + return import(); + }; if (!staysWatched) { + const bool before = covered && walkWrites(tracker, first, stop, StampKind::Cpu, true); + if (!runImport()) return false; if (!before || !walkWrites(tracker, first, stop, StampKind::ImportWindow, true)) unwatchLocked(tracker, first, static_cast(stop - first)); unwatchLocked(tracker, address, bytes); return true; } - const WindowCopy window{first, saved}; - if (!before || !walkWrites(tracker, first, stop, StampKind::Cpu, true, &window)) unwatchLocked(tracker, first, static_cast(stop - first)); + std::vector> spans; + if (covered) spans.emplace_back(first, stop); + else { + for (auto begin = first; begin < stop;) { + const auto end = std::min((begin / WriteBlockBytes + 1) * WriteBlockBytes, stop); + if (tracker.covers(begin, static_cast(end - begin))) { + if (!spans.empty() && spans.back().second == begin) spans.back().second = end; + else spans.emplace_back(begin, end); + } + begin = end; + } + } + std::vector hashes(static_cast((stop - first) / page), 0); + for (const auto& [begin, end] : spans) { + CheckRange(reinterpret_cast(begin), static_cast(end - begin), 1, false); + for (auto cursor = begin; cursor < end; cursor += page) { + hashes[static_cast((cursor - first) / page)] = HashPage(reinterpret_cast(cursor)); + } + } + std::erase_if(spans, [&](const auto& span) { return !walkWrites(tracker, span.first, span.second, StampKind::Cpu, true); }); + if (!runImport()) return false; + const WindowCopy window{first, hashes}; + auto unwatchedFrom = first; + for (const auto& [begin, end] : spans) { + if (!walkWrites(tracker, begin, end, StampKind::Cpu, true, &window)) continue; + if (unwatchedFrom < begin) unwatchLocked(tracker, unwatchedFrom, static_cast(begin - unwatchedFrom)); + unwatchedFrom = end; + } + if (unwatchedFrom < stop) unwatchLocked(tracker, unwatchedFrom, static_cast(stop - unwatchedFrom)); return true; } @@ -1312,9 +1651,7 @@ bool UnchangedSince(std::uint64_t address, std::size_t bytes, std::uint64_t gene const auto lock = lockTracker(tracker); tracker.initialize(); if (!tracker.watched || generation == 0 || bytes == 0 || !tracker.covers(address, bytes)) return false; - const auto first = tracker.blockOf(address); - const auto last = tracker.blockOf(address + bytes - 1); - return tracker.unchanged(first, last, generation); + return tracker.rangeUnchanged(address, bytes, generation); } bool UnchangedSinceAll(std::span queries) { @@ -1324,9 +1661,7 @@ bool UnchangedSinceAll(std::span queries) { if (!tracker.watched) return false; for (const auto& [address, bytes, generation] : queries) { if (generation == 0 || bytes == 0 || !tracker.covers(address, bytes)) return false; - const auto first = tracker.blockOf(address); - const auto last = tracker.blockOf(address + bytes - 1); - if (!tracker.unchanged(first, last, generation)) return false; + if (!tracker.rangeUnchanged(address, bytes, generation)) return false; } return true; } @@ -1362,11 +1697,12 @@ 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, true); return 0; } for (auto block = tracker.blockOf(stored.first); block <= tracker.blockOf(stored.second - 1); ++block) { tracker.stamp(block, tracker.generation, StampKind::Driver); - tracker.noteDriverStore(block, stored.first, stored.second, tracker.generation); + tracker.noteDriverStore(block, stored.first, stored.second, tracker.generation, true); } return tracker.generation; }; @@ -1399,14 +1735,15 @@ std::uint64_t StoreOwnBytes(std::uint64_t address, std::span so }); } -std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { +static std::uint64_t markWritten(std::uint64_t address, std::size_t bytes, bool complete) { auto& tracker = Tracker(); - const auto lock = lockTracker(tracker); + auto lock = lockTracker(tracker); tracker.initialize(); if (bytes == 0) return 0; ++tracker.generation; if (!tracker.watched || !tracker.covers(address, bytes)) { tracker.noteUnstampedStore(address, address + bytes); + if (tracker.watched) tracker.stampWatchedPart(address, address + bytes, tracker.generation, complete); return 0; } { @@ -1414,11 +1751,14 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { const auto blockStop = (address + bytes + WriteBlockBytes - 1) & ~(std::uint64_t{WriteBlockBytes} - 1); StampRuns runs{tracker, StampKind::Fresh}; #ifdef _WIN32 - const bool complete = collectWindowsWrites(runs, blockFirst, blockStop, false); + const bool collected = collectWindowsWrites(runs, blockFirst, blockStop, false); #else - const bool complete = GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast(blockFirst), static_cast(blockStop - blockFirst), &stampWrittenRun, &runs); + const bool collected = GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast(blockFirst), static_cast(blockStop - blockFirst), &stampWrittenRun, &runs); #endif - if (!complete) { + if (!collected) { + lock.unlock(); + beforeUnwatch(blockFirst, static_cast(blockStop - blockFirst)); + lock.lock(); unwatchLocked(tracker, blockFirst, static_cast(blockStop - blockFirst)); tracker.noteUnstampedStore(address, address + bytes); return 0; @@ -1429,11 +1769,19 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { const auto last = tracker.blockOf(address + bytes - 1); for (auto block = first; block <= last; ++block) { tracker.stamp(block, tracker.generation, StampKind::Driver); - tracker.noteDriverStore(block, address, address + bytes, tracker.generation); + tracker.noteDriverStore(block, address, address + bytes, tracker.generation, complete); } return tracker.generation; } +std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { + return markWritten(address, bytes, true); +} + +std::uint64_t MarkPossiblyWritten(std::uint64_t address, std::size_t bytes) { + return markWritten(address, bytes, false); +} + bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { auto& tracker = Tracker(); const auto lock = lockTracker(tracker); @@ -1442,33 +1790,34 @@ bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generati #ifdef _WIN32 if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return true; #endif + return tracker.storedOver(address, address + bytes, generation, false); +} + +bool StoredAllSince_nid_no_patch(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { + if (generation == 0 || bytes == 0 || bytes > std::numeric_limits::max() - address) return false; + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + tracker.initialize(); + if (!tracker.watched || !tracker.covers(address, bytes)) return false; const auto end = address + bytes; - const auto first = tracker.blockOf(address); - const auto last = tracker.blockOf(end - 1); - for (auto block = first; block <= last; ++block) { - if (tracker.stampOf(block) <= generation) continue; - if (const auto found = tracker.cpuPages.find(block); found != tracker.cpuPages.end()) { - const auto begin = tracker.blockBegin(block); - const auto from = std::max(address, begin) - begin; - const auto to = std::min(end, begin + WriteBlockBytes) - begin; - for (auto page = from / WritePageBytes; page <= (to - 1) / WritePageBytes; ++page) { - if (found->second.pages[page] > generation) return true; - } - } else if (tracker.cpuStampOf(block) > generation) { - return true; - } - const auto found = tracker.driverPieces.find(block); - if (found == tracker.driverPieces.end()) return true; - const auto& entry = found->second; - if (entry.whole > generation || entry.dropped > generation) return true; - const auto begin = tracker.blockBegin(block); - const auto from = std::max(address, begin) - begin; - const auto to = std::min(end, begin + WriteBlockBytes) - begin; - for (const auto& piece : entry.pieces) { - if (piece.generation > generation && piece.begin < to && from < piece.end) return true; + if (!tracker.unstampedStores.empty()) { + const auto first = address / WriteBlockBytes; + const auto last = (end - 1u) / WriteBlockBytes; + if (last - first >= WriteTracker::UnstampedSlots) { + if (std::ranges::any_of(tracker.unstampedStores, [&](auto stamp) { return stamp > generation; })) return false; + } else { + for (auto block = first; block <= last; ++block) if (tracker.unstampedStores[block % WriteTracker::UnstampedSlots] > generation) return false; } } - return false; + for (auto block = tracker.blockOf(address); block <= tracker.blockOf(end - 1u); ++block) { + const auto found = tracker.driverPieces.find(block); + if (found == tracker.driverPieces.end()) return false; + const auto start = tracker.blockBegin(block); + const auto from = static_cast(std::max(address, start) - start); + const auto to = static_cast(std::min(end, start + WriteBlockBytes) - start); + if (!found->second.covers(from, to, generation)) return false; + } + return true; } std::uint64_t TrackerGeneration() { @@ -1499,7 +1848,7 @@ bool walkHints(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop) { if (!GuestArena::GuestArenaCollectWrites_nid_postfix(static_cast(cursor), static_cast(stop - cursor), tracker.pages.data(), &count, true)) return false; if (count != 0 && !marked) ++tracker.generation; marked = marked || count != 0; - for (std::size_t i = 0; i < count; ++i) tracker.hintBlocks[tracker.blockOf(reinterpret_cast(tracker.pages[i]))] = tracker.generation; + for (std::size_t i = 0; i < count; ++i) tracker.stampHint(tracker.blockOf(reinterpret_cast(tracker.pages[i]))); if (count < tracker.pages.size()) break; cursor = reinterpret_cast(tracker.pages[count - 1]) + WritePageBytes; } @@ -1522,15 +1871,16 @@ std::uint64_t UntrackedHint(std::uint64_t address, std::size_t bytes) { tracker.initialize(); if (!tracker.watched || !tracker.hinted(first, stop)) return 0; const auto unwatched = unwatchSerial.load(std::memory_order_relaxed); + const auto mapping = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); const bool memoized = collectMemoEnabled() && std::any_of(threadHintMemo.entries.begin(), threadHintMemo.entries.end(), [&](const WriteTracker::Memo& entry) { - return entry.epoch == epoch && entry.unwatched == unwatched && entry.begin <= first && stop <= entry.end; + return entry.epoch == epoch && entry.unwatched == unwatched && entry.mapping == mapping && entry.begin <= first && stop <= entry.end; }); if (memoized) { collectMemoHits.fetch_add(1, std::memory_order_relaxed); } else { const TimedAccess timed(CounterCollect, bytes); if (!walkHints(tracker, first, stop)) return 0; - if (collectMemoEnabled()) threadHintMemo.entries[threadHintMemo.next++ % threadHintMemo.entries.size()] = {first, stop, epoch, unwatched}; + if (collectMemoEnabled()) threadHintMemo.entries[threadHintMemo.next++ % threadHintMemo.entries.size()] = {first, stop, epoch, unwatched, mapping}; } return tracker.newestHint(first, stop); #else @@ -1819,12 +2169,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()); } } @@ -1842,7 +2192,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); @@ -1958,16 +2308,26 @@ unsigned long long CodeOffset(const void* address) { namespace { std::atomic flushHook{nullptr}; +std::atomic readGuard{nullptr}; + +void PrepareHostRead(std::uintptr_t address, std::size_t bytes) { + if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, bytes); + FlushGpuWrites(address, bytes); +} } void SetFlushHook(void (*hook)(std::uint64_t, std::size_t)) { flushHook.store(hook, std::memory_order_release); + GuestArena::GuestArenaSetHostReadHook_nid_no_patch(&PrepareHostRead); } -std::atomic readGuard{nullptr}; - void SetReadGuard(void (*guard)(std::uint64_t address, std::size_t bytes)) { readGuard.store(guard, std::memory_order_release); + GuestArena::GuestArenaSetHostReadHook_nid_no_patch(&PrepareHostRead); +} + +void GuardRead(std::uint64_t address, std::size_t bytes) { + if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, bytes); } void FlushGpuWrites(std::uint64_t address, std::size_t bytes) { @@ -1976,7 +2336,7 @@ void FlushGpuWrites(std::uint64_t address, std::size_t bytes) { void Read(std::uint64_t address, std::span destination, std::size_t alignment) { if (destination.empty()) return; - if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size()); + GuardRead(address, destination.size()); FlushGpuWrites(address, destination.size()); const TimedAccess timed(CounterRead, destination.size()); CountReadCaller(__builtin_return_address(0)); @@ -1988,9 +2348,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) { @@ -2006,7 +2383,7 @@ void copyCommitted(std::uint64_t address, std::span destination) { void ReadCommitted(std::uint64_t address, std::span destination) { if (destination.empty()) return; - if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size()); + GuardRead(address, destination.size()); FlushGpuWrites(address, destination.size()); const TimedAccess timed(CounterRead, destination.size()); CountReadCaller(__builtin_return_address(0)); @@ -2015,7 +2392,7 @@ void ReadCommitted(std::uint64_t address, std::span destination) { void ReadCommittedUnsynced(std::uint64_t address, std::span destination) { if (destination.empty()) return; - if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size()); + GuardRead(address, destination.size()); const TimedAccess timed(CounterRead, destination.size()); CountReadCaller(__builtin_return_address(0)); copyCommitted(address, destination); @@ -2132,3 +2509,10 @@ void Write(std::uint64_t address, std::span source, std::size_t extern "C" void AgcDriverCheckGuestMemory_nid_postfix(const void* pointer, std::size_t bytes, std::size_t alignment, bool writable) { AgcDriver::GuestMemory::CheckRange(pointer, bytes, alignment, writable); } + +extern "C" void AgcDriverStoreGuestLabel_nid_postfix(std::uint64_t* pointer, std::uint64_t value) { + AgcDriver::GuestMemory::CheckRange(pointer, sizeof(*pointer), std::atomic_ref::required_alignment, true); + AgcDriver::GuestMemory::StoreOwnBytes(reinterpret_cast(pointer), sizeof(*pointer), [&] { + std::atomic_ref(*pointer).store(value, std::memory_order_release); + }); +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/HostAllocator.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/HostAllocator.cpp new file mode 100644 index 0000000000..cbbb9c549d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/HostAllocator.cpp @@ -0,0 +1,179 @@ +#ifdef _WIN32 + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::size_t Granule = 16; +constexpr std::size_t LargestCached = 1024; +constexpr std::size_t Classes = LargestCached / Granule; + +constexpr std::uint16_t Depth(std::size_t sizeClass) { + return sizeClass < 256 / Granule ? 64 : 16; +} + +struct Cache { + void* heads[Classes]; + std::uint16_t counts[Classes]; +}; + +Cache* const Closed = reinterpret_cast(std::uintptr_t{1}); + +constexpr unsigned long TlsSlotsOffset = 0x1480; +constexpr DWORD DirectSlots = 64; + +constexpr DWORD Unset = TLS_OUT_OF_INDEXES; +constexpr DWORD Failed = TLS_OUT_OF_INDEXES - 1; +std::atomic tlsSlot{Unset}; +std::atomic flsSlot{FLS_OUT_OF_INDEXES}; + +void Drain(Cache* cache) noexcept { + for (std::size_t sizeClass = 0; sizeClass < Classes; ++sizeClass) { + while (void* block = cache->heads[sizeClass]) { + cache->heads[sizeClass] = *static_cast(block); + std::free(block); + } + cache->counts[sizeClass] = 0; + } +} + +void WINAPI ThreadEnded(void* data) { + auto* cache = static_cast(data); + if (cache == nullptr || cache == Closed) return; + const auto slot = tlsSlot.load(std::memory_order_acquire); + if (slot < Failed) TlsSetValue(slot, Closed); + Drain(cache); + std::free(cache); +} + +DWORD Slot() noexcept { + auto slot = tlsSlot.load(std::memory_order_acquire); + if (slot != Unset) return slot; + const DWORD error = GetLastError(); + const DWORD fls = FlsAlloc(ThreadEnded); + DWORD tls = fls == FLS_OUT_OF_INDEXES ? TLS_OUT_OF_INDEXES : TlsAlloc(); + if (tls == TLS_OUT_OF_INDEXES || tls >= Failed) { + if (tls != TLS_OUT_OF_INDEXES) TlsFree(tls); + if (fls != FLS_OUT_OF_INDEXES) FlsFree(fls); + tls = Failed; + } + DWORD expected = Unset; + if (tlsSlot.compare_exchange_strong(expected, tls, std::memory_order_acq_rel)) { + if (tls != Failed) flsSlot.store(fls, std::memory_order_release); + SetLastError(error); + return tls; + } + if (tls != Failed) { + TlsFree(tls); + FlsFree(fls); + } + SetLastError(error); + return expected; +} + +Cache* SlotValue(DWORD slot) noexcept { + if (slot < DirectSlots) return reinterpret_cast(__readgsqword(TlsSlotsOffset + slot * sizeof(void*))); + const DWORD error = GetLastError(); + auto* value = static_cast(TlsGetValue(slot)); + SetLastError(error); + return value; +} + +Cache* CreateCache(DWORD slot) noexcept { + const auto fls = flsSlot.load(std::memory_order_acquire); + if (fls == FLS_OUT_OF_INDEXES) return nullptr; + auto* cache = static_cast(std::calloc(1, sizeof(Cache))); + if (cache == nullptr) return nullptr; + const DWORD error = GetLastError(); + if (!FlsSetValue(fls, cache) || !TlsSetValue(slot, cache)) { + FlsSetValue(fls, nullptr); + std::free(cache); + cache = nullptr; + } + SetLastError(error); + return cache; +} + +Cache* CurrentCache() noexcept { + const auto slot = Slot(); + if (slot == Failed) return nullptr; + auto* cache = SlotValue(slot); + if (cache == Closed) return nullptr; + return cache != nullptr ? cache : CreateCache(slot); +} + +void* TryAllocate(std::size_t bytes) noexcept { + if (bytes == 0) bytes = 1; + if (bytes <= LargestCached) { + const auto sizeClass = (bytes - 1) / Granule; + if (auto* cache = CurrentCache()) { + if (void* block = cache->heads[sizeClass]) { + cache->heads[sizeClass] = *static_cast(block); + --cache->counts[sizeClass]; + return block; + } + } + return std::malloc((sizeClass + 1) * Granule); + } + return std::malloc(bytes); +} + +void* Allocate(std::size_t bytes) { + for (;;) { + if (void* block = TryAllocate(bytes)) return block; + const auto handler = std::get_new_handler(); + if (handler == nullptr) throw std::bad_alloc(); + handler(); + } +} + +void Release(void* block, std::size_t bytes = 0) noexcept { + if (block == nullptr) return; + if (auto* cache = CurrentCache()) { + const auto usable = bytes != 0 && bytes % Granule == 0 ? bytes : _msize(block); + if (usable >= Granule && usable < LargestCached + Granule) { + const auto sizeClass = usable / Granule - 1; + if (cache->counts[sizeClass] < Depth(sizeClass)) { + *static_cast(block) = cache->heads[sizeClass]; + cache->heads[sizeClass] = block; + ++cache->counts[sizeClass]; + return; + } + } + } + std::free(block); +} + +} + +void* operator new(std::size_t bytes) { return Allocate(bytes); } +void* operator new[](std::size_t bytes) { return Allocate(bytes); } +void* operator new(std::size_t bytes, const std::nothrow_t&) noexcept { + try { + return Allocate(bytes); + } catch (...) { + return nullptr; + } +} +void* operator new[](std::size_t bytes, const std::nothrow_t&) noexcept { + try { + return Allocate(bytes); + } catch (...) { + return nullptr; + } +} +void operator delete(void* block) noexcept { Release(block); } +void operator delete[](void* block) noexcept { Release(block); } +void operator delete(void* block, std::size_t bytes) noexcept { Release(block, bytes); } +void operator delete[](void* block, std::size_t bytes) noexcept { Release(block, bytes); } +void operator delete(void* block, const std::nothrow_t&) noexcept { Release(block); } +void operator delete[](void* block, const std::nothrow_t&) noexcept { Release(block); } + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp new file mode 100644 index 0000000000..63748fa18c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp @@ -0,0 +1,74 @@ +#include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp" +#include +#include +#include +#include + +namespace AgcDriver { +namespace { + +struct HostHdrState { + std::mutex mutex; + HostHdrStatus status; +}; + +HostHdrState& State() { + static HostHdrState state; + return state; +} + +std::atomic presentingHdr10{false}; + +bool SameDisplay(const HostDisplayHdr& a, const HostDisplayHdr& b) { + return a.hdr == b.hdr && a.maxLuminance == b.maxLuminance && a.minLuminance == b.minLuminance && a.maxFullFrameLuminance == b.maxFullFrameLuminance; +} + +void Publish(HostHdrStatus& status, const HostDisplayHdr& display, bool surfaceHdr10) { + const bool displayChanged = !SameDisplay(status.display, display); + const bool available = display.hdr && surfaceHdr10 && std::getenv("APS5_FORCE_SDR") == nullptr; + const bool availabilityChanged = available != status.available; + status.display = display; + status.surfaceHdr10 = surfaceHdr10; + status.available = available; + if (displayChanged) ++status.displayGeneration; + if (!availabilityChanged && !(available && displayChanged)) return; + ++status.generation; + if (available) std::fprintf(stderr, "[videoout] HDR output available: peak %.1f nits, full frame %.1f nits, black %.4f nits\n", display.maxLuminance, display.maxFullFrameLuminance, display.minLuminance); + else std::fprintf(stderr, "[videoout] HDR output unavailable (display in HDR: %s, surface offers HDR10: %s)\n", display.hdr ? "yes" : "no", surfaceHdr10 ? "yes" : "no"); +} + +} + +void SetPresentingHdr10(bool hdr10) { + presentingHdr10.store(hdr10, std::memory_order_release); +} + +} + +extern "C" void AgcDriverSetHostDisplayHdr_nid_postfix(const AgcDriver::HostDisplayHdr& display) { + auto& state = AgcDriver::State(); + std::lock_guard lock(state.mutex); + AgcDriver::Publish(state.status, display, state.status.surfaceHdr10); +} + +extern "C" void AgcDriverSetSurfaceHdr10_nid_postfix(bool offered) { + auto& state = AgcDriver::State(); + std::lock_guard lock(state.mutex); + AgcDriver::Publish(state.status, state.status.display, offered); +} + +extern "C" void AgcDriverNoteReportedToneMapPeak_nid_postfix(float nits) { + auto& state = AgcDriver::State(); + std::lock_guard lock(state.mutex); + state.status.reportedToneMapPeak = nits; +} + +extern "C" void AgcDriverHostHdr_nid_postfix(AgcDriver::HostHdrStatus& status) { + auto& state = AgcDriver::State(); + std::lock_guard lock(state.mutex); + status = state.status; +} + +extern "C" bool AgcDriverPresentingHdr10_nid_postfix() { + return AgcDriver::presentingHdr10.load(std::memory_order_acquire); +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp new file mode 100644 index 0000000000..ca74a794d9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -0,0 +1,785 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" +#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayFrag_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayVert_spv.h" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#endif + +namespace AgcDriver { +namespace { + +static_assert(sizeof(OverlayRect) == 20, "overlay rectangles are read as one ivec4 and one uint per instance"); + +struct OverlayParameters { + std::array, OverlayColorCount> palette; + std::array extent; +}; + +std::atomic toggleCount{0}; +std::atomic gpuTotal{0}; +std::atomic driverTotal{0}; +std::atomic pipelineTotal{0}; + +struct PacketCounterRegistry { + std::mutex mutex; + std::deque counters; + std::vector unused; +}; + +PacketCounterRegistry& packetCounterRegistry() { + static auto* registry = new PacketCounterRegistry; + return *registry; +} + +struct PacketCounterLease { + PacketCounters* counters = nullptr; + ~PacketCounterLease() { + if (counters == nullptr) return; + auto& registry = packetCounterRegistry(); + std::lock_guard lock(registry.mutex); + registry.unused.push_back(counters); + } +}; + +constexpr double GigabyteBytes = 1024.0 * 1024.0 * 1024.0; + +const OverlaySettings& settings() { + static const OverlaySettings parsed = ParseOverlaySettings(std::getenv("ANYPS5_OVERLAY"), std::getenv("ANYPS5_OVERLAY_POS")); + return parsed; +} + +std::int32_t panelWidth(std::int32_t scale) { + return (OverlayMetrics::InnerWidth + 2 * OverlayMetrics::Padding) * scale; +} + +std::int32_t panelHeight(const OverlayContent& content, std::int32_t scale) { + const auto lines = static_cast(content.above.size() + content.below.size()); + const auto glyphGap = OverlayMetrics::LinePitch - OverlayMetrics::GlyphHeight; + return (2 * OverlayMetrics::Padding + lines * OverlayMetrics::LinePitch + OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap - glyphGap) * scale; +} + +bool fits(VkExtent2D extent, const OverlayContent& content, std::int32_t scale) { + const auto margins = 2 * OverlayMetrics::Margin * scale; + return panelWidth(scale) + margins <= static_cast(extent.width) && panelHeight(content, scale) + margins <= static_cast(extent.height); +} + +float srgbDecode(float value) { + return value <= 0.04045f ? value / 12.92f : std::pow((value + 0.055f) / 1.055f, 2.4f); +} + +float pqEncode(float nits) { + constexpr double m1 = 2610.0 / 16384.0; + constexpr double m2 = 2523.0 / 4096.0 * 128.0; + constexpr double c1 = 3424.0 / 4096.0; + constexpr double c2 = 2413.0 / 4096.0 * 32.0; + constexpr double c3 = 2392.0 / 4096.0 * 32.0; + const double power = std::pow(std::clamp(static_cast(nits) / 10000.0, 0.0, 1.0), m1); + return static_cast(std::pow((c1 + c2 * power) / (1.0 + c3 * power), m2)); +} + +bool srgbFormat(VkFormat format) { + return format == VK_FORMAT_B8G8R8A8_SRGB || format == VK_FORMAT_R8G8B8A8_SRGB || format == VK_FORMAT_A8B8G8R8_SRGB_PACK32; +} + +} + +OverlaySettings ParseOverlaySettings(const char* enabled, const char* position) { + OverlaySettings parsed; + const std::string_view visible = enabled != nullptr ? enabled : ""; + if (visible == "1") parsed.visible = true; + else if (!visible.empty() && visible != "0") throw std::runtime_error("ANYPS5_OVERLAY must be 1 or 0, not '" + std::string(visible) + "'"); + const std::string_view corner = position != nullptr ? position : ""; + if (corner.empty() || corner == "tl") parsed.corner = OverlayCorner::TopLeft; + else if (corner == "tr") parsed.corner = OverlayCorner::TopRight; + else if (corner == "bl") parsed.corner = OverlayCorner::BottomLeft; + else if (corner == "br") parsed.corner = OverlayCorner::BottomRight; + else throw std::runtime_error("ANYPS5_OVERLAY_POS must be tl, tr, bl or br, not '" + std::string(corner) + "'"); + return parsed; +} + +void PerformanceCounters::AddGpuNanoseconds(std::uint64_t nanoseconds) { + gpuTotal.fetch_add(nanoseconds, std::memory_order_relaxed); +} + +void PerformanceCounters::AddDriverNanoseconds(std::uint64_t nanoseconds) { + driverTotal.fetch_add(nanoseconds, std::memory_order_relaxed); +} + +void PerformanceCounters::CountPipeline() { + pipelineTotal.fetch_add(1, std::memory_order_relaxed); +} + +std::uint64_t PerformanceCounters::GpuNanoseconds() { + return gpuTotal.load(std::memory_order_relaxed); +} + +std::uint64_t PerformanceCounters::DriverNanoseconds() { + return driverTotal.load(std::memory_order_relaxed); +} + +std::uint64_t PerformanceCounters::Pipelines() { + return pipelineTotal.load(std::memory_order_relaxed); +} + +PacketCounters& PacketCounters::ThisThread() { + auto& lease = HostThreadLocal(); + if (lease.counters == nullptr) { + auto& registry = packetCounterRegistry(); + std::lock_guard lock(registry.mutex); + if (registry.unused.empty()) { + lease.counters = ®istry.counters.emplace_back(); + } else { + lease.counters = registry.unused.back(); + registry.unused.pop_back(); + } + } + return *lease.counters; +} + +PacketCounters::Totals PacketCounters::Sum() { + auto& registry = packetCounterRegistry(); + std::lock_guard lock(registry.mutex); + Totals totals; + for (const auto& counters : registry.counters) { + totals.draws += counters.draws.load(std::memory_order_relaxed); + totals.dispatches += counters.dispatches.load(std::memory_order_relaxed); + } + return totals; +} + +bool PerformanceCounters::GpuEnabled() { + return Graphics::Recorder::GpuTimingEnabled(); +} + +bool PerformanceCounters::DriverEnabled() { + static const bool enabled = std::getenv("APS5_PROFILE_DRAW") != nullptr; + return enabled; +} + +void OverlayStatistics::AddPresent(Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds) { + AddPresent(at, OverlayCounters{gpuNanoseconds, driverNanoseconds}); +} + +void OverlayStatistics::AddPresent(Clock::time_point at, const OverlayCounters& counters) { + if (!window.empty()) { + Graphics::Require(at >= window.back().at, "presents reached the performance overlay out of order"); + frameTimes[frameNext] = std::chrono::duration(at - window.back().at).count(); + frameNext = (frameNext + 1) % FrameHistory; + frameCount = std::min(frameCount + 1, FrameHistory); + previous = window.back(); + } + window.push_back({at, counters}); + while (at - window.front().at > Window) window.pop_front(); +} + +void OverlayStatistics::Reset() { + window.clear(); + previous.reset(); + frameTimes = {}; + frameCount = 0; + frameNext = 0; +} + +double OverlayStatistics::Fps() const { + if (window.size() < 2) { + const auto frameMs = LastFrameMs(); + return frameMs > 0 ? 1000.0 / frameMs : 0; + } + const auto seconds = std::chrono::duration(window.back().at - window.front().at).count(); + return seconds > 0 ? static_cast(window.size() - 1) / seconds : 0; +} + +double OverlayStatistics::LastFrameMs() const { + return frameCount == 0 ? 0 : frameTimes[(frameNext + FrameHistory - 1) % FrameHistory]; +} + +std::vector OverlayStatistics::FrameTimes() const { + std::vector ordered; + ordered.reserve(frameCount); + for (std::size_t i = 0; i < frameCount; ++i) ordered.push_back(frameTimes[(frameNext + FrameHistory - frameCount + i) % FrameHistory]); + return ordered; +} + +OverlayStatistics::Range OverlayStatistics::FrameTimeRange() const { + const auto times = FrameTimes(); + if (times.empty()) return {}; + Range range{times.front(), 0, times.front()}; + double sum = 0; + for (const auto time : times) { + range.minimum = std::min(range.minimum, time); + range.maximum = std::max(range.maximum, time); + sum += time; + } + range.average = sum / static_cast(times.size()); + return range; +} + +double OverlayStatistics::perFrame(std::uint64_t OverlayCounters::*counter) const { + const bool windowed = window.size() >= 2; + if (!windowed && !previous) return 0; + const auto first = windowed ? window.front().counters.*counter : previous->counters.*counter; + const auto last = window.back().counters.*counter; + const auto frames = windowed ? window.size() - 1 : std::size_t{1}; + return last >= first ? static_cast(last - first) / static_cast(frames) : 0; +} + +double OverlayStatistics::GpuMsPerFrame() const { + return perFrame(&OverlayCounters::gpuNanoseconds) / 1e6; +} + +double OverlayStatistics::DriverMsPerFrame() const { + return perFrame(&OverlayCounters::driverNanoseconds) / 1e6; +} + +double OverlayStatistics::DrawsPerFrame() const { + return perFrame(&OverlayCounters::draws); +} + +double OverlayStatistics::DispatchesPerFrame() const { + return perFrame(&OverlayCounters::dispatches); +} + +std::uint64_t OverlayStatistics::PipelinesInWindow() const { + if (window.empty()) return 0; + const auto first = window.size() < 2 && previous ? previous->counters.pipelines : window.front().counters.pipelines; + const auto last = window.back().counters.pipelines; + return last >= first ? last - first : 0; +} + +OverlayCounters OverlayStatistics::Latest() const { + return window.empty() ? OverlayCounters{} : window.back().counters; +} + +const std::array& OverlayGlyph(char character) { + using Glyph = std::array; + static constexpr Glyph space{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array digits{{ + {0x0e, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0e}, + {0x04, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x0e}, + {0x0e, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1f}, + {0x1f, 0x02, 0x04, 0x02, 0x01, 0x11, 0x0e}, + {0x02, 0x06, 0x0a, 0x12, 0x1f, 0x02, 0x02}, + {0x1f, 0x10, 0x1e, 0x01, 0x01, 0x11, 0x0e}, + {0x06, 0x08, 0x10, 0x1e, 0x11, 0x11, 0x0e}, + {0x1f, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08}, + {0x0e, 0x11, 0x11, 0x0e, 0x11, 0x11, 0x0e}, + {0x0e, 0x11, 0x11, 0x0f, 0x01, 0x02, 0x0c}, + }}; + static constexpr Glyph period{0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0c}; + static constexpr Glyph colon{0x00, 0x0c, 0x0c, 0x00, 0x0c, 0x0c, 0x00}; + static constexpr Glyph slash{0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x00}; + static constexpr Glyph a{0x0e, 0x11, 0x11, 0x11, 0x1f, 0x11, 0x11}; + static constexpr Glyph b{0x1e, 0x11, 0x11, 0x1e, 0x11, 0x11, 0x1e}; + static constexpr Glyph c{0x0e, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0e}; + static constexpr Glyph d{0x1c, 0x12, 0x11, 0x11, 0x11, 0x12, 0x1c}; + static constexpr Glyph e{0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x1f}; + static constexpr Glyph f{0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x10}; + static constexpr Glyph g{0x0e, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0f}; + static constexpr Glyph h{0x11, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11}; + static constexpr Glyph i{0x0e, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e}; + static constexpr Glyph k{0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11}; + static constexpr Glyph l{0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f}; + static constexpr Glyph m{0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11}; + static constexpr Glyph n{0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11}; + static constexpr Glyph o{0x0e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e}; + static constexpr Glyph p{0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10}; + static constexpr Glyph r{0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11}; + static constexpr Glyph s{0x0f, 0x10, 0x10, 0x0e, 0x01, 0x01, 0x1e}; + static constexpr Glyph t{0x1f, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04}; + static constexpr Glyph u{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e}; + static constexpr Glyph v{0x11, 0x11, 0x11, 0x11, 0x11, 0x0a, 0x04}; + static constexpr Glyph w{0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0a}; + static constexpr Glyph x{0x11, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x11}; + if (character >= '0' && character <= '9') return digits[static_cast(character - '0')]; + switch (character) { + case ' ': return space; + case '.': return period; + case ':': return colon; + case '/': return slash; + case 'A': return a; + case 'B': return b; + case 'C': return c; + case 'D': return d; + case 'E': return e; + case 'F': return f; + case 'G': return g; + case 'H': return h; + case 'I': return i; + case 'K': return k; + case 'L': return l; + case 'M': return m; + case 'N': return n; + case 'O': return o; + case 'P': return p; + case 'R': return r; + case 'S': return s; + case 'T': return t; + case 'U': return u; + case 'V': return v; + case 'W': return w; + case 'X': return x; + default: throw std::runtime_error(std::string("the performance overlay font has no glyph for '") + character + "'"); + } +} + +std::string FormatOverlayValue(double value) { + const double clamped = std::isfinite(value) ? std::clamp(value, 0.0, 9999.0) : 0.0; + char text[16]; + std::snprintf(text, sizeof(text), clamped < 99.95 ? "%.1f" : "%.0f", clamped); + return text; +} + +std::string FormatOverlayRate(double value) { + if (!std::isfinite(value) || value <= 0 || value >= 0.995) return FormatOverlayValue(value); + char text[16]; + std::snprintf(text, sizeof(text), "%.2f", value); + return text; +} + +std::string FormatOverlayCount(double value) { + const long long rounded = std::isfinite(value) && value > 0 ? std::llround(std::min(value, 1e12)) : 0; + char text[24]; + if (rounded < 10000) std::snprintf(text, sizeof(text), "%lld", rounded); + else if (rounded < 1000000) std::snprintf(text, sizeof(text), "%lldK", rounded / 1000); + else std::snprintf(text, sizeof(text), "%lldM", std::min(rounded / 1000000, 999)); + return text; +} + +std::string FormatOverlayGigabytes(std::uint64_t bytes) { + return FormatOverlayValue(static_cast(bytes) / GigabyteBytes); +} + +OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver) { + return ComposeOverlay(statistics, gpu, driver, OverlayDetails{}); +} + +OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver, const OverlayDetails& details) { + OverlayContent content; + if (gpu || driver) content.above.push_back("PROFILED"); + content.above.push_back("FPS: " + FormatOverlayRate(statistics.Fps())); + content.above.push_back("FRAME: " + FormatOverlayValue(statistics.LastFrameMs()) + " MS"); + content.frameTimes = statistics.FrameTimes(); + const auto range = statistics.FrameTimeRange(); + content.below.push_back("MIN/AVG/MAX: " + FormatOverlayValue(range.minimum) + "/" + FormatOverlayValue(range.average) + "/" + FormatOverlayValue(range.maximum)); + if (gpu) content.below.push_back("GPU: " + FormatOverlayValue(statistics.GpuMsPerFrame()) + " MS"); + if (driver) content.below.push_back("DRIVER: " + FormatOverlayValue(statistics.DriverMsPerFrame()) + " MS"); + const auto latest = statistics.Latest(); + if (latest.draws != 0 || latest.dispatches != 0) content.below.push_back("DRAWS: " + FormatOverlayCount(statistics.DrawsPerFrame()) + " DISPATCH: " + FormatOverlayCount(statistics.DispatchesPerFrame())); + if (latest.pipelines != 0) content.below.push_back("SHADERS: " + FormatOverlayCount(static_cast(statistics.PipelinesInWindow())) + " TOTAL: " + FormatOverlayCount(static_cast(latest.pipelines))); + if (details.vram) content.below.push_back("VRAM: " + FormatOverlayGigabytes(details.vram->used) + "/" + FormatOverlayGigabytes(details.vram->budget) + " GB"); + if (details.ram) content.below.push_back("RAM: " + FormatOverlayGigabytes(*details.ram) + " GB"); + if (details.display.width != 0 && details.display.height != 0 && details.window.width != 0 && details.window.height != 0) { + const auto size = [](VkExtent2D extent) { return std::to_string(extent.width) + "X" + std::to_string(extent.height); }; + for (const std::string_view separator : {" / ", "/"}) { + auto line = "RES: " + size(details.display) + std::string(separator) + size(details.window); + if (line.size() > static_cast(OverlayMetrics::Columns)) continue; + content.below.push_back(std::move(line)); + break; + } + } + return content; +} + +bool OverlaySampleDue(std::optional sampledAt, OverlayStatistics::Clock::time_point now) { + return !sampledAt || now - *sampledAt >= std::chrono::seconds(1); +} + +std::optional ReadVramBudget(const Graphics::Context& context) { + if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return std::nullopt; + const auto heap = Graphics::LargestDeviceLocalHeap(context.memory); + if (!heap) return std::nullopt; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &reported}; + context.memoryProperties2(context.physical, &properties); + if (reported.heapBudget[*heap] == 0) return std::nullopt; + return OverlayMemory{reported.heapUsage[*heap], reported.heapBudget[*heap]}; +} + +std::optional ReadProcessPrivateBytes() { +#ifdef _WIN32 + PROCESS_MEMORY_COUNTERS_EX counters{}; + counters.cb = sizeof(counters); + if (K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&counters), sizeof(counters)) == 0) return std::nullopt; + return static_cast(counters.PrivateUsage); +#else + std::FILE* status = std::fopen("/proc/self/status", "r"); + if (status == nullptr) return std::nullopt; + std::optional anonymousKb; + std::uint64_t swapKb = 0; + char line[256]; + while (std::fgets(line, sizeof(line), status) != nullptr) { + unsigned long long value = 0; + if (std::sscanf(line, "RssAnon: %llu kB", &value) == 1) anonymousKb = value; + else if (std::sscanf(line, "VmSwap: %llu kB", &value) == 1) swapKb = value; + } + std::fclose(status); + if (!anonymousKb) return std::nullopt; + return (*anonymousKb + swapKb) * 1024u; +#endif +} + +std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content) { + auto scale = std::max(1, static_cast((extent.height + 240) / 480)); + while (scale > 1 && !fits(extent, content, scale)) --scale; + return scale; +} + +OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner) { + Graphics::Require(extent.width != 0 && extent.height != 0, "the performance overlay needs a destination"); + Graphics::Require(content.frameTimes.size() <= OverlayStatistics::FrameHistory, "the performance overlay graph holds at most 120 frames"); + OverlayLayout layout; + const auto scale = OverlayScale(extent, content); + layout.scale = scale; + const auto width = panelWidth(scale); + const auto height = panelHeight(content, scale); + const auto margin = OverlayMetrics::Margin * scale; + const bool right = corner == OverlayCorner::TopRight || corner == OverlayCorner::BottomRight; + const bool bottom = corner == OverlayCorner::BottomLeft || corner == OverlayCorner::BottomRight; + const auto left = std::max(0, right ? static_cast(extent.width) - margin - width : margin); + const auto top = std::max(0, bottom ? static_cast(extent.height) - margin - height : margin); + layout.bounds = {left, top, width, height, static_cast(OverlayColor::Background)}; + layout.rects.push_back(layout.bounds); + const auto text = [&](const std::string& line, std::int32_t row) { + Graphics::Require(line.size() <= static_cast(OverlayMetrics::Columns), "a performance overlay line is wider than the panel"); + for (std::size_t column = 0; column < line.size(); ++column) { + const auto& glyph = OverlayGlyph(line[column]); + const auto glyphLeft = left + (OverlayMetrics::Padding + static_cast(column) * OverlayMetrics::Advance) * scale; + for (std::int32_t y = 0; y < OverlayMetrics::GlyphHeight; ++y) { + const auto bits = glyph[static_cast(y)]; + std::int32_t x = 0; + while (x < OverlayMetrics::GlyphWidth) { + if ((bits & (0x10u >> x)) == 0) { + ++x; + continue; + } + auto end = x; + while (end < OverlayMetrics::GlyphWidth && (bits & (0x10u >> end)) != 0) ++end; + layout.rects.push_back({glyphLeft + x * scale, top + (row + y) * scale, (end - x) * scale, scale, static_cast(OverlayColor::Text)}); + x = end; + } + } + } + }; + auto row = OverlayMetrics::Padding; + for (const auto& line : content.above) { + text(line, row); + row += OverlayMetrics::LinePitch; + } + const auto graphBottom = top + (row + OverlayMetrics::GraphHeight) * scale; + if (!content.frameTimes.empty()) { + double maximum = 0; + double sum = 0; + for (const auto time : content.frameTimes) { + maximum = std::max(maximum, time); + sum += time; + } + const double fullScale = std::max(maximum, 2.0 * sum / static_cast(content.frameTimes.size())); + const auto first = static_cast(OverlayStatistics::FrameHistory - content.frameTimes.size()); + for (std::size_t i = 0; i < content.frameTimes.size() && fullScale > 0; ++i) { + const auto pixels = std::clamp(static_cast(std::lround(content.frameTimes[i] / fullScale * OverlayMetrics::GraphHeight * scale)), 1, OverlayMetrics::GraphHeight * scale); + layout.rects.push_back({left + (OverlayMetrics::Padding + first + static_cast(i)) * scale, graphBottom - pixels, scale, pixels, static_cast(OverlayColor::Graph)}); + } + } + row += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap; + for (const auto& line : content.below) { + text(line, row); + row += OverlayMetrics::LinePitch; + } + return layout; +} + +std::array, OverlayColorCount> OverlayPalette(VkFormat format, VkColorSpaceKHR colorSpace) { + std::array, OverlayColorCount> palette{{{0.0f, 0.0f, 0.0f, 0.6f}, {1.0f, 1.0f, 1.0f, 1.0f}, {0.35f, 0.8f, 0.35f, 1.0f}}}; + if (colorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT) { + for (auto& color : palette) { + const float red = srgbDecode(color[0]); + const float green = srgbDecode(color[1]); + const float blue = srgbDecode(color[2]); + const std::array bt2020{ + 0.6274f * red + 0.3293f * green + 0.0433f * blue, + 0.0691f * red + 0.9195f * green + 0.0114f * blue, + 0.0164f * red + 0.0880f * green + 0.8956f * blue}; + for (std::size_t channel = 0; channel < 3; ++channel) color[channel] = pqEncode(bt2020[channel] * PqReferenceWhiteNits); + } + } else if (srgbFormat(format)) { + for (auto& color : palette) { + for (std::size_t channel = 0; channel < 3; ++channel) color[channel] = srgbDecode(color[channel]); + } + } + return palette; +} + +PerformanceOverlay::PerformanceOverlay(const Graphics::Context& context, std::size_t slotCount) : context(context), slots(slotCount) { + Graphics::Require(slotCount != 0, "the performance overlay needs at least one slot"); + try { + const VkPushConstantRange push{VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(OverlayParameters)}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.pushConstantRangeCount = 1; + layoutInfo.pPushConstantRanges = &push; + Graphics::Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout performance overlay"); + vertex = Graphics::CreateShaderModule(context, PERFORMANCE_OVERLAY_VERT_SPV, sizeof(PERFORMANCE_OVERLAY_VERT_SPV), "vkCreateShaderModule performance overlay vertex"); + fragment = Graphics::CreateShaderModule(context, PERFORMANCE_OVERLAY_FRAG_SPV, sizeof(PERFORMANCE_OVERLAY_FRAG_SPV), "vkCreateShaderModule performance overlay fragment"); + } catch (...) { + release(); + throw; + } +} + +PerformanceOverlay::~PerformanceOverlay() { + release(); +} + +void PerformanceOverlay::releaseTransient(Slot& slot) noexcept { + for (const auto& [view, framebuffer] : slot.transient) { + if (framebuffer) context.Function("vkDestroyFramebuffer")(context.device, framebuffer, nullptr); + if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); + } + slot.transient.clear(); +} + +void PerformanceOverlay::Release(std::size_t index) noexcept { + if (index < slots.size()) releaseTransient(slots[index]); +} + +void PerformanceOverlay::release() noexcept { + for (auto& slot : slots) { + releaseTransient(slot); + slot.rects.reset(); + slot.capacity = 0; + } + for (const auto& [format, entry] : pipelines) context.Function("vkDestroyPipeline")(context.device, entry, nullptr); + pipelines.clear(); + for (const auto& [key, entry] : renderPasses) context.Function("vkDestroyRenderPass")(context.device, entry, nullptr); + renderPasses.clear(); + if (fragment) context.Function("vkDestroyShaderModule")(context.device, fragment, nullptr); + if (vertex) context.Function("vkDestroyShaderModule")(context.device, vertex, nullptr); + if (pipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, pipelineLayout, nullptr); + fragment = vertex = VK_NULL_HANDLE; + pipelineLayout = VK_NULL_HANDLE; +} + +bool PerformanceOverlay::Visible() { + return settings().visible != ((toggleCount.load(std::memory_order_relaxed) & 1u) != 0); +} + +void PerformanceOverlay::Toggle() { + toggleCount.fetch_add(1, std::memory_order_relaxed); +} + +OverlayCorner PerformanceOverlay::Corner() { + return settings().corner; +} + +void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display) { + restartIfToggled(); + if (OverlaySampleDue(sampledAt, at)) { + details.vram = ReadVramBudget(context); + details.ram = ReadProcessPrivateBytes(); + sampledAt = at; + } + details.display = display; + const auto packets = PacketCounters::Sum(); + statistics.AddPresent(at, OverlayCounters{PerformanceCounters::GpuEnabled() ? PerformanceCounters::GpuNanoseconds() : 0, PerformanceCounters::DriverEnabled() ? PerformanceCounters::DriverNanoseconds() : 0, packets.draws, packets.dispatches, PerformanceCounters::Pipelines()}); +} + +void PerformanceOverlay::restartIfToggled() { + const auto current = toggleCount.load(std::memory_order_relaxed); + if (current == toggles) return; + statistics.Reset(); + details = {}; + sampledAt.reset(); + toggles = current; +} + +VkRenderPass PerformanceOverlay::renderPass(VkFormat format, VkImageLayout initialLayout, VkImageLayout finalLayout) { + const auto key = std::tuple{format, initialLayout, finalLayout}; + if (const auto found = renderPasses.find(key); found != renderPasses.end()) return found->second; + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + Graphics::Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) != 0, "the performance overlay cannot blend into this format"); + VkAttachmentDescription attachment{}; + attachment.format = format; + attachment.samples = VK_SAMPLE_COUNT_1_BIT; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + 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 = initialLayout; + attachment.finalLayout = finalLayout; + const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &reference; + const bool transfer = finalLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + const VkSubpassDependency dependencies[2] = { + {VK_SUBPASS_EXTERNAL, 0, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, 0}, + {0, VK_SUBPASS_EXTERNAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, transfer ? VK_PIPELINE_STAGE_TRANSFER_BIT : VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, transfer ? VK_ACCESS_TRANSFER_READ_BIT : VkAccessFlags{0}, 0}, + }; + VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + info.attachmentCount = 1; + info.pAttachments = &attachment; + info.subpassCount = 1; + info.pSubpasses = &subpass; + info.dependencyCount = 2; + info.pDependencies = dependencies; + VkRenderPass created = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateRenderPass")(context.device, &info, nullptr, &created), "vkCreateRenderPass performance overlay"); + return renderPasses.emplace(key, created).first->second; +} + +VkPipeline PerformanceOverlay::pipeline(VkFormat format, VkRenderPass compatible) { + if (const auto found = pipelines.find(format); found != pipelines.end()) return found->second; + VkPipelineShaderStageCreateInfo stages[2]{{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}, {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}}; + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[0].module = vertex; + stages[0].pName = "main"; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + stages[1].module = fragment; + stages[1].pName = "main"; + const VkVertexInputBindingDescription binding{0, sizeof(OverlayRect), VK_VERTEX_INPUT_RATE_INSTANCE}; + const VkVertexInputAttributeDescription attributes[2] = {{0, 0, VK_FORMAT_R32G32B32A32_SINT, 0}, {1, 0, VK_FORMAT_R32_UINT, 16}}; + VkPipelineVertexInputStateCreateInfo vertexInput{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + vertexInput.vertexBindingDescriptionCount = 1; + vertexInput.pVertexBindingDescriptions = &binding; + vertexInput.vertexAttributeDescriptionCount = 2; + vertexInput.pVertexAttributeDescriptions = attributes; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewport.viewportCount = 1; + viewport.scissorCount = 1; + VkPipelineRasterizationStateCreateInfo rasterization{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + rasterization.polygonMode = VK_POLYGON_MODE_FILL; + rasterization.cullMode = VK_CULL_MODE_NONE; + rasterization.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + rasterization.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineColorBlendAttachmentState blendAttachment{}; + blendAttachment.blendEnable = VK_TRUE; + blendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + blendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blendAttachment.colorBlendOp = VK_BLEND_OP_ADD; + blendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + blendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + blendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; + blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + blend.attachmentCount = 1; + blend.pAttachments = &blendAttachment; + const VkDynamicState dynamicStates[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + dynamic.dynamicStateCount = 2; + dynamic.pDynamicStates = dynamicStates; + VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + info.stageCount = 2; + info.pStages = stages; + info.pVertexInputState = &vertexInput; + info.pInputAssemblyState = &assembly; + info.pViewportState = &viewport; + info.pRasterizationState = &rasterization; + info.pMultisampleState = &multisample; + info.pColorBlendState = &blend; + info.pDynamicState = &dynamic; + info.layout = pipelineLayout; + info.renderPass = compatible; + VkPipeline created = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateGraphicsPipelines")(context.device, VK_NULL_HANDLE, 1, &info, nullptr, &created), "vkCreateGraphicsPipelines performance overlay"); + return pipelines.emplace(format, created).first->second; +} + +OverlayRect PerformanceOverlay::Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout) { + auto shown = details; + shown.window = extent; + return Record(commands, slot, destination, format, colorSpace, extent, initialLayout, finalLayout, ComposeOverlay(statistics, PerformanceCounters::GpuEnabled(), PerformanceCounters::DriverEnabled(), shown), Corner()); +} + +OverlayRect PerformanceOverlay::Record(VkCommandBuffer commands, std::size_t index, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout, const OverlayContent& content, OverlayCorner corner) { + Graphics::Require(index < slots.size(), "performance overlay slot is out of range"); + Graphics::Require(destination != VK_NULL_HANDLE && extent.width != 0 && extent.height != 0, "the performance overlay destination is unavailable"); + auto& slot = slots[index]; + releaseTransient(slot); + const auto layout = LayOutOverlay(content, extent, corner); + if (slot.capacity < layout.rects.size()) { + const auto capacity = std::max(layout.rects.size(), 1024); + slot.rects = std::make_unique(context, capacity * sizeof(OverlayRect), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + slot.capacity = capacity; + } + std::memcpy(slot.rects->Bytes().data(), layout.rects.data(), layout.rects.size() * sizeof(OverlayRect)); + const auto pass = renderPass(format, initialLayout, finalLayout); + const auto drawing = pipeline(format, pass); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = destination; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = format; + viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkImageViewUsageCreateInfo usageInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + usageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + for (const auto layout : {initialLayout, finalLayout}) { + if (layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) usageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) usageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + } + viewInfo.pNext = &usageInfo; + VkImageView view = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView performance overlay"); + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = pass; + framebufferInfo.attachmentCount = 1; + framebufferInfo.pAttachments = &view; + framebufferInfo.width = extent.width; + framebufferInfo.height = extent.height; + framebufferInfo.layers = 1; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + const auto created = context.Function("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer); + slot.transient.emplace_back(view, framebuffer); + Graphics::Check(created, "vkCreateFramebuffer performance overlay"); + const auto right = std::min(layout.bounds.x + layout.bounds.width, static_cast(extent.width)); + const auto bottom = std::min(layout.bounds.y + layout.bounds.height, static_cast(extent.height)); + const VkRect2D area{{layout.bounds.x, layout.bounds.y}, {static_cast(std::max(1, right - layout.bounds.x)), static_cast(std::max(1, bottom - layout.bounds.y))}}; + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = pass; + begin.framebuffer = framebuffer; + begin.renderArea = area; + context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, drawing); + const VkViewport viewport{0.0f, 0.0f, static_cast(extent.width), static_cast(extent.height), 0.0f, 1.0f}; + context.Function("vkCmdSetViewport")(commands, 0, 1, &viewport); + context.Function("vkCmdSetScissor")(commands, 0, 1, &area); + const OverlayParameters parameters{OverlayPalette(format, colorSpace), {static_cast(extent.width), static_cast(extent.height)}}; + context.Function("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(parameters), ¶meters); + const VkBuffer buffer = slot.rects->Handle(); + const VkDeviceSize offset = 0; + context.Function("vkCmdBindVertexBuffers")(commands, 0, 1, &buffer, &offset); + context.Function("vkCmdDraw")(commands, 4, static_cast(layout.rects.size()), 0, 0); + context.Function("vkCmdEndRenderPass")(commands); + return layout.bounds; +} + +} + +extern "C" void AgcDriverTogglePerformanceOverlay_nid_postfix() { + AgcDriver::PerformanceOverlay::Toggle(); +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp index 6d1b13254a..e9578a2e7c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp @@ -3,11 +3,15 @@ #include "prx/libSceAgcDriver/Execution/include/TraceFile.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/TraceRevision.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include #include +#include #include #include #include #include +#include #ifdef _WIN32 #include #else @@ -15,6 +19,11 @@ #include #include #endif +#if defined(_MSC_VER) && !defined(__clang__) +#include +#elif defined(__x86_64__) || defined(__i386__) +#include +#endif #ifndef _WIN32 extern char** environ; @@ -46,6 +55,9 @@ struct Session { double gpuBusyAtOpen = 0; std::optional gpuClock; std::vector presents; + bool bench = false; + TraceBenchIntervals benchIntervals; + std::uint64_t benchDropped = 0; }; Session& session() { @@ -63,6 +75,11 @@ std::unique_ptr& summaryWriter() { return output; } +std::unique_ptr& benchWriter() { + static std::unique_ptr output; + return output; +} + std::filesystem::path traceDirectory() { const char* directory = std::getenv("APS5_TRACE_DIR"); return std::filesystem::path(directory != nullptr ? directory : "."); @@ -91,6 +108,64 @@ std::string operatingSystem() { #endif } +std::optional processorName() { + std::array words{}; +#if defined(_MSC_VER) && !defined(__clang__) + int registers[4]{}; + __cpuid(registers, static_cast(0x80000000u)); + if (static_cast(registers[0]) < 0x80000004u) return std::nullopt; + for (unsigned leaf = 0; leaf < 3; ++leaf) { + __cpuid(registers, static_cast(0x80000002u + leaf)); + std::memcpy(words.data() + leaf * 4, registers, sizeof registers); + } +#elif defined(__x86_64__) || defined(__i386__) + if (__get_cpuid_max(0x80000000u, nullptr) < 0x80000004u) return std::nullopt; + for (unsigned leaf = 0; leaf < 3; ++leaf) { + unsigned eax = 0, ebx = 0, ecx = 0, edx = 0; + if (__get_cpuid(0x80000002u + leaf, &eax, &ebx, &ecx, &edx) == 0) throw std::runtime_error("cpuid brand string leaf failed"); + words[leaf * 4] = eax; + words[leaf * 4 + 1] = ebx; + words[leaf * 4 + 2] = ecx; + words[leaf * 4 + 3] = edx; + } +#else + return std::nullopt; +#endif + std::string name(reinterpret_cast(words.data()), sizeof words); + name.resize(std::strlen(name.c_str())); + const auto first = name.find_first_not_of(' '); + if (first == std::string::npos) return std::nullopt; + return name.substr(first, name.find_last_not_of(' ') - first + 1); +} + +std::uint64_t memoryMiB() { +#ifdef _WIN32 + MEMORYSTATUSEX status{}; + status.dwLength = sizeof(status); + if (::GlobalMemoryStatusEx(&status) == 0) throw std::system_error(static_cast(::GetLastError()), std::system_category(), "GlobalMemoryStatusEx"); + return status.ullTotalPhys / (1024 * 1024); +#else + const long pages = ::sysconf(_SC_PHYS_PAGES); + const long size = ::sysconf(_SC_PAGE_SIZE); + if (pages <= 0 || size <= 0) throw std::system_error(errno, std::generic_category(), "sysconf physical memory"); + return static_cast(pages) * static_cast(size) / (1024 * 1024); +#endif +} + +const std::optional& host() { + static const auto value = []() -> std::optional { + auto cpu = processorName(); + if (!cpu) return std::nullopt; + return TraceHost{std::move(*cpu), std::thread::hardware_concurrency(), memoryMiB()}; + }(); + return value; +} + +std::optional title() { + if (!HasAppParamJson_nid_no_patch()) return std::nullopt; + return TraceTitle{GetAppTitleId_nid_postfix().value, GetAppTitle_nid_postfix().value, GetAppContentVersion_nid_no_patch().value}; +} + std::vector> environment() { #ifdef _WIN32 char** entries = _environ; @@ -120,6 +195,9 @@ TraceSummary identity(std::uint32_t width, std::uint32_t height, const VulkanDev summary.os = operatingSystem(); summary.width = width; summary.height = height; + summary.host = host(); + static const auto running = title(); + summary.title = running; return summary; } @@ -138,6 +216,7 @@ double seconds(Clock::duration elapsed) { void PerformanceTrace::BindThread(const char* name, std::size_t capacity) { if (!Configured() || local != nullptr) return; const bool windowed = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr; + if (windowed && capacity == TraceThread::DefaultCapacity) capacity = TraceThread::ParseCapacity(std::getenv("APS5_TRACE_EVENTS")); auto bound = std::make_unique(name, presentCount, windowed ? capacity : 0, buffering); auto& state = session(); std::lock_guard lock(state.mutex); @@ -184,6 +263,23 @@ void PerformanceTrace::FlushInterval(Clock::time_point now, std::uint32_t width, summary.gpuBusyMs = VulkanDevice::PresentCounts().gpuBusyMs - state.intervalGpuBusy; summary.interval = std::make_pair(seconds(state.intervalPresents.front() - state.first), seconds(now - state.first)); summaryWriter()->Write(TraceFormat::Summary(summary, snapshots, state.intervalPresents)); + if (!state.bench) return; + TraceBench::Keep(state.benchIntervals, std::make_shared(TraceBenchInterval{summary.interval->first, summary.interval->second, summary.firstPresent, width, height, summary.gpuBusyMs, TraceFormat::FrameMicroseconds(state.intervalPresents)}), state.benchDropped); + if (!benchWriter()) { + const auto name = "aps5-bench-" + summary.commit + "-" + std::to_string(processId()) + ".json.gz"; + benchWriter() = std::make_unique([directory = traceDirectory(), name](const TraceBenchSnapshot& snapshot) { + try { + std::filesystem::create_directories(directory); + TraceFile::ReplaceGzip(directory / name, TraceFormat::Bench(snapshot.summary, snapshot.intervals, snapshot.dropped)); + } catch (const std::exception& error) { + std::fprintf(stderr, "[trace] writing %s failed: %s\n", name.c_str(), error.what()); + std::fflush(stderr); + throw; + } + }); + ProfilePrint_nid_no_patch("[trace] benchmark frames go to %s, rewritten every interval\n", name.c_str()); + } + benchWriter()->Write(TraceBenchSnapshot{std::move(summary), state.benchIntervals, state.benchDropped}); } void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const VulkanDevice* device) { @@ -194,6 +290,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const if (!state.started) { state.window = TraceWindow::Parse(std::getenv("APS5_TRACE_FRAMES"), std::getenv("APS5_TRACE_SECONDS")); state.interval = TraceInterval::Parse(std::getenv("APS5_TRACE_SUMMARY_EVERY")); + state.bench = TraceBench::Parse(std::getenv("APS5_TRACE_BENCH"), state.interval.has_value()); state.first = now; state.started = true; } @@ -220,6 +317,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const { std::lock_guard gpu(gpuMutex); gpuRanges.clear(); + gpuLost = 0; gpuRanges.reserve(GpuRangeCapacity); } state.presents.push_back(now); @@ -255,7 +353,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const if (state.gpuClock && device != nullptr && device->CalibrateClock(clock.gpuNs, clock.cpu)) { std::lock_guard gpu(gpuMutex); gpuSlices = TraceFormat::GpuSlices(gpuRanges, *state.gpuClock, clock, state.presents.front()); - snapshots.push_back({"gpu", gpuSlices, {}, gpuDropped}); + snapshots.push_back({"gpu", gpuSlices, {}, gpuLost != 0, gpuLost}); } const auto process = processId(); auto summary = identity(width, height, device); @@ -285,6 +383,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const void PerformanceTrace::StopOutput() { if (writer()) writer()->Stop(); if (summaryWriter()) summaryWriter()->Stop(); + if (benchWriter()) benchWriter()->Stop(); } } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/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 3563c24a97..20b73f581a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -114,16 +114,14 @@ std::vector copySource(std::uint64_t source, std::size_t bytes, bool return data; } -constexpr std::uint32_t DmaSelectGds = 1; -constexpr std::uint32_t DmaSelectNowhere = 2; bool gdsRange(std::uint64_t offset, std::size_t bytes) { return offset <= GdsBytes && bytes <= GdsBytes - offset; } std::vector dmaSourceBytes(std::span packet) { - const std::size_t bytes = packet[6] & 0x3ffffffu; - if (dmaSource(packet) != DmaSelectGds) return copySource(address(packet[2], packet[3]), bytes, dmaSource(packet) == 2); + const std::size_t bytes = DmaBytes(packet); + if (dmaSource(packet) != DmaSelectGds) return copySource(address(packet[2], packet[3]), bytes, dmaSource(packet) == DmaSourceData); return copySource(GdsAddress() + packet[2], bytes, false); } @@ -133,13 +131,11 @@ void copyMemory(std::uint64_t source, std::uint64_t destination, std::size_t byt GuestMemory::Write(destination, copySource(source, bytes, immediate)); } -constexpr std::uint32_t CopyDataGpuClockSource = 18; +constexpr std::uint32_t CopyDataRegisterSource = 0; constexpr std::uint32_t CopyDataCachePolicy = (3u << 13u) | (3u << 25u); +constexpr std::uint32_t ShaderRegisterBase = 0x2c00; constexpr std::uint32_t ContextRegisterBase = 0xa000; - -bool copyDataToRegister(std::span packet) { - return ((packet[1] >> 8u) & 0xfu) == 0; -} +constexpr std::uint32_t UserConfigRegisterBase = 0xc000; std::uint32_t copyDataCount(std::span packet) { return (packet[1] & 0x10000u) != 0 ? 2u : 1u; @@ -149,9 +145,28 @@ std::uint64_t gpuClockCount() { return static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count() / 10); } -std::vector copyDataSource(std::span packet, std::size_t bytes) { - const auto source = ((packet[1] & 0xfu) << 1u) | ((packet[1] >> 30u) & 1u); - if (source != CopyDataGpuClockSource) return copySource(address(packet[2], packet[3]), bytes, source >= 10); +std::uint32_t registerBankBase(std::uint32_t offset) { + if (offset - ShaderRegisterBase < 0x400u) return ShaderRegisterBase; + if (offset - ContextRegisterBase < 0x400u) return ContextRegisterBase; + if (offset - UserConfigRegisterBase < 0x4000u) return UserConfigRegisterBase; + return 0; +} + +std::vector copyDataSource(std::span packet, const QueueState& queue, std::size_t bytes) { + const auto source = CopyDataSource(packet); + if (source == CopyDataRegisterSource) { + std::vector data(bytes); + for (std::size_t word = 0; word < bytes / 4; ++word) { + const auto offset = packet[2] + static_cast(word); + const auto base = registerBankBase(offset); + const auto& bank = base == ShaderRegisterBase ? queue.shader : base == ContextRegisterBase ? queue.context : queue.userConfig; + if (base == 0 || !bank.contains(offset - base)) throw std::runtime_error("COPY_DATA source register 0x" + ToHex(offset) + " has no known value"); + const auto value = bank.at(offset - base); + for (std::size_t i = 0; i < 4; ++i) data[word * 4 + i] = static_cast(value >> (i * 8)); + } + return data; + } + if (source != CopyDataGpuClockSource) return copySource(address(packet[2], packet[3]), bytes, CopyDataImmediate(source)); const auto value = gpuClockCount(); std::vector data(bytes); for (std::size_t i = 0; i < bytes; ++i) data[i] = static_cast(value >> (i * 8)); @@ -160,7 +175,7 @@ std::vector copyDataSource(std::span packet, std } -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) { @@ -179,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; } @@ -256,7 +276,7 @@ void Validate(std::span packet, std::uint32_t queue) { break; } case 0x0c: break; - case 0x17: graphics(); size(6); break; + case 0x17: size(6); break; case 0x1a: graphics(); require(packet.size() == 3 || packet.size() == 5, "invalid context-state packet size"); @@ -339,10 +359,10 @@ void Validate(std::span packet, std::uint32_t queue) { require((packet[8] & 3u) == 0 && packet[8] >= (opcode == 0x2c ? 16u : 20u), "invalid indirect draw stride"); require((packet[9] & ~0x20u) == (opcode == 0x2c ? 2u : 0u), "unsupported indirect draw initiator"); break; - case 0x15: size(5); if ((packet[4] & ~0xa066u) != 0x1u || (packet[4] & 0x22u) == 0x22u) throw std::runtime_error("dispatch modifiers 0x" + ToHex(packet[4]) + " are not implemented"); break; + case 0x15: size(5); if ((packet[4] & ~0xa06eu) != 0x1u || (packet[4] & 0x22u) == 0x22u) throw std::runtime_error("dispatch modifiers 0x" + ToHex(packet[4]) + " are not implemented"); break; case 0x16: require(packet.size() == 3 || packet.size() == 4, "invalid indirect dispatch size"); - require((packet.back() & ~0xa064u) == 0x1u, "indirect dispatch modifiers are not implemented"); + require((packet.back() & ~0xa06cu) == 0x1u, "indirect dispatch modifiers are not implemented"); break; case 0x22: size(5); @@ -358,9 +378,11 @@ void Validate(std::span packet, std::uint32_t queue) { require((packet[1] & 0x7fffffffu) == 0 && (packet[2] & 0x7fffffffu) == 0, "CONTEXT_CONTROL register loading and shadowing are not implemented"); break; case 0x46: { - require((packet[1] & ~0x73fu) == 0, "unsupported EVENT_WRITE flags or reserved bits"); const auto eventType = packet[1] & 0x3fu; - const auto eventIndex = (packet[1] >> 8u) & 7u; + const auto eventIndex = (packet[1] >> 8u) & 0xfu; + const auto streamOutStatistics = (eventType >= 0x01u && eventType <= 0x03u) || eventType == 0x20u; + require(!streamOutStatistics || (eventIndex != 3 && (eventIndex < 8 || eventIndex > 11)), "stream-out statistics sampling to memory is not implemented"); + require((packet[1] & ~0x73fu) == 0, "unsupported EVENT_WRITE flags or reserved bits"); switch (eventType) { case 0x07: case 0x0f: case 0x10: size(2); @@ -381,6 +403,11 @@ void Validate(std::span packet, std::uint32_t queue) { size(2); require(eventIndex == 0, "invalid VGT_FLUSH event index"); break; + case 0x01: case 0x02: case 0x03: case 0x20: + graphics(); + size(2); + require(eventIndex == 0, "invalid stream-out statistics event index"); + break; case 0x39: graphics(); size(4); @@ -429,7 +456,6 @@ void Validate(std::span packet, std::uint32_t queue) { size(opcode == 0x3c ? 7 : 9); require((packet[1] & 0x10u) != 0, "register-space WAIT_REG_MEM is not implemented"); require((packet[1] & 7u) <= 6u, "invalid WAIT_REG_MEM compare function"); - require((packet[2] & (opcode == 0x3c ? 3u : 7u)) == 0, "misaligned WAIT_REG_MEM address"); break; } case 0x45: { @@ -444,6 +470,8 @@ void Validate(std::span packet, std::uint32_t queue) { case 0x49: { size(8); const auto dataSelect = packet[2] >> 29u; + require(dataSelect != 4, "RELEASE_MEM of the 64-bit system counter (DATA_SEL 4) is not implemented"); + require(dataSelect <= 5, "reserved RELEASE_MEM DATA_SEL"); require(dataSelect <= 3, "GDS RELEASE_MEM data is not implemented"); require(dataSelect == 0 || address(packet[3], packet[4]) == 0 || (packet[3] & (dataSelect == 1 ? 3u : 7u)) == 0, "misaligned RELEASE_MEM destination"); break; @@ -460,6 +488,11 @@ void Validate(std::span packet, std::uint32_t queue) { require(packet.size() >= 5, "WRITE_DATA has no data"); require((packet[1] & ~(0x40110f00u | WriteDataCachePolicy)) == 0, "WRITE_DATA engine or reserved fields are not implemented"); const auto destination = (packet[1] >> 8u) & 0xfu; + if (destination == 0) { + const auto count = (packet[1] & 0x10000u) != 0 ? 1u : packet.size() - 4; + require(packet[3] == 0 && packet[2] >= 0xc340u && packet[2] <= 0xc347u && count <= 0xc348u - packet[2], "WRITE_DATA register destination outside the thread-trace user-data registers is not implemented"); + break; + } require(destination == 1 || destination == 2 || destination == 3 || destination == 5, "WRITE_DATA register destination is not implemented"); require((packet[2] & 3u) == 0, "misaligned WRITE_DATA destination"); break; @@ -467,28 +500,29 @@ void Validate(std::span packet, std::uint32_t queue) { case 0x40: { size(6); require((packet[1] & ~(0x40110f0fu | CopyDataCachePolicy)) == 0, "COPY_DATA engine or reserved fields are not implemented"); - const auto source = ((packet[1] & 0xfu) << 1u) | ((packet[1] >> 30u) & 1u); + const auto source = CopyDataSource(packet); const auto destination = ((packet[1] >> 8u) & 0xfu) << 1u; - if (copyDataToRegister(packet)) { + if (CopyDataToRegister(packet)) { require(source == 2 || source == 4, "COPY_DATA to a register from a register, GDS, immediate or clock source is not implemented"); require((packet[2] & 3u) == 0, "misaligned COPY_DATA source"); require(packet[5] == 0 && packet[4] >= ContextRegisterBase && packet[4] - ContextRegisterBase <= 0x400u - copyDataCount(packet), "COPY_DATA to a register outside the context registers is not implemented"); break; } require(destination == 2 || destination == 4 || destination == 10, "COPY_DATA GDS or performance-counter destination is not implemented"); - require(source == 2 || source == 4 || source == 5 || source == 10 || source == 11 || source == CopyDataGpuClockSource, "COPY_DATA register, GDS or reference-clock source is not implemented"); + require(source == 2 || source == 4 || source == 5 || source == 10 || source == 11 || source == CopyDataGpuClockSource || source == CopyDataRegisterSource, "COPY_DATA GDS or reference-clock source is not implemented"); + require(source != CopyDataRegisterSource || (packet[3] == 0 && registerBankBase(packet[2]) != 0 && registerBankBase(packet[2]) == registerBankBase(packet[2] + copyDataCount(packet) - 1)), "COPY_DATA from a register outside the shader, context and user config registers is not implemented"); require(source < 10 || source == CopyDataGpuClockSource || ((packet[1] & 0x10000u) == 0 && packet[3] == 0), "64-bit immediate COPY_DATA is not implemented"); break; } case 0x50: size(7); require((packet[1] & ~(0xe0300001u | DmaSourceCachePolicy | DmaDestinationCachePolicy)) == 0, "DMA_DATA reserved fields are not implemented"); - require(memorySelector(dmaDestination(packet)) || dmaDestination(packet) == DmaSelectGds || dmaDestination(packet) == DmaSelectNowhere, "DMA_DATA register destination is not implemented"); - require(dmaDestination(packet) != DmaSelectNowhere || memorySelector(dmaSource(packet)), "DMA_DATA prefetch of a register, GDS or immediate source is not implemented"); - require(memorySelector(dmaSource(packet)) || dmaSource(packet) == 2 || dmaSource(packet) == DmaSelectGds, "DMA_DATA register source is not implemented"); - require(dmaSource(packet) != DmaSelectGds || (packet[3] == 0 && gdsRange(packet[2], packet[6] & 0x3ffffffu)), "DMA_DATA GDS source range exceeds the GDS"); - require(dmaDestination(packet) != DmaSelectGds || (packet[5] == 0 && gdsRange(packet[4], packet[6] & 0x3ffffffu)), "DMA_DATA GDS destination range exceeds the GDS"); - require(dmaSource(packet) != 2 || packet[3] == 0, "DMA_DATA immediate exceeds 32 bits"); + require(memorySelector(dmaDestination(packet)) || dmaDestination(packet) == DmaSelectGds || dmaDestination(packet) == DmaDestinationNowhere, "DMA_DATA register destination is not implemented"); + require(dmaDestination(packet) != DmaDestinationNowhere || memorySelector(dmaSource(packet)), "DMA_DATA prefetch of a register, GDS or immediate source is not implemented"); + require(memorySelector(dmaSource(packet)) || dmaSource(packet) == DmaSourceData || dmaSource(packet) == DmaSelectGds, "DMA_DATA register source is not implemented"); + require(dmaSource(packet) != DmaSelectGds || (packet[3] == 0 && gdsRange(packet[2], DmaBytes(packet))), "DMA_DATA GDS source range exceeds the GDS"); + require(dmaDestination(packet) != DmaSelectGds || (packet[5] == 0 && gdsRange(packet[4], DmaBytes(packet))), "DMA_DATA GDS destination range exceeds the GDS"); + require(dmaSource(packet) != DmaSourceData || packet[3] == 0, "DMA_DATA immediate exceeds 32 bits"); break; default: throw std::runtime_error("known packet has no validator"); } @@ -520,20 +554,27 @@ bool waitCompares(std::span packet, bool wide, std::uint64_ } +std::size_t WaitAwaitedBytes(std::span packet) { + const bool wide = ((packet[0] >> 8u) & 0xffu) == 0x93u; + return wide && (packet.size() < 8 || packet[7] != 0) ? 8 : 4; +} + bool WaitSatisfied(std::span packet) { const bool wide = ((packet[0] >> 8u) & 0xffu) == 0x93u; - const auto source = address(packet[2], packet[3]); + const auto awaitedBytes = WaitAwaitedBytes(packet); + const auto source = address(packet[2] & (awaitedBytes == 8 ? ~7u : ~3u), packet[3]); std::uint64_t value = 0; // Named for the [hooksync] attribution (the read goes through the flush hook). const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Wait); - GuestMemory::Read(source, std::as_writable_bytes(std::span(&value, 1)).first(wide ? 8 : 4), wide ? 8 : 4); + GuestMemory::Read(source, std::as_writable_bytes(std::span(&value, 1)).first(awaitedBytes), awaitedBytes); return waitCompares(packet, wide, value); } bool WaitSatisfiedUnchecked(std::span packet) { const bool wide = ((packet[0] >> 8u) & 0xffu) == 0x93u; - const auto source = address(packet[2], packet[3]); - const std::uint64_t value = wide ? *reinterpret_cast(source) : *reinterpret_cast(source); + const auto awaitedBytes = WaitAwaitedBytes(packet); + const auto source = address(packet[2] & (awaitedBytes == 8 ? ~7u : ~3u), packet[3]); + const std::uint64_t value = awaitedBytes == 8 ? *reinterpret_cast(source) : *reinterpret_cast(source); return waitCompares(packet, wide, value); } @@ -542,11 +583,6 @@ bool WaitComparesValue(std::span packet, std::uint64_t valu return waitCompares(packet, wide, value); } -std::size_t WaitAwaitedBytes(std::span packet) { - const bool wide = ((packet[0] >> 8u) & 0xffu) == 0x93u; - return wide && (packet.size() < 8 || packet[7] != 0) ? 8 : 4; -} - std::size_t ConditionalWords(std::span packet) { require(packet.size() == 5 && ((packet[0] >> 8u) & 0xffu) == 0x22u, "expected COND_EXEC packet"); return packet[4] & ConditionalWordsMask; @@ -585,6 +621,7 @@ std::optional DecodeLabelWrite(std::span packet return label; } if (opcode == 0x37 && packet.size() > 4) { + if (((packet[1] >> 8u) & 0xfu) == 0) return std::nullopt; const auto destination = address(packet[2], packet[3]); if (destination == 0) return std::nullopt; // WR_ONE_ADDR stores every value at the same address: the last one remains. @@ -601,28 +638,30 @@ std::optional ResolveStore(std::span packet, co const auto fits = [limit](std::uint64_t destination, std::size_t bytes) { return bytes <= limit && destination % 4 == 0 && bytes % 4 == 0; }; switch ((packet[0] >> 8u) & 0xffu) { case 0x40: { - if (packet.size() < 6 || copyDataToRegister(packet)) return std::nullopt; - const auto source = ((packet[1] & 0xfu) << 1u) | ((packet[1] >> 30u) & 1u); - const std::size_t bytes = (packet[1] & 0x10000u) != 0 ? 8 : 4; + if (packet.size() < 6 || CopyDataToRegister(packet)) return std::nullopt; + if (CopyDataSource(packet) == CopyDataRegisterSource) return std::nullopt; + const std::size_t bytes = CopyDataBytes(packet); const auto destination = address(packet[4], packet[5]); if (!fits(destination, bytes)) return std::nullopt; - return StoreWrite{destination, {}, copyDataSource(packet, bytes)}; + return StoreWrite{destination, {}, copyDataSource(packet, queue, bytes)}; } case 0x50: { if (packet.size() < 7 || dmaDestination(packet) == DmaSelectGds) return std::nullopt; - if (dmaDestination(packet) == DmaSelectNowhere) return StoreWrite{address(packet[2], packet[3]), {}, {}}; - const std::size_t bytes = packet[6] & 0x3ffffffu; + if (dmaDestination(packet) == DmaDestinationNowhere) return StoreWrite{address(packet[2], packet[3]), {}, {}}; + const std::size_t bytes = DmaBytes(packet); const auto destination = address(packet[4], packet[5]); if (!fits(destination, bytes)) return std::nullopt; return StoreWrite{destination, {}, dmaSourceBytes(packet)}; } case 0x83: { - if (packet.size() < 5 || packet[1] / 4 > QueueState::ConstantRamWords || packet[2] > QueueState::ConstantRamWords - packet[1] / 4) return std::nullopt; - const auto words = queue.ConstantRam().subspan(packet[1] / 4, packet[2]); + if (packet.size() != 5) return std::nullopt; + const auto dump = DecodeConstantRamDump(packet); + if (dump.offset / 4 > QueueState::ConstantRamWords || dump.words > QueueState::ConstantRamWords - dump.offset / 4) return std::nullopt; + const auto words = queue.ConstantRam().subspan(dump.offset / 4, dump.words); if (words.size_bytes() > limit) return std::nullopt; // A view of the queue's constant RAM: valid until the next CONST_RAM write on this queue, // which is a later packet of the same worker. - return StoreWrite{address(packet[3], packet[4]), std::as_bytes(words), {}}; + return StoreWrite{dump.destination, std::as_bytes(words), {}}; } default: return std::nullopt; } @@ -636,10 +675,88 @@ std::uint32_t DmaDestination(std::span packet) { return dmaDestination(packet); } +WaitFields DecodeWait(std::span packet) { + const auto opcode = packet.empty() ? 0u : (packet[0] >> 8u) & 0xffu; + require((opcode == 0x3cu && packet.size() == 7) || (opcode == 0x93u && packet.size() == 9), "expected WAIT_REG_MEM or WAIT_REG_MEM_64 packet"); + const bool wide = opcode == 0x93u; + const auto awaitedBytes = WaitAwaitedBytes(packet); + return {packet[1] & 7u, address(packet[2] & (awaitedBytes == 8 ? ~7u : ~3u), packet[3]), wide ? address(packet[4], packet[5]) : packet[4], wide ? address(packet[6], packet[7]) : packet[5], wide ? packet[8] : packet[6], awaitedBytes}; +} + +ConditionalWrite DecodeConditionalWrite(std::span packet) { + require(packet.size() == 9 && ((packet[0] >> 8u) & 0xffu) == 0x45u, "expected COND_WRITE packet"); + return {address(packet[2], packet[3]), address(packet[6], packet[7]), packet[8]}; +} + +ConstantRamDump DecodeConstantRamDump(std::span packet) { + require(packet.size() == 5 && ((packet[0] >> 8u) & 0xffu) == 0x83u, "expected DUMP_CONST_RAM packet"); + return {packet[1], packet[2], address(packet[3], packet[4])}; +} + +std::vector DecodeStores(std::span packet) { + require(!packet.empty(), "truncated PM4 header"); + const auto known = [](std::uint64_t destination, std::span bytes) { + return PacketStore{destination, bytes.size(), PacketStore::Value::Bytes, {bytes.begin(), bytes.end()}, false}; + }; + const auto pattern = [](std::uint64_t destination, std::uint64_t bytes, std::uint32_t value) { + const auto repeated = std::as_bytes(std::span(&value, 1)); + return PacketStore{destination, bytes, PacketStore::Value::Pattern, {repeated.begin(), repeated.end()}, false}; + }; + const auto runTime = [](std::uint64_t destination, std::uint64_t bytes) { return PacketStore{destination, bytes, PacketStore::Value::RunTime, {}, false}; }; + if (FillerPacket(packet[0])) return {}; + switch ((packet[0] >> 8u) & 0xffu) { + case 0x49: { + const auto label = DecodeLabelWrite(packet); + if (!label.has_value()) return {}; + if ((packet[2] >> 29u) == 3) return {runTime(label->address, label->Bytes().size())}; + return {known(label->address, label->Bytes())}; + } + case 0x37: { + const auto label = DecodeLabelWrite(packet); + if (!label.has_value()) return {}; + return {known(label->address, label->Bytes())}; + } + case 0x40: { + require(packet.size() == 6, "invalid COPY_DATA size"); + if (CopyDataToRegister(packet)) return {}; + const auto destination = address(packet[4], packet[5]); + if (CopyDataImmediate(CopyDataSource(packet))) return {pattern(destination, CopyDataBytes(packet), packet[2])}; + return {runTime(destination, CopyDataBytes(packet))}; + } + case 0x50: { + require(packet.size() == 7, "invalid DMA_DATA size"); + const auto bytes = DmaBytes(packet); + if (bytes == 0 || dmaDestination(packet) == DmaDestinationNowhere || dmaDestination(packet) == DmaSelectGds) return {}; + const auto destination = address(packet[4], packet[5]); + if (dmaSource(packet) == DmaSourceData) return {pattern(destination, bytes, packet[2])}; + return {runTime(destination, bytes)}; + } + case 0x45: { + const auto write = DecodeConditionalWrite(packet); + auto store = known(write.destination, std::as_bytes(std::span(&write.value, 1))); + store.conditional = true; + return {store}; + } + case 0x83: { + const auto dump = DecodeConstantRamDump(packet); + if (dump.words == 0) return {}; + return {runTime(dump.destination, static_cast(dump.words) * 4)}; + } + case 0x46: { + if (!SampleDump(packet)) return {}; + require(packet.size() == 4, "invalid EVENT_WRITE sample dump size"); + std::vector stores; + for (std::uint64_t block = 0; block < SampleDumpBlocks; ++block) stores.push_back(runTime(SampleDumpAddress(packet) + block * SampleDumpStride, SampleDumpBlockBytes)); + return stores; + } + default: return {}; + } +} + std::optional DecodeMemoryCopy(std::span packet) { if (packet.size() != 7 || ((packet[0] >> 8u) & 0xffu) != 0x50) return std::nullopt; if (!memorySelector(dmaSource(packet)) || !memorySelector(dmaDestination(packet))) return std::nullopt; - const std::size_t bytes = packet[6] & 0x3ffffffu; + const std::size_t bytes = DmaBytes(packet); const auto source = address(packet[2], packet[3]); const auto destination = address(packet[4], packet[5]); if (bytes == 0 || (source < destination + bytes && destination < source + bytes)) return std::nullopt; @@ -738,13 +855,10 @@ DrawParameters resolveIndirectDraw(std::span packet, const draw.firstVertex = offset->second; indirect.indxOffset = offset->second; } else { - // The CP clamps firstIndex + indexCount to INDEX_BUFFER_SIZE: the declared range is the - // hardware's view of the index buffer, so that is what gets bound. const auto indexSize = indexSizeOf(queue); require(queue.indexBufferSize != 0, "INDEX_BUFFER_SIZE has not been set"); const auto bytes = static_cast(queue.indexBufferSize) * indexSize; require(bytes <= std::numeric_limits::max() && bytes <= std::numeric_limits::max() - queue.indexBase, "index range size overflow"); - GuestMemory::CheckRange(reinterpret_cast(queue.indexBase), static_cast(bytes), indexSize); draw.indexAddress = queue.indexBase; draw.indexCount = queue.indexBufferSize; draw.indexSize = indexSize; @@ -822,15 +936,38 @@ std::vector ReadIndirectRegisters(std::span return pairs; } -void ExecuteIndirectRegisters(std::span packet, std::span pairs, QueueState& queue) { +void NoteDispatchRegisters(std::span packet, QueueState& queue) { + const bool direct = ((packet[0] >> 8u) & 0xffu) == 0x15u; + queue.shader.insert_or_assign(0x200, packet.back()); + for (std::uint32_t i = 0; i < 3; ++i) { + if (direct) queue.shader.insert_or_assign(0x201 + i, packet[1 + i]); + else queue.shader.erase(0x201 + i); + } +} + +bool IndirectRegistersValid(std::span pairs) { + for (std::size_t i = 0; i < pairs.size(); i += 2) { + if (pairs[i] == 0xffffffffu || (pairs[i] & ~0x70000000u) > 0xffffu) return false; + } + return true; +} + +void ExecuteIndirectRegisters(std::span packet, std::span pairs, QueueState& queue, bool skipInvalid) { const auto opcode = (packet[0] >> 8u) & 0xffu; require(packet.size() == 5 && IndirectRegisterOpcode(opcode), "expected indirect register packet"); require(pairs.size() == static_cast(packet[4]) * 2, "indirect register list does not match its packet"); - for (std::size_t i = 0; i < pairs.size(); i += 2) registerOffset(pairs[i]); - for (std::size_t i = 0; i < pairs.size(); i += 2) writeRegister(queue, opcode, registerOffset(pairs[i]), pairs[i + 1]); + const auto usable = [&](std::uint32_t value) { return !skipInvalid || IndirectRegistersValid(std::span(&value, 1)); }; + for (std::size_t i = 0; i < pairs.size(); i += 2) { + if (usable(pairs[i])) registerOffset(pairs[i]); + } + for (std::size_t i = 0; i < pairs.size(); i += 2) { + if (usable(pairs[i])) writeRegister(queue, opcode, registerOffset(pairs[i]), pairs[i + 1]); + } if (queue.savedContext.has_value() && TraceContextState()) { std::fprintf(stderr, "[context] indirect 0x%x:", opcode); - for (std::size_t i = 0; i < pairs.size(); i += 2) std::fprintf(stderr, " %x", registerOffset(pairs[i])); + for (std::size_t i = 0; i < pairs.size(); i += 2) { + if (usable(pairs[i])) std::fprintf(stderr, " %x", registerOffset(pairs[i])); + } std::fprintf(stderr, "\n"); } } @@ -930,7 +1067,7 @@ void Execute(std::span packet, QueueState& queue) { if (!warned && std::chrono::steady_clock::now() - start > std::chrono::seconds(5)) { warned = true; std::fprintf(stderr, "[gpu] WAIT_REG_MEM at 0x%llx still waiting after 5s (function %u ref 0x%x mask 0x%x)\n", - static_cast(address(packet[2], packet[3])), packet[1] & 7u, packet[4], packet[5]); + static_cast(address(packet[2] & (WaitAwaitedBytes(packet) == 8 ? ~7u : ~3u), packet[3])), packet[1] & 7u, packet[4], packet[5]); } } return; @@ -957,19 +1094,27 @@ void Execute(std::span packet, QueueState& queue) { case 0x81: std::copy(packet.begin() + 2, packet.end(), queue.WritableConstantRam().begin() + packet[1] / 4); return; - case 0x83: - GuestMemory::Write(address(packet[3], packet[4]), std::as_bytes(queue.ConstantRam().subspan(packet[1] / 4, packet[2])), 4); + case 0x83: { + const auto dump = DecodeConstantRamDump(packet); + GuestMemory::Write(dump.destination, std::as_bytes(queue.ConstantRam().subspan(dump.offset / 4, dump.words)), 4); return; + } case 0x45: { + const auto write = DecodeConditionalWrite(packet); std::uint32_t value = 0; { const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Wait); - GuestMemory::Read(address(packet[2], packet[3]), std::as_writable_bytes(std::span(&value, 1)), 4); + GuestMemory::Read(write.compared, std::as_writable_bytes(std::span(&value, 1)), 4); } - if (waitCompares(packet, false, value)) GuestMemory::Write(address(packet[6], packet[7]), std::as_bytes(packet.subspan(8, 1)), 4); + if (waitCompares(packet, false, value)) GuestMemory::Write(write.destination, std::as_bytes(std::span(&write.value, 1)), 4); return; } case 0x37: { + if (((packet[1] >> 8u) & 0xfu) == 0) { + const bool repeat = (packet[1] & 0x10000u) != 0; + for (std::size_t i = 4; i < packet.size(); ++i) writeRegister(queue, 0x79, packet[2] - 0xc000u + (repeat ? 0u : static_cast(i - 4)), packet[i]); + return; + } const auto destination = address(packet[2], packet[3]); if ((packet[1] & 0x10000u) != 0) { for (const auto& value : packet.subspan(4)) GuestMemory::Write(destination, std::as_bytes(std::span(&value, 1)), 4); @@ -977,7 +1122,7 @@ void Execute(std::span packet, QueueState& queue) { return; } case 0x40: { - if (copyDataToRegister(packet)) { + if (CopyDataToRegister(packet)) { std::array values{}; const auto count = copyDataCount(packet); const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Registers); @@ -986,15 +1131,15 @@ void Execute(std::span packet, QueueState& queue) { return; } const auto destination = address(packet[4], packet[5]); - const auto data = copyDataSource(packet, (packet[1] & 0x10000u) != 0 ? 8 : 4); + const auto data = copyDataSource(packet, queue, CopyDataBytes(packet)); GuestMemory::CheckRange(reinterpret_cast(destination), data.size(), 1, true); GuestMemory::Write(destination, data); return; } case 0x28: return; case 0x50: { - const std::size_t bytes = packet[6] & 0x3ffffffu; - if (bytes == 0 || dmaDestination(packet) == DmaSelectNowhere) return; + const std::size_t bytes = DmaBytes(packet); + if (bytes == 0 || dmaDestination(packet) == DmaDestinationNowhere) return; auto data = dmaSourceBytes(packet); const auto destination = dmaDestination(packet) == DmaSelectGds ? GdsAddress() + packet[4] : address(packet[4], packet[5]); GuestMemory::CheckRange(reinterpret_cast(destination), bytes, 1, true); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp index ddb45a05f2..e42e4e2b03 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp @@ -70,14 +70,8 @@ PresentationPass::PresentationPass(const Graphics::Context& context, std::size_t tableWrites[i].pBufferInfo = &tableInfo; } context.Function("vkUpdateDescriptorSets")(context.device, static_cast(tableWrites.size()), tableWrites.data(), 0, nullptr); - const auto createModule = context.Function("vkCreateShaderModule"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(PRESENTATION_PASS_VERT_SPV); - moduleInfo.pCode = PRESENTATION_PASS_VERT_SPV; - Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &vertex), "vkCreateShaderModule presentation vertex"); - moduleInfo.codeSize = sizeof(PRESENTATION_PASS_FRAG_SPV); - moduleInfo.pCode = PRESENTATION_PASS_FRAG_SPV; - Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &fragment), "vkCreateShaderModule presentation fragment"); + vertex = Graphics::CreateShaderModule(context, PRESENTATION_PASS_VERT_SPV, sizeof(PRESENTATION_PASS_VERT_SPV), "vkCreateShaderModule presentation vertex"); + fragment = Graphics::CreateShaderModule(context, PRESENTATION_PASS_FRAG_SPV, sizeof(PRESENTATION_PASS_FRAG_SPV), "vkCreateShaderModule presentation fragment"); } catch (...) { release(); throw; @@ -285,7 +279,7 @@ void PresentationPass::draw(VkCommandBuffer commands, const Slot& slot, const Ta context.Function("vkCmdEndRenderPass")(commands); } -void PresentationPass::RecordPresent(VkCommandBuffer commands, std::size_t index, VkImage destination, VkFormat format, VkExtent2D extent, VkImageLayout finalLayout) { +void PresentationPass::RecordPresent(VkCommandBuffer commands, std::size_t index, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout finalLayout) { Graphics::Require(index < slots.size() && slots[index].source != VK_NULL_HANDLE, "presentation pass slot has no source"); Graphics::Require(destination != VK_NULL_HANDLE && extent.width != 0 && extent.height != 0, "presentation pass destination is unavailable"); auto& slot = slots[index]; @@ -300,7 +294,9 @@ void PresentationPass::RecordPresent(VkCommandBuffer commands, std::size_t index } slot.transient.emplace_back(view, framebuffer); const auto rect = ComputeContainRect_nid_postfix(slot.width, slot.height, extent.width, extent.height); - const std::uint32_t mode = slot.mode | (TenBitFormat(format) ? ModeKeepTenBit : 0u); + const bool hdr10 = colorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT; + Graphics::Require(colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR || (hdr10 && (slot.mode & ModePq) != 0 && TenBitFormat(format)), "presentation pass presents only PQ display buffers to a 10-bit HDR10 destination"); + const std::uint32_t mode = (hdr10 ? slot.mode & ~ModePq : slot.mode) | (TenBitFormat(format) ? ModeKeepTenBit : 0u); const std::int32_t parameters[9] = {rect.x, rect.y, static_cast(rect.width), static_cast(rect.height), static_cast(slot.width), static_cast(slot.height), 1, static_cast(mode), std::bit_cast(slot.whiteNits)}; draw(commands, slot, entry, framebuffer, extent, parameters); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp new file mode 100644 index 0000000000..9e60cbac8c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp @@ -0,0 +1,81 @@ +#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" +#include "Decoder/Png.hpp" +#include "SDL_filesystem.h" +#include "SDL_error.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::atomic requested{false}; + +std::filesystem::path directory() { +#ifdef _WIN32 + const wchar_t* configured = _wgetenv(L"ANYPS5_SCREENSHOTS"); +#else + const char* configured = std::getenv("ANYPS5_SCREENSHOTS"); +#endif + if (configured != nullptr && configured[0] != 0) return configured; + const std::unique_ptr basePath(SDL_GetBasePath(), SDL_free); + if (!basePath) throw std::runtime_error(std::string("Screenshot: cannot locate the executable: ") + SDL_GetError()); + return std::filesystem::path(reinterpret_cast(basePath.get())) / "anyps5-screenshots"; +} + +std::string localTimestamp() { + const std::time_t now = std::time(nullptr); + std::tm local{}; +#ifdef _WIN32 + if (localtime_s(&local, &now) != 0) throw std::runtime_error("Screenshot: cannot read the local time"); +#else + if (localtime_r(&now, &local) == nullptr) throw std::runtime_error("Screenshot: cannot read the local time"); +#endif + char text[32]; + if (std::strftime(text, sizeof(text), "%Y%m%d-%H%M%S", &local) == 0) throw std::runtime_error("Screenshot: cannot format the local time"); + return text; +} + +std::filesystem::path unusedPath(const std::filesystem::path& folder, const std::string& stem) { + for (int index = 0; index < 1000; ++index) { + const auto path = folder / (index == 0 ? stem + ".png" : stem + "-" + std::to_string(index) + ".png"); + if (!std::filesystem::exists(path)) return path; + } + throw std::runtime_error("Screenshot: no unused file name for " + stem + " in " + folder.string()); +} + +} + +void AgcDriver::Screenshot::Request() { + requested.store(true); +} + +bool AgcDriver::Screenshot::Take() { + return requested.exchange(false); +} + +std::filesystem::path AgcDriver::Screenshot::Write(std::string_view titleId, std::uint32_t width, std::uint32_t height, std::span bgra) { + if (titleId.empty()) throw std::invalid_argument("Screenshot: empty title ID"); + const auto pixelCount = static_cast(width) * height; + if (width == 0 || height == 0 || bgra.size() != pixelCount * 4) throw std::invalid_argument("Screenshot: invalid BGRA8 frame"); + std::vector rgb(pixelCount * 3); + for (std::size_t pixel = 0; pixel < pixelCount; ++pixel) { + rgb[pixel * 3] = std::to_integer(bgra[pixel * 4 + 2]); + rgb[pixel * 3 + 1] = std::to_integer(bgra[pixel * 4 + 1]); + rgb[pixel * 3 + 2] = std::to_integer(bgra[pixel * 4]); + } + const auto png = Decoder::Png::Encode(rgb, width, height, 3); + const auto folder = directory(); + std::filesystem::create_directories(folder); + const auto path = unusedPath(folder, std::string(titleId) + "-" + localTimestamp()); + std::ofstream file; + file.exceptions(std::ios::failbit | std::ios::badbit); + file.open(path, std::ios::binary); + file.write(reinterpret_cast(png.data()), static_cast(png.size())); + file.close(); + return path; +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp index 0e55dd5eaa..660a9c007c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp @@ -30,6 +30,8 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta const auto* barycentric = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_BARYCENTRIC_FEATURES_KHR); 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"); @@ -45,10 +47,12 @@ 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"); checkFeature(spv::CapabilityMinLod, core.shaderResourceMinLod == VK_TRUE, "shaderResourceMinLod"); + checkFeature(spv::CapabilityInterpolationFunction, core.sampleRateShading == VK_TRUE, "sampleRateShading"); checkFeature(spv::CapabilityStorageImageMultisample, core.shaderStorageImageMultisample == VK_TRUE, "shaderStorageImageMultisample"); checkFeature(spv::CapabilityStorageImageReadWithoutFormat, core.shaderStorageImageReadWithoutFormat == VK_TRUE, "shaderStorageImageReadWithoutFormat"); checkFeature(spv::CapabilityStorageImageWriteWithoutFormat, core.shaderStorageImageWriteWithoutFormat == VK_TRUE, "shaderStorageImageWriteWithoutFormat"); @@ -59,10 +63,12 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta checkFeature(spv::CapabilitySampledImageArrayNonUniformIndexing, sampledNonUniform, "shaderSampledImageArrayNonUniformIndexing"); checkFeature(spv::CapabilityStorageImageArrayNonUniformIndexing, storageNonUniform, "shaderStorageImageArrayNonUniformIndexing"); checkFeature(spv::CapabilityShaderNonUniform, sampledNonUniform || storageNonUniform, "descriptor indexing"); + checkFeature(spv::CapabilityRuntimeDescriptorArray, indexing != nullptr && indexing->runtimeDescriptorArray == VK_TRUE, "runtimeDescriptorArray"); const bool barycentricEnabled = barycentric != nullptr && barycentric->fragmentShaderBarycentric == VK_TRUE; const bool meshEnabled = mesh != nullptr && mesh->meshShader == VK_TRUE; checkFeature(spv::CapabilityFragmentBarycentricKHR, barycentricEnabled, "fragmentShaderBarycentric"); checkFeature(spv::CapabilityMeshShadingEXT, meshEnabled, "meshShader"); + checkFeature(spv::CapabilityDrawParameters, drawParameters != nullptr && drawParameters->shaderDrawParameters == VK_TRUE, "shaderDrawParameters"); checkFeature(spv::CapabilityShaderClockKHR, clock != nullptr && clock->shaderSubgroupClock == VK_TRUE && clock->shaderDeviceClock == VK_TRUE, "shader clock"); Require(!target.fragmentShaderBarycentricEnabled || barycentricEnabled, "shader device profile exposes disabled barycentrics"); Require(!target.mesh || meshEnabled, "shader device profile exposes disabled mesh shaders"); @@ -73,6 +79,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 7344a3418c..253d832b7c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp @@ -1,16 +1,21 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include "Optimization/RequestMemoryView.hpp" #include "Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" +#include "ThreadOwned.hpp" #include #include +#include #include #include #include #include #include +#include #include #include @@ -51,6 +56,13 @@ struct CaptureProfile { // The driver's per-shader source handle memo: captures served from it, and resolves it made. std::atomic handleHits{0}; std::atomic handleMisses{0}; + std::atomic suppliedShared{0}; + std::atomic suppliedCopied{0}; + std::atomic suppliedSpilled{0}; + std::atomic vouchCalls{0}; + std::atomic vouchedBlocks{0}; + std::atomic refusedBlocks{0}; + std::atomic vouchedWords{0}; }; constexpr std::uint64_t WordWaitNanoseconds = 50000; @@ -65,6 +77,12 @@ bool CaptureProfiled() { return profile; } +template +std::uint64_t InitialEnd(const TSnapshots& initial) { + const auto& last = *std::prev(initial.end()); + return last.first + last.second.size(); +} + std::uint64_t NanosecondsSince(std::chrono::steady_clock::time_point started) { return static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()); } @@ -81,24 +99,126 @@ double WaitedMs() { return provider != nullptr ? provider() : 0.0; } +using GuestRuns = std::vector>; + +std::uint64_t RunEnd(const std::pair& run) { + return run.first + static_cast(run.second) * sizeof(std::uint32_t); +} + +bool RunsTouch(const GuestRuns& runs, std::uint64_t begin, std::uint64_t end) { + const auto after = std::upper_bound(runs.begin(), runs.end(), begin, [](std::uint64_t address, const std::pair& run) { return address < run.first; }); + if (after != runs.begin() && RunEnd(*std::prev(after)) > begin) return true; + return after != runs.end() && after->first < end; +} + +bool RunsOverlap(const GuestRuns& left, const GuestRuns& right) { + std::size_t a = 0; + std::size_t b = 0; + while (a < left.size() && b < right.size()) { + if (left[a].first < RunEnd(right[b]) && right[b].first < RunEnd(left[a])) return true; + if (RunEnd(left[a]) <= RunEnd(right[b])) ++a; + else ++b; + } + return false; +} + +template +std::vector>& pagePool() { + struct PagePoolStorage {}; + return HostThreadLocal>, PagePoolStorage>(); +} + +constexpr std::size_t PagePoolLimit = 64; + } void ShaderMemory::SetWaitedMsProvider(WaitedMsProvider provider) { waitedMsProvider.store(provider, std::memory_order_release); } -ShaderMemory::ShaderMemory(std::span regions, PendingWriteQuery pendingWrite, PendingWriteObserver observe, HookWaitCounter hookWaits) : pendingWrite(pendingWrite), observe(observe), hookWaits(hookWaits) { +ShaderMemory::ShaderMemory(std::span regions, PendingWriteQuery pendingWrite, PendingWriteObserver observe, HookWaitCounter hookWaits, PendingWriteQuery pendingSpan) : pendingWrite(pendingWrite), observe(observe), hookWaits(hookWaits), pendingSpan(pendingSpan != nullptr ? pendingSpan : pendingWrite) { const ShaderRecompiler::RequestMemoryView validated(regions); - for (const auto& region : regions) { - initial.emplace(region.guestAddress, region.bytes); + initial.reserve(regions.size()); + for (const auto& region : regions) initial.emplace_back(region.guestAddress, region.bytes); + std::sort(initial.begin(), initial.end(), [](const auto& left, const auto& right) { return left.first < right.first; }); +} + +ShaderMemory::~ShaderMemory() { + auto& pool = pagePool(); + for (const auto& [base, page] : pages) { + if (pool.size() < PagePoolLimit) pool.emplace_back(page); + else delete page; } } +void ShaderMemory::WordMask::SetRun(std::size_t first, std::size_t count) { + while (count != 0) { + const auto shift = first % 64u; + const auto take = std::min(count, 64u - shift); + bits[first / 64u] |= (take == 64u ? ~std::uint64_t{0} : (std::uint64_t{1} << take) - 1u) << shift; + first += take; + count -= take; + } +} + +bool ShaderMemory::WordMask::None() const { + return std::all_of(bits.begin(), bits.end(), [](std::uint64_t word) { return word == 0u; }); +} + +template +void ShaderMemory::WordMask::ForEachRun(TRun&& run) const { + constexpr std::size_t words = PageWords / 64u; + std::size_t index = 0; + while (index < PageWords) { + auto word = index / 64u; + auto set = bits[word] & (~std::uint64_t{0} << (index % 64u)); + while (set == 0u) { + if (++word == words) return; + set = bits[word]; + } + const auto first = word * 64u + static_cast(std::countr_zero(set)); + auto clear = ~bits[word] & (~std::uint64_t{0} << (first % 64u)); + while (clear == 0u && ++word < words) clear = ~bits[word]; + const auto end = word == words ? PageWords : word * 64u + static_cast(std::countr_zero(clear)); + run(first, end); + index = end; + } +} + +std::size_t ShaderMemory::pageAtOrAfter(std::uint64_t base) const { + return static_cast(std::partition_point(pages.begin(), pages.end(), [base](const std::pair& entry) { return entry.first < base; }) - pages.begin()); +} + +ShaderMemory::Page& ShaderMemory::addPage(std::size_t position, std::uint64_t base) { + auto& pool = pagePool(); + std::unique_ptr owned; + if (pool.empty()) { + owned = std::make_unique(); + } else { + owned = std::move(pool.back()); + pool.pop_back(); + } + owned->valid.Reset(); + owned->read.Reset(); + owned->recent.Reset(); + owned->wordwise = false; + owned->lazy = false; + pages.insert(pages.begin() + static_cast(position), {base, owned.get()}); + lastBase = base; + lastPage = owned.release(); + return *lastPage; +} + ShaderMemory::Page& ShaderMemory::page(std::uint64_t base) { - const auto found = pages.find(base); - if (found != pages.end()) return found->second; - auto& page = pages[base]; - ++CaptureTotals().pages; + if (base == lastBase) return *lastPage; + const auto position = pageAtOrAfter(base); + if (position != pages.size() && pages[position].first == base) { + lastBase = base; + lastPage = pages[position].second; + return *lastPage; + } + auto& page = addPage(position, base); + if (CaptureProfiled()) ++CaptureTotals().pages; // A page is either mapped whole or read word by word where the guest mapped less than a page, // or where recorded GPU work writes into it (a whole-page read would wait for that work). if (GuestMemory::Accessible(reinterpret_cast(base), PageBytes)) { @@ -108,9 +228,10 @@ ShaderMemory::Page& ShaderMemory::page(std::uint64_t base) { return page; } const auto started = CaptureProfiled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - GuestMemory::Read(base, std::as_writable_bytes(std::span(page.words)), sizeof(std::uint32_t)); + GuestMemory::GuardRead(base, PageBytes); + GuestMemory::FlushGpuWrites(base, PageBytes); if (CaptureProfiled()) CaptureTotals().pageReadNanoseconds += NanosecondsSince(started); - page.valid.set(); + page.lazy = true; } return page; } @@ -120,9 +241,9 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val if (address % sizeof(*value) != 0 || address > std::numeric_limits::max() - sizeof(*value)) { throw std::runtime_error("AGC driver: invalid shader memory read address"); } - ++CaptureTotals().reads; - if (!self.initial.empty()) { - const auto next = self.initial.upper_bound(address); + if (CaptureProfiled()) ++CaptureTotals().reads; + if (!self.initial.empty() && address + sizeof(*value) > self.initial.begin()->first && address < InitialEnd(self.initial)) { + const auto next = std::upper_bound(self.initial.begin(), self.initial.end(), address, [](std::uint64_t value, const auto& entry) { return value < entry.first; }); if (next != self.initial.begin()) { const auto previous = std::prev(next); const auto offset = address - previous->first; @@ -134,9 +255,15 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val } if (next != self.initial.end() && next->first - address < sizeof(*value)) throw std::runtime_error("AGC driver: shader memory read overlaps a snapshot boundary"); } + if (!self.supplied.empty()) self.spillTouching(address, address + sizeof(*value)); auto& page = self.page(address & ~static_cast(PageBytes - 1)); const auto index = static_cast((address % PageBytes) / sizeof(*value)); - if (!page.valid.test(index)) { + if (!page.valid.Test(index) && page.lazy) { + const auto first = index / BlockWords * BlockWords; + 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)) { std::uint32_t word = 0; auto policy = PendingWrite::Sync; if (page.wordwise) { @@ -163,6 +290,7 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val word = waited; } } else if (policy == PendingWrite::Raw || policy == PendingWrite::VerifyRaw) { + GuestMemory::GuardRead(address, sizeof(word)); std::memcpy(&word, reinterpret_cast(address), sizeof(word)); ++CaptureTotals().wordsRaw; if (policy == PendingWrite::VerifyRaw) { @@ -193,36 +321,237 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val } if (CaptureProfiled()) CaptureTotals().wordReadNanoseconds += NanosecondsSince(fetchStarted); page.words[index] = word; - page.valid.set(index); + page.valid.Set(index); } - page.read.set(index); - page.recent.set(index); + page.read.Set(index); + page.recent.Set(index); *value = page.words[index]; return true; } +bool ShaderMemory::overlapsInitial(std::uint64_t begin, std::uint64_t end) const { + if (initial.empty()) return false; + const auto after = std::lower_bound(initial.begin(), initial.end(), begin, [](const auto& entry, std::uint64_t value) { return entry.first < value; }); + if (after != initial.end() && after->first < end) return true; + return after != initial.begin() && std::prev(after)->first + std::prev(after)->second.size() > begin; +} + +bool ShaderMemory::readWords(void* context, std::uint64_t address, std::uint32_t* values, std::uint64_t count) { + auto& self = *static_cast(context); + if (!self.supplied.empty() && count != 0) self.spillTouching(address, count > (std::numeric_limits::max() - address) / sizeof(std::uint32_t) ? std::numeric_limits::max() : address + count * sizeof(std::uint32_t)); + while (count != 0) { + const auto base = address & ~static_cast(PageBytes - 1); + const auto index = static_cast((address % PageBytes) / sizeof(std::uint32_t)); + const auto words = static_cast(std::min(count, PageWords - index)); + const auto bytes = static_cast(words) * sizeof(std::uint32_t); + Page* page = nullptr; + if (address % sizeof(std::uint32_t) == 0 && address <= std::numeric_limits::max() - bytes && !self.overlapsInitial(address, address + bytes)) page = &self.page(base); + if (page != nullptr && page->lazy) { + const auto end = (index + words + BlockWords - 1u) / BlockWords * BlockWords; + auto first = index / BlockWords * BlockWords; + while (first < end) { + if (page->valid.TestBlock(first)) { + first += BlockWords; + continue; + } + auto last = first + BlockWords; + while (last < end && !page->valid.TestBlock(last)) last += BlockWords; + std::memcpy(&page->words[first], reinterpret_cast(base + first * sizeof(std::uint32_t)), (last - first) * sizeof(std::uint32_t)); + page->valid.SetRun(first, last - first); + first = last; + } + } + if (page != nullptr && (page->lazy || page->valid.All())) { + std::memcpy(values, page->words.data() + index, bytes); + page->read.SetRun(index, words); + page->recent.SetRun(index, words); + CaptureTotals().reads += words; + } else { + for (std::size_t word = 0; word < words; ++word) { + if (!read(context, address + word * sizeof(std::uint32_t), values + word)) return false; + } + } + address += bytes; + values += words; + count -= words; + } + return true; +} + bool ShaderMemory::accessible(void* context, std::uint64_t address, std::uint64_t bytes) { auto& self = *static_cast(context); if (bytes == 0 || address > std::numeric_limits::max() - bytes) return false; const auto end = address + bytes; while (address < end) { - const auto next = self.initial.upper_bound(address); - if (next != self.initial.begin()) { - const auto previous = std::prev(next); - if (address - previous->first < previous->second.size()) { - address = previous->first + previous->second.size(); - continue; + if (!self.initial.empty() && address < InitialEnd(self.initial)) { + const auto next = std::upper_bound(self.initial.begin(), self.initial.end(), address, [](std::uint64_t value, const auto& entry) { return value < entry.first; }); + if (next != self.initial.begin()) { + const auto previous = std::prev(next); + if (address - previous->first < previous->second.size()) { + address = previous->first + previous->second.size(); + continue; + } } } const auto base = address & ~static_cast(PageBytes - 1); const auto pageEnd = std::min(base + PageBytes, end); const auto& page = self.page(base); - if (!page.wordwise && !page.valid.all() && !GuestMemory::Accessible(reinterpret_cast(address), static_cast(pageEnd - address))) return false; + if (!page.wordwise && !page.lazy && !page.valid.All() && !GuestMemory::Accessible(reinterpret_cast(address), static_cast(pageEnd - address))) return false; address = pageEnd; } return true; } +bool ShaderMemory::pending(void* context, std::uint64_t address, std::uint64_t bytes) { + const auto& self = *static_cast(context); + if (self.pendingWrite == nullptr || bytes == 0) return false; + const auto policy = self.pendingWrite(address, static_cast(bytes), {}); + return policy == PendingWrite::Sync || policy == PendingWrite::VerifyRaw || policy == PendingWrite::VerifyKnownValue; +} + +ShaderRecompiler::SrtWriteStamp ShaderMemory::stampWrites(void* context, std::uint64_t address, std::uint64_t bytes) { + const auto& self = *static_cast(context); + if (bytes == 0 || address > std::numeric_limits::max() - bytes || self.overlapsInitial(address, address + bytes)) return {}; + const auto fetched = self.pageAtOrAfter(address & ~static_cast(PageBytes - 1)); + if (fetched != self.pages.size() && self.pages[fetched].first < address + bytes) return {}; + GuestMemory::FlushGpuWrites(address, static_cast(bytes)); + const auto layout = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + const auto generation = GuestMemory::CollectWrites(address, static_cast(bytes)); + if (generation == 0) return {}; + if (self.pendingSpan != nullptr && self.pendingSpan(address, static_cast(bytes), {}) != PendingWrite::None) return {}; + return {generation, layout}; +} + +bool ShaderMemory::Vouched(PendingWriteQuery pending, std::uint64_t address, std::uint64_t bytes, const ShaderRecompiler::SrtWriteStamp& stamp) { + CaptureTotals().vouchCalls.fetch_add(1, std::memory_order_relaxed); + if (stamp.generation == 0 || bytes == 0 || address > std::numeric_limits::max() - bytes || stamp.layout != GuestAllocations::GuestAllocationsGeneration_nid_postfix()) return false; + if (pending != nullptr && pending(address, static_cast(bytes), {}) != PendingWrite::None) return false; + GuestMemory::FlushGpuWrites(address, static_cast(bytes)); + if (GuestMemory::CollectWrites(address, static_cast(bytes)) == 0) return false; + return GuestMemory::UnchangedSince(address, static_cast(bytes), stamp.generation); +} + +bool ShaderMemory::unchangedSince(void* context, std::uint64_t address, std::uint64_t bytes, const ShaderRecompiler::SrtWriteStamp& stamp) { + return Vouched(static_cast(context)->pendingSpan, address, bytes, stamp); +} + +bool ShaderMemory::place(std::uint64_t address, const std::uint32_t* values, std::uint64_t count, bool overwrite, bool recent) { + while (count != 0) { + const auto base = address & ~static_cast(PageBytes - 1); + const auto index = static_cast((address % PageBytes) / sizeof(std::uint32_t)); + const auto words = static_cast(std::min(count, PageWords - index)); + const auto position = pageAtOrAfter(base); + Page* found = position != pages.size() && pages[position].first == base ? pages[position].second : nullptr; + if (found == nullptr) { + if (!overwrite && !GuestMemory::Accessible(reinterpret_cast(base), PageBytes)) return false; + found = &addPage(position, base); + } + auto& page = *found; + if (page.wordwise && !overwrite) return false; + const auto head = index / BlockWords * BlockWords; + const auto tail = (index + words + BlockWords - 1) / BlockWords * BlockWords; + const bool headOpen = page.lazy && !page.valid.Test(head); + const bool tailOpen = page.lazy && !page.valid.Test(tail - 1); + if (overwrite || page.valid.None()) { + std::memcpy(page.words.data() + index, values, words * sizeof(std::uint32_t)); + } else { + for (std::size_t word = 0; word < words; ++word) { + if (!page.valid.Test(index + word)) page.words[index + word] = values[word]; + } + } + if (headOpen && head < index) std::memcpy(&page.words[head], reinterpret_cast(base + head * sizeof(std::uint32_t)), (index - head) * sizeof(std::uint32_t)); + if (tailOpen && index + words < tail) std::memcpy(&page.words[index + words], reinterpret_cast(base + (index + words) * sizeof(std::uint32_t)), (tail - index - words) * sizeof(std::uint32_t)); + page.valid.SetRun(page.lazy ? head : index, page.lazy ? tail - head : words); + page.read.SetRun(index, words); + if (recent) page.recent.SetRun(index, words); + address += static_cast(words) * sizeof(std::uint32_t); + values += words; + count -= words; + } + return true; +} + +void ShaderMemory::spill(std::size_t index) { + auto entry = std::move(supplied[index]); + supplied.erase(supplied.begin() + static_cast(index)); + std::size_t at = 0; + for (const auto& [address, count] : entry.words->runs) { + static_cast(place(address, entry.words->words.data() + at, count, true, entry.recent)); + at += count; + } + CaptureTotals().suppliedSpilled += entry.words->words.size(); + spilled.push_back(std::move(entry.words)); +} + +void ShaderMemory::spillTouching(std::uint64_t begin, std::uint64_t end) { + for (std::size_t index = supplied.size(); index-- > 0;) { + const auto& entry = supplied[index]; + if (entry.begin < end && begin < entry.end && RunsTouch(entry.words->runs, begin, end)) spill(index); + } +} + +bool ShaderMemory::supplyWords(void* context, const std::shared_ptr& words) { + auto& self = *static_cast(context); + if (words == nullptr || words->runs.empty()) return false; + std::uint64_t total = 0; + std::uint64_t end = 0; + if (words->ordered && words->low == words->runs.front().first && words->low < words->high) { + total = words->words.size(); + end = words->high; + } else { + for (const auto& [address, count] : words->runs) { + const auto bytes = static_cast(count) * sizeof(std::uint32_t); + if (count == 0 || address % sizeof(std::uint32_t) != 0 || address < end || address > std::numeric_limits::max() - bytes) return false; + end = address + bytes; + total += count; + } + } + const auto begin = words->runs.front().first; + if (total != words->words.size() || self.overlapsInitial(begin, end)) return false; + auto& totals = CaptureTotals(); + for (auto& entry : self.supplied) { + if (entry.words != words) continue; + entry.recent = true; + totals.suppliedShared += total; + return true; + } + const auto fetched = self.pageAtOrAfter(begin & ~static_cast(PageBytes - 1)); + const bool paged = fetched != self.pages.size() && self.pages[fetched].first < end; + const bool overlapping = std::any_of(self.supplied.begin(), self.supplied.end(), [&](const Supplied& entry) { return entry.begin < end && begin < entry.end && RunsOverlap(entry.words->runs, words->runs); }); + if (paged || overlapping) { + for (std::size_t index = self.supplied.size(); index-- > 0;) { + const auto& entry = self.supplied[index]; + if (entry.begin < end && begin < entry.end && RunsOverlap(entry.words->runs, words->runs)) self.spill(index); + } + std::size_t at = 0; + for (const auto& [address, count] : words->runs) { + if (!self.place(address, words->words.data() + at, count, false, true)) return false; + at += count; + } + totals.suppliedCopied += total; + return true; + } + const auto spanBegin = begin & ~static_cast(PageBytes - 1); + const auto spanEnd = (end + PageBytes - 1) & ~static_cast(PageBytes - 1); + if (!GuestMemory::Accessible(reinterpret_cast(spanBegin), static_cast(spanEnd - spanBegin))) { + std::uint64_t first = std::numeric_limits::max(); + std::uint64_t past = 0; + for (const auto& [address, count] : words->runs) { + const auto from = address & ~static_cast(PageBytes - 1); + const auto to = (address + static_cast(count) * sizeof(std::uint32_t) + PageBytes - 1) & ~static_cast(PageBytes - 1); + if (from > past || first == std::numeric_limits::max()) { + if (first != std::numeric_limits::max() && !GuestMemory::Accessible(reinterpret_cast(first), static_cast(past - first))) return false; + first = from; + } + past = std::max(past, to); + } + if (!GuestMemory::Accessible(reinterpret_cast(first), static_cast(past - first))) return false; + } + self.supplied.push_back({words, begin, end, true}); + totals.suppliedShared += total; + return true; +} + ShaderMemory::KnownValueCounts ShaderMemory::KnownValues() { auto& totals = CaptureTotals(); return {totals.wordsKnown.load(std::memory_order_relaxed), totals.wordsKnownVerified.load(std::memory_order_relaxed), totals.wordsKnownMismatches.load(std::memory_order_relaxed)}; @@ -233,6 +562,24 @@ void ShaderMemory::CountHandleMemo(bool hit) { (hit ? CaptureTotals().handleHits : CaptureTotals().handleMisses).fetch_add(1, std::memory_order_relaxed); } +ShaderMemory::VouchCounts ShaderMemory::Vouches() { + auto& totals = CaptureTotals(); + return {totals.vouchCalls.load(std::memory_order_relaxed), totals.vouchedBlocks.load(std::memory_order_relaxed), totals.refusedBlocks.load(std::memory_order_relaxed), totals.vouchedWords.load(std::memory_order_relaxed)}; +} + +ShaderMemory::SupplyCounts ShaderMemory::Supplies() { + auto& totals = CaptureTotals(); + return {totals.suppliedShared.load(std::memory_order_relaxed), totals.suppliedCopied.load(std::memory_order_relaxed), totals.suppliedSpilled.load(std::memory_order_relaxed)}; +} + +std::vector> ShaderMemory::SuppliedBlocks() const { + std::vector> blocks; + blocks.reserve(supplied.size() + spilled.size()); + for (const auto& entry : supplied) blocks.push_back(entry.words); + blocks.insert(blocks.end(), spilled.begin(), spilled.end()); + return blocks; +} + std::shared_ptr ShaderMemory::Capture(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle) { return capture(request, handle, nullptr); } @@ -256,6 +603,11 @@ std::shared_ptr ShaderMemory::capture(c runtime.readMemory = &read; runtime.readSpecializationMemory = &read; runtime.accessible = &accessible; + runtime.pendingWrite = &pending; + runtime.readWords = &readWords; + runtime.stampWrites = &stampWrites; + runtime.unchangedSince = &unchangedSince; + runtime.supplyWords = &supplyWords; auto capture = invocation != nullptr ? invocation->Capture(runtime) : handle != nullptr ? ShaderRecompiler::CaptureResources(request, runtime, *handle) : ShaderRecompiler::CaptureResources(request, runtime); if (profile) { totals.captureNanoseconds += NanosecondsSince(started); @@ -263,7 +615,11 @@ std::shared_ptr ShaderMemory::capture(c totals.hookWaitNanoseconds += static_cast((WaitedMs() - waitedBefore) * 1e6); std::uint64_t initialBytes = 0; for (const auto& [address, bytes] : initial) initialBytes += bytes.size(); - if (++totals.captures % 500 == 0) { + ++totals.captures; + static std::atomic nextReport{0}; + const auto now = std::chrono::steady_clock::now().time_since_epoch().count(); + auto due = nextReport.load(std::memory_order_relaxed); + if (now >= due && nextReport.compare_exchange_strong(due, now + std::chrono::duration_cast(std::chrono::seconds(10)).count(), std::memory_order_relaxed)) { const auto captures = static_cast(totals.captures.load()); const auto captureNs = totals.captureNanoseconds.load(); const auto resolveNs = totals.resolveNanoseconds.load(); @@ -271,55 +627,168 @@ std::shared_ptr ShaderMemory::capture(c const auto wordNs = totals.wordReadNanoseconds.load(); const auto specializationNs = ShaderRecompiler::ResourceMaterializer::SpecializationNanoseconds(); const auto hookNs = totals.hookWaitNanoseconds.load(); + const auto materializeNs = ShaderRecompiler::ResourceMaterializer::MaterializeNanoseconds(); + const auto evaluateNs = ShaderRecompiler::ResourceMaterializer::EvaluateNanoseconds(); const auto walkNs = captureNs > resolveNs + pageNs + wordNs + specializationNs ? captureNs - resolveNs - pageNs - wordNs - specializationNs : 0; - AgcDriver::ProfilePrint_nid_no_patch( "[capture] %llu captures: %llu word reads, %llu pages fetched (%llu read word by word over pending GPU writes: %llu words waited %.2f s, %llu read raw on evidence, %llu verified with %llu mismatches, %llu served from known values, %llu hook reads not observed: no GPU wait), capture (plan lookup + materialize) %.1f s, %llu KiB registered code this capture; sub-phases (s, us per capture): source resolve %.2f/%.1f, walk %.2f/%.1f, specialization (every materialize) %.2f/%.1f, page reads %.2f/%.1f, word reads %.2f/%.1f, hook GPU waits inside %.2f/%.1f; source handle memo %llu hits, %llu resolves\n", static_cast(totals.captures.load()), static_cast(totals.reads.load()), static_cast(totals.pages.load()), static_cast(totals.pagesWordwise.load()), static_cast(totals.wordWaits.load()), totals.wordWaitNanoseconds.load() / 1e9, static_cast(totals.wordsRaw.load()), static_cast(totals.wordsVerified.load()), static_cast(totals.wordMismatches.load()), static_cast(totals.wordsKnown.load()), static_cast(totals.observationsSkipped.load()), captureNs / 1e9, static_cast(initialBytes / 1024), resolveNs / 1e9, resolveNs / captures / 1e3, walkNs / 1e9, walkNs / captures / 1e3, specializationNs / 1e9, specializationNs / captures / 1e3, pageNs / 1e9, pageNs / captures / 1e3, wordNs / 1e9, wordNs / captures / 1e3, hookNs / 1e9, hookNs / captures / 1e3, static_cast(totals.handleHits.load()), static_cast(totals.handleMisses.load())); + AgcDriver::ProfilePrint_nid_no_patch( "[capture] %llu captures: %llu word reads, %llu pages fetched (%llu read word by word over pending GPU writes: %llu words waited %.2f s, %llu read raw on evidence, %llu verified with %llu mismatches, %llu served from known values, %llu hook reads not observed: no GPU wait), capture (plan lookup + materialize) %.1f s, %llu KiB registered code this capture; sub-phases (s, us per capture): source resolve %.2f/%.1f, walk %.2f/%.1f, specialization (every materialize) %.2f/%.1f, page reads %.2f/%.1f, word reads %.2f/%.1f, hook GPU waits inside %.2f/%.1f, materialize %.2f/%.1f (SRT evaluate %.2f/%.1f); source handle memo %llu hits, %llu resolves\n", static_cast(totals.captures.load()), static_cast(totals.reads.load()), static_cast(totals.pages.load()), static_cast(totals.pagesWordwise.load()), static_cast(totals.wordWaits.load()), totals.wordWaitNanoseconds.load() / 1e9, static_cast(totals.wordsRaw.load()), static_cast(totals.wordsVerified.load()), static_cast(totals.wordMismatches.load()), static_cast(totals.wordsKnown.load()), static_cast(totals.observationsSkipped.load()), captureNs / 1e9, static_cast(initialBytes / 1024), resolveNs / 1e9, resolveNs / captures / 1e3, walkNs / 1e9, walkNs / captures / 1e3, specializationNs / 1e9, specializationNs / captures / 1e3, pageNs / 1e9, pageNs / captures / 1e3, wordNs / 1e9, wordNs / captures / 1e3, hookNs / 1e9, hookNs / captures / 1e3, materializeNs / 1e9, materializeNs / captures / 1e3, evaluateNs / 1e9, evaluateNs / captures / 1e3, static_cast(totals.handleHits.load()), static_cast(totals.handleMisses.load())); } } return capture; } -std::vector ShaderMemory::Regions() const { - std::vector result; - result.reserve(initial.size() + pages.size()); - auto next = initial.begin(); - // Both maps are ordered by address and never overlap, so a merge keeps the result sorted. - for (const auto& [base, page] : pages) { - while (next != initial.end() && next->first < base) { - result.push_back({next->first, next->second}); - ++next; - } - for (std::size_t index = 0; index < PageWords;) { - if (!page.read.test(index)) { - ++index; - continue; - } - const auto first = index; - while (index < PageWords && page.read.test(index)) ++index; - result.push_back({base + first * sizeof(std::uint32_t), std::as_bytes(std::span(page.words).subspan(first, index - first))}); - } +std::vector ShaderMemory::collect(bool recentOnly) const { + struct Cursor { + const ShaderRecompiler::SrtGuestWords* words; + std::size_t run; + std::size_t at; + }; + std::vector cursors; + std::size_t runs = 0; + for (const auto& entry : supplied) { + if (recentOnly && !entry.recent) continue; + cursors.push_back({entry.words.get(), 0u, 0u}); + runs += entry.words->runs.size() + entry.words->words.size() / PageWords; } - for (; next != initial.end(); ++next) result.push_back({next->first, next->second}); + std::vector result; + result.reserve((recentOnly ? 0u : initial.size()) + pages.size() + runs); + auto next = recentOnly ? initial.end() : initial.begin(); + const auto emitBefore = [&](std::uint64_t address) { + while (true) { + Cursor* lowest = nullptr; + for (auto& cursor : cursors) { + if (cursor.run < cursor.words->runs.size() && (lowest == nullptr || cursor.words->runs[cursor.run].first < lowest->words->runs[lowest->run].first)) lowest = &cursor; + } + const auto run = lowest != nullptr ? lowest->words->runs[lowest->run].first : std::numeric_limits::max(); + if (next != initial.end() && next->first < address && next->first < run) { + result.push_back({next->first, next->second}); + ++next; + } else if (lowest != nullptr && run < address) { + const auto count = lowest->words->runs[lowest->run].second; + const auto words = std::span(lowest->words->words).subspan(lowest->at, count); + for (std::size_t done = 0; done < count;) { + const auto at = run + done * sizeof(std::uint32_t); + const auto piece = std::min(count - done, (PageBytes - static_cast(at % PageBytes)) / sizeof(std::uint32_t)); + result.push_back({at, std::as_bytes(words.subspan(done, piece))}); + done += piece; + } + lowest->at += count; + ++lowest->run; + } else { + return; + } + } + }; + for (const auto& [base, pagePointer] : pages) { + const auto& page = *pagePointer; + const auto& marks = recentOnly ? page.recent : page.read; + if (marks.None()) continue; + marks.ForEachRun([&](std::size_t first, std::size_t end) { + const auto address = base + first * sizeof(std::uint32_t); + emitBefore(address); + result.push_back({address, std::as_bytes(std::span(page.words).subspan(first, end - first))}); + }); + } + emitBefore(std::numeric_limits::max()); return result; } +std::vector ShaderMemory::Regions() const { + return collect(false); +} + std::vector ShaderMemory::TakeRecentRegions() { - std::vector result; - for (auto& [base, page] : pages) { - if (page.recent.none()) continue; - for (std::size_t index = 0; index < PageWords;) { - if (!page.recent.test(index)) { - ++index; - continue; - } - const auto first = index; - while (index < PageWords && page.recent.test(index)) ++index; - result.push_back({base + first * sizeof(std::uint32_t), std::as_bytes(std::span(page.words).subspan(first, index - first))}); - } - page.recent.reset(); - } + auto result = collect(true); + for (auto& [base, page] : pages) page->recent.Reset(); + for (auto& entry : supplied) entry.recent = false; return result; } +void AppendSharedWords(std::vector& shared, std::size_t region, std::size_t first, std::span bytes, std::span> blocks) { + const auto address = reinterpret_cast(bytes.data()); + const auto count = bytes.size() / sizeof(std::uint32_t); + if (count == 0) return; + for (const auto& block : blocks) { + if (block == nullptr || block->words.empty()) continue; + const auto begin = reinterpret_cast(block->words.data()); + const auto end = begin + block->words.size() * sizeof(std::uint32_t); + if (address < begin || address >= end || end - address < count * sizeof(std::uint32_t) || (address - begin) % sizeof(std::uint32_t) != 0) continue; + const auto offset = static_cast((address - begin) / sizeof(std::uint32_t)); + if (!shared.empty()) { + auto& last = shared.back(); + if (last.first + last.count == first && last.offset + last.count == offset && last.region + last.regions == region && !last.block.owner_before(block) && !block.owner_before(last.block)) { + last.count += count; + ++last.regions; + return; + } + } + shared.push_back({first, count, offset, block, region, 1u}); + return; + } +} + +bool SameCapturedWords(std::span left, std::span leftShared, std::span right, std::span rightShared, std::size_t* compared) { + if (compared != nullptr) *compared = 0; + if (left.size() != right.size()) return false; + std::size_t checked = 0; + const auto equal = [&](std::size_t from, std::size_t to) { + checked += to - from; + return std::equal(left.begin() + static_cast(from), left.begin() + static_cast(to), right.begin() + static_cast(from)); + }; + std::size_t at = 0; + std::size_t next = 0; + bool same = true; + for (const auto& run : leftShared) { + while (next < rightShared.size() && rightShared[next].first < run.first) ++next; + if (next == rightShared.size()) break; + const auto& other = rightShared[next]; + const bool placed = other.first == run.first && other.count == run.count && other.offset == run.offset && run.first >= at && run.first <= left.size() && run.count <= left.size() - run.first; + if (!placed || run.block.expired() || run.block.owner_before(other.block) || other.block.owner_before(run.block)) continue; + if (!equal(at, run.first)) { + same = false; + break; + } + at = run.first + run.count; + } + same = same && equal(at, left.size()); + if (compared != nullptr) *compared = checked; + return same; +} + +std::size_t VouchSharedRegions(std::span shared, std::size_t regions, ShaderMemory::PendingWriteQuery pending, std::vector& vouched) { + vouched.clear(); + if (shared.empty() || regions == 0) return 0; + thread_local std::vector>* blocksSlot = nullptr; + auto& blocks = ShaderRecompiler::ThreadOwned(blocksSlot); + blocks.clear(); + auto& totals = CaptureTotals(); + std::size_t words = 0; + for (const auto& entry : shared) { + if (entry.regions == 0 || entry.region > regions || entry.regions > regions - entry.region || (!vouched.empty() && entry.region < vouched.back().end)) continue; + const auto block = entry.block.lock(); + if (block == nullptr || block->low >= block->high) continue; + auto known = std::find_if(blocks.begin(), blocks.end(), [&](const auto& seen) { return seen.first == block.get(); }); + if (known == blocks.end()) { + const bool good = ShaderMemory::Vouched(pending, block->low, block->high - block->low, ShaderRecompiler::LoadStamp(*block)); + (good ? totals.vouchedBlocks : totals.refusedBlocks).fetch_add(1, std::memory_order_relaxed); + known = blocks.insert(blocks.end(), {block.get(), good}); + } + if (!known->second) continue; + if (!vouched.empty() && vouched.back().end == entry.region) { + auto& last = vouched.back(); + last.end += entry.regions; + last.words += entry.count; + last.low = std::min(last.low, block->low); + last.high = std::max(last.high, block->high); + } else { + vouched.push_back({entry.region, entry.region + entry.regions, entry.count, block->low, block->high}); + } + words += entry.count; + } + blocks.clear(); + totals.vouchedWords.fetch_add(words, std::memory_order_relaxed); + return words; +} + DataWordPositionCounts DataWordPositions(std::span> runs, std::span> leaves, std::span otherReads, std::span words, std::span flattenedSrt, std::vector& positions, std::vector& slots) { DataWordPositionCounts counts; positions.clear(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp new file mode 100644 index 0000000000..217e5b0c81 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp @@ -0,0 +1,237 @@ +#include "prx/libSceAgcDriver/Execution/include/SubmissionCapture.hpp" +#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { +namespace { + +static_assert(std::endian::native == std::endian::little, "captured dwords are stored in the guest's little-endian order"); + +void requireCapture(bool condition, const std::string& reason) { + if (!condition) throw std::runtime_error("submission capture: " + reason); +} + +std::vector split(std::string_view text, char separator) { + std::vector result; + while (true) { + const auto end = text.find(separator); + result.push_back(text.substr(0, end)); + if (end == std::string_view::npos) break; + text.remove_prefix(end + 1); + } + return result; +} + +template +bool parse(std::string_view text, TValue& value, int base = 10) { + if (text.empty()) return false; + const auto [last, error] = std::from_chars(text.data(), text.data() + text.size(), value, base); + return error == std::errc{} && last == text.data() + text.size(); +} + +SubmissionKind kindFromName(std::string_view name, const std::string& where) { + for (const auto kind : {SubmissionKind::Submit, SubmissionKind::Rewind, SubmissionKind::Suspend}) { + if (SubmissionCapture::KindName(kind) == name) return kind; + } + throw std::runtime_error("submission capture: " + where + ": unknown kind '" + std::string(name) + "'"); +} + +bool waitPacket(std::uint32_t header) { + const auto opcode = (header >> 8u) & 0xffu; + return (header >> 30u) == 3u && (opcode == 0x3c || opcode == 0x93); +} + +std::string heldField(const CapturedSubmission& submission) { + if (submission.heldAtSubmit.empty()) return "-"; + std::string text; + for (const auto offset : submission.heldAtSubmit) text += (text.empty() ? "" : ",") + std::to_string(offset); + return text; +} + +std::string conditionalField(const CapturedSubmission& submission) { + if (submission.conditionalRanges.empty()) return "-"; + std::string text; + for (const auto& [start, end] : submission.conditionalRanges) text += (text.empty() ? "" : ",") + std::to_string(start) + ":" + std::to_string(end); + return text; +} + +std::vector readHeld(std::string_view field, std::size_t dwords, const std::string& where) { + std::vector result; + if (field == "-") return result; + for (const auto item : split(field, ',')) { + std::size_t offset = 0; + requireCapture(parse(item, offset) && offset < dwords && (result.empty() || offset > result.back()), where + ": malformed held offsets '" + std::string(field) + "'"); + result.push_back(offset); + } + return result; +} + +std::vector> readConditional(std::string_view field, std::size_t dwords, const std::string& where) { + std::vector> result; + if (field == "-") return result; + for (const auto item : split(field, ',')) { + const auto bounds = split(item, ':'); + std::size_t start = 0, end = 0; + requireCapture(bounds.size() == 2 && parse(bounds[0], start) && parse(bounds[1], end) && start < end && end <= dwords && (result.empty() || start > result.back().first), + where + ": malformed conditional ranges '" + std::string(field) + "'"); + result.emplace_back(start, end); + } + return result; +} + +} + +SubmissionCapture::SubmissionCapture(const std::filesystem::path& directory) : directory(directory) { + std::error_code error; + std::filesystem::create_directories(directory, error); + requireCapture(!error && std::filesystem::is_directory(directory), "cannot create the directory " + directory.string()); + index.open(directory / IndexName, std::ios::binary | std::ios::trunc); + requireCapture(index.is_open(), "cannot create " + (directory / IndexName).string()); + index << Magic << '\n' << Columns << '\n'; + index.flush(); + requireCapture(index.good(), "cannot write " + (directory / IndexName).string()); +} + +SubmissionCapture* SubmissionCapture::Active() { + static SubmissionCapture* const capture = [] { + const char* directory = std::getenv("APS5_DUMP_SUBMITS"); + return directory != nullptr && *directory != '\0' ? new SubmissionCapture(directory) : nullptr; + }(); + return capture; +} + +CapturedSubmission SubmissionCapture::Prepare(std::uint32_t queue, SubmissionKind kind, std::span commands, const std::map& conditionalEnds) { + QueueType(queue); + requireCapture(kind != SubmissionKind::Suspend || commands.empty(), "a suspend point has no commands"); + CapturedSubmission submission; + submission.queue = queue; + submission.kind = kind; + submission.dwords = commands.size(); + submission.commands.assign(commands.begin(), commands.end()); + for (std::size_t cursor = 0; cursor < commands.size(); cursor += Pm4::PacketWords(commands[cursor])) { + if (waitPacket(commands[cursor])) submission.heldAtSubmit.push_back(cursor); + } + for (const auto& [start, end] : conditionalEnds) { + requireCapture(start < end && end <= commands.size(), "a conditional range lies outside the commands"); + submission.conditionalRanges.emplace_back(start, end); + } + return submission; +} + +void SubmissionCapture::Take(CapturedSubmission& submission, std::uint64_t serial, std::uint64_t timeUs, const std::set& heldAtSubmit) noexcept { + submission.ordinal = next.fetch_add(1, std::memory_order_relaxed); + submission.serial = serial; + submission.timeUs = timeUs; + std::erase_if(submission.heldAtSubmit, [&](std::size_t offset) { return !heldAtSubmit.contains(offset); }); +} + +void SubmissionCapture::Write(const CapturedSubmission& submission) { + requireCapture(submission.dwords == submission.commands.size(), "dword count does not match the commands"); + const auto name = FileName(submission); + if (!submission.commands.empty()) { + std::ofstream file(directory / name, std::ios::binary | std::ios::trunc); + file.write(reinterpret_cast(submission.commands.data()), static_cast(submission.commands.size() * sizeof(std::uint32_t))); + file.close(); + requireCapture(!file.fail(), "cannot write " + (directory / name).string()); + } + char head[160]; + const int length = std::snprintf(head, sizeof(head), "%llu 0x%x %s %llu %llu %zu %s ", static_cast(submission.ordinal), submission.queue, QueueType(submission.queue).data(), + static_cast(submission.serial), static_cast(submission.timeUs), submission.dwords, KindName(submission.kind).data()); + requireCapture(length > 0 && static_cast(length) < sizeof(head), "index line does not fit"); + const auto line = std::string(head, static_cast(length)) + name + " " + heldField(submission) + " " + conditionalField(submission) + "\n"; + std::lock_guard lock(mutex); + index.write(line.data(), static_cast(line.size())); + index.flush(); + requireCapture(index.good(), "cannot append to " + (directory / IndexName).string()); +} + +std::string SubmissionCapture::FileName(const CapturedSubmission& submission) { + if (submission.dwords == 0) return "-"; + char name[48]; + std::snprintf(name, sizeof(name), "submission_%06llu.bin", static_cast(submission.ordinal)); + return name; +} + +std::string_view SubmissionCapture::KindName(SubmissionKind kind) { + switch (kind) { + case SubmissionKind::Submit: return "submit"; + case SubmissionKind::Rewind: return "rewind"; + case SubmissionKind::Suspend: return "suspend"; + } + throw std::runtime_error("submission capture: unknown submission kind"); +} + +std::string_view SubmissionCapture::QueueType(std::uint32_t queue) { + if (queue == 0) return "graphics"; + if (queue >= 0x20 && queue < 0x58) return "compute"; + char what[64]; + std::snprintf(what, sizeof(what), "queue 0x%x is neither the graphics queue nor a compute queue", queue); + throw std::runtime_error(std::string("submission capture: ") + what); +} + +std::vector SubmissionCapture::ReadIndex(const std::filesystem::path& directory) { + const auto path = directory / IndexName; + std::ifstream input(path, std::ios::binary); + requireCapture(input.is_open(), "cannot open " + path.string()); + std::string line; + requireCapture(std::getline(input, line) && line == Magic, path.string() + " does not start with '" + std::string(Magic) + "'"); + requireCapture(std::getline(input, line) && (line == Columns || line == ColumnsWithoutHeld), path.string() + " has no column line '" + std::string(Columns) + "' or '" + std::string(ColumnsWithoutHeld) + "'"); + const bool complete = line == Columns; + const std::size_t columns = complete ? 10 : 8; + std::vector result; + for (std::size_t number = 3; std::getline(input, line); ++number) { + const auto where = path.string() + " line " + std::to_string(number); + requireCapture(!input.eof(), where + " is incomplete"); + const auto values = split(line, ' '); + requireCapture(values.size() == columns, where + " does not have " + std::to_string(columns) + " fields"); + CapturedSubmission submission; + requireCapture(parse(values[0], submission.ordinal) && values[1].starts_with("0x") && parse(values[1].substr(2), submission.queue, 16) && parse(values[3], submission.serial) + && parse(values[4], submission.timeUs) && parse(values[5], submission.dwords), where + " has a malformed number"); + requireCapture(QueueType(submission.queue) == values[2], where + ": queue type does not match the queue"); + submission.kind = kindFromName(values[6], where); + requireCapture(submission.kind != SubmissionKind::Suspend || submission.dwords == 0, where + ": a suspend point has commands"); + requireCapture(submission.dwords <= std::numeric_limits::max() / sizeof(std::uint32_t), where + ": dword count overflows"); + requireCapture(values[7] == FileName(submission), where + ": file name does not match the ordinal and size"); + submission.heldKnown = complete; + submission.conditionalKnown = complete; + if (complete) { + submission.heldAtSubmit = readHeld(values[8], submission.dwords, where); + submission.conditionalRanges = readConditional(values[9], submission.dwords, where); + } + result.push_back(std::move(submission)); + } + requireCapture(input.eof(), "cannot read " + path.string()); + std::sort(result.begin(), result.end(), [](const auto& left, const auto& right) { return left.ordinal < right.ordinal; }); + const auto repeated = std::adjacent_find(result.begin(), result.end(), [](const auto& left, const auto& right) { return left.ordinal == right.ordinal; }); + requireCapture(repeated == result.end(), path.string() + " lists ordinal " + (repeated == result.end() ? std::string() : std::to_string(repeated->ordinal)) + " twice"); + return result; +} + +std::vector SubmissionCapture::ReadCommands(const std::filesystem::path& directory, const CapturedSubmission& submission) { + if (submission.dwords == 0) return {}; + const auto path = directory / FileName(submission); + std::error_code error; + const auto size = std::filesystem::file_size(path, error); + requireCapture(!error, "cannot read the size of " + path.string()); + requireCapture(size == submission.dwords * sizeof(std::uint32_t), path.string() + " does not hold " + std::to_string(submission.dwords) + " dwords"); + std::vector commands(submission.dwords); + std::ifstream input(path, std::ios::binary); + input.read(reinterpret_cast(commands.data()), static_cast(size)); + requireCapture(input.good() && input.peek() == std::char_traits::eof(), "cannot read " + path.string()); + return commands; +} + +std::vector SubmissionCapture::Read(const std::filesystem::path& directory) { + auto submissions = ReadIndex(directory); + for (auto& submission : submissions) submission.commands = ReadCommands(directory, submission); + return submissions; +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp index 40d0a7e089..b8263c881e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp @@ -6,7 +6,11 @@ #include #include #include +#include #include +#ifdef _WIN32 +#include +#endif namespace AgcDriver { @@ -50,6 +54,22 @@ void TraceFile::WriteGzip(const std::filesystem::path& path, std::string_view te output.close(); } +void TraceFile::ReplaceGzip(const std::filesystem::path& path, std::string_view text) { + auto temporary = path; + temporary += ".tmp"; + WriteGzip(temporary, text); + std::error_code error; +#ifdef _WIN32 + if (::MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) == 0) error = std::error_code(static_cast(::GetLastError()), std::system_category()); +#else + std::filesystem::rename(temporary, path, error); +#endif + if (!error) return; + std::error_code removeError; + std::filesystem::remove(temporary, removeError); + throw std::system_error(error, "replacing " + path.string()); +} + void TraceFile::AppendLine(const std::filesystem::path& path, std::string_view line) { std::ofstream output; output.exceptions(std::ios::badbit | std::ios::failbit); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 6e644f6c84..9d93a5127f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1,27 +1,39 @@ #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/FloatFlushProbe.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/Float32FlushControl.hpp" #include "prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp" +#include "prx/libSceAgcDriver/Execution/include/DrawInputsInPlace.hpp" +#include "prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp" #include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" +#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" -#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" +#include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" @@ -42,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -53,6 +66,7 @@ #include #include #include +#include #include #include #include @@ -69,6 +83,7 @@ namespace AgcDriver { namespace { void check(VkResult result, const char* operation) { + if (result == VK_ERROR_DEVICE_LOST) Graphics::ReportDeviceLost(); if (result != VK_SUCCESS) { throw std::runtime_error(std::string(operation) + ": Vulkan result " + std::to_string(result)); } @@ -111,6 +126,7 @@ void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, info.layout = layout; auto cacheLock = synchronization != nullptr ? synchronization->LockForPipelineUse() : Graphics::PipelineCacheSynchronization::PipelineUseLock{}; check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines"); + PerformanceCounters::CountPipeline(); } bool CaptureComputePipeline(std::size_t words) { @@ -131,6 +147,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: @@ -139,19 +156,27 @@ public: std::lock_guard lock(mutex); stopping = true; } - changed.notify_one(); + changed.notify_all(); if (worker.joinable()) worker.join(); } - void Enqueue(int index, std::uint32_t width, std::uint32_t height, std::uint32_t scale, std::vector pixels) { - std::lock_guard lock(mutex); + void Enqueue(int index, std::uint32_t width, std::uint32_t height, std::uint32_t scale, bool screenshot, std::vector pixels) { + std::unique_lock lock(mutex); + require(!stopping, "frame dump writer has stopped"); + if (!screenshot && !frameDumpOutput.Enabled_nid_no_patch()) return; + require(pixels.size() <= maxBytes, "a frame dump exceeds the 64 MiB queue"); + changed.wait(lock, [&] { return stopping || pixels.size() <= maxBytes - queuedBytes; }); require(!stopping, "frame dump writer has stopped"); - require(pixels.size() <= maxBytes - queuedBytes, "frame dump queue exceeded 64 MiB"); if (!worker.joinable()) worker = std::thread([this] { run(); }); const auto bytes = pixels.size(); - pending.push_back({index, width, height, scale, std::move(pixels)}); + pending.push_back({index, width, height, scale, screenshot, std::move(pixels)}); queuedBytes += bytes; - changed.notify_one(); + changed.notify_all(); + } + + bool Idle() { + std::lock_guard lock(mutex); + return queuedBytes == 0; } private: @@ -160,6 +185,7 @@ private: std::uint32_t width; std::uint32_t height; std::uint32_t scale; + bool screenshot; std::vector pixels; }; @@ -174,9 +200,11 @@ private: batch.swap(pending); } for (const auto& frame : batch) { - WriteFrameBmp(frame.index, frame.width, frame.height, frame.pixels, frame.scale); + if (frame.screenshot) std::fprintf(stderr, "[gpu] screenshot saved: %s\n", Screenshot::Write(GetAppTitleId_nid_postfix().value, frame.width, frame.height, frame.pixels).string().c_str()); + else WriteFrameBmp(frame.index, frame.width, frame.height, frame.pixels, frame.scale); std::lock_guard lock(mutex); queuedBytes -= frame.pixels.size(); + changed.notify_all(); } } } catch (const std::exception& error) { @@ -209,13 +237,27 @@ struct ComputePipelineObjects { VkShaderModule module = VK_NULL_HANDLE; VkPipelineLayout layout = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE; + VkDescriptorSetLayout resourcesLayout = VK_NULL_HANDLE; + std::mutex splitMutex; + std::map, VkPipeline> splitPipelines; ~ComputePipelineObjects() { + for (const auto& [base, split] : splitPipelines) destroyPipeline(device, split, nullptr); if (pipeline != VK_NULL_HANDLE) destroyPipeline(device, pipeline, nullptr); if (layout != VK_NULL_HANDLE) destroyLayout(device, layout, nullptr); if (module != VK_NULL_HANDLE) destroyModule(device, module, nullptr); } }; +// 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 { @@ -260,7 +302,10 @@ 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; VkTimeDomainKHR hostTimeDomain = VK_TIME_DOMAIN_DEVICE_KHR; VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; std::vector capabilities{1}; @@ -269,18 +314,31 @@ struct VulkanDevice::State { bool tessellationShader = false; bool meshShader = false; bool fragmentShaderBarycentric = false; + bool shaderTerminateInvocation = false; bool geometryShader = false; + bool largePoints = false; bool sampleRateShading = false; + bool storageImageMultisample = false; + bool storageImageWriteWithoutFormat = false; bool shaderClipDistance = false; bool shaderCullDistance = false; bool shaderClock = false; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; bool float16DenormPreserve = false; + bool float32FlushToZeroCandidate = false; + bool float32FlushToZero = false; + bool float32DenormPreserve = false; + bool probingFloatControls = false; + bool demoteToHelperInvocation = false; + bool earlyFragmentSampleMaskTestBeforeSampleCounting = false; + bool robustBufferAccess2 = false; + bool subgroupControlBarriers = true; // VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in // graphics stages, and compute workgroups wider than a wave). bool descriptorIndexing = false; bool storageBufferUpdateAfterBind = false; + bool sampledImageUpdateAfterBind = false; bool descriptorBindingPartiallyBound = false; bool descriptorBindingVariableDescriptorCount = false; bool runtimeDescriptorArray = false; @@ -288,6 +346,7 @@ struct VulkanDevice::State { bool imageInt64Atomics = false; bool bufferInt64Atomics = false; bool viewportIndexLayer = false; + bool imageFormatList = false; bool primitiveListRestart = false; bool depthClipControl = false; bool graphicsPipelineLibrary = false; @@ -295,27 +354,41 @@ struct VulkanDevice::State { bool imageViewMinLod = false; bool pipelineExecutableInfo = false; bool maintenance8 = false; + bool maintenance5 = false; + bool fillModeNonSolid = false; std::uint32_t srgbDecodeFormats = 0; + std::uint32_t srgbCopyFormats = 0; bool storageImageWithoutFormat = false; VkSampleCountFlags sampleLocationSampleCounts = 0; + VkSampleCountFlags sampleLocationGridSampleCounts = 0; + std::array sampleLocationGridSizes{}; + bool shaderStencilExport = false; bool depthClamp = false; bool depthBounds = false; bool depthBiasClamp = false; bool dualSrcBlend = false; bool occlusionQueryPrecise = false; VkDeviceSize hostImportAlignment = 0; + bool drawInputsInPlace = true; bool dmaBufImport = false; bool sparseBinding = false; std::uint32_t queueFamily = 0; std::uint32_t sparseFamily = VK_QUEUE_FAMILY_IGNORED; VkQueue sparseQueue = VK_NULL_HANDLE; std::mutex sparseQueueMutex; + VkQueue settleQueue = VK_NULL_HANDLE; + std::uint32_t settleFamily = 0; + VkCommandPool settlePool = VK_NULL_HANDLE; + std::mutex settleQueueMutex; bool memoryBudget = false; bool depthRangeUnrestricted = false; bool samplerAnisotropy = false; bool textureCompressionBC = false; bool samplerFilterMinmax = false; bool nonSeamlessCubeMap = false; + VkSampleCountFlags mergedCoverageSampleCounts = 0; + VkSampleCountFlags truncatedCoverageSampleCounts = 0; + bool mixedAttachmentSamples = false; bool fragmentShaderPixelInterlock = false; bool conservativeRasterization = false; bool provokingVertexLast = false; @@ -325,6 +398,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; @@ -347,7 +422,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> 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; @@ -355,6 +435,7 @@ struct VulkanDevice::State { std::unique_ptr textureCache; std::unique_ptr pipelineCache; std::unique_ptr descriptorCache; + std::unique_ptr vertexInputs; std::unique_ptr samplerCache; Graphics::ResourceCache& resourceCache = Graphics::SharedResourceCache(); // Recorded dispatches that write a copied buffer (their results reach guest memory by a CPU @@ -366,7 +447,16 @@ struct VulkanDevice::State { VkPhysicalDeviceMeshShaderPropertiesEXT meshLimits{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_PROPERTIES_EXT}; std::unique_ptr scaler; std::unique_ptr presentation; + std::unique_ptr overlay; VkFormat surfaceFormat = VK_FORMAT_B8G8R8A8_UNORM; + VkColorSpaceKHR surfaceColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; + bool swapchainColorSpaces = false; + bool hdrMetadata = false; + std::vector surfaceFormats; + std::uint64_t surfaceDisplayGeneration = 0; + std::uint64_t metadataGeneration = 0; + std::unique_ptr hdrScaler; + VkFormat hdrScalerSource = VK_FORMAT_UNDEFINED; // The swapchain image AcquireImage took for the next present(), consumed by that present. bool imageAcquired = false; std::uint32_t acquiredIndex = 0; @@ -385,6 +475,7 @@ struct VulkanDevice::State { std::uint32_t dumpHeight = 0; std::uint32_t dumpScale = 1; bool dumpRecorded = false; + bool dumpScreenshot = false; int dumpIndex = 0; std::uint32_t imageIndex = 0; bool inFlight = false; @@ -416,6 +507,7 @@ struct VulkanDevice::State { bool queuePending = false; std::uint32_t queueIndex = 0; int nextDumpIndex = 0; + bool nextDumpScreenshot = false; FrameDumpWriter dumpWriter; template @@ -614,6 +706,11 @@ struct VulkanDevice::State { require(slot.dumpBuffer != nullptr, "frame dump was not recorded"); const auto full = slot.dumpBuffer->Bytes(); const auto index = slot.dumpIndex; + if (slot.dumpScreenshot) { + require(full.size() == static_cast(slot.dumpWidth) * slot.dumpHeight * 4, "invalid screenshot pixels"); + dumpWriter.Enqueue(index, slot.dumpWidth, slot.dumpHeight, 1, true, std::vector(full.begin(), full.end())); + return; + } const auto scale = slot.dumpScale; require(scale != 0 && full.size() == static_cast(slot.dumpWidth) * slot.dumpHeight * 4, "invalid frame dump pixels"); const auto width = (slot.dumpWidth + scale - 1) / scale; @@ -624,7 +721,7 @@ struct VulkanDevice::State { } static const auto start = std::chrono::steady_clock::now(); std::fprintf(stderr, "[gpu] frame_%03d.bmp: %ux%u display read back on the GPU at %.1f s\n", index, slot.dumpWidth, slot.dumpHeight, std::chrono::duration(std::chrono::steady_clock::now() - start).count()); - dumpWriter.Enqueue(index, width, height, 1, std::move(pixels)); + dumpWriter.Enqueue(index, width, height, 1, false, std::move(pixels)); } // Blocks on the fence, or polls it every 50 us with APS5_PRESENT_POLL_FENCE=1 (the difference @@ -646,7 +743,17 @@ 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; + Graphics::DeviceLostOwner() = nullptr; + } if (compileOnly && device != VK_NULL_HANDLE) { reinterpret_cast(deviceProc(device, "vkDestroyDevice"))(device, nullptr); device = VK_NULL_HANDLE; @@ -683,29 +790,35 @@ struct VulkanDevice::State { Graphics::ClearCachedPipelines(device); Graphics::ClearDepthSurfaces(device); Graphics::ClearMultisampleTargets(device); + Graphics::ClearSampleOrders(device); { std::lock_guard pipelines(computePipelinesMutex); computePipelines.clear(); + computePipelinesBuilding.clear(); } // Every ShaderResources (kept by the recorder or the resource cache) is gone now, so the // sets and samplers they borrowed can go. copiedWriters->clear(); resourceCache.Clear(); Graphics::ClearCachedTextures(device); + Graphics::ClearSnapshotCopies(device); Graphics::ClearImageMirrors(device); Graphics::ClearHostImports(device); patternBuffers.clear(); workgroupMemory.reset(); if (workgroupMemoryLayout != VK_NULL_HANDLE) reinterpret_cast(deviceProc(device, "vkDestroyDescriptorSetLayout"))(device, workgroupMemoryLayout, nullptr); descriptorCache.reset(); + vertexInputs.reset(); emptyBuffer.reset(); placeholderImages.reset(); samplerCache.reset(); textureCache.reset(); detiler.reset(); colorTransfer.reset(); + overlay.reset(); presentation.reset(); scaler.reset(); + hdrScaler.reset(); pipelineCache.reset(); context.bufferPool.reset(); bufferPool.reset(); @@ -730,6 +843,7 @@ struct VulkanDevice::State { if (pool != VK_NULL_HANDLE) { destroyPool(device, pool, nullptr); } + if (settlePool != VK_NULL_HANDLE) destroyPool(device, settlePool, nullptr); destroyDevice(device, nullptr); } if (instance != VK_NULL_HANDLE) { @@ -764,6 +878,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(); @@ -817,10 +932,7 @@ ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePip check(state->DeviceFunction("vkCreatePipelineCache")(state->device, &cacheInfo, nullptr, &replay.cache), "vkCreatePipelineCache replay"); result.cacheMilliseconds = elapsed(phase); phase = std::chrono::steady_clock::now(); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = capture.spirv.size() * sizeof(std::uint32_t); - moduleInfo.pCode = capture.spirv.data(); - check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects.module), "vkCreateShaderModule replay"); + objects.module = Graphics::CreateShaderModule(state->DeviceFunction("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, capture.spirv.data(), capture.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule replay"); result.moduleMilliseconds = elapsed(phase); phase = std::chrono::steady_clock::now(); std::array, 2> descriptorCounts{}; @@ -828,13 +940,14 @@ ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePip for (const auto& layout : capture.sets) { const bool updateAfterBind = (layout.flags & VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) != 0; hasUpdateAfterBindLayout |= updateAfterBind; - require(!updateAfterBind || state->storageBufferUpdateAfterBind, "captured update-after-bind descriptors are unsupported"); + require(!updateAfterBind || state->storageBufferUpdateAfterBind || state->sampledImageUpdateAfterBind, "captured update-after-bind descriptors are unsupported"); std::vector bindings; std::vector flags; std::uint32_t variableCount = 0; for (const auto& binding : layout.bindings) { require((binding.flags & VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT) == 0 || state->descriptorBindingPartiallyBound, "captured partially-bound descriptors are unsupported"); require((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) == 0 || state->descriptorBindingVariableDescriptorCount, "captured variable descriptor count is unsupported"); + require((binding.flags & VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT) == 0 || (binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? state->storageBufferUpdateAfterBind : state->sampledImageUpdateAfterBind), "captured update-after-bind descriptors are unsupported"); if ((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) != 0) variableCount = binding.count; bindings.push_back({binding.binding, binding.type, binding.count, binding.stages, nullptr}); flags.push_back(binding.flags); @@ -940,6 +1053,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : throw std::runtime_error(std::string("Vulkan presentation: required instance extension missing: ") + name); } } + if (std::any_of(available.begin(), available.end(), [](const auto& item) { return std::strcmp(item.extensionName, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME) == 0; })) { + instanceExtensions.push_back(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME); + state->swapchainColorSpaces = true; + } create.enabledExtensionCount = static_cast(instanceExtensions.size()); create.ppEnabledExtensionNames = instanceExtensions.data(); } @@ -1042,6 +1159,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->properties = properties.properties; state->physical = selected; state->srgbDecodeFormats = Graphics::SrgbDecodeFormats(state->InstanceFunction("vkGetPhysicalDeviceFormatProperties"), selected); + state->srgbCopyFormats = Graphics::SrgbCopyFormats(state->InstanceFunction("vkGetPhysicalDeviceFormatProperties"), selected, state->srgbDecodeFormats); if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0) { state->capabilities.push_back(spv::CapabilityGroupNonUniform); if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BALLOT_BIT) != 0) state->capabilities.push_back(spv::CapabilityGroupNonUniformBallot); @@ -1058,11 +1176,15 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : std::vector availableExtensions(extensionCount); check(enumerateDeviceExtensions(selected, nullptr, &extensionCount, availableExtensions.data()), "vkEnumerateDeviceExtensionProperties"); const auto hasExtension = [&](const char* name) { return std::any_of(availableExtensions.begin(), availableExtensions.end(), [&](const auto& item) { return std::strcmp(item.extensionName, name) == 0; }); }; + VkDriverId driverId{}; if (hasExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME)) { VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES}; VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &driverProperties); + driverId = driver.driverID; state->driverInfo = std::string(driver.driverName) + " " + driver.driverInfo; + state->individuallyFreeDescriptorSets = driver.driverID == VK_DRIVER_ID_MESA_LLVMPIPE; + state->subgroupControlBarriers = SubgroupControlBarrierUsable(driver.driverID); } auto byteFeatures = QueryBdaByteFeatures(selected, state->InstanceFunction("vkGetPhysicalDeviceFeatures2"), availableExtensions); auto bdaFeatures = QueryBdaFeatures(selected, state->InstanceFunction("vkGetPhysicalDeviceFeatures2"), availableExtensions); @@ -1095,6 +1217,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}; @@ -1116,13 +1244,35 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->narrowSubgroupClock = NarrowSubgroupClock(driver.driverID, state->properties.deviceID, state->properties.deviceName); APS5_LOG_OUT("Shader clock driver=%d narrowSubgroupClock=%d", static_cast(driver.driverID), state->narrowSubgroupClock ? 1 : 0); } + VkPhysicalDeviceShaderDemoteToHelperInvocationFeaturesEXT demoteFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DEMOTE_TO_HELPER_INVOCATION_FEATURES_EXT}; + if (hasExtension(VK_EXT_SHADER_DEMOTE_TO_HELPER_INVOCATION_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &demoteFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + state->demoteToHelperInvocation = demoteFeatures.shaderDemoteToHelperInvocation == VK_TRUE; + } + demoteFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DEMOTE_TO_HELPER_INVOCATION_FEATURES_EXT}; + demoteFeatures.shaderDemoteToHelperInvocation = VK_TRUE; + if (hasExtension(VK_KHR_MAINTENANCE_5_EXTENSION_NAME)) { + VkPhysicalDeviceMaintenance5PropertiesKHR maintenance5{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_PROPERTIES_KHR}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &maintenance5}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); + state->earlyFragmentSampleMaskTestBeforeSampleCounting = maintenance5.earlyFragmentSampleMaskTestBeforeSampleCounting == VK_TRUE; + } std::vector deviceExtensions; if (window != nullptr) deviceExtensions.assign(presentationExtensions.begin(), presentationExtensions.end()); + if (window != nullptr && hasExtension(VK_EXT_HDR_METADATA_EXTENSION_NAME)) { + deviceExtensions.push_back(VK_EXT_HDR_METADATA_EXTENSION_NAME); + state->hdrMetadata = true; + } if (state->fragmentShaderBarycentric) { deviceExtensions.push_back(VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME); 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); @@ -1132,6 +1282,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : deviceExtensions.push_back(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilityStencilExportEXT); state->spirvExtensions.push_back("SPV_EXT_shader_stencil_export"); + state->shaderStencilExport = true; } if (state->shaderClock) { deviceExtensions.push_back(VK_KHR_SHADER_CLOCK_EXTENSION_NAME); @@ -1169,6 +1320,15 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->spirvExtensions.push_back("SPV_EXT_shader_viewport_index_layer"); state->viewportIndexLayer = true; } + if (hasExtension(VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME)) { + deviceExtensions.push_back(VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME); + state->imageFormatList = true; + } + if (state->demoteToHelperInvocation) { + deviceExtensions.push_back(VK_EXT_SHADER_DEMOTE_TO_HELPER_INVOCATION_EXTENSION_NAME); + state->capabilities.push_back(spv::CapabilityDemoteToHelperInvocation); + state->spirvExtensions.push_back("SPV_EXT_demote_to_helper_invocation"); + } deviceExtensions.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilitySignedZeroInfNanPreserve); state->spirvExtensions.push_back("SPV_KHR_float_controls"); @@ -1189,7 +1349,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } state->float16DenormPreserve = floatControls.shaderDenormPreserveFloat16 == VK_TRUE && floatControls.denormBehaviorIndependence == VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL; - if (state->float16DenormPreserve && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve); + const auto float32Flush = Float32FlushControlFor(driverId, floatControls.shaderDenormFlushToZeroFloat32, floatControls.denormBehaviorIndependence); + state->float32FlushToZeroCandidate = float32Flush.probe && std::getenv("APS5_NO_HW_F32_FLUSH") == nullptr; + if (state->float32FlushToZeroCandidate && float32Flush.declared) state->capabilities.push_back(spv::CapabilityDenormFlushToZero); + state->float32DenormPreserve = floatControls.shaderDenormPreserveFloat32 == VK_TRUE && floatControls.denormBehaviorIndependence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE && std::getenv("APS5_NO_HW_F32_PRESERVE") == nullptr; + if ((state->float16DenormPreserve || state->float32DenormPreserve) && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve); deviceExtensions.push_back(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); deviceExtensions.push_back(VK_KHR_8BIT_STORAGE_EXTENSION_NAME); state->capabilities.push_back(4448); @@ -1205,12 +1369,18 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); if (locations.sampleLocationSubPixelBits >= 4 && locations.sampleLocationCoordinateRange[0] <= 0.0f && locations.sampleLocationCoordinateRange[1] >= 15.0f / 16.0f) { const auto multisampleProperties = state->InstanceFunction("vkGetPhysicalDeviceMultisamplePropertiesEXT"); - for (const auto samples : {VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT}) { + std::string grids; + for (const auto samples : {VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT}) { if ((locations.sampleLocationSampleCounts & samples) == 0) continue; VkMultisamplePropertiesEXT multisample{VK_STRUCTURE_TYPE_MULTISAMPLE_PROPERTIES_EXT}; multisampleProperties(selected, samples, &multisample); - if (multisample.maxSampleLocationGridSize.width != 0 && multisample.maxSampleLocationGridSize.height != 0) state->sampleLocationSampleCounts |= samples; + const auto grid = multisample.maxSampleLocationGridSize; + state->sampleLocationGridSizes[static_cast(std::countr_zero(static_cast(samples)))] = grid; + if (grid.width != 0 && grid.height != 0) state->sampleLocationSampleCounts |= samples; + if (AgcDriver::Graphics::HostsSampleLocationGrid(grid)) state->sampleLocationGridSampleCounts |= samples; + grids += (grids.empty() ? "" : ", ") + std::to_string(static_cast(samples)) + " samples " + std::to_string(grid.width) + "x" + std::to_string(grid.height); } + if (!grids.empty()) std::fprintf(stderr, "[gpu] largest sample location grids (vkGetPhysicalDeviceMultisamplePropertiesEXT): %s; maxSampleLocationGridSize for every count %ux%u\n", grids.c_str(), locations.maxSampleLocationGridSize.width, locations.maxSampleLocationGridSize.height); } if (state->sampleLocationSampleCounts != 0) deviceExtensions.push_back(VK_EXT_SAMPLE_LOCATIONS_EXTENSION_NAME); } @@ -1255,6 +1425,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &hostProperties}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); state->hostImportAlignment = hostProperties.minImportedHostPointerAlignment; +#ifdef _WIN32 + constexpr bool windows = true; +#else + constexpr bool windows = false; +#endif + state->drawInputsInPlace = std::getenv("APS5_NO_DRAW_INPUTS_IN_PLACE") == nullptr && (std::getenv("APS5_DRAW_INPUTS_IN_PLACE") != nullptr || DrawInputsInPlaceUsable(driverProperties.driverID, windows)); if (!compileOnly) Graphics::SetHostImportWindow(8ull << 20u); #ifdef _WIN32 // Imported pages are locked; locking is bounded by the process working-set minimum. @@ -1330,11 +1506,38 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } nonSeamlessCubeFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_NON_SEAMLESS_CUBE_MAP_FEATURES_EXT}; nonSeamlessCubeFeatures.nonSeamlessCubeMap = VK_TRUE; + VkPhysicalDeviceCoverageReductionModeFeaturesNV coverageReductionFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COVERAGE_REDUCTION_MODE_FEATURES_NV}; + if (hasExtension(VK_NV_FRAMEBUFFER_MIXED_SAMPLES_EXTENSION_NAME) && hasExtension(VK_NV_COVERAGE_REDUCTION_MODE_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &coverageReductionFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + if (coverageReductionFeatures.coverageReductionMode == VK_TRUE) { + const auto combinations = state->InstanceFunction("vkGetPhysicalDeviceSupportedFramebufferMixedSamplesCombinationsNV"); + std::uint32_t count = 0; + check(combinations(selected, &count, nullptr), "vkGetPhysicalDeviceSupportedFramebufferMixedSamplesCombinationsNV"); + std::vector supported(count, {VK_STRUCTURE_TYPE_FRAMEBUFFER_MIXED_SAMPLES_COMBINATION_NV}); + check(combinations(selected, &count, supported.data()), "vkGetPhysicalDeviceSupportedFramebufferMixedSamplesCombinationsNV"); + for (const auto& combination : supported) { + if (combination.coverageReductionMode == VK_COVERAGE_REDUCTION_MODE_MERGE_NV && combination.colorSamples == VK_SAMPLE_COUNT_1_BIT && combination.depthStencilSamples == 0) state->mergedCoverageSampleCounts |= combination.rasterizationSamples; + if (combination.coverageReductionMode == VK_COVERAGE_REDUCTION_MODE_TRUNCATE_NV && combination.colorSamples == VK_SAMPLE_COUNT_1_BIT && combination.depthStencilSamples == combination.rasterizationSamples) state->truncatedCoverageSampleCounts |= combination.rasterizationSamples; + } + } + if (state->mergedCoverageSampleCounts != 0 || state->truncatedCoverageSampleCounts != 0) { + deviceExtensions.push_back(VK_NV_FRAMEBUFFER_MIXED_SAMPLES_EXTENSION_NAME); + deviceExtensions.push_back(VK_NV_COVERAGE_REDUCTION_MODE_EXTENSION_NAME); + } + } else if (hasExtension(VK_AMD_MIXED_ATTACHMENT_SAMPLES_EXTENSION_NAME)) { + state->mixedAttachmentSamples = true; + state->truncatedCoverageSampleCounts = state->properties.limits.framebufferDepthSampleCounts & state->properties.limits.framebufferColorSampleCounts & ~static_cast(VK_SAMPLE_COUNT_1_BIT); + deviceExtensions.push_back(VK_AMD_MIXED_ATTACHMENT_SAMPLES_EXTENSION_NAME); + } + coverageReductionFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COVERAGE_REDUCTION_MODE_FEATURES_NV}; + coverageReductionFeatures.coverageReductionMode = VK_TRUE; VkPhysicalDeviceGraphicsPipelineLibraryFeaturesEXT libraryFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT}; VkPhysicalDeviceExtendedDynamicStateFeaturesEXT dynamicStateFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT}; VkPhysicalDeviceDynamicRenderingFeaturesKHR renderingFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR}; constexpr std::array libraryExtensions{VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME, VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME, VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME, VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME, VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME, VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME}; - if (std::getenv("APS5_NO_GPL") == nullptr && std::all_of(libraryExtensions.begin(), libraryExtensions.end(), hasExtension)) { + bool libraryDevice = false; + if (std::all_of(libraryExtensions.begin(), libraryExtensions.end(), hasExtension)) { dynamicStateFeatures.pNext = &renderingFeatures; libraryFeatures.pNext = &dynamicStateFeatures; VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &libraryFeatures}; @@ -1342,8 +1545,9 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : VkPhysicalDeviceGraphicsPipelineLibraryPropertiesEXT libraryProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_PROPERTIES_EXT}; VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &libraryProperties}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); - state->graphicsPipelineLibrary = libraryFeatures.graphicsPipelineLibrary == VK_TRUE && dynamicStateFeatures.extendedDynamicState == VK_TRUE && renderingFeatures.dynamicRendering == VK_TRUE && libraryProperties.graphicsPipelineLibraryFastLinking == VK_TRUE && properties.properties.limits.maxPushConstantsSize >= Graphics::PipelinePushConstantBytes; - if (state->graphicsPipelineLibrary) deviceExtensions.insert(deviceExtensions.end(), libraryExtensions.begin(), libraryExtensions.end()); + libraryDevice = libraryFeatures.graphicsPipelineLibrary == VK_TRUE && dynamicStateFeatures.extendedDynamicState == VK_TRUE && renderingFeatures.dynamicRendering == VK_TRUE && libraryProperties.graphicsPipelineLibraryFastLinking == VK_TRUE && properties.properties.limits.maxPushConstantsSize >= Graphics::PipelinePushConstantBytes; + if (libraryDevice) deviceExtensions.insert(deviceExtensions.end(), libraryExtensions.begin(), libraryExtensions.end()); + state->graphicsPipelineLibrary = libraryDevice && std::getenv("APS5_NO_GPL") == nullptr; if (state->graphicsPipelineLibrary && hasExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME)) { VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES}; VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver}; @@ -1351,6 +1555,15 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->pipelineLinkTimeOptimization = PipelineLinkTimeOptimizationUsable(driver.driverID); } } + VkPhysicalDeviceMaintenance5FeaturesKHR maintenance5Features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES_KHR}; + if (state->graphicsPipelineLibrary && hasExtension(VK_KHR_MAINTENANCE_5_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &maintenance5Features}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + state->maintenance5 = maintenance5Features.maintenance5 == VK_TRUE; + if (state->maintenance5) deviceExtensions.push_back(VK_KHR_MAINTENANCE_5_EXTENSION_NAME); + } + maintenance5Features = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES_KHR}; + maintenance5Features.maintenance5 = VK_TRUE; renderingFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR, nullptr, VK_TRUE}; libraryFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT, &dynamicStateFeatures, VK_TRUE}; dynamicStateFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT, &renderingFeatures, VK_TRUE}; @@ -1364,6 +1577,37 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : queueInfo.queueFamilyIndex = family; queueInfo.queueCount = 1; queueInfo.pQueuePriorities = &priority; + const float priorities[2]{1.0f, 1.0f}; + bool settleQueue = false; + std::uint32_t settleFamily = family; + 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; + } + } + } VkDeviceCreateInfo deviceInfo{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; deviceInfo.queueCreateInfoCount = 1; deviceInfo.pQueueCreateInfos = &queueInfo; @@ -1382,6 +1626,14 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : enabled.tessellationShader = available.tessellationShader; state->tessellationShader = enabled.tessellationShader == VK_TRUE; if (state->tessellationShader) state->capabilities.push_back(3); + VkPhysicalDeviceShaderDrawParametersFeatures drawParametersFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES}; + { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &drawParametersFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + } + const bool drawParameters = drawParametersFeatures.shaderDrawParameters == VK_TRUE; + drawParametersFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES, nullptr, VK_TRUE}; + if (drawParameters) state->capabilities.push_back(spv::CapabilityDrawParameters); APS5_LOG_OUT("Vulkan features tessellationAvailable=%u mesh=%u depthClip=%u depthRangeUnrestricted=%u pipelineLibrary=%u", static_cast(state->tessellationShader), static_cast(state->meshShader), static_cast(state->depthClipControl), static_cast(state->depthRangeUnrestricted), static_cast(state->graphicsPipelineLibrary)); require(available.samplerAnisotropy && available.textureCompressionBC, "device lacks sampler anisotropy or BC texture compression support required for texture sampling"); enabled.samplerAnisotropy = VK_TRUE; @@ -1389,7 +1641,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"); @@ -1417,6 +1669,22 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : enabled.sparseBinding = state->sparseBinding ? VK_TRUE : VK_FALSE; } deviceInfo.pQueueCreateInfos = queueInfos; + if (settleQueue && settleFamily != family) { + 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 { + auto& settleInfo = queueInfos[deviceInfo.queueCreateInfoCount++]; + settleInfo = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + settleInfo.queueFamilyIndex = settleFamily; + settleInfo.queueCount = 1; + settleInfo.pQueuePriorities = &priority; + } + } // Indirect draw records with a non-zero first instance, and several records per call. enabled.drawIndirectFirstInstance = available.drawIndirectFirstInstance; enabled.multiDrawIndirect = available.multiDrawIndirect; @@ -1425,6 +1693,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : // PA_CL_CLIP_CNTL near/far clip disable maps to depth clamping. enabled.depthClamp = available.depthClamp; state->depthClamp = enabled.depthClamp == VK_TRUE; + enabled.fillModeNonSolid = available.fillModeNonSolid; + state->fillModeNonSolid = enabled.fillModeNonSolid == VK_TRUE; enabled.depthBounds = available.depthBounds; state->depthBounds = enabled.depthBounds == VK_TRUE; enabled.depthBiasClamp = available.depthBiasClamp; @@ -1435,9 +1705,11 @@ 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; + state->storageImageWriteWithoutFormat = enabled.shaderStorageImageWriteWithoutFormat == VK_TRUE; enabled.shaderStorageImageReadWithoutFormat = available.shaderStorageImageReadWithoutFormat; // Gathers with non-constant offsets (ImageGatherExtended). enabled.shaderImageGatherExtended = available.shaderImageGatherExtended; @@ -1447,7 +1719,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : enabled.sampleRateShading = available.sampleRateShading; enabled.geometryShader = available.geometryShader; state->geometryShader = enabled.geometryShader == VK_TRUE; + enabled.largePoints = available.largePoints; + state->largePoints = enabled.largePoints == VK_TRUE; state->sampleRateShading = enabled.sampleRateShading == VK_TRUE; + if (enabled.sampleRateShading) state->capabilities.push_back(spv::CapabilityInterpolationFunction); if (enabled.geometryShader) state->capabilities.push_back(spv::CapabilityGeometry); enabled.shaderClipDistance = available.shaderClipDistance; enabled.shaderCullDistance = available.shaderCullDistance; @@ -1469,16 +1744,17 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); state->descriptorIndexing = descriptorIndexingFeatures.shaderSampledImageArrayNonUniformIndexing == VK_TRUE && descriptorIndexingFeatures.shaderStorageImageArrayNonUniformIndexing == VK_TRUE; state->storageBufferUpdateAfterBind = descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind == VK_TRUE; - state->descriptorBindingPartiallyBound = compileOnly && descriptorIndexingFeatures.descriptorBindingPartiallyBound == VK_TRUE; - state->descriptorBindingVariableDescriptorCount = compileOnly && descriptorIndexingFeatures.descriptorBindingVariableDescriptorCount == VK_TRUE; - state->runtimeDescriptorArray = compileOnly && descriptorIndexingFeatures.runtimeDescriptorArray == VK_TRUE; - if (state->storageBufferUpdateAfterBind) { + state->sampledImageUpdateAfterBind = descriptorIndexingFeatures.descriptorBindingSampledImageUpdateAfterBind == VK_TRUE; + state->descriptorBindingPartiallyBound = descriptorIndexingFeatures.descriptorBindingPartiallyBound == VK_TRUE; + state->descriptorBindingVariableDescriptorCount = descriptorIndexingFeatures.descriptorBindingVariableDescriptorCount == VK_TRUE; + state->runtimeDescriptorArray = descriptorIndexingFeatures.runtimeDescriptorArray == VK_TRUE; + if (state->storageBufferUpdateAfterBind || state->sampledImageUpdateAfterBind) { VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &state->descriptorIndexingProperties}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); } } descriptorIndexingFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT}; - if (state->descriptorIndexing || state->storageBufferUpdateAfterBind || state->descriptorBindingPartiallyBound || state->descriptorBindingVariableDescriptorCount || state->runtimeDescriptorArray) deviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); + if (state->descriptorIndexing || state->storageBufferUpdateAfterBind || state->sampledImageUpdateAfterBind || state->descriptorBindingPartiallyBound || state->descriptorBindingVariableDescriptorCount || state->runtimeDescriptorArray) deviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); if (state->descriptorIndexing) { descriptorIndexingFeatures.shaderSampledImageArrayNonUniformIndexing = VK_TRUE; descriptorIndexingFeatures.shaderStorageImageArrayNonUniformIndexing = VK_TRUE; @@ -1488,6 +1764,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->spirvExtensions.push_back("SPV_EXT_descriptor_indexing"); } descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind = state->storageBufferUpdateAfterBind ? VK_TRUE : VK_FALSE; + descriptorIndexingFeatures.descriptorBindingSampledImageUpdateAfterBind = state->sampledImageUpdateAfterBind ? VK_TRUE : VK_FALSE; descriptorIndexingFeatures.descriptorBindingPartiallyBound = state->descriptorBindingPartiallyBound ? VK_TRUE : VK_FALSE; descriptorIndexingFeatures.descriptorBindingVariableDescriptorCount = state->descriptorBindingVariableDescriptorCount ? VK_TRUE : VK_FALSE; descriptorIndexingFeatures.runtimeDescriptorArray = state->runtimeDescriptorArray ? VK_TRUE : VK_FALSE; @@ -1510,7 +1787,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : depthClipFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &depthClipFeatures; } - if (state->graphicsPipelineLibrary) { + if (libraryDevice) { renderingFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &libraryFeatures; } @@ -1532,6 +1809,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : fifoLatestReadyFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &fifoLatestReadyFeatures; } + if (state->maintenance5) { + maintenance5Features.pNext = const_cast(deviceInfo.pNext); + deviceInfo.pNext = &maintenance5Features; + } if (state->maintenance8) { maintenance8Features.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &maintenance8Features; @@ -1540,11 +1821,20 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : nonSeamlessCubeFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &nonSeamlessCubeFeatures; } + if (!state->mixedAttachmentSamples && (state->mergedCoverageSampleCounts != 0 || state->truncatedCoverageSampleCounts != 0)) { + coverageReductionFeatures.pNext = const_cast(deviceInfo.pNext); + deviceInfo.pNext = &coverageReductionFeatures; + } byteFeatures.pNext = const_cast(deviceInfo.pNext); if (state->fragmentShaderBarycentric) { 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; @@ -1565,15 +1855,23 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : imageAtomicInt64Features.pNext = byteFeatures.pNext; byteFeatures.pNext = &imageAtomicInt64Features; } + if (state->demoteToHelperInvocation) { + demoteFeatures.pNext = byteFeatures.pNext; + byteFeatures.pNext = &demoteFeatures; + } VkPhysicalDeviceImageRobustnessFeaturesEXT imageRobustnessFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_ROBUSTNESS_FEATURES_EXT, nullptr, VK_TRUE}; if (imageRobustness) { imageRobustnessFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &imageRobustnessFeatures; } - if (state->descriptorIndexing || state->storageBufferUpdateAfterBind || state->descriptorBindingPartiallyBound || state->descriptorBindingVariableDescriptorCount || state->runtimeDescriptorArray) { + if (state->descriptorIndexing || state->storageBufferUpdateAfterBind || state->sampledImageUpdateAfterBind || state->descriptorBindingPartiallyBound || state->descriptorBindingVariableDescriptorCount || state->runtimeDescriptorArray) { descriptorIndexingFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &descriptorIndexingFeatures; } + if (drawParameters) { + drawParametersFeatures.pNext = byteFeatures.pNext; + byteFeatures.pNext = &drawParametersFeatures; + } // Timeline semaphores let a queue worker wait for recorded batches without holding the GPU mutex // (see Recorder::WaitSerial). The instance is 1.1, so the KHR extension is used even on 1.2+ // devices. Debug aid: APS5_NO_TIMELINE=1 leaves it off (drains wait under the mutex as before). @@ -1595,10 +1893,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(); @@ -1606,6 +1906,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) { @@ -1631,15 +1935,39 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : pipelineFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &pipelineFeatures; } + VkPhysicalDeviceFaultFeaturesEXT faultFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT}; + if (hasExtension(VK_EXT_DEVICE_FAULT_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &faultFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + } + const bool deviceFault = faultFeatures.deviceFault == VK_TRUE; + if (deviceFault) { + faultFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT}; + faultFeatures.deviceFault = VK_TRUE; + deviceExtensions.push_back(VK_EXT_DEVICE_FAULT_EXTENSION_NAME); + deviceInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); + deviceInfo.ppEnabledExtensionNames = deviceExtensions.data(); + faultFeatures.pNext = byteFeatures.pNext; + byteFeatures.pNext = &faultFeatures; + } + const bool checkpoints = std::getenv("APS5_GPU_CHECKPOINTS") != nullptr && hasExtension(VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME); + if (checkpoints) { + deviceExtensions.push_back(VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME); + deviceInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); + deviceInfo.ppEnabledExtensionNames = deviceExtensions.data(); + } bdaFeatures.pNext = &byteFeatures; VkPhysicalDeviceRobustness2FeaturesEXT robustness2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT}; if (hasExtension(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME)) { VkPhysicalDeviceFeatures2 queried{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness2}; state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &queried); } - if (robustness2.nullDescriptor == VK_TRUE) { + state->robustBufferAccess2 = robustness2.robustBufferAccess2 == VK_TRUE && enabled.robustBufferAccess == VK_TRUE; + 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); @@ -1672,12 +2000,31 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } state->DeviceFunction("vkGetDeviceQueue")(state->device, family, 0, &state->queue); state->queueFamily = family; + if (settleQueue) { + state->settleFamily = settleFamily; + state->DeviceFunction("vkGetDeviceQueue")(state->device, settleFamily, settleIndex, &state->settleQueue); + } 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) { + const auto faultInfo = deviceFault ? state->DeviceFunction("vkGetDeviceFaultInfoEXT") : nullptr; + const auto checkpointData = checkpoints ? state->DeviceFunction("vkGetQueueCheckpointDataNV") : nullptr; + Graphics::DeviceLostHook() = [device = state->device, queue = state->queue, faultInfo, checkpointData] { + for (const auto& line : DeviceFaultReport(device, faultInfo, queue, checkpointData)) std::fprintf(stderr, "%s\n", line.c_str()); + }; + Graphics::DeviceLostOwner() = state.get(); + state->deviceLostHook = true; + } APS5_LOG_OUT("Vulkan device ready device=%p queue=%p family=%u", reinterpret_cast(state->device), reinterpret_cast(state->queue), family); VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; poolInfo.queueFamilyIndex = family; check(state->DeviceFunction("vkCreateCommandPool")(state->device, &poolInfo, nullptr, &state->pool), "vkCreateCommandPool"); + if (state->settleQueue != VK_NULL_HANDLE) { + VkCommandPoolCreateInfo settlePoolInfo = poolInfo; + settlePoolInfo.queueFamilyIndex = state->settleFamily; + check(state->DeviceFunction("vkCreateCommandPool")(state->device, &settlePoolInfo, nullptr, &state->settlePool), "vkCreateCommandPool settle"); + } static const bool procTable = std::getenv("APS5_NO_PROC_TABLE") == nullptr; if (procTable) { Graphics::FillDeviceFunctions(graphicsContext(), state->deviceFunctions); @@ -1686,11 +2033,12 @@ 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()); state->descriptorCache = std::make_unique(graphicsContext()); + state->vertexInputs = std::make_unique(graphicsContext()); state->samplerCache = std::make_unique(); state->CreateWorkgroupMemoryLayout(); if (!state->shaderProfile->NullDescriptors()) { @@ -1723,12 +2071,9 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : require((surface.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0, "surface does not support transfer destination images"); require((surface.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) != 0, "opaque composition is unavailable"); require((surface.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) != 0, "identity surface transform is unavailable"); - std::uint32_t formatCount = 0; - auto getFormats = state->InstanceFunction("vkGetPhysicalDeviceSurfaceFormatsKHR"); - check(getFormats(selected, state->surface, &formatCount, nullptr), "vkGetPhysicalDeviceSurfaceFormatsKHR"); - std::vector formats(formatCount); - check(getFormats(selected, state->surface, &formatCount, formats.data()), "vkGetPhysicalDeviceSurfaceFormatsKHR"); - require(std::any_of(formats.begin(), formats.end(), [](const auto& format) { return format.format == VK_FORMAT_B8G8R8A8_UNORM && format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; }), "BGRA8 sRGB-nonlinear surface format is unavailable"); + refreshSurfaceFormats(); + SetPresentingHdr10(false); + require(std::any_of(state->surfaceFormats.begin(), state->surfaceFormats.end(), [](const auto& format) { return format.format == VK_FORMAT_B8G8R8A8_UNORM && format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; }), "BGRA8 sRGB-nonlinear surface format is unavailable"); std::uint32_t presentModeCount = 0; auto getPresentModes = state->InstanceFunction("vkGetPhysicalDeviceSurfacePresentModesKHR"); check(getPresentModes(selected, state->surface, &presentModeCount, nullptr), "vkGetPhysicalDeviceSurfacePresentModesKHR"); @@ -1741,7 +2086,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : swapchain.surface = state->surface; swapchain.minImageCount = surface.minImageCount; swapchain.imageFormat = state->surfaceFormat; - swapchain.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; + swapchain.imageColorSpace = state->surfaceColorSpace; swapchain.imageExtent = state->extent; swapchain.imageArrayLayers = 1; swapchain.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; @@ -1774,10 +2119,75 @@ 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()); } + if (state->float32FlushToZeroCandidate) { + const auto probeStart = std::chrono::steady_clock::now(); + state->probingFloatControls = true; + const auto probe = ProbeFloat32FlushToZero(*this, Target()); + state->probingFloatControls = false; + state->float32FlushToZero = probe.mismatches == 0u; + std::fprintf(stderr, "[float-controls] f32 denormal flush: %s (%u of %u probe results differ from the emulated flush, %.0f ms)\n", state->float32FlushToZero ? "hardware" : "emulated", probe.mismatches, probe.compared, std::chrono::duration(std::chrono::steady_clock::now() - probeStart).count()); + } + // 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(); +} + +void VulkanDevice::SavePipelineCache() { + if (state->pipelineCache) state->pipelineCache->SaveNow(); +} + +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); @@ -1786,10 +2196,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() { @@ -1818,6 +2240,7 @@ void VulkanDevice::StoreIdleResults(bool reapFirst) { if (!Graphics::StorageTexture::AnyPending()) return; GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); std::lock_guard gpuLock(GuestMemory::GpuMutex()); + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); Graphics::StorageTexture::FlushAllPending("idle"); SubmitRecorded(reapFirst); } @@ -1868,6 +2291,10 @@ bool VulkanDevice::OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) c return state->recorder && state->recorder->OpenWriteOverlaps(address, bytes); } +void VulkanDevice::SealPassWriteOnly() { + if (state->recorder) state->recorder->SealPassWriteOnly(); +} + bool VulkanDevice::RecordedWritesSettled(std::uint64_t address, std::size_t bytes) const { if (!state->recorder) return true; const auto& recorder = *state->recorder; @@ -1882,6 +2309,8 @@ bool VulkanDevice::StoresPendingOver(std::uint64_t address, std::size_t bytes) c } int VulkanDevice::WriteLabelOnGpu(std::uint64_t address, std::span bytes, std::uint64_t stamp, std::uint32_t queue, bool reapFirst) { + static const bool storeAtLabel = Graphics::StorageTexture::StoreAtLabelRequested(std::getenv("APS5_STORE_AT_LABEL")); + if (storeAtLabel) Graphics::StorageTexture::FlushAllPending("label"); if (!state->recorder || bytes.empty() || bytes.size() > 65536 || address % 4 != 0 || bytes.size() % 4 != 0) return 4; auto& recorder = *state->recorder; // Batches that already finished are retired before the checks: their completions ran, so they @@ -1906,7 +2335,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 @@ -1925,10 +2354,10 @@ int VulkanDevice::WriteLabelOnGpu(std::uint64_t address, std::span action, bool reapFirs } bool VulkanDevice::DumpSamplesOnGpu(std::uint64_t address) { + static const bool syncDumps = std::getenv("APS5_SYNC_SAMPLE_DUMPS") != nullptr || std::getenv("APS5_NO_GPU_SAMPLE_DUMPS") != nullptr; + if (syncDumps) return false; if (!state->recorder) return false; auto& recorder = *state->recorder; - constexpr std::size_t bytes = 15 * 16 + 8; + constexpr std::size_t bytes = Pm4::SampleDumpBytes; if (OpportunisticReap()) recorder.Reap(); if (state->CopiedWriterOverlaps(address, bytes) || (Graphics::Recorder::PendingCompletionLabels() != 0 && recorder.CompletionLabelIn(address, bytes))) return false; const auto context = graphicsContext(); @@ -2041,7 +2472,7 @@ bool VulkanDevice::FillBuffer(std::uint64_t address, std::size_t bytes, std::spa const auto now = std::chrono::steady_clock::now(); if (now - lastReport > std::chrono::seconds(10)) { lastReport = now; - AgcDriver::ProfilePrint_nid_no_patch("[fill-sync] fills: %llu synced (%llu with a completion label pending) waited %.0f ms, %llu ordered by barrier, %llu with no pending write; phases ms: decide %.0f record %.0f notes %.0f; %llu uniform, %llu pattern (%llu doubling steps in place; pattern buffers reused %llu, made %llu)\n", static_cast(synced), static_cast(labelSynced), syncedMs, static_cast(ordered), static_cast(fills - synced - ordered), decideMs, recordMs, notesMs, static_cast(uniformFills), static_cast(patternFills), static_cast(doublingSteps), static_cast(patternBuffersReused), static_cast(patternBuffersMade)); + AgcDriver::ProfilePrint_nid_no_patch("[fill-sync] fills (cumulative): %llu synced (%llu with a completion label pending) waited %.0f ms, %llu ordered by barrier, %llu with no pending write; phases ms: decide %.0f record %.0f notes %.0f; %llu uniform, %llu pattern (%llu doubling steps in place; pattern buffers reused %llu, made %llu)\n", static_cast(synced), static_cast(labelSynced), syncedMs, static_cast(ordered), static_cast(fills - synced - ordered), decideMs, recordMs, notesMs, static_cast(uniformFills), static_cast(patternFills), static_cast(doublingSteps), static_cast(patternBuffersReused), static_cast(patternBuffersMade)); } } auto phaseStart = fillStart; @@ -2105,7 +2536,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]); @@ -2143,7 +2574,7 @@ bool VulkanDevice::FillBuffer(std::uint64_t address, std::size_t bytes, std::spa recorder.EndGpuTiming(timing, bytes); recorder.MarkCovered(filledAccess); phase(recordMs); - if (uniform && pattern[0] == (pattern[0] & 0xffu) * 0x01010101u) recorder.NotePendingFill(address, bytes, static_cast(pattern[0])); + if (uniform) recorder.NotePendingWordFill(address, bytes, pattern[0]); else recorder.NotePendingWrite(address, bytes); GuestMemory::MarkWritten(address, bytes); phase(notesMs); @@ -2159,69 +2590,110 @@ std::atomic copiesAliased{0}, copyAliasCreated{0}, copyAliasNoSou // source range must be exactly a live image's surface, and the destination range either exactly // a live image of the same shape or none (one is made from the source's descriptor rebased). Under // GuestMemory::GpuMutex. Debug aid: APS5_NO_COPY_ALIAS=1 keeps every such copy a transfer. -bool AliasCopy(const Graphics::Context& context, std::uint64_t destination, std::uint64_t source, std::size_t bytes) { +bool CopyAliasDisabled() { static const bool disabled = std::getenv("APS5_NO_COPY_ALIAS") != nullptr; - if (disabled) return false; - auto from = Graphics::StorageTexture::FindLive(source, bytes); - if (from == nullptr) { + return disabled; +} + +struct AliasPlan { + std::shared_ptr from; + std::shared_ptr to; + const char* refusal = nullptr; +}; + +AliasPlan PlanAlias(const Graphics::Context& context, std::uint64_t destination, std::uint64_t source, std::size_t bytes) { + AliasPlan plan; + plan.from = Graphics::StorageTexture::FindLive(source, bytes); + if (plan.from == nullptr) { + plan.refusal = "no source"; + return plan; + } + plan.to = Graphics::StorageTexture::FindLive(destination, bytes); + plan.refusal = Graphics::StorageTexture::CopyRefusal(context, plan.to.get(), *plan.from, destination); + return plan; +} + +bool AliasCopy(const Graphics::Context& context, std::uint64_t destination, std::uint64_t source, std::size_t bytes) { + if (CopyAliasDisabled()) return false; + auto plan = PlanAlias(context, destination, source, bytes); + if (plan.from == nullptr) { copyAliasNoSource.fetch_add(1, std::memory_order_relaxed); return false; } - auto to = Graphics::StorageTexture::FindLive(destination, bytes); - if (to != nullptr && !to->SameSurfaceShape(*from)) { - copyAliasShape.fetch_add(1, std::memory_order_relaxed); + if (plan.refusal != nullptr) { + if (plan.to != nullptr && !plan.to->SameSurfaceShape(*plan.from)) { + copyAliasShape.fetch_add(1, std::memory_order_relaxed); + } else { + static std::atomic reported{0}; + if (reported.fetch_add(1) < 8) std::fprintf(stderr, "[copy] image copy 0x%llx -> 0x%llx (0x%zx bytes) refused: %s\n", static_cast(source), static_cast(destination), bytes, plan.refusal); + copyAliasRefused.fetch_add(1, std::memory_order_relaxed); + } return false; } - if (to == nullptr) { - auto resource = from->Descriptor(); + if (plan.to == nullptr) { + auto resource = plan.from->Descriptor(); resource.baseAddress = destination; resource.dccAddress = 0; - try { - to = Graphics::CachedStorageSurface(context, resource); - } catch (const std::exception& error) { - static std::atomic reported{0}; - if (reported.fetch_add(1) < 8) std::fprintf(stderr, "[copy] no storage image for the copy destination 0x%llx: %s\n", static_cast(destination), error.what()); - copyAliasRefused.fetch_add(1, std::memory_order_relaxed); - return false; - } + plan.to = Graphics::CachedStorageSurface(context, resource); copyAliasCreated.fetch_add(1, std::memory_order_relaxed); } const char* refusal = nullptr; - if (!to->CopyFrom(*from, refusal)) { - static std::atomic reported{0}; - if (reported.fetch_add(1) < 8) std::fprintf(stderr, "[copy] image copy 0x%llx -> 0x%llx (0x%zx bytes) refused: %s\n", static_cast(source), static_cast(destination), bytes, refusal); - copyAliasRefused.fetch_add(1, std::memory_order_relaxed); - return false; - } + if (!plan.to->CopyFrom(*plan.from, refusal)) throw std::runtime_error(std::string("AGC graphics: an image copy its check allowed was refused: ") + refusal); copiesAliased.fetch_add(1, std::memory_order_relaxed); return true; } +bool AliasCopyPossible(const Graphics::Context& context, std::uint64_t destination, std::uint64_t source, std::size_t bytes) { + return !CopyAliasDisabled() && PlanAlias(context, destination, source, bytes).refusal == nullptr; +} + } VulkanDevice::CopyVerification VulkanDevice::CopyVerifyCounts() { return {copiesVerified.load(std::memory_order_relaxed), copySourceChanged.load(std::memory_order_relaxed), copyDestinationChanged.load(std::memory_order_relaxed), copyReaderMissed.load(std::memory_order_relaxed)}; } +namespace { +std::atomic computePipelinesCreated{0}; +std::atomic computePipelinesCreatedLocked{0}; +} + +VulkanDevice::ComputePipelineCreation VulkanDevice::ComputePipelineCounts() { + return {computePipelinesCreated.load(std::memory_order_relaxed), computePipelinesCreatedLocked.load(std::memory_order_relaxed)}; +} + VulkanDevice::CopyAliasing VulkanDevice::CopyAliasCounts() { return {copiesAliased.load(std::memory_order_relaxed), copyAliasCreated.load(std::memory_order_relaxed), copyAliasNoSource.load(std::memory_order_relaxed), copyAliasShape.load(std::memory_order_relaxed), copyAliasRefused.load(std::memory_order_relaxed)}; } +bool VulkanDevice::completionStoresOver(std::uint64_t address, std::size_t bytes) const { + static const bool alwaysSync = std::getenv("APS5_COPY_SYNC") != nullptr; + if (alwaysSync || state->CopiedWriterOverlaps(address, bytes)) return true; + return Graphics::Recorder::PendingCompletionLabels() != 0 && state->recorder->CompletionLabelIn(address, bytes); +} + +bool VulkanDevice::copyWaitsOnCompletions(std::uint64_t address, std::size_t bytes) const { + return state->recorder->PendingWriteOverlaps(address, bytes) && completionStoresOver(address, bytes); +} + +bool VulkanDevice::GpuCopyPossible(std::uint64_t destination, std::uint64_t source, std::size_t bytes) const { + if (!state->recorder || bytes == 0 || copyWaitsOnCompletions(source, bytes) || copyWaitsOnCompletions(destination, bytes)) return false; + const auto context = graphicsContext(); + if (AliasCopyPossible(context, destination, source, bytes)) return true; + return Graphics::HostImportFor(context, destination, bytes) != nullptr && Graphics::HostImportFor(context, source, bytes) != nullptr; +} + VulkanDevice::CopyOutcome VulkanDevice::CopyBuffer(std::uint64_t destination, std::uint64_t source, std::size_t bytes, std::size_t cpuMax, std::size_t gpuMax, std::size_t knownMax, std::uint64_t programAddress, std::uint32_t queue, const CopyWriterNote& noteWriter) { CopyOutcome outcome{2, false, 0, CopyOutcome::None, false, false, 0, 0, 0, 0, false}; if (!state->recorder || bytes == 0) return outcome; auto& recorder = *state->recorder; // Finished batches were retired by the caller (Driver.cpp copyBuffer, on the graphics worker // only, as WriteLabelOnGpu reaps), so their writes neither refuse the CPU copy nor force a wait. - static const bool alwaysSync = std::getenv("APS5_COPY_SYNC") != nullptr; static const bool waitForSource = std::getenv("APS5_COPY_WAIT_SOURCE") != nullptr; static const bool verify = std::getenv("APS5_COPY_VERIFY") != nullptr; // A store a batch's completion makes on the CPU over the range (a copied buffer's write-back, // a completion-deferred label): it lands only when that batch is reaped, whatever its fence. - const auto completionStoreOverlaps = [&](std::uint64_t address) { - if (alwaysSync || state->CopiedWriterOverlaps(address, bytes)) return true; - return Graphics::Recorder::PendingCompletionLabels() != 0 && recorder.CompletionLabelIn(address, bytes); - }; + const auto completionStoreOverlaps = [&](std::uint64_t address) { return completionStoresOver(address, bytes); }; // The CPU decision (see the header): pure queries, in the order that makes the first failing // test the reason; a signaled writer of the source is fine (its in-place stores are final in // host memory) as long as no completion store or label of it is still to land. @@ -2299,8 +2771,8 @@ VulkanDevice::CopyOutcome VulkanDevice::CopyBuffer(std::uint64_t destination, st // before the transfer (or the device copy below) by its barrier; a store a batch's completion // makes on the CPU must land before the copy reads the source or writes the destination, so // the copy waits for that batch (SyncThrough) first. - const bool waitSource = recorder.PendingWriteOverlaps(source, bytes) && completionStoreOverlaps(source); - const bool waitDestination = recorder.PendingWriteOverlaps(destination, bytes) && completionStoreOverlaps(destination); + const bool waitSource = copyWaitsOnCompletions(source, bytes); + const bool waitDestination = copyWaitsOnCompletions(destination, bytes); if (waitSource || waitDestination) { outcome.synced = true; const auto syncStart = std::chrono::steady_clock::now(); @@ -2314,12 +2786,10 @@ VulkanDevice::CopyOutcome VulkanDevice::CopyBuffer(std::uint64_t destination, st } outcome.syncMs = std::chrono::duration(std::chrono::steady_clock::now() - syncStart).count(); } - // A whole surface copied into another: a device copy between their images, whose results reach - // the destination's memory through the image's deferred write-back (nothing crosses to host - // memory for the copy itself). if (AliasCopy(context, destination, source, bytes)) { noteWriter({}, 0); recorder.NotePendingWrite(destination, bytes); + Graphics::StorageTexture::FlushPending(destination, bytes, nullptr, "buffer copy destination", Graphics::PublishScope::Whole); outcome.path = 3; return outcome; } @@ -2363,7 +2833,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. @@ -2373,6 +2843,8 @@ VulkanDevice::CopyOutcome VulkanDevice::CopyBuffer(std::uint64_t destination, st recorder.EndGpuTiming(classTiming, bytes); recorder.MarkCovered(copiedAccess); recorder.NotePendingRead(source, bytes, Graphics::Recorder::ReadKind::CopySource); + GuestMemory::MarkWritten(destination, bytes); + recorder.NotePendingWrite(destination, bytes); // The destination's bytes after the transfer are the source's now when nothing recorded, // pending or still to land can change the source before the transfer reads it (a CPU store // to it in between is a hardware race too): the caller's ring entry carries them, stamped @@ -2398,6 +2870,10 @@ void VulkanDevice::Resize(std::uint32_t width, std::uint32_t height) { return; } if (state->extent.width == width && state->extent.height == height) return; + recreateSwapchain(width, height, false); +} + +void VulkanDevice::recreateSwapchain(std::uint32_t width, std::uint32_t height, bool force) { VkSurfaceCapabilitiesKHR surface{}; check(state->InstanceFunction("vkGetPhysicalDeviceSurfaceCapabilitiesKHR")(state->physical, state->surface, &surface), "vkGetPhysicalDeviceSurfaceCapabilitiesKHR resize"); // The window's drawable size and the surface's extent can disagree for a moment while the window @@ -2410,16 +2886,21 @@ void VulkanDevice::Resize(std::uint32_t width, std::uint32_t height) { state->extent = {0, 0}; return; } - if (state->extent.width == width && state->extent.height == height) return; + if (!force && state->extent.width == width && state->extent.height == height) return; } WaitIdle(); + refreshSurfaceFormats(); + if (state->surfaceColorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT) { + const bool offered = std::any_of(state->surfaceFormats.begin(), state->surfaceFormats.end(), [&](const VkSurfaceFormatKHR& format) { return format.format == state->surfaceFormat && format.colorSpace == state->surfaceColorSpace; }); + if (!offered || !AgcDriver::CurrentHostHdr().available) useSurfaceFormat({VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}); + } require(width >= surface.minImageExtent.width && width <= surface.maxImageExtent.width && height >= surface.minImageExtent.height && height <= surface.maxImageExtent.height, "unsupported resized output extent"); require((surface.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0 && (surface.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) != 0 && (surface.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) != 0, "resized surface capabilities are unsupported"); VkSwapchainCreateInfoKHR create{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; create.surface = state->surface; create.minImageCount = surface.minImageCount; create.imageFormat = state->surfaceFormat; - create.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; + create.imageColorSpace = state->surfaceColorSpace; create.imageExtent = {width, height}; create.imageArrayLayers = 1; create.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; @@ -2441,9 +2922,92 @@ void VulkanDevice::Resize(std::uint32_t width, std::uint32_t height) { check(getImages(state->device, replacement, &count, state->images.data()), "vkGetSwapchainImagesKHR resize"); state->images.resize(count); state->rendered.assign(count, VK_NULL_HANDLE); + if (state->surfaceColorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT) setHdrMetadata(); APS5_LOG_OUT_DEBUG("Resize complete swapchain=%p extent=%ux%u images=%u", reinterpret_cast(state->swapchain), state->extent.width, state->extent.height, count); } +void VulkanDevice::refreshSurfaceFormats() { + const auto getFormats = state->InstanceFunction("vkGetPhysicalDeviceSurfaceFormatsKHR"); + const auto displayGeneration = AgcDriver::CurrentHostHdr().displayGeneration; + std::uint32_t count = 0; + check(getFormats(state->physical, state->surface, &count, nullptr), "vkGetPhysicalDeviceSurfaceFormatsKHR"); + std::vector formats(count); + check(getFormats(state->physical, state->surface, &count, formats.data()), "vkGetPhysicalDeviceSurfaceFormatsKHR"); + formats.resize(count); + constexpr VkFormatFeatureFlags target = VK_FORMAT_FEATURE_BLIT_DST_BIT | VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT; + std::erase_if(formats, [&](const VkSurfaceFormatKHR& format) { + if (format.colorSpace != VK_COLOR_SPACE_HDR10_ST2084_EXT) return false; + VkFormatProperties properties{}; + graphicsContext().formatProperties(state->physical, format.format, &properties); + return (properties.optimalTilingFeatures & target) != target; + }); + state->surfaceFormats = std::move(formats); + state->surfaceDisplayGeneration = displayGeneration; + constexpr VkFormatFeatureFlags scalerSource = VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT; + const std::array pqTexels{VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}; + const bool sources = std::all_of(pqTexels.begin(), pqTexels.end(), [&](VkFormat format) { + VkFormatProperties properties{}; + graphicsContext().formatProperties(state->physical, format, &properties); + return (properties.optimalTilingFeatures & scalerSource) == scalerSource; + }); + AgcDriverSetSurfaceHdr10_nid_postfix(state->swapchainColorSpaces && state->hdrMetadata && sources && Hdr10SurfaceFormat(state->surfaceFormats, VK_FORMAT_UNDEFINED).has_value()); +} + +void VulkanDevice::setHdrMetadata() { + const auto host = AgcDriver::CurrentHostHdr(); + state->metadataGeneration = host.generation; + require(host.available && state->hdrMetadata && state->surfaceColorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT && state->swapchain != VK_NULL_HANDLE, "HDR metadata needs HDR output, an HDR10 swapchain and VK_EXT_hdr_metadata"); + VkHdrMetadataEXT metadata{VK_STRUCTURE_TYPE_HDR_METADATA_EXT}; + metadata.displayPrimaryRed = {0.708f, 0.292f}; + metadata.displayPrimaryGreen = {0.170f, 0.797f}; + metadata.displayPrimaryBlue = {0.131f, 0.046f}; + metadata.whitePoint = {0.3127f, 0.3290f}; + metadata.maxLuminance = host.display.maxLuminance; + metadata.minLuminance = host.display.minLuminance; + metadata.maxContentLightLevel = PqToSdrWhiteNits(host); + metadata.maxFrameAverageLightLevel = std::min(host.display.maxFullFrameLuminance, metadata.maxContentLightLevel); + state->DeviceFunction("vkSetHdrMetadataEXT")(state->device, 1, &state->swapchain, &metadata); + std::fprintf(stderr, "[videoout] HDR metadata: mastering %.1f to %.4f nits, MaxCLL %.1f nits, MaxFALL %.1f nits\n", metadata.maxLuminance, metadata.minLuminance, metadata.maxContentLightLevel, metadata.maxFrameAverageLightLevel); +} + +void VulkanDevice::useSurfaceFormat(VkSurfaceFormatKHR format) { + state->surfaceFormat = format.format; + state->surfaceColorSpace = format.colorSpace; + state->scaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, state->surfaceFormat); + state->hdrScaler.reset(); + state->hdrScalerSource = VK_FORMAT_UNDEFINED; + const bool hdr = state->surfaceColorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT; + SetPresentingHdr10(hdr); + std::fprintf(stderr, "[videoout] presenting %s: swapchain format %d in color space %d\n", hdr ? "HDR10" : "SDR", static_cast(state->surfaceFormat), static_cast(state->surfaceColorSpace)); +} + +void VulkanDevice::SelectDynamicRange(const DisplayBuffer* buffer) { + GuestMemory::AssertGpuLockHeld("VulkanDevice::SelectDynamicRange"); + require(state->swapchain != VK_NULL_HANDLE, "cannot select the dynamic range of an unavailable swapchain"); + if (AgcDriver::CurrentHostHdr().displayGeneration != state->surfaceDisplayGeneration) refreshSurfaceFormats(); + const auto host = AgcDriver::CurrentHostHdr(); + const bool metadataStale = state->surfaceColorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT && host.available && host.generation != state->metadataGeneration; + if (buffer == nullptr || state->extent.width == 0 || state->extent.height == 0) { + if (metadataStale) setHdrMetadata(); + return; + } + const auto wanted = DynamicRangeSurfaceFormat(state->surfaceFormats, buffer->pixelFormat, host.available); + if (wanted.format == state->surfaceFormat && wanted.colorSpace == state->surfaceColorSpace) { + if (metadataStale) setHdrMetadata(); + return; + } + WaitIdle(); + useSurfaceFormat(wanted); + recreateSwapchain(state->extent.width, state->extent.height, true); +} + +void VulkanDevice::ensureHdrScaler(VkFormat source) { + if (state->hdrScaler != nullptr && state->hdrScalerSource == source) return; + if (state->SharedSlotInFlight()) RetirePresents(0); + state->hdrScaler = std::make_unique(graphicsContext(), source, state->surfaceFormat); + state->hdrScalerSource = source; +} + bool VulkanDevice::Presentable() const { return state->extent.width != 0 && state->extent.height != 0; } @@ -2463,20 +3027,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; @@ -2504,6 +3058,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); @@ -2512,20 +3067,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) { @@ -2575,6 +3117,14 @@ bool ResidentKeysMoved(const Graphics::StorageTexture& resident) { return resident.Descriptor().dccAddress != 0 && ResidentKeys(resident) != resident.UploadedKeys(); } +VkClearColorValue PqClearValue(const DisplayBuffer& buffer) { + const auto texel = DisplayBufferClearTexel(buffer, DisplayBufferKeys(buffer)); + std::uint32_t word = 0; + std::memcpy(&word, texel.data(), sizeof(word)); + const auto color = PqClearColor(word, DisplayRedLow(buffer.pixelFormat)); + return VkClearColorValue{{color[0], color[1], color[2], color[3]}}; +} + std::optional> CompressedClearPixel(const DisplayBuffer& buffer, std::size_t bytes) { GuestMemory::FlushGpuWrites(buffer.address, bytes); const auto keys = DisplayBufferKeys(buffer); @@ -2582,6 +3132,13 @@ std::optional> CompressedClearPixel(const DisplayBuffer return DisplayBufferClearPixel(buffer, keys); } +std::vector DisplayPixels(const DisplayBuffer& buffer, const std::optional>& cleared) { + if (!cleared) return ReadDisplayBuffer(buffer); + std::vector pixels(static_cast(buffer.width) * buffer.height * cleared->size()); + for (std::size_t offset = 0; offset < pixels.size(); offset += cleared->size()) std::memcpy(pixels.data() + offset, cleared->data(), cleared->size()); + return pixels; +} + // Debug aid: APS5_NO_RESIDENT_PRESENT=1 always presents through guest memory. bool NoResidentPresent() { static const bool no = std::getenv("APS5_NO_RESIDENT_PRESENT") != nullptr; @@ -2617,21 +3174,30 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { if (storeAtFlip) Graphics::StorageTexture::FlushAllPending("flip"); else Graphics::StorageTexture::FlushDumpSelected("flip"); auto& dumps = Dumps(); + const bool passthrough = state->surfaceColorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT; + require(!passthrough || DisplayPq(buffer.pixelFormat), "only a PQ display buffer is presented to the HDR10 swapchain"); if (buffer.tilingMode == 1) { - const auto pixels = ReadDisplayBuffer(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 (dumps.cpu) { + 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) { WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, pixels, DumpScale()); } else { state->nextDumpIndex = dumps.dumped++; dumpFrame = true; } } - if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame)) { + const bool screenshot = takeScreenshot(dumpFrame); + auto shot = screenshot && passthrough ? ReadDisplayBuffer(buffer) : std::vector{}; + if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame || (screenshot && !passthrough), false, nullptr, 0, passthrough)) { if (dumpFrame) --dumps.dumped; + if (screenshot) Screenshot::Request(); return false; } + if (!shot.empty()) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, 1, true, std::move(shot)); return true; } static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; @@ -2663,9 +3229,12 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { // image first, as a sampled texture of the image would (see cachedTexture). The uncached // walk: the collect memo is per worker packet, so on this thread a memoized answer could // miss a CPU write that landed after a worker's walk of the same range. + static const bool noAdopt = std::getenv("APS5_NO_PRESENT_EPOCH") != nullptr; + if (!noAdopt) GuestMemory::AdoptLatestCollectEpoch(); GuestMemory::CollectWritesUncached(buffer.address, bytes); - if (!GuestMemory::UnchangedSince(buffer.address, bytes, resident->Generation()) || (buffer.dccAddress != 0 && ResidentKeysMoved(*resident))) { - resident->Refresh(); + if (!GuestMemory::UnchangedSince(buffer.address, bytes, resident->GenerationFor(buffer.address, bytes)) || (buffer.dccAddress != 0 && ResidentKeysMoved(*resident))) { + if (resident->ImageLayers() > 1) resident->RefreshLayers(layer, 1); + else resident->Refresh(); ++refreshedPresents; } ++residentPresents; @@ -2673,11 +3242,9 @@ 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 (dumps.cpu) { - auto full = cleared ? std::vector(static_cast(buffer.width) * buffer.height * cleared->size()) : ReadDisplayBuffer(buffer); - for (std::size_t offset = 0; cleared && offset < full.size(); offset += cleared->size()) std::memcpy(full.data() + offset, cleared->data(), cleared->size()); - WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, full, DumpScale()); + if (frameDumpOutput.Enabled_nid_no_patch() && dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { + if (dumps.cpu || (passthrough && resident == nullptr)) { + WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, DisplayPixels(buffer, cleared), DumpScale()); } else { state->nextDumpIndex = dumps.dumped++; dumpFrame = true; @@ -2686,22 +3253,35 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { } if (profile && std::chrono::steady_clock::now() - lastReport > std::chrono::seconds(10)) { lastReport = std::chrono::steady_clock::now(); - AgcDriver::ProfilePrint_nid_no_patch("[flip] %llu presents from the resident image (%llu refreshed first), through guest memory: %llu not pending, %llu unsuitable; %llu GPU frame dumps\n", static_cast(residentPresents), static_cast(refreshedPresents), static_cast(notPending), static_cast(unsuitable), static_cast(gpuDumps)); + AgcDriver::ProfilePrint_nid_no_patch("[flip] (cumulative): %llu presents from the resident image (%llu refreshed first), through guest memory: %llu not pending, %llu unsuitable; %llu GPU frame dumps\n", static_cast(residentPresents), static_cast(refreshedPresents), static_cast(notPending), static_cast(unsuitable), static_cast(gpuDumps)); } + const bool screenshot = takeScreenshot(dumpFrame); + const bool cpuScreenshot = screenshot && passthrough && resident == nullptr; + auto shot = cpuScreenshot ? DisplayPixels(buffer, cleared) : std::vector{}; CaptureTrace::Log("present dump=%d address=%llx width=%u height=%u resident=%d generation=%llu", dumpFrame ? state->nextDumpIndex : -1, static_cast(buffer.address), buffer.width, buffer.height, resident != nullptr, static_cast(resident ? resident->Generation() : 0)); + if (passthrough && resident == nullptr) ensureHdrScaler(DisplayTexelFormat(buffer.pixelFormat)); VkClearColorValue uniform{}; - if (cleared) { + if (cleared && passthrough) { + uniform = PqClearValue(buffer); + } else if (cleared) { const auto channel = [&](std::size_t index) { return static_cast(std::to_integer((*cleared)[index])) / 255.0f; }; uniform = {{channel(2), channel(1), channel(0), channel(3)}}; } - if (!present(buffer.width, buffer.height, true, {}, cleared ? nullptr : &buffer, resident, filter, dumpFrame, convert, cleared ? &uniform : nullptr, layer)) { + if (!present(buffer.width, buffer.height, true, {}, cleared ? nullptr : &buffer, resident, filter, dumpFrame || (screenshot && !cpuScreenshot), convert, cleared ? &uniform : nullptr, layer, passthrough)) { // A dropped frame (swapchain out of date) keeps the dump numbering contiguous. if (dumpFrame) --dumps.dumped; + if (screenshot) Screenshot::Request(); return false; } + if (cpuScreenshot) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, 1, true, std::move(shot)); return true; } +bool VulkanDevice::takeScreenshot(bool dumpFrame) { + state->nextDumpScreenshot = !dumpFrame && state->dumpWriter.Idle() && Screenshot::Take(); + return state->nextDumpScreenshot; +} + bool VulkanDevice::AcquireImage() { PerformanceTimer timing("Vulkan.Acquire"); require(state->swapchain != VK_NULL_HANDLE, "device has no swapchain"); @@ -2743,7 +3323,7 @@ bool VulkanDevice::AcquireImage() { return true; } -bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display, const std::shared_ptr& resident, VkFilter residentFilter, bool dumpFrame, bool residentConvert, const VkClearColorValue* uniform, std::uint32_t residentLayer) { +bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display, const std::shared_ptr& resident, VkFilter residentFilter, bool dumpFrame, bool residentConvert, const VkClearColorValue* uniform, std::uint32_t residentLayer, bool passthrough) { PerformanceTimer timing("Vulkan.Present"); APS5_LOG_OUT_DEBUG("present begin width=%u height=%u opaque=%u pixels=%zu", width, height, static_cast(opaque), pixels.size()); require(state->swapchain != VK_NULL_HANDLE, "device has no swapchain"); @@ -2762,6 +3342,10 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu require(uniform == nullptr || (pixels.empty() && display == nullptr && resident == nullptr), "a uniform presentation has no other source"); const bool direct = resident != nullptr && !residentConvert; const bool drawn = resident != nullptr && residentConvert && !DisplayHalfFloat(display->pixelFormat); + require(passthrough == (state->surfaceColorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT) || clearOnly, "a presentation's dynamic range differs from its swapchain's"); + require(!passthrough || drawn || clearOnly || (state->hdrScaler != nullptr && !dumpFrame), "an HDR10 presentation through the scaler needs the HDR10 scaler and dumps on the CPU"); + auto* scaler = passthrough && !drawn ? state->hdrScaler.get() : state->scaler.get(); + const float whiteNits = PqToSdrWhiteNits(AgcDriver::CurrentHostHdr()); // The scaler's source image, the color transfer's staging and the upload buffer are single // objects an in-flight blit through them may still read: the paths using or re-creating them // wait for every slot first. The game path did so before taking the mutex @@ -2772,6 +3356,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu if (state->presentSlots[state->presentCursor].inFlight) RetirePresents(state->presentSlots.size() - 1); auto& slot = state->presentSlots[state->presentCursor]; require(!slot.inFlight, "presentation slot is still in flight"); + const bool overlay = PerformanceOverlay::Visible(); if (display != nullptr && resident == nullptr) { static_cast(DisplayBufferSize(*display)); state->colorTransfer->Upload(display->address, width, height, Graphics::ColorTileMode::RenderTarget, DisplayTexelBytes(display->pixelFormat)); @@ -2827,8 +3412,8 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu state->DeviceFunction("vkCmdClearColorImage")(commands, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &barrier.subresourceRange); } else { APS5_LOG_OUT_DEBUG("Recording swapchain scaled blit width=%u height=%u bytes=%zu buffer=%p image=%p", width, height, pixels.size(), reinterpret_cast(state->uploadBuffer), reinterpret_cast(barrier.image)); - require(state->scaler != nullptr && state->presentation != nullptr, "presentation scaler is unavailable"); - if (shared) state->scaler->EnsureSourceImage(width, height); + require(scaler != nullptr && state->presentation != nullptr, "presentation scaler is unavailable"); + if (shared) scaler->EnsureSourceImage(width, height); const auto slotIndex = static_cast(&slot - state->presentSlots.data()); VkImageMemoryBarrier residentBarrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; const VkPipelineStageFlags residentStage = drawn ? VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT : VK_PIPELINE_STAGE_TRANSFER_BIT; @@ -2849,34 +3434,36 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); if (residentConvert && !drawn) { state->colorTransfer->DetileImage(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, Graphics::ColorTileMode::RenderTarget, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat), DisplayHalfFloat(display->pixelFormat), residentLayer); - state->scaler->RecordUpload(commands, state->colorTransfer->LinearBuffer()); + scaler->RecordUpload(commands, state->colorTransfer->LinearBuffer()); } } else { - if (display != nullptr) { + if (display != nullptr && passthrough) { + state->colorTransfer->Detile(commands); + } else if (display != nullptr) { const bool pq = DisplayPq(display->pixelFormat); - state->colorTransfer->Detile(commands, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat) && !pq, DisplayHalfFloat(display->pixelFormat), pq ? std::optional(SdrToneMapLuminanceNits) : std::nullopt); + state->colorTransfer->Detile(commands, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat) && !pq, DisplayHalfFloat(display->pixelFormat), pq ? std::optional(whiteNits) : std::nullopt); } - if (uniform != nullptr) state->scaler->RecordClear(commands, *uniform); - else state->scaler->RecordUpload(commands, display != nullptr ? state->colorTransfer->LinearBuffer() : state->uploadBuffer); + if (uniform != nullptr) scaler->RecordClear(commands, *uniform); + else scaler->RecordUpload(commands, display != nullptr ? state->colorTransfer->LinearBuffer() : state->uploadBuffer); } if (drawn) { - state->presentation->Bind(slotIndex, resident->Image(), width, height, display->pixelFormat, SdrToneMapLuminanceNits, residentLayer); - state->presentation->RecordPresent(commands, slotIndex, barrier.image, state->surfaceFormat, state->extent, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + state->presentation->Bind(slotIndex, resident->Image(), width, height, display->pixelFormat, whiteNits, residentLayer); + state->presentation->RecordPresent(commands, slotIndex, barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, overlay ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); } else { - state->scaler->RecordLetterbox(commands, width, height, barrier.image, state->extent.width, state->extent.height); + scaler->RecordLetterbox(commands, width, height, barrier.image, state->extent.width, state->extent.height); if (direct) PresentationScaler::RecordBlitFrom(graphicsContext(), commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, residentFilter, barrier.image, state->extent.width, state->extent.height, residentLayer); - else state->scaler->RecordBlit(commands, barrier.image, state->extent.width, state->extent.height); + else scaler->RecordBlit(commands, barrier.image, state->extent.width, state->extent.height); } if (dumpFrame) { - const auto scale = DumpScale(); + const auto scale = state->nextDumpScreenshot ? 1 : DumpScale(); if (drawn || direct) { const auto extent = PresentationPass::DumpExtent(width, height, scale); slot.dumpWidth = extent.width; slot.dumpHeight = extent.height; slot.dumpScale = 1; } else { - slot.dumpWidth = state->scaler->SourceWidth(); - slot.dumpHeight = state->scaler->SourceHeight(); + slot.dumpWidth = scaler->SourceWidth(); + slot.dumpHeight = scaler->SourceHeight(); slot.dumpScale = scale; } const auto dumpBytes = static_cast(slot.dumpWidth) * slot.dumpHeight * 4; @@ -2889,7 +3476,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu slot.dumpScaler->RecordBlitInto(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_FILTER_NEAREST, width, height, residentLayer); slot.dumpScaler->RecordReadback(commands, slot.dumpBuffer->Handle()); } else { - state->scaler->RecordReadback(commands, slot.dumpBuffer->Handle()); + scaler->RecordReadback(commands, slot.dumpBuffer->Handle()); } } if (resident != nullptr) { @@ -2899,7 +3486,11 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); } } - if (!drawn) { + if (overlay) { + if (!state->overlay) state->overlay = std::make_unique(graphicsContext(), state->presentSlots.size()); + state->overlay->NotePresent(std::chrono::steady_clock::now(), clearOnly ? VkExtent2D{} : VkExtent2D{width, height}); + state->overlay->Record(commands, static_cast(&slot - state->presentSlots.data()), barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, drawn ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + } else if (!drawn) { barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; barrier.dstAccessMask = 0; barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; @@ -2932,6 +3523,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu slot.shared = shared; // Only a submitted readback is written after the fence (a failed submit tears the device down). slot.dumpRecorded = dumpFrame; + slot.dumpScreenshot = dumpFrame && state->nextDumpScreenshot; slot.dumpIndex = state->nextDumpIndex; slot.recorderSerial = batchesAtBlit; slot.previousSerial = state->lastPresentSerial; @@ -3092,9 +3684,23 @@ std::uint32_t VulkanDevice::VendorId() const { return state->properties.vendorID; } +bool VulkanDevice::SupportsImageFormat(VkFormat format, VkImageUsageFlags usage) const { + VkImageFormatProperties properties{}; + const auto query = state->InstanceFunction("vkGetPhysicalDeviceImageFormatProperties"); + return query(state->physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &properties) == VK_SUCCESS; +} + ShaderRecompiler::SpirvTarget VulkanDevice::Target() const { require(state->shaderProfile != nullptr, "shader device profile is unavailable"); - return state->shaderProfile->Target(); + auto target = state->shaderProfile->Target(); + target.float32FlushToZero = state->float32FlushToZero; + target.float32DenormPreserve = state->float32DenormPreserve; + return target; +} + +Graphics::SampleOrder VulkanDevice::HostSampleOrder(const Graphics::State& graphics) const { + if (!graphics.sampleLocationGrid) return {}; + return Graphics::HostSampleLocationsFor(graphicsContext(), graphics).order; } ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { @@ -3105,9 +3711,13 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.narrowSubgroupClock = state->narrowSubgroupClock; target.float64DenormPreserve = state->float64DenormPreserve; target.float16DenormPreserve = state->float16DenormPreserve; + target.spannedWaves = state->spannedWaves; + target.depthRangeRemap = !state->depthRangeUnrestricted; target.fixedPushSlots = state->graphicsPipelineLibrary; target.registrationModules = state->graphicsPipelineLibrary; - target.srgbDecodeFormats = state->srgbDecodeFormats; + target.robustBufferAccess2 = state->robustBufferAccess2; + target.waveLdsMemoryBarriers = !state->subgroupControlBarriers; + target.srgbDecodeFormats = state->srgbDecodeFormats & ~state->srgbCopyFormats; target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0; if (state->meshShader) { const auto& mesh = state->meshLimits; @@ -3123,6 +3733,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; } @@ -3187,14 +3801,20 @@ 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; context.sparseFamily = state->sparseFamily; context.sparseQueue = state->sparseQueue; context.sparseQueueMutex = &state->sparseQueueMutex; + context.settleQueue = state->settlePool != VK_NULL_HANDLE ? state->settleQueue : VK_NULL_HANDLE; + context.settlePool = state->settlePool; + context.settleFamily = state->settleFamily; + context.settleQueueMutex = &state->settleQueueMutex; context.recorder = state->recorder.get(); context.descriptorCache = state->descriptorCache.get(); + context.vertexInputs = state->vertexInputs.get(); context.samplerCache = state->samplerCache.get(); context.drawIndirectFirstInstance = state->drawIndirectFirstInstance; context.multiDrawIndirect = state->multiDrawIndirect; @@ -3203,11 +3823,15 @@ Graphics::Context VulkanDevice::buildContext() const { context.depthBiasClamp = state->depthBiasClamp; context.samplerFilterMinmax = state->samplerFilterMinmax; context.nonSeamlessCubeMap = state->nonSeamlessCubeMap; + context.mergedCoverageSampleCounts = state->mergedCoverageSampleCounts; + context.truncatedCoverageSampleCounts = state->truncatedCoverageSampleCounts; + context.mixedAttachmentSamples = state->mixedAttachmentSamples; context.conservativeRasterization = state->conservativeRasterization; context.meshWave32 = state->meshWave32; context.provokingVertexLast = state->provokingVertexLast; context.graphicsPipelineLibrary = state->graphicsPipelineLibrary; context.pipelineLinkTimeOptimization = state->pipelineLinkTimeOptimization; + context.drawInputsInPlace = state->drawInputsInPlace; context.provokingVertexModePerPipeline = state->provokingVertexModePerPipeline; context.drawIndirectCount = state->drawIndirectCount; context.occlusionQueryPrecise = state->occlusionQueryPrecise; @@ -3217,21 +3841,43 @@ Graphics::Context VulkanDevice::buildContext() const { context.descriptorIndexing = state->descriptorIndexing; context.descriptorIndexingLimits = state->descriptorIndexingProperties; context.imageInt64Atomics = state->imageInt64Atomics; + context.runtimeDescriptorArray = state->runtimeDescriptorArray; + context.partiallyBound = state->descriptorBindingPartiallyBound; + context.variableDescriptorCount = state->descriptorBindingVariableDescriptorCount; + context.sampledImageUpdateAfterBind = state->sampledImageUpdateAfterBind; + context.imageTableCapacity = ImageTableCapacity(context); + context.samplerTableCapacity = SamplerTableCapacity(context); + context.imageTableUpdateAfterBind = ImageTableUpdateAfterBind(context); + context.samplerTableUpdateAfterBind = SamplerTableUpdateAfterBind(context); context.geometryShader = state->geometryShader; + context.largePoints = state->largePoints; context.sampleRateShading = state->sampleRateShading; + context.fillModeNonSolid = state->fillModeNonSolid; + context.maintenance5 = state->maintenance5; + context.storageImageMultisample = state->storageImageMultisample; + context.storageImageWriteWithoutFormat = state->storageImageWriteWithoutFormat; 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(); context.viewportIndexLayer = state->viewportIndexLayer; + context.imageFormatList = state->imageFormatList; + context.demoteToHelperInvocation = state->demoteToHelperInvocation; + context.earlyFragmentSampleMaskTestBeforeSampleCounting = state->earlyFragmentSampleMaskTestBeforeSampleCounting; context.primitiveListRestart = state->primitiveListRestart; context.imageViewMinLod = state->imageViewMinLod; context.pipelineExecutableInfo = state->pipelineExecutableInfo; context.srgbDecodeFormats = state->srgbDecodeFormats; + context.srgbCopyFormats = state->srgbCopyFormats; context.sampleLocationSampleCounts = state->sampleLocationSampleCounts; - context.memoryProperties2 = state->memoryBudget ? state->InstanceFunction("vkGetPhysicalDeviceMemoryProperties2") : nullptr; + context.sampleLocationGridSampleCounts = state->sampleLocationGridSampleCounts; + context.sampleLocationGridSizes = state->sampleLocationGridSizes; + context.shaderStencilExport = state->shaderStencilExport; + context.memoryProperties2 =state->memoryBudget ? state->InstanceFunction("vkGetPhysicalDeviceMemoryProperties2") : nullptr; context.singlePassStorage = state->storageImageWithoutFormat; + context.cacheReclaim = Graphics::ReleaseTextureCaches; return context; } @@ -3243,6 +3889,10 @@ std::optional VulkanDevice::KnownDrawRejection(const Graphics::Stat return Graphics::KnownValidationFailure(graphicsContext(), shaders, graphics); } +void VulkanDevice::DepthCopyPass(const Graphics::DepthCopyPass& pass) { + Graphics::RunDepthCopyPass(graphicsContext(), pass); +} + void VulkanDevice::ColorMetadataPass(const Graphics::ColorMetadataPass& pass) { Graphics::RunColorMetadataPass(graphicsContext(), pass); } @@ -3286,6 +3936,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); @@ -3299,6 +3950,10 @@ void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParamete if (trace) APS5_LOG_CHARS_OUT("VulkanDevice::Draw complete"); } +bool VulkanDevice::DrawInputsInPlace() const { + return state->drawInputsInPlace; +} + namespace { // The resource cache serves dispatches unless disabled; Revalidate proves a hit by texture identity, @@ -3318,11 +3973,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)); @@ -3353,6 +4021,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; @@ -3362,6 +4031,8 @@ struct PreparedDispatch { // The stage-A pre-sync (see PrepareDispatch): the newest recorder serial waited for without the // mutex, whose batches the dispatch reaps under it before stage B (0: nothing waited for). std::uint64_t presyncSerial = 0; + std::shared_ptr objects; + std::vector describedLayoutKey; }; struct RecipeHit { @@ -3383,6 +4054,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}; }; @@ -3548,6 +4221,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(); @@ -3695,15 +4425,31 @@ struct RecordedDispatch { // A template hit's revalidate time, charged to the [rescache] refresh accounting. double revalidateMs; DispatchTimer* timer; + std::optional> htileClear; // Whether the data refresh recorded anything. bool refreshed = false; bool resourceKeyMatched = false; + std::shared_ptr bindings{}; + std::uint64_t cpuArguments = 0; + int cpuReason = 0; + std::array threadGroup{}; + std::array threadLimit{}; }; 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; @@ -3765,23 +4511,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(); } @@ -3790,17 +4544,26 @@ 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; } -std::shared_ptr VulkanDevice::PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span snapshots, std::uint64_t dispatchThreads) { +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. static const bool lockedBuild = std::getenv("APS5_LOCKED_BUILD") != nullptr; @@ -3826,6 +4589,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); } @@ -3854,6 +4626,18 @@ std::shared_ptr VulkanDevice::PrepareDispatch(const ShaderReco phase(PreparedDispatch::PreparePresync); } if (profile) prepared->prepareMs = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + const auto& built = cached != nullptr ? *cached : *prepared->resources; + const std::array shaders{{compute}}; + const auto pushStages = Graphics::PushConstantStages(shaders); + if (pushStages != 0 && state->properties.limits.maxPushConstantsSize < Graphics::ComputePushConstantBytes) return prepared; + if (built.Layout() != VK_NULL_HANDLE) { + const auto layouts = built.SetLayouts(); + prepared->objects = computePipeline(context, shader, layouts.main, layouts.mainKey, pushStages, programAddress); + } else if (context.descriptorCache != nullptr) { + const auto described = Graphics::DescribeSetLayouts(context, shaders); + prepared->describedLayoutKey = described.mainKey; + prepared->objects = computePipeline(context, shader, described.layouts.main, described.mainKey, pushStages, programAddress); + } return prepared; } @@ -3902,12 +4686,12 @@ std::shared_ptr VulkanDevice::PrepareRecipe(const std::shared_ptr snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe) { - static_cast(dispatch(shader, x, y, z, 0, snapshots, programAddress, std::move(prepared), recipe)); +void VulkanDevice::Dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, std::optional> htileClear) { + static_cast(dispatch(shader, x, y, z, 0, snapshots, programAddress, std::move(prepared), recipe, {}, htileClear)); } -VulkanDevice::IndirectOutcome VulkanDevice::DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe) { - return dispatch(shader, 0, 0, 0, arguments, snapshots, programAddress, std::move(prepared), recipe); +VulkanDevice::IndirectOutcome VulkanDevice::DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, const std::array& threadGroup) { + return dispatch(shader, 0, 0, 0, arguments, snapshots, programAddress, std::move(prepared), recipe, threadGroup); } void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& outcome, char* groupsText) { @@ -3915,6 +4699,7 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out auto& timer = *record.timer; const auto& context = *record.context; const auto arguments = record.arguments; + const bool threadDimensions = record.threadGroup != std::array{}; // Decided here, after the resource build (which may retire imports and wait for recorded // work) and before the dispatch itself is recorded, so the import cannot be dropped before // the record. The build already recorded the copy-ins of its misaligned and staged regions @@ -3941,7 +4726,8 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out outcome.cpuReason = 8; } else if (labelPending() || state->CopiedWriterOverlaps(arguments, 12)) { outcome.cpuReason = 2; - } else if ((record.argumentImport = Graphics::HostImportFor(context, arguments, 12)) == nullptr) { + } else if ((record.argumentImport = Graphics::HostImportFor(context, arguments, 12)) == nullptr || (threadDimensions && record.argumentImport->address == 0)) { + record.argumentImport = nullptr; // Valid until the next refreshImports, which only runs under GuestMemory::GpuMutex (held // here, by every Dispatch caller) and Keeps a retired VkBuffer while the recorder is busy. outcome.cpuReason = 3; @@ -3954,15 +4740,20 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out } timer.phase(PhaseDecide); if (outcome.cpuReason == 0) return; - // The read waits for the producing batch through the flush hook, as the driver's resolve - // did for every indirect dispatch before; the counts then go through the direct checks. const auto readStart = std::chrono::steady_clock::now(); + Graphics::FinishLeaseWork(); std::array groups{}; GuestMemory::Read(arguments, std::as_writable_bytes(std::span(groups)), 4); + if (threadDimensions) { + record.threadLimit = groups; + for (std::size_t axis = 0; axis < groups.size(); ++axis) groups[axis] = groups[axis] / record.threadGroup[axis] + (groups[axis] % record.threadGroup[axis] != 0 ? 1u : 0u); + } outcome.argumentReadMs = std::chrono::duration(std::chrono::steady_clock::now() - readStart).count(); record.x = groups[0]; record.y = groups[1]; record.z = groups[2]; + record.cpuArguments = arguments; + record.cpuReason = outcome.cpuReason; // Timed before `arguments` is cleared, so the read counts as an indirect phase. timer.phase(PhaseArgumentRead); // From here on the phases of a CPU-resolved indirect dispatch are charged as direct ones. @@ -3979,6 +4770,14 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { auto& resources = *record.resources; const auto arguments = record.arguments; const auto* argumentImport = record.argumentImport; + const auto* limit = state->properties.limits.maxComputeWorkGroupCount; + if (argumentImport == nullptr && record.shader->program->workgroupMemoryDwords != 0 && (record.x > limit[0] || record.y > limit[1] || record.z > limit[2])) { + char counted[128] = ""; + if (record.cpuArguments != 0) std::snprintf(counted, sizeof(counted), " (read on the CPU from the indirect arguments at 0x%llx, cpuReason %d)", static_cast(record.cpuArguments), record.cpuReason); + char text[384]; + std::snprintf(text, sizeof(text), "Vulkan dispatch: workgroup count %ux%ux%u%s exceeds device limits %ux%ux%u and a split aliases the device-memory LDS slots, whose base is computed from gl_NumWorkGroups of a single chunk", record.x, record.y, record.z, counted, limit[0], limit[1], limit[2]); + throw std::runtime_error(text); + } // The record phase split (APS5_PROFILE_DRAW, rows "record: ..." of the [dispatch] totals): // opening the batch, the keeps, the data refresh, the barriers with the bind and the dispatch // itself, the pending-write notes and marks (MarkGpuWrites), and the completion registration. @@ -4009,8 +4808,13 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { const auto commands = recorder.Commands(&covered); recordStep(PhaseRecordCommands); recorder.Keep(record.objects); + if (record.resources != nullptr) record.resources->KeepStorageImageBytes(recorder); 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 @@ -4028,6 +4832,24 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { } recordStep(PhaseRecordDataRefresh); } + std::shared_ptr threadArguments; + if (record.threadGroup != std::array{}) { + const auto data = resources.shaderDataBuffer(*record.shader); + const auto limitDword = record.shader->program->imageMetadataDword; + Graphics::Require(data != VK_NULL_HANDLE && limitDword >= 3, "Vulkan dispatch: a thread-dimension dispatch has no ShaderData thread limit"); + const VkDeviceSize limitByte = static_cast(limitDword - 3) * sizeof(std::uint32_t); + Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + if (argumentImport != nullptr) { + threadArguments = std::make_shared(context, 24, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + Graphics::Require(recorder.RecordDispatchArguments(commands, argumentImport->address + (arguments - argumentImport->base), threadArguments->DeviceAddress(), &record.threadGroup), "Vulkan dispatch: thread-dimension indirect arguments need the argument kernel"); + const VkBufferCopy limit{12, limitByte, 12}; + context.Function("vkCmdCopyBuffer")(commands, threadArguments->Handle(), data, 1, &limit); + } else { + context.Resolved(&Graphics::DeviceFunctions::cmdUpdateBuffer, "vkCmdUpdateBuffer")(commands, data, limitByte, sizeof(record.threadLimit), record.threadLimit.data()); + } + resources.forgetShaderDataWords(*record.shader); + covered = 0; + } using CommandClass = Graphics::Recorder::CommandClass; if (Graphics::Recorder::BarrierValidate()) { if (argumentImport != nullptr) { @@ -4059,8 +4881,24 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { recorder.EndGpuTiming(timing); ++d.preBarriersRecorded; } + VkBuffer indirectBuffer = argumentImport != nullptr ? argumentImport->buffer : VK_NULL_HANDLE; + VkDeviceSize indirectOffset = argumentImport != nullptr ? Graphics::HostImportOffset(*argumentImport, arguments) : 0; + if (threadArguments != nullptr) { + indirectBuffer = threadArguments->Handle(); + indirectOffset = 0; + recorder.Keep(std::move(threadArguments)); + } else if (argumentImport != nullptr && argumentImport->address != 0) { + auto checked = std::make_shared(context, 12, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + if (recorder.RecordDispatchArguments(commands, argumentImport->address + (arguments - argumentImport->base), checked->DeviceAddress())) { + indirectBuffer = checked->Handle(); + indirectOffset = 0; + recorder.Keep(std::move(checked)); + } + } 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); @@ -4070,31 +4908,33 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { context.Resolved(&Graphics::DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, record.objects->layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes, record.pushBytes->data()); } 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, indirectBuffer, indirectOffset); } else { - const auto* limit = state->properties.limits.maxComputeWorkGroupCount; if (record.x <= limit[0] && record.y <= limit[1] && record.z <= limit[2]) { context.Resolved(&Graphics::DeviceFunctions::cmdDispatch, "vkCmdDispatch")(commands, record.x, record.y, record.z); } else { - if (record.shader->program->workgroupMemoryDwords != 0) { - char text[256]; - std::snprintf(text, sizeof(text), "Vulkan dispatch: workgroup count %ux%ux%u exceeds device limits %ux%ux%u and a split aliases the device-memory LDS slots, whose base is computed from gl_NumWorkGroups of a single chunk", record.x, record.y, record.z, limit[0], limit[1], limit[2]); + char counted[128] = ""; + if (record.cpuArguments != 0) std::snprintf(counted, sizeof(counted), " (read on the CPU from the indirect arguments at 0x%llx, cpuReason %d)", static_cast(record.cpuArguments), record.cpuReason); + const auto offsets = std::ranges::count_if(record.shader->program->specialization, [](const auto& constant) { return ShaderRecompiler::PipelineSpecialization::IsWorkgroupOffset(constant.id); }); + if (offsets != ShaderRecompiler::PipelineSpecialization::WorkgroupOffsetWords) { + char text[320]; + std::snprintf(text, sizeof(text), "Vulkan dispatch: workgroup count %ux%ux%u%s exceeds device limits %ux%ux%u and the shader takes no workgroup offset", record.x, record.y, record.z, counted, limit[0], limit[1], limit[2]); throw std::runtime_error(text); } - if (state->properties.vendorID == 0x10de) { - char text[192]; - std::snprintf(text, sizeof(text), "Vulkan dispatch: workgroup count %ux%ux%u exceeds device limits %ux%ux%u and NVIDIA drops workgroups of a vkCmdDispatchBase split", record.x, record.y, record.z, limit[0], limit[1], limit[2]); - throw std::runtime_error(text); - } - const auto dispatchBase = context.Function("vkCmdDispatchBase"); + const auto bindPipeline = context.Resolved(&Graphics::DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline"); + const auto dispatchChunk = context.Resolved(&Graphics::DeviceFunctions::cmdDispatch, "vkCmdDispatch"); bool firstChunk = true; for (std::uint32_t zBase = 0; zBase < record.z; zBase += limit[2]) { for (std::uint32_t yBase = 0; yBase < record.y; yBase += limit[1]) { for (std::uint32_t xBase = 0; xBase < record.x; xBase += limit[0]) { - if (!firstChunk) Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + if (!firstChunk) { + Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + bindPipeline(commands, VK_PIPELINE_BIND_POINT_COMPUTE, splitComputePipeline(context, *record.shader->program, *record.objects, {xBase, yBase, zBase})); + } firstChunk = false; - dispatchBase(commands, xBase, yBase, zBase, std::min(limit[0], record.x - xBase), std::min(limit[1], record.y - yBase), std::min(limit[2], record.z - zBase)); + dispatchChunk(commands, std::min(limit[0], record.x - xBase), std::min(limit[1], record.y - yBase), std::min(limit[2], record.z - zBase)); } } } @@ -4108,21 +4948,31 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { recorder.EndGpuTiming(trailingTiming); recorder.MarkCovered(dispatchedAccess); recordStep(PhaseRecordBind); + if (record.htileClear.has_value()) Graphics::ArmHtileDepthClear(record.htileClear->first, record.htileClear->second); resources.MarkGpuWrites(recorder); + if (record.htileClear.has_value()) { + Graphics::QueueHtileDepthClear(record.htileClear->first, record.htileClear->second); + recorder.NotePendingWrite(record.htileClear->first, record.htileClear->second); + } recordStep(PhaseRecordMarks); // Only copied written buffers (and BDA fault checks) need work once the GPU is done; without them // the batch can signal its labels from the GPU. - if (resources.NeedsCompletion() && resources.Reusable()) { + const bool completionNeeded = resources.NeedsCompletion() || record.htileClear.has_value(); + if (completionNeeded && resources.Reusable()) { auto kept = record.resources; - recorder.OnComplete([kept] { kept->CheckFaults(); }); - } else if (resources.NeedsCompletion()) { + recorder.OnComplete([kept, htileClear = record.htileClear] { + kept->CheckFaults(); + if (htileClear.has_value()) Graphics::CompleteHtileDepthClear(htileClear->first, htileClear->second); + }); + } else if (completionNeeded) { // Listed for DispatchIndirect until the write-back ran (a non-reusable object, so once). auto writers = state->copiedWriters; auto kept = record.resources; - recorder.OnComplete([kept, writers] { + recorder.OnComplete([kept, writers, htileClear = record.htileClear] { // Delisted before the write-back: one that fails must not keep indirect dispatches on the CPU. writers->erase(std::remove(writers->begin(), writers->end(), kept), writers->end()); kept->WriteBackBuffers(); + if (htileClear.has_value()) Graphics::CompleteHtileDepthClear(htileClear->first, htileClear->second); }); // Listed after the registration: a throw there leaves nothing that would pin the CPU path forever. if (resources.HasCopiedWrites()) writers->push_back(kept); @@ -4130,7 +4980,142 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { recordStep(PhaseRecordCompletion); } -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) { +// 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) { + PerformanceTimer timing("Vulkan.ComputePipeline"); + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + const auto start = std::chrono::steady_clock::now(); + auto objects = std::make_shared(); + objects->device = state->device; + objects->destroyModule = state->DeviceFunction("vkDestroyShaderModule"); + objects->destroyLayout = state->DeviceFunction("vkDestroyPipelineLayout"); + objects->destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); + objects->module = Graphics::CreateShaderModule(state->DeviceFunction("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, shader.spirv.data(), shader.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule"); + objects->resourcesLayout = setLayout; + const std::array setLayouts{setLayout, shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes}; + 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' && ComputePipelineCapture::OptionalEnabled() && 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.WriteOptional(directory); + if (!path.empty()) 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; + if (!state->probingFloatControls) { + 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; +} + +VkPipeline VulkanDevice::splitComputePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, ComputePipelineObjects& objects, const std::array& base) { + std::lock_guard lock(objects.splitMutex); + if (const auto found = objects.splitPipelines.find(base); found != objects.splitPipelines.end()) return found->second; + std::array offsets{}; + for (std::uint32_t component = 0; component < offsets.size(); ++component) offsets[component] = {ShaderRecompiler::PipelineSpecialization::WorkgroupOffsetBase + component, base[component]}; + const Graphics::PipelineSpecialization specialization(shader, offsets); + VkShaderModule module = Graphics::CreateShaderModule(state->DeviceFunction("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, shader.spirv.data(), shader.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule"); + VkPipeline pipeline = VK_NULL_HANDLE; + try { + CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, module, objects.layout, computePipelineFlags(), computeStageFlags(shader), specialization.Info(), computeSubgroupSize(shader), &pipeline, context.pipelineCacheSynchronization); + } catch (...) { + objects.destroyModule(objects.device, module, nullptr); + throw; + } + objects.destroyModule(objects.device, module, nullptr); + 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); + objects.splitPipelines.emplace(base, pipeline); + return pipeline; +} + +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.identity.Empty()) { + std::shared_future> other; + { + std::lock_guard pipelines(state->computePipelinesMutex); + if (const auto found = state->computePipelines.find(pipelineKey); found != state->computePipelines.end()) return found->second; + if (const auto pending = state->computePipelinesBuilding.find(pipelineKey); pending != state->computePipelinesBuilding.end()) other = pending->second; + else state->computePipelinesBuilding.emplace(pipelineKey, building.get_future().share()); + } + if (other.valid()) return other.get(); + } + const auto finish = [&](std::shared_ptr built, std::exception_ptr error) { + if (pipelineKey.identity.Empty()) return built; + { + std::lock_guard pipelines(state->computePipelinesMutex); + if (built != nullptr) built = state->computePipelines.try_emplace(pipelineKey, built).first->second; + state->computePipelinesBuilding.erase(pipelineKey); + } + if (error != nullptr) building.set_exception(error); + else building.set_value(built); + return built; + }; + std::shared_ptr objects; + try { + objects = createComputePipeline(context, shader, setLayout, layoutKey, pushStages, programAddress); + if (Graphics::PipelineJournalEnabled() && !state->probingFloatControls) Graphics::RecordComputePipeline(shader, pushStages != 0); + } catch (...) { + finish(nullptr, std::current_exception()); + throw; + } + return finish(std::move(objects), nullptr); +} + +VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipeOut, const std::array& threadGroup, std::optional> htileClear) { + startJournalReplay(); + for (const auto& binding : shader.bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; + for (std::size_t at = 0; at + 4u <= binding.guestDescriptor.size(); at += 4u) { + const auto* words = binding.guestDescriptor.data() + at; + const auto base = static_cast(words[0]) | (static_cast(words[1] & 0xffffu) << 32u); + const auto stride = (words[1] >> 16u) & 0x3fffu; + const auto records = static_cast(words[2]); + const auto bytes = stride != 0 ? records * stride : records; + if (base != 0 && bytes != 0) static_cast(Graphics::FlushDepthPlanes(base, static_cast(bytes))); + } + } state->recorder->BoundKeptBytes(); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); PerformanceTimer timing("Vulkan.Dispatch"); @@ -4150,11 +5135,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; - std::shared_ptr objects; - if (pipelineKey != 0) { + const auto pipelineKey = ComputePipelineKey::Of(shader, pushStages != 0); + std::shared_ptr objects = prepared != nullptr ? prepared->objects : nullptr; + 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; } @@ -4172,7 +5155,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. @@ -4192,6 +5177,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 = [&] { @@ -4202,7 +5192,10 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec static const bool deferEviction = std::getenv("APS5_NO_DEFERRED_EVICTION") == nullptr; std::vector> evicted; state->resourceCache.Insert(contentKey, resources, deferEviction ? &evicted : nullptr); - for (auto& object : evicted) state->recorder->Keep(std::move(object)); + for (auto& object : evicted) { + object->KeepStorageImageBytes(*state->recorder); + state->recorder->Keep(std::move(object)); + } if (profile) { timer.add(PhaseResourcesInsert, std::chrono::duration(std::chrono::steady_clock::now() - insertStart).count()); if (arguments != 0) d.indirectHold.phaseCounts[PhaseResourcesInsert] += evicted.size(); @@ -4217,6 +5210,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec const auto before = profile ? resources->Timing() : Graphics::ShaderResources::BuildTiming{}; const auto completeStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; resources->Complete(); + require(prepared->describedLayoutKey.empty() || std::ranges::equal(resources->SetLayouts().mainKey, prepared->describedLayoutKey), "the set layout described for the dispatch's pipeline differs from its build"); if (profile) { const auto completeWall = std::chrono::duration(std::chrono::steady_clock::now() - completeStart).count(); timer.add(PhaseResourcesComplete, completeWall); @@ -4239,6 +5233,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) { @@ -4246,17 +5241,63 @@ 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() && threadGroup != std::array{}) { + const auto data = cached->shaderDataAllocation(shaders[0]); + if (data.has_value() && std::ranges::any_of(*moved, [&](const Graphics::ShaderResources::MovedBuffer& entry) { return entry.allocation == *data; })) moved.reset(); + } + if (moved.has_value() && !moved->empty()) { + overlay = cached->PrepareDrawBindings(*state->recorder, *moved, true); + if (overlay != nullptr && overlay->allocation.set == VK_NULL_HANDLE) overlay.reset(); + 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; // Off the mutex with its batch, as an evicted entry (insert()). + cached->KeepStorageImageBytes(*state->recorder); state->recorder->Keep(std::move(cached)); } if (profile) { @@ -4271,6 +5312,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(); } } @@ -4290,82 +5332,33 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec } if (profile && std::chrono::steady_clock::now() - d.cacheReport > std::chrono::seconds(10)) { d.cacheReport = std::chrono::steady_clock::now(); - 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\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)); + AgcDriver::ProfilePrint_nid_no_patch("[rescache] (cumulative): %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 (cumulative): %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) { - 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"); - timing.Mark("validate_shader_module"); - const std::array setLayouts{resources->Layout(), 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; - resourcesLayout.flags = resources->LayoutUpdateAfterBind() ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; - const auto layoutBindings = resources->LayoutBindings(); - const auto bindingFlags = resources->LayoutBindingFlags(); - require(layoutBindings.size() == bindingFlags.size(), "compute capture binding flag count differs from layout"); - for (std::size_t index = 0; index < layoutBindings.size(); ++index) { - const auto& binding = layoutBindings[index]; - require(binding.pImmutableSamplers == nullptr, "compute capture does not support immutable samplers"); - resourcesLayout.bindings.push_back({binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags, bindingFlags[index]}); - } - 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 (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"); } - 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; - Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); + const auto layouts = resources->SetLayouts(); + objects = computePipeline(context, shader, layouts.main, layouts.mainKey, pushStages, programAddress); timing.Mark("pipeline_create"); - if (pipelineKey != 0) { - // Find-or-insert: objects another dispatch of the variant mapped meanwhile serve this - // one too (this call's go with the batch that keeps them). - std::lock_guard pipelines(state->computePipelinesMutex); - objects = state->computePipelines.try_emplace(pipelineKey, objects).first->second; - } timer.phase(PhasePipeline); } // Recorded into the device's open batch: the CPU moves on to the next command while the GPU // works; the batch is waited for where the guest expects results (labels, flips, CPU reads). auto& recorder = *state->recorder; 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}; + 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, htileClear}; record.resourceKeyMatched = fromCache; + record.bindings = overlay; + record.threadGroup = threadGroup; if (arguments != 0) decideIndirect(record, outcome, groupsText); // The indirect hold total below still covers the whole call of a CPU-resolved one. const bool indirect = record.arguments != 0 || outcome.cpuReason != 0; @@ -4419,7 +5412,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; @@ -4440,7 +5433,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec return outcome; } -RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr& hit, IndirectOutcome& outcome, const std::shared_ptr& verify, bool refreshByWords) { +RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr& hit, IndirectOutcome& outcome, const std::shared_ptr& verify, bool refreshByWords, const std::array& threadGroup) { state->recorder->BoundKeptBytes(); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); PerformanceTimer timing("Vulkan.DispatchRecipe"); @@ -4484,11 +5477,12 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu // ordinary path runs with the prepared find (C10: the template goes to the batch). if (verify->cached.get() != hit->resources.get()) { counters.verifyReplaced.fetch_add(1, std::memory_order_relaxed); + hit->resources->KeepStorageImageBytes(*state->recorder); state->recorder->Keep(std::move(hit->resources)); 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; @@ -4520,6 +5514,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu state->resourceCache.Remove(recipe.key, hit->resources.get()); ++d.cacheInvalidated; // Off the mutex with its batch, as an evicted entry. + hit->resources->KeepStorageImageBytes(*state->recorder); state->recorder->Keep(std::move(hit->resources)); counters.rebuilds[static_cast(report.failure)].fetch_add(1, std::memory_order_relaxed); return RecipeOutcome::Rebuild; @@ -4534,6 +5529,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu if (dataRefresh == RecordedDispatch::DataRefresh::Words) counters.refreshByWords.fetch_add(1, std::memory_order_relaxed); RecordedDispatch record{&context, &shaders[0], hit->resources, hit->objects, recipe.pushes ? VkShaderStageFlags{VK_SHADER_STAGE_COMPUTE_BIT} : VkShaderStageFlags{0}, &recipe.pushBytes, x, y, z, arguments, nullptr, programAddress, dataRefresh, recipe.dataWordsHash, 0, &timer}; record.resourceKeyMatched = verify != nullptr; + record.threadGroup = threadGroup; if (arguments != 0) decideIndirect(record, outcome, groupsText); const bool indirect = record.arguments != 0 || outcome.cpuReason != 0; const auto recordStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp index 8b3ad92f49..7bbca06017 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp @@ -27,6 +27,7 @@ namespace { std::jthread processSamplerThread; std::jthread workerSamplerThread; +std::jthread committerSamplerThread; // Debug aid: APS5_SAMPLE_WORKER= samples the calling thread every millisecond and, every 20 s, // writes "module+offset self inclusive" lines to for offline symbolization with nm. @@ -285,11 +286,7 @@ void StartProcessSampler() { }); } -} - -void StartWorkerSampler() { - StartProcessSampler(); - const char* path = std::getenv("APS5_SAMPLE_WORKER"); +void StartCallingThreadSampler(const char* path, std::jthread& slot) { if (path == nullptr) return; auto sampler = std::make_unique(); sampler->path = path; @@ -302,7 +299,7 @@ void StartWorkerSampler() { sampler->stackHigh = reinterpret_cast(block->StackBase); } if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &sampler->target, THREAD_ALL_ACCESS, FALSE, 0)) throw std::system_error(GetLastError(), std::system_category(), "Duplicating sampler thread handle"); - workerSamplerThread = std::jthread([sampler = std::move(sampler)](std::stop_token token) { + slot = std::jthread([sampler = std::move(sampler)](std::stop_token token) { auto flushed = std::chrono::steady_clock::now(); while (!token.stop_requested()) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); @@ -316,14 +313,28 @@ void StartWorkerSampler() { }); } +} + +void StartWorkerSampler() { + StartProcessSampler(); + StartCallingThreadSampler(std::getenv("APS5_SAMPLE_WORKER"), workerSamplerThread); +} + +void StartCommitterSampler() { + StartCallingThreadSampler(std::getenv("APS5_SAMPLE_COMMITTER"), committerSamplerThread); +} + void StopWorkerSampler() { processSamplerThread.request_stop(); workerSamplerThread.request_stop(); + committerSamplerThread.request_stop(); if (processSamplerThread.joinable()) processSamplerThread.join(); if (workerSamplerThread.joinable()) workerSamplerThread.join(); + if (committerSamplerThread.joinable()) committerSamplerThread.join(); } #else void StartWorkerSampler() {} +void StartCommitterSampler() {} void StopWorkerSampler() {} #endif diff --git a/core/libs/prx/libSceAgcDriver/Export.cpp b/core/libs/prx/libSceAgcDriver/Export.cpp new file mode 100644 index 0000000000..18cd1da5d8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Export.cpp @@ -0,0 +1,183 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAgcDriverAcquireRazorACQ(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverCreateQueue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverCwsrResumeAcq(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverCwsrSuspendAcq(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverDebugHardwareStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverDestroyQueue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverGetPaDebugInterfaceVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverGetRegShadowInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverGetRegShadowInfoAgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverGetReservedDmemForAgc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverGetWorkloadStreamInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverIDHSSubmit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverModuleRegistration(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverNotifyDefaultStates(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverPassInfoDownward(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverPatchClearState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetFlip(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetHsOffchipParamDirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetWorkloadComplete(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetWorkloadsActive(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetupAsyncGraphics(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetupRegisterShadow(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSubmitMultiCommandBuffersDirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSubmitToHDRScopesACQ(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSubmitToRazorACQ(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSuspendPointSubmit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSuspendPointSubmitDirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSysEnableSubmitDone45Exception(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSysGetClientNumber(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSysIsGameClosed(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverTmpInitIdhs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverUserDataWritePacket(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverUserDataWritePopMarker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverUserDataWritePushMarker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverUserDataWriteSetMarker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BarrierBatch.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BarrierBatch.hpp new file mode 100644 index 0000000000..3cd1257efd --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BarrierBatch.hpp @@ -0,0 +1,25 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_BARRIERBATCH_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_BARRIERBATCH_HPP + +#ifndef VK_NO_PROTOTYPES +#define VK_NO_PROTOTYPES +#endif +#include + +namespace AgcDriver::Graphics { + +struct BarrierMask { + VkPipelineStageFlags srcStage = 0; + VkPipelineStageFlags dstStage = 0; + VkAccessFlags srcAccess = 0; + VkAccessFlags dstAccess = 0; + bool changesLayout = false; + bool transfersQueue = false; +}; + +bool Covers(const BarrierMask& outer, const BarrierMask& inner); +bool OrderedByTrailing(VkAccessFlags covered, const BarrierMask& wanted); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp index 301201afc9..580236b120 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp @@ -2,6 +2,10 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_BDARESOURCES_HPP #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "Recompiler.hpp" +#include +#include +#include namespace AgcDriver::Graphics { @@ -14,6 +18,18 @@ public: VkDescriptorBufferInfo Table() const; VkDescriptorBufferInfo Fault() const; void CheckFault() const; + struct ImageTableFaults { + std::uint32_t stage = 0; + std::uint64_t shader = 0; + std::vector poison; + std::vector messages; + }; + void SetImageTables(std::vector tables, std::vector> ranges); + struct SrtGuardFaults { + std::uint32_t stage = 0; + std::vector flattenedSrt; + }; + void SetSrtGuards(std::vector guards); // APS5_PROFILE_DRAW: page tables served from the per-device cache and built anew (cumulative), // and how many recent tables the cache holds right now. struct TableCacheStats { @@ -41,9 +57,13 @@ private: // same device addresses share one buffer while one of them is alive (see the table cache in // BdaResources.cpp, which refers to it weakly). void markWrittenPages() const; + void checkSrtGuard() const; std::shared_ptr table; std::unique_ptr fault; std::size_t tableBytes = 0; + std::vector imageTables; + std::vector> imageTableRanges; + std::vector srtGuards; }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index de95acd54e..657c5351c4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -2,8 +2,10 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_BUFFERPOOL_HPP #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include #include #include +#include #include #include #include @@ -24,6 +26,7 @@ struct BufferAllocation { VkDeviceSize offset = 0; bool slab = false; std::size_t bufferBytes = 0; + std::uint32_t memoryType = 0; }; struct SlabSlot { @@ -54,14 +57,19 @@ 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; // The size a buffer for `bytes` is created with: its size class, or `bytes` itself when large. static std::size_t Capacity(std::size_t bytes, VkMemoryPropertyFlags properties); static VkBufferUsageFlags Usage(VkBufferUsageFlags usage, VkMemoryPropertyFlags properties); - VkDeviceMemory AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation); + VkDeviceMemory AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType = std::nullopt, std::uint32_t* allocatedType = nullptr); + static VkDeviceSize ImageBudget(VkDeviceSize deviceBudget); + VkDeviceMemory TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType = std::nullopt, std::uint32_t* allocatedType = nullptr); + void PutImageMemory(VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept; + std::size_t RetainedImages(); std::optional Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); void Put(const BufferAllocation& allocation) noexcept; static bool SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom); @@ -81,6 +89,7 @@ private: }; struct Slab { std::vector available; + std::size_t blocks = 0; std::size_t emptyBlocks = 0; }; static std::uint64_t SlabKey(std::uint32_t memoryType, std::size_t capacity, bool addressable); @@ -88,16 +97,18 @@ private: struct Slot { BufferAllocation allocation; std::uint64_t lastUse; + std::list::iterator order; }; struct SlotKey { std::size_t bytes; VkBufferUsageFlags usage; VkMemoryPropertyFlags properties; + std::uint32_t memoryType = 0; bool operator==(const SlotKey&) const = default; }; struct SlotKeyHash { std::size_t operator()(const SlotKey& key) const noexcept { - return std::hash{}(key.bytes) ^ (static_cast(key.usage) << 32u) ^ (static_cast(key.properties) << 48u); + return std::hash{}(key.bytes) ^ (static_cast(key.usage) << 32u) ^ (static_cast(key.properties) << 48u) ^ (static_cast(key.memoryType) << 56u); } }; // One retention tier: its slots by key (each list oldest first), their count and bytes, the @@ -116,6 +127,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; @@ -124,20 +136,36 @@ private: // caller destroys those after releasing the mutex, so builds taking buffers on other threads // do not wait behind the Vulkan destroy calls. Nothing changes when the vector cannot grow. void evictOldest(Tier& tier, std::vector& evicted); + bool evictOldestImage(std::vector& evicted); + bool imagePooled(VkDeviceSize bytes, std::uint32_t memoryType) const; + bool heapOverBudget(); // The retained-slot bound of each tier (APS5_BUFFER_POOL_SLOTS, default `defaultSlots`), read once. static std::size_t MaxSlots(); std::size_t maxSlots; + VkDeviceSize (*cacheReclaim)(VkDeviceSize bytes); VkDevice device; PFN_vkUnmapMemory unmap; PFN_vkDestroyBuffer destroyBuffer; PFN_vkFreeMemory freeMemory; PFN_vkAllocateMemory allocateMemory; PFN_vkMapMemory mapMemory; + VkPhysicalDevice physical = VK_NULL_HANDLE; + VkPhysicalDeviceMemoryProperties memoryProperties{}; + PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2 = nullptr; std::mutex mutex; // Not `small`/`large`: (via ) defines `small` as a macro. Tier smallTier; Tier largeTier; Tier deviceTier; + std::chrono::steady_clock::time_point heapRead{}; + bool heapOver = false; + VkDeviceSize imageBudget = 0; + VkDeviceSize imageBytes = 0; + std::size_t imageSlots = 0; + std::uint64_t imageHits = 0; + std::uint64_t imageMisses = 0; + std::uint64_t imageEvictions = 0; + std::list imageOrder; std::uint64_t clock = 0; std::mutex slabMutex; std::unordered_map slabs; @@ -153,6 +181,8 @@ private: }; std::shared_ptr GetBufferPool(const Context& context); +VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType = nullptr); +void ReleaseImageMemory(const Context& context, VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 5f907e3404..e1b6966086 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -6,11 +6,14 @@ #endif #include #include +#include #include #include #include +#include #include #include +#include #include #include #include @@ -18,6 +21,7 @@ namespace AgcDriver::Graphics { + class TextureDetiler; class GpuColorTransfer; class BufferPool; @@ -30,6 +34,7 @@ class DescriptorCache; class SamplerCache; class ShaderResources; class PlaceholderImages; +class VertexInputCache; class PipelineCacheSynchronization { public: @@ -48,14 +53,50 @@ inline void Require(bool condition, const std::string& reason) { if (!condition) throw std::runtime_error("AGC graphics: " + reason); } +inline std::function& DeviceLostHook() { + static std::function hook; + return hook; +} + +inline const void*& DeviceLostOwner() { + static const void* owner = nullptr; + return owner; +} + +inline void ReportDeviceLost() { + static std::atomic reported{false}; + if (DeviceLostHook() && !reported.exchange(true)) DeviceLostHook()(); +} + inline void Check(VkResult result, const char* operation) { + if (result == VK_ERROR_DEVICE_LOST) ReportDeviceLost(); if (result != VK_SUCCESS) throw std::runtime_error(std::string("AGC graphics: ") + operation + ": Vulkan result " + std::to_string(result)); } +inline VkShaderModule CreateShaderModule(PFN_vkCreateShaderModule create, VkDevice device, bool graphicsPipelineLibrary, const std::uint32_t* code, std::size_t bytes, const char* operation) { + std::vector keyed(code, code + bytes / sizeof(std::uint32_t)); + if (!graphicsPipelineLibrary && keyed.size() > 2) keyed[2] ^= 0x8000u; + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = keyed.size() * sizeof(std::uint32_t); + info.pCode = keyed.data(); + VkShaderModule module = VK_NULL_HANDLE; + Check(create(device, &info, nullptr, &module), operation); + return module; +} + inline void Require(bool condition, const char* reason) { if (!condition) throw std::runtime_error(std::string("AGC graphics: ") + reason); } +struct DescriptorRejected : std::runtime_error { + using std::runtime_error::runtime_error; +}; + +struct BdaUnalignedAccess : std::runtime_error { + BdaUnalignedAccess(const std::string& message, std::uint32_t instruction) : std::runtime_error(message), instruction(instruction) {} + std::uint32_t instruction; +}; + // Device entry points resolved once per device (VulkanDevice's State fills it after setup): the // loader's vkGetDeviceProcAddr is a name lookup under its global mutex per call, paid at every // record site otherwise. Null members (tests, APS5_NO_PROC_TABLE=1) resolve per call (Resolved). @@ -138,13 +179,19 @@ struct Context { bool depthClamp = false; // Nonzero when VK_EXT_external_memory_host is enabled: the required host pointer alignment. VkDeviceSize hostImportAlignment = 0; + bool drawInputsInPlace = true; bool dmaBufImport = false; bool sparseBinding = false; std::uint32_t queueFamily = 0; std::uint32_t sparseFamily = VK_QUEUE_FAMILY_IGNORED; VkQueue sparseQueue = VK_NULL_HANDLE; std::mutex* sparseQueueMutex = nullptr; + VkQueue settleQueue = VK_NULL_HANDLE; + VkCommandPool settlePool = VK_NULL_HANDLE; + 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; @@ -165,6 +212,9 @@ struct Context { bool dualSrcBlend = false; bool samplerFilterMinmax = false; bool nonSeamlessCubeMap = false; + VkSampleCountFlags mergedCoverageSampleCounts = 0; + VkSampleCountFlags truncatedCoverageSampleCounts = 0; + bool mixedAttachmentSamples = false; bool conservativeRasterization = false; bool meshWave32 = false; VkBuffer emptyBuffer = VK_NULL_HANDLE; @@ -179,29 +229,52 @@ struct Context { bool descriptorIndexing = false; VkPhysicalDeviceDescriptorIndexingPropertiesEXT descriptorIndexingLimits{}; bool imageInt64Atomics = false; + bool runtimeDescriptorArray = false; + bool partiallyBound = false; + bool variableDescriptorCount = false; + bool sampledImageUpdateAfterBind = false; + std::uint32_t imageTableCapacity = 0; + std::uint32_t samplerTableCapacity = 0; + bool individuallyFreeDescriptorSets = false; + bool imageTableUpdateAfterBind = false; + bool samplerTableUpdateAfterBind = false; bool geometryShader = false; + bool largePoints = false; bool sampleRateShading = false; + bool fillModeNonSolid = false; + bool maintenance5 = false; + bool storageImageMultisample = false; + bool storageImageWriteWithoutFormat = false; + bool imageFormatList = false; bool shaderClipDistance = false; bool shaderCullDistance = false; bool nullDescriptors = false; bool bufferInt64Atomics = false; const PlaceholderImages* placeholderImages = nullptr; bool viewportIndexLayer = false; + bool demoteToHelperInvocation = false; + bool earlyFragmentSampleMaskTestBeforeSampleCounting = false; bool primitiveListRestart = false; bool imageViewMinLod = false; bool pipelineExecutableInfo = false; bool graphicsPipelineLibrary = false; bool pipelineLinkTimeOptimization = true; std::uint32_t srgbDecodeFormats = 0; + std::uint32_t srgbCopyFormats = 0; bool provokingVertexLast = false; bool provokingVertexModePerPipeline = false; bool singlePassStorage = false; VkSampleCountFlags sampleLocationSampleCounts = 0; + VkSampleCountFlags sampleLocationGridSampleCounts = 0; + std::array sampleLocationGridSizes{}; + bool shaderStencilExport = false; PipelineCacheSynchronization* pipelineCacheSynchronization = nullptr; + bool shaderTerminateInvocation = false; PipelineCacheSynchronization::PipelineUseLock LockPipelineCacheForUse() const { return pipelineCacheSynchronization != nullptr ? pipelineCacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{}; } + VertexInputCache* vertexInputs = nullptr; template TFunction Function(const char* name) const { @@ -264,23 +337,43 @@ struct Context { const auto budget = reported.heapBudget[heap]; const auto usage = reported.heapUsage[heap]; if (budget == 0 || (usage < budget && bytes <= budget - usage)) return type; + const auto host = HostMemoryType(mask); + if (!host) return type; + static std::atomic placements{0}, placedBytes{0}; + const auto count = placements.fetch_add(1) + 1; + const auto total = placedBytes.fetch_add(bytes) + bytes; + if (count <= 5 || (count & (count - 1)) == 0) std::fprintf(stderr, "[gpu] %s: device-local heap %u uses %llu of its %llu MiB budget, %llu bytes placed in host memory type %u; %llu such allocations, %llu MiB\n", what, heap, static_cast(usage >> 20u), static_cast(budget >> 20u), static_cast(bytes), *host, static_cast(count), static_cast(total >> 20u)); + return *host; + } + + std::optional HostMemoryType(std::uint32_t mask) const { for (std::uint32_t i = 0; i < memory.memoryTypeCount; ++i) { - if ((mask & (1u << i)) == 0 || (memory.memoryHeaps[memory.memoryTypes[i].heapIndex].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0) continue; - static std::atomic placements{0}, placedBytes{0}; - const auto count = placements.fetch_add(1) + 1; - const auto total = placedBytes.fetch_add(bytes) + bytes; - if (count <= 5 || (count & (count - 1)) == 0) std::fprintf(stderr, "[gpu] %s: device-local heap %u uses %llu of its %llu MiB budget, %llu bytes placed in host memory type %u; %llu such allocations, %llu MiB\n", what, heap, static_cast(usage >> 20u), static_cast(budget >> 20u), static_cast(bytes), i, static_cast(count), static_cast(total >> 20u)); - return i; + if ((mask & (1u << i)) != 0 && (memory.memoryHeaps[memory.memoryTypes[i].heapIndex].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0) return i; } - return type; + return std::nullopt; + } + + std::optional RefusedDeviceMemoryFallback(std::uint32_t mask, std::uint32_t refused) const { + if (refused >= memory.memoryTypeCount || (memory.memoryHeaps[memory.memoryTypes[refused].heapIndex].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0) return std::nullopt; + return HostMemoryType(mask); } }; +inline VkShaderModule CreateShaderModule(const Context& context, const std::uint32_t* code, std::size_t bytes, const char* operation) { + return CreateShaderModule(context.Function("vkCreateShaderModule"), context.device, context.graphicsPipelineLibrary, code, bytes, operation); +} + inline void EndRenderPass(const Context& context, VkCommandBuffer commands) { if (context.graphicsPipelineLibrary) context.Resolved(&DeviceFunctions::cmdEndRendering, "vkCmdEndRenderingKHR")(commands); else context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands); } +std::uint32_t ImageTableCapacity(const Context& context); +std::uint32_t StorageTableCapacity(const Context& context); +std::uint32_t SamplerTableCapacity(const Context& context); +bool ImageTableUpdateAfterBind(const Context& context); +bool SamplerTableUpdateAfterBind(const Context& context); + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp index 87256bd957..7f05d121a2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp @@ -36,8 +36,11 @@ bool KeysServeSurface(std::uint64_t followedDcc, DccKeys uploaded, DccKeys fille // The keys covering a surface of `surfaceBytes`, when they all agree. DccKeys ReadDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes); bool IsDccClear(DccKeys keys); +std::optional LandedUniformMetadataByte(std::uint64_t address, std::size_t bytes); DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes); DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount); +std::optional KnownPendingDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes); +std::optional CurrentUniformMetadataByte(std::uint64_t address, std::size_t bytes); // Stores "uncompressed" keys over the surface's metadata on the CPU (a guest memory write: it waits // for recorded GPU work that writes the keys first). void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes); @@ -98,6 +101,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 1c2fac1281..835b80f59b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -2,26 +2,63 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DEPTHSURFACE_HPP #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include #include #include #include #include +#include +#include namespace AgcDriver::Graphics { class Texture; class StorageTexture; -VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); +VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten = true); +SampleOrderTarget DepthSurfaceOrderTarget(const Context& context, const DepthTarget& target); +DepthSampleLocations DepthSurfaceSampleLocations(const Context& context, const DepthTarget& target); +enum class DepthPlaneScope { Overlapped, Whole }; +std::vector> FlushDepthPlanes(std::uint64_t address, std::size_t bytes, DepthPlaneScope scope = DepthPlaneScope::Overlapped); +bool DepthPlanesStale(); +struct DepthPlaneStoreStatistics { + std::uint64_t stores = 0; + std::uint64_t units = 0; + std::uint64_t bytes = 0; +}; +DepthPlaneStoreStatistics DepthPlaneStoreCounts(); +void NoteDepthSurfaceWrite(const Context& context, const DepthTarget& target); bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples); +std::uint64_t DepthSurfacesSerial(); +std::uint64_t DepthSurfacesReleased(); std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel); +std::uint64_t MultisampledDepthBytes_nid_no_patch(VkExtent2D extent, std::uint32_t bytesPerTexel, std::uint32_t samples); +void RequireNoResidentDepthRead_nid_no_patch(std::uint64_t address, std::size_t bytes); +bool HasResidentDepthGuards_nid_no_patch(); +void RetireUnmappedDepth_nid_no_patch(std::uintptr_t address, std::size_t bytes); +void RequireNoNativeDepthFeedback_nid_no_patch(const Context& context, const DepthTarget& target, std::span> sampled); void ClearDepthSurfaces(VkDevice device); +void RetireDepthSurfaces(VkDevice device, std::uint64_t address, std::uint64_t bytes); +struct WrittenStorageExtent { + std::uint64_t address; + std::uint32_t width; + std::uint32_t height; + std::uint32_t bytesPerElement; + std::array block; + std::uint32_t blocksPerRow; +}; +void RetireCoveredDepthSurfaces(VkDevice device, std::span> written, std::span extents); 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); -void NoteHtileDepthClear(std::uint64_t htileAddress); +std::size_t HtileDepthClearBytes_nid_no_patch(std::span code, std::span userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::span packet, std::uint64_t indirectArguments); +bool NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes = 0u); +void ArmHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes); +void QueueHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes); +void CompleteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes); struct MemoryValueFill { std::uint64_t address; std::size_t bytes; @@ -34,11 +71,55 @@ void NoteDepthMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32 VkImageAspectFlags HtileFillClears(std::uint32_t pattern, bool stencilInHtile); std::uint64_t HtileMetadataBytes(VkExtent2D extent); bool HtileFillCovers(std::uint64_t htile, VkExtent2D extent, std::uint64_t address, std::size_t bytes); -std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components); +std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D); VkFormat HostDepthFormat(const Context& context, VkFormat format); float HostDepthBiasConstant(const Context& context, VkFormat format, float constant); std::shared_ptr DepthSurfaceStorage(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip); bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage); +void CopyDepthSample(const Context& context, std::uint64_t depthAddress, std::uint32_t samples, std::uint32_t sample, StorageTexture& destination, VkImageView destinationView, VkExtent2D extent); + +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); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp index d852d67432..b79528bf72 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp @@ -29,9 +29,9 @@ struct DrawInputCopy { std::uint64_t registryGeneration = 0; }; 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); +DrawInputCopy CopyZeroPaddedDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t validBytes, std::size_t stride = 0); 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; @@ -91,8 +91,18 @@ bool DrawRecipes(); // draw index, the vertex ranges, the device features, APS5_NO_GPU_INDIRECT_DRAW) or under the device // lock in Draw (the memory state of the records). `readMs` is the CPU read's time (its sync); // `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 }; +enum class IndirectDrawPath : std::uint8_t { Gpu, NotFolded, FetchUnknown, NonVertexPath, IndxOffset, DrawIndex, VertexRange, FeatureGap, PendingImage, PendingLabelOrCopy, NotImported, Disabled, IndexRange, 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 @@ -117,6 +127,7 @@ void CountDrawSkip(DrawSkip kind, double us); std::shared_ptr>> DrawCopiedWriters(); void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass); +void RunDepthCopyPass(const Context& context, const DepthCopyPass& pass); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 2bcdbbbca0..fa552ac524 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -4,8 +4,14 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "BdaAbi.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include +#include +#include +#include +#include #include #include +#include #include #include #include @@ -36,8 +42,18 @@ struct HostImport { bool joined = false; std::shared_ptr chunk; mutable std::uint64_t lastUse = 0; + std::uint64_t lead = 0; + std::uint64_t memoryBase = 0; + std::uint64_t pinnedBytes = 0; + std::uint32_t memoryType = std::numeric_limits::max(); + std::uint64_t backingSerial = 0; + mutable std::uint64_t spaceSerial = 0; }; +inline VkDeviceSize HostImportOffset(const HostImport& import, std::uint64_t address) { + return address - import.base + import.lead; +} + enum class ImportWatch : std::uint8_t { Watch, Unwatch }; struct ImportProbe { @@ -46,6 +62,9 @@ struct ImportProbe { std::uint32_t pages = 0; std::uint32_t writtenAtImport = 0; std::uint32_t writtenAfterSubmit = 0; + std::uint32_t writtenAfterSecondSubmit = 0; + std::uint32_t writtenDuringSettle = 0; + std::uint32_t writtenAfterSettle = 0; std::uint32_t writtenByCpu = 0; std::uint32_t writtenAfterFill = 0; }; @@ -55,24 +74,73 @@ ImportProbe ProbeDmaBufImportWriteProtection(const Context& context); ImportWatch PrepareImportWatch(const Context& context); void SetImportWatch(const Context& context, ImportWatch watch); +enum class ImportRequester : std::uint8_t { Map, Build, Access, Join, Segment }; +inline constexpr std::size_t ImportRequesterCount = 5; + +struct HostImportRequesterCounts { + std::uint64_t made = 0; + std::uint64_t madeBytes = 0; + std::uint64_t refused = 0; + std::uint64_t refusedBytes = 0; + std::uint64_t failed = 0; + std::uint64_t remembered = 0; +}; + +struct HostImportCounts { + std::uint64_t live = 0; + std::uint64_t liveBytes = 0; + std::array requesters{}; +}; + +const char* ImportRequesterName(ImportRequester requester); +HostImportCounts HostImportCounters(); +std::string FormatHostImportCounts(const HostImportCounts& counts); +void ReportHostImports(); + // The host import of the registered allocation containing [address, address + bytes), made on demand -// (alignment and budget permitting), or null. Bytes at `address` are at `address - import->base` in -// the import's buffer. -const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes); +// (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, ImportRequester requester = ImportRequester::Access); +const HostImport* ExistingHostImportFor(const Context& context, std::uint64_t address, std::size_t bytes); void SetHostImportWindow(std::uint64_t bytes); // Tests: the window of one large registered range (64 MiB) and the import budget (0: the configured one). void SetHostImportRangeWindow(std::uint64_t bytes); void SetHostImportBudget(std::uint64_t bytes); +std::uint64_t HostImportBudgetFor(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t configured, std::uint64_t liveBytes); +std::uint64_t HostImportBudgetWith(std::uint64_t configured, bool set, std::uint64_t reported, bool allocationFailed, std::uint64_t ceiling); +void SetAddressCopyLimit(std::uint64_t bytes); +void SetHostImportDeferredSettles(bool enabled); +// The larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half +// that heap: the other half stays for the buffers, mirrors and textures that share it. +std::uint64_t HostImportBudget(const VkPhysicalDeviceMemoryProperties& memory, std::uint64_t physicalBytes); std::uint64_t HostImportLiveBytes(); +std::uint64_t HostImportBudgetRemaining(const Context& context); +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; + std::uint64_t keptCompared = 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 // made outside the device lock (a sampled texture's path, a dispatch's pre-sync); the path taken // re-checks with HostImportFor when it binds the import. bool HostImportCovers(const Context& context, std::uint64_t address, std::size_t bytes); +struct RegisteredPiece { + std::uint64_t address; + std::uint64_t bytes; + bool writable; + bool gpu; +}; // Whether a readable registered allocation contains [address, address + bytes) right now (one // registry lease): a storage image whose memory was freed or re-registered has nothing to store to. -bool RegisteredReadableCovers(std::uint64_t address, std::size_t bytes); +bool RegisteredReadableCovers(std::uint64_t address, std::size_t bytes, std::vector* pieces = nullptr); std::uint64_t RegisteredReadableEnd(std::uint64_t address); // A persistent device copy of one registered range of the main guest image (which cannot be host @@ -80,6 +148,17 @@ std::uint64_t RegisteredReadableEnd(std::uint64_t address); struct ImageMirror; class Recorder; +template +void OverlappedRanges(std::span sorted, std::size_t first, std::span> writes, std::vector& indices) { + indices.clear(); + for (const auto& [begin, end] : writes) { + auto it = std::partition_point(sorted.begin() + static_cast(first), sorted.end(), [&](const TRange& range) { return range.end <= begin; }); + for (; it != sorted.end() && it->begin < end; ++it) indices.push_back(static_cast(it - sorted.begin())); + } + std::sort(indices.begin(), indices.end()); + indices.erase(std::unique(indices.begin(), indices.end()), indices.end()); +} + // Identity of the import serving [address, address + bytes): a serial unique for the life of one // import, 0 when no import serves the range. An equal serial later means the same VkBuffer still // backs the range (a re-import gets a new serial), which is what a descriptor set written against it @@ -120,6 +199,7 @@ std::uint64_t ImageMirrorSerial(const Context& context, std::uint64_t address, s // prints the [address-sync] leases line every 10 s from LeaseCounters (cumulative). bool SyncLeaseWork(); void CountLeaseOutcome(bool synced, std::uint64_t batchSerial); +void FinishLeaseWork(); struct LeaseStats { std::uint64_t deferred = 0; std::uint64_t synced = 0; @@ -167,6 +247,9 @@ struct MirrorStats { std::uint64_t sparseBinds = 0; }; MirrorStats MirrorCounters(); +std::uint64_t SnapshotMismatches(); +void ClearSnapshotCopies(VkDevice device); +std::vector AddressTableKey(std::span> writes, std::span own); void ClearImageMirrors(VkDevice device); void ClearHostImports(VkDevice device); @@ -265,12 +348,25 @@ public: // through a device-local staging copy of the import (nothing was copied through the CPU, so the // upload can serve a later identical build), else nothing. std::optional>> DirectRegions() const; + struct CutRegion { + std::uint64_t begin; + std::uint64_t end; + std::uint64_t committedEnd; + }; + std::vector CutRegions() const; // Every region the recorded work reads in place through a host import, as [begin, end): the // regions bound in place (`direct`), an address-based build's leased heaps included. A region // the GPU copies out of an import (gpuCopy) notes its read itself when the copy is recorded. // For the recorder's read tracking (ShaderResources::MarkGpuWrites); nothing once committed. std::vector> InPlaceReads() const; + void InPlaceReads(std::vector>& out) const; + struct SplitReads { + std::shared_ptr>> space; + std::vector> own; + }; + std::optional SplitInPlaceReads() const; std::vector> DeviceReads() const; + void DeviceReads(std::vector>& out) const; private: struct Region { @@ -292,6 +388,7 @@ private: // memory; the rest reads as zeros and is never stored. bool sparse = false; bool sparseHeap = false; + bool runCut = false; std::vector> backed {}; std::uint64_t lastView = 0; std::uint64_t bufferEnd = 0; @@ -333,9 +430,10 @@ private: // drops the space: for a region that partially overlaps a base region, and for a build whose // imports changed under the space (`resolve`: the direct regions are re-resolved by UploadFinish). void dissolveSpace(bool resolve); + 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 { @@ -347,6 +445,7 @@ private: // The BDA table entry of an uploaded region. static ShaderRecompiler::BdaAbi::Range addressRange(const Region& region); static std::uint64_t servedEnd(const Region& region); + static std::uint64_t inPlaceEnd(const Region& region); void validate(std::uint64_t address, std::size_t bytes) const; // The region of a descriptor-bound range (AddWritable/AddReadable), committed pages only. A // range inside a base region of the space adds nothing: the region serves it, as today's merge @@ -379,8 +478,8 @@ private: // The cached address space this build maps through (its lease pins the ranges); `regions` then // holds only the regions outside it (V#s, snapshots, ranges copied per build). std::shared_ptr space; - mutable std::uint64_t writeTableSerial = 0; - mutable std::shared_ptr> writeTableRanges; + mutable std::uint64_t tableSerial = 0; + mutable std::shared_ptr> tableRanges; // Import registry epoch when `direct` pointers were taken at acquire time; they are reused while // no import was destroyed since. std::uint64_t importsEpoch = 0; @@ -389,6 +488,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/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index 997e9dc7e7..89e151fb24 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_GUESTTEXTURERESOURCE_HPP #include "Recompiler.hpp" +#include #include #include @@ -58,7 +59,8 @@ enum class TextureDimension { k2DArray, kCube, k3D, - k1DArray + k1DArray, + k2DMsaa }; struct GuestTextureResource { @@ -84,14 +86,19 @@ struct GuestTextureResource { std::uint64_t dccAddress = 0; bool dccAlphaOnMsb = false; bool dccPipeAligned = false; + bool iterate256 = false; std::uint32_t minLod = 0; std::uint32_t allocatedMipCount = 0; + std::uint32_t samples = 1; + std::uint64_t fmaskCmaskAddress = 0; }; float EffectiveMinLod(const GuestTextureResource& resource); +bool IsNullTextureResource(std::span words); GuestTextureResource DecodeTextureResource(std::span words); bool DescriptorSingleLevel(std::span words); +bool FirstLayerDescriptor(std::span words, std::array& narrowed); bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, TextureDimension dimension); bool ArrayLayersServeView(const GuestTextureResource& surface, const GuestTextureResource& view); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleSwizzleEquations.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleSwizzleEquations.hpp new file mode 100644 index 0000000000..358d893eb8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleSwizzleEquations.hpp @@ -0,0 +1,53 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLESWIZZLEEQUATIONS_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLESWIZZLEEQUATIONS_HPP + +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include +#include + +namespace AgcDriver::Graphics { + +struct MultisampleSwizzleEquation { + TextureTileMode tileMode; + std::uint32_t elementBytes; + std::uint32_t fragments; + std::array bits; +}; + +inline constexpr MultisampleSwizzleEquation kMultisampleSwizzleEquations[] = { + {TextureTileMode::kZ64KBX, 1u, 2u, {0x0001000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000100000ull, 0x0000000000400000ull, 0x0000000000000040ull, 0x0000000000800000ull}}, + {TextureTileMode::kZ64KBX, 2u, 2u, {0x0000000000000000ull, 0x0001000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000000040000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0000000000000040ull}}, + {TextureTileMode::kZ64KBX, 4u, 2u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0001000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0000000000840000ull}}, + {TextureTileMode::kZ64KBX, 1u, 4u, {0x0001000000000000ull, 0x0002000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000000040000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0000000000000040ull}}, + {TextureTileMode::kZ64KBX, 2u, 4u, {0x0000000000000000ull, 0x0001000000000000ull, 0x0002000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000000000040ull, 0x0000000000440000ull}}, + {TextureTileMode::kZ64KBX, 4u, 4u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0001000000000000ull, 0x0002000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000440000ull, 0x0000000000800004ull}}, + {TextureTileMode::kZ64KBX, 1u, 8u, {0x0001000000000000ull, 0x0002000000000000ull, 0x0004000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000000000004ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000800400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0000000000840000ull}}, + {TextureTileMode::kZ64KBX, 2u, 8u, {0x0000000000000000ull, 0x0001000000000000ull, 0x0002000000000000ull, 0x0004000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000800400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000440000ull, 0x0000000000800004ull}}, + {TextureTileMode::kZ64KBX, 4u, 8u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0001000000000000ull, 0x0002000000000000ull, 0x0004000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000000440020ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000000400004ull, 0x0000000000820000ull}}, + {TextureTileMode::kR64KBX, 1u, 2u, {0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000000010000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000400000ull, 0x0000000000000040ull, 0x0000000000800000ull, 0x0001000001000000ull}}, + {TextureTileMode::kR64KBX, 2u, 2u, {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000100000ull, 0x0000000000000040ull, 0x0000000000400000ull, 0x0001000000800000ull}}, + {TextureTileMode::kR64KBX, 4u, 2u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0000000000400000ull, 0x0001000000800000ull}}, + {TextureTileMode::kR64KBX, 8u, 2u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0001000000400000ull}}, + {TextureTileMode::kR64KBX, 16u, 2u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0001000000400020ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000000080000ull, 0x0000000000000010ull}}, + {TextureTileMode::kR64KBX, 1u, 4u, {0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000000010000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000400000ull, 0x0000000000000040ull, 0x0001000000800000ull, 0x0002000001000000ull}}, + {TextureTileMode::kR64KBX, 2u, 4u, {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000100000ull, 0x0000000000000040ull, 0x0001000000400000ull, 0x0002000000800000ull}}, + {TextureTileMode::kR64KBX, 4u, 4u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000800080008ull, 0x0000000400100010ull, 0x0000000200200040ull, 0x0000000100400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0001000000400000ull, 0x0002000000800000ull}}, + {TextureTileMode::kR64KBX, 8u, 4u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0002000000200040ull, 0x0000000100400020ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0001000000400000ull}}, + {TextureTileMode::kR64KBX, 16u, 4u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000200080008ull, 0x0000000100100010ull, 0x0002000000200040ull, 0x0001000000400020ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000000080000ull, 0x0000000000000010ull}}, + {TextureTileMode::kR64KBX, 1u, 8u, {0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000000010000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000800400020ull, 0x0000000000400000ull, 0x0001000000800000ull, 0x0002000001000000ull, 0x0004000002000000ull}}, + {TextureTileMode::kR64KBX, 2u, 8u, {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0000000800400020ull, 0x0000000000100000ull, 0x0001000000400000ull, 0x0002000000800000ull, 0x0004000001000000ull}}, + {TextureTileMode::kR64KBX, 4u, 8u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x0000000000010000ull, 0x0000000000020000ull, 0x0000000000040000ull, 0x0000000000000004ull, 0x0000000400080008ull, 0x0000000200100010ull, 0x0000000100200040ull, 0x0004000000400020ull, 0x0000000000080000ull, 0x0000000000000010ull, 0x0001000000400000ull, 0x0002000000800000ull}}, + {TextureTileMode::kR64KBX, 8u, 8u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000000004ull, 0x0000000000020000ull, 0x0000000200080008ull, 0x0000000100100010ull, 0x0004000000200040ull, 0x0002000000400020ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0000000000100000ull, 0x0001000000400000ull}}, + {TextureTileMode::kR64KBX, 16u, 8u, {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000010000ull, 0x0000000000000002ull, 0x0000000000020000ull, 0x0000000200080008ull, 0x0000000100100010ull, 0x0004000000200040ull, 0x0002000000400020ull, 0x0000000000000004ull, 0x0000000000040000ull, 0x0000000000000008ull, 0x0001000000400000ull}}, +}; + +constexpr const MultisampleSwizzleEquation* FindMultisampleSwizzleEquation(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t fragments) { + for (const auto& equation : kMultisampleSwizzleEquations) { + if (equation.tileMode == tileMode && equation.elementBytes == elementBytes && equation.fragments == fragments) return &equation; + } + return nullptr; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index bfcc8fd534..b9c18eb7d1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -3,22 +3,30 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include #include #include +#include #include +#include #include namespace AgcDriver::Graphics { class StorageTexture; +class Texture; +struct GuestTextureResource; + +std::optional MultisampledCmaskKeys(std::uint8_t code); class MultisampledCmask { public: MultisampledCmask() = default; MultisampledCmask(std::uint64_t cmaskAddress, std::uint64_t cmaskBytes) : address(cmaskAddress), bytes(cmaskBytes) {} void NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern); - void SeedKeys(DccKeys seeded); + void SeedCode(std::optional code); void CheckAddress(std::uint64_t cmaskAddress) const; bool TakeClear(); DccKeys Keys() const { return keys; } @@ -46,14 +54,34 @@ 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); +struct MultisampledSingleClear { + std::uint64_t descriptor = 0; + std::array color{}; +}; +std::optional MatchMultisampledSingleClear(std::span code, std::span userData, const std::array& numThreads); +void NoteMultisampledSingleClear(const GuestTextureResource& resource, const std::array& color); +bool TakeMultisampledSingleClear(std::span code, std::span userData, const std::array& numThreads); bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear); -VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); +SampleOrderTarget MultisampleTargetOrderTarget(const Context& context, const ColorTarget& target); +std::shared_ptr MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D); +VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, VkImage* image = nullptr); +VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, std::shared_ptr& owner, VkImage* image = nullptr); +void KeepMultisampleTarget_nid_no_patch(const std::shared_ptr& owner); +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); +std::shared_ptr MultisampleTargetFmaskTexture_nid_no_patch(const Context& context, const GuestTextureResource& resource, VkComponentMapping components); void ClearMultisampleTargets(VkDevice device); +void InstallResidentImageReadGuard_nid_no_patch(); +void UpdateResidentImageUnmapObserver_nid_no_patch(); void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access); +void RequireNoMultisampleColorTarget(std::uint64_t address, std::size_t bytes, const char* access); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp new file mode 100644 index 0000000000..a7960c43dc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp @@ -0,0 +1,54 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETEXTURELAYOUT_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETEXTURELAYOUT_HPP + +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +class MultisampleTextureLayout { +public: + static constexpr std::uint32_t Alignment = 65536u; + + MultisampleTextureLayout(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t fragments, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t pipeBankXor = 0); + std::uint64_t Bytes() const { return sliceBytes * layers; } + std::uint64_t SliceBytes() const { return sliceBytes; } + std::uint64_t LinearBytes() const { return static_cast(width) * height * layers * fragments * elementBytes; } + std::uint32_t Pitch() const { return pitch; } + std::uint32_t PaddedHeight() const { return paddedHeight; } + std::uint32_t BlockWidth() const { return blockWidth; } + std::uint32_t BlockHeight() const { return blockHeight; } + const std::array& InverseBlockEquation() const { return inverseBlockEquation; } + std::uint64_t Offset(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t fragment) const; + void Detile(std::span source, std::span destination) const; + void Tile(std::span source, std::span destination) const; + +private: + std::uint64_t offset(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t fragment) const; + void transfer(std::span source, std::span destination, bool retile) const; + + std::uint32_t elementBytes; + std::uint32_t fragments; + std::uint32_t width; + std::uint32_t height; + std::uint32_t layers; + std::uint32_t pipeBankXor; + std::uint32_t blockWidth; + std::uint32_t blockHeight; + std::uint32_t pitch; + std::uint32_t paddedHeight; + std::uint64_t sliceBytes; + std::array inverseBlockEquation; + std::vector xOffsets; + std::vector yOffsets; + std::vector layerOffsets; + std::vector fragmentOffsets; +}; + +} + +#endif 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/PassHazards.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp index 9ce1cb70fc..298ca9c49e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp @@ -22,6 +22,7 @@ struct PassAccess { std::span> attachments; bool anyReads = false; bool anyWrites = false; + GuestRangeList writeOnly = {}; }; enum class PassHazard : std::uint8_t { None, ReadAfterWrite, WriteAfterWrite, WriteAfterRead, Image, UnknownRead, UnknownWrite }; @@ -32,6 +33,7 @@ public: bool Overlaps(std::uint64_t begin, std::uint64_t end) const; bool Empty() const { return ranges.empty(); } std::size_t Size() const { return ranges.size(); } + GuestRangeList Ranges() const { return ranges; } void Clear() { ranges.clear(); } private: @@ -43,6 +45,7 @@ public: PassHazard Check(const PassAccess& access, bool samePass) const; void Add(const PassAccess& access); void BeginPass() { ++pass; } + void SealWriteOnly(); void Clear(); bool Empty() const; @@ -55,6 +58,7 @@ private: bool overlapsAny(const GuestRangeSet& set, GuestRangeList ranges) const; GuestRangeSet reads; GuestRangeSet writes; + GuestRangeSet writeOnly; std::vector> images; std::vector attachments; std::uint64_t pass = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 56878782df..1ee631c131 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -47,7 +47,7 @@ public: Pipeline(const Pipeline&) = delete; Pipeline& operator=(const Pipeline&) = delete; VkPipelineLayout Layout() const; - std::shared_ptr AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent, std::uint32_t layers = 1, std::span persistent = {}); + std::shared_ptr AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent, std::uint32_t layers = 1); // Begins the render pass on the framebuffer, binds the pipeline and sets its dynamic state. void Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, VkExtent2D extent, const State& state) const; // The same inside a render pass another pipeline of the same attachments began (compatible by @@ -58,6 +58,10 @@ public: void ContinueBackFaces(VkCommandBuffer commands, const State& state) const; bool SplitsSecondChannel() const { return secondChannel != VK_NULL_HANDLE; } void ContinueSecondChannel(VkCommandBuffer commands) const; + const SampleOrder& HostSampleOrder() const { return sampleOrder; } + bool ReordersSamples() const { return Graphics::ReordersSamples(sampleOrder); } + bool SampleLocationsEnabled() const { return sampleLocationsEnabled; } + VkExtent2D HostSampleLocationGrid() const { return sampleLocationGrid; } void PushConstants(VkCommandBuffer commands, VkShaderStageFlags stages, std::span bytes) const; // Forgets the Vulkan objects without destroying them: for entries of a device that is already gone. void Abandon() noexcept; @@ -65,10 +69,11 @@ public: private: struct CachedFramebuffer { std::vector views; - std::vector> owners; + std::vector> owners; std::vector owned; VkExtent2D extent; std::uint32_t layers = 1; + std::uint64_t depthReleases = 0; std::shared_ptr framebuffer; }; void release() noexcept; @@ -91,17 +96,34 @@ private: VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_UNDEFINED; std::vector colorSlots; bool sampleLocationsEnabled = false; + VkExtent2D sampleLocationGrid{1, 1}; + SampleOrder sampleOrder{}; 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,12 +141,15 @@ 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); 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); +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, bool meshShader = true); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp index 6398fde6d1..4aaebda08a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp @@ -4,8 +4,11 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include #include +#include #include #include +#include +#include #include #include @@ -15,17 +18,37 @@ 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(); + void SaveUnlessBusy(); private: + struct FileHeader { + std::uint32_t magic; + std::uint32_t format; + std::uint64_t dataBytes; + std::uint64_t dataHash; + bool operator==(const FileHeader&) const = default; + }; + enum class FileState { Missing, Unreadable, Invalid, Valid }; + struct File { + FileState state = FileState::Missing; + const char* invalidReason = nullptr; + FileHeader header{}; + std::vector contents; + std::span Payload() const; + }; + + File readFile() const; + std::optional readHeader() const; void load(std::vector& initialData); - void save(bool final); + void save(bool final, bool waitForPipelines); void run(); Context context; @@ -33,7 +56,10 @@ private: VkPipelineCache cache = VK_NULL_HANDLE; PipelineCacheSynchronization synchronization; std::filesystem::path path; - std::size_t savedBytes = 0; + bool holdsFile = false; + std::size_t loadedBytes = 0; + std::size_t settledBytes = 0; + std::optional settledFile; std::mutex mutex; std::condition_variable wake; bool stopping = false; 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/PipelineSpecialization.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp index 0d53ce5be0..37ab9d1daa 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp @@ -2,9 +2,11 @@ #define CORE_LIBS_AGCDRIVER_GRAPHICS_PIPELINESPECIALIZATION_HPP #include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" +#include #include #include #include +#include #include #include @@ -52,21 +54,41 @@ public: explicit PipelineSpecialization(const ShaderRecompiler::RecompileResult& shader) { Validate(shader); - entries.reserve(shader.specialization.size()); - for (std::size_t index = 0; index < shader.specialization.size(); ++index) { - const auto offset = index * sizeof(ShaderRecompiler::PipelineSpecializationConstant) + offsetof(ShaderRecompiler::PipelineSpecializationConstant, value); - Require(offset <= std::numeric_limits::max(), "pipeline specialization offset exceeds Vulkan limits"); - entries.push_back({shader.specialization[index].id, static_cast(offset), sizeof(std::uint32_t)}); - } - info.mapEntryCount = static_cast(entries.size()); - info.pMapEntries = entries.data(); - info.dataSize = shader.specialization.size() * sizeof(ShaderRecompiler::PipelineSpecializationConstant); - info.pData = shader.specialization.data(); + describe(shader.specialization); } + PipelineSpecialization(const ShaderRecompiler::RecompileResult& shader, std::span replaced) : values(shader.specialization) { + Validate(shader); + for (const auto& constant : replaced) { + const auto found = std::ranges::find(values, constant.id, &ShaderRecompiler::PipelineSpecializationConstant::id); + Require(found != values.end(), "a replaced pipeline specialization ID is not supplied by the shader"); + found->value = constant.value; + } + describe(values); + } + + PipelineSpecialization(const PipelineSpecialization&) = delete; + PipelineSpecialization& operator=(const PipelineSpecialization&) = delete; + PipelineSpecialization(PipelineSpecialization&&) noexcept = default; + PipelineSpecialization& operator=(PipelineSpecialization&&) noexcept = default; + [[nodiscard]] const VkSpecializationInfo* Info() const { return entries.empty() ? nullptr : &info; } private: + void describe(std::span constants) { + entries.reserve(constants.size()); + for (std::size_t index = 0; index < constants.size(); ++index) { + const auto offset = index * sizeof(ShaderRecompiler::PipelineSpecializationConstant) + offsetof(ShaderRecompiler::PipelineSpecializationConstant, value); + Require(offset <= std::numeric_limits::max(), "pipeline specialization offset exceeds Vulkan limits"); + entries.push_back({constants[index].id, static_cast(offset), sizeof(std::uint32_t)}); + } + info.mapEntryCount = static_cast(entries.size()); + info.pMapEntries = entries.data(); + info.dataSize = constants.size() * sizeof(ShaderRecompiler::PipelineSpecializationConstant); + info.pData = constants.data(); + } + + std::vector values; std::vector entries; VkSpecializationInfo info{}; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp index 7e9a4dc18b..71575f622a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp @@ -5,6 +5,7 @@ #include "RuntimeAbi.hpp" #include #include +#include namespace AgcDriver::Graphics { @@ -51,6 +52,12 @@ constexpr PlaceholderImageShape PlaceholderImageShapeOf(std::uint32_t heap) { samples[dimension]}; } +constexpr std::optional PlaceholderImageShapeForDevice(std::uint32_t heap, bool storageImageMultisample) { + const auto shape = PlaceholderImageShapeOf(heap); + if (!storageImageMultisample && (shape.usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0 && shape.samples != VK_SAMPLE_COUNT_1_BIT) return std::nullopt; + return shape; +} + class PlaceholderImages { public: explicit PlaceholderImages(const Context& context); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 1fda73efd9..6ef4b5cf08 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -1,8 +1,10 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_RECORDER_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_RECORDER_HPP +#include "prx/libSceAgcDriver/Graphics/include/BarrierBatch.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/PassHazards.hpp" +#include #include #include #include @@ -67,6 +69,7 @@ public: }; DrawPassStart StartDrawPass(std::uint64_t key, bool forced, bool fenced, const PassAccess& access); void LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool fenced, const PassAccess& access, std::function afterPass = {}); + void SealPassWriteOnly(); // DCC "uncompressed" key stores (DccMetadata.cpp StoreUncompressedOnGpu): queued on the open // batch and recorded as one run (one barrier pair for every queued fill) at Submit, before a // label store (RecordStore), or before a command that writes or reads a queued range (the @@ -83,6 +86,8 @@ public: void FlushKeyStoresOverlapping(std::uint64_t address, std::size_t bytes) { if (HasQueuedKeyStores() && QueuedKeyStoreOverlaps(address, bytes)) FlushKeyStores(); } bool Recording() const { return open != nullptr; } bool Idle() const { return open == nullptr && inFlight.empty(); } + std::uint64_t OpenSerial() const { return submissions + 1; } + std::uint64_t CompletedThrough() const { return inFlight.empty() ? submissions : inFlight.front()->serial - 1; } // Whether recorded work still has completion actions (write-backs the CPU must see) to run. bool HasCompletions() const; void Keep(std::shared_ptr object, std::size_t bytes = 0); @@ -96,10 +101,20 @@ public: enum class SnapshotUse : std::uint8_t { Storage, Vertex, Index16, Index32, Index16Restart, Index32Restart }; static constexpr std::size_t DrawSnapshotBudget = std::size_t{1024} << 20u; static constexpr std::size_t DrawSnapshotEntries = 1024; + static constexpr std::size_t UnwrittenSnapshotEntries = 4096; static constexpr std::size_t DrawInputBudget = std::size_t{1024} << 20u; static constexpr std::size_t DrawInputEntries = 16384; + std::shared_ptr UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, VkDeviceSize* offset = nullptr); + void NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, std::shared_ptr buffer, VkDeviceSize offset = 0); std::shared_ptr ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use = SnapshotUse::Storage, std::uint32_t* derived = nullptr, std::uint64_t generation = 0); void KeepDrawSnapshot(std::uint64_t address, std::size_t bytes, std::uint64_t generation, std::uint64_t registryGeneration, std::shared_ptr buffer, SnapshotUse use = SnapshotUse::Storage, std::uint32_t derived = 0); + static constexpr std::size_t DrawUploadChunk = std::size_t{1} << 20u; + struct UploadSlice { + std::shared_ptr buffer; + VkDeviceSize offset = 0; + std::byte* bytes = nullptr; + }; + UploadSlice DrawUpload(std::size_t bytes); static constexpr std::size_t IndexRangeEntries = 16384; static constexpr std::size_t KeptIndexRangeIndices = 4096; bool KnownHighestIndex(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::optional& highest); @@ -107,6 +122,7 @@ public: void OnComplete(std::function action); void NotePendingWrite(std::uint64_t address, std::size_t bytes); void NotePendingFill(std::uint64_t address, std::size_t bytes, std::uint8_t value); + void NotePendingWordFill(std::uint64_t address, std::size_t bytes, std::uint32_t word); // Notes several [begin, end) ranges and publishes the snapshot once (a dispatch writes many buffers). void NotePendingWrites(std::span> ranges); bool PendingWriteOverlaps(std::uint64_t address, std::size_t bytes) const; @@ -121,8 +137,10 @@ public: std::uint64_t end; std::uint64_t note; int value; + std::int64_t word = -1; }; std::vector PendingWritesOver(std::uint64_t address, std::size_t bytes) const; + void PendingWriteRanges(std::vector>& ranges) const; // Batch read tracking: the guest ranges the recorded work reads IN PLACE through a host import // and the GPU has not executed yet (a V# element bound in place, a region the GPU copies out of // an import, an address-based build's leased heaps, indirect arguments, a GPU-direct storage @@ -135,6 +153,9 @@ public: static bool ReadTracking(); void NotePendingRead(std::uint64_t address, std::size_t bytes, ReadKind kind); void NotePendingReads(std::span> ranges, ReadKind kind); + using ReadList = std::vector>; + void NotePendingReadList(std::shared_ptr ranges, ReadKind kind); + static bool ValidReadList(const ReadList& ranges); // Whether an unexecuted batch (open, or in flight with its fence unsignaled) reads the range in // place. A hit in an in-flight batch whose fence signaled meanwhile is a miss (one status // query per hit). `ignoreSignaled` false: every in-flight batch counts, whatever its fence. @@ -159,6 +180,7 @@ public: static ReadStatistics ReadCounts(); // Whether the OPEN (unsubmitted) batch writes the range: a wait on such a range must submit it. bool OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) const; + std::uint64_t PendingWriterSerial(std::uint64_t address, std::size_t bytes) const; // What SyncThrough(address, bytes) would wait for, for the [hooksync] attribution: the newest // batch whose noted range overlaps the access (the open batch counts first, with the serial it // will get), whether that batch's fence has already signaled (its GPU work is done, so the @@ -175,10 +197,12 @@ public: std::optional DescribePendingWrite(std::uint64_t address, std::size_t bytes) const; // Ends and submits the open batch without waiting. void Submit(); + void HoldNextSubmit(VkSemaphore semaphore, std::uint64_t value); // Submits the open batch and returns the serial of the newest submitted batch (0 when nothing was // ever submitted); WaitSerial(serial) then covers all recorded work. std::uint64_t SubmitAndEpoch(); bool HasTimeline() const { return timeline != VK_NULL_HANDLE; } + bool RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments, const std::array* threadGroup = nullptr); // Waits until every batch up to `serial` completed on the GPU. Called WITHOUT GuestMemory::GpuMutex: // it touches only the immutable timeline semaphore (the caller keeps the device alive). Requires // HasTimeline(); the debug switch APS5_NO_TIMELINE makes the device create the recorder without one. @@ -209,6 +233,7 @@ public: std::uint64_t end; bool computed; }; + static std::vector QueuedWriteOrder(std::span writes); static std::vector QueuedWriteGroups(std::span writes); // Waits only for the batches up to the newest one that writes the range (submitting the open // batch when it is that one); later batches stay in flight. Fences of one queue signal in @@ -461,6 +486,8 @@ public: // (or APS5_NO_BARRIER_ELISION=1) records every leading barrier, as before. static bool MergeBarriers(); static void CountMerged(CommandClass which); + static bool BarrierBatch(); + static bool ElideCovered(CommandClass which, const BarrierMask& wanted, VkAccessFlags covered); // Hazard tracker, counting mode (APS5_BARRIER_VALIDATE=1): every command tells the tracker // what it reads and writes (guest ranges through host imports, images) with its stage before // it is recorded; the tracker simulates barriers emitted only on a hazard against the accesses @@ -526,6 +553,7 @@ private: std::vector> writes; std::vector writeNotes; std::vector writeValues; + std::vector writeWords; // In-place reads (see NotePendingRead), dying with the batch: a finished batch's reads are done. struct Read { std::uint64_t begin; @@ -533,8 +561,15 @@ private: ReadKind kind; }; std::vector reads; + struct ReadListNote { + std::shared_ptr ranges; + std::size_t position; + ReadKind kind; + }; + std::vector readLists; // Submission number (1-based): identifies a batch after its allocation may have been reused. std::uint64_t serial = 0; + std::uint64_t traceId = 0; bool submitted = false; // The GpuMutex queue tag of the thread that opened the batch, and when it was submitted // (the [syncwait] line reports what a wait's target and the batches ahead of it were). @@ -686,6 +721,9 @@ private: VkPipelineLayout dumpLayout = VK_NULL_HANDLE; VkPipeline dumpPipeline = VK_NULL_HANDLE; std::shared_ptr segmentPools; + int dispatchArgumentState = 0; + VkPipelineLayout dispatchArgumentLayout = VK_NULL_HANDLE; + VkPipeline dispatchArgumentPipeline = VK_NULL_HANDLE; // BeginGpuTiming on the open batch without Commands() (RecordStore times its own run, which // Commands() would close). std::uint32_t beginTiming(std::uint64_t key); @@ -735,17 +773,19 @@ 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); - // The first noted read of `batch` overlapping [address, end), or null. - static const Batch::Read* readOverlap(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, std::optional only = std::nullopt); + static std::vector> notedReads(const Batch& batch); bool signaled(const Batch& batch) const; // Rebuilds the lock-free snapshot of pending writes from open, inFlight and finishing. void publishPendingWrites(std::span> additions = {}) const; // Appends one range to the open batch; returns whether the snapshot must be rebuilt for it. // `ownLabel`: the range is a label's own store (NoteLabel, AfterCompletions), which does not // overwrite the table entries it covers; any other range flags them (table mutex, briefly). - bool noteWrite(std::uint64_t address, std::size_t bytes, bool ownLabel = false, int value = -1); + bool noteWrite(std::uint64_t address, std::size_t bytes, bool ownLabel = false, int value = -1, std::int64_t word = -1); // Appends one range to `batch` (open or in flight) and publishes the snapshot if needed. void noteWriteOn(Batch& batch, std::uint64_t address, std::size_t bytes, bool ownLabel = false); void noteLabelOn(Batch& batch, std::uint64_t address, std::span bytes, std::uint64_t stamp, std::uint32_t queue, bool behindCompletion = false); @@ -767,6 +807,10 @@ private: Context context; VkSemaphore timeline = VK_NULL_HANDLE; + VkSemaphore submitHold = VK_NULL_HANDLE; + std::uint64_t submitHoldValue = 0; + struct ProgressWatcher; + std::unique_ptr progressWatcher; // Identity for a thread that released the mutex around a wait (syncThroughUnlocked): the // recorder may have been torn down meanwhile, so it is looked up by this id, not by pointer. std::uint64_t id = 0; @@ -790,9 +834,12 @@ private: // Batches popped from inFlight whose completions have not run yet: their writes stay in the // snapshot (a rebuild from inside a completion must not drop them) until finish returns. std::vector finishing; + mutable std::shared_ptr published; std::uint64_t submissions = 0; std::size_t inFlightKeptBytes = 0; std::uint64_t writeNoteCount = 0; + std::shared_ptr drawUpload; + VkDeviceSize drawUploadUsed = 0; // Command buffers and fences of completed batches, reused by later ones (hundreds of batches per // frame would otherwise allocate and free their objects each time). std::vector> spare; @@ -814,6 +861,15 @@ private: std::size_t bytes = 0; }; std::map drawSnapshots; + struct UnwrittenSnapshot { + std::size_t bytes; + std::shared_ptr buffer; + VkDeviceSize offset = 0; + }; + std::unordered_map unwrittenSnapshots; + std::array unwrittenStamp{}; + std::size_t unwrittenBytes = 0; + void retireUnwrittenSnapshots(const std::array& stamp); std::array drawSnapshotPools; void eraseDrawSnapshot(std::map::iterator entry); struct IndexRange { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp index 8830f781d9..674cff0c47 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp @@ -63,6 +63,8 @@ private: VkDeviceSize allocationBytes = 0; 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/SampleOrder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp new file mode 100644 index 0000000000..dc1a089fc5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp @@ -0,0 +1,74 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SAMPLEORDER_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SAMPLEORDER_HPP + +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +struct HostSampleLocations { + VkExtent2D grid{1, 1}; + SampleOrder order{}; +}; + +enum class SampleOrderMethod : std::uint8_t { + Storage, + Depth, + DepthStencilExport, + DepthStencilBits +}; + +struct SampleOrderStep { + std::uint32_t attachment = 0; + SampleOrderMethod method = SampleOrderMethod::Storage; + bool toHost = false; + bool toGuest = false; +}; + +struct SampleOrderPass { + SampleOrder order{}; + VkExtent2D extent{}; + std::array locations{}; + bool located = false; +}; + +struct SampleOrderTarget { + VkImage image = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkFormat format = VK_FORMAT_UNDEFINED; + VkExtent2D extent{}; + std::uint32_t samples = 1; + SampleOrderMethod method = SampleOrderMethod::Storage; + std::shared_ptr keep; +}; + +struct DepthSampleLocations { + VkImage image = VK_NULL_HANDLE; + VkFormat format = VK_FORMAT_UNDEFINED; + std::uint32_t samples = 1; + VkExtent2D grid{1, 1}; + std::array locations{}; + std::shared_ptr keep; +}; + +HostSampleLocations HostSampleLocationsFor(const Context& context, const State& state); +bool HostsSampleLocationGrid(VkExtent2D largestGrid); +bool MayReorderSamples(const Context& context, std::uint32_t samples); +SampleOrderMethod DepthSampleOrderMethod(const Context& context, const DepthTarget& depth); +VkFormat SampleOrderStorageFormat(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples); +std::string SampleOrderRefusal(const Context& context, const State& state); +std::vector PlanSampleOrder(const Context& context, const State& state, const SampleOrder& order, VkAttachmentLoadOp load = VK_ATTACHMENT_LOAD_OP_LOAD, VkAttachmentStoreOp store = VK_ATTACHMENT_STORE_OP_STORE); +SampleOrderPass SampleOrderPassFor(const Context& context, const State& state, const SampleOrder& order); +void RecordSampleOrder(const Context& context, VkCommandBuffer commands, const SampleOrderPass& pass, std::span targets, bool toHost, std::vector>& kept); +void RecordDepthSampleLocations(const Context& context, VkCommandBuffer commands, const DepthSampleLocations& depth); +std::array, 9> SampleOrderShaderModules(); +void ClearSampleOrders(VkDevice device); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp index d7749e2da3..ad2f347b21 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp @@ -24,6 +24,8 @@ public: bool RequiresFilterMinmax() const; bool ReadsOpaqueBlackBorder() const; bool RequiresNonSeamlessCube() const; + static void Validate(const Context& context, const GuestSamplerResource& descriptor); + static bool ReadsOpaqueBlackBorder(const GuestSamplerResource& descriptor); private: void release() noexcept; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp index 6ae3f8ab60..d555d389a5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp @@ -11,13 +11,16 @@ namespace AgcDriver::Graphics { ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array& exportMappings, bool nullProgram = false); +bool PixelEarlyZ(std::uint32_t shaderControl); ShaderRecompiler::ShaderPixelStageInfo DefaultUnexportedInputs(ShaderRecompiler::ShaderPixelStageInfo pixel, std::span parameterExports); ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& shader, std::span header); // A guest range DecodeVertexStageInfo read (an attribute word, a vertex V#) with the bytes as read: // the draw cache validates them by value with the stage's capture (design_cpu_final rule RD). struct DecodeRead { std::uint64_t address; - std::vector bytes; + std::array data{}; + std::uint32_t size = 0; + std::span Bytes() const { return {data.data(), size}; } }; ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads = nullptr, bool staticAbi = false, std::uint32_t hiddenUserWords = 0); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 1fa042481d..5321104605 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -9,13 +9,16 @@ #include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" #include #include +#include #include #include #include #include +#include #include #include #include +#include #include namespace AgcDriver { class VulkanDevice; } @@ -26,23 +29,30 @@ 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 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::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory); +std::uint64_t StorageTextureCacheBudget(const Context& context); struct TextureCacheUse { std::size_t sampledEntries = 0; std::uint64_t sampledBytes = 0; std::uint64_t storageBytes = 0; }; TextureCacheUse TextureCacheUsage(); +void OverrideCacheBudgetForTest(std::uint64_t bytes); std::uint64_t SampledTextureCacheBudget(const Context& context); -std::shared_ptr CachedSampledTexture(const Context& context, std::span words); +std::shared_ptr CachedSampledTexture(const Context& context, std::span words, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D); bool SampledTexturesShareEntry(std::span first, std::span second); -void CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes); +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. std::shared_ptr CachedStorageSurface(const Context& context, const GuestTextureResource& resource); +std::shared_ptr CachedStorageLayers(const Context& context, const GuestTextureResource& resource, std::uint32_t firstLayer, std::uint32_t layerCount); // Whether `image` is still the storage cache's image of its surface (what CachedStorageSurface // would return); a true answer counts as a use for the cache's eviction order, as a lookup would. bool StorageImageCached(const Context& context, const StorageTexture* image); @@ -60,6 +70,13 @@ struct PipelineSetLayouts { std::span fragmentKey; }; +struct TableDescriptors { + std::uint32_t binding = 0; + std::vector> runs; + std::vector images; + std::shared_ptr>> textures; +}; + class DescriptorCache { public: explicit DescriptorCache(const Context& context); @@ -75,9 +92,15 @@ public: VkDescriptorPool pool = VK_NULL_HANDLE; std::size_t poolIndex = 0; bool updateAfterBind = false; + VkDescriptorSetLayout layout = VK_NULL_HANDLE; + std::vector sizes; + std::optional variableCount; + std::shared_ptr table; }; - SetAllocation Allocate(VkDescriptorSetLayout layout, std::span sizes, bool updateAfterBind = false); - void Free(const SetAllocation& allocation) noexcept; + SetAllocation Allocate(VkDescriptorSetLayout layout, std::span sizes, bool updateAfterBind = false, std::optional variableCount = std::nullopt); + void Free(const SetAllocation& allocation, std::shared_ptr table = {}) noexcept; + SetAllocation AllocateRewritten(VkDescriptorSetLayout layout, std::span sizes, bool updateAfterBind = false, std::optional variableCount = std::nullopt); + void Recycle(VkDescriptorSetLayout layout, const SetAllocation& allocation) noexcept; // APS5_PROFILE_DRAW counters: layouts served from the map / created, sets allocated, pools opened. struct Stats { std::uint64_t layoutHits = 0; @@ -85,6 +108,8 @@ public: std::uint64_t sets = 0; std::uint64_t pools = 0; std::uint64_t dedicatedPools = 0; + std::uint64_t keptSets = 0; + std::uint64_t reusedSets = 0; }; Stats Counters() const; @@ -93,6 +118,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 { @@ -103,9 +129,30 @@ private: std::vector pools; std::vector updateAfterBindPools; std::vector dedicated; + std::map, std::vector> recycled; + std::vector kept; Stats stats; }; +enum class ContentKeyComponent : std::size_t { Layout, BufferWord0, BufferWord1, BufferWord2, BufferWord3, Image, Sampler, ShaderData, Other, DrawTarget, Count }; + +using ContentKeyDiff = std::array(ContentKeyComponent::Count)>; + +struct ImageTableReuseCounts { + std::uint64_t reused = 0; + std::uint64_t resolved = 0; + std::uint64_t replayed = 0; + std::uint64_t writesKept = 0; + std::uint64_t writesMade = 0; +}; +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); @@ -137,8 +184,13 @@ public: struct Snapshot { std::uint64_t address; std::shared_ptr buffer; + VkDeviceSize offset = 0; + std::size_t bytes = 0; + std::span Bytes() const { return buffer->Bytes().subspan(static_cast(offset), bytes); } }; DescriptorCache* cache = nullptr; + VkDescriptorSetLayout layout = VK_NULL_HANDLE; + VkDescriptorSetLayout fragmentLayout = VK_NULL_HANDLE; DescriptorCache::SetAllocation fragmentAllocation; DescriptorCache::SetAllocation allocation; std::vector snapshots; @@ -148,11 +200,20 @@ public: std::size_t allocation; std::uint64_t address; std::size_t size; - std::vector words; + std::span 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(); + static std::size_t DrawUploadLimit(); void WriteBack(); // Deferred completion: MarkGpuWrites registers the results the recorded work leaves on the GPU // (storage images stay there; buffer ranges are noted so CPU reads wait); WriteBackBuffers runs @@ -160,10 +221,11 @@ public: void MarkGpuWrites(Recorder& recorder); void WriteBackBuffers(); // Whether WriteBackBuffers has anything the CPU must see (copied written buffers, BDA faults). - bool NeedsCompletion() const { return bda != nullptr || guestMemory.HasCopiedWrites(); } + bool NeedsCompletion() const { return (bda != nullptr && faultChecks) || guestMemory.HasCopiedWrites(); } // 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); } @@ -173,11 +235,16 @@ public: // The recorder's hazard tracker inputs (Recorder::NoteAccess): the written elements' guest // ranges, the regions read in place, and the storage images with whether each is written. const std::vector>& GpuWrites() const { return guestMemory.Writes(); } + const std::vector>& WriteOnlyRanges() const { return writeOnlyRanges; } std::vector> InPlaceReads() const { return guestMemory.InPlaceReads(); } + void InPlaceReads(std::vector>& out) const { guestMemory.InPlaceReads(out); } std::vector> StorageImages() const; std::vector> DeviceReads() const { return guestMemory.DeviceReads(); } + void DeviceReads(std::vector>& out) const { guestMemory.DeviceReads(out); } bool ReadsImage(const StorageTexture* image) const; bool HasCachedStorageImages() const; + bool HoldsReleasedTexture() const; + std::uint32_t TableElements() const { return tableElements[0] + tableElements[1]; } const std::vector& LayoutKey() const { return layoutKey; } const std::vector& FragmentLayoutKey() const { return fragmentLayoutKey; } // Debug aid: each bound guest resource with the fraction of sampled bytes that are nonzero. @@ -195,6 +262,8 @@ public: // (their count and size remain): a compute template then serves dispatches whose constants // differ, and the hit refreshes its data buffers with the dispatch's words (RefreshData). static std::vector ContentKey(const CompiledShader& shader, bool dataWords = true, bool movableBuffers = false); + static void AppendContentKey(const CompiledShader& shader, bool dataWords, bool movableBuffers, std::vector& key); + static bool DiffContentKeys(std::span previous, std::span key, bool draw, ContentKeyDiff& diff); // Records the shader's ShaderData and FlattenedSrt words into this object's data buffers // (vkCmdUpdateBuffer, a transfer write the caller's pre-dispatch barrier makes visible; a // buffer already holding the words is left alone). Returns whether anything was recorded. With @@ -204,8 +273,12 @@ public: // ranges are fixed by the build), so a Revalidate's collects on the same worker are memo hits. void PrecollectSurfaces() const; bool Reusable() const { return reusable; } + void KeepStorageImageBytes(Recorder& recorder) const; + 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; } + static std::size_t LookupLogCapacity(); bool ChecksFaults() const { return bda != nullptr; } void CheckFaults() const; // `shaders` are the stages the object was built from, in build order (a recorded draw's vertex @@ -235,7 +308,7 @@ public: bool DataWordsDiffer(const CompiledShader& shader) const; // Why the fast proof of a Revalidate left the object to the full walk (the [rescache] // revalidate line's reasons); Count: it did not. - enum class FastFail : std::size_t { NoRecord, Collect, Pending, Evicted, Changed, Keys, ClearedView, StorageKeys, Depth, Count }; + enum class FastFail : std::size_t { NoRecord, Collect, Pending, Evicted, Changed, Keys, ClearedView, StorageKeys, Depth, Replaced, Audit, Count }; // Why a Pending failure was left to the full walk instead of the own-object refresh (T1, see // refreshOwnObjects); Count: it was not. enum class OwnRefreshFallback : std::size_t { Disabled, Snapshot, Keys, ForeignView, SurfaceKey, NotImported, Uncached, Rerun, Count }; @@ -259,11 +332,14 @@ 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; bool refreshData(VkCommandBuffer commands, const CompiledShader& shader, Recorder* recorder); + std::optional shaderDataAllocation(const CompiledShader& shader) const; + VkBuffer shaderDataBuffer(const CompiledShader& shader) const; + void forgetShaderDataWords(const CompiledShader& shader); struct DescribedRange { const char* kind; std::uint64_t address; @@ -273,6 +349,7 @@ private: std::uint32_t format = 0; int tileMode = 0; std::uint64_t dccAddress = 0; + bool imageTable = false; }; std::vector describedRanges; struct Allocation { @@ -283,6 +360,7 @@ private: ShaderRecompiler::DescriptorRole role = ShaderRecompiler::DescriptorRole::ShaderData; // Guest buffers: whether the shader may store to the range (see addGuestBuffer). bool written = true; + bool writeOnly = false; // Data buffers: the words the buffer holds once the recorded work ran (see RefreshData). std::vector dataWords; std::uint32_t adjustment = 0; @@ -301,13 +379,45 @@ private: VkDescriptorSetLayoutBinding layout; std::vector allocations; std::vector imageAllocations; + VkDescriptorBindingFlags flags = 0; + std::int32_t set = -1; }; + struct TableStage { + ShaderRecompiler::ShaderStage stage = ShaderRecompiler::ShaderStage::Compute; + const ShaderRecompiler::RecompileResult* program = nullptr; + const ShaderRecompiler::DescriptorBinding* images = nullptr; + std::size_t mapAllocation = std::numeric_limits::max(); + std::vector map; + std::vector poison; + std::vector messages; + std::uint32_t elementBase = 0; + 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); + void addSamplerTable(const ShaderRecompiler::DescriptorBinding& binding, VkShaderStageFlags flags, TableStage& table); + void resolveImageTables(); + void finishImageTables(); + // A host-imported buffer region the set reads in place, with its import's identity at build time. struct DirectRegion { std::uint64_t begin; std::uint64_t end; std::uint64_t serial; + std::uint64_t regionEnd = 0; + std::uint64_t committedEnd = 0; }; // The build is two stages. A (buildPrepare, no device lock): the binding plan with its layout @@ -324,7 +434,8 @@ private: // `written` is the element's DescriptorBinding::bufferWritten: a read-only element binds the // 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::size_t addGuestBuffer(std::span words, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes, bool written, bool atomic, bool read, std::uint32_t extent); + 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). @@ -360,6 +471,7 @@ private: struct PreviousSampled { std::array words{}; bool depthCompare = false; + VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D; std::shared_ptr texture; }; PreviousSampled previousSampled; @@ -435,8 +547,12 @@ private: GuestBufferMemory guestMemory; std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes(); std::unique_ptr bda; + std::vector srtGuardFaults; bool usesBda = false; + bool bdaReadOnly = true; + std::vector multisampleSources; bool usesFaultBuffer = false; + bool faultChecks = false; VkDescriptorSetLayout _layout = VK_NULL_HANDLE; // Whether the layout is this object's own (no cache) and destroyed with it. bool ownsLayout = false; @@ -454,14 +570,35 @@ private: bool splitSets = false; std::vector fragmentLayoutKey; SetObjects fragment; + struct TableWrites { + bool recorded = false; + bool written = false; + std::uint32_t binding = 0; + std::size_t firstImage = 0; + std::size_t images = 0; + std::vector> runs; + }; + std::array tableWrites; + std::shared_ptr keptTable(std::size_t set, std::shared_ptr>>& held) noexcept; std::uint32_t setOf(const Binding& binding) const; std::vector allocations; std::vector descriptorBuffers; std::vector descriptorImages; std::vector descriptorWrites; + struct MovedWordBuffer { + std::size_t allocation; + std::vector words; + std::size_t size; + std::shared_ptr buffer; + }; + mutable std::vector movedWordBuffers; + mutable std::size_t movedWordBufferNext = 0; + mutable std::vector drawPoolSizes; // Guest buffer elements bound read-only: each use of this object skips that many pending-write // notes (counted in MarkGpuWrites for the [buffers] line). std::size_t readOnlyBuffers = 0; + std::chrono::steady_clock::time_point lastFullWalk = std::chrono::steady_clock::now(); + std::vector> writeOnlyRanges; bool drawBuild = false; std::uint64_t dispatchThreads = 0; std::vector> textures; @@ -469,6 +606,7 @@ private: std::vector textureBaseLevel; std::vector textureSrgb; std::vector> storageTextures; + std::vector multisampleStorageViews; std::vector storageMips; std::vector storageKeys; std::vector storageFirstLayer; @@ -567,10 +705,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; @@ -581,6 +729,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/Shaders.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp index 76e24b0e23..2ddac45e80 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp @@ -73,7 +73,7 @@ inline VkShaderStageFlags PushConstantStages(std::span sha VkShaderStageFlags result = 0; for (const auto& shader : shaders) { Require(shader.program != nullptr, "missing compiled shader"); - if (!shader.program->pushConstants.empty() || shader.stage == ShaderRecompiler::ShaderStage::Mesh || shader.stage == ShaderRecompiler::ShaderStage::TessellationControl) result |= VulkanStage(shader.stage); + if (!shader.program->pushConstants.empty() || shader.program->readsDepthRemapPush || shader.stage == ShaderRecompiler::ShaderStage::Mesh || shader.stage == ShaderRecompiler::ShaderStage::TessellationControl) result |= VulkanStage(shader.stage); } return result; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp new file mode 100644 index 0000000000..32de92284c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp @@ -0,0 +1,42 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SLIMMUTEX_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SLIMMUTEX_HPP + +#include +#include +#if defined(__x86_64__) || defined(_M_X64) +#include +#endif + +namespace AgcDriver::Graphics { + +class SlimMutex { +public: + SlimMutex() = default; + SlimMutex(const SlimMutex&) = delete; + SlimMutex& operator=(const SlimMutex&) = delete; + + void lock() { + for (unsigned spins = 0;;) { + if (!locked.exchange(true, std::memory_order_acquire)) return; + while (locked.load(std::memory_order_relaxed)) { + if (spins < 128) { + ++spins; +#if defined(__x86_64__) || defined(_M_X64) + _mm_pause(); +#endif + } else { + std::this_thread::yield(); + } + } + } + } + bool try_lock() { return !locked.load(std::memory_order_relaxed) && !locked.exchange(true, std::memory_order_acquire); } + void unlock() { locked.store(false, std::memory_order_release); } + +private: + std::atomic locked{false}; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index b3e056cd72..2008d77be3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -7,6 +7,7 @@ #include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" #include #include +#include #include #include #include @@ -66,6 +67,9 @@ struct ColorTarget { bool uintExport = false; std::uint32_t pipeBankXor = 0; std::uint32_t samples = 1; + std::uint32_t dccControl = 0; + std::uint64_t fmaskAddress = 0; + std::uint32_t fmaskPipeBankXor = 0; }; struct DepthTarget { @@ -78,6 +82,14 @@ struct DepthTarget { std::uint64_t htileAddress = 0; bool htileStencil = false; std::uint32_t samples = 1; + std::uint32_t depthSwizzle = 0; + std::uint32_t pipeBankXor = 0; + std::uint32_t firstSlice = 0; + std::uint32_t lastSlice = 0; + bool iterate256 = false; + bool htilePipeAligned = false; + std::uint32_t htileVrsEncoding = 0; + bool htileDepthOnly = false; }; struct State { @@ -99,12 +111,15 @@ struct State { bool stencilTest = false; VkStencilOpState stencilFront{}; VkStencilOpState stencilBack{}; + std::optional stencilReplayReference; ColorTarget color; std::vector colors; std::vector blends; bool hasColorTarget; bool rectList = false; + bool quadList = false; bool dualSourceBlend = false; + std::uint32_t reversedBlend = 0; VkExtent2D renderExtent; std::uint32_t renderLayers = 1; std::uint32_t layerExports = 0; @@ -113,19 +128,40 @@ struct State { VkViewport viewport; bool negativeOneToOne; bool depthClamp = false; + float depthScale = 1; + float depthOffset = 0; + float depthClampMin = 0; + float depthClampMax = 1; + struct DepthRemap { + std::uint32_t mode = 0; + float scale = 1; + float offset = 0; + bool operator==(const DepthRemap&) const = default; + } depthRemap; bool alphaToCoverage = false; bool ditheredAlphaToCoverage = false; + bool perSampleShading = false; + bool mergedCoverage = false; + bool truncatedCoverage = false; VkConservativeRasterizationModeEXT conservativeRasterization = VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT; VkRect2D scissor; VkCullModeFlags cullMode; + VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL; VkFrontFace frontFace; VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; VkPipelineColorBlendAttachmentState blend; std::array blendConstants; std::uint32_t samples = 1; - std::array sampleLocations{}; + std::array sampleLocations{}; bool customSampleLocations = false; + bool sampleLocationGrid = false; std::uint32_t paClVsOutCntl = 0; + bool vertexPoints = false; + float pointSize = 1.0f; + float pointSizeMin = 0.0f; + float pointSizeMax = 0.0f; + std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; + bool pixelEarlyZ = false; }; // PA_CL_VS_OUT_CNTL bits that place position exports (clip and cull distances, the misc and the two @@ -133,23 +169,54 @@ struct State { inline constexpr std::uint32_t PositionExportLayoutBits = 0xffffu | (1u << 21u) | (1u << 22u) | (1u << 23u); ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false); +void ApplyPointSize(const State& state, ShaderRecompiler::ShaderVertexStageInfo& vertex); State DecodeState(const QueueState& queue, bool vertexWorkgroup = false); -std::array SampleLocations(const State& state); +void ApplyDepthRemap(const Context& context, State& state); +void NormalizeUnusedViewportDepth(State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel); +std::array SampleLocations(const State& state); +std::array PixelSampleLocations(const State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel); +VkExtent2D SampleLocationGrid(const State& state); +using SampleOrder = std::array; +inline constexpr std::uint32_t KeptSampleOrder = 0x76543210u; +std::optional SameSetSampleOrder(const State& state); +bool ReordersSamples(const SampleOrder& order); +std::uint32_t InvertSampleOrder(std::uint32_t order); +std::uint32_t HostSampleBits(std::uint32_t guestBits, std::uint32_t order); +bool SampleMaskFollowsOrder(const State& state, const SampleOrder& order); +std::array QuadSampleMasks(const State& state, const SampleOrder& order); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); -ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot); +bool PartialSampleMask(const State& state); +bool UniformSampleMask(const State& state); +VkSampleMask HostSampleMask(const State& state); +std::uint8_t QuadPixelMask(const State& state); +std::array QuadSampleMasks(const State& state); +ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot, bool allowMultisampleDcc = false); +struct ReversedBlend { + VkPipelineColorBlendAttachmentState blend; + std::uint32_t shaderCode; +}; +ReversedBlend TranslateReversedBlend(std::uint32_t blendControl, std::uint8_t componentMapping, VkFormat format, bool forceDestinationAlphaOne); std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height); std::uint32_t ColorWriteMask(const Registers& context); GuestTextureResource SurfaceForTarget(const ColorTarget& color); struct ColorMetadataPass { - enum class Mode { EliminateFastClear, DccDecompress, Resolve }; + enum class Mode { EliminateFastClear, DccDecompress, Resolve, FmaskDecompress }; Mode mode; std::vector targets; std::optional source; }; std::optional DecodeColorMetadataPass(const QueueState& queue); +struct DepthCopyPass { + std::uint64_t depthAddress; + VkExtent2D extent; + std::uint32_t samples; + std::uint32_t sample; + ColorTarget color; +}; +std::optional DecodeDepthCopyPass(const QueueState& queue); std::string DepthMaintenanceRejection(const QueueState& queue); bool DepthResummarizeDrawsNothing(const QueueState& queue); // The message DecodeState (or the pixel stage decode after it) would throw for the register rules @@ -170,7 +237,27 @@ struct RegisterRead { std::uint32_t offset; }; std::vector*& RegisterReadLog(); +std::atomic& RegisterReadLogsOpen(); +class RegisterReadLogScope { +public: + explicit RegisterReadLogScope(std::vector* log) : active(log != nullptr) { + if (!active) return; + RegisterReadLogsOpen().fetch_add(1, std::memory_order_relaxed); + RegisterReadLog() = log; + } + ~RegisterReadLogScope() { + if (!active) return; + RegisterReadLog() = nullptr; + RegisterReadLogsOpen().fetch_sub(1, std::memory_order_relaxed); + } + RegisterReadLogScope(const RegisterReadLogScope&) = delete; + RegisterReadLogScope& operator=(const RegisterReadLogScope&) = delete; + +private: + bool active; +}; inline void NoteRegisterRead(RegisterBank bank, std::uint32_t offset) { + if (RegisterReadLogsOpen().load(std::memory_order_relaxed) == 0) return; if (auto* log = RegisterReadLog()) log->push_back({bank, offset}); } inline const char* RegisterBankName(RegisterBank bank) { @@ -189,15 +276,15 @@ struct DrawKeyRange { std::uint32_t first; std::uint32_t count; }; -inline constexpr std::array DrawKeyRegisters{{ +inline constexpr std::array DrawKeyRegisters{{ {RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 1}, {RegisterBank::Context, 0x005, 1}, {RegisterBank::Context, 0x007, 7}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 5}, {RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10b, 3}, {RegisterBank::Context, 0x10f, 6}, - // SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL, SPI_SHADER_POS/Z/COL_FORMAT, + // SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL .. SPI_BARYC_CNTL, SPI_SHADER_POS/Z/COL_FORMAT, // CB_BLEND0..7_CONTROL, GE_MAX_OUTPUT_PER_SUBGROUP. - {RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 1}, {RegisterBank::Context, 0x1c3, 3}, {RegisterBank::Context, 0x1e0, 8}, {RegisterBank::Context, 0x1ff, 1}, + {RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 3}, {RegisterBank::Context, 0x1c3, 3}, {RegisterBank::Context, 0x1e0, 8}, {RegisterBank::Context, 0x1ff, 1}, // DB_DEPTH_CONTROL .. PA_CL_VS_OUT_CNTL, PA_SC_MODE_CNTL_0/1, VGT_GS_MODE, VGT_GS_VERT_ITEMSIZE, // PA_SU_VTX_CNTL, the sample masks, PA_SC_CONSERVATIVE_RASTERIZATION_CNTL. - {RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2de, 6}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x2fe, 16}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1}, + {RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2af, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2de, 6}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x2fe, 16}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1}, // CB_COLOR0..7_BASE .. DCC_BASE (15 words a slot), CB_COLOR0..7_BASE_EXT, DCC_BASE_EXT, ATTRIB2, ATTRIB3. {RegisterBank::Context, 0x318, 0x78}, {RegisterBank::Context, 0x390, 8}, {RegisterBank::Context, 0x398, 8}, {RegisterBank::Context, 0x3a8, 0x18}, // The pixel program address, RSRC2 and user words; the geometry-back user pointer and program @@ -208,6 +295,8 @@ inline constexpr std::array DrawKeyRegisters{{ {RegisterBank::UserConfig, 0x242, 1}, {RegisterBank::UserConfig, 0x24b, 1}, {RegisterBank::UserConfig, 0x25b, 1}, }}; bool SplitsAlternateAlpha(const State& state); +bool StencilOperationValueReplaces(const Registers& context); +bool StencilReferenceUnused(const State& state); // Whether DrawKeyRegisters holds the read. bool DrawKeyCovers(RegisterRead read); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 0524592a6c..87a9416e87 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -22,6 +22,7 @@ namespace AgcDriver::Graphics { +class Buffer; class ResidentColor; class CommandBatch; class StorageTexture; @@ -33,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); @@ -44,13 +46,13 @@ struct OwnedImage { OwnedImage(const Context& context, VkImage image, VkDeviceMemory memory) : context(context), image(image), memory(memory) {} OwnedImage(const OwnedImage&) = delete; OwnedImage& operator=(const OwnedImage&) = delete; - ~OwnedImage() { - if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); - if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); - } + ~OwnedImage(); + void Allocate(const Context& device, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation); Context context; VkImage image; VkDeviceMemory memory; + VkDeviceSize memoryBytes = 0; + std::uint32_t memoryType = 0; }; class Texture { @@ -61,14 +63,20 @@ public: // compute pass writing it and the next pass sampling it share one image and copy nothing. // CanCopyFrom says whether the two descriptors address the same surface compatibly. Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components); - Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D); + Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0, std::shared_ptr owner = {}, VkImageUsageFlags usage = 0); struct DepthCopyTag {}; Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag); + struct SrgbExpandTag {}; + Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, SrgbExpandTag); + static bool CanSrgbExpandFrom(const Context& context, const StorageTexture& source, const GuestTextureResource& descriptor); static VkFormat DepthCopyFormat(VkFormat colorFormat); static bool CanDepthCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); struct BlockCopyTag {}; Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, BlockCopyTag); static bool CanBlockCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); + struct RepackCopyTag {}; + Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, RepackCopyTag); + static bool CanRepackCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); bool IsDepthCopy() const { return perUseCopy == PerUseCopy::RefreshedOnReuse; } bool RefreshDepthCopy(); struct DepthCopyCounts { @@ -108,10 +116,13 @@ public: PerUseCopy CopyKind() const { return perUseCopy; } bool RefreshedPerUse() const { return perUseCopy == PerUseCopy::RemadePerUse; } void MarkRefreshedPerUse() { perUseCopy = PerUseCopy::RemadePerUse; } + bool Released() const { return released.load(std::memory_order_acquire); } + void MarkReleased() { released.store(true, std::memory_order_release); } private: void release() noexcept; bool refreshBlockCopy(); + bool refreshSrgbExpand(); void createFirstLayerView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); void prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); void prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); @@ -141,10 +152,14 @@ private: std::shared_ptr storageSource; std::unique_ptr upload; PerUseCopy perUseCopy = PerUseCopy::None; + std::atomic released{false}; bool copiedOnce = false; + bool srgbExpand = false; + GuestTextureResource sourceDescriptor{}; std::uint64_t copiedVersion = 0; VkFormat depthFormat = VK_FORMAT_UNDEFINED; VkFormat blockFormat = VK_FORMAT_UNDEFINED; + bool repackCopy = false; std::uint32_t copyWidth = 0, copyHeight = 0, copyLevels = 0, copyLayers = 0, copyElementBytes = 0; }; @@ -182,8 +197,14 @@ public: void WriteBack(); // Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead). void MarkDirty(); - void MarkSliceDirty(std::uint32_t slice); + void MarkWholeWritten(); + void MarkUnitsWritten(const std::vector& units); + std::vector UnitRects(const std::vector& units) const; + bool BlockUnits() const { return blockUnits; } + std::uint32_t TrackedUnits() const { return trackedLayers; } + void MarkSliceDirty(std::uint32_t slice, std::uint32_t count = 1); void Flush(); + bool Dirty() const; // Stores every pending image overlapping the range, except `except`; returns whether any was. // 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 @@ -193,16 +214,21 @@ public: // overlapping pending image, as before). static bool FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except = nullptr, const char* reason = "memory access", PublishScope scope = PublishScope::Whole, bool* published = nullptr); static void FlushAllPending(const char* reason); + static bool WaitForFlushing(std::uint64_t address, std::size_t bytes, const StorageTexture* except = nullptr); static void SaveBeforeUnwatch(std::uint64_t address, std::size_t bytes); + static constexpr std::uint64_t SmallStoreBytes = 64 * 1024; + static std::size_t StoreSmallPending(const char* reason); static bool AnyPending(); static void FlushDumpSelected(const char* reason); static bool StoreAtFlipRequested(const char* value); + static bool StoreAtLabelRequested(const char* value); // See PendingSerial: a change of a surface's source outside the registry (a unit shadow // retile) moves it too. static void BumpPendingSerial(); // Whether an image with results pending, or one a FlushPending is storing right now, overlaps // any of the [begin, end) ranges: one acquisition of the registry's mutex. static bool AnyPendingOverlaps(std::span> ranges); + static bool AnyLiveOverlaps(std::span> ranges); // Moves with every change of the pending registry (an image marked dirty, stored, flushed or // dropped; after the store for a flush), bumped under the registry's mutex with the change, so // a reader that loads it before and after its own reads knows the registry it consulted stayed @@ -318,6 +344,7 @@ public: // may carry DCC metadata, and this surface must be host-imported (the write-back is GPU-direct). // False, naming why, when the copy must be a transfer. bool CopyFrom(StorageTexture& source, const char*& refusal); + static const char* CopyRefusal(const Context& context, const StorageTexture* destination, const StorageTexture& source, std::uint64_t address); VkImage Image() const { return image; } const GuestTextureResource& Descriptor() const { return descriptor; } std::uint32_t ImageLayers() const { return geometry.imageLayers; } @@ -327,6 +354,7 @@ public: // validates textures copied from this image. std::uint64_t Version() const { return version; } std::uint64_t Generation() const { return generation; } + std::uint64_t GenerationFor(std::uint64_t address, std::uint64_t bytes) const; // The DCC keys the content was uploaded under and the last scan of the surface's keys // (ProvedClearKeys): Refresh's own key rule for the fast revalidation. Under // GuestMemory::GpuMutex only, as Refresh is; never from a build's stage A. @@ -341,10 +369,16 @@ public: // was still current (nothing uploaded). // Keeps the image current with guest memory (see GuestMemory::CollectWrites). bool Refresh(); + bool RefreshLayers(std::uint32_t first, std::uint32_t count); static std::uint64_t SinglePassMoves(); + static std::uint64_t SlabRetiles(); static std::uint64_t RefreshesProved(); + static std::uint64_t ClearHandOvers(); + static std::uint64_t HandedOverSuperseded(); + static void SetClearHandOver(bool enabled); static std::uint64_t UntrackedCompares(); std::uint64_t GuestBytes() const; + std::uint64_t BudgetBytes() const; std::size_t OriginalSnapshotBytes() const { return original.capacity(); } VkDeviceSize AllocationBytes() const { return memoryBytes; } @@ -352,9 +386,8 @@ private: StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel); // The regions of every array layer, or of the tracked layers `layers` selects. std::vector CopyRegions(const std::vector* layers = nullptr) const; - // Uploads the surface, or only the tracked layers `layers` selects (the direct path; the others - // upload everything). void upload(const std::vector* layers = nullptr); + std::vector cpuUploadLayers(const std::vector& units) const; void captureGuestBytes(const std::vector* layers); bool originalHolds(std::uint64_t address, std::size_t bytes) const; bool compareUntracked(std::uint64_t address, std::size_t bytes, std::span changed, bool memoize = false) const; @@ -372,6 +405,17 @@ private: // Drops the saved guest bytes of the units over [address, address + bytes) (another image's fill // or copy supersedes them there); the other units' bytes stay valid. void forgetSavedOver(std::uint64_t address, std::size_t bytes); + // A write-back stored on the GPU over memory outside the write watch: the stored units' bytes are + // copied from its retile scratch into `savedBytes` behind the store and become `original` once + // the batch completed (`savedTicket` names that write-back per unit; anything that replaces or + // drops a unit's saved bytes first clears it). + std::vector prepareStoredSave(const std::vector& layers, const std::vector& skippedLayer); + void commitStoredSave(const std::vector& units); + void applyStoredSave(std::uint64_t ticket, const std::vector& units); + void cancelStoredSave(std::uint32_t unit); + void cancelStoredSaves(); + // Lets the batches holding the saves of the units `layers` selects (every unit for none) complete. + void awaitStoredSave(const std::vector* layers); bool guestBytesSettled() const; // Tracked units as 64 KiB write-stamp blocks (`blockUnits`: a thin tiled surface at a 64 KiB // aligned base; APS5_NO_BLOCK_TRACKING=1 tracks array layers as above instead): a fill of one @@ -417,13 +461,20 @@ private: std::vector> importReads; }; PaddingSeeds paddingSeeds(const HostImport& import, std::vector copied) const; + struct ImportSegment { + const HostImport* import; + std::uint64_t begin; + std::uint64_t end; + }; + std::vector importSegments() const; + PaddingSeeds paddingSeeds(std::span segments, const std::vector& copied) const; // The direct upload and GPU-direct write-back of the runs through their windows (block units); // both return the bytes moved. The upload reads each unit from its unit shadow while fresh, // else from the import (`discard`: the image has no layout yet, a whole-surface first upload). // The write-back retiles into the import's unit shadow where a slab takes the piece // (`shadowed` receives those guest ranges, `imported` the pieces written to the import). std::uint64_t uploadWindows(const HostImport& import, std::span> runs, bool discard = false); - std::uint64_t writeBackWindows(const HostImport& import, std::span> keep, std::uint64_t firstStored, std::uint64_t lastStored, std::vector& shadowed, std::vector>& imported); + std::uint64_t writeBackWindows(std::span segments, std::span> keep, std::uint64_t firstStored, std::uint64_t lastStored, std::vector& shadowed, std::vector>& imported, Buffer* save = nullptr); // Per-layer validity (thin array surfaces whose layers are 64 KiB-aligned guest slices; every // other surface is one tracked layer): a layer's generation is the write generation its guest // bytes were last known to match the image at, and a pending layer holds results (a clear, a @@ -431,7 +482,10 @@ private: // layer generation (the whole-surface fast check), `dirty` whether any layer is pending. std::uint64_t layerBegin(std::uint32_t layer) const { return descriptor.baseAddress + static_cast(layer) * trackedLayerBytes; } bool anyLayerPending() const; + bool refresh(std::uint32_t firstLayer, std::uint32_t lastLayer); + void noteLayersBehind(std::uint32_t firstLayer, std::uint32_t lastLayer, const std::vector& outside); void refreshGeneration(); + std::uint64_t collectedLayerGeneration(std::uint32_t layer, std::uint64_t whole, bool uncached = false) const; bool refreshProved(); void takeRefreshProof(bool aliased); bool otherPendingOverlaps() const; @@ -463,6 +517,8 @@ private: // stays responsible for storing them until this image's own results supersede them // (markLayersPending). APS5_NO_ALIAS_BORROW=1 stores and re-reads as before. std::shared_ptr pendingAlias() const; + std::optional clearHandOver(); + void supersedeHandedOver(std::uint32_t first, std::uint32_t count); // The pending images FlushPending(address, bytes) would store (not the one a Refresh validates, // a pending unit inside the range), kept alive across the caller's use. static std::vector> overlappingPending(std::uint64_t address, std::size_t bytes); @@ -507,7 +563,13 @@ private: std::uint64_t sliceLinearBytes = 0; SurfaceGeometry geometry; std::vector original; - mutable std::array comparedGuestBytes{}; + struct ComparedRange { + std::uint64_t address = 0; + std::size_t bytes = 0; + std::array stamp{}; + }; + mutable std::array comparedRanges{}; + mutable std::uint32_t nextComparedRange = 0; std::vector generationBaseline; // DCC keys the image content was uploaded under: a fast-cleared surface starts as its clear value. DccKeys uploadedKeys = DccKeys::Uncompressed; @@ -533,6 +595,7 @@ private: std::uint64_t trackedLayerBytes = 0; std::vector layerGeneration; std::vector layerPending; + std::vector layerBehind; bool blockUnits = false; // Write-back hysteresis (block units, see writeBack): partial stores in the current window of // presents, its first present, and the present until which every pending unit is stored. @@ -543,6 +606,8 @@ private: std::weak_ptr borrowedFrom; std::uint64_t borrowedVersion = 0; std::vector borrowedUnits; + std::vector, std::uint64_t>> handedOver; + std::vector handedUnits; // Units of this image were borrowed since it was last written: the borrower's proofs // (the resource cache's serial memo) hold only while this image's version does, so the next // write here bumps the pending serial although the image was pending already. @@ -550,6 +615,7 @@ private: VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; VkDeviceSize memoryBytes = 0; + std::uint32_t memoryType = 0; VkImageView view = VK_NULL_HANDLE; std::uint32_t defaultMip = 0; std::map extraViews; @@ -568,9 +634,13 @@ private: // Results are on the GPU only (guarded by the pending-write registry lock). bool dirty = false; std::uint64_t version = 0; + std::uint64_t writes = 0; // `original` holds the guest bytes; false after a GPU-side clear, which never read them. std::vector layerOriginal; std::map keptAtUnwatch; + std::shared_ptr savedBytes; + std::vector savedTicket; + std::uint64_t lastSaveTicket = 0; // The storage cache let the image go (Flush): a lookup makes a new image of the surface, so // no fill may put results into this one (guarded by the live-image registry lock). bool released = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp index 2efc3bc467..e0dc3531e4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp @@ -49,16 +49,30 @@ namespace AgcDriver::Graphics { // leaving the other elements of both buffers alone. void Dispatch(VkCommandBuffer commands, TextureTileMode tileMode, std::uint32_t elementBytes, VkBuffer source, std::uint64_t sourceOffset, VkBuffer destination, std::uint64_t destinationOffset, const TileMipLayout& layout, bool retile = false, std::uint32_t slice = 0, bool thick = false, const DetileWindow& window = {}); void DispatchImage(VkCommandBuffer commands, TextureTileMode tileMode, std::uint32_t elementBytes, VkBuffer tiled, std::uint64_t tiledOffset, VkImageView view, const TileMipLayout& layout, bool retile = false, std::uint32_t slice = 0, const DetileWindow& window = {}); + void DispatchRepack(VkCommandBuffer commands, VkBuffer source, std::uint64_t sourceOffset, VkBuffer destination, std::uint64_t destinationOffset, std::uint64_t bytes); static VkFormat ImageElementFormat(std::uint32_t elementBytes); // Recycles the descriptor sets of the previous batch; call before recording a new command batch. void BeginBatch(); void DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask, std::uint64_t cmaskOffset, std::size_t cmaskBytes, VkImageView view, std::uint32_t width, std::uint32_t height, std::uint32_t elementBytes, const std::array& clearWords, bool write); std::uint32_t CmaskErrors(); + void DispatchMultisampleReadback(VkCommandBuffer commands, VkFormat format, VkBuffer destination, std::uint64_t offset, std::uint64_t bytes, VkImageView arrayView, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t fragments, std::uint32_t firstElement, std::uint32_t elements); + void DispatchDepthSample(VkCommandBuffer commands, VkImageView depth, bool multisampled, VkImageView target, std::uint32_t width, std::uint32_t height, std::uint32_t sample); + void DispatchMultisampleImage(VkCommandBuffer commands, std::uint32_t elementBytes, VkBuffer source, std::uint64_t offset, std::uint64_t bytes, VkImageView arrayView, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t fragments, std::uint32_t firstElement, std::uint32_t elements); private: + VkPipeline multisampleReadbackPipeline(std::uint32_t elementBytes, bool depth); + VkPipeline multisampleUploadPipeline(std::uint32_t elementBytes); VkPipeline pipeline(TextureTileMode tileMode, std::uint32_t elementBytes, bool retile, bool thick, bool image = false); void release() noexcept; + struct DescriptorRing { + std::vector pools; + std::vector serials; + std::size_t current = 0; + std::uint32_t used = 0; + }; VkDescriptorSet allocateSet(bool image = false); + VkDescriptorSet allocateFrom(DescriptorRing& ring, VkDescriptorSetLayout layout, const std::vector& sizes, const char* createWhat, const char* allocateWhat, const char* resetWhat); + void resetRing(DescriptorRing& ring, const char* resetWhat); void checkWindow(const TileMipLayout& layout, const DetileWindow& window, std::uint32_t& columnEnd, std::uint32_t& rowEnd) const; const Context context; @@ -66,19 +80,29 @@ namespace AgcDriver::Graphics { VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; VkShaderModule module = VK_NULL_HANDLE; std::vector> pipelines; - std::vector descriptorPools; - std::size_t allocatedSets = 0; + DescriptorRing descriptorSets; VkDescriptorSetLayout cmaskDescriptorLayout = VK_NULL_HANDLE; VkPipelineLayout cmaskPipelineLayout = VK_NULL_HANDLE; VkPipeline cmaskPipeline = VK_NULL_HANDLE; - std::vector cmaskDescriptorPools; - std::size_t allocatedCmaskSets = 0; + DescriptorRing cmaskDescriptorSets; + VkDescriptorSetLayout depthSampleLayout = VK_NULL_HANDLE; + VkPipelineLayout depthSamplePipelineLayout = VK_NULL_HANDLE; + std::array depthSamplePipelines{}; + VkSampler depthSampler = VK_NULL_HANDLE; + DescriptorRing depthSampleSets; std::unique_ptr cmaskErrors; VkDescriptorSetLayout imageDescriptorLayout = VK_NULL_HANDLE; VkPipelineLayout imagePipelineLayout = VK_NULL_HANDLE; VkShaderModule imageModule = VK_NULL_HANDLE; - std::vector imageDescriptorPools; - std::size_t allocatedImageSets = 0; + VkShaderModule repackModule = VK_NULL_HANDLE; + VkPipeline repackPipeline = VK_NULL_HANDLE; + DescriptorRing imageDescriptorSets; + VkDescriptorSetLayout readbackDescriptorLayout = VK_NULL_HANDLE; + VkPipelineLayout readbackPipelineLayout = VK_NULL_HANDLE; + std::array readbackModules{}; + std::vector> readbackPipelines; + DescriptorRing readbackDescriptorSets; + VkShaderModule multisampleUploadModule = VK_NULL_HANDLE; }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp index fc7be9ec72..a2625c2e6b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp @@ -16,11 +16,16 @@ VkFormat ResolveTextureFormat(std::uint32_t guestFormat); VkComponentSwizzle TextureComponentChannel(std::uint32_t guestFormat, VkComponentSwizzle component); std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical); VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat); +std::uint32_t SrgbCopyFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical, std::uint32_t decodeFormats); +bool SrgbExpandsToRgba8(const Context& context, std::uint32_t guestFormat); bool IsSrgbTextureFormat(std::uint32_t guestFormat); +bool IgnoresForcedDegamma(std::uint32_t guestFormat); std::optional FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes); std::optional FindGuestColorTargetFormat(VkFormat format, std::uint32_t elementBytes); std::uint32_t BytesPerElement(std::uint32_t guestFormat); bool IsConvertedTextureFormat(std::uint32_t guestFormat); +bool IsRepackedTextureFormat(std::uint32_t guestFormat); +std::uint32_t RepackTextureWord(std::uint32_t word); bool IsBlockCompressed(std::uint32_t guestFormat); std::uint32_t BlockWidth(std::uint32_t guestFormat); std::uint32_t BlockHeight(std::uint32_t guestFormat); 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/TextureTiling.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp index b83d4f4f89..5699bf78e1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp @@ -75,6 +75,8 @@ struct SurfaceGeometry { }; SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor); bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t levels); +GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed); +GuestTextureResource ArrayLayerWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp index 8da4772769..aad386bfc1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp @@ -3,6 +3,10 @@ #include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" #include +#include +#include +#include +#include #include #include #include @@ -132,22 +136,110 @@ inline VertexInputLayout BuildVertexInputLayout(const Context& context, std::spa return result; } -// 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 void ValidateVertexAddresses(std::span attributes) { + for (const auto& attribute : attributes) { + if (NullVertexDescriptor(attribute)) continue; + const auto& fields = attribute.resource.fields; + const auto address = fields[0] | (static_cast(fields[1] & 0xffffu) << 32u); + if (address == 0 || address % DecodeVertexFormat(attribute).alignment != 0) Require(false, "unaligned vertex buffer"); + } +} + +class VertexInputCache { +public: + explicit VertexInputCache(const Context& context, std::size_t capacity = 1024) : context(context), capacity(capacity) { + Require(capacity != 0, "vertex input cache capacity is zero"); + } + + std::shared_ptr TryGet(std::uint64_t variant, std::span attributes) { + if (attributes.empty() || variant == 0 || bypassed(variant)) return nullptr; + return get(variant, attributes); + } + + std::size_t Size() const { + std::lock_guard lock(mutex); + return entries.size(); + } + +private: + std::shared_ptr get(std::uint64_t variant, std::span attributes) { + std::lock_guard lock(mutex); + const auto found = entries.find(variant); + if (found != entries.end()) { + if (!sameShape(attributes, found->second.shape)) { + bypass(variant); + order.erase(found->second.order); + entries.erase(found); + return nullptr; + } + ValidateVertexAddresses(attributes); + order.splice(order.begin(), order, found->second.order); + return found->second.layout; + } + if (bypassed(variant)) return nullptr; + auto layout = std::make_shared(BuildVertexInputLayout(context, attributes)); + std::vector shape(attributes.begin(), attributes.end()); + order.push_front(variant); + try { + entries.emplace(variant, Entry{layout, std::move(shape), order.begin()}); + } catch (...) { + order.pop_front(); + throw; + } + if (entries.size() > capacity) { + bypass(order.back()); + entries.erase(order.back()); + order.pop_back(); + } + return layout; + } + + static bool sameShape(std::span a, std::span b) { + return std::equal(a.begin(), a.end(), b.begin(), b.end(), [](const auto& x, const auto& y) { + return x.location == y.location && x.components == y.components && x.formatComponents == y.formatComponents && x.fetchIndex == y.fetchIndex && + (x.resource.fields[1] & 0xffff0000u) == (y.resource.fields[1] & 0xffff0000u) && x.resource.fields[3] == y.resource.fields[3] && NullVertexDescriptor(x) == NullVertexDescriptor(y); + }); + } + static std::size_t bypassIndex(std::uint64_t variant) { + return static_cast((variant ^ (variant >> 32u)) & 65535u); + } + bool bypassed(std::uint64_t variant) const { + const auto index = bypassIndex(variant); + return (bypassBits[index / 64u].load(std::memory_order_relaxed) & (std::uint64_t{1} << (index % 64u))) != 0; + } + void bypass(std::uint64_t variant) { + const auto index = bypassIndex(variant); + bypassBits[index / 64u].fetch_or(std::uint64_t{1} << (index % 64u), std::memory_order_relaxed); + } + struct Entry { + std::shared_ptr layout; + std::vector shape; + std::list::iterator order; + }; + Context context; + std::size_t capacity; + std::array, 1024> bypassBits{}; + mutable std::mutex mutex; + std::list order; + std::unordered_map entries; +}; + inline std::uint32_t VertexBufferOutOfBoundsSelect(const ShaderRecompiler::VertexAttribute& attribute) { return (attribute.resource.fields[3] >> 28u) & 3u; } inline bool VertexFetchOutOfRange(const ShaderRecompiler::VertexAttribute& attribute) { const auto stride = (attribute.resource.fields[1] >> 16u) & 0x3fffu; + if (stride != 0 || NullVertexDescriptor(attribute)) return false; const auto select = VertexBufferOutOfBoundsSelect(attribute); - return stride == 0 && (select == 2u || select == 3u) && attribute.resource.fields[2] == 0; + const auto records = attribute.resource.fields[2]; + if (select == 0u) return true; + if (select == 3u) return records < DecodeVertexFormat(attribute).bytes; + return records == 0; } inline std::uint64_t ZeroStrideAvailableBytes(const ShaderRecompiler::VertexAttribute& attribute) { - const auto records = attribute.resource.fields[2]; - if (VertexBufferOutOfBoundsSelect(attribute) == 2u && records != 0) return std::max(records, DecodeVertexFormat(attribute).bytes); - return records; + return VertexFetchOutOfRange(attribute) ? 0u : DecodeVertexFormat(attribute).bytes; } inline std::size_t VertexBufferExtent(const ShaderRecompiler::VertexAttribute& attribute) { @@ -155,7 +247,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"); @@ -171,7 +263,9 @@ inline std::size_t VertexBufferReadSize(const ShaderRecompiler::VertexAttribute& Require(attribute.fetchIndex <= 1, "unsupported vertex fetch index"); const auto index = attribute.fetchIndex == 0 ? maxIndex : firstInstance + instances - 1u; const auto bytes = DecodeVertexFormat(attribute).bytes; - Require(stride == 0 || VertexBufferOutOfBoundsSelect(attribute) <= 1u || index < records, "vertex fetch exceeds descriptor record count"); + const auto select = VertexBufferOutOfBoundsSelect(attribute); + const auto recordsUnchecked = stride == 0 ? select != 1u : select <= 1u; + Require(recordsUnchecked || index < records, "vertex fetch exceeds descriptor record count"); const auto required = static_cast(stride) * index + bytes; Require(stride != 0 || required <= ZeroStrideAvailableBytes(attribute), "vertex fetch exceeds descriptor byte range"); Require(required <= std::numeric_limits::max(), "vertex fetch exceeds addressable byte range"); @@ -273,23 +367,36 @@ inline std::optional ClampDrawIndices(std::span indice return highest; } +template +std::optional HighestDrawIndexOf(std::span indices, bool skipRestart) { + constexpr auto restartIndex = std::numeric_limits::max(); + const auto count = indices.size() / sizeof(TIndex); + const auto* data = indices.data(); + TIndex highest = 0; + if (!skipRestart) { + for (std::size_t i = 0; i < count; ++i) { + TIndex index; + std::memcpy(&index, data + i * sizeof(TIndex), sizeof(TIndex)); + highest = std::max(highest, index); + } + if (count == 0) return std::nullopt; + return highest; + } + TIndex kept = 0; + for (std::size_t i = 0; i < count; ++i) { + TIndex index; + std::memcpy(&index, data + i * sizeof(TIndex), sizeof(TIndex)); + const TIndex valid = index == restartIndex ? TIndex{0} : TIndex(~TIndex{0}); + highest = std::max(highest, static_cast(index & valid)); + kept |= valid; + } + if (kept == 0) return std::nullopt; + return highest; +} + inline std::optional HighestDrawIndex(std::span indices, std::uint32_t indexSize, bool skipRestart) { Require(indexSize == 2 || indexSize == 4, "unsupported index size"); - const auto restartIndex = indexSize == 2 ? 0xffffu : 0xffffffffu; - std::optional highest; - for (std::size_t offset = 0; offset + indexSize <= indices.size(); offset += indexSize) { - std::uint32_t index = 0; - if (indexSize == 2) { - std::uint16_t value = 0; - std::memcpy(&value, indices.data() + offset, sizeof(value)); - index = value; - } else { - std::memcpy(&index, indices.data() + offset, sizeof(index)); - } - if (skipRestart && index == restartIndex) continue; - highest = std::max(highest.value_or(0u), index); - } - return highest; + return indexSize == 2 ? HighestDrawIndexOf(indices, skipRestart) : HighestDrawIndexOf(indices, skipRestart); } inline std::vector MeshRestartTable(std::span indices, std::uint32_t indexSize) { @@ -329,4 +436,3 @@ inline std::vector SoloZeroPaddedFetchIndices(const std::vector= push.width || pixel.y >= push.height) return; +#if MULTISAMPLED + float value = texelFetch(depth, ivec2(pixel), push.sampleIndex).r; +#else + float value = texelFetch(depth, ivec2(pixel), 0).r; +#endif + imageStore(target, ivec2(pixel), vec4(value, 0.0, 0.0, 0.0)); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/DispatchArguments.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/DispatchArguments.comp new file mode 100644 index 0000000000..2ebd51c40e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/DispatchArguments.comp @@ -0,0 +1,39 @@ +#version 450 +#extension GL_EXT_buffer_reference : require + +layout(local_size_x = 1) in; +layout(buffer_reference, std430, buffer_reference_align = 4) buffer Words { uint words[]; }; +layout(push_constant) uniform Parameters { + Words source; + Words arguments; + uint maxX; + uint maxY; + uint maxZ; + uint threadDimensions; + uint groupX; + uint groupY; + uint groupZ; + uint unused; +} parameters; + +uint groups(uint threads, uint group) { + return threads / group + (threads % group != 0u ? 1u : 0u); +} + +void main() { + uint x = parameters.source.words[0]; + uint y = parameters.source.words[1]; + uint z = parameters.source.words[2]; + if (parameters.threadDimensions != 0u) { + parameters.arguments.words[3] = x; + parameters.arguments.words[4] = y; + parameters.arguments.words[5] = z; + x = groups(x, parameters.groupX); + y = groups(y, parameters.groupY); + z = groups(z, parameters.groupZ); + } + bool valid = x <= parameters.maxX && y <= parameters.maxY && z <= parameters.maxZ; + parameters.arguments.words[0] = valid ? x : 0u; + parameters.arguments.words[1] = valid ? y : 0u; + parameters.arguments.words[2] = valid ? z : 0u; +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/MultisampleSampledReadback.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/MultisampleSampledReadback.comp new file mode 100644 index 0000000000..4acd6a84a3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/MultisampleSampledReadback.comp @@ -0,0 +1,54 @@ +#version 450 +#extension GL_EXT_samplerless_texture_functions : require + +layout(constant_id = 0) const uint ELEMENT_BYTES = 4u; +layout(local_size_x = 64) in; +layout(set = 0, binding = 0, std430) buffer Output { uint data[]; } destinationBuffer; +#if READ_DEPTH +layout(set = 0, binding = 1) uniform texture2DMSArray sourceImage; +#else +layout(set = 0, binding = 1) uniform utexture2DMSArray sourceImage; +#endif +layout(push_constant) uniform Push { + uint width; + uint height; + uint layers; + uint fragments; + uint firstElement; + uint elements; + uint rowThreads; +} params; + +uvec4 loadElement(uint index) { + uint element = params.firstElement + index; + uint pixel = element / params.fragments; + ivec3 coordinate = ivec3(pixel % params.width, (pixel / params.width) % params.height, pixel / (params.width * params.height)); +#if READ_DEPTH + return floatBitsToUint(texelFetch(sourceImage, coordinate, int(element % params.fragments))); +#else + return texelFetch(sourceImage, coordinate, int(element % params.fragments)); +#endif +} + +void main() { + uint index = gl_GlobalInvocationID.y * params.rowThreads + gl_GlobalInvocationID.x; + if (ELEMENT_BYTES < 4u) { + uint packedElements = 4u / ELEMENT_BYTES; + if (index >= params.elements / packedElements + uint(params.elements % packedElements != 0u)) return; + uint first = index * packedElements; + uint value = 0u; + uint mask = 0u; + for (uint packed = 0u; packed < packedElements && first + packed < params.elements; ++packed) { + uint shift = packed * ELEMENT_BYTES * 8u; + uint bits = ELEMENT_BYTES == 1u ? 255u : 65535u; + value |= (loadElement(first + packed).x & bits) << shift; + mask |= bits << shift; + } + if (mask != 0xffffffffu) value |= destinationBuffer.data[index] & ~mask; + destinationBuffer.data[index] = value; + } else { + if (index >= params.elements) return; + uvec4 value = loadElement(index); + for (uint component = 0u; component < ELEMENT_BYTES / 4u; ++component) destinationBuffer.data[index * (ELEMENT_BYTES / 4u) + component] = value[component]; + } +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/MultisampleUpload.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/MultisampleUpload.comp new file mode 100644 index 0000000000..4aa972fc04 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/MultisampleUpload.comp @@ -0,0 +1,32 @@ +#version 450 +#extension GL_EXT_shader_image_load_formatted : require + +layout(constant_id = 0) const uint ELEMENT_BYTES = 4u; +layout(local_size_x = 64) in; +layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } sourceBuffer; +layout(set = 0, binding = 1) uniform uimage2DMSArray destinationImage; +layout(push_constant) uniform Push { + uint width; + uint height; + uint layers; + uint fragments; + uint firstElement; + uint elements; + uint rowThreads; +} params; + +void main() { + uint index = gl_GlobalInvocationID.y * params.rowThreads + gl_GlobalInvocationID.x; + if (index >= params.elements) return; + uint element = params.firstElement + index; + uint pixel = element / params.fragments; + ivec3 coordinate = ivec3(pixel % params.width, (pixel / params.width) % params.height, pixel / (params.width * params.height)); + uint address = index * ELEMENT_BYTES; + uvec4 value = uvec4(0u); + if (ELEMENT_BYTES < 4u) { + value.x = (sourceBuffer.data[address >> 2u] >> ((address & 3u) * 8u)) & (ELEMENT_BYTES == 1u ? 255u : 65535u); + } else { + for (uint component = 0u; component < ELEMENT_BYTES / 4u; ++component) value[component] = sourceBuffer.data[(address >> 2u) + component]; + } + imageStore(destinationImage, coordinate, int(element % params.fragments), value); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag new file mode 100644 index 0000000000..435fb75d5a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag @@ -0,0 +1,12 @@ +#version 450 + +layout(push_constant) uniform Parameters { + vec4 palette[3]; + vec2 extent; +} parameters; +layout(location = 0) flat in uint paletteIndex; +layout(location = 0) out vec4 color; + +void main() { + color = paletteIndex == 1u ? parameters.palette[1] : paletteIndex == 2u ? parameters.palette[2] : parameters.palette[0]; +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.vert b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.vert new file mode 100644 index 0000000000..b59069433c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.vert @@ -0,0 +1,16 @@ +#version 450 + +layout(location = 0) in ivec4 area; +layout(location = 1) in uint color; +layout(push_constant) uniform Parameters { + vec4 palette[3]; + vec2 extent; +} parameters; +layout(location = 0) flat out uint paletteIndex; + +void main() { + vec2 corner = vec2(float(gl_VertexIndex & 1), float((gl_VertexIndex >> 1) & 1)); + vec2 pixel = vec2(area.xy) + corner * vec2(area.zw); + gl_Position = vec4(pixel / parameters.extent * 2.0 - 1.0, 0.0, 1.0); + paletteIndex = color; +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleCounter.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleCounter.comp index 66824ece23..668c934c4a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleCounter.comp +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleCounter.comp @@ -11,6 +11,10 @@ layout(push_constant) uniform Parameters { } parameters; void main() { + if (parameters.store == 2u) { + for (uint db = 0u; db < 16u; ++db) parameters.target.words[db * 4u + 1u] |= 0x80000000u; + return; + } uint low = parameters.counter.words[0]; uint high = parameters.counter.words[1]; for (uint i = 0u; i < parameters.count; ++i) { @@ -18,12 +22,13 @@ void main() { low = uaddCarry(low, parameters.counter.words[4u + 2u * i], carry); high = high + parameters.counter.words[5u + 2u * i] + carry; } + if (parameters.store == 1u) { + for (uint db = 0u; db < 16u; ++db) parameters.target.words[db * 4u + 1u] = db == 0u ? (high & 0x7fffffffu) : 0u; + return; + } + if (parameters.store == 3u) { + for (uint db = 0u; db < 16u; ++db) parameters.target.words[db * 4u] = db == 0u ? low : 0u; + } parameters.counter.words[0] = low; parameters.counter.words[1] = high; - if (parameters.store != 0u) { - for (uint db = 0u; db < 16u; ++db) { - parameters.target.words[db * 4u] = db == 0u ? low : 0u; - parameters.target.words[db * 4u + 1u] = (db == 0u ? high : 0u) | 0x80000000u; - } - } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleDumps.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleDumps.comp index d8e62f91b4..1549983e45 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleDumps.comp +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleDumps.comp @@ -11,6 +11,7 @@ layout(push_constant) uniform Parameters { uint dumpCount; uint firstSegment; uint lastSegment; + uint phase; } parameters; void main() { @@ -26,10 +27,12 @@ void main() { } Words target = Words(uvec2(parameters.dumps.words[dump * 4u], parameters.dumps.words[dump * 4u + 1u])); for (uint db = 0u; db < 16u; ++db) { - target.words[db * 4u] = db == 0u ? low : 0u; - target.words[db * 4u + 1u] = (db == 0u ? high : 0u) | 0x80000000u; + if (parameters.phase == 0u) target.words[db * 4u + 1u] = db == 0u ? (high & 0x7fffffffu) : 0u; + else if (parameters.phase == 1u) target.words[db * 4u] = db == 0u ? low : 0u; + else target.words[db * 4u + 1u] = (db == 0u ? high : 0u) | 0x80000000u; } } + if (parameters.phase != 2u && parameters.dumpCount != 0u) return; for (; segment < parameters.lastSegment; ++segment) { uint carry; low = uaddCarry(low, parameters.counter.words[4u + 2u * segment], carry); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.comp new file mode 100644 index 0000000000..f8a2323e90 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.comp @@ -0,0 +1,60 @@ +#version 450 + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +layout(binding = 0, TEXEL_FORMAT) uniform restrict uimage2DMS samples; + +layout(push_constant) uniform Order { + uvec4 map; + uvec2 extent; + uint count; +} order; + +uvec4 pick(uint source, uvec4 s0, uvec4 s1, uvec4 s2, uvec4 s3, uvec4 s4, uvec4 s5, uvec4 s6, uvec4 s7) { + const bool odd = (source & 1u) != 0u; + const uvec4 low = (source & 2u) == 0u ? (odd ? s1 : s0) : (odd ? s3 : s2); + const uvec4 high = (source & 2u) == 0u ? (odd ? s5 : s4) : (odd ? s7 : s6); + return (source & 4u) == 0u ? low : high; +} + +void reorder(ivec2 at, uint map) { + if (map == 0x76543210u || uint(at.x) >= order.extent.x || uint(at.y) >= order.extent.y) return; + const uvec4 s0 = imageLoad(samples, at, 0); + const uvec4 s1 = imageLoad(samples, at, 1); + uvec4 s2 = uvec4(0u); + uvec4 s3 = uvec4(0u); + uvec4 s4 = uvec4(0u); + uvec4 s5 = uvec4(0u); + uvec4 s6 = uvec4(0u); + uvec4 s7 = uvec4(0u); + if (order.count > 2u) { + s2 = imageLoad(samples, at, 2); + s3 = imageLoad(samples, at, 3); + } + if (order.count > 4u) { + s4 = imageLoad(samples, at, 4); + s5 = imageLoad(samples, at, 5); + s6 = imageLoad(samples, at, 6); + s7 = imageLoad(samples, at, 7); + } + imageStore(samples, at, 0, pick(map & 7u, s0, s1, s2, s3, s4, s5, s6, s7)); + imageStore(samples, at, 1, pick((map >> 4u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7)); + if (order.count > 2u) { + imageStore(samples, at, 2, pick((map >> 8u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7)); + imageStore(samples, at, 3, pick((map >> 12u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7)); + } + if (order.count > 4u) { + imageStore(samples, at, 4, pick((map >> 16u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7)); + imageStore(samples, at, 5, pick((map >> 20u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7)); + imageStore(samples, at, 6, pick((map >> 24u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7)); + imageStore(samples, at, 7, pick((map >> 28u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7)); + } +} + +void main() { + const ivec2 quad = ivec2(gl_GlobalInvocationID.xy) * 2; + reorder(quad, order.map.x); + reorder(quad + ivec2(1, 0), order.map.y); + reorder(quad + ivec2(0, 1), order.map.z); + reorder(quad + ivec2(1, 1), order.map.w); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.frag b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.frag new file mode 100644 index 0000000000..89e5983334 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.frag @@ -0,0 +1,31 @@ +#version 450 +#extension GL_EXT_samplerless_texture_functions : require +#ifdef STENCIL_EXPORT +#extension GL_ARB_shader_stencil_export : require +#endif + +layout(binding = 0) uniform texture2DMS depthSource; +layout(binding = 1) uniform utexture2DMS stencilSource; + +layout(push_constant) uniform Order { + uvec4 map; + uint bit; +} order; + +void main() { + const ivec2 at = ivec2(gl_FragCoord.xy); + const bool right = (at.x & 1) != 0; + const uint top = right ? order.map.y : order.map.x; + const uint bottom = right ? order.map.w : order.map.z; + const uint map = (at.y & 1) != 0 ? bottom : top; + if (map == 0x76543210u) discard; + const int source = int((map >> (uint(gl_SampleID) * 4u)) & 7u); +#ifdef STENCIL_BITS + if (((texelFetch(stencilSource, at, source).r >> order.bit) & 1u) == 0u) discard; +#else + gl_FragDepth = texelFetch(depthSource, at, source).r; +#ifdef STENCIL_EXPORT + gl_FragStencilRefARB = int(texelFetch(stencilSource, at, source).r); +#endif +#endif +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.vert b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.vert new file mode 100644 index 0000000000..05606ecec4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.vert @@ -0,0 +1,6 @@ +#version 450 + +void main() { + const vec2 corner = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2)); + gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureRepack.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureRepack.comp new file mode 100644 index 0000000000..6de2f05e4a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureRepack.comp @@ -0,0 +1,20 @@ +#version 450 + +layout(local_size_x = 64) in; + +layout(std430, set = 0, binding = 0) readonly buffer Source { uint source[]; }; +layout(std430, set = 0, binding = 1) buffer Destination { uint destination[]; }; + +layout(push_constant) uniform Push { + uint sourceBase; + uint destinationBase; + uint words; + uint rowWords; +} push; + +void main() { + const uint index = gl_GlobalInvocationID.y * push.rowWords + gl_GlobalInvocationID.x; + if (gl_GlobalInvocationID.x >= push.rowWords || index >= push.words) return; + const uint word = source[push.sourceBase / 4u + index]; + destination[push.destinationBase / 4u + index] = ((word >> 21u) & 0x7ffu) | (((word >> 10u) & 0x7ffu) << 11u) | ((word & 0x3ffu) << 22u); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BarrierBatch.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BarrierBatch.cpp new file mode 100644 index 0000000000..0b7197ddc6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BarrierBatch.cpp @@ -0,0 +1,27 @@ +#include "prx/libSceAgcDriver/Graphics/include/BarrierBatch.hpp" + +namespace AgcDriver::Graphics { + +namespace { + +bool StageCovers(VkPipelineStageFlags outer, VkPipelineStageFlags inner) { + auto rest = inner & ~outer; + if ((outer & VK_PIPELINE_STAGE_ALL_COMMANDS_BIT) != 0) rest &= VK_PIPELINE_STAGE_HOST_BIT; + return rest == 0; +} + +} + +bool Covers(const BarrierMask& outer, const BarrierMask& inner) { + if (inner.changesLayout || inner.transfersQueue) return false; + return StageCovers(outer.srcStage, inner.srcStage) && StageCovers(outer.dstStage, inner.dstStage) && (inner.srcAccess & ~outer.srcAccess) == 0 && (inner.dstAccess & ~outer.dstAccess) == 0; +} + +bool OrderedByTrailing(VkAccessFlags covered, const BarrierMask& wanted) { + if (wanted.changesLayout || wanted.transfersQueue || (wanted.dstStage & VK_PIPELINE_STAGE_HOST_BIT) != 0) return false; + constexpr VkAccessFlags shaderAndTransfer = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; + const VkAccessFlags needed = shaderAndTransfer | wanted.dstAccess; + return (covered & needed) == needed; +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 0b94d5e0c9..03865c6a92 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" +#include "ImageTableAbi.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include #include @@ -9,6 +10,7 @@ #include #include #include +#include #include namespace AgcDriver::Graphics { @@ -125,6 +127,7 @@ BdaResources::BdaResources(const Context& context) { Require(ShaderRecompiler::BdaAbi::FaultBufferBytes <= context.limits.maxStorageBufferRange, "BDA fault buffer exceeds storage buffer range limit"); fault = std::make_unique(context, ShaderRecompiler::BdaAbi::FaultBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); std::memset(fault->Bytes().data(), 0, ShaderRecompiler::BdaAbi::WrittenListWord * sizeof(std::uint32_t)); + std::memset(fault->Bytes().data() + ShaderRecompiler::BdaAbi::SrtGuardWord * sizeof(std::uint32_t), 0, sizeof(std::uint32_t)); } BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memory) : BdaResources(context) { @@ -192,6 +195,7 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo ++it; } } + Require(ShaderRecompiler::BdaAbi::RangesOrdered(ranges.data(), ranges.size()), "BDA table ranges do not ascend by begin"); for (const auto& range : ranges) Require((range.permissions & ShaderRecompiler::BdaAbi::Write) == 0 || range.end <= ShaderRecompiler::BdaAbi::WrittenAddressLimit, "a writable BDA range ends past the addresses the written-page record can hold"); table = std::make_shared(context, tableBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); const ShaderRecompiler::BdaAbi::Header header{ShaderRecompiler::BdaAbi::Version, static_cast(ranges.size()), sizeof(ShaderRecompiler::BdaAbi::Range), 0}; @@ -241,10 +245,11 @@ void BdaResources::CheckFault() const { ShaderRecompiler::BdaAbi::Fault report{}; std::memcpy(&report, fault->Bytes().data(), sizeof(report)); if (report.state == ShaderRecompiler::BdaAbi::FaultState::Empty) { - Require(static_cast(report.reason) == 0 && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.reserved == 0, "BDA fault record has data without publication"); + Require(static_cast(report.reason) == 0 && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.otherFaults == 0, "BDA fault record has data without publication"); + checkSrtGuard(); return; } - Require(report.state == ShaderRecompiler::BdaAbi::FaultState::Ready && report.reserved == 0, "incomplete or invalid BDA fault record"); + Require(report.state == ShaderRecompiler::BdaAbi::FaultState::Ready && report.otherFaults <= 1, "incomplete or invalid BDA fault record"); Require(report.reason != ShaderRecompiler::BdaAbi::FaultReason::InvalidRectangle, "rect-list requires finite nondegenerate axis-aligned positions with equal positive W"); if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::LoopLimit) { // APS5_LOOP_GUARD: the shader left a loop that ran past the guard; the dispatch result is kept. @@ -253,7 +258,30 @@ void BdaResources::CheckFault() const { std::memset(fault->Bytes().data(), 0, fault->Bytes().size()); return; } + if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::ImageTable) { + const auto table = std::find_if(imageTables.begin(), imageTables.end(), [&](const ImageTableFaults& candidate) { return candidate.stage == report.stage; }); + const auto* tables = table != imageTables.end() ? &*table : imageTables.size() == 1 ? &imageTables.front() : nullptr; + std::ostringstream message; + message << "image table: shader 0x" << std::hex << (tables != nullptr ? tables->shader : 0u) << std::dec << " stage " << report.stage << " pc 0x" << std::hex << report.instruction << " selected the descriptor at 0x" << report.address << std::dec; + if (tables != nullptr && report.bytes < tables->poison.size()) { + const auto& poison = tables->poison[report.bytes]; + if (poison.dwordCount != 0) { + message << " (words"; + for (std::uint32_t index = 0; index < poison.dwordCount && index < poison.words.size(); ++index) message << ' ' << std::hex << std::setw(8) << std::setfill('0') << poison.words[index]; + message << std::dec << ")"; + } + message << ": " << ShaderRecompiler::ImageTableAbi::PoisonReasonName(static_cast(poison.reason)); + if (report.bytes < tables->messages.size() && !tables->messages[report.bytes].empty()) message << ": " << tables->messages[report.bytes]; + } else { + message << ": poison entry " << report.bytes; + } + throw std::runtime_error(message.str()); + } std::ostringstream message; + if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::UnlinkedCall) { + message << "shader stage " << report.stage << " called address 0x" << std::hex << report.address << " at program counter 0x" << report.instruction << ", which no linked callee of that call matches"; + throw std::runtime_error(message.str()); + } message << "BDA access failed: address=0x" << std::hex << report.address << " instruction=0x" << report.instruction << std::dec << " bytes=" << report.bytes << " stage=" << report.stage << " reason=" << static_cast(report.reason); if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::Permission && table != nullptr) { const auto bytes = table->Bytes(); @@ -267,6 +295,10 @@ void BdaResources::CheckFault() const { break; } } + if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::Unaligned) { + if (report.otherFaults == 0) throw BdaUnalignedAccess(message.str(), report.instruction); + message << "; a fault of another kind was recorded in the same dispatch"; + } throw std::runtime_error(message.str()); } @@ -274,13 +306,49 @@ void BdaResources::CheckFault() const { namespace AgcDriver::Graphics { +void BdaResources::SetImageTables(std::vector tables, std::vector> ranges) { + imageTables = std::move(tables); + imageTableRanges = std::move(ranges); +} + +void BdaResources::SetSrtGuards(std::vector guards) { + srtGuards = std::move(guards); +} + +void BdaResources::checkSrtGuard() const { + namespace Abi = ShaderRecompiler::BdaAbi; + std::uint32_t guard = 0; + std::memcpy(&guard, fault->Bytes().data() + Abi::SrtGuardWord * sizeof(std::uint32_t), sizeof(guard)); + if (guard == 0) return; + const auto stage = guard >> Abi::SrtGuardStageShift; + const auto record = guard & ((1u << Abi::SrtGuardStageShift) - 1u); + const auto found = std::find_if(srtGuards.begin(), srtGuards.end(), [&](const SrtGuardFaults& candidate) { return candidate.stage == stage; }); + const auto* guards = found != srtGuards.end() ? &*found : srtGuards.size() == 1 ? &srtGuards.front() : nullptr; + Require(guards != nullptr && record + 2u < guards->flattenedSrt.size(), "a guarded SRT read faulted without its poison record"); + const auto& words = guards->flattenedSrt; + const auto address = static_cast(words[record + 1u]) | (static_cast(words[record + 2u]) << 32u); + std::ostringstream message; + message << "BDA access failed: address=0x" << std::hex << address << " instruction=0x" << words[record] << std::dec << " bytes=4 stage=" << stage << " reason=" << static_cast(Abi::FaultReason::Unmapped); + throw std::runtime_error(message.str()); +} + void BdaResources::markWrittenPages() const { namespace Abi = ShaderRecompiler::BdaAbi; auto* words = reinterpret_cast(fault->Bytes().data()); Require(words[Abi::WrittenOverflowWord] == 0, "more than " + std::to_string(Abi::WrittenPageSlots) + " chunks of " + std::to_string(Abi::WrittenChunkPages) + " pages stored to through the BDA table in one use are not implemented"); const auto count = words[Abi::WrittenCountWord]; if (count == 0) return; - Require(Abi::ForEachWrittenRange(words, [](std::uint64_t address, std::size_t bytes) { GuestMemory::MarkWritten(address, bytes); }), "invalid BDA written-page record"); + Require(Abi::ForEachWrittenRange(words, [&](std::uint64_t begin, std::size_t bytes) { + const auto end = begin + bytes; + for (const auto& [base, size] : imageTableRanges) { + if (size != 0 && begin < base + size && base < end) { + std::ostringstream message; + message << "image table at 0x" << std::hex << base << " is written by its own dispatch"; + Require(false, message.str()); + } + } + GuestMemory::MarkPossiblyWritten(begin, bytes); + }), "invalid BDA written-page record"); for (std::uint32_t claim = 0; claim < count; ++claim) { const auto slot = words[Abi::WrittenListWord + claim]; words[Abi::WrittenSlotsWord + 2 * slot] = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 4ae8a5074f..128970a898 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -1,10 +1,12 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include +#include #include #include #include #include +#include #include namespace AgcDriver::Graphics { @@ -22,14 +24,28 @@ bool exactSizes() { return exact; } +bool systemMemoryHeap(const VkPhysicalDeviceMemoryProperties& memory, std::uint32_t heap) { + bool typed = false; + for (std::uint32_t type = 0; type < std::min(memory.memoryTypeCount, VK_MAX_MEMORY_TYPES); ++type) { + if (memory.memoryTypes[type].heapIndex != heap) continue; + if ((memory.memoryTypes[type].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0) return false; + typed = true; + } + return typed; +} + constexpr VkBufferUsageFlags deviceUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_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) : maxSlots(std::max(1, std::min(MaxSlots(), context.limits.maxMemoryAllocationCount / 6u))), cacheReclaim(context.cacheReclaim), device(context.device), unmap(context.Function("vkUnmapMemory")), destroyBuffer(context.Function("vkDestroyBuffer")), freeMemory(context.Function("vkFreeMemory")), allocateMemory(context.Function("vkAllocateMemory")), mapMemory(context.Function("vkMapMemory")), physical(context.physical), memoryProperties(context.memory), memoryProperties2(context.memoryProperties2) { smallTier.budget = smallBudget; largeTier.budget = budget; - deviceTier.budget = DeviceTierEnabled() ? DeviceBudget(context.memory) : 0; + deviceTier.budget = deviceBudget(context); + memoryProperties = context.memory; + physical = context.physical; + memoryProperties2 = context.memoryProperties2; + imageBudget = ImageBudget(deviceTier.budget); } BufferPool::~BufferPool() { @@ -41,13 +57,16 @@ BufferPool::~BufferPool() { for (auto& [memory, block] : slabBlocks) freeBlock(*block); } -VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation) { +VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType, std::uint32_t* allocatedType) { + if (allocatedType != nullptr) *allocatedType = allocation.memoryTypeIndex; VkDeviceMemory memory = VK_NULL_HANDLE; auto result = allocateMemory(device, &allocation, nullptr, &memory); 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 || value == VK_ERROR_TOO_MANY_OBJECTS; }; + if (exhausted(result)) { reclaimed = trim(); if (reclaimed != 0) { memory = VK_NULL_HANDLE; @@ -55,16 +74,152 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation ++attempts; } } + if (exhausted(result)) { + if (cacheReclaim != nullptr) released = cacheReclaim(ReclaimTarget(allocation.allocationSize)); + if (released != 0) { + reclaimed += trim(); + memory = VK_NULL_HANDLE; + result = allocateMemory(device, &allocation, nullptr, &memory); + ++attempts; + } + } + if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY && hostType.has_value() && *hostType != allocation.memoryTypeIndex) { + auto host = allocation; + host.memoryTypeIndex = *hostType; + memory = VK_NULL_HANDLE; + result = allocateMemory(device, &host, nullptr, &memory); + ++attempts; + if (result == VK_SUCCESS) { + if (allocatedType != nullptr) *allocatedType = *hostType; + static std::atomic placements{0}, placedBytes{0}; + const auto count = placements.fetch_add(1, std::memory_order_relaxed) + 1; + const auto total = placedBytes.fetch_add(allocation.allocationSize, std::memory_order_relaxed) + allocation.allocationSize; + if (count <= 5 || (count & (count - 1)) == 0) std::fprintf(stderr, "[bufferpool] %s: memory type %u is out of device memory for %llu bytes after reclaiming %llu and releasing %llu; placed in host memory type %u until it is released; %llu such allocations, %llu MiB\n", operation, allocation.memoryTypeIndex, static_cast(allocation.allocationSize), static_cast(reclaimed), static_cast(released), *hostType, static_cast(count), static_cast(total >> 20u)); + return memory; + } + } 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); + char details[1536]; + std::snprintf(details, sizeof(details), "%s: allocation=%llu memoryType=%u reclaimed=%llu released=%llu initialResult=%d attempts=%u", operation, static_cast(allocation.allocationSize), allocation.memoryTypeIndex, static_cast(reclaimed), static_cast(released), static_cast(initialResult), attempts); + if (memoryProperties2 != nullptr && physical != VK_NULL_HANDLE) { + VkPhysicalDeviceMemoryBudgetPropertiesEXT budget{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &budget}; + memoryProperties2(physical, &properties); + auto used = std::strlen(details); + for (std::uint32_t heap = 0; heap < memoryProperties.memoryHeapCount && heap < VK_MAX_MEMORY_HEAPS; ++heap) { + const auto remaining = sizeof(details) - used; + const auto appended = std::snprintf(details + used, remaining, "; heap %u %llu/%llu MiB", heap, static_cast(budget.heapUsage[heap] >> 20u), static_cast(budget.heapBudget[heap] >> 20u)); + if (appended < 0 || static_cast(appended) >= remaining) break; + used += static_cast(appended); + } + } Check(result, details); return VK_NULL_HANDLE; } +VkDeviceSize BufferPool::ImageBudget(VkDeviceSize deviceBudget) { + static const char* value = std::getenv("APS5_IMAGE_POOL_MIB"); + if (value != nullptr) return static_cast(std::strtoull(value, nullptr, 10)) << 20u; + return deviceBudget / 4u; +} + +bool BufferPool::imagePooled(VkDeviceSize bytes, std::uint32_t memoryType) const { + return imageBudget != 0 && bytes != 0 && bytes <= imageBudget && bytes <= deviceTier.budget && memoryType < memoryProperties.memoryTypeCount && DeviceTiered(memoryProperties.memoryTypes[memoryType].propertyFlags); +} + +bool BufferPool::heapOverBudget() { + if (memoryProperties2 == nullptr || physical == VK_NULL_HANDLE) return false; + const auto now = std::chrono::steady_clock::now(); + if (heapRead != std::chrono::steady_clock::time_point{} && now - heapRead < std::chrono::milliseconds(100)) return heapOver; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2}; + properties.pNext = &reported; + memoryProperties2(physical, &properties); + heapRead = now; + heapOver = false; + for (std::uint32_t heap = 0; heap < memoryProperties.memoryHeapCount && heap < VK_MAX_MEMORY_HEAPS; ++heap) { + if ((memoryProperties.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && reported.heapBudget[heap] != 0 && reported.heapUsage[heap] > reported.heapBudget[heap]) heapOver = true; + } + return heapOver; +} + +VkDeviceMemory BufferPool::TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType, std::uint32_t* allocatedType) { + const auto type = allocation.memoryTypeIndex; + if (imagePooled(allocation.allocationSize, type)) { + std::lock_guard lock(mutex); + ++clock; + const auto found = deviceTier.free.find({static_cast(allocation.allocationSize), 0, memoryProperties.memoryTypes[type].propertyFlags, type}); + if (found != deviceTier.free.end() && !found->second.empty()) { + auto& slots = found->second; + const auto memory = slots.back().allocation.memory; + imageOrder.erase(slots.back().order); + slots.pop_back(); + if (slots.empty()) deviceTier.free.erase(found); + --deviceTier.slots; + deviceTier.retainedBytes -= allocation.allocationSize; + --imageSlots; + imageBytes -= allocation.allocationSize; + ++imageHits; + if (allocatedType != nullptr) *allocatedType = type; + return memory; + } + ++imageMisses; + } + return AllocateMemory(allocation, operation, hostType, allocatedType); +} + +void BufferPool::PutImageMemory(VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept { + if (memory == VK_NULL_HANDLE) return; + if (!imagePooled(bytes, memoryType)) { + freeMemory(device, memory, nullptr); + return; + } + BufferAllocation allocation{VK_NULL_HANDLE, memory, nullptr, 0, bytes, static_cast(bytes), 0, memoryProperties.memoryTypes[memoryType].propertyFlags}; + allocation.memoryType = memoryType; + std::vector evicted; + try { + std::lock_guard lock(mutex); + if (heapOverBudget()) { + while (imageSlots != 0 && evictOldestImage(evicted)) { + } + evicted.push_back(allocation); + } else { + while (imageSlots != 0 && imageBytes + bytes > imageBudget && evictOldestImage(evicted)) { + } + while (deviceTier.slots != 0 && (deviceTier.retainedBytes + bytes > deviceTier.budget || deviceTier.slots >= maxSlots)) evictOldest(deviceTier, evicted); + const SlotKey imageKey{allocation.bytes, 0, allocation.properties, memoryType}; + auto& slots = deviceTier.free[imageKey]; + slots.push_back({allocation, ++clock, {}}); + try { + slots.back().order = imageOrder.insert(imageOrder.end(), &slots.back()); + } catch (...) { + slots.pop_back(); + if (slots.empty()) deviceTier.free.erase(imageKey); + throw; + } + ++deviceTier.slots; + deviceTier.retainedBytes += bytes; + ++imageSlots; + imageBytes += bytes; + } + } catch (...) { + destroy(allocation); + } + for (const auto& gone : evicted) destroy(gone); +} + +std::size_t BufferPool::RetainedImages() { + std::lock_guard lock(mutex); + return imageSlots; +} + +VkDeviceSize BufferPool::ReclaimTarget(VkDeviceSize bytes) { + return bytes + std::max(bytes, VkDeviceSize{256} << 20u); +} + 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); @@ -96,7 +251,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}; @@ -108,7 +267,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); } @@ -117,9 +275,12 @@ SlabSlot BufferPool::TakeSlot(const Context& context, std::uint32_t memoryType, for (std::uint32_t index = slots; index-- != 0;) block->free.push_back(index); std::lock_guard lock(slabMutex); auto& slab = slabs[key]; + if (slab.blocks == 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.blocks; ++slab.emptyBlocks; return takeFrom(*slab.available.back(), slab); } @@ -145,6 +306,7 @@ VkDeviceSize BufferPool::PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noe return 0; } slab.available.erase(std::find(slab.available.begin(), slab.available.end(), &block)); + --slab.blocks; released = std::move(found->second); slabBlocks.erase(found); } @@ -165,14 +327,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; + 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) largest = std::max(largest, memory.memoryHeaps[heap].size); + if ((memory.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && (!largest || memory.memoryHeaps[heap].size > memory.memoryHeaps[*largest].size)) largest = heap; } - 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 { @@ -182,7 +357,7 @@ VkDeviceSize BufferPool::destroy(const BufferAllocation& allocation) noexcept { } // Device-local allocations (see DeviceBuffer) are never mapped. if (allocation.mapping != nullptr) unmap(device, allocation.memory); - destroyBuffer(device, allocation.buffer, nullptr); + if (allocation.buffer != VK_NULL_HANDLE) destroyBuffer(device, allocation.buffer, nullptr); freeMemory(device, allocation.memory, nullptr); return allocation.allocationBytes; } @@ -209,7 +384,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; } @@ -223,7 +398,7 @@ std::optional BufferPool::Take(std::size_t bytes, VkBufferUsag const auto now = std::chrono::steady_clock::now(); if (now - lastReport > std::chrono::seconds(10)) { lastReport = now; - AgcDriver::ProfilePrint_nid_no_patch("[bufferpool] small: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB); large: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB)%s; device: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB of %llu)\n", static_cast(smallTier.hits), static_cast(smallTier.misses), static_cast(smallTier.evictions), smallTier.slots, smallTier.retainedBytes / 1048576.0, static_cast(largeTier.hits), static_cast(largeTier.misses), static_cast(largeTier.evictions), largeTier.slots, largeTier.retainedBytes / 1048576.0, sharedTiers() ? " (shared)" : "", static_cast(deviceTier.hits), static_cast(deviceTier.misses), static_cast(deviceTier.evictions), deviceTier.slots, deviceTier.retainedBytes / 1048576.0, static_cast(deviceTier.budget >> 20u)); + AgcDriver::ProfilePrint_nid_no_patch("[bufferpool] small: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB); large: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB)%s; device: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB of %llu); images: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB of %llu)\n", static_cast(smallTier.hits), static_cast(smallTier.misses), static_cast(smallTier.evictions), smallTier.slots, smallTier.retainedBytes / 1048576.0, static_cast(largeTier.hits), static_cast(largeTier.misses), static_cast(largeTier.evictions), largeTier.slots, largeTier.retainedBytes / 1048576.0, sharedTiers() ? " (shared)" : "", static_cast(deviceTier.hits), static_cast(deviceTier.misses), static_cast(deviceTier.evictions), deviceTier.slots, deviceTier.retainedBytes / 1048576.0, static_cast(deviceTier.budget >> 20u), static_cast(imageHits), static_cast(imageMisses), static_cast(imageEvictions), imageSlots, imageBytes / 1048576.0, static_cast(imageBudget >> 20u)); } } auto& tier = tierFor(capacity, properties); @@ -248,12 +423,37 @@ void BufferPool::evictOldest(Tier& tier, std::vector& evicted) // First: a throw here leaves the slot retained and counted. evicted.push_back(oldest->second.front().allocation); tier.retainedBytes -= oldest->second.front().allocation.allocationBytes; + if (oldest->second.front().allocation.buffer == VK_NULL_HANDLE) { + imageOrder.erase(oldest->second.front().order); + imageBytes -= oldest->second.front().allocation.allocationBytes; + --imageSlots; + ++imageEvictions; + } oldest->second.pop_front(); if (oldest->second.empty()) tier.free.erase(oldest); --tier.slots; ++tier.evictions; } +bool BufferPool::evictOldestImage(std::vector& evicted) { + if (imageOrder.empty()) return false; + const Slot* oldest = imageOrder.front(); + const SlotKey key{oldest->allocation.bytes, 0, oldest->allocation.properties, oldest->allocation.memoryType}; + const auto found = deviceTier.free.find(key); + evicted.push_back(oldest->allocation); + imageOrder.pop_front(); + const auto bytes = oldest->allocation.allocationBytes; + deviceTier.retainedBytes -= bytes; + imageBytes -= bytes; + found->second.pop_front(); + if (found->second.empty()) deviceTier.free.erase(found); + --deviceTier.slots; + --imageSlots; + ++deviceTier.evictions; + ++imageEvictions; + return true; +} + std::size_t BufferPool::MaxSlots() { // APS5_BUFFER_POOL_SLOTS= bounds the retained allocations; 64 is the capacity the pool had // before least-recently-used retention, for A/B runs. @@ -277,6 +477,10 @@ VkDeviceSize BufferPool::trim() { tier->retainedBytes = 0; tier->slots = 0; } + imageEvictions += imageSlots; + imageBytes = 0; + imageSlots = 0; + imageOrder.clear(); } VkDeviceSize bytes = 0; for (const auto* tier : {&retiredSmall, &retiredLarge, &retiredDevice}) { @@ -292,6 +496,7 @@ VkDeviceSize BufferPool::trim() { if (found == slabBlocks.end()) break; auto& slab = slabs.at(found->second->slab); slab.available.erase(std::find(slab.available.begin(), slab.available.end(), found->second.get())); + --slab.blocks; --slab.emptyBlocks; released = std::move(found->second); slabBlocks.erase(found); @@ -313,7 +518,7 @@ void BufferPool::Put(const BufferAllocation& allocation) noexcept { evicted.push_back(allocation); } else { while (tier.slots != 0 && (tier.retainedBytes + allocation.allocationBytes > tier.budget || tier.slots >= maxSlots)) evictOldest(tier, evicted); - tier.free[{allocation.bytes, allocation.usage, allocation.properties}].push_back({allocation, ++clock}); + tier.free[{allocation.bytes, allocation.usage, allocation.properties}].push_back({allocation, ++clock, {}}); ++tier.slots; tier.retainedBytes += allocation.allocationBytes; } @@ -328,4 +533,17 @@ std::shared_ptr GetBufferPool(const Context& context) { return context.bufferPool; } +VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType) { + return GetBufferPool(context)->TakeImageMemory(allocation, operation, context.RefusedDeviceMemoryFallback(mask, allocation.memoryTypeIndex), allocatedType); +} + +void ReleaseImageMemory(const Context& context, VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept { + if (memory == VK_NULL_HANDLE) return; + if (context.bufferPool) { + context.bufferPool->PutImageMemory(memory, bytes, memoryType); + return; + } + context.Function("vkFreeMemory")(context.device, memory, nullptr); +} + } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index ba5bb1d042..eee078cba1 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); @@ -502,9 +502,9 @@ DccKeys readDccKeys(std::uint64_t metaAddress, std::size_t count, bool& memoized return DccKeys::Mixed; } PublishShadow(metaAddress, count, PublishScope::Whole, PublishReason::Keys); - if (auto* recorder = Recorder::Active()) { + if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->PendingWriteOverlaps(metaAddress, count)) { Recorder::CountSync(2); - recorder->Sync(); + recorder->SyncThrough(metaAddress, count); } } const auto* keys = reinterpret_cast(metaAddress); @@ -580,10 +580,53 @@ DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, st return currentDccKeys(metaAddress, keyCount, memoized); } +std::optional CurrentUniformMetadataByte(std::uint64_t address, std::size_t bytes) { + if (address == 0 || bytes == 0 || !GuestMemory::Accessible(reinterpret_cast(address), bytes)) return std::nullopt; + if (GuestMemory::GpuMutex().HeldByThisThread()) { + if (AnyShadowedOverlaps(address, bytes)) PublishShadow(address, bytes, PublishScope::Whole, PublishReason::Keys); + if (auto* recorder = Recorder::Active(); recorder != nullptr && (recorder->PendingWriteOverlaps(address, bytes) || AnyShadowedOverlaps(address, bytes))) { + const auto landed = KeysAfterPendingWrites(*recorder, address, bytes); + if (landed.mixed) return std::nullopt; + if (!landed.unknown && landed.value >= 0 && !AnyShadowedOverlaps(address, bytes)) return static_cast(landed.value); + Recorder::CountSync(2); + recorder->Sync(); + } + } + std::vector data(bytes); + GuestMemory::Read(address, data); + const auto first = data.front(); + if (!std::all_of(data.begin(), data.end(), [&](std::byte value) { return value == first; })) return std::nullopt; + return std::to_integer(first); +} + +std::optional KnownPendingDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) { + const auto count = DccKeyBytes(surfaceBytes); + if (metaAddress == 0 || count == 0 || !GuestMemory::GpuMutex().HeldByThisThread()) return std::nullopt; + auto* recorder = Recorder::Active(); + if (recorder == nullptr || !recorder->PendingWriteOverlaps(metaAddress, count)) return std::nullopt; + return PendingStoreKeys(*recorder, metaAddress, count); +} + bool IsDccClear(DccKeys keys) { return keys == DccKeys::Clear0000 || keys == DccKeys::Clear0001 || keys == DccKeys::Clear1110 || keys == DccKeys::Clear1111 || keys == DccKeys::ClearRegister; } +std::optional LandedUniformMetadataByte(std::uint64_t address, std::size_t bytes) { + if (address == 0 || bytes == 0 || !GuestMemory::Accessible(reinterpret_cast(address), bytes)) return std::nullopt; + if (AnyShadowedOverlaps(address, bytes)) PublishShadow(address, bytes, PublishScope::Whole, PublishReason::Keys); + if (AnyShadowedOverlaps(address, bytes)) return std::nullopt; + GuestMemory::FlushGpuWrites(address, bytes); + auto* recorder = GuestMemory::GpuMutex().HeldByThisThread() ? Recorder::Active() : nullptr; + if (recorder != nullptr && recorder->PendingWriteOverlaps(address, bytes)) { + const auto landed = KeysAfterPendingWrites(*recorder, address, bytes); + if (landed.mixed || landed.unknown || landed.value < 0) return std::nullopt; + return static_cast(landed.value); + } + const auto* data = reinterpret_cast(address); + if (!AllKeysEqual(data + 1, bytes - 1, data[0])) return std::nullopt; + return data[0]; +} + void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes) { MarkDccUncompressed(metaAddress, surfaceBytes, DccKeyBytes(surfaceBytes)); } @@ -753,13 +796,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 0e0edce359..11f2c659ac 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -1,21 +1,31 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" -#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include #include +#include +#include +#include +#include +#include #include +#include #include #include #include #include #include +#include #include #include #include @@ -24,20 +34,226 @@ namespace AgcDriver::Graphics { namespace { std::uint64_t depthWrites = 0; +std::uint64_t mirrorEpoch = 0; + +std::pair DepthBlockExtent(std::uint32_t bytesPerTexel) { + return {bytesPerTexel == 4 ? 128u : 256u, bytesPerTexel == 1 ? 256u : 128u}; +} Recorder* RecorderFor(const Context& context) { auto* recorder = Recorder::Active(); return recorder != nullptr && recorder->Device() == context.device ? recorder : nullptr; } +bool storageFits(const DepthTarget& target, const GuestTextureResource& descriptor) { + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto format = ResolveTextureFormat(descriptor.format); + const bool sized = d16 ? (format == VK_FORMAT_R16_UINT || format == VK_FORMAT_R16_UNORM || format == VK_FORMAT_R16_SINT || format == VK_FORMAT_R16_SNORM || format == VK_FORMAT_R16_SFLOAT) : (format == VK_FORMAT_R32_SFLOAT || format == VK_FORMAT_R32_UINT || format == VK_FORMAT_R32_SINT); + return sized && descriptor.width == target.extent.width && descriptor.height == target.extent.height && descriptor.mipCount == 1 && (descriptor.dimension == TextureDimension::k2D || descriptor.dimension == TextureDimension::k2DArray); +} + +struct DepthGuard { + VkDevice device; + std::uint64_t address; + std::size_t bytes; + std::shared_ptr> active; + std::vector live; + std::shared_ptr> hostWritten; +}; + +std::shared_mutex& depthGuardMutex() { + static std::shared_mutex mutex; + return mutex; +} + +std::vector& depthGuards() { + static std::vector ranges; + return ranges; +} + +std::atomic& anyDepthGuard() { + static std::atomic any{false}; + return any; +} + +struct DepthSamplingState { + std::array initial{}; + std::string refusal; + + void Refuse(const char* reason) { + if (refusal.empty()) refusal = reason; + } +}; + +bool htileProvesClear(std::uint32_t word, bool depthOnly, float clearDepth) { + if ((word & 15u) == 15u) return false; + const auto depth = depthOnly ? word : word & 0xfffff00fu; + if (depth == 0u) return clearDepth == 0.0f; + if (depth == (depthOnly ? 0xfffffff0u : 0xfffc0000u)) return clearDepth == 1.0f; + return false; +} + +DepthSamplingState prepareDepthSampling(const Context& context, const DepthTarget& target) { + DepthSamplingState state; + const auto refuse = [&](const char* reason) { + state.refusal = reason; + return state; + }; + if (target.samples != 2u && target.samples != 4u && target.samples != 8u) return refuse("unsupported native depth sample count"); + if (target.format != VK_FORMAT_D16_UNORM && target.format != VK_FORMAT_D32_SFLOAT) return refuse("native multisample stencil or depth format import is not implemented"); + if (target.depthSwizzle != 24u || target.firstSlice != 0u || target.lastSlice != 0u) return refuse("native multisample depth arrays or non-Z_X layout are not implemented"); + if ((target.pipeBankXor & ~0xcf00u) != 0u) return refuse("unsupported native multisample depth XOR"); + if (target.address == 0u || (target.address & 65535u) != 0u) return refuse("native multisample depth base is not aligned"); + if (target.stencilAddress != 0u || target.htileStencil) return refuse("native multisample stencil import is not implemented"); + if (target.htileAddress == 0u || (target.htileAddress & 255u) != 0u || !target.htilePipeAligned || target.htileVrsEncoding != 0u) return refuse("native multisample depth initial import requires pipe-aligned HTILE without VRS"); + if (target.clearDepth != 0.0f && target.clearDepth != 1.0f) return refuse("native multisample depth initial clear value is not implemented"); + state.initial = {{{target.address, static_cast(MultisampledDepthBytes_nid_no_patch(target.extent, target.format == VK_FORMAT_D16_UNORM ? 2u : 4u, target.samples)), 0u}, {target.htileAddress, static_cast(HtileMetadataBytes(target.extent)), 0u}}}; + for (const auto& range : state.initial) { + if (range.address > std::numeric_limits::max() - range.bytes) return refuse("native depth import range overflows"); + if (!GuestMemory::Accessible(reinterpret_cast(range.address), range.bytes) || !GuestMemory::Watched(range.address, range.bytes)) return refuse("native multisample depth import requires watched accessible guest memory"); + } + const auto& depth = state.initial[0]; + const auto& htile = state.initial[1]; + if (depth.address + depth.bytes > htile.address && htile.address + htile.bytes > depth.address) return refuse("native depth and HTILE initial ranges overlap"); + for (auto& range : state.initial) { + if (auto* recorder = RecorderFor(context)) recorder->SyncThrough(range.address, range.bytes); + StorageTexture::FlushPending(range.address, range.bytes); + GuestMemory::FlushGpuWrites(range.address, range.bytes); + if (!GuestMemory::Accessible(reinterpret_cast(range.address), range.bytes) || !GuestMemory::Watched(range.address, range.bytes)) return refuse("native multisample depth import requires watched accessible guest memory"); + range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes); + if (range.generation == 0u) return refuse("native multisample depth import requires tracked generations"); + } + std::vector metadata(htile.bytes / sizeof(std::uint32_t)); + GuestMemory::Read(target.htileAddress, std::as_writable_bytes(std::span(metadata)), 256u); + for (const auto word : metadata) { + if (!htileProvesClear(word, target.htileDepthOnly, target.clearDepth)) return refuse("native multisample depth initial HTILE does not prove its clear value"); + } + for (const auto& range : state.initial) GuestMemory::CollectWritesUncached(range.address, range.bytes); + if (!GuestMemory::UnchangedSinceAll(state.initial)) return refuse("guest writes raced native multisample depth import"); + 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; +} + +struct DepthPlaneStoreCounters { + std::atomic stores{0}; + std::atomic units{0}; + std::atomic bytes{0}; +}; + +DepthPlaneStoreCounters& depthPlaneStoreCounters() { + static DepthPlaneStoreCounters counters; + return counters; +} + +bool anyUnit(const std::vector& units) { + return std::find(units.begin(), units.end(), true) != units.end(); +} + +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) : context(context), target(target), hostFormat(HostDepthFormat(context, target.format)) { + DepthSurface(const Context& context, const DepthTarget& target, DepthSamplingState sampling = {}) : context(context), target(target), hostFormat(HostDepthFormat(context, target.format)), sampling(std::move(sampling)) { this->context.bufferPool.reset(); VkFormatProperties properties{}; context.formatProperties(context.physical, hostFormat, &properties); Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0, "depth/stencil format " + std::to_string(hostFormat) + " cannot be an attachment on this device"); - Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0, "depth/stencil format " + std::to_string(hostFormat) + " cannot be sampled on this device"); + Require(target.samples != 1u || (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0, "depth/stencil format " + std::to_string(hostFormat) + " cannot be sampled on this device"); Require(target.extent.width <= context.limits.maxFramebufferWidth && target.extent.height <= context.limits.maxFramebufferHeight, "depth target exceeds framebuffer limits"); const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (target.stencilAddress != 0 ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); try { @@ -50,7 +266,26 @@ public: info.arrayLayers = 1; info.samples = static_cast(target.samples); info.tiling = VK_IMAGE_TILING_OPTIMAL; - info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | (target.samples == 1 ? VK_IMAGE_USAGE_SAMPLED_BIT : 0u); + info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + sampleable = target.samples == 1; + if (target.samples != 1) { + Require(context.imageFormatProperties != nullptr, "native multisample depth requires image format queries"); + VkImageFormatProperties shadowProperties{}; + VkImageFormatProperties directProperties{}; + const auto sampledUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + const auto shadowResult = context.imageFormatProperties(context.physical, hostFormat, info.imageType, info.tiling, sampledUsage, 0, &shadowProperties); + if (shadowResult != VK_ERROR_FORMAT_NOT_SUPPORTED) Check(shadowResult, "vkGetPhysicalDeviceImageFormatProperties sampled depth shadow"); + const auto directResult = context.imageFormatProperties(context.physical, hostFormat, info.imageType, info.tiling, info.usage | VK_IMAGE_USAGE_SAMPLED_BIT, info.flags, &directProperties); + if (directResult != VK_ERROR_FORMAT_NOT_SUPPORTED) Check(directResult, "vkGetPhysicalDeviceImageFormatProperties sampled depth attachment"); + const auto supports = [&](VkResult result, const VkImageFormatProperties& supported) { + return (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0 && result == VK_SUCCESS && (supported.sampleCounts & info.samples) != 0 && supported.maxMipLevels >= 1u && supported.maxArrayLayers >= 1u && supported.maxExtent.depth >= 1u && info.extent.width <= supported.maxExtent.width && info.extent.height <= supported.maxExtent.height; + }; + useSampleShadow = supports(shadowResult, shadowProperties); + const bool directSampleable = supports(directResult, directProperties); + sampleable = useSampleShadow || directSampleable; + if (!useSampleShadow && directSampleable) info.usage |= VK_IMAGE_USAGE_SAMPLED_BIT; + } + if (target.samples == 1 && sampleable) info.usage |= VK_IMAGE_USAGE_SAMPLED_BIT; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage depth"); @@ -59,15 +294,18 @@ public: VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory depth target"); + memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth target"); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory depth"); + if (target.samples != 1u) owned = std::make_shared(this->context, image, memory); VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; viewInfo.format = hostFormat; viewInfo.subresourceRange = {aspects, 0, 1, 0, 1}; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth"); + if (target.samples != 1u && sampleable && this->sampling.refusal.empty()) CheckCurrent(); auto* recorder = RecorderFor(context); + if (recorder != nullptr && owned != nullptr) recorder->Keep(owned); std::unique_ptr batch; if (recorder == nullptr) batch = std::make_unique(context); const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); @@ -96,10 +334,67 @@ public: DepthSurface(const DepthSurface&) = delete; DepthSurface& operator=(const DepthSurface&) = delete; + void CheckCurrent() { + if (target.samples == 1u) return; + const auto clearLease = clearGeneration == 0u ? GuestAllocations::Lease{} : GuestAllocations::GuestAllocationsAcquireAll_nid_postfix(); + Require(active->load(std::memory_order_acquire), "the native multisample depth owner was released"); + Require(sampling.refusal.empty(), sampling.refusal); + Require(!clearPending, "native multisample depth clear has not been recorded"); + for (const auto& range : sampling.initial) { + Require(GuestMemory::Accessible(reinterpret_cast(range.address), range.bytes) && GuestMemory::Watched(range.address, range.bytes), "native multisample depth requires watched accessible guest memory"); + if (const auto* recorder = RecorderFor(context)) Require(!recorder->PendingWriteOverlaps(range.address, range.bytes), "external GPU writes over native multisample depth are not implemented"); + GuestMemory::CollectWritesUncached(range.address, range.bytes); + } + if (clearGeneration != 0u) { + const auto& depth = sampling.initial[0]; + const auto& metadata = sampling.initial[1]; + Require(GuestMemory::UnchangedSince(depth.address, depth.bytes, depth.generation), "guest writes over native multisample depth are not implemented"); + Require(GuestMemory::UnchangedSinceCollected(metadata.address, metadata.bytes, clearGeneration), "CPU writes raced native multisample depth clear"); + Require(GuestMemory::StoredAllSince_nid_no_patch(metadata.address, metadata.bytes, clearGeneration), "native multisample depth clear has no complete tracked HTILE store"); + Require(clearDepth == 0.0f || clearDepth == 1.0f, "native multisample depth clear value is not implemented"); + auto current = sampling.initial; + current[1].generation = GuestMemory::CollectWritesUncached(metadata.address, metadata.bytes); + std::vector words(metadata.bytes / sizeof(std::uint32_t)); + std::memcpy(words.data(), reinterpret_cast(metadata.address), metadata.bytes); + for (const auto word : words) Require(htileProvesClear(word, target.htileDepthOnly, clearDepth), "native multisample depth completed HTILE does not prove its clear value"); + for (const auto& range : current) GuestMemory::CollectWritesUncached(range.address, range.bytes); + Require(GuestMemory::UnchangedSinceAll(current), "guest writes raced native multisample depth clear"); + Require(GuestMemory::UnchangedSinceCollected(metadata.address, metadata.bytes, clearGeneration), "CPU writes raced native multisample depth clear"); + RecordClear(VK_IMAGE_ASPECT_DEPTH_BIT); + sampling.initial = current; + clearGeneration = 0u; + pendingClear = 0u; + } + Require(GuestMemory::UnchangedSinceAll(sampling.initial), "guest writes over native multisample depth are not implemented"); + } + + void CheckResource(const GuestTextureResource& resource) const { + if (target.samples == 1u) return; + Require(resource.baseAddress == target.address && resource.width == target.extent.width && resource.height == target.extent.height && resource.samples == target.samples && resource.format == (target.format == VK_FORMAT_D16_UNORM ? 7u : 22u), "native multisample depth sampled identity or format mismatch"); + Require(resource.dimension == TextureDimension::k2DMsaa && resource.tileMode == TextureTileMode::kZ64KBX && resource.depthOrLastArray == 0u && resource.baseArray == 0u && resource.baseLevel == 0u && resource.lastLevel == 0u && resource.mipCount == 1u && resource.allocatedMipCount == 1u && resource.minLod == 0u, "native multisample depth sampled array, mip or layout is not implemented"); + Require(resource.pipeBankXor == target.pipeBankXor && resource.dccAddress == target.htileAddress && resource.iterate256 == target.iterate256 && resource.dccPipeAligned == target.htilePipeAligned && !resource.dccAlphaOnMsb, "native multisample depth sampled metadata or XOR mismatch"); + } + void ApplyFastClear() { + if (samplingActive) { + CheckCurrent(); + Require(pendingClear == 0u, "native multisample depth fast-clear transition is not implemented"); + } const auto aspects = pendingClear & (VK_IMAGE_ASPECT_DEPTH_BIT | (target.stencilAddress != 0 ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u)); pendingClear = 0; if (aspects == 0) return; + RecordClear(aspects); + } + + 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; @@ -108,43 +403,97 @@ 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(); - else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + else { + Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + recorder->Keep(owned); + } } - std::shared_ptr Sampled(std::span words, const GuestTextureResource& resource, VkComponentMapping components) { - std::array key{}; + 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 || 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, bool drawn = false, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D) { + CheckCurrent(); + CheckResource(resource); + std::array key{}; std::copy_n(words.begin(), std::min(words.size(), 8), key.begin()); key[8] = components.r; key[9] = components.g; key[10] = components.b; key[11] = components.a; - if (const auto found = textures.find(key); found != textures.end()) return found->second; - Require(target.samples == 1, "sampling a multisampled depth surface is not implemented"); + key[12] = static_cast(multisampleViewType); + const bool multisampled = target.samples != 1; + Require(!multisampled || sampleable, "the device cannot sample the native depth target's format and sample count"); const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; + const auto format = ResolveTextureFormat(resource.format); 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 auto format = ResolveTextureFormat(resource.format); const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); - const bool oneSlice = resource.dimension == TextureDimension::k2D || (resource.dimension == TextureDimension::k2DArray && resource.depthOrLastArray == 0); - if ((format != expected && !depthBits) || !oneSlice || resource.width != target.extent.width || resource.height != target.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0) { + if (multisampled) { + Require(!stencil && !depthBits && format == expected, "native multisampled depth sampling needs a numeric depth-plane format"); + 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)", stencil ? "stencil" : "depth", static_cast(target.address), target.extent.width, target.extent.height, static_cast(target.format), resource.width, resource.height, resource.format, static_cast(format), static_cast(resource.tileMode), static_cast(resource.dimension), resource.baseLevel, resource.lastLevel, resource.baseArray, key[0], key[1], key[2], key[3], key[4], key[5], key[6], key[7]); throw std::runtime_error(text); } - const auto viewType = resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; - auto texture = std::make_shared(context, image, hostFormat, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, components, viewType); + if (multisampled && useSampleShadow) { + if (sampleShadow == nullptr) createSampleShadow(); + refreshSampleShadow(drawn); + } + if (const auto found = textures.find(key); found != textures.end()) return found->second; + const auto viewType = multisampled ? multisampleViewType : resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; + const auto sampledImage = useSampleShadow ? sampleShadow : owned; + auto texture = std::make_shared(context, useSampleShadow ? sampleShadow->image : image, hostFormat, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, components, viewType, 0, sampledImage); + if (multisampled) { + Require(samplingActive, "native multisample depth sampling requires its creation-time owner"); + texture->MarkRefreshedPerUse(); + } textures.emplace(key, texture); 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); + NoteHostWrite(); auto* recorder = RecorderFor(context); std::unique_ptr batch; if (recorder == nullptr) batch = std::make_unique(context); @@ -161,15 +510,94 @@ public: else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); } + void CopySample(std::uint32_t sample, StorageTexture& destination, VkImageView destinationView) { + auto* recorder = Recorder::Active(); + Require(recorder != nullptr, "depth sample copies need an active recorder"); + Require(sampleable, "DB depth copy from a depth surface the device cannot sample at its sample count"); + ApplyFastClear(); + if (useSampleShadow) { + if (sampleShadow == nullptr) createSampleShadow(); + refreshSampleShadow(false); + recorder->Keep(sampleShadow); + } + if (depthView == VK_NULL_HANDLE) { + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = useSampleShadow ? sampleShadow->image : image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = hostFormat; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &depthView), "vkCreateImageView depth sample copy"); + } + Require(context.detiler != nullptr, "depth sample copies need the texture detiler"); + context.detiler->DispatchDepthSample(recorder->Commands(), depthView, target.samples > 1, destinationView, target.extent.width, target.extent.height, sample); + Recorder::CountBarriers(Recorder::CommandClass::Copy, 2); + destination.MarkDirty(); + } + 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); + } + + bool SampledElsewhere() const { + return std::any_of(textures.begin(), textures.end(), [](const auto& entry) { return entry.second.use_count() > 1; }); + } + + void NoteHostWrite() { + hostWritten->store(true, std::memory_order_release); } std::uint64_t written = ++depthWrites; + std::shared_ptr> hostWritten = std::make_shared>(false); + bool retired = false; + + bool Overlaps(std::uint64_t address, std::uint64_t bytes) const { + const auto end = address + bytes; + if (address < target.address + depthBytes() && target.address < end) return true; + return target.stencilAddress != 0 && address < target.stencilAddress + stencilBytes() && target.stencilAddress < end; + } + + bool CoveredBy(std::span> written, std::span extents) const { + const auto reaches = [&](std::uint64_t address, std::uint32_t bytesPerTexel) { + const auto [blockWidth, blockHeight] = DepthBlockExtent(bytesPerTexel); + const std::array block{65536u, blockWidth, blockHeight}; + const auto blocksPerRow = (target.extent.width + blockWidth - 1u) / blockWidth; + return std::any_of(extents.begin(), extents.end(), [&](const auto& extent) { + return extent.address == address && extent.bytesPerElement == bytesPerTexel && extent.block == block && extent.blocksPerRow == blocksPerRow && extent.width >= target.extent.width && extent.height >= target.extent.height; + }); + }; + const auto covers = [&](std::uint64_t begin, std::uint64_t end) { + for (const auto& [writtenBegin, writtenEnd] : written) { + if (writtenEnd <= begin) continue; + if (writtenBegin > begin) return false; + begin = writtenEnd; + if (begin >= end) return true; + } + return false; + }; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + if (target.samples == 1u && (reaches(target.address, d16 ? 2u : 4u) || (target.stencilAddress != 0 && reaches(target.stencilAddress, 1u)))) return true; + return covers(target.address, target.address + depthBytes()) || (target.stencilAddress != 0 && covers(target.stencilAddress, target.stencilAddress + stencilBytes())); + } + + void NoteMemory() { + depthMemory = GuestMemory::CollectWrites(target.address, static_cast(depthBytes())); + stencilMemory = target.stencilAddress != 0 ? GuestMemory::CollectWrites(target.stencilAddress, static_cast(stencilBytes())) : 0; + } + + bool OverwrittenInMemory() const { + GuestMemory::CollectWrites(target.address, static_cast(depthBytes())); + if (GuestMemory::WrittenSince(target.address, static_cast(depthBytes()), depthMemory)) return true; + if (target.stencilAddress == 0) return false; + GuestMemory::CollectWrites(target.stencilAddress, static_cast(stencilBytes())); + return GuestMemory::WrittenSince(target.stencilAddress, static_cast(stencilBytes()), stencilMemory); + } 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; @@ -180,7 +608,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) { @@ -188,11 +616,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) { @@ -204,7 +627,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) { @@ -212,16 +636,69 @@ public: if (plane.storage != nullptr) plane.replaced.push_back(plane.storage); plane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); } - copyPlane(plane, 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, stencil, false); + if (stencil || !StoreOverwrite(candidate)) copyPlane(plane, plane.storage, stencil, false); written = ++depthWrites; + NoteHostWrite(); return true; } if (std::none_of(plane.replaced.begin(), plane.replaced.end(), [&](const auto& old) { return old.lock().get() == &candidate; })) continue; @@ -232,12 +709,224 @@ public: return false; } + bool StoreBackCovering(const StorageTexture& candidate) { + for (const bool stencil : {false, true}) { + if (!CoversPlane(candidate, stencil)) continue; + auto& plane = stencil ? stencilPlane : depthPlane; + const auto source = std::const_pointer_cast(candidate.shared_from_this()); + copyPlane(plane, source, stencil, false); + written = ++depthWrites; + NoteHostWrite(); + return true; + } + return false; + } + + void TakeStorage() { + if (target.samples != 1 || hostFormat != target.format) return; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto bytes = DepthSliceBytes(target.extent, d16 ? 2u : 4u); + auto storage = StorageTexture::FindPending(target.address, bytes); + if (storage == nullptr) storage = StorageTexture::FindLive(target.address, bytes); + if (storage == nullptr || !storageFits(target, storage->Descriptor())) return; + storage->Refresh(); + if (depthPlane.staging == nullptr) depthPlane.staging = std::make_unique(context, static_cast(target.extent.width) * target.extent.height * (d16 ? 2u : 4u), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + auto* recorder = RecorderFor(context); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + if (recorder != nullptr) recorder->Keep(storage); + VkBufferImageCopy depthRegion{0, 0, 0, {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}}; + VkBufferImageCopy colorRegion = depthRegion; + colorRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + context.Function("vkCmdCopyImageToBuffer")(commands, storage->Image(), VK_IMAGE_LAYOUT_GENERAL, depthPlane.staging->Handle(), 1, &colorRegion); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdCopyBufferToImage")(commands, depthPlane.staging->Handle(), image, VK_IMAGE_LAYOUT_GENERAL, 1, &depthRegion); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); + } + + std::shared_ptr Mirror(const GuestTextureResource& resource) { + if (mirrorPlane.storage == nullptr) { + mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); + ++mirrorEpoch; + } + return mirrorPlane.storage; + } + + bool StoresUnits(const StorageTexture& mirror) const { + return mirror.BlockUnits() && mirror.TrackedUnits() == staleUnits.size(); + } + + std::shared_ptr StoreDepthPlane(const GuestTextureResource& resource, std::vector& units) { + ApplyFastClear(); + auto mirror = Mirror(resource); + auto& counters = depthPlaneStoreCounters(); + ++counters.stores; + const auto planeBytes = mirror->GuestBytes(); + if (!StoresUnits(*mirror)) { + units.assign(units.size(), true); + copyPlane(mirrorPlane, mirror, false, true, true); + mirror->MarkWholeWritten(); + counters.units += units.size(); + counters.bytes += planeBytes; + return mirror; + } + const auto rects = mirror->UnitRects(units); + copyPlane(mirrorPlane, mirror, false, true, true, &rects); + mirror->MarkUnitsWritten(units); + for (std::size_t unit = 0; unit < units.size(); ++unit) { + if (!units[unit]) continue; + ++counters.units; + counters.bytes += std::min(unitBytes, planeBytes - unit * unitBytes); + } + return mirror; + } + + void SizeUnits(std::uint64_t planeBytes) { + static const bool whole = std::getenv("APS5_DEPTH_PLANE_WHOLE_STORE") != nullptr; + const std::uint64_t bytes = !whole && target.address % 65536u == 0 ? std::uint64_t{65536} : std::max(planeBytes, 1u); + const auto count = static_cast(std::max((planeBytes + bytes - 1u) / bytes, 1u)); + if (unitBytes == bytes && staleUnits.size() == count) return; + unitBytes = bytes; + staleUnits.assign(count, false); + staleCount = 0; + unitGeneration.assign(count, 0); + } + + bool AnyStale() const { + return staleCount != 0; + } + + void MarkAllStale(std::uint64_t planeBytes) { + SizeUnits(planeBytes); + staleUnits.assign(staleUnits.size(), true); + staleCount = staleUnits.size(); + } + + void ClearStale() { + staleUnits.assign(staleUnits.size(), false); + staleCount = 0; + } + + void ClearStale(const std::vector& units) { + for (std::size_t unit = 0; unit < units.size() && unit < staleUnits.size(); ++unit) { + if (!units[unit] || !staleUnits[unit]) continue; + staleUnits[unit] = false; + --staleCount; + } + } + + std::vector StaleUnitsIn(std::uint64_t planeBytes, std::uint64_t begin, std::uint64_t end) const { + std::vector units(staleUnits.size(), false); + const auto plane = target.address; + if (units.empty() || unitBytes == 0 || begin >= end || begin >= plane + planeBytes || plane >= end) return units; + const auto from = std::max(begin, plane) - plane; + const auto to = std::min(end, plane + planeBytes) - plane; + const auto last = std::min((to - 1u) / unitBytes, units.size() - 1u); + for (auto unit = from / unitBytes; unit <= last; ++unit) units[static_cast(unit)] = staleUnits[static_cast(unit)]; + return units; + } + + std::pair UnitHull(const std::vector& units, std::uint64_t planeBytes) const { + std::uint64_t first = planeBytes; + std::uint64_t last = 0; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + if (!units[unit]) continue; + first = std::min(first, unit * unitBytes); + last = std::max(last, std::min(planeBytes, (unit + 1u) * unitBytes)); + } + return {target.address + first, target.address + std::max(first, last)}; + } + + void NoteStored(const std::vector& units, std::uint64_t generation) { + for (std::size_t unit = 0; unit < units.size() && unit < unitGeneration.size(); ++unit) { + if (units[unit]) unitGeneration[unit] = generation; + } + } + + std::vector WrittenUnits(const GuestTextureResource& resource, std::uint64_t& generation) { + const auto bytes = static_cast(DescribeSurface(resource).guestBytes); + SizeUnits(bytes); + GuestMemory::CollectWrites(resource.baseAddress, bytes); + generation = GuestMemory::TrackerGeneration(); + std::vector changed(unitGeneration.size(), false); + if (unitGeneration.size() == 1u) { + changed[0] = GuestMemory::WrittenSince(resource.baseAddress, bytes, unitGeneration[0]); + return changed; + } + const auto oldest = *std::min_element(unitGeneration.begin(), unitGeneration.end()); + if (oldest != 0u && !GuestMemory::WrittenSince(resource.baseAddress, bytes, oldest)) return changed; + std::vector stamped(unitGeneration.size(), GuestMemory::BlockUnchanged); + if (!GuestMemory::ChangedBlocks(resource.baseAddress, bytes, unitGeneration, stamped)) return changed; + for (std::size_t unit = 0; unit < changed.size(); ++unit) changed[unit] = unitGeneration[unit] != 0u && stamped[unit] == GuestMemory::BlockWritten; + return changed; + } + + void SyncFromMemory(const GuestTextureResource& resource) { + std::uint64_t generation = 0; + const auto changed = WrittenUnits(resource, generation); + if ((pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 || !anyUnit(changed)) { + unitGeneration.assign(unitGeneration.size(), generation); + return; + } + ClearStale(changed); + { + struct Syncing { + bool& flag; + ~Syncing() { flag = false; } + } guard{syncing}; + syncing = true; + if (mirrorPlane.storage == nullptr) { + mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); + ++mirrorEpoch; + } else { + static_cast(mirrorPlane.storage->Refresh()); + } + } + unitGeneration.assign(unitGeneration.size(), generation); + const auto mirror = mirrorPlane.storage; + if (StoresUnits(*mirror)) { + const auto rects = mirror->UnitRects(changed); + copyPlane(mirrorPlane, mirror, false, false, true, &rects); + } else { + ClearStale(); + copyPlane(mirrorPlane, mirror, false, false, true); + } + written = ++depthWrites; + } + + void DropMirror() { + mirrorPlane = {}; + } + + std::vector staleUnits; + std::size_t staleCount = 0; + std::vector unitGeneration; + std::uint64_t unitBytes = 0; + bool syncing = false; + std::uint64_t scannedEpoch = 0; + const Context context; const DepthTarget target; const VkFormat hostFormat; VkImage image = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; + VkImageView depthView = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; + std::shared_ptr> active = std::make_shared>(true); + bool sampleable = true; + bool samplingActive = false; + DepthSamplingState sampling; + bool clearPending = false; + std::uint64_t clearGeneration = 0u; + bool clearCompletionQueued = false; + std::shared_ptr owned; + std::shared_ptr sampleShadow; + bool useSampleShadow = false; + std::uint64_t sampleShadowWritten = 0u; private: struct PlaneStorage { @@ -245,40 +934,253 @@ private: std::vector> replaced; std::unique_ptr staging; }; - std::map, std::shared_ptr> textures; + 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; - void copyPlane(PlaneStorage& plane, bool stencil, bool toStorage) { - auto* recorder = Recorder::Active(); - Require(recorder != nullptr, "depth surface storage copies need an active recorder"); + void createSampleShadow() { + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = hostFormat; + info.extent = {target.extent.width, target.extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = static_cast(target.samples); + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage sampled depth shadow"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory sampled depth shadow"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory sampled depth shadow"); + sampleShadow = std::make_shared(context, image, memory); + image = VK_NULL_HANDLE; + memory = VK_NULL_HANDLE; + auto* recorder = RecorderFor(context); + if (recorder != nullptr) recorder->Keep(sampleShadow); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = sampleShadow->image; + barrier.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Draw, 1); + } catch (...) { + if (image != VK_NULL_HANDLE) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, memory, nullptr); + throw; + } + } + + void refreshSampleShadow(bool drawn) { + Require(sampleShadow != nullptr, "native multisample depth shadow was not created"); + if (!drawn && sampleShadowWritten == written) return; + auto* recorder = RecorderFor(context); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + if (recorder != nullptr) recorder->Keep(sampleShadow); + constexpr VkAccessFlags accesses = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, accesses, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + VkImageCopy region{}; + region.srcSubresource = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}; + region.dstSubresource = region.srcSubresource; + region.extent = {target.extent.width, target.extent.height, 1}; + context.Function("vkCmdCopyImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, sampleShadow->image, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, accesses); + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + sampleShadowWritten = written; + } + + std::shared_ptr unormStencil(const std::array& key, VkComponentMapping components, VkImageViewType viewType, bool drawn) { + if (stencilCopy.image == VK_NULL_HANDLE) { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + 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); + stencilCopy.memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "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; - const auto bytes = static_cast(target.extent.width) * target.extent.height * (stencil ? 1u : d16 ? 2u : 4u); + return DepthSliceBytes(target.extent, d16 ? 2u : 4u); + } + + std::uint64_t stencilBytes() const { + return DepthSliceBytes(target.extent, 1u); + } + PlaneStorage mirrorPlane; + + bool CoversPlane(const StorageTexture& storage, bool stencil) const { + const auto& resource = storage.Descriptor(); + const auto address = stencil ? target.stencilAddress : target.address; + if (address == 0 || resource.baseAddress != address || resource.width != target.extent.width || resource.height != target.extent.height || resource.baseLevel != 0 || resource.mipCount != 1) return false; + if (resource.dimension != TextureDimension::k2D && (resource.dimension != TextureDimension::k2DArray || resource.baseArray != 0 || resource.depthOrLastArray != 0)) return false; + if (target.samples != 1 || (!stencil && hostFormat != target.format)) return false; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + return ResolveTextureFormat(resource.format) == (stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT); + } + + void copyPlane(PlaneStorage& plane, const std::shared_ptr& storage, bool stencil, bool toStorage, bool ownBatch = false, const std::vector* rects = nullptr) { + if (rects != nullptr && rects->empty()) return; + auto* recorder = Recorder::Active(); + Require(recorder != nullptr || ownBatch, "depth surface storage copies need an active recorder"); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const std::uint32_t texelBytes = stencil ? 1u : d16 ? 2u : 4u; + const auto bytes = static_cast(target.extent.width) * target.extent.height * texelBytes; if (plane.staging == nullptr) plane.staging = std::make_unique(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); - const auto commands = recorder->Commands(); - recorder->Keep(plane.storage); - const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy); - VkBufferImageCopy planeRegion{0, 0, 0, {stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}}; - VkBufferImageCopy colorRegion = planeRegion; - colorRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - const auto sourceImage = toStorage ? image : plane.storage->Image(); - const auto destinationImage = toStorage ? plane.storage->Image() : image; + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + if (recorder != nullptr) recorder->Keep(storage); + const std::uint32_t timing = recorder != nullptr ? recorder->BeginGpuTiming(Recorder::CommandClass::Copy) : 0u; + const VkBufferImageCopy planeRegion{0, 0, 0, {stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}}; + std::vector planeRegions; + std::size_t moved = bytes; + if (rects == nullptr) { + planeRegions.push_back(planeRegion); + } else { + moved = 0; + for (const auto& rect : *rects) { + auto region = planeRegion; + region.bufferOffset = (static_cast(rect.offset.y) * target.extent.width + static_cast(rect.offset.x)) * texelBytes; + region.bufferRowLength = target.extent.width; + region.imageOffset = {rect.offset.x, rect.offset.y, 0}; + region.imageExtent = {rect.extent.width, rect.extent.height, 1}; + planeRegions.push_back(region); + moved += static_cast(rect.extent.width) * rect.extent.height * texelBytes; + } + } + auto colorRegions = planeRegions; + for (auto& region : colorRegions) region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + const auto count = static_cast(planeRegions.size()); + const auto sourceImage = toStorage ? image : storage->Image(); + const auto destinationImage = toStorage ? storage->Image() : image; RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); - context.Function("vkCmdCopyImageToBuffer")(commands, sourceImage, VK_IMAGE_LAYOUT_GENERAL, plane.staging->Handle(), 1, toStorage ? &planeRegion : &colorRegion); + context.Function("vkCmdCopyImageToBuffer")(commands, sourceImage, VK_IMAGE_LAYOUT_GENERAL, plane.staging->Handle(), count, toStorage ? planeRegions.data() : colorRegions.data()); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); - context.Function("vkCmdCopyBufferToImage")(commands, plane.staging->Handle(), destinationImage, VK_IMAGE_LAYOUT_GENERAL, 1, toStorage ? &colorRegion : &planeRegion); + context.Function("vkCmdCopyBufferToImage")(commands, plane.staging->Handle(), destinationImage, VK_IMAGE_LAYOUT_GENERAL, count, toStorage ? colorRegions.data() : planeRegions.data()); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + if (batch) { + batch->SubmitAndWait(); + return; + } Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); - recorder->EndGpuTiming(timing, bytes); + recorder->EndGpuTiming(timing, moved); } void release() noexcept { depthPlane = {}; stencilPlane = {}; + releaseStencilCopy(); + mirrorPlane = {}; textures.clear(); + if (depthView) context.Function("vkDestroyImageView")(context.device, depthView, 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 (owned != nullptr) owned.reset(); + else { + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + } view = VK_NULL_HANDLE; image = VK_NULL_HANDLE; memory = VK_NULL_HANDLE; @@ -313,9 +1215,10 @@ public: VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory depth plane copy"); + memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth plane copy"); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory depth plane copy"); - staging = std::make_unique(context, static_cast(sliceBytes()) * layers, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + stagingSize = static_cast(sliceBytes()) * layers; + staging = std::make_unique(context, stagingSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } catch (...) { release(); throw; @@ -325,10 +1228,21 @@ public: DepthPlaneCopy(const DepthPlaneCopy&) = delete; DepthPlaneCopy& operator=(const DepthPlaneCopy&) = delete; - std::shared_ptr Refresh(std::span slices, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType) { + std::shared_ptr Refresh(std::span slices, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer) { Require(slices.size() == layers, "depth plane copy slices do not match its layers"); const auto geometry = DescribeSurface(resource); - Require(geometry.layers == layers && geometry.sliceLinearBytes == sliceBytes() && !geometry.mips.empty(), "depth plane copy geometry does not match its layers"); + const bool guestFill = std::any_of(slices.begin(), slices.end(), [](const auto& slice) { return slice.image == VK_NULL_HANDLE; }); + if (guestFill && (geometry.layers != layers || geometry.sliceLinearBytes < sliceBytes() || geometry.mips.empty())) { + char text[320]; + std::snprintf(text, sizeof(text), "AGC graphics: depth plane copy geometry does not match its layers (copy %ux%u, %u layers, %llu bytes a slice; texture %ux%u guest format %u, dimension %d, tile mode %u, %u layers, %llu bytes a slice, %zu mips)", extent.width, extent.height, layers, static_cast(sliceBytes()), resource.width, resource.height, resource.format, static_cast(resource.dimension), resource.tileMode, geometry.layers, static_cast(geometry.sliceLinearBytes), geometry.mips.size()); + throw std::runtime_error(text); + } + const auto stagingBytes = static_cast(geometry.sliceLinearBytes) * layers; + if (stagingBytes > stagingSize) { + retiredStaging.push_back(std::move(staging)); + staging = std::make_unique(context, stagingBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + stagingSize = stagingBytes; + } std::vector> uploads; std::vector uploadLayers; std::vector copiedLayers; @@ -355,10 +1269,10 @@ public: held = current; } if (!copiedLayers.empty() || !uploads.empty()) record(slices, resource, geometry, copiedLayers, uploads, uploadLayers); - const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a), static_cast(viewType)}; + const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a), static_cast(viewType), baseLayer}; auto& texture = textures[key]; if (texture == nullptr) { - texture = std::make_shared(context, image, format, aspect(), components, viewType); + texture = std::make_shared(context, image, format, aspect(), components, viewType, baseLayer); texture->MarkRefreshedPerUse(); } return texture; @@ -381,7 +1295,9 @@ private: VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; std::unique_ptr staging; - std::map, std::shared_ptr> textures; + std::size_t stagingSize = 0; + std::vector> retiredStaging; + std::map, std::shared_ptr> textures; std::vector guestLayers; std::vector surfaceLayers; bool filled = false; @@ -398,9 +1314,11 @@ private: const auto barrier = context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier"); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); std::vector regions; + const auto& mip = geometry.mips.front(); const auto region = [&](std::uint32_t layer, VkImageAspectFlags aspectMask, std::uint32_t imageLayer) { VkBufferImageCopy copy{}; - copy.bufferOffset = sliceBytes() * layer; + copy.bufferOffset = geometry.LinearLayerOffset(layer) + mip.linearOffset; + copy.bufferRowLength = mip.pitchBytes / (format == VK_FORMAT_D32_SFLOAT ? 4u : 2u); copy.imageSubresource = {aspectMask, 0, imageLayer, 1}; copy.imageExtent = {extent.width, extent.height, 1}; return copy; @@ -474,7 +1392,7 @@ struct LayeredSlice { class LayeredDepthStorage { public: - LayeredDepthStorage(const Context& context, const GuestTextureResource& resource, const DepthTarget& base, std::uint32_t bytesPerTexel, std::uint32_t layers) : context(context), base(base), extent(base.extent), bytesPerTexel(bytesPerTexel), layers(layers) { + LayeredDepthStorage(const Context& context, const GuestTextureResource& resource, const DepthTarget& base, std::uint32_t bytesPerTexel, std::uint32_t firstLayer, std::uint32_t layers) : context(context), base(base), extent(base.extent), bytesPerTexel(bytesPerTexel), firstLayer(firstLayer), layers(layers) { const auto bytes = DescribeSurface(resource).guestBytes; StorageTexture::FlushPending(resource.baseAddress, static_cast(bytes), nullptr, "storage image creation", PublishScope::None); storage = StorageTexture::Create(context, *context.detiler, resource, 0); @@ -527,6 +1445,7 @@ public: const DepthTarget base; const VkExtent2D extent; const std::uint32_t bytesPerTexel; + const std::uint32_t firstLayer; const std::uint32_t layers; std::shared_ptr storage; @@ -538,17 +1457,17 @@ private: } }; -std::map, std::unique_ptr>& layeredStorages() { - static auto* storages = new std::map, std::unique_ptr>(); +std::map, std::unique_ptr>& layeredStorages() { + static auto* storages = new std::map, std::unique_ptr>(); return *storages; } bool sameSurface(const DepthTarget& a, const DepthTarget& b) { - return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format && a.samples == b.samples; + return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format && a.samples == b.samples && (a.samples == 1u || (a.htileAddress == b.htileAddress && a.htileStencil == b.htileStencil && a.depthSwizzle == b.depthSwizzle && a.pipeBankXor == b.pipeBankXor && a.firstSlice == b.firstSlice && a.lastSlice == b.lastSlice && a.iterate256 == b.iterate256 && a.htilePipeAligned == b.htilePipeAligned && a.htileVrsEncoding == b.htileVrsEncoding && a.htileDepthOnly == b.htileDepthOnly)); } -std::mutex& surfacesMutex() { - static std::mutex mutex; +std::recursive_mutex& surfacesMutex() { + static std::recursive_mutex mutex; return mutex; } @@ -576,64 +1495,421 @@ PlaneOwners& stencilOwners() { return *owners; } -void takePlane(DepthSurface& surface, PlaneOwners& owners, std::uint64_t address, VkImageAspectFlagBits aspect) { +void takePlane(DepthSurface& surface, PlaneOwners& owners, std::uint64_t address, VkImageAspectFlagBits aspect, bool created, VkImageAspectFlags clearAspects) { auto& entry = owners[{surface.context.device, address}]; auto* owner = entry.surface; - if (owner != nullptr && owner != &surface && owner->target.format == surface.target.format && owner->target.samples == surface.target.samples && owner->target.extent.width == surface.target.extent.width && owner->target.extent.height == surface.target.extent.height) { - if ((owner->pendingClear & aspect) != 0) surface.pendingClear |= aspect; - else surface.CopyAspectFrom(*owner, aspect); + 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 && (!created || (clearAspects & aspect) == 0)) { + if ((owner->pendingClear & aspect) != 0) { + if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) surface.depthFill = owner->depthFill; + else surface.stencilFill = owner->stencilFill; + if (!created || (aspect == VK_IMAGE_ASPECT_DEPTH_BIT ? surface.depthFill.has_value() : surface.stencilFill.has_value())) surface.pendingClear |= aspect; + } else { + surface.CopyAspectFrom(*owner, aspect); + } } entry = {&surface, ++planeBinds}; } -void takePlanes(DepthSurface& surface) { - takePlane(surface, depthOwners(), surface.target.address, VK_IMAGE_ASPECT_DEPTH_BIT); - if (surface.target.stencilAddress != 0) takePlane(surface, stencilOwners(), surface.target.stencilAddress, VK_IMAGE_ASPECT_STENCIL_BIT); +void takePlanes(DepthSurface& surface, bool created, VkImageAspectFlags clearAspects) { + takePlane(surface, depthOwners(), surface.target.address, VK_IMAGE_ASPECT_DEPTH_BIT, created, clearAspects); + if (surface.target.stencilAddress != 0) takePlane(surface, stencilOwners(), surface.target.stencilAddress, VK_IMAGE_ASPECT_STENCIL_BIT, created, clearAspects); } DepthSurface* depthSlice(VkDevice device, std::uint64_t address, const DepthTarget& base) { if (const auto found = depthOwners().find({device, address}); found != depthOwners().end()) { const auto& target = found->second.surface->target; - if (target.extent.width == base.extent.width && target.extent.height == base.extent.height && target.format == base.format) return found->second.surface; + if (found->second.surface->active->load(std::memory_order_acquire) && target.extent.width == base.extent.width && target.extent.height == base.extent.height && target.format == base.format) return found->second.surface; } const auto& list = surfaces(); - const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == device && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format; }); + const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->active->load(std::memory_order_acquire) && surface->context.device == device && !surface->retired && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format; }); return slice == list.rend() ? nullptr : slice->get(); } DepthSurface* planeSurface(VkDevice device, std::uint64_t address) { const auto depth = depthOwners().find({device, address}); const auto stencil = stencilOwners().find({device, address}); - const bool hasDepth = depth != depthOwners().end(); - const bool hasStencil = stencil != stencilOwners().end(); + const bool hasDepth = depth != depthOwners().end() && depth->second.surface->active->load(std::memory_order_acquire); + const bool hasStencil = stencil != stencilOwners().end() && stencil->second.surface->active->load(std::memory_order_acquire); if (hasDepth && (!hasStencil || depth->second.serial > stencil->second.serial)) return depth->second.surface; if (hasStencil) return stencil->second.surface; const auto& list = surfaces(); const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { - return surface->context.device == device && (surface->target.address == address || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == address)); + return surface->active->load(std::memory_order_acquire) && surface->context.device == device && !surface->retired && (surface->target.address == address || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == address)); }); return found == list.rend() ? nullptr : found->get(); } -std::set& clearedHtiles() { - static auto* cleared = new std::set(); +void settlePendingHtileClear(const Context& context, std::uint64_t address) { + if (Recorder::InCompletion()) return; + auto* recorder = RecorderFor(context); + if (recorder == nullptr) return; + std::pair metadata; + { + std::lock_guard lock(surfacesMutex()); + const auto* surface = planeSurface(context.device, address); + if (surface == nullptr || !surface->samplingActive || surface->clearGeneration == 0u || !surface->clearCompletionQueued || !surface->sampling.refusal.empty()) return; + const auto& range = surface->sampling.initial[1]; + metadata = {range.address, range.bytes}; + } + if (recorder->PendingWriteOverlaps(metadata.first, metadata.second)) recorder->SyncThrough(metadata.first, metadata.second); +} + +std::map& clearedHtiles() { + static auto* cleared = new std::map(); return *cleared; } -const DepthSurface* drawnDepth = nullptr; +bool takeClearedHtile(const DepthTarget& target) { + if (target.htileAddress == 0) return false; + auto& cleared = clearedHtiles(); + const auto found = std::find_if(cleared.begin(), cleared.end(), [&](const auto& entry) { return HtileFillCovers(target.htileAddress, target.extent, entry.first, entry.second); }); + if (found == cleared.end()) return false; + cleared.erase(found); + return true; +} +DepthSurface* drawnDepth = nullptr; + +std::atomic surfacesSerial{0}; +std::atomic surfacesReleased{0}; + +void releaseRetired(const Context& context) { + auto* recorder = Recorder::Active(); + if (recorder != nullptr && recorder->Device() != context.device) return; + auto& list = surfaces(); + for (auto it = list.begin(); it != list.end();) { + const auto& surface = **it; + if (!surface.retired || surface.context.device != context.device || surface.target.samples != 1u || surface.SampledElsewhere()) { + ++it; + continue; + } + if (drawnDepth == &surface) drawnDepth = nullptr; + std::shared_ptr released = std::move(*it); + it = list.erase(it); + surfacesReleased.fetch_add(1, std::memory_order_release); + if (recorder != nullptr) recorder->Keep(std::move(released)); + } +} + +std::uint32_t expandedHtileWord(std::uint32_t word, bool stencilInHtile, VkImageAspectFlags cleared) { + if (!stencilInHtile) return (cleared & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 ? 0xfffc000fu : word; + if ((cleared & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) word = (word & 0x3f0u) | 0xfffff00fu; + if ((cleared & VK_IMAGE_ASPECT_STENCIL_BIT) != 0) word |= 0x3f0u; + return word; +} + +std::mutex& htileReadsMutex() { + static std::mutex mutex; + return mutex; +} + +std::map& htileReads() { + static auto* reads = new std::map(); + return *reads; +} + +void forgetHtileReads(std::uint64_t address, std::size_t bytes) { + std::lock_guard lock(htileReadsMutex()); + auto& reads = htileReads(); + reads.erase(reads.lower_bound(address), reads.lower_bound(address + bytes)); +} + +bool RoutesToNativeDepthOwner(const DepthTarget& target) { + if (target.samples != 1u) return true; + if (target.htileAddress == 0 || !anyDepthGuard().load(std::memory_order_acquire)) return false; + const auto bytes = static_cast(HtileMetadataBytes(target.extent)); + std::shared_lock lock(depthGuardMutex()); + return std::ranges::any_of(depthGuards(), [&](const auto& range) { return range.active->load(std::memory_order_acquire) && target.htileAddress < range.address + range.bytes && range.address < target.htileAddress + bytes; }); +} + +std::optional htileAfterPendingFills(const Recorder& recorder, std::uint64_t address, std::size_t bytes) { + auto writes = recorder.PendingWritesOver(address, bytes); + std::sort(writes.begin(), writes.end(), [](const Recorder::PendingWrite& a, const Recorder::PendingWrite& b) { return a.note > b.note; }); + std::vector> unwritten{{address, address + bytes}}; + std::optional landed; + for (const auto& write : writes) { + if (unwritten.empty()) break; + std::vector> rest; + bool covered = false; + for (const auto& [from, to] : unwritten) { + if (write.end <= from || to <= write.begin) { + rest.emplace_back(from, to); + continue; + } + covered = true; + if (from < write.begin) rest.emplace_back(from, write.begin); + if (write.end < to) rest.emplace_back(write.end, to); + } + unwritten = std::move(rest); + if (!covered) continue; + if (write.word < 0 || (landed.has_value() && *landed != static_cast(write.word))) return std::nullopt; + landed = static_cast(write.word); + } + if (!landed.has_value()) return std::nullopt; + for (const auto& [from, to] : unwritten) { + if (from % 4u != 0 || to % 4u != 0) return std::nullopt; + const auto* words = reinterpret_cast(from); + for (std::uint64_t index = 0; index < (to - from) / 4u; ++index) { + if (words[index] != *landed) return std::nullopt; + } + } + return landed; +} + +bool storeHtileOnGpu(const Context& context, Recorder& recorder, std::uint64_t address, std::size_t bytes, std::uint32_t word) { + if (address % 4u != 0 || bytes % 4u != 0) return false; + const auto* import = HostImportFor(context, address, bytes); + if (import == nullptr) return false; + recorder.FlushKeyStoresOverlapping(address, bytes); + recorder.FlushStoresOverlapping(address, bytes); + const auto commands = recorder.Commands(); + const auto timing = recorder.BeginGpuTiming(Recorder::CommandClass::Fill); + 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); + Recorder::CountBarriers(Recorder::CommandClass::Fill); + context.Resolved(&DeviceFunctions::cmdFillBuffer, "vkCmdFillBuffer")(commands, import->buffer, address - import->base, bytes, word); + constexpr VkAccessFlags filledAccess = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_HOST_READ_BIT; + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, filledAccess); + Recorder::CountBarriers(Recorder::CommandClass::Fill); + recorder.EndGpuTiming(timing, bytes); + recorder.MarkCovered(filledAccess); + recorder.NotePendingWordFill(address, bytes, word); + GuestMemory::MarkWritten(address, bytes); + return true; +} + +VkImageAspectFlags storeBackClearedHtile(const Context& context, const DepthTarget& target) { + if (target.htileAddress == 0) return 0; + const auto bytes = static_cast(HtileMetadataBytes(target.extent)); + if (!GuestMemory::Accessible(reinterpret_cast(target.htileAddress), bytes, true)) return 0; + const auto collected = GuestMemory::CollectWrites(target.htileAddress, bytes); + auto* recorder = RecorderFor(context); + const bool pending = recorder != nullptr && recorder->PendingWriteOverlaps(target.htileAddress, bytes); + if (!pending) { + std::lock_guard lock(htileReadsMutex()); + const auto found = htileReads().find(target.htileAddress); + if (found != htileReads().end() && found->second != 0 && GuestMemory::UnchangedSince(target.htileAddress, bytes, found->second)) return 0; + } + static const bool pendingFills = std::getenv("APS5_NO_PENDING_HTILE_FILLS") == nullptr; + if (pending && pendingFills) { + std::lock_guard gpu(GuestMemory::GpuMutex()); + if (const auto landed = htileAfterPendingFills(*recorder, target.htileAddress, bytes)) { + const VkImageAspectFlags cleared = (VK_IMAGE_ASPECT_DEPTH_BIT | (target.htileStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u)) & HtileFillClears(*landed, target.htileStencil); + if (cleared == 0) { + std::lock_guard lock(htileReadsMutex()); + htileReads()[target.htileAddress] = collected; + return 0; + } + if (storeHtileOnGpu(context, *recorder, target.htileAddress, bytes, expandedHtileWord(*landed, target.htileStencil, cleared))) { + const auto generation = GuestMemory::CollectWrites(target.htileAddress, bytes); + std::lock_guard lock(htileReadsMutex()); + htileReads()[target.htileAddress] = generation; + return cleared; + } + } + } + std::vector words(bytes / 4u); + GuestMemory::Read(target.htileAddress, std::as_writable_bytes(std::span(words)), 4); + VkImageAspectFlags cleared = VK_IMAGE_ASPECT_DEPTH_BIT | (target.htileStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); + for (const auto word : words) cleared &= HtileFillClears(word, target.htileStencil); + auto generation = collected; + if (cleared != 0) { + for (auto& word : words) word = expandedHtileWord(word, target.htileStencil, cleared); + GuestMemory::Write(target.htileAddress, std::as_bytes(std::span(words)), 4); + generation = GuestMemory::CollectWrites(target.htileAddress, bytes); + } + std::lock_guard lock(htileReadsMutex()); + htileReads()[target.htileAddress] = generation; + return cleared; +} + +struct StalePlanes { + std::mutex mutex; + std::vector> ranges; + std::atomic begin{std::numeric_limits::max()}; + std::atomic end{0}; +}; + +StalePlanes& stalePlanes() { + static auto* planes = new StalePlanes(); + return *planes; +} + +void publishStaleHull(StalePlanes& planes) { + auto begin = std::numeric_limits::max(); + std::uint64_t end = 0; + for (const auto& range : planes.ranges) { + begin = std::min(begin, range.first); + end = std::max(end, range.second); + } + planes.begin.store(begin, std::memory_order_release); + planes.end.store(end, std::memory_order_release); +} + +void noteStalePlane(std::uint64_t begin, std::uint64_t end) { + auto& planes = stalePlanes(); + std::lock_guard lock(planes.mutex); + if (std::find(planes.ranges.begin(), planes.ranges.end(), std::pair{begin, end}) != planes.ranges.end()) return; + planes.ranges.emplace_back(begin, end); + publishStaleHull(planes); +} + +bool overlapsStalePlane(std::uint64_t begin, std::uint64_t end) { + auto& planes = stalePlanes(); + if (begin >= planes.end.load(std::memory_order_acquire) || planes.begin.load(std::memory_order_acquire) >= end) return false; + std::lock_guard lock(planes.mutex); + return std::any_of(planes.ranges.begin(), planes.ranges.end(), [&](const auto& range) { return begin < range.second && range.first < end; }); +} + +bool mirrorable(const DepthTarget& target) { + return (target.format == VK_FORMAT_D32_SFLOAT || target.format == VK_FORMAT_D32_SFLOAT_S8_UINT) && target.samples == 1 && target.depthSwizzle == XorSwizzleMode(TextureTileMode::kZ64KBX); +} + +GuestTextureResource mirrorResource(const DepthTarget& target) { + GuestTextureResource resource{}; + resource.baseAddress = target.address; + resource.width = target.extent.width; + resource.height = target.extent.height; + resource.mipCount = 1; + resource.allocatedMipCount = 1; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 22; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + return resource; +} + +std::pair stalePlaneRange(const DepthSurface& surface) { + return {surface.target.address, surface.target.address + DescribeSurface(mirrorResource(surface.target)).guestBytes}; +} + +void retire(DepthSurface& surface) { + surface.retired = true; + for (auto* owners : {&depthOwners(), &stencilOwners()}) { + std::erase_if(*owners, [&](const auto& entry) { return entry.second.surface == &surface; }); + } + if (!surface.AnyStale()) return; + surface.ClearStale(); + const auto plane = stalePlaneRange(surface); + if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& other) { return other->AnyStale() && stalePlaneRange(*other) == plane; })) return; + auto& planes = stalePlanes(); + std::lock_guard lock(planes.mutex); + std::erase(planes.ranges, plane); + publishStaleHull(planes); +} + +void retireStoringStale(DepthSurface& surface) { + if (!surface.AnyStale() || !mirrorable(surface.target)) { + retire(surface); + return; + } + const auto resource = mirrorResource(surface.target); + const auto planeBytes = DescribeSurface(resource).guestBytes; + std::uint64_t generation = 0; + const auto written = surface.WrittenUnits(resource, generation); + auto units = surface.staleUnits; + for (std::size_t unit = 0; unit < units.size() && unit < written.size(); ++unit) { + if (written[unit]) units[unit] = false; + } + std::shared_ptr mirror; + if (anyUnit(units) && (!anyUnit(written) || surface.StoresUnits(*surface.Mirror(resource)))) mirror = surface.StoreDepthPlane(resource, units); + retire(surface); + if (mirror == nullptr) return; + const auto [begin, end] = surface.UnitHull(units, planeBytes); + if (StorageTexture::FlushPending(begin, static_cast(end - begin), nullptr, "depth plane store")) surface.NoteStored(units, GuestMemory::TrackerGeneration()); +} +} + +std::uint64_t DepthSurfacesSerial() { + return surfacesSerial.load(std::memory_order_acquire); +} + +std::uint64_t DepthSurfacesReleased() { + return surfacesReleased.load(std::memory_order_acquire); } std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel) { - const std::uint32_t blockWidth = bytesPerTexel == 4 ? 128u : 256u; - const std::uint32_t blockHeight = bytesPerTexel == 1 ? 256u : 128u; + const auto [blockWidth, blockHeight] = DepthBlockExtent(bytesPerTexel); const auto width = static_cast((extent.width + blockWidth - 1) / blockWidth * blockWidth); const auto height = static_cast((extent.height + blockHeight - 1) / blockHeight * blockHeight); return width * height * bytesPerTexel; } +std::uint64_t MultisampledDepthBytes_nid_no_patch(VkExtent2D extent, std::uint32_t bytesPerTexel, std::uint32_t samples) { + Require(extent.width != 0u && extent.width <= 16384u && extent.height != 0u && extent.height <= 16384u, "invalid multisample depth extent"); + Require((bytesPerTexel == 1u || bytesPerTexel == 2u || bytesPerTexel == 4u) && (samples == 2u || samples == 4u || samples == 8u), "unsupported multisample depth footprint"); + const auto sampleBits = std::countr_zero(samples); + const auto pixelBits = 16u - std::countr_zero(bytesPerTexel) - sampleBits; + const auto widthBits = (pixelBits + ((sampleBits & 1u) == 0u ? 1u : 0u)) / 2u; + const auto blockWidth = 1u << widthBits; + const auto blockHeight = 1u << (pixelBits - widthBits); + const auto width = static_cast((extent.width + blockWidth - 1u) / blockWidth * blockWidth); + const auto height = static_cast((extent.height + blockHeight - 1u) / blockHeight * blockHeight); + return width * height * bytesPerTexel * samples; +} + +void RequireNoResidentDepthRead_nid_no_patch(std::uint64_t address, std::size_t bytes) { + if (bytes == 0u || !anyDepthGuard().load(std::memory_order_acquire)) return; + Require(address <= std::numeric_limits::max() - bytes, "native depth read range overflows"); + const auto overlaps = [&](const DepthGuard& range) { return address < range.address + range.bytes && range.address < address + bytes; }; + { + std::shared_lock lock(depthGuardMutex()); + if (std::ranges::none_of(depthGuards(), overlaps)) return; + } + std::unique_lock lock(depthGuardMutex()); + for (const auto& range : depthGuards()) { + if (overlaps(range)) Require(!range.hostWritten->load(std::memory_order_acquire), "sample-preserving guest publication of native multisample depth is not implemented"); + } + for (const auto& range : depthGuards()) { + if (overlaps(range)) range.active->store(false, std::memory_order_release); + } + std::erase_if(depthGuards(), [](const auto& range) { return !range.active->load(std::memory_order_acquire); }); + anyDepthGuard().store(!depthGuards().empty(), std::memory_order_release); +} + +void NoteDepthSurfaceWrite(const Context& context, const DepthTarget& target) { + if (target.samples == 1u || !anyDepthGuard().load(std::memory_order_acquire)) return; + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (surface->active->load(std::memory_order_acquire) && surface->context.device == context.device && sameSurface(surface->target, target)) surface->NoteHostWrite(); + } +} + +bool HasResidentDepthGuards_nid_no_patch() { + return anyDepthGuard().load(std::memory_order_acquire); +} + +void RetireUnmappedDepth_nid_no_patch(std::uintptr_t address, std::size_t bytes) { + if (bytes == 0u || !HasResidentDepthGuards_nid_no_patch()) return; + Require(address <= std::numeric_limits::max() - bytes, "native depth release range overflows"); + const auto end = address + bytes; + std::unique_lock lock(depthGuardMutex()); + for (auto& range : depthGuards()) { + std::vector remaining; + remaining.reserve(range.live.size() + 1u); + for (const auto& part : range.live) { + const auto finish = part.address + part.bytes; + const auto first = std::max(address, part.address); + const auto last = std::min(end, finish); + if (first >= last) remaining.push_back(part); + else { + if (part.address < first) remaining.push_back({part.address, static_cast(first - part.address), 0u}); + if (last < finish) remaining.push_back({last, static_cast(finish - last), 0u}); + } + } + range.live = std::move(remaining); + } + for (const auto& range : depthGuards()) { + if (std::ranges::all_of(depthGuards(), [&](const auto& part) { return part.active != range.active || part.live.empty(); })) range.active->store(false, std::memory_order_release); + } + std::erase_if(depthGuards(), [](const auto& range) { return !range.active->load(std::memory_order_acquire); }); + anyDepthGuard().store(!depthGuards().empty(), std::memory_order_release); +} + 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); @@ -668,32 +1944,194 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const return constant * 256.0f; } -VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { - std::lock_guard lock(surfacesMutex()); - const bool cleared = target.htileAddress != 0 && clearedHtiles().erase(target.htileAddress) != 0; +VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten) { + const auto htileCleared = RoutesToNativeDepthOwner(target) ? VkImageAspectFlags{0} : storeBackClearedHtile(context, target); + std::lock_guard gpu(GuestMemory::GpuMutex()); + settlePendingHtileClear(context, target.address); + std::unique_lock lock(surfacesMutex()); + const bool cleared = takeClearedHtile(target); DepthSurface* surface = nullptr; for (const auto& candidate : surfaces()) { - if (candidate->context.device == context.device && sameSurface(candidate->target, target)) { + if (candidate->active->load(std::memory_order_acquire) && candidate->context.device == context.device && sameSurface(candidate->target, target)) { surface = candidate.get(); break; } } const bool created = surface == nullptr; + GuestAllocations::Lease creationLease; if (created) { - surfaces().push_back(std::make_unique(context, target)); + for (const auto& surface : surfaces()) { + if (!surface->active->load(std::memory_order_acquire) || !surface->samplingActive || surface->context.device != context.device) continue; + const auto bytes = target.samples == 1u ? DepthSliceBytes(target.extent, target.format == VK_FORMAT_D16_UNORM ? 2u : 4u) : MultisampledDepthBytes_nid_no_patch(target.extent, target.format == VK_FORMAT_D16_UNORM ? 2u : 4u, target.samples); + Require(target.address <= std::numeric_limits::max() - bytes && target.htileAddress <= std::numeric_limits::max() - HtileMetadataBytes(target.extent), "native depth attachment range overflows"); + for (const auto& range : surface->sampling.initial) { + Require(target.address >= range.address + range.bytes || range.address >= target.address + bytes, "native multisample depth attachment layout or metadata changed"); + if (target.stencilAddress != 0u) { + const auto stencilBytes = target.samples == 1u ? DepthSliceBytes(target.extent, 1u) : MultisampledDepthBytes_nid_no_patch(target.extent, 1u, target.samples); + Require(target.stencilAddress <= std::numeric_limits::max() - stencilBytes && (target.stencilAddress >= range.address + range.bytes || range.address >= target.stencilAddress + stencilBytes), "native multisample stencil attachment aliases a resident depth surface"); + } + if (target.htileAddress != 0u) Require(target.htileAddress >= range.address + range.bytes || range.address >= target.htileAddress + HtileMetadataBytes(target.extent), "native multisample depth attachment layout or metadata changed"); + } + } + lock.unlock(); + auto sampling = target.samples == 1u ? DepthSamplingState{} : prepareDepthSampling(context, target); + creationLease = target.samples == 1u || !sampling.refusal.empty() ? GuestAllocations::Lease{} : GuestAllocations::GuestAllocationsAcquireAll_nid_postfix(); + lock.lock(); + releaseRetired(context); + surfacesSerial.fetch_add(1, std::memory_order_release); + surfaces().push_back(std::make_unique(context, target, std::move(sampling))); + ++mirrorEpoch; + auto& fresh = *surfaces().back(); + if (target.samples != 1u && fresh.sampleable && fresh.sampling.refusal.empty()) { + std::unique_lock guardLock(depthGuardMutex()); + for (const auto& range : fresh.sampling.initial) depthGuards().push_back({context.device, range.address, range.bytes, fresh.active, {{range.address, range.bytes, 0u}}, fresh.hostWritten}); + anyDepthGuard().store(true, std::memory_order_release); + InstallResidentImageReadGuard_nid_no_patch(); + fresh.samplingActive = true; + } surface = surfaces().back().get(); } - const bool inherits = depthOwners().contains({context.device, target.address}); + surface->retired = false; + const auto owner = depthOwners().find({context.device, target.address}); + const bool inherits = owner != depthOwners().end() && owner->second.surface->active->load(std::memory_order_acquire) && !owner->second.surface->retired; surface->clearDepth = target.clearDepth; surface->clearStencil = target.clearStencil; - takePlanes(*surface); - if (cleared && (!created || inherits)) surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; + takePlanes(*surface, created, htileCleared | (cleared ? VK_IMAGE_ASPECT_DEPTH_BIT : 0u)); + if (created && !inherits && target.htileAddress == 0) surface->TakeStorage(); + if (cleared && (!created || inherits)) { + if (target.samples != 1u) surface->sampling.Refuse("native multisample depth fast-clear transition is not implemented"); + if (!created) surface->NoteHtileClear(VK_IMAGE_ASPECT_DEPTH_BIT); + } + if (!created) surface->NoteHtileClear(htileCleared); + if (mirrorable(target)) surface->SyncFromMemory(mirrorResource(target)); surface->ApplyFastClear(); - if (!created || inherits) surface->written = ++depthWrites; + if (drawnDepth != nullptr && drawnDepth != surface) drawnDepth->written = ++depthWrites; + surface->written = ++depthWrites; + surface->NoteMemory(); + if (mirrorable(target) && surface->scannedEpoch != mirrorEpoch) { + surface->scannedEpoch = mirrorEpoch; + const auto plane = stalePlaneRange(*surface); + for (const auto& other : surfaces()) { + if (other.get() == surface || other->context.device != context.device || other->written >= surface->written || !mirrorable(other->target)) continue; + const auto otherPlane = stalePlaneRange(*other); + if (otherPlane == plane || otherPlane.first >= plane.second || plane.first >= otherPlane.second) continue; + other->ClearStale(); + other->DropMirror(); + } + } + if ((depthWritten || cleared) && mirrorable(target)) { + const auto plane = target.address; + const auto planeBytes = DescribeSurface(mirrorResource(target)).guestBytes; + surface->MarkAllStale(planeBytes); + noteStalePlane(plane, plane + planeBytes); + } drawnDepth = surface; return surface->view; } +SampleOrderTarget DepthSurfaceOrderTarget(const Context& context, const DepthTarget& target) { + std::lock_guard gpu(GuestMemory::GpuMutex()); + std::unique_lock lock(surfacesMutex()); + for (const auto& candidate : surfaces()) { + if (!candidate->active->load(std::memory_order_acquire) || candidate->context.device != context.device || !sameSurface(candidate->target, target)) continue; + Require(candidate->sampleable, "the multisampled depth surface whose samples are put in the guest's order cannot be sampled"); + return {candidate->image, candidate->view, candidate->hostFormat, target.extent, target.samples, DepthSampleOrderMethod(context, target), candidate->owned}; + } + throw std::runtime_error("AGC graphics: the multisampled depth surface whose samples are put in the guest's order is not resident"); +} + +DepthSampleLocations DepthSurfaceSampleLocations(const Context& context, const DepthTarget& target) { + std::lock_guard gpu(GuestMemory::GpuMutex()); + std::unique_lock lock(surfacesMutex()); + for (const auto& candidate : surfaces()) { + if (!candidate->active->load(std::memory_order_acquire) || candidate->context.device != context.device || !sameSurface(candidate->target, target)) continue; + DepthSampleLocations depth; + depth.image = candidate->image; + depth.format = candidate->hostFormat; + depth.samples = target.samples; + depth.keep = candidate->owned; + return depth; + } + throw std::runtime_error("AGC graphics: the depth surface drawn at custom sample locations is not resident"); +} + +bool DepthPlanesStale() { + auto& planes = stalePlanes(); + return planes.begin.load(std::memory_order_acquire) < planes.end.load(std::memory_order_acquire); +} + +std::vector> FlushDepthPlanes(std::uint64_t address, std::size_t bytes, DepthPlaneScope scope) { + static thread_local bool flushing = false; + const auto end = bytes > std::numeric_limits::max() - address ? std::numeric_limits::max() : address + bytes; + if (flushing || !overlapsStalePlane(address, end)) return {}; + if (scope == DepthPlaneScope::Overlapped) { + std::lock_guard lock(surfacesMutex()); + const auto staleInRange = [&](const auto& surface) { return surface->AnyStale() && !surface->syncing && ((surface->pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 || anyUnit(surface->StaleUnitsIn(DescribeSurface(mirrorResource(surface->target)).guestBytes, address, end))); }; + if (std::none_of(surfaces().begin(), surfaces().end(), staleInRange)) return {}; + } + flushing = true; + struct Reset { + ~Reset() { flushing = false; } + } reset; + struct Stored { + DepthSurface* surface; + std::shared_ptr mirror; + std::vector units; + std::uint64_t begin; + std::uint64_t end; + bool flushed; + }; + std::vector stored; + std::vector> ranges; + { + std::lock_guard gpu(GuestMemory::GpuMutex()); + std::lock_guard lock(surfacesMutex()); + std::vector> remaining; + for (const auto& surface : surfaces()) { + if (!surface->AnyStale()) continue; + const auto resource = mirrorResource(surface->target); + const auto plane = surface->target.address; + const auto planeBytes = DescribeSurface(resource).guestBytes; + const bool overlapped = !surface->syncing && plane < end && address < plane + planeBytes; + if (overlapped && (surface->pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) surface->MarkAllStale(planeBytes); + if (overlapped && (scope == DepthPlaneScope::Whole || anyUnit(surface->StaleUnitsIn(planeBytes, address, end)))) { + surface->SyncFromMemory(resource); + auto units = scope == DepthPlaneScope::Whole ? surface->staleUnits : surface->StaleUnitsIn(planeBytes, address, end); + if (anyUnit(units)) { + auto mirror = surface->StoreDepthPlane(resource, units); + surface->ClearStale(units); + const auto hull = surface->UnitHull(units, planeBytes); + ranges.emplace_back(hull.first, hull.second - hull.first); + stored.push_back({surface.get(), std::move(mirror), std::move(units), hull.first, hull.second, false}); + } + } + if (surface->AnyStale()) remaining.emplace_back(plane, plane + planeBytes); + } + auto& planes = stalePlanes(); + std::lock_guard staleLock(planes.mutex); + planes.ranges = std::move(remaining); + publishStaleHull(planes); + } + for (auto& entry : stored) entry.flushed = StorageTexture::FlushPending(entry.begin, static_cast(entry.end - entry.begin), nullptr, "depth plane store"); + const auto generation = GuestMemory::TrackerGeneration(); + std::lock_guard lock(surfacesMutex()); + for (const auto& entry : stored) { + if (!entry.flushed || std::none_of(surfaces().begin(), surfaces().end(), [&](const auto& candidate) { return candidate.get() == entry.surface; })) continue; + entry.surface->NoteStored(entry.units, generation); + entry.surface->NoteMemory(); + } + return ranges; +} + +DepthPlaneStoreStatistics DepthPlaneStoreCounts() { + const auto& counters = depthPlaneStoreCounters(); + DepthPlaneStoreStatistics statistics; + statistics.stores = counters.stores.load(); + statistics.units = counters.units.load(); + statistics.bytes = counters.bytes.load(); + return statistics; +} + std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads) { static constexpr std::array htileMaskKernel{0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u, 0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u, 0xbf810000u}; if (userData.size() < 6 || numThreads[0] != 64 || numThreads[1] != 1 || numThreads[2] != 1) return 0; @@ -727,63 +2165,204 @@ void NoteMemoryValueFill(const MemoryValueFill& fill) { NoteDepthMetadataFill(fill.address, fill.bytes, value); } -void NoteHtileDepthClear(std::uint64_t htileAddress) { +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; + const auto address = HtileDepthClearAddress(code, userData, compute.numThreads); + if (address == 0u || (address & 15u) != 0u) return 0u; + const auto bytes = static_cast(packet[1]) * 1024u; + return address <= std::numeric_limits::max() - bytes ? static_cast(bytes) : 0u; +} + +bool NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes) { std::lock_guard lock(surfacesMutex()); + forgetHtileReads(htileAddress, bytes); bool existing = false; + bool completionCandidate = false; for (const auto& surface : surfaces()) { - if (surface->target.htileAddress != htileAddress) continue; - surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; + if (!surface->active->load(std::memory_order_acquire)) continue; + if (surface->samplingActive && surface->target.htileAddress == htileAddress) { + const auto& metadata = surface->sampling.initial[1]; + if (nativeBytes != bytes || nativeBytes < metadata.bytes || !GuestMemory::Accessible(reinterpret_cast(metadata.address), metadata.bytes) || !GuestMemory::Watched(metadata.address, metadata.bytes)) surface->sampling.Refuse("native multisample depth incomplete or unproven fast-clear transition is not implemented"); + else if (surface->clearPending || surface->clearGeneration != 0u) surface->sampling.Refuse("overlapping native multisample depth clear transitions are not implemented"); + else { + surface->clearPending = true; + surface->clearCompletionQueued = false; + completionCandidate = true; + } + existing = true; + continue; + } + if (!HtileFillCovers(surface->target.htileAddress, surface->target.extent, htileAddress, bytes)) continue; + if (surface->target.samples != 1u) surface->sampling.Refuse("native multisample depth fast-clear transition is not implemented"); + surface->NoteHtileClear(VK_IMAGE_ASPECT_DEPTH_BIT); existing = true; } - if (!existing) clearedHtiles().insert(htileAddress); + if (!existing) clearedHtiles()[htileAddress] = bytes; + return completionCandidate; +} + +void ArmHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (!surface->active->load(std::memory_order_acquire) || !surface->samplingActive || surface->target.htileAddress != htileAddress || !surface->clearPending) continue; + const auto& metadata = surface->sampling.initial[1]; + if (bytes < metadata.bytes) continue; + surface->clearGeneration = GuestMemory::CollectWritesUncached(metadata.address, metadata.bytes); + surface->clearPending = false; + if (surface->clearGeneration == 0u) surface->sampling.Refuse("native multisample depth clear requires tracked HTILE generations"); + } +} + +void QueueHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (!surface->active->load(std::memory_order_acquire) || !surface->samplingActive || surface->target.htileAddress != htileAddress || surface->clearGeneration == 0u || !surface->sampling.refusal.empty()) continue; + if (bytes >= surface->sampling.initial[1].bytes) surface->clearCompletionQueued = true; + } +} + +void CompleteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (!surface->active->load(std::memory_order_acquire) || !surface->samplingActive || surface->target.htileAddress != htileAddress || surface->clearGeneration == 0u) continue; + const auto& metadata = surface->sampling.initial[1]; + if (bytes < metadata.bytes) continue; + Require(GuestMemory::UnchangedSinceCollected(metadata.address, metadata.bytes, surface->clearGeneration), "CPU writes raced native multisample depth clear"); + GuestMemory::MarkWritten(metadata.address, metadata.bytes); + surface->clearCompletionQueued = false; + } } void ClearDepthSurfaces(VkDevice device) { + std::lock_guard gpu(GuestMemory::GpuMutex()); std::lock_guard lock(surfacesMutex()); drawnDepth = nullptr; + surfacesSerial.fetch_add(1, std::memory_order_release); std::erase_if(depthOwners(), [&](const auto& entry) { return entry.first.first == device; }); std::erase_if(stencilOwners(), [&](const auto& entry) { return entry.first.first == device; }); std::erase_if(surfaces(), [&](const auto& surface) { return surface->context.device == device; }); std::erase_if(planeCopies(), [&](const auto& entry) { return std::get<0>(entry.first) == device; }); std::erase_if(layeredStorages(), [&](const auto& entry) { return std::get<0>(entry.first) == device; }); + std::unique_lock guardLock(depthGuardMutex()); + std::erase_if(depthGuards(), [&](const auto& range) { return range.device == device; }); + anyDepthGuard().store(!depthGuards().empty(), std::memory_order_release); + UpdateResidentImageUnmapObserver_nid_no_patch(); } -std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components) { +std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType multisampleViewType) { const auto held = [&](const auto& surface) { - return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); + return surface->active->load(std::memory_order_acquire) && surface->context.device == context.device && !surface->retired && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); }; { std::lock_guard lock(surfacesMutex()); if (std::none_of(surfaces().begin(), surfaces().end(), held)) return nullptr; } std::lock_guard gpu(GuestMemory::GpuMutex()); + settlePendingHtileClear(context, resource.baseAddress); std::lock_guard lock(surfacesMutex()); auto* found = planeSurface(context.device, resource.baseAddress); if (found == nullptr) return nullptr; const auto& base = found->target; - if (resource.width != base.extent.width || resource.height != base.extent.height) return nullptr; const bool stencil = base.stencilAddress != 0 && resource.baseAddress == base.stencilAddress; + if (stencil && BytesPerElement(resource.format) != 1u) return nullptr; + if (base.samples != 1u || resource.dimension == TextureDimension::k2DMsaa) { + Require(base.samples > 1u, "multisample reader overlaps a single-sample depth attachment"); + auto* recorder = RecorderFor(context); + Require(recorder != nullptr, "a native multisampled depth target is sampled only by recorded draws and dispatches"); + found->CheckCurrent(); + found->CheckResource(resource); + Require(found->pendingClear == 0u, "native multisample depth fast-clear transition is not implemented"); + RecordMemoryBarrier(context, recorder->Commands(), VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + Recorder::CountBarriers(Recorder::CommandClass::Draw, 1); + return found->Sampled(words, resource, components, found == drawnDepth, multisampleViewType); + } + if (resource.width != base.extent.width || resource.height != base.extent.height || resource.baseLevel != 0) return nullptr; const bool d16 = base.format == VK_FORMAT_D16_UNORM || base.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto format = ResolveTextureFormat(resource.format); const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); const bool integer = d16 && format == VK_FORMAT_R16_UINT && !depthBits; if (!stencil && format != (d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT) && !depthBits && !integer) return nullptr; - if (stencil && BytesPerElement(resource.format) != 1u) return nullptr; - found->ApplyFastClear(); const bool cube = resource.dimension == TextureDimension::kCube; const bool layered = cube || (resource.dimension == TextureDimension::k2DArray && resource.depthOrLastArray != 0); - if (stencil || (!layered && !integer)) return found->Sampled(words, resource, components); - Require(found->hostFormat == base.format, "sampling several slices or an integer view of a depth plane that the device stores in another depth format is not implemented"); + const bool sampled = stencil || (!layered && !integer); const auto copyFormat = integer ? VK_FORMAT_R16_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; + const bool planes = (format == copyFormat || depthBits) && resource.width == base.extent.width && resource.height == base.extent.height && resource.baseLevel == 0 && resource.lastLevel == 0 && !(resource.baseArray != 0 && (cube || !layered || resource.baseArray > resource.depthOrLastArray)) && !(cube && resource.depthOrLastArray != 0 && resource.depthOrLastArray != 5) && (resource.dimension == TextureDimension::k2D || resource.dimension == TextureDimension::k2DArray || cube); + if (!(sampled ? found->SampledAccepts(words, resource) : planes) && found->OverwrittenInMemory()) { + retireStoringStale(*found); + return nullptr; + } + found->ApplyFastClear(); + 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 ((format != copyFormat && !depthBits) || resource.width != base.extent.width || resource.height != base.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0 || (cube && resource.depthOrLastArray != 0 && resource.depthOrLastArray != 5) || (resource.dimension != TextureDimension::k2D && resource.dimension != TextureDimension::k2DArray && !cube)) { + if (!planes) { char text[320]; std::snprintf(text, sizeof(text), "AGC graphics: sampling the depth planes of depth surface 0x%llx (%ux%u, vk format %d) as a %ux%u texture of guest format %u (vk %d), dimension %d, %u slices, levels %u-%u, base slice %u is not implemented", static_cast(base.address), base.extent.width, base.extent.height, static_cast(base.format), resource.width, resource.height, resource.format, static_cast(format), static_cast(resource.dimension), layers, resource.baseLevel, resource.lastLevel, resource.baseArray); throw std::runtime_error(text); } std::vector slices; + const auto geometry = DescribeSurface(resource); for (std::uint32_t layer = 0; layer < layers; ++layer) { - const auto address = base.address + static_cast(layer) * DepthSliceBytes(base.extent, d16 ? 2u : 4u); + const auto address = base.address + geometry.GuestLayerOffset(layer); auto* slice = depthSlice(context.device, address, base); if (slice != nullptr) slice->ApplyFastClear(); if (slice == nullptr) slices.push_back({}); @@ -794,15 +2373,31 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::span(context, base.extent, imageFormat, layers); } - return copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D); + return copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, layered && !cube ? resource.baseArray : 0u); } bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resource, const Texture* texture) { - if (resource.dimension != TextureDimension::k2D || resource.baseArray != 0) return false; + if ((resource.dimension != TextureDimension::k2D && resource.dimension != TextureDimension::k2DMsaa) || resource.baseArray != 0) return false; + std::lock_guard gpu(GuestMemory::GpuMutex()); const auto address = resource.baseAddress; + settlePendingHtileClear(context, address); std::lock_guard lock(surfacesMutex()); - const auto* found = planeSurface(context.device, address); - return found != nullptr && found->Holds(texture); + auto* found = planeSurface(context.device, address); + if (found == nullptr || !found->Holds(texture)) return false; + found->CheckCurrent(); + found->CheckResource(resource); + return true; +} + +void RequireNoNativeDepthFeedback_nid_no_patch(const Context& context, const DepthTarget& target, std::span> sampled) { + if (target.samples == 1u || sampled.empty()) return; + std::lock_guard gpu(GuestMemory::GpuMutex()); + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (!surface->active->load(std::memory_order_acquire)) continue; + if (surface->context.device != context.device || !sameSurface(surface->target, target)) continue; + Require(std::none_of(sampled.begin(), sampled.end(), [&](const auto& texture) { return texture != nullptr && surface->Holds(texture.get()); }), "sampling a native multisampled depth target the draw attaches is not implemented"); + } } VkImageAspectFlags HtileFillClears(std::uint32_t pattern, bool stencilInHtile) { @@ -821,13 +2416,47 @@ 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); for (const auto& surface : surfaces()) { + if (!surface->active->load(std::memory_order_acquire)) continue; const auto& target = surface->target; - 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 (target.samples != 1u && target.htileAddress != 0u && address < target.htileAddress + HtileMetadataBytes(target.extent) && target.htileAddress < address + bytes) surface->sampling.Refuse("native multisample depth metadata fill transition is not implemented"); + 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); + } } } @@ -850,9 +2479,9 @@ std::vector layeredSlices(const Context& context, const LayeredDep const auto& base = storage.base; std::vector slices; const auto& list = surfaces(); - for (std::uint32_t layer = 0; layer < storage.layers; ++layer) { + for (std::uint32_t layer = storage.firstLayer; layer < storage.layers; ++layer) { const auto address = base.address + static_cast(layer) * DepthSliceBytes(base.extent, storage.bytesPerTexel); - const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == context.device && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format && surface->target.samples == 1; }); + const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == context.device && !surface->retired && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format && surface->target.samples == 1; }); if (slice != list.rend()) slices.push_back({slice->get(), layer}); } return slices; @@ -863,20 +2492,15 @@ 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; - if (resource.baseArray != 0) { - char text[256]; - std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth planes of depth surface 0x%llx through a 2D array view of slices %u-%u is not implemented", static_cast(target.address), resource.baseArray, resource.depthOrLastArray); - throw std::runtime_error(text); - } - surface.RequireStorable(resource, mip); + surface.RequireStorable(resource); const auto geometry = DescribeSurface(resource); if (layers > 1 && geometry.GuestLayerOffset(1) != DepthSliceBytes(target.extent, bytesPerTexel)) { char text[256]; std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth planes of depth surface 0x%llx with a layer pitch of 0x%llx bytes, not the depth slice size 0x%llx, is not implemented", static_cast(target.address), static_cast(geometry.GuestLayerOffset(1)), static_cast(DepthSliceBytes(target.extent, bytesPerTexel))); throw std::runtime_error(text); } - auto& entry = layeredStorages()[{context.device, resource.baseAddress, resource.width, resource.height, layers, resource.format, static_cast(resource.tileMode), static_cast(resource.dimension)}]; - if (entry == nullptr) entry = std::make_unique(context, resource, target, bytesPerTexel, layers); + auto& entry = layeredStorages()[{context.device, resource.baseAddress, resource.width, resource.height, layers, resource.format, static_cast(resource.tileMode), static_cast(resource.dimension), resource.baseArray}]; + if (entry == nullptr) entry = std::make_unique(context, resource, target, bytesPerTexel, resource.baseArray, layers); else static_cast(entry->storage->Refresh()); const auto slices = layeredSlices(context, *entry); for (const auto& slice : slices) slice.surface->ApplyFastClear(); @@ -887,10 +2511,11 @@ 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) { - return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); + return surface->active->load(std::memory_order_acquire) && surface->context.device == context.device && !surface->retired && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); }); if (found == list.rend()) return nullptr; auto& surface = **found; @@ -900,24 +2525,60 @@ std::shared_ptr DepthSurfaceStorage(const Context& context, std: return layeredDepthStorage(context, surface, resource, mip); } +void CopyDepthSample(const Context& context, std::uint64_t depthAddress, std::uint32_t samples, std::uint32_t sample, StorageTexture& destination, VkImageView destinationView, VkExtent2D extent) { + std::lock_guard lock(surfacesMutex()); + auto* found = planeSurface(context.device, depthAddress); + Require(found != nullptr && found->target.address == depthAddress, "DB depth copy from a depth plane no draw rendered"); + Require(found->target.format == VK_FORMAT_D32_SFLOAT || found->target.format == VK_FORMAT_D32_SFLOAT_S8_UINT, "DB depth copy from a depth surface that is not 32-bit float"); + Require(found->target.samples == samples, "DB depth copy from a depth surface of another sample count"); + Require(found->target.extent.width == extent.width && found->target.extent.height == extent.height, "DB depth copy from a depth surface of another size"); + Require(sample < found->target.samples, "DB depth copy of a sample the depth surface does not have"); + found->CopySample(sample, destination, destinationView); +} + bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage) { std::lock_guard lock(surfacesMutex()); for (auto& [key, entry] : layeredStorages()) { if (std::get<0>(key) != context.device || entry->storage.get() != &storage) continue; const auto slices = layeredSlices(context, *entry); entry->Copy(slices, false); - for (const auto& slice : slices) slice.surface->written = ++depthWrites; + for (const auto& slice : slices) { + slice.surface->written = ++depthWrites; + slice.surface->NoteHostWrite(); + } return true; } for (const auto& surface : surfaces()) { - if (surface->context.device == context.device && surface->StoreBack(storage)) return true; + if (surface->active->load(std::memory_order_acquire) && surface->context.device == context.device && surface->StoreBack(storage)) return true; + } + for (auto surface = surfaces().rbegin(); surface != surfaces().rend(); ++surface) { + if ((*surface)->context.device == context.device && (*surface)->StoreBackCovering(storage)) return true; } return false; } bool DepthSurfaceAt(std::uint64_t address) { std::lock_guard lock(surfacesMutex()); - return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->target.address == address || surface->target.stencilAddress == address; }); + 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()) { + if (surface->context.device == device && !surface->retired && surface->Overlaps(address, bytes)) retire(*surface); + } +} + +void RetireCoveredDepthSurfaces(VkDevice device, std::span> written, std::span extents) { + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (surface->context.device == device && !surface->retired && surface->CoveredBy(written, extents)) retire(*surface); + } } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 507e1c603a..bdebdfa453 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -8,15 +8,18 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include "ThreadOwned.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "Optimization/include/Optimization/ShaderStageInputInfo.hpp" +#include "ThreadOwned.hpp" #include #include #include @@ -51,8 +54,11 @@ std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std: mix(state.renderExtent.width); mix(state.renderExtent.height); mix(state.samples); + mix(state.mergedCoverage); + mix(state.truncatedCoverage); mix(state.customSampleLocations); if (state.customSampleLocations) { + mix(state.sampleLocationGrid); for (const auto location : state.sampleLocations) mix(location); } if (!context.provokingVertexModePerPipeline) mix(state.provokingVertexMode); @@ -76,6 +82,10 @@ GuestTextureResource ArraySurfaceForTarget(const ColorTarget& color) { namespace { +bool writesDepthTarget(const State& state) { + return state.depthWrite || (state.stencilTest && (state.stencilFront.writeMask != 0u || state.stencilBack.writeMask != 0u)); +} + std::size_t colorKeyCount(const ColorTarget& color, std::size_t bytes) { if (color.mipCount != 1 || color.depth != 1 || !color.dccPipeAligned) return DccKeyBytes(bytes); return DccKeyCount(ColorTextureTileMode(color.tileMode), color.elementBytes, color.extent.width, color.extent.height, bytes); @@ -141,7 +151,8 @@ void materializeRegisterClear(const Context& context, const ColorTarget& color, if (ProvedCurrentDccKeys(color.dccAddress, color.bytes, resident.TargetKeyProof()) != DccKeys::ClearRegister) return; const auto texel = clearTexel(color, DccKeys::ClearRegister); const char* refusal = nullptr; - bool cleared = clearToTexel(resident, texel, color.elementBytes, refusal); + const bool pendingKeysKnown = KnownPendingDccKeys(color.dccAddress, color.bytes) == DccKeys::ClearRegister; + bool cleared = clearToTexel(resident, texel, color.elementBytes, refusal, pendingKeysKnown); const auto keyBytes = static_cast(color.bytes / 256); if (!cleared && keyBytes != 0) { if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->PendingWriteOverlaps(color.dccAddress, keyBytes)) { @@ -255,8 +266,9 @@ void recordCmaskClear(const Context& context, Recorder& recorder, const HostImpo if (AnyShadowedOverlaps(address, bytes)) PublishShadow(address, bytes, PublishScope::Whole, PublishReason::Keys); recorder.FlushKeyStoresOverlapping(address, bytes); const auto commands = recorder.Commands(); + if (resident != nullptr && !resident->Cached()) recorder.KeepBytes(resident.get(), static_cast(resident->BudgetBytes())); 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)); } @@ -287,7 +299,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(); @@ -298,7 +310,7 @@ bool materializeCmaskClear(const Context& context, const ColorTarget& color, con return filled; } -void imageBarrier(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess) { +void imageBarrier(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess, VkImageSubresourceRange subresources = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}) { VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; barrier.srcAccessMask = sourceAccess; barrier.dstAccessMask = destinationAccess; @@ -307,7 +319,7 @@ void imageBarrier(const Context& context, VkCommandBuffer commands, VkImage imag barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.image = image; - barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + barrier.subresourceRange = subresources; context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier); } @@ -340,7 +352,7 @@ constexpr std::array DrawPhaseNames{"validate", "vertex enum SyncReason : std::size_t { SyncNone, SyncNotResident, SyncNoRecorder, SyncDisabled, SyncCopiedWrites, SyncLease, SyncCount }; constexpr std::array SyncReasonNames{"none", "non-resident target", "no recorder", "disabled", "copied writes", "lease"}; constexpr std::size_t IndirectPathCount = static_cast(IndirectDrawPath::Count); -constexpr std::array IndirectDrawPathNames{"gpu-side", "patched SGPR not folded", "fetch offset unknown", "non-vertex path", "GE_INDX_OFFSET", "draw index", "vertex range too large", "feature gap", "pending image results", "pending label or copied write", "not imported", "disabled"}; +constexpr std::array IndirectDrawPathNames{"gpu-side", "patched SGPR not folded", "fetch offset unknown", "non-vertex path", "GE_INDX_OFFSET", "draw index", "vertex range too large", "feature gap", "pending image results", "pending label or copied write", "not imported", "disabled", "index range not fully readable"}; // What became of one draw, for the totals. struct DrawOutcome { @@ -351,6 +363,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; @@ -423,6 +438,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. @@ -493,6 +509,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; @@ -636,6 +660,7 @@ bool ValidationKey(const Context& context, std::span shade add(state.rectList); add(state.topology); add(state.cullMode); + add(state.polygonMode); add(state.blends.size()); add(context.subgroup.subgroupSize); add(context.subgroup.supportedStages); @@ -644,7 +669,9 @@ 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.vertexPoints); add(state.stages.mesh.has_value()); if (state.stages.mesh) { const auto& mesh = *state.stages.mesh; @@ -656,6 +683,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) { @@ -683,12 +711,19 @@ std::map, std::string>& validationFailures() { return failures; } -std::set CachedFragmentOutputs(const Context& context, std::span shaders, const State& state, bool& memoized, bool& hit) { +std::vector& validationKeyScratch() { + thread_local std::vector* keySlot = nullptr; + auto& key = ShaderRecompiler::ThreadOwned(keySlot); + key.clear(); + return key; +} + +std::uint32_t CachedFragmentOutputs(const Context& context, std::span shaders, const State& state, bool& memoized, bool& hit) { memoized = false; hit = false; - std::vector key; + auto& key = validationKeyScratch(); const bool keyed = ValidationKey(context, shaders, state, key); - static std::map, std::set> memo; + static std::map, std::uint32_t> memo; if (keyed) { memoized = true; std::lock_guard lock(validationMutex()); @@ -697,15 +732,18 @@ std::set CachedFragmentOutputs(const Context& context, std::span< return found->second; } } - std::set outputs; + std::uint32_t outputs = 0; 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); + for (const auto location : 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, context.meshShader)) { + Require(location < 32, "fragment output location exceeds the output mask"); + outputs |= 1u << location; + } } catch (const std::exception& error) { if (keyed) { std::lock_guard lock(validationMutex()); auto& failures = validationFailures(); if (failures.size() >= 1024) failures.clear(); - failures.emplace(std::move(key), error.what()); + failures.emplace(key, error.what()); } throw; } @@ -713,7 +751,7 @@ std::set CachedFragmentOutputs(const Context& context, std::span< std::lock_guard lock(validationMutex()); // A handful of configurations recur; a runaway key space is dropped wholesale. if (memo.size() >= 1024) memo.clear(); - memo.emplace(std::move(key), outputs); + memo.emplace(key, outputs); } return outputs; } @@ -735,7 +773,9 @@ bool MovableBuffers() { } ResourceCache::Key DrawResourceKey(const Context& context, std::span shaders, const ColorTarget& target, std::uint32_t colorAttachments, std::uint64_t indexAddress, std::uint64_t indexBytes, bool ranges) { - ResourceCache::Key key{0xffffffffu}; + ResourceCache::Key key; + key.reserve(256); + key.push_back(0xffffffffu); const auto append64 = [&](std::uint64_t value) { key.push_back(static_cast(value)); key.push_back(static_cast(value >> 32u)); @@ -743,9 +783,10 @@ ResourceCache::Key DrawResourceKey(const Context& context, std::span(context.device)); key.push_back(static_cast(shaders.size())); for (const auto& shader : shaders) { - const auto part = ShaderResources::ContentKey(shader, true, MovableBuffers()); - key.push_back(static_cast(part.size())); - key.insert(key.end(), part.begin(), part.end()); + const auto at = key.size(); + key.push_back(0); + ShaderResources::AppendContentKey(shader, true, MovableBuffers(), key); + key[at] = static_cast(key.size() - at - 1); } key.push_back(colorAttachments); if (ranges) { @@ -770,10 +811,11 @@ void CheckBufferAliases(std::span shaders, const ColorTarg for (std::size_t offset = 0; offset + 4 <= words.size(); offset += 4) { const ShaderRecompiler::ShaderBufferResource descriptor{{words[offset], words[offset + 1], words[offset + 2], words[offset + 3]}}; const auto address = descriptor.Base48(); - const auto size = descriptor.GetSize(); + auto size = descriptor.GetSize(); if (size == 0 || address == 0) continue; const auto element = offset / 4; const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; + if (const std::uint64_t stride = descriptor.Stride(); !written && stride != 0 && address < target.address && address + size > target.address && address + size - target.address < stride) size = target.address - address; Require(!overlap(address, size, target.address, target.bytes), "shader buffer aliases the render target"); Require(!written || !overlap(address, size, indexAddress, indexBytes), "writable shader buffer aliases the index buffer"); } @@ -795,7 +837,7 @@ std::array MeshIndexBufferDescriptor(const Pm4::DrawParameters MeshArgumentRules MeshArgumentRulesFor(const Context& context, const ShaderRecompiler::MeshConfiguration& mesh, std::uint32_t indexCount) { const auto inputSize = mesh.inputPrimitive == 1 ? 1u : mesh.inputPrimitive == 2 ? 2u : 3u; const auto step = mesh.inputPrimitive == 5 || mesh.inputPrimitive == 6 ? 1u : inputSize; - Require(mesh.primitivesPerGroup != 0, "mesh draw contains no complete primitive"); + Require(mesh.primitivesPerGroup != 0, "mesh pipeline groups no primitive (zero primitives per group)"); return {indexCount, inputSize, step, mesh.primitivesPerGroup, context.meshLimits.maxMeshWorkGroupCount[0], context.meshLimits.maxMeshWorkGroupCount[1], context.meshLimits.maxMeshWorkGroupTotalCount}; } @@ -830,6 +872,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; @@ -907,14 +961,20 @@ DrawInputCopy CopyDrawInput(const Context& context, Recorder* recorder, std::uin return copy; } -DrawInputCopy CopyZeroPaddedDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t validBytes) { +DrawInputCopy CopyZeroPaddedDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t validBytes, std::size_t stride) { Require(validBytes <= bytes, "the valid bytes of a vertex fetch exceed the fetch"); GuestMemory::FlushGpuWrites(address, bytes); DrawInputCopy copy; copy.buffer = std::make_shared(context, bytes, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); const auto span = copy.buffer->Bytes(); - GuestMemory::Read(address, span.subspan(0, validBytes), 1); - std::fill(span.begin() + static_cast(validBytes), span.end(), std::byte{0}); + auto readable = validBytes; + if (stride != 0 && !GuestMemory::Accessible(reinterpret_cast(address), validBytes)) { + const auto ranges = GuestMemory::CommittedRanges(address, validBytes); + const std::size_t prefix = !ranges.empty() && ranges.front().first == address ? static_cast(ranges.front().second - address) : 0; + if (validBytes - prefix < stride) readable = prefix; + } + GuestMemory::Read(address, span.subspan(0, readable), 1); + std::fill(span.begin() + static_cast(readable), span.end(), std::byte{0}); return copy; } @@ -923,18 +983,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; + if (recorder == nullptr || bytes == 0 || !context.drawInputsInPlace) 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; } @@ -971,16 +1039,24 @@ struct DrawInputs { VkDeviceSize indexOffset = 0; std::uint32_t maxIndex = 0; VertexInputLayout vertexInput; + std::shared_ptr retainedVertexInput; std::vector> vertexBuffers; std::vector vertexHandles; std::vector vertexOffsets; std::vector> inPlaceReads; - std::set fragmentOutputs; + bool vertexInputBehindGpuWrite = false; + std::uint32_t fragmentOutputs = 0; VkPipelineStageFlags shaderStages = 0; std::uint32_t meshGroups = 0; std::vector restartTable; + std::uint32_t expandedIndexCount = 0; + const VertexInputLayout& VertexLayout() const { return retainedVertexInput != nullptr ? *retainedVertexInput : vertexInput; } }; +bool StripPatches(const State& state, std::span shaders) { + return state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP && BarycentricPatches(state, shaders); +} + std::shared_ptr zeroVertexBuffer(const Context& context, const ShaderRecompiler::VertexAttribute& attribute) { auto zero = std::make_shared(context, DecodeVertexFormat(attribute).bytes, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); std::fill(zero->Bytes().begin(), zero->Bytes().end(), std::byte{0}); @@ -1019,7 +1095,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 && !state.quadList, "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)); @@ -1027,6 +1103,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; @@ -1042,8 +1119,12 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P Require(context.meshShader, "device does not support mesh shaders"); const auto& mesh = *state.stages.mesh; const auto inputSize = mesh.inputPrimitive == 1 ? 1u : mesh.inputPrimitive == 2 ? 2u : 3u; - Require(draw.indexCount >= inputSize && mesh.primitivesPerGroup != 0, "mesh draw contains no complete primitive"); + Require(mesh.primitivesPerGroup != 0, "mesh pipeline groups no primitive (zero primitives per group)"); Require(((state.stages.registerValue >> 19u) & 3u) == 0 || !draw.indexed, "indexed fast-launch draws are unsupported"); + if (args == nullptr && draw.indexCount < inputSize) { + inputs.nothing = true; + return inputs; + } if (args == nullptr) { const auto step = mesh.inputPrimitive == 5 || mesh.inputPrimitive == 6 ? 1u : inputSize; const auto primitives = (draw.indexCount - inputSize) / step + 1u; @@ -1056,9 +1137,89 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P // Viewport and scissor are dynamic pipeline state, so their limits are checked here per draw. ValidateViewport(context, state.viewport); ValidateDepthBounds(context, state); + ValidateSampleMask(context, state, shaders); timer.phase(PhaseValidate); inputs.maxIndex = draw.indexed ? 0u : draw.firstVertex + draw.indexCount - 1u; - if (draw.indexed) { + if (state.quadList || StripPatches(state, shaders)) { + Require(!draw.indirect, state.quadList ? "indirect quad-list draw is unsupported" : "an indirect triangle strip through emulated barycentric patches is unsupported"); + const std::uint32_t primitives = state.quadList ? draw.indexCount / 4u : draw.indexCount >= 3u ? draw.indexCount - 2u : 0u; + if (primitives == 0u || draw.instanceCount == 0u) { + inputs.nothing = true; + return inputs; + } + const std::uint32_t corners = state.quadList ? 6u : 3u; + const std::uint32_t outputIndexCount = primitives * corners; + const std::size_t expandedIndexBytes = static_cast(outputIndexCount) * sizeof(std::uint32_t); + auto indexCopy = std::make_shared(context, expandedIndexBytes, VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + auto* outIndices = reinterpret_cast(indexCopy->Bytes().data()); + std::uint32_t maxIdx = 0; + const bool lastProvoking = state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT; + const auto expand = [&](const auto& source) { + for (std::uint32_t primitive = 0; primitive < primitives; ++primitive) { + auto* out = outIndices + static_cast(primitive) * corners; + if (state.quadList) { + const auto i0 = source(primitive * 4u + 0u); + const auto i1 = source(primitive * 4u + 1u); + const auto i2 = source(primitive * 4u + 2u); + const auto i3 = source(primitive * 4u + 3u); + if (lastProvoking) { + out[0] = i0; + out[1] = i1; + out[2] = i3; + out[3] = i1; + out[4] = i2; + out[5] = i3; + } else { + out[0] = i0; + out[1] = i1; + out[2] = i2; + out[3] = i0; + out[4] = i2; + out[5] = i3; + } + } else { + const std::uint32_t odd = primitive & 1u; + if (lastProvoking) { + out[0] = source(primitive + odd); + out[1] = source(primitive + 1u - odd); + out[2] = source(primitive + 2u); + } else { + out[0] = source(primitive); + out[1] = source(primitive + 1u + odd); + out[2] = source(primitive + 2u - odd); + } + } + for (std::uint32_t corner = 0; corner < corners; ++corner) maxIdx = std::max(maxIdx, out[corner]); + } + }; + if (draw.indexed) { + GuestMemory::FlushGpuWrites(draw.indexAddress, static_cast(indexBytes)); + GuestMemory::CheckRange(reinterpret_cast(draw.indexAddress), static_cast(indexBytes), draw.indexSize); + const auto readIndex = [&](std::uint32_t idx) -> std::uint32_t { + if (draw.indexSize == 2) { + std::uint16_t v = 0; + GuestMemory::Read(draw.indexAddress + static_cast(idx) * 2u, std::as_writable_bytes(std::span(&v, 1)), 2); + return v; + } else { + std::uint32_t v = 0; + GuestMemory::Read(draw.indexAddress + static_cast(idx) * 4u, std::as_writable_bytes(std::span(&v, 1)), 4); + return v; + } + }; + expand(readIndex); + Require(draw.firstVertex <= std::numeric_limits::max() - maxIdx, "indexed draw vertex range overflow"); + } else { + const std::uint32_t lastVertex = state.quadList ? primitives * 4u - 1u : primitives + 1u; + Require(draw.firstVertex <= std::numeric_limits::max() - lastVertex, "auto draw vertex range overflow"); + expand([](std::uint32_t idx) { return idx; }); + } + inputs.maxIndex = maxIdx + draw.firstVertex; + Require(inputs.maxIndex <= context.limits.maxDrawIndexedIndexValue, "index exceeds the device's indexed draw limit"); + inputs.indexHandle = indexCopy->Handle(); + inputs.indexOffset = 0; + inputs.expandedIndexCount = outputIndexCount; + inputs.indices = std::move(indexCopy); + } else if (draw.indexed) { const bool listTopology = state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; const bool fanGeometry = state.stages.mesh && state.stages.mesh->inputPrimitive == 5; const bool skipRestart = state.primitiveRestart && (!listTopology || context.primitiveListRestart) && !fanGeometry; @@ -1109,7 +1270,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); @@ -1119,9 +1280,14 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P APS5_LOG_CHARS_OUT_DEBUG("Index validation OK"); const auto& attributes = shaders.front().program->vertexAttributes; // Validates the vertex descriptors; the layout also keys and builds the pipeline. - if (recipe != nullptr) inputs.vertexInput = recipe->vertexInput; - else inputs.vertexInput = BuildVertexInputLayout(context, attributes); + if (recipe != nullptr) { + inputs.vertexInput = recipe->vertexInput; + } else if (!attributes.empty()) { + if (context.vertexInputs != nullptr) inputs.retainedVertexInput = context.vertexInputs->TryGet(shaders.front().program->variantId, attributes); + if (inputs.retainedVertexInput == nullptr) inputs.vertexInput = BuildVertexInputLayout(context, attributes); + } inputs.vertexOffsets.assign(attributes.size(), 0); + inputs.vertexHandles.reserve(attributes.size()); // An indexed draw's vertex offset moves every fetch: the copy must reach the last one. if (draw.indexed) { Require(draw.firstVertex <= std::numeric_limits::max() - inputs.maxIndex, "indexed draw vertex range overflow"); @@ -1171,9 +1337,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; } @@ -1187,7 +1354,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P for (const auto f : soloFetches) { const auto begin = fetches[f].begin; const auto bytes = static_cast(fetches[f].end - begin); - auto copy = CopyZeroPaddedDrawInput(context, begin, bytes, fetchValid[f]); + auto copy = CopyZeroPaddedDrawInput(context, begin, bytes, fetchValid[f], fetches[f].stride); soloHandles[f] = copy.buffer->Handle(); inputs.vertexBuffers.push_back(std::move(copy.buffer)); } @@ -1320,7 +1487,7 @@ struct IndirectRecord { // The draw commands of one draw: the vertex and index buffer binds, then the direct draw, the // GPU-side indirect draw from `argumentBuffer` or the CPU-read records with the driver's rules. -void recordDrawCommands(const Context& context, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, const DrawInputs& inputs, const IndirectRecord* indirect, VkBuffer argumentBuffer, VkDeviceSize argumentOffset) { +void recordDrawCommands(const Context& context, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, const DrawInputs& inputs, const IndirectRecord* indirect, VkBuffer argumentBuffer, VkDeviceSize argumentOffset, std::uint32_t firstIndex = 0) { if (context.recorder != nullptr) context.recorder->NoteSampledDraw(commands); const auto* args = indirect != nullptr ? indirect->args : nullptr; if (state.stages.mesh && args != nullptr) { @@ -1333,14 +1500,19 @@ void recordDrawCommands(const Context& context, VkCommandBuffer commands, const return; } if (!inputs.vertexHandles.empty()) context.Resolved(&DeviceFunctions::cmdBindVertexBuffers, "vkCmdBindVertexBuffers")(commands, 0, static_cast(inputs.vertexHandles.size()), inputs.vertexHandles.data(), inputs.vertexOffsets.data()); + if (inputs.expandedIndexCount != 0u) { + context.Resolved(&DeviceFunctions::cmdBindIndexBuffer, "vkCmdBindIndexBuffer")(commands, inputs.indexHandle, inputs.indexOffset, VK_INDEX_TYPE_UINT32); + context.Resolved(&DeviceFunctions::cmdDrawIndexed, "vkCmdDrawIndexed")(commands, inputs.expandedIndexCount, draw.instanceCount, 0, static_cast(draw.firstVertex), draw.firstInstance); + return; + } if (draw.indexed) context.Resolved(&DeviceFunctions::cmdBindIndexBuffer, "vkCmdBindIndexBuffer")(commands, inputs.indexHandle, inputs.indexOffset, draw.indexSize == 2 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32); if (args == nullptr) { - if (draw.indexed) context.Resolved(&DeviceFunctions::cmdDrawIndexed, "vkCmdDrawIndexed")(commands, draw.indexCount, draw.instanceCount, 0, static_cast(draw.firstVertex), draw.firstInstance); + if (draw.indexed) context.Resolved(&DeviceFunctions::cmdDrawIndexed, "vkCmdDrawIndexed")(commands, draw.indexCount, draw.instanceCount, firstIndex, static_cast(draw.firstVertex), draw.firstInstance); 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); @@ -1388,7 +1560,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()); @@ -1488,7 +1660,7 @@ struct Kept { // The completion side of a recorded draw: the kept objects, the record check, the lease outcome, // the GPU write notes, the copied-buffer write-back (listed in DrawCopiedWriters or run as a // completion action) and the targets marked dirty. -void keepRecordedDraw(Recorder& recorder, const std::shared_ptr& resources, std::shared_ptr pipeline, std::shared_ptr framebuffer, DrawInputs& inputs, std::vector> targets, std::vector slices, std::unique_ptr scratch, std::function checkRecords, bool listed, bool completion, const DrawOutcome& outcome) { +void keepRecordedDraw(Recorder& recorder, const std::shared_ptr& resources, std::shared_ptr pipeline, std::shared_ptr framebuffer, DrawInputs& inputs, std::vector> targets, std::vector> slices, std::unique_ptr scratch, std::function checkRecords, bool listed, bool completion, const DrawOutcome& outcome) { auto kept = std::make_shared(); kept->resources = resources; kept->pipeline = std::move(pipeline); @@ -1498,6 +1670,10 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr kept->scratch = std::move(scratch); kept->targets = std::move(targets); const auto& residents = kept->targets; + resources->KeepStorageImageBytes(recorder); + for (const auto& target : residents) { + if (target != nullptr && !target->Cached()) recorder.KeepBytes(target.get(), static_cast(target->BudgetBytes())); + } recorder.Keep(kept); if (resources != nullptr) recorder.KeepBytes(resources.get(), resources->CopiedBytes()); if (checkRecords) recorder.OnComplete(std::move(checkRecords)); @@ -1525,9 +1701,13 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr } for (std::size_t index = 0; index < residents.size(); ++index) { if (residents[index] == nullptr) continue; - if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index]); + if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index].first, slices[index].second); else residents[index]->MarkDirty(); } + for (const auto& resident : residents) { + 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 @@ -1541,8 +1721,10 @@ struct RecordedDraw { std::shared_ptr framebuffer; std::span targetViews; std::vector> targets; - std::vector targetSlices; + std::vector> targetSlices; + std::vector> multisampleTargets; std::vector> proxies; + std::vector multisampleImages; const IndirectRecord* indirect = nullptr; std::span moved; bool listed = false; @@ -1550,14 +1732,67 @@ struct RecordedDraw { bool waited = false; const std::array* pushBytes = nullptr; VkShaderStageFlags pushStages = 0; + std::vector>> stencilReplay; + std::vector reordered; + SampleOrderPass reorderPass; + std::optional depthLocations; }; +// A remapped viewport depth range reads its scale and offset from the draw's push block (the vertex shader's +// RemapPositionDepth), so draws that differ only in them share one pipeline; the draw's own state supplies them. +void patchDepthRemapPush(std::array& block, const State& state) { + if (state.depthRemap.mode == 0u) return; + static_assert(ShaderRecompiler::DepthRemapPushOffsetBytes + ShaderRecompiler::DepthRemapPushBytes <= PipelinePushSlotBytes); + const std::array words{state.depthRemap.scale, state.depthRemap.offset}; + std::memcpy(block.data() + ShaderRecompiler::DepthRemapPushOffsetBytes, words.data(), sizeof(words)); +} + +void recordStencilReplay(const Context& context, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, const DrawInputs& inputs, std::span>> replay, std::span shaders, const ShaderResources& resources, const std::array* bytes = nullptr, VkShaderStageFlags stages = 0) { + State selected = state; + auto block = bytes != nullptr ? *bytes : AssemblePushConstants(shaders); + if (bytes == nullptr) { + resources.PatchPushConstants(block); + stages = PushConstantStages(shaders); + } + patchDepthRemapPush(block, state); + const auto fragment = std::ranges::find_if(shaders, [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::Fragment && !shader.program->stencilReferences.empty(); }); + Require(fragment != shaders.end(), "stencil replay fragment stage is missing"); + const auto referenceOffset = fragment->pushConstantOffset + fragment->program->stencilReferenceDword * sizeof(std::uint32_t); + Require(referenceOffset <= block.size() - sizeof(std::uint32_t), "stencil replay reference exceeds the push block"); + const auto run = [&](const Pm4::DrawParameters& piece, std::uint32_t firstIndex) { + for (const auto& [reference, pipeline] : replay) { + selected.stencilReplayReference = reference; + selected.stencilFront.reference = reference; + selected.stencilBack.reference = reference; + context.Resolved(&DeviceFunctions::cmdSetStencilReference, "vkCmdSetStencilReference")(commands, VK_STENCIL_FACE_FRONT_AND_BACK, reference); + std::memcpy(block.data() + referenceOffset, &reference, sizeof(reference)); + pipeline->PushConstants(commands, stages, block); + recordDrawCommands(context, commands, selected, piece, inputs, nullptr, VK_NULL_HANDLE, 0, firstIndex); + } + }; + if (replay.size() == 1u) { + run(draw, 0u); + return; + } + for (std::uint32_t instance = 0; instance < draw.instanceCount; ++instance) { + for (std::uint32_t first = 0; first < draw.indexCount - draw.indexCount % 3u; first += 3u) { + auto piece = draw; + piece.instanceCount = 1u; + piece.firstInstance = draw.firstInstance + instance; + piece.indexCount = 3u; + if (!draw.indexed) piece.firstVertex += first; + run(piece, draw.indexed ? first : 0u); + } + } +} + void pushDrawConstants(const Pipeline& pipeline, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, std::span shaders, const ShaderResources& resources, const std::array* bytes, VkShaderStageFlags stages, VkDeviceAddress meshArguments = 0, bool restartTable = false) { auto block = bytes != nullptr ? *bytes : AssemblePushConstants(shaders); if (bytes == nullptr) { resources.PatchPushConstants(block); stages = PushConstantStages(shaders); } + patchDepthRemapPush(block, state); if (state.stages.tessellation) { for (const auto& shader : shaders) Require(shader.program->pushConstants.empty() || shader.pushConstantOffset >= PipelinePushSlotBytes || shader.pushConstantOffset + shader.program->pushConstants.size() <= ShaderRecompiler::TessellationLevelPushOffsetBytes, "a stage's push constants overlap the tessellation levels"); const std::array levels{state.stages.minTessellationLevel, state.stages.maxTessellationLevel}; @@ -1613,22 +1848,22 @@ void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer c }; if (bindings != nullptr) { for (const auto& snapshot : bindings->snapshots) { - const auto bytes = snapshot.buffer->Bytes(); - addSample(snapshot.address, bytes.size(), snapshot.buffer->Handle(), 0, bytes.data()); + const auto bytes = snapshot.Bytes(); + addSample(snapshot.address, bytes.size(), snapshot.buffer->Handle(), snapshot.offset, bytes.data()); } } for (const auto& [begin, end] : resources.InPlaceReads()) { Require(end >= begin, "invalid capture input range"); const auto bytes = static_cast(end - begin); if (bytes == 0 || bytes > 512) continue; - if (bindings != nullptr && std::any_of(bindings->snapshots.begin(), bindings->snapshots.end(), [&](const auto& snapshot) { return snapshot.address < end && begin < snapshot.address + snapshot.buffer->Bytes().size(); })) continue; + if (bindings != nullptr && std::any_of(bindings->snapshots.begin(), bindings->snapshots.end(), [&](const auto& snapshot) { return snapshot.address < end && begin < snapshot.address + snapshot.bytes; })) continue; if (resources.WritesOverlap(begin, bytes) || recorder.PendingWriteOverlaps(begin, bytes)) { CaptureTrace::Log("input-skip draw=%llu batch=%llu address=%llx bytes=%zu reason=gpu-writer", draw, batch, static_cast(begin), bytes); continue; } 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; @@ -1660,6 +1895,11 @@ void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer c }); } +bool WriteOnlyPasses() { + static const bool enabled = std::getenv("APS5_NO_WRITE_ONLY_PASSES") == nullptr; + return enabled; +} + void recordDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, DrawInputs& inputs, RecordedDraw& record, DrawOutcome& outcome, DrawTimer& timer, double& ownWaitedMs) { auto* recorder = record.recorder; auto& resources = *record.resources; @@ -1682,28 +1922,67 @@ 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; const bool addressBased = resources.HoldsLease(); - auto passReads = addressBased ? std::vector>{} : resources.DeviceReads(); + thread_local std::vector>* passReadsSlot = nullptr; + auto& passReads = ShaderRecompiler::ThreadOwned(passReadsSlot); + passReads.clear(); + if (!addressBased) resources.DeviceReads(passReads); + const auto deviceReads = passReads.size(); if (gpuIndirect) { passReads.emplace_back(args->arguments, args->arguments + args->RangeBytes()); if (args->countIndirect) passReads.emplace_back(args->countAddress, args->countAddress + 4); } + passReads.insert(passReads.end(), inputs.inPlaceReads.begin(), inputs.inPlaceReads.end()); const auto passImages = resources.StorageImages(); std::vector> passAttachments; passAttachments.reserve(record.targets.size()); for (const auto& target : record.targets) { if (target != nullptr) passAttachments.emplace_back(target->Image(), true); } + for (const auto image : record.multisampleImages) passAttachments.emplace_back(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(); + GuestRangeList passWrites = resources.GpuWrites(); + GuestRangeList passWriteOnly{}; + thread_local std::vector>* hazardWritesSlot = nullptr; + if (const auto& writeOnly = resources.WriteOnlyRanges(); !writeOnly.empty() && !unknownMemory && WriteOnlyPasses()) { + const auto listed = [&](const std::pair& range, bool aligned) { + return std::any_of(writeOnly.begin(), writeOnly.end(), [&](const auto& only) { + if (!aligned) return only == range; + const auto begin = only.first & ~std::uint64_t{3}; + const auto end = begin == only.first ? only.second : (only.second + 3) & ~std::uint64_t{3}; + return range.first == begin && range.second == end; + }); + }; + const auto ownReads = passReads.begin() + static_cast(deviceReads); + passReads.erase(std::remove_if(passReads.begin(), ownReads, [&](const auto& range) { return listed(range, true); }), ownReads); + auto& hazardWrites = ShaderRecompiler::ThreadOwned(hazardWritesSlot); + hazardWrites.clear(); + for (const auto& range : resources.GpuWrites()) { + if (!listed(range, false)) hazardWrites.push_back(range); + } + passWrites = hazardWrites; + passWriteOnly = writeOnly; + } + const PassAccess passAccess{passReads, passWrites, passImages, passAttachments, unknownMemory, unknownWrites, passWriteOnly}; const bool fenced = state.depth.has_value() || !record.proxies.empty(); const auto start = recorder->StartDrawPass(passKey, capture || readsTarget || gpuIndirect || meshIndirect, fenced, passAccess); + for (const auto& target : record.multisampleTargets) KeepMultisampleTarget_nid_no_patch(target); const bool continued = start.continued; outcome.passContinued = continued; outcome.passBegun = !continued; @@ -1745,6 +2024,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(); @@ -1753,6 +2036,11 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam } // Earlier recorded work (dispatches, the previous draw) wrote the images in the general // layout, which a lean draw renders in: no transitions. + if (!record.reordered.empty()) { + std::vector> kept; + RecordSampleOrder(context, commands, record.reorderPass, record.reordered, true, kept); + for (auto& object : kept) recorder->Keep(std::move(object)); + } APS5_LOG_OUT_DEBUG("Beginning pipeline renderExtent=%ux%u", state.renderExtent.width, state.renderExtent.height); record.pipeline->Begin(commands, *record.framebuffer, state.renderExtent, state); } @@ -1768,7 +2056,11 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam APS5_LOG_CHARS_OUT_DEBUG("Resources bound"); pushDrawConstants(*record.pipeline, commands, state, draw, shaders, resources, record.pushBytes, record.pushStages, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0, !inputs.restartTable.empty()); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); - recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset); + if (record.stencilReplay.empty()) recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset); + else { + recordStencilReplay(context, commands, state, draw, inputs, record.stencilReplay, shaders, resources, record.pushBytes, record.pushStages); + for (const auto& [reference, pipeline] : record.stencilReplay) recorder->Keep(pipeline); + } if (record.pipeline->SplitsFaces()) { record.pipeline->ContinueBackFaces(commands, state); recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset); @@ -1782,12 +2074,22 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam auto checkRecords = indirectRecordCheck(record.indirect); APS5_LOG_CHARS_OUT_DEBUG("Draw recorded"); std::function storeProxies; - if (!continued && !record.proxies.empty()) { - storeProxies = [proxies = record.proxies](VkCommandBuffer passCommands) { + if (!continued && (!record.proxies.empty() || !record.reordered.empty() || record.depthLocations)) { + storeProxies = [proxies = record.proxies, reordered = record.reordered, reorderPass = record.reorderPass, depthLocations = record.depthLocations, context, recorder](VkCommandBuffer passCommands) { for (const auto& proxy : proxies) { proxy->RecordAttachmentProxyStore(passCommands, VK_IMAGE_LAYOUT_GENERAL); Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); } + if (!reordered.empty()) { + std::vector> kept; + RecordSampleOrder(context, passCommands, reorderPass, reordered, false, kept); + for (auto& object : kept) recorder->Keep(std::move(object)); + } + if (depthLocations) { + RecordDepthSampleLocations(context, passCommands, *depthLocations); + Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + recorder->Keep(depthLocations->keep); + } }; } recorder->LeaveRenderPassOpen(passKey, drawTiming, fenced, passAccess, std::move(storeProxies)); @@ -1809,16 +2111,24 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam // The state with the outputs the pixel shader lacks masked: Vulkan leaves attachments a pixel // shader has no output for undefined, so their writes are masked (shaders that only store to // images or buffers keep their targets as they were). Empty when no mask must change. -std::optional maskedState(const State& state, const std::set& fragmentOutputs) { +std::optional maskedState(const State& state, std::uint32_t fragmentOutputs) { std::optional masked; for (std::size_t index = 0; index < state.blends.size(); ++index) { - if (fragmentOutputs.contains(static_cast(index)) || state.blends[index].colorWriteMask == 0) continue; + if ((index < 32 && ((fragmentOutputs >> index) & 1u) != 0) || state.blends[index].colorWriteMask == 0) continue; if (!masked.has_value()) masked = state; masked->blends[index].colorWriteMask = 0; } return masked; } +std::optional passDepthLocations(const Context& context, const State& state, const Pipeline& pipeline) { + if (!state.depth || state.depth->samples <= 1u || !state.customSampleLocations || !pipeline.SampleLocationsEnabled()) return std::nullopt; + auto locations = DepthSurfaceSampleLocations(context, *state.depth); + locations.grid = pipeline.HostSampleLocationGrid(); + locations.locations = SampleLocations(state); + return locations; +} + // Debug aid: APS5_DUMP_TARGETS= saves the first n renders of every color target as a raw file // (u32 width, u32 height, u32 VkFormat, then tightly packed rows). int DumpTargetLimit() { @@ -1839,7 +2149,7 @@ bool RecordDraws() { } std::optional KnownValidationFailure(const Context& context, std::span shaders, const State& state) { - std::vector key; + auto& key = validationKeyScratch(); if (!ValidationKey(context, shaders, state, key)) return std::nullopt; std::lock_guard lock(validationMutex()); const auto& failures = validationFailures(); @@ -1847,58 +2157,119 @@ std::optional KnownValidationFailure(const Context& context, std::s return std::nullopt; } -void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, std::shared_ptr* recipeOut) { +namespace { + +struct TargetBinding { + ColorTarget color; + bool gpuTiling = false; + std::shared_ptr resident; + bool proxied = false; + std::unique_ptr transfer; + std::unique_ptr tiled; + std::unique_ptr tiledDevice; + std::unique_ptr linearDevice; + std::vector original; + TileMipLayout mip{}; + std::unique_ptr target; + bool multisampled = false; + VkImage multisampleImage = VK_NULL_HANDLE; + std::shared_ptr multisampleOwner; + std::unique_ptr dump; +}; + +struct PreparedDraw { + explicit PreparedDraw(bool profile) : timer(profile) {} + PreparedDraw(const PreparedDraw&) = delete; + PreparedDraw& operator=(const PreparedDraw&) = delete; + std::shared_ptr* recipeOut = nullptr; + std::vector stencilReferences; + std::optional timing; + DrawTimer timer; + DrawOutcome outcome; + const ShaderResources::BuildTiming* built = nullptr; + double waitedBefore = 0; + double ownWaitedMs = 0; + DrawInputs inputs; + const Pm4::DrawParameters::IndirectDraw* args = nullptr; + int dumpLimit = 0; + std::vector targets; + std::vector targetViews; + Recorder* recorder = nullptr; + ResolvedResources resolved; + IndirectRecord indirect; + std::vector records; + bool completion = false; + bool listed = false; + bool recorded = false; + bool lean = false; + bool multisampled = false; + std::optional masked; + std::vector>> stencilReplay; + std::shared_ptr pipeline; + std::vector> owners; + std::vector> framebufferOwners; + std::shared_ptr framebuffer; + std::vector reordered; + SampleOrderPass reorderPass; + std::optional depthLocations; +}; + +bool prepareDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, PreparedDraw& prepared) { + auto*& recipeOut = prepared.recipeOut; + auto& stencilReferences = prepared.stencilReferences; + for (const auto& shader : shaders) { + if (shader.program != nullptr && !shader.program->stencilReferences.empty()) { + Require(shader.stage == ShaderRecompiler::ShaderStage::Fragment && stencilReferences.empty(), "stencil replay requires exactly one fragment stage"); + stencilReferences = shader.program->stencilReferences; + } + } + if (!stencilReferences.empty()) { + Require(!draw.indirect && !state.primitiveRestart && !state.rectList && !state.quadList && !state.stages.mesh && !state.stages.tessellation && state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, "stencil replay supports direct triangle lists without restart, mesh or tessellation"); + Require(!state.depthBiasPerFace && !ShadersWriteMemory(shaders), "stencil replay refuses face splits and shader memory writes"); + Require(std::ranges::all_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->stencilReplaySafe && (shader.stage == ShaderRecompiler::ShaderStage::Vertex || shader.stage == ShaderRecompiler::ShaderStage::Fragment); }), "stencil replay refuses stages with side effects, clocks, subgroup dependencies or primitive ID inputs"); + if (stencilReferences.size() == 256u) { + static std::once_flag unbounded; + std::call_once(unbounded, [] { APS5_LOG_CHARS_OUT("Stencil replay: a draw replays all 256 stencil reference values because its pixel shader's export is not bounded, recording 256 passes per triangle and instance"); }); + } + for (const auto& shader : shaders) { + if (shader.stage != ShaderRecompiler::ShaderStage::Vertex) continue; + Require((shader.program->hostSubgroupSize == 1u || shader.program->hostSubgroupSize == context.subgroup.subgroupSize) && shader.program->spirv.size() >= 5u && shader.program->spirv[1] >= 0x00010000u && shader.program->spirv[1] < 0x00010600u, "stencil replay vertex subgroup contract differs from the physical device"); + } + Require(static_cast(draw.firstInstance) + draw.instanceCount <= 0x100000000ull && (draw.indexed || static_cast(draw.firstVertex) + draw.indexCount <= 0x100000000ull), "stencil replay draw indices overflow"); + Require(state.samples == 1u && (!state.depth || HasStencil(state.depth->format)), "stencil replay requires single-sample rendering and an eight-bit stencil format"); + recipeOut = nullptr; + } + if (state.quadList || StripPatches(state, shaders)) recipeOut = nullptr; if (context.recorder != nullptr) context.recorder->BoundKeptBytes(); - PerformanceTimer timing("Graphics.Draw"); + auto& timing = prepared.timing.emplace("Graphics.Draw"); // APS5_PROFILE_DRAW prints the time of each phase of the draw (microseconds) and the [draws] totals. static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - DrawTimer timer(profile); + auto& timer = prepared.timer; + timer = DrawTimer(profile); // APS5_TRACE_DRAWS (together with APS5_PROFILE_DRAW; alone it only enables Driver.cpp's own // [draw] lines) additionally prints one line per draw with its phases and, for synchronous // draws, their inputs: per-draw string building at ~1400 draws per 10 s is itself a cost the // totals must not carry. static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr; - DrawOutcome outcome; - const ShaderResources::BuildTiming* built = nullptr; + auto& outcome = prepared.outcome; + auto& built = prepared.built; // The thread's GPU waits so far: the difference at the report, less the draw's own syncs // (`ownWaitedMs`, sampled around them), is what this draw waited for through the flush hook // (nested in the draw's hold, see DrawOutcome::hookWaitUs). - const auto waitedBefore = profile ? Recorder::ThreadWaitedMs() : 0.0; - double ownWaitedMs = 0; + const auto waitedBefore = prepared.waitedBefore = profile ? Recorder::ThreadWaitedMs() : 0.0; + auto& ownWaitedMs = prepared.ownWaitedMs; const auto report = [&](const char* suffix) { reportDrawEnd(state, timer, built, outcome, waitedBefore, ownWaitedMs, suffix); }; - auto inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, nullptr); - if (inputs.nothing) return; - const auto* args = draw.indirect ? &*draw.indirect : nullptr; + auto& inputs = prepared.inputs; + inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, nullptr); + if (inputs.nothing) return false; + const auto* args = prepared.args = draw.indirect ? &*draw.indirect : nullptr; const auto indexBytes = inputs.indexBytes; timing.Mark("validate"); timing.Mark("index_upload"); - // One binding per color attachment. Tiled targets are detiled and retiled on the GPU in video memory; - // only changed guest blocks are stored back. - struct TargetBinding { - ColorTarget color; - bool gpuTiling = false; - // Resident target: the surface's cached storage image is attached directly and marked dirty - // afterwards, so nothing is copied in or out per draw. - std::shared_ptr resident; - bool proxied = false; - // Linear pixels converted on the CPU, for targets the GPU detiler does not handle. - std::unique_ptr transfer; - // Guest bytes in host memory, and their device-local copies for the detiler. - std::unique_ptr tiled; - std::unique_ptr tiledDevice; - std::unique_ptr linearDevice; - std::vector original; - TileMipLayout mip{}; - std::unique_ptr target; - bool multisampled = false; - // APS5_DUMP_TARGETS: the rendered linear pixels, read back for target_
_.raw. - std::unique_ptr dump; - }; - const int dumpLimit = DumpTargetLimit(); - static std::mutex dumpMutex; - static std::map dumped; - std::vector targets(state.colors.size()); - std::vector targetViews; + const int dumpLimit = prepared.dumpLimit = DumpTargetLimit(); + auto& targets = prepared.targets; + targets.resize(state.colors.size()); + auto& targetViews = prepared.targetViews; for (std::size_t index = 0; index < state.colors.size(); ++index) { auto& binding = targets[index]; binding.color = state.colors[index]; @@ -1907,17 +2278,21 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (color.samples > 1) { binding.multisampled = true; timer.phase(PhaseSetup); - targetViews.push_back(MultisampleTargetView(context, color)); + targetViews.push_back(MultisampleTargetView(context, color, binding.multisampleOwner, &binding.multisampleImage)); timer.phase(PhaseReadTarget); continue; } APS5_LOG_OUT_DEBUG("Creating color target %zu address=0x%llx bytes=%llu extent=%ux%u", index, static_cast(color.address), static_cast(color.bytes), color.extent.width, color.extent.height); const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes); + static_cast(FlushDepthPlanes(color.address, colorLayout.Bytes(), DepthPlaneScope::Whole)); + RetireDepthSurfaces(context.device, color.address, colorLayout.Bytes()); constexpr VkBufferUsageFlags copies = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; timer.phase(PhaseSetup); // 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 @@ -1928,8 +2303,12 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (color.arrayLastSlice != 0) { Require(color.dccAddress == 0 && color.cmaskAddress == 0 && AttachmentProxyFormat(context, color.format) == VK_FORMAT_UNDEFINED, "a slice of a 2D array color target with DCC, CMASK or an attachment proxy has no resident array image"); const auto array = ArraySurfaceForTarget(color); - Require(color.surfaceAddress == color.arrayAddress + color.arraySlice * DescribeSurface(array).layerBytes, "the array image's layer pitch differs from the color view's slice offset"); - auto resident = CachedStorageSurface(context, array); + // DecodeColorBuffer offsets slice s by s times this size; every slice the image stores must agree. + const auto sliceBytes = ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(color.tileMode), color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount), 1); + const auto geometry = DescribeSurface(array); + for (std::uint32_t slice = 0; slice <= color.arrayLastSlice; ++slice) Require(geometry.GuestLayerOffset(slice) == slice * sliceBytes, "the array image's layer pitch differs from the color view's slice offset"); + Require(color.surfaceAddress == color.arrayAddress + color.arraySlice * sliceBytes, "the array image's layer pitch differs from the color view's slice offset"); + auto resident = CachedStorageLayers(context, array, color.arraySlice, color.layers); Require(resident->Attachable(), "storage format cannot be a color attachment"); return resident; } @@ -1948,6 +2327,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& materializeCmaskClear(context, color, nullptr); Require(!color.mipTail || binding.gpuTiling, "rendering into a packed mip tail requires the GPU tiled fallback"); Require(color.depth == 1, "rendering into a 3D color target needs the resident image of its surface"); + Require(color.layers == 1, "layered rendering into a 2D array color target needs the resident image of its array"); Require(color.arraySlice == 0 || !ColorTileModeIsXor(color.tileMode), "rendering into a slice of a 2D array color target with an XOR tile mode needs the resident image of its array"); if (binding.gpuTiling) { binding.mip = color.mipTail ? ComputeColorTargetMipLayout(color.tileMode, color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount, color.mip) : ColorTargetMip(color, colorLayout); @@ -1981,13 +2361,43 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& binding.target = std::make_unique(context, color, state.blends.at(color.exportIndex).blendEnable != 0); targetViews.push_back(binding.target->View()); } - if (state.depth) targetViews.push_back(DepthSurfaceView(context, *state.depth)); + if (state.depth) { + targetViews.push_back(DepthSurfaceView(context, *state.depth, state.depthWrite)); + if (writesDepthTarget(state)) NoteDepthSurfaceWrite(context, *state.depth); + } timer.phase(PhasePrepare); const bool recordDraws = RecordDraws(); - auto* recorder = Recorder::Active(); - const bool recordable = recordDraws && recorder != nullptr && dumpLimit == 0 && std::all_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.resident != nullptr || binding.multisampled; }); - auto resolved = resolveDrawResources(context, state, draw, shaders, snapshots, indexBytes, recordable, outcome, timer); + auto* recorder = prepared.recorder = Recorder::Active(); + bool recordable = recordDraws && recorder != nullptr && dumpLimit == 0 && std::all_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.resident != nullptr || binding.multisampled; }); + auto& resolved = prepared.resolved; + resolved = resolveDrawResources(context, state, draw, shaders, snapshots, indexBytes, recordable, outcome, timer); auto& resources = resolved.resources; + if (!stencilReferences.empty() && resources->ChecksFaults()) recordable = false; + if (!stencilReferences.empty()) { + const auto readImages = resources->StorageImages(); + const auto inputOverlaps = [&](std::uint64_t address, std::uint64_t bytes) { + return std::ranges::any_of(inputs.inPlaceReads, [&](const auto& range) { return address < range.second && range.first < address + bytes; }); + }; + for (const auto& target : targets) { + const bool imageRead = target.resident != nullptr && std::ranges::any_of(readImages, [&](const auto& image) { return image.first == target.resident->Image(); }); + Require(!imageRead && !resources->ReadsImage(target.resident.get()) && !resources->ReadsOverlap(target.color.address, static_cast(target.color.bytes)) && !inputOverlaps(target.color.address, target.color.bytes), "stencil replay refuses attachment feedback reads"); + } + if (state.depth) { + const auto bytes = static_cast(state.depth->extent.width) * state.depth->extent.height * 4u; + Require(!resources->ReadsOverlap(state.depth->address, bytes) && !resources->ReadsOverlap(state.depth->stencilAddress, bytes) && !inputOverlaps(state.depth->address, bytes) && !inputOverlaps(state.depth->stencilAddress, bytes), "stencil replay refuses depth or stencil feedback reads"); + for (const auto& shader : shaders) { + for (const auto& binding : shader.program->bindings) { + 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"); + } + } + } + } + } const auto& contentKey = resolved.contentKey; const bool cacheable = resolved.cacheable; built = resolved.built; @@ -2000,9 +2410,9 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // and records plain draws with the same per-dimension rules. The import pointer stays valid: // refreshImports runs only under GuestMemory::GpuMutex, held here, and a retired import's // buffer is kept while the recorder is busy. - IndirectRecord indirect; + auto& indirect = prepared.indirect; indirect.args = args; - std::vector records; + auto& records = prepared.records; if (args != nullptr) { static const bool gpuIndirectDraws = std::getenv("APS5_NO_GPU_INDIRECT_DRAW") == nullptr; const auto decide = [&](std::uint64_t address, std::size_t bytes, const HostImport*& import) { @@ -2023,7 +2433,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& const auto rangeBytes = args->RangeBytes(); if (rangeBytes == 0) { report(" indirect draw without records"); - return; + return false; } Require(rangeBytes <= std::numeric_limits::max(), "indirect draw record range overflow"); if (!gpuIndirectDraws) indirect.path = IndirectDrawPath::Disabled; @@ -2044,7 +2454,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (std::none_of(records.begin(), records.end(), [](const Pm4::DrawArguments& record) { return record.count != 0 && record.instances != 0; })) { CountIndirectDraw(indirect.path, indirect.readMs); report(" indirect draw with empty records"); - return; + return false; } } } @@ -2069,14 +2479,14 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& static const bool syncCompletionDraws = std::getenv("APS5_SYNC_COMPLETION_DRAWS") != nullptr; static const bool recorderSyncDraws = std::getenv("APS5_NO_RECORDER_SYNC_DRAWS") == nullptr; static const bool recordCopiedDraws = std::getenv("APS5_NO_RECORD_COPIED_DRAWS") == nullptr; - const bool completion = resources->NeedsCompletion(); + const bool completion = prepared.completion = resources->NeedsCompletion(); const bool copiedWrites = completion && resources->HasCopiedWrites(); // A lease only forces the wait when deferred release is off. const bool lease = resources->HoldsLease() && SyncLeaseWork(); outcome.completion = completion; outcome.recorded = recordable; // Whether the copied write-back is listed in DrawCopiedWriters instead of waited for. - bool listed = false; + auto& listed = prepared.listed; if (!recordable) { outcome.reason = !recordDraws || dumpLimit != 0 ? SyncDisabled : recorder == nullptr ? SyncNoRecorder : SyncNotResident; } else if (completion && (syncCompletionDraws || copiedWrites || lease)) { @@ -2086,7 +2496,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& else if (recorderSyncDraws) outcome.waited = true; else outcome.recorded = false; } - const bool recorded = outcome.recorded; + const bool recorded = prepared.recorded = outcome.recorded; // A build this recorded draw can share with later identical ones goes into the cache (a cache // hit is reusable by construction, so `recorded` holds for it; one with completion work is // never reusable). @@ -2095,16 +2505,21 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // aid: APS5_DRAW_TRANSITIONS=1 keeps the per-draw upload and download barriers, the layout // transitions of every target and one pass per draw, as before. static const bool drawTransitions = std::getenv("APS5_DRAW_TRANSITIONS") != nullptr; - const bool lean = recorded && !drawTransitions; - const bool multisampled = std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.multisampled; }); + const bool lean = prepared.lean = recorded && !drawTransitions; + const bool multisampled = prepared.multisampled = std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.multisampled; }); Require(lean || !multisampled, "multisampled draws are only rendered as recorded draws"); + for (const auto& binding : targets) { + if (binding.multisampled) Require(std::find(resources->MultisampleSources().begin(), resources->MultisampleSources().end(), binding.color.surfaceAddress) == resources->MultisampleSources().end(), "sampling a multisampled color target the draw renders to is not implemented"); + } if (!lean && !resolved.moved.empty()) { resolved.resources = std::make_shared(context, shaders, state.color, static_cast(state.colors.size()), draw.indexAddress, static_cast(indexBytes), snapshots); resolved.moved.clear(); } + if (state.depth) RequireNoNativeDepthFeedback_nid_no_patch(context, *state.depth, resources->SampledTextures()); APS5_LOG_CHARS_OUT_DEBUG("Creating Pipeline"); // The state is copied only when a mask must change. - auto masked = maskedState(state, inputs.fragmentOutputs); + auto& masked = prepared.masked; + masked = maskedState(state, inputs.fragmentOutputs); if (std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.proxied; })) recipeOut = nullptr; for (std::size_t index = 0; index < targets.size(); ++index) { if (!targets[index].proxied) continue; @@ -2119,20 +2534,84 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& const auto built = resources->SetLayouts(); Require(described.layouts.main == built.main && described.layouts.fragment == built.fragment && described.layouts.split == built.split && std::ranges::equal(described.mainKey, built.mainKey) && std::ranges::equal(described.fragmentKey, built.fragmentKey), "set layouts described from the shaders differ from the draw's"); } - auto pipeline = CachedPipeline(context, pipelineState, inputs.vertexInput, *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + auto& stencilReplay = prepared.stencilReplay; + for (const auto reference : stencilReferences) { + auto selected = pipelineState; + selected.stencilReplayReference = reference; + selected.stencilFront.reference = reference; + selected.stencilBack.reference = reference; + auto selectedPipeline = stencilReplay.empty() ? CachedPipeline(context, selected, inputs.VertexLayout(), *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) : stencilReplay.front().second; + Require(!selectedPipeline->SplitsFaces() && !selectedPipeline->SplitsSecondChannel(), "stencil replay refuses pipeline face or channel splits"); + stencilReplay.emplace_back(reference, std::move(selectedPipeline)); + } + auto& pipeline = prepared.pipeline; + pipeline = stencilReplay.empty() ? CachedPipeline(context, pipelineState, inputs.VertexLayout(), *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) : stencilReplay.front().second; // Resident targets' views are stable while their storage image lives, so the framebuffer is // reused with the pipeline; a per-draw RenderTarget gets a framebuffer of its own. - std::vector> owners; + auto& owners = prepared.owners; owners.reserve(targets.size()); - std::vector persistent; - persistent.reserve(targets.size()); + auto& framebufferOwners = prepared.framebufferOwners; + framebufferOwners.reserve(targets.size()); for (const auto& binding : targets) { owners.push_back(binding.resident); - persistent.push_back(binding.multisampled ? 1u : 0u); + framebufferOwners.push_back(binding.multisampled ? binding.multisampleOwner : binding.resident); } - auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent, state.renderLayers, persistent); + auto& framebuffer = prepared.framebuffer; + framebuffer = pipeline->AcquireFramebuffer(targetViews, framebufferOwners, state.renderExtent, state.renderLayers); + auto& reordered = prepared.reordered; + auto& reorderPass = prepared.reorderPass; + if (pipeline->ReordersSamples()) { + reorderPass = SampleOrderPassFor(context, pipelineState, pipeline->HostSampleOrder()); + for (const auto& step : PlanSampleOrder(context, state, pipeline->HostSampleOrder())) { + if (step.attachment < targets.size()) reordered.push_back(MultisampleTargetOrderTarget(context, targets[step.attachment].color)); + else reordered.push_back(DepthSurfaceOrderTarget(context, *state.depth)); + Require(reordered.back().method == step.method, "a reordered attachment's surface disagrees with the planned way of putting its samples in order"); + } + } + prepared.depthLocations = passDepthLocations(context, pipelineState, *pipeline); timer.phase(PhasePipeline); APS5_LOG_CHARS_OUT_DEBUG("Pipeline created"); + return true; +} + +void recordPreparedDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, PreparedDraw& prepared) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr; + static std::mutex dumpMutex; + static std::map dumped; + auto& timing = *prepared.timing; + auto& timer = prepared.timer; + auto& outcome = prepared.outcome; + const auto* built = prepared.built; + const auto waitedBefore = prepared.waitedBefore; + auto& ownWaitedMs = prepared.ownWaitedMs; + const auto report = [&](const char* suffix) { reportDrawEnd(state, timer, built, outcome, waitedBefore, ownWaitedMs, suffix); }; + auto* recipeOut = prepared.recipeOut; + const auto& stencilReferences = prepared.stencilReferences; + auto& inputs = prepared.inputs; + const auto* args = prepared.args; + const int dumpLimit = prepared.dumpLimit; + auto& targets = prepared.targets; + const auto& targetViews = prepared.targetViews; + auto* recorder = prepared.recorder; + auto& resolved = prepared.resolved; + auto& resources = resolved.resources; + const auto& contentKey = resolved.contentKey; + const bool cacheable = resolved.cacheable; + auto& indirect = prepared.indirect; + const bool completion = prepared.completion; + const bool listed = prepared.listed; + const bool recorded = prepared.recorded; + const bool lean = prepared.lean; + const bool multisampled = prepared.multisampled; + const auto& masked = prepared.masked; + const auto& stencilReplay = prepared.stencilReplay; + const auto& pipeline = prepared.pipeline; + const auto& owners = prepared.owners; + const auto& framebuffer = prepared.framebuffer; + const auto& reordered = prepared.reordered; + const auto& reorderPass = prepared.reorderPass; + const auto& depthLocations = prepared.depthLocations; if (lean) { RecordedDraw record; record.recorder = recorder; @@ -2142,20 +2621,28 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& record.framebuffer = framebuffer; record.targetViews = targetViews; record.targets = owners; - for (const auto& binding : targets) record.targetSlices.push_back(binding.color.arraySlice); + for (const auto& binding : targets) record.targetSlices.emplace_back(binding.color.arraySlice, binding.color.arrayLastSlice != 0 ? binding.color.layers : 1u); for (const auto& binding : targets) { if (binding.proxied) record.proxies.push_back(binding.resident); + if (binding.multisampled) { + record.multisampleImages.push_back(binding.multisampleImage); + record.multisampleTargets.push_back(binding.multisampleOwner); + } } record.indirect = args != nullptr ? &indirect : nullptr; record.listed = listed; record.completion = completion; record.waited = outcome.waited; + record.stencilReplay = stencilReplay; + record.reordered = reordered; + record.reorderPass = reorderPass; + record.depthLocations = depthLocations; recordDraw(context, state, draw, shaders, inputs, record, outcome, timer, ownWaitedMs); // The recipe for the caller's draw-cache entry (design_cpu_final M8, rule R3 for draws): // only a template the resource cache serves under this content key (reusable: no lease, // no copied writes, every direct region import- or mirror-served) of a direct draw, so a // hit's proof is the template's ProveCurrent and nothing needs completion work. - if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && resolved.moved.empty() && !multisampled) { + if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && resolved.moved.empty() && !multisampled && !pipeline->ReordersSamples()) { auto recipe = std::make_shared(); recipe->device = context.device; recipe->templateRef = resources; @@ -2165,7 +2652,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& for (const auto& owner : owners) recipe->targets.emplace_back(owner); recipe->targetViews = targetViews; recipe->passKey = DrawRenderPassKey(context, state, targetViews); - recipe->vertexInput = inputs.vertexInput; + recipe->vertexInput = inputs.VertexLayout(); recipe->pushStages = PushConstantStages(shaders); if (recipe->pushStages != 0) { recipe->pushBytes = AssemblePushConstants(shaders); @@ -2235,6 +2722,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); @@ -2251,7 +2742,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& continue; } if (binding.resident != nullptr) { - imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); + imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, {VK_IMAGE_ASPECT_COLOR_BIT, binding.color.mip, 1, binding.resident->ImageDepth() == 1 ? binding.color.arraySlice : 0, binding.resident->ImageDepth() == 1 ? binding.color.layers : 1}); countBarrier(); continue; } @@ -2270,6 +2761,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& context.Resolved(&DeviceFunctions::cmdCopyBufferToImage, "vkCmdCopyBufferToImage")(commands, linear, binding.target->Image(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); imageBarrier(context, commands, binding.target->Image(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); } + std::vector> reorderKept; + RecordSampleOrder(context, commands, reorderPass, reordered, true, reorderKept); APS5_LOG_OUT_DEBUG("Beginning pipeline renderExtent=%ux%u", state.renderExtent.width, state.renderExtent.height); pipeline->Begin(commands, *framebuffer, state.renderExtent, state); APS5_LOG_CHARS_OUT_DEBUG("Pipeline Begin OK"); @@ -2277,7 +2770,11 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& APS5_LOG_CHARS_OUT_DEBUG("Resources bound"); pushDrawConstants(*pipeline, commands, state, draw, shaders, *resources, nullptr, 0, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0, !inputs.restartTable.empty()); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); - recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset); + if (stencilReplay.empty()) recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset); + else { + recordStencilReplay(context, commands, state, draw, inputs, stencilReplay, shaders, *resources); + if (recorded) for (const auto& [reference, selectedPipeline] : stencilReplay) recorder->Keep(selectedPipeline); + } if (pipeline->SplitsFaces()) { pipeline->ContinueBackFaces(commands, state); recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset); @@ -2292,6 +2789,15 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& APS5_LOG_CHARS_OUT_DEBUG("Draw recorded"); if (recorded) recorder->EndPassSamples(); EndRenderPass(context, commands); + RecordSampleOrder(context, commands, reorderPass, reordered, false, reorderKept); + if (depthLocations) { + RecordDepthSampleLocations(context, commands, *depthLocations); + countBarrier(2); + reorderKept.push_back(depthLocations->keep); + } + if (recorded) { + for (auto& object : reorderKept) recorder->Keep(std::move(object)); + } APS5_LOG_CHARS_OUT_DEBUG("Render pass ended"); for (auto& binding : targets) { if (binding.proxied) { @@ -2300,7 +2806,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& continue; } if (binding.resident != nullptr) { - imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT); + imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT, {VK_IMAGE_ASPECT_COLOR_BIT, binding.color.mip, 1, binding.resident->ImageDepth() == 1 ? binding.color.arraySlice : 0, binding.resident->ImageDepth() == 1 ? binding.color.layers : 1}); countBarrier(); continue; } @@ -2343,7 +2849,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& APS5_LOG_CHARS_OUT_DEBUG("SubmitAndWait"); timer.phase(PhaseRecord); if (recorded) { - keepRecordedDraw(*recorder, resources, pipeline, framebuffer, inputs, owners, [&] { std::vector slices; for (const auto& binding : targets) slices.push_back(binding.color.arraySlice); return slices; }(), std::move(scratch), std::move(checkRecords), listed, completion, outcome); + keepRecordedDraw(*recorder, resources, pipeline, framebuffer, inputs, owners, [&] { std::vector> slices; for (const auto& binding : targets) slices.emplace_back(binding.color.arraySlice, binding.color.arrayLastSlice != 0 ? binding.color.layers : 1u); return slices; }(), std::move(scratch), std::move(checkRecords), listed, completion, outcome); timer.phase(PhaseKeep); if (outcome.waited) { // The wait the dispatch path makes for such work (source 3, "address-based"): the @@ -2362,6 +2868,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // The recorder sync SubmitAndWait makes is the draw's own wait, not a nested hook wait. const auto ownBefore = profile ? Recorder::ThreadWaitedMs() : 0.0; batch->SubmitAndWait(); + if (!stencilReferences.empty()) resources->CheckFaults(); if (profile) ownWaitedMs += Recorder::ThreadWaitedMs() - ownBefore; } if (checkRecords) checkRecords(); @@ -2388,7 +2895,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (skipTargetWrite) break; if (binding.resident != nullptr) { // The results stay on the GPU until something reads the target's memory. - binding.resident->MarkSliceDirty(binding.color.arraySlice); + binding.resident->MarkSliceDirty(binding.color.arraySlice, binding.color.arrayLastSlice != 0 ? binding.color.layers : 1u); continue; } if (binding.gpuTiling) GuestMemory::WriteChanged(binding.color.address, binding.tiled->Bytes(), binding.original); @@ -2429,6 +2936,15 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& APS5_LOG_CHARS_OUT_DEBUG("Draw finished"); } +} + +void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, std::shared_ptr* recipeOut) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + PreparedDraw prepared(profile); + prepared.recipeOut = recipeOut; + if (prepareDraw(context, state, draw, shaders, snapshots, prepared)) recordPreparedDraw(context, state, draw, shaders, prepared); +} + DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, const DrawRecipe& recipe) { if (context.recorder != nullptr) context.recorder->BoundKeptBytes(); static_cast(snapshots); @@ -2451,7 +2967,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con }; Require(!draw.indirect, "a draw recipe covers direct draws only"); auto* recorder = Recorder::Active(); - if (!RecordDraws() || recorder == nullptr || DumpTargetLimit() != 0) return miss(DrawRecipeMiss::NotRecordable); + if (state.quadList || StripPatches(state, shaders) || !RecordDraws() || recorder == nullptr || DumpTargetLimit() != 0) return miss(DrawRecipeMiss::NotRecordable); if (recipe.hasDepth != state.depth.has_value()) return miss(DrawRecipeMiss::TargetGone); const std::size_t expectedViews = state.colors.size() + (recipe.hasDepth ? 1u : 0u); Require(recipe.targets.size() == state.colors.size() && recipe.targetViews.size() == expectedViews, "draw recipe targets do not match the state"); @@ -2473,7 +2989,8 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con auto stored = recipe.targets[index].lock(); if (stored == nullptr || !StorageImageCached(context, stored.get()) || !StorageImageServesKeys(*stored, state.colors[index].dccAddress)) return miss(DrawRecipeMiss::TargetGone); auto resident = refreshResidentTarget(context, state, state.colors[index], outcome, profile, [&] { - stored->Refresh(); + if (state.colors[index].arrayLastSlice != 0) stored->RefreshLayers(state.colors[index].arraySlice, 1); + else stored->Refresh(); return stored; }); timer.phase(PhaseReadTarget); @@ -2482,7 +2999,8 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con } if (recipe.hasDepth) { timer.phase(PhaseSetup); - const auto depthView = DepthSurfaceView(context, *state.depth); + const auto depthView = DepthSurfaceView(context, *state.depth, state.depthWrite); + if (writesDepthTarget(state)) NoteDepthSurfaceWrite(context, *state.depth); timer.phase(PhaseReadTarget); if (recipe.targetViews.back() != depthView) return miss(DrawRecipeMiss::TargetGone); } @@ -2499,6 +3017,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con if (!proved) { SharedResourceCache().Remove(recipe.key, resources.get()); countCache(&DrawProfile::cacheInvalidated); + resources->KeepStorageImageBytes(*recorder); recorder->Keep(std::move(resources)); timer.phase(PhaseLookup); return miss(DrawRecipeMiss::Proof); @@ -2516,7 +3035,10 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con auto pipeline = recipe.pipeline.lock(); if (pipeline == nullptr) return miss(DrawRecipeMiss::ObjectsGone); auto framebuffer = recipe.framebuffer.lock(); - if (framebuffer == nullptr) framebuffer = pipeline->AcquireFramebuffer(recipe.targetViews, targets, state.renderExtent, state.renderLayers); + if (framebuffer == nullptr) { + const std::vector> framebufferOwners(targets.begin(), targets.end()); + framebuffer = pipeline->AcquireFramebuffer(recipe.targetViews, framebufferOwners, state.renderExtent, state.renderLayers); + } timer.phase(PhasePipeline); const auto recordStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; RecordedDraw record; @@ -2527,9 +3049,10 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con record.framebuffer = std::move(framebuffer); record.targetViews = recipe.targetViews; record.targets = std::move(targets); - for (const auto& color : state.colors) record.targetSlices.push_back(color.arraySlice); + for (const auto& color : state.colors) record.targetSlices.emplace_back(color.arraySlice, color.arrayLastSlice != 0 ? color.layers : 1u); record.pushBytes = &recipe.pushBytes; record.pushStages = recipe.pushStages; + record.depthLocations = passDepthLocations(context, state, *record.pipeline); recordDraw(context, state, draw, shaders, inputs, record, outcome, timer, ownWaitedMs); if (profile) { result.recordUs = std::chrono::duration(std::chrono::steady_clock::now() - recordStart).count(); @@ -2553,16 +3076,26 @@ std::shared_ptr metadataPassResident(const Context& context, con } +void RunDepthCopyPass(const Context& context, const DepthCopyPass& pass) { + Require(pass.color.tileMode != ColorTileMode::Linear && context.detiler != nullptr, "DB depth copy into a target without a resident image is not implemented"); + const auto resident = CachedStorageSurface(context, SurfaceForTarget(pass.color)); + CopyDepthSample(context, pass.depthAddress, pass.samples, pass.sample, *resident, resident->StorageView(0, false), pass.extent); +} + void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) { if (pass.mode == ColorMetadataPass::Mode::Resolve) { Require(pass.source.has_value() && pass.targets.size() == 1, "CB resolve without its source and destination"); const auto& destination = pass.targets.front(); - Require(destination.tileMode == ColorTileMode::RenderTarget && context.detiler != nullptr, "CB resolve into a target without a resident image is not implemented"); + Require(destination.tileMode != ColorTileMode::Linear && context.detiler != nullptr, "CB resolve into a target without a resident image is not implemented"); const auto resident = CachedStorageSurface(context, SurfaceForTarget(destination)); Require(resident->Attachable(), "storage format cannot be a color attachment"); ResolveMultisampleTarget(context, *pass.source, *resident, resident->AttachmentView(destination.format, destination.mip)); return; } + if (pass.mode == ColorMetadataPass::Mode::FmaskDecompress) { + for (const auto& color : pass.targets) MaterializeMultisampleTargetClear(context, color); + return; + } for (const auto& color : pass.targets) { if (color.cmaskAddress != 0) { const auto resident = metadataPassResident(context, color); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp index 973aff1e34..9881ba2473 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp @@ -28,10 +28,7 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) { layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout color transfer"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(COLOR_TRANSFER_SPV); - moduleInfo.pCode = COLOR_TRANSFER_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule color transfer"); + module = CreateShaderModule(context, COLOR_TRANSFER_SPV, sizeof(COLOR_TRANSFER_SPV), "vkCreateShaderModule color transfer"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index bdb5794498..c46494982e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -11,6 +12,7 @@ #include "prx/libc/include/CpuTopology.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" +#include "prx/libc/include/general/AtomicSharedPtr.hpp" #ifdef _WIN32 #include #else @@ -29,8 +31,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -72,6 +76,7 @@ struct ImageMirror { // Identity for the life of this mirror (see ImageMirrorSerial); the top bit keeps mirror serials // apart from host import serials. std::uint64_t serial = 0; + std::atomic watchedSerial{0}; }; // The address space. An address-based build maps every readable registered range; building that @@ -110,8 +115,15 @@ struct GuestBufferMemory::AddressSpace { std::vector copied; // The BDA table entries of `base`, in its order. std::vector ranges; + std::shared_ptr>> directReads; mutable std::mutex tableMutex; - mutable std::map>, std::pair>>> writeTables; + struct Table { + std::uint64_t serial; + std::shared_ptr> ranges; + std::list*>::iterator recent; + }; + mutable std::map, Table> tables; + mutable std::list*> tableRecency; }; namespace { @@ -119,7 +131,7 @@ namespace { struct RetiredImport; struct AddressSpaceCache { - std::atomic> current; + AtomicSharedPtr current; std::atomic serials{0}; // Set by the waiter's drop, cleared by the next publish: the rebuild's reason. std::atomic droppedByWaiter{false}; @@ -151,6 +163,67 @@ struct ImportFailure { GuestAllocations::Mapped ranges; }; +struct ImportRefusal { + std::uint64_t base; + std::uint64_t bytes; + GuestAllocations::Mapped ranges; + std::uint64_t liveBytes; + std::uint64_t budget; +}; + +struct EvictingRefusal { + std::uint64_t base; + std::uint64_t bytes; + GuestAllocations::Mapped ranges; + std::uint64_t epoch; + std::uint64_t liveBytes; + std::uint64_t budget; + std::uint64_t room; + std::uint64_t window; + std::uint64_t floor; + std::uint64_t ready; + std::uint64_t readyHeld; + bool joined; +}; + +#ifdef _WIN32 +constexpr std::size_t settleBatchCount = 4; +constexpr std::uint32_t settleBatchCopies = 256; + +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 { @@ -163,7 +236,11 @@ struct HostImports { std::uint64_t liveBytes = 0; std::map failed; std::map failedJoins; - std::set unaliasedSpans; + std::map sparseRunEnds; + std::map sparseRunJoins; + std::map, ImportFailure> failedBlocks; + std::map, ImportRefusal> refusals; + std::map, EvictingRefusal> evictingRefusals; // Registry generation the imports were last reconciled with. std::uint64_t refreshedGeneration = 0; // Bumped whenever an import is dropped, so HostImport pointers taken under the lock earlier can be @@ -172,8 +249,34 @@ struct HostImports { VkDevice watchDevice = VK_NULL_HANDLE; bool unwatchImports = false; bool unwatchDmaBufImports = false; + bool watchPrivateImports = false; + std::map settles; +#ifdef _WIN32 + 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 settlesKeptCompared = 0; + std::uint64_t settlesParked = 0; + std::uint64_t settleUnmapWaits = 0; +#endif std::uint64_t useCounter = 0; std::uint64_t buildFloor = 0; + std::array counts{}; + VkPhysicalDevice reportedDevice = VK_NULL_HANDLE; + std::chrono::steady_clock::time_point reportedAt{}; + std::uint64_t reportedBudget = 0; + std::uint32_t reportedHeap = 0; + std::uint64_t reportedHeapBudget = 0; + std::uint64_t reportedHeapUsage = 0; + std::uint64_t ceiling = 0; + std::uint32_t allocationFailures = 0; + bool allocationFailed = false; + std::uint8_t reportNoted = 0; + std::map spaceUses; }; HostImports& Imports() { @@ -181,6 +284,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 (batch.busy) 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); @@ -217,13 +371,22 @@ 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. RetireShadow(device, it->second, [&lease, joined = it->second.joined](std::uint64_t begin, std::uint64_t end) { return runCovers(lease, begin, end) || (joined && joinedCovers(lease, begin, end)); }); ++state.epoch; - state.liveBytes -= it->second.bytes; + state.liveBytes -= it->second.pinnedBytes; 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)); @@ -250,10 +413,65 @@ 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 (const auto alias : batch.aliases) settle.settleDestroyBuffer(device, alias, nullptr); + batch.aliases.clear(); + 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 + +VkResult createImportBuffer(const Context& context, VkExternalMemoryHandleTypeFlagBits handleType, VkDeviceSize size, VkBuffer* buffer) { + const VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, nullptr, static_cast(handleType)}; + VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; + info.size = size; + 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; + return context.Function("vkCreateBuffer")(context.device, &info, nullptr, buffer); +} + const char* bindImport(const Context& context, HostImport& entry, VkExternalMemoryHandleTypeFlagBits handleType, const void* import, std::uint32_t importTypes, bool& allocated, VkResult& failure) { const auto failed = [&](const char* step, VkResult result) -> const char* { if (entry.buffer) context.Function("vkDestroyBuffer")(context.device, entry.buffer, nullptr); - if (entry.memory) context.Function("vkFreeMemory")(context.device, entry.memory, nullptr); + if (allocated && entry.memory) context.Function("vkFreeMemory")(context.device, entry.memory, nullptr); entry.buffer = VK_NULL_HANDLE; entry.memory = VK_NULL_HANDLE; failure = result; @@ -266,7 +484,13 @@ 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; - if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &entry.buffer); result != VK_SUCCESS) return failed("vkCreateBuffer", result); +#ifndef _WIN32 + ImportFamilies families; + if (handleType == VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT) shareDmaBufImport(context, info, families, false); +#endif + VkBuffer buffer = VK_NULL_HANDLE; + if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer); result != VK_SUCCESS) return failed("vkCreateBuffer", result); + entry.buffer = buffer; VkMemoryRequirements requirements{}; context.Function("vkGetBufferMemoryRequirements")(context.device, entry.buffer, &requirements); const auto types = requirements.memoryTypeBits & importTypes; @@ -277,6 +501,7 @@ const char* bindImport(const Context& context, HostImport& entry, VkExternalMemo allocation.memoryTypeIndex = static_cast(std::countr_zero(types)); if (const auto result = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &entry.memory); result != VK_SUCCESS) return failed("vkAllocateMemory", result); allocated = true; + entry.memoryType = allocation.memoryTypeIndex; if (const auto result = context.Function("vkBindBufferMemory")(context.device, entry.buffer, entry.memory, 0); result != VK_SUCCESS) return failed("vkBindBufferMemory", result); const VkBufferDeviceAddressInfo addressInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, entry.buffer}; entry.address = context.Function("vkGetBufferDeviceAddressKHR")(context.device, &addressInfo); @@ -341,6 +566,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; @@ -444,7 +670,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; @@ -497,11 +724,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{}; @@ -515,13 +739,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); @@ -572,9 +798,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 @@ -628,13 +855,315 @@ ImportWatchRequest importWatchRequest() { } #endif +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); + 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, &settle.settleTarget)) != VK_SUCCESS) { + settle.settleTarget = VK_NULL_HANDLE; + return "vkCreateBuffer (settle)"; + } + VkMemoryRequirements 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, &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, &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, &settle.settleFence)) != VK_SUCCESS) { + settle.settleFence = VK_NULL_HANDLE; + return "vkCreateFence (settle)"; + } + } + 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; + VkBufferCreateInfo aliasInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; + aliasInfo.size = std::min(entry.bytes, 65536); + aliasInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + if ((result = context.Function("vkCreateBuffer")(context.device, &aliasInfo, nullptr, &alias)) != VK_SUCCESS) return "vkCreateBuffer (settle alias)"; + if ((result = context.Function("vkBindBufferMemory")(context.device, alias, entry.memory, 0)) != VK_SUCCESS) return "vkBindBufferMemory (settle alias)"; + source = alias; + } +#endif + if ((result = context.Function("vkResetFences")(context.device, 1, &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")(settle.settleCommands, &begin)) != VK_SUCCESS) return "vkBeginCommandBuffer (settle)"; + const VkBufferCopy region{0, 0, 4}; + 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 = &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; + for (const auto base : state.settleReady) { + const auto settle = state.pendingSettles.find(base); + if (settle == state.pendingSettles.end() || settle->second.phase != SettlePhase::Settled) continue; + state.pendingSettles.erase(settle); + const auto import = state.imports.find(base); + if (import == state.imports.end() || !import->second.unwatched) continue; + const std::array, 1> span{{{base, base + import->second.bytes}}}; + if (StorageTexture::AnyLiveOverlaps(span)) { + ++state.settlesKeptCompared; + continue; + } + ranges.push_back(span.front()); + } + state.settleReady.clear(); + 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) { if (state.watchDevice == context.device) return; #ifdef _WIN32 state.watchDevice = context.device; state.unwatchImports = true; state.unwatchDmaBufImports = false; - if (context.hostImportAlignment != 0 && GuestMemory::WriteWatched()) std::fprintf(stderr, "[write-watch] Windows direct host imports use comparisons; separate shared aliases retain guest write tracking\n"); + state.watchPrivateImports = false; + if (context.hostImportAlignment == 0 || !GuestMemory::WriteWatched()) return; + if (context.settleQueue == VK_NULL_HANDLE || context.settleQueueMutex == nullptr) { + std::fprintf(stderr, "[write-watch] Windows direct host imports use comparisons (no second queue to settle imports on); separate shared aliases retain guest write tracking\n"); + return; + } + const auto probe = ProbeImportWriteProtection(context); + if (probe.failure != nullptr) { + std::fprintf(stderr, "[write-watch] host imports of write-watched memory keep the write watch: unknown (probe failed at %s, %d); direct host imports use comparisons, separate shared aliases retain guest write tracking\n", probe.failure, static_cast(probe.result)); + return; + } + state.watchPrivateImports = probe.writtenAfterSettle == 0 && probe.writtenAfterSubmit == 0 && probe.writtenAfterSecondSubmit == 0 && probe.writtenAfterFill == 0 && probe.writtenByCpu != 0; + std::fprintf(stderr, "[write-watch] host imports of write-watched memory keep the write watch: %s (%u of %u scratch pages reported written at the import, %u by its 4-byte settle read, %u after it, %u after a full GPU read, %u after a second, %u after a GPU write, %u after a CPU store); direct host imports of private memory %s, separate shared aliases retain guest write tracking\n", state.watchPrivateImports ? "yes" : "no", probe.writtenAtImport, probe.pages, probe.writtenDuringSettle, probe.writtenAfterSettle, probe.writtenAfterSubmit, probe.writtenAfterSecondSubmit, probe.writtenAfterFill, probe.writtenByCpu, state.watchPrivateImports ? "are settled and stay watched" : "use comparisons"); #else const auto request = importWatchRequest(); state.watchDevice = context.device; @@ -704,30 +1233,77 @@ std::size_t leasedIndexContaining(const GuestAllocations::Lease& lease, std::uin std::atomic importWindow{0}; -ImportRun importRun(const GuestAllocations::Lease& lease, std::uint64_t address) { +ImportRun leasedRun(const GuestAllocations::Lease& lease, std::size_t first, std::size_t last) { + ImportRun run{lease[first]->address, lease[last]->address + lease[last]->bytes - lease[first]->address, {}}; + run.ranges.assign(lease.begin() + static_cast(first), lease.begin() + static_cast(last) + 1); + return run; +} + +template +const Remembered* rememberedRun(const std::map, Remembered>& runs, const GuestAllocations::Lease& lease, std::size_t first, std::size_t last) { + const auto base = lease[first]->address; + const auto found = runs.find({base, lease[last]->address + lease[last]->bytes - base}); + if (found == runs.end() || found->second.ranges.size() != last - first + 1) return nullptr; + for (std::size_t index = first; index <= last; ++index) { + const auto& remembered = found->second.ranges[index - first]; + if (remembered.owner_before(lease[index]) || lease[index].owner_before(remembered)) return nullptr; + } + return &found->second; +} + +bool stillRefused(const HostImports& state, const ImportRefusal* refusal, std::uint64_t budget) { + return refusal != nullptr && budget != 0 && budget <= refusal->budget && state.liveBytes >= refusal->liveBytes; +} + +ImportRun importRun(const GuestAllocations::Lease& lease, std::uint64_t address, std::uint64_t largest = std::numeric_limits::max(), std::uint64_t budget = 0) { const auto index = leasedIndexContaining(lease, address); if (index == lease.size()) return {}; const auto& containing = *lease[index]; + const auto& state = Imports(); + const auto window = importWindow.load(std::memory_order_relaxed); std::uint64_t low = 0; std::uint64_t high = std::numeric_limits::max(); - const auto window = importWindow.load(std::memory_order_relaxed); if (window != 0) { low = address / window * window; high = low + window; - if (containing.address < low || containing.address + containing.bytes > high) { - low = containing.address; - high = containing.address + containing.bytes; - } + if (containing.address < low || containing.address + containing.bytes > high) return leasedRun(lease, index, index); } auto first = index; while (first > 0 && lease[first - 1]->address >= low && continuesRun(*lease[first - 1], *lease[first])) --first; auto last = index; while (last + 1 < lease.size() && lease[last + 1]->address + lease[last + 1]->bytes <= high && continuesRun(*lease[last], *lease[last + 1])) ++last; - if (window != 0 && (lease[first]->address != low || lease[last]->address + lease[last]->bytes != high)) first = last = index; - if (first != last && Imports().unaliasedSpans.contains(lease[first]->address)) return {lease[index]->address, lease[index]->bytes, {lease[index]}}; - ImportRun run{lease[first]->address, lease[last]->address + lease[last]->bytes - lease[first]->address, {}}; - run.ranges.assign(lease.begin() + static_cast(first), lease.begin() + static_cast(last) + 1); - return run; + const auto skipped = [&](std::size_t from, std::size_t to) { return rememberedRun(state.failedBlocks, lease, from, to) != nullptr || stillRefused(state, rememberedRun(state.refusals, lease, from, to), budget); }; + if (window == 0) return first == last || lease[last]->address + lease[last]->bytes - lease[first]->address > largest || skipped(first, last) ? leasedRun(lease, index, index) : leasedRun(lease, first, last); + const auto bounding = [&](std::size_t from, std::size_t to, std::uint64_t value, bool start) { + const auto found = std::lower_bound(lease.begin() + static_cast(from), lease.begin() + static_cast(to) + 1, value, [start](const auto& range, std::uint64_t wanted) { return (start ? range->address : range->address + range->bytes) < wanted; }); + const auto position = static_cast(found - lease.begin()); + return position <= to && (start ? (*found)->address : (*found)->address + (*found)->bytes) == value ? position : lease.size(); + }; + for (auto size = window; size >= containing.bytes; size /= 2) { + if (size > largest) continue; + const auto blockLow = address / size * size; + const auto blockHigh = blockLow + size; + if (containing.address < blockLow || containing.address + containing.bytes > blockHigh) break; + if (blockLow < lease[first]->address || blockHigh > lease[last]->address + lease[last]->bytes) continue; + const auto blockFirst = bounding(first, index, blockLow, true); + const auto blockLast = bounding(index, last, blockHigh, false); + if (blockFirst == lease.size() || blockLast == lease.size()) continue; + if (blockFirst == blockLast) break; + if (skipped(blockFirst, blockLast)) continue; + return leasedRun(lease, blockFirst, blockLast); + } + return leasedRun(lease, index, index); +} + +ImportRun exactRun(const GuestAllocations::Lease& lease, std::uint64_t base, std::uint64_t bytes) { + const auto first = leasedIndexContaining(lease, base); + if (first == lease.size() || lease[first]->address != base) return {}; + auto last = first; + while (lease[last]->address + lease[last]->bytes < base + bytes) { + if (last + 1 == lease.size() || !continuesRun(*lease[last], *lease[last + 1])) return {}; + ++last; + } + return lease[last]->address + lease[last]->bytes == base + bytes ? leasedRun(lease, first, last) : ImportRun{}; } bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { @@ -784,21 +1360,38 @@ bool wouldRetireImport(const HostImports& state, const ImportRun& run) { return previous.base + previous.bytes > run.base; } -bool sameRange(const HostImport& entry, const GuestAllocations::Lease& lease) { +bool sameRange(const ImportFailure& entry, const GuestAllocations::Lease& lease, std::uint64_t budget = 0) { + const auto current = entry.ranges.size() == 1 ? leasedRangeOwner(lease, entry.base, entry.base + entry.bytes) : nullptr; + if (current != nullptr && entry.bytes < current->bytes) return !entry.ranges.front().owner_before(current) && !current.owner_before(entry.ranges.front()); + return sameRun(entry, importRun(lease, entry.base, std::numeric_limits::max(), budget)); +} + +bool sameRange(const HostImport& entry, const GuestAllocations::Lease& lease, std::uint64_t budget = 0) { if (entry.ranges.size() == 1 && !entry.joined && entry.bytes < entry.ranges.front()->bytes) { const auto current = leasedRangeOwner(lease, entry.base, entry.base + entry.bytes); return current != nullptr && !entry.ranges.front().owner_before(current) && !current.owner_before(entry.ranges.front()); } - return sameRun(entry, entry.joined ? joinedRun(lease, entry.base, entry.base + entry.bytes) : importRun(lease, entry.base)); + return sameRun(entry, entry.joined ? joinedRun(lease, entry.base, entry.base + entry.bytes) : importRun(lease, entry.base, std::numeric_limits::max(), budget)); +} + +std::uint64_t effectiveUse(const HostImports& state, const HostImport& entry) { + if (entry.spaceSerial == 0) return entry.lastUse; + const auto found = state.spaceUses.find(entry.spaceSerial); + return found == state.spaceUses.end() ? entry.lastUse : std::max(entry.lastUse, found->second); } bool pointerHeld(const HostImports& state, const HostImport& entry) { - return entry.lastUse >= state.buildFloor; + return effectiveUse(state, entry) >= state.buildFloor; } +constexpr std::uint64_t ProtectedUses = 256; + bool evictable(const HostImports& state, const HostImport& entry) { - constexpr std::uint64_t protectedUses = 256; - return !pointerHeld(state, entry) && entry.lastUse + protectedUses < state.useCounter; + return !pointerHeld(state, entry) && effectiveUse(state, entry) + ProtectedUses < state.useCounter; +} + +bool evictingRefusalCurrent(const HostImports& state, const EvictingRefusal& refusal, std::uint64_t budget) { + return refusal.epoch == state.epoch && refusal.liveBytes == state.liveBytes && budget <= refusal.budget && refusal.window == importWindow.load(std::memory_order_relaxed) && state.useCounter < refusal.ready && (state.buildFloor <= refusal.floor || state.useCounter < refusal.readyHeld); } std::uint64_t retireLeastRecent(const Context& context, HostImports& state, const GuestAllocations::Lease& lease, std::uint64_t wanted) { @@ -806,10 +1399,10 @@ std::uint64_t retireLeastRecent(const Context& context, HostImports& state, cons while (freed < wanted) { auto oldest = state.imports.end(); for (auto it = state.imports.begin(); it != state.imports.end(); ++it) { - if (evictable(state, it->second) && (oldest == state.imports.end() || it->second.lastUse < oldest->second.lastUse)) oldest = it; + if (evictable(state, it->second) && (oldest == state.imports.end() || effectiveUse(state, it->second) < effectiveUse(state, oldest->second))) oldest = it; } if (oldest == state.imports.end()) break; - freed += oldest->second.bytes; + freed += oldest->second.pinnedBytes; retireImport(context.device, state, oldest, lease); } return freed; @@ -828,24 +1421,73 @@ std::uint64_t physicalMemoryBytes() { } std::atomic budgetOverride{0}; +std::atomic addressCopyLimitOverride{0}; -// Pinned imports count against the driver's system memory heap, which imports and type-3 mirrors share; past it -// ordinary host allocations fail. The budget is APS5_HOST_IMPORT_MIB, else the larger of 6 GiB and half the physical -// memory, at most the largest host heap less 2 GiB. -std::uint64_t importBudget(const Context& context) { +// Pinned imports count against the driver's system memory heap, which imports, type-3 mirrors, buffers and textures +// placed in host memory share; past it ordinary host allocations fail. The budget is APS5_HOST_IMPORT_MIB, else +// HostImportBudget. +std::uint64_t configuredBudget() { if (const auto set = budgetOverride.load(std::memory_order_relaxed); set != 0) return set; static const std::uint64_t configured = []() -> std::uint64_t { const char* value = std::getenv("APS5_HOST_IMPORT_MIB"); return value != nullptr ? std::strtoull(value, nullptr, 10) << 20u : 0u; }(); - if (configured != 0) return configured; - std::uint64_t hostHeap = 0; - for (std::uint32_t index = 0; index < context.memory.memoryHeapCount; ++index) { - if ((context.memory.memoryHeaps[index].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0) hostHeap = std::max(hostHeap, context.memory.memoryHeaps[index].size); + return configured; +} + +std::uint64_t importBudget(const Context& context) { + if (const auto configured = configuredBudget(); configured != 0) return configured; + return HostImportBudget(context.memory, physicalMemoryBytes()); +} + +std::optional hostHeapIndex(const VkPhysicalDeviceMemoryProperties& memory) { + std::optional heap; + for (std::uint32_t index = 0; index < memory.memoryHeapCount; ++index) { + if ((memory.memoryHeaps[index].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0 && (!heap || memory.memoryHeaps[index].size > memory.memoryHeaps[*heap].size)) heap = index; } - const auto fromMemory = std::max(6144ull << 20u, physicalMemoryBytes() / 2); - constexpr std::uint64_t reserve = 2048ull << 20u; - return hostHeap > reserve ? std::min(fromMemory, hostHeap - reserve) : fromMemory; + return heap; +} + +void noteReportedBudget(HostImports& state, std::uint64_t configured, bool set, std::uint64_t reported) { + if (reported >= configured || state.reportedHeapBudget == 0) return; + const std::uint8_t stage = !set && state.allocationFailed ? 2 : 1; + if (state.reportNoted >= stage) return; + state.reportNoted = stage; + const char* outcome = set ? "the setting stays, as the report never lowers it" : stage == 2 ? "imports are held to it since an import's vkAllocateMemory failed" : "the default stays until an import's vkAllocateMemory fails"; + std::fprintf(stderr, "[gpu] host import budget: host heap %u reports a VK_EXT_memory_budget budget of %llu MiB and a usage of %llu MiB with %llu MiB of imports live, which leaves imports %llu MiB of the %llu MiB %s; %s\n", state.reportedHeap, static_cast(state.reportedHeapBudget >> 20u), static_cast(state.reportedHeapUsage >> 20u), static_cast(state.liveBytes >> 20u), static_cast(reported >> 20u), static_cast(configured >> 20u), set ? "APS5_HOST_IMPORT_MIB sets" : "of the default", outcome); +} + +std::uint64_t effectiveBudget(const Context& context, HostImports& state) { + const auto configured = importBudget(context); + const bool set = configuredBudget() != 0; + auto reported = std::numeric_limits::max(); + if (context.memoryProperties2 != nullptr && context.physical != VK_NULL_HANDLE) { + const auto now = std::chrono::steady_clock::now(); + if (state.reportedDevice != context.physical || now - state.reportedAt >= std::chrono::seconds(1)) { + VkPhysicalDeviceMemoryBudgetPropertiesEXT report{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &report}; + context.memoryProperties2(context.physical, &properties); + state.reportedBudget = HostImportBudgetFor(context.memory, &report, std::numeric_limits::max(), state.liveBytes); + const auto heap = hostHeapIndex(context.memory); + state.reportedHeap = heap.value_or(0); + state.reportedHeapBudget = heap ? report.heapBudget[*heap] : 0; + state.reportedHeapUsage = heap ? report.heapUsage[*heap] : 0; + state.reportedDevice = context.physical; + state.reportedAt = now; + } + reported = state.reportedBudget; + } + noteReportedBudget(state, configured, set, reported); + return HostImportBudgetWith(configured, set, reported, state.allocationFailed, state.ceiling); +} + +std::string budgetSource(const Context& context, const HostImports& state, std::uint64_t budget) { + const auto configured = importBudget(context); + const char* source = state.ceiling != 0 && budget == state.ceiling ? "lowered after vkAllocateMemory failed" : budget < configured ? "the host heap's VK_EXT_memory_budget report" : configuredBudget() != 0 ? "APS5_HOST_IMPORT_MIB" : "the default"; + if (state.reportedHeapBudget == 0) return source; + char text[192]; + std::snprintf(text, sizeof(text), "%s; host heap %u reports a budget of %llu MiB and a usage of %llu MiB", source, state.reportedHeap, static_cast(state.reportedHeapBudget >> 20u), static_cast(state.reportedHeapUsage >> 20u)); + return text; } std::atomic rangeImportWindow{64ull << 20u}; @@ -881,27 +1523,70 @@ 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); +const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict, ImportRequester requester); // The bytes retireLeastRecent could free outside [begin, end). -std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std::uint64_t end) { +std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std::uint64_t end, std::uint64_t& ready, std::uint64_t& readyHeld) { std::uint64_t bytes = 0; for (const auto& [base, entry] : state.imports) { - if ((entry.base + entry.bytes <= begin || entry.base >= end) && evictable(state, entry)) bytes += entry.bytes; + if (entry.base + entry.bytes > begin && entry.base < end) continue; + if (evictable(state, entry)) { + bytes += entry.pinnedBytes; + continue; + } + auto& horizon = pointerHeld(state, entry) ? readyHeld : ready; + horizon = std::min(horizon, effectiveUse(state, entry) + ProtectedUses + 1); } return bytes; } -const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end = 0, bool evict = true) { +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, ImportRequester requester, std::uint64_t end = 0, bool evict = true, std::uint64_t largest = std::numeric_limits::max()) { + 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; } else if (!fresh) { - if (const auto* current = findImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) return current; + if (const auto* current = peekImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) { + current->lastUse = ++state.useCounter; + return current; + } } - auto run = importRun(lease, address); + const auto budget = evict ? 0 : effectiveBudget(context, state); + if (const auto index = leasedIndexContaining(lease, address); budget != 0 && index != lease.size() && stillRefused(state, rememberedRun(state.refusals, lease, index, index), budget) && findImport(state, address, address + 1) == nullptr) { + auto& counts = state.counts[static_cast(requester)]; + ++counts.refused; + counts.refusedBytes += lease[index]->bytes; + ++counts.remembered; + return nullptr; + } + auto run = importRun(lease, address, largest, budget); // Without an end the caller views the whole run through the import (AcquireRegistered): a window that holds the // first byte is shorter than that, so only an import of the whole run is a hit; a window is replaced below. if (fresh && end == 0) { @@ -909,58 +1594,95 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s } if (end != 0) run = windowRun(state, std::move(run), address, end, context.hostImportAlignment); if (run.ranges.empty()) return nullptr; - return importRunAt(context, state, std::move(run), false, address, lease, end, evict); + return importRunAt(context, state, std::move(run), false, address, lease, end, evict, requester); } const HostImport* importJoined(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) { - 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; - return importRunAt(context, state, std::move(run), true, begin, lease, end, true); + return importRunAt(context, state, std::move(run), true, begin, lease, end, true, ImportRequester::Join); } -const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict) { +const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict, ImportRequester requester) { + auto& counts = state.counts[static_cast(requester)]; const auto base = run.base; const auto bytes = run.bytes; auto& failures = joined ? state.failedJoins : state.failed; + const bool stepDown = !joined && run.ranges.size() > 1 && importWindow.load(std::memory_order_relaxed) != 0; const auto alignment = context.hostImportAlignment; if (joined && (alignment == 0 || base % alignment != 0 || bytes % alignment != 0)) { failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end())}); return nullptr; } - if (const auto found = state.imports.find(base); found != state.imports.end() && sameRun(found->second, run)) return &found->second; + if (const auto found = state.imports.find(base); found != state.imports.end() && sameRun(found->second, run)) { + found->second.lastUse = ++state.useCounter; + return &found->second; + } if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr; - if (const auto failed = failures.find(base); failed != failures.end()) { + if (const auto failed = failures.find(base); !stepDown && failed != failures.end()) { if (sameRun(failed->second, run)) return nullptr; failures.erase(failed); } - // 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. + const auto budget = effectiveBudget(context, state); + const auto containing = std::find_if(run.ranges.begin(), run.ranges.end(), [address](const auto& range) { return address >= range->address && address < range->address + range->bytes; }); + if (const auto remembered = state.evictingRefusals.find({base, bytes}); evict && remembered != state.evictingRefusals.end()) { + if (remembered->second.joined == joined && evictingRefusalCurrent(state, remembered->second, budget) && sameRun(remembered->second, run)) { + ++counts.refused; + counts.refusedBytes += bytes; + ++counts.remembered; + const auto room = remembered->second.room - std::min(remembered->second.room, remembered->second.budget - budget); + if (stepDown && containing != run.ranges.end() && room >= (*containing)->bytes) return importAllocation(context, state, address, lease, requester, end, evict, std::min(bytes / 2, room)); + return nullptr; + } + state.evictingRefusals.erase(remembered); + } auto first = state.imports.lower_bound(base); if (first != state.imports.begin() && std::prev(first)->second.base + std::prev(first)->second.bytes > base) --first; std::uint64_t replaced = 0; // 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; + if (held(it->second)) return stepDown ? importAllocation(context, state, address, lease, requester, end, evict, bytes / 2) : nullptr; + replaced += it->second.pinnedBytes; } - const auto budget = importBudget(context); const auto live = state.liveBytes - replaced; - if (live + bytes > budget && (!evict || live + bytes - evictableBytes(state, base, base + bytes) > budget)) { + auto ready = std::numeric_limits::max(); + auto readyHeld = std::numeric_limits::max(); + const auto evictable = evict && live + bytes > budget ? evictableBytes(state, base, base + bytes, ready, readyHeld) : 0; + if (live + bytes > budget + evictable) { // A refused import turns every later use of the range into CPU copies, so say so. - static std::uint64_t refused = 0, refusedBytes = 0; static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60); - ++refused; - refusedBytes += bytes; + ++counts.refused; + counts.refusedBytes += bytes; + const auto remember = [&](std::uint64_t at, std::uint64_t size, GuestAllocations::Mapped ranges) { + if (!evict && !joined) state.refusals.insert_or_assign(std::pair{at, size}, ImportRefusal{at, size, std::move(ranges), state.liveBytes, budget}); + }; + remember(base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end())); if (std::chrono::steady_clock::now() - lastReport > std::chrono::seconds(10)) { lastReport = std::chrono::steady_clock::now(); - std::fprintf(stderr, "[gpu] host import budget: %llu MiB live of %llu MiB (APS5_HOST_IMPORT_MIB); %llu imports (%llu MiB) refused so far, last 0x%llx+0x%llx\n", static_cast(state.liveBytes >> 20u), static_cast(budget >> 20u), static_cast(refused), static_cast(refusedBytes >> 20u), static_cast(base), static_cast(bytes)); + std::uint64_t refused = 0; + std::uint64_t refusedBytes = 0; + std::uint64_t remembered = 0; + for (const auto& requested : state.counts) { + refused += requested.refused; + refusedBytes += requested.refusedBytes; + remembered += requested.remembered; + } + std::fprintf(stderr, "[gpu] host import budget: %llu MiB live of %llu MiB (%s); %llu imports (%llu MiB) refused so far, %llu of them remembered, last 0x%llx+0x%llx by %s\n", static_cast(state.liveBytes >> 20u), static_cast(budget >> 20u), budgetSource(context, state, budget).c_str(), static_cast(refused), static_cast(refusedBytes >> 20u), static_cast(remembered), static_cast(base), static_cast(bytes), ImportRequesterName(requester)); } + const auto room = budget + evictable > live ? budget + evictable - live : 0; + if (evict) state.evictingRefusals.insert_or_assign(std::pair{base, bytes}, EvictingRefusal{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end()), state.epoch, state.liveBytes, budget, room, importWindow.load(std::memory_order_relaxed), state.buildFloor, ready, readyHeld, joined}); + if (stepDown && containing != run.ranges.end() && room >= (*containing)->bytes) return importAllocation(context, state, address, lease, requester, end, evict, std::min(bytes / 2, room)); + if (containing != run.ranges.end() && run.ranges.size() > 1) remember((*containing)->address, (*containing)->bytes, GuestAllocations::Mapped{*containing}); return nullptr; } for (auto it = first; it != state.imports.end() && it->first < base + bytes;) { @@ -969,20 +1691,33 @@ 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); + entry.memoryBase = base; + entry.pinnedBytes = bytes; + entry.backingSerial = entry.ranges.size() == 1 ? entry.range->backingSerial : 0; + const auto failedBlockImport = [&]() -> const HostImport* { + ++counts.failed; + state.failedBlocks.insert_or_assign(std::pair{base, bytes}, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); + return importAllocation(context, state, address, lease, requester, end, evict); + }; + const auto failedImport = [&]() -> const HostImport* { + if (stepDown) return failedBlockImport(); + ++counts.failed; + failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); + return nullptr; + }; #ifdef _WIN32 // Drivers pin imported pages, so every page must be committed and accessible. + const GuestArena::HostImportAccess importAccess(reinterpret_cast(base), static_cast(bytes)); bool writable = true; bool readOnly = true; MEMORY_BASIC_INFORMATION refused{}; for (std::uint64_t cursor = base; cursor < base + bytes;) { MEMORY_BASIC_INFORMATION info{}; - if (VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == 0 || info.State != MEM_COMMIT || (info.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) { - failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); - return nullptr; - } + if (VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == 0 || info.State != MEM_COMMIT || (info.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) return failedImport(); const auto protection = info.Protect & 0xffu; const bool pageWritable = (info.Type == MEM_PRIVATE || info.Type == MEM_IMAGE) && (protection == PAGE_READWRITE || protection == PAGE_EXECUTE_READWRITE || protection == PAGE_WRITECOPY || protection == PAGE_EXECUTE_WRITECOPY); const bool pageReadOnly = info.Type != MEM_MAPPED && (protection == PAGE_READONLY || protection == PAGE_EXECUTE_READ); @@ -991,10 +1726,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import if (!pageWritable) writable = false; cursor = reinterpret_cast(info.BaseAddress) + info.RegionSize; } - if (!writable && readOnly) { - failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); - return nullptr; - } + if (!writable && readOnly) return failedImport(); if (!writable) { if (refused.BaseAddress != nullptr) { char text[256]; @@ -1005,25 +1737,19 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import entry.alias = GuestArena::GuestArenaMapAlias_nid_postfix(static_cast(base), static_cast(bytes)); } catch (const std::runtime_error&) { if (entry.ranges.size() == 1) throw; - if (joined) { - failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); - return nullptr; - } - state.unaliasedSpans.insert(base); - return importAllocation(context, state, address, lease, end, evict); + return joined ? failedImport() : failedBlockImport(); } + if (entry.alias == nullptr) return nullptr; } #else - if (!GuestMemory::Accessible(reinterpret_cast(base), static_cast(bytes), true)) { - failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); - return nullptr; - } + if (!GuestMemory::Accessible(reinterpret_cast(base), static_cast(bytes), true)) return failedImport(); #endif decideImportWatch(context, state); VkResult result = VK_SUCCESS; const char* step = nullptr; #ifdef _WIN32 - const bool watchedHostPointer = entry.alias != nullptr || !state.unwatchImports; + const bool settled = entry.alias == nullptr && state.watchPrivateImports && writable && bytes >= settleFloorBytes; + const bool watchedHostPointer = entry.alias != nullptr || !state.unwatchImports || settled; const bool watchedDmaBuf = watchedHostPointer; #else const bool watchedHostPointer = !state.unwatchImports; @@ -1031,6 +1757,13 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import #endif GuestMemory::ImportWatched(base, bytes, [&] { step = watchedDmaBuf == watchedHostPointer ? createImport(context, entry, result) : createHostPointerImport(context, entry, result); +#ifdef _WIN32 + if (step == nullptr && settled && (step = settleImport(context, state, entry, result)) != nullptr) { + destroyImport(context, entry); + entry.buffer = VK_NULL_HANDLE; + entry.memory = VK_NULL_HANDLE; + } +#endif return step == nullptr; }, watchedHostPointer); #ifndef _WIN32 @@ -1040,14 +1773,32 @@ 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); + GuestArena::GuestArenaUnmapAlias_nid_postfix(std::exchange(entry.alias, nullptr)); #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)); + std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx failed at %s (%d); falling back to copies, requested by %s\n", static_cast(base), static_cast(bytes), step, static_cast(result), ImportRequesterName(requester)); + if (std::string_view(step) == "vkAllocateMemory") { + state.allocationFailed = true; + if (++state.allocationFailures >= 3 || state.liveBytes + bytes + budget / 8 >= budget) { + state.ceiling = std::max(state.liveBytes, 1); + std::fprintf(stderr, "[gpu] host import budget lowered to the %.1f MiB live after vkAllocateMemory failed %s\n", state.liveBytes / 1048576.0, state.allocationFailures >= 3 ? "3 times in a row" : "within an eighth of the budget"); + } + } #ifdef _WIN32 static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr; for (std::uint64_t cursor = base; trace && cursor < base + bytes;) { @@ -1070,16 +1821,34 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import std::fclose(maps); } #endif - return nullptr; + return failedImport(); } static std::uint64_t importedBytes = 0; importedBytes += bytes; static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr; state.liveBytes += bytes; - if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)%s\n", static_cast(base), static_cast(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0, joined ? ", joined at bind" : ""); + state.allocationFailures = 0; + ++counts.made; + counts.madeBytes += bytes; + for (auto it = state.refusals.lower_bound({base, 0}); it != state.refusals.end() && it->first.first < base + bytes;) it = it->first.first + it->first.second <= base + bytes ? state.refusals.erase(it) : std::next(it); + if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)%s by %s\n", static_cast(base), static_cast(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0, joined ? ", joined at bind" : "", ImportRequesterName(requester)); entry.joined = joined; entry.lastUse = ++state.useCounter; - 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. @@ -1094,6 +1863,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) { @@ -1107,6 +1879,15 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc state.liveBytes = 0; state.failed.clear(); state.failedJoins.clear(); + state.failedBlocks.clear(); + state.refusals.clear(); + state.evictingRefusals.clear(); + state.ceiling = 0; + state.allocationFailures = 0; + state.allocationFailed = false; + state.reportNoted = 0; + state.sparseRunEnds.clear(); + state.sparseRunJoins.clear(); state.device = context.device; state.destroyBuffer = context.Function("vkDestroyBuffer"); state.freeMemory = context.Function("vkFreeMemory"); @@ -1118,8 +1899,15 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); if (generation == state.refreshedGeneration) return; state.refreshedGeneration = generation; + const auto starts = [&](std::uint64_t address) { + const auto index = leasedIndexContaining(lease, address); + return index != lease.size() && lease[index]->address == address; + }; + std::erase_if(state.sparseRunEnds, [&](const auto& entry) { return !starts(entry.first); }); + std::erase_if(state.sparseRunJoins, [&](const auto& entry) { return !starts(entry.first); }); + const auto budget = effectiveBudget(context, state); for (auto it = state.imports.begin(); it != state.imports.end();) { - if (sameRange(it->second, lease)) { + if (sameRange(it->second, lease, budget)) { if (it->second.unwatched) GuestMemory::Unwatch(it->first, it->second.bytes); ++it; continue; @@ -1130,23 +1918,30 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc retireImport(context.device, state, it, lease); it = next; } - for (auto it = state.failed.begin(); it != state.failed.end();) it = sameRun(it->second, importRun(lease, it->first)) ? std::next(it) : state.failed.erase(it); + for (auto it = state.failed.begin(); it != state.failed.end();) it = sameRange(it->second, lease, budget) ? std::next(it) : state.failed.erase(it); for (auto it = state.failedJoins.begin(); it != state.failedJoins.end();) it = sameRun(it->second, joinedRun(lease, it->second.base, it->second.base + it->second.bytes)) ? std::next(it) : state.failedJoins.erase(it); + for (auto it = state.failedBlocks.begin(); it != state.failedBlocks.end();) it = sameRun(it->second, exactRun(lease, it->first.first, it->first.second)) ? std::next(it) : state.failedBlocks.erase(it); + for (auto it = state.refusals.begin(); it != state.refusals.end();) it = stillRefused(state, &it->second, budget) && sameRun(it->second, exactRun(lease, it->first.first, it->first.second)) ? std::next(it) : state.refusals.erase(it); + for (auto it = state.evictingRefusals.begin(); it != state.evictingRefusals.end();) it = evictingRefusalCurrent(state, it->second, budget) && sameRun(it->second, exactRun(lease, it->first.first, it->first.second)) ? std::next(it) : state.evictingRefusals.erase(it); } -const HostImport* 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* importAccess(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) { +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, ImportRequester requester) { if (!runCovers(lease, begin, end)) return findImport(state, begin, end); - const auto* entry = importAllocation(context, state, begin, lease, end); + const auto* entry = importAllocation(context, state, begin, lease, requester, end); return entry != nullptr && findImport(state, begin, end) == entry ? entry : nullptr; } @@ -1225,6 +2020,64 @@ ImageMirrors& Mirrors() { return mirrors; } +struct SnapshotCopies { + using Key = std::tuple; + static constexpr std::size_t Budget = std::size_t{16} << 20u; + static constexpr std::size_t MaxEntries = 1024; + std::mutex mutex; + VkDevice device = VK_NULL_HANDLE; + struct Entry { + std::shared_ptr buffer; + std::vector bytes; + std::list::iterator recent; + }; + std::map entries; + std::list recency; + std::size_t bytes = 0; + void clear() { + entries.clear(); + recency.clear(); + bytes = 0; + } +}; + +SnapshotCopies& SnapshotCopyCache() { + static SnapshotCopies copies; + return copies; +} + +std::shared_ptr snapshotCopy(const Context& context, std::uint64_t address, std::span bytes, VkBufferUsageFlags usage) { + auto& copies = SnapshotCopyCache(); + std::lock_guard lock(copies.mutex); + if (copies.device != context.device) { + copies.clear(); + copies.device = context.device; + } + const SnapshotCopies::Key key{address, bytes.size(), usage}; + if (const auto found = copies.entries.find(key); found != copies.entries.end()) { + if (std::memcmp(found->second.bytes.data(), bytes.data(), bytes.size()) == 0) { + copies.recency.splice(copies.recency.end(), copies.recency, found->second.recent); + return found->second.buffer; + } + copies.recency.erase(found->second.recent); + copies.bytes -= bytes.size(); + copies.entries.erase(found); + } + auto buffer = std::make_shared(context, bytes.size(), usage); + std::memcpy(buffer->Bytes().data(), bytes.data(), bytes.size()); + if (bytes.size() > SnapshotCopies::Budget) return buffer; + while (!copies.recency.empty() && (copies.bytes + bytes.size() > SnapshotCopies::Budget || copies.entries.size() >= SnapshotCopies::MaxEntries)) { + const auto oldest = copies.entries.find(copies.recency.front()); + copies.bytes -= std::get<1>(oldest->first); + copies.entries.erase(oldest); + copies.recency.pop_front(); + } + copies.recency.push_back(key); + copies.entries.emplace(key, SnapshotCopies::Entry{buffer, {bytes.begin(), bytes.end()}, std::prev(copies.recency.end())}); + copies.bytes += bytes.size(); + return buffer; +} + bool mirrorsEnabled() { static const bool disabled = std::getenv("APS5_NO_LEASE_MIRROR") != nullptr; return !disabled; @@ -1467,6 +2320,24 @@ void appendChangedBlocks(std::span mirrors, std::uint64_t ge } } +void markUnwatched(ImageMirror& mirror) { + ++Mirrors().heapUnwatched; + std::fill(mirror.generations.begin(), mirror.generations.end(), 0); +} + +void appendHeapRun(std::span mirrors, std::uint64_t generation, std::vector& blocks) { + if (generation != 0) { + appendChangedBlocks(mirrors, generation, blocks); + return; + } + for (std::size_t index = 0; index < mirrors.size(); ++index) { + auto& mirror = *mirrors[index]; + const auto own = GuestMemory::CollectWrites(mirror.base, static_cast(mirror.bytes)); + if (own == 0) markUnwatched(mirror); + appendChangedBlocks(mirrors.subspan(index, 1), own, blocks); + } +} + void refreshHeapMirrors(std::vector& mirrors, std::vector& blocks) { std::sort(mirrors.begin(), mirrors.end(), [](const ImageMirror* left, const ImageMirror* right) { return left->base < right->base; }); for (std::size_t first = 0; first < mirrors.size();) { @@ -1476,8 +2347,7 @@ void refreshHeapMirrors(std::vector& mirrors, std::vectorbase + mirrors[last - 1]->bytes - begin; prepareRange(begin, bytes); const auto generation = GuestMemory::CollectWrites(begin, static_cast(bytes)); - Require(generation != 0, "a heap mirror's range is no longer write-watched"); - appendChangedBlocks(std::span(mirrors).subspan(first, last - first), generation, blocks); + appendHeapRun(std::span(mirrors).subspan(first, last - first), generation, blocks); first = last; } } @@ -1487,6 +2357,8 @@ bool sparseMirrorsEnabled() { return !disabled; } +constexpr std::uint32_t MaxSparseRunJoins = 64; + using Pieces = std::vector>; std::uint64_t sparseExtent(std::uint64_t base, std::uint64_t reserved, const Pieces& pieces) { @@ -1576,7 +2448,7 @@ bool refreshSparseMirror(ImageMirror& mirror, std::vector& blocks) if (!pieces.empty() && pieces.back().second > mirror.base + mirror.bytes) return false; syncRange(mirror.base, mirror.bytes); const auto generation = collectPieces(pieces); - Require(generation != 0, "a sparse heap mirror's range is no longer write-watched"); + if (generation == 0) markUnwatched(mirror); appendSparseBlocks(mirror, std::move(pieces), generation, blocks); return true; } @@ -1611,14 +2483,25 @@ std::vector> refreshHeapRuns(const GuestBu const auto& run = space.heapRuns[index]; prepareRange(run.begin, run.bytes); const auto generation = GuestMemory::CollectWrites(run.begin, static_cast(run.bytes)); - Require(generation != 0, "a heap mirror's range is no longer write-watched"); const auto since = space.verified[index].load(std::memory_order_relaxed); - if (since == 0 || !GuestMemory::UnchangedSince(run.begin, static_cast(run.bytes), since)) appendChangedBlocks(std::span(space.heapMirrors).subspan(run.first, run.last - run.first), generation, blocks); + if (since == 0 || !GuestMemory::UnchangedSince(run.begin, static_cast(run.bytes), since)) appendHeapRun(std::span(space.heapMirrors).subspan(run.first, run.last - run.first), generation, blocks); checked.emplace_back(index, generation); } return checked; } +bool heapMirrorWatched(ImageMirror& mirror) { + const auto serial = GuestMemory::UnwatchSerial(); + if (mirror.watchedSerial.load(std::memory_order_relaxed) == serial) return true; + if (!GuestMemory::Watched(mirror.base, static_cast(mirror.bytes))) return false; + mirror.watchedSerial.store(serial, std::memory_order_relaxed); + return true; +} + +bool importsNow(const Context& context, HostImports& state, const GuestAllocations::Lease& lease, std::uint64_t address) { + return context.hostImportAlignment != 0 && (importsStale(context, state) || importAllocation(context, state, address, lease, ImportRequester::Build, 0, false) != nullptr); +} + // The mirror for a leased image range: the existing one, or a new one when none exists or the // registered Range object changed (a protection change). Null when the range cannot be mirrored. An // existing writable mirror's blocks are appended to `blocks` for the caller's one compare of every @@ -1662,6 +2545,7 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh mirror->range = range; if (heap) { std::uint64_t generation = 0; + const auto serial = GuestMemory::UnwatchSerial(); if (sparse) { syncRange(mirror->base, mirror->bytes); if (GuestMemory::Watched(mirror->base, static_cast(mirror->bytes))) generation = collectPieces(pieces); @@ -1672,10 +2556,18 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh constexpr std::uint64_t block = 65536; mirror->generations.assign(static_cast(((mirror->base + mirror->bytes + block - 1) / block) - mirror->base / block), generation); if (generation == 0) { + std::shared_ptr stale; std::lock_guard lock(state.mutex); ++state.heapUnwatched; + if (const auto found = state.entries.find(range->address); found != state.entries.end() && found->second->heap) { + state.heapBytes -= found->second->bytes; + stale = std::move(found->second); + state.entries.erase(found); + ++state.version; + } return nullptr; } + mirror->watchedSerial.store(serial, std::memory_order_relaxed); if (heapMirrorsBeside(*range) + mirror->bytes > heapMirrorBudget()) { sweepMirrors(); if (const auto held = heapMirrorsBeside(*range); held + mirror->bytes > heapMirrorBudget()) throw std::runtime_error(heapMirrorError(*range, held, "past the budget")); @@ -1796,6 +2688,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 kept compared under a storage image, %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.settlesKeptCompared), 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 @@ -1834,6 +2731,7 @@ LeaseState& Leases() { struct SnapshotStats { std::atomic checked{0}; std::atomic skipped{0}; + std::atomic mismatched{0}; }; SnapshotStats& Snapshots() { @@ -1841,11 +2739,12 @@ 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; bool retiredImport = false; + bool droppedAddressSpace = false; // A range pinned only by the cached address space is released by dropping the cache's // reference: no GPU wait, no device lock (the mutating thread may be a driver thread holding // it). The registry rescans; a range still pinned by a build or a batch holding the space comes @@ -1854,26 +2753,104 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) noexcept Spaces().droppedByWaiter.store(true, std::memory_order_relaxed); Spaces().waiterDrops.fetch_add(1, std::memory_order_relaxed); dropped.reset(); - auto& state = Leases(); - std::lock_guard lock(state.mutex); - ++state.stats.contentionWaits; - ++state.stats.cacheDrops; - state.stats.contentionMs += std::chrono::duration(std::chrono::steady_clock::now() - start).count(); - return true; + droppedAddressSpace = true; + auto& imports = Imports(); + const auto end = static_cast(address) + bytes; + bool overlapsImport = false; + { + std::lock_guard importsLock(imports.mutex); + const auto next = imports.imports.lower_bound(address); + const auto previous = next == imports.imports.begin() ? imports.imports.end() : std::prev(next); + overlapsImport = (next != imports.imports.end() && next->first < end) || (previous != imports.imports.end() && previous->first + previous->second.bytes > address); + } + if (!overlapsImport) { + auto& state = Leases(); + std::lock_guard lock(state.mutex); + ++state.stats.contentionWaits; + ++state.stats.cacheDrops; + 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); @@ -1895,84 +2872,15 @@ 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); ++state.stats.contentionWaits; + if (droppedAddressSpace) ++state.stats.cacheDrops; if (synced) ++state.stats.contentionSyncs; if (drained) ++state.stats.contentionDrains; state.stats.contentionMs += std::chrono::duration(std::chrono::steady_clock::now() - start).count(); @@ -2023,6 +2931,17 @@ void CountLeaseOutcome(bool synced, std::uint64_t batchSerial) { AgcDriver::ProfilePrint_nid_no_patch("[address-sync] leases: %llu released at completion, %llu synced at once; %llu pin-contention waits by guest threads (%llu finished the lease batch, %llu drained the recorder, %llu cache-only drops) %.1f s; BDA table cache %llu hits / %llu misses (%zu tables held); snapshot compares: %llu skipped (live-backed region), %llu made\n", static_cast(stats.deferred), static_cast(stats.synced), static_cast(stats.contentionWaits), static_cast(stats.contentionSyncs), static_cast(stats.contentionDrains), static_cast(stats.cacheDrops), stats.contentionMs / 1000, static_cast(table.hits), static_cast(table.misses), table.held, static_cast(snapshots.skipped.load(std::memory_order_relaxed)), static_cast(snapshots.checked.load(std::memory_order_relaxed))); } +void FinishLeaseWork() { + GuestMemory::AssertGpuLockHeld("FinishLeaseWork"); + const auto& state = Leases(); + auto* recorder = Recorder::Active(); + const auto target = state.newestLeaseSerial; + if (recorder == nullptr || recorder != state.leaseRecorder || target <= recorder->CompletedThrough()) return; + if (target >= recorder->OpenSerial()) recorder->Submit(); + Recorder::AnnounceSyncSite(__builtin_return_address(0)); + recorder->FinishUpTo(target); +} + LeaseStats LeaseCounters() { auto& state = Leases(); std::lock_guard lock(state.mutex); @@ -2059,6 +2978,27 @@ MirrorStats MirrorCounters() { return stats; } +std::uint64_t SnapshotMismatches() { + return Snapshots().mismatched.load(std::memory_order_relaxed); +} + +std::vector AddressTableKey(std::span> writes, std::span own) { + std::vector key; + key.reserve(writes.size() * 2u + 1u + own.size() * 4u); + for (const auto& [begin, end] : writes) key.insert(key.end(), {begin, end}); + key.push_back(std::numeric_limits::max()); + for (const auto& range : own) key.insert(key.end(), {range.begin, range.end, range.deviceAddress, (static_cast(range.reserved) << 32u) | range.permissions}); + return key; +} + +void ClearSnapshotCopies(VkDevice device) { + auto& copies = SnapshotCopyCache(); + std::lock_guard lock(copies.mutex); + if (copies.device != device) return; + copies.clear(); + copies.device = VK_NULL_HANDLE; +} + void ClearImageMirrors(VkDevice device) { auto space = Spaces().current.load(); if (space != nullptr && space->device == device) { @@ -2080,6 +3020,20 @@ void ClearHostImports(VkDevice device) { auto& state = Imports(); std::lock_guard lock(state.mutex); state.parked.erase(device); +#ifdef _WIN32 + 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); @@ -2092,26 +3046,27 @@ void ClearHostImports(VkDevice device) { state.liveBytes = 0; state.failed.clear(); state.failedJoins.clear(); + state.failedBlocks.clear(); + state.refusals.clear(); + state.evictingRefusals.clear(); + state.ceiling = 0; + state.allocationFailures = 0; + state.allocationFailed = false; + state.reportNoted = 0; state.device = VK_NULL_HANDLE; state.refreshedGeneration = 0; ++state.epoch; } -#ifndef _WIN32 namespace { -void runImportProbe(const Context& context, std::uint64_t base, std::uint64_t bytes, const std::function& importStep, ImportProbe& probe) { - GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(reinterpret_cast(base), bytes); +void runImportProbe(const Context& context, std::uint64_t base, std::uint64_t bytes, const std::function& importStep, const std::function& collect, ImportProbe& probe, const std::function& settleStep = {}) { HostImport import{base, bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0}; VkBuffer destination = VK_NULL_HANDLE; VkDeviceMemory destinationMemory = VK_NULL_HANDLE; VkCommandBuffer commands[2]{}; VkFence fence = VK_NULL_HANDLE; bool submitted = false; - const auto collect = [&](std::uint32_t& pages) { - pages = 0; - return GuestWriteWatch::GuestWriteWatchCollect_nid_postfix(static_cast(base), static_cast(bytes), [](void* count, std::uintptr_t begin, std::uintptr_t end) { *static_cast(count) += static_cast((end - begin) / 4096); }, &pages); - }; const auto run = [&]() -> const char* { std::uint32_t quiet = 0; if (!collect(quiet)) return "the first collect"; @@ -2119,6 +3074,11 @@ void runImportProbe(const Context& context, std::uint64_t base, std::uint64_t by if (quiet != 0) return "an unwritten scratch range"; if (const char* step = importStep(import, probe.result)) return step; if (!collect(probe.writtenAtImport)) return "the collect after the import"; + if (settleStep) { + if (const char* step = settleStep(import, probe.result)) return step; + if (!collect(probe.writtenDuringSettle)) return "the collect after the settle"; + if (!collect(probe.writtenAfterSettle)) return "the collect after a settled import"; + } VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; info.size = bytes; info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; @@ -2138,11 +3098,11 @@ void runImportProbe(const Context& context, std::uint64_t base, std::uint64_t by allocate.commandBufferCount = 2; if ((probe.result = context.Function("vkAllocateCommandBuffers")(context.device, &allocate, commands)) != VK_SUCCESS) return "vkAllocateCommandBuffers"; VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; - begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; if ((probe.result = context.Function("vkBeginCommandBuffer")(commands[0], &begin)) != VK_SUCCESS) return "vkBeginCommandBuffer"; const VkBufferCopy region{0, 0, bytes}; context.Function("vkCmdCopyBuffer")(commands[0], import.buffer, destination, 1, ®ion); if ((probe.result = context.Function("vkEndCommandBuffer")(commands[0])) != VK_SUCCESS) return "vkEndCommandBuffer"; + begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; if ((probe.result = context.Function("vkBeginCommandBuffer")(commands[1], &begin)) != VK_SUCCESS) return "vkBeginCommandBuffer"; context.Function("vkCmdFillBuffer")(commands[1], import.buffer, 0, bytes, 0x02020202u); if ((probe.result = context.Function("vkEndCommandBuffer")(commands[1])) != VK_SUCCESS) return "vkEndCommandBuffer"; @@ -2161,6 +3121,8 @@ void runImportProbe(const Context& context, std::uint64_t base, std::uint64_t by }; if (const char* step = execute(commands[0])) return step; if (!collect(probe.writtenAfterSubmit)) return "the collect after the submission"; + if (const char* step = execute(commands[0])) return step; + if (!collect(probe.writtenAfterSecondSubmit)) return "the collect after the second submission"; if (const char* step = execute(commands[1])) return step; if (!collect(probe.writtenAfterFill)) return "the collect after the fill"; *reinterpret_cast(base) = 2; @@ -2174,17 +3136,49 @@ void runImportProbe(const Context& context, std::uint64_t base, std::uint64_t by if (destination != VK_NULL_HANDLE) context.Function("vkDestroyBuffer")(context.device, destination, nullptr); if (destinationMemory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, destinationMemory, nullptr); if (import.buffer != VK_NULL_HANDLE) destroyImport(context, import); - GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast(base), bytes); } +#ifndef _WIN32 +bool collectWriteWatch(std::uint64_t base, std::uint64_t bytes, std::uint32_t& pages) { + pages = 0; + return GuestWriteWatch::GuestWriteWatchCollect_nid_postfix(static_cast(base), static_cast(bytes), [](void* count, std::uintptr_t begin, std::uintptr_t end) { *static_cast(count) += static_cast((end - begin) / 4096); }, &pages); } #endif +} + ImportProbe ProbeImportWriteProtection(const Context& context) { ImportProbe probe; #ifdef _WIN32 - static_cast(context); - probe.failure = "the Linux write watch"; + if (context.hostImportAlignment == 0) { + probe.failure = "host import support"; + return probe; + } + constexpr std::uint64_t page = 4096; + const std::uint64_t alignment = std::max(context.hostImportAlignment, page); + const std::uint64_t bytes = (4096 * page + alignment - 1) / alignment * alignment; + probe.pages = static_cast(bytes / page); + void* raw = VirtualAlloc(nullptr, static_cast(bytes + alignment), MEM_RESERVE | MEM_COMMIT | MEM_WRITE_WATCH, PAGE_READWRITE); + if (raw == nullptr) { + probe.failure = "VirtualAlloc"; + return probe; + } + const auto base = (reinterpret_cast(raw) + alignment - 1) / alignment * alignment; + auto* scratch = reinterpret_cast(base); + for (std::uint64_t offset = 0; offset < bytes; offset += page) scratch[offset] = 1; + std::vector written(static_cast(bytes / page)); + const auto collect = [&](std::uint32_t& pages) { + ULONG_PTR count = written.size(); + DWORD granularity = 0; + if (GetWriteWatch(WRITE_WATCH_FLAG_RESET, reinterpret_cast(base), static_cast(bytes), written.data(), &count, &granularity) != 0) return false; + pages = static_cast(count); + return true; + }; + std::uint32_t touched = 0; + if (!collect(touched)) probe.failure = "GetWriteWatch"; + else if (context.settleQueue == VK_NULL_HANDLE || context.settleQueueMutex == nullptr) probe.failure = "the settle queue"; + else runImportProbe(context, base, bytes, [&](HostImport& import, VkResult& result) { return createImport(context, import, result); }, collect, probe, [&](HostImport& import, VkResult& result) { return settleImport(context, Imports(), import, result); }); + VirtualFree(raw, 0, MEM_RELEASE); return probe; #else if (context.hostImportAlignment == 0) { @@ -2207,7 +3201,9 @@ ImportProbe ProbeImportWriteProtection(const Context& context) { const auto base = (reinterpret_cast(raw) + alignment - 1) / alignment * alignment; auto* scratch = reinterpret_cast(base); for (std::uint64_t offset = 0; offset < bytes; offset += page) scratch[offset] = 1; - runImportProbe(context, base, bytes, [&](HostImport& import, VkResult& result) { return createImport(context, import, result); }, probe); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(reinterpret_cast(base), bytes); + runImportProbe(context, base, bytes, [&](HostImport& import, VkResult& result) { return createImport(context, import, result); }, [&](std::uint32_t& pages) { return collectWriteWatch(base, bytes, pages); }, probe); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast(base), bytes); munmap(raw, bytes + alignment); return probe; #endif @@ -2247,6 +3243,7 @@ ImportProbe ProbeDmaBufImportWriteProtection(const Context& context) { const auto base = reinterpret_cast(mapped); auto* scratch = reinterpret_cast(base); for (std::uint64_t offset = 0; offset < bytes; offset += page) scratch[offset] = 1; + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(reinterpret_cast(base), bytes); runImportProbe(context, base, bytes, [&](HostImport& import, VkResult& result) -> const char* { const int copy = dup(file); if (copy < 0) { @@ -2254,7 +3251,8 @@ ImportProbe ProbeDmaBufImportWriteProtection(const Context& context) { return "dup"; } return createDmaBufImport(context, import, copy, 0, result); - }, probe); + }, [&](std::uint32_t& pages) { return collectWriteWatch(base, bytes, pages); }, probe); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast(base), bytes); munmap(mapped, bytes); close(file); return probe; @@ -2286,6 +3284,49 @@ void SetHostImportRangeWindow(std::uint64_t bytes) { void SetHostImportBudget(std::uint64_t bytes) { budgetOverride.store(bytes, std::memory_order_relaxed); + auto& state = Imports(); + std::lock_guard lock(state.mutex); + state.ceiling = 0; + state.allocationFailures = 0; + state.allocationFailed = false; + state.reportNoted = 0; +} + +std::uint64_t HostImportBudgetFor(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t configured, std::uint64_t liveBytes) { + const auto heap = hostHeapIndex(memory); + if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return configured; + const auto granted = reported->heapBudget[*heap]; + const auto others = reported->heapUsage[*heap] - std::min(reported->heapUsage[*heap], liveBytes); + const auto held = others + granted / 8u; + constexpr std::uint64_t step = 64ull << 20u; + return std::min(configured, granted > held ? (granted - held) / step * step : 0u); +} + +std::uint64_t HostImportBudgetWith(std::uint64_t configured, bool set, std::uint64_t reported, bool allocationFailed, std::uint64_t ceiling) { + const auto budget = set || !allocationFailed ? configured : std::max(std::min(configured, reported), 1); + return ceiling != 0 ? std::min(budget, ceiling) : budget; +} + +void SetAddressCopyLimit(std::uint64_t bytes) { + addressCopyLimitOverride.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 HostImportBudget(const VkPhysicalDeviceMemoryProperties& memory, const std::uint64_t physicalBytes) { + std::uint64_t hostHeap = 0; + for (std::uint32_t index = 0; index < std::min(memory.memoryHeapCount, VK_MAX_MEMORY_HEAPS); ++index) { + if ((memory.memoryHeaps[index].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0) hostHeap = std::max(hostHeap, memory.memoryHeaps[index].size); + } + const auto fromMemory = std::max(6144ull << 20u, physicalBytes / 2); + const auto reserve = std::max(2048ull << 20u, hostHeap / 2); + return hostHeap > reserve ? std::min(fromMemory, hostHeap - reserve) : fromMemory; } std::uint64_t HostImportLiveBytes() { @@ -2294,11 +3335,78 @@ std::uint64_t HostImportLiveBytes() { return state.liveBytes; } -const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) { +std::uint64_t HostImportBudgetRemaining(const Context& context) { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + const auto budget = effectiveBudget(context, state); + return budget > state.liveBytes ? budget - state.liveBytes : 0; +} + +const char* ImportRequesterName(ImportRequester requester) { + switch (requester) { + case ImportRequester::Map: return "map"; + case ImportRequester::Build: return "build"; + case ImportRequester::Access: return "access"; + case ImportRequester::Join: return "join"; + case ImportRequester::Segment: return "segment"; + } + return "unknown"; +} + +HostImportCounts HostImportCounters() { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + return {state.imports.size(), state.liveBytes, state.counts}; +} + +std::string FormatHostImportCounts(const HostImportCounts& counts) { + HostImportRequesterCounts total; + std::string requesters; + for (std::size_t index = 0; index < counts.requesters.size(); ++index) { + const auto& requested = counts.requesters[index]; + total.made += requested.made; + total.madeBytes += requested.madeBytes; + total.refused += requested.refused; + total.refusedBytes += requested.refusedBytes; + total.failed += requested.failed; + total.remembered += requested.remembered; + char part[96]; + std::snprintf(part, sizeof(part), "%s%s %llu/%llu/%llu", index == 0 ? "" : ", ", ImportRequesterName(static_cast(index)), static_cast(requested.made), static_cast(requested.refused), static_cast(requested.failed)); + requesters += part; + } + char text[256]; + std::snprintf(text, sizeof(text), "%llu live (%.1f MiB); made %llu (%.1f MiB), refused %llu (%.1f MiB, %llu remembered), failed %llu; by requester (made/refused/failed): ", static_cast(counts.live), counts.liveBytes / 1048576.0, static_cast(total.made), total.madeBytes / 1048576.0, static_cast(total.refused), total.refusedBytes / 1048576.0, static_cast(total.remembered), static_cast(total.failed)); + return text + requesters; +} + +void ReportHostImports() { + const auto counts = HostImportCounters(); + if (std::all_of(counts.requesters.begin(), counts.requesters.end(), [](const HostImportRequesterCounts& requested) { return requested.made == 0 && requested.refused == 0 && requested.failed == 0; })) return; + std::fprintf(stderr, "[gpu] host imports at shutdown: %s\n", FormatHostImportCounts(counts).c_str()); +} + +HostImportSettleCounts HostImportSettles() { +#ifdef _WIN32 + auto& state = Imports(); + std::lock_guard lock(state.mutex); + return {state.settlesSubmitted, state.settlesWaited, state.settlesRewatched, state.settlesParked, state.settleUnmapWaits, state.settlesKeptCompared}; +#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, ImportRequester requester) { 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)) { @@ -2308,7 +3416,15 @@ const HostImport* HostImportFor(const Context& context, std::uint64_t address, s } const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); refreshImports(context, state, lease); - return importAccess(context, state, address, address + bytes, lease); + return importAccess(context, state, address, address + bytes, lease, requester); +} + +const HostImport* ExistingHostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) { + 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) { @@ -2331,11 +3447,11 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& if (range == nullptr || leasedRangeOwner(current, range->address) != range) continue; try { if (!GuestMemory::GpuMutex().HeldByThisThread()) { - const auto run = importRun(current, range->address); + const auto run = importRun(current, range->address, std::numeric_limits::max(), effectiveBudget(context, state)); const std::array, 1> span{{{run.base, run.base + run.bytes}}}; if (wouldRetireImport(state, run) || StorageTexture::AnyPendingOverlaps(span)) return false; } - if (importAllocation(context, state, range->address, current, 0, false) == nullptr && GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation) state.failed.erase(importRun(current, range->address).base); + importAllocation(context, state, range->address, current, ImportRequester::Map, 0, false); } catch (const std::exception& error) { std::fprintf(stderr, "[gpu] import of GPU memory mapped at 0x%llx+0x%zx failed: %s\n", static_cast(range->address), range->bytes, error.what()); } @@ -2343,10 +3459,18 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& return true; } -bool RegisteredReadableCovers(std::uint64_t address, std::size_t bytes) { +bool RegisteredReadableCovers(std::uint64_t address, std::size_t bytes, std::vector* pieces) { + if (pieces != nullptr) pieces->clear(); if (bytes == 0 || bytes > std::numeric_limits::max() - address) return false; const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); - return containingRange(lease, address, address + bytes) != nullptr; + const auto end = address + bytes; + for (auto at = address; at < end;) { + const auto* range = containingRange(lease, at, at + 1); + if (range == nullptr) return false; + if (pieces != nullptr) pieces->push_back({range->address, range->bytes, range->writable, range->gpu}); + at = range->address + range->bytes; + } + return true; } std::uint64_t RegisteredReadableEnd(std::uint64_t address) { @@ -2362,7 +3486,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) {} @@ -2430,16 +3554,33 @@ void GuestBufferMemory::AcquireRegistered() { { auto& state = Imports(); std::lock_guard lock(state.mutex); + const auto keeps = [&](ImageMirror* mirror) { return heapMirrorWatched(*mirror) || !importsNow(context, state, current->lease, mirror->base); }; if (current == nullptr) (spaces.droppedByWaiter.exchange(false, std::memory_order_relaxed) ? spaces.rebuiltWaiterDrop : spaces.rebuiltFirst).fetch_add(1, std::memory_order_relaxed); else if (current->device != context.device) spaces.rebuiltDevice.fetch_add(1, std::memory_order_relaxed); else if (current->generation != generation) spaces.rebuiltGeneration.fetch_add(1, std::memory_order_relaxed); else if (current->importsEpoch != state.epoch) spaces.rebuiltEpoch.fetch_add(1, std::memory_order_relaxed); - else hit = true; + else hit = std::all_of(current->heapMirrors.begin(), current->heapMirrors.end(), keeps) && std::all_of(current->sparseMirrors.begin(), current->sparseMirrors.end(), keeps); importFloor = state.useCounter + 1; if (hit) { const auto stamp = ++state.useCounter; - for (const auto& region : current->base) { - if (region.direct != nullptr) region.direct->lastUse = stamp; + static const bool stampOnce = std::getenv("APS5_NO_SPACE_STAMP_ONCE") == nullptr; + auto& spaceUse = state.spaceUses[current->serial]; + if (!stampOnce || spaceUse == 0) { + for (const auto& region : current->base) { + if (region.direct == nullptr) continue; + region.direct->lastUse = stamp; + if (stampOnce) region.direct->spaceSerial = current->serial; + } + } + spaceUse = stamp; + if (state.spaceUses.size() > 64) { + const auto oldest = std::min_element(state.spaceUses.begin(), state.spaceUses.end(), [](const auto& left, const auto& right) { return left.second < right.second; }); + for (auto& [base, entry] : state.imports) { + if (entry.spaceSerial != oldest->first) continue; + entry.lastUse = std::max(entry.lastUse, oldest->second); + entry.spaceSerial = 0; + } + state.spaceUses.erase(oldest); } } } @@ -2495,15 +3636,25 @@ void GuestBufferMemory::AcquireRegistered() { regions.reserve(lease.size()); std::vector copies; std::vector imported; + const HostImport* covering = nullptr; + std::uint64_t coveredBegin = 0; + std::uint64_t coveredEnd = 0; for (const auto& range : lease) { if (!range->readable || !range->gpu) continue; validate(range->address, range->bytes); Region region{range->address, range->address + range->bytes, range->writable, {}, nullptr}; const HostImport* entry = nullptr; - if (importable) { + if (importable && covering != nullptr && range->address >= coveredBegin && range->address + range->bytes <= coveredEnd) { + entry = covering; + } else if (importable) { auto& state = Imports(); std::lock_guard lock(state.mutex); - entry = importAllocation(context, state, range->address, lease, 0, false); + entry = importAllocation(context, state, range->address, lease, ImportRequester::Build, 0, false); + covering = entry; + if (entry != nullptr) { + coveredBegin = entry->base; + coveredEnd = entry->base + entry->bytes; + } } lap(timing.importsUs, at); if (entry != nullptr) { @@ -2545,7 +3696,8 @@ void GuestBufferMemory::AcquireRegistered() { const char* value = std::getenv("APS5_ADDRESS_COPY_MAX_MIB"); return (value ? std::strtoull(value, nullptr, 10) : 64ull) << 20u; }(); - if (const auto overflow = AddressCopyOverflow(std::move(copies), copyLimit); !overflow.empty()) { + const auto limit = addressCopyLimitOverride.load(std::memory_order_relaxed); + if (const auto overflow = AddressCopyOverflow(std::move(copies), limit != 0 ? limit : copyLimit); !overflow.empty()) { std::fprintf(stderr, "FATAL: %s\n", overflow.c_str()); std::fflush(stderr); std::abort(); @@ -2604,6 +3756,13 @@ void GuestBufferMemory::AcquireRegistered() { regions = std::move(extras); lease.clear(); planSpaceMirrors(*built); + { + auto reads = std::make_shared(); + for (const auto& region : built->base) { + if (region.direct != nullptr) reads->emplace_back(region.begin, region.end); + } + if (Recorder::ValidReadList(*reads)) built->directReads = std::move(reads); + } space = std::move(built); spaces.droppedByWaiter.store(false, std::memory_order_relaxed); spaces.current.store(space); @@ -2670,9 +3829,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())); @@ -2689,15 +3851,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; } @@ -2859,7 +4028,24 @@ void GuestBufferMemory::AddSnapshot(const GuestMemorySnapshot& snapshot) { } const auto offset = static_cast(snapshot.address - owner->begin); const auto* source = live ? reinterpret_cast(snapshot.address) : owner->snapshot.data() + offset; - Require(std::memcmp(source, snapshot.bytes.data(), snapshot.bytes.size()) == 0, "guest snapshot differs from registered memory"); + if (std::memcmp(source, snapshot.bytes.data(), snapshot.bytes.size()) != 0) { + std::size_t first = 0; + while (first < snapshot.bytes.size() && source[first] == snapshot.bytes[first]) ++first; + char text[256]; + std::snprintf(text, sizeof(text), "guest snapshot differs from registered memory: snapshot 0x%llx+0x%zx (%s region 0x%llx+0x%llx%s%s%s) differs first at +0x%zx", static_cast(snapshot.address), snapshot.bytes.size(), live ? "live" : "snapshot-backed", static_cast(owner->begin), static_cast(owner->end - owner->begin), owner->writable ? ", writable" : "", owner->hostBacked ? ", host-backed" : "", owner->mirror != nullptr ? ", mirrored" : "", first); + static const bool fatal = std::getenv("APS5_SNAPSHOT_CHECK_FATAL") != nullptr; + if (!live || fatal) throw std::runtime_error(text); + static std::mutex reportMutex; + static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60); + std::lock_guard lock(reportMutex); + const auto mismatches = Snapshots().mismatched.fetch_add(1, std::memory_order_relaxed) + 1; + const auto now = std::chrono::steady_clock::now(); + if (now - lastReport >= std::chrono::seconds(10)) { + lastReport = now; + std::fprintf(stderr, "[gpu] %s; the live region serves the GPU (%llu mismatches so far; APS5_SNAPSHOT_CHECK_FATAL=1 fails the dispatch instead)\n", text, static_cast(mismatches)); + } + return; + } if (profile) Snapshots().checked.fetch_add(1, std::memory_order_relaxed); return; } @@ -3203,7 +4389,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)); @@ -3247,15 +4433,33 @@ void GuestBufferMemory::UploadPrepare(bool addressable) { } } +void GuestBufferMemory::completeSpaceWrites() const { + auto* recorder = Recorder::Active(); + if (recorder == nullptr || Recorder::InCompletion()) return; + const auto& base = space->base; + std::vector> writes; + std::vector candidates; + std::size_t next = 0; + while (next < base.size()) { + recorder->PendingWriteRanges(writes); + OverlappedRanges(std::span(base), next, writes, candidates); + next = base.size(); + for (const auto index : candidates) { + if (!CompletePendingCpuWrites(context, base[index].begin, static_cast(base[index].end - base[index].begin))) continue; + next = index + 1; + break; + } + } +} + void GuestBufferMemory::UploadFinish(bool addressable) { Require(prepared && !uploaded, "guest memory upload was not prepared"); static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto started = std::chrono::steady_clock::now(); for (const auto& region : regions) CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); - if (space != nullptr) { - for (const auto& region : space->base) CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); - } + if (space != nullptr) completeSpaceWrites(); uploaded = true; + for (const auto& region : regions) static_cast(FlushDepthPlanes(region.begin, static_cast(region.end - region.begin))); for (const auto& [begin, end] : writes) StorageTexture::FlushPending(begin, static_cast(end - begin), nullptr, "written buffer range"); if (context.hostImportAlignment != 0) { auto& state = Imports(); @@ -3338,12 +4542,6 @@ void GuestBufferMemory::UploadFinish(bool addressable) { if (profile) refreshUs += microsecondsSince(refreshStart); continue; } - if (sparseHeapCandidate(region)) { - const auto refreshStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - const bool bound = bindSparseMirror(region, importRanges()); - if (profile) refreshUs += microsecondsSince(refreshStart); - if (bound) continue; - } bool copyOnGpu = false; if (context.hostImportAlignment != 0) { auto& state = Imports(); @@ -3358,14 +4556,41 @@ void GuestBufferMemory::UploadFinish(bool addressable) { const HostImport* entry = region.direct != nullptr && state.epoch == importsEpoch ? region.direct : nullptr; region.direct = nullptr; if (entry == nullptr) entry = findImport(state, region.begin, region.end); - if (entry == nullptr) { - entry = importAccess(context, state, region.begin, region.end, importRanges()); + const bool sparseRun = region.sparseHeap && region.sparse; + if (entry == nullptr && !sparseRun) { + entry = importAccess(context, state, region.begin, region.end, importRanges(), ImportRequester::Build); + } + bool runImport = false; + if (entry == nullptr && sparseRun && !region.backed.empty() && region.backed.front().first == region.begin && !WritesOverlap(region.begin, static_cast(bytes))) { + const auto committedEnd = region.backed.back().second; + const auto neededEnd = std::min(region.end, std::max(committedEnd, std::max(region.lastView, region.begin) + 4u)); + const auto covers = [&](const HostImport* candidate) { return candidate != nullptr && candidate->base <= region.begin && candidate->base + candidate->bytes >= neededEnd; }; + entry = findImport(state, region.begin, neededEnd); + if (entry == nullptr && runCovers(importRanges(), region.begin, neededEnd)) entry = importAllocation(context, state, region.begin, importRanges(), ImportRequester::Build); + if (!covers(entry)) entry = nullptr; + 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 = 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; + const auto seen = state.sparseRunEnds.find(region.begin); + if (seen != state.sparseRunEnds.end() && seen->second == committedEnd && made < MaxSparseRunJoins) { + entry = importJoined(context, state, region.begin, neededEnd, lease); + if (covers(entry) && ++state.sparseRunJoins[entry->base] == MaxSparseRunJoins) std::fprintf(stderr, "[gpu] the sparse heap at 0x%llx was joined into one import %u times; it is not joined again and is copied once its run outgrows the import\n", static_cast(entry->base), MaxSparseRunJoins); + } + state.sparseRunEnds[region.begin] = committedEnd; + } + if (!covers(entry)) entry = nullptr; + runImport = entry != nullptr; } // 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(); // A buffer UploadPrepare made for a GPU copy is not needed: the import serves the // region in place (Descriptor prefers `direct`), and a kept buffer would count as copied. @@ -3379,7 +4604,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) { @@ -3403,6 +4628,12 @@ void GuestBufferMemory::UploadFinish(bool addressable) { gpuCopies.push_back(®ion); continue; } + if (sparseHeapCandidate(region)) { + const auto refreshStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + const bool bound = bindSparseMirror(region, importRanges()); + if (profile) refreshUs += microsecondsSince(refreshStart); + if (bound) continue; + } if (region.deviceLocal) { // The staging shadow UploadPrepare made cannot take the CPU copy (no host mapping): the // region falls back to a host buffer of its own. @@ -3448,13 +4679,13 @@ void GuestBufferMemory::UploadFinish(bool addressable) { void GuestBufferMemory::copyRegion(Region& region, bool addressable) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto bytes = region.end - region.begin; - const auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | (addressable ? VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT : 0u); + const auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | (addressable ? VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT : 0u); std::uint64_t readBytes = 0; bool inImport = false; if (profile && context.hostImportAlignment != 0) { auto& state = Imports(); std::lock_guard lock(state.mutex); - inImport = findImport(state, region.begin, region.end) != nullptr; + 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)) { @@ -3467,7 +4698,9 @@ void GuestBufferMemory::copyRegion(Region& region, bool addressable) { } const auto allocated = region.bufferEnd != 0 ? region.bufferEnd - region.begin : bytes; // A buffer made by UploadPrepare for a GPU copy that fell back here (its import went) is kept. - if (region.buffer == nullptr) region.buffer = std::make_shared(context, static_cast(allocated), usage); + const bool shared = region.buffer == nullptr && !region.hostBacked && !region.writable && !region.sparse && region.bufferEnd == 0 && region.snapshot.size() == bytes; + if (shared) region.buffer = snapshotCopy(context, region.begin, region.snapshot, usage); + else if (region.buffer == nullptr) region.buffer = std::make_shared(context, static_cast(allocated), usage); const auto readStart = std::chrono::steady_clock::now(); if (profile) allocateUs.fetch_add(microsecondsSince(allocateStart), std::memory_order_relaxed); // Named for the [hooksync] attribution: the reads below go through the flush hook. @@ -3497,7 +4730,7 @@ void GuestBufferMemory::copyRegion(Region& region, bool addressable) { } } else { - std::memcpy(region.buffer->Bytes().data(), region.snapshot.data(), region.snapshot.size()); + if (!shared) std::memcpy(region.buffer->Bytes().data(), region.snapshot.data(), region.snapshot.size()); readBytes += region.snapshot.size(); } region.snapshot.clear(); @@ -3539,15 +4772,19 @@ void GuestBufferMemory::recordGpuCopies(std::span copies, bool ad recorder->FlushKeyStoresOverlapping(region->begin, static_cast(region->end - region->begin)); recorder->FlushStoresOverlapping(region->begin, static_cast(region->end - region->begin)); } - const auto commands = recorder->Commands(); + VkAccessFlags covered = 0; + const auto commands = recorder->Commands(&covered); const auto timing = recorder->BeginGpuTiming(StagingCopyInKey); if (Recorder::BarrierValidate()) { std::vector> reads; for (const auto* region : copies) reads.emplace_back(region->begin, region->end); recorder->NoteAccess(Recorder::CommandClass::StagingIn, Recorder::Access{reads, {}, {}, VK_PIPELINE_STAGE_TRANSFER_BIT}); } - 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_READ_BIT); - Recorder::CountBarriers(Recorder::CommandClass::StagingIn); + constexpr BarrierMask leading{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_READ_BIT}; + if (!Recorder::ElideCovered(Recorder::CommandClass::StagingIn, leading, covered)) { + RecordMemoryBarrier(context, commands, leading.srcStage, leading.dstStage, leading.srcAccess, leading.dstAccess); + Recorder::CountBarriers(Recorder::CommandClass::StagingIn); + } bool stagedAny = false; std::uint64_t copiedBytes = 0; for (auto* region : copies) { @@ -3578,7 +4815,7 @@ void GuestBufferMemory::recordGpuCopies(std::span copies, bool ad std::memcpy(inputSnapshot->Bytes().data(), reinterpret_cast(address), static_cast(bytes)); copySource = inputSnapshot->Handle(); copyOffset = 0; - recorder->Keep(inputSnapshot); + recorder->Keep(inputSnapshot, inputSnapshot->Bytes().size()); CaptureTrace::Log("copy-input-snapshot batch=%llu address=%llx bytes=%llu", batch, static_cast(address), static_cast(bytes)); } if (capture && bytes == 32) { @@ -3658,11 +4895,15 @@ void GuestBufferMemory::RecordCopyBacks(Recorder& recorder) { const auto to = std::min(end, region.end); if (from >= to) continue; if (!recording) { - commands = recorder.Commands(); + VkAccessFlags covered = 0; + commands = recorder.Commands(&covered); timing = recorder.BeginGpuTiming(StagingCopyBackKey); // The shader's stores into the buffer (whichever stage made them) precede the copy. - RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); - Recorder::CountBarriers(Recorder::CommandClass::StagingOut); + constexpr BarrierMask leading{VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT}; + if (!Recorder::ElideCovered(Recorder::CommandClass::StagingOut, leading, covered)) { + RecordMemoryBarrier(context, commands, leading.srcStage, leading.dstStage, leading.srcAccess, leading.dstAccess); + Recorder::CountBarriers(Recorder::CommandClass::StagingOut); + } recording = true; } // Exactly the sub-range the shader may write, in place in the import: what the shader @@ -3696,7 +4937,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) { @@ -3705,14 +4946,14 @@ 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"); adjustment = static_cast(offset % std::max(context.limits.minStorageBufferOffsetAlignment, 4)); auto range = ViewBytes(bytes, adjustment); const auto end = region.direct != nullptr ? region.direct->base + region.direct->bytes : servedEnd(region); - const bool cut = region.direct == nullptr && end < region.end; + const bool cut = (region.direct == nullptr || region.runCut) && end < region.end; if (cut) { Require(address - adjustment < end, "guest buffer view starts past its copy"); range = std::min(range, end - (address - adjustment)); @@ -3724,7 +4965,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) { @@ -3732,7 +4976,7 @@ ShaderRecompiler::BdaAbi::Range GuestBufferMemory::addressRange(const Region& re const auto address = region.direct != nullptr ? region.direct->address + (region.begin - region.direct->base) : region.mirror != nullptr ? region.mirror->buffer->DeviceAddress() + (region.begin - region.mirror->base) : region.buffer->DeviceAddress(); Require(region.end - region.begin <= std::numeric_limits::max() - address, "GPU address range overflow"); const auto permissions = ShaderRecompiler::BdaAbi::Read | (region.direct != nullptr && region.writable ? ShaderRecompiler::BdaAbi::Write : 0u); - return {region.begin, region.direct != nullptr ? region.end : servedEnd(region), address, permissions, 0}; + return {region.begin, region.direct != nullptr ? inPlaceEnd(region) : servedEnd(region), address, permissions, 0}; } std::uint64_t GuestBufferMemory::servedEnd(const Region& region) { @@ -3740,16 +4984,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; @@ -3776,7 +5077,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; } @@ -3793,27 +5094,50 @@ std::vector GuestBufferMemory::AddressRanges() } std::optional GuestBufferMemory::CachedAddressTable() const { - if (space == nullptr || !regions.empty()) return std::nullopt; + if (space == nullptr) return std::nullopt; Require(uploaded && !committed, "guest GPU address ranges are not available"); - if (writes.empty()) return CachedTable{space->serial, &space->ranges}; - if (writeTableRanges != nullptr) return CachedTable{writeTableSerial, writeTableRanges.get()}; + if (writes.empty() && regions.empty()) return CachedTable{space->serial, &space->ranges}; + if (tableRanges != nullptr) return CachedTable{tableSerial, tableRanges.get()}; + const auto& spaceRanges = space->ranges; + const bool inWritable = regions.empty() && std::all_of(writes.begin(), writes.end(), [&](const auto& written) { + auto at = std::partition_point(spaceRanges.begin(), spaceRanges.end(), [&](const auto& range) { return range.end <= written.first; }); + for (; at != spaceRanges.end() && at->begin < written.second; ++at) { + if ((at->permissions & ShaderRecompiler::BdaAbi::Write) == 0u) return false; + } + return true; + }); + if (inWritable) return CachedTable{space->serial, &space->ranges}; + std::vector own; + own.reserve(regions.size()); + for (const auto& region : regions) own.push_back(addressRange(region)); + auto key = AddressTableKey(writes, own); + static const bool verify = std::getenv("APS5_VERIFY_BDA_TABLES") != nullptr; std::lock_guard lock(space->tableMutex); - const auto found = space->writeTables.find(writes); - if (found != space->writeTables.end()) { - writeTableSerial = found->second.first; - writeTableRanges = found->second.second; + const auto found = space->tables.find(key); + if (found != space->tables.end()) { + space->tableRecency.splice(space->tableRecency.end(), space->tableRecency, found->second.recent); + tableSerial = found->second.serial; + tableRanges = found->second.ranges; + if (verify) { + const auto built = AddressRanges(); + Require(built.size() == tableRanges->size() && (built.empty() || std::memcmp(built.data(), tableRanges->data(), built.size() * sizeof(built.front())) == 0), "a cached BDA table differs from the ranges of the build that found it"); + } } else { - writeTableRanges = std::make_shared>(AddressRanges()); - writeTableSerial = Spaces().serials.fetch_add(1, std::memory_order_relaxed) + 1; - if (space->writeTables.size() >= 64u) space->writeTables.erase(space->writeTables.begin()); - space->writeTables.emplace(writes, std::pair{writeTableSerial, writeTableRanges}); + tableRanges = std::make_shared>(AddressRanges()); + tableSerial = Spaces().serials.fetch_add(1, std::memory_order_relaxed) + 1; + if (space->tables.size() >= 64u) { + space->tables.erase(*space->tableRecency.front()); + space->tableRecency.pop_front(); + } + const auto inserted = space->tables.emplace(std::move(key), AddressSpace::Table{tableSerial, tableRanges, {}}).first; + inserted->second.recent = space->tableRecency.insert(space->tableRecency.end(), &inserted->first); } - return CachedTable{writeTableSerial, writeTableRanges.get()}; + return CachedTable{tableSerial, tableRanges.get()}; } 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; @@ -3833,7 +5157,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; @@ -3846,10 +5170,10 @@ 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)); + if (region.direct != nullptr || (region.gpuCopy && region.copiedBack)) GuestMemory::MarkPossiblyWritten(begin, static_cast(end - begin)); } } @@ -3913,7 +5237,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; @@ -3930,14 +5254,14 @@ 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. if (region.direct == nullptr && !(region.gpuCopy && (region.copiedBack || region.deviceLocal)) && !(region.mirror != nullptr && !region.mirror->writable)) Recorder::NoteWrittenBack(begin, static_cast(end - begin)); if (region.direct != nullptr) { // The GPU wrote imported guest memory directly; page write watching did not see it. - GuestMemory::MarkWritten(begin, static_cast(end - begin)); + GuestMemory::MarkPossiblyWritten(begin, static_cast(end - begin)); continue; } if (region.mirror != nullptr) { @@ -3967,7 +5291,7 @@ void GuestBufferMemory::WriteBack() { if (region.copiedBack) { // The GPU copied the written sub-range back into the import; page write watching // did not see it. - GuestMemory::MarkWritten(begin, static_cast(end - begin)); + GuestMemory::MarkPossiblyWritten(begin, static_cast(end - begin)); continue; } static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; @@ -4019,6 +5343,9 @@ std::optional>> GuestBufferM result.emplace_back(region.begin, region.end); } } + auto& state = Imports(); + std::lock_guard lock(state.mutex); + const bool sameDevice = state.device == context.device; for (const auto& region : regions) { // A read-only mirror is as fixed as an import: its bytes and VkBuffer never change while its // A staged region is keyed by its import like one bound in place: the import is the source @@ -4030,33 +5357,60 @@ std::optional>> GuestBufferM // A later use copies from `copySource` again, so it must still be the import serving // the range (the flush before the copy-in can retire and remake imports): the serial // the caller records names the current import, and its buffer must be that one. - auto& state = Imports(); - std::lock_guard lock(state.mutex); - const auto* entry = state.device == context.device ? findImport(state, region.begin, region.end) : nullptr; + const auto* entry = sameDevice ? findImport(state, region.begin, region.end) : nullptr; if (entry == nullptr || entry->buffer != region.copySource) return std::nullopt; } - result.emplace_back(region.begin, region.end); + if (region.direct != nullptr && (!sameDevice || findImport(state, region.begin, region.begin + 1) != region.direct)) return std::nullopt; + result.emplace_back(region.begin, inPlaceEnd(region)); } return result; } -std::vector> GuestBufferMemory::InPlaceReads() const { - std::vector> result; +std::vector GuestBufferMemory::CutRegions() const { + std::vector result; if (!uploaded || committed) return result; + for (const auto& region : regions) { + if (region.direct == nullptr || !region.runCut) continue; + result.push_back({region.begin, region.end, inPlaceEnd(region)}); + } + return result; +} + +std::uint64_t GuestBufferMemory::inPlaceEnd(const Region& region) { + return region.direct != nullptr && region.runCut ? std::min(region.end, region.direct->base + region.direct->bytes) : region.end; +} + +void GuestBufferMemory::InPlaceReads(std::vector>& result) const { + result.clear(); + if (!uploaded || committed) return; if (space != nullptr) { for (const auto& region : space->base) { if (region.direct != nullptr) result.emplace_back(region.begin, region.end); } } for (const auto& region : regions) { - if (region.direct != nullptr) result.emplace_back(region.begin, region.end); + if (region.direct != nullptr) result.emplace_back(region.begin, inPlaceEnd(region)); } +} + +std::vector> GuestBufferMemory::InPlaceReads() const { + std::vector> result; + InPlaceReads(result); return result; } -std::vector> GuestBufferMemory::DeviceReads() const { - std::vector> result; - if (!uploaded || committed) return result; +std::optional GuestBufferMemory::SplitInPlaceReads() const { + if (!uploaded || committed || space == nullptr || space->directReads == nullptr) return std::nullopt; + SplitReads split{space->directReads, {}}; + for (const auto& region : regions) { + if (region.direct != nullptr) split.own.emplace_back(region.begin, region.end); + } + return split; +} + +void GuestBufferMemory::DeviceReads(std::vector>& result) const { + result.clear(); + if (!uploaded || committed) return; if (space != nullptr) { for (const auto& region : space->base) { if (region.direct != nullptr || region.mirror != nullptr) result.emplace_back(region.begin, region.end); @@ -4065,6 +5419,11 @@ std::vector> GuestBufferMemory::DeviceRe for (const auto& region : regions) { if (region.direct != nullptr || region.mirror != nullptr) result.emplace_back(region.begin, region.end); } +} + +std::vector> GuestBufferMemory::DeviceReads() const { + std::vector> result; + DeviceReads(result); return result; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp index 87a8c37b6d..e08b9e47fd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp @@ -90,26 +90,23 @@ GuestSamplerResource DecodeSamplerResource(std::span words, Require(!forceUnormCoords || unnormalizedProven, "guest sampler descriptor uses unnormalized coordinates which are not implemented"); if (forceUnormCoords) { - Require(xyMagFilter == xyMinFilter, "guest sampler descriptor uses unnormalized coordinates with different minification and magnification filters, which is not implemented"); - Require(!isAnisoFilter(xyMagFilter), "guest sampler descriptor uses unnormalized coordinates with anisotropic filtering, which is not implemented"); - Require(clampX == 2u || clampX == 6u, "guest sampler descriptor uses unnormalized coordinates with clamp mode " + std::to_string(clampX) + " on X; only clamp-to-last-texel and clamp-to-border are implemented"); - Require(clampY == 2u || clampY == 6u, "guest sampler descriptor uses unnormalized coordinates with clamp mode " + std::to_string(clampY) + " on Y; only clamp-to-last-texel and clamp-to-border are implemented"); + Require(!isAnisoFilter(xyMagFilter) && !isAnisoFilter(xyMinFilter), "guest sampler descriptor uses unnormalized coordinates with anisotropic filtering, which is not implemented"); + Require(clampX == 0u || clampX == 2u || clampX == 6u, "guest sampler descriptor uses unnormalized coordinates with clamp mode " + std::to_string(clampX) + " on X; only clamp-to-last-texel and clamp-to-border are implemented"); + Require(clampY == 0u || clampY == 2u || clampY == 6u, "guest sampler descriptor uses unnormalized coordinates with clamp mode " + std::to_string(clampY) + " on Y; only clamp-to-last-texel and clamp-to-border are implemented"); Require(!truncCoord, "guest sampler descriptor uses unnormalized coordinates with TRUNC_COORD, which is not implemented"); Require(!mcCoordTrunc, "guest sampler descriptor uses unnormalized coordinates with MC_COORD_TRUNC, which is not implemented"); } else { Require(!unnormalizedProven, "guest sampler descriptor is bound as unnormalized without FORCE_UNNORMALIZED"); } Require(!forceSrgb || forceDegammaPaired, "guest sampler descriptor forces sRGB decoding which is not implemented"); - // TRUNC_COORD picks point-sampled texels by truncation instead of rounding, and the perf fields - // trade mip/depth precision for speed; Vulkan's nearest filtering already floors, so these only - // move texel selection by half a texel at most and are accepted as is. ANISO_THRESHOLD and + // The perf fields trade mip/depth precision for speed and are accepted as is. ANISO_THRESHOLD and // ANISO_BIAS only tune how many anisotropic taps the hardware takes; the host filter picks its own. static_cast(truncCoord); static_cast(anisoThreshold); static_cast(anisoBias); const auto reductionMode = toVkReductionMode(filterMode); Require(!disableDegamma, "guest sampler descriptor disables degamma which is not implemented"); - Require(lodBiasSec == 0, "guest sampler descriptor uses a secondary LOD bias which is not implemented"); + static_cast(lodBiasSec); Require(!pointPreclamp, "guest sampler descriptor uses point preclamping which is not implemented"); static_cast(anisoOverride); Require(!blendZeroPrt, "guest sampler descriptor uses PRT blend-zero which is not implemented"); @@ -117,7 +114,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)); @@ -164,7 +160,10 @@ GuestSamplerResource DecodeSamplerResource(std::span words, result.nonSeamlessCube = disableCubeWrap; if (forceUnormCoords) { result.unnormalizedCoordinates = true; + result.minFilter = result.magFilter; result.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + result.addressModeU = clampX == 6u ? VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER : VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + result.addressModeV = clampY == 6u ? VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER : VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; result.minLod = 0.0f; result.maxLod = 0.0f; result.lodBias = 0.0f; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 55ecd30e30..1249e08037 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -3,7 +3,10 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include +#include +#include #include #include @@ -44,10 +47,57 @@ TextureDimension resolveDimension(std::uint32_t raw) { case 11: return TextureDimension::kCube; case 12: return TextureDimension::k1DArray; case 13: return TextureDimension::k2DArray; + case 14: return TextureDimension::k2DMsaa; default: throw std::runtime_error("AGC graphics: guest texture descriptor uses an unsupported image type " + std::to_string(raw)); } } +bool ZeroTail(std::span words) { + return words[4] == 0u && words[5] == 0u && words[6] == 0u && words[7] == 0u; +} + +bool SameLevels(const TileMipLayout& a, const TileMipLayout& b) { + return a.tiledOffset == b.tiledOffset && a.tiledSize == b.tiledSize && a.width == b.width && a.height == b.height && a.blocksPerRow == b.blocksPerRow && a.pitchBytes == b.pitchBytes && a.tail == b.tail && a.tailX == b.tailX && a.tailY == b.tailY; +} + +std::optional> R128ResidentLevelWords(std::span words) { + const auto baseLevel = (words[3] >> 12u) & 0xfu; + const auto lastLevel = (words[3] >> 16u) & 0xfu; + const auto typeRaw = (words[3] >> 28u) & 0xfu; + if (baseLevel == 0u || baseLevel > lastLevel || (typeRaw != 8u && typeRaw != 9u)) return std::nullopt; + const std::array chainWords{words[0], words[1], words[2], words[3], 0u, lastLevel << 4u, 0u, 0u}; + const auto width = (((words[1] >> 30u) & 0x3u) | ((words[2] & 0xfffu) << 2u)) + 1u; + const auto height = ((words[2] >> 14u) & 0x3fffu) + 1u; + const auto minLod = (words[1] >> 8u) & 0xfffu; + const auto widthMinus1 = std::max(width >> baseLevel, 1u) - 1u; + const auto heightMinus1 = std::max(height >> baseLevel, 1u) - 1u; + const auto rebasedMinLod = minLod > baseLevel * 256u ? minLod - baseLevel * 256u : 0u; + const auto levels = lastLevel - baseLevel; + auto rebased = chainWords; + rebased[1] = (words[1] & ~((0xfffu << 8u) | (0x3u << 30u))) | (rebasedMinLod << 8u) | ((widthMinus1 & 0x3u) << 30u); + rebased[2] = (words[2] & ~(0xfffu | (0x3fffu << 14u))) | ((widthMinus1 >> 2u) & 0xfffu) | ((heightMinus1 & 0x3fffu) << 14u); + rebased[3] = (words[3] & ~(0xffu << 12u)) | (levels << 16u); + rebased[5] = levels << 4u; + try { + const auto chain = DescribeSurface(DecodeTextureResource(chainWords)); + const auto view = DescribeSurface(DecodeTextureResource(rebased)); + if (chain.mips.size() != lastLevel + 1u || view.mips.size() != levels + 1u || chain.layers != view.layers) return std::nullopt; + for (std::uint32_t level = baseLevel; level <= lastLevel; ++level) { + if (!SameLevels(chain.mips[level], view.mips[level - baseLevel])) return std::nullopt; + } + } catch (const std::exception&) { + return std::nullopt; + } + return rebased; +} + +} + +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 <= 1; } GuestTextureResource DecodeTextureResource(std::span words) { @@ -76,14 +126,14 @@ GuestTextureResource DecodeTextureResource(std::span words) const auto baseArray = (words[4] >> 16u) & 0x1fffu; const auto arrayPitch = (words[5] >> 0u) & 0xfu; - const auto maxMip = (words[5] >> 4u) & 0xfu; + auto maxMip = (words[5] >> 4u) & 0xfu; const auto minLodWarn = (words[5] >> 8u) & 0xfffu; const auto cornerSample = ((words[5] >> 23u) & 0x1u) != 0; const auto mipStatsCntEn = ((words[5] >> 25u) & 0x1u) != 0; const auto prtDefColor = ((words[5] >> 26u) & 0x1u) != 0; const auto mipStatsCntId = words[6] & 0xffu; - const auto msaaDepth = ((words[6] >> 10u) & 0x1u) != 0; + const auto iterate256 = ((words[6] >> 10u) & 0x1u) != 0; const auto maxUncompBlkSize = (words[6] >> 15u) & 0x3u; const auto maxCompBlkSize = (words[6] >> 17u) & 0x3u; const auto metaPipeAligned = ((words[6] >> 19u) & 0x1u) != 0; @@ -111,7 +161,7 @@ GuestTextureResource DecodeTextureResource(std::span words) Require(!cornerSample, "guest texture descriptor uses corner sampling which is not implemented"); Require(!prtDefColor, "guest texture descriptor uses a partially resident default color which is not implemented"); Require(arrayPitch == 0, "guest texture descriptor uses a nonzero array pitch which is not implemented"); - Require(!msaaDepth, "guest texture descriptor uses MSAA which is not implemented"); + Require(!iterate256 || (typeRaw == 14u && tileModeRaw == 24u && (format == 7u || format == 22u)), "guest texture descriptor ITERATE_256 is implemented only for multisampled Z depth"); static_cast(maxUncompBlkSize); static_cast(maxCompBlkSize); // Surfaces are always written uncompressed here (render targets and storage images bypass DCC), so @@ -128,6 +178,14 @@ GuestTextureResource DecodeTextureResource(std::span words) Require(baseLevel <= lastLevel, "guest texture descriptor has a base mip level past its last mip level"); + const bool fmask = ShaderRecompiler::IsFmaskTextureFormat(static_cast(format)); + if (fmask) { + Require(format == static_cast(ShaderRecompiler::IrBufferFormat::Fmask32_S8_F8), "guest FMASK format is not implemented"); + Require(!writeCompress && (!metaCompress || metaAddr != 0u), "FMASK CMASK compression is not implemented without readable metadata"); + Require(!metaCompress || metaPipeAligned, "FMASK CMASK metadata must be pipe aligned"); + Require((typeRaw == 9u || typeRaw == 13u) && tileModeRaw == 24u && baseLevel == 0u && lastLevel == 0u && maxMip == 0u, "FMASK reads require a single-mip 2D or 2D-array Z_64KB_X surface"); + for (const auto selector : {dstSelX, dstSelY, dstSelZ, dstSelW}) Require(selector == 0u || selector == 1u || selector == 4u, "FMASK destination channel selector is not implemented"); + } const auto describe = [&] { return " (format " + std::to_string(format) + ", " + std::to_string(width) + "x" + std::to_string(height) + "x" + std::to_string(depth + 1) + ", type " + std::to_string(typeRaw) + ", tile " + std::to_string(tileModeRaw) + ", mips " + std::to_string(baseLevel) + ".." + std::to_string(lastLevel) + " of " + std::to_string(maxMip + 1) + ")"; }; TextureTileMode tileMode; @@ -138,6 +196,13 @@ GuestTextureResource DecodeTextureResource(std::span words) } catch (const std::exception& error) { throw std::runtime_error(std::string(error.what()) + describe()); } + std::uint32_t samples = 1; + if (dimension == TextureDimension::k2DMsaa) { + Require(baseLevel == 0 && lastLevel >= 1u && lastLevel <= 4u && maxMip == lastLevel, "guest multisampled texture descriptor does not hold the same sample count of 2 to 16 in its last level and its last mip, which is not implemented" + describe()); + samples = 1u << lastLevel; + lastLevel = 0; + maxMip = 0; + } // Views may name levels past MAX_MIP (a 512x512 view through 1x1 over a 9-level surface); the // hardware never addresses them, so the view ends at the surface's last level. if (baseLevel <= maxMip) lastLevel = std::min(lastLevel, maxMip); @@ -161,6 +226,9 @@ GuestTextureResource DecodeTextureResource(std::span words) } Require(depth == 0 && baseArray == 0, "guest 2D texture descriptor has a nonzero depth or base array"); break; + case TextureDimension::k2DMsaa: + Require(depth == 0 && baseArray == 0, "guest multisampled 2D texture descriptor has a nonzero depth or base array"); + break; case TextureDimension::k3D: // word4 DEPTH holds the depth minus one; BASE_ARRAY has no meaning for volumes. Require(baseArray == 0, "guest 3D texture descriptor has a nonzero base array"); @@ -177,8 +245,8 @@ GuestTextureResource DecodeTextureResource(std::span words) Require(width == height, "guest cube texture descriptor is not square"); Require(baseArray <= depth, "guest cube texture descriptor has a base array past its last array slice"); if (baseArray == depth) depth += 5u; - if ((depth - baseArray + 1u) % 6u != 0) { - Require(false, "guest cube texture descriptor does not contain a multiple of 6 array slices" + + if (depth - baseArray + 1u < 6u) { + Require(false, "guest cube texture descriptor holds fewer than 6 array slices" + describe() + " (base array " + std::to_string(baseArray) + ")"); } break; @@ -197,18 +265,25 @@ GuestTextureResource DecodeTextureResource(std::span words) result.lastLevel = lastLevel; result.tileMode = tileMode; result.dimension = dimension; - result.format = format; + result.format = fmask ? static_cast(ShaderRecompiler::IrBufferFormat::Format32UInt) : format; result.dstSelX = static_cast(dstSelX); result.dstSelY = static_cast(dstSelY); result.dstSelZ = static_cast(dstSelZ); result.dstSelW = static_cast(dstSelW); result.bcSwizzle = static_cast(bcSwizzle); - result.dccAddress = metaCompress ? metaAddr << 8u : 0u; + result.dccAddress = metaCompress && !fmask ? metaAddr << 8u : 0u; + result.fmaskCmaskAddress = metaCompress && fmask ? metaAddr << 8u : 0u; result.dccAlphaOnMsb = dccAlphaPos; result.dccPipeAligned = metaPipeAligned; + result.iterate256 = iterate256; result.minLod = minLod; + result.samples = samples; if (baseLevel > maxMip) { - Require(LevelsFitAllocation(result, lastLevel + 1u), "guest texture descriptor starts past the surface's last mip level at levels that would move the surface's own" + describe()); + const bool fits = LevelsFitAllocation(result, lastLevel + 1u); + if (!fits && ZeroTail(words)) { + if (const auto resident = R128ResidentLevelWords(words)) return DecodeTextureResource(*resident); + } + Require(fits, "guest texture descriptor starts past the surface's last mip level at levels that would move the surface's own" + describe()); result.mipCount = lastLevel + 1u; } return result; @@ -223,6 +298,15 @@ bool DescriptorSingleLevel(std::span words) { return ((words[3] >> 12u) & 0xfu) == ((words[3] >> 16u) & 0xfu); } +bool FirstLayerDescriptor(std::span words, std::array& narrowed) { + if (words.size() != narrowed.size()) return false; + const auto resource = DecodeTextureResource(words); + if (resource.dimension != TextureDimension::k2DArray || resource.depthOrLastArray <= resource.baseArray) return false; + std::copy(words.begin(), words.end(), narrowed.begin()); + narrowed[4] = (narrowed[4] & ~0x1fffu) | resource.baseArray; + return true; +} + bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, TextureDimension dimension) { switch (shape) { case ShaderRecompiler::DescriptorImageShape::Image1D: return dimension == TextureDimension::k1D; @@ -231,6 +315,8 @@ bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, Texture case ShaderRecompiler::DescriptorImageShape::ImageCube: return dimension == TextureDimension::kCube; case ShaderRecompiler::DescriptorImageShape::Image3D: return dimension == TextureDimension::k3D; case ShaderRecompiler::DescriptorImageShape::Image1DArray: return dimension == TextureDimension::k1DArray; + case ShaderRecompiler::DescriptorImageShape::Image2DMultisample: return dimension == TextureDimension::k2DMsaa; + case ShaderRecompiler::DescriptorImageShape::Image2DMultisampleArray: return dimension == TextureDimension::k2DMsaa; } throw std::runtime_error("AGC graphics: MatchesGuestDimension encountered an unknown descriptor image shape"); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp new file mode 100644 index 0000000000..fe92fa1b97 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp @@ -0,0 +1,106 @@ +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/MultisampleReadbackUint_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/MultisampleReadbackDepth_spv.h" +#include +#include +#include + +namespace AgcDriver::Graphics { +namespace { + +std::uint32_t readbackElementBytes(VkFormat format) { + switch (format) { + case VK_FORMAT_R8_UINT: return 1u; + case VK_FORMAT_R16_UINT: return 2u; + case VK_FORMAT_R32_UINT: case VK_FORMAT_D32_SFLOAT: return 4u; + case VK_FORMAT_R32G32_UINT: return 8u; + case VK_FORMAT_R32G32B32A32_UINT: return 16u; + default: Require(false, "multisample readback requires an unsigned element view or D32 depth"); return 0u; + } +} + +} + +VkPipeline TextureDetiler::multisampleReadbackPipeline(std::uint32_t elementBytes, bool depth) { + const auto key = elementBytes | (depth ? 32u : 0u); + for (const auto& entry : readbackPipelines) if (entry.first == key) return entry.second; + if (readbackDescriptorLayout == VK_NULL_HANDLE) { + const std::array bindings{{ + {0u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1u, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + {1u, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1u, VK_SHADER_STAGE_COMPUTE_BIT, nullptr} + }}; + VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + info.bindingCount = static_cast(bindings.size()); + info.pBindings = bindings.data(); + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &info, nullptr, &readbackDescriptorLayout), "vkCreateDescriptorSetLayout multisample readback"); + } + if (readbackPipelineLayout == VK_NULL_HANDLE) { + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0u, 7u * sizeof(std::uint32_t)}; + VkPipelineLayoutCreateInfo info{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + info.setLayoutCount = 1u; + info.pSetLayouts = &readbackDescriptorLayout; + info.pushConstantRangeCount = 1u; + info.pPushConstantRanges = &push; + Check(context.Function("vkCreatePipelineLayout")(context.device, &info, nullptr, &readbackPipelineLayout), "vkCreatePipelineLayout multisample readback"); + } + auto& module = readbackModules[depth ? 1u : 0u]; + if (module == VK_NULL_HANDLE) { + module = depth ? CreateShaderModule(context, MULTISAMPLE_READBACK_DEPTH_SPV, sizeof(MULTISAMPLE_READBACK_DEPTH_SPV), "vkCreateShaderModule multisample readback") + : CreateShaderModule(context, MULTISAMPLE_READBACK_UINT_SPV, sizeof(MULTISAMPLE_READBACK_UINT_SPV), "vkCreateShaderModule multisample readback"); + } + const VkSpecializationMapEntry entry{0u, 0u, sizeof(elementBytes)}; + const VkSpecializationInfo specialization{1u, &entry, sizeof(elementBytes), &elementBytes}; + VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + info.stage.module = module; + info.stage.pName = "main"; + info.stage.pSpecializationInfo = &specialization; + info.layout = readbackPipelineLayout; + readbackPipelines.reserve(readbackPipelines.size() + 1u); + VkPipeline result = VK_NULL_HANDLE; + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1u, &info, nullptr, &result), "vkCreateComputePipelines multisample readback"); + } + readbackPipelines.emplace_back(key, result); + return result; +} + +void TextureDetiler::DispatchMultisampleReadback(VkCommandBuffer commands, VkFormat format, VkBuffer destination, std::uint64_t offset, std::uint64_t bytes, VkImageView arrayView, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t fragments, std::uint32_t firstElement, std::uint32_t elements) { + GuestMemory::AssertGpuLockHeld("TextureDetiler::DispatchMultisampleReadback"); + const auto elementBytes = readbackElementBytes(format); + Require(commands != VK_NULL_HANDLE && destination != VK_NULL_HANDLE && arrayView != VK_NULL_HANDLE, "multisample readback requires commands, buffer and array view"); + Require(width != 0u && width <= 16384u && height != 0u && height <= 16384u && layers != 0u && layers <= 8192u, "multisample readback extent is unsupported"); + Require(fragments == 2u || fragments == 4u || fragments == 8u, "multisample readback fragment count is unsupported"); + const auto total = static_cast(width) * height * layers * fragments; + Require(total <= std::numeric_limits::max() && elements != 0u && static_cast(firstElement) + elements <= total, "multisample readback element range is invalid"); + const auto required = (static_cast(elements) * elementBytes + 3u) & ~3ull; + const auto alignment = std::max(4u, context.limits.minStorageBufferOffsetAlignment); + Require(offset % alignment == 0u && bytes >= required && bytes <= context.limits.maxStorageBufferRange && bytes % 4u == 0u && offset <= std::numeric_limits::max() - bytes, "multisample readback buffer range is invalid"); + Require(context.limits.maxComputeWorkGroupInvocations >= 64u && context.limits.maxComputeWorkGroupSize[0] >= 64u && context.limits.maxComputeWorkGroupSize[1] >= 1u && context.limits.maxComputeWorkGroupSize[2] >= 1u && context.limits.maxComputeWorkGroupCount[2] >= 1u, "device does not support the multisample readback workgroup"); + const auto packedElements = elementBytes < 4u ? 4u / elementBytes : 1u; + const auto threads = (static_cast(elements) + packedElements - 1u) / packedElements; + const auto groups = (threads + 63u) / 64u; + const auto groupsX = std::min(groups, context.limits.maxComputeWorkGroupCount[0]); + Require(groupsX != 0u, "device has no multisample readback workgroups"); + const auto groupsY = (groups + groupsX - 1u) / groupsX; + Require(groupsY <= context.limits.maxComputeWorkGroupCount[1] && groupsX * groupsY * 64u <= std::numeric_limits::max(), "multisample readback dispatch exceeds device limits"); + const auto pipeline = multisampleReadbackPipeline(elementBytes, format == VK_FORMAT_D32_SFLOAT); + const auto set = allocateFrom(readbackDescriptorSets, readbackDescriptorLayout, {{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 64u}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 64u}}, "vkCreateDescriptorPool multisample readback", "vkAllocateDescriptorSets multisample readback", "vkResetDescriptorPool multisample readback"); + const VkDescriptorBufferInfo buffer{destination, offset, bytes}; + const VkDescriptorImageInfo image{VK_NULL_HANDLE, arrayView, VK_IMAGE_LAYOUT_GENERAL}; + const std::array writes{{ + {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, set, 0u, 0u, 1u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, nullptr, &buffer, nullptr}, + {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, set, 1u, 0u, 1u, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, &image, nullptr, nullptr} + }}; + context.Function("vkUpdateDescriptorSets")(context.device, static_cast(writes.size()), writes.data(), 0u, nullptr); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); + context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, readbackPipelineLayout, 0u, 1u, &set, 0u, nullptr); + const std::array push{width, height, layers, fragments, firstElement, elements, static_cast(groupsX * 64u)}; + context.Function("vkCmdPushConstants")(commands, readbackPipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0u, sizeof(push), push.data()); + context.Function("vkCmdDispatch")(commands, static_cast(groupsX), static_cast(groupsY), 1u); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 792b1395a0..809e680d42 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -1,9 +1,16 @@ #include #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "IntermediateRepresentation/IrMetadata/BufferFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include @@ -12,19 +19,89 @@ #include #include #include +#include #include #include #include +#include +#include #include #include namespace AgcDriver::Graphics { namespace { +std::uint32_t MultisampledBlockLog2(ColorTileMode mode) { + switch (mode) { + case ColorTileMode::Standard4KB: return 12u; + case ColorTileMode::Standard64KB: case ColorTileMode::Z64KBX: case ColorTileMode::D64KBX: case ColorTileMode::RenderTarget: return 16u; + default: return 0u; + } +} + +std::uint64_t collectBlocks(std::uint64_t address, std::size_t bytes) { + constexpr std::uint64_t block = 65536; + const auto first = address & ~(block - 1); + const auto stop = (address + bytes + block - 1) & ~(block - 1); + const auto generation = GuestMemory::CollectWritesUncached(first, static_cast(stop - first)); + return generation != 0 ? generation : GuestMemory::CollectWritesUncached(address, bytes); +} + +GuestTextureResource fmaskSnapshotResource(const GuestTextureResource& resource) { + Require(resource.format == static_cast(ShaderRecompiler::IrBufferFormat::Format32UInt) && resource.fmaskCmaskAddress != 0u, "FMASK image format read semantics are not implemented"); + Require(resource.dimension == TextureDimension::k2D || resource.dimension == TextureDimension::k2DArray, "FMASK image dimension is not implemented"); + Require(resource.tileMode == TextureTileMode::kZ64KBX && (resource.pipeBankXor & ~0xcf00u) == 0u, "FMASK image swizzle or pipe/bank XOR is not implemented"); + Require(resource.width != 0u && resource.width <= 16384u && resource.height != 0u && resource.height <= 16384u && resource.depthOrLastArray < 8192u && resource.baseArray <= resource.depthOrLastArray, "invalid FMASK image extent or layer range"); + Require(resource.dimension == TextureDimension::k2DArray || (resource.depthOrLastArray == 0u && resource.baseArray == 0u), "FMASK 2D image has an array range"); + Require(resource.mipCount == 1u && resource.allocatedMipCount == 1u && resource.baseLevel == 0u && resource.lastLevel == 0u && resource.minLod == 0u, "FMASK image mip fields are not implemented"); + Require(resource.fmaskCmaskAddress == 0u || resource.dccPipeAligned, "FMASK image CMASK must be pipe aligned"); + for (const auto selector : {resource.dstSelX, resource.dstSelY, resource.dstSelZ, resource.dstSelW}) Require(selector == 0u || selector == 1u || selector == 4u, "FMASK image selects an unverified channel"); + auto snapshot = resource; + snapshot.format = static_cast(ShaderRecompiler::IrBufferFormat::Format32UInt); + snapshot.tileMode = TextureTileMode::kLinear; + snapshot.pipeBankXor = 0u; + snapshot.dccAddress = 0u; + snapshot.fmaskCmaskAddress = 0u; + snapshot.dccAlphaOnMsb = false; + snapshot.dccPipeAligned = false; + return snapshot; +} + +GuestTextureResource fmaskResource(const ColorTarget& target) { + GuestTextureResource resource{}; + resource.baseAddress = target.fmaskAddress; + resource.width = target.extent.width; + resource.height = target.extent.height; + resource.mipCount = 1u; + resource.allocatedMipCount = 1u; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = static_cast(ShaderRecompiler::IrBufferFormat::Format32UInt); + resource.dstSelX = 4u; + resource.pipeBankXor = target.fmaskPipeBankXor; + resource.fmaskCmaskAddress = target.cmaskAddress; + resource.dccPipeAligned = true; + return resource; +} + bool sameTarget(const ColorTarget& a, const ColorTarget& b) { return a.surfaceAddress == b.surfaceAddress && a.format == b.format && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.samples == b.samples; } +constexpr VkImageUsageFlags twinViewUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + +VkFormat colorTwinFormat(const Context& context, VkFormat format) { + if (const auto srgb = SrgbTwinFormat(context, format); srgb != VK_FORMAT_UNDEFINED) return srgb; + for (const auto unorm : {VK_FORMAT_R8_UNORM, VK_FORMAT_R8G8_UNORM, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_A8B8G8R8_UNORM_PACK32}) { + if (SrgbTwinFormat(context, unorm) == format) return unorm; + } + return VK_FORMAT_UNDEFINED; +} + +bool recreated(const ColorTarget& resident, const ColorTarget& use) { + return use.dccAddress != 0 && use.pipeBankXor != resident.pipeBankXor && use.address == resident.address && use.dccAddress == resident.dccAddress && use.dccControl == resident.dccControl && use.dccAlphaOnMsb == resident.dccAlphaOnMsb && use.dccPipeAligned == resident.dccPipeAligned && use.cmaskAddress == resident.cmaskAddress && use.cmaskBytes == resident.cmaskBytes && use.tileMode == resident.tileMode && use.elementBytes == resident.elementBytes; +} + VkClearColorValue fastClearColor(const ColorTarget& target) { Require(target.elementBytes != 0 && target.elementBytes <= sizeof(target.clearWords) && 16u % target.elementBytes == 0, "the fast clear of a multisampled target over 64-bit texels is not modeled"); std::array texels{}; @@ -39,63 +116,57 @@ VkClearColorValue fastClearColor(const ColorTarget& target) { template void recordCommands(const Context& context, Record&& record) { auto* recorder = Recorder::Active(); + if (recorder != nullptr && recorder->Device() != context.device) recorder = nullptr; std::unique_ptr batch; if (recorder == nullptr) batch = std::make_unique(context); record(recorder != nullptr ? recorder->Commands() : batch->Handle(), recorder); if (batch) batch->SubmitAndWait(); } -class MultisampleTarget { +class MultisampleTarget : public std::enable_shared_from_this { 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"); - constexpr 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; + orderFormat = MayReorderSamples(context, target.samples) ? SampleOrderStorageFormat(context, target.format, target.elementBytes, target.samples) : VK_FORMAT_UNDEFINED; + const VkImageCreateFlags orderFlags = orderFormat != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT : 0u; + if (orderFormat != VK_FORMAT_UNDEFINED) usage |= VK_IMAGE_USAGE_STORAGE_BIT; VkImageFormatProperties supported{}; - Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target"); + sampleable = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0 && context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage | VK_IMAGE_USAGE_SAMPLED_BIT, orderFlags, &supported) == VK_SUCCESS && (supported.sampleCounts & samples) != 0; + if (sampleable) usage |= VK_IMAGE_USAGE_SAMPLED_BIT; + Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, orderFlags, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target"); Require((supported.sampleCounts & samples) != 0 && target.extent.width <= supported.maxExtent.width && target.extent.height <= supported.maxExtent.height, "multisampled render target exceeds device image limits"); + storageFormat = HostStorageFormat(context, target.format); + storageFlags = storageFormat != target.format ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT : 0u; + 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; + twinFormat = colorTwinFormat(context, target.format); + if (twinFormat != VK_FORMAT_UNDEFINED) { + VkFormatProperties twinProperties{}; + context.formatProperties(context.physical, twinFormat, &twinProperties); + const VkImageCreateFlags twinFlags = orderFlags | VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; + if ((twinProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) == 0 || !storageSupports(target.format, usage, twinFlags) || !storageSupports(twinFormat, twinViewUsage, twinFlags)) twinFormat = 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, orderFlags); + image = created.image; + memory = created.memory; + view = created.view; + allocationBytes = created.bytes; 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); }); @@ -108,9 +179,141 @@ 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; + VkDeviceSize bytes = 0; + }; + + Allocation allocate(VkImageUsageFlags imageUsage, VkImageCreateFlags flags) { + Allocation created; + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.flags = flags; + const std::array viewFormats{target.format, twinFormat}; + VkImageFormatListCreateInfo formatList{VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO}; + if (twinFormat != VK_FORMAT_UNDEFINED) { + if ((flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0 && context.imageFormatList) { + formatList.viewFormatCount = static_cast(viewFormats.size()); + formatList.pViewFormats = viewFormats.data(); + info.pNext = &formatList; + } + info.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; + } + 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; + created.bytes = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + created.memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "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}; + VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + viewUsage.usage = imageUsage & ~VK_IMAGE_USAGE_STORAGE_BIT; + if (orderFormat != VK_FORMAT_UNDEFINED) viewInfo.pNext = &viewUsage; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &created.view), "vkCreateImageView multisampled target"); + } catch (...) { + destroy(created); + 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 (auto& [key, texture] : textures) retiredTextures.push_back(std::move(texture)); + textures.clear(); + retired.push_back({VK_NULL_HANDLE, VK_NULL_HANDLE, std::exchange(twinView, VK_NULL_HANDLE)}); + retired.push_back({image, memory, view}); + image = storable.image; + memory = storable.memory; + view = storable.view; + usage |= VK_IMAGE_USAGE_STORAGE_BIT; + allocationBytes += storable.bytes; + recorder.Keep(shared_from_this(), static_cast(storable.bytes)); + } + + std::shared_ptr FmaskSampled(const GuestTextureResource& resource, VkComponentMapping components) { + const auto snapshot = fmaskSnapshotResource(resource); + Require(target.samples == 8u && target.dccAddress != 0u && fmaskRange.has_value(), "native FMASK sampling requires a tracked eight-fragment DCC target"); + Require(target.tileMode == ColorTileMode::RenderTarget && target.depth == 1u && target.depthSlice == 0u && target.layers == 1u && target.arrayLastSlice == 0u && target.arraySlice == 0u && target.mip == 0u && target.mipCount == 1u, "native FMASK target layout is not implemented"); + Require(target.fmaskAddress == resource.baseAddress && target.fmaskPipeBankXor == resource.pipeBankXor && target.cmaskAddress == resource.fmaskCmaskAddress, "native FMASK descriptor does not match its resident target"); + Require(target.extent.width == resource.width && target.extent.height == resource.height && resource.baseArray == 0u && resource.depthOrLastArray == 0u, "native FMASK extent or subresource does not match its resident target"); + auto* recorder = Recorder::Active(); + Require(recorder != nullptr && recorder->Device() == context.device && !Recorder::InCompletion(), "native FMASK sampling requires its device's active recorder outside completion"); + ApplyFastClear(target); + Keep(); + const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a), static_cast(resource.dimension)}; + auto& texture = fmaskTextures[key]; + if (texture == nullptr) { + const auto geometry = DescribeSurface(snapshot); + std::vector bytes(geometry.guestBytes); + constexpr std::uint32_t expanded = 0x76543210u; + for (std::size_t offset = 0; offset < bytes.size(); offset += sizeof(expanded)) std::memcpy(bytes.data() + offset, &expanded, sizeof(expanded)); + texture = std::make_unique(context, *context.detiler, snapshot, components, bytes); + } + return {shared_from_this(), texture.get()}; + } + + void Keep() { + if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Device() == context.device) { + recorder->Keep(shared_from_this()); + recorder->KeepBytes(this, allocationBytes); + } + } + void ApplyFastClear(const ColorTarget& use) { VkClearColorValue clear{}; - if (!TakeMultisampledClear(cmask, colorFill, use, clear)) return; + const bool refilled = absorbDccFill(use, clear); + checkDccCurrent(use, refilled); + if (!refilled && !TakeMultisampledClear(cmask, colorFill, use, clear)) return; + Keep(); recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; @@ -120,14 +323,142 @@ public: }); } - VkRenderPass ResolvePass() { + void Recreate(const ColorTarget& use) { + SettleStorageWrites(); + target = use; + cmask = MultisampledCmask(use.cmaskAddress, use.cmaskBytes); + colorFill = MultisampledColorFill(use); + fromTexture = false; + dccRanges.clear(); + fmaskRange.reset(); + metadataValid = std::make_shared>(true); + const auto initial = initialDccClear(use); + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &initial, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + }); + } + + void SettleStorageWrites() { + if (!storageWritten) return; + storageWritten = false; + 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(htileRefusal.empty(), htileRefusal); + 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 && recording->Device() == context.device, "a multisampled color target is only written by recorded dispatches on its device"); + Keep(); + 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; + } + + VkImageView OrderView() { + Require(orderFormat != VK_FORMAT_UNDEFINED, "the multisampled color target has no storage view to put its samples in the guest's order"); + if (orderView != VK_NULL_HANDLE) return orderView; + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = orderFormat; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + viewUsage.usage = VK_IMAGE_USAGE_STORAGE_BIT; + viewInfo.pNext = &viewUsage; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &orderView), "vkCreateImageView multisampled target storage"); + return orderView; + } + + void NoteMetadataFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern) { + const auto note = [&](std::uint64_t address, std::uint64_t bytes, std::optional& filled) { + if (address == 0 || fillAddress >= address + bytes || address >= fillAddress + fillBytes) return; + filled = fillAddress <= address && fillAddress + fillBytes >= address + bytes ? std::optional(pattern) : std::nullopt; + }; + note(target.dccAddress, MultisampledDccBytes(target), dccFill); + note(target.cmaskAddress, target.cmaskBytes, cmaskFill); + } + + std::shared_ptr Sampled(VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D) { + Require(sampleable, "the device cannot sample the multisampled color target's format at its sample count"); + Require(cmask.Refusal().empty(), cmask.Refusal()); + Require(htileRefusal.empty(), htileRefusal); + Require(cmask.Keys() != DccKeys::Clear0000, "sampling a multisampled color target whose CMASK fast clear no draw has applied is not implemented"); + auto* recording = Recorder::Active(); + Require(recording != nullptr && recording->Device() == context.device, "a multisampled color target is only sampled by recorded draws and dispatches on its device"); + Keep(); + 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); + Recorder::CountBarriers(Recorder::CommandClass::Draw, 1); + }); + const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a), static_cast(viewType)}; + auto& texture = textures[key]; + if (texture == nullptr) texture = std::make_shared(context, image, target.format, VK_IMAGE_ASPECT_COLOR_BIT, components, viewType, 0, std::shared_ptr{}, orderFormat != VK_FORMAT_UNDEFINED ? VK_IMAGE_USAGE_SAMPLED_BIT : VkImageUsageFlags{0}); + return {shared_from_this(), texture.get()}; + } + + VkImageView View(VkFormat format) { + if (format == target.format) return view; + Require(twinFormat != VK_FORMAT_UNDEFINED && format == twinFormat, "a multisampled color target is only viewed in its own format or its sRGB twin"); + if (twinView == VK_NULL_HANDLE) { + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = twinFormat; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + viewUsage.usage = twinViewUsage; + viewInfo.pNext = &viewUsage; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &twinView), "vkCreateImageView multisampled target twin"); + } + return twinView; + } + + bool Serves(const ColorTarget& use) const { + if (use.format == target.format) return sameTarget(target, use); + auto twin = use; + twin.format = target.format; + return twinFormat != VK_FORMAT_UNDEFINED && use.format == twinFormat && sameTarget(target, twin); + } + + VkRenderPass ResolvePass(VkFormat format) { + auto& resolvePass = resolvePasses[format]; if (resolvePass != VK_NULL_HANDLE) return resolvePass; std::array attachments{}; - attachments[0].format = target.format; + attachments[0].format = format; attachments[0].samples = static_cast(target.samples); attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; - attachments[1].format = target.format; + attachments[1].format = format; attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE; @@ -155,20 +486,152 @@ 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; VkImageView view = VK_NULL_HANDLE; - VkRenderPass resolvePass = VK_NULL_HANDLE; + std::map resolvePasses; MultisampledCmask cmask; MultisampledColorFill colorFill; + bool fromTexture = false; + VkFormat orderFormat = VK_FORMAT_UNDEFINED; + VkImageView orderView = VK_NULL_HANDLE; + std::uint64_t htileAddress = 0; + std::uint64_t htileBytes = 0; + std::string htileRefusal; private: + bool sampleable = false; + VkImageUsageFlags usage = 0; + VkFormat storageFormat = VK_FORMAT_UNDEFINED; + VkImageCreateFlags storageFlags = 0; + VkFormat twinFormat = VK_FORMAT_UNDEFINED; + VkImageView twinView = VK_NULL_HANDLE; + std::vector retired; + VkImageView storageView = VK_NULL_HANDLE; + bool storageWritten = false; + VkDeviceSize allocationBytes = 0; + std::optional fmaskRange; + std::map, std::unique_ptr> fmaskTextures; + std::map, std::shared_ptr> textures; + std::vector> retiredTextures; + std::vector dccRanges; + std::optional dccFill; + std::optional cmaskFill; + + bool absorbDccFill(const ColorTarget& use, VkClearColorValue& clear) { + const auto dcc = std::exchange(dccFill, std::nullopt); + const auto cmaskPattern = std::exchange(cmaskFill, std::nullopt); + if (dccRanges.size() < 2 || !dcc || use.dccAddress != target.dccAddress || use.cmaskAddress != target.cmaskAddress || use.cmaskBytes != target.cmaskBytes) return false; + const auto keys = LandedUniformMetadataByte(dccRanges[1].address, dccRanges[1].bytes); + if (!keys || *dcc != *keys * 0x01010101u) return false; + std::uint8_t cmaskKeys = 0; + if (dccRanges.size() > 2) { + const auto& range = dccRanges[2]; + auto* recorder = Recorder::Active(); + const bool written = !GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || (recorder != nullptr && recorder->PendingWriteOverlaps(range.address, range.bytes)); + const auto landed = LandedUniformMetadataByte(range.address, range.bytes); + if (!landed || (written && (!cmaskPattern || *cmaskPattern != *landed * 0x01010101u))) return false; + cmaskKeys = *landed; + } + std::vector metadata(dccRanges.begin() + 1, dccRanges.end()); + for (auto& range : metadata) { + if (!GuestMemory::Watched(range.address, range.bytes)) return false; + range.generation = collectBlocks(range.address, range.bytes); + if (range.generation == 0) return false; + } + clear = MultisampledDccClear(use, std::vector(dccRanges[1].bytes, std::byte{*keys}), std::vector(use.cmaskBytes, std::byte{cmaskKeys})); + std::copy(metadata.begin(), metadata.end(), dccRanges.begin() + 1); + return true; + } + + VkClearColorValue initialDccClear(const ColorTarget& use) { + if (use.dccAddress == 0) return {}; + 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 = collectBlocks(range.address, range.bytes); + Require(range.generation != 0, "initial multisampled DCC contents have no write generation"); + } + for (std::size_t first = 0; first < dccRanges.size(); ++first) { + 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"); + } + } + if (use.samples == 8u && use.fmaskAddress != 0u && use.cmaskAddress != 0u) { + const auto resource = fmaskResource(use); + static_cast(fmaskSnapshotResource(resource)); + fmaskRange = GuestMemory::UnchangedQuery{use.fmaskAddress, static_cast(DescribeSurface(resource).guestBytes), 0}; + auto& range = *fmaskRange; + GuestMemory::CheckRange(reinterpret_cast(range.address), range.bytes, 65536u); + Require(GuestMemory::Watched(range.address, range.bytes), "native FMASK contents require watched guest memory"); + range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes); + Require(range.generation != 0, "native FMASK contents have no write generation"); + for (const auto& other : dccRanges) Require(range.address >= other.address + other.bytes || other.address >= range.address + range.bytes, "native FMASK overlaps its color or metadata planes"); + } + 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) && (!fmaskRange || GuestMemory::UnchangedSinceAll(std::span(&*fmaskRange, 1u))), "initial multisampled DCC contents changed during import"); + return clear; + } + + void requireDccRange(bool valid, const GuestMemory::UnchangedQuery& range, const char* reason) const { + if (valid) return; + const auto* plane = range.address == target.address ? "color" : range.address == target.dccAddress ? "DCC" : range.address == target.fmaskAddress ? "FMASK" : "CMASK"; + char text[384]; + std::snprintf(text, sizeof(text), "%s (%s range 0x%llx+0x%zx, %u samples)", reason, plane, static_cast(range.address), range.bytes, target.samples); + Require(false, text); + } + + void checkDccCurrent(const ColorTarget& use, bool metadataRefilled) const { + if (dccRanges.empty()) { + Require(use.dccAddress == 0, "adding DCC to a resident multisampled color target is not implemented"); + return; + } + 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"); + const auto ranges = std::span(dccRanges).first(metadataRefilled ? 1u : dccRanges.size()); + for (const auto& range : ranges) requireDccRange(GuestMemory::Watched(range.address, range.bytes), range, "multisampled DCC target reuse requires watched guest memory"); + for (const auto& range : ranges) { + GuestMemory::CollectWritesUncached(range.address, range.bytes); + requireDccRange(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), range, "writes to a resident multisampled DCC target or its metadata are not implemented"); + if (auto* recorder = Recorder::Active(); recorder != nullptr) requireDccRange(!recorder->PendingWriteOverlaps(range.address, range.bytes), range, "pending writes to a resident multisampled DCC target or its metadata are not implemented"); + } + if (fmaskRange) { + Require(use.fmaskAddress == target.fmaskAddress && use.fmaskPipeBankXor == target.fmaskPipeBankXor, "native FMASK target layout changed"); + const auto& range = *fmaskRange; + requireDccRange(GuestMemory::Watched(range.address, range.bytes), range, "native FMASK sampling requires watched guest memory"); + GuestMemory::CollectWritesUncached(range.address, range.bytes); + requireDccRange(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), range, "writes to a resident native FMASK are not implemented"); + if (auto* recorder = Recorder::Active(); recorder != nullptr) requireDccRange(!recorder->PendingWriteOverlaps(range.address, range.bytes), range, "pending writes to a resident native FMASK are not implemented"); + } + } + void release() noexcept { - 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); + fmaskTextures.clear(); + textures.clear(); + retiredTextures.clear(); + for (const auto& [format, resolvePass] : resolvePasses) context.Function("vkDestroyRenderPass")(context.device, resolvePass, nullptr); + if (orderView) context.Function("vkDestroyImageView")(context.device, orderView, nullptr); + if (twinView) context.Function("vkDestroyImageView")(context.device, twinView, nullptr); + if (storageView) context.Function("vkDestroyImageView")(context.device, storageView, nullptr); + destroy({image, memory, view}); + for (const auto& allocation : retired) destroy(allocation); } }; @@ -178,6 +641,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 { @@ -207,19 +671,53 @@ void guardReads(std::uint64_t address, std::size_t bytes) { RequireNoMultisampleTarget(address, bytes, "a CPU read"); } +struct RecordedSingleClear { + std::uint64_t surface = 0; + std::uint32_t pipeBankXor = 0; + std::uint32_t width = 0; + std::uint32_t height = 0; + std::uint32_t samples = 0; + std::array color{}; +}; + +std::mutex& singleClearsMutex() { + static std::mutex mutex; + return mutex; +} + +std::vector& singleClears() { + static std::vector list; + return list; +} + +std::atomic& anySingleClears() { + static std::atomic any{false}; + return any; +} + +void forgetSingleClears(std::uintptr_t address, std::size_t bytes) { + std::lock_guard lock(singleClearsMutex()); + std::erase_if(singleClears(), [&](const RecordedSingleClear& clear) { return clear.surface >= address && clear.surface - address < bytes; }); + anySingleClears().store(!singleClears().empty(), std::memory_order_release); +} + void retireUnmapped(std::uintptr_t address, std::size_t bytes) { + forgetSingleClears(address, bytes); + RetireUnmappedDepth_nid_no_patch(address, bytes); std::unique_lock lock(guardedMutex()); std::erase_if(guarded(), [&](const auto& range) { + 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)) { + if (MultisampledBlockLog2(target.tileMode) == 16u && target.tileMode != ColorTileMode::Standard64KB && std::has_single_bit(target.elementBytes) && std::has_single_bit(target.samples)) { const auto log2Samples = std::countr_zero(target.samples); const auto log2Pixels = 16u - std::countr_zero(target.elementBytes) - log2Samples; const auto log2Width = (log2Pixels + ((log2Samples & 1u) == 0u ? 1u : 0u)) / 2u; @@ -230,12 +728,13 @@ 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}); + if (target.samples == 8u && target.fmaskAddress != 0u && target.cmaskAddress != 0u) guarded().push_back({context.device, target.fmaskAddress, DescribeSurface(fmaskResource(target)).guestBytes, target.samples, active, metadataValid}); + } anyGuarded().store(true, std::memory_order_release); - static std::once_flag installed; - std::call_once(installed, [] { - GuestMemory::SetReadGuard(&guardReads); - }); - GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(&retireUnmapped); + InstallResidentImageReadGuard_nid_no_patch(); } std::mutex& targetsMutex() { @@ -243,27 +742,150 @@ std::mutex& targetsMutex() { return mutex; } -std::vector>& targets() { - static std::vector> list; +std::vector>& targets() { + static std::vector> list; return list; } -MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { +void discardRetiredTargets() { + std::erase_if(targets(), [](const auto& target) { return !target->active->load(std::memory_order_acquire); }); +} + +MultisampleTarget& acquire(const Context& context, const ColorTarget& requested) { + discardRetiredTargets(); 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->context.device != context.device || !target->Serves(requested)) continue; + auto use = requested; + use.format = target->target.format; + if (recreated(target->target, use)) { + { + std::unique_lock lock(guardedMutex()); + std::erase_if(guarded(), [&](const auto& range) { return range.active == target->active; }); + anyGuarded().store(!guarded().empty(), std::memory_order_release); + } + target->Recreate(use); + guard(context, use, target->active, target->metadataValid); + target->ApplyFastClear(use); + return *target; + } + if (target->fromTexture && use.cmaskAddress != 0) { + target->fromTexture = false; + target->cmask = MultisampledCmask(use.cmaskAddress, use.cmaskBytes); + } target->cmask.CheckAddress(use.cmaskAddress); + target->Keep(); + target->SettleStorageWrites(); target->ApplyFastClear(use); return *target; } - targets().push_back(std::make_unique(context, use)); + const auto& use = requested; + auto target = std::make_shared(context, use); + target->Keep(); + targets().push_back(std::move(target)); 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.SeedCode(CurrentUniformMetadataByte(use.cmaskAddress, static_cast(use.cmaskBytes))); created.ApplyFastClear(use); + created.Keep(); return created; } +MultisampleTarget& createFilled(const Context& context, const GuestTextureResource& resource, VkFormat format, const char* access) { + const auto refuse = [&](const std::string& reason) { + 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{}; + switch (resource.tileMode) { + case TextureTileMode::kStandard4KB: use.tileMode = ColorTileMode::Standard4KB; break; + case TextureTileMode::kStandard64KB: use.tileMode = ColorTileMode::Standard64KB; break; + case TextureTileMode::kZ64KBX: use.tileMode = ColorTileMode::Z64KBX; break; + case TextureTileMode::kD64KBX: use.tileMode = ColorTileMode::D64KBX; break; + case TextureTileMode::kR64KBX: use.tileMode = ColorTileMode::RenderTarget; break; + default: refuse("whose tile mode " + std::to_string(static_cast(resource.tileMode)) + " has no modeled multisampled color footprint"); + } + use.address = resource.baseAddress; + use.surfaceAddress = resource.baseAddress; + use.extent = {resource.width, resource.height}; + use.surfaceExtent = use.extent; + use.format = format; + use.elementBytes = BytesPerElement(resource.format); + use.pipeBankXor = resource.pipeBankXor; + use.samples = resource.samples; + const auto bytes = MultisampledColorBytes(use); + if (bytes == 0) refuse("whose " + std::to_string(use.elementBytes) + "-byte elements have no modeled multisampled color footprint"); + use.bytes = ColorTargetLayout(use.extent.width, use.extent.height, use.tileMode, use.elementBytes).Bytes(); + char range[64]; + std::snprintf(range, sizeof(range), "0x%llx+0x%llx", static_cast(use.address), static_cast(bytes)); + RequireNoMultisampleTarget(use.address, static_cast(bytes), "sampling a multisampled texture no draw rendered to"); + GuestMemory::FlushGpuWrites(use.address, static_cast(bytes)); + if (!GuestMemory::Accessible(reinterpret_cast(use.address), static_cast(bytes))) refuse(std::string("whose color memory ") + range + " is not mapped"); + std::uint64_t htileBytes = 0; + bool htileCleared = false; + if (resource.dccAddress != 0) { + if (use.tileMode != ColorTileMode::Z64KBX) refuse("whose compression metadata over a tile mode " + std::to_string(static_cast(resource.tileMode)) + " surface is not modeled"); + htileBytes = HtileMetadataBytes(use.extent); + char htileRange[64]; + std::snprintf(htileRange, sizeof(htileRange), "0x%llx+0x%llx", static_cast(resource.dccAddress), static_cast(htileBytes)); + GuestMemory::FlushGpuWrites(resource.dccAddress, static_cast(htileBytes)); + if (!GuestMemory::Accessible(reinterpret_cast(resource.dccAddress), static_cast(htileBytes))) refuse(std::string("whose HTILE ") + htileRange + " is not mapped"); + std::vector words(static_cast(htileBytes / sizeof(std::uint32_t))); + std::memcpy(words.data(), reinterpret_cast(resource.dccAddress), words.size() * sizeof(std::uint32_t)); + htileCleared = words.front() == 0u; + const auto mixed = std::find_if(words.begin(), words.end(), [&](std::uint32_t word) { return htileCleared ? word != 0u : (word & 15u) != 15u; }); + if (mixed != words.end()) { + char text[96]; + std::snprintf(text, sizeof(text), " holds 0x%08x at +0x%llx after 0x%08x", *mixed, static_cast((mixed - words.begin()) * sizeof(std::uint32_t)), words.front()); + refuse(std::string("whose HTILE ") + htileRange + text + ", which is neither all cleared to 0.0 nor all expanded"); + } + } + const auto* memory = reinterpret_cast(use.address); + std::uint32_t value = 0; + if (!htileCleared) std::memcpy(&value, memory, sizeof(value)); + char held[16]; + std::snprintf(held, sizeof(held), "0x%08x", value); + const auto repeated = htileCleared ? memory + bytes : std::mismatch(memory + sizeof(value), memory + bytes, memory).first; + if (repeated != memory + bytes) { + char offset[32]; + std::snprintf(offset, sizeof(offset), "+0x%llx", static_cast(repeated - memory)); + refuse(std::string("whose color memory ") + range + " does not hold one repeated 32-bit value (" + held + " until " + offset + ")"); + } + VkClearColorValue clear{}; + if (!MultisampledFillClearColor(use, value, clear)) refuse(std::string("whose color memory ") + range + " holds " + held + ", which has no exact clear value in its format"); + auto target = std::make_shared(context, use); + target->Keep(); + targets().push_back(std::move(target)); + auto& created = *targets().back(); + guard(context, use, created.active, created.metadataValid); + created.colorFill.NoteFill(use.address, static_cast(bytes), value); + created.fromTexture = true; + created.htileAddress = resource.dccAddress; + created.htileBytes = htileBytes; + return created; +} + +} + +void InstallResidentImageReadGuard_nid_no_patch() { + static std::once_flag installed; + std::call_once(installed, [] { GuestMemory::SetReadGuard(&guardReads); }); + UpdateResidentImageUnmapObserver_nid_no_patch(); +} + +void UpdateResidentImageUnmapObserver_nid_no_patch() { + static std::mutex mutex; + std::lock_guard lock(mutex); + GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(anyGuarded().load(std::memory_order_acquire) || anySingleClears().load(std::memory_order_acquire) || HasResidentDepthGuards_nid_no_patch() ? &retireUnmapped : nullptr); +} + +std::optional MultisampledCmaskKeys(std::uint8_t code) { + const auto tile = code & 0xfu; + if ((code >> 4u) != tile) return std::nullopt; + if (tile == 0u) return DccKeys::Clear0000; + if ((tile >> 2u) == 3u) return DccKeys::Uncompressed; + return std::nullopt; } void MultisampledCmask::NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern) { @@ -275,24 +897,24 @@ void MultisampledCmask::NoteFill(std::uint64_t fillAddress, std::size_t fillByte return; } refusal.clear(); - if (pattern == 0) { - keys = DccKeys::Clear0000; - } else if (pattern == 0xffffffffu) { - keys = DccKeys::Uncompressed; + const auto code = static_cast(pattern); + if (const auto filled = MultisampledCmaskKeys(code); filled && pattern == code * 0x01010101u) { + keys = *filled; } else { - char text[192]; - std::snprintf(text, sizeof(text), "a fill of the CMASK 0x%llx+0x%llx of a multisampled color target with 0x%08x leaves its tiles mixed (neither fast-cleared 0 nor expanded 0xffffffff)", static_cast(address), static_cast(bytes), pattern); + char text[224]; + std::snprintf(text, sizeof(text), "a fill of the CMASK 0x%llx+0x%llx of a multisampled color target with 0x%08x leaves its tiles mixed (neither a register color clear 0 nor a color-expanded code such as 0xcc or 0xff)", static_cast(address), static_cast(bytes), pattern); refusal = text; } } -void MultisampledCmask::SeedKeys(DccKeys seeded) { - if (seeded == DccKeys::Clear0000 || seeded == DccKeys::Uncompressed) { - keys = seeded; +void MultisampledCmask::SeedCode(std::optional code) { + if (const auto seeded = code ? MultisampledCmaskKeys(*code) : std::nullopt) { + keys = *seeded; return; } char text[192]; - std::snprintf(text, sizeof(text), "the CMASK 0x%llx of a multisampled color target reads as %s, which is not modeled", static_cast(address), DccKeysName(seeded)); + if (code) std::snprintf(text, sizeof(text), "the CMASK 0x%llx of a multisampled color target reads as code 0x%02x, which is not modeled", static_cast(address), *code); + else std::snprintf(text, sizeof(text), "the CMASK 0x%llx of a multisampled color target reads as mixed, which is not modeled", static_cast(address)); refusal = text; } @@ -311,7 +933,7 @@ bool MultisampledCmask::TakeClear() { } std::uint64_t MultisampledColorBytes(const ColorTarget& target) { - const auto blockLog2 = target.tileMode == ColorTileMode::Standard4KB ? 12u : target.tileMode == ColorTileMode::Standard64KB || target.tileMode == ColorTileMode::RenderTarget ? 16u : 0u; + const auto blockLog2 = MultisampledBlockLog2(target.tileMode); if (blockLog2 == 0 || !std::has_single_bit(target.elementBytes) || !std::has_single_bit(target.samples)) return 0; const auto samplesLog2 = static_cast(std::countr_zero(target.samples)); const auto elementsLog2 = blockLog2 - static_cast(std::countr_zero(target.elementBytes)) - samplesLog2; @@ -321,6 +943,101 @@ 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); +} + +namespace { + +std::optional> recordedSingleClear(const ColorTarget& target) { + std::lock_guard lock(singleClearsMutex()); + for (const auto& clear : singleClears()) { + if (clear.surface == target.surfaceAddress && clear.pipeBankXor == target.pipeBankXor && clear.width == target.extent.width && clear.height == target.extent.height && clear.samples == target.samples) return clear.color; + } + return std::nullopt; +} + +VkClearColorValue singleClearColor(const ColorTarget& target) { + const auto color = recordedSingleClear(target); + Require(color.has_value(), "multisampled DCC comp-to-single keys without a recorded clear color are not implemented"); + const auto elementBytes = static_cast(target.elementBytes); + Require(elementBytes == 1 || elementBytes == 2 || elementBytes == 4 || elementBytes == 8 || elementBytes == 16, "multisampled DCC comp-to-single clears of this texel size are not implemented"); + std::array pattern{}; + for (std::size_t offset = 0; offset < sizeof(pattern); offset += elementBytes) std::memcpy(reinterpret_cast(pattern.data()) + offset, color->data(), elementBytes); + VkClearColorValue clear{}; + Require(ClearColorForTexel(target.format, target.elementBytes, pattern, clear), "multisampled DCC comp-to-single clear has no exact host value"); + return clear; +} + +} + +std::optional MatchMultisampledSingleClear(std::span code, std::span userData, const std::array& numThreads) { + static constexpr std::array kernel{0xbe800303u, 0xbe830382u, 0xf40c0401u, 0xfa000000u, 0xd7460000u, 0x04010609u, 0xd7460001u, 0x0405060au, 0x7e060208u, 0x7e0c020bu, 0x7e0e0280u, 0xd5690004u, 0x02000100u, 0x7e000205u, 0xd5690005u, 0x02000901u, 0xd7000001u, 0x02000c80u, 0xbf8cc07fu, 0xf0201f38u, 0x00040004u, 0xbf810000u}; + if (userData.size() < 9 || numThreads != std::array{8u, 8u, 1u}) return std::nullopt; + if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt; + if (userData[3] == 0 || userData[4] == 0) return std::nullopt; + return MultisampledSingleClear{userData[2] | (std::uint64_t{2} << 32u), {userData[5], userData[6], userData[7], userData[8]}}; +} + +void NoteMultisampledSingleClear(const GuestTextureResource& resource, const std::array& color) { + { + std::lock_guard lock(singleClearsMutex()); + auto& list = singleClears(); + std::erase_if(list, [&](const RecordedSingleClear& clear) { return clear.surface == resource.baseAddress; }); + list.push_back({resource.baseAddress, resource.pipeBankXor, resource.width, resource.height, resource.samples, color}); + anySingleClears().store(true, std::memory_order_release); + } + UpdateResidentImageUnmapObserver_nid_no_patch(); +} + +bool TakeMultisampledSingleClear(std::span code, std::span userData, const std::array& numThreads) { + const auto clear = MatchMultisampledSingleClear(code, userData, numThreads); + if (!clear) return false; + std::array words{}; + if (!GuestMemory::Accessible(reinterpret_cast(clear->descriptor), sizeof(words))) return false; + GuestMemory::Read(clear->descriptor, std::as_writable_bytes(std::span(words))); + const auto resource = DecodeTextureResource(words); + if (resource.samples < 2) return false; + NoteMultisampledSingleClear(resource, clear->color); + return true; +} + +VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span dcc, std::span cmask) { + Require(target.dccAddress != 0 && (target.dccControl & 0x3ff80u) == 0 && (target.pipeBankXor & ~0xcf00u) == 0, "multisampled DCC address, color transform, lossy mode or XOR is not implemented"); + Require(dcc.size() == MultisampledDccBytes(target), "multisampled DCC metadata snapshot size mismatch"); + 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"); + } + if (dcc.front() == std::byte{0x10}) return singleClearColor(target); + 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; @@ -375,20 +1092,116 @@ bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colo return true; } -VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target) { +SampleOrderTarget MultisampleTargetOrderTarget(const Context& context, const ColorTarget& target) { std::lock_guard lock(targetsMutex()); - return acquire(context, target).view; + auto& found = acquire(context, target); + return {found.image, found.OrderView(), found.orderFormat, target.extent, target.samples, SampleOrderMethod::Storage, found.shared_from_this()}; +} + +VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, VkImage* image) { + std::lock_guard lock(targetsMutex()); + auto& acquired = acquire(context, target); + if (image != nullptr) *image = acquired.image; + return acquired.View(target.format); +} + +VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, std::shared_ptr& owner, VkImage* image) { + std::lock_guard lock(targetsMutex()); + auto& acquired = acquire(context, target); + owner = acquired.shared_from_this(); + if (image != nullptr) *image = acquired.image; + return acquired.View(target.format); +} + +void KeepMultisampleTarget_nid_no_patch(const std::shared_ptr& owner) { + std::static_pointer_cast(owner)->Keep(); +} + +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, VkImageViewType viewType) { + std::lock_guard lock(targetsMutex()); + discardRetiredTargets(); + 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; + 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, viewType); + if (sameShape && redBlueSwapped(rendered.format, format)) return target->Sampled(swapRedBlue(components), viewType); + 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, "sampling").Sampled(components, viewType); + } + 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)); + throw std::runtime_error(text); +} + +void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget& target) { + std::lock_guard lock(targetsMutex()); + acquire(context, target); +} + +VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource) { + std::lock_guard lock(targetsMutex()); + discardRetiredTargets(); + 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); - const std::array views{target.view, destinationView}; - auto framebuffer = std::make_shared(context, target.ResolvePass(), views, source.extent); + target.Keep(); + const std::array views{target.View(source.format), destinationView}; + auto framebuffer = std::make_shared(context, target.ResolvePass(source.format), views, source.extent); recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; - begin.renderPass = target.ResolvePass(); + begin.renderPass = target.ResolvePass(source.format); begin.framebuffer = framebuffer->Handle(); begin.renderArea = {{0, 0}, source.extent}; context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); @@ -416,6 +1229,20 @@ void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32 for (const auto& target : targets()) { if (!target->active->load(std::memory_order_acquire)) continue; target->colorFill.NoteFill(address, bytes, pattern); + if (target->htileBytes != 0 && address < target->htileAddress + target->htileBytes && target->htileAddress < address + bytes) { + if (pattern == 0u && address <= target->htileAddress && address + bytes >= target->htileAddress + target->htileBytes) { + target->htileRefusal.clear(); + target->colorFill.NoteFill(target->target.address, static_cast(MultisampledColorBytes(target->target)), 0u); + } else { + char text[224]; + std::snprintf(text, sizeof(text), "a fill of 0x%llx+0x%zx with 0x%08x over the HTILE 0x%llx+0x%llx of a sampled multisampled depth-layout texture is not modeled", static_cast(address), bytes, pattern, static_cast(target->htileAddress), static_cast(target->htileBytes)); + target->htileRefusal = text; + } + } + if (target->target.dccAddress != 0) { + target->NoteMetadataFill(address, bytes, pattern); + continue; + } const auto before = target->cmask.Keys(); target->cmask.NoteFill(address, bytes, pattern); if (before == DccKeys::Clear0000 || target->cmask.Keys() != DccKeys::Clear0000) continue; @@ -424,16 +1251,35 @@ void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32 } } +std::shared_ptr MultisampleTargetFmaskTexture_nid_no_patch(const Context& context, const GuestTextureResource& resource, VkComponentMapping components) { + std::lock_guard lock(targetsMutex()); + discardRetiredTargets(); + std::shared_ptr owner; + for (const auto& candidate : targets()) { + if (!candidate->active->load(std::memory_order_acquire) || candidate->context.device != context.device) continue; + const auto& target = candidate->target; + if (target.fmaskAddress != resource.baseAddress && (resource.fmaskCmaskAddress == 0u || target.cmaskAddress != resource.fmaskCmaskAddress)) continue; + Require(owner == nullptr, "FMASK descriptor refers to more than one resident target"); + owner = candidate; + } + return owner != nullptr ? owner->FmaskSampled(resource, components) : nullptr; +} + void ClearMultisampleTargets(VkDevice device) { std::lock_guard lock(targetsMutex()); std::erase_if(targets(), [&](const auto& target) { return target->context.device == device; }); std::unique_lock guardLock(guardedMutex()); std::erase_if(guarded(), [&](const auto& range) { return range.device == device; }); anyGuarded().store(!guarded().empty(), std::memory_order_release); - if (guarded().empty()) GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(nullptr); + UpdateResidentImageUnmapObserver_nid_no_patch(); } void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) { + RequireNoResidentDepthRead_nid_no_patch(address, bytes); + RequireNoMultisampleColorTarget(address, bytes, access); +} + +void RequireNoMultisampleColorTarget(std::uint64_t address, std::size_t bytes, const char* access) { if (bytes == 0 || !anyGuarded().load(std::memory_order_acquire)) return; std::shared_lock lock(guardedMutex()); for (const auto& range : guarded()) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTextureLayout.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTextureLayout.cpp new file mode 100644 index 0000000000..96a5cbc02a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTextureLayout.cpp @@ -0,0 +1,120 @@ +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleSwizzleEquations.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include + +namespace AgcDriver::Graphics { +namespace { + +std::array inverseBlock(const MultisampleSwizzleEquation& equation, std::uint32_t byteBits, std::uint32_t fragmentBits, std::uint32_t widthBits) { + std::array rows{}, inverse{}; + for (std::uint32_t input = 0; input < 16u; ++input) { + std::uint32_t address = 0u; + if (input < byteBits) address = 1u << input; + else { + const auto coordinateBit = input < byteBits + fragmentBits ? 48u + input - byteBits : input < byteBits + fragmentBits + widthBits ? input - byteBits - fragmentBits : 16u + input - byteBits - fragmentBits - widthBits; + const auto coordinate = std::uint64_t{1} << coordinateBit; + for (std::uint32_t output = 0; output < 16u; ++output) address |= static_cast(std::popcount(coordinate & equation.bits[output]) & 1u) << output; + } + for (std::uint32_t output = 0; output < 16u; ++output) rows[output] |= ((address >> output) & 1u) << input; + inverse[input] = 1u << input; + } + for (std::uint32_t input = 0; input < 16u; ++input) { + auto pivot = input; + while (pivot < 16u && (rows[pivot] & (1u << input)) == 0u) ++pivot; + Require(pivot < 16u, "multisample block address equation is not invertible"); + std::swap(rows[input], rows[pivot]); + std::swap(inverse[input], inverse[pivot]); + for (std::uint32_t row = 0; row < 16u; ++row) { + if (row == input || (rows[row] & (1u << input)) == 0u) continue; + rows[row] ^= rows[input]; + inverse[row] ^= inverse[input]; + } + } + return inverse; +} + +std::vector axisOffsets(const MultisampleSwizzleEquation& equation, std::uint32_t count, std::uint32_t shift) { + std::vector offsets(count); + for (std::uint32_t value = 0; value < count; ++value) { + const auto coordinate = static_cast(value) << shift; + for (std::uint32_t bit = 0; bit < equation.bits.size(); ++bit) { + offsets[value] |= static_cast(std::popcount(coordinate & equation.bits[bit]) & 1u) << bit; + } + } + return offsets; +} + +} + +MultisampleTextureLayout::MultisampleTextureLayout(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t fragments, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t pipeBankXor) : elementBytes(elementBytes), fragments(fragments), width(width), height(height), layers(layers), pipeBankXor(pipeBankXor) { + Require(width != 0 && height != 0 && width <= 16384u && height <= 16384u && layers != 0 && layers <= 8192u, "invalid multisampled surface extent or layer count"); + Require(std::has_single_bit(elementBytes) && elementBytes <= 16u, "unsupported multisampled surface element size"); + Require(fragments == 2u || fragments == 4u || fragments == 8u, "unsupported multisampled surface fragment count"); + Require((pipeBankXor & ~0xcf00u) == 0, "multisampled surface pipe/bank XOR uses bits outside its 16-pipe swizzle"); + Require(tileMode != TextureTileMode::kZ64KBX || elementBytes <= 4u, "multisampled Z surface element size exceeds 32 bits"); + const auto* equation = FindMultisampleSwizzleEquation(tileMode, elementBytes, fragments); + Require(equation != nullptr, "unsupported multisampled surface tile mode"); + + const auto fragmentBits = static_cast(std::countr_zero(fragments)); + const auto elementBits = 16u - static_cast(std::countr_zero(elementBytes)) - fragmentBits; + const auto widthBits = (elementBits + ((fragmentBits & 1u) == 0 ? 1u : 0u)) / 2u; + blockWidth = 1u << widthBits; + blockHeight = 1u << (elementBits - widthBits); + inverseBlockEquation = inverseBlock(*equation, static_cast(std::countr_zero(elementBytes)), fragmentBits, widthBits); + pitch = (width + blockWidth - 1u) / blockWidth * blockWidth; + paddedHeight = (height + blockHeight - 1u) / blockHeight * blockHeight; + sliceBytes = static_cast(pitch) * paddedHeight * fragments * elementBytes; + Require(sliceBytes <= std::numeric_limits::max() / layers, "multisampled surface size overflows"); + xOffsets = axisOffsets(*equation, width, 0u); + yOffsets = axisOffsets(*equation, height, 16u); + layerOffsets = axisOffsets(*equation, layers, 32u); + fragmentOffsets = axisOffsets(*equation, fragments, 48u); +} + +std::uint64_t MultisampleTextureLayout::offset(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t fragment) const { + const auto block = static_cast(y / blockHeight) * (pitch / blockWidth) + x / blockWidth; + const auto withinBlock = xOffsets[x] ^ yOffsets[y] ^ layerOffsets[layer] ^ fragmentOffsets[fragment] ^ pipeBankXor; + return static_cast(layer) * sliceBytes + block * Alignment + withinBlock; +} + +std::uint64_t MultisampleTextureLayout::Offset(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t fragment) const { + Require(x < width && y < height && layer < layers && fragment < fragments, "multisampled surface coordinate is outside its extent"); + return offset(x, y, layer, fragment); +} + +void MultisampleTextureLayout::transfer(std::span source, std::span destination, bool retile) const { + const auto sourceBytes = retile ? LinearBytes() : Bytes(); + const auto destinationBytes = retile ? Bytes() : LinearBytes(); + Require(source.size() >= sourceBytes && destination.size() >= destinationBytes, "multisampled surface transfer buffer is too small"); + const auto sourceAddress = reinterpret_cast(source.data()); + const auto destinationAddress = reinterpret_cast(destination.data()); + Require(sourceAddress <= std::numeric_limits::max() - sourceBytes && destinationAddress <= std::numeric_limits::max() - destinationBytes, "multisampled surface transfer range overflows"); + Require(sourceAddress + sourceBytes <= destinationAddress || destinationAddress + destinationBytes <= sourceAddress, "multisampled surface transfer buffers overlap"); + + std::uint64_t linear = 0; + for (std::uint32_t layer = 0; layer < layers; ++layer) { + for (std::uint32_t y = 0; y < height; ++y) { + for (std::uint32_t x = 0; x < width; ++x) { + for (std::uint32_t fragment = 0; fragment < fragments; ++fragment) { + const auto tiled = offset(x, y, layer, fragment); + std::memcpy(destination.data() + (retile ? tiled : linear), source.data() + (retile ? linear : tiled), elementBytes); + linear += elementBytes; + } + } + } + } +} + +void MultisampleTextureLayout::Detile(std::span source, std::span destination) const { + transfer(source, destination, false); +} + +void MultisampleTextureLayout::Tile(std::span source, std::span destination) const { + transfer(source, destination, true); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleUpload.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleUpload.cpp new file mode 100644 index 0000000000..419ed7648e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleUpload.cpp @@ -0,0 +1,68 @@ +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/MultisampleUpload_spv.h" +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +VkPipeline TextureDetiler::multisampleUploadPipeline(std::uint32_t elementBytes) { + const auto key = (1u << 19u) | elementBytes; + for (const auto& entry : pipelines) if (entry.first == key) return entry.second; + if (multisampleUploadModule == VK_NULL_HANDLE) { + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = sizeof(MULTISAMPLE_UPLOAD_SPV); + info.pCode = MULTISAMPLE_UPLOAD_SPV; + Check(context.Function("vkCreateShaderModule")(context.device, &info, nullptr, &multisampleUploadModule), "vkCreateShaderModule multisample upload"); + } + const VkSpecializationMapEntry entry{0u, 0u, sizeof(elementBytes)}; + const VkSpecializationInfo specialization{1u, &entry, sizeof(elementBytes), &elementBytes}; + VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + info.stage.module = multisampleUploadModule; + info.stage.pName = "main"; + info.stage.pSpecializationInfo = &specialization; + info.layout = imagePipelineLayout; + pipelines.reserve(pipelines.size() + 1u); + VkPipeline result = VK_NULL_HANDLE; + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1u, &info, nullptr, &result), "vkCreateComputePipelines multisample upload"); + } + pipelines.emplace_back(key, result); + return result; +} + +void TextureDetiler::DispatchMultisampleImage(VkCommandBuffer commands, std::uint32_t elementBytes, VkBuffer source, std::uint64_t offset, std::uint64_t bytes, VkImageView arrayView, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t fragments, std::uint32_t firstElement, std::uint32_t elements) { + Require(context.storageImageMultisample && context.storageImageWriteWithoutFormat, "device does not enable multisampled storage image writes without format"); + Require(commands != VK_NULL_HANDLE && source != VK_NULL_HANDLE && arrayView != VK_NULL_HANDLE, "multisample upload requires commands, buffer and array view"); + Require(std::has_single_bit(elementBytes) && elementBytes <= 16u, "multisample upload element size is unsupported"); + Require(width != 0u && width <= 16384u && height != 0u && height <= 16384u && layers != 0u && layers <= 8192u, "multisample upload extent is unsupported"); + Require(fragments == 2u || fragments == 4u || fragments == 8u, "multisample upload fragment count is unsupported"); + const auto total = static_cast(width) * height * layers * fragments; + Require(total <= std::numeric_limits::max() && elements != 0u && static_cast(firstElement) + elements <= total, "multisample upload element range is invalid"); + const auto required = (static_cast(elements) * elementBytes + 3u) & ~3ull; + const auto alignment = std::max(4u, context.limits.minStorageBufferOffsetAlignment); + Require(offset % alignment == 0u && bytes >= required && bytes <= context.limits.maxStorageBufferRange && bytes % 4u == 0u && offset <= std::numeric_limits::max() - bytes, "multisample upload buffer range is invalid"); + Require(context.limits.maxComputeWorkGroupInvocations >= 64u && context.limits.maxComputeWorkGroupSize[0] >= 64u && context.limits.maxComputeWorkGroupSize[1] >= 1u && context.limits.maxComputeWorkGroupSize[2] >= 1u && context.limits.maxComputeWorkGroupCount[2] >= 1u, "device does not support the multisample upload workgroup"); + const auto groups = (static_cast(elements) + 63u) / 64u; + const auto groupsX = std::min(groups, context.limits.maxComputeWorkGroupCount[0]); + Require(groupsX != 0u, "device has no multisample upload workgroups"); + const auto groupsY = (groups + groupsX - 1u) / groupsX; + Require(groupsY <= context.limits.maxComputeWorkGroupCount[1] && groupsX * groupsY * 64u <= std::numeric_limits::max(), "multisample upload dispatch exceeds the device limits"); + const auto target = multisampleUploadPipeline(elementBytes); + const auto set = allocateSet(true); + const VkDescriptorBufferInfo buffer{source, offset, bytes}; + const VkDescriptorImageInfo image{VK_NULL_HANDLE, arrayView, VK_IMAGE_LAYOUT_GENERAL}; + const std::array writes{{{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, set, 0u, 0u, 1u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, nullptr, &buffer, nullptr}, {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, set, 1u, 0u, 1u, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, &image, nullptr, nullptr}}}; + context.Function("vkUpdateDescriptorSets")(context.device, 2u, writes.data(), 0u, nullptr); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, target); + context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, imagePipelineLayout, 0u, 1u, &set, 0u, nullptr); + const std::array push{width, height, layers, fragments, firstElement, elements, static_cast(groupsX * 64u)}; + context.Function("vkCmdPushConstants")(commands, imagePipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0u, sizeof(push), push.data()); + context.Function("vkCmdDispatch")(commands, static_cast(groupsX), static_cast(groupsY), 1u); +} + +} 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/PassHazards.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PassHazards.cpp index a3a4c97382..58673799a3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PassHazards.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PassHazards.cpp @@ -34,15 +34,16 @@ bool PassHazards::overlapsAny(const GuestRangeSet& set, GuestRangeList ranges) c PassHazard PassHazards::Check(const PassAccess& access, bool samePass) const { const auto touchesMemory = [](const PassAccess& use) { const auto nonEmpty = [](GuestRangeList ranges) { return std::any_of(ranges.begin(), ranges.end(), [](const auto& range) { return range.first < range.second; }); }; - return use.anyReads || use.anyWrites || nonEmpty(use.reads) || nonEmpty(use.writes); + return use.anyReads || use.anyWrites || nonEmpty(use.reads) || nonEmpty(use.writes) || nonEmpty(use.writeOnly); }; - const bool earlierTouches = anyReads || anyWrites || !reads.Empty() || !writes.Empty(); + const bool earlierTouches = anyReads || anyWrites || !reads.Empty() || !writes.Empty() || !writeOnly.Empty(); if ((access.anyWrites && earlierTouches) || (anyWrites && touchesMemory(access))) return PassHazard::UnknownWrite; - const bool writesRanges = std::any_of(access.writes.begin(), access.writes.end(), [](const auto& range) { return range.first < range.second; }); - if ((access.anyReads && !writes.Empty()) || (anyReads && writesRanges)) return PassHazard::UnknownRead; - if (overlapsAny(writes, access.reads)) return PassHazard::ReadAfterWrite; - if (overlapsAny(writes, access.writes)) return PassHazard::WriteAfterWrite; - if (overlapsAny(reads, access.writes)) return PassHazard::WriteAfterRead; + const auto nonEmpty = [](GuestRangeList ranges) { return std::any_of(ranges.begin(), ranges.end(), [](const auto& range) { return range.first < range.second; }); }; + const bool writesRanges = nonEmpty(access.writes) || nonEmpty(access.writeOnly); + if ((access.anyReads && (!writes.Empty() || !writeOnly.Empty())) || (anyReads && writesRanges)) return PassHazard::UnknownRead; + if (overlapsAny(writes, access.reads) || overlapsAny(writeOnly, access.reads)) return PassHazard::ReadAfterWrite; + if (overlapsAny(writes, access.writes) || overlapsAny(writeOnly, access.writes) || overlapsAny(writes, access.writeOnly)) return PassHazard::WriteAfterWrite; + if (overlapsAny(reads, access.writes) || overlapsAny(reads, access.writeOnly)) return PassHazard::WriteAfterRead; const auto conflicts = [](bool earlier, bool later) { return earlier || later; }; for (const auto& [image, written] : access.images) { if (image == VK_NULL_HANDLE) continue; @@ -60,6 +61,7 @@ PassHazard PassHazards::Check(const PassAccess& access, bool samePass) const { void PassHazards::Add(const PassAccess& access) { for (const auto& [begin, end] : access.reads) reads.Insert(begin, end); for (const auto& [begin, end] : access.writes) writes.Insert(begin, end); + for (const auto& [begin, end] : access.writeOnly) writeOnly.Insert(begin, end); for (const auto& [image, written] : access.images) { if (image == VK_NULL_HANDLE) continue; const auto it = std::find_if(images.begin(), images.end(), [&](const auto& seen) { return seen.first == image; }); @@ -76,16 +78,22 @@ void PassHazards::Add(const PassAccess& access) { anyWrites = anyWrites || access.anyWrites; } +void PassHazards::SealWriteOnly() { + for (const auto& [begin, end] : writeOnly.Ranges()) writes.Insert(begin, end); + writeOnly.Clear(); +} + void PassHazards::Clear() { reads.Clear(); writes.Clear(); + writeOnly.Clear(); images.clear(); attachments.clear(); anyReads = anyWrites = false; } bool PassHazards::Empty() const { - return reads.Empty() && writes.Empty() && images.empty() && attachments.empty() && !anyReads && !anyWrites; + return reads.Empty() && writes.Empty() && writeOnly.Empty() && images.empty() && attachments.empty() && !anyReads && !anyWrites; } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index e010e98c22..ba31cb17a8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -1,12 +1,17 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #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/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include +#include #include #include #include @@ -94,6 +99,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) { @@ -103,21 +112,34 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp } return; } + for (const auto& shader : shaders) { + if (shader.stage != ShaderRecompiler::ShaderStage::Fragment || shader.program == nullptr) continue; + Require(std::none_of(shader.program->fragmentParameters.begin(), shader.program->fragmentParameters.end(), [](const ShaderRecompiler::FragmentParameter& parameter) { return parameter.flatP20; }), "v_interp_mov p20 of a flat input with the last provoking vertex is not measured"); + } + if (BarycentricPatches(state, shaders)) { + for (const auto& shader : shaders) { + if (shader.stage != ShaderRecompiler::ShaderStage::Fragment || shader.program == nullptr) continue; + Require(std::none_of(shader.program->fragmentParameters.begin(), shader.program->fragmentParameters.end(), [](const ShaderRecompiler::FragmentParameter& parameter) { return parameter.flat && parameter.perVertex; }), "last provoking vertex with explicit per-vertex flat interpolation is unsupported"); + } + return; + } Require(context.provokingVertexLast, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); - 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"); + const bool meshPrimitives = state.stages.path == ShaderPath::Geometry && state.stages.mesh; + Require(!state.stages.tessellation && (meshPrimitives || (state.stages.path == ShaderPath::Vertex && !state.stages.mesh)), "last provoking vertex is unsupported for generated primitive pipelines"); + Require(!meshPrimitives || state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for triangle fans assembled for a geometry program"); + Require(state.rectList || state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for this primitive topology"); for (const auto& shader : shaders) { if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue; Require(shader.program != nullptr, "missing compiled fragment shader"); Require(std::none_of(shader.program->fragmentParameters.begin(), shader.program->fragmentParameters.end(), [](const ShaderRecompiler::FragmentParameter& parameter) { return parameter.flat && parameter.perVertex; }), "last provoking vertex with explicit per-vertex flat interpolation is unsupported"); - Require(state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || std::none_of(shader.program->fragmentParameters.begin(), shader.program->fragmentParameters.end(), [](const ShaderRecompiler::FragmentParameter& parameter) { return parameter.perVertex; }), "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported"); + Require(state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || std::none_of(shader.program->fragmentParameters.begin(), shader.program->fragmentParameters.end(), [](const ShaderRecompiler::FragmentParameter& parameter) { return parameter.vertexOrder; }), "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported"); } } bool ValidateSampleLocations(const Context& context, const State& state) { const bool extensionSupportsSamples = (context.sampleLocationSampleCounts & state.samples) != 0; Require(!state.customSampleLocations || extensionSupportsSamples, "custom sample locations require VK_EXT_sample_locations support for the draw's sample count"); - const bool enabled = extensionSupportsSamples && (!state.depth || SampleLocationsCompatibleDepth(context, 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; } @@ -157,6 +179,21 @@ bool ShadersWriteMemory(std::span shaders) { return false; } +void ValidateSampleMask(const Context& context, const State& state, std::span shaders) { + if (!PartialSampleMask(state)) return; + const bool uniform = UniformSampleMask(state) && !SampleMaskFollowsOrder(state, HostSampleLocationsFor(context, state).order); + for (const auto& shader : shaders) { + if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue; + Require(shader.program != nullptr, "missing compiled fragment shader"); + if (uniform) { + Require(context.earlyFragmentSampleMaskTestBeforeSampleCounting || !DeclaresEarlyFragmentTests(*shader.program), "a sample mask with early fragment tests needs VkPhysicalDeviceMaintenance5Properties::earlyFragmentSampleMaskTestBeforeSampleCounting, which the device lacks"); + continue; + } + const bool tested = state.depth.has_value() && (state.depthTest || state.stencilTest || state.depthBoundsTest); + Require(!state.pixelEarlyZ || !tested || !ShadersWriteMemory(std::span(&shader, 1)), "a sample mask that differs between the pixels of a quad, from a pixel program with early depth or stencil tests that writes memory, is unsupported: the masked pixels are removed in the shader, after the tests"); + } +} + namespace { thread_local bool prebuilding = false; @@ -175,13 +212,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; @@ -191,6 +230,11 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& } } } + if (resources.split) { + const auto unused = 3u & ~(keys.preRasterizationSets | keys.fragmentSets); + keys.preRasterizationSets |= unused & 1u; + keys.fragmentSets |= unused & 2u; + } for (const auto& binding : input.bindings) { appendKey(keys.vertexInput, binding.binding); appendKey(keys.vertexInput, binding.stride); @@ -206,6 +250,7 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& appendKey(keys.vertexInput, state.topology); appendKey(keys.vertexInput, state.primitiveRestart); appendKey(keys.preRasterization, state.provokingVertexMode); + appendKey(keys.preRasterization, state.polygonMode); appendKey(keys.preRasterization, state.negativeOneToOne); appendKey(keys.preRasterization, state.depthClamp && context.depthClamp); appendKey(keys.preRasterization, state.conservativeRasterization); @@ -214,15 +259,26 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& if (state.stages.tessellation) appendKey(keys.preRasterization, state.stages.tessellation->inputControlPoints); appendKey(keys.fragmentShader, state.samples); appendKey(keys.fragmentOutput, state.samples); + appendKey(keys.fragmentShader, state.perSampleShading); + appendKey(keys.fragmentOutput, state.perSampleShading); + appendKey(keys.fragmentShader, state.mergedCoverage); + appendKey(keys.fragmentOutput, state.mergedCoverage); + appendKey(keys.fragmentShader, state.truncatedCoverage); + appendKey(keys.fragmentOutput, state.truncatedCoverage); const auto sampleLocationsEnabled = ValidateSampleLocations(context, state); appendKey(keys.fragmentShader, sampleLocationsEnabled); appendKey(keys.fragmentOutput, sampleLocationsEnabled); appendKey(keys.fragmentShader, state.customSampleLocations); appendKey(keys.fragmentOutput, state.customSampleLocations); if (state.customSampleLocations) { - for (std::uint32_t sample = 0; sample < state.samples; ++sample) { - appendKey(keys.fragmentShader, state.sampleLocations[sample]); - appendKey(keys.fragmentOutput, state.sampleLocations[sample]); + appendKey(keys.fragmentShader, state.sampleLocationGrid); + appendKey(keys.fragmentOutput, state.sampleLocationGrid); + const auto grid = SampleLocationGrid(state); + for (std::uint32_t pixel = 0; pixel < grid.width * grid.height; ++pixel) { + for (std::uint32_t sample = 0; sample < state.samples; ++sample) { + appendKey(keys.fragmentShader, state.sampleLocations[pixel * 8u + sample]); + appendKey(keys.fragmentOutput, state.sampleLocations[pixel * 8u + sample]); + } } } for (const auto& blend : state.blends) appendKey(keys.fragmentOutput, blend); @@ -234,6 +290,14 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& } appendKey(keys.renderPass, state.depth.has_value()); if (state.depth) appendKey(keys.renderPass, state.depth->format); + appendKey(keys.fragmentShader, HostSampleMask(state)); + appendKey(keys.fragmentOutput, HostSampleMask(state)); + for (auto* key : {&keys.fragmentShader, &keys.fragmentOutput}) { + appendKey(*key, context.sampleLocationSampleCounts); + appendKey(*key, context.sampleLocationGridSampleCounts); + appendKey(*key, context.storageImageMultisample); + appendKey(*key, context.shaderStencilExport); + } if (!resources.split) { appendKey(keys.layout, pushStagesOf(context)); for (const auto word : resources.mainKey) appendKey(keys.layout, word); @@ -266,6 +330,9 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) { PerformanceTimer timing("Vulkan.GraphicsPipeline"); sampleLocationsEnabled = ValidateSampleLocations(context, state); + const auto host = HostSampleLocationsFor(context, state); + sampleLocationGrid = host.grid; + sampleOrder = host.order; // A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep // the buffer pool, which is reset with the device, alive past it. this->context.bufferPool.reset(); @@ -276,10 +343,17 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput Require(!depthBounds || context.depthBounds, "device does not support the depth bounds test"); Require(!depthBias || state.depthBiasClamp == 0.0f || context.depthBiasClamp, "device does not support depth bias clamping"); Require(!state.dualSourceBlend || (context.dualSrcBlend && state.blends.size() <= context.limits.maxFragmentDualSrcAttachments), "device does not support dual-source blending into this many attachments"); + Require(state.reversedBlend == 0 || (context.dualSrcBlend && state.blends.size() <= context.limits.maxFragmentDualSrcAttachments), "blending into a color target with a reversed component order through a second source color needs dual-source blending, which the device does not support"); Require(!state.negativeOneToOne || context.depthClipControl, "negative-one-to-one depth clipping requires VK_EXT_depth_clip_control with depthClipControl enabled"); + Require(!state.perSampleShading || context.sampleRateShading, "per-sample pixel shading requires the sampleRateShading device feature"); + Require(!state.mergedCoverage || (context.mergedCoverageSampleCounts & state.samples) != 0, "an overrasterized draw (a multisampled rasterizer into one-sample color targets) requires VK_NV_framebuffer_mixed_samples with merged coverage reduction"); + Require(!(state.vertexPoints || (state.stages.mesh.has_value() && state.stages.mesh->outputPrimitiveSize == 1u)) || context.largePoints || (state.pointSize == 1.0f && ((state.paClVsOutCntl & (1u << 16u)) == 0 || (state.pointSizeMin == 1.0f && state.pointSizeMax == 1.0f))), "points of a size other than one pixel require the largePoints device feature"); + Require(!state.truncatedCoverage || (context.truncatedCoverageSampleCounts & state.samples) != 0, "one-sample color targets beside a multisampled depth target require VK_AMD_mixed_attachment_samples or VK_NV_framebuffer_mixed_samples with truncated coverage reduction"); 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.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) && !state.primitiveRestart, "emulated barycentrics through tessellation require a triangle list or strip 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"); @@ -304,10 +378,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput const VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT meshSubgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT, nullptr, 32u}; for (std::uint32_t i = 0; i < shaders.size(); ++i) { const auto& shader = *shaders[i].program; - VkShaderModuleCreateInfo module{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - module.codeSize = shader.spirv.size() * sizeof(std::uint32_t); - module.pCode = shader.spirv.data(); - Check(context.Function("vkCreateShaderModule")(context.device, &module, nullptr, &_modules[i]), "vkCreateShaderModule graphics"); + _modules[i] = CreateShaderModule(context, shader.spirv.data(), shader.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule graphics"); const auto stage = VulkanStage(shaders[i].stage); stages[i].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[i].stage = stage; @@ -317,6 +388,12 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput specializations.emplace_back(shader); stages[i].pSpecializationInfo = specializations.back().Info(); } + if (std::ranges::any_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && !shader.program->stencilReferences.empty(); })) { + for (std::size_t i = 0; i < shaders.size(); ++i) { + if (shaders[i].stage != ShaderRecompiler::ShaderStage::Vertex) continue; + Require(stages[i].flags == 0u && stages[i].pNext == nullptr, "stencil replay vertex stage allows varying subgroup size"); + } + } // A descriptor set layout with the same bindings as this one is compatible with the pipeline // layout, so later draws bind their own ShaderResources' set under it. const std::array setLayouts{resources.main, resources.fragment}; @@ -334,7 +411,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput for (std::uint32_t index = 0; index < state.colors.size(); ++index) { VkAttachmentDescription color{}; color.format = state.colors[index].format; - color.samples = static_cast(state.samples); + color.samples = state.mergedCoverage || state.truncatedCoverage ? VK_SAMPLE_COUNT_1_BIT : static_cast(state.samples); color.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; @@ -367,6 +444,12 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput VkPipelineRenderingCreateInfoKHR rendering{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR}; rendering.colorAttachmentCount = static_cast(renderingColors.size()); rendering.pColorAttachmentFormats = renderingColors.empty() ? nullptr : renderingColors.data(); + const std::vector mergedColorSamples(renderingColors.size(), VK_SAMPLE_COUNT_1_BIT); + VkAttachmentSampleCountInfoNV attachmentSamples{VK_STRUCTURE_TYPE_ATTACHMENT_SAMPLE_COUNT_INFO_NV}; + attachmentSamples.colorAttachmentCount = static_cast(mergedColorSamples.size()); + attachmentSamples.pColorAttachmentSamples = mergedColorSamples.empty() ? nullptr : mergedColorSamples.data(); + if (state.truncatedCoverage) attachmentSamples.depthStencilAttachmentSamples = static_cast(state.samples); + if (state.mergedCoverage || state.truncatedCoverage) rendering.pNext = &attachmentSamples; if (state.depth) { rendering.depthAttachmentFormat = HostDepthFormat(context, state.depth->format); if (HasStencil(state.depth->format)) rendering.stencilAttachmentFormat = rendering.depthAttachmentFormat; @@ -389,7 +472,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 @@ -403,6 +486,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput PipelineLibraryKeys keys; libraries = libraryKeys(keys, context, state, vertexInput, resources, shaders); std::vector dynamicStates{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + if (state.stencilReplayReference) dynamicStates.push_back(VK_DYNAMIC_STATE_STENCIL_REFERENCE); if (depthBounds) dynamicStates.push_back(VK_DYNAMIC_STATE_DEPTH_BOUNDS); if (depthBias) dynamicStates.push_back(VK_DYNAMIC_STATE_DEPTH_BIAS); if (libraries) dynamicStates.assign(PipelineLibraryDynamicStates().begin(), PipelineLibraryDynamicStates().end()); @@ -410,7 +494,9 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput dynamic.dynamicStateCount = static_cast(dynamicStates.size()); dynamic.pDynamicStates = dynamicStates.data(); VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; - raster.polygonMode = VK_POLYGON_MODE_FILL; + Require(state.polygonMode == VK_POLYGON_MODE_FILL || context.fillModeNonSolid, "a point or line polygon mode needs the fillModeNonSolid feature"); + Require(state.polygonMode != VK_POLYGON_MODE_POINT || context.maintenance5, "a point polygon mode needs VK_KHR_maintenance5 for its one-pixel point size"); + raster.polygonMode = state.polygonMode; raster.depthClampEnable = state.depthClamp && context.depthClamp ? VK_TRUE : VK_FALSE; Require(!state.depthBiasPerFace || (depthBias && state.cullMode == VK_CULL_MODE_NONE), "per-face depth bias needs both faces rasterized"); raster.cullMode = state.depthBiasPerFace ? VK_CULL_MODE_BACK_BIT : state.cullMode; @@ -428,11 +514,20 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput } VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; samples.rasterizationSamples = static_cast(state.samples); + samples.sampleShadingEnable = state.perSampleShading ? VK_TRUE : VK_FALSE; + samples.minSampleShading = state.perSampleShading ? 1.0f : 0.0f; const auto locations = SampleLocations(state); VkPipelineSampleLocationsStateCreateInfoEXT sampleLocations{VK_STRUCTURE_TYPE_PIPELINE_SAMPLE_LOCATIONS_STATE_CREATE_INFO_EXT}; sampleLocations.sampleLocationsEnable = VK_TRUE; - sampleLocations.sampleLocationsInfo = {VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, samples.rasterizationSamples, {1, 1}, state.samples, locations.data()}; + const auto grid = sampleLocationGrid; + sampleLocations.sampleLocationsInfo = {VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, samples.rasterizationSamples, grid, state.samples * grid.width * grid.height, locations.data()}; if (sampleLocationsEnabled) samples.pNext = &sampleLocations; + VkPipelineCoverageReductionStateCreateInfoNV coverageReduction{VK_STRUCTURE_TYPE_PIPELINE_COVERAGE_REDUCTION_STATE_CREATE_INFO_NV}; + coverageReduction.coverageReductionMode = state.truncatedCoverage ? VK_COVERAGE_REDUCTION_MODE_TRUNCATE_NV : VK_COVERAGE_REDUCTION_MODE_MERGE_NV; + if (state.mergedCoverage || (state.truncatedCoverage && !context.mixedAttachmentSamples)) { + coverageReduction.pNext = samples.pNext; + samples.pNext = &coverageReduction; + } VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; depthStencil.depthTestEnable = state.depthTest; depthStencil.depthWriteEnable = state.depthWrite; @@ -441,6 +536,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput depthStencil.stencilTestEnable = state.stencilTest; depthStencil.front = state.stencilFront; depthStencil.back = state.stencilBack; + const VkSampleMask sampleMask = HostSampleMask(state); + if (PartialSampleMask(state) && UniformSampleMask(state) && !SampleMaskFollowsOrder(state, sampleOrder)) samples.pSampleMask = &sampleMask; VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; const bool alternateAlpha = SplitsAlternateAlpha(state); std::vector firstChannels(state.blends.begin(), state.blends.end()); @@ -470,8 +567,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; @@ -516,6 +613,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); _modules.clear(); timing.Mark("create"); + PerformanceCounters::CountPipeline(); LogPipelineStatistics_nid_no_patch(context, pipeline); } catch (...) { release(); @@ -524,6 +622,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput } bool Pipeline::Prebuild(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders) { + if (std::ranges::any_of(shaders, [](const CompiledShader& shader) { return !shader.program->stencilReferences.empty(); })) return false; struct Scope { Scope() { prebuilding = true; prebuilt = false; } ~Scope() { prebuilding = false; } @@ -577,22 +676,24 @@ VkPipelineLayout Pipeline::Layout() const { return layout; } -std::shared_ptr Pipeline::AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent, std::uint32_t layers, std::span persistent) { +std::shared_ptr Pipeline::AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent, std::uint32_t layers) { Require(targets.size() == attachments && owners.size() == colorAttachments, "render targets do not match the pipeline's attachments"); bool resident = true; - for (std::size_t i = 0; i < owners.size(); ++i) resident = resident && (owners[i] != nullptr || (i < persistent.size() && persistent[i] != 0)); + for (const auto& owner : owners) resident = resident && owner != nullptr; + const auto depthReleases = attachments > colorAttachments ? DepthSurfacesReleased() : 0u; if (resident) { // Entries whose views are gone can never match again and go as soon as no recorded draw holds // them (Kept keeps its framebuffer until the batch completes). - std::erase_if(framebuffers, [](const CachedFramebuffer& entry) { + std::erase_if(framebuffers, [&](const CachedFramebuffer& entry) { if (entry.framebuffer.use_count() != 1) return false; + if (entry.depthReleases != depthReleases) return true; for (std::size_t i = 0; i < entry.owners.size(); ++i) { if (entry.owned[i] && entry.owners[i].expired()) return true; } return false; }); for (auto it = framebuffers.begin(); it != framebuffers.end(); ++it) { - if (it->extent.width != extent.width || it->extent.height != extent.height || it->layers != layers || !std::equal(it->views.begin(), it->views.end(), targets.begin(), targets.end())) continue; + if (it->extent.width != extent.width || it->extent.height != extent.height || it->layers != layers || it->depthReleases != depthReleases || !std::equal(it->views.begin(), it->views.end(), targets.begin(), targets.end())) continue; // View handles are recycled once a StorageTexture is destroyed, so the owners must be the // very objects the views were made for. bool same = true; @@ -617,6 +718,7 @@ std::shared_ptr Pipeline::AcquireFramebuffer(std::span(state.samples), {1, 1}, state.samples, locations.data()}; + const auto grid = sampleLocationGrid; + const VkSampleLocationsInfoEXT info{VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast(state.samples), grid, state.samples * grid.width * grid.height, locations.data()}; const VkAttachmentSampleLocationsEXT initial{static_cast(state.colors.size()), info}; const VkSubpassSampleLocationsEXT post{0, info}; VkRenderPassSampleLocationsBeginInfoEXT sampleLocations{VK_STRUCTURE_TYPE_RENDER_PASS_SAMPLE_LOCATIONS_BEGIN_INFO_EXT}; @@ -671,10 +774,12 @@ 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); context.Resolved(&DeviceFunctions::cmdSetScissor, "vkCmdSetScissor")(commands, 0, 1, &state.scissor); + if (state.stencilReplayReference) context.Resolved(&DeviceFunctions::cmdSetStencilReference, "vkCmdSetStencilReference")(commands, VK_STENCIL_FACE_FRONT_AND_BACK, *state.stencilReplayReference); if (libraries) { setLibraryState(commands, state); return; @@ -738,25 +843,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); } @@ -783,6 +890,7 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, state.topology); append(key, state.primitiveRestart); append(key, state.cullMode); + append(key, state.polygonMode); append(key, state.frontFace); append(key, state.provokingVertexMode); append(key, state.negativeOneToOne); @@ -792,25 +900,40 @@ void pipelineKey(std::vector& key, const Context& context, const Stat for (const auto& blend : state.blends) append(key, blend); for (const auto value : state.blendConstants) append(key, value); append(key, state.samples); + append(key, state.perSampleShading); + append(key, state.mergedCoverage); + append(key, state.truncatedCoverage); append(key, state.customSampleLocations); - if (state.customSampleLocations) append(key, state.sampleLocations); + if (state.customSampleLocations) { + append(key, state.sampleLocationGrid); + 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); - append(key, state.stencilFront); - append(key, state.stencilBack); + auto front = state.stencilFront; + auto back = state.stencilBack; + if (state.stencilReplayReference) front.reference = back.reference = 0u; + append(key, state.stencilReplayReference.has_value()); + append(key, front); + append(key, back); } append(key, state.stages.mesh.has_value()); if (state.stages.mesh) { @@ -823,6 +946,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) { @@ -836,6 +960,11 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, tessellation.primitiveGroupSize); append(key, tessellation.breakAtInstance); } + append(key, HostSampleMask(state)); + append(key, context.sampleLocationSampleCounts); + append(key, context.sampleLocationGridSampleCounts); + append(key, context.storageImageMultisample); + append(key, context.shaderStencilExport); } std::uint64_t hashKey(const std::vector& key) { @@ -848,12 +977,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; @@ -861,6 +990,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(); }; @@ -873,8 +1003,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; } @@ -886,6 +1020,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; @@ -903,57 +1042,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 ec51c5a20b..73fac4402d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp @@ -1,9 +1,13 @@ #include "prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp" #include "ShaderCacheDirectory.hpp" +#include "ShaderDiskCache.hpp" +#include #include #include #include #include +#include +#include #include #include #include @@ -15,22 +19,25 @@ namespace { constexpr std::uint32_t FileMagic = 0x50565041u; constexpr std::uint32_t FileFormat = 1; -struct FileHeader { - std::uint32_t magic; - std::uint32_t format; - std::uint64_t dataBytes; - std::uint64_t dataHash; -}; -static_assert(sizeof(FileHeader) == 24); - bool profiling() { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; return profile; } +std::mutex& pathMutex(const std::filesystem::path& path) { + struct Registry { + std::mutex mutex; + std::map paths; + }; + static auto& registry = *new Registry; + std::lock_guard lock(registry.mutex); + return registry.paths[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()) { std::string uuid; @@ -39,12 +46,17 @@ PipelineCache::PipelineCache(const Context& context, const VkPhysicalDevicePrope std::snprintf(digits, sizeof(digits), "%02x", static_cast(byte)); uuid += digits; } + char version[17]; + std::snprintf(version, sizeof(version), "%016llx", static_cast(ShaderRecompiler::ShaderDiskCache::SourceVersion())); char name[160]; std::snprintf(name, sizeof(name), "vk-pipelines-%04x-%04x-%08x-%s.bin", properties.vendorID, properties.deviceID, properties.driverVersion, uuid.c_str()); - path = directory / name; + path = directory / version / name; } std::vector initialData; - if (!path.empty()) load(initialData); + if (!path.empty()) { + std::lock_guard pathLock(pathMutex(path)); + if (loadFromFile) load(initialData); + } VkPipelineCacheCreateInfo info{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO}; info.initialDataSize = initialData.size(); info.pInitialData = initialData.empty() ? nullptr : initialData.data(); @@ -54,7 +66,9 @@ PipelineCache::PipelineCache(const Context& context, const VkPhysicalDevicePrope std::fprintf(stderr, "[pipeline-cache] the driver refused %s (Vulkan result %d); starting empty\n", path.string().c_str(), static_cast(result)); info.initialDataSize = 0; info.pInitialData = nullptr; - savedBytes = 0; + loadedBytes = 0; + settledBytes = 0; + settledFile.reset(); result = create(context.device, &info, nullptr, &cache); } Check(result, "vkCreatePipelineCache"); @@ -69,24 +83,47 @@ PipelineCache::~PipelineCache() { } wake.notify_all(); saver.join(); - save(true); + save(true, true); } context.Function("vkDestroyPipelineCache")(context.device, cache, nullptr); } -void PipelineCache::load(std::vector& initialData) { - std::vector file; - if (!ShaderRecompiler::ReadWholeFile(path, file)) return; - const auto reject = [&](const char* why) { - std::fprintf(stderr, "[pipeline-cache] ignoring %s: %s\n", path.string().c_str(), why); - }; - FileHeader header{}; - if (file.size() < sizeof(header)) return reject("truncated"); - std::memcpy(&header, file.data(), sizeof(header)); - if (header.magic != FileMagic || header.format != FileFormat) return reject("another format"); - if (header.dataBytes != file.size() - sizeof(header)) return reject("truncated"); - const auto data = std::span(file).subspan(sizeof(header)); - if (ShaderRecompiler::HashBytes(data) != header.dataHash) return reject("checksum mismatch"); +std::span PipelineCache::File::Payload() const { + return std::span(contents).subspan(sizeof(FileHeader)); +} + +PipelineCache::File PipelineCache::readFile() const { + File file; + if (!ShaderRecompiler::ReadWholeFile(path, file.contents)) { + std::error_code error; +#ifdef _WIN32 + const auto native = ShaderRecompiler::NativeCachePath_nid_no_patch(path, error); + const bool exists = !error && std::filesystem::exists(native, error); +#else + const bool exists = std::filesystem::exists(path, error); +#endif + file.state = exists || error ? FileState::Unreadable : FileState::Missing; + return file; + } + file.state = FileState::Invalid; + if (file.contents.size() < sizeof(FileHeader)) { + file.invalidReason = "truncated"; + return file; + } + std::memcpy(&file.header, file.contents.data(), sizeof(FileHeader)); + if (file.header.magic != FileMagic || file.header.format != FileFormat) { + file.invalidReason = "another format"; + return file; + } + if (file.header.dataBytes != file.contents.size() - sizeof(FileHeader)) { + file.invalidReason = "truncated"; + return file; + } + const auto data = file.Payload(); + if (ShaderRecompiler::HashBytes(data) != file.header.dataHash) { + file.invalidReason = "checksum mismatch"; + return file; + } struct VulkanHeader { std::uint32_t headerSize; std::uint32_t headerVersion; @@ -95,42 +132,153 @@ void PipelineCache::load(std::vector& initialData) { std::uint8_t uuid[VK_UUID_SIZE]; } vulkan{}; static_assert(sizeof(VulkanHeader) == 32); - if (data.size() < sizeof(vulkan)) return reject("no Vulkan header"); + if (data.size() < sizeof(vulkan)) { + file.invalidReason = "no Vulkan header"; + return file; + } std::memcpy(&vulkan, data.data(), sizeof(vulkan)); - if (vulkan.headerSize < sizeof(vulkan) || vulkan.headerVersion != VK_PIPELINE_CACHE_HEADER_VERSION_ONE || vulkan.vendorID != properties.vendorID || vulkan.deviceID != properties.deviceID || std::memcmp(vulkan.uuid, properties.pipelineCacheUUID, VK_UUID_SIZE) != 0) return reject("made for another device or driver"); - initialData.assign(data.begin(), data.end()); - savedBytes = initialData.size(); + if (vulkan.headerSize < sizeof(vulkan) || vulkan.headerVersion != VK_PIPELINE_CACHE_HEADER_VERSION_ONE || vulkan.vendorID != properties.vendorID || vulkan.deviceID != properties.deviceID || std::memcmp(vulkan.uuid, properties.pipelineCacheUUID, VK_UUID_SIZE) != 0) { + file.invalidReason = "made for another device or driver"; + return file; + } + file.state = FileState::Valid; + return file; +} + +std::optional PipelineCache::readHeader() const { +#ifdef _WIN32 + std::error_code error; + const auto native = ShaderRecompiler::NativeCachePath_nid_no_patch(path, error); + if (error) return std::nullopt; + std::ifstream file(native, std::ios::binary); +#else + std::ifstream file(path, std::ios::binary); +#endif + FileHeader header{}; + if (!file.read(reinterpret_cast(&header), sizeof(header))) return std::nullopt; + return header; +} + +void PipelineCache::load(std::vector& initialData) { + 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()); + return; + } + if (file.state == FileState::Invalid) { + std::fprintf(stderr, "[pipeline-cache] ignoring %s: %s\n", path.string().c_str(), file.invalidReason); + return; + } + 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()); } -void PipelineCache::save(bool final) { +void PipelineCache::save(bool final, bool waitForPipelines) { + std::lock_guard pathLock(pathMutex(path)); const auto getData = context.Function("vkGetPipelineCacheData"); + const auto snapshot = [&] { return waitForPipelines ? synchronization.LockForSnapshot() : synchronization.TryLockForSnapshot(); }; + std::size_t liveBytes = 0; + { + auto synchronizationLock = snapshot(); + if (!synchronizationLock.owns_lock()) return; + if (getData(context.device, cache, &liveBytes, nullptr) != VK_SUCCESS || liveBytes == 0) 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 = 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 = fileUnchanged ? std::max(loadedBytes, settledFile->dataBytes) : 0; + VkPipelineCache merged = VK_NULL_HANDLE; + struct DestroyMerged { + const Context& context; + const VkPipelineCache& cache; + ~DestroyMerged() { + if (cache != VK_NULL_HANDLE) context.Function("vkDestroyPipelineCache")(context.device, cache, nullptr); + } + } destroyMerged{context, merged}; + if (disk.state == FileState::Valid) { + const auto data = disk.Payload(); + VkPipelineCacheCreateInfo info{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO}; + info.initialDataSize = data.size(); + info.pInitialData = data.data(); + const auto result = context.Function("vkCreatePipelineCache")(context.device, &info, nullptr, &merged); + if (result == VK_SUCCESS) { + floor = std::max(loadedBytes, data.size()); + } else { + merged = VK_NULL_HANDLE; + std::fprintf(stderr, "[pipeline-cache] replacing %s: the driver refused it (Vulkan result %d)\n", path.string().c_str(), static_cast(result)); + } + } + if (merged != VK_NULL_HANDLE) { + auto synchronizationLock = snapshot(); + if (!synchronizationLock.owns_lock()) return; + const auto result = context.Function("vkMergePipelineCaches")(context.device, merged, 1, &cache); + if (result != VK_SUCCESS) { + std::fprintf(stderr, "[pipeline-cache] cannot merge into %s (Vulkan result %d); not saving over it\n", path.string().c_str(), static_cast(result)); + return; + } + } + const auto source = merged != VK_NULL_HANDLE ? merged : cache; std::size_t size = 0; std::vector file; { - auto synchronizationLock = synchronization.LockForSnapshot(); - if (getData(context.device, cache, &size, nullptr) != VK_SUCCESS || size == 0 || size == savedBytes) return; + auto synchronizationLock = source == cache ? snapshot() : PipelineCacheSynchronization::SnapshotLock{}; + if (source == cache && !synchronizationLock.owns_lock()) return; + if (getData(context.device, source, &size, nullptr) != VK_SUCCESS || size == 0) return; + if (merged != VK_NULL_HANDLE && size == disk.header.dataBytes) { + settle(disk.header); + return; + } file.resize(sizeof(FileHeader) + size); - const auto result = getData(context.device, cache, &size, file.data() + sizeof(FileHeader)); + const auto result = getData(context.device, source, &size, file.data() + sizeof(FileHeader)); if (result != VK_SUCCESS && result != VK_INCOMPLETE) return; file.resize(sizeof(FileHeader) + size); } + 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(fileUnchanged ? *settledFile : disk.header); + return; + } const FileHeader header{FileMagic, FileFormat, size, ShaderRecompiler::HashBytes(std::span(file).subspan(sizeof(FileHeader)))}; std::memcpy(file.data(), &header, sizeof(header)); - const auto started = std::chrono::steady_clock::now(); if (!ShaderRecompiler::WriteFileAtomically(path, file)) { std::fprintf(stderr, "[pipeline-cache] cannot write %s\n", path.string().c_str()); return; } - savedBytes = size; - if (final || profiling()) std::fprintf(stderr, "[pipeline-cache] saved %.1f KiB to %s in %.1f ms%s\n", static_cast(size) / 1024.0, path.string().c_str(), std::chrono::duration(std::chrono::steady_clock::now() - started).count(), final ? " (teardown)" : ""); + settle(header); + 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, true); +} + +void PipelineCache::SaveUnlessBusy() { + save(false, false); } void PipelineCache::run() { std::unique_lock lock(mutex); while (!wake.wait_for(lock, std::chrono::seconds(SaveIntervalSeconds), [&] { return stopping; })) { lock.unlock(); - save(false); + save(false, false); lock.lock(); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp new file mode 100644 index 0000000000..ceea544e4e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -0,0 +1,1172 @@ +#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) == 824, "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 = 8u; +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.polygonMode)); + 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); + writer.Bool(state.mergedCoverage); + writer.Bool(state.truncatedCoverage); + for (const auto mask : state.quadSampleMask) writer.Raw(mask); + writer.U32(state.depthRemap.mode); + writer.Bool(state.vertexPoints); + writer.Raw(state.pointSize); + writer.Raw(state.pointSizeMin); + writer.Raw(state.pointSizeMax); +} + +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.polygonMode = static_cast(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(); + state.mergedCoverage = reader.Bool(); + state.truncatedCoverage = reader.Bool(); + for (auto& mask : state.quadSampleMask) mask = reader.Raw(); + state.depthRemap.mode = reader.U32(); + state.vertexPoints = reader.Bool(); + state.pointSize = reader.Raw(); + state.pointSizeMin = reader.Raw(); + state.pointSizeMax = 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; + auto directory = ShaderRecompiler::ShaderDiskCache::EntryDirectory(); +#ifdef _WIN32 + std::error_code error; + const auto native = ShaderRecompiler::NativeCachePath_nid_no_patch(directory, error); + if (error) throw std::filesystem::filesystem_error("cannot resolve the pipeline journal directory", directory, error); + directory = native; +#endif + path = directory / "pipelines.journal"; + lockPath = directory / "pipelines.journal.lock"; + maxBytes = configured("APS5_PIPELINE_JOURNAL_MAX_MIB", 1024u) << 20u; + std::thread([this] { run(); }).detach(); + } + + // 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.polygonMode = state.polygonMode; + 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.mergedCoverage = state.mergedCoverage; + canonical.truncatedCoverage = state.truncatedCoverage; + canonical.quadSampleMask = state.quadSampleMask; + canonical.depthRemap.mode = state.depthRemap.mode; + canonical.vertexPoints = state.vertexPoints; + canonical.pointSize = state.pointSize; + canonical.pointSizeMin = state.pointSizeMin; + canonical.pointSizeMax = state.pointSizeMax; + 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..77119254d5 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); @@ -232,16 +255,32 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip } if (partsOnly) return VK_NULL_HANDLE; PerformanceTimer timing("Vulkan.GraphicsPipelineLink"); + std::optional, VkSampleCountFlagBits>> sampleCounts; + for (auto* next = static_cast(rendering.pNext); next != nullptr; next = next->pNext) { + if (next->sType != VK_STRUCTURE_TYPE_ATTACHMENT_SAMPLE_COUNT_INFO_NV) continue; + const auto& counts = *reinterpret_cast(next); + sampleCounts.emplace(std::vector(counts.pColorAttachmentSamples, counts.pColorAttachmentSamples + counts.colorAttachmentCount), counts.depthStencilAttachmentSamples); + } + const auto attachmentSamples = [](const std::pair, VkSampleCountFlagBits>& counts) { + return VkAttachmentSampleCountInfoNV{VK_STRUCTURE_TYPE_ATTACHMENT_SAMPLE_COUNT_INFO_NV, nullptr, static_cast(counts.first.size()), counts.first.empty() ? nullptr : counts.first.data(), counts.second}; + }; + auto linkedSamples = sampleCounts ? attachmentSamples(*sampleCounts) : VkAttachmentSampleCountInfoNV{}; VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; linked.libraryCount = static_cast(libraries.size()); linked.pLibraries = libraries.data(); VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked}; + if (sampleCounts) { + linkedSamples.pNext = create.pNext; + create.pNext = &linkedSamples; + } 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,12 +291,17 @@ 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, sampleCounts, attachmentSamples, &store, devicePointer = &device] { if (slot->released.load()) return; + auto linkedSamples = sampleCounts ? attachmentSamples(*sampleCounts) : VkAttachmentSampleCountInfoNV{}; VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; linked.libraryCount = static_cast(libraries.size()); linked.pLibraries = libraries.data(); VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked}; + if (sampleCounts) { + linkedSamples.pNext = create.pNext; + create.pNext = &linkedSamples; + } create.flags = VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT; create.layout = libraryLayout; VkPipeline result = VK_NULL_HANDLE; @@ -272,7 +316,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 +332,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 +350,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 +362,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/PlaceholderImages.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp index cc352d0d9c..68487401c7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp @@ -21,18 +21,18 @@ PlaceholderImages::PlaceholderImages(const Context& context) : context(context) try { std::vector toClear; for (std::uint32_t index = 0; index < images.size(); ++index) { - const auto shape = PlaceholderImageShapeOf(ShaderRecompiler::RuntimeAbi::FirstImageBinding + index); - if (!Supported(context, shape)) continue; + const auto shape = PlaceholderImageShapeForDevice(ShaderRecompiler::RuntimeAbi::FirstImageBinding + index, context.storageImageMultisample); + if (!shape || !Supported(context, *shape)) continue; auto& image = images[index]; VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; - info.imageType = shape.type; - info.format = shape.format; + info.imageType = shape->type; + info.format = shape->format; info.extent = {1, 1, 1}; info.mipLevels = 1; info.arrayLayers = 1; - info.samples = shape.samples; + info.samples = shape->samples; info.tiling = VK_IMAGE_TILING_OPTIMAL; - info.usage = shape.usage; + info.usage = shape->usage; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image.image), "vkCreateImage placeholder"); @@ -45,9 +45,9 @@ PlaceholderImages::PlaceholderImages(const Context& context) : context(context) Check(context.Function("vkBindImageMemory")(context.device, image.image, image.memory, 0), "vkBindImageMemory placeholder"); VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image.image; - viewInfo.viewType = shape.viewType; - viewInfo.format = shape.format; - viewInfo.subresourceRange = {shape.aspect, 0, 1, 0, 1}; + viewInfo.viewType = shape->viewType; + viewInfo.format = shape->format; + viewInfo.subresourceRange = {shape->aspect, 0, 1, 0, 1}; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &image.view), "vkCreateImageView placeholder"); VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index f3acc6488e..9f03343a67 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1,7 +1,9 @@ +#include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -9,10 +11,12 @@ #include "prx/libSceAgcDriver/Graphics/shaders/SampleCounter_spv.h" #include "prx/libSceAgcDriver/Graphics/shaders/SampleDumps_spv.h" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp" #include "prx/libc/include/CpuTopology.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/DispatchArguments_spv.h" #include #include #include @@ -29,6 +33,8 @@ #include #include #include +#include +#include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::Graphics { @@ -181,7 +187,7 @@ constexpr std::uint32_t SegmentFoldCapacity = 1024; constexpr std::uint32_t DumpFoldCapacity = 1024; constexpr std::uint32_t SampleDumpBytes = 15 * 16 + 8; constexpr VkDeviceSize DumpListOffset = 16 + 8 * SegmentFoldCapacity; -constexpr std::uint32_t SampleDumpPushBytes = 32; +constexpr std::uint32_t SampleDumpPushBytes = 40; std::atomic dumpsQueued{0}, dumpsQueuedInPass{0}, dumpsAtOnce{0}, segmentsClosed{0}, dumpFolds{0}; struct SamplePoolCache { @@ -503,6 +509,8 @@ void ReleaseDeferredKeeps() { // Lock-free state of the active recorder's open batch, read by the queue workers between packets // and inside WAIT_REG_MEM polls (see the static readers in Recorder.hpp). Written under the mutex. constexpr std::int64_t NoPendingLabel = std::numeric_limits::min(); +constexpr BarrierMask PassEndBarrier{VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT}; +constexpr BarrierMask LabelLeadingBarrier{VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT}; std::atomic pendingLabelSince{NoPendingLabel}; std::atomic writeGeneration{0}; std::atomic publishGeneration{0}; @@ -633,7 +641,7 @@ using WriteRanges = Recorder::WriteRanges; // work. A range leaves the snapshot only after its batch's completions (CPU write-backs) ran, so a // reader that sees no overlap either precedes the note (the queues are unordered then, as on the // GPU) or follows the write-back. -std::atomic> pendingWrites; +AtomicSharedPtr pendingWrites; bool HookSnapshotEnabled() { // Debug aid: APS5_NO_HOOK_SNAPSHOT=1 takes the GpuMutex on every access as before. @@ -655,13 +663,17 @@ bool SnapshotOverlaps(std::uint64_t address, std::size_t bytes) { } // Whether one merged range of the snapshot contains [address, end) entirely. -bool SnapshotCovers(std::uint64_t address, std::uint64_t end) { - const auto snapshot = pendingWrites.load(std::memory_order_acquire); +bool SnapshotCovers(const WriteRanges* snapshot, std::uint64_t address, std::uint64_t end) { if (snapshot == nullptr || snapshot->empty()) return false; const auto it = std::partition_point(snapshot->begin(), snapshot->end(), [&](const auto& range) { return range.second <= address; }); return it != snapshot->end() && it->first <= address && end <= it->second; } +bool SnapshotCovers(std::uint64_t address, std::uint64_t end) { + const auto snapshot = pendingWrites.load(std::memory_order_acquire); + return SnapshotCovers(snapshot.get(), address, end); +} + // Attribution of the pending-write syncs the hook makes (the [hooksync] line every 10 s, under // APS5_PROFILE_DRAW; APS5_NO_HOOKSYNC_PROFILE=1 leaves only the plain counts). Every sync is charged // to the packet the accessing thread executes and the read site it named (or, when it named none, @@ -1046,6 +1058,7 @@ void FlushForAccess(std::uint64_t address, std::size_t bytes) { std::size_t images = 0, evicted = 0; const bool kept = !HookFlushCpuBlocks() && StorageTexture::AccessKeptByCpu(address, bytes, &images, &evicted); if (kept || (images == 0 && !HookFlushCpuBlocks())) { + stored = StorageTexture::WaitForFlushing(address, bytes); published = StorageTexture::PublishShadowsOnly(address, bytes, scope); if (kept && HookSyncProfiled()) CountRecordedStoreSkip(RecordedStoreKey(), evicted); } else { @@ -1071,6 +1084,60 @@ void FlushForAccess(std::uint64_t address, std::size_t bytes) { } +struct Recorder::ProgressWatcher { + std::mutex mutex; + std::condition_variable wake; + std::uint64_t submitted = 0; + bool stop = false; + std::thread thread; + + ProgressWatcher(VkDevice device, VkSemaphore timeline, PFN_vkWaitSemaphoresKHR waitSemaphores, PFN_vkGetSemaphoreCounterValueKHR counterValue) { + thread = std::thread([this, device, timeline, waitSemaphores, counterValue] { + CpuTopology::PinHelperThread("GPU progress watcher"); + std::uint64_t seen = 0; + for (;;) { + { + std::unique_lock lock(mutex); + wake.wait(lock, [&] { return stop || submitted > seen; }); + if (stop) return; + } + const std::uint64_t target = seen + 1; + VkSemaphoreWaitInfoKHR info{VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO_KHR}; + info.semaphoreCount = 1; + info.pSemaphores = &timeline; + info.pValues = ⌖ + const auto result = waitSemaphores(device, &info, 50'000'000ull); + if (result == VK_TIMEOUT) continue; + if (result != VK_SUCCESS) { + std::fprintf(stderr, "[gpu] progress watcher: vkWaitSemaphoresKHR failed (%d), watcher stops\n", static_cast(result)); + return; + } + std::uint64_t value = target; + if (counterValue(device, timeline, &value) != VK_SUCCESS) value = target; + seen = std::max(target, value); + KernelWakeSleepers_nid_no_patch(); + } + }); + } + + void Submitted(std::uint64_t serial) { + { + std::lock_guard lock(mutex); + submitted = serial; + } + wake.notify_one(); + } + + ~ProgressWatcher() { + { + std::lock_guard lock(mutex); + stop = true; + } + wake.notify_one(); + thread.join(); + } +}; + Recorder::Recorder(const Context& context, bool timelineSemaphores) : context(context), id(nextRecorderId.fetch_add(1)) { { std::lock_guard lock(liveRecordersMutex); @@ -1117,6 +1184,7 @@ Recorder::~Recorder() { } catch (const std::exception& error) { std::fprintf(stderr, "[gpu] recorder teardown: %s\n", error.what()); } + progressWatcher.reset(); holdKept.clear(); if (activeRecorder == this) { activeRecorder = nullptr; @@ -1167,6 +1235,8 @@ Recorder::~Recorder() { context.Function("vkDestroyPipelineLayout")(context.device, dumpLayout, nullptr); sampleCounter.reset(); } + if (dispatchArgumentPipeline != VK_NULL_HANDLE) context.Function("vkDestroyPipeline")(context.device, dispatchArgumentPipeline, nullptr); + if (dispatchArgumentLayout != VK_NULL_HANDLE) context.Function("vkDestroyPipelineLayout")(context.device, dispatchArgumentLayout, nullptr); for (auto& [commands, fence] : spare) { context.Function("vkFreeCommandBuffers")(context.device, context.pool, 1, &commands); context.Function("vkDestroyFence")(context.device, fence, nullptr); @@ -1406,6 +1476,10 @@ void Recorder::LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool open->passHazards.Add(access); } +void Recorder::SealPassWriteOnly() { + if (open != nullptr) open->passHazards.SealWriteOnly(); +} + void Recorder::endOpenRenderPass(bool barrier) { auto& pass = open->renderPass; if (open->segmentActive) endSegment(*open); @@ -1413,7 +1487,7 @@ void Recorder::endOpenRenderPass(bool barrier) { // The pass's attachment and shader writes are visible to everything recorded after it (the // host sees them at the batch's fence). if (barrier) { - recordBarrier(open->commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + recordBarrier(open->commands, PassEndBarrier.srcStage, PassEndBarrier.dstStage, PassEndBarrier.srcAccess, PassEndBarrier.dstAccess); CountBarriers(CommandClass::Draw); } if (pass.afterPass) pass.afterPass(open->commands); @@ -1520,6 +1594,11 @@ void Recorder::ensureOpen() { dropStaleHoldKept(); batch->kept.insert(batch->kept.end(), holdKept.begin(), holdKept.end()); open = std::move(batch); + if (PerformanceTrace::Recording()) { + static std::atomic traceIds{0}; + open->traceId = traceIds.fetch_add(1, std::memory_order_relaxed) + 1; + } + TraceBatchTag = open->traceId; // The whole-batch range: its start stamp waits for the previous batches like any // bottom-of-pipe stamp, so it marks when this batch's execution began. if (BatchStampsEnabled()) open->batchTiming = beginTiming(BatchTimingKey); @@ -1628,6 +1707,7 @@ bool Recorder::closeStoreRun(bool atSubmit) { // A label says the work before it is done: the queued key stores (results of that work) // land ahead of every store of the run, and the pass a draw left open ends. if (!open->keyStores.empty()) recordKeyStores(true); + const bool passEndLast = open->renderPass.open && !open->renderPass.afterPass; if (open->renderPass.open) endOpenRenderPass(); const auto commands = open->commands; run.timing = beginTiming(ClassKey(CommandClass::LabelRun)); @@ -1638,14 +1718,32 @@ bool Recorder::closeStoreRun(bool atSubmit) { } // At Submit every result of the batch reaches the host before the stores (the title's // pollers read results after the label); in place, later work sees the stores as well. - if (atSubmit) recordBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_READ_BIT); - else recordBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); std::uint32_t barriers = 2; + if (atSubmit) { + recordBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_READ_BIT); + } else if (passEndLast && BarrierBatch() && Covers(PassEndBarrier, LabelLeadingBarrier)) { + CountMerged(CommandClass::LabelRun); + barriers = 1; + } else { + recordBarrier(commands, LabelLeadingBarrier.srcStage, LabelLeadingBarrier.dstStage, LabelLeadingBarrier.srcAccess, LabelLeadingBarrier.dstAccess); + } std::uint64_t storedBytes = 0; const auto update = function(cmdUpdateBuffer, "vkCmdUpdateBuffer"); std::vector writes; writes.reserve(stores.size()); for (const auto& store : stores) writes.push_back({store.address, store.End(), store.dumpTarget != 0}); + if (const auto order = QueuedWriteOrder(writes); !std::is_sorted(order.begin(), order.end())) { + std::vector ordered; + std::vector orderedWrites; + ordered.reserve(stores.size()); + orderedWrites.reserve(writes.size()); + for (const auto index : order) { + ordered.push_back(std::move(stores[index])); + orderedWrites.push_back(writes[index]); + } + stores = std::move(ordered); + writes = std::move(orderedWrites); + } const bool dumps = std::any_of(writes.begin(), writes.end(), [](const QueuedWrite& write) { return write.computed; }); if (dumps) recordQueuedDumps(commands, stores); std::size_t first = 0; @@ -1671,7 +1769,7 @@ bool Recorder::closeStoreRun(bool atSubmit) { } const auto before = dumpIndex; foldQueuedDumps(commands, stores, first, end, dumpIndex, consumed); - if (dumpIndex != before) ++barriers; + if (dumpIndex != before) barriers += 3; first = end; } if (dumps) { @@ -1824,6 +1922,17 @@ bool Recorder::MergeBarriers() { return merge; } +bool Recorder::BarrierBatch() { + static const bool batch = MergeBarriers() && std::getenv("APS5_NO_BARRIER_BATCH") == nullptr; + return batch; +} + +bool Recorder::ElideCovered(CommandClass which, const BarrierMask& wanted, VkAccessFlags covered) { + if (!BarrierBatch() || !OrderedByTrailing(covered, wanted)) return false; + CountMerged(which); + return true; +} + void Recorder::CountMerged(CommandClass which) { if (!DrawOrGpuProfiled()) return; classMerged[static_cast(which)].fetch_add(1, std::memory_order_relaxed); @@ -1940,6 +2049,53 @@ void Recorder::EndGpuTiming(std::uint32_t index, std::uint64_t bytes) { context.Function("vkCmdWriteTimestamp")(open->commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, open->queries, index * 2 + 1); } +bool Recorder::RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments, const std::array* threadGroup) { + constexpr std::uint32_t PushBytes = 48; + if (dispatchArgumentState == 0) { + dispatchArgumentState = -1; + VkShaderModule module = VK_NULL_HANDLE; + try { + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, PushBytes}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.pushConstantRangeCount = 1; + layoutInfo.pPushConstantRanges = &push; + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &dispatchArgumentLayout), "vkCreatePipelineLayout dispatch arguments"); + module = CreateShaderModule(context, DISPATCH_ARGUMENTS_SPV, sizeof(DISPATCH_ARGUMENTS_SPV), "vkCreateShaderModule dispatch arguments"); + VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + pipelineInfo.stage.module = module; + pipelineInfo.stage.pName = "main"; + pipelineInfo.layout = dispatchArgumentLayout; + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &dispatchArgumentPipeline), "vkCreateComputePipelines dispatch arguments"); + context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + dispatchArgumentState = 1; + } catch (const std::exception& error) { + if (module != VK_NULL_HANDLE) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + std::fprintf(stderr, "[gpu] indirect dispatches read their group counts unchecked: %s\n", error.what()); + } + } + if (dispatchArgumentState < 0) return false; + recordBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + function(cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, dispatchArgumentPipeline); + const auto* limit = context.limits.maxComputeWorkGroupCount; + struct { + VkDeviceAddress source; + VkDeviceAddress arguments; + std::array limits; + std::array group; + } parameters{source, arguments, {limit[0], limit[1], limit[2], threadGroup != nullptr ? 1u : 0u}, {1u, 1u, 1u, 0u}}; + if (threadGroup != nullptr) std::copy(threadGroup->begin(), threadGroup->end(), parameters.group.begin()); + static_assert(sizeof(parameters) == PushBytes); + function(cmdPushConstants, "vkCmdPushConstants")(commands, dispatchArgumentLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, PushBytes, ¶meters); + function(cmdDispatch, "vkCmdDispatch")(commands, 1, 1, 1); + const VkPipelineStageFlags readers = VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | (threadGroup != nullptr ? VK_PIPELINE_STAGE_TRANSFER_BIT : 0u); + const VkAccessFlags reads = VK_ACCESS_INDIRECT_COMMAND_READ_BIT | (threadGroup != nullptr ? VK_ACCESS_TRANSFER_READ_BIT : 0u); + recordBarrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, readers, VK_ACCESS_SHADER_WRITE_BIT, reads); + CountBarriers(CommandClass::DispatchLeading, 2); + return true; +} + void Recorder::CountSamples() { GuestMemory::AssertGpuLockHeld("Recorder::CountSamples"); gpuSampleCounter(); @@ -1988,10 +2144,7 @@ bool Recorder::gpuSampleCounter() { layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &sampleLayout), "vkCreatePipelineLayout sample counter"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(SAMPLE_COUNTER_SPV); - moduleInfo.pCode = SAMPLE_COUNTER_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule sample counter"); + module = CreateShaderModule(context, SAMPLE_COUNTER_SPV, sizeof(SAMPLE_COUNTER_SPV), "vkCreateShaderModule sample counter"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; @@ -2007,9 +2160,7 @@ bool Recorder::gpuSampleCounter() { const VkPushConstantRange dumpPush{VK_SHADER_STAGE_COMPUTE_BIT, 0, SampleDumpPushBytes}; layoutInfo.pPushConstantRanges = &dumpPush; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &dumpLayout), "vkCreatePipelineLayout sample dumps"); - moduleInfo.codeSize = sizeof(SAMPLE_DUMPS_SPV); - moduleInfo.pCode = SAMPLE_DUMPS_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule sample dumps"); + module = CreateShaderModule(context, SAMPLE_DUMPS_SPV, sizeof(SAMPLE_DUMPS_SPV), "vkCreateShaderModule sample dumps"); pipelineInfo.stage.module = module; pipelineInfo.layout = dumpLayout; { @@ -2038,6 +2189,56 @@ std::uint64_t Recorder::Batch::StoreRun::Queued::End() const { return address + (dumpTarget != 0 ? SampleDumpBytes : bytes.size()); } +std::vector Recorder::QueuedWriteOrder(std::span writes) { + std::vector order(writes.size()); + for (std::size_t i = 0; i < order.size(); ++i) order[i] = i; + std::vector coveredLater(writes.size()), overLater(writes.size()); + std::map later; + for (auto i = writes.size(); i-- > 0;) { + const auto& write = writes[i]; + if (!write.computed) { + const auto after = later.upper_bound(write.begin); + coveredLater[i] = after != later.begin() && std::prev(after)->second >= write.end; + const auto last = later.lower_bound(write.end); + overLater[i] = last != later.begin() && std::prev(last)->second > write.begin; + continue; + } + auto begin = write.begin, end = write.end; + auto at = later.lower_bound(begin); + if (at != later.begin() && std::prev(at)->second >= begin) --at; + while (at != later.end() && at->first <= end) { + begin = std::min(begin, at->first); + end = std::max(end, at->second); + at = later.erase(at); + } + later.emplace(begin, end); + } + std::vector before, dumps, after; + std::vector> earlier, delayed; + for (std::size_t i = 0; i < writes.size(); ++i) { + const auto& write = writes[i]; + const auto over = [&](const std::vector>& ranges) { + return std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return write.begin < range.second && range.first < write.end; }); + }; + if (write.computed) { + dumps.push_back(i); + earlier.emplace_back(write.begin, write.end); + } else if (dumps.empty()) { + before.push_back(i); + } else if (overLater[i]) { + if (over(delayed) || (!coveredLater[i] && over(earlier))) return order; + before.push_back(i); + } else { + after.push_back(i); + delayed.emplace_back(write.begin, write.end); + } + } + order = std::move(before); + order.insert(order.end(), dumps.begin(), dumps.end()); + order.insert(order.end(), after.begin(), after.end()); + return order; +} + std::vector Recorder::QueuedWriteGroups(std::span writes) { std::vector ends; std::vector> computed; @@ -2165,10 +2366,22 @@ void Recorder::dispatchDumps(VkCommandBuffer commands, std::uint32_t firstDump, std::uint32_t dumps; std::uint32_t firstSegment; std::uint32_t lastSegment; - } parameters{sampleCounter->DeviceAddress(), sampleCounter->DeviceAddress() + DumpListOffset, firstDump, dumps, firstSegment, lastSegment}; + std::uint32_t phase; + } parameters{sampleCounter->DeviceAddress(), sampleCounter->DeviceAddress() + DumpListOffset, firstDump, dumps, firstSegment, lastSegment, 0u}; static_assert(sizeof(parameters) == SampleDumpPushBytes); - function(cmdPushConstants, "vkCmdPushConstants")(commands, dumpLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, SampleDumpPushBytes, ¶meters); - function(cmdDispatch, "vkCmdDispatch")(commands, 1, 1, 1); + const auto push = function(cmdPushConstants, "vkCmdPushConstants"); + const auto dispatch = function(cmdDispatch, "vkCmdDispatch"); + push(commands, dumpLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, SampleDumpPushBytes, ¶meters); + dispatch(commands, 1, 1, 1); + if (dumps == 0) return; + const auto publish = [&](std::uint32_t phase) { + recordBarrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_HOST_READ_BIT); + parameters.phase = phase; + push(commands, dumpLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, SampleDumpPushBytes, ¶meters); + dispatch(commands, 1, 1, 1); + }; + publish(1u); + publish(2u); } void Recorder::foldQueuedDumps(VkCommandBuffer commands, std::span writes, std::size_t first, std::size_t end, std::uint32_t& dumpIndex, std::uint32_t& consumed) { @@ -2231,9 +2444,19 @@ void Recorder::foldSamples(Batch& batch, VkDeviceAddress target) { static_assert(sizeof(parameters) == SampleCounterPushBytes); push(commands, sampleLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, SampleCounterPushBytes, ¶meters); dispatch(commands, 1, 1, 1); + if (parameters.store == 1u) { + const auto publish = [&](std::uint32_t store) { + recordBarrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_HOST_READ_BIT); + parameters.store = store; + push(commands, sampleLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, SampleCounterPushBytes, ¶meters); + dispatch(commands, 1, 1, 1); + }; + publish(3u); + publish(2u); + } } while (done < pendingSamples.size()); recordBarrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_HOST_READ_BIT); - CountBarriers(CommandClass::Copy, 2); + CountBarriers(CommandClass::Copy, target != 0 ? 4u : 2u); pendingSamples.clear(); } @@ -2282,10 +2505,7 @@ bool Recorder::RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress rec layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &meshArgumentLayout), "vkCreatePipelineLayout mesh arguments"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(MESH_ARGUMENTS_SPV); - moduleInfo.pCode = MESH_ARGUMENTS_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule mesh arguments"); + module = CreateShaderModule(context, MESH_ARGUMENTS_SPV, sizeof(MESH_ARGUMENTS_SPV), "vkCreateShaderModule mesh arguments"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; @@ -2360,14 +2580,14 @@ void Recorder::readSamples(Batch& batch) { void Recorder::readGpuTiming(Batch& batch) { if (batch.queries == VK_NULL_HANDLE || batch.timedKeys.empty()) return; std::vector stamps(batch.timedKeys.size() * 2); - const auto result = context.Function("vkGetQueryPoolResults")(context.device, batch.queries, 0, static_cast(stamps.size()), stamps.size() * sizeof(std::uint64_t), stamps.data(), sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); + const auto result = context.Function("vkGetQueryPoolResults")(context.device, batch.queries, 0, static_cast(stamps.size()), stamps.size() * sizeof(std::uint64_t), stamps.data(), sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT); if (result != VK_SUCCESS) return; const auto period = context.limits.timestampPeriod; if (!GpuTimingEnabled()) { if (batch.batchTiming == 0 && stamps[1] >= stamps[0]) { batch.gpuStartNs = static_cast(stamps[0]) * period; batch.gpuEndNs = static_cast(stamps[1]) * period; - PerformanceTrace::GpuRange("batch", batch.gpuStartNs, batch.gpuEndNs); + PerformanceTrace::GpuRange("batch", batch.gpuStartNs, batch.gpuEndNs, batch.traceId); } return; } @@ -2383,12 +2603,13 @@ void Recorder::readGpuTiming(Batch& batch) { // The whole batch: apart from the per-program totals it contains. batch.gpuStartNs = static_cast(stamps[i * 2]) * period; batch.gpuEndNs = static_cast(stamps[i * 2 + 1]) * period; - PerformanceTrace::GpuRange("batch", batch.gpuStartNs, batch.gpuEndNs); + PerformanceTrace::GpuRange("batch", batch.gpuStartNs, batch.gpuEndNs, batch.traceId); timingBatchMs += ns / 1e6; + AgcDriver::PerformanceCounters::AddGpuNanoseconds(static_cast(ns)); continue; } const auto key = batch.timedKeys[i]; - if (key >= ClassKey(CommandClass::DispatchLeading) && key < ClassKey(CommandClass::Count)) PerformanceTrace::GpuRange(CommandClassNames[key - ClassKey(CommandClass::DispatchLeading)], static_cast(stamps[i * 2]) * period, static_cast(stamps[i * 2 + 1]) * period); + if (key >= ClassKey(CommandClass::DispatchLeading) && key < ClassKey(CommandClass::Count)) PerformanceTrace::GpuRange(CommandClassNames[key - ClassKey(CommandClass::DispatchLeading)], static_cast(stamps[i * 2]) * period, static_cast(stamps[i * 2 + 1]) * period, batch.traceId); auto& totals = timingByKey[key]; ++totals.count; totals.ms += ns / 1e6; @@ -2511,6 +2732,43 @@ void Recorder::eraseDrawSnapshot(std::map::iterat drawSnapshots.erase(entry); } +Recorder::UploadSlice Recorder::DrawUpload(std::size_t bytes) { + const VkDeviceSize alignment = std::max(context.limits.minStorageBufferOffsetAlignment, 16); + auto offset = (drawUploadUsed + alignment - 1) & ~(alignment - 1); + if (drawUpload == nullptr || offset + bytes > drawUpload->Bytes().size()) { + drawUpload = std::make_shared(context, std::max(DrawUploadChunk, bytes), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + offset = 0; + } + drawUploadUsed = offset + bytes; + return {drawUpload, offset, drawUpload->Bytes().data() + offset}; +} + +void Recorder::retireUnwrittenSnapshots(const std::array& stamp) { + if (stamp == unwrittenStamp) return; + unwrittenSnapshots.clear(); + unwrittenBytes = 0; + unwrittenStamp = stamp; +} + +std::shared_ptr Recorder::UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, VkDeviceSize* offset) { + retireUnwrittenSnapshots(stamp); + const auto found = unwrittenSnapshots.find(address); + if (found == unwrittenSnapshots.end() || found->second.bytes != bytes) return {}; + if (offset != nullptr) *offset = found->second.offset; + return found->second.buffer; +} + +void Recorder::NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, std::shared_ptr buffer, VkDeviceSize offset) { + retireUnwrittenSnapshots(stamp); + if (const auto found = unwrittenSnapshots.find(address); found != unwrittenSnapshots.end()) { + unwrittenBytes -= found->second.bytes; + unwrittenSnapshots.erase(found); + } + if (unwrittenSnapshots.size() >= UnwrittenSnapshotEntries || bytes > DrawSnapshotBudget - unwrittenBytes) return; + unwrittenSnapshots.emplace(address, UnwrittenSnapshot{bytes, std::move(buffer), offset}); + unwrittenBytes += bytes; +} + std::shared_ptr Recorder::ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::uint32_t* derived, std::uint64_t generation) { auto found = use == SnapshotUse::Vertex ? drawSnapshots.lower_bound({address, use, bytes}) : drawSnapshots.find({address, use, bytes}); if (found == drawSnapshots.end() || std::get<0>(found->first) != address || std::get<1>(found->first) != use) return {}; @@ -2639,14 +2897,16 @@ void Recorder::OnComplete(std::function action) { writeBackCompletions.fetch_add(1, std::memory_order_acq_rel); } -bool Recorder::noteWrite(std::uint64_t address, std::size_t bytes, bool ownLabel, int value) { +bool Recorder::noteWrite(std::uint64_t address, std::size_t bytes, bool ownLabel, int value, std::int64_t word) { CaptureTrace::Log("buffer-write batch=%llu address=%llx bytes=%zu label=%d", static_cast(submissions + 1), static_cast(address), bytes, ownLabel); if (bytes == 0) return false; ensureOpen(); const auto end = address + bytes; open->writes.emplace_back(address, end); open->writeNotes.push_back(++writeNoteCount); + if (published != nullptr && !SnapshotCovers(published.get(), address, end)) published = nullptr; open->writeValues.push_back(static_cast(value)); + open->writeWords.push_back(word); if (!ownLabel) markOverwritten(address, end); // A poller waiting on this range learns that the open batch may now hold its producer. if (activeRecorder == this) writeGeneration.fetch_add(1, std::memory_order_release); @@ -2673,7 +2933,9 @@ void Recorder::noteWriteOn(Batch& batch, std::uint64_t address, std::size_t byte // moves as well, so a poller re-consults the label table for a completion label. batch.writes.emplace_back(address, address + bytes); batch.writeNotes.push_back(++writeNoteCount); + if (published != nullptr && !SnapshotCovers(published.get(), address, address + bytes)) published = nullptr; batch.writeValues.push_back(-1); + batch.writeWords.push_back(-1); if (!ownLabel) markOverwritten(address, address + bytes); if (activeRecorder == this) writeGeneration.fetch_add(1, std::memory_order_release); if (!SnapshotCovers(address, address + bytes)) { @@ -2700,7 +2962,15 @@ void Recorder::NotePendingWrite(std::uint64_t address, std::size_t bytes) { } void Recorder::NotePendingFill(std::uint64_t address, std::size_t bytes, std::uint8_t value) { - if (!noteWrite(address, bytes, false, value)) return; + if (!noteWrite(address, bytes, false, value, value * std::int64_t{0x01010101})) return; + const std::array ranges{std::pair{address, address + bytes}}; + publishPendingWrites(ranges); + std::atomic_thread_fence(std::memory_order_seq_cst); +} + +void Recorder::NotePendingWordFill(std::uint64_t address, std::size_t bytes, std::uint32_t word) { + const int value = word == (word & 0xffu) * 0x01010101u ? static_cast(word & 0xffu) : -1; + if (!noteWrite(address, bytes, false, value, word)) return; const std::array ranges{std::pair{address, address + bytes}}; publishPendingWrites(ranges); std::atomic_thread_fence(std::memory_order_seq_cst); @@ -2761,6 +3031,7 @@ void Recorder::publishPendingWrites(std::spanbegin(), merged->end(), [](const auto& range) { return range.second > range.first; }); publishedWriteRanges = merged; + published = merged; pendingWrites.store(std::move(merged), std::memory_order_release); publishGeneration.fetch_add(1, std::memory_order_release); ++snapshotRebuilds; @@ -2776,6 +3047,7 @@ bool Recorder::overlaps(const Batch& batch, std::uint64_t address, std::uint64_t bool Recorder::PendingWriteOverlaps(std::uint64_t address, std::size_t bytes) const { if (bytes == 0) return false; + if (published != nullptr && !SnapshotOverlaps(published.get(), address, bytes)) return false; const auto end = address + bytes; if (open != nullptr && overlaps(*open, address, end)) return true; for (const auto& batch : inFlight) { @@ -2788,6 +3060,17 @@ bool Recorder::OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) const return bytes != 0 && open != nullptr && overlaps(*open, address, address + bytes); } +std::uint64_t Recorder::PendingWriterSerial(std::uint64_t address, std::size_t bytes) const { + if (bytes == 0) return 0; + const auto end = address + bytes; + const auto writes = [&](const Batch& batch) { return overlaps(batch, address, end) || readOverlap(batch, address, end, ReadKind::AddressBased).has_value(); }; + if (open != nullptr && writes(*open)) return OpenSerial(); + for (auto it = inFlight.rbegin(); it != inFlight.rend(); ++it) { + if (writes(**it)) return (*it)->serial; + } + return 0; +} + bool Recorder::signaled(const Batch& batch) const { return batch.submitted && fenceStatus(batch.fence) == VK_SUCCESS; } @@ -2828,7 +3111,7 @@ std::vector Recorder::PendingWritesOver(std::uint64_t ad const auto end = address + bytes; const auto scan = [&](const Batch& batch) { for (std::size_t i = 0; i < batch.writes.size(); ++i) { - if (address < batch.writes[i].second && batch.writes[i].first < end) found.push_back({batch.writes[i].first, batch.writes[i].second, batch.writeNotes[i], batch.writeValues[i]}); + if (address < batch.writes[i].second && batch.writes[i].first < end) found.push_back({batch.writes[i].first, batch.writes[i].second, batch.writeNotes[i], batch.writeValues[i], batch.writeWords[i]}); } }; if (open != nullptr) scan(*open); @@ -2836,6 +3119,12 @@ std::vector Recorder::PendingWritesOver(std::uint64_t ad return found; } +void Recorder::PendingWriteRanges(std::vector>& ranges) const { + ranges.clear(); + if (open != nullptr) ranges.insert(ranges.end(), open->writes.begin(), open->writes.end()); + for (const auto& batch : inFlight) ranges.insert(ranges.end(), batch->writes.begin(), batch->writes.end()); +} + bool Recorder::ReadTracking() { return ReadTrackingEnabled(); } @@ -2858,11 +3147,61 @@ void Recorder::NotePendingReads(std::span ranges, ReadKind kind) { + if (ranges == nullptr || ranges->empty() || !ReadTrackingEnabled()) return; + if (CaptureTrace::Enabled() || FlipReadCheck()) { + NotePendingReads(*ranges, kind); + return; + } + ensureOpen(); + readsNoted.fetch_add(ranges->size(), std::memory_order_relaxed); + for (const auto& note : open->readLists) { + if (note.ranges == ranges && note.kind == kind) return; + } + open->readLists.push_back({std::move(ranges), open->reads.size(), kind}); +} + +std::optional Recorder::readOverlap(const Batch& batch, std::uint64_t address, std::uint64_t end, std::optional only) { + const auto listOverlaps = [&](const Batch::ReadListNote& note) { + if (only.has_value() && note.kind != *only) return false; + const auto& list = *note.ranges; + const auto found = std::partition_point(list.begin(), list.end(), [&](const auto& range) { return range.second <= address; }); + return found != list.end() && found->first < end; + }; + std::size_t next = 0; + for (std::size_t index = 0; index < batch.reads.size(); ++index) { + for (; next < batch.readLists.size() && batch.readLists[next].position == index; ++next) { + if (listOverlaps(batch.readLists[next])) return batch.readLists[next].kind; + } + const auto& read = batch.reads[index]; + if ((!only.has_value() || read.kind == *only) && address < read.end && read.begin < end) return read.kind; + } + for (; next < batch.readLists.size(); ++next) { + if (listOverlaps(batch.readLists[next])) return batch.readLists[next].kind; + } + return std::nullopt; +} + +std::vector> Recorder::notedReads(const Batch& batch) { + std::vector> result; + std::size_t next = 0; + const auto expand = [&](std::size_t position) { + for (; next < batch.readLists.size() && batch.readLists[next].position == position; ++next) result.insert(result.end(), batch.readLists[next].ranges->begin(), batch.readLists[next].ranges->end()); + }; + for (std::size_t index = 0; index < batch.reads.size(); ++index) { + expand(index); + result.emplace_back(batch.reads[index].begin, batch.reads[index].end); + } + expand(batch.reads.size()); + return result; } bool Recorder::PendingReadOverlaps(std::uint64_t address, std::size_t bytes, bool ignoreSignaled) const { @@ -2870,20 +3209,20 @@ bool Recorder::PendingReadOverlaps(std::uint64_t address, std::size_t bytes, boo readQueries.fetch_add(1, std::memory_order_relaxed); const auto end = address + bytes; if (open != nullptr) { - if (const auto* read = readOverlap(*open, address, end)) { - readHits[static_cast(read->kind)].fetch_add(1, std::memory_order_relaxed); + if (const auto kind = readOverlap(*open, address, end)) { + readHits[static_cast(*kind)].fetch_add(1, std::memory_order_relaxed); return true; } } // Newest first: the batch most likely still running decides without a status query per batch. for (auto it = inFlight.rbegin(); it != inFlight.rend(); ++it) { - const auto* read = readOverlap(**it, address, end); - if (read == nullptr) continue; + const auto kind = readOverlap(**it, address, end); + if (!kind.has_value()) continue; if (ignoreSignaled && signaled(**it)) { readStaleIgnored.fetch_add(1, std::memory_order_relaxed); continue; } - readHits[static_cast(read->kind)].fetch_add(1, std::memory_order_relaxed); + readHits[static_cast(*kind)].fetch_add(1, std::memory_order_relaxed); return true; } return false; @@ -2893,10 +3232,10 @@ std::optional Recorder::DescribePendingRead(std::uint if (bytes == 0) return std::nullopt; const auto end = address + bytes; if (open != nullptr) { - if (const auto* read = readOverlap(*open, address, end)) return PendingReadInfo{submissions + 1, open->queue, read->kind, true, false}; + if (const auto kind = readOverlap(*open, address, end)) return PendingReadInfo{submissions + 1, open->queue, *kind, true, false}; } for (auto it = inFlight.rbegin(); it != inFlight.rend(); ++it) { - if (const auto* read = readOverlap(**it, address, end)) return PendingReadInfo{(*it)->serial, (*it)->queue, read->kind, false, signaled(**it)}; + if (const auto kind = readOverlap(**it, address, end)) return PendingReadInfo{(*it)->serial, (*it)->queue, *kind, false, signaled(**it)}; } return std::nullopt; } @@ -3173,6 +3512,11 @@ bool Recorder::writtenBackSince(std::uint64_t sequence, std::uint64_t begin, std return false; } +void Recorder::HoldNextSubmit(VkSemaphore semaphore, std::uint64_t value) { + submitHold = semaphore; + submitHoldValue = value; +} + void Recorder::Submit() { PerformanceTimer frameTiming("Recorder.Submit"); // The work count is cleared even when nothing is open: the driver counts a dispatch after its @@ -3204,9 +3548,16 @@ void Recorder::Submit() { if (FlipReadCheck()) { // A CPU write into a noted read after this collect (and so possibly before the GPU read // it) fails UnchangedSince at the presenter's check (VulkanDevice RetirePresents). - for (const auto& read : open->reads) open->readGeneration = std::max(open->readGeneration, GuestMemory::CollectWrites(read.begin, read.end - read.begin)); + 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; @@ -3215,15 +3566,30 @@ void Recorder::Submit() { const std::uint64_t serial = submissions + 1; CaptureTrace::Log("submit batch=%llu reads=%zu writes=%zu", static_cast(serial), batch->reads.size(), batch->writes.size()); VkTimelineSemaphoreSubmitInfoKHR timelineInfo{VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO_KHR}; - timelineInfo.signalSemaphoreValueCount = 1; - timelineInfo.pSignalSemaphoreValues = &serial; + const VkSemaphore hold = std::exchange(submitHold, VK_NULL_HANDLE); + const std::uint64_t holdValue = submitHoldValue; + VkPipelineStageFlags holdStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + if (hold != VK_NULL_HANDLE) { + submission.waitSemaphoreCount = 1; + submission.pWaitSemaphores = &hold; + submission.pWaitDstStageMask = &holdStage; + timelineInfo.waitSemaphoreValueCount = 1; + timelineInfo.pWaitSemaphoreValues = &holdValue; + } if (timeline != VK_NULL_HANDLE) { - submission.pNext = &timelineInfo; + timelineInfo.signalSemaphoreValueCount = 1; + timelineInfo.pSignalSemaphoreValues = &serial; submission.signalSemaphoreCount = 1; submission.pSignalSemaphores = &timeline; } + if (hold != VK_NULL_HANDLE || timeline != VK_NULL_HANDLE) submission.pNext = &timelineInfo; const auto submitStart = std::chrono::steady_clock::now(); - 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(); @@ -3239,6 +3605,13 @@ void Recorder::Submit() { newestSubmitted = batch->serial; newestSubmittedAt = batch->submittedAt; } + if (timeline != VK_NULL_HANDLE) { + if (!progressWatcher) { + progressWatcher = std::make_unique(context.device, timeline, context.Function("vkWaitSemaphoresKHR"), + context.Function("vkGetSemaphoreCounterValueKHR")); + } + progressWatcher->Submitted(batch->serial); + } inFlight.push_back(std::move(batch)); inFlightKeptBytes += inFlight.back()->keptBytes; if (activeRecorder == this) pendingLabelSince.store(NoPendingLabel, std::memory_order_release); @@ -3703,7 +4076,7 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source, bool entry.readGeneration = batch->readGeneration; entry.reads.clear(); if (FlipReadCheck()) { - for (const auto& read : batch->reads) entry.reads.emplace_back(read.begin, read.end); + entry.reads = notedReads(*batch); } } // Completions store GPU results to guest memory; a failing one is reported, the rest still run. @@ -3719,11 +4092,22 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source, bool for (auto& action : batch->completions) { try { action(); + } catch (const BdaUnalignedAccess& error) { + static std::mutex failuresMutex; + static std::unordered_map failures; + constexpr std::uint64_t kDeferredFailureFirst = 8, kDeferredFailureEvery = 1024; + const auto count = [&] { + std::lock_guard lock(failuresMutex); + return ++failures[error.instruction]; + }(); + if (count <= kDeferredFailureFirst) std::fprintf(stderr, "[gpu] deferred write-back failed: %s\n", error.what()); + else if (count % kDeferredFailureEvery == 0) std::fprintf(stderr, "[gpu] deferred write-back failed with an unaligned BDA access at pc 0x%x %llu times so far; latest: %s\n", error.instruction, static_cast(count), error.what()); } catch (const std::exception& error) { - 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()); } } } + if (!batch->completions.empty()) KernelWakeSleepers_nid_no_patch(); if (profile) holdCounters.completions += batch->completions.size(); const auto keptStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; // Deferred only from inside a hold: the unlock that releases the list is this thread's own, and @@ -3752,18 +4136,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 e675a2c212..4f30207def 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 { @@ -173,9 +177,13 @@ DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsa VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = ClassAllocationBytes(requirements, capacity); allocationBytes = allocation.allocationSize; - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - memory = cache->AllocateMemory(allocation, "vkAllocateMemory device buffer"); + allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, allocation.allocationSize, "device buffer"); + const auto deviceType = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + std::uint32_t allocatedType = allocation.memoryTypeIndex; + memory = cache->AllocateMemory(allocation, "vkAllocateMemory device buffer", context.RefusedDeviceMemoryFallback(requirements.memoryTypeBits, allocation.memoryTypeIndex), &allocatedType); + hostPlaced = allocatedType != deviceType; Check(context.Function("vkBindBufferMemory")(context.device, buffer, memory, 0), "vkBindBufferMemory device"); + ready = true; } catch (...) { release(); throw; @@ -187,7 +195,7 @@ DeviceBuffer::~DeviceBuffer() { } void DeviceBuffer::release() noexcept { - if (buffer && memory && cache) { + 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; } @@ -286,7 +294,7 @@ RenderTarget::RenderTarget(const Context& context, const ColorTarget& target, bo 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 render target"); + memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory render target"); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory"); VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; @@ -328,7 +336,9 @@ CommandBatch::CommandBatch(const Context& context) : context(context) { allocation.commandBufferCount = 1; Check(context.Function("vkAllocateCommandBuffers")(context.device, &allocation, &commands), "vkAllocateCommandBuffers"); VkFenceCreateInfo info{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; - Check(context.Function("vkCreateFence")(context.device, &info, nullptr, &fence), "vkCreateFence"); + VkFence created = VK_NULL_HANDLE; + Check(context.Function("vkCreateFence")(context.device, &info, nullptr, &created), "vkCreateFence"); + fence = created; VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; Check(context.Function("vkBeginCommandBuffer")(commands, &begin), "vkBeginCommandBuffer"); @@ -372,9 +382,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/SampleOrder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp new file mode 100644 index 0000000000..dfcef91efc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp @@ -0,0 +1,672 @@ +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderDepth_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderR16_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderR32_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderR8_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderRG32_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderRGBA32_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderStencilBits_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderStencilExport_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderVertex_spv.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { +namespace { + +constexpr VkAccessFlags anyWrite = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; +constexpr VkAccessFlags anyAccess = anyWrite | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_INPUT_ATTACHMENT_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT; +constexpr VkPipelineStageFlags reorderStages = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; +constexpr std::uint32_t setsPerPool = 64; + +struct StoragePush { + std::array map; + std::array extent; + std::uint32_t count; +}; + +struct DepthPush { + std::array map; + std::uint32_t bit; +}; + +bool exactDepth(VkFormat format) { + return format == VK_FORMAT_D16_UNORM || format == VK_FORMAT_D32_SFLOAT || format == VK_FORMAT_D16_UNORM_S8_UINT || format == VK_FORMAT_D32_SFLOAT_S8_UINT; +} + +bool stencilOf(const Context& context, const DepthTarget& depth) { + return depth.stencilAddress != 0 && HasStencil(HostDepthFormat(context, depth.format)); +} + +bool depthSampleable(const Context& context, VkFormat format, std::uint32_t samples) { + if (context.imageFormatProperties == nullptr || context.formatProperties == nullptr) return false; + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + if ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) return false; + VkImageFormatProperties supported{}; + const VkImageCreateFlags flags = SampleLocationsCompatibleDepth(context, format, samples) ? VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT : 0u; + constexpr VkImageUsageFlags usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + if (context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, flags, &supported) != VK_SUCCESS) return false; + return (supported.sampleCounts & samples) != 0; +} + +std::array mapFor(const SampleOrder& order, bool toHost) { + std::array map{}; + for (std::size_t pixel = 0; pixel < map.size(); ++pixel) { + const auto word = order[pixel] == 0u ? KeptSampleOrder : order[pixel]; + map[pixel] = toHost ? word : InvertSampleOrder(word); + } + return map; +} + +VkExtent2D regionOf(const SampleOrderPass& pass, VkExtent2D extent) { + return {std::min(pass.extent.width, extent.width), std::min(pass.extent.height, extent.height)}; +} + +void transitionLocated(const Context& context, VkCommandBuffer commands, VkImage image, VkFormat format, const VkSampleLocationsInfoEXT& locations, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags sourceStages, VkAccessFlags sourceAccess, VkPipelineStageFlags destinationStages, VkAccessFlags destinationAccess) { + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, &locations}; + barrier.srcAccessMask = sourceAccess; + barrier.dstAccessMask = destinationAccess; + barrier.oldLayout = from; + barrier.newLayout = to; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT | (HasStencil(format) ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u), 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, sourceStages, destinationStages, 0, 0, nullptr, 0, nullptr, 1, &barrier); +} + +class CopyImage { +public: + CopyImage(const Context& context, VkFormat format, std::uint32_t samples, VkExtent2D extent, bool stencil) : context(context), format(format), samples(samples), extent(extent) { + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {extent.width, extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = static_cast(samples); + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage sample order copy"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory sample order copy"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory sample order copy"); + depthView = createView(VK_IMAGE_ASPECT_DEPTH_BIT); + if (stencil) stencilView = createView(VK_IMAGE_ASPECT_STENCIL_BIT); + } catch (...) { + release(); + throw; + } + } + ~CopyImage() { release(); } + CopyImage(const CopyImage&) = delete; + CopyImage& operator=(const CopyImage&) = delete; + + Context context; + VkFormat format; + std::uint32_t samples; + VkExtent2D extent; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView depthView = VK_NULL_HANDLE; + VkImageView stencilView = VK_NULL_HANDLE; + bool general = false; + +private: + VkImageView createView(VkImageAspectFlags aspect) { + VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + info.image = image; + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.format = format; + info.subresourceRange = {aspect, 0, 1, 0, 1}; + VkImageView view = VK_NULL_HANDLE; + Check(context.Function("vkCreateImageView")(context.device, &info, nullptr, &view), "vkCreateImageView sample order copy"); + return view; + } + + void release() noexcept { + if (stencilView) context.Function("vkDestroyImageView")(context.device, stencilView, nullptr); + if (depthView) context.Function("vkDestroyImageView")(context.device, depthView, nullptr); + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + } +}; + +class SampleOrders { +public: + explicit SampleOrders(const Context& context) : context(context) { + this->context.bufferPool.reset(); + } + ~SampleOrders() { release(); } + SampleOrders(const SampleOrders&) = delete; + SampleOrders& operator=(const SampleOrders&) = delete; + + void Storage(VkCommandBuffer commands, const SampleOrderTarget& target, const std::array& map, VkExtent2D region) { + const auto pipeline = storagePipeline(target.format); + const auto set = allocate(storageSetLayout, {{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, setsPerPool}}); + const VkDescriptorImageInfo imageInfo{VK_NULL_HANDLE, target.view, VK_IMAGE_LAYOUT_GENERAL}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = set; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + write.pImageInfo = &imageInfo; + context.Function("vkUpdateDescriptorSets")(context.device, 1, &write, 0, nullptr); + const StoragePush push{map, {region.width, region.height}, target.samples}; + context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); + context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, storagePipelineLayout, 0, 1, &set, 0, nullptr); + context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, storagePipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), &push); + context.Resolved(&DeviceFunctions::cmdDispatch, "vkCmdDispatch")(commands, ((region.width + 1u) / 2u + 7u) / 8u, ((region.height + 1u) / 2u + 7u) / 8u, 1); + } + + void Depth(VkCommandBuffer commands, const SampleOrderTarget& target, const std::array& map, VkExtent2D region, const SampleOrderPass& order, std::vector>& kept) { + const bool stencil = target.method != SampleOrderMethod::Depth; + auto copy = copyFor(target.format, target.samples, target.extent, stencil); + kept.push_back(copy); + const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); + if (!copy->general) { + VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.image = copy->image; + toGeneral.subresourceRange = {aspects, 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + copy->general = true; + } + VkImageCopy region3d{}; + region3d.srcSubresource = {aspects, 0, 0, 1}; + region3d.dstSubresource = {aspects, 0, 0, 1}; + region3d.extent = {region.width, region.height, 1}; + const auto& locations = order.locations; + const bool located = order.located; + const VkSampleLocationsInfoEXT info{VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast(target.samples), {1, 1}, target.samples, locations.data()}; + constexpr VkPipelineStageFlags fragmentStages = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + constexpr VkAccessFlags depthAccess = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + if (located) transitionLocated(context, commands, target.image, target.format, info, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, anyWrite, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT); + context.Function("vkCmdCopyImage")(commands, target.image, located ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL, copy->image, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion3d); + if (located) transitionLocated(context, commands, target.image, target.format, info, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, fragmentStages, depthAccess); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, fragmentStages, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | depthAccess); + const auto& pass = depthPass(target.format, target.samples, target.method, located, locations); + const auto set = allocate(sampledSetLayout, {{VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, setsPerPool * 2u}}); + std::array images{{{VK_NULL_HANDLE, copy->depthView, VK_IMAGE_LAYOUT_GENERAL}, {VK_NULL_HANDLE, stencil ? copy->stencilView : copy->depthView, VK_IMAGE_LAYOUT_GENERAL}}}; + std::array writes{}; + for (std::uint32_t binding = 0; binding < writes.size(); ++binding) { + writes[binding].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[binding].dstSet = set; + writes[binding].dstBinding = binding; + writes[binding].descriptorCount = 1; + writes[binding].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + writes[binding].pImageInfo = &images[binding]; + } + context.Function("vkUpdateDescriptorSets")(context.device, stencil ? 2u : 1u, writes.data(), 0, nullptr); + const std::array views{target.view}; + auto framebuffer = std::make_shared(context, pass.renderPass, views, region); + kept.push_back(framebuffer); + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = pass.renderPass; + begin.framebuffer = framebuffer->Handle(); + begin.renderArea = {{0, 0}, region}; + const VkAttachmentSampleLocationsEXT initial{0, info}; + const VkSubpassSampleLocationsEXT post{0, info}; + VkRenderPassSampleLocationsBeginInfoEXT sampleLocations{VK_STRUCTURE_TYPE_RENDER_PASS_SAMPLE_LOCATIONS_BEGIN_INFO_EXT}; + sampleLocations.attachmentInitialSampleLocationsCount = 1; + sampleLocations.pAttachmentInitialSampleLocations = &initial; + sampleLocations.postSubpassSampleLocationsCount = 1; + sampleLocations.pPostSubpassSampleLocations = &post; + if (located) begin.pNext = &sampleLocations; + context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + const VkViewport viewport{0.0f, 0.0f, static_cast(region.width), static_cast(region.height), 0.0f, 1.0f}; + const VkRect2D scissor{{0, 0}, region}; + context.Resolved(&DeviceFunctions::cmdSetViewport, "vkCmdSetViewport")(commands, 0, 1, &viewport); + context.Resolved(&DeviceFunctions::cmdSetScissor, "vkCmdSetScissor")(commands, 0, 1, &scissor); + context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, sampledPipelineLayout, 0, 1, &set, 0, nullptr); + const auto draw = [&](VkPipeline pipeline, std::uint32_t bit, std::uint32_t writeMask, std::uint32_t reference) { + context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + context.Resolved(&DeviceFunctions::cmdSetStencilWriteMask, "vkCmdSetStencilWriteMask")(commands, VK_STENCIL_FACE_FRONT_AND_BACK, writeMask); + context.Resolved(&DeviceFunctions::cmdSetStencilReference, "vkCmdSetStencilReference")(commands, VK_STENCIL_FACE_FRONT_AND_BACK, reference); + const DepthPush push{map, bit}; + context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, sampledPipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), &push); + context.Resolved(&DeviceFunctions::cmdDraw, "vkCmdDraw")(commands, 3, 1, 0, 0); + }; + draw(pass.depth, 0u, 0xffu, 0u); + if (target.method == SampleOrderMethod::DepthStencilBits) { + for (std::uint32_t bit = 0; bit < 8u; ++bit) draw(pass.stencilBits, bit, 1u << bit, 0xffu); + } + context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands); + } + + Context context; + +private: + struct Ring { + std::vector pools; + std::vector serials; + std::size_t current = 0; + std::uint32_t used = 0; + }; + + struct DepthPass { + VkRenderPass renderPass = VK_NULL_HANDLE; + VkPipeline depth = VK_NULL_HANDLE; + VkPipeline stencilBits = VK_NULL_HANDLE; + }; + + using DepthKey = std::tuple>; + using CopyKey = std::tuple; + + VkShaderModule module(const std::uint32_t* code, std::size_t bytes) { + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = bytes; + info.pCode = code; + VkShaderModule created = VK_NULL_HANDLE; + Check(context.Function("vkCreateShaderModule")(context.device, &info, nullptr, &created), "vkCreateShaderModule sample order"); + return created; + } + + void destroyModule(VkShaderModule shader) { + context.Function("vkDestroyShaderModule")(context.device, shader, nullptr); + } + + VkDescriptorSetLayout setLayout(VkDescriptorType type, std::uint32_t count, VkShaderStageFlags stage) { + std::array bindings{}; + for (std::uint32_t binding = 0; binding < count; ++binding) bindings[binding] = {binding, type, 1, stage, nullptr}; + VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + info.bindingCount = count; + info.pBindings = bindings.data(); + VkDescriptorSetLayout layout = VK_NULL_HANDLE; + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &info, nullptr, &layout), "vkCreateDescriptorSetLayout sample order"); + return layout; + } + + VkPipelineLayout pipelineLayout(VkDescriptorSetLayout set, VkShaderStageFlags stage, std::uint32_t pushBytes) { + const VkPushConstantRange range{stage, 0, pushBytes}; + VkPipelineLayoutCreateInfo info{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + info.setLayoutCount = 1; + info.pSetLayouts = &set; + info.pushConstantRangeCount = 1; + info.pPushConstantRanges = ⦥ + VkPipelineLayout layout = VK_NULL_HANDLE; + Check(context.Function("vkCreatePipelineLayout")(context.device, &info, nullptr, &layout), "vkCreatePipelineLayout sample order"); + return layout; + } + + VkPipeline storagePipeline(VkFormat format) { + const std::array formats{VK_FORMAT_R8_UINT, VK_FORMAT_R16_UINT, VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT, VK_FORMAT_R32G32B32A32_UINT}; + const auto found = std::ranges::find(formats, format); + Require(found != formats.end(), "reordering the samples of a color target through a storage view of this format is not implemented"); + const auto index = static_cast(found - formats.begin()); + if (storagePipelines[index] != VK_NULL_HANDLE) return storagePipelines[index]; + if (storageSetLayout == VK_NULL_HANDLE) storageSetLayout = setLayout(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT); + if (storagePipelineLayout == VK_NULL_HANDLE) storagePipelineLayout = pipelineLayout(storageSetLayout, VK_SHADER_STAGE_COMPUTE_BIT, sizeof(StoragePush)); + const std::array, 5> code{{{SAMPLE_ORDER_R8_SPV, sizeof(SAMPLE_ORDER_R8_SPV)}, {SAMPLE_ORDER_R16_SPV, sizeof(SAMPLE_ORDER_R16_SPV)}, {SAMPLE_ORDER_R32_SPV, sizeof(SAMPLE_ORDER_R32_SPV)}, {SAMPLE_ORDER_RG32_SPV, sizeof(SAMPLE_ORDER_RG32_SPV)}, {SAMPLE_ORDER_RGBA32_SPV, sizeof(SAMPLE_ORDER_RGBA32_SPV)}}}; + const auto shader = module(code[index].first, code[index].second); + VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + info.stage.module = shader; + info.stage.pName = "main"; + info.layout = storagePipelineLayout; + VkResult created; + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + created = context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &info, nullptr, &storagePipelines[index]); + } + destroyModule(shader); + Check(created, "vkCreateComputePipelines sample order"); + return storagePipelines[index]; + } + + const DepthPass& depthPass(VkFormat format, std::uint32_t samples, SampleOrderMethod method, bool located, const std::array& locations) { + std::array standIn{}; + if (located) { + for (std::uint32_t sample = 0; sample < samples && sample < 8u; ++sample) { + standIn[sample * 2u] = std::bit_cast(locations[sample].x); + standIn[sample * 2u + 1u] = std::bit_cast(locations[sample].y); + } + } + const DepthKey key{format, samples, method, located, standIn}; + if (const auto found = depthPasses.find(key); found != depthPasses.end()) return found->second; + if (sampledSetLayout == VK_NULL_HANDLE) sampledSetLayout = setLayout(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 2, VK_SHADER_STAGE_FRAGMENT_BIT); + if (sampledPipelineLayout == VK_NULL_HANDLE) sampledPipelineLayout = pipelineLayout(sampledSetLayout, VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(DepthPush)); + const bool stencil = method != SampleOrderMethod::Depth; + DepthPass pass; + try { + VkAttachmentDescription attachment{}; + attachment.format = format; + attachment.samples = static_cast(samples); + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.stencilLoadOp = stencil ? VK_ATTACHMENT_LOAD_OP_LOAD : VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = stencil ? VK_ATTACHMENT_STORE_OP_STORE : VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = VK_IMAGE_LAYOUT_GENERAL; + attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + if (HasStencil(format) && !stencil) { + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; + } + const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_GENERAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.pDepthStencilAttachment = &reference; + VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + passInfo.attachmentCount = 1; + passInfo.pAttachments = &attachment; + passInfo.subpassCount = 1; + passInfo.pSubpasses = &subpass; + Check(context.Function("vkCreateRenderPass")(context.device, &passInfo, nullptr, &pass.renderPass), "vkCreateRenderPass sample order"); + const auto create = [&](const std::uint32_t* code, std::size_t bytes, bool writesDepth) { + const auto vertex = module(SAMPLE_ORDER_VERTEX_SPV, sizeof(SAMPLE_ORDER_VERTEX_SPV)); + const auto fragment = module(code, bytes); + std::array stages{}; + stages[0] = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_VERTEX_BIT, vertex, "main", nullptr}; + stages[1] = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_FRAGMENT_BIT, fragment, "main", nullptr}; + VkPipelineVertexInputStateCreateInfo input{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + VkPipelineViewportStateCreateInfo viewports{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewports.viewportCount = 1; + viewports.scissorCount = 1; + VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + raster.polygonMode = VK_POLYGON_MODE_FILL; + raster.cullMode = VK_CULL_MODE_NONE; + raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + raster.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = static_cast(samples); + multisample.sampleShadingEnable = VK_TRUE; + multisample.minSampleShading = 1.0f; + VkPipelineSampleLocationsStateCreateInfoEXT sampleLocations{VK_STRUCTURE_TYPE_PIPELINE_SAMPLE_LOCATIONS_STATE_CREATE_INFO_EXT}; + sampleLocations.sampleLocationsEnable = VK_TRUE; + sampleLocations.sampleLocationsInfo = {VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast(samples), {1, 1}, samples, locations.data()}; + if (located) multisample.pNext = &sampleLocations; + VkStencilOpState replace{}; + replace.failOp = VK_STENCIL_OP_REPLACE; + replace.passOp = VK_STENCIL_OP_REPLACE; + replace.depthFailOp = VK_STENCIL_OP_REPLACE; + replace.compareOp = VK_COMPARE_OP_ALWAYS; + replace.compareMask = 0xffu; + replace.writeMask = 0xffu; + VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; + depthStencil.depthTestEnable = writesDepth ? VK_TRUE : VK_FALSE; + depthStencil.depthWriteEnable = writesDepth ? VK_TRUE : VK_FALSE; + depthStencil.depthCompareOp = VK_COMPARE_OP_ALWAYS; + depthStencil.stencilTestEnable = stencil ? VK_TRUE : VK_FALSE; + depthStencil.front = replace; + depthStencil.back = replace; + const std::array dynamicStates{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, VK_DYNAMIC_STATE_STENCIL_WRITE_MASK, VK_DYNAMIC_STATE_STENCIL_REFERENCE}; + VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + dynamic.dynamicStateCount = static_cast(dynamicStates.size()); + dynamic.pDynamicStates = dynamicStates.data(); + VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + info.stageCount = static_cast(stages.size()); + info.pStages = stages.data(); + info.pVertexInputState = &input; + info.pInputAssemblyState = &assembly; + info.pViewportState = &viewports; + info.pRasterizationState = &raster; + info.pMultisampleState = &multisample; + info.pDepthStencilState = &depthStencil; + info.pDynamicState = &dynamic; + info.layout = sampledPipelineLayout; + info.renderPass = pass.renderPass; + VkPipeline pipeline = VK_NULL_HANDLE; + VkResult created; + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + created = context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &info, nullptr, &pipeline); + } + destroyModule(fragment); + destroyModule(vertex); + Check(created, "vkCreateGraphicsPipelines sample order"); + return pipeline; + }; + if (method == SampleOrderMethod::DepthStencilExport) pass.depth = create(SAMPLE_ORDER_STENCILEXPORT_SPV, sizeof(SAMPLE_ORDER_STENCILEXPORT_SPV), true); + else pass.depth = create(SAMPLE_ORDER_DEPTH_SPV, sizeof(SAMPLE_ORDER_DEPTH_SPV), true); + if (method == SampleOrderMethod::DepthStencilBits) pass.stencilBits = create(SAMPLE_ORDER_STENCILBITS_SPV, sizeof(SAMPLE_ORDER_STENCILBITS_SPV), false); + } catch (...) { + destroyPass(pass); + throw; + } + return depthPasses.emplace(key, pass).first->second; + } + + std::shared_ptr copyFor(VkFormat format, std::uint32_t samples, VkExtent2D extent, bool stencil) { + const CopyKey key{format, samples, extent.width, extent.height, stencil}; + if (const auto found = copies.find(key); found != copies.end()) { + found->second.second = ++uses; + return found->second.first; + } + constexpr std::size_t bound = 4; + while (copies.size() >= bound) { + const auto oldest = std::ranges::min_element(copies, [](const auto& a, const auto& b) { return a.second.second < b.second.second; }); + copies.erase(oldest); + } + auto copy = std::make_shared(context, format, samples, extent, stencil); + copies.emplace(key, std::pair{copy, ++uses}); + return copy; + } + + VkDescriptorSet allocate(VkDescriptorSetLayout layout, const std::vector& sizes) { + auto& ring = layout == storageSetLayout ? storageRing : sampledRing; + const auto* recorder = Recorder::Active(); + if (ring.pools.empty() || ring.used == setsPerPool) { + const auto completed = recorder != nullptr ? recorder->CompletedThrough() : 0; + auto next = ring.pools.size(); + for (std::size_t step = 1; step <= ring.pools.size(); ++step) { + const auto index = (ring.current + step) % ring.pools.size(); + if (ring.serials[index] <= completed) { + next = index; + break; + } + } + if (next < ring.pools.size()) { + Check(context.Function("vkResetDescriptorPool")(context.device, ring.pools[next], 0), "vkResetDescriptorPool sample order"); + } else { + VkDescriptorPoolCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + info.maxSets = setsPerPool; + info.poolSizeCount = static_cast(sizes.size()); + info.pPoolSizes = sizes.data(); + VkDescriptorPool pool = VK_NULL_HANDLE; + Check(context.Function("vkCreateDescriptorPool")(context.device, &info, nullptr, &pool), "vkCreateDescriptorPool sample order"); + ring.pools.push_back(pool); + ring.serials.push_back(0); + } + ring.current = next; + ring.used = 0; + } + VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.descriptorPool = ring.pools[ring.current]; + allocation.descriptorSetCount = 1; + allocation.pSetLayouts = &layout; + VkDescriptorSet set = VK_NULL_HANDLE; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &set), "vkAllocateDescriptorSets sample order"); + ++ring.used; + ring.serials[ring.current] = recorder != nullptr && recorder->Device() == context.device ? recorder->OpenSerial() : std::numeric_limits::max(); + return set; + } + + void destroyPass(const DepthPass& pass) noexcept { + if (pass.stencilBits) context.Function("vkDestroyPipeline")(context.device, pass.stencilBits, nullptr); + if (pass.depth) context.Function("vkDestroyPipeline")(context.device, pass.depth, nullptr); + if (pass.renderPass) context.Function("vkDestroyRenderPass")(context.device, pass.renderPass, nullptr); + } + + void release() noexcept { + copies.clear(); + for (const auto& [key, pass] : depthPasses) destroyPass(pass); + depthPasses.clear(); + for (const auto pipeline : storagePipelines) { + if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + } + for (auto* ring : {&storageRing, &sampledRing}) { + for (const auto pool : ring->pools) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); + ring->pools.clear(); + } + for (const auto layout : {storagePipelineLayout, sampledPipelineLayout}) { + if (layout) context.Function("vkDestroyPipelineLayout")(context.device, layout, nullptr); + } + for (const auto layout : {storageSetLayout, sampledSetLayout}) { + if (layout) context.Function("vkDestroyDescriptorSetLayout")(context.device, layout, nullptr); + } + } + + VkDescriptorSetLayout storageSetLayout = VK_NULL_HANDLE; + VkDescriptorSetLayout sampledSetLayout = VK_NULL_HANDLE; + VkPipelineLayout storagePipelineLayout = VK_NULL_HANDLE; + VkPipelineLayout sampledPipelineLayout = VK_NULL_HANDLE; + std::array storagePipelines{}; + std::map depthPasses; + std::map, std::uint64_t>> copies; + std::uint64_t uses = 0; + Ring storageRing; + Ring sampledRing; +}; + +std::mutex& ordersMutex() { + static std::mutex mutex; + return mutex; +} + +std::vector>& orders() { + static std::vector> list; + return list; +} + +SampleOrders& ordersFor(const Context& context) { + std::lock_guard lock(ordersMutex()); + for (const auto& entry : orders()) { + if (entry->context.device == context.device) return *entry; + } + orders().push_back(std::make_unique(context)); + return *orders().back(); +} + +} + +SampleOrderMethod DepthSampleOrderMethod(const Context& context, const DepthTarget& depth) { + if (!stencilOf(context, depth)) return SampleOrderMethod::Depth; + return context.shaderStencilExport ? SampleOrderMethod::DepthStencilExport : SampleOrderMethod::DepthStencilBits; +} + +bool HostsSampleLocationGrid(VkExtent2D largestGrid) { + return largestGrid.width != 0u && largestGrid.height != 0u && largestGrid.width % 2u == 0u && largestGrid.height % 2u == 0u; +} + +bool MayReorderSamples(const Context& context, std::uint32_t samples) { + return samples > 1u && (context.sampleLocationSampleCounts & samples) != 0 && (context.sampleLocationGridSampleCounts & samples) == 0; +} + +VkFormat SampleOrderStorageFormat(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples) { + if (!context.storageImageMultisample || context.imageFormatProperties == nullptr || samples <= 1u) return VK_FORMAT_UNDEFINED; + const auto alias = TextureDetiler::ImageElementFormat(elementBytes); + if (alias == VK_FORMAT_UNDEFINED) return VK_FORMAT_UNDEFINED; + VkImageFormatProperties supported{}; + if (context.imageFormatProperties(context.physical, alias, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_STORAGE_BIT, 0, &supported) != VK_SUCCESS || (supported.sampleCounts & samples) == 0) return VK_FORMAT_UNDEFINED; + constexpr VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT; + constexpr VkImageCreateFlags flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT; + if (context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, flags, &supported) != VK_SUCCESS || (supported.sampleCounts & samples) == 0) return VK_FORMAT_UNDEFINED; + return alias; +} + +std::string SampleOrderRefusal(const Context& context, const State& state) { + if (!context.sampleRateShading) return "the device lacks sampleRateShading"; + if (!context.storageImageMultisample) return "the device lacks shaderStorageImageMultisample"; + for (const auto& color : state.colors) { + if (color.samples > 1u && SampleOrderStorageFormat(context, color.format, color.elementBytes, color.samples) == VK_FORMAT_UNDEFINED) return "color format " + std::to_string(color.format) + " has no storage view at " + std::to_string(color.samples) + " samples"; + } + if (state.depth && state.depth->samples > 1u) { + const auto host = HostDepthFormat(context, state.depth->format); + if (!exactDepth(host)) return "depth format " + std::to_string(host) + " is not written back exactly through FragDepth"; + if (!depthSampleable(context, host, state.depth->samples)) return "depth format " + std::to_string(host) + " cannot be sampled at " + std::to_string(state.depth->samples) + " samples"; + } + return {}; +} + +HostSampleLocations HostSampleLocationsFor(const Context& context, const State& state) { + if (!state.sampleLocationGrid || (context.sampleLocationGridSampleCounts & state.samples) != 0) return {SampleLocationGrid(state), {}}; + const auto order = SameSetSampleOrder(state); + const auto refusal = order ? SampleOrderRefusal(context, state) : std::string("its pixels hold different locations"); + static std::atomic reported{0}; + const auto bit = refusal.empty() ? state.samples : state.samples << 8u; + if ((reported.fetch_or(bit, std::memory_order_relaxed) & bit) == 0) { + if (refusal.empty()) std::fprintf(stderr, "[gpu] no 2x2 sample location grid at sample count %u on this device: pixel (0,0)'s sample locations stand in for every pixel, and each pixel's samples are put in the guest's order at the bounds of its render passes\n", state.samples); + else std::fprintf(stderr, "[gpu] no 2x2 sample location grid at sample count %u on this device: sample locations varying between pixels take pixel (0,0)'s in every pixel (%s)\n", state.samples, refusal.c_str()); + } + return {{1, 1}, refusal.empty() ? *order : SampleOrder{}}; +} + +std::vector PlanSampleOrder(const Context& context, const State& state, const SampleOrder& order, VkAttachmentLoadOp load, VkAttachmentStoreOp store) { + std::vector steps; + if (!ReordersSamples(order)) return steps; + const bool toHost = load == VK_ATTACHMENT_LOAD_OP_LOAD; + const bool toGuest = store == VK_ATTACHMENT_STORE_OP_STORE; + if (!toHost && !toGuest) return steps; + for (std::uint32_t index = 0; index < state.colors.size(); ++index) { + if (state.colors[index].samples > 1u) steps.push_back({index, SampleOrderMethod::Storage, toHost, toGuest}); + } + if (state.depth && state.depth->samples > 1u) steps.push_back({static_cast(state.colors.size()), DepthSampleOrderMethod(context, *state.depth), toHost, toGuest}); + return steps; +} + +SampleOrderPass SampleOrderPassFor(const Context& context, const State& state, const SampleOrder& order) { + return {order, state.renderExtent, SampleLocations(state), ReordersSamples(order) && ValidateSampleLocations(context, state)}; +} + +void RecordSampleOrder(const Context& context, VkCommandBuffer commands, const SampleOrderPass& pass, std::span targets, bool toHost, std::vector>& kept) { + if (targets.empty() || !ReordersSamples(pass.order)) return; + auto& reorder = ordersFor(context); + const auto map = mapFor(pass.order, toHost); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, reorderStages, anyWrite, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); + for (const auto& target : targets) { + Require(target.image != VK_NULL_HANDLE && target.view != VK_NULL_HANDLE, "a reordered attachment has no image"); + const auto region = regionOf(pass, target.extent); + if (region.width == 0 || region.height == 0) continue; + if (target.method == SampleOrderMethod::Storage) reorder.Storage(commands, target, map, region); + else reorder.Depth(commands, target, map, region, pass, kept); + if (target.keep != nullptr) kept.push_back(target.keep); + } + RecordMemoryBarrier(context, commands, reorderStages, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, anyAccess); + if (Recorder::Active() != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); +} + +void RecordDepthSampleLocations(const Context& context, VkCommandBuffer commands, const DepthSampleLocations& depth) { + Require(depth.image != VK_NULL_HANDLE && depth.samples * depth.grid.width * depth.grid.height <= depth.locations.size(), "a depth target drawn at custom sample locations has no image or too many locations"); + const VkSampleLocationsInfoEXT info{VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast(depth.samples), depth.grid, depth.samples * depth.grid.width * depth.grid.height, depth.locations.data()}; + transitionLocated(context, commands, depth.image, depth.format, info, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, anyWrite, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT); + transitionLocated(context, commands, depth.image, depth.format, info, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, anyAccess); +} + +std::array, 9> SampleOrderShaderModules() { + return {SAMPLE_ORDER_R8_SPV, SAMPLE_ORDER_R16_SPV, SAMPLE_ORDER_R32_SPV, SAMPLE_ORDER_RG32_SPV, SAMPLE_ORDER_RGBA32_SPV, SAMPLE_ORDER_VERTEX_SPV, SAMPLE_ORDER_DEPTH_SPV, SAMPLE_ORDER_STENCILEXPORT_SPV, SAMPLE_ORDER_STENCILBITS_SPV}; +} + +void ClearSampleOrders(VkDevice device) { + std::lock_guard lock(ordersMutex()); + std::erase_if(orders(), [&](const auto& entry) { return entry->context.device == device; }); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp index 214fcaf05f..d7b1fb0056 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp @@ -6,18 +6,24 @@ namespace AgcDriver::Graphics { - Sampler::Sampler(const Context& context, const GuestSamplerResource& descriptor) : context(context) { + void Sampler::Validate(const Context& context, const GuestSamplerResource& descriptor) { Require(!descriptor.anisotropyEnable || context.samplerAnisotropy, "guest sampler descriptor requests anisotropic filtering which the device does not support"); Require(descriptor.maxAnisotropy <= context.limits.maxSamplerAnisotropy, "guest sampler descriptor requests an anisotropy ratio beyond the device limit"); Require(descriptor.lodBias >= -context.limits.maxSamplerLodBias && descriptor.lodBias <= context.limits.maxSamplerLodBias, "guest sampler descriptor requests a LOD bias beyond the device limit"); Require(!(descriptor.unnormalizedCoordinates && descriptor.compareEnable), "guest sampler descriptor with unnormalized coordinates enables depth comparison, which is not implemented"); + if (descriptor.reductionMode != VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT) { + Require(context.samplerFilterMinmax, "guest sampler descriptor uses a min or max reduction which the device does not support"); + Require(!descriptor.compareEnable, "guest sampler descriptor combines a min or max reduction with depth comparison, which is not implemented"); + } + } + + Sampler::Sampler(const Context& context, const GuestSamplerResource& descriptor) : context(context) { + Validate(context, descriptor); forcesDegamma = descriptor.forceDegamma; VkSamplerCreateInfo info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; VkSamplerReductionModeCreateInfoEXT reduction{VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO_EXT, nullptr, descriptor.reductionMode}; if (descriptor.reductionMode != VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT) { - Require(context.samplerFilterMinmax, "guest sampler descriptor uses a min or max reduction which the device does not support"); - Require(!descriptor.compareEnable, "guest sampler descriptor combines a min or max reduction with depth comparison, which is not implemented"); info.pNext = &reduction; requiresFilterMinmax = descriptor.magFilter == VK_FILTER_LINEAR || descriptor.minFilter == VK_FILTER_LINEAR; } @@ -35,8 +41,7 @@ namespace AgcDriver::Graphics { info.minLod = descriptor.minLod; info.maxLod = descriptor.maxLod; info.borderColor = descriptor.borderColor; - const bool clampsToBorder = descriptor.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER || descriptor.addressModeV == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER || descriptor.addressModeW == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; - opaqueBlackBorder = clampsToBorder && (descriptor.borderColor == VK_BORDER_COLOR_INT_OPAQUE_BLACK || descriptor.borderColor == VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK); + opaqueBlackBorder = ReadsOpaqueBlackBorder(descriptor); info.unnormalizedCoordinates = descriptor.unnormalizedCoordinates ? VK_TRUE : VK_FALSE; if (descriptor.nonSeamlessCube) { if (context.nonSeamlessCubeMap) info.flags |= VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT; @@ -65,6 +70,11 @@ namespace AgcDriver::Graphics { return opaqueBlackBorder; } + bool Sampler::ReadsOpaqueBlackBorder(const GuestSamplerResource& descriptor) { + const bool clampsToBorder = descriptor.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER || descriptor.addressModeV == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER || descriptor.addressModeW == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; + return clampsToBorder && (descriptor.borderColor == VK_BORDER_COLOR_INT_OPAQUE_BLACK || descriptor.borderColor == VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK); + } + SamplerCache::SamplerCache(std::size_t capacity) : capacity(std::max(capacity, 1)) {} std::shared_ptr SamplerCache::Get(const Context& context, std::span words, bool compareEnable, bool unnormalizedProven, bool forceDegammaPaired, bool integerBorder) { @@ -114,7 +124,7 @@ namespace AgcDriver::Graphics { } bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers) { - if (IsSrgbTextureFormat(guestFormat)) return false; + if (IsSrgbTextureFormat(guestFormat) || IgnoresForcedDegamma(guestFormat)) return false; for (std::uint32_t element = 0; element < 32u && element < samplers.size(); ++element) { if (((samplerMask >> element) & 1u) != 0u && samplers[element]->ForcesDegamma()) return true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp index 04601fb8eb..2915b76a5a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp @@ -1,10 +1,121 @@ #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) { Require(shader.program != nullptr, "missing compiled shader"); std::uint32_t tables = 0; @@ -20,7 +131,10 @@ 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)); } if (usesBda) { Require(context.bufferDeviceAddress, "buffer device address is not enabled"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index df4bfe5fe8..3859fc17d0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -24,6 +24,7 @@ constexpr std::uint32_t spiPsInputCntl0 = 0x191; constexpr std::uint32_t spiPsInputEna = 0x1B3; constexpr std::uint32_t spiPsInputAddr = 0x1B4; constexpr std::uint32_t spiPsInControl = 0x1B6; +constexpr std::uint32_t spiBarycCntl = 0x1B8; constexpr std::uint32_t dbShaderControl = 0x203; constexpr std::uint32_t spiShaderColFormat = 0x1C5; constexpr std::uint32_t defaultPixelInputs = 0x2u; @@ -117,7 +118,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con constexpr std::uint32_t knownMask = PixelInputBit(PixelInput::PerspectiveSample) | PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::PerspectiveCentroid) | PixelInputBit(PixelInput::LinearSample) | PixelInputBit(PixelInput::LinearCenter) | PixelInputBit(PixelInput::LinearCentroid) | PixelInputBit(PixelInput::PositionX) | PixelInputBit(PixelInput::PositionY) | PixelInputBit(PixelInput::PositionZ) | PixelInputBit(PixelInput::PositionW) | - PixelInputBit(PixelInput::FrontFace) | PixelInputBit(PixelInput::Ancillary) | PixelInputBit(PixelInput::LineStipple) | PixelInputBit(PixelInput::PositionFixedPoint); + PixelInputBit(PixelInput::FrontFace) | PixelInputBit(PixelInput::Ancillary) | PixelInputBit(PixelInput::SampleCoverage) | PixelInputBit(PixelInput::LineStipple) | PixelInputBit(PixelInput::PositionFixedPoint); if ((activeInputs & ~knownMask) != 0) { char message[128]; std::snprintf(message, sizeof(message), "AGC graphics: unsupported SPI_PS_INPUT_ENA/ADDR bit combination (ena 0x%x addr 0x%x)", ena, addr); @@ -142,10 +143,19 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con const bool pixelKillEnable = ((shaderControl >> 6u) & 0x1u) != 0; const bool depthExportEnable = (shaderControl & 0x1u) != 0; const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0; - const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0; - const bool depthBeforeShader = ((shaderControl >> 12u) & 0x1u) != 0; - const auto zOrder = (shaderControl >> 4u) & 0x3u; + const bool stencilOperationValue = !nullProgram && StencilOperationValueReplaces(context); + const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0 || stencilOperationValue; + if (stencilOperationValue && PixelEarlyZ(shaderControl)) throw std::runtime_error("AGC graphics: early tests with a pixel shader that exports the stencil operation value are unsupported"); const auto loaded = [&](PixelInput input) { return (activeInputs & PixelInputBit(input)) != 0; }; + std::uint32_t positionControl = 0; + if (loaded(PixelInput::PositionX) || loaded(PixelInput::PositionY)) { + NoteRegisterRead(RegisterBank::Context, spiBarycCntl); + if (const auto it = context.find(spiBarycCntl); it != context.end()) positionControl = it->second; + } + const auto floatLocation = (positionControl >> 16u) & 0x3u; + if (floatLocation == 3u) { + throw std::runtime_error("AGC graphics: SPI_BARYC_CNTL.POS_FLOAT_LOCATION uses the reserved value 3"); + } return ShaderRecompiler::ShaderPixelStageInfo{ .interpolatorCount = inputNum, .interpolatorSettings = interpolatorSettings, @@ -158,6 +168,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .posZ = loaded(PixelInput::PositionZ), .posW = loaded(PixelInput::PositionW), .frontFace = loaded(PixelInput::FrontFace), + .frontFaceAllBits = loaded(PixelInput::FrontFace) && ((read(context, spiBarycCntl, RegisterBank::Context) >> 24u) & 0x1u) != 0, .ancillary = loaded(PixelInput::Ancillary), .sampleShading = loaded(PixelInput::PerspectiveSample) && loaded(PixelInput::LinearSample), .noPerspective = loaded(PixelInput::LinearCenter), @@ -165,16 +176,28 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .pixelKillEnable = pixelKillEnable, .depthExportEnable = depthExportEnable, .sampleMaskExportEnable = sampleMaskExportEnable, - .earlyZ = depthBeforeShader || ((zOrder & 1u) != 0u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable), + .earlyZ = PixelEarlyZ(shaderControl), .executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0, .conservativeZExport = static_cast(conservativeZExport), .orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0, .targetOutputMode = targetOutputMode, .targetExportMapping = nullProgram ? std::array{} : exportMappings, - .stencilRefExportEnable = stencilRefExportEnable + .stencilRefExportEnable = stencilRefExportEnable, + .stencilOperationValueExport = stencilOperationValue, + .positionLocation = (positionControl & 0x100000u) != 0u ? ShaderRecompiler::PixelPositionLocation::UpperLeft : static_cast(floatLocation) }; } +bool PixelEarlyZ(std::uint32_t shaderControl) { + const bool pixelKillEnable = ((shaderControl >> 6u) & 0x1u) != 0; + const bool depthExportEnable = (shaderControl & 0x1u) != 0; + const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0; + const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0; + const bool depthBeforeShader = ((shaderControl >> 12u) & 0x1u) != 0; + const auto zOrder = (shaderControl >> 4u) & 0x3u; + return depthBeforeShader || ((zOrder & 1u) != 0u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable); +} + ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads, bool staticAbi, std::uint32_t hiddenUserWords) { if (header.empty()) return {}; if (header.size() < sizeof(Shader)) throw std::runtime_error("AGC graphics: shader header is smaller than the fixed AGC header"); @@ -213,6 +236,14 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span(vertexAttribReg); info.fetchBufferReg = static_cast(vertexBufferReg); + const auto record = [&](std::uint64_t address, std::span bytes) { + if (reads == nullptr) return; + auto& read = reads->emplace_back(); + read.address = address; + std::copy(bytes.begin(), bytes.end(), read.data.begin()); + read.size = static_cast(bytes.size()); + }; + if (reads != nullptr) reads->reserve(reads->size() + 2u * shader.num_input_semantics); for (std::uint32_t i = 0; i < shader.num_input_semantics; ++i) { const auto& semantic = semantics[i]; if (semantic.static_vb_index == 1 || semantic.static_attribute == 1) throw std::runtime_error("AGC graphics: statically bound vertex attributes are not implemented"); @@ -226,7 +257,7 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span attribWordBytes{}; const auto attribWordAddress = attribTableAddr + static_cast(semantic.semantic) * 4u; AgcDriver::GuestMemory::Read(attribWordAddress, attribWordBytes, 4); - if (reads != nullptr) reads->push_back({attribWordAddress, {attribWordBytes.begin(), attribWordBytes.end()}}); + record(attribWordAddress, attribWordBytes); std::uint32_t attribWord; std::memcpy(&attribWord, attribWordBytes.data(), 4); const auto index = attribWord & 0x1fu; @@ -237,7 +268,7 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span sharpBytes{}; const auto sharpAddress = bufferTableAddr + static_cast(index) * 16u; AgcDriver::GuestMemory::Read(sharpAddress, sharpBytes, 4); - if (reads != nullptr) reads->push_back({sharpAddress, {sharpBytes.begin(), sharpBytes.end()}}); + record(sharpAddress, sharpBytes); std::array sharp{}; std::memcpy(sharp.data(), sharpBytes.data(), 16); if (info.resourcesNum >= ShaderRecompiler::ShaderVertexStageInfo::MaxResources) throw std::runtime_error("AGC graphics: vertex resource count exceeds the supported domain"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 4926b3dd01..d8a6109735 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4,6 +4,7 @@ #include "ThreadOwned.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" @@ -14,10 +15,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" @@ -27,6 +30,8 @@ #include "prx/libc/include/General.hpp" #include "Optimization/include/Optimization/ShaderStageInputInfo.hpp" #include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" +#include "IntermediateRepresentation/IrMetadata/ShaderStage.hpp" +#include "ImageTableAbi.hpp" #include #include #include @@ -59,7 +64,7 @@ VkComponentSwizzle ComponentSwizzleFor(std::uint8_t dstSel) { } VkComponentMapping ViewComponents(const GuestTextureResource& resource) { - if (IsConvertedTextureFormat(resource.format)) return {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A}; + if (IsConvertedTextureFormat(resource.format) && !IsRepackedTextureFormat(resource.format)) return {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A}; const auto channel = [&](std::uint8_t dstSel) { return TextureComponentChannel(resource.format, ComponentSwizzleFor(dstSel)); }; return {channel(resource.dstSelX), channel(resource.dstSelY), channel(resource.dstSelZ), channel(resource.dstSelW)}; } @@ -113,6 +118,10 @@ struct CachedTexture { std::uint64_t accounted = 0; std::uint64_t present = 0; std::optional>> committed; + // The untrackedKey the snapshot was last read or compared equal at, for a surface outside the + // write watch; zero when none was taken. + std::array untracked{}; + std::uint64_t surfaceBytes = 0; }; // Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the @@ -140,6 +149,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; }; @@ -149,6 +159,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; @@ -167,6 +197,16 @@ void noteUncached(const std::shared_ptr& image) { uncached.any.store(true, std::memory_order_release); } +std::atomic& TexturesReleased() { + static std::atomic released{false}; + return released; +} + +void noteReleased(const std::shared_ptr& texture) { + texture->MarkReleased(); + TexturesReleased().store(true, std::memory_order_release); +} + bool SamplerCacheDisabled() { static const bool disabled = std::getenv("APS5_NO_SAMPLER_CACHE") != nullptr; return disabled; @@ -198,6 +238,7 @@ void eraseTexture(TextureCache& cache, std::list::iterator it) { } } } + if (it->texture.use_count() > 1) noteReleased(it->texture); cache.index.erase(it->key); cache.entries.erase(it); ++cache.erasures; @@ -236,16 +277,17 @@ bool admitTexture(TextureCache& cache, const TextureKey& key, std::uint64_t inco cache.firstUses.clear(); cache.firstUsesPresent = present; } - const bool firstUse = incoming != 0 && present != 0 && !cache.firstUses.contains(key); + const bool scan = incoming != 0 && present != 0; + const bool firstUse = scan && !cache.firstUses.contains(key); const auto ceiling = budget + budget / 4u; while (!cache.entries.empty() && cache.bytes + incoming > budget) { const auto back = std::prev(cache.entries.end()); - if (firstUse && back->present + 1u >= present) { + const bool over = cache.bytes + incoming > ceiling; + if (scan && !over && (firstUse ? back->present + 1u >= present : back->present == present)) { cache.firstUses.insert(key); ++cache.counts.uncached; return false; } - const bool over = cache.bytes + incoming > ceiling; const bool keep = back->accounted == 0 || (!over && back->texture.use_count() > 1); if (keep && cache.passed < cache.entries.size()) { cache.entries.splice(cache.entries.begin(), cache.entries, back); @@ -268,7 +310,7 @@ void reportCacheCounts(TextureCache& cache, std::uint64_t budget) { if (now - cache.countsReported < std::chrono::seconds(10)) return; cache.countsReported = now; const auto counts = std::exchange(cache.counts, {}); - AgcDriver::ProfilePrint_nid_no_patch("[texture-cache] (10 s): %llu textures cached, %llu used uncached (first use this frame, the least recently used entry used this frame or the last), %llu evicted, %llu held ones passed over, %llu found changed; %llu MiB in %zu entries, budget %llu MiB, frame %llu\n", static_cast(counts.inserted), static_cast(counts.uncached), static_cast(counts.evicted), static_cast(counts.passed), static_cast(counts.stale), static_cast(cache.bytes >> 20u), cache.entries.size(), static_cast(budget >> 20u), static_cast(Recorder::Presents())); + AgcDriver::ProfilePrint_nid_no_patch("[texture-cache] (10 s): %llu textures cached, %llu used uncached (first use this frame with the least recently used entry used this frame or the last, or a later use with it used this frame), %llu evicted, %llu held ones passed over, %llu found changed; %llu MiB in %zu entries, budget %llu MiB, frame %llu\n", static_cast(counts.inserted), static_cast(counts.uncached), static_cast(counts.evicted), static_cast(counts.passed), static_cast(counts.stale), static_cast(cache.bytes >> 20u), cache.entries.size(), static_cast(budget >> 20u), static_cast(Recorder::Presents())); } struct ImageMemoEntry { @@ -336,6 +378,15 @@ struct TextureCounters { // Storage images a Revalidate refreshed directly instead of through the lookups (T1, see // ShaderResources::refreshOwnObjects). std::atomic ownRefreshes{0}; + std::atomic tablesReused{0}; + std::atomic tablesReplayed{0}; + std::atomic tablesResolved{0}; + std::atomic tableWritesKept{0}; + std::atomic tableWritesMade{0}; + // Snapshot hits over memory outside the write watch: kept by an unchanged UntrackedHint key, or + // by a byte compare. + std::atomic untrackedKept{0}; + std::atomic untrackedCompared{0}; std::atomic lastReport{0}; }; @@ -352,7 +403,7 @@ void reportTextureCounters() { auto last = counters.lastReport.load(); if (nowMs - last < 10000 || !counters.lastReport.compare_exchange_strong(last, nowMs)) return; const auto count = [](const std::atomic& value) { return static_cast(value.load(std::memory_order_relaxed)); }; - AgcDriver::ProfilePrint_nid_no_patch("[textures] sampled created: %llu from storage images, %llu snapshots (%llu snapshot reads over recorded writes inside cachedTexture, %llu storage-path fallbacks); stage-A records %llu: %llu fast hits, %llu full lookups; storage images %llu hits, %llu created, %llu own-object refreshes\n", count(counters.fromStorage), count(counters.snapshots), count(counters.pendingReads), count(counters.storageFallbacks), count(counters.records), count(counters.fastHits), count(counters.fastMisses), count(counters.storageHits), count(counters.storageCreated), count(counters.ownRefreshes)); + AgcDriver::ProfilePrint_nid_no_patch("[textures] sampled created: %llu from storage images, %llu snapshots (%llu snapshot reads over recorded writes inside cachedTexture, %llu storage-path fallbacks); stage-A records %llu: %llu fast hits, %llu full lookups; storage images %llu hits, %llu created, %llu own-object refreshes; image tables %llu reused, %llu resolved; untracked snapshots %llu kept by the write-watch hint, %llu compared\n", count(counters.fromStorage), count(counters.snapshots), count(counters.pendingReads), count(counters.storageFallbacks), count(counters.records), count(counters.fastHits), count(counters.fastMisses), count(counters.storageHits), count(counters.storageCreated), count(counters.ownRefreshes), count(counters.tablesReused), count(counters.tablesResolved), count(counters.untrackedKept), count(counters.untrackedCompared)); } // What the sampled-texture lookups on this thread proved their returned objects current against, @@ -371,24 +422,55 @@ struct LookupRecord { }; thread_local std::vector* lookupLogSlot = nullptr; +constexpr std::size_t LookupLogBound = 1024; +constexpr std::size_t NoBuildLookups = std::numeric_limits::max(); +thread_local std::size_t buildLookupsStart = NoBuildLookups; +thread_local bool buildLookupsPastBound = false; +std::atomic lookupBuildsPastBound{0}; std::vector& lookupLog() { return ShaderRecompiler::ThreadOwned(lookupLogSlot); } void logLookup(const LookupRecord& record) { - // The bound drops the oldest half, never everything: one build's lookups (a few dozen at most) - // are the newest records, and a capture must find them even when the bound trips mid-build. - constexpr std::size_t bound = 1024; auto& log = lookupLog(); - if (log.size() >= bound) log.erase(log.begin(), log.begin() + bound / 2); + if (log.size() >= LookupLogBound) { + const auto dropped = std::min(LookupLogBound / 2, buildLookupsStart); + if (dropped < LookupLogBound / 2) buildLookupsPastBound = true; + log.erase(log.begin(), log.begin() + static_cast(dropped)); + if (buildLookupsStart != NoBuildLookups) buildLookupsStart -= dropped; + } log.push_back(record); } -std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes = 0); +class BuildLookups { +public: + BuildLookups() : outermost(buildLookupsStart == NoBuildLookups) { buildLookupsStart = std::min(buildLookupsStart, lookupLog().size()); } + ~BuildLookups() { + if (!outermost) return; + buildLookupsStart = NoBuildLookups; + if (buildLookupsPastBound) lookupBuildsPastBound.fetch_add(1, std::memory_order_relaxed); + buildLookupsPastBound = false; + auto& log = lookupLog(); + if (log.size() > LookupLogBound) log.erase(log.begin(), log.end() - static_cast(LookupLogBound)); + if (log.capacity() > LookupLogBound + LookupLogBound / 2) log.shrink_to_fit(); + } + BuildLookups(const BuildLookups&) = delete; + BuildLookups& operator=(const BuildLookups&) = delete; + +private: + bool outermost; +}; + +void refreshForView(StorageTexture& source, const GuestTextureResource& resource) { + if (resource.dimension == TextureDimension::k2D && source.ImageLayers() > 1) source.RefreshLayers(resource.baseArray, 1); + else source.Refresh(); +} + +std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes = 0, bool written = false, std::optional> layers = std::nullopt); std::uint64_t heldBytes(const StorageTexture& image) { - return std::max(image.AllocationBytes(), image.GuestBytes()); + return image.BudgetBytes(); } bool MetadataMoved(const StorageTexture& image, const GuestTextureResource& resource) { @@ -478,7 +560,10 @@ std::optional largestDeviceLocalHeap(const VkPhysicalDeviceMemory return heap; } +std::atomic budgetOverride{0}; + std::uint64_t forcedCacheBudget() { + if (const auto overridden = budgetOverride.load(std::memory_order_relaxed); overridden != 0) return overridden; static const std::uint64_t forced = [] { const char* text = std::getenv("APS5_TEXTURE_CACHE_MIB"); return text != nullptr ? std::strtoull(text, nullptr, 10) << 20u : 0u; @@ -488,7 +573,25 @@ std::uint64_t forcedCacheBudget() { std::uint64_t storageBudget(const Context& context) { const auto forced = forcedCacheBudget(); - return forced != 0 ? forced : TextureCacheBudget(context.memory); + if (forced != 0) return forced; + if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return TextureCacheBudget(context.memory); + static struct { + std::mutex mutex; + VkPhysicalDevice device{VK_NULL_HANDLE}; + std::chrono::steady_clock::time_point read{}; + std::uint64_t budget{0}; + } cached; + const auto now = std::chrono::steady_clock::now(); + std::lock_guard lock(cached.mutex); + if (cached.device == context.physical && now - cached.read < std::chrono::seconds(1)) return cached.budget; + 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); + cached.device = context.physical; + cached.read = now; + cached.budget = StorageTextureBudget(context.memory, &reported, SampledTextureMemory(), storageCacheBytes()); + return cached.budget; } std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { @@ -504,11 +607,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(TextureCacheBudget(context.memory) >> 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; @@ -517,8 +626,71 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { return budget; } +// A snapshot over memory outside the write watch is compared with guest memory again only when this +// key changed: a write-watch mark in the range, a driver store, a mapping change or a recorded GPU +// write (as StorageTexture::compareUntracked). Taken before the snapshot is read or compared, so a +// CPU store racing with that leaves a mark for the next key. Zero when the range has no hint. +std::array untrackedKey(std::uint64_t address, std::size_t bytes) { + if (GuestMemory::CollectEpoch() == 0) return {}; + const auto hint = GuestMemory::UntrackedHint(address, bytes); + if (hint == 0) return {}; + const auto* recorder = Recorder::Active(); + const std::array key{hint, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; + return (key[2] & 1u) == 0 ? key : std::array{}; +} + +std::span firstLayerWords(std::span words, std::optional shape, std::array& narrowed, bool report) { + if (shape != ShaderRecompiler::DescriptorImageShape::Image2D || words.size() != narrowed.size() || IsNullTextureResource(words)) return words; + try { + if (!FirstLayerDescriptor(words, narrowed)) return words; + } catch (const std::runtime_error&) { + return words; + } + if (report) { + static std::atomic narrowings{0}; + const auto count = narrowings.fetch_add(1) + 1; + if (count <= 5 || (count & (count - 1)) == 0) { + const auto resource = DecodeTextureResource(words); + std::fprintf(stderr, "[gpu] 2D read of the 2D array 0x%llx (%ux%u format %u, slices %u..%u) built from slice %u alone; %llu such reads\n", static_cast(resource.baseAddress), resource.width, resource.height, resource.format, resource.baseArray, resource.depthOrLastArray, resource.baseArray, static_cast(count)); + } + } + return narrowed; +} + +void reportGuestAllocation(const GuestTextureResource& resource, std::uint64_t guestBytes) { + const auto range = GuestAllocations::GuestAllocationsLookup_nid_postfix(resource.baseAddress, false); + const auto geometry = DescribeSurface(resource); + if (range == nullptr) { + std::fprintf(stderr, "[gpu] view 0x%llx of %llu MiB lies in no registered guest allocation\n", static_cast(resource.baseAddress), static_cast(guestBytes >> 20u)); + return; + } + const auto end = range->address + range->bytes; + const auto fitting = geometry.thick || geometry.layerBytes == 0 ? 0ull : std::min(geometry.layers, (end - resource.baseAddress) / geometry.layerBytes); + std::fprintf(stderr, "[gpu] view 0x%llx of %llu MiB lies in the guest allocation 0x%llx..0x%llx (%llu MiB, range 0x%llx..0x%llx, %s%s%s): %llu of its %u slices fit in the range\n", static_cast(resource.baseAddress), static_cast(guestBytes >> 20u), static_cast(range->allocationAddress), static_cast(range->allocationAddress + range->allocationBytes), static_cast(range->allocationBytes >> 20u), static_cast(range->address), static_cast(end), range->readable ? "r" : "-", range->writable ? "w" : "-", range->gpu ? "g" : "-", static_cast(fitting), geometry.layers); +} + // `guestBytes` is the surface size when the caller described the surface already (0: described here). -std::shared_ptr cachedTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false) { +std::shared_ptr cachedTexture(const Context& context, std::span words, const GuestTextureResource& requested, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D) { + const auto resource = ArrayLayerWindow(requested, context.limits.maxImageArrayLayers); + if (resource.baseAddress != requested.baseAddress) guestBytes = 0; + if (resource.dimension == TextureDimension::k2DMsaa) { + Require(!depthCompare, "native multisampled depth comparison sampling is not implemented"); + if (auto depth = DepthSurfaceTexture(context, words, resource, components, multisampleViewType)) { + logLookup({depth.get(), resource, 0, DccKeys::Uncompressed, 0, nullptr, true}); + return depth; + } + auto target = MultisampleTargetTexture(context, resource, components, multisampleViewType); + logLookup({target.get(), resource, 0, DccKeys::Uncompressed, 0, nullptr, true}); + return target; + } + if (resource.fmaskCmaskAddress != 0u) { + Require(!depthCompare, "FMASK depth comparison sampling is not implemented"); + Require(resource.baseArray == 0u && resource.depthOrLastArray == 0u, "native FMASK array sampling is not implemented"); + CompletePendingCpuWrites(context, resource.fmaskCmaskAddress, CmaskBytes(resource.width, resource.height)); + auto native = MultisampleTargetFmaskTexture_nid_no_patch(context, resource, components); + Require(native != nullptr, "compressed FMASK sampling without a matching resident native target is not implemented"); + return native; + } CaptureTrace::Log("sampled-lookup address=%llx width=%u height=%u dcc=%llx", static_cast(resource.baseAddress), resource.width, resource.height, static_cast(resource.dccAddress)); RequireNoMultisampleTarget(resource.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a texture read"); if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes(); @@ -527,10 +699,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) { @@ -538,6 +711,21 @@ std::shared_ptr cachedTexture(const Context& context, std::span windows{0}; + const auto count = windows.fetch_add(1) + 1; + if (count <= 5 || (count & (count - 1)) == 0) std::fprintf(stderr, "[gpu] sampled array view 0x%llx (%ux%u format %u, dimension %d, slices %u..%u) served from its viewed slices at 0x%llx; %llu such views\n", static_cast(resource.baseAddress), resource.width, resource.height, resource.format, static_cast(resource.dimension), resource.baseArray, resource.depthOrLastArray, static_cast(window.baseAddress), static_cast(count)); + return cachedTexture(context, words, window, components, 0, depthCompare); + } + if (guestBytes == 0) guestBytes = DescribeSurface(resource).guestBytes; + if (guestBytes >= (1ull << 30u)) { + static std::atomic large{0}; + const auto count = large.fetch_add(1) + 1; + if (count <= 5 || (count & (count - 1)) == 0) { + std::fprintf(stderr, "[gpu] sampled view 0x%llx (%ux%u format %u, dimension %d, tile %d, slices %u..%u) covers %llu MiB; %llu such views\n", static_cast(resource.baseAddress), resource.width, resource.height, resource.format, static_cast(resource.dimension), static_cast(resource.tileMode), resource.baseArray, resource.depthOrLastArray, static_cast(guestBytes >> 20u), static_cast(count)); + reportGuestAllocation(resource, guestBytes); + } + } static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; const bool profile = LookupOutcomes::Profiled(); const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; @@ -547,17 +735,19 @@ std::shared_ptr cachedTexture(const Context& context, std::span(guestBytes); + static_cast(FlushDepthPlanes(address, bytes)); // A storage image whose results for this surface (or for a mip chain containing it) are still on // the GPU supplies the texture by a view of it; anything else needs those results in guest // memory first. const bool depthCopy = depthCompare && guestBytes >= DepthCopyMinBytes() && Texture::DepthCopyFormat(ResolveTextureFormat(resource.format)) != VK_FORMAT_UNDEFINED && resource.dimension != TextureDimension::k3D; + const bool srgbExpand = !depthCompare && SrgbExpandsToRgba8(context, resource.format); auto source = StorageTexture::FindPending(address, guestBytes); - const bool blockCopy = source != nullptr && !depthCompare && Texture::CanBlockCopyFrom(*source, resource); - if (source != nullptr && !blockCopy && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset(); + const bool blockCopy = source != nullptr && !depthCompare && !srgbExpand && Texture::CanBlockCopyFrom(*source, resource); + const bool repackCopy = source != nullptr && !depthCompare && Texture::CanRepackCopyFrom(*source, resource); + if (source != nullptr && !blockCopy && !repackCopy && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : srgbExpand ? !Texture::CanSrgbExpandFrom(context, *source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset(); if (source != nullptr && MetadataMoved(*source, resource)) source.reset(); std::optional keys; bool clearThroughKeys = false; @@ -589,6 +779,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span cachedTexture(const Context& context, std::span cachedTexture(const Context& context, std::spanGeneration())) { + if (source != nullptr && !GuestMemory::UnchangedSince(address, bytes, source->GenerationFor(address, bytes))) { // The CPU wrote the memory while results were pending: Refresh merges them the usual way. - source->Refresh(); + refreshForView(*source, resource); } // Results stored to guest memory just now are stamped newer than `generation`; a snapshot read // after them is current at the generation of a second (memoized) collect. The store marks the // surface's DCC keys uncompressed, so the keys are read after it. const auto flushStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - if (source == nullptr && !clearThroughKeys && StorageTexture::FlushPending(address, bytes, nullptr, "sampled texture")) { + const auto observedKeys = keys.has_value() ? *keys : scanKeys(); + const bool stagedClear = source != nullptr && resource.dccAddress != 0 && (IsDccClear(observedKeys) || observedKeys == DccKeys::ClearSingle); + bool stagedFlushed = false; + if ((source == nullptr || stagedClear) && !clearThroughKeys && StorageTexture::FlushPending(address, bytes, nullptr, "sampled texture")) { if (profile) LookupOutcomes::Add(LookupOutcomes::PendingFlush, flushStart); generation = GuestMemory::CollectWrites(address, bytes); - keys.reset(); + if (stagedClear) { + stagedFlushed = true; + keys = DccKeys::Uncompressed; + } else { + keys.reset(); + } + if (stagedFlushed) source.reset(); } if (!keys.has_value()) keys = scanKeys(); if (disabled) { @@ -634,6 +835,16 @@ std::shared_ptr cachedTexture(const Context& context, std::spanRefreshDepthCopy(); return texture; } + if (source != nullptr && IsRepackedTextureFormat(resource.format)) { + auto texture = std::make_shared(context, source, resource, components, Texture::RepackCopyTag{}); + texture->RefreshDepthCopy(); + return texture; + } + if (source != nullptr && srgbExpand) { + auto texture = std::make_shared(context, source, resource, components, Texture::SrgbExpandTag{}); + texture->RefreshDepthCopy(); + return texture; + } if (source != nullptr) return std::make_shared(context, source, resource, components); std::vector snapshot(bytes); ReadTextureSurface(resource, *keys, snapshot); @@ -648,17 +859,19 @@ std::shared_ptr cachedTexture(const Context& context, std::span{}; auto& cache = Textures(); const auto key = MakeTextureKey(context.device, words, components, depthCompare); std::lock_guard lock(cache.mutex); + const CacheHold held(sampledCacheHeld); dropUncachedViews(cache); if (auto it = findTexture(cache, key); it != cache.entries.end()) { if (it->source != nullptr) { // The view follows the storage image, whatever the GPU wrote to it since; guest memory // written meanwhile is taken in by refreshing the image. A fast clear the image cannot // see (keys, and no pending results to prefer) ends the view: a snapshot holds the clear. - if ((source == nullptr || source == it->source) && (source != nullptr || *keys == DccKeys::Uncompressed) && StorageImageServesKeys(*it->source, resource.dccAddress)) { - if (!GuestMemory::UnchangedSince(address, bytes, it->source->Generation())) it->source->Refresh(); + if (!stagedFlushed && (source == nullptr || source == it->source) && (source != nullptr || *keys == DccKeys::Uncompressed) && StorageImageServesKeys(*it->source, resource.dccAddress)) { + if (!GuestMemory::UnchangedSince(address, bytes, it->source->GenerationFor(address, bytes))) refreshForView(*it->source, resource); if (it->texture->IsDepthCopy()) it->texture->RefreshDepthCopy(); it->keys = *keys; touchTexture(cache, it); @@ -667,22 +880,30 @@ std::shared_ptr cachedTexture(const Context& context, std::spantexture; } - } else if (source == nullptr && it->bytes.size() == guestBytes && it->keys == *keys) { + } else if (source == nullptr && it->surfaceBytes == guestBytes && it->keys == *keys) { // Unwritten pages need no comparison; partially resident textures compare only committed // pages. The compare goes through the flush hook: it waits for recorded work over the // surface (counted, and named for the [hooksync] line). const auto equalsCommitted = [&] { + if (it->bytes.empty()) return false; if (Recorder::SnapshotWriteOverlaps(address, bytes)) counters.pendingReads.fetch_add(1, std::memory_order_relaxed); const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureCompare); return GuestMemory::EqualsCommitted(address, it->bytes); }; const auto unchanged = [&] { if (it->committed != committed) return false; - return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, it->bytes.size()) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation); + return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, static_cast(it->surfaceBytes)) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, static_cast(it->surfaceBytes), it->generation); }; - if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || unchanged() || equalsCommitted()) { + const auto kept = [&] { + if (untracked[0] == 0) return false; + const bool same = it->untracked == untracked && !Recorder::SnapshotWriteOverlaps(address, bytes); + (same ? counters.untrackedKept : counters.untrackedCompared).fetch_add(1, std::memory_order_relaxed); + return same; + }; + if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || unchanged() || kept() || equalsCommitted()) { it->generation = snapshotGeneration; it->committed = std::move(committed); + it->untracked = untracked; touchTexture(cache, it); logLookup({it->texture.get(), resource, guestBytes, *keys, generation, nullptr}); reportTextureCounters(); @@ -695,6 +916,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration}; entry.committed = std::move(committed); + entry.surfaceBytes = bytes; if (source != nullptr) { entry.source = source; entry.sourceVersion = source->Version(); @@ -704,6 +926,12 @@ std::shared_ptr cachedTexture(const Context& context, std::span(context, source, resource, components, Texture::BlockCopyTag{}); entry.texture->RefreshDepthCopy(); + } else if (IsRepackedTextureFormat(resource.format)) { + entry.texture = std::make_shared(context, source, resource, components, Texture::RepackCopyTag{}); + entry.texture->RefreshDepthCopy(); + } else if (srgbExpand) { + entry.texture = std::make_shared(context, source, resource, components, Texture::SrgbExpandTag{}); + entry.texture->RefreshDepthCopy(); } else { entry.texture = std::make_shared(context, source, resource, components); } @@ -711,8 +939,10 @@ std::shared_ptr cachedTexture(const Context& context, std::span cachedTexture(const Context& context, std::span(context, *context.detiler, resource, components, entry.bytes, depthCompare); entry.accounted = entry.texture->AllocationBytes(); + constexpr std::size_t SnapshotCompareBytes = 256u << 10u; + if (entry.bytes.size() > SnapshotCompareBytes && (entry.generation != 0 || entry.untracked[0] != 0)) std::vector().swap(entry.bytes); counters.snapshots.fetch_add(1, std::memory_order_relaxed); } const auto budget = sampledBudget(context, cache); auto texture = entry.texture; if (!admitTexture(cache, key, entry.accounted, budget)) { + noteReleased(texture); logLookup({texture.get(), resource, guestBytes, *keys, generation, source.get()}); reportCacheCounts(cache, budget); reportTextureCounters(); @@ -862,10 +1095,53 @@ GuestTextureResource StorageSurface(const Context& context, const GuestTextureRe return surface; } +struct WrittenStorage { + std::vector> bytes; + std::vector extents; +}; + +static WrittenStorage WrittenStorageBytes(const GuestTextureResource& viewed, std::uint32_t mip) { + WrittenStorage written; + const auto geometry = DescribeSurface(viewed); + if (geometry.thick || mip >= geometry.mips.size()) return written; + const auto& level = geometry.mips[mip]; + const auto bytesPerElement = BytesPerElement(viewed.format); + const auto covered = CoveredMipBytes(viewed.tileMode, bytesPerElement, level); + const bool blocks = viewed.tileMode != TextureTileMode::kLinear && !level.tail; + const auto first = geometry.layers > 1u && geometry.imageDepth <= 1u ? std::min(viewed.baseArray, geometry.layers) : 0u; + for (auto layer = first; layer < geometry.layers; ++layer) { + if (!geometry.HasLayer(mip, layer)) continue; + const auto begin = viewed.baseAddress + geometry.GuestLayerOffset(layer) + level.tiledOffset; + for (const auto& [rangeBegin, rangeEnd] : covered) { + if (!written.bytes.empty() && written.bytes.back().second == begin + rangeBegin) written.bytes.back().second = begin + rangeEnd; + else written.bytes.emplace_back(begin + rangeBegin, begin + rangeEnd); + } + if (blocks) written.extents.push_back({begin, level.width, level.height, bytesPerElement, ThinBlockLayout(viewed.tileMode, bytesPerElement), level.blocksPerRow}); + } + return written; +} + // `guestBytes` is the surface size when the caller described the surface already (0: described here). -std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes) { +std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& requested, std::uint32_t mip, std::uint64_t guestBytes, bool written, std::optional> layers) { + const auto viewed = ArrayLayerWindow(requested, context.limits.maxImageArrayLayers); + if (viewed.baseAddress != requested.baseAddress) guestBytes = 0; + Require(viewed.dimension != TextureDimension::k2DMsaa, "storage image access to a multisampled texture is not implemented"); RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a storage image access"); if (auto depth = DepthSurfaceStorage(context, words, viewed, mip)) return depth; + const auto viewedBytes = guestBytes != 0 ? guestBytes : DescribeSurface(viewed).guestBytes; + if (viewedBytes >= (1ull << 30u)) { + static std::atomic large{0}; + const auto count = large.fetch_add(1) + 1; + if (count <= 5 || (count & (count - 1)) == 0) { + std::fprintf(stderr, "[gpu] storage view 0x%llx (%ux%u format %u, dimension %d, tile %d, slices %u..%u, mip %u) covers %llu MiB; %llu such views\n", static_cast(viewed.baseAddress), viewed.width, viewed.height, viewed.format, static_cast(viewed.dimension), static_cast(viewed.tileMode), viewed.baseArray, viewed.depthOrLastArray, mip, static_cast(viewedBytes >> 20u), static_cast(count)); + reportGuestAllocation(viewed, viewedBytes); + } + } + if (DepthPlanesStale() || written) static_cast(FlushDepthPlanes(viewed.baseAddress, static_cast(viewedBytes), written ? DepthPlaneScope::Whole : DepthPlaneScope::Overlapped)); + if (written && DepthSurfaceOverlaps(context.device, viewed.baseAddress, viewedBytes)) { + const auto storage = WrittenStorageBytes(viewed, mip); + RetireCoveredDepthSurfaces(context.device, storage.bytes, storage.extents); + } static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; if (disabled) return StorageTexture::Create(context, *context.detiler, viewed, mip); static_cast(words); @@ -880,6 +1156,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); @@ -891,7 +1168,8 @@ std::shared_ptr cachedStorageTexture(const Context& context, std it = cache.entries.end(); } if (it != cache.entries.end()) { - it->texture->Refresh(); + if (layers) it->texture->RefreshLayers(layers->first, layers->second); + else it->texture->Refresh(); cache.entries.splice(cache.entries.begin(), cache.entries, it); texture = it->texture; counters.storageHits.fetch_add(1, std::memory_order_relaxed); @@ -919,9 +1197,18 @@ std::shared_ptr cachedStorageTexture(const Context& context, std 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)); + if (image.use_count() > 1) recorder->KeepBytes(image.get(), static_cast(heldBytes(*image))); + for (auto& resource : resources) { + resource->KeepStorageImageBytes(*recorder); + recorder->Keep(std::move(resource)); + } } } + if (!evicted.empty()) { + auto& sampled = Textures(); + std::lock_guard lock(sampled.mutex); + dropUncachedViews(sampled); + } return texture; } @@ -932,6 +1219,16 @@ 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 sampledBytes, std::uint64_t storageBytes) { + const auto heap = largestDeviceLocalHeap(memory); + if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); + const auto budget = reported->heapBudget[*heap]; + const auto usage = reported->heapUsage[*heap]; + const auto reserved = usage - std::min(usage, sampledBytes + storageBytes) + 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) { const auto heap = largestDeviceLocalHeap(memory); if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); @@ -946,6 +1243,18 @@ bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::c return change > reported / 10u && sinceReport >= std::chrono::seconds(10); } +std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory) { + return largestDeviceLocalHeap(memory); +} + +std::uint64_t StorageTextureCacheBudget(const Context& context) { + return storageBudget(context); +} + +void OverrideCacheBudgetForTest(std::uint64_t bytes) { + budgetOverride.store(bytes, std::memory_order_relaxed); +} + TextureCacheUse TextureCacheUsage() { TextureCacheUse use; { @@ -965,9 +1274,9 @@ std::uint64_t SampledTextureCacheBudget(const Context& context) { return sampledBudget(context, cache); } -std::shared_ptr CachedSampledTexture(const Context& context, std::span words) { +std::shared_ptr CachedSampledTexture(const Context& context, std::span words, VkImageViewType multisampleViewType) { const auto resource = DecodeTextureResource(words); - return cachedTexture(context, words, resource, ViewComponents(resource)); + return cachedTexture(context, words, resource, ViewComponents(resource), 0, false, multisampleViewType); } bool SampledTexturesShareEntry(std::span first, std::span second) { @@ -1020,11 +1329,98 @@ void ClearCachedTextures(VkDevice device) { 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)); + for (auto& resource : resources) { + resource->KeepStorageImageBytes(*recorder); + recorder->Keep(std::move(resource)); + } } } } +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); @@ -1055,6 +1451,10 @@ std::shared_ptr CachedStorageSurface(const Context& context, con return cachedStorageTexture(context, {}, resource, 0); } +std::shared_ptr CachedStorageLayers(const Context& context, const GuestTextureResource& resource, std::uint32_t firstLayer, std::uint32_t layerCount) { + return cachedStorageTexture(context, {}, resource, 0, 0, false, std::pair{firstLayer, layerCount}); +} + bool StorageImageServesKeys(const StorageTexture& image, std::uint64_t dccAddress) { GuestTextureResource resource{}; resource.dccAddress = dccAddress; @@ -1073,6 +1473,10 @@ const char* roleName(ShaderRecompiler::DescriptorRole role) { case ShaderRecompiler::DescriptorRole::FaultBuffer: return "FaultBuffer"; case ShaderRecompiler::DescriptorRole::FlattenedSrt: return "FlattenedSrt"; case ShaderRecompiler::DescriptorRole::ShaderData: return "ShaderData"; + 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"); } @@ -1109,6 +1513,16 @@ bool SameAsPreviousStorageElement(const ShaderRecompiler::DescriptorBinding& bin ShaderResources::ShaderResources(const Context& context, const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment, const ColorTarget& target, std::uint64_t indexAddress, std::size_t indexBytes) : ShaderResources(context, std::array{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, static_cast(vertex.pushConstants.size())}}}, target, 1, indexAddress, indexBytes) {} +void ShaderResources::KeepStorageImageBytes(Recorder& recorder) const { + const auto keep = [&](const std::shared_ptr& image) { + if (image != nullptr && !image->Cached()) recorder.KeepBytes(image.get(), static_cast(image->BudgetBytes())); + }; + for (const auto& image : storageTextures) keep(image); + for (const auto& texture : textures) { + if (texture != nullptr) keep(texture->SharedStorageSource()); + } +} + ShaderResources::ShaderResources(const Context& context, std::span shaders, const ColorTarget& target, std::uint32_t colorAttachments, std::uint64_t indexAddress, std::size_t indexBytes, std::span snapshots) : context(context), guestMemory(context) { drawBuild = true; prepareAddressBindings(shaders, snapshots); @@ -1290,8 +1704,147 @@ double ShaderResources::phase(BuildPhase which) { namespace { +std::uint32_t IrStage(ShaderRecompiler::ShaderStage stage) { + using Ir = ShaderRecompiler::IrShaderStage; + switch (stage) { + case ShaderRecompiler::ShaderStage::Compute: return static_cast(Ir::Compute); + case ShaderRecompiler::ShaderStage::Vertex: return static_cast(Ir::Vertex); + case ShaderRecompiler::ShaderStage::Fragment: return static_cast(Ir::Pixel); + case ShaderRecompiler::ShaderStage::Mesh: return static_cast(Ir::Mesh); + case ShaderRecompiler::ShaderStage::Local: return static_cast(Ir::Local); + case ShaderRecompiler::ShaderStage::TessellationControl: return static_cast(Ir::TessellationControl); + case ShaderRecompiler::ShaderStage::TessellationEvaluation: return static_cast(Ir::TessellationEvaluation); + case ShaderRecompiler::ShaderStage::Geometry: break; + } + return static_cast(Ir::Unknown); +} + +bool TableReuseEnabled() { + static const bool disabled = std::getenv("APS5_NO_TABLE_REUSE") != nullptr; + return !disabled; +} + +bool TableReplay() { + static const bool disabled = std::getenv("APS5_NO_TABLE_REPLAY") != nullptr; + return !disabled; +} + +struct TableState { + std::uint64_t epoch = 0; + std::uint64_t unwatched = 0; + std::uint64_t allocations = 0; + std::uint64_t depthSurfaces = 0; + bool operator==(const TableState&) const = default; +}; + +TableState CurrentTableState() { + if (!TableReuseEnabled()) return {}; + return {GuestMemory::ThreadCollectEpoch(), GuestMemory::UnwatchSerial(), GuestAllocations::GuestAllocationsGeneration_nid_postfix(), DepthSurfacesSerial()}; +} + +struct ReusedTableElement { + TextureKey key{}; + std::weak_ptr texture; + GuestTextureResource resource{}; + std::uint64_t guestBytes = 0; + bool firstLayer = false; + bool rejected = false; + std::string message; +}; + +struct ReusedTable { + VkDevice device = VK_NULL_HANDLE; + TableState state; + std::uint64_t pendingSerial = 0; + std::vector words; + std::vector shapes; + std::vector depthCompare; + std::vector elements; + std::vector queries; + std::vector> ranges; + std::vector imageSamplers; + std::vector samplers; + std::vector bound; + bool replayable = false; +}; + +constexpr std::size_t ReusedTableCount = 4; + +std::vector& ReusedTables() { + thread_local std::vector* slot = nullptr; + return ShaderRecompiler::ThreadOwned(slot); +} + +bool SameTable(const ReusedTable& table, VkDevice device, const ShaderRecompiler::DescriptorBinding& binding) { + return table.device == device && table.words == binding.guestDescriptor && table.shapes == binding.imageShapes && table.depthCompare == binding.imageDepthCompare; +} + +ReusedTable* FindReusedTable(VkDevice device, const ShaderRecompiler::DescriptorBinding& binding, const TableState& state) { + if (state.epoch == 0) return nullptr; + auto& tables = ReusedTables(); + std::erase_if(tables, [&](const ReusedTable& table) { return table.state != state; }); + for (auto& table : tables) { + if (SameTable(table, device, binding)) return &table; + } + return nullptr; +} + +bool TableCurrent(ReusedTable& table) { + const auto serial = StorageTexture::PendingSerial(); + if (serial != table.pendingSerial) { + if (StorageTexture::AnyPendingOverlaps(table.ranges) || StorageTexture::PendingSerial() != serial) return false; + table.pendingSerial = serial; + } + return GuestMemory::UnchangedSinceAll(table.queries); +} + +bool CachedTableTextures(const ReusedTable& table, std::vector>& cached) { + auto& cache = Textures(); + std::lock_guard lock(cache.mutex); + std::vector::iterator> entries; + entries.reserve(table.queries.size()); + for (const auto& element : table.elements) { + if (element.rejected) continue; + const auto it = findTexture(cache, element.key); + if (it == cache.entries.end() || it->texture != element.texture.lock()) return false; + entries.push_back(it); + } + cached.reserve(entries.size()); + for (const auto it : entries) { + cached.push_back(it->texture); + touchTexture(cache, it); + } + return true; +} + +void RememberTable(ReusedTable&& table, const ShaderRecompiler::DescriptorBinding& binding) { + auto& tables = ReusedTables(); + std::erase_if(tables, [&](const ReusedTable& entry) { return SameTable(entry, table.device, binding); }); + table.words = binding.guestDescriptor; + table.shapes = binding.imageShapes; + table.depthCompare = binding.imageDepthCompare; + tables.insert(tables.begin(), std::move(table)); + if (tables.size() > ReusedTableCount) tables.resize(ReusedTableCount); +} + +std::string HexText(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "%llx", static_cast(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; + const auto& indexed = context.descriptorIndexingLimits; + const bool sampledUpdateAfterBind = updateAfterBind && context.sampledImageUpdateAfterBind; + const auto maxStageResources = updateAfterBind ? indexed.maxPerStageUpdateAfterBindResources : context.limits.maxPerStageResources; + const auto maxStageSampledImages = sampledUpdateAfterBind ? indexed.maxPerStageDescriptorUpdateAfterBindSampledImages : context.limits.maxPerStageDescriptorSampledImages; + const auto maxStageSamplers = sampledUpdateAfterBind ? indexed.maxPerStageDescriptorUpdateAfterBindSamplers : context.limits.maxPerStageDescriptorSamplers; + const auto maxSetSampledImages = sampledUpdateAfterBind ? indexed.maxDescriptorSetUpdateAfterBindSampledImages : context.limits.maxDescriptorSetSampledImages; + const auto maxSetSamplers = sampledUpdateAfterBind ? indexed.maxDescriptorSetUpdateAfterBindSamplers : context.limits.maxDescriptorSetSamplers; std::map heapTotals; std::map, std::uint64_t> stageTotals; std::map stageResources; @@ -1303,12 +1856,13 @@ void ValidateDescriptorLayout(const Context& context, std::span maxStageSamplers) throw std::runtime_error(DescriptorLimitError("typed heaps exceed per-stage sampler capacity", count, maxStageSamplers)); } } - 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] <= maxSetSampledImages && heapTotals[VK_DESCRIPTOR_TYPE_STORAGE_IMAGE] <= context.limits.maxDescriptorSetStorageImages, "typed heaps exceed descriptor set capacity"); + if (heapTotals[VK_DESCRIPTOR_TYPE_SAMPLER] > maxSetSamplers) throw std::runtime_error(DescriptorLimitError("typed heaps exceed descriptor set sampler capacity", heapTotals[VK_DESCRIPTOR_TYPE_SAMPLER], maxSetSamplers)); } void ValidateRuntimeResources(const CompiledShader& shader, std::span words) { @@ -1357,6 +1911,11 @@ void ShaderResources::buildPrepare(std::span shaders, cons for (const auto& shader : shaders) { Require(shader.program != nullptr, "missing compiled shader"); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.program->runtimeAbiVersion); + if (shader.program->poisonedSrtReads != 0) { + const auto& programBindings = shader.program->bindings; + const auto flat = std::find_if(programBindings.begin(), programBindings.end(), [](const ShaderRecompiler::DescriptorBinding& binding) { return binding.role == ShaderRecompiler::DescriptorRole::FlattenedSrt; }); + if (flat != programBindings.end()) srtGuardFaults.push_back({IrStage(shader.stage), flat->guestDescriptor}); + } const VkShaderStageFlags flags = VulkanStage(shader.stage); std::uint64_t stageStorageBuffers = 0; std::uint64_t stageResources = shader.stage == ShaderRecompiler::ShaderStage::Fragment ? colorAttachments : 0; @@ -1373,7 +1932,38 @@ 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 || 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(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; + } Require(occupied.insert(binding.binding).second, "duplicate shader binding"); + if (binding.role == ShaderRecompiler::DescriptorRole::SamplerTable) { + addSamplerTable(binding, flags, tableStage(shader)); + continue; + } + if (binding.role == ShaderRecompiler::DescriptorRole::ImageTableMap) { + Require(binding.kind == ShaderRecompiler::DescriptorKind::StorageBuffer && binding.count == 1 && binding.guestDescriptor.size() >= ShaderRecompiler::ImageTableAbi::HeaderWords && !binding.readOnly && binding.guestDescriptor[ShaderRecompiler::ImageTableAbi::HeaderVersion] == ShaderRecompiler::ImageTableAbi::Version, "image table map binding is malformed"); + const auto size = binding.guestDescriptor.size() * sizeof(std::uint32_t); + Require(size <= context.limits.maxStorageBufferRange, "image table map exceeds the storage buffer range limit"); + stageStorageBuffers += 1; + stageResources += 1; + storageBuffers += 1; + requireStageLimits(); + auto& table = tableStage(shader); + Require(table.mapAllocation == std::numeric_limits::max(), "a stage binds two image table maps"); + table.map = binding.guestDescriptor; + allocations.push_back({0, size, false, std::make_unique(context, size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT), ShaderRecompiler::DescriptorRole::ImageTableMap}); + table.mapAllocation = allocations.size() - 1u; + bindings.push_back({{binding.binding, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, flags, nullptr}, {allocations.size() - 1u}}); + continue; + } const bool addressRole = binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable || binding.role == ShaderRecompiler::DescriptorRole::FaultBuffer; const bool bufferRole = addressRole || binding.role == ShaderRecompiler::DescriptorRole::GuestBuffers || binding.role == ShaderRecompiler::DescriptorRole::ShaderData || binding.role == ShaderRecompiler::DescriptorRole::FlattenedSrt || binding.role == ShaderRecompiler::DescriptorRole::Gds; const bool imageRole = binding.role == ShaderRecompiler::DescriptorRole::GuestImages || binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers; @@ -1400,7 +1990,10 @@ void ShaderResources::buildPrepare(std::span shaders, cons const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; const bool atomic = element < binding.bufferAtomic.size() && binding.bufferAtomic[element]; const bool read = element < binding.bufferRead.size() && binding.bufferRead[element]; - const auto index = addGuestBuffer(std::span(binding.guestDescriptor).subspan(static_cast(element) * 4, 4), target, indexAddress, indexBytes, written, atomic, read); + const std::uint32_t extent = element < binding.bufferExtent.size() ? binding.bufferExtent[element] : 0u; + const auto index = addGuestBuffer(std::span(binding.guestDescriptor).subspan(static_cast(element) * 4, 4), target, indexAddress, indexBytes, written, atomic, read, extent); + const bool classified = element < binding.bufferWritten.size() && element < binding.bufferRead.size() && element < binding.bufferAtomic.size(); + allocations[index].writeOnly = classified && allocations[index].guest && allocations[index].written && binding.bufferWritten[element] && !read && !atomic; const auto& push = shader.program->pushConstants; if (!push.empty()) { const auto position = shader.program->memoryOffsetDword * 4u + element; @@ -1437,6 +2030,41 @@ void ShaderResources::buildPrepare(std::span shaders, cons deferredImages[index].firstSampler = firstSampler; deferredImages[index].samplerCount = samplers.size() - firstSampler; } + for (auto& table : tableStages) { + if (table.program == shader.program && table.images != nullptr) table.samplerCount = samplers.size() - table.firstSampler; + } + } + 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; + Require(map.size() >= ShaderRecompiler::ImageTableAbi::HeaderWords, "image table map is truncated"); + const auto elements = table.images != nullptr ? table.images->count : 0u; + Require(map[ShaderRecompiler::ImageTableAbi::HeaderElementCount] == elements, "image table map disagrees with its image binding"); + const auto faults = map[ShaderRecompiler::ImageTableAbi::HeaderElementFaults]; + const auto samplerFaults = map[ShaderRecompiler::ImageTableAbi::HeaderSamplerFaults]; + Require(static_cast(faults) + elements <= map.size() && static_cast(samplerFaults) + map[ShaderRecompiler::ImageTableAbi::HeaderSamplerCount] / 4u <= map.size(), "image table map is truncated"); + if (table.images == nullptr) continue; + table.set = splitSets && table.stage == ShaderRecompiler::ShaderStage::Fragment ? 1u : 0u; + table.elementBase = tableElements[table.set]; + table.map[ShaderRecompiler::ImageTableAbi::HeaderElementBase] = table.elementBase; + tableElements[table.set] += elements; + 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; + Require(context.imageTableCapacity != 0, "image tables need runtimeDescriptorArray, descriptorBindingPartiallyBound and descriptorBindingVariableDescriptorCount, which the device lacks"); + const VkDescriptorBindingFlags imageTableFlags = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT | (context.imageTableUpdateAfterBind ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); + bindings.push_back({{ShaderRecompiler::RuntimeAbi::ImageTableBinding, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, context.imageTableCapacity, tableFlags[set], nullptr}, {}, {}, imageTableFlags, static_cast(set)}); + tableBindings[set] = bindings.size() - 1u; } if (storageBuffers > context.limits.maxDescriptorSetStorageBuffers) { Require(storageBuffers <= context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers, "pipeline descriptors exceed device limits"); @@ -1451,19 +2079,48 @@ void ShaderResources::buildPrepare(std::span shaders, cons timing.uploadMs = phase(BuildPhase::Upload); splitSets = SplitsSets(context, shaders); std::array, 2> descriptions; + bool tablesUpdateAfterBind = false; for (const auto& binding : bindings) { const auto set = setOf(binding); descriptions[set].push_back(binding.layout); - (set == 0 ? layoutBindingFlags : fragmentBindingFlags).push_back(updateAfterBind && binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); + (set == 0 ? layoutBindingFlags : fragmentBindingFlags).push_back(binding.flags | (updateAfterBind && binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0)); + tablesUpdateAfterBind |= (binding.flags & VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT) != 0; auto& key = set == 0 ? layoutKey : fragmentLayoutKey; key.insert(key.end(), {binding.layout.binding, static_cast(binding.layout.descriptorType), binding.layout.descriptorCount, binding.layout.stageFlags}); } + updateAfterBind = updateAfterBind || tablesUpdateAfterBind; if (updateAfterBind) { layoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); if (splitSets) fragmentLayoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); } static const bool noLayoutCache = std::getenv("APS5_NO_LAYOUT_CACHE") != nullptr; static const bool noPoolCache = std::getenv("APS5_NO_POOL_CACHE") != nullptr; + const auto tableDescriptorCount = [&](std::size_t set) { return std::max(1u, std::min(tableElements[set], context.imageTableCapacity)); }; + const auto tableVariableCount = [&](std::size_t set) { return tableBindings[set] != std::numeric_limits::max() ? std::optional(tableDescriptorCount(set)) : std::nullopt; }; + const auto splitPoolSizes = [&](std::size_t set) { + std::map counts; + for (std::size_t index = 0; index < bindings.size(); ++index) { + if (setOf(bindings[index]) != set) continue; + counts[bindings[index].layout.descriptorType] += index == tableBindings[set] ? tableDescriptorCount(set) : bindings[index].layout.descriptorCount; + } + std::vector sizes; + for (const auto& [type, count] : counts) sizes.push_back({type, count}); + return sizes; + }; + const auto allocateVariableSet = [&](VkDescriptorPool from, VkDescriptorSetLayout layout, std::optional variableCount) { + const std::uint32_t variable = variableCount.value_or(0u); + VkDescriptorSetVariableDescriptorCountAllocateInfoEXT variableInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_ALLOCATE_INFO_EXT}; + variableInfo.descriptorSetCount = 1; + variableInfo.pDescriptorCounts = &variable; + VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.pNext = variableCount.has_value() ? &variableInfo : nullptr; + allocation.descriptorPool = from; + allocation.descriptorSetCount = 1; + allocation.pSetLayouts = &layout; + VkDescriptorSet set = VK_NULL_HANDLE; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &set), "vkAllocateDescriptorSets"); + return set; + }; const auto createLayout = [&](const std::vector& key, const std::vector& description, const std::vector& flags, bool& owns) { if (context.descriptorCache != nullptr && !noLayoutCache) return context.descriptorCache->Layout(key, description, updateAfterBind, flags); ValidateDescriptorLayout(context, description, updateAfterBind); @@ -1474,34 +2131,28 @@ void ShaderResources::buildPrepare(std::span shaders, cons if (splitSets) { fragment.layout = createLayout(fragmentLayoutKey, descriptions[1], fragmentBindingFlags, fragment.ownsLayout); { - const auto sizes = PoolSizes(descriptions[1]); + const auto sizes = splitPoolSizes(1); + const auto variableCount = tableVariableCount(1); if (context.descriptorCache != nullptr && !noPoolCache) { - fragment.cacheAllocation = context.descriptorCache->Allocate(fragment.layout, sizes, updateAfterBind); + fragment.cacheAllocation = context.descriptorCache->Allocate(fragment.layout, sizes, updateAfterBind, variableCount); fragment.set = fragment.cacheAllocation.set; } if (fragment.set == VK_NULL_HANDLE) { fragment.pool = CreateSetPool(context, sizes, updateAfterBind); - VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; - allocation.descriptorPool = fragment.pool; - allocation.descriptorSetCount = 1; - allocation.pSetLayouts = &fragment.layout; - Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &fragment.set), "vkAllocateDescriptorSets"); + fragment.set = allocateVariableSet(fragment.pool, fragment.layout, variableCount); } } } if (splitSets) { - const auto sizes = PoolSizes(descriptions[0]); + const auto sizes = splitPoolSizes(0); + const auto variableCount = tableVariableCount(0); if (context.descriptorCache != nullptr && !noPoolCache) { - cacheAllocation = context.descriptorCache->Allocate(_layout, sizes, updateAfterBind); + cacheAllocation = context.descriptorCache->Allocate(_layout, sizes, updateAfterBind, variableCount); _set = cacheAllocation.set; } if (_set == VK_NULL_HANDLE) { pool = CreateSetPool(context, sizes, updateAfterBind); - VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; - allocation.descriptorPool = pool; - allocation.descriptorSetCount = 1; - allocation.pSetLayouts = &_layout; - Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &_set), "vkAllocateDescriptorSets"); + _set = allocateVariableSet(pool, _layout, variableCount); } } if (!splitSets && !bindings.empty()) { @@ -1509,23 +2160,43 @@ void ShaderResources::buildPrepare(std::span shaders, cons // when stage B looks it up. std::vector sizes; if (storageBuffers != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, static_cast(storageBuffers)}); - if (plannedSampledImages != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, plannedSampledImages}); - if (plannedStorageImages != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, plannedStorageImages}); - if (!samplers.empty()) sizes.push_back({VK_DESCRIPTOR_TYPE_SAMPLER, static_cast(samplers.size())}); + 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}); + 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; + } + if (samplerDescriptors != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_SAMPLER, samplerDescriptors}); + const auto variableCount = tableVariableCount(0); if (context.descriptorCache != nullptr && !noPoolCache) { - const auto allocated = context.descriptorCache->Allocate(_layout, sizes, updateAfterBind); + const auto allocated = context.descriptorCache->Allocate(_layout, sizes, updateAfterBind, variableCount); _set = allocated.set; cacheAllocation = allocated; } if (_set == VK_NULL_HANDLE) { pool = CreateSetPool(context, sizes, updateAfterBind); + const std::uint32_t variable = variableCount.value_or(0u); + VkDescriptorSetVariableDescriptorCountAllocateInfoEXT variableInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_ALLOCATE_INFO_EXT}; + variableInfo.descriptorSetCount = 1; + variableInfo.pDescriptorCounts = &variable; VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.pNext = variableCount.has_value() ? &variableInfo : nullptr; allocation.descriptorPool = pool; allocation.descriptorSetCount = 1; allocation.pSetLayouts = &_layout; Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &_set), "vkAllocateDescriptorSets"); } } + writeOnlyRanges.clear(); + for (const auto& item : allocations) { + if (!item.writeOnly || item.size == 0) continue; + const auto begin = item.address; + const auto end = item.address + item.size; + const bool shared = std::any_of(allocations.begin(), allocations.end(), [&](const Allocation& other) { return !other.writeOnly && other.size != 0 && other.address < end && begin < other.address + other.size; }); + if (!shared) writeOnlyRanges.emplace_back(begin, end); + } timing.descriptorsMs = phase(BuildPhase::Descriptors); if (BuildProfiled()) { timing.prepareMs = std::chrono::duration(std::chrono::steady_clock::now() - stageStart).count(); @@ -1548,7 +2219,11 @@ DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span, 2> descriptions; const bool split = SplitsSets(context, shaders); + std::array, 2> bindingFlags; + std::array tableFlags{}; + std::array storageTableFlags{}; bool updateAfterBind = false; + bool tablesUpdateAfterBind = false; std::uint64_t storageBuffers = 0; for (const auto& shader : shaders) { Require(shader.program != nullptr, "missing compiled shader"); @@ -1556,6 +2231,21 @@ DescribedSetLayouts DescribeSetLayouts(const Context& context, std::spanbindings) { + const auto set = binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet ? 1 : 0; + if (binding.role == ShaderRecompiler::DescriptorRole::ImageTable) { + tableFlags[split && shader.stage == ShaderRecompiler::ShaderStage::Fragment ? 1 : 0] |= flags; + continue; + } + if (binding.role == ShaderRecompiler::DescriptorRole::ImageStorageTable) { + storageTableFlags[split && shader.stage == ShaderRecompiler::ShaderStage::Fragment ? 1 : 0] |= flags; + continue; + } + if (binding.role == ShaderRecompiler::DescriptorRole::SamplerTable) { + descriptions[set].push_back({binding.binding, VK_DESCRIPTOR_TYPE_SAMPLER, context.samplerTableCapacity, flags, nullptr}); + bindingFlags[set].push_back(VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | (context.samplerTableUpdateAfterBind ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0)); + tablesUpdateAfterBind |= context.samplerTableUpdateAfterBind; + continue; + } const bool image = binding.role == ShaderRecompiler::DescriptorRole::GuestImages || binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers; const auto type = !image ? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER : binding.kind == ShaderRecompiler::DescriptorKind::StorageImage ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE : binding.kind == ShaderRecompiler::DescriptorKind::SampledImage ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_SAMPLER; if (type != VK_DESCRIPTOR_TYPE_SAMPLER) stageResources += binding.count; @@ -1564,23 +2254,42 @@ DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span context.limits.maxPerStageDescriptorStorageBuffers || stageResources > context.limits.maxPerStageResources) updateAfterBind = true; - descriptions[binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet ? 1 : 0].push_back({binding.binding, type, binding.count, flags, nullptr}); + descriptions[set].push_back({binding.binding, type, binding.count, flags, nullptr}); + bindingFlags[set].push_back(0); } } + for (std::size_t set = 0; set < storageTableFlags.size(); ++set) { + if (storageTableFlags[set] == 0) continue; + descriptions[set].push_back({ShaderRecompiler::RuntimeAbi::ImageStorageTableBinding, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, StorageTableCapacity(context), storageTableFlags[set], nullptr}); + bindingFlags[set].push_back(VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT); + } + for (std::size_t set = 0; set < tableFlags.size(); ++set) { + if (tableFlags[set] == 0) continue; + descriptions[set].push_back({ShaderRecompiler::RuntimeAbi::ImageTableBinding, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, context.imageTableCapacity, tableFlags[set], nullptr}); + bindingFlags[set].push_back(VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT | (context.imageTableUpdateAfterBind ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0)); + tablesUpdateAfterBind |= context.imageTableUpdateAfterBind; + } if (storageBuffers > context.limits.maxDescriptorSetStorageBuffers) updateAfterBind = true; + for (std::size_t set = 0; set < descriptions.size(); ++set) { + for (std::size_t index = 0; index < descriptions[set].size(); ++index) { + if (updateAfterBind && descriptions[set][index].descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) bindingFlags[set][index] |= VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT; + } + } + updateAfterBind = updateAfterBind || tablesUpdateAfterBind; for (std::size_t set = 0; set < descriptions.size(); ++set) { auto& key = set == 0 ? result.mainKey : result.fragmentKey; for (const auto& binding : descriptions[set]) key.insert(key.end(), {binding.binding, static_cast(binding.descriptorType), binding.descriptorCount, binding.stageFlags}); if (updateAfterBind && (set == 0 || split)) key.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); } result.layouts.split = split; - result.layouts.main = context.descriptorCache->Layout(result.mainKey, descriptions[0], updateAfterBind); - if (split) result.layouts.fragment = context.descriptorCache->Layout(result.fragmentKey, descriptions[1], updateAfterBind); + result.layouts.main = context.descriptorCache->Layout(result.mainKey, descriptions[0], updateAfterBind, bindingFlags[0]); + if (split) result.layouts.fragment = context.descriptorCache->Layout(result.fragmentKey, descriptions[1], updateAfterBind, bindingFlags[1]); return result; } void ShaderResources::buildComplete() { PerformanceTimer frameTiming("Resources.Complete"); + BuildLookups lookups; Require(!completed, "cannot update a published descriptor heap"); const auto stageStart = std::chrono::steady_clock::now(); phaseStart = stageStart; @@ -1600,11 +2309,14 @@ void ShaderResources::buildComplete() { imageRecords.shrink_to_fit(); nextImageRecord = 0; Require(textures.size() == plannedSampledImages && storageTextures.size() == plannedStorageImages, "image lookups disagree with the descriptor plan"); + resolveImageTables(); timing.bindingsMs += phase(BuildPhase::Images); guestMemory.UploadFinish(usesBda); timing.uploadMs += phase(BuildPhase::Upload); if (usesBda) bda = std::make_unique(context, guestMemory); else if (usesFaultBuffer) bda = std::make_unique(context); + if (bda) bda->SetSrtGuards(std::move(srtGuardFaults)); + finishImageTables(); phase(BuildPhase::Bda); if (_set != VK_NULL_HANDLE || fragment.set != VK_NULL_HANDLE) { // One update call for the whole set: the info arrays are sized up front so every write's @@ -1613,7 +2325,7 @@ void ShaderResources::buildComplete() { std::size_t imageCount = 0; for (const auto& binding : bindings) { bufferCount += binding.allocations.size(); - if (binding.layout.descriptorType != VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) imageCount += binding.layout.descriptorCount; + if (binding.layout.descriptorType != VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) imageCount += binding.flags != 0 ? binding.imageAllocations.size() : binding.layout.descriptorCount; } auto& buffers = descriptorBuffers; auto& images = descriptorImages; @@ -1621,12 +2333,58 @@ void ShaderResources::buildComplete() { images.reserve(imageCount); auto& writes = descriptorWrites; writes.reserve(bindings.size()); + std::vector> keptWrites; const auto absentImage = [this](std::uint32_t binding) { constexpr auto bindingCount = static_cast(ShaderRecompiler::RuntimeAbi::Binding::Count); const auto view = context.nullDescriptors ? VK_NULL_HANDLE : context.placeholderImages->View(binding % bindingCount); return VkDescriptorImageInfo{VK_NULL_HANDLE, view, VK_IMAGE_LAYOUT_GENERAL}; }; for (const auto& binding : bindings) { + if (binding.flags != 0) { + const auto set = setOf(binding); + const bool sampled = binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + const auto& allocation = set == 0 ? cacheAllocation : fragment.cacheAllocation; + const bool recorded = sampled && set < tableWrites.size() && allocation.layout != VK_NULL_HANDLE; + std::vector> runs; + const auto firstWrite = writes.size(); + const auto firstImage = images.size(); + for (std::size_t first = 0; first < binding.imageAllocations.size();) { + if (binding.imageAllocations[first] == std::numeric_limits::max()) { + ++first; + continue; + } + auto last = first; + while (last < binding.imageAllocations.size() && binding.imageAllocations[last] != std::numeric_limits::max()) ++last; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = set == 0 ? _set : fragment.set; + write.dstBinding = binding.layout.binding; + write.dstArrayElement = static_cast(first); + write.descriptorCount = static_cast(last - first); + write.descriptorType = binding.layout.descriptorType; + write.pImageInfo = images.data() + images.size(); + for (auto element = first; element < last; ++element) { + const auto index = binding.imageAllocations[element]; + if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLER) images.push_back({samplers[index]->Handle(), 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()}); + } + if (recorded) runs.emplace_back(write.dstArrayElement, write.descriptorCount); + writes.push_back(write); + first = last; + } + if (!recorded) continue; + const auto& held = allocation.table; + auto& counters = TextureCounts(); + const bool heldAlive = held != nullptr && held->textures != nullptr && std::none_of(held->textures->begin(), held->textures->end(), [](const std::weak_ptr& texture) { return texture.expired(); }); + if (heldAlive && held->binding == binding.layout.binding && held->runs == runs && std::equal(held->images.begin(), held->images.end(), images.begin() + static_cast(firstImage), images.end(), [](const VkDescriptorImageInfo& a, const VkDescriptorImageInfo& b) { return a.sampler == b.sampler && a.imageView == b.imageView && a.imageLayout == b.imageLayout; })) { + keptWrites.emplace_back(firstWrite, writes.size()); + counters.tableWritesKept.fetch_add(1, std::memory_order_relaxed); + } else { + counters.tableWritesMade.fetch_add(1, std::memory_order_relaxed); + } + tableWrites[set] = {true, false, binding.layout.binding, firstImage, images.size() - firstImage, std::move(runs)}; + continue; + } VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; write.dstSet = setOf(binding) == 0 ? _set : fragment.set; write.dstBinding = binding.layout.binding; @@ -1651,6 +2409,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"); @@ -1664,7 +2423,22 @@ void ShaderResources::buildComplete() { } writes.push_back(write); } - context.Resolved(&DeviceFunctions::updateDescriptorSets, "vkUpdateDescriptorSets")(context.device, static_cast(writes.size()), writes.data(), 0, nullptr); + if (keptWrites.empty()) { + context.Resolved(&DeviceFunctions::updateDescriptorSets, "vkUpdateDescriptorSets")(context.device, static_cast(writes.size()), writes.data(), 0, nullptr); + } else { + std::vector made; + made.reserve(writes.size()); + std::size_t next = 0; + for (std::size_t index = 0; index < writes.size(); ++index) { + if (next < keptWrites.size() && index == keptWrites[next].first) { + index = keptWrites[next++].second - 1u; + continue; + } + made.push_back(writes[index]); + } + if (!made.empty()) context.Resolved(&DeviceFunctions::updateDescriptorSets, "vkUpdateDescriptorSets")(context.device, static_cast(made.size()), made.data(), 0, nullptr); + } + for (auto& table : tableWrites) table.written = table.recorded; } for (const auto& allocation : allocations) { if (allocation.adjustment == 0) continue; @@ -1723,6 +2497,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}; } @@ -1740,9 +2515,9 @@ void ShaderResources::noteReusable() { captureValidation(); reusable = false; directRegions.clear(); - if (guestMemory.HasCopiedWrites() || HoldsLease() || usesBda || (bda != nullptr && LoopGuardEnabled())) return; + if (guestMemory.HasCopiedWrites() || HoldsLease() || usesBda || !multisampleSources.empty() || (bda != nullptr && LoopGuardEnabled())) return; if (std::any_of(textures.begin(), textures.end(), [](const std::shared_ptr& texture) { return texture != nullptr && texture->RefreshedPerUse(); })) return; - if (TemplateDataRefresh() && std::any_of(allocations.begin(), allocations.end(), [](const Allocation& allocation) { return allocation.buffer != nullptr && !allocation.guest && allocation.size > MaxRefreshBytes; })) return; + if (TemplateDataRefresh() && std::any_of(allocations.begin(), allocations.end(), [](const Allocation& allocation) { return allocation.buffer != nullptr && !allocation.guest && allocation.role != ShaderRecompiler::DescriptorRole::ImageTableMap && allocation.size > MaxRefreshBytes; })) return; const auto regions = guestMemory.DirectRegions(); if (!regions.has_value()) return; for (const auto& [begin, end] : *regions) { @@ -1755,9 +2530,20 @@ void ShaderResources::noteReusable() { if (serial == 0) return; directRegions.push_back({begin, end, serial}); } + for (const auto& cut : guestMemory.CutRegions()) { + for (auto& region : directRegions) { + if (region.begin != cut.begin) continue; + region.regionEnd = cut.end; + region.committedEnd = cut.committedEnd; + } + } 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) { @@ -1818,6 +2604,7 @@ void VisitContentKey(const CompiledShader& shader, bool dataWords, bool movableB packBits(binding.imageAtomic); packBits(binding.samplerUnnormalized); packBits(binding.samplerIntegerBorder); + packBits(binding.samplerCoordinatesIgnored); packBits(binding.imageUnnormalized); packBits(binding.imageUnnormalizedBaseLevel); // Read-only elements are bound without a write set: an object built for one written set @@ -1825,26 +2612,127 @@ void VisitContentKey(const CompiledShader& shader, bool dataWords, bool movableB packBits(binding.bufferWritten); visit(static_cast(binding.imageSamplers.size())); for (const auto sampler : binding.imageSamplers) visit(sampler); + visit(static_cast(binding.imageShapes.size())); + for (const auto shape : binding.imageShapes) visit(static_cast(shape)); } } std::vector ShaderResources::ContentKey(const CompiledShader& shader, bool dataWords, bool movableBuffers) { - Require(shader.program != nullptr, "missing compiled shader"); std::vector key; - std::size_t words = 5; + AppendContentKey(shader, dataWords, movableBuffers, key); + return key; +} + +void ShaderResources::AppendContentKey(const CompiledShader& shader, bool dataWords, bool movableBuffers, std::vector& key) { + Require(shader.program != nullptr, "missing compiled shader"); + std::size_t words = key.size() + 5; const auto addWords = [&](std::size_t count) { if (count > key.max_size() - words) throw std::length_error("resource content key exceeds vector limits"); words += count; }; for (const auto& binding : shader.program->bindings) { - addWords(18); + addWords(20); addWords(binding.imageSamplers.size()); + addWords(binding.imageShapes.size()); if ((dataWords && !movableBuffers) || !DataRole(binding.role)) addWords(binding.guestDescriptor.size()); - for (const auto* bits : {&binding.imageWritten, &binding.samplerDepthCompare, &binding.imageDepthCompare, &binding.imageAtomic, &binding.samplerUnnormalized, &binding.samplerIntegerBorder, &binding.imageUnnormalized, &binding.imageUnnormalizedBaseLevel, &binding.bufferWritten}) addWords(bits->size() / 32u + (bits->size() % 32u != 0 ? 1u : 0u)); + for (const auto* bits : {&binding.imageWritten, &binding.samplerDepthCompare, &binding.imageDepthCompare, &binding.imageAtomic, &binding.samplerUnnormalized, &binding.samplerIntegerBorder, &binding.samplerCoordinatesIgnored, &binding.imageUnnormalized, &binding.imageUnnormalizedBaseLevel, &binding.bufferWritten}) addWords(bits->size() / 32u + (bits->size() % 32u != 0 ? 1u : 0u)); } key.reserve(words); VisitContentKey(shader, dataWords, movableBuffers, [&](std::uint32_t word) { key.push_back(word); }); - return key; +} + +namespace { + +using Component = ContentKeyComponent; + +bool LabelShaderKey(std::span key, std::size_t at, std::size_t end, bool movable, std::vector& labels) { + const auto label = [&](std::size_t first, std::size_t count, Component component) { + if (first + count > end) return false; + std::fill(labels.begin() + static_cast(first), labels.begin() + static_cast(first + count), static_cast(component)); + return true; + }; + if (!label(at, 5, Component::Layout)) return false; + const bool dataWords = key[at] != 0; + const auto bindings = key[at + 4]; + at += 5; + for (std::uint32_t binding = 0; binding < bindings; ++binding) { + if (!label(at, 8, Component::Layout)) return false; + const auto role = static_cast(key[at + 1]); + const auto size = key[at + 7]; + at += 8; + const auto words = DataRole(role) && !(dataWords && !movable) ? 0u : size; + if (at + words > end) return false; + for (std::uint32_t i = 0; i < words; ++i) { + auto component = Component::Other; + if (role == ShaderRecompiler::DescriptorRole::GuestBuffers) component = static_cast(static_cast(Component::BufferWord0) + i % 4u); + else if (role == ShaderRecompiler::DescriptorRole::GuestImages || role == ShaderRecompiler::DescriptorRole::ImageTable) component = Component::Image; + else if (role == ShaderRecompiler::DescriptorRole::GuestSamplers || role == ShaderRecompiler::DescriptorRole::SamplerTable) component = Component::Sampler; + else if (DataRole(role)) component = Component::ShaderData; + labels[at + i] = static_cast(component); + } + at += words; + for (int flags = 0; flags < 9; ++flags) { + if (at >= end) return false; + const auto count = 1 + (static_cast(key[at]) + 31) / 32; + if (!label(at, count, Component::Other)) return false; + at += count; + } + if (at >= end) return false; + const auto samplers = static_cast(key[at]); + if (!label(at, 1 + samplers, Component::Sampler)) return false; + at += 1 + samplers; + if (at >= end) return false; + const auto shapes = static_cast(key[at]); + if (!label(at, 1 + shapes, Component::Image)) return false; + at += 1 + shapes; + } + return at == end; +} + +bool LabelContentKey(std::span key, bool draw, bool movable, std::vector& labels) { + labels.assign(key.size(), static_cast(Component::Layout)); + if (!draw) { + if (LabelShaderKey(key, 0, key.size(), movable, labels)) return true; + if (key.size() < 2) return false; + labels.assign(key.size(), static_cast(Component::Layout)); + if (!LabelShaderKey(key, 0, key.size() - 2, movable, labels)) return false; + labels[key.size() - 2] = static_cast(Component::Other); + labels[key.size() - 1] = static_cast(Component::Other); + return true; + } + if (key.size() < 4) return false; + std::fill(labels.begin() + 1, labels.begin() + 3, static_cast(Component::Other)); + std::size_t at = 4; + for (std::uint32_t stage = 0; stage < key[3]; ++stage) { + if (at >= key.size()) return false; + const auto size = static_cast(key[at]); + ++at; + if (at + size > key.size() || !LabelShaderKey(key, at, at + size, movable, labels)) return false; + at += size; + } + if (at >= key.size()) return false; + for (auto tail = at + 1; tail < key.size(); ++tail) labels[tail] = static_cast(Component::DrawTarget); + return true; +} + +} + +bool ShaderResources::DiffContentKeys(std::span previous, std::span key, bool draw, ContentKeyDiff& diff) { + diff = {}; + std::vector labels; + bool labelled = false; + for (const bool movable : {draw, false}) { + labelled = LabelContentKey(key, draw, movable, labels); + if (labelled) break; + } + if (!labelled || previous.size() != key.size()) { + diff[static_cast(Component::Layout)] = true; + return false; + } + for (std::size_t i = 0; i < key.size(); ++i) { + if (previous[i] != key[i]) diff[labels[i]] = true; + } + return true; } bool MatchesContentKey(const CompiledShader& shader, std::span key) { @@ -1864,7 +2752,7 @@ namespace { // of the reused ones, fast = every image proved current from stamps, full = the lookups were repeated. // Why the fast path left an object to the full walk (the "fast-fail by reason" counts). using FastFail = ShaderResources::FastFail; -constexpr std::array(FastFail::Count)> FastFailNames{"no record", "collect", "pending image", "evicted image", "memory changed", "keys", "cleared view", "storage keys", "depth surface"}; +constexpr std::array(FastFail::Count)> FastFailNames{"no record", "collect", "pending image", "evicted image", "memory changed", "keys", "cleared view", "storage keys", "depth surface", "replaced texture", "audit"}; using OwnRefreshFallback = ShaderResources::OwnRefreshFallback; constexpr std::array(OwnRefreshFallback::Count)> OwnRefreshFallbackNames{"disabled", "snapshot texture", "cleared view", "foreign view", "surface key", "not imported", "uncached", "re-run failed"}; @@ -1980,10 +2868,10 @@ void countRevalidate(bool fast, bool ok, std::chrono::steady_clock::time_point s const auto nowMs = std::chrono::duration_cast(now.time_since_epoch()).count(); auto last = profile.lastReport.load(); if (nowMs - last < 10000 || !profile.lastReport.compare_exchange_strong(last, nowMs)) return; - const auto byReason = [](const auto& counts, const auto& names) { + const auto byReason = [](auto& counts, const auto& names) { std::string reasons; for (std::size_t i = 0; i < names.size(); ++i) { - const auto count = counts[i].load(std::memory_order_relaxed); + const auto count = counts[i].exchange(0, std::memory_order_relaxed); if (count == 0) continue; reasons += " " + std::string(names[i]) + " " + std::to_string(count); } @@ -1992,7 +2880,7 @@ void countRevalidate(bool fast, bool ok, std::chrono::steady_clock::time_point s const auto reasons = byReason(profile.fastFails, FastFailNames); const auto fullReasons = byReason(profile.fullByReason, FastFailNames); const auto fallbacks = byReason(profile.ownFallbacks, OwnRefreshFallbackNames); - AgcDriver::ProfilePrint_nid_no_patch("[rescache] revalidate %llu calls %.1f ms: fast %llu, full %llu, failed %llu; fast-fail by reason:%s; full walks: T1 refreshed %llu (%llu storage images, %llu view sources, %llu views served by their own pending source, %llu overlaps left by the refresh accepted), full by reason:%s, T1 fallback by reason:%s, proofs verified %llu; fast-fail pending image: own 0 (the query excepts the own object), foreign %llu, snapshot %llu, T1 eligible %llu; keys proven: %llu sampled, %llu storage; epoch gate: %llu registry scans skipped, %llu import loops skipped\n", static_cast(profile.calls.load()), profile.nanoseconds.load() / 1e6, static_cast(profile.fast.load()), static_cast(profile.full.load()), static_cast(profile.failed.load()), reasons.c_str(), static_cast(profile.ownRefreshed.load()), static_cast(profile.ownStorageRefreshes.load()), static_cast(profile.ownViewRefreshes.load()), static_cast(profile.ownSourceViews.load()), static_cast(profile.refreshedOverlaps.load()), fullReasons.c_str(), fallbacks.c_str(), static_cast(profile.proofsVerified.load()), static_cast(profile.pendingForeign.load()), static_cast(profile.pendingSnapshot.load()), static_cast(profile.pendingT1Eligible.load()), static_cast(profile.keysProven.load()), static_cast(profile.storageKeysProven.load()), static_cast(profile.serialSkips.load()), static_cast(profile.importSkips.load())); + AgcDriver::ProfilePrint_nid_no_patch("[rescache] revalidate (10 s): %llu calls %.1f ms: fast %llu, full %llu, failed %llu; fast-fail by reason:%s; full walks: T1 refreshed %llu (%llu storage images, %llu view sources, %llu views served by their own pending source, %llu overlaps left by the refresh accepted), full by reason:%s, T1 fallback by reason:%s, proofs verified %llu; fast-fail pending image: own 0 (the query excepts the own object), foreign %llu, snapshot %llu, T1 eligible %llu; keys proven: %llu sampled, %llu storage; epoch gate: %llu registry scans skipped, %llu import loops skipped; lookup log: %llu builds and full walks kept their records past the bound\n", static_cast(profile.calls.exchange(0, std::memory_order_relaxed)), profile.nanoseconds.exchange(0, std::memory_order_relaxed) / 1e6, static_cast(profile.fast.exchange(0, std::memory_order_relaxed)), static_cast(profile.full.exchange(0, std::memory_order_relaxed)), static_cast(profile.failed.exchange(0, std::memory_order_relaxed)), reasons.c_str(), static_cast(profile.ownRefreshed.exchange(0, std::memory_order_relaxed)), static_cast(profile.ownStorageRefreshes.exchange(0, std::memory_order_relaxed)), static_cast(profile.ownViewRefreshes.exchange(0, std::memory_order_relaxed)), static_cast(profile.ownSourceViews.exchange(0, std::memory_order_relaxed)), static_cast(profile.refreshedOverlaps.exchange(0, std::memory_order_relaxed)), fullReasons.c_str(), fallbacks.c_str(), static_cast(profile.proofsVerified.exchange(0, std::memory_order_relaxed)), static_cast(profile.pendingForeign.exchange(0, std::memory_order_relaxed)), static_cast(profile.pendingSnapshot.exchange(0, std::memory_order_relaxed)), static_cast(profile.pendingT1Eligible.exchange(0, std::memory_order_relaxed)), static_cast(profile.keysProven.exchange(0, std::memory_order_relaxed)), static_cast(profile.storageKeysProven.exchange(0, std::memory_order_relaxed)), static_cast(profile.serialSkips.exchange(0, std::memory_order_relaxed)), static_cast(profile.importSkips.exchange(0, std::memory_order_relaxed)), static_cast(lookupBuildsPastBound.exchange(0, std::memory_order_relaxed))); } // APS5_NO_EPOCH_REVALIDATE=1: the per-element registry, cache and stamp checks and the per-region @@ -2002,6 +2890,19 @@ bool EpochRevalidate() { return enabled; } +std::chrono::steady_clock::duration TemplateAuditInterval() { + static const auto interval = [] { + const char* text = std::getenv("APS5_TEMPLATE_AUDIT_MS"); + return std::chrono::milliseconds(text != nullptr ? std::strtoull(text, nullptr, 10) : 1000ull); + }(); + return interval; +} + +bool TemplateAuditDue(std::chrono::steady_clock::time_point last) { + const auto interval = TemplateAuditInterval(); + return interval.count() != 0 && std::chrono::steady_clock::now() - last >= interval; +} + } // Turns the lookups' records into this object's per-texture validation records (see @@ -2021,6 +2922,10 @@ void ShaderResources::captureValidation() { lookupLog().clear(); } +std::size_t ShaderResources::LookupLogCapacity() { + return lookupLog().capacity(); +} + // Proves every texture and storage image still current without repeating its lookup: exactly the // "unchanged" branches of cachedTexture and StorageTexture::Refresh, evaluated from write stamps, // the pending-results registry and the DCC keys. Per element: collect first (stamps come only from @@ -2076,8 +2981,10 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::spanReleased()) return fail(FastFail::Replaced); const auto address = surface.resource.baseAddress; const auto bytes = static_cast(surface.bytes); + if (!FlushDepthPlanes(address, bytes).empty()) return fail(FastFail::Changed); surface.collected = GuestMemory::CollectWrites(address, bytes); if (surface.collected == 0) return fail(FastFail::Collect); const auto* source = surface.source; @@ -2117,7 +3024,7 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::spanCached()) return fail(FastFail::Evicted); - queries.push_back({address, bytes, source->Generation()}); + queries.push_back({address, bytes, source->GenerationFor(address, bytes)}); images.push_back(source); } else { queries.push_back({address, bytes, surface.generation}); @@ -2131,6 +3038,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 @@ -2141,7 +3052,7 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::spanCached()) return fail(FastFail::Evicted); - queries.push_back({address, bytes, image->Generation()}); + queries.push_back({address, bytes, image->GenerationFor(address, bytes)}); images.push_back(image); } if (!pending.empty()) { @@ -2198,8 +3109,10 @@ bool ShaderResources::fastRevalidateEach() { if (!DepthSurfaceHolds(context, surface.resource, textures[i].get())) return false; continue; } + if (textures[i]->Released()) return false; const auto address = surface.resource.baseAddress; const auto bytes = static_cast(surface.bytes); + if (!FlushDepthPlanes(address, bytes).empty()) return false; surface.collected = GuestMemory::CollectWrites(address, bytes); if (surface.collected == 0) return false; if (PendingStorageOverlaps(address, bytes, surface.source)) return false; @@ -2207,7 +3120,7 @@ bool ShaderResources::fastRevalidateEach() { if (surface.source != nullptr) { // The view follows the image: it needs the image current with guest memory, as the // lookup's Refresh would make it. - if (!StorageImageCached(context, surface.source) || !GuestMemory::UnchangedSince(address, bytes, surface.source->Generation())) return false; + if (!StorageImageCached(context, surface.source) || !GuestMemory::UnchangedSince(address, bytes, surface.source->GenerationFor(address, bytes))) return false; } else if (!GuestMemory::UnchangedSince(address, bytes, surface.generation)) { return false; } @@ -2234,10 +3147,14 @@ 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; - if (!GuestMemory::UnchangedSince(address, bytes, image->Generation())) return false; + if (!GuestMemory::UnchangedSince(address, bytes, image->GenerationFor(address, bytes))) return false; } // Every check passed: snapshot textures are current at the collects issued before them. Storage // images keep their own generation: nothing was stamped above it, so the blocks a later write @@ -2271,9 +3188,9 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span if (found != nullptr && !Texture::CanCopyFrom(*found, surface.resource)) found.reset(); if (found != nullptr) { if (found != source) return OwnRefreshFallback::ForeignView; - if (!GuestMemory::UnchangedSince(address, bytes, source->Generation())) { + if (!GuestMemory::UnchangedSince(address, bytes, source->GenerationFor(address, bytes))) { countOwnRefresh(false); - source->Refresh(); + refreshForView(*source, surface.resource); } return OwnRefreshFallback::Count; } @@ -2304,7 +3221,7 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage) { for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8u, 8u); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + if (IsNullTextureResource(words)) continue; if (textureIndex >= textures.size()) return OwnRefreshFallback::Uncached; const auto index = textureIndex++; const auto overlap = listed(index, false); @@ -2314,7 +3231,7 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span } else if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) { for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8u, 8u); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + if (IsNullTextureResource(words)) continue; if (storageIndex >= storageTextures.size()) return OwnRefreshFallback::Uncached; const auto index = storageIndex++; if (listed(index, true) == overlapping.end()) continue; @@ -2323,24 +3240,45 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span } } } + for (const auto& table : tableStages) { + for (const auto index : table.elementTextures) { + if (index == std::numeric_limits::max()) continue; + if (index != textureIndex++ || index >= textures.size()) return OwnRefreshFallback::Uncached; + const auto overlap = listed(index, false); + if (overlap == overlapping.end()) continue; + if (const auto fallback = refreshView(index, *overlap); fallback != OwnRefreshFallback::Count) return fallback; + } + } return OwnRefreshFallback::Count; } -void CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes) { - if (bytes == 0 || Recorder::InCompletion()) return; +bool CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes) { + if (bytes == 0 || Recorder::InCompletion()) return false; auto* recorder = Recorder::Active(); - if (recorder == nullptr) return; + if (recorder == nullptr) return false; const auto drawWriters = DrawCopiedWriters(); const bool labels = Recorder::PendingCompletionLabels() != 0; - if ((context.copiedWriters == nullptr || context.copiedWriters->empty()) && drawWriters->empty() && !labels) return; - if (!recorder->PendingWriteOverlaps(address, bytes)) return; + if ((context.copiedWriters == nullptr || context.copiedWriters->empty()) && drawWriters->empty() && !labels) return false; + if (!recorder->PendingWriteOverlaps(address, bytes)) return false; const auto overlaps = [&](const auto& writers) { - return std::any_of(writers.begin(), writers.end(), [&](const auto& writer) { return writer->WritesOverlap(address, bytes); }); + return std::any_of(writers.begin(), writers.end(), [&](const auto& writer) { return writer->CopiedWritesOverlap(address, bytes); }); }; if ((context.copiedWriters != nullptr && overlaps(*context.copiedWriters)) || overlaps(*drawWriters) || (labels && recorder->PendingLabelIn(address, bytes))) { Recorder::CountSync(4); recorder->SyncThrough(address, bytes); + return true; } + return false; +} + +const ShaderRecompiler::DescriptorBinding* ShaderResources::tableImages(std::span shaders, ShaderRecompiler::ShaderStage stage) { + for (const auto& shader : shaders) { + if (shader.stage != stage || shader.program == nullptr) continue; + for (const auto& binding : shader.program->bindings) { + if (binding.role == ShaderRecompiler::DescriptorRole::ImageTable) return &binding; + } + } + return nullptr; } bool ShaderResources::Revalidate(std::span shaders, ProofReport* report) { @@ -2348,8 +3286,12 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR if (!reusable || shaders.empty()) return false; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - for (const auto& region : directRegions) CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); + for (const auto& region : directRegions) { + static_cast(FlushDepthPlanes(region.begin, static_cast(region.end - region.begin))); + CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); + } for (const auto& range : describedRanges) { + if (!range.imageTable) RequireNoMultisampleColorTarget(range.address, static_cast(range.bytes), "a cached image access"); CompletePendingCpuWrites(context, range.address, static_cast(range.bytes)); if (range.dccAddress != 0) CompletePendingCpuWrites(context, range.dccAddress, DccKeyBytes(range.bytes)); } @@ -2372,7 +3314,9 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR // they must hand back the very objects the set's views belong to. The build appended them stage // by stage, binding by binding, so the walk repeats that order. const auto fullWalk = [&] { + lastFullWalk = std::chrono::steady_clock::now(); lookupLog().clear(); + BuildLookups lookups; std::size_t textureIndex = 0; std::size_t storageIndex = 0; for (const auto& shader : shaders) { @@ -2382,27 +3326,65 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage) { const auto elementWords = binding.count != 0 ? binding.guestDescriptor.size() / binding.count : 0; for (std::uint32_t element = 0; element < binding.count; ++element) { - const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + const auto raw = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); + std::array narrowed{}; + const auto words = firstLayerWords(raw, binding.imageShape, narrowed, false); + if (IsNullTextureResource(words)) continue; const auto resource = DecodeTextureResource(words); const VkComponentMapping components = ViewComponents(resource); - if (textureIndex >= textures.size() || cachedTexture(context, words, resource, components, 0, !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element)) != textures[textureIndex]) return false; + const VkImageViewType multisampleViewType = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2DMultisampleArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; + if (textureIndex >= textures.size() || cachedTexture(context, words, resource, components, 0, !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element), multisampleViewType) != textures[textureIndex]) return false; ++textureIndex; } } else if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) { for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8u, 8u); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + if (IsNullTextureResource(words)) continue; if (storageIndex >= storageTextures.size()) return false; std::shared_ptr expected; if (SameAsPreviousStorageElement(binding, element) && StorageDedupeEnabled()) expected = storageTextures[storageIndex - 1]; - else expected = cachedStorageTexture(context, words, DecodeTextureResource(words), storageMips[storageIndex]); + else expected = cachedStorageTexture(context, words, DecodeTextureResource(words), storageMips[storageIndex], 0, storageWritten[storageIndex]); if (expected != storageTextures[storageIndex]) return false; ++storageIndex; } } } } + for (const auto& table : tableStages) { + if (table.elementTextures.empty()) continue; + const auto* images = tableImages(shaders, table.stage); + if (images == nullptr || images->count != table.elementTextures.size()) return false; + std::vector> cached; + if (auto* reused = FindReusedTable(context.device, *images, CurrentTableState()); reused != nullptr && TableCurrent(*reused) && CachedTableTextures(*reused, cached)) { + auto next = cached.begin(); + bool same = true; + for (std::uint32_t element = 0; same && element < images->count; ++element) { + const auto& entry = reused->elements[element]; + const auto index = table.elementTextures[element]; + if (entry.rejected) { + same = index == std::numeric_limits::max(); + continue; + } + const auto& texture = *next++; + if (index == std::numeric_limits::max()) continue; + same = index == textureIndex && textureIndex < textures.size() && textures[textureIndex] == texture; + ++textureIndex; + } + if (!same) return false; + TextureCounts().tablesReused.fetch_add(1, std::memory_order_relaxed); + continue; + } + for (std::uint32_t element = 0; element < images->count; ++element) { + if (table.elementTextures[element] == std::numeric_limits::max()) continue; + const auto raw = std::span(images->guestDescriptor).subspan(static_cast(element) * 8u, 8u); + std::array narrowed{}; + const auto words = firstLayerWords(raw, images->imageShapes.at(element), narrowed, false); + const auto resource = DecodeTextureResource(words); + const VkImageViewType multisampleViewType = images->imageShapes.at(element) == ShaderRecompiler::DescriptorImageShape::Image2DMultisampleArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; + if (textureIndex >= textures.size() || table.elementTextures[element] != textureIndex || cachedTexture(context, words, resource, ViewComponents(resource), 0, images->imageDepthCompare.at(element), multisampleViewType) != textures[textureIndex]) return false; + ++textureIndex; + } + } if (textureIndex != textures.size() || storageIndex != storageTextures.size()) return false; // The walk proved every object current again: the records move to what it proved, or one // spurious stamp (a label sharing a 64 KiB block with a surface's edge) would keep this @@ -2438,6 +3420,11 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR } if (fallback != OwnRefreshFallback::Count) countOwnRefreshFallback(fallback); } + if (fast && TemplateAuditDue(lastFullWalk)) { + fast = false; + ownRefreshed = false; + reason = FastFail::Audit; + } if (report != nullptr) report->path = !fast ? ProofPath::Full : ownRefreshed ? ProofPath::OwnRefreshed : ProofPath::Fast; if (!fast) { countFullWalk(reason); @@ -2445,7 +3432,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR const auto failure = [&] { switch (reason) { case FastFail::Pending: return ProofFailure::Pending; - case FastFail::Evicted: return ProofFailure::Evicted; + case FastFail::Evicted: case FastFail::Replaced: return ProofFailure::Evicted; case FastFail::Changed: return ProofFailure::Changed; case FastFail::Keys: case FastFail::ClearedView: case FastFail::StorageKeys: return ProofFailure::Keys; default: return ProofFailure::Other; @@ -2488,6 +3475,10 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR auto serial = HostImportSerial(context, region.begin, static_cast(region.end - region.begin), true); if (serial == 0) serial = ImageMirrorSerial(context, region.begin, static_cast(region.end - region.begin)); if (serial != region.serial) return false; + if (region.regionEnd != 0) { + const auto committed = GuestMemory::DescribeCommitted(region.begin, static_cast(region.regionEnd - region.begin)); + if (committed.ranges.size() != 1 || committed.ranges.front().first != region.begin || committed.ranges.front().second != region.committedEnd) return false; + } } return true; }; @@ -2540,6 +3531,14 @@ namespace { // seen for the same shader variant(s) and the first differing word is charged to its binding's role // (and to its index within the V# or T# element), printed as [rescache] every 10 s: it says whether // V# bases of ring allocations, SRT words or image descriptors churn. +struct KeyMissWindow { + std::uint64_t compared = 0; + std::uint64_t noCandidate = 0; + std::uint64_t unlabelled = 0; + std::uint64_t identical = 0; + std::array(ContentKeyComponent::Count)> differing{}; +}; + struct ChurnCounts { std::array roles{}; std::array bufferWords{}; @@ -2548,6 +3547,7 @@ struct ChurnCounts { std::uint64_t sameKey = 0; std::uint64_t layout = 0; std::uint64_t drawWords = 0; + KeyMissWindow window; }; // Dispatch keys and draw keys are counted apart: what churns a draw's key decides the draw steps. @@ -2556,9 +3556,21 @@ struct ChurnProfile { std::unordered_map lastByVariant; ChurnCounts dispatch; ChurnCounts draws; + std::unordered_map tracedByVariant; + std::size_t tracedLines = 0; std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); }; +constexpr std::size_t MaxTracedLines = 256; +constexpr std::uint32_t MaxTracedLinesPerVariant = 4; + +const char* componentName(std::size_t component) { + constexpr std::array(ContentKeyComponent::Count)> names{"layout", "V# word 0", "V# word 1", "V# word 2", "V# word 3", "image", "sampler", "shader-data", "other", "draw target/index"}; + return names[component]; +} + +void traceMiss(ChurnProfile& churn, bool draw, std::uint64_t variants, const ContentKeyDiff* parts); + ChurnProfile& Churn() { static ChurnProfile profile; return profile; @@ -2614,16 +3626,87 @@ 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) { + if (churn.tracedLines >= MaxTracedLines || ++churn.tracedByVariant[variants] > MaxTracedLinesPerVariant) return; + ++churn.tracedLines; + std::string changed = parts == nullptr ? "first-seen" : ""; + for (std::size_t i = 0; parts != nullptr && i < parts->size(); ++i) { + if (!(*parts)[i]) continue; + if (!changed.empty()) changed += ", "; + changed += componentName(i); + } + if (changed.empty()) changed = "no-component-change"; + const auto hashed = (variants ^ 0x9e3779b97f4a7c15ull) * 0xff51afd7ed558ccdull; + AgcDriver::ProfilePrint_nid_no_patch("[keymiss] resource-content %s shader set %016llx: %s\n", draw ? "draw" : "dispatch", static_cast(hashed ^ (hashed >> 33u)), changed.c_str()); +} + +} + void ResourceCache::noteMiss(const Key& key) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - if (!profile || key.size() < 5) return; + static const bool trace = std::getenv("APS5_TRACE_RESOURCE_KEY_MISSES") != nullptr; + if ((!profile && !trace) || key.size() < 5) return; // The variant(s) the key belongs to: a compute key starts with the data-words flag, the stage // and its variant, a draw key with a marker, the device and its stages' keys (Draw.cpp // DrawResourceKey). @@ -2644,6 +3727,8 @@ void ResourceCache::noteMiss(const Key& key) { const auto found = churn.lastByVariant.find(variants); if (found == churn.lastByVariant.end()) { ++counts.firstSeen; + ++counts.window.noCandidate; + if (trace) traceMiss(churn, draw, variants, nullptr); churn.lastByVariant.emplace(variants, key); } else { const auto& previous = found->second; @@ -2664,6 +3749,19 @@ void ResourceCache::noteMiss(const Key& key) { std::size_t at = 0; if (!chargeShaderKey(counts, key, at, diff)) ++counts.layout; } + if (trace) { + ContentKeyDiff parts; + ++counts.window.compared; + if (!ShaderResources::DiffContentKeys(previous, key, draw, parts)) ++counts.window.unlabelled; + bool anyPart = false; + for (std::size_t i = 0; i < parts.size(); ++i) { + if (!parts[i]) continue; + ++counts.window.differing[i]; + anyPart = true; + } + if (!anyPart) ++counts.window.identical; + traceMiss(churn, draw, variants, &parts); + } found->second = key; } const auto now = std::chrono::steady_clock::now(); @@ -2688,13 +3786,26 @@ void ResourceCache::noteMiss(const Key& key) { line += item; AgcDriver::ProfilePrint_nid_no_patch("%s\n", line.c_str()); }; - report("dispatch", churn.dispatch); - report("draws", churn.draws); + if (profile) { + report("dispatch", churn.dispatch); + report("draws", churn.draws); + } + const auto keyMiss = [](const char* what, KeyMissWindow& window) { + const auto count = [](std::uint64_t value) { return static_cast(value); }; + const auto part = [&](ContentKeyComponent component) { return count(window.differing[static_cast(component)]); }; + AgcDriver::ProfilePrint_nid_no_patch("[keymiss] resource-content %s (10 s): misses %llu; differing: layout %llu, V# word0 %llu, V# word1 %llu, V# word2 %llu, V# word3 %llu, image %llu, sampler %llu, shader-data %llu, other %llu, draw target/index %llu; no-candidate %llu; identical %llu; not comparable %llu\n", what, count(window.compared + window.noCandidate), part(ContentKeyComponent::Layout), part(ContentKeyComponent::BufferWord0), part(ContentKeyComponent::BufferWord1), part(ContentKeyComponent::BufferWord2), part(ContentKeyComponent::BufferWord3), part(ContentKeyComponent::Image), part(ContentKeyComponent::Sampler), part(ContentKeyComponent::ShaderData), part(ContentKeyComponent::Other), part(ContentKeyComponent::DrawTarget), count(window.noCandidate), count(window.identical), count(window.unlabelled)); + window = KeyMissWindow{}; + }; + if (trace) { + keyMiss("dispatch", churn.dispatch.window); + keyMiss("draw", churn.draws.window); + } } void ResourceCache::Insert(const Key& key, std::shared_ptr resources, std::vector>* evicted) { std::lock_guard lock(mutex); - if (!resources->HasCachedStorageImages()) return; + dropReleasedTextures(evicted); + if (!resources->HasCachedStorageImages() || resources->HoldsReleasedTexture()) return; if (const auto found = index.find(key); found != index.end()) { if (evicted != nullptr) evicted->push_back(std::move(found->second->second)); entries.erase(found->second); @@ -2702,16 +3813,33 @@ 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(); } + if (entries.front().second->TableElements() == 0) return; + std::uint64_t tables = 0; + for (const auto& entry : entries) tables += entry.second->TableElements(); + for (auto it = std::prev(entries.end()); tables > TableElementBudget() && it != entries.begin();) { + const auto elements = it->second->TableElements(); + if (elements == 0) { + --it; + continue; + } + tables -= elements; + if (evicted != nullptr) evicted->push_back(std::move(it->second)); + index.erase(it->first); + it = std::prev(entries.erase(it)); + } } void ResourceCache::Remove(const Key& key, const ShaderResources* object) { @@ -2736,12 +3864,26 @@ void ResourceCache::RemoveImage(const StorageTexture* image, std::vector>* evicted) { + if (!TexturesReleased().exchange(false, std::memory_order_acq_rel)) return; + for (auto it = entries.begin(); it != entries.end();) { + if (!it->second->HoldsReleasedTexture()) { + ++it; + continue; + } + if (evicted != nullptr) evicted->push_back(std::move(it->second)); + index.erase(it->first); + it = entries.erase(it); + } +} + bool ResourceCache::Touch(const Key& key) { resourceCacheTouches.fetch_add(1, std::memory_order_relaxed); std::lock_guard lock(mutex); const auto found = index.find(key); if (found == index.end()) return false; entries.splice(entries.begin(), entries, found->second); + noteUse(key); return true; } @@ -2757,6 +3899,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 { @@ -2764,6 +3921,27 @@ 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"); + const auto parsed = value ? std::strtoull(value, nullptr, 10) : 65536ull; + return static_cast(parsed != 0 ? parsed : 65536ull); + }(); + return elements; +} + +std::uint64_t ResourceCache::TableElements() const { + std::lock_guard lock(mutex); + std::uint64_t tables = 0; + for (const auto& entry : entries) tables += entry.second->TableElements(); + return tables; +} + void ResourceCache::erase(const Key& key) { const auto found = index.find(key); if (found == index.end()) return; @@ -2776,12 +3954,26 @@ ResourceCache& SharedResourceCache() { return *cache; } -DescriptorCache::DescriptorCache(const Context& context) : context(context), destroyLayout(context.Function("vkDestroyDescriptorSetLayout")), destroyPool(context.Function("vkDestroyDescriptorPool")), resetPool(context.Function("vkResetDescriptorPool")) {} +namespace { +std::size_t KeptSets() { + static const std::size_t count = [] { + const char* text = std::getenv("APS5_KEPT_DESCRIPTOR_SETS"); + return text != nullptr ? static_cast(std::strtoull(text, nullptr, 10)) : std::size_t{8}; + }(); + return count; +} + +} + +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) { + kept.reserve(KeptSets() + 1u); +} DescriptorCache::~DescriptorCache() { for (const auto& pool : pools) destroyPool(context.device, pool.handle, nullptr); for (const auto& pool : updateAfterBindPools) destroyPool(context.device, pool.handle, nullptr); for (const auto pool : dedicated) destroyPool(context.device, pool, nullptr); + for (const auto& set : kept) destroyPool(context.device, set.pool, nullptr); for (const auto& [key, layout] : layouts) destroyLayout(context.device, layout, nullptr); } @@ -2803,17 +3995,25 @@ 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; } namespace { -std::uint32_t SetDescriptorLimit(const VkPhysicalDeviceLimits& limits, VkDescriptorType type) { +std::uint32_t SetDescriptorLimit(const Context& context, VkDescriptorType type, bool updateAfterBind) { + const auto& limits = context.limits; + const auto& indexed = context.descriptorIndexingLimits; + const bool sampledUpdateAfterBind = updateAfterBind && context.sampledImageUpdateAfterBind; switch (type) { - case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER: return limits.maxDescriptorSetStorageBuffers; - case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE: return limits.maxDescriptorSetSampledImages; + case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER: return updateAfterBind ? indexed.maxDescriptorSetUpdateAfterBindStorageBuffers : limits.maxDescriptorSetStorageBuffers; + case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE: return sampledUpdateAfterBind ? indexed.maxDescriptorSetUpdateAfterBindSampledImages : limits.maxDescriptorSetSampledImages; case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE: return limits.maxDescriptorSetStorageImages; - case VK_DESCRIPTOR_TYPE_SAMPLER: return limits.maxDescriptorSetSamplers; + case VK_DESCRIPTOR_TYPE_SAMPLER: return sampledUpdateAfterBind ? indexed.maxDescriptorSetUpdateAfterBindSamplers : limits.maxDescriptorSetSamplers; default: throw std::runtime_error("AGC graphics: descriptor set uses an unsupported descriptor type " + std::to_string(type)); } } @@ -2824,21 +4024,37 @@ constexpr std::array ChainPoolSizes{{{VK_DESCRIPTOR_TYP constexpr std::array ChainPoolCreateSizes{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4096 + 2}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1024}, {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1024}, {VK_DESCRIPTOR_TYPE_SAMPLER, 512}}}; } -DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout layout, std::span sizes, bool updateAfterBind) { +DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout layout, std::span sizes, bool updateAfterBind, std::optional variableCount) { bool fits = true; for (const auto& size : sizes) { - const auto limit = updateAfterBind && size.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers : SetDescriptorLimit(context.limits, size.type); + const auto limit = SetDescriptorLimit(context, size.type, updateAfterBind); Require(size.descriptorCount <= limit, "descriptor set exceeds the device's per-set descriptor limit"); const auto capacity = std::find_if(ChainPoolSizes.begin(), ChainPoolSizes.end(), [&](const auto& item) { return item.type == size.type; }); if (capacity == ChainPoolSizes.end() || size.descriptorCount > capacity->descriptorCount) fits = false; } std::lock_guard lock(mutex); const auto allocate = context.Resolved(&DeviceFunctions::allocateDescriptorSets, "vkAllocateDescriptorSets"); + const std::uint32_t variable = variableCount.value_or(0u); + VkDescriptorSetVariableDescriptorCountAllocateInfoEXT variableInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_ALLOCATE_INFO_EXT}; + variableInfo.descriptorSetCount = 1; + variableInfo.pDescriptorCounts = &variable; VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.pNext = variableCount.has_value() ? &variableInfo : nullptr; allocation.descriptorSetCount = 1; allocation.pSetLayouts = &layout; if (!fits) { dedicated.reserve(dedicated.size() + 1); + const auto same = std::find_if(kept.begin(), kept.end(), [&](const SetAllocation& set) { + return set.layout == layout && set.updateAfterBind == updateAfterBind && set.variableCount == variableCount && std::equal(set.sizes.begin(), set.sizes.end(), sizes.begin(), sizes.end(), [](const VkDescriptorPoolSize& a, const VkDescriptorPoolSize& b) { return a.type == b.type && a.descriptorCount == b.descriptorCount; }); + }); + if (same != kept.end()) { + auto reused = std::move(*same); + kept.erase(same); + dedicated.push_back(reused.pool); + ++stats.reusedSets; + ++stats.sets; + return reused; + } const auto pool = CreateSetPool(context, sizes, updateAfterBind); allocation.descriptorPool = pool; VkDescriptorSet set = VK_NULL_HANDLE; @@ -2850,7 +4066,7 @@ DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout l dedicated.push_back(pool); ++stats.dedicatedPools; ++stats.sets; - return {set, pool}; + return {set, pool, 0, updateAfterBind, layout, std::vector(sizes.begin(), sizes.end()), variableCount, {}}; } auto& chain = updateAfterBind ? updateAfterBindPools : pools; for (std::size_t index = chain.size(); index != 0; --index) { @@ -2862,16 +4078,18 @@ DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout l if (result == VK_SUCCESS) { ++stats.sets; ++pool.liveSets; - return {set, pool.handle, index - 1, updateAfterBind}; + return {set, pool.handle, index - 1, updateAfterBind, VK_NULL_HANDLE, {}, variableCount, {}}; } if (result != VK_ERROR_OUT_OF_POOL_MEMORY && result != VK_ERROR_FRAGMENTED_POOL) Check(result, "vkAllocateDescriptorSets"); pool.exhausted = true; } 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}); @@ -2881,21 +4099,60 @@ DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout l Check(allocate(context.device, &allocation, &set), "vkAllocateDescriptorSets"); chain.back().liveSets = 1; ++stats.sets; - return {set, pool, chain.size() - 1, updateAfterBind}; + return {set, pool, chain.size() - 1, updateAfterBind, VK_NULL_HANDLE, {}, variableCount, {}}; } -void DescriptorCache::Free(const SetAllocation& allocation) noexcept { +DescriptorCache::SetAllocation DescriptorCache::AllocateRewritten(VkDescriptorSetLayout layout, std::span sizes, bool updateAfterBind, std::optional variableCount) { + { + std::lock_guard lock(mutex); + if (const auto found = recycled.find({layout, variableCount.value_or(0u)}); found != recycled.end() && !found->second.empty()) { + const auto allocation = found->second.back(); + found->second.pop_back(); + return allocation; + } + } + return Allocate(layout, sizes, updateAfterBind, variableCount); +} + +void DescriptorCache::Recycle(VkDescriptorSetLayout layout, const SetAllocation& allocation) noexcept { + if (allocation.set == VK_NULL_HANDLE || allocation.pool == VK_NULL_HANDLE) return; + { + std::lock_guard lock(mutex); + try { + auto& list = recycled[{layout, allocation.variableCount.value_or(0u)}]; + if (list.size() < 4096) { + list.push_back(allocation); + return; + } + } catch (...) { + } + } + Free(allocation); +} + +void DescriptorCache::Free(const SetAllocation& allocation, std::shared_ptr table) noexcept { if (allocation.set == VK_NULL_HANDLE || allocation.pool == VK_NULL_HANDLE) return; std::lock_guard lock(mutex); if (const auto found = std::find(dedicated.begin(), dedicated.end(), allocation.pool); found != dedicated.end()) { dedicated.erase(found); - destroyPool(context.device, allocation.pool, nullptr); + if (KeptSets() == 0u || allocation.layout == VK_NULL_HANDLE) { + destroyPool(context.device, allocation.pool, nullptr); + return; + } + if (kept.size() >= KeptSets()) { + destroyPool(context.device, kept.back().pool, nullptr); + kept.pop_back(); + } + kept.insert(kept.begin(), allocation); + kept.front().table = std::move(table); + ++stats.keptSets; return; } auto& chain = allocation.updateAfterBind ? updateAfterBindPools : pools; 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; @@ -2906,13 +4163,34 @@ DescriptorCache::Stats DescriptorCache::Counters() const { return stats; } -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) { +std::uint64_t ShaderResources::guestBufferBytes(std::span words) const { + const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1] & ~0x40000000u, 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, std::uint32_t extent) { 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]}}; + const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1] & ~0x40000000u, 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; @@ -2920,21 +4198,14 @@ std::size_t ShaderResources::addGuestBuffer(std::span words Require(byteSize <= context.limits.maxStorageBufferRange, "shader buffer exceeds descriptor range limit"); Require(byteSize <= std::numeric_limits::max(), "shader buffer size exceeds host address space"); 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); - } - } + if (const std::uint64_t stride = descriptor.Stride(); !written && target != nullptr && stride != 0 && address < target->address && address + size > target->address && address + size - target->address < stride) size = static_cast(target->address - address); Require(target == nullptr || !overlap(address, size, target->address, target->bytes), "shader buffer aliases the render target"); // APS5_ALL_BUFFERS_WRITTEN=1: every element is noted as written, as before bufferWritten existed. static const bool allWritten = std::getenv("APS5_ALL_BUFFERS_WRITTEN") != nullptr; Require(!written || !overlap(address, size, indexAddress, indexBytes), "writable shader buffer aliases the index buffer"); written = written || allWritten; - const bool swept = read && dispatchThreads != 0 && descriptor.Stride() != 0 && dispatchThreads * descriptor.Stride() * 2u >= byteSize; + const std::uint64_t recordBytes = descriptor.Stride() != 0 ? descriptor.Stride() : extent; + const bool swept = read && dispatchThreads != 0 && recordBytes != 0 && dispatchThreads * recordBytes * 2u >= byteSize; if (written) guestMemory.AddWritable(address, size, atomic, swept); else { guestMemory.AddReadable(address, size); @@ -3012,7 +4283,7 @@ std::size_t ShaderResources::addDataBuffer(std::span words) const auto size = words.size() * sizeof(std::uint32_t); Require(size <= context.limits.maxStorageBufferRange, "shader data buffer exceeds descriptor range limit"); const bool refreshable = TemplateDataRefresh() && size <= MaxRefreshBytes; - auto buffer = std::make_unique(context, size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | (refreshable ? VK_BUFFER_USAGE_TRANSFER_DST_BIT : 0u)); + auto buffer = std::make_unique(context, size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); std::memcpy(buffer->Bytes().data(), words.data(), size); Allocation allocation{0, size, false, std::move(buffer)}; if (refreshable) allocation.dataWords.assign(words.begin(), words.end()); @@ -3026,7 +4297,7 @@ void ShaderResources::rehashDataWords() { // Data buffers are the non-guest allocations with a buffer (an address-role allocation has // none), appended in binding order by buildPrepare: the order DataWordsHash(shader) hashes. for (const auto& allocation : allocations) { - if (!allocation.guest && allocation.buffer != nullptr) mixDataWords(dataWordsHash, allocation.dataWords); + if (!allocation.guest && allocation.buffer != nullptr && allocation.role != ShaderRecompiler::DescriptorRole::ImageTableMap) mixDataWords(dataWordsHash, allocation.dataWords); } } @@ -3088,6 +4359,31 @@ bool ShaderResources::refreshData(VkCommandBuffer commands, const CompiledShader return recorded; } +std::optional ShaderResources::shaderDataAllocation(const CompiledShader& shader) const { + Require(shader.program != nullptr, "missing compiled shader"); + const auto& program = *shader.program; + if (program.bindings.size() != bindings.size()) return std::nullopt; + for (std::size_t index = 0; index < program.bindings.size(); ++index) { + if (program.bindings[index].role != ShaderRecompiler::DescriptorRole::ShaderData || bindings[index].allocations.size() != 1) continue; + const auto allocation = bindings[index].allocations.front(); + if (allocations[allocation].guest || allocations[allocation].buffer == nullptr) return std::nullopt; + return allocation; + } + return std::nullopt; +} + +VkBuffer ShaderResources::shaderDataBuffer(const CompiledShader& shader) const { + const auto allocation = shaderDataAllocation(shader); + return allocation.has_value() ? allocations[*allocation].buffer->Handle() : VK_NULL_HANDLE; +} + +void ShaderResources::forgetShaderDataWords(const CompiledShader& shader) { + if (const auto allocation = shaderDataAllocation(shader)) { + allocations[*allocation].dataWords.clear(); + rehashDataWords(); + } +} + void ShaderResources::writeDataWords(VkCommandBuffer commands, std::size_t allocation, std::span words) const { const auto& buffer = *allocations[allocation].buffer; const auto size = words.size() * sizeof(std::uint32_t); @@ -3147,11 +4443,18 @@ 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) { - const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); + auto guestWords = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); + std::array normalizedWords{}; + if (element < binding.samplerCoordinatesIgnored.size() && binding.samplerCoordinatesIgnored[element]) { + std::copy(guestWords.begin(), guestWords.end(), normalizedWords.begin()); + normalizedWords[0] &= ~(1u << 15u); + guestWords = normalizedWords; + } + const auto words = guestWords; const bool compareEnable = binding.samplerDepthCompare.at(element); const bool unnormalized = element < binding.samplerUnnormalized.size() && binding.samplerUnnormalized[element]; const bool integerBorder = element < binding.samplerIntegerBorder.size() && binding.samplerIntegerBorder[element]; @@ -3212,8 +4515,10 @@ bool ShaderResources::precollectImages() { const auto& binding = *deferred.binding; const auto elementWords = binding.guestDescriptor.size() / binding.count; for (std::uint32_t element = 0; element < binding.count; ++element) { - const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); const bool sampled = binding.kind == ShaderRecompiler::DescriptorKind::SampledImage; + const auto raw = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); + std::array narrowed{}; + const auto words = firstLayerWords(raw, sampled ? binding.imageShape : std::nullopt, narrowed, false); if (sampled && !imageRecords.empty()) { const auto& previous = imageRecords.back(); if (previous.sampled && previous.decoded && words.size() == 8 && std::equal(words.begin(), words.end(), previous.words.begin()) && (binding.imageDepthCompare.empty() || !binding.imageDepthCompare.at(element))) { @@ -3230,7 +4535,7 @@ bool ShaderResources::precollectImages() { } ImageRecord record; record.sampled = sampled; - if (words[0] != 0u || (words[1] & 0xffu) != 0u) { + if (!IsNullTextureResource(words)) { const auto* memo = words.size() == 8 ? &decodedImage(context.device, words) : nullptr; record.resource = memo != nullptr ? memo->resource : DecodeTextureResource(words); record.guestBytes = memo != nullptr ? memo->guestBytes : DescribeSurface(record.resource).guestBytes; @@ -3277,7 +4582,8 @@ std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) // between the stages marks them uncompressed). const auto address = record.resource.baseAddress; const auto bytes = static_cast(record.guestBytes); - if (DepthSurfaceAt(address)) return nullptr; + RequireNoMultisampleColorTarget(address, bytes != 0 ? bytes : 1u, "a texture read"); + if (DepthSurfaceAt(address) || !FlushDepthPlanes(address, bytes).empty()) return nullptr; if (GuestMemory::CollectWrites(address, bytes) == 0) return nullptr; if (PendingStorageOverlaps(address, bytes, record.source.get())) return nullptr; auto keys = record.keys; @@ -3292,7 +4598,7 @@ std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) // keys and stays valid while they are unchanged); a change is the full lookup's to judge. if (keys != record.entryKeys) return nullptr; if (record.source != nullptr) { - if (!StorageImageCached(context, record.source.get()) || !GuestMemory::UnchangedSince(address, bytes, record.source->Generation())) return nullptr; + if (!StorageImageCached(context, record.source.get()) || !GuestMemory::UnchangedSince(address, bytes, record.source->GenerationFor(address, bytes))) return nullptr; if (record.resource.dccAddress != 0 && IsDccClear(record.source->FilledKeys())) return nullptr; // Under fast-clear keys the view holds only while its image's results are still pending over // the surface; flushed, the clear the image cannot see makes the lookup take a snapshot. @@ -3337,9 +4643,11 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind Require(binding.imageSamplers.size() == binding.count, "guest sampled image binding is missing its image-sampler pairs"); const auto elementWords = binding.guestDescriptor.size() / binding.count; for (std::uint32_t element = 0; element < binding.count; ++element) { - const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); + const auto raw = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); + std::array narrowed{}; + const auto words = firstLayerWords(raw, binding.imageShape, narrowed, true); const auto* record = nextRecord(); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) { + if (IsNullTextureResource(words)) { item.imageAllocations.push_back(std::numeric_limits::max()); --plannedSampledImages; continue; @@ -3347,20 +4655,23 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words); const bool forcedSrgb = ForcesDegammaOn(resource.format, binding.imageSamplers[element], shaderSamplers); RequireNonSeamlessCube(resource.dimension == TextureDimension::kCube, binding.imageSamplers[element], shaderSamplers); - const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray; - if (!firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); + const bool depthCompare = !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element); + const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && (resource.dimension == TextureDimension::k2DArray || (depthCompare && resource.dimension == TextureDimension::k1DArray)); + const bool depth1D = depthCompare && ((binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k1D) || (binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2DArray && resource.dimension == TextureDimension::k1DArray)); + if (!firstLayer && !depth1D && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); const VkComponentMapping components = ViewComponents(resource); const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes; CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); std::shared_ptr texture; - const bool depthCompare = !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element); - if (previousSampled.texture != nullptr && previousSampled.depthCompare == depthCompare && words.size() == 8 && std::equal(words.begin(), words.end(), previousSampled.words.begin())) texture = previousSampled.texture; + const VkImageViewType multisampleViewType = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2DMultisampleArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; + if (previousSampled.texture != nullptr && previousSampled.depthCompare == depthCompare && previousSampled.multisampleViewType == multisampleViewType && words.size() == 8 && std::equal(words.begin(), words.end(), previousSampled.words.begin())) texture = previousSampled.texture; if (texture == nullptr && record != nullptr && record->texture != nullptr) { texture = fastTexture(*record); (texture != nullptr ? counters.fastHits : counters.fastMisses).fetch_add(1, std::memory_order_relaxed); } - if (texture == nullptr) texture = cachedTexture(context, words, resource, components, guestBytes, depthCompare); + if (texture == nullptr) texture = cachedTexture(context, words, resource, components, guestBytes, depthCompare, multisampleViewType); + if (resource.dimension == TextureDimension::k2DMsaa) multisampleSources.push_back(resource.baseAddress); bool baseLevel = false; if (element < binding.imageUnnormalized.size() && binding.imageUnnormalized[element]) { const auto range = texture->SampledViewRange(firstLayer); @@ -3385,6 +4696,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind if (words.size() == 8) { std::copy(words.begin(), words.end(), previousSampled.words.begin()); previousSampled.depthCompare = depthCompare; + previousSampled.multisampleViewType = multisampleViewType; previousSampled.texture = texture; } textures.push_back(std::move(texture)); @@ -3408,7 +4720,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind if (sameAsPrevious) ++mipOffset; else mipOffset = 0; const auto* record = nextRecord(); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) { + if (IsNullTextureResource(words)) { item.imageAllocations.push_back(std::numeric_limits::max()); --plannedStorageImages; continue; @@ -3416,19 +4728,35 @@ 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; CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); + // Images the shader only reads have nothing to store back. + const bool written = element >= binding.imageWritten.size() || binding.imageWritten[element]; // The same surface as the previous element: its image was just looked up and refreshed. if (sameAsPrevious && StorageDedupeEnabled()) storageTextures.push_back(storageTextures.back()); - else storageTextures.push_back(cachedStorageTexture(context, words, resource, mip, guestBytes)); + else storageTextures.push_back(cachedStorageTexture(context, words, resource, mip, guestBytes, written)); storageMips.push_back(mip); storageKeys.push_back(resource.dccAddress); storageFirstLayer.push_back(firstLayer); - // Images the shader only reads have nothing to store back. - storageWritten.push_back(element >= binding.imageWritten.size() || binding.imageWritten[element]); + storageWritten.push_back(written); storageAtomic.push_back(element < binding.imageAtomic.size() && binding.imageAtomic[element]); storageAtomic64.push_back(element < binding.imageAtomic64.size() && binding.imageAtomic64[element]); describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); @@ -3436,15 +4764,404 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind } } -std::vector> ShaderResources::PresyncSurfaces() const { - std::vector> surfaces; +std::uint32_t StorageTableCapacity(const Context& context) { + return std::min({context.imageTableCapacity, 4096u, context.limits.maxPerStageDescriptorStorageImages / 4u}); +} + +namespace { + +std::uint64_t Room(std::uint64_t limit, std::uint64_t reserve) { + return limit > reserve ? limit - reserve : 0u; +} + +std::uint32_t ImageTableCapacityFor(const Context& context, bool updateAfterBind) { + if (!context.runtimeDescriptorArray || !context.partiallyBound || !context.variableDescriptorCount || !context.descriptorIndexing) return 0u; + if (updateAfterBind && !context.sampledImageUpdateAfterBind) return 0u; + constexpr std::uint64_t sampledHeaps = static_cast(ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding - ShaderRecompiler::RuntimeAbi::FirstImageBinding) * ShaderRecompiler::RuntimeAbi::SampledHeapCapacity; + constexpr std::uint64_t storageHeaps = static_cast(static_cast(ShaderRecompiler::RuntimeAbi::Binding::Samplers) - ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding) * ShaderRecompiler::RuntimeAbi::StorageHeapCapacity; + constexpr std::uint64_t buffers = ShaderRecompiler::RuntimeAbi::BufferCapacity + 8u; + const auto& indexed = context.descriptorIndexingLimits; + const std::uint64_t stageImages = updateAfterBind ? indexed.maxPerStageDescriptorUpdateAfterBindSampledImages : context.limits.maxPerStageDescriptorSampledImages; + const std::uint64_t stageResources = updateAfterBind ? indexed.maxPerStageUpdateAfterBindResources : context.limits.maxPerStageResources; + const std::uint64_t setImages = updateAfterBind ? indexed.maxDescriptorSetUpdateAfterBindSampledImages : context.limits.maxDescriptorSetSampledImages; + std::uint64_t capacity = static_cast(ShaderRecompiler::RuntimeAbi::StageCount) * ShaderRecompiler::ImageTableAbi::ImageElementBudget; + capacity = std::min(capacity, Room(stageImages, sampledHeaps)); + capacity = std::min(capacity, Room(stageResources, sampledHeaps + storageHeaps + buffers)); + capacity = std::min(capacity, Room(setImages, sampledHeaps * ShaderRecompiler::RuntimeAbi::StageCount)); + return static_cast(capacity); +} + +std::uint32_t SamplerTableCapacityFor(const Context& context, bool updateAfterBind) { + if (!context.runtimeDescriptorArray || !context.partiallyBound) return 0u; + if (updateAfterBind && !context.sampledImageUpdateAfterBind) return 0u; + const auto& indexed = context.descriptorIndexingLimits; + const std::uint64_t stageSamplers = updateAfterBind ? indexed.maxPerStageDescriptorUpdateAfterBindSamplers : context.limits.maxPerStageDescriptorSamplers; + const std::uint64_t setSamplers = updateAfterBind ? indexed.maxDescriptorSetUpdateAfterBindSamplers : context.limits.maxDescriptorSetSamplers; + std::uint64_t capacity = ShaderRecompiler::ImageTableAbi::SamplerElementBudget; + capacity = std::min(capacity, Room(stageSamplers, ShaderRecompiler::RuntimeAbi::SamplerHeapCapacity)); + capacity = std::min(capacity, Room(setSamplers / ShaderRecompiler::RuntimeAbi::StageCount, ShaderRecompiler::RuntimeAbi::SamplerHeapCapacity)); + return static_cast(capacity - capacity % ShaderRecompiler::ImageTableAbi::SamplerEntryElements); +} + +} + +std::uint32_t ImageTableCapacity(const Context& context) { + const auto capacity = ImageTableCapacityFor(context, false); + return capacity != 0u ? capacity : ImageTableCapacityFor(context, true); +} + +std::uint32_t SamplerTableCapacity(const Context& context) { + const auto capacity = SamplerTableCapacityFor(context, false); + return capacity != 0u ? capacity : SamplerTableCapacityFor(context, true); +} + +bool ImageTableUpdateAfterBind(const Context& context) { + return ImageTableCapacityFor(context, false) == 0u && ImageTableCapacityFor(context, true) != 0u; +} + +bool SamplerTableUpdateAfterBind(const Context& context) { + return SamplerTableCapacityFor(context, false) == 0u && SamplerTableCapacityFor(context, true) != 0u; +} + +ShaderResources::TableStage& ShaderResources::tableStage(const CompiledShader& shader) { + for (auto& table : tableStages) { + if (table.program == shader.program) return table; + } + auto& table = tableStages.emplace_back(); + table.stage = shader.stage; + table.program = shader.program; + table.poison = shader.program->imageTablePoison; + table.messages.assign(table.poison.size(), std::string{}); + return table; +} + +std::uint32_t ShaderResources::rejectTableElement(TableStage& table, std::span words, const std::string& message) { + ShaderRecompiler::ImageTableEntryPoison poison{}; + std::copy(words.begin(), words.end(), poison.words.begin()); + poison.dwordCount = static_cast(words.size()); + poison.resource = ShaderRecompiler::NoTable; + poison.reason = static_cast(ShaderRecompiler::ImageTableAbi::PoisonReason::DriverRejected); + table.poison.push_back(poison); + table.messages.push_back(message); + faultChecks = true; + return ShaderRecompiler::ImageTableAbi::PoisonCode(static_cast(table.poison.size() - 1u)); +} + +void ShaderResources::addSamplerTable(const ShaderRecompiler::DescriptorBinding& binding, VkShaderStageFlags flags, TableStage& table) { + namespace Abi = ShaderRecompiler::ImageTableAbi; + Require(binding.kind == ShaderRecompiler::DescriptorKind::Sampler && binding.guestDescriptor.size() == static_cast(binding.count) * 4u && binding.samplerDepthCompare.size() == binding.count && binding.count % Abi::SamplerEntryElements == 0u, "sampler table binding is malformed"); + Require(context.samplerTableCapacity != 0, "sampler tables need runtimeDescriptorArray and descriptorBindingPartiallyBound, which the device lacks"); + const VkDescriptorBindingFlags samplerTableFlags = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | (context.samplerTableUpdateAfterBind ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); + Binding item{{binding.binding, VK_DESCRIPTOR_TYPE_SAMPLER, context.samplerTableCapacity, flags, nullptr}, {}, {}, samplerTableFlags}; + static const bool noSamplerCache = std::getenv("APS5_NO_SAMPLER_CACHE") != nullptr; + for (std::uint32_t entry = 0; entry < binding.count / Abi::SamplerEntryElements; ++entry) { + const auto first = entry * Abi::SamplerEntryElements; + const auto words = std::span(binding.guestDescriptor).subspan(static_cast(first) * 4u, 4u); + std::string rejected; + if (first + Abi::SamplerEntryElements > context.samplerTableCapacity) rejected = "the sampler table exceeds the device's capacity of " + std::to_string(context.samplerTableCapacity) + " samplers"; + for (std::uint32_t element = first; rejected.empty() && element < first + Abi::SamplerEntryElements; ++element) { + try { + const auto elementWords = std::span(binding.guestDescriptor.data() + static_cast(element) * 4u, 4u); + auto resource = DecodeSamplerResource(elementWords); + resource.compareEnable = binding.samplerDepthCompare.at(element); + Sampler::Validate(context, resource); + Require(!Sampler::ReadsOpaqueBlackBorder(resource), "a sampler table entry with an opaque black border needs dynamic texture BC-swizzle pairing, which is not implemented"); + Require(resource.addressModeU != VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER && resource.addressModeV != VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER && resource.addressModeW != VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, "a sampler table entry with a border needs dynamic texture numeric-class pairing, which is not implemented"); + Require(!resource.nonSeamlessCube || context.nonSeamlessCubeMap, "a sampler table entry that disables seamless cube filtering needs VK_EXT_non_seamless_cube_map because dynamic texture/sampler pairing is not implemented"); + Require(!SingleLevelSamplerWords(elementWords, true, false).has_value(), "a sampler table entry with anisotropic ANISO_OVERRIDE needs dynamic texture mip-level pairing, which is not implemented"); + } catch (const std::runtime_error& error) { + rejected = error.what(); + } + } + if (!rejected.empty()) { + table.samplerRejections.emplace_back(entry, rejectTableElement(table, words, rejected)); + item.imageAllocations.insert(item.imageAllocations.end(), Abi::SamplerEntryElements, std::numeric_limits::max()); + continue; + } + for (std::uint32_t element = first; element < first + Abi::SamplerEntryElements; ++element) { + const auto elementWords = std::span(binding.guestDescriptor).subspan(static_cast(element) * 4u, 4u); + const bool compareEnable = binding.samplerDepthCompare.at(element); + if (context.samplerCache != nullptr && !noSamplerCache) { + samplers.push_back(context.samplerCache->Get(context, elementWords, compareEnable, false)); + } else { + auto resource = DecodeSamplerResource(elementWords, false); + resource.compareEnable = compareEnable; + samplers.push_back(std::make_shared(context, resource)); + } + samplerSources.push_back({{elementWords[0], elementWords[1], elementWords[2], elementWords[3]}, compareEnable, false}); + item.imageAllocations.push_back(samplers.size() - 1u); + } + } + const auto setSamplers = context.samplerTableUpdateAfterBind ? context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindSamplers : context.limits.maxDescriptorSetSamplers; + Require(samplers.size() <= setSamplers, "pipeline sampler descriptors exceed device limits"); + bindings.push_back(std::move(item)); +} + +void ShaderResources::resolveImageTables() { + 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 = 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); + table.elementTextures.assign(binding.count, std::numeric_limits::max()); + const auto capacityMessage = [&] { return "the image table exceeds the device's capacity of " + std::to_string(context.imageTableCapacity) + " images"; }; + std::vector samplerKey; + for (std::uint32_t sampler = 0; sampler < table.samplerCount; ++sampler) { + const auto& source = samplerSources.at(table.firstSampler + sampler); + samplerKey.insert(samplerKey.end(), source.words.begin(), source.words.end()); + samplerKey.push_back((source.compareEnable ? 1u : 0u) | (source.unnormalized ? 2u : 0u) | (source.integerBorder ? 4u : 0u)); + } + const auto place = [&](std::uint32_t element, std::shared_ptr texture, const GuestTextureResource& resource, std::uint64_t guestBytes, bool firstLayer) { + textures.push_back(std::move(texture)); + textureFirstLayer.push_back(firstLayer); + describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress, true}); + table.elementTextures[element] = textures.size() - 1u; + item->imageAllocations[static_cast(table.elementBase) + element] = textures.size() - 1u; + }; + const auto bind = [&](std::uint32_t element, std::span words, std::shared_ptr texture, const GuestTextureResource& resource, std::uint64_t guestBytes, bool firstLayer) -> std::string { + try { + RequireFilterMinmax(context, texture->ViewFormat(), binding.imageSamplers.at(element), stageSamplers); + RequireBorderSwizzle(resource.bcSwizzle, binding.imageSamplers.at(element), stageSamplers); + RequireNonSeamlessCube(resource.dimension == TextureDimension::kCube, binding.imageSamplers.at(element), stageSamplers); + const auto numeric = ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) != 0u ? ShaderRecompiler::IrTextureNumericClass::Float : ShaderRecompiler::SampledTextureNumericClass(static_cast(resource.format)); + const bool integerImage = numeric == ShaderRecompiler::IrTextureNumericClass::Uint || numeric == ShaderRecompiler::IrTextureNumericClass::Sint; + for (std::uint32_t sampler = 0; sampler < stageSamplers.size() && sampler < 32u; ++sampler) { + if (((binding.imageSamplers.at(element) >> sampler) & 1u) == 0) continue; + const auto& source = samplerSources.at(table.firstSampler + sampler); + const auto descriptor = DecodeSamplerResource(source.words, source.unnormalized, true, source.integerBorder); + const bool border = descriptor.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER || descriptor.addressModeV == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER || descriptor.addressModeW == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; + Require(!border || source.integerBorder == integerImage, "an image table entry's sampler border color numeric class disagrees with its sampled image class, which needs dynamic pairing and is not implemented"); + Require(!SingleLevelSamplerWords(source.words, true, false).has_value(), "an image table entry paired with anisotropic ANISO_OVERRIDE needs dynamic texture mip-level pairing, which is not implemented"); + } + } catch (const std::runtime_error& error) { + table.map[faults + element] = rejectTableElement(table, words, error.what()); + return error.what(); + } + place(element, std::move(texture), resource, guestBytes, firstLayer); + return {}; + }; + const auto state = CurrentTableState(); + std::vector> cached; + const bool storageTable = std::find(binding.imageWritten.begin(), binding.imageWritten.end(), true) != binding.imageWritten.end(); + if (auto* reused = storageTable || item == nullptr ? nullptr : FindReusedTable(context.device, binding, state); reused != nullptr && TableCurrent(*reused) && CachedTableTextures(*reused, cached)) { + const bool replay = TableReplay() && reused->replayable && reused->imageSamplers == binding.imageSamplers && reused->samplers == samplerKey; + if (!replay) reused->bound.assign(binding.count, {}); + 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); + const auto& entry = reused->elements[element]; + if (static_cast(table.elementBase) + element >= context.imageTableCapacity) { + if (!entry.rejected) ++next; + table.map[faults + element] = rejectTableElement(table, words, capacityMessage()); + } else if (entry.rejected) { + table.map[faults + element] = rejectTableElement(table, words, entry.message); + } else { + CompletePendingCpuWrites(context, entry.resource.baseAddress, static_cast(entry.guestBytes)); + if (!replay) { + reused->bound[element] = bind(element, words, std::move(*next++), entry.resource, entry.guestBytes, entry.firstLayer); + } else if (!reused->bound[element].empty()) { + ++next; + table.map[faults + element] = rejectTableElement(table, words, reused->bound[element]); + } else { + place(element, std::move(*next++), entry.resource, entry.guestBytes, entry.firstLayer); + } + } + } + if (!replay) { + reused->imageSamplers = binding.imageSamplers; + reused->samplers = samplerKey; + reused->replayable = static_cast(table.elementBase) + binding.count <= context.imageTableCapacity; + } + (replay ? counters.tablesReplayed : counters.tablesReused).fetch_add(1, std::memory_order_relaxed); + continue; + } + ReusedTable fresh; + fresh.bound.assign(binding.count, {}); + bool reusable = state.epoch != 0; + for (std::uint32_t element = 0; element < binding.count; ++element) { + const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8u, 8u); + const auto absolute = static_cast(table.elementBase) + element; + if (absolute >= context.imageTableCapacity) { + table.map[faults + element] = rejectTableElement(table, words, capacityMessage()); + reusable = false; + continue; + } + GuestTextureResource resource{}; + std::uint64_t guestBytes = 0; + bool firstLayer = false; + auto shape = ShaderRecompiler::DescriptorImageShape::Image2D; + try { + resource = DecodeTextureResource(words); + shape = binding.imageShapes.at(element); + const bool depthCompare = element < binding.imageDepthCompare.size() && binding.imageDepthCompare[element]; + firstLayer = shape == ShaderRecompiler::DescriptorImageShape::Image2D && (resource.dimension == TextureDimension::k2DArray || (depthCompare && resource.dimension == TextureDimension::k1DArray)); + const bool depth1D = depthCompare && ((shape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k1D) || (shape == ShaderRecompiler::DescriptorImageShape::Image2DArray && resource.dimension == TextureDimension::k1DArray)); + if (!firstLayer && !depth1D && !MatchesGuestDimension(shape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the table entry's image shape (shape " + std::to_string(static_cast(shape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); + guestBytes = DescribeSurface(resource).guestBytes; + if (!GuestMemory::Accessible(reinterpret_cast(resource.baseAddress), static_cast(guestBytes)) && GuestMemory::CommittedRanges(resource.baseAddress, static_cast(guestBytes), false).empty()) throw std::runtime_error("AGC graphics: the table entry's surface at 0x" + HexText(resource.baseAddress) + " (0x" + HexText(guestBytes) + " bytes) is not mapped"); + static_cast(ResolveTextureFormat(resource.format)); + } catch (const std::runtime_error& error) { + table.map[faults + element] = rejectTableElement(table, words, error.what()); + if (reusable) fresh.elements.push_back({{}, {}, {}, 0, false, true, error.what()}); + 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, true}); + 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; + } + std::array narrowed{}; + const auto sampledWords = firstLayerWords(words, binding.imageShapes.at(element), narrowed, true); + if (sampledWords.data() != words.data()) { + resource = DecodeTextureResource(sampledWords); + guestBytes = DescribeSurface(resource).guestBytes; + } + CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); + if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); + std::shared_ptr texture; + const auto components = ViewComponents(resource); + const bool depthCompare = binding.imageDepthCompare.at(element); + const VkImageViewType multisampleViewType = shape == ShaderRecompiler::DescriptorImageShape::Image2DMultisampleArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; + try { + texture = cachedTexture(context, sampledWords, resource, components, guestBytes, depthCompare, multisampleViewType); + } catch (const std::runtime_error& error) { + table.map[faults + element] = rejectTableElement(table, words, error.what()); + if (reusable) fresh.elements.push_back({{}, {}, {}, 0, false, true, error.what()}); + continue; + } + if (reusable) { + const auto& log = lookupLog(); + const auto* looked = log.empty() ? nullptr : &log.back(); + reusable = looked != nullptr && looked->object == texture.get() && looked->source == nullptr && looked->generation != 0 && looked->keys == DccKeys::Uncompressed && resource.dccAddress == 0; + if (reusable) { + fresh.elements.push_back({MakeTextureKey(context.device, sampledWords, components, depthCompare), texture, resource, guestBytes, firstLayer, false, {}}); + fresh.queries.push_back({resource.baseAddress, static_cast(guestBytes), looked->generation}); + fresh.ranges.emplace_back(resource.baseAddress, resource.baseAddress + guestBytes); + } + } + fresh.bound[element] = bind(element, sampledWords, std::move(texture), resource, guestBytes, firstLayer); + } + counters.tablesResolved.fetch_add(1, std::memory_order_relaxed); + if (reusable && CurrentTableState() == state) { + const auto serial = StorageTexture::PendingSerial(); + if (!StorageTexture::AnyPendingOverlaps(fresh.ranges) && StorageTexture::PendingSerial() == serial) { + fresh.device = context.device; + fresh.state = state; + fresh.pendingSerial = serial; + fresh.imageSamplers = binding.imageSamplers; + fresh.samplers = std::move(samplerKey); + fresh.replayable = static_cast(table.elementBase) + binding.count <= context.imageTableCapacity; + RememberTable(std::move(fresh), binding); + } + } + } + reportTextureCounters(); +} + +ImageTableReuseCounts ImageTableReuses() { + const auto& counters = TextureCounts(); + return {counters.tablesReused.load(std::memory_order_relaxed) + counters.tablesReplayed.load(std::memory_order_relaxed), counters.tablesResolved.load(std::memory_order_relaxed), counters.tablesReplayed.load(std::memory_order_relaxed), counters.tableWritesKept.load(std::memory_order_relaxed), counters.tableWritesMade.load(std::memory_order_relaxed)}; +} + +void ShaderResources::finishImageTables() { + namespace Abi = ShaderRecompiler::ImageTableAbi; + if (tableStages.empty()) return; + const auto overlaps = [](std::uint64_t base, std::uint64_t size, std::uint64_t address, std::uint64_t bytes) { return size != 0 && bytes != 0 && (base <= address ? address - base < size : base - address < bytes); }; + std::vector faults; + std::vector> ranges; + for (auto& table : tableStages) { + auto& map = table.map; + for (const auto& [entry, code] : table.samplerRejections) map[map[Abi::HeaderSamplerFaults] + entry] = code; + for (std::uint32_t index = 0; index < map[Abi::HeaderTableCount]; ++index) { + const auto header = Abi::TableHeader(index); + if (map[header + Abi::TableFault] != 0u) continue; + const auto base = static_cast(map[header + Abi::TableBaseLow]) | (static_cast(map[header + Abi::TableBaseHigh]) << 32u); + const auto size = static_cast(map[header + Abi::TableSizeLow]) | (static_cast(map[header + Abi::TableSizeHigh]) << 32u); + const auto tableOverlaps = [&](std::uint64_t address, std::uint64_t bytes) { + if (overlaps(base, size, address, bytes)) return true; + if (index >= table.program->imageTableReadRanges.size()) return false; + const auto& reads = table.program->imageTableReadRanges[index]; + return std::any_of(reads.begin(), reads.end(), [&](const auto& range) { return overlaps(range.first, range.second, address, bytes); }); + }; + bool written = false; + for (const auto& allocation : allocations) written = written || (allocation.guest && allocation.written && tableOverlaps(allocation.address, allocation.size)); + for (std::size_t image = 0; image < storageTextures.size(); ++image) written = written || (storageWritten[image] && storageTextures[image] != nullptr && tableOverlaps(storageTextures[image]->Descriptor().baseAddress, storageTextures[image]->GuestBytes())); + if (!written) continue; + ShaderRecompiler::ImageTableEntryPoison poison{}; + poison.resource = index; + poison.reason = static_cast(Abi::PoisonReason::HeapWritten); + table.poison.push_back(poison); + table.messages.push_back("the table at 0x" + HexText(base) + " is written by its own dispatch"); + map[header + Abi::TableFault] = Abi::PoisonCode(static_cast(table.poison.size() - 1u)); + faultChecks = true; + } + map[Abi::HeaderPoisonCount] = static_cast(table.poison.size()); + auto& allocation = allocations.at(table.mapAllocation); + Require(allocation.buffer != nullptr && allocation.size == map.size() * sizeof(std::uint32_t), "image table map buffer disagrees with the map"); + std::memcpy(allocation.buffer->Bytes().data(), map.data(), allocation.size); + faults.push_back({IrStage(table.stage), table.program->imageTableShader, table.poison, table.messages}); + ranges.insert(ranges.end(), table.program->imageTableRanges.begin(), table.program->imageTableRanges.end()); + } + if (bda == nullptr) return; + bda->SetImageTables(std::move(faults), std::move(ranges)); +} + +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 @@ -3460,28 +5177,46 @@ 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; } // Stage A has not run (an address-based build, or the pass is off): decoded from the shader. if (deferredCompute.program == nullptr) return surfaces; forEachImageElement(*deferredCompute.program, [&](const ShaderRecompiler::DescriptorBinding& binding, std::uint32_t, std::span words) { - if (words[0] == 0u && (words[1] & 0xffu) == 0u) return; + 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)); + const auto guestBytes = DescribeSurface(resource).guestBytes; + consider(resource.format, resource.baseAddress, guestBytes, true); + considerKeys(resource.dccAddress, guestBytes); + } catch (const std::runtime_error&) { + } + } + } return surfaces; } @@ -3489,12 +5224,36 @@ ShaderResources::~ShaderResources() { release(); } +std::shared_ptr ShaderResources::keptTable(std::size_t set, std::shared_ptr>>& held) noexcept { + const auto& table = tableWrites[set]; + if (!table.written || KeptSets() == 0u || table.firstImage + table.images > descriptorImages.size()) return nullptr; + try { + if (held == nullptr) { + std::vector> weak; + weak.reserve(textures.size()); + for (const auto& texture : textures) { + if (texture != nullptr) weak.push_back(texture); + } + held = std::make_shared>>(std::move(weak)); + } + auto kept = std::make_shared(); + kept->binding = table.binding; + kept->runs = table.runs; + kept->images.assign(descriptorImages.begin() + static_cast(table.firstImage), descriptorImages.begin() + static_cast(table.firstImage + table.images)); + kept->textures = held; + return kept; + } catch (...) { + return nullptr; + } +} + void ShaderResources::release() noexcept { // A set from the cache's pool chain goes back to it; a dedicated pool dies with its set. - if (cacheAllocation.pool && context.descriptorCache != nullptr) context.descriptorCache->Free(cacheAllocation); + std::shared_ptr>> held; + if (cacheAllocation.pool && context.descriptorCache != nullptr) context.descriptorCache->Free(cacheAllocation, keptTable(0, held)); if (pool) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); if (_layout && ownsLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, _layout, nullptr); - if (fragment.cacheAllocation.pool && context.descriptorCache != nullptr) context.descriptorCache->Free(fragment.cacheAllocation); + if (fragment.cacheAllocation.pool && context.descriptorCache != nullptr) context.descriptorCache->Free(fragment.cacheAllocation, keptTable(1, held)); if (fragment.pool) context.Function("vkDestroyDescriptorPool")(context.device, fragment.pool, nullptr); if (fragment.layout && fragment.ownsLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, fragment.layout, nullptr); cacheAllocation = {}; @@ -3502,10 +5261,12 @@ void ShaderResources::release() noexcept { _set = VK_NULL_HANDLE; _layout = VK_NULL_HANDLE; fragment = {}; + tableWrites = {}; } std::uint32_t ShaderResources::setOf(const Binding& binding) const { constexpr auto count = static_cast(ShaderRecompiler::RuntimeAbi::Binding::Count); + if (binding.set >= 0) return static_cast(binding.set); return splitSets && binding.layout.binding / count == static_cast(ShaderRecompiler::RuntimeAbi::Stage::Fragment) ? 1u : 0u; } @@ -3524,8 +5285,14 @@ bool ShaderResources::LayoutUpdateAfterBind() const { } ShaderResources::DrawBindings::~DrawBindings() { - if (cache != nullptr && allocation.set != VK_NULL_HANDLE) cache->Free(allocation); - if (cache != nullptr && fragmentAllocation.set != VK_NULL_HANDLE) cache->Free(fragmentAllocation); + if (cache == nullptr) return; + const auto release = [&](VkDescriptorSetLayout setLayout, const DescriptorCache::SetAllocation& set) { + if (set.set == VK_NULL_HANDLE) return; + if (setLayout != VK_NULL_HANDLE) cache->Recycle(setLayout, set); + else cache->Free(set); + }; + release(layout, allocation); + release(fragmentLayout, fragmentAllocation); } std::optional> ShaderResources::MovedReadOnlyBuffers(std::span shaders, Recorder& recorder) const { @@ -3544,6 +5311,7 @@ std::optional> ShaderResources::MovedR const bool same = !item.dataWords.empty() ? item.dataWords == binding.guestDescriptor : !patched && std::memcmp(item.buffer->Bytes().data(), binding.guestDescriptor.data(), item.size) == 0; if (same) continue; if (item.dataWords.empty() && patched) return std::nullopt; + if (moved.empty()) moved.reserve(allocations.size()); moved.push_back({index, 0, item.size, binding.guestDescriptor}); continue; } @@ -3569,6 +5337,7 @@ std::optional> ShaderResources::MovedR const auto bytes = static_cast(GuestBufferMemory::ViewBytes(size, item.adjustment)); if (!GuestMemory::Accessible(reinterpret_cast(begin), bytes)) return std::nullopt; if (guestMemory.WritesOverlap(begin, bytes) || recorder.PendingWriteOverlaps(begin, bytes) || PendingStorageOverlaps(begin, bytes, nullptr) || AnyShadowedOverlaps(begin, bytes)) return std::nullopt; + if (moved.empty()) moved.reserve(allocations.size()); moved.push_back({index, address, static_cast(size)}); } } @@ -3576,30 +5345,117 @@ 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::size_t ShaderResources::DrawUploadLimit() { + static const std::size_t limit = [] { + const char* value = std::getenv("APS5_DRAW_UPLOAD_MAX"); + return value != nullptr ? static_cast(std::strtoull(value, nullptr, 10)) : std::size_t{4096}; + }(); + return limit; +} + +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(); + thread_local std::vector>* readsSlot = nullptr; + auto& reads = ShaderRecompiler::ThreadOwned(readsSlot); + guestMemory.InPlaceReads(reads); const auto maxSnapshotBytes = maxDrawSnapshotBytes; + const auto uploadLimit = DrawUploadLimit(); std::size_t totalSnapshotBytes = 0; auto result = std::make_shared(); - std::vector selected; + thread_local std::vector* selectedSlot = nullptr; + auto& selected = ShaderRecompiler::ThreadOwned(selectedSlot); + selected.clear(); + std::array stamp{}; + bool stamped = false; + const auto keep = [&](DrawBindings::Snapshot snapshot) { + if (result->snapshots.empty()) result->snapshots.reserve(allocations.size()); + result->snapshots.push_back(std::move(snapshot)); + }; for (std::size_t index = 0; index < allocations.size(); ++index) { const auto& item = allocations[index]; const auto override = std::find_if(moved.begin(), moved.end(), [&](const MovedBuffer& entry) { return entry.allocation == index; }); if (override != moved.end() && !override->words.empty()) { - auto buffer = std::make_shared(context, override->size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); - std::memcpy(buffer->Bytes().data(), override->words.data(), override->size); - for (const auto& patch : dataPatches) { - if (patch.allocation == index && patch.byte < override->size) buffer->Bytes()[patch.byte] = static_cast(patch.adjustment); + constexpr std::size_t MovedWordBufferSlots = 8; + const auto held = std::find_if(movedWordBuffers.begin(), movedWordBuffers.end(), [&](const MovedWordBuffer& entry) { return entry.allocation == index && entry.size == override->size && std::ranges::equal(entry.words, override->words); }); + std::shared_ptr buffer; + if (held != movedWordBuffers.end()) { + buffer = held->buffer; + } else { + buffer = std::make_shared(context, override->size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::memcpy(buffer->Bytes().data(), override->words.data(), override->size); + for (const auto& patch : dataPatches) { + if (patch.allocation == index && patch.byte < override->size) buffer->Bytes()[patch.byte] = static_cast(patch.adjustment); + } + MovedWordBuffer entry{index, {override->words.begin(), override->words.end()}, override->size, buffer}; + if (movedWordBuffers.size() < MovedWordBufferSlots) movedWordBuffers.push_back(std::move(entry)); + else movedWordBuffers[movedWordBufferNext++ % MovedWordBufferSlots] = std::move(entry); } selected.push_back(index); - result->snapshots.push_back({0, std::move(buffer)}); + keep({0, std::move(buffer), 0, override->size}); continue; } std::uint64_t address = item.address; @@ -3608,7 +5464,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; } @@ -3616,49 +5472,103 @@ std::shared_ptr ShaderResources::PrepareDrawBindi const auto bytes = static_cast(GuestBufferMemory::ViewBytes(size, item.adjustment)); if (override == moved.end() && (bytes > maxSnapshotBytes || totalSnapshotBytes + bytes > maxSnapshotBytes)) continue; const auto registryGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); - const auto generation = GuestMemory::CollectWrites(begin, bytes); - auto buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation); + if (!stamped) { + stamp = {GuestMemory::TrackerGeneration(), GuestMemory::CollectEpoch(), registryGeneration}; + stamped = true; + } + VkDeviceSize offset = 0; + auto buffer = recorder.UnwrittenDrawSnapshot(begin, bytes, stamp, &offset); + static const bool verifySnapshots = std::getenv("APS5_VERIFY_DRAW_SNAPSHOTS") != nullptr; + if (verifySnapshots && buffer != nullptr) { + static std::atomic checked{0}, differing{0}; + const auto fast = buffer->Bytes().subspan(static_cast(offset), bytes); + const auto slow = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, GuestMemory::CollectWrites(begin, bytes)); + const bool same = slow == buffer || (slow != nullptr && slow->Bytes().size() == fast.size() && std::memcmp(slow->Bytes().data(), fast.data(), fast.size()) == 0) || (slow == nullptr && std::memcmp(fast.data(), reinterpret_cast(begin), bytes) == 0); + if (!same) differing.fetch_add(1); + if (checked.fetch_add(1) % 200000 == 199999) std::fprintf(stderr, "[draw-snapshots] verify: %llu fast answers checked, %llu differing%c", static_cast(checked.load()), static_cast(differing.load()), 10); + } if (buffer == nullptr) { - buffer = std::make_shared(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); - std::memcpy(buffer->Bytes().data(), reinterpret_cast(begin), bytes); - recorder.KeepDrawSnapshot(begin, bytes, generation, registryGeneration, buffer); + const bool uploaded = bytes != 0 && bytes <= uploadLimit; + if (uploaded) { + auto slice = recorder.DrawUpload(bytes); + std::memcpy(slice.bytes, reinterpret_cast(begin), bytes); + buffer = std::move(slice.buffer); + offset = slice.offset; + } else { + const auto generation = GuestMemory::CollectWrites(begin, bytes); + buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation); + if (buffer == nullptr) { + buffer = std::make_shared(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memcpy(buffer->Bytes().data(), reinterpret_cast(begin), bytes); + recorder.KeepDrawSnapshot(begin, bytes, generation, registryGeneration, buffer); + } + } + if (GuestMemory::TrackerGeneration() != stamp[0]) stamped = false; + else if (!uploaded || stamp[1] != 0) recorder.NoteUnwrittenDrawSnapshot(begin, bytes, stamp, buffer, offset); } selected.push_back(index); - result->snapshots.push_back({begin, std::move(buffer)}); + keep({begin, std::move(buffer), offset, bytes}); totalSnapshotBytes += bytes; CaptureTrace::Log("draw-snapshot batch=%llu address=%llx bytes=%zu", static_cast(recorder.Submissions() + 1), static_cast(begin), bytes); } if (selected.empty()) return {}; Require(context.descriptorCache != nullptr, "draw snapshots require a descriptor cache"); - std::array, 2> descriptions; - for (const auto& binding : bindings) descriptions[setOf(binding)].push_back(binding.layout); + std::array described{}; + thread_local std::array, 2>* countsSlot = nullptr; + auto& counts = ShaderRecompiler::ThreadOwned(countsSlot); + for (auto& sizes : counts) sizes.clear(); + for (const auto& binding : bindings) { + const auto set = setOf(binding); + described[set] = true; + const auto count = (binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) != 0 ? std::max(1u, static_cast(binding.imageAllocations.size())) : binding.layout.descriptorCount; + if (count == 0) continue; + auto& sizes = counts[set]; + const auto held = std::find_if(sizes.begin(), sizes.end(), [&](const VkDescriptorPoolSize& entry) { return entry.type == binding.layout.descriptorType; }); + if (held != sizes.end()) held->descriptorCount += count; + else sizes.push_back({binding.layout.descriptorType, count}); + } + for (auto& sizes : counts) std::sort(sizes.begin(), sizes.end(), [](const VkDescriptorPoolSize& a, const VkDescriptorPoolSize& b) { return a.type < b.type; }); + const auto sizesOf = [&](std::size_t set) { return std::span(counts[set]); }; + const auto variableOf = [&](std::size_t set) { return tableBindings[set] != std::numeric_limits::max() ? std::optional(std::max(1u, static_cast(bindings[tableBindings[set]].imageAllocations.size()))) : std::nullopt; }; result->cache = context.descriptorCache; - if (!descriptions[0].empty() || splitSets) { - result->allocation = result->cache->Allocate(_layout, PoolSizes(descriptions[0])); + if (described[0] || splitSets) { + result->allocation = result->cache->AllocateRewritten(_layout, sizesOf(0), updateAfterBind, variableOf(0)); + result->layout = _layout; Require(result->allocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); } - if (!descriptions[1].empty() || splitSets) { - result->fragmentAllocation = result->cache->Allocate(fragment.layout, PoolSizes(descriptions[1])); + if (described[1] || splitSets) { + result->fragmentAllocation = result->cache->AllocateRewritten(fragment.layout, sizesOf(1), updateAfterBind, variableOf(1)); + result->fragmentLayout = fragment.layout; Require(result->fragmentAllocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); } - auto buffers = descriptorBuffers; - auto writes = descriptorWrites; - std::vector snapshotIndices(allocations.size(), selected.size()); + thread_local std::vector* buffersSlot = nullptr; + auto& buffers = ShaderRecompiler::ThreadOwned(buffersSlot); + buffers = descriptorBuffers; + thread_local std::vector* writesSlot = nullptr; + auto& writes = ShaderRecompiler::ThreadOwned(writesSlot); + writes = descriptorWrites; + thread_local std::vector* indicesSlot = nullptr; + auto& snapshotIndices = ShaderRecompiler::ThreadOwned(indicesSlot); + snapshotIndices.assign(allocations.size(), selected.size()); for (std::size_t index = 0; index < selected.size(); ++index) snapshotIndices[selected[index]] = index; - for (std::size_t index = 0; index < bindings.size(); ++index) { - const auto& binding = bindings[index]; - auto& write = writes[index]; - write.dstSet = setOf(binding) == 0 ? result->allocation.set : result->fragmentAllocation.set; - if (binding.allocations.empty()) continue; - const auto offset = static_cast(write.pBufferInfo - descriptorBuffers.data()); - write.pBufferInfo = buffers.data() + offset; - for (std::size_t element = 0; element < binding.allocations.size(); ++element) { - const auto snapshotIndex = snapshotIndices[binding.allocations[element]]; - if (snapshotIndex == selected.size()) continue; - const auto& snapshot = result->snapshots[snapshotIndex]; - buffers[offset + element] = {snapshot.buffer->Handle(), 0, snapshot.buffer->Bytes().size()}; + auto nextWrite = writes.begin(); + for (const auto& binding : bindings) { + 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; + const auto offset = static_cast(write.pBufferInfo - descriptorBuffers.data()); + write.pBufferInfo = buffers.data() + offset; + for (std::size_t element = 0; element < binding.allocations.size(); ++element) { + const auto snapshotIndex = snapshotIndices[binding.allocations[element]]; + if (snapshotIndex == selected.size()) continue; + const auto& snapshot = result->snapshots[snapshotIndex]; + buffers[offset + element] = {snapshot.buffer->Handle(), snapshot.offset, snapshot.bytes}; + } } } + Require(nextWrite == writes.end(), "draw snapshot writes disagree with their descriptor bindings"); context.Resolved(&DeviceFunctions::updateDescriptorSets, "vkUpdateDescriptorSets")(context.device, static_cast(writes.size()), writes.data(), 0, nullptr); recorder.Keep(result); for (const auto& snapshot : result->snapshots) recorder.KeepBytes(snapshot.buffer.get(), snapshot.buffer->Bytes().size()); @@ -3679,23 +5589,36 @@ bool SkipWriteBack() { } void ShaderResources::WriteBack() { - WriteBackBuffers(); - if (SkipWriteBack()) return; - for (std::size_t index = 0; index < storageTextures.size(); ++index) { - if (!storageWritten[index]) continue; - storageTextures[index]->MarkDirty(); - static_cast(DepthSurfaceStoreBack(context, *storageTextures[index])); + std::exception_ptr unaligned; + try { + WriteBackBuffers(); + } catch (const BdaUnalignedAccess&) { + unaligned = std::current_exception(); } + if (!SkipWriteBack()) { + for (std::size_t index = 0; index < storageTextures.size(); ++index) { + if (!storageWritten[index] || storageTextures[index] == nullptr) continue; + storageTextures[index]->MarkDirty(); + static_cast(DepthSurfaceStoreBack(context, *storageTextures[index])); + } + } + if (unaligned) std::rethrow_exception(unaligned); } void ShaderResources::MarkGpuWrites(Recorder& recorder) { // The ranges this use reads in place through their host imports (read-only and written elements // alike, and an address-based build's whole leased heaps), before the writes: a CPU store into // one of them (the copy HLE) must not land before the recorded work read it. - recorder.NotePendingReads(guestMemory.InPlaceReads(), guestMemory.HoldsLease() ? Recorder::ReadKind::AddressBased : Recorder::ReadKind::DispatchElement); + const auto kind = guestMemory.HoldsLease() ? Recorder::ReadKind::AddressBased : Recorder::ReadKind::DispatchElement; + if (auto split = guestMemory.SplitInPlaceReads()) { + recorder.NotePendingReadList(std::move(split->space), kind); + recorder.NotePendingReads(split->own, kind); + } else { + recorder.NotePendingReads(guestMemory.InPlaceReads(), kind); + } 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])); } @@ -3717,7 +5640,7 @@ void ShaderResources::MarkGpuWrites(Recorder& recorder) { if (nowMs - last < 10000 || !counters.lastReport.compare_exchange_strong(last, nowMs)) return; const auto elements = counters.elements.load(); const auto readOnly = counters.readOnly.load(); - AgcDriver::ProfilePrint_nid_no_patch("[buffers] descriptor elements bound: %llu total, %llu written, %llu read-only; %llu pending-write notes skipped\n", static_cast(elements), static_cast(elements - readOnly), static_cast(readOnly), static_cast(counters.notesSkipped.load())); + AgcDriver::ProfilePrint_nid_no_patch("[buffers] descriptor elements bound (cumulative): %llu total, %llu written, %llu read-only; %llu pending-write notes skipped\n", static_cast(elements), static_cast(elements - readOnly), static_cast(readOnly), static_cast(counters.notesSkipped.load())); } void ShaderResources::CheckFaults() const { @@ -3725,9 +5648,16 @@ void ShaderResources::CheckFaults() const { } void ShaderResources::WriteBackBuffers() { - if (bda) bda->CheckFault(); - if (SkipWriteBack()) return; - guestMemory.WriteBack(); + std::exception_ptr unaligned; + if (bda) { + try { + bda->CheckFault(); + } catch (const BdaUnalignedAccess&) { + unaligned = std::current_exception(); + } + } + if (!SkipWriteBack()) guestMemory.WriteBack(); + if (unaligned) std::rethrow_exception(unaligned); } bool ShaderResources::ReadsOverlap(std::uint64_t address, std::size_t bytes) const { @@ -3751,6 +5681,10 @@ bool ShaderResources::HasCachedStorageImages() const { return std::none_of(textures.begin(), textures.end(), [](const auto& texture) { return texture != nullptr && texture->StorageSource() != nullptr && !texture->StorageSource()->Cached(); }); } +bool ShaderResources::HoldsReleasedTexture() const { + return std::any_of(textures.begin(), textures.end(), [](const auto& texture) { return texture != nullptr && texture->Released(); }); +} + bool ShaderResources::ReadsImage(const StorageTexture* image) const { if (image == nullptr) return false; for (const auto& texture : textures) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 0b410d2dd0..abcf13c57c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -38,6 +38,7 @@ struct Module { std::map decorations; std::map, std::uint32_t> builtins; std::map, std::uint32_t> offsets; + std::set> perPrimitiveMembers; std::map variables; std::map constants; std::set interface; @@ -47,9 +48,13 @@ 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; + bool meshCapability = false; + std::uint32_t primitiveInputs = 0; + bool pointSizeOutput = false; + bool pointSize = false; std::map> modes; const std::vector& Type(std::uint32_t id) const { @@ -74,17 +79,17 @@ struct Module { throw std::runtime_error("AGC graphics: unsupported SPIR-V interface type"); } - std::uint32_t AddLocations(std::map& locations, std::uint32_t location, std::uint32_t typeId, bool patch, std::uint32_t depth = 0) const { + std::uint32_t AddLocations(std::map& locations, std::uint32_t location, std::uint32_t typeId, bool patch, bool primitive = false, std::uint32_t depth = 0) const { Require(depth < 8 && location < 256, "graphics interface exceeds supported locations"); const auto& type = Type(typeId); if ((type[0] & 0xffffu) == spv::OpTypeArray) { Require(type.size() == 4, "malformed interface array"); const auto count = constants.find(type[3]); Require(count != constants.end() && count->second != 0 && count->second <= 256, "invalid interface array length"); - for (std::uint32_t i = 0; i < count->second; ++i) location = AddLocations(locations, location, type[2], patch, depth + 1); + for (std::uint32_t i = 0; i < count->second; ++i) location = AddLocations(locations, location, type[2], patch, primitive, depth + 1); return location; } - Require(locations.emplace(location | (patch ? 0x10000u : 0u), std::string(patch ? "patch:" : "vertex:") + Signature(typeId)).second, "duplicate shader interface location"); + Require(locations.emplace(location | (patch ? 0x10000u : 0u), std::string(patch ? "patch:" : primitive ? "primitive:" : "vertex:") + Signature(typeId)).second, "duplicate shader interface location"); return location + 1; } @@ -115,7 +120,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,27 +136,38 @@ 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::BuiltInPointSize && pointSizeOutput && signature == "f32" && !pointSize) { + pointSize = true; + return; + } + if (!input && value == spv::BuiltInPrimitivePointIndicesEXT) primitiveIndices = 1u; + 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::BuiltInPrimitivePointIndicesEXT && 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); if (vertex && storage == spv::StorageClassInput) { - Require((value == spv::BuiltInVertexIndex || value == spv::BuiltInInstanceIndex) && signature == "i32", "unsupported vertex built-in input"); + Require((value == spv::BuiltInVertexIndex || value == spv::BuiltInInstanceIndex || value == spv::BuiltInBaseInstance) && signature == "i32", "unsupported vertex built-in input"); } else if (vertex && storage == spv::StorageClassOutput) { if (value == spv::BuiltInLayer && layerOutput && (signature == "u32" || signature == "i32")) return; + if (value == spv::BuiltInPointSize && pointSizeOutput && signature == "f32" && !pointSize) { + pointSize = true; + return; + } Require(value == spv::BuiltInPosition && signature == "f32x4" && !position, "unsupported or duplicate vertex built-in output"); position = true; } else { - Require((storage == spv::StorageClassInput && ((value == spv::BuiltInFragCoord && signature == "f32x4") || ((value == spv::BuiltInFrontFacing || value == spv::BuiltInHelperInvocation) && signature == "bool") || ((value == spv::BuiltInSampleId || value == spv::BuiltInLayer) && signature == "i32"))) || (storage == spv::StorageClassOutput && ((value == spv::BuiltInFragDepth && signature == "f32") || (value == spv::BuiltInFragStencilRefEXT && signature == "i32") || (value == spv::BuiltInSampleMask && (signature == "i32[1]" || signature == "u32[1]")))), "unsupported fragment built-in"); + Require((storage == spv::StorageClassInput && ((value == spv::BuiltInFragCoord && signature == "f32x4") || ((value == spv::BuiltInFrontFacing || value == spv::BuiltInHelperInvocation) && signature == "bool") || ((value == spv::BuiltInSampleId || value == spv::BuiltInLayer) && signature == "i32") || (value == spv::BuiltInSampleMask && signature == "i32[1]"))) || (storage == spv::StorageClassOutput && ((value == spv::BuiltInFragDepth && signature == "f32") || (value == spv::BuiltInFragStencilRefEXT && signature == "i32") || (value == spv::BuiltInSampleMask && (signature == "i32[1]" || signature == "u32[1]")))), "unsupported fragment built-in"); } } }; -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) { +Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation, bool shaderTerminateInvocation, std::span primitiveLocations = {}) { using Stage = ShaderRecompiler::ShaderStage; Require(compiled.program != nullptr, "missing compiled shader"); const auto& shader = *compiled.program; @@ -180,11 +196,13 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(words[4] == 0, "unsupported SPIR-V schema: " + std::to_string(words[4])); Module module; + module.pointSizeOutput = (state.vertexPoints && stage == Stage::Vertex) || (mesh && state.stages.mesh.has_value() && state.stages.mesh->outputPrimitiveSize == 1u); std::uint32_t entries = 0; std::uint32_t memoryModels = 0; 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"); @@ -234,8 +252,9 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi (control || evaluation) && capability == spv::CapabilityTessellation; + if (fragment && capability == spv::CapabilityMeshShadingEXT) module.meshCapability = true; const bool isMeshCapability = - mesh && + (mesh || (fragment && !primitiveLocations.empty())) && capability == spv::CapabilityMeshShadingEXT; const bool isGeometryCapability = @@ -250,7 +269,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,9 +281,11 @@ 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) || + (fragment && sampleRateShading && capability == spv::CapabilityInterpolationFunction) || capability == spv::CapabilityImageMSArray || capability == spv::CapabilityStorageImageMultisample || ((vertex || mesh) && shaderCullDistance && capability == spv::CapabilityCullDistance) || @@ -275,19 +296,22 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi capability == spv::CapabilityFloat16 || capability == spv::CapabilityRoundingModeRTE || capability == spv::CapabilityDenormPreserve || + capability == spv::CapabilityDenormFlushToZero || capability == spv::CapabilityFloat64 || capability == spv::CapabilityStorageBuffer8BitAccess || capability == spv::CapabilityPhysicalStorageBufferAddresses || capability == spv::CapabilitySampledImageArrayDynamicIndexing || - capability == spv::CapabilityStorageImageArrayDynamicIndexing; + capability == spv::CapabilityStorageImageArrayDynamicIndexing || + (fragment && demoteToHelperInvocation && capability == spv::CapabilityDemoteToHelperInvocation); // Bindless image tables index their slots non-uniformly in graphics stages // (VK_EXT_descriptor_indexing, enabled by the device setup when available). const bool isDescriptorIndexingCapability = - descriptorIndexing && + (descriptorIndexing && (capability == spv::CapabilityShaderNonUniform || capability == spv::CapabilitySampledImageArrayNonUniformIndexing || - capability == spv::CapabilityStorageImageArrayNonUniformIndexing); + capability == spv::CapabilityStorageImageArrayNonUniformIndexing)) || + (runtimeDescriptorArray && capability == spv::CapabilityRuntimeDescriptorArray); Require( isBaseCapability || @@ -328,9 +352,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; } - Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (viewportIndexLayer && extension == "SPV_EXT_shader_viewport_index_layer") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension"); + 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")) || (fragment && !primitiveLocations.empty() && extension == "SPV_EXT_mesh_shader") || ((descriptorIndexing || runtimeDescriptorArray) && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension"); break; } + case spv::OpTerminateInvocation: + 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: @@ -356,7 +390,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi for (std::size_t i = 5; i < count; ++i) Require(module.interface.insert(instruction[i]).second, "duplicate SPIR-V interface ID"); break; case spv::OpExecutionMode: - if (count == 4 && (instruction[2] == spv::ExecutionModeSignedZeroInfNanPreserve || instruction[2] == spv::ExecutionModeRoundingModeRTE || instruction[2] == spv::ExecutionModeDenormPreserve)) { + if (count == 4 && (instruction[2] == spv::ExecutionModeSignedZeroInfNanPreserve || instruction[2] == spv::ExecutionModeRoundingModeRTE || instruction[2] == spv::ExecutionModeDenormPreserve || instruction[2] == spv::ExecutionModeDenormFlushToZero)) { Require(instruction[3] == 16u || instruction[3] == 32u || instruction[3] == 64u, "float controls execution modes require Float16, Float32 or Float64"); executionModeTargets.insert(instruction[1]); break; @@ -395,6 +429,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi auto& fields = instruction[3] == spv::DecorationBuiltIn ? module.builtins : module.offsets; Require(fields.emplace(std::make_pair(instruction[1], instruction[2]), instruction[4]).second, "duplicate SPIR-V member decoration"); } + if (instruction[3] == spv::DecorationPerPrimitiveEXT) module.perPrimitiveMembers.emplace(instruction[1], instruction[2]); break; case spv::OpTypeBool: case spv::OpTypeInt: @@ -426,6 +461,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; @@ -434,8 +470,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 == 1u ? spv::ExecutionModeOutputPoints : 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}); @@ -445,7 +487,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi const bool quads = state.rectList || state.stages.tessellation->domain == 2u; const bool counterClockwise = !state.rectList && state.stages.tessellation->outputTopology == 3u; mode(quads ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles, {}); - mode(state.rectList || state.stages.tessellation->partitioning == 0u ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd, {}); + mode(state.rectList || state.stages.tessellation->partitioning == 0u ? spv::ExecutionModeSpacingEqual : state.stages.tessellation->partitioning == 3u ? spv::ExecutionModeSpacingFractionalEven : spv::ExecutionModeSpacingFractionalOdd, {}); mode(counterClockwise ? spv::ExecutionModeVertexOrderCcw : spv::ExecutionModeVertexOrderCw, {}); Require(module.modes.size() == 3, "unsupported tessellation-evaluation execution mode"); } else if (geometry) { @@ -483,18 +525,18 @@ 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 || module.perPrimitiveMembers.contains({outer[2], 0u}) || (decoration.builtin && (*decoration.builtin == spv::BuiltInPrimitivePointIndicesEXT || *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]; } const auto& type = module.Type(typeId); - Require(!decoration.index || (fragment && output && state.dualSourceBlend && decoration.location == 0u && *decoration.index == 1u), "only the second dual-source blend color may use the Index decoration, at location 0 index 1"); + Require(!decoration.index || (fragment && output && (state.dualSourceBlend || state.reversedBlend != 0) && decoration.location == 0u && *decoration.index == 1u), "only the second dual-source blend color may use the Index decoration, at location 0 index 1"); if (variable.storage == spv::StorageClassInput || variable.storage == spv::StorageClassOutput) { Require(module.interface.contains(id), "SPIR-V input or output is absent from the entry point interface"); if (decoration.index) { @@ -502,10 +544,13 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi module.secondSource = module.Signature(typeId); } else if (decoration.location) { Require(!vertexArray || (outer[0] & 0xffffu) == spv::OpTypeArray, "per-vertex interface lacks a control-point or mesh-output dimension"); - Require(!decoration.perPrimitive, "per-primitive user outputs are unsupported"); + Require(!decoration.perPrimitive || ((mesh && output) || (fragment && input)), "per-primitive user interface is unsupported for this stage"); + Require(!decoration.perPrimitive || !decoration.patch, "a per-primitive interface cannot be a patch"); + Require(!decoration.perPrimitive || std::ranges::find(primitiveLocations, *decoration.location) != primitiveLocations.end(), "per-primitive user interface at a location the barycentric emulation does not reserve"); Require(!decoration.builtin, "shader input cannot have both location and built-in decorations"); auto& locations = variable.storage == spv::StorageClassInput ? module.inputs : module.outputs; - module.AddLocations(locations, *decoration.location, typeId, decoration.patch); + module.AddLocations(locations, *decoration.location, typeId, decoration.patch, decoration.perPrimitive); + if (decoration.perPrimitive && fragment) module.primitiveInputs++; } else if (decoration.builtin) { module.Builtin(*decoration.builtin, typeId, variable.storage, stage, subgroup); } else { @@ -517,7 +562,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } } } else if (variable.storage == spv::StorageClassPushConstant) { - Require(!push && (!shader.pushConstants.empty() || mesh || (control && !rectListControl)) && (type[0] & 0xffffu) == spv::OpTypeStruct, "invalid push constant interface"); + Require(!push && (!shader.pushConstants.empty() || shader.readsDepthRemapPush || mesh || (control && !rectListControl)) && (type[0] & 0xffffu) == spv::OpTypeStruct, "invalid push constant interface"); push = true; Require(type.size() == 3 && module.decorations[typeId].block, "push constant variable must be a Block struct with exactly one member"); const auto offset = module.offsets.find({typeId, 0}); @@ -533,10 +578,14 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } else if (variable.storage == spv::StorageClassUniformConstant) { Require(decoration.set && decoration.binding, "unbound shader resource"); const auto key = std::make_pair(*decoration.set, *decoration.binding); - Require(descriptors.insert(key).second, "duplicate SPIR-V resource binding"); const auto binding = std::find_if(shader.bindings.begin(), shader.bindings.end(), [&](const auto& item) { return item.descriptorSet == key.first && item.binding == key.second; }); + 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::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; + } Require(type.size() == 4 && (type[0] & 0xffffu) == spv::OpTypeArray, "typed heap must be declared as a descriptor array"); const auto heap = static_cast(binding->binding % static_cast(ShaderRecompiler::RuntimeAbi::Binding::Count)); const auto capacity = ShaderRecompiler::RuntimeAbi::HeapCapacity(heap); @@ -552,7 +601,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(binding != shader.bindings.end(), "SPIR-V resource is absent from recompiler binding metadata"); Require(binding->kind == ShaderRecompiler::DescriptorKind::StorageBuffer, "SPIR-V descriptor type disagrees with recompiler binding metadata"); Require(!binding->readOnly, "read-only descriptor metadata is unsupported because the recompiler emits no NonWritable decoration"); - Require(array || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (binding->role == ShaderRecompiler::DescriptorRole::BdaPagetable || binding->role == ShaderRecompiler::DescriptorRole::FaultBuffer)) || binding->role == ShaderRecompiler::DescriptorRole::ShaderData || binding->role == ShaderRecompiler::DescriptorRole::FlattenedSrt || binding->role == ShaderRecompiler::DescriptorRole::Gds, "SPIR-V descriptor role is unsupported"); + Require(array || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (binding->role == ShaderRecompiler::DescriptorRole::BdaPagetable || binding->role == ShaderRecompiler::DescriptorRole::FaultBuffer)) || binding->role == ShaderRecompiler::DescriptorRole::ShaderData || binding->role == ShaderRecompiler::DescriptorRole::FlattenedSrt || binding->role == ShaderRecompiler::DescriptorRole::ImageTableMap || binding->role == ShaderRecompiler::DescriptorRole::Gds, "SPIR-V descriptor role is unsupported"); auto blockId = typeId; if (array) { Require(type.size() == 4 && (type[0] & 0xffffu) == spv::OpTypeArray, "guest buffer descriptors must be declared as a descriptor array"); @@ -585,37 +634,68 @@ 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() || shader.readsDepthRemapPush || mesh || (control && !rectListControl && !barycentricPatches)), "recompiler push constant metadata disagrees with SPIR-V"); for (const auto id : module.interface) Require(module.variables.contains(id), "entry point interface contains an unknown variable"); - if (mesh) Require(module.primitiveIndices, "mesh shader does not export primitive indices"); + if (mesh) Require(module.primitiveIndices == state.stages.mesh->outputPrimitiveSize, "mesh shader does not export primitive indices"); + if (fragment) Require(module.primitiveInputs == 0 || !primitiveLocations.empty(), "per-primitive pixel inputs need the barycentric emulation layout"); + if (fragment) Require(!module.meshCapability || module.primitiveInputs != 0, "a pixel shader declares mesh shading without a per-primitive input"); if (stage == Stage::Vertex || mesh || evaluation || geometry) Require(module.position, "vertex shader does not export position"); return module; } } -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 DeclaresEarlyFragmentTests(const ShaderRecompiler::RecompileResult& program) { + 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; + Require(count != 0 && count <= words.size() - cursor, "truncated SPIR-V instruction"); + const auto op = static_cast(words[cursor] & 0xffffu); + if (op == spv::OpFunction) return false; + if (op == spv::OpExecutionMode && count >= 3 && words[cursor + 2] == spv::ExecutionModeEarlyFragmentTests) return true; + cursor += count; + } + 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, bool shaderTerminateInvocation, bool meshShader) { using Stage = ShaderRecompiler::ShaderStage; const bool tessellation = state.stages.path == ShaderPath::Tessellation; const bool mesh = state.stages.path == ShaderPath::Geometry; Require(state.stages.path == ShaderPath::Vertex || tessellation || mesh, "unsupported graphics shader path"); Require(state.stages.mesh.has_value() == mesh && state.stages.tessellation.has_value() == tessellation, "graphics stage configuration disagrees with its path"); + Require(!mesh || meshShader, "device does not support mesh shaders"); 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 || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, "emulated barycentrics through tessellation require a triangle list or strip"); 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"); } const std::array tessStages{Stage::Local, Stage::TessellationControl, Stage::TessellationEvaluation, Stage::Fragment}; static_cast(AssemblePushConstants(shaders)); + std::vector primitiveLocations; + if (mesh && !shaders.empty() && shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active) { + const auto& last = *shaders.back().program; + const auto layout = ShaderRecompiler::LayoutBarycentricEmulation(last.fragmentParameters, last.barycentricEmulation, true); + if (layout.positionsLocation != ShaderRecompiler::BarycentricEmulationLayout::NoLocation) { + for (std::uint32_t corner = 0; corner < 3u; ++corner) primitiveLocations.push_back(layout.positionsLocation + corner); + for (const auto& [guest, relocated] : layout.perVertexLocations) { + static_cast(guest); + for (std::uint32_t corner = 0; corner < 3u; ++corner) primitiveLocations.push_back(relocated + corner); + } + } + } Module previous; for (std::size_t i = 0; i < shaders.size(); ++i) { - const auto expected = state.rectList ? (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); + const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer, runtimeDescriptorArray, demoteToHelperInvocation, shaderTerminateInvocation, primitiveLocations); if (i != 0) { for (const auto& [location, signature] : current.inputs) { const auto output = previous.outputs.find(location); @@ -642,10 +722,10 @@ 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(color == state.colors.end() || signature == (uintExport || packed ? "vertex:u32x4" : "vertex:f32x4"), packed ? "fragment shader must export uint4 words to its packed color attachments" : uintExport ? "fragment shader must export uint4 colors to its unsigned integer attachments" : "fragment shader must export float4 colors to its attachments"); locations.insert(location); } - Require(!state.dualSourceBlend || previous.secondSource == "f32x4", "dual-source blending needs a float4 second color at location 0 index 1"); + Require((!state.dualSourceBlend && state.reversedBlend == 0) || previous.secondSource == "f32x4", "dual-source blending needs a float4 second color at location 0 index 1"); return locations; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 955504eccb..c696cab63a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1,5 +1,6 @@ #include #include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" @@ -16,8 +17,17 @@ #include #include #include +#include namespace AgcDriver::Graphics { + +void NormalizeUnusedViewportDepth(State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel) { + if (state.depth || pixel.posZ) return; + if (!std::isfinite(state.viewport.minDepth) || !std::isfinite(state.viewport.maxDepth)) return; + state.viewport.minDepth = std::clamp(state.viewport.minDepth, 0.0f, 1.0f); + state.viewport.maxDepth = std::clamp(state.viewport.maxDepth, 0.0f, 1.0f); +} + namespace { std::uint32_t read(const Registers& registers, std::uint32_t offset, RegisterBank bank = RegisterBank::Context) { @@ -43,12 +53,67 @@ float readFloat(const Registers& registers, std::uint32_t offset) { return value; } +float readDepthBound(const Registers& registers, std::uint32_t offset) { + const auto value = std::bit_cast(read(registers, offset)); + if (std::isnan(value)) Require(false, "NaN depth bound at DWORD " + std::to_string(offset)); + return value; +} + std::string zeroMessage(std::uint32_t offset, std::uint32_t value, const char* name) { char detail[64]; std::snprintf(detail, sizeof(detail), " (register 0x%x = 0x%08x)", offset, value); return std::string(name) + " is unsupported" + detail; } +VkPolygonMode decodePolygonMode(const Registers& cx, std::uint32_t raster, const State& state, bool pointsOrLines) { + const auto mode = (raster >> 3u) & 3u; + if (mode == 0) return VK_POLYGON_MODE_FILL; + if (mode != 1) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "a reserved POLY_MODE")); + const auto front = (raster >> 5u) & 7u; + const auto back = (raster >> 8u) & 7u; + if (front > 2 || back > 2) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "a reserved polygon type")); + if (pointsOrLines && (state.stages.path == ShaderPath::Vertex || state.stages.mesh.has_value())) return VK_POLYGON_MODE_FILL; + const bool frontDrawn = (state.cullMode & VK_CULL_MODE_FRONT_BIT) == 0; + const bool backDrawn = (state.cullMode & VK_CULL_MODE_BACK_BIT) == 0; + if (!frontDrawn && !backDrawn) return VK_POLYGON_MODE_FILL; + if (frontDrawn && backDrawn && front != back) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "different front and back polygon modes")); + const auto type = frontDrawn ? front : back; + if (type == 2) return VK_POLYGON_MODE_FILL; + if (state.rectList) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "a point or line polygon mode on a rect list")); + if ((raster & 0x3800u) != 0) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "depth bias with a point or line polygon mode")); + if (type == 1) { + const auto line = find(cx, 0x282); + if (line == cx.end()) throw std::runtime_error("AGC graphics: a line polygon mode needs PA_SU_LINE_CNTL"); + if ((line->second & 0xffffu) != 8u) throw std::runtime_error("AGC graphics: " + zeroMessage(0x282, line->second, "a line polygon mode wider or narrower than one pixel")); + return VK_POLYGON_MODE_LINE; + } + const auto size = find(cx, 0x280); + const auto limits = find(cx, 0x281); + if (size == cx.end() || limits == cx.end()) throw std::runtime_error("AGC graphics: a point polygon mode needs PA_SU_POINT_SIZE and PA_SU_POINT_MINMAX"); + if (size->second != 0x80008u) throw std::runtime_error("AGC graphics: " + zeroMessage(0x280, size->second, "a point polygon mode with a point size other than one pixel")); + if ((limits->second & 0xffffu) > 8u || (limits->second >> 16u) < 8u) throw std::runtime_error("AGC graphics: " + zeroMessage(0x281, limits->second, "a point polygon mode whose size limits exclude one pixel")); + return VK_POLYGON_MODE_POINT; +} + +std::string tessellationMessage(std::uint32_t config, std::uint32_t parameters, const ShaderRecompiler::TessellationConfiguration& tess) { + static constexpr std::array Types{"TESS_ISOLINE", "TESS_TRIANGLE", "TESS_QUAD"}; + static constexpr std::array Partitionings{"PART_INTEGER", "PART_POW2", "PART_FRAC_ODD", "PART_FRAC_EVEN"}; + static constexpr std::array Topologies{"OUTPUT_POINT", "OUTPUT_LINE", "OUTPUT_TRIANGLE_CW", "OUTPUT_TRIANGLE_CCW"}; + std::ostringstream message; + const auto field = [&](std::string_view name, std::uint32_t value, std::span names) { + message << name << '=' << value; + if (value < names.size()) message << ' ' << names[value]; + }; + message << "VGT_TF_PARAM=0x" << std::hex << parameters << std::dec << " ("; + field("TYPE", tess.domain, Types); + message << ", "; + field("PARTITIONING", tess.partitioning, Partitionings); + message << ", "; + field("TOPOLOGY", tess.outputTopology, Topologies); + message << "), VGT_LS_HS_CONFIG=0x" << std::hex << config << std::dec << " (NUM_PATCHES=" << tess.patchesPerGroup << ", HS_NUM_INPUT_CP=" << tess.inputControlPoints << ", HS_NUM_OUTPUT_CP=" << tess.outputControlPoints << "): only triangle and quad domains with integer, fractional-even or fractional-odd partitioning and triangle output are supported by the reference path"; + return message.str(); +} + void zero(const Registers& registers, std::uint32_t offset, std::uint32_t mask, const char* name, RegisterBank bank = RegisterBank::Context) { const auto value = read(registers, offset, bank); if ((value & mask) != 0) throw std::runtime_error(zeroMessage(offset, value, name)); @@ -91,9 +156,9 @@ VkConservativeRasterizationModeEXT decodeConservativeRasterization(const QueueSt requireConservativeTriangles((domain == 1u || domain == 2u) && (topology == 2u || topology == 3u), topology == 0u ? "points" : topology == 1u || domain == 0u ? "lines" : "other primitive types", "VGT_TF_PARAM", parameters); } else { const auto primitive = read(queue.userConfig, 0x242, RegisterBank::UserConfig); - requireConservativeTriangles(primitive == 4u || primitive == 5u || primitive == 6u, primitive == 1u ? "points" : primitive == 2u || primitive == 3u ? "lines" : primitive == 7u || primitive == 17u ? "rectangles" : "other primitive types", "VGT_PRIMITIVE_TYPE", primitive); + requireConservativeTriangles(primitive == 4u || primitive == 5u || primitive == 6u || primitive == 21u, primitive == 1u ? "points" : primitive == 2u || primitive == 3u ? "lines" : primitive == 7u || primitive == 17u ? "rectangles" : "other primitive types", "VGT_PRIMITIVE_TYPE", primitive); } - Require((read(cx, 0x1b3) & 0x44u) == 0, "conservative rasterization with centroid interpolation is unsupported"); + Require((read(cx, 0x1b3) & 0x44u) == 0 || (read(cx, 0x2f8) & 7u) == 0, "conservative rasterization with centroid interpolation of more than one rasterized sample is unsupported"); return VK_CONSERVATIVE_RASTERIZATION_MODE_OVERESTIMATE_EXT; } @@ -117,17 +182,50 @@ std::uint32_t inactiveVertexPointSize(const QueueState& queue) { case 6: case 7: case 17: + case 21: + case 19: return 1u << 16u; default: return 0; } } +bool pointOutput(const QueueState& queue) { + const auto stages = find(queue.context, 0x2d5); + const auto primitive = find(queue.userConfig, 0x242, RegisterBank::UserConfig); + if (stages == queue.context.end()) return false; + if ((stages->second & 0x24u) == 0) return primitive != queue.userConfig.end() && primitive->second == 1u; + const auto output = find(queue.context, 0x29b); + return (stages->second & 0x24u) == 0x20u && output != queue.context.end() && output->second == 0u; +} + +void decodePointSize(const QueueState& queue, State& result) { + const auto& cx = queue.context; + const auto size = read(cx, 0x280); + const auto limits = read(cx, 0x281); + const bool fromVertex = (result.paClVsOutCntl & (1u << 16u)) != 0; + Require(!fromVertex || (result.paClVsOutCntl & (1u << 21u)) != 0, "USE_VTX_POINT_SIZE without VS_OUT_MISC_VEC_ENA is unsupported"); + Require(fromVertex || (size & 0xffffu) == size >> 16u, "points whose PA_SU_POINT_SIZE width and height differ are unsupported"); + Require(fromVertex || (size & 0xffffu) != 0, "points of size 0 (PA_SU_POINT_SIZE 0) are unsupported"); + Require((limits & 0xffffu) <= limits >> 16u, "PA_SU_POINT_MINMAX with a minimum above its maximum is unsupported"); + Require(!fromVertex || (limits >> 16u) != 0, "a vertex point size clamped to 0 (PA_SU_POINT_MINMAX maximum 0) is unsupported"); + result.pointSize = static_cast(size & 0xffffu) / 8.0f; + result.pointSizeMin = static_cast(limits & 0xffffu) / 8.0f; + result.pointSizeMax = static_cast(limits >> 16u) / 8.0f; + const auto interpolation = find(cx, 0x1b5); + const auto inputs = find(cx, 0x1b6); + if (interpolation == cx.end() || (interpolation->second & 2u) == 0 || inputs == cx.end()) return; + for (std::uint32_t input = 0; input < (inputs->second & 0x3fu); ++input) { + const auto control = find(cx, 0x191 + input); + Require(control == cx.end() || (control->second & 0x820000u) == 0, "point sprite texture coordinates (SPI_PS_INPUT_CNTL PT_SPRITE_TEX) are not implemented"); + } +} + constexpr std::uint32_t DepthControlMask = ~0x007007f0u; // EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader // to run, which it always does here. constexpr std::uint32_t ShaderControlMask = ~(0x0000f870u | 0x00020600u | 0x00010000u); -constexpr std::uint32_t PixelStageRunsMask = 0x00020747u; +constexpr std::uint32_t PixelStageRunsMask = 0x000207c7u; constexpr std::uint32_t AlphaToCoverageMask = ~0x0001ff00u; constexpr std::uint32_t AlphaToCoverageUndithered = 0x0000aa01u; constexpr std::uint32_t AlphaToCoverageDithered = 0x00018701u; @@ -152,10 +250,14 @@ bool ditheredAlphaLocationsSupported(const Registers& cx) { return true; } constexpr std::uint32_t ScanModeMask = ~0x22u; -constexpr std::uint32_t ScanControlMask = ~0x76023fffu; +constexpr std::uint32_t ScanControlMask = ~0x76033fffu; +constexpr std::uint32_t PixelShaderIteratesSamples = 0x10000u; +constexpr std::uint32_t PixelSampleCoverageInput = 0x4000u; +constexpr std::uint32_t PixelSampleRateInputs = 0x2011u; constexpr std::uint32_t ScreenOffsetMask = ~0x01ff01ffu; -// Bits 26/27 (ZCLIP_NEAR/FAR_DISABLE) become depth clamping; bit 19 selects the [0, 1] clip space. -constexpr std::uint32_t ClipControlMask = ~(0x80000u | 0x01000000u | 0x0c000000u); +// Bits 26/27 (ZCLIP_NEAR/FAR_DISABLE) become depth clamping; bit 19 selects the [0, 1] clip space; +// bit 16 disables clipping. +constexpr std::uint32_t ClipControlMask = ~(0x10000u | 0x80000u | 0x01000000u | 0x0c000000u); bool zFormatSupported(std::uint32_t format) { return format == 0u || format == 1u || format == 2u || format == 3u || format == 9u; @@ -164,10 +266,11 @@ bool zFormatSupported(std::uint32_t format) { std::uint32_t shaderControlMask(std::uint32_t zFormat) { constexpr std::uint32_t zExportEnable = 0x1u; constexpr std::uint32_t stencilTestValExportEnable = 0x2u; + constexpr std::uint32_t stencilOpValExportEnable = 0x4u; constexpr std::uint32_t maskExportEnable = 0x100u; auto mask = ShaderControlMask; if (zFormat != 0u && zFormatSupported(zFormat)) mask &= ~zExportEnable; - if (zFormat == 2u || zFormat == 9u) mask &= ~stencilTestValExportEnable; + if (zFormat == 2u || zFormat == 9u) mask &= ~(stencilTestValExportEnable | stencilOpValExportEnable); if (zFormat == 9u) mask &= ~maskExportEnable; return mask; } @@ -200,7 +303,7 @@ bool depthPlanesAbsent(const Registers& cx) { return z != cx.end() && stencil != cx.end() && (z->second & 3u) == 0 && (stencil->second & 1u) == 0; } -VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint32_t refMask, bool readOnly, bool testValExport) { +VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint32_t refMask, bool readOnly, bool testValExport, bool opValExport, bool opValReference) { VkStencilOpState face{}; face.compareOp = static_cast(compare); face.compareMask = (refMask >> 8u) & 0xffu; @@ -209,9 +312,15 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint const auto opValue = refMask >> 24u; auto reference = test; auto fixed = compare == 0 || compare == 7 ? 0u : face.compareMask; - const auto convert = [&](std::uint32_t op) { + std::uint32_t invertMask = 0; + std::uint32_t inverted = 0; + const auto convert = [&](std::uint32_t op, bool testFailed) { if (face.writeMask == 0) return VK_STENCIL_OP_KEEP; - char detail[96]; + char detail[112]; + if (opValExport && ((op == 4 && !opValReference) || op == 5 || op == 6 || op == 8 || op == 9)) { + std::snprintf(detail, sizeof(detail), "AGC graphics: stencil operation %u with a pixel shader that exports the stencil operation value is unsupported", op); + throw std::runtime_error(detail); + } switch (op) { case 0: return VK_STENCIL_OP_KEEP; case 1: return VK_STENCIL_OP_ZERO; @@ -222,8 +331,22 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint if (op != 3) throw std::runtime_error("AGC graphics: stencil replacement by a value other than the test value is unsupported with a pixel shader that exports the stencil test value"); return VK_STENCIL_OP_REPLACE; } + if (opValReference) { + if (op != 4) throw std::runtime_error("AGC graphics: stencil replacement by a value other than the operation value is unsupported with a pixel shader that exports the stencil operation value"); + return VK_STENCIL_OP_REPLACE; + } const auto value = op == 2 ? 0xffu : op == 3 ? test : opValue; if (((reference ^ value) & fixed & face.writeMask) != 0) { + const auto compared = test & face.compareMask; + const bool passKnown = compare == 2 || (compare == 3 && compared == face.compareMask) || (compare == 6 && compared == 0u); + const bool failKnown = compare == 5 || (compare == 1 && compared == 0u) || (compare == 4 && compared == face.compareMask); + const bool testValueKnown = testFailed ? failKnown : passKnown; + const auto flipped = (test ^ value) & face.writeMask; + if (testValueKnown && (face.writeMask & ~face.compareMask) == 0 && (invertMask == 0 || invertMask == flipped)) { + invertMask = flipped; + ++inverted; + return VK_STENCIL_OP_INVERT; + } std::snprintf(detail, sizeof(detail), "AGC graphics: stencil replacement 0x%02x against test value 0x%02x (masks 0x%02x/0x%02x) is unsupported", value, test, face.compareMask, face.writeMask); throw std::runtime_error(detail); } @@ -246,23 +369,31 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint throw std::runtime_error(detail); } }; - face.failOp = convert(ops & 0xfu); - face.passOp = convert((ops >> 4u) & 0xfu); - face.depthFailOp = convert((ops >> 8u) & 0xfu); + face.failOp = convert(ops & 0xfu, true); + face.passOp = convert((ops >> 4u) & 0xfu, false); + face.depthFailOp = convert((ops >> 8u) & 0xfu, false); + if (invertMask != 0) { + const auto active = ((ops & 0xfu) != 0u) + (((ops >> 4u) & 0xfu) != 0u) + (((ops >> 8u) & 0xfu) != 0u); + if (inverted != active) throw std::runtime_error("AGC graphics: a stencil replacement drawn as an inversion beside another stencil operation is unsupported"); + face.writeMask = invertMask; + } face.reference = reference; + Require(!opValReference || compare == 0 || compare == 7 || face.compareMask == 0, "a stencil compare against the test value is unsupported with a pixel shader that exports the stencil operation value"); return face; } -void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValExport, State& result) { - zero(cx, 0x000, 0x00001f9cu, "depth copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); +void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValExport, bool opValExport, State& result) { + const bool opValReference = opValExport && StencilOperationValueReplaces(cx); + zero(cx, 0x000, 0x0000101cu, "depth copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); const bool depthClear = (read(cx, 0x000) & 1u) != 0; - const bool stencilClear = (read(cx, 0x000) & 2u) != 0; const auto view = read(cx, 0x002); - zero(cx, 0x002, 0x3c000000u, "depth mips (DB_DEPTH_VIEW MIP_LEVEL)"); - zero(cx, 0x010, 0x000f1000u, "partially resident or mipmapped depth (DB_Z_INFO)"); + zero(cx, 0x010, 0x00001000u, "partially resident depth (DB_Z_INFO)"); + const auto viewMip = (view >> 26u) & 0xfu; + const auto maxMip = (read(cx, 0x010) >> 16u) & 0xfu; zero(cx, 0x011, 0x00001000u, "partially resident stencil (DB_STENCIL_INFO)"); const auto zFormat = read(cx, 0x010) & 3u; const bool stencil = (read(cx, 0x011) & 1u) != 0; + const bool stencilClear = stencil && (read(cx, 0x000) & 2u) != 0; Require(zFormat != 2, "Z_24 depth is unsupported"); if (zFormat == 0) depthControl &= ~6u; if (!stencil) depthControl &= ~1u; @@ -272,27 +403,62 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx }; const bool depthReadOnly = (view & 0x01000000u) != 0; const bool stencilReadOnly = (view & 0x02000000u) != 0; - Require(!stencilClear || (stencil && !stencilReadOnly), "stencil clear requires a writable stencil plane"); + Require(!stencilClear || !stencilReadOnly, "stencil clear requires a writable stencil plane"); Require(!stencilClear || !testValExport, "stencil clear with a pixel shader that exports the stencil test value is unsupported"); Require(!depthClear || (zFormat != 0 && !depthReadOnly), "depth clear requires a writable depth plane"); DepthTarget depth{}; depth.samples = 1u << ((read(cx, 0x010) >> 2u) & 3u); + depth.depthSwizzle = (read(cx, 0x010) >> 4u) & 31u; + depth.iterate256 = (read(cx, 0x010) & (1u << 20u)) != 0; + depth.firstSlice = view & 0x1fffu; + depth.lastSlice = (view >> 13u) & 0x7ffu; depth.address = zFormat != 0 ? base(0x012, 0x01a) : 0; depth.stencilAddress = stencil ? base(0x013, 0x01b) : 0; - Require(zFormat == 0 || depthReadOnly || base(0x014, 0x01c) == depth.address, "depth read and written at different addresses is unsupported"); + const bool depthWritten = zFormat != 0 && !depthReadOnly && (depthClear || (depthControl & 6u) == 6u || (read(cx, 0x000) & 0x60u) != 0); + Require(!depthWritten || base(0x014, 0x01c) == depth.address, "depth read and written at different addresses is unsupported"); Require(!stencil || stencilReadOnly || base(0x015, 0x01d) == depth.stencilAddress, "stencil read and written at different addresses is unsupported"); + if (depth.samples != 1u) { + depth.pipeBankXor = static_cast(depth.address & 65535u); + depth.address &= ~std::uint64_t{65535u}; + depth.stencilAddress &= ~std::uint64_t{65535u}; + if (const auto surface = find(cx, 0x2af); surface != cx.end()) { + depth.htilePipeAligned = (surface->second & (1u << 18u)) != 0; + depth.htileVrsEncoding = (surface->second >> 19u) & 3u; + } + } const auto size = read(cx, 0x007); depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u}; - if (const auto slice = view & 0x1fffu; slice != 0) { + if (maxMip == 0 && viewMip != 0) Require(false, "depth view mip " + std::to_string(viewMip) + " on a surface without mips"); + if (maxMip != 0) { + char message[320]; + std::snprintf(message, sizeof(message), "depth view mip %u of %u at %ux%u (DB_Z_INFO 0x%08x, DB_DEPTH_VIEW 0x%08x)", viewMip, maxMip + 1u, depth.extent.width, depth.extent.height, read(cx, 0x010), view); + const std::string where(message); + Require(viewMip <= maxMip, "the " + where + " is beyond the surface's mips"); + Require(depth.samples == 1u, "multisampled " + where + " is unsupported"); + Require(depth.depthSwizzle == XorSwizzleMode(TextureTileMode::kZ64KBX), "the " + where + " uses a swizzle other than 64 KiB Z_X"); + const auto depthMips = ComputeElementMipLayout(TextureTileMode::kZ64KBX, zFormat == 1 ? 2u : 4u, depth.extent.width, depth.extent.height, maxMip + 1u); + const auto stencilMips = ComputeElementMipLayout(TextureTileMode::kZ64KBX, 1u, depth.extent.width, depth.extent.height, maxMip + 1u); + Require(zFormat == 0 || !depthMips.at(viewMip).tail, "rendering the " + where + " into the packed mip tail is unsupported"); + Require(!stencil || !stencilMips.at(viewMip).tail, "rendering the stencil of the " + where + " into the packed mip tail is unsupported"); + Require((read(cx, 0x010) & (1u << 29u)) == 0, "HTILE for the " + where + " is unsupported"); + if (const auto slice = view & 0x1fffu; slice != 0) { + if (depth.address != 0) depth.address += static_cast(slice) * ComputeSurfaceSize(depthMips, 1u); + if (depth.stencilAddress != 0) depth.stencilAddress += static_cast(slice) * ComputeSurfaceSize(stencilMips, 1u); + } + if (depth.address != 0) depth.address += depthMips.at(viewMip).tiledOffset; + if (depth.stencilAddress != 0) depth.stencilAddress += stencilMips.at(viewMip).tiledOffset; + depth.extent = {depthMips.at(viewMip).width, depthMips.at(viewMip).height}; + } else if (const auto slice = view & 0x1fffu; slice != 0) { if (depth.address != 0) depth.address += static_cast(slice) * DepthSliceBytes(depth.extent, zFormat == 1 ? 2u : 4u); if (depth.stencilAddress != 0) depth.stencilAddress += static_cast(slice) * DepthSliceBytes(depth.extent, 1u); } depth.format = zFormat == 1 ? (stencil ? VK_FORMAT_D16_UNORM_S8_UINT : VK_FORMAT_D16_UNORM) : (stencil ? VK_FORMAT_D32_SFLOAT_S8_UINT : VK_FORMAT_D32_SFLOAT); depth.clearDepth = readFloat(cx, 0x00b); depth.clearStencil = static_cast(read(cx, 0x00a) & 0xffu); + depth.htileDepthOnly = (read(cx, 0x011) & (1u << 29u)) != 0u; if (const auto htile = find(cx, 0x005); zFormat != 0 && htile != cx.end() && htile->second != 0 && (read(cx, 0x010) & (1u << 29u)) != 0) { - depth.htileAddress = base(0x005, 0x01e); - depth.htileStencil = stencil && (read(cx, 0x011) & (1u << 29u)) == 0; + depth.htileAddress = base(0x005, 0x01e) + static_cast(view & 0x1fffu) * HtileMetadataBytes(depth.extent); + depth.htileStencil = stencil && !depth.htileDepthOnly; } result.depth = depth; result.depthTest = (depthControl & 2u) != 0; @@ -301,8 +467,8 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx result.depthBoundsTest = (depthControl & 8u) != 0; if (result.depthBoundsTest) { Require(zFormat != 0, "depth bounds without a depth plane"); - result.minDepthBounds = readFloat(cx, 0x008); - result.maxDepthBounds = readFloat(cx, 0x009); + result.minDepthBounds = readDepthBound(cx, 0x008); + result.maxDepthBounds = readDepthBound(cx, 0x009); } if (depthClear) { result.depthTest = true; @@ -325,8 +491,8 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx result.stencilBack = clear; } else if (result.stencilTest) { const auto ops = read(cx, 0x10b); - result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly, testValExport); - result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly, testValExport) : result.stencilFront; + result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly, testValExport, opValExport, opValReference); + result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly, testValExport, opValExport, opValReference) : result.stencilFront; } } @@ -368,6 +534,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 && *primitive != 21u && !(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 { @@ -381,26 +570,41 @@ 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 and rectangles", true); +} + +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 CentroidInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid); +bool overrasterized(const Registers& cx, std::uint32_t samples) { + const auto eqaa = find(cx, 0x201); + const auto mode = find(cx, 0x292); + return samples > 1u && eqaa != cx.end() && mode != cx.end() && (mode->second & 1u) != 0 && ((eqaa->second >> 24u) & 7u) == static_cast(std::countr_zero(samples)); +} + +bool sampleCoverageSupported(const QueueState& queue, std::uint32_t samples, bool perSampleShading) { + if (samples == 1u || PixelProgramSkipped(queue)) return true; + const auto ena = find(queue.context, 0x1b3); + const auto addr = find(queue.context, 0x1b4); + if (ena == queue.context.end() || addr == queue.context.end()) return true; + const auto inputs = ena->second & addr->second; + return (inputs & PixelSampleCoverageInput) == 0u || (!perSampleShading && (inputs & PixelSampleRateInputs) == 0u); +} + bool coveredCentroidSupported(const QueueState& queue) { const auto config = find(queue.context, 0x2f8); if (config == queue.context.end() || (config->second & CoveredCentroidIsCenter) == 0 || PixelProgramSkipped(queue)) return true; @@ -409,6 +613,40 @@ bool coveredCentroidSupported(const QueueState& queue) { return ena != queue.context.end() && addr != queue.context.end() && (ena->second & addr->second & CentroidInputs) == 0; } +std::array sampleMaskFields(std::uint32_t top, std::uint32_t bottom) { + return {top & 0xffffu, top >> 16u, bottom & 0xffffu, bottom >> 16u}; +} + +std::string sampleMaskRejection(const QueueState& queue, std::uint32_t config, std::uint32_t top, std::uint32_t bottom) { + const auto log2 = config & 7u; + if (log2 > 3u) return {}; + const auto full = (1u << (1u << log2)) - 1u; + auto masks = sampleMaskFields(top, bottom); + for (auto& mask : masks) mask &= full; + if (std::ranges::all_of(masks, [&](std::uint32_t mask) { return mask == full; })) return {}; + const bool uniform = std::ranges::all_of(masks, [&](std::uint32_t mask) { return mask == masks[0]; }); + const bool wholePixels = std::ranges::all_of(masks, [&](std::uint32_t mask) { return mask == 0u || mask == full; }); + char detail[96]; + std::snprintf(detail, sizeof(detail), " (PA_SC_AA_MASK_X0Y0_X1Y0=0x%08x, PA_SC_AA_MASK_X0Y1_X1Y1=0x%08x)", top, bottom); + const auto conservative = find(queue.context, 0x313); + if (conservative != queue.context.end() && (conservative->second & ~0x001f6000u) != 0) return std::string("AGC graphics: sample masks with conservative rasterization are unsupported") + detail; + if (!wholePixels && uniform && !PixelProgramSkipped(queue)) { + const auto ena = find(queue.context, 0x1b3); + const auto addr = find(queue.context, 0x1b4); + if (ena != queue.context.end() && addr != queue.context.end() && (ena->second & addr->second & CentroidInputs) != 0) return std::string("AGC graphics: sample masks that remove some samples of a pixel with centroid interpolation are unsupported") + detail; + } + return {}; +} + +std::array decodeQuadSampleMask(const Registers& cx, std::uint32_t samples) { + const bool singleSample = (read(cx, 0x2f8) & 7u) == 0u; + const auto full = (1u << samples) - 1u; + const auto fields = sampleMaskFields(read(cx, 0x30e), read(cx, 0x30f)); + std::array masks{}; + for (std::size_t pixel = 0; pixel < masks.size(); ++pixel) masks[pixel] = static_cast(singleSample ? ((fields[pixel] & 1u) != 0u ? full : 0u) : fields[pixel] & full); + return masks; +} + std::uint32_t decodeSamples(const Registers& cx) { const auto config = read(cx, 0x2f8); zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u | CoveredCentroidIsCenter), "coverage conversion or nonstandard multisampling"); @@ -432,16 +670,30 @@ void decodeSampleLocations(const Registers& cx, State& state) { const auto bits = (word >> (8u * (sample % 4u))) & 0xffu; const auto x = static_cast(static_cast(bits << 4u)) >> 4; const auto y = static_cast(static_cast(bits & 0xf0u)) >> 4; - if (pixel == 0) { - state.sampleLocations[sample] = static_cast(bits); - state.customSampleLocations |= x != standard[sample][0] || y != standard[sample][1]; - } else { - Require(bits == state.sampleLocations[sample], "nonstandard sample locations varying between pixels are unsupported"); - } + state.sampleLocations[pixel * 8u + sample] = static_cast(bits); + state.customSampleLocations |= x != standard[sample][0] || y != standard[sample][1]; + state.sampleLocationGrid |= bits != state.sampleLocations[sample]; } } } +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; + return (inputs & refused) == 0; +} + +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); @@ -511,6 +763,142 @@ VkBlendOp blendOp(std::uint32_t value) { } } +constexpr std::uint8_t TermComplement = 0x80u; +constexpr std::uint8_t TermUnsupported = 0xffu; + +std::uint8_t exportTerm(std::uint32_t component) { + return static_cast(1u + component); +} + +std::uint8_t destinationTerm(std::uint32_t component) { + return static_cast(5u + component); +} + +std::uint8_t guestBlendTerm(VkBlendFactor factor, std::uint32_t component) { + switch (factor) { + case VK_BLEND_FACTOR_ZERO: return 0u; + case VK_BLEND_FACTOR_ONE: return TermComplement; + case VK_BLEND_FACTOR_SRC_COLOR: return exportTerm(component); + case VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR: return TermComplement | exportTerm(component); + case VK_BLEND_FACTOR_SRC_ALPHA: return exportTerm(0u); + case VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA: return TermComplement | exportTerm(0u); + case VK_BLEND_FACTOR_DST_COLOR: return destinationTerm(component); + case VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR: return TermComplement | destinationTerm(component); + case VK_BLEND_FACTOR_DST_ALPHA: return destinationTerm(0u); + case VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA: return TermComplement | destinationTerm(0u); + case VK_BLEND_FACTOR_SRC_ALPHA_SATURATE: return component == 0u ? TermComplement : TermUnsupported; + default: return TermUnsupported; + } +} + +std::uint8_t routedTerm(std::uint32_t route, std::uint32_t component) { + namespace Code = ShaderRecompiler::ReversedBlendCode; + switch (route) { + case Code::Own: return exportTerm(component); + case Code::Component0: return exportTerm(0u); + case Code::Zero: return 0u; + default: return TermComplement; + } +} + +std::uint8_t hostBlendTerm(VkBlendFactor factor, std::uint32_t component, const std::array& color, const std::array& second) { + switch (factor) { + case VK_BLEND_FACTOR_ZERO: return 0u; + case VK_BLEND_FACTOR_ONE: return TermComplement; + case VK_BLEND_FACTOR_SRC_COLOR: return routedTerm(color[component], component); + case VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR: return TermComplement ^ routedTerm(color[component], component); + case VK_BLEND_FACTOR_SRC_ALPHA: return routedTerm(color[3], 3u); + case VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA: return TermComplement ^ routedTerm(color[3], 3u); + case VK_BLEND_FACTOR_DST_COLOR: return destinationTerm(component); + case VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR: return TermComplement | destinationTerm(component); + case VK_BLEND_FACTOR_DST_ALPHA: return destinationTerm(3u); + case VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA: return TermComplement | destinationTerm(3u); + case VK_BLEND_FACTOR_SRC1_COLOR: return routedTerm(second[component], component); + case VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR: return TermComplement ^ routedTerm(second[component], component); + case VK_BLEND_FACTOR_SRC1_ALPHA: return routedTerm(second[3], 3u); + case VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA: return TermComplement ^ routedTerm(second[3], 3u); + default: return TermUnsupported; + } +} + +struct BlendRouting { + std::array factors; + std::uint32_t code; +}; + +std::optional routeReversedBlend(const std::array& color, const std::array& alpha) { + namespace Code = ShaderRecompiler::ReversedBlendCode; + static constexpr std::array colorFactors{ + VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_SRC_COLOR, VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_FACTOR_DST_COLOR, + VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR, VK_BLEND_FACTOR_DST_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA, VK_BLEND_FACTOR_SRC1_COLOR, VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR, VK_BLEND_FACTOR_SRC1_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA}; + static constexpr std::array alphaFactors{ + VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_FACTOR_DST_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA, VK_BLEND_FACTOR_SRC1_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA}; + std::array, 4> wanted{}; + std::uint32_t replaceable = 0; + for (std::uint32_t component = 0; component < 4; ++component) { + for (std::size_t position = 0; position < 2; ++position) { + wanted[component][position] = guestBlendTerm(component == 0u ? alpha[position] : color[position], component); + if (wanted[component][position] == TermUnsupported) return std::nullopt; + } + if (wanted[component][0] == TermComplement) replaceable |= 1u << component; + } + const auto search = [&](bool routed) -> std::optional { + const auto usable = [&](VkBlendFactor factor) { return routed || !secondSource(factor); }; + const std::uint32_t routeCount = routed ? 4u : 1u; + std::uint32_t ones = 0; + do { + std::array colorRoutes{}; + for (std::uint32_t component = 0; component < 4; ++component) colorRoutes[component] = ((ones >> component) & 1u) != 0 ? Code::One : Code::Own; + const auto matches = [&](VkBlendFactor factor, std::size_t position, std::uint32_t component, const std::array& second) { + const auto term = hostBlendTerm(factor, component, colorRoutes, second); + if (position == 1 || colorRoutes[component] == Code::Own) return term == wanted[component][position]; + return term == exportTerm(component); + }; + for (std::uint32_t secondAlpha = 0; secondAlpha < routeCount; ++secondAlpha) { + const std::array second{Code::Own, Code::Own, Code::Own, secondAlpha}; + std::array, 2> alphaFactor{}; + for (std::size_t position = 0; position < 2; ++position) { + const auto found = std::find_if(alphaFactors.begin(), alphaFactors.end(), [&](VkBlendFactor factor) { return usable(factor) && matches(factor, position, 3u, second); }); + if (found != alphaFactors.end()) alphaFactor[position] = *found; + } + if (!alphaFactor[0] || !alphaFactor[1]) continue; + std::array, colorFactors.size()>, 2> masks{}; + for (std::size_t position = 0; position < 2; ++position) { + for (std::size_t index = 0; index < colorFactors.size(); ++index) { + if (!usable(colorFactors[index])) continue; + for (std::uint32_t component = 0; component < 3; ++component) { + auto routes = second; + for (std::uint32_t route = 0; route < routeCount; ++route) { + routes[component] = route; + if (matches(colorFactors[index], position, component, routes)) masks[position][index][component] |= static_cast(1u << route); + } + } + } + } + for (std::size_t source = 0; source < colorFactors.size(); ++source) { + for (std::size_t destination = 0; destination < colorFactors.size(); ++destination) { + std::uint32_t code = Code::Enable; + for (std::uint32_t component = 0; component < 4; ++component) { + const auto shared = component == 3u ? 1u << secondAlpha : static_cast(masks[0][source][component] & masks[1][destination][component]); + if (shared == 0u) { + code = 0u; + break; + } + const auto route = static_cast(std::countr_zero(shared)); + code |= (colorRoutes[component] << (Code::ColorShift + Code::RouteBits * component)) | (route << (Code::SecondShift + Code::RouteBits * component)); + } + if (code != 0u) return BlendRouting{{colorFactors[source], colorFactors[destination], *alphaFactor[0], *alphaFactor[1]}, routed ? code : 0u}; + } + } + } + ones = (ones - replaceable) & replaceable; + } while (routed && ones != 0u); + return std::nullopt; + }; + if (auto native = search(false)) return native; + return search(true); +} + struct DecodedColorFormat { VkFormat format; std::uint32_t elementBytes; @@ -518,6 +906,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) { @@ -528,7 +920,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); @@ -547,6 +939,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, const auto mapping = static_cast(alternate ? 0xecu : 0xe4u); if (number == unorm) return {VK_FORMAT_R8G8_UNORM, 2, mapping}; if (number == snorm) return {VK_FORMAT_R8G8_SNORM, 2, mapping}; + if (number == uint) return {VK_FORMAT_R8G8_UINT, 2, mapping}; return fail(); } case 4: @@ -566,6 +959,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 != 0 && swap != 2) || number != floating) return fail(); + return {VK_FORMAT_R32_UINT, 4, static_cast(swap == 2 ? 0xc6u : 0xe4u), 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(); @@ -596,6 +992,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}; @@ -612,7 +1011,6 @@ void intersect(VkRect2D& result, const Registers& registers, std::uint32_t offse const auto y = (tl >> 16u) & (screen ? 0xffffu : 0x7fffu); const auto right = br & 0xffffu; const auto bottom = br >> 16u; - Require(x <= right && y <= bottom, "inverted scissor rectangle"); const auto oldRight = static_cast(result.offset.x) + result.extent.width; const auto oldBottom = static_cast(result.offset.y) + result.extent.height; const auto left = std::max(static_cast(result.offset.x), x); @@ -623,23 +1021,107 @@ void intersect(VkRect2D& result, const Registers& registers, std::uint32_t offse } -std::array SampleLocations(const State& state) { +VkExtent2D SampleLocationGrid(const State& state) { + return state.sampleLocationGrid ? VkExtent2D{2, 2} : VkExtent2D{1, 1}; +} + +std::optional SameSetSampleOrder(const State& state) { + if (!state.customSampleLocations || !state.sampleLocationGrid || state.samples < 2u || state.samples > 8u) return std::nullopt; + SampleOrder order{}; + for (std::uint32_t pixel = 0; pixel < order.size(); ++pixel) { + std::uint32_t word = KeptSampleOrder; + std::uint32_t taken = 0; + for (std::uint32_t sample = 0; sample < state.samples; ++sample) { + std::uint32_t guest = 0; + while (guest < state.samples && (((taken >> guest) & 1u) != 0u || state.sampleLocations[pixel * 8u + guest] != state.sampleLocations[sample])) ++guest; + if (guest == state.samples) return std::nullopt; + taken |= 1u << guest; + word = (word & ~(0xfu << (sample * 4u))) | (guest << (sample * 4u)); + } + order[pixel] = word; + } + return order; +} + +bool ReordersSamples(const SampleOrder& order) { + return std::ranges::any_of(order, [](std::uint32_t word) { return word != 0u && word != KeptSampleOrder; }); +} + +std::uint32_t InvertSampleOrder(std::uint32_t order) { + std::uint32_t inverse = 0; + for (std::uint32_t sample = 0; sample < 8u; ++sample) inverse |= sample << (((order >> (sample * 4u)) & 0x7u) * 4u); + return inverse; +} + +std::uint32_t HostSampleBits(std::uint32_t guestBits, std::uint32_t order) { + std::uint32_t host = 0; + for (std::uint32_t sample = 0; sample < 8u; ++sample) host |= ((guestBits >> ((order >> (sample * 4u)) & 0x7u)) & 1u) << sample; + return host | (guestBits & ~0xffu); +} + +bool SampleMaskFollowsOrder(const State& state, const SampleOrder& order) { + if (!ReordersSamples(order) || !PartialSampleMask(state) || !UniformSampleMask(state)) return false; + const auto full = (1u << state.samples) - 1u; + const auto mask = state.quadSampleMask[0] & full; + return std::ranges::any_of(order, [&](std::uint32_t word) { return (HostSampleBits(mask, word) & full) != mask; }); +} + +std::array QuadSampleMasks(const State& state, const SampleOrder& order) { + if (!SampleMaskFollowsOrder(state, order)) return QuadSampleMasks(state); + const auto mask = static_cast(state.quadSampleMask[0] & ((1u << state.samples) - 1u)); + return {mask, mask, mask, mask}; +} + +std::array SampleLocations(const State& state) { Require(state.samples == 1 || state.samples == 2 || state.samples == 4 || state.samples == 8, "invalid sample count"); - std::array locations{}; - for (std::uint32_t sample = 0; sample < state.samples; ++sample) { - std::array position{}; - if (state.customSampleLocations) { - const auto bits = state.sampleLocations[sample]; - position = {static_cast(bits & 7u) - static_cast(bits & 8u), static_cast((bits >> 4u) & 7u) - static_cast((bits >> 4u) & 8u)}; - } else if (state.samples != 1) { - const auto& standard = state.samples == 2 ? StandardLocations2[sample] : state.samples == 4 ? StandardLocations4[sample] : StandardLocations8[sample]; - position = {standard[0], standard[1]}; + std::array locations{}; + const auto grid = SampleLocationGrid(state); + for (std::uint32_t pixel = 0; pixel < grid.width * grid.height; ++pixel) { + for (std::uint32_t sample = 0; sample < state.samples; ++sample) { + std::array position{}; + if (state.customSampleLocations) { + const auto bits = state.sampleLocations[pixel * 8u + sample]; + position = {static_cast(bits & 7u) - static_cast(bits & 8u), static_cast((bits >> 4u) & 7u) - static_cast((bits >> 4u) & 8u)}; + } else if (state.samples != 1) { + const auto& standard = state.samples == 2 ? StandardLocations2[sample] : state.samples == 4 ? StandardLocations4[sample] : StandardLocations8[sample]; + position = {standard[0], standard[1]}; + } + locations[pixel * state.samples + sample] = {0.5f + position[0] / 16.0f, 0.5f + position[1] / 16.0f}; } - locations[sample] = {0.5f + position[0] / 16.0f, 0.5f + position[1] / 16.0f}; } return locations; } +std::array PixelSampleLocations(const State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel) { + using ShaderRecompiler::PixelInput; + using ShaderRecompiler::PixelInputBit; + std::array locations{}; + const bool sampleBarycentrics = (pixel.inputAddr & (PixelInputBit(PixelInput::PerspectiveSample) | PixelInputBit(PixelInput::LinearSample))) != 0u; + const bool samplePosition = !state.perSampleShading && (pixel.posX || pixel.posY) && pixel.positionLocation == ShaderRecompiler::PixelPositionLocation::Sample; + if (!sampleBarycentrics && !samplePosition) return locations; + const auto samples = state.perSampleShading ? state.samples : 1u; + for (std::uint32_t pixelIndex = 0; pixelIndex < 4u; ++pixelIndex) { + for (std::uint32_t sample = 0; sample < samples; ++sample) { + if (state.customSampleLocations) { + locations[pixelIndex * 8u + sample] = state.sampleLocations[pixelIndex * 8u + sample]; + } else if (state.samples != 1) { + const auto& standard = state.samples == 2 ? StandardLocations2[sample] : state.samples == 4 ? StandardLocations4[sample] : StandardLocations8[sample]; + locations[pixelIndex * 8u + sample] = static_cast((standard[0] & 0xf) | ((standard[1] & 0xf) << 4u)); + } + } + } + return locations; +} + +void ApplyPointSize(const State& state, ShaderRecompiler::ShaderVertexStageInfo& vertex) { + if (!state.vertexPoints && !(state.stages.mesh.has_value() && state.stages.mesh->outputPrimitiveSize == 1u)) return; + vertex.paClVsOutCntl |= state.paClVsOutCntl & (1u << 16u); + vertex.pointList = true; + vertex.pointSize = state.pointSize; + vertex.pointSizeMin = state.pointSizeMin; + vertex.pointSizeMax = state.pointSizeMax; +} + ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { const auto value = read(queue.context, 0x2d5); const auto validate = [&](bool condition, const char* reason) { @@ -661,7 +1143,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { APS5_LOG_OUT_DEBUG("DecodeShaderStages value=0x%x primitive=%u path=%u vertexWave=%u", value, primitive, static_cast(result.path), result.vertexWaveSize); if (path == ShaderPath::Vertex) { validate((value & 0x2000u) != 0, "legacy vertex routing without PRIMGEN_EN is unsupported"); - validate((value & ~0x0247a010u) == 0, "unsupported vertex routing, scheduling or wave-ID state"); + validate((value & ~0x0247a010u) == 0 || value == 0x02002008u, "unsupported vertex routing, scheduling or wave-ID state"); } else if (path == ShaderPath::Tessellation) { validate((value & 0x00600020u) == 0, "wave32 tessellation or geometry amplification is unsupported"); validate((value & ~0x0207ed0du) == 0 && (value & 0x02002000u) == 0x02002000u && (value & 3u) == 1u && ((value >> 3u) & 3u) == 1u, "unsupported tessellation routing"); @@ -672,7 +1154,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { validate(tess.inputControlPoints != 0 && tess.inputControlPoints <= 32 && tess.outputControlPoints != 0 && tess.outputControlPoints <= 32, "invalid tessellation control-point counts"); validate(tess.patchesPerGroup != 0, "VGT_LS_HS_CONFIG.NUM_PATCHES is zero"); if ((parameters & ~0x000600ffu) != 0) Require(false, zeroMessage(0x2db, parameters, "VGT_TF_PARAM donut, factor detection, memory type or reserved state")); - validate((tess.domain == 1 || tess.domain == 2) && (tess.partitioning == 0 || tess.partitioning == 2) && (tess.outputTopology == 2 || tess.outputTopology == 3), "only triangle and quad domains with integer or fractional-odd partitioning and triangle output are supported by the reference path"); + if (!((tess.domain == 1 || tess.domain == 2) && (tess.partitioning == 0 || tess.partitioning == 2 || tess.partitioning == 3) && (tess.outputTopology == 2 || tess.outputTopology == 3))) validate(false, tessellationMessage(config, parameters, tess).c_str()); result.minTessellationLevel = readFloat(queue.context, 0x287); result.maxTessellationLevel = readFloat(queue.context, 0x286); Require(result.minTessellationLevel >= 0.0f && result.minTessellationLevel <= 63.0f && result.minTessellationLevel <= result.maxTessellationLevel && result.maxTessellationLevel >= 1.0f, "tessellation level clamps need 0 <= VGT_HOS_MIN_TESS_LEVEL <= 63, VGT_HOS_MAX_TESS_LEVEL >= 1 and the minimum at most the maximum"); @@ -686,7 +1168,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 == 0 || outputType == 1) && !vertexWorkgroup), "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a point list or line strip from a geometry program, is unsupported"); + const auto outputSize = outputType == 0 ? 1u : 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; @@ -694,14 +1179,15 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { const auto groupPrimitives = std::min({primitives, (vertices - inputSize) / inputStep + 1u, outputLimit}); validate(groupPrimitives != 0, "geometry subgroup contains no primitives"); const auto resources = read(queue.shader, 0x8b, RegisterBank::Shader); - validate(vertexWorkgroup || fastLaunch || (((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3 && ((resources >> 16u) & 3u) == 3), "unsupported geometry VGPR allocation"); const auto esgsItemSize = read(queue.context, 0x2ab); validate(esgsItemSize != 0 && esgsItemSize * vertices <= 0xffffu, "invalid VGT_ESGS_RING_ITEMSIZE"); - const auto 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}; + const auto raster = find(queue.context, 0x205); + const auto provokingVertex = raster != queue.context.end() && (raster->second & (1u << 19u)) != 0 ? outputSize - 1u : 0u; + result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, maxPrimitives, ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, provokingVertex, esgsItemSize, outputSize}; } return result; } @@ -723,13 +1209,84 @@ 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"); } +ReversedBlend TranslateReversedBlend(std::uint32_t blendControl, std::uint8_t componentMapping, VkFormat format, bool forceDestinationAlphaOne) { + const auto refuse = [&](const char* reason) { + char text[320]; + std::snprintf(text, sizeof(text), "AGC graphics: blending into a color target with a reversed component order %s (CB_BLEND_CONTROL 0x%08x, component mapping 0x%02x, VkFormat %d)", reason, blendControl, componentMapping, static_cast(format)); + throw std::runtime_error(text); + }; + Require(componentMapping == 0x1bu || componentMapping == 0x93u, "a reversed-order blend needs the component mapping 0x1b or 0x93"); + if (format == VK_FORMAT_R8G8B8A8_SRGB) refuse("is not implemented for sRGB targets"); + if (format == VK_FORMAT_R8G8B8A8_SNORM || format == VK_FORMAT_R16G16B16A16_SNORM) refuse("is not implemented for signed normalized targets"); + if (format != VK_FORMAT_R8G8B8A8_UNORM && format != VK_FORMAT_R16G16B16A16_UNORM && format != VK_FORMAT_R16G16B16A16_SFLOAT) refuse("is not implemented for integer targets"); + const auto alphaControl = (blendControl & 0x20000000u) != 0 ? blendControl >> 16u : blendControl; + std::array color{blendFactor(blendControl & 0x1fu), blendFactor((blendControl >> 8u) & 0x1fu)}; + std::array alpha{blendFactor(alphaControl & 0x1fu), blendFactor((alphaControl >> 8u) & 0x1fu)}; + const auto operation = blendOp((blendControl >> 5u) & 7u); + const auto alphaOperation = blendOp((alphaControl >> 5u) & 7u); + const auto any = [&](auto predicate) { return predicate(color[0]) || predicate(color[1]) || predicate(alpha[0]) || predicate(alpha[1]); }; + const auto constant = [](VkBlendFactor factor) { return factor == VK_BLEND_FACTOR_CONSTANT_COLOR || factor == VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR || factor == VK_BLEND_FACTOR_CONSTANT_ALPHA || factor == VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA; }; + if (any(secondSource)) refuse("with a second-source factor is not implemented, as dual-source blending into a component-swapped color target is unsupported"); + if (any(constant)) refuse("with a constant blend factor is not implemented"); + if (operation != alphaOperation) refuse("with different color and alpha operations is not implemented"); + if (forceDestinationAlphaOne) { + for (auto& factor : color) factor = colorFactorWithOpaqueDestination(factor); + for (auto& factor : alpha) factor = alphaFactorWithOpaqueDestination(factor); + } + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_TRUE; + blend.colorBlendOp = operation; + blend.alphaBlendOp = operation; + if (operation == VK_BLEND_OP_MIN || operation == VK_BLEND_OP_MAX) { + if (any([](VkBlendFactor factor) { return factor != VK_BLEND_FACTOR_ONE; })) refuse("with a MIN or MAX operation whose factors are not ONE is not implemented"); + blend.srcColorBlendFactor = blend.dstColorBlendFactor = blend.srcAlphaBlendFactor = blend.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + return {blend, 0u}; + } + for (const auto factor : color) { + if (factor == VK_BLEND_FACTOR_DST_ALPHA || factor == VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA || factor == VK_BLEND_FACTOR_SRC_ALPHA_SATURATE) refuse("with a factor of the destination alpha in the color equation, which the target keeps in its first channel, is not implemented"); + } + const auto routing = routeReversedBlend(color, alpha); + if (!routing) refuse("with factors the host blend cannot form from copies of the export is not implemented"); + blend.srcColorBlendFactor = routing->factors[0]; + blend.dstColorBlendFactor = routing->factors[1]; + blend.srcAlphaBlendFactor = routing->factors[2]; + blend.dstAlphaBlendFactor = routing->factors[3]; + return {blend, routing->code}; +} + +void ApplyDepthRemap(const Context& context, State& state) { + const auto& viewport = state.viewport; + const bool inRange = viewport.minDepth >= 0 && viewport.minDepth <= 1 && viewport.maxDepth >= 0 && viewport.maxDepth <= 1; + if (context.depthRangeUnrestricted || inRange) return; + Require(state.stages.path == ShaderPath::Vertex && !state.stages.mesh && !state.stages.tessellation, "viewport depth outside [0, 1] is remapped on the vertex path only"); + Require(context.depthClamp, "viewport depth outside [0, 1] needs depth clamping to stand in for the console's clamp"); + const float clampMin = std::clamp(state.depthClampMin, 0.0f, 1.0f); + const float clampMax = std::clamp(state.depthClampMax, 0.0f, 1.0f); + const bool floatDepth = state.depth.has_value() && (state.depth->format == VK_FORMAT_D32_SFLOAT || state.depth->format == VK_FORMAT_D32_SFLOAT_S8_UINT); + if ((clampMin != state.depthClampMin || clampMax != state.depthClampMax) && floatDepth && (state.depthWrite || state.depthTest)) { + static bool reported = false; + if (!reported) { + reported = true; + std::fprintf(stderr, "[gpu] viewport depth clamp bounds [%g, %g] on a float depth target are clipped to [0, 1]: the device has no VK_EXT_depth_range_unrestricted\n", state.depthClampMin, state.depthClampMax); + } + } + const float range = clampMax - clampMin; + const float scale = range > 0.0f ? state.depthScale / range : state.depthScale; + const float offset = range > 0.0f ? (state.depthOffset - clampMin) / range : state.depthOffset; + state.depthRemap = {state.negativeOneToOne ? 2u : 1u, scale, offset}; + state.viewport.minDepth = clampMin; + state.viewport.maxDepth = clampMax; + state.negativeOneToOne = false; + state.depthClamp = true; +} + State DecodeState(const QueueState& queue, bool vertexWorkgroup) { if (auto reason = DepthMaintenanceRejection(queue); !reason.empty()) throw std::runtime_error(reason); const auto& cx = queue.context; @@ -739,7 +1296,11 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const auto primitive = read(queue.userConfig, 0x242, RegisterBank::UserConfig); APS5_LOG_OUT_DEBUG("DecodeState primitive=%u path=%u vertexWave=%u", primitive, static_cast(result.stages.path), result.stages.vertexWaveSize); switch (primitive) { - case 1: Require(result.stages.mesh.has_value(), "point-list vertex rendering requires point-size output support"); result.topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST; break; + case 1: + Require(result.stages.mesh.has_value() || result.stages.path == ShaderPath::Vertex, "point lists are only drawn through the vertex and geometry paths"); + result.topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST; + result.vertexPoints = result.stages.path == ShaderPath::Vertex; + break; case 2: result.topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST; break; case 3: result.topology = VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; break; case 7: @@ -750,15 +1311,24 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { break; case 9: result.topology = VK_PRIMITIVE_TOPOLOGY_PATCH_LIST; break; case 4: result.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; break; - case 5: result.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN; break; + case 5: + case 21: + result.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN; + break; case 6: result.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; break; + case 19: + Require(result.stages.path == ShaderPath::Vertex, "quad-list requires vertex routing"); + result.quadList = true; + result.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + break; default: throw std::runtime_error("AGC graphics: unsupported primitive type " + std::to_string(primitive)); } APS5_LOG_OUT_DEBUG("Topology=%u", static_cast(result.topology)); - result.primitiveRestart = read(queue.userConfig, 0x24b, RegisterBank::UserConfig) != 0 && !result.rectList && result.topology != VK_PRIMITIVE_TOPOLOGY_PATCH_LIST; - zero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports"); + result.primitiveRestart = read(queue.userConfig, 0x24b, RegisterBank::UserConfig) != 0 && !result.rectList && !result.quadList && result.topology != VK_PRIMITIVE_TOPOLOGY_PATCH_LIST; + zero(cx, 0x207, ~(LayerExports | ClipCullExports | ((result.vertexPoints || (result.stages.mesh.has_value() && result.stages.mesh->outputPrimitiveSize == 1u)) ? 1u << 16u : inactiveVertexPointSize(queue))), "layer, viewport or auxiliary vertex exports"); Require(std::popcount(read(cx, 0x207) & 0xffffu) <= 8, "more than eight clip and cull distances are unsupported"); result.paClVsOutCntl = read(cx, 0x207); + if (result.vertexPoints || (result.stages.mesh.has_value() && result.stages.mesh->outputPrimitiveSize == 1u)) decodePointSize(queue, result); result.samples = decodeSamples(cx); Require(coveredCentroidSupported(queue), "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs is not implemented"); decodeSampleLocations(cx, result); @@ -767,8 +1337,8 @@ 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))) { - decodeDepth(cx, depthControl, (read(cx, 0x203) & 2u) != 0 && !PixelProgramSkipped(queue), result); + if ((clear || tests != 0) && depthSurfaceBound(cx)) { + decodeDepth(cx, depthControl, (read(cx, 0x203) & 2u) != 0 && !PixelProgramSkipped(queue), (read(cx, 0x203) & 4u) != 0 && !PixelProgramSkipped(queue), result); } else if (depthPassThrough(depthControl) || ((depthControl & 0xbu) != 0 && (tests == 0 || depthPlanesAbsent(cx)))) { static bool reported = false; if (!reported) { @@ -797,20 +1367,50 @@ 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 (depthKeepsSampleZero && (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; + const auto sampleInputs = read(cx, 0x1b3) & read(cx, 0x1b4) & (ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage)); + Require(filled && !result.alphaToCoverage && (PixelProgramSkipped(queue) || sampleInputs == 0) && (read(cx, 0x203) & 0x100u) == 0 && (read(cx, 0x201) & 0x0f0c0000u) == 0, "a one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is unsupported for points or lines and with alpha-to-coverage, a sample index or coverage input, a sample-mask export, or DB_EQAA Z interpolation or overrasterization"); + result.samples = 1; + result.customSampleLocations = false; + result.sampleLocations.fill(0); + result.sampleLocationGrid = false; + } + if (result.samples > 1 && (read(cx, 0x292) & 1u) == 0) { + Require(RasterPrimitiveOf(result) != ShaderRecompiler::RasterPrimitive::Point && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points or alpha-to-coverage"); + if (const auto eqaa = find(cx, 0x201); result.depth && eqaa != cx.end() && (eqaa->second & 0x0f0c0000u) != 0) throw std::runtime_error(zeroMessage(0x201, eqaa->second, "a multisampled depth target rasterized without MSAA_ENABLE with Z interpolation or overrasterization (DB_EQAA)")); + if (const auto control = find(cx, 0x203); result.depth && control != cx.end() && (control->second & 0x100u) != 0) throw std::runtime_error(zeroMessage(0x203, control->second, "a multisampled depth target rasterized without MSAA_ENABLE with a sample-mask export")); + 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, "sample iteration, primitive discard or out-of-order rasterization"); + zero(cx, 0x293, ScanControlMask, "primitive discard or out-of-order rasterization"); + result.perSampleShading = result.samples > 1 && (read(cx, 0x293) & PixelShaderIteratesSamples) != 0; + Require(sampleCoverageSupported(queue, result.samples, result.perSampleShading), "SAMPLE_COVERAGE in a multisampled pixel shader that runs per sample, or reads sample-rate or ancillary inputs, is unsupported (the input holds the whole pixel's coverage, gl_SampleMaskIn only the invocation's samples)"); + if (depthKeepsSampleZero && result.samples > 1) { + 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"); Require((read(cx, 0x8c) & 0xfu) == 0xau, "nonstandard triangle edge rules are unsupported"); Require(read(cx, 0x2f9) == 0x2du, "nonstandard pixel center or vertex quantization is unsupported"); - Require(read(cx, 0x30e) == 0xffffffffu && read(cx, 0x30f) == 0xffffffffu, "sample masks are unsupported"); + if (auto reason = sampleMaskRejection(queue, read(cx, 0x2f8), read(cx, 0x30e), read(cx, 0x30f)); !reason.empty()) throw std::runtime_error(reason); const auto viewportControl = read(cx, 0x206); if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl)); zero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); @@ -819,12 +1419,13 @@ 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")); + const auto rasterMode = raster & ~0x7u & ~0x7f8u & ~(1u << 24u) & ~0x1800u & ~lineOffset & ~(1u << 19u) & ~(1u << 16u); + if (rasterMode != 0) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "depth bias or nonstandard rasterization")); result.cullMode = ((raster & 1u) != 0 ? VK_CULL_MODE_FRONT_BIT : 0u) | ((raster & 2u) != 0 ? VK_CULL_MODE_BACK_BIT : 0u); if (result.rectList) result.cullMode = VK_CULL_MODE_NONE; + result.polygonMode = decodePolygonMode(cx, raster, result, pointsOrLines); if ((raster & 0x1800u) != 0 && result.depth) decodeDepthBias(cx, raster, result); result.frontFace = (raster & 4u) != 0 ? VK_FRONT_FACE_CLOCKWISE : VK_FRONT_FACE_COUNTER_CLOCKWISE; result.provokingVertexMode = (raster & (1u << 19u)) != 0 ? VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT : VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; @@ -865,12 +1466,24 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { if (slotExport == 7 && ((read(cx, 0x31c + slot * 0xfu) >> 8u) & 7u) != 4u) throw std::runtime_error("AGC graphics: color export format 7 (UINT16_ABGR) into a target that is not unsigned integer is unsupported"); color.exportIndex = index; color.uintExport = slotExport == 7; + if (color.samples == 1 && result.samples > 1 && result.colors.empty() && !result.depth && (read(cx, 0x292) & 1u) == 0) { + const auto sampleInputs = read(cx, 0x1b3) & read(cx, 0x1b4) & (ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage)); + Require(!result.perSampleShading && sampleInputs == 0 && (read(cx, 0x203) & 0x100u) == 0, "a one-sample color target under a multisampled rasterizer without MSAA_ENABLE is unsupported with PS_ITER_SAMPLE, a sample index or coverage input, or a sample-mask export"); + result.samples = 1; + result.customSampleLocations = false; + } if (color.samples > 1 && result.samples == 1 && result.colors.empty() && !result.depth && !result.alphaToCoverage && (read(cx, 0x292) & 1u) == 0) { + Require(result.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT || (read(cx, 0x1b3) & 0x44u) == 0, "conservative rasterization with centroid interpolation of a draw raised to a multisampled target is unsupported"); result.samples = color.samples; result.customSampleLocations = true; result.sampleLocations.fill(0); } - Require(color.samples == result.samples, "a color target's sample count differs from the rasterizer's"); + const bool merged = color.samples == 1u && !result.depth && !result.perSampleShading && (result.colors.empty() || result.mergedCoverage) && overrasterized(cx, result.samples); + if (!merged && color.samples == 1 && result.colors.empty() && !result.depth && rasterizesSampleZeroOnly(cx, result)) rasterizeSampleZero(result); + const bool truncated = color.samples == 1u && result.samples > 1u && result.depth && result.depth->samples == result.samples && (result.colors.empty() || result.truncatedCoverage) && !overrasterized(cx, result.samples); + Require((color.samples == result.samples && !result.mergedCoverage && !result.truncatedCoverage) || merged || truncated, "a color target's sample count differs from the rasterizer's"); + result.mergedCoverage = merged; + result.truncatedCoverage = truncated; 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()) { result.renderExtent = color.extent; @@ -893,10 +1506,11 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } else { const auto screenBottomRight = read(cx, 0xd); APS5_LOG_OUT_DEBUG("No color target, screen BR register=0x%x", screenBottomRight); - result.renderExtent = {screenBottomRight & 0xffffu, screenBottomRight >> 16u}; + result.renderExtent = {std::max(screenBottomRight & 0xffffu, 1u), std::max(screenBottomRight >> 16u, 1u)}; APS5_LOG_OUT_DEBUG("Render extent from screen=%ux%u", result.renderExtent.width, result.renderExtent.height); - Require(result.renderExtent.width != 0 && result.renderExtent.height != 0, "empty framebuffer extent for a draw without color writes"); } + result.quadSampleMask = decodeQuadSampleMask(cx, result.samples); + result.pixelEarlyZ = !PixelProgramSkipped(queue) && PixelEarlyZ(read(cx, 0x203)); if ((read(cx, 0x207) & LayerExports & ~result.layerExports & ~(1u << 21u)) != 0) { static bool reported = false; if (!reported) { @@ -921,6 +1535,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { throw std::runtime_error(message.str()); } Require(readFloat(cx, 0xb4) <= readFloat(cx, 0xb5), "inverted viewport depth clamp bounds"); + result.depthScale = zs; + result.depthOffset = zo; + result.depthClampMin = readFloat(cx, 0xb4); + result.depthClampMax = readFloat(cx, 0xb5); result.viewport = collapsed ? VkViewport{xo, yo, 1, 1, minDepth, maxDepth} : VkViewport{xo - xs, yo - ys, 2 * xs, 2 * ys, minDepth, maxDepth}; if (const auto renderControl = find(cx, 0x000); result.depth && renderControl != cx.end() && (renderControl->second & 1u) != 0) { result.viewport.minDepth = result.depth->clearDepth; @@ -950,26 +1568,38 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { if (((exportedMask >> ((mapping >> (2u * component)) & 3u)) & 1u) != 0) state.colorWriteMask |= 1u << component; } state.blendEnable = ((blend >> 30u) & 1u) != 0 && (read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0; - if (state.blendEnable && (mapping == 0x1bu || mapping == 0x93u)) throw std::runtime_error("AGC graphics: blending into a color target with a reversed component order is not implemented"); Require(!state.blendEnable || !color.uintExport, "blending into an unsigned integer target is unsupported"); + const bool reversed = mapping == 0x1bu || mapping == 0x93u; if (state.blendEnable) { - state.srcColorBlendFactor = blendFactor(blend & 0x1fu); - state.dstColorBlendFactor = blendFactor((blend >> 8u) & 0x1fu); - state.colorBlendOp = blendOp((blend >> 5u) & 7u); - const auto alpha = (blend & 0x20000000u) != 0 ? blend >> 16u : blend; - state.srcAlphaBlendFactor = blendFactor(alpha & 0x1fu); - state.dstAlphaBlendFactor = blendFactor((alpha >> 8u) & 0x1fu); - state.alphaBlendOp = blendOp((alpha >> 5u) & 7u); - if ((mapping & 3u) == 3u) { - state.srcColorBlendFactor = state.srcAlphaBlendFactor; - state.dstColorBlendFactor = state.dstAlphaBlendFactor; - state.colorBlendOp = state.alphaBlendOp; - } - if ((read(cx, 0x31d + slot * 0xfu) & 0x20000u) != 0) { - state.srcColorBlendFactor = colorFactorWithOpaqueDestination(state.srcColorBlendFactor); - state.dstColorBlendFactor = colorFactorWithOpaqueDestination(state.dstColorBlendFactor); - state.srcAlphaBlendFactor = alphaFactorWithOpaqueDestination(state.srcAlphaBlendFactor); - state.dstAlphaBlendFactor = alphaFactorWithOpaqueDestination(state.dstAlphaBlendFactor); + const bool opaqueDestination = (read(cx, 0x31d + slot * 0xfu) & 0x20000u) != 0; + if (reversed) { + const auto translated = TranslateReversedBlend(blend, mapping, color.format, opaqueDestination); + state.srcColorBlendFactor = translated.blend.srcColorBlendFactor; + state.dstColorBlendFactor = translated.blend.dstColorBlendFactor; + state.colorBlendOp = translated.blend.colorBlendOp; + state.srcAlphaBlendFactor = translated.blend.srcAlphaBlendFactor; + state.dstAlphaBlendFactor = translated.blend.dstAlphaBlendFactor; + state.alphaBlendOp = translated.blend.alphaBlendOp; + if (translated.shaderCode != 0u) result.reversedBlend = translated.shaderCode; + } else { + state.srcColorBlendFactor = blendFactor(blend & 0x1fu); + state.dstColorBlendFactor = blendFactor((blend >> 8u) & 0x1fu); + state.colorBlendOp = blendOp((blend >> 5u) & 7u); + const auto alpha = (blend & 0x20000000u) != 0 ? blend >> 16u : blend; + state.srcAlphaBlendFactor = blendFactor(alpha & 0x1fu); + state.dstAlphaBlendFactor = blendFactor((alpha >> 8u) & 0x1fu); + state.alphaBlendOp = blendOp((alpha >> 5u) & 7u); + if ((mapping & 3u) == 3u) { + state.srcColorBlendFactor = state.srcAlphaBlendFactor; + state.dstColorBlendFactor = state.dstAlphaBlendFactor; + state.colorBlendOp = state.alphaBlendOp; + } + if (opaqueDestination) { + state.srcColorBlendFactor = colorFactorWithOpaqueDestination(state.srcColorBlendFactor); + state.dstColorBlendFactor = colorFactorWithOpaqueDestination(state.dstColorBlendFactor); + state.srcAlphaBlendFactor = alphaFactorWithOpaqueDestination(state.srcAlphaBlendFactor); + state.dstAlphaBlendFactor = alphaFactorWithOpaqueDestination(state.dstAlphaBlendFactor); + } } for (std::uint32_t i = 0; i < 4; ++i) result.blendConstants[i] = readFloat(cx, 0x105 + i); const auto constantColor = [](VkBlendFactor factor) { return factor == VK_BLEND_FACTOR_CONSTANT_COLOR || factor == VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR; }; @@ -990,22 +1620,29 @@ 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 && (secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor))) { + 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) { if ((requiredBlendMask & (1u << i)) != 0) result.blendConstants[i] = requiredBlendConstants[i]; } if (!result.colors.empty()) result.blend = result.blends[result.colors.front().exportIndex]; + Require(result.reversedBlend == 0 || (exportSlots.size() == 1 && exportCount == 1), "blending into a color target with a reversed component order through a second source color needs that target to be the shader's only color export"); if (result.dualSourceBlend) { Require(exportSlots.size() == 2 && exportSlots[0] == 0 && exportSlots[1] == 1 && exportCount == 1, "dual-source blending needs pixel exports to MRT slots 0 and 1 and color writes to slot 0 alone"); Require(result.colors.front().componentMapping == 0xe4u, "dual-source blending into a component-swapped color target is unsupported"); @@ -1031,8 +1668,47 @@ std::array ExportPackings(const State& return packings; } +bool PartialSampleMask(const State& state) { + const auto full = (1u << state.samples) - 1u; + return std::ranges::any_of(state.quadSampleMask, [&](std::uint16_t mask) { return (mask & full) != full; }); +} + +bool UniformSampleMask(const State& state) { + const auto full = (1u << state.samples) - 1u; + return std::ranges::all_of(state.quadSampleMask, [&](std::uint16_t mask) { return (mask & full) == (state.quadSampleMask[0] & full); }); +} + +VkSampleMask HostSampleMask(const State& state) { + const auto full = (1u << state.samples) - 1u; + return UniformSampleMask(state) ? state.quadSampleMask[0] & full : full; +} + +std::uint8_t QuadPixelMask(const State& state) { + if (!PartialSampleMask(state) || UniformSampleMask(state)) return 0xfu; + const auto full = (1u << state.samples) - 1u; + std::uint8_t pixels = 0; + for (std::uint32_t pixel = 0; pixel < state.quadSampleMask.size(); ++pixel) { + if ((state.quadSampleMask[pixel] & full) != 0u) pixels = static_cast(pixels | (1u << pixel)); + } + return pixels; +} + +std::array QuadSampleMasks(const State& state) { + std::array masks{0xffffu, 0xffffu, 0xffffu, 0xffffu}; + if (!PartialSampleMask(state) || UniformSampleMask(state)) return masks; + const auto full = (1u << state.samples) - 1u; + if (std::ranges::all_of(state.quadSampleMask, [&](std::uint16_t mask) { return (mask & full) == 0u || (mask & full) == full; })) return masks; + for (std::size_t pixel = 0; pixel < masks.size(); ++pixel) { + const auto mask = state.quadSampleMask[pixel] & full; + masks[pixel] = static_cast(mask == full ? 0xffffu : mask); + } + return masks; +} + ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state) { if (state.rectList) return ShaderRecompiler::RasterPrimitive::Rectangle; + if (state.stages.mesh.has_value() && state.stages.mesh->outputPrimitiveSize == 1u) return ShaderRecompiler::RasterPrimitive::Point; + 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; @@ -1076,7 +1752,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; @@ -1101,7 +1778,7 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) { return surface; } -ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { +ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allowMultisampleDcc) { const auto stride = slot * 0xfu; ColorTarget color{}; color.slot = slot; @@ -1117,10 +1794,9 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { 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(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; @@ -1137,7 +1813,11 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { Require(((attrib3 >> 24u) & 3u) != 0u || (attrib2 & 0x3fffu) == 0u, "1D color targets taller than one row are unsupported"); if (volume) { color.depth = (attrib3 & 0x1fffu) + 1u; - Require(maxMip == 0, "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; @@ -1148,7 +1828,13 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { } if (!volume && lastSlice != slice) { const auto exports = find(cx, 0x207); - Require(exports == cx.end() || (exports->second & (1u << 18u)) == 0, "layered draws into a color view of several array slices are unsupported"); + if (exports != cx.end() && (exports->second & (1u << 18u)) != 0) { + if (color.arrayLastSlice == 0) { + color.arrayLastSlice = lastSlice; + color.arraySlice = slice; + } + color.layers = lastSlice + 1u - slice; + } } color.extent = {((attrib2 >> 14u) & 0x3fffu) + 1u, (attrib2 & 0x3fffu) + 1u}; color.elementBytes = decoded.elementBytes; @@ -1156,13 +1842,19 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { 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); @@ -1191,25 +1883,54 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { 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; + if (const auto fmask = find(cx, 0x321 + stride); color.samples > 1u && fmask != cx.end()) { + const auto high = find(cx, 0x3a0 + slot); + const auto fmaskHigh = high == cx.end() ? 0u : high->second; + Require((fmaskHigh & ~0xffu) == 0u, "invalid FMASK address extension"); + const auto address = (static_cast(fmaskHigh) << 40u) | (static_cast(fmask->second) << 8u); + color.fmaskPipeBankXor = static_cast(address & 0xffffu); + color.fmaskAddress = address - color.fmaskPipeBankXor; + } } if ((info & 0x10000000u) != 0) { if (maxMip == 0) { @@ -1217,6 +1938,11 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { 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) { @@ -1232,6 +1958,61 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { return color; } +namespace { + +bool drawCovers(const Registers& cx, VkExtent2D extent) { + const auto viewportControl = read(cx, 0x206); + if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl)); + const auto xs = std::fabs(readFloat(cx, 0x10f)); + const auto xo = readFloat(cx, 0x110); + const auto ys = std::fabs(readFloat(cx, 0x111)); + const auto yo = readFloat(cx, 0x112); + VkRect2D covered{{0, 0}, {0x7fffu, 0x7fffu}}; + intersect(covered, cx, 0xc, true); + intersect(covered, cx, 0x81, false); + intersect(covered, cx, 0x90, false); + if ((read(cx, 0x292) & 2u) != 0) intersect(covered, cx, 0x94, false); + const auto width = static_cast(extent.width); + const auto height = static_cast(extent.height); + const bool viewportCovers = xo - xs <= 0.0f && xo + xs >= width && yo - ys <= 0.0f && yo + ys >= height; + const bool scissorCovers = covered.offset.x == 0 && covered.offset.y == 0 && covered.extent.width >= extent.width && covered.extent.height >= extent.height; + return viewportCovers && scissorCovers; +} + +} + +std::optional DecodeDepthCopyPass(const QueueState& queue) { + const auto& cx = queue.context; + const auto control = find(cx, 0x000); + if (control == cx.end() || (control->second & 4u) == 0) return std::nullopt; + Require((control->second & ~0xf84u) == 0, "DB depth copy combined with a stencil copy, clear, resummarize or decompress"); + Require((read(cx, 0x200) & 0xfu) == 0, "DB depth copy with depth or stencil tests"); + Require((read(cx, 0x010) & 3u) == 3u, "DB depth copy from a depth plane that is not Z_32_FLOAT"); + Require(ColorWriteMask(cx) == 1u, "DB depth copy into anything but the red channel of color target 0"); + const auto viewRegister = find(cx, 0x002); + const auto view = viewRegister == cx.end() ? 0u : viewRegister->second; + Require((read(cx, 0x010) & 0x000f1000u) == 0 && (view & 0x3c000000u) == 0, "DB depth copy from a mipmapped or partially resident depth plane"); + DepthCopyPass pass{}; + const auto high = find(cx, 0x01a); + pass.depthAddress = (high == cx.end() ? 0ull : static_cast(high->second & 0xffu) << 40u) | (static_cast(read(cx, 0x012)) << 8u); + const auto size = read(cx, 0x007); + pass.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u}; + pass.samples = 1u << ((read(cx, 0x010) >> 2u) & 3u); + if (pass.samples != 1u) pass.depthAddress &= ~std::uint64_t{65535u}; + if (const auto slice = view & 0x1fffu; slice != 0) pass.depthAddress += static_cast(slice) * DepthSliceBytes(pass.extent, 4u); + const auto maskLow = find(cx, 0x30e); + const auto maskHigh = find(cx, 0x30f); + Require((maskLow == cx.end() || maskLow->second == 0xffffffffu) && (maskHigh == cx.end() || maskHigh->second == 0xffffffffu), "DB depth copy with a sample mask is unsupported"); + pass.sample = (control->second & 0x80u) != 0 ? 0u : (control->second >> 8u) & 0xfu; + Require(pass.sample < pass.samples, "DB depth copy of a sample the depth plane does not have"); + pass.color = DecodeColorBuffer(cx, 0); + Require(pass.color.format == VK_FORMAT_R32_SFLOAT && pass.color.samples == 1 && pass.color.mip == 0 && pass.color.layers == 1, "DB depth copy into a color target that is not a single-sample R32 float"); + Require(pass.color.extent.width == pass.extent.width && pass.color.extent.height == pass.extent.height, "DB depth copy into a color target of another size"); + Require(pass.color.cmaskAddress == 0, "DB depth copy into a color target with CMASK fast clears is unsupported"); + Require(drawCovers(cx, pass.extent), "DB depth copy over part of the depth surface"); + return pass; +} + std::optional DecodeColorMetadataPass(const QueueState& queue) { const auto& cx = queue.context; const auto control = find(cx, 0x202); @@ -1242,8 +2023,9 @@ std::optional DecodeColorMetadataPass(const QueueState& queue const auto sourceAttrib = find(cx, 0x31d); const bool sourceSamples = sourceAttrib != cx.end() && ((sourceAttrib->second >> 12u) & 7u) != 0; const bool resolve = mode == 3u && (rasterizerSamples || sourceSamples); - if (mode != 2u && mode != 6u && !resolve) return std::nullopt; - ColorMetadataPass pass{resolve ? ColorMetadataPass::Mode::Resolve : mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}, std::nullopt}; + const bool fmaskDecompress = mode == 5u; + if (mode != 2u && mode != 6u && !resolve && !fmaskDecompress) return std::nullopt; + ColorMetadataPass pass{resolve ? ColorMetadataPass::Mode::Resolve : fmaskDecompress ? ColorMetadataPass::Mode::FmaskDecompress : mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}, std::nullopt}; Require((control->second & ~0x70u) == 0xcc0000u, "CB metadata pass with a nonstandard ROP, dual quads disabled or degamma"); const auto value = [&](std::uint32_t offset) { const auto found = find(cx, offset); @@ -1257,27 +2039,12 @@ std::optional DecodeColorMetadataPass(const QueueState& queue std::snprintf(text, sizeof(text), "CB metadata pass with depth or stencil work (DB_DEPTH_CONTROL 0x%08x, DB_RENDER_CONTROL 0x%08x, DB_Z_INFO 0x%08x, DB_STENCIL_INFO 0x%08x)", value(0x200), value(0x0), value(0x010), value(0x011)); throw std::runtime_error(text); } - const auto samples = resolve && rasterizerSamples ? decodeSamples(cx) : 1u; + const auto modeControl = find(cx, 0x292); + const bool multisampleRaster = modeControl != cx.end() && (modeControl->second & 1u) != 0; + const auto samples = resolve && rasterizerSamples && multisampleRaster ? decodeSamples(cx) : 1u; if (!resolve) zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); zero(cx, 0x80, ~0u, "window offset"); - const auto viewportControl = read(cx, 0x206); - if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl)); - const auto xs = std::fabs(readFloat(cx, 0x10f)); - const auto xo = readFloat(cx, 0x110); - const auto ys = std::fabs(readFloat(cx, 0x111)); - const auto yo = readFloat(cx, 0x112); - VkRect2D covered{{0, 0}, {0x7fffu, 0x7fffu}}; - intersect(covered, cx, 0xc, true); - intersect(covered, cx, 0x81, false); - intersect(covered, cx, 0x90, false); - if ((read(cx, 0x292) & 2u) != 0) intersect(covered, cx, 0x94, false); - const auto requireCovered = [&](const ColorTarget& target) { - const auto width = static_cast(target.extent.width); - const auto height = static_cast(target.extent.height); - const bool viewportCovers = xo - xs <= 0.0f && xo + xs >= width && yo - ys <= 0.0f && yo + ys >= height; - const bool scissorCovers = covered.offset.x == 0 && covered.offset.y == 0 && covered.extent.width >= target.extent.width && covered.extent.height >= target.extent.height; - Require(viewportCovers && scissorCovers, "CB metadata pass over part of a color target"); - }; + const auto requireCovered = [&](const ColorTarget& target) { Require(drawCovers(cx, target.extent), "CB metadata pass over part of a color target"); }; if (resolve) { const auto source = DecodeColorBuffer(cx, 0); const auto destination = DecodeColorBuffer(cx, 1); @@ -1285,6 +2052,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); @@ -1293,8 +2061,13 @@ std::optional DecodeColorMetadataPass(const QueueState& queue const auto targetMask = read(cx, 0x8e); for (std::uint32_t slot = 0; slot < 8; ++slot) { if (((targetMask >> (4u * slot)) & 0xfu) == 0 || ((read(cx, 0x31c + slot * 0xfu) >> 2u) & 0x1fu) == 0) continue; - const auto target = DecodeColorBuffer(cx, slot); - Require(target.samples == 1, "CB metadata pass over a multisampled color target, whose texels are on the host, is unsupported (only a CB resolve is modeled)"); + const auto target = DecodeColorBuffer(cx, slot, fmaskDecompress); + if (fmaskDecompress) { + Require(target.samples > 1, "CB metadata pass over a single-sample target"); + Require(target.cmaskAddress != 0, "FMASK decompress of a target without a CMASK address"); + } else { + Require(target.samples == 1, "CB metadata pass over a multisampled color target, whose texels are on the host, is unsupported (only a CB resolve is modeled)"); + } Require((read(cx, 0x31c + slot * 0xfu) & 0x10000000u) == 0 || target.dccAddress != 0, "CB metadata pass over a mipmapped DCC color target, whose keys are not modeled"); requireCovered(target); pass.targets.push_back(target); @@ -1322,20 +2095,21 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (indexed && value(queue.userConfig, 0x24b, word) && word != 0) { std::uint32_t primitive = 0; std::uint32_t resetIndex = 0; - if (value(queue.userConfig, 0x242, primitive) && (primitive & 0x3fu) != 1 && (primitive & 0x3fu) != 2 && (primitive & 0x3fu) != 3 && (primitive & 0x3fu) != 4 && (primitive & 0x3fu) != 5 && (primitive & 0x3fu) != 6) return "AGC graphics: primitive restart is only supported for point, line and triangle topologies"; + if (value(queue.userConfig, 0x242, primitive) && (primitive & 0x3fu) != 1 && (primitive & 0x3fu) != 2 && (primitive & 0x3fu) != 3 && (primitive & 0x3fu) != 4 && (primitive & 0x3fu) != 5 && (primitive & 0x3fu) != 6 && (primitive & 0x3fu) != 21) return "AGC graphics: primitive restart is only supported for point, line and triangle topologies"; if (value(cx, 0x103, resetIndex) && (resetIndex & 0xffffu) != 0xffffu) return "AGC graphics: primitive restart index other than all ones is unsupported"; } - if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports | (pointOutput(queue) ? 1u << 16u : inactiveVertexPointSize(queue))), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; if (value(cx, 0x207, word) && std::popcount(word & 0xffffu) > 8) return require(false, "more than eight clip and cull distances are unsupported"); if (value(cx, 0x200, word)) { const auto renderControl = find(cx, 0x000); const bool depthClear = renderControl != cx.end() && (renderControl->second & 1u) != 0; - const bool stencilClear = renderControl != cx.end() && (renderControl->second & 2u) != 0; - if (stencilClear) { - 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"); + std::uint32_t stencilInfo = 0; + const bool stencilClear = renderControl != cx.end() && (renderControl->second & 2u) != 0 && !(value(cx, 0x011, stencilInfo) && (stencilInfo & 1u) == 0); + if (stencilClear && !depthPlanesAbsent(cx)) { + std::uint32_t view = 0; + if (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"); } @@ -1371,15 +2145,17 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (!multisampled && value(cx, 0x292, word) && (word & 1u) != 0) { if (auto reason = singleSampleMsaaRejection(queue); !reason.empty()) return reason; } - if (auto reason = nonzero(cx, 0x293, ScanControlMask, "sample iteration, primitive discard or out-of-order rasterization"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x293, ScanControlMask, "primitive discard or out-of-order rasterization"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x80, ~0u, "window offset"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x8d, ScreenOffsetMask, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits"); !reason.empty()) return reason; if (value(cx, 0x83, word) && word != 0xffffu) return require(false, "clip rectangles are unsupported"); if (value(cx, 0x8c, word) && (word & 0xfu) != 0xau) return require(false, "nonstandard triangle edge rules are unsupported"); if (value(cx, 0x2f9, word) && word != 0x2du) return require(false, "nonstandard pixel center or vertex quantization is unsupported"); if (value(cx, 0x313, word) && (word & ~0x001f6000u) != 0 && word != 0x6001u) return conservativeMessage(word); - std::uint32_t other = 0; - if (value(cx, 0x30e, word) && value(cx, 0x30f, other) && (word != 0xffffffffu || other != 0xffffffffu)) return require(false, "sample masks are unsupported"); + std::uint32_t config = 0, other = 0; + if (value(cx, 0x2f8, config) && value(cx, 0x30e, word) && value(cx, 0x30f, other)) { + if (auto reason = sampleMaskRejection(queue, config, word, other); !reason.empty()) return reason; + } if (value(cx, 0x206, word) && word != 0x43fu) return vteMessage(word); if (auto reason = nonzero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); !reason.empty()) return reason; std::uint32_t targetMask = 0, shaderMask = 0; @@ -1402,17 +2178,20 @@ bool PixelProgramSkipped(const QueueState& queue) { const auto& cx = queue.context; const auto targetMask = find(cx, 0x8e); const auto shaderMask = find(cx, 0x8f); - const auto zFormat = find(cx, 0x1c4); const auto shaderControl = find(cx, 0x203); - if (targetMask == cx.end() || shaderMask == cx.end() || zFormat == cx.end() || shaderControl == cx.end()) return false; - return (targetMask->second & shaderMask->second) == 0 && zFormat->second == 0 && (shaderControl->second & PixelStageRunsMask) == 0; + if (targetMask == cx.end() || shaderMask == cx.end() || shaderControl == cx.end()) return false; + const auto alphaToMask = find(cx, 0x2dc); + if (alphaToMask != cx.end() && (alphaToMask->second & 1u) != 0) return false; + return (targetMask->second & shaderMask->second) == 0 && (shaderControl->second & PixelStageRunsMask) == 0; } std::string NullPixelProgramRejection(const QueueState& queue) { const auto targetMask = find(queue.context, 0x8e); const auto shaderMask = find(queue.context, 0x8f); if (targetMask == queue.context.end() || shaderMask == queue.context.end()) return "AGC graphics: a draw without a pixel program needs CB_TARGET_MASK and CB_SHADER_MASK"; - if (ColorWriteMask(queue.context) != 0) return "AGC graphics: a draw without a pixel program writes color"; + const auto colorControl = find(queue.context, 0x202); + const bool colorDisabled = colorControl != queue.context.end() && ((colorControl->second >> 4u) & 7u) == 0u; + if (!colorDisabled && ColorWriteMask(queue.context) != 0) return "AGC graphics: a draw without a pixel program writes color"; return {}; } @@ -1421,6 +2200,11 @@ std::vector*& RegisterReadLog() { return log; } +std::atomic& RegisterReadLogsOpen() { + static std::atomic open{0}; + return open; +} + bool SplitsAlternateAlpha(const State& state) { for (std::size_t index = 0; index < state.colors.size() && index < state.blends.size(); ++index) { const auto& blend = state.blends[index]; @@ -1430,6 +2214,42 @@ bool SplitsAlternateAlpha(const State& state) { return false; } +bool StencilOperationValueReplaces(const Registers& context) { + const auto value = [&](std::uint32_t offset) { + const auto found = find(context, offset); + return found == context.end() ? 0u : found->second; + }; + if ((value(0x203) & 6u) != 4u) return false; + const auto depthControl = value(0x200); + if ((depthControl & 1u) == 0 || (value(0x011) & 1u) == 0 || (value(0x002) & 0x02000000u) != 0 || (value(0x000) & 2u) != 0) return false; + const auto replaces = [](std::uint32_t ops, std::uint32_t refMask) { + if (((refMask >> 16u) & 0xffu) == 0) return false; + return (ops & 0xfu) == 4u || ((ops >> 4u) & 0xfu) == 4u || ((ops >> 8u) & 0xfu) == 4u; + }; + const auto ops = value(0x10b); + return replaces(ops, value(0x10c)) || ((depthControl & 0x80u) != 0 && replaces(ops >> 12u, value(0x10d))); +} + +bool StencilReferenceUnused(const State& state) { + if (!state.depth || !state.stencilTest) return true; + if (state.depthTest && (state.depthCompare < VK_COMPARE_OP_NEVER || state.depthCompare > VK_COMPARE_OP_ALWAYS)) return false; + const auto faceUnused = [&](const VkStencilOpState& face) { + if (face.compareOp < VK_COMPARE_OP_NEVER || face.compareOp > VK_COMPARE_OP_ALWAYS) return false; + for (const auto op : {face.failOp, face.passOp, face.depthFailOp}) { + if (op < VK_STENCIL_OP_KEEP || op > VK_STENCIL_OP_DECREMENT_AND_WRAP) return false; + } + if (face.compareOp != VK_COMPARE_OP_ALWAYS && face.compareOp != VK_COMPARE_OP_NEVER && (face.compareMask & 0xffu) != 0) return false; + if ((face.writeMask & 0xffu) == 0) return true; + if (state.depthBoundsTest) return face.failOp != VK_STENCIL_OP_REPLACE && face.passOp != VK_STENCIL_OP_REPLACE && face.depthFailOp != VK_STENCIL_OP_REPLACE; + const bool passes = face.compareOp == VK_COMPARE_OP_ALWAYS || face.compareOp == VK_COMPARE_OP_EQUAL || face.compareOp == VK_COMPARE_OP_LESS_OR_EQUAL || face.compareOp == VK_COMPARE_OP_GREATER_OR_EQUAL; + if (!passes) return face.failOp != VK_STENCIL_OP_REPLACE; + const bool depthPass = !state.depthTest || state.depthCompare != VK_COMPARE_OP_NEVER; + const bool depthFail = state.depthTest && state.depthCompare != VK_COMPARE_OP_ALWAYS; + return (!depthPass || face.passOp != VK_STENCIL_OP_REPLACE) && (!depthFail || face.depthFailOp != VK_STENCIL_OP_REPLACE); + }; + return faceUnused(state.stencilFront) && faceUnused(state.stencilBack); +} + bool DrawKeyCovers(RegisterRead read) { // One bit per register offset of each bank, set from the table once. static const auto covered = [] { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 0ea0b61619..1e10ed8d97 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1,7 +1,12 @@ +#include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" @@ -30,11 +35,23 @@ #include #include #include +#include #include #include namespace AgcDriver::Graphics { +OwnedImage::~OwnedImage() { + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + ReleaseImageMemory(context, memory, memoryBytes, memoryType); +} + +void OwnedImage::Allocate(const Context& device, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation) { + memory = AllocateImageMemory(device, allocation, mask, operation, &memoryType); + memoryBytes = allocation.allocationSize; + context.bufferPool = device.bufferPool; +} + namespace { // APS5_PROFILE_DRAW: accumulate texture setup phases and report every 200 textures. @@ -61,6 +78,12 @@ TextureProfile& Profile() { return profile; } +void keepStorageTexture(Recorder& recorder, std::shared_ptr texture) { + if (texture == nullptr) return; + if (!texture->Cached()) recorder.KeepBytes(texture.get(), static_cast(texture->BudgetBytes())); + recorder.Keep(std::move(texture)); +} + // APS5_PROFILE_DRAW, every 10 s on the [storage] line: storage image bytes uploaded by path // (recorded DCC clear, direct detile from the import, CPU) and written back by the reason of the // FlushPending that forced the write-back (the other callers name themselves: "refresh", "cache @@ -78,13 +101,14 @@ thread_local const char* uploadReason = "first"; // selections widened to every pending unit (the hysteresis in writeBack), units dropped as dead // (DiscardPendingInside) and units an alias's own results superseded (markLayersPending); relaxed, // reported only. -std::atomic singlePassMoves{0}; -std::atomic partialUploads{0}, partialUploadBytes{0}, partialWriteBacks{0}, partialWriteBackBytes{0}, emptyWriteBacks{0}, coalescedWriteBacks{0}, unitsDropped{0}, unitsSuperseded{0}; +std::atomic singlePassMoves{0}, slabRetiles{0}; +std::atomic partialUploads{0}, partialUploadBytes{0}, partialWriteBacks{0}, partialWriteBackBytes{0}, emptyWriteBacks{0}, coalescedWriteBacks{0}, unitsDropped{0}, unitsSuperseded{0}, clearHandOvers{0}, handedOverSuperseded{0}; // FlushPending listings skipped because no pending unit of the image lay inside the range, images // whose selected pending units were dropped because their memory is no longer registered (see // writeBackLayers), and pending units a DCC clear -> uncompressed key flip kept as the texels // (Refresh). std::atomic pretestSkipped{0}, unregisteredDropped{0}, keyFlipKept{0}; +std::atomic queuedReadHeld{0}; // Images stored by a FlushPending after a hook skip of theirs (AccessKeptByCpu), of which by the // hook for the read site of the last skip; images evictStale dropped (the [hooksync] and [storage] lines). std::atomic flushedAfterSkip{0}, flushedAfterSkipSameSite{0}, staleEvicted{0}; @@ -131,7 +155,7 @@ void reportStorageTraffic(StorageTraffic& traffic) { const auto take = [](std::atomic& counter) { return static_cast(counter.exchange(0, std::memory_order_relaxed)); }; const auto partialUploadCount = take(partialUploads), partialWriteBackCount = take(partialWriteBacks); const auto uploadMiB = take(partialUploadBytes) / 1048576.0, writeBackMiB = take(partialWriteBackBytes) / 1048576.0; - AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, evicted stale %llu\n", line.c_str(), static_cast(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(staleEvicted)); + AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, handed over at %llu clear-key refreshes (%llu units superseded), dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, held for a queued draw read %llu, evicted stale %llu\n", line.c_str(), static_cast(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(clearHandOvers), take(handedOverSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(queuedReadHeld), take(staleEvicted)); traffic.writeBacks.clear(); traffic.uploadReasons.clear(); traffic.directWriteBacks = 0; @@ -211,6 +235,7 @@ VkImageViewType ViewTypeFor(TextureDimension dimension, [[maybe_unused]] std::ui case TextureDimension::kCube: return VK_IMAGE_VIEW_TYPE_2D_ARRAY; case TextureDimension::k3D: return VK_IMAGE_VIEW_TYPE_3D; case TextureDimension::k1DArray: return VK_IMAGE_VIEW_TYPE_1D_ARRAY; + case TextureDimension::k2DMsaa: break; } throw std::runtime_error("AGC graphics: Texture encountered an unknown guest texture dimension"); } @@ -228,6 +253,53 @@ void ChainMinLod(const Context& context, const GuestTextureResource& descriptor, viewInfo.pNext = &minLod; } +struct ExpandImage { + VkImage image = VK_NULL_HANDLE; + std::shared_ptr owned; + VkDeviceSize bytes = 0; +}; + +ExpandImage CreateExpandImage(const Context& context, const GuestTextureResource& descriptor, std::uint32_t depth, std::uint32_t layers) { + ExpandImage result; + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | (descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u); + imageInfo.imageType = ImageTypeFor(descriptor.dimension); + imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM; + imageInfo.extent = {descriptor.width, descriptor.height, depth}; + imageInfo.mipLevels = descriptor.mipCount; + imageInfo.arrayLayers = layers; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &result.image), "vkCreateImage sRGB copy"); + result.owned = std::make_shared(context, result.image, VK_NULL_HANDLE); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, result.image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); + result.owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory sRGB copy"); + Check(context.Function("vkBindImageMemory")(context.device, result.image, result.owned->memory, 0), "vkBindImageMemory sRGB copy"); + result.bytes = requirements.size; + return result; +} + +void RecordExpandBlit(const Context& context, VkCommandBuffer commands, VkImage source, VkImageLayout sourceLayout, VkImage target, const GuestTextureResource& descriptor, std::uint32_t depth, std::uint32_t layers) { + std::vector regions; + for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) { + const VkOffset3D end{static_cast(std::max(descriptor.width >> level, 1u)), static_cast(std::max(descriptor.height >> level, 1u)), static_cast(descriptor.dimension == TextureDimension::k3D ? std::max(depth >> level, 1u) : 1u)}; + VkImageBlit region{}; + region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, layers}; + region.srcOffsets[1] = end; + region.dstSubresource = region.srcSubresource; + region.dstOffsets[1] = end; + regions.push_back(region); + } + context.Function("vkCmdBlitImage")(commands, source, sourceLayout, target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data(), VK_FILTER_NEAREST); +} + std::atomic& SampledMemoryCounter() { static std::atomic bytes{0}; return bytes; @@ -245,6 +317,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex PhaseTimer timer; try { const auto colorFormat = SampledTextureFormat(context, descriptor.format); + const bool srgbExpand = !depthCompare && SrgbExpandsToRgba8(context, descriptor.format); Require(!depthCompare || colorFormat == VK_FORMAT_R32_SFLOAT || colorFormat == VK_FORMAT_R16_UNORM, "comparison sampling requires an R32 float or R16 unorm depth texture"); Require(!depthCompare || descriptor.dimension != TextureDimension::k3D, "comparison sampling does not support 3D depth textures"); const auto vkFormat = depthCompare ? (colorFormat == VK_FORMAT_R32_SFLOAT ? VK_FORMAT_D32_SFLOAT : VK_FORMAT_D16_UNORM) : colorFormat; @@ -257,9 +330,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"); @@ -268,16 +341,18 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex Require(arrayLayers == 0 || sliceLinearBytes <= UINT64_MAX / arrayLayers, "detiled texture buffer size overflows"); const auto linearBytes = sliceLinearBytes * arrayLayers; + const bool depth1D = depthCompare && (descriptor.dimension == TextureDimension::k1D || descriptor.dimension == TextureDimension::k1DArray); VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.flags = (descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u) | (SrgbTwinFormat(context, vkFormat) != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0u); - imageInfo.imageType = ImageTypeFor(descriptor.dimension); + imageInfo.imageType = depth1D ? VK_IMAGE_TYPE_2D : ImageTypeFor(descriptor.dimension); imageInfo.format = vkFormat; - imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth}; + imageInfo.extent = {descriptor.width, depth1D ? 1u : descriptor.height, geometry.imageDepth}; imageInfo.mipLevels = descriptor.mipCount; + Require(geometry.imageLayers <= context.limits.maxImageArrayLayers, "a texture array with more layers than maxImageArrayLayers is not implemented"); imageInfo.arrayLayers = geometry.imageLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; - imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | (srgbExpand ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0u); imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage"); @@ -289,9 +364,19 @@ 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->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory texture"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory"); + const VkImage uploadImage = image; + std::shared_ptr uploadOwned; + if (srgbExpand) { + auto expanded = CreateExpandImage(context, descriptor, geometry.imageDepth, geometry.imageLayers); + uploadOwned = std::move(owned); + owned = std::move(expanded.owned); + image = expanded.image; + allocationBytes = expanded.bytes; + } + { // Debug aid: APS5_DUMP_TEXTURE= saves the detiled first mip // of those sampled textures on their first 8 uploads as texture_
_.raw (u32 @@ -330,6 +415,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex recorder->Keep(tiled, static_cast(guestBytes)); recorder->Keep(linear, static_cast(linearBytes)); recorder->Keep(owned); + if (uploadOwned != nullptr) recorder->Keep(uploadOwned); } else { batch = std::make_unique(context); commands = batch->Handle(); @@ -348,6 +434,16 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex detiler.Dispatch(commands, descriptor.tileMode, elementBytes, tiled->Handle(), guestLayerOffset + mip.tiledOffset, linear->Handle(), linearLayerOffset + mip.linearOffset, mip, false, layer, geometry.thick, {.pipeBankXor = descriptor.pipeBankXor}); } } + if (IsRepackedTextureFormat(descriptor.format)) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { + for (std::uint32_t level = 0; level < mips.size(); ++level) { + if (!geometry.HasLayer(level, layer)) continue; + const auto offset = geometry.LinearLayerOffset(layer) + mips[level].linearOffset; + detiler.DispatchRepack(commands, linear->Handle(), offset, linear->Handle(), offset, mips[level].linearSize); + } + } + } VkBufferMemoryBarrier linearReadBarrier{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; linearReadBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; @@ -365,7 +461,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - toTransferDst.image = image; + toTransferDst.image = uploadImage; toTransferDst.subresourceRange = {aspect, 0, descriptor.mipCount, 0, geometry.imageLayers}; context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearReadBarrier, 1, &toTransferDst); @@ -386,7 +482,19 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex regions.push_back(region); } } - context.Function("vkCmdCopyBufferToImage")(commands, linear->Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data()); + context.Function("vkCmdCopyBufferToImage")(commands, linear->Handle(), uploadImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data()); + if (srgbExpand) { + std::array toBlit{}; + toBlit[0] = toTransferDst; + toBlit[0].srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toBlit[0].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + toBlit[0].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toBlit[0].newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + toBlit[1] = toTransferDst; + toBlit[1].image = image; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 2, toBlit.data()); + RecordExpandBlit(context, commands, uploadImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, descriptor, geometry.imageDepth, geometry.imageLayers); + } std::unique_ptr dump; if (dumpWanted) { dump = std::make_unique(context, static_cast(mips[0].linearSize), VK_BUFFER_USAGE_TRANSFER_DST_BIT); @@ -431,14 +539,14 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; - viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); - viewInfo.format = vkFormat; - viewFormat = vkFormat; + viewInfo.viewType = depth1D ? (descriptor.dimension == TextureDimension::k1DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D) : ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.format = srgbExpand ? VK_FORMAT_R8G8B8A8_SRGB : vkFormat; + viewFormat = viewInfo.format; viewInfo.components = depthCompare ? VkComponentMapping{} : components; viewInfo.subresourceRange = {aspect, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; @@ -492,6 +600,73 @@ bool Texture::CanBlockCopyFrom(const StorageTexture& source, const GuestTextureR return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers; } +bool Texture::CanRepackCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor) { + const auto& from = source.Descriptor(); + if (!IsRepackedTextureFormat(descriptor.format) || descriptor.dimension == TextureDimension::k3D || ResolveTextureFormat(from.format) != ResolveTextureFormat(descriptor.format)) return false; + if (descriptor.baseAddress != from.baseAddress || descriptor.pipeBankXor != from.pipeBankXor || descriptor.width != from.width || descriptor.height != from.height || !ArrayLayersServeView(from, descriptor) || descriptor.tileMode != from.tileMode || descriptor.mipCount > from.mipCount) return false; + const auto geometry = DescribeSurface(descriptor); + return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers; +} + +Texture::Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, RepackCopyTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse), repackCopy(true) { + try { + Require(source != nullptr && CanRepackCopyFrom(*source, descriptor), "storage image cannot serve a repacked copy of the sampled texture"); + const auto format = SampledTextureFormat(context, descriptor.format); + const auto geometry = DescribeSurface(descriptor); + copyWidth = descriptor.width; + copyHeight = descriptor.height; + copyLevels = descriptor.mipCount; + copyLayers = geometry.imageLayers; + copyElementBytes = BytesPerElement(descriptor.format); + AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u repacked copy of storage image (guest format %u -> vk %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, descriptor.format, static_cast(format)); + + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.flags = descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u; + imageInfo.imageType = ImageTypeFor(descriptor.dimension); + imageInfo.format = format; + imageInfo.extent = {descriptor.width, descriptor.height, 1u}; + imageInfo.mipLevels = copyLevels; + imageInfo.arrayLayers = copyLayers; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage repacked copy"); + owned = std::make_shared(context, image, VK_NULL_HANDLE); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocationBytes = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory repacked copy"); + Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory repacked copy"); + + const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; + const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; + if (descriptor.dimension == TextureDimension::kCube) { + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); + } + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.format = format; + viewFormat = format; + viewInfo.components = components; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; + VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; + ChainMinLod(context, descriptor, viewInfo, minLod); + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView repacked copy"); + viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; + createFirstLayerView(descriptor, viewInfo); + prepareBaseLevelView(descriptor, viewInfo); + } catch (...) { + release(); + throw; + } +} + Texture::Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, BlockCopyTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse) { try { Require(source != nullptr && CanBlockCopyFrom(*source, descriptor), "storage image cannot serve a block copy of the sampled texture"); @@ -502,7 +677,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; @@ -523,7 +698,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "vkAllocateMemory block copy"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory block copy"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory block copy"); const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; @@ -558,13 +733,14 @@ 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)); + const bool depth1D = descriptor.dimension == TextureDimension::k1D || descriptor.dimension == TextureDimension::k1DArray; VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.flags = descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u; - imageInfo.imageType = ImageTypeFor(descriptor.dimension); + imageInfo.imageType = depth1D ? VK_IMAGE_TYPE_2D : ImageTypeFor(descriptor.dimension); imageInfo.format = depthFormat; - imageInfo.extent = {descriptor.width, descriptor.height, 1u}; + imageInfo.extent = {descriptor.width, depth1D ? 1u : descriptor.height, 1u}; imageInfo.mipLevels = copyLevels; imageInfo.arrayLayers = copyLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; @@ -580,9 +756,55 @@ 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->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth copy"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory depth copy"); + const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; + const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; + if (descriptor.dimension == TextureDimension::kCube) { + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); + } + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = depth1D ? (descriptor.dimension == TextureDimension::k1DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D) : ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.format = depthFormat; + viewInfo.components = VkComponentMapping{}; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; + VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; + ChainMinLod(context, descriptor, viewInfo, minLod); + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth copy"); + viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; + createFirstLayerView(descriptor, viewInfo); + prepareBaseLevelView(descriptor, viewInfo); + static_cast(components); + depthCopyTextures.fetch_add(1, std::memory_order_relaxed); + } catch (...) { + release(); + throw; + } +} + +bool Texture::CanSrgbExpandFrom(const Context& context, const StorageTexture& source, const GuestTextureResource& descriptor) { + if (!SrgbExpandsToRgba8(context, descriptor.format) || !CanCopyFrom(source, descriptor) || descriptor.dimension == TextureDimension::k3D) return false; + if (!StorageFormatAvailable(context, source.Descriptor().format) || StorageFormatForGuest(context, source.Descriptor().format) != SampledTextureFormat(context, descriptor.format)) return false; + const auto geometry = DescribeSurface(descriptor); + return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers && source.Descriptor().mipCount >= descriptor.mipCount; +} + +Texture::Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, SrgbExpandTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse), srgbExpand(true) { + try { + Require(source != nullptr && CanSrgbExpandFrom(context, *source, descriptor), "storage image cannot serve an sRGB copy of the sampled texture"); + const auto geometry = DescribeSurface(descriptor); + copyWidth = descriptor.width; + copyHeight = descriptor.height; + copyLevels = descriptor.mipCount; + copyLayers = geometry.imageLayers; + APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u layers=%u sRGB copy of storage image (guest format %u)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, descriptor.format); + auto expanded = CreateExpandImage(context, descriptor, 1u, copyLayers); + image = expanded.image; + owned = std::move(expanded.owned); + allocationBytes = expanded.bytes; + const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { @@ -591,50 +813,31 @@ Texture::Texture(const Context& context, const std::shared_ptr& VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); - viewInfo.format = depthFormat; - viewInfo.components = VkComponentMapping{}; - viewInfo.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; + viewInfo.format = VK_FORMAT_R8G8B8A8_SRGB; + viewFormat = viewInfo.format; + viewInfo.components = components; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; ChainMinLod(context, descriptor, viewInfo, minLod); - Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth copy"); + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView sRGB copy"); + viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); - static_cast(components); - depthCopyTextures.fetch_add(1, std::memory_order_relaxed); + prepareBaseLevelView(descriptor, viewInfo); + sourceDescriptor = descriptor; + countedBytes = allocationBytes; + SampledMemoryCounter().fetch_add(countedBytes, std::memory_order_relaxed); } catch (...) { release(); throw; } } -bool Texture::RefreshDepthCopy() { - if (perUseCopy != PerUseCopy::RefreshedOnReuse || storageSource == nullptr) return false; - if (copiedOnce && storageSource->Version() == copiedVersion) return false; - if (blockFormat != VK_FORMAT_UNDEFINED) return refreshBlockCopy(); - std::uint64_t linearBytes = 0; - for (std::uint32_t level = 0; level < copyLevels; ++level) linearBytes += static_cast(std::max(copyWidth >> level, 1u)) * std::max(copyHeight >> level, 1u) * copyElementBytes * copyLayers; - auto linear = std::make_shared(context, static_cast(linearBytes), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); - std::vector fromSource, toDepth; - std::uint64_t offset = 0; - for (std::uint32_t level = 0; level < copyLevels; ++level) { - const VkExtent3D extent{std::max(copyWidth >> level, 1u), std::max(copyHeight >> level, 1u), 1u}; - VkBufferImageCopy region{}; - region.bufferOffset = offset; - region.bufferRowLength = 0; - region.bufferImageHeight = 0; - region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, copyLayers}; - region.imageOffset = {0, 0, 0}; - region.imageExtent = extent; - fromSource.push_back(region); - region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; - toDepth.push_back(region); - offset += static_cast(extent.width) * extent.height * copyElementBytes * copyLayers; - } +bool Texture::refreshSrgbExpand() { auto* recorder = Recorder::Active(); std::unique_ptr batch; VkCommandBuffer commands = VK_NULL_HANDLE; if (recorder != nullptr) { commands = recorder->Commands(); - recorder->Keep(linear); recorder->Keep(owned); recorder->Keep(storageSource); } else { @@ -651,7 +854,68 @@ bool Texture::RefreshDepthCopy() { toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toTransferDst.image = image; - toTransferDst.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, copyLevels, 0, copyLayers}; + toTransferDst.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, copyLevels, 0, copyLayers}; + barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &produced, 0, nullptr, 1, &toTransferDst); + RecordExpandBlit(context, commands, storageSource->Image(), VK_IMAGE_LAYOUT_GENERAL, image, sourceDescriptor, 1u, copyLayers); + VkImageMemoryBarrier toShaderRead = toTransferDst; + toShaderRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toShaderRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + toShaderRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toShaderRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toShaderRead); + if (batch) batch->SubmitAndWait(); + copiedOnce = true; + copiedVersion = storageSource->Version(); + return true; +} + +bool Texture::RefreshDepthCopy() { + if (perUseCopy != PerUseCopy::RefreshedOnReuse || storageSource == nullptr) return false; + if (copiedOnce && storageSource->Version() == copiedVersion) return false; + if (blockFormat != VK_FORMAT_UNDEFINED) return refreshBlockCopy(); + if (srgbExpand) return refreshSrgbExpand(); + std::uint64_t linearBytes = 0; + for (std::uint32_t level = 0; level < copyLevels; ++level) linearBytes += static_cast(std::max(copyWidth >> level, 1u)) * std::max(copyHeight >> level, 1u) * copyElementBytes * copyLayers; + auto linear = std::make_shared(context, static_cast(linearBytes), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | (repackCopy ? VK_BUFFER_USAGE_STORAGE_BUFFER_BIT : 0u)); + std::vector fromSource, toDepth; + std::uint64_t offset = 0; + for (std::uint32_t level = 0; level < copyLevels; ++level) { + const VkExtent3D extent{std::max(copyWidth >> level, 1u), std::max(copyHeight >> level, 1u), 1u}; + VkBufferImageCopy region{}; + region.bufferOffset = offset; + region.bufferRowLength = 0; + region.bufferImageHeight = 0; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, copyLayers}; + region.imageOffset = {0, 0, 0}; + region.imageExtent = extent; + fromSource.push_back(region); + region.imageSubresource.aspectMask = repackCopy ? VK_IMAGE_ASPECT_COLOR_BIT : VK_IMAGE_ASPECT_DEPTH_BIT; + toDepth.push_back(region); + offset += static_cast(extent.width) * extent.height * copyElementBytes * copyLayers; + } + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + VkCommandBuffer commands = VK_NULL_HANDLE; + if (recorder != nullptr) { + commands = recorder->Commands(); + recorder->Keep(linear, linear->Size()); + recorder->Keep(owned); + recorder->Keep(storageSource); + } else { + batch = std::make_unique(context); + commands = batch->Handle(); + } + const auto barrier = context.Function("vkCmdPipelineBarrier"); + const VkMemoryBarrier produced{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT}; + VkImageMemoryBarrier toTransferDst{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toTransferDst.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + toTransferDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toTransferDst.oldLayout = copiedOnce ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED; + toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransferDst.image = image; + toTransferDst.subresourceRange = {static_cast(repackCopy ? VK_IMAGE_ASPECT_COLOR_BIT : VK_IMAGE_ASPECT_DEPTH_BIT), 0, copyLevels, 0, copyLayers}; barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &produced, 0, nullptr, 1, &toTransferDst); context.Function("vkCmdCopyImageToBuffer")(commands, storageSource->Image(), VK_IMAGE_LAYOUT_GENERAL, linear->Handle(), static_cast(fromSource.size()), fromSource.data()); VkBufferMemoryBarrier linearReady{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; @@ -662,7 +926,20 @@ bool Texture::RefreshDepthCopy() { linearReady.buffer = linear->Handle(); linearReady.offset = 0; linearReady.size = VK_WHOLE_SIZE; - barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearReady, 0, nullptr); + if (repackCopy) { + Require(context.detiler != nullptr, "a repacked texture copy needs the texture detiler"); + linearReady.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; + barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 1, &linearReady, 0, nullptr); + for (const auto& region : fromSource) { + const auto bytes = static_cast(region.imageExtent.width) * region.imageExtent.height * copyElementBytes * copyLayers; + context.detiler->DispatchRepack(commands, linear->Handle(), region.bufferOffset, linear->Handle(), region.bufferOffset, bytes); + } + linearReady.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + linearReady.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearReady, 0, nullptr); + } else { + barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearReady, 0, nullptr); + } context.Function("vkCmdCopyBufferToImage")(commands, linear->Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(toDepth.size()), toDepth.data()); VkImageMemoryBarrier toShaderRead{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; toShaderRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; @@ -677,7 +954,7 @@ bool Texture::RefreshDepthCopy() { if (batch) batch->SubmitAndWait(); copiedOnce = true; copiedVersion = storageSource->Version(); - depthCopyRefreshes.fetch_add(1, std::memory_order_relaxed); + if (!repackCopy) depthCopyRefreshes.fetch_add(1, std::memory_order_relaxed); return true; } @@ -735,6 +1012,7 @@ bool Texture::CanCopyFrom(const StorageTexture& source, const GuestTextureResour if (IsBlockCompressed(descriptor.format) || IsBlockCompressed(from.format)) return false; // Same memory, same layout, same texel size: the GPU copy reinterprets the texels exactly as a // guest read through the sampled descriptor would. + if (IsRepackedTextureFormat(descriptor.format)) return false; return descriptor.baseAddress == from.baseAddress && descriptor.pipeBankXor == from.pipeBankXor && descriptor.width == from.width && descriptor.height == from.height && ArrayLayersServeView(from, descriptor) && descriptor.tileMode == from.tileMode && descriptor.mipCount <= from.mipCount && BytesPerElement(descriptor.format) == BytesPerElement(from.format) && BlockWidth(descriptor.format) == BlockWidth(from.format); } @@ -745,13 +1023,15 @@ 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)); +#ifdef APS5_DEBUG_LOG + 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)); +#endif // 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; const auto viewLayerCount = std::min(geometry.imageLayers, source->ImageLayers()) - descriptor.baseArray; if (descriptor.dimension == TextureDimension::kCube) { - Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices"); } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = source->Image(); @@ -779,15 +1059,19 @@ Texture::Texture(const Context& context, const std::shared_ptr& } } -Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType) : context(context) { +Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer, std::shared_ptr owner, VkImageUsageFlags usage) : context(context), owned(std::move(owner)) { + Require(owned == nullptr || (owned->image == depthImage && owned->context.device == context.device), "a depth sampled view must retain its own device and image"); layout = VK_IMAGE_LAYOUT_GENERAL; VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + viewUsage.usage = usage; + if (usage != 0) viewInfo.pNext = &viewUsage; viewInfo.image = depthImage; viewInfo.viewType = viewType; viewInfo.format = depthFormat; viewFormat = depthFormat; viewInfo.components = components; - viewInfo.subresourceRange = {aspect, 0, 1, 0, viewType == VK_IMAGE_VIEW_TYPE_2D ? 1u : VK_REMAINING_ARRAY_LAYERS}; + viewInfo.subresourceRange = {aspect, 0, 1, baseLayer, viewType == VK_IMAGE_VIEW_TYPE_2D ? 1u : VK_REMAINING_ARRAY_LAYERS}; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth plane"); viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; if (viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY) return; @@ -802,7 +1086,7 @@ Texture::~Texture() { } void Texture::createFirstLayerView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) { - if (descriptor.dimension != TextureDimension::k2DArray) return; + if (descriptor.dimension != TextureDimension::k2DArray && (viewInfo.viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY || descriptor.dimension != TextureDimension::k1DArray)) return; viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; viewInfo.subresourceRange.layerCount = 1; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &firstLayerView), "vkCreateImageView first layer"); @@ -811,9 +1095,10 @@ void Texture::createFirstLayerView(const GuestTextureResource& descriptor, VkIma void Texture::prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) { if (viewInfo.subresourceRange.levelCount == 1u) return; - if (descriptor.dimension != TextureDimension::k1D && descriptor.dimension != TextureDimension::k2D && descriptor.dimension != TextureDimension::k2DArray) return; + const bool depth1D = (viewInfo.subresourceRange.aspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) && (descriptor.dimension == TextureDimension::k1D || descriptor.dimension == TextureDimension::k1DArray); + if (!depth1D && descriptor.dimension != TextureDimension::k1D && descriptor.dimension != TextureDimension::k2D && descriptor.dimension != TextureDimension::k2DArray) return; viewInfo.pNext = nullptr; - viewInfo.viewType = descriptor.dimension == TextureDimension::k1D ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D; + viewInfo.viewType = (descriptor.dimension == TextureDimension::k1D && !depth1D) ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D; viewInfo.subresourceRange.levelCount = 1; viewInfo.subresourceRange.layerCount = 1; baseLevelInfo = viewInfo; @@ -860,7 +1145,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; } @@ -974,6 +1265,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: @@ -1084,8 +1379,10 @@ 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, +#ifdef APS5_DEBUG_LOG + 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)); +#endif geometry = DescribeSurface(descriptor); mips = geometry.mips; arrayLayers = geometry.layers; @@ -1103,6 +1400,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, layerGeneration.assign(trackedLayers, 0); layerPending.assign(trackedLayers, false); layerOriginal.assign(trackedLayers, true); + savedTicket.assign(trackedLayers, 0); sliceLinearBytes = geometry.sliceLinearBytes; const auto linearBytes = sliceLinearBytes * arrayLayers; @@ -1113,6 +1411,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, imageInfo.format = vkFormat; imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth}; imageInfo.mipLevels = descriptor.mipCount; + Require(geometry.imageLayers <= context.limits.maxImageArrayLayers, "a texture array with more layers than maxImageArrayLayers is not implemented"); imageInfo.arrayLayers = geometry.imageLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; @@ -1132,7 +1431,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, allocation.allocationSize = requirements.size; memoryBytes = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "storage texture"); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory storage texture"); + memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory storage texture", &memoryType); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory storage"); } catch (...) { release(); @@ -1145,28 +1444,48 @@ namespace { class PendingList { public: using iterator = std::vector::iterator; + using Ranges = std::vector>; iterator begin() { return textures.begin(); } iterator end() { return textures.end(); } void push_back(StorageTexture* texture) { textures.push_back(texture); - ++version; + changed(); } iterator erase(iterator it) { - ++version; - return textures.erase(it); + const auto next = textures.erase(it); + changed(); + return next; } void remove(const StorageTexture* texture) { - if (std::erase(textures, texture) != 0) ++version; + if (std::erase(textures, texture) != 0) changed(); } bool MayOverlap(std::uint64_t address, std::size_t bytes) { - if (textures.empty() || bytes == 0) return false; if (indexed != version) rebuild(); + return !textures.empty() && Overlaps(ranges, address, bytes); + } + bool SurelyClear(std::uint64_t address, std::size_t bytes) const { + const auto current = published.load(std::memory_order_acquire); + return current != nullptr && current->version == changes.load(std::memory_order_acquire) && !Overlaps(current->ranges, address, bytes); + } + +private: + struct Snapshot { + std::uint64_t version = 0; + Ranges ranges; + }; + + static bool Overlaps(const Ranges& ranges, std::uint64_t address, std::size_t bytes) { + if (bytes == 0) return false; const auto end = bytes > std::numeric_limits::max() - address ? std::numeric_limits::max() : address + bytes; const auto it = std::upper_bound(ranges.begin(), ranges.end(), address, [](std::uint64_t value, const std::pair& range) { return value < range.second; }); return it != ranges.end() && it->first < end; } -private: + void changed() { + ++version; + changes.store(version, std::memory_order_release); + } + void rebuild() { ranges.clear(); for (const auto* texture : textures) ranges.emplace_back(texture->Descriptor().baseAddress, texture->Descriptor().baseAddress + texture->GuestBytes()); @@ -1178,19 +1497,23 @@ private: } ranges.resize(merged); indexed = version; + published.store(std::make_shared(Snapshot{version, ranges}), std::memory_order_release); } std::vector textures; - std::vector> ranges; + Ranges ranges; std::uint64_t version = 0; std::uint64_t indexed = ~std::uint64_t{0}; + std::atomic changes{0}; + std::atomic> published; }; // Storage images whose results have not reached guest memory yet. struct PendingWrites { - std::mutex mutex; + SlimMutex mutex; PendingList textures; // Images taken out of `textures` by a FlushPending still storing them (see adjacentPendingUnchanged). std::vector flushing; + std::atomic flushingCount{0}; }; PendingWrites& Pending() { @@ -1521,8 +1844,9 @@ VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, s Require(attachable, "storage image cannot be a color attachment"); Require(mip < descriptor.mipCount, "attachment mip exceeds the storage image"); const bool volume = descriptor.dimension == TextureDimension::k3D; - Require((depthSlice == 0 && layers == 1) || (volume && mip == 0 && layers != 0 && depthSlice + layers - 1u <= descriptor.depthOrLastArray), "attachment slice is outside the storage image"); - Require(layer == 0 || (!volume && descriptor.dimension == TextureDimension::k2DArray && descriptor.baseArray + layer <= descriptor.depthOrLastArray), "attachment layer is outside the storage image"); + const bool array = !volume && descriptor.dimension == TextureDimension::k2DArray; + Require((depthSlice == 0 && layers == 1) || (volume && mip == 0 && layers != 0 && depthSlice + layers - 1u <= descriptor.depthOrLastArray) || (array && depthSlice == 0 && layers != 0), "attachment slice is outside the storage image"); + Require((layer == 0 && layers == 1) || (volume && layer == 0) || (array && descriptor.baseArray + layer + layers - 1u <= descriptor.depthOrLastArray), "attachment layer is outside the storage image"); const auto found = attachmentViews.find({format, mip, depthSlice, layers, layer}); if (found != attachmentViews.end()) return found->second; VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; @@ -1562,7 +1886,7 @@ VkImageView StorageTexture::AttachmentProxyView() { 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, &proxyMemory), "vkAllocateMemory attachment proxy"); + proxyMemory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory attachment proxy"); Check(context.Function("vkBindImageMemory")(context.device, proxyImage, proxyMemory, 0), "vkBindImageMemory attachment proxy"); VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = proxyImage; @@ -1712,11 +2036,31 @@ LookupOutcomes& ThreadLookupOutcomes() { } bool StorageTexture::Refresh() { + return refresh(0, trackedLayers); +} + +bool StorageTexture::RefreshLayers(std::uint32_t first, std::uint32_t count) { + if (count == 0 || geometry.thick || geometry.imageLayers <= 1 || first >= geometry.imageLayers || count > geometry.imageLayers - first || geometry.layerBytes == 0 || trackedLayerBytes == 0 || descriptor.dccAddress != 0) return Refresh(); + const auto begin = geometry.GuestLayerOffset(first); + const auto end = geometry.GuestLayerOffset(first + count - 1) + geometry.layerBytes; + const auto firstLayer = static_cast(begin / trackedLayerBytes); + const auto lastLayer = static_cast(std::min((end + trackedLayerBytes - 1) / trackedLayerBytes, trackedLayers)); + if (firstLayer >= lastLayer || (firstLayer == 0 && lastLayer == trackedLayers)) return Refresh(); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (layerPending[layer] && (layer < firstLayer || layer >= lastLayer)) return Refresh(); + } + if (pendingAlias() != nullptr) return Refresh(); + return refresh(firstLayer, lastLayer); +} + +bool StorageTexture::refresh(std::uint32_t firstLayer, std::uint32_t lastLayer) { const bool profile = LookupOutcomes::Profiled(); auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + const bool scoped = firstLayer != 0 || lastLayer != trackedLayers; CompletePendingCpuWrites(context, descriptor.baseAddress, static_cast(guestBytes)); if (descriptor.dccAddress != 0) CompletePendingCpuWrites(context, descriptor.dccAddress, DccKeyBytes(guestBytes)); if (refreshProved()) { + layerBehind.clear(); NoteProved(); refreshesProved.fetch_add(1, std::memory_order_relaxed); if (profile) LookupOutcomes::Add(LookupOutcomes::RefreshProved, start); @@ -1738,7 +2082,7 @@ bool StorageTexture::Refresh() { // generation, no results of this image's own there) or dead to its own write-back (tracked and // stamped); an untracked unit is stored whole by that write-back, and a unit pending in both // images takes the old order (store, then this image re-uploads it). - auto alias = pendingAlias(); + auto alias = scoped ? std::shared_ptr{} : pendingAlias(); if (alias != nullptr && !(ProvedClearKeys(descriptor, guestBytes, keyProof) == DccKeys::Uncompressed && HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes)) != nullptr)) alias = nullptr; if (alias != nullptr) { std::vector aliasStamped(trackedLayers); @@ -1751,19 +2095,35 @@ bool StorageTexture::Refresh() { } } } + const auto scopeBegin = scoped ? layerBegin(firstLayer) : descriptor.baseAddress; + const auto scopeBytes = scoped ? static_cast(layerBegin(lastLayer - 1) + layerBytes(lastLayer - 1) - scopeBegin) : static_cast(guestBytes); // No publish: the upload below reads the stored units from the unit shadow itself. - const bool flushed = FlushPending(descriptor.baseAddress, static_cast(guestBytes), alias.get(), "storage refresh", PublishScope::None); + handedOver.clear(); + handedUnits.clear(); + const auto handOverKeys = alias == nullptr && !scoped ? clearHandOver() : std::nullopt; + const bool flushed = !handOverKeys.has_value() && FlushPending(scopeBegin, scopeBytes, alias.get(), "storage refresh", PublishScope::None); if (profile && flushed) start = LookupOutcomes::Add(LookupOutcomes::PendingFlush, start); // `original` holds the guest bytes the image was last uploaded from or written back as; while the // guest memory and the DCC keys still match, the image content is current. Pages nobody wrote // since `generation` need no comparison. const auto current = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast(guestBytes)); + const auto scopedCurrent = current != 0 || !scoped ? current : GuestMemory::CollectWrites(scopeBegin, scopeBytes); + std::vector collected; + if (scopedCurrent == 0) { + collected.resize(trackedLayers); + for (auto layer = firstLayer; layer < lastLayer; ++layer) collected[layer] = collectedLayerGeneration(layer, 0); + } + const auto currentFor = [&](std::uint32_t layer) { + if (scoped && (layer < firstLayer || layer >= lastLayer)) return current; + return scopedCurrent != 0 ? scopedCurrent : collected[layer]; + }; // Per tracked layer: changed since its generation (a keys change makes every layer stale). std::vector changed(trackedLayers, false); // Per 64 KiB block of the surface (blockGenerations): stamped since its layer's generation, and // by a CPU store; scanned once when anything changed. std::vector generations; std::vector stampedBlocks, cpuBlocks; + std::vector outside; // Units taken from the alias's image instead of guest memory. std::vector borrowed; bool anyBorrowed = false; @@ -1779,6 +2139,7 @@ bool StorageTexture::Refresh() { const auto equalsOriginal = [&] { // Named for the [hooksync] attribution: the compare goes through the flush hook. const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureCompare); + if (scoped) return GuestMemory::EqualsCommitted(scopeBegin, std::span(original).subspan(static_cast(scopeBegin - descriptor.baseAddress), scopeBytes)); return GuestMemory::EqualsCommitted(descriptor.baseAddress, original); }; const auto scanBlocks = [&] { @@ -1786,17 +2147,24 @@ bool StorageTexture::Refresh() { stampedBlocks.assign(generations.size(), 0); cpuBlocks.assign(generations.size(), 0); tracked = GuestMemory::ChangedBlocks(descriptor.baseAddress, static_cast(guestBytes), generations, stampedBlocks, cpuBlocks); - compared = !tracked && compareUntracked(descriptor.baseAddress, static_cast(guestBytes), stampedBlocks, true); + if (!tracked) awaitStoredSave(nullptr); + const auto firstBlock = static_cast(scopeBegin / 65536 - descriptor.baseAddress / 65536); + const auto blocks = static_cast((scopeBegin + scopeBytes - 1) / 65536 - scopeBegin / 65536 + 1); + compared = !tracked && (scoped ? compareUntracked(scopeBegin, scopeBytes, std::span(stampedBlocks).subspan(firstBlock, blocks), true) : compareUntracked(descriptor.baseAddress, static_cast(guestBytes), stampedBlocks, true)); if (compared) { for (const auto& [from, unitGeneration] : keptAtUnwatch) { const auto unit = static_cast((from - descriptor.baseAddress) / trackedLayerBytes); if (keptAtUnwatchHolds(from, unit)) stampedBlocks[static_cast(from / 65536 - descriptor.baseAddress / 65536)] = GuestMemory::BlockWritten; } cpuBlocks = stampedBlocks; + if (scoped) { + std::fill(stampedBlocks.begin(), stampedBlocks.begin() + static_cast(firstBlock), GuestMemory::BlockWritten); + std::fill(stampedBlocks.begin() + static_cast(firstBlock + blocks), stampedBlocks.end(), GuestMemory::BlockWritten); + } } }; const auto keysStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - keys = ProvedKeys(); + keys = handOverKeys.has_value() ? *handOverKeys : ProvedKeys(); if (profile && descriptor.dccAddress != 0) LookupOutcomes::Add(LookupOutcomes::DccScan, keysStart); if (keys != uploadedKeys) { changed.assign(trackedLayers, true); @@ -1811,7 +2179,7 @@ bool StorageTexture::Refresh() { scanBlocks(); for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { if (stampedBlocks[unit] == 0) { - layerGeneration[unit] = current; + layerGeneration[unit] = currentFor(unit); continue; } changed[unit] = true; @@ -1837,6 +2205,15 @@ bool StorageTexture::Refresh() { } unchanged = std::none_of(changed.begin(), changed.end(), [](bool value) { return value; }); } + if (scoped && !keysChanged) { + outside.assign(trackedLayers, false); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if ((layer >= firstLayer && layer < lastLayer) || (!changed[layer] && !compared)) continue; + outside[layer] = true; + changed[layer] = false; + } + unchanged = std::none_of(changed.begin(), changed.end(), [](bool value) { return value; }); + } for (std::size_t k = 0; k < cpuBlocks.size(); ++k) { if (cpuBlocks[k] != 0 && (compared || (tracked && generations[k] != 0))) cpuWrote = true; } @@ -1847,12 +2224,11 @@ bool StorageTexture::Refresh() { for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (changed[layer] && layerPending[layer]) pendingChanged = true; } - if (!unchanged && !keysChanged && !pendingChanged && originalHolds(descriptor.baseAddress, static_cast(guestBytes)) && equalsOriginal()) { + if (!unchanged && !keysChanged && !pendingChanged && (scoped ? originalHolds(scopeBegin, scopeBytes) : originalHolds(descriptor.baseAddress, static_cast(guestBytes))) && equalsOriginal()) { unchanged = true; stamped = false; + if (!scoped) outside.clear(); } - // Only the direct path uploads the selected layers alone (see upload); the others replace - // the whole image, so every pending layer's results are stored first. direct = keys == DccKeys::Uncompressed && HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes)) != nullptr; if (alias != nullptr && direct) { // The alias's pending units that nothing stamped since its generation (a CPU store or a @@ -1877,11 +2253,16 @@ bool StorageTexture::Refresh() { } } } + const bool outsideChanged = std::any_of(outside.begin(), outside.end(), [](bool value) { return value; }); + if (scoped) noteLayersBehind(firstLayer, lastLayer, outside); + else layerBehind.clear(); if (unchanged) { ++Profile().storageReused; - layerGeneration.assign(trackedLayers, current); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (!outsideChanged || !outside[layer]) layerGeneration[layer] = currentFor(layer); + } refreshGeneration(); - takeRefreshProof(alias != nullptr); + if (layerBehind.empty()) takeRefreshProof(alias != nullptr); if (profile) LookupOutcomes::Add(stamped ? LookupOutcomes::RefreshUnchanged : LookupOutcomes::RefreshCompared, start); return true; } @@ -1931,6 +2312,7 @@ bool StorageTexture::Refresh() { } std::fprintf(stderr, "[dcc-keys] refresh 0x%llx+0x%llx keys %s -> %s (dirty %d, %zu pending units, %zu cpu-stamped units%s)\n", static_cast(descriptor.baseAddress), static_cast(guestBytes), DccKeysName(uploadedKeys), DccKeysName(keys), dirty ? 1 : 0, pendingUnits, stampedUnits, keyFlip ? ", flip keeps unstamped results" : ""); } + const bool selectedCpu = !direct && keys == DccKeys::Uncompressed && !cpuUploadLayers(changed).empty(); for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (!layerPending[layer]) continue; if (keyFlip && layerGeneration[layer] != 0 && stampedBlocks[layer] != GuestMemory::BlockWritten) { @@ -1949,7 +2331,7 @@ bool StorageTexture::Refresh() { dropped = true; continue; } - } else if (direct) { + } else if (direct || selectedCpu) { continue; } stored[layer] = true; @@ -1971,7 +2353,7 @@ bool StorageTexture::Refresh() { uploadReason = "alias"; borrowUnits(*alias, borrowed); for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { - if (borrowed[unit]) layerGeneration[unit] = current; + if (borrowed[unit]) layerGeneration[unit] = currentFor(unit); } refreshGeneration(); } @@ -1997,6 +2379,10 @@ std::uint64_t StorageTexture::SinglePassMoves() { return singlePassMoves.load(std::memory_order_relaxed); } +std::uint64_t StorageTexture::SlabRetiles() { + return slabRetiles.load(std::memory_order_relaxed); +} + bool StorageTexture::otherPendingOverlaps() const { auto& pending = Pending(); std::lock_guard lock(pending.mutex); @@ -2080,7 +2466,7 @@ void traceKeyStore(const char* path, const GuestTextureResource& descriptor, std void StorageTexture::captureGuestBytes(const std::vector* layers) { original.resize(static_cast(guestBytes)); - comparedGuestBytes = {}; + comparedRanges = {}; std::vector selected(trackedLayers, true); if (layers != nullptr) selected = *layers; for (const auto& [begin, end] : unitRuns(selected)) GuestMemory::FlushGpuWrites(descriptor.baseAddress + begin, static_cast(end - begin)); @@ -2089,6 +2475,7 @@ void StorageTexture::captureGuestBytes(const std::vector* layers) { const auto offset = static_cast(layerBegin(layer) - descriptor.baseAddress); GuestMemory::ReadCommittedUnsynced(layerBegin(layer), std::span(original).subspan(offset, static_cast(layerBytes(layer)))); layerOriginal[layer] = true; + cancelStoredSave(layer); } } @@ -2116,11 +2503,10 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, Require(changed.size() == (end - 1) / blockBytes - first + 1, "texture comparison block count differs"); const auto* recorder = Recorder::Active(); const auto epoch = GuestMemory::CollectEpoch(); - const bool whole = address == descriptor.baseAddress && bytes == guestBytes; - const auto hint = memoize && epoch != 0 && whole ? GuestMemory::UntrackedHint(address, bytes) : 0; + const auto hint = memoize && epoch != 0 ? GuestMemory::UntrackedHint(address, bytes) : 0; const std::array stamp{hint != 0, hint != 0 ? hint : epoch, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; - const bool cacheable = memoize && epoch != 0 && (stamp[3] & 1u) == 0 && whole; - if (cacheable && comparedGuestBytes == stamp) { + const bool cacheable = memoize && epoch != 0 && (stamp[3] & 1u) == 0; + if (cacheable && std::any_of(comparedRanges.begin(), comparedRanges.end(), [&](const ComparedRange& entry) { return entry.address == address && entry.bytes == bytes && entry.stamp == stamp; })) { std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged); return true; } @@ -2128,10 +2514,14 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, const auto saved = std::span(original).subspan(static_cast(address - descriptor.baseAddress), bytes); if (GuestMemory::EqualsCommittedUnsynced(address, saved)) { std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged); - if (cacheable) comparedGuestBytes = stamp; + if (cacheable) { + auto found = std::find_if(comparedRanges.begin(), comparedRanges.end(), [&](const ComparedRange& entry) { return entry.address == address && entry.bytes == bytes; }); + if (found == comparedRanges.end()) found = comparedRanges.begin() + nextComparedRange++ % comparedRanges.size(); + *found = {address, bytes, stamp}; + } return true; } - comparedGuestBytes = {}; + comparedRanges = {}; for (std::size_t index = 0; index < changed.size(); ++index) { const auto begin = std::max(address, (first + index) * blockBytes); const auto stop = std::min(end, (first + index + 1) * blockBytes); @@ -2143,6 +2533,25 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, constexpr std::uint64_t GenerationBaselineBytes = 1ull << 20; +std::vector StorageTexture::cpuUploadLayers(const std::vector& units) const { + if (descriptor.dimension != TextureDimension::k2DArray || descriptor.mipCount != 1 || descriptor.dccAddress != 0 || IsBlockCompressed(descriptor.format) || geometry.thick || arrayLayers <= 1 || geometry.imageLayers != arrayLayers || trackedLayerBytes == 0 || units.size() != trackedLayers) return {}; + std::vector selected(arrayLayers, false); + for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { + const auto begin = geometry.GuestLayerOffset(layer); + const auto end = begin + geometry.layerBytes; + if (end <= begin || end > guestBytes || begin % trackedLayerBytes != 0 || (end % trackedLayerBytes != 0 && end != guestBytes)) return {}; + const auto first = static_cast(begin / trackedLayerBytes); + const auto last = static_cast((end + trackedLayerBytes - 1) / trackedLayerBytes); + if (first >= last || last > trackedLayers) return {}; + selected[layer] = units[first]; + for (auto unit = first + 1; unit < last; ++unit) { + if (units[unit] != selected[layer]) return {}; + } + } + if (std::none_of(selected.begin(), selected.end(), [](bool value) { return value; }) || std::all_of(selected.begin(), selected.end(), [](bool value) { return value; })) return {}; + return selected; +} + void StorageTexture::upload(const std::vector* layers) { const bool profile = LookupOutcomes::Profiled(); const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; @@ -2151,8 +2560,6 @@ void StorageTexture::upload(const std::vector* layers) { CaptureTrace::Log("upload image=%llx bytes=%llu generation=%llu reason=%s partial=%d", static_cast(descriptor.baseAddress), static_cast(guestBytes), static_cast(generation), uploadReason, layers != nullptr); const auto elementBytes = BytesPerElement(descriptor.format); const auto linearBytes = sliceLinearBytes * arrayLayers; - // A layer selection applies to the direct path alone (the others upload everything); it names - // array layers, which the tracked layers are when there are several. if (layers != nullptr && ((!blockUnits && trackedLayers != arrayLayers) || std::all_of(layers->begin(), layers->end(), [](bool selected) { return selected; }))) layers = nullptr; const auto now = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast(guestBytes)); const auto stampLayers = [&](bool selectedOnly) { @@ -2164,7 +2571,7 @@ void StorageTexture::upload(const std::vector* layers) { } for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (!whole && !(*layers)[layer]) continue; - layerGeneration[layer] = now; + layerGeneration[layer] = collectedLayerGeneration(layer, now); if (whole || generationBaseline.size() != guestBytes) continue; const auto offset = static_cast(layerBegin(layer) - descriptor.baseAddress); GuestMemory::ReadCommitted(layerBegin(layer), std::span(generationBaseline).subspan(offset, static_cast(layerBytes(layer)))); @@ -2180,8 +2587,12 @@ void StorageTexture::upload(const std::vector* layers) { // batch of its own submitted the recorder's work first and waited for all of it, 25-40 ms // under the GPU mutex at the movie stage. APS5_NO_RECORDED_CLEAR=1 waits as before. static_cast(HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes))); - if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) layerOriginal.assign(trackedLayers, false); - else captureGuestBytes(nullptr); + if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) { + layerOriginal.assign(trackedLayers, false); + cancelStoredSaves(); + } else { + captureGuestBytes(nullptr); + } forgetBorrowed(0, trackedLayers); stampLayers(false); static const bool recordClear = std::getenv("APS5_NO_RECORDED_CLEAR") == nullptr; @@ -2192,7 +2603,7 @@ void StorageTexture::upload(const std::vector* layers) { if (recorder != nullptr) { commands = recorder->Commands(); timing = recorder->BeginGpuTiming(Recorder::CommandClass::DccClear); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); ++Profile().storageRecordedClears; Recorder::CountBarriers(Recorder::CommandClass::DccClear, 2); if (Recorder::BarrierValidate()) { @@ -2229,8 +2640,12 @@ void StorageTexture::upload(const std::vector* layers) { return; } if (const auto* import = uploadedKeys == DccKeys::Uncompressed ? HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes)) : nullptr) { - if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) layerOriginal.assign(trackedLayers, false); - else captureGuestBytes(layers); + if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) { + layerOriginal.assign(trackedLayers, false); + cancelStoredSaves(); + } else { + captureGuestBytes(layers); + } stampLayers(true); // A whole-surface upload of a block-unit image goes through the windows too when a unit // shadow holds part of the surface (the detile then reads the slabs; a new image has no @@ -2280,7 +2695,7 @@ void StorageTexture::upload(const std::vector* layers) { timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageUpload); if (linear) recorder->Keep(linear, linear->Size()); // The image itself must outlive the recorded copy: the cache may evict it right after. - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); // The detile reads the tiled bytes from the import when the batch runs. recorder->NotePendingRead(descriptor.baseAddress, static_cast(guestBytes), Recorder::ReadKind::StorageUpload); Recorder::CountBarriers(Recorder::CommandClass::StorageUpload, direct ? 2 : 3); @@ -2293,7 +2708,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) { @@ -2351,14 +2766,36 @@ void StorageTexture::upload(const std::vector* layers) { if (profile) LookupOutcomes::Add(LookupOutcomes::UploadDirect, start); return; } - layerOriginal.assign(trackedLayers, true); - forgetBorrowed(0, trackedLayers); - stampLayers(false); - original.resize(static_cast(guestBytes)); - GuestMemory::ReadCommitted(descriptor.baseAddress, original); + const auto cpuLayers = uploadedKeys == DccKeys::Uncompressed && layers != nullptr ? cpuUploadLayers(*layers) : std::vector{}; + const bool selectedCpu = !cpuLayers.empty(); + std::uint64_t uploadedBytes = guestBytes; + if (selectedCpu) { + captureGuestBytes(layers); + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { + if ((*layers)[unit]) forgetBorrowed(unit, 1); + } + stampLayers(true); + uploadedBytes = 0; + for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { + if (cpuLayers[layer]) uploadedBytes += geometry.layerBytes; + } + } else { + layerOriginal.assign(trackedLayers, true); + cancelStoredSaves(); + forgetBorrowed(0, trackedLayers); + stampLayers(false); + original.resize(static_cast(guestBytes)); + GuestMemory::ReadCommitted(descriptor.baseAddress, original); + } { Buffer staging(context, original.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); - if (uploadedKeys == DccKeys::Uncompressed) std::memcpy(staging.Bytes().data(), original.data(), original.size()); + if (selectedCpu) { + for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { + if (!cpuLayers[layer]) continue; + const auto offset = geometry.GuestLayerOffset(layer); + std::memcpy(staging.Bytes().data() + offset, original.data() + offset, static_cast(geometry.layerBytes)); + } + } else if (uploadedKeys == DccKeys::Uncompressed) std::memcpy(staging.Bytes().data(), original.data(), original.size()); else ReadTextureSurface(descriptor, uploadedKeys, staging.Bytes().first(original.size())); DeviceBuffer tiled(context, original.size(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); DeviceBuffer linear(context, static_cast(linearBytes), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); @@ -2366,9 +2803,16 @@ void StorageTexture::upload(const std::vector* layers) { CommandBatch batch(context); const auto commands = batch.Handle(); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); - CopyBuffer(context, commands, staging.Handle(), 0, tiled.Handle(), 0, original.size()); + if (selectedCpu) { + for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { + if (!cpuLayers[layer]) continue; + const auto offset = geometry.GuestLayerOffset(layer); + CopyBuffer(context, commands, staging.Handle(), offset, tiled.Handle(), offset, geometry.layerBytes); + } + } else CopyBuffer(context, commands, staging.Handle(), 0, tiled.Handle(), 0, original.size()); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { + if (selectedCpu && !cpuLayers[layer]) continue; for (std::uint32_t level = 0; level < mips.size(); ++level) { if (!geometry.HasLayer(level, layer)) continue; const auto& mip = mips[level]; @@ -2377,15 +2821,16 @@ void StorageTexture::upload(const std::vector* layers) { } const auto linearRead = WholeBufferBarrier(linear.Handle(), VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toTransfer.srcAccessMask = selectedCpu ? VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT : 0u; toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - toTransfer.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toTransfer.oldLayout = selectedCpu ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED; toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; toTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toTransfer.image = image; toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, descriptor.mipCount, 0, geometry.imageLayers}; - context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearRead, 1, &toTransfer); - const auto regions = CopyRegions(); + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | (selectedCpu ? VK_PIPELINE_STAGE_ALL_COMMANDS_BIT : 0u), VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearRead, 1, &toTransfer); + const auto regions = CopyRegions(selectedCpu ? &cpuLayers : nullptr); context.Function("vkCmdCopyBufferToImage")(commands, linear.Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data()); VkImageMemoryBarrier toGeneral = toTransfer; toGeneral.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; @@ -2397,7 +2842,7 @@ void StorageTexture::upload(const std::vector* layers) { batch.SubmitAndWait(); APS5_LOG_CHARS_OUT("StorageTexture upload done"); } - countStorageUpload(2, guestBytes); + countStorageUpload(2, uploadedBytes); ++version; if (profile) LookupOutcomes::Add(LookupOutcomes::UploadCpu, start); } @@ -2495,6 +2940,16 @@ std::vector StorageTexture::sliceWindows(std::span< return windows; } +std::vector StorageTexture::UnitRects(const std::vector& units) const { + std::vector rects; + if (units.size() != trackedLayers) return rects; + const auto runs = unitRuns(units); + for (const auto& window : sliceWindows(runs)) { + for (const auto& region : window.regions) rects.push_back({{region.imageOffset.x, region.imageOffset.y}, {region.imageExtent.width, region.imageExtent.height}}); + } + return rects; +} + std::vector> StorageTexture::uncoveredBytes(std::span> runs) const { std::vector> covered; if (!geometry.thick) { @@ -2572,6 +3027,53 @@ StorageTexture::PaddingSeeds StorageTexture::paddingSeeds(const HostImport& impo return seeds; } +StorageTexture::PaddingSeeds StorageTexture::paddingSeeds(std::span segments, const std::vector& copied) const { + if (segments.size() == 1) return paddingSeeds(*segments.front().import, copied); + PaddingSeeds merged; + for (const auto& segment : segments) { + const auto segmentBegin = segment.begin - descriptor.baseAddress; + const auto segmentEnd = segment.end - descriptor.baseAddress; + std::vector inside; + for (const auto& range : copied) { + const auto begin = std::max(range.begin, segmentBegin); + const auto end = std::min(range.end, segmentEnd); + if (begin < end) inside.push_back({begin, end, range.scratch + (begin - range.begin)}); + } + if (inside.empty()) continue; + auto seeds = paddingSeeds(*segment.import, std::move(inside)); + for (auto& entry : seeds.copies) merged.copies.push_back(std::move(entry)); + for (auto& slab : seeds.slabs) merged.slabs.push_back(std::move(slab)); + merged.importReads.insert(merged.importReads.end(), seeds.importReads.begin(), seeds.importReads.end()); + } + return merged; +} + +std::vector StorageTexture::importSegments() const { + const auto begin = descriptor.baseAddress; + const auto end = begin + guestBytes; + const bool unitAligned = blockUnits && UnitShadowEnabled(); + const auto samePiece = [](const RegisteredPiece& a, const RegisteredPiece& b) { return a.address == b.address && a.bytes == b.bytes && a.writable == b.writable && a.gpu == b.gpu; }; + for (std::uint32_t attempt = 0; attempt < 2; ++attempt) { + std::vector pieces; + if (!RegisteredReadableCovers(begin, static_cast(guestBytes), &pieces) || pieces.size() < 2) return {}; + if (std::any_of(pieces.begin(), pieces.end(), [&](const RegisteredPiece& piece) { return piece.writable != pieces.front().writable || piece.gpu != pieces.front().gpu; })) return {}; + std::vector segments; + const auto proof = HostImportsIdentity(context); + for (const auto& piece : pieces) { + const auto from = std::max(piece.address, begin); + const auto to = std::min(piece.address + piece.bytes, end); + const auto* import = HostImportFor(context, from, static_cast(to - from), ImportRequester::Segment); + if (import == nullptr) return {}; + if (!segments.empty() && segments.back().import == import) segments.back().end = to; + else if (!segments.empty() && unitAligned && from % 65536 != 0) return {}; + else segments.push_back({import, from, to}); + } + std::vector still; + if (HostImportsUnchanged(context, proof) && RegisteredReadableCovers(begin, static_cast(guestBytes), &still) && std::equal(pieces.begin(), pieces.end(), still.begin(), still.end(), samePiece)) return segments; + } + return {}; +} + void StorageTexture::recordDirectUploadBarrier(VkCommandBuffer commands, bool discard) { const VkMemoryBarrier sourceReady{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; VkImageMemoryBarrier toWrite{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; @@ -2660,7 +3162,7 @@ std::uint64_t StorageTexture::uploadWindows(const HostImport& import, std::span< for (const auto& source : sources) { if (source.slab != nullptr) recorder->Keep(source.slab); } - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); if (!reads.empty()) recorder->NotePendingReads(reads, Recorder::ReadKind::StorageUpload); Recorder::CountBarriers(Recorder::CommandClass::StorageUpload, direct ? 2 : 3); if (Recorder::BarrierValidate()) { @@ -2671,7 +3173,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) { @@ -2739,7 +3241,7 @@ std::uint64_t StorageTexture::uploadWindows(const HostImport& import, std::span< return uploadedBytes; } -std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::span> keep, std::uint64_t firstStored, std::uint64_t lastStored, std::vector& shadowed, std::vector>& imported) { +std::uint64_t StorageTexture::writeBackWindows(std::span segments, std::span> keep, std::uint64_t firstStored, std::uint64_t lastStored, std::vector& shadowed, std::vector>& imported, Buffer* save) { const auto elementBytes = BytesPerElement(descriptor.format); std::vector> runs; std::uint64_t storedBytes = 0; @@ -2749,6 +3251,10 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp } if (runs.empty()) return 0; auto windows = sliceWindows(runs); + // A kept unit can lie outside every window: the tracking grid is flat over the guest range + // while the slices of a 3D surface taper in depth with the level (HasLayer), so the upper + // depth slices' higher-level blocks belong to no window. There is no tiled data to retile. + if (windows.empty()) return 0; // The linear rows and the tiled bytes of every window, each at a 256-byte position; the tail // levels of a layer retile into one scratch region (their block is shared). std::vector linearPositions, scratchPositions; @@ -2777,23 +3283,45 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp std::shared_ptr pin; std::vector copies; }; + struct SeedUnit { + std::uint64_t begin; + std::uint64_t end; + std::shared_ptr slab; + const HostImport* import; + }; std::vector slabPieces; - std::vector importCopies; - std::vector seeds; + std::vector> importCopies(segments.size()); + std::vector seeds; + struct AddedPiece { + std::size_t pieces; + std::size_t copy; + std::uint64_t begin; + std::uint64_t end; + bool slab; + bool seeded; + }; + std::vector added; const auto addPiece = [&](std::uint64_t scratchOffset, std::uint64_t guestBegin, std::uint64_t guestEnd) { while (guestBegin < guestEnd) { - const auto pieceEnd = UnitShadowEnabled() ? std::min(guestEnd, SlabBoundary(import, guestBegin)) : guestEnd; + std::size_t segment = 0; + while (segment + 1 < segments.size() && segments[segment + 1].begin <= guestBegin) ++segment; + Require(segments[segment].begin <= guestBegin && guestBegin < segments[segment].end, "storage image write-back piece lies outside its import segments"); + const auto& import = *segments[segment].import; + auto pieceEnd = std::min(guestEnd, segments[segment].end); + if (UnitShadowEnabled()) pieceEnd = std::min(pieceEnd, SlabBoundary(import, guestBegin)); const auto destination = ShadowDestinationFor(context, import, guestBegin, pieceEnd); if (destination.has_value()) { if (slabPieces.empty() || slabPieces.back().slab != destination->slab) slabPieces.push_back({destination->slab, destination->pin, {}}); slabPieces.back().copies.push_back({scratchOffset, destination->offset, pieceEnd - guestBegin}); for (const auto& [seedBegin, seedEnd] : destination->seedUnits) { - if (std::none_of(seeds.begin(), seeds.end(), [&](const ShadowedRange& seed) { return seed.begin == seedBegin; })) seeds.push_back({seedBegin, seedEnd, destination->slab}); + if (std::none_of(seeds.begin(), seeds.end(), [&](const SeedUnit& seed) { return seed.begin == seedBegin; })) seeds.push_back({seedBegin, seedEnd, destination->slab, &import}); } + added.push_back({slabPieces.size() - 1, slabPieces.back().copies.size() - 1, guestBegin - descriptor.baseAddress, pieceEnd - descriptor.baseAddress, true, !destination->seedUnits.empty()}); 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.push_back({scratchOffset, guestBegin - import.base, pieceEnd - guestBegin}); + added.push_back({0, 0, guestBegin - descriptor.baseAddress, pieceEnd - descriptor.baseAddress, false, false}); + 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); } @@ -2801,18 +3329,39 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp guestBegin = pieceEnd; } }; + struct SlabRetile { + std::size_t window; + std::size_t first; + std::size_t last; + }; + std::vector slabRetiled; + const auto blockBytes = ThinBlockLayout(descriptor.tileMode, elementBytes)[0]; for (std::size_t i = 0; i < windows.size(); ++i) { const auto& window = windows[i]; // The tail levels follow each other, sharing the previous one's region. if (i != 0 && scratchPositions[i] == scratchPositions[i - 1]) continue; + const auto first = added.size(); + std::size_t kept = 0; + bool whole = false; for (const auto& [from, to] : runs) { const auto begin = std::max(from, window.tiledBegin); const auto end = std::min(to, window.tiledEnd); if (begin >= end) continue; + ++kept; addPiece(scratchPositions[i] + (begin - window.tiledBegin), descriptor.baseAddress + begin, descriptor.baseAddress + end); + whole = begin == window.tiledBegin && end == window.tiledEnd; } + if (save != nullptr || geometry.thick || mips[window.level].tail || std::count(scratchPositions.begin(), scratchPositions.end(), scratchPositions[i]) != 1 || kept != 1 || !whole) continue; + const auto last = added.size(); + const auto parts = std::span(added).subspan(first, last - first); + const auto sliceBegin = window.tiledBegin - window.window.rangeBegin; + const auto intoSlabs = std::all_of(parts.begin(), parts.end(), [&](const AddedPiece& part) { + const auto& copy = slabPieces[part.pieces].copies[part.copy]; + return part.slab && !part.seeded && copy.dstOffset % 4 == 0 && copy.size % 4 == 0 && (part.begin - sliceBegin) % blockBytes == 0 && (part.end - sliceBegin) % blockBytes == 0; + }); + if (intoSlabs) slabRetiled.push_back({i, first, last}); } - std::sort(seeds.begin(), seeds.end(), [](const ShadowedRange& a, const ShadowedRange& b) { return a.begin < b.begin; }); + std::sort(seeds.begin(), seeds.end(), [](const SeedUnit& a, const SeedUnit& b) { return a.begin < b.begin; }); std::vector copied; for (std::size_t i = 0; i < windows.size(); ++i) { const auto& window = windows[i]; @@ -2823,7 +3372,92 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp if (begin < end) copied.push_back({begin, end, scratchPositions[i] + (begin - window.tiledBegin)}); } } - const auto padding = paddingSeeds(import, std::move(copied)); + std::vector saves; + if (save != nullptr) { + for (const auto& piece : copied) saves.push_back({piece.scratch, piece.begin, piece.end - piece.begin}); + } + auto padding = paddingSeeds(segments, copied); + struct DirectPiece { + VkBuffer slab; + VkDeviceSize offset; + DetileWindow window; + std::uint64_t linearOffset; + }; + std::vector directPieces; + std::vector> directRanges(windows.size(), {0, 0}); + struct HoleCopies { + VkBuffer source; + VkBuffer slab; + std::vector copies; + }; + std::vector holeCopies; + if (!slabRetiled.empty()) { + std::vector> ranges; + for (const auto& retiled : slabRetiled) ranges.emplace_back(windows[retiled.window].tiledBegin, windows[retiled.window].tiledEnd); + const auto holes = uncoveredBytes(ranges); + for (const auto& retiled : slabRetiled) { + const auto& window = windows[retiled.window]; + const auto parts = std::span(added).subspan(retiled.first, retiled.last - retiled.first); + const auto position = scratchPositions[retiled.window]; + const auto length = window.tiledEnd - window.tiledBegin; + const auto inside = [&](const VkBufferCopy& hole) { return hole.size != 0 && hole.dstOffset >= position && hole.dstOffset + hole.size <= position + length; }; + const auto partCopy = [&](const AddedPiece& part) -> VkBufferCopy& { return slabPieces[part.pieces].copies[part.copy]; }; + const auto partSlab = [&](const AddedPiece& part) { return slabPieces[part.pieces].slab->buffer; }; + std::uint64_t holeBytes = 0, sourcedBytes = 0; + for (const auto& [first, last] : holes) { + const auto begin = std::max(first, window.tiledBegin); + const auto end = std::min(last, window.tiledEnd); + if (begin < end) holeBytes += end - begin; + } + bool sameSlabMoved = false; + for (const auto& [source, copies] : padding.copies) { + for (const auto& hole : copies) { + if (!inside(hole)) continue; + sourcedBytes += hole.size; + const auto holeBegin = window.tiledBegin + (hole.dstOffset - position); + for (const auto& part : parts) { + const auto from = std::max(holeBegin, part.begin); + const auto to = std::min(holeBegin + hole.size, part.end); + if (from < to && source == partSlab(part) && hole.srcOffset + (from - holeBegin) != partCopy(part).dstOffset + (from - part.begin)) sameSlabMoved = true; + } + } + } + if (sourcedBytes != holeBytes || sameSlabMoved) continue; + for (auto& [source, copies] : padding.copies) { + for (auto& hole : copies) { + if (!inside(hole)) continue; + const auto holeBegin = window.tiledBegin + (hole.dstOffset - position); + for (const auto& part : parts) { + const auto from = std::max(holeBegin, part.begin); + const auto to = std::min(holeBegin + hole.size, part.end); + if (from >= to || source == partSlab(part)) continue; + const auto slab = partSlab(part); + auto entry = std::find_if(holeCopies.begin(), holeCopies.end(), [&](const HoleCopies& entry) { return entry.source == source && entry.slab == slab; }); + if (entry == holeCopies.end()) entry = holeCopies.insert(holeCopies.end(), {source, slab, {}}); + entry->copies.push_back({hole.srcOffset + (from - holeBegin), partCopy(part).dstOffset + (from - part.begin), to - from}); + } + hole.size = 0; + } + } + directRanges[retiled.window] = {directPieces.size(), directPieces.size() + parts.size()}; + for (const auto& part : parts) { + auto& copy = partCopy(part); + if (parts.size() == 1) { + directPieces.push_back({partSlab(part), copy.dstOffset, window.window, 0}); + } else { + const std::pair run{part.begin, part.end}; + const auto narrowed = sliceWindows(std::span(&run, 1)); + Require(narrowed.size() == 1, "a storage write-back slab piece is not one window"); + directPieces.push_back({partSlab(part), copy.dstOffset, narrowed.front().window, narrowed.front().window.linearBase - window.window.linearBase}); + } + copy.size = 0; + } + slabRetiles.fetch_add(1, std::memory_order_relaxed); + } + for (auto& pieces : slabPieces) std::erase_if(pieces.copies, [](const VkBufferCopy& copy) { return copy.size == 0; }); + for (auto& entry : padding.copies) std::erase_if(entry.second, [](const VkBufferCopy& copy) { return copy.size == 0; }); + std::erase_if(padding.copies, [](const auto& entry) { return entry.second.empty(); }); + } // A queued label or key store inside a seeded unit lands before the seed copies the import. auto flushBegin = firstStored; auto flushEnd = lastStored; @@ -2833,7 +3467,8 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp } const bool direct = singlePass(); auto linear = direct ? nullptr : std::make_shared(context, static_cast(linearTotal), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); - auto tiledScratch = std::make_shared(context, static_cast(scratchTotal), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const bool scratchUsed = std::any_of(directRanges.begin(), directRanges.end(), [](const auto& range) { return range.first == range.second; }) || std::any_of(slabPieces.begin(), slabPieces.end(), [](const SlabPieces& pieces) { return !pieces.copies.empty(); }) || std::any_of(importCopies.begin(), importCopies.end(), [](const auto& copies) { return !copies.empty(); }) || !saves.empty() || !padding.copies.empty() || !seeds.empty(); + auto tiledScratch = scratchUsed ? std::make_shared(context, static_cast(scratchTotal), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT) : nullptr; detiler.BeginBatch(); auto* recorder = Recorder::Active(); std::unique_ptr batch; @@ -2846,11 +3481,11 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp commands = recorder->Commands(&covered); timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageWriteBack); if (linear) recorder->Keep(linear, linear->Size()); - recorder->Keep(tiledScratch, tiledScratch->Size()); + if (tiledScratch) recorder->Keep(tiledScratch, tiledScratch->Size()); for (const auto& pieces : slabPieces) recorder->Keep(pieces.slab); for (const auto& slab : padding.slabs) recorder->Keep(slab); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); - Recorder::CountBarriers(Recorder::CommandClass::StorageWriteBack, direct ? 3 : 4); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); + Recorder::CountBarriers(Recorder::CommandClass::StorageWriteBack, (direct ? 3 : 4) - (tiledScratch ? 0 : 1)); if (Recorder::BarrierValidate()) { const std::pair read{image, false}; std::vector> seedReads; @@ -2872,7 +3507,7 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp toSource.image = image; toSource.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, descriptor.mipCount, 0, geometry.imageLayers}; if (!direct) context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toSource); - if (!seeds.empty() || !padding.copies.empty()) { + if (!seeds.empty() || !padding.copies.empty() || !holeCopies.empty()) { // The seeded units' import bytes (every earlier writer of them, host stores included) // precede the seed copies; the previous command's trailing barrier may have covered that. constexpr VkAccessFlags transferAccess = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; @@ -2882,15 +3517,16 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp 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, transferAccess); Recorder::CountBarriers(Recorder::CommandClass::StorageWriteBack); } - for (const auto& [begin, end, slab] : seeds) { + 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}; - context.Resolved(&DeviceFunctions::cmdCopyBuffer, "vkCmdCopyBuffer")(commands, import.buffer, slab->buffer, 1, &seed); + 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); } // The padding's current bytes, under the retile that follows (the import-ready barrier // orders the scratch writes). for (const auto& [source, copies] : padding.copies) context.Resolved(&DeviceFunctions::cmdCopyBuffer, "vkCmdCopyBuffer")(commands, source, tiledScratch->Handle(), static_cast(copies.size()), copies.data()); + for (const auto& [source, slab, copies] : holeCopies) context.Resolved(&DeviceFunctions::cmdCopyBuffer, "vkCmdCopyBuffer")(commands, source, slab, static_cast(copies.size()), copies.data()); } const VkMemoryBarrier importReady{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT}; if (direct) { @@ -2898,12 +3534,19 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp toRead.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; toRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; toRead.newLayout = VK_IMAGE_LAYOUT_GENERAL; - context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &importReady, 0, nullptr, 1, &toRead); + const BarrierMask ready{VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, importReady.srcAccessMask | toRead.srcAccessMask, importReady.dstAccessMask | toRead.dstAccessMask, toRead.oldLayout != toRead.newLayout, toRead.srcQueueFamilyIndex != toRead.dstQueueFamilyIndex}; + const bool ordered = recorder != nullptr && seeds.empty() && padding.copies.empty() && Recorder::ElideCovered(Recorder::CommandClass::StorageWriteBack, ready, covered); + if (!ordered) context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &importReady, 0, nullptr, 1, &toRead); for (std::size_t i = 0; i < windows.size(); ++i) { const auto& window = windows[i]; auto retile = window.window; retile.pipeBankXor = descriptor.pipeBankXor; - detiler.DispatchImage(commands, descriptor.tileMode, elementBytes, tiledScratch->Handle(), scratchPositions[i], elementLayerView(window.level, window.layer), mips[window.level], true, window.layer, retile); + for (auto k = directRanges[i].first; k < directRanges[i].second; ++k) { + auto piece = directPieces[k].window; + piece.pipeBankXor = descriptor.pipeBankXor; + detiler.DispatchImage(commands, descriptor.tileMode, elementBytes, directPieces[k].slab, directPieces[k].offset, elementLayerView(window.level, window.layer), mips[window.level], true, window.layer, piece); + } + if (directRanges[i].first == directRanges[i].second) detiler.DispatchImage(commands, descriptor.tileMode, elementBytes, tiledScratch->Handle(), scratchPositions[i], elementLayerView(window.level, window.layer), mips[window.level], true, window.layer, retile); } singlePassMoves.fetch_add(1, std::memory_order_relaxed); } else { @@ -2921,17 +3564,27 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp const auto& window = windows[i]; auto retile = window.window; retile.pipeBankXor = descriptor.pipeBankXor; - detiler.Dispatch(commands, descriptor.tileMode, elementBytes, linear->Handle(), linearPositions[i], tiledScratch->Handle(), scratchPositions[i], mips[window.level], true, window.layer, false, retile); + for (auto k = directRanges[i].first; k < directRanges[i].second; ++k) { + auto piece = directPieces[k].window; + piece.pipeBankXor = descriptor.pipeBankXor; + detiler.Dispatch(commands, descriptor.tileMode, elementBytes, linear->Handle(), linearPositions[i] + directPieces[k].linearOffset, directPieces[k].slab, directPieces[k].offset, mips[window.level], true, window.layer, false, piece); + } + if (directRanges[i].first == directRanges[i].second) detiler.Dispatch(commands, descriptor.tileMode, elementBytes, linear->Handle(), linearPositions[i], tiledScratch->Handle(), scratchPositions[i], mips[window.level], true, window.layer, false, retile); } } - { + if (tiledScratch) { // The retiled scratch (and a seed's slab bytes, which the scratch copies overwrite in // part) precede the copies into the slabs and the import. const VkMemoryBarrier scratchDone{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT}; context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &scratchDone, 0, nullptr, 0, nullptr); const auto copyBuffer = context.Resolved(&DeviceFunctions::cmdCopyBuffer, "vkCmdCopyBuffer"); - for (const auto& pieces : slabPieces) copyBuffer(commands, tiledScratch->Handle(), pieces.slab->buffer, static_cast(pieces.copies.size()), pieces.copies.data()); - if (!importCopies.empty()) copyBuffer(commands, tiledScratch->Handle(), import.buffer, static_cast(importCopies.size()), importCopies.data()); + for (const auto& pieces : slabPieces) { + if (!pieces.copies.empty()) copyBuffer(commands, tiledScratch->Handle(), pieces.slab->buffer, static_cast(pieces.copies.size()), pieces.copies.data()); + } + for (std::size_t segment = 0; segment < segments.size(); ++segment) { + if (!importCopies[segment].empty()) copyBuffer(commands, tiledScratch->Handle(), segments[segment].import->buffer, static_cast(importCopies[segment].size()), importCopies[segment].data()); + } + if (!saves.empty()) copyBuffer(commands, tiledScratch->Handle(), save->Handle(), static_cast(saves.size()), saves.data()); } VkImageMemoryBarrier backToGeneral = toSource; backToGeneral.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; @@ -3005,14 +3658,14 @@ void StorageTexture::MarkDirty() { markLayersPending(0, trackedLayers); } -void StorageTexture::MarkSliceDirty(std::uint32_t slice) { - if (geometry.thick || geometry.imageLayers <= 1 || slice >= geometry.imageLayers || geometry.layerBytes == 0 || trackedLayerBytes == 0 || descriptor.dccAddress != 0) { +void StorageTexture::MarkSliceDirty(std::uint32_t slice, std::uint32_t count) { + if (geometry.thick || geometry.imageLayers <= 1 || count == 0 || slice >= geometry.imageLayers || count > geometry.imageLayers - slice || geometry.layerBytes == 0 || trackedLayerBytes == 0 || descriptor.dccAddress != 0) { MarkDirty(); return; } const auto begin = geometry.GuestLayerOffset(slice); const auto first = static_cast(begin / trackedLayerBytes); - const auto last = static_cast(std::min((begin + geometry.layerBytes + trackedLayerBytes - 1) / trackedLayerBytes, trackedLayers)); + const auto last = static_cast(std::min((begin + geometry.layerBytes * count + trackedLayerBytes - 1) / trackedLayerBytes, trackedLayers)); if (first >= last) { MarkDirty(); return; @@ -3028,8 +3681,54 @@ void StorageTexture::refreshGeneration() { generation = *std::min_element(layerGeneration.begin(), layerGeneration.end()); } +std::uint64_t StorageTexture::GenerationFor(std::uint64_t address, std::uint64_t bytes) const { + if (layerBehind.empty() && generation != 0) return generation; + std::optional oldest; + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + const auto begin = layerBegin(layer); + if (address >= begin + layerBytes(layer) || begin >= address + bytes) continue; + if (!layerBehind.empty() && layerBehind[layer]) return 0; + oldest = std::min(oldest.value_or(layerGeneration[layer]), layerGeneration[layer]); + } + return oldest.value_or(generation); +} + +std::uint64_t StorageTexture::collectedLayerGeneration(std::uint32_t layer, std::uint64_t whole, bool uncached) const { + if (whole != 0 || trackedLayers <= 1) return whole; + const auto begin = layerBegin(layer); + const auto bytes = static_cast(layerBytes(layer)); + return uncached ? GuestMemory::CollectWritesUncached(begin, bytes) : GuestMemory::CollectWrites(begin, bytes); +} + +void StorageTexture::noteLayersBehind(std::uint32_t firstLayer, std::uint32_t lastLayer, const std::vector& outside) { + if (layerBehind.size() != trackedLayers) layerBehind.assign(trackedLayers, false); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (layer >= firstLayer && layer < lastLayer) layerBehind[layer] = false; + else if (layer < outside.size() && outside[layer]) layerBehind[layer] = true; + } + const auto layersEnd = layerBegin(lastLayer - 1) + layerBytes(lastLayer - 1); + const std::array, 2> ranges{{{descriptor.baseAddress, layerBegin(firstLayer)}, {layersEnd, descriptor.baseAddress + guestBytes}}}; + for (const auto& [from, to] : ranges) { + if (to <= from) continue; + for (const auto& other : overlappingPending(from, static_cast(to - from))) { + const bool whole = other->descriptor.dccAddress != 0 && other->uploadedKeys != DccKeys::Uncompressed; + for (std::uint32_t unit = 0; unit < other->trackedLayers; ++unit) { + if (!whole && !other->layerPending[unit]) continue; + const auto begin = std::max(from, other->layerBegin(unit)); + const auto end = std::min(to, other->layerBegin(unit) + other->layerBytes(unit)); + if (end <= begin) continue; + const auto first = static_cast((begin - descriptor.baseAddress) / trackedLayerBytes); + const auto last = static_cast(std::min((end - descriptor.baseAddress + trackedLayerBytes - 1) / trackedLayerBytes, trackedLayers)); + for (auto layer = first; layer < last; ++layer) layerBehind[layer] = true; + } + } + } + if (std::none_of(layerBehind.begin(), layerBehind.end(), [](bool behind) { return behind; })) layerBehind.clear(); +} + void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) { CaptureTrace::Log("image-write image=%llx first=%u count=%u generation=%llu", static_cast(descriptor.baseAddress), first, count, static_cast(generation)); + ++writes; static const bool eager = std::getenv("APS5_EAGER_WRITEBACK") != nullptr || std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; std::vector unsaved(trackedLayers, false); bool anyUnsaved = false; @@ -3054,6 +3753,7 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) } if (superseded) source->reconcilePending(); } + if (!handedOver.empty() || !handedUnits.empty()) supersedeHandedOver(first, count); if (eager) { WriteBack(); return; @@ -3091,6 +3791,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(); @@ -3126,8 +3832,25 @@ bool StorageTexture::pendingUnitInside(std::uint64_t address, std::size_t bytes) return false; } +namespace { + +void waitForIdlePublish(const char* reason) { + if (reason == nullptr || std::strcmp(reason, "idle") != 0 || Recorder::InCompletion()) return; + if (auto* recorder = Recorder::Active()) { + Recorder::CountSync(0); + recorder->Sync(); + } +} + +} + bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except, const char* reason, PublishScope scope, bool* published) { + static_cast(FlushDepthPlanes(address, bytes)); if (published != nullptr) *published = false; + if (AgcDriver::DriverDetail::DrawPipeline::HeldReads(address, bytes)) { + queuedReadHeld.fetch_add(1, std::memory_order_relaxed); + return false; + } // The images stay alive across the scan: their last owner may be a batch's kept list, which // another thread releases outside the GPU mutex once the batch completed. std::vector> flush; @@ -3156,14 +3879,16 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons } // Still pending for the adjacency rule of writeBack until each one is stored. for (const auto& texture : flush) pending.flushing.push_back(texture.get()); + pending.flushingCount.store(pending.flushing.size(), std::memory_order_release); if (!flush.empty()) BumpPendingSerial(); } if (flush.empty()) { + const bool waited = WaitForFlushing(address, bytes, except); // Units shadowed over the range (another image's results retiled into the import's unit // shadow, or an earlier flush's) still reach the import as the scope asks. const bool units = PublishShadowsOnly(address, bytes, scope, reason); if (published != nullptr) *published = units; - return false; + return waited; } struct Unregister { const std::vector>& flush; @@ -3171,6 +3896,7 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons auto& pending = Pending(); std::lock_guard lock(pending.mutex); for (const auto& texture : flush) std::erase(pending.flushing, texture.get()); + pending.flushingCount.store(pending.flushing.size(), std::memory_order_release); BumpPendingSerial(); } } unregister{flush}; @@ -3180,6 +3906,20 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons for (const auto& texture : flush) std::fprintf(stderr, "[flush] image 0x%llx+0x%llx for %s 0x%llx+0x%zx\n", static_cast(texture->descriptor.baseAddress), static_cast(texture->guestBytes), reason, static_cast(address), bytes); } std::lock_guard gpu(GuestMemory::GpuMutex()); + if (auto* recorder = Recorder::Active(); recorder != nullptr && !Recorder::InCompletion() && recorder->PendingWriterSerial(address, bytes) != 0) { + try { + recorder->Reap(); + if (const auto writer = recorder->PendingWriterSerial(address, bytes); writer == recorder->OpenSerial()) { + Recorder::CountSync(recorder->OpenWriteOverlaps(address, bytes) ? 1 : 3); + recorder->Sync(); + } else if (writer != 0) { + recorder->FinishUpTo(writer); + } + } catch (...) { + for (const auto& texture : flush) texture->reconcilePending(); + throw; + } + } std::exception_ptr failure; const auto previousReason = std::exchange(flushReason, reason); for (const auto& texture : flush) { @@ -3197,11 +3937,26 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons // import, so they are published under the same hold, after every store. if (scope != PublishScope::None) { const auto units = PublishShadow(address, bytes, scope, PublishReasonFor(reason)); + if (units != 0) waitForIdlePublish(reason); if (published != nullptr) *published = units != 0; } return true; } +bool StorageTexture::WaitForFlushing(std::uint64_t address, std::size_t bytes, const StorageTexture* except) { + auto& pending = Pending(); + if (pending.flushingCount.load(std::memory_order_acquire) == 0 || GuestMemory::GpuMutex().HeldByThisThread()) return false; + const auto* exempt = refreshing; + for (bool waited = false;; waited = true) { + { + std::lock_guard lock(pending.mutex); + if (std::none_of(pending.flushing.begin(), pending.flushing.end(), [&](const StorageTexture* texture) { return texture != except && texture != exempt && texture->overlaps(address, bytes); })) return waited; + } + { std::lock_guard gpu(GuestMemory::GpuMutex()); } + std::this_thread::yield(); + } +} + void StorageTexture::SaveBeforeUnwatch(std::uint64_t address, std::size_t bytes) { std::vector> overlapping; { @@ -3234,22 +3989,32 @@ void StorageTexture::forgetSavedOver(std::uint64_t address, std::size_t bytes) { // pending there (the flush before the fill or copy stored only the units it overlaps) are // compared with it at their store when the memory is outside the write watch. for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { - if (layerBegin(unit) < address + bytes && address < layerBegin(unit) + layerBytes(unit)) layerOriginal[unit] = false; + if (layerBegin(unit) < address + bytes && address < layerBegin(unit) + layerBytes(unit)) { + layerOriginal[unit] = false; + cancelStoredSave(unit); + } } } void StorageTexture::saveBeforeUnwatch() { std::vector units(trackedLayers, false); - for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) units[unit] = layerPending[unit] && !layerOriginal[unit] && layerGeneration[unit] != 0; + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) units[unit] = layerPending[unit] && !layerOriginal[unit]; const auto runs = unitRuns(units); if (runs.empty()) return; original.resize(static_cast(guestBytes)); - comparedGuestBytes = {}; + comparedRanges = {}; for (const auto& [begin, end] : runs) GuestMemory::ReadCommittedUnsynced(descriptor.baseAddress + begin, std::span(original).subspan(static_cast(begin), static_cast(end - begin))); for (const auto& [begin, end] : runs) GuestMemory::CollectWritesUncached(descriptor.baseAddress + begin, static_cast(end - begin)); constexpr std::uint64_t block = 65536; for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { if (!units[unit]) continue; + if (layerGeneration[unit] == 0) { + // Without a write generation the watch stores the unit whole: the bytes before this + // moment are the ones its results replace. + layerOriginal[unit] = true; + cancelStoredSave(unit); + continue; + } const auto begin = layerBegin(unit); const auto end = begin + layerBytes(unit); const auto spanBegin = begin & ~(block - 1); @@ -3265,13 +4030,82 @@ void StorageTexture::saveBeforeUnwatch() { if (changedBlocks[static_cast((at - spanBegin) / block)] == GuestMemory::BlockWritten && (!edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[unit])) && !unchangedSinceBaseline(from, to)) keptAtUnwatch.emplace(from, layerGeneration[unit]); } layerOriginal[unit] = true; + cancelStoredSave(unit); } } +std::vector StorageTexture::prepareStoredSave(const std::vector& layers, const std::vector& skippedLayer) { + std::vector units; + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { + if (layers[unit] && !skippedLayer[unit]) units.push_back(unit); + } + if (!units.empty() && !savedBytes) savedBytes = std::make_shared(context, static_cast(guestBytes), VK_BUFFER_USAGE_TRANSFER_DST_BIT); + return units; +} + +void StorageTexture::commitStoredSave(const std::vector& units) { + if (units.empty()) return; + const auto ticket = ++lastSaveTicket; + for (const auto unit : units) savedTicket[unit] = ticket; + auto* recorder = Recorder::Active(); + if (recorder == nullptr) { + applyStoredSave(ticket, units); + return; + } + recorder->OnComplete([texture = weak_from_this(), bytes = savedBytes, ticket, units] { + if (auto alive = texture.lock()) alive->applyStoredSave(ticket, units); + }); +} + +void StorageTexture::applyStoredSave(std::uint64_t ticket, const std::vector& units) { + if (!savedBytes) return; + const auto stored = savedBytes->Bytes(); + for (const auto unit : units) { + if (savedTicket[unit] != ticket) continue; + if (original.size() != guestBytes) original.resize(static_cast(guestBytes)); + comparedRanges = {}; + const auto offset = static_cast(layerBegin(unit) - descriptor.baseAddress); + std::memcpy(original.data() + offset, stored.data() + offset, static_cast(layerBytes(unit))); + layerOriginal[unit] = true; + savedTicket[unit] = 0; + } +} + +void StorageTexture::cancelStoredSave(std::uint32_t unit) { + if (unit < savedTicket.size()) savedTicket[unit] = 0; +} + +void StorageTexture::cancelStoredSaves() { + savedTicket.assign(trackedLayers, 0); +} + +void StorageTexture::awaitStoredSave(const std::vector* layers) { + std::uint64_t first = descriptor.baseAddress + guestBytes; + std::uint64_t last = descriptor.baseAddress; + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { + if (savedTicket[unit] == 0 || (layers != nullptr && !(*layers)[unit])) continue; + first = std::min(first, layerBegin(unit)); + last = std::max(last, layerBegin(unit) + layerBytes(unit)); + } + if (last <= first) return; + auto* recorder = Recorder::Active(); + Require(recorder != nullptr, "a write-back's saved guest bytes are pending without a recorder"); + recorder->SyncThrough(first, static_cast(last - first)); + const auto outstanding = [&] { + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { + if (savedTicket[unit] != 0 && (layers == nullptr || (*layers)[unit])) return true; + } + return false; + }; + if (outstanding()) recorder->Sync(); +} + bool StorageTexture::PublishShadowsOnly(std::uint64_t address, std::size_t bytes, PublishScope scope, const char* reason) { if (scope == PublishScope::None || !AnyShadowedOverlaps(address, bytes)) return false; std::lock_guard gpu(GuestMemory::GpuMutex()); - return PublishShadow(address, bytes, scope, PublishReasonFor(reason)) != 0; + if (PublishShadow(address, bytes, scope, PublishReasonFor(reason)) == 0) return false; + waitForIdlePublish(reason); + return true; } namespace { @@ -3323,6 +4157,26 @@ void StorageTexture::FlushAllPending(const char* reason) { static_cast(FlushPending(0, std::numeric_limits::max(), nullptr, reason)); } +std::size_t StorageTexture::StoreSmallPending(const char* reason) { + std::vector> small; + { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + for (auto* texture : pending.textures) { + if (texture->guestBytes > SmallStoreBytes) continue; + if (auto alive = texture->weak_from_this().lock()) small.push_back(std::move(alive)); + } + } + std::size_t stored = 0; + for (const auto& texture : small) { + const auto address = texture->descriptor.baseAddress; + const auto bytes = static_cast(texture->guestBytes); + if (!HostImportCovers(texture->context, address, bytes)) continue; + if (FlushPending(address, bytes, nullptr, reason)) ++stored; + } + return stored; +} + bool StorageTexture::AnyPending() { auto& pending = Pending(); std::lock_guard lock(pending.mutex); @@ -3335,6 +4189,12 @@ bool StorageTexture::StoreAtFlipRequested(const char* value) { throw std::runtime_error(std::string("APS5_STORE_AT_FLIP=") + value + ": expected 1"); } +bool StorageTexture::StoreAtLabelRequested(const char* value) { + if (value == nullptr) return false; + if (std::strcmp(value, "1") == 0) return true; + throw std::runtime_error(std::string("APS5_STORE_AT_LABEL=") + value + ": expected 1"); +} + std::shared_ptr StorageTexture::FindPending(std::uint64_t address, std::uint64_t bytes) { auto& pending = Pending(); std::lock_guard lock(pending.mutex); @@ -3401,6 +4261,19 @@ bool StorageTexture::AnyPendingOverlaps(std::span> ranges) { + auto& live = Live(); + std::lock_guard lock(live.mutex); + for (const auto& range : ranges) { + if (range.second <= range.first) continue; + const auto bytes = static_cast(range.second - range.first); + for (const auto* texture : live.textures) { + if (texture->overlapsLive(range.first, bytes)) return true; + } + } + return false; +} + std::uint64_t StorageTexture::PendingSerial() { return pendingSerial.load(std::memory_order_acquire); } @@ -3413,6 +4286,7 @@ std::vector> StorageTexture::overlappingPending( // FlushPending's listing; the images stay alive across the caller's scan as there. std::vector> overlapping; auto& pending = Pending(); + if (pending.textures.SurelyClear(address, bytes)) return overlapping; std::lock_guard lock(pending.mutex); if (!pending.textures.MayOverlap(address, bytes)) return overlapping; const auto* exempt = refreshing; @@ -3543,6 +4417,7 @@ bool StorageTexture::evictStale(std::uint64_t address, std::size_t bytes, std::s if (!selected(layer)) continue; layerPending[layer] = false; layerOriginal[layer] = false; + cancelStoredSave(layer); } if (!anyLayerPending()) { ++dropped; @@ -3576,9 +4451,11 @@ bool StorageTexture::ScanPending(std::span queries) { query.found = nullptr; if (query.end <= query.begin) continue; const auto bytes = static_cast(query.end - query.begin); - for (const auto* texture : pending.textures) { - if (query.found == nullptr && texture->descriptor.baseAddress == query.begin && texture->guestBytes >= bytes) query.found = texture; - if (texture != query.except && texture->overlaps(query.begin, bytes)) query.overlaps = true; + if (pending.textures.MayOverlap(query.begin, bytes)) { + for (const auto* texture : pending.textures) { + if (query.found == nullptr && texture->descriptor.baseAddress == query.begin && texture->guestBytes >= bytes) query.found = texture; + if (texture != query.except && texture->overlaps(query.begin, bytes)) query.overlaps = true; + } } for (const auto* texture : pending.flushing) { if (texture != query.except && texture->overlaps(query.begin, bytes)) query.overlaps = true; @@ -3604,6 +4481,7 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz texture->dirty = false; texture->layerPending.assign(texture->trackedLayers, false); texture->layerOriginal.assign(texture->trackedLayers, false); + texture->cancelStoredSaves(); it = pending.textures.erase(it); ++discarded; } else if (texture != exempt && texture->blockUnits && texture->overlaps(address, bytes)) { @@ -3616,6 +4494,7 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz if (!texture->layerPending[unit] || unitBegin < address || unitBegin + texture->layerBytes(unit) > end) continue; texture->layerPending[unit] = false; texture->layerOriginal[unit] = false; + texture->cancelStoredSave(unit); unitsDropped.fetch_add(1, std::memory_order_relaxed); dropped = true; } @@ -3793,7 +4672,7 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) { } const auto commands = recorder->Commands(); const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::DccClear); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); Recorder::CountBarriers(Recorder::CommandClass::DccClear, 2); if (Recorder::BarrierValidate()) { const std::pair clearedImage{image, true}; @@ -3818,11 +4697,13 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) { context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); recorder->EndGpuTiming(timing, guestBytes); layerOriginal.assign(trackedLayers, false); + cancelStoredSaves(); uploadedKeys = keys; filledKeys = DccKeys::Uncompressed; keyProof = {}; forgetBorrowed(0, trackedLayers); - layerGeneration.assign(trackedLayers, GuestMemory::CollectWrites(descriptor.baseAddress, static_cast(guestBytes))); + const auto now = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast(guestBytes)); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) layerGeneration[layer] = collectedLayerGeneration(layer, now); refreshGeneration(); ++version; if (traceKeys) std::fprintf(stderr, "[dcc-keys] key fill %s clears 0x%llx+0x%llx at once (keys 0x%llx, %zu pending units dropped, dirty %d)\n", DccKeysName(keys), static_cast(descriptor.baseAddress), static_cast(guestBytes), static_cast(descriptor.dccAddress), droppedUnits, wasDirty ? 1 : 0); @@ -3927,7 +4808,7 @@ bool StorageTexture::FillClear(std::span pattern, std::u const auto commands = recorder->Commands(); const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::FillClear); Recorder::CountBarriers(Recorder::CommandClass::FillClear, 2); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); if (Recorder::BarrierValidate()) { const std::pair cleared{image, true}; recorder->NoteAccess(Recorder::CommandClass::FillClear, Recorder::Access{{}, {}, std::span(&cleared, 1), VK_PIPELINE_STAGE_TRANSFER_BIT}); @@ -3960,7 +4841,10 @@ bool StorageTexture::FillClear(std::span pattern, std::u // walk, the write-back's own walk would stamp them above this generation and keep their // blocks, and the fill's pattern would be lost in them. const bool watched = GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes)); - if (watched) layerOriginal.assign(trackedLayers, false); + if (watched) { + layerOriginal.assign(trackedLayers, false); + cancelStoredSaves(); + } uploadedKeys = DccKeys::Uncompressed; if (layer != WholeImage && trackedLayers > 1) { // The cleared layer's units alone: their stamps above lie below this generation, the other @@ -3980,7 +4864,8 @@ bool StorageTexture::FillClear(std::span pattern, std::u return true; } if (!watched) captureGuestBytes(nullptr); - layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast(guestBytes))); + const auto now = GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast(guestBytes)); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) layerGeneration[layer] = collectedLayerGeneration(layer, now, true); refreshGeneration(); forgetBorrowed(0, trackedLayers); MarkDirty(); @@ -4010,28 +4895,19 @@ bool StorageTexture::SameSurfaceShape(const StorageTexture& other) const { return guestBytes == other.guestBytes && mine.width == theirs.width && mine.height == theirs.height && mine.depthOrLastArray == theirs.depthOrLastArray && mine.baseArray == theirs.baseArray && mine.mipCount == theirs.mipCount && mine.tileMode == theirs.tileMode && mine.dimension == theirs.dimension && mine.format == theirs.format && storageFormat == other.storageFormat && geometry.imageLayers == other.geometry.imageLayers && geometry.imageDepth == other.geometry.imageDepth; } +const char* StorageTexture::CopyRefusal(const Context& context, const StorageTexture* destination, const StorageTexture& source, std::uint64_t address) { + if (destination != nullptr && (destination == &source || !destination->SameSurfaceShape(source))) return "shape"; + if (source.descriptor.dccAddress != 0 || (destination != nullptr && destination->descriptor.dccAddress != 0)) return "keys"; + if (source.released || !source.Cached() || (destination != nullptr && (destination->released || !destination->Cached()))) return "released"; + if (HostImportFor(context, address, static_cast(source.guestBytes)) == nullptr) return "not imported"; + if (Recorder::Active() == nullptr) return "no recorder"; + return nullptr; +} + bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { - if (&source == this || !SameSurfaceShape(source)) { - refusal = "shape"; - return false; - } - if (descriptor.dccAddress != 0 || source.descriptor.dccAddress != 0) { - refusal = "keys"; - return false; - } - if (released || source.released || !Cached() || !source.Cached()) { - refusal = "released"; - return false; - } - if (HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes)) == nullptr) { - refusal = "not imported"; - return false; - } + refusal = CopyRefusal(context, this, source, descriptor.baseAddress); + if (refusal != nullptr) return false; auto* recorder = Recorder::Active(); - if (recorder == nullptr) { - refusal = "no recorder"; - return false; - } // The source image must hold the surface: a stale one uploads first (and other images' // results pending over its memory land first, as the transfer's flush would have them). source.Refresh(); @@ -4051,8 +4927,8 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { const auto commands = recorder->Commands(); const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy); Recorder::CountBarriers(Recorder::CommandClass::Copy, 2); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); - if (auto other = source.weak_from_this().lock()) recorder->Keep(std::move(other)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); + if (auto other = source.weak_from_this().lock()) keepStorageTexture(*recorder, std::move(other)); if (Recorder::BarrierValidate()) { const std::array, 2> images{{{source.image, false}, {image, true}}}; recorder->NoteAccess(Recorder::CommandClass::Copy, Recorder::Access{{}, {}, images, VK_PIPELINE_STAGE_TRANSFER_BIT}); @@ -4092,17 +4968,58 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { copied.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &copied); recorder->EndGpuTiming(timing, guestBytes); + MarkWholeWritten(); + return true; +} + +void StorageTexture::MarkWholeWritten() { // As after a clear: the image holds the surface, guest memory follows at the write-back (which // keeps the 64 KiB blocks the CPU writes from now on; the collect is uncached for the reason // given in FillClear). - if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) layerOriginal.assign(trackedLayers, false); - else captureGuestBytes(nullptr); + if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) { + layerOriginal.assign(trackedLayers, false); + cancelStoredSaves(); + } else { + captureGuestBytes(nullptr); + } uploadedKeys = DccKeys::Uncompressed; - layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast(guestBytes))); + const auto now = GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast(guestBytes)); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) layerGeneration[layer] = collectedLayerGeneration(layer, now, true); refreshGeneration(); forgetBorrowed(0, trackedLayers); MarkDirty(); - return true; +} + +void StorageTexture::MarkUnitsWritten(const std::vector& units) { + if (!blockUnits || descriptor.dccAddress != 0 || units.size() != trackedLayers || std::all_of(units.begin(), units.end(), [](bool unit) { return unit; })) { + MarkWholeWritten(); + return; + } + const auto runs = unitRuns(units); + if (runs.empty()) return; + if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) { + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { + if (!units[unit]) continue; + layerOriginal[unit] = false; + cancelStoredSave(unit); + } + } else { + captureGuestBytes(&units); + } + uploadedKeys = DccKeys::Uncompressed; + for (const auto& [begin, end] : runs) { + const auto first = static_cast(begin / trackedLayerBytes); + const auto count = static_cast((end - begin + trackedLayerBytes - 1) / trackedLayerBytes); + const auto collected = GuestMemory::CollectWritesUncached(descriptor.baseAddress + begin, static_cast(end - begin)); + for (auto unit = first; unit < first + count; ++unit) layerGeneration[unit] = collected; + } + refreshGeneration(); + for (const auto& [begin, end] : runs) { + const auto first = static_cast(begin / trackedLayerBytes); + const auto count = static_cast((end - begin + trackedLayerBytes - 1) / trackedLayerBytes); + forgetBorrowed(first, count); + markLayersPending(first, count); + } } // See AdjacentGenerationEnabled above. @@ -4174,6 +5091,104 @@ void StorageTexture::blockGenerations(std::vector& generations) c } } +namespace { + +std::atomic& ClearHandOverOn() { + static std::atomic on{std::getenv("APS5_NO_CLEAR_HAND_OVER") == nullptr}; + return on; +} + +} + +void StorageTexture::SetClearHandOver(bool enabled) { + ClearHandOverOn().store(enabled, std::memory_order_relaxed); +} + +std::uint64_t StorageTexture::ClearHandOvers() { + return clearHandOvers.load(std::memory_order_relaxed); +} + +std::uint64_t StorageTexture::HandedOverSuperseded() { + return handedOverSuperseded.load(std::memory_order_relaxed); +} + +std::optional StorageTexture::clearHandOver() { + if (!ClearHandOverOn().load(std::memory_order_relaxed) || !blockUnits || descriptor.dccAddress == 0 || anyLayerPending()) return std::nullopt; + if (!GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) return std::nullopt; + const auto others = overlappingPending(descriptor.baseAddress, static_cast(guestBytes)); + if (others.empty()) return std::nullopt; + const auto begin = descriptor.baseAddress; + const auto end = descriptor.baseAddress + guestBytes; + const auto keyBytes = DccKeyBytes(guestBytes); + for (const auto& other : others) { + if (!other->blockUnits || (other->descriptor.dccAddress != 0 && other->uploadedKeys != DccKeys::Uncompressed)) return std::nullopt; + if (other->descriptor.dccAddress != 0 && other->descriptor.dccAddress < descriptor.dccAddress + keyBytes && descriptor.dccAddress < other->descriptor.dccAddress + DccKeyBytes(other->guestBytes)) return std::nullopt; + for (std::uint32_t unit = 0; unit < other->trackedLayers; ++unit) { + if (!other->layerPending[unit]) continue; + const auto from = other->layerBegin(unit); + const auto to = from + other->layerBytes(unit); + if (to <= begin || from >= end) continue; + if (from < begin || to > end || other->layerBytes(unit) != trackedLayerBytes) return std::nullopt; + } + } + const auto keys = ProvedKeys(); + VkClearColorValue clear{}; + if (!IsDccClear(keys) || !ClearColorFor(storageFormat, keys, clear)) return std::nullopt; + if (handedUnits.size() != trackedLayers) handedUnits.assign(trackedLayers, false); + for (const auto& other : others) { + for (std::uint32_t unit = 0; unit < other->trackedLayers; ++unit) { + const auto from = other->layerBegin(unit); + if (other->layerPending[unit] && from >= begin && from < end) handedUnits[static_cast((from - begin) / trackedLayerBytes)] = true; + } + const auto same = std::find_if(handedOver.begin(), handedOver.end(), [&](const auto& entry) { return entry.first.lock() == other; }); + if (same != handedOver.end()) same->second = other->writes; + else handedOver.emplace_back(other, other->writes); + } + clearHandOvers.fetch_add(1, std::memory_order_relaxed); + return keys; +} + +void StorageTexture::supersedeHandedOver(std::uint32_t first, std::uint32_t count) { + const auto begin = layerBegin(first); + const auto end = first + count >= trackedLayers ? descriptor.baseAddress + guestBytes : layerBegin(first + count); + for (auto entry = handedOver.begin(); entry != handedOver.end();) { + const auto source = entry->first.lock(); + if (source == nullptr || source->writes != entry->second) { + entry = handedOver.erase(entry); + continue; + } + bool superseded = false; + for (std::uint32_t unit = 0; unit < source->trackedLayers; ++unit) { + const auto from = source->layerBegin(unit); + if (!source->layerPending[unit] || from < begin || from + source->layerBytes(unit) > end) continue; + source->layerPending[unit] = false; + handedOverSuperseded.fetch_add(1, std::memory_order_relaxed); + superseded = true; + } + if (superseded) source->reconcilePending(); + ++entry; + } + if (first == 0 && count >= trackedLayers) handedOver.clear(); + if (std::any_of(handedUnits.begin() + first, handedUnits.begin() + std::min(first + count, handedUnits.size()), [](bool handed) { return handed; })) { + const auto current = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast(guestBytes)); + std::vector generations; + blockGenerations(generations); + std::vector stamped(generations.size(), 0); + std::vector cpu(generations.size(), 0); + const bool tracked = GuestMemory::ChangedBlocks(descriptor.baseAddress, static_cast(guestBytes), generations, stamped, cpu); + bool moved = false; + for (std::uint32_t unit = first; unit < first + count && unit < handedUnits.size(); ++unit) { + if (!handedUnits[unit]) continue; + handedUnits[unit] = false; + if (!tracked || unit >= cpu.size() || cpu[unit] != 0) continue; + layerGeneration[unit] = current; + moved = true; + } + if (moved) refreshGeneration(); + } + if (std::none_of(handedUnits.begin(), handedUnits.end(), [](bool handed) { return handed; })) handedUnits.clear(); +} + std::shared_ptr StorageTexture::pendingAlias() const { static const bool disabled = std::getenv("APS5_NO_ALIAS_BORROW") != nullptr; if (disabled || !blockUnits) return nullptr; @@ -4225,8 +5240,8 @@ std::uint64_t StorageTexture::borrowUnits(StorageTexture& source, const std::vec if (recorder != nullptr) { commands = recorder->Commands(); timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageUpload); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); - if (auto other = source.weak_from_this().lock()) recorder->Keep(std::move(other)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); + if (auto other = source.weak_from_this().lock()) keepStorageTexture(*recorder, std::move(other)); Recorder::CountBarriers(Recorder::CommandClass::StorageUpload, 2); if (Recorder::BarrierValidate()) { const std::array, 2> images{{{source.image, false}, {image, true}}}; @@ -4378,6 +5393,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { const bool tracked = GuestMemory::ChangedBlocks(firstStored, static_cast(lastStored - firstStored), generations, changedBlocks); std::vector compared(trackedLayers, false); if (!tracked) { + awaitStoredSave(&layers); std::vector saved(trackedLayers, false); for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) saved[layer] = layers[layer] && layerOriginal[layer]; for (const auto& [begin, end] : unitRuns(saved)) { @@ -4389,7 +5405,9 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { } const auto forgetStored = [&] { for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { - if (layers[layer]) layerOriginal[layer] = false; + if (!layers[layer]) continue; + layerOriginal[layer] = false; + cancelStoredSave(layer); } }; bool unsavedOutsideWatch = false; @@ -4439,23 +5457,32 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // own stamps, so every later import writer stamps newer. std::vector shadowed; std::vector> imported; - const HostImport* shadowImport = nullptr; + std::vector segments; + const auto markShadowed = [&](std::uint64_t generation) { + for (const auto& segment : segments) { + std::vector ranges; + for (const auto& range : shadowed) { + if (range.begin < segment.begin || range.begin >= segment.end) continue; + Require(range.end <= segment.end, "a shadowed storage write-back range crosses the end of its import segment"); + ranges.push_back(range); + } + if (!ranges.empty()) MarkShadowed(*segment.import, ranges, generation); + } + }; const auto settle = [&](bool memoizedCollect) { for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (layers[layer]) layerPending[layer] = false; } - const bool allKept = std::none_of(skippedLayer.begin(), skippedLayer.end(), [](bool kept) { return kept; }); - if (allKept || !adjacent.empty()) { - const auto now = memoizedCollect ? GuestMemory::CollectWrites(firstStored, static_cast(lastStored - firstStored)) : GuestMemory::CollectWritesUncached(firstStored, static_cast(lastStored - firstStored)); - for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { - if (layers[layer] && !skippedLayer[layer]) layerGeneration[layer] = now; - } - refreshGeneration(); - advanceAdjacent(adjacent, now, firstBlock, lastBlock); - if (!shadowed.empty()) MarkShadowed(*shadowImport, shadowed, now); - } else if (!shadowed.empty()) { - MarkShadowed(*shadowImport, shadowed, GuestMemory::TrackerGeneration()); + bool advances = false; + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) advances = advances || (layers[layer] && !skippedLayer[layer]); + if (!advances && adjacent.empty() && shadowed.empty()) return; + const auto now = memoizedCollect ? GuestMemory::CollectWrites(firstStored, static_cast(lastStored - firstStored)) : GuestMemory::CollectWritesUncached(firstStored, static_cast(lastStored - firstStored)); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (layers[layer] && !skippedLayer[layer]) layerGeneration[layer] = collectedLayerGeneration(layer, now, !memoizedCollect); } + refreshGeneration(); + advanceAdjacent(adjacent, now, firstBlock, lastBlock); + if (!shadowed.empty()) markShadowed(now); }; if (keep.empty()) { // Every selected block was written by the CPU since the layer's generation: nothing to @@ -4466,12 +5493,15 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { settle(true); return; } - if (const auto* import = HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes))) { + if (const auto* import = HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes))) segments.push_back({import, descriptor.baseAddress, descriptor.baseAddress + guestBytes}); + else segments = importSegments(); + if (!segments.empty()) { Require(!unsavedOutsideWatch, "storing storage image results pending over memory outside the write watch without the guest bytes saved before them is not implemented"); + // Outside the write watch the stored bytes are what the next store compares with. + const auto saveUnits = GuestMemory::Watched(firstStored, static_cast(lastStored - firstStored)) ? std::vector{} : prepareStoredSave(layers, skippedLayer); if (blockUnits) { // The kept blocks alone pass through the retiler (windows of their slices). - shadowImport = import; - const auto storedBytes = writeBackWindows(*import, keep, firstStored, lastStored, shadowed, imported); + const auto storedBytes = writeBackWindows(segments, keep, firstStored, lastStored, shadowed, imported, saveUnits.empty() ? nullptr : savedBytes.get()); countStorageWriteBack(storedBytes, true); if (profile) Profile().storageGpu += timer.lap(); forgetStored(); @@ -4480,6 +5510,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { uploadedKeys = DccKeys::Uncompressed; for (const auto& [from, to] : keep) GuestMemory::MarkWritten(from, static_cast(to - from)); settle(true); + commitStoredSave(saveUnits); ++Profile().storageDirectWriteBacks; return; } @@ -4494,7 +5525,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { auto tiledScratch = std::make_shared(context, static_cast(guestBytes), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); std::vector copied; for (const auto& [from, to] : keep) copied.push_back({from - descriptor.baseAddress, to - descriptor.baseAddress, from - descriptor.baseAddress}); - const auto padding = paddingSeeds(*import, std::move(copied)); + const auto padding = paddingSeeds(segments, copied); detiler.BeginBatch(); auto* recorder = Recorder::Active(); std::unique_ptr batch; @@ -4511,7 +5542,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { recorder->Keep(tiledScratch, tiledScratch->Size()); for (const auto& slab : padding.slabs) recorder->Keep(slab); // The image itself must outlive the recorded retile: the cache may evict it right after. - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); Recorder::CountBarriers(Recorder::CommandClass::StorageWriteBack, padding.copies.empty() ? 4 : 5); if (Recorder::BarrierValidate()) { const std::pair read{image, false}; @@ -4521,7 +5552,6 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { batch = std::make_unique(context); commands = batch->Handle(); } - const auto importOffset = descriptor.baseAddress - import->base; // The tracked layers are the array layers when there are several; one tracked layer means // the whole surface. const auto* storedLayers = trackedLayers == arrayLayers && trackedLayers > 1 ? &layers : nullptr; @@ -4558,9 +5588,20 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { { const auto scratchDone = WholeBufferBarrier(tiledScratch->Handle(), VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &scratchDone, 0, nullptr); - std::vector copies; - for (const auto& [from, to] : keep) copies.push_back({from - descriptor.baseAddress, importOffset + (from - descriptor.baseAddress), to - from}); - if (!copies.empty()) context.Function("vkCmdCopyBuffer")(commands, tiledScratch->Handle(), import->buffer, static_cast(copies.size()), copies.data()); + for (const auto& segment : segments) { + std::vector copies; + 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, HostImportOffset(*segment.import, begin), end - begin}); + } + if (!copies.empty()) context.Function("vkCmdCopyBuffer")(commands, tiledScratch->Handle(), segment.import->buffer, static_cast(copies.size()), copies.data()); + } + if (!saveUnits.empty()) { + std::vector saves; + for (const auto& [from, to] : keep) saves.push_back({from - descriptor.baseAddress, from - descriptor.baseAddress, to - from}); + context.Function("vkCmdCopyBuffer")(commands, tiledScratch->Handle(), savedBytes->Handle(), static_cast(saves.size()), saves.data()); + } } VkImageMemoryBarrier backToGeneral = toSource; backToGeneral.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; @@ -4591,6 +5632,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // newer than this value when its own range is collected. No CPU wrote the pages here // (MarkWritten made the only stamps), so the memoized collect is exact. settle(true); + commitStoredSave(saveUnits); ++Profile().storageDirectWriteBacks; return; } @@ -4599,7 +5641,25 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // the round trip through the CPU below would store stale texels over whatever the memory // holds now. Its selected units are dropped instead. APS5_NO_UNREGISTERED_DROP=1 stores. static const bool unregisteredDrop = std::getenv("APS5_NO_UNREGISTERED_DROP") == nullptr; + bool overhang = false; if (unregisteredDrop && !RegisteredReadableCovers(descriptor.baseAddress, static_cast(guestBytes))) { + const auto allocationEnd = RegisteredReadableEnd(descriptor.baseAddress); + const auto& level = mips.front(); + const auto rowBytes = std::uint64_t{level.height} * level.pitchBytes; + const auto lastRowTail = mips.size() == 1 && arrayLayers == 1 && !level.tail && level.tiledSize > rowBytes ? level.tiledSize - rowBytes : 0; + const auto end = descriptor.baseAddress + guestBytes; + if (allocationEnd > descriptor.baseAddress && end - allocationEnd <= lastRowTail) { + auto covered = allocationEnd; + while (covered < end) { + const auto next = RegisteredReadableEnd(covered); + if (next <= covered) break; + covered = next; + } + overhang = covered >= end; + } + } + const bool registered = std::all_of(keep.begin(), keep.end(), [](const auto& range) { return RegisteredReadableCovers(range.first, static_cast(range.second - range.first)); }); + if (unregisteredDrop && !overhang && !registered) { static std::atomic reports{0}; if (reports.fetch_add(1) < 4) std::fprintf(stderr, "[gpu] storage image 0x%llx+0x%llx: memory no longer registered; %zu pending ranges dropped\n", static_cast(descriptor.baseAddress), static_cast(guestBytes), keep.size()); unregisteredDropped.fetch_add(1, std::memory_order_relaxed); @@ -4617,7 +5677,13 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { Require(!unsavedOutsideWatch, "storing storage image results pending over memory outside the write watch without the guest bytes saved before them is not implemented"); std::vector unsaved = layerOriginal; unsaved.flip(); - for (const auto& [begin, end] : unitRuns(unsaved)) GuestMemory::ReadCommitted(descriptor.baseAddress + begin, std::span(original).subspan(static_cast(begin), static_cast(end - begin))); + for (const auto& [begin, end] : unitRuns(unsaved)) { + for (const auto& [from, to] : keep) { + const auto first = std::max(descriptor.baseAddress + begin, from); + const auto last = std::min(descriptor.baseAddress + end, to); + if (first < last) GuestMemory::ReadCommitted(first, std::span(original).subspan(static_cast(first - descriptor.baseAddress), static_cast(last - first))); + } + } DeviceBuffer linear(context, static_cast(sliceLinearBytes * arrayLayers), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); DeviceBuffer tiled(context, original.size(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); Buffer host(context, original.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); @@ -4702,7 +5768,9 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // The store above is the only write to these pages, so `original` is current at a fresh // generation, unless blocks were kept for the CPU: then the image is stale there. for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { - if (layers[layer]) layerOriginal[layer] = !skippedLayer[layer]; + if (!layers[layer]) continue; + layerOriginal[layer] = !skippedLayer[layer]; + cancelStoredSave(layer); } // As in the GPU-direct path, but the store's memcpy dirtied this surface's pages in the write // watch: a memoized collect would leave them for the next walk to stamp newer than this value @@ -4753,13 +5821,17 @@ void StorageTexture::release() noexcept { if (proxyMemory) context.Function("vkFreeMemory")(context.device, proxyMemory, nullptr); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); - if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + ReleaseImageMemory(context, memory, memoryBytes, memoryType); } std::uint64_t StorageTexture::GuestBytes() const { return guestBytes; } +std::uint64_t StorageTexture::BudgetBytes() const { + return std::max(AllocationBytes(), GuestBytes()); +} + VkImageView StorageTexture::View() const { return view; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp index 95ed372398..c571bd1710 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp @@ -4,7 +4,10 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/shaders/TextureDetile_spv.h" #include "prx/libSceAgcDriver/Graphics/shaders/CmaskClear_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/DepthSampleMultisample_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/DepthSampleSingle_spv.h" #include "prx/libSceAgcDriver/Graphics/shaders/TextureDetileImage_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/TextureRepack_spv.h" #include #include #include @@ -88,10 +91,7 @@ TextureDetiler::TextureDetiler(const Context& context) : context(context) { pipelineLayoutInfo.pushConstantRangeCount = 1; pipelineLayoutInfo.pPushConstantRanges = &pushRange; Check(context.Function("vkCreatePipelineLayout")(context.device, &pipelineLayoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(TEXTURE_DETILE_SPV); - moduleInfo.pCode = TEXTURE_DETILE_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule"); + module = CreateShaderModule(context, TEXTURE_DETILE_SPV, sizeof(TEXTURE_DETILE_SPV), "vkCreateShaderModule"); bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &layoutInfo, nullptr, &imageDescriptorLayout), "vkCreateDescriptorSetLayout"); pipelineLayoutInfo.pSetLayouts = &imageDescriptorLayout; @@ -107,14 +107,27 @@ TextureDetiler::~TextureDetiler() { } void TextureDetiler::release() noexcept { - for (const auto pool : cmaskDescriptorPools) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); + for (const auto pool : readbackDescriptorSets.pools) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); + for (const auto& entry : readbackPipelines) context.Function("vkDestroyPipeline")(context.device, entry.second, nullptr); + for (const auto shader : readbackModules) if (shader) context.Function("vkDestroyShaderModule")(context.device, shader, nullptr); + if (multisampleUploadModule) context.Function("vkDestroyShaderModule")(context.device, multisampleUploadModule, nullptr); + if (readbackPipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, readbackPipelineLayout, nullptr); + if (readbackDescriptorLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, readbackDescriptorLayout, nullptr); + for (const auto pool : cmaskDescriptorSets.pools) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); if (cmaskPipeline) context.Function("vkDestroyPipeline")(context.device, cmaskPipeline, nullptr); if (cmaskPipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, cmaskPipelineLayout, nullptr); if (cmaskDescriptorLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, cmaskDescriptorLayout, nullptr); cmaskErrors.reset(); - for (const auto pool : descriptorPools) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); - for (const auto pool : imageDescriptorPools) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); + for (const auto pool : descriptorSets.pools) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); + for (const auto pool : imageDescriptorSets.pools) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); + for (const auto pool : depthSampleSets.pools) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); + for (const auto pipeline : depthSamplePipelines) if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + if (depthSamplePipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, depthSamplePipelineLayout, nullptr); + if (depthSampleLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, depthSampleLayout, nullptr); + if (depthSampler) context.Function("vkDestroySampler")(context.device, depthSampler, nullptr); if (imageModule) context.Function("vkDestroyShaderModule")(context.device, imageModule, nullptr); + if (repackPipeline) context.Function("vkDestroyPipeline")(context.device, repackPipeline, nullptr); + if (repackModule) context.Function("vkDestroyShaderModule")(context.device, repackModule, nullptr); if (imagePipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, imagePipelineLayout, nullptr); if (imageDescriptorLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, imageDescriptorLayout, nullptr); for (const auto& entry : pipelines) context.Function("vkDestroyPipeline")(context.device, entry.second, nullptr); @@ -144,8 +157,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; @@ -160,10 +173,7 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem std::copy(equation->bits.begin(), equation->bits.end(), values.begin() + 4); } if (image && imageModule == VK_NULL_HANDLE) { - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(TEXTURE_DETILE_IMAGE_SPV); - moduleInfo.pCode = TEXTURE_DETILE_IMAGE_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &imageModule), "vkCreateShaderModule"); + imageModule = CreateShaderModule(context, TEXTURE_DETILE_IMAGE_SPV, sizeof(TEXTURE_DETILE_IMAGE_SPV), "vkCreateShaderModule"); } std::array entries{}; for (std::uint32_t index = 0; index < entries.size(); ++index) entries[index] = {index, index * 4u, 4}; @@ -261,6 +271,51 @@ void TextureDetiler::Dispatch(VkCommandBuffer commands, TextureTileMode tileMode context.Function("vkCmdDispatch")(commands, groupsX, groupsY, 1); } +void TextureDetiler::DispatchRepack(VkCommandBuffer commands, VkBuffer source, std::uint64_t sourceOffset, VkBuffer destination, std::uint64_t destinationOffset, std::uint64_t bytes) { + Require(commands != VK_NULL_HANDLE && source != VK_NULL_HANDLE && destination != VK_NULL_HANDLE, "a texel repack needs a command buffer and its buffers"); + Require(bytes != 0 && bytes % 4u == 0 && sourceOffset % 4u == 0 && destinationOffset % 4u == 0, "a texel repack needs whole aligned words"); + if (repackPipeline == VK_NULL_HANDLE) { + repackModule = CreateShaderModule(context, TEXTURE_REPACK_SPV, sizeof(TEXTURE_REPACK_SPV), "vkCreateShaderModule repack"); + VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + stage.module = repackModule; + stage.pName = "main"; + VkComputePipelineCreateInfo createInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + createInfo.stage = stage; + createInfo.layout = pipelineLayout; + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &createInfo, nullptr, &repackPipeline), "vkCreateComputePipelines repack"); + } + const auto alignment = std::max(context.limits.minStorageBufferOffsetAlignment, 4); + const auto sourceDescriptorOffset = sourceOffset - sourceOffset % alignment; + const auto destinationDescriptorOffset = destinationOffset - destinationOffset % alignment; + const auto sourceRange = sourceOffset - sourceDescriptorOffset + bytes; + const auto destinationRange = destinationOffset - destinationDescriptorOffset + bytes; + Require(sourceRange <= context.limits.maxStorageBufferRange && destinationRange <= context.limits.maxStorageBufferRange, "a texel repack exceeds the device's storage buffer range"); + const auto words = bytes / 4u; + constexpr std::uint64_t rowWords = 64u * 1024u; + const auto rows = (words + rowWords - 1u) / rowWords; + Require(words <= UINT32_MAX && rows <= context.limits.maxComputeWorkGroupCount[1], "a texel repack exceeds the device's dispatch limits"); + const auto set = allocateSet(); + const VkDescriptorBufferInfo sourceInfo{source, sourceDescriptorOffset, sourceRange}; + const VkDescriptorBufferInfo destinationInfo{destination, destinationDescriptorOffset, destinationRange}; + std::array writes{}; + for (std::uint32_t index = 0; index < writes.size(); ++index) { + writes[index].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[index].dstSet = set; + writes[index].dstBinding = index; + writes[index].descriptorCount = 1; + writes[index].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + writes[index].pBufferInfo = index == 0 ? &sourceInfo : &destinationInfo; + } + context.Function("vkUpdateDescriptorSets")(context.device, static_cast(writes.size()), writes.data(), 0, nullptr); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, repackPipeline); + context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipelineLayout, 0, 1, &set, 0, nullptr); + const std::array push{static_cast(sourceOffset - sourceDescriptorOffset), static_cast(destinationOffset - destinationDescriptorOffset), static_cast(words), static_cast(std::min(words, rowWords))}; + context.Function("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), push.data()); + context.Function("vkCmdDispatch")(commands, static_cast((std::min(words, rowWords) + 63u) / 64u), static_cast(rows), 1); +} + std::uint32_t TextureDetiler::CmaskErrors() { if (cmaskErrors == nullptr) return 0; std::uint32_t codes = 0; @@ -291,11 +346,7 @@ void TextureDetiler::DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask pipelineLayoutInfo.pushConstantRangeCount = 1; pipelineLayoutInfo.pPushConstantRanges = &pushRange; Check(context.Function("vkCreatePipelineLayout")(context.device, &pipelineLayoutInfo, nullptr, &cmaskPipelineLayout), "vkCreatePipelineLayout CMASK clear"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(CMASK_CLEAR_SPV); - moduleInfo.pCode = CMASK_CLEAR_SPV; - VkShaderModule cmaskModule = VK_NULL_HANDLE; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &cmaskModule), "vkCreateShaderModule CMASK clear"); + VkShaderModule cmaskModule = CreateShaderModule(context, CMASK_CLEAR_SPV, sizeof(CMASK_CLEAR_SPV), "vkCreateShaderModule CMASK clear"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; @@ -312,26 +363,7 @@ void TextureDetiler::DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask cmaskErrors = std::make_unique(context, 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); std::memset(cmaskErrors->Bytes().data(), 0, 4); } - constexpr std::uint32_t setsPerPool = 64; - const auto poolIndex = allocatedCmaskSets / setsPerPool; - if (poolIndex == cmaskDescriptorPools.size()) { - const std::array sizes{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, setsPerPool * 2}, {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, setsPerPool}}}; - VkDescriptorPoolCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; - info.maxSets = setsPerPool; - info.poolSizeCount = static_cast(sizes.size()); - info.pPoolSizes = sizes.data(); - cmaskDescriptorPools.reserve(cmaskDescriptorPools.size() + 1); - VkDescriptorPool pool = VK_NULL_HANDLE; - Check(context.Function("vkCreateDescriptorPool")(context.device, &info, nullptr, &pool), "vkCreateDescriptorPool CMASK clear"); - cmaskDescriptorPools.push_back(pool); - } - VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; - allocation.descriptorPool = cmaskDescriptorPools.at(poolIndex); - allocation.descriptorSetCount = 1; - allocation.pSetLayouts = &cmaskDescriptorLayout; - VkDescriptorSet set = VK_NULL_HANDLE; - Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &set), "vkAllocateDescriptorSets CMASK clear"); - ++allocatedCmaskSets; + const auto set = allocateFrom(cmaskDescriptorSets, cmaskDescriptorLayout, {{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 64 * 2}, {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 64}}, "vkCreateDescriptorPool CMASK clear", "vkAllocateDescriptorSets CMASK clear", "vkResetDescriptorPool CMASK clear"); const auto alignment = std::max(context.limits.minStorageBufferOffsetAlignment, 4); const auto descriptorOffset = cmaskOffset - cmaskOffset % alignment; const auto base = cmaskOffset - descriptorOffset; @@ -358,18 +390,91 @@ void TextureDetiler::DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask }; constexpr VkAccessFlags anyWrite = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT; constexpr VkAccessFlags anyAccess = anyWrite | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_HOST_READ_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT; - barrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, anyWrite, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + barrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, anyWrite, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, cmaskPipeline); context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, cmaskPipelineLayout, 0, 1, &set, 0, nullptr); const std::uint32_t mask = elementBytes >= 4 ? 0xffffffffu : (1u << (elementBytes * 8u)) - 1u; const std::array push{static_cast(base), width, height, (width + 1023u) / 1024u, clearWords[0] & mask, elementBytes == 8 ? clearWords[1] : 0u, write ? 1u : 0u}; context.Function("vkCmdPushConstants")(commands, cmaskPipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), push.data()); context.Function("vkCmdDispatch")(commands, (width + 7u) / 8u, (height + 7u) / 8u, 1); - barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT, anyAccess); + barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT, anyAccess); context.Function("vkCmdFillBuffer")(commands, cmask, cmaskOffset, cmaskBytes, 0xffffffffu); barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, anyAccess); } +void TextureDetiler::DispatchDepthSample(VkCommandBuffer commands, VkImageView depth, bool multisampled, VkImageView target, std::uint32_t width, std::uint32_t height, std::uint32_t sample) { + Require(commands != VK_NULL_HANDLE && depth != VK_NULL_HANDLE && target != VK_NULL_HANDLE, "a depth sample copy needs a command buffer, the depth view and the target view"); + if (depthSamplePipelineLayout == VK_NULL_HANDLE) { + VkSamplerCreateInfo samplerInfo{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; + samplerInfo.magFilter = VK_FILTER_NEAREST; + samplerInfo.minFilter = VK_FILTER_NEAREST; + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + Check(context.Function("vkCreateSampler")(context.device, &samplerInfo, nullptr, &depthSampler), "vkCreateSampler depth sample copy"); + std::array bindings{}; + bindings[0] = {0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, &depthSampler}; + bindings[1] = {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; + VkDescriptorSetLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + layoutInfo.bindingCount = static_cast(bindings.size()); + layoutInfo.pBindings = bindings.data(); + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &layoutInfo, nullptr, &depthSampleLayout), "vkCreateDescriptorSetLayout depth sample copy"); + const VkPushConstantRange pushRange{VK_SHADER_STAGE_COMPUTE_BIT, 0, 3 * sizeof(std::uint32_t)}; + VkPipelineLayoutCreateInfo pipelineLayoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + pipelineLayoutInfo.setLayoutCount = 1; + pipelineLayoutInfo.pSetLayouts = &depthSampleLayout; + pipelineLayoutInfo.pushConstantRangeCount = 1; + pipelineLayoutInfo.pPushConstantRanges = &pushRange; + Check(context.Function("vkCreatePipelineLayout")(context.device, &pipelineLayoutInfo, nullptr, &depthSamplePipelineLayout), "vkCreatePipelineLayout depth sample copy"); + } + auto& depthSamplePipeline = depthSamplePipelines[multisampled ? 1u : 0u]; + if (depthSamplePipeline == VK_NULL_HANDLE) { + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = multisampled ? sizeof(DEPTH_SAMPLE_MULTISAMPLE_SPV) : sizeof(DEPTH_SAMPLE_SINGLE_SPV); + moduleInfo.pCode = multisampled ? DEPTH_SAMPLE_MULTISAMPLE_SPV : DEPTH_SAMPLE_SINGLE_SPV; + VkShaderModule depthModule = VK_NULL_HANDLE; + Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &depthModule), "vkCreateShaderModule depth sample copy"); + VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + pipelineInfo.stage.module = depthModule; + pipelineInfo.stage.pName = "main"; + pipelineInfo.layout = depthSamplePipelineLayout; + const auto created = context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &depthSamplePipeline); + context.Function("vkDestroyShaderModule")(context.device, depthModule, nullptr); + Check(created, "vkCreateComputePipelines depth sample copy"); + } + const auto set = allocateFrom(depthSampleSets, depthSampleLayout, {{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 64}, {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 64}}, "vkCreateDescriptorPool depth sample copy", "vkAllocateDescriptorSets depth sample copy", "vkResetDescriptorPool depth sample copy"); + const VkDescriptorImageInfo depthInfo{VK_NULL_HANDLE, depth, VK_IMAGE_LAYOUT_GENERAL}; + const VkDescriptorImageInfo targetInfo{VK_NULL_HANDLE, target, VK_IMAGE_LAYOUT_GENERAL}; + std::array writes{}; + for (std::uint32_t index = 0; index < writes.size(); ++index) { + writes[index].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[index].dstSet = set; + writes[index].dstBinding = index; + writes[index].descriptorCount = 1; + writes[index].descriptorType = index == 0 ? VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER : VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + } + writes[0].pImageInfo = &depthInfo; + writes[1].pImageInfo = &targetInfo; + context.Function("vkUpdateDescriptorSets")(context.device, static_cast(writes.size()), writes.data(), 0, nullptr); + const auto barrier = [&](VkPipelineStageFlags source, VkPipelineStageFlags destination, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess) { + VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + memory.srcAccessMask = sourceAccess; + memory.dstAccessMask = destinationAccess; + context.Function("vkCmdPipelineBarrier")(commands, source, destination, 0, 1, &memory, 0, nullptr, 0, nullptr); + }; + constexpr VkAccessFlags anyWrite = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT; + constexpr VkAccessFlags anyAccess = anyWrite | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_HOST_READ_BIT; + barrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, anyWrite, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, depthSamplePipeline); + context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, depthSamplePipelineLayout, 0, 1, &set, 0, nullptr); + const std::array push{width, height, sample}; + context.Function("vkCmdPushConstants")(commands, depthSamplePipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), push.data()); + context.Function("vkCmdDispatch")(commands, (width + 7u) / 8u, (height + 7u) / 8u, 1); + barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, anyAccess); +} + void TextureDetiler::DispatchImage(VkCommandBuffer commands, TextureTileMode tileMode, std::uint32_t elementBytes, VkBuffer tiled, std::uint64_t tiledOffset, VkImageView view, const TileMipLayout& layout, bool retile, std::uint32_t slice, const DetileWindow& window) { Require(commands != VK_NULL_HANDLE, "texture detiling requires an active command buffer"); Require(window.rangeBegin < window.rangeEnd && window.tiledBase <= window.rangeBegin, "texture detiling window is empty"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetilerDescriptors.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetilerDescriptors.cpp index 4d482528cf..5f78f5729c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetilerDescriptors.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetilerDescriptors.cpp @@ -1,55 +1,72 @@ #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include #include +#include namespace AgcDriver::Graphics { void TextureDetiler::BeginBatch() { GuestMemory::AssertGpuLockHeld("TextureDetiler::BeginBatch"); - // Sets of recorded work that has not completed stay allocated; the pools are recycled once the - // recorder is idle again. if (const auto* recorder = Recorder::Active(); recorder != nullptr && !recorder->Idle()) return; - for (const auto pool : descriptorPools) { - Check(context.Function("vkResetDescriptorPool")(context.device, pool, 0), "vkResetDescriptorPool texture detiler"); - } - allocatedSets = 0; - for (const auto pool : cmaskDescriptorPools) { - Check(context.Function("vkResetDescriptorPool")(context.device, pool, 0), "vkResetDescriptorPool CMASK clear"); - } - allocatedCmaskSets = 0; - for (const auto pool : imageDescriptorPools) { - Check(context.Function("vkResetDescriptorPool")(context.device, pool, 0), "vkResetDescriptorPool texture detiler"); - } - allocatedImageSets = 0; + resetRing(descriptorSets, "vkResetDescriptorPool texture detiler"); + resetRing(cmaskDescriptorSets, "vkResetDescriptorPool CMASK clear"); + resetRing(imageDescriptorSets, "vkResetDescriptorPool texture detiler"); + resetRing(readbackDescriptorSets, "vkResetDescriptorPool multisample readback"); } -VkDescriptorSet TextureDetiler::allocateSet(bool image) { +void TextureDetiler::resetRing(DescriptorRing& ring, const char* resetWhat) { + for (const auto pool : ring.pools) Check(context.Function("vkResetDescriptorPool")(context.device, pool, 0), resetWhat); + std::fill(ring.serials.begin(), ring.serials.end(), std::uint64_t{0}); + ring.current = 0; + ring.used = 0; +} + +VkDescriptorSet TextureDetiler::allocateFrom(DescriptorRing& ring, VkDescriptorSetLayout layout, const std::vector& sizes, const char* createWhat, const char* allocateWhat, const char* resetWhat) { constexpr std::uint32_t setsPerPool = 64; - auto& pools = image ? imageDescriptorPools : descriptorPools; - auto& allocated = image ? allocatedImageSets : allocatedSets; - const auto poolIndex = allocated / setsPerPool; - if (poolIndex == pools.size()) { - std::array sizes{}; - sizes[0] = {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, image ? setsPerPool : setsPerPool * 2}; - sizes[1] = {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, setsPerPool}; - VkDescriptorPoolCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; - info.maxSets = setsPerPool; - info.poolSizeCount = image ? 2u : 1u; - info.pPoolSizes = sizes.data(); - pools.reserve(pools.size() + 1); - VkDescriptorPool pool = VK_NULL_HANDLE; - Check(context.Function("vkCreateDescriptorPool")(context.device, &info, nullptr, &pool), "vkCreateDescriptorPool texture detiler"); - pools.push_back(pool); + const auto* recorder = Recorder::Active(); + if (ring.pools.empty() || ring.used == setsPerPool) { + const auto completed = recorder != nullptr ? recorder->CompletedThrough() : 0; + auto next = ring.pools.size(); + for (std::size_t step = 1; step <= ring.pools.size(); ++step) { + const auto index = (ring.current + step) % ring.pools.size(); + if (ring.serials[index] <= completed) { + next = index; + break; + } + } + if (next < ring.pools.size()) { + Check(context.Function("vkResetDescriptorPool")(context.device, ring.pools[next], 0), resetWhat); + } else { + VkDescriptorPoolCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + info.maxSets = setsPerPool; + info.poolSizeCount = static_cast(sizes.size()); + info.pPoolSizes = sizes.data(); + ring.pools.reserve(ring.pools.size() + 1); + ring.serials.reserve(ring.serials.size() + 1); + VkDescriptorPool pool = VK_NULL_HANDLE; + Check(context.Function("vkCreateDescriptorPool")(context.device, &info, nullptr, &pool), createWhat); + ring.pools.push_back(pool); + ring.serials.push_back(0); + } + ring.current = next; + ring.used = 0; } VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; - allocation.descriptorPool = pools.at(poolIndex); + allocation.descriptorPool = ring.pools[ring.current]; allocation.descriptorSetCount = 1; - allocation.pSetLayouts = image ? &imageDescriptorLayout : &descriptorLayout; + allocation.pSetLayouts = &layout; VkDescriptorSet set = VK_NULL_HANDLE; - Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &set), "vkAllocateDescriptorSets texture detiler"); - ++allocated; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &set), allocateWhat); + ++ring.used; + ring.serials[ring.current] = recorder != nullptr ? recorder->OpenSerial() : std::numeric_limits::max(); return set; } +VkDescriptorSet TextureDetiler::allocateSet(bool image) { + if (image) return allocateFrom(imageDescriptorSets, imageDescriptorLayout, {{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 64}, {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 64}}, "vkCreateDescriptorPool texture detiler", "vkAllocateDescriptorSets texture detiler", "vkResetDescriptorPool texture detiler"); + return allocateFrom(descriptorSets, descriptorLayout, {{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 64 * 2}}, "vkCreateDescriptorPool texture detiler", "vkAllocateDescriptorSets texture detiler", "vkResetDescriptorPool texture detiler"); +} + } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index ca7f4da8b3..e667909a5b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -117,7 +117,7 @@ VkFormat ResolveTextureFormat(std::uint32_t guestFormat) { std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical) { const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); - const bool always = forced != nullptr && std::strcmp(forced, "1") == 0; + const bool always = forced != nullptr && (std::strcmp(forced, "1") == 0 || std::strcmp(forced, "copy") == 0); const auto sampled = [&](ShaderRecompiler::IrBufferFormat format) { VkFormatProperties properties{}; formatProperties(physical, ResolveTextureFormat(static_cast(format)), &properties); @@ -131,11 +131,32 @@ std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatPr } VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat) { + if (IsRepackedTextureFormat(guestFormat)) return VK_FORMAT_B10G11R11_UFLOAT_PACK32; const auto format = static_cast(guestFormat); if ((context.srgbDecodeFormats & ShaderRecompiler::SrgbDecodeBit(format)) == 0u) return ResolveTextureFormat(guestFormat); return ResolveTextureFormat(static_cast(ShaderRecompiler::SrgbUnormFormat(format))); } +std::uint32_t SrgbCopyFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical, std::uint32_t decodeFormats) { + const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); + if (forced != nullptr && std::strcmp(forced, "1") == 0) return 0u; + VkFormatProperties target{}; + formatProperties(physical, VK_FORMAT_R8G8B8A8_UNORM, &target); + if ((target.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT) == 0) return 0u; + std::uint32_t formats = 0; + for (const auto format : {ShaderRecompiler::IrBufferFormat::Format8Srgb, ShaderRecompiler::IrBufferFormat::Format8_8Srgb}) { + if ((decodeFormats & ShaderRecompiler::SrgbDecodeBit(format)) == 0u) continue; + VkFormatProperties source{}; + formatProperties(physical, ResolveTextureFormat(static_cast(ShaderRecompiler::SrgbUnormFormat(format))), &source); + if ((source.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) != 0) formats |= ShaderRecompiler::SrgbDecodeBit(format); + } + return formats; +} + +bool SrgbExpandsToRgba8(const Context& context, std::uint32_t guestFormat) { + return (context.srgbCopyFormats & ShaderRecompiler::SrgbDecodeBit(static_cast(guestFormat))) != 0u; +} + bool IsSrgbTextureFormat(std::uint32_t guestFormat) { switch (findFormatEntry(guestFormat).vkFormat) { case VK_FORMAT_R8_SRGB: @@ -149,6 +170,14 @@ bool IsSrgbTextureFormat(std::uint32_t guestFormat) { } } +bool IgnoresForcedDegamma(std::uint32_t guestFormat) { + switch (guestFormat) { + case 7u: case 8u: case 13u: case 22u: case 23u: case 24u: case 29u: case 36u: case 50u: case 64u: case 65u: case 66u: case 71u: case 77u: + case 132u: case 133u: case 134u: case 136u: case 175u: case 176u: case 177u: case 178u: case 179u: case 180u: return true; + default: return false; + } +} + std::optional FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes) { for (const auto& entry : kFormatLookup) { if (!entry.blockCompressed && entry.vkFormat == format && entry.bytesPerElement == elementBytes) return entry.guestFormat; @@ -166,6 +195,7 @@ VkComponentSwizzle TextureComponentChannel(std::uint32_t guestFormat, VkComponen case 133: return bgrb[index]; case 134: return bgra[index]; case 136: return argb[index]; + case 43: return bgra[index]; default: return component; } } @@ -178,6 +208,14 @@ bool IsConvertedTextureFormat(std::uint32_t guestFormat) { return remapGuestFormat(guestFormat) != guestFormat; } +bool IsRepackedTextureFormat(std::uint32_t guestFormat) { + return guestFormat == static_cast(ShaderRecompiler::IrBufferFormat::Format10_11_11Float); +} + +std::uint32_t RepackTextureWord(std::uint32_t word) { + return ((word >> 21u) & 0x7ffu) | (((word >> 10u) & 0x7ffu) << 11u) | ((word & 0x3ffu) << 22u); +} + std::optional FindGuestColorTargetFormat(VkFormat format, std::uint32_t elementBytes) { switch (format) { case VK_FORMAT_B8G8R8A8_UNORM: format = VK_FORMAT_R8G8B8A8_UNORM; break; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index 6eca2ae659..ff1bed0787 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" #include #include #include @@ -262,11 +263,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 +318,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]; @@ -382,6 +384,13 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) { SurfaceGeometry geometry; + if (descriptor.samples != 1u && descriptor.tileMode != TextureTileMode::kLinear) { + Require(descriptor.samples == 2u || descriptor.samples == 4u || descriptor.samples == 8u, "multisampled surface fragment count is not implemented"); + Require(descriptor.dimension == TextureDimension::k2DMsaa && descriptor.baseArray == 0u && descriptor.depthOrLastArray == 0u, "multisampled surface dimension or array range is not implemented"); + Require(descriptor.mipCount == 1u && descriptor.allocatedMipCount == 1u && descriptor.baseLevel == 0u && descriptor.lastLevel == 0u, "multisampled surface mip levels are not implemented"); + Require(descriptor.width != 0u && descriptor.width <= 16384u && descriptor.height != 0u && descriptor.height <= 16384u, "invalid multisampled surface extent"); + Require(!IsBlockCompressed(descriptor.format), "multisampled block-compressed surface is not implemented"); + } if (descriptor.dimension == TextureDimension::k3D && (descriptor.tileMode == TextureTileMode::kZ64KBX || descriptor.tileMode == TextureTileMode::kR64KBX)) { const auto depth = descriptor.depthOrLastArray + 1u; geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, descriptor.mipCount); @@ -405,6 +414,29 @@ SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) { geometry.layerBytes = thick.slabBytes; return geometry; } + if (descriptor.dimension == TextureDimension::k2DMsaa && descriptor.tileMode != TextureTileMode::kLinear) { + Require(descriptor.samples > 1u && std::has_single_bit(descriptor.samples), "multisampled texture has an invalid sample count"); + const auto bytesPerElement = BytesPerElement(descriptor.format); + Require(std::has_single_bit(bytesPerElement), "multisampled texture format has an unsupported element size"); + const auto block = GetBlockLayout(descriptor.tileMode, bytesPerElement); + Require(block.blockSize == 4096u || block.blockSize == 65536u, "multisampled surface requires 4 KiB or 64 KiB blocks"); + const auto log2Samples = static_cast(std::countr_zero(descriptor.samples)); + const auto log2Pixels = static_cast(std::countr_zero(block.blockSize)) - static_cast(std::countr_zero(bytesPerElement)) - log2Samples; + const auto log2Width = (log2Pixels + ((log2Samples & 1u) == 0u ? 1u : 0u)) / 2u; + const auto blockWidth = 1u << log2Width; + const auto blockHeight = 1u << (log2Pixels - log2Width); + const auto align = [](std::uint32_t extent, std::uint32_t blockDimension) { + return (static_cast(extent) + blockDimension - 1u) / blockDimension * blockDimension; + }; + geometry.guestBytes = align(descriptor.width, blockWidth) * align(descriptor.height, blockHeight) * bytesPerElement * descriptor.samples; + geometry.layerBytes = geometry.guestBytes; + geometry.layers = 1; + geometry.imageLayers = 1; + geometry.imageDepth = 1; + geometry.sliceLinearBytes = static_cast(descriptor.width) * descriptor.height * descriptor.samples * bytesPerElement; + geometry.mips.push_back({0u, geometry.guestBytes, 0u, geometry.sliceLinearBytes, descriptor.width, descriptor.height, static_cast(align(descriptor.width, blockWidth) / blockWidth), descriptor.width * descriptor.samples * bytesPerElement, false, 0u, 0u}); + return geometry; + } geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, descriptor.mipCount); const bool layered = descriptor.dimension == TextureDimension::k2DArray || descriptor.dimension == TextureDimension::k1DArray || descriptor.dimension == TextureDimension::kCube; geometry.layers = layered ? descriptor.depthOrLastArray + 1u : 1u; @@ -434,6 +466,45 @@ bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t leve return true; } +GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed) { + const bool layered = viewed.dimension == TextureDimension::k2DArray || viewed.dimension == TextureDimension::k1DArray || viewed.dimension == TextureDimension::kCube; + if (!layered || viewed.baseArray == 0 || viewed.baseArray > viewed.depthOrLastArray || viewed.dccAddress != 0) return viewed; + if (const auto xorMode = XorSwizzleMode(viewed.tileMode); xorMode != 0u) { + const auto* equation = FindTextureSwizzleEquation(xorMode, BytesPerElement(viewed.format)); + if (equation == nullptr) return viewed; + std::uint32_t sliceBits = 0; + for (const auto bit : equation->bits) sliceBits |= bit >> 24u; + const auto sliceMask = sliceBits == 0 ? 0u : (1u << std::bit_width(sliceBits)) - 1u; + if ((viewed.baseArray & sliceMask) != 0) return viewed; + } + const auto geometry = DescribeSurface(viewed); + if (geometry.thick || geometry.layers != viewed.depthOrLastArray + 1u) return viewed; + auto window = viewed; + window.baseAddress = viewed.baseAddress + geometry.GuestLayerOffset(viewed.baseArray); + window.depthOrLastArray = viewed.depthOrLastArray - viewed.baseArray; + window.baseArray = 0; + return window; +} + +GuestTextureResource ArrayLayerWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers) { + const bool layered = viewed.dimension == TextureDimension::k2DArray || viewed.dimension == TextureDimension::k1DArray || viewed.dimension == TextureDimension::kCube; + if (viewed.depthOrLastArray < maxArrayLayers || !layered) return viewed; + Require(viewed.dimension != TextureDimension::kCube, "an image with more layers than maxImageArrayLayers that is not a 1D or 2D array is not implemented"); + Require(viewed.baseArray <= viewed.depthOrLastArray, "an image view's first array slice lies past its last"); + Require(viewed.depthOrLastArray - viewed.baseArray < maxArrayLayers, "an image view of more array slices than maxImageArrayLayers is not implemented"); + if (const auto xorMode = XorSwizzleMode(viewed.tileMode); xorMode != 0u) { + const auto* equation = FindTextureSwizzleEquation(xorMode, BytesPerElement(viewed.format)); + Require(equation != nullptr && std::none_of(equation->bits.begin(), equation->bits.end(), [](std::uint32_t bit) { return (bit & 0xff000000u) != 0; }), "an image view past maxImageArrayLayers of a swizzle that depends on the slice is not implemented"); + } + const auto geometry = DescribeSurface(viewed); + Require(!geometry.thick && geometry.layers == viewed.depthOrLastArray + 1u, "an array past maxImageArrayLayers must be a thin array"); + auto window = viewed; + window.baseAddress = viewed.baseAddress + geometry.GuestLayerOffset(viewed.baseArray); + window.depthOrLastArray = viewed.depthOrLastArray - viewed.baseArray; + window.baseArray = 0; + return window; +} + std::vector ComputeElementMipLayout(TextureTileMode tileMode, std::uint32_t bytesPerElement, std::uint32_t width, std::uint32_t height, std::uint32_t mipCount) { Require(width != 0 && height != 0 && mipCount != 0 && mipCount <= 16u, "invalid surface mip chain"); if (tileMode == TextureTileMode::kLinear) return ComputeLinearMipLayout(bytesPerElement, 1u, 1u, width, height, mipCount); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp index a6a8c6af1d..cd2998c06b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" @@ -66,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. @@ -97,7 +99,7 @@ struct UnitShadow { }; struct Shadows { - std::mutex mutex; + SlimMutex mutex; std::map> byBase; std::uint64_t liveBytes = 0; std::uint64_t peakBytes = 0; @@ -161,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); @@ -278,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); @@ -335,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; @@ -605,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; @@ -667,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/Resource/src/Registration.cpp b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp index fe08199be9..f3fbd0ba67 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp +++ b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp @@ -150,4 +150,16 @@ int APS5_VABI sceAgcDriverFindResourcesPublic(const void* callback, void* user_d return SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE; } +int APS5_VABI sceAgcDriverRegisterOwnerForSystem() { + return SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverInitResourceRegistrationInterface() { + return SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverSetResourceRegistrationInfo() { + return 0; +} + } diff --git a/core/libs/prx/libSceAgcDriver/State/src/Status.cpp b/core/libs/prx/libSceAgcDriver/State/src/Status.cpp index ae1a8543f7..825a9e9001 100644 --- a/core/libs/prx/libSceAgcDriver/State/src/Status.cpp +++ b/core/libs/prx/libSceAgcDriver/State/src/Status.cpp @@ -5,6 +5,12 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int CAPTURE_UNAVAILABLE = static_cast(0x8A6C1000); + +} + extern "C" { bool APS5_VABI sceAgcDriverIsCaptureInProgress(void) { @@ -21,18 +27,15 @@ bool APS5_VABI sceAgcDriverIsSubmitValidationEnabled(void) { int APS5_VABI sceAgcDriverRequestCaptureStart(const char* path) { (void)path; - NotImplemented_nid_no_patch(__func__); - return 0; + return CAPTURE_UNAVAILABLE; } int APS5_VABI sceAgcDriverRequestCaptureStop() { - NotImplemented_nid_no_patch(__func__); - return 0; + return CAPTURE_UNAVAILABLE; } int APS5_VABI sceAgcDriverTriggerCapture() { - NotImplemented_nid_no_patch(__func__); - return 0; + return CAPTURE_UNAVAILABLE; } } diff --git a/core/libs/prx/libSceAgcDriver/Submit/include/CommandBuffer.hpp b/core/libs/prx/libSceAgcDriver/Submit/include/CommandBuffer.hpp index ef4b9e6894..52fb4cb31d 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/include/CommandBuffer.hpp +++ b/core/libs/prx/libSceAgcDriver/Submit/include/CommandBuffer.hpp @@ -1,4 +1,16 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_SUBMIT_INCLUDE_COMMANDBUFFER_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_SUBMIT_INCLUDE_COMMANDBUFFER_HPP +#include "SceTypes.hpp" +#include + +struct Packet; + +extern "C" { + +int APS5_VABI sceAgcDriverSubmitCommandBuffer(void* queue_context, const Packet* packet); +int APS5_VABI sceAgcDriverSubmitMultiCommandBuffers(void* queue_context, uint32_t* const* command_buffers, const uint32_t* sizes_in_dwords, uint32_t count); + +} + #endif diff --git a/core/libs/prx/libSceAgcDriver/Submit/include/Dcb.hpp b/core/libs/prx/libSceAgcDriver/Submit/include/Dcb.hpp index 3b1cf467aa..9e8fe9fdf7 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/include/Dcb.hpp +++ b/core/libs/prx/libSceAgcDriver/Submit/include/Dcb.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_SUBMIT_INCLUDE_DCB_HPP #include "SceTypes.hpp" +#include namespace AgcDriver { @@ -9,6 +10,9 @@ int SubmitMany(std::uint32_t* const* addresses, const std::uint32_t* sizes, std: } +extern "C" std::uint32_t APS5_VABI sceAgcDriverGetWaitRenderingPacketSizeInDwords(); +extern "C" std::uint32_t APS5_VABI sceAgcDriverGetSetFlipPacketSizeInDwords(); + extern "C" int APS5_VABI sceAgcDriverSubmitDcb(const Packet* packet); extern "C" int APS5_VABI sceAgcDriverAgrSubmitDcb(const Packet* packet); extern "C" int APS5_VABI sceAgcDriverSubmitMultiDcbs(std::uint32_t* const* dcbGpuAddrs, const std::uint32_t* dcbSizesInDwords, std::uint32_t count); diff --git a/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp b/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp index 646827ca11..7b326f7f9d 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp +++ b/core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp @@ -1,26 +1,35 @@ #include "prx/libSceAgcDriver/Submit/include/CommandBuffer.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { int APS5_VABI sceAgcDriverSubmitCommandBuffer(void* queue_context, const Packet* packet) { - (void)queue_context; - (void)packet; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!queue_context || !packet) return EINVAL; + const auto queue = *reinterpret_cast(static_cast(queue_context) + 4); + if (queue != 0 && (queue < 0x20 || queue >= 0x58)) { + throw std::runtime_error(std::string(__func__) + ": unsupported queue"); + } + AgcDriver::Submit(packet, queue); + return 0; } int APS5_VABI sceAgcDriverSubmitMultiCommandBuffers(void* queue_context, uint32_t* const* command_buffers, const uint32_t* sizes_in_dwords, uint32_t count) { - (void)queue_context; - (void)command_buffers; - (void)sizes_in_dwords; - (void)count; - NotImplemented_nid_no_patch(__func__); - return 0; + if (count == 0) return 0; + if (!queue_context || !command_buffers || !sizes_in_dwords) return EINVAL; + const auto queue = *reinterpret_cast(static_cast(queue_context) + 4); + if (queue != 0 && (queue < 0x20 || queue >= 0x58)) { + throw std::runtime_error(std::string(__func__) + ": unsupported queue"); + } + return AgcDriver::SubmitMany(command_buffers, sizes_in_dwords, count, queue); } } diff --git a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp index 4e9bba3299..04702130a0 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp +++ b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp @@ -22,12 +22,50 @@ int AgcDriver::SubmitMany(std::uint32_t* const* addresses, const std::uint32_t* return 0; } +namespace { + +std::uint32_t PayloadDwords(std::uint32_t bytes) { + return static_cast((std::uint64_t{bytes} + 3) >> 2); +} + +std::uint32_t WorkloadDataPacketDwords(std::uint32_t bytes) { + if (bytes == 0) return 3; + const std::uint32_t payload = PayloadDwords(bytes); + return payload == 1 ? 4 : payload + 7; +} + +} + extern "C" { std::uint32_t APS5_VABI sceAgcDriverGetWaitRenderingPacketSizeInDwords() { return AgcDriver::RenderingWaitPacketWords; } +std::uint32_t APS5_VABI sceAgcDriverGetSetFlipPacketSizeInDwords() { + return AgcDriver::FlipPacketWords; +} + +std::uint32_t APS5_VABI sceAgcDriverGetSetWorkloadsActivePacketSize() { + return WorkloadDataPacketDwords(8) + 9; +} + +std::uint32_t APS5_VABI sceAgcDriverGetSetWorkloadCompletePacketSize() { + return WorkloadDataPacketDwords(0) + 9; +} + +std::uint32_t APS5_VABI sceAgcDriverGetSetWorkloadStreamInactivePacketSize() { + return WorkloadDataPacketDwords(8); +} + +std::uint32_t APS5_VABI sceAgcDriverNopDataGetPacketSize(std::uint32_t bytes) { + return PayloadDwords(bytes) + 2; +} + +std::uint32_t APS5_VABI sceAgcDriverUserDataGetPacketSize(std::uint32_t bytes) { + return WorkloadDataPacketDwords(bytes); +} + std::uint32_t APS5_VABI sceAgcDriverWaitUntilSafeForRendering(std::uint32_t** command, std::uint32_t capacity, std::uint32_t mode, std::uint32_t handle, int index) { if (!command || capacity < AgcDriver::RenderingWaitPacketWords || mode != 0 || index < 0) diff --git a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp index 1a2d0f5c58..dd330964bc 100644 --- a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp +++ b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp @@ -1,11 +1,16 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" namespace { constexpr int DEBUG_UNAVAILABLE = static_cast(0x8A6C1000); +constexpr int DRIVER_FEATURE_UNAVAILABLE = static_cast(0x8A6D0001); +constexpr int DRIVER_NULL_OUTPUT = static_cast(0x8A6D0003); +constexpr int GPU_RESET_UNAVAILABLE = static_cast(0x8A6C0045); +constexpr std::size_t TOOL_MEMORY_RANGE_SIZE = 12; } @@ -15,9 +20,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) { @@ -52,4 +57,165 @@ int APS5_VABI sceAgcDriverGetHsOffchipParam(uint16_t* first, uint16_t* second) { return 0; } +int APS5_VABI sceAgcDriverNopDataWritePacket(void* destination, uint32_t address, const void* data, uint32_t bytes) { + const auto alignedBytes = (static_cast(bytes) + 3) & ~std::uint64_t{3}; + const auto totalBytes = alignedBytes + 8; + std::memset(destination, 0, static_cast(totalBytes)); + const auto header = (static_cast(totalBytes) << 14) + 0x3ffe0000U | 0xc0001000U; + std::memcpy(destination, &header, sizeof(header)); + std::memcpy(static_cast(destination) + 4, &address, sizeof(address)); + if (data != nullptr && bytes != 0) std::memcpy(static_cast(destination) + 8, data, bytes); + return data == nullptr || bytes == 0 ? 2 : static_cast(totalBytes / 4); +} + + +int APS5_VABI sceAgcDriverSetValidationDisabled() { + return DEBUG_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverGetCaptureConfig() { + return DEBUG_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverSetCaptureConfig() { + return DEBUG_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverGetGpuPrintfWorkArea() { + return DRIVER_FEATURE_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverSetGpuPrintfEnabled() { + return DRIVER_FEATURE_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverDbgReadRegister() { + return 0; +} + +int APS5_VABI sceAgcDriverDbgWriteRegister() { + return DRIVER_FEATURE_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverGetGpuRefClks() { + return DRIVER_FEATURE_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverIsSuspendPointInFlight() { + return DRIVER_FEATURE_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverIsPaDebug() { + return 0; +} + +int APS5_VABI sceAgcDriverGetExceptionStatus() { + return 0; +} + +int APS5_VABI sceAgcDriverGetTraceInitiator() { + return 0; +} + +int APS5_VABI sceAgcDriverRegisterGpuResetCallbacks() { + return 0; +} + +int APS5_VABI sceAgcDriverUnregisterGpuResetCallbacks() { + return 0; +} + +int APS5_VABI sceAgdResetGpu() { + return GPU_RESET_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverSysSubmitFlipHandleProxy() { + return 0; +} + +int APS5_VABI sceAgcDriverUserDataImmediateWrite() { + return 0; +} + +int APS5_VABI sceAgcDriverUserDataImmediateWritePacket() { + return 0; +} + +int APS5_VABI sceAgcDriverGetCoredumpMode(uint32_t* mode) { + if (mode == nullptr) return DRIVER_NULL_OUTPUT; + *mode = 0; + return 0; +} + +int APS5_VABI sceAgcDriverAllocateToolMemoryForGpuReset(uint64_t, uint64_t, uint64_t, void* range) { + if (range != nullptr) std::memset(range, 0, TOOL_MEMORY_RANGE_SIZE); + return DRIVER_FEATURE_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverGetAllocatedToolMemoryForGpuReset(uint64_t, void* range) { + if (range != nullptr) std::memset(range, 0, TOOL_MEMORY_RANGE_SIZE); + return DRIVER_FEATURE_UNAVAILABLE; +} + +} +extern "C" { + +int APS5_VABI sceAgcDriverAcquireHDRScopesACQ() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI sceAgcDriverGetCoredumpAddress() { + return nullptr; +} + +int APS5_VABI sceAgcDriverHp3dLockEnd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverHp3dLockStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverIsEsvmProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverReleaseHDRScopesACQ() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetTargetRingForDiag() { + return DRIVER_FEATURE_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverSetValueToSharedGds() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetWorkloadStreamInactive() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSubmitReprojectionAcb() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSuspendPointSubmitCdbg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSysGetGfxAppGpuResetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAgcDriver/tests/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..f334f4b036 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp @@ -0,0 +1,343 @@ +#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 +#ifdef __linux__ +#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 +#ifdef __linux__ + prctl(PR_SET_DUMPABLE, 0); +#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/BarrierBatch.cpp b/core/libs/prx/libSceAgcDriver/tests/BarrierBatch.cpp new file mode 100644 index 0000000000..440bf1bde3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/BarrierBatch.cpp @@ -0,0 +1,113 @@ +#include "prx/libSceAgcDriver/Graphics/include/BarrierBatch.hpp" +#include + +using namespace AgcDriver::Graphics; + +namespace { + +int failures = 0; + +void Expect(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", what); + ++failures; +} + +constexpr VkPipelineStageFlags All = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; +constexpr VkPipelineStageFlags Transfer = VK_PIPELINE_STAGE_TRANSFER_BIT; +constexpr VkPipelineStageFlags Compute = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; +constexpr VkPipelineStageFlags Host = VK_PIPELINE_STAGE_HOST_BIT; +constexpr VkAccessFlags SW = VK_ACCESS_SHADER_WRITE_BIT; +constexpr VkAccessFlags SR = VK_ACCESS_SHADER_READ_BIT; +constexpr VkAccessFlags TW = VK_ACCESS_TRANSFER_WRITE_BIT; +constexpr VkAccessFlags TR = VK_ACCESS_TRANSFER_READ_BIT; +constexpr VkAccessFlags HW = VK_ACCESS_HOST_WRITE_BIT; +constexpr VkAccessFlags HR = VK_ACCESS_HOST_READ_BIT; +constexpr VkAccessFlags CAW = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; +constexpr VkAccessFlags DSW = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; +constexpr VkAccessFlags Full = SR | SW | TR | TW; + +const BarrierMask PassEnd{All, All, SW | CAW | DSW | TW, Full}; +const BarrierMask LabelLeading{All, Transfer, SW | TW | CAW, TW}; +const BarrierMask StagingInLeading{All | Host, Transfer, SW | TW | HW, TR}; +const BarrierMask StagingOutLeading{All, Transfer, SW, TR}; +const BarrierMask WriteBackReady{All | Host, Compute, SR | SW | TR | TW | HW | CAW, SR | SW}; + +void testCoversStages() { + Expect(Covers(PassEnd, LabelLeading), "a pass end covers the label run's leading barrier"); + Expect(!Covers(LabelLeading, PassEnd), "the leading barrier does not cover the pass end"); + Expect(Covers(PassEnd, PassEnd), "a barrier covers itself"); + Expect(Covers(BarrierMask{}, BarrierMask{}), "the empty barrier covers the empty one"); + Expect(Covers(PassEnd, StagingOutLeading), "ALL_COMMANDS holds a transfer destination and a shader write"); + Expect(!Covers(PassEnd, StagingInLeading), "ALL_COMMANDS does not hold the host stage or a host write"); + Expect(Covers(StagingInLeading, StagingOutLeading), "a wider source covers the narrower one"); + const BarrierMask computeOnly{Compute, Compute, SW, SR}; + Expect(!Covers(computeOnly, PassEnd), "a compute-only barrier does not cover ALL_COMMANDS"); + Expect(!Covers(computeOnly, StagingOutLeading), "a compute destination does not hold a transfer destination"); +} + +void testCoversAccess() { + const BarrierMask noColor{All, All, SW | TW, Full}; + Expect(!Covers(noColor, LabelLeading), "a source without the colour attachment write does not cover a barrier that asks for it"); + const BarrierMask readOnlyDestination{All, All, SW | CAW | DSW | TW, SR | SW}; + Expect(!Covers(readOnlyDestination, LabelLeading), "a destination without the transfer write does not cover a barrier that asks for it"); + const BarrierMask same{All, Transfer, SW | TW | CAW, TW}; + Expect(Covers(same, LabelLeading) && Covers(LabelLeading, same), "equal barriers cover each other"); +} + +void testCoversImage() { + BarrierMask transition = LabelLeading; + transition.changesLayout = true; + Expect(!Covers(PassEnd, transition), "a barrier that changes a layout is never covered by a memory barrier"); + BarrierMask transfer = LabelLeading; + transfer.transfersQueue = true; + Expect(!Covers(PassEnd, transfer), "a queue family ownership transfer is never covered"); + BarrierMask sameLayout = LabelLeading; + Expect(Covers(PassEnd, sameLayout), "an image barrier that keeps the layout and the queue is covered like a memory barrier"); +} + +void testOrderedByTrailing() { + constexpr VkAccessFlags dispatched = Full | HR; + constexpr VkAccessFlags filled = Full | VK_ACCESS_INDIRECT_COMMAND_READ_BIT | HR; + Expect(OrderedByTrailing(dispatched, StagingInLeading), "a dispatch's trailing barrier orders the staging copy-in"); + Expect(OrderedByTrailing(dispatched, StagingOutLeading), "a dispatch's trailing barrier orders the staging copy-out"); + Expect(OrderedByTrailing(filled, WriteBackReady), "a fill's trailing barrier orders the write-back's import-ready barrier"); + Expect(OrderedByTrailing(Full, LabelLeading), "the pass end's destination accesses order the label run"); + Expect(!OrderedByTrailing(0, StagingInLeading), "an intervening command clears the covered accesses, so the barrier is kept"); + Expect(!OrderedByTrailing(0, StagingOutLeading) && !OrderedByTrailing(0, WriteBackReady), "no trailing barrier, no elision"); + Expect(!OrderedByTrailing(SR | SW, StagingInLeading), "a shader-only trailing barrier does not order a transfer read"); + Expect(!OrderedByTrailing(SR | SW | TW, StagingInLeading), "a trailing barrier without the transfer read is kept"); + constexpr VkAccessFlags stagedIn = SR | VK_ACCESS_UNIFORM_READ_BIT | SW; + Expect(!OrderedByTrailing(stagedIn, StagingInLeading), "the staging copy-in's own trailing barrier does not order the next copy-in"); + Expect(!OrderedByTrailing(Full, BarrierMask{All, Transfer, SW, TR | VK_ACCESS_INDIRECT_COMMAND_READ_BIT}), "a destination access the trailing barrier lacks is kept"); +} + +void testHostAndImage() { + BarrierMask toHost = StagingOutLeading; + toHost.dstStage |= Host; + toHost.dstAccess |= HR; + Expect(!OrderedByTrailing(Full | HR, toHost), "a barrier that makes writes visible to the host is kept"); + BarrierMask ready = WriteBackReady; + Expect(OrderedByTrailing(Full | HR, ready), "an image barrier that keeps its layout is elided like a memory barrier"); + ready.changesLayout = true; + Expect(!OrderedByTrailing(Full | HR, ready), "an image barrier that changes the layout is kept"); + ready.changesLayout = false; + ready.transfersQueue = true; + Expect(!OrderedByTrailing(Full | HR, ready), "a queue family transfer is kept"); +} + +} + +int main() { + testCoversStages(); + testCoversAccess(); + testCoversImage(); + testOrderedByTrailing(); + testHostAndImage(); + if (failures != 0) { + std::fprintf(stderr, "%d barrier batch checks failed\n", failures); + return 1; + } + std::printf("barrier batch tests passed\n"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index cf54e315dd..ad805c3f66 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -1,10 +1,121 @@ #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 +#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(); } @@ -15,10 +126,36 @@ void reject(TAction action, const char* expected) { throw std::runtime_error(std::string("missing contract rejection: ") + expected); } +std::uint32_t CallSites(const std::vector& spirv, const char* name) { + std::uint32_t function = 0; + std::uint32_t calls = 0; + for (std::size_t at = 5; at < spirv.size() && (spirv[at] >> 16u) != 0u; at += spirv[at] >> 16u) { + const auto opcode = spirv[at] & 0xffffu; + if (opcode == spv::OpName && std::strcmp(reinterpret_cast(&spirv[at + 2u]), name) == 0) function = spirv[at + 1u]; + if (opcode == spv::OpFunctionCall && function != 0u && spirv[at + 3u] == function) ++calls; + } + return calls; +} + } 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}; @@ -56,4 +193,14 @@ void RunBdaContractTests() { invalid[0] = invalid[0] == 'A' ? 'B' : 'A'; reject([&] { static_cast(serializer.Deserialize(invalid)); }, "request signature"); reject([&] { static_cast(serializer.Deserialize("NVNQQWMAAAA=")); }, "serialization version"); + const auto ordered = [](std::initializer_list ranges) { return BdaAbi::RangesOrdered(ranges.begin(), ranges.size()); }; + AgcDriver::Graphics::Require(ordered({}) && ordered({{0x1000, 0x2000, 1, 1, 0}, {0x2000, 0x2000, 1, 1, 0}, {0x2000, 0x3000, 1, 1, 0}, {0x4000, 0x5000, 1, 1, 0}}), "ascending BDA ranges were refused"); + AgcDriver::Graphics::Require(ordered({{0x1000, 0x3000, 1, 1, 0}, {0x2f00, 0x3100, 1, 1, 0}}), "BDA ranges ascending by begin and overlapping were refused"); + AgcDriver::Graphics::Require(!ordered({{0x4000, 0x5000, 1, 1, 0}, {0x1000, 0x2000, 1, 1, 0}}), "unsorted BDA ranges were accepted"); + for (const std::uint32_t bits : {8u, 16u, 32u}) { + const auto shader = MakeBdaWriteTestShader(0x7fff1235fffdULL, 0xa1b2c3d4u, bits, false); + const auto lookups = CallSites(shader, "get_bda_write_pointer"); + const auto loops = CallSites(shader, "write_bda_bytes"); + AgcDriver::Graphics::Require(loops == 1u && lookups == (bits == 32u ? 2u : 1u), "a runtime V# write of " + std::to_string(bits) + " bits has " + std::to_string(lookups) + " write lookups and " + std::to_string(loops) + " byte-loop calls, not its byte path through write_bda_bytes"); + } } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp index 860643bd27..deb5bb8e8d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp @@ -1,14 +1,23 @@ #include "BdaShader.hpp" #include "ColorTransferTests.hpp" +#include "HostImportFailure.hpp" #include #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include +#include #include +#include #include +#include #include #include +#include +#include +#include +#include +#include #include namespace AgcDriver::Graphics { @@ -21,9 +30,66 @@ namespace { using namespace AgcDriver::Graphics; +std::optional selectDevice(std::span devices, std::string_view request) { + const auto rankDeviceType = [](VkPhysicalDeviceType type) { + switch (type) { + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3; + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 2; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 1; + default: return 0; + } + }; + std::optional selected; + int selectedRank = -1; + for (std::size_t index = 0; index < devices.size(); ++index) { + const auto& candidate = devices[index]; + if (candidate.apiVersion < VK_API_VERSION_1_1) continue; + const std::string_view name(candidate.deviceName); + if (!request.empty() && std::search(name.begin(), name.end(), request.begin(), request.end(), [](char left, char right) { + return std::tolower(static_cast(left)) == std::tolower(static_cast(right)); + }) == name.end()) continue; + const int rank = request.empty() ? rankDeviceType(candidate.deviceType) : 4; + if (rank <= selectedRank) continue; + selected = index; + selectedRank = rank; + } + return selected; +} + +bool canSkipVulkanFailure(const char* requestedName, const char* requireVulkan) { + return (requestedName == nullptr || *requestedName == '\0') && requireVulkan == nullptr; +} + +void deviceSelectionTests() { + const auto properties = [](const char* name, VkPhysicalDeviceType type) { + VkPhysicalDeviceProperties result{}; + result.apiVersion = VK_API_VERSION_1_1; + result.deviceType = type; + std::memcpy(result.deviceName, name, std::strlen(name) + 1u); + return result; + }; + std::array devices{ + properties("AMD Radeon(TM) Graphics", VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU), + properties("NVIDIA GeForce RTX 5080", VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU), + }; + Require(!selectDevice({}, {}).has_value(), "an empty Vulkan device list selected a device"); + Require(selectDevice(devices, {}) == 1u, "default selection did not prefer the discrete GPU"); + Require(selectDevice(devices, "AMD") == 0u, "an AMD request lost to an unrelated discrete GPU"); + Require(selectDevice(devices, "aMd rAdEoN") == 0u, "GPU name matching was not case-insensitive"); + Require(selectDevice(devices, "RTX 5080") == 1u, "partial GPU name matching failed"); + Require(!selectDevice(devices, "missing device").has_value(), "an unmatched GPU request selected another device"); + Require(canSkipVulkanFailure(nullptr, nullptr), "an optional Vulkan test without a device request cannot skip"); + Require(canSkipVulkanFailure("", nullptr), "an empty GPU request prevents an optional Vulkan skip"); + Require(!canSkipVulkanFailure("missing device", nullptr), "an unmatched GPU request can be reported as a skip"); + Require(!canSkipVulkanFailure(nullptr, "1"), "a required Vulkan test can be reported as a skip"); + devices[0].apiVersion = VK_API_VERSION_1_0; + Require(!selectDevice(devices, "AMD").has_value(), "a GPU without Vulkan 1.1 matched the request"); + std::cout << "BDA device selection CPU tests passed\n"; +} + class Device { public: - Device() { + explicit Device(bool importHostMemory = false) { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); #else @@ -44,27 +110,29 @@ public: Require(count != 0, "no Vulkan device"); std::vector devices(count); Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); - const auto rankDeviceType = [](VkPhysicalDeviceType type) { - switch (type) { - case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3; - case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 2; - case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 1; - default: return 0; - } - }; + devices.resize(count); const auto physicalProperties = function("vkGetPhysicalDeviceProperties"); - int selectedRank = -1; - for (const auto physical : devices) { - VkPhysicalDeviceProperties candidate{}; - physicalProperties(physical, &candidate); - if (candidate.apiVersion < VK_API_VERSION_1_1) continue; - const int rank = rankDeviceType(candidate.deviceType); - if (rank <= selectedRank) continue; - context.physical = physical; - selectedRank = rank; - cpu = candidate.deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU; + std::vector deviceProperties(count); + for (std::size_t index = 0; index < devices.size(); ++index) { + physicalProperties(devices[index], &deviceProperties[index]); } - Require(context.physical != VK_NULL_HANDLE, "no Vulkan 1.1 device"); + const char* requestedName = std::getenv("ANYPS5_GPU"); + const std::string_view request = requestedName != nullptr ? requestedName : ""; + const auto selected = selectDevice(deviceProperties, request); + if (!selected.has_value()) { + if (!request.empty()) { + std::string candidateNames; + for (const auto& candidate : deviceProperties) { + if (!candidateNames.empty()) candidateNames += ", "; + candidateNames += candidate.deviceName; + } + throw std::runtime_error(std::string("no Vulkan 1.1 device whose name contains ANYPS5_GPU=\"") + std::string(request) + "\"; devices: " + candidateNames); + } + throw std::runtime_error("no Vulkan 1.1 device"); + } + context.physical = devices[*selected]; + cpu = deviceProperties[*selected].deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU; + std::cout << "Using \"" << deviceProperties[*selected].deviceName << "\"\n"; const auto extensions = function("vkEnumerateDeviceExtensionProperties"); Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); std::vector available(count); @@ -86,11 +154,13 @@ public: VkPhysicalDeviceFeatures enabled{}; enabled.shaderInt64 = VK_TRUE; address.pNext = &bytes; - const std::array extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; + std::vector extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; + const bool externalHost = importHostMemory && std::any_of(available.begin(), available.end(), [](const auto& item) { return std::strcmp(item.extensionName, VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME) == 0; }); + if (externalHost) extensionsEnabled.push_back(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME); VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &address}; device.queueCreateInfoCount = 1; device.pQueueCreateInfos = &queue; - device.enabledExtensionCount = 2; + device.enabledExtensionCount = static_cast(extensionsEnabled.size()); device.ppEnabledExtensionNames = extensionsEnabled.data(); device.pEnabledFeatures = &enabled; Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); @@ -99,9 +169,17 @@ public: VkPhysicalDeviceProperties properties{}; function("vkGetPhysicalDeviceProperties")(context.physical, &properties); context.limits = properties.limits; + if (externalHost) { + VkPhysicalDeviceExternalMemoryHostPropertiesEXT host{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 extended{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &host}; + function("vkGetPhysicalDeviceProperties2")(context.physical, &extended); + context.hostImportAlignment = host.minImportedHostPointerAlignment; + } + if (importHostMemory) std::printf("host import device %s vendor=0x%04x device=0x%04x driver=0x%08x alignment=%llu\n", properties.deviceName, properties.vendorID, properties.deviceID, properties.driverVersion, static_cast(context.hostImportAlignment)); context.bufferDeviceAddress = true; context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + 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 (...) { @@ -141,8 +219,11 @@ private: int main(int argc, char** argv) { try { + deviceSelectionTests(); + if (argc == 2 && std::string_view(argv[1]) == "--device-selection-cpu") return 0; RunBdaContractTests(); - if (argc == 2) { + const bool importFailures = argc == 2 && std::strcmp(argv[1], "--host-import-failures") == 0; + if (argc == 2 && !importFailures) { const auto shader = MakeBdaTestShader(0x7fff12340000ULL, 32); std::ofstream file(argv[1], std::ios::binary); file.write(reinterpret_cast(shader.data()), static_cast(shader.size() * sizeof(std::uint32_t))); @@ -151,12 +232,20 @@ int main(int argc, char** argv) { } std::unique_ptr device; try { - device = std::make_unique(); + device = std::make_unique(importFailures); } catch (const std::exception& error) { - if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + if (!canSkipVulkanFailure(std::getenv("ANYPS5_GPU"), std::getenv("ANYPS5_REQUIRE_VULKAN"))) throw; std::cout << "skipped, no usable Vulkan device: " << error.what() << '\n'; return 77; } + if (importFailures) { + if (device->GetContext().hostImportAlignment == 0) { + std::cout << "skipped, no external host memory import support\n"; + return 77; + } + AgcDriver::Tests::RunHostImportFailureTests(device->GetContext(), true); + return 0; + } Buffer buffer(device->GetContext(), 256, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); Require(buffer.DeviceAddress() != 0 && buffer.Bytes().size() == 256, "invalid real BDA buffer"); buffer.Bytes()[255] = std::byte{0x5a}; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp index 2804e7e81b..098d311465 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp @@ -4,9 +4,11 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include #include +#include #include #include #include +#include #include #include #include @@ -14,6 +16,12 @@ namespace { +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); + return text; +} + using namespace AgcDriver::Graphics; class Pipeline { @@ -213,6 +221,187 @@ static void RunBdaDwordWriteTests(const Context& context) { write(guest + 0x7c, 2, 8, static_cast(0), 64); } +static void RunBdaAnchorTests(const Context& context) { + namespace Abi = ShaderRecompiler::BdaAbi; + constexpr std::uint64_t guest = 0x7fff12340001ULL; + constexpr std::uint64_t limit = 0xffffffff00000000ULL; + constexpr std::uint64_t highest = 0xffffffffffffff00ULL; + constexpr std::uint32_t lanes = 64; + const auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT; + Buffer first(context, 3, usage); + Buffer second(context, 2, usage); + Buffer words(context, 16, usage); + Buffer shifted(context, 20, usage); + Buffer large(context, 1024, usage); + Buffer top(context, 16, usage); + Buffer above(context, 16, usage); + Buffer overlap(context, 0x200, usage); + std::uint32_t seed = 0x11; + for (auto* buffer : {&first, &second, &words, &shifted, &large, &top, &above, &overlap}) { + for (std::size_t index = 0; index < buffer->Bytes().size(); ++index) buffer->Bytes()[index] = std::byte(static_cast(seed + index * 7u)); + seed += 0x33; + } + for (std::uint32_t index = 0; index < 16u; ++index) words.Bytes()[index] = std::byte(static_cast(0xa0u + index)); + for (std::uint32_t index = 0; index < 20u; ++index) shifted.Bytes()[index] = std::byte(static_cast(0xc0u + index)); + std::vector ranges{{guest, guest + 3, first.DeviceAddress(), Abi::Read, 0}, + {guest + 3, guest + 5, second.DeviceAddress(), Abi::Read, 0}, + {guest + 0x100, guest + 0x110, words.DeviceAddress(), Abi::Read, 0}, + {guest + 0x200, guest + 0x210, shifted.DeviceAddress() + 1u, Abi::Read, 0}, + {guest + 0x400, guest + 0x800, large.DeviceAddress(), Abi::Read, 0}, + {guest + 0x700, guest + 0x900, overlap.DeviceAddress(), Abi::Read, 0}, + {limit - 0x10, limit, top.DeviceAddress(), Abi::Read, 0}, + {highest, highest + 0x10, above.DeviceAddress(), Abi::Read, 0}}; + Require(Abi::RangesOrdered(ranges.data(), ranges.size()), "the BDA anchor test table does not ascend by begin"); + Buffer table(context, sizeof(Abi::Header) + ranges.size() * sizeof(Abi::Range), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer fault(context, sizeof(Abi::Fault), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer output(context, (BdaAnchoredReadInputWord + 3u + lanes) * sizeof(std::uint32_t), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + struct Outcome { + Abi::Fault fault{}; + std::array values{}; + }; + std::uint32_t count = static_cast(ranges.size()); + std::uint32_t permissions = Abi::Read; + const auto run = [&](Pipeline& pipeline, std::uint32_t read, std::uint64_t anchor, const std::vector& lows) { + auto copy = ranges; + if (copy.size() > 2u) copy[2].permissions = permissions; + const Abi::Header header{Abi::Version, count, sizeof(Abi::Range), 0}; + std::memcpy(table.Bytes().data(), &header, sizeof(header)); + std::memcpy(table.Bytes().data() + sizeof(header), copy.data(), copy.size() * sizeof(Abi::Range)); + std::memset(fault.Bytes().data(), 0, fault.Bytes().size()); + std::memset(output.Bytes().data(), 0xa5, BdaAnchoredReadInputWord * sizeof(std::uint32_t)); + std::vector inputs{static_cast(anchor), static_cast(anchor >> 32u), read}; + inputs.insert(inputs.end(), lows.begin(), lows.end()); + std::memcpy(output.Bytes().data() + BdaAnchoredReadInputWord * sizeof(std::uint32_t), inputs.data(), inputs.size() * sizeof(std::uint32_t)); + pipeline.Run(1); + Outcome outcome; + std::memcpy(&outcome.fault, fault.Bytes().data(), sizeof(outcome.fault)); + std::memcpy(outcome.values.data(), output.Bytes().data(), sizeof(outcome.values)); + return outcome; + }; + struct Programs { + Programs(const Context& context, const BdaAnchoredReadProgram& reads, const std::array& buffers) : reads(reads), plain(context, MakeBdaAnchoredReadTestShader(reads, false), buffers), anchored(context, MakeBdaAnchoredReadTestShader(reads, true), buffers) {} + BdaAnchoredReadProgram reads; + Pipeline plain; + Pipeline anchored; + }; + const auto build = [&](const BdaAnchoredReadProgram& reads) { return std::make_unique(context, reads, std::array{&table, &fault, &output}); }; + const auto compare = [&](Programs& programs, std::initializer_list anchors, const std::vector& lows) { + std::vector> batches; + if (programs.reads.lanes == 1u) { + for (const auto low : lows) batches.push_back({low}); + } else { + batches.push_back(lows); + batches.back().resize(programs.reads.lanes, lows.back()); + } + for (std::uint32_t index = 0; index < programs.reads.reads.size(); ++index) { + const auto& read = programs.reads.reads[index]; + for (const auto anchor : anchors) { + for (const auto& batch : batches) { + const auto expected = run(programs.plain, index, anchor, batch); + const auto actual = run(programs.anchored, index, anchor, batch); + const bool faultsMatch = programs.reads.lanes == 1u ? std::memcmp(&expected.fault, &actual.fault, sizeof(expected.fault)) == 0 : expected.fault.state == actual.fault.state; + Require(faultsMatch && expected.values == actual.values, "an anchored BDA read differs from the same read through the lookups at anchor " + Hex(anchor) + " + " + std::to_string(batch[0]) + " + (" + std::to_string(read.immediate) + "), " + std::to_string(read.bits) + " bits, " + std::to_string(read.dwords) + " dwords"); + } + } + } + }; + std::vector reads; + for (const std::int32_t immediate : {0, 4, -1, -8, 0x7ffffff0, std::numeric_limits::min()}) { + for (const auto& [bits, dwords, used] : std::initializer_list>{{8, 1, 1}, {16, 1, 1}, {32, 1, 1}, {32, 4, 15}, {32, 4, 10}, {32, 3, 4}}) reads.push_back({immediate, bits, dwords, used}); + } + const auto anchors = {guest + 0x100, guest + 0x108, guest, guest + 3, guest + 0x200, guest - 1, guest + 0x110, guest + 0x400, guest + 0x6f0, guest + 0x700, guest + 0x800, limit - 0x10, limit - 4, highest, highest + 8, std::numeric_limits::max() - 2}; + std::vector lows{0u, 1u, 2u, 3u, 4u, 8u, 0xcu, 0xdu, 0xeu, 0xfu, 0x10u, 0x14u, 0x100u, 0x108u, 0x10cu, 0x1fcu, 0x200u, 0x20eu, 0x3f0u, 0x3fcu, 0x400u, 0x7fffu, 0x7fffffffu, 0x80000000u, 0xfffffff0u, 0xfffffffcu, 0xffffffffu}; + for (std::uint32_t lane = 0; lane < 32u; ++lane) lows.push_back(lane * 0x24u); + std::vector strided; + for (std::uint32_t lane = 0; lane < lanes; ++lane) strided.push_back(lane * 16u); + auto halfword = build({{{-1, 16, 1, 1}}, false, false, false, lanes}); + compare(*halfword, {guest + 0x100}, {1u}); + compare(*halfword, {guest + 0x100}, {0u, 1u}); + auto continuing = build({reads, false, false, false, lanes}); + compare(*continuing, anchors, lows); + compare(*continuing, {guest + 0x400}, strided); + auto elected = build({reads, false, true, false, lanes}); + compare(*elected, anchors, lows); + compare(*elected, {guest + 0x400}, strided); + auto coherent = build({{reads[0], reads[2], reads[9], reads[16], reads[22]}, false, false, true, lanes}); + compare(*coherent, anchors, lows); + auto stopping = build({reads, true, true, false, 1}); + compare(*stopping, anchors, {0u, 0x80000000u, 0xfffffffcu}); + const auto check = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low) { return run(stopping->anchored, read, anchor, {low}); }; + auto direct = std::make_unique(context, MakeBdaAnchoredReadTestShader({reads, true, false, false, 1, true}, true), std::array{&table, &fault, &output}); + const auto dword = [](Buffer& buffer, std::size_t offset) { + std::uint32_t value = 0; + std::memcpy(&value, buffer.Bytes().data() + offset, sizeof(value)); + return value; + }; + const auto loads = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low, std::initializer_list values) { + const auto outcome = run(*direct, read, anchor, {low}); + Require(outcome.fault.state == Abi::FaultState::Empty && std::equal(values.begin(), values.end(), outcome.values.begin()), "a read the anchor holds did not load directly at anchor " + Hex(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read)); + }; + const auto misses = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low) { + const auto outcome = run(*direct, read, anchor, {low}); + Require(outcome.fault.state == Abi::FaultState::Empty && outcome.values[0] == BdaAnchoredReadSentinel, "a read the anchor does not hold did not take the lookup path at anchor " + Hex(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read)); + }; + loads(2, guest + 0x100, 0, {dword(words, 0)}); + loads(2, guest + 0x100, 0xc, {dword(words, 0xc)}); + loads(3, guest + 0x100, 0, {dword(words, 0), dword(words, 4), dword(words, 8), dword(words, 0xc)}); + loads(0, guest + 0x200, 0xd, {0xceu}); + loads(0, guest + 3, 1, {static_cast(second.Bytes()[1])}); + loads(1, guest, 1, {static_cast(first.Bytes()[1]) | static_cast(first.Bytes()[2]) << 8u}); + loads(2, guest + 0x400, 0x2fc, {dword(large, 0x2fc)}); + loads(2, guest + 0x700, 0x1fc, {dword(overlap, 0x1fc)}); + loads(2, limit - 0x10, 0xc, {dword(top, 0xc)}); + misses(2, guest + 0x100, 0xd); + misses(3, guest + 0x100, 4); + misses(2, guest + 0x200, 0); + misses(1, guest + 2, 0); + misses(0, guest + 3, 2); + misses(2, guest + 0x400, 0x300); + misses(2, guest + 0x400, 0x2fd); + misses(2, limit - 0x10, 0xd); + misses(2, highest, 0); + misses(2, guest - 1, 1); + const auto wide = check(3, guest + 0x100, 0); + Require(wide.fault.state == Abi::FaultState::Empty && wide.values[0] == 0xa3a2a1a0u && wide.values[1] == 0xa7a6a5a4u && wide.values[2] == 0xabaaa9a8u && wide.values[3] == 0xafaeadacu, "an anchored span read returned wrong data"); + const auto some = check(4, guest + 0x100, 0); + Require(some.fault.state == Abi::FaultState::Empty && some.values[0] == 0u && some.values[1] == 0xa7a6a5a4u && some.values[2] == 0u && some.values[3] == 0xafaeadacu, "an anchored read of some dwords returned wrong data"); + const auto odd = check(2, guest + 0x200, 0); + Require(odd.fault.state == Abi::FaultState::Empty && odd.values[0] == 0xc4c3c2c1u, "an anchored read of an odd device address returned wrong data"); + const auto byte = check(0, guest + 0x200, 0xd); + Require(byte.fault.state == Abi::FaultState::Empty && byte.values[0] == 0xceu, "an anchored byte read returned wrong data"); + const auto below = check(20, guest + 0x108, 4); + Require(below.fault.state == Abi::FaultState::Empty && below.values[0] == 0xa7a6a5a4u, "an anchored read below its base returned wrong data"); + const auto unmapped = check(2, guest + 0x100, 0x10); + Require(unmapped.fault.state == Abi::FaultState::Ready && unmapped.fault.reason == Abi::FaultReason::Unmapped && unmapped.fault.address == guest + 0x110 && unmapped.fault.instruction == 0x1234 && unmapped.values[0] == 0xa5a5a5a5u, "an anchored read past its range did not fault"); + permissions = 0; + misses(2, guest + 0x100, 0); + compare(*stopping, {guest + 0x100}, {0u, 4u}); + compare(*elected, {guest + 0x100}, lows); + permissions = Abi::Read; + count = 3; + misses(2, guest + 0x100, 0); + misses(0, guest, 0); + compare(*stopping, {guest + 0x100, guest}, {0u, 4u}); + compare(*elected, {guest + 0x100, guest}, lows); + ranges = {{0, limit, large.DeviceAddress(), Abi::Read, 0}}; + count = 1; + compare(*stopping, {limit - 1}, {0xffffffffu}); + ranges = {{0x100000, 0x100010, top.DeviceAddress(), Abi::Read, 0}}; + for (std::uint64_t index = 1; index < 8u; ++index) ranges.push_back({0x10000000 + index * 0x1000, 0x10000010 + index * 0x1000, above.DeviceAddress(), Abi::Read, 0}); + count = static_cast(ranges.size()); + std::vector mixed(lanes, 0x100u); + mixed[26] = 0x1u; + mixed[31] = 0x110u; + mixed[40] = 0x10cu; + const auto divergent = run(continuing->plain, 2, 0xfff00, mixed); + Require(divergent.fault.state == Abi::FaultState::Ready && divergent.fault.reason == Abi::FaultReason::Unmapped, "a subgroup reading below the first range, past a range's end and inside it did not record the unmapped read"); + for (std::uint32_t lane = 0; lane < lanes; ++lane) { + const auto expected = lane == 26u || lane == 31u ? 0u : dword(top, lane == 40u ? 0xcu : 0u); + Require(divergent.values[lane * 4u] == expected, "a subgroup reading below the first range, past a range's end and inside it returned a wrong value in lane " + std::to_string(lane)); + } + compare(*continuing, {0xfff00}, mixed); +} + void RunBdaExecutionTests(const Context& context) { namespace Abi = ShaderRecompiler::BdaAbi; Buffer first(context, 3, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); @@ -332,7 +521,22 @@ void RunBdaExecutionTests(const Context& context) { } ranges[0].permissions = 0; run(guest, 8, 0, Abi::FaultReason::Permission); + const auto sequence = [&](std::initializer_list reasons, Abi::FaultReason recorded, std::uint32_t otherFaults) { + const Abi::Header header{Abi::Version, 2, sizeof(Abi::Range), 0}; + std::memcpy(table.Bytes().data(), &header, sizeof(header)); + std::memcpy(table.Bytes().data() + sizeof(header), ranges.data(), sizeof(ranges)); + std::memset(fault.Bytes().data(), 0, fault.Bytes().size()); + Pipeline pipeline(context, MakeBdaFaultSequenceTestShader(reasons), {&table, &fault, &output}); + pipeline.Run(1); + Abi::Fault report{}; + std::memcpy(&report, fault.Bytes().data(), sizeof(report)); + Require(report.state == Abi::FaultState::Ready && report.reason == recorded && report.otherFaults == otherFaults, "BDA faults of one dispatch were not recorded with the first reason and the sticky other-fault flag"); + }; + sequence({Abi::FaultReason::Unaligned, Abi::FaultReason::Unaligned}, Abi::FaultReason::Unaligned, 0); + sequence({Abi::FaultReason::Unaligned, Abi::FaultReason::Unmapped}, Abi::FaultReason::Unaligned, 1); + sequence({Abi::FaultReason::Unmapped, Abi::FaultReason::Unaligned}, Abi::FaultReason::Unmapped, 1); RunBdaDwordWriteTests(context); + RunBdaAnchorTests(context); } void RunBdaStoreExecutionTests(const Context& context) { @@ -364,9 +568,9 @@ void RunBdaStoreExecutionTests(const Context& context) { Abi::Fault report{}; std::memcpy(&report, fault.Bytes().data(), sizeof(report)); if (reason == Abi::FaultReason{}) { - Require(report.state == Abi::FaultState::Empty && report.reason == Abi::FaultReason{} && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.reserved == 0, label + "mapped store published a fault"); + Require(report.state == Abi::FaultState::Empty && report.reason == Abi::FaultReason{} && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.otherFaults == 0, label + "mapped store published a fault"); } else { - Require(report.state == Abi::FaultState::Ready && report.reason == reason && report.address == faultAddress && report.bytes == faultBytes && report.stage == static_cast(ShaderRecompiler::IrShaderStage::Compute) && report.instruction == 0x1234 && report.reserved == 0, label + "incorrect first fault metadata"); + Require(report.state == Abi::FaultState::Ready && report.reason == reason && report.address == faultAddress && report.bytes == faultBytes && report.stage == static_cast(ShaderRecompiler::IrShaderStage::Compute) && report.instruction == 0x1234 && report.otherFaults == 1, label + "incorrect first fault metadata"); } std::array words{}; std::memcpy(words.data(), fault.Bytes().data(), sizeof(words)); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 356fe0d4ff..860ba51020 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -1,4 +1,6 @@ #include "BdaTests.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestHeap.hpp" @@ -19,11 +21,15 @@ #include #include #include +#include +#include #include #include +#include #ifndef _WIN32 #include +#include #endif namespace { @@ -40,6 +46,118 @@ void reject(TAction action, const char* reason) { throw std::runtime_error(std::string("expected BDA rejection: ") + reason); } +void addressTableKeyTests() { + using ShaderRecompiler::BdaAbi::Range; + const std::array, 1> written{{{0x1000, 0x1040}}}; + const std::array, 1> otherWritten{{{0x1000, 0x1080}}}; + const std::array own{{{0x2000, 0x2100, 0x70000000, ShaderRecompiler::BdaAbi::Read, 0}}}; + const std::array moved{{{0x2000, 0x2100, 0x70010000, ShaderRecompiler::BdaAbi::Read, 0}}}; + const std::array, 2> ownAsWrites{{{0x2000, 0x2100}, {0x70000000, ShaderRecompiler::BdaAbi::Read}}}; + const auto plain = AddressTableKey({}, own); + Require(AddressTableKey({}, own) == plain, "one build's BDA table key is not stable"); + Require(AddressTableKey(written, own) != plain && AddressTableKey(written, own) != AddressTableKey(otherWritten, own), "BDA table keys of builds that differ only in their written ranges are equal"); + Require(AddressTableKey({}, moved) != plain, "BDA table keys of builds whose own region moved are equal"); + Require(AddressTableKey(ownAsWrites, {}) != plain, "written ranges and own regions with the same words give one BDA table key"); +} + +void snapshotTableTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t piece = 65536; + void* block = ::operator new(piece, std::align_val_t{piece}); + std::memset(block, 0x3c, piece); + auto importing = context; + importing.hostImportAlignment = piece; + const auto change = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, piece, true, true, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + change(true); + alignas(256) static std::array source{}; + const auto snapshotAddress = reinterpret_cast(source.data()); + std::array first{}; + std::array second{}; + first.fill(std::byte{0x11}); + second.fill(std::byte{0x22}); + struct Build { + std::unique_ptr memory; + std::unique_ptr bda; + std::uint64_t snapshot = 0; + VkBuffer table = VK_NULL_HANDLE; + VkBuffer view = VK_NULL_HANDLE; + }; + const auto build = [&](std::span bytes, bool written, bool addressable, std::uint64_t address) { + Build result; + result.memory = std::make_unique(importing); + auto& memory = *result.memory; + if (addressable) memory.AcquireRegistered(); + if (written) memory.AddWritable(reinterpret_cast(block) + 256, 64); + memory.AddSnapshot({address, bytes}); + memory.Upload(addressable); + std::uint32_t adjustment = 0; + result.view = memory.Descriptor(address, bytes.size(), adjustment).buffer; + if (!addressable) return result; + const auto ranges = memory.AddressRanges(); + const auto found = std::find_if(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == address; }); + Require(found != ranges.end() && found->end == address + bytes.size(), "the snapshot has no BDA table entry of its own"); + result.snapshot = found->deviceAddress; + const auto held = access.addressBytes(result.snapshot); + Require(held.size() >= bytes.size() && std::memcmp(held.data(), bytes.data(), bytes.size()) == 0, "the snapshot's device copy does not hold its bytes"); + if (const auto cached = memory.CachedAddressTable()) { + Require(cached->ranges->size() == ranges.size() && std::memcmp(cached->ranges->data(), ranges.data(), ranges.size() * sizeof(ranges.front())) == 0, "a cached BDA table differs from the build's ranges"); + } + result.bda = std::make_unique(importing, memory); + result.table = result.bda->Table().buffer; + return result; + }; + const bool spaces = AddressSpaceCounters().enabled; + { + auto a = build(first, false, true, snapshotAddress); + auto plain = build(first, false, false, snapshotAddress); + auto d = build(first, true, true, snapshotAddress); + auto b = build(first, false, true, snapshotAddress); + auto c = build(second, false, true, snapshotAddress); + Require(a.snapshot == b.snapshot && a.view == b.view, "two builds of one snapshot with the same bytes do not share its device copy"); + Require(c.snapshot != a.snapshot, "a snapshot with other bytes took the device copy of the first"); + Require(plain.view != a.view && d.snapshot == a.snapshot, "a snapshot copy made for BDA serves a build bound by descriptor, or a build with written ranges did not share the snapshot copy"); + if (spaces) { + Require(a.memory->CachedAddressTable().has_value() && c.memory->CachedAddressTable().has_value() && d.memory->CachedAddressTable().has_value(), "a build with its own regions over the address space has no cached BDA table"); + Require(a.table == b.table, "two builds of the same space and snapshot do not share their BDA table"); + Require(c.table != a.table, "a build with other snapshot bytes took another build's BDA table"); + Require(a.memory->CachedAddressTable()->serial == b.memory->CachedAddressTable()->serial && c.memory->CachedAddressTable()->serial != a.memory->CachedAddressTable()->serial, "cached BDA table serials do not follow the build's ranges"); + } + ClearSnapshotCopies(importing.device); + auto e = build(first, false, true, snapshotAddress); + Require(e.snapshot != a.snapshot, "a snapshot copy outlived the teardown of its device's copies"); + for (auto* item : {&a, &b, &c, &d, &e, &plain}) item->memory->WriteBack(); + } + if (spaces) { + std::vector> variants(66); + for (std::size_t index = 0; index < variants.size(); ++index) { + variants[index].fill(std::byte{0x40}); + variants[index][0] = static_cast(index); + variants[index][1] = static_cast(index >> 8u); + } + alignas(256) static std::array sources{}; + std::vector builds; + const auto serial = [&](std::size_t index) { + builds.push_back(build(variants[index], false, true, reinterpret_cast(sources.data()) + index * 512)); + return builds.back().memory->CachedAddressTable()->serial; + }; + const auto kept = serial(0); + const auto evicted = serial(1); + for (std::size_t index = 2; index < 64; ++index) serial(index); + Require(serial(0) == kept, "a BDA table was dropped before 64 newer tables of its space"); + serial(64); + Require(serial(0) == kept, "the most recently used BDA table of a space was evicted"); + Require(serial(1) != evicted, "the least recently used BDA table of a space was kept past 64 tables"); + for (auto& item : builds) item.memory->WriteBack(); + } + ClearSnapshotCopies(importing.device); + change(false); + ::operator delete(block, std::align_val_t{piece}); +} + void importCrossingTests(const Context& context, const BdaTestAccess& access) { constexpr std::size_t half = 65536; void* block = ::operator new(2 * half, std::align_val_t{half}); @@ -119,6 +237,59 @@ void importSpanTests(const Context& context) { ::operator delete(block, std::align_val_t{piece}); } +void spaceWritableWritesTests(const Context& context) { + if (!AddressSpaceCounters().enabled) return; + constexpr std::size_t piece = 65536; + auto* writable = static_cast(::operator new(piece, std::align_val_t{piece})); + auto* readOnly = static_cast(::operator new(piece, std::align_val_t{piece})); + std::memset(writable, 0x11, piece); + std::memset(readOnly, 0x22, piece); + auto importing = context; + importing.hostImportAlignment = piece; + const auto change = [&](std::uint8_t* range, bool add, bool canWrite) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, canWrite, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + change(writable, true, true); + change(readOnly, true, false); + const auto same = [](const std::vector& left, const std::vector& right) { + return left.size() == right.size() && (left.empty() || std::memcmp(left.data(), right.data(), left.size() * sizeof(left.front())) == 0); + }; + struct Built { + std::uint64_t serial; + std::vector ranges; + }; + const auto build = [&](std::initializer_list> written, const char* step) { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + for (const auto& [offset, bytes] : written) leased.AddWritable(reinterpret_cast(writable) + offset, bytes); + leased.Upload(true); + const auto cached = leased.CachedAddressTable(); + Require(cached.has_value() && cached->ranges != nullptr, std::string("a build of the space alone has no cached table: ") + step); + Built result{cached->serial, leased.AddressRanges()}; + Require(same(*cached->ranges, result.ranges), std::string("the cached table differs from AddressRanges(): ") + step); + leased.WriteBack(); + return result; + }; + const auto plain = build({}, "no writes"); + const auto once = build({{256, 512}}, "one write in a writable import"); + const auto twice = build({{4096, 64}, {8192, 128}}, "two writes in a writable import"); + Require(once.serial == plain.serial && twice.serial == plain.serial, "writes in a writable import did not bind the space's table"); + Require(same(once.ranges, plain.ranges) && same(twice.ranges, plain.ranges), "writes in a writable import changed the BDA table"); + const auto writableBegin = reinterpret_cast(writable); + const auto readOnlyBegin = reinterpret_cast(readOnly); + for (const auto& range : once.ranges) { + if (range.begin == writableBegin) Require((range.permissions & ShaderRecompiler::BdaAbi::Write) != 0u, "the writable import lost its write permission"); + if (range.begin == readOnlyBegin) Require((range.permissions & ShaderRecompiler::BdaAbi::Write) == 0u, "the read-only import gained write permission"); + } + change(writable, false, true); + change(readOnly, false, false); + ::operator delete(writable, std::align_val_t{piece}); + ::operator delete(readOnly, std::align_val_t{piece}); +} + void failedImportRemapTests(const Context& context, const BdaTestAccess& access) { constexpr std::size_t piece = 65536; auto* block = static_cast(::operator new(4 * piece, std::align_val_t{piece})); @@ -147,7 +318,7 @@ void failedImportRemapTests(const Context& context, const BdaTestAccess& access) access.limitMemory(0); const auto attempts = access.allocationAttempts(); Require(HostImportFor(importing, address, 2 * piece) == nullptr && access.allocationAttempts() == attempts + 1, "host import failure was not exercised"); - Require(owner.use_count() == 1, "a failed import pins its owner"); + Require(owner.use_count() == 2, "a failed import pins its owner beyond the registry and its kept lease snapshot"); Require(HostImportFor(importing, address, 2 * piece) == nullptr && access.allocationAttempts() == attempts + 1, "an unchanged failed import was retried"); add(3); Require(HostImportFor(importing, address, 2 * piece) == nullptr && access.allocationAttempts() == attempts + 1, "an unrelated mapping retried a failed import"); @@ -211,6 +382,62 @@ void spaceImportRunTests(const Context& context) { ::operator delete(block, std::align_val_t{piece}); } +void spaceReadListTests(const Context& context) { + if (!AddressSpaceCounters().enabled) return; + constexpr std::size_t piece = 65536; + void* block = ::operator new(3 * piece, std::align_val_t{piece}); + std::memset(block, 0x5c, 3 * piece); + auto importing = context; + importing.hostImportAlignment = piece; + auto* guest = static_cast(block); + 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, true, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto build = [&](const char* step) { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.Upload(true); + const auto split = leased.SplitInPlaceReads(); + Require(split.has_value() && split->space != nullptr, std::string("an address-based build that took the space whole has no split reads: ") + step); + auto joined = *split->space; + joined.insert(joined.end(), split->own.begin(), split->own.end()); + Require(joined == leased.InPlaceReads(), std::string("the split reads differ from InPlaceReads: ") + step); + Require(Recorder::ValidReadList(*split->space), std::string("the space's read list is not sorted and disjoint: ") + step); + leased.WriteBack(); + Require(!leased.SplitInPlaceReads().has_value(), "a committed build still offers split reads"); + return split->space; + }; + const auto lists = [&](const std::shared_ptr& list, const std::uint8_t* range) { + const auto begin = reinterpret_cast(range); + return std::any_of(list->begin(), list->end(), [&](const auto& read) { return read.first <= begin && begin + piece <= read.second; }); + }; + change(guest, true); + change(guest + piece, true); + const auto first = build("first build"); + Require(lists(first, guest) && lists(first, guest + piece) && !lists(first, guest + 2 * piece), "the space's read list does not hold exactly the registered pieces"); + const auto again = build("unchanged registry"); + Require(again == first, "a build in an unchanged registry did not share the space's read list"); + change(guest + 2 * piece, true); + const auto added = build("range added"); + Require(added != first && lists(added, guest + 2 * piece), "a registered range is missing from the read list after the registry changed"); + change(guest + piece, false); + const auto removed = build("range removed"); + Require(removed != added && !lists(removed, guest + piece) && lists(removed, guest), "a removed range is still in the read list"); + { + GuestBufferMemory plain(importing); + plain.AddReadable(reinterpret_cast(guest), 256); + plain.Upload(false); + Require(!plain.SplitInPlaceReads().has_value(), "a build without the address space offers split reads"); + plain.WriteBack(); + } + change(guest, false); + change(guest + 2 * piece, false); + ::operator delete(block, std::align_val_t{piece}); +} + void importWindowTests(const Context& context) { constexpr std::size_t piece = 65536; constexpr std::size_t window = 2 * piece; @@ -325,6 +552,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 { @@ -383,6 +741,196 @@ void importWholeRangeOverWindowTests(const Context& context) { leased.WriteBack(); } +struct ObservedHostImport { + const void* pointer; + VkDeviceSize bytes; +}; + +class ImportAllocationProbe { +public: + explicit ImportAllocationProbe(Context& context) : context(context), original(context.deviceProc) { + Require(active == nullptr && original != nullptr, "invalid import allocation probe scope"); + active = this; + context.deviceProc = Resolve; + } + ~ImportAllocationProbe() { + context.deviceProc = original; + active = nullptr; + } + ImportAllocationProbe(const ImportAllocationProbe&) = delete; + ImportAllocationProbe& operator=(const ImportAllocationProbe&) = delete; + + std::vector calls; + VkResult failure = VK_SUCCESS; + std::function onImport; + +private: + static PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice device, const char* name) { + Require(active != nullptr, "import allocation probe escaped its scope"); + if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast(Allocate); + return active->original(device, name); + } + static VkResult VKAPI_CALL Allocate(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) { + Require(active != nullptr, "import allocation callback escaped its scope"); + for (const auto* next = static_cast(info->pNext); next != nullptr; next = next->pNext) { + if (next->sType != VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT) continue; + const auto* imported = reinterpret_cast(next); + active->calls.push_back({imported->pHostPointer, info->allocationSize}); + if (active->onImport) { + auto callback = std::move(active->onImport); + callback(); + } + if (active->failure != VK_SUCCESS) return active->failure; + } + const auto allocate = reinterpret_cast(active->original(device, "vkAllocateMemory")); + Require(allocate != nullptr, "import allocation probe has no original allocator"); + return allocate(device, info, allocator, memory); + } + + Context& context; + PFN_vkGetDeviceProcAddr original; + inline static thread_local ImportAllocationProbe* active = nullptr; +}; + +class UnrelatedImportProbeRange { +public: + UnrelatedImportProbeRange() { + GuestAllocations::Mutation().Add(storage.data(), storage.size(), true, false, false); + } + ~UnrelatedImportProbeRange() { + GuestAllocations::Mutation().Remove(storage.data()); + } + UnrelatedImportProbeRange(const UnrelatedImportProbeRange&) = delete; + UnrelatedImportProbeRange& operator=(const UnrelatedImportProbeRange&) = delete; + +private: + std::array storage{}; +}; + +void importMappedFailureGenerationTests(const Context& context) { + ClearHostImports(context.device); + WindowedRange range(context, WindowedRange::piece, 1); + ImportAllocationProbe probe(range.importing); + probe.failure = VK_ERROR_INVALID_EXTERNAL_HANDLE; + GuestAllocations::Mapped mapped; + const auto collect = [&] { + mapped.clear(); + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + for (const auto& owner : lease) if (owner->address == range.First()) mapped.push_back(owner); + Require(mapped.size() == 1u, "the map failure fixture lost its registered range"); + }; + collect(); + std::unique_ptr unrelated; + probe.onImport = [&] { unrelated = std::make_unique(); }; + const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(ImportMappedRanges(range.importing, mapped, generation, true), "the first failed map import was deferred"); + Require(probe.calls.size() == 1u && unrelated != nullptr, "the map failure fixture did not mutate an unrelated range during allocation"); + Require(GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation, "the concurrent unrelated mapping did not advance the registry"); + Require(ImportMappedRanges(range.importing, mapped, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true), "the repeated failed map import was deferred"); + Require(probe.calls.size() == 1u, "an unrelated mapping during a failed import discarded its owner failure memo"); + } + { + GuestAllocations::Mutation mutation; + mutation.Remove(reinterpret_cast(range.First())); + mutation.Add(reinterpret_cast(range.First()), range.bytes, true, false, true); + } + collect(); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(ImportMappedRanges(range.importing, mapped, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true), "the replacement owner map import was deferred"); + Require(probe.calls.size() == 2u, "a replacement owner did not retry its predecessor's failed import"); + } + ClearHostImports(context.device); +} + +void importStaleProbeTests(const Context& context, const BdaTestAccess& access) { + for (const auto scenario : {0u, 1u, 2u}) { + const bool held = scenario == 1u; + const bool exactWhole = scenario == 2u; + WindowedRange range(context, 2 * WindowedRange::piece, 2); + ImportAllocationProbe probe(range.importing); + const auto first = range.First(); + auto* guest = reinterpret_cast(first); + for (std::size_t offset = 17u; offset < range.bytes; offset += 4096u) guest[offset] = static_cast((offset / 4096u) & 0xffu); + const std::vector originalGuest(guest, guest + range.bytes); + const auto* window = HostImportFor(range.importing, first, exactWhole ? range.bytes : 32u); + Require(window != nullptr && !window->joined && window->base == first && window->bytes == (exactWhole ? range.bytes : range.window), "the stale-probe fixture did not create its ordinary import extent"); + const auto originalBuffer = window->buffer; + const auto originalUse = window->lastUse; + const auto originalSerial = HostImportSerial(range.importing, first, 32u, false); + auto mockContents = access.bytes(window->buffer); + Require(mockContents.size() == window->bytes, "the mock import buffer has an incorrect extent"); + std::copy_n(originalGuest.begin(), window->bytes, mockContents.begin()); + std::uint32_t scratch = 0x12345678u; + { + GuestBufferMemory copied(range.importing); + copied.AddReadable(reinterpret_cast(&scratch), sizeof(scratch)); + copied.Upload(false); + copied.WriteBack(); + } + Require(window->lastUse == originalUse && HostImportSerial(range.importing, first, 32u, false) == originalSerial, "an unrelated copied build touched or replaced the prefix window"); + GuestBufferMemory reader(range.importing); + VkDescriptorBufferInfo heldView{}; + std::uint32_t heldAdjustment = 0u; + if (held) { + reader.AddReadable(first + 16u, 32u); + reader.Upload(false); + heldView = reader.Descriptor(first + 16u, 32u, heldAdjustment); + Require(heldView.buffer == originalBuffer, "the held control did not bind the prefix window"); + } + GuestAllocations::Mapped mapped; + { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + for (const auto& owner : lease) if (owner->address == first && owner->bytes == range.bytes) mapped.push_back(owner); + } + Require(mapped.size() == 1u, "the stale-probe fixture lost its registered range"); + const auto originalOwner = mapped.front().lock(); + const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + UnrelatedImportProbeRange unrelated; + Require(GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation && HostImportsIdentity(range.importing).device == VK_NULL_HANDLE, + "the unrelated registry change did not make imports stale"); + const auto attempts = access.allocationAttempts(); + probe.calls.clear(); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(ImportMappedRanges(range.importing, mapped, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false), "the current device did not admit the map-time import pass"); + } + Require(mapped.front().lock() == originalOwner, "an unrelated registry change replaced the import's memory owner"); + if (held) { + Require(access.allocationAttempts() == attempts && probe.calls.empty(), "a stale probe retired an import held by an actual descriptor"); + const auto* kept = HostImportFor(range.importing, first + 16u, 32u); + Require(kept != nullptr && kept->buffer == originalBuffer && kept->bytes == range.window && HostImportSerial(range.importing, first, 32u, false) == originalSerial, + "the actual held import lost its buffer, extent or serial"); + std::uint32_t adjustment = 0u; + const auto view = reader.Descriptor(first + 16u, 32u, adjustment); + Require(view.buffer == heldView.buffer && view.offset == heldView.offset && view.range == heldView.range && adjustment == heldAdjustment, + "a map-time stale probe invalidated a descriptor that still uses its import"); + const auto contents = access.bytes(view.buffer); + Require(view.offset + adjustment + 32u <= contents.size() && std::memcmp(contents.data() + view.offset + adjustment, reinterpret_cast(first + 16u), 32u) == 0, + "the protected descriptor no longer reads the registered bytes"); + reader.WriteBack(); + } else if (exactWhole) { + Require(access.allocationAttempts() == attempts && probe.calls.empty() && window->lastUse > originalUse, + "accepting the stale exact whole import reallocated it or did not protect its returned pointer"); + const auto* kept = HostImportFor(range.importing, first, range.bytes); + Require(kept != nullptr && kept->buffer == originalBuffer && kept->ranges.size() == 1u && kept->ranges.front() == originalOwner && HostImportSerial(range.importing, first, range.bytes, false) == originalSerial, + "accepting the exact whole import changed its native or registered identity"); + } else { + Require(access.allocationAttempts() == attempts + 1u, "a discarded stale probe made an unheld prefix window block the whole import"); + const auto* whole = HostImportFor(range.importing, first, range.bytes); + Require(whole != nullptr && whole->base == first && whole->bytes == range.bytes && whole->ranges.size() == 1u && whole->ranges.front() == originalOwner, + "the stale-probe repair did not retain the exact registered owner over the whole import"); + Require(HostImportSerial(range.importing, first, range.bytes, false) != originalSerial, "replacing the unheld window reused its old import identity"); + Require(probe.calls.size() == 1u && probe.calls.front().pointer == reinterpret_cast(first) && probe.calls.front().bytes == range.bytes, + "the replacement native import did not reference the exact whole registered memory"); + Require(access.bytes(whole->buffer).size() == range.bytes, "the replacement import buffer does not cover its registered extent"); + } + Require(scratch == 0x12345678u && std::equal(originalGuest.begin(), originalGuest.end(), guest), "the import probe changed guest bytes or unrelated copied data"); + } +} + // An address-based build whose whole-range import does not fit the budget leaves the range's window alone: before, // every such build retired the window and the next access imported it again. void importRefusedWholeRangeTests(const Context& context) { @@ -474,6 +1022,807 @@ void importAtMapOverBudgetTests(const Context& context) { for (auto* block : {kept, used, mapped}) ::operator delete(block, std::align_val_t{bytes}); } +void importRequesterTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t piece = 65536; + constexpr std::size_t pieces = 8; + auto* block = static_cast(::operator new(pieces * piece, std::align_val_t{piece})); + std::memset(block, 0x2d, pieces * piece); + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(0); + const auto address = [&](std::size_t index) { return reinterpret_cast(block + index * piece); }; + const auto change = [&](std::size_t index, bool add) { + GuestAllocations::Mutation mutation; + if (add) mutation.Add(block + index * piece, piece, true, index % 2 == 0, true); + else mutation.Remove(block + index * piece); + }; + const auto counted = [](ImportRequester requester) { return HostImportCounters().requesters[static_cast(requester)]; }; + const auto made = [&](ImportRequester requester) { return counted(requester).made; }; + Require(std::string(ImportRequesterName(ImportRequester::Map)) == "map" && std::string(ImportRequesterName(ImportRequester::Build)) == "build" && std::string(ImportRequesterName(ImportRequester::Access)) == "access" && std::string(ImportRequesterName(ImportRequester::Join)) == "join" && std::string(ImportRequesterName(ImportRequester::Segment)) == "segment", "an import requester has the wrong name"); + change(0, true); + { + GuestAllocations::Mapped ranges; + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + for (const auto& range : lease) { + if (range->address == address(0)) ranges.push_back(range); + } + Require(ranges.size() == 1, "the requester test's mapped range is not registered"); + const auto before = made(ImportRequester::Map); + static_cast(ImportMappedRanges(importing, ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true)); + Require(made(ImportRequester::Map) == before + 1 && HostImportCovers(importing, address(0), piece), "a range imported at map time was not counted for the map requester"); + } + change(1, true); + { + const auto before = made(ImportRequester::Access); + Require(HostImportFor(importing, address(1), 16) != nullptr && made(ImportRequester::Access) == before + 1, "a lookup's import was not counted for the access requester"); + } + change(2, true); + { + const auto before = made(ImportRequester::Segment); + const auto access = made(ImportRequester::Access); + Require(HostImportFor(importing, address(2), 16, ImportRequester::Segment) != nullptr && made(ImportRequester::Segment) == before + 1 && made(ImportRequester::Access) == access, "a write-back segment's import was not counted for the segment requester"); + } + change(3, true); + { + const auto before = counted(ImportRequester::Build); + const auto access = made(ImportRequester::Access); + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.Upload(true); + leased.WriteBack(); + Require(made(ImportRequester::Build) > before.made && HostImportCovers(importing, address(3), piece) && made(ImportRequester::Access) == access, "an address-based build's import was not counted for the build requester"); + } + { + const auto live = HostImportLiveBytes(); + SetHostImportBudget(live); + change(4, true); + const auto before = counted(ImportRequester::Build); + { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.Upload(true); + leased.WriteBack(); + } + const auto after = counted(ImportRequester::Build); + SetHostImportBudget(0); + Require(after.refused > before.refused && after.refusedBytes >= before.refusedBytes + piece && after.made == before.made, "an address-based build's refusal was not counted for the build requester"); + } + change(5, true); + { + const auto before = counted(ImportRequester::Access); + access.limitMemory(0); + const auto* failed = HostImportFor(importing, address(5), 16); + access.limitMemory(std::nullopt); + Require(failed == nullptr && counted(ImportRequester::Access).failed == before.failed + 1, "a lookup's failed import was not counted for the access requester"); + } + { + constexpr std::size_t window = 2 * piece; + constexpr std::size_t large = 3 * piece; + auto* joined = static_cast(::operator new(2 * large, std::align_val_t{window})); + std::memset(joined, 0x1d, 2 * large); + SetHostImportWindow(window); + { + GuestAllocations::Mutation mutation; + mutation.Add(joined, large, true, false, true); + mutation.Add(joined + large, large, true, false, true); + } + const auto before = made(ImportRequester::Join); + const auto second = reinterpret_cast(joined + large); + { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(second - 16, 32); + leased.Upload(true); + leased.WriteBack(); + } + Require(made(ImportRequester::Join) == before + 1, "a join at bind was not counted for the join requester"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(joined); + mutation.Remove(joined + large); + } + SetHostImportWindow(0); + ::operator delete(joined, std::align_val_t{window}); + } + const auto counts = HostImportCounters(); + Require(counts.live >= 4 && counts.liveBytes == HostImportLiveBytes(), "the live import counts disagree with the live bytes"); + for (std::size_t index = 0; index < 6; ++index) change(index, false); + ClearHostImports(importing.device); + ::operator delete(block, std::align_val_t{piece}); +} + +void hostImportReportTests() { + HostImportCounts counts; + counts.live = 3; + counts.liveBytes = 3u << 20u; + counts.requesters[static_cast(ImportRequester::Map)] = {2, 1u << 20u, 0, 0, 0}; + counts.requesters[static_cast(ImportRequester::Build)] = {5, 4u << 20u, 7, 7u << 19u, 1, 4}; + counts.requesters[static_cast(ImportRequester::Segment)] = {1, 1u << 16u, 0, 0, 2}; + const auto text = FormatHostImportCounts(counts); + Require(text == "3 live (3.0 MiB); made 8 (5.1 MiB), refused 7 (3.5 MiB, 4 remembered), failed 3; by requester (made/refused/failed): map 2/0/0, build 5/7/1, access 0/0/0, join 0/0/0, segment 1/0/2", "the host import report is wrong: " + text); +} + +class PieceWindow { +public: + static constexpr std::size_t Piece = 65536; + static constexpr std::size_t Window = std::size_t{8} << 20u; + + explicit PieceWindow(const Context& context, bool views = false) : importing(context) { +#ifndef _WIN32 + if (views) { + reserved = mmap(nullptr, 2 * Window, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(reserved != MAP_FAILED, "cannot reserve the block of 16 KiB views"); + block = reinterpret_cast((reinterpret_cast(reserved) + Window - 1) / Window * Window); + file = memfd_create("anyps5-import-block-views", MFD_CLOEXEC); + Require(file >= 0 && ftruncate(file, static_cast(Window)) == 0, "cannot create the file behind the 16 KiB views"); + constexpr std::size_t view = 16384; + constexpr std::size_t count = Window / view; + for (std::size_t index = 0; index < count; ++index) { + const auto offset = static_cast((index % 4 * (count / 4) + index / 4) * view); + Require(mmap(block + index * view, view, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, file, offset) == block + index * view, "cannot map a 16 KiB view"); + } + } +#endif + if (block == nullptr) block = static_cast(::operator new(Window, std::align_val_t{Window})); + std::memset(block, 0x4b, Window); + importing.hostImportAlignment = Piece; + SetHostImportWindow(Window); + ClearHostImports(importing.device); + } + ~PieceWindow() { + for (const auto offset : registered) { + GuestAllocations::Mutation mutation; + mutation.Remove(block + offset); + } + ClearHostImports(importing.device); + SetHostImportBudget(0); + SetHostImportWindow(0); +#ifndef _WIN32 + if (reserved != nullptr) { + munmap(reserved, 2 * Window); + close(file); + return; + } +#endif + ::operator delete(block, std::align_val_t{Window}); + } + PieceWindow(const PieceWindow&) = delete; + PieceWindow& operator=(const PieceWindow&) = delete; + + void Add(std::size_t offset, std::size_t bytes, bool writable = true, bool gpu = true) { + GuestAllocations::Mutation mutation; + mutation.Add(block + offset, bytes, true, writable, gpu); + registered.push_back(offset); + } + void AddPieces(std::size_t from, std::size_t to) { + for (auto offset = from; offset < to; offset += Piece) Add(offset, Piece); + } + void Remove(std::size_t offset) { + GuestAllocations::Mutation mutation; + mutation.Remove(block + offset); + registered.erase(std::find(registered.begin(), registered.end(), offset)); + } + std::uintptr_t Address(std::size_t offset) const { return reinterpret_cast(block + offset); } + void Build() const { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.Upload(true); + leased.WriteBack(); + } + std::uint64_t Serial(std::size_t offset) const { return HostImportSerial(importing, Address(offset), Piece, false); } + std::size_t Imports(std::size_t from, std::size_t to) const { + std::vector serials; + for (auto offset = from; offset < to; offset += Piece) { + if (const auto serial = Serial(offset); serial != 0) serials.push_back(serial); + } + std::sort(serials.begin(), serials.end()); + return static_cast(std::unique(serials.begin(), serials.end()) - serials.begin()); + } + bool Covered(std::size_t from, std::size_t to) const { + for (auto offset = from; offset < to; offset += Piece) { + if (Serial(offset) == 0) return false; + } + return true; + } + bool Block(std::size_t offset, std::size_t bytes) const { return HostImportCovers(importing, Address(offset), bytes) && Serial(offset) == Serial(offset + bytes - Piece) && (offset == 0 || Serial(offset - Piece) != Serial(offset)) && (offset + bytes == Window || Serial(offset + bytes) != Serial(offset)); } + + Context importing; + +private: + std::uint8_t* block = nullptr; + void* reserved = nullptr; + int file = -1; + std::vector registered; +}; + +std::string blockText(const PieceWindow& window, std::size_t from, std::size_t to) { + return std::to_string(window.Imports(from, to)) + " imports"; +} + +void importBlockMapTests(const Context& context, bool views) { + PieceWindow window(context, views); + constexpr auto half = PieceWindow::Window / 2; + for (std::size_t offset = 0; offset < half; offset += PieceWindow::Piece) { + window.Add(offset, PieceWindow::Piece); + GuestAllocations::Mapped ranges; + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + for (const auto& range : lease) { + if (range->address == window.Address(offset)) ranges.push_back(range); + } + static_cast(ImportMappedRanges(window.importing, ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true)); + } + Require(HostImportFor(window.importing, window.Address(0), 16) != nullptr, "the first of 64 pieces mapped one by one was not imported"); + Require(window.Imports(0, half) == 1 && window.Block(0, half), std::string(views ? "64 adjacent 64 KiB pieces of four 16 KiB views each" : "64 adjacent 64 KiB pieces") + " mapped one by one were not imported as one 4 MiB block: " + blockText(window, 0, half)); +} + +void importBlockBuildTests(const Context& context) { + PieceWindow window(context); + constexpr auto half = PieceWindow::Window / 2; + window.AddPieces(half, PieceWindow::Window); + window.Build(); + Require(window.Imports(half, PieceWindow::Window) == 1 && window.Block(half, half), "64 adjacent 64 KiB pieces were not imported as one 4 MiB block by an address-based build: " + blockText(window, half, PieceWindow::Window)); + window.Add(0, half, false); + window.Build(); + Require(window.Imports(0, PieceWindow::Window) == 2 && window.Block(0, half) && window.Block(half, half), "a 4 MiB read-only range and 64 writable 64 KiB pieces in one window were not imported as the range and one 4 MiB block: " + blockText(window, 0, PieceWindow::Window)); +} + +void importBlockHoleTests(const Context& context) { + PieceWindow window(context); + constexpr auto half = PieceWindow::Window / 2; + window.Add(0, half); + window.AddPieces(half, PieceWindow::Window - PieceWindow::Piece); + window.Build(); + constexpr std::size_t KiB = 1024; + Require(window.Imports(0, PieceWindow::Window) == 7, "a window whose last 64 KiB is unmapped was not imported as 7 blocks: " + blockText(window, 0, PieceWindow::Window)); + Require(window.Block(0, half) && window.Block(half, 2048 * KiB) && window.Block(half + 2048 * KiB, 1024 * KiB) && window.Block(half + 3072 * KiB, 512 * KiB) && window.Block(half + 3584 * KiB, 256 * KiB) && window.Block(half + 3840 * KiB, 128 * KiB) && window.Block(half + 3968 * KiB, 64 * KiB), "a window whose last 64 KiB is unmapped was not split into the largest aligned blocks"); +} + +void importBlockUnalignedTests(const Context& context) { + PieceWindow window(context); + constexpr std::size_t KiB = 1024; + window.AddPieces(768 * KiB, 3840 * KiB); + window.Build(); + Require(window.Imports(768 * KiB, 3840 * KiB) == 5, "48 pieces from 768 KiB were not imported as 5 aligned blocks: " + blockText(window, 768 * KiB, 3840 * KiB)); + Require(window.Block(768 * KiB, 256 * KiB) && window.Block(1024 * KiB, 1024 * KiB) && window.Block(2048 * KiB, 1024 * KiB) && window.Block(3072 * KiB, 512 * KiB) && window.Block(3584 * KiB, 256 * KiB), "48 pieces from 768 KiB were not split into 256 KiB, 1 MiB, 1 MiB, 512 KiB and 256 KiB blocks"); +} + +void importBlockFlagTests(const Context& context) { + PieceWindow window(context); + constexpr auto half = PieceWindow::Window / 2; + constexpr auto split = half / 2; + window.AddPieces(0, split); + window.Add(split, PieceWindow::Piece, true, false); + window.AddPieces(split + PieceWindow::Piece, half); + window.Build(); + Require(window.Serial(split) == 0 && window.Imports(0, split) == 1 && window.Block(0, split), "pieces before a range that is not GPU mapped were not imported as one block: " + blockText(window, 0, split)); + Require(window.Imports(split + PieceWindow::Piece, half) == 5 && window.Covered(split + PieceWindow::Piece, half), "pieces after a range that is not GPU mapped were not imported as the 5 aligned blocks after it: " + blockText(window, split + PieceWindow::Piece, half)); +} + +void importBlockRemapTests(const Context& context) { + PieceWindow window(context); + constexpr auto half = PieceWindow::Window / 2; + window.AddPieces(0, half); + window.Build(); + Require(window.Imports(0, half) == 1 && window.Block(0, half), "64 pieces were not imported as one block before the remap: " + blockText(window, 0, half)); + const auto before = window.Serial(0); + constexpr auto remapped = 10 * PieceWindow::Piece; + window.Remove(remapped); + window.Add(remapped, PieceWindow::Piece); + window.Build(); + Require(window.Imports(0, half) == 1 && window.Block(0, half) && window.Serial(0) != before, "a piece remapped inside a block did not retire the block and import it again whole: " + blockText(window, 0, half)); +} + +void importBlockFailureTests(const Context& context, const BdaTestAccess& access) { + PieceWindow window(context); + constexpr std::size_t MiB = std::size_t{1} << 20u; + constexpr auto half = PieceWindow::Window / 2; + window.AddPieces(0, PieceWindow::Window); + access.limitImports(half); + window.Build(); + Require(window.Imports(0, PieceWindow::Window) == 2 && window.Block(0, half) && window.Block(half, half), "a window whose 8 MiB import failed did not step down to two 4 MiB blocks: " + blockText(window, 0, PieceWindow::Window)); + const auto attempts = access.allocationAttempts(); + window.Build(); + Require(access.allocationAttempts() == attempts && window.Imports(0, PieceWindow::Window) == 2, "an unchanged failed block was tried again"); + access.limitImports(std::nullopt); + constexpr auto remapped = 7 * MiB; + window.Remove(remapped); + window.Add(remapped, PieceWindow::Piece); + window.Build(); + Require(window.Imports(0, PieceWindow::Window) == 1 && window.Block(0, PieceWindow::Window), "a block that failed was not tried again after one of its ranges was mapped again: " + blockText(window, 0, PieceWindow::Window)); +} + +void importBlockBudgetTests(const Context& context) { + PieceWindow window(context); + constexpr std::size_t MiB = std::size_t{1} << 20u; + constexpr auto half = PieceWindow::Window / 2; + window.AddPieces(0, half); + SetHostImportBudget(HostImportLiveBytes() + 3 * MiB); + window.Build(); + Require(window.Imports(0, half) == 2 && window.Block(0, 2 * MiB) && window.Block(2 * MiB, MiB) && !window.Covered(3 * MiB, half), "a 4 MiB block over the budget did not step down to a 2 MiB and a 1 MiB block: " + blockText(window, 0, half)); + const auto low = window.Serial(0); + const auto high = window.Serial(2 * MiB); + window.Add(6 * MiB, PieceWindow::Piece); + window.Build(); + Require(window.Serial(0) == low && window.Serial(2 * MiB) == high && window.Imports(0, PieceWindow::Window) == 2, "blocks stepped down for the budget were imported again after an unrelated range was mapped: " + blockText(window, 0, PieceWindow::Window)); +} + +void importRefusalMemoryTests(const Context& context) { + constexpr std::size_t piece = 4096; + constexpr std::size_t count = 12000; + constexpr std::size_t fitting = count * 6 / 10; + constexpr std::size_t refused = count - fitting; + auto importing = context; + importing.hostImportAlignment = piece; + auto* block = static_cast(::operator new(2 * count * piece, std::align_val_t{piece})); + auto* other = static_cast(::operator new(piece, std::align_val_t{piece})); + const auto at = [&](std::size_t index) { return block + 2 * index * piece; }; + SetHostImportWindow(std::size_t{8} << 20u); + ClearHostImports(importing.device); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Add(at(index), piece, true, true, true); + } + const auto budget = HostImportLiveBytes() + fitting * piece; + SetHostImportBudget(budget); + bool added = false; + const auto rebuild = [&] { + { + GuestAllocations::Mutation mutation; + if (added) mutation.Remove(other); + else mutation.Add(other, piece, true, true, false); + } + added = !added; + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + }; + const auto counted = [](ImportRequester requester) { return HostImportCounters().requesters[static_cast(requester)]; }; + const auto tries = [](const HostImportRequesterCounts& requested) { return requested.refused - requested.remembered; }; + auto before = counted(ImportRequester::Build); + rebuild(); + auto after = counted(ImportRequester::Build); + Require(after.made - before.made == fitting && tries(after) - tries(before) == refused, "a registry of 12000 ranges over a budget for 7200 of them was not imported and refused as expected: " + std::to_string(after.made - before.made) + " made, " + std::to_string(tries(after) - tries(before)) + " refused"); + for (int round = 1; round <= 4; ++round) { + before = after; + rebuild(); + after = counted(ImportRequester::Build); + Require(tries(after) == tries(before), "rebuild " + std::to_string(round) + " tried " + std::to_string(tries(after) - tries(before)) + " refused ranges again with the budget unchanged"); + Require(after.refused - before.refused == refused && after.remembered - before.remembered == refused && after.made == before.made, "rebuild " + std::to_string(round) + " did not count its remembered refusals as refusals"); + } + { + GuestAllocations::Mutation mutation; + mutation.Remove(at(0)); + } + before = after; + rebuild(); + after = counted(ImportRequester::Build); + Require(after.made - before.made == 1 && HostImportCovers(importing, reinterpret_cast(at(fitting)), piece), "the budget a retired import freed was not used for a remembered refusal: " + std::to_string(after.made - before.made) + " made"); + SetHostImportBudget(budget + piece); + before = after; + rebuild(); + after = counted(ImportRequester::Build); + Require(after.made - before.made == 1 && HostImportCovers(importing, reinterpret_cast(at(fitting + 1)), piece), "a larger budget did not end the remembered refusals: " + std::to_string(after.made - before.made) + " made"); + const auto lookups = counted(ImportRequester::Access); + static_cast(HostImportFor(importing, reinterpret_cast(at(count - 1)), 16)); + const auto looked = counted(ImportRequester::Access); + Require(looked.remembered == lookups.remembered && looked.made + looked.refused + looked.failed == lookups.made + lookups.refused + lookups.failed + 1, "a lookup that may evict was answered from the build's remembered refusals"); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 1; index < count; ++index) mutation.Remove(at(index)); + if (added) mutation.Remove(other); + } + ClearHostImports(importing.device); + SetHostImportBudget(0); + SetHostImportWindow(0); + ::operator delete(other, std::align_val_t{piece}); + ::operator delete(block, std::align_val_t{piece}); +} + +void evictingRefusalMemoryTests(const Context& context) { + constexpr std::size_t piece = 65536; + constexpr std::size_t filled = 32; + constexpr std::size_t count = filled + 3; + auto importing = context; + importing.hostImportAlignment = piece; + auto* block = static_cast(::operator new(2 * count * piece, std::align_val_t{piece})); + const auto at = [&](std::size_t index) { return block + 2 * index * piece; }; + const auto address = [&](std::size_t index) { return reinterpret_cast(at(index)); }; + ClearHostImports(importing.device); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Add(at(index), piece, true, true, true); + } + const auto budget = HostImportLiveBytes() + filled * piece; + SetHostImportBudget(budget); + for (std::size_t index = 0; index < filled; ++index) Require(HostImportFor(importing, address(index), 16) != nullptr, "a piece inside the import budget was not imported"); + Require(HostImportLiveBytes() == budget, "the pieces did not fill the import budget"); + const auto counted = [] { return HostImportCounters().requesters[static_cast(ImportRequester::Access)]; }; + auto before = counted(); + for (int request = 0; request < 8; ++request) Require(HostImportFor(importing, address(filled), 16) == nullptr, "a piece over a full budget with nothing evictable was imported"); + auto after = counted(); + Require(after.refused - before.refused == 8 && after.remembered - before.remembered == 7, "8 evicting requests for one refused piece at a full budget with nothing evictable scanned the imports " + std::to_string(8 - (after.remembered - before.remembered)) + " times, not once"); + SetHostImportBudget(budget - piece); + before = counted(); + Require(HostImportFor(importing, address(filled), 16) == nullptr && counted().remembered == before.remembered + 1, "a remembered evicting refusal ended when the budget was lowered"); + SetHostImportBudget(budget); + { + GuestAllocations::Mutation mutation; + mutation.Remove(at(0)); + } + before = counted(); + Require(HostImportFor(importing, address(filled), 16) != nullptr && counted().remembered == before.remembered, "an evicting request was answered from its remembered refusal after an import retired"); + static_cast(HostImportFor(importing, address(filled + 1), 16)); + before = counted(); + Require(HostImportFor(importing, address(filled + 1), 16) == nullptr && counted().remembered == before.remembered + 1, "a second evicting request for a refused piece was not answered from memory"); + SetHostImportBudget(budget + piece); + before = counted(); + Require(HostImportFor(importing, address(filled + 1), 16) != nullptr && counted().remembered == before.remembered, "an evicting request was answered from its remembered refusal after the budget was raised"); + static_cast(HostImportFor(importing, address(filled + 2), 16)); + { + GuestBufferMemory upload(importing); + upload.Upload(false); + upload.WriteBack(); + } + before = counted(); + Require(HostImportFor(importing, address(filled + 2), 16) == nullptr && counted().remembered == before.remembered + 1, "a remembered evicting refusal ended when a build raised the floor while no import could age out of its protection yet"); + for (int use = 0; use < 200; ++use) Require(HostImportFor(importing, address(filled + 1), 16) != nullptr, "a live import was not found"); + before = counted(); + Require(HostImportFor(importing, address(filled + 2), 16) != nullptr && counted().remembered == before.remembered, "an evicting request was answered from its remembered refusal after the imports aged out of their protection"); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 1; index < count; ++index) mutation.Remove(at(index)); + } + ClearHostImports(importing.device); + SetHostImportBudget(0); + ::operator delete(block, std::align_val_t{piece}); +} + +void evictingRefusalAgingTests(const Context& context) { + constexpr std::size_t piece = 65536; + constexpr std::size_t filled = 16; + auto importing = context; + importing.hostImportAlignment = piece; + auto* block = static_cast(::operator new(2 * (filled + 1) * piece, std::align_val_t{piece})); + const auto at = [&](std::size_t index) { return block + 2 * index * piece; }; + const auto address = [&](std::size_t index) { return reinterpret_cast(at(index)); }; + ClearHostImports(importing.device); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index <= filled; ++index) mutation.Add(at(index), piece, true, true, true); + } + SetHostImportBudget(HostImportLiveBytes() + filled * piece); + for (std::size_t index = 0; index < filled; ++index) Require(HostImportFor(importing, address(index), 16) != nullptr, "a piece inside the import budget was not imported"); + { + GuestBufferMemory upload(importing); + upload.Upload(false); + upload.WriteBack(); + } + const auto counted = [] { return HostImportCounters().requesters[static_cast(ImportRequester::Access)]; }; + static_cast(HostImportFor(importing, address(filled), 16)); + auto before = counted(); + Require(HostImportFor(importing, address(filled), 16) == nullptr && counted().remembered == before.remembered + 1, "a refusal while the imports were recent but no longer held by a build was not remembered"); + for (int use = 0; use < 200; ++use) Require(HostImportFor(importing, address(filled - 1), 16) != nullptr, "a live import was not found"); + before = counted(); + Require(HostImportFor(importing, address(filled), 16) != nullptr && counted().remembered == before.remembered, "an evicting request was answered from its remembered refusal after imports no build held aged out of their protection"); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index <= filled; ++index) mutation.Remove(at(index)); + } + ClearHostImports(importing.device); + SetHostImportBudget(0); + ::operator delete(block, std::align_val_t{piece}); +} + +void evictingRefusalRangeTests(const Context& context) { + PieceWindow window(context); + constexpr auto piece = PieceWindow::Piece; + constexpr auto fillers = PieceWindow::Window / 2; + window.Add(0, 2 * piece); + window.Add(2 * piece, piece); + window.Add(3 * piece, piece); + for (std::size_t filler = 0; filler < 4; ++filler) { + window.Add(fillers + 2 * filler * piece, piece); + Require(HostImportFor(window.importing, window.Address(fillers + 2 * filler * piece), 16) != nullptr, "a filler piece inside the import budget was not imported"); + } + SetHostImportBudget(HostImportLiveBytes() + piece); + Require(HostImportFor(window.importing, window.Address(0), 16) == nullptr, "a 128 KiB range in a 256 KiB block over the budget was imported"); + Require(HostImportFor(window.importing, window.Address(2 * piece), 16) != nullptr && window.Block(2 * piece, piece), "a 64 KiB range of the same 256 KiB block was answered from the 128 KiB range's refusal instead of stepping down to its own import"); +} + +void evictingRefusalBlockRangesTests(const Context& context) { + PieceWindow window(context); + constexpr auto piece = PieceWindow::Piece; + constexpr auto fillers = PieceWindow::Window / 2; + window.AddPieces(0, 4 * piece); + for (std::size_t filler = 0; filler < 4; ++filler) { + window.Add(fillers + 2 * filler * piece, piece); + Require(HostImportFor(window.importing, window.Address(fillers + 2 * filler * piece), 16) != nullptr, "a filler piece inside the import budget was not imported"); + } + SetHostImportBudget(HostImportLiveBytes()); + const auto counted = [] { return HostImportCounters().requesters[static_cast(ImportRequester::Access)]; }; + const auto before = counted(); + for (int request = 0; request < 8; ++request) Require(HostImportFor(window.importing, window.Address(static_cast(request % 2) * piece), 16) == nullptr, "a 64 KiB range of a block over a full budget with nothing evictable was imported"); + const auto after = counted(); + Require(after.refused - before.refused == 8 && after.remembered - before.remembered == 7, "8 evicting requests alternating between two 64 KiB ranges of one refused 256 KiB block scanned the imports " + std::to_string(8 - (after.remembered - before.remembered)) + " times, not once"); + SetHostImportBudget(HostImportLiveBytes() + piece); + Require(HostImportFor(window.importing, window.Address(piece), 16) != nullptr && window.Block(piece, piece), "a 64 KiB range of the block was answered from the full budget's refusal instead of stepping down to its own import once the budget had room for it"); +} + +void evictingRefusalJoinTests(const Context& context) { + PieceWindow window(context); + constexpr auto piece = PieceWindow::Piece; + constexpr auto fillers = PieceWindow::Window / 2; + window.AddPieces(0, 3 * piece); + for (std::size_t filler = 0; filler < 4; ++filler) { + window.Add(fillers + 2 * filler * piece, piece); + Require(HostImportFor(window.importing, window.Address(fillers + 2 * filler * piece), 16) != nullptr, "a filler piece inside the import budget was not imported"); + } + SetHostImportBudget(HostImportLiveBytes()); + const auto upload = [&] { + GuestBufferMemory plain(window.importing); + plain.AddReadable(window.Address(0), 3 * piece); + plain.Upload(false); + plain.WriteBack(); + }; + const auto counted = [] { return HostImportCounters().requesters[static_cast(ImportRequester::Join)]; }; + const auto before = counted(); + for (int request = 0; request < 8; ++request) upload(); + const auto after = counted(); + Require(!HostImportCovers(window.importing, window.Address(0), 3 * piece), "a join of three 64 KiB ranges over a full budget with nothing evictable was imported"); + Require(after.refused - before.refused == 8 && after.remembered - before.remembered == 7, "8 uploads bound across three refused ranges scanned the imports for their join " + std::to_string(8 - (after.remembered - before.remembered)) + " times, not once"); + SetHostImportBudget(HostImportLiveBytes() + 3 * piece); + upload(); + Require(HostImportCovers(window.importing, window.Address(0), 3 * piece) && counted().made == after.made + 1, "the join was answered from the full budget's refusal after the budget grew"); +} + +void evictingRefusalStepDownTests(const Context& context) { + PieceWindow window(context); + constexpr auto piece = PieceWindow::Piece; + constexpr auto fillers = PieceWindow::Window / 2; + window.AddPieces(0, 4 * piece); + for (std::size_t filler = 0; filler < 4; ++filler) { + window.Add(fillers + 2 * filler * piece, piece); + Require(HostImportFor(window.importing, window.Address(fillers + 2 * filler * piece), 16) != nullptr, "a filler piece inside the import budget was not imported"); + } + SetHostImportBudget(HostImportLiveBytes() + 2 * piece); + { + GuestBufferMemory leased(window.importing); + leased.AcquireRegistered(); + leased.Upload(true); + leased.WriteBack(); + } + Require(window.Block(0, 2 * piece) && window.Serial(2 * piece) == 0, "the address-based build did not step down from the refused 256 KiB block to the 128 KiB block that fits"); + { + GuestBufferMemory upload(window.importing); + upload.Upload(false); + upload.WriteBack(); + } + const auto counted = [] { return HostImportCounters().requesters[static_cast(ImportRequester::Access)]; }; + const auto before = counted(); + for (int request = 0; request < 8; ++request) Require(HostImportFor(window.importing, window.Address(2 * piece), 16) == nullptr, "the rest of a block over a full budget with nothing evictable was imported"); + const auto after = counted(); + const auto scans = (after.refused - before.refused) - (after.remembered - before.remembered); + Require(after.refused - before.refused == 16 && scans == 1, "8 evicting requests that step down from a refused block scanned the imports " + std::to_string(scans) + " times, not once"); + SetHostImportBudget(HostImportLiveBytes() + piece); + Require(HostImportFor(window.importing, window.Address(2 * piece), 16) != nullptr && window.Block(2 * piece, piece), "a 64 KiB range was answered from a block's refusal remembered before the budget grew instead of stepping down to its own import"); +} + +void evictingRefusalWindowTests(const Context& context) { + PieceWindow window(context); + constexpr auto piece = PieceWindow::Piece; + constexpr auto fillers = PieceWindow::Window / 2; + SetHostImportWindow(0); + window.Add(0, piece); + window.Add(piece, piece); + for (std::size_t filler = 0; filler < 4; ++filler) { + window.Add(fillers + 2 * filler * piece, piece); + Require(HostImportFor(window.importing, window.Address(fillers + 2 * filler * piece), 16) != nullptr, "a filler piece inside the import budget was not imported"); + } + SetHostImportBudget(HostImportLiveBytes() + piece); + Require(HostImportFor(window.importing, window.Address(0), 16) == nullptr, "two adjacent 64 KiB ranges over the budget were imported without an import window"); + SetHostImportWindow(PieceWindow::Window); + Require(HostImportFor(window.importing, window.Address(0), 16) != nullptr && window.Block(0, piece), "a 64 KiB range was answered from a refusal made without the import window instead of stepping down once the window was set"); +} + +void importDeviceBudgetTests() { + constexpr std::uint64_t MiB = std::uint64_t{1} << 20u; + constexpr std::uint64_t GiB = std::uint64_t{1} << 30u; + VkPhysicalDeviceMemoryProperties memory{}; + memory.memoryHeapCount = 2; + memory.memoryHeaps[0] = {8 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + memory.memoryHeaps[1] = {16 * GiB, 0}; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + const auto text = [](std::uint64_t bytes) { return std::to_string(bytes >> 20u) + " MiB"; }; + Require(HostImportBudgetFor(memory, nullptr, 6 * GiB, 0) == 6 * GiB, "an import budget without a memory budget report is not the configured one"); + Require(HostImportBudgetFor(memory, &reported, 6 * GiB, 0) == 6 * GiB, "a zero reported budget of the host heap did not leave the configured import budget"); + reported.heapBudget[1] = 12 * GiB; + reported.heapUsage[1] = 5 * GiB; + const auto granted = HostImportBudgetFor(memory, &reported, 10 * GiB, 2 * GiB); + Require(granted == 7 * GiB + 512 * MiB, "a 12 GiB host heap budget using 3 GiB besides 2 GiB of imports did not give 7.5 GiB to imports: " + text(granted)); + Require(HostImportBudgetFor(memory, &reported, 6 * GiB, 2 * GiB) == 6 * GiB, "a reported host heap budget raised the configured import budget"); + reported.heapUsage[1] = 5 * GiB + 10 * MiB; + const auto rounded = HostImportBudgetFor(memory, &reported, 10 * GiB, 2 * GiB); + Require(rounded == 7 * GiB + 448 * MiB, "the import budget from a reported heap was not rounded down to 64 MiB: " + text(rounded)); + reported.heapUsage[1] = 5 * GiB; + const auto uncounted = HostImportBudgetFor(memory, &reported, 64 * GiB, 8 * GiB); + Require(uncounted == 10 * GiB + 512 * MiB, "imports the heap usage does not count were taken from other usage: " + text(uncounted)); + reported.heapBudget[1] = 4 * GiB; + reported.heapUsage[1] = 6 * GiB; + Require(HostImportBudgetFor(memory, &reported, 6 * GiB, GiB) == 0, "a host heap used past its reported budget left room for imports"); + reported.heapBudget[0] = 1 * GiB; + reported.heapBudget[1] = 0; + Require(HostImportBudgetFor(memory, &reported, 6 * GiB, 0) == 6 * GiB, "the device-local heap's budget was taken for the host heap's"); + constexpr auto unreported = std::numeric_limits::max(); + Require(HostImportBudgetWith(6400 * MiB, true, 4480 * MiB, false, 0) == 6400 * MiB, "a host heap report lowered a set import budget"); + Require(HostImportBudgetWith(6400 * MiB, true, 4480 * MiB, true, 0) == 6400 * MiB, "a host heap report lowered a set import budget after a vkAllocateMemory failure"); + Require(HostImportBudgetWith(6400 * MiB, true, 4480 * MiB, true, 3752 * MiB) == 3752 * MiB, "the ceiling after a vkAllocateMemory failure did not lower a set import budget"); + Require(HostImportBudgetWith(5888 * MiB, false, 4480 * MiB, false, 0) == 5888 * MiB, "a host heap report lowered the default import budget before any vkAllocateMemory failure"); + Require(HostImportBudgetWith(5888 * MiB, false, 4480 * MiB, true, 0) == 4480 * MiB, "the default import budget did not take a lower host heap report after a vkAllocateMemory failure"); + Require(HostImportBudgetWith(5888 * MiB, false, 8 * GiB, true, 0) == 5888 * MiB, "a higher host heap report raised the default import budget"); + Require(HostImportBudgetWith(5888 * MiB, false, unreported, true, 0) == 5888 * MiB, "a device without a memory budget report changed the default import budget"); + Require(HostImportBudgetWith(5888 * MiB, false, 4480 * MiB, true, 3000 * MiB) == 3000 * MiB, "the ceiling below a host heap report did not hold the default import budget"); + Require(HostImportBudgetWith(5888 * MiB, false, 0, true, 0) != 0, "a host heap report that leaves imports no room made the default import budget 0, which the refusal memory reads as no budget"); +} + +void importCeilingTests(const Context& context, const BdaTestAccess& access, bool nearBudget) { + constexpr std::size_t piece = 65536; + constexpr std::size_t count = 24; + auto* block = static_cast(::operator new(2 * count * piece, std::align_val_t{piece})); + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(0); + ClearHostImports(importing.device); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Add(block + 2 * index * piece, piece, true, true, true); + } + std::size_t next = 0; + const auto lookup = [&] { return HostImportFor(importing, reinterpret_cast(block + 2 * next++ * piece), 16) != nullptr; }; + const auto counted = [] { return HostImportCounters().requesters[static_cast(ImportRequester::Access)]; }; + const auto outcome = [](const HostImportRequesterCounts& before, const HostImportRequesterCounts& after) { return std::to_string(after.failed - before.failed) + " failed, " + std::to_string(after.refused - before.refused) + " refused"; }; + if (nearBudget) { + SetHostImportBudget(HostImportLiveBytes() + 2 * piece); + Require(lookup(), "the ceiling test's first import was not made"); + access.limitImports(0); + const auto before = counted(); + for (int attempt = 0; attempt < 3; ++attempt) static_cast(lookup()); + const auto after = counted(); + access.limitImports(std::nullopt); + Require(after.failed - before.failed == 1 && after.refused - before.refused == 2, "imports after a vkAllocateMemory failure within an eighth of the budget were not one failure and then refusals: " + outcome(before, after)); + } else { + SetHostImportBudget(64 * piece); + Require(lookup(), "the ceiling test's first import was not made"); + access.limitImports(0); + auto before = counted(); + Require(!lookup(), "an import past the mock's import limit was made"); + access.limitImports(std::nullopt); + Require(lookup() && counted().failed == before.failed + 1, "one vkAllocateMemory failure far below the import budget lowered it"); + access.limitImports(0); + before = counted(); + for (int attempt = 0; attempt < 5; ++attempt) static_cast(lookup()); + const auto after = counted(); + access.limitImports(std::nullopt); + Require(after.failed - before.failed == 3 && after.refused - before.refused == 2, "five imports after vkAllocateMemory failures far below the budget were not three failures and then refusals: " + outcome(before, after)); + } + Require(!lookup(), "an import was made over the lowered ceiling"); + SetHostImportBudget(0); + Require(lookup(), "a new import budget did not lift the ceiling"); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Remove(block + 2 * index * piece); + } + ClearHostImports(importing.device); + ::operator delete(block, std::align_val_t{piece}); +} + +std::uint64_t reportedHostHeapBudget = 0; +std::uint64_t reportedHostHeapUsage = 0; + +void VKAPI_PTR reportHostHeap(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { + for (auto* next = static_cast(properties->pNext); next != nullptr; next = next->pNext) { + if (next->sType != VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT) continue; + auto* reported = reinterpret_cast(next); + reported->heapBudget[1] = reportedHostHeapBudget; + reported->heapUsage[1] = reportedHostHeapUsage; + } +} + +void importReportFloorTests(const Context& context, const BdaTestAccess& access, bool setBudget) { + constexpr std::size_t piece = 65536; + constexpr std::size_t count = 24; + constexpr std::uint64_t GiB = std::uint64_t{1} << 30u; + static std::uintptr_t physical = 0x1000; + auto* block = static_cast(::operator new(2 * count * piece, std::align_val_t{piece})); + auto importing = context; + importing.hostImportAlignment = piece; + importing.memory.memoryHeapCount = 2; + importing.memory.memoryHeaps[0] = {8 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + importing.memory.memoryHeaps[1] = {16 * GiB, 0}; + importing.memoryProperties2 = reportHostHeap; + importing.physical = reinterpret_cast(physical += 0x10); + reportedHostHeapBudget = 8 * (std::uint64_t{1} << 20u); + reportedHostHeapUsage = reportedHostHeapBudget; + SetHostImportWindow(0); + SetHostImportBudget(setBudget ? 64 * piece : 0); + ClearHostImports(importing.device); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Add(block + 2 * index * piece, piece, true, true, true); + } + std::size_t next = 0; + const auto lookup = [&] { return HostImportFor(importing, reinterpret_cast(block + 2 * next++ * piece), 16) != nullptr; }; + const std::string budget = setBudget ? "a set import budget" : "the default import budget"; + for (int made = 0; made < 3; ++made) Require(lookup(), budget + " was lowered below " + std::to_string(made + 1) + " imports by a host heap report that leaves imports no room, before any vkAllocateMemory failure"); + access.limitImports(0); + Require(!lookup(), "an import past the mock's import limit was made"); + access.limitImports(std::nullopt); + if (setBudget) Require(lookup(), "a set import budget took the host heap report's limit after a vkAllocateMemory failure far below it"); + else Require(!lookup(), "the default import budget did not take the host heap report's limit after a vkAllocateMemory failure"); + SetHostImportBudget(0); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Remove(block + 2 * index * piece); + } + ClearHostImports(importing.device); + ::operator delete(block, std::align_val_t{piece}); +} + +void importReportRefusalMemoryTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t piece = 65536; + constexpr std::size_t count = 24; + constexpr std::uint64_t GiB = std::uint64_t{1} << 30u; + static std::uintptr_t physical = 0x800000; + auto* block = static_cast(::operator new(2 * count * piece, std::align_val_t{piece})); + auto* other = static_cast(::operator new(piece, std::align_val_t{piece})); + auto importing = context; + importing.hostImportAlignment = piece; + importing.memory.memoryHeapCount = 2; + importing.memory.memoryHeaps[0] = {8 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + importing.memory.memoryHeaps[1] = {16 * GiB, 0}; + importing.memoryProperties2 = reportHostHeap; + importing.physical = reinterpret_cast(physical += 0x10); + reportedHostHeapBudget = 8 * (std::uint64_t{1} << 20u); + reportedHostHeapUsage = reportedHostHeapBudget; + SetHostImportWindow(0); + SetHostImportBudget(0); + ClearHostImports(importing.device); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Add(block + 2 * index * piece, piece, true, true, true); + } + access.limitImports(0); + Require(HostImportFor(importing, reinterpret_cast(block), 16) == nullptr, "an import past the mock's import limit was made"); + access.limitImports(std::nullopt); + bool added = false; + const auto rebuild = [&] { + { + GuestAllocations::Mutation mutation; + if (added) mutation.Remove(other); + else mutation.Add(other, piece, true, true, false); + } + added = !added; + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + }; + const auto counted = [] { return HostImportCounters().requesters[static_cast(ImportRequester::Build)]; }; + const auto tries = [](const HostImportRequesterCounts& requested) { return requested.refused - requested.remembered; }; + auto before = counted(); + rebuild(); + auto after = counted(); + Require(after.made == before.made && tries(after) > tries(before), "an address-based build under a host heap report that leaves imports no room imported or refused nothing: " + std::to_string(after.made - before.made) + " made, " + std::to_string(tries(after) - tries(before)) + " refused"); + before = after; + rebuild(); + after = counted(); + Require(tries(after) == tries(before) && after.remembered > before.remembered, "a second address-based build under a host heap report that leaves imports no room tried " + std::to_string(tries(after) - tries(before)) + " refused ranges again instead of answering them from the refusal memory"); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Remove(block + 2 * index * piece); + if (added) mutation.Remove(other); + } + ClearHostImports(importing.device); + ::operator delete(other, std::align_val_t{piece}); + ::operator delete(block, std::align_val_t{piece}); +} + void spaceReadAcrossImportsTests(const Context& context, const BdaTestAccess& access) { constexpr std::size_t piece = 65536; constexpr std::size_t window = 2 * piece; @@ -557,6 +1906,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}; } @@ -733,6 +2227,95 @@ void importWatchTests() { #endif } +void sparseImportedHeapTests(const Context& context, const BdaTestAccess& access) { +#ifndef _WIN32 + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::cout << "write watch unavailable: sparse imported heaps not tested\n"; + return; + } + constexpr std::size_t bytes = 65536; + void* raw = mmap(nullptr, 4 * bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot reserve the sparse imported heap fixture"); + const auto base = (reinterpret_cast(raw) + bytes - 1) & ~(bytes - 1); + auto* guest = reinterpret_cast(base); + Require(mprotect(guest, bytes, PROT_READ | PROT_WRITE) == 0, "cannot commit the sparse imported heap prefix"); + std::memset(guest, 0x57, bytes); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(guest, bytes); + auto importing = context; + importing.hostImportAlignment = bytes; + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest, bytes, true, true, true); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + const auto* imported = HostImportFor(importing, base, bytes); + Require(imported != nullptr, "the sparse heap prefix was not imported"); + const auto before = MirrorCounters(); + for (unsigned index = 0; index < 3; ++index) { + { + GuestBufferMemory described(importing); + described.AddReadable(base, 2 * bytes); + described.Upload(false); + std::uint32_t adjustment = 0; + const auto view = described.Descriptor(base, 2 * bytes, adjustment); + Require(view.buffer == imported->buffer && view.range == bytes && adjustment == 0, "the sparse descriptor did not bind its imported prefix"); + Require(described.CopiedBytes() == 0 && MirrorCounters().rebuilds == before.rebuilds, "the sparse imported descriptor rebuilt or copied its heap mirror"); + reject([&] { static_cast(described.Descriptor(base + bytes + 16, 4, adjustment)); }, "starts past its copy"); + described.WriteBack(); + } + { + GuestBufferMemory addressed(importing); + addressed.AcquireRegistered(); + addressed.Upload(true); + Require(MirrorCounters().rebuilds == before.rebuilds && MirrorCounters().heapMirrors == before.heapMirrors, "alternating descriptor and address builds rebuilt the imported heap mirror"); + addressed.WriteBack(); + } + } + { + auto misaligned = importing; + misaligned.limits.minStorageBufferOffsetAlignment = 256; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Recorder recorder(misaligned); + recorder.Activate(); + GuestBufferMemory memory(misaligned); + memory.AddReadable(base + 16, 2 * bytes - 16); + memory.Upload(false); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(base + 16, 2 * bytes - 16, adjustment); + Require(view.buffer != imported->buffer, "a misaligned sparse descriptor bound its import in place"); + Require(view.range != 0 && view.range <= bytes && memory.CopiedBytes() <= bytes && access.bytes(view.buffer)[view.offset + adjustment] == std::byte{0x57}, "a misaligned sparse descriptor does not serve its committed prefix alone"); + memory.WriteBack(); + } + { + GuestBufferMemory written(importing); + written.AddWritable(base, 2 * bytes); + written.Upload(false); + std::uint32_t adjustment = 0; + const auto view = written.Descriptor(base, 2 * bytes, adjustment); + Require(view.buffer != imported->buffer && view.range == bytes && MirrorCounters().rebuilds == before.rebuilds + 1, "a sparse writable descriptor bound a shortened import"); + access.bytes(view.buffer)[view.offset] = std::byte{0x68}; + guest[8] = 0x79; + written.WriteBack(); + Require(guest[0] == 0x68 && guest[8] == 0x79, "the sparse writable fallback lost a GPU store or rolled back a CPU store"); + } + mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + Require(HostImportFor(importing, base, bytes) == nullptr, "the sparse heap import outlived its range"); + { + GuestBufferMemory sweep(importing); + sweep.AcquireRegistered(); + sweep.Upload(true); + sweep.WriteBack(); + } + Require(MirrorCounters().heapMirrors == before.heapMirrors, "the sparse imported heap fixture leaked its mirror"); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(guest, bytes); + munmap(raw, 4 * bytes); + std::cout << "Sparse imported heap tests passed\n"; +#else + static_cast(context); + static_cast(access); +#endif +} + void heapMirrorTests(const Context& context, const BdaTestAccess& access) { if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { std::cout << "guest arena unavailable or not write-watched: heap mirrors not tested\n"; @@ -1152,6 +2735,57 @@ void sparseHeapTests(const Context& context, const BdaTestAccess& access) { releaseSparse(block, reserved); } +void spaceCommittedPagesTests(const Context& context, const BdaTestAccess& access) { + if (!AddressSpaceCounters().enabled) return; + constexpr std::size_t unit = 65536; + constexpr std::size_t reserved = 3 * unit; + auto* block = reserveSparse(reserved, unit); + std::memset(block, 0x31, unit); + const auto base = reinterpret_cast(block); + auto copying = context; + copying.hostImportAlignment = 0; + { + GuestAllocations::Mutation mutation; + mutation.Add(block, reserved, true, false, true); + } + const auto build = [&](std::byte expected, bool hit, const char* step) { + const auto before = AddressSpaceCounters(); + GuestBufferMemory leased(copying); + leased.AcquireRegistered(); + leased.AddReadable(base, 16); + leased.AddReadable(base + 2 * unit, 16); + leased.Upload(true); + std::uint32_t adjustment = 0; + const auto head = leased.Descriptor(base, 16, adjustment); + const auto headBytes = access.bytes(head.buffer); + Require(head.offset + adjustment < headBytes.size() && headBytes[head.offset + adjustment] == std::byte{0x31}, std::string("the committed head of a registered sparse range was not read: ") + step); + const auto tail = leased.Descriptor(base + 2 * unit, 16, adjustment); + const auto tailBytes = access.bytes(tail.buffer); + Require(tail.offset + adjustment < tailBytes.size() && tailBytes[tail.offset + adjustment] == expected, std::string("a cached address space served another commit state of its pages: ") + step); + leased.WriteBack(); + const auto after = AddressSpaceCounters(); + if (hit) Require(after.hits == before.hits + 1, std::string("the unchanged registry did not hit the cached address space: ") + step); + }; + build(std::byte{}, false, "first build"); + build(std::byte{}, true, "hit before the commit"); + { + GuestAllocations::Mutation mutation; +#ifdef _WIN32 + Require(VirtualAlloc(block + 2 * unit, unit, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the sparse range tail"); +#else + Require(mprotect(block + 2 * unit, unit, PROT_READ | PROT_WRITE) == 0, "cannot commit the sparse range tail"); +#endif + std::memset(block + 2 * unit, 0x72, unit); + } + build(std::byte{0x72}, false, "first build after the commit"); + build(std::byte{0x72}, true, "hit after the commit"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + releaseSparse(block, reserved); +} + bool RunSparseBufferHoleTests(const Context& context, const BdaTestAccess& access) { constexpr std::size_t unit = 65536; constexpr std::size_t copied = 3 * unit; @@ -1179,7 +2813,7 @@ bool RunSparseBufferHoleTests(const Context& context, const BdaTestAccess& acces std::memset(reservation.data + 2 * unit, 0x72, unit); VkBuffer seeded = VK_NULL_HANDLE; { - Buffer seed(context, copied, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer seed(context, copied, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); std::fill(seed.Bytes().begin(), seed.Bytes().end(), std::byte{0xa5}); seeded = seed.Handle(); } @@ -1215,6 +2849,280 @@ bool RunSparseBufferHoleTests(const Context& context, const BdaTestAccess& acces return passed; } +void RunHeapMirrorUnwatchTests(const Context& context, const BdaTestAccess& access, bool throughImport) { + constexpr std::size_t block = 65536; + constexpr std::size_t heapBytes = 32 * block; + constexpr std::size_t idleBytes = 16 * block; + constexpr std::size_t busyBytes = block; +#ifdef _WIN32 + if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { + std::cout << "guest arena unavailable or not write-watched: heap mirrors leaving the write watch not tested\n"; + return; + } +#else + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::cout << "write watch unavailable: heap mirrors leaving the write watch not tested\n"; + return; + } +#endif + struct Piece { + void* pointer = nullptr; + void* mapping = nullptr; + std::size_t allocated = 0; + std::size_t bytes = 0; + }; + std::array pieces{}; + const std::array, 3> sizes{{{heapBytes + block, heapBytes}, {heapBytes + block, idleBytes}, {busyBytes + block, busyBytes}}}; + for (std::size_t index = 0; index < pieces.size(); ++index) { + auto& piece = pieces[index]; + piece.allocated = sizes[index].first; + piece.bytes = sizes[index].second; +#ifdef _WIN32 + piece.pointer = GuestArena::GuestArenaAllocate_nid_postfix(piece.allocated, block); + Require(piece.pointer != nullptr, "cannot allocate the heap mirror ranges in the guest arena"); + GuestArena::GuestArenaCommit_nid_postfix(piece.pointer, piece.allocated, PAGE_READWRITE, piece.allocated); + piece.mapping = piece.pointer; +#else + piece.mapping = mmap(nullptr, piece.allocated + block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(piece.mapping != MAP_FAILED, "cannot map the heap mirror ranges"); + piece.pointer = reinterpret_cast((reinterpret_cast(piece.mapping) + block - 1) & ~std::uintptr_t{block - 1}); +#endif + std::memset(piece.pointer, 0x5a, piece.allocated); + } + if (pieces[1].pointer < pieces[0].pointer) { + std::swap(pieces[0].pointer, pieces[1].pointer); + std::swap(pieces[0].mapping, pieces[1].mapping); + } + const auto heap = reinterpret_cast(pieces[0].pointer); + const auto idle = reinterpret_cast(pieces[1].pointer); + const auto busy = reinterpret_cast(pieces[2].pointer); +#ifndef _WIN32 + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(pieces[0].pointer, heapBytes); +#endif + const auto registry = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + for (const auto& piece : pieces) { + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, piece.pointer, piece.bytes, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, piece.pointer); + } + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + registry(true); + auto importing = context; + importing.hostImportAlignment = block; + SetHostImportBudget(heapBytes + idleBytes + busyBytes - block); + Require(HostImportFor(importing, idle, 64) != nullptr && HostImportFor(importing, busy, 64) != nullptr, "the idle and busy ranges were not imported"); + Require(HostImportLiveBytes() == idleBytes + busyBytes && !HostImportCovers(importing, heap, heapBytes), "the imports before the first build are not the idle and busy ranges"); + Require(AgcDriver::GuestMemory::Watched(heap, heapBytes), "the heap is not write-watched"); + const auto before = MirrorCounters(); + { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.Upload(true); + Require(MirrorCounters().heapMirrors == before.heapMirrors + 1 && !HostImportCovers(importing, heap, heapBytes), "the first build did not mirror the heap"); + leased.WriteBack(); + } + if (throughImport) { + const auto decided = PrepareImportWatch(importing); + SetImportWatch(importing, ImportWatch::Unwatch); + SetHostImportBudget(2 * (heapBytes + idleBytes + busyBytes)); + Require(HostImportFor(importing, heap, heapBytes) != nullptr && HostImportCovers(importing, heap, heapBytes), "the heap was not imported once the budget held it"); + SetImportWatch(importing, decided); + } else { + AgcDriver::GuestMemory::Unwatch(heap + heapBytes / 2, block); + } + Require(!AgcDriver::GuestMemory::Watched(heap, heapBytes), "the heap is still write-watched"); + std::memset(reinterpret_cast(heap + heapBytes / 2), 0x6b, block); + std::memset(reinterpret_cast(heap + block), 0x3c, block); + SetAddressCopyLimit(heapBytes / 2); + const auto refreshed = MirrorCounters(); + { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.Upload(true); + const bool imported = HostImportCovers(importing, heap, heapBytes); + Require(imported == throughImport, throughImport ? "the second build did not take the heap's import" : "the second build imported the heap past the budget"); + Require(MirrorCounters().heapMirrors == before.heapMirrors + (imported ? 0 : 1), imported ? "the heap mirror whose range the import took off the write watch was kept" : "the refused heap's mirror was dropped once its range left the write watch"); + Require(imported || MirrorCounters().blocksCopied >= refreshed.blocksCopied + heapBytes / block, "the refused heap's mirror was not refreshed in full"); + const auto ranges = leased.AddressRanges(); + const auto found = std::find_if(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == heap; }); + Require(found != ranges.end() && found->deviceAddress != 0u, "the heap is missing from the BDA table"); + Require(imported || (access.addressBytes(found->deviceAddress)[heapBytes / 2 + 3] == std::byte{0x6b} && access.addressBytes(found->deviceAddress)[block + 3] == std::byte{0x3c}), "the second build does not serve the heap's new bytes"); + leased.WriteBack(); + } + if (!throughImport) { + SetHostImportBudget(2 * (heapBytes + idleBytes + busyBytes)); + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.Upload(true); + Require(HostImportCovers(importing, heap, heapBytes) && MirrorCounters().heapMirrors == before.heapMirrors, "the heap was not imported again once the budget held it"); + leased.WriteBack(); + } + SetAddressCopyLimit(0); + SetHostImportBudget(0); + registry(false); + ClearImageMirrors(importing.device); + ClearHostImports(importing.device); + ClearSnapshotCopies(importing.device); +#ifndef _WIN32 + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(pieces[0].pointer, heapBytes); +#endif + for (const auto& piece : pieces) { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(piece.pointer, piece.allocated); + GuestArena::GuestArenaRelease_nid_postfix(piece.pointer, piece.allocated); +#else + munmap(piece.mapping, piece.allocated + block); +#endif + } + std::cout << (throughImport ? "an address-based build plans a heap again once a host import took its mirror's range off the write watch\n" : "an address-based build keeps and refreshes a refused heap's mirror once its range has left the write watch, and imports the heap once the budget holds it\n"); +} + +void RunSparseHeapMirrorUnwatchTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t block = 65536; + constexpr std::size_t reserved = 64 * block; + constexpr std::size_t committed = 32 * block; +#ifdef _WIN32 + if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { + std::cout << "guest arena unavailable or not write-watched: sparse heap mirrors leaving the write watch not tested\n"; + return; + } + auto* guest = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(reserved, block)); + Require(guest != nullptr, "cannot allocate the sparse heap in the guest arena"); + GuestArena::GuestArenaCommit_nid_postfix(guest, committed, PAGE_READWRITE, block); +#else + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::cout << "write watch unavailable: sparse heap mirrors leaving the write watch not tested\n"; + return; + } + auto* guest = reserveSparse(reserved, committed); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(guest, reserved); +#endif + std::memset(guest, 0x5a, committed); + const auto base = reinterpret_cast(guest); + const auto registry = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest, reserved, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + registry(true); + Require(AgcDriver::GuestMemory::Watched(base, committed), "the sparse heap is not write-watched"); + const auto before = MirrorCounters(); + { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + leased.Upload(true); + Require(MirrorCounters().heapMirrors == before.heapMirrors + 1, "the first build did not mirror the sparse heap"); + leased.WriteBack(); + } + AgcDriver::GuestMemory::Unwatch(base + block, block); + Require(!AgcDriver::GuestMemory::Watched(base, committed), "the sparse heap is still write-watched"); + std::memset(guest + block, 0x6b, block); + std::memset(guest + 2 * block, 0x3c, block); + SetAddressCopyLimit(committed / 2); + const auto refreshed = MirrorCounters(); + { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + leased.Upload(true); + Require(MirrorCounters().heapMirrors == before.heapMirrors + 1, "the sparse heap's mirror was dropped once its range left the write watch"); + Require(MirrorCounters().blocksCopied >= refreshed.blocksCopied + committed / block, "the sparse heap's mirror was not refreshed in full"); + const auto ranges = leased.AddressRanges(); + const auto found = std::find_if(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == base; }); + Require(found != ranges.end() && found->deviceAddress != 0u, "the sparse heap is missing from the BDA table"); + Require(access.addressBytes(found->deviceAddress)[block + 3] == std::byte{0x6b} && access.addressBytes(found->deviceAddress)[2 * block + 3] == std::byte{0x3c}, "the second build does not serve the sparse heap's new bytes"); + leased.WriteBack(); + } + SetAddressCopyLimit(0); + registry(false); + ClearImageMirrors(context.device); + ClearSnapshotCopies(context.device); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(guest, reserved); + GuestArena::GuestArenaRelease_nid_postfix(guest, reserved); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(guest, reserved); + releaseSparse(guest, reserved); +#endif + std::cout << "an address-based build keeps and refreshes a sparse heap's mirror once its range has left the write watch\n"; +} + +void RunHeapMirrorRunUnwatchTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t block = 65536; + constexpr std::size_t heapBytes = 32 * block; + constexpr std::size_t allocated = 2 * heapBytes + block; +#ifdef _WIN32 + if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { + std::cout << "guest arena unavailable or not write-watched: a heap run leaving the write watch not tested\n"; + return; + } + auto* guest = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(allocated, block)); + Require(guest != nullptr, "cannot allocate the heap run in the guest arena"); + GuestArena::GuestArenaCommit_nid_postfix(guest, allocated, PAGE_READWRITE, allocated); +#else + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::cout << "write watch unavailable: a heap run leaving the write watch not tested\n"; + return; + } + void* mapping = mmap(nullptr, allocated + block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(mapping != MAP_FAILED, "cannot map the heap run"); + auto* guest = reinterpret_cast((reinterpret_cast(mapping) + block - 1) & ~std::uintptr_t{block - 1}); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(guest, 2 * heapBytes); +#endif + std::memset(guest, 0x5a, allocated); + const auto first = reinterpret_cast(guest); + const auto second = first + heapBytes; + const auto registry = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + for (auto* piece : {guest, guest + heapBytes}) { + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, piece, heapBytes, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, piece); + } + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + registry(true); + Require(AgcDriver::GuestMemory::Watched(first, 2 * heapBytes), "the heap run is not write-watched"); + const auto before = MirrorCounters(); + { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + leased.Upload(true); + Require(MirrorCounters().heapMirrors == before.heapMirrors + 2, "the first build did not mirror both heaps of the run"); + leased.WriteBack(); + } + AgcDriver::GuestMemory::Unwatch(first + block, block); + Require(!AgcDriver::GuestMemory::Watched(first, heapBytes) && AgcDriver::GuestMemory::Watched(second, heapBytes), "only the first heap of the run should have left the write watch"); + std::memset(guest + block, 0x6b, block); + std::memset(guest + heapBytes + 2 * block, 0x3c, block); + { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + leased.Upload(true); + Require(MirrorCounters().heapMirrors == before.heapMirrors + 2, "a heap mirror of the run was dropped once part of the run left the write watch"); + const auto ranges = leased.AddressRanges(); + const auto served = [&](std::uintptr_t base) { + const auto found = std::find_if(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == base; }); + Require(found != ranges.end() && found->deviceAddress != 0u, "a heap of the run is missing from the BDA table"); + return access.addressBytes(found->deviceAddress); + }; + Require(served(first)[block + 3] == std::byte{0x6b}, "the unwatched heap of the run does not serve its new bytes"); + Require(served(second)[2 * block + 3] == std::byte{0x3c}, "the watched heap beside an unwatched one missed the CPU's write"); + leased.WriteBack(); + } + registry(false); + ClearImageMirrors(context.device); + ClearSnapshotCopies(context.device); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(guest, allocated); + GuestArena::GuestArenaRelease_nid_postfix(guest, allocated); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(guest, 2 * heapBytes); + munmap(mapping, allocated + block); +#endif + std::cout << "an address-based build refreshes the unwatched heap of a run in full and its watched neighbour by its writes\n"; +} + void hostWriteWatchExclusionTests() { #ifdef _WIN32 std::vector host(64, 0x31); @@ -1464,6 +3372,27 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { reject([&] { resources.WriteBack(); }, "rect-list requires"); Require(guest[0] == 123, "failed GPU command published writes"); } + { + const auto fail = [&](ShaderRecompiler::BdaAbi::FaultReason reason, std::uint32_t word, std::uint32_t value, std::uint32_t otherFaults = 0) { + ShaderResources resources(context, compiled); + std::memcpy(access.bytes(access.descriptor(6).buffer).data() + word * sizeof(std::uint32_t), &value, sizeof(value)); + const ShaderRecompiler::BdaAbi::Fault report{ShaderRecompiler::BdaAbi::FaultState::Ready, reason, 0x7fff99880002ULL, 4, 0, 0x5c8, otherFaults}; + std::memcpy(access.bytes(access.descriptor(5).buffer).data(), &report, sizeof(report)); + try { + resources.WriteBack(); + } catch (const BdaUnalignedAccess& error) { + Require(std::string(error.what()).find("BDA access failed") != std::string::npos, "an unaligned BDA access did not report the failed access"); + return true; + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("BDA access failed") != std::string::npos, "a BDA fault did not report the failed access"); + return false; + } + throw std::runtime_error("a recorded BDA fault was not reported"); + }; + Require(fail(ShaderRecompiler::BdaAbi::FaultReason::Unaligned, 2, 777) && guest[2] == 777, "an unaligned BDA access lost the stores of the dispatch's other invocations"); + Require(!fail(ShaderRecompiler::BdaAbi::FaultReason::Unmapped, 3, 888) && guest[3] == 0, "a failed GPU command published writes"); + Require(!fail(ShaderRecompiler::BdaAbi::FaultReason::Unaligned, 5, 999, 1) && guest[5] == 0, "an unaligned BDA access that hid a fault of another kind published writes"); + } { auto aligned = context; aligned.limits.minStorageBufferOffsetAlignment = 16; @@ -1550,17 +3479,56 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { if (dropped.enabled) Require(dropped.waiterDrops == after.waiterDrops + 1 && LeaseCounters().cacheDrops == dropped.waiterDrops, "the free did not drop the cached address space"); } importCrossingTests(context, access); + addressTableKeyTests(); + snapshotTableTests(context, access); importSpanTests(context); failedImportRemapTests(context, access); spaceImportRunTests(context); + spaceWritableWritesTests(context); + spaceReadListTests(context); 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); + importMappedFailureGenerationTests(context); + importStaleProbeTests(context, access); importRefusedWholeRangeTests(context); importNeighbouringEdgesTests(context); importAtMapOverBudgetTests(context); + importRequesterTests(context, access); + hostImportReportTests(); + importBlockMapTests(context, false); +#ifndef _WIN32 + importBlockMapTests(context, true); +#endif + importBlockBuildTests(context); + importBlockHoleTests(context); + importBlockUnalignedTests(context); + importBlockFlagTests(context); + importBlockRemapTests(context); + importBlockFailureTests(context, access); + importBlockBudgetTests(context); + importRefusalMemoryTests(context); + evictingRefusalMemoryTests(context); + evictingRefusalAgingTests(context); + evictingRefusalRangeTests(context); + evictingRefusalWindowTests(context); + evictingRefusalBlockRangesTests(context); + evictingRefusalJoinTests(context); + evictingRefusalStepDownTests(context); + importDeviceBudgetTests(); + importCeilingTests(context, access, false); + importCeilingTests(context, access, true); + importReportFloorTests(context, access, true); + importReportFloorTests(context, access, false); + importReportRefusalMemoryTests(context, access); + sparseImportedHeapTests(context, access); heapMirrorTests(context, access); importedHeapMirrorTests(context, access); importWatchTests(); @@ -1568,9 +3536,11 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { gpuMappingTests(context); stridedOverhangTests(context, access); sparseHeapTests(context, access); + spaceCommittedPagesTests(context, access); hostWriteWatchExclusionTests(); sparseHeapMirrorTests(context, access); Require(AddressCopyOverflow({{0x1000, 0x3000, 0x2000, "uncommitted pages"}}, 0x2000).empty(), "copies within the limit were refused"); const auto copies = AddressCopyOverflow({{0x1000, 0x2000, 0x1000, "not mirrored"}, {0x10000, 0x30000, 0x18000, "uncommitted pages"}}, 0x2000); Require(!copies.empty() && copies.find("0x10000+0x20000 (0.1 MiB committed, uncommitted pages)") < copies.find("0x1000+0x1000"), "the copy limit does not name the largest copy first"); + ClearSnapshotCopies(context.device); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp index 60a2e87853..9ca39a5b01 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp @@ -133,3 +133,102 @@ std::vector MakeBdaWriteTestShader(std::uint64_t address, std::ui #endif return shader; } + +std::vector MakeBdaFaultSequenceTestShader(std::initializer_list reasons) { + using namespace ShaderRecompiler; + return MakeShader(0u, false, true, [&](SpirvValueEmitContext& ctx, const IrValue&, std::uint32_t base) { + for (const auto reason : reasons) RecordBdaFault(ctx.state, base, ConstantU32(ctx.state, 4u), ConstantU32(ctx.state, 0x1234u), reason); + return std::vector{ConstantU32(ctx.state, 1u)}; + }); +} + +std::vector MakeBdaAnchoredReadTestShader(const BdaAnchoredReadProgram& reads, bool anchored) { + using namespace ShaderRecompiler; + IrProgram program; + program.Resources().stage = IrShaderStage::Compute; + program.Info().usesDma = true; + if (reads.coherent) { + MemoryInfo memory; + memory.coherent = true; + program.Resources().memoryInfo = {memory}; + } + SpirvEmitterState state(program, {}); + EmitBaseHeader(state.module, program); + if (reads.elect) { + state.module.EmitCapability(spv::CapabilityGroupNonUniform); + state.module.EmitCapability(spv::CapabilityGroupNonUniformBallot); + state.requirements.subgroupBallot = true; + } + const auto define = [&](std::uint32_t binding) { + const auto variable = state.module.DefineGlobalVariable(TypeStorageBufferPointer(state), spv::StorageClassStorageBuffer); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, 0u); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBinding, binding); + return variable; + }; + state.bdaPagetableVariable = define(0); + state.faultBufferVariable = define(1); + const auto output = define(2); + std::vector interfaceVariables{state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassInput, TypeU32(state)), spv::StorageClassInput)}; + state.module.AddAnnotation(spv::OpDecorate, interfaceVariables[0], spv::DecorationBuiltIn, spv::BuiltInLocalInvocationIndex); + state.bdaStopsInvocations = reads.stops; + DefineGetBdaPointer(state); + if (anchored) DefineBdaAnchorFunction(state); + const auto main = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunction, TypeVoid(state), main, spv::FunctionControlMaskNone, TypeFunction(state)); + EmitLabel(state, state.module.AllocateId()); + SpirvValueEmitContext ctx(state); + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + const auto input = [&](std::uint32_t word) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, u32, value, BdaWord(state, output, word)); + return value; + }; + const auto base = Binary(state, spv::OpBitwiseOr, u64, Unary(state, spv::OpUConvert, u64, input(ConstantU32(state, BdaAnchoredReadInputWord))), Binary(state, spv::OpShiftLeftLogical, u64, Unary(state, spv::OpUConvert, u64, input(ConstantU32(state, BdaAnchoredReadInputWord + 1u))), BdaConstant(state, 32u))); + SpirvEmitterState::BdaAnchor anchor{}; + if (anchored) EmitBdaAnchorRange(state, anchor, base); + const auto selector = input(ConstantU32(state, BdaAnchoredReadInputWord + 2u)); + const auto laneIndex = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, u32, laneIndex, interfaceVariables[0]); + const auto low = input(Binary(state, spv::OpIAdd, u32, laneIndex, ConstantU32(state, BdaAnchoredReadInputWord + 3u))); + const auto slot = Binary(state, spv::OpIMul, u32, laneIndex, ConstantU32(state, 4u)); + IrBuilder ir(program); + for (std::uint32_t index = 0; index < reads.reads.size(); ++index) { + const auto& read = reads.reads[index]; + const auto opcode = read.bits == 8u ? IrOpcode::LoadAddressU8 : read.bits == 16u ? IrOpcode::LoadAddressU16 : read.dwords == 2u ? IrOpcode::LoadAddressU32x2 : read.dwords == 3u ? IrOpcode::LoadAddressU32x3 : read.dwords == 4u ? IrOpcode::LoadAddressU32x4 : IrOpcode::LoadAddressU32; + const auto type = read.bits != 32u || read.dwords == 1u ? IrType::U32 : read.dwords == 2u ? IrType::U32x2 : read.dwords == 3u ? IrType::U32x3 : IrType::U32x4; + auto& instruction = program.CreateValue(opcode, type); + MemoryFlags flags{}; + flags.pc = 0x1234; + instruction.SetFlags(flags); + if (read.bits == 32u && read.dwords > 1u) { + const auto extract = read.dwords == 2u ? IrOpcode::CompositeExtractU32x2 : read.dwords == 3u ? IrOpcode::CompositeExtractU32x3 : IrOpcode::CompositeExtractU32x4; + for (std::uint32_t dword = 0; dword < read.dwords; ++dword) { + if ((read.used & (1u << dword)) == 0u) continue; + auto& user = program.CreateValue(extract, IrType::U32); + user.AddArgument(&instruction); + user.AddArgument(&ir.Constant(dword)); + } + } + EmitIfCondition(state, Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, index)), [&] { + const auto lookup = [&] { + if (reads.sentinel) { + const auto sentinel = ConstantU32(state, BdaAnchoredReadSentinel); + return std::array{sentinel, sentinel, sentinel, sentinel}; + } + const auto address = AddBdaAddress(ctx, instruction, base, Unary(state, spv::OpUConvert, u64, low), false); + if (read.bits == 32u) return EmitBdaDwordReads(ctx, instruction, address, static_cast(read.immediate), read.dwords); + return std::array{EmitBdaRead(ctx, instruction, AddBdaImmediate(ctx, instruction, address, read.immediate), read.bits)}; + }; + const auto values = anchored ? EmitAnchoredBdaRead(state, instruction, anchor, low, read.immediate, read.bits, read.dwords, 0u, lookup) : lookup(); + for (std::uint32_t dword = 0; dword < (read.bits == 32u ? read.dwords : 1u); ++dword) { + state.module.AddFunction(spv::OpStore, BdaWord(state, output, Binary(state, spv::OpIAdd, u32, slot, ConstantU32(state, dword))), values[dword]); + } + }); + } + state.module.AddFunction(spv::OpReturn); + state.module.AddFunction(spv::OpFunctionEnd); + state.module.AddExecutionMode(main, spv::ExecutionModeLocalSize, reads.lanes, 1u, 1u); + state.module.EmitEntryPoint(spv::ExecutionModelGLCompute, main, "main", interfaceVariables); + return state.module.Finalize(); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp index 62a3adcb19..e31020caf7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp @@ -2,15 +2,35 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_BDASHADER_HPP #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "BdaAbi.hpp" #include +#include #include std::vector MakeBdaTestShader(std::uint64_t address, std::uint32_t bits, std::int64_t offset = 0); std::vector MakeBdaDwordReadTestShader(std::uint64_t address, std::uint32_t dwords, bool coherent, bool stops); std::vector MakeBdaSpanReadTestShader(std::uint64_t address, std::uint32_t offset, std::uint32_t extracted, bool coherent, bool stops); +std::vector MakeBdaFaultSequenceTestShader(std::initializer_list reasons); std::vector MakeBdaDwordWriteTestShader(std::uint64_t address, std::uint32_t dwords, const std::uint32_t* values); std::vector MakeBdaStoreTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent); std::vector MakeBdaWriteTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent); +inline constexpr std::uint32_t BdaAnchoredReadInputWord = 256; +struct BdaAnchoredRead { + std::int32_t immediate = 0; + std::uint32_t bits = 32; + std::uint32_t dwords = 1; + std::uint32_t used = 1; +}; +struct BdaAnchoredReadProgram { + std::vector reads; + bool stops = true; + bool elect = false; + bool coherent = false; + std::uint32_t lanes = 1; + bool sentinel = false; +}; +inline constexpr std::uint32_t BdaAnchoredReadSentinel = 0xdeadbeefu; +std::vector MakeBdaAnchoredReadTestShader(const BdaAnchoredReadProgram& reads, bool anchored); void RunBdaStoreExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaContractTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp index b8c859c022..6aea082fff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp @@ -12,10 +12,14 @@ struct BdaTestAccess { std::function(VkDeviceAddress)> addressBytes; std::function)> limitMemory; std::function allocationAttempts; + std::function)> limitImports; }; void RunBdaResourceTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); bool RunSparseBufferHoleTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); +void RunHeapMirrorUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access, bool throughImport); +void RunSparseHeapMirrorUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); +void RunHeapMirrorRunUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); void RunGuestAllocationTests(); void RunGuestLeaseWaitTests(); void RunUnmappedGapTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index 3ab9d30d26..abdf8f0166 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -1,21 +1,32 @@ #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "AllocationFailure.hpp" +#include "SlabReturn.hpp" #include +#include #include +#include #include #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) { @@ -37,12 +48,18 @@ struct MockDevice { std::uint64_t destroyedBuffers = 0; VkDeviceSize liveBytes = 0; std::map memoryBytes; + std::map memoryTypes; std::map> bound; std::set liveBuffers; std::deque results; 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; @@ -74,23 +91,30 @@ VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAlloca *memory = reinterpret_cast(static_cast(mock.next++)); if (info->memoryTypeIndex == 1) mock.hostMemory[*memory].resize(info->allocationSize); mock.memoryBytes[*memory] = info->allocationSize; + mock.memoryTypes[*memory] = info->memoryTypeIndex; mock.liveBytes += info->allocationSize; ++mock.allocations; return VK_SUCCESS; } 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"); @@ -147,6 +171,114 @@ Context mockContext() { return context; } +VkDeviceSize reportedBudget = 0; +VkDeviceSize reportedUsage = 0; + +VKAPI_ATTR void VKAPI_CALL mockMemoryProperties2(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { + auto* reported = static_cast(properties->pNext); + reported->heapBudget[0] = reportedBudget; + reported->heapUsage[0] = reportedUsage; +} + +std::uint64_t diagnosticBudgetQueries = 0; + +VKAPI_ATTR void VKAPI_CALL mockDiagnosticMemoryProperties2(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { + ++diagnosticBudgetQueries; + auto* reported = static_cast(properties->pNext); + reported->heapUsage[0] = 1500 * MiB; + reported->heapBudget[0] = 2000 * MiB; + reported->heapUsage[1] = 2500 * MiB; + reported->heapBudget[1] = 3000 * MiB; +} + +Context budgetContext() { + auto context = mockContext(); + context.physical = reinterpret_cast(std::uintptr_t{1}); + context.memoryProperties2 = mockMemoryProperties2; + context.memory.memoryHeapCount = 2; + context.memory.memoryHeaps[0] = {2048 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + context.memory.memoryHeaps[1] = {16384 * MiB, 0}; + context.memory.memoryTypes[0].heapIndex = 0; + context.memory.memoryTypes[1].heapIndex = 1; + return context; +} + +void HostPlacement() { + mock = MockDevice{}; + auto context = budgetContext(); + constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + reportedBudget = 1620 * MiB; + reportedUsage = 1600 * MiB; + VkDeviceMemory placed = VK_NULL_HANDLE; + { + DeviceBuffer large(context, 176 * MiB, usage); + placed = MemoryOf(large.Handle()); + Expect(mock.memoryTypes.at(placed) == 1, "a device buffer over the device-local budget was not placed in the host memory type"); + } + Expect(!mock.memoryBytes.contains(placed) && mock.liveBuffers.empty(), "a device buffer placed in host memory was kept in the device pool after its release"); + reportedUsage = 200 * MiB; + VkDeviceMemory device = VK_NULL_HANDLE; + { + DeviceBuffer large(context, 176 * MiB, usage); + device = MemoryOf(large.Handle()); + Expect(mock.memoryTypes.at(device) == 0, "a device buffer within the device-local budget was not placed in device-local memory"); + } + Expect(mock.memoryBytes.contains(device), "a device-local buffer was not kept in the device pool after its release"); + DeviceBuffer again(context, 176 * MiB, usage); + Expect(MemoryOf(again.Handle()) == device, "a retained device-local buffer was not reused"); +} + +void RefusedDevicePlacement() { + mock = MockDevice{}; + auto context = budgetContext(); + constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + reportedBudget = 1620 * MiB; + reportedUsage = 200 * MiB; + mock.results = {VK_ERROR_OUT_OF_DEVICE_MEMORY}; + VkDeviceMemory placed = VK_NULL_HANDLE; + { + DeviceBuffer refused(context, 9 * MiB, usage); + placed = MemoryOf(refused.Handle()); + Expect(mock.memoryTypes.at(placed) == 1, "a device buffer whose device-local allocation ran out of device memory within the reported budget was not placed in the host memory type"); + } + Expect(!mock.memoryBytes.contains(placed) && mock.liveBuffers.empty(), "a device buffer placed in host memory after a refused device-local allocation was kept in the device pool after its release"); + mock.results = {VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}; + context.memory.memoryHeaps[1].flags = VK_MEMORY_HEAP_DEVICE_LOCAL_BIT; + bool threw = false; + try { + DeviceBuffer refused(context, 9 * MiB, usage); + } catch (const std::runtime_error&) { + threw = true; + } + Expect(threw, "a refused device-local allocation without a host memory type did not fail"); + Expect(mock.liveBuffers.empty(), "a failed device buffer left its VkBuffer alive"); +} + +void AttachmentReclaim() { + mock = MockDevice{}; + auto context = budgetContext(); + constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + reportedBudget = 1620 * MiB; + reportedUsage = 200 * MiB; + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = 8 * MiB; + allocation.memoryTypeIndex = 0; + const auto refuse = [&](std::initializer_list results) { + { DeviceBuffer idle(context, 4 * MiB, usage); } + Expect(mock.memoryBytes.size() == 1, "a released device buffer was not kept in the device pool"); + const auto pooled = mock.memoryBytes.begin()->first; + mock.results = results; + const auto memory = AllocateImageMemory(context, allocation, 3, "vkAllocateMemory attachment"); + Expect(!mock.memoryBytes.contains(pooled), "an attachment refused device memory did not reclaim the pool before retrying"); + const auto type = mock.memoryTypes.at(memory); + mockFreeMemory(VK_NULL_HANDLE, memory, nullptr); + return type; + }; + Expect(refuse({VK_ERROR_OUT_OF_DEVICE_MEMORY}) == 0, "an attachment was placed in host memory although reclaiming the pool made room in device memory"); + Expect(refuse({VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}) == 1, "an attachment still refused device memory after reclaiming the pool was not placed in host memory"); + Expect(mock.liveBytes == 0, "an attachment allocation leaked memory"); +} + void SizeClassesAndDirections() { mock = MockDevice{}; auto context = mockContext(); @@ -167,6 +299,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(); @@ -283,6 +440,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"); @@ -453,6 +670,168 @@ 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 ImageCacheReclaim() { + mock = MockDevice{}; + reclaimMock = ReclaimMock{}; + auto context = budgetContext(); + context.cacheReclaim = mockCacheReclaim; + reportedBudget = 1620 * MiB; + reportedUsage = 200 * MiB; + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = 8 * MiB; + allocation.memoryTypeIndex = 0; + const auto refuse = [&](std::initializer_list results) { + ExternalAllocation(4 * MiB); + mock.results = results; + const auto requests = reclaimMock.requests; + const auto memory = AllocateImageMemory(context, allocation, 3, "vkAllocateMemory texture"); + Expect(reclaimMock.requests == requests + 1 && reclaimMock.external == VK_NULL_HANDLE, "a texture refused device memory did not release the texture caches before retrying"); + const auto type = mock.memoryTypes.at(memory); + mockFreeMemory(VK_NULL_HANDLE, memory, nullptr); + return type; + }; + Expect(refuse({VK_ERROR_OUT_OF_DEVICE_MEMORY}) == 0, "a texture was placed in host memory although releasing the texture caches made room in device memory"); + Expect(refuse({VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}) == 1, "a texture still refused device memory after releasing the texture caches was not placed in host memory"); + Expect(mock.liveBytes == 0, "a texture allocation leaked memory"); +} + +void AllocationFailureReportsHeapBudgets() { + mock = MockDevice{}; + diagnosticBudgetQueries = 0; + std::string failure; + std::uint64_t allocationPathBudgetQueries = 0; + { + auto context = mockContext(); + context.physical = reinterpret_cast(std::uintptr_t{1}); + context.memoryProperties2 = mockDiagnosticMemoryProperties2; + context.memory.memoryHeapCount = 2; + context.bufferPool = std::make_shared(context); + const auto beforeAllocation = diagnosticBudgetQueries; + mock.results = {VK_ERROR_DEVICE_LOST}; + try { + DeviceBuffer buffer(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + } catch (const std::exception& error) { + failure = error.what(); + } + allocationPathBudgetQueries = diagnosticBudgetQueries - beforeAllocation; + } + Expect(failure.find("heap 0 1500/2000 MiB") != std::string::npos, "allocation failure omitted the first heap's memory budget"); + Expect(failure.find("heap 1 2500/3000 MiB") != std::string::npos, "allocation failure omitted a later heap's memory budget"); + Expect(allocationPathBudgetQueries == 2, "allocation failure did not query the memory budget for both type selection and its diagnostic"); + + mock = MockDevice{}; + diagnosticBudgetQueries = 0; + failure.clear(); + { + auto context = mockContext(); + context.physical = reinterpret_cast(std::uintptr_t{1}); + context.memory.memoryHeapCount = 2; + mock.results = {VK_ERROR_DEVICE_LOST}; + try { + DeviceBuffer buffer(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + } catch (const std::exception& error) { + failure = error.what(); + } + } + Expect(failure.find("heap 0") == std::string::npos && failure.find("heap 1") == std::string::npos, "allocation failure emitted heap data when VK_EXT_memory_budget was unavailable"); + Expect(diagnosticBudgetQueries == 0, "missing-extension fallback attempted a memory budget query"); +} + void SuccessfulAllocationKeepsPool() { mock = MockDevice{}; { @@ -485,16 +864,169 @@ 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() { +} + +VkDeviceMemory reclaimImage = VK_NULL_HANDLE; +const Context* reclaimImageContext = nullptr; + +VkDeviceSize mockImageReclaim(VkDeviceSize) { + if (reclaimImage == VK_NULL_HANDLE) return 0; + ReleaseImageMemory(*reclaimImageContext, reclaimImage, 10 * MiB, 0); + reclaimImage = VK_NULL_HANDLE; + return 10 * MiB; +} + +void ImageMemoryReclaim() { + mock = MockDevice{}; + auto context = mockContext(); + context.cacheReclaim = mockImageReclaim; + reclaimImageContext = &context; + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = 10 * MiB; + allocation.memoryTypeIndex = 0; + std::uint32_t type = 0; + reclaimImage = AllocateImageMemory(context, allocation, 1u, "test image", &type); + mock.limit = 12 * MiB; + auto request = allocation; + request.allocationSize = 4 * MiB; + const auto before = mock.attempts; try { + const auto recovered = AllocateImageMemory(context, request, 1u, "test image", &type); + Expect(recovered != VK_NULL_HANDLE && mock.attempts == before + 2 && mock.liveBytes == 4 * MiB && context.bufferPool->RetainedImages() == 0, "the image memory a cache release returns to the pool is freed before the retry"); + ReleaseImageMemory(context, recovered, 4 * MiB, 0); + } catch (const std::exception& error) { + Expect(false, std::string("the cache release of an image did not permit recovery: ") + error.what()); + } + reclaimImageContext = nullptr; +} + +void ImageMemoryOverBudget() { + mock = MockDevice{}; + auto context = budgetContext(); + reportedBudget = 1000 * MiB; + reportedUsage = 500 * MiB; + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = 4 * MiB; + allocation.memoryTypeIndex = 0; + std::uint32_t type = 0; + const auto first = AllocateImageMemory(context, allocation, 1u, "test image", &type); + ReleaseImageMemory(context, first, 4 * MiB, 0); + Expect(mock.frees == 0 && context.bufferPool->RetainedImages() == 1, "image memory is retained while the heap is under its reported budget"); + reportedUsage = 1100 * MiB; + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + const auto second = AllocateImageMemory(context, allocation, 1u, "test image", &type); + Expect(second == first && mock.allocations == 1, "a retained allocation is still handed out over the budget"); + ReleaseImageMemory(context, second, 4 * MiB, 0); + Expect(mock.frees == 1 && context.bufferPool->RetainedImages() == 0, "image memory released while the heap is over its reported budget is freed"); + const auto third = AllocateImageMemory(context, allocation, 1u, "test image", &type); + ReleaseImageMemory(context, third, 4 * MiB, 0); + reportedUsage = 500 * MiB; + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + const auto fourth = AllocateImageMemory(context, allocation, 1u, "test image", &type); + ReleaseImageMemory(context, fourth, 4 * MiB, 0); + Expect(mock.frees == 2 && context.bufferPool->RetainedImages() == 1, "the pool retains image memory again once the heap is under its budget"); +} + +void ImageMemoryReuse() { + mock = MockDevice{}; + auto context = mockContext(); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = 4 * MiB; + allocation.memoryTypeIndex = 0; + std::uint32_t type = 99; + const auto first = AllocateImageMemory(context, allocation, 1u, "test image", &type); + Expect(type == 0 && mock.allocations == 1, "the first image allocates device memory of its type"); + ReleaseImageMemory(context, first, 4 * MiB, 0); + Expect(mock.frees == 0 && context.bufferPool->RetainedImages() == 1, "a released device-local image allocation is retained"); + const auto again = AllocateImageMemory(context, allocation, 1u, "test image", &type); + Expect(again == first && type == 0 && mock.allocations == 1 && context.bufferPool->RetainedImages() == 0, "the next image of the same type and size takes the retained allocation"); + auto smaller = allocation; + smaller.allocationSize = 2 * MiB; + const auto other = AllocateImageMemory(context, smaller, 1u, "test image", &type); + Expect(other != again && mock.allocations == 2, "an image of another size allocates"); + ReleaseImageMemory(context, other, 2 * MiB, 0); + ReleaseImageMemory(context, again, 4 * MiB, 0); + auto host = allocation; + host.memoryTypeIndex = 1; + const auto placed = AllocateImageMemory(context, host, 2u, "test image", &type); + Expect(type == 1 && mock.allocations == 3, "a host-visible image allocation is made"); + ReleaseImageMemory(context, placed, 4 * MiB, 1); + Expect(mock.frees == 1 && context.bufferPool->RetainedImages() == 2, "a host-visible image allocation is freed when released"); + const auto unsized = AllocateImageMemory(context, smaller, 1u, "test image", &type); + Expect(unsized == other && mock.allocations == 3 && context.bufferPool->RetainedImages() == 1, "an image of a retained size takes that allocation"); + ReleaseImageMemory(context, unsized, 0, 0); + Expect(mock.frees == 2 && context.bufferPool->RetainedImages() == 1, "an allocation of unknown size is freed"); + std::vector many; + for (int i = 0; i < 33; ++i) many.push_back(AllocateImageMemory(context, allocation, 1u, "test image", &type)); + Expect(many.front() == again && mock.allocations == 35, "a burst takes the retained allocation first and allocates the rest"); + for (const auto memory : many) ReleaseImageMemory(context, memory, 4 * MiB, 0); + Expect(context.bufferPool->RetainedImages() == 32 && mock.frees == 3 && !mock.memoryBytes.contains(many.front()) && mock.memoryBytes.contains(many.back()), "retained image memory is evicted least recently used under its budget"); + mock.results.push_back(VK_ERROR_OUT_OF_DEVICE_MEMORY); + auto third = allocation; + third.allocationSize = 3 * MiB; + const auto recovered = AllocateImageMemory(context, third, 1u, "test image", &type); + Expect(recovered != VK_NULL_HANDLE && mock.allocations == 36 && mock.frees == 35 && context.bufferPool->RetainedImages() == 0, "an allocation failure frees the retained image memory"); + ReleaseImageMemory(context, recovered, 3 * MiB, 0); + Expect(mock.frees == 35 && context.bufferPool->RetainedImages() == 1, "the pool retains image memory again after a trim"); +} + +int main(int argc, char** argv) { + try { + if (argc == 2 && std::string_view(argv[1]) == "--small-device-retention") { + SmallDeviceRetention(); + return failures == 0 ? 0 : 1; + } + if (argc == 2) { + const bool host = std::strcmp(argv[1], "--slab-return-host") == 0; + Expect(host || std::strcmp(argv[1], "--slab-return-device") == 0, "unknown slab return test mode"); + AgcDriver::Tests::SlabReturnWithoutAllocation(mockContext(), host ? 1u : 0u); + Expect(mock.liveBytes == 0, "slab return test leaked memory"); + return failures == 0 ? 0 : 1; + } + AgcDriver::Tests::SlabReturnWithoutAllocation(mockContext(), 0); + AgcDriver::Tests::SlabReturnWithoutAllocation(mockContext(), 1); + Expect(mock.liveBytes == 0, "slab return test leaked memory"); SizeClassesAndDirections(); LargerClass(); KeptUntilFence(); + 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); @@ -506,8 +1038,17 @@ int main() { ReclaimSlabs(); AllocationFailure(VK_ERROR_DEVICE_LOST, VK_ERROR_DEVICE_LOST, true); AllocationFailure(VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_DEVICE_LOST, true); + AllocationFailureReportsHeapBudgets(); SuccessfulAllocationKeepsPool(); PartialSlabDoesNotRetry(); + CacheReclaimRecovery(); + ImageCacheReclaim(); + ImageMemoryReuse(); + ImageMemoryReclaim(); + ImageMemoryOverBudget(); + HostPlacement(); + RefusedDevicePlacement(); + AttachmentReclaim(); } catch (const std::exception& error) { std::fprintf(stderr, "FAIL: %s\n", error.what()); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferResourceVcc.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferResourceVcc.cpp new file mode 100644 index 0000000000..aecc7400eb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/BufferResourceVcc.cpp @@ -0,0 +1,45 @@ +#include "RdnaDecoder/RdnaMemoryOpDecoder.hpp" +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +int main() { + try { + for (const auto first : {0xe8b00000u, 0xe8b40000u, 0xe0000000u, 0xe0300000u, 0xe0700000u}) { + const auto decode = [&](std::uint32_t resource) { + const std::array code{first, (128u << 24u) | (resource << 16u) | (7u << 8u) | 3u}; + return first >> 26u == 0x3au ? DecodeRdnaMtbuf(0x100u, code, 0u) : DecodeRdnaMubuf(0x100u, code, 0u); + }; + for (std::uint32_t resource = 0u; resource <= 26u; ++resource) { + const auto instruction = decode(resource); + if (instruction.programCounter != 0x100u || instruction.sourceCount != 3u || + instruction.source1.kind != RdnaOperandKind::ScalarRegister || instruction.source1.reg != resource * 4u || + instruction.source0.kind != RdnaOperandKind::VectorRegister || instruction.source0.reg != 3u || + instruction.destination.kind != RdnaOperandKind::VectorRegister || instruction.destination.reg != 7u) + throw std::runtime_error("buffer descriptor decode changed another operand"); + if (first >> 26u == 0x3au && (instruction.dataFormat != 6u || instruction.numberFormat != 1u)) + throw std::runtime_error("MTBUF VCC descriptor changed the instruction format"); + } + for (std::uint32_t resource = 27u; resource < 32u; ++resource) { + bool refused = false; + try { static_cast(decode(resource)); } + catch (const std::runtime_error& error) { + const auto expected = resource != 31u ? "unsupported scalar source operand" : + first >> 26u == 0x3au ? "MTBUF resource descriptor register range overflow" : + "MUBUF resource descriptor register range overflow"; + refused = std::string_view(error.what()) == expected; + } + if (!refused) throw std::runtime_error("buffer descriptor extends past the VCC tuple"); + } + } + std::puts("MUBUF and MTBUF mixed SGPR/VCC descriptors and invalid-base refusals pass"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} 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..eb6a6b104b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace { @@ -21,6 +22,17 @@ void Reject(TAction action, const char* message) { Require(rejected, message); } +template +void RejectWithMessage(TAction action, const char* expected) { + bool rejected = false; + try { + action(); + } catch (const std::exception& error) { + rejected = std::string_view(error.what()).find(expected) != std::string_view::npos; + } + Require(rejected, "strict capture did not preserve its diagnostic"); +} + class Directory { public: Directory() { @@ -62,6 +74,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 +160,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}}; }); @@ -155,10 +175,121 @@ void CheckMalformedRecords() { invalid([](auto& copy) { copy.sets = {{0, {{}, {}}}}; }); } +void CheckOptionalEntryCap() { + Directory directory; + auto capture = AgcDriver::ComputePipelineFixture(); + std::filesystem::path first; + for (std::uint32_t index = 0; index < 128u; ++index) { + capture.specialization[0].value = index; + const auto path = capture.WriteOptional(directory.path); + Require(!path.empty() && AgcDriver::ComputePipelineCapture::OptionalEnabled(), "optional capture stopped before accepting 128 records"); + if (index == 0u) first = path; + } + capture.specialization[0].value = 0u; + Require(capture.WriteOptional(directory.path) == first, "optional capture did not deduplicate at the entry cap"); + capture.specialization[0].value = 128u; + Require(capture.WriteOptional(directory.path).empty() && !AgcDriver::ComputePipelineCapture::OptionalEnabled(), "optional capture did not stop at the entry cap"); + RejectWithMessage([&] { capture.Write(directory.path); }, "compute pipeline capture: capture directory reached 128 entries"); + for (unsigned index = 0; index < 64u; ++index) Require(capture.WriteOptional(directory.path / "after-stop").empty(), "disabled optional capture wrote another file"); + Require(!std::filesystem::exists(directory.path / "after-stop"), "disabled optional capture touched its output directory"); + capture.specialization[0].value = 0u; + Require(capture.Write(directory.path) == first, "optional stop disabled strict capture deduplication"); } -int main() { +void CheckOptionalByteCap() { + Directory directory; + auto capture = AgcDriver::ComputePipelineFixture(); + const auto smallBytes = capture.Encode().size(); + constexpr std::size_t chunkBytes = 32u * 1024u * 1024u; + for (std::uint32_t index = 0; index < 8u; ++index) { + auto large = capture; + large.specialization[0].value = index; + const auto bytes = chunkBytes - (index == 7u ? smallBytes : 0u); + Require(bytes >= smallBytes && (bytes - smallBytes) % 4u == 0u, "byte-cap fixture size is not representable"); + large.spirv.insert(large.spirv.end() - 2, (bytes - smallBytes) / 4u, 0x00010000u); + Require(std::filesystem::file_size(large.Write(directory.path)) == bytes, "byte-cap fixture size differs"); + } + capture.specialization[0].value = 8u; + Require(!capture.WriteOptional(directory.path).empty() && AgcDriver::ComputePipelineCapture::OptionalEnabled(), "optional capture refused exactly 256 MiB"); + std::uintmax_t total = 0; + for (const auto& entry : std::filesystem::directory_iterator(directory.path)) total += std::filesystem::file_size(entry.path() / "capture.aps5cp"); + Require(total == 256u * 1024u * 1024u, "byte-cap fixture did not reach exactly 256 MiB"); + capture.specialization[0].value = 9u; + Require(capture.WriteOptional(directory.path).empty() && !AgcDriver::ComputePipelineCapture::OptionalEnabled(), "optional capture did not stop beyond the byte cap"); + RejectWithMessage([&] { capture.Write(directory.path); }, "compute pipeline capture: capture directory exceeds 256 MiB"); +} + +void CheckOptionalIo() { + Directory directory; + const auto blocked = directory.path / "blocked"; + { + std::ofstream output(blocked, std::ios::binary); + output.put('x'); + Require(output.good(), "cannot create output conflict"); + } + const auto capture = AgcDriver::ComputePipelineFixture(); + Require(capture.WriteOptional(blocked / "captures").empty() && !AgcDriver::ComputePipelineCapture::OptionalEnabled(), "optional capture did not stop after an output I/O error"); + Require(std::filesystem::file_size(blocked) == 1u, "optional capture changed the conflicting file"); + RejectWithMessage([&] { capture.Write(blocked / "captures"); }, "compute pipeline capture: cannot create capture directory"); + Require(capture.WriteOptional(directory.path / "after-stop").empty() && !std::filesystem::exists(directory.path / "after-stop"), "disabled optional capture retried output"); + auto malformed = capture; + malformed.spirv[0] = 0u; + Reject([&] { malformed.WriteOptional(directory.path / "after-stop"); }, "disabled optional capture swallowed an invalid pipeline input"); +} + +void CheckOptionalInvalid() { + Directory directory; + auto capture = AgcDriver::ComputePipelineFixture(); + auto malformed = capture; + malformed.spirv[0] = 0u; + Reject([&] { malformed.WriteOptional(directory.path / "malformed"); }, "optional capture swallowed an invalid pipeline input"); + Require(AgcDriver::ComputePipelineCapture::OptionalEnabled() && !std::filesystem::exists(directory.path / "malformed"), "invalid input disabled optional capture or touched its output"); + const auto path = capture.WriteOptional(directory.path); + Require(!path.empty(), "optional capture refused valid input"); + std::ofstream(path, std::ios::binary | std::ios::trunc).put('x'); + Reject([&] { capture.WriteOptional(directory.path); }, "optional capture swallowed an existing damaged record"); + Require(AgcDriver::ComputePipelineCapture::OptionalEnabled() && std::filesystem::file_size(path) == 1u, "damaged record disabled optional capture or was overwritten"); + Require(std::filesystem::remove(path), "cannot remove owned damaged record"); + Reject([&] { capture.WriteOptional(directory.path); }, "optional capture swallowed an incomplete existing record"); + Require(AgcDriver::ComputePipelineCapture::OptionalEnabled() && !std::filesystem::exists(path), "incomplete record disabled optional capture or was replaced"); + Require(std::filesystem::create_directory(path), "cannot create owned nonregular capture"); + Reject([&] { capture.WriteOptional(directory.path); }, "optional capture swallowed a nonregular existing capture"); + Require(AgcDriver::ComputePipelineCapture::OptionalEnabled() && std::filesystem::is_directory(path), "nonregular capture disabled optional capture or was changed"); + std::filesystem::remove_all(path.parent_path()); + std::ofstream(path.parent_path(), std::ios::binary).put('x'); + Reject([&] { capture.WriteOptional(directory.path); }, "optional capture swallowed a non-directory existing record"); + Require(AgcDriver::ComputePipelineCapture::OptionalEnabled() && std::filesystem::file_size(path.parent_path()) == 1u, "non-directory record disabled optional capture or was changed"); + Directory unexpected; + const auto finder = unexpected.path / ".DS_Store"; + std::ofstream(finder, std::ios::binary).put('x'); + capture.specialization[0].value = 1u; + Require(!capture.Write(unexpected.path).empty(), "strict capture rejected a Finder metadata file"); + capture.specialization[0].value = 2u; + Require(!capture.WriteOptional(unexpected.path).empty() && AgcDriver::ComputePipelineCapture::OptionalEnabled(), "optional capture rejected a Finder metadata file"); + Require(std::filesystem::file_size(finder) == 1u, "Finder metadata file was changed"); + capture.specialization[0].value = 3u; + const auto foreign = unexpected.path / "foreign"; + std::ofstream(foreign, std::ios::binary).put('x'); + RejectWithMessage([&] { capture.Write(unexpected.path); }, "capture directory contains an unexpected entry"); + Reject([&] { capture.WriteOptional(unexpected.path); }, "optional capture swallowed an unexpected directory entry"); + Require(AgcDriver::ComputePipelineCapture::OptionalEnabled() && std::filesystem::file_size(foreign) == 1u, "unexpected entry disabled optional capture or was changed"); +} + +} + +int main(int argc, char** argv) { try { + if (argc == 2) { + const std::string_view mode = argv[1]; + if (mode == "--optional-entry-cap") CheckOptionalEntryCap(); + else if (mode == "--optional-byte-cap") CheckOptionalByteCap(); + else if (mode == "--optional-io") CheckOptionalIo(); + else if (mode == "--optional-invalid") CheckOptionalInvalid(); + else throw std::runtime_error("unknown optional capture test mode"); + std::puts("optional compute capture policy passed"); + return 0; + } + Require(argc == 1, "unknown capture test arguments"); CheckRoundTrip(); CheckMalformedRecords(); CheckBindingFlags(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp index 38131c019d..06d73e6578 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 @@ -28,6 +29,122 @@ bool foldsTo(std::uint32_t word, bool expected, bool maskedShift) { return result->HasImmediate() && result->ImmediateBool() == expected; } +bool masksFold(std::uint32_t inner, std::uint32_t outer) { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& sum = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(1u), &lane}); + auto& first = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&sum, &builder.Constant(inner)}); + auto& second = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&first, &builder.Constant(outer)}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&first})); + auto& user = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&second}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + const IrValue* result = user.Argument(0)->Resolve(); + if ((inner & ~outer) == 0u) return result == &first; + return result->Opcode() == IrOpcode::BitwiseAnd32 && result->Argument(0)->Resolve() == &sum && result->Argument(1)->Resolve()->ImmediateU32() == outer; +} + +bool mergedHalfMask() { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& sum = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(1u), &lane}); + auto& low = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&sum, &builder.Constant(0xffffu)}); + auto& high = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&lane, &builder.Constant(16u)}); + auto& merged = builder.Emit(IrOpcode::BitwiseOr32, IrType::U32, {&low, &high}); + auto& masked = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&merged, &builder.Constant(0x7fffu)}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&low})); + auto& user = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&masked}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + const IrValue* result = user.Argument(0)->Resolve(); + return result->Opcode() == IrOpcode::BitwiseAnd32 && result->Argument(0)->Resolve() == ∑ +} + +bool halfExtractions() { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& low = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(3u), &lane}); + auto& high = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(5u), &lane}); + auto& lowHalf = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&low, &builder.Constant(0xffffu)}); + auto& highHalf = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&high, &builder.Constant(16u)}); + auto& merged = builder.Emit(IrOpcode::BitwiseOr32, IrType::U32, {&lowHalf, &highHalf}); + auto& top = builder.Emit(IrOpcode::ShiftRightLogical32, IrType::U32, {&merged, &builder.Constant(16u)}); + auto& field = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&merged, &builder.Constant(16u), &builder.Constant(16u)}); + auto& bottom = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&merged, &builder.Constant(0u), &builder.Constant(16u)}); + auto& topUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&top}); + auto& fieldUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&field}); + auto& bottomUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&bottom}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + const IrValue* topResult = topUser.Argument(0)->Resolve(); + const IrValue* fieldResult = fieldUser.Argument(0)->Resolve(); + const IrValue* bottomResult = bottomUser.Argument(0)->Resolve(); + const bool topMasked = topResult->Opcode() == IrOpcode::BitwiseAnd32 && topResult->Argument(0)->Resolve() == &high && topResult->Argument(1)->Resolve()->ImmediateU32() == 0xffffu; + const bool fieldThrough = fieldResult->Opcode() == IrOpcode::BitFieldUExtract && fieldResult->Argument(0)->Resolve() == &high && fieldResult->Argument(1)->Resolve()->ImmediateU32() == 0u; + const bool bottomThrough = bottomResult->Opcode() == IrOpcode::BitFieldUExtract && bottomResult->Argument(0)->Resolve() == &low; + return topMasked && fieldThrough && bottomThrough; +} + +bool halvesRejoin() { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& word = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(7u), &lane}); + auto& low = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&word, &builder.Constant(0u), &builder.Constant(16u)}); + auto& high = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&word, &builder.Constant(16u), &builder.Constant(16u)}); + auto& raised = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&high, &builder.Constant(16u)}); + auto& joined = builder.Emit(IrOpcode::BitwiseOr32, IrType::U32, {&low, &raised}); + auto& shifted = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&word, &builder.Constant(16u)}); + auto& back = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&shifted, &builder.Constant(16u), &builder.Constant(16u)}); + auto& joinedUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&joined}); + auto& backUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&back}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + const IrValue* rejoined = joinedUser.Argument(0)->Resolve(); + const IrValue* field = backUser.Argument(0)->Resolve(); + const bool fieldBack = field->Opcode() == IrOpcode::BitFieldUExtract && field->Argument(0)->Resolve() == &word && field->Argument(1)->Resolve()->ImmediateU32() == 0u; + return rejoined == &word && fieldBack; +} + +bool factorFieldKept(bool multiplied) { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& sum = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(1u), &lane}); + auto& masked = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&sum, &builder.Constant(0xffffu)}); + auto& field = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&masked, &builder.Constant(0u), &builder.Constant(24u)}); + auto& used = multiplied ? builder.IMul(field, field) : field; + auto& user = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&used}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + const IrValue* result = user.Argument(0)->Resolve(); + if (!multiplied) return result == &masked; + return result->Opcode() == IrOpcode::IMul32 && result->Argument(0)->Resolve() == &field && field.Opcode() == IrOpcode::BitFieldUExtract && field.Argument(0)->Resolve() == &masked; +} + bool reversesTo(std::uint32_t word, std::uint32_t expected) { IrProgram program; auto& block = program.CreateBlock(); @@ -44,6 +161,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,11 +217,31 @@ 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)); passed &= run("a reversed mixed word must fold to its mirror image", reversesTo(0x12345678u, 0x1e6a2c48u)); passed &= run("a reversed descriptor dword must fold to its mirror image", reversesTo(0xdeadbeefu, 0xf77db57bu)); + passed &= run("a repeated mask folds into the first one", masksFold(0xffffu, 0xffffu)); + passed &= run("a narrower mask reads through the wider one", masksFold(0xffffu, 0x7fffu)); + passed &= run("a wider mask after a narrower one is dropped", masksFold(0x00ffu, 0xffffu)); + passed &= run("a mask under a merge of the halves reads through it", mergedHalfMask()); + passed &= run("half extractions read the half that was merged in", halfExtractions()); + passed &= run("the two halves of one word rejoin into the word", halvesRejoin()); + passed &= run("a 24-bit field read of a value masked to 16 bits folds into the value", factorFieldKept(false)); + passed &= run("a 24-bit field read that a multiply uses is kept, even for a factor masked to 16 bits", factorFieldKept(true)); return passed ? 0 : 1; } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); diff --git a/core/libs/prx/libSceAgcDriver/tests/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/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index ef18897afc..3febba4f40 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -270,13 +270,13 @@ void verifyRequest(const char* path) { std::printf("%s: structured with %zu added blocks, %zu cloned instructions, %zu route variables\n", path, result.addedBlocks, result.clonedInstructions, result.routeVariables); } -std::size_t recompile(const std::string& name, std::span code, std::uint32_t subgroupSize) { +std::size_t recompile(const std::string& name, std::span code, std::uint32_t subgroupSize, std::uint32_t waveSize = 64u) { const std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu}; const std::array capabilities{1u, 61u}; const std::array extensions{"SPV_KHR_storage_buffer_storage_class"}; RecompileRequest request{}; request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; - request.context.waveSize = 64; + request.context.waveSize = waveSize; request.context.userDataBaseRegister = 0; request.context.userData = userData; request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; @@ -296,6 +296,26 @@ std::size_t recompile(const std::string& name, std::span co return result.spirv.size(); } +void verifyCallLinkOverwrittenByWave64Compare() { + const std::vector code{0xbb080007u, 0xd4060008u, 0x0001e500u, 0xd5010001u, 0x0021e880u, 0xe0700000u, 0x80010100u, 0xbf810000u, 0xbe802008u, 0xbf9f0000u}; + static_cast(recompile("a wave64 compare that overwrites a call's link pair before a v_cndmask reads it", code, 64u)); +} + +void verifyCallLinkMaskReadByScalarInstruction() { + const std::array readers{0xbe8a2408u, 0xbe8a0408u, 0xbe8a0309u}; + for (const auto reader : readers) { + const std::vector code{0xbb080007u, 0xd4060008u, 0x0001e500u, reader, 0xbf800000u, 0xe0700000u, 0x80010100u, 0xbf810000u, 0xbe802008u, 0xbf9f0000u}; + static_cast(recompile("a wave64 scalar read of a compare mask in a call's link pair", code, 64u)); + bool refused = false; + try { + static_cast(recompile("a wave32 scalar read of a call's return address", code, 32u, 32u)); + } catch (const std::exception& error) { + refused = std::string_view(error.what()).find("escapes the constant-offset call/return model") != std::string_view::npos; + } + require(refused, "a wave32 program reading a call's return address high half was not refused"); + } +} + void verifyLargeLoopExitFallback() { const std::vector code{ 0x060202ffu, 0x3f800000u, 0x060202ffu, 0x3f000000u, 0x060202ffu, 0x3f001000u, 0x060202ffu, 0x3f002000u, @@ -471,6 +491,31 @@ void verifyLongBranch(const std::string& name, std::span co require(source != graph.blocks.end() && source->terminator.kind == TerminatorKind::Branch && graph.FindBlock(source->terminator.trueBlock).startProgramCounter == target, name + ": the jump does not branch to " + std::to_string(target)); } +void verifyScheduledJumpTable() { + const std::vector code{ + 0x83888208u, 0x7e020280u, 0x8f088308u, 0x7e040280u, 0xbe8c1f00u, 0x800cff0cu, 0x00000048u, + 0x820d800du, 0xf4040386u, 0x10000000u, 0xbf8cc07fu, 0xbe901f00u, 0x80100e10u, 0x82110f11u, + 0xbe802010u, 0x7e020281u, 0xbf820003u, 0x7e020282u, 0xbf820001u, 0x7e020283u, 0xe0700000u, + 0x80000100u, 0xbf810000u, 0x0000000cu, 0u, 0x00000014u, 0u, 0x0000001cu, 0u, + }; + verifyJumpTable("scheduled two-dword table", code, 0x20u, {0xcu, 0x14u, 0x1cu}, {0x3cu, 0x44u, 0x4cu}); + static_cast(recompile("scheduled two-dword table", code, 32u)); + static_cast(recompile("scheduled two-dword table", code, 64u)); + auto vcc = patched(code, 0, {0x83ea826au}); + vcc = patched(std::move(vcc), 2, {0x8f6a836au}); + vcc = patched(std::move(vcc), 9, {0xd4000000u}); + verifyJumpTable("scheduled two-dword table through vcc", vcc, 0x20u, {0xcu, 0x14u, 0x1cu}, {0x3cu, 0x44u, 0x4cu}); + static_cast(recompile("scheduled two-dword table through vcc", vcc, 32u)); + static_cast(recompile("scheduled two-dword table through vcc", vcc, 64u)); + const std::vector> refused{ + patched(code, 1, {0xbe880380u}), + patched(code, 3, {0xbe880380u}), + patched(code, 1, {0xbf820001u}), + patched(code, 3, {0xbf850000u}), + }; + for (const auto& rejected : refused) require(refusal(rejected).find("unsupported dynamic s_setpc_b64") != std::string::npos, "a rewritten or conditional two-dword table index was accepted"); +} + void verifyNullSwappc() { const std::array jump{0xbefd210cu, 0xbf810000u}; const auto swappc = RdnaInstructionDecoder{}.Decode(jump).instructions.front(); @@ -510,7 +555,7 @@ void VerifyUserDataTail() { require(!FindUserDataTail(decoded, 9u, 2u) && !FindUserDataTail(decoded, 8u, 1u), "tail outside user data was accepted"); const std::vector> refused{ {0xbe880380u, 0xbe802008u, 0xbf9f0000u}, - {0xbe890380u, 0xbe802008u, 0xbf9f0000u}, + {0xbe890300u, 0xbe802008u, 0xbf9f0000u}, {0xbe880480u, 0xbe802008u, 0xbf9f0000u}, {0xf4080200u, 0xfa000000u, 0xbe802008u, 0xbf9f0000u}, {0xbf820000u, 0xbe802008u, 0xbf9f0000u}, @@ -533,6 +578,43 @@ void VerifyUserDataTail() { require(refusedPc, "a continuation reading its relocated PC was accepted"); } +void VerifyConstantHighTail() { + const RdnaInstructionDecoder decoder; + const std::vector> accepted{ + {0xbe850382u, 0xbe802004u, 0xbf9f0000u}, + {0xbe8503ffu, 0x12345678u, 0xbe802004u, 0xbf9f0000u}, + {0xbe850380u, 0xbe802004u, 0xbf9f0000u}, + {0xbe850300u, 0xbe850382u, 0xbe802004u, 0xbf9f0000u}, + }; + const std::array values{2u, 0x12345678u, 0u, 2u}; + const std::vector end{0xbf810000u}; + for (std::size_t index = 0; index < accepted.size(); ++index) { + const auto program = decoder.Decode(accepted[index]); + const auto tail = FindUserDataTail(program, 0u, 5u); + require(tail && tail->userDataRegister == 4u && tail->constantHigh == values[index], "constant high half was not resolved without a sixth user SGPR"); + const auto linked = LinkUserDataTail(program, *tail, decoder.Decode(end)); + require(std::equal(accepted[index].begin(), accepted[index].end() - 2u, linked.begin()), "constant high tail changed preceding scalar operations"); + auto mismatched = *tail; + mismatched.constantHigh = values[index] ^ 1u; + bool rejected = false; + try { + static_cast(LinkUserDataTail(program, mismatched, decoder.Decode(end))); + } catch (const std::invalid_argument&) { + rejected = true; + } + require(rejected, "linker accepted a stale high-half proof"); + } + const std::vector> refused{ + {0xbe850382u, 0xbe850300u, 0xbe802004u, 0xbf9f0000u}, + {0xbe850382u, 0x80058105u, 0xbe802004u, 0xbf9f0000u}, + {0xbe850382u, 0xbe840380u, 0xbe802004u, 0xbf9f0000u}, + {0xbe850382u, 0xf4000140u, 0xfa000000u, 0xbe802004u, 0xbf9f0000u}, + {0xbf840001u, 0xbe850382u, 0xbe802004u, 0xbf9f0000u}, + }; + for (const auto& code : refused) require(!FindUserDataTail(decoder.Decode(code), 0u, 6u), "unknown, overwritten or conditional tail address was accepted"); + require(!FindUserDataTail(decoder.Decode(accepted.front()), 0u, 4u), "tail low half outside user data was accepted"); +} + int main(int argc, char** argv) { try { verifyLoopMergeDoesNotEnterNestedSelection(); @@ -1332,7 +1414,11 @@ done: } try { verifyNullSwappc(); + verifyScheduledJumpTable(); VerifyUserDataTail(); + VerifyConstantHighTail(); + verifyCallLinkOverwrittenByWave64Compare(); + verifyCallLinkMaskReadByScalarInstruction(); verifyLargeLoopExitFallback(); verifyNestedExitForwarders(); verifyIrreducibleLoopEntry(); 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/DepthRangeRemap.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp new file mode 100644 index 0000000000..8452a2ae1a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/DepthRangeRemap.cpp @@ -0,0 +1,166 @@ +#include "Recompiler.hpp" +#include "PipelineSpecialization.hpp" +#include "Optimization/ResourceProgram.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct ModuleFacts { + std::map specializationDefaults; + std::uint32_t clipDistanceLength = 0; + bool clipDistanceBuiltIn = false; + std::size_t multiplies = 0; + bool pushConstantVariable = false; + std::set pushDwordLoads; +}; + +ModuleFacts inspect(std::span words) { + ModuleFacts facts; + std::map specIds; + std::map specValues; + std::map constants; + std::map arrayLengths; + std::map pointerTargets; + std::map variableTypes; + std::set clipVariables; + std::set pushVariables; + std::vector pushIndexIds; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + const auto count = words[at] >> 16u; + if (op == spv::OpDecorate && count == 4 && words[at + 2] == spv::DecorationSpecId) specIds[words[at + 1]] = words[at + 3]; + if (op == spv::OpDecorate && count == 4 && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == spv::BuiltInClipDistance) clipVariables.insert(words[at + 1]); + if (op == spv::OpSpecConstant && count == 4) specValues[words[at + 2]] = words[at + 3]; + if (op == spv::OpConstant && count == 4) constants[words[at + 2]] = words[at + 3]; + if (op == spv::OpTypeArray && count == 4) arrayLengths[words[at + 1]] = words[at + 3]; + if (op == spv::OpTypePointer && count == 4) pointerTargets[words[at + 1]] = words[at + 3]; + if (op == spv::OpVariable && count >= 4) variableTypes[words[at + 2]] = words[at + 1]; + if (op == spv::OpVariable && count >= 4 && words[at + 3] == spv::StorageClassPushConstant) pushVariables.insert(words[at + 2]); + if (op == spv::OpAccessChain && count == 6 && pushVariables.contains(words[at + 3])) pushIndexIds.push_back(words[at + 5]); + if (op == spv::OpFMul) ++facts.multiplies; + } + for (const auto& [id, specId] : specIds) { + if (const auto value = specValues.find(id); value != specValues.end()) facts.specializationDefaults[specId] = value->second; + } + facts.pushConstantVariable = !pushVariables.empty(); + for (const auto id : pushIndexIds) { + if (const auto value = constants.find(id); value != constants.end()) facts.pushDwordLoads.insert(value->second); + } + for (const auto variable : clipVariables) { + facts.clipDistanceBuiltIn = true; + const auto pointer = variableTypes.find(variable); + if (pointer == variableTypes.end()) continue; + const auto array = pointerTargets.find(pointer->second); + if (array == pointerTargets.end()) continue; + const auto length = arrayLengths.find(array->second); + if (length == arrayLengths.end()) continue; + const auto constant = constants.find(length->second); + if (constant != constants.end()) facts.clipDistanceLength = constant->second; + } + return facts; +} + +ModuleFacts inspect(const ShaderRecompiler::RecompileResult& result) { + return inspect(result.spirv.Words()); +} + +constexpr std::array PositionExport{0xf80008cfu, 0u, 0xbf810000u}; + +ShaderRecompiler::RecompileRequest baseRequest(bool remap) { + using namespace ShaderRecompiler; + RecompileRequest request{}; + request.shader = {ShaderStage::Vertex, 0x10000u, PositionExport, 0, {}}; + request.context.waveSize = 64; + ShaderVertexStageInfo vertex{}; + vertex.depthRemapMode = 2u; + request.context.vertex = vertex; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityClipDistance}; + request.target.supportedCapabilities = capabilities; + request.target.depthRangeRemap = remap; + request.layout.pushConstantSizeBytes = ShaderRecompiler::DepthRemapPushOffsetBytes; + request.useCache = false; + return request; +} + +void require(bool condition, const char* what) { + if (!condition) throw std::runtime_error(what); +} + +void preparedTransformTests() { + using namespace ShaderRecompiler; + auto request = baseRequest(true); + request.useCache = true; + const auto handle = PrepareShader(request); + const auto materialize = [&](std::uint32_t mode) { + auto moved = request; + moved.context.vertex->depthRemapMode = mode; + const auto invocation = PreparedShaderInvocation::TryCreate(moved, handle); + require(invocation.has_value(), "the prepared vertex shader did not match a request of its own mode"); + return invocation->Materialize(*invocation->Capture(SrtRuntime{})); + }; + const auto prepared = inspect(GetPreparedArtifact(*handle).spirv.Words()); + require(prepared.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase) && !prepared.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 1u) && !prepared.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 2u), "only the depth remap mode may be a specialization constant of the prepared module"); + const auto first = materialize(2u); + require(materialize(2u)->PipelineVariantId() == first->PipelineVariantId(), "the same depth remap mode did not reuse its materialized vertex shader"); + auto other = request; + other.context.vertex->depthRemapMode = 1u; + require(!PreparedShaderInvocation::TryCreate(other, handle).has_value(), "a prepared vertex shader matched a request of another depth remap mode"); +} + +void pushedTransformTests() { + using namespace ShaderRecompiler; + const auto remapped = Recompile(baseRequest(true)); + const auto plain = Recompile(baseRequest(false)); + const auto facts = inspect(remapped); + require(!facts.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 1u) && !facts.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 2u), "the depth remap scale or offset is a specialization constant"); + require(facts.pushConstantVariable && facts.pushDwordLoads.contains(DepthRemapPushOffsetBytes / 4u) && facts.pushDwordLoads.contains(DepthRemapPushOffsetBytes / 4u + 1u), "the remapped vertex shader does not read the scale and offset from the push block"); + require(!inspect(plain).pushConstantVariable, "a vertex shader without a depth remap gained a push constant block"); + auto crowded = baseRequest(true); + crowded.layout.pushConstantSizeBytes = DepthRemapPushOffsetBytes + 4u; + bool refused = false; + try { + static_cast(Recompile(crowded)); + } catch (const std::runtime_error&) { + refused = true; + } + require(refused, "a remapped vertex shader with push data over the depth remap words was accepted"); +} + +} + +int main() { + using namespace ShaderRecompiler; + try { + const auto remapped = inspect(Recompile(baseRequest(true))); + const auto plain = inspect(Recompile(baseRequest(false))); + require(remapped.clipDistanceBuiltIn && remapped.clipDistanceLength == plain.clipDistanceLength + 2u, "the remap did not add its two clip distances"); + require(remapped.multiplies >= plain.multiplies + 2u, "the remap does not scale the clip z and offset it by w"); + require(!plain.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase) && !remapped.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 3u), "unexpected specialization constants"); + preparedTransformTests(); + pushedTransformTests(); + std::cout << "Depth range remap tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "Depth range remap test failed: " << error.what() << '\n'; + for (const bool remap : {true, false}) { + const auto facts = inspect(Recompile(baseRequest(remap))); + std::cerr << " remap=" << remap << ": spec ids"; + for (const auto& [id, value] : facts.specializationDefaults) std::cerr << ' ' << id << '=' << value; + std::cerr << "; clip builtin " << facts.clipDistanceBuiltIn << " length " << facts.clipDistanceLength << "; fmul " << facts.multiplies << '\n'; + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index deec634f74..79878c2766 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include #include #include #include @@ -10,12 +11,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; @@ -39,11 +42,20 @@ THandle newHandle() { VkFormat lastImageFormat = VK_FORMAT_UNDEFINED; std::vector createdImages; std::vector> imageCopies; +VkImage lastImage = VK_NULL_HANDLE; +std::vector clearedImages; +VkImageUsageFlags lastImageUsage = 0; +std::uint32_t imagesDestroyed = 0; +std::vector> imageReads; +std::vector> imageWrites; +std::vector destroyedImages; VKAPI_ATTR VkResult VKAPI_CALL createImage(VkDevice, const VkImageCreateInfo* info, const VkAllocationCallbacks*, VkImage* image) { lastImageFormat = info->format; + lastImageUsage = info->usage; *image = newHandle(); createdImages.push_back(*image); + lastImage = *image; return VK_SUCCESS; } @@ -98,19 +110,64 @@ VKAPI_ATTR VkResult VKAPI_CALL getFenceStatus(VkDevice, VkFence) { VKAPI_ATTR void VKAPI_CALL freeCommandBuffers(VkDevice, VkCommandPool, std::uint32_t, const VkCommandBuffer*) {} VKAPI_ATTR void VKAPI_CALL destroyFence(VkDevice, VkFence, const VkAllocationCallbacks*) {} VKAPI_ATTR void VKAPI_CALL destroyImageView(VkDevice, VkImageView, const VkAllocationCallbacks*) {} -VKAPI_ATTR void VKAPI_CALL destroyImage(VkDevice, VkImage, const VkAllocationCallbacks*) {} +VKAPI_ATTR void VKAPI_CALL destroyImage(VkDevice, VkImage image, const VkAllocationCallbacks*) { + ++imagesDestroyed; + destroyedImages.push_back(image); +} VKAPI_ATTR void VKAPI_CALL freeMemory(VkDevice, VkDeviceMemory, const VkAllocationCallbacks*) {} VKAPI_ATTR void VKAPI_CALL cmdPipelineBarrier(VkCommandBuffer, VkPipelineStageFlags, VkPipelineStageFlags, VkDependencyFlags, std::uint32_t, const VkMemoryBarrier*, std::uint32_t, const VkBufferMemoryBarrier*, std::uint32_t, const VkImageMemoryBarrier*) {} -VKAPI_ATTR void VKAPI_CALL cmdClearDepthStencilImage(VkCommandBuffer, VkImage, VkImageLayout, const VkClearDepthStencilValue*, std::uint32_t, const VkImageSubresourceRange*) {} +VKAPI_ATTR void VKAPI_CALL cmdClearDepthStencilImage(VkCommandBuffer, VkImage image, VkImageLayout, const VkClearDepthStencilValue*, std::uint32_t, const VkImageSubresourceRange*) { + clearedImages.push_back(image); +} VKAPI_ATTR void VKAPI_CALL cmdCopyImage(VkCommandBuffer, VkImage source, VkImageLayout, VkImage destination, VkImageLayout, std::uint32_t count, const VkImageCopy* regions) { for (std::uint32_t i = 0; i < count; ++i) imageCopies.emplace_back(source, destination, regions[i].dstSubresource.aspectMask); } +VKAPI_ATTR void VKAPI_CALL 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; } +VKAPI_ATTR VkResult VKAPI_CALL sampledDepthProperties(VkPhysicalDevice, VkFormat, VkImageType, VkImageTiling, VkImageUsageFlags, VkImageCreateFlags, VkImageFormatProperties* properties) { + *properties = {}; + properties->maxExtent = {16384, 16384, 1}; + properties->maxArrayLayers = 1; + properties->maxMipLevels = 1; + properties->sampleCounts = VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT | VK_SAMPLE_COUNT_8_BIT; + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL attachmentOnlyProperties(VkPhysicalDevice, VkFormat, VkImageType, VkImageTiling, VkImageUsageFlags, VkImageCreateFlags, VkImageFormatProperties*) { + return VK_ERROR_FORMAT_NOT_SUPPORTED; +} + VKAPI_ATTR void VKAPI_CALL formatPropertiesWithoutD16S8(VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) { *properties = {}; if (format != VK_FORMAT_D16_UNORM_S8_UINT) properties->optimalTilingFeatures = VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; @@ -142,7 +199,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; @@ -156,6 +221,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) { @@ -166,8 +234,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; } @@ -176,6 +245,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; @@ -227,6 +327,94 @@ void TestD16StencilFallback(const Context& context) { Require(threw, "a constant 16-bit depth bias on D32_SFLOAT_S8_UINT storage must throw"); } +void TestMultisampleDepthOwnership(Context context) { + Require(MultisampledDepthBytes_nid_no_patch({3840, 2160}, 4, 8) == 0xff00000u, "the eight-sample D32 guard must cover its complete footprint"); + for (const auto bytes : {2u, 4u}) { + for (const auto samples : {2u, 4u, 8u}) { + Require(MultisampledDepthBytes_nid_no_patch({1, 1}, bytes, samples) == 65536u, "a small native depth guard must occupy one sample-aware block"); + } + } + context.imageFormatProperties = attachmentOnlyProperties; + DepthTarget target{0x71000000, 0, {129, 67}, VK_FORMAT_D32_SFLOAT, 0.0f, 0, 0, false, 4}; + Require(DepthSurfaceView(context, target) != VK_NULL_HANDLE, "lack of sampled MSAA support must retain attachment rendering"); + Require((lastImageUsage & VK_IMAGE_USAGE_SAMPLED_BIT) == 0, "unsupported sampled usage must not be requested for a native depth attachment"); + ClearDepthSurfaces(context.device); + context.imageFormatProperties = sampledDepthProperties; + Require(DepthSurfaceView(context, target) != VK_NULL_HANDLE, "a supported sampled MSAA format must retain attachment rendering without import eligibility"); + Require((lastImageUsage & VK_IMAGE_USAGE_SAMPLED_BIT) == 0, "a native MSAA depth attachment must keep sampled usage off when the shadow path is available"); + ClearDepthSurfaces(context.device); + const auto destroyed = imagesDestroyed; + auto owner = std::make_shared(context, newHandle(), newHandle()); + auto texture = std::make_shared(context, owner->image, VK_FORMAT_D32_SFLOAT, VK_IMAGE_ASPECT_DEPTH_BIT, VkComponentMapping{}, VK_IMAGE_VIEW_TYPE_2D, 0, owner); + owner.reset(); + Require(imagesDestroyed == destroyed, "a depth image must stay alive while its sampled view is held"); + texture.reset(); + Require(imagesDestroyed == destroyed + 1u, "the last sampled view must release its native depth image exactly once"); +} + +void TestMultisampleStencilAliases(Context context) { + context.imageFormatProperties = sampledDepthProperties; + for (const auto format : {VK_FORMAT_D16_UNORM_S8_UINT, VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT_S8_UINT}) { + for (const auto samples : {2u, 4u, 8u}) { + DepthTarget target{0x73000000, 0x73800000, {1920, 1080}, format, 1.0f, 0}; + target.samples = samples; + Require(DepthSurfaceView(context, target) != VK_NULL_HANDLE, "a multisampled stencil alias fixture must retain attachment rendering"); + for (const auto colorFormat : {FormatR11G11B10Float, FormatR8G8B8A8Unorm, FormatR32Float}) { + Require(lookup(context, {target.stencilAddress, colorFormat, 1920, 1080, TileRenderTarget64KB}) == nullptr, "a non-stencil color format at a multisampled stencil address must remain reused memory"); + } + const auto depthFormat = format == VK_FORMAT_D16_UNORM_S8_UINT ? FormatR16Unorm : FormatR32Float; + for (const auto view : {View{target.stencilAddress, FormatR8Uint, 1920, 1080}, View{target.address, depthFormat, 1920, 1080}}) { + bool refused = false; + try { + lookup(context, view); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("only by recorded draws and dispatches") != std::string_view::npos; + } + Require(refused, "a genuine multisample stencil or depth reader must retain its recording requirement"); + } + ClearDepthSurfaces(context.device); + } + } +} + +} + +void TestRetiredSurfaceRelease(const Context& context) { + ClearDepthSurfaces(context.device); + constexpr VkExtent2D extent{256, 256}; + const auto bytes = DepthSliceBytes(extent, 4u); + const auto destroyed = [](VkImage image) { return std::find(destroyedImages.begin(), destroyedImages.end(), image) != destroyedImages.end(); }; + constexpr std::uint64_t moving = 0x90000000; + std::vector images; + for (std::uint64_t frame = 0; frame < 4; ++frame) { + DepthSurfaceView(context, {moving + frame * 0x100000u, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}); + images.push_back(lastImage); + RetireDepthSurfaces(context.device, moving + frame * 0x100000u, bytes); + } + const auto released = DepthSurfacesReleased(); + DepthSurfaceView(context, {moving + 4u * 0x100000u, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}); + Require(std::all_of(images.begin(), images.end(), destroyed), "a retired depth surface must free its image once a new surface is created"); + Require(!destroyed(lastImage), "the new depth surface must keep its image"); + Require(DepthSurfacesReleased() > released, "freeing retired depth surfaces must be counted for the framebuffer cache"); + + constexpr std::uint64_t sampled = 0x98000000; + DepthSurfaceView(context, {sampled, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}); + const auto sampledImage = lastImage; + auto view = lookup(context, {sampled, FormatR32Float, 256, 256}); + Require(view != nullptr, "an R32F view of a D32 surface's own extent must sample its depth plane"); + RetireDepthSurfaces(context.device, sampled, bytes); + DepthSurfaceView(context, {sampled + 0x100000u, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}); + Require(!destroyed(sampledImage), "a retired depth surface must keep its image while its sampled view is held"); + view.reset(); + DepthSurfaceView(context, {sampled + 0x200000u, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}); + Require(destroyed(sampledImage), "a retired depth surface must free its image once its sampled view is released"); + + constexpr std::uint64_t kept = 0x9c000000; + DepthSurfaceView(context, {kept, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}); + const auto keptImage = lastImage; + DepthSurfaceView(context, {kept + 0x100000u, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}); + Require(!destroyed(keptImage), "a depth surface that was not retired must keep its image"); + ClearDepthSurfaces(context.device); } void RunDepthSurfaceReuseTests() { @@ -241,6 +429,9 @@ void RunDepthSurfaceReuseTests() { context.limits.maxFramebufferHeight = 16384; context.limits.maxFramebufferLayers = 1; 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; @@ -259,6 +450,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"); @@ -307,6 +500,30 @@ void RunDepthSurfaceReuseTests() { imageCopies.clear(); const auto otherWithA = pairing(otherDepth, stencilA); Require(imageCopies.size() == 1 && imageCopies[0] == std::make_tuple(withA, otherWithA, static_cast(VK_IMAGE_ASPECT_STENCIL_BIT)), "a stencil plane paired with another depth plane must carry its stencil over"); + { + 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}; + first.htileAddress = htile; + DepthTarget second{arrayBase + DepthSliceBytes(extent, 4u), 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + second.htileAddress = htile + sliceHtile; + DepthSurfaceView(context, first); + const auto firstImage = lastImage; + DepthSurfaceView(context, second); + const auto secondImage = lastImage; + const auto clearsAfter = [&](std::size_t bytes) { + clearedImages.clear(); + NoteHtileDepthClear(htile, static_cast(bytes)); + DepthSurfaceView(context, first); + DepthSurfaceView(context, second); + return clearedImages; + }; + Require(clearsAfter(sliceHtile) == std::vector{firstImage}, "an HTILE clear of the first slice must clear that slice alone"); + Require(clearsAfter(2u * sliceHtile) == std::vector{firstImage, secondImage}, "an HTILE clear over both slices must clear both"); + } + TestRetiredSurfaceRelease(context); ClearDepthSurfaces(context.device); } 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/DeviceFault.cpp b/core/libs/prx/libSceAgcDriver/tests/DeviceFault.cpp new file mode 100644 index 0000000000..67475a41de --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/DeviceFault.cpp @@ -0,0 +1,124 @@ +#include "prx/libSceAgcDriver/Execution/include/DeviceFault.hpp" +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +int faultCalls = 0; +bool faultFails = false; + +VKAPI_ATTR VkResult VKAPI_CALL MockFaultInfo(VkDevice, VkDeviceFaultCountsEXT* counts, VkDeviceFaultInfoEXT* info) { + ++faultCalls; + if (faultFails) return VK_ERROR_DEVICE_LOST; + if (info == nullptr) { + counts->addressInfoCount = 2; + counts->vendorInfoCount = 1; + counts->vendorBinarySize = 4096; + return VK_SUCCESS; + } + Require(counts->vendorBinarySize == 0, "the vendor binary was requested"); + Require(counts->addressInfoCount == 2 && info->pAddressInfos != nullptr, "the address array was not passed"); + Require(counts->vendorInfoCount == 1 && info->pVendorInfos != nullptr, "the vendor array was not passed"); + std::memset(info->description, 'd', sizeof(info->description)); + std::memcpy(info->description, "page fault", 10); + info->description[10] = '\0'; + info->pAddressInfos[0] = {VK_DEVICE_FAULT_ADDRESS_TYPE_READ_INVALID_EXT, 0x1234000ull, 0xfffull}; + info->pAddressInfos[1] = {VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_FAULT_EXT, 0xabcdull, 0x3ull}; + std::memset(info->pVendorInfos[0].description, 'v', sizeof(info->pVendorInfos[0].description)); + info->pVendorInfos[0].vendorFaultCode = 0x77ull; + info->pVendorInfos[0].vendorFaultData = 0x88ull; + return VK_SUCCESS; +} + +std::uint32_t checkpointCount = 0; + +VKAPI_ATTR void VKAPI_CALL MockCheckpointData(VkQueue, std::uint32_t* count, VkCheckpointDataNV* data) { + if (data == nullptr) { + *count = checkpointCount; + return; + } + Require(*count == checkpointCount, "the checkpoint array has the wrong size"); + for (std::uint32_t i = 0; i < *count; ++i) { + Require(data[i].sType == VK_STRUCTURE_TYPE_CHECKPOINT_DATA_NV, "checkpoint entries were not initialised"); + data[i].stage = i == 0 ? VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT : VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; + data[i].pCheckpointMarker = reinterpret_cast(static_cast(0x10000ull + i * 0x100ull)); + } +} + +VkDevice FakeDevice() { + return reinterpret_cast(static_cast(0x1000)); +} + +VkQueue FakeQueue() { + return reinterpret_cast(static_cast(0x2000)); +} + +void FaultInfo() { + faultCalls = 0; + faultFails = false; + const auto lines = AgcDriver::DeviceFaultReport(FakeDevice(), MockFaultInfo, VK_NULL_HANDLE, nullptr); + Require(faultCalls == 2, "the fault info was not queried for counts and then for data"); + Require(lines.size() == 4, "the report does not have a result, two addresses and a vendor line"); + Require(lines[0] == "[device-fault] result 0: page fault", "the description line changed"); + Require(lines[1] == "[device-fault] read invalid at 0x1234000 (precision 0xfff)", "the read fault line changed"); + Require(lines[2] == "[device-fault] instruction pointer fault at 0xabcd (precision 0x3)", "the instruction pointer line changed"); + Require(lines[3] == "[device-fault] vendor: " + std::string(VK_MAX_DESCRIPTION_SIZE, 'v') + " (code 0x77, data 0x88)", "an unterminated vendor description was not bounded"); +} + +void FaultInfoFails() { + faultCalls = 0; + faultFails = true; + const auto lines = AgcDriver::DeviceFaultReport(FakeDevice(), MockFaultInfo, VK_NULL_HANDLE, nullptr); + Require(faultCalls == 1, "a failed count query was followed by a data query"); + Require(lines.size() == 1 && lines[0] == "[device-fault] vkGetDeviceFaultInfoEXT reported nothing", "a failed query was not reported"); +} + +void Checkpoints() { + checkpointCount = 2; + auto lines = AgcDriver::DeviceFaultReport(VK_NULL_HANDLE, nullptr, FakeQueue(), MockCheckpointData); + Require(lines.size() == 2, "each checkpoint gets one line"); + Require(lines[0] == "[device-fault] checkpoint: program 0x10000 at stage 0x1", "the first checkpoint line changed"); + Require(lines[1] == "[device-fault] checkpoint: program 0x10100 at stage 0x2000", "the second checkpoint line changed"); + checkpointCount = 0; + lines = AgcDriver::DeviceFaultReport(VK_NULL_HANDLE, nullptr, FakeQueue(), MockCheckpointData); + Require(lines.size() == 1 && lines[0] == "[device-fault] no checkpoint reached", "an empty checkpoint list was not reported"); +} + +void Nothing() { + faultCalls = 0; + Require(AgcDriver::DeviceFaultReport(FakeDevice(), nullptr, FakeQueue(), nullptr).empty(), "a device without either extension reported something"); + Require(faultCalls == 0, "the fault info was queried without the extension"); +} + +void AddressKinds() { + Require(std::strcmp(AgcDriver::DeviceFaultAddressKind(VK_DEVICE_FAULT_ADDRESS_TYPE_NONE_EXT), "none") == 0, "none"); + Require(std::strcmp(AgcDriver::DeviceFaultAddressKind(VK_DEVICE_FAULT_ADDRESS_TYPE_WRITE_INVALID_EXT), "write invalid") == 0, "write invalid"); + Require(std::strcmp(AgcDriver::DeviceFaultAddressKind(VK_DEVICE_FAULT_ADDRESS_TYPE_EXECUTE_INVALID_EXT), "execute invalid") == 0, "execute invalid"); + Require(std::strcmp(AgcDriver::DeviceFaultAddressKind(VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_UNKNOWN_EXT), "instruction pointer unknown") == 0, "instruction pointer unknown"); + Require(std::strcmp(AgcDriver::DeviceFaultAddressKind(VK_DEVICE_FAULT_ADDRESS_TYPE_INSTRUCTION_POINTER_INVALID_EXT), "instruction pointer invalid") == 0, "instruction pointer invalid"); + Require(std::strcmp(AgcDriver::DeviceFaultAddressKind(static_cast(99)), "unknown") == 0, "an unknown kind"); +} + +} + +int main() { + try { + FaultInfo(); + FaultInfoFails(); + Checkpoints(); + Nothing(); + AddressKinds(); + } catch (const std::exception& error) { + std::fprintf(stderr, "agc_device_fault: %s\n", error.what()); + return 1; + } + std::puts("agc_device_fault: passed"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp b/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp index 2ed8ec1ba9..a5ff02af5d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp @@ -53,6 +53,16 @@ void Choose(const char* what, bool budgetExtension, std::uint64_t budget, std::u Expect(type == expected, std::string(what) + ": chose memory type " + std::to_string(type) + ", expected " + std::to_string(expected)); } +void RefusedFallbackTests() { + const auto context = mockContext(true); + const std::uint32_t both = (1u << HostType) | (1u << DeviceType); + const auto refused = context.RefusedDeviceMemoryFallback(both, DeviceType); + Expect(refused.has_value() && *refused == HostType, "a device-local type out of device memory did not fall back to the host type"); + Expect(!context.RefusedDeviceMemoryFallback(both, HostType).has_value(), "a host type out of memory was given a fallback"); + Expect(!context.RefusedDeviceMemoryFallback(1u << DeviceType, DeviceType).has_value(), "a fallback was chosen outside the allowed type mask"); + Expect(!context.RefusedDeviceMemoryFallback(both, 7).has_value(), "an unknown memory type was given a fallback"); +} + void BufferPlacementTests() { constexpr auto coherent = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; Context context = mockContext(true); @@ -106,6 +116,7 @@ int main() { Choose("usage already over the budget", true, 2952 * MiB, 3633 * MiB, both, HostType); Choose("no host type allowed", true, 3000 * MiB, 3000 * MiB, 1u << DeviceType, DeviceType); BufferPlacementTests(); + RefusedFallbackTests(); } catch (const std::exception& error) { std::fprintf(stderr, "FAIL: %s\n", error.what()); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/DrawInputsInPlace.cpp b/core/libs/prx/libSceAgcDriver/tests/DrawInputsInPlace.cpp new file mode 100644 index 0000000000..5a329bb0db --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/DrawInputsInPlace.cpp @@ -0,0 +1,32 @@ +#include "prx/libSceAgcDriver/Execution/include/DrawInputsInPlace.hpp" +#include + +namespace { + +struct Case { + VkDriverId driver; + bool windows; + bool inPlace; +}; + +constexpr Case Cases[]{ + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, true, false}, + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, false, true}, + {VK_DRIVER_ID_AMD_PROPRIETARY, true, true}, + {VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS, true, true}, + {VK_DRIVER_ID_MESA_RADV, false, true}, + {VK_DRIVER_ID_MESA_LLVMPIPE, false, true}, +}; + +} + +int main() { + int failures = 0; + for (const auto& item : Cases) { + if (AgcDriver::DrawInputsInPlaceUsable(item.driver, item.windows) != item.inPlace) { + std::fprintf(stderr, "driver %d on %s: expected draw inputs %s\n", static_cast(item.driver), item.windows ? "Windows" : "another host", item.inPlace ? "in place" : "copied"); + ++failures; + } + } + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/DrawProgramTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/DrawProgramTrace.cpp new file mode 100644 index 0000000000..bedff70e8c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/DrawProgramTrace.cpp @@ -0,0 +1,41 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawProgramTrace.hpp" +#include +#include +#include + +namespace { + +void Check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void EmptyTrace() { + const auto trace = AgcDriver::DriverDetail::FormatDrawProgramTrace(12, 3, {}); + Check(trace == "[draw-programs] submission=12 queue=0x3 count=0", "empty draw trace changed shape"); +} + +void ProgramAddresses() { + using namespace AgcDriver::DriverDetail; + using ShaderRecompiler::ProgramRole; + using ShaderRecompiler::ShaderStage; + const DrawProgramTraceEntry entries[]{ + {ShaderStage::Vertex, ProgramRole::Main, 0x100000, 0x100040}, + {ShaderStage::Fragment, ProgramRole::Fragment, 0x200000, 0x200020}, + }; + const auto trace = FormatDrawProgramTrace(42, 0x9, entries); + Check(trace == "[draw-programs] submission=42 queue=0x9 count=2 vertex/main=0x100000(entry=0x100040) fragment/fragment=0x200000(entry=0x200020)", + "draw trace omitted the selected shader code or entry address"); +} + +} + +int main() { + try { + EmptyTrace(); + ProgramAddresses(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/EmbeddedVertexFetch.cpp b/core/libs/prx/libSceAgcDriver/tests/EmbeddedVertexFetch.cpp index 46d1e6a891..5a1fddb472 100644 --- a/core/libs/prx/libSceAgcDriver/tests/EmbeddedVertexFetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/EmbeddedVertexFetch.cpp @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include using namespace ShaderRecompiler; @@ -35,6 +37,54 @@ int Expect(const char* name, std::uint32_t source, std::uint32_t field, std::uin return 1; } +int NoCarryOffsets() { + const std::array code{ + 0x4a0a0a10u, 0x4a101011u, + 0xf4080506u, 0xfa000000u, + 0x02060b08u, + 0xbf8cc07fu, + 0xe0002000u, 0x80050a03u, + 0xbf8c3f70u, 0xbf810000u, + }; + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + int failures = 0; + for (const auto wave : {32u, 64u}) { + const auto analyze = [&](const RdnaProgram& program) { return EmbeddedVertexFetchAnalyzer{}.Analyze(program, 6, 4, 8, 16, wave); }; + const auto plan = analyze(decoded); + if (plan.loads.size() != 1 || plan.vertexOffsetSgpr != 16 || plan.instanceOffsetSgpr != 17 || plan.vertexOffsetShared || plan.instanceOffsetShared || plan.vertexIndexObserved || plan.instanceIndexObserved) { + std::fprintf(stderr, "wave%u: no-carry offsets: loads=%zu vertex=%d instance=%d shared=%d/%d observed=%d/%d\n", wave, plan.loads.size(), plan.vertexOffsetSgpr, plan.instanceOffsetSgpr, plan.vertexOffsetShared, plan.instanceOffsetShared, plan.vertexIndexObserved, plan.instanceIndexObserved); + ++failures; + } + const auto rejected = [&](const char* label, std::initializer_list instruction) { + std::vector words(instruction); + words.insert(words.end(), code.begin() + 1, code.end()); + if (analyze(RdnaInstructionDecoder{}.Decode(words)).vertexOffsetSgpr == -1) return; + std::fprintf(stderr, "wave%u: %s was admitted as a no-carry offset\n", wave, label); + ++failures; + }; + rejected("VOP3 saturation", {0xd5258005u, 0x00020a10u}); + rejected("SDWA byte source", {0x4a0a0af9u, 0x06800610u}); + rejected("vector offset", {0x4a0a0b10u}); + rejected("different index source", {0x4a0a0c10u}); + rejected("non-user scalar", {0x4a0a0a1fu}); + rejected("subtraction", {0x4c0a0a10u}); + auto sharedCode = std::vector(code.begin(), code.end()); + sharedCode.insert(sharedCode.end() - 1, 0x7e080210u); + const auto shared = analyze(RdnaInstructionDecoder{}.Decode(sharedCode)); + if (shared.vertexOffsetSgpr != 16 || !shared.vertexOffsetShared) { + std::fprintf(stderr, "wave%u: a second reader of the offset was not marked shared\n", wave); + ++failures; + } + auto observedCode = std::vector(code.begin(), code.end()); + observedCode.insert(observedCode.begin(), 0x7e020305u); + if (!analyze(RdnaInstructionDecoder{}.Decode(observedCode)).vertexIndexObserved) { + std::fprintf(stderr, "wave%u: a read before the offset add was not preserved\n", wave); + ++failures; + } + } + return failures; +} + } int main() { @@ -43,5 +93,6 @@ int main() { failures += Expect("width 8 at offset 4", 0xfff00320u, 0x00080004u, 0x32u); failures += Expect("width 0", 0x00000320u, 0x00000000u, 0u); failures += Expect("width above 32", 0x00000320u, 0x007f0000u, 0x320u); + failures += NoCarryOffsets(); return failures == 0 ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/F16SingleConvert.cpp b/core/libs/prx/libSceAgcDriver/tests/F16SingleConvert.cpp new file mode 100644 index 0000000000..affffdd32c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/F16SingleConvert.cpp @@ -0,0 +1,93 @@ +#include "BdaAbi.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void Check(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +SpirvTarget Target(bool nativeFloat16) { + static constexpr std::array plain{ + spv::CapabilityInt64, spv::CapabilityFloat64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance, + }; + static constexpr std::array native{ + spv::CapabilityInt64, spv::CapabilityFloat64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance, + spv::CapabilityFloat16, spv::CapabilityRoundingModeRTE, spv::CapabilityDenormPreserve, + }; + static constexpr std::array extensions{ + "SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer", + }; + if (nativeFloat16) return {0x00401000u, 0x00010300u, 32, BdaAbi::Version, native, extensions, false, {1024, 1024, 64}, 1024, 32768, {}, {}}; + return {0x00401000u, 0x00010300u, 32, BdaAbi::Version, plain, extensions, false, {1024, 1024, 64}, 1024, 32768, {}, {}}; +} + +std::size_t ZeroPairs(std::span words) { + Check(words.size() >= 5 && words[0] == spv::MagicNumber, "f16 single convert: missing SPIR-V module"); + std::unordered_set floats; + std::unordered_set pairs; + std::unordered_set zeros; + std::size_t count = 0; + for (std::size_t offset = 5; offset < words.size();) { + const auto length = words[offset] >> spv::WordCountShift; + Check(length != 0 && length <= words.size() - offset, "f16 single convert: malformed SPIR-V instruction"); + const auto opcode = words[offset] & spv::OpCodeMask; + if (opcode == spv::OpTypeFloat && length == 3u && words[offset + 2u] == 32u) floats.insert(words[offset + 1u]); + if (opcode == spv::OpTypeVector && length == 4u && floats.contains(words[offset + 2u]) && words[offset + 3u] == 2u) pairs.insert(words[offset + 1u]); + if (opcode == spv::OpConstant && length == 4u && floats.contains(words[offset + 1u]) && words[offset + 3u] == 0u) zeros.insert(words[offset + 2u]); + if (opcode == spv::OpConstantNull && length == 3u && floats.contains(words[offset + 1u])) zeros.insert(words[offset + 2u]); + if (opcode == spv::OpCompositeConstruct && length == 5u && pairs.contains(words[offset + 1u]) && zeros.contains(words[offset + 4u])) ++count; + offset += length; + } + return count; +} + +std::size_t Compile(bool nativeFloat16) { + std::vector code{0xe0300000u, 0x80010100u, 0xbf8c3f70u}; + for (std::uint32_t op = 0; op < 4u; ++op) code.push_back(0x7e04a900u | (256u + (op == 0u ? 1u : 2u))); + code.insert(code.end(), {0xe0700000u, 0x80010200u, 0xbf810000u}); + const std::span words(code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; + const std::array userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}; + request.target = Target(nativeFloat16); + request.layout = {0, 0, 0, 128}; + 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.context.memory = memory; + request.useCache = false; + return ZeroPairs(Recompile(request).spirv.Words()); +} + +} + +int main() { + try { + for (const bool nativeFloat16 : {false, true}) { + const auto pairs = Compile(nativeFloat16); + Check(pairs == 0u, std::string("f16 single convert: four v_rcp_f16 emitted ") + std::to_string(pairs) + " (x, 0.0) f32 pairs to convert one f16 each" + (nativeFloat16 ? " with native f16" : " without native f16")); + } + std::puts("f16 single convert 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/F32DenormPreserve.cpp b/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp new file mode 100644 index 0000000000..0f3fe85da7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp @@ -0,0 +1,95 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +constexpr std::array CvtF16F32Store{0x7e021500u, 0xe0700000u, 0x80000100u, 0xbf810000u}; + +std::vector recompile(bool preserve, bool flush, bool flushCapability = true) { + const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityDenormPreserve, spv::CapabilityDenormFlushToZero}; + const std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu}; + const std::array extensions{"SPV_KHR_storage_buffer_storage_class", "SPV_KHR_float_controls"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, CvtF16F32Store, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 64; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = std::span(capabilities).first(flushCapability ? capabilities.size() : capabilities.size() - 1); + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.target.float32DenormPreserve = preserve; + request.target.float32FlushToZero = flush; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + auto result = Recompile(request); + require(!result.spirv.empty(), "the shader did not recompile"); + return result.spirv.Copy(); +} + +struct Modes { + bool preserve32 = false; + bool flush32 = false; + bool preserveCapability = false; +}; + +Modes scan(std::span words) { + Modes modes; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto op = static_cast(words[cursor] & 0xffffu); + require(count != 0 && cursor + count <= words.size(), "truncated SPIR-V instruction"); + if (op == spv::OpCapability && words[cursor + 1] == spv::CapabilityDenormPreserve) modes.preserveCapability = true; + if (op == spv::OpExecutionMode && count == 4 && words[cursor + 3] == 32u) { + if (words[cursor + 2] == spv::ExecutionModeDenormPreserve) modes.preserve32 = true; + if (words[cursor + 2] == spv::ExecutionModeDenormFlushToZero) modes.flush32 = true; + } + cursor += count; + } + return modes; +} + +void verify() { + const auto preserved = scan(recompile(true, false)); + require(preserved.preserve32 && preserved.preserveCapability && !preserved.flush32, "float32DenormPreserve did not emit the 32-bit DenormPreserve execution mode and capability"); + const auto plain = scan(recompile(false, false)); + require(!plain.preserve32 && !plain.flush32, "a target without float32DenormPreserve or float32FlushToZero set a 32-bit denormal execution mode"); + const auto flushed = scan(recompile(true, true)); + require(flushed.flush32 && !flushed.preserve32, "the hardware 32-bit flush did not take precedence over the preserve flag"); + const auto nativeFlush = scan(recompile(true, true, false)); + require(!nativeFlush.preserve32 && !nativeFlush.flush32, "a hardware flush without the DenormFlushToZero capability declared a 32-bit denormal execution mode"); +} + +} + +int main() { + try { + verify(); + } catch (const std::exception& error) { + std::fprintf(stderr, "F32DenormPreserve: %s\n", error.what()); + return 1; + } + std::puts("F32DenormPreserve: the 32-bit DenormPreserve execution mode follows the target flag and yields to the hardware flush"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp index c09560589b..ec73dea01a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp @@ -9,12 +9,28 @@ #include #include #include +#include +#include +#include #include #include #include #include #include #include +#ifdef _WIN32 +#include +#define APS5_DUP _dup +#define APS5_DUP2 _dup2 +#define APS5_CLOSE _close +#define APS5_FILENO _fileno +#else +#include +#define APS5_DUP dup +#define APS5_DUP2 dup2 +#define APS5_CLOSE close +#define APS5_FILENO fileno +#endif namespace { @@ -120,7 +136,6 @@ void testFlipAndBoundary() { expectFailure([&] { AgcDriverRegisterVideoOutput_nid_postfix(7, output); }); std::array words{0xc004105c, 7, 0xfffffffeu, 1, 0, 0}; Packet packet{words.data(), 6, 0, {}}; - expectFailure([&] { sceAgcDriverSubmitAcb(0x20, &packet); }); words[0] = 0xc004105d; expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }); words[0] = 0xc004105c; @@ -306,6 +321,18 @@ void testComputeRenderingWait() { AgcDriverUnregisterVideoOutput_nid_postfix(7, output); } +void testComputeFlip() { + auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + std::array words{0xc004105c, 7, 0xfffffffeu, 1, 0x76543211u, 0xfedcba98u}; + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + check(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "compute flip submission failed"); + AgcDriverWaitIdle_nid_postfix(); + check(output->state->ready == 1 && output->state->failed == 0, "a flip submitted on a compute queue was not handed to its output"); + check(output->state->last.argument == -0x123456789abcdefLL && output->state->last.index == -2 && output->state->last.mode == 1, "decoded compute flip arguments changed"); + AgcDriverUnregisterVideoOutput_nid_postfix(7, output); +} + void testFailure() { auto output = std::make_shared(); output->state->fail = true; @@ -323,6 +350,40 @@ void testFailure() { AgcDriverUnregisterVideoOutput_nid_postfix(7, output); } +void testFailureLog() { + const auto path = std::filesystem::temp_directory_path() / ("aps5_worker_failure_log_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); + std::fflush(stderr); + const int saved = APS5_DUP(APS5_FILENO(stderr)); + check(saved >= 0, "stderr could not be saved"); + check(std::freopen(path.string().c_str(), "wb", stderr) != nullptr, "stderr could not be redirected"); + std::string message; + std::string error; + try { + auto output = std::make_shared(); + output->state->fail = true; + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + submitFlip(); + message = expectFailure([] { AgcDriverWaitIdle_nid_postfix(); }); + check(expectFailure([] { submitFlip(); }) == message, "the stored failure was not rethrown on the next driver call"); + AgcDriverUnregisterVideoOutput_nid_postfix(7, output); + } catch (const std::exception& caught) { + error = caught.what(); + } + std::fflush(stderr); + APS5_DUP2(saved, APS5_FILENO(stderr)); + APS5_CLOSE(saved); + std::string captured; + { + std::ifstream file(path, std::ios::binary); + captured.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + } + std::filesystem::remove(path); + if (!error.empty()) throw std::runtime_error(error); + check(message == "intentional flip failure", "the worker failure was not rethrown"); + check(captured.find("[gpu] driver failure: intentional flip failure\n") != std::string::npos, "the worker failure was not printed"); + check(captured.find("driver failure") == captured.rfind("driver failure"), "the worker failure was printed more than once"); +} + void testReset(bool compute) { std::array registers{0xc0027600, 0x20c, 1, 0}; Packet packet{registers.data(), 4, 0, {}}; @@ -365,8 +426,15 @@ int main(int argc, char** argv) { std::puts("present mode selection tests passed"); return 0; } + if (argc == 2 && std::string(argv[1]) == "failure-log") { + testFailureLog(); + const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); }); + check(shutdown == "intentional flip failure", "shutdown lost worker failure"); + std::puts("AGC worker failure log test passed"); + return 0; + } if (argc == 2) testReset(std::string(argv[1]) == "compute"); - else { testFlipAndBoundary(); testFlipHold(); testComputeRenderingWait(); testFailure(); } + else { testFlipAndBoundary(); testFlipHold(); testComputeRenderingWait(); testComputeFlip(); testFailure(); } const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); }); check(shutdown.find(argc == 2 ? (std::string(argv[1]) == "compute" ? "not readable" : "required shader register") : "intentional flip failure") != std::string::npos, "shutdown lost worker failure"); std::puts("AGC flip and suspend tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp b/core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp new file mode 100644 index 0000000000..f51d28a2bf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp @@ -0,0 +1,37 @@ +#include "prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp" +#include + +namespace { + +struct Case { + VkDriverId driver; + VkBool32 flushToZero; + VkShaderFloatControlsIndependence independence; + bool probe; + bool declared; +}; + +constexpr Case Cases[]{ + {VK_DRIVER_ID_MOLTENVK, VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE, true, false}, + {VK_DRIVER_ID_MOLTENVK, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, true, true}, + {VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, true, true}, + {VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_32_BIT_ONLY, true, true}, + {VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE, false, false}, + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, false, false}, + {VK_DRIVER_ID_AMD_PROPRIETARY, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_32_BIT_ONLY, true, true}, + {static_cast(0), VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE, false, false}, +}; + +} + +int main() { + int failures = 0; + for (const auto& item : Cases) { + const auto control = AgcDriver::Float32FlushControlFor(item.driver, item.flushToZero, item.independence); + if (control.probe != item.probe || control.declared != item.declared) { + std::fprintf(stderr, "driver %d flush %u independence %d: expected probe %d declared %d, got %d %d\n", static_cast(item.driver), static_cast(item.flushToZero), static_cast(item.independence), item.probe ? 1 : 0, item.declared ? 1 : 0, control.probe ? 1 : 0, control.declared ? 1 : 0); + ++failures; + } + } + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/FmaMixOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FmaMixOnce.cpp new file mode 100644 index 0000000000..aa48f4f1e7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/FmaMixOnce.cpp @@ -0,0 +1,93 @@ +#include "BdaAbi.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void Check(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +SpirvTarget Target() { + static constexpr std::array capabilities{ + spv::CapabilityInt64, spv::CapabilityFloat64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance, + }; + static constexpr std::array extensions{ + "SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer", + }; + return {0x00401000u, 0x00010300u, 32, BdaAbi::Version, capabilities, extensions, false, + {1024, 1024, 64}, 1024, 32768, {}, {}}; +} + +struct Counts { + std::size_t words = 0; + std::size_t functions = 0; +}; + +Counts Count(std::span words) { + Check(words.size() >= 5 && words[0] == spv::MagicNumber, "fma mix once: missing SPIR-V module"); + Counts counts{words.size()}; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + Check(count != 0 && count <= words.size() - offset, "fma mix once: malformed SPIR-V instruction"); + if ((words[offset] & spv::OpCodeMask) == spv::OpFunction) ++counts.functions; + offset += count; + } + return counts; +} + +Counts Compile(std::uint32_t uses) { + std::vector code; + for (std::uint32_t load = 0; load < 3u; ++load) code.insert(code.end(), {0xe0300000u | (load * 4u), 0x80010000u | ((1u + load) << 8u)}); + code.push_back(0xbf8c3f70u); + std::uint32_t result = 1u; + for (std::uint32_t use = 0; use < uses; ++use) { + const std::uint32_t opcode = use % 2u == 0u ? 0x21u : 0x22u; + code.insert(code.end(), {0xcc000000u | (opcode << 16u) | 10u, (256u + result) | ((256u + 2u) << 9u) | ((256u + 3u) << 18u)}); + result = 10u; + } + code.insert(code.end(), {0xe0700000u, 0x80010000u | (result << 8u)}); + code.push_back(0xbf810000u); + const std::span words(code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; + const std::array userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}; + request.target = Target(); + request.layout = {0, 0, 0, 128}; + 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.context.memory = memory; + request.useCache = false; + return Count(Recompile(request).spirv.Words()); +} + +} + +int main() { + try { + const auto none = Compile(0); + const auto one = Compile(1); + const auto three = Compile(3); + Check(three.words - one.words < one.words - none.words, "fma mix once: 2 more v_fma_mixlo/mixhi_f16 added " + std::to_string(three.words - one.words) + " SPIR-V words, as many as the first one with its shared emulation (" + std::to_string(one.words - none.words) + ")"); + Check(one.functions == none.functions + 1u, "fma mix once: one v_fma_mixlo_f16 emitted " + std::to_string(one.functions - none.functions) + " functions more than none, expected one shared emulation function"); + Check(three.functions == one.functions, "fma mix once: two more v_fma_mixlo/mixhi_f16 in the same float mode emitted " + std::to_string(three.functions - one.functions) + " more functions, expected calls to the shared one"); + std::printf("fma mix once tests passed (%zu words without v_fma_mix, %zu for 1, %zu for 3)\n", none.words, one.words, three.words); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp index 5cd2842612..0eba15d628 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp @@ -104,11 +104,12 @@ int main() { const auto storeThree = Compile(3, true); Check(storeOne.functions == none.functions + 2 && storeOne.calls == none.calls + 2, "formatted buffer once: a formatted load and a formatted store to another resource emitted " + std::to_string(storeOne.functions - none.functions) + " functions and " + std::to_string(storeOne.calls - none.calls) + " calls more than none, expected one function and one call each"); Check(storeThree.functions == storeOne.functions && storeThree.calls == storeOne.calls + 4 && storeThree.switches == storeOne.switches, "formatted buffer once: two more formatted loads and stores emitted " + std::to_string(storeThree.functions - storeOne.functions) + " functions, " + std::to_string(storeThree.calls - storeOne.calls) + " calls and " + std::to_string(storeThree.switches - storeOne.switches) + " switches more, expected four calls only"); - Check(three.words - one.words < one.words - none.words, "formatted buffer once: 2 more formatted loads added " + std::to_string(three.words - one.words) + " SPIR-V words, as many as the first one with its shared function (" + std::to_string(one.words - none.words) + ")"); + Check(three.words - one.words < (one.words - none.words) / 2u, "formatted buffer once: 2 more formatted loads added " + std::to_string(three.words - one.words) + " SPIR-V words, as many as the first one with its shared function (" + std::to_string(one.words - none.words) + ")"); const auto gpuThree = Compile(3, false, true); Check(gpuThree.functions == gpuOne.functions && gpuThree.calls > gpuOne.calls, "formatted buffer once: two more formatted loads from a GPU-selected descriptor emitted " + std::to_string(gpuThree.functions - gpuOne.functions) + " more functions, expected calls to the shared one"); Check(gpuThree.switches == gpuOne.switches, "formatted buffer once: two more formatted loads from a GPU-selected descriptor emitted " + std::to_string(gpuThree.switches - gpuOne.switches) + " more format switches"); Check(gpuThree.words - gpuOne.words < gpuOne.words / 10u, "formatted buffer once: 2 more formatted loads from a GPU-selected descriptor added " + std::to_string(gpuThree.words - gpuOne.words) + " SPIR-V words to a " + std::to_string(gpuOne.words) + "-word module"); + Check(gpuOne.words < 40000u, "formatted buffer once: a formatted load from a GPU-selected descriptor takes " + std::to_string(gpuOne.words) + " SPIR-V words, expected under 40000 since every driver that inlines the shared function copies all of it"); std::printf("formatted buffer once tests passed (%zu words for 1 load, %zu for 3; GPU-selected %zu and %zu; vertex %zu specialized, %zu registration)\n", one.words, three.words, gpuOne.words, gpuThree.words, vertexSpecialized.words, vertexRegistration.words); return 0; } catch (const std::exception& error) { 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/GcnDecode.cpp b/core/libs/prx/libSceAgcDriver/tests/GcnDecode.cpp new file mode 100644 index 0000000000..855c0aecdb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/GcnDecode.cpp @@ -0,0 +1,45 @@ +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +int failures = 0; + +void Check(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s\n", what); + ++failures; +} + +RdnaInstruction Decode(std::span words) { + return DecodeGcnInstruction(0u, words, 0u); +} + +void SetregImmKeepsItsLiteral() { + const std::array setreg{0xba80f801u, 0x12345678u, 0xbf810000u}; + const auto instruction = Decode(setreg); + Check(instruction.op == RdnaOpcode::SSetregImm32B32, "s_setreg_imm32_b32 did not decode as its RDNA opcode"); + Check(instruction.wordCount == 2u, "a GCN s_setreg_imm32_b32 is not two words"); + Check(instruction.source0.value == 0x12345678u, "s_setreg_imm32_b32 lost its literal"); + const std::array movk{0xb0001234u, 0xbf810000u}; + Check(Decode(movk).wordCount == 1u, "s_movk_i32 took a literal word"); +} + +} + +int main() { + try { + SetregImmKeepsItsLiteral(); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } + if (failures == 0) std::puts("GCN decode tests passed"); + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 89a5761f97..4b05f827f4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2,19 +2,24 @@ #include "GraphicsTests.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/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" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "ShaderDiskCache.hpp" +#include "ShaderCacheDirectory.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "SceShaders.hpp" #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" #include "ControlFlow/RequestSerializer.hpp" @@ -24,17 +29,31 @@ #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/GuestHeap.hpp" +#include "ImageTableAbi.hpp" +#include "SpirvBackend/SpirvSpecialization.hpp" #include +#ifdef _WIN32 +#include +#else +#include +#endif #include #include +#include #include #include +#include #include +#include #include #include #include +#include +#include +#include #include #include +#include #include #include #include @@ -61,7 +80,7 @@ AgcDriver::QueueState makeState() { queue.userConfig[0x242] = 4; queue.context = { {0x2d5, 0x2000}, - {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x1b6, 0}, {0x1b8, 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}, @@ -109,7 +128,7 @@ void inactiveVertexPointSizeTests() { queue.context[0x207] = 0x01210000; return queue; }; - for (const auto primitive : {2u, 4u, 5u, 6u, 7u, 17u}) { + for (const auto primitive : {2u, 4u, 5u, 6u, 7u, 17u, 19u, 21u}) { auto queue = pointSizeState(); queue.userConfig[0x242] = primitive; for (const auto raster : {0u, 0x240u, 0x248u}) { @@ -144,9 +163,15 @@ void inactiveVertexPointSizeTests() { } throw std::runtime_error("potentially active point size passed the state decoder"); }; - for (const auto primitive : {1u, 9u}) { + { auto queue = pointSizeState(); - queue.userConfig[0x242] = primitive; + queue.userConfig[0x242] = 9; + rejected(queue); + } + { + auto queue = pointSizeState(); + queue.userConfig[0x242] = 1; + queue.context[0x2d5] = 0x2020; rejected(queue); } for (const auto stages : {0x2004u, 0x2020u}) { @@ -186,6 +211,132 @@ void inactiveVertexPointSizeTests() { rejected(queue); } +void PolygonPrimitiveTests() { + auto queue = makeState(); + queue.userConfig[0x242] = 21; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "a polygon did not decode to a triangle fan"); + Require(!state.primitiveRestart, "polygon decoding enabled primitive restart by default"); + Require(AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Triangle, "a polygon was not classified as a triangle primitive"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected a direct polygon draw"); + queue.userConfig[0x24b] = 1; + queue.context[0x103] = 0xffffffffu; + Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "the precheck rejected an indexed polygon draw with restart"); + const auto indexedState = AgcDriver::Graphics::DecodeState(queue); + Require(indexedState.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN && indexedState.primitiveRestart, "indexed polygon decoding did not keep primitive restart"); +} + +void vertexPointListTests() { + const auto pointState = [](std::uint32_t control, std::uint32_t size, std::uint32_t limits) { + auto queue = makeState(); + queue.userConfig[0x242] = 1; + queue.context[0x1b3] = queue.context[0x1b4] = 2; + queue.context[0x207] = control; + queue.context[0x280] = size; + queue.context[0x281] = limits; + return queue; + }; + { + const auto queue = pointState(0x01210000u, 0x00080008u, 0x00080008u); + const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); + Require(rejection.empty(), "a vertex-path point list with a vertex point size failed the draw precheck: " + rejection); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST && state.paClVsOutCntl == 0x01210000u, "a vertex-path point list with a vertex point size did not decode as a point list"); + Require(AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Point, "a vertex-path point list is not rasterized as points"); + Require(state.vertexPoints && state.pointSize == 1.0f && state.pointSizeMin == 1.0f && state.pointSizeMax == 1.0f, "PA_SU_POINT_SIZE and PA_SU_POINT_MINMAX of 8 did not decode as one-pixel points"); + ShaderRecompiler::ShaderVertexStageInfo vertex{}; + vertex.paClVsOutCntl = 1u << 21u; + AgcDriver::Graphics::ApplyPointSize(state, vertex); + Require(vertex.pointList && vertex.paClVsOutCntl == ((1u << 16u) | (1u << 21u)) && vertex.pointSize == 1.0f && vertex.pointSizeMin == 1.0f && vertex.pointSizeMax == 1.0f, "the vertex stage did not receive the vertex point size"); + } + { + const auto state = AgcDriver::Graphics::DecodeState(pointState(0, 0x00200020u, 0x00100004u)); + Require(state.vertexPoints && state.pointSize == 4.0f && state.pointSizeMin == 0.5f && state.pointSizeMax == 2.0f, "a register point size did not decode as diameters"); + ShaderRecompiler::ShaderVertexStageInfo vertex{}; + vertex.paClVsOutCntl = 1u << 21u; + AgcDriver::Graphics::ApplyPointSize(state, vertex); + Require(vertex.pointList && vertex.paClVsOutCntl == (1u << 21u) && vertex.pointSize == 4.0f, "a register point size turned on the vertex point size"); + } + { + const auto state = AgcDriver::Graphics::DecodeState(pointState(0x01210000u, 0x00200010u, 0x00400000u)); + Require(state.pointSizeMin == 0.0f && state.pointSizeMax == 8.0f, "a vertex point size refused a register size whose width and height differ"); + } + { + auto queue = makeState(); + queue.context[0x1b3] = queue.context[0x1b4] = 2; + queue.context[0x207] = 0x01210000u; + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(!state.vertexPoints, "a triangle draw decoded as vertex points"); + ShaderRecompiler::ShaderVertexStageInfo vertex{}; + AgcDriver::Graphics::ApplyPointSize(state, vertex); + Require(!vertex.pointList && vertex.paClVsOutCntl == 0, "a triangle draw received a point size"); + } + expectFailure([&] { AgcDriver::Graphics::DecodeState(pointState(0, 0x00200010u, 0x00080008u)); }, "points whose PA_SU_POINT_SIZE width and height differ are unsupported"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(pointState(0, 0x00080008u, 0x00080010u)); }, "PA_SU_POINT_MINMAX with a minimum above its maximum is unsupported"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(pointState(1u << 16u, 0x00080008u, 0x00080008u)); }, "USE_VTX_POINT_SIZE without VS_OUT_MISC_VEC_ENA is unsupported"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(pointState(0, 0, 0x00080008u)); }, "points of size 0 (PA_SU_POINT_SIZE 0) are unsupported"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(pointState(0x01210000u, 0x00080008u, 0)); }, "a vertex point size clamped to 0 (PA_SU_POINT_MINMAX maximum 0) is unsupported"); + { + const auto state = AgcDriver::Graphics::DecodeState(pointState(0x01210000u, 0, 0x00200000u)); + Require(state.pointSize == 0.0f && state.pointSizeMin == 0.0f && state.pointSizeMax == 4.0f, "a vertex point size refused a zero register size or a zero clamp minimum"); + static_cast(AgcDriver::Graphics::DecodeState(pointState(0, 0x00080008u, 0))); + } + for (const auto sprite : {1u << 17u, 1u << 23u}) { + auto queue = pointState(0x01210000u, 0x00080008u, 0x00080008u); + queue.context[0x1b5] = 0x486bu; + queue.context[0x1b6] = 2; + queue.context[0x191] = 0; + queue.context[0x192] = sprite; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "point sprite texture coordinates (SPI_PS_INPUT_CNTL PT_SPRITE_TEX) are not implemented"); + queue.context[0x1b5] = 0x4869u; + Require(AgcDriver::Graphics::DecodeState(queue).vertexPoints, "PT_SPRITE_TEX was refused with point sprites off"); + } +} + +void retainedPixelStageTests() { + using namespace AgcDriver::Graphics; + auto queue = makeState(); + queue.context[0x8e] = queue.context[0x8f] = queue.context[0x203] = 0; + for (const auto format : {0u, 1u, 2u, 3u, 9u}) { + queue.context[0x1c4] = format; + Require(PixelProgramSkipped(queue), "a retained export format forced an unused pixel program to run"); + Require(DrawRejection(queue, true).empty(), "an unused pixel program with a retained export format was rejected"); + for (const auto control : {1u, 2u, 4u, 0x40u, 0x80u, 0x100u, 0x200u, 0x400u, 0x20000u}) { + queue.context[0x203] = control; + Require(!PixelProgramSkipped(queue), "an enabled export or execution flag was skipped"); + } + queue.context[0x203] = 0; + queue.context[0x8e] = 0xf0000000u; + queue.context[0x8f] = 0x0f000000u; + Require(PixelProgramSkipped(queue), "disjoint color masks ran an unused pixel program"); + queue.context[0x8f] = 0xf0000000u; + Require(!PixelProgramSkipped(queue), "an enabled high render target lost its pixel program"); + queue.context[0x8e] = queue.context[0x8f] = 0; + } + queue.context[0x1b3] = queue.context[0x1b4] = 2; + for (const auto [format, control] : {std::pair{1u, 1u}, std::pair{2u, 2u}, std::pair{9u, 0x100u}, std::pair{9u, 0x103u}}) { + queue.context[0x1c4] = format; + queue.context[0x203] = control; + const auto state = DecodeState(queue); + Require(DrawRejection(queue, true).empty(), "a valid pixel export combination was rejected"); + const auto pixel = DecodePixelStageInfo(queue.context, ExportMappings(state), PixelProgramSkipped(queue)); + Require(pixel.depthExportEnable == ((control & 1u) != 0) && pixel.stencilRefExportEnable == ((control & 2u) != 0) && pixel.sampleMaskExportEnable == ((control & 0x100u) != 0), "decoding lost an enabled pixel export"); + } + queue.context[0x203] = 0; + for (const auto alpha : {0xaa00u, 0xaa01u}) { + queue.context[0x2dc] = alpha; + Require(PixelProgramSkipped(queue) == ((alpha & 1u) == 0), "alpha-to-coverage enable was not respected"); + } + queue.context[0x2dc] = 0xaa00u; + for (const auto offset : {0x8eu, 0x8fu, 0x203u}) { + auto missing = queue; + missing.context.erase(offset); + Require(!PixelProgramSkipped(missing), "incomplete execution state skipped a bound pixel program"); + } +} + void hdrComponentSwapTests() { using namespace AgcDriver::Graphics; for (const auto number : {0u, 1u, 4u, 7u}) { @@ -265,20 +416,24 @@ void stateTests() { queue.context[0x103] = 0xffffffffu; Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "primitive restart was rejected for an indexed strip"); Require(AgcDriver::Graphics::DecodeState(queue).primitiveRestart, "primitive restart was not decoded for a strip"); + queue.userConfig[0x242] = 21; + Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "primitive restart was rejected for an indexed polygon"); + Require(AgcDriver::Graphics::DecodeState(queue).primitiveRestart, "primitive restart was not decoded for a polygon"); queue.context[0x103] = 5; Require(AgcDriver::Graphics::DrawRejection(queue, true).find("all ones") != std::string::npos, "a restart index other than all ones was accepted"); queue = makeState(); queue.context[0x293] = 0x06020000u; (void)AgcDriver::Graphics::DecodeState(queue); - Require(AgcDriver::Graphics::DrawRejection(queue, false).find("sample iteration") == std::string::npos, "per-engine primitive discard was rejected"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("out-of-order") == std::string::npos, "per-engine primitive discard was rejected"); queue.context[0x293] = 0x760201bcu; (void)AgcDriver::Graphics::DecodeState(queue); - Require(AgcDriver::Graphics::DrawRejection(queue, false).find("sample iteration") == std::string::npos, "an out-of-order watermark without out-of-order rasterization was rejected"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("out-of-order") == std::string::npos, "an out-of-order watermark without out-of-order rasterization was rejected"); queue.context[0x293] = 0x7e0201bcu; - Require(AgcDriver::Graphics::DrawRejection(queue, false).find("sample iteration") != std::string::npos, "out-of-order rasterization was accepted"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("out-of-order") != std::string::npos, "out-of-order rasterization was accepted"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "out-of-order"); queue.context[0x293] = 0x06030000u; - Require(AgcDriver::Graphics::DrawRejection(queue, false).find("sample iteration") != std::string::npos, "per-sample shading was accepted"); - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "sample iteration"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("out-of-order") == std::string::npos, "per-sample iteration of a single-sample target was rejected"); + Require(!AgcDriver::Graphics::DecodeState(queue).perSampleShading, "per-sample iteration of a single-sample target requested sample shading"); queue.context[0x293] = 0; for (const auto disabled : {0x6000u, 0x00100000u, 0u, 0x000e6000u}) { queue.context[0x313] = disabled; @@ -303,6 +458,12 @@ void stateTests() { queue.context[0x90] = 0x10003; state = AgcDriver::Graphics::DecodeState(queue); Require(state.scissor.offset.x == 3 && state.scissor.offset.y == 1 && state.scissor.extent.width == 29 && state.scissor.extent.height == 2, "a scissor that applies the zero window offset changed"); + queue.context[0x90] = 0x80040003; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.scissor.extent.height == 0, "a scissor whose top is below its bottom passes pixels"); + queue.context[0x90] = 0x80010021; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.scissor.extent.width == 0, "a scissor whose left edge is right of its right edge passes pixels"); queue.context[0x90] = 0x80008000; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "scissor reserved bits"); queue.context[0x90] = 0x80010003; @@ -396,14 +557,18 @@ void stateTests() { // the draw key otherwise), and nothing is recorded without a log pointer. queue = makeState(); std::vector log; - AgcDriver::Graphics::RegisterReadLog() = &log; - queue.context[0x1b3] = 2; - queue.context[0x1b4] = 2; - state = AgcDriver::Graphics::DecodeState(queue); - Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "precheck rejected the reference state"); - static_cast(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state))); - AgcDriver::Graphics::RegisterReadLog() = nullptr; + { + const AgcDriver::Graphics::RegisterReadLogScope logScope(&log); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + state = AgcDriver::Graphics::DecodeState(queue); + Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "precheck rejected the reference state"); + static_cast(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state))); + } Require(!log.empty(), "the register facade recorded nothing"); + const auto recorded = log.size(); + static_cast(AgcDriver::Graphics::DecodeState(queue)); + Require(log.size() == recorded, "the register facade recorded without an open log"); for (const auto read : log) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a register the decoders read: " + std::string(AgcDriver::Graphics::RegisterBankName(read.bank)) + " " + std::to_string(read.offset)); Require(!AgcDriver::Graphics::DrawKeyCovers({AgcDriver::Graphics::RegisterBank::Context, 0x100}) && AgcDriver::Graphics::DrawKeyCovers({AgcDriver::Graphics::RegisterBank::Shader, 0xab}) && !AgcDriver::Graphics::DrawKeyCovers({AgcDriver::Graphics::RegisterBank::Shader, 0xac}), "DrawKeyRegisters coverage changed"); log.clear(); @@ -430,6 +595,12 @@ void stateTests() { queue.shader[0x008] = 0; Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "a zero pixel program address was not read as unset"); Require(AgcDriver::Graphics::DrawRejection(queue, true).find("writes color") != std::string::npos, "a draw without a pixel program that writes color was accepted"); + auto colorDisabled = queue; + colorDisabled.context[0x202] = colorDisabled.context.at(0x202) & ~0x70u; + Require(AgcDriver::Graphics::DrawRejection(colorDisabled, true).empty(), "a draw without a pixel program was rejected while CB_COLOR_CONTROL disables color"); + Require(!AgcDriver::Graphics::DecodeState(colorDisabled).hasColorTarget, "a draw with CB_COLOR_CONTROL disabling color decoded a color target"); + colorDisabled.context.erase(0x202); + Require(AgcDriver::Graphics::DrawRejection(colorDisabled, true).find("writes color") != std::string::npos, "a draw without a pixel program and an unwritten CB_COLOR_CONTROL was accepted"); queue.shader.erase(0x008); queue.shader.erase(0x009); Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "an unwritten pixel program address was not read as unset"); @@ -458,7 +629,18 @@ void stateTests() { Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program with Z_EXPORT_ENABLE was skipped"); queue.context[0x203] = 0; queue.context[0x1c4] = 1; - Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program exporting depth was skipped"); + Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "a retained Z export format ran a pixel program with depth exports disabled"); + Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "a depth-only draw with a retained Z export format was rejected"); + for (const auto control : {1u, 2u, 4u, 0x40u, 0x80u, 0x100u, 0x200u, 0x400u, 0x20000u}) { + queue.context[0x203] = control; + Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "an enabled pixel export or execution flag was ignored with a retained Z format"); + } + queue.context[0x203] = 0; + queue.context[0x2dc] = 0xaa01; + Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "alpha-to-coverage skipped the pixel program"); + queue.context[0x203] = 0x800; + Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "the Vulkan alpha-to-coverage state lost its pixel program with DB's override set"); + queue.context[0x203] = queue.context[0x2dc] = 0; queue.context[0x1c4] = 0; queue.context[0x8e] = 0xf; Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program writing color was skipped"); @@ -539,6 +721,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; @@ -591,6 +782,169 @@ void UnwrittenSampleLocationTests() { Require(state.samples == 1 && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target with unwritten sample locations was refused"); } +void msaaEnableClearOneSampleTargetTests() { + using AgcDriver::Graphics::DecodeState; + using AgcDriver::Graphics::DrawRejection; + const auto drawC = [](std::uint32_t config = 0x0020c002u) { + auto queue = makeState(); + queue.context[0x2f8] = config; + queue.context[0x292] = 2; + for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) queue.context[offset] = (offset - 0x2feu) % 4u == 0 ? 0x622ae6aeu : 0u; + queue.context[0x31c] = 0x8028; + queue.context[0x31d] = 0; + queue.context[0x010] = 0x80000183; + queue.context[0x200] = 0x710; + queue.context[0x201] = 0x00010002; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + return queue; + }; + const auto requireOneSample = [](const AgcDriver::QueueState& queue, const std::string& what) { + const auto state = DecodeState(queue); + Require(state.samples == 1 && state.colors.size() == 1 && state.colors[0].samples == 1 && !state.depth, what + " was not drawn as one sample"); + Require(!state.customSampleLocations && !state.sampleLocationGrid && !state.perSampleShading && state.sampleLocations == decltype(state.sampleLocations){}, what + " kept the multisampled rasterizer's sample state"); + const auto rejection = DrawRejection(queue, false); + Require(rejection.empty(), "the precheck refused " + what + ": " + rejection); + }; + auto queue = drawC(); + requireOneSample(queue, "PPSA18216's draw C (a one-sample color target under a 4x PA_SC_AA_CONFIG without MSAA_ENABLE)"); + auto reference = drawC(0); + const auto state = DecodeState(queue); + const auto expected = DecodeState(reference); + Require(state.samples == expected.samples && state.customSampleLocations == expected.customSampleLocations && state.sampleLocations == expected.sampleLocations && state.perSampleShading == expected.perSampleShading && state.colors[0].format == expected.colors[0].format, "draw C decoded differently from the same draw with a one-sample PA_SC_AA_CONFIG"); + for (const auto config : {0x00100001u, 0x00306003u}) requireOneSample(drawC(config), "a one-sample color target under a 2x or 8x PA_SC_AA_CONFIG without MSAA_ENABLE"); + queue = drawC(); + queue.context[0x302] = 0xae62e62a; + requireOneSample(queue, "a one-sample color target under a 4x pattern that varies between pixels"); + for (const auto input : {ShaderRecompiler::PixelInput::PerspectiveSample, ShaderRecompiler::PixelInput::PerspectiveCentroid, ShaderRecompiler::PixelInput::LinearSample, ShaderRecompiler::PixelInput::LinearCentroid}) { + queue = drawC(); + queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(input); + requireOneSample(queue, "draw C with sample or centroid inputs"); + } + queue = drawC(); + queue.context[0x1b3] |= ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + requireOneSample(queue, "draw C with an enabled but unaddressed ANCILLARY input"); + constexpr std::string_view refused = "a one-sample color target under a multisampled rasterizer without MSAA_ENABLE is unsupported with PS_ITER_SAMPLE, a sample index or coverage input, or a sample-mask export"; + for (const auto input : {ShaderRecompiler::PixelInput::Ancillary, ShaderRecompiler::PixelInput::SampleCoverage}) { + queue = drawC(); + queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(input); + expectFailure([&] { DecodeState(queue); }, refused); + } + queue = drawC(); + queue.context[0x293] = 0x10000u; + expectFailure([&] { DecodeState(queue); }, refused); + queue = drawC(); + queue.context[0x1c4] = 9; + queue.context[0x203] |= 0x100u; + expectFailure([&] { DecodeState(queue); }, refused); + queue = drawC(); + queue.context[0x2dc] = 0x0000aa01u; + expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points or alpha-to-coverage"); + for (const auto primitive : {2u, 3u}) { + queue = drawC(); + queue.userConfig[0x242] = primitive; + requireOneSample(queue, "a line list or strip into PPSA18216's draw C target"); + } + const auto twoTargets = [&](std::uint32_t first, std::uint32_t second) { + auto pair = drawC(); + for (const auto offset : {0x318u, 0x31bu, 0x31cu}) pair.context[offset + 0xfu] = pair.context.at(offset); + for (const auto offset : {0x390u, 0x3b0u, 0x3b8u}) pair.context[offset + 1u] = pair.context.at(offset); + pair.context[0x31d] = first; + pair.context[0x32c] = second; + pair.context[0x1c5] = 0x99u; + pair.context[0x8e] = pair.context[0x8f] = 0xffu; + pair.context[0x1e1] = 0; + return pair; + }; + queue = twoTargets(0, 0); + Require(DecodeState(queue).samples == 1 && DecodeState(queue).colors.size() == 2, "two one-sample color targets under a 4x PA_SC_AA_CONFIG without MSAA_ENABLE were not drawn as one sample"); + for (const auto [first, second] : {std::pair{0u, 0x12000u}, std::pair{0x12000u, 0u}}) { + queue = twoTargets(first, second); + expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's"); + } + const auto depthTarget = [&](bool color, std::uint32_t config = 0x0020c002u) { + auto depth = drawC(config); + depth.context[0x0] = 0; + depth.context[0x002] = 0; + depth.context[0x007] = (3u << 16u) | 63u; + depth.context[0x00a] = 0; + depth.context[0x00b] = 0; + depth.context[0x011] = 0x20000180; + for (const auto offset : {0x012u, 0x014u}) depth.context[offset] = 0x100; + depth.context[0x200] = 0x16; + if (!color) depth.context[0x8e] = 0; + return depth; + }; + const auto requireOneSampleDepth = [](const AgcDriver::QueueState& queue, std::size_t colors, const std::string& what) { + const auto state = DecodeState(queue); + Require(state.samples == 1 && state.depth && state.depth->samples == 1 && state.colors.size() == colors && std::ranges::all_of(state.colors, [](const auto& color) { return color.samples == 1; }), what + " was not drawn as one sample"); + Require(!state.customSampleLocations && !state.sampleLocationGrid && !state.perSampleShading && state.sampleLocations == decltype(state.sampleLocations){}, what + " kept the multisampled rasterizer's sample state"); + const auto rejection = DrawRejection(queue, false); + Require(rejection.empty(), "the precheck refused " + what + ": " + rejection); + }; + queue = depthTarget(false); + requireOneSampleDepth(queue, 0, "PPSA18216's opening-movie draw (a one-sample depth target and no color target under a 4x PA_SC_AA_CONFIG without MSAA_ENABLE)"); + const auto movie = DecodeState(queue); + const auto movieReference = DecodeState(depthTarget(false, 0)); + Require(!movie.hasColorTarget && movie.renderExtent.width == movieReference.renderExtent.width && movie.renderExtent.height == movieReference.renderExtent.height && movie.depth->format == movieReference.depth->format && movie.sampleLocations == movieReference.sampleLocations && movie.customSampleLocations == movieReference.customSampleLocations, "the opening-movie draw decoded differently from the same draw with a one-sample PA_SC_AA_CONFIG"); + requireOneSampleDepth(depthTarget(true), 1, "a one-sample depth and color pair under a 4x PA_SC_AA_CONFIG without MSAA_ENABLE"); + queue = depthTarget(false); + queue.context[0x302] = 0xae62e62a; + requireOneSampleDepth(queue, 0, "a one-sample depth target under a 4x pattern that varies between pixels"); + for (const auto config : {0x00100001u, 0x00306003u}) { + requireOneSampleDepth(depthTarget(false, config), 0, "a one-sample depth target under a 2x or 8x PA_SC_AA_CONFIG without MSAA_ENABLE"); + requireOneSampleDepth(depthTarget(true, config), 1, "a one-sample depth and color pair under a 2x or 8x PA_SC_AA_CONFIG without MSAA_ENABLE"); + } + for (const auto input : {ShaderRecompiler::PixelInput::PerspectiveSample, ShaderRecompiler::PixelInput::PerspectiveCentroid, ShaderRecompiler::PixelInput::LinearSample, ShaderRecompiler::PixelInput::LinearCentroid}) { + queue = depthTarget(false); + queue.context[0x203] |= 0x40u; + queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(input); + requireOneSampleDepth(queue, 0, "a one-sample depth target under a 4x PA_SC_AA_CONFIG without MSAA_ENABLE with sample or centroid inputs"); + } + queue = depthTarget(false); + queue.context[0x293] = 0x10000u; + queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage); + Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "the depth-only draw did not skip its pixel program"); + requireOneSampleDepth(queue, 0, "a one-sample depth target whose skipped pixel program has PS_ITER_SAMPLE and sample index and coverage inputs"); + constexpr std::string_view depthRefused = "a one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is unsupported for points or lines and with alpha-to-coverage, a sample index or coverage input, a sample-mask export, or DB_EQAA Z interpolation or overrasterization"; + for (const bool color : {false, true}) { + for (const auto input : {ShaderRecompiler::PixelInput::Ancillary, ShaderRecompiler::PixelInput::SampleCoverage}) { + queue = depthTarget(color); + queue.context[0x203] |= 0x40u; + queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(input); + expectFailure([&] { DecodeState(queue); }, depthRefused); + } + queue = depthTarget(color); + queue.context[0x203] |= 0x40u; + queue.context[0x293] = 0x10000u; + Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "the PS_ITER_SAMPLE draw skipped its pixel program"); + requireOneSampleDepth(queue, color ? 1u : 0u, "a one-sample depth target whose running pixel program has PS_ITER_SAMPLE and no sample index or coverage input"); + Require(!DecodeState(queue).perSampleShading, "a PS_ITER_SAMPLE program drawn as one sample still shades per sample"); + queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + expectFailure([&] { DecodeState(queue); }, depthRefused); + queue = depthTarget(color); + queue.context[0x1c4] = 9; + queue.context[0x203] |= 0x100u; + expectFailure([&] { DecodeState(queue); }, depthRefused); + queue = depthTarget(color); + queue.context[0x2dc] = 0x0000aa01u; + expectFailure([&] { DecodeState(queue); }, depthRefused); + for (const auto primitive : {2u, 3u}) { + queue = depthTarget(color); + queue.userConfig[0x242] = primitive; + expectFailure([&] { DecodeState(queue); }, depthRefused); + } + for (const auto eqaa : {0x00050002u, 0x00090002u, 0x01010002u, 0x08010002u}) { + queue = depthTarget(color); + queue.context[0x201] = eqaa; + expectFailure([&] { DecodeState(queue); }, depthRefused); + } + } + queue = depthTarget(true); + queue.context[0x31d] = 0x12000; + expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's"); +} + void MsaaEnableSingleSampleTests() { using AgcDriver::Graphics::DecodeState; using AgcDriver::Graphics::DrawRejection; @@ -643,18 +997,18 @@ void MsaaEnableSingleSampleTests() { queue = enabled(); queue.context[0x313] = 0x6001; refused(queue, "MSAA_ENABLE on a single-sample target with conservative rasterization"); - for (const auto primitive : {2u, 7u, 17u}) { + for (const auto primitive : {2u, 3u}) { queue = enabled(); queue.userConfig[0x242] = primitive; - refused(queue, "primitives other than triangles"); + refused(queue, "primitives other than triangles and rectangles"); } queue = enabled(); queue.userConfig[0x242] = 1; - Require(DrawRejection(queue, false).find("primitives other than triangles") != std::string::npos, "the precheck accepted MSAA_ENABLE on a single-sample target for points"); - for (const auto primitive : {5u, 6u}) { + Require(DrawRejection(queue, false).find("primitives other than triangles and rectangles") != std::string::npos, "the precheck accepted MSAA_ENABLE on a single-sample target for points"); + for (const auto primitive : {5u, 6u, 7u, 17u, 21u}) { queue = enabled(); queue.userConfig[0x242] = primitive; - Require(DecodeState(queue).samples == 1 && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target was refused for a triangle fan or strip"); + Require(DecodeState(queue).samples == 1 && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target was refused for a filled triangle or rectangle primitive"); } queue = enabled(); queue.context[0x292] = 7; @@ -679,6 +1033,22 @@ void ShaderStageTests() { } } } + { + auto vertex = makeState(); + vertex.context[0x2d5] = 0x02002008u; + const auto state = AgcDriver::Graphics::DecodeState(vertex); + Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Vertex + && state.stages.registerValue == 0x02002008u, + "the reported vertex-routing state did not decode as vertex"); + vertex.context[0x2d5] |= 0x10u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(vertex); }, "reserved LS_EN, ES_EN or VS_EN"); + vertex.context[0x2d5] = 0x02002008u | 0x800000u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(vertex); }, "unsupported vertex routing"); + vertex.context[0x2d5] = 0x2008u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(vertex); }, "unsupported vertex routing"); + vertex.context.erase(0x2d5); + expectFailure([&] { AgcDriver::Graphics::DecodeState(vertex); }, "missing register"); + } queue = makeState(); queue.context[0x2d5] = 0x2020; queue.userConfig[0x25b] = (64u << 9u) | 21u; @@ -691,6 +1061,12 @@ void ShaderStageTests() { auto stages = AgcDriver::Graphics::DecodeState(queue).stages; Require(stages.path == AgcDriver::Graphics::ShaderPath::Geometry && stages.mesh && stages.mesh->primitivesPerGroup == 21 && stages.mesh->verticesPerGroup == 63, "geometry assembly changed"); Require(stages.mesh->maxVertices == 64 && stages.mesh->maxPrimitives == 21 && stages.mesh->threadsPerGroup == 64 && stages.mesh->esgsItemSize == 4, "geometry subgroup outputs changed"); + queue.shader[0x8a] = 0x222c0007u; + queue.shader[0x8b] = 0x00c00010u; + stages = AgcDriver::Graphics::DecodeState(queue).stages; + Require(stages.path == AgcDriver::Graphics::ShaderPath::Geometry && stages.mesh && stages.mesh->primitivesPerGroup == 21 && stages.mesh->esgsItemSize == 4, "a geometry program with fewer GS and ES input VGPRs did not decode"); + queue.shader[0x8a] = 3u << 29u; + queue.shader[0x8b] = 3u << 16u; { auto fan = makeState(); fan.userConfig[0x242] = 5; @@ -735,6 +1111,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; @@ -747,6 +1127,71 @@ 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"); + Require(mesh.provokingVertex == 0, "a first-vertex geometry draw does not take its flat inputs and layer from the first vertex of each output primitive"); + const auto raster = lines.context.at(0x205); + lines.context[0x205] = raster | (1u << 19u); + Require(AgcDriver::Graphics::DecodeState(lines).stages.mesh->provokingVertex == 2 && AgcDriver::Graphics::DecodeShaderStages(lines).mesh->provokingVertex == 2, "a last-vertex geometry draw does not take its flat inputs and layer from the third vertex of each output triangle"); + lines.context[0x29b] = 1; + Require(AgcDriver::Graphics::DecodeState(lines).stages.mesh->provokingVertex == 1, "a last-vertex geometry draw does not take its flat inputs and layer from the second vertex of each output line"); + lines.context[0x29b] = 0; + expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "missing register in context bank at DWORD 0x280"); + lines.context[0x280] = 0x00080008u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "missing register in context bank at DWORD 0x281"); + lines.context[0x281] = 0xffff0000u; + state = AgcDriver::Graphics::DecodeState(lines); + mesh = *state.stages.mesh; + Require(mesh.outputPrimitiveSize == 1 && mesh.maxPrimitives == 255 && AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Point, "a point-list subgroup did not decode as points"); + Require(mesh.provokingVertex == 0, "a point draw has provoking vertex 0"); + Require(state.pointSize == 1.0f && state.pointSizeMin == 0.0f && state.pointSizeMax == 8191.875f, "geometry points did not decode the queue's default point size and limits"); + lines.context[0x205] = raster; + auto points = lines; + points.userConfig[0x242] = 6; + points.userConfig[0x25b] = 0x5c2e; + points.context[0x1ff] = 0x8a; + points.context[0x2ce] = 3; + points.context[0x29b] = 0; + points.context[0x280] = 0x00180018u; + points.context[0x281] = 0x00400008u; + const auto pointState = AgcDriver::Graphics::DecodeState(points); + const auto& pointMesh = *pointState.stages.mesh; + ShaderRecompiler::ShaderVertexStageInfo pointInfo{}; + AgcDriver::Graphics::ApplyPointSize(pointState, pointInfo); + Require(pointInfo.pointList && pointInfo.pointSize == 3.0f && pointInfo.pointSizeMin == 1.0f && pointInfo.pointSizeMax == 8.0f, "geometry points lost their register sizes"); + Require(pointMesh.outputPrimitiveSize == 1u && pointMesh.primitivesPerGroup == 44u && pointMesh.verticesPerGroup == 46u && pointMesh.maxVertices == 138u && pointMesh.maxPrimitives == 138u && pointMesh.threadsPerGroup == 192u && AgcDriver::Graphics::RasterPrimitiveOf(pointState) == ShaderRecompiler::RasterPrimitive::Point, "triangle-strip input with point output did not decode the reported subgroup"); + 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 : {3u, 0x80000001u, 0x101u}) { + lines.context[0x29b] = output; + expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a point list or 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); }, "depth bias or nonstandard rasterization"); + } queue.context[0x2ab] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "invalid VGT_ESGS_RING_ITEMSIZE"); queue.context[0x2ab] = 4; @@ -767,20 +1212,23 @@ void ShaderStageTests() { queue.context[0x2d5] = 0x0200210d; { std::vector tessellationLog; - AgcDriver::Graphics::RegisterReadLog() = &tessellationLog; - stages = AgcDriver::Graphics::DecodeState(queue).stages; - AgcDriver::Graphics::RegisterReadLog() = nullptr; + { + const AgcDriver::Graphics::RegisterReadLogScope logScope(&tessellationLog); + stages = AgcDriver::Graphics::DecodeState(queue).stages; + } for (const auto read : tessellationLog) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a tessellation register the decoder reads: " + std::to_string(read.offset)); } Require(stages.path == AgcDriver::Graphics::ShaderPath::Tessellation && stages.tessellation, "tessellation routing changed"); const auto& tessellation = *stages.tessellation; Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2 && stages.minTessellationLevel == 1.0f && stages.maxTessellationLevel == 64.0f && tessellation.patchesPerGroup == 85 && tessellation.primitiveGroupSize == 85 && !tessellation.breakAtInstance, "the passthrough triangle-patch configuration decoded differently"); - for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "fractional-odd partitioning"}, std::pair{0x4004du, "fractional-odd partitioning"}, std::pair{0x40045u, "fractional-odd partitioning"}, std::pair{0x40048u, "fractional-odd partitioning"}, std::pair{0x40029u, "fractional-odd partitioning"}, std::pair{0x4004bu, "fractional-odd partitioning"}}) { + for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "fractional-odd partitioning"}, std::pair{0x40045u, "fractional-odd partitioning"}, std::pair{0x40048u, "fractional-odd partitioning"}, std::pair{0x40029u, "fractional-odd partitioning"}, std::pair{0x4004bu, "fractional-odd partitioning"}}) { queue.context[0x2db] = parameters; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason); } queue.context[0x2db] = 0x40041; Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->partitioning == 0, "integer-partitioned triangle patches decoded differently"); + queue.context[0x2db] = 0x4004d; + Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->partitioning == 3, "fractional-even-partitioned triangle patches decoded differently"); queue.context[0x2d6] = 0x1043f; queue.context[0x2db] = 0x4006a; queue.context[0x286] = 0x41100000; @@ -830,6 +1278,29 @@ void ShaderStageTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); } +void UnsupportedTessellationFieldTests() { + auto queue = makeState(); + queue.context[0x2d5] = 0x0200210d; + queue.userConfig[0x242] = 9; + queue.userConfig[0x25b] = 0x1fe55; + queue.context[0x286] = 0x42800000; + queue.context[0x287] = 0x3f800000; + for (const auto [config, parameters, fields] : { + std::tuple{0xc355u, 0x40045u, "VGT_TF_PARAM=0x40045 (TYPE=1 TESS_TRIANGLE, PARTITIONING=1 PART_POW2, TOPOLOGY=2 OUTPUT_TRIANGLE_CW), VGT_LS_HS_CONFIG=0xc355 (NUM_PATCHES=85, HS_NUM_INPUT_CP=3, HS_NUM_OUTPUT_CP=3)"}, + std::tuple{0x1033fu, 0x40066u, "VGT_TF_PARAM=0x40066 (TYPE=2 TESS_QUAD, PARTITIONING=1 PART_POW2, TOPOLOGY=3 OUTPUT_TRIANGLE_CCW), VGT_LS_HS_CONFIG=0x1033f (NUM_PATCHES=63, HS_NUM_INPUT_CP=3, HS_NUM_OUTPUT_CP=4)"}, + std::tuple{0xc355u, 0x40048u, "VGT_TF_PARAM=0x40048 (TYPE=0 TESS_ISOLINE, PARTITIONING=2 PART_FRAC_ODD, TOPOLOGY=2 OUTPUT_TRIANGLE_CW)"}, + std::tuple{0xc355u, 0x40029u, "(TYPE=1 TESS_TRIANGLE, PARTITIONING=2 PART_FRAC_ODD, TOPOLOGY=1 OUTPUT_LINE)"}, + std::tuple{0xc355u, 0x4000du, "(TYPE=1 TESS_TRIANGLE, PARTITIONING=3 PART_FRAC_EVEN, TOPOLOGY=0 OUTPUT_POINT)"}, + std::tuple{0xc355u, 0x4004bu, "(TYPE=3, PARTITIONING=2 PART_FRAC_ODD, TOPOLOGY=2 OUTPUT_TRIANGLE_CW)"}, + std::tuple{0xc355u, 0x400f1u, "(TYPE=1 TESS_TRIANGLE, PARTITIONING=4, TOPOLOGY=7)"}}) { + queue.context[0x2d6] = config; + queue.context[0x2db] = parameters; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "VGT_SHADER_STAGES_EN=0x200210d: VGT_TF_PARAM=0x"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, fields); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "): only triangle and quad domains with integer, fractional-even or fractional-odd partitioning and triangle output are supported by the reference path"); + } +} + void NggVertexWorkgroupTests() { auto queue = makeState(); queue.context[0x2d5] = 0x12010; @@ -846,6 +1317,7 @@ void NggVertexWorkgroupTests() { Require(AgcDriver::Graphics::DecodeState(queue).stages.path == AgcDriver::Graphics::ShaderPath::Vertex, "an NGG vertex program without a workgroup left the vertex path"); auto state = AgcDriver::Graphics::DecodeState(queue, true); Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.stages.mesh, "an NGG vertex program that needs a workgroup did not take the mesh path"); + expectFailure([&] { AgcDriver::Graphics::ValidateShaders({}, state, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false, false, false, false, false, false, false, false, false, false, false, false, false); }, "device does not support mesh shaders"); auto mesh = *state.stages.mesh; Require(mesh.inputPrimitive == 4 && mesh.primitivesPerGroup == 42 && mesh.verticesPerGroup == 126 && mesh.maxVertices == 128 && mesh.maxPrimitives == 128 && mesh.threadsPerGroup == 128 && mesh.ldsSizeDwords == 1024 && mesh.esgsItemSize == 1, "the NGG vertex subgroup of a triangle list changed"); queue.userConfig[0x242] = 6; @@ -863,6 +1335,11 @@ 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 point list or line strip from a geometry program, is unsupported"); + queue.context[0x29b] = 0; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a point list or line strip from a geometry program, is unsupported"); } constexpr std::array IdentityExports{0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}; @@ -897,6 +1374,36 @@ std::vector pixelBuiltinsRead(std::uint32_t ena, std::uint32_t add return read; } +std::size_t sampleMaskInputs(std::uint32_t inputs, std::uint16_t quadMask) { + auto queue = makeState(); + queue.context[0x1b3] = inputs; + queue.context[0x1b4] = inputs; + auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); + pixel.quadSampleMask[0] = quadMask; + const auto source = ShaderRecompiler::PixelInputVgpr(inputs, ShaderRecompiler::PixelInput::SampleCoverage); + const std::array code{0xf800180fu, source * 0x01010101u, 0xbf810000u}; + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.pixel = pixel; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.target.fragmentShaderBarycentricEnabled = true; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + const auto& words = result.spirv.Words(); + std::map storage; + std::vector sampleMasks; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpVariable) storage[words[at + 2]] = words[at + 3]; + if (op == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == spv::BuiltInSampleMask) sampleMasks.push_back(words[at + 1]); + } + return static_cast(std::ranges::count_if(sampleMasks, [&](std::uint32_t id) { return storage[id] == spv::StorageClassInput; })); +} + bool readsBuiltin(const std::vector& read, spv::BuiltIn builtin) { return std::find(read.begin(), read.end(), builtin) != read.end(); } @@ -1128,11 +1635,31 @@ void PixelInputLayoutTests() { Require(PixelInputVgpr(pixel.inputAddr, PixelInput::PerspectiveCenter) == 0u && PixelInputVgpr(pixel.inputAddr, PixelInput::PerspectiveCentroid) == 2u && PixelInputVgpr(pixel.inputAddr, PixelInput::LinearCenter) == 4u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionX) == 6u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionY) == 7u, "the centroid layout moved the inputs"); pixel = decode(0x1146u, 0x1146u); Require(pixel.linearCentroid && !pixel.noPerspective && PixelInputVgpr(pixel.inputAddr, PixelInput::LinearCentroid) == 4u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionX) == 6u && PixelInputVgpr(pixel.inputAddr, PixelInput::FrontFace) == 7u, "the linear centroid layout moved the inputs"); + Require(!pixel.frontFaceAllBits, "FRONT_FACE_ALL_BITS was decoded from a clear SPI_BARYC_CNTL"); + queue.context[0x1b8] = 1u << 24u; + Require(decode(0x1146u, 0x1146u).frontFaceAllBits, "SPI_BARYC_CNTL FRONT_FACE_ALL_BITS was not decoded"); + Require(!decode(0x326u, 0x326u).frontFaceAllBits, "FRONT_FACE_ALL_BITS was decoded for a pixel program without the front-face input"); + using ShaderRecompiler::PixelPositionLocation; + Require(decode(0x326u, 0x326u).positionLocation == PixelPositionLocation::Center, "POS_FLOAT_LOCATION 0 was not decoded as the pixel center"); + queue.context[0x1b8] = 0x10000u; + Require(decode(0x326u, 0x326u).positionLocation == PixelPositionLocation::Centroid, "POS_FLOAT_LOCATION 1 was not decoded as the centroid"); + queue.context[0x1b8] = 0x20000u; + Require(decode(0x326u, 0x326u).positionLocation == PixelPositionLocation::Sample, "POS_FLOAT_LOCATION 2 was not decoded as the iterated sample"); + queue.context[0x1b8] = 0x120000u; + Require(decode(0x326u, 0x326u).positionLocation == PixelPositionLocation::UpperLeft, "POS_FLOAT_ULC did not override POS_FLOAT_LOCATION"); + queue.context[0x1b8] = 0x30000u; + expectFailure([&] { static_cast(decode(0x326u, 0x326u)); }, "POS_FLOAT_LOCATION uses the reserved value 3"); + Require(decode(0x1002u, 0x1002u).positionLocation == PixelPositionLocation::Center, "POS_FLOAT_LOCATION was decoded for a pixel program without positions"); + queue.context.erase(0x1b8); + Require(decode(0x326u, 0x326u).positionLocation == PixelPositionLocation::Center, "positions without SPI_BARYC_CNTL were not at the pixel center"); + queue.context[0x1b8] = 0u; pixel = decode(0x506u, 0x7afu); Require(pixel.inputAddr == 0x7afu && !pixel.posY && pixel.posZ && PixelInputVgpr(pixel.inputAddr, PixelInput::PerspectiveCentroid) == 4u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionX) == 12u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionZ) == 14u, "ADDR-only inputs did not reserve their VGPRs"); - for (const auto bit : {0x8u, 0x4000u}) { - expectFailure([&] { static_cast(decode(0x2u | bit, 0x2u | bit)); }, "unsupported SPI_PS_INPUT_ENA/ADDR"); - } + expectFailure([&] { static_cast(decode(0xau, 0xau)); }, "unsupported SPI_PS_INPUT_ENA/ADDR"); + pixel = decode(0x4002u, 0x4002u); + Require(pixel.inputAddr == 0x4002u && PixelInputVgpr(pixel.inputAddr, PixelInput::SampleCoverage) == 2u, "SAMPLE_COVERAGE did not take its VGPR after the perspective center"); + Require(readsBuiltin(pixelBuiltinsRead(0x4002u, 0x4002u, 2u), spv::BuiltInSampleMask), "SAMPLE_COVERAGE is not read from gl_SampleMaskIn"); + Require(sampleMaskInputs(0x4002u, 0xffffu) == 1u && sampleMaskInputs(0x4002u, 0x0003u) == 1u, "SAMPLE_COVERAGE beside a partial quad sample mask declared gl_SampleMaskIn more than once"); pixel = decode(0x80u, 0x80u); Require(pixel.inputAddr == 0x80u && PixelInputVgpr(pixel.inputAddr, PixelInput::LineStipple) == 0u, "a pixel shader with only LINE_STIPPLE_TEX enabled was rejected"); pixel = decode(0x8082u, 0x8082u); @@ -1193,6 +1720,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; @@ -1220,7 +1789,12 @@ void DisabledColorTests() { Require(!state.hasColorTarget && state.color.address == 0 && state.color.bytes == 0, "disabled color writes accessed a color surface"); Require(state.renderExtent.width == 64 && state.renderExtent.height == 4, "attachment-free framebuffer lost screen scissor extent"); queue.context[0xd] = 0; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "empty framebuffer extent"); + const auto emptyScreen = AgcDriver::Graphics::DecodeState(queue); + Require(emptyScreen.renderExtent.width == 1 && emptyScreen.renderExtent.height == 1, "an empty screen scissor without targets did not get a one-pixel framebuffer"); + Require(emptyScreen.scissor.extent.width == 0 && emptyScreen.scissor.extent.height == 0, "an empty screen scissor without targets rasterized pixels"); + queue.context[0xd] = 64u; + const auto emptyRows = AgcDriver::Graphics::DecodeState(queue); + Require(emptyRows.renderExtent.width == 64 && emptyRows.scissor.extent.height == 0, "a screen scissor without rows rasterized pixels"); queue = makeState(); queue.context[0x8e] = 3; const auto partial = AgcDriver::Graphics::DecodeState(queue); @@ -1248,6 +1822,43 @@ void DisabledColorTests() { queue.context[0x00b] = std::bit_cast(1.0f); const auto depthOnly = AgcDriver::Graphics::DecodeState(queue); Require(!depthOnly.hasColorTarget && depthOnly.depth && depthOnly.depthTest && depthOnly.depthWrite && depthOnly.renderExtent.height == 64, "COLOR_INVALID discarded a depth-only draw"); + Require(depthOnly.depth->firstSlice == 0, "a depth view of slice 0 decoded another slice"); + queue.context[0x002] = 3; + const auto thirdSlice = AgcDriver::Graphics::DecodeState(queue); + Require(thirdSlice.depth && thirdSlice.depth->firstSlice == 3 && thirdSlice.depth->address == depthOnly.depth->address + 3u * AgcDriver::Graphics::DepthSliceBytes(depthOnly.depth->extent, 4u), "a depth view of slice 3 lost its slice or its Z offset"); + queue.context[0x002] = 1u << 26u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "on a surface without mips"); + queue.context[0x002] = 0; + { + using AgcDriver::Graphics::ComputeElementMipLayout; + using AgcDriver::Graphics::TextureTileMode; + auto mipped = queue; + mipped.context[0x007] = 0x03ff03ff; + mipped.context[0x010] = 0x80000181u | (2u << 16u); + const auto mips = ComputeElementMipLayout(TextureTileMode::kZ64KBX, 2u, 1024u, 1024u, 3u); + Require(!mips.at(1).tail && mips.at(1).width == 512u && mips.at(1).height == 512u, "the 1024x1024 Z_X layout does not give a 512x512 second mip outside the tail"); + const auto first = AgcDriver::Graphics::DecodeState(mipped); + Require(first.depth && first.depth->address == depthOnly.depth->address + mips.at(0).tiledOffset && first.depth->extent.width == 1024u && first.depth->extent.height == 1024u, "view mip 0 of a mip chain is not its first level"); + mipped.context[0x002] = 1u << 26u; + const auto second = AgcDriver::Graphics::DecodeState(mipped); + Require(second.depth && second.depth->address == depthOnly.depth->address + mips.at(1).tiledOffset, "view mip 1 is not at the second level's offset in the Z_X mip chain"); + Require(second.depth->extent.width == 512u && second.depth->extent.height == 512u, "view mip 1 does not render at the second level's extent"); + mipped.context[0x002] = (1u << 26u) | 1u | (1u << 13u); + const auto sliced = AgcDriver::Graphics::DecodeState(mipped); + Require(sliced.depth && sliced.depth->address == second.depth->address + AgcDriver::Graphics::ComputeSurfaceSize(mips, 1u), "slice 1 of a mip chain does not step by the whole chain"); + mipped.context[0x002] = 3u << 26u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(mipped); }, "is beyond the surface's mips"); + mipped.context[0x002] = 2u << 26u; + mipped.context[0x007] = 0x003f003f; + expectFailure([&] { AgcDriver::Graphics::DecodeState(mipped); }, "into the packed mip tail is unsupported"); + mipped.context[0x007] = 0x03ff03ff; + mipped.context[0x002] = 1u << 26u; + mipped.context[0x010] |= 1u << 29u; + mipped.context[0x005] = 0x300; + expectFailure([&] { AgcDriver::Graphics::DecodeState(mipped); }, "HTILE for the depth view mip"); + mipped.context[0x010] = (0x80000181u | (2u << 16u)) & ~(31u << 4u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(mipped); }, "uses a swizzle other than 64 KiB Z_X"); + } for (const auto colorControl : {0x0u, 0xcc0000u}) { queue = makeState(); queue.context[0x202] = colorControl; @@ -1334,7 +1945,9 @@ void ReversedComponentOrderTests() { Require(state.colors.size() == 1 && state.colors[0].format == VK_FORMAT_R8G8B8A8_UNORM && state.colors[0].componentMapping == mapping, "an 8_8_8_8 target with a reversed component order did not map exports onto RGBA8"); Require(AgcDriver::Graphics::ExportMappings(state)[0] == mapping && state.blends[0].colorWriteMask == 0xfu, "a reversed 8_8_8_8 target did not write all four channels through its export mapping"); queue.context[0x1e0] = 0x40010001u; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "reversed component order"); + for (const auto offset : {0x105u, 0x106u, 0x107u, 0x108u}) queue.context[offset] = 0; + const auto blended = AgcDriver::Graphics::DecodeState(queue); + Require(blended.blends[0].blendEnable && blended.blends[0].srcColorBlendFactor == VK_BLEND_FACTOR_ONE && blended.blends[0].dstColorBlendFactor == VK_BLEND_FACTOR_ZERO && blended.blends[0].srcAlphaBlendFactor == VK_BLEND_FACTOR_ONE && blended.blends[0].dstAlphaBlendFactor == VK_BLEND_FACTOR_ZERO && blended.reversedBlend == 0u && !blended.dualSourceBlend, "a ONE + ZERO blend into a reversed 8_8_8_8 target did not stay a plain Vulkan blend"); queue.context[0x1e0] = 0; queue.context[0x31c] = (queue.context[0x31c] & ~((0x1fu << 2u) | (7u << 8u))) | (12u << 2u) | (7u << 8u); const auto wide = AgcDriver::Graphics::DecodeState(queue); @@ -1342,6 +1955,234 @@ void ReversedComponentOrderTests() { } } +AgcDriver::QueueState reversedBlendQueue(std::uint32_t format, std::uint32_t number, std::uint32_t swap, std::uint32_t control) { + auto queue = makeState(); + queue.context[0x31c] = (queue.context[0x31c] & ~0x1f7cu) | (format << 2u) | (number << 8u) | (swap << 11u); + const auto address = reinterpret_cast(sliceMemory.data()); + queue.context[0x318] = static_cast(address >> 8u); + queue.context[0x390] = static_cast(address >> 40u); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + queue.context[0x1e0] = control; + for (std::uint32_t i = 0; i < 4; ++i) queue.context[0x105 + i] = 0; + return queue; +} + +void ReversedComponentOrderBlendTests() { + using AgcDriver::Graphics::DecodeState; + struct Target { + std::uint32_t format; + std::uint32_t number; + VkFormat vkFormat; + }; + struct Case { + std::uint32_t control; + VkBlendFactor source; + VkBlendFactor destination; + VkBlendFactor sourceAlpha; + VkBlendFactor destinationAlpha; + VkBlendOp operation; + std::uint32_t code; + bool opaqueDestination; + }; + const std::array targets{{ + {10u, 0u, VK_FORMAT_R8G8B8A8_UNORM}, {12u, 7u, VK_FORMAT_R16G16B16A16_SFLOAT}, {12u, 0u, VK_FORMAT_R16G16B16A16_UNORM} + }}; + const std::array cases{{ + {0x65010504u, VK_BLEND_FACTOR_SRC1_COLOR, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA, VK_BLEND_FACTOR_SRC1_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA, VK_BLEND_OP_ADD, 0x570001u, false}, + {0x65810584u, VK_BLEND_FACTOR_SRC1_COLOR, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA, VK_BLEND_FACTOR_SRC1_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA, VK_BLEND_OP_REVERSE_SUBTRACT, 0x570001u, false}, + {0x40000501u, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA, VK_BLEND_OP_ADD, 0x540001u, false}, + {0x61000104u, VK_BLEND_FACTOR_SRC1_COLOR, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_SRC1_ALPHA, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, 0x560001u, false}, + {0x61020302u, VK_BLEND_FACTOR_SRC_COLOR, VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD, 0x020001u, false}, + {0x61010504u, VK_BLEND_FACTOR_SRC1_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR, VK_BLEND_FACTOR_SRC1_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA, VK_BLEND_OP_ADD, 0x560301u, false}, + {0x60060008u, VK_BLEND_FACTOR_DST_COLOR, VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_DST_ALPHA, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, 0u, false}, + {0x40000101u, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, 0u, false}, + {0x40000141u, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MIN, 0u, false}, + {0x40000706u, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, 0u, true}, + }}; + const std::array, 10> refused{{ + {0x40000706u, "with a factor of the destination alpha in the color equation"}, + {0x4000000au, "with a factor of the destination alpha in the color equation"}, + {0x61000008u, "with factors the host blend cannot form from copies of the export is not implemented"}, + {0x61000501u, "with factors the host blend cannot form from copies of the export is not implemented"}, + {0x65050302u, "with factors the host blend cannot form from copies of the export is not implemented"}, + {0x4000000du, "with a constant blend factor"}, + {0x40000013u, "with a constant blend factor"}, + {0x4000000fu, "with a second-source factor is not implemented, as dual-source blending into a component-swapped color target is unsupported"}, + {0x65240504u, "with different color and alpha operations"}, + {0x40000544u, "with a MIN or MAX operation whose factors are not ONE"}, + }}; + for (const auto& [swap, mapping] : {std::pair{2u, std::uint8_t{0x1bu}}, std::pair{3u, std::uint8_t{0x93u}}}) { + for (const auto& target : targets) { + for (const auto& test : cases) { + auto queue = reversedBlendQueue(target.format, target.number, swap, test.control); + if (test.opaqueDestination) queue.context[0x31d] = 0x20000u; + const auto state = DecodeState(queue); + const auto code = test.code; + char what[128]; + std::snprintf(what, sizeof(what), "CB_BLEND0_CONTROL 0x%08x into color format %u number type %u with component mapping 0x%02x", test.control, target.format, target.number, mapping); + Require(state.colors.size() == 1 && state.colors[0].format == target.vkFormat && state.colors[0].componentMapping == mapping, std::string(what) + ": the target decoded differently"); + const auto& blend = state.blends.at(0); + Require(blend.blendEnable && blend.srcColorBlendFactor == test.source && blend.dstColorBlendFactor == test.destination && blend.srcAlphaBlendFactor == test.sourceAlpha && blend.dstAlphaBlendFactor == test.destinationAlpha && blend.colorBlendOp == test.operation && blend.alphaBlendOp == test.operation && blend.colorWriteMask == 0xfu, std::string(what) + ": wrong Vulkan blend state"); + Require(state.reversedBlend == code && !state.dualSourceBlend, std::string(what) + ": wrong pixel shader blend code " + std::to_string(state.reversedBlend)); + Require(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state)).targetExportMapping[0] == mapping, std::string(what) + ": the export mapping changed"); + const auto direct = AgcDriver::Graphics::TranslateReversedBlend(test.control, mapping, target.vkFormat, test.opaqueDestination); + Require(direct.shaderCode == code && direct.blend.srcColorBlendFactor == blend.srcColorBlendFactor && direct.blend.dstColorBlendFactor == blend.dstColorBlendFactor && direct.blend.srcAlphaBlendFactor == blend.srcAlphaBlendFactor && direct.blend.dstAlphaBlendFactor == blend.dstAlphaBlendFactor && direct.blend.colorBlendOp == blend.colorBlendOp && direct.blend.alphaBlendOp == blend.alphaBlendOp, std::string(what) + ": TranslateReversedBlend disagrees with DecodeState"); + } + } + for (const auto& [control, reason] : refused) { + const auto queue = reversedBlendQueue(10u, 0u, swap, control); + expectFailure([&] { DecodeState(queue); }, std::string("blending into a color target with a reversed component order ") + reason); + } + char registers[96]; + std::snprintf(registers, sizeof(registers), "(CB_BLEND_CONTROL 0x65010504, component mapping 0x%02x, VkFormat %d)", mapping, static_cast(VK_FORMAT_R8G8B8A8_SRGB)); + expectFailure([&] { DecodeState(reversedBlendQueue(10u, 6u, swap, 0x65010504u)); }, std::string("reversed component order is not implemented for sRGB targets ") + registers); + expectFailure([&] { DecodeState(reversedBlendQueue(10u, 1u, swap, 0x65010504u)); }, "reversed component order is not implemented for signed normalized targets"); + expectFailure([&] { DecodeState(reversedBlendQueue(12u, 1u, swap, 0x65010504u)); }, "reversed component order is not implemented for signed normalized targets"); + expectFailure([&] { DecodeState(reversedBlendQueue(12u, 4u, swap, 0x65010504u)); }, "reversed component order is not implemented for integer targets"); + auto masked = reversedBlendQueue(10u, 0u, swap, 0x65010504u); + masked.context[0x8e] = 7u; + Require(DecodeState(masked).blends.at(0).colorWriteMask == 0xeu, "a reversed target's blend did not keep the export-order write mask of its first three exports"); + auto twoTargets = reversedBlendQueue(10u, 0u, swap, 0x65010504u); + for (const auto offset : {0x318u, 0x31bu, 0x31cu, 0x31du}) twoTargets.context[offset + 0xfu] = twoTargets.context.at(offset); + for (const auto offset : {0x390u, 0x3b0u, 0x3b8u}) twoTargets.context[offset + 1u] = twoTargets.context.at(offset); + twoTargets.context[0x31c + 0xfu] &= ~(3u << 11u); + twoTargets.context[0x1c5] = 0x99u; + twoTargets.context[0x8e] = 0xffu; + twoTargets.context[0x8f] = 0xffu; + twoTargets.context[0x1e1] = 0; + expectFailure([&] { DecodeState(twoTargets); }, "reversed component order through a second source color needs that target to be the shader's only color export"); + twoTargets.context[0x1e0] = 0x40000101u; + const auto two = DecodeState(twoTargets); + Require(two.colors.size() == 2 && two.reversedBlend == 0u && two.blends.at(0).srcColorBlendFactor == VK_BLEND_FACTOR_ONE && two.blends.at(0).dstColorBlendFactor == VK_BLEND_FACTOR_ONE, "a reversed target blended by Vulkan alone was refused next to a second target"); + twoTargets.context[0x31c + 0xfu] |= swap << 11u; + twoTargets.context[0x1e0] = 0x65010504u; + twoTargets.context[0x1e1] = 0x40000101u; + expectFailure([&] { DecodeState(twoTargets); }, "reversed component order through a second source color needs that target to be the shader's only color export"); + } +} + +void ReversedBlendRoutingTests() { + namespace Code = ShaderRecompiler::ReversedBlendCode; + using Term = std::pair; + constexpr Term zero{false, 0u}; + constexpr Term one{true, 0u}; + const auto flip = [](Term term) { return Term{!term.first, term.second}; }; + const auto guest = [&](std::uint32_t factor, std::uint32_t component) -> std::optional { + const std::uint32_t exported = 10u + component; + const std::uint32_t stored = 20u + component; + switch (factor) { + case 0: return zero; + case 1: return one; + case 2: return Term{false, exported}; + case 3: return Term{true, exported}; + case 4: return Term{false, 10u}; + case 5: return Term{true, 10u}; + case 6: return Term{false, 20u}; + case 7: return Term{true, 20u}; + case 8: return Term{false, stored}; + case 9: return Term{true, stored}; + default: return component == 0u ? std::optional{one} : std::nullopt; + } + }; + const auto product = [&](Term first, Term second) { + if (first == zero || second == zero) return std::vector{zero}; + std::vector factors; + for (const auto term : {first, second}) { + if (term != one) factors.push_back(term); + } + std::sort(factors.begin(), factors.end()); + return factors; + }; + std::uint32_t routed = 0; + std::uint32_t native = 0; + for (std::uint32_t factors = 0; factors < 11u * 11u * 11u * 11u; ++factors) { + const std::array guestFactors{factors % 11u, factors / 11u % 11u, factors / 121u % 11u, factors / 1331u}; + const auto control = guestFactors[0] | (guestFactors[1] << 8u) | (guestFactors[2] << 16u) | (guestFactors[3] << 24u) | 0x20000000u; + std::optional translated; + try { + translated = AgcDriver::Graphics::TranslateReversedBlend(control, 0x1bu, VK_FORMAT_R8G8B8A8_UNORM, false); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("reversed component order with") != std::string::npos, std::string("an unexpected reversed-order blend refusal: ") + error.what()); + continue; + } + const auto code = translated->shaderCode; + Require(code == 0u || (code & Code::Enable) != 0u, "a routed reversed-order blend code lacks its enable bit"); + const auto route = [&](std::uint32_t shift, std::uint32_t component) { + switch ((code >> (shift + Code::RouteBits * component)) & Code::RouteMask) { + case Code::Own: return Term{false, 10u + component}; + case Code::Component0: return Term{false, 10u}; + case Code::Zero: return zero; + default: return one; + } + }; + const auto host = [&](VkBlendFactor factor, std::uint32_t component) -> std::optional { + switch (factor) { + case VK_BLEND_FACTOR_ZERO: return zero; + case VK_BLEND_FACTOR_ONE: return one; + case VK_BLEND_FACTOR_SRC_COLOR: return route(Code::ColorShift, component); + case VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR: return flip(route(Code::ColorShift, component)); + case VK_BLEND_FACTOR_SRC_ALPHA: return route(Code::ColorShift, 3u); + case VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA: return flip(route(Code::ColorShift, 3u)); + case VK_BLEND_FACTOR_DST_COLOR: return Term{false, 20u + component}; + case VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR: return Term{true, 20u + component}; + case VK_BLEND_FACTOR_DST_ALPHA: return Term{false, 23u}; + case VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA: return Term{true, 23u}; + case VK_BLEND_FACTOR_SRC1_COLOR: return route(Code::SecondShift, component); + case VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR: return flip(route(Code::SecondShift, component)); + case VK_BLEND_FACTOR_SRC1_ALPHA: return route(Code::SecondShift, 3u); + case VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA: return flip(route(Code::SecondShift, 3u)); + default: return std::nullopt; + } + }; + const auto& blend = translated->blend; + const auto secondColor = [](VkBlendFactor factor) { return factor == VK_BLEND_FACTOR_SRC1_COLOR || factor == VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR || factor == VK_BLEND_FACTOR_SRC1_ALPHA || factor == VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA; }; + Require(code != 0u || !(secondColor(blend.srcColorBlendFactor) || secondColor(blend.dstColorBlendFactor) || secondColor(blend.srcAlphaBlendFactor) || secondColor(blend.dstAlphaBlendFactor)), "a reversed-order blend without a second color reads one"); + for (std::uint32_t component = 0; component < 4; ++component) { + const auto wantedSource = guest(guestFactors[component == 0u ? 2u : 0u], component); + const auto wantedDestination = guest(guestFactors[component == 0u ? 3u : 1u], component); + const auto source = host(component == 3u ? blend.srcAlphaBlendFactor : blend.srcColorBlendFactor, component); + const auto destination = host(component == 3u ? blend.dstAlphaBlendFactor : blend.dstColorBlendFactor, component); + Require(wantedSource && wantedDestination && source && destination, "a reversed-order blend was accepted with a factor the routing check cannot express"); + char what[160]; + std::snprintf(what, sizeof(what), "CB_BLEND_CONTROL 0x%08x, code 0x%06x, component %u", control, code, component); + Require(product(route(Code::ColorShift, component), *source) == product(Term{false, 10u + component}, *wantedSource), std::string(what) + ": the routed source term differs from the guest's"); + Require(*destination == *wantedDestination, std::string(what) + ": the routed destination factor differs from the guest's"); + } + ++(code == 0u ? native : routed); + } + 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); @@ -1436,15 +2277,52 @@ void DepthStencilTests() { queue.context[0x10d] = 0x01000001; queue.context[0x200] = 0x00700711; std::vector log; - AgcDriver::Graphics::RegisterReadLog() = &log; - auto state = AgcDriver::Graphics::DecodeState(queue); - AgcDriver::Graphics::RegisterReadLog() = nullptr; + auto state = [&] { + const AgcDriver::Graphics::RegisterReadLogScope logScope(&log); + return AgcDriver::Graphics::DecodeState(queue); + }(); for (const auto read : log) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a depth register the decoder reads: " + std::to_string(read.offset)); queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); Require(rejection.empty(), "precheck rejected a stencil draw with a surface: " + rejection); Require(state.depth && state.depth->address == 0x10000 && state.depth->stencilAddress == 0x20000 && state.depth->format == VK_FORMAT_D32_SFLOAT_S8_UINT && state.depth->clearStencil == 7 && state.depth->htileAddress == 0x10000030000ull && !state.depth->htileStencil, "depth surface decode changed"); + { + auto split = queue; + split.context[0x014] = 0; + for (const auto control : {0x00700711u, 0x00700715u, 0x00700713u}) { + split.context[0x200] = control; + const auto splitState = AgcDriver::Graphics::DecodeState(split); + Require(splitState.depth && splitState.depth->address == 0x10000 && !splitState.depthWrite, "a depth plane the draw does not write rejected another write address"); + Require(AgcDriver::Graphics::DrawRejection(split, false).empty(), "precheck rejected a depth plane the draw does not write at another write address"); + } + split.context[0x200] = 0x00700717u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(split); }, "depth read and written at different addresses"); + split.context[0x200] = 0x00700711u; + for (const auto control : {0x01u, 0x20u, 0x40u, 0x60u}) { + split.context[0x000] = control; + expectFailure([&] { AgcDriver::Graphics::DecodeState(split); }, "depth read and written at different addresses"); + } + } + for (const auto clear : {0u, 1u, 2u, 3u}) { + auto retained = queue; + retained.context[0x8e] = retained.context[0x8f] = retained.context[0x203] = 0; + retained.context[0x000] = clear; + retained.context[0x200] = 0x00700777u; + auto unbound = retained; + unbound.shader[0x008] = unbound.shader[0x009] = 0; + const auto expected = AgcDriver::Graphics::DecodeState(unbound); + for (const auto format : {1u, 2u, 3u, 9u}) { + retained.context[0x1c4] = format; + Require(AgcDriver::Graphics::PixelProgramSkipped(retained), "a retained format ran the pixel program during a depth/stencil pass"); + const auto actual = AgcDriver::Graphics::DecodeState(retained); + Require(actual.depth && expected.depth && actual.depth->address == expected.depth->address && actual.depth->stencilAddress == expected.depth->stencilAddress && actual.depth->format == expected.depth->format && actual.depth->clearDepth == expected.depth->clearDepth && actual.depth->clearStencil == expected.depth->clearStencil, "null substitution changed the depth/stencil surface"); + Require(actual.depthTest == expected.depthTest && actual.depthWrite == expected.depthWrite && actual.depthCompare == expected.depthCompare && actual.depthBoundsTest == expected.depthBoundsTest && actual.stencilTest == expected.stencilTest, "null substitution changed fixed-function depth/stencil tests"); + Require(std::memcmp(&actual.stencilFront, &expected.stencilFront, sizeof(VkStencilOpState)) == 0 && std::memcmp(&actual.stencilBack, &expected.stencilBack, sizeof(VkStencilOpState)) == 0, "null substitution changed stencil operations"); + Require(actual.viewport.minDepth == expected.viewport.minDepth && actual.viewport.maxDepth == expected.viewport.maxDepth && actual.renderExtent.width == expected.renderExtent.width && actual.renderExtent.height == expected.renderExtent.height, "null substitution changed depth coverage"); + Require(AgcDriver::Graphics::DrawRejection(retained, true).empty(), "a retained-format depth/stencil pass was rejected"); + } + } { queue.context[0x011] &= ~(1u << 29u); const auto stencilTiled = AgcDriver::Graphics::DecodeState(queue); @@ -1477,11 +2355,19 @@ void DepthStencilTests() { queue.context[0x005] = 0x300; queue.context[0x01e] = 0x01; log.clear(); - AgcDriver::Graphics::RegisterReadLog() = &log; - state = AgcDriver::Graphics::DecodeState(queue); - AgcDriver::Graphics::RegisterReadLog() = nullptr; + { + const AgcDriver::Graphics::RegisterReadLogScope logScope(&log); + state = AgcDriver::Graphics::DecodeState(queue); + } for (const auto read : log) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks an HTILE register the decoder reads: " + std::to_string(read.offset)); Require(state.depth->htileAddress == (0x30000ull | (1ull << 40u)), "the HTILE address was not decoded from DB_HTILE_DATA_BASE and its high byte"); + { + const auto view = queue.context[0x002]; + queue.context[0x002] = 2u | (2u << 13u); + const auto slice = AgcDriver::Graphics::DecodeState(queue); + Require(slice.depth->htileAddress == (0x30000ull | (1ull << 40u)) + 2u * AgcDriver::Graphics::HtileMetadataBytes(slice.depth->extent), "slice 2 of a depth array must use the third slice of its HTILE"); + queue.context[0x002] = view; + } queue.context[0x010] &= ~0x20000000u; state = AgcDriver::Graphics::DecodeState(queue); Require(state.depth->htileAddress == 0, "a surface without TILE_SURFACE_ENABLE decoded an HTILE address"); @@ -1505,7 +2391,41 @@ void DepthStencilTests() { queue.context[0x10b] = 0x40; queue.context[0x10c] = 0x05ffff02; queue.context[0x200] = 0x00200211; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.passOp == VK_STENCIL_OP_INVERT && state.stencilFront.failOp == VK_STENCIL_OP_KEEP && state.stencilFront.depthFailOp == VK_STENCIL_OP_KEEP && state.stencilFront.compareOp == VK_COMPARE_OP_EQUAL, "an equal test replaced by another value is not drawn as an inversion"); + Require(state.stencilFront.reference == 2 && state.stencilFront.compareMask == 0xff && state.stencilFront.writeMask == 7, "an inverted replacement does not keep the test value and write only the bits that differ from it"); + queue.context[0x10b] = 0x440; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.passOp == VK_STENCIL_OP_INVERT && state.stencilFront.depthFailOp == VK_STENCIL_OP_INVERT && state.stencilFront.writeMask == 7, "a replacement on pass and depth fail is not drawn as one inversion"); + queue.context[0x10b] = 0x4; + queue.context[0x200] = 0x00200511; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.failOp == VK_STENCIL_OP_INVERT && state.stencilFront.passOp == VK_STENCIL_OP_KEEP && state.stencilFront.writeMask == 7, "a not-equal test that fails is not replaced as an inversion"); + queue.context[0x10c] = 0x40ffffff; + queue.context[0x10b] = 0x40; + queue.context[0x200] = 0x00200311; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.passOp == VK_STENCIL_OP_INVERT && state.stencilFront.writeMask == 0xbf && state.stencilFront.reference == 0xff, "a less-or-equal test against 0xff, which passes only on 0xff, is not replaced as an inversion"); + queue.context[0x10b] = 0x4; + queue.context[0x200] = 0x00200411; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.failOp == VK_STENCIL_OP_INVERT && state.stencilFront.writeMask == 0xbf, "a greater test against 0xff, which fails only on 0xff, is not replaced as an inversion"); + queue.context[0x10c] = 0x40ffff7f; + queue.context[0x10b] = 0x40; + queue.context[0x200] = 0x00200311; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement"); + queue.context[0x10c] = 0x05ffff02; + queue.context[0x200] = 0x00200511; + queue.context[0x10b] = 0x40; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement"); + queue.context[0x200] = 0x00200111; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement"); + queue.context[0x200] = 0x00200211; + queue.context[0x10c] = 0x05ff0f02; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement"); + queue.context[0x10c] = 0x05ffff02; + queue.context[0x10b] = 0x41; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "beside another stencil operation"); queue.context[0x10b] = 0x50; queue.context[0x200] = 0x00700711; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil add/subtract"); @@ -1563,16 +2483,39 @@ 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"); } - queue.context[0x000] = 0x22; + queue.context[0x000] = 0x23; queue.context[0x011] = 0; - 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"); + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depthWrite && state.depthCompare == VK_COMPARE_OP_ALWAYS && !state.stencilTest && state.colors.size() == 2, "a depth and stencil clear without a stencil plane does not clear depth alone"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "precheck rejected a stencil clear without a stencil plane"); + queue.context[0x000] = 0x22; 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"); @@ -1587,7 +2530,8 @@ void DepthStencilTests() { const auto firstSlice = AgcDriver::Graphics::DecodeState(queue); Require(firstSlice.color.address == reinterpret_cast(colorMemory.data()), "a color view of several slices did not retain its first slice"); queue.context[0x207] = 1u << 18u; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "layered draws into a color view of several array slices"); + const auto layeredSingle = AgcDriver::Graphics::DecodeState(queue); + Require(layeredSingle.color.address == firstSlice.color.address && layeredSingle.color.arrayAddress == firstSlice.color.address && layeredSingle.color.arrayLastSlice == 1u && layeredSingle.color.layers == 2u && layeredSingle.renderLayers == 2u, "a layered draw into slices 0 to 1 of a surface of one slice did not render them as a two-slice array"); queue.context[0x207] = 0; const auto sliced = reinterpret_cast(sliceMemory.data()); queue.context[0x318] = static_cast(sliced >> 8u); @@ -1602,7 +2546,8 @@ void DepthStencilTests() { const auto range = AgcDriver::Graphics::DecodeState(queue); Require(range.color.address == sliced && range.color.bytes == 1024u, "a color view of slices 0 to 1 of a 2D array target did not render into its first slice"); queue.context[0x207] = 1u << 18u; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "layered draws into a color view of several array slices"); + const auto layeredArray = AgcDriver::Graphics::DecodeState(queue); + Require(layeredArray.color.address == sliced && layeredArray.color.arraySlice == 0u && layeredArray.color.layers == 2u && layeredArray.renderLayers == 2u && layeredArray.layerExports != 0u, "a layeredArray draw into slices 0 to 1 of a 2D array target did not render two layers"); queue.context[0x207] = 0; queue.context[0x31b] = 2u | (2u << 13u); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "beyond the array surface"); @@ -1613,6 +2558,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); @@ -1652,6 +2603,26 @@ void OneDimensionalColorTests() { Require(line.color.extent.width == 64u && line.color.extent.height == 1u && line.color.address == reinterpret_cast(colorMemory.data()) && line.color.depth == 1u, "a 1D color target did not decode as one row"); } +void multisampledSingleClearTests() { + static constexpr std::array kernel{0xbe800303u, 0xbe830382u, 0xf40c0401u, 0xfa000000u, 0xd7460000u, 0x04010609u, 0xd7460001u, 0x0405060au, 0x7e060208u, 0x7e0c020bu, 0x7e0e0280u, 0xd5690004u, 0x02000100u, 0x7e000205u, 0xd5690005u, 0x02000901u, 0xd7000001u, 0x02000c80u, 0xbf8cc07fu, 0xf0201f38u, 0x00040004u, 0xbf810000u}; + const std::array userData{0u, 0u, 0x0f7f0008u, 8u, 8u, 0xff00ffccu, 0u, 0u, 0u}; + const std::array group{8, 8, 1}; + const auto match = [&](std::span code, std::span words, const std::array& threads) { + return AgcDriver::Graphics::MatchMultisampledSingleClear(code, words, threads); + }; + const auto clear = match(kernel, userData, group); + Require(clear.has_value() && clear->descriptor == 0x20f7f0008ull && clear->color == std::array{0xff00ffccu, 0u, 0u, 0u}, "RADV's multisampled comp-to-single clear was not recognized"); + Require(!match(kernel, userData, {64, 1, 1}), "the comp-to-single clear matched with another group size"); + Require(!match(kernel, std::span(userData).first(8), group), "the comp-to-single clear matched without its clear color"); + auto unsized = userData; + unsized[3] = 0; + Require(!match(kernel, unsized, group), "a comp-to-single clear with no block size was recognized"); + auto other = kernel; + other[17] ^= 1u; + Require(!match(other, userData, group), "a different kernel was taken for RADV's comp-to-single clear"); + Require(!match(std::span(kernel).first(21), userData, group), "a truncated kernel was taken for RADV's comp-to-single clear"); +} + void htileDepthClearTests() { static constexpr std::array kernel{0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u, 0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u, 0xbf810000u}; const std::array wave{64, 1, 1}; @@ -1721,7 +2692,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; @@ -1735,7 +2706,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"); @@ -1834,6 +2805,28 @@ void DepthBoundsBiasTests() { queue.context[0x009] = bits(0.75f); auto state = AgcDriver::Graphics::DecodeState(queue); Require(state.depthTest && state.depthBoundsTest && state.minDepthBounds == 0.25f && state.maxDepthBounds == 0.75f, "depth bounds decode changed"); + const auto infinity = std::numeric_limits::infinity(); + for (const auto bounds : {std::array{0.0f, infinity}, std::array{-infinity, 1.0f}, std::array{-infinity, infinity}}) { + queue.context[0x008] = bits(bounds[0]); + queue.context[0x009] = bits(bounds[1]); + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depthBoundsTest && state.minDepthBounds == bounds[0] && state.maxDepthBounds == bounds[1], "infinite depth bounds changed"); + AgcDriver::Graphics::Context context{}; + expectFailure([&] { AgcDriver::Graphics::ValidateDepthBounds(context, state); }, "depth bounds outside [0, 1]"); + context.depthRangeUnrestricted = true; + AgcDriver::Graphics::ValidateDepthBounds(context, state); + } + for (const auto offset : {0x008u, 0x009u}) { + queue.context[0x008] = bits(0.25f); + queue.context[0x009] = bits(0.75f); + queue.context[offset] = bits(std::numeric_limits::quiet_NaN()); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "NaN depth bound"); + } + queue.context[0x008] = bits(0.25f); + queue.context[0x009] = bits(0.75f); + queue.context[0x00b] = bits(infinity); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "non-finite register"); + queue.context[0x00b] = bits(0.0f); queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); @@ -1872,9 +2865,38 @@ void DepthBoundsBiasTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "window offset"); queue.context[0x80] = 0; queue.userConfig[0x242] = 2; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "polygon mode, depth bias or nonstandard rasterization"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "depth bias or nonstandard rasterization"); queue.userConfig[0x242] = 3; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "polygon mode, depth bias or nonstandard rasterization"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "depth bias or nonstandard rasterization"); + queue.userConfig[0x242] = 4; + queue.context[0x280] = 0x80008u; + queue.context[0x281] = 0xffff0000u; + queue.context[0x282] = 8u; + const auto polygonMode = [&](std::uint32_t raster) { + queue.context[0x205] = raster; + return AgcDriver::Graphics::DecodeState(queue).polygonMode; + }; + Require(polygonMode(0x8u) == VK_POLYGON_MODE_POINT, "POLY_MODE with point fronts and backs did not decode to the point polygon mode"); + Require(polygonMode(0x128u) == VK_POLYGON_MODE_LINE, "POLY_MODE with line fronts and backs did not decode to the line polygon mode"); + Require(polygonMode(0x248u) == VK_POLYGON_MODE_FILL, "POLY_MODE with filled fronts and backs did not decode to the fill polygon mode"); + Require(polygonMode(0x20u) == VK_POLYGON_MODE_FILL, "polygon types without POLY_MODE did not keep the triangles filled"); + Require(polygonMode(0x4au) == VK_POLYGON_MODE_FILL, "the point type of culled back faces changed the filled front faces"); + Require(polygonMode(0x109u) == VK_POLYGON_MODE_LINE, "culled front faces did not leave the back faces' line type"); + expectFailure([&] { polygonMode(0x48u); }, "different front and back polygon modes"); + expectFailure([&] { polygonMode(0x18u); }, "a reserved POLY_MODE"); + expectFailure([&] { polygonMode(0x68u); }, "a reserved polygon type"); + expectFailure([&] { polygonMode(0x808u); }, "depth bias with a point or line polygon mode"); + queue.context[0x280] = 0x100010u; + expectFailure([&] { polygonMode(0x8u); }, "a point polygon mode with a point size other than one pixel"); + queue.context[0x280] = 0x80008u; + queue.context[0x281] = 0xffff0010u; + expectFailure([&] { polygonMode(0x8u); }, "a point polygon mode whose size limits exclude one pixel"); + queue.context[0x281] = 0xffff0000u; + queue.context[0x282] = 0x10u; + expectFailure([&] { polygonMode(0x128u); }, "a line polygon mode wider or narrower than one pixel"); + queue.context[0x282] = 8u; + queue.userConfig[0x242] = 2; + Require(polygonMode(0x8u) == VK_POLYGON_MODE_FILL, "a line list took the point polygon mode of its triangles"); queue.userConfig[0x242] = 4; queue.context[0x205] = 0x00001a4au; queue.context[0x2de] = 0x1f0u; @@ -1891,6 +2913,65 @@ void DepthBoundsBiasTests() { alignas(256) std::array dccKeys{}; +void depthCopyPassTests() { + using AgcDriver::Graphics::DecodeDepthCopyPass; + auto queue = makeState(); + queue.context[0x0] = 0; + Require(!DecodeDepthCopyPass(queue).has_value(), "a draw without DEPTH_COPY decoded as a DB depth copy"); + queue.context[0x000] = 0x84; + queue.context[0x010] = 0x80000187; + queue.context[0x011] = 0x20000180; + queue.context[0x012] = 0x00001000; + queue.context[0x007] = 0x0003003f; + queue.context[0x31c] = 0x40710; + queue.context[0x8e] = 1; + auto pass = DecodeDepthCopyPass(queue); + Require(pass.has_value() && pass->depthAddress == 0x100000 && pass->samples == 2 && pass->sample == 0 && pass->extent.width == 64 && pass->extent.height == 4, "a centroid DB depth copy of a two-sample depth plane decoded differently"); + Require(pass->color.format == VK_FORMAT_R32_SFLOAT && pass->color.samples == 1 && pass->color.address == reinterpret_cast(colorMemory.data()), "the DB depth copy target decoded differently"); + queue.context[0x000] = 0x104; + Require(DecodeDepthCopyPass(queue)->sample == 1, "COPY_SAMPLE without COPY_CENTROID did not select the sample"); + queue.context[0x000] = 0x204; + expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "a sample the depth plane does not have"); + queue.context[0x000] = 0x8c; + expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "stencil copy"); + queue.context[0x000] = 0x84; + queue.context[0x30e] = 0xfffffffe; + expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "sample mask"); + queue.context[0x30e] = 0xffffffff; + queue.context[0x200] = 2; + expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "depth or stencil tests"); + queue.context[0x200] = 0; + queue.context[0x010] = 0x80000185; + expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "Z_32_FLOAT"); + queue.context[0x010] = 0x80000187; + queue.context[0x8e] = 0xf; + expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "red channel"); + queue.context[0x8e] = 1; + queue.context[0x31c] = 0x28028; + expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "single-sample R32 float"); + queue.context[0x31c] = 0x40710; + queue.context[0x10f] = std::bit_cast(16.0f); + expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "over part of the depth surface"); + queue.context[0x10f] = std::bit_cast(32.0f); + queue.context[0x010] = 0x80001187; + expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "mipmapped or partially resident"); + queue.context[0x010] = 0x80000187; + queue.context[0x012] = 0x00001003; + pass = DecodeDepthCopyPass(queue); + Require(pass.has_value() && pass->depthAddress == 0x100000, "a DB depth copy kept the pipe-bank XOR of a two-sample depth plane in its address"); + queue.context[0x012] = 0x00001000; + queue.context[0x002] = 1u | (1u << 13u); + pass = DecodeDepthCopyPass(queue); + Require(pass.has_value() && pass->depthAddress == 0x100000 + AgcDriver::Graphics::DepthSliceBytes({64, 4}, 4u), "a DB depth copy of slice 1 did not address the slice the depth surface registers"); + queue.context[0x002] = 0; + Require(DecodeDepthCopyPass(queue).has_value(), "the restored DB depth copy was refused"); + queue.context[0x010] = 0x80000183; + pass = DecodeDepthCopyPass(queue); + Require(pass.has_value() && pass->samples == 1 && pass->sample == 0, "a centroid DB depth copy of a one-sample depth plane decoded differently"); + queue.context[0x000] = 0x104; + expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "a sample the depth plane does not have"); +} + void metadataPassTests() { using AgcDriver::Graphics::ColorMetadataPass; using AgcDriver::Graphics::DecodeColorMetadataPass; @@ -2091,6 +3172,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()); @@ -2102,24 +3184,68 @@ void cmaskTests() { const auto target = DecodeColorBuffer(queue.context, 0); Require(target.cmaskAddress == cmaskAddress && target.cmaskBytes == cmaskMemory.size(), "the fast-clear target lost its CMASK"); std::vector log; - AgcDriver::Graphics::RegisterReadLog() = &log; - static_cast(DecodeColorBuffer(queue.context, 0)); - AgcDriver::Graphics::RegisterReadLog() = nullptr; + { + const AgcDriver::Graphics::RegisterReadLogScope logScope(&log); + static_cast(DecodeColorBuffer(queue.context, 0)); + } Require(!log.empty(), "the register facade recorded nothing for a fast-clear target"); for (const auto read : log) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a register the fast-clear target decode reads: " + std::to_string(read.offset)); 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"); @@ -2214,6 +3340,17 @@ void cmaskTests() { Require(texels(0x5a5a5a5au), "a refused pass changed the texels of a DCC target"); } +void uint8x2TargetTests() { + auto queue = makeState(); + queue.context[0x1c5] = 7; + queue.context[0x31c] = (queue.context[0x31c] & ~0x1ffcu) | (3u << 2u) | (4u << 8u); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.color.format == VK_FORMAT_R8G8_UINT && state.color.elementBytes == 2 && state.color.componentMapping == 0xe4u && state.color.uintExport, "an 8_8 UINT color target was rejected"); + queue.context[0x31c] = (queue.context[0x31c] & ~0x1800u) | (1u << 11u); + const auto alt = AgcDriver::Graphics::DecodeState(queue); + Require(alt.color.format == VK_FORMAT_R8G8_UINT && alt.color.componentMapping == 0xecu, "an 8_8 UINT SWAP_ALT color target was rejected"); +} + void uint8x4TargetTests() { auto queue = makeState(); queue.context[0x1c5] = 7; @@ -2255,8 +3392,118 @@ VKAPI_ATTR VkResult VKAPI_CALL sampleLocationDepthProperties(VkPhysicalDevice, V return depthSampleResult; } +void msaaEnableClearDepthTests() { + using AgcDriver::Graphics::DecodeState; + const auto drawB = [] { + auto queue = makeState(); + queue.context[0x2f8] = 0x0020c002; + queue.context[0x292] = 2; + for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) queue.context[offset] = (offset - 0x2feu) % 4u == 0 ? 0x622ae6aeu : 0u; + queue.context[0x31c] = 0x8028; + queue.context[0x31d] = 0x12000; + queue.context[0x3b8] = 0x0dc00000u; + 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] = 0xa290098b; + queue.context[0x011] = 0x20000180; + for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; + queue.context[0x200] = 0x16; + queue.context[0x201] = 0x00010002; + 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, what + " did not keep the 4x depth target"); + 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"); + }; + auto queue = drawB(); + queue.context[0x200] = 0x711; + queue.context[0x011] = 0x20000181; + for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200; + queue.context[0x10b] = 0x333; + queue.context[0x10c] = 0x00ff0000; + queue.context[0x10d] = 0; + auto state = DecodeState(queue); + Require(state.colors.size() == 1 && state.colors[0].samples == 4, "PPSA18216's draw B lost its 4x color target"); + Require(state.stencilTest && !state.depthTest && state.depth && state.depth->format == VK_FORMAT_D32_SFLOAT_S8_UINT && state.stencilFront.compareOp == VK_COMPARE_OP_ALWAYS, "PPSA18216's draw B (DB_DEPTH_CONTROL 0x711) lost its 4x stencil plane"); + Require(state.stencilFront.passOp == VK_STENCIL_OP_REPLACE && state.stencilFront.failOp == VK_STENCIL_OP_REPLACE && state.stencilFront.depthFailOp == VK_STENCIL_OP_REPLACE && state.stencilFront.writeMask == 0xffu && state.stencilFront.reference == 0u, "PPSA18216's draw B did not decode as a stencil clear to 0"); + requireCentered(state, "a 4x depth and stencil target rasterized without MSAA_ENABLE (PPSA18216's draw B)"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused PPSA18216's draw B"); + queue.context[0x200] = 0x710; + state = DecodeState(queue); + Require(!state.depth && state.samples == 4 && state.colors.size() == 1, "draw B with DB_DEPTH_CONTROL 0x710 bound a depth target"); + queue.context[0x201] = 0x00050002u; + Require(!DecodeState(queue).depth, "DB_EQAA Z interpolation refused a 4x draw without a depth target"); + queue = drawB(); + state = DecodeState(queue); + Require(state.depthTest && state.colors.size() == 1, "a depth-tested 4x depth and color pair lost a target"); + requireCentered(state, "a depth-tested 4x depth and color pair rasterized without MSAA_ENABLE"); + queue.context[0x8e] = 0; + state = DecodeState(queue); + Require(state.colors.empty(), "a depth-only 4x draw without MSAA_ENABLE decoded a color target"); + requireCentered(state, "a 4x depth target rasterized without MSAA_ENABLE"); + for (const auto primitive : {2u, 3u}) { + queue = drawB(); + queue.userConfig[0x242] = primitive; + state = DecodeState(queue); + Require(state.topology == (primitive == 2u ? VK_PRIMITIVE_TOPOLOGY_LINE_LIST : VK_PRIMITIVE_TOPOLOGY_LINE_STRIP) && state.colors.size() == 1 && state.colors[0].samples == 4, "PPSA18216's line list or strip into its 4x depth and color pair lost its topology or color target"); + requireCentered(state, "a line list or strip into PPSA18216's 4x depth and color pair without MSAA_ENABLE"); + Require(!state.sampleLocationGrid && !state.perSampleShading, "a line list or strip without MSAA_ENABLE kept a sample location grid or sample shading"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused a line list or strip into PPSA18216's 4x depth and color pair"); + } + queue = drawB(); + queue.userConfig[0x242] = 1; + queue.context[0x2d5] = 0x2030; + queue.userConfig[0x25b] = 0x6633; + queue.context[0x1ff] = 255; + queue.context[0x2ce] = 5; + queue.context[0x29b] = 2; + queue.context[0x2ab] = 4; + queue.shader[0x8a] = 3u << 29u; + queue.shader[0x8b] = 3u << 16u; + state = DecodeState(queue); + Require(state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST && state.stages.mesh && state.stages.mesh->outputPrimitiveSize == 3u && AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Triangle && state.colors.size() == 1 && state.colors[0].samples == 4, "a point list into a geometry program that outputs triangle strips into PPSA18216's 4x depth and color pair did not decode as triangles"); + requireCentered(state, "triangles a geometry program makes from a point list into PPSA18216's 4x depth and color pair without MSAA_ENABLE"); + for (const auto primitive : {4u, 2u, 1u}) { + queue = drawB(); + queue.userConfig[0x242] = primitive; + queue.context[0x2d5] = 0x2030; + queue.userConfig[0x25b] = 0x6633; + queue.context[0x1ff] = 255; + queue.context[0x2ce] = 5; + queue.context[0x29b] = 0; + queue.context[0x2ab] = 4; + queue.context[0x280] = 0x00080008u; // point-output geometry programs decode PA_SU_POINT_SIZE / PA_SU_POINT_MINMAX before this rejection + queue.context[0x281] = 0x00080008u; + queue.shader[0x8a] = 3u << 29u; + queue.shader[0x8b] = 3u << 16u; + expectFailure([&] { DecodeState(queue); }, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points or alpha-to-coverage"); + } + for (const auto eqaa : {0x00050002u, 0x00090002u, 0x01010002u, 0x08010002u}) { + queue = drawB(); + queue.context[0x201] = eqaa; + expectFailure([&] { DecodeState(queue); }, "a multisampled depth target rasterized without MSAA_ENABLE with Z interpolation or overrasterization (DB_EQAA)"); + } + queue = drawB(); + queue.context[0x1c4] = 9; + queue.context[0x203] |= 0x100u; + expectFailure([&] { DecodeState(queue); }, "a multisampled depth target rasterized without MSAA_ENABLE with a sample-mask export"); + queue = drawB(); + queue.context[0x2dc] = 0x0000aa01u; + expectFailure([&] { DecodeState(queue); }, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points or alpha-to-coverage"); + queue = drawB(); + queue.context[0x010] = 0xa2900987; + expectFailure([&] { DecodeState(queue); }, "the depth target's sample count differs from the rasterizer's"); +} + void multisampleTests() { using AgcDriver::Graphics::ColorMetadataPass; + using AgcDriver::Graphics::DecodeColorBuffer; using AgcDriver::Graphics::DecodeColorMetadataPass; using AgcDriver::Graphics::DecodeState; const auto multisampled = [] { @@ -2281,8 +3528,131 @@ void multisampleTests() { Require(state.samples == 2 && state.colors.size() == 1 && state.colors[0].samples == 2 && state.colors[0].cmaskAddress == 0, "2x multisampling did not decode"); const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); Require(rejection.empty(), "the precheck refused 2x multisampling: " + rejection); + Require(!state.perSampleShading, "2x multisampling requested sample shading without PS_ITER_SAMPLE"); + queue.context[0x293] = 0x10000u; + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "PS_ITER_SAMPLE on a 2x target was refused"); + Require(DecodeState(queue).perSampleShading, "PS_ITER_SAMPLE on a 2x target did not request sample shading"); + queue.context[0x1b3] = 0x4002u; + queue.context[0x1b4] = 0x4002u; + expectFailure([&] { DecodeState(queue); }, "SAMPLE_COVERAGE in a multisampled pixel shader that runs per sample"); + queue.context[0x293] = 0; + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty() && !DecodeState(queue).perSampleShading, "SAMPLE_COVERAGE with center interpolation on a 2x target was refused"); + for (const auto sampleRate : {0x1u, 0x10u, 0x2000u}) { + queue.context[0x1b3] = 0x4002u | sampleRate; + queue.context[0x1b4] = 0x4002u | sampleRate; + expectFailure([&] { DecodeState(queue); }, "SAMPLE_COVERAGE in a multisampled pixel shader that runs per sample"); + queue.context[0x1b4] = 0x4002u; + Require(DecodeState(queue).samples == 2, "a sample-rate input enabled but not addressed beside SAMPLE_COVERAGE decoded differently"); + } + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + { + auto smooth = multisampled(); + smooth.context[0x2f8] = 0x0020c002u; + smooth.context[0x31d] = 0u; + smooth.context[0x201] = 0x02130000u; + smooth.context[0x1b3] = 0x4002u; + smooth.context[0x1b4] = 0x4002u; + const auto merged = DecodeState(smooth); + Require(merged.samples == 4 && merged.mergedCoverage && merged.colors.size() == 1 && merged.colors[0].samples == 1 && !merged.perSampleShading, "a radeonsi smoothing draw (4 raster samples, OVERRASTERIZATION_AMOUNT 2, one-sample target) did not decode as merged coverage"); + smooth.context[0x201] = 0x01130000u; + expectFailure([&] { DecodeState(smooth); }, "a color target's sample count differs from the rasterizer's"); + smooth.context[0x201] = 0x02130000u; + smooth.context[0x293] = 0x10000u; + expectFailure([&] { DecodeState(smooth); }, "SAMPLE_COVERAGE in a multisampled pixel shader"); + smooth.context[0x1b3] = 2u; + smooth.context[0x1b4] = 2u; + const auto iteratedSmooth = DecodeState(smooth); + Require(iteratedSmooth.samples == 1 && !iteratedSmooth.mergedCoverage && !iteratedSmooth.perSampleShading, "an overrasterized draw that runs per sample into a one-sample target did not rasterize sample 0 alone"); + smooth.context[0x293] = 0u; + smooth.context[0x292] = 2u; + const auto unmerged = DecodeState(smooth); + Require(unmerged.samples == 1 && !unmerged.mergedCoverage, "an overrasterized draw into a one-sample target without MSAA_ENABLE did not rasterize as one sample"); + } queue.context[0x302] = 0x44cc; - expectFailure([&] { DecodeState(queue); }, "nonstandard sample locations"); + state = DecodeState(queue); + Require(state.customSampleLocations && state.sampleLocationGrid, "a standard 2x pattern with its samples swapped in one pixel did not decode as a grid"); + for (const auto [pixel, word] : {std::pair{0u, 0x0c04u}, std::pair{1u, 0x0c04u}, std::pair{2u, 0x040cu}, std::pair{3u, 0x040cu}}) queue.context[0x2fe + pixel * 4u] = word; + state = DecodeState(queue); + Require(state.samples == 2 && state.customSampleLocations && state.sampleLocationGrid, "the 2x checkerboard pattern (PPSA27836 and PPSA01556) did not decode as a grid"); + const auto grid = AgcDriver::Graphics::SampleLocationGrid(state); + Require(grid.width == 2 && grid.height == 2, "a pattern varying between pixels did not use a 2x2 grid"); + const auto checkerboard = AgcDriver::Graphics::SampleLocations(state); + const std::array expectedCheckerboard{{{0.75f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}}}; + for (std::size_t index = 0; index < expectedCheckerboard.size(); ++index) Require(checkerboard[index].x == expectedCheckerboard[index].x && checkerboard[index].y == expectedCheckerboard[index].y, "grid sample locations are not in Vulkan's (x + y * width) * samples + sample order"); + AgcDriver::Graphics::Context gridContext{}; + gridContext.sampleLocationSampleCounts = VK_SAMPLE_COUNT_2_BIT; + Require(AgcDriver::Graphics::ValidateSampleLocations(gridContext, state), "a pattern varying between pixels was refused on a device without a 2x2 sample location grid"); + gridContext.sampleLocationGridSampleCounts = VK_SAMPLE_COUNT_4_BIT; + Require(AgcDriver::Graphics::ValidateSampleLocations(gridContext, state), "a 2x pattern varying between pixels was refused on a device with a 2x2 grid for 4 samples only"); + 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; + Require(AgcDriver::Graphics::ValidateSampleLocations(gridContext, state), "a one-sample pattern varying between pixels was refused on a device without a one-sample 2x2 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}) { + state = DecodeState(withInputs(queue, {ShaderRecompiler::PixelInput::Ancillary, position})); + Require(state.samples == 1 && !state.perSampleShading, "a sample-index and position input into a one-sample target did not rasterize sample 0 alone"); + } + 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_COVERAGE in a multisampled pixel shader that runs per sample"); + state = DecodeState(withInputs(iterated, {ShaderRecompiler::PixelInput::Ancillary, ShaderRecompiler::PixelInput::PositionX})); + Require(state.samples == 1 && !state.perSampleShading, "PS_ITER_SAMPLE with sample-index and position inputs did not rasterize sample 0 alone"); + 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; + state = DecodeState(singleScan); + Require(state.samples == 1 && state.colors.size() == 1 && state.colors[0].samples == 1 && !state.customSampleLocations, "a one-sample target under a 2x scan without MSAA_ENABLE did not rasterize as one sample"); queue = multisampled(); queue.context[0x2f8] = 0; queue.context[0x292] = 2; @@ -2306,7 +3676,7 @@ void multisampleTests() { queue.context[0x31d] = 0x12000; for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x22eea66a; state = DecodeState(queue); - Require(state.samples == 4 && state.customSampleLocations, "uniform custom sample locations were not decoded"); + Require(state.samples == 4 && state.customSampleLocations && !state.sampleLocationGrid, "uniform custom sample locations were not decoded as one pixel's pattern"); const auto locations = AgcDriver::Graphics::SampleLocations(state); const std::array expected{{{0.125f, 0.875f}, {0.875f, 0.125f}, {0.375f, 0.375f}, {0.625f, 0.625f}}}; for (std::size_t sample = 0; sample < expected.size(); ++sample) Require(locations[sample].x == expected[sample].x && locations[sample].y == expected[sample].y, "custom sample coordinates or indices changed"); @@ -2382,10 +3752,10 @@ void multisampleTests() { queue.context[0x2ff + pixel * 4u] = log2 == 3 ? 0x9773f95bu : 0u; } std::vector sampleReads; - AgcDriver::Graphics::RegisterReadLog() = &sampleReads; + std::optional sampleLog(std::in_place, &sampleReads); const auto coveredCenter = DecodeState(queue); const auto coveredRejection = AgcDriver::Graphics::DrawRejection(queue, false); - AgcDriver::Graphics::RegisterReadLog() = nullptr; + sampleLog.reset(); Require(coveredCenter.samples == (1u << log2) && !coveredCenter.customSampleLocations, "covered-centroid mode changed the sample count or locations without centroid inputs"); for (const auto read : sampleReads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a covered-centroid register"); Require(coveredRejection.empty(), "covered-centroid mode without centroid inputs was refused: " + coveredRejection); @@ -2423,10 +3793,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 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"); @@ -2452,12 +3833,18 @@ void multisampleTests() { queue.context[0x31f] = 0; expectFailure([&] { DecodeState(queue); }, "without a CMASK address"); queue = multisampled(); + queue.context[0x31c] |= 0x10000000u; + 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); std::vector reads; - AgcDriver::Graphics::RegisterReadLog() = &reads; - static_cast(DecodeState(queue)); - AgcDriver::Graphics::RegisterReadLog() = nullptr; + { + const AgcDriver::Graphics::RegisterReadLogScope logScope(&reads); + static_cast(DecodeState(queue)); + } for (const auto read : reads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a CMASK register the decoder reads: " + std::to_string(read.offset)); queue = multisampled(); @@ -2479,9 +3866,31 @@ void multisampleTests() { queue.context[0x2f8] = 0x00200002; for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6aeu; expectFailure([&] { DecodeColorMetadataPass(queue); }, "differs from the rasterizer's"); + queue.context[0x292] &= ~1u; + pass = DecodeColorMetadataPass(queue); + Require(pass.has_value() && pass->mode == ColorMetadataPass::Mode::Resolve && pass->source.has_value() && pass->source->samples == 2 && pass->targets.size() == 1, "a CB resolve with MSAA_ENABLE off did not take its sample count from the source target despite a stale PA_SC_AA_CONFIG"); queue = rasterizedResolve; + std::vector resolvedBlock(2u * 65536u); + const auto resolvedAddress = (reinterpret_cast(resolvedBlock.data()) + 65535u) / 65536u * 65536u; + for (const auto mode : {AgcDriver::Graphics::ColorTileMode::Standard4KB, AgcDriver::Graphics::ColorTileMode::Standard64KB, AgcDriver::Graphics::ColorTileMode::RenderTarget}) { + auto tiled = queue; + tiled.context[0x327] = static_cast(resolvedAddress >> 8u); + tiled.context[0x391] = static_cast(resolvedAddress >> 40u); + tiled.context[0x3b9] = (tiled.context[0x3b9] & ~(0x1fu << 14u)) | (static_cast(mode) << 14u); + const auto tiledPass = DecodeColorMetadataPass(tiled); + Require(tiledPass.has_value() && tiledPass->mode == ColorMetadataPass::Mode::Resolve && tiledPass->targets.size() == 1 && tiledPass->targets[0].tileMode == mode && tiledPass->targets[0].samples == 1, "a CB resolve into a tile mode " + std::to_string(static_cast(mode)) + " destination did not decode"); + } 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; + 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; @@ -2497,6 +3906,37 @@ void multisampleTests() { queue.context[0x0] = 0; queue.context[0x202] = 0xcc0020; expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled color target"); + queue.context[0x31c] |= 0x10000000u; + 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; + setCmaskAddress(queue); + 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; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "over a single-sample target"); + queue = multisampled(); + queue.context[0x2f8] = 0; + queue.context[0x0] = 0; + queue.context[0x202] = 0x00cc0050; + queue.context[0x31c] = 0x00008028; + queue.context[0x31d] = 0x00012000; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "FMASK decompress of a target without a CMASK address"); using AgcDriver::Graphics::DccKeys; using AgcDriver::Graphics::MultisampledCmask; @@ -2513,12 +3953,36 @@ void multisampleTests() { Require(!cmask.TakeClear() && cmask.Keys() == DccKeys::Uncompressed, "a fill of another range changed the CMASK"); cmask.CheckAddress(0x10000); expectFailure([&] { cmask.CheckAddress(0x20000); }, "used CMASK 0x20000"); + for (unsigned value = 0; value < 256u; ++value) { + std::optional expected; + if (value == 0u) expected = DccKeys::Clear0000; + else if (value == 0xccu || value == 0xddu || value == 0xeeu || value == 0xffu) expected = DccKeys::Uncompressed; + Require(AgcDriver::Graphics::MultisampledCmaskKeys(static_cast(value)) == expected, "MSAA CMASK byte classification differs at " + std::to_string(value)); + } + std::cout << "All 256 MSAA CMASK byte classifications passed\n"; MultisampledCmask seeded(0x30000, 0x1000); - seeded.SeedKeys(DccKeys::Clear0000); + seeded.SeedCode(0x00u); Require(seeded.TakeClear(), "a fast-cleared seed did not clear its first use"); + Require(!seeded.TakeClear(), "a subsequent use applied the clear again"); MultisampledCmask mixedSeed(0x30000, 0x1000); - mixedSeed.SeedKeys(DccKeys::Mixed); - expectFailure([&] { mixedSeed.TakeClear(); }, "not modeled"); + mixedSeed.SeedCode(std::nullopt); + expectFailure([&] { mixedSeed.TakeClear(); }, "reads as mixed, which is not modeled"); + for (const std::uint8_t expanded : {0xccu, 0xddu, 0xeeu, 0xffu}) { + MultisampledCmask expandedSeed(0x30000, 0x1000); + expandedSeed.SeedCode(expanded); + Require(!expandedSeed.TakeClear() && expandedSeed.Keys() == DccKeys::Uncompressed, "a color-expanded CMASK seed applied a fast clear or was refused"); + MultisampledCmask expandedFill(0x30000, 0x1000); + expandedFill.NoteFill(0x30000, 0x1000, expanded * 0x01010101u); + Require(!expandedFill.TakeClear() && expandedFill.Keys() == DccKeys::Uncompressed, "a color-expanded CMASK fill applied a fast clear or was refused"); + } + for (const std::uint8_t unmodeled : {0x11u, 0x33u, 0x44u, 0x88u, 0xcdu}) { + MultisampledCmask unmodeledSeed(0x30000, 0x1000); + unmodeledSeed.SeedCode(unmodeled); + expectFailure([&] { unmodeledSeed.TakeClear(); }, "reads as code 0x"); + } + MultisampledCmask unevenFill(0x30000, 0x1000); + unevenFill.NoteFill(0x30000, 0x1000, 0xccccccdcu); + expectFailure([&] { unevenFill.TakeClear(); }, "leaves its tiles mixed"); MultisampledCmask unaddressed; unaddressed.NoteFill(0, 0x1000, 0); Require(!unaddressed.TakeClear(), "a target without a CMASK cleared"); @@ -2549,6 +4013,11 @@ void multisampleTests() { Require(MultisampledColorBytes(narrow) == 0x50000, "an 8x 4-byte target is not 32x64 pixels per 64 KiB block"); srgb.extent = {100, 100}; Require(MultisampledColorBytes(srgb) == 0x40000, "a 4x 100x100 4-byte target does not span 2x2 64 KiB blocks"); + for (const auto mode : {ColorTileMode::Z64KBX, ColorTileMode::D64KBX}) { + auto swizzled = srgb; + swizzled.tileMode = mode; + Require(MultisampledColorBytes(swizzled) == 0x40000, "a 4x 100x100 4-byte surface of another 64 KiB swizzle does not span 2x2 64 KiB blocks"); + } srgb.tileMode = ColorTileMode::Standard4KB; Require(MultisampledColorBytes(srgb) == 0x31000, "a 4x 4-byte target is not 16x16 pixels per 4 KiB block"); srgb.tileMode = ColorTileMode::Linear; @@ -2579,6 +4048,83 @@ void multisampleTests() { Require(MultisampledFillClearColor(srgb, 0x80ffffffu, clear) && clear.float32[2] == 1.0f && clear.float32[3] == 128.0f / 255.0f, "an sRGB fill of white with linear alpha 0x80 did not clear to it"); Require(!MultisampledFillClearColor(srgb, 0x00000080u, clear), "an sRGB fill whose color decodes inexactly cleared"); + using AgcDriver::Graphics::MultisampledDccClear; + auto single = srgb; + single.format = VK_FORMAT_R8G8B8A8_UNORM; + single.tileMode = ColorTileMode::RenderTarget; + single.extent = {581, 729}; + single.address = single.surfaceAddress = 0x40010000; + single.dccAddress = 0x50010000; + single.dccPipeAligned = true; + const std::vector singleKeys(AgcDriver::Graphics::MultisampledDccBytes(single), std::byte{0x10}); + expectFailure([&] { MultisampledDccClear(single, singleKeys, {}); }, "comp-to-single keys without a recorded clear color"); + AgcDriver::Graphics::GuestTextureResource cleared{}; + cleared.baseAddress = single.surfaceAddress; + cleared.width = 581; + cleared.height = 729; + cleared.samples = 4; + AgcDriver::Graphics::NoteMultisampledSingleClear(cleared, {0xff00ffccu, 0u, 0u, 0u}); + clear = MultisampledDccClear(single, singleKeys, {}); + Require(clear.float32[0] == 204.0f / 255.0f && clear.float32[1] == 1.0f && clear.float32[2] == 0.0f && clear.float32[3] == 1.0f, "comp-to-single keys did not clear to the recorded RGBA8 color"); + auto resized = single; + resized.extent = {580, 729}; + const std::vector resizedKeys(AgcDriver::Graphics::MultisampledDccBytes(resized), std::byte{0x10}); + expectFailure([&] { MultisampledDccClear(resized, resizedKeys, {}); }, "comp-to-single keys without a recorded clear color"); + auto wide = halfFloat; + wide.surfaceAddress = 0x40000000; + wide.dccAddress = 0x50000000; + wide.dccPipeAligned = true; + AgcDriver::Graphics::GuestTextureResource wideCleared{}; + wideCleared.baseAddress = wide.surfaceAddress; + wideCleared.width = wide.extent.width; + wideCleared.height = wide.extent.height; + wideCleared.samples = wide.samples; + AgcDriver::Graphics::NoteMultisampledSingleClear(wideCleared, {0x3c003c00u, 0x3c003c00u, 0u, 0u}); + const std::vector wideKeys(AgcDriver::Graphics::MultisampledDccBytes(wide), std::byte{0x10}); + clear = MultisampledDccClear(wide, wideKeys, {}); + Require(clear.float32[0] == 1.0f && clear.float32[1] == 1.0f && clear.float32[2] == 1.0f && clear.float32[3] == 1.0f, "comp-to-single keys did not clear to the recorded RGBA16F color"); + auto widest = wide; + widest.format = VK_FORMAT_R32G32B32A32_SFLOAT; + widest.elementBytes = 16; + widest.address = widest.surfaceAddress = 0x48000000; + widest.dccAddress = 0x58000000; + auto widestCleared = wideCleared; + widestCleared.baseAddress = widest.surfaceAddress; + AgcDriver::Graphics::NoteMultisampledSingleClear(widestCleared, {0x3f800000u, 0x3f000000u, 0u, 0x3f800000u}); + const std::vector widestKeys(AgcDriver::Graphics::MultisampledDccBytes(widest), std::byte{0x10}); + clear = MultisampledDccClear(widest, widestKeys, {}); + Require(clear.float32[0] == 1.0f && clear.float32[1] == 0.5f && clear.float32[2] == 0.0f && clear.float32[3] == 1.0f, "comp-to-single keys did not clear to the recorded RGBA32F color"); + auto swizzled = single; + swizzled.pipeBankXor = 0x4000; + expectFailure([&] { MultisampledDccClear(swizzled, singleKeys, {}); }, "comp-to-single keys without a recorded clear color"); + constexpr std::size_t reusedBytes = 0x10000; + auto* reusedBlock = static_cast(::operator new(reusedBytes, std::align_val_t{reusedBytes})); + const auto mapReused = [&] { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, reusedBlock, reusedBytes, true, true, true); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto unmapReused = [&] { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, reusedBlock); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + mapReused(); + auto reused = single; + reused.address = reused.surfaceAddress = reinterpret_cast(reusedBlock); + auto reusedCleared = cleared; + reusedCleared.baseAddress = reused.surfaceAddress; + AgcDriver::Graphics::NoteMultisampledSingleClear(reusedCleared, {0xff00ffccu, 0u, 0u, 0u}); + clear = MultisampledDccClear(reused, singleKeys, {}); + Require(clear.float32[0] == 204.0f / 255.0f && clear.float32[3] == 1.0f, "comp-to-single keys on mapped guest memory did not clear to the recorded color"); + unmapReused(); + mapReused(); + expectFailure([&] { MultisampledDccClear(reused, singleKeys, {}); }, "comp-to-single keys without a recorded clear color"); + clear = MultisampledDccClear(single, singleKeys, {}); + Require(clear.float32[0] == 204.0f / 255.0f, "unmapping one surface dropped another surface's comp-to-single clear"); + unmapReused(); + ::operator delete(reusedBlock, std::align_val_t{reusedBytes}); + using AgcDriver::Graphics::TakeMultisampledClear; halfFloat.clearWords = {0x3c003c00u, 0x3c003c00u}; MultisampledCmask clearedCmask(0x50000000, 0x1000); @@ -2596,6 +4142,166 @@ 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; + state = DecodeState(queue); + Require(state.truncatedCoverage && !state.mergedCoverage && state.colors.size() == 1 && state.colors[0].samples == 1, "a single-sample scan into a 4x depth target and a one-sample color target did not take the color from sample 0"); + requireCentered(state, "a single-sample scan into a 4x depth target and a one-sample color target"); + const auto truncatedTwoSample = [&] { + auto 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[0x010] = 0x22900987; + queue.context[0x31d] = 0; + return queue; + }; + queue = truncatedTwoSample(); + state = DecodeState(queue); + Require(state.samples == 2 && state.depth && state.depth->samples == 2 && state.colors.size() == 1 && state.colors[0].samples == 1 && state.truncatedCoverage && !state.mergedCoverage && !state.perSampleShading, "a one-sample color target beside a 2x depth target under a 2x rasterizer with MSAA_ENABLE (PPSA02225's registers) did not take the color from sample 0"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused a one-sample color target beside a 2x depth target"); + queue = truncatedTwoSample(); + queue.context[0x293] = 0x10000u; + queue.context[0x1b3] = queue.context[0x1b4] = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveSample) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::FrontFace); + state = DecodeState(queue); + Require(state.truncatedCoverage && state.perSampleShading && state.samples == 2 && state.colors[0].samples == 1, "PS_ITER_SAMPLE into a one-sample color target beside a 2x depth target (PPSA02225's registers) did not run per sample with sample 0's color"); + queue = truncatedTwoSample(); + queue.context[0x31d] = 0x12000; + 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; @@ -2620,6 +4326,254 @@ void framebufferLayerTests() { } } +void quadPixelMaskTests() { + using AgcDriver::Graphics::DecodeState; + using AgcDriver::Graphics::DrawRejection; + using AgcDriver::Graphics::HostSampleMask; + using AgcDriver::Graphics::PartialSampleMask; + using AgcDriver::Graphics::QuadPixelMask; + using AgcDriver::Graphics::UniformSampleMask; + using Masks = std::array; + const auto masked = [](std::uint32_t top, std::uint32_t bottom, std::uint32_t log2 = 0u) { + auto queue = makeState(); + queue.context[0x30e] = top; + queue.context[0x30f] = bottom; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + if (log2 != 0u) { + queue.context[0x2f8] = (log2 << 20u) | log2; + queue.context[0x292] = 3; + queue.context[0x3b8] = 0x0dc00000u; + queue.context[0x31d] = (log2 << 12u) | (log2 << 15u); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = log2 == 1u ? 0xcc44u : 0x622ae6aeu; + } + return queue; + }; + const auto accepted = [](const AgcDriver::QueueState& queue, const std::string& what) { + std::vector reads; + AgcDriver::Graphics::RegisterReadLog() = &reads; + const auto state = DecodeState(queue); + const auto rejection = DrawRejection(queue, false); + AgcDriver::Graphics::RegisterReadLog() = nullptr; + Require(rejection.empty(), what + " was refused by the precheck: " + rejection); + for (const auto read : reads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a register decoded for " + what); + return state; + }; + const auto refused = [](const AgcDriver::QueueState& queue, std::string_view reason) { + expectFailure([&] { DecodeState(queue); }, reason); + Require(DrawRejection(queue, false).find(reason) != std::string::npos, "the precheck accepted a draw DecodeState refuses for " + std::string(reason)); + }; + auto state = accepted(masked(0x0000ffffu, 0u), "the X0Y0 single-sample mask"); + Require(state.quadSampleMask == Masks{1, 0, 0, 0} && PartialSampleMask(state) && !UniformSampleMask(state) && QuadPixelMask(state) == 0x1u, "PA_SC_AA_MASK 0x0000ffff/0 did not cover only the even-x, even-y pixel of each quad"); + state = accepted(masked(0xffff0000u, 0u), "the X1Y0 single-sample mask"); + Require(QuadPixelMask(state) == 0x2u, "PA_SC_AA_MASK 0xffff0000/0 did not cover only the odd-x, even-y pixel of each quad"); + state = accepted(masked(0xfffe0001u, 0x0001fffeu), "a single-sample mask with bits above sample 0"); + Require(state.quadSampleMask == Masks{1, 0, 0, 1} && QuadPixelMask(state) == 0x9u, "a single-sample mask did not keep only bit 0 of each pixel's field"); + state = accepted(masked(0x00010001u, 0x00010001u), "a single-sample mask with only sample 0 set"); + Require(!PartialSampleMask(state) && QuadPixelMask(state) == 0xfu && HostSampleMask(state) == 1u, "PA_SC_AA_MASK 0x00010001 at one sample is not a full mask"); + state = accepted(masked(0u, 0u), "an empty single-sample mask"); + Require(PartialSampleMask(state) && UniformSampleMask(state) && HostSampleMask(state) == 0u && QuadPixelMask(state) == 0xfu, "an empty mask equal for the whole quad did not become a host sample mask of 0"); + Require(AgcDriver::Graphics::DecodePixelStageInfo(masked(0x0000ffffu, 0u).context, {}).quadPixelMask == 0xfu, "the pixel stage decode read the sample mask registers instead of the decoded state"); + auto queue = masked(0x0000ffffu, 0u); + queue.context[0x203] = 0x10u; + Require(accepted(queue, "an early-Z masked draw").pixelEarlyZ, "DB_SHADER_CONTROL Z_ORDER EARLY_Z_THEN_LATE_Z did not decode as early Z"); + queue.context[0x203] = 0x50u; + Require(!accepted(queue, "a killing masked draw").pixelEarlyZ, "a pixel program with kill decoded as early Z"); + queue.context[0x313] = 0x6001u; + refused(queue, "sample masks with conservative rasterization"); + queue = masked(0xffffffffu, 0xffffffffu); + queue.context[0x313] = 0x6001u; + Require(!PartialSampleMask(accepted(queue, "a full mask with conservative rasterization")), "a full mask with conservative rasterization decoded as partial"); + state = accepted(masked(0x00050005u, 0x00050005u, 2u), "a 4x mask equal for the whole quad"); + Require(state.samples == 4 && PartialSampleMask(state) && UniformSampleMask(state) && HostSampleMask(state) == 0x5u && QuadPixelMask(state) == 0xfu, "a 4x mask 0x5 equal for the whole quad did not become the host sample mask 0x5"); + state = accepted(masked(0xfff5fff5u, 0xfff5fff5u, 2u), "a 4x mask with bits above sample 3"); + Require(HostSampleMask(state) == 0x5u && state.quadSampleMask == Masks{5, 5, 5, 5}, "a 4x mask kept bits of samples the rasterizer does not have"); + state = accepted(masked(0x0000000fu, 0u, 2u), "a 4x mask that covers whole pixels"); + Require(!UniformSampleMask(state) && QuadPixelMask(state) == 0x1u && state.quadSampleMask == Masks{0xf, 0, 0, 0}, "a 4x mask covering all samples of one quad pixel did not become a quad pixel mask"); + state = accepted(masked(0x00000003u, 0u, 1u), "a 2x mask that covers whole pixels"); + Require(QuadPixelMask(state) == 0x1u, "a 2x mask covering both samples of one quad pixel did not become a quad pixel mask"); + using AgcDriver::Graphics::QuadSampleMasks; + constexpr Masks NoSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; + state = accepted(masked(0x00020001u, 0x00080004u, 2u), "a 4x mask with one sample per quad pixel"); + Require(QuadPixelMask(state) == 0xfu && QuadSampleMasks(state) == Masks{1, 2, 4, 8}, "a 4x mask of sample q for quad pixel q did not become per-pixel sample masks"); + state = accepted(masked(0x0000000fu, 0x00030000u, 2u), "a 4x mask mixing full, empty and partial pixels"); + Require(QuadPixelMask(state) == 0x9u && QuadSampleMasks(state) == Masks{0xffff, 0, 0, 3}, "a 4x mask mixing full, empty and partial pixels did not keep full pixels as all samples"); + Require(QuadSampleMasks(accepted(masked(0x0000000fu, 0u, 2u), "a whole-pixel 4x mask")) == NoSampleMask && QuadSampleMasks(accepted(masked(0x00050005u, 0x00050005u, 2u), "a uniform 4x mask")) == NoSampleMask && QuadSampleMasks(accepted(masked(0x0000ffffu, 0u), "a single-sample mask")) == NoSampleMask, "a mask without partial pixels that differ produced per-pixel sample masks"); + for (const auto centroid : {4u, 64u}) { + queue = masked(0x00020001u, 0x00080004u, 2u); + queue.context[0x1b3] = 2u | centroid; + queue.context[0x1b4] = 2u | centroid; + Require(QuadSampleMasks(accepted(queue, "centroid interpolation under per-pixel sample masks")) == Masks{1, 2, 4, 8}, "centroid interpolation refused per-pixel sample masks"); + } + for (const auto centroid : {4u, 64u}) { + queue = masked(0x00050005u, 0x00050005u, 2u); + queue.context[0x1b3] = 2u | centroid; + queue.context[0x1b4] = 2u | centroid; + refused(queue, "sample masks that remove some samples of a pixel with centroid interpolation"); + queue.context[0x1b4] = 2u; + Require(HostSampleMask(accepted(queue, "an unaddressed centroid input")) == 0x5u, "an unaddressed centroid input refused a 4x mask"); + queue = masked(0x0000000fu, 0u, 2u); + queue.context[0x1b3] = 2u | centroid; + queue.context[0x1b4] = 2u | centroid; + Require(QuadPixelMask(accepted(queue, "centroid interpolation under a whole-pixel mask")) == 0x1u, "centroid interpolation refused a mask that covers whole pixels"); + } + queue = masked(0x0000ffffu, 0u); + queue.context[0x31d] = 0x12000; + state = accepted(queue, "a single-sample mask into a 4x target"); + Require(state.samples == 4 && state.quadSampleMask == Masks{0xf, 0, 0, 0} && QuadPixelMask(state) == 0x1u, "single-sample rasterization into a 4x target did not apply each pixel's sample 0 bit to all of its samples"); + queue.context[0x30e] = queue.context[0x30f] = 0xfffefffeu; + state = accepted(queue, "an empty single-sample mask into a 4x target"); + Require(UniformSampleMask(state) && HostSampleMask(state) == 0u, "an empty single-sample mask into a 4x target did not remove every sample"); + + AgcDriver::Graphics::Context context{}; + ShaderRecompiler::RecompileResult early; + early.spirv = std::vector{spv::MagicNumber, 0x10300u, 0u, 2u, 0u, (3u << 16u) | spv::OpExecutionMode, 1u, spv::ExecutionModeEarlyFragmentTests, (5u << 16u) | spv::OpFunction, 0u, 1u, 0u, 0u}; + ShaderRecompiler::RecompileResult late; + late.spirv = std::vector{spv::MagicNumber, 0x10300u, 0u, 2u, 0u, (3u << 16u) | spv::OpExecutionMode, 1u, spv::ExecutionModeOriginUpperLeft}; + Require(AgcDriver::Graphics::DeclaresEarlyFragmentTests(early) && !AgcDriver::Graphics::DeclaresEarlyFragmentTests(late), "early fragment tests were not read from the SPIR-V execution modes"); + using Stage = ShaderRecompiler::ShaderStage; + AgcDriver::Graphics::State uniform{}; + uniform.samples = 4; + uniform.quadSampleMask = {5, 5, 5, 5}; + const std::array earlyShaders{{{Stage::Fragment, &early, 0}}}; + const std::array lateShaders{{{Stage::Fragment, &late, 0}}}; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, uniform, earlyShaders); }, "earlyFragmentSampleMaskTestBeforeSampleCounting"); + AgcDriver::Graphics::ValidateSampleMask(context, uniform, lateShaders); + context.earlyFragmentSampleMaskTestBeforeSampleCounting = true; + AgcDriver::Graphics::ValidateSampleMask(context, uniform, earlyShaders); + ShaderRecompiler::RecompileResult stores; + ShaderRecompiler::DescriptorBinding written{}; + written.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + written.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + written.count = 1; + written.bufferWritten = {true}; + stores.bindings.push_back(written); + const std::array storing{{{Stage::Vertex, &late, 0}, {Stage::Fragment, &stores, 0}}}; + const std::array vertexStoring{{{Stage::Vertex, &stores, 0}, {Stage::Fragment, &late, 0}}}; + AgcDriver::Graphics::State varying{}; + varying.quadSampleMask = {1, 0, 0, 0}; + varying.pixelEarlyZ = true; + varying.depth = AgcDriver::Graphics::DepthTarget{}; + varying.depthTest = true; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, varying, storing); }, "differs between the pixels of a quad"); + AgcDriver::Graphics::ValidateSampleMask(context, varying, vertexStoring); + for (const auto change : {0, 1, 2}) { + auto relaxed = varying; + if (change == 0) relaxed.pixelEarlyZ = false; + if (change == 1) relaxed.depthTest = false; + if (change == 2) relaxed.quadSampleMask = {1, 1, 1, 1}; + AgcDriver::Graphics::ValidateSampleMask(context, relaxed, storing); + } + varying.depthTest = false; + varying.stencilTest = true; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, varying, storing); }, "differs between the pixels of a quad"); + varying.stencilTest = false; + varying.depthBoundsTest = true; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, varying, storing); }, "differs between the pixels of a quad"); +} + +void DepthRemapTests() { + auto queue = makeState(); + queue.context[0x204] = 0; + queue.context[0x113] = std::bit_cast(1.0f); + queue.context[0x114] = 0; + AgcDriver::Graphics::Context remapping{}; + remapping.depthClamp = true; + auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.negativeOneToOne && state.viewport.minDepth == -1 && state.viewport.maxDepth == 1 && state.depthRemap.mode == 0, "a negative-one-to-one depth transform decoded with a remap"); + AgcDriver::Graphics::ApplyDepthRemap(remapping, state); + Require(state.depthRemap.mode == 2 && state.depthRemap.scale == 1.0f && state.depthRemap.offset == 0.0f, "the remap did not take the viewport's depth scale and offset"); + Require(state.viewport.minDepth == 0 && state.viewport.maxDepth == 1 && !state.negativeOneToOne && state.depthClamp, "the remapped viewport does not clamp at the console's bounds in zero-to-one clip space"); + AgcDriver::Graphics::Context unrestricted{}; + unrestricted.depthRangeUnrestricted = true; + auto kept = AgcDriver::Graphics::DecodeState(queue); + AgcDriver::Graphics::ApplyDepthRemap(unrestricted, kept); + Require(kept.depthRemap.mode == 0 && kept.viewport.minDepth == -1 && kept.negativeOneToOne && !kept.depthClamp, "an unrestricted device remapped the depth range"); + AgcDriver::Graphics::Context noClamp{}; + auto refused = AgcDriver::Graphics::DecodeState(queue); + expectFailure([&] { AgcDriver::Graphics::ApplyDepthRemap(noClamp, refused); }, "depth clamping"); + queue.context[0x113] = std::bit_cast(0.5f); + queue.context[0x114] = std::bit_cast(0.5f); + auto inRange = AgcDriver::Graphics::DecodeState(queue); + AgcDriver::Graphics::ApplyDepthRemap(remapping, inRange); + Require(inRange.depthRemap.mode == 0 && inRange.negativeOneToOne && !inRange.depthClamp, "an in-range depth transform was remapped"); + queue.context[0x204] = 0x80000u; + queue.context[0x113] = std::bit_cast(2.0f); + queue.context[0x114] = std::bit_cast(-0.5f); + queue.context[0xb4] = std::bit_cast(0.25f); + queue.context[0xb5] = std::bit_cast(0.75f); + auto bounded = AgcDriver::Graphics::DecodeState(queue); + AgcDriver::Graphics::ApplyDepthRemap(remapping, bounded); + Require(bounded.depthRemap.mode == 1 && bounded.depthRemap.scale == 4.0f && bounded.depthRemap.offset == -1.5f && bounded.viewport.minDepth == 0.25f && bounded.viewport.maxDepth == 0.75f, "a zero-to-one remap did not divide its transform by the clamp range it gives the viewport"); + queue.context[0xb4] = std::bit_cast(-1.0f); + queue.context[0xb5] = std::bit_cast(2.0f); + auto clipped = AgcDriver::Graphics::DecodeState(queue); + AgcDriver::Graphics::ApplyDepthRemap(remapping, clipped); + Require(clipped.depthRemap.mode == 1 && clipped.viewport.minDepth == 0 && clipped.viewport.maxDepth == 1, "clamp bounds beyond [0, 1] were not clipped to the representable depth range"); + Require(clipped.depthRemap.scale == 2.0f && clipped.depthRemap.offset == -0.5f, "clamp bounds clipped to [0, 1] changed the depth transform"); + auto unorm = AgcDriver::Graphics::DecodeState(queue); + unorm.depth = AgcDriver::Graphics::DepthTarget{0x10000, 0, {64, 64}, VK_FORMAT_D16_UNORM, 0.0f, 0}; + unorm.depthTest = unorm.depthWrite = true; + AgcDriver::Graphics::ApplyDepthRemap(remapping, unorm); + Require(unorm.viewport.minDepth == 0 && unorm.viewport.maxDepth == 1, "a normalized depth target refused clipped clamp bounds"); + auto floating = AgcDriver::Graphics::DecodeState(queue); + floating.depth = AgcDriver::Graphics::DepthTarget{0x10000, 0, {64, 64}, VK_FORMAT_D32_SFLOAT, 0.0f, 0}; + floating.depthWrite = true; + AgcDriver::Graphics::ApplyDepthRemap(remapping, floating); + Require(floating.depthRemap.mode == 1 && floating.viewport.minDepth == 0 && floating.viewport.maxDepth == 1 && floating.depthClamp, "a written float depth target refused clipped clamp bounds"); +} + +void UnusedViewportDepthTests() { + auto queue = makeState(); + queue.context[0x204] = 0; + queue.context[0x113] = std::bit_cast(1.0f); + queue.context[0x114] = 0; + const auto decoded = AgcDriver::Graphics::DecodeState(queue); + Require(!decoded.depth && decoded.viewport.minDepth == -1 && decoded.viewport.maxDepth == 1, "unused depth fixture changed"); + AgcDriver::Graphics::Context context{}; + context.limits.maxViewportDimensions[0] = context.limits.maxViewportDimensions[1] = 16384; + context.limits.viewportBoundsRange[0] = -32768; + context.limits.viewportBoundsRange[1] = 32767; + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.posX = pixel.posY = pixel.posW = true; + auto color = decoded; + AgcDriver::Graphics::NormalizeUnusedViewportDepth(color, pixel); + AgcDriver::Graphics::ValidateViewport(context, color.viewport); + Require(color.viewport.minDepth == 0 && color.viewport.maxDepth == 1, "unused viewport depth is not representable"); + AgcDriver::Graphics::Context remapping{}; + remapping.depthClamp = true; + const auto normalized = color; + AgcDriver::Graphics::ApplyDepthRemap(remapping, color); + Require(color.depthRemap.mode == 0 && color.viewport.minDepth == 0 && color.viewport.maxDepth == 1 && color.negativeOneToOne == normalized.negativeOneToOne && color.depthClamp == normalized.depthClamp, "the depth remap acted on a normalized viewport"); + Require(color.viewport.x == decoded.viewport.x && color.viewport.y == decoded.viewport.y && color.viewport.width == decoded.viewport.width && color.viewport.height == decoded.viewport.height && color.negativeOneToOne == decoded.negativeOneToOne && color.depthClamp == decoded.depthClamp, "unused depth changed coverage or clipping"); + auto observed = decoded; + pixel.posZ = true; + AgcDriver::Graphics::NormalizeUnusedViewportDepth(observed, pixel); + Require(observed.viewport.minDepth == -1, "fragment position depth was changed"); + expectFailure([&] { AgcDriver::Graphics::ValidateViewport(context, observed.viewport); }, "viewport depth outside"); + pixel.posZ = false; + auto attached = decoded; + attached.depth.emplace(); + AgcDriver::Graphics::NormalizeUnusedViewportDepth(attached, pixel); + Require(attached.viewport.minDepth == -1, "attached depth was changed"); + expectFailure([&] { AgcDriver::Graphics::ValidateViewport(context, attached.viewport); }, "viewport depth outside"); + auto reversed = decoded; + reversed.viewport.minDepth = 2; + reversed.viewport.maxDepth = -1; + AgcDriver::Graphics::NormalizeUnusedViewportDepth(reversed, pixel); + AgcDriver::Graphics::ValidateViewport(context, reversed.viewport); + Require(reversed.viewport.minDepth == 1 && reversed.viewport.maxDepth == 0, "reversed unused depth is not representable"); + auto bounded = decoded; + bounded.viewport.minDepth = 0.25f; + bounded.viewport.maxDepth = 0.75f; + AgcDriver::Graphics::NormalizeUnusedViewportDepth(bounded, pixel); + Require(bounded.viewport.minDepth == 0.25f && bounded.viewport.maxDepth == 0.75f, "an in-range viewport changed"); + auto invalid = decoded; + invalid.viewport.minDepth = std::numeric_limits::infinity(); + AgcDriver::Graphics::NormalizeUnusedViewportDepth(invalid, pixel); + expectFailure([&] { AgcDriver::Graphics::ValidateViewport(context, invalid.viewport); }, "non-finite viewport"); +} + void DepthClipTests() { auto queue = makeState(); const auto direct = AgcDriver::Graphics::DecodeState(queue); @@ -2663,7 +4617,7 @@ void DepthClipTests() { collapsedQueue.context[0x10f] = std::bit_cast(-1.0f); expectFailure([&] { AgcDriver::Graphics::DecodeState(collapsedQueue); }, "unsupported viewport transform"); for (std::uint32_t bit = 0; bit < 32; ++bit) { - if (bit == 19 || bit == 24 || bit == 26 || bit == 27) continue; + if (bit == 16 || bit == 19 || bit == 24 || bit == 26 || bit == 27) continue; for (const auto linearBit : {0u, 0x01000000u}) { queue.context[0x204] = (1u << bit) | linearBit; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "PA_CL_CLIP_CNTL"); @@ -2676,10 +4630,12 @@ void DepthClipTests() { for (const auto clip : {0x00080000u, 0x00000000u, 0x0c080000u, 0x04000000u}) { queue.context[0x204] = clip; const auto plain = AgcDriver::Graphics::DecodeState(queue); - queue.context[0x204] = clip | 0x01000000u; - const auto linearClip = AgcDriver::Graphics::DecodeState(queue); - Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected DX_LINEAR_ATTR_CLIP_ENA"); - Require(linearClip.negativeOneToOne == plain.negativeOneToOne && linearClip.depthClamp == plain.depthClamp && linearClip.viewport.minDepth == plain.viewport.minDepth && linearClip.viewport.maxDepth == plain.viewport.maxDepth, "DX_LINEAR_ATTR_CLIP_ENA changed the clip space, depth clamping or depth range"); + for (const auto extraBit : {0x00010000u, 0x01000000u, 0x01010000u}) { + queue.context[0x204] = clip | extraBit; + const auto decoded = AgcDriver::Graphics::DecodeState(queue); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected CLIP_DISABLE or DX_LINEAR_ATTR_CLIP_ENA"); + Require(decoded.negativeOneToOne == plain.negativeOneToOne && decoded.depthClamp == plain.depthClamp && decoded.viewport.minDepth == plain.viewport.minDepth && decoded.viewport.maxDepth == plain.viewport.maxDepth, "CLIP_DISABLE or DX_LINEAR_ATTR_CLIP_ENA changed the clip space, depth clamping or depth range"); + } } } @@ -2822,11 +4778,47 @@ struct MockDescriptorWrite { std::uint32_t count; VkDescriptorType type; std::vector buffers; + 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; + bool deferFences = false; + std::map signaled; + std::uint64_t fenceWaits = 0; + struct BufferCopy { VkBuffer source; VkBuffer destination; VkBufferCopy region; }; + std::vector bufferCopies; + struct ImageBufferCopy { VkImage image; VkBuffer buffer; VkBufferImageCopy region; bool fromImage; }; + std::vector imageBufferCopies; + std::set destroyedImages; std::map bufferSizes; std::map bufferUsage; std::map allocationFlags; @@ -2837,14 +4829,17 @@ struct MockVulkan { VkDeviceSize allocatedBytes = 0; std::optional memoryLimit; std::uint64_t allocationAttempts = 0; + std::optional importLimit; std::uint32_t bufferMemoryTypeBits = 1; std::vector allocationTypes; std::vector layoutBindings; VkDescriptorSetLayoutCreateFlags layoutFlags = 0; std::vector layoutBindingFlags; + std::map variableLayoutCounts; std::vector poolSizes; std::uint32_t poolMaxSets = 0; VkDescriptorPoolCreateFlags poolFlags = 0; + std::uint32_t freedDescriptorSets = 0; struct DescriptorPool { VkDescriptorPoolCreateFlags flags; std::uint32_t maxSets; @@ -2855,7 +4850,10 @@ struct MockVulkan { std::map descriptorPools; std::uint32_t descriptorPoolLimit = 1000000; VkResult descriptorResetResult = VK_SUCCESS; + std::optional variableDescriptorCount; std::vector writes; + std::map samplerFlags; + std::uint32_t samplerCreateCount = 0; std::vector copies; bool allowCopies = false; std::uint32_t boundSets = 0; @@ -2874,6 +4872,11 @@ struct MockVulkan { }; MockVulkan mock; +std::mutex pipelineCreateMutex; +std::condition_variable pipelineCreateCondition; +bool blockLibraryCreate = false; +bool libraryCreateEntered = false; +bool releaseLibraryCreate = false; template THandle makeHandle() { @@ -2898,6 +4901,7 @@ VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAlloca ++mock.allocationAttempts; mock.allocationTypes.push_back(info->memoryTypeIndex); if (mock.memoryLimit.has_value() && mock.allocatedBytes + info->allocationSize > *mock.memoryLimit) return VK_ERROR_OUT_OF_DEVICE_MEMORY; + if (mock.importLimit.has_value() && info->allocationSize > *mock.importLimit && info->pNext != nullptr && static_cast(info->pNext)->pNext != nullptr) return VK_ERROR_OUT_OF_DEVICE_MEMORY; *memory = makeHandle(); mock.memories[*memory] = std::vector(info->allocationSize); mock.allocationSizes[*memory] = info->allocationSize; @@ -2943,6 +4947,23 @@ VKAPI_ATTR void VKAPI_CALL mockFreeMemory(VkDevice, VkDeviceMemory memory, const --mock.live; } +VKAPI_ATTR VkResult VKAPI_CALL mockCreateImage(VkDevice, const VkImageCreateInfo*, const VkAllocationCallbacks*, VkImage* image) { *image = makeHandle(); ++mock.live; return VK_SUCCESS; } +VKAPI_ATTR void VKAPI_CALL mockGetImageMemoryRequirements(VkDevice, VkImage, VkMemoryRequirements* requirements) { *requirements = {4096, 1, 1}; } +VKAPI_ATTR VkResult VKAPI_CALL mockBindImageMemory(VkDevice, VkImage, VkDeviceMemory, VkDeviceSize) { return VK_SUCCESS; } +VKAPI_ATTR void VKAPI_CALL mockDestroyImage(VkDevice, VkImage image, const VkAllocationCallbacks*) { mock.destroyedImages.insert(image); --mock.live; } +VKAPI_ATTR VkResult VKAPI_CALL mockCreateImageView(VkDevice, const VkImageViewCreateInfo*, const VkAllocationCallbacks*, VkImageView* view) { *view = makeHandle(); ++mock.live; return VK_SUCCESS; } +VKAPI_ATTR void VKAPI_CALL mockDestroyImageView(VkDevice, VkImageView, const VkAllocationCallbacks*) { --mock.live; } +VKAPI_ATTR void VKAPI_CALL mockFormatProperties(VkPhysicalDevice, VkFormat, VkFormatProperties* properties) { + *properties = {}; + properties->optimalTilingFeatures = VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT; +} +VKAPI_ATTR void VKAPI_CALL mockCmdCopyImageToBuffer(VkCommandBuffer, VkImage image, VkImageLayout, VkBuffer buffer, std::uint32_t count, const VkBufferImageCopy* regions) { + for (std::uint32_t index = 0; index < count; ++index) mock.imageBufferCopies.push_back({image, buffer, regions[index], true}); +} +VKAPI_ATTR void VKAPI_CALL mockCmdCopyBufferToImage(VkCommandBuffer, VkBuffer buffer, VkImage image, VkImageLayout, std::uint32_t count, const VkBufferImageCopy* regions) { + for (std::uint32_t index = 0; index < count; ++index) mock.imageBufferCopies.push_back({image, buffer, regions[index], false}); +} + VKAPI_ATTR VkResult VKAPI_CALL mockCreateDescriptorSetLayout(VkDevice, const VkDescriptorSetLayoutCreateInfo* info, const VkAllocationCallbacks*, VkDescriptorSetLayout* layout) { *layout = makeHandle(); mock.layoutBindings.assign(info->pBindings, info->pBindings + info->bindingCount); @@ -2950,11 +4971,15 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreateDescriptorSetLayout(VkDevice, const VkD const auto* bindingFlags = static_cast(info->pNext); if (bindingFlags != nullptr) mock.layoutBindingFlags.assign(bindingFlags->pBindingFlags, bindingFlags->pBindingFlags + bindingFlags->bindingCount); else mock.layoutBindingFlags.clear(); + for (std::size_t index = 0; index < mock.layoutBindingFlags.size(); ++index) { + if ((mock.layoutBindingFlags[index] & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) != 0) mock.variableLayoutCounts[*layout] = info->pBindings[index].descriptorCount; + } ++mock.live; return VK_SUCCESS; } -VKAPI_ATTR void VKAPI_CALL mockDestroyDescriptorSetLayout(VkDevice, VkDescriptorSetLayout, const VkAllocationCallbacks*) { +VKAPI_ATTR void VKAPI_CALL mockDestroyDescriptorSetLayout(VkDevice, VkDescriptorSetLayout layout, const VkAllocationCallbacks*) { + mock.variableLayoutCounts.erase(layout); --mock.live; } @@ -2979,6 +5004,13 @@ VKAPI_ATTR VkResult VKAPI_CALL mockAllocateDescriptorSets(VkDevice, const VkDesc ++pool.attempts; if (pool.allocated == pool.maxSets) return VK_ERROR_OUT_OF_POOL_MEMORY; ++pool.allocated; + mock.variableDescriptorCount.reset(); + if (info->pNext != nullptr) { + const auto* variable = static_cast(info->pNext); + Require(variable->sType == VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_ALLOCATE_INFO && variable->descriptorSetCount == 1, "variable descriptor allocation is malformed"); + mock.variableDescriptorCount = variable->pDescriptorCounts[0]; + Require(*mock.variableDescriptorCount <= mock.variableLayoutCounts.at(info->pSetLayouts[0]), "variable descriptor count exceeds its layout binding"); + } sets[0] = makeHandle(); return VK_SUCCESS; } @@ -2993,12 +5025,35 @@ 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; + ++mock.samplerCreateCount; + ++mock.live; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL mockDestroySampler(VkDevice, VkSampler sampler, const VkAllocationCallbacks*) { + mock.samplerFlags.erase(sampler); + --mock.live; +} + VKAPI_ATTR void VKAPI_CALL mockUpdateDescriptorSets(VkDevice, std::uint32_t count, const VkWriteDescriptorSet* writes, std::uint32_t copyCount, const VkCopyDescriptorSet* copies) { Require(copyCount == 0 || mock.allowCopies, "descriptor copies are not expected"); if (copyCount != 0) mock.copies.insert(mock.copies.end(), copies, copies + copyCount); for (std::uint32_t i = 0; i < count; ++i) { - MockDescriptorWrite write{writes[i].dstBinding, writes[i].dstArrayElement, writes[i].descriptorCount, writes[i].descriptorType, {}}; - write.buffers.assign(writes[i].pBufferInfo, writes[i].pBufferInfo + writes[i].descriptorCount); + MockDescriptorWrite write{writes[i].dstBinding, writes[i].dstArrayElement, writes[i].descriptorCount, writes[i].descriptorType, {}, {}}; + if (writes[i].pBufferInfo != nullptr) write.buffers.assign(writes[i].pBufferInfo, writes[i].pBufferInfo + writes[i].descriptorCount); + if (writes[i].pImageInfo != nullptr) write.images.assign(writes[i].pImageInfo, writes[i].pImageInfo + writes[i].descriptorCount); mock.writes.push_back(write); } } @@ -3033,7 +5088,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; @@ -3101,14 +5172,17 @@ VKAPI_ATTR VkResult VKAPI_CALL mockAllocateCommandBuffers(VkDevice, const VkComm } VKAPI_ATTR void VKAPI_CALL mockFreeCommandBuffers(VkDevice, VkCommandPool, std::uint32_t count, const VkCommandBuffer*) { mock.live -= count; } -VKAPI_ATTR VkResult VKAPI_CALL mockCreateFence(VkDevice, const VkFenceCreateInfo*, const VkAllocationCallbacks*, VkFence* fence) { *fence = makeHandle(); ++mock.live; return VK_SUCCESS; } -VKAPI_ATTR void VKAPI_CALL mockDestroyFence(VkDevice, VkFence, const VkAllocationCallbacks*) { --mock.live; } -VKAPI_ATTR VkResult VKAPI_CALL mockGetFenceStatus(VkDevice, VkFence) { return VK_SUCCESS; } -VKAPI_ATTR VkResult VKAPI_CALL mockResetFences(VkDevice, std::uint32_t, const VkFence*) { return VK_SUCCESS; } -VKAPI_ATTR VkResult VKAPI_CALL mockWaitForFences(VkDevice, std::uint32_t, const VkFence*, VkBool32, std::uint64_t) { return VK_SUCCESS; } +VKAPI_ATTR VkResult VKAPI_CALL mockCreateFence(VkDevice, const VkFenceCreateInfo*, const VkAllocationCallbacks*, VkFence* fence) { *fence = makeHandle(); mock.signaled[*fence] = !mock.deferFences; ++mock.live; return VK_SUCCESS; } +VKAPI_ATTR void VKAPI_CALL mockDestroyFence(VkDevice, VkFence fence, const VkAllocationCallbacks*) { mock.signaled.erase(fence); --mock.live; } +VKAPI_ATTR VkResult VKAPI_CALL mockGetFenceStatus(VkDevice, VkFence fence) { return mock.signaled.at(fence) ? VK_SUCCESS : VK_NOT_READY; } +VKAPI_ATTR VkResult VKAPI_CALL mockResetFences(VkDevice, std::uint32_t count, const VkFence* fences) { for (std::uint32_t index = 0; index < count; ++index) mock.signaled.at(fences[index]) = !mock.deferFences; return VK_SUCCESS; } +VKAPI_ATTR VkResult VKAPI_CALL mockWaitForFences(VkDevice, std::uint32_t count, const VkFence* fences, VkBool32, std::uint64_t) { for (std::uint32_t index = 0; index < count; ++index) mock.signaled.at(fences[index]) = true; ++mock.fenceWaits; 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 source, VkBuffer destination, std::uint32_t count, const VkBufferCopy* regions) { + for (std::uint32_t index = 0; index < count; ++index) mock.bufferCopies.push_back({source, destination, regions[index]}); +} VKAPI_ATTR void VKAPI_CALL mockCmdPipelineBarrier(VkCommandBuffer, VkPipelineStageFlags, VkPipelineStageFlags, VkDependencyFlags, std::uint32_t, const VkMemoryBarrier*, std::uint32_t, const VkBufferMemoryBarrier*, std::uint32_t, const VkImageMemoryBarrier*) {} VKAPI_ATTR void VKAPI_CALL mockCmdBeginQuery(VkCommandBuffer, VkQueryPool, std::uint32_t, VkQueryControlFlags) {} VKAPI_ATTR void VKAPI_CALL mockCmdEndQuery(VkCommandBuffer, VkQueryPool, std::uint32_t) {} @@ -3127,6 +5201,7 @@ PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { {"vkBeginCommandBuffer", reinterpret_cast(mockBeginCommandBuffer)}, {"vkEndCommandBuffer", reinterpret_cast(mockEndCommandBuffer)}, {"vkQueueSubmit", reinterpret_cast(mockQueueSubmit)}, + {"vkCmdCopyBuffer", reinterpret_cast(mockCmdCopyBuffer)}, {"vkCmdPipelineBarrier", reinterpret_cast(mockCmdPipelineBarrier)}, {"vkCmdBeginQuery", reinterpret_cast(mockCmdBeginQuery)}, {"vkCmdEndQuery", reinterpret_cast(mockCmdEndQuery)}, @@ -3148,7 +5223,10 @@ 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)}, {"vkUpdateDescriptorSets", reinterpret_cast(mockUpdateDescriptorSets)}, {"vkCmdBindDescriptorSets", reinterpret_cast(mockCmdBindDescriptorSets)}, {"vkCreateGraphicsPipelines", reinterpret_cast(mockCreateGraphicsPipelines)}, @@ -3167,6 +5245,21 @@ PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { return it == table.end() ? nullptr : it->second; } +PFN_vkVoidFunction VKAPI_CALL mockDepthCopyProc(VkDevice device, const char* name) { + static const std::map table{ + {"vkCreateImage", reinterpret_cast(mockCreateImage)}, + {"vkGetImageMemoryRequirements", reinterpret_cast(mockGetImageMemoryRequirements)}, + {"vkBindImageMemory", reinterpret_cast(mockBindImageMemory)}, + {"vkDestroyImage", reinterpret_cast(mockDestroyImage)}, + {"vkCreateImageView", reinterpret_cast(mockCreateImageView)}, + {"vkDestroyImageView", reinterpret_cast(mockDestroyImageView)}, + {"vkCmdCopyImageToBuffer", reinterpret_cast(mockCmdCopyImageToBuffer)}, + {"vkCmdCopyBufferToImage", reinterpret_cast(mockCmdCopyBufferToImage)}, + }; + const auto found = table.find(name); + return found != table.end() ? found->second : mockProc(device, name); +} + AgcDriver::Graphics::Context mockContext() { AgcDriver::Graphics::Context context{}; context.deviceProc = mockProc; @@ -3179,9 +5272,121 @@ AgcDriver::Graphics::Context mockContext() { context.limits.maxPerStageDescriptorStorageBuffers = 16; context.limits.maxPerStageResources = 128; context.limits.maxDescriptorSetStorageBuffers = 32; + context.limits.maxImageArrayLayers = 2048; return context; } +struct CommandBatchFailureProbe { + VkResult allocation = VK_SUCCESS; + VkResult creation = VK_SUCCESS; + VkResult begin = VK_SUCCESS; + VkFence failedFence = VK_NULL_HANDLE; + std::uint32_t freed = 0; + std::uint32_t creates = 0; + std::uint32_t destroys = 0; + std::uint32_t invalidDestroys = 0; + std::uint32_t begins = 0; +}; + +CommandBatchFailureProbe batchProbe; + +VKAPI_ATTR VkResult VKAPI_CALL batchAllocateCommands(VkDevice device, const VkCommandBufferAllocateInfo* info, VkCommandBuffer* commands) { + if (batchProbe.allocation != VK_SUCCESS) { + std::fill_n(commands, info->commandBufferCount, VK_NULL_HANDLE); + return batchProbe.allocation; + } + return mockAllocateCommandBuffers(device, info, commands); +} + +VKAPI_ATTR VkResult VKAPI_CALL batchCreateFence(VkDevice device, const VkFenceCreateInfo* info, const VkAllocationCallbacks* allocator, VkFence* fence) { + ++batchProbe.creates; + if (batchProbe.creation != VK_SUCCESS) { + *fence = batchProbe.failedFence; + return batchProbe.creation; + } + return mockCreateFence(device, info, allocator, fence); +} + +VKAPI_ATTR void VKAPI_CALL batchFreeCommands(VkDevice device, VkCommandPool pool, std::uint32_t count, const VkCommandBuffer* commands) { + batchProbe.freed += count; + mockFreeCommandBuffers(device, pool, count, commands); +} + +VKAPI_ATTR void VKAPI_CALL batchDestroyFence(VkDevice device, VkFence fence, const VkAllocationCallbacks* allocator) { + ++batchProbe.destroys; + if (fence != VK_NULL_HANDLE && fence == batchProbe.failedFence) { + ++batchProbe.invalidDestroys; + return; + } + mockDestroyFence(device, fence, allocator); +} + +VKAPI_ATTR VkResult VKAPI_CALL batchBeginCommands(VkCommandBuffer commands, const VkCommandBufferBeginInfo* info) { + ++batchProbe.begins; + if (batchProbe.begin != VK_SUCCESS) return batchProbe.begin; + return mockBeginCommandBuffer(commands, info); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL batchFailureProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkAllocateCommandBuffers") == 0) return reinterpret_cast(batchAllocateCommands); + if (std::strcmp(name, "vkCreateFence") == 0) return reinterpret_cast(batchCreateFence); + if (std::strcmp(name, "vkFreeCommandBuffers") == 0) return reinterpret_cast(batchFreeCommands); + if (std::strcmp(name, "vkDestroyFence") == 0) return reinterpret_cast(batchDestroyFence); + if (std::strcmp(name, "vkBeginCommandBuffer") == 0) return reinterpret_cast(batchBeginCommands); + return mockProc(device, name); +} + +void commandBatchFenceFailureTests() { + using namespace AgcDriver::Graphics; + const std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + std::uint32_t invalidDestroys = 0; + for (const auto result : {VK_ERROR_OUT_OF_HOST_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}) { + for (const bool nonnull : {false, true}) { + mock = MockVulkan{}; + batchProbe = {}; + batchProbe.creation = result; + batchProbe.failedFence = nonnull ? reinterpret_cast(std::uintptr_t{0xdeadbeef}) : VK_NULL_HANDLE; + auto context = mockContext(); + context.deviceProc = batchFailureProc; + bool failed = false; + try { + CommandBatch batch(context); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + failed = message.find("vkCreateFence") != std::string::npos && message.find("Vulkan result " + std::to_string(result)) != std::string::npos; + } + Require(failed && batchProbe.creates == 1 && batchProbe.begins == 0, "fence creation failure lost its error or continued construction"); + Require(mock.live == 0 && batchProbe.freed == 1, "fence creation failure leaked the allocated command buffer"); + invalidDestroys += batchProbe.invalidDestroys; + std::printf("command batch fence result=%d nonnull=%d invalid destroys=%u\n", result, nonnull, batchProbe.invalidDestroys); + const auto destroyed = batchProbe.destroys; + const auto invalid = batchProbe.invalidDestroys; + batchProbe.creation = VK_SUCCESS; + { + CommandBatch recovered(context); + Require(recovered.Handle() != VK_NULL_HANDLE, "command batch did not recover after fence creation failed"); + } + Require(mock.live == 0 && batchProbe.freed == 2 && batchProbe.destroys == destroyed + 1 && batchProbe.invalidDestroys == invalid, "recovered command batch mishandled its owned objects"); + } + } + for (const bool allocated : {false, true}) { + mock = MockVulkan{}; + batchProbe = {}; + batchProbe.allocation = allocated ? VK_SUCCESS : VK_ERROR_OUT_OF_HOST_MEMORY; + batchProbe.begin = VK_ERROR_OUT_OF_HOST_MEMORY; + auto context = mockContext(); + context.deviceProc = batchFailureProc; + bool failed = false; + try { + CommandBatch batch(context); + } catch (const std::runtime_error& error) { + failed = std::string(error.what()).find(allocated ? "vkBeginCommandBuffer" : "vkAllocateCommandBuffers") != std::string::npos; + } + Require(failed && mock.live == 0 && batchProbe.freed == (allocated ? 1u : 0u) && batchProbe.destroys == (allocated ? 1u : 0u) && batchProbe.invalidDestroys == 0, "command batch constructor failure mishandled its owned objects"); + } + Require(invalidDestroys == 0, "command batch destroyed undefined failed fence outputs"); +} + void addressSpaceTeardownTests() { #ifdef _WIN32 _putenv_s("APS5_NO_LEASE_MIRROR", "1"); @@ -3248,6 +5453,168 @@ void bufferBudgetPlacementTests() { } } } + +void copyInputKeptBytesTests() { + using namespace AgcDriver::Graphics; + constexpr std::size_t piece = 65536; + constexpr std::size_t bytes = 32; + constexpr auto properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + auto block = std::unique_ptr(static_cast(::operator new(piece, std::align_val_t{piece})), [](std::byte* value) { ::operator delete(value, std::align_val_t{piece}); }); + std::fill_n(block.get(), piece, std::byte{0}); + const auto base = reinterpret_cast(block.get()); + const auto registry = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block.get(), piece, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block.get()); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + mock = MockVulkan{}; + mock.deferFences = true; + { + auto context = mockContext(); + context.device = makeHandle(); + context.bufferDeviceAddress = true; + context.hostImportAlignment = piece; + context.limits.minStorageBufferOffsetAlignment = 256; + context.limits.maxMemoryAllocationCount = 4096; + SetImportWatch(context, ImportWatch::Unwatch); + const auto pool = GetBufferPool(context); + registry(true); + const auto unregister = [&](std::byte*) { registry(false); }; + std::unique_ptr registered(block.get(), unregister); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Recorder recorder(context); + recorder.Activate(); + { + GuestBufferMemory aligned(context); + aligned.AddReadable(base + 256, bytes); + aligned.Upload(false); + Require(mock.bufferCopies.empty() && recorder.OpenKeptBytes() == 0, "an aligned imported view made or charged an input snapshot"); + aligned.WriteBack(); + } + std::vector snapshots; + for (unsigned batch = 0; batch < 3; ++batch) { + recorder.Keep(std::make_shared(0), Recorder::KeptBytesBudget - bytes); + block[4] = static_cast(batch + 1); + { + GuestBufferMemory memory(context); + memory.AddReadable(base + 4, bytes); + memory.Upload(false); + Require(mock.bufferCopies.size() == batch + 1, "a misaligned imported view did not record exactly one GPU copy"); + const auto& copy = mock.bufferCopies.back(); + const auto* imported = HostImportFor(context, base + 4, bytes); + Require(imported != nullptr && copy.source != imported->buffer && copy.region.srcOffset == 0 && copy.region.size == bytes, "the read-only GPU copy did not use its own exact input snapshot"); + snapshots.push_back(copy.source); + Require(mockBufferMemory(copy.source).front() == static_cast(batch + 1), "the input snapshot missed the guest bytes at record time"); + block[4] = std::byte{0x7f}; + Require(mockBufferMemory(copy.source).front() == static_cast(batch + 1), "a later CPU store changed the recorded input snapshot"); + memory.WriteBack(); + } + Require(recorder.OpenKeptBytes() == Recorder::KeptBytesBudget, "the actual GPU-copy input snapshot did not complete the kept-byte budget"); + Require(!pool->Take(bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, properties), "an input snapshot became reusable before its GPU fence"); + recorder.BoundKeptBytes(); + Require(recorder.Submissions() == batch + 1 && !recorder.Recording(), "the copy input charge did not submit a full budget batch"); + if (batch < 2) { + Require(!recorder.Reap() && mock.fenceWaits == 0, "a pending copy batch was reaped or waited before exceeding two budgets"); + Require(recorder.InFlightKeptBytes() == (batch + 1) * Recorder::KeptBytesBudget, "submitted copy input bytes were lost from the in-flight count"); + } + } + Require(mock.fenceWaits == 1 && recorder.InFlightBatches() == 2 && recorder.InFlightKeptBytes() == 2 * Recorder::KeptBytesBudget, "three copy input batches did not retire just the oldest budget"); + const auto released = pool->Take(bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, properties); + Require(released.has_value() && released->buffer == snapshots.front(), "the completed batch did not release its input snapshot for reuse"); + Require(!pool->Take(bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, properties), "an unfinished copy batch released its input snapshot"); + pool->Put(*released); + recorder.Sync(); + Require(recorder.InFlightKeptBytes() == 0, "completed copy inputs still count as in flight"); + } + registered.reset(); + ClearHostImports(context.device); + } + Require(mock.live == 0, "copy input accounting tests leaked Vulkan objects"); +} + +void depthCopyKeptBytesTests() { + using namespace AgcDriver::Graphics; + constexpr std::size_t bytes = (8 * 8 + 4 * 4) * sizeof(float); + constexpr auto properties = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + const auto usage = BufferPool::Usage(VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, properties); + alignas(256) std::array guest{}; + mock = MockVulkan{}; + { + auto sourceContext = mockContext(); + sourceContext.device = makeHandle(); + sourceContext.deviceProc = mockDepthCopyProc; + sourceContext.formatProperties = mockFormatProperties; + sourceContext.memory.memoryTypes[0].propertyFlags |= properties; + sourceContext.memory.memoryHeapCount = 1; + sourceContext.memory.memoryHeaps[0] = {std::uint64_t{4} << 30u, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + sourceContext.limits.maxMemoryAllocationCount = 4096; + GetBufferPool(sourceContext); + TextureDetiler detiler(sourceContext); + GuestTextureResource resource{}; + resource.baseAddress = reinterpret_cast(guest.data()); + resource.width = 8; + resource.height = 8; + resource.mipCount = 2; + resource.lastLevel = 1; + resource.tileMode = TextureTileMode::kLinear; + resource.dimension = TextureDimension::k2D; + resource.format = static_cast(ShaderRecompiler::IrBufferFormat::Format32Float); + auto source = StorageTexture::Create(sourceContext, detiler, resource, 0); + const auto sourceImage = source->Image(); + std::weak_ptr sourceLifetime = source; + auto context = sourceContext; + context.bufferPool = std::make_shared(context); + const auto pool = context.bufferPool; + mock.bufferCopies.clear(); + mock.imageBufferCopies.clear(); + mock.fenceWaits = 0; + mock.deferFences = true; + std::vector linears; + std::vector depths; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Recorder recorder(context); + recorder.Activate(); + for (unsigned batch = 0; batch < 3; ++batch) { + recorder.Keep(std::make_shared(0), Recorder::KeptBytesBudget - bytes); + { + Texture depth(context, source, resource, {}, Texture::DepthCopyTag{}); + Require(depth.RefreshDepthCopy(), "a fresh depth copy did not record its linear transfer"); + const auto begin = batch * 4; + Require(mock.imageBufferCopies.size() == begin + 4, "a depth refresh did not copy both mips through its linear buffer"); + const auto& first = mock.imageBufferCopies[begin]; + linears.push_back(first.buffer); + depths.push_back(mock.imageBufferCopies[begin + 2].image); + for (unsigned mip = 0; mip < 2; ++mip) { + const auto& from = mock.imageBufferCopies[begin + mip]; + const auto& to = mock.imageBufferCopies[begin + 2 + mip]; + Require(from.fromImage && !to.fromImage && from.image == sourceImage && to.image == depths.back() && from.buffer == linears.back() && to.buffer == linears.back(), "the depth refresh did not retain one linear buffer between its image transfers"); + Require(from.region.imageSubresource.mipLevel == mip && to.region.imageSubresource.mipLevel == mip && from.region.imageSubresource.aspectMask == VK_IMAGE_ASPECT_COLOR_BIT && to.region.imageSubresource.aspectMask == VK_IMAGE_ASPECT_DEPTH_BIT && from.region.bufferOffset == (mip == 0 ? 0 : 8 * 8 * sizeof(float)) && to.region.bufferOffset == from.region.bufferOffset, "the depth refresh's mip offsets or aspects changed"); + } + Require(!depth.RefreshDepthCopy() && mock.imageBufferCopies.size() == begin + 4, "an unchanged storage version made another linear depth copy"); + } + Require(recorder.OpenKeptBytes() == Recorder::KeptBytesBudget, "the real depth-copy linear buffer did not complete the kept-byte budget"); + Require(!pool->Take(bytes, usage, properties) && !mock.destroyedImages.contains(depths.back()), "a recorded depth transfer released its linear buffer or destination before its fence"); + recorder.BoundKeptBytes(); + Require(recorder.Submissions() == batch + 1 && !recorder.Recording(), "the depth-copy charge did not submit a full budget batch"); + if (batch < 2) Require(!recorder.Reap() && mock.fenceWaits == 0, "a pending depth-copy batch was reaped or waited too soon"); + } + source.reset(); + Require(!sourceLifetime.expired() && !mock.destroyedImages.contains(sourceImage), "a pending depth copy released its storage source"); + Require(mock.fenceWaits == 1 && recorder.InFlightBatches() == 2 && recorder.InFlightKeptBytes() == 2 * Recorder::KeptBytesBudget, "three depth-copy budgets did not retire just the oldest batch"); + const auto released = pool->Take(bytes, usage, properties); + Require(released.has_value() && released->buffer == linears.front() && mock.destroyedImages.contains(depths.front()), "the completed depth-copy batch did not release its linear buffer and destination"); + Require(!pool->Take(bytes, usage, properties) && !mock.destroyedImages.contains(depths[1]) && !mock.destroyedImages.contains(depths[2]), "an unfinished depth-copy batch released its resources"); + pool->Put(*released); + recorder.Sync(); + Require(recorder.InFlightKeptBytes() == 0, "completed depth-copy buffers still count as in flight"); + } + Require(sourceLifetime.expired() && mock.destroyedImages.contains(sourceImage) && mock.destroyedImages.contains(depths[1]) && mock.destroyedImages.contains(depths[2]), "the completed depth-copy batches kept their source or destinations after teardown"); + } + Require(mock.live == 0, "depth copy accounting tests leaked Vulkan objects"); +} using Kind = ShaderRecompiler::DescriptorKind; alignas(16) std::array guestFirst{0x11111111, 0x22222222, 0x33333333, 0x44444444}; @@ -3324,12 +5691,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{}; @@ -3446,6 +5863,16 @@ void pushConstantTests() { fragment.pushConstants.clear(); shaders[1].pushConstantOffset = 999; Require(AgcDriver::Graphics::PushConstantStages(shaders) == VK_SHADER_STAGE_VERTEX_BIT, "empty push constants contributed a stage"); + { + ShaderRecompiler::RecompileResult reading; + reading.readsDepthRemapPush = true; + std::vector encoded; + ShaderRecompiler::ShaderDiskCache::EncodeResult(reading, encoded); + ShaderRecompiler::RecompileResult replayed; + Require(ShaderRecompiler::ShaderDiskCache::DecodeResult(encoded, replayed), "a result that reads the depth remap push words does not decode"); + const std::array remapped{{{ShaderRecompiler::ShaderStage::Vertex, &replayed, 0}}}; + Require(replayed.pushConstants.empty() && AgcDriver::Graphics::PushConstantStages(remapped) == VK_SHADER_STAGE_VERTEX_BIT, "a journalled vertex shader that reads the depth remap push words lost its push stage"); + } Require(AgcDriver::Graphics::AssemblePushConstants(shaders)[8] == std::byte{0}, "empty push constants were copied"); shaders[1].program = nullptr; expectFailure([&] { AgcDriver::Graphics::AssemblePushConstants(shaders); }, "missing compiled shader"); @@ -3474,10 +5901,12 @@ void descriptorSnapshotTests() { const auto original = findWrite(0).buffers; const auto originalThird = findWrite(4).buffers; std::vector moved; + std::array, 2> words; for (std::size_t index = 0; index < 2; ++index) { if ((selected & (1u << index)) == 0) continue; const std::size_t bytes = index == 0 ? 24 : 12; - moved.push_back({index, 0, bytes, std::vector(bytes / 4, static_cast(0x12340000u + index))}); + words[index].assign(bytes / 4, static_cast(0x12340000u + index)); + moved.push_back({index, 0, bytes, words[index]}); } mock.writes.clear(); const auto snapshots = resources.PrepareDrawBindings(recorder, moved); @@ -3506,6 +5935,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(); @@ -3625,12 +6200,22 @@ void resourceTests() { expectSingleFailure(changed([](auto& binding) { binding.role = Role::ShaderData; }), "invalid compact shader data size"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::ShaderData; binding.guestDescriptor = ShaderDataWords({}); binding.guestDescriptor.pop_back(); }), "invalid compact shader data size"); 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"); + expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x40000000u; }), "a V# with reserved bit 30"); 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"); + const auto stridedBefore = [&](std::uint32_t past) { + constexpr std::uint32_t stride = 60; + constexpr std::uint32_t records = 4; + auto descriptor = vsharp(reinterpret_cast(state.color.address - (stride * records - past)), records); + descriptor[1] |= stride << 16u; + return descriptor; + }; + expectSingleAccepted(changed([&](auto& binding) { binding.guestDescriptor = stridedBefore(8); binding.bufferWritten = {false}; }), "a read-only strided V# whose last record runs 8 bytes into the render target"); + expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = stridedBefore(8); binding.bufferWritten = {true}; }), "aliases the render target"); + expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = stridedBefore(60); }), "aliases the render target"); expectSingleAccepted(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast(0x1000), 8)); }), "an unmapped V# element"); expectSingleFailure(changed([&](auto& binding) { binding.count = 2; binding.guestDescriptor = join(vsharp(guestFirst.data(), 16), vsharp(reinterpret_cast(state.color.address), 64)); }), "aliases the render target"); expectSingleFailure(changed([](auto& binding) { binding.count = 17; binding.guestDescriptor.assign(68, 0); }), "per-stage limits"); @@ -3682,6 +6267,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; @@ -3738,19 +6335,27 @@ void descriptorCacheTests() { Require(mock.poolSizes.size() == 2 && mock.poolSizes[0].type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER && mock.poolSizes[0].descriptorCount == 4097 && mock.poolSizes[1].type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE && mock.poolSizes[1].descriptorCount == 3, "the dedicated pool is not sized to its set"); Require(cache.Counters().pools == 0 && cache.Counters().sets == 1, "an oversized set was counted in the chain pools"); cache.Free(dedicated); - Require(mock.live == live, "freeing an oversized set did not destroy its pool"); + Require(mock.live == live + 1 && cache.Counters().keptSets == 1, "freeing an oversized set did not keep its set and pool"); + const auto again = cache.Allocate(large, oversized); + Require(again.set == dedicated.set && again.pool == dedicated.pool && mock.live == live + 1 && cache.Counters().reusedSets == 1 && cache.Counters().dedicatedPools == 1, "an oversized set of the kept set's layout and sizes did not take the kept set"); + cache.Free(again); + const std::array otherSizes{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4098}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 3}}}; + const auto other = cache.Allocate(large, otherSizes); + Require(other.pool != dedicated.pool && mock.live == live + 2 && cache.Counters().dedicatedPools == 2, "an oversized set of other sizes took a kept set"); + cache.Free(other); + Require(mock.live == live + 2 && cache.Counters().keptSets == 3, "freeing a second oversized set did not keep it"); const std::array fitting{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4096}}}; const auto chained = cache.Allocate(layout(4096), fitting); Require(chained.set != VK_NULL_HANDLE && mock.poolMaxSets == 1024 && mock.poolFlags == 0 && cache.Counters().pools == 1, "a set the chain pool holds left the chain"); cache.Free(chained); - Require(mock.descriptorPools.at(chained.pool).resets == 1 && mock.live == live + 2, "a chain pool was not reset after its last set"); + Require(mock.descriptorPools.at(chained.pool).resets == 1 && mock.live == live + 4, "a chain pool was not reset after its last set"); const std::array regular{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4096}}}; const auto first = cache.Allocate(layout(4096), regular); const auto oversizedMixed = cache.Allocate(large, oversized); const auto second = cache.Allocate(layout(4096), regular); Require(first.pool == chained.pool && second.pool == chained.pool && oversizedMixed.pool != chained.pool && mock.descriptorPools.at(chained.pool).resets == 1, "an oversized set disturbed the chain pool"); cache.Free(oversizedMixed); - Require(mock.live == live + 2 && mock.descriptorPools.count(oversizedMixed.pool) == 0 && mock.descriptorPools.at(chained.pool).resets == 1, "freeing an oversized set between chain sets touched the chain pool"); + Require(oversizedMixed.pool == dedicated.pool && mock.live == live + 4 && mock.descriptorPools.count(oversizedMixed.pool) == 1 && mock.descriptorPools.at(chained.pool).resets == 1, "freeing an oversized set between chain sets touched the chain pool"); cache.Free(first); Require(mock.descriptorPools.at(chained.pool).resets == 1, "a chain pool reset while a set was live"); cache.Free(second); @@ -3761,8 +6366,445 @@ void descriptorCacheTests() { expectFailure([&] { cache.Allocate(large, storageImages); }, "descriptor set exceeds the device's per-set descriptor limit"); const std::array uniformBuffers{{{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1}}}; expectFailure([&] { cache.Allocate(large, uniformBuffers); }, "descriptor set uses an unsupported descriptor type 6"); + const auto keptBefore = mock.live; + std::vector spread; + for (std::uint32_t index = 0; index < 10; ++index) { + const std::array sized{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4100 + index}}}; + spread.push_back(cache.Allocate(large, sized)); + } + Require(mock.live == keptBefore + 10, "ten oversized sets of new sizes did not get ten pools"); + for (const auto& set : spread) cache.Free(set); + Require(mock.live == keptBefore - 2 + 8, "freeing more oversized sets than the cache keeps did not destroy the oldest pools"); } Require(mock.live == 0, "the descriptor cache leaked a pool or layout"); + mock = MockVulkan{}; + context.limits.maxDescriptorSetStorageBuffers = 16; + context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers = 8192; + context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 16384; + context.limits.maxPerStageDescriptorSampledImages = 2048; + { + AgcDriver::Graphics::DescriptorCache cache(context); + const std::array bindings{{{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4097, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}, {1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 32, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}}}; + constexpr auto tableFlags = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; + const std::array flags{VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT, tableFlags}; + const std::array key{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4097, VK_SHADER_STAGE_FRAGMENT_BIT, 1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 32, VK_SHADER_STAGE_FRAGMENT_BIT, VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT}; + const auto layout = cache.Layout(key, bindings, true, flags); + Require(mock.layoutFlags == VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT && mock.layoutBindingFlags == std::vector{VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT, tableFlags}, "a variable image table lost its flags when the storage binding used update-after-bind"); + Require(cache.Layout(key, bindings, true, flags) == layout && cache.Counters().layoutHits == 1, "the combined descriptor layout was not reused"); + const std::array oversized{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4097}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 3}}}; + const auto dedicated = cache.Allocate(layout, oversized, true, 3u); + Require(mock.poolMaxSets == 1 && mock.poolFlags == VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT && mock.variableDescriptorCount == 3u, "a dedicated update-after-bind pool lost the variable image count"); + cache.Free(dedicated); + auto fittingBindings = bindings; + fittingBindings[0].descriptorCount = 17; + auto fittingKey = key; + fittingKey[2] = 17; + const auto fittingLayout = cache.Layout(fittingKey, fittingBindings, true, flags); + const std::array fitting{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 17}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 3}}}; + const auto chained = cache.Allocate(fittingLayout, fitting, true, 3u); + Require(mock.poolMaxSets == 1024 && mock.poolFlags == VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT && mock.variableDescriptorCount == 3u, "a chained update-after-bind pool lost the variable image count"); + cache.Free(chained); + const auto reused = cache.Allocate(fittingLayout, fitting, true, 0u); + Require(reused.pool == chained.pool && mock.variableDescriptorCount == 0u, "reusing an update-after-bind pool lost an empty variable image table"); + cache.Free(reused); + const auto rewritten = cache.AllocateRewritten(fittingLayout, fitting, true, 3u); + cache.Recycle(fittingLayout, rewritten); + const std::array grown{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 17}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 5}}}; + const auto larger = cache.AllocateRewritten(fittingLayout, grown, true, 5u); + Require(larger.set != rewritten.set && mock.variableDescriptorCount == 5u, "a recycled descriptor set served a larger variable image table than it was allocated for"); + const auto same = cache.AllocateRewritten(fittingLayout, fitting, true, 3u); + Require(same.set == rewritten.set, "a recycled descriptor set was not reused for a variable image table of its own size"); + cache.Free(larger); + cache.Free(same); + 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() { + namespace Abi = ShaderRecompiler::ImageTableAbi; + for (const bool cached : {false, true}) { + for (const std::uint32_t count : {0u, 2u, 3u}) { + mock = MockVulkan{}; + auto context = mockContext(); + context.imageTableCapacity = 2; + context.limits.maxDescriptorSetSampledImages = 2; + context.limits.maxPerStageDescriptorSampledImages = 2; + { + AgcDriver::Graphics::DescriptorCache cache(context); + if (cached) context.descriptorCache = &cache; + ShaderRecompiler::RecompileResult program; + auto map = makeBinding(Role::ImageTableMap, static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap), 1, std::vector(Abi::HeaderWords + count)); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderElementCount] = count; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords + count; + program.bindings.push_back(map); + auto images = makeBinding(Role::ImageTable, ShaderRecompiler::RuntimeAbi::ImageTableBinding, count, std::vector(count * 8u)); + images.kind = Kind::SampledImage; + images.imageShapes.resize(count); + images.imageDepthCompare.resize(count); + images.imageSamplers.resize(count); + program.bindings.push_back(std::move(images)); + const AgcDriver::Graphics::CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + { + AgcDriver::Graphics::ShaderResources resources(context, shader, {}, true); + Require(findLayoutBinding(ShaderRecompiler::RuntimeAbi::ImageTableBinding).descriptorCount == 2u, "the image table layout changed its device capacity"); + const auto physicalCount = std::max(1u, std::min(count, 2u)); + Require(mock.variableDescriptorCount == physicalCount, "image table allocation did not reserve its bounded physical descriptor count"); + const auto sampled = std::find_if(mock.poolSizes.begin(), mock.poolSizes.end(), [](const auto& size) { return size.type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; }); + Require(sampled != mock.poolSizes.end() && sampled->descriptorCount >= physicalCount && (cached || sampled->descriptorCount == physicalCount), "the image table pool did not cover its bounded allocation"); + resources.Complete(); + const auto& bytes = bufferBytes(findWrite(map.binding).buffers.at(0).buffer); + std::vector actual(bytes.size() / sizeof(std::uint32_t)); + std::memcpy(actual.data(), bytes.data(), bytes.size()); + Require(actual[Abi::HeaderElementCount] == count, "bounding the Vulkan allocation truncated the guest table domain"); + if (count > context.imageTableCapacity) Require((actual[Abi::HeaderWords + 2u] & Abi::PoisonFlag) != 0u, "an image beyond Vulkan capacity lost its deferred fault"); + } + } + Require(mock.live == 0, "the bounded image table leaked Vulkan objects"); + } + } +} + +void imageTableSnapshotAllocationTests() { + namespace Abi = ShaderRecompiler::ImageTableAbi; + using namespace AgcDriver::Graphics; + mock = MockVulkan{}; + auto context = mockContext(); + context.imageTableCapacity = 2; + context.limits.maxDescriptorSetSampledImages = 2; + context.limits.maxPerStageDescriptorSampledImages = 2; + { + DescriptorCache cache(context); + context.descriptorCache = &cache; + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Recorder recorder(context); + ShaderRecompiler::RecompileResult vertex, fragment; + auto buffer = makeBinding(Role::GuestBuffers, 0, 1, vsharp(guestFirst.data(), 16)); + buffer.bufferWritten = {false}; + vertex.bindings.push_back(buffer); + auto map = makeBinding(Role::ImageTableMap, static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap), 1, std::vector(Abi::HeaderWords)); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords; + vertex.bindings.push_back(map); + auto images = makeBinding(Role::ImageTable, ShaderRecompiler::RuntimeAbi::ImageTableBinding, 0, {}); + images.kind = Kind::SampledImage; + vertex.bindings.push_back(images); + const auto state = DecodeState(makeState()); + ShaderResources resources(context, vertex, fragment, state.color, 0, 0); + const auto originalMap = findWrite(map.binding).buffers.at(0); + const std::array movedWords{11, 22, 33, 44}; + const std::array moved{{{0, 0, 16, movedWords}}}; + const auto sets = cache.Counters().sets; + mock.writes.clear(); + mock.variableDescriptorCount.reset(); + const auto snapshots = resources.PrepareDrawBindings(recorder, moved); + Require(snapshots != nullptr && snapshots->snapshots.size() == 1 && cache.Counters().sets == sets + 1, "an empty image table prevented draw snapshot allocation"); + Require(mock.variableDescriptorCount == 1u, "an empty draw snapshot table did not reserve one physical image descriptor"); + Require(sameBytes(bufferBytes(snapshots->snapshots[0].buffer->Handle()), moved[0].words.data(), moved[0].size), "an empty image table changed draw snapshot contents"); + const auto& currentMap = findWrite(map.binding).buffers.at(0); + Require(currentMap.buffer == originalMap.buffer && currentMap.offset == originalMap.offset && currentMap.range == originalMap.range, "draw snapshots replaced the empty image table map"); + std::array header{}; + std::memcpy(header.data(), bufferBytes(currentMap.buffer).data() + currentMap.offset, sizeof(header)); + Require(header[Abi::HeaderElementCount] == 0u, "draw snapshots changed the empty guest table domain"); + Require(std::none_of(mock.writes.begin(), mock.writes.end(), [](const auto& write) { return write.binding == ShaderRecompiler::RuntimeAbi::ImageTableBinding; }), "an empty draw snapshot table wrote an image descriptor"); + recorder.Sync(); + } + Require(mock.live == 0, "the empty image table draw snapshot leaked Vulkan objects"); +} + +void samplerTableNonSeamlessCubeTests() { + namespace Abi = ShaderRecompiler::ImageTableAbi; + constexpr auto count = 2u * Abi::SamplerEntryElements; + for (const bool supported : {false, true}) { + for (const std::uint32_t flaggedEntry : {0u, 1u}) { + mock = MockVulkan{}; + auto context = mockContext(); + context.nonSeamlessCubeMap = supported; + context.samplerTableCapacity = count; + context.limits.maxDescriptorSetSamplers = count; + context.limits.maxPerStageDescriptorSamplers = count; + context.limits.maxSamplerAnisotropy = 1.0f; + ShaderRecompiler::RecompileResult program; + auto map = makeBinding(Role::ImageTableMap, static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap), 1, std::vector(Abi::HeaderWords + 2u)); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderSamplerCount] = count; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords; + program.bindings.push_back(map); + auto samplers = makeBinding(Role::SamplerTable, static_cast(ShaderRecompiler::RuntimeAbi::Binding::SamplerTable), count, {}); + samplers.kind = Kind::Sampler; + for (std::uint32_t entry = 0; entry < 2u; ++entry) { + const std::array words{0x92u | (entry == flaggedEntry ? 1u << 28u : 0u), (4u * 256u) << 12u, 0u, 0u}; + for (std::uint32_t variant = 0; variant < Abi::SamplerEntryElements; ++variant) { + samplers.guestDescriptor.insert(samplers.guestDescriptor.end(), words.begin(), words.end()); + samplers.samplerDepthCompare.push_back(variant >= 2u); + } + } + const auto samplerBinding = samplers.binding; + program.bindings.push_back(std::move(samplers)); + const AgcDriver::Graphics::CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + { + AgcDriver::Graphics::ShaderResources resources(context, shader, {}, true); + resources.Complete(); + const auto& bytes = bufferBytes(findWrite(map.binding).buffers.at(0).buffer); + std::vector actual(bytes.size() / sizeof(std::uint32_t)); + std::memcpy(actual.data(), bytes.data(), bytes.size()); + Require(actual[Abi::HeaderSamplerCount] == count, "non-seamless sampler rejection changed the guest table domain"); + Require(actual[Abi::HeaderPoisonCount] == (supported ? 0u : 1u), "non-seamless sampler table has the wrong deferred poison count"); + for (std::uint32_t entry = 0; entry < 2u; ++entry) { + const auto expected = !supported && entry == flaggedEntry ? Abi::PoisonCode(0u) : 0u; + Require(actual[Abi::HeaderWords + entry] == expected, "non-seamless sampler rejection poisoned the wrong entry"); + } + const auto& write = findWrite(samplerBinding); + const auto first = supported ? 0u : (1u - flaggedEntry) * Abi::SamplerEntryElements; + const auto written = supported ? count : Abi::SamplerEntryElements; + Require(findLayoutBinding(samplerBinding).descriptorCount == count && write.type == VK_DESCRIPTOR_TYPE_SAMPLER && write.arrayElement == first && write.count == written && write.images.size() == written, "non-seamless sampler rejection compacted or truncated descriptor indices"); + Require(mock.samplerFlags.size() == written, "a rejected sampler table entry allocated a Vulkan sampler"); + for (std::uint32_t index = 0; index < written; ++index) { + const auto expected = supported && (first + index) / Abi::SamplerEntryElements == flaggedEntry ? VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT : 0u; + Require(mock.samplerFlags.at(write.images[index].sampler) == expected, "a sampler table variant has the wrong non-seamless cube creation flag"); + } + } + Require(mock.live == 0 && mock.samplerFlags.empty(), "the non-seamless sampler table leaked Vulkan objects"); + } + } +} + +void samplerTableDeviceLimitTests(std::optional selected = std::nullopt) { + namespace Abi = ShaderRecompiler::ImageTableAbi; + constexpr auto count = 2u * Abi::SamplerEntryElements; + const std::array valid{0x92u, (4u * 256u) << 12u, 0u, 0u}; + const std::array, 3> invalid{{ + {0x92u, valid[1], 256u, 0u}, + {0x92u | (2u << 9u), valid[1], (2u << 20u) | (2u << 22u), 0u}, + {0x92u | (1u << 29u), valid[1], 0u, 0u} + }}; + for (std::uint32_t kind = 0; kind < invalid.size(); ++kind) { + if (selected.has_value() && kind != *selected) continue; + for (const std::uint32_t badEntry : {0u, 1u}) { + mock = MockVulkan{}; + auto context = mockContext(); + context.samplerTableCapacity = count; + context.limits.maxDescriptorSetSamplers = count; + context.limits.maxPerStageDescriptorSamplers = count; + context.samplerAnisotropy = true; + context.samplerFilterMinmax = true; + context.limits.maxSamplerAnisotropy = 2.0f; + context.limits.maxSamplerLodBias = 0.5f; + ShaderRecompiler::RecompileResult program; + auto map = makeBinding(Role::ImageTableMap, static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap), 1, std::vector(Abi::HeaderWords + 2u)); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderSamplerCount] = count; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords; + program.bindings.push_back(map); + auto samplers = makeBinding(Role::SamplerTable, static_cast(ShaderRecompiler::RuntimeAbi::Binding::SamplerTable), count, {}); + samplers.kind = Kind::Sampler; + for (std::uint32_t entry = 0; entry < 2u; ++entry) { + const auto& words = entry == badEntry ? invalid[kind] : valid; + for (std::uint32_t variant = 0; variant < Abi::SamplerEntryElements; ++variant) { + samplers.guestDescriptor.insert(samplers.guestDescriptor.end(), words.begin(), words.end()); + samplers.samplerDepthCompare.push_back(variant >= 2u); + } + } + const auto samplerBinding = samplers.binding; + program.bindings.push_back(std::move(samplers)); + const AgcDriver::Graphics::CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + { + AgcDriver::Graphics::ShaderResources resources(context, shader, {}, true); + resources.Complete(); + const auto& bytes = bufferBytes(findWrite(map.binding).buffers.at(0).buffer); + std::vector actual(bytes.size() / sizeof(std::uint32_t)); + std::memcpy(actual.data(), bytes.data(), bytes.size()); + Require(actual[Abi::HeaderSamplerCount] == count && actual[Abi::HeaderPoisonCount] == 1u, "sampler device validation changed the domain or deferred poison count"); + for (std::uint32_t entry = 0; entry < 2u; ++entry) { + Require(actual[Abi::HeaderWords + entry] == (entry == badEntry ? Abi::PoisonCode(0u) : 0u), "sampler device validation poisoned the valid entry"); + } + const auto& write = findWrite(samplerBinding); + Require(write.type == VK_DESCRIPTOR_TYPE_SAMPLER && write.arrayElement == (1u - badEntry) * Abi::SamplerEntryElements && write.count == Abi::SamplerEntryElements && write.images.size() == Abi::SamplerEntryElements, "sampler device validation changed valid descriptor indices"); + Require(mock.samplerCreateCount == Abi::SamplerEntryElements && mock.samplerFlags.size() == Abi::SamplerEntryElements, "a rejected sampler table entry created a Vulkan sampler"); + } + Require(mock.live == 0 && mock.samplerFlags.empty(), "sampler device validation leaked Vulkan objects"); + } + } +} + +void hostImportBudgetTests() { + using AgcDriver::Graphics::HostImportBudget; + constexpr std::uint64_t MiB = 1ull << 20u; + constexpr std::uint64_t GiB = 1ull << 30u; + VkPhysicalDeviceMemoryProperties memory{}; + Require(HostImportBudget(memory, 0) == 6 * GiB, "a device without memory heaps does not keep the 6 GiB import budget"); + Require(HostImportBudget(memory, 32 * GiB) == 16 * GiB, "a device without a host heap does not take half the physical memory"); + memory.memoryHeapCount = 3; + memory.memoryHeaps[0] = {7920 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + memory.memoryHeaps[1] = {8128 * MiB, 0}; + memory.memoryHeaps[2] = {256 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + Require(HostImportBudget(memory, 16 * GiB) == 4064 * MiB, "imports on a 16 GiB machine take more than half its 8128 MiB host heap"); + memory.memoryHeaps[1].size = 16 * GiB; + Require(HostImportBudget(memory, 32 * GiB) == 8 * GiB, "imports on a 32 GiB machine take more than half its 16 GiB host heap"); + memory.memoryHeaps[1].size = 32 * GiB; + Require(HostImportBudget(memory, 64 * GiB) == 16 * GiB, "imports on a 64 GiB machine take more than half its 32 GiB host heap"); + memory.memoryHeaps[1].size = 3 * GiB; + Require(HostImportBudget(memory, 16 * GiB) == 1 * GiB, "a 3 GiB host heap does not keep its 2 GiB reserve"); + memory.memoryHeaps[1].size = 8128 * MiB; + memory.memoryHeaps[0].size = 64 * GiB; + Require(HostImportBudget(memory, 16 * GiB) == 4064 * MiB, "a device-local heap counted as the host heap"); + memory.memoryHeapCount = 1; + Require(HostImportBudget(memory, 16 * GiB) == 8 * GiB, "a host heap past memoryHeapCount limits the import budget"); +} + +void tableUpdateAfterBindTests() { + namespace Abi = ShaderRecompiler::ImageTableAbi; + using namespace AgcDriver::Graphics; + auto argumentLimits = mockContext(); + argumentLimits.runtimeDescriptorArray = true; + argumentLimits.partiallyBound = true; + argumentLimits.variableDescriptorCount = true; + argumentLimits.descriptorIndexing = true; + argumentLimits.limits.maxPerStageDescriptorSamplers = 16; + argumentLimits.limits.maxPerStageDescriptorSampledImages = 256; + argumentLimits.limits.maxPerStageResources = 287; + argumentLimits.limits.maxDescriptorSetSamplers = 80; + argumentLimits.limits.maxDescriptorSetSampledImages = 1280; + argumentLimits.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindSamplers = 500000; + argumentLimits.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindSampledImages = 1000000; + argumentLimits.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 1000000; + argumentLimits.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindSamplers = 500000; + argumentLimits.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindSampledImages = 1000000; + Require(ImageTableCapacity(argumentLimits) == 0u && SamplerTableCapacity(argumentLimits) == 0u && !ImageTableUpdateAfterBind(argumentLimits) && !SamplerTableUpdateAfterBind(argumentLimits), "tables beyond the regular descriptor limits were enabled without update-after-bind sampled images"); + argumentLimits.sampledImageUpdateAfterBind = true; + Require(ImageTableCapacity(argumentLimits) == ShaderRecompiler::RuntimeAbi::StageCount * Abi::ImageElementBudget && ImageTableUpdateAfterBind(argumentLimits), "an image table beyond the regular descriptor limits did not take the update-after-bind limits"); + Require(SamplerTableCapacity(argumentLimits) == Abi::SamplerElementBudget && SamplerTableUpdateAfterBind(argumentLimits), "a sampler table beyond the regular descriptor limits did not take the update-after-bind limits"); + auto regularLimits = argumentLimits; + regularLimits.limits.maxPerStageDescriptorSamplers = 1048576; + regularLimits.limits.maxPerStageDescriptorSampledImages = 1048576; + regularLimits.limits.maxPerStageResources = 1048576; + regularLimits.limits.maxDescriptorSetSamplers = 1048576; + regularLimits.limits.maxDescriptorSetSampledImages = 1048576; + Require(ImageTableCapacity(regularLimits) == ShaderRecompiler::RuntimeAbi::StageCount * Abi::ImageElementBudget && !ImageTableUpdateAfterBind(regularLimits), "an image table within the regular descriptor limits took the update-after-bind path"); + Require(SamplerTableCapacity(regularLimits) == Abi::SamplerElementBudget && !SamplerTableUpdateAfterBind(regularLimits), "a sampler table within the regular descriptor limits took the update-after-bind path"); + + const auto tableContext = [] { + auto context = mockContext(); + context.sampledImageUpdateAfterBind = true; + context.limits.maxPerStageDescriptorSampledImages = 1; + context.limits.maxDescriptorSetSampledImages = 1; + context.limits.maxPerStageDescriptorSamplers = 1; + context.limits.maxDescriptorSetSamplers = 1; + context.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindSampledImages = 2; + context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindSampledImages = 2; + context.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindSamplers = 2u * Abi::SamplerEntryElements; + context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindSamplers = 2u * Abi::SamplerEntryElements; + context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 128; + context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers = 32; + context.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindStorageBuffers = 16; + return context; + }; + const auto requireTableLayout = [](std::uint32_t tableBinding, VkDescriptorBindingFlags tableFlags, std::string_view what) { + Require(mock.layoutFlags == VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT, std::string(what) + " did not get an update-after-bind layout"); + Require(mock.layoutBindingFlags.size() == mock.layoutBindings.size(), std::string(what) + " lost its binding flags"); + for (std::size_t index = 0; index < mock.layoutBindings.size(); ++index) { + const auto expected = mock.layoutBindings[index].binding == tableBinding ? tableFlags | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0u; + Require(mock.layoutBindingFlags[index] == expected, std::string(what) + " put update-after-bind on the wrong bindings"); + } + Require(mock.poolFlags == VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT, std::string(what) + " did not come from an update-after-bind pool"); + }; + for (const bool cached : {false, true}) { + mock = MockVulkan{}; + auto context = tableContext(); + context.imageTableCapacity = 2; + context.imageTableUpdateAfterBind = true; + { + DescriptorCache cache(context); + if (cached) context.descriptorCache = &cache; + ShaderRecompiler::RecompileResult program; + auto map = makeBinding(Role::ImageTableMap, static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap), 1, std::vector(Abi::HeaderWords + 2u)); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderElementCount] = 2u; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords + 2u; + program.bindings.push_back(map); + auto images = makeBinding(Role::ImageTable, ShaderRecompiler::RuntimeAbi::ImageTableBinding, 2u, std::vector(16u)); + images.kind = Kind::SampledImage; + images.imageShapes.resize(2u); + images.imageDepthCompare.resize(2u); + images.imageSamplers.resize(2u); + program.bindings.push_back(std::move(images)); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + { + ShaderResources resources(context, shader, {}, true); + requireTableLayout(ShaderRecompiler::RuntimeAbi::ImageTableBinding, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT, "an image table beyond the regular limits"); + if (cached) Require(DescribeSetLayouts(context, std::span(&shader, 1)).layouts.main == resources.Layout(), "the described image table layout differs from the built one"); + } + } + Require(mock.live == 0, "the update-after-bind image table leaked Vulkan objects"); + } + for (const bool cached : {false, true}) { + mock = MockVulkan{}; + auto context = tableContext(); + constexpr auto count = 2u * Abi::SamplerEntryElements; + context.samplerTableCapacity = count; + context.samplerTableUpdateAfterBind = true; + context.samplerAnisotropy = true; + context.samplerFilterMinmax = true; + context.limits.maxSamplerAnisotropy = 2.0f; + context.limits.maxSamplerLodBias = 0.5f; + { + DescriptorCache cache(context); + if (cached) context.descriptorCache = &cache; + ShaderRecompiler::RecompileResult program; + auto map = makeBinding(Role::ImageTableMap, static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap), 1, std::vector(Abi::HeaderWords + 2u)); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderSamplerCount] = count; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords; + program.bindings.push_back(map); + const auto samplerBinding = static_cast(ShaderRecompiler::RuntimeAbi::Binding::SamplerTable); + auto samplers = makeBinding(Role::SamplerTable, samplerBinding, count, {}); + samplers.kind = Kind::Sampler; + const std::array valid{0x92u, (4u * 256u) << 12u, 0u, 0u}; + for (std::uint32_t element = 0; element < count; ++element) { + samplers.guestDescriptor.insert(samplers.guestDescriptor.end(), valid.begin(), valid.end()); + samplers.samplerDepthCompare.push_back(false); + } + program.bindings.push_back(std::move(samplers)); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + { + ShaderResources resources(context, shader, {}, true); + resources.Complete(); + requireTableLayout(samplerBinding, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT, "a sampler table beyond the regular limits"); + Require(findWrite(samplerBinding).count == count, "the update-after-bind sampler table did not write its samplers"); + if (cached) Require(DescribeSetLayouts(context, std::span(&shader, 1)).layouts.main == resources.Layout(), "the described sampler table layout differs from the built one"); + } + } + Require(mock.live == 0 && mock.samplerFlags.empty(), "the update-after-bind sampler table leaked Vulkan objects"); + } } void textureCacheBudgetTests() { @@ -3810,6 +6852,74 @@ void sampledTextureBudgetTests() { Require(SampledTextureBudget(memory, &reported, 0) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of sampled textures"); } +void storageTextureBudgetTests() { + using AgcDriver::Graphics::StorageTextureBudget; + constexpr std::uint64_t GiB = 1ull << 30u; + constexpr std::uint64_t MiB = 1ull << 20u; + VkPhysicalDeviceMemoryProperties memory{}; + memory.memoryHeapCount = 3; + memory.memoryHeaps[0] = {256 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + 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, 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, 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, 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 + 512 * MiB; + Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2304 * MiB, "a 10 GiB budget with 5632 MiB used outside the texture caches does not leave 2304 MiB after 1 GiB of sampled textures and 1280 MiB of headroom"); + reported.heapUsage[2] = 9 * GiB; + Require(StorageTextureBudget(memory, &reported, 3 * GiB, 2 * GiB) == 1792 * MiB, "3 GiB of sampled textures were not reserved in full"); + reported.heapUsage[2] = 1 * GiB; + Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2560 * MiB, "usage below the cached bytes is not read as no other device memory in use"); + Require(StorageTextureBudget(memory, &reported, 7 * GiB, 1 * GiB) == 1792 * MiB, "7 GiB of sampled textures with the usage below the cached bytes do not leave 1792 MiB after their reservation and 1280 MiB of headroom"); + 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, 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, 0, 0) == 512 * MiB, "a 1 GiB budget does not keep the 512 MiB floor"); + reported.heapBudget[2] = 40 * GiB; + Require(StorageTextureBudget(memory, &reported, 0, 0) == 4 * GiB, "a budget above the heap size raised the storage image cache past a quarter of the heap"); + memory.memoryHeaps[2].size = 8 * GiB; + reported.heapBudget[2] = 7189 * MiB; + reported.heapUsage[2] = 4491 * MiB; + Require(StorageTextureBudget(memory, &reported, 49 * MiB, 506 * MiB) == 7189 * MiB / 4u, "49 MiB of sampled textures on an 8 GiB heap with a 7189 MiB budget and 3936 MiB used outside the texture caches held the storage image cache below a quarter of the budget"); + reported.heapUsage[2] = 6490 * MiB; + Require(StorageTextureBudget(memory, &reported, 2 * GiB, 506 * MiB) == 512 * MiB, "2 GiB of sampled textures on an 8 GiB heap with a 7189 MiB budget and 3936 MiB used outside the texture caches did not take the storage image cache back to its 512 MiB floor"); + memory.memoryHeaps[2].size = 10 * GiB; + reported.heapBudget[2] = 8767 * MiB; + reported.heapUsage[2] = 7981 * MiB; + Require(StorageTextureBudget(memory, &reported, 1427 * MiB, 490 * MiB) == 512 * MiB, "a 10 GiB heap at 7981 of its 8767 MiB budget with 1427 MiB of sampled textures does not keep the storage image cache at its 512 MiB floor"); + for (const auto heapGiB : {2u, 4u, 6u, 8u, 16u, 32u}) { + memory.memoryHeaps[2].size = heapGiB * GiB; + for (std::uint64_t budget = GiB; budget <= heapGiB * GiB; budget += 512 * MiB) { + reported.heapBudget[2] = budget; + for (std::uint64_t sampled = 0; sampled <= budget; sampled += 256 * MiB) { + std::uint64_t previous = 0; + for (std::uint64_t outside = 0; outside <= budget + GiB; outside += 128 * MiB) { + reported.heapUsage[2] = outside + sampled + 512 * MiB; + const auto limit = StorageTextureBudget(memory, &reported, sampled, 512 * MiB); + Require(limit >= 512 * MiB, "pressure sweep lost the storage cache floor"); + Require(limit <= std::max(512 * MiB, budget / 4u), "pressure sweep exceeded a quarter of the reported budget above the floor"); + Require(limit <= std::max(2 * GiB, heapGiB * GiB / 4u), "pressure sweep exceeded the heap storage cache cap"); + if (outside != 0) Require(limit <= previous, "additional non-cache memory usage grew the storage cache budget"); + if (limit > 512 * MiB) Require(outside + sampled + budget / 8u + limit <= budget, "storage cache budget consumed live sampled memory or pressure headroom"); + previous = limit; + } + } + } + } + memory.memoryHeaps[0].flags = 0; + memory.memoryHeaps[2].flags = 0; + Require(StorageTextureBudget(memory, &reported, 0, 0) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of storage images"); +} + void sampledBudgetReportTests() { using AgcDriver::Graphics::SampledBudgetReportDue; using std::chrono::seconds; @@ -3881,6 +6991,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) { @@ -4075,7 +7187,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}); @@ -4101,6 +7213,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}); @@ -4119,6 +7239,565 @@ std::vector makeModule(const ModuleShape& shape) { return words; } +struct SampleLocationsUse { + VkExtent2D grid{}; + std::vector locations; +}; + +std::vector pipelineSampleLocations; +std::vector passSampleLocations; + +void recordSampleLocations(std::vector& uses, const VkSampleLocationsInfoEXT& info) { + uses.push_back({info.sampleLocationGridSize, {info.pSampleLocations, info.pSampleLocations + info.sampleLocationsCount}}); +} + +VKAPI_ATTR VkResult VKAPI_CALL sampleLocationCreateGraphicsPipelines(VkDevice, VkPipelineCache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks*, VkPipeline* pipelines) { + Require(count == 1, "the sample location mock expects one pipeline per call"); + const auto* locations = infos[0].pMultisampleState != nullptr ? static_cast(infos[0].pMultisampleState->pNext) : nullptr; + if (locations != nullptr && locations->sType == VK_STRUCTURE_TYPE_PIPELINE_SAMPLE_LOCATIONS_STATE_CREATE_INFO_EXT && locations->sampleLocationsEnable) recordSampleLocations(pipelineSampleLocations, locations->sampleLocationsInfo); + *pipelines = makeHandle(); + ++mock.live; + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL sampleLocationCreateRenderPass(VkDevice, const VkRenderPassCreateInfo*, const VkAllocationCallbacks*, VkRenderPass* pass) { + *pass = makeHandle(); + ++mock.live; + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL sampleLocationCreateFramebuffer(VkDevice, const VkFramebufferCreateInfo*, const VkAllocationCallbacks*, VkFramebuffer* framebuffer) { + *framebuffer = makeHandle(); + ++mock.live; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL sampleLocationDestroyRenderPass(VkDevice, VkRenderPass, const VkAllocationCallbacks*) { --mock.live; } +VKAPI_ATTR void VKAPI_CALL sampleLocationDestroyFramebuffer(VkDevice, VkFramebuffer, const VkAllocationCallbacks*) { --mock.live; } +VKAPI_ATTR void VKAPI_CALL sampleLocationSetViewport(VkCommandBuffer, std::uint32_t, std::uint32_t, const VkViewport*) {} +VKAPI_ATTR void VKAPI_CALL sampleLocationSetScissor(VkCommandBuffer, std::uint32_t, std::uint32_t, const VkRect2D*) {} + +VKAPI_ATTR void VKAPI_CALL sampleLocationBeginRenderPass(VkCommandBuffer, const VkRenderPassBeginInfo* begin, VkSubpassContents) { + const auto* locations = static_cast(begin->pNext); + if (locations == nullptr || locations->sType != VK_STRUCTURE_TYPE_RENDER_PASS_SAMPLE_LOCATIONS_BEGIN_INFO_EXT) return; + for (std::uint32_t index = 0; index < locations->attachmentInitialSampleLocationsCount; ++index) recordSampleLocations(passSampleLocations, locations->pAttachmentInitialSampleLocations[index].sampleLocationsInfo); + for (std::uint32_t index = 0; index < locations->postSubpassSampleLocationsCount; ++index) recordSampleLocations(passSampleLocations, locations->pPostSubpassSampleLocations[index].sampleLocationsInfo); +} + +PFN_vkVoidFunction VKAPI_CALL sampleLocationProc(VkDevice device, const char* name) { + static const std::map table{ + {"vkCreateGraphicsPipelines", reinterpret_cast(sampleLocationCreateGraphicsPipelines)}, + {"vkCreateRenderPass", reinterpret_cast(sampleLocationCreateRenderPass)}, + {"vkDestroyRenderPass", reinterpret_cast(sampleLocationDestroyRenderPass)}, + {"vkCreateFramebuffer", reinterpret_cast(sampleLocationCreateFramebuffer)}, + {"vkDestroyFramebuffer", reinterpret_cast(sampleLocationDestroyFramebuffer)}, + {"vkCmdBeginRenderPass", reinterpret_cast(sampleLocationBeginRenderPass)}, + {"vkCmdSetViewport", reinterpret_cast(sampleLocationSetViewport)}, + {"vkCmdSetScissor", reinterpret_cast(sampleLocationSetScissor)} + }; + const auto it = table.find(name); + return it != table.end() ? it->second : mockProc(device, name); +} + +void sampleLocationGridFallbackTests() { + using namespace AgcDriver::Graphics; + auto queue = makeState(); + queue.context[0x2f8] = 0x00100001; + queue.context[0x292] = 3; + queue.context[0x3b8] = 0x0dc00000u; + queue.context[0x31d] = 0x9000; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + for (const auto [pixel, word] : {std::pair{0u, 0x0c04u}, std::pair{1u, 0x0c04u}, std::pair{2u, 0x040cu}, std::pair{3u, 0x040cu}}) queue.context[0x2fe + pixel * 4u] = word; + const auto checkerboard = DecodeState(queue); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x0c04u; + const auto uniform = DecodeState(queue); + Require(checkerboard.samples == 2 && checkerboard.customSampleLocations && checkerboard.sampleLocationGrid && uniform.samples == 2 && uniform.customSampleLocations && !uniform.sampleLocationGrid, "the 2x checkerboard (PPSA01556) or the uniform pattern of its pixel (0,0) did not decode"); + auto sampleZero = checkerboard; + sampleZero.samples = 1; + sampleZero.sampleLocations = {}; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) sampleZero.sampleLocations[pixel * 8u] = checkerboard.sampleLocations[pixel * 8u]; + const auto previousDepthSamples = depthSampleCounts; + depthSampleCounts = VK_SAMPLE_COUNT_2_BIT; + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + vertex.variantId = 1; + fragment.variantId = 2; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + const std::array mainKey{41}; + const auto draw = [&](const State& decoded, VkSampleCountFlags gridSamples, bool libraries, bool depth) { + mock = MockVulkan{}; + pipelineSampleLocations.clear(); + passSampleLocations.clear(); + auto context = mockContext(); + context.deviceProc = sampleLocationProc; + context.device = makeHandle(); + context.graphicsPipelineLibrary = libraries; + context.sampleLocationSampleCounts = VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT; + context.sampleLocationGridSampleCounts = gridSamples; + context.imageFormatProperties = sampleLocationDepthProperties; + context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT; + context.limits.framebufferDepthSampleCounts = VK_SAMPLE_COUNT_2_BIT; + context.limits.maxFramebufferWidth = 64; + context.limits.maxFramebufferHeight = 64; + context.limits.maxFramebufferLayers = 1; + State state{}; + state.samples = decoded.samples; + state.sampleLocations = decoded.sampleLocations; + state.customSampleLocations = decoded.customSampleLocations; + state.sampleLocationGrid = decoded.sampleLocationGrid; + if (depth) { + state.depth = DepthTarget{0, 0, {64, 64}, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + state.depth->samples = state.samples; + state.depthTest = true; + state.depthCompare = VK_COMPARE_OP_LESS; + } + const PipelineSetLayouts resources{makeHandle(), VK_NULL_HANDLE, false, mainKey, {}}; + if (libraries) { + Require(Pipeline::Prebuild(context, state, VertexInputLayout{}, resources, shaders), "the sample location pipeline's libraries were not prebuilt"); + } else { + Pipeline pipeline(context, state, VertexInputLayout{}, resources, shaders, VK_IMAGE_LAYOUT_GENERAL); + const std::array depthView{makeHandle()}; + const auto framebuffer = pipeline.AcquireFramebuffer(std::span(depthView).first(depth ? 1u : 0u), {}, {64, 64}); + pipeline.Begin(makeHandle(), *framebuffer, {64, 64}, state); + } + ClearPipelineLibraries(context.device); + Require(mock.live == 0, "sample location grid fallback tests leaked Vulkan objects"); + Require(pipelineSampleLocations.size() == (libraries ? 2u : 1u) && passSampleLocations.size() == (libraries ? 0u : depth ? 2u : 1u), "the pipeline or render pass did not take its sample locations once per library or attachment"); + auto uses = pipelineSampleLocations; + uses.insert(uses.end(), passSampleLocations.begin(), passSampleLocations.end()); + return uses; + }; + const auto expect = [](const std::vector& uses, VkExtent2D grid, std::span locations, const std::string& what) { + for (const auto& use : uses) { + Require(use.grid.width == grid.width && use.grid.height == grid.height && use.locations.size() == locations.size(), what + " took the wrong sample location grid"); + for (std::size_t index = 0; index < locations.size(); ++index) Require(use.locations[index].x == locations[index].x && use.locations[index].y == locations[index].y, what + " took the wrong sample locations"); + } + }; + const std::array pixelZero{{{0.75f, 0.5f}, {0.25f, 0.5f}}}; + const std::array grid{{{0.75f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}}}; + const std::array sampleZeroPixelZero{{{0.75f, 0.5f}}}; + const std::array sampleZeroGrid{{{0.75f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.25f, 0.5f}}}; + for (const bool libraries : {true, false}) { + for (const bool depth : {false, true}) { + for (const VkSampleCountFlags gridSamples : {VkSampleCountFlags{0}, VkSampleCountFlags{VK_SAMPLE_COUNT_4_BIT}}) expect(draw(checkerboard, gridSamples, libraries, depth), {1, 1}, pixelZero, "a 2x checkerboard on a device without a 2x2 grid for 2 samples"); + expect(draw(checkerboard, VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT, libraries, depth), {2, 2}, grid, "a 2x checkerboard on a device with a 2x2 grid for 2 samples"); + for (const VkSampleCountFlags gridSamples : {VkSampleCountFlags{0}, VkSampleCountFlags{VK_SAMPLE_COUNT_2_BIT}}) expect(draw(uniform, gridSamples, libraries, depth), {1, 1}, pixelZero, "a uniform 2x pattern"); + } + expect(draw(sampleZero, VK_SAMPLE_COUNT_2_BIT, libraries, false), {1, 1}, sampleZeroPixelZero, "one sample at sample 0's checkerboard locations on a device without a one-sample grid"); + expect(draw(sampleZero, VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT, libraries, false), {2, 2}, sampleZeroGrid, "one sample at sample 0's checkerboard locations on a device with a one-sample grid"); + } + depthSampleCounts = previousDepthSamples; + mock = MockVulkan{}; +} + +std::string varyingIndex(std::span code) { + Require(code.size() > 5 && code[0] == spv::MagicNumber, "a sample order shader is not a SPIR-V module"); + std::set constants; + for (std::size_t at = 5; at < code.size();) { + const auto count = code[at] >> 16u; + const auto opcode = static_cast(code[at] & 0xffffu); + Require(count != 0 && at + count <= code.size(), "a sample order shader holds a malformed instruction"); + if (opcode == spv::OpConstant) constants.insert(code[at + 2]); + if (opcode == spv::OpVectorExtractDynamic || opcode == spv::OpVectorInsertDynamic) return "a vector component chosen per invocation (opcode " + std::to_string(opcode) + ")"; + if (opcode == spv::OpAccessChain || opcode == spv::OpInBoundsAccessChain || opcode == spv::OpPtrAccessChain || opcode == spv::OpInBoundsPtrAccessChain) { + for (std::size_t operand = 4; operand < count; ++operand) { + if (!constants.contains(code[at + operand])) return "an access chain into %" + std::to_string(code[at + 3]) + " through the non-constant index %" + std::to_string(code[at + operand]); + } + } + at += count; + } + return {}; +} + +VkSampleCountFlags orderStorageCounts = VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; +bool orderSixteenBitStencil = true; +std::vector pipelineSampleMasks; + +VKAPI_ATTR VkResult VKAPI_CALL sampleOrderImageProperties(VkPhysicalDevice, VkFormat, VkImageType, VkImageTiling, VkImageUsageFlags usage, VkImageCreateFlags, VkImageFormatProperties* properties) { + *properties = {}; + properties->maxExtent = {16384, 16384, 1}; + properties->maxMipLevels = 1; + properties->maxArrayLayers = 1; + properties->sampleCounts = (usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0 ? orderStorageCounts : VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL sampleOrderFormatProperties(VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) { + *properties = {}; + if (format == VK_FORMAT_D16_UNORM_S8_UINT && !orderSixteenBitStencil) return; + properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT | VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT; +} + +VKAPI_ATTR VkResult VKAPI_CALL sampleOrderCreateGraphicsPipelines(VkDevice device, VkPipelineCache cache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks* allocator, VkPipeline* pipelines) { + const auto* samples = infos[0].pMultisampleState; + pipelineSampleMasks.push_back(samples != nullptr && samples->pSampleMask != nullptr ? *samples->pSampleMask : ~VkSampleMask{0}); + return sampleLocationCreateGraphicsPipelines(device, cache, count, infos, allocator, pipelines); +} + +PFN_vkVoidFunction VKAPI_CALL sampleOrderProc(VkDevice device, const char* name) { + if (std::string_view(name) == "vkCreateGraphicsPipelines") return reinterpret_cast(sampleOrderCreateGraphicsPipelines); + return sampleLocationProc(device, name); +} + +struct DepthOrderEvent { + bool copy = false; + VkImage image = VK_NULL_HANDLE; + VkImageLayout from = VK_IMAGE_LAYOUT_UNDEFINED; + VkImageLayout to = VK_IMAGE_LAYOUT_UNDEFINED; + VkImageAspectFlags aspects = 0; + VkExtent2D grid{}; + std::vector locations; +}; + +std::vector depthOrderEvents; + +VKAPI_ATTR VkResult VKAPI_CALL depthOrderCreateImage(VkDevice, const VkImageCreateInfo*, const VkAllocationCallbacks*, VkImage* image) { + *image = makeHandle(); + ++mock.live; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL depthOrderDestroyImage(VkDevice, VkImage, const VkAllocationCallbacks*) { --mock.live; } +VKAPI_ATTR void VKAPI_CALL depthOrderImageRequirements(VkDevice, VkImage, VkMemoryRequirements* requirements) { *requirements = {256, 1, 1}; } +VKAPI_ATTR VkResult VKAPI_CALL depthOrderBindImage(VkDevice, VkImage, VkDeviceMemory, VkDeviceSize) { return VK_SUCCESS; } + +VKAPI_ATTR VkResult VKAPI_CALL depthOrderCreateView(VkDevice, const VkImageViewCreateInfo*, const VkAllocationCallbacks*, VkImageView* view) { + *view = makeHandle(); + ++mock.live; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL depthOrderDestroyView(VkDevice, VkImageView, const VkAllocationCallbacks*) { --mock.live; } + +VKAPI_ATTR void VKAPI_CALL depthOrderBarrier(VkCommandBuffer, VkPipelineStageFlags, VkPipelineStageFlags, VkDependencyFlags, std::uint32_t, const VkMemoryBarrier*, std::uint32_t, const VkBufferMemoryBarrier*, std::uint32_t count, const VkImageMemoryBarrier* barriers) { + for (std::uint32_t index = 0; index < count; ++index) { + const auto& barrier = barriers[index]; + DepthOrderEvent event{false, barrier.image, barrier.oldLayout, barrier.newLayout, barrier.subresourceRange.aspectMask, {}, {}}; + const auto* locations = static_cast(barrier.pNext); + if (locations != nullptr && locations->sType == VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT) { + event.grid = locations->sampleLocationGridSize; + event.locations.assign(locations->pSampleLocations, locations->pSampleLocations + locations->sampleLocationsCount); + } + depthOrderEvents.push_back(event); + } +} + +VKAPI_ATTR void VKAPI_CALL depthOrderCopy(VkCommandBuffer, VkImage source, VkImageLayout layout, VkImage, VkImageLayout, std::uint32_t, const VkImageCopy*) { + depthOrderEvents.push_back({true, source, layout, layout, 0, {}, {}}); +} + +VKAPI_ATTR void VKAPI_CALL depthOrderEndPass(VkCommandBuffer) {} +VKAPI_ATTR void VKAPI_CALL depthOrderDraw(VkCommandBuffer, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t) {} +VKAPI_ATTR void VKAPI_CALL depthOrderStencil(VkCommandBuffer, VkStencilFaceFlags, std::uint32_t) {} + +PFN_vkVoidFunction VKAPI_CALL depthOrderProc(VkDevice device, const char* name) { + static const std::map table{ + {"vkCreateImage", reinterpret_cast(depthOrderCreateImage)}, + {"vkDestroyImage", reinterpret_cast(depthOrderDestroyImage)}, + {"vkGetImageMemoryRequirements", reinterpret_cast(depthOrderImageRequirements)}, + {"vkBindImageMemory", reinterpret_cast(depthOrderBindImage)}, + {"vkCreateImageView", reinterpret_cast(depthOrderCreateView)}, + {"vkDestroyImageView", reinterpret_cast(depthOrderDestroyView)}, + {"vkCmdPipelineBarrier", reinterpret_cast(depthOrderBarrier)}, + {"vkCmdCopyImage", reinterpret_cast(depthOrderCopy)}, + {"vkCmdEndRenderPass", reinterpret_cast(depthOrderEndPass)}, + {"vkCmdDraw", reinterpret_cast(depthOrderDraw)}, + {"vkCmdSetStencilWriteMask", reinterpret_cast(depthOrderStencil)}, + {"vkCmdSetStencilReference", reinterpret_cast(depthOrderStencil)} + }; + const auto it = table.find(name); + return it != table.end() ? it->second : sampleLocationProc(device, name); +} + +void sampleOrderTests() { + using namespace AgcDriver::Graphics; + auto queue = makeState(); + queue.context[0x2f8] = 0x00100001; + queue.context[0x292] = 3; + queue.context[0x3b8] = 0x0dc00000u; + queue.context[0x31d] = 0x9000; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + const auto decode = [&](std::array words) { + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = words[pixel]; + return DecodeState(queue); + }; + const auto village = decode({0x0c04u, 0x0c04u, 0x040cu, 0x040cu}); + const auto residentEvil4 = decode({0x040cu, 0x040cu, 0x0c04u, 0x0c04u}); + const auto uniform = decode({0x0c04u, 0x0c04u, 0x0c04u, 0x0c04u}); + const auto moved = decode({0x0c04u, 0x0c04u, 0x040bu, 0x040bu}); + const SampleOrder rowSwap{KeptSampleOrder, KeptSampleOrder, 0x76543201u, 0x76543201u}; + Require(SameSetSampleOrder(village) == rowSwap, "the 2x checkerboard (PPSA01556) did not reorder only its odd rows' two samples"); + Require(SameSetSampleOrder(residentEvil4) == rowSwap, "the 2x checkerboard with its rows swapped (PPSA07411) did not reorder only its odd rows' two samples"); + Require(!SameSetSampleOrder(uniform) && !SameSetSampleOrder(moved), "a uniform pattern or one whose odd rows hold other positions was taken as a reordering of one set"); + State four{}; + four.samples = 4; + four.customSampleLocations = true; + four.sampleLocationGrid = true; + const std::array positions{0x2eu, 0xe6u, 0x2au, 0x62u}; + const std::array, 4> placed{{{0, 1, 2, 3}, {1, 0, 3, 2}, {2, 3, 1, 0}, {3, 2, 0, 1}}}; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + for (std::uint32_t sample = 0; sample < 4; ++sample) four.sampleLocations[pixel * 8u + placed[pixel][sample]] = positions[sample]; + } + const SampleOrder fourOrder{KeptSampleOrder, 0x76542301u, 0x76540132u, 0x76541023u}; + Require(SameSetSampleOrder(four) == fourOrder, "a 4x pattern whose pixels hold one set of positions in four orders did not give each pixel its order"); + Require(InvertSampleOrder(0x76543201u) == 0x76543201u && InvertSampleOrder(0x76540132u) == 0x76541023u && InvertSampleOrder(KeptSampleOrder) == KeptSampleOrder, "inverting a sample order did not undo it"); + Require(HostSampleBits(0x1u, 0x76543201u) == 0x2u && HostSampleBits(0x1u, 0x76540132u) == 0x8u && HostSampleBits(0xffffffffu, 0x76540132u) == 0xffffffffu, "a guest sample mask did not move to the host samples at its guest samples' locations"); + Require(ReordersSamples(rowSwap) && !ReordersSamples(SampleOrder{}) && !ReordersSamples(SampleOrder{KeptSampleOrder, KeptSampleOrder, KeptSampleOrder, KeptSampleOrder}), "a sample order that keeps every index counted as a reordering"); + Require(!HostsSampleLocationGrid({0, 0}) && !HostsSampleLocationGrid({1, 1}) && !HostsSampleLocationGrid({2, 1}) && !HostsSampleLocationGrid({3, 2}) && HostsSampleLocationGrid({2, 2}) && HostsSampleLocationGrid({4, 2}), "a device's largest sample location grid was taken for one that holds a 2x2 grid, or not, wrongly"); + std::size_t module = 0; + for (const auto code : SampleOrderShaderModules()) { + const auto varying = varyingIndex(code); + Require(varying.empty(), "sample order shader " + std::to_string(module) + " picks a pixel's order word or a held sample through " + varying); + ++module; + } + + const auto capable = [] { + auto context = mockContext(); + context.physical = makeHandle(); + context.device = makeHandle(); + context.deviceProc = sampleOrderProc; + context.imageFormatProperties = sampleOrderImageProperties; + context.formatProperties = sampleOrderFormatProperties; + context.sampleLocationSampleCounts = VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + context.sampleLocationGridSampleCounts = 0; + context.storageImageMultisample = true; + context.sampleRateShading = true; + context.shaderStencilExport = true; + context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + context.limits.framebufferDepthSampleCounts = VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + context.limits.framebufferColorSampleCounts = VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + context.limits.maxColorAttachments = 8; + context.limits.maxFramebufferWidth = 64; + context.limits.maxFramebufferHeight = 64; + context.limits.maxFramebufferLayers = 1; + return context; + }; + const auto attached = [](const State& decoded, VkFormat depthFormat, bool stencil) { + State state = decoded; + ColorTarget color{}; + color.address = 0x100000u; + color.extent = {64, 64}; + color.format = VK_FORMAT_R16G16B16A16_SFLOAT; + color.elementBytes = 8; + color.samples = state.samples; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + state.blends = {blend}; + state.renderExtent = {64, 64}; + if (depthFormat != VK_FORMAT_UNDEFINED) { + state.depth = DepthTarget{0x200000u, stencil ? 0x300000u : 0u, {64, 64}, depthFormat, 1.0f, 0}; + state.depth->samples = state.samples; + } + return state; + }; + const auto withStencil = attached(village, VK_FORMAT_D32_SFLOAT_S8_UINT, true); + { + const auto context = capable(); + const auto host = HostSampleLocationsFor(context, withStencil); + Require(host.grid.width == 1 && host.grid.height == 1 && host.order == rowSwap, "a 2x checkerboard on a device without a 2x2 grid that can reorder samples did not take the stand-in with its odd rows reordered"); + auto gridded = context; + gridded.sampleLocationGridSampleCounts = VK_SAMPLE_COUNT_2_BIT; + const auto native = HostSampleLocationsFor(gridded, withStencil); + Require(native.grid.width == 2 && native.grid.height == 2 && !ReordersSamples(native.order), "a 2x checkerboard on a device with a 2x2 grid was reordered"); + Require(!ReordersSamples(HostSampleLocationsFor(context, attached(uniform, VK_FORMAT_D32_SFLOAT, false)).order) && !ReordersSamples(HostSampleLocationsFor(context, attached(moved, VK_FORMAT_D32_SFLOAT, false)).order), "a uniform pattern, or one whose pixels hold other positions, was reordered"); + const auto refused = [&](Context device, const State& state, std::string_view reason, const std::string& what) { + Require(!ReordersSamples(HostSampleLocationsFor(device, state).order), what + " was reordered"); + Require(SampleOrderRefusal(device, state).find(reason) != std::string::npos, what + " was not refused for " + std::string(reason) + ": " + SampleOrderRefusal(device, state)); + }; + auto noStorage = context; + noStorage.storageImageMultisample = false; + auto depthOnly = withStencil; + depthOnly.colors.clear(); + depthOnly.blends.clear(); + depthOnly.hasColorTarget = false; + Require(ReordersSamples(HostSampleLocationsFor(context, depthOnly).order), "a depth-only pass on a device with multisampled storage images was not reordered"); + refused(noStorage, depthOnly, "lacks shaderStorageImageMultisample", "a depth-only pass on a device without multisampled storage images"); + Require(HostSampleLocationsFor(noStorage, depthOnly).order == HostSampleLocationsFor(noStorage, withStencil).order, "a depth-only pass and a color pass on one depth target took different sample orders on a device without multisampled storage images"); + refused(noStorage, withStencil, "lacks shaderStorageImageMultisample", "a color target on a device without multisampled storage images"); + auto noSampleShading = context; + noSampleShading.sampleRateShading = false; + refused(noSampleShading, withStencil, "sampleRateShading", "a device without sample-rate shading"); + orderStorageCounts = VK_SAMPLE_COUNT_4_BIT; + refused(context, withStencil, "has no storage view at 2 samples", "a color format without 2x storage"); + orderStorageCounts = VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + auto twentyFour = context; + twentyFour.physical = makeHandle(); + orderSixteenBitStencil = false; + refused(twentyFour, attached(village, VK_FORMAT_D16_UNORM_S8_UINT, true), "is not written back exactly", "16-bit depth with stencil kept as 24-bit depth"); + orderSixteenBitStencil = true; + auto sixteen = context; + sixteen.physical = makeHandle(); + Require(ReordersSamples(HostSampleLocationsFor(sixteen, attached(village, VK_FORMAT_D16_UNORM_S8_UINT, true)).order), "16-bit depth with stencil kept as 16-bit depth was not reordered"); + + using Method = SampleOrderMethod; + const auto steps = [](const std::vector& plan, std::initializer_list expected, const std::string& what) { + Require(plan.size() == expected.size(), what + " planned " + std::to_string(plan.size()) + " attachments"); + std::size_t index = 0; + for (const auto& step : expected) { + const auto& planned = plan[index++]; + Require(planned.attachment == step.attachment && planned.method == step.method && planned.toHost == step.toHost && planned.toGuest == step.toGuest, what + " planned attachment " + std::to_string(planned.attachment) + " differently"); + } + }; + steps(PlanSampleOrder(context, withStencil, rowSwap), {{0, Method::Storage, true, true}, {1, Method::DepthStencilExport, true, true}}, "a color and depth-stencil pass with stencil export"); + auto noExport = context; + noExport.shaderStencilExport = false; + steps(PlanSampleOrder(noExport, withStencil, rowSwap), {{0, Method::Storage, true, true}, {1, Method::DepthStencilBits, true, true}}, "a color and depth-stencil pass without stencil export"); + steps(PlanSampleOrder(context, attached(village, VK_FORMAT_D32_SFLOAT, false), rowSwap), {{0, Method::Storage, true, true}, {1, Method::Depth, true, true}}, "a color and depth pass"); + steps(PlanSampleOrder(context, attached(village, VK_FORMAT_D32_SFLOAT_S8_UINT, false), rowSwap), {{0, Method::Storage, true, true}, {1, Method::Depth, true, true}}, "a depth target whose host format has stencil but the guest's has none"); + steps(PlanSampleOrder(context, withStencil, rowSwap, VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_STORE), {{0, Method::Storage, false, true}, {1, Method::DepthStencilExport, false, true}}, "a pass that clears its attachments"); + steps(PlanSampleOrder(context, withStencil, rowSwap, VK_ATTACHMENT_LOAD_OP_LOAD, VK_ATTACHMENT_STORE_OP_DONT_CARE), {{0, Method::Storage, true, false}, {1, Method::DepthStencilExport, true, false}}, "a pass that discards its attachments"); + steps(PlanSampleOrder(context, withStencil, rowSwap, VK_ATTACHMENT_LOAD_OP_DONT_CARE, VK_ATTACHMENT_STORE_OP_DONT_CARE), {}, "a pass that neither loads nor stores"); + steps(PlanSampleOrder(context, withStencil, SampleOrder{}), {}, "a pass that keeps the guest's order"); + auto single = withStencil; + single.samples = 1; + single.colors[0].samples = 1; + single.depth->samples = 1; + steps(PlanSampleOrder(context, single, rowSwap), {}, "a single-sampled pass"); + } + + auto masked = attached(village, VK_FORMAT_UNDEFINED, false); + masked.quadSampleMask = {1, 1, 1, 1}; + Require(SampleMaskFollowsOrder(masked, rowSwap) && QuadSampleMasks(masked, rowSwap) == std::array{1, 1, 1, 1} && QuadSampleMasks(masked) == std::array{0xffffu, 0xffffu, 0xffffu, 0xffffu}, "a uniform mask of sample 0 did not move to the pixel shader under the checkerboard's order"); + auto empty = masked; + empty.quadSampleMask = {0, 0, 0, 0}; + Require(!SampleMaskFollowsOrder(empty, rowSwap) && !SampleMaskFollowsOrder(masked, SampleOrder{}), "an empty mask, or a mask without a reordering, moved to the pixel shader"); + { + auto earlyTested = attached(village, VK_FORMAT_D32_SFLOAT, false); + earlyTested.quadSampleMask = {1, 1, 1, 1}; + earlyTested.pixelEarlyZ = true; + earlyTested.depthTest = true; + ShaderRecompiler::RecompileResult memoryWriter, memoryKeeper; + memoryWriter.spirv = makeModule({.fragment = true}); + memoryKeeper.spirv = memoryWriter.spirv; + ShaderRecompiler::DescriptorBinding written{}; + written.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + written.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + written.count = 1; + written.bufferWritten = {true}; + memoryWriter.bindings.push_back(written); + const std::array writing{{{ShaderRecompiler::ShaderStage::Fragment, &memoryWriter, 0}}}; + const std::array notWriting{{{ShaderRecompiler::ShaderStage::Fragment, &memoryKeeper, 0}}}; + const auto context = capable(); + Require(SampleMaskFollowsOrder(earlyTested, HostSampleLocationsFor(context, earlyTested).order), "a uniform mask of sample 0 with a depth target did not move to the pixel shader under the checkerboard's order"); + expectFailure([&] { ValidateSampleMask(context, earlyTested, writing); }, "from a pixel program with early depth or stencil tests that writes memory, is unsupported"); + ValidateSampleMask(context, earlyTested, notWriting); + auto gridded = context; + gridded.sampleLocationGridSampleCounts = VK_SAMPLE_COUNT_2_BIT; + ValidateSampleMask(gridded, earlyTested, writing); + } + + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + vertex.variantId = 1; + fragment.variantId = 2; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + const std::array mainKey{41}; + const std::array pixelZero{{{0.75f, 0.5f}, {0.25f, 0.5f}}}; + for (const bool libraries : {false, true}) { + for (const bool maskedPass : {false, true}) { + mock = MockVulkan{}; + pipelineSampleLocations.clear(); + passSampleLocations.clear(); + pipelineSampleMasks.clear(); + auto context = capable(); + context.graphicsPipelineLibrary = libraries; + context.pipelineLinkTimeOptimization = false; + auto state = maskedPass ? masked : attached(village, VK_FORMAT_UNDEFINED, false); + const PipelineSetLayouts resources{makeHandle(), VK_NULL_HANDLE, false, mainKey, {}}; + std::size_t reorderedMasks = 0; + { + Pipeline pipeline(context, state, VertexInputLayout{}, resources, shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(pipeline.ReordersSamples() && pipeline.HostSampleOrder() == rowSwap, "a 2x checkerboard pipeline on a device without a 2x2 grid did not reorder its odd rows"); + reorderedMasks = pipelineSampleMasks.size(); + auto gridded = context; + gridded.sampleLocationGridSampleCounts = VK_SAMPLE_COUNT_2_BIT; + Pipeline native(gridded, state, VertexInputLayout{}, resources, shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(!native.ReordersSamples(), "a 2x checkerboard pipeline on a device with a 2x2 grid reordered its samples"); + } + ClearPipelineLibraries(context.device); + Require(mock.live == 0, "sample order pipeline tests leaked Vulkan objects"); + Require(!pipelineSampleLocations.empty(), "the reordered pipeline took no sample locations"); + const auto& standIn = pipelineSampleLocations.front(); + Require(standIn.grid.width == 1 && standIn.grid.height == 1 && standIn.locations.size() == 2 && standIn.locations[0].x == pixelZero[0].x && standIn.locations[1].x == pixelZero[1].x, "the reordered pipeline did not draw with pixel (0,0)'s locations"); + const auto keepsMask = [&](std::size_t begin, std::size_t end) { return std::any_of(pipelineSampleMasks.begin() + static_cast(begin), pipelineSampleMasks.begin() + static_cast(end), [](VkSampleMask mask) { return mask == 1u; }); }; + Require(reorderedMasks != 0 && !keepsMask(0, reorderedMasks), "the reordered pipeline kept a static sample mask its odd rows would apply to the other sample"); + Require(keepsMask(reorderedMasks, pipelineSampleMasks.size()) == maskedPass, "the pipeline on a device with a 2x2 grid did not keep the static sample mask exactly when the pass has one"); + } + } + mock = MockVulkan{}; + + { + auto context = capable(); + context.deviceProc = depthOrderProc; + context.memory.memoryTypes[0].propertyFlags |= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + const auto commands = makeHandle(); + const auto depthImage = makeHandle(); + const SampleOrderTarget target{depthImage, makeHandle(), VK_FORMAT_D32_SFLOAT_S8_UINT, {16, 8}, 2, SampleOrderMethod::Depth, nullptr}; + const auto onTarget = [&] { + std::vector events; + for (const auto& event : depthOrderEvents) { + if (event.image == depthImage) events.push_back(event); + } + return events; + }; + const auto located = [](const DepthOrderEvent& event, VkImageLayout from, VkImageLayout to, VkImageAspectFlags aspects, VkExtent2D grid, std::span locations) { + if (event.copy || event.from != from || event.to != to || event.aspects != aspects || event.grid.width != grid.width || event.grid.height != grid.height || event.locations.size() != locations.size()) return false; + for (std::size_t index = 0; index < locations.size(); ++index) { + if (event.locations[index].x != locations[index].x || event.locations[index].y != locations[index].y) return false; + } + return true; + }; + constexpr VkImageAspectFlags depthStencil = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + std::vector> kept; + for (const bool standIn : {true, false}) { + depthOrderEvents.clear(); + SampleOrderPass pass{rowSwap, {16, 8}, {}, standIn}; + std::copy(pixelZero.begin(), pixelZero.end(), pass.locations.begin()); + RecordSampleOrder(context, commands, pass, std::span(&target, 1u), false, kept); + const auto events = onTarget(); + if (standIn) { + Require(events.size() == 3 && located(events[0], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, depthStencil, {1, 1}, pixelZero) && events[1].copy && events[1].from == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL && located(events[2], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, depthStencil, {1, 1}, pixelZero), "the depth swap did not copy its target between transitions that name the stand-in sample locations it was rendered with"); + } else { + Require(events.size() == 1 && events[0].copy && events[0].from == VK_IMAGE_LAYOUT_GENERAL, "the depth swap of a pass without sample locations transitioned its target"); + } + } + depthOrderEvents.clear(); + DepthSampleLocations depth; + depth.image = depthImage; + depth.format = VK_FORMAT_D32_SFLOAT; + depth.samples = 2; + depth.grid = {2, 2}; + const std::array grid{{{0.75f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}}}; + std::copy(grid.begin(), grid.end(), depth.locations.begin()); + RecordDepthSampleLocations(context, commands, depth); + const auto events = onTarget(); + Require(events.size() == 2 && located(events[0], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT, {2, 2}, grid) && located(events[1], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_ASPECT_DEPTH_BIT, {2, 2}, grid), "a depth target drawn on a 2x2 sample location grid was not left in the general layout through transitions that name that grid's locations"); + kept.clear(); + ClearSampleOrders(context.device); + Require(mock.live == 0, "the depth swap tests leaked Vulkan objects"); + depthOrderEvents.clear(); + } + mock = MockVulkan{}; + + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x20000u, {}, 0, {}}; + request.context.waveSize = 64; + request.context.pixel = ShaderRecompiler::ShaderPixelStageInfo{}; + const auto keptHash = ShaderRecompiler::RecompileCacheKey::ContextHash(request); + request.context.pixel->sampleOrder = rowSwap; + Require(ShaderRecompiler::RecompileCacheKey::ContextHash(request) != keptHash, "the pixel shader's sample order did not key its variant"); + const ShaderRecompiler::RequestSerializer serializer; + const auto back = serializer.Deserialize(serializer.Serialize(request)); + Require(back.request.context.pixel.has_value() && back.request.context.pixel->sampleOrder == rowSwap, "the recompile request lost the pixel shader's sample order"); +} + void pipelineLibrarySetTests() { using namespace AgcDriver::Graphics; for (const auto mask : {0u, 1u, 2u, 3u}) { @@ -4174,6 +7853,116 @@ 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 framebufferOwnerTests() { + using namespace AgcDriver::Graphics; + mock = MockVulkan{}; + auto context = mockContext(); + context.graphicsPipelineLibrary = true; + context.pipelineLinkTimeOptimization = false; + context.limits.maxColorAttachments = 1; + context.limits.maxFramebufferWidth = 4; + context.limits.maxFramebufferHeight = 4; + context.limits.maxFramebufferLayers = 1; + context.deviceProc = +[](VkDevice device, const char* name) -> PFN_vkVoidFunction { + if (std::strcmp(name, "vkCreateGraphicsPipelines") == 0) { + return reinterpret_cast(+[](VkDevice, VkPipelineCache, std::uint32_t count, const VkGraphicsPipelineCreateInfo*, const VkAllocationCallbacks*, VkPipeline* pipeline) -> VkResult { + Require(count == 1, "framebuffer owner test expects one graphics pipeline"); + *pipeline = makeHandle(); + ++mock.live; + return VK_SUCCESS; + }); + } + return mockProc(device, name); + }; + { + State state{}; + state.hasColorTarget = true; + state.samples = 2; + state.colors.resize(1); + state.colors[0].format = VK_FORMAT_R8G8B8A8_UNORM; + state.colors[0].samples = 2; + state.blends.resize(1); + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + Pipeline pipeline(context, state, VertexInputLayout{}, PipelineSetLayouts{}, shaders); + const std::array views{makeHandle()}; + std::array, 1> owners{std::make_shared(1)}; + const std::weak_ptr firstOwner = owners[0]; + auto first = pipeline.AcquireFramebuffer(views, owners, {4, 4}); + auto repeated = pipeline.AcquireFramebuffer(views, owners, {4, 4}); + Require(first == repeated, "a live framebuffer owner did not reuse its framebuffer"); + auto retainedOwner = owners[0]; + owners[0] = std::make_shared(2); + auto distinct = pipeline.AcquireFramebuffer(views, owners, {4, 4}); + Require(distinct != first, "identical image-view handles reused another live owner's framebuffer"); + retainedOwner.reset(); + Require(firstOwner.expired(), "the framebuffer cache retained its image owner"); + const std::weak_ptr retired = first; + auto recycled = pipeline.AcquireFramebuffer(views, owners, {4, 4}); + Require(recycled == distinct && recycled != first, "an expired owner matched a recycled image-view handle"); + first.reset(); + repeated.reset(); + recycled = pipeline.AcquireFramebuffer(views, owners, {4, 4}); + Require(retired.expired(), "the framebuffer cache retained an unused framebuffer after its owner expired"); + const std::weak_ptr lastOwner = owners[0]; + owners[0].reset(); + Require(lastOwner.expired(), "a cached framebuffer kept the final image owner alive"); + } + ClearPipelineLibraries(context.device); + Require(mock.live == 0, "framebuffer owner tests leaked Vulkan objects"); +} + void barycentricEmulationTests() { using namespace ShaderRecompiler; using namespace AgcDriver::Graphics; @@ -4276,7 +8065,95 @@ 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, 0u); + Require(modes(patches.control) == std::vector{spv::ExecutionModeOutputVertices, spv::ExecutionModeTriangles, spv::ExecutionModeSpacingEqual, spv::ExecutionModeVertexOrderCw}, "the generated control stage does not declare three output vertices and the triangle domain"); + Require(modes(patches.evaluation) == std::vector{spv::ExecutionModeTriangles, spv::ExecutionModeSpacingEqual, spv::ExecutionModeVertexOrderCw}, "the generated evaluation stage does not declare the triangle domain"); + const std::array patchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; + 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); + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + ValidateShaders(patchShaders, state, subgroup, false); + for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, VK_PRIMITIVE_TOPOLOGY_POINT_LIST}) { + state.topology = topology; + expectFailure([&] { ValidateShaders(patchShaders, state, subgroup, false); }, "require a triangle list or strip"); + } + 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, 0u)); }, "does not emulate barycentrics"); + auto noTessellation = tessellationTarget; + noTessellation.tessellation.reset(); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, noTessellation, 128u, 0u)); }, "tessellation shaders are unavailable"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, target, 128u, 0u)); }, "tessellation shaders are unavailable"); + auto noCapability = tessellationTarget; + noCapability.supportedCapabilities = std::span(capabilities).first(1); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, noCapability, 128u, 0u)); }, "tessellation capability is unavailable"); + auto smallPatches = tessellationTarget; + smallPatches.tessellation->maxEvaluationOutputComponents = 4u; + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, smallPatches, 128u, 0u)); }, "exceeds device limits"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 0u, 0u)); }, "exceeds device limits"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u, 1u)); }, "first or the last"); + static_cast(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u, 2u)); + { + auto context = mockContext(); + context.provokingVertexLast = false; + auto lastState = state; + lastState.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT; + ValidateProvokingVertex(context, lastState, patchShaders); + expectFailure([&] { ValidateProvokingVertex(context, lastState, shaders); }, "last provoking vertex requires VK_EXT_provoking_vertex"); + RecompileResult flatVertices = fragment; + for (auto& parameter : flatVertices.fragmentParameters) parameter.flat = true; + const std::array flatPatchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &flatVertices, 0}}}; + expectFailure([&] { ValidateProvokingVertex(context, lastState, flatPatchShaders); }, "explicit per-vertex flat interpolation"); + } + + RecompileResult clipped; + clipped.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 2}); + clipped.parameterExports = {0u}; + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(clipped, fragment, tessellationTarget, 128u, 0u)); }, "need shaderClipDistance"); + const std::array clipCapabilities{spv::CapabilityShader, spv::CapabilityTessellation, spv::CapabilityClipDistance}; + auto clipTarget = tessellationTarget; + clipTarget.supportedCapabilities = clipCapabilities; + auto clippedPatches = BuildBarycentricTessellationShaders(clipped, fragment, clipTarget, 128u, 0u); + const auto memberBuiltIn = [](const RecompileResult& result, spv::BuiltIn builtin) { + const auto& words = result.spirv.Words(); + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + 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(variableBuiltIn(clippedPatches.control, spv::BuiltInClipDistance) && memberBuiltIn(clippedPatches.control, spv::BuiltInClipDistance), "the generated control stage does not read the vertex clip distances as a separate built-in variable and pass them on in its per-vertex block"); + Require(memberBuiltIn(clippedPatches.evaluation, spv::BuiltInClipDistance) && !variableBuiltIn(clippedPatches.evaluation, spv::BuiltInClipDistance), "the generated evaluation stage does not keep the clip distances in its per-vertex blocks"); + 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, 0u)); }, "not exported by the vertex shader"); expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, fragment, target, limits)); }, "not exported by the vertex shader"); } @@ -4373,6 +8250,57 @@ void copiedWriterIndirectOverlapTests() { ::operator delete(copiedBlock, std::align_val_t{half}); } +void registeredReadableCoverTests() { + using namespace AgcDriver::Graphics; + + constexpr std::size_t block = 65536; + auto* memory = static_cast(::operator new(4 * block, std::align_val_t{block})); + const auto base = reinterpret_cast(memory); + const auto add = [&](std::size_t index, bool readable) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, memory + index * block, block, readable, readable, true); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto remove = [&](std::size_t index) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, memory + index * block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + + add(0, true); + add(1, true); + Require(RegisteredReadableCovers(base, block), "a registered allocation must be covered"); + Require(RegisteredReadableCovers(base + block / 2, block), "a range across two adjacent registered allocations must be covered"); + Require(RegisteredReadableCovers(base, 2 * block), "two adjacent registered allocations must be covered together"); + Require(!RegisteredReadableCovers(base + block, 2 * block), "a range past the last registered allocation must not be covered"); + add(3, true); + Require(!RegisteredReadableCovers(base, 4 * block), "a range over a gap between registered allocations must not be covered"); + add(2, false); + Require(!RegisteredReadableCovers(base + block, 2 * block), "a range over an unreadable registered allocation must not be covered"); + Require(RegisteredReadableCovers(base + 3 * block, block), "the readable allocation after an unreadable one must be covered"); + Require(!RegisteredReadableCovers(base, 0), "an empty range must not be covered"); + for (std::size_t index = 0; index < 4; ++index) remove(index); + Require(!RegisteredReadableCovers(base, block), "a removed allocation must not be covered"); + ::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; @@ -4406,17 +8334,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"); @@ -4455,6 +8401,41 @@ void rectListTests() { Draw(context, state, noInstances, {}); } +void quadListTests() { + using namespace AgcDriver::Graphics; + auto queue = makeState(); + queue.userConfig[0x242] = 19; + queue.context[0x1b3] = queue.context[0x1b4] = 2; + Require(DrawRejection(queue, false).empty(), "valid quad-list draw failed precheck"); + auto state = DecodeState(queue); + Require(state.quadList, "quad-list state flag was not set"); + Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, "quad-list did not decode to triangle list topology"); + Require(RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Triangle, "quad-list did not produce triangle raster primitive"); + Require(!state.primitiveRestart, "quad-list must not have primitive restart enabled"); + queue.userConfig[0x24b] = 1; + state = DecodeState(queue); + Require(!state.primitiveRestart, "quad-list must suppress primitive restart"); + queue.userConfig[0x24b] = 0; + const Context context{}; + std::array indices{0, 1, 2, 3, 2, 3, 4, 5}; + const auto drawOutcome = [&](std::uint32_t count, bool indexed) { + const AgcDriver::Pm4::DrawParameters draw = indexed ? AgcDriver::Pm4::DrawParameters{reinterpret_cast(indices.data()), count, 2, 1, 0, true} : AgcDriver::Pm4::DrawParameters{0, count, 0, 1, 0, false}; + try { + Draw(context, state, draw, {}); + } catch (const std::exception& error) { + return std::string(error.what()); + } + return std::string{}; + }; + for (const auto indexed : {false, true}) { + const auto complete = drawOutcome(4, indexed); + for (const auto count : {5u, 6u, 7u}) Require(drawOutcome(count, indexed) == complete, "a quad-list draw with partial trailing vertices was not drawn like complete quads"); + } + Require(drawOutcome(0, true).empty(), "an indexed quad-list draw without indices was not skipped"); + const AgcDriver::Pm4::DrawParameters noInstances{reinterpret_cast(indices.data()), 4, 2, 0, 0, true}; + Draw(context, state, noInstances, {}); +} + ShaderRecompiler::RecompileResult recompilePixel(std::initializer_list controls, std::span code) { auto queue = makeState(); queue.context[0x1b3] = 0x2u; @@ -4609,6 +8590,11 @@ void earlyFragmentTestsTests() { request.context.pixel->alphaToCoverage = true; const auto replayed = serializer.Deserialize(serializer.Serialize(request)); Require(!declaresEarlyFragmentTests(request) && !declaresEarlyFragmentTests(replayed.request), "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with alpha-to-coverage declared early fragment tests"); + request = depthExportRequest(shaderControl); + request.context.pixel->quadPixelMask = 0x1u; + const auto masked = serializer.Deserialize(serializer.Serialize(request)); + Require(!declaresEarlyFragmentTests(request) && !declaresEarlyFragmentTests(masked.request), "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with a quad pixel mask declared early fragment tests"); + Require(AgcDriver::Graphics::DeclaresEarlyFragmentTests(ShaderRecompiler::Recompile(depthExportRequest(shaderControl))), "the driver did not read early fragment tests from a recompiled pixel shader"); } } @@ -4651,7 +8637,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; @@ -4669,7 +8655,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; @@ -4693,6 +8679,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); @@ -4741,9 +8728,85 @@ 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 entryOps = [](std::span words) { + std::map counts; + std::uint32_t entry = 0, function = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpEntryPoint) entry = words[at + 2]; + if (op == spv::OpFunction) function = words[at + 2]; + if (function != 0 && function == entry) ++counts[op]; + } + return counts; + }; + const auto unorderedWords = ShaderRecompiler::Recompile(unordered).spirv.Copy(); + const auto unorderedOps = countOps(unorderedWords); + Require(!unorderedOps.contains(spv::OpBeginInvocationInterlockEXT), "a pixel shader without PRIMITIVE_ORDERED_PIXEL_SHADER began the interlock"); + Require(unorderedOps.at(spv::OpTerminateInvocation) == 1u && entryOps(unorderedWords).at(spv::OpTerminateInvocation) == 1u && unorderedOps.at(spv::OpKill) == 1u, "a pixel shader with BDA reads and SPV_KHR_terminate_invocation did not end its BDA faults through one OpTerminateInvocation in its entry point (" + std::to_string(unorderedOps.at(spv::OpTerminateInvocation)) + " in the module)"); + unordered.target.supportedExtensions = bdaExtensions; + const auto unorderedKillWords = ShaderRecompiler::Recompile(unordered).spirv.Copy(); + const auto unorderedKills = countOps(unorderedKillWords); + Require(!unorderedKills.contains(spv::OpTerminateInvocation) && unorderedKills.at(spv::OpKill) == 2u && entryOps(unorderedKillWords).at(spv::OpKill) == 1u, "a pixel shader with BDA reads and no SPV_KHR_terminate_invocation did not end its BDA faults through one OpKill in its entry point (" + std::to_string(unorderedKills.at(spv::OpKill)) + " OpKill in the module)"); + auto unreadPixel = orderedPixelRequest(0x20600u, bdaCapabilities); + unreadPixel.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + for (const auto extensions : {std::span(terminateExtensions), std::span(bdaExtensions)}) { + unreadPixel.target.supportedExtensions = extensions; + const auto unread = ShaderRecompiler::Recompile(unreadPixel); + const auto unreadOps = countOps(unread.spirv.Words()); + const auto unreadEntry = entryOps(unread.spirv.Words()); + Require(!unreadOps.contains(spv::OpTerminateInvocation) && !unreadEntry.contains(spv::OpKill), "a pixel shader without BDA reads ended its entry point through a BDA fault exit (" + std::to_string(extensions.size()) + " extensions)"); + AgcDriver::Graphics::State state{}; + state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &unread, 0}}}; + AgcDriver::Graphics::ValidateShaders(shaders, state, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); + } + 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"); } @@ -4809,12 +8872,52 @@ void stencilRefExportTests() { Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted a stencil reference export with Z format " + std::to_string(zFormat)); } } - for (const auto shaderControl : {0x54u, 0x56u}) { - const auto queue = stencilRefExportQueue(shaderControl, 2u); - expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "depth export, shader coverage or ordered fragment execution"); - Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted STENCIL_OP_VAL_EXPORT_ENABLE in DB_SHADER_CONTROL " + std::to_string(shaderControl)); + for (const auto shaderControl : {0x54u, 0x56u, 0x45u}) { + for (const auto zFormat : {2u, 9u}) { + const auto unused = stencilRefExportQueue(shaderControl, zFormat); + static_cast(AgcDriver::Graphics::DecodeState(unused)); + Require(AgcDriver::Graphics::DrawRejection(unused, false).empty(), "the precheck rejected STENCIL_OP_VAL_EXPORT_ENABLE without a stencil test in DB_SHADER_CONTROL " + std::to_string(shaderControl)); + Require(AgcDriver::Graphics::DecodePixelStageInfo(unused.context, IdentityExports).stencilRefExportEnable == ((shaderControl & 2u) != 0), "STENCIL_OP_VAL_EXPORT_ENABLE without a stencil test exported the stencil reference"); + const bool replaces = (shaderControl & 6u) == 4u; + for (const auto [ops, writeMask, compare, accepted] : {std::tuple{0x000u, 0xffu, 7u, true}, std::tuple{0x737u, 0xffu, 7u, true}, std::tuple{0x110u, 0xffu, 7u, true}, std::tuple{0x050u, 0x00u, 7u, true}, std::tuple{0x040u, 0xffu, 7u, replaces}, std::tuple{0x40040u, 0xffu, 7u, replaces}, std::tuple{0x404u, 0x0fu, 0u, replaces}, std::tuple{0x040u, 0xffu, 2u, false}, std::tuple{0x340u, 0xffu, 7u, false}, std::tuple{0x042u, 0xffu, 7u, false}, std::tuple{0x050u, 0xffu, 7u, false}, std::tuple{0x600u, 0xffu, 7u, false}, std::tuple{0x008u, 0xffu, 7u, false}, std::tuple{0x900u, 0xffu, 7u, false}, std::tuple{0x5000u, 0xffu, 7u, false}}) { + auto stenciled = stencilRefExportQueue(shaderControl, zFormat); + for (const auto [offset, word] : {std::pair{0x010u, 0x80000180u}, std::pair{0x011u, 0x20000181u}, std::pair{0x012u, 0x00001000u}, std::pair{0x013u, 0x00002000u}, std::pair{0x015u, 0x00002000u}, std::pair{0x007u, 0x003f003fu}, std::pair{0x000u, 0u}, std::pair{0x002u, 0u}, std::pair{0x00au, 0u}, std::pair{0x00bu, 0x3f800000u}, std::pair{0x200u, 0x007007b3u}}) stenciled.context[offset] = word; + stenciled.context[0x10b] = ops; + stenciled.context[0x10c] = 0x0100ff00u | (writeMask << 16u); + stenciled.context[0x10d] = stenciled.context[0x10c]; + stenciled.context[0x200] = 0x000000b3u | (compare << 8u) | (compare << 20u); + const auto name = "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with DB_STENCIL_CONTROL " + std::to_string(ops) + ", write mask " + std::to_string(writeMask) + " and compare " + std::to_string(compare); + if (accepted) { + const auto state = AgcDriver::Graphics::DecodeState(stenciled); + Require(state.stencilTest && state.depth.has_value(), "the stencil operation value fixture has no stencil test: " + name); + const bool routed = replaces && (ops == 0x040u || ops == 0x40040u || ops == 0x404u); + const auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(stenciled.context, IdentityExports); + Require(pixel.stencilRefExportEnable == (routed || (shaderControl & 2u) != 0) && pixel.stencilOperationValueExport == routed, "the stencil reference export does not follow the stencil operation value: " + name); + if (routed) { + const auto replaced = [&](const VkStencilOpState& face, std::uint32_t faceOps) { + const auto expected = [&](std::uint32_t shift) { return ((faceOps >> shift) & 0xfu) == 4u ? VK_STENCIL_OP_REPLACE : VK_STENCIL_OP_KEEP; }; + return face.failOp == expected(0u) && face.passOp == expected(4u) && face.depthFailOp == expected(8u) && face.writeMask == writeMask; + }; + Require(replaced(state.stencilFront, ops) && replaced(state.stencilBack, ops >> 12u) && !AgcDriver::Graphics::StencilReferenceUnused(state), "REPLACE_OP did not replace with the exported stencil value: " + name); + } + } else { + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(stenciled)); }, "with a pixel shader that exports the stencil"); + } + } + } + for (const auto zFormat : {0u, 1u, 3u, 7u}) { + const auto queue = stencilRefExportQueue(shaderControl, zFormat); + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "depth export, shader coverage or ordered fragment execution"); + Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted STENCIL_OP_VAL_EXPORT_ENABLE with Z format " + std::to_string(zFormat)); + } } Require(!AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x50u, 2u).context, IdentityExports).stencilRefExportEnable, "a pixel stage without STENCIL_TEST_VAL_EXPORT_ENABLE exports the stencil reference"); + { + auto early = stencilRefExportQueue(0x1004u, 2u); + for (const auto [offset, word] : {std::pair{0x011u, 0x20000181u}, std::pair{0x002u, 0u}, std::pair{0x000u, 0u}, std::pair{0x200u, 0x007007b3u}, std::pair{0x10bu, 0x040u}, std::pair{0x10cu, 0x01ffff00u}}) early.context[offset] = word; + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodePixelStageInfo(early.context, IdentityExports)); }, "early tests with a pixel shader that exports the stencil operation value"); + Require(!AgcDriver::Graphics::DecodePixelStageInfo(early.context, IdentityExports, true).stencilRefExportEnable, "a skipped pixel program exported the stencil operation value"); + } Require(AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x1012u, 2u).context, IdentityExports).earlyZ, "DEPTH_BEFORE_SHADER with a stencil reference export did not get early tests"); const std::array stencilExport{spv::CapabilityShader, spv::CapabilityStencilExportEXT}; const std::array plain{spv::CapabilityShader}; @@ -4842,7 +8945,73 @@ void stencilRefExportTests() { Require(both == "capability extension depth replacing builtin store ", "a depth and stencil reference export declared the wrong outputs: " + both); const auto depthOnly = stencilRefShape(ShaderRecompiler::Recompile(stencilRefExportRequest(0x51u, stencilExport, DepthStencilRefExportCode)).spirv.Words()); Require(depthOnly == "depth ", "an MRTZ stencil channel without STENCIL_TEST_VAL_EXPORT_ENABLE declared the wrong outputs: " + depthOnly); - expectFailure([&] { static_cast(ShaderRecompiler::Recompile(stencilRefExportRequest(0x52u, plain))); }, "VK_EXT_shader_stencil_export"); + const auto emulated = ShaderRecompiler::Recompile(stencilRefExportRequest(0x52u, plain)); + Require(emulated.stencilReferences == std::vector{0u} && emulated.stencilReplaySafe && stencilRefShape(emulated.spirv.Words()).empty(), "a constant stencil reference was not lowered to replay"); + Require(emulated.stencilReferenceDword == 0u && emulated.pushConstants.size() == sizeof(std::uint32_t), "a stencil-only shader did not reserve dynamic reference data"); + { + const std::array literalStencil{0x7e0e02ffu, 0x00002a05u, 0xf8001882u, 0x700u, 0xbf810000u}; + const auto readsOperationByte = [](std::span words) { + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if ((words[at] & 0xffffu) == spv::OpShiftRightLogical) return true; + if ((words[at] & 0xffffu) == spv::OpConstant && (words[at] >> 16u) == 4u && words[at + 3] == 0x2au) return true; + } + return false; + }; + const auto testValue = stencilRefExportRequest(0x52u, plain, literalStencil); + auto operationValue = testValue; + operationValue.context.pixel->stencilOperationValueExport = true; + Require(ShaderRecompiler::Recompile(testValue).stencilReferences == std::vector{0x05u}, "a stencil test value replay did not take the low byte of the MRTZ stencil channel"); + Require(ShaderRecompiler::Recompile(operationValue).stencilReferences == std::vector{0x2au}, "a stencil operation value replay did not take the second byte of the MRTZ stencil channel"); + const auto exportedTest = stencilRefExportRequest(0x52u, stencilExport, literalStencil); + auto exportedOperation = exportedTest; + exportedOperation.context.pixel->stencilOperationValueExport = true; + Require(!readsOperationByte(ShaderRecompiler::Recompile(exportedTest).spirv.Words()) && readsOperationByte(ShaderRecompiler::Recompile(exportedOperation).spirv.Words()), "the exported stencil operation value was not taken from the second byte of the MRTZ stencil channel"); + Require(serializer.Deserialize(serializer.Serialize(exportedOperation)).request.context.pixel->stencilOperationValueExport, "the serialized request lost the stencil operation value export"); + std::vector testKey; + std::vector operationKey; + ShaderRecompiler::RecompileCacheKey::Build(exportedTest, testKey); + ShaderRecompiler::RecompileCacheKey::Build(exportedOperation, operationKey); + Require(testKey != operationKey, "the recompile cache key ignores the stencil operation value export"); + } + auto replayRequest = stencilRefExportRequest(0x52u, plain); + const auto replayHandle = ShaderRecompiler::PrepareShader(replayRequest); + const auto replayCapture = ShaderRecompiler::CaptureResources(replayRequest, ShaderRecompiler::SrtRuntime{}, *replayHandle); + const auto firstReplay = ShaderRecompiler::MaterializeShader(replayRequest, *replayCapture, *replayHandle); + replayRequest.layout.pushConstantOffsetBytes = 128u; + Require(ShaderRecompiler::MatchesPreparedShader(replayRequest, *replayHandle), "moving the stencil replay stage slot invalidated the prepared artifact"); + const auto movedReplay = ShaderRecompiler::MaterializeShader(replayRequest, *replayCapture, *replayHandle); + Require(firstReplay->spirv != movedReplay->spirv && firstReplay->variantId == movedReplay->variantId && firstReplay->pushConstants == movedReplay->pushConstants, "prepared stencil replay did not move only the dynamic reference offset"); + replayRequest.layout.pushConstantSizeBytes = 0u; + Require(!ShaderRecompiler::MatchesPreparedShader(replayRequest, *replayHandle), "prepared stencil replay accepted an exhausted push range"); + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(replayRequest)); }, "unused push constant dword"); + const std::array pushedDepthStencil{0x7e000200u, 0x7e0e02f2u, 0xf8001883u, 0x700u, 0xbf810000u}; + const std::array pushedDepth{0x3f000000u}; + auto pushedRequest = stencilRefExportRequest(0x53u, plain, pushedDepthStencil); + pushedRequest.context.userData = pushedDepth; + pushedRequest.layout = {0u, 0u, 96u, 32u}; + const auto pushedHandle = ShaderRecompiler::PrepareShader(pushedRequest); + ShaderRecompiler::SrtRuntime pushedRuntime{}; + pushedRuntime.userData = pushedDepth; + const auto pushedCapture = ShaderRecompiler::CaptureResources(pushedRequest, pushedRuntime, *pushedHandle); + const auto pushedResult = ShaderRecompiler::MaterializeShader(pushedRequest, *pushedCapture, *pushedHandle); + Require(pushedResult->stencilReferenceDword == 1u && pushedResult->pushConstants.size() == 8u, "a nonaligned replay stage did not reserve its reference after existing push data"); + for (const auto [offset, bytes] : {std::pair{128u, 128u}, std::pair{120u, 8u}}) { + pushedRequest.layout.pushConstantOffsetBytes = offset; + pushedRequest.layout.pushConstantSizeBytes = bytes; + Require(ShaderRecompiler::MatchesPreparedShader(pushedRequest, *pushedHandle), "a replay payload could not move within a valid stage range"); + const auto moved = ShaderRecompiler::MaterializeShader(pushedRequest, *pushedCapture, *pushedHandle); + Require(moved->pushConstants == pushedResult->pushConstants && moved->spirv != pushedResult->spirv, "moving replay push data changed its payload or retained its old offset"); + } + pushedRequest.layout = {0u, 0u, 124u, 4u}; + Require(!ShaderRecompiler::MatchesPreparedShader(pushedRequest, *pushedHandle), "a full push budget reused a replay artifact"); + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(pushedRequest)); }, "unused push constant dword"); + pushedRequest.layout.pushConstantSizeBytes = 8u; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(pushedRequest)); }, "crosses a stage slot"); + const std::array noStencilExport{0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u}; + auto noStencilRequest = stencilRefExportRequest(0x52u, plain, noStencilExport); + noStencilRequest.layout.pushConstantSizeBytes = 0u; + const auto noStencil = ShaderRecompiler::Recompile(noStencilRequest); + Require(noStencil.stencilReferences.empty() && noStencil.stencilReferenceDword == 0xffffffffu && noStencil.pushConstants.empty(), "a stencil export flag without a remaining stencil output reserved replay state"); auto queue = stencilRefExportQueue(0x52u, 2u); queue.context[0x000] = 0; queue.context[0x002] = 0; @@ -4873,6 +9042,153 @@ void stencilRefExportTests() { static_cast(AgcDriver::Graphics::DecodeState(queue)); } +bool stencilComparison(VkCompareOp op, std::uint32_t reference, std::uint32_t stencil) { + switch (op) { + case VK_COMPARE_OP_NEVER: return false; + case VK_COMPARE_OP_LESS: return reference < stencil; + case VK_COMPARE_OP_EQUAL: return reference == stencil; + case VK_COMPARE_OP_LESS_OR_EQUAL: return reference <= stencil; + case VK_COMPARE_OP_GREATER: return reference > stencil; + case VK_COMPARE_OP_NOT_EQUAL: return reference != stencil; + case VK_COMPARE_OP_GREATER_OR_EQUAL: return reference >= stencil; + case VK_COMPARE_OP_ALWAYS: return true; + default: throw std::runtime_error("invalid stencil comparison in the reference oracle"); + } +} + +std::pair stencilReferenceOutcome(const AgcDriver::Graphics::State& state, const VkStencilOpState& face, std::uint32_t reference, std::uint32_t stencil, bool depthPass, bool boundsPass) { + if (state.depthBoundsTest && !boundsPass) return {stencil, false}; + const bool stencilPass = stencilComparison(face.compareOp, reference & face.compareMask & 0xffu, stencil & face.compareMask & 0xffu); + if (!state.depthTest || state.depthCompare == VK_COMPARE_OP_ALWAYS) depthPass = true; + if (state.depthTest && state.depthCompare == VK_COMPARE_OP_NEVER) depthPass = false; + const auto op = !stencilPass ? face.failOp : !depthPass ? face.depthFailOp : face.passOp; + auto updated = stencil; + switch (op) { + case VK_STENCIL_OP_KEEP: break; + case VK_STENCIL_OP_ZERO: updated = 0; break; + case VK_STENCIL_OP_REPLACE: updated = reference; break; + case VK_STENCIL_OP_INCREMENT_AND_CLAMP: updated = std::min(stencil + 1u, 255u); break; + case VK_STENCIL_OP_DECREMENT_AND_CLAMP: updated = stencil == 0 ? 0 : stencil - 1u; break; + case VK_STENCIL_OP_INVERT: updated = ~stencil; break; + case VK_STENCIL_OP_INCREMENT_AND_WRAP: updated = stencil + 1u; break; + case VK_STENCIL_OP_DECREMENT_AND_WRAP: updated = stencil - 1u; break; + default: throw std::runtime_error("invalid stencil operation in the reference oracle"); + } + const auto writeMask = face.writeMask & 0xffu; + return {((stencil & ~writeMask) | (updated & writeMask)) & 0xffu, stencilPass && depthPass}; +} + +void stencilReferenceUnusedTests() { + using AgcDriver::Graphics::StencilReferenceUnused; + AgcDriver::Graphics::State state{}; + state.depth.emplace(); + state.depth->format = VK_FORMAT_D32_SFLOAT_S8_UINT; + state.stencilTest = true; + state.stencilFront = {VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 37u}; + state.stencilBack = state.stencilFront; + std::size_t accepted = 0; + std::size_t countermodels = 0; + const auto verify = [&](const auto& candidate, bool unused, bool witness, std::string_view name) { + Require(StencilReferenceUnused(candidate) == unused, "wrong unused stencil reference decision for " + std::string(name)); + bool changed = false; + for (const auto& face : {candidate.stencilFront, candidate.stencilBack}) { + for (std::uint32_t stencil = 0; stencil < 256u; ++stencil) { + for (const bool depthPass : {false, true}) { + for (const bool boundsPass : {false, true}) { + const auto fixed = stencilReferenceOutcome(candidate, face, face.reference, stencil, depthPass, boundsPass); + for (std::uint32_t reference = 0; reference < 256u; ++reference) { + changed |= stencilReferenceOutcome(candidate, face, reference, stencil, depthPass, boundsPass) != fixed; + } + } + } + } + } + Require(!unused || !changed, "dropping the stencil reference changes a sample outcome for " + std::string(name)); + Require(!witness || changed, "the dependent stencil reference has no countermodel for " + std::string(name)); + accepted += unused; + countermodels += witness; + }; + verify(state, true, false, "ALWAYS with KEEP"); + for (const auto op : {VK_STENCIL_OP_ZERO, VK_STENCIL_OP_INCREMENT_AND_CLAMP, VK_STENCIL_OP_DECREMENT_AND_CLAMP, VK_STENCIL_OP_INVERT, VK_STENCIL_OP_INCREMENT_AND_WRAP, VK_STENCIL_OP_DECREMENT_AND_WRAP}) { + auto independent = state; + independent.stencilFront.passOp = op; + independent.stencilBack = independent.stencilFront; + verify(independent, true, false, "reference-independent update"); + } + for (std::uint32_t comparison = 0; comparison < 8u; ++comparison) { + auto masked = state; + masked.stencilFront.compareOp = static_cast(comparison); + masked.stencilFront.compareMask = 0x100u; + masked.stencilBack = masked.stencilFront; + verify(masked, true, false, "effective zero compare mask"); + if (comparison != VK_COMPARE_OP_ALWAYS && comparison != VK_COMPARE_OP_NEVER) { + auto dependent = masked; + dependent.stencilBack.compareMask = 0x80u; + dependent.stencilBack.writeMask = 0; + verify(dependent, false, true, "dependent back-face comparison with no writes"); + } + } + auto replace = state; + replace.stencilFront.passOp = VK_STENCIL_OP_REPLACE; + verify(replace, false, true, "reachable front pass REPLACE"); + replace.stencilFront.passOp = VK_STENCIL_OP_KEEP; + replace.stencilBack.failOp = VK_STENCIL_OP_REPLACE; + verify(replace, true, false, "unreachable back fail REPLACE"); + replace.stencilBack.compareOp = VK_COMPARE_OP_NEVER; + verify(replace, false, true, "reachable back fail REPLACE"); + replace.stencilBack.failOp = VK_STENCIL_OP_KEEP; + replace.stencilBack.passOp = VK_STENCIL_OP_REPLACE; + replace.stencilBack.depthFailOp = VK_STENCIL_OP_REPLACE; + verify(replace, true, false, "NEVER with unreachable depth operations"); + replace = state; + replace.stencilBack.passOp = VK_STENCIL_OP_REPLACE; + replace.stencilBack.writeMask = 0x100u; + verify(replace, true, false, "effective zero write mask"); + replace.stencilBack.writeMask = 0x80u; + verify(replace, false, true, "partially writable REPLACE"); + replace = state; + replace.stencilBack.depthFailOp = VK_STENCIL_OP_REPLACE; + verify(replace, true, false, "disabled depth test"); + replace.depthTest = true; + replace.depthCompare = VK_COMPARE_OP_ALWAYS; + verify(replace, true, false, "always passing depth test"); + replace.depthCompare = VK_COMPARE_OP_LESS; + verify(replace, false, true, "possibly failing depth test"); + replace.depthCompare = VK_COMPARE_OP_NEVER; + verify(replace, false, true, "always failing depth test"); + replace.stencilBack.depthFailOp = VK_STENCIL_OP_KEEP; + replace.stencilBack.passOp = VK_STENCIL_OP_REPLACE; + verify(replace, true, false, "unreachable depth pass REPLACE"); + replace = state; + replace.depthBoundsTest = true; + verify(replace, true, false, "bounds with reference-independent operations"); + replace.stencilBack.depthFailOp = VK_STENCIL_OP_REPLACE; + Require(!StencilReferenceUnused(replace), "depth bounds admitted an unchecked REPLACE operation"); + auto clear = state; + clear.stencilFront = {VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_REPLACE, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 7u}; + clear.stencilBack = clear.stencilFront; + verify(clear, false, true, "clear replacement state"); + for (const bool front : {false, true}) { + auto invalid = state; + (front ? invalid.stencilFront : invalid.stencilBack).compareOp = static_cast(8u); + Require(!StencilReferenceUnused(invalid), "an unknown stencil comparison lost its export"); + invalid = state; + (front ? invalid.stencilFront : invalid.stencilBack).passOp = static_cast(8u); + Require(!StencilReferenceUnused(invalid), "an unknown stencil operation lost its export"); + } + auto invalidDepth = state; + invalidDepth.depthTest = true; + invalidDepth.depthCompare = static_cast(8u); + Require(!StencilReferenceUnused(invalidDepth), "an unknown depth comparison lost its stencil export"); + auto inactive = clear; + inactive.stencilTest = false; + Require(StencilReferenceUnused(inactive), "a disabled stencil test retained the export"); + inactive.stencilTest = true; + inactive.depth.reset(); + Require(StencilReferenceUnused(inactive), "an absent attachment retained the export"); + std::cout << "Unused stencil reference sample equivalence passed: " << accepted << " accepted states, " << countermodels << " dependent countermodels\n"; +} + std::set bufferViews(std::span words) { std::set views; for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { @@ -4938,6 +9254,25 @@ void LineStripTests() { Require(rejection.empty(), "the draw precheck rejected an indexed line strip with primitive restart: " + rejection); } +std::uint32_t keyedGenerator = 0; +std::size_t keyedWords = 0; + +VKAPI_ATTR VkResult VKAPI_CALL keyedCreateShaderModule(VkDevice, const VkShaderModuleCreateInfo* info, const VkAllocationCallbacks*, VkShaderModule* module) { + keyedWords = info->codeSize / sizeof(std::uint32_t); + keyedGenerator = info->pCode[2]; + *module = makeHandle(); + return VK_SUCCESS; +} + +void ShaderModuleKeyTests() { + const std::array code{0x07230203u, 0x00010300u, 0x00080001u, 8u, 0u}; + static_cast(AgcDriver::Graphics::CreateShaderModule(keyedCreateShaderModule, VK_NULL_HANDLE, true, code.data(), sizeof(code), "keyed module")); + Require(keyedWords == code.size() && keyedGenerator == 0x00080001u, "a shader module with graphics pipeline libraries changed its generator word"); + static_cast(AgcDriver::Graphics::CreateShaderModule(keyedCreateShaderModule, VK_NULL_HANDLE, false, code.data(), sizeof(code), "keyed module")); + Require(keyedWords == code.size() && keyedGenerator == (0x00080001u ^ 0x8000u), "a shader module without graphics pipeline libraries kept the generator word of the other mode"); + Require(code[2] == 0x00080001u, "keying a shader module changed its source words"); +} + void ProvokingVertexTests() { using namespace AgcDriver::Graphics; constexpr std::uint32_t lastVertex = 1u << 19u; @@ -4994,6 +9329,28 @@ void ProvokingVertexTests() { const std::array flatP10Shaders{{{ShaderRecompiler::ShaderStage::Fragment, &flatP10Pixel, 0}}}; ValidateProvokingVertex(supported, last, flatP10Shaders); expectFailure([&] { ValidateProvokingVertex(supported, first, flatP10Shaders); }, "v_interp_mov p10 of a flat input with the first provoking vertex is not measured"); + const std::array flatP20Color{0xc8120001u, 0xc8160101u, 0xc81a0201u, 0xc81e0301u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + const auto flatP20Pixel = recompilePixel({0x400u}, flatP20Color); + const auto p20Inputs = locatedInputs(flatP20Pixel.spirv.Words()); + Require(p20Inputs.size() == 1 && p20Inputs[0].location == 0 && p20Inputs[0].flat && !p20Inputs[0].perVertex, "flat v_interp_mov P20 did not compile to a flat input"); + Require(flatP20Pixel.fragmentParameters.size() == 1 && flatP20Pixel.fragmentParameters[0].flatP20 && !flatP20Pixel.fragmentParameters[0].flatP10 && !flatP20Pixel.fragmentParameters[0].perVertex && !flatP10Pixel.fragmentParameters[0].flatP20, "flat v_interp_mov P20 was not recorded apart from P10"); + const std::array flatP20Shaders{{{ShaderRecompiler::ShaderStage::Fragment, &flatP20Pixel, 0}}}; + ValidateProvokingVertex(supported, first, flatP20Shaders); + expectFailure([&] { ValidateProvokingVertex(supported, last, flatP20Shaders); }, "v_interp_mov p20 of a flat input with the last provoking vertex is not measured"); + auto lastStrip = last; + lastStrip.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + auto lastList = last; + lastList.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + const std::array smoothColor{0xc8100000u, 0xc8110001u, 0xc8140100u, 0xc8150101u, 0xc8180200u, 0xc8190201u, 0xc81c0300u, 0xc81d0301u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + const auto smoothPixel = recompilePixel({0x0u}, smoothColor); + Require(smoothPixel.fragmentParameters.size() == 1 && smoothPixel.fragmentParameters[0].perVertex && !smoothPixel.fragmentParameters[0].vertexOrder, "v_interp_p1/p2 of a smooth input was recorded as reading a vertex slot"); + const std::array smoothShaders{{{ShaderRecompiler::ShaderStage::Fragment, &smoothPixel, 0}}}; + ValidateProvokingVertex(supported, lastStrip, smoothShaders); + const auto slotPixel = recompilePixel({0x0u}, flatP10Color); + Require(slotPixel.fragmentParameters.size() == 1 && slotPixel.fragmentParameters[0].perVertex && slotPixel.fragmentParameters[0].vertexOrder, "v_interp_mov P10 of a smooth input was not recorded as reading a vertex slot"); + const std::array slotShaders{{{ShaderRecompiler::ShaderStage::Fragment, &slotPixel, 0}}}; + expectFailure([&] { ValidateProvokingVertex(supported, lastStrip, slotShaders); }, "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported"); + ValidateProvokingVertex(supported, lastList, slotShaders); ShaderRecompiler::RecompileResult fragment{}; const std::array shaders{{{ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN}) { @@ -5001,16 +9358,19 @@ void ProvokingVertexTests() { state.topology = topology; for (const auto flat : {false, true}) { for (const auto perVertex : {false, true}) { - fragment.fragmentParameters = {{0, 0, flat, perVertex}}; - auto firstState = state; - firstState.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; - ValidateProvokingVertex(unsupported, firstState, shaders); - if (flat && perVertex) { - expectFailure([&] { ValidateProvokingVertex(supported, state, shaders); }, "last provoking vertex with explicit per-vertex flat interpolation is unsupported"); - } else if (perVertex && topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) { - expectFailure([&] { ValidateProvokingVertex(supported, state, shaders); }, "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported"); - } else { - ValidateProvokingVertex(supported, state, shaders); + for (const auto vertexOrder : {false, true}) { + if (vertexOrder && !perVertex) continue; + fragment.fragmentParameters = {{0, 0, flat, perVertex, false, false, vertexOrder}}; + auto firstState = state; + firstState.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; + ValidateProvokingVertex(unsupported, firstState, shaders); + if (flat && perVertex) { + expectFailure([&] { ValidateProvokingVertex(supported, state, shaders); }, "last provoking vertex with explicit per-vertex flat interpolation is unsupported"); + } else if (vertexOrder && topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) { + expectFailure([&] { ValidateProvokingVertex(supported, state, shaders); }, "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported"); + } else { + ValidateProvokingVertex(supported, state, shaders); + } } } } @@ -5022,13 +9382,43 @@ 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 meshDraw = last; + meshDraw.stages.path = ShaderPath::Geometry; + meshDraw.stages.mesh.emplace(); + meshDraw.topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST; + for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_POINT_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST}) { + meshDraw.topology = topology; + for (const auto& drawn : {std::span{}, std::span(flatShaders), std::span(flatP10Shaders), std::span(smoothShaders), std::span(slotShaders)}) ValidateProvokingVertex(supported, meshDraw, drawn); + } + meshDraw.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + for (const auto& drawn : {std::span{}, std::span(flatShaders), std::span(flatP10Shaders), std::span(smoothShaders)}) ValidateProvokingVertex(supported, meshDraw, drawn); + fragment.fragmentParameters = {{0, 0, false, true, false, false, false}, {1, 1, false, true, false, false, false}, {2, 2, false, true, false, false, false}, {3, 3, false, true, false, false, false}}; + ValidateProvokingVertex(supported, meshDraw, shaders); + expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, slotShaders); }, "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported"); + meshDraw.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN; + expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, flatShaders); }, "last provoking vertex is unsupported for triangle fans assembled for a geometry program"); + for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN}) { + auto firstDraw = meshDraw; + firstDraw.topology = topology; + firstDraw.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; + ValidateProvokingVertex(supported, firstDraw, flatShaders); + } + meshDraw.topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST; + expectFailure([&] { ValidateProvokingVertex(unsupported, meshDraw, flatShaders); }, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); + fragment.fragmentParameters = {{0, 0, true, true, false, false, false}}; + expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, shaders); }, "last provoking vertex with explicit per-vertex flat interpolation is unsupported"); + meshDraw.stages.tessellation.emplace(); + expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, flatShaders); }, "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"); } @@ -5074,7 +9464,7 @@ void ConservativeRasterizationTests() { queue.context[0x1b4] = 2; Require(AgcDriver::Graphics::DecodeState(queue).conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT, "PA_SC_CONSERVATIVE_RASTERIZATION_CNTL 0x6000 enabled conservative rasterization"); queue.context[0x313] = 0x6001; - for (const auto primitive : {4u, 5u, 6u}) { + for (const auto primitive : {4u, 5u, 6u, 21u}) { queue.userConfig[0x242] = primitive; Require(AgcDriver::Graphics::DecodeState(queue).conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_OVERESTIMATE_EXT, "OVER_RAST_ENABLE did not decode to overestimation for primitive type " + std::to_string(primitive)); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected OVER_RAST_ENABLE"); @@ -5091,6 +9481,8 @@ void ConservativeRasterizationTests() { geometry.context[0x2ce] = 3; geometry.context[0x29b] = 2; geometry.context[0x2ab] = 4; + geometry.context[0x280] = 0x00080008u; // PA_SU_POINT_SIZE and PA_SU_POINT_MINMAX: a point-output geometry program decodes them + geometry.context[0x281] = 0x00080008u; geometry.shader[0x8a] = 3u << 29u; geometry.shader[0x8b] = 3u << 16u; for (const auto input : {1u, 2u, 4u}) { @@ -5121,7 +9513,15 @@ void ConservativeRasterizationTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(tessellation); }, "conservative rasterization of lines is unsupported (VGT_GS_OUT_PRIM_TYPE=0x1)"); for (const auto inputs : {0x6u, 0x42u}) { queue.context[0x1b3] = inputs; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "conservative rasterization with centroid interpolation"); + Require(AgcDriver::Graphics::DecodeState(queue).conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_OVERESTIMATE_EXT, "OVER_RAST_ENABLE rejected centroid inputs " + std::to_string(inputs) + " of a single rasterized sample"); + auto multisampled = queue; + multisampled.context[0x2f8] = 0x00200002; + multisampled.context[0x31d] = (1u << 12u) | (1u << 15u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(multisampled); }, "conservative rasterization with centroid interpolation of more than one rasterized sample is unsupported"); + auto raised = queue; + raised.context[0x31d] = (1u << 12u) | (1u << 15u); + raised.context[0x292] = 2; + expectFailure([&] { AgcDriver::Graphics::DecodeState(raised); }, "conservative rasterization with centroid interpolation of a draw raised to a multisampled target is unsupported"); } queue.context[0x1b3] = 2; for (const auto control : {0x6003u, 0x6020u, 0x6401u, 0xe06001u, 0x1u}) { @@ -5272,13 +9672,91 @@ 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 targetless = plainState; + targetless.colors.clear(); + targetless.blends.clear(); + Require(validate(packed, targetless) == std::set{0u}, "a uint4 export from a draw with no color target was refused"); + auto gap = plainState; + gap.blends.insert(gap.blends.begin(), gap.blends.front()); + gap.colors[0].exportIndex = 1; + Require(validate(packed, gap) == std::set{0u}, "a uint4 export into the unused slot below a color target was refused"); auto uintExport = request; uintExport.context.pixel->targetOutputMode[0] = 7; - expectFailure([&] { static_cast(ShaderRecompiler::Recompile(uintExport)); }, "fragment output 0 has no 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"); + changed = queue; + changed.context[0x31c] &= ~0x1800u; + const auto standard = AgcDriver::Graphics::DecodeState(changed); + Require(standard.color.format == VK_FORMAT_R32_UINT && standard.color.componentMapping == 0xe4u && standard.color.packing == ColorExportPacking::Float11_11_10, "an 11_11_10 float target with the standard component order did not decode"); + Require(AgcDriver::Graphics::ExportMappings(standard)[0] == 0xe4u, "export 0 reordered its components into the standard-order 11_11_10 float target"); + for (const auto swap : {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() { @@ -5416,6 +9894,74 @@ void DualSourceBlendTests() { expectFailure([&] { validate(dual, singleState); }, "Index decoration"); } +void ReversedBlendShaderTests() { + const auto queue = reversedBlendQueue(10u, 0u, 2u, 0x65010504u); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.reversedBlend == 0x570001u && !state.dualSourceBlend && state.blends.size() == 1, "the reversed-order source-alpha blend did not decode to a pixel shader blend code"); + constexpr std::array code{0xf800180fu, 0x07060504u, 0xbf810000u}; + auto request = packedExportRequest(queue, state, code); + request.context.pixel->reversedBlend = state.reversedBlend; + auto plain = request; + plain.context.pixel->reversedBlend = 0u; + const ShaderRecompiler::RequestSerializer serializer; + Require(serializer.Deserialize(serializer.Serialize(request)).request.context.pixel->reversedBlend == 0x570001u, "the reversed-order blend code was lost in serialization"); + std::vector key; + std::vector plainKey; + ShaderRecompiler::BuildPreparedShaderKey(request, key); + ShaderRecompiler::BuildPreparedShaderKey(plain, plainKey); + Require(key == plainKey, "the reversed-order blend code entered the static shader ABI"); + const auto prepared = ShaderRecompiler::PrepareShader(plain); + Require(ShaderRecompiler::MatchesPreparedShader(request, *prepared), "a reversed-order blended draw did not match the registered pixel shader"); + const auto reversed = ShaderRecompiler::Recompile(request); + const auto single = ShaderRecompiler::Recompile(plain); + Require(reversed.variantId == single.variantId && reversed.variantId == ShaderRecompiler::GetPreparedArtifact(*prepared).variantId, "the reversed-order blended draw did not reuse the registered artifact"); + Require(reversed.PipelineVariantId() != single.PipelineVariantId() && ShaderRecompiler::Recompile(request).PipelineVariantId() == reversed.PipelineVariantId(), "the reversed-order blend code did not key its own specialization"); + const auto arithmetic = [](std::span words) { + std::size_t count = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpFAdd || op == spv::OpFSub || op == spv::OpFMul || op == spv::OpFDiv || op == spv::OpFNegate || op == spv::OpExtInst || op == spv::OpSelect) ++count; + } + return count; + }; + for (const auto shaderCode : {0x570001u, 0x540001u, 0x560001u, 0x020001u, 0x560301u, 0xffff01u}) { + auto changed = request; + changed.context.pixel->reversedBlend = shaderCode; + const auto specialized = ShaderRecompiler::Recompile(changed); + const auto words = specialized.spirv.Words(); + const auto outputs = outputDecorations(words); + Require(outputs.locations.size() == 2 && std::all_of(outputs.locations.begin(), outputs.locations.end(), [](const auto& entry) { return entry.second == 0u; }) && outputs.indices.size() == 1 && outputs.indices.begin()->second == 1u, "blend code " + std::to_string(shaderCode) + " did not add a second color at location 0 index 1"); + Require(arithmetic(words) == arithmetic(single.spirv.Words()), "blend code " + std::to_string(shaderCode) + " computed in the pixel shader instead of copying the export"); + } + const auto singleOutputs = outputDecorations(single.spirv.Words()); + Require(singleOutputs.locations.size() == 1 && singleOutputs.locations.begin()->second == 0u && singleOutputs.indices.empty(), "a pixel shader without a reversed-order blend code changed its outputs"); + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + const auto validate = [&](const ShaderRecompiler::RecompileResult& pixel, const AgcDriver::Graphics::State& target) { + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}}; + return AgcDriver::Graphics::ValidateShaders(shaders, target, subgroup, false); + }; + Require(validate(reversed, state) == std::set{0u}, "the reversed-order blended shader did not validate against its draw"); + expectFailure([&] { validate(single, state); }, "needs a float4 second color at location 0 index 1"); + auto plainState = state; + plainState.reversedBlend = 0u; + expectFailure([&] { validate(reversed, plainState); }, "Index decoration"); + for (const auto invalid : {0x2u, 0x80u, 0x100u, 0x10000u, 0x1000001u, 0x80000001u}) { + auto changed = request; + changed.context.pixel->reversedBlend = invalid; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(changed)); }, "invalid prepared reversed-order blend"); + } + const auto twoQueue = dualSourceQueue(); + const auto twoState = AgcDriver::Graphics::DecodeState(twoQueue); + constexpr std::array twoExports{0xf800000fu, 0x07060504u, 0xf800181fu, 0x0b0a0908u, 0xbf810000u}; + auto both = packedExportRequest(twoQueue, twoState, twoExports); + both.context.pixel->reversedBlend = 0x570001u; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(both)); }, "reversed-order blending needs MRT0 to be the only color output"); + both.context.pixel->dualSourceBlend = true; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(both)); }, "reversed-order blending cannot be combined with dual-source blending"); +} + void validationTests() { AgcDriver::Graphics::State state{}; state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; @@ -5463,6 +10009,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; @@ -5524,26 +10098,63 @@ void validationTests() { attribute.resource.fields[3] = 77u << 12u; attribute.fetchIndex = 1; attribute.resource.fields[1] = 0; - attribute.resource.fields[2] = 16; + const auto zeroStride = [&](std::uint32_t select, std::uint32_t records) { + attribute.resource.fields[3] = (77u << 12u) | (select << 28u); + attribute.resource.fields[2] = records; + }; + for (const auto records : {0u, 1u, 16u, 0xffffffffu}) { + zeroStride(0, records); + Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 0 must read every zero-stride fetch as zero"); + Require(AgcDriver::Graphics::ZeroStrideAvailableBytes(attribute) == 0, "OOB_SELECT 0 zero-stride fetch has bytes to read"); + expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range"); + expectFailure([&] { AgcDriver::Graphics::VertexBufferExtent(attribute); }, "empty or oversized"); + } + zeroStride(1, 16); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "zero stride must repeat one value"); Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 100, 7) == 0, "zero-stride input skipped its repeated value"); - attribute.resource.fields[2] = 8; - expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range"); - Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "a structured zero-stride fetch was treated as out of range"); - attribute.resource.fields[3] = (77u << 12u) | (2u << 28u); - attribute.resource.fields[2] = 1; + Require(AgcDriver::Graphics::VertexBufferExtent(attribute) == 16, "OOB_SELECT 1 extent must cover the whole fetch"); + zeroStride(1, 0); + Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 1 without records must read zero"); + zeroStride(1, 8); + Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 1 counted NUM_RECORDS in bytes"); + Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 8) == 16, "OOB_SELECT 1 must read the whole element for every index below NUM_RECORDS"); + Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1, 7) == 16, "OOB_SELECT 1 must read the whole element for the last index below NUM_RECORDS"); + expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 9); }, "record count"); + expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1, 8); }, "record count"); + zeroStride(1, 1); + Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1) == 16, "OOB_SELECT 1 with one record must read the element at index 0"); + expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 2); }, "record count"); + attribute.fetchIndex = 0; + zeroStride(1, 3); + Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 2, 1) == 16, "OOB_SELECT 1 must read the element for index 2 of 3 records"); + expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1); }, "record count"); + attribute.fetchIndex = 1; + zeroStride(2, 1); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "OOB_SELECT 2 must read a zero-stride fetch whole while NUM_RECORDS is nonzero"); Require(AgcDriver::Graphics::VertexBufferExtent(attribute) == 16, "OOB_SELECT 2 extent must cover the whole fetch"); Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 2 with records was treated as out of range"); - attribute.resource.fields[2] = 0; + zeroStride(2, 0); Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 2 without records must read zero"); - attribute.resource.fields[3] = (77u << 12u) | (3u << 28u); + zeroStride(3, 0); Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 without records must read zero"); - attribute.resource.fields[2] = 8; - Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 with records was treated as out of range"); - expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range"); - attribute.resource.fields[2] = 16; - Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1) == 16, "OOB_SELECT 3 must read a fetch inside the range"); + for (const auto records : {1u, 8u, 15u}) { + zeroStride(3, records); + Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 must read a fetch that only partly fits as zero"); + expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range"); + } + for (const auto records : {16u, 17u, 0xffffffffu}) { + zeroStride(3, records); + Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 with the whole fetch in range was treated as out of range"); + Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "OOB_SELECT 3 must read a fetch inside the range, whatever the index"); + Require(AgcDriver::Graphics::VertexBufferExtent(attribute) == 16, "OOB_SELECT 3 extent must cover only the fetch"); + } + attribute.resource.fields[3] = (22u << 12u) | (3u << 28u); + attribute.resource.fields[2] = 4; + Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 compared NUM_RECORDS with the wrong fetch size"); + attribute.resource.fields[2] = 3; + Require(AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "OOB_SELECT 3 read a 4-byte fetch past 3 bytes"); + attribute.resource.fields[3] = (113u << 12u) | (3u << 28u); + expectFailure([&] { AgcDriver::Graphics::VertexFetchOutOfRange(attribute); }, "unsupported vertex format"); attribute.resource.fields[3] = 113u << 12u; expectFailure([&] { AgcDriver::Graphics::DecodeVertexFormat(attribute); }, "unsupported vertex format"); attribute.resource.fields[3] = 50u << 12u; @@ -5760,6 +10371,26 @@ void stagingPoolBudgetTests() { Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{512} << 20u, "the staging pool budget fell below 512 MiB"); memory.memoryHeapCount = 1; Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{512} << 20u, "a host heap sized the staging pool budget"); + memory.memoryHeapCount = 2; + memory.memoryHeaps[1].size = VkDeviceSize{16} << 30u; + memory.memoryTypeCount = 2; + memory.memoryTypes[0] = {VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, 0}; + memory.memoryTypes[1] = {VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, 1}; + Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{256} << 20u, "the staging pool budget on a 16 GiB heap of system memory is not a sixty-fourth of it"); + memory.memoryHeaps[1].size = VkDeviceSize{4} << 30u; + 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() { @@ -5813,6 +10444,24 @@ void highestDrawIndexTests() { Require(HighestDrawIndex(bytesOf(restartOnly), 2, false) == 0xffff, "a 16-bit all-ones index without restart was skipped"); Require(HighestDrawIndex(bytesOf(narrow).first(4), 2, true) == 7 && HighestDrawIndex(bytesOf(narrow).first(2), 2, true) == 0, "the scan read past its index range"); expectFailure([&] { HighestDrawIndex(bytesOf(narrow), 1, false); }, "unsupported index size"); + Require(!HighestDrawIndex({}, 2, false).has_value() && !HighestDrawIndex({}, 4, true).has_value(), "an empty index range reached a vertex"); + std::vector long16(1027, 0xffff); + long16[1026] = 0; + Require(HighestDrawIndex(std::as_bytes(std::span(long16)), 2, true) == 0, "index 0 after many restart indices was not reached"); + long16[513] = 4000; + Require(HighestDrawIndex(std::as_bytes(std::span(long16)), 2, true) == 4000, "a 16-bit index inside a long range was missed"); + std::vector long32(1029); + for (std::size_t i = 0; i < long32.size(); ++i) long32[i] = static_cast(i * 7u % 1000u); + long32[1028] = 0x80000001u; + Require(HighestDrawIndex(std::as_bytes(std::span(long32)), 4, true) == 0x80000001u, "a 32-bit index above 2^31 in the tail was missed"); + long32[1028] = 0xffffffffu; + Require(HighestDrawIndex(std::as_bytes(std::span(long32)), 4, true) == 999u && HighestDrawIndex(std::as_bytes(std::span(long32)), 4, false) == 0xffffffffu, "a 32-bit restart index at the end of a long range"); + std::vector shifted(2 * 1000 + 3); + for (std::size_t i = 0; i < 1000; ++i) { + const auto value = static_cast(i == 777 ? 0x8001u : i % 300u); + std::memcpy(shifted.data() + 1 + 2 * i, &value, sizeof(value)); + } + Require(HighestDrawIndex(std::span(shifted).subspan(1, 2 * 1000 + 1), 2, false) == 0x8001u, "an unaligned 16-bit range with a trailing byte"); } void storeAtFlipTests() { @@ -5822,6 +10471,13 @@ void storeAtFlipTests() { for (const char* value : {"0", "", "true", "11"}) expectFailure([&] { StorageTexture::StoreAtFlipRequested(value); }, "expected 1"); } +void storeAtLabelTests() { + using AgcDriver::Graphics::StorageTexture; + Require(!StorageTexture::StoreAtLabelRequested(nullptr), "an unset APS5_STORE_AT_LABEL stored at each label"); + Require(StorageTexture::StoreAtLabelRequested("1"), "APS5_STORE_AT_LABEL=1 did not store at each label"); + for (const char* value : {"0", "", "true", "11"}) expectFailure([&] { StorageTexture::StoreAtLabelRequested(value); }, "expected 1"); +} + void nullVertexDescriptorTests() { using AgcDriver::Graphics::DecodeVertexFormat; constexpr std::array formats{VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT, VK_FORMAT_R32G32B32_SFLOAT, VK_FORMAT_R32G32B32A32_SFLOAT}; @@ -5897,6 +10553,39 @@ void vertexZeroFillTests() { Require(std::all_of(bytes.begin() + static_cast(valid), bytes.end(), [](std::byte value) { return value == std::byte{0}; }), "a vertex fetch past the record count did not read as zeros"); } +void vertexOverhangTests() { + using AgcDriver::Graphics::CopyZeroPaddedDrawInput; + constexpr std::size_t page = 65536; + constexpr std::size_t stride = 44; + constexpr std::size_t overhang = 8; +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, page * 2, MEM_RESERVE, PAGE_NOACCESS)); + Require(block != nullptr && VirtualAlloc(block, page, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot reserve the guest pages of the overhang test"); +#else + void* mapped = mmap(nullptr, page * 2, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(mapped != MAP_FAILED && mprotect(mapped, page, PROT_READ | PROT_WRITE) == 0, "cannot reserve the guest pages of the overhang test"); + auto* block = static_cast(mapped); +#endif + constexpr std::size_t offset = 0x650; + const auto readable = page - offset; + std::fill(block + offset, block + page, std::byte{0x7a}); + const auto address = reinterpret_cast(block) + offset; + { + const auto copy = CopyZeroPaddedDrawInput(mockContext(), address, readable + overhang, readable + overhang, stride); + const auto bytes = copy.buffer->Bytes(); + Require(bytes.size() == readable + overhang, "an overhanging vertex fetch changed its size"); + Require(std::all_of(bytes.begin(), bytes.begin() + static_cast(readable), [](std::byte value) { return value == std::byte{0x7a}; }), "an overhanging vertex fetch did not copy the readable bytes"); + Require(std::all_of(bytes.begin() + static_cast(readable), bytes.end(), [](std::byte value) { return value == std::byte{0}; }), "an overhanging vertex fetch did not zero its last record"); + } + expectFailure([&] { CopyZeroPaddedDrawInput(mockContext(), address, readable + stride, readable + stride, stride); }, "is not readable"); + expectFailure([&] { CopyZeroPaddedDrawInput(mockContext(), address, readable + overhang, readable + overhang, 0); }, "is not readable"); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + munmap(block, page * 2); +#endif +} + void vertexZeroFillMergeTests() { using AgcDriver::Graphics::PlanVertexCopies; using AgcDriver::Graphics::SoloZeroPaddedFetchIndices; @@ -5960,6 +10649,7 @@ void ResourceContentKeyTests(bool benchmark) { for (std::size_t i = 0; i < bits->size(); ++i) (*bits)[i] = i % 3u == 0; } images.imageSamplers.resize(count, 7u); + images.imageShapes.resize(count, ShaderRecompiler::DescriptorImageShape::Image2D); program.bindings.push_back(std::move(images)); ShaderRecompiler::DescriptorBinding data{}; data.kind = DescriptorKind::StorageBuffer; @@ -5995,6 +10685,9 @@ void ResourceContentKeyTests(bool benchmark) { ++program.bindings[0].imageSamplers.back(); Require(ShaderResources::ContentKey(shader, dataWords, movable) != original, "content key ignored the last sampler association"); --program.bindings[0].imageSamplers.back(); + program.bindings[0].imageShapes.back() = ShaderRecompiler::DescriptorImageShape::Image3D; + Require(ShaderResources::ContentKey(shader, dataWords, movable) != original, "content key ignored the last image shape"); + program.bindings[0].imageShapes.back() = ShaderRecompiler::DescriptorImageShape::Image2D; } Require(original.capacity() == original.size(), "content key reserved a different word count than it serialized"); std::uint64_t hash = 14695981039346656037ull; @@ -6016,6 +10709,427 @@ 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(bool longPath = false, bool extended = false) { + using namespace AgcDriver::Graphics; + using Stage = ShaderRecompiler::ShaderStage; + const auto root = std::filesystem::temp_directory_path() / ("aps5-pipeline-journal-mock-" + std::to_string(std::random_device{}())); + auto directory = root; + if (longPath) { + Require(root.native().size() < 180, "the temporary root is too long for the journal path fixture"); + directory /= std::filesystem::path(u8"cache-\u00e4\u6f22"); + directory /= std::string(229 - directory.native().size(), 'd'); + } + std::filesystem::create_directories(directory); + struct Cleanup { + std::filesystem::path directory; + std::filesystem::path journal; + ~Cleanup() { + ShaderRecompiler::ShaderDiskCache::Shutdown(); + std::error_code error; + if (!journal.empty()) { + std::filesystem::remove(journal, error); + auto lock = journal; + lock += ".lock"; + std::filesystem::remove(lock, error); + } + std::filesystem::remove_all(directory, error); + } + } cleanup{root, {}}; +#ifdef _WIN32 + if (extended) directory = std::filesystem::path(L"\\\\?\\" + directory.make_preferred().native()); + _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); + _wputenv_s(L"ANYPS5_SHADER_CACHE_DIR", directory.c_str()); + _putenv_s("APS5_PIPELINE_JOURNAL_THREADS", "2"); +#else + setenv("ANYPS5_NO_SHADER_CACHE", "0", 1); + 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"); + auto journalFile = PipelineJournalPath(); +#ifdef _WIN32 + std::error_code pathError; + journalFile = ShaderRecompiler::NativeCachePath_nid_no_patch(journalFile, pathError); + Require(!pathError, "cannot normalize the journal fixture path"); +#endif + cleanup.journal = journalFile; + if (longPath) Require(journalFile.native().size() > 260, "the journal fixture did not cross MAX_PATH"); + const auto module = [](std::uint32_t salt) { + ShaderRecompiler::RecompileResult result; + result.spirv = std::vector{0x07230203u, 0x00010000u, 0u, 8u, 0u, 0x00020011u, 1u, 0x00020011u, 2u + salt}; + 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 fmaskState = state; + fmaskState.samples = 8; + fmaskState.color.samples = 8; + fmaskState.color.fmaskAddress = 0x30000; + fmaskState.color.fmaskPipeBankXor = 0x1200; + for (auto& target : fmaskState.colors) { + target.samples = 8; + target.fmaskAddress = 0x40000; + target.fmaskPipeBankXor = 0x2300; + } + const auto fmaskRecipe = MakeGraphicsPipelineRecipe(fmaskState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(fmaskRecipe.state.color.fmaskAddress == 0 && fmaskRecipe.state.color.fmaskPipeBankXor == 0, "the canonical state kept the primary target's FMASK location"); + for (const auto& canonicalColor : fmaskRecipe.state.colors) { + Require(canonicalColor.fmaskAddress == 0 && canonicalColor.fmaskPipeBankXor == 0, "the canonical state kept an attachment's FMASK location"); + } + const auto fmaskBytes = EncodeGraphicsPipelineRecipe(fmaskRecipe); + GraphicsPipelineRecipe fmaskDecoded; + Require(DecodeGraphicsPipelineRecipe(fmaskBytes, fmaskDecoded) && EncodeGraphicsPipelineRecipe(fmaskDecoded) == fmaskBytes, "the FMASK pipeline recipe did not round-trip byte for byte"); + std::vector fmaskKey, fmaskReplayedKey; + BuildPipelineStoreKey(fmaskKey, context, fmaskState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(fmaskReplayedKey, context, fmaskDecoded.state, fmaskDecoded.vertexInput, layouts.View(), shaders, fmaskDecoded.attachmentLayout); + Require(fmaskKey != original && fmaskKey == fmaskReplayedKey, "the decoded FMASK state keys another pipeline"); + auto relocatedFmask = fmaskState; + relocatedFmask.color.fmaskAddress += 0x10000; + relocatedFmask.color.fmaskPipeBankXor ^= 0x100; + for (auto& target : relocatedFmask.colors) { + target.fmaskAddress += 0x20000; + target.fmaskPipeBankXor ^= 0x200; + } + std::vector relocatedFmaskKey; + BuildPipelineStoreKey(relocatedFmaskKey, context, relocatedFmask, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(relocatedFmaskKey == fmaskKey, "the FMASK resource location changed the pipeline key"); + Require(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(relocatedFmask, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)) == fmaskBytes, "the FMASK resource location changed the journal recipe"); + 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 pointState = state; + pointState.vertexPoints = true; + pointState.pointSize = 4.0f; + pointState.pointSizeMin = 0.5f; + pointState.pointSizeMax = 8.0f; + GraphicsPipelineRecipe pointDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(pointState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), pointDecoded), "the point list recipe does not decode"); + Require(pointDecoded.state.vertexPoints && pointDecoded.state.pointSize == 4.0f && pointDecoded.state.pointSizeMin == 0.5f && pointDecoded.state.pointSizeMax == 8.0f, "the recipe dropped the vertex point list or its point sizes"); + auto meshPointState = pointState; + meshPointState.vertexPoints = false; + meshPointState.stages.mesh = *lineState.stages.mesh; + meshPointState.stages.mesh->outputPrimitiveSize = 1u; + meshPointState.stages.mesh->provokingVertex = 0u; + GraphicsPipelineRecipe meshPointDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(meshPointState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), meshPointDecoded), "the mesh point recipe does not decode"); + Require(!meshPointDecoded.state.vertexPoints && meshPointDecoded.state.stages.mesh && meshPointDecoded.state.stages.mesh->outputPrimitiveSize == 1u && meshPointDecoded.state.pointSize == 4.0f && meshPointDecoded.state.pointSizeMin == 0.5f && meshPointDecoded.state.pointSizeMax == 8.0f, "the recipe dropped mesh points or their sizes without the vertex point flag"); + 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 mergedState = fourSampleState; + mergedState.mergedCoverage = true; + GraphicsPipelineRecipe mergedDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(mergedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), mergedDecoded), "the merged coverage recipe does not decode"); + Require(mergedDecoded.state.mergedCoverage, "the recipe dropped merged coverage"); + std::vector mergedKey, mergedReplayed; + BuildPipelineStoreKey(mergedKey, context, mergedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(mergedReplayed, context, mergedDecoded.state, mergedDecoded.vertexInput, layouts.View(), shaders, mergedDecoded.attachmentLayout); + Require(mergedKey != fourSamples && mergedKey == mergedReplayed, "the decoded merged coverage 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(journalFile) > 0, "the journal file is empty"); + const auto storeBefore = PipelineStoreTotals(); + std::vector receivedWords; + ShaderRecompiler::RecompileResult received; + bool receivedPush = false; + std::uint32_t computeCalls = 0; + std::mutex computeMutex; + PipelineJournalHooks hooks{[&](const ShaderRecompiler::RecompileResult& shader, bool pushConstants) { + std::lock_guard lock(computeMutex); + ++computeCalls; + received = shader; + receivedPush = pushConstants; + 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 = journalFile; + const auto whole = std::filesystem::file_size(journalPath); + Require(whole % 8 == 0 && whole > 3, "the journal file is not record aligned"); + std::filesystem::resize_file(journalPath, whole - 3); + 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"); + const auto writtenBefore = PipelineJournalTotals().writtenBytes; + RecordComputePipeline(compute, true); + FlushPipelineJournal(); + Require(PipelineJournalTotals().recordedCompute == reloaded.recordedCompute + 1 && PipelineJournalTotals().recordedModules == reloaded.recordedModules, "the pipeline whose record was torn was not recorded again, or its module was"); + const auto recordBytes = PipelineJournalTotals().writtenBytes - writtenBefore; + std::vector bytes; + Require(ShaderRecompiler::ReadWholeFile(journalFile, bytes) && recordBytes > 0 && recordBytes <= bytes.size(), "cannot read the re-recorded journal entry"); + { + std::ofstream file(journalFile, std::ios::binary | std::ios::app); + file.write(reinterpret_cast(bytes.data() + bytes.size() - recordBytes), static_cast(recordBytes)); + const std::array tornTail{}; + file.write(tornTail.data(), tornTail.size()); + file.close(); + Require(file.good(), "cannot append an external complete record and torn tail"); + } + auto another = module(5); + another.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 1, std::vector(4))); + const auto beforeAppend = PipelineJournalTotals(); + RecordComputePipeline(another, false); + FlushPipelineJournal(); + const auto appended = PipelineJournalTotals(); + Require(appended.writeFailures == 0 && appended.recordedCompute == beforeAppend.recordedCompute + 1, "the append after an external torn tail failed"); + Require(std::filesystem::file_size(journalFile) == bytes.size() + recordBytes + appended.writtenBytes - beforeAppend.writtenBytes, "tail recovery discarded an external complete record or retained torn bytes"); + auto recovered = StartPipelineJournalReplay(context, fingerprint, hooks); + Require(recovered != nullptr, "the replay after the external append did not start"); + recovered->Wait(); + Require(PipelineJournalTotals().loadedModules == 5 && PipelineJournalTotals().loadedRecipes == 4 && PipelineJournalTotals().droppedRecords == 0, "the external append and torn-tail recovery did not replay cleanly"); + } + { + const auto before = PipelineJournalTotals(); + 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(journalFile, 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"); @@ -6025,6 +11139,48 @@ 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-viewport-depth") { + UnusedViewportDepthTests(); + DepthClipTests(); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--unused-stencil-reference") { + stencilReferenceUnusedTests(); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--command-batch-fence-failure") { + commandBatchFenceFailureTests(); + return 0; + } + if (argc == 2 && (std::string_view(argv[1]) == "--pipeline-journal" || std::string_view(argv[1]) == "--pipeline-journal-long-paths" || std::string_view(argv[1]) == "--pipeline-journal-extended-paths")) { + pipelineJournalTests(std::string_view(argv[1]) != "--pipeline-journal", std::string_view(argv[1]) == "--pipeline-journal-extended-paths"); + std::cout << "pipeline journal recipe, record and replay tests passed\n"; + return 0; + } + 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]) == "--copy-input-kept-bytes") { + copyInputKeptBytesTests(); + std::cout << "GPU-copy input snapshot accounting and retention passed\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--depth-copy-kept-bytes") { + depthCopyKeptBytesTests(); + std::cout << "Depth-copy linear buffer accounting and retention passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--sparse-buffer-holes") { bool passed = false; { @@ -6038,6 +11194,69 @@ int main(int argc, char** argv) { std::cout << "sparse buffer hole tests " << (passed ? "passed" : "failed") << '\n'; return passed ? 0 : 1; } + if (argc == 2 && (std::string_view(argv[1]) == "--heap-mirror-unwatch" || std::string_view(argv[1]) == "--heap-mirror-unwatched-import")) { + { + mock = MockVulkan{}; + auto context = mockContext(); + context.bufferDeviceAddress = true; + context.limits.maxStorageBufferRange = 1u << 27; + RunHeapMirrorUnwatchTests(context, { + [](VkBuffer buffer) -> std::span { return mockBufferMemory(buffer); }, + {}, + [](VkDeviceAddress address) { + const auto offset = address - 0x100000000000ULL; + const auto buffer = reinterpret_cast(offset / 0x10000); + return mockBufferMemory(buffer).subspan(offset % 0x10000); + }, + {}, + {} + }, std::string_view(argv[1]) == "--heap-mirror-unwatched-import"); + } + Require(mock.live == 0, "the heap mirror unwatch test leaked Vulkan objects"); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--sparse-heap-mirror-unwatch") { + { + mock = MockVulkan{}; + auto context = mockContext(); + context.bufferDeviceAddress = true; + context.limits.maxStorageBufferRange = 1u << 27; + RunSparseHeapMirrorUnwatchTests(context, { + [](VkBuffer buffer) -> std::span { return mockBufferMemory(buffer); }, + {}, + [](VkDeviceAddress address) { + const auto offset = address - 0x100000000000ULL; + const auto buffer = reinterpret_cast(offset / 0x10000); + return mockBufferMemory(buffer).subspan(offset % 0x10000); + }, + {}, + {} + }); + } + Require(mock.live == 0, "the sparse heap mirror unwatch test leaked Vulkan objects"); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--heap-mirror-run-unwatch") { + { + mock = MockVulkan{}; + auto context = mockContext(); + context.bufferDeviceAddress = true; + context.limits.maxStorageBufferRange = 1u << 27; + RunHeapMirrorRunUnwatchTests(context, { + [](VkBuffer buffer) -> std::span { return mockBufferMemory(buffer); }, + {}, + [](VkDeviceAddress address) { + const auto offset = address - 0x100000000000ULL; + const auto buffer = reinterpret_cast(offset / 0x10000); + return mockBufferMemory(buffer).subspan(offset % 0x10000); + }, + {}, + {} + }); + } + Require(mock.live == 0, "the heap mirror run unwatch test leaked Vulkan objects"); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--address-space-teardown") { addressSpaceTeardownTests(); std::cout << "address-space device teardown releases cached guest allocations\n"; @@ -6047,6 +11266,16 @@ int main(int argc, char** argv) { bufferBudgetPlacementTests(); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--sample-location-grid-fallback") { + sampleLocationGridFallbackTests(); + std::cout << "Sample location grid fallback checks passed\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--sample-order") { + sampleOrderTests(); + std::cout << "Sample order checks passed\n"; + return 0; + } { AgcDriver::Graphics::PipelineCacheSynchronization synchronization; auto pipelineUse = synchronization.LockForPipelineUse(); @@ -6077,6 +11306,21 @@ int main(int argc, char** argv) { std::cout << "Alpha-to-coverage state and refusal tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--image-table-allocation") { + imageTableAllocationTests(); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--image-table-snapshot") { + imageTableSnapshotAllocationTests(); + return 0; + } + const std::array samplerCases{"--sampler-table-lod-bias", "--sampler-table-anisotropy", "--sampler-table-reduction-compare"}; + for (std::uint32_t index = 0; index < samplerCases.size(); ++index) { + if (argc == 2 && std::string_view(argv[1]) == samplerCases[index]) { + samplerTableDeviceLimitTests(index); + return 0; + } + } if (argc == 2 && std::string_view(argv[1]) == "--descriptor-pool-reset-failure") { descriptorPoolResetFailureTest(); std::cerr << "descriptor pool reset failure did not terminate\n"; @@ -6087,6 +11331,16 @@ 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; + } + if (argc == 2 && std::string_view(argv[1]) == "--depth-bounds-state") { + DepthBoundsBiasTests(); + std::cout << "Depth bounds state checks passed\n"; + return 0; + } { const AgcDriver::Graphics::Context context{}; const AgcDriver::Graphics::State state{}; @@ -6120,48 +11374,75 @@ int main(int argc, char** argv) { RunGuestLeaseWaitTests(); ResourceContentKeyTests(false); stateTests(); + pipelineStoreLazyBufferPoolTests(); + retainedPixelStageTests(); pipelineLibrarySetTests(); + pipelineLibraryClearWaitsForBuildTests(); + framebufferOwnerTests(); hdrComponentSwapTests(); depthMaintenanceTests(); hardwareScreenOffsetTests(); srgb8TargetTests(); DepthClipTests(); + DepthRemapTests(); + UnusedViewportDepthTests(); DepthStencilTests(); OneDimensionalColorTests(); htileDepthClearTests(); + multisampledSingleClearTests(); memoryValueFillTests(); ZExportTests(); unorm10_11_11TargetTests(); + float11_11_10TargetTests(); DepthBoundsBiasTests(); conservativeZExportTests(); earlyFragmentTestsTests(); orderedPixelShaderTests(); stencilRefExportTests(); + stencilReferenceUnusedTests(); aliasedBufferViewTests(); LineStripTests(); + ShaderModuleKeyTests(); ProvokingVertexTests(); ProvokingVertexRenderPassTests(); ConservativeRasterizationTests(); DisabledColorTests(); OpaqueDestinationAlphaTests(); + Color565TargetTests(); Color1555TargetTests(); BlendBypassTests(); + UnclampedNormalizedColorTests(); CompactedExportTests(); ReversedComponentOrderTests(); + ReversedComponentOrderBlendTests(); + ReversedBlendRoutingTests(); ShadersWriteMemoryTests(); DualSourceBlendTests(); + ReversedBlendShaderTests(); metadataPassTests(); + depthCopyPassTests(); cmaskTests(); uint16ExportTests(); + uint8x2TargetTests(); uint8x4TargetTests(); + msaaEnableClearDepthTests(); multisampleTests(); MsaaEnableSingleSampleTests(); + multisampledDepthTests(); clipDistanceTests(); framebufferLayerTests(); + msaaEnableClearOneSampleTargetTests(); UnwrittenSampleLocationTests(); + sampleLocationGridFallbackTests(); + sampleOrderTests(); + depthCopyFormatTests(); + quadPixelMaskTests(); ShaderStageTests(); + UnsupportedTessellationFieldTests(); NggVertexWorkgroupTests(); inactiveVertexPointSizeTests(); + PolygonPrimitiveTests(); + vertexPointListTests(); TuningFieldTests(); PixelInputLayoutTests(); UnexportedPixelInputTests(); @@ -6175,13 +11456,23 @@ int main(int argc, char** argv) { descriptorPoolTests(); pushConstantTests(); resourceTests(); + oversizedDescriptorTests(); + guestBufferViewBytesTests(); descriptorSnapshotTests(); + dispatchMoveTests(); descriptorCacheTests(); bufferBudgetPlacementTests(); + imageTableAllocationTests(); + imageTableSnapshotAllocationTests(); + samplerTableNonSeamlessCubeTests(); + samplerTableDeviceLimitTests(); + tableUpdateAfterBindTests(); misalignedShaderDataTests(); debugBranchTests(); + hostImportBudgetTests(); textureCacheBudgetTests(); sampledTextureBudgetTests(); + storageTextureBudgetTests(); sampledBudgetReportTests(); meshArgumentTests(); meshIndexBufferTests(); @@ -6190,15 +11481,19 @@ int main(int argc, char** argv) { vertexCopyTests(); highestDrawIndexTests(); storeAtFlipTests(); + storeAtLabelTests(); nullVertexDescriptorTests(); vertexZeroFillTests(); + vertexOverhangTests(); vertexZeroFillMergeTests(); parallelCompareTests(); pixelParameterSlotTests(); rectListTests(); + quadListTests(); floatControlsModeTests(); barycentricEmulationTests(); copiedWriterIndirectOverlapTests(); + registeredReadableCoverTests(); mock = MockVulkan{}; auto bdaContext = mockContext(); bdaContext.bufferDeviceAddress = true; @@ -6219,7 +11514,8 @@ int main(int argc, char** argv) { [](std::optional headroom) { mock.memoryLimit = headroom.has_value() ? std::optional(mock.allocatedBytes + *headroom) : std::nullopt; }, - [] { return mock.allocationAttempts; } + [] { return mock.allocationAttempts; }, + [](std::optional limit) { mock.importLimit = limit; } }); Require(mock.live == 0, "BDA resources leaked Vulkan objects"); RunGuestAllocationTests(); @@ -6270,7 +11566,13 @@ int main(int argc, char** argv) { return capture; }, [] { return mock.pipelineCreateCount; }, - [] { return mock.pipelineSpecializations.back(); } + [] { return mock.pipelineSpecializations.back(); }, + [] { return mock.descriptorPools.size(); }, + [] { + std::uint32_t resets = 0; + for (const auto& [handle, pool] : mock.descriptorPools) resets += pool.resets; + return resets; + } }); std::cout << "Graphics validation tests passed\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/GraphicsTests.hpp b/core/libs/prx/libSceAgcDriver/tests/GraphicsTests.hpp index 3a48ac3e94..28a442b75d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GraphicsTests.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/GraphicsTests.hpp @@ -27,6 +27,8 @@ struct TextureDetilerTestAccess { std::function lastDispatch; std::function pipelineCount; std::function()> lastSpecialization; + std::function descriptorPoolCount; + std::function descriptorPoolResets; }; void RunTextureFormatTests(); 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/GuestAllocations.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp index dd5b45a647..89d83afd29 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp @@ -52,7 +52,42 @@ void leaseSnapshotTests() { const auto third = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); Require(&third[0] != &first[0] && std::any_of(third.begin(), third.end(), [&](const auto& range) { return range->address == address + 64; }), "a lease taken after a registry change still shares the old snapshot"); } - Require(held.use_count() == 1, "a snapshot outlived its leases"); + const auto* kept = &GuestAllocations::GuestAllocationsAcquire_nid_postfix()[0]; + Require(&GuestAllocations::GuestAllocationsAcquire_nid_postfix()[0] == kept, "a lease taken after the others ended, without a registry change, rebuilt the snapshot"); + Require(held.use_count() == 2, "the kept snapshot does not hold the range once"); + { + GuestAllocations::Mutation mutation; + } + Require(held.use_count() == 1, "a snapshot outlived a registry change"); + std::weak_ptr readable; + { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + const auto found = std::find_if(lease.begin(), lease.end(), [&](const auto& range) { return range->address == address + 64; }); + Require(found != lease.end(), "the snapshot misses a readable range"); + readable = *found; + Require(!GuestAllocations::GuestAllocationsAcquireAll_nid_postfix().empty(), "the snapshot of all ranges is empty"); + } + Require(readable.use_count() == 3, "the kept snapshots do not hold a readable range once each"); + { + GuestAllocations::Mutation mutation; + mutation.Protect(memory.data() + 64, 64, false, false, true, [] {}); + } + Require(readable.expired(), "a kept snapshot holds a range that a protection change replaced"); + { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + Require(std::none_of(lease.begin(), lease.end(), [&](const auto& range) { return range->address == address + 64; }), "a lease taken after a range turned unreadable returned the old snapshot"); + const auto all = GuestAllocations::GuestAllocationsAcquireAll_nid_postfix(); + const auto found = std::find_if(all.begin(), all.end(), [&](const auto& range) { return range->address == address + 64; }); + Require(found != all.end() && !(*found)->readable && !(*found)->writable, "a lease of all ranges taken after a range turned unreadable returned the old snapshot"); + } + { + GuestAllocations::Mutation mutation; + mutation.Protect(memory.data() + 64, 64, true, true, true, [] {}); + } + { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + Require(std::any_of(lease.begin(), lease.end(), [&](const auto& range) { return range->address == address + 64 && range->readable; }), "a lease taken after a range turned readable again returned the old snapshot"); + } GuestAllocations::Mutation mutation; mutation.Remove(memory.data()); mutation.Remove(memory.data() + 64); @@ -223,6 +258,16 @@ void RunUnmappedGapTests() { Require(!GuestMemory::Accessible(reinterpret_cast(std::uintptr_t{0x18}), 4), "a near-null address counts as accessible"); munmap(block, page); munmap(block + 2 * page, page); + alignas(4096) static std::array imagePage{}; + const auto imageAddress = reinterpret_cast(imagePage.data()); + Require(GuestMemory::Accessible(imagePage.data(), page, true), "a main image data page is not writable"); + Require(mprotect(imagePage.data(), page, PROT_READ) == 0, "cannot protect the main image test page behind the registry"); + Require(GuestMemory::Accessible(imagePage.data(), page, true), "a checked main image page was queried again instead of read from the page table"); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(imageAddress, page); + Require(!GuestMemory::Accessible(imagePage.data(), page, true) && GuestMemory::Accessible(imagePage.data(), page), "an invalidated main image page kept its old access"); + Require(mprotect(imagePage.data(), page, PROT_READ | PROT_WRITE) == 0, "cannot restore the main image test page"); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(imageAddress, page); + Require(GuestMemory::Accessible(imagePage.data(), page, true), "a restored main image page is not writable"); if (!GuestArena::GuestArenaAvailable_nid_postfix()) return; auto* guest = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(2 * page, page)); Require(mmap(guest, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == guest, "cannot map the arena page test pages"); @@ -253,6 +298,33 @@ void RunUnmappedGapTests() { mutation.Unmap(guest + page, page, release); } Require(!GuestMemory::Accessible(guest, page) && !GuestMemory::Accessible(guest + page, page), "unmapped arena pages are still accessible"); + Require(mmap(guest, page, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == guest, "cannot reserve the arena test page"); + Require(!GuestMemory::Accessible(guest, page), "a reserved arena page is accessible"); + Require(mprotect(guest, page, PROT_READ | PROT_WRITE) == 0, "cannot open the reserved arena test page behind the registry"); + Require(!GuestMemory::Accessible(guest, page), "a checked inaccessible arena page was queried again instead of read from the page table"); + { + GuestAllocations::Mutation mutation; + mutation.Add(guest, page, true, true, true); + } + Require(GuestMemory::Accessible(guest, page, true), "an arena page added through the registry kept its inaccessible state"); + { + GuestAllocations::Mutation mutation; + mutation.Unmap(guest, page, [](const void* pointer, std::size_t bytes, const void*, bool) { Require(munmap(const_cast(pointer), bytes) == 0, "cannot unmap the arena test page"); }); + } GuestArena::GuestArenaRelease_nid_postfix(guest, 2 * page); + constexpr std::uint32_t ReadWriteProtection = 0x04; + auto* spare = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(page, page)); + const auto spareAddress = reinterpret_cast(spare); + Require(!GuestMemory::Accessible(spare, page), "a reserved arena page is accessible"); + GuestArena::GuestArenaCommit_nid_postfix(spare, page, ReadWriteProtection, page); + Require(GuestMemory::Accessible(spare, page, true), "an arena page committed outside the registry kept its inaccessible state"); + GuestArena::GuestArenaReset_nid_postfix(spare, page); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(spareAddress, page); + Require(!GuestMemory::Accessible(spare, page), "a reset arena page is still accessible"); + GuestArena::GuestArenaSetProtection_nid_postfix(spareAddress, page, ReadWriteProtection); + Require(GuestMemory::Accessible(spare, page, true), "an arena page opened outside the registry kept its inaccessible state"); + GuestArena::GuestArenaReset_nid_postfix(spare, page); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(spareAddress, page); + GuestArena::GuestArenaRelease_nid_postfix(spare, page); #endif } 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 ecf28b1e91..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) { @@ -396,19 +421,35 @@ void RunGuestSamplerResourceTests() { Fields unequalFilters = unnormalizedBase; unequalFilters.xyMinFilter = 0; - rejectUnnormalized(unequalFilters, "unnormalized coordinates with different minification and magnification filters"); + const auto unequal = DecodeSamplerResource(pack(unequalFilters), true); + requireUnnormalized(unequal, "unnormalized S# with different min and mag filters"); + Require(unequal.magFilter == VK_FILTER_LINEAR && unequal.minFilter == VK_FILTER_LINEAR, "unnormalized coordinates must use mag filter for both filters"); Fields unnormalizedAniso = unnormalizedBase; unnormalizedAniso.xyMagFilter = 2; unnormalizedAniso.xyMinFilter = 2; rejectUnnormalized(unnormalizedAniso, "unnormalized coordinates with anisotropic filtering"); - for (const std::uint32_t clamp : {0u, 1u, 3u, 4u, 5u, 7u}) { + for (const std::uint32_t clamp : {1u, 3u, 4u, 5u, 7u}) { Fields clampX = unnormalizedBase; clampX.clampX = clamp; rejectUnnormalized(clampX, "unnormalized coordinates with clamp mode " + std::to_string(clamp) + " on X"); } - Fields clampY = unnormalizedBase; - clampY.clampY = 0; - rejectUnnormalized(clampY, "unnormalized coordinates with clamp mode 0 on Y"); + for (const std::uint32_t clamp : {1u, 3u, 4u, 5u, 7u}) { + Fields clampY = unnormalizedBase; + clampY.clampY = clamp; + rejectUnnormalized(clampY, "unnormalized coordinates with clamp mode " + std::to_string(clamp) + " on Y"); + } + Fields clampZero = unnormalizedBase; + clampZero.clampX = 0; + clampZero.clampY = 0; + const auto zeroDecoded = DecodeSamplerResource(pack(clampZero), true); + requireUnnormalized(zeroDecoded, "unnormalized S# with clamp mode 0"); + Require(zeroDecoded.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && zeroDecoded.addressModeV == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, "unnormalized clamp mode 0 must decode to clamp-to-edge"); + Fields unnormalizedBiased = unnormalizedBase; + unnormalizedBiased.lodBiasRaw = 256; + unnormalizedBiased.lodBiasSec = 8; + const auto unnormalizedBiasedResult = DecodeSamplerResource(pack(unnormalizedBiased), true); + requireUnnormalized(unnormalizedBiasedResult, "unnormalized S# with primary and secondary LOD bias"); + Require(unnormalizedBiasedResult.lodBias == 0.0f, "unnormalized coordinates must zero host lodBias"); Fields unnormalizedTruncated = unnormalizedBase; unnormalizedTruncated.truncCoord = true; rejectUnnormalized(unnormalizedTruncated, "unnormalized coordinates with TRUNC_COORD"); @@ -441,9 +482,17 @@ void RunGuestSamplerResourceTests() { badDegamma.disableDegamma = true; rejectFields(badDegamma, "disables degamma"); - Fields badLodBiasSec = base; - badLodBiasSec.lodBiasSec = 1; - rejectFields(badLodBiasSec, "secondary LOD bias"); + Fields secLodBias = base; + secLodBias.lodBiasSec = 1; + const auto secResult = DecodeSamplerResource(pack(secLodBias)); + Require(nearlyEqual(secResult.lodBias, 0.0f), "secondary LOD bias must not alter host lodBias"); + secLodBias.lodBiasSec = 0x3f; + const auto negSecResult = DecodeSamplerResource(pack(secLodBias)); + Require(nearlyEqual(negSecResult.lodBias, 0.0f), "negative secondary LOD bias must not alter host lodBias"); + secLodBias.lodBiasRaw = 256; + secLodBias.lodBiasSec = 8; + const auto combinedResult = DecodeSamplerResource(pack(secLodBias)); + Require(nearlyEqual(combinedResult.lodBias, 1.0f), "nonzero secondary LOD bias must leave primary lodBias unaffected"); Fields badPreclamp = base; badPreclamp.pointPreclamp = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index bd5e36614f..0cc0c41ed7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include #include #include @@ -94,8 +95,66 @@ void rejectFields(const Fields& f, std::string_view reason) { } +static void RunR128TextureTests() { + constexpr std::array streamed{0xa0c18800u, 0xcad00000u, 0x01ffc1ffu, 0x905b2facu, 0u, 0u, 0u, 0u}; + const auto view = DecodeTextureResource(streamed); + Require(view.baseAddress == 0xa0c1880000ull && view.format == 173u && view.tileMode == TextureTileMode::kStandard4KB && view.width == 512u && view.height == 512u && view.baseLevel == 0u && view.lastLevel == 9u && view.mipCount == 10u && view.allocatedMipCount == 10u && view.minLod == 0u, + "a 128-bit BC3 T# (2048x2048, levels 2..11, words 4-7 zero) must read as the 512x512 chain of its 10 resident levels, got " + std::to_string(view.width) + "x" + std::to_string(view.height) + " levels " + std::to_string(view.baseLevel) + ".." + std::to_string(view.lastLevel) + " of " + std::to_string(view.mipCount)); + auto chainWords = streamed; + chainWords[3] &= ~(0xfu << 12u); + chainWords[5] = 11u << 4u; + const auto full = DecodeTextureResource(chainWords); + Require(full.width == 2048u && full.mipCount == 12u, "the same surface with MAX_MIP 11 must be the 12-level 2048x2048 chain"); + const auto chain = DescribeSurface(full); + const auto resident = DescribeSurface(view); + Require(chain.mips.size() == 12u && resident.mips.size() == 10u, "the chain and the resident levels must have 12 and 10 levels"); + for (std::uint32_t level = 0; level < 10u; ++level) { + const auto& a = chain.mips[level + 2u]; + const auto& b = resident.mips[level]; + Require(a.tiledOffset == b.tiledOffset && a.tiledSize == b.tiledSize && a.width == b.width && a.height == b.height && a.blocksPerRow == b.blocksPerRow && a.tail == b.tail && a.tailX == b.tailX && a.tailY == b.tailY, "resident level " + std::to_string(level) + " must be level " + std::to_string(level + 2u) + " of the 12-level chain"); + } + Require(resident.guestBytes <= chain.mips[1].tiledOffset && chain.mips[0].tiledOffset > chain.mips[1].tiledOffset, "the resident levels must end before the chain's levels 1 and 0, which come last in a GFX10 swizzled chain"); + auto levelZero = streamed; + levelZero[3] &= ~(0xfu << 12u); + const auto single = DecodeTextureResource(levelZero); + Require(single.mipCount == 1u && single.lastLevel == 0u, "a T# with words 4-7 zero that starts at level 0 must stay one level"); + auto tailed = streamed; + tailed[5] = 4u << 20u; + reject([&] { DecodeTextureResource(tailed); }, "would move the surface's own"); + + constexpr std::array tall{0xa0c1de00u, 0xcad00000u, 0x01ffc0ffu, 0x905b4facu, 0u, 0u, 0u, 0u}; + const auto tallView = DecodeTextureResource(tall); + Require(tallView.width == 64u && tallView.height == 128u && tallView.lastLevel == 7u && tallView.mipCount == 8u, "a 128-bit 1024x2048 T# of levels 4..11 must read as its 64x128 chain of 8 levels"); + + Fields clamped; + clamped.base40 = 0x55ea000ull; + clamped.format = 20; + clamped.width = 1024; + clamped.height = 1024; + clamped.tileModeRaw = 0x1b; + clamped.baseLevel = 2; + clamped.lastLevel = 10; + clamped.minLod = 2u * 256u + 64u; + const auto clampedView = DecodeTextureResource(pack(clamped)); + Require(clampedView.width == 256u && clampedView.mipCount == 9u && clampedView.minLod == 64u, "reading a 128-bit T# from its resident levels must move its MIN_LOD clamp with the base level"); + clamped.minLod = 256u; + Require(DecodeTextureResource(pack(clamped)).minLod == 0u, "a MIN_LOD clamp below the base level must vanish when read from the resident levels"); +} + void RunGuestTextureResourceTests() { Fields base; + const auto baseWords = pack(base); + Require(!IsNullTextureResource(baseWords), "a nonzero 2D texture was classified as null"); + Fields nullBase = base; + nullBase.base40 = 0; + Require(IsNullTextureResource(pack(nullBase)), "a zero-base texture was not classified as null"); + 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"); @@ -104,6 +163,52 @@ void RunGuestTextureResourceTests() { Require(result.tileMode == TextureTileMode::kLinear, "decoded tile mode changed"); Require(result.dimension == TextureDimension::k2D, "decoded dimension changed"); Require(result.format == 56, "decoded format changed"); + Fields fmask = base; + fmask.format = 166u; + fmask.tileModeRaw = 24u; + fmask.dstSelY = fmask.dstSelZ = fmask.dstSelW = 0u; + const auto fmaskResource = DecodeTextureResource(pack(fmask)); + Require(fmaskResource.format == 20u && fmaskResource.dimension == TextureDimension::k2D && fmaskResource.tileMode == TextureTileMode::kZ64KBX, "uncompressed 32-bit FMASK did not use an integer Z surface"); + auto compressedFmask = fmask; + compressedFmask.metaCompress = compressedFmask.metaPipeAligned = true; + compressedFmask.metaAddr = 0x123456789aull; + const auto compressedResource = DecodeTextureResource(pack(compressedFmask)); + Require(compressedResource.fmaskCmaskAddress == compressedFmask.metaAddr << 8u && compressedResource.dccAddress == 0u && compressedResource.dccPipeAligned, "compressed FMASK CMASK was interpreted as color DCC"); + compressedFmask.metaPipeAligned = false; + rejectFields(compressedFmask, "FMASK CMASK metadata must be pipe aligned"); + compressedFmask.metaPipeAligned = compressedFmask.writeCompress = true; + rejectFields(compressedFmask, "FMASK CMASK compression is not implemented"); + Fields fmaskArray = fmask; + fmaskArray.typeRaw = 13u; + fmaskArray.depth = 2u; + fmaskArray.baseArray = 1u; + const auto fmaskArrayResource = DecodeTextureResource(pack(fmaskArray)); + Require(fmaskArrayResource.format == 20u && fmaskArrayResource.baseArray == 1u && fmaskArrayResource.depthOrLastArray == 2u, "FMASK normalization changed the array view"); + for (std::uint32_t format = 156u; format <= 168u; ++format) { + if (format == 166u) continue; + auto unsupported = fmask; + unsupported.format = format; + rejectFields(unsupported, "FMASK format is not implemented"); + } + for (const auto field : {&Fields::metaCompress, &Fields::writeCompress}) { + auto compressed = fmask; + compressed.*field = true; + rejectFields(compressed, "FMASK CMASK compression is not implemented"); + } + for (const auto selector : {5u, 6u, 7u}) { + auto unsupported = fmask; + unsupported.dstSelX = selector; + rejectFields(unsupported, "FMASK destination channel selector is not implemented"); + } + auto unsupportedFmask = fmask; + unsupportedFmask.tileModeRaw = 0u; + rejectFields(unsupportedFmask, "FMASK reads require a single-mip"); + unsupportedFmask = fmask; + unsupportedFmask.lastLevel = unsupportedFmask.maxMip = 1u; + rejectFields(unsupportedFmask, "FMASK reads require a single-mip"); + unsupportedFmask = fmask; + unsupportedFmask.typeRaw = 10u; + rejectFields(unsupportedFmask, "FMASK reads require a single-mip"); Require(result.dstSelX == 4 && result.dstSelY == 5 && result.dstSelZ == 6 && result.dstSelW == 7, "decoded destination selectors changed"); Fields wide = base; @@ -240,10 +345,157 @@ void RunGuestTextureResourceTests() { Fields cubeBadArray = cube; cubeBadArray.baseArray = 6; rejectFields(cubeBadArray, "base array past its last array slice"); - Fields cubeNotMultiple = cube; - cubeNotMultiple.depth = 4; - rejectFields(cubeNotMultiple, "multiple of 6"); + Fields cubeExtraSlice = cube; + cubeExtraSlice.depth = 6; + result = DecodeTextureResource(pack(cubeExtraSlice)); + Require(result.baseArray == 0 && result.depthOrLastArray == 6, "a cube descriptor with a slice past its last whole cube lost a slice"); + Require(DescribeSurface(result).imageLayers == 7, "a cube allocation with a slice past its last whole cube omitted it"); + Fields cubePartial = cube; + cubePartial.depth = 4; + rejectFields(cubePartial, "fewer than 6 array slices"); + Fields multisampled = base; + multisampled.typeRaw = 14; + multisampled.width = 297; + multisampled.height = 297; + multisampled.tileModeRaw = 0x1b; + multisampled.lastLevel = 2; + multisampled.maxMip = 2; + result = DecodeTextureResource(pack(multisampled)); + Require(result.dimension == TextureDimension::k2DMsaa && result.samples == 4 && result.width == 297 && result.height == 297, "4x multisampled descriptor decoded incorrectly"); + Require(result.mipCount == 1 && result.allocatedMipCount == 1 && result.baseLevel == 0 && result.lastLevel == 0, "a multisampled descriptor's sample count was decoded as mip levels"); + Require(DecodeTextureResource(pack(base)).samples == 1, "a single-sample descriptor decoded with several samples"); + const std::array canvas{0x021f2000u, 0x03800000u, 0x804a004au, 0xe1b20facu, 0x00000000u, 0x00400020u, 0x00000000u, 0x00000000u}; + result = DecodeTextureResource(canvas); + Require(result.dimension == TextureDimension::k2DMsaa && result.samples == 4 && result.baseAddress == 0x21f200000ull && result.width == 297 && result.height == 297 && result.format == 56 && result.tileMode == TextureTileMode::kR64KBX, "the 4x canvas descriptor of PPSA99006 decoded incorrectly"); + Require(DescribeSurface(result).guestBytes == 0x190000, "4x render-target geometry must include fragment-aware padding for the PPSA99006 canvas"); + const auto canvasGeometry = DescribeSurface(result); + const std::array sonic{0x405fc080u, 0xc4700000u, 0x821bc3bfu, 0xe1b30facu, 0x00000000u, 0x00400030u, 0x406b0000u, 0x0040819fu}; + const auto sonicResource = DecodeTextureResource(sonic); + const auto sonicGeometry = DescribeSurface(sonicResource); + Require(sonicResource.width == 3840u && sonicResource.height == 2160u && sonicResource.samples == 8u && sonicResource.format == 71u, "the PPSA99902 multisampled color descriptor decoded incorrectly"); + Require(sonicGeometry.guestBytes == 0x1fe00000ull, "the PPSA99902 8x surface footprint omits fragments: " + std::to_string(sonicGeometry.guestBytes)); + Require(canvasGeometry.guestBytes == 0x190000u, "the 297x297 4x surface footprint does not use fragment-aware blocks"); + auto singleSample = sonicResource; + singleSample.samples = 1u; + singleSample.dimension = TextureDimension::k2D; + Require(DescribeSurface(singleSample).guestBytes == 0x3fc0000u, "single-sample surface geometry changed"); + struct FootprintCase { + TextureTileMode tile; + VkFormat format; + std::uint32_t elementBytes, width, height, samples, blocksPerRow; + std::uint64_t bytes; + }; + const std::array footprints{ + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R8_UINT, 1u, 129u, 65u, 2u, 2u, 0x20000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R16_UINT, 2u, 65u, 129u, 4u, 1u, 0x30000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32_UINT, 4u, 129u, 65u, 2u, 3u, 0x30000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32_UINT, 4u, 65u, 129u, 2u, 2u, 0x40000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32_UINT, 4u, 297u, 297u, 4u, 5u, 0x190000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32_UINT, 4u, 65u, 129u, 8u, 3u, 0x90000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32G32_SFLOAT, 8u, 3840u, 2160u, 8u, 120u, 0x1fe00000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32G32B32A32_UINT, 16u, 16384u, 16384u, 8u, 1024u, 0x800000000ull}, + FootprintCase{TextureTileMode::kZ64KBX, VK_FORMAT_R16_UNORM, 2u, 129u, 65u, 2u, 2u, 0x20000u}, + FootprintCase{TextureTileMode::kZ64KBX, VK_FORMAT_R32_SFLOAT, 4u, 65u, 129u, 8u, 3u, 0x90000u}, + FootprintCase{TextureTileMode::kStandard4KB, VK_FORMAT_R32_UINT, 4u, 17u, 33u, 4u, 2u, 0x6000u}, + FootprintCase{TextureTileMode::kStandard64KB, VK_FORMAT_R32_UINT, 4u, 129u, 65u, 2u, 3u, 0x30000u} + }; + for (const auto& test : footprints) { + auto resource = sonicResource; + const auto format = FindGuestTextureFormat(test.format, test.elementBytes); + Require(format.has_value(), "the multisample footprint test format has no guest encoding"); + resource.format = format.value(); + resource.width = test.width; + resource.height = test.height; + resource.samples = test.samples; + resource.tileMode = test.tile; + const auto geometry = DescribeSurface(resource); + const auto linearBytes = static_cast(test.width) * test.height * test.samples * test.elementBytes; + Require(geometry.guestBytes == test.bytes && geometry.layerBytes == test.bytes && geometry.sliceLinearBytes == linearBytes, "multisampled surface footprint or linear size disagrees with the reference"); + Require(geometry.layers == 1u && geometry.imageLayers == 1u && geometry.imageDepth == 1u && !geometry.thick && geometry.blockDepth == 1u && geometry.mips.size() == 1u, "multisampled surface geometry is not one thin 2D level"); + const auto& mip = geometry.mips.front(); + Require(mip.tiledOffset == 0u && mip.tiledSize == test.bytes && mip.linearOffset == 0u && mip.linearSize == linearBytes && mip.width == test.width && mip.height == test.height && mip.blocksPerRow == test.blocksPerRow && mip.pitchBytes == test.width * test.samples * test.elementBytes && !mip.tail && mip.tailX == 0u && mip.tailY == 0u, "multisampled mip geometry lost its sample pitch or block layout"); + } + auto unsupportedSurface = sonicResource; + for (const auto samples : {0u, 3u, 16u}) { + unsupportedSurface.samples = samples; + reject([&] { DescribeSurface(unsupportedSurface); }, "fragment count"); + } + unsupportedSurface = sonicResource; + unsupportedSurface.mipCount = 2u; + reject([&] { DescribeSurface(unsupportedSurface); }, "mip levels"); + unsupportedSurface = sonicResource; + unsupportedSurface.dimension = TextureDimension::k2DArray; + reject([&] { DescribeSurface(unsupportedSurface); }, "dimension or array"); + unsupportedSurface = sonicResource; + unsupportedSurface.tileMode = TextureTileMode::kStandard256B; + reject([&] { DescribeSurface(unsupportedSurface); }, "4 KiB or 64 KiB"); + for (const auto format : {56u, 71u}) { + for (const auto samples : {2u, 4u, 8u}) { + for (const auto extent : {std::array{1u, 1u}, std::array{17u, 5u}, std::array{297u, 297u}}) { + auto linear = sonicResource; + linear.tileMode = TextureTileMode::kLinear; + linear.format = format; + linear.samples = samples; + linear.width = extent[0]; + linear.height = extent[1]; + auto single = linear; + single.samples = 1u; + single.dimension = TextureDimension::k2D; + const auto expected = DescribeSurface(single); + const auto actual = DescribeSurface(linear); + Require(actual.guestBytes == expected.guestBytes && actual.layerBytes == expected.layerBytes && actual.sliceLinearBytes == expected.sliceLinearBytes && actual.mips.size() == expected.mips.size(), "linear multisampled descriptors must retain the existing geometry path"); + const auto& mip = actual.mips.front(); + const auto& baseline = expected.mips.front(); + Require(mip.tiledSize == baseline.tiledSize && mip.linearSize == baseline.linearSize && mip.pitchBytes == baseline.pitchBytes && mip.blocksPerRow == baseline.blocksPerRow, "linear multisampled descriptors changed existing mip geometry"); + } + } + } + Fields fewerFragments = multisampled; + fewerFragments.maxMip = 1; + rejectFields(fewerFragments, "same sample count"); + Fields singleSampleType = multisampled; + singleSampleType.lastLevel = 0; + singleSampleType.maxMip = 0; + rejectFields(singleSampleType, "same sample count"); + Fields offsetLevel = multisampled; + offsetLevel.baseLevel = 1; + rejectFields(offsetLevel, "same sample count"); + Fields multisampledSlice = multisampled; + multisampledSlice.depth = 1; + rejectFields(multisampledSlice, "nonzero depth or base array"); + Fields multisampledArray = multisampled; + multisampledArray.typeRaw = 15; + rejectFields(multisampledArray, "unsupported image type"); + + for (const auto format : {7u, 22u}) { + for (const auto log2 : {1u, 2u, 3u}) { + auto nativeDepth = base; + nativeDepth.typeRaw = 14; + nativeDepth.format = format; + nativeDepth.tileModeRaw = 24; + nativeDepth.lastLevel = log2; + nativeDepth.maxMip = log2; + nativeDepth.msaaDepth = true; + const auto decoded = DecodeTextureResource(pack(nativeDepth)); + Require(decoded.iterate256 && decoded.samples == (1u << log2) && decoded.tileMode == TextureTileMode::kZ64KBX && decoded.format == format, "a native multisampled depth descriptor lost ITERATE_256 or its identity"); + Require(decoded.mipCount == 1 && decoded.allocatedMipCount == 1 && decoded.baseLevel == 0 && decoded.lastLevel == 0, "native multisampled depth retained sample counts as mip levels"); + auto wrongTile = nativeDepth; + wrongTile.tileModeRaw = 27; + rejectFields(wrongTile, "ITERATE_256"); + auto wrongFormat = nativeDepth; + wrongFormat.format = 56; + rejectFields(wrongFormat, "ITERATE_256"); + auto wrongType = nativeDepth; + wrongType.typeRaw = 9; + rejectFields(wrongType, "ITERATE_256"); + wrongType.typeRaw = 15; + rejectFields(wrongType, "ITERATE_256"); + auto mismatchedSamples = nativeDepth; + mismatchedSamples.maxMip = log2 - 1u; + rejectFields(mismatchedSamples, "same sample count"); + } + } Fields badType = base; badType.typeRaw = 0; rejectFields(badType, "unsupported image type"); @@ -315,7 +567,7 @@ void RunGuestTextureResourceTests() { Fields badMsaa = base; badMsaa.msaaDepth = true; - rejectFields(badMsaa, "MSAA"); + rejectFields(badMsaa, "ITERATE_256"); Fields blockSize = base; blockSize.maxUncompBlkSize = 1; @@ -423,6 +675,7 @@ void RunGuestTextureResourceTests() { pastUntailed.maxMip = 0; pastUntailed.baseLevel = 1; pastUntailed.lastLevel = 1; + pastUntailed.perfMod = 4; rejectFields(pastUntailed, "would move the surface's own"); auto untailedChain = tailView; untailedChain.mipCount = 1; @@ -430,6 +683,8 @@ void RunGuestTextureResourceTests() { untailedChain.mipCount = 2; Require(untailedAllocated == 0xff0000u && DescribeSurface(untailedChain).guestBytes == 0x1470000u, "a second level must grow a 1920x1080 64 bpp SW_64KB_R_X surface from addrlib's 0xff0000 to 0x1470000 bytes"); + RunR128TextureTests(); + std::array shortWords{}; reject([&] { DecodeTextureResource(shortWords); }, "8 dwords"); @@ -438,6 +693,11 @@ void RunGuestTextureResourceTests() { Require(MatchesGuestDimension(Shape::Image2D, TextureDimension::k2D), "2D shape must match 2D dimension"); Require(!MatchesGuestDimension(Shape::Image2D, TextureDimension::k2DArray), "2D shape must not match 2D array dimension"); Require(MatchesGuestDimension(Shape::Image2DArray, TextureDimension::k2DArray), "2D array shape must match 2D array dimension"); + Require(MatchesGuestDimension(Shape::Image2DMultisample, TextureDimension::k2DMsaa), "multisampled shape must match the multisampled dimension"); + Require(!MatchesGuestDimension(Shape::Image2DMultisample, TextureDimension::k2D), "multisampled shape must not match 2D dimension"); + Require(MatchesGuestDimension(Shape::Image2DMultisampleArray, TextureDimension::k2DMsaa), "multisampled array shape must match the multisampled dimension"); + Require(!MatchesGuestDimension(Shape::Image2DMultisampleArray, TextureDimension::k2DArray), "multisampled array shape must not match 2D array dimension"); + Require(!MatchesGuestDimension(Shape::Image2D, TextureDimension::k2DMsaa), "2D shape must not match the multisampled dimension"); Require(MatchesGuestDimension(Shape::Image2DArray, TextureDimension::kCube), "a cube must be readable as a 2D array of its faces"); Require(MatchesGuestDimension(Shape::ImageCube, TextureDimension::kCube), "cube shape must match cube dimension"); Require(!MatchesGuestDimension(Shape::ImageCube, TextureDimension::k1D), "cube shape must not match 1D dimension"); @@ -483,4 +743,21 @@ void RunGuestTextureResourceTests() { Fields cornered = base; cornered.cornerSample = true; Require(!SampledTexturesShareEntry(pack(base), pack(cornered)), "T#s differing in a field next to the feedback fields must not share a sampled texture entry"); + + Fields arrayFields = base; + arrayFields.typeRaw = 13; + arrayFields.depth = 1536; + arrayFields.baseArray = 3; + const auto arrayWords = pack(arrayFields); + std::array narrowed{}; + Require(FirstLayerDescriptor(arrayWords, narrowed), "a 2D array T# naming more than its base slice must narrow to that slice"); + const auto slice = DecodeTextureResource(narrowed); + const auto whole = DecodeTextureResource(arrayWords); + Require(slice.dimension == TextureDimension::k2DArray && slice.baseArray == 3 && slice.depthOrLastArray == 3, "a narrowed 2D array T# must name its base slice alone"); + Require(slice.baseAddress == whole.baseAddress && slice.width == whole.width && slice.format == whole.format && slice.mipCount == whole.mipCount && slice.tileMode == whole.tileMode, "narrowing a 2D array T# must keep every other field"); + Require(!SampledTexturesShareEntry(arrayWords, narrowed), "a narrowed 2D array T# must not share the whole array's sampled texture entry"); + Fields single = arrayFields; + single.depth = 3; + Require(!FirstLayerDescriptor(pack(single), narrowed), "a 2D array T# of its base slice alone needs no narrowing"); + Require(!FirstLayerDescriptor(baseWords, narrowed), "a 2D T# needs no narrowing"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/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/HostAllocator.cpp b/core/libs/prx/libSceAgcDriver/tests/HostAllocator.cpp new file mode 100644 index 0000000000..e6b6ff60c5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostAllocator.cpp @@ -0,0 +1,154 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#define PSAPI_VERSION 2 +#include +#include +#endif + +namespace { + +int failures = 0; + +void Check(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", what); + ++failures; +} + +void FillAndVerify(unsigned char* bytes, std::size_t size, unsigned char seed) { + for (std::size_t i = 0; i < size; ++i) bytes[i] = static_cast(seed + i); + for (std::size_t i = 0; i < size; ++i) { + if (bytes[i] != static_cast(seed + i)) { + Check(false, "a block holds the bytes written to it"); + return; + } + } +} + +void EverySizeRoundTrips() { + for (int round = 0; round < 4; ++round) { + std::vector blocks; + for (std::size_t size = 1; size <= 2048; ++size) { + auto* block = new unsigned char[size]; +#ifdef _WIN32 + Check(_msize(block) >= size, "a block holds at least the bytes asked for"); +#endif + FillAndVerify(block, size, static_cast(size)); + blocks.push_back(block); + } + for (auto* block : blocks) delete[] block; + } +} + +void SizedDeletesKeepTheirClass() { + for (int round = 0; round < 1000; ++round) { + for (std::size_t size = 16; size <= 1024; size += 16) { + void* block = ::operator new(size); + std::memset(block, 0x5a, size); + ::operator delete(block, size); + } + } + for (std::size_t size = 1; size <= 1024; ++size) { + auto* block = static_cast(::operator new(size)); + FillAndVerify(block, size, 3); + ::operator delete(block, size); + } +} + +void BlocksCrossIntoLibstdcxx() { + for (int round = 0; round < 2000; ++round) { + std::string grown(static_cast(round % 300) + 20, 'a'); + grown.reserve(grown.size() * 3 + 1); + grown.append(static_cast(round % 500), 'b'); + std::istringstream input(std::string(static_cast(round % 700) + 1, 'c') + "\nnext"); + std::string line(40, 'd'); + std::getline(input, line); + Check(line.size() == static_cast(round % 700) + 1, "getline replaces the buffer made here"); + Check(grown.size() == static_cast(round % 300) + 20 + static_cast(round % 500), "a grown string keeps its bytes"); + } +} + +void BlocksMoveBetweenThreads() { + constexpr int perThread = 20000; + std::vector> made(perThread); + std::thread maker([&] { + for (int i = 0; i < perThread; ++i) { + made[i] = std::make_unique(static_cast(i % 64) + 1); + made[i][0] = static_cast(i); + } + }); + maker.join(); + std::thread freer([&] { + for (int i = 0; i < perThread; ++i) { + Check(made[i][0] == static_cast(i), "a block made on another thread keeps its bytes"); + made[i].reset(); + } + }); + freer.join(); +} + +std::atomic exitedThreads{0}; + +std::size_t PrivateBytes() { +#ifdef _WIN32 + PROCESS_MEMORY_COUNTERS_EX counters{}; + counters.cb = sizeof(counters); + if (!GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&counters), sizeof(counters))) return 0; + return counters.PrivateUsage; +#else + return 0; +#endif +} + +void ThreadsEndWithCachedBlocks() { + constexpr int waves = 24; + std::size_t afterEarlyWaves = 0; + for (int wave = 0; wave < waves; ++wave) { + std::vector threads; + for (int t = 0; t < 32; ++t) { + threads.emplace_back([] { + std::vector blocks; + for (int i = 0; i < 4000; ++i) blocks.push_back(::operator new(static_cast(i % 1024) + 1)); + for (auto* block : blocks) ::operator delete(block); + ++exitedThreads; + }); + } + for (auto& thread : threads) thread.join(); + if (wave == 7) afterEarlyWaves = PrivateBytes(); + } + Check(exitedThreads.load() == waves * 32, "every thread ran to its end"); + const auto afterLastWave = PrivateBytes(); + std::printf("private bytes after 8 and after %d waves of ending threads: %zu, %zu\n", waves, afterEarlyWaves, afterLastWave); + Check(afterLastWave < afterEarlyWaves + (128u << 20), "threads that end give their cached blocks back to the heap"); + auto* after = new std::uint64_t[128]; + FillAndVerify(reinterpret_cast(after), 128 * sizeof(std::uint64_t), 9); + delete[] after; +} + +} + +int main() { + EverySizeRoundTrips(); + SizedDeletesKeepTheirClass(); + BlocksCrossIntoLibstdcxx(); + BlocksMoveBetweenThreads(); + ThreadsEndWithCachedBlocks(); + if (failures != 0) { + std::fprintf(stderr, "%d host allocator checks failed\n", failures); + return 1; + } + std::printf("Host allocator tests passed\n"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/HostHdr.cpp b/core/libs/prx/libSceAgcDriver/tests/HostHdr.cpp new file mode 100644 index 0000000000..6ac8547f1c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostHdr.cpp @@ -0,0 +1,189 @@ +#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +int failures = 0; + +void Expect(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", what.c_str()); + ++failures; +} + +void resetHost() { + AgcDriverSetHostDisplayHdr_nid_postfix({}); + AgcDriverSetSurfaceHdr10_nid_postfix(false); + AgcDriverNoteReportedToneMapPeak_nid_postfix(0.0f); +} + +void dynamicRangeChoice() { + using AgcDriver::DynamicRangeSurfaceFormat; + constexpr std::uint64_t rgbPq = 0x8100070422000000ull; + constexpr std::uint64_t bgrPq = 0x8100070400000000ull; + const std::array hdr{{{VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}, {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_COLOR_SPACE_HDR10_ST2084_EXT}, {VK_FORMAT_A2R10G10B10_UNORM_PACK32, VK_COLOR_SPACE_HDR10_ST2084_EXT}}}; + const auto sdr = [](VkSurfaceFormatKHR format) { return format.format == VK_FORMAT_B8G8R8A8_UNORM && format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; }; + Expect(sdr(DynamicRangeSurfaceFormat(hdr, rgbPq, false)), "a PQ buffer without HDR output does not get the SDR swapchain"); + for (const auto pixelFormat : {0x8000000000000000ull, 0x8100000022000000ull, 0xc001000622000000ull}) Expect(sdr(DynamicRangeSurfaceFormat(hdr, pixelFormat, true)), "an SDR buffer with HDR output does not get the SDR swapchain"); + const auto red = DynamicRangeSurfaceFormat(hdr, rgbPq, true); + const auto blue = DynamicRangeSurfaceFormat(hdr, bgrPq, true); + Expect(red.format == VK_FORMAT_A2B10G10R10_UNORM_PACK32 && red.colorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT && blue.format == VK_FORMAT_A2R10G10B10_UNORM_PACK32 && blue.colorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT, "a PQ buffer with HDR output does not get the HDR10 swapchain in its own order"); + bool refused = false; + try { + static_cast(DynamicRangeSurfaceFormat(std::span(hdr).first(1), rgbPq, true)); + } catch (const std::exception&) { + refused = true; + } + Expect(refused, "HDR output without an HDR10 surface format is presented SDR silently"); + const std::uint32_t word = 600u | (500u << 10u) | (300u << 20u) | (3u << 30u); + const auto low = AgcDriver::PqClearColor(word, true); + const auto high = AgcDriver::PqClearColor(word, false); + Expect(low == std::array{600.0f / 1023.0f, 500.0f / 1023.0f, 300.0f / 1023.0f, 1.0f} && high == std::array{300.0f / 1023.0f, 500.0f / 1023.0f, 600.0f / 1023.0f, 1.0f}, "a PQ clear texel does not give its 10-bit codes in RGBA order"); +} + +void displayDescription() { + using AgcDriver::DisplayHdrFromOutput; + const auto hdr = DisplayHdrFromOutput(true, 1015.0f, 0.005f, 640.0f); + Expect(hdr.hdr && hdr.maxLuminance == 1015.0f && hdr.minLuminance == 0.005f && hdr.maxFullFrameLuminance == 640.0f, "an HDR10 output's luminance is not kept"); + Expect(!DisplayHdrFromOutput(false, 1015.0f, 0.005f, 640.0f).hdr, "an SDR output is described as HDR"); + for (const float peak : {0.0f, 0.5f, std::numeric_limits::quiet_NaN(), std::numeric_limits::infinity()}) { + const auto undescribed = DisplayHdrFromOutput(true, peak, 0.005f, 640.0f); + Expect(undescribed.hdr && undescribed.maxLuminance == 1000.0f && undescribed.minLuminance == 0.0f && undescribed.maxFullFrameLuminance == 1000.0f, "an HDR10 output without a usable peak luminance does not get the default luminance"); + } + const auto partial = DisplayHdrFromOutput(true, 600.0f, -1.0f, 0.0f); + Expect(partial.hdr && partial.minLuminance == 0.0f && partial.maxFullFrameLuminance == 600.0f, "an unusable black level or full-frame peak is kept"); + const auto inverted = DisplayHdrFromOutput(true, 400.0f, 500.0f, 900.0f); + Expect(inverted.hdr && inverted.minLuminance == 0.0f && inverted.maxFullFrameLuminance == 400.0f, "a black level or full-frame peak above the peak is kept"); +} + +void surfaceFormats() { + using AgcDriver::Hdr10SurfaceFormat; + const std::array sdr{{{VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}, {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}}}; + Expect(!Hdr10SurfaceFormat(sdr, VK_FORMAT_A2B10G10R10_UNORM_PACK32), "an SDR surface offers HDR10"); + const std::array floatOnly{{{VK_FORMAT_R16G16B16A16_SFLOAT, VK_COLOR_SPACE_HDR10_ST2084_EXT}}}; + Expect(!Hdr10SurfaceFormat(floatOnly, VK_FORMAT_UNDEFINED), "a float HDR10 surface offers a 10-bit HDR10 swapchain"); + const std::array both{{{VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}, {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_COLOR_SPACE_HDR10_ST2084_EXT}, {VK_FORMAT_A2R10G10B10_UNORM_PACK32, VK_COLOR_SPACE_HDR10_ST2084_EXT}}}; + for (const auto preferred : {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}) { + const auto chosen = Hdr10SurfaceFormat(both, preferred); + Expect(chosen && chosen->format == preferred && chosen->colorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT, "the display buffer's own 10-bit order is not preferred"); + } + const auto first = Hdr10SurfaceFormat(both, VK_FORMAT_B8G8R8A8_UNORM); + Expect(first && first->format == VK_FORMAT_A2B10G10R10_UNORM_PACK32, "a preferred format that is not 10-bit HDR10 is chosen"); + const std::array blueLow{{{VK_FORMAT_A2R10G10B10_UNORM_PACK32, VK_COLOR_SPACE_HDR10_ST2084_EXT}}}; + const auto only = Hdr10SurfaceFormat(blueLow, VK_FORMAT_A2B10G10R10_UNORM_PACK32); + Expect(only && only->format == VK_FORMAT_A2R10G10B10_UNORM_PACK32, "the only 10-bit HDR10 format is not chosen"); +} + +void whiteLevels() { + AgcDriver::HostHdrStatus status{}; + Expect(AgcDriver::ToneMapPeakNits(status) == 100.0f && AgcDriver::PqToSdrWhiteNits(status) == 100.0f, "an SDR host does not use SDR reference white"); + status.available = true; + status.display = AgcDriver::DisplayHdrFromOutput(true, 800.0f, 0.01f, 400.0f); + Expect(AgcDriver::ToneMapPeakNits(status) == 800.0f && AgcDriver::PqToSdrWhiteNits(status) == 800.0f, "an HDR host that was never asked does not use the display's peak"); + status.available = false; + status.reportedToneMapPeak = 800.0f; + Expect(AgcDriver::ToneMapPeakNits(status) == 100.0f && AgcDriver::PqToSdrWhiteNits(status) == 800.0f, "the peak the title was told is not the white level after HDR went away"); +} + +void hostState() { + resetHost(); + const auto initial = AgcDriver::CurrentHostHdr(); + Expect(!initial.available && !initial.display.hdr && !initial.surfaceHdr10 && initial.reportedToneMapPeak == 0.0f, "HDR output is available after the host was reset"); + AgcDriverSetSurfaceHdr10_nid_postfix(true); + auto status = AgcDriver::CurrentHostHdr(); + Expect(!status.available && status.generation == initial.generation, "a surface offering HDR10 makes HDR output available on an SDR display"); + AgcDriverSetHostDisplayHdr_nid_postfix(AgcDriver::DisplayHdrFromOutput(true, 800.0f, 0.01f, 400.0f)); + status = AgcDriver::CurrentHostHdr(); + Expect(status.available && status.generation != initial.generation && status.displayGeneration != initial.displayGeneration, "an HDR10 display and surface do not make HDR output available"); + Expect(status.display.maxLuminance == 800.0f && status.display.minLuminance == 0.01f && status.display.maxFullFrameLuminance == 400.0f, "the display's luminance is not published"); + const auto available = status.generation; + AgcDriverSetHostDisplayHdr_nid_postfix(AgcDriver::DisplayHdrFromOutput(true, 800.0f, 0.01f, 400.0f)); + Expect(AgcDriver::CurrentHostHdr().generation == available, "an unchanged display report changes the generation"); + AgcDriverSetHostDisplayHdr_nid_postfix(AgcDriver::DisplayHdrFromOutput(true, 1000.0f, 0.01f, 400.0f)); + status = AgcDriver::CurrentHostHdr(); + Expect(status.available && status.generation != available && status.display.maxLuminance == 1000.0f, "a new peak luminance is not announced"); + const auto moved = status.generation; + AgcDriverSetSurfaceHdr10_nid_postfix(false); + status = AgcDriver::CurrentHostHdr(); + Expect(!status.available && status.generation != moved, "a surface without HDR10 leaves HDR output available"); + const auto lost = status.generation; + AgcDriverSetHostDisplayHdr_nid_postfix(AgcDriver::DisplayHdrFromOutput(true, 600.0f, 0.01f, 400.0f)); + status = AgcDriver::CurrentHostHdr(); + Expect(!status.available && status.generation == lost && status.displayGeneration != initial.displayGeneration, "a display change without HDR output is announced as an output change"); + AgcDriverSetSurfaceHdr10_nid_postfix(true); + AgcDriverSetHostDisplayHdr_nid_postfix({}); + Expect(!AgcDriver::CurrentHostHdr().available, "a display switched to SDR leaves HDR output available"); +} + +void forceSdr() { + resetHost(); +#ifdef _WIN32 + _putenv_s("APS5_FORCE_SDR", "1"); +#else + setenv("APS5_FORCE_SDR", "1", 1); +#endif + AgcDriverSetSurfaceHdr10_nid_postfix(true); + AgcDriverSetHostDisplayHdr_nid_postfix(AgcDriver::DisplayHdrFromOutput(true, 800.0f, 0.01f, 400.0f)); + const auto forced = AgcDriver::CurrentHostHdr(); + Expect(!forced.available && forced.display.hdr && forced.surfaceHdr10, "forcing SDR does not suppress HDR availability while preserving host capabilities"); + Expect(AgcDriver::ToneMapPeakNits(forced) == 100.0f, "forcing SDR does not report SDR reference white"); +#ifdef _WIN32 + _putenv_s("APS5_FORCE_SDR", ""); +#else + unsetenv("APS5_FORCE_SDR"); +#endif + AgcDriverSetSurfaceHdr10_nid_postfix(true); + const auto restored = AgcDriver::CurrentHostHdr(); + Expect(restored.available && restored.generation != forced.generation, "removing the SDR override does not restore HDR availability"); + resetHost(); +} + +void reportedWhiteLevel() { + resetHost(); + constexpr std::uint64_t rgbPq = 0x8100070422000000ull; + const std::uint32_t word = 700u | (700u << 10u) | (700u << 20u) | (3u << 30u); + std::array source{}; + std::memcpy(source.data(), &word, 4); + const AgcDriver::DisplayBuffer linear{65536, rgbPq, 1, 1, 1, 0}; + const auto sdr = AgcDriver::DecodeDisplayBuffer(linear, source); + Expect(std::to_integer(sdr[0]) == 255, "a 537 nit grey is not white for a title told 100 nits"); + AgcDriverNoteReportedToneMapPeak_nid_postfix(1000.0f); + const auto told = AgcDriver::DecodeDisplayBuffer(linear, source); + const auto expected = AgcDriver::PqToSdrPixel(word, true, 1000.0f); + Expect(std::memcmp(told.data(), expected.data(), 4) == 0 && std::to_integer(told[0]) == 252, "a linear PQ display buffer is not converted for the peak the title was told"); + const AgcDriver::DisplayBuffer cleared{65536, rgbPq, 128, 128, 0, 0, 0x20000, word}; + const auto clear = AgcDriver::DisplayBufferClearPixel(cleared, AgcDriver::Graphics::DccKeys::ClearRegister); + Expect(std::memcmp(clear.data(), expected.data(), 4) == 0, "a PQ DCC clear value is not converted for the peak the title was told"); + const auto raw = AgcDriver::DisplayBufferClearTexel(cleared, AgcDriver::Graphics::DccKeys::ClearRegister); + Expect(std::memcmp(raw.data(), &word, 4) == 0, "the raw PQ clear texel is not kept for the HDR10 swapchain"); + std::array texels{}; + for (std::size_t i = 0; i < texels.size(); ++i) texels[i] = static_cast(i * 37u + 5u); + const auto rows = AgcDriver::DisplayBufferTexels({65536, rgbPq, 1, 2, 1, 2}, texels); + Expect(rows.size() == 8 && std::memcmp(rows.data(), texels.data(), 4) == 0 && std::memcmp(rows.data() + 4, texels.data() + 8, 4) == 0, "raw linear PQ texels are not read row by row unchanged"); +} + +} + +int main() { + dynamicRangeChoice(); + displayDescription(); + surfaceFormats(); + whiteLevels(); + hostState(); + reportedWhiteLevel(); + forceSdr(); + if (failures != 0) return 1; + std::puts("host HDR tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.cpp new file mode 100644 index 0000000000..939c31b830 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.cpp @@ -0,0 +1,176 @@ +#include "HostImportFailure.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#ifdef _WIN32 +#include +#endif +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +enum class Step { None, Create, Allocate, Bind }; + +struct Probe { + PFN_vkGetDeviceProcAddr backend = nullptr; + Step fail = Step::None; + VkResult result = VK_SUCCESS; + unsigned injected = 0; + unsigned invalid = 0; + std::set buffers; + std::set memories; +}; + +Probe probe; + +VKAPI_ATTR VkResult VKAPI_CALL CreateBuffer(VkDevice device, const VkBufferCreateInfo* info, const VkAllocationCallbacks* allocator, VkBuffer* buffer) { + if (probe.fail == Step::Create) { + probe.fail = Step::None; + ++probe.injected; + *buffer = reinterpret_cast(std::uintptr_t{0xdeadbeef}); + return probe.result; + } + const auto result = reinterpret_cast(probe.backend(device, "vkCreateBuffer"))(device, info, allocator, buffer); + if (result == VK_SUCCESS) probe.buffers.insert(*buffer); + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL AllocateMemory(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) { + if (probe.fail == Step::Allocate) { + probe.fail = Step::None; + ++probe.injected; + *memory = reinterpret_cast(std::uintptr_t{0xdeadbeef}); + return probe.result; + } + const auto result = reinterpret_cast(probe.backend(device, "vkAllocateMemory"))(device, info, allocator, memory); + if (result == VK_SUCCESS) probe.memories.insert(*memory); + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL BindBufferMemory(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) { + if (probe.fail == Step::Bind) { + probe.fail = Step::None; + ++probe.injected; + return probe.result; + } + return reinterpret_cast(probe.backend(device, "vkBindBufferMemory"))(device, buffer, memory, offset); +} + +VKAPI_ATTR void VKAPI_CALL DestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) { + if (probe.buffers.erase(buffer) != 1) { + ++probe.invalid; + return; + } + reinterpret_cast(probe.backend(device, "vkDestroyBuffer"))(device, buffer, allocator); +} + +VKAPI_ATTR void VKAPI_CALL FreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* allocator) { + if (probe.memories.erase(memory) != 1) { + ++probe.invalid; + return; + } + reinterpret_cast(probe.backend(device, "vkFreeMemory"))(device, memory, allocator); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL DeviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateBuffer") == 0) return reinterpret_cast(CreateBuffer); + if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast(AllocateMemory); + if (std::strcmp(name, "vkBindBufferMemory") == 0) return reinterpret_cast(BindBufferMemory); + if (std::strcmp(name, "vkDestroyBuffer") == 0) return reinterpret_cast(DestroyBuffer); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast(FreeMemory); + return probe.backend(device, name); +} + +constexpr std::size_t Alignment = 65536; +constexpr std::size_t Bytes = 4096; + +class Heap { +public: + Heap() { +#ifdef _WIN32 + data = static_cast(VirtualAlloc(nullptr, Alignment, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + data = static_cast(std::aligned_alloc(Alignment, Alignment)); +#endif + Require(data != nullptr, "cannot allocate the host import failure heap"); + std::memset(data, 0x37, Alignment); + GuestAllocations::Mutation().Add(data, Alignment, true, true, true); + } + + ~Heap() { + GuestAllocations::Mutation().Remove(data); +#ifdef _WIN32 + VirtualFree(data, 0, MEM_RELEASE); +#else + std::free(data); +#endif + } + + std::byte* Data() const { return data; } + std::uint64_t Address() const { return reinterpret_cast(data); } + +private: + std::byte* data = nullptr; +}; + +} + +void AgcDriver::Tests::RunHostImportFailureTests(const Graphics::Context& original, bool native) { + unsigned failures = 0; + for (const auto step : {Step::Create, Step::Allocate, Step::Bind}) { + for (const auto result : {VK_ERROR_OUT_OF_HOST_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}) { + Heap heap; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + ClearHostImports(original.device); + probe = {}; + probe.backend = original.deviceProc; + probe.fail = step; + probe.result = result; + auto context = original; + context.deviceProc = DeviceProc; + struct Cleanup { + VkDevice device; + ~Cleanup() { ClearHostImports(device); } + } cleanup{context.device}; + SetImportWatch(context, ImportWatch::Unwatch); + const HostImport* failed = nullptr; + bool threw = false; + try { + failed = HostImportFor(context, heap.Address(), Bytes); + } catch (const std::runtime_error&) { + threw = true; + } + const bool rejected = failed == nullptr && probe.injected == 1 && ExistingHostImportFor(context, heap.Address(), Bytes) == nullptr && HostImportLiveBytes() == 0; + const bool released = probe.buffers.empty() && probe.memories.empty(); + const auto invalid = probe.invalid; + ClearHostImports(context.device); + probe.fail = Step::None; + const auto* recovered = HostImportFor(context, heap.Address(), Bytes); + Require(recovered != nullptr && recovered->buffer != VK_NULL_HANDLE && recovered->memory != VK_NULL_HANDLE && recovered->address != 0, "host import retry failed"); + if (native) { + Buffer readback(original, Bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::memset(readback.Bytes().data(), 0, Bytes); + CommandBatch batch(original); + RecordMemoryBarrier(original, batch.Handle(), VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(original, batch.Handle(), recovered->buffer, heap.Address() - recovered->base, readback.Handle(), 0, Bytes); + RecordMemoryBarrier(original, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + readback.Invalidate(); + Require(std::memcmp(heap.Data(), readback.Bytes().data(), Bytes) == 0, "host import retry returned incorrect GPU readback data"); + } + ClearHostImports(context.device); + const bool clean = probe.buffers.empty() && probe.memories.empty() && HostImportLiveBytes() == 0; + std::printf("host import step=%u result=%d injected=%u rejected=%u threw=%u invalid releases=%u failure cleaned=%u retry cleaned=%u readbacks=%u\n", static_cast(step), static_cast(result), probe.injected, rejected, threw, invalid, released, clean, native ? 1u : 0u); + if (!rejected || !released || !clean || invalid != 0 || probe.invalid != invalid) ++failures; + } + } + Require(failures == 0, "failed host import creation released an undefined handle or leaked a resource"); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.hpp b/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.hpp new file mode 100644 index 0000000000..70364786ad --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.hpp @@ -0,0 +1,12 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_HOSTIMPORTFAILURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_HOSTIMPORTFAILURE_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" + +namespace AgcDriver::Tests { + +void RunHostImportFailureTests(const Graphics::Context& context, bool native); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp new file mode 100644 index 0000000000..603d4bb0a2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp @@ -0,0 +1,807 @@ +#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) { + constexpr std::size_t PartUnits = 16; + Allocation allocation(fixture, 3 * PartUnits); + Allocation unrelated(fixture); + const auto part = PartUnits * allocation.alignment; + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + const auto other = fixture.Import(unrelated.Address(), unrelated.bytes); + auto* middle = static_cast(allocation.data) + part; + { + GuestAllocations::Mutation mutation; + mutation.Unmap(middle, part, [&](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 == part && original == allocation.data && !last, "partial unmap changed the wrong guest extent"); + }); + } + Require(!RegisteredReadableCovers(reinterpret_cast(middle), part), "the middle of a partial unmap remains registered"); + const auto low = fixture.Import(allocation.Address(), part); + const auto high = fixture.Import(allocation.Address() + 2 * part, part); + Require(imported->freed.load(), "the side kept from an unmap inside the host import reused memory imported across the unmapped range"); + Require(high->address == allocation.Address() + 2 * part && high->bytes == part, "the kept side of the host import does not cover its remaining range"); + 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..8d0d9cddc7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp @@ -1,3 +1,4 @@ +#include "HostImportFailure.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -247,6 +248,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,8 +370,10 @@ int main() { setenv("APS5_WRITE_WATCH_IMPORTS", "unwatch", 1); #endif try { + WindowPromotionOutlivesTheHold(); IdleRetireOutlivesTheHold(); BusyRetireOutlivesALaterBatch(); + AgcDriver::Tests::RunHostImportFailureTests(mockContext(), false); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp index a6d05d4dc7..6f127df513 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,41 @@ 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; + VkResult allocationResult = VK_SUCCESS; + std::uint64_t allocationAttempts = 0; 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,12 +114,15 @@ 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; } VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* memory) { + ++mock.allocationAttempts; + if (mock.allocationResult != VK_SUCCESS) return mock.allocationResult; *memory = reinterpret_cast(mock.next++); mock.memories[*memory] = info->allocationSize; return VK_SUCCESS; @@ -132,6 +155,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)}, @@ -214,6 +244,10 @@ struct SharedRange { mutation.Add(backing.base, static_cast(bytes), true, true, true); } + ~SharedRange() { + std::erase(backings, &backing); + } + std::uint64_t Address() const { return reinterpret_cast(backing.base); } @@ -223,6 +257,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); @@ -247,18 +311,67 @@ 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"); } +void RetainsFailedWindowAcrossUnrelatedMutation() { + constexpr std::uint64_t window = 4ull << 20u; + SharedRange range(window * 2, 0, true); + const auto context = mockContext(); + SetHostImportRangeWindow(window); + mock.allocationResult = VK_ERROR_INVALID_EXTERNAL_HANDLE; + const auto address = range.Address() + window; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto attempts = mock.allocationAttempts; + Expect(HostImportFor(context, address, Alignment) == nullptr, "the failed import window was accepted"); + const auto failedAttempts = mock.allocationAttempts; + Expect(failedAttempts > attempts, "the import window did not reach allocation"); + Expect(HostImportFor(context, address, Alignment) == nullptr && mock.allocationAttempts == failedAttempts, "an unchanged failed window was retried"); + SharedRange unrelated(Alignment, 0, true); + Expect(HostImportFor(context, address, Alignment) == nullptr && mock.allocationAttempts == failedAttempts, "an unrelated registry change retried a failed import window"); + Expect(HostImportFor(context, range.Address(), Alignment) == nullptr && mock.allocationAttempts > failedAttempts, "a different import window inherited the first window's failure"); + const auto beforeRemap = mock.allocationAttempts; + { + GuestAllocations::Mutation mutation; + mutation.Remove(range.backing.base); + mutation.Add(range.backing.base, static_cast(range.backing.bytes), true, true, true); + } + mock.allocationResult = VK_SUCCESS; + const auto* imported = HostImportFor(context, address, Alignment); + Expect(imported != nullptr && imported->base == address && imported->bytes == window && mock.allocationAttempts > beforeRemap, "a remapped failed import window was not retried successfully"); + SetHostImportRangeWindow(64ull << 20u); +} + void FailureAfterTheDecisionThrows(bool sealed, VkResult bind, const char* step) { const auto bytes = RangeBytes(); SharedRange range(bytes, 0, sealed); @@ -286,13 +399,22 @@ 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"); + RetainsFailedWindowAcrossUnrelatedMutation(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/IntegerFolding.cpp b/core/libs/prx/libSceAgcDriver/tests/IntegerFolding.cpp new file mode 100644 index 0000000000..8b29e2246e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/IntegerFolding.cpp @@ -0,0 +1,439 @@ +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/ConstantFolder.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +struct Builder { + IrProgram program; + IrBlock* block = nullptr; + std::vector kept; + + Builder() { + block = &NewBlock(); + program.SetEntryBlock(*block); + } + + IrBlock& NewBlock() { + auto& created = program.CreateBlock(); + program.BlockOrder().push_back(&created); + return created; + } + + IrValue& Constant(std::uint32_t value) { + auto& created = program.CreateValue(IrOpcode::Void, IrType::U32); + created.SetImmediateU32(value); + return created; + } + + IrValue& Emit(IrOpcode opcode, IrType type, std::initializer_list arguments) { + auto& created = program.CreateValue(opcode, type); + for (auto* argument : arguments) created.AddArgument(argument); + block->AppendInstruction(&created); + return created; + } + + IrValue& Unknown() { + return Emit(IrOpcode::LaneId, IrType::U32, {}); + } + + IrValue& Binary(IrOpcode opcode, IrValue& lhs, IrValue& rhs) { + return Emit(opcode, IrType::U32, {&lhs, &rhs}); + } + + IrValue& Binary(IrOpcode opcode, IrValue& lhs, std::uint32_t rhs) { + return Binary(opcode, lhs, Constant(rhs)); + } + + IrValue& Shifted(std::uint32_t amount) { + return Binary(IrOpcode::ShiftLeftLogical32, Unknown(), amount); + } + + std::size_t Compare(IrOpcode opcode, IrValue& value, std::uint32_t immediate) { + auto& test = Emit(opcode, IrType::Bool, {&value, &Constant(immediate)}); + auto& user = Emit(IrOpcode::Reference, IrType::Void, {&test}); + kept.push_back(&user); + return kept.size() - 1u; + } + + std::size_t Equal(IrValue& value, std::uint32_t immediate) { + return Compare(IrOpcode::IEqual32, value, immediate); + } + + std::size_t NotEqual(IrValue& value, std::uint32_t immediate) { + return Compare(IrOpcode::INotEqual32, value, immediate); + } + + std::size_t Less(IrValue& value, std::uint32_t immediate) { + return Compare(IrOpcode::ULessThan32, value, immediate); + } + + void Fold() { + ConstantFolder().Fold(program); + } + + std::optional Result(std::size_t index) const { + const IrValue* value = kept.at(index)->Argument(0)->Resolve(); + if (!value->HasImmediate()) return std::nullopt; + return value->ImmediateBool(); + } + + bool Folds(std::size_t index, bool expected) const { + return Result(index) == std::optional(expected); + } + + bool Keeps(std::size_t index) const { + return !Result(index).has_value(); + } +}; + +bool expect(const char* name, bool condition) { + if (!condition) std::fprintf(stderr, "%s\n", name); + return condition; +} + +bool run(const char* name, const std::function& test) { + try { + return expect(name, test()); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s: %s\n", name, error.what()); + return false; + } +} + +} + +int main() { + bool passed = true; + + passed &= run("a compare against a constant that contradicts the zero bits of a left shift folds", [] { + Builder b; + auto& value = b.Shifted(2u); + const auto odd = b.Equal(value, 5u); + const auto oddNot = b.NotEqual(value, 6u); + const auto aligned = b.Equal(value, 4u); + const auto alignedNot = b.NotEqual(value, 8u); + b.Fold(); + return b.Folds(odd, false) && b.Folds(oddNot, true) && b.Keeps(aligned) && b.Keeps(alignedNot); + }); + + passed &= run("a compare against an unknown value or a variable shift is kept", [] { + Builder b; + auto& lane = b.Unknown(); + auto& variable = b.Binary(IrOpcode::ShiftLeftLogical32, b.Unknown(), lane); + const auto plain = b.Equal(lane, 5u); + const auto byLane = b.Equal(variable, 1u); + b.Fold(); + return b.Keeps(plain) && b.Keeps(byLane); + }); + + passed &= run("known bits pass through right shifts, bitfield extracts, and, or and xor", [] { + Builder b; + auto& high = b.Binary(IrOpcode::ShiftRightLogical32, b.Unknown(), 4u); + auto& field = b.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&b.Shifted(4u), &b.Constant(2u), &b.Constant(4u)}); + auto& masked = b.Binary(IrOpcode::BitwiseAnd32, b.Unknown(), 0xfffffff0u); + auto& merged = b.Binary(IrOpcode::BitwiseOr32, b.Shifted(4u), 3u); + auto& flipped = b.Binary(IrOpcode::BitwiseXor32, merged, 1u); + const auto highTop = b.Equal(high, 0x10000000u); + const auto highFits = b.Equal(high, 0x0fffffffu); + const auto fieldLow = b.Equal(field, 1u); + const auto fieldTop = b.Equal(field, 0x10u); + const auto fieldFits = b.Equal(field, 4u); + const auto maskedLow = b.Equal(masked, 0x21u); + const auto mergedLow = b.Equal(merged, 0x10u); + const auto mergedFits = b.Equal(merged, 0x13u); + const auto flippedLow = b.Equal(flipped, 0x13u); + const auto flippedFits = b.NotEqual(flipped, 0x12u); + b.Fold(); + return b.Folds(highTop, false) && b.Keeps(highFits) && b.Folds(fieldLow, false) && b.Folds(fieldTop, false) && b.Keeps(fieldFits) && b.Folds(maskedLow, false) && b.Folds(mergedLow, false) && b.Keeps(mergedFits) && b.Folds(flippedLow, false) && b.Keeps(flippedFits); + }); + + passed &= run("known bits of a sum stop at the first unknown bit and include the carries below it", [] { + Builder b; + auto& offset = b.Binary(IrOpcode::IAdd32, b.Shifted(2u), 1u); + auto& carried = b.Binary(IrOpcode::IAdd32, b.Binary(IrOpcode::BitwiseOr32, b.Shifted(2u), 3u), 1u); + auto& partial = b.Binary(IrOpcode::IAdd32, b.Shifted(2u), b.Unknown()); + const auto offsetWrong = b.Equal(offset, 6u); + const auto offsetFits = b.Equal(offset, 5u); + const auto carriedWrong = b.Equal(carried, 1u); + const auto carriedFits = b.Equal(carried, 4u); + const auto carriedHigh = b.Equal(carried, 8u); + const auto partialAny = b.Equal(partial, 1u); + b.Fold(); + return b.Folds(offsetWrong, false) && b.Keeps(offsetFits) && b.Folds(carriedWrong, false) && b.Keeps(carriedFits) && b.Keeps(carriedHigh) && b.Keeps(partialAny); + }); + + passed &= run("a product has the trailing zeros of both factors", [] { + Builder b; + auto& product = b.Binary(IrOpcode::IMul32, b.Shifted(1u), b.Shifted(2u)); + auto& single = b.Binary(IrOpcode::IMul32, b.Shifted(1u), b.Unknown()); + const auto productLow = b.Equal(product, 4u); + const auto productFits = b.Equal(product, 8u); + const auto singleLow = b.Equal(single, 1u); + const auto singleFits = b.Equal(single, 2u); + b.Fold(); + return b.Folds(productLow, false) && b.Keeps(productFits) && b.Folds(singleLow, false) && b.Keeps(singleFits); + }); + + passed &= run("an unsigned minimum keeps the common bits and the width of the smaller bound", [] { + Builder b; + auto& both = b.Binary(IrOpcode::UMin32, b.Shifted(2u), b.Shifted(3u)); + auto& bounded = b.Binary(IrOpcode::UMin32, b.Binary(IrOpcode::BitwiseAnd32, b.Unknown(), 0xffu), b.Shifted(4u)); + const auto bothLow = b.Equal(both, 2u); + const auto bothFits = b.Equal(both, 4u); + const auto boundedHigh = b.Equal(bounded, 0x100u); + const auto boundedFits = b.Equal(bounded, 0xf0u); + b.Fold(); + return b.Folds(bothLow, false) && b.Keeps(bothFits) && b.Folds(boundedHigh, false) && b.Keeps(boundedFits); + }); + + passed &= run("a select keeps the bits common to both arms", [] { + Builder b; + auto& condition = b.Emit(IrOpcode::ULessThan32, IrType::Bool, {&b.Unknown(), &b.Constant(16u)}); + auto& choice = b.Emit(IrOpcode::SelectU32, IrType::U32, {&condition, &b.Shifted(2u), &b.Binary(IrOpcode::BitwiseOr32, b.Shifted(3u), 4u)}); + const auto choiceLow = b.Equal(choice, 2u); + const auto choiceArm = b.Equal(choice, 4u); + b.Fold(); + return b.Folds(choiceLow, false) && b.Keeps(choiceArm); + }); + + passed &= run("a phi keeps the bits common to its incoming values and ignores itself", [] { + Builder b; + IrBlock& entry = *b.block; + IrBlock& other = b.NewBlock(); + IrBlock& join = b.NewBlock(); + entry.AddBranch(&other); + entry.AddBranch(&join); + other.AddBranch(&join); + join.AddBranch(&join); + auto& first = b.Shifted(2u); + b.block = &other; + auto& second = b.Binary(IrOpcode::BitwiseOr32, b.Shifted(3u), 4u); + b.block = &join; + auto& merged = b.program.CreateValue(IrOpcode::Phi, IrType::U32); + join.AppendInstruction(&merged); + merged.AddPhiOperand(&entry, &first); + merged.AddPhiOperand(&other, &second); + merged.AddPhiOperand(&join, &merged); + const auto mergedLow = b.Equal(merged, 1u); + const auto mergedFits = b.Equal(merged, 8u); + b.Fold(); + return b.Folds(mergedLow, false) && b.Keeps(mergedFits); + }); + + passed &= run("a loop counter phi keeps every compare its values can satisfy", [] { + Builder b; + IrBlock& entry = *b.block; + IrBlock& loop = b.NewBlock(); + entry.AddBranch(&loop); + loop.AddBranch(&loop); + b.block = &loop; + auto& counter = b.program.CreateValue(IrOpcode::Phi, IrType::U32); + loop.AppendInstruction(&counter); + auto& next = b.Binary(IrOpcode::IAdd32, counter, 1u); + counter.AddPhiOperand(&entry, &b.Constant(0u)); + counter.AddPhiOperand(&loop, &next); + const auto counterSix = b.Equal(counter, 6u); + const auto counterBelow = b.Less(counter, 1u); + b.Fold(); + return b.Keeps(counterSix) && b.Keeps(counterBelow); + }); + + passed &= run("a lane read has the known bits of its source", [] { + Builder b; + auto& first = b.Emit(IrOpcode::ReadFirstLane, IrType::U32, {&b.Shifted(2u), &b.Emit(IrOpcode::IEqual32, IrType::Bool, {&b.Unknown(), &b.Constant(0u)})}); + auto& lane = b.Emit(IrOpcode::ReadLane, IrType::U32, {&b.Binary(IrOpcode::IAdd32, b.Shifted(2u), 3u), &b.Unknown()}); + auto& offset = b.Binary(IrOpcode::IAdd32, first, 1u); + const auto firstLow = b.Equal(first, 3u); + const auto offsetLow = b.NotEqual(offset, 4u); + const auto offsetFits = b.Equal(offset, 5u); + const auto laneLow = b.Equal(lane, 4u); + const auto laneFits = b.Equal(lane, 7u); + b.Fold(); + return b.Folds(firstLow, false) && b.Folds(offsetLow, true) && b.Keeps(offsetFits) && b.Folds(laneLow, false) && b.Keeps(laneFits); + }); + + passed &= run("a small value set folds compares against values outside it", [] { + Builder b; + auto& scaled = b.Binary(IrOpcode::IMul32, b.Binary(IrOpcode::UMin32, b.Unknown(), 3u), 5u); + auto& field = b.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&b.Unknown(), &b.Constant(8u), &b.Constant(2u)}); + auto& sum = b.Binary(IrOpcode::IAdd32, field, scaled); + auto& single = b.Binary(IrOpcode::BitwiseAnd32, b.Binary(IrOpcode::UMin32, b.Unknown(), 0u), 7u); + const auto scaledOutside = b.Equal(scaled, 7u); + const auto scaledInside = b.NotEqual(scaled, 10u); + const auto sumOutside = b.Equal(sum, 19u); + const auto sumInside = b.Equal(sum, 18u); + const auto singleInside = b.Equal(single, 0u); + const auto singleOutside = b.NotEqual(single, 1u); + b.Fold(); + return b.Folds(scaledOutside, false) && b.Keeps(scaledInside) && b.Folds(sumOutside, false) && b.Keeps(sumInside) && b.Folds(singleInside, true) && b.Folds(singleOutside, true); + }); + + passed &= run("a sum of two 64-value sets is not enumerated and keeps its compares", [] { + Builder b; + auto& sum = b.Binary(IrOpcode::IAdd32, b.Binary(IrOpcode::UMin32, b.Unknown(), 63u), b.Binary(IrOpcode::UMin32, b.Unknown(), 63u)); + std::vector compares; + for (std::uint32_t entry = 1u; entry <= 100u; ++entry) compares.push_back(b.Equal(sum, entry)); + b.Fold(); + bool kept = true; + for (const auto compare : compares) kept &= b.Keeps(compare); + return kept; + }); + + passed &= run("the value set of x & mask leaves out the known bits of x", [] { + Builder b; + auto& masked = b.Binary(IrOpcode::BitwiseAnd32, b.Shifted(4u), 0x3ffu); + auto& scaled = b.Binary(IrOpcode::IMul32, masked, 3u); + auto& fixed = b.Binary(IrOpcode::IMul32, b.Binary(IrOpcode::BitwiseAnd32, b.Binary(IrOpcode::BitwiseOr32, b.Shifted(3u), 5u), 0x1fu), 3u); + auto& wide = b.Binary(IrOpcode::IMul32, b.Binary(IrOpcode::BitwiseAnd32, b.Shifted(2u), 0x3ffu), 3u); + const auto scaledOutside = b.Equal(scaled, 32u); + const auto scaledInside = b.Equal(scaled, 48u); + const auto fixedOutside = b.Equal(fixed, 27u); + const auto fixedInside = b.Equal(fixed, 39u); + const auto wideUnknown = b.Equal(wide, 4u); + b.Fold(); + return b.Folds(scaledOutside, false) && b.Keeps(scaledInside) && b.Folds(fixedOutside, false) && b.Keeps(fixedInside) && b.Keeps(wideUnknown); + }); + + passed &= run("wrapping sums and products keep compares that their values straddle", [] { + Builder b; + auto& wrapped = b.Binary(IrOpcode::IAdd32, b.Binary(IrOpcode::BitwiseAnd32, b.Unknown(), 1u), 0xffffffffu); + auto& product = b.Binary(IrOpcode::IMul32, b.Binary(IrOpcode::BitwiseAnd32, b.Unknown(), 1u), 0x80000000u); + auto& scaled = b.Binary(IrOpcode::IMul32, b.Binary(IrOpcode::BitwiseAnd32, b.Unknown(), 3u), 0x80000001u); + const auto wrappedZero = b.Equal(wrapped, 0u); + const auto wrappedBelow = b.Less(wrapped, 1u); + const auto wrappedMax = b.Equal(wrapped, 0xffffffffu); + const auto wrappedOutside = b.Equal(wrapped, 1u); + const auto productZero = b.Equal(product, 0u); + const auto productBelow = b.Less(product, 1u); + const auto productTop = b.Equal(product, 0x80000000u); + const auto productOutside = b.Equal(product, 1u); + const auto scaledWrapped = b.Equal(scaled, 2u); + const auto scaledBelow = b.Less(scaled, 0x80000001u); + const auto scaledOutside = b.Equal(scaled, 0x80000002u); + b.Fold(); + return b.Keeps(wrappedZero) && b.Keeps(wrappedBelow) && b.Keeps(wrappedMax) && b.Folds(wrappedOutside, false) && b.Keeps(productZero) && b.Keeps(productBelow) && b.Keeps(productTop) && b.Folds(productOutside, false) && b.Keeps(scaledWrapped) && b.Keeps(scaledBelow) && b.Folds(scaledOutside, false); + }); + + passed &= run("a chain of wide phi joins exhausts the analysis budget, folds nothing and never folds a satisfiable compare", [] { + Builder b; + std::vector predecessors; + for (std::uint32_t edge = 0u; edge < 100u; ++edge) predecessors.push_back(&b.NewBlock()); + IrValue* previous = nullptr; + for (std::uint32_t level = 0u; level < 5u; ++level) { + auto& join = b.NewBlock(); + auto& phi = b.program.CreateValue(IrOpcode::Phi, IrType::U32); + join.AppendInstruction(&phi); + for (std::uint32_t edge = 0u; edge < 100u; ++edge) { + IrValue* incoming = &b.Constant(3u); + if (previous == nullptr && edge == 0u) incoming = &b.Constant(1u); + if (previous != nullptr && edge != 99u) incoming = previous; + phi.AddPhiOperand(predecessors[edge], incoming); + } + previous = φ + } + auto& masked = b.Binary(IrOpcode::BitwiseAnd32, *previous, 0xffu); + const auto isOne = b.Equal(masked, 1u); + const auto isThree = b.Equal(masked, 3u); + const auto isTwo = b.Equal(masked, 2u); + b.Fold(); + return b.Keeps(isOne) && b.Keeps(isThree) && b.Keeps(isTwo); + }); + + passed &= run("a phi charges every incoming edge to the analysis budget, so only a phi within the budget folds", [] { + Builder b; + const auto oddCompare = [&](std::uint32_t edges) { + auto& join = b.NewBlock(); + auto& phi = b.program.CreateValue(IrOpcode::Phi, IrType::U32); + join.AppendInstruction(&phi); + for (std::uint32_t edge = 0u; edge < edges; ++edge) phi.AddPhiOperand(&b.NewBlock(), &b.Constant(2u * (edge + 1u))); + return b.Equal(b.Binary(IrOpcode::BitwiseAnd32, phi, 1u), 1u); + }; + const auto narrow = oddCompare(100u); + const auto wide = oddCompare(3000u); + b.Fold(); + return b.Folds(narrow, false) && b.Keeps(wide); + }); + + passed &= run("right shifts by 26 to 31 enumerate every value of the shifted-out field", [] { + Builder b; + std::vector reachable; + std::vector unreachable; + for (std::uint32_t shift = 26u; shift <= 31u; ++shift) { + const std::uint32_t largest = (std::uint32_t{1} << (32u - shift)) - 1u; + auto& scaled = b.Binary(IrOpcode::IMul32, b.Binary(IrOpcode::ShiftRightLogical32, b.Unknown(), shift), 3u); + reachable.push_back(b.Equal(scaled, largest * 3u)); + reachable.push_back(b.Equal(scaled, 3u)); + reachable.push_back(b.Equal(scaled, 0u)); + unreachable.push_back(b.Equal(scaled, largest * 3u + 1u)); + unreachable.push_back(b.Equal(scaled, 4u)); + } + auto& tooWide = b.Binary(IrOpcode::IMul32, b.Binary(IrOpcode::ShiftRightLogical32, b.Unknown(), 25u), 3u); + const auto wide = b.Equal(tooWide, 4u); + b.Fold(); + bool matched = b.Keeps(wide); + for (const auto compare : reachable) matched &= b.Keeps(compare); + for (const auto compare : unreachable) matched &= b.Folds(compare, false); + return matched; + }); + + passed &= run("value sets propagate through select, or and xor", [] { + Builder b; + auto& five = b.Binary(IrOpcode::IMul32, b.Binary(IrOpcode::UMin32, b.Unknown(), 3u), 5u); + auto& seven = b.Binary(IrOpcode::IMul32, b.Binary(IrOpcode::UMin32, b.Unknown(), 2u), 7u); + auto& condition = b.Emit(IrOpcode::ULessThan32, IrType::Bool, {&b.Unknown(), &b.Constant(16u)}); + auto& choice = b.Emit(IrOpcode::SelectU32, IrType::U32, {&condition, &five, &seven}); + auto& merged = b.Binary(IrOpcode::BitwiseOr32, five, 16u); + auto& flipped = b.Binary(IrOpcode::BitwiseXor32, five, 0xf0u); + const auto choiceMissing = b.Equal(choice, 12u); + const auto choiceMissingNot = b.NotEqual(choice, 11u); + const auto choiceFirst = b.Equal(choice, 15u); + const auto choiceSecond = b.Equal(choice, 14u); + const auto choiceShared = b.Equal(choice, 0u); + const auto mergedMissing = b.Equal(merged, 22u); + const auto mergedPresent = b.Equal(merged, 21u); + const auto flippedMissing = b.Equal(flipped, 246u); + const auto flippedPresent = b.Equal(flipped, 245u); + b.Fold(); + return b.Folds(choiceMissing, false) && b.Folds(choiceMissingNot, true) && b.Keeps(choiceFirst) && b.Keeps(choiceSecond) && b.Keeps(choiceShared) && b.Folds(mergedMissing, false) && b.Keeps(mergedPresent) && b.Folds(flippedMissing, false) && b.Keeps(flippedPresent); + }); + + passed &= run("an unsigned less-than folds from the known-bit and value-set bounds", [] { + Builder b; + auto& scaled = b.Binary(IrOpcode::IMul32, b.Binary(IrOpcode::BitwiseAnd32, b.Shifted(4u), 0x3ffu), 3u); + auto& masked = b.Binary(IrOpcode::BitwiseAnd32, b.Unknown(), 0xfffu); + auto& raised = b.Binary(IrOpcode::BitwiseOr32, b.Unknown(), 0x100u); + const auto scaledBelow = b.Less(scaled, 3025u); + const auto scaledEdge = b.Less(scaled, 3024u); + const auto maskedBelow = b.Less(masked, 0x1000u); + const auto maskedEdge = b.Less(masked, 0xfffu); + const auto raisedAbove = b.Less(raised, 0x100u); + const auto raisedEdge = b.Less(raised, 0x101u); + const auto lane = b.Less(b.Unknown(), 0xffffffffu); + b.Fold(); + return b.Folds(scaledBelow, true) && b.Keeps(scaledEdge) && b.Folds(maskedBelow, true) && b.Keeps(maskedEdge) && b.Folds(raisedAbove, false) && b.Keeps(raisedEdge) && b.Keeps(lane); + }); + + passed &= run("relative indexing compares fold for an offset of 4 * i + 1", [] { + Builder b; + auto& index = b.Emit(IrOpcode::ReadFirstLane, IrType::U32, {&b.Shifted(2u), &b.Emit(IrOpcode::IEqual32, IrType::Bool, {&b.Unknown(), &b.Constant(0u)})}); + auto& offset = b.Binary(IrOpcode::IAdd32, index, 1u); + std::vector hits; + for (std::uint32_t entry = 1u; entry < 16u; ++entry) hits.push_back(b.Equal(offset, entry)); + b.Fold(); + bool matched = true; + for (std::uint32_t entry = 1u; entry < 16u; ++entry) matched &= (entry & 3u) == 1u ? b.Keeps(hits[entry - 1u]) : b.Folds(hits[entry - 1u], false); + return matched; + }); + + return passed ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp b/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp index 86fd98da3e..1e855b1d1a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp @@ -53,9 +53,8 @@ int main() { resources.controlFlow.emplace_back(); resources.controlFlow.back().condition = pinned[9]; resources.uniformFill.values[0] = pinned[10]; - auto external = std::make_unique(IrOpcode::Identity, IrType::U32, value.Id()); - external->AddArgument(pinned[11]); - resources.valueStorage.push_back(std::move(external)); + resources.valueStorage.Reserve(1); + resources.valueStorage.Create(IrOpcode::Identity, IrType::U32, value.Id())->AddArgument(pinned[11]); const auto removed = program.ReclaimUnusedValues(); if (removed != 3u) throw std::runtime_error("detached identity and cyclic garbage were not reclaimed"); if (value.Id() != id || value.ImmediateU32() != 42u || value.Uses().size() != 1u) throw std::runtime_error("a live instruction lost its value or retained an unused backlink"); diff --git a/core/libs/prx/libSceAgcDriver/tests/KeyMiss.cpp b/core/libs/prx/libSceAgcDriver/tests/KeyMiss.cpp new file mode 100644 index 0000000000..c091e0c3b4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/KeyMiss.cpp @@ -0,0 +1,156 @@ +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include +#include +#include +#include + +using namespace AgcDriver; +using Graphics::ContentKeyComponent; +using Graphics::ContentKeyDiff; +using Graphics::ShaderResources; +using ShaderRecompiler::DescriptorRole; + +namespace { + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +struct Binding { + DescriptorRole role; + std::vector words; + std::uint32_t count = 1; + std::vector shapes; +}; + +std::vector shaderKey(bool dataWords, bool movable, const std::vector& bindings) { + std::vector key{dataWords ? 1u : 0u, 0u, 7u, 0u, static_cast(bindings.size())}; + for (const auto& binding : bindings) { + const bool data = binding.role == DescriptorRole::ShaderData || binding.role == DescriptorRole::FlattenedSrt; + for (const auto word : {0u, static_cast(binding.role), 0u, 0u, binding.count, 0u, 0u, static_cast(binding.words.size())}) key.push_back(word); + const bool emitted = (movable && binding.role == DescriptorRole::GuestBuffers && binding.words.size() == binding.count * 4u) || (dataWords && !movable) || !data; + if (emitted) key.insert(key.end(), binding.words.begin(), binding.words.end()); + for (int flags = 0; flags < 9; ++flags) key.push_back(0u); + key.push_back(0u); + key.push_back(static_cast(binding.shapes.size())); + key.insert(key.end(), binding.shapes.begin(), binding.shapes.end()); + } + return key; +} + +std::vector drawKey(std::uint32_t device, const std::vector& stage, std::uint32_t target) { + std::vector key{0xffffffffu, device, 0u, 1u, static_cast(stage.size())}; + key.insert(key.end(), stage.begin(), stage.end()); + key.push_back(1u); + for (const auto word : {target, 0u, 0x100u, 0u, 0u, 0u, 0u, 0u}) key.push_back(word); + return key; +} + +std::vector bindings() { + return {{DescriptorRole::GuestBuffers, {10, 11, 12, 13}}, {DescriptorRole::GuestImages, {20, 21, 22, 23, 24, 25, 26, 27}}, {DescriptorRole::GuestSamplers, {30, 31, 32, 33}}, {DescriptorRole::ShaderData, {40, 41, 42, 43}}}; +} + +void expectOnly(const ContentKeyDiff& diff, std::optional component, const char* message) { + for (std::size_t i = 0; i < diff.size(); ++i) { + check(diff[i] == (component && i == static_cast(*component)), message); + } +} + +void testDispatchKey() { + ContentKeyDiff diff; + const auto base = shaderKey(true, false, bindings()); + check(ShaderResources::DiffContentKeys(base, base, false, diff), "equal keys were not comparable"); + expectOnly(diff, std::nullopt, "equal keys reported a difference"); + const auto change = [&](std::size_t binding, std::size_t word, ContentKeyComponent expected, const char* message) { + auto changed = bindings(); + changed[binding].words[word] += 1; + check(ShaderResources::DiffContentKeys(base, shaderKey(true, false, changed), false, diff), message); + expectOnly(diff, expected, message); + }; + change(0, 0, ContentKeyComponent::BufferWord0, "V# word 0"); + change(0, 1, ContentKeyComponent::BufferWord1, "V# word 1"); + change(0, 2, ContentKeyComponent::BufferWord2, "V# word 2"); + change(0, 3, ContentKeyComponent::BufferWord3, "V# word 3"); + change(1, 5, ContentKeyComponent::Image, "image descriptor word"); + change(2, 1, ContentKeyComponent::Sampler, "sampler word"); + change(3, 2, ContentKeyComponent::ShaderData, "shader data word"); + auto two = bindings(); + two[0].words[0] += 1; + two[0].words[3] += 1; + check(ShaderResources::DiffContentKeys(base, shaderKey(true, false, two), false, diff), "two components"); + check(diff[static_cast(ContentKeyComponent::BufferWord0)] && diff[static_cast(ContentKeyComponent::BufferWord3)] && !diff[static_cast(ContentKeyComponent::BufferWord1)], "two components in one miss are both counted"); + auto fewer = bindings(); + fewer.pop_back(); + check(!ShaderResources::DiffContentKeys(base, shaderKey(true, false, fewer), false, diff), "different shapes were compared"); + expectOnly(diff, ContentKeyComponent::Layout, "shape change is a layout change"); + const auto noData = shaderKey(false, false, bindings()); + auto changedData = bindings(); + changedData[3].words[0] += 1; + check(ShaderResources::DiffContentKeys(noData, shaderKey(false, false, changedData), false, diff), "data words left out"); + expectOnly(diff, std::nullopt, "data words are not part of the key without dataWords"); + std::vector table{{DescriptorRole::ImageTable, {50, 51, 52, 53, 54, 55, 56, 57}, 1, {1}}, {DescriptorRole::SamplerTable, {60, 61, 62, 63}}}; + const auto tableBase = shaderKey(true, false, table); + auto tableWord = table; + tableWord[0].words[2] += 1; + check(ShaderResources::DiffContentKeys(tableBase, shaderKey(true, false, tableWord), false, diff), "image table word"); + expectOnly(diff, ContentKeyComponent::Image, "image table word"); + auto tableShape = table; + tableShape[0].shapes[0] += 1; + check(ShaderResources::DiffContentKeys(tableBase, shaderKey(true, false, tableShape), false, diff), "image table shape"); + expectOnly(diff, ContentKeyComponent::Image, "image table shape"); + auto samplerWord = table; + samplerWord[1].words[1] += 1; + check(ShaderResources::DiffContentKeys(tableBase, shaderKey(true, false, samplerWord), false, diff), "sampler table word"); + expectOnly(diff, ContentKeyComponent::Sampler, "sampler table word"); +} + +std::vector withDevice(std::vector key, std::uint32_t device) { + key.push_back(device); + key.push_back(0u); + return key; +} + +void testDispatchKeyWithDevice() { + ContentKeyDiff diff; + const auto base = withDevice(shaderKey(false, false, bindings()), 5); + check(ShaderResources::DiffContentKeys(base, base, false, diff), "dispatch key with device words was not comparable"); + expectOnly(diff, std::nullopt, "equal dispatch keys with device words reported a difference"); + auto buffer = bindings(); + buffer[0].words[0] += 1; + check(ShaderResources::DiffContentKeys(base, withDevice(shaderKey(false, false, buffer), 5), false, diff), "dispatch V# word 0 with device words"); + expectOnly(diff, ContentKeyComponent::BufferWord0, "dispatch V# word 0 with device words"); + check(ShaderResources::DiffContentKeys(base, withDevice(shaderKey(false, false, bindings()), 6), false, diff), "dispatch device change"); + expectOnly(diff, ContentKeyComponent::Other, "dispatch device change"); +} + +void testDrawKey() { + ContentKeyDiff diff; + const auto base = drawKey(5, shaderKey(true, true, bindings()), 0x1000); + const auto compare = [&](const std::vector& key, std::optional expected, const char* message) { + check(ShaderResources::DiffContentKeys(base, key, true, diff), message); + expectOnly(diff, expected, message); + }; + compare(drawKey(5, shaderKey(true, true, bindings()), 0x2000), ContentKeyComponent::DrawTarget, "draw target"); + compare(drawKey(6, shaderKey(true, true, bindings()), 0x1000), ContentKeyComponent::Other, "device"); + auto buffer = bindings(); + buffer[0].words[0] += 1; + compare(drawKey(5, shaderKey(true, true, buffer), 0x1000), ContentKeyComponent::BufferWord0, "draw V# word 0"); + auto data = bindings(); + data[3].words[0] += 1; + compare(drawKey(5, shaderKey(true, true, data), 0x1000), std::nullopt, "movable draw keys leave data words out"); +} + +} + +int main() { + try { + testDispatchKey(); + testDispatchKeyWithDevice(); + testDrawKey(); + std::puts("Key miss attribution 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/LayerInput.cpp b/core/libs/prx/libSceAgcDriver/tests/LayerInput.cpp new file mode 100644 index 0000000000..2536891052 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/LayerInput.cpp @@ -0,0 +1,92 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +constexpr std::array FragmentCode{ + 0xbefc0300u, 0x7e028700u, 0xd5480001u, 0x02111101u, + 0x7e020d01u, 0x060202f2u, 0xf800180fu, 0x01010101u, 0xbf810000u +}; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +std::vector Compile(std::span capabilities) { + RecompileRequest request{}; + request.shader = {ShaderStage::Fragment, 0x30000u, FragmentCode, 0, {}}; + request.context.waveSize = 32; + const std::array data{0u}; + request.context.userData = data; + ShaderPixelStageInfo pixel{}; + pixel.wave32 = true; + pixel.inputAddr = PixelInputBit(PixelInput::Ancillary); + pixel.ancillary = true; + pixel.targetOutputMode[0] = 9u; + pixel.targetExportMapping.fill(0xe4u); + request.context.pixel = pixel; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + const auto result = Recompile(request); + const auto words = result.spirv.Words(); + return {words.begin(), words.end()}; +} + +bool Declares(std::span words, spv::Capability capability) { + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + if (count == 0 || count > words.size() - cursor) return false; + if ((words[cursor] & 0xffffu) == spv::OpCapability && words[cursor + 1] == static_cast(capability)) return true; + cursor += count; + } + return false; +} + +std::string Extensions(std::span words) { + std::string text; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + if (count == 0 || count > words.size() - cursor) break; + if ((words[cursor] & 0xffffu) == spv::OpExtension) { + text.append(reinterpret_cast(&words[cursor + 1]), (count - 1) * 4u); + } + cursor += count; + } + return text; +} + +} + +int main() { + try { + const std::array withGeometry{spv::CapabilityShader, spv::CapabilityGeometry}; + const std::array withoutGeometry{spv::CapabilityShader, spv::CapabilityShaderViewportIndexLayerEXT}; + const auto geometry = Compile(withGeometry); + Require(Declares(geometry, spv::CapabilityGeometry), "a Layer input on a device with geometry shaders must declare Geometry"); + Require(!Declares(geometry, spv::CapabilityShaderViewportIndexLayerEXT), "a Layer input on a device with geometry shaders must not declare the viewport index layer capability"); + const auto extension = Compile(withoutGeometry); + Require(Declares(extension, spv::CapabilityShaderViewportIndexLayerEXT), "a Layer input without geometry shaders must declare the viewport index layer capability"); + Require(!Declares(extension, spv::CapabilityGeometry), "a Layer input without geometry shaders must not declare Geometry"); + Require(Extensions(extension).find("SPV_EXT_shader_viewport_index_layer") != std::string::npos, "a Layer input without geometry shaders must declare SPV_EXT_shader_viewport_index_layer"); + std::printf("layer input capability tests passed\n"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp new file mode 100644 index 0000000000..d409fc1d07 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp @@ -0,0 +1,142 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +std::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; +} + +std::set compareExchangeWidths(std::span words) { + std::map widths; + std::set found; + std::uint32_t faultFunction = 0; + bool inFaultFunction = false; + 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::OpName && std::string_view(reinterpret_cast(instruction.data() + 2)) == "record_bda_fault") faultFunction = instruction[1]; + if (opcode == spv::OpFunction) inFaultFunction = instruction[2] == faultFunction; + if (opcode == spv::OpTypeInt) widths.emplace(instruction[1], instruction[2]); + if (opcode == spv::OpAtomicCompareExchange && !inFaultFunction) found.insert(widths.at(instruction[1])); + offset += count; + } + return found; +} + +RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit, bool int64Atomics) { + 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, spv::CapabilityInt64Atomics}; + 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 = std::span(capabilities).first(int64Atomics ? 8u : 7u); + 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, bool int64Atomics) { + const auto result = compile(waveSize, limit, int64Atomics); + 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 ? 18u : 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"); + const auto widths = compareExchangeWidths(result.spirv.Words()); + const bool native = int64Atomics && limit < 68u; + require(widths.contains(64u) == native, native ? "device-memory LDS did not use a 64-bit compare exchange" : "a 64-bit compare exchange appeared without device-memory LDS and shaderBufferInt64Atomics"); + require(widths.contains(32u) != native, native ? "device-memory LDS with shaderBufferInt64Atomics kept the workgroup lock" : "the 64-bit LDS atomic lost its workgroup lock"); + require(result.bdaAbiVersion == (limit < 68u ? 1u : 0u), "the fault buffer for a misaligned 64-bit LDS atomic does not follow device-memory LDS at limit " + std::to_string(limit)); +} + +} + +int main() { + unsigned failures = 0; + for (const auto waveSize : {32u, 64u}) { + for (const auto limit : {64u, 68u, 72u}) { + for (const bool int64Atomics : {false, true}) { + const auto name = "wave" + std::to_string(waveSize) + " limit" + std::to_string(limit) + (int64Atomics ? " int64 atomics" : ""); + try { + verify(waveSize, limit, int64Atomics); + 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/LiveStack.cpp b/core/libs/prx/libSceAgcDriver/tests/LiveStack.cpp index b0dce8e47b..facdaddbe3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/LiveStack.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/LiveStack.cpp @@ -75,6 +75,7 @@ void RunLiveStackAccessTests() { pthread_join(thread, nullptr); pthread_attr_destroy(&attributes); Require(GuestMemory::Accessible(block + page, 64, true) && !GuestMemory::Accessible(block, 16) && !GuestMemory::Accessible(block + stackBytes, 16), "the released test stack is misreported"); + Require(GuestMemory::Accessible(block + page, 64) && !GuestMemory::Accessible(block + page - 8, 16) && !GuestMemory::Accessible(block + stackBytes - 8, 16), "a read of the released test stack is misreported"); munmap(block, stackBytes + page); Require(arguments.failure.empty(), arguments.failure.c_str()); #endif diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index 175fdaf979..ff2a3e40a7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -1,5 +1,7 @@ #include "IntermediateRepresentation/IrProgram.hpp" +#include "IntermediateRepresentation/IrBuilder.hpp" #include "Optimization/MaskedSelectEliminator.hpp" +#include "StencilReplay.hpp" #include "RdnaDecoder/RdnaInstruction.hpp" #include #include @@ -26,6 +28,7 @@ struct Builder { IrBlock& newBlock() { auto& created = program.CreateBlock(); program.BlockOrder().push_back(&created); + program.Metadata().blockInfo.push_back(BlockInfo{created.Id()}); return created; } @@ -118,6 +121,25 @@ bool expect(const char* name, bool condition) { return condition; } +bool messageSafety(IrShaderStage stage, bool malformed = false, bool allocate = false) { + IrProgram program; + program.Resources().stage = stage; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + if (allocate) static_cast(builder.Emit(IrOpcode::MeshAllocate, IrType::Void, {&builder.Constant(0u)})); + else if (malformed) { + auto& message = program.CreateValue(IrOpcode::Sendmsg, IrType::Void); + message.AddArgument(&builder.Constant(0u)); + block.AppendInstruction(&message); + } + else static_cast(builder.Emit(IrOpcode::Sendmsg, IrType::Void, {})); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + return StencilReplaySafe(program); +} + bool run(const char* name, const std::function& test) { try { return expect(name, test()); @@ -767,6 +789,470 @@ int main() { return removed(written); }; + passed &= run("a vertex message emitted as void is replay-safe", [&] { return messageSafety(IrShaderStage::Vertex); }); + passed &= run("pixel message classification remains unchanged", [&] { return !messageSafety(IrShaderStage::Pixel); }); + passed &= run("compute message classification remains unchanged", [&] { return !messageSafety(IrShaderStage::Compute); }); + passed &= run("a message with operands is refused", [&] { return !messageSafety(IrShaderStage::Vertex, true); }); + passed &= run("mesh allocation remains refused", [&] { return !messageSafety(IrShaderStage::Vertex, false, true); }); + + passed &= run("guard implication preserves a mask within its whole-quad expansion", [&] { + Builder b; + auto& live = b.mask(16u); + auto& quads = wholeQuads(b, live); + return GuardImplies(b.program, &live, &quads) && !GuardImplies(b.program, &quads, &live); + }); + + passed &= run("guard implication intersects both paths through a phi", [&] { + Builder b; + auto& entry = *b.block; + auto& live = b.mask(16u); + auto& quads = wholeQuads(b, live); + auto& left = b.newBlock(); + auto& right = b.newBlock(); + auto& merge = b.newBlock(); + entry.AddBranch(&left); + entry.AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + b.block = &merge; + auto& subset = b.phi(IrType::Bool, left, live); + subset.AddPhiOperand(&right, &b.logicalAnd(live, b.mask(8u))); + auto& wider = b.phi(IrType::Bool, left, live); + wider.AddPhiOperand(&right, &b.truth()); + return GuardImplies(b.program, &subset, &quads) && !GuardImplies(b.program, &wider, &quads); + }); + + passed &= run("guard implication handles whole-quad masks intersected with own active ballots", [&] { + Builder b; + auto& live = b.mask(16u); + const auto [low, high] = b.ballotWords(live); + auto& wqm = b.emit(IrOpcode::WqmU64, IrType::U64, {&b.emit(IrOpcode::CompositeConstructU64, IrType::U64, {low, high})}); + auto& wideLow = b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(0u)}); + auto& wideHigh = b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(1u)}); + const auto [allLow, allHigh] = b.ballotWords(b.truth()); + auto& guard = b.threadBit(b.wordAnd(wideLow, *allLow), b.wordAnd(*allHigh, wideHigh)); + auto& narrower = b.logicalAnd(live, b.mask(8u)); + return GuardImplies(b.program, &live, &guard) && GuardImplies(b.program, &narrower, &guard) && !GuardImplies(b.program, &guard, &live); + }); + + passed &= run("a paired mask phi with a wider incoming arm does not imply a narrower write mask", [] { + Builder b; + auto& entry = *b.block; + auto& live = b.mask(16u); + const auto [low, high] = b.ballotWords(live); + auto& left = b.newBlock(); + auto& right = b.newBlock(); + auto& merge = b.newBlock(); + entry.AddBranch(&left); + entry.AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + b.block = &merge; + auto& lowPhi = b.phi(IrType::U32, left, *low); + lowPhi.AddPhiOperand(&right, &b.constant(0xffffffffu)); + auto& highPhi = b.phi(IrType::U32, left, *high); + highPhi.AddPhiOperand(&right, &b.constant(0xffffffffu)); + auto& merged = b.threadBit(lowPhi, highPhi); + return !GuardImplies(b.program, &merged, &live); + }); + + passed &= run("a reconverged active ballot dominates a later mask observation", [] { + Builder b; + auto& entry = *b.block; + auto& left = b.newBlock(); + auto& right = b.newBlock(); + auto& merge = b.newBlock(); + auto& exit = b.newBlock(); + entry.AddBranch(&left); + entry.AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + merge.AddBranch(&exit); + b.block = &merge; + auto& active = b.ballotBit(b.truth()); + b.block = &exit; + auto& observed = b.mask(16u); + return GuardImplies(b.program, &observed, &active); + }); + + passed &= run("an active ballot from one branch does not dominate a merged observation", [] { + Builder b; + auto& entry = *b.block; + auto& left = b.newBlock(); + auto& right = b.newBlock(); + auto& merge = b.newBlock(); + entry.AddBranch(&left); + entry.AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + b.block = &left; + auto& active = b.ballotBit(b.truth()); + b.block = &merge; + auto& observed = b.mask(16u); + return !GuardImplies(b.program, &observed, &active); + }); + + passed &= run("a later ballot in a reconverged block cannot justify an earlier observation", [] { + Builder b; + auto& entry = *b.block; + auto& left = b.newBlock(); + auto& right = b.newBlock(); + auto& merge = b.newBlock(); + entry.AddBranch(&left); + entry.AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + b.block = &merge; + auto& observed = b.mask(16u); + auto& active = b.ballotBit(b.truth()); + return !GuardImplies(b.program, &observed, &active); + }); + + passed &= run("guard implication refuses a loop-carried condition", [] { + Builder b; + auto& entry = *b.block; + auto& header = b.newBlock(); + entry.AddBranch(&header); + header.AddBranch(&header); + b.block = &header; + auto& condition = b.mask(8u); + auto& consequence = b.mask(16u); + return !GuardImplies(b.program, &condition, &consequence); + }); + + passed &= run("stencil replay refuses neighbor-lane operations even without LDS storage", [] { + for (const auto opcode : {IrOpcode::PermuteU32, IrOpcode::BpermuteU32, IrOpcode::SwizzleU32, IrOpcode::ReadLane, IrOpcode::ReadFirstLane, IrOpcode::DppMoveU32, IrOpcode::DppUpdateU32, IrOpcode::Permlane16U32, IrOpcode::WqmU64}) { + Builder b; + b.emit(opcode, IrType::U32, {}); + if (StencilReplaySafe(b.program)) return false; + } + return true; + }); + + const auto exportUnder = [](Builder& b, IrValue& guard, bool stencil, bool live) { + ExportInfo info{}; + info.kind = stencil ? ExportTargetKind::MrtZ : ExportTargetKind::Mrt; + info.en = stencil ? 2u : 15u; + info.vm = live; + ExportFlags flags{static_cast(b.program.Metadata().exportInfo.size())}; + b.program.Metadata().exportInfo.push_back(info); + std::uint64_t bits = 0; + std::memcpy(&bits, &flags, sizeof(flags)); + auto& zero = b.constant(0u); + auto& value = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&zero, &zero, &zero, &zero}); + b.emit(IrOpcode::SetAttribute, IrType::Void, {&value, &guard}, bits); + }; + + const auto pixelPredicate = [](Builder& b) -> IrValue& { + auto& value = b.emit(IrOpcode::GetBuiltin, IrType::U32, {&b.constant(static_cast(StageInputKind::FragCoord)), &b.constant(0u)}); + return b.emit(IrOpcode::INotEqual32, IrType::Bool, {&value, &b.constant(0u)}); + }; + + const auto helperPredicate = [](Builder& b, bool helper) -> IrValue& { + auto& value = b.emit(IrOpcode::GetBuiltin, IrType::U32, {&b.constant(static_cast(StageInputKind::HelperInvocation)), &b.constant(0u)}); + return b.emit(helper ? IrOpcode::INotEqual32 : IrOpcode::IEqual32, IrType::Bool, {&value, &b.constant(0u)}); + }; + + const auto stencilByMask = [](Builder& b, IrValue& mask) { + auto& zero = b.constant(0u); + auto& reference = b.select(mask, zero, b.constant(255u)); + ExportInfo info{}; + info.kind = ExportTargetKind::MrtZ; + info.en = 2u; + info.vm = true; + ExportFlags flags{static_cast(b.program.Metadata().exportInfo.size())}; + b.program.Metadata().exportInfo.push_back(info); + std::uint64_t bits = 0; + std::memcpy(&bits, &flags, sizeof(flags)); + auto& value = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&zero, &reference, &zero, &zero}); + b.emit(IrOpcode::SetAttribute, IrType::Void, {&value, &b.truth()}, bits); + }; + + passed &= run("finite stencil references selected by an undefined value cannot be replayed", [] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& undefined = b.emit(IrOpcode::UndefU1, IrType::Bool, {}); + auto& zero = b.constant(0u); + auto& reference = b.select(undefined, zero, b.constant(255u)); + ExportInfo info{}; + info.kind = ExportTargetKind::MrtZ; + info.en = 2u; + info.vm = true; + b.program.Metadata().exportInfo.push_back(info); + ExportFlags flags{0u}; + std::uint64_t bits = 0; + std::memcpy(&bits, &flags, sizeof(flags)); + auto& value = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&zero, &reference, &zero, &zero}); + b.emit(IrOpcode::SetAttribute, IrType::Void, {&value, &b.truth()}, bits); + return StencilReferenceValues(b.program) == std::vector{0u, 255u} && !StencilReplaySafe(b.program); + }); + + passed &= run("an unprovable stencil reference replays every eight-bit value", [] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& zero = b.constant(0u); + auto& reference = b.emit(IrOpcode::UndefU32, IrType::U32, {}); + ExportInfo info{}; + info.kind = ExportTargetKind::MrtZ; + info.en = 2u; + info.vm = true; + b.program.Metadata().exportInfo.push_back(info); + ExportFlags flags{0u}; + std::uint64_t bits = 0; + std::memcpy(&bits, &flags, sizeof(flags)); + auto& value = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&zero, &reference, &zero, &zero}); + b.emit(IrOpcode::SetAttribute, IrType::Void, {&value, &b.truth()}, bits); + const auto values = StencilReferenceValues(b.program); + return values.size() == 256u && values.front() == 0u && values.back() == 255u; + }); + + passed &= run("a proven stencil reference replays its eight-bit value", [] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& zero = b.constant(0u); + ExportInfo info{}; + info.kind = ExportTargetKind::MrtZ; + info.en = 2u; + info.vm = true; + b.program.Metadata().exportInfo.push_back(info); + ExportFlags flags{0u}; + std::uint64_t bits = 0; + std::memcpy(&bits, &flags, sizeof(flags)); + auto& value = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&zero, &b.constant(300u), &zero, &zero}); + b.emit(IrOpcode::SetAttribute, IrType::Void, {&value, &b.truth()}, bits); + return StencilReferenceValues(b.program) == std::vector{44u}; + }); + + passed &= run("stencil replay admits own-lane masks and their whole-quad expansion as export guards", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& quads = wholeQuads(b, b.logicalAnd(pixelPredicate(b), helperPredicate(b, false))); + exportUnder(b, quads, true, true); + return StencilReplaySafe(b.program); + }); + + passed &= run("helper participation cannot choose a kept lane's stencil through whole-quad expansion", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + stencilByMask(b, wholeQuads(b, helperPredicate(b, true))); + return StencilReferenceValues(b.program) == std::vector{0u, 255u} && !StencilReplaySafe(b.program); + }); + + passed &= run("nested active ballots do not erase helper uncertainty before whole-quad expansion", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& active = b.ballotBit(b.truth()); + stencilByMask(b, wholeQuads(b, b.logicalAnd(active, helperPredicate(b, true)))); + return !StencilReplaySafe(b.program); + }); + + passed &= run("an uncovered helper coordinate predicate cannot choose a real stencil through a ballot", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& bits = b.emit(IrOpcode::GetBuiltin, IrType::U32, {&b.constant(static_cast(StageInputKind::FragCoord)), &b.constant(0u)}); + auto& coordinate = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&bits}); + auto& boundary = b.program.CreateValue(IrOpcode::Void, IrType::F32); + boundary.SetImmediateF32(1.0f); + auto& predicate = b.emit(IrOpcode::FPOrdGreaterThan32, IrType::Bool, {&coordinate, &boundary}); + stencilByMask(b, wholeQuads(b, predicate)); + return !StencilReplaySafe(b.program); + }); + + passed &= run("explicit helper exclusion is preserved through nested ballot mask plumbing", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& real = b.ballotBit(helperPredicate(b, false)); + stencilByMask(b, wholeQuads(b, b.logicalAnd(real, pixelPredicate(b)))); + return StencilReplaySafe(b.program); + }); + + passed &= run("only guarded read-only SRT diagnostics can be replayed", [] { + Builder b; + b.program.Info().usesFaultBuffer = true; + if (StencilReplaySafe(b.program)) return false; + b.program.Resources().guardedSrtSlots.push_back(0u); + if (!StencilReplaySafe(b.program)) return false; + b.program.Metadata().blockInfo.front().terminator.loopHeader = true; + if (StencilReplaySafe(b.program)) return false; + b.program.Metadata().blockInfo.front().terminator.loopHeader = false; + b.emit(IrOpcode::Loop, IrType::Void, {}); + return !StencilReplaySafe(b.program); + }); + + passed &= run("guarded SRT replay refuses an unannotated control-flow cycle", [] { + Builder b; + b.program.Info().usesFaultBuffer = true; + b.program.Resources().guardedSrtSlots.push_back(0u); + auto& entry = *b.block; + auto& cycle = b.newBlock(); + entry.AddBranch(&cycle); + cycle.AddBranch(&cycle); + auto& metadata = b.program.Metadata().blockInfo; + metadata[0u].terminator.kind = TerminatorKind::Branch; + metadata[0u].terminator.trueBlock = cycle.Id(); + metadata[1u].terminator.kind = TerminatorKind::Branch; + metadata[1u].terminator.trueBlock = cycle.Id(); + return !StencilReplaySafe(b.program); + }); + + passed &= run("guarded SRT replay refuses missing control-flow targets", [] { + Builder b; + b.program.Info().usesFaultBuffer = true; + b.program.Resources().guardedSrtSlots.push_back(0u); + auto& term = b.program.Metadata().blockInfo.front().terminator; + term.kind = TerminatorKind::Branch; + term.trueBlock = 1024u; + return !StencilReplaySafe(b.program); + }); + + passed &= run("a captured active ballot cannot provide helper texture coordinates", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& active = b.ballotBit(b.truth()); + auto& helperValue = b.emit(IrOpcode::GetBuiltin, IrType::U32, {&b.constant(static_cast(StageInputKind::HelperInvocation)), &b.constant(0u)}); + auto& helper = b.emit(IrOpcode::INotEqual32, IrType::Bool, {&helperValue, &b.constant(0u)}); + auto& coordinate = b.select(b.logicalAnd(active, helper), b.constant(1u), b.constant(0u)); + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&coordinate}); + auto& image = b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}); + auto& sampler = b.emit(IrOpcode::GetSamplerResource, IrType::SamplerResource, {}); + b.emit(IrOpcode::ImageSampleRaw, IrType::U32x4, {&image, &sampler, &address}); + exportUnder(b, b.truth(), true, true); + return !StencilReplaySafe(b.program); + }); + + const auto optionalPixelBody = [&](bool derivativeConsumer, IrOpcode observer = IrOpcode::ImageSampleRaw) { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& entry = *b.block; + auto& predicate = pixelPredicate(b); + auto& zero = b.constant(0u); + auto& condition = b.emit(IrOpcode::LogicalNot, IrType::Bool, {&predicate}); + auto& body = b.newBlock(); + auto& merge = b.newBlock(); + entry.AddBranch(&body); + entry.AddBranch(&merge); + body.AddBranch(&merge); + auto& entryInfo = b.program.Metadata().blockInfo[0u]; + entryInfo.terminator.kind = TerminatorKind::ConditionalBranch; + entryInfo.terminator.condition = BranchCondition::ExecZero; + entryInfo.terminator.trueBlock = merge.Id(); + entryInfo.terminator.falseBlock = body.Id(); + entryInfo.condition = &condition; + auto& bodyInfo = b.program.Metadata().blockInfo[1u]; + bodyInfo.terminator.kind = TerminatorKind::Branch; + bodyInfo.terminator.trueBlock = merge.Id(); + b.block = &body; + auto& written = b.select(predicate, b.constant(42u), zero); + b.block = &merge; + auto& merged = b.phi(IrType::U32, entry, zero); + merged.AddPhiOperand(&body, &written); + if (derivativeConsumer) { + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&merged}); + auto& image = b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}); + auto& sampler = b.emit(IrOpcode::GetSamplerResource, IrType::SamplerResource, {}); + b.emit(observer, IrType::U32x4, {&image, &sampler, &address}); + } else b.keep(merged); + exportUnder(b, b.truth(), true, true); + return StencilReplaySafe(b.program); + }; + + passed &= run("an optional wave body preserves inactive values consumed only within a lane", [&] { return optionalPixelBody(false); }); + passed &= run("an optional wave body cannot change helper coordinates for later implicit samples", [&] { return !optionalPixelBody(true); }); + passed &= run("an optional wave body cannot change helper coordinates for LOD queries", [&] { return !optionalPixelBody(true, IrOpcode::ImageQueryLod); }); + + passed &= run("varying attribute sources cannot be treated as uniform any-active predicates", [&] { + for (const auto opcode : {IrOpcode::GetAttribute, IrOpcode::GetInterpolationParameter, IrOpcode::GetInterpolationParameterF16, IrOpcode::GetTessellationAttribute}) { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& entry = *b.block; + auto& varying = b.emit(opcode, IrType::U32, {&b.constant(0u), &b.constant(0u), &b.constant(0u)}); + auto& predicate = b.emit(IrOpcode::INotEqual32, IrType::Bool, {&varying, &b.constant(0u)}); + auto& condition = b.emit(IrOpcode::LogicalNot, IrType::Bool, {&predicate}); + auto& left = b.newBlock(); + auto& right = b.newBlock(); + entry.AddBranch(&left); + entry.AddBranch(&right); + auto& info = b.program.Metadata().blockInfo[0u]; + info.terminator.kind = TerminatorKind::ConditionalBranch; + info.terminator.condition = BranchCondition::ExecZero; + info.terminator.trueBlock = left.Id(); + info.terminator.falseBlock = right.Id(); + info.condition = &condition; + b.block = &left; + exportUnder(b, b.truth(), true, true); + b.block = &right; + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&varying}); + auto& image = b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}); + auto& sampler = b.emit(IrOpcode::GetSamplerResource, IrType::SamplerResource, {}); + b.emit(IrOpcode::ImageSampleRaw, IrType::U32x4, {&image, &sampler, &address}); + exportUnder(b, b.truth(), true, true); + if (StencilReplaySafe(b.program)) return false; + } + return true; + }); + + const auto anyActiveGate = [&](bool unrelatedLiveMask) { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& entry = *b.block; + const auto [low, high] = b.ballotWords(pixelPredicate(b)); + auto& own = b.threadBit(*low, *high); + auto& words = b.emit(IrOpcode::BitwiseOr32, IrType::U32, {low, high}); + auto& any = b.emit(IrOpcode::INotEqual32, IrType::Bool, {&words, &b.constant(0u)}); + auto& condition = b.emit(IrOpcode::LogicalNot, IrType::Bool, {&any}); + auto& killed = b.newBlock(); + auto& kept = b.newBlock(); + entry.AddBranch(&killed); + entry.AddBranch(&kept); + auto& info = b.program.Metadata().blockInfo[0u]; + info.terminator.kind = TerminatorKind::ConditionalBranch; + info.terminator.condition = BranchCondition::SccZero; + info.terminator.trueBlock = killed.Id(); + info.terminator.falseBlock = kept.Id(); + info.condition = &condition; + b.block = &killed; + auto& falseValue = b.program.CreateValue(IrOpcode::Void, IrType::Bool); + falseValue.SetImmediateBool(false); + exportUnder(b, falseValue, true, true); + b.block = &kept; + exportUnder(b, unrelatedLiveMask ? b.truth() : own, true, true); + return StencilReplaySafe(b.program); + }; + + passed &= run("any-active early return is stable for the source mask's kept quads", [&] { return anyActiveGate(false); }); + passed &= run("an any-active early return cannot control unrelated kept invocations", [&] { return !anyActiveGate(true); }); + + passed &= run("kept invocations must be covered by the stencil export guard", [&] { + Builder b; + auto& written = b.mask(16u); + exportUnder(b, written, true, false); + exportUnder(b, b.logicalAnd(written, b.mask(8u)), false, true); + if (!StencilReferenceDefinedForKeptInvocations(b.program)) return false; + exportUnder(b, b.truth(), false, true); + return !StencilReferenceDefinedForKeptInvocations(b.program); + }); + + passed &= run("an export on one branch does not cover a kept return on another", [&] { + Builder b; + auto& entry = *b.block; + auto& left = b.newBlock(); + auto& right = b.newBlock(); + auto& merge = b.newBlock(); + entry.AddBranch(&left); + entry.AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + auto& metadata = b.program.Metadata().blockInfo; + metadata[0].terminator = {TerminatorKind::ConditionalBranch, BranchCondition::Unknown, left.Id(), right.Id()}; + metadata[1].terminator = {TerminatorKind::Branch, BranchCondition::Always, merge.Id()}; + metadata[2].terminator = {TerminatorKind::Branch, BranchCondition::Always, merge.Id()}; + b.block = &left; + exportUnder(b, b.truth(), true, false); + b.block = &merge; + exportUnder(b, b.truth(), false, true); + return !StencilReferenceDefinedForKeptInvocations(b.program); + }); + passed &= run("an implicit-LOD pixel sample reads its quad's coordinates and keeps the select", [&] { return !sample(IrShaderStage::Pixel, 0u); }); diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index ac2fff1057..ce6f2d65f3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -1,9 +1,11 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "Recompiler.hpp" +#include "MeshGeometryFixture.hpp" #include "execution/VulkanTestDevice.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include #include +#include #include #include #include @@ -12,6 +14,7 @@ #include #include #include +#include #include namespace { @@ -24,21 +27,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 +43,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 +94,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{}; @@ -142,6 +145,7 @@ struct MeshDraw { VkCullModeFlags cull = VK_CULL_MODE_NONE; std::span code = {}; std::uint32_t stagesRegister = 0x20u; + ShaderRecompiler::ShaderVertexStageInfo vertex{}; }; bool ReadsNumWorkgroups(std::span words) { @@ -174,7 +178,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { const std::array geometryMemory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(code))}}}; ShaderRecompiler::RecompileRequest geometry{ {ShaderStage::Mesh, reinterpret_cast(code.data()), code, 0, {}}, - {setup.waveSize, 0, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, geometryMemory}, + {setup.waveSize, 0, userData, std::nullopt, std::nullopt, setup.vertex, geometryMemory}, target, {0, 0, 0, setup.geometryPushBytes}, ShaderRecompiler::GraphicsCompileContext{0, {}, setup.mesh, std::nullopt, {setup.draw.indexAddress, setup.draw.indexCount, setup.draw.indexSize, setup.draw.instanceCount}} @@ -205,6 +209,10 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, setup.stagesRegister, setup.waveSize, 64, setup.mesh, std::nullopt}; + state.pointSize = setup.vertex.pointSize; + state.pointSizeMin = setup.vertex.pointSizeMin; + state.pointSizeMax = setup.vertex.pointSizeMax; + state.paClVsOutCntl = setup.vertex.paClVsOutCntl; state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; state.colors = {state.color}; state.hasColorTarget = true; @@ -292,6 +300,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 +392,89 @@ int main() { } Require(fastLaunchIndexedRefused, "an indexed fast-launch draw was drawn"); + for (const bool indexed : {true, false}) { + ClearPixels(); + DrawMesh(device, {SmallSubgroup, {indexed ? reinterpret_cast(Indices.data()) : 0u, 2u, indexed ? 2u : 0u, 1, 0, indexed}, VertexBufferDescriptor(indexed ? Scrambled.data() : Ordered.data(), static_cast(Ordered.size()))}); + for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) { + Require(PixelIs(CellPixel(triangle), Background), std::string(indexed ? "indexed" : "non-indexed") + " triangle-list draw of two vertices drew triangle " + std::to_string(triangle) + ", pixel " + PixelText(CellPixel(triangle))); + } + } + + ClearPixels(); + auto pointSubgroup = SmallSubgroup; + pointSubgroup.outputPrimitiveSize = 1u; + for (const auto pointWave : {32u, 64u}) { + ClearPixels(); + DrawMesh(device, {pointSubgroup, {0, static_cast(Ordered.size()), 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, pointWave}); + for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) { + const auto& position = Ordered[3u * triangle].position; + const auto x = static_cast((position[0] + 1.0f) * 0.5f * Width); + const auto y = static_cast((1.0f - position[1]) * 0.5f * Height); + const auto offset = (static_cast(y) * Width + x) * 4u; + Require(PixelIs(offset, TriangleColor(triangle)), "mesh point missing at first exported index for primitive " + std::to_string(triangle)); + Require(PixelIs(CellPixel(triangle), Background), "mesh point output filled the interior of its packed triangle"); + } + } + + for (const auto pointWave : {32u, 64u}) { + for (const auto size : {3.0f, 4.0f}) { + ClearPixels(); + MeshDraw points{pointSubgroup, {0, static_cast(Ordered.size()), 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size()))}; + points.waveSize = pointWave; + points.vertex.pointSize = size; + DrawMesh(device, points); + for (std::uint32_t primitive = 0; primitive < Triangles; ++primitive) { + std::uint32_t covered = 0; + for (std::size_t pixel = 0; pixel < Pixels.size(); pixel += 4u) covered += PixelIs(pixel, TriangleColor(primitive)); + Require(covered == static_cast(size * size), "mesh point did not cover its register-sized square"); + } + } + for (const auto exportedSize : {6.0f, -3.0f}) { + ClearPixels(); + const auto& baseCode = pointWave == 32u ? Wave32GeometryCode : GeometryCode; + std::array sizedCode{}; + std::copy_n(baseCode.begin(), 101u, sizedCode.begin()); + sizedCode[92] = 0xbf88000eu; + sizedCode[99] = 0xf80000cfu; + sizedCode[101] = 0x7e6802ffu; + sizedCode[102] = std::bit_cast(exportedSize); + sizedCode[103] = 0xf80008d1u; + sizedCode[104] = 0x00000034u; + std::copy(baseCode.begin() + 101u, baseCode.end(), sizedCode.begin() + 105u); + MeshDraw exported{pointSubgroup, {0, static_cast(Ordered.size()), 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size()))}; + exported.waveSize = pointWave; + exported.code = sizedCode; + exported.vertex.paClVsOutCntl = (1u << 16u) | (1u << 21u); + exported.vertex.pointSizeMin = 3.0f; + exported.vertex.pointSizeMax = 4.0f; + DrawMesh(device, exported); + for (std::uint32_t primitive = 0; primitive < Triangles; ++primitive) { + std::uint32_t covered = 0; + for (std::size_t pixel = 0; pixel < Pixels.size(); pixel += 4u) covered += PixelIs(pixel, TriangleColor(primitive)); + const auto side = exportedSize < 3.0f ? 3u : 4u; + Require(covered == side * side, "mesh point ignored its exported size or clamp"); + } + } + } + + 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/MeshOutputBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/MeshOutputBudget.cpp new file mode 100644 index 0000000000..01b079f477 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/MeshOutputBudget.cpp @@ -0,0 +1,64 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +ShaderRecompiler::MeshTargetLimits Limits(std::uint32_t vertexGranularity, std::uint32_t primitiveGranularity) { + ShaderRecompiler::MeshTargetLimits limits{}; + limits.maxOutputMemoryBytes = 32768u; + limits.maxOutputComponents = 128u; + limits.outputPerVertexGranularity = vertexGranularity; + limits.outputPerPrimitiveGranularity = primitiveGranularity; + return limits; +} + +ShaderRecompiler::MeshConfiguration Mesh(std::uint32_t vertices, std::uint32_t primitives) { + ShaderRecompiler::MeshConfiguration mesh{}; + mesh.maxVertices = vertices; + mesh.maxPrimitives = primitives; + return mesh; +} + +} + +int main() { + try { + using ShaderRecompiler::MeshBarycentricOutputBudget; + using ShaderRecompiler::MeshOutputUse; + + MeshOutputUse ccleste; + ccleste.flatParameters = 6u; + ccleste.perVertexParameters = 2u; + ccleste.clipVaryings = 1u; + const auto budget = MeshBarycentricOutputBudget(Mesh(128u, 128u), Limits(32u, 32u), ccleste); + Require(budget.vertexLocations == 8u, "per-vertex locations: position, six flat exports and the smooth clip varying make 8"); + Require(budget.primitiveLocations == 10u, "per-primitive locations: three corners, two per-vertex parameters of three vertices and the cull flag make 10"); + Require(budget.components == 72u, "components are 4 per location: 18 locations make 72"); + Require(budget.bytes == 36864u, "bytes: (8 + 10) locations x 16 bytes x 128 = 36864, got " + std::to_string(budget.bytes)); + Require(budget.bytes > Limits(32u, 32u).maxOutputMemoryBytes, "ccleste's configuration does not fit 32768 bytes"); + + Require(MeshBarycentricOutputBudget(Mesh(100u, 100u), Limits(32u, 32u), ccleste).bytes == 36864u, "100 vertices and primitives are padded to the granularity of 32, which gives 128"); + Require(MeshBarycentricOutputBudget(Mesh(128u, 128u), Limits(1u, 1u), ccleste).bytes == 36864u, "a granularity of 1 changes nothing for 128"); + + MeshOutputUse distances = ccleste; + distances.clipDistances = 5u; + distances.cullDistances = 1u; + distances.layer = true; + const auto more = MeshBarycentricOutputBudget(Mesh(128u, 128u), Limits(32u, 32u), distances); + Require(more.vertexLocations == 8u + 2u + 1u, "five clip distances take 2 locations and one cull distance 1"); + Require(more.primitiveLocations == 11u, "the layer output takes one per-primitive location"); + + std::puts("mesh output budget tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/MultisampleTextureLayout.cpp b/core/libs/prx/libSceAgcDriver/tests/MultisampleTextureLayout.cpp new file mode 100644 index 0000000000..2986c79da0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/MultisampleTextureLayout.cpp @@ -0,0 +1,182 @@ +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace AgcDriver::Graphics; + +namespace { + +struct ReferenceCase { + TextureTileMode tileMode; + std::uint32_t elementBytes; + std::uint32_t fragments; + std::uint32_t blockWidth; + std::uint32_t blockHeight; + std::uint32_t pitch; + std::uint32_t paddedHeight; + std::uint64_t sliceBytes; + std::array addresses; +}; + +constexpr std::array, 7> kCoordinates{{ + {0u, 0u, 0u}, {128u, 66u, 16u}, {64u, 32u, 1u}, {32u, 64u, 2u}, + {63u, 31u, 7u}, {127u, 65u, 15u}, {1u, 1u, 3u} +}}; + +constexpr ReferenceCase kReferences[] = { + {TextureTileMode::kR64KBX, 1u, 2u, 128u, 256u, 256u, 256u, 131072ull, {{0ull, 2201608ull, 141312ull, 300032ull, 919295ull, 2013271ull, 396305ull}}}, + {TextureTileMode::kR64KBX, 1u, 4u, 128u, 128u, 256u, 128u, 131072ull, {{0ull, 2185224ull, 174080ull, 316416ull, 919295ull, 1996887ull, 429073ull}}}, + {TextureTileMode::kR64KBX, 1u, 8u, 64u, 128u, 192u, 128u, 196608ull, {{0ull, 3291144ull, 279552ull, 422400ull, 1413119ull, 3061847ull, 640529ull}}}, + {TextureTileMode::kR64KBX, 2u, 2u, 128u, 128u, 256u, 128u, 131072ull, {{0ull, 2213920ull, 141312ull, 312320ull, 923390ull, 2025630ull, 396306ull}}}, + {TextureTileMode::kR64KBX, 2u, 4u, 128u, 64u, 256u, 128u, 262144ull, {{0ull, 4392992ull, 305152ull, 689152ull, 1840894ull, 4073630ull, 822290ull}}}, + {TextureTileMode::kR64KBX, 2u, 8u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6621216ull, 476160ull, 999936ull, 2793470ull, 6195358ull, 1230354ull}}}, + {TextureTileMode::kR64KBX, 4u, 2u, 64u, 128u, 192u, 128u, 196608ull, {{0ull, 3328032ull, 264192ull, 443392ull, 1390332ull, 3074204ull, 592916ull}}}, + {TextureTileMode::kR64KBX, 4u, 4u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6621216ull, 493568ull, 1016832ull, 2766588ull, 6170780ull, 1215508ull}}}, + {TextureTileMode::kR64KBX, 4u, 8u, 32u, 64u, 160u, 128u, 655360ull, {{0ull, 11077664ull, 820224ull, 1737216ull, 4667388ull, 10365084ull, 2002452ull}}}, + {TextureTileMode::kR64KBX, 8u, 2u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6621312ull, 460800ull, 984064ull, 2782968ull, 6170744ull, 1182744ull}}}, + {TextureTileMode::kR64KBX, 8u, 4u, 64u, 32u, 192u, 96u, 589824ull, {{0ull, 9963648ull, 855040ull, 1574400ull, 4158456ull, 9317496ull, 1773080ull}}}, + {TextureTileMode::kR64KBX, 8u, 8u, 32u, 32u, 160u, 96u, 983040ull, {{0ull, 16648320ull, 1444352ull, 2689280ull, 6979576ull, 15605880ull, 2952984ull}}}, + {TextureTileMode::kR64KBX, 16u, 2u, 32u, 64u, 160u, 128u, 655360ull, {{0ull, 11075712ull, 787456ull, 1706496ull, 4718576ull, 10397808ull, 1967664ull}}}, + {TextureTileMode::kR64KBX, 16u, 4u, 32u, 32u, 160u, 96u, 983040ull, {{0ull, 16646272ull, 1443328ull, 2690304ull, 7011312ull, 15641712ull, 2950960ull}}}, + {TextureTileMode::kR64KBX, 16u, 8u, 16u, 32u, 144u, 96u, 1769472ull, {{0ull, 30017664ull, 2624000ull, 4851968ull, 12615664ull, 28201072ull, 5312304ull}}}, + {TextureTileMode::kZ64KBX, 1u, 2u, 128u, 256u, 256u, 256u, 131072ull, {{0ull, 2172945ull, 149504ull, 271361ull, 923390ull, 1992879ull, 396294ull}}}, + {TextureTileMode::kZ64KBX, 1u, 4u, 128u, 128u, 256u, 128u, 131072ull, {{0ull, 2181153ull, 165890ull, 279555ull, 931580ull, 2025565ull, 396302ull}}}, + {TextureTileMode::kZ64KBX, 1u, 8u, 64u, 128u, 192u, 128u, 196608ull, {{0ull, 3295297ull, 263170ull, 410115ull, 1425404ull, 3041469ull, 591390ull}}}, + {TextureTileMode::kZ64KBX, 2u, 2u, 128u, 128u, 256u, 128u, 131072ull, {{0ull, 2181154ull, 165888ull, 279554ull, 931580ull, 2025566ull, 396300ull}}}, + {TextureTileMode::kZ64KBX, 2u, 4u, 128u, 64u, 256u, 128u, 262144ull, {{0ull, 4425794ull, 280580ull, 689158ull, 1881848ull, 4118714ull, 789532ull}}}, + {TextureTileMode::kZ64KBX, 2u, 8u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6637698ull, 459780ull, 999942ull, 2818040ull, 6219898ull, 1181244ull}}}, + {TextureTileMode::kZ64KBX, 4u, 2u, 64u, 128u, 192u, 128u, 196608ull, {{0ull, 3295300ull, 264192ull, 410628ull, 1423096ull, 3041468ull, 592920ull}}}, + {TextureTileMode::kZ64KBX, 4u, 4u, 64u, 64u, 192u, 128u, 393216ull, {{0ull, 6637700ull, 460808ull, 1000460ull, 2815728ull, 6219892ull, 1182776ull}}}, + {TextureTileMode::kZ64KBX, 4u, 8u, 32u, 64u, 160u, 128u, 655360ull, {{0ull, 11126788ull, 787464ull, 1720844ull, 4716528ull, 10359028ull, 1967736ull}}}, +}; + +template +void requireRefusal(TFunction action) { + bool refused = false; + try { action(); } catch (const std::exception&) { refused = true; } + Require(refused, "multisampled layout accepted an unsupported operation"); +} + +void checkReference(const ReferenceCase& reference) { + MultisampleTextureLayout layout(reference.tileMode, reference.elementBytes, reference.fragments, 129u, 67u, 17u); + Require(layout.BlockWidth() == reference.blockWidth && layout.BlockHeight() == reference.blockHeight, "multisampled block extent differs from AMD reference"); + Require(layout.Pitch() == reference.pitch && layout.PaddedHeight() == reference.paddedHeight, "multisampled padded extent differs from AMD reference"); + Require(layout.SliceBytes() == reference.sliceBytes && layout.Bytes() == reference.sliceBytes * 17u, "multisampled allocation differs from AMD reference"); + Require(layout.LinearBytes() == 129ull * 67u * 17u * reference.fragments * reference.elementBytes, "multisampled linear region omits fragments or layers"); + for (std::uint32_t n = 0; n < kCoordinates.size(); ++n) { + const auto& coordinate = kCoordinates[n]; + const auto address = layout.Offset(coordinate[0], coordinate[1], coordinate[2], n % reference.fragments); + Require(address == reference.addresses[n], "multisampled fragment address differs from AMD reference"); + for (auto xorBytes : {0x100u, 0x800u, 0x4000u, 0x8000u, 0xcf00u}) { + MultisampleTextureLayout swizzled(reference.tileMode, reference.elementBytes, reference.fragments, 129u, 67u, 17u, xorBytes); + const auto expected = (address & ~65535ull) | ((address & 65535ull) ^ xorBytes); + Require(swizzled.Offset(coordinate[0], coordinate[1], coordinate[2], n % reference.fragments) == expected, "multisampled pipe/bank XOR moves a fragment outside its tile"); + } + } +} + +void checkTransfer(const ReferenceCase& reference) { + MultisampleTextureLayout layout(reference.tileMode, reference.elementBytes, reference.fragments, 129u, 67u, 3u, 0x8100u); + std::vector source(layout.LinearBytes() + 32u, std::byte{0xa5}); + auto linear = std::span(source).subspan(16u, layout.LinearBytes()); + for (std::size_t n = 0; n < linear.size(); ++n) linear[n] = static_cast((n * 13u + n / 251u) & 255u); + const auto savedSource = source; + std::vector tiled(layout.Bytes() + 32u, std::byte{0xa5}); + auto guest = std::span(tiled).subspan(16u, layout.Bytes()); + layout.Tile(linear, guest); + Require(source == savedSource, "multisampled retile changed its source"); + std::vector covered(guest.size()); + std::size_t position = 0; + for (std::uint32_t layer = 0; layer < 3u; ++layer) { + for (std::uint32_t y = 0; y < 67u; ++y) { + for (std::uint32_t x = 0; x < 129u; ++x) { + for (std::uint32_t fragment = 0; fragment < reference.fragments; ++fragment) { + const auto address = layout.Offset(x, y, layer, fragment); + for (std::uint32_t channel = 0; channel < reference.elementBytes; ++channel) { + Require(address + channel < guest.size() && !covered[address + channel], "multisampled element addresses overlap or exceed the allocation"); + covered[address + channel] = true; + Require(guest[address + channel] == linear[position++], "multisampled retile changed element bytes"); + } + } + } + } + } + for (std::size_t n = 0; n < guest.size(); ++n) Require(covered[n] || guest[n] == std::byte{0xa5}, "multisampled retile changed tile padding"); + Require(std::all_of(tiled.begin(), tiled.begin() + 16u, [](auto value) { return value == std::byte{0xa5}; }) && std::all_of(tiled.end() - 16u, tiled.end(), [](auto value) { return value == std::byte{0xa5}; }), "multisampled retile changed guard bytes"); + const auto savedTiled = tiled; + std::vector restored(source.size(), std::byte{0xa5}); + layout.Detile(guest, std::span(restored).subspan(16u, layout.LinearBytes())); + Require(restored == source && tiled == savedTiled, "multisampled detile changed element, source or guard bytes"); +} + +void checkInverse(const ReferenceCase& reference) { + const auto byteBits = static_cast(std::countr_zero(reference.elementBytes)); + const auto fragmentBits = static_cast(std::countr_zero(reference.fragments)); + const auto widthBits = static_cast(std::countr_zero(reference.blockWidth)); + for (const auto xorBytes : {0u, 0x4000u, 0x8000u, 0xcf00u}) { + const MultisampleTextureLayout layout(reference.tileMode, reference.elementBytes, reference.fragments, reference.blockWidth * 4u, reference.blockHeight * 4u, 8u, xorBytes); + for (const auto origin : {std::array{0u, 0u, 0u}, std::array{reference.blockWidth * 2u, reference.blockHeight * 3u, 7u}}) { + const auto base = static_cast(layout.Offset(origin[0], origin[1], origin[2], 0u) & 65535u); + const auto step = xorBytes == 0u && origin[2] == 0u ? 1u : 257u; + for (std::uint32_t packed = 0u; packed < 65536u; packed += step) { + const auto byte = packed & (reference.elementBytes - 1u); + const auto fragment = (packed >> byteBits) & (reference.fragments - 1u); + const auto x = (packed >> (byteBits + fragmentBits)) & (reference.blockWidth - 1u); + const auto y = packed >> (byteBits + fragmentBits + widthBits); + const auto physical = static_cast((layout.Offset(origin[0] + x, origin[1] + y, origin[2], fragment) + byte) & 65535u) ^ base; + std::uint32_t restored = 0u; + for (std::uint32_t bit = 0; bit < 16u; ++bit) restored |= static_cast(std::popcount(physical & layout.InverseBlockEquation()[bit]) & 1u) << bit; + Require(restored == packed, "multisampled inverse equation changed a byte, fragment or pixel coordinate"); + } + } + } +} + +void checkRefusals() { + for (const auto& extent : {std::array{0u, 3u, 1u}, std::array{5u, 0u, 1u}, std::array{16385u, 3u, 1u}, std::array{5u, 16385u, 1u}, std::array{5u, 3u, 0u}, std::array{5u, 3u, 8193u}}) { + requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kR64KBX, 8u, 8u, extent[0], extent[1], extent[2]); }); + } + for (auto elementBytes : {0u, 3u, 32u}) requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kR64KBX, elementBytes, 8u, 5u, 3u, 1u); }); + for (auto fragments : {0u, 1u, 3u, 16u}) requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kR64KBX, 8u, fragments, 5u, 3u, 1u); }); + for (auto tileMode : {TextureTileMode::kLinear, TextureTileMode::kStandard4KB, TextureTileMode::kStandard64KB, TextureTileMode::kS64KBX, TextureTileMode::kD64KBX}) requireRefusal([&] { MultisampleTextureLayout invalid(tileMode, 8u, 8u, 5u, 3u, 1u); }); + for (auto elementBytes : {8u, 16u}) requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kZ64KBX, elementBytes, 8u, 5u, 3u, 1u); }); + for (auto xorBytes : {1u, 255u, 0x1000u, 0x2000u, 0x10000u}) requireRefusal([&] { MultisampleTextureLayout invalid(TextureTileMode::kR64KBX, 8u, 8u, 5u, 3u, 1u, xorBytes); }); + + MultisampleTextureLayout layout(TextureTileMode::kR64KBX, 8u, 8u, 5u, 3u, 2u); + for (const auto& coordinate : {std::array{5u, 0u, 0u, 0u}, std::array{0u, 3u, 0u, 0u}, std::array{0u, 0u, 2u, 0u}, std::array{0u, 0u, 0u, 8u}}) requireRefusal([&] { layout.Offset(coordinate[0], coordinate[1], coordinate[2], coordinate[3]); }); + std::vector source(layout.Bytes(), std::byte{0xa5}), destination(layout.Bytes(), std::byte{0x5a}); + const auto savedSource = source, savedDestination = destination; + requireRefusal([&] { layout.Detile(std::span(source).first(layout.Bytes() - 1u), destination); }); + requireRefusal([&] { layout.Detile(source, std::span(destination).first(layout.LinearBytes() - 1u)); }); + requireRefusal([&] { layout.Tile(std::span(source).first(layout.LinearBytes() - 1u), destination); }); + requireRefusal([&] { layout.Tile(source, std::span(destination).first(layout.Bytes() - 1u)); }); + requireRefusal([&] { layout.Detile(source, source); }); + requireRefusal([&] { layout.Tile(source, source); }); + requireRefusal([&] { layout.Detile(source, std::span(source).subspan(8u)); }); + Require(source == savedSource && destination == savedDestination, "refused multisampled transfer changed a buffer"); +} + +} + +int main() { + try { + for (const auto& reference : kReferences) { + checkReference(reference); + checkTransfer(reference); + checkInverse(reference); + } + checkRefusals(); + std::cout << "Multisampled texture layout reference, fragment, layer, padding and refusal 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/NopDataWritePacket.cpp b/core/libs/prx/libSceAgcDriver/tests/NopDataWritePacket.cpp new file mode 100644 index 0000000000..587bc082bc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/NopDataWritePacket.cpp @@ -0,0 +1,38 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceAgcDriverNopDataWritePacket(void* destination, uint32_t address, const void* data, uint32_t bytes); + +int main() { + std::array storage; + storage.fill(std::byte{0xaa}); + const std::array data{std::byte{0x11}, std::byte{0x22}, std::byte{0x33}, std::byte{0x44}, std::byte{0x55}}; + const auto words = sceAgcDriverNopDataWritePacket(storage.data() + 4, 0x12345678, data.data(), static_cast(data.size())); + std::uint32_t header = 0; + std::uint32_t address = 0; + std::memcpy(&header, storage.data() + 4, sizeof(header)); + std::memcpy(&address, storage.data() + 8, sizeof(address)); + if (words != 4 || header != 0xc0021000U || address != 0x12345678U) return 1; + if (!std::equal(data.begin(), data.end(), storage.begin() + 12)) return 2; + if (storage[17] != std::byte{0} || storage[18] != std::byte{0} || storage[19] != std::byte{0}) return 3; + if (storage[3] != std::byte{0xaa} || storage[20] != std::byte{0xaa}) return 4; + + storage.fill(std::byte{0xaa}); + const auto emptyWords = sceAgcDriverNopDataWritePacket(storage.data() + 4, 0x87654321, nullptr, 5); + std::memcpy(&header, storage.data() + 4, sizeof(header)); + std::memcpy(&address, storage.data() + 8, sizeof(address)); + if (emptyWords != 2 || header != 0xc0021000U || address != 0x87654321U) return 5; + if (storage[12] != std::byte{0} || storage[19] != std::byte{0} || storage[20] != std::byte{0xaa}) return 6; + + storage.fill(std::byte{0xaa}); + const auto zeroBytesWords = sceAgcDriverNopDataWritePacket(storage.data() + 4, 0x01020304, data.data(), 0); + std::memcpy(&header, storage.data() + 4, sizeof(header)); + std::memcpy(&address, storage.data() + 8, sizeof(address)); + if (zeroBytesWords != 2 || header != 0xc0001000U || address != 0x01020304U) return 7; + if (storage[12] != std::byte{0xaa}) return 8; + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp b/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp index 7689e5d90b..25332fe672 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp @@ -34,8 +34,9 @@ struct Use { Images attachments; bool anyReads = false; bool anyWrites = false; + Ranges writeOnly; - PassAccess View() const { return {reads, writes, images, attachments, anyReads, anyWrites}; } + PassAccess View() const { return {reads, writes, images, attachments, anyReads, anyWrites, writeOnly}; } }; void testRangeSet() { @@ -184,12 +185,82 @@ void testAttachments() { } +void testWriteOnly() { + PassHazards hazards; + hazards.BeginPass(); + Use feedback; + feedback.writeOnly = {{0x1000, 0x8f80}}; + hazards.Add(feedback.View()); + Expect(hazards.Check(feedback.View(), true) == PassHazard::None, "draws only storing to the same buffer continue the pass"); + Use reader; + reader.reads = {{0x2000, 0x2100}}; + Expect(hazards.Check(reader.View(), true) == PassHazard::ReadAfterWrite, "a read of write-only stores of the pass"); + Use writer; + writer.writes = {{0x8000, 0x9000}}; + Expect(hazards.Check(writer.View(), true) == PassHazard::WriteAfterWrite, "a read-write use after write-only stores"); + Use unknownReader; + unknownReader.anyReads = true; + Expect(hazards.Check(unknownReader.View(), true) == PassHazard::UnknownRead, "unknown reads after write-only stores"); + Use elsewhere; + elsewhere.writeOnly = {{0x9000, 0xa000}}; + elsewhere.reads = {{0xa000, 0xb000}}; + Expect(hazards.Check(elsewhere.View(), true) == PassHazard::None, "write-only stores and reads elsewhere"); + hazards.Clear(); + hazards.BeginPass(); + Use readWrite; + readWrite.reads = {{0x1000, 0x2000}}; + readWrite.writes = {{0x1000, 0x2000}}; + hazards.Add(readWrite.View()); + Use storesOver; + storesOver.writeOnly = {{0x1800, 0x1900}}; + Expect(hazards.Check(storesOver.View(), true) == PassHazard::WriteAfterWrite, "write-only stores after a read-write use"); + hazards.Clear(); + hazards.BeginPass(); + Use readOnly; + readOnly.reads = {{0x1000, 0x2000}}; + hazards.Add(readOnly.View()); + Expect(hazards.Check(storesOver.View(), true) == PassHazard::WriteAfterRead, "write-only stores after a read"); + hazards.Clear(); + hazards.BeginPass(); + Use unknownWriter; + unknownWriter.anyWrites = true; + unknownWriter.anyReads = true; + hazards.Add(feedback.View()); + Expect(hazards.Check(unknownWriter.View(), true) == PassHazard::UnknownWrite, "unknown writes after write-only stores"); + hazards.Clear(); + hazards.Add(unknownWriter.View()); + Expect(hazards.Check(feedback.View(), true) == PassHazard::UnknownWrite, "write-only stores after unknown writes"); + hazards.Clear(); + Use unknownRead; + unknownRead.anyReads = true; + hazards.Add(unknownRead.View()); + Expect(hazards.Check(feedback.View(), true) == PassHazard::UnknownRead, "write-only stores after unknown reads"); + hazards.Clear(); + hazards.BeginPass(); + hazards.Add(feedback.View()); + hazards.SealWriteOnly(); + Expect(hazards.Check(feedback.View(), true) == PassHazard::WriteAfterWrite, "write-only stores over stores sealed by a flush"); + Use afterSeal; + afterSeal.writeOnly = {{0x9000, 0xa000}}; + Expect(hazards.Check(afterSeal.View(), true) == PassHazard::None, "write-only stores elsewhere after a seal"); + hazards.Add(afterSeal.View()); + Expect(hazards.Check(afterSeal.View(), true) == PassHazard::None, "write-only stores after a seal continue the pass among themselves"); + hazards.Clear(); + Expect(hazards.Check(feedback.View(), true) == PassHazard::None, "a cleared seal"); + hazards.Clear(); + hazards.Add(feedback.View()); + Expect(!hazards.Empty(), "write-only stores are tracked"); + hazards.Clear(); + Expect(hazards.Empty(), "cleared write-only stores"); +} + int main() { testRangeSet(); testRanges(); testUnknown(); testImages(); testAttachments(); + testWriteOnly(); if (failures != 0) { std::fprintf(stderr, "%d pass hazard checks failed\n", failures); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp new file mode 100644 index 0000000000..b5db144ac8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp @@ -0,0 +1,730 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" +#include "prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +using namespace AgcDriver::Graphics; +using AgcDriver::OverlayContent; +using AgcDriver::OverlayCorner; +using AgcDriver::OverlayMetrics; +using AgcDriver::OverlayRect; +using AgcDriver::PerformanceOverlay; +using AgcDriver::PresentationPass; + +constexpr int VulkanTestSkipped = 77; +constexpr std::uint64_t Bgra8 = 0x8000000000000000ull; + +std::optional selectDevice(std::span devices, std::string_view request) { + const auto rankDeviceType = [](VkPhysicalDeviceType type) { + switch (type) { + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3; + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 2; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 1; + default: return 0; + } + }; + std::optional selected; + int selectedRank = -1; + for (std::size_t index = 0; index < devices.size(); ++index) { + const auto& candidate = devices[index]; + if (candidate.apiVersion < VK_API_VERSION_1_1) continue; + const std::string_view name(candidate.deviceName); + if (!request.empty() && std::search(name.begin(), name.end(), request.begin(), request.end(), [](char left, char right) { + return std::tolower(static_cast(left)) == std::tolower(static_cast(right)); + }) == name.end()) continue; + const int rank = request.empty() ? rankDeviceType(candidate.deviceType) : 4; + if (rank <= selectedRank) continue; + selected = index; + selectedRank = rank; + } + return selected; +} + +void deviceSelectionTests() { + const auto properties = [](const char* name, VkPhysicalDeviceType type) { + VkPhysicalDeviceProperties result{}; + result.apiVersion = VK_API_VERSION_1_1; + result.deviceType = type; + std::memcpy(result.deviceName, name, std::strlen(name) + 1u); + return result; + }; + std::array devices{ + properties("AMD Radeon Graphics", VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU), + properties("NVIDIA GeForce RTX 5080", VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU), + properties("Virtual GPU", VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU), + properties("CPU", VK_PHYSICAL_DEVICE_TYPE_CPU), + }; + Require(!selectDevice({}, {}).has_value(), "an empty device list selected a device"); + Require(selectDevice(devices, {}) == 1u, "default selection did not prefer the discrete GPU"); + Require(selectDevice(devices, "AMD") == 0u, "the AMD request did not select the integrated GPU"); + Require(selectDevice(devices, "NVIDIA") == 1u, "the NVIDIA request did not select the discrete GPU"); + Require(selectDevice(devices, "aMd rAdEoN") == 0u, "a requested integrated GPU lost to an unrelated discrete GPU"); + Require(selectDevice(devices, "rtx 5080") == 1u, "a partial GPU name did not match without regard to case"); + Require(selectDevice(devices, "Virtual") == 2u, "a requested virtual GPU was not selected"); + Require(selectDevice(devices, "cpu") == 3u, "a requested CPU device was not selected"); + Require(!selectDevice(devices, "missing").has_value(), "an unmatched request selected another GPU"); + Require(!selectDevice(devices, "NVIDIA GeForce RTX 5080 with a longer name").has_value(), "a request longer than the device name matched"); + Require(selectDevice(std::span(devices).subspan(2), {}) == 0u, "default selection did not prefer the virtual GPU to the CPU"); + devices[1].apiVersion = VK_API_VERSION_1_0; + Require(selectDevice(devices, {}) == 0u, "default selection did not prefer the integrated GPU to the virtual GPU"); + Require(!selectDevice(devices, "NVIDIA").has_value(), "a requested device without Vulkan 1.1 was selected"); + devices[1] = devices[0]; + Require(selectDevice(devices, {}) == 0u && selectDevice(devices, "AMD") == 0u, "equally ranked matches did not retain enumeration order"); + for (auto& device : devices) device.apiVersion = VK_API_VERSION_1_0; + Require(!selectDevice(devices, {}).has_value(), "a list without Vulkan 1.1 selected a device"); + std::puts("performance-overlay device selection CPU tests passed"); +} + +class Device { +public: + Device() { +#ifdef _WIN32 + library = SDL_LoadObject("vulkan-1.dll"); +#else + library = SDL_LoadObject("libvulkan.so.1"); +#endif + Require(library != nullptr, "cannot load Vulkan"); + try { + instanceProc = reinterpret_cast(SDL_LoadFunction(library, "vkGetInstanceProcAddr")); + Require(instanceProc != nullptr, "missing Vulkan instance resolver"); + VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + application.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + info.pApplicationInfo = &application; + Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); + std::uint32_t count = 0; + const auto enumerate = function("vkEnumeratePhysicalDevices"); + Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices"); + Require(count != 0, "no Vulkan device"); + std::vector devices(count); + Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); + const auto physicalProperties = function("vkGetPhysicalDeviceProperties"); + std::vector deviceProperties(devices.size()); + for (std::size_t index = 0; index < devices.size(); ++index) physicalProperties(devices[index], &deviceProperties[index]); + const char* requestedName = std::getenv("ANYPS5_GPU"); + const auto selectedIndex = selectDevice(deviceProperties, requestedName != nullptr ? requestedName : ""); + Require(selectedIndex.has_value(), requestedName != nullptr && *requestedName != '\0' ? std::string("no Vulkan 1.1 device matching ANYPS5_GPU=\"") + requestedName + "\"" : "no Vulkan 1.1 device"); + context.physical = devices[*selectedIndex]; + const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); + queues(context.physical, &count, nullptr); + std::vector families(count); + queues(context.physical, &count, families.data()); + std::uint32_t family = 0; + while (family < count && (families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0) ++family; + Require(family < count, "no Vulkan graphics queue"); + timestampBits = families[family].timestampValidBits; + const auto enumerateExtensions = function("vkEnumerateDeviceExtensionProperties"); + Check(enumerateExtensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); + std::vector extensions(count); + Check(enumerateExtensions(context.physical, nullptr, &count, extensions.data()), "vkEnumerateDeviceExtensionProperties"); + memoryBudget = std::any_of(extensions.begin(), extensions.end(), [](const VkExtensionProperties& extension) { return std::string(extension.extensionName) == VK_EXT_MEMORY_BUDGET_EXTENSION_NAME; }); + const char* const budgetExtension = VK_EXT_MEMORY_BUDGET_EXTENSION_NAME; + const float priority = 1; + VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue.queueFamilyIndex = family; + queue.queueCount = 1; + queue.pQueuePriorities = &priority; + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + device.queueCreateInfoCount = 1; + device.pQueueCreateInfos = &queue; + device.enabledExtensionCount = memoryBudget ? 1 : 0; + device.ppEnabledExtensionNames = memoryBudget ? &budgetExtension : nullptr; + Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + if (memoryBudget) context.memoryProperties2 = function("vkGetPhysicalDeviceMemoryProperties2"); + context.deviceProc = function("vkGetDeviceProcAddr"); + function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + context.limits = properties.limits; + name = properties.deviceName; + context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); + context.imageFormatProperties = function("vkGetPhysicalDeviceImageFormatProperties"); + context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); + VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + pool.queueFamilyIndex = family; + Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); + } catch (...) { + release(); + throw; + } + } + + ~Device() { release(); } + const Context& GetContext() const { return context; } + const std::string& Name() const { return name; } + std::uint32_t TimestampBits() const { return timestampBits; } + bool MemoryBudget() const { return memoryBudget; } + + void VerifySelection() const { + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + std::cout << "Selected GPU name=" << properties.deviceName << " vendor=" << properties.vendorID << " device=" << properties.deviceID << '\n'; + const char* request = std::getenv("ANYPS5_GPU"); + Require(selectDevice(std::span(&properties, 1), request != nullptr ? request : "").has_value(), "the performance-overlay fixture physical device does not match ANYPS5_GPU"); + } + +private: + template + TFunction function(const char* functionName) const { + const auto result = reinterpret_cast(instanceProc(instance, functionName)); + Require(result != nullptr, functionName); + return result; + } + + void release() noexcept { + if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); + context.bufferPool.reset(); + if (context.device != VK_NULL_HANDLE) function("vkDestroyDevice")(context.device, nullptr); + if (instance != VK_NULL_HANDLE) function("vkDestroyInstance")(instance, nullptr); + if (library != nullptr) SDL_UnloadObject(library); + } + + void* library = nullptr; + PFN_vkGetInstanceProcAddr instanceProc = nullptr; + VkInstance instance = VK_NULL_HANDLE; + Context context{}; + std::string name; + std::uint32_t timestampBits = 0; + bool memoryBudget = false; +}; + +class Image { +public: + Image(const Context& context, VkFormat format, VkExtent2D extent, VkImageUsageFlags usage, VkImageCreateFlags flags = 0) : context(context), extent(extent) { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.flags = flags; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {extent.width, extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = usage; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage performance overlay test"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + const auto allocated = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory); + if (allocated != VK_SUCCESS) context.Function("vkDestroyImage")(context.device, image, nullptr); + Check(allocated, "vkAllocateMemory performance overlay test"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory performance overlay test"); + } + ~Image() { + context.Function("vkDestroyImage")(context.device, image, nullptr); + context.Function("vkFreeMemory")(context.device, memory, nullptr); + } + Image(const Image&) = delete; + Image& operator=(const Image&) = delete; + VkImage Handle() const { return image; } + VkExtent2D Extent() const { return extent; } + +private: + const Context& context; + VkExtent2D extent; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; +}; + +void transition(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess) { + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.srcAccessMask = sourceAccess; + barrier.dstAccessMask = destinationAccess; + barrier.oldLayout = from; + barrier.newLayout = to; + barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdPipelineBarrier")(commands, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier); +} + +void readImage(const Context& context, VkCommandBuffer commands, const Image& image, Buffer& destination) { + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {image.Extent().width, image.Extent().height, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, image.Handle(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); +} + +std::vector wordsOf(Buffer& buffer, VkExtent2D extent) { + std::vector words(static_cast(extent.width) * extent.height); + std::memcpy(words.data(), buffer.Bytes().data(), words.size() * sizeof(std::uint32_t)); + return words; +} + +void setEnvironment(const char* name, const char* value) { +#ifdef _WIN32 + _putenv_s(name, value); +#else + setenv(name, value, 1); +#endif +} + +bool inside(const OverlayRect& rect, std::int32_t x, std::int32_t y) { + return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height; +} + +OverlayContent sixtyFps() { + AgcDriver::OverlayStatistics statistics; + const auto start = AgcDriver::OverlayStatistics::Clock::time_point{} + std::chrono::hours(1); + for (int i = 0; i <= 60; ++i) statistics.AddPresent(start + std::chrono::nanoseconds(16'666'667) * i, 0, 0); + auto content = AgcDriver::ComposeOverlay(statistics, false, false); + Require(content.above.front() == "FPS: 60.0", "60 presents a second compose as '" + content.above.front() + "'"); + return content; +} + +std::vector clearAndOverlay(const Context& context, PerformanceOverlay* overlay, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, const VkClearColorValue& clear, const OverlayContent& content, OverlayCorner corner, OverlayRect* bounds) { + Image destination(context, format, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + Buffer readback(context, static_cast(extent.width) * extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdClearColorImage")(commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &range); + if (overlay != nullptr) { + *bounds = overlay->Record(commands, 0, destination.Handle(), format, colorSpace, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, content, corner); + } else { + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + } + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + } + if (overlay != nullptr) overlay->Release(0); + return wordsOf(readback, extent); +} + +void onlyInside(const std::vector& reference, const std::vector& drawn, VkExtent2D extent, const OverlayRect& bounds, const std::string& what, bool everyPixel = true) { + std::size_t changed = 0; + for (std::int32_t y = 0; y < static_cast(extent.height); ++y) { + for (std::int32_t x = 0; x < static_cast(extent.width); ++x) { + const auto index = static_cast(y) * extent.width + static_cast(x); + if (!inside(bounds, x, y)) { + Require(drawn[index] == reference[index], what + ": the overlay changed pixel " + std::to_string(x) + "," + std::to_string(y) + " outside its panel"); + continue; + } + if (drawn[index] != reference[index]) ++changed; + else Require(!everyPixel, what + ": the overlay left pixel " + std::to_string(x) + "," + std::to_string(y) + " of its panel unchanged"); + } + } + Require(changed != 0, what + ": the overlay changed nothing"); +} + +int channel(std::uint32_t word, unsigned shift) { + return static_cast((word >> shift) & 0xffu); +} + +void requireTextDrawn(const std::vector& drawn, VkExtent2D extent, const OverlayRect& bounds, std::int32_t scale, std::uint32_t frameWord, const std::string& line, std::int32_t row, std::size_t first = 0, std::size_t count = std::string::npos) { + const auto end = count == std::string::npos ? line.size() : std::min(line.size(), first + count); + for (std::size_t index = first; index < end; ++index) { + const auto& glyph = AgcDriver::OverlayGlyph(line[index]); + const auto column = static_cast(index); + for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { + for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) { + const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast(gy)] & (0x10u >> gx)) != 0; + for (std::int32_t py = 0; py < scale; ++py) { + for (std::int32_t px = 0; px < scale; ++px) { + const auto x = bounds.x + (OverlayMetrics::Padding + column * OverlayMetrics::Advance + gx) * scale + px; + const auto y = bounds.y + (row + gy) * scale + py; + const auto word = drawn[static_cast(y) * extent.width + static_cast(x)]; + const auto where = std::string("'") + line[index] + "' of '" + line + "' glyph pixel " + std::to_string(gx) + "," + std::to_string(gy); + Require(channel(word, 24) == channel(frameWord, 24), where + " changed the alpha"); + if (set) { + Require((word & 0x00ffffffu) == 0x00ffffffu, where + " is not white text: " + Hex(word)); + continue; + } + for (const unsigned shift : {0u, 8u, 16u}) { + const int expected = static_cast(std::lround(channel(frameWord, shift) * 0.4)); + Require(std::abs(channel(word, shift) - expected) <= 1, where + " is not the panel over the frame: " + std::to_string(channel(word, shift)) + " for " + std::to_string(expected)); + } + } + } + } + } + } +} + +AgcDriver::OverlayDetails measuredDetails(const Context& context, VkExtent2D display, VkExtent2D window) { + AgcDriver::OverlayDetails details; + details.vram = AgcDriver::ReadVramBudget(context); + details.ram = AgcDriver::ReadProcessPrivateBytes(); + details.display = display; + details.window = window; + return details; +} + +AgcDriver::OverlayDetails fixedDetails(VkExtent2D display, VkExtent2D window) { + constexpr double GiB = 1024.0 * 1024.0 * 1024.0; + AgcDriver::OverlayDetails details; + details.vram = AgcDriver::OverlayMemory{static_cast(6.1 * GiB), static_cast(7.8 * GiB)}; + details.ram = static_cast(3.2 * GiB); + details.display = display; + details.window = window; + return details; +} + +OverlayContent fullPanel(const AgcDriver::OverlayDetails& details) { + AgcDriver::OverlayStatistics statistics; + const auto start = AgcDriver::OverlayStatistics::Clock::time_point{} + std::chrono::hours(1); + for (std::uint64_t i = 0; i <= 60; ++i) { + AgcDriver::OverlayCounters counters; + counters.gpuNanoseconds = i * 4'200'000u; + counters.driverNanoseconds = i * 1'300'000u; + counters.draws = i * 1240; + counters.dispatches = i * 85; + counters.pipelines = i < 50 ? 1500 : 1503; + statistics.AddPresent(start + std::chrono::nanoseconds(16'666'667) * i, counters); + } + auto content = AgcDriver::ComposeOverlay(statistics, true, true, details); + Require(content.above.size() == 3 && content.above[0] == "PROFILED" && content.above[1] == "FPS: 60.0" && content.below.size() == 5u + (details.vram ? 1u : 0u) + (details.ram ? 1u : 0u) + (details.display.width != 0 ? 1u : 0u), "the full panel composes as '" + content.above.front() + "' with " + std::to_string(content.below.size()) + " lines below its graph"); + return content; +} + +void sdrOverlayTest(const Context& context, PerformanceOverlay& overlay) { + const VkExtent2D extent{1280, 720}; + VkClearColorValue clear{}; + clear.float32[0] = 0.75f; + clear.float32[1] = 0.5f; + clear.float32[2] = 0.25f; + clear.float32[3] = 1.0f; + const auto content = sixtyFps(); + OverlayRect bounds{}; + const auto reference = clearAndOverlay(context, nullptr, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, nullptr); + const auto drawn = clearAndOverlay(context, &overlay, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, &bounds); + Require(bounds.x == AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopLeft).bounds.x && bounds.width > 0 && bounds.height > 0, "the overlay drew another panel than its layout"); + onlyInside(reference, drawn, extent, bounds, "SDR 1280x720"); + const auto scale = AgcDriver::OverlayScale(extent, content); + const std::string digits = "60.0"; + const auto firstColumn = content.above.front().find(digits); + Require(firstColumn != std::string::npos, "the FPS line has no '60.0'"); + requireTextDrawn(drawn, extent, bounds, scale, reference.front(), content.above.front(), OverlayMetrics::Padding, firstColumn, digits.size()); + std::printf("SDR overlay: panel %dx%d at %d,%d (scale %d), FPS digits drawn bit for bit\n", bounds.width, bounds.height, bounds.x, bounds.y, scale); +} + +void hdrOverlayTest(const Context& context, PerformanceOverlay& overlay) { + const auto format = VK_FORMAT_A2B10G10R10_UNORM_PACK32; + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + if ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) == 0) { + std::printf("A2B10G10R10 cannot be blended into here; HDR10 overlay not exercised\n"); + return; + } + const VkExtent2D extent{1920, 1080}; + VkClearColorValue clear{}; + clear.float32[0] = 0.4f; + clear.float32[1] = 0.5f; + clear.float32[2] = 0.6f; + clear.float32[3] = 1.0f; + const auto content = sixtyFps(); + OverlayRect bounds{}; + const auto reference = clearAndOverlay(context, nullptr, format, VK_COLOR_SPACE_HDR10_ST2084_EXT, extent, clear, content, OverlayCorner::BottomRight, nullptr); + const auto drawn = clearAndOverlay(context, &overlay, format, VK_COLOR_SPACE_HDR10_ST2084_EXT, extent, clear, content, OverlayCorner::BottomRight, &bounds); + onlyInside(reference, drawn, extent, bounds, "HDR10 1920x1080"); + const auto scale = AgcDriver::OverlayScale(extent, content); + const auto& six = AgcDriver::OverlayGlyph('6'); + const auto column = static_cast(content.above.front().find('6')); + std::size_t checked = 0; + for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { + for (std::int32_t gx = 0; gx < OverlayMetrics::GlyphWidth; ++gx) { + if ((six[static_cast(gy)] & (0x10u >> gx)) == 0) continue; + const auto x = bounds.x + (OverlayMetrics::Padding + column * OverlayMetrics::Advance + gx) * scale + scale / 2; + const auto y = bounds.y + (OverlayMetrics::Padding + gy) * scale + scale / 2; + const auto word = drawn[static_cast(y) * extent.width + static_cast(x)]; + for (const unsigned shift : {0u, 10u, 20u}) { + const int code = static_cast((word >> shift) & 0x3ffu); + Require(std::abs(code - 594) <= 1, "HDR10 text is not at 203 nits: PQ code " + std::to_string(code) + " for 594"); + } + Require((word >> 30u) == (reference.front() >> 30u), "HDR10 text changed the alpha"); + ++checked; + } + } + Require(checked != 0, "no HDR10 text pixel was checked"); + std::printf("HDR10 overlay: text at PQ code 594 (203 nits), nothing changed outside the %dx%d panel\n", bounds.width, bounds.height); +} + +void cornerTest(const Context& context, PerformanceOverlay& overlay) { + const VkExtent2D extent{640, 360}; + VkClearColorValue clear{}; + clear.float32[1] = 1.0f; + clear.float32[3] = 1.0f; + const auto content = sixtyFps(); + const auto reference = clearAndOverlay(context, nullptr, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, nullptr); + for (const auto corner : {OverlayCorner::TopLeft, OverlayCorner::TopRight, OverlayCorner::BottomLeft, OverlayCorner::BottomRight}) { + OverlayRect bounds{}; + const auto drawn = clearAndOverlay(context, &overlay, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, corner, &bounds); + const auto expected = AgcDriver::LayOutOverlay(content, extent, corner).bounds; + Require(bounds.x == expected.x && bounds.y == expected.y && bounds.width == expected.width && bounds.height == expected.height, "the drawn panel is not where the layout puts it"); + onlyInside(reference, drawn, extent, bounds, "corner " + std::to_string(static_cast(corner))); + } + std::printf("overlay corners: tl, tr, bl and br drawn where the layout puts them\n"); +} + +void fullPanelTest(const Device& device, PerformanceOverlay& overlay) { + const auto& context = device.GetContext(); + const VkExtent2D extent{1920, 1080}; + auto details = measuredDetails(context, extent, extent); + Require(details.ram.has_value(), "the process's private memory cannot be read"); + Require(details.vram.has_value() == device.MemoryBudget(), device.MemoryBudget() ? "VK_EXT_memory_budget gives no device-local budget" : "VRAM is read without VK_EXT_memory_budget"); + auto withoutBudget = context; + withoutBudget.memoryProperties2 = nullptr; + Require(!AgcDriver::ReadVramBudget(withoutBudget).has_value(), "VRAM is read without vkGetPhysicalDeviceMemoryProperties2"); + VkClearColorValue clear{}; + clear.float32[0] = 0.75f; + clear.float32[1] = 0.5f; + clear.float32[2] = 0.25f; + clear.float32[3] = 1.0f; + const auto content = fullPanel(details); + OverlayRect bounds{}; + const auto reference = clearAndOverlay(context, nullptr, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, nullptr); + const auto drawn = clearAndOverlay(context, &overlay, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, &bounds); + const auto layout = AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopLeft); + Require(bounds.x == layout.bounds.x && bounds.y == layout.bounds.y && bounds.width == layout.bounds.width && bounds.height == layout.bounds.height, "the full panel is not drawn where its layout puts it"); + onlyInside(reference, drawn, extent, bounds, "full panel 1920x1080"); + std::int32_t row = OverlayMetrics::Padding; + for (const auto& line : content.above) { + requireTextDrawn(drawn, extent, bounds, layout.scale, reference.front(), line, row); + row += OverlayMetrics::LinePitch; + } + row += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap; + for (const auto& line : content.below) { + requireTextDrawn(drawn, extent, bounds, layout.scale, reference.front(), line, row); + row += OverlayMetrics::LinePitch; + } + std::printf("full panel at 1920x1080: %dx%d at %d,%d (scale %d, %.1f%% of the height), every line drawn bit for bit, nothing changed outside it:\n", bounds.width, bounds.height, bounds.x, bounds.y, layout.scale, 100.0 * bounds.height / extent.height); + for (const auto& line : content.above) std::printf(" %s\n", line.c_str()); + std::printf(" (graph of %zu frame times)\n", content.frameTimes.size()); + for (const auto& line : content.below) std::printf(" %s\n", line.c_str()); +} + +void notePresentTest(const Device& device) { + const auto& context = device.GetContext(); + const VkExtent2D extent{1280, 720}; + PerformanceOverlay overlay(context, 1); + overlay.NotePresent(AgcDriver::OverlayStatistics::Clock::now(), VkExtent2D{640, 360}); + const auto& details = overlay.Details(); + Require(details.ram.has_value() && details.vram.has_value() == device.MemoryBudget() && details.display.width == 640 && details.display.height == 360, "a shown frame did not sample the memory or keep the display buffer size"); + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + OverlayRect bounds{}; + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + batch.SubmitAndWait(); + } + overlay.Release(0); + auto shown = details; + shown.window = extent; + const auto expected = AgcDriver::ComposeOverlay(overlay.Statistics(), AgcDriver::PerformanceCounters::GpuEnabled(), AgcDriver::PerformanceCounters::DriverEnabled(), shown); + Require(expected.below.back() == "RES: 640X360 / 1280X720", "the RES line reads '" + expected.below.back() + "'"); + const auto layout = AgcDriver::LayOutOverlay(expected, extent, PerformanceOverlay::Corner()); + Require(bounds.height == layout.bounds.height && bounds.width == layout.bounds.width, "a shown frame's panel does not hold the sampled VRAM, RAM and RES lines"); + std::printf("shown frame: memory sampled, %zu lines below the graph, the last '%s'\n", expected.below.size(), expected.below.back().c_str()); +} + +class Presenter { +public: + explicit Presenter(const Context& context) : context(context), pass(context, 1), overlay(context, 1), source(context, VK_FORMAT_B8G8R8A8_UNORM, {256, 144}, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT) { + Buffer upload(context, static_cast(256) * 144 * 4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::uint32_t seed = 0x2545f491u; + for (auto& byte : upload.Bytes()) { + seed = seed * 1664525u + 1013904223u; + byte = static_cast(seed >> 24u); + } + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {256, 144, 1}; + context.Function("vkCmdCopyBufferToImage")(commands, upload.Handle(), source.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + batch.SubmitAndWait(); + } + + std::vector Drawn(OverlayRect* bounds) { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, Extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + Buffer readback(context, static_cast(Extent.width) * Extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const bool visible = PerformanceOverlay::Visible(); + pass.Bind(0, source.Handle(), 256, 144, Bgra8, AgcDriver::SdrToneMapLuminanceNits); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + pass.RecordPresent(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, visible ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + if (visible) *bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, fullPanel(fixedDetails({256, 144}, Extent)), OverlayCorner::TopLeft); + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + } + pass.Release(0); + overlay.Release(0); + return wordsOf(readback, Extent); + } + + std::vector Blitted(OverlayRect* bounds) { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, Extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + Buffer readback(context, static_cast(Extent.width) * Extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const bool visible = PerformanceOverlay::Visible(); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkClearColorValue black{}; + black.float32[3] = 1.0f; + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdClearColorImage")(commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + AgcDriver::PresentationScaler::RecordBlitFrom(context, commands, source.Handle(), VK_IMAGE_LAYOUT_GENERAL, 256, 144, VK_FILTER_LINEAR, destination.Handle(), Extent.width, Extent.height); + if (visible) { + *bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, fullPanel(fixedDetails({256, 144}, Extent)), OverlayCorner::TopLeft); + } else { + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + } + transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT); + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + } + overlay.Release(0); + return wordsOf(readback, Extent); + } + + static constexpr VkExtent2D Extent{400, 300}; + +private: + const Context& context; + PresentationPass pass; + PerformanceOverlay overlay; + Image source; +}; + +void disabledIsUnchangedTest(const Context& context) { + Require(!PerformanceOverlay::Visible(), "the performance overlay is shown without ANYPS5_OVERLAY=1"); + Presenter presenter(context); + OverlayRect unused{}; + const auto drawnBefore = presenter.Drawn(&unused); + const auto blittedBefore = presenter.Blitted(&unused); + PerformanceOverlay::Toggle(); + Require(PerformanceOverlay::Visible(), "F10 does not show the performance overlay"); + OverlayRect bounds{}; + onlyInside(drawnBefore, presenter.Drawn(&bounds), Presenter::Extent, bounds, "presentation pass with the overlay", false); + onlyInside(blittedBefore, presenter.Blitted(&bounds), Presenter::Extent, bounds, "blit with the overlay", false); + PerformanceOverlay::Toggle(); + Require(!PerformanceOverlay::Visible(), "F10 does not hide the performance overlay again"); + Require(presenter.Drawn(&unused) == drawnBefore, "the presentation pass output differs after the overlay was hidden"); + Require(presenter.Blitted(&unused) == blittedBefore, "the blit output differs after the overlay was hidden"); + std::printf("overlay hidden: presentation pass and blit output bit-identical before and after showing it\n"); +} + +void costReport(const Device& device, PerformanceOverlay& overlay) { + const auto& context = device.GetContext(); + if (device.TimestampBits() == 0) { + std::printf("the graphics queue has no timestamps; overlay cost not measured\n"); + return; + } + VkQueryPoolCreateInfo info{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO}; + info.queryType = VK_QUERY_TYPE_TIMESTAMP; + info.queryCount = 2; + VkQueryPool queries = VK_NULL_HANDLE; + Check(context.Function("vkCreateQueryPool")(context.device, &info, nullptr, &queries), "vkCreateQueryPool overlay cost"); + const auto sampled = std::chrono::steady_clock::now(); + for (int run = 0; run < 100; ++run) static_cast(AgcDriver::ReadVramBudget(context)); + const auto vramUs = std::chrono::duration(std::chrono::steady_clock::now() - sampled).count() / 100; + std::printf("VRAM budget read in %.1f us (%s), once a second while the overlay is shown\n", vramUs, device.MemoryBudget() ? "VK_EXT_memory_budget" : "no VK_EXT_memory_budget"); + for (const VkExtent2D extent : {VkExtent2D{1920, 1080}, VkExtent2D{3840, 2160}}) { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + const auto content = fullPanel(fixedDetails(extent, extent)); + std::vector gpuMs; + std::vector cpuMs; + for (int run = 0; run < 12; ++run) { + CommandBatch batch(context); + const auto commands = batch.Handle(); + context.Function("vkCmdResetQueryPool")(commands, queries, 0, 2); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdWriteTimestamp")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, queries, 0); + const auto recordStart = std::chrono::steady_clock::now(); + static_cast(overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, content, OverlayCorner::TopLeft)); + cpuMs.push_back(std::chrono::duration(std::chrono::steady_clock::now() - recordStart).count()); + context.Function("vkCmdWriteTimestamp")(commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queries, 1); + batch.SubmitAndWait(); + overlay.Release(0); + std::array stamps{}; + Check(context.Function("vkGetQueryPoolResults")(context.device, queries, 0, 2, sizeof(stamps), stamps.data(), sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "vkGetQueryPoolResults overlay cost"); + if (run >= 2) gpuMs.push_back(static_cast(stamps[1] - stamps[0]) * context.limits.timestampPeriod / 1e6); + } + std::sort(gpuMs.begin(), gpuMs.end()); + std::sort(cpuMs.begin(), cpuMs.end()); + std::printf("full panel cost on %s at %ux%u: GPU median %.3f ms (min %.3f, max %.3f), recording median %.3f ms\n", device.Name().c_str(), extent.width, extent.height, gpuMs[gpuMs.size() / 2], gpuMs.front(), gpuMs.back(), cpuMs[cpuMs.size() / 2]); + } + context.Function("vkDestroyQueryPool")(context.device, queries, nullptr); +} + +} + +int main(int argc, char** argv) { + try { + const std::string_view mode = argc > 1 ? argv[1] : ""; + Require(argc <= 2 && (mode.empty() || mode == "device-selection" || mode == "device-selection-cpu"), "unknown performance-overlay test mode"); + if (mode == "device-selection-cpu") { + deviceSelectionTests(); + return 0; + } + setEnvironment("ANYPS5_OVERLAY", ""); + setEnvironment("ANYPS5_OVERLAY_POS", ""); + std::unique_ptr device; + try { + device = std::make_unique(); + } catch (const std::exception& error) { + const char* requestedName = std::getenv("ANYPS5_GPU"); + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr || (requestedName != nullptr && *requestedName != '\0')) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return VulkanTestSkipped; + } + device->VerifySelection(); + if (mode == "device-selection") return 0; + const auto& context = device->GetContext(); + { + PerformanceOverlay overlay(context, 1); + sdrOverlayTest(context, overlay); + hdrOverlayTest(context, overlay); + cornerTest(context, overlay); + fullPanelTest(*device, overlay); + costReport(*device, overlay); + } + notePresentTest(*device); + disabledIsUnchangedTest(context); + std::cout << "performance overlay 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/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp new file mode 100644 index 0000000000..b829a08c01 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -0,0 +1,500 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::OverlayContent; +using AgcDriver::OverlayCorner; +using AgcDriver::OverlayMetrics; +using AgcDriver::OverlayRect; +using AgcDriver::OverlayStatistics; +using Clock = OverlayStatistics::Clock; +using namespace std::chrono_literals; + +constexpr Clock::time_point Start = Clock::time_point{} + std::chrono::hours(1); + +void require(bool condition, const std::string& reason) { + if (!condition) throw std::runtime_error(reason); +} + +bool within(double value, double expected, double tolerance = 1e-9) { + return std::abs(value - expected) <= tolerance; +} + +void fpsWindowTests() { + OverlayStatistics statistics; + require(statistics.Fps() == 0 && statistics.LastFrameMs() == 0 && statistics.FrameTimes().empty(), "a new overlay shows statistics"); + statistics.AddPresent(Start, 0, 0); + require(statistics.Fps() == 0, "one present reads as a frame rate"); + for (int i = 1; i <= 120; ++i) statistics.AddPresent(Start + i * 10ms, 0, 0); + require(within(statistics.Fps(), 100.0), "a present every 10 ms reads as " + std::to_string(statistics.Fps()) + " FPS, not 100"); + statistics.AddPresent(Start + 1700ms, 0, 0); + require(within(statistics.Fps(), 51.0), "after a 500 ms stall the last second holds 52 presents, read as " + std::to_string(statistics.Fps()) + " FPS, not 51"); + statistics.AddPresent(Start + 3700ms, 0, 0); + require(within(statistics.Fps(), 0.5), "after a 2 s stall the last second holds one present, read as " + std::to_string(statistics.Fps()) + " FPS, not the last frame's 0.5"); + statistics.AddPresent(Start + 3750ms, 0, 0); + require(within(statistics.Fps(), 20.0), "two presents 50 ms apart read as " + std::to_string(statistics.Fps()) + " FPS, not 20"); + bool refused = false; + try { + statistics.AddPresent(Start + 3740ms, 0, 0); + } catch (const std::exception&) { + refused = true; + } + require(refused, "a present earlier than the previous one was accepted"); + statistics.Reset(); + require(statistics.Fps() == 0 && statistics.LastFrameMs() == 0 && statistics.FrameTimes().empty(), "a reset kept statistics"); +} + +void frameRingTests() { + OverlayStatistics statistics; + auto at = Start; + statistics.AddPresent(at, 0, 0); + for (int i = 1; i <= 4; ++i) { + at += std::chrono::milliseconds(i); + statistics.AddPresent(at, 0, 0); + } + const auto partial = statistics.FrameTimes(); + require(partial.size() == 4 && within(partial[0], 1) && within(partial[3], 4), "the frame times of a short run are not in present order"); + for (int i = 5; i <= 150; ++i) { + at += std::chrono::milliseconds(i); + statistics.AddPresent(at, 0, 0); + } + const auto times = statistics.FrameTimes(); + require(times.size() == OverlayStatistics::FrameHistory, "the ring holds " + std::to_string(times.size()) + " frames, not 120"); + for (std::size_t i = 0; i < times.size(); ++i) require(within(times[i], 31.0 + static_cast(i)), "frame " + std::to_string(i) + " of the ring is " + std::to_string(times[i]) + " ms"); + require(within(statistics.LastFrameMs(), 150), "the last frame is not the newest"); + const auto range = statistics.FrameTimeRange(); + require(within(range.minimum, 31) && within(range.maximum, 150) && within(range.average, 90.5), "the graph range of 31..150 ms reads as " + std::to_string(range.minimum) + "/" + std::to_string(range.average) + "/" + std::to_string(range.maximum)); +} + +void counterTests() { + OverlayStatistics statistics; + constexpr std::uint64_t Earlier = 1'000'000'000u; + for (std::uint64_t i = 0; i <= 60; ++i) statistics.AddPresent(Start + i * 16ms, Earlier + i * 4'000'000u, Earlier + i * 1'500'000u); + require(within(statistics.GpuMsPerFrame(), 4.0) && within(statistics.DriverMsPerFrame(), 1.5), "4 ms of GPU and 1.5 ms of driver time per frame read as " + std::to_string(statistics.GpuMsPerFrame()) + " and " + std::to_string(statistics.DriverMsPerFrame())); + statistics.AddPresent(Start + 61 * 16ms, 5, 5); + require(statistics.GpuMsPerFrame() == 0 && statistics.DriverMsPerFrame() == 0, "a counter that went back is read as time"); +} + +void formatTests() { + const std::pair cases[] = {{59.94, "59.9"}, {0, "0.0"}, {99.94, "99.9"}, {99.96, "100"}, {144.4, "144"}, {12345, "9999"}, {-3, "0.0"}, {std::nan(""), "0.0"}}; + for (const auto& [value, expected] : cases) require(AgcDriver::FormatOverlayValue(value) == expected, std::to_string(value) + " is shown as '" + AgcDriver::FormatOverlayValue(value) + "', not '" + expected + "'"); + const std::pair rates[] = {{0.5, "0.50"}, {1000.0 / 12000.0, "0.08"}, {0.05, "0.05"}, {0.004, "0.00"}, {0.994, "0.99"}, {0.996, "1.0"}, {1.5, "1.5"}, {59.94, "59.9"}, {144.4, "144"}, {12345, "9999"}, {0, "0.0"}, {-1, "0.0"}, {std::nan(""), "0.0"}}; + for (const auto& [value, expected] : rates) require(AgcDriver::FormatOverlayRate(value) == expected, "a rate of " + std::to_string(value) + " is shown as '" + AgcDriver::FormatOverlayRate(value) + "', not '" + expected + "'"); +} + +void lowRateTests() { + OverlayStatistics slow; + constexpr std::uint64_t Gpu[] = {30'000'000u, 45'000'000u, 20'000'000u}; + constexpr std::uint64_t Driver[] = {12'000'000u, 20'000'000u, 8'000'000u}; + std::uint64_t gpuTotal = 0; + std::uint64_t driverTotal = 0; + slow.AddPresent(Start, 0, 0); + require(slow.Fps() == 0 && slow.GpuMsPerFrame() == 0 && slow.DriverMsPerFrame() == 0, "a single present reads as a frame"); + for (int i = 0; i < 3; ++i) { + gpuTotal += Gpu[i]; + driverTotal += Driver[i]; + slow.AddPresent(Start + (i + 1) * 2s, gpuTotal, driverTotal); + require(within(slow.Fps(), 0.5) && within(slow.LastFrameMs(), 2000), "a present every 2 s reads as " + std::to_string(slow.Fps()) + " FPS and " + std::to_string(slow.LastFrameMs()) + " ms, not 0.5 and 2000"); + require(within(slow.GpuMsPerFrame(), static_cast(Gpu[i]) / 1e6) && within(slow.DriverMsPerFrame(), static_cast(Driver[i]) / 1e6), "frame " + std::to_string(i + 1) + " every 2 s reads " + std::to_string(slow.GpuMsPerFrame()) + " ms of GPU and " + std::to_string(slow.DriverMsPerFrame()) + " ms of driver time, not its own"); + } + const auto everyTwoSeconds = AgcDriver::ComposeOverlay(slow, true, true); + require(everyTwoSeconds.above == std::vector{"PROFILED", "FPS: 0.50", "FRAME: 2000 MS"}, "a present every 2 s shows " + std::to_string(everyTwoSeconds.above.size()) + " lines above the graph, the last '" + everyTwoSeconds.above.back() + "'"); + require(everyTwoSeconds.below[0] == "MIN/AVG/MAX: 2000/2000/2000" && everyTwoSeconds.below[1] == "GPU: 20.0 MS" && everyTwoSeconds.below[2] == "DRIVER: 8.0 MS", "a present every 2 s shows '" + everyTwoSeconds.below[0] + "', '" + everyTwoSeconds.below[1] + "' and '" + everyTwoSeconds.below[2] + "'"); + + OverlayStatistics glacial; + glacial.AddPresent(Start, 0, 0); + for (int i = 1; i <= 5; ++i) glacial.AddPresent(Start + i * 12s, static_cast(i) * 250'000'000u, static_cast(i) * 12'500'000'000u); + require(within(glacial.Fps(), 1000.0 / 12000.0) && within(glacial.LastFrameMs(), 12000) && within(glacial.GpuMsPerFrame(), 250) && within(glacial.DriverMsPerFrame(), 12500), "a present every 12 s reads as " + std::to_string(glacial.Fps()) + " FPS, " + std::to_string(glacial.LastFrameMs()) + " ms, " + std::to_string(glacial.GpuMsPerFrame()) + " and " + std::to_string(glacial.DriverMsPerFrame()) + " ms"); + const auto everyTwelveSeconds = AgcDriver::ComposeOverlay(glacial, true, true); + require(everyTwelveSeconds.above == std::vector{"PROFILED", "FPS: 0.08", "FRAME: 9999 MS"}, "a present every 12 s shows " + std::to_string(everyTwelveSeconds.above.size()) + " lines above the graph, the last '" + everyTwelveSeconds.above.back() + "'"); + require(everyTwelveSeconds.below[0] == "MIN/AVG/MAX: 9999/9999/9999" && everyTwelveSeconds.below[1] == "GPU: 250 MS" && everyTwelveSeconds.below[2] == "DRIVER: 9999 MS", "a present every 12 s shows '" + everyTwelveSeconds.below[0] + "', '" + everyTwelveSeconds.below[1] + "' and '" + everyTwelveSeconds.below[2] + "'"); + require(everyTwelveSeconds.frameTimes == std::vector(5, 12000.0), "the graph does not hold the five 12 s frames"); + const auto layout = AgcDriver::LayOutOverlay(everyTwelveSeconds, {1920, 1080}, OverlayCorner::TopLeft); + std::size_t bars = 0; + for (const auto& rect : layout.rects) { + if (rect.color != static_cast(AgcDriver::OverlayColor::Graph)) continue; + ++bars; + require(rect.height == OverlayMetrics::GraphHeight * layout.scale / 2, "steady 12 s frames do not reach half the graph"); + } + require(bars == 5, "the graph has " + std::to_string(bars) + " bars for five 12 s frames"); + + OverlayStatistics rising; + auto at = Start; + std::uint64_t total = 0; + rising.AddPresent(at, 0, 0); + const auto step = [&](Clock::duration interval, std::uint64_t gpuNanoseconds) { + at += interval; + total += gpuNanoseconds; + rising.AddPresent(at, total, 0); + }; + const auto check = [&](double fps, double gpuMs, const char* shown, const std::string& when) { + require(within(rising.Fps(), fps) && within(rising.GpuMsPerFrame(), gpuMs) && AgcDriver::FormatOverlayRate(rising.Fps()) == shown, when + " reads as " + std::to_string(rising.Fps()) + " FPS ('" + AgcDriver::FormatOverlayRate(rising.Fps()) + "') and " + std::to_string(rising.GpuMsPerFrame()) + " ms of GPU time, not " + std::to_string(fps) + " ('" + shown + "') and " + std::to_string(gpuMs)); + }; + step(2s, 40'000'000u); + check(0.5, 40, "0.50", "a 2 s frame"); + step(1250ms, 30'000'000u); + check(0.8, 30, "0.80", "a 1.25 s frame"); + step(1001ms, 20'000'000u); + check(1000.0 / 1001.0, 20, "1.0", "a 1.001 s frame, the last second holding one present"); + step(1000ms, 10'000'000u); + check(1.0, 10, "1.0", "a 1 s frame, the last second holding two presents"); + step(750ms, 6'000'000u); + check(1.0 / 0.75, 6, "1.3", "a 750 ms frame"); + for (int i = 0; i < 8; ++i) step(250ms, i % 2 == 0 ? 2'000'000u : 6'000'000u); + check(4.0, 4.0, "4.0", "four presents a second, alternating 2 and 6 ms of GPU time"); + step(3s, 9'000'000u); + check(1.0 / 3.0, 9, "0.33", "a 3 s frame after the fast ones"); +} + +void settingsTests() { + const auto hidden = AgcDriver::ParseOverlaySettings(nullptr, nullptr); + require(!hidden.visible && hidden.corner == OverlayCorner::TopLeft, "the overlay is not off and top-left by default"); + require(!AgcDriver::ParseOverlaySettings("", "").visible && !AgcDriver::ParseOverlaySettings("0", nullptr).visible, "an empty or 0 ANYPS5_OVERLAY shows the overlay"); + require(AgcDriver::ParseOverlaySettings("1", nullptr).visible, "ANYPS5_OVERLAY=1 does not show the overlay"); + const std::pair corners[] = {{"tl", OverlayCorner::TopLeft}, {"tr", OverlayCorner::TopRight}, {"bl", OverlayCorner::BottomLeft}, {"br", OverlayCorner::BottomRight}}; + for (const auto& [name, corner] : corners) require(AgcDriver::ParseOverlaySettings("1", name).corner == corner, std::string("ANYPS5_OVERLAY_POS=") + name + " picks another corner"); + const auto refused = [](const char* enabled, const char* position, const char* variable) { + try { + static_cast(AgcDriver::ParseOverlaySettings(enabled, position)); + } catch (const std::exception& error) { + return std::string(error.what()).find(variable) != std::string::npos; + } + return false; + }; + require(refused("yes", nullptr, "ANYPS5_OVERLAY") && refused("2", nullptr, "ANYPS5_OVERLAY"), "an ANYPS5_OVERLAY value other than 1 or 0 was accepted or not named"); + require(refused("1", "center", "ANYPS5_OVERLAY_POS") && refused(nullptr, "TL", "ANYPS5_OVERLAY_POS"), "an unknown ANYPS5_OVERLAY_POS was accepted or not named"); +} + +void composeTests() { + OverlayStatistics statistics; + for (std::uint64_t i = 0; i <= 100; ++i) statistics.AddPresent(Start + i * 10ms, i * 2'000'000u, i * 500'000u); + const auto plain = AgcDriver::ComposeOverlay(statistics, false, false); + require(plain.above.size() == 2 && plain.above[0] == "FPS: 100" && plain.above[1] == "FRAME: 10.0 MS", "the frame lines read '" + plain.above[0] + "' and '" + plain.above[1] + "'"); + require(plain.below.size() == 1 && plain.below[0] == "MIN/AVG/MAX: 10.0/10.0/10.0", "the graph line reads '" + plain.below[0] + "'"); + require(plain.frameTimes.size() == 100, "the graph does not hold every frame time"); + const auto profiled = AgcDriver::ComposeOverlay(statistics, true, true); + require(profiled.below.size() == 3 && profiled.below[1] == "GPU: 2.0 MS" && profiled.below[2] == "DRIVER: 0.5 MS", "the profiled lines read '" + profiled.below[1] + "' and '" + profiled.below[2] + "'"); + std::vector marked{"PROFILED"}; + marked.insert(marked.end(), plain.above.begin(), plain.above.end()); + require(profiled.above == marked && AgcDriver::ComposeOverlay(statistics, true, false).above == marked && AgcDriver::ComposeOverlay(statistics, false, true).above == marked, "a profiled panel does not start with PROFILED above the unprofiled lines: it starts with '" + profiled.above.front() + "'"); + require(AgcDriver::ComposeOverlay(statistics, true, false).below.size() == 2 && AgcDriver::ComposeOverlay(statistics, false, true).below[1].rfind("DRIVER", 0) == 0, "a line is shown without its source"); +} + +std::vector coverage(const AgcDriver::OverlayLayout& layout, std::uint32_t color) { + const auto& bounds = layout.bounds; + std::vector covered(static_cast(bounds.width) * static_cast(bounds.height), 0); + for (const auto& rect : layout.rects) { + if (rect.color != color) continue; + for (std::int32_t y = rect.y; y < rect.y + rect.height; ++y) { + for (std::int32_t x = rect.x; x < rect.x + rect.width; ++x) ++covered[static_cast(y - bounds.y) * static_cast(bounds.width) + static_cast(x - bounds.x)]; + } + } + return covered; +} + +void requireLineDrawn(const AgcDriver::OverlayLayout& layout, const std::vector& text, const std::string& line, std::int32_t top) { + const auto s = layout.scale; + for (std::size_t column = 0; column < line.size(); ++column) { + const auto& glyph = AgcDriver::OverlayGlyph(line[column]); + for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { + for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) { + const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast(gy)] & (0x10u >> gx)) != 0; + for (std::int32_t py = 0; py < s; ++py) { + for (std::int32_t px = 0; px < s; ++px) { + const auto x = (OverlayMetrics::Padding + static_cast(column) * OverlayMetrics::Advance + gx) * s + px; + const auto y = (top + gy) * s + py; + require(text[static_cast(y) * static_cast(layout.bounds.width) + static_cast(x)] == (set ? 1 : 0), "'" + std::string(1, line[column]) + "' of '" + line + "' is drawn wrong at glyph pixel " + std::to_string(gx) + "," + std::to_string(gy)); + } + } + } + } + } +} + +void layoutTests() { + OverlayContent content{{"FPS: 60.0", "FRAME: 16.7 MS"}, {}, {"MIN/AVG/MAX: 16.7/16.7/16.7"}}; + for (int i = 0; i < 40; ++i) content.frameTimes.push_back(i == 39 ? 50.0 : 10.0); + const std::pair scales[] = {{{1280, 720}, 2}, {{1920, 1080}, 2}, {{2560, 1440}, 3}, {{3840, 2160}, 5}, {{320, 180}, 1}, {{2560, 720}, 2}}; + for (const auto& [extent, scale] : scales) require(AgcDriver::OverlayScale(extent, content) == scale, std::to_string(extent.width) + "x" + std::to_string(extent.height) + " draws the overlay at scale " + std::to_string(AgcDriver::OverlayScale(extent, content)) + ", not " + std::to_string(scale)); + + const VkExtent2D extent{1920, 1080}; + const auto layout = AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopLeft); + const auto s = layout.scale; + const auto margin = OverlayMetrics::Margin * s; + require(layout.bounds.x == margin && layout.bounds.y == margin && layout.bounds.width == (OverlayMetrics::InnerWidth + 2 * OverlayMetrics::Padding) * s, "the top-left panel is not at the margin"); + require(layout.rects.front().x == layout.bounds.x && layout.rects.front().width == layout.bounds.width && layout.rects.front().color == static_cast(AgcDriver::OverlayColor::Background), "the background is not drawn first over the whole panel"); + for (const auto& rect : layout.rects) { + require(rect.width > 0 && rect.height > 0 && rect.x >= layout.bounds.x && rect.y >= layout.bounds.y && rect.x + rect.width <= layout.bounds.x + layout.bounds.width && rect.y + rect.height <= layout.bounds.y + layout.bounds.height, "a rectangle leaves the panel"); + } + + const auto text = coverage(layout, static_cast(AgcDriver::OverlayColor::Text)); + const auto lineTop = [&](std::size_t line) { return OverlayMetrics::Padding + static_cast(line) * OverlayMetrics::LinePitch; }; + requireLineDrawn(layout, text, content.above[0], lineTop(0)); + requireLineDrawn(layout, text, content.above[1], lineTop(1)); + requireLineDrawn(layout, text, content.below[0], lineTop(2) + OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap); + + std::vector bars; + for (const auto& rect : layout.rects) { + if (rect.color == static_cast(AgcDriver::OverlayColor::Graph)) bars.push_back(rect); + } + require(bars.size() == content.frameTimes.size(), "the graph has " + std::to_string(bars.size()) + " bars for 40 frames"); + const auto graphBottom = layout.bounds.y + (lineTop(2) + OverlayMetrics::GraphHeight) * s; + const auto full = OverlayMetrics::GraphHeight * s; + require(bars.back().x == layout.bounds.x + (OverlayMetrics::Padding + static_cast(OverlayStatistics::FrameHistory) - 1) * s, "the newest frame is not at the right end of the graph"); + for (std::size_t i = 0; i + 1 < bars.size(); ++i) require(bars[i + 1].x == bars[i].x + s && bars[i].width == s, "the graph bars are not side by side"); + require(bars.back().height == full && bars.back().y + bars.back().height == graphBottom, "the slowest frame does not fill the graph"); + require(bars.front().height == static_cast(std::lround(10.0 / 50.0 * full)) && bars.front().y + bars.front().height == graphBottom, "a 10 ms frame is not a fifth of a 50 ms one"); + OverlayContent steady = content; + std::fill(steady.frameTimes.begin(), steady.frameTimes.end(), 16.0); + for (const auto& rect : AgcDriver::LayOutOverlay(steady, extent, OverlayCorner::TopLeft).rects) { + if (rect.color == static_cast(AgcDriver::OverlayColor::Graph)) require(rect.height == full / 2, "steady frames do not reach half the graph"); + } + + const std::pair> corners[] = {{OverlayCorner::TopRight, {true, false}}, {OverlayCorner::BottomLeft, {false, true}}, {OverlayCorner::BottomRight, {true, true}}}; + for (const auto& [corner, sides] : corners) { + const auto placed = AgcDriver::LayOutOverlay(content, extent, corner).bounds; + const auto x = sides.first ? static_cast(extent.width) - margin - placed.width : margin; + const auto y = sides.second ? static_cast(extent.height) - margin - placed.height : margin; + require(placed.x == x && placed.y == y && placed.width == layout.bounds.width && placed.height == layout.bounds.height, "a corner places the panel elsewhere"); + } + OverlayContent profiled = content; + profiled.below.push_back("GPU: 2.0 MS"); + require(AgcDriver::LayOutOverlay(profiled, extent, OverlayCorner::TopLeft).bounds.height == layout.bounds.height + OverlayMetrics::LinePitch * s, "a profiled line does not add one line to the panel"); + + bool refused = false; + try { + static_cast(AgcDriver::OverlayGlyph('q')); + } catch (const std::exception&) { + refused = true; + } + require(refused, "a character without a glyph was drawn"); +} + +constexpr std::uint64_t GiB = 1ull << 30u; + +OverlayStatistics countedStatistics() { + OverlayStatistics statistics; + for (std::uint64_t i = 0; i <= 60; ++i) { + AgcDriver::OverlayCounters counters; + counters.gpuNanoseconds = i * 4'000'000u; + counters.driverNanoseconds = i * 1'500'000u; + counters.draws = 5000 + i * 1240; + counters.dispatches = 70 + i * 85; + counters.pipelines = i < 50 ? 1500 : 1503; + statistics.AddPresent(Start + i * 16ms, counters); + } + return statistics; +} + +AgcDriver::OverlayDetails fullDetails() { + AgcDriver::OverlayDetails details; + details.vram = AgcDriver::OverlayMemory{static_cast(6.1 * GiB), static_cast(7.8 * GiB)}; + details.ram = static_cast(3.2 * GiB); + details.display = {1920, 1080}; + details.window = {2560, 1440}; + return details; +} + +void packetStatisticsTests() { + const auto statistics = countedStatistics(); + require(within(statistics.DrawsPerFrame(), 1240) && within(statistics.DispatchesPerFrame(), 85), "1240 draws and 85 dispatches per frame read as " + std::to_string(statistics.DrawsPerFrame()) + " and " + std::to_string(statistics.DispatchesPerFrame())); + require(statistics.PipelinesInWindow() == 3 && statistics.Latest().pipelines == 1503, "3 pipelines created within the last second of 1503 read as " + std::to_string(statistics.PipelinesInWindow()) + " of " + std::to_string(statistics.Latest().pipelines)); + require(within(statistics.GpuMsPerFrame(), 4.0) && within(statistics.DriverMsPerFrame(), 1.5), "the time counters changed with the packet counters"); + auto stalled = statistics; + auto counters = stalled.Latest(); + counters.draws += 900; + counters.dispatches += 40; + counters.pipelines += 7; + stalled.AddPresent(Start + 3000ms, counters); + require(within(stalled.DrawsPerFrame(), 900) && within(stalled.DispatchesPerFrame(), 40) && stalled.PipelinesInWindow() == 7 && stalled.Latest().pipelines == 1510, "a present alone in its second reads as " + std::to_string(stalled.DrawsPerFrame()) + " draws, " + std::to_string(stalled.DispatchesPerFrame()) + " dispatches and " + std::to_string(stalled.PipelinesInWindow()) + " pipelines, not the last frame's 900, 40 and 7"); + auto restarted = statistics; + counters = restarted.Latest(); + counters.draws = 3; + restarted.AddPresent(Start + 61 * 16ms, counters); + require(restarted.DrawsPerFrame() == 0, "a draw counter that went back is read as draws"); + OverlayStatistics empty; + require(empty.PipelinesInWindow() == 0 && empty.Latest().draws == 0, "an overlay without presents shows packet counts"); +} + +void packetCounterTests() { + const auto before = AgcDriver::PacketCounters::Sum(); + auto& counters = AgcDriver::PacketCounters::ThisThread(); + require(&counters == &AgcDriver::PacketCounters::ThisThread(), "one thread got two packet counters"); + for (int i = 0; i < 1240; ++i) counters.CountDraw(); + for (int i = 0; i < 85; ++i) counters.CountDispatch(); + bool separate = false; + std::thread worker([&] { + auto& own = AgcDriver::PacketCounters::ThisThread(); + separate = &own != &counters; + for (int i = 0; i < 1000; ++i) own.CountDraw(); + own.CountDispatch(); + }); + worker.join(); + require(separate, "two workers share packet counters"); + const auto after = AgcDriver::PacketCounters::Sum(); + require(after.draws - before.draws == 2240 && after.dispatches - before.dispatches == 86, "two workers' 2240 draws and 86 dispatches sum to " + std::to_string(after.draws - before.draws) + " and " + std::to_string(after.dispatches - before.dispatches)); + std::thread next([] { AgcDriver::PacketCounters::ThisThread().CountDraw(); }); + next.join(); + require(AgcDriver::PacketCounters::Sum().draws == after.draws + 1, "the counts of a worker that ended were lost, or a new worker restarted them"); +} + +void countFormatTests() { + const std::pair counts[] = {{0, "0"}, {0.4, "0"}, {0.5, "1"}, {85, "85"}, {1239.6, "1240"}, {9999, "9999"}, {9999.5, "10K"}, {12345, "12K"}, {999999, "999K"}, {1e6, "1M"}, {2.5e9, "999M"}, {-3, "0"}, {std::nan(""), "0"}, {INFINITY, "0"}}; + for (const auto& [value, expected] : counts) require(AgcDriver::FormatOverlayCount(value) == expected, std::to_string(value) + " is counted as '" + AgcDriver::FormatOverlayCount(value) + "', not '" + expected + "'"); + const std::pair sizes[] = {{0, "0.0"}, {static_cast(6.1 * GiB), "6.1"}, {static_cast(7.8 * GiB), "7.8"}, {GiB / 2, "0.5"}, {1536 * GiB, "1536"}, {20000 * GiB, "9999"}}; + for (const auto& [bytes, expected] : sizes) require(AgcDriver::FormatOverlayGigabytes(bytes) == expected, std::to_string(bytes) + " bytes are shown as '" + AgcDriver::FormatOverlayGigabytes(bytes) + "' GB, not '" + expected + "'"); +} + +void detailComposeTests() { + const auto statistics = countedStatistics(); + const auto full = AgcDriver::ComposeOverlay(statistics, true, true, fullDetails()); + const std::vector expected{"MIN/AVG/MAX: 16.0/16.0/16.0", "GPU: 4.0 MS", "DRIVER: 1.5 MS", "DRAWS: 1240 DISPATCH: 85", "SHADERS: 3 TOTAL: 1503", "VRAM: 6.1/7.8 GB", "RAM: 3.2 GB", "RES: 1920X1080 / 2560X1440"}; + require(full.below == expected, "the full panel's lines below the graph read '" + (full.below.empty() ? std::string() : full.below.back()) + "' last, of " + std::to_string(full.below.size())); + for (const auto& line : full.below) require(line.size() <= static_cast(OverlayMetrics::Columns), "'" + line + "' is wider than the panel"); + + OverlayStatistics uncounted; + for (int i = 0; i <= 60; ++i) uncounted.AddPresent(Start + i * 16ms, 0, 0); + const auto plain = AgcDriver::ComposeOverlay(uncounted, false, false); + const auto none = AgcDriver::ComposeOverlay(uncounted, false, false, AgcDriver::OverlayDetails{}); + require(none.above == plain.above && none.below == plain.below && none.below.size() == 1, "a panel without packets, pipelines, memory or a display buffer shows more than the first lines"); + + auto details = fullDetails(); + details.vram.reset(); + details.ram.reset(); + details.display = {}; + const auto partial = AgcDriver::ComposeOverlay(statistics, false, false, details); + require(partial.below.size() == 3 && partial.below.back().rfind("SHADERS", 0) == 0, "a line is shown without its data: the last line is '" + partial.below.back() + "'"); + + details = fullDetails(); + details.display = {3840, 2160}; + details.window = {16384, 16384}; + require(AgcDriver::ComposeOverlay(uncounted, false, false, details).below.back() == "RES: 3840X2160/16384X16384", "a long RES line is not written without spaces"); + details.display = {12345, 12345}; + details.window = {12345, 12345}; + require(AgcDriver::ComposeOverlay(uncounted, false, false, details).below.back() == "RAM: 3.2 GB", "a RES line wider than the panel is shown"); + details = fullDetails(); + details.window = {}; + require(AgcDriver::ComposeOverlay(uncounted, false, false, details).below.back() == "RAM: 3.2 GB", "RES is shown without a window size"); + auto large = countedStatistics(); + auto counters = large.Latest(); + counters.draws += 12345 * 2; + counters.pipelines = 123456; + large.AddPresent(Start + 62 * 16ms, counters); + const auto busy = AgcDriver::ComposeOverlay(large, false, false); + require(busy.below.size() == 3 && busy.below[2] == "SHADERS: 121K TOTAL: 123K", "large counts are not shortened: '" + busy.below.back() + "'"); +} + +void fullPanelLayoutTests() { + auto content = AgcDriver::ComposeOverlay(countedStatistics(), true, true, fullDetails()); + content.below[3] = "DRAWS: 9999 DISPATCH: 12K"; + content.below[4] = "SHADERS: 999M TOTAL: 9999"; + const VkExtent2D hd{1920, 1080}; + const auto layout = AgcDriver::LayOutOverlay(content, hd, OverlayCorner::TopLeft); + require(layout.scale == 2 && layout.bounds.height * 3 <= static_cast(hd.height), "the full panel at 1080p is " + std::to_string(layout.bounds.height) + " pixels tall at scale " + std::to_string(layout.scale) + ", more than a third of the window"); + const auto text = coverage(layout, static_cast(AgcDriver::OverlayColor::Text)); + std::int32_t top = OverlayMetrics::Padding; + for (const auto& line : content.above) { + requireLineDrawn(layout, text, line, top); + top += OverlayMetrics::LinePitch; + } + top += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap; + for (const auto& line : content.below) { + requireLineDrawn(layout, text, line, top); + top += OverlayMetrics::LinePitch; + } + require(layout.bounds.height == (top - OverlayMetrics::LinePitch + OverlayMetrics::GlyphHeight + OverlayMetrics::Padding) * layout.scale, "the panel does not end one padding below its last line"); + const std::pair scales[] = {{{1280, 720}, 2}, {{2560, 1440}, 3}, {{3840, 2160}, 5}, {{320, 180}, 1}}; + for (const auto& [extent, scale] : scales) { + const auto placed = AgcDriver::LayOutOverlay(content, extent, OverlayCorner::BottomRight); + require(placed.scale == scale && placed.bounds.x >= 0 && placed.bounds.y >= 0 && placed.bounds.y + placed.bounds.height <= static_cast(extent.height), "the full panel does not fit " + std::to_string(extent.width) + "x" + std::to_string(extent.height) + " at scale " + std::to_string(scale)); + } + const std::string added = "BCHKLOTW"; + for (const char letter : added) { + const auto& glyph = AgcDriver::OverlayGlyph(letter); + require(std::any_of(glyph.begin(), glyph.end(), [](std::uint8_t row) { return row != 0; }) && std::all_of(glyph.begin(), glyph.end(), [](std::uint8_t row) { return row <= 0x1f; }), std::string("the glyph of '") + letter + "' is empty or wider than 5 pixels"); + for (const char other : std::string("0123456789ADEFGIMNPRSUVX") + added) { + if (other != letter) require(glyph != AgcDriver::OverlayGlyph(other), std::string("'") + letter + "' and '" + other + "' have the same glyph"); + } + } +} + +void sampleTests() { + require(AgcDriver::OverlaySampleDue(std::nullopt, Start), "the first frame is not sampled"); + require(!AgcDriver::OverlaySampleDue(Start, Start + 999ms) && AgcDriver::OverlaySampleDue(Start, Start + 1000ms), "memory is sampled more than once a second, or not after a second"); + static std::uint8_t* volatile touched = nullptr; + const auto before = AgcDriver::ReadProcessPrivateBytes(); + require(before.has_value() && *before != 0, "the process's private memory cannot be read"); + constexpr std::size_t Bytes = std::size_t{64} << 20u; + const auto block = std::make_unique(Bytes); + touched = block.get(); + std::memset(touched, 0x5a, Bytes); + const auto after = AgcDriver::ReadProcessPrivateBytes(); + require(after.has_value() && *after >= *before + Bytes * 3 / 4, "touching 64 MiB raised the private memory from " + std::to_string(*before) + " to " + std::to_string(after.value_or(0)) + " bytes"); + require(touched[Bytes - 1] == 0x5a, "the touched block was not written"); +} + +void costReport() { + auto& counters = AgcDriver::PacketCounters::ThisThread(); + constexpr int Increments = 20'000'000; + const auto counted = Clock::now(); + for (int i = 0; i < Increments; ++i) counters.CountDraw(); + const auto countNs = std::chrono::duration(Clock::now() - counted).count() / Increments; + const auto summed = Clock::now(); + for (int i = 0; i < 1000; ++i) static_cast(AgcDriver::PacketCounters::Sum()); + const auto sumUs = std::chrono::duration(Clock::now() - summed).count() / 1000; + const auto sampled = Clock::now(); + for (int i = 0; i < 100; ++i) static_cast(AgcDriver::ReadProcessPrivateBytes()); + const auto ramUs = std::chrono::duration(Clock::now() - sampled).count() / 100; + std::printf("hidden cost: %.2f ns per counted draw or dispatch packet; shown: %.2f us to sum the packet counters per frame, %.1f us to read the private memory once a second\n", countNs, sumUs, ramUs); +} + +void paletteTests() { + const auto sdr = AgcDriver::OverlayPalette(VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR); + require(sdr[1] == std::array{1.0f, 1.0f, 1.0f, 1.0f} && sdr[0][3] > 0.0f && sdr[0][3] < 1.0f, "the SDR palette is not opaque white text over a translucent panel"); + const auto srgb = AgcDriver::OverlayPalette(VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR); + require(within(srgb[2][1], std::pow((0.8 + 0.055) / 1.055, 2.4), 1e-5) && srgb[1][0] == 1.0f, "an sRGB swapchain gets encoded colors"); + const auto hdr = AgcDriver::OverlayPalette(VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_COLOR_SPACE_HDR10_ST2084_EXT); + for (std::size_t channel = 0; channel < 3; ++channel) require(within(hdr[1][channel], 0.5807, 0.0015), "HDR10 white text is not at 203 nits (PQ " + std::to_string(hdr[1][channel]) + ")"); + require(std::lround(hdr[0][0] * 1023.0f) == 0 && hdr[0][3] == sdr[0][3], "the HDR10 panel is not black at the same opacity"); +} + +} + +int main() { + try { + fpsWindowTests(); + frameRingTests(); + counterTests(); + formatTests(); + lowRateTests(); + settingsTests(); + composeTests(); + layoutTests(); + paletteTests(); + packetStatisticsTests(); + packetCounterTests(); + countFormatTests(); + detailComposeTests(); + fullPanelLayoutTests(); + sampleTests(); + costReport(); + std::cout << "performance overlay statistics, layout, palette, packet, pipeline, memory and resolution 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/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp index 28c5136504..efad88d641 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp @@ -1,9 +1,12 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/TraceFile.hpp" #include "zlib.h" +#include +#include #include #include #include +#include namespace { @@ -57,9 +60,56 @@ void BufferCap() { for (int index = 0; index < 3; ++index) thread.Add("Driver.Packet", nullptr, origin + milliseconds(index), origin + milliseconds(index) + microseconds(30)); const auto snapshot = thread.Collect(); Check(snapshot.slices.size() == 2 && snapshot.dropped, "a full buffer did not stop and set the dropped flag"); + Check(snapshot.eventsLost == 1 && snapshot.eventsFullAt == origin + milliseconds(2) && !snapshot.totalsFull, "a full buffer did not report the lost slice and its time apart from the totals"); Check(snapshot.totals.size() == 1 && snapshot.totals[0].count == 3 && snapshot.totals[0].length == microseconds(90), "totals missed a slice past the cap"); } +void LostEventsCount() { + std::atomic presents{1}; + TraceThread thread("queue worker 0x0", presents, 2); + const auto origin = Clock::now(); + Check(!thread.Collect().eventsFullAt && thread.Collect().eventsLost == 0, "a thread that lost nothing reported a loss"); + for (int index = 0; index < 10; ++index) thread.Add("Driver.Packet", nullptr, origin + milliseconds(index), origin + milliseconds(index) + microseconds(30)); + const auto snapshot = thread.Collect(); + Check(snapshot.eventsLost == 8 && snapshot.eventsFullAt == origin + milliseconds(2), "the lost slices were not counted from the first one"); +} + +void TotalsOverflow() { + static char scopes[600]; + std::atomic presents{1}; + TraceThread thread("presenter", presents, 4096); + const auto origin = Clock::now(); + for (const auto& scope : scopes) thread.Add(&scope, nullptr, origin, origin + microseconds(30)); + const auto snapshot = thread.Collect(); + Check(snapshot.totalsFull && snapshot.dropped && snapshot.eventsLost == 0 && !snapshot.eventsFullAt, "a full totals table was not reported apart from the event buffer"); + Check(snapshot.totals.size() == 512 && snapshot.slices.size() == 600, "a full totals table stopped the event buffer or kept more than its entries"); +} + +void CapacitySettings() { + Check(TraceThread::ParseCapacity(nullptr) == TraceThread::DefaultCapacity, "the default event capacity changed"); + Check(TraceThread::ParseCapacity("1") == 1, "the smallest event capacity was rejected"); + Check(TraceThread::ParseCapacity("3145728") == 3145728, "a larger event capacity was rejected"); + Check(TraceThread::ParseCapacity("4194304") == TraceThread::MaximumCapacity, "the maximum event capacity was rejected"); + std::atomic presents{1}; + const auto requested = std::to_string(TraceThread::DefaultCapacity + 1); + TraceThread thread("queue worker 0x0", presents, TraceThread::ParseCapacity(requested.c_str())); + const auto origin = Clock::now(); + for (std::size_t index = 0; index <= TraceThread::DefaultCapacity; ++index) + thread.Add("Driver.Packet", nullptr, origin, origin + microseconds(30)); + auto snapshot = thread.Collect(); + Check(snapshot.slices.size() == TraceThread::DefaultCapacity + 1 && !snapshot.dropped, "the configured buffer stopped at the default capacity"); + thread.Add("Driver.Packet", nullptr, origin, origin + microseconds(30)); + snapshot = thread.Collect(); + Check(snapshot.slices.size() == TraceThread::DefaultCapacity + 1 && snapshot.dropped, "the configured buffer grew beyond its capacity"); + Check(snapshot.totals.size() == 1 && snapshot.totals[0].count == TraceThread::DefaultCapacity + 2, "totals lost the event past the configured capacity"); + for (const auto* text : {"", "0", "-1", "+1", "1.5", " 1", "1 ", "1x", "4194305", "18446744073709551616"}) { + bool rejected = false; + try { TraceThread::ParseCapacity(text); } + catch (const std::invalid_argument& error) { rejected = std::string_view(error.what()).starts_with("APS5_TRACE_EVENTS="); } + Check(rejected, "an invalid event capacity did not report its setting"); + } +} + void FoldShortSlices() { std::atomic presents{1}; TraceThread thread("queue worker 0x0", presents, 16); @@ -106,6 +156,43 @@ void EventsShape() { Check(json == expected, "trace events changed shape"); } +void BatchIds() { + const auto origin = Clock::now(); + const std::vector slices{{"Driver.Packet", "dispatch", origin + microseconds(10), microseconds(30), 42}}; + const std::vector threads{{"queue worker 0x0", slices, {}, false}}; + const std::vector presents{origin, origin + milliseconds(16)}; + const auto json = TraceFormat::Events(7, origin, threads, 0, presents, 5); + Check(json.find("{\"name\":\"Driver.Packet.dispatch\",\"ph\":\"X\",\"ts\":10.000,\"pid\":7,\"tid\":1,\"dur\":30.000,\"args\":{\"batch\":42}}") != std::string::npos, "a slice's batch id was not written"); + std::atomic count{1}; + TraceThread thread("queue worker 0x0", count, 4); + AgcDriver::TraceBatchTag = 9; + thread.Add("Driver.Packet", nullptr, origin, origin + microseconds(30)); + AgcDriver::TraceBatchTag = 0; + thread.Add("Driver.Packet", nullptr, origin + milliseconds(1), origin + milliseconds(1) + microseconds(30)); + const auto snapshot = thread.Collect(); + Check(snapshot.slices.size() == 2 && snapshot.slices[0].batch == 9 && snapshot.slices[1].batch == 0, "a slice did not take its thread's batch id"); + const AgcDriver::TraceClockSample first{1000.0, origin + microseconds(10)}; + const AgcDriver::TraceClockSample last{41000.0, origin + microseconds(30)}; + const std::vector ranges{{"batch", 5000.0, 9000.0, 3}}; + Check(TraceFormat::GpuSlices(ranges, first, last, origin)[0].batch == 3, "a GPU range lost its batch id"); +} + +void FullBufferEvents() { + const auto origin = Clock::now(); + const std::vector slices{{"Driver.Packet", nullptr, origin + milliseconds(1), microseconds(30)}}; + TraceThread::Snapshot snapshot{"queue worker 0x0", slices, {}, true, 7, origin + milliseconds(3), false}; + const std::vector threads{snapshot}; + const std::vector presents{origin, origin + milliseconds(16)}; + const auto json = TraceFormat::Events(7, origin, threads, 0, presents, 5); + const std::string expected = + "{\"name\":\"trace buffer full\",\"ph\":\"i\",\"ts\":3000.000,\"pid\":7,\"tid\":1,\"s\":\"t\",\"args\":{\"events_lost\":7}},\n" + "{\"name\":\"present\""; + Check(json.find(expected) != std::string::npos, "the cut-off instant event is missing or has the wrong shape"); + snapshot.eventsLost = 0; + snapshot.eventsFullAt.reset(); + Check(TraceFormat::Events(7, origin, std::vector{snapshot}, 0, presents, 5).find("trace buffer full") == std::string::npos, "a thread that lost nothing got a cut-off event"); +} + void SummaryFields() { const auto origin = Clock::now(); AgcDriver::TraceSummary summary; @@ -138,6 +225,24 @@ void SummaryFields() { auto dropped = threads; dropped[1].dropped = true; Check(TraceFormat::Summary(summary, dropped, presents).find("\"trace_dropped\":true") != std::string::npos, "a dropped thread did not mark the summary"); + Check(line.find("trace_dropped_threads") == std::string::npos, "a summary without drops listed threads"); + dropped[0].dropped = true; + dropped[0].eventsLost = 2240000; + dropped[0].eventsFullAt = presents.front() + milliseconds(32); + dropped[1].totalsFull = true; + const auto detailed = TraceFormat::Summary(summary, dropped, presents); + Check(detailed.find("\"trace_dropped\":true,\"trace_dropped_threads\":{\"queue worker 0x0\":{\"events_full_after_s\":0.032,\"events_lost\":2240000,\"totals_full\":false},\"presenter\":{\"totals_full\":true}},\"trace\":") != std::string::npos, "the dropped threads were not listed with their reasons"); + auto shared = threads; + shared.push_back({"shader prepare", {}, {}, true}); + shared.push_back({"shader prepare", {}, {}, true}); + shared[2].eventsLost = 5; + shared[2].eventsFullAt = presents.front() + milliseconds(30); + shared[3].eventsLost = 7; + shared[3].eventsFullAt = presents.front() + milliseconds(10); + shared[3].totalsFull = true; + const auto merged = TraceFormat::Summary(summary, shared, presents); + Check(merged.find("\"trace_dropped_threads\":{\"shader prepare\":{\"events_full_after_s\":0.010,\"events_lost\":12,\"totals_full\":true}},\"trace\":") != std::string::npos, "threads with one name were not merged into one entry"); + Check(merged.find("shader prepare") == merged.rfind("shader prepare"), "threads with one name were listed twice"); } void GpuClock() { @@ -225,6 +330,194 @@ void IntervalSummaryFields() { Check(line == expected, "interval summary line changed"); } +void IntervalDroppedThreads() { + const auto origin = Clock::now(); + AgcDriver::TraceSummary summary; + summary.interval = std::make_pair(30.0, 60.0); + std::vector threads{{"queue worker 0x0", {}, {}, true, 5}}; + const std::vector presents{origin + milliseconds(100), origin + milliseconds(116)}; + threads[0].eventsFullAt = origin + milliseconds(40); + auto line = TraceFormat::Summary(summary, threads, presents); + Check(line.find("\"queue worker 0x0\":{\"events_lost\":5,\"totals_full\":false}") != std::string::npos, "a loss before the interval printed its time"); + threads[0].eventsFullAt = origin + milliseconds(104); + line = TraceFormat::Summary(summary, threads, presents); + Check(line.find("{\"events_full_after_s\":0.004,\"events_lost\":5,") != std::string::npos, "a loss inside the interval lost its time"); + threads[0].eventsFullAt = origin + milliseconds(120); + line = TraceFormat::Summary(summary, threads, presents); + Check(line.find("\"queue worker 0x0\":{\"events_lost\":5,\"totals_full\":false}") != std::string::npos && line.find("events_full_after_s") == std::string::npos, "a loss after the last present printed its time"); + line = TraceFormat::Summary(summary, threads, {}); + Check(line.find("\"queue worker 0x0\":{\"events_lost\":5,\"totals_full\":false}") != std::string::npos && line.find("events_full_after_s") == std::string::npos, "an interval without presents printed a loss time"); +} + +void SummaryMachineFields() { + const auto origin = Clock::now(); + AgcDriver::TraceSummary summary; + summary.commit = "abc"; + summary.dirty = false; + summary.gpu = "GPU"; + summary.driver = "Driver 1"; + summary.os = "OS 1"; + summary.width = 1920; + summary.height = 1080; + summary.window = "summary_every 30"; + summary.host = AgcDriver::TraceHost{"CPU \"X\"", 16, 32000}; + summary.title = AgcDriver::TraceTitle{"PPSA00000", "Example", "01.002.000"}; + const std::vector presents{origin, origin + milliseconds(16)}; + const auto line = TraceFormat::Summary(summary, {}, presents); + Check(line.find("\"os\":\"OS 1\",\"cpu\":\"CPU \\\"X\\\"\",\"cpu_threads\":16,\"memory_mib\":32000,\"title_id\":\"PPSA00000\",\"title\":\"Example\",\"content_version\":\"01.002.000\",\"resolution\":\"1920x1080\"") != std::string::npos, "the summary line did not name the machine and title"); + summary.title.reset(); + Check(TraceFormat::Summary(summary, {}, presents).find("title_id") == std::string::npos, "a summary without param.json named a title"); + summary.host.reset(); + const auto bare = TraceFormat::Summary(summary, {}, presents); + Check(bare.find("\"os\":\"OS 1\",\"resolution\":\"1920x1080\"") != std::string::npos && bare.find("cpu") == std::string::npos, "a summary on a host without a CPU name changed its fields"); +} + +void FrameMicroseconds() { + const auto origin = Clock::now(); + const std::vector presents{origin, origin + microseconds(16667), origin + microseconds(50000), origin + microseconds(50000)}; + Check(TraceFormat::FrameMicroseconds(presents) == std::vector{16667, 33333, 0}, "frame intervals in microseconds changed"); + Check(TraceFormat::FrameMicroseconds({}).empty(), "no presents gave frame intervals"); +} + +void BenchFormat() { + AgcDriver::TraceSummary summary; + summary.commit = "abc"; + summary.environment = {{"APS5_TRACE_BENCH", "1"}}; + summary.gpu = "GPU"; + summary.driver = "Driver 1"; + summary.os = "OS 1"; + summary.host = AgcDriver::TraceHost{"CPU", 8, 16000}; + const AgcDriver::TraceBenchIntervals intervals{ + std::make_shared(AgcDriver::TraceBenchInterval{0, 30.5, 0, 1920, 1080, 120.25, {16667, 16666}}), + std::make_shared(AgcDriver::TraceBenchInterval{30.5, 61, 1830, 2560, 1440, 98.5, {}}), + }; + const std::string expected = + "{\"format\":\"aps5-bench/1\",\"commit\":\"abc\",\"dirty\":null,\"env\":{\"APS5_TRACE_BENCH\":\"1\"},\"gpu\":\"GPU\",\"driver\":\"Driver 1\",\"os\":\"OS 1\"," + "\"cpu\":\"CPU\",\"cpu_threads\":8,\"memory_mib\":16000,\"intervals_dropped\":3,\"intervals\":[\n" + "{\"start_s\":0.000,\"end_s\":30.500,\"first_present\":0,\"resolution\":\"1920x1080\",\"gpu_busy_ms\":120.250,\"frame_us\":[16667,16666]},\n" + "{\"start_s\":30.500,\"end_s\":61.000,\"first_present\":1830,\"resolution\":\"2560x1440\",\"gpu_busy_ms\":98.500,\"frame_us\":[]}\n]}\n"; + Check(TraceFormat::Bench(summary, intervals, 3) == expected, "benchmark file changed"); +} + +void BenchRotation() { + AgcDriver::TraceBenchIntervals kept; + std::uint64_t dropped = 0; + for (std::size_t index = 0; index < AgcDriver::TraceBench::MaximumIntervals; ++index) AgcDriver::TraceBench::Keep(kept, std::make_shared(AgcDriver::TraceBenchInterval{static_cast(index)}), dropped); + Check(kept.size() == AgcDriver::TraceBench::MaximumIntervals && dropped == 0, "intervals were dropped before the file was full"); + for (std::size_t index = 0; index < 5; ++index) AgcDriver::TraceBench::Keep(kept, std::make_shared(AgcDriver::TraceBenchInterval{static_cast(AgcDriver::TraceBench::MaximumIntervals + index)}), dropped); + Check(kept.size() == AgcDriver::TraceBench::MaximumIntervals && dropped == 5, "a full file did not drop one interval per new one"); + Check(kept.front()->startS == 5 && kept.back()->startS == static_cast(AgcDriver::TraceBench::MaximumIntervals + 4), "the file did not keep the newest intervals"); + const auto file = TraceFormat::Bench({}, kept, dropped); + Check(file.find("\"intervals_dropped\":5,") != std::string::npos && file.find("{\"start_s\":5.000,") != std::string::npos && file.find("{\"start_s\":4.000,") == std::string::npos, "the file does not match the kept intervals"); +} + +void BenchWriterKeepsNewest() { + std::mutex mutex; + std::condition_variable changed; + bool entered = false; + bool release = false; + std::vector written; + AgcDriver::TraceBenchWriter writer([&](const AgcDriver::TraceBenchSnapshot& snapshot) { + std::unique_lock lock(mutex); + written.push_back(snapshot.dropped); + entered = true; + changed.notify_all(); + changed.wait(lock, [&] { return release; }); + }); + writer.Write({{}, {}, 1}); + { + std::unique_lock lock(mutex); + changed.wait(lock, [&] { return entered; }); + } + writer.Write({{}, {}, 2}); + writer.Write({{}, {}, 3}); + writer.Write({{}, {}, 4}); + { + std::lock_guard lock(mutex); + release = true; + } + changed.notify_all(); + writer.Flush(); + writer.Stop(); + Check(written == std::vector{1, 4}, "a benchmark snapshot was written after a newer one replaced it"); + Check(writer.Superseded() == 2, "replaced benchmark snapshots were not counted"); +} + +void BenchWriterStopWrites() { + std::vector written; + { + AgcDriver::TraceBenchWriter writer([&](const AgcDriver::TraceBenchSnapshot& snapshot) { written.push_back(snapshot.dropped); }); + writer.Write({{}, {}, 7}); + } + Check(written == std::vector{7}, "stopping the benchmark writer lost its pending snapshot"); +} + +std::string ReadGzip(const std::filesystem::path& path) { + std::ifstream input(path, std::ios::binary); + const std::vector bytes((std::istreambuf_iterator(input)), std::istreambuf_iterator()); + Check(bytes.size() > 18, "the gzip file is truncated"); + std::size_t size = 0; + for (unsigned index = 0; index < 4; ++index) size |= static_cast(bytes[bytes.size() - 4 + index]) << (8 * index); + z_stream stream{}; + Check(inflateInit2(&stream, -MAX_WBITS) == Z_OK, "inflateInit2 failed"); + std::string inflated(size, 0); + stream.next_in = const_cast(bytes.data() + 10); + stream.avail_in = static_cast(bytes.size() - 18); + stream.next_out = reinterpret_cast(inflated.data()); + stream.avail_out = static_cast(inflated.size()); + const int status = inflate(&stream, Z_FINISH); + inflateEnd(&stream); + Check(status == Z_STREAM_END && stream.avail_out == 0, "the gzip file does not inflate to its size"); + return inflated; +} + +void ReplaceGzipFile() { + const auto directory = std::filesystem::temp_directory_path() / ("aps5-bench-test-" + std::to_string(Clock::now().time_since_epoch().count())); + std::filesystem::create_directories(directory); + const auto path = directory / "aps5-bench.json.gz"; + auto temporary = path; + temporary += ".tmp"; + AgcDriver::TraceFile::ReplaceGzip(path, "{\"first\":1}\n"); + AgcDriver::TraceFile::ReplaceGzip(path, "{\"second\":2}\n"); + Check(ReadGzip(path) == "{\"second\":2}\n" && !std::filesystem::exists(temporary), "the benchmark file was not replaced"); + const auto blocked = directory / "aps5-bench-blocked.json.gz"; + auto blockedTemporary = blocked; + blockedTemporary += ".tmp"; + std::filesystem::create_directory(blocked); + bool failed = false; + try { AgcDriver::TraceFile::ReplaceGzip(blocked, "{\"third\":3}\n"); } catch (const std::system_error&) { failed = true; } + const bool blockedKept = std::filesystem::is_directory(blocked); + const bool temporaryLeft = std::filesystem::exists(blockedTemporary); + const auto kept = ReadGzip(path); +#ifndef _WIN32 + // A directory at .tmp makes the write itself fail, which is the case ReplaceGzip exists + // for: the previous file must survive, so an in-place rewrite fails this check. The failure is + // a std::ios_base::failure thrown inside libstdc++'s ofstream, which MinGW builds could not + // unwind to this catch, so the case runs on the other platforms only. + std::filesystem::create_directory(temporary); + bool writeFailed = false; + try { AgcDriver::TraceFile::ReplaceGzip(path, "{\"third\":3}\n"); } catch (const std::exception&) { writeFailed = true; } + const auto keptAfterWrite = ReadGzip(path); +#endif + std::filesystem::remove_all(directory); + Check(failed && blockedKept && !temporaryLeft, "a replace that could not move its file over the destination did not fail cleanly or left its temporary file"); + Check(kept == "{\"second\":2}\n", "a failed replace of another file damaged the benchmark file"); +#ifndef _WIN32 + Check(writeFailed && keptAfterWrite == "{\"second\":2}\n", "a failed rewrite damaged the previous benchmark file"); +#endif +} + +void BenchSwitch() { + Check(!AgcDriver::TraceBench::Parse(nullptr, false), "the benchmark file was enabled without its switch"); + Check(AgcDriver::TraceBench::Parse("1", true), "APS5_TRACE_BENCH=1 was not accepted"); + for (const auto& [text, every] : {std::pair{"1", false}, std::pair{"0", true}, std::pair{"yes", true}, std::pair{"", true}}) { + bool rejected = false; + try { AgcDriver::TraceBench::Parse(text, every); } + catch (const std::invalid_argument&) { rejected = true; } + Check(rejected, "a malformed or unsupported APS5_TRACE_BENCH was accepted"); + } +} + void GzipFile() { std::string text; for (int index = 0; index < 20000; ++index) text += "{\"name\":\"Driver.Packet\",\"ph\":\"X\",\"ts\":" + std::to_string(index) + "}\n"; @@ -277,16 +570,30 @@ int main() { WindowBySeconds(); WindowRejects(); BufferCap(); + LostEventsCount(); + TotalsOverflow(); + CapacitySettings(); + FullBufferEvents(); FoldShortSlices(); EscapeStrings(); EventsShape(); + BatchIds(); SummaryFields(); GpuClock(); IntervalDue(); SummaryOnlyThread(); TotalsSince(); IntervalSummaryFields(); + IntervalDroppedThreads(); + SummaryMachineFields(); + FrameMicroseconds(); + BenchFormat(); + BenchRotation(); + BenchWriterKeepsNewest(); + BenchWriterStopWrites(); + BenchSwitch(); GzipFile(); + ReplaceGzipFile(); AppendLines(); std::cout << "performance trace tests passed\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/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/PlaceholderImages.cpp b/core/libs/prx/libSceAgcDriver/tests/PlaceholderImages.cpp index e00241e373..f345d9f076 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PlaceholderImages.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PlaceholderImages.cpp @@ -65,6 +65,8 @@ void CheckEveryHeap() { continue; } const auto shape = AgcDriver::Graphics::PlaceholderImageShapeOf(heap); + const auto withoutStorageMsaa = AgcDriver::Graphics::PlaceholderImageShapeForDevice(heap, false); + const auto withStorageMsaa = AgcDriver::Graphics::PlaceholderImageShapeForDevice(heap, true); const auto where = " for heap " + std::to_string(heap); const bool storage = resourceClass == ImageResourceClass::Storage; const bool multisampled = dimension == RdnaImageDimension::Dim2DMsaa || dimension == RdnaImageDimension::Dim2DMsaaArray; @@ -73,6 +75,10 @@ void CheckEveryHeap() { Require(shape.format == Format(image), "placeholder format disagrees with the numeric class" + where); Require(shape.viewType == ViewType(dimension), "placeholder view type disagrees with the dimension" + where); Require((shape.samples != VK_SAMPLE_COUNT_1_BIT) == multisampled, "placeholder sample count disagrees with multisampling" + where); + Require(withStorageMsaa.has_value(), "an enabled device lost a placeholder image shape" + where); + Require(withoutStorageMsaa.has_value() == !(storage && multisampled), "a device without storage-image MSAA exposes an unsupported placeholder" + where); + if (withoutStorageMsaa) Require(withoutStorageMsaa->samples == shape.samples && withoutStorageMsaa->viewType == shape.viewType && withoutStorageMsaa->usage == shape.usage, + "a device-specific placeholder changed the typed heap shape" + where); Require(((shape.features & VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT) != 0) == atomic, "placeholder format features disagree with atomics" + where); heaps.insert(heap); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index c2012bbc67..fd239f7c00 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -3,16 +3,30 @@ #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/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#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 #include @@ -21,6 +35,9 @@ #define NOMINMAX #endif #include +#else +#include +#include #endif extern "C" int APS5_VABI sceKernelAvailableFlexibleMemorySize(size_t* size); @@ -62,18 +79,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() { @@ -168,16 +189,22 @@ void testRegisterFile() { void testContextAndBases() { AgcDriver::QueueState state; + check(state.context.contains(0x280) && state.context.at(0x280) == 0x00080008u && state.context.contains(0x281) && state.context.at(0x281) == 0xffff0000u, "initial point context differs from the SDK defaults"); + execute(state, makePacket(0x69, {0x280, 0x00180018u, 0x00400008u})); + check(state.context.at(0x280) == 0x00180018u && state.context.at(0x281) == 0x00400008u, "explicit point size and limits were not stored"); for (const auto offset : {0x10bu, 0x10cu, 0x10du}) execute(state, makePacket(0x69, {offset, 0xffffffffu})); execute(state, makePacket(0x12, {0})); for (const auto offset : {0x10bu, 0x10cu, 0x10du}) check(state.context.at(offset) == 0, "CLEAR_STATE did not restore stencil defaults"); + check(state.context.at(0x280) == 0x00080008u && state.context.at(0x281) == 0xffff0000u, "CLEAR_STATE did not restore point size and limits"); execute(state, makePacket(0x69, {0x10, 17})); execute(state, makePacket(0x76, {0x20c, 2})); execute(state, makePacket(0x10, {3, 0}, 0x68)); check(state.context == AgcDriver::InitialContextRegisters() && state.shader.at(0x20c) == 2, "push-clear reset wrong state"); expectFailure([&] { execute(state, makePacket(0x10, {1, 0}, 0x68)); }, "already pushed"); execute(state, makePacket(0x69, {0x10, 19})); + execute(state, makePacket(0x69, {0x280, 0x00100010u, 0x00800010u})); execute(state, makePacket(0x10, {2, 0}, 0x68)); + check(state.context.at(0x280) == 0x00080008u && state.context.at(0x281) == 0xffff0000u, "context pop did not restore saved point size and limits"); check(state.context.at(0x10) == 17, "pop did not restore context"); expectFailure([&] { execute(state, makePacket(0x10, {2, 0}, 0x68)); }, "not been pushed"); alignas(8) std::array arguments{7, 8, 9, 0}; @@ -209,7 +236,10 @@ void testContextAndBases() { expectFailure([] { static_cast(AgcDriver::Pm4::PartialGroupThreads({0x10002, 1, 1}, {0x0001ffff, 0x00010001, 0x00010001})); }, "exceeds 32 bits"); expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x2063}, 2), 0); }, "dispatch modifiers"); expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x41}, 4), 0); }, "header flags"); - expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x49}), 0); }, "dispatch modifiers"); + AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x49}), 0); + AgcDriver::Pm4::Validate(makePacket(0x16, {0, 0x49}), 0); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x51}), 0); }, "dispatch modifiers"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x16, {0, 0x51}), 0); }, "indirect dispatch modifiers"); execute(state, makePacket(0x13, {32})); execute(state, makePacket(0x26, {0x1000, 1})); execute(state, makePacket(0x2a, {1})); @@ -393,7 +423,13 @@ void testIndexAttributesIndirect() { check(sameDraw(indirectFromRecord, AgcDriver::Pm4::ResolveDraw(indirectDraw, direct)) && indirectFromRecord.indexAddress == indexAddress && indirectFromRecord.indexCount == 12 && indirectFromRecord.indexSize == 4, "an indexed indirect draw after INDEX_ATTRIBUTES_INDIRECT differs from the same draw after the direct packets"); records[6] = 0x7fffffffu; execute(loaded, packet); - expectFailure([&] { AgcDriver::Pm4::ResolveDraw(indirectDraw, loaded); }, "guest"); + const auto largeIndirect = AgcDriver::Pm4::ResolveDraw(indirectDraw, loaded); + check(largeIndirect.indexAddress == indexAddress && largeIndirect.indexCount == records[6] && largeIndirect.indexSize == 4, "an indirect draw did not preserve its declared index limit for record resolution"); + loaded.indexBase = std::numeric_limits::max() - 3u; + expectFailure([&] { AgcDriver::Pm4::ResolveDraw(indirectDraw, loaded); }, "index range size overflow"); + loaded.indexBase = indexAddress + 1u; + expectFailure([&] { AgcDriver::Pm4::ResolveDraw(indirectDraw, loaded); }, "misaligned index base"); + loaded.indexBase = indexAddress; records[6] = 12; static std::size_t flushed = 0; @@ -506,6 +542,29 @@ void testCopies() { const auto clockStore =AgcDriver::Pm4::ResolveStore(clockCopy(&clock[0]), state, 64); check(clockStore.has_value() && clockStore->Bytes().size() == 8, "GPU clock COPY_DATA did not resolve as an 8-byte store"); expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x40, {0x1020a, 0, 0, low(&clock[0]), high(&clock[0])}), 0); }, "reference-clock"); + alignas(8) std::array fromRegister{0xdeadbeef, 0xdeadbeef}; + const auto registerCopy = [&](std::uint32_t control, std::uint32_t offset) { return makePacket(0x40, {control, offset, 0, low(fromRegister.data()), high(fromRegister.data())}); }; + execute(state, makePacket(0x76, {0x20c, 0x02754800, 0x2})); + execute(state, registerCopy(0x00100200, 0x2e0c)); + check(fromRegister[0] == 0x02754800 && fromRegister[1] == 0xdeadbeef, "COPY_DATA from a shader register failed"); + execute(state, registerCopy(0x00110200, 0x2e0c)); + check(fromRegister[0] == 0x02754800 && fromRegister[1] == 0x2, "64-bit COPY_DATA from two shader registers failed"); + execute(state, makePacket(0x69, {0x10, 0xabc})); + execute(state, registerCopy(0x00100200, 0xa010)); + check(fromRegister[0] == 0xabc, "COPY_DATA from a context register failed"); + check(!AgcDriver::Pm4::ResolveStore(registerCopy(0x00100200, 0x2e0c), state, 64).has_value(), "COPY_DATA from a register resolved before its turn"); + AgcDriver::Pm4::NoteDispatchRegisters(makePacket(0x15, {4, 2, 1, 0x8041}), state); + execute(state, registerCopy(0x00100200, 0x2e00)); + check(fromRegister[0] == 0x8041, "COPY_DATA did not read the last dispatch initiator"); + execute(state, registerCopy(0x00100200, 0x2e01)); + check(fromRegister[0] == 4, "COPY_DATA did not read the last direct dispatch's group count"); + AgcDriver::Pm4::NoteDispatchRegisters(makePacket(0x16, {0, 0x8001}), state); + execute(state, registerCopy(0x00100200, 0x2e00)); + check(fromRegister[0] == 0x8001, "COPY_DATA did not read the last indirect dispatch initiator"); + expectFailure([&] { execute(state, registerCopy(0x00100200, 0x2e01)); }, "has no known value"); + expectFailure([&] { execute(state, registerCopy(0x00100200, 0x2e3f)); }, "has no known value"); + expectFailure([&] { AgcDriver::Pm4::Validate(registerCopy(0x00100200, 0x1000), 0); }, "outside the shader, context and user config"); + expectFailure([&] { AgcDriver::Pm4::Validate(registerCopy(0x00110200, 0x2fff), 0); }, "outside the shader, context and user config"); execute(state, makePacket(0x50, {0x60000000, low(source.data()), high(source.data()), low(destination.data()), high(destination.data()), 16})); check(source == destination, "DMA_DATA copy failed"); execute(state, makePacket(0x50, {0x40000000, 0x44332211, 0, low(destination.data()), high(destination.data()), 6})); @@ -553,6 +612,21 @@ void testCopies() { #endif } +void testTraceRegisterWrites() { + AgcDriver::QueueState state; + const auto repeated = makePacket(0x37, {0x06010000u, 0xc343u, 0u, 0x1234u, 0x5678u}); + check(!AgcDriver::Pm4::DecodeLabelWrite(repeated), "trace register payload was decoded as a guest memory label"); + execute(state, repeated); + check(state.userConfig.at(0x343u) == 0x5678u, "one-address trace write did not retain the last register value"); + execute(state, makePacket(0x37, {0x06000000u, 0xc340u, 0u, 1u, 2u, 3u})); + check(state.userConfig.at(0x340u) == 1u && state.userConfig.at(0x341u) == 2u && state.userConfig.at(0x342u) == 3u, "sequential trace write did not advance the register address"); + execute(state, makePacket(0x37, {0x06010000u, 0xc347u, 0u, 4u, 5u})); + check(state.userConfig.at(0x347u) == 5u, "one-address trace write crossed the register range"); + for (const auto& invalid : {makePacket(0x37, {0x06000000u, 0xc347u, 0u, 1u, 2u}), makePacket(0x37, {0x06010000u, 0xc348u, 0u, 1u}), makePacket(0x37, {0x06010000u, 0xc343u, 1u, 1u})}) { + expectFailure([&] { execute(state, invalid); }, "register destination outside"); + } +} + void testMemoryCopyDecode() { using AgcDriver::Pm4::DecodeMemoryCopy; constexpr std::uint64_t source = 0x1120000000ull, destination = 0x403d7b400ull; @@ -748,6 +822,22 @@ void testEventWrite() { expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x24}), 0x20); }, "compute queue"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x424}), 0); }, "VGT_FLUSH event index"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x1e, 0x1000, 0}), 0); }, "event type 30"); + for (const auto eventType : {0x01u, 0x02u, 0x03u, 0x20u}) { + AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0x20); }, "compute queue"); + for (std::uint32_t index = 1; index < 8; ++index) { + if (index == 3) continue; + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "stream-out statistics event index"); + } + for (const auto index : {3u, 8u, 9u, 10u, 11u}) { + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType, 0x1000, 0x2}), 0); }, "stream-out statistics sampling to memory is not implemented"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "stream-out statistics sampling to memory is not implemented"); + } + for (const auto index : {12u, 13u, 14u, 15u}) { + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "reserved bits"); + } + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {eventType, 0x1000, 0x2}), 0); }, "packet size"); + } } void testAcquireMem() { @@ -852,6 +942,8 @@ void testDriverSubmission() { makePacket(0x46, {0x2a}), makePacket(0x46, {0x72c}), makePacket(0x46, {0x2e}), + makePacket(0x46, {0x01}), + makePacket(0x46, {0x20}), makePacket(0x58, {0x02007fc0, 0, 0, 0, 0, 10, 0x200}), makePacket(0x58, {0x00800000, 0xffffffff, 0, 0, 0, 10}), makePacket(0x83, {0, 1, low(destination.data()), high(destination.data())}) @@ -922,6 +1014,54 @@ void testRegisterListsReadAtSubmission() { check(std::atomic_ref(done).load() == 1, "register list rewritten after submission was read by the worker"); } +void testInvalidRegisterListEntries() { + std::array pairs{0x038f038fu, 0xc8e9037eu, 0x10, 41, 0x11, 42}; + const auto packet = makePacket(0x9f, {low(pairs.data()), high(pairs.data()), 0x80000000, 3}); + check(!AgcDriver::Pm4::IndirectRegistersValid(pairs), "an offset dword with bits 16-27 set was taken as a register offset"); + check(!AgcDriver::Pm4::IndirectRegistersValid(std::array{0xffffffffu, 0}), "the sentinel was taken as a register offset"); + check(AgcDriver::Pm4::IndirectRegistersValid(std::array{0x10, 1, 0x70000011u, 2}), "a valid register list was rejected"); + AgcDriver::QueueState state; + expectFailure([&] { AgcDriver::Pm4::ExecuteIndirectRegisters(packet, pairs, state); }, "extended register"); + check(!state.context.contains(0x10) && !state.context.contains(0x11), "a rejected register list partially changed state"); + AgcDriver::Pm4::ExecuteIndirectRegisters(packet, pairs, state, true); + check(state.context.at(0x10) == 41 && state.context.at(0x11) == 42 && !state.context.contains(0x38f), "skipping an invalid entry lost the valid ones or wrote the invalid one"); +} + +void testPartlyUpdatedRegisterListSkipsOnlyItsInvalidEntries() { +#ifdef _WIN32 + check(_putenv_s("APS5_SKIP_INVALID_INDIRECT_REGISTERS", "1") == 0, "cannot enable skipping invalid register list entries"); +#else + check(setenv("APS5_SKIP_INVALID_INDIRECT_REGISTERS", "1", 1) == 0, "cannot enable skipping invalid register list entries"); +#endif + alignas(8) static std::uint32_t gate = 0; + alignas(8) static std::uint32_t copied = 0; + static std::array registers{0x038f038fu, 5, 0x11, 9}; + auto words = joinPackets({ + makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x19}), + makePacket(0x9f, {low(registers.data()), high(registers.data()), 0x80000000, 2}), + makePacket(0x40, {0x100, 0xa010, 0, low(&copied), high(&copied)})}); + submitWords(words); + registers = {0x10, 77, 0x03900390u, 6}; + std::atomic_ref(gate).store(1); + AgcDriverWaitIdle_nid_postfix(); + check(std::atomic_ref(copied).load() == 77, "a register list partly updated before its packet ran lost the current valid entries"); +} + +void testRegisterListReadAgainAtExecution() { + alignas(8) static std::uint32_t gate = 0; + static std::uint32_t done = 0; + static std::array registers{0x038f038fu, 74}; + auto words = joinPackets({ + makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x19}), + makePacket(0x9f, {low(registers.data()), high(registers.data()), 0x80000000, 1}), + writeWord(done, 1)}); + submitWords(words); + registers[0] = 0x10; + std::atomic_ref(gate).store(1); + AgcDriverWaitIdle_nid_postfix(); + check(std::atomic_ref(done).load() == 1, "a register list invalid at submission and valid at execution was not read again"); +} + void testIndexAttributesReadAtExecution() { alignas(8) static std::uint32_t gate = 0; static std::uint32_t done = 0; @@ -939,6 +1079,15 @@ void testIndexAttributesReadAtExecution() { check(std::atomic_ref(done).load() == 1, "INDEX_ATTRIBUTES_INDIRECT did not read its record when the packet ran"); } +void testReleaseMemDataSelects() { + alignas(8) static std::uint64_t target = 0; + const auto release = [](std::uint32_t select) { return makePacket(0x49, {0x0030c514, select << 29u, low(&target), high(&target), 1, 0, 0}); }; + for (std::uint32_t select = 0; select <= 3; ++select) AgcDriver::Pm4::Validate(release(select), 0); + expectFailure([&] { AgcDriver::Pm4::Validate(release(4), 0); }, "64-bit system counter (DATA_SEL 4)"); + expectFailure([&] { AgcDriver::Pm4::Validate(release(5), 0); }, "GDS RELEASE_MEM data"); + for (const auto select : {6u, 7u}) expectFailure([&] { AgcDriver::Pm4::Validate(release(select), 0); }, "reserved RELEASE_MEM DATA_SEL"); +} + void testSuspendPointWritesQueuedLabels() { alignas(8) static std::uint32_t gate = 0, marker = 0; gate = 0; @@ -1198,6 +1347,417 @@ 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"); +} + +void testRewindTailTrace() { + using AgcDriver::DriverDetail::DrawPipeline; + if (DrawPipeline::Depth() == 0) throw std::runtime_error("the REWIND trace test needs the pipelined draw path"); + check(AgcDriver::PerformanceTrace::Configured(), "the REWIND trace test runs without a trace window"); + const char* directory = std::getenv("APS5_TRACE_DIR"); + check(directory != nullptr, "the REWIND trace test has no APS5_TRACE_DIR"); + const auto summaryPath = std::filesystem::path(directory) / "aps5-summary.jsonl"; + std::filesystem::remove(summaryPath); + alignas(8) static std::uint32_t started = 0; + static auto warm = joinPackets({writeWord(started, 1)}); + submitWords(warm); + AgcDriverWaitIdle_nid_postfix(); + check(std::atomic_ref(started).load() == 1, "the queue worker did not start"); + constexpr std::size_t Tails = 12; + alignas(8) static std::array gates{}; + alignas(8) static std::uint32_t done = 0; + static std::vector words; + words.clear(); + for (std::size_t index = 0; index < Tails; ++index) { + const auto rewind = makePacket(0x59, {0x80000000u}); + const auto wait = makePacket(0x3c, {0x13, low(&gates[index]), high(&gates[index]), 1, 0xffffffffu, 0x19}); + words.insert(words.end(), rewind.begin(), rewind.end()); + words.insert(words.end(), wait.begin(), wait.end()); + } + const auto last = writeWord(done, 1); + words.insert(words.end(), last.begin(), last.end()); + AgcDriver::PerformanceTrace::Present(1, 1, nullptr); + submitWords(words); + for (auto& gate : gates) { + std::this_thread::sleep_for(std::chrono::milliseconds(3)); + std::atomic_ref(gate).store(1); + } + AgcDriverWaitIdle_nid_postfix(); + check(std::atomic_ref(done).load() == 1, "the last REWIND tail did not run"); + AgcDriver::PerformanceTrace::Present(1, 1, nullptr); + AgcDriver::PerformanceTrace::StopOutput(); + std::ifstream summary(summaryPath); + std::string line; + check(static_cast(std::getline(summary, line)), "the trace window wrote no summary line"); + const std::string_view key = "\"worker_busy_pct\":{\"queue worker 0x0\":"; + const auto at = line.find(key); + check(at != std::string::npos, "the summary line has no busy share for queue worker 0x0"); + const double busy = std::strtod(line.c_str() + at + key.size(), nullptr); + std::printf("queue worker 0x0 busy over %zu released REWIND tails: %.1f%%\n", Tails, busy); + check(busy <= 150.0, "the queue worker's busy share counts nested REWIND tails more than once"); +} + +struct RenderingGateState { + std::mutex mutex; + std::condition_variable changed; + 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); @@ -1217,6 +1777,129 @@ void testUnwrittenUserData() { expectFailure([&] { static_cast(AgcDriver::DriverDetail::readRegister(shader, 0x95)); }, "required shader register"); } +void testWaitRegMemMasking() { + alignas(8) static std::uint32_t mem32[4] = {0x12345678u, 0x9abcdef0u, 0x55555555u, 0xaaaaaaaau}; + const auto base32 = reinterpret_cast(&mem32[0]); + for (std::uint32_t offset = 0; offset < 4; ++offset) { + const auto misaligned = base32 + offset; + const auto packet = makePacket(0x3c, {0x13, static_cast(misaligned), static_cast(misaligned >> 32u), 0x12345678u, 0xffffffffu, 0}); + AgcDriver::Pm4::Validate(packet, 0); + AgcDriver::Pm4::Validate(packet, 0x20); + const auto decoded = AgcDriver::Pm4::DecodeWait(packet); + check(decoded.address == base32, "WAIT_REG_MEM did not mask low 2 address bits"); + check(decoded.bytes == 4, "WAIT_REG_MEM awaited bytes mismatch"); + check(AgcDriver::Pm4::WaitSatisfied(packet), "WAIT_REG_MEM satisfied check failed with misaligned address"); + check(AgcDriver::Pm4::WaitSatisfiedUnchecked(packet), "WAIT_REG_MEM satisfied unchecked failed with misaligned address"); + } + + alignas(8) static std::uint64_t mem64[2] = {0x0123456789abcdefull, 0xfedcba9876543210ull}; + const auto base64 = reinterpret_cast(&mem64[0]); + for (std::uint32_t offset = 0; offset < 8; ++offset) { + const auto misaligned = base64 + offset; + const auto packet = makePacket(0x93, {0x13, static_cast(misaligned), static_cast(misaligned >> 32u), 0x89abcdefu, 0x01234567u, 0xffffffffu, 0xffffffffu, 0}); + AgcDriver::Pm4::Validate(packet, 0); + AgcDriver::Pm4::Validate(packet, 0x20); + const auto decoded = AgcDriver::Pm4::DecodeWait(packet); + check(decoded.address == base64, "WAIT_REG_MEM_64 did not mask low 3 address bits"); + check(decoded.bytes == 8, "WAIT_REG_MEM_64 awaited bytes mismatch"); + check(AgcDriver::Pm4::WaitSatisfied(packet), "WAIT_REG_MEM_64 satisfied check failed with misaligned address"); + check(AgcDriver::Pm4::WaitSatisfiedUnchecked(packet), "WAIT_REG_MEM_64 satisfied unchecked failed with misaligned address"); + } + + const auto misaligned4 = base64 + 4; + for (std::uint32_t offset = 0; offset < 4; ++offset) { + const auto misaligned = misaligned4 + offset; + const auto packet = makePacket(0x93, {0x13, static_cast(misaligned), static_cast(misaligned >> 32u), 0x01234567u, 0, 0xffffffffu, 0, 0}); + AgcDriver::Pm4::Validate(packet, 0); + AgcDriver::Pm4::Validate(packet, 0x20); + const auto decoded = AgcDriver::Pm4::DecodeWait(packet); + check(decoded.address == misaligned4, "WAIT_REG_MEM_64 with zero high mask did not mask to 4-byte alignment"); + check(decoded.bytes == 4, "WAIT_REG_MEM_64 with zero high mask expected 4 awaited bytes"); + check(AgcDriver::Pm4::WaitSatisfied(packet), "WAIT_REG_MEM_64 zero high mask satisfied check failed"); + check(AgcDriver::Pm4::WaitSatisfiedUnchecked(packet), "WAIT_REG_MEM_64 zero high mask unchecked check failed"); + } +} + +void waitDrawPipelineIdle() { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (AgcDriver::DriverDetail::DrawPipeline::Queue0().Outstanding() != 0 && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +void testWaitRegMemPipelineDrain() { + alignas(8) static std::uint32_t satisfiedTarget = 42; + alignas(8) static std::uint32_t pendingTarget = 0; + const auto pendingAddr = reinterpret_cast(&pendingTarget); + + std::atomic blockerHold{true}; + std::atomic blockerStarted{false}; + AgcDriver::DriverDetail::DrawPipeline::Queue0().Enqueue([&] { + blockerStarted.store(true); + while (blockerHold.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }, { {pendingAddr, pendingAddr + 4} }); + + while (!blockerStarted.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + check(AgcDriver::DriverDetail::DrawPipeline::Queue0().Outstanding() == 1, "draw pipeline should have 1 item"); + + auto nonOverlappingWords = makePacket(0x3c, {0x13, low(&satisfiedTarget), high(&satisfiedTarget), 42, 0xffffffffu, 0}); + submitWords(nonOverlappingWords, 0); + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (AgcDriver::DriverDetail::DrawPipeline::Queue0().Outstanding() < 2 && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + check(AgcDriver::DriverDetail::DrawPipeline::Queue0().Outstanding() >= 2, "non-overlapping wait did not elide pipeline drain"); + + blockerHold.store(false); + AgcDriverWaitIdle_nid_postfix(); + waitDrawPipelineIdle(); + check(AgcDriver::DriverDetail::DrawPipeline::Queue0().Outstanding() == 0, "draw pipeline should be idle"); + + std::atomic blocker2Hold{true}; + std::atomic blocker2Started{false}; + AgcDriver::DriverDetail::DrawPipeline::Queue0().Enqueue([&] { + blocker2Started.store(true); + while (blocker2Hold.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }, { {pendingAddr, pendingAddr + 4} }); + + while (!blocker2Started.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + + auto overlappingWords = makePacket(0x3c, {0x13, low(&pendingTarget), high(&pendingTarget), 99, 0xffffffffu, 0}); + submitWords(overlappingWords, 0); + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + check(AgcDriver::DriverDetail::DrawPipeline::Queue0().Outstanding() == 1, "overlapping wait proceeded past drain while pipeline held"); + + std::atomic_ref(pendingTarget).store(99); + blocker2Hold.store(false); + + AgcDriverWaitIdle_nid_postfix(); + waitDrawPipelineIdle(); + check(AgcDriver::DriverDetail::DrawPipeline::Queue0().Outstanding() == 0, "pipeline should be idle after overlapping wait drain"); +} + +void testWaitRegMem64ZeroHighMaskDrain() { + alignas(8) static std::uint32_t qword[2] = {42, 0}; + const auto base = reinterpret_cast(&qword[0]); + const auto packetAddr = base + 4; + std::atomic hold{true}; + std::atomic started{false}; + AgcDriver::DriverDetail::DrawPipeline::Queue0().Enqueue([&] { + started.store(true); + while (hold.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }, { {base, base + 4} }); + while (!started.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + auto words = makePacket(0x93, {0x13, static_cast(packetAddr), static_cast(packetAddr >> 32u), 42, 0, 0xffffffffu, 0, 0}); + submitWords(words, 0); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + const auto outstanding = AgcDriver::DriverDetail::DrawPipeline::Queue0().Outstanding(); + std::atomic_ref(qword[1]).store(42); + hold.store(false); + AgcDriverWaitIdle_nid_postfix(); + check(outstanding == 1, "WAIT_REG_MEM_64 with zero high mask at 4 mod 8 skipped the drain of a pending write to the dword it compares"); +} + int main(int argc, char** argv) { try { if (argc == 2 && std::string(argv[1]) == "failure") { @@ -1229,6 +1912,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}, {"rewind-trace", testRewindTailTrace}}; + 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(); @@ -1241,12 +1946,14 @@ int main(int argc, char** argv) { testIndirectDraw(); testIndexAttributesIndirect(); testMemory(); + testTraceRegisterWrites(); testCopies(); testMemoryCopyDecode(); testMemorySynchronization(); testConditionalValidation(); testConditionReadSynchronization(); testEventWrite(); + testReleaseMemDataSelects(); testAcquireMem(); testConditionalWrite(); testConditionalWriteSubmission(); @@ -1254,12 +1961,18 @@ int main(int argc, char** argv) { testUnwrittenUserData(); testDriverSubmission(); testRegisterListsReadAtSubmission(); + testInvalidRegisterListEntries(); + testPartlyUpdatedRegisterListSkipsOnlyItsInvalidEntries(); + testRegisterListReadAgainAtExecution(); testIndexAttributesReadAtExecution(); testPredicatedSubmission(); testConditionalSubmission(); testSuspendPointWritesQueuedLabels(); testNopPadSubmission(); testBranchSubmission(); + testWaitRegMemMasking(); + testWaitRegMemPipelineDrain(); + testWaitRegMem64ZeroHighMaskDrain(); LibcRunShutdown_nid_postfix(); std::puts("PM4 catalog, registers, state, memory, conditional execution and submission tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4Replay.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4Replay.cpp new file mode 100644 index 0000000000..98cf833fd3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4Replay.cpp @@ -0,0 +1,730 @@ +#include "../tools/Pm4Analysis.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelStores.hpp" +#include "prx/libSceAgcDriver/Execution/include/SubmissionCapture.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +std::uint32_t* APS5_VABI sceAgcDcbWriteData(CommandBuffer*, std::uint8_t, std::uint8_t, std::uint64_t, const void*, std::uint32_t, std::uint8_t, std::uint8_t); +std::uint32_t* APS5_VABI sceAgcAcbWriteData(CommandBuffer*, std::uint8_t, std::uint8_t, std::uint64_t, const void*, std::uint32_t, std::uint8_t, std::uint8_t); +std::uint32_t* APS5_VABI sceAgcDcbWaitRegMem(CommandBuffer*, std::uint8_t, std::uint8_t, std::uint8_t, std::uint8_t, const volatile void*, std::uint64_t, std::uint64_t, std::uint32_t); +std::uint32_t* APS5_VABI sceAgcAcbWaitRegMem(CommandBuffer*, std::uint8_t, std::uint8_t, std::uint8_t, const volatile void*, std::uint64_t, std::uint64_t, std::uint32_t); +std::uint32_t* APS5_VABI sceAgcCbReleaseMem(CommandBuffer*, std::uint8_t, std::uint16_t, std::uint8_t, std::uint8_t, const volatile Label*, std::uint8_t, std::uint64_t, std::uint16_t, std::uint16_t, std::uint8_t, std::uint32_t); +std::uint32_t* APS5_VABI sceAgcDcbDmaData(CommandBuffer*, std::uint8_t, std::uint8_t, std::uint8_t, std::uint64_t, std::uint8_t, std::uint8_t, std::uint64_t, std::uint32_t, std::uint8_t, std::uint8_t, std::uint8_t); +std::uint32_t* APS5_VABI sceAgcDcbCondExec(CommandBuffer*, const volatile std::uint32_t*, std::uint32_t); +std::uint32_t* APS5_VABI sceAgcAcbRewind(CommandBuffer*, std::uint32_t); +int APS5_VABI sceAgcAsyncRewindPatchSetRewindState(std::uint32_t*, std::uint8_t); +} + +namespace { + +using AgcDriver::CapturedSubmission; +using AgcDriver::SubmissionCapture; +using AgcDriver::SubmissionKind; +using AgcDriver::Pm4Analysis::WaitReport; +using AgcDriver::Pm4Analysis::WriterMatch; + +constexpr std::uint8_t GraphicsMemory = 0x0a; +constexpr std::uint8_t ComputeMemory = 5; +constexpr std::uint8_t Equal = 3; +constexpr std::uint8_t Bits32 = 0; +constexpr std::uint8_t Bits64 = 1; +constexpr std::uint8_t ReleaseEvent = 0x14; +constexpr std::uint8_t Data32 = 1; +constexpr std::uint8_t Data64 = 2; +constexpr std::uint8_t DmaMemory = 0; +constexpr std::uint8_t DmaFill = 2; +constexpr std::uint32_t WrittenValue = 0x11; +constexpr std::uint64_t ReleasedValue = 0x0000123400000007ull; +constexpr std::uint32_t FillValue = 0x5a5a5a5au; +constexpr std::uint32_t HeldValue = 9; +constexpr std::uint32_t CpuValue = 5; +constexpr std::uint32_t One = 1; +constexpr std::uint32_t Zero = 0; +constexpr std::uint32_t BeforeRewindValue = 1; +constexpr std::uint32_t AfterRewindValue = 2; +constexpr std::uint32_t WriteDataWords = 5; + +alignas(8) std::uint32_t writtenLabel = 0; +alignas(8) Label releasedLabel{}; +alignas(16) std::array filled{}; +alignas(8) std::uint32_t heldLabel = 0; +alignas(8) std::uint32_t cpuLabel = 0; +alignas(8) std::uint32_t reusedLabel = 0; +alignas(8) std::uint32_t conditionLabel = 0; +alignas(8) std::uint32_t guardedLabel = 0; +alignas(8) std::uint32_t beforeRewind = 0; +alignas(8) std::uint32_t afterRewind = 0; +alignas(8) std::uint32_t syntheticLabel = 0; +alignas(8) std::uint32_t otherLabel = 0; + +void check(bool condition, const std::string& reason) { + if (!condition) throw std::runtime_error(reason); +} + +std::uint64_t at(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::string hex(const volatile void* pointer) { + char text[32]; + std::snprintf(text, sizeof(text), "0x%llx", static_cast(at(pointer))); + return text; +} + +class Commands { +public: + Commands() = default; + Commands(const Commands&) = delete; + Commands& operator=(const Commands&) = delete; + CommandBuffer* Buffer() { return &buffer; } + std::vector Words(std::size_t from = 0) const { return {words.begin() + static_cast(from), words.begin() + (buffer.cursor_up - words.data())}; } + std::size_t Offset(const std::uint32_t* packet) const { return static_cast(packet - words.data()); } + std::size_t Size() const { return static_cast(buffer.cursor_up - words.data()); } + void Submit(std::uint32_t queue) { + Packet packet{words.data(), static_cast(buffer.cursor_up - words.data()), 0, {}}; + check((queue == 0 ? sceAgcDriverSubmitDcb(&packet) : sceAgcDriverSubmitAcb(queue, &packet)) == 0, "submission failed"); + } + +private: + std::array words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; +}; + +std::size_t waitAt(const Commands& commands, const std::uint32_t* firstTag) { + return commands.Offset(firstTag) + 4; +} + +std::vector names(CapturedSubmission submission) { + std::vector result; + for (const auto& packet : AgcDriver::Pm4Analysis::Decode(submission)) result.push_back(packet.name); + return result; +} + +const WaitReport& waitOn(const AgcDriver::Pm4Analysis::Analysis& analysis, const volatile void* address, std::size_t skip = 0) { + for (const auto& report : analysis.waits) { + if (report.wait.kind != AgcDriver::Pm4Analysis::MemoryWait::Kind::Memory || report.wait.fields.address != at(address)) continue; + if (skip-- == 0) return report; + } + throw std::runtime_error("no wait on " + hex(address) + " in the analysis"); +} + +bool listed(const WaitReport& report, WriterMatch::Effect effect, std::uint64_t ordinal) { + return std::any_of(report.writers.begin(), report.writers.end(), [&](const WriterMatch& writer) { return writer.effect == effect && writer.at.ordinal == ordinal; }); +} + +std::string verdict(const WaitReport& report) { + return std::to_string(static_cast(report.verdict)) + " route " + std::to_string(static_cast(report.route)) + ": " + report.reason; +} + +std::pair report(const std::filesystem::path& directory, bool full = false) { + std::FILE* output = std::tmpfile(); + check(output != nullptr, "cannot create a temporary file for the report"); + const auto code = AgcDriver::Pm4Analysis::Report(directory, full, output); + std::string text; + std::rewind(output); + for (int character; (character = std::fgetc(output)) != EOF;) text.push_back(static_cast(character)); + std::fclose(output); + return {code, text}; +} + +std::vector lines(const std::filesystem::path& path) { + std::ifstream input(path, std::ios::binary); + std::vector result; + for (std::string line; std::getline(input, line);) result.push_back(line); + return result; +} + +std::vector fields(const std::string& line) { + std::istringstream input(line); + std::vector result; + for (std::string field; input >> field;) result.push_back(field); + return result; +} + +struct Planned { + std::uint32_t queue = 0; + std::uint64_t serial = 0; + SubmissionKind kind = SubmissionKind::Submit; + std::vector commands; + std::set held; + std::map conditional; +}; + +Planned planned(std::uint32_t queue, std::uint64_t serial, const Commands& commands, std::set held = {}) { + return {queue, serial, SubmissionKind::Submit, commands.Words(), std::move(held), {}}; +} + +void writeCapture(const std::filesystem::path& directory, const std::vector& submissions) { + SubmissionCapture capture(directory); + for (const auto& submission : submissions) { + auto captured = SubmissionCapture::Prepare(submission.queue, submission.kind, submission.commands, submission.conditional); + capture.Take(captured, submission.serial, submission.serial * 1000, submission.held); + capture.Write(captured); + } +} + +void testDriverCapture(const std::filesystem::path& directory) { + heldLabel = HeldValue; + conditionLabel = One; + + Commands graphicsWrites; + sceAgcDcbWriteData(graphicsWrites.Buffer(), GraphicsMemory, 0, at(&writtenLabel), &WrittenValue, 1, 0, 1); + sceAgcDcbDmaData(graphicsWrites.Buffer(), 0, DmaMemory, 0, at(filled.data()), DmaFill, 0, FillValue, sizeof(filled), 0, 1, 0); + graphicsWrites.Submit(0); + + Commands computeRelease; + sceAgcAcbWaitRegMem(computeRelease.Buffer(), Bits32, Equal, 0, &writtenLabel, WrittenValue, 0xffffffffu, 0); + sceAgcCbReleaseMem(computeRelease.Buffer(), ReleaseEvent, 0, 0, 0, &releasedLabel, Data64, ReleasedValue, 0, 0, 0, 0); + computeRelease.Submit(0x20); + + AgcDriverSuspendPoint_nid_postfix(); + + Commands graphicsWaits; + sceAgcDcbWaitRegMem(graphicsWaits.Buffer(), Bits64, Equal, 0, 0, &releasedLabel, ReleasedValue, ~0ull, 0); + sceAgcDcbWaitRegMem(graphicsWaits.Buffer(), Bits32, Equal, 0, 0, &filled[2], FillValue, 0xffffffffu, 0); + const auto heldWait = waitAt(graphicsWaits, sceAgcDcbWaitRegMem(graphicsWaits.Buffer(), Bits32, Equal, 0, 0, &heldLabel, HeldValue, 0xffffffffu, 0)); + const auto cpuWait = waitAt(graphicsWaits, sceAgcDcbWaitRegMem(graphicsWaits.Buffer(), Bits32, Equal, 0, 0, &cpuLabel, CpuValue, 0xffffffffu, 0)); + sceAgcDcbWriteData(graphicsWaits.Buffer(), GraphicsMemory, 0, at(&reusedLabel), &One, 1, 0, 1); + sceAgcDcbWriteData(graphicsWaits.Buffer(), GraphicsMemory, 0, at(&reusedLabel), &Zero, 1, 0, 1); + sceAgcDcbWaitRegMem(graphicsWaits.Buffer(), Bits32, Equal, 0, 0, &reusedLabel, One, 0xffffffffu, 0); + const auto condition = graphicsWaits.Offset(sceAgcDcbCondExec(graphicsWaits.Buffer(), &conditionLabel, WriteDataWords)); + sceAgcDcbWriteData(graphicsWaits.Buffer(), GraphicsMemory, 0, at(&guardedLabel), &One, 1, 0, 1); + sceAgcDcbWaitRegMem(graphicsWaits.Buffer(), Bits32, Equal, 0, 0, &guardedLabel, One, 0xffffffffu, 0); + graphicsWaits.Submit(0); + std::atomic_ref(cpuLabel).store(CpuValue); + + Commands rewound; + sceAgcAcbWriteData(rewound.Buffer(), ComputeMemory, 0, at(&beforeRewind), &BeforeRewindValue, 1, 0, 1); + auto* rewind = sceAgcAcbRewind(rewound.Buffer(), 0); + const auto tail = rewound.Offset(rewind) + 2; + sceAgcAcbWriteData(rewound.Buffer(), ComputeMemory, 0, at(&afterRewind), &AfterRewindValue, 1, 0, 1); + const auto submittedRewound = rewound.Words(); + rewound.Submit(0x21); + sceAgcAsyncRewindPatchSetRewindState(rewind, 1); + AgcDriverWaitIdle_nid_postfix(); + check(std::atomic_ref(beforeRewind).load() == BeforeRewindValue && std::atomic_ref(afterRewind).load() == AfterRewindValue, "the driver did not execute the captured submissions"); + check(std::atomic_ref(guardedLabel).load() == One, "the driver did not execute the WRITE_DATA its COND_EXEC guards"); + + const auto index = lines(directory / SubmissionCapture::IndexName); + check(index.size() == 8 && index[0] == SubmissionCapture::Magic && index[1] == SubmissionCapture::Columns, "the index does not hold its two header lines and one line per submission"); + const auto computeLine = fields(index[3]); + check(computeLine.size() == 10 && computeLine[0] == "1" && computeLine[1] == "0x20" && computeLine[2] == "compute" && computeLine[6] == "submit" && computeLine[7] == "submission_000001.bin" && computeLine[9] == "-", "the index line of the compute submission is " + index[3]); + check(index[4].starts_with("2 0x0 graphics ") && index[4].ends_with(" 0 suspend - - -"), "the index line of the suspend point is " + index[4]); + const auto waitsLine = fields(index[5]); + check(waitsLine.size() == 10 && waitsLine[0] == "3", "the index line of the graphics waits is " + index[5]); + const auto conditionalEnd = condition + 5 + WriteDataWords; + check(waitsLine[9] == std::to_string(condition) + ":" + std::to_string(conditionalEnd), "the index does not list the COND_EXEC range: " + index[5]); + + const auto captured = SubmissionCapture::Read(directory); + check(captured.size() == 6, "the capture does not hold the four submissions, the suspend point and the rewind tail"); + const std::vector queues{0, 0x20, 0, 0, 0x21, 0x21}; + const std::vector kinds{SubmissionKind::Submit, SubmissionKind::Submit, SubmissionKind::Suspend, SubmissionKind::Submit, SubmissionKind::Submit, SubmissionKind::Rewind}; + for (std::size_t i = 0; i < captured.size(); ++i) { + check(captured[i].ordinal == i && captured[i].queue == queues[i] && captured[i].kind == kinds[i], "captured submission " + std::to_string(i) + " has the wrong ordinal, queue or kind"); + check(i == 0 || captured[i].timeUs >= captured[i - 1].timeUs, "capture times go backwards"); + check(i == 0 || i == 5 || captured[i].serial > captured[i - 1].serial, "capture serials are not in submission order"); + } + const auto& held = captured[3].heldAtSubmit; + check(std::find(held.begin(), held.end(), heldWait) != held.end(), "the wait on a label that held its value at submit is not marked held: " + index[5]); + check(std::find(held.begin(), held.end(), cpuWait) == held.end(), "the wait on a label the CPU stores after the submit is marked held: " + index[5]); + check(captured[5].serial == captured[4].serial, "the rewind tail does not carry the serial of its submission"); + check(captured[0].commands == graphicsWrites.Words() && captured[1].commands == computeRelease.Words() && captured[2].commands.empty() && captured[3].commands == graphicsWaits.Words(), "captured dwords differ from the submitted ones"); + check(captured[4].commands == std::vector(submittedRewound.begin(), submittedRewound.begin() + static_cast(tail)), "captured dwords up to the REWIND differ from the submitted ones, whose valid bit was still clear"); + check(captured[5].commands == rewound.Words(tail), "captured dwords of the rewind tail differ from the submitted ones"); + + check(names(captured[0]) == std::vector{"WRITE_DATA", "DMA_DATA"}, "decoded packets of the graphics writes"); + check(names(captured[1]) == std::vector{"SET_UCONFIG_REG", "WAIT_REG_MEM", "SET_UCONFIG_REG", "RELEASE_MEM"}, "decoded packets of the compute release"); + check(names(captured[4]) == std::vector{"WRITE_DATA", "REWIND"} && names(captured[5]) == std::vector{"WRITE_DATA"}, "decoded packets of the rewound submission"); + const auto release = AgcDriver::Pm4Analysis::Decode(captured[1]).back(); + check(release.operands.find("address " + hex(&releasedLabel)) != std::string::npos, "RELEASE_MEM operands do not name its label: " + release.operands); + check(release.stores.size() == 1 && release.stores[0].store.address == at(&releasedLabel) && release.stores[0].store.bytes == 8 && release.stores[0].label, "RELEASE_MEM does not decode to its 8-byte label store"); + const auto fill = AgcDriver::Pm4Analysis::Decode(captured[0]).back(); + check(fill.stores.size() == 1 && fill.stores[0].store.address == at(filled.data()) && fill.stores[0].store.bytes == sizeof(filled) && fill.stores[0].store.value == AgcDriver::Pm4::PacketStore::Value::Pattern && !fill.stores[0].label, "DMA_DATA does not decode to its fill"); + const auto waits = AgcDriver::Pm4Analysis::Decode(captured[3]); + const auto guarded = std::find_if(waits.begin(), waits.end(), [](const auto& packet) { return !packet.stores.empty() && packet.stores[0].store.address == at(&guardedLabel); }); + check(guarded != waits.end() && guarded->conditional && guarded->stores[0].conditional, "the WRITE_DATA inside the COND_EXEC range is not conditional"); + + const auto analysis = AgcDriver::Pm4Analysis::Analyze(directory); + check(analysis.waits.size() == 8, "expected eight waits, got " + std::to_string(analysis.waits.size())); + for (const auto* label : {static_cast(&writtenLabel), static_cast(&releasedLabel)}) { + const auto& crossQueue = waitOn(analysis, label); + check((crossQueue.verdict == WaitReport::Verdict::Released && crossQueue.route == WaitReport::Route::HeldAtSubmit) || (crossQueue.verdict == WaitReport::Verdict::Unknown && crossQueue.crossQueue), + "a wait on a store of another queue is decided by something else than its value at submit: " + verdict(crossQueue)); + } + check(waitOn(analysis, &releasedLabel).wait.fields.bytes == 8, "the wait on the RELEASE_MEM label does not await 8 bytes"); + const auto& filledWait = waitOn(analysis, &filled[2]); + check(filledWait.verdict == WaitReport::Verdict::Released && (filledWait.route == WaitReport::Route::HeldAtSubmit || (filledWait.route == WaitReport::Route::CurrentValue && listed(filledWait, WriterMatch::Effect::CurrentValue, 0))), + "the wait inside the fill is not released by the earlier DMA_DATA on its queue: " + verdict(filledWait)); + const auto& heldReport = waitOn(analysis, &heldLabel); + check(heldReport.verdict == WaitReport::Verdict::Released && heldReport.route == WaitReport::Route::HeldAtSubmit, "the wait whose value held at submit is not released by it: " + verdict(heldReport)); + const auto& cpu = waitOn(analysis, &cpuLabel); + check(cpu.verdict == WaitReport::Verdict::Unreleased && cpu.writerCount == 0 && cpu.reason.starts_with("unreleased by the GPU; a CPU store could release it"), "the wait on a label the CPU stores after the submit is not unreleased: " + verdict(cpu)); + const auto& reused = waitOn(analysis, &reusedLabel); + check(reused.verdict == WaitReport::Verdict::Released && reused.route == WaitReport::Route::LabelHistory && listed(reused, WriterMatch::Effect::LabelHistory, 3), + "the wait after a store of 1 and a store of 0 in its own submission is not released by the label history, as the driver releases it: " + verdict(reused)); + const auto& conditional = waitOn(analysis, &guardedLabel); + check(conditional.verdict == WaitReport::Verdict::Unknown && !conditional.crossQueue && listed(conditional, WriterMatch::Effect::Undecided, 3) && conditional.reason.find("is conditional") != std::string::npos, + "the wait on a store inside a COND_EXEC range is not unknown: " + verdict(conditional)); + const auto rewindWait = std::find_if(analysis.waits.begin(), analysis.waits.end(), [](const auto& report) { return report.wait.kind == AgcDriver::Pm4Analysis::MemoryWait::Kind::Rewind; }); + check(rewindWait != analysis.waits.end() && rewindWait->verdict == WaitReport::Verdict::Released && rewindWait->route == WaitReport::Route::RewindTail && rewindWait->at.ordinal == 4, "the REWIND is not released by its captured tail"); + check(analysis.Count(WaitReport::Verdict::Unreleased) == 1 && analysis.Count(WaitReport::Verdict::Released) + analysis.Count(WaitReport::Verdict::Unknown) == 7, "expected one unreleased wait and seven released or unknown"); + check(report(directory).first == 1, "the report does not fail on an unreleased wait"); +} + +void testWriterPresent(const std::filesystem::path& source, const std::filesystem::path& directory) { + std::vector submissions; + for (const auto& captured : SubmissionCapture::Read(source)) { + Planned copy{captured.queue, captured.serial, captured.kind, captured.commands, {captured.heldAtSubmit.begin(), captured.heldAtSubmit.end()}, {captured.conditionalRanges.begin(), captured.conditionalRanges.end()}}; + submissions.push_back(std::move(copy)); + } + Commands cpuWriter; + sceAgcAcbWriteData(cpuWriter.Buffer(), ComputeMemory, 0, at(&cpuLabel), &CpuValue, 1, 0, 1); + submissions.push_back(planned(0x22, submissions.back().serial + 1, cpuWriter)); + writeCapture(directory, submissions); + const auto analysis = AgcDriver::Pm4Analysis::Analyze(directory); + check(analysis.submissions.size() == submissions.size() && analysis.waits.size() == 8, "the rewritten capture does not hold the submissions and their waits"); + check(analysis.Count(WaitReport::Verdict::Unreleased) == 0, "a wait is unreleased although a store in the capture may release it"); + const auto& cpu = waitOn(analysis, &cpuLabel); + check(cpu.verdict == WaitReport::Verdict::Unknown && cpu.crossQueue && cpu.reason.starts_with("unknown: depends on cross-queue timing (stores on queue 0x22 at #6)") && cpu.writers.front().at.queue == 0x22 && cpu.writers.front().order == WriterMatch::Order::OtherQueue, + "the wait on a label another queue stores is not unknown by cross-queue timing: " + verdict(cpu)); + check(report(directory).first == 0, "the report fails without an unreleased wait"); +} + +void testOverwrittenAcrossSubmissions(const std::filesystem::path& directory) { + Commands storeOne, storeZero, waitOne; + sceAgcDcbWriteData(storeOne.Buffer(), GraphicsMemory, 0, at(&syntheticLabel), &One, 1, 0, 1); + sceAgcDcbWriteData(storeZero.Buffer(), GraphicsMemory, 0, at(&syntheticLabel), &Zero, 1, 0, 1); + sceAgcDcbWaitRegMem(waitOne.Buffer(), Bits32, Equal, 0, 0, &syntheticLabel, One, 0xffffffffu, 0); + writeCapture(directory, {planned(0, 1, storeOne), planned(0, 2, storeZero), planned(0, 3, waitOne)}); + const auto analysis = AgcDriver::Pm4Analysis::Analyze(directory); + const auto& wait = waitOn(analysis, &syntheticLabel); + check(wait.verdict == WaitReport::Verdict::Unknown && !wait.crossQueue && listed(wait, WriterMatch::Effect::Rejected, 1) && listed(wait, WriterMatch::Effect::Overwritten, 0) && wait.reason.find("depends on when it ran") != std::string::npos, + "a store of 1 in an earlier submission, overwritten by a store of 0, does not leave the wait unknown (the label history holds it only if it ran after the waiting submission entered the driver): " + verdict(wait)); +} + +void testLabelHistory(const std::filesystem::path& directory, std::size_t newer) { + constexpr std::array Newer{2, 3, 4, 5, 6, 7, 8, 9}; + Commands graphics; + sceAgcDcbWriteData(graphics.Buffer(), GraphicsMemory, 0, at(&syntheticLabel), &One, 1, 0, 1); + for (std::size_t i = 0; i < newer; ++i) sceAgcDcbWriteData(graphics.Buffer(), GraphicsMemory, 0, at(&syntheticLabel), &Newer.at(i), 1, 0, 1); + sceAgcDcbWaitRegMem(graphics.Buffer(), Bits32, Equal, 0, 0, &syntheticLabel, One, 0xffffffffu, 0); + writeCapture(directory, {planned(0, 1, graphics)}); + const auto analysis = AgcDriver::Pm4Analysis::Analyze(directory); + const auto& wait = waitOn(analysis, &syntheticLabel); + if (newer < AgcDriver::DriverDetail::LabelStoreHistory) { + check(wait.verdict == WaitReport::Verdict::Released && wait.route == WaitReport::Route::LabelHistory && listed(wait, WriterMatch::Effect::LabelHistory, 0), "a store kept in the label history does not release the wait: " + verdict(wait)); + } else { + check(wait.verdict == WaitReport::Verdict::Unknown && !wait.crossQueue && listed(wait, WriterMatch::Effect::Evicted, 0), "a store pushed out of the label history releases the wait: " + verdict(wait)); + } +} + +void testUnreleasable(const std::filesystem::path& directory) { + constexpr std::uint32_t StoredValue = 3; + Commands waitFirst, writeAfter, releaseOther, waitOther, rewindForever; + sceAgcDcbWaitRegMem(waitFirst.Buffer(), Bits32, Equal, 0, 0, &syntheticLabel, One, 0xffffffffu, 0); + sceAgcDcbWriteData(writeAfter.Buffer(), GraphicsMemory, 0, at(&syntheticLabel), &One, 1, 0, 1); + sceAgcCbReleaseMem(releaseOther.Buffer(), ReleaseEvent, 0, 0, 0, reinterpret_cast(&otherLabel), Data32, StoredValue, 0, 0, 0, 0); + sceAgcDcbWaitRegMem(waitOther.Buffer(), Bits32, Equal, 0, 0, &otherLabel, StoredValue + 1, 0xffffffffu, 0); + sceAgcAcbRewind(rewindForever.Buffer(), 0); + writeCapture(directory, {planned(0, 1, waitFirst), planned(0, 2, writeAfter), planned(0x20, 3, releaseOther), planned(0, 4, waitOther), planned(0x23, 5, rewindForever)}); + const auto analysis = AgcDriver::Pm4Analysis::Analyze(directory); + check(analysis.waits.size() == 3 && analysis.Count(WaitReport::Verdict::Unreleased) == 3, "expected three unreleased waits"); + const auto& later = waitOn(analysis, &syntheticLabel); + check(later.writerCount == 1 && listed(later, WriterMatch::Effect::After, 1) && later.writers.front().order == WriterMatch::Order::LaterOnQueue && later.reason.find("are later on its queue and run after it") != std::string::npos, + "a store later on the waiting queue releases the wait: " + verdict(later)); + const auto& rejected = waitOn(analysis, &otherLabel); + check(rejected.writerCount == 1 && listed(rejected, WriterMatch::Effect::OtherQueue, 2) && !rejected.writers.front().accepted && rejected.writers.front().stored == StoredValue && !rejected.crossQueue, + "a store of another queue the compare rejects does not leave the wait unreleased: " + verdict(rejected)); + check(analysis.waits.back().verdict == WaitReport::Verdict::Unreleased && analysis.waits.back().wait.kind == AgcDriver::Pm4Analysis::MemoryWait::Kind::Rewind, "a REWIND without a tail is not reported as unreleased"); + + const auto [code, text] = report(directory); + const auto newest = text.find("UNRELEASED #4 "); + const auto middle = text.find("UNRELEASED #3 "); + const auto oldest = text.find("UNRELEASED #0 "); + check(code == 1 && newest != std::string::npos && middle != std::string::npos && oldest != std::string::npos && newest < middle && middle < oldest, "the report does not list unreleased waits newest first:\n" + text); +} + +std::vector rawWait(std::uint64_t address, std::uint32_t reference) { + return {0xc0053c00u, 0x13u, static_cast(address), static_cast(address >> 32u), reference, 0xffffffffu, 0}; +} + +std::vector rawWrite(std::uint64_t address, std::uint32_t value) { + return {0xc0033700u, 0x100500u, static_cast(address), static_cast(address >> 32u), value}; +} + +std::vector joined(std::vector first, const std::vector& second) { + first.insert(first.end(), second.begin(), second.end()); + return first; +} + +std::vector repeated(std::size_t count, const std::vector& packet) { + std::vector result; + for (std::size_t i = 0; i < count; ++i) result = joined(std::move(result), packet); + return result; +} + +Planned raw(std::uint32_t queue, std::uint64_t serial, std::vector commands, std::set held = {}) { + return {queue, serial, SubmissionKind::Submit, std::move(commands), std::move(held), {}}; +} + +const WaitReport& waitAtAddress(const AgcDriver::Pm4Analysis::Analysis& analysis, std::uint64_t address) { + for (const auto& report : analysis.waits) { + if (report.wait.kind == AgcDriver::Pm4Analysis::MemoryWait::Kind::Memory && report.wait.fields.address == address) return report; + } + throw std::runtime_error("no wait on the address in the analysis"); +} + +void testReviewerCaptures(const std::filesystem::path& directory) { + constexpr std::uint64_t A = 0x10000100; + constexpr std::uint64_t B = 0x10000200; + constexpr std::size_t StaleComputeWait = 5; + const auto stale = [&](std::set held) { return std::vector{raw(0, 1, rawWrite(B, 1)), raw(0x20, 2, joined(joined(rawWrite(A, 1), rawWait(B, 1)), rawWrite(A, 0)), std::move(held)), raw(0, 3, rawWait(A, 1))}; }; + const auto evict = [&](std::size_t newer) { return std::vector{raw(0, 1, joined(rawWait(B, 1), rawWait(A, 1))), raw(0x20, 2, joined(joined(rawWrite(A, 1), repeated(newer, rawWrite(A, 2))), rawWrite(B, 1)))}; }; + const std::vector>> captures{ + {"deadlock", {raw(0, 1, joined(rawWait(A, 1), rawWrite(B, 1))), raw(0x20, 2, joined(rawWait(B, 1), rawWrite(A, 1)))}}, + {"stale", stale({})}, + {"stale-held", stale({StaleComputeWait})}, + {"evict3", evict(3)}, + {"evict4", evict(4)}, + {"evict-own4", {raw(0, 1, joined(joined(rawWrite(A, 1), rawWait(B, 1)), rawWait(A, 1))), raw(0x20, 2, joined(repeated(4, rawWrite(A, 0)), rawWrite(B, 1)))}}, + }; + for (const auto& [name, submissions] : captures) { + const auto path = directory / name; + writeCapture(path, submissions); + const auto analysis = AgcDriver::Pm4Analysis::Analyze(path); + check(analysis.waits.size() == 2, "the reviewer capture " + name + " does not hold its two waits"); + for (const auto& wait : analysis.waits) { + const bool held = wait.wait.heldAtSubmit && wait.verdict == WaitReport::Verdict::Released && wait.route == WaitReport::Route::HeldAtSubmit; + check(held || (wait.verdict == WaitReport::Verdict::Unknown && wait.crossQueue && wait.reason.starts_with("unknown: depends on cross-queue timing (stores on queue ")), + "the reviewer capture " + name + " reports the wait at #" + std::to_string(wait.at.ordinal) + " +" + std::to_string(wait.at.offset) + " as " + verdict(wait)); + } + check(analysis.Count(WaitReport::Verdict::Released) == (name == "stale-held" ? 1u : 0u), "the reviewer capture " + name + " releases a wait the capture does not prove released"); + check(report(path).first == 0, "the report of the reviewer capture " + name + " claims an unreleased wait"); + } +} + +void testProvableRoutes(const std::filesystem::path& directory) { + constexpr std::uint64_t A = 0x10000100; + constexpr std::uint64_t B = 0x10000200; + constexpr std::uint64_t C = 0x10000300; + constexpr std::uint32_t AlwaysCompare = 0x10; + const auto analyzed = [&](const std::string& name, const std::vector& submissions) { + writeCapture(directory / name, submissions); + return AgcDriver::Pm4Analysis::Analyze(directory / name); + }; + auto always = rawWait(C, 1); + always[1] = AlwaysCompare; + + const auto own = analyzed("own-submission", {raw(0, 1, joined(joined(rawWrite(A, 1), rawWait(A, 1)), always))}); + const auto& ownWait = waitAtAddress(own, A); + check(ownWait.verdict == WaitReport::Verdict::Released && ownWait.route == WaitReport::Route::CurrentValue && listed(ownWait, WriterMatch::Effect::CurrentValue, 0), "a store earlier in the waiting submission does not release the wait: " + verdict(ownWait)); + const auto& alwaysWait = waitAtAddress(own, C); + check(alwaysWait.verdict == WaitReport::Verdict::Released && alwaysWait.route == WaitReport::Route::Always, "a wait whose compare always passes is not released: " + verdict(alwaysWait)); + + const auto earlier = analyzed("earlier-submission", {raw(0, 1, rawWrite(A, 1)), raw(0x20, 2, rawWrite(B, 5)), raw(0, 3, rawWait(A, 1))}); + const auto& earlierWait = waitAtAddress(earlier, A); + check(earlierWait.verdict == WaitReport::Verdict::Released && earlierWait.route == WaitReport::Route::CurrentValue && listed(earlierWait, WriterMatch::Effect::CurrentValue, 0), + "a store in an earlier submission on the waiting queue, with no store of another queue to its bytes, does not release the wait: " + verdict(earlierWait)); + + for (const auto& [name, submissions, writer] : std::vector, std::string>>{ + {"other-queue-after", {raw(0, 1, rawWrite(A, 1)), raw(0x20, 2, rawWrite(A, 0)), raw(0, 3, rawWait(A, 1))}, "queue 0x20 at #1"}, + {"other-queue-before", {raw(0x20, 1, rawWrite(A, 0)), raw(0, 2, rawWrite(A, 1)), raw(0, 3, rawWait(A, 1))}, "queue 0x20 at #0"}, + }) { + const auto analysis = analyzed(name, submissions); + const auto& wait = waitAtAddress(analysis, A); + check(wait.verdict == WaitReport::Verdict::Unknown && wait.crossQueue && wait.reason.starts_with("unknown: depends on cross-queue timing (stores on " + writer + ")"), + name + ": a store of another queue to the awaited bytes, which can run between the store on the waiting queue and the wait whatever their submit order, leaves the wait other than unknown: " + verdict(wait)); + } + + const auto history = analyzed("history-other-queue", {raw(0, 1, joined(joined(rawWrite(A, 1), rawWait(B, 1)), rawWait(A, 1))), raw(0x20, 2, joined(repeated(AgcDriver::DriverDetail::LabelStoreHistory - 1, rawWrite(A, 0)), rawWrite(B, 1)))}); + const auto& historyWait = waitAtAddress(history, A); + check(historyWait.verdict == WaitReport::Verdict::Released && historyWait.route == WaitReport::Route::LabelHistory && listed(historyWait, WriterMatch::Effect::LabelHistory, 0), + "a label earlier in the waiting submission with fewer stores of all queues after it than the label history keeps does not release the wait: " + verdict(historyWait)); + check(waitAtAddress(history, B).verdict == WaitReport::Verdict::Unknown && waitAtAddress(history, B).crossQueue, "the wait on the store of another queue is not unknown: " + verdict(waitAtAddress(history, B))); + + Commands notKept; + sceAgcDcbDmaData(notKept.Buffer(), 0, DmaMemory, 0, at(&syntheticLabel), DmaFill, 0, One, sizeof(syntheticLabel), 0, 1, 0); + sceAgcDcbWriteData(notKept.Buffer(), GraphicsMemory, 0, at(&syntheticLabel), &Zero, 1, 0, 1); + sceAgcDcbWaitRegMem(notKept.Buffer(), Bits32, Equal, 0, 0, &syntheticLabel, One, 0xffffffffu, 0); + const auto fill = analyzed("fill-overwritten", {planned(0, 1, notKept)}); + const auto& fillWait = waitOn(fill, &syntheticLabel); + check(fillWait.verdict == WaitReport::Verdict::Unknown && !fillWait.crossQueue && listed(fillWait, WriterMatch::Effect::Overwritten, 0) && fillWait.reason.find("is a RELEASE_MEM or WRITE_DATA label") != std::string::npos, + "a DMA_DATA fill the label history does not keep, overwritten before the wait, does not leave the wait unknown: " + verdict(fillWait)); +} + +struct Step { + std::uint32_t queue = 0; + std::vector commands; +}; + +using Outcome = std::tuple; +using Outcomes = std::map, Outcome>; + +Outcomes outcomes(const AgcDriver::Pm4Analysis::Analysis& analysis) { + Outcomes result; + std::map seen; + for (const auto& report : analysis.waits) { + const auto address = report.wait.fields.address; + result.emplace(std::pair{address, seen[address]++}, Outcome{report.verdict, report.route, report.crossQueue}); + } + return result; +} + +std::vector concatenatedOrder(const std::vector>& groups, bool reversed) { + std::vector order; + for (std::size_t i = 0; i < groups.size(); ++i) { + const auto group = reversed ? groups.size() - 1 - i : i; + order.insert(order.end(), groups[group].size(), group); + } + return order; +} + +std::vector roundRobinOrder(const std::vector>& groups, std::size_t total, bool reversed) { + std::vector order; + for (std::size_t step = 0; order.size() < total; ++step) { + for (std::size_t i = 0; i < groups.size(); ++i) { + const auto group = reversed ? groups.size() - 1 - i : i; + if (step < groups[group].size()) order.push_back(group); + } + } + return order; +} + +std::vector shuffledOrder(const std::vector>& groups, std::size_t total, std::uint64_t& random) { + std::vector left; + for (const auto& group : groups) left.push_back(group.size()); + std::vector order; + while (order.size() < total) { + random = random * 6364136223846793005ull + 1442695040888963407ull; + const auto group = static_cast(random >> 33u) % groups.size(); + if (left[group] == 0) continue; + --left[group]; + order.push_back(group); + } + return order; +} + +void testPermutedSubmissions(const std::filesystem::path& directory) { + constexpr std::uint64_t Base = 0x10001000; + constexpr std::size_t Submissions = 15; + constexpr std::size_t Shuffles = 4; + constexpr std::size_t Full = AgcDriver::DriverDetail::LabelStoreHistory; + const auto address = [](std::size_t group, std::size_t slot) { return Base + group * 0x100 + slot * 0x10; }; + const std::vector> groups{ + {{0x20, rawWrite(address(0, 1), 1)}, {0x21, joined(joined(rawWrite(address(0, 0), 1), rawWait(address(0, 1), 1)), rawWrite(address(0, 0), 0))}, {0x20, rawWait(address(0, 0), 1)}}, + {{0x22, joined(rawWait(address(1, 0), 1), rawWrite(address(1, 1), 1))}, {0x23, joined(rawWait(address(1, 1), 1), rawWrite(address(1, 0), 1))}}, + {{0x24, rawWrite(address(2, 0), 1)}, {0x25, joined(rawWrite(address(2, 1), 5), rawWait(address(2, 1), 6))}, {0x24, joined(rawWait(address(2, 0), 1), rawWait(address(2, 2), 1))}}, + {{0x26, joined(joined(rawWrite(address(3, 0), 1), rawWrite(address(3, 0), 0)), rawWait(address(3, 0), 1))}, {0x27, rawWrite(address(3, 1), 3)}, {0x26, rawWait(address(3, 1), 4)}}, + {{0x28, joined(joined(rawWrite(address(4, 0), 1), rawWait(address(4, 1), 1)), rawWait(address(4, 0), 1))}, {0x29, joined(repeated(Full - 1, rawWrite(address(4, 0), 0)), rawWrite(address(4, 1), 1))}, + {0x2a, joined(joined(rawWrite(address(4, 2), 1), rawWait(address(4, 3), 1)), rawWait(address(4, 2), 1))}, {0x2b, joined(repeated(Full, rawWrite(address(4, 2), 0)), rawWrite(address(4, 3), 1))}}, + }; + using Verdict = WaitReport::Verdict; + using Route = WaitReport::Route; + const Outcome unknown{Verdict::Unknown, Route::None, true}; + const Outcome unreleased{Verdict::Unreleased, Route::None, false}; + const Outcomes expected{ + {{address(0, 1), 0}, unknown}, {{address(0, 0), 0}, unknown}, + {{address(1, 0), 0}, unknown}, {{address(1, 1), 0}, unknown}, + {{address(2, 0), 0}, {Verdict::Released, Route::CurrentValue, false}}, {{address(2, 1), 0}, unreleased}, {{address(2, 2), 0}, unreleased}, + {{address(3, 0), 0}, {Verdict::Released, Route::LabelHistory, false}}, {{address(3, 1), 0}, unreleased}, + {{address(4, 0), 0}, {Verdict::Released, Route::LabelHistory, false}}, {{address(4, 1), 0}, unknown}, {{address(4, 2), 0}, unknown}, {{address(4, 3), 0}, unknown}, + }; + std::vector> orders{concatenatedOrder(groups, false), concatenatedOrder(groups, true), roundRobinOrder(groups, Submissions, false), roundRobinOrder(groups, Submissions, true)}; + std::uint64_t random = 0x2545f4914f6cdd1dull; + for (std::size_t shuffle = 0; shuffle < Shuffles; ++shuffle) orders.push_back(shuffledOrder(groups, Submissions, random)); + for (std::size_t permutation = 0; permutation < orders.size(); ++permutation) { + check(orders[permutation].size() == Submissions, "a submission order does not hold the 15 submissions"); + std::vector next(groups.size(), 0); + std::vector submissions; + std::string described; + for (const auto group : orders[permutation]) { + const auto& step = groups[group][next[group]++]; + submissions.push_back(raw(step.queue, submissions.size() + 1, step.commands)); + described += std::to_string(group); + } + const auto path = directory / ("order-" + std::to_string(permutation)); + writeCapture(path, submissions); + const auto result = outcomes(AgcDriver::Pm4Analysis::Analyze(path)); + check(result.size() == expected.size(), "the capture in submission order " + described + " does not hold the 13 waits"); + for (const auto& [key, outcome] : result) { + check(expected.contains(key) && expected.at(key) == outcome, "submission order " + described + " gives the wait on " + hex(reinterpret_cast(static_cast(key.first))) + " the verdict " + std::to_string(static_cast(std::get<0>(outcome))) + " route " + + std::to_string(static_cast(std::get<1>(outcome))) + " cross-queue " + std::to_string(std::get<2>(outcome))); + } + } +} + +template +void expectFailure(TAction action, const std::string& text) { + try { + action(); + } catch (const std::runtime_error& error) { + check(std::string(error.what()).find(text) != std::string::npos, "expected a failure naming '" + text + "', got: " + error.what()); + return; + } + throw std::runtime_error("expected a failure naming '" + text + "'"); +} + +void testFailedWrite(const std::filesystem::path& directory) { + const auto blocker = directory / "submission_000006.bin"; + std::filesystem::create_directory(blocker); + Commands store; + sceAgcDcbWriteData(store.Buffer(), GraphicsMemory, 0, at(&otherLabel), &One, 1, 0, 1); + expectFailure([&] { store.Submit(0); }, "cannot write"); + expectFailure([&] { store.Submit(0); }, "cannot write"); + std::filesystem::remove(blocker); + check(lines(directory / SubmissionCapture::IndexName).size() == 8, "the index lists a submission whose file was not written"); +} + +void testHeldAtSubmit(const std::filesystem::path& directory) { + Commands waitHeld; + const auto wait = waitAt(waitHeld, sceAgcDcbWaitRegMem(waitHeld.Buffer(), Bits32, Equal, 0, 0, &syntheticLabel, One, 0xffffffffu, 0)); + writeCapture(directory, {planned(0, 1, waitHeld, {wait})}); + const auto analysis = AgcDriver::Pm4Analysis::Analyze(directory); + const auto& held = waitOn(analysis, &syntheticLabel); + check(held.verdict == WaitReport::Verdict::Released && held.route == WaitReport::Route::HeldAtSubmit, "a wait whose value held at submit is not released: " + verdict(held)); + check(report(directory).first == 0, "the report fails on a wait released at submit"); +} + +void testRewindValidWithoutTail(const std::filesystem::path& directory) { + Commands rewound; + sceAgcAcbWriteData(rewound.Buffer(), ComputeMemory, 0, at(&beforeRewind), &BeforeRewindValue, 1, 0, 1); + auto* rewind = sceAgcAcbRewind(rewound.Buffer(), 0); + sceAgcAsyncRewindPatchSetRewindState(rewind, 1); + writeCapture(directory, {planned(0x20, 1, rewound)}); + const auto analysis = AgcDriver::Pm4Analysis::Analyze(directory); + const auto rewindWait = std::find_if(analysis.waits.begin(), analysis.waits.end(), [](const auto& report) { return report.wait.kind == AgcDriver::Pm4Analysis::MemoryWait::Kind::Rewind; }); + check(rewindWait != analysis.waits.end(), "the capture of a REWIND valid at submit lists no REWIND"); + check(rewindWait->verdict == WaitReport::Verdict::Unknown && rewindWait->reason.find("reads it again") != std::string::npos, + "a REWIND valid at submit without a captured tail is not unknown: " + verdict(*rewindWait)); +} + +void replaceField(const std::filesystem::path& directory, std::size_t field, const std::string& value) { + auto index = lines(directory / SubmissionCapture::IndexName); + check(index.size() == 3, "the capture to corrupt does not hold one submission"); + auto values = fields(index[2]); + values.at(field) = value; + std::ofstream output(directory / SubmissionCapture::IndexName, std::ios::binary | std::ios::trunc); + output << index[0] << '\n' << index[1] << '\n'; + for (std::size_t i = 0; i < values.size(); ++i) output << (i == 0 ? "" : " ") << values[i]; + output << '\n'; + check(output.good(), "cannot rewrite the index to corrupt"); +} + +void dropHeldColumns(const std::filesystem::path& directory) { + const auto index = lines(directory / SubmissionCapture::IndexName); + std::ofstream output(directory / SubmissionCapture::IndexName, std::ios::binary | std::ios::trunc); + output << index.at(0) << '\n' << SubmissionCapture::ColumnsWithoutHeld << '\n'; + for (std::size_t line = 2; line < index.size(); ++line) { + const auto values = fields(index[line]); + check(values.size() == 10, "the index line to rewrite does not have 10 fields: " + index[line]); + for (std::size_t i = 0; i < 8; ++i) output << (i == 0 ? "" : " ") << values[i]; + output << '\n'; + } + check(output.good(), "cannot rewrite the index without its held and conditional columns"); +} + +void testIndexWithoutHeld(const std::filesystem::path& directory) { + Commands graphics, compute; + sceAgcDcbWaitRegMem(graphics.Buffer(), Bits32, Equal, 0, 0, &cpuLabel, CpuValue, 0xffffffffu, 0); + sceAgcDcbWriteData(graphics.Buffer(), GraphicsMemory, 0, at(&otherLabel), &One, 1, 0, 1); + sceAgcDcbCondExec(graphics.Buffer(), &conditionLabel, WriteDataWords); + sceAgcDcbWriteData(graphics.Buffer(), GraphicsMemory, 0, at(&guardedLabel), &One, 1, 0, 1); + sceAgcDcbWaitRegMem(graphics.Buffer(), Bits32, Equal, 0, 0, &guardedLabel, One, 0xffffffffu, 0); + sceAgcAcbWaitRegMem(compute.Buffer(), Bits32, Equal, 0, &otherLabel, One, 0xffffffffu, 0); + writeCapture(directory, {planned(0, 1, graphics), planned(0x20, 2, compute)}); + dropHeldColumns(directory); + const auto captured = SubmissionCapture::ReadIndex(directory); + check(captured.size() == 2 && !captured[0].heldKnown && !captured[0].conditionalKnown, "an index without the held and conditional columns is read as if it had them"); + const auto analysis = AgcDriver::Pm4Analysis::Analyze(directory); + check(analysis.waits.size() == 3, "expected three waits in the capture without held waits"); + const auto& cpu = waitOn(analysis, &cpuLabel); + check(cpu.verdict == WaitReport::Verdict::Unknown && !cpu.crossQueue && cpu.reason.starts_with("unknown: the index has no held column"), "a wait no store releases is not unknown when the index does not say whether it was held at submit: " + verdict(cpu)); + const auto& behind = waitOn(analysis, &otherLabel); + check(behind.verdict == WaitReport::Verdict::Unknown && behind.crossQueue, "a store of another queue decides a wait offline: " + verdict(behind)); + const auto& guarded = waitOn(analysis, &guardedLabel); + check(guarded.verdict == WaitReport::Verdict::Unknown && !guarded.crossQueue && guarded.wait.conditional && guarded.reason.find("is conditional") != std::string::npos, "a store after a COND_EXEC whose range the index does not hold is not conditional: " + verdict(guarded)); + const auto [code, text] = report(directory); + check(code == 0 && text.find("unknown 3 (cross-queue timing 1), unreleased 0\n") != std::string::npos && text.find("the index has no held and conditional columns") != std::string::npos && text.find("UNKNOWN #0 ") != std::string::npos, + "the report does not show the waits of unknown outcome:\n" + text); +} + +void testCorrupt(const std::filesystem::path& directory) { + constexpr std::size_t HeldField = 8; + constexpr std::size_t ConditionalField = 9; + Commands waitOnly; + sceAgcDcbWaitRegMem(waitOnly.Buffer(), Bits32, Equal, 0, 0, &syntheticLabel, One, 0xffffffffu, 0); + const auto expectThrow = [&](const std::vector& submissions, std::size_t field, const std::string& value, const std::string& what) { + std::filesystem::remove_all(directory); + writeCapture(directory, submissions); + if (!value.empty()) replaceField(directory, field, value); + bool threw = false; + try { + AgcDriver::Pm4Analysis::Analyze(directory); + } catch (const std::runtime_error&) { + threw = true; + } + check(threw, "the analysis accepts " + what); + }; + expectThrow({{0, 1, SubmissionKind::Submit, {0xc0044700u, 0x00000528u, static_cast(at(&syntheticLabel)), 0x40000000u, 1, 0}, {}, {}}}, 0, {}, "an EVENT_WRITE_EOP the driver rejects"); + expectThrow({planned(0, 1, waitOnly)}, HeldField, "0", "a wait held at submit that marks no wait packet"); + expectThrow({planned(0, 1, waitOnly)}, ConditionalField, "4:11", "a conditional range that starts at no COND_EXEC"); +} + +} + +int main() { + std::filesystem::path scratch; + try { + const char* captured = std::getenv("APS5_DUMP_SUBMITS"); + check(captured != nullptr && *captured != '\0', "set APS5_DUMP_SUBMITS to the capture directory of this test"); + const std::filesystem::path directory = captured; + std::filesystem::remove_all(directory); + scratch = std::filesystem::temp_directory_path() / ("anyps5-pm4-replay-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + testDriverCapture(directory); + testWriterPresent(directory, scratch / "writer-present"); + testOverwrittenAcrossSubmissions(scratch / "overwritten"); + testLabelHistory(scratch / "history-kept", AgcDriver::DriverDetail::LabelStoreHistory - 1); + testLabelHistory(scratch / "history-evicted", AgcDriver::DriverDetail::LabelStoreHistory); + testUnreleasable(scratch / "unreleasable"); + testReviewerCaptures(scratch / "reviewed"); + testProvableRoutes(scratch / "routes"); + testPermutedSubmissions(scratch / "permuted"); + testHeldAtSubmit(scratch / "held"); + testRewindValidWithoutTail(scratch / "rewind-valid"); + testIndexWithoutHeld(scratch / "without-held"); + testCorrupt(scratch / "corrupt"); + std::filesystem::remove_all(scratch); + testFailedWrite(directory); + expectFailure([] { LibcRunShutdown_nid_postfix(); }, "cannot write"); + std::puts("PM4 submission capture, decode and wait release tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + std::error_code ignored; + if (!scratch.empty()) std::filesystem::remove_all(scratch, ignored); + try { + LibcRunShutdown_nid_postfix(); + } catch (const std::exception& shutdown) { + std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4ReplayTool.cmake b/core/libs/prx/libSceAgcDriver/tests/Pm4ReplayTool.cmake new file mode 100644 index 0000000000..d578167acf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4ReplayTool.cmake @@ -0,0 +1,38 @@ +foreach(variable TOOL CAPTURE) + if(NOT DEFINED ${variable}) + message(FATAL_ERROR "${variable} is not set") + endif() +endforeach() + +execute_process(COMMAND "${TOOL}" "${CAPTURE}" RESULT_VARIABLE code OUTPUT_VARIABLE output ERROR_VARIABLE errors) +if(NOT code STREQUAL "1") + message(FATAL_ERROR "agc_pm4_replay exited with ${code} instead of 1 on a capture with an unreleased wait:\n${output}${errors}") +endif() +if(NOT output MATCHES "waits 8: released [4-6] \\([^)]*\\), unknown [1-3] \\(cross-queue timing [0-2]\\), unreleased 1\n") + message(FATAL_ERROR "agc_pm4_replay does not report seven released or unknown waits and one unreleased:\n${output}") +endif() +if(NOT output MATCHES "the tool does not model cross-queue timing") + message(FATAL_ERROR "agc_pm4_replay does not say that it does not model cross-queue timing:\n${output}") +endif() +if(NOT output MATCHES "unreleased waits, then unknown waits, newest first:\n\nUNRELEASED #3 queue 0x0 [^\n]*\n unreleased by the GPU; a CPU store could release it") + message(FATAL_ERROR "agc_pm4_replay does not list the unreleased wait of submission #3:\n${output}") +endif() + +execute_process(COMMAND "${TOOL}" "${CAPTURE}" --full RESULT_VARIABLE code OUTPUT_VARIABLE output ERROR_VARIABLE errors) +if(NOT code STREQUAL "1" OR NOT output MATCHES "submissions in execution order per queue:") + message(FATAL_ERROR "agc_pm4_replay --full exited with ${code} or printed no submissions:\n${output}${errors}") +endif() + +execute_process(COMMAND "${TOOL}" "${CAPTURE}/missing" RESULT_VARIABLE code OUTPUT_VARIABLE output ERROR_VARIABLE errors) +if(NOT code STREQUAL "2" OR NOT errors MATCHES "cannot open") + message(FATAL_ERROR "agc_pm4_replay exited with ${code} instead of 2 on a missing capture:\n${output}${errors}") +endif() + +set(unicodeCapture "${CAPTURE}-ação-日本") +file(REMOVE_RECURSE "${unicodeCapture}") +file(COPY "${CAPTURE}/" DESTINATION "${unicodeCapture}") +execute_process(COMMAND "${TOOL}" "${unicodeCapture}" RESULT_VARIABLE code OUTPUT_VARIABLE output ERROR_VARIABLE errors) +file(REMOVE_RECURSE "${unicodeCapture}") +if(NOT code STREQUAL "1" OR NOT output MATCHES "unreleased 1\n") + message(FATAL_ERROR "agc_pm4_replay exited with ${code} on a copy of the capture under a non-ASCII path:\n${output}${errors}") +endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/PositionInvariance.cpp b/core/libs/prx/libSceAgcDriver/tests/PositionInvariance.cpp index 5b6d1b3be7..22b9923a2f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PositionInvariance.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PositionInvariance.cpp @@ -58,6 +58,45 @@ void verifyVertex() { requireInvariantPositions(Recompile(baseRequest(ShaderStage::Vertex)), "vertex shader"); } +std::uint32_t clipDistanceLength(const ShaderRecompiler::RecompileResult& result) { + const auto& words = result.spirv.Words(); + std::uint32_t variable = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto count = words[at] >> 16u; + if (static_cast(words[at] & 0xffffu) == spv::OpDecorate && count == 4 && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == spv::BuiltInClipDistance) variable = words[at + 1]; + } + if (variable == 0) return 0; + std::uint32_t pointer = 0; + std::uint32_t array = 0; + std::uint32_t length = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpVariable && words[at + 2] == variable) pointer = words[at + 1]; + } + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpTypePointer && words[at + 1] == pointer) array = words[at + 3]; + } + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpTypeArray && words[at + 1] == array) length = words[at + 3]; + } + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpConstant && words[at + 2] == length) return words[at + 3]; + } + throw std::runtime_error("the clip distance array has no constant length"); +} + +constexpr std::array DistanceExports{0xf80000dfu, 0u, 0xf80000efu, 0u, 0xf80008cfu, 0u, 0xbf810000u}; + +void verifyGuestClipDistances() { + using namespace ShaderRecompiler; + for (const auto [enabled, expected] : {std::pair{0x007fu, 8u}, std::pair{0x00ffu, 8u}, std::pair{0xf007u, 4u}, std::pair{0xf00fu, 4u}}) { + auto request = baseRequest(ShaderStage::Vertex); + request.shader.code = DistanceExports; + request.context.vertex->paClVsOutCntl = 0x00c00000u | enabled; + const auto length = clipDistanceLength(Recompile(request)); + if (length != expected) throw std::runtime_error("clip and cull distance enables " + std::to_string(enabled) + " gave a clip distance array of " + std::to_string(length)); + } +} + void verifyMesh() { using namespace ShaderRecompiler; static constexpr std::array capabilities{spv::CapabilityMeshShadingEXT}; @@ -81,6 +120,7 @@ void verifyMesh() { int main() { try { verifyVertex(); + verifyGuestClipDistances(); verifyMesh(); std::cout << "Vertex and mesh position outputs are Invariant\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp index da2323b3de..f55704d524 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp @@ -275,7 +275,7 @@ std::vector convertPathPresent(const Context& context, const Source& return bytesOf(readback, bytes); } -std::vector passPresent(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, VkExtent2D extent, VkFormat format, std::uint32_t layer = 0, float whiteNits = AgcDriver::SdrToneMapLuminanceNits) { +std::vector passPresent(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, VkExtent2D extent, VkFormat format, std::uint32_t layer = 0, float whiteNits = AgcDriver::SdrToneMapLuminanceNits, VkColorSpaceKHR colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { Image destination(context, format, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); const auto bytes = static_cast(extent.width) * extent.height * 4; Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); @@ -283,7 +283,7 @@ std::vector passPresent(const Context& context, PresentationPass& pas { CommandBatch batch(context); const auto commands = batch.Handle(); - pass.RecordPresent(commands, 0, destination.Handle(), format, extent, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + pass.RecordPresent(commands, 0, destination.Handle(), format, colorSpace, extent, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); readImage(context, commands, destination, readback); batch.SubmitAndWait(); } @@ -350,6 +350,48 @@ bool renderable(const Context& context, VkFormat format) { return (properties.optimalTilingFeatures & needed) == needed; } +void passPassesPqThrough(const Context& context, PresentationPass& pass, const Source& source, const Image& resident) { + const bool redLowSource = AgcDriver::DisplayRedLow(source.pixelFormat); + for (const auto format : {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}) { + if (!renderable(context, format)) { + std::printf(" 10-bit destination VkFormat %d is not renderable here; skipped\n", static_cast(format)); + continue; + } + const bool redLowDestination = format == VK_FORMAT_A2B10G10R10_UNORM_PACK32; + const VkExtent2D extent{source.width + 40, source.height}; + const auto rect = AgcDriver::ComputeContainRect_nid_postfix(source.width, source.height, extent.width, extent.height); + const auto drawn = passPresent(context, pass, source, resident, extent, format, 0, 1000.0f, VK_COLOR_SPACE_HDR10_ST2084_EXT); + for (std::uint32_t y = 0; y < extent.height; ++y) { + for (std::uint32_t x = 0; x < extent.width; ++x) { + std::uint32_t word = 0; + std::memcpy(&word, drawn.data() + (static_cast(y) * extent.width + x) * 4, 4); + if (static_cast(x) < rect.x || static_cast(x) >= rect.x + static_cast(rect.width)) { + Require(word == 0xc0000000u, "an HDR10 letterbox band is not opaque PQ black: " + describe(source, extent)); + continue; + } + std::uint32_t sourceWord = 0; + std::memcpy(&sourceWord, source.words.data() + (static_cast(y) * source.width + (x - static_cast(rect.x))) * 4, 4); + const auto red = [](std::uint32_t value, bool redLow) { return redLow ? value & 0x3ffu : (value >> 20u) & 0x3ffu; }; + const auto blue = [](std::uint32_t value, bool redLow) { return redLow ? (value >> 20u) & 0x3ffu : value & 0x3ffu; }; + Require(red(word, redLowDestination) == red(sourceWord, redLowSource) && ((word >> 10u) & 0x3ffu) == ((sourceWord >> 10u) & 0x3ffu) && blue(word, redLowDestination) == blue(sourceWord, redLowSource) && (word >> 30u) == 3u, "the HDR10 presentation changed the PQ code values: " + describe(source, extent) + " into VkFormat " + std::to_string(static_cast(format))); + } + } + } +} + +void passRefusesSdrToHdr10(const Context& context, PresentationPass& pass) { + const auto source = makeSource(Bgra8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 200, 130); + const auto resident = residentImage(context, source); + bool refused = false; + try { + static_cast(passPresent(context, pass, source, *resident, {200, 130}, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 0, AgcDriver::SdrToneMapLuminanceNits, VK_COLOR_SPACE_HDR10_ST2084_EXT)); + } catch (const std::exception&) { + refused = true; + } + pass.Release(0); + Require(refused, "the presentation pass presented an SDR display buffer to an HDR10 destination"); +} + void passKeepsTenBits(const Context& context, PresentationPass& pass, const Source& source, const Image& resident) { const bool redLowSource = AgcDriver::DisplayRedLow(source.pixelFormat); for (const auto format : {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}) { @@ -510,7 +552,9 @@ void residentPresentTests(const Context& context) { const auto resident = residentImage(context, source); passConvertsPq(context, pass, source, *resident, AgcDriver::SdrToneMapLuminanceNits); passConvertsPq(context, pass, source, *resident, 1000.0f); + passPassesPqThrough(context, pass, source, *resident); } + passRefusesSdrToHdr10(context, pass); passPresentsSlice(context, pass); blitLetterboxMatchesClear(context); } diff --git a/core/libs/prx/libSceAgcDriver/tests/ProfileOutput.cpp b/core/libs/prx/libSceAgcDriver/tests/ProfileOutput.cpp index 72fb292b4e..0314674b2a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ProfileOutput.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ProfileOutput.cpp @@ -90,13 +90,17 @@ void ConcurrentWriters() { } void WriterFailure() { - AgcDriver::ProfileOutput output([](std::string_view) { throw std::runtime_error("test writer failure"); }); + AgcDriver::ProfileOutput output([](std::string_view) { throw std::runtime_error("test writer failure"); }, 8192); output.Write("test"); - for (int operation = 0; operation < 2; ++operation) { + for (int operation = 0; operation < 6; ++operation) { bool failed = false; try { if (operation == 0) output.Flush(); - else output.Write("next"); + else if (operation == 1) output.Write("next"); + else if (operation == 2) { output.Stop(); output.Write("after failed stop"); } + else if (operation == 3) output.Print("late failure"); + else if (operation == 4) output.Print("%s", std::string(6000, 'x').c_str()); + else output.Print("%s", std::string(10000, 'x').c_str()); } catch (const std::runtime_error& error) { failed = std::string(error.what()) == "test writer failure"; } @@ -106,16 +110,44 @@ void WriterFailure() { void ExplicitStop() { std::string written; - AgcDriver::ProfileOutput output([&](std::string_view text) { written += text; }); + AgcDriver::ProfileOutput output([&](std::string_view text) { written += text; }, 8192); output.Write("pending output"); output.Stop(); output.Stop(); output.Flush(); Check(written == "pending output", "explicit stop did not drain pending output"); - bool failed = false; - try { output.Write("after stop"); } - catch (const std::runtime_error& error) { failed = std::string(error.what()) == "profile output is stopping"; } - Check(failed, "write after stop was accepted"); + Check(!output.Write("after stop"), "write after stop was accepted"); + Check(!output.Write(""), "empty write after stop was accepted"); + output.Print("late small diagnostic %d", 1); + output.Print("%s", std::string(6000, 'x').c_str()); + output.Print("%s", std::string(10000, 'x').c_str()); + output.Flush(); + Check(written == "pending output", "late output reached the stopped writer"); +} + +void StopWithProducers() { + std::string written; + AgcDriver::ProfileOutput output([&](std::string_view text) { written += text; }); + std::atomic accepted{0}; + std::atomic start{false}; + std::vector producers; + for (int thread = 0; thread < 8; ++thread) { + producers.emplace_back([&] { + start.wait(false); + for (int item = 0; item < 1000; ++item) { + if (output.Write("x")) ++accepted; + } + }); + } + Check(output.Write("x"), "initial write was rejected"); + ++accepted; + start.store(true); + start.notify_all(); + output.Stop(); + for (auto& producer : producers) producer.join(); + output.Flush(); + Check(written == std::string(accepted.load(), 'x'), "stop did not drain exactly the accepted output"); + Check(!output.Write("late"), "concurrent stop left the writer open"); } } @@ -127,6 +159,7 @@ int main() { ConcurrentWriters(); WriterFailure(); ExplicitStop(); + StopWithProducers(); std::cout << "profile output tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index ce1b896d3e..8a0b4d0ddc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -3,6 +3,7 @@ #include "Triangle_vert_spv.h" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" @@ -27,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" @@ -35,25 +38,36 @@ #include "VertexRange_vert_spv.h" #include "VertexRangeProducer_frag_spv.h" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp" +#include "BdaAbi.hpp" +#include "ShaderCacheSourceVersion.hpp" #include #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX #endif +#include #include #else +#include #include +#include #endif #include #include #include #include +#include #include #include +#include #include #include +#include +#include +#include #include #include +#include #include #include #include @@ -68,6 +82,7 @@ int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::siz 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); int APS5_VABI sceKernelMunmap(void*, std::size_t); +int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); } using namespace AgcDriver::Graphics; @@ -102,11 +117,177 @@ void ReleaseWatched(void* block, std::size_t bytes) { #endif } +bool collectMappingHistoryTests() { + constexpr std::size_t bytes = 131072; + auto* memory = static_cast(AllocateWatched(bytes, 65536)); + if (memory == nullptr) return false; + GuestAllocations::Mutation().Add(memory, bytes, true, true, false); + struct Cleanup { + std::uint8_t* memory; + ~Cleanup() { + GuestAllocations::Mutation().Remove(memory); + ReleaseWatched(memory, bytes); + } + } cleanup{memory}; + const auto address = reinterpret_cast(memory); + std::memset(memory, 0x11, bytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto before = AgcDriver::GuestMemory::CollectWrites(address, bytes); + Require(before != 0, "the mapping fixture has no complete write history"); + memory[4096] = 0x22; + GuestAllocations::Mutation().Remove(memory); + GuestAllocations::Mutation().Add(memory, bytes, true, true, false); + Require(AgcDriver::GuestMemory::CollectWrites(address, bytes) != 0, "the replacement mapping lost write tracking"); + Require(!AgcDriver::GuestMemory::UnchangedSince(address, bytes, before), "a cached collect hid a CPU store across allocation replacement"); + Require(memory[4096] == 0x22, "collecting the replacement mapping changed its CPU bytes"); + return true; +} + +bool partialWatchRestorationTests() { +#ifdef _WIN32 + constexpr std::size_t bytes = 131072; + constexpr std::size_t page = 4096; + constexpr std::size_t restoredOffset = 4 * page; + constexpr std::size_t restoredBytes = 4 * page; + if (!AgcDriver::GuestMemory::WriteWatched()) return false; + auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536)); + if (memory == nullptr) return false; + GuestArena::GuestArenaCommit_nid_postfix(memory, bytes, PAGE_READWRITE, restoredBytes); + struct Cleanup { + std::uint8_t* memory; + ~Cleanup() { ReleaseWatched(memory, bytes); } + } cleanup{memory}; + const auto address = reinterpret_cast(memory); + AgcDriver::GuestMemory::Unwatch(address, 65536); + Require(!AgcDriver::GuestMemory::Watched(address, page), "the partial remap fixture was not excluded"); + memory[page] = 0x22; + AgcDriver::GuestMemory::MarkWritten(address + page, 1); + Require(AgcDriver::GuestMemory::UntrackedHint(address + page, 1) != 0, "the excluded remap fixture has no driver hint"); + GuestArena::GuestArenaReset_nid_postfix(memory + restoredOffset, restoredBytes); + GuestArena::GuestArenaCommit_nid_postfix(memory + restoredOffset, restoredBytes, PAGE_READWRITE, restoredBytes); + Require(AgcDriver::GuestMemory::Watched(address + restoredOffset, restoredBytes), "a partial private remap did not restore its write watch"); + Require(!AgcDriver::GuestMemory::Watched(address, restoredOffset), "a partial remap restored an excluded prefix"); + Require(!AgcDriver::GuestMemory::Watched(address + restoredOffset + restoredBytes, page), "a partial remap restored an excluded suffix"); + Require(AgcDriver::GuestMemory::UntrackedHint(address + page, 1) == 0, "a partial remap retained a stale driver hint"); + std::memset(memory + restoredOffset, 0x33, restoredBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto generation = AgcDriver::GuestMemory::CollectWrites(address + restoredOffset, restoredBytes); + Require(generation != 0, "the private remap has no collected CPU write history"); + memory[restoredOffset + page] = 0x44; + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(AgcDriver::GuestMemory::CollectWrites(address + restoredOffset, restoredBytes) != 0, "the private remap stopped collecting CPU writes"); + Require(!AgcDriver::GuestMemory::UnchangedSince(address + restoredOffset, restoredBytes, generation), "the private remap hid a genuine CPU write"); + GuestArena::GuestArenaReset_nid_postfix(memory, restoredBytes); + GuestArena::GuestArenaCommit_nid_postfix(memory, restoredBytes, PAGE_READWRITE, restoredBytes); + Require(AgcDriver::GuestMemory::Watched(address, restoredBytes), "the first private pages did not regain coverage"); + Require(!AgcDriver::GuestMemory::Watched(address + 12 * page, page), "restoring the first pages restored an excluded tail"); + memory[12 * page] = 0x55; + AgcDriver::GuestMemory::MarkWritten(address + 12 * page, 1); + AgcDriver::GuestMemory::Unwatch(address + 12 * page, page); + Require(AgcDriver::GuestMemory::UntrackedHint(address + 12 * page, 1) == 0, "excluding a partially watched block invented a write-watch hint"); + return true; +#else + return false; +#endif +} + +std::optional selectDevice(std::span devices, std::string_view request) { + const auto rankDeviceType = [](VkPhysicalDeviceType type) { + switch (type) { + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3; + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 2; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 1; + default: return 0; + } + }; + std::optional selected; + int selectedRank = -1; + for (std::size_t index = 0; index < devices.size(); ++index) { + const auto& candidate = devices[index]; + if (candidate.apiVersion < VK_API_VERSION_1_1) continue; + const std::string_view name(candidate.deviceName); + if (!request.empty() && std::search(name.begin(), name.end(), request.begin(), request.end(), [](char left, char right) { + return std::tolower(static_cast(left)) == std::tolower(static_cast(right)); + }) == name.end()) continue; + const int rank = request.empty() ? rankDeviceType(candidate.deviceType) : 4; + if (rank <= selectedRank) continue; + selected = index; + selectedRank = rank; + } + return selected; +} + +bool hintMappingHistoryTests() { +#ifdef _WIN32 + constexpr std::size_t bytes = 131072; + auto* memory = static_cast(AllocateWatched(bytes, 65536)); + if (memory == nullptr) return false; + GuestAllocations::Mutation().Add(memory, bytes, true, true, false); + struct Cleanup { + std::uint8_t* memory; + ~Cleanup() { + GuestAllocations::Mutation().Remove(memory); + ReleaseWatched(memory, bytes); + } + } cleanup{memory}; + const auto address = reinterpret_cast(memory); + std::memset(memory, 0x11, bytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + AgcDriver::GuestMemory::Unwatch(address, bytes); + const auto before = AgcDriver::GuestMemory::UntrackedHint(address, bytes); + Require(before != 0, "the hint mapping fixture has no write hint"); + Require(AgcDriver::GuestMemory::UntrackedHint(address, bytes) == before, "an unchanged hint range advanced its history"); + memory[4096] = 0x22; + GuestAllocations::Mutation().Remove(memory); + GuestAllocations::Mutation().Add(memory, bytes, true, true, false); + const auto after = AgcDriver::GuestMemory::UntrackedHint(address, bytes); + Require(after > before, "a cached hint walk hid a CPU store across allocation replacement"); + Require(memory[4096] == 0x22 && memory[bytes - 4096] == 0x11, "collecting replacement hints changed guest bytes"); + GuestAllocations::Mutation().Remove(memory); + GuestAllocations::Mutation().Add(memory, bytes, true, true, false); + Require(AgcDriver::GuestMemory::UntrackedHint(address, bytes) == after, "allocation replacement invented a CPU write hint"); + return true; +#else + return false; +#endif +} + +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 { public: - Device() { + explicit Device(bool settleOnOtherFamily = false) { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); #else @@ -127,28 +308,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"); @@ -170,10 +337,32 @@ public: while (family < count && (families[family].queueFlags & VK_QUEUE_COMPUTE_BIT) == 0) ++family; Require(family < count, "no Vulkan compute queue"); const float priority = 1; + const float priorities[2]{1, 1}; VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; queue.queueFamilyIndex = family; queue.queueCount = 1; queue.pQueuePriorities = &priority; + VkDeviceQueueCreateInfo settle{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + settle.queueCount = 1; + settle.pQueuePriorities = &priority; + constexpr std::uint32_t noSettleFamily = UINT32_MAX; + std::uint32_t settleFamily = noSettleFamily; +#ifdef _WIN32 + if (!settleOnOtherFamily && families[family].queueCount >= 2) { + queue.queueCount = 2; + queue.pQueuePriorities = priorities; + settleFamily = family; + } else { + const auto copies = [&](std::uint32_t index) { return index != family && families[index].queueCount != 0 && (families[index].queueFlags & (VK_QUEUE_TRANSFER_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) != 0; }; + for (std::uint32_t index = 0; index < families.size() && settleFamily == noSettleFamily; ++index) { + if (copies(index) && (families[index].queueFlags & (VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) == 0) settleFamily = index; + } + for (std::uint32_t index = 0; index < families.size() && settleFamily == noSettleFamily; ++index) { + if (copies(index)) settleFamily = index; + } + settle.queueFamilyIndex = settleFamily; + } +#endif VkPhysicalDeviceFeatures enabled{}; enabled.shaderInt64 = VK_TRUE; graphics = (families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0; @@ -183,6 +372,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}; @@ -237,6 +428,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}; @@ -251,8 +464,9 @@ public: address.pNext = &robustness; } VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &address}; - device.queueCreateInfoCount = 1; - device.pQueueCreateInfos = &queue; + const std::array queueInfos{queue, settle}; + device.queueCreateInfoCount = settleFamily != noSettleFamily && settleFamily != family ? 2 : 1; + device.pQueueCreateInfos = queueInfos.data(); device.enabledExtensionCount = static_cast(extensionsEnabled.size()); device.ppEnabledExtensionNames = extensionsEnabled.data(); device.pEnabledFeatures = &enabled; @@ -264,13 +478,24 @@ 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); + context.queueFamily = family; 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"); +#ifdef _WIN32 + if (settleFamily != noSettleFamily) { + context.Function("vkGetDeviceQueue")(context.device, settleFamily, settleFamily == family ? 1 : 0, &context.settleQueue); + pool.queueFamilyIndex = settleFamily; + Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.settlePool), "vkCreateCommandPool settle"); + context.settleFamily = settleFamily; + context.settleQueueMutex = &settleMutex; + } +#endif } catch (...) { release(); throw; @@ -281,6 +506,19 @@ public: bool NullDescriptors() const { return nullDescriptors; } const Context& GetContext() const { return context; } bool Graphics() const { return graphics; } + bool Timestamps() const { + std::uint32_t count = 0; + const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); + queues(context.physical, &count, nullptr); + std::vector families(count); + queues(context.physical, &count, families.data()); + return families.at(context.queueFamily).timestampValidBits != 0; + } + VkPhysicalDeviceProperties Properties() const { + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + return properties; + } void WaitQueue() const { Check(context.Function("vkQueueWaitIdle")(context.queue), "vkQueueWaitIdle"); } private: @@ -292,6 +530,8 @@ private: } void release() noexcept { + if (context.device != VK_NULL_HANDLE) ClearHostImports(context.device); + if (context.settlePool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.settlePool, nullptr); if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); context.bufferPool.reset(); if (context.device != VK_NULL_HANDLE) { @@ -307,6 +547,7 @@ private: VkInstance instance = VK_NULL_HANDLE; Context context{}; bool graphics = false; + std::mutex settleMutex; bool nullDescriptors = false; }; @@ -453,6 +694,39 @@ void copiedDirectOrderingTests(const Device& device, Recorder& recorder) { update(direct, 2); recorder.Sync(); Require(*block == 45, "direct consumers lost GPU-ordered updates"); + { + std::array unregistered{}; + struct Finish { + Recorder& recorder; + ~Finish() { recorder.Sync(); } + } finish{recorder}; + const auto copiedAddress = reinterpret_cast(unregistered.data()); + auto mixedProgram = program; + auto& mixedBinding = mixedProgram.bindings[0]; + mixedBinding.count = 2; + mixedBinding.guestDescriptor.insert(mixedBinding.guestDescriptor.end(), {static_cast(copiedAddress), static_cast(copiedAddress >> 32u) & 0xffffu, 1024, 0x31000000u}); + mixedBinding.bufferWritten = {true, true}; + const CompiledShader mixedCompute{ShaderRecompiler::ShaderStage::Compute, &mixedProgram, 0}; + for (const bool draw : {false, true}) { + auto mixed = std::make_shared(context, mixedCompute); + Require(mixed->HasCopiedWrites() && mixed->WritesOverlap(address, 4) && !mixed->CopiedWritesOverlap(address, 4) && mixed->CopiedWritesOverlap(copiedAddress, 4), "the mixed producer did not retain direct and CPU-copied writes"); + auto& pending = draw ? *DrawCopiedWriters() : writers; + static_cast(recorder.Commands()); + recorder.Keep(mixed); + mixed->MarkGpuWrites(recorder); + pending.push_back(mixed); + auto completed = std::make_shared(0); + recorder.OnComplete([mixed, &pending, completed] { + mixed->WriteBackBuffers(); + std::erase(pending, mixed); + ++*completed; + }); + Require(!CompletePendingCpuWrites(context, address, 4), "a direct write in a mixed producer caused a CPU write-back wait"); + Require(*completed == 0 && !pending.empty(), "a disjoint copied write-back was completed while binding a direct buffer"); + Require(CompletePendingCpuWrites(context, copiedAddress, 4), "a CPU-copied write in a mixed producer was not completed"); + Require(*completed == 1 && pending.empty(), "the required CPU write-back did not release the mixed producer"); + } + } { TextureDetiler detiler(context); auto imageContext = context; @@ -731,6 +1005,190 @@ void pendingWriteSnapshotBenchmark(Recorder& recorder) { recorder.Sync(); } +void readListTests(const Device& device, Recorder& recorder) { + using Ranges = Recorder::ReadList; + Require(Recorder::ValidReadList({{0x1000, 0x2000}, {0x2000, 0x2100}}) && Recorder::ValidReadList({}), "a sorted disjoint list is refused"); + Require(!Recorder::ValidReadList({{0x2000, 0x2100}, {0x1000, 0x2000}}) && !Recorder::ValidReadList({{0x1000, 0x2001}, {0x2000, 0x2100}}) && !Recorder::ValidReadList({{0x1000, 0x1000}}), "an unsorted, overlapping or empty list is accepted"); + std::uint64_t seed = 0x9e3779b97f4a7c15ull; + const auto next = [&](std::uint64_t bound) { + seed = seed * 6364136223846793005ull + 1442695040888963407ull; + return (seed >> 33u) % bound; + }; + const auto makeList = [&](std::uint64_t base, std::size_t count) { + auto list = std::make_shared(); + auto at = base; + for (std::size_t index = 0; index < count; ++index) { + at += next(3) * 0x100; + const auto bytes = (next(16) + 1) * 0x10; + list->emplace_back(at, at + bytes); + at += bytes; + } + return list; + }; + const auto first = makeList(0x100000, 300); + const auto second = makeList(0x100000, 120); + const Ranges before{{0x100400, 0x100410}, {0x200000, 0x200100}}; + const Ranges between{{0x100800, 0x100900}, {0x300000, 0x300040}}; + const Ranges after{{0x100000, 0x180000}}; + const auto note = [&](bool listed) { + recorder.NotePendingReads(before, Kind::DispatchElement); + if (listed) recorder.NotePendingReadList(first, Kind::AddressBased); + else recorder.NotePendingReads(*first, Kind::AddressBased); + recorder.NotePendingReads(between, Kind::GpuCopy); + if (listed) { + recorder.NotePendingReadList(second, Kind::Indirect); + recorder.NotePendingReadList(first, Kind::AddressBased); + } else { + recorder.NotePendingReads(*second, Kind::Indirect); + } + recorder.NotePendingReads(after, Kind::CopySource); + }; + std::vector> queries; + for (const auto* list : {first.get(), second.get()}) { + for (const auto& [begin, end] : *list) { + for (const auto at : {begin - 1, begin, end - 1, end}) queries.emplace_back(at, 1 + next(0x80)); + } + } + for (int query = 0; query < 2000; ++query) queries.emplace_back(0xff000 + next(0x300000), 1 + next(0x2000)); + const auto answers = [&] { + std::vector result; + for (const auto& [address, bytes] : queries) { + const auto described = recorder.DescribePendingRead(address, bytes); + result.push_back(recorder.PendingReadOverlaps(address, bytes) ? 1 : 0); + result.push_back(described.has_value() ? 10 + static_cast(described->kind) : -1); + } + recorder.Submit(); + for (const auto& [address, bytes] : queries) result.push_back(recorder.PendingReadOverlaps(address, bytes, false) ? 1 : 0); + recorder.Sync(); + Require(recorder.Idle() && !recorder.PendingReadOverlaps(0x100000, 0x200000, false), "listed reads outlived their batch"); + return result; + }; + const auto notedBefore = Recorder::ReadCounts().noted; + note(false); + const auto plain = answers(); + const auto notedPlain = Recorder::ReadCounts().noted - notedBefore; + note(true); + const auto listed = answers(); + const auto notedListed = Recorder::ReadCounts().noted - notedBefore - notedPlain; + Require(plain == listed, "reads noted by reference answer a query differently from the same ranges noted one by one"); + Require(notedListed == notedPlain + first->size(), "a list's ranges are not counted as noted"); + for (const auto kind : {Kind::DispatchElement, Kind::AddressBased, Kind::CopySource}) Require(std::count(plain.begin(), plain.end(), 10 + static_cast(kind)) != 0, "the queries miss a reader kind: the comparison proves too little"); + recorder.NotePendingReadList(std::make_shared(), Kind::AddressBased); + recorder.NotePendingReadList(nullptr, Kind::AddressBased); + Require(!recorder.Recording(), "an empty list opened a batch"); + static_cast(device); +} + +struct TestRange { + std::uint64_t begin; + std::uint64_t end; +}; + +void overlappedRangeTests(Recorder& recorder) { + Require(recorder.Idle(), "the recorder is busy before the overlap tests"); + recorder.NotePendingWrite(0x210000, 0x100); + recorder.NotePendingWrite(0x210400, 0x10); + recorder.Submit(); + recorder.NotePendingWrite(0x210800, 0x200); + recorder.NotePendingWrite(0x210900, 0x200); + const std::array ranges{{{0x20f000, 0x210000}, {0x210000, 0x210080}, {0x210080, 0x210400}, {0x210400, 0x210410}, {0x210410, 0x210800}, {0x210800, 0x210801}, {0x210801, 0x210a00}, {0x210a00, 0x210b00}, {0x210b00, 0x211000}, {0x300000, 0x300010}}}; + std::vector> writes; + recorder.PendingWriteRanges(writes); + Require(writes.size() == 4, "the pending write ranges miss a batch's writes"); + std::vector indices; + std::size_t matched = 0; + for (const std::size_t first : {0u, 3u, 6u, 10u}) { + OverlappedRanges(std::span(ranges), first, writes, indices); + Require(std::is_sorted(indices.begin(), indices.end()) && std::adjacent_find(indices.begin(), indices.end()) == indices.end(), "the overlapped ranges are not sorted and unique"); + for (std::size_t index = 0; index < ranges.size(); ++index) { + const bool listed = std::find(indices.begin(), indices.end(), index) != indices.end(); + const bool pending = index >= first && recorder.PendingWriteOverlaps(ranges[index].begin, static_cast(ranges[index].end - ranges[index].begin)); + Require(listed == pending, "a range is listed as overlapped differently from PendingWriteOverlaps"); + matched += listed ? 1 : 0; + } + } + Require(matched == 12, "the overlap comparison covered too few overlapped ranges"); + recorder.Sync(); + recorder.PendingWriteRanges(writes); + Require(writes.empty(), "pending write ranges outlived their batches"); +} + +void spaceWriteSyncTests(const Device& device, Recorder& recorder) { + const auto& context = device.GetContext(); + if (context.hostImportAlignment == 0) { + std::cout << "host imports unavailable: address space pending writes not tested\n"; + return; + } + constexpr std::size_t bytes = 0x10000; + constexpr std::size_t count = 4; + void* block = ::operator new(2 * count * bytes, std::align_val_t{65536}); + std::memset(block, 0, 2 * count * bytes); + auto* memory = static_cast(block); + const auto registry = [&](bool add) { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) { + if (add) mutation.Add(memory + 2 * index * bytes, bytes, true, false, true); + else mutation.Remove(memory + 2 * index * bytes); + } + }; + const auto rangeAt = [&](std::size_t index) { return reinterpret_cast(memory + 2 * index * bytes); }; + const auto build = [&] { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + leased.AddReadable(rangeAt(0) + 64, 16); + leased.Upload(true); + const auto reads = leased.DeviceReads(); + leased.WriteBack(); + return reads.size(); + }; + registry(true); + Require(recorder.Idle(), "the recorder is busy before the address space tests"); + const auto regions = build(); + if (regions < count) { + registry(false); + ::operator delete(block, std::align_val_t{65536}); + std::cout << "the registered ranges are not served in place: address space pending writes not tested\n"; + return; + } + const std::array value{std::byte{0x5a}, std::byte{0xa5}, std::byte{0x5a}, std::byte{0xa5}}; + const auto landed = [&](std::uint64_t address) { return std::memcmp(reinterpret_cast(address), value.data(), value.size()) == 0; }; + const auto inside = rangeAt(2) + 0x100; + static_cast(recorder.Commands()); + recorder.AfterCompletions(inside, value, 7000, 0, false); + Require(Recorder::PendingCompletionLabels() == 1 && !landed(inside), "the label inside a registered range is not pending"); + build(); + Require(landed(inside) && Recorder::PendingCompletionLabels() == 0, "a build over the address space did not complete the label pending in one of its ranges"); + recorder.Sync(); + const auto gap = rangeAt(2) + bytes + 0x100; + static_cast(recorder.Commands()); + recorder.AfterCompletions(gap, value, 7001, 0, false); + build(); + Require(!landed(gap) && Recorder::PendingCompletionLabels() == 1, "a build over the address space completed a label outside its ranges"); + recorder.Sync(); + Require(landed(gap) && Recorder::PendingCompletionLabels() == 0, "the label outside the ranges did not land"); + static_cast(recorder.Commands()); + recorder.AfterCompletions(rangeAt(1) + 0x40, value, 7002, 0, false); + recorder.Submit(); + static_cast(recorder.Commands()); + recorder.AfterCompletions(rangeAt(3) + 0x40, value, 7003, 0, false); + build(); + Require(landed(rangeAt(1) + 0x40) && landed(rangeAt(3) + 0x40) && Recorder::PendingCompletionLabels() == 0, "a build over the address space missed a label behind an earlier sync"); + recorder.Sync(); + static_cast(recorder.Commands()); + recorder.AfterCompletions(rangeAt(1) + 0x80, value, 7004, 0, false); + bool noted = false; + recorder.OnComplete([&] { + static_cast(recorder.Commands()); + recorder.AfterCompletions(rangeAt(3) + 0x80, value, 7005, 0, false); + noted = true; + }); + build(); + Require(noted && landed(rangeAt(1) + 0x80) && landed(rangeAt(3) + 0x80) && Recorder::PendingCompletionLabels() == 0, "a build over the address space missed a label a completion noted during its sync"); + recorder.Sync(); + registry(false); + ::operator delete(block, std::align_val_t{65536}); +} + void writeSettledTests(const Device& device, Recorder& recorder) { Require(recorder.PendingWriteSettled(0x50000, 0x100), "an unwritten range is not settled"); recorder.NotePendingWrite(0x50000, 0x100); @@ -794,6 +1252,67 @@ void completionCountTests(const Device& device, Recorder& recorder) { Require(memory[0] == 1 && memory[2] == (storeAlways ? 1u : 7u) && memory[16] == 1, "completion stores ran for the wrong labels (overlapped and not-imported ones store, the untouched GPU-stored one skips)"); } +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + Require(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + _dup2(_fileno(file), _fileno(stderr)); +#else + saved = dup(fileno(stderr)); + dup2(fileno(file), fileno(stderr)); +#endif + } + + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + +std::size_t DeferredFailureLines(Recorder& recorder, std::size_t count, const std::function& failure, std::string* log = nullptr) { + StderrCapture capture; + for (std::size_t index = 0; index < count; ++index) recorder.OnComplete(failure); + recorder.Sync(); + const auto text = capture.Finish(); + if (log != nullptr) *log = text; + std::size_t lines = 0; + for (auto at = text.find("deferred write-back failed"); at != std::string::npos; at = text.find("deferred write-back failed", at + 1)) ++lines; + return lines; +} + +void deferredFailureReportTests(Recorder& recorder) { + std::uint32_t serial = 0; + const auto plain = [&serial] { throw std::runtime_error("image table: poisoned entry " + std::to_string(serial++)); }; + const auto unaligned = [](std::uint32_t instruction) { return [instruction] { throw BdaUnalignedAccess("BDA access failed: unaligned", instruction); }; }; + Require(DeferredFailureLines(recorder, 12, plain) == 12, "a deferred failure other than an unaligned BDA access was not printed every time"); + Require(DeferredFailureLines(recorder, 12, unaligned(0x5c8)) == 8, "the first 8 deferred reports of an unaligned BDA access were not the only ones printed"); + std::string log; + Require(DeferredFailureLines(recorder, 1012, unaligned(0x5c8), &log) == 1 && log.find("pc 0x5c8 1024 times so far") != std::string::npos, "the 1024th deferred report of an unaligned BDA access was not the only one printed"); + Require(DeferredFailureLines(recorder, 12, unaligned(0x7a0)) == 8, "unaligned BDA reports at one instruction limited the printing of the reports at another"); + Require(DeferredFailureLines(recorder, 12, plain) == 12, "unaligned BDA reports limited the printing of other deferred failures"); +} + void directCompletionLabelTests(Recorder& recorder) { using namespace AgcDriver::GuestMemory; constexpr std::size_t size = 65536; @@ -859,6 +1378,44 @@ void afterRecordedWorkTests(const Device& device, Recorder& recorder) { Require(seen == 1, "an action ran before the completion label recorded ahead of it"); } +int gpuTimingCompletionTests(const Device& device, bool incomplete) { + const auto& context = device.GetContext(); + if (!device.Timestamps()) { + std::cout << "skipped, queue timestamp queries unavailable\n"; + return 77; + } + Require(Recorder::GpuTimingEnabled(), "GPU timing completion test requires APS5_PROFILE_GPU"); + std::unique_lock gpu(GpuMutex()); + int completed = 0; + Recorder recorder(context); + recorder.Activate(); + for (int round = 0; round < 3; ++round) { + const bool missingEnd = incomplete && round == 1; + const auto first = recorder.BeginGpuTiming(0x12340); + Require(first != Recorder::NoTiming, "first GPU timing range was not allocated"); + if (!missingEnd) recorder.EndGpuTiming(first); + const auto second = recorder.BeginGpuTiming(0x12341); + Require(second != Recorder::NoTiming && second != first, "second GPU timing range was not allocated"); + recorder.EndGpuTiming(second); + recorder.OnComplete([&] { ++completed; }); + const auto serial = recorder.Submissions() + 1; + recorder.Submit(); + recorder.Sync(); + Require(completed == round + 1 && recorder.Idle(), "GPU timing prevented batch completion"); + std::size_t missing = 0; + const auto batches = recorder.CompletedBatches(serial - 1, serial, missing); + Require(missing == 0 && batches.size() == 1 && batches.front().serial == serial, "GPU timing batch completion record is missing"); + const auto& batch = batches.front(); + if (missingEnd) { + Require(batch.gpuStartNs == 0 && batch.gpuEndNs == 0, "incomplete query batch published GPU timings"); + } else { + Require(batch.gpuStartNs > 0 && batch.gpuEndNs >= batch.gpuStartNs, "complete query batch lost GPU timings"); + } + } + std::cout << "GPU timing batch completion checked, incomplete=" << incomplete << '\n'; + return 0; +} + void batchStampTests(Recorder& recorder) { Require(recorder.Idle(), "batch stamps: the recorder is busy"); double previousEnd = 0; @@ -1304,6 +1861,475 @@ 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; +} + +bool settleStorageImageTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + const auto& base = device.GetContext(); + constexpr std::size_t unit = 65536; + constexpr std::array red{255, 0, 0, 255}; + if (base.hostImportAlignment == 0 || base.hostImportAlignment > unit || base.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { + std::cout << "host imports within 64 KiB, a settle queue or write watching unavailable: a settled import under a storage image not tested\n"; + return false; + } + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(base); + if (!settledImportsWatchPrivate(base)) { + std::cout << "this device compares every Windows import: a settled import under a storage image not tested\n"; + return false; + } + void* block = AllocateWatched(unit, unit); + Require(block != nullptr, "(i) no watched block although write watching is on"); + struct Release { + void* block; + ~Release() { ReleaseWatched(block, unit); } + } release{block}; + const auto address = reinterpret_cast(block); + std::memset(block, 0x5a, unit); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, unit, true, true, true); + } + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = 128; + resource.height = 128; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + Require(DescribeSurface(resource).guestBytes == unit, "(i) unexpected surface size"); + Require(Watched(address, unit) && ExistingHostImportFor(context, address, unit) == nullptr, "(i) the block was imported or out of the write watch before the image was made"); + Recorder recorder(context); + recorder.Activate(); + const auto before = HostImportSettles(); + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const auto* import = ExistingHostImportFor(context, address, unit); + Require(import != nullptr && import->unwatched && !Watched(address, unit), "(i) the image's first upload did not import its block compared"); + const auto decided = [&] { + const auto now = HostImportSettles(); + return now.rewatched != before.rewatched || now.keptCompared != before.keptCompared; + }; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!decided() && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + } + Require(decided(), "(i) the settle read of the image's import did not finish"); + const auto after = HostImportSettles(); + const bool returned = Watched(address, unit); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const VkClearColorValue value{{1.0f, 0.0f, 0.0f, 1.0f}}; + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + image->Refresh(); + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + std::vector read(unit); + AgcDriver::GuestMemory::Read(address, read); + std::size_t lost = 0; + for (std::size_t at = 0; at < unit; at += 4) { + for (std::size_t c = 0; c < 4; ++c) { + if (std::to_integer(read[at + c]) == red[c]) continue; + ++lost; + break; + } + } + std::cout << "(i) the image's import " << (returned ? "returned to the write watch" : "stayed compared") << " after its settle read (" << after.rewatched - before.rewatched << " back in the write watch, " << after.keptCompared - before.keptCompared << " kept compared); the refresh after the image's GPU results lost " << lost << " of " << unit / 4 << " texels\n"; + Require(lost == 0, "(i) the refresh after the image's GPU results dropped " + std::to_string(lost) + " of " + std::to_string(unit / 4) + " texels: the import's return to the write watch stamped its block as a CPU store after the image took its generation"); + Require(!returned && after.keptCompared == before.keptCompared + 1, "(i) the settled import under a live storage image returned to the write watch"); + } + recorder.Sync(); + recorder.FlushDeferredReleases(); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, unit); + std::cout << "a settled import under a storage image tests passed\n"; + return true; +} +#endif + void remappedImportTests(const Device& device) { const auto& context = device.GetContext(); if (context.hostImportAlignment == 0) { @@ -1498,6 +2524,318 @@ void secondMappingImportTests(const Device& device) { Require(second != 0 && second != first, "the import of direct memory whose pages were replaced was kept"); HostImportFor(context, address, bytes); } + +void requireImportServesGuest(const Context& context, Recorder& recorder, const HostImport& import, std::uint64_t begin, std::uint64_t end, std::uint32_t salt, const std::string& what) { + using namespace AgcDriver::GuestMemory; + constexpr std::size_t page = 4096; + constexpr std::size_t probe = 256; + const auto bytes = static_cast(end - begin); + Require(begin >= import.base && end <= import.base + import.bytes, what + " is not inside its import"); + const auto collected = CollectWritesUncached(begin, bytes); + Require(collected != 0 && Watched(begin, bytes), what + " left the write watch"); + Require(UnchangedSinceCollected(begin, page, collected) && UnchangedSinceCollected(end - page, page, collected), what + " reports writes nobody made"); + auto* first = reinterpret_cast(begin); + auto* last = reinterpret_cast(end - page); + for (std::size_t index = 0; index < probe; ++index) { + first[index] = static_cast((index * 7u + salt) & 0xffu); + last[index] = static_cast((index * 13u + salt * 5u + 1u) & 0xffu); + } + Require(!UnchangedSinceCollected(begin, page, collected) && !UnchangedSinceCollected(end - page, page, collected), "a CPU store into " + what + " was not collected"); + Buffer readback(context, 2 * probe, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(context, commands, import.buffer, begin - import.base, readback.Handle(), 0, probe); + CopyBuffer(context, commands, import.buffer, end - page - import.base, readback.Handle(), probe, probe); + 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(); + readback.Invalidate(); + const auto seen = readback.Bytes(); + Require(std::equal(first, first + probe, seen.begin()) && std::equal(last, last + probe, seen.begin() + probe), "the GPU reads other bytes than the guest's through " + what); +} + +int reimportBench(const Device& device) { + const auto& context = device.GetContext(); + if (context.hostImportAlignment == 0 || context.settleQueue == VK_NULL_HANDLE || !AgcDriver::GuestMemory::WriteWatched()) { + std::cout << "skipped, the re-import bench requires host imports, a settle queue and write watching\n"; + return 77; + } + constexpr std::size_t bytes = 1126ull << 20u; + constexpr std::size_t step = 1440ull << 10u; + constexpr std::size_t guard = 16ull << 10u; + constexpr std::size_t inside = 256ull << 20u; + constexpr std::size_t moves = 5; + constexpr std::size_t page = 4096; + constexpr std::size_t alignment = 2ull << 20u; + constexpr std::size_t requiredImportBytes = bytes + inside; + const auto availableImportBytes = HostImportBudgetRemaining(context); + if (availableImportBytes < requiredImportBytes) { + std::cout << "skipped, the re-import bench needs " << (requiredImportBytes >> 20u) << " MiB of simultaneous host imports; the current budget has " << (availableImportBytes >> 20u) << " MiB available\n"; + return 77; + } + struct Release { + void* mapping = nullptr; + std::int64_t physical = -1; + ~Release() { + if (mapping != nullptr) sceKernelMunmap(mapping, bytes); + if (physical >= 0) sceKernelReleaseDirectMemory(physical, bytes); + } + }; + enum class Cut : std::uint8_t { Head, Tail, Inside }; + struct Pattern { + Cut cut; + const char* name; + const char* moving; + }; + constexpr std::array patterns{{ + {Cut::Head, "head", "the head loses GPU access 1440 KiB at a time"}, + {Cut::Tail, "tail", "the tail loses GPU access 1440 KiB at a time"}, + {Cut::Inside, "inside", "a 16 KiB page inside the kept side loses all access, 256 MiB in and then 1440 KiB above the one before"}, + }}; + const auto milliseconds = [](std::chrono::steady_clock::time_point from, std::chrono::steady_clock::time_point to) { return std::chrono::duration(to - from).count(); }; + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + Recorder recorder(context); + recorder.Activate(); + const auto cleanup = [&] { + if (!gpu.owns_lock()) gpu.lock(); + recorder.Sync(); + recorder.FlushDeferredReleases(); + ClearHostImports(context.device); + }; + std::uint32_t salt = 0; + const auto measure = [&](const Pattern& pattern) { + Release release; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, alignment, 0, &release.physical) == 0, "allocate the re-import bench backing"); + Require(sceKernelMapDirectMemory(&release.mapping, bytes, 0x33, 0, release.physical, alignment) == 0, "map the re-import bench backing"); + const auto address = reinterpret_cast(release.mapping); + auto* memory = static_cast(release.mapping); + for (std::size_t offset = 0; offset < bytes; offset += 4 * page) std::memset(memory + offset, 0x5a, 64); + std::cout << "re-import bench, " << pattern.name << " cuts: " << (bytes >> 20u) << " MiB of singly mapped direct memory at 0x" << std::hex << address << std::dec << ", watched=" << AgcDriver::GuestMemory::Watched(address, bytes) << ", 1 of 4 pages nonzero, " << pattern.moving << '\n'; + gpu.lock(); + auto begin = address; + auto end = address + bytes; + std::size_t again = 0; + double slowest = 0; + for (std::size_t move = 0; move <= moves; ++move) { + double moveMs = 0; + std::uint64_t smaller = 0; + std::uint64_t smallerEnd = 0; + if (move != 0) { + std::uint64_t from = 0; + std::uint64_t to = 0; + if (pattern.cut == Cut::Head) { + from = begin; + to = begin = from + step; + } else if (pattern.cut == Cut::Tail) { + to = end; + from = end = to - step; + } else { + from = move == 1 ? address + inside : begin + step; + to = from + guard; + smaller = begin; + smallerEnd = from; + begin = to; + } + gpu.unlock(); + const auto moving = std::chrono::steady_clock::now(); + Require(sceKernelMprotect(reinterpret_cast(from), static_cast(to - from), pattern.cut == Cut::Inside ? 0 : 0x03) == 0, "cut the re-import bench range"); + moveMs = milliseconds(moving, std::chrono::steady_clock::now()); + gpu.lock(); + } + const auto started = std::chrono::steady_clock::now(); + const auto* import = HostImportFor(context, begin, static_cast(end - begin)); + const auto imported = std::chrono::steady_clock::now(); + Require(import != nullptr && import->base == begin && import->base + import->bytes == end, "the re-import bench range was not imported whole"); + const char* kind = import->alias != nullptr ? "through a shared alias" : import->unwatched ? "compared" : "settled and watched"; + const bool kept = import->memoryBase == address && import->pinnedBytes == bytes; + if (move != 0) { + again += kept ? 0 : 1; + slowest = std::max(slowest, milliseconds(started, imported)); + } + double smallerMs = 0; + if (smallerEnd != smaller) { + const auto importing = std::chrono::steady_clock::now(); + Require(HostImportFor(context, smaller, static_cast(smallerEnd - smaller)) != nullptr, "the smaller side of a cut inside the re-import bench range was not imported"); + smallerMs = milliseconds(importing, std::chrono::steady_clock::now()); + } + const auto syncing = std::chrono::steady_clock::now(); + recorder.Sync(); + recorder.FlushDeferredReleases(); + const auto synced = std::chrono::steady_clock::now(); + std::printf("re-import %s %zu: %.1f MiB at +%.2f MiB, %s, %s, cut %.2f ms, import %.2f ms", pattern.name, move, static_cast(end - begin) / 1048576.0, static_cast(begin - address) / 1048576.0, kind, move == 0 ? "first import" : kept ? "kept" : "imported again", moveMs, milliseconds(started, imported)); + if (smallerEnd != smaller) std::printf(", smaller side %.2f MiB imported in %.2f ms", static_cast(smallerEnd - smaller) / 1048576.0, smallerMs); + std::printf(", recorder sync after it %.2f ms, %.1f MiB imported in all\n", milliseconds(syncing, synced), static_cast(HostImportLiveBytes()) / 1048576.0); + std::fflush(stdout); + } + const auto* tail = HostImportFor(context, begin, static_cast(end - begin)); + Require(tail != nullptr && tail->base == begin && tail->base + tail->bytes == end, "the re-import bench lost the import of its kept side"); + requireImportServesGuest(context, recorder, *tail, begin, end, ++salt, std::string("the kept side after the ") + pattern.name + " cuts"); + std::printf("re-import bench, %s cuts: the kept side was imported again after %zu of %zu cuts, slowest import after a cut %.2f ms; CPU stores into it are collected and the GPU reads them through its buffer\n", pattern.name, again, moves, slowest); + cleanup(); + gpu.unlock(); + return again; + }; + std::string importedAgain; + gpu.unlock(); + try { + for (const auto& pattern : patterns) { + if (measure(pattern) != 0) importedAgain += std::string(importedAgain.empty() ? "" : ", ") + pattern.name; + } + } catch (...) { + cleanup(); + throw; + } + gpu.lock(); + Require(importedAgain == "head, tail, inside", "a cut kept memory imported across the changed host range instead of importing the remaining side again: " + importedAgain); + std::printf("re-import bench: every cut at the head, at the tail and inside the range retired the original import before importing the remaining side\n"); + return 0; +} + +int hostImportCutTests(const Device& device) { + const auto& context = device.GetContext(); + if (context.hostImportAlignment == 0 || context.settleQueue == VK_NULL_HANDLE || !AgcDriver::GuestMemory::WriteWatched()) { + std::cout << "skipped, the host import cut tests require host imports, a settle queue and write watching\n"; + return 77; + } + constexpr std::size_t bytes = 32ull << 20u; + constexpr std::size_t run = 2ull << 20u; + constexpr std::size_t guard = 16ull << 10u; + constexpr std::size_t page = 4096; + constexpr std::size_t inside = 6ull << 20u; + struct Case { + const char* name; + std::size_t offset; + std::size_t length; + bool unmap; + std::size_t kept; + std::size_t keptEnd; + std::size_t smaller; + std::size_t smallerEnd; + }; + constexpr std::array cases{{ + {"a page made inaccessible inside the import, nearer its start", inside + guard, guard, false, inside + 2 * guard, bytes, 0, inside + guard}, + {"a page made inaccessible inside the import, nearer its end", bytes - inside - 2 * guard, guard, false, 0, bytes - inside - 2 * guard, bytes - inside - guard, bytes}, + {"an unmap of a 2 MiB run inside the import", inside, run, true, inside + run, bytes, 0, inside}, + {"an unmap of the import's first 2 MiB run", 0, run, true, run, bytes, 0, 0}, + {"an unmap of 1440 KiB at the start of the import's first 2 MiB run", 0, 1440ull << 10u, true, 1440ull << 10u, bytes, 0, 0}, + }}; + struct Backing { + void* mapping = nullptr; + std::int64_t physical = -1; + std::size_t hole = 0; + std::size_t holeBytes = 0; + ~Backing() { + auto* memory = static_cast(mapping); + if (memory != nullptr && hole != 0) sceKernelMunmap(memory, hole); + if (memory != nullptr && hole + holeBytes < bytes) sceKernelMunmap(memory + hole + holeBytes, bytes - hole - holeBytes); + if (physical >= 0) sceKernelReleaseDirectMemory(physical, bytes); + } + }; + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + Recorder recorder(context); + recorder.Activate(); + const auto cleanup = [&] { + if (!gpu.owns_lock()) gpu.lock(); + recorder.Sync(); + recorder.FlushDeferredReleases(); + ClearHostImports(context.device); + }; + std::uint32_t salt = 0; + const auto check = [&](const Case& test) { + const std::string name = test.name; + Backing backing; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, run, 0, &backing.physical) == 0, "allocate the host import cut test backing"); + Require(sceKernelMapDirectMemory(&backing.mapping, bytes, 0x33, 0, backing.physical, run) == 0, "map the host import cut test backing"); + const auto address = reinterpret_cast(backing.mapping); + auto* memory = static_cast(backing.mapping); + for (std::size_t offset = 0; offset < bytes; offset += page) std::memset(memory + offset, static_cast(0x40u + (offset / page) % 64u), 64); + gpu.lock(); + const auto* whole = HostImportFor(context, address, bytes); + Require(whole != nullptr && whole->base == address && whole->bytes == bytes && whole->alias == nullptr && !whole->unwatched, name + ": the range was not imported whole, settled and watched"); + auto* cut = memory + test.offset; + gpu.unlock(); + if (test.unmap) { + Require(sceKernelMunmap(cut, test.length) == 0, name + ": the unmap failed"); + backing.hole = test.offset; + backing.holeBytes = test.length; + } else { + Require(sceKernelMprotect(cut, test.length, 0) == 0, name + ": the protection failed"); + } + gpu.lock(); + const auto begin = address + test.kept; + const auto end = address + test.keptEnd; + const auto* kept = HostImportFor(context, begin, static_cast(end - begin)); + Require(kept != nullptr && kept->base == begin && kept->base + kept->bytes == end, name + ": the larger side is not imported whole"); + Require(kept->memoryBase == begin && kept->pinnedBytes == end - begin, "the side of " + name + " is served from the import that pinned the changed host range"); + requireImportServesGuest(context, recorder, *kept, begin, end, ++salt, "the larger side of " + name); + if (test.smallerEnd != test.smaller) { + const auto* smaller = HostImportFor(context, address + test.smaller, test.smallerEnd - test.smaller); + Require(smaller != nullptr && smaller->base == address + test.smaller && smaller->bytes == test.smallerEnd - test.smaller && smaller->memoryBase == address + test.smaller && smaller->pinnedBytes == test.smallerEnd - test.smaller, "the smaller side of " + name + " is not imported apart from the larger one"); + requireImportServesGuest(context, recorder, *smaller, address + test.smaller, address + test.smallerEnd, ++salt, "the smaller side of " + name); + Require(HostImportFor(context, begin, static_cast(end - begin)) == kept && kept->memoryBase == begin && kept->pinnedBytes == end - begin, "importing the smaller side of " + name + " replaced the safe import of the larger one"); + } + if (test.unmap) { + gpu.unlock(); + void* again = cut; + Require(sceKernelMapDirectMemory(&again, test.length, 0x33, 0x10, backing.physical + static_cast(test.offset), guard) == 0 && again == cut, name + ": the pages could not be mapped again"); + backing.hole = 0; + backing.holeBytes = 0; + gpu.lock(); + const auto from = reinterpret_cast(cut); + const auto* remapped = HostImportFor(context, address, bytes); + Require(remapped != nullptr && remapped->base == address && remapped->bytes == bytes, name + ": the range mapped again is not imported whole"); + Require(remapped->memoryBase == address && remapped->pinnedBytes == bytes, "after " + name + " the pages mapped again are served from the import that pinned the old ones"); + requireImportServesGuest(context, recorder, *remapped, from, from + test.length, ++salt, "the pages mapped again after " + name); + requireImportServesGuest(context, recorder, *remapped, begin, end, ++salt, "the larger side after " + name + " and a new mapping"); + } + cleanup(); + gpu.unlock(); + std::cout << "host import after cut: " << name << ": the changed import was retired before the remaining range was imported, and the GPU and write watch see the guest's bytes" << (test.unmap ? ", also after the pages were mapped again" : "") << '\n'; + }; + const auto remapWhileTrimmed = [&] { + const std::string name = "an unmap and a new mapping of the retained side while the old import is retired"; + Backing backing; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, run, 0, &backing.physical) == 0, "allocate the host import cut test backing"); + Require(sceKernelMapDirectMemory(&backing.mapping, bytes, 0x33, 0, backing.physical, run) == 0, "map the host import cut test backing"); + const auto address = reinterpret_cast(backing.mapping); + auto* memory = static_cast(backing.mapping); + for (std::size_t offset = 0; offset < bytes; offset += page) std::memset(memory + offset, static_cast(0x40u + (offset / page) % 64u), 64); + gpu.lock(); + const auto* whole = HostImportFor(context, address, bytes); + Require(whole != nullptr && whole->base == address && whole->bytes == bytes && whole->alias == nullptr && !whole->unwatched, name + ": the range was not imported whole, settled and watched"); + gpu.unlock(); + Require(sceKernelMprotect(memory, run, 0x03) == 0, name + ": the protection of the head failed"); + Require(sceKernelMunmap(memory + run, bytes - run) == 0, name + ": the unmap of the kept side failed"); + backing.hole = run; + backing.holeBytes = bytes - run; + void* again = memory + run; + Require(sceKernelMapDirectMemory(&again, bytes - run, 0x33, 0x10, backing.physical + static_cast(run), guard) == 0 && again == memory + run, name + ": the kept side could not be mapped again"); + backing.hole = 0; + backing.holeBytes = 0; + gpu.lock(); + const auto begin = address + run; + const auto end = address + bytes; + const auto* remapped = HostImportFor(context, begin, static_cast(end - begin)); + Require(remapped != nullptr && remapped->base == begin && remapped->base + remapped->bytes == end, name + ": the pages mapped again are not imported whole"); + requireImportServesGuest(context, recorder, *remapped, begin, end, ++salt, "the pages mapped again after " + name); + Require(remapped->memoryBase == begin && remapped->pinnedBytes == end - begin, "after " + name + " the pages mapped again are served from the import that pinned the old ones"); + cleanup(); + gpu.unlock(); + std::cout << "host import cut: " << name << ": the pages mapped again were imported again and the GPU and the write watch see the guest's bytes\n"; + }; + gpu.unlock(); + try { + for (const auto& test : cases) check(test); + remapWhileTrimmed(); + } catch (...) { + cleanup(); + throw; + } + gpu.lock(); + std::cout << "Host import cut lifetime tests passed\n"; + return 0; +} #endif void movedMetadataTests(const Device& device, Recorder& recorder) { @@ -1750,16 +3088,19 @@ 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); const auto copy = snapshotContext.Function("vkCmdCopyBuffer"); - VkBufferCopy region{0, 0, elementBytes}; + VkBufferCopy region{first->snapshots[0].offset, 0, elementBytes}; copy(commands, first->snapshots[0].buffer->Handle(), downloaded->Handle(), 1, ®ion); + region.srcOffset = second->snapshots[0].offset; region.dstOffset = elementBytes; copy(commands, second->snapshots[0].buffer->Handle(), downloaded->Handle(), 1, ®ion); RecordMemoryBarrier(snapshotContext, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); @@ -1785,11 +3126,11 @@ void resourceReadTests(const Device& device, Recorder& recorder) { HostImportFor(context, address, bytes); } -void readWrittenStagingTests(const Device& device, Recorder& recorder) { +bool readWrittenStagingTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); if (context.hostImportAlignment == 0) { std::cout << "host imports unavailable: read and written staging not tested\n"; - return; + return false; } constexpr std::size_t bytes = 8u << 20u; #ifdef _WIN32 @@ -1806,7 +3147,7 @@ void readWrittenStagingTests(const Device& device, Recorder& recorder) { } if (HostImportFor(context, address, bytes) == nullptr) { std::cout << "host import of the test block refused: read and written staging not tested\n"; - return; + return false; } const auto boundInPlace = [&](std::uint64_t element, std::size_t elementBytes, bool read) { GuestBufferMemory memory(context); @@ -1823,10 +3164,10 @@ void readWrittenStagingTests(const Device& device, Recorder& recorder) { Require(!boundInPlace(address + 4096, 1u << 20u, false), "a written 1 MiB element was not staged"); Require(boundInPlace(address + 4096, 4u << 20u, false), "a written-only 4 MiB element was staged past the written window"); Require(!boundInPlace(address + 4096, 4u << 20u, true), "a read and written 4 MiB element was bound in place"); - const auto buildInPlace = [&](std::uint64_t dispatchThreads, bool read) { + const auto buildInPlace = [&](std::uint64_t dispatchThreads, bool read, std::uint32_t stride, std::uint32_t extent) { const auto element = address + 4096; - constexpr std::uint32_t stride = 16; - constexpr std::uint32_t records = (4u << 20u) / stride; + constexpr std::uint32_t elementBytes = 4u << 20u; + const std::uint32_t records = stride != 0 ? elementBytes / stride : elementBytes; ShaderRecompiler::RecompileResult program; ShaderRecompiler::DescriptorBinding binding; binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; @@ -1837,25 +3178,202 @@ void readWrittenStagingTests(const Device& device, Recorder& recorder) { binding.guestDescriptor = {static_cast(element), (static_cast(element >> 32u) & 0xffffu) | (stride << 16u), records, 0x31000000u}; binding.bufferWritten = {true}; binding.bufferRead = {read}; + binding.bufferExtent = {extent}; program.bindings.push_back(binding); const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; ShaderResources resources(context, compute, {}, false, dispatchThreads); const auto reads = resources.InPlaceReads(); - const bool direct = std::any_of(reads.begin(), reads.end(), [&](const auto& range) { return range.first <= element && range.second >= element + records * stride; }); + const bool direct = std::any_of(reads.begin(), reads.end(), [&](const auto& range) { return range.first <= element && range.second >= element + elementBytes; }); resources.MarkGpuWrites(recorder); recorder.Sync(); return direct; }; - Require(!buildInPlace((4u << 20u) / 16u, true), "a dispatch sweeping a read and written 4 MiB element bound it in place"); - Require(buildInPlace(1024, true), "a dispatch touching 16 KiB of a read and written 4 MiB element staged all of it"); - Require(buildInPlace(0, true), "a dispatch of unknown size staged a read and written 4 MiB element"); - Require(buildInPlace((4u << 20u) / 16u, false), "a dispatch sweeping a written-only 4 MiB element staged it"); + Require(!buildInPlace((4u << 20u) / 16u, true, 16, 0), "a dispatch sweeping a read and written 4 MiB element bound it in place"); + Require(buildInPlace(1024, true, 16, 0), "a dispatch touching 16 KiB of a read and written 4 MiB element staged all of it"); + Require(buildInPlace(0, true, 16, 0), "a dispatch of unknown size staged a read and written 4 MiB element"); + Require(buildInPlace((4u << 20u) / 16u, false, 16, 0), "a dispatch sweeping a written-only 4 MiB element staged it"); + Require(!buildInPlace((4u << 20u) / 32u, true, 0, 32), "a dispatch sweeping a read and written 4 MiB raw element bound it in place"); + Require(buildInPlace(1024, true, 0, 32), "a dispatch touching 32 KiB of a read and written 4 MiB raw element staged all of it"); + Require(buildInPlace((4u << 20u) / 32u, true, 0, 0), "a raw element without an access extent was staged"); Require(std::all_of(static_cast(block), static_cast(block) + bytes, [](std::byte value) { return value == std::byte{0x11}; }), "a staged copy-back changed the guest bytes"); { GuestAllocations::Mutation mutation; mutation.Remove(block); } HostImportFor(context, address, bytes); + return true; +} + +bool sparseRunImportTests(const Device& device, bool copied) { + const auto& context = device.GetContext(); + constexpr std::size_t unit = 65536; + constexpr std::size_t heap = 8 * unit; + struct Heap { + const Context& context; + void* area = nullptr; + std::uint8_t* guest = nullptr; + std::size_t mapped = 0; + ~Heap() { + ClearHostImports(context.device); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < mapped; ++index) mutation.Remove(guest + index * unit); + } + SetHostImportWindow(0); +#ifdef _WIN32 + if (area != nullptr) VirtualFree(area, 0, MEM_RELEASE); +#else + if (area != nullptr) munmap(area, heap + unit); +#endif + } + } state{context}; +#ifdef _WIN32 + state.area = VirtualAlloc(nullptr, heap, MEM_RESERVE, PAGE_NOACCESS); + Require(state.area != nullptr, "cannot reserve the sparse run heap"); + state.guest = static_cast(state.area); +#else + void* area = mmap(nullptr, heap + unit, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(area != MAP_FAILED, "cannot reserve the sparse run heap"); + state.area = area; + state.guest = reinterpret_cast((reinterpret_cast(area) + unit - 1) & ~std::uintptr_t{unit - 1}); +#endif + auto* guest = state.guest; + const auto address = reinterpret_cast(guest); + const auto map = [&] { + auto* piece = guest + state.mapped * unit; +#ifdef _WIN32 + Require(VirtualAlloc(piece, unit, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot map a piece of the sparse run heap"); +#else + Require(mprotect(piece, unit, PROT_READ | PROT_WRITE) == 0, "cannot map a piece of the sparse run heap"); +#endif + for (std::size_t at = 0; at < unit; ++at) piece[at] = static_cast((state.mapped * unit + at) * 7u + 3u); + GuestAllocations::Mutation mutation; + mutation.Add(piece, unit, true, true, true); + ++state.mapped; + }; + for (int piece = 0; piece < 3; ++piece) map(); + PrepareImportWatch(context); + SetHostImportWindow(unit); + if (HostImportFor(context, address, unit) == nullptr) { + std::cout << "host import of the sparse run heap refused: sparse run imports not tested\n"; + return false; + } + Recorder recorder(context); + recorder.Activate(); + const auto run = [&] { return HostImportFor(context, address, state.mapped * unit); }; + const auto bound = [&](GuestBufferMemory& memory, std::uint64_t begin, std::uint64_t bytes) { + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(begin, bytes, adjustment); + const auto* import = run(); + return import != nullptr && view.buffer == import->buffer; + }; + const auto upload = [&](bool written, std::uint64_t pastRun, const std::function& check) { + GuestBufferMemory memory(context); + if (written) memory.AddWritable(address, heap); + else memory.AddReadable(address, heap); + if (pastRun != 0) memory.AddReadable(address + pastRun, 16); + memory.Upload(true); + check(memory); + memory.WriteBack(); + }; + const auto committed = 3 * unit; + upload(false, 0, [&](GuestBufferMemory& memory) { + Require(!bound(memory, address, heap) && memory.InPlaceReads().empty(), "a sparse run heap was joined the first time its run was seen"); + }); + if (copied) { + upload(false, 5 * unit, [&](GuestBufferMemory& memory) { + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(address, heap, adjustment); + Require(!bound(memory, address, heap) && memory.InPlaceReads().empty(), "a sparse run heap with a view past its stable run is bound in place"); + Buffer readback(context, committed, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + { + std::unique_lock gpu(GpuMutex()); + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(context, commands, view.buffer, view.offset, readback.Handle(), 0, committed); + 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(); + } + readback.Invalidate(); + Require(std::memcmp(readback.Bytes().data(), guest, committed) == 0, "the GPU does not read a copied sparse run heap's mapped bytes"); + }); + recorder.Sync(); + std::cout << "sparse run import tests passed (copied)\n"; + return true; + } + upload(false, 0, [&](GuestBufferMemory& memory) { + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(address, heap, adjustment); + const auto ranges = memory.AddressRanges(); + Require(adjustment == 0 && view.range == committed && ranges.size() == 1 && ranges[0].begin == address && ranges[0].end == address + committed, "a sparse run heap's view or address range is not cut at its last committed byte"); + const auto* import = run(); + Require(import != nullptr && view.buffer == import->buffer && view.offset == address - import->base && ranges[0].deviceAddress == import->address + (address - import->base), "a sparse run heap is not bound in place from its run's import"); + const auto direct = memory.DirectRegions(); + const auto reads = memory.InPlaceReads(); + const auto cuts = memory.CutRegions(); + Require(direct.has_value() && direct->size() == 1 && direct->front() == std::pair{address, address + committed} && reads.size() == 1 && reads.front() == direct->front(), "a sparse run heap bound in place is not named up to its import's end for reuse"); + Require(cuts.size() == 1 && cuts.front().begin == address && cuts.front().end == address + heap && cuts.front().committedEnd == address + committed, "a sparse run heap bound in place does not report its cut"); + Buffer readback(context, committed, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + { + std::unique_lock gpu(GpuMutex()); + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(context, commands, view.buffer, view.offset, readback.Handle(), 0, committed); + 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(); + } + readback.Invalidate(); + Require(std::memcmp(readback.Bytes().data(), guest, committed) == 0, "the GPU does not read a sparse run heap's bytes through its in-place view"); + }); + upload(true, 0, [&](GuestBufferMemory& memory) { + Require(!bound(memory, address, heap), "a written sparse run heap is bound in place"); + }); + upload(false, 5 * unit, [&](GuestBufferMemory& memory) { + Require(!bound(memory, address, heap), "a sparse run heap with a view past its run is bound in place"); + }); + { + auto regionContext = context; + DescriptorCache cache(regionContext); + regionContext.descriptorCache = &cache; + Recorder regionRecorder(regionContext); + regionRecorder.Activate(); + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding binding; + binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + binding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + binding.descriptorSet = 0; + binding.binding = 0; + binding.count = 1; + binding.guestDescriptor = {static_cast(address), static_cast(address >> 32u) & 0xffffu, static_cast(heap), 0x31000000u}; + binding.bufferWritten = {false}; + program.bindings.push_back(binding); + program.pushConstants.resize(16); + program.memoryOffsetDword = 0; + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + { + ShaderResources resources(regionContext, compute); + const auto reads = resources.InPlaceReads(); + Require(resources.Reusable() && reads.size() == 1 && reads.front() == std::pair{address, address + committed}, "a set over a sparse run heap bound in place is not reusable"); + Require(resources.Revalidate(compute), "a set over an unchanged sparse run heap failed its proof"); + map(); + Require(!resources.Revalidate(compute), "a set over a sparse run heap mapped past its cut was reused"); + regionRecorder.Submit(); + device.WaitQueue(); + regionRecorder.Sync(); + } + recorder.Activate(); + upload(false, 0, [&](GuestBufferMemory& memory) { + Require(!bound(memory, address, heap), "a grown sparse run heap was joined again the first time its grown run was seen"); + }); + upload(false, 0, [&](GuestBufferMemory& memory) { + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(address, heap, adjustment); + Require(bound(memory, address, heap) && view.range == 4 * unit, "a grown sparse run heap is not joined again over its grown run"); + }); + } + recorder.Sync(); + std::cout << "sparse run import tests passed (in place)\n"; + return true; } void misalignedRegionTests(const Device& device, Recorder& recorder) { @@ -1933,12 +3451,13 @@ void misalignedRegionTests(const Device& device, Recorder& recorder) { const auto check = [&] { const auto bindings = resources.PrepareDrawBindings(regionRecorder); Require(bindings != nullptr && bindings->snapshots.size() == 1, "the read-only misaligned draw input was not snapshotted"); - const auto contents = bindings->snapshots.front().buffer->Bytes(); + const auto contents = bindings->snapshots.front().Bytes(); Require(bindings->snapshots.front().address == view - adjustment && contents.size() == adjustment + elementBytes, "the draw snapshot does not start at the aligned offset below the view"); Require(std::memcmp(contents.data() + adjustment, guest + offset, elementBytes) == 0, "the shader's patched offset into the draw snapshot misses the view's bytes"); }; check(); guest[offset + 8] ^= 0xffu; + AgcDriver::GuestMemory::MarkWritten(view, elementBytes); check(); regionRecorder.Sync(); } @@ -2017,9 +3536,9 @@ void misalignedSnapshotTests(const Device& device, Recorder& recorder) { Require(push[0] == std::byte{0} && adjustment == offset % alignment, "the inner view's shader offset is not its distance from the binding"); const auto bindings = resources.PrepareDrawBindings(snapshotRecorder); Require(bindings != nullptr && bindings->snapshots.size() == 2, "read-only draw inputs were not snapshotted"); - const auto outerContents = bindings->snapshots[0].buffer->Bytes(); + const auto outerContents = bindings->snapshots[0].Bytes(); Require(outerContents.size() >= outerBytes && std::memcmp(outerContents.data(), guest + outer, outerBytes) == 0, "an aligned draw snapshot misses its view's bytes"); - const auto contents = bindings->snapshots[1].buffer->Bytes(); + const auto contents = bindings->snapshots[1].Bytes(); Require(contents.size() >= adjustment + elementBytes, "a draw snapshot ends before the bytes the shader reads"); Require(std::memcmp(contents.data() + adjustment, guest + offset, elementBytes) == 0, "the shader's offset into a draw snapshot misses the view's bytes"); snapshotRecorder.Sync(); @@ -2032,6 +3551,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")) { @@ -2050,6 +3570,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]; @@ -2099,7 +3620,8 @@ void drawDescriptorWriteTests(const Device& device, Recorder& recorder) { const auto original = updates.calls.front(); Require(original.buffers.size() == 2 && original.images.size() == 2, "descriptor test lacks mixed bindings and an image array"); for (std::size_t allocation = 0; allocation < 2; ++allocation) { - const ShaderResources::MovedBuffer moved{allocation, 0, 16, {5, 6, 7, 8}}; + const std::array words{5, 6, 7, 8}; + const ShaderResources::MovedBuffer moved{allocation, 0, 16, words}; 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"); Require(updates.calls.size() == allocation + 2 && updates.copies == 0, "draw descriptors copied GPU memory or needed multiple updates"); @@ -2120,6 +3642,145 @@ 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 std::array movedWords{5, 6, 7, 8}; + const ShaderResources::MovedBuffer moved{0, 0, 16, movedWords}; + 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 imageTableFirstLayerRevalidateTests(const Device& device, Recorder& recorder) { + namespace Abi = ShaderRecompiler::ImageTableAbi; + using ShaderRecompiler::DescriptorKind; + using ShaderRecompiler::DescriptorRole; + const auto& base = device.GetContext(); + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + if (context.imageTableCapacity < 1u) { + std::cout << "image tables unavailable: image table first-layer revalidation not tested\n"; + return; + } + constexpr std::uint32_t width = 4; + constexpr std::uint32_t height = 4; + constexpr std::uint32_t lastSlice = 3; + alignas(256) static std::array texels{}; + texels.fill(1.0f); + const auto address = static_cast(reinterpret_cast(texels.data())); + const std::array texture{static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (22u << 20u) | (((width - 1u) & 3u) << 30u), ((width - 1u) >> 2u) | ((height - 1u) << 14u), 0xfacu | (13u << 28u), lastSlice, 0u, 0u, 0u}; + const auto whole = DecodeTextureResource(texture); + Require(whole.dimension == TextureDimension::k2DArray && whole.depthOrLastArray == lastSlice && DescribeSurface(whole).guestBytes <= sizeof(texels), "the image table test T# is not a 2D array over its texels"); + constexpr auto bindingCount = static_cast(ShaderRecompiler::RuntimeAbi::Binding::Count); + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding map{}; + map.kind = DescriptorKind::StorageBuffer; + map.role = DescriptorRole::ImageTableMap; + map.binding = static_cast(ShaderRecompiler::RuntimeAbi::Stage::Main) * bindingCount + static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap); + map.count = 1; + map.guestDescriptor.assign(Abi::HeaderWords + 1u, 0u); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderElementCount] = 1u; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords + 1u; + program.bindings.push_back(map); + ShaderRecompiler::DescriptorBinding images{}; + images.kind = DescriptorKind::SampledImage; + images.role = DescriptorRole::ImageTable; + images.binding = ShaderRecompiler::RuntimeAbi::ImageTableBinding; + images.count = 1; + images.guestDescriptor.assign(texture.begin(), texture.end()); + images.imageShapes.assign(1, ShaderRecompiler::DescriptorImageShape::Image2D); + images.imageDepthCompare.assign(1, false); + images.imageSamplers.assign(1, 0u); + program.bindings.push_back(std::move(images)); + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + { + ShaderResources resources(context, compute); + Require(resources.SampledTextures().size() == 1, "a 2D read of a 2D array in an image table bound no texture"); + const auto bound = resources.SampledTextures().front(); + ShaderResources::ProofReport report; + Require(resources.Revalidate(std::span(&compute, 1), &report), "an image table holding a 2D read of a 2D array did not revalidate"); + Require(report.path == ShaderResources::ProofPath::Full, "an image table holding a 2D read of a 2D array was not proven on the full path"); + Require(resources.SampledTextures().front() == bound, "revalidating an image table holding a 2D read of a 2D array changed its texture"); + Require(resources.Revalidate(std::span(&compute, 1)), "an image table holding a 2D read of a 2D array did not revalidate twice"); + recorder.Sync(); + } + ClearCachedTextures(context.device); +} + void drawSnapshotReuseTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); constexpr std::size_t bytes = 65536; @@ -2158,7 +3819,7 @@ void drawSnapshotReuseTests(const Device& device, Recorder& recorder) { return; } const auto element = address + 4096; - constexpr std::size_t elementBytes = 1024; + constexpr std::size_t elementBytes = 8192; ShaderRecompiler::RecompileResult program; ShaderRecompiler::DescriptorBinding binding; binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; @@ -2319,6 +3980,83 @@ void drawSnapshotPatchTests(const Device& device) { } } +void drawSnapshotEpochTests(const Device& device) { + const auto& context = device.GetContext(); + constexpr std::size_t bytes = 4 * 65536; + std::unique_lock gpu(GpuMutex()); + void* block = context.hostImportAlignment != 0 ? AllocateWatched(bytes, 65536) : nullptr; + if (block == nullptr) { + std::cout << "host imports unavailable: draw snapshot reuse within a collect epoch not tested\n"; + return; + } + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + auto* guest = static_cast(block); + for (std::size_t at = 0; at < bytes; ++at) guest[at] = static_cast(at * 7u + 3u); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, reinterpret_cast(block), bytes); + } + } unregister{context, block}; + if (HostImportFor(context, address, bytes) == nullptr) { + std::cout << "host import of the block refused: draw snapshot reuse within a collect epoch not tested\n"; + return; + } + constexpr std::size_t elementOffset = 4096; + constexpr std::size_t elementBytes = 2 * 65536; + const auto element = address + elementOffset; + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding binding; + binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + binding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + binding.descriptorSet = 0; + binding.binding = 0; + binding.count = 1; + binding.guestDescriptor = {static_cast(element), static_cast(element >> 32u) & 0xffffu, static_cast(elementBytes), 0x31000000u}; + binding.bufferWritten = {false}; + program.bindings.push_back(binding); + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + auto snapshotContext = context; + DescriptorCache cache(snapshotContext); + snapshotContext.descriptorCache = &cache; + Recorder snapshotRecorder(snapshotContext); + snapshotRecorder.Activate(); + { + ShaderResources resources(snapshotContext, compute); + const auto snapshot = [&] { + auto bindings = resources.PrepareDrawBindings(snapshotRecorder); + Require(bindings != nullptr && bindings->snapshots.size() == 1, "read-only draw input was not snapshotted"); + auto buffer = bindings->snapshots[0].buffer; + const auto contents = buffer->Bytes(); + Require(contents.size() == elementBytes && std::memcmp(contents.data(), reinterpret_cast(element), elementBytes) == 0, "a draw snapshot does not hold the guest bytes of its draw"); + return buffer; + }; + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto first = snapshot(); + Require(snapshot() == first, "a second draw in the same collect epoch copied its unchanged input again"); + guest[elementOffset + 65536 + 17] ^= 0xffu; + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto changed = snapshot(); + Require(changed != first, "a draw after a store and a new collect epoch reused the snapshot taken before the store"); + Require(snapshot() == changed, "a second draw after the store copied its input again"); + snapshotRecorder.Sync(); + } + std::cout << "draw snapshot reuse within a collect epoch: ok\n"; +} + void drawSnapshotCapTests(const Device& device, Recorder& recorder) { using namespace AgcDriver::GuestMemory; const auto& context = device.GetContext(); @@ -2515,6 +4253,117 @@ void drawSnapshotEvictionTests(const Device& device) { Require(cache.ReusableDrawSnapshot(address, 16) == nullptr && cache.ReusableDrawSnapshot(address, 16) == nullptr, "a snapshot outlived a CPU store"); } +void unwrittenSnapshotTests(const Device& device) { + Recorder cache(device.GetContext()); + const auto buffer = std::make_shared(device.GetContext(), 16, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + constexpr std::uint64_t address = 0x10000000; + const std::array stamp{1, 1, 1}; + const std::array next{1, 2, 1}; + cache.NoteUnwrittenDrawSnapshot(address, 16, stamp, buffer); + Require(cache.UnwrittenDrawSnapshot(address, 16, stamp) == buffer, "a noted unwritten snapshot is not found under its stamp"); + cache.NoteUnwrittenDrawSnapshot(address + 8192, Recorder::DrawSnapshotBudget, stamp, buffer); + Require(cache.UnwrittenDrawSnapshot(address + 8192, Recorder::DrawSnapshotBudget, stamp) == nullptr, "unwritten snapshots grew past the byte budget"); + cache.NoteUnwrittenDrawSnapshot(address, Recorder::DrawSnapshotBudget, stamp, buffer); + Require(cache.UnwrittenDrawSnapshot(address, Recorder::DrawSnapshotBudget, stamp) == buffer, "replacing an unwritten snapshot kept the bytes of the old one"); + Require(cache.UnwrittenDrawSnapshot(address, Recorder::DrawSnapshotBudget, next) == nullptr && cache.UnwrittenDrawSnapshot(address, Recorder::DrawSnapshotBudget, stamp) == nullptr, "an unwritten snapshot outlived a newer stamp"); + for (std::size_t entry = 0; entry < Recorder::UnwrittenSnapshotEntries; ++entry) cache.NoteUnwrittenDrawSnapshot(address + entry * 16, 1, next, buffer); + cache.NoteUnwrittenDrawSnapshot(address + Recorder::UnwrittenSnapshotEntries * 16, 1, next, buffer); + Require(cache.UnwrittenDrawSnapshot(address, 1, next) == buffer && cache.UnwrittenDrawSnapshot(address + Recorder::UnwrittenSnapshotEntries * 16, 1, next) == nullptr, "unwritten snapshots grew past the entry cap"); + const std::array later{1, 3, 1}; + cache.NoteUnwrittenDrawSnapshot(address, 16, later, buffer, 256); + VkDeviceSize offset = 0; + Require(cache.UnwrittenDrawSnapshot(address, 16, later, &offset) == buffer && offset == 256, "an unwritten snapshot lost its offset in its buffer"); +} + +void drawSnapshotUploadTests(const Device& device, Recorder& recorder) { + const auto& context = device.GetContext(); + const auto limit = ShaderResources::DrawUploadLimit(); + if (limit < 256 || limit > 16384) { + std::cout << "draw upload limit outside 256..16384 bytes: draw snapshot uploads not tested\n"; + return; + } + constexpr std::size_t bytes = 65536; + void* block = context.hostImportAlignment != 0 ? AllocateWatched(bytes, 65536) : nullptr; + if (block == nullptr) { + std::cout << "host imports or write watching unavailable: draw snapshot uploads not tested\n"; + return; + } + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + for (std::size_t index = 0; index < bytes; ++index) static_cast(block)[index] = static_cast(index * 7u + 1u); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, reinterpret_cast(block), bytes); + } + } unregister{context, block}; + if (HostImportFor(context, address, bytes) == nullptr) { + std::cout << "host import of the watched block refused: draw snapshot uploads not tested\n"; + return; + } + const std::array elements{address + 4096, address + 8192, address + 32768}; + const std::array sizes{256, limit, limit + 256}; + ShaderRecompiler::RecompileResult program; + for (std::uint32_t index = 0; index < elements.size(); ++index) { + ShaderRecompiler::DescriptorBinding binding; + binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + binding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + binding.descriptorSet = 0; + binding.binding = index; + binding.count = 1; + binding.guestDescriptor = {static_cast(elements[index]), static_cast(elements[index] >> 32u) & 0xffffu, static_cast(sizes[index]), 0x31000000u}; + binding.bufferWritten = {false}; + program.bindings.push_back(binding); + } + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + { + auto snapshotContext = context; + DescriptorCache cache(snapshotContext); + snapshotContext.descriptorCache = &cache; + Recorder snapshotRecorder(snapshotContext); + snapshotRecorder.Activate(); + ShaderResources resources(snapshotContext, compute); + const auto alignment = std::max(snapshotContext.limits.minStorageBufferOffsetAlignment, 16); + const auto bindings = resources.PrepareDrawBindings(snapshotRecorder); + Require(bindings != nullptr && bindings->snapshots.size() == elements.size(), "read-only draw inputs were not snapshotted"); + for (std::size_t index = 0; index < elements.size(); ++index) { + const auto& snapshot = bindings->snapshots[index]; + Require(snapshot.address == elements[index] && snapshot.bytes == sizes[index] && snapshot.offset % alignment == 0, "a draw snapshot is not an aligned copy of its input"); + Require(std::memcmp(snapshot.Bytes().data(), reinterpret_cast(elements[index]), sizes[index]) == 0, "a draw snapshot does not hold the guest bytes of its draw"); + } + Require(bindings->snapshots[0].buffer == bindings->snapshots[1].buffer && bindings->snapshots[0].offset != bindings->snapshots[1].offset, "draw inputs up to the upload limit do not share the recorder's upload chunk"); + Require(bindings->snapshots[2].buffer != bindings->snapshots[0].buffer && bindings->snapshots[2].offset == 0, "a draw input above the upload limit was copied into the upload chunk"); + const auto earlier = bindings->snapshots[0].Bytes(); + const std::vector before(earlier.begin(), earlier.end()); + std::memset(reinterpret_cast(elements[0]), 0x5a, sizes[0]); + const auto again = resources.PrepareDrawBindings(snapshotRecorder); + Require(again != nullptr && again->snapshots.size() == elements.size(), "read-only draw inputs were not snapshotted again"); + const auto copied = again->snapshots[0].Bytes(); + Require(again->snapshots[0].buffer != bindings->snapshots[0].buffer || again->snapshots[0].offset != bindings->snapshots[0].offset, "a draw input copied again took the slice an earlier draw reads"); + Require(std::all_of(copied.begin(), copied.end(), [](std::byte value) { return value == std::byte{0x5a}; }), "a draw snapshot copied again misses the CPU store"); + Require(std::equal(before.begin(), before.end(), earlier.begin()), "copying a draw input again changed the slice an earlier draw reads"); + const auto first = snapshotRecorder.DrawUpload(16); + const auto second = snapshotRecorder.DrawUpload(16); + Require(second.buffer == first.buffer && second.offset >= first.offset + 16 && second.offset % alignment == 0, "upload slices overlap or are not aligned"); + const auto whole = snapshotRecorder.DrawUpload(Recorder::DrawUploadChunk); + Require(whole.buffer != second.buffer && whole.offset == 0, "a full upload chunk was not replaced"); + snapshotRecorder.Sync(); + } + recorder.Activate(); +} + void drawInputReuseTests(const Device& device, Recorder& recorder) { using namespace AgcDriver::GuestMemory; using Use = Recorder::SnapshotUse; @@ -2591,7 +4440,7 @@ void drawInputReuseTests(const Device& device, Recorder& recorder) { recorder.Sync(); } -void drawInputInPlaceTests(const Device& device, Recorder& recorder) { +void drawInputInPlaceTests(const Device& device, Recorder& recorder, bool copied = false) { auto context = device.GetContext(); constexpr std::size_t bytes = 65536; void* block = context.hostImportAlignment != 0 ? AllocateWatched(bytes, 65536) : nullptr; @@ -2626,6 +4475,12 @@ void drawInputInPlaceTests(const Device& device, Recorder& recorder) { context.recorder = &recorder; recorder.Sync(); const auto vertices = address + 4096; + context.drawInputsInPlace = !copied; + if (copied) { + Require(InPlaceDrawInput(context, vertices, 256, 4) == nullptr, "an imported draw input was bound in place where draw inputs are copied"); + Require(!recorder.PendingReadOverlaps(vertices, 256), "a copied draw input's read was noted as an in-place read"); + return; + } const auto* import = InPlaceDrawInput(context, vertices, 256, 4); Require(import != nullptr && import->base <= vertices && vertices + 256 <= import->base + import->bytes, "an imported draw input was not bound in place"); const auto read = recorder.DescribePendingRead(vertices, 256); @@ -2634,10 +4489,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"); @@ -2692,17 +4553,17 @@ void drawInputInPlaceTests(const Device& device, Recorder& recorder) { // makes the unit stale), the scopes, the slab boundary, and the retire publish. With // APS5_UNIT_SHADOW_MIB=8 (one slab) the second slab's allocation evicts the first with a publish; // with APS5_NO_UNIT_SHADOW=1 every primitive is inert. -void unitShadowTests(const Device& device, Recorder& recorder) { +bool unitShadowTests(const Device& device, Recorder& recorder) { using namespace AgcDriver::GuestMemory; const auto& context = device.GetContext(); if (!UnitShadowEnabled()) { Require(!AnyShadowedOverlaps(0x10000, 16) && PublishShadow(0x10000, 16, PublishScope::Whole, PublishReason::Hook) == 0, "unit shadows are off but not inert"); std::cout << "unit shadows off (APS5_NO_UNIT_SHADOW, or no write watching): primitives inert\n"; - return; + return true; } if (context.hostImportAlignment == 0) { std::cout << "host imports unavailable: unit shadows not tested\n"; - return; + return false; } constexpr std::uint64_t unit = 65536; // Three slabs at the default 8 MiB: units 0..127, 128..255, 256..271. @@ -2736,11 +4597,11 @@ void unitShadowTests(const Device& device, Recorder& recorder) { const auto* import = HostImportFor(context, address, bytes); if (import == nullptr) { std::cout << "host import of the shadow test block refused: unit shadows not tested\n"; - return; + return false; } if (!Watched(address, bytes)) { std::cout << "host imports are compared, not watched: unit shadows not tested\n"; - return; + return false; } Require(import->base == address && import->bytes == bytes, "the import does not cover the block"); CollectWritesUncached(address, bytes); @@ -2765,7 +4626,7 @@ void unitShadowTests(const Device& device, Recorder& recorder) { const auto commands = recorder.Commands(); recorder.Keep(pattern); recorder.Keep(destination.slab); - RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); if (seed) { const VkBufferCopy seedCopy{begin - import->base, SlabOffset(*import, *destination.slab, begin), unit}; copyBuffer(commands, import->buffer, destination.buffer, 1, &seedCopy); @@ -2875,6 +4736,83 @@ void unitShadowTests(const Device& device, Recorder& recorder) { Require(words[2 * unit] == 0x99 && words[3 * unit - 1] == 0x99, "the retire did not publish the unit still registered"); Require(words[5 * unit] == 0x88 && words[5 * unit + unit - 1] == 0x88, "the retire did not publish a unit covered by the old registration"); Require(words[127 * unit] == 0x66 && words[128 * unit] == 0x77, "the retire did not publish the second slab's units covered by the old registration"); + return true; +} + +void idlePublishWaitTests(const Device& device, Recorder& recorder) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + constexpr std::uint64_t unit = 65536; + constexpr std::size_t bytes = 8 * unit; + void* block = AllocateWatched(bytes, 65536); + Require(block != nullptr, "unit shadows are on without write watching"); + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + std::memset(block, 0x11, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + } + } unregister{context, block, address}; + const auto* import = HostImportFor(context, address, bytes); + if (import == nullptr || !Watched(address, bytes)) { + std::cout << "host import of the idle publish test block refused or not watched: idle publish wait not tested\n"; + return; + } + recorder.Sync(); + CollectWritesUncached(address, bytes); + auto* words = static_cast(block); + const auto copyBuffer = context.Function("vkCmdCopyBuffer"); + const auto retile = [&](std::uint64_t begin, std::uint8_t value) { + auto destination = ShadowDestinationFor(context, *import, begin, begin + unit); + Require(destination.has_value() && destination->seedUnits.empty(), "a whole-unit retile piece has no destination"); + auto pattern = std::make_shared(context, static_cast(unit), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memset(pattern->Bytes().data(), value, pattern->Bytes().size()); + const auto commands = recorder.Commands(); + recorder.Keep(pattern); + recorder.Keep(destination->slab); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + const VkBufferCopy piece{0, destination->offset, unit}; + copyBuffer(commands, pattern->Handle(), destination->buffer, 1, &piece); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + recorder.MarkCovered(VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + const ShadowedRange range{begin, begin + unit, destination->slab}; + MarkShadowed(*import, std::span(&range, 1), TrackerGeneration()); + Require(AnyShadowedOverlaps(begin, unit), "the retiled unit is not shadowed"); + }; + retile(address + 2 * unit, 0x22); + retile(address + 3 * unit, 0x33); + StorageTexture::FlushAllPending("idle"); + Require(!AnyShadowedOverlaps(address + 2 * unit, static_cast(2 * unit)), "the idle flush did not publish the shadowed units"); + Require(!recorder.PendingWriteOverlaps(address + 2 * unit, static_cast(2 * unit)), "the idle flush returned before its publish's copies completed"); + words[2 * unit] = 0x44; + words[4 * unit - 1] = 0x45; + recorder.Sync(); + Require(words[2 * unit] == 0x44 && words[4 * unit - 1] == 0x45, "a CPU store made after the idle flush returned was overwritten by its publish"); + Require(words[2 * unit + 1] == 0x22 && words[3 * unit - 1] == 0x22 && words[3 * unit] == 0x33 && words[1 * unit] == 0x11 && words[4 * unit] == 0x11, "the idle publish's bytes are wrong"); + words[5 * unit] = 0x11; + CollectWritesUncached(address, bytes); + retile(address + 5 * unit, 0x55); + StorageTexture::FlushAllPending("label"); + const bool pending = recorder.PendingWriteOverlaps(address + 5 * unit, unit); + recorder.Sync(); + Require(words[5 * unit] == 0x55 && words[6 * unit - 1] == 0x55, "the label flush's publish did not land: the unit read as written after its retile, so the publish dropped it as stale"); + Require(pending, "a publish of a flush other than the idle one was waited"); + std::cout << "idle publish wait verified\n"; } void storageRefreshTests(const Device& device, Recorder& recorder, bool watched) { @@ -3299,7 +5237,21 @@ void importWatchTests(const Device& device) { const auto& context = device.GetContext(); #ifdef _WIN32 Require(PrepareImportWatch(context) == ImportWatch::Unwatch, "Windows host imports stayed watched by default"); - std::cout << "import watch decisions: Windows host imports use comparisons\n"; + std::cout << "import watch decisions: Windows host imports through views use comparisons\n"; + if (context.hostImportAlignment == 0 || !WriteWatched()) { + 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"; + Require(probe.writtenByCpu != 0, "(w) a CPU store into a host-imported MEM_WRITE_WATCH range was not collected"); + Require(probe.writtenAfterSettle == 0 || PrepareImportWatch(context) == ImportWatch::Unwatch, "(w) a settled import kept reporting writes and stayed watched"); #else if (context.hostImportAlignment == 0 || !WriteWatched()) { std::cout << "host imports or write watching unavailable: import watch decisions not tested\n"; @@ -3309,7 +5261,11 @@ void importWatchTests(const Device& device) { Require(probe.failure == nullptr, std::string("(u) the import probe failed at ") + (probe.failure != nullptr ? probe.failure : "") + " (" + std::to_string(static_cast(probe.result)) + ")"); std::cout << "import probe: " << probe.writtenAfterSubmit << " of " << probe.pages << " scratch pages written after a GPU read, " << probe.writtenAfterFill << " after a GPU write, " << probe.writtenAtImport << " after the import\n"; Require(probe.writtenByCpu != 0, "(u) a CPU store after the import probe's GPU read was not collected"); - if (context.dmaBufImport) { + const int udmabuf = context.dmaBufImport ? open("/dev/udmabuf", O_RDWR | O_CLOEXEC) : -1; + if (udmabuf >= 0) close(udmabuf); + if (context.dmaBufImport && udmabuf < 0) { + std::cout << "/dev/udmabuf is not accessible: the dma-buf import probe not tested\n"; + } else if (context.dmaBufImport) { const auto dmaBuf = ProbeDmaBufImportWriteProtection(context); Require(dmaBuf.failure == nullptr, std::string("(u) the dma-buf import probe failed at ") + (dmaBuf.failure != nullptr ? dmaBuf.failure : "") + " (" + std::to_string(static_cast(dmaBuf.result)) + ")"); Require(dmaBuf.writtenByCpu != 0, "(u) a CPU store into a dma-buf imported scratch range was not collected"); @@ -3532,17 +5488,21 @@ void writeBackPaddingTests(const Device& device, Recorder& recorder, bool watche std::uint32_t height; std::uint32_t mipCount; std::uint64_t offset; + bool direct = false; }; constexpr Case cases[] = { {"64 KiB R_X 200x150", TextureTileMode::kR64KBX, 200, 150, 1, 0}, {"64 KiB R_X 200x150, 3 mips", TextureTileMode::kR64KBX, 200, 150, 3, 0}, - {"64 KiB R_X 520x260, 6 mips", TextureTileMode::kR64KBX, 520, 260, 6, 0}, + {"64 KiB R_X 520x260, 6 mips", TextureTileMode::kR64KBX, 520, 260, 6, 0, true}, {"64 KiB R_X 200x150 off a 64 KiB boundary, 3 mips", TextureTileMode::kR64KBX, 200, 150, 3, 4096}, {"4 KiB S 100x70, 2 mips", TextureTileMode::kStandard4KB, 100, 70, 2, 0}, {"linear 200x150", TextureTileMode::kLinear, 200, 150, 1, 0}, + {"64 KiB R_X 512x512, 3 mips, no padding below the tail", TextureTileMode::kR64KBX, 512, 512, 3, 0, true}, + {"64 KiB R_X 1000x700", TextureTileMode::kR64KBX, 1000, 700, 1, 0, true}, + {"64 KiB R_X 1900x1100, across two 8 MiB slabs", TextureTileMode::kR64KBX, 1900, 1100, 1, 0, true}, }; const auto pattern = [](std::size_t i) { return static_cast(i * 131u + 7u); }; - constexpr std::array red{255, 0, 0, 255}; + constexpr std::array, 2> colours{{{255, 0, 0, 255}, {0, 255, 0, 255}}}; std::string failures; for (const auto& test : cases) { const std::string what = std::string(watched ? "(watched) " : "") + test.name; @@ -3609,6 +5569,148 @@ void writeBackPaddingTests(const Device& device, Recorder& recorder, bool watche auto context = base; context.detiler = &detiler; resource.baseAddress = address; + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, test.mipCount, 0, 1}; + for (std::size_t pass = 0; pass < (test.direct ? colours.size() : 1u); ++pass) { + const auto& colour = colours[pass]; + const auto commands = recorder.Commands(); + recorder.Keep(image); + const VkClearColorValue value{{colour[0] / 255.0f, colour[1] / 255.0f, colour[2] / 255.0f, colour[3] / 255.0f}}; + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + const auto slabRetilesBefore = StorageTexture::SlabRetiles(); + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + const auto slabRetiled = StorageTexture::SlabRetiles() - slabRetilesBefore; + if (test.direct && watched && UnitShadowEnabled() && AgcDriver::GuestMemory::Watched(address, surfaceBytes) && slabRetiled == 0) failures += what + ": no window was retiled straight into its unit-shadow slab\n"; + std::vector read(bytes); + if (watched) AgcDriver::GuestMemory::Read(blockAddress, read); + else std::memcpy(read.data(), memory, bytes); + std::size_t texels = 0, garbage = 0, outside = 0; + for (std::size_t i = 0; i < bytes; i += 4) { + const bool inside = i >= test.offset && i < test.offset + surfaceBytes; + bool isTexel = inside, isPattern = true; + for (std::size_t c = 0; c < 4; ++c) { + const auto byte = std::to_integer(read[i + c]); + isTexel = isTexel && byte == colour[c]; + isPattern = isPattern && byte == pattern(i + c); + } + if (isTexel) ++texels; + else if (!isPattern) ++(inside ? garbage : outside); + } + if (garbage != 0 || outside != 0 || texels != texelCount) { + failures += what + ", write-back " + std::to_string(pass + 1) + ": stored " + std::to_string(texels) + " texels of " + std::to_string(texelCount) + " and changed " + std::to_string(garbage) + " words no texel holds (" + std::to_string(outside) + " past the surface)\n"; + } + } + } + recorder.Sync(); + } + if (!failures.empty()) throw std::runtime_error("write-back padding:\n" + failures); + std::cout << "write-back padding" << (watched ? " (watched)" : "") << ": ok\n"; +} + +void splitWriteBackTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if (base.hostImportAlignment == 0 || 4096 % base.hostImportAlignment != 0) { + std::cout << "host imports unavailable: the split write-back not tested\n"; + return; + } + struct Case { + const char* name; + TextureTileMode tileMode; + std::uint32_t mipCount; + std::uint64_t offset; + std::uint64_t split; + bool gpu; + bool direct; + bool twoAllocations; + bool secondStore; + }; + const Case cases[] = { + {"64 KiB R_X 200x150, 3 mips, ranges split at 128 KiB", TextureTileMode::kR64KBX, 3, 0, 131072, true, true, false, false}, + {"64 KiB R_X 200x150, 3 mips, off a 64 KiB boundary, ranges split at 132 KiB", TextureTileMode::kR64KBX, 3, 4096, 135168, true, true, false, false}, + {"linear 200x150, ranges split at 64 KiB", TextureTileMode::kLinear, 1, 0, 65536, true, true, false, false}, + {"64 KiB R_X 200x150, 3 mips, second range of another access", TextureTileMode::kR64KBX, 3, 0, 131072, false, false, false, false}, + {"64 KiB R_X 200x150, 3 mips, ranges split inside a 64 KiB unit", TextureTileMode::kR64KBX, 3, 0, 135168, true, !UnitShadowEnabled(), false, false}, + {"64 KiB R_X 200x150, 3 mips, two adjacent allocations split at 128 KiB", TextureTileMode::kR64KBX, 3, 0, 131072, true, true, true, false}, + {"64 KiB R_X 200x150, 3 mips, ranges split at 128 KiB, a second store outside the write watch", TextureTileMode::kR64KBX, 3, 0, 131072, true, true, false, true}, + {"linear 200x150, ranges split at 64 KiB, a second store outside the write watch", TextureTileMode::kLinear, 1, 0, 65536, true, true, false, true}, + }; + SetHostImportWindow(64ull << 10u); + struct Window { + ~Window() { SetHostImportWindow(0); } + } window; + const auto pattern = [](std::size_t i) { return static_cast(i * 131u + 7u); }; + constexpr std::array red{255, 0, 0, 255}; + std::string failures; + for (const auto& test : cases) { + GuestTextureResource resource{}; + resource.width = 200; + resource.height = 150; + resource.mipCount = test.mipCount; + resource.tileMode = test.tileMode; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + const auto surfaceBytes = static_cast(geometry.guestBytes); + Require(test.offset < test.split && test.split < test.offset + surfaceBytes, "the split test's ranges do not divide its surface"); + std::size_t texelCount = 0; + for (const auto& mip : geometry.mips) texelCount += static_cast(mip.width) * mip.height; + const auto bytes = (static_cast(test.offset) + surfaceBytes + 65535) / 65536 * 65536 + 65536; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(65536, bytes); +#endif + Require(block != nullptr, "cannot allocate the split write-back block"); + auto* memory = static_cast(block); + for (std::size_t i = 0; i < bytes; ++i) memory[i] = pattern(i); + const auto blockAddress = reinterpret_cast(block); + const auto address = blockAddress + test.offset; + { + GuestAllocations::Mutation mutation; + if (test.twoAllocations) { + mutation.Add(block, static_cast(test.split), true, true, true); + mutation.Add(memory + test.split, static_cast(bytes - test.split), true, true, test.gpu); + } else { + mutation.Add(block, bytes, true, true, true); + mutation.Protect(memory + test.split, static_cast(bytes - test.split), true, true, test.gpu, [] {}); + } + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + std::size_t bytes; + void* second; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + if (second != nullptr) mutation.Remove(second); + } + HostImportFor(context, address, bytes); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + } unregister{base, block, blockAddress, bytes, test.twoAllocations ? static_cast(memory + test.split) : nullptr}; + Require(HostImportFor(base, address, surfaceBytes) == nullptr, std::string(test.name) + ": one import covers both ranges"); + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + resource.baseAddress = address; { auto image = StorageTexture::Create(context, detiler, resource, 0); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, test.mipCount, 0, 1}; @@ -3620,32 +5722,162 @@ void writeBackPaddingTests(const Device& device, Recorder& recorder, bool watche RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); image->MarkDirty(); image->WriteBack(); + const bool direct = !image->GuestSnapshotValid(); recorder.Submit(); device.WaitQueue(); recorder.Sync(); - std::vector read(bytes); - if (watched) AgcDriver::GuestMemory::Read(blockAddress, read); - else std::memcpy(read.data(), memory, bytes); std::size_t texels = 0, garbage = 0, outside = 0; for (std::size_t i = 0; i < bytes; i += 4) { const bool inside = i >= test.offset && i < test.offset + surfaceBytes; bool isTexel = inside, isPattern = true; for (std::size_t c = 0; c < 4; ++c) { - const auto byte = std::to_integer(read[i + c]); - isTexel = isTexel && byte == red[c]; - isPattern = isPattern && byte == pattern(i + c); + isTexel = isTexel && memory[i + c] == red[c]; + isPattern = isPattern && memory[i + c] == pattern(i + c); } if (isTexel) ++texels; else if (!isPattern) ++(inside ? garbage : outside); } - if (garbage != 0 || outside != 0 || texels != texelCount) { - failures += what + ": the write-back stored " + std::to_string(texels) + " texels of " + std::to_string(texelCount) + " and changed " + std::to_string(garbage) + " words no texel holds (" + std::to_string(outside) + " past the surface)\n"; + if (direct != test.direct) failures += std::string(test.name) + ": the write-back took the " + (direct ? "GPU-direct" : "CPU") + " path\n"; + if (direct && !(HostImportCovers(base, blockAddress, static_cast(test.split)) && HostImportCovers(base, blockAddress + test.split, static_cast(bytes - test.split)))) failures += std::string(test.name) + ": a range has no import after the GPU-direct write-back\n"; + if (garbage != 0 || outside != 0 || texels != texelCount) failures += std::string(test.name) + ": the write-back stored " + std::to_string(texels) + " texels of " + std::to_string(texelCount) + " and changed " + std::to_string(garbage) + " words no texel holds (" + std::to_string(outside) + " past the surface)\n"; + else if (test.secondStore) { + const auto cpuPage = static_cast(test.split) + 16384; + Require(cpuPage + 4096 <= test.offset + surfaceBytes, std::string(test.name) + ": the CPU's page lies past the surface"); + if (AgcDriver::GuestMemory::Watched(address, surfaceBytes)) failures += std::string(test.name) + ": the surface is write-watched\n"; + if (!image->GuestSnapshotValid()) failures += std::string(test.name) + ": the first write-back left no saved bytes\n"; + for (std::size_t i = cpuPage; i < cpuPage + 4096; ++i) memory[i] = static_cast(~pattern(i)); + const auto again = recorder.Commands(); + const VkClearColorValue greenValue{{0.0f, 1.0f, 0.0f, 1.0f}}; + RecordMemoryBarrier(context, again, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(again, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &greenValue, 1, &range); + RecordMemoryBarrier(context, again, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + constexpr std::array green{0, 255, 0, 255}; + std::size_t kept = 0, greenBelow = 0, greenAbove = 0; + for (std::size_t i = cpuPage; i < cpuPage + 4096; ++i) kept += memory[i] == static_cast(~pattern(i)) ? 1 : 0; + for (std::size_t i = test.offset; i < test.offset + surfaceBytes; i += 4) { + if (i >= cpuPage && i < cpuPage + 4096) continue; + if (std::equal(green.begin(), green.end(), memory + i)) ++(i < test.split ? greenBelow : greenAbove); + } + if (kept != 4096) failures += std::string(test.name) + ": the second write-back overwrote " + std::to_string(4096 - kept) + " of the 4096 bytes the CPU wrote after the first\n"; + if (greenBelow == 0 || greenAbove == 0) failures += std::string(test.name) + ": the second write-back stored " + std::to_string(greenBelow) + " texels below the split and " + std::to_string(greenAbove) + " above it\n"; } } recorder.Sync(); } - if (!failures.empty()) throw std::runtime_error("write-back padding:\n" + failures); - std::cout << "write-back padding" << (watched ? " (watched)" : "") << ": ok\n"; + if (!failures.empty()) throw std::runtime_error("split write-back:\n" + failures); + std::cout << "split write-back: ok\n"; +} + +void writeBackBlockTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if (base.hostImportAlignment == 0 || 65536 % base.hostImportAlignment != 0) { + std::cout << "host imports unavailable: write-back blocks not tested\n"; + return; + } + constexpr std::size_t piece = 65536; + constexpr std::size_t window = std::size_t{8} << 20u; + struct Case { + const char* name; + std::size_t offset; + std::size_t registeredEnd; + bool alternate; + bool direct; + std::size_t imports; + }; + const Case cases[] = { + {"a 4 MiB image over 64 adjacent 64 KiB pieces", 0, 4u << 20u, false, true, 1}, + {"a 4 MiB image at 64 KiB over 66 adjacent 64 KiB pieces", piece, (4u << 20u) + 2 * piece, false, true, 2}, + {"a 4 MiB image over 64 KiB pieces of which every other one is not GPU mapped", 0, 4u << 20u, true, false, 0}, + }; + SetHostImportWindow(window); + struct Window { + ~Window() { SetHostImportWindow(0); } + } restore; + const auto pattern = [](std::size_t i) { return static_cast(i * 131u + 7u); }; + constexpr std::array red{255, 0, 0, 255}; + std::string failures; + for (const auto& test : cases) { + GuestTextureResource resource{}; + resource.width = 1024; + resource.height = 1024; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kLinear; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto surfaceBytes = static_cast(DescribeSurface(resource).guestBytes); + Require(surfaceBytes == (4u << 20u) && test.offset + surfaceBytes <= test.registeredEnd, std::string(test.name) + ": the surface does not fit its pieces"); +#ifdef _WIN32 + void* reserved = VirtualAlloc(nullptr, 2 * window, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* reserved = std::aligned_alloc(window, 2 * window); +#endif + Require(reserved != nullptr, "cannot allocate the write-back block"); + auto* memory = static_cast(reinterpret_cast((reinterpret_cast(reserved) + window - 1) / window * window)); + for (std::size_t i = 0; i < window; ++i) memory[i] = pattern(i); + std::vector pieces; + for (std::size_t offset = 0; offset < test.registeredEnd; offset += piece) pieces.push_back(offset); + { + GuestAllocations::Mutation mutation; + for (const auto offset : pieces) mutation.Add(memory + offset, piece, true, true, !test.alternate || offset / piece % 2 == 0); + } + ClearHostImports(base.device); + const auto address = reinterpret_cast(memory) + test.offset; + const auto before = HostImportCounters(); + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + resource.baseAddress = address; + std::size_t texels = 0; + bool direct = false; + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto commands = recorder.Commands(); + recorder.Keep(image); + const VkClearColorValue value{{1.0f, 0.0f, 0.0f, 1.0f}}; + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + image->WriteBack(); + direct = !image->GuestSnapshotValid(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + for (std::size_t i = test.offset; i < test.offset + surfaceBytes; i += 4) texels += std::equal(red.begin(), red.end(), memory + i) ? 1 : 0; + } + const auto after = HostImportCounters(); + std::size_t made = 0; + std::size_t segments = 0; + for (std::size_t index = 0; index < ImportRequesterCount; ++index) made += after.requesters[index].made - before.requesters[index].made; + segments = after.requesters[static_cast(ImportRequester::Segment)].made - before.requesters[static_cast(ImportRequester::Segment)].made; + std::printf("%s: %zu imports made (%zu by write-back segments), %s write-back, %zu of %zu texels stored\n", test.name, made, segments, direct ? "GPU-direct" : "CPU", texels, surfaceBytes / 4); + if (made != test.imports) failures += std::string(test.name) + ": the write-back made " + std::to_string(made) + " imports, expected " + std::to_string(test.imports) + "\n"; + if (direct != test.direct) failures += std::string(test.name) + ": the write-back took the " + (direct ? "GPU-direct" : "CPU") + " path\n"; + if (texels != surfaceBytes / 4) failures += std::string(test.name) + ": the write-back stored " + std::to_string(texels) + " of " + std::to_string(surfaceBytes / 4) + " texels\n"; + { + GuestAllocations::Mutation mutation; + for (const auto offset : pieces) mutation.Remove(memory + offset); + } + ClearHostImports(base.device); + recorder.Sync(); +#ifdef _WIN32 + VirtualFree(reserved, 0, MEM_RELEASE); +#else + std::free(reserved); +#endif + } + if (!failures.empty()) throw std::runtime_error("write-back blocks:\n" + failures); + std::cout << "write-back blocks: ok\n"; } void singlePassTests(const Device& device, Recorder& recorder, bool watched) { @@ -4223,6 +6455,135 @@ void unchangedCpuStampTests(const Device& device, Recorder& recorder) { std::cout << "unchanged CPU stamps: ok\n"; } +void keptUnitSettleTests(const Device& device, Recorder& recorder) { + Require(AgcDriver::GuestMemory::WriteWatched(), "(k) kept-unit settle tests require host write watching"); + const auto& base = device.GetContext(); + constexpr std::uint32_t width = 1024; + constexpr std::uint32_t height = 512; + constexpr std::size_t surfaceBytes = std::size_t{width} * height * 4; + constexpr std::size_t unitBytes = 65536; + constexpr std::array red{255, 0, 0, 255}; + constexpr std::array blue{0, 0, 255, 255}; + for (const bool direct : {false, true}) { + const std::string what = direct ? "GPU-mapped direct memory" : "watched memory"; + if (direct && base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + struct Backing { + Recorder& recorder; + bool direct; + void* block = nullptr; + std::int64_t physical = -1; + ~Backing() { + try { + recorder.Sync(); + } catch (const std::exception& error) { + std::cerr << "(k) the recorded work did not finish, the test memory stays mapped: " << error.what() << '\n'; + return; + } + if (direct) { + if (block != nullptr) sceKernelMunmap(block, surfaceBytes); + if (physical >= 0) sceKernelReleaseDirectMemory(physical, surfaceBytes); + return; + } + if (block == nullptr) return; + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + ReleaseWatched(block, surfaceBytes); + } + } backing{recorder, direct}; + if (direct) { + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, surfaceBytes, unitBytes, 0, &backing.physical) == 0, "(k) allocate the direct memory"); + Require(sceKernelMapDirectMemory(&backing.block, surfaceBytes, 0x33, 0, backing.physical, unitBytes) == 0, "(k) map the direct memory for the GPU"); + } else { + backing.block = AllocateWatched(surfaceBytes, unitBytes); + Require(backing.block != nullptr, "(k) no watched block although write watching is on"); + GuestAllocations::Mutation mutation; + mutation.Add(backing.block, surfaceBytes, true, true, true); + } + auto* memory = static_cast(backing.block); + std::memset(memory, 0x5a, surfaceBytes); + const auto address = reinterpret_cast(backing.block); + AgcDriver::GuestMemory::CollectWritesUncached(address, surfaceBytes); + TextureDetiler detiler(base); + auto context = base; + if (!direct) context.hostImportAlignment = 0; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = width; + resource.height = height; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + Require(DescribeSurface(resource).guestBytes == surfaceBytes, "(k) unexpected surface size"); + if (direct && !AgcDriver::GuestMemory::Watched(address, surfaceBytes)) { + std::cout << "GPU-mapped direct memory is not write-watched here: units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + Require(AgcDriver::GuestMemory::Watched(address, surfaceBytes), "(k) " + what + " is not watched"); + if (direct && HostImportFor(context, address, surfaceBytes) == nullptr) { + std::cout << "host import of the direct memory refused: units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + if (direct && !AgcDriver::GuestMemory::Watched(address, surfaceBytes)) { + std::cout << "the host import of the direct memory took it out of the write watch (its imports are compared): units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + Require(direct || HostImportFor(context, address, surfaceBytes) == nullptr, "(k) watched memory was imported although host imports are off for it"); + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto clear = [&](VkClearColorValue value) { + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + }; + const auto store = [&] { + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + }; + const auto unitHolds = [&](std::size_t unit, const std::array& texel) { + std::vector read(unitBytes); + AgcDriver::GuestMemory::Read(address + unit * unitBytes, read); + for (std::size_t i = 0; i < unitBytes; ++i) { + if (std::to_integer(read[i]) != texel[i % 4]) return false; + } + return true; + }; + clear({{1.0f, 0.0f, 0.0f, 1.0f}}); + memory[16] = 0x77; + AgcDriver::GuestMemory::CollectWritesUncached(address, surfaceBytes); + store(); + std::vector head(unitBytes); + AgcDriver::GuestMemory::Read(address, head); + Require(std::to_integer(head[16]) == 0x77, "(k) " + what + ": the CPU store into unit 0 was overwritten by results it was newer than"); + for (std::size_t unit = 1; unit < surfaceBytes / unitBytes; ++unit) Require(unitHolds(unit, red), "(k) " + what + ": the first write-back did not store unit " + std::to_string(unit)); + clear({{0.0f, 0.0f, 1.0f, 1.0f}}); + store(); + for (std::size_t unit = 1; unit < surfaceBytes / unitBytes; ++unit) { + Require(unitHolds(unit, blue), "(k) " + what + ": unit " + std::to_string(unit) + " kept the results of the earlier write-back; that write-back's own store was taken for a CPU write because a neighbouring unit was kept for the CPU"); + } + AgcDriver::GuestMemory::Read(address, head); + Require(std::to_integer(head[16]) == 0x77, "(k) " + what + ": the second write-back stored over unit 0, which the CPU wrote after that unit's generation"); + } + std::cout << "units stored next to a unit kept for the CPU in " << what << ": ok\n"; + } +} + void settledBaselineTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); #ifdef _WIN32 @@ -4455,7 +6816,32 @@ void queuedWriteGroupTests() { } return memory; }; + const auto recorded = [](const std::vector& writes) { + const auto order = Recorder::QueuedWriteOrder(writes); + std::vector ordered; + for (const auto index : order) ordered.push_back(writes[index]); + const auto groups = Recorder::QueuedWriteGroups(ordered); + std::vector sequence; + std::size_t first = 0; + for (const auto end : groups) { + for (const bool computed : {false, true}) { + for (auto i = first; i < end; ++i) { + if (ordered[i].computed == computed) sequence.push_back(order[i]); + } + } + first = end; + } + return std::pair{sequence, groups.size()}; + }; Require(Recorder::QueuedWriteGroups({}).empty(), "no queued writes made a group"); + const std::vector flagged{{0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x200, 0x208, false}, {0x400, 0x500, false}, {0x400, 0x4f8, true}, {0x408, 0x500, true}, {0x500, 0x508, false}, {0x800, 0x804, false}}; + Require(recorded(flagged) == std::pair{std::vector{0, 4, 1, 2, 5, 6, 3, 7, 8}, std::size_t{2}}, "the dumps of a run with a flag after each query slot were not folded once, with the clears before them and the flags after them"); + const std::vector open{{0x50, 0x58, false}, {0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x200, 0x208, false}, {0x400, 0x500, false}, {0x400, 0x4f8, true}}; + Require(recorded(open) == std::pair{std::vector{0, 1, 5, 2, 3, 6, 4}, std::size_t{2}}, "the clear of a slot whose end dump the run does not hold was not moved ahead of the dumps"); + const std::vector overDump{{0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x1f0, 0x410, false}, {0x400, 0x4f8, true}}; + Require(Recorder::QueuedWriteOrder(overDump) == std::vector{0, 1, 2, 3, 4}, "a store over an earlier dump that the later dumps cover only in part was moved ahead of that dump"); + const std::vector overFlag{{0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x500, 0x508, false}, {0x400, 0x508, false}, {0x400, 0x4f8, true}, {0x408, 0x500, true}}; + Require(Recorder::QueuedWriteOrder(overFlag) == std::vector{0, 1, 2, 3, 4, 5, 6}, "a clear over an earlier store after a dump was moved ahead of that store"); const std::vector query{{0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x200, 0x300, false}, {0x200, 0x2f8, true}, {0x208, 0x300, true}}; Require(Recorder::QueuedWriteGroups(query) == Groups{3, 6}, "the clear of the next query slot did not wait for the dumps before it"); const std::vector reused{{0x100, 0x1f8, true}, {0x100, 0x200, false}, {0x100, 0x1f8, true}}; @@ -4485,6 +6871,26 @@ void queuedWriteGroupTests() { const auto groups = Recorder::QueuedWriteGroups(writes); Require(!groups.empty() && groups.back() == writes.size() && std::is_sorted(groups.begin(), groups.end()), "queued write groups do not end at the last write in order"); Require(paint(writes, groups, bytes) == programOrder(writes, bytes), "recording each group's stores before its computed writes changed the final bytes of the program order"); + const auto sequence = recorded(writes).first; + std::vector position(writes.size(), writes.size()); + for (std::size_t i = 0; i < sequence.size(); ++i) position[sequence[i]] = i; + Require(sequence.size() == writes.size() && std::find(position.begin(), position.end(), writes.size()) == position.end(), "the recorded order is not a permutation of the queued writes"); + std::vector memory(bytes, 0); + for (const auto index : sequence) { + for (auto at = writes[index].begin; at < writes[index].end; ++at) memory[at] = static_cast(index + 1); + } + Require(memory == programOrder(writes, bytes), "the recorded order changed the final bytes of the program order"); + for (std::size_t j = 0; j < writes.size(); ++j) { + if (writes[j].computed) continue; + std::vector covered(bytes); + for (auto k = j + 1; k < writes.size(); ++k) { + if (writes[k].computed) std::fill(covered.begin() + writes[k].begin, covered.begin() + writes[k].end, true); + } + if (std::any_of(covered.begin() + writes[j].begin, covered.begin() + writes[j].end, [](bool value) { return value; })) continue; + for (std::size_t i = 0; i < j; ++i) { + Require(position[i] < position[j], writes[i].computed ? "a store no later dump overlaps was recorded before a dump ahead of it" : "a store no later dump overlaps was recorded before a store ahead of it"); + } + } } } @@ -4527,35 +6933,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) { @@ -4602,7 +7010,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(); @@ -4619,11 +7032,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); @@ -4652,6 +7089,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; @@ -4714,11 +7152,11 @@ void inPassSampleDumpTests(Recorder& recorder, const Context& context, const Cov if (context.occlusionQueryPrecise) Require(folded == base + passes * CoverPass::Samples, "segments folded at Submit without a dump counted the wrong samples"); } -void pendingKeyStoreTests(const Device& device, Recorder& recorder) { +bool pendingKeyStoreTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); if (context.hostImportAlignment == 0) { std::cout << "host imports unavailable: pending key stores not tested\n"; - return; + return false; } constexpr std::size_t surfaceBytes = 65536; constexpr std::size_t keyCount = surfaceBytes / 256; @@ -4749,10 +7187,10 @@ void pendingKeyStoreTests(const Device& device, Recorder& recorder) { } unregister{context, block, address}; if (HostImportFor(context, address, bytes) == nullptr) { std::cout << "host import of the pending key store block refused: pending key stores not tested\n"; - return; + return false; } 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"); @@ -4764,8 +7202,13 @@ void pendingKeyStoreTests(const Device& device, Recorder& recorder) { recorder.NotePendingWrite(address, keyCount); NoteKeysFillOnGpu(address, keyCount, DccKeys::Clear0001); Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Clear0001 && recorder.PendingWriteOverlaps(address, keyCount), "the keys of a pending fill did not read as its keys without a wait"); + Require(KnownPendingDccKeys(address, surfaceBytes) == DccKeys::Clear0001 && recorder.PendingWriteOverlaps(address, keyCount), "the keys of a pending fill were not known without a wait"); + Require(KnownPendingDccKeys(address + 16, surfaceBytes / 2) == DccKeys::Clear0001, "a key range inside a pending fill was not known"); + Require(!KnownPendingDccKeys(address, 2 * surfaceBytes).has_value() && recorder.PendingWriteOverlaps(address, keyCount), "a range wider than the pending fill was known, or waited for"); recorder.NotePendingWrite(address, 2 * keyCount); + Require(!KnownPendingDccKeys(address, surfaceBytes).has_value() && recorder.PendingWriteOverlaps(address, keyCount), "the keys under a later writer over a pending fill were known"); Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Uncompressed && !recorder.PendingWriteOverlaps(address, keyCount), "a later wider writer over a pending fill was not waited for"); + Require(!KnownPendingDccKeys(address, surfaceBytes).has_value(), "keys nothing writes were known as pending keys"); recorder.NotePendingFill(address, keyCount / 2, 0x40); recorder.NotePendingFill(address + keyCount / 2, keyCount / 2, 0x40); Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Clear0001 && recorder.PendingWriteOverlaps(address, keyCount), "the keys of pending 0001 fills over both halves did not read as 0001 without a wait"); @@ -4803,6 +7246,7 @@ void pendingKeyStoreTests(const Device& device, Recorder& recorder) { Require(CurrentDccKeys(address, displayBytes, displayKeys) == DccKeys::Uncompressed && !recorder.PendingWriteOverlaps(address + 40000, 1), "a later writer of key 40000 over a pending 1920x1080 store was not waited for"); Require(std::all_of(keys, keys + consoleKeys, [](std::uint8_t key) { return key == 0xff; }), "the uncompressed key store over a 1920x1080 surface did not land on all 49152 keys"); recorder.Sync(); + return true; } bool shadowedKeyStoreTests(const Device& device, Recorder& recorder) { @@ -4866,7 +7310,7 @@ bool shadowedKeyStoreTests(const Device& device, Recorder& recorder) { const auto commands = recorder.Commands(); recorder.Keep(pattern); recorder.Keep(destination->slab); - RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); const VkBufferCopy piece{0, destination->offset, unit}; copyBuffer(commands, pattern->Handle(), destination->buffer, 1, &piece); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); @@ -4882,6 +7326,12 @@ bool shadowedKeyStoreTests(const Device& device, Recorder& recorder) { Require(keys[0] == 0x40 && keys[unit - 1] == 0x40 && keys[unit] == 0xff && !AnyShadowedOverlaps(address, unit), "the fresh unit's publish did not land"); recorder.Sync(); shadow(0x22); + MarkWritten(address, unit); + recorder.NotePendingFill(address + unit, unit, 0x11); + Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Clear0001 && !AnyShadowedOverlaps(address, unit), "the keys under a stale unit shadow were not read from guest memory"); + Require(recorder.PendingWriteOverlaps(address + unit, unit), "a key read under a stale unit shadow waited for a write outside the keys"); + recorder.Sync(); + shadow(0x22); recorder.NotePendingFill(address, unit, 0xc0); MarkWritten(address, unit); Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Clear1111 && recorder.PendingWriteOverlaps(address, keyCount), "the keys of a pending fill over a stale unit shadow were not read as the fill's keys without a wait"); @@ -5164,6 +7614,25 @@ void metadataPassTests(const Device& device, Recorder& recorder) { const auto keyed = StorageTexture::FindPending(address, surfaceBytes); Require(keyed != nullptr && keyed != unkeyed && keyed->Descriptor().dccAddress == color.dccAddress, "a pass over a resident image under other keys did not remake it under the target's keys"); Require(keysUncompressed() && memoryHolds({0xff, 0xff, 0xff, 0xff}), "a pass over a remade resident image did not store the 1111 value"); + { + const auto* import = HostImportFor(base, address, bytes); + Require(import != nullptr, "the metadata pass block lost its import"); + const auto commands = recorder.Commands(); + context.Function("vkCmdFillBuffer")(commands, import->buffer, color.dccAddress - import->base, keyCount, 0x20202020u); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_HOST_READ_BIT); + recorder.NotePendingWrite(color.dccAddress, keyCount); + NoteKeysFillOnGpu(color.dccAddress, keyCount, DccKeys::ClearRegister); + } + Require(KnownPendingDccKeys(color.dccAddress, surfaceBytes) == DccKeys::ClearRegister, "the keys of a pending register clear store were not known"); + const std::array clearPattern{0x80402010u, 0x80402010u, 0x80402010u, 0x80402010u}; + const char* refusal = nullptr; + Require(!keyed->FillClear(clearPattern, StorageTexture::WholeImage, refusal) && refusal != nullptr && std::strcmp(refusal, "keys pending") == 0, "a clear over pending keys was not refused without the proof"); + refusal = nullptr; + Require(keyed->FillClear(clearPattern, StorageTexture::WholeImage, refusal, true), "a clear over known pending keys was refused"); + Require(recorder.PendingWriteOverlaps(color.dccAddress, keyCount) && keys[0] == 0xff, "the clear over known pending keys waited for the key store"); + MarkDccUncompressed(context, color.dccAddress, surfaceBytes); + Require(memoryHolds({0x10, 0x20, 0x40, 0x80}), "the clear recorded behind a pending key store did not store CB_COLOR_CLEAR_WORD"); + Require(std::all_of(keys, keys + keyCount, [](std::uint8_t key) { return key == 0xff; }), "the keys after a clear behind a pending key store are not uncompressed"); } // The data word positions of a dispatch-cache variant (Driver.cpp's data-only hits): leaves @@ -5403,9 +7872,11 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { target.format = VK_FORMAT_R8G8B8A8_UNORM; target.bytes = targetBytes; target.samples = 4; + const bool ordered = MayReorderSamples(context, target.samples) && SampleOrderStorageFormat(context, target.format, target.elementBytes, target.samples) != VK_FORMAT_UNDEFINED; { std::lock_guard gpu(GpuMutex()); Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the multisampled target has no view"); + Require(MultisampleTargetStorable(context, target) == ordered, ordered ? "a multisampled target whose samples may be reordered was created without storage usage" : "a multisampled target nothing stored to was created with storage usage"); recorder.Submit(); device.WaitQueue(); recorder.Sync(); @@ -5415,6 +7886,31 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { refused([&] { AgcDriver::GuestMemory::ReadCommitted(address + 256u, texels); }, "a CPU read", "a committed CPU read"); refused([&] { AgcDriver::GuestMemory::ReadCommittedUnsynced(address + 256u, texels); }, "a CPU read", "a committed CPU read without the flush hook"); refused([&] { RequireNoMultisampleTarget(address, 64u, "a DMA copy"); }, "a DMA copy", "a DMA copy"); + const auto captureSrt = [](std::uint64_t srt, AgcDriver::ShaderMemory::PendingWriteQuery pendingWrite) { + static constexpr std::array code{0xf4080100u, 0xfa000000u, 0xbf8cc07fu, 0xe0700000u, 0x80010000u, 0xbf810000u}; + const std::array userData{static_cast(srt), static_cast(srt >> 32u)}; + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Compute, 0x24000u, code, 0, {}}; + request.context.waveSize = 32u; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderRecompiler::ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + request.layout.pushConstantSizeBytes = 128; + AgcDriver::ShaderMemory memory({}, pendingWrite); + static_cast(memory.Capture(request)); + }; + refused([&] { captureSrt(address + 256u, nullptr); }, "a CPU read", "a shader capture"); + captureSrt(address + 4u * 65536u + 256u, nullptr); + refused([&] { captureSrt(address + 256u, nullptr); }, "a CPU read", "a shader capture on a pooled page"); + const auto raw = [](std::uint64_t, std::size_t, std::span) { return AgcDriver::ShaderMemory::PendingWrite::Raw; }; + const auto synced = [](std::uint64_t, std::size_t, std::span) { return AgcDriver::ShaderMemory::PendingWrite::Sync; }; + refused([&] { captureSrt(address + 256u, raw); }, "a CPU read", "a shader capture's raw word read"); + refused([&] { captureSrt(address + 256u, synced); }, "a CPU read", "a shader capture's synced word read"); + captureSrt(address + 4u * 65536u + 256u, raw); ShaderRecompiler::RecompileResult program; ShaderRecompiler::DescriptorBinding binding; binding.kind = ShaderRecompiler::DescriptorKind::SampledImage; @@ -5440,6 +7936,87 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { std::lock_guard gpu(GpuMutex()); ShaderResources resources(context, compute); }, "a texture read", "a texture read"); + const auto multisampledWords = [&](std::uint32_t width, std::uint32_t sampleShift, std::uint64_t base) { + return std::vector{ + static_cast(base >> 8u), + static_cast((base >> 40u) & 0xffu) | (56u << 20u) | (((width - 1u) & 3u) << 30u), + ((width - 1u) >> 2u) | ((side - 1u) << 14u), + 0xfacu | (sampleShift << 16u) | (14u << 28u), + 0u, + sampleShift << 4u, + 0u, + 0u, + }; + }; + const auto sampledProgram = [&](ShaderRecompiler::DescriptorKind kind, std::vector words, ShaderRecompiler::DescriptorImageShape shape = ShaderRecompiler::DescriptorImageShape::Image2DMultisample) { + ShaderRecompiler::RecompileResult result; + auto element = binding; + element.kind = kind; + if (kind == ShaderRecompiler::DescriptorKind::StorageImage) element.binding = ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding; + element.imageShape = shape; + element.guestDescriptor = std::move(words); + result.bindings.push_back(std::move(element)); + return result; + }; + const auto refusedSample = [&](ShaderRecompiler::DescriptorKind kind, std::vector words, const std::string& expected, const std::string& what) { + const auto refusedProgram = sampledProgram(kind, std::move(words)); + const CompiledShader refusedCompute{ShaderRecompiler::ShaderStage::Compute, &refusedProgram, 0}; + std::string message; + try { + std::lock_guard gpu(GpuMutex()); + ShaderResources resources(context, refusedCompute); + } catch (const std::exception& error) { + message = error.what(); + } + Require(message.find(expected) != std::string::npos, what + " did not throw: " + message); + }; + { + const auto sampled = sampledProgram(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address)); + const auto sampledArray = sampledProgram(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address), ShaderRecompiler::DescriptorImageShape::Image2DMultisampleArray); + auto bothShapes = sampled; + auto arrayBinding = sampledArray.bindings.front(); + arrayBinding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding + 1u; + bothShapes.bindings.push_back(std::move(arrayBinding)); + const CompiledShader sampledCompute{ShaderRecompiler::ShaderStage::Compute, &sampled, 0}; + const CompiledShader sampledArrayCompute{ShaderRecompiler::ShaderStage::Compute, &sampledArray, 0}; + const CompiledShader bothShapesCompute{ShaderRecompiler::ShaderStage::Compute, &bothShapes, 0}; + std::lock_guard gpu(GpuMutex()); + ShaderResources first(context, sampledCompute); + ShaderResources second(context, sampledCompute); + ShaderResources arrayFirst(context, sampledArrayCompute); + ShaderResources arraySecond(context, sampledArrayCompute); + ShaderResources combined(context, bothShapesCompute); + Require(first.SampledTextures().size() == 1 && first.SampledTextures().front() != nullptr, "the multisampled target was not bound as a sampled texture"); + Require(first.SampledTextures().front()->Layout() == VK_IMAGE_LAYOUT_GENERAL && first.SampledTextures().front()->ViewFormat() == target.format, "the sampled multisampled target is not a view of the target's image in its layout"); + Require(first.SampledTextures().front()->SampledViewRange(false).type == VK_IMAGE_VIEW_TYPE_2D, "Image2DMultisample did not bind a 2D image view"); + Require(second.SampledTextures().front() == first.SampledTextures().front(), "a second lookup of the multisampled target made another texture"); + Require(arrayFirst.SampledTextures().size() == 1 && arrayFirst.SampledTextures().front() != nullptr, "the multisampled array target was not bound as a sampled texture"); + Require(arrayFirst.SampledTextures().front()->SampledViewRange(false).type == VK_IMAGE_VIEW_TYPE_2D_ARRAY, "Image2DMultisampleArray did not bind a 2D array image view"); + Require(arrayFirst.SampledTextures().front()->View() != first.SampledTextures().front()->View(), "Image2DMultisampleArray reused the non-arrayed multisampled image view"); + Require(arraySecond.SampledTextures().front() == arrayFirst.SampledTextures().front(), "a second lookup of the multisampled array target made another texture"); + Require(combined.SampledTextures().size() == 2 && combined.SampledTextures()[0] == first.SampledTextures().front() && combined.SampledTextures()[1] == arrayFirst.SampledTextures().front(), "consecutive multisampled bindings of different shapes did not preserve their distinct views"); + Require(first.MultisampleSources() == std::vector{address} && !first.Reusable(), "resources sampling a multisampled target are kept for reuse without the barrier their lookup records"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + 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"); + 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) == ordered, "sampling the multisampled target changed its storage usage"); + ShaderResources resources(context, storedCompute); + Require(MultisampleTargetStorable(context, target) && (MultisampleTargetView(context, target) != rendered) != ordered, "a storage binding did not move the multisampled target to an image with storage usage, or moved one that had it"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } 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(); @@ -5489,12 +8066,19 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { target.surfaceExtent = target.extent; target.samples = 4; VkImageView previous = VK_NULL_HANDLE; + std::shared_ptr previousTexture; for (const bool removeAllocation : {false, true}) { VkImageView view = VK_NULL_HANDLE; + std::shared_ptr texture; { + const auto sampled = sampledProgram(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address)); + const CompiledShader sampledCompute{ShaderRecompiler::ShaderStage::Compute, &sampled, 0}; std::lock_guard gpu(GpuMutex()); view = MultisampleTargetView(context, target); Require(view != VK_NULL_HANDLE && view != previous, "a remapped allocation reused its retired multisampled image"); + ShaderResources resources(context, sampledCompute); + texture = resources.SampledTextures().front(); + Require(texture != nullptr && texture != previousTexture, "a remapped allocation sampled its retired multisampled image"); recorder.Submit(); device.WaitQueue(); recorder.Sync(); @@ -5521,12 +8105,211 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { } RequireNoMultisampleTarget(address, bytes, "a texture read"); AgcDriver::GuestMemory::Read(address + 256u, texels); + refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address), "which no draw rendered to", "sampling a multisampled target whose allocation was unmapped"); previous = view; + previousTexture = texture; } clearTargets(); std::cout << "Multisampled target guest access tests passed\n"; } +void fmaskDecompressMsaaTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0 || base.hostImportAlignment == 0) { + std::cout << "4x color targets or host imports unavailable: FMASK decompress not tested\n"; + return; + } + constexpr std::uint32_t side = 128; + constexpr std::size_t targetBytes = 65536u; + constexpr std::size_t cmaskBytes = 4096u; + constexpr std::size_t destBytes = targetBytes; + constexpr std::size_t bytes = 5u * 65536u + cmaskBytes + 2u * destBytes; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + 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); + const auto address = reinterpret_cast(block); + const auto cmaskAddress = address + 5u * 65536u; + const auto destAddress = cmaskAddress + cmaskBytes; + const auto destAddress2 = destAddress + destBytes; + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + ClearMultisampleTargets(context.device); + ClearCachedTextures(context.device); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + } unregister{base, block}; + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + ColorTarget source{}; + source.address = address; + source.surfaceAddress = address; + source.extent = {side, side}; + source.surfaceExtent = source.extent; + source.format = VK_FORMAT_R8G8B8A8_UNORM; + source.elementBytes = 4; + source.bytes = targetBytes; + source.samples = 4; + source.cmaskAddress = cmaskAddress; + source.cmaskBytes = cmaskBytes; + source.cmaskFastClear = true; + source.clearWords = {0x11223344u, 0}; + ColorTarget destination{}; + destination.address = destAddress; + destination.surfaceAddress = destAddress; + destination.extent = {side, side}; + destination.surfaceExtent = destination.extent; + destination.format = VK_FORMAT_R8G8B8A8_UNORM; + destination.elementBytes = 4; + destination.bytes = destBytes; + destination.samples = 1; + destination.tileMode = ColorTileMode::RenderTarget; + { + std::lock_guard gpu(GpuMutex()); + RunColorMetadataPass(context, {ColorMetadataPass::Mode::FmaskDecompress, {source}}); + auto recolored = source; + recolored.clearWords = {0x55667788u, 0}; + RunColorMetadataPass(context, {ColorMetadataPass::Mode::Resolve, {destination}, recolored}); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + std::vector resolved(destBytes / 4); + AgcDriver::GuestMemory::Read(destAddress, std::as_writable_bytes(std::span(resolved))); + Require(std::ranges::all_of(resolved, [](auto word) { return word == 0x11223344u; }), "FMASK decompress did not materialize the seeded clear to the host multisampled image"); + source.clearWords = {0x55667788u, 0}; + ColorTarget destination2 = destination; + destination2.address = destAddress2; + destination2.surfaceAddress = destAddress2; + { + std::lock_guard gpu(GpuMutex()); + RunColorMetadataPass(context, {ColorMetadataPass::Mode::FmaskDecompress, {source}}); + RunColorMetadataPass(context, {ColorMetadataPass::Mode::Resolve, {destination2}, source}); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + std::vector resolved2(destBytes / 4); + AgcDriver::GuestMemory::Read(destAddress2, std::as_writable_bytes(std::span(resolved2))); + Require(std::ranges::all_of(resolved2, [](auto word) { return word == 0x11223344u; }), "subsequent FMASK decompress re-cleared the multisampled target"); + std::cout << "FMASK decompress MSAA clear and resolve tests passed\n"; +} + +void tiledResolveTests(const Device& device, Recorder& recorder) { + namespace GuestMemory = AgcDriver::GuestMemory; + auto context = device.GetContext(); + if ((context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0 || !device.Graphics()) { + std::cout << "4x color targets or graphics unavailable: CB resolves into tiled targets not tested\n"; + return; + } + context.hostImportAlignment = 0; + TextureDetiler detiler(context); + context.detiler = &detiler; + constexpr std::uint32_t side = 128; + constexpr std::size_t targetBytes = side * side * 4u; + constexpr std::size_t sourceBytes = targetBytes * 4u; + constexpr std::size_t bytes = targetBytes + sourceBytes + 65536u; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(65536, bytes); +#endif + Require(block != nullptr, "cannot allocate the tiled resolve block"); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Release { + const Context& context; + void* block; + ~Release() { + ClearMultisampleTargets(context.device); + ClearCachedTextures(context.device); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + } release{context, block}; + ColorTarget source{}; + source.address = address + targetBytes; + source.surfaceAddress = source.address; + source.extent = {side, side}; + source.surfaceExtent = source.extent; + source.format = VK_FORMAT_R8G8B8A8_UNORM; + source.bytes = targetBytes; + source.componentMapping = 0xe4u; + source.tileMode = ColorTileMode::RenderTarget; + source.samples = 4; + source.cmaskAddress = address + targetBytes + sourceBytes; + source.cmaskBytes = 4096; + source.cmaskFastClear = true; + const std::array, 3> modes{{{ColorTileMode::Standard4KB, 0x80402010u}, {ColorTileMode::Standard64KB, 0x10204080u}, {ColorTileMode::RenderTarget, 0x40801020u}}}; + for (const auto& [mode, clear] : modes) { + const auto name = std::to_string(static_cast(mode)); + std::memset(block, 0x55, targetBytes); + std::memset(static_cast(block) + targetBytes + sourceBytes, 0, 4096); + ColorTarget destination{}; + destination.address = address; + destination.surfaceAddress = address; + destination.extent = {side, side}; + destination.surfaceExtent = destination.extent; + destination.format = VK_FORMAT_R8G8B8A8_UNORM; + destination.bytes = targetBytes; + destination.componentMapping = 0xe4u; + destination.tileMode = mode; + source.clearWords = {clear, 0}; + RunColorMetadataPass(context, {ColorMetadataPass::Mode::Resolve, {destination}, source}); + std::vector resolved(targetBytes / 4); + GuestMemory::Read(address, std::as_writable_bytes(std::span(resolved))); + const auto texels = std::ranges::count(resolved, clear); + Require(texels == static_cast(resolved.size()), "a CB resolve into a tile mode " + name + " target left " + std::to_string(resolved.size() - static_cast(texels)) + " texels without the resolved color"); + recorder.Sync(); + ClearMultisampleTargets(context.device); + ClearCachedTextures(context.device); + } + { + ColorTarget destination{}; + destination.address = address; + destination.extent = {side, side}; + destination.format = VK_FORMAT_R8G8B8A8_UNORM; + destination.bytes = targetBytes; + destination.tileMode = ColorTileMode::Linear; + std::string message; + try { + RunColorMetadataPass(context, {ColorMetadataPass::Mode::Resolve, {destination}, source}); + } catch (const std::exception& error) { + message = error.what(); + } + Require(message.find("without a resident image is not implemented") != std::string::npos, "a CB resolve into a linear target did not throw: " + message); + } + recorder.Sync(); +} + void depthSurfaceProofTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (!device.NullDescriptors()) { @@ -5810,6 +8593,134 @@ void singleCubeTests(const Device& device, Recorder& recorder) { } } +void cubeExtraSliceTests(const Device& device, Recorder& recorder) { + auto context = device.GetContext(); + TextureDetiler detiler(context); + context.detiler = &detiler; + SampleProgram program(context, recorder, SAMPLE_ARRAY_SPV); + const VkComponentMapping identity{}; + std::array words{0x1000u, (56u << 20u) | (3u << 30u), 15u | (63u << 14u), 0xb0000facu, 6u, 0, 0, 0}; + auto resource = DecodeTextureResource(words); + const auto geometry = DescribeSurface(resource); + Require(geometry.imageLayers == 7u, "a cube with a slice past its last whole cube does not allocate it"); + std::vector memory(static_cast(geometry.guestBytes) + 256); + auto* surface = reinterpret_cast((reinterpret_cast(memory.data()) + 255) & ~std::uintptr_t{255}); + for (std::uint32_t layer = 0; layer < geometry.imageLayers; ++layer) { + std::memset(surface + geometry.GuestLayerOffset(layer), 16 * (layer + 1), static_cast(geometry.layerBytes)); + } + resource.baseAddress = reinterpret_cast(surface); + const std::span snapshot(surface, static_cast(geometry.guestBytes)); + Texture texture(context, detiler, resource, identity, snapshot); + auto storage = StorageTexture::Create(context, detiler, resource, 0); + recorder.Keep(storage); + Texture storageView(context, storage, resource, identity); + const auto wordCube = [&words](std::vector& backing, std::uint32_t format, const std::function& word) { + auto cubeWords = words; + cubeWords[1] = (format << 20u) | (3u << 30u); + auto described = DecodeTextureResource(cubeWords); + const auto layout = DescribeSurface(described); + backing.assign(static_cast(layout.guestBytes) + 256, std::byte{}); + auto* base = reinterpret_cast((reinterpret_cast(backing.data()) + 255) & ~std::uintptr_t{255}); + for (std::uint32_t layer = 0; layer < layout.imageLayers; ++layer) { + const auto value = word(layer); + for (std::uint64_t offset = 0; offset < layout.layerBytes; offset += sizeof(value)) std::memcpy(base + layout.GuestLayerOffset(layer) + offset, &value, sizeof(value)); + } + described.baseAddress = reinterpret_cast(base); + return described; + }; + std::vector depthMemory; + const auto depth = wordCube(depthMemory, 22u, [](std::uint32_t layer) { + const float value = static_cast(layer + 1) / 8.0f; + std::uint32_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + return bits; + }); + auto depthStorage = StorageTexture::Create(context, detiler, depth, 0); + recorder.Keep(depthStorage); + Require(Texture::CanDepthCopyFrom(*depthStorage, depth), "a cube with a slice past its last whole cube cannot take a depth copy"); + Texture depthCopy(context, depthStorage, depth, identity, Texture::DepthCopyTag{}); + depthCopy.RefreshDepthCopy(); + constexpr std::array unsignedFloats{960u, 1024u, 1056u, 1088u, 1104u, 1120u, 1136u}; + std::vector repackMemory; + const auto repacked = wordCube(repackMemory, 43u, [&unsignedFloats](std::uint32_t layer) { return unsignedFloats[layer] << 21u; }); + auto repackStorage = StorageTexture::Create(context, detiler, repacked, 0); + recorder.Keep(repackStorage); + Require(Texture::CanRepackCopyFrom(*repackStorage, repacked), "a cube with a slice past its last whole cube cannot take a repacked copy"); + Texture repackCopy(context, repackStorage, repacked, identity, Texture::RepackCopyTag{}); + repackCopy.RefreshDepthCopy(); + for (std::uint32_t layer = 0; layer < geometry.imageLayers; ++layer) { + const auto expected = 16.0f * (layer + 1) / 255.0f; + expectRed(program.Red(texture.View(), texture.Layout(), 0, static_cast(layer)), expected, "a cube snapshot with a slice past its last whole cube sampled the wrong slice"); + expectRed(program.Red(storageView.View(), storageView.Layout(), 0, static_cast(layer)), expected, "a cube storage view with a slice past its last whole cube sampled the wrong slice"); + expectRed(program.Red(depthCopy.View(), depthCopy.Layout(), 0, static_cast(layer)), static_cast(layer + 1) / 8.0f, "a cube depth copy with a slice past its last whole cube sampled the wrong slice"); + expectRed(program.Red(repackCopy.View(), repackCopy.Layout(), 0, static_cast(layer)), static_cast(layer + 1), "a cube repacked copy with a slice past its last whole cube sampled the wrong slice"); + } +} + +struct SampledViewProbe { + Context& context; + PFN_vkGetDeviceProcAddr resolver; + 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); @@ -6093,8 +9004,8 @@ bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool impor const std::array range{std::pair{address + unit, address + 2 * unit}}; Require(StorageTexture::AnyPendingOverlaps(range), "the color producer did not leave GPU results pending"); }; - const auto attributeFor = [](std::uint64_t base, std::uint32_t stride, std::uint32_t fetchIndex) { - return ShaderRecompiler::VertexAttribute{0, 4, {{static_cast(base), static_cast(base >> 32u) | (stride << 16u), static_cast(bytes / 16), 77u << 12u}}, fetchIndex}; + const auto attributeFor = [](std::uint64_t base, std::uint32_t stride, std::uint32_t fetchIndex, std::uint32_t outOfBoundsSelect = 0) { + return ShaderRecompiler::VertexAttribute{0, 4, {{static_cast(base), static_cast(base >> 32u) | (stride << 16u), static_cast(bytes / 16), (77u << 12u) | (outOfBoundsSelect << 28u)}}, fetchIndex}; }; const auto check = [&](std::uint32_t edge) { std::vector texels(64 * 64 * 4); @@ -6125,7 +9036,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); @@ -6165,9 +9076,12 @@ bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool impor reject([&] { Draw(context, consumer, indexed, shaders); }, "index buffer aliases the render target"); } reject([&] { Draw(context, consumer, triangle, shaders); }, "vertex buffer aliases the render target"); - vertex.vertexAttributes = {attributeFor(address, 0, fetchIndex)}; + vertex.vertexAttributes = {attributeFor(address, 0, fetchIndex, 3)}; reject([&] { Draw(context, consumer, draw, shaders); }, "vertex buffer aliases the render target"); } + vertex.vertexAttributes = {attributeFor(address, 0, 0)}; + Draw(context, consumer, triangle, shaders); + WaitForOptimizedPipelines(context.device); std::cout << "Vertex range and pending color producer tests passed (" << (imported ? "imported" : "copied") << ")\n"; return true; } @@ -6228,15 +9142,220 @@ 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"; } +int pipelineCacheFileTests(const Device& device) { + const auto& context = device.GetContext(); + const auto directory = std::filesystem::temp_directory_path() / ("aps5-pipeline-cache-file-test-" + std::to_string(std::random_device{}())); + struct RemoveDirectory { + std::filesystem::path directory; + ~RemoveDirectory() { + std::error_code error; + std::filesystem::remove_all(directory, error); + } + } removeDirectory{directory}; +#ifdef _WIN32 + _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); + _putenv_s("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str()); +#else + setenv("ANYPS5_NO_SHADER_CACHE", "0", 1); + setenv("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str(), 1); +#endif + const auto properties = device.Properties(); + struct Objects { + const Context& context; + VkDescriptorSetLayout setLayout = VK_NULL_HANDLE; + VkPipelineLayout layout = VK_NULL_HANDLE; + std::array modules{}; + ~Objects() { + for (const auto module : modules) { + if (module != VK_NULL_HANDLE) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + } + if (layout != VK_NULL_HANDLE) context.Function("vkDestroyPipelineLayout")(context.device, layout, nullptr); + if (setLayout != VK_NULL_HANDLE) context.Function("vkDestroyDescriptorSetLayout")(context.device, setLayout, nullptr); + } + } objects{context}; + const std::array bindings{VkDescriptorSetLayoutBinding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, VkDescriptorSetLayoutBinding{1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}}; + VkDescriptorSetLayoutCreateInfo setLayoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + setLayoutInfo.bindingCount = static_cast(bindings.size()); + setLayoutInfo.pBindings = bindings.data(); + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &setLayoutInfo, nullptr, &objects.setLayout), "vkCreateDescriptorSetLayout"); + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(float) * 2}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = 1; + layoutInfo.pSetLayouts = &objects.setLayout; + layoutInfo.pushConstantRangeCount = 1; + layoutInfo.pPushConstantRanges = &push; + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &objects.layout), "vkCreatePipelineLayout"); + const std::array, 2> codes{std::span(SAMPLE_LOD_SPV), std::span(SAMPLE_ARRAY_SPV)}; + for (std::size_t index = 0; index < codes.size(); ++index) { + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = codes[index].size_bytes(); + moduleInfo.pCode = codes[index].data(); + Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &objects.modules[index]), "vkCreateShaderModule"); + } + const auto build = [&](VkPipelineCache cache, std::size_t index) { + VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_COMPUTE_BIT, objects.modules[index], "main", nullptr}; + info.layout = objects.layout; + VkPipeline pipeline = VK_NULL_HANDLE; + Check(context.Function("vkCreateComputePipelines")(context.device, cache, 1, &info, nullptr, &pipeline), "vkCreateComputePipelines"); + context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + }; + const auto bytes = [&](VkPipelineCache cache) { + std::size_t size = 0; + Check(context.Function("vkGetPipelineCacheData")(context.device, cache, &size, nullptr), "vkGetPipelineCacheData"); + return size; + }; + const auto footprint = [&](std::size_t index) { + VkPipelineCacheCreateInfo info{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO}; + VkPipelineCache fresh = VK_NULL_HANDLE; + Check(context.Function("vkCreatePipelineCache")(context.device, &info, nullptr, &fresh), "vkCreatePipelineCache"); + const auto before = bytes(fresh); + build(fresh, index); + const auto after = bytes(fresh); + context.Function("vkDestroyPipelineCache")(context.device, fresh, nullptr); + return after > before ? after - before : 0; + }; + const std::array footprints{footprint(0), footprint(1)}; + const auto holds = [&](VkPipelineCache cache, std::size_t index) { + const auto before = bytes(cache); + build(cache, index); + const auto after = bytes(cache); + return after < before + footprints[index] / 2; + }; + const auto cacheFile = [&] { + for (const auto& entry : std::filesystem::directory_iterator(directory)) Require(entry.is_directory(), "a pipeline cache file lives in the shader cache root"); + char version[17]; + std::snprintf(version, sizeof(version), "%016llx", static_cast(ShaderRecompiler::ShaderDiskCache::Generated::SourceVersion)); + const auto versionDirectory = directory / version; + Require(std::filesystem::is_directory(versionDirectory), "the current shader cache version has no directory"); + std::vector found; + for (const auto& entry : std::filesystem::directory_iterator(versionDirectory)) found.push_back(entry.path()); + Require(found.size() == 1 && found.front().filename().string().starts_with("vk-pipelines-"), "the current shader cache version's directory does not hold exactly one pipeline cache file"); + return found.front(); + }; + const auto contents = [](const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + return std::vector(std::istreambuf_iterator(stream), std::istreambuf_iterator()); + }; + if (footprints[0] == 0 || footprints[1] == 0) { + std::cout << "skipped, the device does not keep pipelines in a pipeline cache\n"; + return 77; + } + for (const bool firstSavesFirst : {true, false}) { + std::filesystem::remove_all(directory); + auto first = std::make_unique(context, properties); + auto second = std::make_unique(context, properties); + build(first->Handle(), 0); + build(second->Handle(), 1); + if (firstSavesFirst) { + first.reset(); + second.reset(); + } else { + second.reset(); + first.reset(); + } + PipelineCache third(context, properties); + Require(holds(third.Handle(), 0), firstSavesFirst ? "the cache file lost the pipeline of the object that saved first" : "the cache file lost the pipeline of the object that saved last"); + Require(holds(third.Handle(), 1), firstSavesFirst ? "the cache file lost the pipeline of the object that saved last" : "the cache file lost the pipeline of the object that saved first"); + } + const auto path = cacheFile(); + const auto stored = contents(path); + { + 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"; + } + { + PipelineCache replacing(context, properties); + build(replacing.Handle(), 1); + } + { + PipelineCache reloaded(context, properties); + Require(holds(reloaded.Handle(), 1), "an invalid cache file was not replaced by the saving object's pipelines"); + } + { + PipelineCache busy(context, properties); + build(busy.Handle(), 0); + std::promise held; + std::promise release; + std::thread creation([&] { + auto use = busy.Synchronization().LockForPipelineUse(); + held.set_value(); + release.get_future().wait(); + }); + held.get_future().wait(); + auto saving = std::async(std::launch::async, [&] { busy.SaveUnlessBusy(); }); + const bool saveReturned = saving.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + bool otherCreationProceeded = false; + if (saveReturned) { + auto other = std::async(std::launch::async, [&] { auto use = busy.Synchronization().LockForPipelineUse(); }); + otherCreationProceeded = other.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + release.set_value(); + other.wait(); + } else { + release.set_value(); + } + creation.join(); + saving.wait(); + Require(saveReturned, "a periodic pipeline cache save waited for a pipeline creation to finish"); + Require(otherCreationProceeded, "a pipeline creation waited behind a periodic pipeline cache save"); + } + std::cout << "Pipeline cache file merge tests passed\n"; + return 0; +} + void descriptorPoolTests(const Device& device) { DescriptorCache cache(device.GetContext()); const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 32, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; @@ -6395,6 +9514,216 @@ void integerClearTests(const Device& device) { std::cout << "integer storage clears: " << tested << " formats ok\n"; } +bool clearHandOverTests(const Device& device, Recorder& recorder, bool watched) { + const auto& base = device.GetContext(); + const std::string what = watched ? "clear-key hand-over (watched)" : "clear-key hand-over"; + if (base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: " << what << " not tested\n"; + return false; + } + constexpr std::size_t surfaceA = 256 * 256 * 4; + constexpr std::size_t surfaceB = 128 * 128 * 8; + constexpr std::size_t keyCount = surfaceB / 256; + constexpr std::size_t bytes = surfaceA + 65536; + static_assert(surfaceB <= surfaceA && keyCount <= bytes - surfaceA, "B lies over the first units of A, and B's DCC keys follow A inside the block"); + const auto decided = PrepareImportWatch(base); +#ifndef _WIN32 + if (watched && decided == ImportWatch::Unwatch) { + std::cout << "host imports are compared on this device: " << what << " not tested\n"; + return false; + } +#endif + void* block = nullptr; + if (watched) { + block = AllocateWatched(bytes, 65536); + if (block == nullptr) { + std::cout << "no write watching: " << what << " not tested\n"; + return false; + } + } else { +#ifdef _WIN32 + block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + block = std::aligned_alloc(65536, bytes); +#endif + } + Require(block != nullptr && reinterpret_cast(block) % 65536 == 0, "cannot allocate the clear-key hand-over block"); + if (watched) SetImportWatch(base, ImportWatch::Watch); + struct RestoreWatch { + const Context& context; + ImportWatch decided; + ~RestoreWatch() { SetImportWatch(context, decided); } + } restoreWatch{base, decided}; + auto* memory = static_cast(block); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + bool watched; + ~Unregister() { + StorageTexture::SetClearHandOver(true); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + if (watched) ReleaseWatched(block, bytes); + } + } unregister{base, block, address, watched}; + if (HostImportFor(base, address, bytes) == nullptr) { + std::cout << "host import of the clear-key hand-over block refused: " << what << " not tested\n"; + return false; + } + if (watched && !AgcDriver::GuestMemory::Watched(address, bytes)) { + std::cout << "the imported block left the write watch: " << what << " not tested\n"; + return false; + } + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + const auto surface = [&](std::uint32_t width, std::uint32_t height, std::uint32_t format, std::uint64_t dcc) { + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = width; + resource.height = height; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = format; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + resource.dccAddress = dcc; + return resource; + }; + const auto resourceA = surface(256, 256, 56, 0); + const auto resourceB = surface(128, 128, 71, address + surfaceA); + Require(DescribeSurface(resourceA).guestBytes == surfaceA && DescribeSurface(resourceB).guestBytes == surfaceB, "the clear-key hand-over surfaces have unexpected sizes"); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto draw = [&](const std::shared_ptr& image, VkClearColorValue value) { + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + }; + const auto readImage = [&](const std::shared_ptr& image, std::uint32_t width, std::uint32_t height, std::size_t texelBytes) { + Buffer readback(context, static_cast(width) * height * texelBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {width, height, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + const auto pixels = readback.Bytes(); + return std::vector(pixels.begin(), pixels.end()); + }; + const auto readMemory = [&](std::uint64_t from, std::size_t length) { + std::vector read(length); + AgcDriver::GuestMemory::Read(from, read); + recorder.Sync(); + if (!watched) std::memcpy(read.data(), memory + (from - address), length); + return read; + }; + const auto filled = [](std::span read, std::span texel) { + for (std::size_t i = 0; i < read.size(); ++i) { + if (std::to_integer(read[i]) != texel[i % texel.size()]) return false; + } + return true; + }; + constexpr std::array red{255, 0, 0, 255}; + constexpr std::array blue{0, 0, 255, 255}; + constexpr std::array green{0, 0, 0x00, 0x3c, 0, 0, 0x00, 0x3c}; + constexpr std::array cleared{}; + enum class Order { Superseded, ReadFirst, Rewritten, CpuStore }; + struct Outcome { + std::vector bytes; + std::vector imageB; + std::uint64_t handOvers = 0; + std::uint64_t superseded = 0; + }; + const auto run = [&](bool on, Order order) { + const std::string where = what + (order == Order::Superseded ? ", A never read again" : order == Order::ReadFirst ? ", A read before B wrote" : order == Order::Rewritten ? ", A written again after the hand-over" : ", a CPU store before B wrote") + (on ? ", on" : ", off"); + StorageTexture::SetClearHandOver(on); + recorder.Sync(); + std::memset(memory, 0x55, surfaceA); + std::memset(memory + surfaceA, 0x00, keyCount); + Outcome outcome{}; + const auto handOvers = StorageTexture::ClearHandOvers(); + const auto superseded = StorageTexture::HandedOverSuperseded(); + { + auto imageA = StorageTexture::Create(context, detiler, resourceA, 0); + auto imageB = StorageTexture::Create(context, detiler, resourceB, 0); + draw(imageA, {{1.0f, 0.0f, 0.0f, 1.0f}}); + imageB->Refresh(); + if (on && watched) { + Require(StorageTexture::ClearHandOvers() == handOvers + 1, where + ": B's refresh under clear keys did not hand A's results over"); + Require(StorageTexture::FindPending(address, surfaceA) == imageA, where + ": A's results did not stay pending"); + Require(std::all_of(memory, memory + 2 * 65536, [](std::uint8_t value) { return value == 0x55; }), where + ": B's refresh stored A's results over B"); + } else { + Require(StorageTexture::ClearHandOvers() == handOvers, where + ": the hand-over ran while off"); + } + outcome.imageB = readImage(imageB, 128, 128, 8); + Require(filled(outcome.imageB, cleared), where + ": B does not hold its keys' clear"); + if (order == Order::ReadFirst) { + Require(filled(readMemory(address, 65536), red), where + ": a read of A's memory before B wrote it did not see A's results"); + imageA->Refresh(); + Require(filled(readImage(imageA, 256, 256, 4), red), where + ": A read again as A lost its results"); + } + if (order == Order::Rewritten) { + draw(imageA, {{0.0f, 0.0f, 1.0f, 1.0f}}); + Require(filled(readMemory(address, 65536), blue), where + ": a read of A's memory did not see A's newer results"); + imageB->Refresh(); + Require(filled(readImage(imageB, 128, 128, 8), cleared), where + ": B refreshed again does not hold its keys' clear"); + } + if (order == Order::CpuStore) { + std::memset(memory + 65536, 0x77, 65536); + } + draw(imageB, {{0.0f, 1.0f, 0.0f, 1.0f}}); + Require(StorageTexture::FlushPending(address, bytes, nullptr, "clear-key hand-over test", PublishScope::Whole), where + ": nothing was pending at the end"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + outcome.bytes = readMemory(address, surfaceA); + Require(filled(readImage(imageB, 128, 128, 8), green), where + ": B lost its results"); + } + outcome.handOvers = StorageTexture::ClearHandOvers() - handOvers; + outcome.superseded = StorageTexture::HandedOverSuperseded() - superseded; + recorder.Sync(); + return outcome; + }; + for (const auto order : {Order::Superseded, Order::ReadFirst, Order::Rewritten, Order::CpuStore}) { + const std::string name = what + (order == Order::Superseded ? ", A never read again" : order == Order::ReadFirst ? ", A read before B wrote" : order == Order::Rewritten ? ", A written again after the hand-over" : ", a CPU store before B wrote"); + const auto off = run(false, order); + const auto on = run(true, order); + const auto& lastA = order == Order::Rewritten ? blue : red; + if (order != Order::CpuStore) Require(filled(std::span(off.bytes).first(2 * 65536), green), name + ": B's results did not reach guest memory with the hand-over off"); + Require(filled(std::span(off.bytes).subspan(2 * 65536), lastA), name + ": A's results did not reach guest memory with the hand-over off"); + Require(on.imageB == off.imageB, name + ": B held other texels with the hand-over on than off"); + Require(on.bytes == off.bytes, name + ": the hand-over left other guest bytes than the old order"); + Require(off.handOvers == 0 && off.superseded == 0, name + ": the hand-over ran while off"); + if (!watched) { + Require(on.handOvers == 0, name + ": results were handed over in memory the write tracker does not watch"); + continue; + } + if (order == Order::Superseded) Require(on.superseded == 2, name + ": B's results did not drop A's two units under B (dropped " + std::to_string(on.superseded) + ")"); + if (order == Order::ReadFirst) Require(on.superseded <= 1, name + ": a unit stored by the read was dropped again"); + } + std::cout << what << ": ok\n"; + return true; +} + void denormalClearTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { @@ -6538,13 +9867,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; @@ -6576,6 +9905,15 @@ void pipelineLibraryTests(const Device& device) { Require(otherPattern != pattern && counters().built[2] == beforePatternChange[2] + 1u && counters().built[3] == beforePatternChange[3] + 1u && counters().built[0] == beforePatternChange[0] && counters().built[1] == beforePatternChange[1], "a custom sample pattern did not rebuild exactly the fragment shader and fragment output libraries"); state.sampleLocations[0] = 0x10; Require(lookup() == pattern && counters().built[2] == beforePatternChange[2] + 1u && counters().built[3] == beforePatternChange[3] + 1u, "a cached custom sample pattern was not reused"); + if ((context.sampleLocationGridSampleCounts & VK_SAMPLE_COUNT_4_BIT) != 0) { + state.sampleLocationGrid = true; + state.sampleLocations[3u * 8u] = 0x20; + const auto gridPattern = lookup(); + Require(gridPattern != pattern && counters().built[2] == beforePatternChange[2] + 2u && counters().built[3] == beforePatternChange[3] + 2u, "a pattern varying between pixels shared a uniform pattern's fragment libraries"); + state.sampleLocationGrid = false; + state.sampleLocations[3u * 8u] = 0; + Require(lookup() == pattern, "the uniform pattern was not reused after a grid pattern"); + } std::optional compatibleDepth, incompatibleDepth; state.customSampleLocations = false; @@ -6748,6 +10086,26 @@ void Run(Context context) { int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--collect-mapping-history-only") { + if (!collectMappingHistoryTests()) return 77; + std::cout << "collect mapping history tests passed\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--hint-mapping-history-only") { + if (!hintMappingHistoryTests()) return 77; + std::cout << "hint mapping history tests passed\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--partial-watch-restoration-only") { + if (!partialWatchRestorationTests()) return 77; + std::cout << "partial private write-watch restoration tests passed\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--device-selection-only") { + deviceSelectionTests(); + std::cout << "recorder device selection tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--queued-write-groups-only") { queuedWriteGroupTests(); std::cout << "queued write group tests passed\n"; @@ -6755,13 +10113,52 @@ int main(int argc, char** argv) { } std::unique_ptr created; try { - created = std::make_unique(); + 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" || std::string_view(argv[1]) == "--host-import-lifetime-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]) == "--gpu-timing-complete-only" || std::string_view(argv[1]) == "--gpu-timing-incomplete-only")) { + return gpuTimingCompletionTests(device, std::string_view(argv[1]) == "--gpu-timing-incomplete-only"); + } + if (argc == 2 && std::string_view(argv[1]) == "--import-watch-only") { + importWatchTests(device); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--import-watch-other-family-only") { + if (device.GetContext().settleQueue == VK_NULL_HANDLE) { + std::cout << "skipped, no other queue family to settle imports on\n"; + return 77; + } + importWatchTests(device); + return 0; + } +#ifdef _WIN32 + if (argc == 2 && std::string_view(argv[1]) == "--host-import-lifetime-other-family-only") { + if (device.GetContext().settleFamily == device.GetContext().queueFamily) { + std::cout << "skipped, no other queue family to settle imports on\n"; + return 77; + } + return RunHostImportLifetimeTests(device.GetContext()); + } + 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; + if (argc == 2 && std::string_view(argv[1]) == "--settle-storage-image-only") return settleStorageImageTests(device) ? 0 : 77; + if (argc == 2 && std::string_view(argv[1]) == "--reimport-bench") return reimportBench(device); + if (argc == 2 && std::string_view(argv[1]) == "--host-import-cut-only") return hostImportCutTests(device); +#endif if (argc == 2 && std::string_view(argv[1]) == "--host-import-retirement-only") { const auto alignment = device.GetContext().hostImportAlignment; if (alignment == 0 || alignment > 65536) { @@ -6771,6 +10168,20 @@ int main(int argc, char** argv) { hostImportRetirementTests(device); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--sparse-run-import-only") { + if (device.GetContext().hostImportAlignment == 0) { + std::cout << "skipped, sparse run imports require host imports\n"; + return 77; + } + return sparseRunImportTests(device, false) ? 0 : 77; + } + if (argc == 2 && std::string_view(argv[1]) == "--sparse-run-copied-only") { + if (device.GetContext().hostImportAlignment == 0) { + std::cout << "skipped, sparse run imports require host imports\n"; + return 77; + } + return sparseRunImportTests(device, true) ? 0 : 77; + } if (argc == 2 && std::string_view(argv[1]) == "--pipeline-library-only") { pipelineLibraryTests(device); return 0; @@ -6780,6 +10191,10 @@ int main(int argc, char** argv) { return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-cache-file-only") { + return pipelineCacheFileTests(device); + } + if (argc == 2 && std::string_view(argv[1]) == "--descriptor-pool-only") { descriptorPoolTests(device); return 0; @@ -6816,15 +10231,41 @@ int main(int argc, char** argv) { storageUploadLifetimeTests(device, recorder, std::string_view(argv[1]) == "--storage-upload-failure-only"); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--tiled-resolve-only") { + if ((device.GetContext().limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0 || !device.Graphics()) { + std::cout << "skipped, the device lacks 4x color targets or a graphics queue\n"; + return 77; + } + tiledResolveTests(device, recorder); + std::cout << "CB resolves into tiled targets tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--cube-only") { singleCubeTests(device, recorder); + cubeExtraSliceTests(device, recorder); std::cout << "Single cube snapshot and storage sampling tests passed\n"; return 0; } + 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; } + if (argc == 2 && std::string_view(argv[1]) == "--kept-unit-settle-only") { + keptUnitSettleTests(device, recorder); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--clear-hand-over-only") { + clearHandOverTests(device, recorder, false); + if (!clearHandOverTests(device, recorder, true)) { + std::cout << "skipped, the device imports no watched host memory for the clear-key hand-over\n"; + return 77; + } + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--integer-clear-only") { integerClearTests(device); return 0; @@ -6843,11 +10284,26 @@ int main(int argc, char** argv) { std::cout << "Completion label storage and ordering tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--read-written-staging-only") { + if (!readWrittenStagingTests(device, recorder)) return 77; + std::cout << "read and written staging tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--written-buffer-order-only") { if (!writtenBufferOrderTests(device, recorder)) return 77; std::cout << "written buffer order tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--read-list-only") { + readListTests(device, recorder); + std::cout << "Read list tests passed\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--deferred-failure-report-only") { + deferredFailureReportTests(recorder); + std::cout << "Deferred failure report tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--host-import-unmap-only") { gpu.unlock(); hostImportUnmapTests(device, recorder); @@ -6866,12 +10322,44 @@ int main(int argc, char** argv) { parkedImportUnmapTests(device); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--idle-publish-wait-only") { + if (!AgcDriver::Graphics::UnitShadowEnabled() || device.GetContext().hostImportAlignment == 0) { + std::cout << "skipped, unit shadows or host imports are unavailable\n"; + return 77; + } + idlePublishWaitTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--unit-shadow-only") { if (!AgcDriver::Graphics::UnitShadowEnabled() || device.GetContext().hostImportAlignment == 0) { std::cout << "skipped, unit shadows or host imports are unavailable\n"; return 77; } - unitShadowTests(device, recorder); + if (!unitShadowTests(device, recorder)) { + std::cout << "skipped, the device does not import watched host memory for unit shadows\n"; + return 77; + } + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--split-writeback-only") { + if (device.GetContext().hostImportAlignment == 0) { + std::cout << "skipped, host imports are unavailable\n"; + return 77; + } + splitWriteBackTests(device, recorder); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--storage-write-back-only") { + if (device.GetContext().hostImportAlignment == 0) { + std::cout << "skipped, host imports are unavailable\n"; + return 77; + } + storageRefreshTests(device, recorder, false); + storageRefreshTests(device, recorder, true); + writeBackPaddingTests(device, recorder, false); + writeBackPaddingTests(device, recorder, true); + singlePassTests(device, recorder, false); + singlePassTests(device, recorder, true); return 0; } if (argc == 2 && std::string_view(argv[1]) == "--cmask-only") { @@ -6879,6 +10367,14 @@ int main(int argc, char** argv) { std::cout << "CMASK copied clear, write-back and failure tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--writeback-blocks-only") { + if (device.GetContext().hostImportAlignment == 0) { + std::cout << "skipped, host imports are unavailable\n"; + return 77; + } + writeBackBlockTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--metadata-pass-only") { metadataPassTests(device, recorder); std::cout << "Color metadata pass tests passed\n"; @@ -6889,10 +10385,26 @@ int main(int argc, char** argv) { std::cout << "skipped, host imports are unavailable\n"; return 77; } - pendingKeyStoreTests(device, recorder); + if (!pendingKeyStoreTests(device, recorder)) { + std::cout << "skipped, the device does not import the pending key store block\n"; + return 77; + } std::cout << "Pending DCC key store tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--sample-dumps-only") { + if (device.GetContext().hostImportAlignment == 0) { + std::cout << "skipped, host imports are unavailable\n"; + return 77; + } + if (!device.GetContext().bufferDeviceAddress) { + std::cout << "skipped, the device lacks buffer device addresses\n"; + return 77; + } + sampleDumpTests(device, recorder); + std::cout << "Occlusion counter dump tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--shadowed-key-stores-only") { if (!shadowedKeyStoreTests(device, recorder)) { std::cout << "skipped, the device imports no watched host memory under a unit shadow\n"; @@ -6906,20 +10418,40 @@ int main(int argc, char** argv) { std::cout << "In-place draw input tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--draw-inputs-copied-only") { + drawInputInPlaceTests(device, recorder, true); + std::cout << "Copied draw input tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--multisample-only") { multisampleGuestAccessTests(device, recorder); + fmaskDecompressMsaaTests(device, recorder); return 0; } if (argc == 2 && std::string_view(argv[1]) == "--draw-descriptors-only") { drawDescriptorWriteTests(device, recorder); + drawSnapshotTableSetTests(device, recorder); + imageTableFirstLayerRevalidateTests(device, recorder); std::cout << "Draw descriptor writes and snapshot overrides passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--space-pending-writes-only") { + overlappedRangeTests(recorder); + spaceWriteSyncTests(device, recorder); + std::cout << "Address space pending write tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-cap-only") { drawSnapshotCapTests(device, recorder); + unwrittenSnapshotTests(device); + drawSnapshotUploadTests(device, recorder); std::cout << "Draw snapshot cap tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-epoch-only") { + drawSnapshotEpochTests(device); + return 0; + } if (argc == 2 && (std::string_view(argv[1]) == "--vertex-target-range-copied" || std::string_view(argv[1]) == "--vertex-target-range-imported")) { return vertexTargetRangeTests(device, recorder, std::string_view(argv[1]) == "--vertex-target-range-imported") ? 0 : 77; @@ -6932,6 +10464,9 @@ int main(int argc, char** argv) { } #endif readTrackingTests(device, recorder); + readListTests(device, recorder); + overlappedRangeTests(recorder); + spaceWriteSyncTests(device, recorder); writeSettledTests(device, recorder); completionCountTests(device, recorder); afterRecordedWorkTests(device, recorder); @@ -6949,12 +10484,18 @@ int main(int argc, char** argv) { readWrittenStagingTests(device, recorder); depthSurfaceProofTests(device, recorder); multisampleGuestAccessTests(device, recorder); + tiledResolveTests(device, recorder); + fmaskDecompressMsaaTests(device, recorder); misalignedSnapshotTests(device, recorder); misalignedRegionTests(device, recorder); drawSnapshotReuseTests(device, recorder); drawDescriptorWriteTests(device, recorder); + drawSnapshotTableSetTests(device, recorder); + imageTableFirstLayerRevalidateTests(device, recorder); drawSnapshotCapTests(device, recorder); drawSnapshotEvictionTests(device); + unwrittenSnapshotTests(device); + drawSnapshotUploadTests(device, recorder); drawInputReuseTests(device, recorder); drawInputInPlaceTests(device, recorder); RunResidentPresentTests(device.GetContext()); @@ -6968,6 +10509,8 @@ int main(int argc, char** argv) { movedMetadataTests(device, recorder); viewPastLastMipTests(device, recorder); keysFillTests(device, recorder); + clearHandOverTests(device, recorder, false); + clearHandOverTests(device, recorder, true); integerClearTests(device); denormalClearTests(device, recorder); unitShadowTests(device, recorder); @@ -6976,9 +10519,12 @@ int main(int argc, char** argv) { static_cast(writtenBufferOrderTests(device, recorder)); writeBackPaddingTests(device, recorder, false); writeBackPaddingTests(device, recorder, true); + splitWriteBackTests(device, recorder); + writeBackBlockTests(device, recorder); singlePassTests(device, recorder, false); singlePassTests(device, recorder, true); unchangedCpuStampTests(device, recorder); + keptUnitSettleTests(device, recorder); settledBaselineTests(device, recorder); importWatchTests(device); staleGenerationTests(device, recorder); @@ -6988,7 +10534,9 @@ int main(int argc, char** argv) { imageMemoTests(device, recorder); minLodTests(device, recorder); firstLayerViewTests(device, recorder); + sampledStorageBaseLevelUsageTests(device, recorder); singleCubeTests(device, recorder); + cubeExtraSliceTests(device, recorder); atomicViewTests(device, recorder); metadataPassTests(device, recorder); cmaskPassTests(device, recorder); @@ -6999,6 +10547,7 @@ int main(int argc, char** argv) { gpu.unlock(); hostImportUnmapTests(device, recorder); drawSnapshotPatchTests(device); + drawSnapshotEpochTests(device); gpu.lock(); recorder.Activate(); std::cout << "Recorder read tracking and label tests passed\n"; 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/RectListPositionInvariant.cpp b/core/libs/prx/libSceAgcDriver/tests/RectListPositionInvariant.cpp index 4c5be55a9a..a14108fb90 100644 --- a/core/libs/prx/libSceAgcDriver/tests/RectListPositionInvariant.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/RectListPositionInvariant.cpp @@ -1,13 +1,17 @@ #include "Recompiler.hpp" #include #include +#include +#include #include #include #include +#include #include #include #include #include +#include #include #include @@ -42,6 +46,146 @@ bool PositionIsInvariant(std::span words) { return true; } +bool InstructionHasType(spv::Op opcode) { + switch (opcode) { + case spv::OpConstant: + case spv::OpConstantComposite: + case spv::OpVariable: + case spv::OpLoad: + case spv::OpAccessChain: + case spv::OpFAdd: + case spv::OpIAdd: + case spv::OpIMul: + case spv::OpConvertFToS: + case spv::OpCompositeExtract: + case spv::OpCopyObject: + case spv::OpFunction: + return true; + default: + return false; + } +} + +struct CornerValue { + enum class Kind { Float, Int, Vector }; + Kind kind; + float x = 0; + float y = 0; + int index = 0; +}; + +int CornerIndex(std::span words, float x, float y) { + Check(words.size() >= 5 && words[0] == spv::MagicNumber, "rect-list corner: missing SPIR-V module"); + std::unordered_map definitions; + std::unordered_map floatTypes; + std::unordered_map intTypes; + std::uint32_t tessCoord = 0; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + Check(count != 0 && count <= words.size() - offset, "rect-list corner: malformed SPIR-V instruction"); + const auto opcode = static_cast(words[offset] & spv::OpCodeMask); + if (opcode == spv::OpTypeFloat && count >= 2) floatTypes[words[offset + 1]] = true; + if (opcode == spv::OpTypeInt && count >= 2) intTypes[words[offset + 1]] = true; + if (opcode == spv::OpDecorate && count == 4 && words[offset + 2] == spv::DecorationBuiltIn && words[offset + 3] == spv::BuiltInTessCoord) tessCoord = words[offset + 1]; + if (InstructionHasType(opcode) && count >= 3) definitions[words[offset + 2]] = offset; + offset += count; + } + Check(tessCoord != 0, "rect-list evaluation shader has no gl_TessCoord"); + + std::unordered_map known; + std::unordered_map pending; + const auto evaluate = [&](auto&& self, std::uint32_t id) -> CornerValue { + if (const auto found = known.find(id); found != known.end()) return found->second; + Check(!pending[id], "rect-list corner index depends on itself"); + const auto defined = definitions.find(id); + Check(defined != definitions.end(), "rect-list corner index depends on an undefined value"); + pending[id] = true; + const auto offset = defined->second; + const auto count = words[offset] >> spv::WordCountShift; + const auto opcode = static_cast(words[offset] & spv::OpCodeMask); + const auto operand = [&](std::uint32_t index) { + Check(3u + index < count, "rect-list corner instruction is missing an operand"); + return words[offset + 3u + index]; + }; + CornerValue value{CornerValue::Kind::Int}; + switch (opcode) { + case spv::OpConstant: + if (floatTypes[words[offset + 1]]) value = {CornerValue::Kind::Float, std::bit_cast(words[offset + 3])}; + else if (intTypes[words[offset + 1]]) value = {CornerValue::Kind::Int, 0, 0, static_cast(words[offset + 3])}; + else Check(false, "rect-list corner constant is neither float nor int"); + break; + case spv::OpCopyObject: + value = self(self, operand(0)); + break; + case spv::OpLoad: + if (operand(0) == tessCoord) { + value = {CornerValue::Kind::Vector, x, y}; + break; + } + if (const auto pointer = definitions.find(operand(0)); pointer != definitions.end() && static_cast(words[pointer->second] & spv::OpCodeMask) == spv::OpAccessChain && (words[pointer->second] >> spv::WordCountShift) >= 5 && words[pointer->second + 3] == tessCoord) { + const auto component = self(self, words[pointer->second + 4]); + Check(component.kind == CornerValue::Kind::Int && (component.index == 0 || component.index == 1), "rect-list gl_TessCoord component is not x or y"); + value = {CornerValue::Kind::Float, component.index == 0 ? x : y}; + break; + } + Check(false, "rect-list corner index loads something other than gl_TessCoord"); + break; + case spv::OpCompositeExtract: { + const auto composite = self(self, operand(0)); + Check(composite.kind == CornerValue::Kind::Vector && count == 5, "rect-list corner extract is not a gl_TessCoord component"); + const auto component = words[offset + 4]; + Check(component <= 1u, "rect-list corner extract uses gl_TessCoord.z"); + value = {CornerValue::Kind::Float, component == 0u ? composite.x : composite.y}; + break; + } + case spv::OpFAdd: { + const auto left = self(self, operand(0)); + const auto right = self(self, operand(1)); + Check(left.kind == CornerValue::Kind::Float && right.kind == CornerValue::Kind::Float, "rect-list corner addition is not float"); + value = {CornerValue::Kind::Float, left.x + right.x}; + break; + } + case spv::OpConvertFToS: { + const auto source = self(self, operand(0)); + Check(source.kind == CornerValue::Kind::Float, "rect-list corner conversion is not from float"); + value = {CornerValue::Kind::Int, 0, 0, static_cast(source.x)}; + break; + } + case spv::OpIAdd: + case spv::OpIMul: { + const auto left = self(self, operand(0)); + const auto right = self(self, operand(1)); + Check(left.kind == CornerValue::Kind::Int && right.kind == CornerValue::Kind::Int, "rect-list corner arithmetic is not integral"); + value = {CornerValue::Kind::Int, 0, 0, opcode == spv::OpIAdd ? left.index + right.index : left.index * right.index}; + break; + } + default: + Check(false, "rect-list corner index uses opcode " + std::to_string(static_cast(opcode))); + break; + } + pending[id] = false; + known[id] = value; + return value; + }; + + std::optional selected; + for (const auto& [id, offset] : definitions) { + const auto count = words[offset] >> spv::WordCountShift; + if (static_cast(words[offset] & spv::OpCodeMask) != spv::OpAccessChain || words[offset + 3] == tessCoord) continue; + for (std::uint32_t operand = 4; operand < count; ++operand) { + const auto indexId = words[offset + operand]; + const auto defined = definitions.find(indexId); + if (defined != definitions.end() && static_cast(words[defined->second] & spv::OpCodeMask) == spv::OpConstant) continue; + const auto value = evaluate(evaluate, indexId); + Check(value.kind == CornerValue::Kind::Int, "rect-list vertex index is not an integer"); + Check(!selected.has_value() || *selected == value.index, "rect-list evaluation indexes different vertices from one coordinate"); + selected = value.index; + } + } + Check(selected.has_value(), "rect-list evaluation does not index a tessellation corner"); + return *selected; +} + } int main() { @@ -57,6 +201,22 @@ int main() { const auto shaders = BuildRectListShaders(vertex, fragment, target); Check(PositionIsInvariant(shaders.control.spirv.Words()), "rect-list control gl_Position is not Invariant"); Check(PositionIsInvariant(shaders.evaluation.spirv.Words()), "rect-list evaluation gl_Position is not Invariant"); + const auto belowOne = std::nextafter(1.0f, 0.0f); + const std::array, 7> corners{{ + {0.0f, 0.0f, 0.0f}, + {1.0f, 0.0f, 1.0f}, + {0.0f, 1.0f, 2.0f}, + {1.0f, 1.0f, 3.0f}, + {belowOne, 0.0f, 1.0f}, + {0.0f, belowOne, 2.0f}, + {belowOne, belowOne, 3.0f}, + }}; + for (const auto& corner : corners) { + const auto selected = CornerIndex(shaders.evaluation.spirv.Words(), corner[0], corner[1]); + char message[160]; + std::snprintf(message, sizeof(message), "rect-list corner (%.9g, %.9g) selected vertex %d, expected %.0f", static_cast(corner[0]), static_cast(corner[1]), selected, static_cast(corner[2])); + Check(selected == static_cast(corner[2]), message); + } std::printf("rect-list position invariant tests passed\n"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp index 55fb0a5297..a17b7cf87d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp @@ -256,6 +256,34 @@ int main() { return b.eliminate() == 0u && AllUsed({&plain, &flagged, &otherType, &floatImmediate}); }); + passed &= run("four-operand expressions retain distinct trailing immediates across a large block", [] { + Builder b; + auto& source = b.unknown(); + std::vector reads; + std::vector duplicates; + std::vector different; + for (std::uint32_t index = 0; index < 256u; ++index) { + const auto composite = [&](std::uint32_t last) -> IrValue& { + return b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&source, &b.constant(index), &b.constant(7u), &b.constant(last)}); + }; + auto& first = composite(0u); + auto& second = composite(0u); + auto& changed = composite(1u); + auto& firstRead = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&first, &b.constant(3u)}); + auto& secondRead = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&second, &b.constant(3u)}); + auto& changedRead = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&changed, &b.constant(3u)}); + for (auto* read : {&firstRead, &secondRead, &changedRead}) b.keep(*read); + reads.push_back(&firstRead); + duplicates.push_back(&second); + duplicates.push_back(&secondRead); + different.push_back(&changedRead); + } + if (b.eliminate() != 512u || !AllUsed(different)) return false; + for (const auto* duplicate : duplicates) if (duplicate->HasUses()) return false; + for (const auto* read : reads) if (read->UseCount() != 2u) return false; + return true; + }); + passed &= run("float immediates compare by bits: +0 and -0 and two NaN payloads stay apart, one NaN merges", [] { Builder b; auto& x = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()}); diff --git a/core/libs/prx/libSceAgcDriver/tests/RegisterKey.cpp b/core/libs/prx/libSceAgcDriver/tests/RegisterKey.cpp index 448d3455a2..f9f295a3d3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/RegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/RegisterKey.cpp @@ -52,6 +52,22 @@ void testFingerprints() { static_assert(!std::is_convertible_v); std::mt19937 random(42); QueueState queue; + RegisterStateKeyParts parts; + const auto initialKey = RegisterStateKey(queue, false, true, nullptr, &parts); + const auto initialParts = parts; + queue.context[0x000] = 1; + check(RegisterStateKey(queue, false, true, nullptr, &parts) != initialKey, "stable register mutation did not change the key"); + check(parts.userDataRegisters == initialParts.userDataRegisters && parts.auxiliaryRegisters == initialParts.auxiliaryRegisters, "stable register change altered a dynamic-register fingerprint"); + check(parts.stableRegisters != initialParts.stableRegisters, "stable register change was not attributed"); + queue = QueueState{}; + queue.shader[0x00b] = 2; + queue.shader[0x00c] = 1; + const auto beforeRuntimeKey = RegisterStateKey(queue, false, true, nullptr, &parts); + const auto beforeRuntimeMutation = parts; + queue.shader[0x00c] = 2; + check(RegisterStateKey(queue, false, true, nullptr, &parts) != beforeRuntimeKey, "runtime-register mutation did not change the key"); + check(parts.userDataRegisters != beforeRuntimeMutation.userDataRegisters && parts.auxiliaryRegisters == beforeRuntimeMutation.auxiliaryRegisters && parts.stableRegisters == beforeRuntimeMutation.stableRegisters, "user-data register change was not isolated"); + queue = QueueState{}; auto reference = baselineRegisterStateKey(queue); auto tracked = RegisterStateKey(queue); const auto compare = [&] { @@ -125,12 +141,24 @@ void testFingerprints() { check(registers.Fingerprint(low) == lowHash && registers.Fingerprint(high) == registers.RecomputeFingerprint(*high), "projection switch lost a masked write"); } +void testShaderSetKey() { + QueueState queue; + queue.shader[0x008] = 1; + queue.shader[0x009] = 0; + ShaderRegistry first; + ShaderRegistry second; + first[0x100].push_back(std::make_shared()); + second[0x100].push_back(std::make_shared()); + check(DrawShaderSetKey(queue, first) != DrawShaderSetKey(queue, second), "different shader snapshots shared a shader-set key"); +} + } int main() { try { testFingerprints(); + testShaderSetKey(); std::puts("Register fingerprint dependencies and mutations passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp b/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp index e52bb5b232..2d5dfcd701 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp @@ -40,6 +40,8 @@ void decisionTests() { Require(ResidentPresentPath(VK_FORMAT_B8G8R8A8_UNORM, Bgra8, false) == ResidentPresent::Convert, "an 8-bit image that cannot be a blit source is not converted"); Require(ResidentPresentPath(VK_FORMAT_R8G8B8A8_UNORM, Bgra8, true) == ResidentPresent::Convert && ResidentPresentPath(VK_FORMAT_B8G8R8A8_UNORM, Rgba8, true) == ResidentPresent::Convert, "an 8-bit image in the other order is not converted"); Require(ResidentPresentPath(VK_FORMAT_R8G8B8A8_SRGB, Rgba8, true) == ResidentPresent::Convert, "an sRGB image is blitted (the blit would decode it)"); + Require(ResidentPresentPath(VK_FORMAT_R8G8B8A8_UINT, Rgba8, true) == ResidentPresent::Convert && ResidentPresentPath(VK_FORMAT_B8G8R8A8_UINT, Bgra8, true) == ResidentPresent::Convert, "an 8-bit integer image is not converted"); + Require(ResidentPresentPath(VK_FORMAT_R8G8B8A8_UINT, Rgba8 | TenBit, true) == ResidentPresent::None, "an 8-bit integer image presents a 10-bit display"); for (const auto display : {Bgra8 | TenBit, Rgba8 | TenBit}) { for (const auto storage : {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}) { Require(ResidentPresentPath(storage, display, true) == ResidentPresent::Convert, "a 10-bit image is blitted (the blit rounds where the guest path truncates)"); diff --git a/core/libs/prx/libSceAgcDriver/tests/SavedExecMaskLoop.hpp b/core/libs/prx/libSceAgcDriver/tests/SavedExecMaskLoop.hpp new file mode 100644 index 0000000000..d6aa73bc18 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SavedExecMaskLoop.hpp @@ -0,0 +1,19 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SAVEDEXECMASKLOOP_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SAVEDEXECMASKLOOP_HPP + +#include +#include + +namespace AgcTest { + +inline std::vector SavedExecMaskLoop(std::uint32_t rounds) { + std::vector code{0xbea2047eu, 0xbea40380u}; + for (std::uint32_t step = 0; step < rounds; ++step) code.insert(code.end(), {0xbefe0422u, 0x7c080080u, 0x8aa26a7eu}); + const auto branch = 0xbf850000u | static_cast(2 - static_cast(code.size() + 3)); + code.insert(code.end(), {0x80248124u, 0xbf0a8424u, branch, 0xbefe0422u}); + return code; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp b/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp new file mode 100644 index 0000000000..4adf0c824d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Screenshot.cpp @@ -0,0 +1,88 @@ +#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" +#include "Decoder/Png.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void setScreenshotDirectory(const std::filesystem::path& directory) { +#ifdef _WIN32 + require(_wputenv_s(L"ANYPS5_SCREENSHOTS", directory.wstring().c_str()) == 0, "set ANYPS5_SCREENSHOTS"); +#else + require(::setenv("ANYPS5_SCREENSHOTS", directory.c_str(), 1) == 0, "set ANYPS5_SCREENSHOTS"); +#endif +} + +std::vector readFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + require(file.good(), "open the written screenshot"); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +void requestsMerge() { + require(!AgcDriver::Screenshot::Take(), "no screenshot is requested at start"); + AgcDriver::Screenshot::Request(); + AgcDriver::Screenshot::Request(); + require(AgcDriver::Screenshot::Take(), "a requested screenshot is taken"); + require(!AgcDriver::Screenshot::Take(), "repeated requests are taken once"); +} + +void writesRgbPng(const std::filesystem::path& directory) { + const std::array bgra{std::byte{0x10}, std::byte{0x20}, std::byte{0x30}, std::byte{0x00}, std::byte{0xA0}, std::byte{0xB0}, std::byte{0xC0}, std::byte{0x7F}}; + const auto first = AgcDriver::Screenshot::Write("TEST00001", 2, 1, bgra); + const auto second = AgcDriver::Screenshot::Write("TEST00001", 2, 1, bgra); + require(first.parent_path() == directory, "the screenshot is written to ANYPS5_SCREENSHOTS"); + require(first.extension() == ".png" && first.filename().string().starts_with("TEST00001-"), "the screenshot is named after the title ID"); + require(first != second && std::filesystem::exists(second), "a second screenshot does not replace the first"); + const auto image = Decoder::Png::Decode(readFile(first)); + require(image.has_value() && image->width == 2 && image->height == 1, "the screenshot decodes at the frame extent"); + const std::vector expected{0x30, 0x20, 0x10, 0xFF, 0xC0, 0xB0, 0xA0, 0xFF}; + require(image->pixels == expected, "the screenshot holds the frame's colors as opaque RGB"); +} + +void rejectsInvalidFrames() { + const std::array pixel{}; + const auto rejects = [](auto write) { + try { + write(); + } catch (const std::invalid_argument&) { + return true; + } + return false; + }; + require(rejects([&] { AgcDriver::Screenshot::Write("TEST00001", 2, 1, pixel); }), "a frame smaller than its extent is rejected"); + require(rejects([&] { AgcDriver::Screenshot::Write("TEST00001", 0, 1, {}); }), "an empty frame is rejected"); + require(rejects([&] { AgcDriver::Screenshot::Write("", 1, 1, pixel); }), "an empty title ID is rejected"); +} + +} + +int main() try { + const auto root = std::filesystem::temp_directory_path() / ("anyps5_screenshot-" + std::to_string(std::random_device{}())); + const auto directory = root / std::filesystem::path(u8"nested-a\u00e7\u00e3o-\u65e5\u672c"); + std::filesystem::remove_all(root); + setScreenshotDirectory(directory); + requestsMerge(); + writesRgbPng(directory); + rejectsInvalidFrames(); + std::filesystem::remove_all(root); + std::cout << "agc screenshot tests passed\n"; + return 0; +} catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp index c5f9c8d5da..20112c62f3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp @@ -1,7 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp" #include "BdaAbi.hpp" #include "Optimization/BindingAllocator.hpp" -#include "Optimization/ResourceMaterializer.hpp" #include #include #include @@ -10,6 +9,7 @@ #include #include #include +#include namespace { @@ -17,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}; @@ -55,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() { @@ -81,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; @@ -100,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); @@ -107,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() { @@ -119,7 +144,8 @@ void CheckAbi() { Require(bindings.insert(BindingNumber(static_cast(stage), static_cast(binding))).second, "runtime ABI bindings overlap"); } } - Require(BindingNumber(Stage::Main, Binding::ShaderData) == 62u && BindingNumber(Stage::Fragment, Binding::ShaderData) == 125u, "runtime ABI binding numbers changed"); + Require(BindingNumber(Stage::Main, Binding::ShaderData) == 62u && BindingNumber(Stage::Fragment, Binding::ShaderData) == 127u, "runtime ABI binding numbers changed"); + Require(BindingNumber(Stage::Main, Binding::ImageTable) == ShaderRecompiler::RuntimeAbi::ImageTableBinding && BindingNumber(Stage::TessellationEvaluation, Binding::ImageTable) == ShaderRecompiler::RuntimeAbi::ImageTableBinding && !bindings.contains(ShaderRecompiler::RuntimeAbi::ImageTableBinding), "the shared image table binding overlaps a stage binding"); Reject([] { BindingNumber(static_cast(4u), Binding::Buffers); }, "invalid stage or binding"); Reject([] { BindingNumber(Stage::Main, Binding::Count); }, "invalid stage or binding"); Reject([] { ShaderRecompiler::RuntimeAbi::RequireVersion(0u); }, "incompatible version"); @@ -142,13 +168,17 @@ 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"); Reject([&] { allocate(image, RuntimeAbi::SampledHeapCapacity + 1u); }, "heap capacity exceeded"); Require(BindingAllocator{}.FindBinding(allocate(image, 40u).layout, DescriptorBindingForImage(image)).resources.size() == 40u, "a sampled heap does not hold 40 images of one class"); Require(BindingAllocator{}.FindBinding(allocate(image, RuntimeAbi::SampledHeapCapacity).layout, DescriptorBindingForImage(image)).resources.size() == RuntimeAbi::SampledHeapCapacity, "a sampled heap does not hold a full class of images"); - Require(ResourceMaterializer::BindlessSlots() == 16u, "bindless image tables changed size with the sampled heap"); Reject([&] { allocate(image, 1u, RuntimeAbi::SamplerHeapCapacity + 1u); }, "metadata capacity"); image.resourceClass = ImageResourceClass::Storage; image.mipMode = ImageMipMode::DynamicStorage; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 130508267e..8ef8dfde37 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1,5 +1,6 @@ #include "ShaderDiskCache.hpp" #include "CacheKey.hpp" +#include "ShaderCachePack.hpp" #include "VertexInputSpecialization.hpp" #include "SpirvBackend/SpirvSpecialization.hpp" #include "Optimization/ResourceMaterializer.hpp" @@ -16,7 +17,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -28,6 +31,7 @@ #include #include #include +#include #include #ifdef _WIN32 #ifndef NOMINMAX @@ -37,6 +41,9 @@ #define WIN32_LEAN_AND_MEAN #endif #include +#include +#include +#include "prx/libc/include/General.hpp" #endif namespace { @@ -55,8 +62,66 @@ void setEnvironment(const char* name, const std::string& value) { #endif } +#ifdef _WIN32 +void verifyHostStandardError() { + const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-host-stderr-" + std::to_string(std::random_device{}())); + setEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); + setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + int capture[2], guest[2]; + require(::_pipe(capture, 65536, _O_BINARY) == 0, "cannot create host capture"); + require(::_pipe(guest, 65536, _O_BINARY) == 0, "cannot create guest capture"); + const int original = ::_dup(2); + require(original >= 0 && ::_dup2(capture[1], 2) == 0, "cannot capture initial stderr"); + const int saved = ::_dup(2); + require(saved >= 0 && AdoptHostStandardStream_nid_no_patch(2, saved), "cannot preserve host stderr"); + require(::_dup2(guest[1], 2) == 0, "cannot redirect guest stderr"); + require(ShaderDiskCache::Enabled(), "shader disk cache was disabled"); + ShaderDiskCache::Shutdown(); + std::fflush(stderr); + std::fflush(HostStandardErrorFile_nid_no_patch()); + require(::_dup2(original, 2) == 0, "cannot restore stderr"); + ::_close(original); + DWORD hostBytes = 0, guestBytes = 0; + require(::PeekNamedPipe(reinterpret_cast(::_get_osfhandle(capture[0])), nullptr, 0, nullptr, &hostBytes, nullptr) != 0, "cannot inspect host capture"); + require(::PeekNamedPipe(reinterpret_cast(::_get_osfhandle(guest[0])), nullptr, 0, nullptr, &guestBytes, nullptr) != 0, "cannot inspect guest capture"); + std::string host(hostBytes, '\0'); + if (hostBytes != 0) require(::_read(capture[0], host.data(), hostBytes) == static_cast(hostBytes), "cannot read host capture"); + ::_close(capture[0]); + ::_close(capture[1]); + ::_close(guest[0]); + ::_close(guest[1]); + std::filesystem::remove_all(directory); + require(guestBytes == 0, "shader cache diagnostic entered the guest stderr pipe"); + require(host.find("[shader-disk-cache]") != std::string::npos, "shader cache diagnostic did not reach host stderr"); +} +#endif + +void removeCacheDirectory(const std::filesystem::path& directory) { + std::error_code error; +#ifdef _WIN32 + 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.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; + 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.bufferExtent == right.bufferExtent && left.samplerUnnormalized == right.samplerUnnormalized && left.samplerIntegerBorder == right.samplerIntegerBorder && left.samplerCoordinatesIgnored == right.samplerCoordinatesIgnored && left.imageUnnormalized == right.imageUnnormalized && left.imageUnnormalizedBaseLevel == right.imageUnnormalizedBaseLevel && left.imageSamplers == right.imageSamplers; } bool sameBindings(const std::vector& left, const std::vector& right) { @@ -74,13 +139,16 @@ 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"); require(left.vertexOffsetSgpr == right.vertexOffsetSgpr && left.instanceOffsetSgpr == right.instanceOffsetSgpr, prefix + "offset SGPRs differ"); require(left.vertexOffsetShared == right.vertexOffsetShared && left.instanceOffsetShared == right.instanceOffsetShared && left.vertexOffsetConflict == right.vertexOffsetConflict && left.instanceOffsetConflict == right.instanceOffsetConflict, prefix + "offset flags differ"); require(left.parameterExports == right.parameterExports, prefix + "parameter exports differ"); + require(left.stencilReferences == right.stencilReferences, prefix + "stencil reference set differs"); + require(left.stencilReplaySafe == right.stencilReplaySafe, prefix + "stencil replay proof differs"); + require(left.stencilReferenceDword == right.stencilReferenceDword, prefix + "stencil replay push offset differs"); require(left.fragmentParameters.size() == right.fragmentParameters.size(), prefix + "fragment parameter count differs"); for (std::size_t i = 0; i < left.fragmentParameters.size(); ++i) { const auto& a = left.fragmentParameters[i]; @@ -94,9 +162,19 @@ void requireSameResult(const RecompileResult& left, const RecompileResult& right const std::string prefix = std::string(what) + ": "; require(sameBindings(left.bindings, right.bindings), prefix + "bindings differ"); require(left.pushConstants == right.pushConstants, prefix + "push constants differ"); + require(left.readsDepthRemapPush == right.readsDepthRemapPush, prefix + "depth remap push reads differ"); 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"); + require(left.userDataRegisterReads == right.userDataRegisterReads, prefix + "user data register read mask differs"); + for (std::size_t i = 0; i < left.imageTablePoison.size(); ++i) { + const auto& a = left.imageTablePoison[i]; + const auto& b = right.imageTablePoison[i]; + require(a.words == b.words && a.dwordCount == b.dwordCount && a.resource == b.resource && a.reason == b.reason, prefix + "image table poison differs"); + } require(left.vertexAttributes.size() == right.vertexAttributes.size(), prefix + "vertex attribute count differs"); for (std::size_t i = 0; i < left.vertexAttributes.size(); ++i) { const auto& a = left.vertexAttributes[i]; @@ -104,6 +182,7 @@ void requireSameResult(const RecompileResult& left, const RecompileResult& right require(a.location == b.location && a.components == b.components && a.resource.fields == b.resource.fields && a.fetchIndex == b.fetchIndex && a.formatComponents == b.formatComponents, prefix + "vertex attribute differs"); } require(left.barycentricEmulation.active == right.barycentricEmulation.active && left.barycentricEmulation.smooth == right.barycentricEmulation.smooth && left.barycentricEmulation.linear == right.barycentricEmulation.linear, prefix + "barycentric emulation differs"); + require(left.meshBarycentrics == right.meshBarycentrics, prefix + "mesh barycentric layout differs"); } void requireSameVariant(const CompiledVariant& left, const CompiledVariant& right, const char* what) { @@ -132,14 +211,17 @@ DescriptorBinding sampleBinding(std::uint32_t seed) { binding.imageDepthCompare = {false, true, false}; binding.imageWritten = {false, true, true}; binding.imageAtomic = {false, true, false}; + binding.imageShapes = {DescriptorImageShape::Image2D, DescriptorImageShape::Image3D}; binding.bufferAtomic = {true}; binding.bufferWritten = {false, false, true, true, false}; binding.samplerUnnormalized = {false, true, true}; binding.samplerIntegerBorder = {true, false, seed % 2 == 1}; + binding.samplerCoordinatesIgnored = {false, seed % 2 == 0, true}; binding.imageUnnormalized = {true, false, seed % 2 == 0}; binding.imageUnnormalizedBaseLevel = {false, true, seed % 3 == 0}; binding.imageSamplers = {0x5u, 0u, 0x80000000u}; binding.bufferRead = {true, false, false, true, true, false}; + binding.bufferExtent = {32, 0, 16, 4096, 8, 0}; return binding; } @@ -154,9 +236,11 @@ RecompileResult sampleResult() { result.bindings[1].role = DescriptorRole::GuestBuffers; result.bindings[1].imageShape.reset(); result.pushConstants = {std::byte{1}, std::byte{0xff}, std::byte{0}, std::byte{0x80}, std::byte{7}}; + result.readsDepthRemapPush = true; 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}}}; @@ -173,9 +257,20 @@ RecompileResult sampleResult() { result.instanceOffsetConflict = true; result.parameterExports = {0, 3, 7}; result.fragmentParameters = {{0, 1, true, false, true}, {2, 3, false, true}}; + result.stencilReferences = {0u, 17u, 255u}; + result.stencilReplaySafe = true; + result.stencilReferenceDword = 23u; result.poisonedSrtReads = 3; + result.shaderDataUserWords = {2u, 0xffffffffu, 9u}; result.barycentricEmulation = {true, false, true}; + result.meshBarycentrics = MeshBarycentricLayout{{true, true, false}, {FragmentParameter{0, 2, false, true}, FragmentParameter{3, 3, false, false}}}; result.variantId = 99; + result.imageTablePoison = {{{1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u}, 8u, 2u, 6u}, {{9u, 0u, 0u, 0u, 0u, 0u, 0u, 0u}, 4u, 0xffffffffu, 19u}}; + result.imageTableRanges = {{0x1000u, 0x200u}, {0x5000000000ull, 48u}}; + result.imageTableReadRanges = {{{0x1000u, 32u}, {0x1040u, 32u}}, {}, {{0x5000000000ull, 16u}}}; + result.imageTableShader = 0x80003aa700ull; + result.imageTableFaults = 3u; + result.userDataRegisterReads = (std::uint64_t{1} << 63u) | (std::uint64_t{1} << 32u) | 0x25u; return result; } @@ -221,18 +316,23 @@ CompiledVariant sampleVariant() { image.depthBitsCompatible = false; image.constantSwizzle = true; image.constantSwizzleCompatible = false; + image.cubeFaceCompatible = false; image.flatVolumeCompatible = false; + image.singleMipPlaneBiasCompatible = true; + image.volumeArraySampleCompatible = true; image.flatLineCompatible = false; + image.flatVolumeLineCompatible = false; image.foldTexelOffsetsOnly = true; + image.remappedDepth1D = true; image.byElements = 4; image.byComponents = 1; - image.indirectRoot = 0; - image.indirectMappingOffset = 12; - image.indirectSearchIterations = 3; - image.indirectResources = {1, 2, 3}; + image.emulatedCompare = 0x1a07u; + image.table = 0; + image.tableOperation = TableOperation::Unsupported; info.info.images = {image}; ResourceMaterializer::PrepareImageModes(info.info); - info.info.samplers = {{7, 0x10, 3, true, false, SamplerUseExplicitLod | SamplerUseGather, true}}; + info.info.samplers = {{7, 0x10, 3, true, false, SamplerUseExplicitLod | SamplerUseGather, true, 1}}; + info.info.usesFaultBuffer = true; info.info.sampledPairs = {{0, 0, 0x10}}; StageInput input{}; input.kind = StageInputKind::GlobalInvocationId; @@ -257,6 +357,7 @@ CompiledVariant sampleVariant() { info.bindings.pushDataStartDword = 2; info.bindings.memoryOffsetDword = 1; info.bindings.memoryOffsetCount = 5; + info.bindings.stencilReferenceDword = 23u; info.bindings.userDataRegisters = {4, 5, 9}; info.bindings.descriptors = {{DescriptorBindingKind::Buffers, {0, 1}}, {DescriptorBindingKind::FlattenedSrt, {}}}; variant.bindings.layout = info.bindings; @@ -358,12 +459,220 @@ void verifyEntryRoundTrip() { auto previousVersion = file; previousVersion[4] = static_cast(ShaderDiskCache::FormatVersion - 1u); require(ShaderDiskCache::DecodeEntry(previousVersion, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, "an entry with the previous format version loads"); + auto priorStencilVersion = file; + priorStencilVersion[4] = std::byte{32}; + require(ShaderDiskCache::DecodeEntry(priorStencilVersion, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, "an artifact with the prior stencil format loads"); + auto upstreamVersion = file; + upstreamVersion[4] = std::byte{33}; + require(ShaderDiskCache::DecodeEntry(upstreamVersion, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, "a stencil artifact with the upstream resource plan format loads"); auto incompatible = variant; incompatible.artifact.runtimeAbiVersion = RuntimeAbi::Version + 1u; const auto incompatibleFile = ShaderDiskCache::EncodeEntry(key, incompatible); require(ShaderDiskCache::DecodeEntry(incompatibleFile, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, "an entry with an incompatible runtime ABI loads"); } +std::vector packBytes(const std::vector, std::vector>>& entries) { + using namespace ShaderRecompiler::ShaderDiskCache; + std::vector records; + std::uint64_t offset = sizeof(PackHeader) + entries.size() * sizeof(PackRecord); + for (const auto& [key, entry] : entries) { + records.push_back({PackKeyHash(key), offset, entry.size()}); + offset += entry.size(); + } + std::sort(records.begin(), records.end(), [](const PackRecord& left, const PackRecord& right) { return left.hash < right.hash; }); + PackHeader header; + header.sourceVersion = SourceVersion(); + header.fileBytes = offset; + header.entryCount = entries.size(); + header.indexHash = ShaderRecompiler::HashBytes(std::as_bytes(std::span(records))); + std::vector bytes(offset); + std::memcpy(bytes.data(), &header, sizeof(header)); + std::memcpy(bytes.data() + sizeof(header), records.data(), records.size() * sizeof(PackRecord)); + std::size_t at = sizeof(header) + records.size() * sizeof(PackRecord); + for (const auto& [key, entry] : entries) { + std::memcpy(bytes.data() + at, entry.data(), entry.size()); + at += entry.size(); + } + return bytes; +} + +void refreshIndexHash(std::vector& pack, std::size_t entries) { + using namespace ShaderRecompiler::ShaderDiskCache; + const auto index = std::span(pack).subspan(sizeof(PackHeader), entries * sizeof(PackRecord)); + const auto hash = ShaderRecompiler::HashBytes(index); + std::memcpy(pack.data() + offsetof(PackHeader, indexHash), &hash, sizeof(hash)); +} + +void verifyPrebuiltPack(const std::filesystem::path& directory) { + using namespace ShaderRecompiler::ShaderDiskCache; + SampleRequest sample; + const auto key = sample.Key(); + const auto variant = sampleVariant(); + const auto file = EncodeEntry(key, variant); + auto otherKey = key; + otherKey.back() ^= std::byte{1}; + const auto otherFile = EncodeEntry(otherKey, variant); + std::filesystem::create_directories(directory); + const auto writePack = [&](const char* name, const std::vector& bytes) { + const auto path = directory / name; + require(WriteFileAtomically(path, bytes), "cannot write the test pack"); + return path; + }; + PackLoadTimings timings; + CompiledVariant decoded; + { + CachePack pack(writePack("valid.pack", packBytes({{key, file}, {otherKey, otherFile}}))); + require(pack.Valid(), "a valid pack is rejected: " + pack.Error()); + require(pack.Count() == 2, "the pack does not hold both entries"); + require(pack.Load(key, decoded, timings) == LoadStatus::Loaded, "a pack entry does not load"); + requireSameVariant(variant, decoded, "pack round trip"); + auto missingKey = key; + missingKey.front() ^= std::byte{1}; + require(pack.Load(missingKey, decoded, timings) == LoadStatus::Absent, "an absent key loads from the pack"); + } + { + std::vector, std::vector>> mismatched; + mismatched.emplace_back(otherKey, file); + CachePack pack(writePack("mismatch.pack", packBytes(mismatched))); + require(pack.Valid(), "the key mismatch pack is rejected: " + pack.Error()); + require(pack.Load(otherKey, decoded, timings) != LoadStatus::Loaded, "an entry stored under another key loads"); + } + { + auto bytes = packBytes({{key, file}}); + const auto truncated = static_cast(file.size() / 2); + std::memcpy(bytes.data() + sizeof(PackHeader) + offsetof(PackRecord, size), &truncated, sizeof(truncated)); + refreshIndexHash(bytes, 1); + CachePack pack(writePack("truncated.pack", bytes)); + require(pack.Valid(), "the truncated-entry pack is rejected: " + pack.Error()); + require(pack.Load(key, decoded, timings) == LoadStatus::Rejected, "a truncated pack entry loads"); + } + { + auto bytes = packBytes({{key, file}}); + bytes[sizeof(PackHeader)] ^= std::byte{0x10}; + CachePack pack(writePack("corrupt-index.pack", bytes)); + require(!pack.Valid(), "a pack with a damaged index is accepted"); + } + { + auto bytes = packBytes({{key, file}}); + const auto outside = static_cast(bytes.size()) + 16; + std::memcpy(bytes.data() + sizeof(PackHeader) + offsetof(PackRecord, offset), &outside, sizeof(outside)); + refreshIndexHash(bytes, 1); + CachePack pack(writePack("corrupt-record.pack", bytes)); + require(!pack.Valid(), "a pack with an out-of-range record is accepted"); + } + { + auto bytes = packBytes({{key, file}}); + const auto wrong = SourceVersion() + 1; + std::memcpy(bytes.data() + offsetof(PackHeader, sourceVersion), &wrong, sizeof(wrong)); + CachePack pack(writePack("wrong-version.pack", bytes)); + require(!pack.Valid(), "a pack from another compiler source version is accepted"); + } + { + auto bytes = packBytes({{key, file}}); + bytes.front() ^= std::byte{0x10}; + CachePack pack(writePack("unknown-format.pack", bytes)); + require(!pack.Valid(), "a pack with an unknown format is accepted"); + } +} + +SampleRequest prebuiltSample() { + SampleRequest sample; + sample.code[3] ^= 0x5a5a5a5au; + return sample; +} + +int runPrebuiltLoadingProcess() { + using namespace ShaderRecompiler::ShaderDiskCache; + SampleRequest sample = prebuiltSample(); + const auto key = sample.Key(); + const auto expected = sampleVariant(); + CompiledVariant loaded; + require(Load(key, loaded), "the prebuilt pack entry does not load"); + requireSameVariant(expected, loaded, "prebuilt load"); + require(Totals().prebuiltHits == 1, "the prebuilt load was not counted"); + const auto afterPack = Totals(); + require(afterPack.prebuiltBytesRead > 0 && afterPack.prebuiltLoadNanoseconds > 0, "the prebuilt load was not timed"); + require(afterPack.hits == 0 && afterPack.loadFailures == 0 && afterPack.bytesRead == 0 && afterPack.loadNanoseconds == 0, "the prebuilt load reached the writable totals"); + auto looseKey = key; + looseKey.back() ^= std::byte{1}; + CompiledVariant loose; + require(Load(looseKey, loose), "the loose prebuilt entry does not load"); + requireSameVariant(expected, loose, "loose prebuilt load"); + require(Totals().prebuiltHits == 2, "the loose prebuilt load was not counted"); + require(Totals().hits == 0 && Totals().loadFailures == 0 && Totals().bytesRead == 0, "the loose prebuilt load reached the writable totals"); + auto corruptKey = looseKey; + corruptKey.front() ^= std::byte{1}; + CompiledVariant corrupt; + require(!Load(corruptKey, corrupt), "a corrupt loose prebuilt entry loads"); + require(Totals().prebuiltRejected >= 1, "the corrupt loose prebuilt entry was not counted as rejected"); + require(Totals().loadFailures == 0, "the corrupt loose prebuilt entry reached the writable failures"); + auto otherKey = looseKey; + otherKey.front() ^= std::byte{1}; + CompiledVariant other; + require(!Load(otherKey, other), "a key absent from prebuilt storage loads"); + Store(otherKey, std::make_shared(sampleVariant())); + Flush(); + require(Load(otherKey, other), "the writable cache fall-through does not load"); + std::cout << "prebuilt process loaded from the pack and fell through to the writable cache\n"; + return 0; +} + +void verifyPrebuiltAcrossProcesses(const char* self, const std::filesystem::path& directory) { + using namespace ShaderRecompiler::ShaderDiskCache; + SampleRequest sample = prebuiltSample(); + const auto prebuilt = directory / "prebuilt"; + std::filesystem::create_directories(prebuilt); + const auto bytes = packBytes({{sample.Key(), EncodeEntry(sample.Key(), sampleVariant())}}); + require(WriteFileAtomically(prebuilt / "test.pack", bytes), "cannot write the prebuilt pack"); + auto looseKey = sample.Key(); + looseKey.back() ^= std::byte{1}; + require(WriteFileAtomically(prebuilt / EntryName(looseKey), EncodeEntry(looseKey, sampleVariant())), "cannot write the loose prebuilt entry"); + auto corruptKey = looseKey; + corruptKey.front() ^= std::byte{1}; + auto corruptEntry = EncodeEntry(corruptKey, sampleVariant()); + corruptEntry.resize(corruptEntry.size() / 2); + require(WriteFileAtomically(prebuilt / EntryName(corruptKey), corruptEntry), "cannot write the corrupt loose prebuilt entry"); + const std::string command = "\"" + std::string(self) + "\" --load-prebuilt"; + require(std::system(command.c_str()) == 0, "the prebuilt loading process failed"); +} + +void verifyPrebuiltDiscovery(std::string_view layout, bool seed) { + using namespace ShaderRecompiler::ShaderDiskCache; + require(layout == "pack" || layout == "loose" || layout == "versioned" || layout == "file" || layout == "empty" || layout == "missing", "unknown prebuilt discovery layout"); + Shutdown(); + SampleRequest sample = prebuiltSample(); + const auto key = sample.Key(); + const auto variant = sampleVariant(); + if (seed) { + const auto directory = ShaderRecompiler::ShaderCacheDirectory() / "prebuilt"; + const auto entry = EncodeEntry(key, variant); + if (layout == "pack") { + auto otherKey = key; + otherKey.back() ^= std::byte{1}; + require(WriteFileAtomically(directory / "a.pack", packBytes({{key, entry}})), "cannot seed the matching prebuilt pack"); + require(WriteFileAtomically(directory / "z.pack", packBytes({{otherKey, EncodeEntry(otherKey, variant)}})), "cannot seed the other prebuilt pack"); + } else if (layout == "loose") { + require(WriteFileAtomically(directory / EntryName(key), entry), "cannot seed the loose prebuilt entry"); + } else if (layout == "versioned") { + require(WriteFileAtomically(directory / EntryDirectory().filename() / EntryName(key), entry), "cannot seed the versioned prebuilt entry"); + } else if (layout == "file") { + require(WriteFileAtomically(directory, packBytes({{key, entry}})), "cannot seed the direct prebuilt pack"); + } else if (layout == "empty") { + require(WriteFileAtomically(directory / "ignored.txt", entry), "cannot seed the ignored prebuilt file"); + } + std::cout << "prebuilt discovery seeded " << layout << '\n'; + return; + } + CompiledVariant loaded; + const bool expected = layout != "empty" && layout != "missing"; + require(Load(key, loaded) == expected, "prebuilt discovery returned the wrong result for " + std::string(layout)); + if (expected) requireSameVariant(variant, loaded, "prebuilt discovery"); + const auto totals = Totals(); + require(totals.prebuiltHits == (expected ? 1u : 0u) && totals.hits == 0 && totals.writes == 0 && Writers() == 0, + "prebuilt discovery did not remain a read-only prebuilt lookup"); + std::cout << "prebuilt discovery verified " << layout << '\n'; +} + void verifyArtifactStorageIsolation() { SampleRequest sample; const auto key = sample.Key(); @@ -381,6 +690,18 @@ void verifyArtifactStorageIsolation() { require(ShaderDiskCache::EncodeEntry(key, variant) != before, "the stored artifact ignores vertex input metadata"); } +void verifyMeshBarycentricRequestSerialization() { + for (const auto& layout : {std::optional{}, std::optional{MeshBarycentricLayout{{true, true, true}, {FragmentParameter{0, 2, false, true}, FragmentParameter{4, 4, false, false}}}}}) { + SampleRequest sample; + sample.request.shader.stage = ShaderStage::Mesh; + sample.request.context.compute.reset(); + sample.request.graphics = GraphicsCompileContext{0, {}, MeshConfiguration{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}, std::nullopt, {}, layout}; + const auto text = RequestSerializer{}.Serialize(sample.request); + const auto restored = RequestSerializer{}.Deserialize(text); + require(restored.request.graphics.has_value() && restored.request.graphics->meshBarycentrics == layout, "a captured mesh request lost its barycentric layout"); + } +} + void verifyKeySensitivity() { SampleRequest base; const auto key = base.Key(); @@ -423,6 +744,10 @@ void verifyKeySensitivity() { changes("the narrow subgroup clock", [](SampleRequest& sample) { sample.request.target.narrowSubgroupClock = true; }); changes("f64 denormal preservation", [](SampleRequest& sample) { sample.request.target.float64DenormPreserve = true; }); changes("f16 denormal preservation", [](SampleRequest& sample) { sample.request.target.float16DenormPreserve = true; }); + changes("f32 denormal preservation", [](SampleRequest& sample) { sample.request.target.float32DenormPreserve = true; }); + changes("spanned waves", [](SampleRequest& sample) { sample.request.target.spannedWaves = true; }); + changes("robust buffer access 2", [](SampleRequest& sample) { sample.request.target.robustBufferAccess2 = true; }); + changes("wave LDS memory barriers", [](SampleRequest& sample) { sample.request.target.waveLdsMemoryBarriers = true; }); changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; }); changes("the invocation limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupInvocations = 512; }); changes("the shared memory limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSharedMemoryBytes = 32768; }); @@ -447,6 +772,40 @@ void verifyKeySensitivity() { require(changed.first != astro.first && changed.second != astro.second, std::string("the key ignores ") + what); } + const auto meshKeys = [&](const std::optional& layout) { + SampleRequest sample; + sample.request.shader.stage = ShaderStage::Mesh; + sample.request.context.compute.reset(); + sample.request.graphics = GraphicsCompileContext{0, {}, MeshConfiguration{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}, std::nullopt, {}, layout}; + return std::pair{sample.Key(), RecompileCacheKey::ContextHash(sample.request)}; + }; + const std::vector>> meshLayouts{ + {"no mesh barycentric layout", std::nullopt}, + {"smooth mesh barycentrics", MeshBarycentricLayout{{true, true, false}, {}}}, + {"linear mesh barycentrics", MeshBarycentricLayout{{true, false, true}, {}}}, + {"smooth and linear mesh barycentrics", MeshBarycentricLayout{{true, true, true}, {}}}, + {"a per-vertex mesh parameter", MeshBarycentricLayout{{true, true, false}, {FragmentParameter{0, 0, false, true}}}}, + {"another per-vertex mesh parameter source", MeshBarycentricLayout{{true, true, false}, {FragmentParameter{0, 1, false, true}}}}, + {"a flat mesh parameter", MeshBarycentricLayout{{true, true, false}, {FragmentParameter{0, 0, false, false}}}}, + }; + for (std::size_t i = 0; i < meshLayouts.size(); ++i) { + require(meshKeys(meshLayouts[i].second) == meshKeys(meshLayouts[i].second), std::string("the key of ") + meshLayouts[i].first + " is not deterministic"); + for (std::size_t j = i + 1; j < meshLayouts.size(); ++j) { + const auto left = meshKeys(meshLayouts[i].second); + const auto right = meshKeys(meshLayouts[j].second); + require(left.first != right.first && left.second != right.second, std::string("the mesh key confuses ") + meshLayouts[i].first + " with " + meshLayouts[j].first); + } + } + + const auto pixelKeys = [&](bool meshPath) { + SampleRequest sample; + sample.request.shader.stage = ShaderStage::Fragment; + sample.request.context.compute.reset(); + sample.request.graphics = GraphicsCompileContext{0, {}, meshPath ? std::optional(MeshConfiguration{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}) : std::nullopt, std::nullopt, {}, std::nullopt}; + return std::pair{sample.Key(), RecompileCacheKey::ContextHash(sample.request)}; + }; + require(pixelKeys(true).first != pixelKeys(false).first && pixelKeys(true).second != pixelKeys(false).second, "a pixel shader on the mesh path shares a key with the vertex path"); + SampleRequest moved; moved.userData[0] ^= 0x10000u; moved.request.shader.codeAddress += 0x1000000u; @@ -507,6 +866,73 @@ 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); + struct Cleanup { + std::filesystem::path directory; + ~Cleanup() { + std::error_code error; + std::filesystem::remove_all(directory, error); + } + } cleanup{directory}; + const auto space = std::filesystem::space(directory, error); + if (error || space.available < size + (std::uintmax_t{1} << 30u)) { + std::cout << "skipping: not enough free disk space for a file of more than 2 GiB\n"; + 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::filesystem::remove_all(directory, error); + if (error) std::cerr << "cannot remove the large file test directory: " << error.message() << '\n'; + std::cerr.flush(); + std::_Exit(1); + } + 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 @@ -635,6 +1061,62 @@ void verifyPersist() { requireSameArtifact(variant.artifact, persisted->artifact, "a persisted entry whose file was replaced and removed"); } +#ifdef _WIN32 +void verifyLongHostPaths() { + using namespace ShaderRecompiler::ShaderDiskCache; + auto directory = std::filesystem::temp_directory_path() / ("aps5-cache-long-" + std::to_string(std::random_device{}())); + require(directory.native().size() < 224, "temporary path is too long for the cache control"); + directory /= std::wstring(224 - directory.native().size(), L'd'); + const auto extended = [](const auto& path) { return std::filesystem::path(L"\\\\?\\" + path.native()); }; + std::filesystem::create_directories(extended(directory)); + setEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); + setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + std::vector files; + std::filesystem::path entries; + struct Cleanup { + const std::filesystem::path& directory; + const std::vector& files; + const std::filesystem::path& entries; + ~Cleanup() { + ShaderRecompiler::ShaderDiskCache::Shutdown(); + std::error_code error; + for (const auto& file : files) std::filesystem::remove(std::filesystem::path(L"\\\\?\\" + file.native()), error); + if (!entries.empty()) std::filesystem::remove(std::filesystem::path(L"\\\\?\\" + entries.native()), error); + std::filesystem::remove(std::filesystem::path(L"\\\\?\\" + directory.native()), error); + } + } cleanup{directory, files, entries}; + SampleRequest sample; + const auto key = sample.Key(); + const auto variant = sampleVariant(); + entries = ShaderDiskCache::EntryDirectory(); + files.push_back(entries / EntryName(key)); + files.push_back(entries / "last-used"); + require(files.front().native().size() >= MAX_PATH, "the cache persistence control did not cross MAX_PATH"); + const auto persisted = ShaderDiskCache::Persist(key, variant); + std::vector published; + require(ReadWholeFile(files.front(), published) && published == EncodeEntry(key, variant), + "the long-path cache entry was not correctly published before mapping"); + require(persisted != nullptr, "a long-path cache entry was not persisted and mapped"); + MEMORY_BASIC_INFORMATION region{}; + require(VirtualQuery(persisted->artifact.spirv.data(), ®ion, sizeof(region)) == sizeof(region) && region.Type == MEM_MAPPED, + "long-path persisted SPIR-V is not mapped"); + requireSameVariant(variant, *persisted, "long-path persist round trip"); + CompiledVariant loaded; + require(ShaderDiskCache::Load(key, loaded), "a long-path persisted cache entry did not load"); + requireSameVariant(variant, loaded, "long-path load round trip"); + const auto packPath = directory / (std::wstring(75, L'p') + L".pack"); + files.push_back(packPath); + require(WriteFileAtomically(packPath, packBytes({{key, EncodeEntry(key, variant)}})), "cannot publish the long-path cache pack"); + CachePack pack(packPath); + require(pack.Valid(), "a long-path cache pack was rejected: " + pack.Error()); + PackLoadTimings timings; + CompiledVariant decoded; + require(pack.Load(key, decoded, timings) == LoadStatus::Loaded, "a long-path cache pack entry did not load"); + requireSameVariant(variant, decoded, "long-path cache pack round trip"); + std::cout << "long-path shader cache persistence and pack tests passed\n"; +} +#endif + struct ComputeRequest { std::vector code{0xe0700000u, 0x80000000u, 0xbf810000u}; std::array userData{0x10000000u, 0x00000000u, 0x40u, 0x00027facu}; @@ -912,7 +1394,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; @@ -1220,6 +1703,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)); @@ -1246,6 +1733,15 @@ std::vector planFixtures() { add("descriptor through a pointer", 64, {0xf4040280u, 0xfa000018u, 0xbf8cc07fu, 0xf4080105u, 0xfa000000u, 0xbf8cc07fu, 0x34040082u, 0xe0701000u, 0x80010002u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, chained}, {PlanPointerAddress, bufferWords(PlanOutputAddress, 0u, 256u, 0x30027facu)}}); add("descriptor invariant phi", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000000u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}}); + std::vector palette(0x50u + 257u * 8u, 0u); + place(palette, 0, bufferWords(PlanMaterialAddress, 0u, 32u)); + place(palette, 4, bufferWords(PlanOutputAddress, 0u, 16u)); + palette[12] = static_cast(PlanSrtAddress + 0x140u); + palette[13] = static_cast((PlanSrtAddress + 0x140u) >> 32u); + place(palette, 0x10u, imageWords(PlanTextureAddress)); + place(palette, 0x18u, imageWords(PlanTextureAddress + 0x100u)); + add("pointer image table", 64, {0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x7e200500u, 0x8f108210u, 0xf4200444u, 0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0xf40c0900u, 0x24000040u, 0xf09c0f08u, 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, palette}, {PlanMaterialAddress, {0u, 1u, 0u, 1u, 0u, 0u, 0u, 0u}}}); + PlanFixture fragment; fragment.name = "fragment parameters"; fragment.stage = ShaderStage::Fragment; @@ -1267,10 +1763,10 @@ std::vector planFixtures() { IrResourcePlan handBuiltPlan() { IrResourcePlan plan; + plan.valueStorage.Reserve(64); std::uint32_t ids = 0; const auto make = [&](IrOpcode opcode, IrType type) -> IrValue& { - plan.valueStorage.push_back(std::make_unique(opcode, type, ids++)); - return *plan.valueStorage.back(); + return *plan.valueStorage.Create(opcode, type, ids++); }; const auto constant = [&](std::uint32_t value) -> IrValue& { auto& immediate = make(IrOpcode::Void, IrType::U32); @@ -1355,7 +1851,10 @@ IrResourcePlan handBuiltPlan() { DescriptorSource table; table.dwordCount = 8u; table.dwords[0] = &readConst(3); - table.indirectImage = DescriptorSource::IndirectImage{3u, 2u, 16u, 4u, 2u, 32u, true}; + 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; + table.tableColumn->pointerHops = {PointerHop{0u, 0x10u}, PointerHop{8u, 0x20u}}; DescriptorSource heap; heap.dwordCount = 4u; heap.dwords[1] = &readConst(6); @@ -1366,7 +1865,7 @@ IrResourcePlan handBuiltPlan() { wide.SetImmediateU64(0x8000000000000001ull); material.dwords[3] = &wide; plan.descriptorSources = {buffer, table, heap, material}; - plan.controlFlow = {{&condition, {1u, 2u}, {0u}, {}}, {nullptr, {2u}, {3u}, {0u}}, {nullptr, {}, {}, {}}}; + plan.controlFlow = {{&condition, {1u, 2u}, {0u}, {}, {1u}}, {nullptr, {2u}, {3u}, {0u}, {0u, 1u}}, {nullptr, {}, {}, {}, {}}}; plan.materializationSources = {0u, 2u}; plan.srtReads = {{&readConst(4), 2u}, {&pointer, 1u}}; plan.guardedSrtSlots = {1u}; @@ -1396,14 +1895,14 @@ std::vector encodedPlan(const IrResourcePlan& plan) { } bool samePlan(const IrResourcePlan& left, const IrResourcePlan& right) { - if (left.valueStorage.size() != right.valueStorage.size() || left.blockStorage.size() != right.blockStorage.size()) return false; + if (left.valueStorage.Size() != right.valueStorage.Size() || left.blockStorage.size() != right.blockStorage.size()) return false; std::map leftValues; std::map rightValues; std::map leftBlocks; std::map rightBlocks; - for (std::size_t index = 0; index < left.valueStorage.size(); ++index) { - leftValues.emplace(left.valueStorage[index].get(), index); - rightValues.emplace(right.valueStorage[index].get(), index); + for (std::size_t index = 0; index < left.valueStorage.Size(); ++index) { + leftValues.emplace(&left.valueStorage[index], index); + rightValues.emplace(&right.valueStorage[index], index); } for (std::size_t index = 0; index < left.blockStorage.size(); ++index) { leftBlocks.emplace(left.blockStorage[index].get(), index); @@ -1423,9 +1922,9 @@ bool samePlan(const IrResourcePlan& left, const IrResourcePlan& right) { for (std::size_t index = 0; index < left.blockStorage.size(); ++index) { if (left.blockStorage[index]->Id() != right.blockStorage[index]->Id()) return false; } - for (std::size_t index = 0; index < left.valueStorage.size(); ++index) { - const auto& a = *left.valueStorage[index]; - const auto& b = *right.valueStorage[index]; + for (std::size_t index = 0; index < left.valueStorage.Size(); ++index) { + const auto& a = left.valueStorage[index]; + const auto& b = right.valueStorage[index]; if (a.Opcode() != b.Opcode() || a.Type() != b.Type() || a.Id() != b.Id() || a.Flags() != b.Flags() || !(a.Register() == b.Register()) || a.HasImmediate() != b.HasImmediate() || (a.HasImmediate() && a.ImmediateU64() != b.ImmediateU64())) return false; if (a.ArgumentCount() != b.ArgumentCount() || a.IsPhi() != b.IsPhi() || a.PhiBlockCount() != b.PhiBlockCount()) return false; for (std::size_t operand = 0; operand < a.ArgumentCount(); ++operand) { @@ -1440,7 +1939,7 @@ bool samePlan(const IrResourcePlan& left, const IrResourcePlan& right) { for (std::size_t index = 0; index < left.descriptorSources.size(); ++index) { const auto& a = left.descriptorSources[index]; const auto& b = right.descriptorSources[index]; - if (a.dwordCount != b.dwordCount || a.indirectImage != b.indirectImage) return false; + if (a.dwordCount != b.dwordCount || a.tableColumn != b.tableColumn || (a.tableColumn.has_value() && (a.tableColumn->keyDomains != b.tableColumn->keyDomains || a.tableColumn->unkeyed != b.tableColumn->unkeyed || a.tableColumn->maxKey != b.tableColumn->maxKey))) return false; for (std::size_t dword = 0; dword < a.dwords.size(); ++dword) { if (!sameValue(a.dwords[dword], b.dwords[dword])) return false; } @@ -1448,7 +1947,7 @@ bool samePlan(const IrResourcePlan& left, const IrResourcePlan& right) { for (std::size_t index = 0; index < left.controlFlow.size(); ++index) { const auto& a = left.controlFlow[index]; const auto& b = right.controlFlow[index]; - if (!sameValue(a.condition, b.condition) || a.successors != b.successors || a.sources != b.sources || a.images != b.images) return false; + if (!sameValue(a.condition, b.condition) || a.successors != b.successors || a.sources != b.sources || a.images != b.images || a.flatSlots != b.flatSlots) return false; } for (std::size_t index = 0; index < left.srtReads.size(); ++index) { if (!sameValue(left.srtReads[index].value, right.srtReads[index].value) || left.srtReads[index].flatOffset != right.srtReads[index].flatOffset) return false; @@ -1477,14 +1976,14 @@ MaterializedPlan materializePlan(const IrResourcePlan& plan, SrtRuntime runtime) } catch (const std::exception& error) { outcome.error = error.what(); } - outcome.leaf = static_cast(std::find_if(plan.valueStorage.begin(), plan.valueStorage.end(), [&](const auto& value) { return value.get() == outcome.trace.leaf; }) - plan.valueStorage.begin()); + outcome.leaf = static_cast(std::find_if(plan.valueStorage.begin(), plan.valueStorage.end(), [&](const auto& value) { return &value == outcome.trace.leaf; }) - plan.valueStorage.begin()); return outcome; } bool sameMaterialization(const MaterializedPlan& left, const MaterializedPlan& right) { const auto& a = left.snapshot; const auto& b = right.snapshot; - return left.ok == right.ok && left.error == right.error && a.buffers == b.buffers && a.images == b.images && a.samplers == b.samplers && a.flattenedSrt == b.flattenedSrt && a.userData == b.userData && a.uniformFill == b.uniformFill && a.srtPoison == b.srtPoison && a.nullRootReads == b.nullRootReads && + return left.ok == right.ok && left.error == right.error && a.buffers == b.buffers && a.images == b.images && a.samplers == b.samplers && a.flattenedSrt == b.flattenedSrt && a.userData == b.userData && a.uniformFill == b.uniformFill && a.srtPoison == b.srtPoison && a.nullRootReads == b.nullRootReads && a.tables == b.tables && left.leaf == right.leaf && left.trace.leafSlot == right.trace.leafSlot && left.trace.leaves == right.trace.leaves && left.trace.otherReads == right.trace.otherReads; } @@ -1602,23 +2101,20 @@ void verifyMalformedPlans() { rejects("an image past the sources", [](IrResourcePlan& plan) { plan.info.images[0].source = 5u; }); rejects("a descriptor of nine dwords", [](IrResourcePlan& plan) { plan.descriptorSources[1].dwordCount = 9u; }); rejects("a uniform fill of five words", [](IrResourcePlan& plan) { plan.uniformFill.fill.words = 5u; }); - rejects("an image table heap past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].indirectImage->heapSource = 4u; }); - rejects("an image table material past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].indirectImage->materialSource = 4u; }); + 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; std::vector bytes; require(!ShaderDiskCache::EncodePlan(foreign, bytes), "a plan reading a value it does not own was encoded"); auto unblocked = handBuiltPlan(); - auto& phi = *unblocked.valueStorage.emplace_back(std::make_unique(IrOpcode::Phi, IrType::U32, 100u)); - phi.AddArgument(unblocked.valueStorage[0].get()); + auto& phi = *unblocked.valueStorage.Create(IrOpcode::Phi, IrType::U32, 100u); + phi.AddArgument(&unblocked.valueStorage[0]); unblocked.srtReads[0].value = φ bytes.clear(); require(!ShaderDiskCache::EncodePlan(unblocked, bytes), "a phi without predecessor blocks was encoded"); - auto emptied = handBuiltPlan(); - emptied.valueStorage.push_back(nullptr); - bytes.clear(); - require(!ShaderDiskCache::EncodePlan(emptied, bytes), "a plan with an empty value slot was encoded"); } void verifyPlanRoundTrip() { @@ -1626,7 +2122,7 @@ void verifyPlanRoundTrip() { const auto handBytes = encodedPlan(handBuilt); IrResourcePlan handDecoded; require(ShaderDiskCache::DecodePlan(handBytes, handDecoded), "the hand-built plan does not decode"); - require(encodedPlan(handDecoded) == handBytes && handDecoded.valueStorage.size() == handBuilt.valueStorage.size() && handDecoded.blockStorage.size() == handBuilt.blockStorage.size(), "the hand-built plan changed in a round trip"); + require(encodedPlan(handDecoded) == handBytes && handDecoded.valueStorage.Size() == handBuilt.valueStorage.Size() && handDecoded.blockStorage.size() == handBuilt.blockStorage.size(), "the hand-built plan changed in a round trip"); const auto& phi = *handDecoded.descriptorSources[0].dwords[2]; require(phi.IsPhi() && phi.PhiBlockCount() == 2u && phi.PhiBlock(0)->Id() == 3u && phi.PhiBlock(1)->Id() == 7u && phi.Argument(1)->ImmediateU32() == 0u, "the decoded phi lost its blocks or operands"); require(handDecoded.uniformFill.values[0] == handDecoded.uniformFill.values[1] && handDecoded.uniformFill.values[0] != nullptr && handDecoded.uniformFill.values[2] == nullptr, "the decoded uniform fill lost its shared or null values"); @@ -1652,21 +2148,87 @@ void verifyPlanRoundTrip() { require(ShaderDiskCache::DecodePlan(bytes, decoded), fixture.name + ": the plan does not decode"); require(encodedPlan(decoded) == bytes, fixture.name + ": the plan changed in a round trip"); require(samePlan(*plan, decoded), fixture.name + ": the decoded plan differs from the plan in a field or a value"); - require(decoded.valueStorage.size() == plan->valueStorage.size() && decoded.blockStorage.size() == plan->blockStorage.size(), fixture.name + ": the decoded plan holds another graph"); + require(decoded.valueStorage.Size() == plan->valueStorage.Size() && decoded.blockStorage.size() == plan->blockStorage.size(), fixture.name + ": the decoded plan holds another graph"); const auto succeeded = verifyRoundTripRuntimes(*plan, decoded, fixture, 0x5eedu + static_cast(index), fixture.name); require(succeeded != 0u, fixture.name + ": no runtime materialized"); std::vector key; ShaderDiskCache::BuildPlanKey(request, 32u, key); if (index % 4u == 0u) verifyPlanEntryDamage(key, *plan, fixture.name); - std::cout << "plan round trip: " << fixture.name << ", " << plan->valueStorage.size() << " values, " << ShaderDiskCache::EncodePlanEntry(key, *plan).size() << " bytes per entry, " << succeeded << " of 1000 runtimes materialized\n"; + std::cout << "plan round trip: " << fixture.name << ", " << plan->valueStorage.Size() << " values, " << ShaderDiskCache::EncodePlanEntry(key, *plan).size() << " bytes per entry, " << succeeded << " of 1000 runtimes materialized\n"; ++index; } + auto releaseFixture = fixtures.front(); + releaseFixture.waveSize = 32u; + auto registeredRequest = releaseFixture.Request(true); + const auto registeredHandle = PrepareShader(registeredRequest); + const auto registeredPlan = GetResourcePlan(registeredRequest); + ShaderDiskCache::Flush(); + ReleaseUndrawnResourcePlan(*registeredHandle); + const auto beforeReload = ShaderDiskCache::Totals(); + const auto reloadedPlan = GetResourcePlan(registeredRequest); + require(ShaderDiskCache::Totals().planHits == beforeReload.planHits + 1u, "a released registration plan was not reloaded from the disk cache"); + require(samePlan(*registeredPlan, *reloadedPlan), "a released registration plan changed when reloaded"); + + auto reloadFixture = fixtures.front(); + reloadFixture.waveSize = 32u; + reloadFixture.code.insert(reloadFixture.code.begin(), 0xbf800004u); + const auto reloadRequest = reloadFixture.Request(true); + const auto reloadHandle = PrepareShader(reloadRequest); + static_cast(GetResourcePlan(reloadRequest)); + ShaderDiskCache::Flush(); + std::vector reloadKey; + ShaderDiskCache::BuildPlanKey(reloadRequest, 32u, reloadKey); + const auto reloadPath = ShaderDiskCache::EntryDirectory() / ShaderDiskCache::EntryName(reloadKey); + require(std::filesystem::is_regular_file(reloadPath), "the reload regression plan was not persisted"); + const auto preservedPath = reloadPath.string() + ".reload-preserved"; + std::filesystem::rename(reloadPath, preservedPath); + ReleaseUndrawnResourcePlan(*reloadHandle); + static_cast(GetResourcePlan(reloadRequest)); + auto secondLayout = reloadRequest; + secondLayout.layout.pushConstantSizeBytes = 0u; + ShaderPreparationContext preparation; + static_cast(PrepareShader(secondLayout, &preparation)); + require(preparation.ResourcePrograms() == 1u, "a plan reload retained full IR after an artifact for that layout was already available"); + + auto relocatedFixture = fixtures.front(); + relocatedFixture.waveSize = 32u; + relocatedFixture.code.insert(relocatedFixture.code.begin(), 0xbf800005u); + const auto relocatedRequest = relocatedFixture.Request(true); + const auto relocatedHandle = PrepareShader(relocatedRequest); + static_cast(GetResourcePlan(relocatedRequest)); + ShaderDiskCache::Flush(); + std::vector relocatedKey; + ShaderDiskCache::BuildPlanKey(relocatedRequest, 32u, relocatedKey); + const auto relocatedPath = ShaderDiskCache::EntryDirectory() / ShaderDiskCache::EntryName(relocatedKey); + std::filesystem::rename(relocatedPath, relocatedPath.string() + ".reload-preserved"); + ReleaseUndrawnResourcePlan(*relocatedHandle); + auto drawn = relocatedRequest; + drawn.layout.pushConstantOffsetBytes = 16u; + drawn.layout.pushConstantSizeBytes = 112u; + const auto relocatedInvocation = PreparedShaderInvocation::TryCreate(drawn, relocatedHandle); + require(relocatedInvocation.has_value(), "the prepared artifact refused a relocated push layout"); + RequestMemoryView drawnMemory(drawn.context.memory); + static_cast(relocatedInvocation->Materialize(*relocatedInvocation->Capture(drawnMemory.MakeRuntime(drawn.context.userData, drawn.shader.codeAddress)))); + auto relocatedProbe = relocatedRequest; + relocatedProbe.layout.pushConstantSizeBytes = 0u; + ShaderPreparationContext relocatedPreparation; + static_cast(PrepareShader(relocatedProbe, &relocatedPreparation)); + require(relocatedPreparation.ResourcePrograms() == 1u, "a plan reload retained full IR after the prepared artifact served a relocated push layout"); + + RequestMemoryView registeredMemory(registeredRequest.context.memory); + const auto runtimeCapture = CaptureResources(registeredRequest, registeredMemory.MakeRuntime(registeredRequest.context.userData, registeredRequest.shader.codeAddress), *registeredHandle); + const auto beforeKeep = ShaderDiskCache::Totals(); + ReleaseUndrawnResourcePlan(*registeredHandle); + require(GetResourcePlan(registeredRequest) == runtimeCapture->plan && ShaderDiskCache::Totals().planHits == beforeKeep.planHits, "a runtime-captured plan was released"); const auto covers = [&](const std::function& predicate) { return std::any_of(plans.begin(), plans.end(), [&](const std::shared_ptr& plan) { return predicate(*plan); }); }; - require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.indirectImage.has_value(); }); }), "no fixture plans an indirect image table"); + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.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 std::any_of(plan.valueStorage.begin(), plan.valueStorage.end(), [](const auto& value) { return value.IsPhi(); }); }), "no fixture plans a phi"); require(covers([](const IrResourcePlan& plan) { return !plan.guardedSrtSlots.empty(); }), "no fixture plans a guarded SRT read"); require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.pureFlatSlots.begin(), plan.pureFlatSlots.end(), [](std::uint8_t pure) { return pure != 0u; }); }), "no fixture plans a pure flat slot"); } @@ -1773,8 +2335,12 @@ std::filesystem::path planEntryPath(PlanFixture& fixture) { } int runPlanLoadingProcess(std::string_view mode) { - if (mode == "--load-plan-switch") setEnvironment("APS5_TRACE_BINDLESS", "1"); + if (mode == "--load-plan-switch") setEnvironment("APS5_TRACE_IMAGE_TABLE", "1"); if (mode == "--load-plan-off") setEnvironment("APS5_NO_PERF_DISK_PLANS", "1"); + if (mode == "--load-plan-reuse-off") { + setEnvironment("APS5_NO_TABLE_SNAPSHOT_REUSE", "1"); + setEnvironment("APS5_NO_TABLE_RESOLUTION_REUSE", "1"); + } auto fixtures = planFixtures(); const auto count = fixtures.size(); std::vector> plans; @@ -1790,6 +2356,8 @@ int runPlanLoadingProcess(std::string_view mode) { } if (mode == "--load-plan") { require(builds == 0u && totals.planHits == count && totals.planMisses == 0u && totals.planLoadFailures == 0u && totals.hits == count && totals.misses == 0u, "the second process ran the front end" + counts); + } else if (mode == "--load-plan-reuse-off") { + require(builds == 0u && totals.planHits == count && totals.planMisses == 0u && totals.planLoadFailures == 0u && totals.hits == count && totals.misses == 0u, "turning the image table reuse off changed the stored plan or variant keys" + counts); } else if (mode == "--load-plan-off") { require(builds == count && totals.planHits == 0u && totals.planMisses == 0u && totals.planWrites == 0u && totals.hits == count && totals.misses == 0u, "APS5_NO_PERF_DISK_PLANS=1 used the stored plans or missed the stored variants" + counts); } else { @@ -1809,6 +2377,8 @@ int runPlanLoadingProcess(std::string_view mode) { const auto after = ShaderDiskCache::Totals(); require(FrontEndBuilds() == 1u && after.planHits == count && after.planMisses == 1u, "a changed code word reused a stored plan"); std::cout << "second process prepared " << count << " shaders from stored plans without running the front end\n"; + } else if (mode == "--load-plan-reuse-off") { + std::cout << "with the image table reuse off, a process loaded all " << count << " stored plans and variants with the same output\n"; } else if (mode == "--load-plan-off") { std::cout << "APS5_NO_PERF_DISK_PLANS=1 ran the front end for all " << count << " shaders with the same output\n"; } else { @@ -1852,6 +2422,7 @@ void verifyPlanAcrossProcesses(const char* self) { }; run("--load-plan"); run("--load-plan-off"); + run("--load-plan-reuse-off"); std::vector entry; const auto damaged = planEntryPath(fixtures.front()); require(ReadWholeFile(damaged, entry), "cannot read a stored plan entry"); @@ -1881,7 +2452,44 @@ void verifyDefaultDirectory(const char* self) { int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--housekeeping-safety") { + const auto directory = ShaderDiskCache::EntryDirectory(); + require(!directory.empty(), "the housekeeping fixture cache is disabled"); + ShaderDiskCache::Shutdown(); + require(ShaderDiskCache::Writers() == 0, "the housekeeping writer did not stop"); + std::cout << "housekeeping version " << directory.filename().string() << '\n'; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--resource-plan-codec") { + verifyResultRoundTrip(); + verifyEntryRoundTrip(); + verifyPlanRoundTrip(); + std::cout << "resource plan codec tests passed\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--artifact-codec") { + verifyResultRoundTrip(); + verifyEntryRoundTrip(); + std::cout << "artifact codec tests passed\n"; + return 0; + } +#ifdef _WIN32 + if (argc == 2 && std::string_view(argv[1]) == "--host-stderr") { + verifyHostStandardError(); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--long-host-paths") { + verifyLongHostPaths(); + return 0; + } +#endif + if (argc == 3 && (std::string_view(argv[1]) == "--seed-prebuilt-discovery" || std::string_view(argv[1]) == "--verify-prebuilt-discovery")) { + verifyPrebuiltDiscovery(argv[2], std::string_view(argv[1]) == "--seed-prebuilt-discovery"); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--large-file") return runLargeFileWrite(); if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess(); + if (argc == 2 && std::string_view(argv[1]) == "--load-prebuilt") return runPrebuiltLoadingProcess(); if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo(); if (argc == 2 && std::string_view(argv[1]).starts_with("--load-plan")) return runPlanLoadingProcess(argv[1]); const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-test-" + std::to_string(std::random_device{}())); @@ -1892,14 +2500,17 @@ int main(int argc, char** argv) { verifyAtomicReplacement(directory); verifyResultRoundTrip(); verifyEntryRoundTrip(); + verifyPrebuiltPack(directory / "packs"); verifyArtifactStorageIsolation(); verifyKeySensitivity(); + verifyMeshBarycentricRequestSerialization(); verifyFloatModeSerialization(); verifyPlanRoundTrip(); verifyPlanKey(); verifyStore(); verifyPersist(); verifyAcrossProcesses(argv[0]); + verifyPrebuiltAcrossProcesses(argv[0], directory); verifyPlanAcrossProcesses(argv[0]); verifyEmissionFailureMemo(); verifyFailureMemoSwitch(argv[0]); @@ -1910,9 +2521,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 9021647520..8d8b12086e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1,4 +1,8 @@ #include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include "ControlFlow/RequestSerializer.hpp" #include "Optimization/DescriptorBindingBuilder.hpp" #include "Optimization/RequestMemoryView.hpp" @@ -10,16 +14,21 @@ #include "Optimization/SrtWalker/SrtEvaluator.hpp" #include "Optimization/SrtWalker/SrtInstructionPredicates.hpp" #include "Optimization/SrtWalker/SrtFlatSlotClasses.hpp" +#include "Optimization/SrtWalker/SrtTape.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include "Optimization/SrtWalker.hpp" #include "IntermediateRepresentation/IrBuilder.hpp" #include "IntermediateRepresentation/IrProgram.hpp" #include "SpirvBackend/SpirvAnalysis.hpp" +#include "SavedExecMaskLoop.hpp" #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" #endif #include "CacheKey.hpp" #include "CompiledVariant.hpp" #include "BdaAbi.hpp" +#include "ImageTableAbi.hpp" +#include "ShaderDiskCache.hpp" #include #include #include @@ -31,8 +40,13 @@ #include #include #include +#include #include #include +#include +#include +#include +#include #include #include #include @@ -52,6 +66,34 @@ 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 new(std::size_t bytes, const std::nothrow_t&) noexcept { + try { + return ::operator new(bytes); + } catch (...) { + return nullptr; + } +} + +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"}; @@ -193,7 +235,7 @@ void verifyFrontendProgram(const ShaderRecompiler::IrProgram& reused, const Shad for (std::size_t index = 0; index < left.descriptorSources.size(); ++index) { const auto& a = left.descriptorSources[index]; const auto& b = right.descriptorSources[index]; - require(a.dwordCount == b.dwordCount && a.indirectImage == b.indirectImage, "frontend pair: descriptor planning changed"); + require(a.dwordCount == b.dwordCount && a.tableColumn == b.tableColumn, "frontend pair: descriptor planning changed"); for (std::size_t word = 0; word < a.dwords.size(); ++word) { require((a.dwords[word] == nullptr) == (b.dwords[word] == nullptr), "frontend pair: descriptor word presence changed"); if (a.dwords[word] != nullptr) require(a.dwords[word] != b.dwords[word] && a.dwords[word]->Id() == b.dwords[word]->Id(), "frontend pair: descriptor values were shared or changed"); @@ -519,7 +561,116 @@ void verifySignedMinimum() { SrtRuntime runtime{data}; Detail::Evaluator evaluator(plan, runtime); std::uint32_t result = 0; - require(evaluator.Evaluate(&minimum, result) && result == expected, "SRT signed minimum used unsigned ordering or failed evaluation"); + require(evaluator.Evaluate(Detail::IrNode(&minimum), result) && result == expected, "SRT signed minimum used unsigned ordering or failed evaluation"); + } +} + +void verifyMayEvaluate() { + using namespace ShaderRecompiler; + IrResourcePlan plan; + std::vector> values; + const auto make = [&](IrOpcode opcode, IrType type, std::initializer_list arguments = {}) -> IrValue& { + values.push_back(std::make_unique(opcode, type, static_cast(values.size()))); + for (auto* argument : arguments) values.back()->AddArgument(argument); + return *values.back(); + }; + const auto constant = [&](std::uint32_t value) -> IrValue& { + auto& leaf = make(IrOpcode::Void, IrType::U32); + leaf.SetImmediateU32(value); + return leaf; + }; + auto& scalar = make(IrOpcode::Void, IrType::ScalarReg); + scalar.SetRegister({RegisterBank::Scalar, 0}); + auto& vector = make(IrOpcode::Void, IrType::VectorReg); + vector.SetRegister({RegisterBank::Vector, 0}); + auto& input = make(IrOpcode::GetUserData, IrType::U32, {&scalar}); + auto& lane = make(IrOpcode::GetRegister, IrType::U32, {&vector}); + auto& uniform = make(IrOpcode::IEqual32, IrType::Bool, {&input, &constant(4)}); + auto& varying = make(IrOpcode::IEqual32, IrType::Bool, {&input, &lane}); + auto& select = make(IrOpcode::SelectU32, IrType::U32, {&uniform, &constant(1), &lane}); + auto& unevaluated = make(IrOpcode::FPAdd32, IrType::F32, {&input, &input}); + auto& invariant = make(IrOpcode::Phi, IrType::Bool); + invariant.AddArgument(&uniform); + invariant.AddArgument(&uniform); + auto& divergent = make(IrOpcode::Phi, IrType::Bool); + divergent.AddArgument(&uniform); + divergent.AddArgument(&varying); + auto& counter = make(IrOpcode::Phi, IrType::U32); + auto& next = make(IrOpcode::CompositeExtractU32x2, IrType::U32, {&make(IrOpcode::IAddCarry32, IrType::U32x2, {&counter, &constant(1)}), &constant(0)}); + counter.AddArgument(&constant(0)); + 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}); + auto& conjMask = make(IrOpcode::LogicalAnd, IrType::Bool, {&varying, &uniform}); + auto& firstActiveConjunct = make(IrOpcode::ReadFirstLane, IrType::U32, {&make(IrOpcode::SelectU32, IrType::U32, {&varying, &input, &lane}), &conjMask}); + auto& otherCond = make(IrOpcode::IEqual32, IrType::Bool, {&lane, &constant(5)}); + auto& firstNotImplied = make(IrOpcode::ReadFirstLane, IrType::U32, {&make(IrOpcode::SelectU32, IrType::U32, {&otherCond, &input, &lane}), &conjMask}); + plan.srtReads = {{&input, 0u}, {&lane, 4u}, {nullptr, 8u}}; + auto& constInput = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(0)}); + auto& constLane = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(1)}); + 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, 23>{{ + {&constant(3), true, "a constant"}, + {&input, true, "user data"}, + {&uniform, true, "a comparison of user data"}, + {&lane, false, "a vector register read"}, + {&varying, false, "a comparison with a vector register"}, + {&select, false, "a select with a vector register arm"}, + {&unevaluated, false, "an opcode the evaluator does not evaluate"}, + {&invariant, true, "a phi whose operands agree"}, + {&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"}, + {&firstActiveConjunct, true, "a first-lane read of a select whose condition is implied by a conjunct mask"}, + {&firstNotImplied, false, "a first-lane read of a select whose condition is not implied by the mask"}, + {&constInput, true, "a constant read whose slot holds user data"}, + {&constLane, false, "a constant read whose slot holds a vector register"}, + {&constOutside, false, "a constant read past the last slot"}, + {&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()); + } + require(SrtWalker().ValidateRuntimeValue(plan, &firstActiveConjunct), "ValidateRuntimeValue rejects select on a conjunct of active mask"); + require(!SrtWalker().ValidateRuntimeValue(plan, &firstNotImplied), "ValidateRuntimeValue accepts select not implied by active mask"); + auto& countBits = make(IrOpcode::BitCount32, IrType::U32, {&input}); + auto& findILsb = make(IrOpcode::FindILsb32, IrType::U32, {&input}); + auto& findUMsb = make(IrOpcode::FindUMsb32, IrType::U32, {&input}); + Detail::Evaluator bitEvaluator(plan, runtime); + std::uint32_t bitResult = 0; + require(bitEvaluator.Evaluate(Detail::IrNode(&countBits), bitResult) && bitResult == 1u, "Evaluator evaluated BitCount32 incorrectly"); + require(bitEvaluator.Evaluate(Detail::IrNode(&findILsb), bitResult) && bitResult == 2u, "Evaluator evaluated FindILsb32 incorrectly"); + require(bitEvaluator.Evaluate(Detail::IrNode(&findUMsb), bitResult) && bitResult == 2u, "Evaluator evaluated FindUMsb32 incorrectly"); + const std::array takenAsEvaluable{IrOpcode::Identity, IrOpcode::Phi, IrOpcode::Void, IrOpcode::GetUserData, IrOpcode::GetShaderBase, IrOpcode::LoadAddressU32, IrOpcode::ReadConstBuffer}; + for (std::uint16_t raw = 0; raw < static_cast(IrOpcode::Count); ++raw) { + const auto opcode = static_cast(raw); + if (std::ranges::find(takenAsEvaluable, opcode) != takenAsEvaluable.end()) continue; + auto& value = make(opcode, IrType::U32, {&constant(0), &constant(0), &constant(0), &constant(0)}); + Detail::Evaluator evaluator(plan, runtime); + std::uint32_t result = 0; + require(Detail::Evaluator::MayEvaluate(plan, &value) == evaluator.Evaluate(Detail::IrNode(&value), result), (std::string("MayEvaluate disagreed with the evaluator on ") + std::string(IrOpcodeName(opcode)) + " over constants").c_str()); } } @@ -607,6 +758,53 @@ void verifySignedSrtComparison() { } } +void verifyTypeZeroImageMaterialization() { + using namespace ShaderRecompiler; + 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 | (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)); + + 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 or 1 atomic image descriptor was accepted without hardware evidence" : "TYPE=0 or 1 writable image descriptor was accepted without hardware evidence"); + } + } +} + void verifyOutOfRangeConstantReads() { std::vector code{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++) { @@ -654,7 +852,8 @@ void verifyOutOfRangeConstantReads() { require(plan->info.images.size() == 2u && plan->info.samplers.size() == 1u, "out-of-range s_buffer_load: the two T#s were not planned as one image each"); AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(request); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; static_cast(Recompile(request, *capture)); const auto holds = [&](const std::array& words) { return std::ranges::any_of(capture->snapshot.images, [&](const DescriptorValue& value) { @@ -737,9 +936,9 @@ void verifyPureFlatSlots() { plan.uniformFill.values[0] = &readConst(3); require(Detail::ComputePureFlatSlots(plan) == Pure{1, 0, 0, 0}, "pure flat slots: a uniform-fill value was not excluded"); plan.uniformFill = {}; - plan.descriptorSources[0].indirectImage = DescriptorSource::IndirectImage{}; - require(Detail::ComputePureFlatSlots(plan) == Pure{0, 0, 0, 0}, "pure flat slots: an indirect image did not disqualify the plan"); - plan.descriptorSources[0].indirectImage.reset(); + plan.descriptorSources[0].tableColumn = TableColumn{}; + require(Detail::ComputePureFlatSlots(plan) == Pure{0, 0, 0, 0}, "pure flat slots: a table column did not disqualify the plan"); + plan.descriptorSources[0].tableColumn.reset(); plan.requiresSpecializationMemory = true; require(Detail::ComputePureFlatSlots(plan) == Pure{0, 0, 0, 0}, "pure flat slots: specialization memory did not disqualify the plan"); plan.requiresSpecializationMemory = false; @@ -755,7 +954,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, 0u, 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())); @@ -801,6 +1000,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({}); @@ -821,8 +1028,10 @@ void verifyGuardedDescriptorSources() { fill(valid, mode); AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(storeRequest); - storeRequest.context.memory = memory.Regions(); - return Recompile(storeRequest, *capture); + const auto memoryRegions = memory.Regions(); + auto compileRequest = storeRequest; + compileRequest.context.memory = memoryRegions; + return Recompile(compileRequest, *capture); }; const auto storePlan = GetResourcePlan(storeRequest); require(storePlan->info.images.size() == 2u && storePlan->descriptorSources.size() == 1u, "guarded images: the two stores were not planned as two images of one T#"); @@ -830,69 +1039,83 @@ 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"); } -// A compute program sampling a T# loaded from a table buffer at a runtime key (a bindless image -// table): mode M enumerates the keys from the material records, mode T binds the whole table. -void verifyBindlessTable() { +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; - const std::uint32_t slots = ResourceMaterializer::BindlessSlots(); - struct alignas(256) Texture { std::array bytes{}; }; - static Texture textures[2]; - struct alignas(4096) GuestTables { - std::array, 4> heap{}; - std::array, 3> materials{}; - std::array output{}; - std::array srt{}; - }; - static GuestTables guest; - auto& heap = guest.heap; - auto& materials = guest.materials; - auto& output = guest.output; - auto& srt = guest.srt; - const auto makeTexture = [&](std::uint32_t entry, const Texture& texture) { - const auto base = static_cast(reinterpret_cast(texture.bytes.data())); - heap[entry] = {static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; - }; - makeTexture(0, textures[0]); - makeTexture(1, textures[1]); - heap[3] = heap[0]; - materials = {{{0u, 1u, 0u, 0u}, {0u, 0u, 0u, 0u}, {0u, 3u, 0u, 0u}}}; - const auto bufferDescriptor = [](const void* base, std::uint32_t stride, std::uint32_t records) { - const auto address = static_cast(reinterpret_cast(base)); - return std::array{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), records, 0xfacu}; - }; - const auto fillSrt = [&](std::uint32_t heapRecords) { - const auto heapV = bufferDescriptor(heap.data(), 32u, heapRecords); - const auto materialV = bufferDescriptor(materials.data(), 16u, 3u); - const auto outputV = bufferDescriptor(output.data(), 0u, 16u); - std::copy(heapV.begin(), heapV.end(), srt.begin()); - srt[4] = 0u; srt[5] = 0u; srt[6] = 0u; srt[7] = 0u; - std::copy(materialV.begin(), materialV.end(), srt.begin() + 8); - std::copy(outputV.begin(), outputV.end(), srt.begin() + 12); - }; - fillSrt(4u); - - // s_load_dwordx4 x4 (heap V#, S#, material V#, output V#); v_readfirstlane_b32 s16, v0; - // s_mul_i32 s16, s16, 16; s_buffer_load_dword s16, s[12:15], s16 offset:4; s_lshl_b32 s16, s16, 5; - // s_buffer_load_dwordx8 s[20:27], s[4:7], s16; image_sample_lz v[0:3], v[0:1], s[20:27], s[8:11]; - // buffer_store_dword v0, off, s[28:31], 0; s_endpgm. - const std::vector materialCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x93109010u, 0xf4200406u, 0x20000004u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; - // The same without the material read: the key is the wave's first lane id. - const std::vector wholeCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + 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"}; - // A wave64 workgroup on a 32-wide host is held by one subgroup (two lanes per invocation). - const auto makeRequest = [&](std::span code) { + const auto requestFor = [&](std::uint64_t address, const std::vector& code) { RecompileRequest request{}; - request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; - request.context.waveSize = 64; + request.shader = {ShaderStage::Compute, address, 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.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 32; @@ -903,204 +1126,2465 @@ void verifyBindlessTable() { request.layout.pushConstantSizeBytes = 128; return request; }; - const auto covered = [](const std::vector& regions, const void* pointer, std::size_t bytes) { - auto address = static_cast(reinterpret_cast(pointer)); - const auto end = address + bytes; - while (address < end) { - const auto region = std::find_if(regions.begin(), regions.end(), [&](const MemoryRegion& candidate) { return address >= candidate.guestAddress && address < candidate.guestAddress + candidate.bytes.size(); }); - if (region == regions.end()) return false; - address = region->guestAddress + region->bytes.size(); + 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{}; +}; + +std::array TableT(const void* base, std::uint32_t format, std::uint32_t type, std::uint32_t height = 3u, std::uint32_t swizzleMode = 0u) { + const auto address = static_cast(reinterpret_cast(base)); + return {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (format << 20u) | (3u << 30u), height << 14u, 0xfacu | (swizzleMode << 20u) | (type << 28u), 0u, 0u, 0u, 0u}; +} + +std::array TableV(const void* base, std::uint32_t stride, std::uint32_t records) { + const auto address = static_cast(reinterpret_cast(base)); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), records, stride == 0u ? 0x31016facu : 0x01016facu}; +} + +bool TableCovered(const std::vector& regions, const void* pointer, std::size_t bytes) { + auto address = static_cast(reinterpret_cast(pointer)); + const auto end = address + bytes; + while (address < end) { + const auto region = std::find_if(regions.begin(), regions.end(), [&](const ShaderRecompiler::MemoryRegion& candidate) { return address >= candidate.guestAddress && address < candidate.guestAddress + candidate.bytes.size(); }); + if (region == regions.end()) return false; + address = region->guestAddress + region->bytes.size(); + } + return true; +} + +const ShaderRecompiler::DescriptorBinding* TableBinding(const ShaderRecompiler::RecompileResult& result, ShaderRecompiler::DescriptorRole role) { + const auto found = std::find_if(result.bindings.begin(), result.bindings.end(), [&](const ShaderRecompiler::DescriptorBinding& binding) { return binding.role == role; }); + return found == result.bindings.end() ? nullptr : &*found; +} + +const std::vector& TableMap(const ShaderRecompiler::RecompileResult& result) { + const auto* binding = TableBinding(result, ShaderRecompiler::DescriptorRole::ImageTableMap); + require(binding != nullptr && binding->guestDescriptor.size() >= ShaderRecompiler::ImageTableAbi::HeaderWords, "image table: the result has no map"); + return binding->guestDescriptor; +} + +std::vector TableCodes(const std::vector& map, std::uint32_t table) { + namespace Abi = ShaderRecompiler::ImageTableAbi; + const auto header = Abi::TableHeader(table); + const auto start = map.at(header + Abi::TableMapStart); + const auto keys = map.at(header + Abi::TableKeyCount); + require(static_cast(start) + keys <= map.size(), "image table: the map is truncated"); + return std::vector(map.begin() + start, map.begin() + start + keys); +} + +struct TableSpirvScan { + bool runtimeArray = false; + bool dynamicIndexing = false; + bool shaderNonUniform = false; + bool nonUniform = false; + bool switched = false; +}; + +TableSpirvScan ScanTableSpirv(std::span words) { + TableSpirvScan result; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + require(count != 0 && count <= words.size() - cursor, "image table: truncated SPIR-V instruction"); + const auto op = words[cursor] & 0xffffu; + if (op == spv::OpCapability && words[cursor + 1] == spv::CapabilityRuntimeDescriptorArray) result.runtimeArray = true; + if (op == spv::OpCapability && words[cursor + 1] == spv::CapabilitySampledImageArrayDynamicIndexing) result.dynamicIndexing = true; + if (op == spv::OpCapability && words[cursor + 1] == spv::CapabilityShaderNonUniform) result.shaderNonUniform = true; + if (op == spv::OpDecorate && words[cursor + 2] == spv::DecorationNonUniform) result.nonUniform = true; + if (op == spv::OpSwitch) result.switched = true; + cursor += count; + } + return result; +} + +struct TableGuest { + std::array, 128> heap{}; + std::array, 3> materials{}; + std::array output{}; + 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; +constexpr std::uint32_t TableFormat11_11_10UInt = 34; +constexpr std::uint32_t TableType2D = 9; +constexpr std::uint32_t TableType3D = 10; + +const std::vector TableMaterialCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x93109010u, 0xf4200406u, 0x20000004u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; +const std::vector TableSrtKeyCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0xf4000400u, 0xfa000040u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; +const std::vector TableTwoKeyCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0xf4000400u, 0xfa000040u, 0xf4000440u, 0xfa000044u, 0x8f108510u, 0x8f118511u, 0xf42c0502u, 0x20000000u, 0xf42c0902u, 0x22000000u, 0xf09c0f08u, 0x00450000u, 0xf09c0f08u, 0x00490400u, 0x06000900u, 0xe0700000u, 0x80070000u, 0xbf810000u}; +const std::vector TableMixedKeyCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0xf4000400u, 0xfa000040u, 0x34020084u, 0xbf8cc07fu, 0xe0301004u, 0x80030101u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xbf8c3f70u, 0x7e220501u, 0x8f118511u, 0xf42c0902u, 0x22000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00450000u, 0xf09c0f08u, 0x00490400u, 0xbf8c3f70u, 0x06000900u, 0xe0700000u, 0x80070000u, 0xbf810000u}; +const std::vector TableLateKeyCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0xf4000400u, 0xfa000040u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xf4000440u, 0xfa000044u, 0x8f118511u, 0xf42c0902u, 0x22000000u, 0xf09c0f08u, 0x00490400u, 0x06000900u, 0xe0700000u, 0x80070000u, 0xbf810000u}; +const std::vector TableMixedKeyFirstCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0xf4000400u, 0xfa000040u, 0x34020084u, 0xbf8cc07fu, 0xe0301004u, 0x80030101u, 0xbf8c3f70u, 0x7e220501u, 0x8f118511u, 0xf42c0902u, 0x22000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00490400u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00450000u, 0xbf8c3f70u, 0x06000900u, 0xe0700000u, 0x80070000u, 0xbf810000u}; +const std::vector TableWholeCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + +std::vector TableLoadCode(std::vector code) { + const auto sample = std::find(code.begin(), code.end(), 0xf09c0f08u); + require(sample != code.end() && sample + 1 != code.end(), "image table: the program has no image_sample_lz"); + sample[0] = 0xf0000f08u; + sample[1] = 0x00050000u; + return code; +} + +ShaderRecompiler::RecompileRequest TableRequest(const std::vector& code, const std::array& userData, std::uint32_t subgroupSize = 32u) { + using namespace ShaderRecompiler; + static constexpr std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + static constexpr std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + std::uint64_t hash = 0xcbf29ce484222325ull; + for (const auto word : code) hash = (hash ^ word) * 0x100000001b3ull; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x100000u + (hash & 0xffffu) * 0x100u, code, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = subgroupSize; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + return request; +} + +void FillTableSrt(TableGuest& guest, const void* heap, std::uint32_t stride, std::uint32_t records) { + const auto heapV = TableV(heap, stride, records); + const auto materialV = TableV(guest.materials.data(), 16u, 3u); + const auto outputV = TableV(guest.output.data(), 0u, 16u); + std::copy(heapV.begin(), heapV.end(), guest.srt.begin()); + std::fill(guest.srt.begin() + 4, guest.srt.begin() + 8, 0u); + std::copy(materialV.begin(), materialV.end(), guest.srt.begin() + 8); + std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 12); +} + +std::array TableUserData(const TableGuest& guest) { + const auto address = static_cast(reinterpret_cast(guest.srt.data())); + return {static_cast(address), static_cast(address >> 32u)}; +} + +struct TableCompiled { + std::shared_ptr capture; + std::shared_ptr result; + std::vector regions; + std::vector key; +}; + +TableCompiled CompileTable(ShaderRecompiler::RecompileRequest request) { + TableCompiled out; + AgcDriver::ShaderMemory memory({}); + out.capture = memory.Capture(request); + out.regions = memory.Regions(); + request.context.memory = out.regions; + out.result = Recompile(request, *out.capture); + ShaderRecompiler::ShaderDiskCache::BuildKey(request, request.target.subgroupSize, out.key); + 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 column.words.at(column.codes[0]).dwords; +} + +void verifyImageTableKeys() { + 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> large{}; + 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); + large[0] = TableT(textures[2].bytes.data(), TableFormat16SInt, TableType2D); + large[1] = TableT(textures[2].bytes.data(), TableFormat11_11_10UInt, TableType2D); + large[2] = TableT(textures[3].bytes.data(), TableFormat32UInt, TableType2D, 3u, 4u); + large[3] = TableT(textures[3].bytes.data(), TableFormat8888UNorm, TableType3D); + large[4] = {0x1234u, 0x5678u, 0x9abcu, 0x1000ffacu, 0u, 0u, 0u, 0u}; + large[5] = TableT(textures[3].bytes.data(), 0u, TableType2D); + for (std::uint32_t entry = 6; entry < Entries; entry++) large[entry] = TableT(textures[entry % 4u].bytes.data(), TableFormat8888UNorm, TableType2D, entry); + const auto userData = TableUserData(guest); + for (const auto& code : {TableMaterialCode, TableWholeCode, TableLoadCode(TableMaterialCode), TableLoadCode(TableWholeCode)}) { + const auto request = TableRequest(code, userData); + guest.materials = {{{0u, 1u, 0u, 0u}, {0u, 0u, 0u, 0u}, {0u, 2u, 0u, 0u}}}; + FillTableSrt(guest, guest.heap.data(), 32u, 3u); + const auto first = CompileTable(request); + guest.materials = {{{0u, 3u, 0u, 0u}, {0u, 4u, 0u, 0u}, {0u, 2999u, 0u, 0u}}}; + FillTableSrt(guest, large.data(), 32u, Entries); + const auto second = CompileTable(request); + const auto& a = *first.result; + const auto& b = *second.result; + require(a.variantId == b.variantId, "image table keys: the table contents changed the compiled artifact"); + require(a.PipelineVariantId() == b.PipelineVariantId() && a.specializationId == b.specializationId && a.specialization == b.specialization, "image table keys: the table contents changed the specialization"); + require(a.spirv == b.spirv, "image table keys: the table contents changed the module"); + require(first.key == second.key, "image table keys: the table contents changed the cache key"); + require(TableMap(a) != TableMap(b), "image table keys: the maps do not differ"); + const auto* tableA = TableBinding(a, DescriptorRole::ImageTable); + const auto* tableB = TableBinding(b, DescriptorRole::ImageTable); + require(tableA != nullptr && tableB != nullptr && tableA->binding == tableB->binding && tableA->guestDescriptor != tableB->guestDescriptor, "image table keys: the image table bindings are missing or equal"); + require(a.bindings.size() == b.bindings.size(), "image table keys: the table contents changed the binding list"); + for (std::size_t index = 0; index < a.bindings.size(); index++) { + require(a.bindings[index].role == b.bindings[index].role && a.bindings[index].binding == b.bindings[index].binding && a.bindings[index].kind == b.bindings[index].kind, "image table keys: the table contents changed a binding"); + } + const auto& mapB = TableMap(b); + require(mapB[Abi::HeaderTableCount] == 1u && mapB[Abi::TableHeader(0) + Abi::TableKeyCount] == Entries, "image table keys: the large table was not snapshotted whole"); + if (code.size() == TableWholeCode.size()) require(mapB[Abi::HeaderElementCount] > 2000u, "image table keys: the large table's entries were not bound"); +#if ANYPS5_ENABLE_SPIRV_TOOLS + static_cast(ValidateAndOptimizeSpirv(a.spirv, request.target.vulkanVersion, request.target.spirvVersion)); +#endif + } +} + +struct CountingMemory { + std::uint64_t begin = 0; + std::uint64_t end = 0; + std::uint64_t unmapped = 0; + std::uint64_t wordReads = 0; + std::uint64_t bulkCalls = 0; + std::uint64_t bulkWords = 0; +}; + +bool CountingAccessible(void* context, std::uint64_t address, std::uint64_t bytes) { + const auto& memory = *static_cast(context); + return memory.unmapped == 0u || address >= memory.unmapped + 4096u || address + bytes <= memory.unmapped; +} + +bool CountingRead(void* context, std::uint64_t address, std::uint32_t* value) { + auto& memory = *static_cast(context); + if (!CountingAccessible(context, address, sizeof(*value))) return false; + if (address >= memory.begin && address < memory.end) memory.wordReads++; + std::memcpy(value, reinterpret_cast(static_cast(address)), sizeof(*value)); + return true; +} + +bool CountingReadWords(void* context, std::uint64_t address, std::uint32_t* values, std::uint64_t count) { + auto& memory = *static_cast(context); + if (!CountingAccessible(context, address, count * sizeof(std::uint32_t))) return false; + memory.bulkCalls++; + memory.bulkWords += count; + std::memcpy(values, reinterpret_cast(static_cast(address)), static_cast(count) * sizeof(std::uint32_t)); + return true; +} + +void verifyImageTableBulkReads() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + constexpr std::uint32_t Records = 128; + static TableTexture textures[3]; + alignas(4096) static TableGuest guest; + alignas(4096) static std::array words{}; + const auto capture = [&](const RecompileRequest& request, 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 place = [&](std::uint32_t firstWord, std::uint32_t records) { + std::fill(words.begin(), words.end(), 0u); + for (std::uint32_t record = 0; record < records; record++) { + const auto image = TableT(textures[record % 3u].bytes.data(), TableFormat8888UNorm, TableType2D, 1u + record % 5u); + std::copy(image.begin(), image.end(), words.begin() + firstWord + record * 8u); + } + FillTableSrt(guest, words.data() + firstWord, 32u, records); + }; + const auto userData = TableUserData(guest); + const auto request = TableRequest(TableWholeCode, userData); + place(512u, Records); + CountingMemory byWordReads{reinterpret_cast(words.data() + 512u), reinterpret_cast(words.data() + 512u + Records * 8u)}; + CountingMemory byRunReads = byWordReads; + const auto byWord = capture(request, byWordReads, false); + const auto byRun = capture(request, byRunReads, true); + require(byWordReads.wordReads == Records * 8u && byWordReads.bulkCalls == 0u, "image table bulk reads: without a bulk reader the records were not read word by word once"); + require(byRunReads.wordReads == 0u && byRunReads.bulkCalls == 1u && byRunReads.bulkWords == Records * 8u, "image table bulk reads: the contiguous records across a page boundary were not read in one run"); + require(byRun->snapshot.tables == byWord->snapshot.tables && byRun->snapshot.tables.tables.at(0).keys == Records, "image table bulk reads: reading in runs changed the snapshot"); + AgcDriver::ShaderMemory memory({}); + const auto captured = memory.Capture(request); + require(captured->snapshot.tables == byWord->snapshot.tables && TableCovered(memory.Regions(), words.data() + 512u, Records * 32u), "image table bulk reads: the shader memory's run reads changed the snapshot or its captured regions"); + + place(1012u, 4u); + CountingMemory cutWordReads{reinterpret_cast(words.data() + 1012u), reinterpret_cast(words.data() + 1044u), reinterpret_cast(words.data() + 1024u)}; + CountingMemory cutRunReads = cutWordReads; + const auto cutByWord = capture(request, cutWordReads, false); + const auto cutByRun = capture(request, cutRunReads, true); + const auto unmapped = Abi::PoisonCode(static_cast(Abi::PoisonReason::Unmapped)); + const auto& cut = cutByRun->snapshot.tables.tables.at(0); + require(cut.keys == 4u && (cut.codes[0] & Abi::PoisonFlag) == 0u && cut.codes[1] == unmapped && cut.codes[2] == unmapped && cut.codes[3] == unmapped, "image table bulk reads: a record crossing into an unmapped page was not Unmapped poison"); + 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"); + + place(1028u, 4u); + CountingMemory insideWordReads{reinterpret_cast(words.data() + 1028u), reinterpret_cast(words.data() + 1060u)}; + const auto insideByWord = capture(request, insideWordReads, false); + AgcDriver::ShaderMemory inside({}); + const auto insideCaptured = inside.Capture(request); + const auto regions = inside.Regions(); + const auto first = static_cast(reinterpret_cast(words.data())); + const auto past = first + words.size() * sizeof(std::uint32_t); + const auto tableFirst = first + 1028u * sizeof(std::uint32_t); + const auto tablePast = first + 1060u * sizeof(std::uint32_t); + const bool exact = std::all_of(regions.begin(), regions.end(), [&](const ShaderRecompiler::MemoryRegion& region) { return region.guestAddress + region.bytes.size() <= first || region.guestAddress >= past || (region.guestAddress >= tableFirst && region.guestAddress + region.bytes.size() <= tablePast); }); + require(insideCaptured->snapshot.tables == insideByWord->snapshot.tables && TableCovered(regions, words.data() + 1028u, 32u * sizeof(std::uint32_t)) && exact, "image table bulk reads: run reads inside 16-word blocks changed the snapshot or captured words outside the records"); +} + +std::uint64_t pendingTableWord = 0; +std::uint32_t pendingTableValue = 0; +bool pendingTableKnown = false; + +AgcDriver::ShaderMemory::PendingWrite PendingTableWrite(std::uint64_t address, std::size_t bytes, std::span known) { + using Pending = AgcDriver::ShaderMemory::PendingWrite; + if (pendingTableWord < address || pendingTableWord - address >= bytes) return Pending::None; + if (!pendingTableKnown) return Pending::Sync; + if (known.size() == sizeof(pendingTableValue)) std::memcpy(known.data(), &pendingTableValue, sizeof(pendingTableValue)); + return Pending::KnownValue; +} + +std::uint64_t deferredTableRecord = 0; +std::array deferredTableWords{}; +bool deferredTableStored = false; + +void StoreDeferredTableRecord(std::uint64_t address, std::size_t bytes) { + if (deferredTableStored || deferredTableRecord < address || deferredTableRecord - address >= bytes) return; + deferredTableStored = true; + AgcDriver::GuestMemory::StoreOwnBytes(deferredTableRecord, std::as_bytes(std::span(deferredTableWords))); +} + +std::uint32_t* AllocateWatchedTable(std::size_t bytes) { + auto* heap = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536)); + require(heap != nullptr, "table snapshot reuse: cannot allocate the table in the guest arena"); + GuestArena::GuestArenaCommit_nid_postfix(heap, bytes, 0x04u, bytes); +#ifndef _WIN32 + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(heap, bytes); +#endif + return heap; +} + +void ProtectTablePage(std::uintptr_t page, std::uint32_t protection) { + GuestArena::GuestArenaSetProtection_nid_postfix(page, 4096u, protection); +#ifdef _WIN32 + DWORD previous = 0; + require(VirtualProtect(reinterpret_cast(page), 4096u, protection, &previous) != 0, "table snapshot reuse: cannot protect a table page"); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(page, 4096u); +} + +void verifyTableSnapshotReuse() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + constexpr std::uint32_t Records = 4096; + constexpr std::size_t Bytes = Records * 32u; + const auto texture = [](std::uint32_t index) { return TableT(reinterpret_cast(static_cast(0x100000000ull + index * 256ull)), TableFormat8888UNorm, TableType2D, 1u + index % 5u); }; + const auto fill = [&](std::uint32_t* table, std::uint32_t records) { + for (std::uint32_t record = 0; record < records; record++) { + const auto words = texture(record % 1024u); + std::copy(words.begin(), words.end(), table + static_cast(record) * 8u); + } + }; + struct Outcome { + ImageTableSnapshot tables; + bool covered = false; + ImageTableReuseCounts reuse; + AgcDriver::ShaderMemory::SupplyCounts supplied{}; + const std::byte* tableBytes = nullptr; + std::size_t tableRegionBytes = 0; + std::vector> tableRegions; + }; + const auto sortedDisjoint = [](const std::vector& regions) { + for (std::size_t index = 1; index < regions.size(); index++) { + if (regions[index - 1].guestAddress + regions[index - 1].bytes.size() > regions[index].guestAddress) return false; } return true; }; - const auto mappingOf = [&](const ResourceSnapshot& snapshot) { - require(snapshot.flattenedSrt.size() >= 1u + 2u * slots, "bindless: the mapping block is missing from the flattened SRT"); - return std::vector(snapshot.flattenedSrt.end() - static_cast(1u + 2u * slots), snapshot.flattenedSrt.end()); - }; - const auto tableRoot = [&](const ResourceCapture& capture, std::uint32_t direct) { - require(capture.snapshot.images.size() == direct + slots - 1u, "bindless: the snapshot does not hold the table slots"); - std::uint32_t root = ImageResource::NoIndirectImage; - for (std::uint32_t i = 0; i < direct; i++) { - if (capture.plan->descriptorSources.at(capture.plan->info.images.at(i).source).indirectImage.has_value()) root = i; + const auto holds = [](const std::vector& regions, const std::uint32_t* table, std::size_t bytes, const std::uint32_t* expected) { + const auto begin = static_cast(reinterpret_cast(table)); + std::size_t found = 0; + for (const auto& region : regions) { + const auto from = std::max(region.guestAddress, begin); + const auto to = std::min(region.guestAddress + region.bytes.size(), begin + bytes); + if (from >= to) continue; + if (std::memcmp(region.bytes.data() + (from - region.guestAddress), reinterpret_cast(expected) + (from - begin), static_cast(to - from)) != 0) return false; + found += static_cast(to - from); } - require(root != ImageResource::NoIndirectImage, "bindless: no table root"); - return root; + return found == bytes; + }; + const auto capture = [&](const RecompileRequest& request, const std::uint32_t* table, std::size_t bytes, AgcDriver::ShaderMemory::PendingWriteQuery pending) { + const auto before = ResourceMaterializer::TableReuse(); + const auto suppliedBefore = AgcDriver::ShaderMemory::Supplies(); + Outcome outcome; + AgcDriver::ShaderMemory memory({}, pending); + outcome.tables = memory.Capture(request)->snapshot.tables; + const auto regions = memory.Regions(); + const auto suppliedAfter = AgcDriver::ShaderMemory::Supplies(); + outcome.supplied = {suppliedAfter.shared - suppliedBefore.shared, suppliedAfter.copied - suppliedBefore.copied, suppliedAfter.spilled - suppliedBefore.spilled}; + for (const auto& region : regions) { + if (region.guestAddress < reinterpret_cast(table) || region.guestAddress >= reinterpret_cast(table) + bytes) continue; + if (outcome.tableBytes == nullptr) outcome.tableBytes = region.bytes.data(); + if (region.bytes.data() == outcome.tableBytes + outcome.tableRegionBytes) outcome.tableRegionBytes += region.bytes.size(); + outcome.tableRegions.emplace_back(region.guestAddress, region.bytes.size()); + } + outcome.covered = TableCovered(regions, table, bytes) && std::all_of(regions.begin(), regions.end(), [&](const MemoryRegion& region) { + return region.guestAddress < reinterpret_cast(table) || region.guestAddress >= reinterpret_cast(table) + bytes || std::memcmp(reinterpret_cast(static_cast(region.guestAddress)), region.bytes.data(), region.bytes.size()) == 0; + }); + const auto after = ResourceMaterializer::TableReuse(); + outcome.reuse = {after.unwritten - before.unwritten, after.compared - before.compared, after.wordsRead - before.wordsRead, after.shared - before.shared, after.columns - before.columns}; + return outcome; + }; + const auto recordWords = [](const Outcome& outcome, std::uint32_t record) { return outcome.tables.tables.at(0).words.at(outcome.tables.tables.at(0).codes.at(record)).dwords; }; + const auto heldRecords = [&](const Outcome& outcome, const std::uint32_t* table, std::uint32_t records) { + for (std::uint32_t record = 0; record < records; record++) { + std::array words{}; + std::copy_n(table + static_cast(record) * 8u, 8u, words.begin()); + if (recordWords(outcome, record) != words) return false; + } + return true; + }; + const auto sharesColumn = [](const Outcome& left, const Outcome& right) { + const auto& a = left.tables.tables.at(0); + const auto& b = right.tables.tables.at(0); + return a.codes.Shares(b.codes) && a.words.Shares(b.words) && a.readRanges.Shares(b.readRanges); + }; + const auto sharedColumnOf = [](const std::shared_ptr& capture, const Outcome& outcome) { + const auto& a = capture->snapshot.tables.tables.at(0); + const auto& b = outcome.tables.tables.at(0); + return a.codes.Shares(b.codes) && a.words.Shares(b.words) && a.readRanges.Shares(b.readRanges); }; - auto request = makeRequest(materialCode); - const auto plan = GetResourcePlan(request); - std::size_t tables = 0; - for (const auto& source : plan->descriptorSources) { - if (!source.indirectImage.has_value()) continue; - ++tables; - const auto& table = *source.indirectImage; - require(table.hasMaterial && table.selectorStride == 16u && table.selectorOffset == 4u && table.entryOffset == 0u, "bindless: the material pattern was not recorded"); + auto* heap = AllocateWatchedTable(Bytes); + fill(heap, Records); + const auto address = reinterpret_cast(heap); + const bool watched = AgcDriver::GuestMemory::CollectWrites(address, Bytes) != 0; + alignas(4096) static TableGuest guest; + FillTableSrt(guest, heap, 32u, Records); + const auto userData = TableUserData(guest); + const auto request = TableRequest(TableWholeCode, userData); + const auto expectUnwritten = [&](const Outcome& outcome, const std::string& what) { + require(heldRecords(outcome, heap, Records) && outcome.covered, ("table snapshot reuse: " + what + ": the snapshot or the captured words do not hold the table").c_str()); + if (watched) require(outcome.reuse.unwritten == 1u && outcome.reuse.compared == 0u && outcome.reuse.wordsRead == 0u, ("table snapshot reuse: " + what + ": an unwritten table was read again").c_str()); + else require(outcome.reuse.compared == 1u && outcome.reuse.wordsRead == Records * 8u, ("table snapshot reuse: " + what + ": an unchanged unwatched table was not compared and reused").c_str()); + }; + const auto expectReread = [&](const Outcome& outcome, const std::string& what) { + require(outcome.reuse.unwritten == 0u && outcome.reuse.wordsRead == Records * 8u, ("table snapshot reuse: " + what + ": the table was not read again exactly once").c_str()); + }; + + const auto fresh = capture(request, heap, Bytes, nullptr); + require(fresh.reuse.unwritten == 0u && fresh.reuse.compared == 0u && fresh.reuse.wordsRead == Records * 8u && heldRecords(fresh, heap, Records) && fresh.covered, "table snapshot reuse: the first snapshot is wrong"); + require(fresh.reuse.columns == 1u && fresh.reuse.shared == 0u, "table snapshot reuse: the first snapshot did not build one column"); + const std::vector original(heap, heap + Bytes / sizeof(std::uint32_t)); + const auto unchanged = capture(request, heap, Bytes, nullptr); + expectUnwritten(unchanged, "an unchanged table"); + require(unchanged.tables == fresh.tables, "table snapshot reuse: a reused snapshot differs from the snapshot it reuses"); + const auto reusedAgain = capture(request, heap, Bytes, nullptr); + expectUnwritten(reusedAgain, "an unchanged table reused again"); + require(sharesColumn(unchanged, fresh) && sharesColumn(reusedAgain, fresh) && unchanged.reuse.shared == 1u && reusedAgain.reuse.shared == 1u && unchanged.reuse.columns == 0u && reusedAgain.reuse.columns == 0u, "table snapshot reuse: an unchanged table reused twice did not share one column without a copy"); + if (watched) { + const auto sharedWords = [&](const Outcome& outcome) { return outcome.supplied.shared == Records * 8u && outcome.supplied.copied == 0u && outcome.supplied.spilled == 0u && outcome.tableRegionBytes == Bytes; }; + require(sharedWords(unchanged) && sharedWords(reusedAgain) && unchanged.tableBytes != nullptr && unchanged.tableBytes == reusedAgain.tableBytes && fresh.tableBytes != unchanged.tableBytes && fresh.supplied.shared == 0u, "table snapshot reuse: the captured words of an unchanged table were copied instead of shared"); } - require(tables == 1, "bindless: the table source was not planned"); - for (const auto& image : plan->info.images) require(image.indirectSearchIterations == 0u, "bindless: the plan carries a search depth"); - const auto direct = static_cast(plan->info.images.size()); + require(fresh.tableRegions.size() == Bytes / 4096u && unchanged.tableRegions == fresh.tableRegions && reusedAgain.tableRegions == fresh.tableRegions, "table snapshot reuse: a reused table's words are not reported in the regions a read of them reports"); + { + AgcDriver::ShaderMemory stages({}); + const auto suppliedBefore = AgcDriver::ShaderMemory::Supplies(); + const auto vertex = stages.Capture(request); + const auto vertexRecent = stages.TakeRecentRegions(); + const auto pixel = stages.Capture(request); + const auto pixelRecent = stages.TakeRecentRegions(); + const auto both = stages.Regions(); + require(sharedColumnOf(vertex, fresh) && sharedColumnOf(pixel, fresh) && sortedDisjoint(vertexRecent) && sortedDisjoint(pixelRecent) && sortedDisjoint(both) && holds(vertexRecent, heap, Bytes, original.data()) && holds(pixelRecent, heap, Bytes, original.data()) && holds(both, heap, Bytes, original.data()), "table snapshot reuse: two stages of one shader memory did not each capture the shared table once"); + alignas(4096) static TableGuest halfGuest; + FillTableSrt(halfGuest, heap, 32u, Records / 2u); + const auto halfUserData = TableUserData(halfGuest); + const auto half = stages.Capture(TableRequest(TableWholeCode, halfUserData)); + const auto all = stages.Regions(); + const auto suppliedAfter = AgcDriver::ShaderMemory::Supplies(); + require(half->snapshot.tables.tables.at(0).keys == Records / 2u && sortedDisjoint(all) && holds(all, heap, Bytes, original.data()), "table snapshot reuse: a read over supplied words reported them twice or changed them"); + if (watched) require(suppliedAfter.shared - suppliedBefore.shared == 2u * Records * 8u && suppliedAfter.copied == suppliedBefore.copied && suppliedAfter.spilled - suppliedBefore.spilled == Records * 8u, "table snapshot reuse: a second stage copied the shared words, or a read over them did not move them into pages once"); + } + AgcDriver::ShaderMemory heldMemory({}); + const auto heldCapture = heldMemory.Capture(request); + const auto freshColumn = [&](const Outcome& outcome, const std::string& what) { + require(outcome.reuse.columns == 1u && outcome.reuse.shared == 0u && !sharesColumn(outcome, fresh) && !(outcome.tables == fresh.tables) && outcome.supplied.shared == 0u, ("table snapshot reuse: " + what + ": the snapshot shares the old column or holds its words").c_str()); + require(heldRecords(unchanged, original.data(), Records) && heldRecords(reusedAgain, original.data(), Records) && sharesColumn(reusedAgain, fresh), ("table snapshot reuse: " + what + ": the old column changed under the snapshots sharing it").c_str()); + require(sharedColumnOf(heldCapture, fresh) && holds(heldMemory.Regions(), heap, Bytes, original.data()), ("table snapshot reuse: " + what + ": the words a live shader memory captured before it changed").c_str()); + }; + + const auto changeRecord = [&](std::uint32_t record, std::uint32_t replacement, const std::function&)>& store) { + const auto words = texture(replacement); + store(address + static_cast(record) * 32u, words); + }; + changeRecord(77u, 2000u, [](std::uint64_t at, const std::array& words) { std::memcpy(reinterpret_cast(static_cast(at)), words.data(), sizeof(words)); }); + const auto cpuStore = capture(request, heap, Bytes, nullptr); + expectReread(cpuStore, "a CPU store"); + require(heldRecords(cpuStore, heap, Records) && cpuStore.covered && recordWords(cpuStore, 77u) == texture(2000u), "table snapshot reuse: the snapshot after a CPU store misses the stored T#"); + freshColumn(cpuStore, "a CPU store"); + expectUnwritten(capture(request, heap, Bytes, nullptr), "the table after a CPU store"); + + changeRecord(1234u, 2001u, [](std::uint64_t at, const std::array& words) { AgcDriver::GuestMemory::Write(at, std::as_bytes(std::span(words)), 4); }); + const auto driverStore = capture(request, heap, Bytes, nullptr); + expectReread(driverStore, "a driver store (CP write, DMA copy or fill)"); + require(heldRecords(driverStore, heap, Records) && recordWords(driverStore, 1234u) == texture(2001u), "table snapshot reuse: the snapshot after a driver store misses the stored T#"); + freshColumn(driverStore, "a driver store"); + expectUnwritten(capture(request, heap, Bytes, nullptr), "the table after a driver store"); + + changeRecord(2048u, 2002u, [](std::uint64_t at, const std::array& words) { AgcDriver::GuestMemory::StoreOwnBytes(at, std::as_bytes(std::span(words))); }); + const auto gpuStore = capture(request, heap, Bytes, nullptr); + expectReread(gpuStore, "a GPU result stored by the driver"); + require(heldRecords(gpuStore, heap, Records) && recordWords(gpuStore, 2048u) == texture(2002u), "table snapshot reuse: the snapshot after a stored GPU result misses the stored T#"); + freshColumn(gpuStore, "a GPU result stored by the driver"); + expectUnwritten(capture(request, heap, Bytes, nullptr), "the table after a stored GPU result"); + + AgcDriver::GuestMemory::MarkWritten(address + 3000u * 32u, 32u); + const auto marked = capture(request, heap, Bytes, nullptr); + require(marked.reuse.unwritten == 0u && marked.reuse.compared == 1u && heldRecords(marked, heap, Records) && marked.tables == gpuStore.tables, "table snapshot reuse: a GPU write into an import (MarkWritten) did not make the table compared again"); + require(sharesColumn(marked, gpuStore) && marked.reuse.columns == 0u, "table snapshot reuse: a compared table did not share the column it matched"); + + pendingTableWord = address + 500u * 32u; + pendingTableValue = 0x0badf00du; + pendingTableKnown = true; + const auto known = capture(request, heap, Bytes, &PendingTableWrite); + expectReread(known, "a pending GPU write with a known value"); + require(recordWords(known, 500u)[0] == pendingTableValue && recordWords(known, 499u) == recordWords(gpuStore, 499u), "table snapshot reuse: the snapshot under a pending GPU write misses its known value"); + freshColumn(known, "a pending GPU write with a known value"); + require(!sharesColumn(known, gpuStore), "table snapshot reuse: a pending GPU write's snapshot shares the column read before it"); + pendingTableKnown = false; + const auto synced = capture(request, heap, Bytes, &PendingTableWrite); + require(synced.reuse.unwritten == 0u && heldRecords(synced, heap, Records), "table snapshot reuse: a table under a pending GPU write was reused without a read"); + require(!sharesColumn(synced, known) && recordWords(known, 500u)[0] == pendingTableValue, "table snapshot reuse: a synced read shares or changed the column of the known value it replaced"); + pendingTableWord = 0; + const auto settled = capture(request, heap, Bytes, nullptr); + require(settled.reuse.unwritten == 0u && heldRecords(settled, heap, Records), "table snapshot reuse: a snapshot taken under a pending GPU write was reused without a read"); + expectUnwritten(capture(request, heap, Bytes, nullptr), "the table after the pending GPU write"); + + deferredTableRecord = address + 3500u * 32u; + deferredTableWords = texture(2003u); + struct FlushHookReset { + ~FlushHookReset() { AgcDriver::GuestMemory::SetFlushHook(nullptr); } + } flushHookReset; + AgcDriver::GuestMemory::SetFlushHook(&StoreDeferredTableRecord); + const auto deferred = capture(request, heap, Bytes, nullptr); + AgcDriver::GuestMemory::SetFlushHook(nullptr); + require(deferredTableStored && deferred.reuse.unwritten == 0u && heldRecords(deferred, heap, Records) && recordWords(deferred, 3500u) == texture(2003u), "table snapshot reuse: a GPU result a read would store first was not stored and read before the table was reused"); + freshColumn(deferred, "a GPU result stored by the flush hook"); + expectUnwritten(capture(request, heap, Bytes, nullptr), "the table after a GPU result stored by the flush hook"); + + constexpr std::size_t UnrelatedBytes = 65536; + void* unrelated = ::operator new(UnrelatedBytes, std::align_val_t{UnrelatedBytes}); + { + GuestAllocations::Mutation mutation; + mutation.Add(unrelated, UnrelatedBytes, true, true, false); + } + const auto remapped = capture(request, heap, Bytes, nullptr); + { + GuestAllocations::Mutation mutation; + mutation.Remove(unrelated); + } + ::operator delete(unrelated, std::align_val_t{UnrelatedBytes}); + require(remapped.reuse.unwritten == 0u && remapped.reuse.compared == 1u && heldRecords(remapped, heap, Records), "table snapshot reuse: a table was reused without a read after a guest mapping changed"); + + auto& flipped = heap[100u * 8u + 1u]; + const auto keptWord = flipped; + flipped ^= 0x100u; + const auto changed = capture(request, heap, Bytes, nullptr); + flipped = keptWord; + const auto restored = capture(request, heap, Bytes, nullptr); + const auto& remappedColumn = remapped.tables.tables.at(0); + const auto& changedColumn = changed.tables.tables.at(0); + const auto& restoredColumn = restored.tables.tables.at(0); + require(changed.reuse.columns == 1u && !changedColumn.words.Shares(remappedColumn.words) && !(changedColumn.words == remappedColumn.words) && changedColumn.words.Hash() != remappedColumn.words.Hash(), "table snapshot reuse: a changed table matched the column read before the change"); + require(restored.reuse.columns == 1u && !sharesColumn(restored, remapped) && !sharesColumn(restored, changed) && restored.tables == remapped.tables && restoredColumn.codes.Hash() == remappedColumn.codes.Hash() && restoredColumn.words.Hash() == remappedColumn.words.Hash() && !(restored.tables == changed.tables), "table snapshot reuse: a table written back to its old words is not a new column equal to the old one"); + const SharedValues sealed(std::vector{1u, 2u, 3u}); + const auto alias = sealed; + const SharedValues equal(std::vector{1u, 2u, 3u}); + const SharedValues unequal(std::vector{1u, 2u, 4u}); + require(alias.Shares(sealed) && alias == sealed && !equal.Shares(sealed) && equal == sealed && equal.Hash() == sealed.Hash() && !unequal.Shares(sealed) && !(unequal == sealed) && unequal.Hash() != sealed.Hash() && SharedValues{}.Shares(SharedValues(std::vector{})), "table snapshot reuse: shared values compare by identity before their contents"); + + struct CapturedVariant { + std::vector> runs; + std::vector words; + std::vector shared; + }; + const auto captureVariant = [](const RecompileRequest& request) { + CapturedVariant variant; + AgcDriver::ShaderMemory memory({}); + static_cast(memory.Capture(request)); + const auto supplied = memory.SuppliedBlocks(); + for (const auto& region : memory.TakeRecentRegions()) { + variant.runs.emplace_back(region.guestAddress, region.guestAddress + region.bytes.size()); + const auto offset = variant.words.size(); + variant.words.resize(offset + region.bytes.size() / sizeof(std::uint32_t)); + std::memcpy(variant.words.data() + offset, region.bytes.data(), region.bytes.size()); + AgcDriver::AppendSharedWords(variant.shared, variant.runs.size() - 1u, offset, region.bytes, supplied); + } + return variant; + }; + const auto sameVariant = [](const CapturedVariant& left, const CapturedVariant& right, std::size_t& compared) { return left.runs == right.runs && AgcDriver::SameCapturedWords(left.words, left.shared, right.words, right.shared, &compared); }; + const auto sameBlock = [](const CapturedVariant& left, const CapturedVariant& right) { return left.shared.size() == 1u && right.shared.size() == 1u && !left.shared[0].block.owner_before(right.shared[0].block) && !right.shared[0].block.owner_before(left.shared[0].block); }; + const auto tableWords = static_cast(Records) * 8u; + const auto keptVariant = captureVariant(request); + const auto freshVariant = captureVariant(request); + std::size_t comparedWords = 0; + require(sameVariant(keptVariant, freshVariant, comparedWords) && keptVariant.words == freshVariant.words, "draw cache identity: two captures of an unchanged table did not match"); + if (watched) require(sameBlock(keptVariant, freshVariant) && keptVariant.shared[0].count == tableWords && comparedWords == keptVariant.words.size() - tableWords, "draw cache identity: two captures sharing a table column compared its words instead of its identity"); + else require(keptVariant.shared.empty() && comparedWords == keptVariant.words.size(), "draw cache identity: captures of an unwatched table were not compared word by word"); + + alignas(16) static std::array variantBuffer{}; + const auto variantBufferV = TableV(variantBuffer.data(), 0u, static_cast(sizeof(variantBuffer))); + const std::array variantKeptV{guest.srt[12], guest.srt[13], guest.srt[14], guest.srt[15]}; + std::copy(variantBufferV.begin(), variantBufferV.end(), guest.srt.begin() + 12); + const auto otherSrtVariant = captureVariant(request); + std::copy(variantKeptV.begin(), variantKeptV.end(), guest.srt.begin() + 12); + require(keptVariant.runs == otherSrtVariant.runs && !sameVariant(keptVariant, otherSrtVariant, comparedWords), "draw cache identity: a capture with other SRT words matched through the table column it shares"); + if (watched) require(sameBlock(keptVariant, otherSrtVariant), "draw cache identity: a capture with other SRT words did not share the unchanged table column"); + + auto& variantWord = heap[9u * 8u + 1u]; + const auto variantKeptWord = variantWord; + variantWord ^= 0x100u; + const auto writtenVariant = captureVariant(request); + variantWord = variantKeptWord; + require(keptVariant.runs == writtenVariant.runs && !sameVariant(keptVariant, writtenVariant, comparedWords) && comparedWords == keptVariant.words.size(), "draw cache identity: a capture after a CPU store matched the capture before it"); + const auto rereadVariant = captureVariant(request); + const auto rewrittenVariant = captureVariant(request); + require(sameVariant(keptVariant, rereadVariant, comparedWords) && comparedWords == keptVariant.words.size() && sameVariant(keptVariant, rewrittenVariant, comparedWords) && comparedWords == keptVariant.words.size(), "draw cache identity: a table written back to its old words did not match by its words alone"); + if (watched) require(writtenVariant.shared.empty() && rereadVariant.shared.empty() && rewrittenVariant.shared.size() == 1u && !sameBlock(keptVariant, rewrittenVariant) && keptVariant.shared[0].block.expired(), "draw cache identity: a new column with the old words was taken for the replaced column"); + + const auto identityBlock = std::make_shared(SrtGuestWords{{{0x1000u, 4u}}, {1u, 2u, 3u, 4u}}); + const auto twinBlock = std::make_shared(SrtGuestWords{{{0x1000u, 4u}}, {1u, 2u, 3u, 4u}}); + const std::vector> identityBlocks{identityBlock, twinBlock}; + const auto identityBytes = std::as_bytes(std::span(identityBlock->words)); + const std::vector copiedWords(identityBlock->words); + std::vector whole, tail, twin, copied, misaligned, nothing; + AgcDriver::AppendSharedWords(whole, 0u, 1u, identityBytes, identityBlocks); + AgcDriver::AppendSharedWords(tail, 0u, 2u, identityBytes.subspan(4u), identityBlocks); + AgcDriver::AppendSharedWords(twin, 0u, 1u, std::as_bytes(std::span(twinBlock->words)), identityBlocks); + AgcDriver::AppendSharedWords(copied, 0u, 1u, std::as_bytes(std::span(copiedWords)), identityBlocks); + AgcDriver::AppendSharedWords(misaligned, 0u, 1u, identityBytes.subspan(2u, 8u), identityBlocks); + AgcDriver::AppendSharedWords(nothing, 0u, 1u, identityBytes.subspan(0u, 0u), identityBlocks); + require(whole.size() == 1u && whole[0].first == 1u && whole[0].count == 4u && whole[0].offset == 0u && tail.size() == 1u && tail[0].first == 2u && tail[0].count == 3u && tail[0].offset == 1u && twin.size() == 1u && twin[0].block.lock() == twinBlock && copied.empty() && misaligned.empty() && nothing.empty(), "draw cache identity: words were credited to a block they were not copied from"); + std::vector joined, gapped, crossed, skipped; + AgcDriver::AppendSharedWords(joined, 0u, 1u, identityBytes.subspan(0u, 8u), identityBlocks); + AgcDriver::AppendSharedWords(joined, 1u, 3u, identityBytes.subspan(8u), identityBlocks); + AgcDriver::AppendSharedWords(gapped, 0u, 1u, identityBytes.subspan(0u, 4u), identityBlocks); + AgcDriver::AppendSharedWords(gapped, 1u, 3u, identityBytes.subspan(4u, 4u), identityBlocks); + AgcDriver::AppendSharedWords(gapped, 2u, 4u, identityBytes.subspan(12u, 4u), identityBlocks); + AgcDriver::AppendSharedWords(crossed, 0u, 1u, identityBytes.subspan(0u, 4u), identityBlocks); + AgcDriver::AppendSharedWords(crossed, 1u, 2u, std::as_bytes(std::span(twinBlock->words)).subspan(4u, 4u), identityBlocks); + AgcDriver::AppendSharedWords(skipped, 0u, 1u, identityBytes.subspan(0u, 8u), identityBlocks); + AgcDriver::AppendSharedWords(skipped, 2u, 3u, identityBytes.subspan(8u), identityBlocks); + require(skipped.size() == 2u && skipped[0].region == 0u && skipped[0].regions == 1u && skipped[1].region == 2u && skipped[1].regions == 1u, "draw cache identity: words of regions that are not consecutive were joined"); + require(joined.size() == 1u && joined[0].first == 1u && joined[0].count == 4u && joined[0].offset == 0u && joined[0].region == 0u && joined[0].regions == 2u && gapped.size() == 3u && gapped[1].first == 3u && gapped[1].offset == 1u && gapped[2].first == 4u && gapped[2].offset == 3u && crossed.size() == 2u && crossed[1].block.lock() == twinBlock, "draw cache identity: consecutive words of one block were not joined, or words of two places were"); + const std::vector leftWords{9u, 1u, 2u, 3u, 4u}; + auto rightWords = leftWords; + std::size_t identityCompared = 0; + require(AgcDriver::SameCapturedWords(leftWords, whole, rightWords, whole, &identityCompared) && identityCompared == 1u, "draw cache identity: words copied from one block were compared"); + require(AgcDriver::SameCapturedWords(leftWords, whole, rightWords, twin, &identityCompared) && identityCompared == leftWords.size(), "draw cache identity: equal words from two blocks did not match by their words"); + rightWords[0] = 8u; + require(!AgcDriver::SameCapturedWords(leftWords, whole, rightWords, whole, &identityCompared) && identityCompared == 1u, "draw cache identity: a shared block hid a difference outside it"); + rightWords = {9u, 1u, 2u, 3u, 5u}; + const std::vector unowned{{1u, 4u, 0u, {}}}; + const std::vector shifted{{1u, 4u, 1u, identityBlock}}; + const std::vector beyond{{3u, 4u, 0u, identityBlock}}; + auto doomed = std::make_shared(SrtGuestWords{{{0x2000u, 4u}}, {1u, 2u, 3u, 4u}}); + std::vector dead; + AgcDriver::AppendSharedWords(dead, 0u, 1u, std::as_bytes(std::span(doomed->words)), std::span>(&doomed, 1u)); + doomed.reset(); + require(!AgcDriver::SameCapturedWords(leftWords, whole, rightWords, twin, &identityCompared) && identityCompared == leftWords.size(), "draw cache identity: different words from two blocks matched"); + require(!AgcDriver::SameCapturedWords(leftWords, unowned, rightWords, unowned, &identityCompared) && identityCompared == leftWords.size(), "draw cache identity: runs without a block vouched for words"); + require(!AgcDriver::SameCapturedWords(leftWords, whole, rightWords, shifted, &identityCompared) && identityCompared == leftWords.size(), "draw cache identity: runs at other offsets of one block vouched for words"); + require(!AgcDriver::SameCapturedWords(leftWords, beyond, rightWords, beyond, &identityCompared) && identityCompared == leftWords.size(), "draw cache identity: a run past the words vouched for them"); + require(dead.size() == 1u && dead[0].block.expired() && !AgcDriver::SameCapturedWords(leftWords, dead, rightWords, dead, &identityCompared) && identityCompared == leftWords.size(), "draw cache identity: a released block vouched for words"); + require(!AgcDriver::SameCapturedWords(leftWords, whole, std::vector(leftWords.begin(), leftWords.end() - 1), {}, &identityCompared) && identityCompared == 0u, "draw cache identity: captures of different sizes matched"); + + const auto compiled = CompileTable(request); + const auto& map = TableMap(*compiled.result); + const auto codes = TableCodes(map, 0); + const auto* images = TableBinding(*compiled.result, DescriptorRole::ImageTable); + require(images != nullptr && codes.size() == Records, "table snapshot reuse: the reused table has no image table binding"); + for (const auto key : {0u, 77u, 1234u, 2048u, 3071u, 4095u}) { + const auto element = map.at(codes.at(key) + Abi::ImageRecordElement); + require((codes[key] & Abi::PoisonFlag) == 0u && std::equal(heap + static_cast(key) * 8u, heap + static_cast(key) * 8u + 8u, images->guestDescriptor.begin() + static_cast(element) * 8), ("table snapshot reuse: key " + std::to_string(key) + " selects the wrong record of a reused table").c_str()); + } + + const auto resolvedBefore = ResourceMaterializer::TableReuse(); + alignas(16) static std::array otherOutput{}; + const auto otherOutputV = TableV(otherOutput.data(), 0u, static_cast(sizeof(otherOutput))); + const std::array outputV{guest.srt[12], guest.srt[13], guest.srt[14], guest.srt[15]}; + std::copy(otherOutputV.begin(), otherOutputV.end(), guest.srt.begin() + 12); + const auto otherBuffer = CompileTable(request); + std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 12); + const auto resolvedShared = ResourceMaterializer::TableReuse(); + require(otherBuffer.result != compiled.result && otherBuffer.result->variantId == compiled.result->variantId && resolvedShared.resolutionsShared - resolvedBefore.resolutionsShared == 1u && resolvedShared.resolutions == resolvedBefore.resolutions && TableMap(*otherBuffer.result) == map && TableBinding(*otherBuffer.result, DescriptorRole::ImageTable)->guestDescriptor == images->guestDescriptor, "table snapshot reuse: a result for another buffer did not reuse the unchanged table's resolution"); + const auto storedWord = heap[5u * 8u]; + heap[5u * 8u] = heap[6u * 8u]; + const auto restoredMap = map; + const auto storedTable = CompileTable(request); + heap[5u * 8u] = storedWord; + const auto resolvedStored = ResourceMaterializer::TableReuse(); + require(resolvedStored.resolutionsShared == resolvedShared.resolutionsShared && resolvedStored.resolutions - resolvedShared.resolutions == 1u && TableMap(*storedTable.result) != restoredMap, "table snapshot reuse: a table changed by a CPU store took the resolution of its old words"); + const auto& storedMap = TableMap(*storedTable.result); + const auto storedCodes = TableCodes(storedMap, 0); + const auto* storedImages = TableBinding(*storedTable.result, DescriptorRole::ImageTable); + const auto storedElement = storedMap.at(storedCodes.at(5u) + Abi::ImageRecordElement); + require(storedImages != nullptr && std::equal(heap + 6u * 8u, heap + 6u * 8u + 1u, storedImages->guestDescriptor.begin() + static_cast(storedElement) * 8) && std::equal(heap + 5u * 8u + 1u, heap + 5u * 8u + 8u, storedImages->guestDescriptor.begin() + static_cast(storedElement) * 8 + 1), "table snapshot reuse: the resolution after a CPU store does not hold the stored record"); + + constexpr std::uint32_t SmallRecords = 64; + std::vector> smallStorage(17, std::vector(SmallRecords * 8u + 1024u)); + std::vector smallGuests(smallStorage.size()); + std::vector> smallUserData(smallStorage.size()); + std::vector smallRequests; + std::vector smallTables; + for (std::size_t index = 0; index < smallStorage.size(); index++) { + auto* table = reinterpret_cast((reinterpret_cast(smallStorage[index].data()) + 4095u) & ~std::uintptr_t{4095}); + fill(table, SmallRecords); + FillTableSrt(smallGuests[index], table, 32u, SmallRecords); + smallTables.push_back(table); + smallUserData[index] = TableUserData(smallGuests[index]); + smallRequests.push_back(TableRequest(TableWholeCode, smallUserData[index])); + } + const auto smallBytes = static_cast(SmallRecords) * 32u; + const auto first = capture(smallRequests[0], smallTables[0], smallBytes, nullptr); + const auto again = capture(smallRequests[0], smallTables[0], smallBytes, nullptr); + require(first.reuse.compared == 0u && again.reuse.unwritten == 0u && again.reuse.compared == 1u && heldRecords(again, smallTables[0], SmallRecords), "table snapshot reuse: an unchanged table outside the write watch was not compared and reused"); + smallTables[0][5u * 8u] ^= 0x10u; + const auto unwatchedStore = capture(smallRequests[0], smallTables[0], smallBytes, nullptr); + require(unwatchedStore.reuse.compared == 0u && heldRecords(unwatchedStore, smallTables[0], SmallRecords), "table snapshot reuse: a store into a table outside the write watch was not seen"); + for (std::size_t index = 1; index < smallStorage.size(); index++) static_cast(capture(smallRequests[index], smallTables[index], smallBytes, nullptr)); + const auto evicted = capture(smallRequests[0], smallTables[0], smallBytes, nullptr); + const auto kept = capture(smallRequests.back(), smallTables.back(), smallBytes, nullptr); + require(evicted.reuse.unwritten == 0u && evicted.reuse.compared == 0u && kept.reuse.compared == 1u, "table snapshot reuse: the memo kept more than 16 columns"); + + constexpr std::uint32_t PagedRecords = 512; + constexpr std::size_t PagedBytes = PagedRecords * 32u; + auto* paged = AllocateWatchedTable(PagedBytes); + fill(paged, PagedRecords); + alignas(4096) static TableGuest pagedGuest; + FillTableSrt(pagedGuest, paged, 32u, PagedRecords); + const auto pagedUserData = TableUserData(pagedGuest); + const auto pagedRequest = TableRequest(TableWholeCode, pagedUserData); + const auto pagedFresh = capture(pagedRequest, paged, PagedBytes, nullptr); + const auto pagedReused = capture(pagedRequest, paged, PagedBytes, nullptr); + require(pagedFresh.reuse.columns == 1u && sharesColumn(pagedReused, pagedFresh) && heldRecords(pagedReused, paged, PagedRecords), "table snapshot reuse: an unchanged paged table did not share its column"); + const auto hidden = reinterpret_cast(paged) + 4096u; + ProtectTablePage(hidden, 0x01u); + const auto pagedUnmapped = capture(pagedRequest, paged, PagedBytes, nullptr); + ProtectTablePage(hidden, 0x04u); + const auto unmappedCode = Abi::PoisonCode(static_cast(Abi::PoisonReason::Unmapped)); + const auto& hiddenCodes = pagedUnmapped.tables.tables.at(0).codes; + require(pagedUnmapped.reuse.unwritten == 0u && pagedUnmapped.reuse.shared == 0u && pagedUnmapped.reuse.columns == 1u && pagedUnmapped.supplied.shared == 0u && hiddenCodes.at(127u) != unmappedCode && hiddenCodes.at(128u) == unmappedCode && hiddenCodes.at(255u) == unmappedCode && hiddenCodes.at(256u) != unmappedCode, "table snapshot reuse: the records of an unmapped page were not read again as Unmapped poison"); + require(heldRecords(pagedReused, paged, PagedRecords) && sharesColumn(pagedReused, pagedFresh), "table snapshot reuse: unmapping a page changed the column read before it"); + const auto pagedBack = capture(pagedRequest, paged, PagedBytes, nullptr); + require(pagedBack.reuse.unwritten == 0u && pagedBack.reuse.compared == 1u && sharesColumn(pagedBack, pagedFresh) && heldRecords(pagedBack, paged, PagedRecords), "table snapshot reuse: the column read before a page was unmapped was not compared and shared once the page came back"); + static_cast(capture(pagedRequest, paged, PagedBytes, nullptr)); + GuestArena::GuestArenaReset_nid_postfix(paged, PagedBytes); + GuestArena::GuestArenaCommit_nid_postfix(paged, PagedBytes, 0x04u, PagedBytes); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(paged), PagedBytes); + const auto pagedRemapped = capture(pagedRequest, paged, PagedBytes, nullptr); + require(pagedRemapped.reuse.unwritten == 0u && pagedRemapped.reuse.shared == 0u && pagedRemapped.reuse.columns == 1u && !sharesColumn(pagedRemapped, pagedFresh) && heldRecords(pagedRemapped, paged, PagedRecords), "table snapshot reuse: a table mapped again with new contents was not read again"); + std::cout << "table snapshot reuse " << (watched ? "with" : "without") << " write watching passed\n"; +} + +std::atomic tableRacePhase{0}; +std::uint64_t tableRaceBegin = 0; +std::uint64_t tableRaceBytes = 0; + +AgcDriver::ShaderMemory::PendingWrite RacingTableWrite(std::uint64_t address, std::size_t bytes, std::span) { + if (tableRaceBytes != 0u && address == tableRaceBegin && bytes >= tableRaceBytes) { + tableRaceBytes = 0; + tableRacePhase.store(1, std::memory_order_release); + while (tableRacePhase.load(std::memory_order_acquire) != 2) std::this_thread::yield(); + } + return AgcDriver::ShaderMemory::PendingWrite::None; +} + +void verifyTableStampRace() { + using namespace ShaderRecompiler; + constexpr std::uint32_t Records = 4096; + constexpr std::uint32_t Stride = 48; + constexpr std::size_t Bytes = static_cast(Records) * Stride; + constexpr std::size_t RecordWords = Stride / 4u; + const auto texture = [](std::uint32_t index) { return TableT(reinterpret_cast(static_cast(0x100000000ull + index * 256ull)), TableFormat8888UNorm, TableType2D, 1u + index % 5u); }; + auto* heap = AllocateWatchedTable(Bytes); + for (std::uint32_t record = 0; record < Records; record++) { + const auto words = texture(record % 1024u); + std::copy(words.begin(), words.end(), heap + record * RecordWords); + std::fill_n(heap + record * RecordWords + 8u, 4u, 0x5a5a5a5au); + } + const auto address = static_cast(reinterpret_cast(heap)); + if (AgcDriver::GuestMemory::CollectWrites(address, Bytes) == 0) { + std::cout << "table stamp race without write watching skipped\n"; + return; + } + alignas(4096) static TableGuest guest; + FillTableSrt(guest, heap, Stride, Records); + const auto userData = TableUserData(guest); + auto wideCode = TableWholeCode; + std::replace(wideCode.begin(), wideCode.end(), 0x8f108510u, 0x9310b010u); + const auto request = TableRequest(wideCode, userData); + const auto capture = [&](AgcDriver::ShaderMemory::PendingWriteQuery pending) { + const auto before = ResourceMaterializer::TableReuse(); + AgcDriver::ShaderMemory memory({}, pending); + static_cast(memory.Capture(request)); + const auto after = ResourceMaterializer::TableReuse(); + return ImageTableReuseCounts{after.unwritten - before.unwritten, after.compared - before.compared, after.wordsRead - before.wordsRead, after.shared - before.shared, after.columns - before.columns}; + }; + tableRacePhase.store(0, std::memory_order_relaxed); + tableRaceBegin = address; + tableRaceBytes = Bytes - 16u; + std::thread writer([&] { + while (tableRacePhase.load(std::memory_order_acquire) != 1) std::this_thread::yield(); + AgcDriver::GuestMemory::MarkWritten(address + 5u * Stride, 32u); + tableRacePhase.store(2, std::memory_order_release); + }); + const auto first = capture(&RacingTableWrite); + writer.join(); + require(tableRacePhase.load(std::memory_order_relaxed) == 2 && first.columns == 1u, "table stamp race: the racing store did not run while the table was stamped"); + const auto next = capture(nullptr); + require(next.unwritten == 0u && next.compared == 1u, "table stamp race: a GPU write stamped while the table was stamped, before it was noted pending, was hidden by the table's stamp"); + std::cout << "table stamp race with write watching passed\n"; +} + +void verifyTableVouch() { + using namespace ShaderRecompiler; + constexpr std::uint32_t Records = 4096; + constexpr std::uint32_t Stride = 48; + constexpr std::size_t Bytes = static_cast(Records) * Stride; + constexpr std::size_t RecordWords = Stride / 4u; + const auto texture = [](std::uint32_t index) { return TableT(reinterpret_cast(static_cast(0x100000000ull + index * 256ull)), TableFormat8888UNorm, TableType2D, 1u + index % 5u); }; + const auto fill = [&](std::uint32_t* heap) { + for (std::uint32_t record = 0; record < Records; record++) { + const auto words = texture(record % 1024u); + std::copy(words.begin(), words.end(), heap + record * RecordWords); + std::fill_n(heap + record * RecordWords + 8u, 4u, 0x5a5a5a5au); + } + }; + struct Captured { + ImageTableReuseCounts reuse; + std::uint64_t calls = 0; + std::vector addresses; + std::vector counts; + std::vector words; + std::vector shared; + std::vector> blocks; + }; + const auto capture = [](const RecompileRequest& request, AgcDriver::ShaderMemory::PendingWriteQuery pending) { + Captured captured; + const auto before = ResourceMaterializer::TableReuse(); + const auto calls = AgcDriver::ShaderMemory::Vouches().calls; + AgcDriver::ShaderMemory memory({}, pending); + static_cast(memory.Capture(request)); + captured.calls = AgcDriver::ShaderMemory::Vouches().calls - calls; + captured.blocks = memory.SuppliedBlocks(); + for (const auto& region : memory.TakeRecentRegions()) { + const auto offset = captured.words.size(); + captured.addresses.push_back(region.guestAddress); + captured.counts.push_back(region.bytes.size() / sizeof(std::uint32_t)); + captured.words.resize(offset + region.bytes.size() / sizeof(std::uint32_t)); + std::memcpy(captured.words.data() + offset, region.bytes.data(), region.bytes.size()); + AgcDriver::AppendSharedWords(captured.shared, captured.addresses.size() - 1u, offset, region.bytes, captured.blocks); + } + const auto after = ResourceMaterializer::TableReuse(); + captured.reuse = {after.unwritten - before.unwritten, after.compared - before.compared, after.wordsRead - before.wordsRead, after.shared - before.shared, after.columns - before.columns}; + return captured; + }; + const auto vouch = [](const Captured& captured, AgcDriver::ShaderMemory::PendingWriteQuery pending, std::vector& vouched) { + std::vector ranges; + const auto words = AgcDriver::VouchSharedRegions(captured.shared, captured.addresses.size(), pending, ranges); + vouched.assign(captured.addresses.size(), 0u); + std::size_t covered = 0; + for (const auto& range : ranges) { + std::fill(vouched.begin() + static_cast(range.begin), vouched.begin() + static_cast(range.end), std::uint8_t{1}); + for (auto index = range.begin; index < range.end; index++) covered += captured.counts[index]; + } + return covered == words ? words : std::size_t{0}; + }; + + auto* heap = AllocateWatchedTable(Bytes); + fill(heap); + const auto address = static_cast(reinterpret_cast(heap)); + const bool watched = AgcDriver::GuestMemory::CollectWrites(address, Bytes) != 0; + alignas(4096) static TableGuest guest; + FillTableSrt(guest, heap, Stride, Records); + const auto userData = TableUserData(guest); + auto wideCode = TableWholeCode; + std::replace(wideCode.begin(), wideCode.end(), 0x8f108510u, 0x9310b010u); + const auto request = TableRequest(wideCode, userData); + const auto tableWords = static_cast(Records) * 8u; + const auto onlyTable = [&](const Captured& captured, const std::vector& vouched) { + if (vouched.size() != captured.addresses.size()) return false; + for (std::size_t index = 0; index < vouched.size(); index++) { + if ((captured.addresses[index] >= address && captured.addresses[index] < address + Bytes) != (vouched[index] != 0u)) return false; + } + return true; + }; + + const auto fresh = capture(request, nullptr); + require(fresh.reuse.columns == 1u && fresh.reuse.wordsRead == tableWords, "table vouch: the first snapshot did not read the strided table once"); + const auto unchanged = capture(request, nullptr); + if (watched) { + require(unchanged.reuse.unwritten == 1u && unchanged.reuse.wordsRead == 0u, "table vouch: an unwritten strided table was read again"); + require(unchanged.calls == 1u, ("table vouch: an unwritten table of " + std::to_string(Records) + " strided records was vouched for in " + std::to_string(unchanged.calls) + " calls instead of one").c_str()); + require(unchanged.blocks.size() == 1u && unchanged.blocks[0]->ordered && unchanged.blocks[0]->runs.size() == Records && unchanged.blocks[0]->low == address && unchanged.blocks[0]->high == address + static_cast(Records - 1u) * Stride + 32u, "table vouch: the strided table's words were not kept as one ordered block over the span of its records"); + } else { + require(unchanged.reuse.compared == 1u && unchanged.calls == 0u, "table vouch: an unchanged unwatched strided table was not compared"); + } + + std::vector vouched; + const auto vouchesBefore = AgcDriver::ShaderMemory::Vouches(); + const auto unchangedWords = vouch(unchanged, nullptr, vouched); + const auto vouchesAfter = AgcDriver::ShaderMemory::Vouches(); + if (watched) { + require(unchangedWords == tableWords && onlyTable(unchanged, vouched), "table vouch: the regions of an unchanged shared table were not vouched for, or other regions were"); + require(vouchesAfter.calls - vouchesBefore.calls == 1u && vouchesAfter.blocks - vouchesBefore.blocks == 1u && vouchesAfter.refused == vouchesBefore.refused && vouchesAfter.words - vouchesBefore.words == tableWords, "table vouch: one shared block was not vouched for once"); + } else { + require(unchangedWords == 0u && unchanged.shared.empty() && std::none_of(vouched.begin(), vouched.end(), [](std::uint8_t flag) { return flag != 0u; }), "table vouch: an unwatched table's regions were vouched for"); + } + if (!watched) { + std::cout << "table vouch without write watching passed\n"; + return; + } + const auto refused = [&](const std::string& what, AgcDriver::ShaderMemory::PendingWriteQuery pending) { + const auto before = AgcDriver::ShaderMemory::Vouches(); + const auto words = vouch(unchanged, pending, vouched); + const auto after = AgcDriver::ShaderMemory::Vouches(); + require(words == 0u && std::none_of(vouched.begin(), vouched.end(), [](std::uint8_t flag) { return flag != 0u; }) && after.refused - before.refused == 1u, ("table vouch: " + what + ": the shared table was still vouched for").c_str()); + }; + + pendingTableKnown = false; + pendingTableWord = address + 100u * Stride; + refused("a pending GPU write into a record", &PendingTableWrite); + pendingTableWord = address + 100u * Stride + 40u; + refused("a pending GPU write between two records", &PendingTableWrite); + pendingTableWord = address + Bytes + 4096u; + require(vouch(unchanged, &PendingTableWrite, vouched) == tableWords, "table vouch: a pending GPU write outside the table refused it"); + pendingTableWord = 0; + + constexpr std::size_t UnrelatedBytes = 65536; + void* unrelated = ::operator new(UnrelatedBytes, std::align_val_t{UnrelatedBytes}); + { + GuestAllocations::Mutation mutation; + mutation.Add(unrelated, UnrelatedBytes, true, true, false); + } + refused("a changed guest mapping", nullptr); + { + GuestAllocations::Mutation mutation; + mutation.Remove(unrelated); + } + ::operator delete(unrelated, std::align_val_t{UnrelatedBytes}); + + const auto rewrite = [&](const std::string& what, const std::function& store, bool gap) { + auto held = capture(request, nullptr); + if (held.reuse.unwritten == 0u) held = capture(request, nullptr); + std::vector heldVouched; + require(held.reuse.unwritten == 1u && vouch(held, nullptr, heldVouched) == tableWords, ("table vouch: " + what + ": the table was not vouched for before the store").c_str()); + store(); + const auto before = AgcDriver::ShaderMemory::Vouches(); + require(vouch(held, nullptr, heldVouched) == 0u && AgcDriver::ShaderMemory::Vouches().refused - before.refused == 1u, ("table vouch: " + what + ": the table captured before the store was still vouched for").c_str()); + const auto after = capture(request, nullptr); + if (gap) { + require(after.reuse.unwritten == 0u && after.reuse.compared == 1u && after.reuse.columns == 0u, ("table vouch: " + what + ": the table was not compared and kept").c_str()); + require(vouch(held, nullptr, heldVouched) == tableWords, ("table vouch: " + what + ": a capture sharing the compared table was not vouched for again").c_str()); + require(capture(request, nullptr).reuse.unwritten == 1u, ("table vouch: " + what + ": the compared table was not reused unwritten after").c_str()); + } else { + require(after.reuse.unwritten == 0u && after.reuse.compared == 0u && after.reuse.columns == 1u, ("table vouch: " + what + ": the table was not read again").c_str()); + require(vouch(held, nullptr, heldVouched) == 0u, ("table vouch: " + what + ": the replaced table was vouched for after it was read again").c_str()); + } + }; + std::uint32_t replacement = 3000u; + const auto record = [&](std::uint32_t index) { return address + static_cast(index) * Stride; }; + rewrite("a CPU store into a record", [&] { const auto words = texture(replacement++); std::memcpy(heap + 7u * RecordWords, words.data(), sizeof(words)); }, false); + rewrite("a driver store (CP write, DMA copy or fill)", [&] { const auto words = texture(replacement++); AgcDriver::GuestMemory::Write(record(1234u), std::as_bytes(std::span(words)), 4); }, false); + rewrite("a GPU result stored by the driver", [&] { const auto words = texture(replacement++); AgcDriver::GuestMemory::StoreOwnBytes(record(2048u), std::as_bytes(std::span(words))); }, false); + rewrite("a CPU store between two records", [&] { heap[300u * RecordWords + 9u] ^= 0x10u; }, true); + rewrite("a GPU write into an import (MarkWritten)", [&] { AgcDriver::GuestMemory::MarkWritten(record(3000u) + 32u, 16u); }, true); + + { + auto held = capture(request, nullptr); + std::vector heldVouched; + require(held.reuse.unwritten == 1u && !held.blocks.empty() && vouch(held, nullptr, heldVouched) == tableWords, "table vouch: the held table was not vouched for"); + held.blocks.clear(); + heap[11u * RecordWords + 2u] ^= 0x10u; + const auto replaced = capture(request, nullptr); + heap[11u * RecordWords + 2u] ^= 0x10u; + const auto before = AgcDriver::ShaderMemory::Vouches(); + require(replaced.reuse.columns == 1u && held.shared.size() == 1u && held.shared[0].block.expired() && vouch(held, nullptr, heldVouched) == 0u && AgcDriver::ShaderMemory::Vouches().calls == before.calls, "table vouch: a released table block was vouched for"); + static_cast(capture(request, nullptr)); + } + + std::cout << "table vouch with write watching passed\n"; +} + +std::uint64_t spanQueryBegin = 0; +std::uint64_t spanQueryEnd = 0; +std::uint64_t spanQueriesToPages = 0; +bool spanQueryQueued = false; + +AgcDriver::ShaderMemory::PendingWrite PageQuery(std::uint64_t address, std::size_t bytes, std::span) { + if (address <= spanQueryBegin && address + bytes >= spanQueryEnd) ++spanQueriesToPages; + return AgcDriver::ShaderMemory::PendingWrite::None; +} + +AgcDriver::ShaderMemory::PendingWrite QueuedSpanQuery(std::uint64_t address, std::size_t bytes, std::span) { + return spanQueryQueued && address < spanQueryEnd && spanQueryBegin < address + bytes ? AgcDriver::ShaderMemory::PendingWrite::Sync : AgcDriver::ShaderMemory::PendingWrite::None; +} + +void verifyTableSpanQuery() { + using namespace ShaderRecompiler; + constexpr std::uint32_t Records = 4096; + constexpr std::uint32_t Stride = 48; + constexpr std::size_t Bytes = static_cast(Records) * Stride; + constexpr std::size_t RecordWords = Stride / 4u; + auto* heap = AllocateWatchedTable(Bytes); + for (std::uint32_t record = 0; record < Records; record++) { + const auto words = TableT(reinterpret_cast(static_cast(0x100000000ull + (record % 1024u) * 256ull)), TableFormat8888UNorm, TableType2D, 1u + record % 5u); + std::copy(words.begin(), words.end(), heap + record * RecordWords); + std::fill_n(heap + record * RecordWords + 8u, 4u, 0x5a5a5a5au); + } + const auto address = static_cast(reinterpret_cast(heap)); + if (AgcDriver::GuestMemory::CollectWrites(address, Bytes) == 0) { + std::cout << "table span query without write watching skipped\n"; + return; + } + alignas(4096) static TableGuest guest; + FillTableSrt(guest, heap, Stride, Records); + auto wideCode = TableWholeCode; + std::replace(wideCode.begin(), wideCode.end(), 0x8f108510u, 0x9310b010u); + const auto userData = TableUserData(guest); + const auto request = TableRequest(wideCode, userData); + spanQueryBegin = address; + spanQueryEnd = address + static_cast(Records - 1u) * Stride + 32u; + spanQueriesToPages = 0; + spanQueryQueued = false; + const auto capture = [&] { + const auto before = ResourceMaterializer::TableReuse(); + AgcDriver::ShaderMemory memory({}, &PageQuery, nullptr, nullptr, &QueuedSpanQuery); + static_cast(memory.Capture(request)); + const auto after = ResourceMaterializer::TableReuse(); + return ImageTableReuseCounts{after.unwritten - before.unwritten, after.compared - before.compared, after.wordsRead - before.wordsRead, after.shared - before.shared, after.columns - before.columns}; + }; + require(capture().columns == 1u, "table span query: the first capture did not read the table"); + require(capture().unwritten == 1u, "table span query: the unwritten table was not reused through the span query"); + spanQueryQueued = true; + const auto queued = capture(); + require(queued.unwritten == 0u && queued.compared == 1u && queued.columns == 0u, "table span query: a write queued over the table's span did not make the capture compare the table"); + spanQueryQueued = false; + require(capture().unwritten == 1u, "table span query: the table was not reused unwritten once the queued write was gone"); + require(spanQueriesToPages == 0u, ("table span query: " + std::to_string(spanQueriesToPages) + " whole-span checks went to the page query, which waits for queued draws").c_str()); + std::cout << "table span query passed\n"; +} + +void verifyTableEdgeStore() { + using namespace ShaderRecompiler; + constexpr std::uint32_t Records = 4096; + constexpr std::uint32_t Stride = 48; + constexpr std::size_t Bytes = static_cast(Records) * Stride; + constexpr std::size_t Tail = 65536; + constexpr std::size_t RecordWords = Stride / 4u; + const auto texture = [](std::uint32_t index) { return TableT(reinterpret_cast(static_cast(0x100000000ull + index * 256ull)), TableFormat8888UNorm, TableType2D, 1u + index % 5u); }; + auto* heap = AllocateWatchedTable(Bytes + Tail); + for (std::uint32_t record = 0; record < Records; record++) { + const auto words = texture(record % 1024u); + std::copy(words.begin(), words.end(), heap + record * RecordWords); + std::fill_n(heap + record * RecordWords + 8u, 4u, 0x5a5a5a5au); + } + const auto address = static_cast(reinterpret_cast(heap)); + if (AgcDriver::GuestMemory::CollectWrites(address, Bytes) == 0) { + std::cout << "table edge store without write watching skipped\n"; + return; + } + AgcDriver::GuestMemory::BumpCollectEpoch(); + AgcDriver::GuestMemory::Unwatch(address + Bytes, Tail); + alignas(4096) static TableGuest guest; + FillTableSrt(guest, heap, Stride, Records); + auto wideCode = TableWholeCode; + std::replace(wideCode.begin(), wideCode.end(), 0x8f108510u, 0x9310b010u); + const auto userData = TableUserData(guest); + const auto request = TableRequest(wideCode, userData); + struct Captured { + ImageTableReuseCounts reuse; + std::vector> blocks; + }; + const auto capture = [&] { + Captured captured; + const auto before = ResourceMaterializer::TableReuse(); + AgcDriver::ShaderMemory memory({}, nullptr); + static_cast(memory.Capture(request)); + captured.blocks = memory.SuppliedBlocks(); + const auto after = ResourceMaterializer::TableReuse(); + captured.reuse = {after.unwritten - before.unwritten, after.compared - before.compared, after.wordsRead - before.wordsRead, after.shared - before.shared, after.columns - before.columns}; + return captured; + }; + const auto vouched = [](const Captured& captured) { + const auto& block = captured.blocks.front(); + return AgcDriver::ShaderMemory::Vouched(nullptr, block->low, block->high - block->low, LoadStamp(*block)); + }; + const auto last = address + static_cast(Records - 1u) * Stride; + std::uint32_t replacement = 3000u; + const auto across = [&](const std::string& what, const std::function)>& store) { + auto held = capture(); + if (held.reuse.unwritten == 0u) held = capture(); + require(held.reuse.unwritten == 1u && held.blocks.size() == 1u && vouched(held), ("table edge store: " + what + ": the table was not vouched for before the store").c_str()); + std::array words{}; + const auto record = texture(replacement++); + std::copy(record.begin(), record.end(), words.begin()); + std::fill(words.begin() + 8, words.end(), 0x77777777u); + store(std::as_bytes(std::span(words))); + require(!vouched(held), ("table edge store: " + what + " across the heap's edge into unwatched memory did not end the heap's epoch").c_str()); + require(capture().reuse.columns == 1u, ("table edge store: " + what + ": the table was not read again").c_str()); + }; + static_cast(capture()); + across("a driver store", [&](std::span bytes) { AgcDriver::GuestMemory::Write(last, bytes, 4); }); + across("a GPU result stored by the driver", [&](std::span bytes) { AgcDriver::GuestMemory::StoreOwnBytes(last, bytes); }); + std::cout << "table edge store passed\n"; +} + +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& staticImages = staticProgram.Resources().info.images; - require(staticImages.size() == direct + slots - 1u, "bindless: the static interface needs runtime descriptors"); - const auto staticRoot = std::ranges::find_if(staticImages, [](const ImageResource& image) { return image.indirectSearchIterations != 0u; }); - require(staticRoot != staticImages.end() && staticRoot->indirectResources.size() == slots && staticRoot->indirectMappingOffset == plan->srtReads.size(), "bindless: the static table interface is incomplete"); - - AgcDriver::ShaderMemory memory({}); - const auto capture = memory.Capture(request); - const auto root = tableRoot(*capture, direct); - require(capture->snapshot.images[root].dwords == heap[0] && capture->snapshot.images[direct].dwords == heap[1] && capture->snapshot.images[direct + 1u].dwords == heap[3], "bindless: the slots do not hold the keyed entries"); - for (std::uint32_t i = direct + 2u; i < capture->snapshot.images.size(); i++) require(capture->snapshot.images[i].dwords == heap[2], "bindless: a pad slot is not null"); - const auto mapping = mappingOf(capture->snapshot); - require(std::vector(mapping.begin(), mapping.begin() + 7) == std::vector{3u, 0u, 0u, 1u, 1u, 3u, 2u}, "bindless: the (key, slot) mapping is wrong"); - require(capture->plan->srtReads.size() + mapping.size() == capture->snapshot.flattenedSrt.size(), "bindless: the mapping offset does not name the block"); - auto regions = memory.Regions(); - for (const auto& material : materials) require(covered(regions, &material[1], sizeof(std::uint32_t)), "bindless: a material key was not captured"); - for (const auto entry : {0u, 1u, 3u}) require(covered(regions, heap[entry].data(), 32u), "bindless: a table entry was not captured"); - request.context.memory = regions; - const auto compiled = Recompile(request, *capture); - bool sampled = false; - bool flattened = false; - for (const auto& binding : compiled->bindings) { - if (binding.role == DescriptorRole::FlattenedSrt) flattened = true; - if (binding.kind != DescriptorKind::SampledImage) continue; - sampled = true; - require(binding.count == direct + slots - 1u && binding.guestDescriptor.size() == 8u * binding.count, "bindless: the sampled image binding does not hold the table slots"); - require(std::none_of(binding.imageWritten.begin(), binding.imageWritten.end(), [](bool written) { return written; }), "bindless: a table slot is marked written"); - } - require(sampled && flattened, "bindless: the bindings lack the image array or the flattened SRT"); - struct SpirvScan { - bool dynamicIndexing = false; - bool shaderNonUniform = false; - bool nonUniform = false; - bool switched = false; + const auto& info = staticProgram.Resources().info; + struct Resolved { + ImageTableSnapshot snapshot; + ResolvedImageTables tables; + std::uint64_t allocations = 0; }; - const auto scan = [&](std::span words) { - SpirvScan result; - for (std::size_t cursor = 5; cursor < words.size();) { - const auto count = words[cursor] >> 16u; - require(count != 0 && count <= words.size() - cursor, "bindless: truncated SPIR-V instruction"); - const auto op = words[cursor] & 0xffffu; - if (op == 17u && words[cursor + 1] == 29u) result.dynamicIndexing = true; - if (op == 17u && words[cursor + 1] == 5301u) result.shaderNonUniform = true; - if (op == 71u && words[cursor + 2] == 5300u) result.nonUniform = true; - if (op == 251u) result.switched = true; - cursor += count; - } - return result; + 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; }; - const auto uniform = scan(compiled->spirv); - require(uniform.dynamicIndexing && uniform.switched, "bindless: the SPIR-V does not index the image array dynamically"); - require(!uniform.shaderNonUniform && !uniform.nonUniform, "bindless: a single-subgroup workgroup was decorated NonUniform"); -#if ANYPS5_ENABLE_SPIRV_TOOLS - static_cast(ValidateAndOptimizeSpirv(compiled->spirv, request.target.vulkanVersion, request.target.spirvVersion)); -#endif - - // A wave64 workgroup kept at one lane per invocation (a 64-wide host) spans two subgroups, so - // the slot needs NonUniform: rejected without the descriptor indexing capabilities, decorated - // with them. - auto split = request; - split.target.subgroupSize = 64; - AgcDriver::ShaderMemory splitMemory({}); - const auto splitCapture = splitMemory.Capture(split); - expectFailure([&] { static_cast(Recompile(split, *splitCapture)); }, "not uniform over the workgroup", "bindless: a split wave indexed the image array as uniform"); - const std::array indexingCapabilities{29u, 5301u, 5307u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; - const std::array indexingExtensions{"SPV_EXT_descriptor_indexing", "SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; - split.target.supportedCapabilities = indexingCapabilities; - split.target.supportedExtensions = indexingExtensions; - AgcDriver::ShaderMemory indexingMemory({}); - const auto indexingCapture = indexingMemory.Capture(split); - const auto splitScan = scan(Recompile(split, *indexingCapture)->spirv); - require(splitScan.dynamicIndexing && splitScan.shaderNonUniform && splitScan.nonUniform, "bindless: a split wave's slot is not decorated NonUniform"); - - // A key past the table (the no-texture marker 0xffffffff among them) and a key naming a null - // entry are left out of the mapping, so they sample zeros; the variant is the same. - for (const auto unmapped : {9u, 0xffffffffu}) { - materials[2][1] = unmapped; - AgcDriver::ShaderMemory rangeMemory({}); - const auto rangeCapture = rangeMemory.Capture(request); - const auto rangeMapping = mappingOf(rangeCapture->snapshot); - require(std::vector(rangeMapping.begin(), rangeMapping.begin() + 5) == std::vector{2u, 0u, 0u, 1u, 1u}, "bindless: an out-of-range key was kept"); - request.context.memory = rangeMemory.Regions(); - require(Recompile(request, *rangeCapture)->variantId == compiled->variantId, "bindless: the keys changed the variant"); + 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.tables.at(0).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"); } - materials[2][1] = 2u; - AgcDriver::ShaderMemory nullMemory({}); - const auto nullCapture = nullMemory.Capture(request); - const auto nullMapping = mappingOf(nullCapture->snapshot); - require(std::vector(nullMapping.begin(), nullMapping.begin() + 5) == std::vector{2u, 0u, 0u, 1u, 1u}, "bindless: a null entry's key was mapped"); - require(nullCapture->snapshot.images[direct + 1u].dwords == heap[2], "bindless: a null entry's slot is not the pad"); - materials[2][1] = 3u; - - auto whole = makeRequest(wholeCode); - const auto wholePlan = GetResourcePlan(whole); - for (const auto& source : wholePlan->descriptorSources) { - if (source.indirectImage.has_value()) require(!source.indirectImage->hasMaterial, "bindless: a material pattern was recorded without one"); - } - AgcDriver::ShaderMemory wholeMemory({}); - const auto wholeCapture = wholeMemory.Capture(whole); - const auto wholeDirect = static_cast(wholePlan->info.images.size()); - const auto wholeRoot = tableRoot(*wholeCapture, wholeDirect); - const auto wholeMapping = mappingOf(wholeCapture->snapshot); - require(std::vector(wholeMapping.begin(), wholeMapping.begin() + 7) == std::vector{3u, 0u, 0u, 1u, 1u, 3u, 3u}, "bindless: mode T is not the identity mapping"); - require(wholeCapture->snapshot.images[wholeRoot].dwords == heap[0] && wholeCapture->snapshot.images[wholeDirect].dwords == heap[1] && wholeCapture->snapshot.images[wholeDirect + 1u].dwords == heap[2] && wholeCapture->snapshot.images[wholeDirect + 2u].dwords == heap[3], "bindless: mode T slots are wrong"); - whole.context.memory = wholeMemory.Regions(); - require(!Recompile(whole, *wholeCapture)->spirv.empty(), "bindless: mode T did not compile"); - std::vector latchCode(wholeCode.begin(), wholeCode.begin() + 12); - latchCode.insert(latchCode.end(), {0xbea80380u, 0xf09c0f08u, 0x00450000u, 0x80288128u, 0xbf0a8428u, 0xbf85fffbu, 0xe0700000u, 0x80070000u, 0xbf810000u}); - auto latch = makeRequest(latchCode); - AgcDriver::ShaderMemory latchMemory({}); - const auto latchCapture = latchMemory.Capture(latch); - const auto latchRegions = latchMemory.Regions(); - latch.context.memory = latchRegions; - require(!Recompile(latch, *latchCapture)->spirv.empty(), "bindless: a sample in a loop latch did not compile"); - - std::vector queryCode(wholeCode.begin(), wholeCode.begin() + 8); - queryCode.insert(queryCode.end(), {0xbea80380u, 0x7e040280u, 0x7e200500u, 0x80102810u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf0380f08u, 0x00050402u, 0xf09c0f08u, 0x00450800u, 0x80288128u, 0xbf0a8228u, 0xbf85fff4u, 0xe0700000u, 0x80070800u, 0xe0700004u, 0x80070400u, 0xbf810000u}); - auto query = makeRequest(queryCode); - const auto queryPlan = GetResourcePlan(query); - require(std::ranges::count_if(queryPlan->descriptorSources, [](const DescriptorSource& source) { return source.indirectImage.has_value(); }) == 1, "bindless: a T# both queried and sampled was not planned as one table"); - AgcDriver::ShaderMemory queryMemory({}); - const auto queryCapture = queryMemory.Capture(query); - const auto queryDirect = static_cast(queryPlan->info.images.size()); - require(queryDirect == 1u, "bindless: the queried and sampled T# became more than one image"); - const auto queryRoot = tableRoot(*queryCapture, queryDirect); - require(queryCapture->snapshot.images[queryRoot].dwords == heap[0] && queryCapture->snapshot.images[queryDirect].dwords == heap[1], "bindless: the queried and sampled table slots are wrong"); - query.context.memory = queryMemory.Regions(); - const auto queried = Recompile(query, *queryCapture); - require(!queried->spirv.empty(), "bindless: a T# both queried and sampled did not compile"); -#if ANYPS5_ENABLE_SPIRV_TOOLS - static_cast(ValidateAndOptimizeSpirv(queried->spirv, query.target.vulkanVersion, query.target.spirvVersion)); -#endif - - // A table wider than the slots without a material pattern is rejected. - fillSrt(100u); - AgcDriver::ShaderMemory wideMemory({}); - expectFailure([&] { static_cast(wideMemory.Capture(whole)); }, "bindless image table has 100 entries", "bindless: a wide table was bound"); - fillSrt(4u); - const auto savedHeap = heap; - heap = {}; - AgcDriver::ShaderMemory emptyMemory({}); - const auto emptyCapture = emptyMemory.Capture(request); - require(mappingOf(emptyCapture->snapshot).front() == 0u, "bindless: an empty table has mapped keys"); - request.context.memory = emptyMemory.Regions(); - require(Recompile(request, *emptyCapture)->variantId == compiled->variantId, "bindless: an empty table changed the artifact"); - heap = savedHeap; - heap[0][3] &= 0x0fffffffu; - AgcDriver::ShaderMemory invalidMemory({}); - expectFailure([&] { static_cast(invalidMemory.Capture(request)); }, "invalid descriptor", "bindless: an invalid T# was accepted"); - heap = savedHeap; - srt[1] &= 0xffffu; - srt[2] = 127u; - AgcDriver::ShaderMemory partialMemory({}); - expectFailure([&] { static_cast(partialMemory.Capture(request)); }, "partial descriptor", "bindless: a partial heap descriptor was accepted"); - fillSrt(4u); + 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; + static TableTexture textures[2]; + alignas(4096) static TableGuest guest; + auto& heap = guest.heap; + auto& materials = guest.materials; + 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[3] = heap[0]; + materials = {{{0u, 1u, 0u, 0u}, {0u, 0u, 0u, 0u}, {0u, 3u, 0u, 0u}}}; + FillTableSrt(guest, heap.data(), 32u, 4u); + const auto userData = TableUserData(guest); + auto request = TableRequest(TableMaterialCode, userData); + + const std::vector invariantPhiCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, + 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xbe910380u, + 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf068011u, 0xbf850003u, + 0xf42c0502u, 0x20000000u, 0xbf820002u, 0xf42c0502u, 0x20000000u, + 0x80118111u, 0xbf0a8211u, 0xbf85fff2u, 0xbf810000u, + }; + const auto invariantPhiRequest = TableRequest(invariantPhiCode, userData); + const auto invariantPhiPlan = GetResourcePlan(invariantPhiRequest); + require(invariantPhiPlan->info.images.size() == 1u && invariantPhiPlan->info.images[0].table == 0u, "image tables: an invariant descriptor Phi was not planned as one table"); + const auto invariantPhi = CompileTable(invariantPhiRequest); + require(invariantPhi.capture->snapshot.tables.tables.size() == 1u && invariantPhi.capture->snapshot.tables.tables[0].records == 4u && TableBinding(*invariantPhi.result, DescriptorRole::ImageTable) != nullptr && TableBinding(*invariantPhi.result, DescriptorRole::ImageTable)->count == 2u && !invariantPhi.result->spirv.empty(), "image tables: an invariant descriptor Phi lost its table records"); + + auto varyingPhiCode = invariantPhiCode; + varyingPhiCode[23] = 0x20000020u; + bool varyingRefusedOrSplit = false; + try { + const auto varying = GetResourcePlan(TableRequest(varyingPhiCode, userData)); + varyingRefusedOrSplit = varying->info.images.size() > 1u; + } catch (const std::runtime_error& error) { + varyingRefusedOrSplit = std::string_view(error.what()).find("GetImageResource dword 0 is not a valid runtime value") != std::string_view::npos; + } + require(varyingRefusedOrSplit, "image tables: a descriptor Phi with a differing offset became one invariant table"); + + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && plan->info.images[0].tableOperation == TableOperation::Supported, "image tables: 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->stride == 32u && column->addend == 0u && column->offset == 0u && column->dwordCount == 8u && !column->sampler, "image tables: the column is wrong"); + require(!column->keyDomains.empty() && column->keyDomains[0].offset == 4u && column->keyDomains[0].stride == 16u, "image tables: the material key domain was not recorded"); + auto staticRequest = request; + const std::array absentResources{}; + staticRequest.context.userData = absentResources; + staticRequest.context.memory = {}; + auto staticProgram = PrepareResourceProgram(staticRequest); + ResourceMaterializer{}.ApplyStaticInterface(staticProgram); + const auto& staticInfo = staticProgram.Resources().info; + require(staticInfo.images.size() == 1u && staticInfo.images[0].table == 0u && !staticInfo.runtimeImageModes.at(0).empty() && staticInfo.runtimeImageModes[0].size() <= Abi::MaxModes && staticInfo.usesFaultBuffer, "image tables: the static interface lacks the table modes or the fault buffer"); + + const auto compiled = CompileTable(request); + const auto& tables = compiled.capture->snapshot.tables; + require(tables.tables.size() == 1u, "image tables: the snapshot has no table"); + const auto& snapshot = tables.tables[0]; + require(snapshot.base == static_cast(reinterpret_cast(heap.data())) && snapshot.size == 128u && snapshot.records == 4u && snapshot.keys == 4u && snapshot.fault == 0u && !snapshot.outside, "image tables: the column snapshot is wrong"); + require(snapshot.codes.size() == 4u && snapshot.codes[0] == snapshot.codes[3] && snapshot.codes[0] != snapshot.codes[1] && snapshot.codes[2] == Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)), "image tables: the narrowed codes are wrong"); + require(snapshot.words.at(snapshot.codes[0]).dwords == heap[0] && snapshot.words.at(snapshot.codes[1]).dwords == heap[1], "image tables: the snapshot words are wrong"); + for (const auto& material : materials) require(TableCovered(compiled.regions, &material[1], sizeof(std::uint32_t)), "image tables: a material key was not captured"); + for (const auto entry : {0u, 1u, 3u}) require(TableCovered(compiled.regions, heap[entry].data(), 32u), "image tables: a table entry was not captured"); + const auto& result = *compiled.result; + const auto* images = TableBinding(result, DescriptorRole::ImageTable); + require(images != nullptr && images->kind == DescriptorKind::SampledImage && images->count == 2u && images->guestDescriptor.size() == 16u && images->imageShapes.size() == 2u, "image tables: the image table binding is wrong"); + require(std::none_of(images->imageWritten.begin(), images->imageWritten.end(), [](bool written) { return written; }), "image tables: a table element is marked written"); + require(TableBinding(result, DescriptorRole::FaultBuffer) != nullptr && TableBinding(result, DescriptorRole::GuestImages) == nullptr, "image tables: the bindings lack the fault buffer or hold table slots"); + const auto& map = TableMap(result); + require(map[Abi::HeaderVersion] == Abi::Version && map[Abi::HeaderTableCount] == 1u && map[Abi::HeaderElementCount] == 2u && map[Abi::HeaderSamplerCount] == 0u, "image tables: the map header is wrong"); + const auto codes = TableCodes(map, 0); + require(codes.size() == 4u && codes[0] == codes[3] && codes[2] == Abi::PoisonCode(Abi::OutsideDomainPoison) && (codes[0] & Abi::PoisonFlag) == 0u && (codes[1] & Abi::PoisonFlag) == 0u && codes[0] != codes[1], "image tables: the map codes are wrong"); + const auto modeOf = [&](std::uint32_t code) { return (map.at(code + Abi::ImageRecordFlags) >> Abi::ModeShift) & Abi::ModeMask; }; + const auto& modes = staticInfo.runtimeImageModes[0]; + require(map.at(codes[0] + Abi::ImageRecordElement) == 0u && map.at(codes[1] + Abi::ImageRecordElement) == 1u && modes.at(modeOf(codes[0])).numericClass == IrTextureNumericClass::Float && modes.at(modeOf(codes[1])).numericClass == IrTextureNumericClass::Uint, "image tables: the records are wrong"); + require(map.at(codes[1] + Abi::ImageRecordDword3) == heap[1][3], "image tables: the record does not carry the T# swizzle word"); + require(result.imageTableFaults == 0u, "image tables: a narrowed table expects faults"); + const auto uniform = ScanTableSpirv(result.spirv); + require(uniform.runtimeArray && uniform.dynamicIndexing && uniform.switched, "image tables: the SPIR-V does not index the table"); + require(!uniform.shaderNonUniform && !uniform.nonUniform, "image tables: a single-subgroup workgroup was decorated NonUniform"); +#if ANYPS5_ENABLE_SPIRV_TOOLS + static_cast(ValidateAndOptimizeSpirv(result.spirv, request.target.vulkanVersion, request.target.spirvVersion)); +#endif + + const auto split = CompileTable(TableRequest(TableMaterialCode, userData, 64u)); + const auto splitScan = ScanTableSpirv(split.result->spirv); + require(splitScan.shaderNonUniform && splitScan.nonUniform, "image tables: a split wave's table index is not decorated NonUniform"); + + for (const auto unmapped : {9u, 0xffffffffu}) { + materials[2][1] = unmapped; + const auto range = CompileTable(request); + const auto rangeCodes = TableCodes(TableMap(*range.result), 0); + require(rangeCodes.size() == 4u && rangeCodes[3] == Abi::PoisonCode(Abi::OutsideDomainPoison) && !range.capture->snapshot.tables.tables[0].outside && range.result->imageTableFaults == 0u, "image tables: a key past the V# range was kept or expects a fault"); + require(range.result->variantId == result.variantId, "image tables: the keys changed the variant"); + } + materials[2][1] = 2u; + const auto nulled = CompileTable(request); + require(TableCodes(TableMap(*nulled.result), 0).at(2) == Abi::NullCode, "image tables: a null entry's key was mapped to a record"); + materials[2][1] = 3u; + + std::vector latchCode(TableWholeCode.begin(), TableWholeCode.begin() + 12); + latchCode.insert(latchCode.end(), {0xbea80380u, 0xf09c0f08u, 0x00450000u, 0x80288128u, 0xbf0a8428u, 0xbf85fffbu, 0xe0700000u, 0x80070000u, 0xbf810000u}); + const auto latch = CompileTable(TableRequest(latchCode, userData)); + require(!latch.result->spirv.empty(), "image tables: a sample in a loop latch did not compile"); + + const auto whole = CompileTable(TableRequest(TableWholeCode, userData)); + const auto& wholePlan = *whole.capture->plan; + require(wholePlan.descriptorSources.at(wholePlan.info.images.at(0).source).tableColumn->keyDomains.empty(), "image tables: a key domain was recorded without a material read"); + const auto wholeCodes = TableCodes(TableMap(*whole.result), 0); + require(wholeCodes.size() == 4u && wholeCodes[0] == wholeCodes[3] && wholeCodes[2] == Abi::NullCode && wholeCodes[1] != wholeCodes[0], "image tables: the whole table map is wrong"); + + for (std::uint32_t entry = 4; entry < 100u; entry++) heap[entry] = TableT(textures[entry % 2u].bytes.data(), entry % 3u == 0u ? TableFormat32UInt : TableFormat8888UNorm, TableType2D, entry); + FillTableSrt(guest, heap.data(), 32u, 100u); + const auto wide = CompileTable(TableRequest(TableWholeCode, userData)); + const auto& wideMap = TableMap(*wide.result); + require(wideMap[Abi::TableHeader(0) + Abi::TableKeyCount] == 100u && wideMap[Abi::HeaderElementCount] == 98u && wide.result->variantId == whole.result->variantId, "image tables: a wide table was not bound in full"); + for (std::uint32_t entry = 4; entry < 100u; entry++) heap[entry] = {}; + FillTableSrt(guest, heap.data(), 32u, 4u); + + const auto saved = heap; + std::fill(heap.begin(), heap.end(), std::array{}); + const auto empty = CompileTable(request); + const auto emptyCodes = TableCodes(TableMap(*empty.result), 0); + require(TableMap(*empty.result)[Abi::HeaderElementCount] == 0u && std::all_of(emptyCodes.begin(), emptyCodes.end(), [](std::uint32_t code) { return code == Abi::NullCode || code == Abi::PoisonCode(Abi::OutsideDomainPoison); }), "image tables: an empty table has records"); + require(empty.result->variantId == result.variantId, "image tables: an empty table changed the artifact"); + heap = saved; + + std::vector queryCode(TableWholeCode.begin(), TableWholeCode.begin() + 8); + queryCode.insert(queryCode.end(), {0xbea80380u, 0x7e040280u, 0x7e200500u, 0x80102810u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf0380f08u, 0x00050402u, 0xf09c0f08u, 0x00450800u, 0x80288128u, 0xbf0a8228u, 0xbf85fff4u, 0xe0700000u, 0x80070800u, 0xe0700004u, 0x80070400u, 0xbf810000u}); + const auto query = CompileTable(TableRequest(queryCode, userData)); + const auto& queryPlan = *query.capture->plan; + require(std::ranges::count_if(queryPlan.descriptorSources, [](const DescriptorSource& source) { return source.tableColumn.has_value(); }) == 1 && queryPlan.info.images.size() == 1u && queryPlan.info.images[0].table == 0u, "image tables: a T# both queried and sampled was not planned as one table column"); + const auto queryCodes = TableCodes(TableMap(*query.result), 0); + require(queryCodes.size() == 4u && queryCodes[0] == queryCodes[3] && queryCodes[2] == Abi::NullCode && queryCodes[1] != queryCodes[0], "image tables: the queried and sampled table's map is wrong"); + require(!query.result->spirv.empty(), "image tables: a T# both queried and sampled did not compile"); +#if ANYPS5_ENABLE_SPIRV_TOOLS + static_cast(ValidateAndOptimizeSpirv(query.result->spirv, request.target.vulkanVersion, request.target.spirvVersion)); +#endif + + heap[0][3] = (heap[0][3] & 0x0fffffffu) | (1u << 28u); + const auto invalid = CompileTable(request); + const auto& invalidMap = TableMap(*invalid.result); + const auto invalidCodes = TableCodes(invalidMap, 0); + const auto invalidFlags = invalidMap.at(invalidCodes[0] + Abi::ImageRecordFlags); + require((invalidCodes[0] & Abi::PoisonFlag) == 0u && (invalidFlags & Abi::PoisonFlag) != 0u && invalidCodes[0] != invalidCodes[3], "image tables: an invalid 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[0][3] && invalid.result->imageTableFaults != 0u, "image tables: an invalid T# was not poisoned with its words"); + require(invalid.result->variantId == result.variantId, "image tables: an invalid T# changed the artifact"); + + heap[0][3] &= 0x0fffffffu; + const auto typeZeroTable = CompileTable(request); + const auto typeZeroCodes = TableCodes(TableMap(*typeZeroTable.result), 0); + const auto typeZeroFlags = TableMap(*typeZeroTable.result).at(typeZeroCodes[0] + Abi::ImageRecordFlags); + require((typeZeroCodes[0] & Abi::PoisonFlag) == 0u && (typeZeroFlags & Abi::PoisonFlag) != 0u && typeZeroTable.result->imageTableFaults != 0u, + "image tables: a TYPE=0 table entry was treated as a measured static null image"); + heap = saved; + + FillTableSrt(guest, heap.data(), 0u, 100u); + const auto partial = CompileTable(request); + const auto& partialTables = partial.capture->snapshot.tables; + require(partialTables.tables[0].keys == 4u && partialTables.tables[0].size == 100u, "image tables: a partial table has the wrong record count"); + const auto& tail = partialTables.tables[0].words.at(partialTables.tables[0].codes.at(3)); + require(tail.dwords[0] == heap[3][0] && std::all_of(tail.dwords.begin() + 1, tail.dwords.end(), [](std::uint32_t word) { return word == 0u; }), "image tables: the straddling record's tail is not zero"); + const auto& partialMap = TableMap(*partial.result); + require((partialMap.at(TableCodes(partialMap, 0).at(3) + Abi::ImageRecordFlags) & Abi::PoisonFlag) != 0u, "image tables: a straddling record was bound"); + FillTableSrt(guest, heap.data(), 32u, 4u); + + 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, "image tables: cannot reserve an inaccessible page"); + FillTableSrt(guest, reserved, 32u, 4u); + const auto unmapped = CompileTable(TableRequest(TableWholeCode, userData)); + const auto unmappedCodes = unmapped.capture->snapshot.tables.tables.at(0).codes; + require(unmappedCodes.size() == 4u && std::all_of(unmappedCodes.begin(), unmappedCodes.end(), [](std::uint32_t code) { return code == Abi::PoisonCode(static_cast(Abi::PoisonReason::Unmapped)); }), "image tables: an unmapped table is not Unmapped poison"); + const auto unmappedRange = std::any_of(unmapped.regions.begin(), unmapped.regions.end(), [&](const MemoryRegion& region) { const auto address = static_cast(reinterpret_cast(reserved)); return region.guestAddress < address + 4096u && address < region.guestAddress + region.bytes.size(); }); + require(!unmappedRange, "image tables: an unmapped record was read"); +#ifdef _WIN32 + VirtualFree(reserved, 0, MEM_RELEASE); +#else + munmap(reserved, 4096); +#endif + FillTableSrt(guest, heap.data(), 32u, 4u); + + alignas(4096) static std::array, 70000> capped{}; + FillTableSrt(guest, capped.data(), 32u, static_cast(capped.size())); + const auto cappedResult = CompileTable(TableRequest(TableWholeCode, userData)); + const auto& cappedColumn = cappedResult.capture->snapshot.tables.tables.at(0); + require(cappedColumn.keys == Abi::MaxKeys && cappedColumn.records == capped.size() && cappedColumn.outside && cappedResult.result->imageTableFaults == 1u, "image tables: the key cap did not mark the table's tail outside the snapshot"); + FillTableSrt(guest, heap.data(), 32u, 4u); + + guest.srt[3] |= 0x40000000u; + const auto typed = CompileTable(request); + const auto& typedMap = TableMap(*typed.result); + const auto typedFault = typedMap[Abi::TableHeader(0) + Abi::TableFault]; + require((typedFault & Abi::PoisonFlag) != 0u && typed.result->imageTablePoison.at(typedFault & ~Abi::PoisonFlag).reason == static_cast(Abi::PoisonReason::NotBuffer), "image tables: a typed table V# was not a whole-table fault"); + FillTableSrt(guest, heap.data(), 32u, 4u); +} + +void verifyImageTableKeyUnion() { + using namespace ShaderRecompiler; + static TableTexture textures[3]; + 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] = TableT(textures[2].bytes.data(), TableFormat8888UNorm, TableType2D); + FillTableSrt(guest, heap.data(), 32u, 4u); + guest.srt[16] = 1u; + guest.srt[17] = 3u; + const auto userData = TableUserData(guest); + const auto request = TableRequest(TableTwoKeyCode, userData); + const auto plan = GetResourcePlan(request); + 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& 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 == 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& 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; + static TableTexture textures[3]; + 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] = TableT(textures[2].bytes.data(), TableFormat8888UNorm, TableType2D); + FillTableSrt(guest, heap.data(), 32u, 4u); + guest.srt[16] = 1u; + guest.srt[17] = 3u; + const auto userData = TableUserData(guest); + const auto request = TableRequest(TableLateKeyCode, userData); + const auto plan = GetResourcePlan(request); + 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& 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 == 2u, "image table late key: the binding does not hold the two distinct images of the selected records"); +} + +void verifyImageTableUnkeyedAccess() { + for (const auto* code : {&TableMixedKeyCode, &TableMixedKeyFirstCode}) { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture textures[4]; + 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] = TableT(textures[2].bytes.data(), TableFormat8888UNorm, TableType2D); + heap[3] = TableT(textures[3].bytes.data(), TableFormat32UInt, TableType2D); + FillTableSrt(guest, heap.data(), 32u, 4u); + guest.srt[16] = 2u; + guest.materials = {}; + guest.materials[0][1] = 3u; + guest.materials[1][1] = 1u; + const auto userData = TableUserData(guest); + const auto request = TableRequest(*code, userData); + const auto plan = GetResourcePlan(request); + 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& 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 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"); + } +} + +struct TableManyGuest { + std::array, 128> heap{}; + std::array output{}; + std::array srt{}; +}; + +constexpr std::uint32_t TableManyDirectWord = 96u; +constexpr std::uint32_t TableManyExtraSamplerWord = 112u; + +std::vector TableManyKeyCode(std::uint32_t first, std::uint32_t second, bool twoSamplers, std::uint32_t direct = 0u, bool extraSampler = false) { + std::vector code{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0xf4080a00u, 0xfa000040u, 0x7e100280u}; + for (std::uint32_t image = 0; image < direct; image++) code.insert(code.end(), {0xf40c0000u | ((44u + image * 8u) << 6u), 0xfa000000u | ((TableManyDirectWord + image * 8u) * 4u)}); + if (extraSampler) code.insert(code.end(), {0xf4080f00u, 0xfa000000u | (TableManyExtraSamplerWord * 4u)}); + for (std::uint32_t read = 0; read < first + second; read++) { + code.insert(code.end(), {0xf4000400u, 0xfa000050u + read * 4u, 0x8f108510u, read < first ? 0xf42c0502u : 0xf42c0514u, 0x20000000u, 0xf09c0f08u, 0x00450400u, 0x06100908u}); + if (twoSamplers) code.insert(code.end(), {0xf09c0f08u, 0x00650400u, 0x06100908u}); + } + for (std::uint32_t image = 0; image < direct; image++) { + const auto resource = ((44u + image * 8u) / 4u) << 16u; + code.insert(code.end(), {0xf09c0f08u, 0x00400400u | resource, 0x06100908u}); + if (twoSamplers) code.insert(code.end(), {0xf09c0f08u, 0x00600400u | resource, 0x06100908u}); + if (extraSampler && image + 1u == direct) code.insert(code.end(), {0xf09c0f08u, 0x01e00400u | resource, 0x06100908u}); + } + code.insert(code.end(), {0xe0700000u, 0x80070800u, 0xbf810000u}); + return code; +} + +void verifySelectedRecordsWithinLimits(bool pairs, bool direct) { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture textures[4]; + alignas(4096) static TableManyGuest guest; + auto& heap = guest.heap; + for (std::uint32_t record = 0; record < heap.size(); record++) heap[record] = TableT(textures[record % 4u].bytes.data(), TableFormat8888UNorm, TableType2D, record + 1u); + const auto heapV = TableV(heap.data(), 32u, static_cast(heap.size())); + const auto outputV = TableV(guest.output.data(), 0u, 16u); + guest.srt = {}; + std::copy(heapV.begin(), heapV.end(), guest.srt.begin()); + guest.srt[8] = 0x92u; + std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 12); + std::copy(heapV.begin(), heapV.end(), guest.srt.begin() + 16); + for (std::uint32_t read = 0; read + 20u < guest.srt.size(); read++) guest.srt[20u + read] = read + 2u; + for (std::uint32_t image = 0; image < 2u; image++) { + const auto direct = TableT(textures[image].bytes.data(), TableFormat8888UNorm, TableType2D, 200u + image); + std::copy(direct.begin(), direct.end(), guest.srt.begin() + TableManyDirectWord + image * 8u); + } + guest.srt[TableManyExtraSamplerWord] = 0x91u; + const auto address = static_cast(reinterpret_cast(guest.srt.data())); + const std::array userData{static_cast(address), static_cast(address >> 32u)}; + const auto columnOf = [](const IrResourcePlan& plan, const ImageResource& image) { return *plan.descriptorSources.at(image.source).tableColumn; }; + const auto selectedTables = [&](const IrResourcePlan& plan) { + return static_cast(std::count_if(plan.info.images.begin(), plan.info.images.end(), [&](const ImageResource& image) { return image.table != NoTable && columnOf(plan, image).selected; })); + }; + const std::string name = pairs ? "selected records within the pair limit" : "selected records within the image limit"; + const std::uint32_t limit = pairs ? ShaderInfo::MaxSampledPairs / 2u : ShaderInfo::MaxImages; + const auto outside = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); + if (direct) { + const auto directImages = [&](const IrResourcePlan& plan) { + return static_cast(std::count_if(plan.info.images.begin(), plan.info.images.end(), [](const ImageResource& image) { return image.table == NoTable; })); + }; + const auto reads = limit - 1u; + const auto fittingDirectCode = TableManyKeyCode(reads, 0u, pairs, 1u); + const auto fittingDirect = GetResourcePlan(TableRequest(fittingDirectCode, userData)); + const auto describe = [&](const IrResourcePlan& plan) { + return std::to_string(plan.info.images.size()) + " images (" + std::to_string(selectedTables(plan)) + " selected tables, " + std::to_string(directImages(plan)) + " direct), " + std::to_string(plan.info.sampledPairs.size()) + " pairs"; + }; + require(fittingDirect->info.images.size() == limit && selectedTables(*fittingDirect) == reads && directImages(*fittingDirect) == 1u && fittingDirect->info.sampledPairs.size() == (pairs ? 2u : 1u) * limit, (name + ": selected reads and a direct image that fit lost the per-key split").c_str()); + std::cout << name << ": " << reads << " selected reads and 1 direct image" << (pairs ? ", each with 2 samplers" : "") << ": " << describe(*fittingDirect) << '\n'; + const auto pastDirectCode = TableManyKeyCode(reads, 0u, pairs, pairs ? 1u : 2u, pairs); + const auto pastDirectRequest = TableRequest(pastDirectCode, userData); + const auto pastDirect = GetResourcePlan(pastDirectRequest); + const auto mergedDirect = std::find_if(pastDirect->info.images.begin(), pastDirect->info.images.end(), [](const ImageResource& image) { return image.table != NoTable; }); + require(pastDirect->info.images.size() == (pairs ? 2u : 3u) && selectedTables(*pastDirect) == 0u && directImages(*pastDirect) == (pairs ? 1u : 2u) && pastDirect->info.sampledPairs.size() == (pairs ? 5u : 3u) && mergedDirect != pastDirect->info.images.end(), (name + ": selected reads that the direct " + (pairs ? "sampler" : "images") + " push past the limit were not merged into one table").c_str()); + const auto mergedColumn = columnOf(*pastDirect, *mergedDirect); + require(!mergedColumn.selected && mergedColumn.selection == 0u && !mergedColumn.unkeyed && mergedColumn.keyDomains.size() == reads, (name + ": the table merged beside the direct " + (pairs ? "sampler" : "images") + " is not the union of the keys").c_str()); + const auto compiledDirect = CompileTable(pastDirectRequest); + const auto& directTables = compiledDirect.capture->snapshot.tables; + require(directTables.tables.size() == 1u, (name + ": the snapshot beside the direct " + (pairs ? "sampler" : "images") + " does not hold one table").c_str()); + const auto& directSnapshot = directTables.tables[mergedDirect->table]; + for (const auto key : {2u, 2u + reads / 2u, 1u + reads}) { + require(key < directSnapshot.codes.size() && (directSnapshot.codes[key] & Abi::PoisonFlag) == 0u && directSnapshot.words.at(directSnapshot.codes[key]).dwords == heap[key], (name + ": a key beside the direct " + (pairs ? "sampler" : "images") + " does not bind its record").c_str()); + } + require(directSnapshot.codes.at(1) == outside && directSnapshot.codes.at(reads + 2u) == outside, (name + ": a key no read selects was bound beside the direct " + (pairs ? "sampler" : "images")).c_str()); + std::cout << name << ": " << reads << " selected reads and " << (pairs ? "1 direct image with a third sampler" : "2 direct images") << ": " << describe(*pastDirect) << ", the reads merged into one table with " << mergedColumn.keyDomains.size() << " key domains\n"; + return; + } + const auto fitting = TableManyKeyCode(limit, 0u, pairs); + const auto atLimit = GetResourcePlan(TableRequest(fitting, userData)); + require(atLimit->info.images.size() == limit && selectedTables(*atLimit) == limit && atLimit->info.sampledPairs.size() == (pairs ? 2u : 1u) * limit, (name + ": reads that fit lost the per-key split").c_str()); + const auto past = TableManyKeyCode(limit + 1u, 0u, pairs); + const auto request = TableRequest(past, userData); + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && plan->info.sampledPairs.size() == (pairs ? 2u : 1u), (name + ": reads past the limit were not merged into one table").c_str()); + const auto merged = columnOf(*plan, plan->info.images[0]); + require(!merged.selected && merged.selection == 0u && !merged.unkeyed && merged.keyDomains.size() == limit + 1u, (name + ": the merged table is not the union of the keys").c_str()); + const auto compiled = CompileTable(request); + const auto& tables = compiled.capture->snapshot.tables; + require(tables.tables.size() == 1u, (name + ": the snapshot does not hold one table").c_str()); + const auto& snapshot = tables.tables[0]; + for (const auto key : {2u, 2u + limit / 2u, 2u + limit}) { + require(key < snapshot.codes.size() && (snapshot.codes[key] & Abi::PoisonFlag) == 0u && snapshot.words.at(snapshot.codes[key]).dwords == heap[key], (name + ": a key does not bind its record").c_str()); + } + require(snapshot.codes.at(1) == outside && snapshot.codes.at(limit + 3u) == outside, (name + ": a key no read selects was bound").c_str()); + if (pairs) return; + const auto split = TableManyKeyCode(40u, 30u, false); + const auto twoHeaps = GetResourcePlan(TableRequest(split, userData)); + const auto unselected = std::find_if(twoHeaps->info.images.begin(), twoHeaps->info.images.end(), [&](const ImageResource& image) { return !columnOf(*twoHeaps, image).selected; }); + require(twoHeaps->info.images.size() == 31u && selectedTables(*twoHeaps) == 30u && unselected != twoHeaps->info.images.end() && columnOf(*twoHeaps, *unselected).keyDomains.size() == 40u, (name + ": the larger heap was not merged first, or the smaller one lost its split").c_str()); +} + +void verifyImageTableSrtKey() { + 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.srt[16] = 2u; + const auto userData = TableUserData(guest); + const auto request = TableRequest(TableSrtKeyCode, userData); + 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->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& 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 && snapshot.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 == 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& movedTables = moved.capture->snapshot.tables; + require(movedTables.tables.at(0).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() { + 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[3] = heap[0]; + guest.materials = {{{0u, 0x101u, 0u, 0u}, {0u, 0u, 0u, 0u}, {0u, 0x203u, 0u, 0u}}}; + FillTableSrt(guest, heap.data(), 32u, 4u); + std::vector code(TableMaterialCode); + const auto shift = std::find(code.begin(), code.end(), 0x8f108510u); + require(shift != code.end(), "image table masked offset: the program has no s_lshl_b32"); + code.insert(shift + 1, {0x8710ff10u, 0x60u}); + const auto userData = TableUserData(guest); + const auto request = TableRequest(code, userData); + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && plan->info.images[0].tableOperation == TableOperation::Supported, "image table masked offset: 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->stride == 32u && column->addend == 0u && column->offset == 0u && column->dwordCount == 8u, "image table masked offset: the column is wrong"); + require(column->keyDomains.empty() && column->unkeyed, "image table masked offset: the masked key was narrowed by its unmasked value"); + 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 && std::none_of(snapshot.codes.begin(), snapshot.codes.end(), [&](std::uint32_t entry) { return entry == outside; }), "image table masked offset: a record was left outside the domain"); + const auto codes = TableCodes(TableMap(*compiled.result), 0); + require(codes.size() == 4u && codes[0] == codes[3] && codes[2] == Abi::NullCode && codes[1] != codes[0] && (codes[1] & Abi::PoisonFlag) == 0u, "image table masked offset: the whole table map is wrong"); +} + +void verifyImageTableRegistration() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture texture; + alignas(4096) static TableGuest guest; + std::fill(guest.heap.begin(), guest.heap.end(), std::array{}); + guest.heap[0] = TableT(texture.bytes.data(), TableFormat32UInt, TableType2D); + guest.heap[1] = TableT(texture.bytes.data(), TableFormat8888UNorm, TableType2D); + FillTableSrt(guest, guest.heap.data(), 32u, 4u); + const auto userData = TableUserData(guest); + 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::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]; + 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); + compareRequest.target.nonConstantImageOffsets = false; + const auto compare = CompileTable(compareRequest); + const auto& compareImage = compare.capture->plan->info.images.at(0); + require(compareImage.table == 0u && compareImage.depthCompare, "image table registration: the compare T# is not a table"); + const auto compareFault = TableMap(*compare.result)[Abi::TableHeader(0) + Abi::TableFault]; + require((compareFault & Abi::PoisonFlag) != 0u && compare.result->imageTablePoison.at(compareFault & ~Abi::PoisonFlag).reason == static_cast(Abi::PoisonReason::UnsupportedOperation), "image table registration: a native compare with a nonconstant offset through a table does not fault on use"); + + guest.heap[0] = {0x00000092u, 0x00fff000u, 0x05500000u, 0u, 0u, 0u, 0u, 0u}; + FillTableSrt(guest, guest.heap.data(), 0u, 64u); + const auto direct = TableT(texture.bytes.data(), TableFormat8888UNorm, TableType2D); + std::copy(direct.begin(), direct.end(), guest.srt.begin() + 4); + const std::vector colorCompareCode{0xf4080100u, 0xfa000000u, 0xf40c0500u, 0xfa000010u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108410u, 0xf4280202u, 0x20000000u, 0xf0bc0108u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + auto colorRequest = TableRequest(colorCompareCode, userData); + const auto colorPlan = GetResourcePlan(colorRequest); + require(colorPlan->info.samplers.size() == 1u && colorPlan->info.samplers[0].table != NoTable && colorPlan->info.images.size() == 1u && colorPlan->info.images[0].table == NoTable, "image table registration: the color compare program does not pair a direct T# with a sampler table"); + AgcDriver::ShaderMemory colorMemory({}); + const auto colorCapture = colorMemory.Capture(colorRequest); + const auto colorRegions = colorMemory.Regions(); + colorRequest.context.memory = colorRegions; + expectFailure([&] { static_cast(Recompile(colorRequest, *colorCapture)); }, "comparison sampling of a color texture through a sampler table is not implemented", "image table registration: a color compare through a sampler table was accepted"); +} + +bool TableTouched(const std::vector& regions, const void* pointer, std::size_t bytes) { + const auto address = static_cast(reinterpret_cast(pointer)); + return std::any_of(regions.begin(), regions.end(), [&](const ShaderRecompiler::MemoryRegion& region) { return region.guestAddress < address + bytes && address < region.guestAddress + region.bytes.size(); }); +} + +void verifyScalarAddressRule() { + namespace Abi = ShaderRecompiler::ImageTableAbi; + require(Abi::ScalarAddressDword(0x1000u, 0x23u, 0x41u) == 0x1060u && Abi::ScalarAddressDword(0x1001u, 0x20u, 0u) == 0x1021u, "scalar address rule: the offset and immediate are not dword-aligned on their own"); + require(Abi::ScalarAddressDword(0x1000u, 0x20u, 0xfffffff0u) == 0x1010u && !Abi::ScalarAddressDword(0x8u, 0x4u, 0xfffffff0u).has_value(), "scalar address rule: a negative immediate is wrong"); + require(!Abi::ScalarAddressDword(0xfffffffffffffff0ull, 0x10u, 0u).has_value() && !Abi::ScalarAddressDword(0xfffffffffffffff0ull, 0x8u, 0x8u).has_value() && Abi::ScalarAddressDword(0xfffffffffffffff0ull, 0x8u, 0x4u) == 0xfffffffffffffffcull, "scalar address rule: a carry past 2^64 is wrong"); + for (const std::uint32_t stride : {4u, 32u, 48u, 0x330u}) { + const std::uint64_t records = Abi::ScalarAddressRecords(stride); + require((records - 1u) * stride <= 0xffffffffull && records * stride > 0xffffffffull, "scalar address rule: the record count does not end at 2^32"); + for (const std::uint32_t maxKey : {0u, 0xffu, 0xffffu, 0x3fffffffu, 0x40000000u, 0xffffffffu}) { + const bool exact = static_cast(maxKey) * stride <= 0xffffffffull; + require(Abi::KeyRecords(maxKey, stride) == (exact ? maxKey + 1u : 0xffffffffu), "scalar address rule: the key bound is wrong"); + } + } +} + +void verifyPointerImageTable() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + constexpr std::uint32_t Palette = 0x40; + constexpr std::uint32_t Entries = 256; + constexpr std::size_t Committed = 0x1f0000; + constexpr std::size_t Reserved = 0x400000; + static TableTexture textures[2]; + void* block = nullptr; +#ifdef _WIN32 + block = VirtualAlloc(nullptr, Reserved, MEM_RESERVE, PAGE_NOACCESS); + require(block != nullptr && VirtualAlloc(block, Committed, MEM_COMMIT, PAGE_READWRITE) == block, "pointer image table: cannot map the root"); +#else + block = mmap(nullptr, Reserved, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + require(block != MAP_FAILED && mprotect(block, Committed, PROT_READ | PROT_WRITE) == 0, "pointer image table: cannot map the root"); +#endif + auto* root = static_cast(block); + const auto base = static_cast(reinterpret_cast(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); + std::copy(outputV.begin(), outputV.end(), root + 4); + const auto pointer = base + Palette + 0x100u; + root[12] = static_cast(pointer); + root[13] = static_cast(pointer >> 32u); + const auto entry = [&](std::uint32_t index, std::array words) { + std::copy(words.begin(), words.end(), root + (Palette + index * 32u) / 4u); + }; + entry(0, TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D)); + entry(1, TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D)); + entry(2, TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D)); + entry(4, {0x1234u, 0x5678u, 0x9abcu, 0x1000ffacu, 0u, 0u, 0u, 0u}); + entry(255, TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D)); + const auto address = static_cast(reinterpret_cast(root)); + const std::array userData{static_cast(address), static_cast(address >> 32u)}; + const auto request = [&](const std::vector& code, std::uint64_t at) { + auto made = TableRequest(code, userData); + made.shader.codeAddress = at; + return made; + }; + + const std::vector maskedCode{0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x7e200500u, 0x8f108210u, + 0xf4200444u, 0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0xf40c0900u, 0x24000040u, 0xf09c0f08u, 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + const auto masked = request(maskedCode, 0x31000u); + const auto plan = GetResourcePlan(masked); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "pointer image table: the T# was not planned as a table"); + const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + require(column.address && column.stride == 32u && column.addend == 0u && column.offset == Palette && column.dwordCount == 8u && column.maxKey == 0xffu && column.keyDomains.empty() && column.unkeyed, "pointer image table: the column is wrong"); + require(plan->descriptorSources.at(column.heapSource).dwordCount == 2u, "pointer image table: the table base is not a 64-bit pointer"); + std::uint32_t planningOnly = 0; + for (const auto& memory : plan->memoryInfo) { + if (memory.kind == ResourceKind::ScalarAddress && memory.offset >= Palette && memory.offset < Palette + 32u && memory.planningOnly) planningOnly++; + } + require(planningOnly == 8u && !plan->info.usesDma, "pointer image table: the descriptor loads still read through BDA"); + + const auto compiled = CompileTable(masked); + const auto& tables = compiled.capture->snapshot.tables; + const auto& snapshot = tables.tables.at(0); + require(snapshot.base == base && snapshot.size == 0u && snapshot.keys == Entries && snapshot.records == Entries && !snapshot.outside && snapshot.fault == 0u, "pointer image table: the column snapshot is wrong"); + require(snapshot.codes[0] == snapshot.codes[1] && snapshot.codes[2] == snapshot.codes[255] && snapshot.codes[0] != snapshot.codes[2] && snapshot.words.at(snapshot.codes[2]).dwords[0] == root[(Palette + 2u * 32u) / 4u], "pointer image table: the snapshot words are wrong"); + require(tables.ranges == std::vector>{{base + Palette, Entries * 32u}}, "pointer image table: the snapshot range is wrong"); + require(snapshot.readRanges == tables.ranges && compiled.result->imageTableReadRanges.size() == 1u && compiled.result->imageTableReadRanges[0] == snapshot.readRanges, "pointer image table: the consumed ranges did not retain table ownership"); + require(TableCovered(compiled.regions, root + Palette / 4u, Entries * 32u) && !TableTouched(compiled.regions, root + (Palette + Entries * 32u) / 4u, Committed - Palette - Entries * 32u), "pointer image table: the captured regions are wrong"); + const auto& map = TableMap(*compiled.result); + const auto codes = TableCodes(map, 0); + require(codes.size() == Entries && codes[0] == codes[1] && codes[2] == codes[255] && codes[3] == Abi::NullCode && map[Abi::HeaderElementCount] == 2u, "pointer image table: the map codes are wrong"); + const auto invalidFlags = map.at(codes[4] + Abi::ImageRecordFlags); + require((invalidFlags & Abi::PoisonFlag) != 0u && compiled.result->imageTablePoison.at(invalidFlags & ~Abi::PoisonFlag).reason == static_cast(Abi::PoisonReason::NotImage), "pointer image table: an invalid entry is not NotImage poison"); + require(compiled.result->imageTableRanges == tables.ranges && map[Abi::TableHeader(0) + Abi::TableBaseLow] == static_cast(base) && map[Abi::TableHeader(0) + Abi::TableBaseHigh] == static_cast(base >> 32u), "pointer image table: the compiled table is wrong"); +#if ANYPS5_ENABLE_SPIRV_TOOLS + static_cast(ValidateAndOptimizeSpirv(compiled.result->spirv, masked.target.vulkanVersion, masked.target.spirvVersion)); +#endif + + const std::vector pointerCode{0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0xf4040500u, 0xfa000030u, + 0x7e200500u, 0x8f108210u, 0xf4200444u, 0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0xf40c090au, 0x241fff00u, 0xf09c0f08u, 0x01090000u, + 0xe0700000u, 0x80070000u, 0xbf810000u}; + const auto loaded = request(pointerCode, 0x32000u); + const auto loadedPlan = GetResourcePlan(loaded); + require(loadedPlan->descriptorSources.at(loadedPlan->info.images.at(0).source).tableColumn->offset == 0xffffff00u, "pointer image table: a negative immediate was not kept"); + const auto loadedTable = CompileTable(loaded); + require(loadedTable.capture->snapshot.tables.tables.at(0).codes == snapshot.codes && loadedTable.capture->snapshot.tables.ranges == tables.ranges, "pointer image table: a base loaded from the SRT with a negative immediate snapshots other records"); + require(loadedTable.result->imageTableReadRanges == compiled.result->imageTableReadRanges, "pointer image table: a negative immediate changed the consumed ranges"); + require(TableCodes(TableMap(*loadedTable.result), 0) == codes, "pointer image table: a loaded base maps other records"); + root[12] = static_cast(pointer) | 2u; + const auto unaligned = CompileTable(loaded); + const auto& unalignedCodes = unaligned.capture->snapshot.tables.tables.at(0).codes; + require(unalignedCodes.size() == Entries && std::all_of(unalignedCodes.begin(), unalignedCodes.end(), [](std::uint32_t code) { return code == Abi::PoisonCode(static_cast(Abi::PoisonReason::Unmapped)); }), "pointer image table: a pointer that is not dword-aligned was read"); + require(unaligned.result->imageTableReadRanges == std::vector>>{{}}, "pointer image table: an unaligned pointer recorded consumed ranges"); + root[12] = static_cast(pointer); + + const std::vector directCode{0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0xf4000440u, 0xfa000038u, 0x8f128511u, 0xf40c0900u, + 0x24000040u, 0xf09c0f08u, 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + root[14] = 2u; + const auto direct = request(directCode, 0x33000u); + const auto directPlan = GetResourcePlan(direct); + require(directPlan->info.images.size() == 1u && directPlan->info.images[0].table == NoTable, "pointer image table: a T# at an offset known at record time is not a direct binding"); + AgcDriver::ShaderMemory directMemory({}); + const auto directCapture = directMemory.Capture(direct); + require(directCapture->snapshot.images.size() == 1u && directCapture->snapshot.images[0].dwords[0] == root[(Palette + 2u * 32u) / 4u] && !TableTouched(directMemory.Regions(), root + (Palette + 3u * 32u) / 4u, Committed - Palette - 3u * 32u), "pointer image table: a T# at an offset known at record time read other records"); + root[14] = 0u; + + const std::vector wideCode{0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x7e220500u, 0x8f128511u, 0xf40c0900u, 0x24000040u, + 0xf09c0f08u, 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + const auto wide = request(wideCode, 0x34000u); + const auto widePlan = GetResourcePlan(wide); + const auto& wideColumn = *widePlan->descriptorSources.at(widePlan->info.images.at(0).source).tableColumn; + require(wideColumn.maxKey == 0xffffffffu && wideColumn.keyDomains.empty() && wideColumn.unkeyed, "pointer image table: a lane key has a bound or a key domain"); + const auto wideTable = CompileTable(wide); + const auto& wideSnapshot = wideTable.capture->snapshot.tables.tables.at(0); + constexpr std::uint32_t mapped = static_cast((Committed - Palette) / 32u); + require(wideSnapshot.keys == Abi::MaxKeys && wideSnapshot.outside && wideTable.result->imageTableFaults != 0u, "pointer image table: an unbounded key did not snapshot the capped records"); + require(std::equal(snapshot.codes.begin(), snapshot.codes.end(), wideSnapshot.codes.begin()) && (wideSnapshot.codes[mapped - 1u] & Abi::PoisonFlag) == 0u, "pointer image table: an unbounded key changed the mapped records"); + require(wideSnapshot.codes[mapped] == Abi::PoisonCode(static_cast(Abi::PoisonReason::Unmapped)) && wideSnapshot.codes.back() == wideSnapshot.codes[mapped], "pointer image table: records past the mapping are not Unmapped poison"); + require(!TableTouched(wideTable.regions, root + Committed / 4u, Reserved - Committed), "pointer image table: an unmapped record was read"); + + auto narrowCode = maskedCode; + narrowCode.erase(narrowCode.begin() + 10, narrowCode.begin() + 12); + keys = {2u, 255u, 0x102u, 2u, 0u, 0u, 0u, 0u}; + const auto narrow = request(narrowCode, 0x35000u); + const auto narrowPlan = GetResourcePlan(narrow); + const auto& narrowColumn = *narrowPlan->descriptorSources.at(narrowPlan->info.images.at(0).source).tableColumn; + require(narrowColumn.keyDomains.size() == 1u && !narrowColumn.unkeyed && narrowColumn.maxKey == 0xffffffffu, "pointer image table: a key read from a V# has no key domain"); + const auto narrowTable = CompileTable(narrow); + const auto& narrowSnapshot = narrowTable.capture->snapshot.tables.tables.at(0); + const auto outsideDomain = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); + require(narrowSnapshot.keys == Abi::MaxKeys && narrowSnapshot.codes[0] == snapshot.codes[0] && narrowSnapshot.codes[2] == snapshot.codes[2] && narrowSnapshot.codes[255] == snapshot.codes[255] && (narrowSnapshot.codes[0x102] & Abi::PoisonFlag) == 0u, "pointer image table: a record in the key domain was not resolved"); + require(narrowSnapshot.codes[1] == outsideDomain && narrowSnapshot.codes[4] == outsideDomain && narrowSnapshot.codes.back() == outsideDomain, "pointer image table: a record outside the key domain is not OutsideDomain poison"); + require(!TableTouched(narrowTable.regions, root + (Palette + 32u) / 4u, 32u) && !TableTouched(narrowTable.regions, root + Committed / 4u, Reserved - Committed), "pointer image table: a record outside the key domain was read"); + const std::vector> narrowReads{{base + Palette, 32u}, {base + Palette + 2u * 32u, 32u}, {base + Palette + 255u * 32u, 32u}, {base + Palette + 0x102u * 32u, 32u}}; + require(narrowSnapshot.readRanges == narrowReads && narrowTable.result->imageTableReadRanges.size() == 1u && narrowTable.result->imageTableReadRanges[0] == narrowReads, "pointer image table: consumed ranges include skipped keys or the key-domain buffer"); + keys = {}; +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + munmap(block, Reserved); +#endif +} + +void verifyPointerTableOffsetCarry() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + constexpr std::size_t Low = 0x10000; + constexpr std::uint64_t High = 0xffff0000ull; + constexpr std::size_t HighBytes = 0x20000; + constexpr std::uint64_t Reserved = High + HighBytes; + static TableTexture textures[2]; + void* block = nullptr; +#ifdef _WIN32 + block = VirtualAlloc(nullptr, static_cast(Reserved), MEM_RESERVE, PAGE_NOACCESS); + require(block != nullptr && VirtualAlloc(block, Low, MEM_COMMIT, PAGE_READWRITE) == block && VirtualAlloc(static_cast(block) + High, HighBytes, MEM_COMMIT, PAGE_READWRITE) == static_cast(block) + High, "pointer table offset carry: cannot map the root"); +#else + block = mmap(nullptr, Reserved, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + require(block != MAP_FAILED && mprotect(block, Low, PROT_READ | PROT_WRITE) == 0 && mprotect(static_cast(block) + High, HighBytes, PROT_READ | PROT_WRITE) == 0, "pointer table offset carry: cannot map the root"); +#endif + auto* root = static_cast(block); + const auto base = static_cast(reinterpret_cast(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); + std::copy(outputV.begin(), outputV.end(), root + 4); + const auto at = [&](std::uint64_t offset, std::array words) { + std::copy(words.begin(), words.end(), root + offset / 4u); + }; + at(0x100000010ull, TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D)); + at(0x50u, TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D)); + 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, 0x8113ff12u, 0xffffff10u, 0x8114ff12u, 0xffffff20u, 0xf4080900u, 0x26000040u, 0xf4080a00u, + 0x28000040u, 0xf09c0f08u, 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + auto request = TableRequest(code, userData); + request.shader.codeAddress = 0x36000u; + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "pointer table 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.address && column.stride == 32u && column.addend == 0xffffff10u && column.offset == 0x40u && column.maxKey == 0xffu && column.foldedAddend == 0xffffff10u && column.foldedCarry == 16u, "pointer table offset carry: the column is wrong"); + const auto compiled = CompileTable(request); + const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); + require(snapshot.keys == 256u && snapshot.codes[7] == Abi::PoisonCode(static_cast(Abi::PoisonReason::OffsetCarry)), "pointer table offset carry: the record whose second load wraps past 32 bits is not OffsetCarry poison"); + require((snapshot.codes[6] & Abi::PoisonFlag) == 0u && (snapshot.codes[8] & Abi::PoisonFlag) == 0u && snapshot.words.at(snapshot.codes[6]).dwords[0] == root[0x100000010ull / 4u] && snapshot.words.at(snapshot.codes[8]).dwords[0] == root[0x50u / 4u] && root[0x100000010ull / 4u] != root[0x50u / 4u], "pointer table offset carry: the records on either side of the carry were not resolved"); + require(!TableTouched(compiled.regions, root + 0x100000030ull / 4u, 0x20u), "pointer table offset carry: the record whose second load wraps was read"); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + munmap(block, Reserved); +#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 = snapshot.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 verifyPointerChainImageTable() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + constexpr std::uint32_t Entries = 256; + static TableTexture textures[2]; + alignas(4096) static std::array root{}; + alignas(4096) static std::array records{}; + alignas(4096) static std::array, 4> links{}; + alignas(4096) static std::array, 3> descriptors{}; + static std::array keys{}; + static std::array output{}; + const auto address = [](const void* pointer) { return static_cast(reinterpret_cast(pointer)); }; + const auto keysV = TableV(keys.data(), 0u, static_cast(sizeof(keys))); + const auto outputV = TableV(output.data(), 0u, static_cast(sizeof(output))); + const auto recordsV = TableV(records.data(), 0u, static_cast(sizeof(records))); + std::copy(keysV.begin(), keysV.end(), root.begin()); + std::copy(outputV.begin(), outputV.end(), root.begin() + 4); + std::copy(recordsV.begin(), recordsV.end(), root.begin() + 12); + const auto store = [](std::uint32_t* at, std::uint64_t pointer) { + at[0] = static_cast(pointer); + at[1] = static_cast(pointer >> 32u); + }; + const auto descriptor0 = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D); + const auto descriptor1 = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D); + std::copy(descriptor0.begin(), descriptor0.end(), descriptors[0].begin() + 8); + std::copy(descriptor0.begin(), descriptor0.end(), descriptors[1].begin() + 8); + std::copy(descriptor1.begin(), descriptor1.end(), descriptors[2].begin() + 8); + for (std::uint32_t record = 0; record < 3u; record++) { + store(records.data() + record * 4u + 2u, address(links[record].data())); + store(links[record].data() + 4u, address(descriptors[record].data())); + } + store(records.data() + 3u * 4u + 2u, address(links[3].data())); + const std::array userData{static_cast(address(root.data())), static_cast(address(root.data()) >> 32u)}; + const std::vector code{0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0xf4080100u, 0xfa000030u, 0x7e200500u, + 0x8f108210u, 0xf4200444u, 0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128411u, 0xf4240b02u, 0x24000008u, 0xf4040b96u, 0xfa000010u, 0xf40c0917u, + 0xfa000020u, 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, "pointer chain image table: a T# behind two pointers was not planned as a table"); + const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + require(column.indirect && !column.address && column.stride == 16u && column.addend == 0u && column.offset == 8u && column.dwordCount == 8u && column.maxKey == 0xffu, "pointer chain image table: the record column is wrong"); + require(column.indirectOffset == 0u && column.indirectImmediate == 0x20u && column.pointerHops == std::vector{{0u, 0x10u}}, "pointer chain image table: the pointer hops are wrong"); + for (const auto& memory : plan->memoryInfo) { + require(memory.kind != ResourceKind::ScalarAddress || memory.planningOnly, "pointer chain image table: a load along the pointer chain still reads through BDA"); + } + require(!plan->info.usesDma, "pointer chain image table: the shader still reads through BDA"); + + const auto compiled = CompileTable(request); + const auto& tables = compiled.capture->snapshot.tables; + const auto& snapshot = tables.tables.at(0); + require(snapshot.keys == Entries && snapshot.codes[0] == snapshot.codes[1] && snapshot.codes[0] != snapshot.codes[2] && (snapshot.codes[0] & Abi::PoisonFlag) == 0u && (snapshot.codes[2] & Abi::PoisonFlag) == 0u, "pointer chain image table: the records behind two pointers do not resolve to one element per distinct T#"); + require(snapshot.words.at(snapshot.codes[0]).dwords[0] == descriptor0[0] && snapshot.words.at(snapshot.codes[2]).dwords[0] == descriptor1[0] && snapshot.words.at(snapshot.codes[2]).dwords[1] == descriptor1[1], "pointer chain image table: the snapshot words are wrong"); + const auto unmapped = Abi::PoisonCode(static_cast(Abi::PoisonReason::Unmapped)); + require(snapshot.codes[3] == unmapped && snapshot.codes[4] == unmapped && snapshot.codes[255] == unmapped, "pointer chain image table: a record whose chain reaches a null pointer is not Unmapped poison"); + for (std::uint32_t record = 0; record < 3u; record++) { + require(TableCovered(compiled.regions, links[record].data() + 4u, 8u) && TableCovered(compiled.regions, descriptors[record].data() + 8u, 32u), "pointer chain image table: the captured regions miss a link or a T#"); + const auto link = address(links[record].data() + 4u); + require(std::ranges::any_of(tables.ranges, [&](const auto& range) { return range.first <= link && link + 8u <= range.first + range.second; }), "pointer chain image table: the snapshot ranges miss a link of the chain"); + } + + store(links[1].data() + 4u, address(descriptors[2].data())); + const auto relinked = CompileTable(request); + const auto& changed = relinked.capture->snapshot.tables.tables.at(0); + require(changed.codes[1] == changed.codes[2] && changed.codes[1] != changed.codes[0], "pointer chain image table: changing the second pointer of a record did not change its T#"); +#if ANYPS5_ENABLE_SPIRV_TOOLS + static_cast(ValidateAndOptimizeSpirv(compiled.result->spirv, request.target.vulkanVersion, request.target.spirvVersion)); +#endif + store(links[1].data() + 4u, address(descriptors[1].data())); +} + +void verifyWaterfallImageTable() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture textures[2]; + alignas(256) static std::array root{}; + static std::array keys{}; + static std::array output{}; + const auto keysV = TableV(keys.data(), 0u, static_cast(sizeof(keys))); + const auto outputV = TableV(output.data(), 0u, static_cast(sizeof(output))); + std::copy(keysV.begin(), keysV.end(), root.begin()); + std::copy(outputV.begin(), outputV.end(), root.begin() + 4); + const auto at = [&](std::uint32_t offset, std::array words) { + std::copy(words.begin(), words.end(), root.begin() + offset / 4u); + }; + at(0x40u + 2u * 32u, TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D)); + at(0x40u + 4u * 32u, TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D)); + const auto base = static_cast(reinterpret_cast(root.data())); + const std::array userData{static_cast(base), static_cast(base >> 32u)}; + const std::vector code{0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x7e220500u, 0x8f128511u, 0xf40c0900u, 0x24000040u, + 0xf09c0f08u, 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + auto request = TableRequest(code, userData); + request.shader.codeAddress = 0x38000u; + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "waterfall image table: the T# was not planned as a table"); + const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + require(column.address && column.stride == 32u && column.offset == 0x40u && column.dwordCount == 8u && column.unkeyed, "waterfall image table: the column is wrong"); + const auto compiled = CompileTable(request); + const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); + require(snapshot.keys == Abi::MaxKeys && (snapshot.codes[2] & Abi::PoisonFlag) == 0u && (snapshot.codes[4] & Abi::PoisonFlag) == 0u, "waterfall image table: the snapshot failed"); + auto boundedCode = code; + boundedCode.insert(boundedCode.begin() + 4, {0x7d8a0080u, 0x7e020284u, 0x02000282u}); + auto boundedRequest = TableRequest(boundedCode, userData); + boundedRequest.shader.codeAddress = 0x39000u; + const auto boundedPlan = GetResourcePlan(boundedRequest); + require(boundedPlan->info.images.size() == 1u && boundedPlan->info.images[0].table == 0u, "waterfall image table: bounded key was not planned as a table"); + const auto& boundedColumn = *boundedPlan->descriptorSources.at(boundedPlan->info.images[0].source).tableColumn; + require(boundedColumn.maxKey == 6u, "waterfall image table: bounded key maxKey is wrong"); + const auto boundedCompiled = CompileTable(boundedRequest); + const auto& boundedSnapshot = boundedCompiled.capture->snapshot.tables.tables.at(0); + require(boundedSnapshot.keys == 7u && boundedSnapshot.keys < Abi::MaxKeys && (boundedSnapshot.codes[2] & Abi::PoisonFlag) == 0u && (boundedSnapshot.codes[4] & Abi::PoisonFlag) == 0u, "waterfall image table: bounded key snapshot failed"); + IrResourcePlan planA; + std::vector> values; + const auto make = [&](IrOpcode opcode, IrType type, std::initializer_list arguments = {}) -> IrValue& { + values.push_back(std::make_unique(opcode, type, static_cast(values.size()))); + for (auto* argument : arguments) values.back()->AddArgument(argument); + return *values.back(); + }; + const auto constant = [&](std::uint32_t val) -> IrValue& { + auto& leaf = make(IrOpcode::Void, IrType::U32); + leaf.SetImmediateU32(val); + return leaf; + }; + auto& scalar = make(IrOpcode::Void, IrType::ScalarReg); + scalar.SetRegister({RegisterBank::Scalar, 0}); + auto& vector = make(IrOpcode::Void, IrType::VectorReg); + vector.SetRegister({RegisterBank::Vector, 0}); + auto& input = make(IrOpcode::GetUserData, IrType::U32, {&scalar}); + auto& lane = make(IrOpcode::GetRegister, IrType::U32, {&vector}); + auto& condA = make(IrOpcode::IEqual32, IrType::Bool, {&input, &constant(4)}); + auto& condB = make(IrOpcode::IEqual32, IrType::Bool, {&input, &lane}); + auto& conjMask = make(IrOpcode::LogicalAnd, IrType::Bool, {&condB, &condA}); + auto& selectConj = make(IrOpcode::SelectU32, IrType::U32, {&condB, &input, &lane}); + auto& firstConj = make(IrOpcode::ReadFirstLane, IrType::U32, {&selectConj, &conjMask}); + require(SrtWalker().ValidateRuntimeValue(planA, &firstConj), "waterfall image table: conjunct select was refused by validator"); + planA.srtPlanComplete = true; + planA.descriptorSources.push_back({}); + planA.descriptorSources.back().dwordCount = 1; + planA.descriptorSources.back().dwords[0] = &firstConj; + planA.materializationSources.push_back(0); + const std::array userDataA{42u}; + SrtRuntime runtimeA{userDataA}; + const auto* tapeA = Detail::SrtTape::For(planA); + require(tapeA != nullptr, "waterfall image table: tape was not built for conjunct select"); + std::vector resultsA; + std::vector flatA; + std::vector activeA; + require(tapeA->Run(planA, runtimeA, resultsA, flatA, activeA), "waterfall image table: tape failed to run conjunct select"); + require(!resultsA.empty() && resultsA[0].dwords[0] == 42u, "waterfall image table: tape evaluated conjunct select to wrong value"); + auto& otherCond = make(IrOpcode::IEqual32, IrType::Bool, {&lane, &constant(5)}); + auto& selectOther = make(IrOpcode::SelectU32, IrType::U32, {&otherCond, &input, &lane}); + auto& firstOther = make(IrOpcode::ReadFirstLane, IrType::U32, {&selectOther, &conjMask}); + require(!SrtWalker().ValidateRuntimeValue(planA, &firstOther), "waterfall image table: non-implied condition was accepted by validator"); +} + +void verifySplitWaterfallImageTable() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture texture; + alignas(256) static std::array root{}; + static std::array output{}; + const auto outputV = TableV(output.data(), 0u, static_cast(sizeof(output))); + std::copy(outputV.begin(), outputV.end(), root.begin() + 4); + const auto descriptor = TableT(texture.bytes.data(), TableFormat8888UNorm, TableType2D); + std::copy(descriptor.begin(), descriptor.end(), root.begin() + 0x40u / 4u); + const auto base = static_cast(reinterpret_cast(root.data())); + const std::array userData{static_cast(base), static_cast(base >> 32u)}; + const auto code = [](std::uint32_t addend) { + return std::vector{0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x7da80086u, 0x7e220500u, 0xd7460001u, 0x00010b00u | (addend << 18u), 0x7e260501u, 0x8f128511u, + 0xf4080900u, 0x24000040u, 0xf4080a00u, 0x26000040u, 0xf09c0f08u, 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + }; + const auto split = code(0x90u); + auto request = TableRequest(split, userData); + request.shader.codeAddress = 0x3a000u; + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "split waterfall image table: the T# was not planned as a table"); + const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + require(column.address && column.stride == 32u && column.offset == 0x40u && column.dwordCount == 8u && column.foldedCarry == 16u, "split waterfall image table: the column is wrong"); + const auto compiled = CompileTable(request); + const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); + require((snapshot.codes[0] & Abi::PoisonFlag) == 0u && snapshot.words.at(snapshot.codes[0]).dwords == descriptor, "split waterfall image table: record 0 was not read whole"); + for (const std::uint32_t addend : {0xa0u, 0x88u}) { + const auto otherCode = code(addend); + auto other = TableRequest(otherCode, userData); + other.shader.codeAddress = 0x3b000u + addend * 0x100u; + expectFailure([&] { static_cast(GetResourcePlan(other)); }, "GetImageResource dword 0 is not a valid runtime value", "split waterfall image table: a second half outside the record was accepted"); + } +} void verifyDescriptorPhis() { using namespace ShaderRecompiler; @@ -1169,7 +3653,8 @@ void verifyDescriptorPhis() { require(plan->info.images.size() == images && plan->info.samplers.size() == samplers && plan->info.sampledPairs.size() == 2u, "descriptor Phi: the edges were not given one resource each"); AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(request); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; const auto compiled = Recompile(request, *capture); require(countOps(compiled->spirv, OpImageSampleExplicitLod) == 2u, "descriptor Phi: the specialized SPIR-V does not sample once per edge"); #if ANYPS5_ENABLE_SPIRV_TOOLS @@ -1200,7 +3685,8 @@ void verifyDescriptorPhis() { auto twoLane = makeRequest(samplerCode, 64u); AgcDriver::ShaderMemory twoLaneMemory({}); const auto twoLaneCapture = twoLaneMemory.Capture(twoLane); - twoLane.context.memory = twoLaneMemory.Regions(); + const auto twoLaneRegions = twoLaneMemory.Regions(); + twoLane.context.memory = twoLaneRegions; require(countOps(Recompile(twoLane, *twoLaneCapture)->spirv, OpImageSampleExplicitLod) == 4u, "descriptor Phi: the specialized two-lane SPIR-V does not sample once per edge and half"); auto dynamic = makeRequest(dynamicCode); @@ -1222,9 +3708,25 @@ void verifyDescriptorPhis() { require(sequentialPlan->info.images.size() == 2u && sequentialPlan->info.samplers.size() == 2u, "descriptor Phi: a T# and an S# chosen in separate blocks were not split into their SRT descriptors"); AgcDriver::ShaderMemory sequentialMemory({}); const auto sequentialCapture = sequentialMemory.Capture(sequential); - sequential.context.memory = sequentialMemory.Regions(); + const auto sequentialRegions = sequentialMemory.Regions(); + sequential.context.memory = sequentialRegions; static_cast(Recompile(sequential, *sequentialCapture)); + const auto planRestoredSampler = [&](std::span code, const char* message) { + auto request = makeRequest(code); + const auto plan = GetResourcePlan(request); + require(plan->info.samplers.size() == 1u && plan->info.sampledPairs.size() == 1u, message); + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; + static_cast(Recompile(request, *capture)); + }; + std::vector restoredSamplerCode{0xf40c0200u, 0xfa000000u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbe930380u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20400u, 0xbe900380u, 0xbe910380u, 0xbe9c0414u, 0xbe9e0416u, 0xbf068011u, 0xbf850001u, 0x80128112u, 0xbe94041cu, 0xbe96041eu, 0x80118111u, 0xbf0a8211u, 0xbf85fff6u, 0xf09c0f08u, 0x00a20000u, 0x80108110u, 0xbf0a8210u, 0xbf85fff0u, 0x80138113u, 0xbf0a8213u, 0xbf85ffecu, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xe0700000u, 0x80060400u, 0x7d880090u, 0xbea23c6au, 0x4a020081u, 0xe0700000u, 0x80060100u, 0xbf810000u}; + planRestoredSampler(restoredSamplerCode, "descriptor Phi: an S# copied out and back in three nested loops was planned as more than one sampler"); + restoredSamplerCode.erase(restoredSamplerCode.end() - 6, restoredSamplerCode.end() - 1); + planRestoredSampler(restoredSamplerCode, "descriptor Phi: an S# copied out and back in three nested loops without a masked write was planned as more than one sampler"); + const auto entryWrites = [&](std::initializer_list words) { auto code = loopCode; code.insert(code.begin() + 8, words); @@ -1322,12 +3824,16 @@ void verifyUniformTableImage() { request.target.fragmentShaderBarycentricEnabled = false; request.layout.pushConstantSizeBytes = 128; const auto plan = GetResourcePlan(request); - require(plan->info.images.size() == 1u && !plan->descriptorSources.at(plan->info.images[0].source).indirectImage.has_value(), "uniform table image: the T# read from a constant buffer was not planned as one image"); + 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 && 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); - require(capture->snapshot.images.size() == 1u && std::equal(expected.begin(), expected.end(), capture->snapshot.images[0].dwords.begin()), "uniform table image: the T# of the record the user data selects was not read"); + const auto& tables = capture->snapshot.tables; + 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"); } } @@ -1387,7 +3893,8 @@ void verifyLaneSelectedDescriptors() { require(plan->info.images.size() == 2u && plan->info.samplers.size() == 1u, "lane-selected T#: the T#s selected per lane were not planned as one image each"); AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(request); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; static_cast(Recompile(request, *capture)); const auto holds = [&](const std::array& words) { return std::ranges::any_of(capture->snapshot.images, [&](const DescriptorValue& value) { @@ -1433,7 +3940,8 @@ void verifyProgramCounterRelativeData() { }; AgcDriver::ShaderMemory memory({}); static_cast(memory.Capture(request)); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; const auto first = Recompile(request); require(dataBase(first) == codeAddress + 56u, "program counter data: the V# does not name the data at the shader's address"); auto relocated = request; @@ -1581,6 +4089,18 @@ void verifyHalfWaveReduction() { {0u, 0x38000000u, 0x88000000u, spv::OpGroupNonUniformBitwiseOr, "half-wave reduction: the wave64 OR scan did not read lane 31 as a subgroup OR on 32 lanes"}, }}; for (const auto& reduction : scans) require(reductions(pixel(scan(reduction.identity, reduction.vector, reduction.scalar), 32u, true), reduction.reduce) == 1u, reduction.message); + auto loopScan = scan(0xffffffffu, 0x26000000u, 0x83800000u); + const auto savedMaskLoop = AgcTest::SavedExecMaskLoop(8u); + loopScan.insert(loopScan.begin(), savedMaskLoop.begin(), savedMaskLoop.end()); + auto helperLoop = loopScan; + helperLoop[0] = 0xbea204c1u; + expectFailure([&] { static_cast(pixel(helperLoop, 32u, true)); }, "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup", "half-wave reduction: a loop initialized with helper lanes was proven to exclude them"); + auto upperHelperLoop = loopScan; + upperHelperLoop[0] = 0xbea2037eu; + upperHelperLoop.insert(upperHelperLoop.begin() + 1, 0xbea303c1u); + expectFailure([&] { static_cast(pixel(upperHelperLoop, 32u, true)); }, "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup", "half-wave reduction: the low-word loop proof was reused for helper-inclusive upper lanes"); + require(reductions(pixel(loopScan, 32u, true), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: a loop-carried saved EXEC mask exhausted the entry-lane proof on 32 lanes"); + require(halves(pixel(loopScan, 64u, true), spv::OpGroupNonUniformUMin, 0xffffffffu), "half-wave reduction: a loop-carried saved EXEC mask did not preserve the two half-wave UMin reductions"); auto comparedScan = scan(0u, 0x38000000u, 0x88000000u); *std::find(comparedScan.begin(), comparedScan.end(), 0xbe98047eu) = 0x8a986a7eu; comparedScan.insert(comparedScan.begin(), 0x7c080080u); @@ -1698,6 +4218,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}; @@ -1922,6 +4481,28 @@ void verifyMeshConfiguration() { RecompileCacheKey::Build(vertex, unclipped); require(clipped != unclipped, "PA_CL_VS_OUT_CNTL is not part of the recompile cache key"); } + { + RecompileRequest fragment{}; + fragment.shader = {ShaderStage::Fragment, 0x10000u, code, 0, {}}; + fragment.context.waveSize = 64; + ShaderPixelStageInfo info{}; + info.quadPixelMask = 0x2u; + fragment.context.pixel = info; + const auto back = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(fragment)); + require(back.request.context.pixel.has_value() && back.request.context.pixel->quadPixelMask == 0x2u, "the quad pixel mask was lost in serialization"); + std::vector masked; + RecompileCacheKey::Build(fragment, masked); + fragment.context.pixel->quadPixelMask = 0xfu; + std::vector full; + RecompileCacheKey::Build(fragment, full); + require(masked != full, "the quad pixel mask is not part of the recompile cache key"); + fragment.context.pixel->quadSampleMask = {0x1u, 0x2u, 0x0u, 0xffffu}; + const auto sampled = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(fragment)); + require(sampled.request.context.pixel.has_value() && sampled.request.context.pixel->quadSampleMask == std::array{0x1u, 0x2u, 0x0u, 0xffffu}, "the quad sample masks were lost in serialization"); + std::vector partial; + RecompileCacheKey::Build(fragment, partial); + require(partial != full, "the quad sample masks are not part of the recompile cache key"); + } std::vector key; RecompileCacheKey::Build(request, key); const auto first = key; @@ -1931,6 +4512,16 @@ 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"); + for (const auto primitiveSize : {1u, 2u}) { + otherMesh = mesh; + otherMesh.outputPrimitiveSize = primitiveSize; + other.graphics = GraphicsCompileContext{0u, {}, otherMesh, std::nullopt, {}}; + const auto primitiveReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(other)); + require(primitiveReplay.request.graphics.has_value() && primitiveReplay.request.graphics->mesh.has_value() && primitiveReplay.request.graphics->mesh->outputPrimitiveSize == primitiveSize, "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; @@ -1958,16 +4549,31 @@ void verifyMeshConfiguration() { RecompileCacheKey::Build(primitiveGrouped, key); require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the primitive group size, which rebuilds their relative patch index"); } - auto shared = hull; - shared.graphics->tessellation->breakAtInstance = false; + auto instanced = hull; + instanced.graphics->tessellation->breakAtInstance = false; + instanced.graphics->draw.instanceCount = 2u; + instanced.graphics->draw.indexCount = 12u; + auto longer = instanced; + longer.graphics->draw.indexCount = 16u; + auto breaking = instanced; + breaking.graphics->tessellation->breakAtInstance = true; for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) { hull.shader.stage = stage; - shared.shader.stage = stage; + instanced.shader.stage = stage; + longer.shader.stage = stage; + breaking.shader.stage = stage; RecompileCacheKey::Build(hull, key); - const auto patchKey = key; - RecompileCacheKey::Build(shared, key); - require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on BREAK_WAVE_AT_EOI, which decides whether their relative patch index can be rebuilt"); + const auto single = key; + RecompileCacheKey::Build(instanced, key); + const auto shared = key; + RecompileCacheKey::Build(longer, key); + require((shared != single && key != shared) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the patches per instance of an instanced draw"); + RecompileCacheKey::Build(breaking, key); + require(key == single, "an instanced draw whose instances start hull threadgroups (BREAK_WAVE_AT_EOI) changed the cache key"); } + breaking.shader.stage = ShaderStage::TessellationControl; + const auto breakingReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(breaking)); + require(breakingReplay.request.graphics && breakingReplay.request.graphics->tessellation && breakingReplay.request.graphics->tessellation->breakAtInstance, "BREAK_WAVE_AT_EOI was lost in serialization"); const std::array users{}; request.context.waveSize = 32u; request.context.userData = users; @@ -2187,10 +4793,10 @@ void verifyPixelRequestSerialization() { const auto fields = [](const ShaderPixelStageInfo& value) { return std::tie(value.interpolatorCount, value.interpolatorSettings, value.wave32, value.inputAddr, value.hasPerspectiveCenterVgpr, value.perspectiveCentroid, value.posX, value.posY, - value.posZ, value.posW, value.frontFace, value.ancillary, value.sampleShading, + value.posZ, value.posW, value.frontFace, value.frontFaceAllBits, value.ancillary, value.sampleShading, value.noPerspective, value.linearCentroid, value.pixelKillEnable, value.depthExportEnable, value.sampleMaskExportEnable, value.earlyZ, value.executeOnNoop, value.conservativeZExport, value.orderedPixelShader, - value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable, value.rasterPrimitive, value.layerExported, value.ditheredAlphaToCoverage); + value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable, value.rasterPrimitive, value.layerExported, value.ditheredAlphaToCoverage, value.reversedBlend); }; const std::array code{0xbf810000u}; const std::array userData{0x12345678u, 0xabcdef01u, 0x87654321u}; @@ -2219,6 +4825,7 @@ void verifyPixelRequestSerialization() { .posZ = true, .posW = true, .frontFace = true, + .frontFaceAllBits = true, .ancillary = true, .sampleShading = true, .noPerspective = true, @@ -2236,6 +4843,7 @@ void verifyPixelRequestSerialization() { pixel.alphaToCoverage = true; pixel.stencilRefExportEnable = true; pixel.ditheredAlphaToCoverage = true; + pixel.reversedBlend = 0x570001u; const std::array, 3> mappings{{ {0x00u, 0xe4u, 0xc6u, 0x1bu, 0xffu, 0x80u, 0x55u, 0xaau}, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}, @@ -2264,6 +4872,10 @@ void verifyPixelRequestSerialization() { undithered.context.pixel->ditheredAlphaToCoverage = false; RecompileCacheKey::Build(undithered, replayKey); require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(undithered), "dithered coverage did not change shader identity"); + auto withoutReversedBlend = request; + withoutReversedBlend.context.pixel->reversedBlend = 0u; + RecompileCacheKey::Build(withoutReversedBlend, replayKey); + require(key == replayKey && RecompileCacheKey::ContextHash(request) == RecompileCacheKey::ContextHash(withoutReversedBlend), "the reversed-order blend code changed the static shader identity"); auto withoutCoverage = request; withoutCoverage.context.pixel->alphaToCoverage = false; RecompileCacheKey::Build(withoutCoverage, replayKey); @@ -2286,7 +4898,13 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQRkAAAA=", "new requests did not use serialization version 25"); + require(requestPrefix(encoded, 8u) == "NVNQQSsAAAA=", "new requests did not use serialization version 43"); + const std::array fallbacks{0x40u, 0x1ac4u}; + auto withFallbacks = minimal; + withFallbacks.context.linkedCallFallbacks = fallbacks; + const auto fallbackRequest = serializer.Deserialize(serializer.Serialize(withFallbacks)); + require(std::ranges::equal(fallbackRequest.request.context.linkedCallFallbacks, fallbacks), "the linked call fallbacks did not round-trip"); + require(serializer.Deserialize(encoded).request.context.linkedCallFallbacks.empty(), "a request without linked call fallbacks gained some"); require(serializer.Deserialize(encoded).request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && serializer.Deserialize(encoded).request.context.pixel->ditheredAlphaToCoverage, "the pixel primitive class or dither mode did not round-trip"); require(serializer.Deserialize(encoded).request.context.pixel->layerExported, "the pixel layer export did not round-trip"); const auto legacy16 = serializer.Deserialize( @@ -2395,10 +5013,10 @@ void verifyPixelRequestSerialization() { require(RecompileCacheKey::ContextHash(legacy19.request) == RecompileCacheKey::ContextHash(upgraded19.request), "upgrading version19 changed shader identity"); } constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; - for (std::size_t bytes = 0; bytes < 23u; ++bytes) { - expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export or dither flag was accepted"); + for (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=", "NVNQQRoAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSwAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -2409,6 +5027,7 @@ void verifyPixelRequestSerialization() { "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAB0gQAAAE="}; const auto version18 = serializer.Deserialize(version18Request); require(version18.request.context.pixel.has_value() && version18.request.context.pixel->rasterPrimitive == RasterPrimitive::Triangle, "a version 18 request read a primitive class"); + require(version18.request.context.pixel->reversedBlend == 0u, "a version 18 request read a reversed-order blend code"); require(version18.request.target.registrationModules && version18.request.target.registrationWordLimit == 1234u && version18.request.target.vertexSubgroups, "a version 18 request trailer was misread"); } @@ -2488,6 +5107,9 @@ 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.pop_back(); + version20Vertex.pop_back(); version20Vertex[4] = 20u; const auto replay20Vertex = serializer.Deserialize(encodeBase64(version20Vertex)); require(replay20Vertex.request.context.vertex.has_value() && replay20Vertex.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "a main version20 request lost PA_CL_VS_OUT_CNTL"); @@ -2496,6 +5118,9 @@ 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.pop_back(); + legacy.pop_back(); legacy.erase(legacy.begin() + offset, legacy.begin() + offset + 4u); for (const auto version : {16u, 17u, 18u, 19u}) { legacy[4] = static_cast(version); @@ -2512,21 +5137,257 @@ void verifyVertexInfoVersion19() { auto exportedPixel = pixel; exportedPixel.context.pixel->layerExported = true; const auto exportedBytes = decodeBase64(serializer.Serialize(exportedPixel)); - auto legacyPixel = decodeBase64(encodedPixel); + auto blended = pixel; + blended.context.pixel->reversedBlend = 0x89abcdefu; + const auto plainPixel = decodeBase64(encodedPixel); + const auto blendedPixel = decodeBase64(serializer.Serialize(blended)); + require(plainPixel.size() == blendedPixel.size() && plainPixel.size() == exportedBytes.size(), "the layer export flag or the reversed-order blend code changed the request size"); std::size_t flagOffset = 0; - while (flagOffset < legacyPixel.size() && legacyPixel[flagOffset] == exportedBytes[flagOffset]) ++flagOffset; - require(flagOffset < legacyPixel.size(), "the pixel layer export flag is not serialized"); - legacyPixel.erase(legacyPixel.begin() + static_cast(flagOffset), legacyPixel.begin() + static_cast(flagOffset + 2u)); + while (flagOffset < plainPixel.size() && plainPixel[flagOffset] == exportedBytes[flagOffset]) ++flagOffset; + require(flagOffset < plainPixel.size(), "the pixel layer export flag is not serialized"); + std::size_t blendOffset = 0; + while (blendOffset < plainPixel.size() && plainPixel[blendOffset] == blendedPixel[blendOffset]) ++blendOffset; + require(blendOffset + 4u <= plainPixel.size() && blendedPixel[blendOffset] == 0xefu && blendedPixel[blendOffset + 1u] == 0xcdu && blendedPixel[blendOffset + 2u] == 0xabu && blendedPixel[blendOffset + 3u] == 0x89u, "the reversed-order blend code is not the four bytes the version 30 pixel info adds"); + require(std::equal(plainPixel.begin() + static_cast(blendOffset + 4u), plainPixel.end(), blendedPixel.begin() + static_cast(blendOffset + 4u)), "the reversed-order blend code moved the fields after it"); + auto allBitsPixel = pixel; + allBitsPixel.context.pixel->frontFaceAllBits = true; + const auto allBitsBytes = decodeBase64(serializer.Serialize(allBitsPixel)); + std::size_t allBitsOffset = 0; + while (allBitsOffset < plainPixel.size() && plainPixel[allBitsOffset] == allBitsBytes[allBitsOffset]) ++allBitsOffset; + require(allBitsOffset == flagOffset + 2u, "the front-face all-bits flag does not follow the dithered coverage flag"); + require(blendOffset == allBitsOffset + 18u, "the reversed-order blend code is not serialized after the front-face all-bits flag, quad pixel mask and quad sample masks"); + constexpr std::size_t sampleMaskBytes = 16u; + constexpr std::size_t iterationBytes = 35u; + constexpr std::size_t sampleOrderBytes = 16u; + const auto orderOffset = blendOffset + 4u + iterationBytes; + auto ordered = pixel; + ordered.context.pixel->sampleOrder = {0x76543210u, 0x76543210u, 0x76543201u, 0x76543201u}; + const auto orderedBytes = decodeBase64(serializer.Serialize(ordered)); + require(orderedBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + static_cast(orderOffset), orderedBytes.begin()) && orderedBytes[orderOffset] == 0x10u && orderedBytes[orderOffset + 11u] == 0x76u && orderedBytes[orderOffset + 12u] == 0x01u && std::equal(plainPixel.begin() + static_cast(orderOffset + sampleOrderBytes), plainPixel.end(), orderedBytes.begin() + static_cast(orderOffset + sampleOrderBytes)), "the sample order is not the four words the version 42 pixel info adds after the stencil operation value export flag"); + auto orderedVersion34 = orderedBytes; + orderedVersion34.pop_back(); + orderedVersion34.pop_back(); + orderedVersion34.pop_back(); + orderedVersion34.erase(orderedVersion34.begin() + static_cast(orderOffset - 1u), orderedVersion34.begin() + static_cast(orderOffset + sampleOrderBytes)); + orderedVersion34[4] = 34u; + const auto orderedReplay34 = serializer.Deserialize(encodeBase64(orderedVersion34)); + require(orderedReplay34.request.context.pixel.has_value() && orderedReplay34.request.context.pixel->sampleOrder == std::array{} && serializer.Serialize(orderedReplay34.request) == encodedPixel, "a version 34 request read a sample order, or misread the fields after its pixel iteration fields"); + auto orderedVersion30 = orderedBytes; + orderedVersion30.resize(orderedVersion30.size() - 4u); + orderedVersion30.erase(orderedVersion30.begin() + static_cast(blendOffset + 4u), orderedVersion30.begin() + static_cast(orderOffset + sampleOrderBytes)); + orderedVersion30[4] = 30u; + const auto orderedReplay30 = serializer.Deserialize(encodeBase64(orderedVersion30)); + require(orderedReplay30.request.context.pixel.has_value() && orderedReplay30.request.context.pixel->sampleOrder == std::array{} && serializer.Serialize(orderedReplay30.request) == encodedPixel, "a version 30 request read a sample order, or misread the fields after its reversed-order blend code"); + auto orderedSpanned = ordered; + orderedSpanned.target.spannedWaves = true; + auto orderedVersion32 = decodeBase64(serializer.Serialize(orderedSpanned)); + orderedVersion32.pop_back(); + require(orderedVersion32.size() > 2u && orderedVersion32.back() == 0u && orderedVersion32[orderedVersion32.size() - 2u] == 0u && orderedVersion32[orderedVersion32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); + orderedVersion32.pop_back(); + orderedVersion32.pop_back(); + orderedVersion32.erase(orderedVersion32.begin() + static_cast(blendOffset + 4u), orderedVersion32.begin() + static_cast(orderOffset + sampleOrderBytes)); + orderedVersion32[4] = 32u; + const auto orderedReplay32 = serializer.Deserialize(encodeBase64(orderedVersion32)); + auto unordered = pixel; + unordered.target.spannedWaves = true; + require(orderedReplay32.request.context.pixel.has_value() && orderedReplay32.request.context.pixel->sampleOrder == std::array{} && orderedReplay32.request.target.spannedWaves && serializer.Serialize(orderedReplay32.request) == serializer.Serialize(unordered), "a version 32 request read a sample order or lost its spanned-waves flag"); + auto maskedPixel = pixel; + maskedPixel.context.pixel->quadPixelMask = 0x2u; + const auto maskedBytes = decodeBase64(serializer.Serialize(maskedPixel)); + std::size_t maskOffset = 0; + while (maskOffset < plainPixel.size() && plainPixel[maskOffset] == maskedBytes[maskOffset]) ++maskOffset; + require(maskOffset == allBitsOffset + 1u && maskOffset < plainPixel.size(), "the quad pixel mask is not the byte after the front-face all-bits flag"); + auto sampledPixel = maskedPixel; + sampledPixel.context.pixel->quadSampleMask = {0x1u, 0x2u, 0x4u, 0x8u}; + 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 version30 = sampledBytes; + version30.pop_back(); + version30.pop_back(); + version30.pop_back(); + version30.pop_back(); + version30.erase(version30.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u), version30.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); + version30[4] = 30u; + const auto replay30 = serializer.Deserialize(encodeBase64(version30)); + require(replay30.request.context.pixel.has_value() && !replay30.request.context.pixel->iteratesSamples && replay30.request.context.pixel->positionLocation == PixelPositionLocation::Center && replay30.request.context.pixel->sampleLocations == std::array{} && decodeBase64(serializer.Serialize(replay30.request)) == sampledBytes, "a version 30 request read pixel iteration, a position location or sample locations, or misread the fields after its reversed-order blend code"); + auto spanned = sampledPixel; + spanned.target.spannedWaves = true; + auto version32 = decodeBase64(serializer.Serialize(spanned)); + version32.pop_back(); + require(version32.size() > 2u && version32.back() == 0u && version32[version32.size() - 2u] == 0u && version32[version32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); + version32.pop_back(); + version32.pop_back(); + version32.erase(version32.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u), version32.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); + version32[4] = 32u; + const auto replay32 = serializer.Deserialize(encodeBase64(version32)); + require(replay32.request.context.pixel.has_value() && !replay32.request.context.pixel->iteratesSamples && replay32.request.context.pixel->sampleLocations == std::array{} && replay32.request.target.spannedWaves && serializer.Serialize(replay32.request) == serializer.Serialize(spanned), "a version 32 request read pixel iteration or sample locations, or lost its spanned-waves flag"); + auto version29 = sampledBytes; + version29.pop_back(); + version29.pop_back(); + version29.pop_back(); + version29.pop_back(); + version29.erase(version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); + auto version36 = sampledBytes; + version36.pop_back(); + version36.erase(version36.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u), version36.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); + version36[4] = 36u; + const auto replay36 = serializer.Deserialize(encodeBase64(version36)); + require(replay36.request.context.pixel.has_value() && !replay36.request.context.pixel->stencilOperationValueExport && decodeBase64(serializer.Serialize(replay36.request)) == sampledBytes, "a version 36 request read a stencil operation value export or misread the fields after the sample locations"); + version29[4] = 29u; + const auto replay29 = serializer.Deserialize(encodeBase64(version29)); + require(replay29.request.context.pixel.has_value() && replay29.request.context.pixel->quadSampleMask == std::array{0x1u, 0x2u, 0x4u, 0x8u} && replay29.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay29.request)) == sampledBytes, "a version 29 request read a reversed-order blend code or misread the fields after the quad sample masks"); + auto version28 = sampledBytes; + version28.pop_back(); + version28.pop_back(); + version28.pop_back(); + version28.pop_back(); + version28.erase(version28.begin() + static_cast(maskOffset + 1u), version28.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); + version28[4] = 28u; + const auto replay28 = serializer.Deserialize(encodeBase64(version28)); + require(replay28.request.context.pixel.has_value() && replay28.request.context.pixel->quadPixelMask == 0x2u && replay28.request.context.pixel->quadSampleMask == std::array{0xffffu, 0xffffu, 0xffffu, 0xffffu} && replay28.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or a reversed-order blend code, or misread the fields after them"); + auto version27 = maskedBytes; + version27.pop_back(); + version27.pop_back(); + version27.pop_back(); + version27.pop_back(); + version27.erase(version27.begin() + static_cast(maskOffset), version27.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); + version27[4] = 27u; + const auto replay27 = serializer.Deserialize(encodeBase64(version27)); + require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && replay27.request.context.pixel->reversedBlend == 0u && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or reversed-order blend code, or misread the fields after it"); + auto legacyPixel = plainPixel; + legacyPixel.pop_back(); + legacyPixel.pop_back(); + legacyPixel.pop_back(); + legacyPixel.pop_back(); + legacyPixel.erase(legacyPixel.begin() + static_cast(allBitsOffset), legacyPixel.begin() + static_cast(allBitsOffset + 2u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); + legacyPixel[4] = 26u; + const auto replay26 = serializer.Deserialize(encodeBase64(legacyPixel)); + require(replay26.request.context.pixel.has_value() && replay26.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replay26.request.context.pixel->frontFaceAllBits && replay26.request.context.pixel->reversedBlend == 0u, "a version 26 request lost its pixel primitive class or read a front-face all-bits flag"); + require(serializer.Serialize(replay26.request) == encodedPixel, "a version 26 request misread the fields after its dithered coverage flag"); + legacyPixel.resize(legacyPixel.size() - sizeof(std::uint64_t)); + legacyPixel[4] = 25u; + const auto replay25 = serializer.Deserialize(encodeBase64(legacyPixel)); + require(replay25.request.context.pixel.has_value() && !replay25.request.context.pixel->layerExported && !replay25.request.context.pixel->ditheredAlphaToCoverage && replay25.request.context.pixel->reversedBlend == 0u, "a main version 25 request read a reversed-order blend code"); + require(serializer.Serialize(replay25.request) == encodedPixel, "a main version 25 request misread the fields after its dither flag"); + legacyPixel.erase(legacyPixel.begin() + static_cast(flagOffset + 1u)); + for (const auto version : {23u, 24u}) { + legacyPixel[4] = static_cast(version); + const auto replayPixel = serializer.Deserialize(encodeBase64(legacyPixel)); + const auto what = "a main version " + std::to_string(version) + " request"; + require(replayPixel.request.context.pixel.has_value() && !replayPixel.request.context.pixel->layerExported && !replayPixel.request.context.pixel->ditheredAlphaToCoverage && replayPixel.request.context.pixel->reversedBlend == 0u, (what + " read a dither flag or reversed-order blend code").c_str()); + require(serializer.Serialize(replayPixel.request) == encodedPixel, (what + " misread the fields after its layer export flag").c_str()); + } + legacyPixel.erase(legacyPixel.begin() + static_cast(flagOffset)); for (const auto version : {19u, 20u, 21u, 22u}) { auto versionPixel = legacyPixel; versionPixel[4] = static_cast(version); if (version < 22u) versionPixel.pop_back(); const auto replayPixel = serializer.Deserialize(encodeBase64(versionPixel)); - require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replayPixel.request.context.pixel->layerExported && !replayPixel.request.context.pixel->ditheredAlphaToCoverage, "a main version 19 to 22 request lost its pixel primitive class or read a layer export or dither flag"); - require(serializer.Serialize(replayPixel.request) == encodedPixel, "a main version 19 to 22 request misread the fields after its pixel primitive class"); + const auto what = "a main version " + std::to_string(version) + " request"; + require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replayPixel.request.context.pixel->layerExported && !replayPixel.request.context.pixel->ditheredAlphaToCoverage && replayPixel.request.context.pixel->reversedBlend == 0u, (what + " lost its pixel primitive class or read a layer export, dither flag or reversed-order blend code").c_str()); + require(serializer.Serialize(replayPixel.request) == encodedPixel, (what + " misread the fields after its pixel primitive class").c_str()); } } +void verifyDepthRemapRequests() { + using namespace ShaderRecompiler; + const std::array code{0xbf810000u}; + const RequestSerializer serializer; + RecompileRequest vertex{}; + vertex.shader = {ShaderStage::Vertex, 0x10000u, code, 0, {}}; + vertex.context.waveSize = 64; + vertex.context.vertex = ShaderVertexStageInfo{}; + vertex.context.vertex->paClVsOutCntl = 0x89abcdefu; + const auto plain = serializer.Serialize(vertex); + vertex.target.depthRangeRemap = true; + vertex.context.vertex->depthRemapMode = 2u; + const auto remapped = serializer.Serialize(vertex); + const auto replay = serializer.Deserialize(remapped); + require(replay.request.target.depthRangeRemap && replay.request.context.vertex.has_value() && replay.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "the depth remap target flag or the vertex info did not round-trip"); + require(replay.request.context.vertex->depthRemapMode == 2u, "the depth remap mode did not round-trip"); + require(serializer.Serialize(replay.request) == remapped, "the depth remap request changed on re-serialization"); + const auto plainBytes = decodeBase64(plain); + const auto remappedBytes = decodeBase64(remapped); + constexpr std::size_t pointTailBytes = 13u; + require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 7u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 7 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - pointTailBytes, plainBytes.end(), remappedBytes.end() - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags and the point tail of a vertex request"); + for (const std::uint8_t legacyVersion : {32u, 34u}) { + auto legacyBytes = remappedBytes; + legacyBytes.pop_back(); + legacyBytes[4] = legacyVersion; + legacyBytes.resize(legacyBytes.size() - 6u - pointTailBytes); + const auto legacy = serializer.Deserialize(encodeBase64(legacyBytes)); + require(!legacy.request.target.depthRangeRemap && legacy.request.context.vertex.has_value() && legacy.request.context.vertex->depthRemapMode == 0u, ("a version " + std::to_string(legacyVersion) + " vertex request read depth remap fields").c_str()); + require(serializer.Serialize(legacy.request) == plain, ("a version " + std::to_string(legacyVersion) + " vertex request misread the fields before the depth remap").c_str()); + } + auto version35 = remappedBytes; + version35.pop_back(); + version35[4] = 35u; + version35.resize(version35.size() - 1u - pointTailBytes); + const auto replay35 = serializer.Deserialize(encodeBase64(version35)); + require(replay35.request.target.depthRangeRemap && replay35.request.context.vertex->depthRemapMode == 2u && !replay35.request.target.float32FlushToZero && serializer.Serialize(replay35.request) == remapped, "a version 35 request misread the depth remap fields or read an f32 flush flag"); + auto truncated = version35; + truncated.resize(truncated.size() - 2u); + expectFailure([&] { static_cast(serializer.Deserialize(encodeBase64(truncated))); }, "truncated data", "a truncated depth remap mode was accepted"); + auto trailing = remappedBytes; + trailing.push_back(0u); + expectFailure([&] { static_cast(serializer.Deserialize(encodeBase64(trailing))); }, "unexpected trailing data", "a byte after the depth remap fields was accepted"); + RecompileRequest pixel{}; + pixel.shader = {ShaderStage::Fragment, 0x20000u, code, 0, {}}; + pixel.context.waveSize = 64; + pixel.context.pixel = ShaderPixelStageInfo{}; + pixel.target.depthRangeRemap = true; + const auto pixelReplay = serializer.Deserialize(serializer.Serialize(pixel)); + require(pixelReplay.request.target.depthRangeRemap && !pixelReplay.request.context.vertex.has_value(), "a pixel request lost the depth remap target flag or gained vertex info"); + std::vector key; + std::vector other; + RecompileCacheKey::Build(vertex, key); + auto zeroToOne = vertex; + zeroToOne.context.vertex->depthRemapMode = 1u; + RecompileCacheKey::Build(zeroToOne, other); + require(key != other, "the depth remap mode did not change the shader cache key"); +} + +void verifyMergedHullConfiguration() { + using namespace ShaderRecompiler; + static constexpr std::array code{0xbf810000u}; + const RequestSerializer serializer; + RecompileRequest request{}; + request.shader = {ShaderStage::TessellationControl, 0x10000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.vertex = ShaderVertexStageInfo{}; + request.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{3u, 3u, 1u, 2u, 2u}, {}}; + const auto separate = serializer.Serialize(request); + std::vector separateKey; + RecompileCacheKey::Build(request, separateKey); + request.graphics->tessellation->mergedHull = true; + const auto merged = serializer.Serialize(request); + std::vector mergedKey; + RecompileCacheKey::Build(request, mergedKey); + const auto replay = serializer.Deserialize(merged); + require(replay.request.graphics.has_value() && replay.request.graphics->tessellation.has_value() && replay.request.graphics->tessellation->mergedHull, "a merged hull configuration was lost in serialization"); + require(!serializer.Deserialize(separate).request.graphics->tessellation->mergedHull, "a separate hull configuration was read as merged"); + constexpr std::size_t pointTailBytes = 13u; + auto legacy = decodeBase64(merged); + require(legacy.size() > 53u + pointTailBytes && legacy[4] == 43u, "a merged hull request did not use serialization version 43"); + legacy.resize(legacy.size() - pointTailBytes); + legacy.pop_back(); + require(legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration"); + legacy.erase(legacy.end() - 52); + for (const auto version : {30u, 31u, 32u, 34u, 35u, 36u, 37u, 38u}) { + auto older = legacy; + older[4] = static_cast(version); + if (version < 38u) older.erase(older.end() - 31); + if (version < 36u) older.pop_back(); + if (version < 35u) older.resize(older.size() - 5u); + if (version < 32u) older.pop_back(); + const auto upgraded = serializer.Deserialize(encodeBase64(older)); + const auto what = "a main version " + std::to_string(version) + " tessellation request"; + require(upgraded.request.graphics.has_value() && upgraded.request.graphics->tessellation.has_value() && !upgraded.request.graphics->tessellation->mergedHull, (what + " read a merged hull flag").c_str()); + require(serializer.Serialize(upgraded.request) == separate, (what + " misread the fields after its tessellation configuration").c_str()); + } + require(mergedKey != separateKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(serializer.Deserialize(separate).request), "the cache keys ignore whether the hull program is merged"); +} + void verifyLegacyPixelRequests() { using namespace ShaderRecompiler; static constexpr std::array legacyPixelRequests{ @@ -2614,6 +5475,7 @@ void verifyLegacyPixelRequests() { require(!pixel.alphaToCoverage && !pixel.ditheredAlphaToCoverage, "a legacy request enabled alpha-to-coverage"); require(!pixel.stencilRefExportEnable, "a legacy request exported a stencil reference"); require(pixel.rasterPrimitive == RasterPrimitive::Triangle, "a legacy request drew something other than triangles"); + require(pixel.reversedBlend == 0u, "a legacy request blended into a reversed component order"); require(pixel.inputAddr == 2u && pixel.hasPerspectiveCenterVgpr && pixel.targetOutputMode[0] == 9u, "legacy pixel layout was misread"); require(pixel.conservativeZExport == (version >= 7u ? ConservativeZExport::GreaterThanZ : ConservativeZExport::AnyZ), "legacy conservative Z layout was misread"); require(!pixel.orderedPixelShader, "a legacy request became a primitive-ordered pixel shader"); @@ -2853,7 +5715,8 @@ void verifyComputedTexelOffsets() { request.useCache = false; AgcDriver::ShaderMemory memory({}); static_cast(memory.Capture(request)); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; return Recompile(request).spirv; }; const auto sampleOperands = [](std::span words) { @@ -2903,7 +5766,8 @@ void verifyShaderClockScopes() { request.useCache = false; AgcDriver::ShaderMemory memory({}); static_cast(memory.Capture(request)); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; const auto words = Recompile(request).spirv; std::map constants; std::vector scopeIds; @@ -3035,7 +5899,7 @@ void verifySpecializedBufferAtomicBindings() { } require(!physical, "specialized buffer atomic: the module addresses physical storage without an address access"); require(int64, "specialized buffer atomic: the fault address is 64-bit and the module does not declare Int64"); - require(faultFunction != 0 && faultCalls == 1, "specialized buffer atomic: an unaligned atomic does not record exactly one fault"); + require(faultCalls == 0, "specialized buffer atomic: a 32-bit atomic records an unaligned fault instead of acting on its dword"); const std::array shaderOnly{spv::CapabilityShader}; expectFailure([&] { static_cast(compile(shaderOnly)); }, "buffer atomic fault addresses require shaderInt64", "specialized buffer atomic: the program compiled without shaderInt64"); } @@ -3217,7 +6081,8 @@ void verifyUnnormalizedSamplers() { request.layout.pushConstantSizeBytes = 128; AgcDriver::ShaderMemory memory({}); static_cast(memory.Capture(request)); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; return Recompile(request); }; const auto flags = [](const RecompileResult& result, DescriptorRole role) { @@ -3266,6 +6131,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"); @@ -3377,6 +6251,67 @@ 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 verifyNullVolumeUnnormalizedSampler() { + using namespace ShaderRecompiler; + ImageResource volume{}; + volume.resourceClass = ImageResourceClass::Sampled; + volume.numericClass = IrTextureNumericClass::Float; + volume.dimension = RdnaImageDimension::Dim3D; + volume.read = true; + ImageResource plane = volume; + plane.dimension = RdnaImageDimension::Dim2D; + SamplerResource sampler{}; + sampler.uses = SamplerUseExplicitLod; + const DescriptorValue nullDescriptor{{0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u}, 8u}; + const DescriptorValue volumeDescriptor{{0x00001000u, 0x03800000u, 0x0000c000u, 0xa0000facu, 3u, 0u, 0u, 0u}, 8u}; + const DescriptorValue planeDescriptor{{0x00002000u, 0x03800000u, 0x0000c000u, 0x90000facu, 0u, 0u, 0u, 0u}, 8u}; + const DescriptorValue unnormalized{{0x00008092u, 0x00fff000u, 0x02500000u, 0u}, 4u}; + const auto populate = [&](std::vector images, std::vector descriptors) { + ShaderInfo info; + info.images = images; + info.samplers = {sampler}; + for (std::uint32_t index = 0; index < images.size(); ++index) { + info.sampledPairs.push_back({index, 0u, 0u}); + info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(images[index])); + } + ResourceSnapshot snapshot; + snapshot.images = descriptors; + snapshot.samplers = {unnormalized}; + BindingAllocationResult allocation; + for (std::uint32_t index = 0; index < images.size(); ++index) allocation.layout.descriptors.push_back({DescriptorBindingForImage(images[index]), {index}}); + allocation.layout.descriptors.push_back({DescriptorBindingKind::Samplers, {0u}}); + DescriptorBindingBuilder{}.Populate(allocation, info, IrShaderStage::Compute, 0u, snapshot, {}); + return allocation.bindings.back(); + }; + const auto nullOnly = populate({volume}, {nullDescriptor}); + require(nullOnly.samplerUnnormalized == std::vector{false} && nullOnly.samplerCoordinatesIgnored == std::vector{true}, "unnormalized samplers: an S# paired only with a null 3D T# was refused or kept its unnormalized coordinates"); + const DescriptorValue zeroSwizzle{{0x00003000u, 0x00100000u, 0u, 0xa0000000u, 0u, 0u, 0u, 0u}, 8u}; + const auto constantAndNull = populate({volume, volume}, {zeroSwizzle, nullDescriptor}); + require(constantAndNull.samplerUnnormalized == std::vector{false} && constantAndNull.samplerCoordinatesIgnored == std::vector{true}, "unnormalized samplers: an S# paired with a null 3D T# and a 3D T# whose channels all select constants was refused or kept its unnormalized coordinates"); + expectFailure([&] { static_cast(populate({volume}, {volumeDescriptor})); }, "samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 3D T# was accepted"); + expectFailure([&] { static_cast(populate({volume, plane}, {nullDescriptor, planeDescriptor})); }, "samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: an S# shared by a null 3D T# and a 2D T# was accepted"); } void verifyUnusedUnnormalizedSampler() { @@ -3400,10 +6335,21 @@ void verifyUnusedUnnormalizedSampler() { DescriptorBindingBuilder{}.Populate(allocation, shader, IrShaderStage::Compute, 0u, snapshot, {}); return allocation.bindings; }; + auto constantModes = ResourceMaterializer::RuntimeImageModes(image); + std::erase_if(constantModes, [](const auto& mode) { return !mode.constantSwizzle; }); + require(!constantModes.empty(), "constant image selectors: no dedicated runtime modes were prepared"); + require(!ResourceMaterializer::TryRuntimeImageMode(image, snapshot.images[0], constantModes).mode.has_value(), "constant image selectors: ordinary selectors selected a constant-only mode"); + auto constantDescriptor = snapshot.images[0]; + constantDescriptor.dwords[3] = 0xa0000041u; + require(ResourceMaterializer::TryRuntimeImageMode(image, constantDescriptor, constantModes).mode.has_value(), "constant image selectors: constant selectors did not select their dedicated mode"); + auto tableImage = image; + tableImage.table = 0u; + const auto tableModes = ResourceMaterializer::RuntimeImageModes(tableImage); + require(std::none_of(tableModes.begin(), tableModes.end(), [](const auto& mode) { return mode.constantSwizzle; }), "constant image selectors: a table gained a direct-only constant mode"); const auto bindings = populate(info); require(bindings.size() == 2u && bindings[0].imageUnnormalized == std::vector{true} && bindings[1].samplerUnnormalized == std::vector{true}, "unnormalized samplers: an S# without live uses was not flagged"); auto selected = info; - selected.images[0].indirectRoot = 0u; + selected.images[0].table = 0u; expectFailure([&] { static_cast(populate(selected)); }, "unnormalized guest sampler samples an image selected at run time, which is not implemented", "unnormalized samplers: an image table root was accepted"); snapshot.images[0].dwords[3] = 0xa0000041u; const auto constant = populate(info); @@ -3447,11 +6393,36 @@ 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"); static_cast(populate(offset)); expectFailure([&] { static_cast(populate(foldOnly)); }, "normalized guest sampler is used with a texel offset that is not a constant", "unnormalized samplers: a normalized S# was accepted for an offset that only folds"); + ImageResource msaaArrayImage{}; + msaaArrayImage.resourceClass = ImageResourceClass::Sampled; + msaaArrayImage.numericClass = IrTextureNumericClass::Float; + msaaArrayImage.dimension = RdnaImageDimension::Dim2DMsaaArray; + msaaArrayImage.read = true; + ShaderInfo msaaArrayInfo; + msaaArrayInfo.images = {msaaArrayImage}; + msaaArrayInfo.runtimeImageModes = {ResourceMaterializer::RuntimeImageModes(msaaArrayImage)}; + BindingAllocationResult msaaArrayAllocation; + msaaArrayAllocation.layout.descriptors = {{DescriptorBindingForImage(msaaArrayImage), {0u}}}; + auto msaaArraySnapshot = snapshot; + msaaArraySnapshot.images[0].dwords[3] = 0xf0020facu; + msaaArraySnapshot.images[0].dwords[5] = 0x00000020u; + DescriptorBindingBuilder{}.Populate(msaaArrayAllocation, msaaArrayInfo, IrShaderStage::Compute, 0u, msaaArraySnapshot, {}); + require(msaaArrayAllocation.bindings.size() == 1u && msaaArrayAllocation.bindings[0].imageShape == DescriptorImageShape::Image2DMultisampleArray, "multisampled array image resource did not receive Image2DMultisampleArray descriptor shape"); } void verifyWaveUniformValues() { @@ -3534,7 +6505,8 @@ void verifyTwoLaneUniformValues() { request.layout.pushConstantSizeBytes = 128; AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(request); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; const auto words = Recompile(request, *capture)->spirv; std::size_t count = 0; for (std::size_t cursor = 5; cursor < words.size();) { @@ -3666,6 +6638,102 @@ void verifyBdaReadFallbackFunctions() { } } +void verifyBdaAnchors() { + using namespace ShaderRecompiler; + const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + alignas(256) static std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const std::array userData{0x10000000u, 0u, 0u, 0u, static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 128u, 0x01016facu}; + const auto compile = [&](std::uint32_t scalarBase, std::uint32_t stride) { + std::vector code{0x7e020200u, 0x7e040201u}; + for (std::uint32_t load = 0; load < scalarBase; ++load) { + code.push_back(0xdc308000u | (stride * load + 4u)); + code.push_back(((3u + load) << 24u) | 0x00040000u); + } + code.push_back(0xdc308000u | 4u); + code.push_back(((3u + scalarBase) << 24u) | 0x007d0001u); + code.push_back(0xbf8c3f70u); + for (std::uint32_t load = 1; load <= scalarBase; ++load) code.push_back(0x4a060103u | ((3u + load) << 9u)); + code.insert(code.end(), {0xe0700000u, 0x80010300u, 0xbf810000u}); + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x60000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return Recompile(request).spirv; + }; + for (const std::uint32_t stride : {4u, 32u}) { + for (const std::uint32_t loads : {0u, 1u, 2u, 4u}) { + const bool anchored = stride == 32u && loads >= 2u; + const auto words = compile(loads, stride); + std::map names; + std::string function; + std::map mainCalls; + std::size_t anchorFunctions = 0; + std::size_t branchesBeforeAnchor = 0; + std::size_t directLoads = 0; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto length = words[cursor] >> 16u; + require(length != 0 && length <= words.size() - cursor, "BDA anchors: truncated SPIR-V instruction"); + const auto op = words[cursor] & 0xffffu; + if (op == spv::OpName) names[words[cursor + 1]] = reinterpret_cast(&words[cursor + 2]); + if (op == spv::OpFunction) { + function = names[words[cursor + 2]]; + if (function == "bda_anchor_range") ++anchorFunctions; + } + if (op == spv::OpFunctionCall && function == "main") { + const auto& callee = names[words[cursor + 3]]; + if (callee == "bda_anchor_range") require(mainCalls.empty(), "BDA anchors: the anchor is searched after a read"); + ++mainCalls[callee.starts_with("read_bda_span") ? std::string("read_bda_span") : callee]; + } + if (op == spv::OpBranchConditional && function == "main" && mainCalls["bda_anchor_range"] == 0u) ++branchesBeforeAnchor; + if (op == spv::OpLoad && function == "main" && length > 4u && words[cursor + 4] == spv::MemoryAccessAlignedMask && words[cursor + 5] == 4u) ++directLoads; + cursor += length; + } + const auto spans = loads == 0u ? 1u : stride == 4u ? 2u : loads + 1u; + require(anchorFunctions == (anchored ? 1u : 0u), "BDA anchors: the anchor search is defined without anchored reads, or twice"); + require(mainCalls["bda_anchor_range"] == (anchored ? 1u : 0u), "BDA anchors: the reads of one user data base do not share one anchor"); + require(!anchored || branchesBeforeAnchor == 0u, "BDA anchors: the anchor is not searched at the program's entry"); + require(mainCalls["read_bda_span"] == spans, "BDA anchors: a read lost its lookup path"); + require(directLoads == (anchored ? loads : 0u), "BDA anchors: an anchored read does not load its dwords directly"); + } + } + RecompileRequest plain{}; + const std::vector endOnly{0xbf810000u}; + plain.shader = {ShaderStage::Compute, 0x60000u, endOnly, 0, {}}; + plain.context.waveSize = 32; + plain.context.userDataBaseRegister = 0; + plain.context.userData = userData; + plain.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + plain.target.vulkanVersion = 0x00401000u; + plain.target.spirvVersion = 0x00010300u; + plain.target.subgroupSize = 32; + plain.target.bdaAbiVersion = BdaAbi::Version; + plain.target.fragmentShaderBarycentricEnabled = false; + plain.layout.pushConstantSizeBytes = 128; + plain.useCache = false; + const auto plainWords = Recompile(plain).spirv; + for (std::size_t cursor = 5; cursor < plainWords.size();) { + const auto length = plainWords[cursor] >> 16u; + require(length != 0 && length <= plainWords.size() - cursor, "BDA anchors: truncated SPIR-V instruction"); + const auto op = plainWords[cursor] & 0xffffu; + require(!(op == spv::OpTypeInt && plainWords[cursor + 2] == 64u), "BDA anchors: a program without anchors declares a 64-bit integer type"); + require(!(op == spv::OpCapability && plainWords[cursor + 1] == spv::CapabilityInt64), "BDA anchors: a program without anchors requires Int64"); + cursor += length; + } +} + void verifyFunctionLdsBound() { using namespace ShaderRecompiler; const auto build = [](const auto& body) { @@ -3914,6 +6982,10 @@ void verifySrtReadPatching() { const auto* replacement = repeatedUse.Argument(0); require(replacement->Opcode() == IrOpcode::ReadConst, "SRT read patching: the uses do not reference a flat read"); require(repeatedUse.Argument(1) == replacement && secondUser.Argument(0) == replacement, "SRT read patching: not every operand uses the same replacement"); + const std::vector order(block.Instructions().begin(), block.Instructions().end()); + const auto readAt = std::find(order.begin(), order.end(), &read); + require(readAt != order.end() && readAt - order.begin() >= 2 && *(readAt - 1) == replacement && *(readAt - 2) == replacement->Argument(0) && (*(readAt - 2))->Opcode() == IrOpcode::GetSrtResource, "SRT read patching: the flat read is not right before the original read"); + require(order.back()->Opcode() == IrOpcode::ReferenceU32 && order.back()->Argument(0) == &read, "SRT read patching: the planning reference is not at the end of the block"); const auto& remainingUses = read.OperandUses(); require(remainingUses.size() == 1u && remainingUses.front().user->Opcode() == IrOpcode::ReferenceU32, "SRT read patching: the original read retains non-planning uses"); } @@ -3975,6 +7047,161 @@ bool GuardRecords(const ShaderRecompiler::ResourceCapture& capture) { return true; } +void verifyGuardedDirectRoot() { + using namespace ShaderRecompiler; + constexpr std::size_t Reserved = 0x10000; + void* reserved = nullptr; +#ifdef _WIN32 + reserved = VirtualAlloc(nullptr, Reserved, MEM_RESERVE, PAGE_NOACCESS); +#else + reserved = mmap(nullptr, Reserved, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (reserved == MAP_FAILED) reserved = nullptr; +#endif + require(reserved != nullptr, "guarded direct root: cannot reserve an inaccessible range"); + const auto unmapped = static_cast(reinterpret_cast(reserved)); + alignas(256) static std::array output{}; + alignas(256) static std::array root{}; + static std::uint32_t payload = 0x3f800000u; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const auto rootAddress = static_cast(reinterpret_cast(root.data())); + const auto payloadAddress = static_cast(reinterpret_cast(&payload)); + root = {static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), static_cast(sizeof(output)), 0x30027facu, 1u}; + const std::vector branchCode{0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xbe8a0302u, 0xbe8b0303u, 0xbe8c03ffu, 0x11111111u, + 0xbf8cc07fu, 0xbf068008u, 0xbf850003u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u}; + for (const auto index : {4u, 5u}) { + const auto move = DecodeRdnaInstruction(index * 4u, branchCode, index); + require(move.op == RdnaOpcode::SMovB32 && move.destination.kind == RdnaOperandKind::ScalarRegister && move.destination.reg == index + 6u && + move.source0.kind == RdnaOperandKind::ScalarRegister && move.source0.reg == index - 2u, "guarded direct root: the fixture does not move the user-data pointer to s10:s11"); + } + const auto compare = DecodeRdnaInstruction(0x24u, branchCode, 9u); + require(compare.op == RdnaOpcode::SCmpEqU32 && compare.source0.kind == RdnaOperandKind::ScalarRegister && compare.source0.reg == 8u && + compare.source1.kind == RdnaOperandKind::IntegerInlineConstant && compare.source1.value == 0u, "guarded direct root: the fixture does not compare the branch word with zero"); + const auto condition = DecodeRdnaInstruction(0x28u, branchCode, 10u); + require(condition.op == RdnaOpcode::SCbranchScc1 && condition.branchTarget == 0x38u, "guarded direct root: the fixture does not skip the scalar load when the branch word is zero"); + NestedRequest branch(branchCode, root.data()); + std::array userData{static_cast(rootAddress), static_cast(rootAddress >> 32u), + static_cast(payloadAddress), static_cast(payloadAddress >> 32u)}; + branch.request.context.userData = userData; + AgcDriver::ShaderMemory validMemory({}); + const auto valid = validMemory.Capture(branch.request); + require(valid->snapshot.srtPoison.empty(), "guarded direct root: a mapped direct pointer was poisoned"); + const auto read = std::find_if(valid->plan->srtReads.begin(), valid->plan->srtReads.end(), [](const auto& candidate) { return candidate.value->template Flags().pc == 0x2cu; }); + require(read != valid->plan->srtReads.end(), "guarded direct root: the scalar read was not planned"); + require(!Detail::LoadedFromMemory(*read->value->Argument(0)), "guarded direct root: the pointer was classified as loaded from memory"); + require(valid->snapshot.flattenedSrt.at(read->flatOffset) == payload, "guarded direct root: the mapped scalar read has the wrong word"); + const auto validRegions = validMemory.Regions(); + branch.request.context.memory = validRegions; + const auto validResult = Recompile(branch.request, *valid); + require(validResult->poisonedSrtReads == 0u, "guarded direct root: the mapped program reports poison"); +#if ANYPS5_ENABLE_SPIRV_TOOLS + static_cast(ValidateAndOptimizeSpirv(validResult->spirv, branch.request.target.vulkanVersion, branch.request.target.spirvVersion)); +#endif + std::cout << "guarded direct root: mapped taken load passed\n"; + root[4] = 0u; + branch.request.context.memory = {}; + AgcDriver::ShaderMemory mappedSkippedMemory({}); + const auto mappedSkipped = mappedSkippedMemory.Capture(branch.request); + const auto mappedSkippedRegions = mappedSkippedMemory.Regions(); + branch.request.context.memory = mappedSkippedRegions; + const auto mappedSkippedResult = Recompile(branch.request, *mappedSkipped); + require(mappedSkippedResult->poisonedSrtReads == 0u, "guarded direct root: the mapped skipped program reports poison"); +#if ANYPS5_ENABLE_SPIRV_TOOLS + static_cast(ValidateAndOptimizeSpirv(mappedSkippedResult->spirv, branch.request.target.vulkanVersion, branch.request.target.spirvVersion)); +#endif + std::cout << "guarded direct root: mapped skipped load passed\n"; + root[4] = 1u; + userData[2] = static_cast(unmapped); + userData[3] = static_cast(unmapped >> 32u); + branch.request.context.memory = {}; + expectFailure([&] { + AgcDriver::ShaderMemory memory({}); + static_cast(memory.Capture(branch.request)); + }, "guest memory is not readable", "guarded direct root: a taken inaccessible load was accepted"); + std::cout << "guarded direct root: inaccessible taken load rejected\n"; + root[4] = 0u; + std::vector encodedPlan; + require(ShaderDiskCache::EncodePlan(*valid->plan, encodedPlan), "guarded direct root: cannot encode the resource plan"); + IrResourcePlan decodedPlan; + require(ShaderDiskCache::DecodePlan(encodedPlan, decodedPlan), "guarded direct root: cannot decode the resource plan"); + require(!valid->plan->controlFlow.empty() && decodedPlan.controlFlow.size() == valid->plan->controlFlow.size(), "guarded direct root: the scalar branch was not retained in the plan"); + for (std::size_t block = 0; block < decodedPlan.controlFlow.size(); ++block) { + require(decodedPlan.controlFlow[block].flatSlots == valid->plan->controlFlow[block].flatSlots, "guarded direct root: cached control flow lost its flat slots"); + } + IrResourcePlan decodedIrPlan; + require(ShaderDiskCache::DecodePlan(encodedPlan, decodedIrPlan), "guarded direct root: cannot decode the IR control plan"); + decodedIrPlan.compact = {}; + struct ReadContext { + std::uint64_t root; + std::uint64_t payload; + bool guardReadable; + std::uint32_t invalidReads = 0; + SrtMemoryReader reader = nullptr; + }; + const auto walk = [&](const IrResourcePlan& plan, bool tape, bool guardReadable) { + ReadContext context{rootAddress, payloadAddress, guardReadable}; + context.reader = [](void* opaque, std::uint64_t address, std::uint32_t* value) { + auto& context = *static_cast(opaque); + if (!((address >= context.root && address - context.root <= sizeof(root) - sizeof(*value)) || address == context.payload)) { + ++context.invalidReads; + return false; + } + std::memcpy(value, reinterpret_cast(address), sizeof(*value)); + return true; + }; + SrtRuntime runtime{userData}; + runtime.userContext = &context; + runtime.readMemory = context.reader; + runtime.readSpecializationMemory = [](void* opaque, std::uint64_t address, std::uint32_t* value) { + auto& context = *static_cast(opaque); + if (!context.guardReadable && address == context.root + 16u) return false; + return context.reader(opaque, address, value); + }; + const std::vector sourceCopy(plan.materializationSources.begin(), plan.materializationSources.end()); + const std::vector cleanCopy(plan.cleanFlatSlots.begin(), plan.cleanFlatSlots.end()); + const auto sources = tape ? std::span(plan.materializationSources) : std::span(sourceCopy); + const auto clean = tape ? std::span(plan.cleanFlatSlots) : std::span(cleanCopy); + std::vector descriptors; + std::vector flat; + std::vector active; + std::vector poison; + const auto* compiled = tape ? Detail::SrtTape::For(plan) : nullptr; + if (tape) require(compiled != nullptr, "guarded direct root: the plan has no SRT tape"); + const bool ok = tape ? compiled->Run(plan, runtime, descriptors, flat, active) : Detail::EvaluateRuntimeSourcesImpl(plan, sources, runtime, descriptors, flat, true, clean, active, &poison, nullptr); + require(ok == guardReadable, "guarded direct root: a known skipped load failed, or an unresolved guard silently succeeded"); + if (guardReadable) require(context.invalidReads == 0u && poison.empty(), "guarded direct root: the walker touched a known skipped load"); + }; + for (const IrResourcePlan* plan : {valid->plan.get(), static_cast(&decodedPlan), static_cast(&decodedIrPlan)}) { + for (const bool tape : {false, true}) { + walk(*plan, tape, true); + walk(*plan, tape, false); + } + } + auto repeatedCode = branchCode; + repeatedCode.insert(repeatedCode.begin() + 14, {0xf4000305u, 0xfa000000u, 0xbf8cc07fu}); + NestedRequest repeated(std::move(repeatedCode), root.data()); + repeated.request.context.userData = userData; + expectFailure([&] { + AgcDriver::ShaderMemory memory({}); + static_cast(memory.Capture(repeated.request)); + }, "guest memory is not readable", "guarded direct root: an unconditional repeated load was skipped with its guarded use"); + AgcDriver::ShaderMemory skippedMemory({}); + const auto skipped = skippedMemory.Capture(branch.request); + const auto skippedRegions = skippedMemory.Regions(); + require(skipped->snapshot.srtPoison.empty() && !RegionsTouch(skippedRegions, unmapped, 4u), "guarded direct root: a skipped scalar load was captured or poisoned"); + branch.request.context.memory = skippedRegions; + const auto skippedResult = Recompile(branch.request, *skipped); + require(skippedResult->poisonedSrtReads == 0u, "guarded direct root: the skipped program reports poison"); +#if ANYPS5_ENABLE_SPIRV_TOOLS + static_cast(ValidateAndOptimizeSpirv(skippedResult->spirv, branch.request.target.vulkanVersion, branch.request.target.spirvVersion)); +#endif + std::cout << "guarded direct root: inaccessible skipped load passed\n"; +#ifdef _WIN32 + VirtualFree(reserved, 0, MEM_RELEASE); +#else + munmap(reserved, Reserved); +#endif +} + void verifyGuardedNullPointers() { using namespace ShaderRecompiler; constexpr std::size_t Reserved = 0x10000; @@ -4012,6 +7239,28 @@ void verifyGuardedNullPointers() { const auto samePipeline = [](const RecompileResult& left, const RecompileResult& right) { return left.variantId == right.variantId && left.specializationId == right.specializationId && left.PipelineVariantId() == right.PipelineVariantId() && left.spirv == right.spirv; }; + const auto guardSites = [](const RecompileResult& result) { + const auto words = result.spirv.Words(); + std::map names; + std::map constants; + std::vector guardWords; + std::string function; + std::size_t calls = 0; + std::size_t stores = 0; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto opcode = words[cursor] & 0xffffu; + require(count != 0 && count <= words.size() - cursor, "guarded pointer: truncated SPIR-V instruction"); + if (opcode == spv::OpName) names[words[cursor + 1]] = reinterpret_cast(&words[cursor + 2]); + if (opcode == spv::OpConstant && count == 4u) constants[words[cursor + 2]] = words[cursor + 3]; + if (opcode == spv::OpFunction) function = names[words[cursor + 2]]; + if (opcode == spv::OpAccessChain && count == 6u && names[words[cursor + 3]] == "fault_buffer" && constants.contains(words[cursor + 5]) && constants[words[cursor + 5]] == BdaAbi::SrtGuardWord) guardWords.push_back(words[cursor + 2]); + if (function == "main" && opcode == spv::OpFunctionCall && names[words[cursor + 3]] == "record_bda_fault") ++calls; + if (function == "main" && opcode == spv::OpAtomicStore && std::find(guardWords.begin(), guardWords.end(), words[cursor + 1]) != guardWords.end()) ++stores; + cursor += count; + } + return std::pair{calls, stores}; + }; NestedRequest branch(branchCode, root.data()); point(payloadAddress); @@ -4019,9 +7268,12 @@ void verifyGuardedNullPointers() { const auto valid = validMemory.Capture(branch.request); require(valid->snapshot.srtPoison.empty() && valid->plan->guardedSrtSlots.size() == 1u && slotPc(*valid->plan, valid->plan->guardedSrtSlots[0]) == 0x2cu && GuardRecords(*valid), "guarded pointer: a mapped nested pointer was poisoned, or its read has no guard"); require(valid->snapshot.flattenedSrt.at(valid->plan->guardedSrtSlots[0]) == payload, "guarded pointer: a mapped nested read does not hold its word"); - branch.request.context.memory = validMemory.Regions(); + const auto validRegions = validMemory.Regions(); + branch.request.context.memory = validRegions; const auto validResult = Recompile(branch.request, *valid); require(validResult->poisonedSrtReads == 0u && faultBinding(*validResult) && validResult->bdaAbiVersion == BdaAbi::Version, "guarded pointer: a guarded program has no fault buffer"); + const auto [branchCalls, branchStores] = guardSites(*validResult); + require(branchCalls == 0u && branchStores == 1u, ("guarded pointer: a guarded read calls record_bda_fault " + std::to_string(branchCalls) + " times in main and stores its record to the fault buffer's guard word " + std::to_string(branchStores) + " times, expected one store and no call").c_str()); for (const auto pointer : {std::uint64_t{0}, unmapped}) { for (const auto condition : {1u, 0u}) { @@ -4031,16 +7283,16 @@ void verifyGuardedNullPointers() { AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(branch.request); const auto& poison = capture->snapshot.srtPoison; - require(poison.size() == 1u && poison[0].pc == 0x2cu && poison[0].address == pointer && slotPc(*capture->plan, poison[0].slot) == 0x2cu, "guarded pointer: the nested read was not poisoned at its pc and address"); + require(condition == 0u ? poison.empty() : poison.size() == 1u && poison[0].pc == 0x2cu && poison[0].address == pointer && slotPc(*capture->plan, poison[0].slot) == 0x2cu, "guarded pointer: a skipped nested read was poisoned, or an executed read has the wrong poison record"); require(GuardRecords(*capture), "guarded pointer: the poison record is not where the guard reads it"); const auto regions = memory.Regions(); require(!RegionsTouch(regions, pointer, 4u) && RegionsCover(regions, root.data() + 6, 8u), "guarded pointer: the captured regions are wrong"); branch.request.context.memory = regions; const auto result = Recompile(branch.request, *capture); - require(result->poisonedSrtReads == 1u && faultBinding(*result) && result->bdaAbiVersion == BdaAbi::Version, "guarded pointer: the poisoned capture does not report through the fault buffer"); + require(result->poisonedSrtReads == condition && faultBinding(*result) && result->bdaAbiVersion == BdaAbi::Version, "guarded pointer: the capture has the wrong poison count or no fault buffer"); require(samePipeline(*result, *validResult), "guarded pointer: poison changed the variant, the specialization or the module"); const auto replay = Recompile(branch.request); - require(replay.poisonedSrtReads == 1u && samePipeline(replay, *result), "guarded pointer: a replay of the captured regions did not reproduce the poison"); + require(replay.poisonedSrtReads == condition && samePipeline(replay, *result), "guarded pointer: a replay of the captured regions did not reproduce the poison"); verifyResult(*result, replay); #if ANYPS5_ENABLE_SPIRV_TOOLS static_cast(ValidateAndOptimizeSpirv(result->spirv, branch.request.target.vulkanVersion, branch.request.target.spirvVersion)); @@ -4061,22 +7313,27 @@ void verifyGuardedNullPointers() { const bool throughPointer = own == 0x38u && poison.address == unmapped; require(poison.pc == 0x2cu && (pointerWord || throughPointer), "guarded pointer: a derived poison does not name the inaccessible read"); } - chain.request.context.memory = chainMemory.Regions(); + const auto chainRegions = chainMemory.Regions(); + chain.request.context.memory = chainRegions; const auto chainResult = Recompile(chain.request, *chained); indirect = 0; point(static_cast(reinterpret_cast(&indirect))); AgcDriver::ShaderMemory nullChainMemory({}); const auto nullChain = nullChainMemory.Capture(chain.request); require(nullChain->snapshot.srtPoison.size() == 1u && nullChain->snapshot.srtPoison[0].pc == 0x38u && nullChain->snapshot.srtPoison[0].address == 0u && GuardRecords(*nullChain), "guarded pointer: a null pointer read from memory did not poison the read through it"); - chain.request.context.memory = nullChainMemory.Regions(); + const auto nullChainRegions = nullChainMemory.Regions(); + chain.request.context.memory = nullChainRegions; const auto nullChainResult = Recompile(chain.request, *nullChain); indirect = payloadAddress; AgcDriver::ShaderMemory mappedChainMemory({}); const auto mappedChain = mappedChainMemory.Capture(chain.request); require(mappedChain->snapshot.srtPoison.empty() && mappedChain->plan->guardedSrtSlots.size() == 3u && GuardRecords(*mappedChain), "guarded pointer: a mapped pointer chain was poisoned, or its reads have no guards"); - chain.request.context.memory = mappedChainMemory.Regions(); + const auto mappedChainRegions = mappedChainMemory.Regions(); + chain.request.context.memory = mappedChainRegions; const auto mappedChainResult = Recompile(chain.request, *mappedChain); require(chainResult->poisonedSrtReads == 3u && nullChainResult->poisonedSrtReads == 1u && mappedChainResult->poisonedSrtReads == 0u, "guarded pointer: a chain result does not count its poisoned reads"); + const auto [chainCalls, chainStores] = guardSites(*mappedChainResult); + require(chainCalls == 0u && chainStores == mappedChain->plan->guardedSrtSlots.size(), ("guarded pointer: a chain's guarded reads call record_bda_fault " + std::to_string(chainCalls) + " times in main and store to the guard word " + std::to_string(chainStores) + " times, expected one store per guard and no call").c_str()); require(samePipeline(*chainResult, *mappedChainResult) && samePipeline(*nullChainResult, *mappedChainResult), "guarded pointer: a chain's poison changed the variant, the specialization or the module"); const auto poisonedWords = [](const ResourceCapture& capture, std::uint32_t pc, std::uint64_t base) { @@ -4097,7 +7354,8 @@ void verifyGuardedNullPointers() { AgcDriver::ShaderMemory zeroMemory({}); const auto zeroCapture = zeroMemory.Capture(descriptor.request); require(zeroCapture->snapshot.srtPoison.empty() && zeroBuffer(*zeroCapture) && GuardRecords(*zeroCapture), "guarded pointer: a zero V# read from mapped memory was poisoned or not read"); - descriptor.request.context.memory = zeroMemory.Regions(); + const auto zeroRegions = zeroMemory.Regions(); + descriptor.request.context.memory = zeroRegions; const auto zeroResult = Recompile(descriptor.request, *zeroCapture); for (const auto pointer : {std::uint64_t{0}, unmapped}) { point(pointer); @@ -4139,11 +7397,11 @@ void verifyGuardedNullPointers() { vertex.request.context.memory = {}; AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(vertex.request); - require(poisonedWords(*capture, 0x20u, 0x10u), "guarded pointer: a vertex V# behind a branch and a null pointer did not poison its four words at its pc"); + require(guard == 0u ? capture->snapshot.srtPoison.empty() && GuardRecords(*capture) : poisonedWords(*capture, 0x20u, 0x10u), "guarded pointer: a skipped vertex V# was poisoned, or an executed read has the wrong poison record"); const auto regions = memory.Regions(); vertex.request.context.memory = regions; const auto result = Recompile(vertex.request, *capture); - require(result->poisonedSrtReads == 4u && faultBinding(*result) && !result->spirv.empty(), "guarded pointer: a vertex capture with a poisoned V# has no fault buffer"); + require(result->poisonedSrtReads == guard * 4u && faultBinding(*result) && !result->spirv.empty(), "guarded pointer: a vertex capture has the wrong poison count or no fault buffer"); #if ANYPS5_ENABLE_SPIRV_TOOLS static_cast(ValidateAndOptimizeSpirv(result->spirv, vertex.request.target.vulkanVersion, vertex.request.target.spirvVersion)); #endif @@ -4153,7 +7411,8 @@ void verifyGuardedNullPointers() { const auto rootlessCapture = rootlessMemory.Capture(rootless.request); require(rootlessCapture->snapshot.nullRootReads != 0u && GuardRecords(*rootlessCapture), "guarded pointer: reads through a null user-data pointer were not counted"); require(std::all_of(rootlessCapture->snapshot.buffers.begin(), rootlessCapture->snapshot.buffers.end(), [](const DescriptorValue& value) { return std::all_of(value.dwords.begin(), value.dwords.end(), [](std::uint32_t word) { return word == 0u; }); }), "guarded pointer: a V# read through a null user-data pointer is not zero"); - rootless.request.context.memory = rootlessMemory.Regions(); + const auto rootlessRegions = rootlessMemory.Regions(); + rootless.request.context.memory = rootlessRegions; const auto rootlessResult = Recompile(rootless.request, *rootlessCapture); require(rootlessResult->poisonedSrtReads >= rootlessCapture->snapshot.nullRootReads, "guarded pointer: a capture through a null user-data pointer is cacheable"); point(0u); @@ -4164,6 +7423,86 @@ void verifyGuardedNullPointers() { #endif } +void verifyQuadMaskNeighbourReads() { + using namespace ShaderRecompiler; + constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic}; + constexpr std::array demoteCapabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityDemoteToHelperInvocation}; + constexpr std::array demoteExtensions{"SPV_EXT_demote_to_helper_invocation"}; + constexpr std::array constant{0x7e0202f2u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + constexpr std::array explicitExec{0xbefe04c1u, 0x7e000500u, 0x7e020200u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + constexpr std::array constantWqm{0xbefe0ac1u, 0x7e000500u, 0x7e020200u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + constexpr std::array permute{0xbefe04c1u, 0x7e040284u, 0xdac80000u, 0x01000002u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + constexpr std::array comparisonBallot{0xbe8003ffu, 0x3f000000u, 0xbefe04c1u, 0x7d840000u, 0xbe80046au, 0x7e020200u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + const auto compile = [&](std::span code, bool masked, bool demote) { + RecompileRequest request{}; + request.shader = {ShaderStage::Fragment, 0x58000u, code, 0, {}}; + request.context.waveSize = 32u; + request.context.pixel = ShaderPixelStageInfo{}; + request.context.pixel->inputAddr = PixelInputBit(PixelInput::PositionX); + request.context.pixel->posX = true; + request.context.pixel->quadPixelMask = masked ? 0x2u : 0xfu; + request.context.pixel->targetOutputMode[0] = 9u; + request.context.pixel->targetExportMapping.fill(0xe4u); + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32u; + request.target.supportedCapabilities = demote ? std::span(demoteCapabilities) : std::span(capabilities); + if (demote) request.target.supportedExtensions = demoteExtensions; + request.layout.pushConstantSizeBytes = 128u; + request.useCache = false; + return Recompile(request); + }; + const auto declares = [](const RecompileResult& result, spv::Op opcode) { + const auto& words = result.spirv.Words(); + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + require(count != 0 && count <= words.size() - cursor, "quad mask: truncated SPIR-V instruction"); + if ((words[cursor] & 0xffffu) == static_cast(opcode)) return true; + cursor += count; + } + return false; + }; + require(declares(compile(constant, true, false), spv::OpKill), "a quad mask without neighbour reads did not retain its kill fallback"); + for (const auto code : {std::span(explicitExec), std::span(constantWqm), std::span(permute)}) { + const auto unmasked = compile(code, false, false); + require(declares(unmasked, spv::OpGroupNonUniformShuffle), "the neighbour-read test did not retain its cross-lane shuffle"); + expectFailure([&] { static_cast(compile(code, true, false)); }, "shaderDemoteToHelperInvocation", "a quad mask killed helper lanes before a cross-lane read with EXEC enabled for those lanes"); + const auto masked = compile(code, true, true); + require(declares(masked, spv::OpDemoteToHelperInvocation) && declares(masked, spv::OpGroupNonUniformShuffle), "a quad mask with demote did not preserve its cross-lane shuffle path"); + } + require(declares(compile(comparisonBallot, false, false), spv::OpGroupNonUniformBallot), "the comparison test did not retain its subgroup ballot"); + expectFailure([&] { static_cast(compile(comparisonBallot, true, false)); }, "shaderDemoteToHelperInvocation", "a quad mask killed helper lanes before a ballot with EXEC enabled for those lanes"); + const auto maskedBallot = compile(comparisonBallot, true, true); + require(declares(maskedBallot, spv::OpDemoteToHelperInvocation) && declares(maskedBallot, spv::OpGroupNonUniformBallot), "a quad mask with demote did not preserve its ballot path"); +} + +void verifyFmaskImageMode() { + using namespace ShaderRecompiler; + ImageResource image{}; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2D; + DescriptorValue descriptor{{0x100u, 166u << 20u, 0u, 0x91800004u, 0u, 0u, 0u, 0u}, 8u}; + const auto modes = ResourceMaterializer::RuntimeImageModes(image); + const auto selected = ResourceMaterializer::RuntimeImageMode(image, descriptor, modes); + require(modes[selected].numericClass == IrTextureNumericClass::Uint && modes[selected].packedFormat == IrBufferFormat::Invalid && !modes[selected].constantSwizzle, "FMASK did not select a bound integer image mode"); + for (std::uint32_t format = 156u; format <= 168u; ++format) { + if (format == 166u) continue; + descriptor.dwords[1] = format << 20u; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(image, descriptor, modes)); }, "FMASK format is not implemented", "an unsupported FMASK format was accepted"); + } + descriptor.dwords[1] = 166u << 20u; + for (const auto compression : {1u << 20u, 1u << 21u}) { + descriptor.dwords[6] = compression; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(image, descriptor, modes)); }, "FMASK CMASK compression is not implemented", "compressed FMASK was accepted without decoding its metadata"); + } + descriptor.dwords[6] = 0u; + image.packed = true; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(image, descriptor, modes)); }, "packed FMASK image reads are not implemented", "packed FMASK was accepted"); + image.packed = false; + image.fmaskCompatible = false; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(image, descriptor, modes)); }, "FMASK requires a direct 32-bit image load", "an incompatible FMASK operation was accepted"); +} + void verifyCubeDepthCompareMode() { using namespace ShaderRecompiler; ImageResource image{}; @@ -4187,6 +7526,13 @@ void verifyVolumeArrayLoadMode() { require(modes[selected].dimension == RdnaImageDimension::Dim3D && !modes[selected].cube, "2D-array image load on a 3D T# did not select a volume runtime mode"); image.fmaskCompatible = false; expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(image, descriptor, ResourceMaterializer::RuntimeImageModes(image))); }, "incompatible with the static runtime image interface", "2D-array image sample on a 3D T# was accepted"); + image.volumeArraySampleCompatible = true; + const auto sampleModes = ResourceMaterializer::RuntimeImageModes(image); + const auto& sampled = sampleModes[ResourceMaterializer::RuntimeImageMode(image, descriptor, sampleModes)]; + require(sampled.dimension == RdnaImageDimension::Dim3D && !sampled.cube, "2D-array image sample on a 3D T# did not select a volume runtime mode"); + const DescriptorValue arrayDescriptor{{0x02466600u, 0x01400000u, 0u, 0xd0500204u, 1u, 0x00700000u, 0u, 0u}, 8u}; + require(sampleModes[ResourceMaterializer::RuntimeImageMode(image, arrayDescriptor, sampleModes)].dimension == RdnaImageDimension::Dim2DArray, "2D-array image sample on a 2D-array T# no longer selects the 2D-array runtime mode"); + image.volumeArraySampleCompatible = false; ImageResource store{}; store.resourceClass = ImageResourceClass::Storage; store.dimension = RdnaImageDimension::Dim2DArray; @@ -4198,6 +7544,86 @@ 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 verifyVolumeBiasTableRejection() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + ImageResource image{}; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2D; + image.singleMipPlaneBiasCompatible = true; + const DescriptorValue volume{{0x100u, TableFormat8888UNorm << 20u, 0u, (TableType3D << 28u) | 0xfacu, 0u, 0u, 0u, 0u}, 8u, 0xffu}; + const auto directModes = ResourceMaterializer::RuntimeImageModes(image); + require(directModes[ResourceMaterializer::RuntimeImageMode(image, volume, directModes)].dimension == RdnaImageDimension::Dim3D, "volume bias: a supported direct one-slice volume was rejected"); + image.table = 0u; + image.tableOperation = TableOperation::Supported; + ShaderInfo info{}; + info.images.push_back(image); + info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(image)); + ResourceSnapshot snapshot{}; + snapshot.tables.tables.resize(1u); + auto& column = snapshot.tables.tables.front(); + column.records = 2u; + column.keys = 2u; + column.codes = SharedValues(std::vector{0u, 1u}); + auto plane = volume; + plane.dwords[3] = (TableType2D << 28u) | 0xfacu; + for (const auto [word, bits] : {std::pair{4u, 1u}, std::pair{3u, 1u << 12u}, std::pair{3u, 1u << 16u}, std::pair{5u, 1u << 4u}}) { + auto invalid = volume; + invalid.dwords[word] |= bits; + column.words = SharedValues(std::vector{plane, invalid}); + const auto resolved = ResourceMaterializer::ResolveImageTables(info, snapshot); + const auto mapStart = resolved.map.at(Abi::TableHeader(0u) + Abi::TableMapStart); + const auto good = resolved.map.at(mapStart); + const auto bad = resolved.map.at(mapStart + 1u); + require(resolved.elements.size() == 1u && (resolved.map.at(good + Abi::ImageRecordFlags) & Abi::PoisonFlag) == 0u, "volume bias: an unsupported table neighbour prevented the valid plane binding"); + const auto fault = resolved.map.at(bad + Abi::ImageRecordFlags); + require((fault & Abi::PoisonFlag) != 0u && resolved.faults == 1u, "volume bias: an unsupported volume was not marked to fault on selection"); + const auto& poison = resolved.poison.at(fault & ~Abi::PoisonFlag); + require(poison.reason == Abi::PoisonReason::Dimension && poison.words == invalid.dwords, "volume bias: the table fault lost its reason or descriptor"); + image.table = NoTable; + const auto match = ResourceMaterializer::TryRuntimeImageMode(image, invalid, directModes); + require(!match.mode.has_value() && match.reason == Abi::PoisonReason::Dimension, "volume bias: an unsupported direct descriptor selected a mode"); + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(image, invalid, directModes)); }, "one slice and mip level zero only", "volume bias: direct use of an unsupported descriptor 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 verifyArrayVolumeStoreMode() { + using namespace ShaderRecompiler; + const DescriptorValue descriptor{{0x11800100u, 0xc3800000u, 0x0003c003u, 0xd0900facu, 0u, 0x00700000u, 0u, 0u}, 8u}; + ImageResource store{}; + store.resourceClass = ImageResourceClass::Storage; + store.dimension = RdnaImageDimension::Dim3D; + store.written = true; + store.fmaskCompatible = false; + const auto modes = ResourceMaterializer::RuntimeImageModes(store); + require(modes[ResourceMaterializer::RuntimeImageMode(store, descriptor, modes)].dimension == RdnaImageDimension::Dim2DArray, "3D image store on a 2D-array T# did not select an array runtime mode"); + store.read = true; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(store, descriptor, ResourceMaterializer::RuntimeImageModes(store))); }, "incompatible with the static runtime image interface", "3D image load and store on a 2D-array T# was accepted"); + store.read = false; + store.atomic = true; + const DescriptorValue uintDescriptor{{0x11800100u, 0xc1400000u, 0x0003c003u, 0xd0900facu, 0u, 0x00700000u, 0u, 0u}, 8u}; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(store, uintDescriptor, ResourceMaterializer::RuntimeImageModes(store))); }, "incompatible with the static runtime image interface", "3D image atomic on a 2D-array T# was accepted"); +} + void verifySrgbDepthCompareModes() { using namespace ShaderRecompiler; for (const auto decodeFormats : {0u, 1u, 2u, 3u}) { @@ -4218,7 +7644,17 @@ void verifySrgbDepthCompareModes() { int main(int argc, char** argv) { try { +#ifdef _WIN32 + _putenv_s("ANYPS5_NO_SHADER_CACHE", "1"); +#else + setenv("ANYPS5_NO_SHADER_CACHE", "1", 1); +#endif using namespace ShaderRecompiler; + if (argc == 2 && std::string_view(argv[1]) == "--quad-mask-no-demote") { + verifyQuadMaskNeighbourReads(); + std::cout << "Quad masks preserve neighbour-lane helpers or refuse without demote\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--frontend-pair") { verifyFrontendPair(); std::cout << "Prepared frontend reuse preserves full and partial-thread programs\n"; @@ -4229,9 +7665,64 @@ 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") { - verifyBindlessTable(); - std::cout << "Bindless mapping, indexing capabilities and strict validation passed\n"; + verifyImageTableKeys(); + verifyImageTables(); + verifyImageTableBulkReads(); + verifyImageTableScratchReuse(); + verifyImageTableBulkKeyScan(); + verifyImageTableSrtKey(); + verifyImageTableMaskedOffset(); + verifyImageTableKeyUnion(); + verifyImageTableLateKey(); + verifyImageTableUnkeyedAccess(); + verifyImageTableRegistration(); + verifyScalarAddressRule(); + verifyPointerImageTable(); + verifyPointerTableOffsetCarry(); + verifySelectedRecordFallbacks(); + verifySelectedRecordOffsetCarry(); + verifySelectedRecordDirectOperations(); + verifyEvaluableHeapSamplerDirect(); + verifySelectedRecordUniformIndex(); + verifyChainedTableOffset(); + verifyPointerChainImageTable(); + verifyWaterfallImageTable(); + verifySplitWaterfallImageTable(); + std::cout << "Image table keys, maps, records and registration passed\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--table-snapshot-reuse") { + verifyTableSnapshotReuse(); + verifyTableStampRace(); + verifyTableVouch(); + verifyTableSpanQuery(); + verifyTableEdgeStore(); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--selected-records-image-limit") { + verifySelectedRecordsWithinLimits(false, false); + std::cout << "Selected image-table records past the image limit merge back into one table per heap\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--selected-records-pair-limit") { + verifySelectedRecordsWithinLimits(true, false); + std::cout << "Selected image-table records past the sampled pair limit merge back into one table per heap\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--selected-records-direct-image-limit") { + verifySelectedRecordsWithinLimits(false, true); + std::cout << "Direct images count toward the image limit beside selected image-table records\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--selected-records-direct-pair-limit") { + verifySelectedRecordsWithinLimits(true, true); + std::cout << "Direct samplers count toward the sampled pair limit beside selected image-table records\n"; return 0; } if (argc == 2 && std::string_view(argv[1]) == "--srt-read-patching") { @@ -4239,17 +7730,34 @@ int main(int argc, char** argv) { std::cout << "SRT read patching replaces repeated and multiple uses\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--type-zero-image-materialization") { + verifyTypeZeroImageMaterialization(); + std::cout << "TYPE=0 image descriptor materialization passed\n"; + return 0; + } require(argc == 1, "unknown shader memory test arguments"); verifyRegisterSources(); verifyEvaluatedValues(); verifyFrontendPair(); verifyLayoutPair(); verifySignedSrtComparison(); + verifyTypeZeroImageMaterialization(); verifyOutOfRangeConstantReads(); verifySignedMinimum(); + verifyMayEvaluate(); verifyPureFlatSlots(); verifyGuardedDescriptorSources(); - verifyBindlessTable(); + verifySrtTape(); + verifyImageTableKeys(); + verifyImageTables(); + verifyImageTableRegistration(); + verifyScalarAddressRule(); + verifyPointerImageTable(); + verifyPointerTableOffsetCarry(); + verifyChainedTableOffset(); + verifyPointerChainImageTable(); + verifyWaterfallImageTable(); + verifySplitWaterfallImageTable(); verifyDescriptorPhis(); verifyUniformTableImage(); verifyLaneSelectedDescriptors(); @@ -4263,6 +7771,8 @@ int main(int argc, char** argv) { verifyCanonicalVersion20And21Requests(); verifyPixelRequestSerialization(); verifyVertexInfoVersion19(); + verifyDepthRemapRequests(); + verifyMergedHullConfiguration(); verifyPositionExportComponents(); verifyLegacyPixelRequests(); verifyPixelExportReplay(); @@ -4275,16 +7785,24 @@ int main(int argc, char** argv) { verifyCoherentBufferAccesses(); verifyUnnormalizedSamplers(); verifyUnusedUnnormalizedSampler(); + verifyNullVolumeUnnormalizedSampler(); verifyWaveUniformValues(); verifyTwoLaneUniformValues(); verifyBdaReadFallbackFunctions(); + verifyBdaAnchors(); verifyFunctionLdsBound(); verifyFunctionLdsUpdates(); verifySrtReadPatching(); + verifyGuardedDirectRoot(); verifyGuardedNullPointers(); + verifyFmaskImageMode(); verifyCubeDepthCompareMode(); verifyVolumeArrayLoadMode(); + verifyArrayVolumeStoreMode(); + verifyVolumeBiasTableRejection(); + verifyDynamicStorageMipChain(); verifySrgbDepthCompareModes(); + verifyQuadMaskNeighbourReads(); verifyCoherentGds(); #if ANYPS5_ENABLE_SPIRV_TOOLS const std::vector minimalSpirv{ @@ -4402,6 +7920,19 @@ int main(int argc, char** argv) { auto float16Denormals = uncached; float16Denormals.target.float16DenormPreserve = true; require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float16Denormals)).request.target.float16DenormPreserve, "f16 denormal preservation was lost in serialization"); + auto float32Flush = uncached; + float32Flush.target.float32FlushToZero = true; + require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float32Flush)).request.target.float32FlushToZero, "f32 hardware flush was lost in serialization"); + std::vector float32FlushKey; + RecompileCacheKey::Build(float32Flush, float32FlushKey); + require(plainKey != float32FlushKey, "f32 hardware flush did not change the cache key"); + auto float32Preserve = uncached; + float32Preserve.target.float32DenormPreserve = true; + require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float32Preserve)).request.target.float32DenormPreserve, "f32 denormal preservation was lost in serialization"); + require(!RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(uncached)).request.target.float32DenormPreserve, "f32 denormal preservation appeared from nowhere in serialization"); + std::vector float32PreserveKey; + RecompileCacheKey::Build(float32Preserve, float32PreserveKey); + require(plainKey != float32PreserveKey && float32FlushKey != float32PreserveKey, "f32 denormal preservation did not change the cache key"); auto changedLayout = request; changedLayout.layout.pushConstantSizeBytes = 64; require(!Recompile(changedLayout).cacheHit, "binding layout change reused an incompatible variant"); @@ -4417,6 +7948,20 @@ int main(int argc, char** argv) { expectFailure([&] { static_cast(ValidateAndOptimizeSpirv(first.spirv, 0x00400000u, 0x00010600u)); }, "unsupported Vulkan/SPIR-V target", "incompatible target accepted"); expectFailure([&] { static_cast(ValidateAndOptimizeSpirv(first.spirv, 0x00405000u, 0x00010600u)); }, "unsupported Vulkan target", "unknown Vulkan target accepted"); #endif + const auto readOnly = [&](const char* name) { + std::optional id; + bool decorated = false; + for (std::size_t at = 5; at < first.spirv.size() && (first.spirv[at] >> 16u) != 0u; at += first.spirv[at] >> 16u) { + const auto opcode = first.spirv[at] & 0xffffu; + if (opcode == spv::OpName && std::strcmp(reinterpret_cast(&first.spirv[at + 2u]), name) == 0) id = first.spirv[at + 1u]; + } + for (std::size_t at = 5; id && at < first.spirv.size() && (first.spirv[at] >> 16u) != 0u; at += first.spirv[at] >> 16u) { + decorated = decorated || ((first.spirv[at] & 0xffffu) == spv::OpDecorate && first.spirv[at + 1u] == *id && first.spirv[at + 2u] == spv::DecorationNonWritable); + } + return std::pair{id.has_value(), decorated}; + }; + require(readOnly("flattened_srt") == std::pair{true, true}, "the flattened SRT is not decorated NonWritable"); + for (const auto* table : {"bda_pagetable", "table_map"}) require(!readOnly(table).first || readOnly(table).second, "a read-only BDA page table or image table map is not decorated NonWritable"); payload = 0x40000000u; AgcDriver::ShaderMemory updatedMemory({}); const auto updatedCapture = updatedMemory.Capture(request); @@ -4438,7 +7983,8 @@ int main(int argc, char** argv) { const auto changedCapture = changedMemory.Capture(request); auto changed = updated; changed.useCache = true; - changed.context.memory = changedMemory.Regions(); + const auto changedRegions = changedMemory.Regions(); + changed.context.memory = changedRegions; auto before = updated; before.useCache = true; before.context.memory = updatedRegions; @@ -4501,7 +8047,8 @@ int main(int argc, char** argv) { AgcDriver::ShaderMemory nullRoot({}); const auto nullRootCapture = nullRoot.Capture(request); require(nullRootCapture->snapshot.nullRootReads != 0u, "reads through a null user-data pointer were not counted"); - request.context.memory = nullRoot.Regions(); + const auto nullRootRegions = nullRoot.Regions(); + request.context.memory = nullRootRegions; require(Recompile(request, *nullRootCapture)->poisonedSrtReads >= nullRootCapture->snapshot.nullRootReads, "a capture through a null user-data pointer is cacheable"); std::cout << "Shader memory capture, strict validation and deterministic replay passed\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderModeRegisters.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderModeRegisters.cpp index 6321819593..534787fa76 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderModeRegisters.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderModeRegisters.cpp @@ -12,6 +12,7 @@ #include #include #include +#include namespace { @@ -77,14 +78,74 @@ void verifyReads() { } } +std::vector opcodeSequence(const IrProgram& program) { + std::vector sequence; + for (const auto& block : program.Blocks()) { + for (const auto* instruction : block->Instructions()) { + if (instruction->Opcode() != IrOpcode::ControlNop) sequence.push_back(static_cast(instruction->Opcode())); + } + } + return sequence; +} + +constexpr std::uint32_t endProgram = 0xbf810000; +constexpr std::uint32_t getregMode = 0xb9141801; +constexpr std::uint32_t vAddF32 = 0x06000300; +constexpr std::uint32_t vCvtF32I32 = 0x7e000b00; +constexpr std::uint32_t vCvtF32U32 = 0x7e000d00; +constexpr std::uint32_t vCvtF16F32 = 0x7e001500; +constexpr std::uint32_t vAddF16 = 0x64000300; + +std::uint32_t roundMode(std::uint32_t value) { + return 0xbfa40000u | value; +} + +void requireOpcode(std::uint32_t word, RdnaOpcode expected) { + const std::array code{word, 0u, endProgram}; + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + require(!decoded.instructions.empty() && decoded.instructions.front().op == expected, "test encoding " + std::to_string(word) + " does not decode to the expected opcode"); +} + +void verifyEncodings() { + for (const auto& [word, opcode] : {std::pair{vAddF32, RdnaOpcode::VAddF32}, {vCvtF32I32, RdnaOpcode::VCvtF32I32}, {vCvtF32U32, RdnaOpcode::VCvtF32U32}, + {vCvtF16F32, RdnaOpcode::VCvtF16F32}, {vAddF16, RdnaOpcode::VAddF16}, {roundMode(8), RdnaOpcode::SRoundMode}}) { + const std::array code{word, endProgram}; + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + require(decoded.instructions.front().op == opcode, "test encoding " + std::to_string(word) + " does not decode to the expected opcode"); + } + requireOpcode(0xba801801, RdnaOpcode::SSetregImm32B32); + const std::array sgpr{0xb9800001, endProgram}; + require(RdnaInstructionDecoder{}.Decode(sgpr).instructions.front().op == RdnaOpcode::SSetregB32, "s_setreg_b32 encoding does not decode"); +} + void verifyDynamicWrites() { - for (const auto instruction : {0xb9800001u, 0xb9801801u, 0xb9800041u, 0xb9801881u, 0xb980f801u}) { - const std::array code{instruction, 0xbf810000}; + for (const auto instruction : {0xb9800001u, 0xb9801801u, 0xb9800041u}) { + const ShaderFloatMode mode{0xc0}; + const std::array unused{instruction, endProgram}; + static_cast(translate(unused, mode)); + expectRejected(unused, std::nullopt, "s_setreg_b32"); + const std::array read{instruction, getregMode, endProgram}; + expectRejected(read, mode, "s_getreg_b32", 4); + const std::array used{instruction, vAddF32, endProgram}; + expectRejected(used, mode, "reads MODE rounding bits", 4); + } + for (const auto instruction : {0xb9801881u, 0xb980f801u, 0xb9800101u}) { + const std::array code{instruction, endProgram}; expectRejected(code, std::nullopt, "s_setreg_b32"); expectRejected(code, ShaderFloatMode{0xc0}, "s_setreg_b32"); } } +void verifyDynamicWriteSurvivesPartialOverwrite() { + constexpr ShaderFloatMode mode{0xc0}; + const std::array code{0xb9801801, 0xba801801, 0, getregMode, vAddF32, endProgram}; + expectRegister(translate(code, mode), 20, 0); + const std::array partial{0xb9801801, 0xba800801, 0, getregMode, vAddF32, endProgram}; + expectRejected(partial, mode, "s_getreg_b32", 12); + const std::array split{0xb9801801, 0xba800801, 0, 0xba800881, 0, endProgram}; + static_cast(translate(split, mode)); +} + void verifyImmediateWrites() { constexpr std::array instructions{0xba801801, 0xba800801, 0xba800881, 0xba800841}; constexpr std::array masks{15, 3, 12, 6}; @@ -92,44 +153,41 @@ void verifyImmediateWrites() { for (std::uint32_t rounding = 0; rounding < 16; ++rounding) { for (std::uint32_t value = 0; value < 16; ++value) { for (std::uint32_t field = 0; field < instructions.size(); ++field) { - const std::array code{instructions[field], 0xfffffff0u | value, 0xb9141801, 0xbf810000}; + const std::array code{instructions[field], 0xfffffff0u | value, getregMode, endProgram}; const ShaderFloatMode mode{0xc0u | rounding}; - if (((value << offsets[field]) & masks[field]) != (rounding & masks[field])) { - expectRejected(code, mode, "s_setreg_imm32_b32"); - } else { - expectRegister(translate(code, mode), 20, rounding); - } + const std::uint32_t expected = (rounding & ~masks[field]) | ((value << offsets[field]) & masks[field]); + expectRegister(translate(code, mode), 20, expected); } } } - constexpr std::array full{0xba801801, 0, 0xbf810000}; + constexpr std::array full{0xba801801, 0, endProgram}; static_cast(translate(full, std::nullopt)); - constexpr std::array nonzero{0xba801801, 15, 0xbf810000}; - constexpr std::array denorm{0xba801881, 0, 0xbf810000}; - expectRejected(nonzero, ShaderFloatMode{0xc0}, "s_setreg_imm32_b32"); + constexpr std::array nonzero{0xba801801, 15, endProgram}; + expectRejected(nonzero, std::nullopt, "s_setreg_imm32_b32"); + constexpr std::array denorm{0xba801881, 0, endProgram}; expectRejected(denorm, ShaderFloatMode{0xc0}, "s_setreg_imm32_b32"); + constexpr std::array otherRegister{0xba801802, 0, endProgram}; + expectRejected(otherRegister, ShaderFloatMode{0xc0}, "s_setreg_imm32_b32"); } void verifyRoundInstruction() { for (std::uint32_t rounding = 0; rounding < 16; ++rounding) { for (std::uint32_t value = 0; value < 16; ++value) { for (const auto upper : {0u, 0x8000u}) { - const std::array code{0xbfa40000u | upper | value, 0xb9141801, 0xbf810000}; - const ShaderFloatMode mode{0xc0u | rounding}; - if (value != rounding) expectRejected(code, mode, "s_round_mode"); - else expectRegister(translate(code, mode), 20, rounding); + const std::array code{roundMode(upper | value), getregMode, endProgram}; + expectRegister(translate(code, ShaderFloatMode{0xc0u | rounding}), 20, value); } } } - constexpr std::array code{0xbfa40000, 0xbf810000}; + constexpr std::array code{0xbfa40000, endProgram}; static_cast(translate(code, std::nullopt)); - constexpr std::array nonzero{0xbfa40003, 0xbf810000}; - expectRejected(nonzero, ShaderFloatMode{0xc0}, "s_round_mode"); + constexpr std::array nonzero{0xbfa40003, endProgram}; + expectRejected(nonzero, std::nullopt, "s_round_mode"); } void verifyUnsupportedReads() { for (const auto instruction : {0xb9140002u, 0xb9140101u, 0xb91408c1u, 0xb914f801u}) { - const std::array code{instruction, 0xbf810000}; + const std::array code{instruction, endProgram}; expectRejected(code, ShaderFloatMode{0xc0}, "s_getreg_b32"); } } @@ -137,13 +195,85 @@ void verifyUnsupportedReads() { void verifyBranch() { for (std::uint32_t rounding = 0; rounding < 16; ++rounding) { const ShaderFloatMode mode{0xc0u | rounding}; - std::array code{0xbf840002, 0xba801801, rounding, 0xb9141801, 0xbf810000}; + std::array code{0xbf840002, 0xba801801, rounding, getregMode, endProgram}; expectRegister(translate(code, mode), 20, rounding); code[2] ^= 1u; - expectRejected(code, mode, "s_setreg_imm32_b32", 4); + expectRejected(code, mode, "s_getreg_b32", 12); } } +void verifyDiamond() { + for (std::uint32_t rounding = 0; rounding < 16; ++rounding) { + const ShaderFloatMode mode{0xc0u | rounding}; + const std::uint32_t other = rounding ^ 8u; + const std::array same{0xbf840003, 0xba801801, other, 0xbf820002, 0xba801801, other, getregMode, endProgram, endProgram}; + expectRegister(translate(same, mode), 20, other); + const std::array differing{0xbf840003, 0xba801801, other, 0xbf820002, 0xba801801, rounding, getregMode, endProgram, endProgram}; + expectRejected(differing, mode, "s_getreg_b32", 24); + const std::array differingUse{0xbf840003, 0xba801801, 3, 0xbf820002, 0xba801801, 0, vCvtF32I32, endProgram, endProgram}; + if (rounding == 0u) expectRejected(differingUse, mode, "reads MODE rounding bits", 24); + } +} + +void verifyRoundingBeforeDirected32Use() { + for (const auto instruction : {vCvtF32I32, vCvtF32U32}) { + for (std::uint32_t rounding = 1; rounding < 4; ++rounding) { + const std::array runtime{roundMode(rounding), instruction, endProgram}; + const std::array declared{instruction, endProgram}; + const auto converted = opcodeSequence(translate(runtime, ShaderFloatMode{0xc0})); + require(converted == opcodeSequence(translate(declared, ShaderFloatMode{0xc0u | rounding})), "runtime rounding does not match the declared rounding"); + require(converted != opcodeSequence(translate(declared, ShaderFloatMode{0xc0})), "runtime rounding did not change the conversion"); + } + } +} + +void verifyRoundingRestoredBeforeUse() { + for (const auto instruction : {vAddF32, vAddF16}) { + for (std::uint32_t directed = 1; directed < 16; ++directed) { + const std::array restored{roundMode(directed), roundMode(0), instruction, roundMode(directed), endProgram}; + const std::array plain{instruction, endProgram}; + require(opcodeSequence(translate(restored, ShaderFloatMode{0xc0})) == opcodeSequence(translate(plain, ShaderFloatMode{0xc0})), "restored rounding changed the translation"); + } + } +} + +void verifyUntouchedFieldsStayAccepted() { + const std::array f16Only{roundMode(8), vAddF32, roundMode(0), endProgram}; + const std::array plain{vAddF32, endProgram}; + require(opcodeSequence(translate(f16Only, ShaderFloatMode{0xc0})) == opcodeSequence(translate(plain, ShaderFloatMode{0xc0})), "f16 rounding changed an f32 operation"); + const std::array f32Only{roundMode(2), vAddF16, roundMode(0), endProgram}; + const std::array plain16{vAddF16, endProgram}; + require(opcodeSequence(translate(f32Only, ShaderFloatMode{0xc0})) == opcodeSequence(translate(plain16, ShaderFloatMode{0xc0})), "f32 rounding changed an f16 operation"); +} + +void verifyUnmodelledDirectedRounding() { + for (std::uint32_t directed = 1; directed < 4; ++directed) { + const std::array f32{roundMode(directed), vAddF32, endProgram}; + expectRejected(f32, ShaderFloatMode{0xc0}, "does not model directed rounding", 4); + const std::array f16{roundMode(directed << 2u), vAddF16, endProgram}; + expectRejected(f16, ShaderFloatMode{0xc0}, "does not model directed rounding", 4); + } +} + +void verifyDirectedF16Conversion() { + for (std::uint32_t rounding = 1; rounding < 4; ++rounding) { + const std::array runtime{roundMode(rounding << 2u), vCvtF16F32, endProgram}; + const std::array declared{vCvtF16F32, endProgram}; + const auto converted = opcodeSequence(translate(runtime, ShaderFloatMode{0xc0})); + require(converted == opcodeSequence(translate(declared, ShaderFloatMode{0xc0u | (rounding << 2u)})), "runtime f16 rounding does not match the declared rounding"); + expectRejected(runtime, ShaderFloatMode{0x00}, "f16 denormals not kept", 4); + } +} + +void verifyRoundingInLoop() { + const std::array stable{roundMode(0), vAddF32, 0xbf84fffd, roundMode(0), endProgram, endProgram}; + static_cast(translate(stable, ShaderFloatMode{0xc0})); + const std::array alternating{roundMode(0), vAddF32, 0xbf84fffd, roundMode(3), endProgram, endProgram}; + static_cast(translate(alternating, ShaderFloatMode{0xc0})); + const std::array carried{vAddF32, roundMode(3), 0xbf84fffd, getregMode, endProgram, endProgram}; + expectRejected(carried, ShaderFloatMode{0xc0}, "reads MODE rounding bits", 0); +} + void verifyRecompilePropagation() { constexpr std::array code{0xbfa40005, 0xbf810000}; constexpr std::array capabilities{1, 61}; @@ -175,12 +305,24 @@ void verifyRecompilePropagation() { } int main() { + struct Check { + const char* name; + void (*run)(); + }; + constexpr std::array checks{{ + {"encodings", verifyEncodings}, {"reads", verifyReads}, {"dynamicWrites", verifyDynamicWrites}, + {"partialOverwrite", verifyDynamicWriteSurvivesPartialOverwrite}, {"immediateWrites", verifyImmediateWrites}, + {"roundInstruction", verifyRoundInstruction}, {"unsupportedReads", verifyUnsupportedReads}, {"branch", verifyBranch}, + {"diamond", verifyDiamond}, {"directed32", verifyRoundingBeforeDirected32Use}, {"restored", verifyRoundingRestoredBeforeUse}, + {"untouchedFields", verifyUntouchedFieldsStayAccepted}, {"unmodelled", verifyUnmodelledDirectedRounding}, + {"directedF16", verifyDirectedF16Conversion}, {"loop", verifyRoundingInLoop}, {"propagation", verifyRecompilePropagation}, + }}; int failures = 0; - for (const auto check : {verifyReads, verifyDynamicWrites, verifyImmediateWrites, verifyRoundInstruction, verifyUnsupportedReads, verifyBranch, verifyRecompilePropagation}) { + for (const auto& check : checks) { try { - check(); + check.run(); } catch (const std::exception& error) { - std::fprintf(stderr, "%s\n", error.what()); + std::fprintf(stderr, "%s: %s\n", check.name, error.what()); ++failures; } } 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/SlabReturn.hpp b/core/libs/prx/libSceAgcDriver/tests/SlabReturn.hpp new file mode 100644 index 0000000000..3298f86e59 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SlabReturn.hpp @@ -0,0 +1,80 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SLABRETURN_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SLABRETURN_HPP + +#include "AllocationFailure.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Tests { + +inline void SlabReturnWithoutAllocation(const Graphics::Context& context, std::uint32_t memoryType) { + constexpr std::size_t capacity = std::size_t{512} << 10u; + constexpr std::size_t slotsPerBlock = 8; + Graphics::Require(Graphics::BufferPool::SlabBlockBytes(capacity) == capacity * slotsPerBlock, "unexpected slab size in the return test"); + Graphics::BufferPool pool(context); + std::array slots{}; + std::array reused{}; + unsigned returned = 0; + for (const std::size_t blocks : {4u, 6u, 3u}) { + std::printf("slab return type=%u blocks=%zu filling\n", memoryType, blocks); + std::fflush(stdout); + const auto count = blocks * slotsPerBlock; + for (std::size_t index = 0; index < count; ++index) { + slots[index] = pool.TakeSlot(context, memoryType, capacity, false); + if (slots[index].mapping != nullptr) std::memcpy(slots[index].mapping, &index, sizeof(index)); + } + Graphics::Require(pool.SlabBlocks() == blocks, "the return test did not fill every slab"); + std::puts("slab return arming host allocation failure"); + std::fflush(stdout); + const auto returnSlots = [&] { + const auto previous = std::set_terminate([] { + std::fputs("FAIL: slab return terminated during host allocation failure\n", stderr); + std::fflush(stderr); + std::_Exit(1); + }); + SetAllocationFailureCountdown(0); + for (std::size_t block = 0; block < blocks; ++block) pool.PutSlot(reused[block].memory, reused[block].offset); + SetAllocationFailureCountdown(-1); + std::set_terminate(previous); + }; + for (std::size_t block = 0; block < blocks; ++block) reused[block] = slots[block * slotsPerBlock]; + returnSlots(); + for (std::size_t block = 0; block < blocks; ++block) { + const auto index = block * slotsPerBlock + 1; + if (slots[index].mapping == nullptr) continue; + std::size_t value = 0; + std::memcpy(&value, slots[index].mapping, sizeof(value)); + Graphics::Require(value == index, "returning a slot changed a live sibling"); + } + std::array seen{}; + for (std::size_t block = 0; block < blocks; ++block) { + reused[block] = pool.TakeSlot(context, memoryType, capacity, false); + bool matched = false; + for (std::size_t candidate = 0; candidate < blocks; ++candidate) { + const auto& expected = slots[candidate * slotsPerBlock]; + if (reused[block].memory != expected.memory || reused[block].offset != expected.offset) continue; + Graphics::Require(!seen[candidate], "a returned slot was duplicated"); + seen[candidate] = true; + matched = true; + } + Graphics::Require(matched, "a returned slot was lost"); + } + returnSlots(); + VkDeviceSize released = 0; + for (std::size_t index = 0; index < count; ++index) { + if (index % slotsPerBlock != 0) released += pool.PutSlot(slots[index].memory, slots[index].offset); + } + Graphics::Require(pool.SlabBlocks() == 1 && released == (blocks - 1) * capacity * slotsPerBlock, "empty slab retirement retained the wrong number of blocks"); + returned += static_cast(blocks * 2); + } + std::printf("slab return type=%u cycles=3 returned=%u host allocation failures armed\n", memoryType, returned); +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvFailureText.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvFailureText.cpp new file mode 100644 index 0000000000..10559775ea --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvFailureText.cpp @@ -0,0 +1,75 @@ +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/ShaderStageInputInfo.hpp" +#include "SpirvBackend/SpirvEmitter.hpp" +#include "PipelineSpecialization.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +enum class Failure { MissingComputeInput, UnloweredInstruction }; + +std::string emissionFailure(Failure failure) { + IrProgram program; + program.SetWaveSize(32); + program.Resources().stage = IrShaderStage::Compute; + program.Resources().shaderHash = 0x1173ae1000ull; + program.Resources().srtPlanComplete = true; + program.Resources().resourceTrackingComplete = true; + program.Metadata().shaderInfoComplete = true; + program.Metadata().bindingLayoutComplete = true; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + program.Metadata().blockInfo.emplace_back(); + if (failure == Failure::UnloweredInstruction) block.AppendInstruction(&program.CreateValue(IrOpcode::Unreachable, IrType::Void)); + ShaderComputeInputInfo compute; + compute.threadsNum[0] = 32; + compute.threadsNum[1] = 1; + compute.threadsNum[2] = 1; + compute.hostSubgroupSize = 32; + compute.waveSize = 32; + ShaderStageInputInfo inputs; + if (failure != Failure::MissingComputeInput) inputs.compute = &compute; + CompiledBindingLayout bindings; + bindings.layout = program.Metadata().bindings; + SpirvTargetOptions target{}; + target.vulkanVersion = 0x00401000; + target.spirvVersion = 0x00010300; + target.subgroupSize = 32; + try { + static_cast(SpirvEmitter().Emit(program, inputs, bindings, target)); + } catch (const std::runtime_error& error) { + return error.what(); + } + throw std::runtime_error("the program emitted without a failure"); +} + +void requireFailure(Failure failure, std::string_view prefix, std::string_view reason) { + const auto text = emissionFailure(failure); + require(text.starts_with(prefix) && text.find(reason) != std::string::npos, "expected \"" + std::string(prefix) + "...\" with \"" + std::string(reason) + "\", got \"" + text + "\""); +} + +} + +int main() { + try { + requireFailure(Failure::MissingComputeInput, "SPIR-V emission failed: hash=0x1173ae1000 stage=", " reason=compute input info is missing"); + requireFailure(Failure::UnloweredInstruction, "SPIR-V emission failed: hash=0x1173ae1000 stage=", " opcode=Unreachable reason="); + } catch (const std::exception& error) { + std::fprintf(stderr, "SpirvFailureText: %s\n", error.what()); + return 1; + } + std::puts("SpirvFailureText: emission failures name the shader hash in hex"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvLdsAtomicLock.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvLdsAtomicLock.cpp index 977d01d44d..8155564e0b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvLdsAtomicLock.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvLdsAtomicLock.cpp @@ -1,9 +1,11 @@ #include "Recompiler.hpp" #include +#include #include #include #include #include +#include #include #include #include @@ -26,8 +28,12 @@ struct Usage { std::size_t elects = 0; std::size_t exchanges = 0; std::size_t releases = 0; + std::size_t storeReleases = 0; std::size_t adds = 0; bool addBetweenLock = false; + bool lockInitIsLaneZero = false; + bool fenceBetweenLock = false; + bool releaseWithoutReleaseSemantics = false; }; void Require(bool condition, const std::string& message) { @@ -63,25 +69,82 @@ std::vector RecompileProgram(std::span code) Usage Scan(std::span words) { Usage usage; bool locked = false; + std::uint32_t localIndexVariable = 0u; + std::uint32_t localIndexLoad = 0u; + std::uint32_t u32Type = 0u; + std::uint32_t zero = 0u; + std::uint32_t laneZeroCondition = 0u; + std::uint32_t laneZeroBody = 0u; + std::uint32_t lockPointer = 0u; + std::map constants; for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> spv::WordCountShift; const auto op = static_cast(words[cursor] & spv::OpCodeMask); Require(count != 0 && cursor + count <= words.size(), "truncated SPIR-V instruction"); + if (op == spv::OpDecorate && count >= 4u && words[cursor + 2u] == spv::DecorationBuiltIn && words[cursor + 3u] == spv::BuiltInLocalInvocationIndex) { + localIndexVariable = words[cursor + 1u]; + } + if (op == spv::OpTypeInt && count >= 4u && words[cursor + 2u] == 32u && words[cursor + 3u] == 0u) u32Type = words[cursor + 1u]; + if (op == spv::OpConstant && count >= 4u && words[cursor + 1u] == u32Type && words[cursor + 3u] == 0u) zero = words[cursor + 2u]; + if (op == spv::OpLoad && count >= 4u && words[cursor + 3u] == localIndexVariable) localIndexLoad = words[cursor + 2u]; + if (op == spv::OpIEqual && count >= 5u && ((words[cursor + 3u] == localIndexLoad && words[cursor + 4u] == zero) || (words[cursor + 4u] == localIndexLoad && words[cursor + 3u] == zero))) { + laneZeroCondition = words[cursor + 2u]; + } + if (op == spv::OpBranchConditional && count >= 4u && words[cursor + 1u] == laneZeroCondition) laneZeroBody = words[cursor + 2u]; + if (op == spv::OpConstant && count == 4) constants[words[cursor + 2]] = words[cursor + 3]; if (op == spv::OpGroupNonUniformElect) ++usage.elects; if (op == spv::OpAtomicCompareExchange) { ++usage.exchanges; locked = true; + if (count >= 4u) lockPointer = words[cursor + 3u]; } - if (op == spv::OpAtomicStore) { + if (op == spv::OpAtomicExchange) { ++usage.releases; + const auto semantics = constants.find(words[cursor + 5]); + if (semantics == constants.end() || (semantics->second & spv::MemorySemanticsReleaseMask) == 0) usage.releaseWithoutReleaseSemantics = true; locked = false; } + if (op == spv::OpAtomicStore) { + ++usage.storeReleases; + locked = false; + } + if (op == spv::OpMemoryBarrier && locked) usage.fenceBetweenLock = true; if (op == spv::OpAtomicIAdd) { ++usage.adds; if (locked) usage.addBetweenLock = true; } cursor += count; } + std::uint32_t lockBase = 0u; + std::uint32_t lockIndex = 0u; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> spv::WordCountShift; + const auto op = static_cast(words[cursor] & spv::OpCodeMask); + if (op == spv::OpAccessChain && count >= 5u && words[cursor + 2u] == lockPointer) { + lockBase = words[cursor + 3u]; + lockIndex = words[cursor + 4u]; + break; + } + cursor += count; + } + std::vector lockPointers{lockPointer}; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> spv::WordCountShift; + const auto op = static_cast(words[cursor] & spv::OpCodeMask); + if (op == spv::OpAccessChain && count >= 5u && words[cursor + 3u] == lockBase && words[cursor + 4u] == lockIndex) { + lockPointers.push_back(words[cursor + 2u]); + } + cursor += count; + } + bool inLaneZeroBody = false; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> spv::WordCountShift; + const auto op = static_cast(words[cursor] & spv::OpCodeMask); + if (op == spv::OpLabel) inLaneZeroBody = words[cursor + 1u] == laneZeroBody; + if (inLaneZeroBody && op == spv::OpStore && count >= 3u && std::find(lockPointers.begin(), lockPointers.end(), words[cursor + 1u]) != lockPointers.end()) usage.lockInitIsLaneZero = true; + if (inLaneZeroBody && (op == spv::OpBranch || op == spv::OpBranchConditional || op == spv::OpReturn)) inLaneZeroBody = false; + cursor += count; + } return usage; } @@ -95,22 +158,28 @@ Usage Compile(std::initializer_list body) { void Check64BitOnlyLocks() { const auto usage = Compile({DsAddU64, DsOperands}); Require(usage.elects == 1 && usage.exchanges == 1 && usage.releases == 1, "a ds_add_u64 is not serialised by one elected lane taking the workgroup lock"); + Require(usage.lockInitIsLaneZero, "the workgroup lock is not initialized by only local invocation zero"); + Require(usage.storeReleases == 0 && !usage.releaseWithoutReleaseSemantics, "the workgroup lock is not released by an exchange with release semantics: atomic stores " + std::to_string(usage.storeReleases)); + Require(!usage.fenceBetweenLock, "a ds_add_u64 emits a memory barrier while it holds the workgroup lock"); } void Check32BitTakesLockWith64Bit() { const auto usage = Compile({DsAddU64, DsOperands, DsAddU32Hi, DsOperand32}); Require(usage.elects == 2 && usage.exchanges == 2 && usage.releases == 2, "ds_add_u32 in a program with ds_add_u64 does not take the workgroup lock: elect " + std::to_string(usage.elects) + ", lock acquire " + std::to_string(usage.exchanges) + ", lock release " + std::to_string(usage.releases) + ", expected 2 each"); Require(usage.adds == 1 && usage.addBetweenLock, "the 32-bit OpAtomicIAdd is not between the lock acquire and the lock release"); + Require(!usage.fenceBetweenLock, "ds_add_u32 emits a memory barrier while it holds the workgroup lock"); } void CheckOtherUpdatePathTakesLock() { const auto usage = Compile({DsAddU64, DsOperands, DsIncU32Hi, DsOperand32}); Require(usage.elects == 2 && usage.exchanges >= 2 && usage.releases == 2, "ds_inc_u32 in a program with ds_add_u64 does not take the workgroup lock: elect " + std::to_string(usage.elects) + ", lock release " + std::to_string(usage.releases) + ", expected 2 each"); + Require(!usage.fenceBetweenLock, "ds_inc_u32 emits a memory barrier while it holds the workgroup lock"); } void CheckFlatAtomicTakesLockWith64Bit() { const auto usage = Compile({DsAddU64, DsOperands, FlatAtomicAddU32, FlatAtomicOperands}); Require(usage.elects == 2, "a 32-bit flat atomic into the LDS aperture in a program with ds_add_u64 does not take the workgroup lock: elect " + std::to_string(usage.elects) + ", expected 2"); + Require(!usage.fenceBetweenLock, "a 32-bit flat atomic into the LDS aperture emits a memory barrier while it holds the workgroup lock"); } void CheckFlatAtomicOnlyIsUnchanged() { diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index d067e97200..1be75623e3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -298,6 +298,64 @@ void testIdsBeyondTheBoundAreRejected() { check(rejected, "a module whose ids reach its bound was specialized"); } +void testBlockBallotsOfOnePredicateShareOneBallot() { + SpirvModule module; + const auto typeBool = module.Type(spv::OpTypeBool); + const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u); + const auto typeBallot = module.Type(spv::OpTypeVector, typeU32, 4u); + const auto typeVoid = module.Type(spv::OpTypeVoid); + const auto typeFunction = module.Type(spv::OpTypeFunction, typeVoid); + const auto scope = module.Constant(spv::OpConstant, typeU32, static_cast(spv::ScopeSubgroup)); + const auto truth = module.Constant(spv::OpConstantTrue, typeBool); + const auto other = module.Constant(spv::OpConstantFalse, typeBool); + const auto helper = module.AllocateId(); + const auto main = module.AllocateId(); + const auto first = module.AllocateId(); + const auto repeated = module.AllocateId(); + const auto distinct = module.AllocateId(); + const auto inHelper = module.AllocateId(); + const auto afterHelper = module.AllocateId(); + const auto afterCall = module.AllocateId(); + const auto afterLabel = module.AllocateId(); + const auto next = module.AllocateId(); + module.AddFunction(spv::OpFunction, typeVoid, main, spv::FunctionControlMaskNone, typeFunction); + module.AddFunction(spv::OpLabel, module.AllocateId()); + module.AddSubgroupBallot(typeBallot, first, scope, truth); + module.AddSubgroupBallot(typeBallot, repeated, scope, truth); + module.AddSubgroupBallot(typeBallot, distinct, scope, other); + module.BeginHelperFunction(); + module.AddFunction(spv::OpFunction, typeVoid, helper, spv::FunctionControlMaskNone, typeFunction); + module.AddFunction(spv::OpLabel, module.AllocateId()); + module.AddSubgroupBallot(typeBallot, inHelper, scope, truth); + module.AddFunction(spv::OpReturn); + module.AddFunction(spv::OpFunctionEnd); + module.EndHelperFunction(); + module.AddSubgroupBallot(typeBallot, afterHelper, scope, truth); + module.AddFunction(spv::OpFunctionCall, typeVoid, module.AllocateId(), helper); + module.AddSubgroupBallot(typeBallot, afterCall, scope, truth); + module.AddFunction(spv::OpBranch, next); + module.AddFunction(spv::OpLabel, next); + module.AddSubgroupBallot(typeBallot, afterLabel, scope, truth); + module.AddFunction(spv::OpReturn); + module.AddFunction(spv::OpFunctionEnd); + const auto words = module.Finalize(); + std::map ballotSources; + std::map copySources; + for (std::size_t offset = 5; offset < words.size(); offset += words[offset] >> spv::WordCountShift) { + if ((words[offset] >> spv::WordCountShift) == 0u) break; + const auto opcode = words[offset] & 0xffffu; + if (opcode == spv::OpGroupNonUniformBallot) ballotSources[words[offset + 2u]] = words[offset + 4u]; + if (opcode == spv::OpCopyObject) copySources[words[offset + 2u]] = words[offset + 3u]; + } + check(ballotSources.size() == 5u, "a block did not ballot each predicate exactly once"); + check(copySources.size() == 2u && copySources.contains(repeated) && copySources.contains(afterHelper), "a repeated ballot in one block was not a copy of the first"); + check(copySources[repeated] == first && copySources[afterHelper] == first, "a repeated ballot did not copy the block's first ballot"); + check(ballotSources[distinct] == other, "a ballot of another predicate was reused"); + check(ballotSources.contains(inHelper), "a helper function reused a ballot of the function it interrupted"); + check(ballotSources.contains(afterCall), "a ballot after a function call reused one from before the call"); + check(ballotSources.contains(afterLabel), "a ballot in a later block reused one from an earlier block"); +} + int main() { testFoldingLeavesNoDeadBlocksOrConstants(); testIdsBeyondTheBoundAreRejected(); @@ -312,6 +370,7 @@ int main() { testModulesAreIndependent(); testIdsStayUniqueBeyondCacheCapacity(); testEmittedWordsAreStable(); + testBlockBallotsOfOnePredicateShareOneBallot(); if (failures != 0) { std::fprintf(stderr, "%d check(s) failed\n", failures); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp index b9c3911406..eb5a967d31 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include #include @@ -103,13 +105,13 @@ OpDecorate %buffer Binding 0 return text + "OpReturn\nOpFunctionEnd\n"; } -std::vector assemble(const std::string& source) { +std::vector assemble(const std::string& source, std::uint32_t options = SPV_TEXT_TO_BINARY_OPTION_NONE) { spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); tools.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&, const char* message) { std::fprintf(stderr, "SPIRV-Tools: %s\n", message); }); std::vector words; - check(tools.Assemble(source, &words), "test shader did not assemble"); + check(tools.Assemble(source, &words, options), "test shader did not assemble"); return words; } @@ -166,6 +168,8 @@ void testNarrowConversions(bool optimizationEnabled) { check(opcodeCount(normalized, spv::OpStore) == (vector ? 1u : 2u), label + ": optimizer lost buffer stores"); if (!optimizationEnabled) { check(output == input, label + ": none mode changed the shader"); + const auto specialized = ShaderRecompiler::ValidateAndOptimizeSpirv(input, Vulkan11, Spirv13, false, true, true); + check(specialized == input, label + ": none mode changed the specialized shader"); } else { check(opcodeCount(normalized, spv::OpIAdd) == 0, label + ": optimizer did not remove dead arithmetic"); if (arithmetic && !type.floating) { @@ -289,6 +293,398 @@ 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()); + } +} + +std::optional> storedLaneFunction(const std::vector& words) { + std::map> values; + std::set inputs; + std::optional> stored; + bool function = false; + for (std::size_t offset = 5; offset < words.size(); offset += words[offset] >> spv::WordCountShift) { + const auto opcode = static_cast(words[offset] & spv::OpCodeMask); + const auto* operands = &words[offset + 1]; + if (opcode == spv::OpFunction) function = true; + if (opcode == spv::OpConstant) values[operands[1]] = {0u, operands[2]}; + if (opcode == spv::OpVariable && operands[2] == spv::StorageClassInput) inputs.insert(operands[1]); + if (!function) continue; + const auto operand = [&](std::uint32_t id) -> std::optional> { + const auto found = values.find(id); + return found != values.end() ? std::optional(found->second) : std::nullopt; + }; + switch (opcode) { + case spv::OpFunction: + case spv::OpLabel: + case spv::OpReturn: + case spv::OpFunctionEnd: + break; + case spv::OpLoad: + if (!inputs.contains(operands[2])) return std::nullopt; + values[operands[1]] = {1u, 0u}; + break; + case spv::OpCopyObject: { + const auto source = operand(operands[2]); + if (!source) return std::nullopt; + values[operands[1]] = *source; + break; + } + case spv::OpIAdd: { + const auto left = operand(operands[2]); + const auto right = operand(operands[3]); + if (!left || !right) return std::nullopt; + values[operands[1]] = {left->first + right->first, left->second + right->second}; + break; + } + case spv::OpStore: { + const auto value = operand(operands[1]); + if (!value || stored) return std::nullopt; + stored = value; + break; + } + default: + return std::nullopt; + } + } + return stored; +} + +void testSpecializedConstantsAreShared() { + const std::string source = R"(OpCapability Shader +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" %index +OpExecutionMode %main LocalSize 8 1 1 +OpName %103 "unused" +OpDecorate %index BuiltIn LocalInvocationIndex +%void = OpTypeVoid +%bool = OpTypeBool +%uint = OpTypeInt 32 0 +%float = OpTypeFloat 32 +%input = OpTypePointer Input %uint +%private = OpTypePointer Private %uint +%function = OpTypeFunction %void +%one = OpConstant %uint 1 +%two = OpConstant %uint 2 +%three = OpConstant %uint 3 +%index = OpVariable %input Input +%output = OpVariable %private Private +%main = OpFunction %void None %function +%entry = OpLabel +%lane = OpLoad %uint %index +%100 = OpIAdd %uint %one %one +%101 = OpIMul %uint %two %one +%102 = OpShiftLeftLogical %uint %one %one +%103 = OpIAdd %uint %two %three +%104 = OpIEqual %bool %103 %three +%105 = OpSelect %uint %104 %100 %101 +%106 = OpIAdd %uint %103 %two +%107 = OpIAdd %uint %three %103 +%108 = OpIAdd %uint %lane %100 +%109 = OpIAdd %uint %108 %101 +%110 = OpIAdd %uint %109 %102 +%111 = OpBitcast %float %110 +%112 = OpBitcast %uint %111 +%113 = OpBitcast %uint %112 +%114 = OpIAdd %uint %113 %105 +%115 = OpIAdd %uint %114 %106 +%116 = OpIAdd %uint %115 %107 +OpStore %output %116 +OpReturn +OpFunctionEnd +)"; + try { + const auto words = assemble(source, SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS); + const auto specialized = ShaderRecompiler::SpecializeSpirv(words); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specialized, Vulkan11, Spirv13, false, false)); + std::map, std::size_t> constants; + std::size_t names = 0; + for (std::size_t offset = 5; offset < specialized.size(); offset += specialized[offset] >> spv::WordCountShift) { + const auto opcode = specialized[offset] & spv::OpCodeMask; + if (opcode == spv::OpConstant) ++constants[{specialized[offset + 1], specialized[offset + 3]}]; + if (opcode == spv::OpName) ++names; + } + bool shared = true; + for (const auto& [constant, count] : constants) shared &= count == 1u; + check(shared, "specialization emitted one value of one type as several constants"); + check(constants.size() == 5u, "specialization kept an unreferenced folded constant or lost a referenced one"); + check(opcodeCount(specialized, spv::OpConstantTrue) + opcodeCount(specialized, spv::OpConstantFalse) == 0u, "specialization kept an unreferenced folded condition"); + check(names == 0u, "specialization kept the name of a dropped constant"); + check(opcodeCount(specialized, spv::OpBitcast) == 0u, "specialization kept a bitcast to the operand's own type or back to the original type"); + check(opcodeCount(specialized, spv::OpCopyObject) == 0u, "specialization left a copy of a shared constant or bitcast"); + check(opcodeCount(specialized, spv::OpIAdd) == 6u, "specialization folded " + std::to_string(10u - opcodeCount(specialized, spv::OpIAdd)) + " of the 4 lane-independent additions"); + const auto stored = storedLaneFunction(specialized); + check(stored == std::optional(std::pair{1u, 23u}), "specialization stored " + (stored ? "lane * " + std::to_string(stored->first) + " + " + std::to_string(stored->second) : std::string("a value that is not a sum of the lane and constants")) + ", expected lane + 23"); + check(ShaderRecompiler::SpecializeSpirv(specialized) == specialized, "constant sharing specialization is not stable"); + } catch (const std::exception& error) { + check(false, std::string("specialized constant sharing: ") + error.what()); + } +} + +void testFoldedConstantUsedByUndecodedInstructionIsKept() { + const std::string source = R"(OpCapability Shader +%glsl = OpExtInstImport "GLSL.std.450" +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" %index +OpExecutionMode %main LocalSize 8 1 1 +OpDecorate %index BuiltIn LocalInvocationIndex +%void = OpTypeVoid +%uint = OpTypeInt 32 0 +%input = OpTypePointer Input %uint +%private = OpTypePointer Private %uint +%function = OpTypeFunction %void +%one = OpConstant %uint 1 +%index = OpVariable %input Input +%output = OpVariable %private Private +%main = OpFunction %void None %function +%entry = OpLabel +%lane = OpLoad %uint %index +%two = OpIAdd %uint %one %one +%limited = OpExtInst %uint %glsl UMin %lane %two +OpStore %output %limited +OpReturn +OpFunctionEnd +)"; + try { + const auto words = assemble(source, SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS); + const auto specialized = ShaderRecompiler::SpecializeSpirv(words); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specialized, Vulkan11, Spirv13, false, false)); + check(opcodeCount(specialized, spv::OpExtInst) == 1u, "specialization dropped the live extended instruction"); + check(opcodeCount(specialized, spv::OpIAdd) == 0u, "specialization kept the folded addition"); + bool kept = false; + for (std::size_t offset = 5; offset < specialized.size(); offset += specialized[offset] >> spv::WordCountShift) { + if ((specialized[offset] & spv::OpCodeMask) == spv::OpConstant && specialized[offset + 3] == 2u) kept = true; + } + check(kept, "specialization dropped a folded constant used only by an undecoded instruction"); + } catch (const std::exception& error) { + check(false, std::string("folded constant used by an undecoded instruction: ") + error.what()); + } +} + +std::map booleanConstants(const std::vector& words) { + std::map constants; + for (std::size_t offset = 5; offset < words.size(); offset += words[offset] >> spv::WordCountShift) { + const auto opcode = words[offset] & spv::OpCodeMask; + if (opcode == spv::OpConstantTrue) constants[words[offset + 2]] = true; + if (opcode == spv::OpConstantFalse) constants[words[offset + 2]] = false; + } + return constants; +} + +std::vector ballotPredicates(const std::vector& words) { + std::vector predicates; + for (std::size_t offset = 5; offset < words.size(); offset += words[offset] >> spv::WordCountShift) { + if ((words[offset] & spv::OpCodeMask) == spv::OpGroupNonUniformBallot) predicates.push_back(words[offset + 4]); + } + return predicates; +} + +void testSharedConstantsAreNotCopiedForUndecodedUsers() { + const std::string source = R"(OpCapability Shader +OpCapability GroupNonUniformBallot +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" +OpExecutionMode %main LocalSize 8 1 1 +%void = OpTypeVoid +%bool = OpTypeBool +%uint = OpTypeInt 32 0 +%uvec4 = OpTypeVector %uint 4 +%private = OpTypePointer Private %uvec4 +%function = OpTypeFunction %void +%one = OpConstant %uint 1 +%two = OpConstant %uint 2 +%three = OpConstant %uint 3 +%subgroup = OpConstant %uint 3 +%output = OpVariable %private Private +%main = OpFunction %void None %function +%entry = OpLabel +%a = OpIEqual %bool %one %one +%b = OpIEqual %bool %two %two +%c = OpIEqual %bool %three %three +%d = OpINotEqual %bool %one %two +%ballotA = OpGroupNonUniformBallot %uvec4 %subgroup %a +%ballotB = OpGroupNonUniformBallot %uvec4 %subgroup %b +%ballotC = OpGroupNonUniformBallot %uvec4 %subgroup %c +%ballotD = OpGroupNonUniformBallot %uvec4 %subgroup %d +%sumAB = OpIAdd %uvec4 %ballotA %ballotB +%sumCD = OpIAdd %uvec4 %ballotC %ballotD +%sum = OpIAdd %uvec4 %sumAB %sumCD +OpStore %output %sum +OpReturn +OpFunctionEnd +)"; + try { + const auto words = assemble(source); + const auto specialized = ShaderRecompiler::SpecializeSpirv(words); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specialized, Vulkan11, Spirv13, false, false)); + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + check(tools.Validate(specialized), "specialized module with live ballots does not validate"); + check(opcodeCount(specialized, spv::OpCopyObject) == 0u, "specialization left copies of folded predicates for live ballots"); + check(opcodeCount(specialized, spv::OpIEqual) + opcodeCount(specialized, spv::OpINotEqual) == 0u, "specialization kept a constant predicate"); + check(opcodeCount(specialized, spv::OpGroupNonUniformBallot) == 4u, "specialization dropped a live ballot"); + check(specialized.size() < words.size(), "specialization made a module with constant predicates larger"); + check(opcodeCount(specialized, spv::OpConstantTrue) + opcodeCount(specialized, spv::OpConstantFalse) == 4u, "specialization emitted a different number of predicate constants than one per live predicate"); + const auto constants = booleanConstants(specialized); + const auto predicates = ballotPredicates(specialized); + check(predicates.size() == 4u, "specialization changed the number of ballot predicates"); + for (std::size_t index = 0; index < predicates.size(); ++index) { + const auto found = constants.find(predicates[index]); + check(found != constants.end() && found->second, "ballot " + std::to_string(index) + " is not fed by a true predicate constant"); + } + } catch (const std::exception& error) { + check(false, std::string("shared constants for undecoded users: ") + error.what()); + } +} + +void testSharedBooleanConstantsKeepTheirValues() { + const std::string source = R"(OpCapability Shader +OpCapability GroupNonUniformBallot +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" %index +OpExecutionMode %main LocalSize 8 1 1 +OpDecorate %index BuiltIn LocalInvocationIndex +%void = OpTypeVoid +%bool = OpTypeBool +%uint = OpTypeInt 32 0 +%uvec4 = OpTypeVector %uint 4 +%input = OpTypePointer Input %uint +%private = OpTypePointer Private %uvec4 +%function = OpTypeFunction %void +%one = OpConstant %uint 1 +%two = OpConstant %uint 2 +%subgroup = OpConstant %uint 3 +%index = OpVariable %input Input +%output = OpVariable %private Private +%main = OpFunction %void None %function +%entry = OpLabel +%lane = OpLoad %uint %index +%yes = OpIEqual %bool %one %one +%no = OpIEqual %bool %one %two +%runtime = OpULessThan %bool %lane %two +%picked = OpSelect %bool %runtime %yes %no +%ballotYes = OpGroupNonUniformBallot %uvec4 %subgroup %yes +%ballotNo = OpGroupNonUniformBallot %uvec4 %subgroup %no +%ballotPicked = OpGroupNonUniformBallot %uvec4 %subgroup %picked +%sumYesNo = OpIAdd %uvec4 %ballotYes %ballotNo +%sum = OpIAdd %uvec4 %sumYesNo %ballotPicked +OpStore %output %sum +OpReturn +OpFunctionEnd +)"; + try { + const auto words = assemble(source); + const auto specialized = ShaderRecompiler::SpecializeSpirv(words); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specialized, Vulkan11, Spirv13, false, false)); + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + check(tools.Validate(specialized), "specialized module with a true and a false predicate does not validate"); + std::uint32_t trueId = 0u, falseId = 0u, trueCount = 0u, falseCount = 0u; + for (std::size_t offset = 5; offset < specialized.size(); offset += specialized[offset] >> spv::WordCountShift) { + const auto opcode = specialized[offset] & spv::OpCodeMask; + if (opcode == spv::OpConstantTrue) { + trueId = specialized[offset + 2]; + ++trueCount; + } else if (opcode == spv::OpConstantFalse) { + falseId = specialized[offset + 2]; + ++falseCount; + } + } + check(trueCount == 1u && falseCount == 1u, "specialization did not emit one true and one false predicate constant"); + const auto constants = booleanConstants(specialized); + const auto predicates = ballotPredicates(specialized); + check(predicates.size() == 3u, "specialization changed the number of ballot predicates"); + if (predicates.size() == 3u) { + check(predicates[0] == trueId && constants.count(predicates[0]) != 0u && constants.at(predicates[0]), "ballotYes is not fed by the true predicate constant"); + check(predicates[1] == falseId && constants.count(predicates[1]) != 0u && !constants.at(predicates[1]), "ballotNo is not fed by the false predicate constant"); + } + bool distinctSelect = false; + for (std::size_t offset = 5; offset < specialized.size(); offset += specialized[offset] >> spv::WordCountShift) { + if ((specialized[offset] & spv::OpCodeMask) == spv::OpSelect && specialized[offset + 4] == trueId && specialized[offset + 5] == falseId) distinctSelect = true; + } + check(distinctSelect, "the select on a runtime condition lost its true and false arms"); + } catch (const std::exception& error) { + check(false, std::string("true and false predicates: ") + 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"); @@ -378,16 +774,186 @@ OpFunctionEnd } } +void testLongControlFlow(bool optimizationEnabled, bool large) { + const char* mode = std::getenv("APS5_SPIRV_OPT"); + const bool full = mode != nullptr && std::string(mode) == "full"; + const std::size_t blocks = large && optimizationEnabled && !full ? 512 : 8; + std::string source = R"(OpCapability Shader +OpCapability StorageBuffer8BitAccess +OpExtension "SPV_KHR_8bit_storage" +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" +OpExecutionMode %main LocalSize 1 1 1 +OpDecorate %array ArrayStride 4 +OpMemberDecorate %buffer 0 Offset 0 +OpDecorate %buffer Block +OpDecorate %data DescriptorSet 0 +OpDecorate %data Binding 0 +OpDecorate %byteArray ArrayStride 1 +OpMemberDecorate %byteBuffer 0 Offset 0 +OpDecorate %byteBuffer Block +OpDecorate %bytes DescriptorSet 0 +OpDecorate %bytes Binding 1 +%void = OpTypeVoid +%uint = OpTypeInt 32 0 +%boolean = OpTypeBool +%byte = OpTypeInt 8 0 +%byteArray = OpTypeRuntimeArray %byte +%byteBuffer = OpTypeStruct %byteArray +%byteBufferPointer = OpTypePointer StorageBuffer %byteBuffer +%bytePointer = OpTypePointer StorageBuffer %byte +%array = OpTypeRuntimeArray %uint +%buffer = OpTypeStruct %array +%bufferPointer = OpTypePointer StorageBuffer %buffer +%valuePointer = OpTypePointer StorageBuffer %uint +%function = OpTypeFunction %void +%data = OpVariable %bufferPointer StorageBuffer +%bytes = OpVariable %byteBufferPointer StorageBuffer +%zero = OpConstant %uint 0 +%one = OpConstant %uint 1 +%two = OpConstant %uint 2 +)"; + for (std::size_t i = 0; i < blocks * 4; ++i) source += "%index" + std::to_string(i) + " = OpConstant %uint " + std::to_string(i + 1) + "\n"; + source += "%main = OpFunction %void None %function\n%entry = OpLabel\n%input = OpAccessChain %valuePointer %data %zero %zero\n%seed = OpLoad %uint %input Volatile\n%condition = OpINotEqual %boolean %seed %zero\nOpBranch %block0\n"; + for (std::size_t i = 0; i < blocks; ++i) { + const auto number = std::to_string(i); + const auto previous = i == 0 ? "%seed" : "%next" + std::to_string(i - 1); + source += "%block" + number + " = OpLabel\n%sum" + number + " = OpIAdd %uint " + previous + " %one\n"; + source += "%duplicate" + number + " = OpIAdd %uint " + previous + " %one\n%shared" + number + " = OpIAdd %uint %seed %one\n"; + for (std::size_t j = 0; j < 3; ++j) { + const auto index = std::to_string(i * 4 + j); + source += "%slot" + index + " = OpAccessChain %valuePointer %data %zero %index" + index + "\n"; + source += "OpStore %slot" + index + " %" + (j == 0 ? "sum" : j == 1 ? "duplicate" : "shared") + number + " Volatile\n"; + } + const auto branchIndex = std::to_string(i * 4 + 3); + source += "%branchSlot" + number + " = OpAccessChain %valuePointer %data %zero %index" + branchIndex + "\n"; + source += "OpSelectionMerge %join" + number + " None\nOpBranchConditional %condition %left" + number + " %right" + number + "\n"; + source += "%left" + number + " = OpLabel\n%leftValue" + number + " = OpIAdd %uint %sum" + number + " %one\nOpStore %branchSlot" + number + " %leftValue" + number + " Volatile\nOpBranch %join" + number + "\n"; + source += "%right" + number + " = OpLabel\n%rightCopy" + number + " = OpIAdd %uint %sum" + number + " %one\nOpStore %branchSlot" + number + " %rightCopy" + number + " Volatile\n"; + source += "%rightValue" + number + " = OpIAdd %uint %sum" + number + " %two\nOpStore %branchSlot" + number + " %rightValue" + number + " Volatile\nOpBranch %join" + number + "\n"; + source += "%join" + number + " = OpLabel\n%next" + number + " = OpPhi %uint %leftValue" + number + " %left" + number + " %rightValue" + number + " %right" + number + "\n"; + if (i + 1 == blocks) source += "%byteSlot = OpAccessChain %bytePointer %bytes %zero %zero\n%truncated = OpUConvert %byte %next" + number + "\nOpStore %byteSlot %truncated Volatile\nOpReturn\n"; + else source += "OpBranch %block" + std::to_string(i + 1) + "\n"; + } + source += "OpFunctionEnd\n"; + const auto input = assemble(source); + const auto output = ShaderRecompiler::ValidateAndOptimizeSpirv(input, Vulkan11, Spirv13, false); + const auto stores = opcodeCount(output, spv::OpStore); + check(stores >= blocks * 4 + 1 && stores <= blocks * 6 + 1, "long control-flow optimization discarded observable stores"); + const auto adds = opcodeCount(output, spv::OpIAdd); + if (!optimizationEnabled) check(output == input, "disabled optimization changed the long control-flow module"); + else check(adds >= blocks && adds <= blocks * 4, "control-flow optimization lost arithmetic or retained redundant values"); +} + +std::size_t repeatedBlockBallots(const std::vector& words) { + std::size_t repeats = 0; + std::set> seen; + for (std::size_t offset = 5; offset < words.size(); offset += words[offset] >> spv::WordCountShift) { + const auto opcode = words[offset] & spv::OpCodeMask; + if (opcode == spv::OpLabel || opcode == spv::OpFunctionCall) seen.clear(); + if (opcode == spv::OpGroupNonUniformBallot && !seen.insert({words[offset + 1], words[offset + 3], words[offset + 4]}).second) ++repeats; + } + return repeats; +} + +std::string ballotModuleSource(const std::string& body) { + return R"(OpCapability Shader +OpCapability GroupNonUniformBallot +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" +OpExecutionMode %main LocalSize 32 1 1 +OpDecorate %runtime ArrayStride 16 +OpDecorate %block Block +OpMemberDecorate %block 0 Offset 0 +OpDecorate %buffer DescriptorSet 0 +OpDecorate %buffer Binding 0 +%void = OpTypeVoid +%function = OpTypeFunction %void +%bool = OpTypeBool +%uint = OpTypeInt 32 0 +%uvec4 = OpTypeVector %uint 4 +%runtime = OpTypeRuntimeArray %uvec4 +%block = OpTypeStruct %runtime +%blockPointer = OpTypePointer StorageBuffer %block +%elementPointer = OpTypePointer StorageBuffer %uvec4 +%buffer = OpVariable %blockPointer StorageBuffer +%zero = OpConstant %uint 0 +%one = OpConstant %uint 1 +%two = OpConstant %uint 2 +%three = OpConstant %uint 3 +%subgroup = OpConstant %uint 3 +%helper = OpFunction %void None %function +%helperEntry = OpLabel +OpReturn +OpFunctionEnd +%main = OpFunction %void None %function +%entry = OpLabel +%slot0 = OpAccessChain %elementPointer %buffer %zero %zero +%slot1 = OpAccessChain %elementPointer %buffer %zero %one +%slot2 = OpAccessChain %elementPointer %buffer %zero %two +%slot3 = OpAccessChain %elementPointer %buffer %zero %three +%loaded = OpLoad %uvec4 %slot0 +%value = OpCompositeExtract %uint %loaded 0 +%predicate = OpINotEqual %bool %value %zero +%other = OpIEqual %bool %value %one +)" + body + "OpReturn\nOpFunctionEnd\n"; +} + +void testBallotsMergedIntoOneBlockShareOneBallot(bool optimizationEnabled) { + const auto merged = assemble(ballotModuleSource(R"(%first = OpGroupNonUniformBallot %uvec4 %subgroup %predicate +OpStore %slot1 %first +OpBranch %next +%next = OpLabel +%second = OpGroupNonUniformBallot %uvec4 %subgroup %predicate +OpStore %slot2 %second +)")); + const auto output = ShaderRecompiler::ValidateAndOptimizeSpirv(merged, Vulkan11, Spirv13, false); + check(repeatedBlockBallots(output) == 0, "a block merged by the optimizer ballots one predicate twice"); + check(opcodeCount(output, spv::OpStore) == 2, "ballot collapse lost a store"); + if (optimizationEnabled) check(opcodeCount(output, spv::OpGroupNonUniformBallot) == 1, "blocks merged by the optimizer kept both ballots"); + + const auto oneBlock = assemble(ballotModuleSource(R"(%first = OpGroupNonUniformBallot %uvec4 %subgroup %predicate +%different = OpGroupNonUniformBallot %uvec4 %subgroup %other +%second = OpGroupNonUniformBallot %uvec4 %subgroup %predicate +OpStore %slot1 %first +OpStore %slot2 %different +OpStore %slot3 %second +)")); + for (const bool optimize : {false, true}) { + const auto collapsed = ShaderRecompiler::ValidateAndOptimizeSpirv(oneBlock, Vulkan11, Spirv13, false, optimize); + const auto label = std::string(optimize ? "optimized" : "unoptimized"); + check(repeatedBlockBallots(collapsed) == 0, label + ": one block ballots one predicate twice"); + check(opcodeCount(collapsed, spv::OpGroupNonUniformBallot) == 2, label + ": a different predicate lost its ballot"); + } + + const auto called = assemble(ballotModuleSource(R"(%first = OpGroupNonUniformBallot %uvec4 %subgroup %predicate +OpStore %slot1 %first +%call = OpFunctionCall %void %helper +%second = OpGroupNonUniformBallot %uvec4 %subgroup %predicate +OpStore %slot2 %second +)")); + const auto acrossCall = ShaderRecompiler::ValidateAndOptimizeSpirv(called, Vulkan11, Spirv13, false, false); + check(opcodeCount(acrossCall, spv::OpGroupNonUniformBallot) == 2, "a ballot was shared across a function call"); +} + } 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(); testSpecializedDeadContinue(); + testLongControlFlow(optimizationEnabled, false); + testLongControlFlow(optimizationEnabled, true); + testSpecializedConstantsAreShared(); + testFoldedConstantUsedByUndecodedInstructionIsKept(); + testSharedConstantsAreNotCopiedForUndecodedUsers(); + testSharedBooleanConstantsKeepTheirValues(); + testBallotsMergedIntoOneBlockShareOneBallot(optimizationEnabled); if (failures != 0) { std::fprintf(stderr, "%d optimizer check(s) failed\n", failures); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvSubvectorLoopBegin.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvSubvectorLoopBegin.cpp new file mode 100644 index 0000000000..b0d590b7d7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvSubvectorLoopBegin.cpp @@ -0,0 +1,172 @@ +#define SPV_ENABLE_UTILITY_CODE +#include +#undef SPV_ENABLE_UTILITY_CODE +#include "Recompiler.hpp" +#include "SubvectorLoopFixture.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +std::vector recompile(bool robustBufferAccess2) { + const std::array userData{0x10000000u, 0u, 0x100u, 0x31016facu, 0x10001000u, 0u, 0x900u, 0x31016facu}; + const std::array extensions{"SPV_KHR_storage_buffer_storage_class"}; + const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, SubvectorLoopFixture::ExecKeptCode, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 64; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.target.robustBufferAccess2 = robustBufferAccess2; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + auto result = Recompile(request); + require(!result.spirv.empty(), "the shader did not recompile"); + return result.spirv.Copy(); +} + +enum class EmptyExecTest { + Other, + OneCompare, + TwoCompares +}; + +class BranchConditions { +public: + explicit BranchConditions(std::vector module) : words(std::move(module)) { + require(words.size() >= 5, "the SPIR-V header is missing"); + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + require(count != 0 && count <= words.size() - offset, "a SPIR-V instruction is truncated"); + const auto opcode = static_cast(words[offset] & spv::OpCodeMask); + bool result = false; + bool type = false; + spv::HasResultAndType(opcode, &result, &type); + if (result) definitions.emplace(words[offset + (type ? 2 : 1)], offset); + if (opcode == spv::OpBranchConditional) conditions.push_back(words[offset + 1]); + offset += count; + } + } + + std::vector Tests() const { + std::vector tests; + for (const auto condition : conditions) tests.push_back(classify(condition)); + return tests; + } + +private: + std::vector words; + std::vector conditions; + std::map definitions; + + std::span instruction(std::uint32_t id, spv::Op opcode, std::size_t count) const { + const auto found = definitions.find(id); + if (found == definitions.end()) return {}; + const auto offset = found->second; + if ((words[offset] & spv::OpCodeMask) != static_cast(opcode) || (words[offset] >> spv::WordCountShift) != count) return {}; + return std::span(words).subspan(offset, count); + } + + bool zero(std::uint32_t id) const { + const auto constant = instruction(id, spv::OpConstant, 4); + return !constant.empty() && constant[3] == 0u; + } + + std::optional comparedWithZero(std::uint32_t id) const { + const auto compare = instruction(id, spv::OpIEqual, 5); + if (compare.empty()) return std::nullopt; + if (zero(compare[4])) return compare[3]; + if (zero(compare[3])) return compare[4]; + return std::nullopt; + } + + std::optional> execWord(std::uint32_t id) const { + const auto extract = instruction(id, spv::OpCompositeExtract, 5); + if (extract.empty() || instruction(extract[3], spv::OpGroupNonUniformBallot, 5).empty()) return std::nullopt; + return std::pair{extract[3], extract[4]}; + } + + bool execWords(std::uint32_t first, std::uint32_t second) const { + const auto low = execWord(first); + const auto high = execWord(second); + return low && high && low->first == high->first && low->second <= 1u && high->second <= 1u && low->second != high->second; + } + + EmptyExecTest classify(std::uint32_t condition) const { + if (const auto negation = instruction(condition, spv::OpLogicalNot, 4); !negation.empty()) return classify(negation[3]); + if (const auto operand = comparedWithZero(condition)) { + const auto combined = instruction(*operand, spv::OpBitwiseOr, 5); + if (!combined.empty() && execWords(combined[3], combined[4])) return EmptyExecTest::OneCompare; + } + const auto both = instruction(condition, spv::OpLogicalAnd, 5); + if (!both.empty()) { + const auto first = comparedWithZero(both[3]); + const auto second = comparedWithZero(both[4]); + if (first && second && execWords(*first, *second)) return EmptyExecTest::TwoCompares; + } + return EmptyExecTest::Other; + } +}; + +int failures = 0; + +void fail(const std::string& message) { + std::fprintf(stderr, "SpirvSubvectorLoopBegin: %s\n", message.c_str()); + ++failures; +} + +void check(bool robustBufferAccess2) { + const std::string mode = robustBufferAccess2 ? "robustBufferAccess2 on" : "robustBufferAccess2 off"; + std::uint32_t oneCompare = 0; + std::uint32_t twoCompares = 0; + for (const auto test : BranchConditions(recompile(robustBufferAccess2)).Tests()) { + if (test == EmptyExecTest::OneCompare) ++oneCompare; + if (test == EmptyExecTest::TwoCompares) ++twoCompares; + } + if (twoCompares != 0u) { + fail(mode + ": branches that compare exec_lo and exec_hi with 0 separately: " + std::to_string(twoCompares) + ", expected 0"); + } + if (oneCompare != 1u) { + fail(mode + ": branches that compare (exec_lo | exec_hi) with 0: " + std::to_string(oneCompare) + ", expected 1 (s_subvector_loop_begin)"); + } +} + +} + +int main() { + try { + check(false); + check(true); + } catch (const std::exception& error) { + fail(error.what()); + } + if (failures != 0) return 1; + std::puts("SpirvSubvectorLoopBegin: s_subvector_loop_begin tests an empty exec mask as (exec_lo | exec_hi) == 0 in both robustBufferAccess2 modes"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp index ca8b02d465..c3f0359632 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp @@ -1,6 +1,7 @@ #include "ControlFlow/Structurizer.hpp" #include #include +#include #include #include #include @@ -251,6 +252,17 @@ void requireSameStructuredExecutions(const std::vector>& successors, const char* name) { + if (std::getenv("APS5_FORCE_STRUCTURIZER_FALLBACK") != nullptr) return; + auto graph = makeGraph(successors); + Structurizer{}.Structurize(graph); + for (const auto& block : graph.blocks) { + if (block.terminator.condition == BranchCondition::GotoVariable || block.terminator.gotoValue >= 0) { + throw std::runtime_error(std::string(name) + ": block " + std::to_string(block.id) + " routes through a goto variable instead of a structured selection"); + } + } +} + void requireGrowthBudgetRefusal(ControlFlowGraph graph, const char* name) { const std::string growthBudget = "CFG shared merge splitting exceeded growth budget"; try { @@ -423,6 +435,9 @@ int main() { requireStructuredBranches(elseReturn, "an early return in one arm of a selection whose other arm reaches the same merge"); requireDominatedMerges(elseReturn, "an early return in one arm of a selection whose other arm reaches the same merge"); requireOriginalBranches(elseReturn, elseReturnSuccessors, "an early return in one arm of a selection whose other arm reaches the same merge"); + const std::vector> branchingReturn{{5, 1}, {3, 2}, {13}, {9, 4}, {13}, {10, 6}, {8, 7}, {13}, {13, 9}, {13}, {12, 11}, {12}, {13}, {}}; + requireSameStructuredExecutions(branchingReturn, "an arm whose returns branch once a tail shared across arms is cloned"); + requireNoRouteVariables(branchingReturn, "an arm whose returns branch once a tail shared across arms is cloned"); std::vector> diamonds; for (std::uint32_t diamond = 0; diamond < 50u; ++diamond) { const auto head = diamond * 3u; diff --git a/core/libs/prx/libSceAgcDriver/tests/SubgroupBarrier.cpp b/core/libs/prx/libSceAgcDriver/tests/SubgroupBarrier.cpp new file mode 100644 index 0000000000..272e5ed914 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SubgroupBarrier.cpp @@ -0,0 +1,110 @@ +#include "prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Case { + VkDriverId driver; + bool usable; +}; + +constexpr Case Cases[]{ + {VK_DRIVER_ID_AMD_PROPRIETARY, false}, + {VK_DRIVER_ID_AMD_OPEN_SOURCE, true}, + {VK_DRIVER_ID_MESA_RADV, true}, + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, true}, + {VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS, true}, + {VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA, true}, + {VK_DRIVER_ID_MESA_LLVMPIPE, true}, + {VK_DRIVER_ID_MOLTENVK, true}, +}; + +constexpr std::array BranchedLdsStep{ + 0x34040082u, 0x4a020081u, 0xd8340000u, 0x00000102u, 0xbf8cc07fu, 0xbf8a0000u, 0x7da800ffu, 0x00000080u, + 0xbf880006u, 0xd8d80200u, 0x03000002u, 0xbf8cc07fu, 0x4a020701u, 0xd8340000u, 0x00000102u, 0xbefe03c1u, + 0xbf8cc07fu, 0xbf8a0000u, 0xbf810000u, +}; + +struct Barriers { + std::uint32_t subgroupControl = 0; + std::uint32_t subgroupMemory = 0; +}; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +Barriers recompile(std::uint32_t threads, bool waveLdsMemoryBarriers) { + using namespace ShaderRecompiler; + const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform}; + const std::array extensions{"SPV_KHR_storage_buffer_storage_class"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, BranchedLdsStep, 0, {}}; + request.context.waveSize = 32; + request.context.compute = ShaderComputeStageInfo{{threads, 1u, 1u}, 256u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 32; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.target.waveLdsMemoryBarriers = waveLdsMemoryBarriers; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + const auto result = Recompile(request); + const auto& words = result.spirv.Words(); + std::unordered_map constants; + Barriers barriers; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpConstant && (words[at] >> 16u) == 4u) constants[words[at + 2]] = words[at + 3]; + if (op != spv::OpControlBarrier && op != spv::OpMemoryBarrier) continue; + const auto scope = constants.find(words[at + 1]); + require(scope != constants.end(), "a barrier scope is not a 32-bit constant"); + if (scope->second != spv::ScopeSubgroup) continue; + if (op == spv::OpControlBarrier) ++barriers.subgroupControl; + else ++barriers.subgroupMemory; + } + return barriers; +} + +void verifyDriverQuirk() { + for (const auto& item : Cases) { + require(AgcDriver::SubgroupControlBarrierUsable(item.driver) == item.usable, "driver " + std::to_string(static_cast(item.driver)) + ": expected subgroup control barriers to be " + (item.usable ? "usable" : "unusable")); + } +} + +void verifyEmittedBarriers() { + const auto control = recompile(256, false); + require(control.subgroupControl != 0 && control.subgroupMemory == 0, "a wave32 program in a workgroup of eight waves did not order its LDS accesses with subgroup-scope OpControlBarrier only"); + const auto memory = recompile(256, true); + require(memory.subgroupControl == 0 && memory.subgroupMemory != 0, "with wave LDS memory barriers, a wave32 program in a workgroup of eight waves did not order its LDS accesses with subgroup-scope OpMemoryBarrier only"); + const auto single = recompile(32, true); + require(single.subgroupControl == 0 && single.subgroupMemory == 0, "with wave LDS memory barriers, a single-wave workgroup did not keep workgroup-scope barriers"); +} + +} + +int main() { + try { + verifyDriverQuirk(); + verifyEmittedBarriers(); + std::puts("subgroup barrier tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index 721edbaa26..d97862941b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libc/include/GuestHeap.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" @@ -195,11 +196,26 @@ void testEndOfPipeInterrupts() { check(sceAgcDriverAddEqEvent(eq, 0, &graphicsTag) == 0, "graphics event registration failed"); check(sceAgcDriverAddEqEvent(eq, 0x20, &computeTag) == 0, "compute event registration failed"); expectFailure([] { sceAgcDriverAddEqEvent(0, 0, nullptr); }); + struct Output final : AgcDriver::IVideoOutput, AgcDriver::IFlipRequest, std::enable_shared_from_this { + std::atomic ready{0}; + std::shared_ptr Reserve(const AgcDriver::FlipInfo&) override { return shared_from_this(); } + void GpuReady(const std::shared_ptr&) override { ready.fetch_add(1); } + void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); } + }; + const auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + std::array flip{AgcDriver::FlipPacketHeader, 7, 0xfffffffeu, 1, 0, 0}; + Packet flipPacket{flip.data(), static_cast(flip.size()), 0, {}}; + std::array events{}; + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip-only queue submit failed"); + AgcDriverWaitIdle_nid_postfix(); + check(output->ready.load() == 1, "flip-only queue flip was not handed to its output"); + check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &graphicsTag && events[0].data == 1 && sceAgcDriverGetEqEventType(events.data()) == 0, "flip on a queue that never requested a release interrupt did not raise its event"); + check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip-only queue event was not cleared"); std::array words{0xc0064900, 0, 1u << 24u, 0, 0, 0, 0, 0}; Packet packet{words.data(), static_cast(words.size()), 0, {}}; check(sceAgcDriverSubmitDcb(&packet) == 0 && sceAgcDriverSubmitDcb(&packet) == 0, "interrupt submit failed"); AgcDriverWaitIdle_nid_postfix(); - std::array events{}; check(owner->GetTriggeredEvents(events.data(), 2) == 1, "graphics end-of-pipe interrupt was not delivered to its queue only"); check(events[0].filter == -14 && events[0].udata == &graphicsTag && events[0].data == 2 && sceAgcDriverGetEqEventType(events.data()) == 0, "graphics end-of-pipe event encoding is wrong"); check(sceAgcDriverGetEqContextId(events.data()) == 0, "graphics end-of-pipe event reports another context"); @@ -212,21 +228,39 @@ void testEndOfPipeInterrupts() { check(sceAgcDriverSubmitDcb(&packet) == 0, "plain release submit failed"); AgcDriverWaitIdle_nid_postfix(); check(owner->GetTriggeredEvents(events.data(), 2) == 0, "release without INT_SEL raised an interrupt"); - struct Output final : AgcDriver::IVideoOutput, AgcDriver::IFlipRequest, std::enable_shared_from_this { - std::atomic ready{0}; - std::shared_ptr Reserve(const AgcDriver::FlipInfo&) override { return shared_from_this(); } - void GpuReady(const std::shared_ptr&) override { ready.fetch_add(1); } - void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); } - }; - const auto output = std::make_shared(); - AgcDriverRegisterVideoOutput_nid_postfix(7, output); - std::array flip{AgcDriver::FlipPacketHeader, 7, 0xfffffffeu, 1, 0, 0}; - Packet flipPacket{flip.data(), static_cast(flip.size()), 0, {}}; - check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip submit failed"); + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip after a release interrupt submit failed"); AgcDriverWaitIdle_nid_postfix(); - check(output->ready.load() == 1, "flip was not handed to its output"); - check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &graphicsTag && events[0].data == 1 && sceAgcDriverGetEqEventType(events.data()) == 0, "flip did not raise the graphics end-of-pipe event"); - check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip event was not cleared"); + check(output->ready.load() == 2, "flip after a release interrupt was not handed to its output"); + check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip on a queue that already raised its own release interrupt raised a second event"); + words[2] = 1u << 24u; + check(sceAgcDriverSubmitDcb(&packet) == 0, "release before sustained flips submit failed"); + AgcDriverWaitIdle_nid_postfix(); + check(owner->GetTriggeredEvents(events.data(), 2) == 1, "a release interrupt did not deliver on a queue whose flip event is suppressed"); + std::uint32_t sustainedFlips = 0; + std::uint32_t flipsUntilEvent = 0; + while (sustainedFlips < 8 && flipsUntilEvent == 0) { + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "sustained flip submit failed"); + AgcDriverWaitIdle_nid_postfix(); + ++sustainedFlips; + check(output->ready.load() == static_cast(2 + sustainedFlips), "sustained flip was not handed to its output"); + if (owner->GetTriggeredEvents(events.data(), 2) != 0) flipsUntilEvent = sustainedFlips; + } + check(flipsUntilEvent != 0, "a flip never raised an event again on a queue whose own release interrupts stopped"); + check(flipsUntilEvent > 2, "a flip raised an event within two flips of the queue's own release interrupt"); + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip after the latch cleared submit failed"); + AgcDriverWaitIdle_nid_postfix(); + check(output->ready.load() == static_cast(3 + sustainedFlips), "flip after the latch cleared was not handed to its output"); + check(owner->GetTriggeredEvents(events.data(), 2) == 1, "a flip did not raise an event after the latch cleared"); + words[2] = 1u << 24u; + check(sceAgcDriverSubmitDcb(&packet) == 0, "release after a cleared latch submit failed"); + AgcDriverWaitIdle_nid_postfix(); + check(owner->GetTriggeredEvents(events.data(), 2) == 1, "a release interrupt did not deliver after the latch cleared"); + for (int index = 0; index < 2; ++index) { + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip paired with a release interrupt submit failed"); + AgcDriverWaitIdle_nid_postfix(); + check(output->ready.load() == static_cast(4 + sustainedFlips + index), "flip paired with a release interrupt was not handed to its output"); + check(owner->GetTriggeredEvents(events.data(), 2) == 0, "a flip raised an event within two flips of the queue's own release interrupt"); + } AgcDriverUnregisterVideoOutput_nid_postfix(7, output); alignas(8) static volatile std::uint64_t label = 0; const auto labelAddress = reinterpret_cast(&label); @@ -489,6 +523,34 @@ std::array memoryCopy(const void* destination, const void* sou return {0xc0055000, 0x60000000, static_cast(from), static_cast(static_cast(from) >> 32u), static_cast(to), static_cast(static_cast(to) >> 32u), bytes}; } +std::array dataFill(const void* destination, std::uint32_t value, std::uint32_t bytes) { + const auto to = reinterpret_cast(destination); + return {0xc0055000, 0x40300000, value, 0, static_cast(to), static_cast(static_cast(to) >> 32u), bytes}; +} + +void testLargeDataFill() { + alignas(64) static volatile std::uint32_t filled = 0; + constexpr std::size_t words = 65536; + constexpr auto bytes = static_cast(words * 4); + constexpr std::uint32_t value = 0x5a5a5a5au; + auto* produced = static_cast(GuestHeap::GuestHeapAlignForGpu_nid_postfix(65536, bytes)); + auto* destination = static_cast(GuestHeap::GuestHeapAlignForGpu_nid_postfix(65536, bytes)); + for (std::size_t i = 0; i < words; ++i) { + produced[i] = 0x96000000u | static_cast(i); + destination[i] = 0; + } + const auto drainsBefore = AgcDriver::DriverDetail::storesDrained.load(); + const auto fillsBefore = AgcDriver::DriverDetail::dmaFills.load(); + submit(0, commands(memoryCopy(destination, produced, bytes), dataFill(destination, value, bytes), writeData(&filled, 1))); + waitFor(&filled, 1, "the label after a DMA_DATA fill never landed"); + AgcDriverWaitIdle_nid_postfix(); + check(std::all_of(destination, destination + words, [](std::uint32_t word) { return word == value; }), "a DMA_DATA fill over a GPU copy's destination lost the fill or ran before the copy"); + check(AgcDriver::DriverDetail::storesDrained.load() == drainsBefore, "a DMA_DATA fill larger than 64 KiB drained the device"); + check(AgcDriver::DriverDetail::dmaFills.load() == fillsBefore + 1, "a DMA_DATA fill larger than 64 KiB was not stored as a buffer fill"); + GuestHeap::GuestHeapFree_nid_postfix(destination); + GuestHeap::GuestHeapFree_nid_postfix(produced); +} + void testCopyIntoHostMemory() { constexpr std::size_t words = 32768; constexpr auto bytes = static_cast(words * 4); @@ -512,6 +574,49 @@ void testCopyIntoHostMemory() { GuestHeap::GuestHeapFree_nid_postfix(produced); } +void testSmallCopyBehindPendingSource() { + alignas(64) static volatile std::uint32_t copied = 0, settledCopied = 0, hostCopied = 0; + constexpr std::size_t words = 32768; + constexpr auto bytes = static_cast(words * 4); + constexpr std::size_t smallWords = 1024; + constexpr auto smallBytes = static_cast(smallWords * 4); + auto* produced = static_cast(GuestHeap::GuestHeapAlignForGpu_nid_postfix(65536, bytes)); + auto* source = static_cast(GuestHeap::GuestHeapAlignForGpu_nid_postfix(65536, bytes)); + auto* destination = static_cast(GuestHeap::GuestHeapAlignForGpu_nid_postfix(65536, smallBytes)); + for (std::size_t i = 0; i < words; ++i) { + produced[i] = 0xc3000000u | static_cast(i); + source[i] = 0x3c000000u | static_cast(i); + } + std::fill(destination, destination + smallWords, 0u); + const auto before = AgcDriver::DriverDetail::copiesBehindPendingSources.load(); + submit(0, commands(memoryCopy(source, produced, bytes), memoryCopy(destination, source, smallBytes), writeData(&copied, 1))); + waitFor(&copied, 1, "the label after a small copy behind a pending source never landed"); + AgcDriverWaitIdle_nid_postfix(); + check(std::equal(destination, destination + smallWords, produced), "a small copy did not see the GPU copy into its source before it"); + if (std::getenv("APS5_COPY_SYNC") != nullptr) check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == before, "a small copy that has to wait for a completion store over its source went through the GPU copy"); + else check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == before + 1, "a small copy whose source the GPU still stores into was not recorded on the GPU"); + std::fill(destination, destination + smallWords, 0u); + const auto settled = AgcDriver::DriverDetail::copiesBehindPendingSources.load(); + submit(0, commands(memoryCopy(destination, produced, smallBytes), writeData(&settledCopied, 1))); + waitFor(&settledCopied, 1, "the label after a small copy of a settled source never landed"); + AgcDriverWaitIdle_nid_postfix(); + check(std::equal(destination, destination + smallWords, produced), "a small copy of a settled source lost bytes"); + check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == settled, "a small copy of a settled source was recorded on the GPU"); + std::vector hostDestination(smallWords); + for (std::size_t i = 0; i < words; ++i) source[i] = 0x3c000000u | static_cast(i); + const auto hostCopies = AgcDriver::DriverDetail::copiesBehindPendingSources.load(); + const auto aliasChecks = AgcDriver::VulkanDevice::CopyAliasCounts().noSource; + submit(0, commands(memoryCopy(source, produced, bytes), memoryCopy(hostDestination.data(), source, smallBytes), writeData(&hostCopied, 1))); + waitFor(&hostCopied, 1, "the label after a small copy into memory that is not imported never landed"); + AgcDriverWaitIdle_nid_postfix(); + check(std::equal(hostDestination.begin(), hostDestination.end(), produced), "a small copy into memory that is not imported did not see the GPU copy into its source"); + check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == hostCopies, "a small copy into memory that is not imported was recorded on the GPU"); + check(AgcDriver::VulkanDevice::CopyAliasCounts().noSource == aliasChecks + 1, "a small copy into memory that is not imported went through the GPU copy before its CPU path"); + GuestHeap::GuestHeapFree_nid_postfix(destination); + GuestHeap::GuestHeapFree_nid_postfix(source); + GuestHeap::GuestHeapFree_nid_postfix(produced); +} + void testShaderHeaderAlignment() { alignas(256) static const std::array code{0xbf810000}; struct Header { @@ -607,6 +712,25 @@ void testRegisteredFloatMode() { } } +void testDispatchFloatMode() { + using AgcDriver::DriverDetail::DispatchFloatMode; + using AgcDriver::DriverDetail::ShaderSnapshot; + using AgcDriver::DriverDetail::RegisteredShaderState; + const AgcDriver::Registers live{{0x212, (0xc0u << 12u) | (1u << 21u) | (1u << 26u)}}; + ShaderSnapshot raw{0x20000, 0, 0, {}, {}}; + const auto fromLive = DispatchFloatMode(raw, live); + check(fromLive.has_value() && fromLive->floatMode == 0xc0u && fromLive->dx10Clamp && !fromLive->ieeeMode && fromLive->fp16Overflow, "an unregistered compute shader does not take its float mode from the live COMPUTE_PGM_RSRC1"); + check(!DispatchFloatMode(raw, AgcDriver::Registers{}).has_value(), "an unregistered compute shader without a live COMPUTE_PGM_RSRC1 has a float mode"); + auto state = std::make_shared(); + state->shader.emplace(0x212, 1u << 23u); + ShaderSnapshot registered{0x20000, 0, 0, {}, {}}; + registered.registeredState = state; + const auto fromRegistered = DispatchFloatMode(registered, live); + check(fromRegistered.has_value() && fromRegistered->ieeeMode && fromRegistered->floatMode == 0u, "a registered compute shader takes its float mode from the live registers"); + registered.registeredState = std::make_shared(); + check(!DispatchFloatMode(registered, live).has_value(), "a registered compute shader without RSRC1 takes its float mode from the live registers"); +} + void testProgramSnapshots() { alignas(256) static std::array registered{}; registered.fill(0xbf800000); @@ -624,9 +748,10 @@ void testProgramSnapshots() { shaders[registeredAddress] = {entry}; check(AgcDriver::DriverDetail::ProgramSnapshot(shaders, registeredAddress, {2}) == entry, "a registered program was not resolved to its registration"); check(AgcDriver::DriverDetail::ProgramSnapshot(shaders, registeredAddress + 4 * 8, {2}) == entry, "an entry inside registered code was not resolved to its registration"); - check(!expectFailure([&] { static_cast(AgcDriver::DriverDetail::ProgramSnapshot(shaders, rawAddress, {2})); }).empty(), "an unregistered program was read raw while shaders are registered"); - const auto unregistered = AgcDriver::DriverDetail::ProgramSnapshot({}, rawAddress, {2}); - check(unregistered != entry && unregistered->codeAddress == rawAddress && unregistered->header.empty() && unregistered->code.size() == 2, "an empty registry did not read the program as raw code"); + const auto unregistered = AgcDriver::DriverDetail::ProgramSnapshot(shaders, rawAddress, {2}); + check(unregistered != entry && unregistered->codeAddress == rawAddress && unregistered->header.empty() && unregistered->code.size() == 2, "an unregistered program beside registered shaders was not read as raw code"); + check(AgcDriver::DriverDetail::ProgramSnapshot({}, rawAddress, {2}) == unregistered, "an empty registry did not read the program as raw code"); + check(!expectFailure([&] { static_cast(AgcDriver::DriverDetail::ProgramSnapshot(shaders, registeredAddress, {1})); }).empty(), "a registered program was accepted as another shader type"); check(AgcDriver::DriverDetail::ProgramSnapshot({}, rawAddress, {2}) == unregistered, "unchanged raw code lost its snapshot identity"); check(!expectFailure([] { static_cast(AgcDriver::DriverDetail::ProgramSnapshot({}, 0, {2})); }).empty(), "an unmapped unregistered program was accepted"); AgcDriver::DriverDetail::DrawDecode decode{}; @@ -643,6 +768,27 @@ void testProgramSnapshots() { check(AgcDriver::DriverDetail::DecodeProgramsCurrent(decode, shaders), "a decode with the rewritten program was not current"); } +void testRawGraphicsRanges() { + using AgcDriver::DriverDetail::RawGraphicsDrawnIn; + constexpr std::uint64_t base = 0x7e0000000000ull; + const auto note = [](std::uint64_t address, std::size_t dwords) { + AgcDriver::DriverDetail::ShaderSnapshot snapshot{address, 0, 0, std::vector(dwords, 0xbf810000u), {}}; + AgcDriver::DriverDetail::NoteRawGraphicsDraw(snapshot); + check(snapshot.rawGraphicsNoted->load(), "a raw graphics program was not marked as noted"); + AgcDriver::DriverDetail::NoteRawGraphicsDraw(snapshot); + }; + note(base + 0x100, 0x10); + note(base, 0x100); + note(base + 0x3f0, 0x8); + note(base + 0x800, 0x1); + check(RawGraphicsDrawnIn(base + 0x200, base + 0x210), "code inside a raw program that contains a smaller one was not reported"); + check(RawGraphicsDrawnIn(base + 0x40c, base + 0x500), "the end of a raw program that overlaps another was not reported"); + check(!RawGraphicsDrawnIn(base + 0x410, base + 0x800), "code between raw programs was reported"); + check(!RawGraphicsDrawnIn(base - 4, base), "code before a raw program was reported"); + check(RawGraphicsDrawnIn(base + 0x700, base + 0x900), "a range covering a raw program was not reported"); + check(!RawGraphicsDrawnIn(base + 0x804, base + 0x808) && !RawGraphicsDrawnIn(base + 0x200, base + 0x200), "code after a raw program or an empty range was reported"); +} + void testProgramWorkgroupUse() { const auto snapshot = [](std::vector code) { AgcDriver::DriverDetail::ShaderSnapshot result{0x1000, 0, 2, std::move(code), {}}; @@ -715,6 +861,36 @@ void testRegisteredVertexWorkgroupRouting() { check(expectFailure([&] { static_cast(VertexWorkgroupRequired(queue, registry)); }).find("reserved graphics program address") != std::string::npos, "reserved NGG program address bits were accepted"); } +void testRegisteredFastLaunchWorkgroupVertex() { + alignas(256) static const std::array code{ + 0xd8340000u, 0x00000100u, 0xbf8a0000u, 0xf80008cfu, 0x00000000u, 0xbf810000u, + }; + struct Header { + Shader shader{}; + ShaderUserData userData{}; + std::array registers{}; + }; + alignas(8) static Header header{}; + const auto address = reinterpret_cast(code.data()); + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = sizeof(code); + header.shader.code = code.data(); + header.shader.type = 2u; + header.shader.user_data = &header.userData; + header.registers = {{{0x0c8u, static_cast(address >> 8u)}, {0x0c9u, static_cast(address >> 40u)}, {0x08au, 0x022c0245u}, {0x08bu, 0x0009002au}}}; + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = header.registers.size(); + AgcDriverRegisterShader_nid_postfix(&header.shader); + const std::array stages{&header.shader}; + const std::array context{{ + {0x2d5u, 0x00482030u}, {0x1ffu, 32u}, {0x2ceu, 32u}, {0x2d3u, 64u}, {0x29bu, 2u}, {0x2abu, 4u}, + }}; + const std::array primitive{{{0x242u, 1u}, {0x25bu, 0x201u}}}; + AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, context, primitive); +} + void testWorkerFailure() { std::array words{0xc0031500, 1, 1, 1, 0x41}; Packet packet{words.data(), static_cast(words.size()), 0, {}}; @@ -735,6 +911,12 @@ void testWorkerFailure() { int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--eop-interrupts") { + testEndOfPipeInterrupts(); + LibcRunShutdown_nid_postfix(); + std::puts("AGC end-of-pipe interrupt queue mode tests passed"); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--readonly-resummarize") { testReadOnlyResummarize(); LibcRunShutdown_nid_postfix(); @@ -743,6 +925,13 @@ int main(int argc, char** argv) { } const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if (argc == 2 && std::string_view(argv[1]) == "--small-copy-sync") { + testHeaderWithoutProgramAddress(); + testSmallCopyBehindPendingSource(); + LibcRunShutdown_nid_postfix(); + std::puts("Small copies with APS5_COPY_SYNC passed"); + return 0; + } alignas(256) std::array rawCode{}; rawCode.fill(0xbf800000); rawCode[0] = 0xbe8003ff; @@ -813,9 +1002,11 @@ int main(int argc, char** argv) { check(!unterminated.empty() && bounded->code.size() == 64, "raw compute crossed inaccessible memory or missed its last instruction"); #endif testProgramSnapshots(); + testRawGraphicsRanges(); testProgramWorkgroupUse(); testVertexTailSnapshots(); testRegisteredVertexWorkgroupRouting(); + testRegisteredFastLaunchWorkgroupVertex(); testEvents(); testValidation(); testClearState(); @@ -834,7 +1025,10 @@ int main(int argc, char** argv) { testShaderHeaderAlignment(); testHeaderWithoutProgramAddress(); testCopyIntoHostMemory(); + testLargeDataFill(); + testSmallCopyBehindPendingSource(); testRegisteredFloatMode(); + testDispatchFloatMode(); testReadOnlyResummarize(); testWorkerFailure(); check(expectFailure([] { LibcRunShutdown_nid_postfix(); }).find("required shader register") != std::string::npos, "shutdown lost worker failure"); diff --git a/core/libs/prx/libSceAgcDriver/tests/SubvectorLoopFixture.hpp b/core/libs/prx/libSceAgcDriver/tests/SubvectorLoopFixture.hpp new file mode 100644 index 0000000000..2438f79b12 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SubvectorLoopFixture.hpp @@ -0,0 +1,21 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SUBVECTORLOOPFIXTURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SUBVECTORLOOPFIXTURE_HPP + +#include +#include + +namespace SubvectorLoopFixture { + +alignas(256) constexpr std::array ExecKeptCode{ + 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, + 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000a, 0x80148114, 0x4a141481, + 0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850001, 0xbe26fff6, + 0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, 0x80010a01, + 0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, 0x80010e01, + 0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, 0x80011201, + 0xbf810000, +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp b/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp new file mode 100644 index 0000000000..2ccb9129ac --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp @@ -0,0 +1,265 @@ +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/ShaderStageInputInfo.hpp" +#include "Optimization/TessellationLowering.hpp" +#include "SpirvBackend/SpirvEmitter.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +constexpr std::uint32_t InputControlPoints = 3u; +constexpr std::uint32_t LocalStride = 32u; +constexpr std::uint32_t PatchStride = InputControlPoints * LocalStride; +constexpr std::uint32_t ReadPc = 0x40u; + +bool IsKind(const IrValue* value, TessellationAttribute kind) { + return value->HasImmediate() && value->Type() == IrType::U32 && value->ImmediateU32() == static_cast(kind); +} + +struct Hull { + IrProgram program; + IrBuilder builder{program}; + IrBlock* block = nullptr; + + Hull() { + program.Resources().stage = IrShaderStage::TessellationControl; + program.Resources().memoryInfo.push_back(MemoryInfo{.kind = ResourceKind::Lds}); + static_cast(newBlock()); + } + + IrBlock& newBlock() { + auto& created = program.CreateBlock(); + program.BlockOrder().push_back(&created); + BlockInfo info; + info.id = created.Id(); + program.Metadata().blockInfo.push_back(info); + if (block == nullptr) program.SetEntryBlock(created); + block = &created; + builder.SetInsertionPoint(created); + return created; + } + + IrValue& invocation() { + return builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::InvocationId)), &builder.Constant(0u)}); + } + + IrValue& relativePatch() { + return builder.Emit(IrOpcode::TessellationBase, IrType::U32, {&builder.Constant(static_cast(TessellationBaseKind::RelativeIndex))}); + } + + IrValue& computed(std::uint32_t limit, std::uint32_t multiple) { + auto& index = builder.Emit(IrOpcode::UMin32, IrType::U32, {&invocation(), &builder.Constant(limit)}); + return builder.IMul(index, builder.Constant(multiple)); + } + + IrValue& firstInvocation() { + return builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {&invocation(), &builder.Constant(1u)}); + } + + IrValue& select(std::uint32_t firstLimit, std::uint32_t firstMultiple, IrValue& second) { + return builder.Emit(IrOpcode::SelectU32, IrType::U32, {&firstInvocation(), &computed(firstLimit, firstMultiple), &second}); + } + + IrValue& inPatch(IrValue& offset) { + return builder.IAdd(builder.IMul(relativePatch(), builder.Constant(PatchStride)), offset); + } + + IrValue& read(IrValue& address) { + auto& loaded = builder.Emit(IrOpcode::LoadSharedU32, IrType::U32, {&address, &builder.ConstantBool(true)}, MemoryFlags{0u, ReadPc}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&loaded})); + return loaded; + } + + IrValue& merge(std::uint32_t firstMultiple, std::uint32_t secondMultiple) { + IrBlock& entry = *block; + IrBlock& first = newBlock(); + IrBlock& second = newBlock(); + IrBlock& together = newBlock(); + builder.SetInsertionPoint(entry); + builder.BranchConditional(firstInvocation(), first, second); + builder.SetInsertionPoint(first); + auto& left = computed(1u, firstMultiple); + builder.Branch(together); + builder.SetInsertionPoint(second); + auto& right = computed(1u, secondMultiple); + builder.Branch(together); + builder.SetInsertionPoint(together); + auto& merged = builder.Emit(IrOpcode::Phi, IrType::U32, {}); + merged.AddPhiOperand(&first, &left); + merged.AddPhiOperand(&second, &right); + return merged; + } +}; + +struct Outcome { + bool lowered = false; + std::string reason; + const IrValue* emitted = nullptr; +}; + +const ShaderTessellationInputInfo Tessellation{.inputControlPoints = InputControlPoints, .outputControlPoints = InputControlPoints, .patchesPerGroup = 1u}; +const ShaderTessellationInputInfo GroupedTessellation{.inputControlPoints = InputControlPoints, .outputControlPoints = InputControlPoints, .patchesPerGroup = 1u, .primitiveGroupSize = 1u}; + +Outcome Lower(IrProgram& program, const ShaderTessellationInputInfo& tessellation = Tessellation) { + Outcome outcome; + try { + TessellationLowering{}.Lower(program, tessellation); + } catch (const std::exception& error) { + outcome.reason = error.what(); + return outcome; + } + outcome.lowered = true; + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::GetTessellationAttribute && IsKind(inst->Argument(0), TessellationAttribute::ControlInput)) { + outcome.emitted = inst->Argument(1)->Resolve(); + } + } + } + return outcome; +} + +bool LoweredAt(IrProgram& program, IrValue& address) { + const Outcome outcome = Lower(program); + if (!outcome.lowered) std::fprintf(stderr, "refused: %s\n", outcome.reason.c_str()); + return outcome.lowered && outcome.emitted == address.Resolve(); +} + +bool RefusedBecause(IrProgram& program, const char* reason) { + const Outcome outcome = Lower(program); + if (outcome.lowered) std::fprintf(stderr, "lowered instead of refusing: %s\n", reason); + return !outcome.lowered && outcome.reason.find(reason) != std::string::npos; +} + +std::uint32_t EmittedEvaluationControlPoint(std::uint32_t address) { + constexpr std::uint32_t ControlStride = 16u; + IrProgram program; + IrBuilder builder{program}; + program.SetWaveSize(64); + program.Resources().stage = IrShaderStage::TessellationEvaluation; + program.Resources().srtPlanComplete = true; + program.Resources().resourceTrackingComplete = true; + program.Metadata().shaderInfoComplete = true; + program.Metadata().bindingLayoutComplete = true; + program.Metadata().tessellationControlStride = ControlStride; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + BlockInfo info; + info.id = block.Id(); + program.Metadata().blockInfo.push_back(info); + builder.SetInsertionPoint(block); + auto& read = builder.Emit(IrOpcode::GetTessellationAttribute, IrType::U32, {&builder.Constant(static_cast(TessellationAttribute::EvaluationInput)), &builder.Constant(address), &builder.ConstantBool(true)}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&read})); + ShaderVertexInputInfo vertex; + vertex.tess.inputControlPoints = InputControlPoints; + vertex.tess.outputControlPoints = InputControlPoints; + vertex.tess.domain = 1u; + vertex.tess.partitioning = 2u; + vertex.tess.outputTopology = 2u; + ShaderStageInputInfo inputs; + inputs.vertex = &vertex; + CompiledBindingLayout bindings; + bindings.layout = program.Metadata().bindings; + SpirvTargetOptions target{}; + target.vulkanVersion = 0x00401000; + target.spirvVersion = 0x00010300; + target.subgroupSize = 64; + const auto words = SpirvEmitter().Emit(program, inputs, bindings, target); + std::unordered_map constants; + for (std::size_t at = 5; at < words.size(); at += words[at] >> 16u) { + const auto count = words[at] >> 16u; + if (count == 0u || at + count > words.size()) throw std::runtime_error("the domain program compiled to a malformed SPIR-V instruction"); + const auto opcode = words[at] & 0xffffu; + if (opcode == spv::OpConstant && count == 4u) constants[words[at + 2u]] = words[at + 3u]; + if (opcode == spv::OpAccessChain && count == 7u) return constants.at(words[at + 4u]); + } + throw std::runtime_error("the domain program reads no control point attribute"); +} + +bool run(const char* name, const std::function& body) { + bool passed = false; + try { + passed = body(); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s: %s\n", name, error.what()); + } + if (!passed) std::fprintf(stderr, "failed: %s\n", name); + return passed; +} + +} + +int main() { + bool passed = true; + + passed &= run("a hull read at a computed control-point index keeps the program's address", [] { + Hull hull; + auto& address = hull.inPatch(hull.computed(2u, LocalStride)); + hull.read(address); + return LoweredAt(hull.program, address); + }); + + passed &= run("a hull read at a control point selected from data keeps the program's address", [] { + Hull hull; + auto& address = hull.inPatch(hull.select(2u, LocalStride, hull.computed(1u, LocalStride))); + hull.read(address); + return LoweredAt(hull.program, address); + }); + + passed &= run("a hull read at a control point merged by a phi keeps the program's address", [] { + Hull hull; + auto& address = hull.inPatch(hull.merge(LocalStride, LocalStride)); + hull.read(address); + return LoweredAt(hull.program, address); + }); + + passed &= run("a selected control point at half a slot is refused", [] { + Hull hull; + auto& address = hull.inPatch(hull.select(2u, LocalStride, hull.computed(1u, LocalStride / 2u))); + hull.read(address); + return RefusedBecause(hull.program, "not a whole number of control points"); + }); + + passed &= run("a control point merged by a phi at half a slot is refused", [] { + Hull hull; + auto& address = hull.inPatch(hull.merge(LocalStride, LocalStride / 2u)); + hull.read(address); + return RefusedBecause(hull.program, "not a whole number of control points"); + }); + + passed &= run("a selected control point at two different offsets is refused", [] { + Hull hull; + auto& offset = hull.builder.IAdd(hull.computed(1u, LocalStride), hull.builder.Constant(LocalStride)); + auto& address = hull.inPatch(hull.select(2u, LocalStride, offset)); + hull.read(address); + return RefusedBecause(hull.program, "not a whole number of control points"); + }); + + passed &= run("a hull read at a computed control-point index stays in its patch when the relative patch is also data", [] { + Hull hull; + auto& address = hull.inPatch(hull.computed(2u, LocalStride)); + hull.read(address); + static_cast(hull.builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&hull.relativePatch()})); + const Outcome outcome = Lower(hull.program, GroupedTessellation); + if (!outcome.lowered) std::fprintf(stderr, "refused: %s\n", outcome.reason.c_str()); + if (!outcome.lowered || outcome.emitted->Opcode() != IrOpcode::ISub32 || outcome.emitted->Argument(0)->Resolve() != address.Resolve()) return false; + const IrValue* patch = outcome.emitted->Argument(1)->Resolve(); + return patch->Opcode() == IrOpcode::IMul32 && patch->Argument(1)->Resolve()->ImmediateU32() == PatchStride; + }); + + passed &= run("a domain read at an immediate address past the last control point reads the last control point", [] { + return EmittedEvaluationControlPoint(InputControlPoints * 16u) == InputControlPoints - 1u; + }); + + return passed ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp index fe008dfb1b..5874fbb5c2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp @@ -1,5 +1,7 @@ #include "GraphicsTests.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include #include @@ -184,6 +186,28 @@ void RunTextureDetilerTests(const Context& context, const TextureDetilerTestAcce reject([] { ComputeColorTargetMipLayout(ColorTileMode::RenderTarget, 4, 1024, 1024, 11, 11); }, "outside its surface"); } + { + const auto pools = access.descriptorPoolCount(); + const auto resets = access.descriptorPoolResets(); + for (int dispatch = 0; dispatch < 3 * 64; ++dispatch) detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, source, 0, destination, 0, makeLayout(8, 8, 32, 2, 32, 32), 0); + Require(access.descriptorPoolResets() == resets, "a descriptor pool was reset with no recorder to tell that its sets were no longer recorded"); + Require(access.descriptorPoolCount() >= pools + 2, "full descriptor pools with no recorder must be joined by new pools"); + } + + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Recorder recorder(context); + recorder.Activate(); + std::size_t pools = 0; + for (int batch = 0; batch < 4; ++batch) { + const auto batchCommands = recorder.Commands(); + for (int dispatch = 0; dispatch < 64; ++dispatch) detiler.Dispatch(batchCommands, TextureTileMode::kStandard4KB, 4, source, 0, destination, 0, makeLayout(8, 8, 32, 2, 32, 32), 0); + recorder.Sync(); + if (batch == 0) pools = access.descriptorPoolCount(); + } + Require(access.descriptorPoolCount() == pools, "a full descriptor pool whose batches completed must be reset and reused, not joined by a new pool"); + } + reject([&] { detiler.Dispatch(VK_NULL_HANDLE, TextureTileMode::kStandard4KB, 4, source, 0, destination, 0, layout, 0); }, "active command buffer"); reject([&] { detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, VK_NULL_HANDLE, 0, destination, 0, layout, 0); }, "source and destination buffers"); reject([&] { detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, source, 0, VK_NULL_HANDLE, 0, layout, 0); }, "source and destination buffers"); diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index 16f6aa1e66..9feb514924 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -5,6 +5,8 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include +#include +#include #include #include #include @@ -96,6 +98,70 @@ void packedChannelTests() { Require(TextureComponentChannel(133, VK_COMPONENT_SWIZZLE_ONE) == VK_COMPONENT_SWIZZLE_ONE && TextureComponentChannel(56, VK_COMPONENT_SWIZZLE_R) == VK_COMPONENT_SWIZZLE_R, "constant selectors and other formats must keep their channels"); } +float unsignedSmallFloat(std::uint32_t bits, std::uint32_t mantissaBits) { + const auto exponent = bits >> mantissaBits; + const auto mantissa = bits & ((1u << mantissaBits) - 1u); + const auto scale = static_cast(1u << mantissaBits); + if (exponent == 31u) return mantissa == 0u ? std::numeric_limits::infinity() : std::numeric_limits::quiet_NaN(); + if (exponent == 0u) return std::ldexp(static_cast(mantissa) / scale, -14); + return std::ldexp(1.0f + static_cast(mantissa) / scale, static_cast(exponent) - 15); +} + +bool sameValue(float left, float right) { + return (std::isnan(left) && std::isnan(right)) || left == right; +} + +void repackedFormatTests() { + constexpr std::uint32_t guest = 43u; + Require(IsRepackedTextureFormat(guest) && !IsRepackedTextureFormat(36u) && !IsRepackedTextureFormat(20u), "only texture format 43 is sampled through a repacked copy"); + Context context{}; + Require(SampledTextureFormat(context, guest) == VK_FORMAT_B10G11R11_UFLOAT_PACK32 && ResolveTextureFormat(guest) == VK_FORMAT_R32_UINT, "format 43 must be sampled as B10G11R11_UFLOAT and stored as R32_UINT"); + const auto field = [](std::uint32_t word, VkComponentSwizzle channel) -> std::pair { + switch (channel) { + case VK_COMPONENT_SWIZZLE_R: return {word & 0x7ffu, 6u}; + case VK_COMPONENT_SWIZZLE_G: return {(word >> 11u) & 0x7ffu, 6u}; + case VK_COMPONENT_SWIZZLE_B: return {word >> 22u, 5u}; + default: throw std::runtime_error("B10G11R11 has no such channel"); + } + }; + const auto check = [&](std::uint32_t red, std::uint32_t green, std::uint32_t blue) { + const auto word = red | (green << 10u) | (blue << 21u); + const auto repacked = RepackTextureWord(word); + const std::array, 3> expected{{{red, 5u}, {green, 6u}, {blue, 6u}}}; + constexpr std::array components{VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B}; + for (std::size_t component = 0; component < components.size(); ++component) { + const auto [bits, mantissaBits] = field(repacked, TextureComponentChannel(guest, components[component])); + Require(bits == expected[component].first && mantissaBits == expected[component].second && sameValue(unsignedSmallFloat(bits, mantissaBits), unsignedSmallFloat(expected[component].first, expected[component].second)), "the repacked word " + std::to_string(repacked) + " does not read guest component " + std::to_string(component) + " of " + std::to_string(word)); + } + }; + for (std::uint32_t red = 0; red < 1024u; ++red) check(red, 0x5a5u, 0x2c3u); + for (std::uint32_t value = 0; value < 2048u; ++value) { + check(0x155u, value, 0x7c0u); + check(0x3e0u, 0x001u, value); + } + Require(std::isinf(unsignedSmallFloat(0x3e0u, 5u)) && std::isnan(unsignedSmallFloat(0x3e1u, 5u)) && unsignedSmallFloat(0x001u, 6u) == std::ldexp(1.0f, -20), "the reference decode misreads Inf, NaN or a denormal"); + Require(TextureComponentChannel(guest, VK_COMPONENT_SWIZZLE_A) == VK_COMPONENT_SWIZZLE_A && TextureComponentChannel(guest, VK_COMPONENT_SWIZZLE_ZERO) == VK_COMPONENT_SWIZZLE_ZERO, "format 43 keeps alpha and constant selectors"); +} + +void gcnBufferFormatTests() { + using ShaderRecompiler::IrBufferFormat; + const auto& formats = ShaderRecompiler::GcnBufferFormats(); + const auto gcn = [&](std::uint32_t data, std::uint32_t number) { return static_cast(formats[(data << 3u) | number]); }; + std::array seen{}; + for (const auto format : formats) ++seen[format]; + for (std::uint32_t format = 1; format < seen.size(); ++format) Require(seen[format] == 1u, "unified buffer format " + std::to_string(format) + " must come from exactly one GCN format pair"); + Require(gcn(0u, 4u) == IrBufferFormat::Invalid && gcn(15u, 7u) == IrBufferFormat::Invalid, "GCN DATA_FORMAT 0 and 15 have no buffer form"); + Require(gcn(1u, 7u) == IrBufferFormat::Invalid && gcn(4u, 0u) == IrBufferFormat::Invalid, "8-bit float and 32-bit UNORM have no buffer form"); + Require(gcn(1u, 0u) == IrBufferFormat::Format8UNorm && gcn(2u, 7u) == IrBufferFormat::Format16Float, "8 and 16-bit pairs must map"); + Require(gcn(4u, 4u) == IrBufferFormat::Format32UInt && gcn(4u, 7u) == IrBufferFormat::Format32Float, "32-bit pairs must map"); + Require(gcn(6u, 7u) == IrBufferFormat::Format11_11_10Float && gcn(7u, 7u) == IrBufferFormat::Format10_11_11Float, "GCN 10_11_11 and 11_11_10 must map by bit layout"); + Require(gcn(8u, 0u) == IrBufferFormat::Format2_10_10_10UNorm && gcn(9u, 0u) == IrBufferFormat::Format10_10_10_2UNorm, "GCN 10_10_10_2 and 2_10_10_10 must map by bit layout"); + Require(gcn(10u, 0u) == IrBufferFormat::Format8_8_8_8UNorm && gcn(14u, 7u) == IrBufferFormat::Format32_32_32_32Float, "four-channel pairs must map"); + const std::uint32_t word3 = 0x00000facu | (7u << 12u) | (14u << 15u) | (7u << 27u); + Require(ShaderRecompiler::GcnBufferWord3(16u << 16u, word3) == (0x00000facu | (77u << 12u) | (1u << 28u)), "a strided GCN V# must check its index range"); + Require(ShaderRecompiler::GcnBufferWord3(0u, word3) == (0x00000facu | (77u << 12u) | (3u << 28u)), "a GCN V# without a stride must check its byte range"); +} + void convertedDccClearTests() { alignas(64) std::array keys{}; GuestTextureResource resource{}; @@ -163,6 +229,7 @@ void RunTextureFormatTests() { srgbDecodeTests(); srgbTwinTests(); packedChannelTests(); + repackedFormatTests(); Require(ResolveTextureFormat(1) == VK_FORMAT_R8_UNORM, "format 1 must resolve to R8_UNORM"); Require(ResolveTextureFormat(2) == VK_FORMAT_R8_SNORM, "format 2 must resolve to R8_SNORM"); Require(BytesPerElement(1) == 1u, "format 1 must be one byte wide"); @@ -215,7 +282,10 @@ void RunTextureFormatTests() { reject([] { IsBlockCompressed(200); }, "unsupported guest texture format"); for (const std::uint32_t format : {128u, 129u, 130u, 170u, 172u, 174u, 182u}) Require(IsSrgbTextureFormat(format), "guest format " + std::to_string(format) + " must be an sRGB texture format"); for (const std::uint32_t format : {1u, 14u, 56u, 71u, 169u, 171u, 173u, 181u}) Require(!IsSrgbTextureFormat(format), "guest format " + std::to_string(format) + " must not be an sRGB texture format"); + for (const std::uint32_t format : {7u, 13u, 22u, 36u, 50u, 77u, 132u, 133u, 175u, 179u}) Require(IgnoresForcedDegamma(format), "guest format " + std::to_string(format) + " must sample the same through FORCE_DEGAMMA"); + for (const std::uint32_t format : {1u, 2u, 14u, 15u, 56u, 57u, 128u, 130u, 169u, 171u, 173u, 181u}) Require(!IgnoresForcedDegamma(format), "guest format " + std::to_string(format) + " must not sample the same through FORCE_DEGAMMA"); convertedDccClearTests(); singleDccKeysTests(); packedFloatDccClearTests(); + gcnBufferFormatTests(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index c72b4f3b02..48b7f6fa98 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"); @@ -368,6 +386,124 @@ void RunTextureTilingTests() { Require(dense.pitchBytes == 1024 && CoveredMipBytes(TextureTileMode::kLinear, 4, dense) == std::vector>{{0, 3072}}, "linear rows without padding are not one range"); } + { + const auto* equation = FindTextureSwizzleEquation(XorSwizzleMode(TextureTileMode::kZ64KBX), 4u); + Require(equation != nullptr, "missing the 64KB Z_X swizzle equation of 4-byte elements"); + for (const auto mask : equation->bits) Require((mask & 0x00f80f80u) == 0u, "a 64KB Z_X address bit of a 4-byte element reads x or y bits past one 128x128 tile"); + GuestTextureResource plane{}; + plane.width = 3840; + plane.height = 2160; + plane.mipCount = 1; + plane.tileMode = TextureTileMode::kZ64KBX; + plane.dimension = TextureDimension::k2D; + plane.format = 22; + const auto geometry = DescribeSurface(plane); + const auto& mip = geometry.mips.front(); + const auto block = ThinBlockLayout(TextureTileMode::kZ64KBX, 4u); + Require(block[0] == 65536u && block[1] == 128u && block[2] == 128u, "a 64KB Z_X tile of 4-byte elements is not 128x128"); + Require(!mip.tail && mip.tiledOffset == 0u && mip.blocksPerRow == 30u && geometry.guestBytes == 510ull * 65536ull, "a 3840x2160 D32 Z_X plane is not 30x17 tiles"); + const std::array, 8> samples{{{0, 0}, {127, 127}, {128, 0}, {3839, 0}, {0, 128}, {1000, 700}, {2047, 1151}, {3839, 2159}}}; + for (const auto& [x, y] : samples) { + const auto offset = mip.tiledOffset + (static_cast(y / block[2]) * mip.blocksPerRow + x / block[1]) * block[0] + equationOffset(*equation, x, y, 0); + Require(offset / 65536u == static_cast(y >> 7u) * 30u + (x >> 7u), "element (" + std::to_string(x) + ", " + std::to_string(y) + ") of a 3840x2160 D32 Z_X plane lies outside the 64 KiB unit of its 128x128 tile"); + } + } + reject([] { ComputeSurfaceSize({}, 1); }, "empty mip chain"); reject([] { ComputeSurfaceSize(ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 4, 1), 0); }, "zero array layers"); + + { + GuestTextureResource array{}; + array.baseAddress = 0x10000000ull; + array.width = 3328; + array.height = 1872; + array.format = 1; + array.mipCount = 2; + array.dimension = TextureDimension::k2DArray; + array.tileMode = TextureTileMode::kR64KBX; + array.depthOrLastArray = 1536; + array.baseArray = 1536; + const auto layerBytes = DescribeSurface(array).layerBytes; + const auto window = ViewedArrayWindow(array); + Require(window.baseAddress == array.baseAddress + 1536ull * layerBytes && window.depthOrLastArray == 0 && window.baseArray == 0, "a sampled view of one slice at 1536 of an R_X array must start at that slice"); + Require(DescribeSurface(window).layerBytes == layerBytes && DescribeSurface(window).layers == 1, "a sampled array window must keep the slice layout"); + + auto unaligned = array; + unaligned.baseArray = 1530; + Require(ViewedArrayWindow(unaligned).baseAddress == array.baseAddress, "an R_X window at a slice the swizzle mixes in must keep the whole array"); + + auto plain = array; + plain.tileMode = TextureTileMode::kStandard64KB; + plain.baseArray = 1530; + Require(ViewedArrayWindow(plain).baseAddress == array.baseAddress + 1530ull * DescribeSurface(plain).layerBytes && ViewedArrayWindow(plain).depthOrLastArray == 6, "a non-XOR window may start at any slice"); + + auto fromZero = array; + fromZero.baseArray = 0; + Require(ViewedArrayWindow(fromZero).baseAddress == array.baseAddress && ViewedArrayWindow(fromZero).depthOrLastArray == 1536, "a view from slice 0 keeps its descriptor"); + + auto compressed = array; + compressed.dccAddress = 0x20000000ull; + Require(ViewedArrayWindow(compressed).baseAddress == array.baseAddress, "a view with DCC metadata keeps its descriptor"); + + GuestTextureResource cube{}; + cube.baseAddress = 0x30000000ull; + cube.width = 256; + cube.height = 256; + cube.format = 71; + cube.mipCount = 1; + cube.dimension = TextureDimension::kCube; + cube.tileMode = TextureTileMode::kStandard4KB; + cube.depthOrLastArray = 7232; + cube.baseArray = 7227; + const auto faces = ViewedArrayWindow(cube); + Require(faces.baseAddress == cube.baseAddress + 7227ull * DescribeSurface(cube).layerBytes && faces.depthOrLastArray == 5 && faces.baseArray == 0 && faces.dimension == TextureDimension::kCube, "a cube view of six faces at 7227 must start at its first face"); + + auto volume = cube; + volume.dimension = TextureDimension::k3D; + volume.baseArray = 0; + Require(ViewedArrayWindow(volume).baseAddress == cube.baseAddress, "a volume keeps its descriptor"); + } + + { + GuestTextureResource array{}; + array.baseAddress = 0x10000000ull; + array.width = 16; + array.height = 16; + array.depthOrLastArray = 3200; + array.baseArray = 1536; + array.mipCount = 2; + array.tileMode = TextureTileMode::kStandard64KB; + array.dimension = TextureDimension::k2DArray; + array.format = 56; + const auto geometry = DescribeSurface(array); + const auto window = ArrayLayerWindow(array, 2048); + Require(window.baseAddress == array.baseAddress + 1536ull * geometry.layerBytes, "a storage view past maxImageArrayLayers must start at its first viewed slice"); + Require(window.baseArray == 0 && window.depthOrLastArray == 1664, "a storage view past maxImageArrayLayers must hold exactly its viewed slices"); + Require(DescribeSurface(window).layerBytes == geometry.layerBytes && DescribeSurface(window).mips[1].tiledOffset == geometry.mips[1].tiledOffset, "a viewed slice must keep the layout of the whole array's slice"); + + auto fitting = array; + fitting.depthOrLastArray = 2047; + const auto kept = ArrayLayerWindow(fitting, 2048); + Require(kept.baseAddress == fitting.baseAddress && kept.baseArray == fitting.baseArray && kept.depthOrLastArray == fitting.depthOrLastArray, "an array within maxImageArrayLayers must keep its descriptor"); + + auto linear = array; + linear.tileMode = TextureTileMode::kLinear; + Require(ArrayLayerWindow(linear, 2048).baseAddress == linear.baseAddress + 1536ull * DescribeSurface(linear).layerBytes, "a linear storage view past maxImageArrayLayers must start at its first viewed slice"); + + auto wide = array; + wide.baseArray = 1000; + reject([&] { static_cast(ArrayLayerWindow(wide, 2048)); }, "more array slices than maxImageArrayLayers"); + auto xorSwizzle = array; + xorSwizzle.tileMode = TextureTileMode::kR64KBX; + reject([&] { static_cast(ArrayLayerWindow(xorSwizzle, 2048)); }, "depends on the slice"); + auto volume = array; + volume.dimension = TextureDimension::k3D; + Require(ArrayLayerWindow(volume, 2048).baseAddress == volume.baseAddress && ArrayLayerWindow(volume, 2048).depthOrLastArray == volume.depthOrLastArray, "a volume deeper than maxImageArrayLayers is not an array and keeps its descriptor"); + auto flat = array; + flat.dimension = TextureDimension::k2D; + Require(ArrayLayerWindow(flat, 2048).baseAddress == flat.baseAddress && ArrayLayerWindow(flat, 2048).depthOrLastArray == flat.depthOrLastArray, "a 2D image is one layer whatever its last array field holds and keeps its descriptor"); + auto cube = array; + cube.dimension = TextureDimension::kCube; + reject([&] { static_cast(ArrayLayerWindow(cube, 2048)); }, "not a 1D or 2D array"); + } } diff --git a/core/libs/prx/libSceAgcDriver/tests/UnsupportedDiagnostics.cpp b/core/libs/prx/libSceAgcDriver/tests/UnsupportedDiagnostics.cpp new file mode 100644 index 0000000000..db2f77f1ec --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/UnsupportedDiagnostics.cpp @@ -0,0 +1,10 @@ +#include "SceTypes.hpp" + +extern "C" void* APS5_VABI sceAgcDriverGetCoredumpAddress(); +extern "C" int APS5_VABI sceAgcDriverSetTargetRingForDiag(); + +int main() { + if (sceAgcDriverGetCoredumpAddress() != nullptr) return 1; + if (sceAgcDriverSetTargetRingForDiag() != static_cast(0x8A6D0001)) return 2; + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp index 2028dcb2b1..a69e325898 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp @@ -3,6 +3,7 @@ #include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include "Translation/InstructionTranslator.hpp" #include +#include #include #include #include @@ -21,6 +22,7 @@ using namespace ShaderRecompiler; constexpr std::uint32_t MoveM0Two = 0xbefc0382u; constexpr std::uint32_t MoveM0GroupX = 0xbefc0304u; constexpr std::uint32_t MoveV4One = 0x7e0802f2u; +constexpr std::uint32_t MoveV4Two = 0x7e0802f4u; constexpr std::uint32_t MoveV5Two = 0x7e0a02f4u; constexpr std::uint32_t MoveV6Four = 0x7e0c02f6u; constexpr std::uint32_t MovrelsV1V4 = 0x7e028704u; @@ -117,6 +119,24 @@ Shape Scan(std::span words) { return shape; } +std::size_t WindowStores(std::span words) { + std::vector functionPointers; + std::vector pointers; + std::size_t stores = 0; + const auto contains = [](const std::vector& ids, std::uint32_t id) { return std::find(ids.begin(), ids.end(), id) != ids.end(); }; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> spv::WordCountShift; + const auto op = static_cast(words[cursor] & spv::OpCodeMask); + Require(count != 0 && cursor + count <= words.size(), "truncated SPIR-V instruction"); + if (op == spv::OpTypePointer && words[cursor + 2] == spv::StorageClassFunction) functionPointers.push_back(words[cursor + 1]); + if (op == spv::OpVariable && words[cursor + 3] == spv::StorageClassFunction && contains(functionPointers, words[cursor + 1])) pointers.push_back(words[cursor + 2]); + if (op == spv::OpAccessChain && contains(pointers, words[cursor + 3])) pointers.push_back(words[cursor + 2]); + if (op == spv::OpStore && contains(pointers, words[cursor + 1])) ++stores; + cursor += count; + } + return stores; +} + IrProgram TranslateStage(ShaderStageKind stage, std::uint32_t waveSize, std::uint32_t base = 0u, std::uint32_t fetchStart = 0u, std::uint32_t fetchCount = 0u) { ShaderComputeInputInfo compute{}; compute.threadIdsNum = 3; @@ -293,6 +313,15 @@ int main(int argc, char** argv) { return shape.functionArrays == 1; }); + passed &= run("a window register written twice before a relative read keeps only its last window store", [] { + const std::array overwritten{MoveM0GroupX, MoveV4One, MoveV4Two, MoveV5Two, MoveV6Four, MovrelsV1V4, 0xe0700000u, 0x80000100u, EndProgram}; + const std::array once{MoveM0GroupX, MoveV4Two, MoveV5Two, MoveV6Four, MovrelsV1V4, 0xe0700000u, 0x80000100u, EndProgram}; + const auto twice = WindowStores(RecompileProgram(overwritten)); + const auto single = WindowStores(RecompileProgram(once)); + if (twice != single || single < 3u) std::fprintf(stderr, " window stores: written twice %zu, written once %zu\n", twice, single); + return twice == single && single >= 3u; + }); + if (passed) std::puts("Vector window shape and SSA control tests passed"); return passed ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp index 453fbbab75..2b0313c64d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp @@ -11,6 +11,8 @@ namespace { constexpr std::array BarrierExport{0xbf8a0000u, 0xf80008cfu, 0u, 0xbf810000u}; constexpr std::array BarrierLdsExport{0xd8340000u, 0u, 0xbf8a0000u, 0xf80008cfu, 0u, 0xbf810000u}; +constexpr std::array LeadingBarrierLds{0xbf8a0000u, 0xd8340000u, 0u, 0xf80008cfu, 0u, 0xbf810000u}; +constexpr std::array ScalarBarrierLds{0xbe8003c1u, 0xbf8a0000u, 0xd8340000u, 0u, 0xf80008cfu, 0u, 0xbf810000u}; constexpr std::array VertexCapabilities{spv::CapabilityClipDistance}; @@ -42,6 +44,13 @@ void verifyBarrierWithoutMemory() { if (hasControlBarrier(result)) throw std::runtime_error("a vertex program's s_barrier became a workgroup OpControlBarrier, which the Vertex execution model does not allow"); } +void verifyBarrierBeforeLds() { + for (const auto code : {std::span(LeadingBarrierLds), std::span(ScalarBarrierLds)}) { + const auto result = ShaderRecompiler::Recompile(vertexRequest(code)); + if (hasControlBarrier(result)) throw std::runtime_error("an s_barrier before every memory access of a vertex program became a workgroup OpControlBarrier"); + } +} + void verifyBarrierWithLds() { try { (void)ShaderRecompiler::Recompile(vertexRequest(BarrierLdsExport)); @@ -57,6 +66,7 @@ void verifyBarrierWithLds() { int main() { try { verifyBarrierWithoutMemory(); + verifyBarrierBeforeLds(); verifyBarrierWithLds(); std::cout << "vertex barrier tests passed\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp new file mode 100644 index 0000000000..5d56b47f05 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp @@ -0,0 +1,309 @@ +#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; +using ShaderRecompiler::VertexAttribute; +std::atomic queries{0}; + +Context context() { + Context result{}; + result.limits.maxVertexInputBindings = 32; + result.limits.maxVertexInputAttributes = 32; + result.limits.maxVertexInputBindingStride = 2048; + result.formatProperties = [](VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) { + ++queries; + *properties = {}; + if (format != VK_FORMAT_R8_UINT) properties->bufferFeatures = VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; + }; + return result; +} + +VertexAttribute attribute(std::uint32_t location = 0) { + VertexAttribute result{}; + result.location = location; + result.components = 4; + result.resource.fields = {0x1000, 32u << 16u, 128, 77u << 12u}; + return result; +} + +void same(const VertexInputLayout& a, const VertexInputLayout& b) { + Require(a.bindings.size() == b.bindings.size() && a.attributes.size() == b.attributes.size(), "layout sizes differ"); + for (std::size_t i = 0; i < a.bindings.size(); ++i) { + const auto& x = a.bindings[i]; + const auto& y = b.bindings[i]; + Require(x.binding == y.binding && x.stride == y.stride && x.inputRate == y.inputRate, "binding differs"); + const auto& p = a.attributes[i]; + const auto& q = b.attributes[i]; + Require(p.location == q.location && p.binding == q.binding && p.format == q.format && p.offset == q.offset, "attribute differs"); + } +} + +std::shared_ptr get(VertexInputCache& cache, const Context& ctx, std::uint64_t variant, std::span attributes) { + auto result = cache.TryGet(variant, attributes); + return result != nullptr ? result : std::make_shared(BuildVertexInputLayout(ctx, attributes)); +} + +template void fails(TAction&& fn) { + bool failed = false; + try { fn(); } catch (const std::runtime_error&) { failed = true; } + Require(failed, "invalid vertex input was accepted"); +} + +void run() { + auto ctx = context(); + VertexInputCache cache(ctx, 2); + std::array attributes{attribute(0), attribute(1)}; + auto first = get(cache, ctx, 1, attributes); + const auto count = queries.load(); + attributes[0].resource.fields[0] += 256; + attributes[0].resource.fields[1] |= 0x12u; + attributes[0].resource.fields[2] = 17; + Require(get(cache, ctx, 1, attributes) == first && queries == count, "live addresses or counts rebuilt the layout"); + auto invalid = attributes; + invalid[0].resource.fields[0] += 1; + fails([&] { get(cache, ctx, 1, invalid); }); + invalid = attributes; + invalid[0].resource.fields[0] = 0; + invalid[0].resource.fields[1] &= 0xffff0000u; + fails([&] { get(cache, ctx, 1, invalid); }); + for (unsigned field = 0; field < 8; ++field) { + invalid = attributes; + switch (field) { + case 0: invalid[0].components = 0; break; + case 1: invalid[0].fetchIndex = 2; break; + case 2: invalid[0].location = invalid[1].location; break; + case 3: invalid[0].resource.fields[1] |= 0x80000000u; break; + case 4: invalid[0].resource.fields[3] |= 0x00800000u; break; + case 5: invalid[0].resource.fields[3] |= 0x40000000u; break; + case 6: invalid[0].resource.fields[1] = 4096u << 16u; break; + case 7: invalid[0].resource.fields[3] = 5u << 12u; break; + } + fails([&] { get(cache, ctx, 1, invalid); }); + fails([&] { BuildVertexInputLayout(ctx, invalid); }); + } + attributes[0].resource.fields[1] = 16u << 16u; + auto second = get(cache, ctx, 2, attributes); + Require(first != second, "stride change reused a layout"); + attributes[0].fetchIndex = 1; + auto third = get(cache, ctx, 3, attributes); + Require(third != second && cache.Size() == 2 && first->bindings[0].stride == 32, "eviction or retained lifetime failed"); + attributes[0] = attribute(0); + Require(get(cache, ctx, 1, attributes) != first, "least recently used layout survived eviction"); + Require(get(cache, ctx, 0, attributes) != get(cache, ctx, 0, attributes), "unidentified variant was cached"); + VertexInputCache other(ctx); + Require(get(other, ctx, 1, attributes) != get(cache, ctx, 1, attributes), "device caches shared ownership"); + auto restricted = ctx; + restricted.limits.maxVertexInputBindings = 1; + VertexInputCache restrictedCache(restricted); + fails([&] { get(restrictedCache, restricted, 1, attributes); }); + Require(get(cache, ctx, 1, {})->attributes.empty(), "empty layout failed"); + VertexInputCache generated(ctx, 16); + std::mt19937 random(719); + for (unsigned i = 0; i < 10000; ++i) { + std::vector input; + const auto size = random() % 9; + for (unsigned n = 0; n < size; ++n) { + auto a = attribute(n); + a.components = 1 + random() % 4; + a.fetchIndex = random() % 2; + a.resource.fields[0] += (random() % 4096) * 16; + a.resource.fields[1] = (random() % 33) * 16u << 16u; + a.resource.fields[2] = random(); + const std::array formats{13u, 22u, 36u, 50u, 56u, 64u, 71u, 74u, 77u}; + a.resource.fields[3] = formats[random() % formats.size()] << 12u; + input.push_back(a); + } + const auto expected = BuildVertexInputLayout(ctx, input); + same(*get(generated, ctx, 1 + i, input), expected); + for (auto& a : input) a.resource.fields[0] += 256; + same(*get(generated, ctx, 1 + i, input), BuildVertexInputLayout(ctx, input)); + } + { + VertexInputCache live(ctx); + std::array input{attribute()}; + const auto stored = live.TryGet(100, input); + for (const auto records : {0u, 1u, 128u}) { + 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 - 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; + const auto zeroStride = live.TryGet(101, input); + Require(VertexFetchOutOfRange(input[0]), "zero records did not select zero fill"); + input[0].resource.fields[2] = 64; + Require(live.TryGet(101, input) == zeroStride && !VertexFetchOutOfRange(input[0]) && VertexBufferExtent(input[0]) == 16, "cached zero-stride layout froze the live zero-fill decision"); + input[0].resource.fields[2] = 0; + Require(live.TryGet(101, input) == zeroStride && VertexFetchOutOfRange(input[0]), "record count zero reused a live fetch"); + input[0] = attribute(); + input[0].resource.fields[1] = 0; + input[0].resource.fields[3] |= 3u << 28u; + input[0].resource.fields[2] = 16; + const auto wholeFetch = live.TryGet(105, input); + for (const auto records : {0u, 8u, 15u}) { + input[0].resource.fields[2] = records; + Require(live.TryGet(105, input) == wholeFetch && VertexFetchOutOfRange(input[0]), "a fetch that only partly fits was not read as zero"); + } + for (const auto records : {16u, 17u, 0xffffffffu}) { + input[0].resource.fields[2] = records; + Require(live.TryGet(105, input) == wholeFetch && !VertexFetchOutOfRange(input[0]) && VertexBufferExtent(input[0]) == 16, "a fetch inside the byte range was read as zero or sized by NUM_RECORDS"); + } + input[0].resource.fields[3] &= ~(3u << 28u); + Require(VertexFetchOutOfRange(input[0]), "OOB_SELECT 0 read a zero-stride fetch"); + input[0] = attribute(); + input[0].formatComponents = 1; + const auto narrow = live.TryGet(102, input); + input[0].formatComponents = 2; + const auto wide = get(live, ctx, 102, input); + Require(narrow->attributes[0].format == VK_FORMAT_R32_SFLOAT && wide->attributes[0].format == VK_FORMAT_R32G32_SFLOAT, "format component count reused the wrong vertex layout"); + input[0] = {}; + input[0].components = 4; + input[0].formatComponents = 1; + const auto nullLayout = live.TryGet(103, input); + Require(live.TryGet(103, input) == nullLayout && nullLayout->attributes[0].format == VK_FORMAT_R32G32B32A32_SFLOAT, "null vertex descriptor was rejected or narrowed on reuse"); + input[0].resource.fields[2] = 1; + fails([&] { get(live, ctx, 103, input); }); + input[0] = attribute(); + input[0].resource.fields[3] |= 2u << 28u; + input[0].resource.fields[2] = 6; + const auto bounded = live.TryGet(104, input); + Require(VertexBufferReadSize(input[0], 5, 1) == 176, "bounded vertex fetch failed"); + input[0].resource.fields[2] = 5; + Require(live.TryGet(104, input) == bounded, "bounded record count rebuilt the layout"); + fails([&] { VertexBufferReadSize(input[0], 5, 1); }); + } + { + VertexInputCache thrashing(ctx, 2); + std::array input{attribute()}; + const auto kept = thrashing.TryGet(1, input); + thrashing.TryGet(2, input); + thrashing.TryGet(3, input); + const auto queryCount = queries.load(); + for (unsigned i = 0; i < 1000; ++i) Require(thrashing.TryGet(1, input) == nullptr, "an evicted variant kept re-entering the cache"); + Require(queries == queryCount && thrashing.Size() == 2 && kept->bindings[0].stride == 32, "bypassed variant rebuilt or invalidated a retained layout"); + Require(thrashing.TryGet(65537, input) == nullptr, "bypass hash collision was not conservative"); + same(*get(thrashing, ctx, 65537, input), BuildVertexInputLayout(ctx, input)); + input[0].resource.fields[0] = 1; + fails([&] { get(thrashing, ctx, 1, input); }); + Require(thrashing.TryGet(0, input) == nullptr && thrashing.TryGet(2, {}) == nullptr, "empty or unidentified layout entered the cache"); + input[0] = attribute(); + input[0].resource.fields[1] = 16u << 16u; + Require(thrashing.TryGet(2, input) == nullptr, "a changing variant shape stayed cached"); + const auto afterChange = queries.load(); + input[0].resource.fields[1] = 32u << 16u; + for (unsigned i = 0; i < 1000; ++i) Require(thrashing.TryGet(2, input) == nullptr, "a changing variant shape kept taking the cache path"); + Require(queries == afterChange, "changing shape rebuilt in the cache"); + } + VertexInputCache concurrent(ctx); + std::array, 8> results; + std::barrier ready(8); + std::vector threads; + for (unsigned i = 0; i < results.size(); ++i) threads.emplace_back([&, i] { + ready.arrive_and_wait(); + results[i] = concurrent.TryGet(1, attributes); + }); + for (auto& thread : threads) thread.join(); + for (const auto& result : results) Require(result == results[0], "concurrent cache identity differs"); + VertexInputCache evicting(ctx, 4); + const auto expected = BuildVertexInputLayout(ctx, attributes); + threads.clear(); + for (unsigned i = 0; i < 8; ++i) threads.emplace_back([&, i] { + ready.arrive_and_wait(); + for (unsigned call = 0; call < 1000; ++call) same(*get(evicting, ctx, 1 + (call + i * 17) % 64, attributes), expected); + }); + for (auto& thread : threads) thread.join(); + Require(evicting.Size() <= 4, "concurrent evictions exceeded the layout budget"); +} + +template void benchmark() { + auto ctx = context(); + ctx.formatProperties = [](VkPhysicalDevice, VkFormat, VkFormatProperties* properties) { + *properties = {}; + properties->bufferFeatures = VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; + }; + for (const unsigned count : {0u, 1u, 4u, 8u}) { + for (const auto scenario : {"reuse-1", "reuse-64", "churn-2048", "evicted", "shape-churn"}) { + VertexInputCache cache(ctx); + std::vector input; + for (unsigned i = 0; i < count; ++i) input.push_back(attribute(i)); + const std::string_view name(scenario); + const unsigned variants = name == "reuse-64" ? 64u : name == "churn-2048" ? 2048u : name == "evicted" ? 1024u : 1u; + if (name == "evicted") for (unsigned variant = 1; variant <= 2048; ++variant) cache.TryGet(variant, input); + std::vector samples; + std::vector wallSamples; + std::uint64_t checksum = 0; + unsigned hits = 0; + constexpr unsigned calls = 32768; + for (unsigned pass = 0; pass < 10; ++pass) { + hits = 0; + const auto start = std::chrono::steady_clock::now(); + const auto cpuStart = std::clock(); + for (unsigned call = 0; call < calls; ++call) { + if constexpr (TCached) { + if (input.empty()) continue; + } + if (name == "shape-churn" && !input.empty()) input[0].resource.fields[1] = (32u + (call % 2u) * 16u) << 16u; + const auto variant = 1u + call % variants; + VertexInputLayout owned; + std::shared_ptr retained; + if constexpr (TCached) { + if (!input.empty()) retained = cache.TryGet(variant, input); + } + if (retained == nullptr) owned = BuildVertexInputLayout(ctx, input); + const auto& layout = retained != nullptr ? *retained : owned; + hits += retained != nullptr; + checksum += layout.bindings.size(); + for (const auto& binding : layout.bindings) checksum += binding.binding + binding.stride + binding.inputRate; + for (const auto& attribute : layout.attributes) checksum += attribute.location + attribute.binding + attribute.format + attribute.offset; + } + const auto cpuElapsed = static_cast(std::clock() - cpuStart) * 1e9 / CLOCKS_PER_SEC / calls; + const auto wallElapsed = std::chrono::duration(std::chrono::steady_clock::now() - start).count() / calls; + if (pass != 0) { + samples.push_back(cpuElapsed); + wallSamples.push_back(wallElapsed); + } + } + std::sort(samples.begin(), samples.end()); + std::sort(wallSamples.begin(), wallSamples.end()); + std::cout << count << ' ' << name << ": " << samples[4] << " ns/call, wall " << wallSamples[4] << " ns/call, hits " << hits << '/' << calls << ", checksum " << checksum << '\n'; + } + } +} + +} + +int main(int argc, char** argv) { + try { + if (argc == 3 && std::string_view(argv[1]) == "--benchmark-vertex-input") { + const std::string_view mode(argv[2]); + Require(mode == "baseline" || mode == "cache", "unknown vertex benchmark mode"); + if (mode == "cache") benchmark(); + else benchmark(); + return 0; + } + run(); + std::cout << "Vertex input cache tests passed\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexInputValidation.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexInputValidation.cpp new file mode 100644 index 0000000000..1411867478 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/VertexInputValidation.cpp @@ -0,0 +1,150 @@ +#include "Translation/ShaderInputInfoBuilder.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +struct BufferInput { + std::uint64_t address; + std::uint32_t stride; + std::uint32_t records; + std::uint32_t fetchIndex; +}; + +ShaderVertexStageInfo vertexInputs(const std::vector& buffers, bool embedded = false) { + ShaderVertexStageInfo vertex{}; + for (const auto& buffer : buffers) { + const auto index = vertex.resourcesNum++; + vertex.resources[index].fields = {static_cast(buffer.address), static_cast(buffer.address >> 32u) | buffer.stride << 16u, buffer.records, 0u}; + vertex.resourcesDst[index] = {static_cast(index * 4u), 4, static_cast(index), buffer.fetchIndex}; + } + vertex.fetchEmbedded = embedded; + return vertex; +} + +template +std::string errorOf(TRun&& run) { + try { + run(); + } catch (const std::exception& error) { + return error.what(); + } + return {}; +} + +} + +int main() { + std::vector> vertexCases{std::nullopt}; + auto overfull = vertexInputs({{0x10000u, 16u, 100u, 0u}}); + overfull.resourcesNum = ShaderVertexStageInfo::MaxResources + 1u; + vertexCases.push_back(overfull); + vertexCases.push_back(vertexInputs({{0x10000u, 16u, 100u, 0u}, {0x10004u, 16u, 99u, 0u}})); + vertexCases.push_back(vertexInputs({{0x10000u, 16u, 100u, 0u}, {0x10004u, 16u, 99u, 0u}}, true)); + vertexCases.push_back(vertexInputs({{0x10000u, 16u, 100u, 0u}, {0x10004u, 16u, 100u, 0u}, {0x20000u, 8u, 50u, 0u}, {0x10008u, 16u, 100u, 1u}})); + std::vector attributes; + std::vector separate; + for (std::uint32_t index = 0; index < ShaderVertexStageInfo::MaxResources; ++index) { + attributes.push_back({0x30000u + index * 4u, 128u, 10u, 0u}); + separate.push_back({0x40000u + index * 0x1000u, 4u + index * 4u, 10u, 0u}); + } + vertexCases.push_back(vertexInputs(attributes)); + vertexCases.push_back(vertexInputs(separate)); + + const MeshConfiguration mesh{3u, 64u, 64u, 64u, 64u, 64u, 0u, 0u}; + auto noThreads = mesh; + noThreads.threadsPerGroup = 0u; + auto badProvoking = mesh; + badProvoking.provokingVertex = 3u; + std::vector> meshCases{std::nullopt, mesh, noThreads, badProvoking}; + struct MeshExpectation { + std::uint32_t outputPrimitiveSize; + std::uint32_t provokingVertex; + bool accepted; + }; + constexpr std::array meshExpectations{MeshExpectation{0u, 0u, false}, MeshExpectation{1u, 0u, true}, MeshExpectation{1u, 1u, false}, MeshExpectation{2u, 0u, true}, MeshExpectation{2u, 1u, true}, MeshExpectation{2u, 2u, false}, MeshExpectation{3u, 2u, true}, MeshExpectation{3u, 3u, false}, MeshExpectation{4u, 0u, false}, MeshExpectation{4u, 3u, false}}; + const auto meshWith = [&](const MeshExpectation& expectation) { + auto varied = mesh; + varied.outputPrimitiveSize = expectation.outputPrimitiveSize; + varied.provokingVertex = expectation.provokingVertex; + return varied; + }; + for (const auto& expectation : meshExpectations) meshCases.push_back(meshWith(expectation)); + + const TessellationConfiguration tessellation{3u, 3u, 1u, 2u, 2u, 16u}; + std::vector> tessellationCases{std::nullopt, tessellation}; + const auto vary = [&](auto change) { + auto varied = tessellation; + change(varied); + tessellationCases.push_back(varied); + }; + vary([](TessellationConfiguration& config) { config.inputControlPoints = 0u; }); + vary([](TessellationConfiguration& config) { config.outputControlPoints = 33u; }); + vary([](TessellationConfiguration& config) { config.domain = 3u; }); + vary([](TessellationConfiguration& config) { config.partitioning = 1u; }); + vary([](TessellationConfiguration& config) { config.outputTopology = 1u; }); + vary([](TessellationConfiguration& config) { config.patchesPerGroup = 0u; }); + vary([](TessellationConfiguration& config) { config.patchesPerGroup = 257u; }); + + constexpr std::array stages{ShaderStageKind::Vertex, ShaderStageKind::Local, ShaderStageKind::TessellationControl, ShaderStageKind::TessellationEvaluation, ShaderStageKind::Mesh}; + std::size_t mismatches = 0; + std::size_t accepted = 0; + std::set errors; + for (const auto stage : stages) { + for (std::size_t vertexCase = 0; vertexCase < vertexCases.size(); ++vertexCase) { + for (std::size_t meshCase = 0; meshCase < meshCases.size(); ++meshCase) { + for (std::size_t tessellationCase = 0; tessellationCase < tessellationCases.size(); ++tessellationCase) { + GuestContext context{}; + context.waveSize = 64u; + context.vertex = vertexCases[vertexCase]; + const auto* meshConfig = meshCases[meshCase] ? &*meshCases[meshCase] : nullptr; + const auto* tessellationConfig = tessellationCases[tessellationCase] ? &*tessellationCases[tessellationCase] : nullptr; + const auto built = errorOf([&] { static_cast(BuildShaderStageInputInfo(stage, context, 64u, meshConfig, tessellationConfig)); }); + const auto validated = errorOf([&] { ValidateVertexStageInputs(stage, context, meshConfig, tessellationConfig); }); + if (built != validated) { + ++mismatches; + std::fprintf(stderr, "stage %d, vertex case %zu, mesh case %zu, tessellation case %zu: the build says \"%s\", the validation \"%s\"\n", static_cast(stage), vertexCase, meshCase, tessellationCase, built.c_str(), validated.c_str()); + } + if (built.empty()) ++accepted; + else errors.insert(built); + } + } + } + } + const std::array expected{"GuestContext.vertex is not set", "invalid vertex resource count", "disagree on record count", "tessellation configuration is missing", "invalid tessellation control-point counts", "unsupported tessellation configuration", "invalid tessellation patches per group", "has no mesh configuration", "invalid mesh configuration"}; + for (const auto* message : expected) { + const bool seen = std::any_of(errors.begin(), errors.end(), [&](const std::string& error) { return error.find(message) != std::string::npos; }); + if (!seen) { + ++mismatches; + std::fprintf(stderr, "no case raised \"%s\"\n", message); + } + } + GuestContext meshContext{}; + meshContext.waveSize = 64u; + meshContext.vertex = vertexInputs({{0x10000u, 16u, 100u, 0u}}); + for (const auto& expectation : meshExpectations) { + const auto varied = meshWith(expectation); + const bool built = errorOf([&] { static_cast(BuildShaderStageInputInfo(ShaderStageKind::Mesh, meshContext, 64u, &varied, nullptr)); }).empty(); + const bool validated = errorOf([&] { ValidateVertexStageInputs(ShaderStageKind::Mesh, meshContext, &varied, nullptr); }).empty(); + if (built != expectation.accepted || validated != expectation.accepted) { + ++mismatches; + std::fprintf(stderr, "mesh output primitive size %u, provoking vertex %u: expected %s, the build %s, the validation %s\n", expectation.outputPrimitiveSize, expectation.provokingVertex, expectation.accepted ? "accepted" : "rejected", built ? "accepts" : "rejects", validated ? "accepts" : "rejects"); + } + } + if (accepted == 0) { + ++mismatches; + std::fprintf(stderr, "no case was accepted\n"); + } + if (mismatches != 0) return 1; + std::printf("vertex input validation matches the input info build in %zu cases (%zu accepted)\n", stages.size() * vertexCases.size() * meshCases.size() * tessellationCases.size(), accepted); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp b/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp index 72c88cbc47..ee2a3fb221 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include "prx/libSceAgcDriver/Submit/include/CommandBuffer.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libc/include/Shutdown.hpp" @@ -50,6 +51,46 @@ void Submit(std::uint32_t queue, const std::vector& words) { Require(result == 0, "shutdown wait submission failed"); } +void CheckCommandBufferSubmission() { + struct MockQueueContext { + std::uint32_t unused = 0; + std::uint32_t queue_id = 0; + }; + MockQueueContext graphicsCtx{0, 0}; + MockQueueContext computeCtx{0, 0x20}; + MockQueueContext invalidCtx{0, 1}; + + Packet emptyPacket{nullptr, 0, 0, {}}; + Require(sceAgcDriverSubmitCommandBuffer(nullptr, &emptyPacket) == 22, "null context submit must return EINVAL"); + Require(sceAgcDriverSubmitCommandBuffer(&graphicsCtx, nullptr) == 22, "null packet submit must return EINVAL"); + Require(sceAgcDriverSubmitMultiCommandBuffers(nullptr, nullptr, nullptr, 0) == 0, "count 0 multi submit must return 0"); + + std::uint32_t* cmdBufs[] = {nullptr}; + const std::uint32_t cmdSizes[] = {0}; + Require(sceAgcDriverSubmitMultiCommandBuffers(nullptr, cmdBufs, cmdSizes, 1) == 22, "null context multi submit must return EINVAL"); + Require(sceAgcDriverSubmitMultiCommandBuffers(&graphicsCtx, nullptr, cmdSizes, 1) == 22, "null cmdBufs multi submit must return EINVAL"); + Require(sceAgcDriverSubmitMultiCommandBuffers(&graphicsCtx, cmdBufs, nullptr, 1) == 22, "null cmdSizes multi submit must return EINVAL"); + Require(sceAgcDriverSubmitMultiCommandBuffers(&graphicsCtx, cmdBufs, cmdSizes, 0) == 0, "empty multi submission failed"); + Require(sceAgcDriverSubmitMultiCommandBuffers(&graphicsCtx, cmdBufs, cmdSizes, 1) == 0, "graphics context multi submit failed"); + Require(sceAgcDriverSubmitMultiCommandBuffers(&computeCtx, cmdBufs, cmdSizes, 1) == 0, "compute context multi submit failed"); + + bool submitRejected = false; + try { + sceAgcDriverSubmitCommandBuffer(&invalidCtx, &emptyPacket); + } catch (const std::runtime_error&) { + submitRejected = true; + } + Require(submitRejected, "unsupported queue context was not rejected"); + + bool multiRejected = false; + try { + sceAgcDriverSubmitMultiCommandBuffers(&invalidCtx, cmdBufs, cmdSizes, 1); + } catch (const std::runtime_error&) { + multiRejected = true; + } + Require(multiRejected, "unsupported queue context for multi submit was not rejected"); +} + void CheckShutdownCancellation() { static std::array states{}; constexpr std::array queues{0, 0x20, 0x28}; @@ -92,6 +133,7 @@ int main(int argc, char** argv) { return 1; } try { + CheckCommandBufferSubmission(); CheckShutdownCancellation(); std::puts("unsatisfied label waits cancelled cleanly at shutdown"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/WaveControlDecode.cpp b/core/libs/prx/libSceAgcDriver/tests/WaveControlDecode.cpp index e75cde7a9f..c202166efb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WaveControlDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WaveControlDecode.cpp @@ -62,7 +62,7 @@ void verifyDecode() { require(decoded[11].op == RdnaOpcode::SCodeEnd, "s_code_end"); } -std::string recompile(std::span code) { +std::string recompile(std::span code, std::uint32_t subgroupSize = 64u) { const std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu}; const std::array capabilities{1u, 61u}; const std::array extensions{"SPV_KHR_storage_buffer_storage_class"}; @@ -74,7 +74,7 @@ std::string recompile(std::span code) { request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; request.target.vulkanVersion = 0x00403000u; request.target.spirvVersion = 0x00010600u; - request.target.subgroupSize = 64u; + request.target.subgroupSize = subgroupSize; request.target.bdaAbiVersion = 1; request.target.supportedCapabilities = capabilities; request.target.supportedExtensions = extensions; @@ -102,6 +102,11 @@ void verifyTranslation() { require(recompile(withStore({0xbf8d0000u})).empty(), "s_sethalt 0 is refused: " + recompile(withStore({0xbf8d0000u}))); const std::vector trailingLoop{0x7e020281u, 0xbf820003u, 0xe0700000u, 0x80000100u, 0xbf810000u, 0xbf82fffcu, 0xbf9f0000u, 0xbf9f0000u, 0xffffffffu}; require(recompile(trailingLoop).empty(), "a program padded with s_code_end after its last branch is refused: " + recompile(trailingLoop)); + const auto orderedAdd = withStore({0xd8fe0000u, 0x05000001u}); + require(recompile(orderedAdd).empty(), "ds_ordered_count adding to a GDS counter is refused: " + recompile(orderedAdd)); + require(recompile(orderedAdd, 32u).empty(), "ds_ordered_count of a wave64 on 32-lane subgroups is refused: " + recompile(orderedAdd, 32u)); + const auto orderedSwap = withStore({0xd8fe1000u, 0x05000001u}); + require(recompile(orderedSwap).empty(), "ds_ordered_count swapping a GDS counter is refused: " + recompile(orderedSwap)); const std::vector, std::string_view>> refused{ {"s_sethalt 1", withStore({0xbf8d0001u}), "s_sethalt 1 at pc 0 halts the wave"}, @@ -114,7 +119,7 @@ void verifyTranslation() { {"ds_gws_sema_p", withStore({0xd8720000u, 0x00000000u}), "ds_gws_sema_p at pc 0: global wave sync"}, {"ds_gws_sema_release_all", withStore({0xd8620000u, 0x00000000u}), "ds_gws_sema_release_all at pc 0: global wave sync"}, {"ds_gws_barrier", withStore({0xd8760000u, 0x00000003u}), "ds_gws_barrier at pc 0: global wave sync"}, - {"ds_ordered_count", withStore({0xd8fe0000u, 0x05000001u}), "ds_ordered_count at pc 0: GDS counters"}, + {"ds_ordered_count of two dwords", withStore({0xd8fe4000u, 0x05000001u}), "ds_ordered_count at pc 0: counting more than one dword is not modeled"}, }; for (const auto& [name, code, message] : refused) { const auto error = recompile(code); diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index cec733ba76..267c945d64 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -7,16 +7,23 @@ #define NOMINMAX #endif #include +#include #else #include +#include +#endif +#ifdef __GLIBC__ +#include #endif #include #include #include #include +#include #include #include #include +#include #include #include @@ -27,12 +34,79 @@ using namespace AgcDriver::GuestMemory; constexpr std::size_t Block = 65536; #ifdef _WIN32 constexpr wchar_t HostHoleMode[] = L"--host-hole"; +constexpr wchar_t ResidentRemapMode[] = L"--resident-direct-remap"; #endif void Require(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); } +#ifdef _WIN32 +bool CheckMetadataCommit() { + std::uintptr_t arena = 0; + std::size_t bytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arena, &bytes); + if (bytes == 0 || !GuestArena::GuestArenaWriteWatched_nid_postfix()) return false; + PROCESS_MEMORY_COUNTERS_EX before{}; + PROCESS_MEMORY_COUNTERS_EX after{}; + Require(K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&before), sizeof(before)), "cannot measure private commit before tracking"); + Require(WriteWatched(), "the arena is not write watched"); + Require(K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&after), sizeof(after)), "cannot measure private commit after tracking"); + const auto committed = after.PrivateUsage > before.PrivateUsage ? after.PrivateUsage - before.PrivateUsage : 0; + std::cout << "write-tracker initialization: " << committed / 1024 << " KiB private commit for " << (bytes >> 30u) << " GiB guest arena\n"; + Require(committed < (64u << 20u), "write tracking committed dense generation tables for untouched guest ranges"); + const auto generation = TrackerGeneration(); + for (std::uint64_t offset = 0; offset + Block <= bytes; offset += std::uint64_t{1} << 32u) { + Require(NewestStamp(arena + offset, Block) == 0 && UnchangedSince(arena + offset, Block, generation), "an untouched leaf has a write stamp"); + } + PROCESS_MEMORY_COUNTERS_EX queried{}; + Require(K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&queried), sizeof(queried)), "cannot measure private commit after empty-range queries"); + Require(queried.PrivateUsage < after.PrivateUsage + (4u << 20u), "queries allocated untouched tracking leaves"); + return true; +} + +void CheckSparseLeafBoundaries() { + constexpr std::uintptr_t LeafBytes = std::uintptr_t{1} << 32; + std::uintptr_t arena = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arena, &arenaBytes); + const auto boundary = arena + (arenaBytes / (2 * LeafBytes)) * LeafBytes; + constexpr std::size_t bytes = 4 * Block; + void* memory = GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(boundary - Block, bytes, Block); + struct Release { + void* memory; + ~Release() { + GuestArena::GuestArenaReset_nid_postfix(memory, bytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, bytes); + } + } release{memory}; + const auto base = reinterpret_cast(memory); + Require(base == boundary - Block, "the sparse tracking fixture did not straddle its 4 GiB leaf boundary"); + GuestArena::GuestArenaCommit_nid_postfix(memory, bytes, PAGE_READWRITE, bytes); + std::memset(memory, 0x11, bytes); + BumpCollectEpoch(); + const auto collected = CollectWrites(base, bytes); + Require(collected != 0, "the cross-leaf range was not collected"); + MarkWritten(boundary, 4); + Require(UnchangedSince(base, Block, collected) && !UnchangedSince(base, bytes, collected), "a driver store across a leaf boundary did not stamp exactly its block"); + Require(UnchangedSinceCollected(boundary, Block, collected), "the cross-leaf driver store became a CPU store"); + static_cast(memory)[0] = 0x22; + CollectWritesUncached(base, bytes); + Require(!UnchangedSinceCollected(base, Block, collected), "a CPU store before the leaf boundary was missed"); + Require(UnchangedSinceCollected(boundary, 3 * Block, collected), "the CPU store stamped the other leaf"); + Unwatch(base, bytes); + const auto hint = UntrackedHint(base, bytes); + Require(hint != 0, "the cross-leaf untracked range has no hint"); + static_cast(memory)[0] = 0x33; + static_cast(memory)[3 * Block] = 0x44; + BumpCollectEpoch(); + const auto changed = UntrackedHint(base, bytes); + Require(changed > hint, "cross-leaf CPU stores did not change their untracked hint"); + Require(UntrackedHint(base, Block) == changed && UntrackedHint(base + 3 * Block, Block) == changed, "a cross-leaf hint store was missed"); + Require(UntrackedHint(boundary, 2 * Block) == hint, "hint stores changed untouched blocks in the other leaf"); +} +#endif + void* AllocateWatched(std::size_t bytes, bool crossLeaf = false) { #ifdef _WIN32 static_cast(crossLeaf); @@ -100,6 +174,264 @@ void CheckSharedBlock() { if (!Watched(outside, unwatched.size())) Require(StoredOver(outside, unwatched.size(), TrackerGeneration()), "an unwatched range reads as not stored over"); } +void CheckStoredOverUsesPageEvidence() { + constexpr std::size_t Page = 4096; + auto* memory = static_cast(AllocateWatched(2 * Block)); + const auto base = reinterpret_cast(memory); + std::memset(memory, 0x11, 2 * Block); + const auto before = CollectWritesUncached(base, 2 * Block); + Require(before != 0, "the stored-over page fixture was not collected"); + + static_cast(memory)[0] = 0x22; + const auto after = CollectWritesUncached(base, Block); + Require(after > before, "the page-local CPU write was not collected"); + Require(StoredOver(base, Page, before), "a collected CPU write on a page was not reported as stored over"); + Require(!StoredOver(base + Page, Page, before), "a page-local CPU write made an untouched page in the same block count as stored over"); +} + +void CheckStoredAllSince() { + constexpr std::size_t bytes = 4 * Block; + auto* memory = static_cast(AllocateWatched(bytes)); + struct Cleanup { + void* memory; + ~Cleanup() { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory, bytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, bytes); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, bytes); + munmap(memory, bytes); +#endif + } + } cleanup{memory}; + const auto base = reinterpret_cast(memory); + std::memset(memory, 0x11, bytes); + auto before = CollectWritesUncached(base, bytes); + Require(before != 0, "the complete-store block is not collected"); + Require(!StoredAllSince_nid_no_patch(base, 64, before), "a range with no driver store has complete coverage"); + + MarkWritten(base + Block, Block); + Require(StoredAllSince_nid_no_patch(base + Block, Block, before), "the first whole-block driver store has no complete coverage"); + Require(StoredAllSince_nid_no_patch(base + Block + 1, Block - 2, before), "a whole-block store lost its interior coverage"); + const auto whole = TrackerGeneration(); + Require(!StoredAllSince_nid_no_patch(base + Block, Block, whole), "a whole-block store at the baseline is taken as newer"); + + before = TrackerGeneration(); + static_cast(memory)[2 * Block + 7] = 0x22; + CollectWritesUncached(base + 2 * Block, Block); + Require(!StoredAllSince_nid_no_patch(base + 2 * Block, Block, before), "CPU-only stores prove complete driver coverage"); + + before = TrackerGeneration(); + const std::array dword{}; + Write(base + 16, dword); + Require(StoredAllSince_nid_no_patch(base + 16, dword.size(), before), "a tracked dword store is absent from its exact range"); + Require(!StoredAllSince_nid_no_patch(base, 64, before), "one dword store proves complete surface coverage"); + Require(!StoredAllSince_nid_no_patch(base + 15, 5, before) && !StoredAllSince_nid_no_patch(base + 16, 5, before), "a dword store covers an unstored boundary byte"); + const auto dwordStored = TrackerGeneration(); + Require(!StoredAllSince_nid_no_patch(base + 16, dword.size(), dwordStored), "a dword store at the baseline is taken as newer"); + MarkWritten(base, 64); + Require(StoredAllSince_nid_no_patch(base, 64, before), "a complete non-block-aligned range store is absent"); + + before = TrackerGeneration(); + MarkPossiblyWritten(base + 2048, 16); + Require(StoredOver(base + 2048, 16, before), "a possible driver write did not invalidate its exact range"); + Require(!StoredOver(base + 2064, 16, before), "a possible driver write invalidated an adjacent range"); + Require(!UnchangedSince(base + 2048, 16, before), "a possible driver write left its exact range unchanged"); + Require(UnchangedSince(base + 2064, 16, before), "a possible driver write changed an adjacent range"); + Require(!StoredAllSince_nid_no_patch(base + 2048, 16, before), "a possible driver write proved complete byte coverage"); + for (unsigned repeat = 0; repeat < 6; ++repeat) MarkPossiblyWritten(base + 2048, 16); + Require(StoredOver(base + 2048, 16, before), "repeated possible writes lost their overlap evidence"); + Require(!StoredOver(base + 2064, 16, before), "repeated possible writes invalidated an adjacent range"); + Require(!StoredAllSince_nid_no_patch(base + 2048, 16, before), "repeated possible writes proved complete byte coverage"); + + before = TrackerGeneration(); + constexpr unsigned PossibleIntervals = 8; + auto beforeNewest = before; + for (unsigned interval = 0; interval < PossibleIntervals; ++interval) { + beforeNewest = TrackerGeneration(); + MarkPossiblyWritten(base + 4096 + static_cast(interval) * 32, 16); + } + Require(StoredOver(base + 4096, 16, before), "an evicted possible-write interval was forgotten instead of marking its block"); + Require(StoredOver(base + 4096 + 16, 16, before), "an evicted possible write did not mark the rest of its block"); + Require(StoredOver(base + 4096 + (PossibleIntervals - 1) * 32, 16, beforeNewest), "the newest possible-write interval was evicted"); + const auto beforeRecord = TrackerGeneration(); + MarkPossiblyWritten(base + 4096, 16); + Require(StoredOver(base + 4096, 16, beforeRecord), "a possible-write interval was not retained after being re-recorded"); + Require(!StoredAllSince_nid_no_patch(base + 4096, 16, before), "possible-write eviction or re-recording proved complete byte coverage"); + + before = TrackerGeneration(); + MarkWritten(base + 2560, 16); + MarkPossiblyWritten(base, Block); + Require(StoredAllSince_nid_no_patch(base + 2560, 16, before), "a possible whole-block range erased an exact partial-store proof"); + Require(!StoredAllSince_nid_no_patch(base, Block, before), "a possible whole-block range proved complete block coverage"); + + before = TrackerGeneration(); + MarkWritten(base + 512, 16); + MarkWritten(base + 544, 16); + Require(!StoredAllSince_nid_no_patch(base + 512, 48, before), "split driver stores prove coverage across a hole"); + MarkWritten(base + 528, 16); + Require(StoredAllSince_nid_no_patch(base + 512, 48, before), "out-of-order split driver stores did not form complete coverage"); + Require(!StoredAllSince_nid_no_patch(base + 512, 49, before), "split coverage includes an unstored final byte"); + + before = TrackerGeneration(); + MarkWritten(base + 1024 + 8, 24); + MarkWritten(base + 1024, 16); + Require(StoredAllSince_nid_no_patch(base + 1024, 32, before), "overlapping driver intervals did not form complete coverage"); + + before = TrackerGeneration(); + MarkWritten(base + Block - 4, 8); + Require(StoredAllSince_nid_no_patch(base + Block - 4, 8, before), "a tracked store across a block boundary lost coverage"); + Require(!StoredAllSince_nid_no_patch(base + Block - 5, 9, before) && !StoredAllSince_nid_no_patch(base + Block - 4, 9, before), "a boundary store covers an unstored adjacent byte"); + + before = TrackerGeneration(); + for (unsigned piece = 0; piece < 6; ++piece) MarkWritten(base + static_cast(piece) * 64, 4); + Require(!StoredAllSince_nid_no_patch(base, 4, before), "dropped driver evidence proves complete coverage"); + MarkWritten(base, Block); + Require(StoredAllSince_nid_no_patch(base, Block, before), "a new whole-block store did not replace dropped evidence"); + + before = TrackerGeneration(); + MarkWritten(base, 3 * Block); + Require(StoredAllSince_nid_no_patch(base, 3 * Block, before), "complete multi-block stores lost coverage"); + Require(StoredAllSince_nid_no_patch(base + 1, 3 * Block - 2, before) && StoredAllSince_nid_no_patch(base + 3 * Block - 1, 1, before), "complete multi-block stores lost edge bytes"); + const auto complete = TrackerGeneration(); + Require(!StoredAllSince_nid_no_patch(base, 3 * Block, complete), "stores older than the baseline prove complete coverage"); + + before = TrackerGeneration(); + MarkWritten(base + 2 * Block, Block); + const auto driverStored = TrackerGeneration(); + static_cast(memory)[2 * Block + 7] = 0x33; + CollectWritesUncached(base + 2 * Block, Block); + Require(StoredAllSince_nid_no_patch(base + 2 * Block, Block, before), "a later CPU store erased the proof that the driver stored every byte"); + Require(!UnchangedSinceCollected(base + 2 * Block, Block, driverStored), "the separate CPU-write check missed a store after complete driver coverage"); + + Require(!StoredAllSince_nid_no_patch(base, 0, before) && !StoredAllSince_nid_no_patch(base, 1, 0), "empty or unknown-baseline complete coverage is accepted"); + Require(!StoredAllSince_nid_no_patch(std::numeric_limits::max(), 2, before), "overflowing complete coverage is accepted"); + Unwatch(base + 3 * Block, Block); + Require(MarkWritten(base + 3 * Block, Block) == 0, "an unwatched whole store acquired trusted driver coverage"); + Require(!StoredAllSince_nid_no_patch(base + 3 * Block, Block, before), "unwatched stores prove complete coverage"); + Require(!StoredAllSince_nid_no_patch(base + 3 * Block - 1, 2, before), "a range entering unwatched memory has complete coverage"); + std::array outside{}; + const auto outsideAddress = reinterpret_cast(outside.data()); + if (!Watched(outsideAddress, outside.size())) { + MarkWritten(outsideAddress, outside.size()); + Require(!StoredAllSince_nid_no_patch(outsideAddress, outside.size(), before), "an outside untracked range has complete coverage"); + } +} + +void CheckSharedBlockProofs() { + constexpr std::size_t page = 4096; + void* memory = AllocateWatched(2 * Block); + auto* guest = static_cast(memory); + const auto base = reinterpret_cast(memory); + const auto range = base + Block + 8 * page; + constexpr std::size_t rangeBytes = 4 * page; + std::memset(memory, 0x11, 2 * Block); + const auto synced = CollectWrites(base, 2 * Block); + Require(synced != 0, "the proof blocks are not collected"); + + Require(MarkWritten(base + Block, 64) != 0, "a driver store into watched memory is not stamped"); + guest[Block + 2 * page + 5] = 0x22; + CollectWritesUncached(base, 2 * Block); + Require(UnchangedSince(range, rangeBytes, synced), "stores beside the range in its 64 KiB block count for it"); + const std::array beside{{{range, rangeBytes, synced}, {base + Block + 4 * page, page, synced}}}; + Require(UnchangedSinceAll(beside), "a batch over ranges beside the stores reads as changed"); + + const auto beforeCpu = TrackerGeneration(); + guest[Block + 9 * page + 5] = 0x33; + CollectWritesUncached(base, 2 * Block); + Require(!UnchangedSince(range, rangeBytes, beforeCpu), "a CPU store inside the range is not seen"); + const std::array mixed{{{base + Block + 4 * page, page, beforeCpu}, {range, rangeBytes, beforeCpu}}}; + Require(!UnchangedSinceAll(mixed), "a batch with a range written by the CPU reads as unchanged"); + + const auto beforeDriver = TrackerGeneration(); + MarkWritten(range + 100, 4); + Require(!UnchangedSince(range, rangeBytes, beforeDriver), "a driver store inside the range is not seen"); + + const auto beforeWhole = TrackerGeneration(); + MarkWritten(base + Block, Block); + Require(!UnchangedSince(range, rangeBytes, beforeWhole), "a store over the whole block is not seen"); + + const auto beforeOwn = TrackerGeneration(); + const auto stored = base + Block + 12 * page + 0x100; + StoreOwnBytes(stored, 64, [&] { + std::memset(reinterpret_cast(stored), 0x55, 64); + guest[Block + 12 * page + 0x800] = 0x66; + }); + Require(!UnchangedSince(base + Block + 12 * page + 0x800, 16, beforeOwn), "a guest store beside a driver store in its page during the store is not seen"); + Require(UnchangedSince(base + Block + 4 * page, page, beforeOwn), "a driver store counts for another page of its block"); + + const auto beforeNoRecord = TrackerGeneration(); + guest[2 * page + 5] = 0x44; + CollectWritesUncached(base, 2 * Block); + Require(UnchangedSince(base + 8 * page, page, beforeNoRecord), "a CPU store beside the range in a block without driver stores counts for it"); + + const auto beforeFirstWhole = TrackerGeneration(); + MarkWritten(base, Block); + Require(!UnchangedSince(base + 8 * page, page, beforeFirstWhole), "a whole-block store in a block without earlier driver stores is not seen"); + MarkWritten(base, 64); + Require(!UnchangedSince(base + 8 * page, page, beforeFirstWhole), "a partial store after a whole-block store hides it"); + + const auto beforeSpan = TrackerGeneration(); + const std::vector span(6 * page, std::byte{0x77}); + Write(base + 2 * page, span); + Require(!UnchangedSince(base + 4 * page, page, beforeSpan), "a store over several pages of a block is not seen"); + Require(UnchangedSince(base + 12 * page, page, beforeSpan), "a store over several pages of a block counts for the rest of it"); + + const auto beforeExact = TrackerGeneration(); + const std::vector nearlyWhole(Block - 512, std::byte{0x5a}); + Write(base + Block + 256, nearlyWhole); + Require(StoredAllSince_nid_no_patch(base + Block + 256, nearlyWhole.size(), beforeExact), "an own store lost its exact coverage"); + Require(!StoredAllSince_nid_no_patch(base + Block, Block, beforeExact), "the dirty pages of an own store prove its whole block stored"); + Require(!StoredAllSince_nid_no_patch(base + Block, 256, beforeExact), "the dirty first page of an own store proves the bytes before the store stored"); + + const auto beforeClear = TrackerGeneration(); + MarkWritten(base + Block + 0x100, 64); + const std::array label{}; + for (const std::uint64_t at : {4u, 6u, 10u}) Write(base + Block + at * page + 0x200, label); + Require(StoredAllSince_nid_no_patch(base + Block + 0x100, 64, beforeClear), "own stores elsewhere in the block forget a driver store's complete coverage"); + + const auto beforeNeighbour = TrackerGeneration(); + Write(base + Block + 14 * page + 0x100, label); + Require(!StoredOver(base + Block + 14 * page + 0x800, 16, beforeNeighbour), "a neighbour in the page of an own store reads as stored over"); + Require(!UnchangedSince(base + Block + 14 * page + 0x800, 16, beforeNeighbour), "a range in the page an own store dirtied reads as unchanged"); +} + +void CheckRepeatedPossibleStores() { + void* memory = AllocateWatched(3 * Block); + const auto base = reinterpret_cast(memory); + const auto boundary = base + 2 * Block - 0x9000; + std::memset(memory, 0x11, 3 * Block); + + const auto synced = CollectWrites(base, 3 * Block); + Require(synced != 0, "the test block is not collected"); + + const auto exact = TrackerGeneration(); + MarkWritten(base + Block, 0x100); + for (std::uint64_t store = 0; store < 5; ++store) MarkPossiblyWritten(boundary + store * 0x1000, 0x100); + Require(StoredAllSince_nid_no_patch(base + Block, 0x100, exact), "possible writes beyond the ring forgot an exact store's coverage"); + + void* fresh = AllocateWatched(2 * Block); + const auto freshBase = reinterpret_cast(fresh); + std::memset(fresh, 0x11, 2 * Block); + const auto freshSynced = CollectWrites(freshBase, 2 * Block); + const auto neighbour = freshBase + Block - 0x9000; + MarkPossiblyWritten(neighbour, 0x100); + const auto secondStored = MarkPossiblyWritten(neighbour + 0x1000, 0x100); + const auto thirdStored = MarkPossiblyWritten(neighbour + 0x2000, 0x100); + MarkPossiblyWritten(neighbour + 0x3000, 0x100); + const auto settled = TrackerGeneration(); + MarkPossiblyWritten(neighbour, 0x100); + MarkPossiblyWritten(neighbour, 0x100); + MarkPossiblyWritten(neighbour + 0x5000, 0x100); + Require(!StoredOver(freshBase, static_cast(neighbour - freshBase), secondStored), "a full possible ring evicted a newer piece instead of its oldest"); + Require(StoredOver(freshBase, static_cast(neighbour - freshBase), freshSynced), "an evicted possible write is not taken as a store over the rest of the block"); + + const auto partial = TrackerGeneration(); + MarkPossiblyWritten(neighbour + 0x5080, 0x100); + Require(StoredOver(neighbour + 0x5000, 0x10, settled) && StoredOver(neighbour + 0x5100, 0x10, partial), "a possible write that overlaps an earlier piece without containing it replaced it"); + Require(!StoredOver(freshBase, static_cast(neighbour - freshBase), thirdStored), "a possible write that overlaps without containing evicted a newer piece instead of the oldest"); +} + void CheckOwnStore() { void* memory = AllocateWatched(2 * Block); const auto base = reinterpret_cast(memory); @@ -183,6 +515,92 @@ 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 + +void CheckCoarseRuns() { + constexpr std::size_t bytes = 192 * Block; + void* memory = AllocateWatched(bytes); + const auto base = reinterpret_cast(memory); + std::memset(memory, 0x11, bytes); + BumpCollectEpoch(); + const auto collected = CollectWrites(base, bytes); + Require(collected != 0 && UnchangedSince(base, bytes, collected), "a range nothing wrote after its collect reports a change"); + const auto stored = MarkWritten(base + bytes / 2, 4); + Require(stored > collected && !UnchangedSince(base, bytes, collected), "a driver store inside whole 4 MiB runs of the range is not seen"); + Require(UnchangedSince(base, bytes / 2, collected), "a driver store after a range made it changed"); + Require(UnchangedSince(base, bytes, stored), "a range checked at its newest stamp reports a change"); + static_cast(memory)[bytes - 1] = 0x22; + CollectWritesUncached(base + bytes - Block, Block); + Require(!UnchangedSince(base, bytes, stored), "a CPU store into the last block of the range is not seen"); +} + #ifdef _WIN32 void CheckDeferredSharedImports() { constexpr std::size_t bytes = 4 * Block; @@ -275,6 +693,125 @@ 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 CheckResidentDirectMappingReuse() { + constexpr std::size_t bytes = 5 * Block; + auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block)); + Require(memory != nullptr, "cannot reserve the resident remap fixture"); + struct Cleanup { + std::uint8_t* memory; + std::array sections{}; + ~Cleanup() { + GuestArena::GuestArenaReset_nid_postfix(memory, bytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, bytes); + for (const auto section : sections) { + if (section == nullptr) continue; + GuestArena::GuestArenaForgetSection_nid_postfix(section); + CloseHandle(section); + } + } + } cleanup{memory}; + GuestArena::GuestArenaCommit_nid_postfix(memory, 3 * Block, PAGE_READWRITE, 3 * Block); + GuestArena::GuestArenaCommit_nid_postfix(memory + 3 * Block, 2 * Block, PAGE_READWRITE, 2 * Block); + Require(Watched(reinterpret_cast(memory), bytes), "the resident remap fixture is not watched"); + for (auto& section : cleanup.sections) { + section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, 3 * Block, nullptr); + Require(section != nullptr, "cannot create the resident remap backing"); + } + const auto base = reinterpret_cast(memory); + const auto nativeType = [&](std::size_t offset) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(memory + offset, &info, sizeof(info)) == sizeof(info), "cannot query the resident remap backing"); + return info.Type; + }; + for (std::size_t use = 0; use < 2; ++use) { + BumpCollectEpoch(); + const auto before = CollectWrites(base, 3 * Block); + Require(before != 0, "the backing before a resident remap was not collected"); + Unwatch(base + Block, Block); + Require(!Watched(base + Block, Block), "the resident remap exclusion was not established"); + const auto outside = CollectWrites(base + 3 * Block, 2 * Block); + const auto mapping = GuestArena::GuestArenaCommitGeneration_nid_postfix(); + GuestArena::GuestArenaMapDirect_nid_postfix(memory, 3 * Block, cleanup.sections[use], 0, PAGE_READWRITE); + Require(nativeType(0) == MEM_PRIVATE && nativeType(2 * Block) == MEM_PRIVATE, "the resident remap did not create private memory"); + Require(Watched(base, 3 * Block), "a resident direct remap retained an old import exclusion"); + Require(GuestArena::GuestArenaCommitGeneration_nid_postfix() > mapping, "a resident direct remap reused the mapping generation"); + Require(!UnchangedSince(base, Block, before) && !UnchangedSince(base + 2 * Block, Block, before), "a resident direct remap reused old neighbour stamps"); + Require(UnchangedSince(base + 3 * Block, 2 * Block, outside), "a resident direct remap invalidated memory outside its range"); + const auto after = CollectWritesUncached(base, 3 * Block); + Require(after > before && UnchangedSince(base, 3 * Block, after), "the resident remap could not establish a current generation"); + memory[Block + 9] = static_cast(0x51 + use); + CollectWritesUncached(base, 3 * Block); + Require(!UnchangedSince(base + Block, Block, after), "a CPU store after a resident direct remap was missed"); + std::cout << "resident direct remap " << use << ": private, watched, new mapping generation, CPU store collected\n"; + } + Unwatch(base, 3 * Block); + GuestArena::GuestArenaMap_nid_postfix(memory, 3 * Block, cleanup.sections[0], 0, PAGE_READWRITE); + Require(nativeType(0) == MEM_MAPPED, "the shared remap did not create mapped memory"); + for (std::size_t block = 0; block < 3; ++block) Require(!Watched(base + block * Block, Block), "an ordinary shared remap restored a private import exclusion"); + GuestArena::GuestArenaMapDirect_nid_postfix(memory + 3 * Block, 2 * Block, cleanup.sections[2], 0, PAGE_READWRITE); + GuestArena::GuestArenaMapDirect_nid_postfix(memory, 3 * Block, cleanup.sections[2], 0, PAGE_READWRITE); + Require(nativeType(0) == MEM_MAPPED && nativeType(Block) == MEM_MAPPED && nativeType(2 * Block) == MEM_PRIVATE, "the mixed direct remap did not establish shared and private spans"); + Require(!Watched(base, Block) && !Watched(base + Block, Block) && Watched(base + 2 * Block, Block), "the mixed direct remap restored an exclusion outside its new private span"); + std::cout << "resident direct remap controls: shared exclusions retained, only the new private span restored\n"; + Unwatch(base, 3 * Block); + bool refused = false; + try { + GuestArena::GuestArenaMapDirect_nid_postfix(memory, 3 * Block, cleanup.sections[1], 0, 0); + } catch (const std::system_error& error) { + Require(error.code().value() == ERROR_INVALID_PARAMETER, "the failed resident remap reported an unexpected error"); + refused = true; + } + std::uintptr_t createdBegin = 0; + std::uintptr_t createdEnd = 0; + Require(refused && GuestArena::GuestArenaResidentSpan_nid_postfix(base, 1, &createdBegin, &createdEnd), "the failing protection change did not leave a new private span"); + Require(createdBegin == base && createdEnd <= base + 3 * Block && Watched(base, createdEnd - base), "a failed resident remap did not report its completed private span"); + for (auto block = createdEnd; block < base + 3 * Block; block += Block) Require(!Watched(block, Block), "a failed resident remap restored an uncreated span"); + std::cout << "resident direct remap failure: completed private span watched, original mapping error retained\n"; +} + void CheckUntrackedHint() { constexpr std::size_t bytes = 4 * Block; auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block)); @@ -426,6 +963,39 @@ void CheckHostHole() { }) && imported, "an import into an arena host hole did not run"); Require(!Watched(hole, bytes), "an import made an arena host hole watched"); } + +std::uint64_t failedCollectBegin = 0; +std::uint64_t failedCollectEnd = 0; +std::uint64_t unwatchHookBegin = 0; +std::uint64_t unwatchHookEnd = 0; + +void RecordUnwatch(std::uint64_t address, std::size_t bytes) { + unwatchHookBegin = address; + unwatchHookEnd = address + bytes; +} + +bool FailFreshCollect(std::uintptr_t address, std::size_t bytes, void (*)(void*, std::uintptr_t, std::uintptr_t), void*) { + failedCollectBegin = address; + failedCollectEnd = address + bytes; + return false; +} + +void CheckFailedCollectUnwatch() { + void* memory = AllocateWatched(3 * Block); + const auto base = reinterpret_cast(memory); + std::memset(memory, 0x11, 3 * Block); + Require(Watched(base + Block, Block), "the probe block was not watched"); + SetFreshCollectHook(&FailFreshCollect); + SetUnwatchHook(&RecordUnwatch); + const auto stored = MarkWritten(base + Block + 0x8000, 4); + SetFreshCollectHook(nullptr); + SetUnwatchHook(nullptr); + Require(stored == 0, "a failed fresh collect produced trusted stamps"); + Require(!Watched(base + Block, Block), "a failed collect left its block watched"); + Require(Watched(base, Block) && Watched(base + 2 * Block, Block), "a failed collect unwatched unrelated blocks"); + Require(failedCollectBegin == base + Block && failedCollectEnd == base + 2 * Block, "the failed fresh collect did not cover its block"); + Require(unwatchHookBegin <= base + Block && unwatchHookEnd >= base + 2 * Block, "a failed collect did not run the unwatch hook over its block"); +} #endif void CheckVersionRanges() { @@ -448,28 +1018,43 @@ void CheckVersionRanges() { std::memset(memory, 17, bytes); const auto initial = CollectWritesUncached(base, bytes); Require(initial != 0, "version ranges could not be collected"); - struct Write { std::size_t block; std::uint64_t generation; }; + struct Write { std::size_t block; std::uint64_t generation; std::size_t begin; std::size_t end; }; std::vector history; std::vector checkpoints{initial}; std::uint32_t random = 0x61387a29; const auto next = [&] { random = random * 1664525u + 1013904223u; return random; }; - const auto expected = [&](const UnchangedQuery& query) { - if (query.generation == 0) return false; + const auto blocksUnchanged = [&](const UnchangedQuery& query) { for (const auto& write : history) { const auto begin = base + write.block * Block; if (write.generation > query.generation && begin < query.address + query.bytes && query.address < begin + Block) return false; } return true; }; + const auto bytesUnchanged = [&](const UnchangedQuery& query) { + for (const auto& write : history) { + const auto begin = base + write.block * Block + write.begin; + const auto end = base + write.block * Block + write.end; + if (write.generation > query.generation && begin < query.address + query.bytes && query.address < end) return false; + } + return true; + }; for (unsigned step = 0; step < 512; ++step) { const auto block = step < 3 ? step : next() % count; std::uint64_t generation; + Write write{block, 0, 13, 17}; if (step % 3 == 0) { memory[block * Block + 9] ^= 1; generation = CollectWritesUncached(base + block * Block, Block); + write.begin = 0; + write.end = 4096; + } else if (step % 7 == 1) { + generation = MarkWritten(base + block * Block, Block); + write.begin = 0; + write.end = Block; } else generation = MarkWritten(base + block * Block + 13, 4); Require(generation > checkpoints.back(), "version range write did not advance the tracker"); - history.push_back({block, generation}); + write.generation = generation; + history.push_back(write); checkpoints.push_back(generation); for (unsigned check = 0; check < 16; ++check) { std::array queries; @@ -477,11 +1062,12 @@ void CheckVersionRanges() { for (auto& query : queries) { const auto offset = next() % bytes; query = {base + offset, 1 + next() % (bytes - offset), checkpoints[next() % checkpoints.size()]}; - const auto same = expected(query); - Require(UnchangedSince(query.address, query.bytes, query.generation) == same, "version range disagrees with write history"); + const auto same = UnchangedSince(query.address, query.bytes, query.generation); + Require(!same || bytesUnchanged(query), "version range reads as unchanged over a newer write in it"); + Require(same || !blocksUnchanged(query), "version range reads as changed while its blocks are unchanged"); all = all && same; } - Require(UnchangedSinceAll(queries) == all, "batched version ranges disagree with write history"); + Require(UnchangedSinceAll(queries) == all, "batched version ranges disagree with the single ranges"); } } Require(!UnchangedSince(base, bytes, 0), "an unknown generation was accepted"); @@ -570,6 +1156,122 @@ void CheckImportWindow() { Require(!UnchangedSince(base, Block, after), "a store after the import window was missed"); } +void CheckImportWindowOtherThread() { + void* memory = AllocateWatched(2 * Block); + const auto base = reinterpret_cast(memory); + auto* bytes = static_cast(memory); + std::memset(memory, 0x11, 2 * Block); + const auto synced = CollectWritesUncached(base, 2 * Block); + Require(synced != 0, "the other-thread import window block is not collected"); + Require(ImportWatched(base, 2 * Block, [&] { + std::thread store([&] { + bytes[0x100] = 0x11; + bytes[Block + 0x100] = 0x77; + }); + store.join(); + return true; + }), "the other-thread import window import failed"); + const std::array generations{synced, synced}; + std::array changed{}; + std::array cpu{}; + Require(ChangedBlocks(base, 2 * Block, generations, changed, cpu), "the other-thread import window block is not tracked"); + Require(changed[0] == BlockUnchanged && cpu[0] == 1, "a same-value store from another thread within an import window lost its possible CPU touch"); + Require(changed[1] == BlockWritten && WrittenSince(base + Block, Block, synced) && cpu[1] == 1, "a store of new bytes from another thread within an import window was missed"); +} + +void CheckImportWindowZeroPages() { + void* memory = AllocateWatched(5 * Block); + const auto base = reinterpret_cast(memory); + auto* bytes = static_cast(memory); + auto* plain = static_cast(memory); + std::memset(plain, 0x11, Block); + std::memset(plain + Block, 0, 2 * Block); + std::memset(plain + 3 * Block, 0x44, Block); + std::memset(plain + 4 * Block, 0x66, Block); + const auto synced = CollectWritesUncached(base, 5 * Block); + Require(synced != 0, "the zero page import window block is not collected"); + Require(ImportWatched(base, 5 * Block, [&] { + bytes[0x100] = 0x11; + bytes[Block + 0x100] = 0; + bytes[2 * Block + 0x2000] = 0x22; + bytes[3 * Block + 0x3000] = 0x44; + bytes[4 * Block + 0x100] = 0x55; + return true; + }), "the zero page import window import failed"); + const std::array generations{synced, synced, synced, synced, synced}; + std::array changed{}; + std::array cpu{}; + Require(ChangedBlocks(base, 5 * Block, generations, changed, cpu), "the zero page import window block is not tracked"); + Require(changed[0] == BlockUnchanged && cpu[0] == 1, "a page before the zero pages of an import window reads as written"); + Require(changed[1] == BlockUnchanged && UnchangedSince(base + Block, Block, synced), "a zero page an import window resolved without changing its bytes reads as written"); + Require(changed[2] == BlockWritten && WrittenSince(base + 2 * Block, Block, synced), "a store of new bytes to a zero page within an import window was missed"); + Require(changed[3] == BlockUnchanged && UnchangedSince(base + 3 * Block, Block, synced), "a page after the zero pages of an import window was compared with other bytes"); + Require(changed[4] == BlockWritten && WrittenSince(base + 4 * Block, Block, synced), "a store of new bytes after the zero pages of an import window was missed"); +} + +void CheckImportWindowOverUnwatchedBlock() { + void* memory = AllocateWatched(4 * Block); + const auto base = reinterpret_cast(memory); + auto* bytes = static_cast(memory); + std::memset(memory, 0x11, 4 * Block); + Unwatch(base + Block, Block); + const auto synced = CollectWritesUncached(base + 2 * Block, 2 * Block); + const auto syncedFirst = CollectWritesUncached(base, Block); + Require(synced != 0 && syncedFirst != 0, "the watched blocks around the unwatched one are not collected"); + bytes[0x500] = 0x44; + Require(ImportWatched(base, 4 * Block, [&] { + bytes[0x100] = 0x11; + bytes[2 * Block + 0x100] = 0x11; + bytes[3 * Block + 0x2000] = 0x22; + return true; + }), "the import over an unwatched block failed"); + Require(!Watched(base + Block, Block), "an import window made an unwatched block watched"); + Require(Watched(base, Block) && Watched(base + 2 * Block, 2 * Block), "an import window over an unwatched block took the watched blocks out of the write watch"); + const std::array generations{synced, synced}; + std::array changed{}; + std::array cpu{}; + Require(ChangedBlocks(base + 2 * Block, 2 * Block, generations, changed, cpu), "the watched blocks after an unwatched block are not tracked"); + Require(changed[0] == BlockUnchanged && UnchangedSince(base + 2 * Block, Block, synced), "a page an import window over an unwatched block resolved without changing its bytes reads as written"); + Require(changed[1] == BlockWritten && WrittenSince(base + 3 * Block, Block, synced), "a store of new bytes within an import window over an unwatched block was missed"); + Require(WrittenSince(base, Block, syncedFirst), "a store before an import window over an unwatched block was missed"); + const auto after = CollectWritesUncached(base, Block); + bytes[0x100] = 0x33; + static_cast(CollectWritesUncached(base, Block)); + Require(after != 0 && !UnchangedSince(base, Block, after), "a store after an import window over an unwatched block was missed"); +} + +void CheckImportWindowFootprint() { +#ifdef __GLIBC__ + constexpr std::size_t bytes = std::size_t{64} << 20; + void* memory = AllocateWatched(bytes); + const auto base = reinterpret_cast(memory); + std::memset(memory, 0x5a, bytes); + Require(CollectWritesUncached(base, bytes) != 0, "the footprint import window block is not collected"); + const auto heapBytes = [] { + const auto info = mallinfo2(); + return info.uordblks + info.hblkhd; + }; + const auto before = heapBytes(); + std::size_t during = 0; + Require(ImportWatched(base, bytes, [&] { + during = heapBytes(); + return true; + }), "the footprint import window import failed"); + Require(Watched(base, bytes), "the footprint import window left its range unwatched"); + const auto held = during > before ? during - before : 0; + Require(held <= bytes / 64, "an import window over " + std::to_string(bytes >> 20) + " MiB held " + std::to_string(held >> 20) + " MiB of host memory during the import: it copies the range instead of hashing its pages"); + auto* plain = static_cast(memory); + const auto synced = CollectWritesUncached(base, bytes); + Require(ImportWatched(base, bytes, [&] { + plain[0x100] = 0x5a; + plain[Block + 0x100] = 0x5b; + return true; + }), "the footprint import window re-import failed"); + Require(UnchangedSince(base, Block, synced), "a page a hashed import window resolved without changing its bytes reads as written"); + Require(WrittenSince(base + Block, Block, synced), "a store of new bytes within a hashed import window was missed"); +#endif +} + void ProtectPage(std::uint64_t address, bool readable) { #ifdef _WIN32 DWORD previous = 0; @@ -580,6 +1282,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); @@ -768,8 +1488,37 @@ void CheckCommittedRanges() { Require(!UnchangedCommittedSince(first, 0), "an unknown generation was accepted for the committed parts"); } +void CheckAdoptLatestEpoch() { + std::uint64_t bumped = 0; + std::thread([&] { + BumpCollectEpoch(); + bumped = CollectEpoch(); + }).join(); + Require(bumped != 0, "a bumped thread has no collect epoch"); + std::uint64_t before = 1; + std::uint64_t adopted = 0; + bool keptOwn = false; + std::thread([&] { + before = CollectEpoch(); + AdoptLatestCollectEpoch(); + adopted = CollectEpoch(); + BumpCollectEpoch(); + const auto own = CollectEpoch(); + AdoptLatestCollectEpoch(); + keptOwn = CollectEpoch() == own; + }).join(); + Require(before == 0, "a thread that never bumped had a collect epoch"); + Require(adopted == bumped, "a thread that never bumped did not take the newest bumped collect epoch"); + Require(keptOwn, "adopting the newest collect epoch moved a thread off its own"); +} + int main() { try { +#ifdef _WIN32 + if (std::wstring_view(GetCommandLineW()).ends_with(L"--metadata-commit")) { + return CheckMetadataCommit() ? 0 : 77; + } +#endif if (!WriteWatched()) { std::cout << "no write watching: skipped\n"; return 77; @@ -779,22 +1528,45 @@ int main() { CheckHostHole(); return 0; } + if (std::wstring_view(GetCommandLineW()).ends_with(ResidentRemapMode)) { + CheckResidentDirectMappingReuse(); + return 0; + } #endif CheckSharedBlock(); + CheckStoredOverUsesPageEvidence(); + CheckStoredAllSince(); + CheckSharedBlockProofs(); + CheckRepeatedPossibleStores(); CheckOwnStore(); CheckUnwatch(); CheckNewestStamp(); + CheckPartlyWatchedStore(); +#ifndef _WIN32 + CheckWatchOverlaps(); + CheckWatchedMiddleStore(); +#endif + CheckCoarseRuns(); #ifdef _WIN32 + CheckSparseLeafBoundaries(); CheckDeferredSharedImports(); CheckPrivateMappingReuse(); + CheckRewatch(); CheckUntrackedHint(); CheckHostHoleInChild(); + CheckFailedCollectUnwatch(); #endif CheckVersionRanges(); CheckCollectBoundaries(); CheckImportWindow(); + CheckImportWindowZeroPages(); + CheckImportWindowOtherThread(); + CheckImportWindowOverUnwatchedBlock(); + CheckStoreAcrossImports(); + CheckImportWindowFootprint(); CheckUnwatchedImport(); CheckCommittedRanges(); + CheckAdoptLatestEpoch(); } catch (const std::exception& error) { std::cerr << "write tracking test failed: " << error.what() << "\n"; return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp index 083a984de1..62e56cfe52 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp @@ -18,6 +18,13 @@ int main() { require(coverage.Covers(base, 4 * block + 1), "the initialized arena is not watched"); require(!coverage.Covers(base - 1, 1) && !coverage.Covers(base + 4 * block + 1, 1), "outside memory is watched"); require(!coverage.Covers(base, 0) && !coverage.Covers(base, std::numeric_limits::max()), "an empty or overflowing range is watched"); + std::uint64_t spanBegin = 0; + std::uint64_t spanEnd = 0; + require(coverage.ExcludedSpan(base + block + 4096, 4096, &spanBegin, &spanEnd) && spanBegin == base + block && spanEnd == base + 2 * block, "the span of an imported page is not its whole block"); + require(coverage.ExcludedSpan(base + 3 * block - 1, 2, &spanBegin, &spanEnd) && spanBegin == base + 2 * block && spanEnd == base + 4 * block, "the span of a boundary import is not both blocks"); + require(coverage.ExcludedSpan(base + 4 * block, std::numeric_limits::max(), &spanBegin, &spanEnd) && spanBegin == base + 4 * block && spanEnd == base + 4 * block + 1, "the span of the last partial block is not clipped to the arena"); + require(coverage.ExcludedSpan(base - 1, 2, &spanBegin, &spanEnd) && spanBegin == base && spanEnd == base + block, "the span of an import crossing the arena start is not clipped"); + require(!coverage.ExcludedSpan(base - block, block, &spanBegin, &spanEnd) && !coverage.ExcludedSpan(base + 5 * block, block, &spanBegin, &spanEnd) && !coverage.ExcludedSpan(base, 0, &spanBegin, &spanEnd), "an outside or empty import has a span"); require(coverage.Exclude(base + block + 4096, 1), "an imported page was not excluded"); require(!coverage.Covers(base + block, block), "the imported block is still watched"); require(!coverage.Covers(base + block - 1, 2), "a range crossing into the import is watched"); @@ -34,7 +41,8 @@ int main() { require(coverage.Covers(base + block, block), "clipping an import excluded the rest of the arena"); coverage.Initialize(base, 4 * block + 1); coverage.Exclude(base, 4 * block + 1); - require(!coverage.Restore(base + 1, block - 1) && !coverage.Covers(base, block), "a partial replacement restored an excluded block"); + require(coverage.Restore(base + 1, block - 1) && coverage.Covers(base + 4096, block - 4096), "a partial replacement did not restore its complete pages"); + require(!coverage.Covers(base, 4096) && !coverage.Covers(base, block), "a partial replacement restored an incompletely remapped page"); require(coverage.Restore(base + block, block) && coverage.Covers(base + block, block), "fresh private backing stayed excluded"); require(!coverage.Restore(base + block, block), "restoring an already watched block reports a change"); require(!coverage.Covers(base, block) && !coverage.Covers(base + 2 * block, block), "restoring one block restored its neighbours"); @@ -53,6 +61,44 @@ int main() { coverage.Exclude(base, block, 7); require(!coverage.Restore(base, block, 6) && !coverage.Covers(base, block), "a delayed mapping notification erased a repeated import"); require(coverage.Restore(base, block, 8), "a fresh replacement did not restore the latest import exclusion"); + coverage.Initialize(base, 2 * block); + coverage.Exclude(base, block, 10); + require(coverage.Restore(base + 16384, 16384, 11), "a fresh partial-block private mapping stayed excluded"); + require(coverage.Covers(base + 16384, 16384), "the restored private pages are not watched"); + require(!coverage.Covers(base, 16384) && !coverage.Covers(base + 32768, 32768), "restoring private pages watched an imported neighbour"); + require(!coverage.Covers(base + 16383, 2) && !coverage.Covers(base + 32767, 2), "a boundary query included excluded pages"); + require(!coverage.Restore(base + 32768, 4096, 10), "a stale partial mapping restored an exclusion"); + coverage.Exclude(base + 4096, 1, 12); + require(!coverage.Covers(base + 16384, 16384), "a repeated import kept partial coverage from an older mapping"); + require(!coverage.Restore(base + 16384, 16384, 11), "a delayed partial mapping restored a newer import"); + require(coverage.Restore(base + 16384, 16384, 13), "fresh partial coverage was not restored after re-import"); + constexpr std::size_t page = 4096; + for (std::size_t first = 0; first < block / page; ++first) { + for (std::size_t count = 1; count <= block / page - first; ++count) { + coverage.Initialize(base, 3 * block); + coverage.Exclude(base + block, block, 20); + require(coverage.Restore(base + block + first * page, count * page, 21), "a complete-page replacement was not restored"); + for (std::size_t index = 0; index < 3 * block / page; ++index) { + const bool expected = index < block / page || index >= 2 * block / page || (index >= block / page + first && index < block / page + first + count); + require(coverage.Covers(base + index * page, page) == expected, "partial restoration changed an unrelated page"); + if (index != 0) { + const bool previous = index - 1 < block / page || index - 1 >= 2 * block / page || (index - 1 >= block / page + first && index - 1 < block / page + first + count); + require(coverage.Covers(base + index * page - 1, 2) == (previous && expected), "a page boundary proof ignored an excluded page"); + } + } + require(coverage.Covers(base + block, block) == (count == block / page), "partial page coverage proved a whole block"); + require(!coverage.Restore(base + block + first * page, count * page, 21), "restoring watched pages again reports a change"); + } + } + coverage.Initialize(base, 2 * block + 17); + coverage.Exclude(base + 2 * block, 17, 30); + require(!coverage.Restore(base + 2 * block, 16, 31), "an incomplete terminal page was restored"); + require(coverage.Restore(base + 2 * block, 17, 31) && coverage.Covers(base + 2 * block, 17), "a complete terminal arena page stayed excluded"); + coverage.Initialize(base, 2 * block); + coverage.Exclude(base, block, 40); + coverage.Exclude(base + block, block, 43); + require(coverage.Restore(base + block - page, 2 * page, 42), "a newer boundary page was not restored"); + require(coverage.Covers(base + block - page, page) && !coverage.Covers(base + block, page), "a stale boundary mapping erased a newer neighbouring exclusion"); std::cout << "write watch coverage passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/AliasCopyLabel.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AliasCopyLabel.cpp new file mode 100644 index 0000000000..6db8ed0908 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AliasCopyLabel.cpp @@ -0,0 +1,148 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 8; +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t SurfaceBytes = 0x40000; +constexpr std::size_t LabelOffset = 2 * SurfaceBytes; +constexpr std::size_t BlockBytes = LabelOffset + 0x10000; +constexpr std::uint32_t LabelValue = 0x600dcafeu; + +alignas(256) constexpr std::array StoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, 0x12345678u, 0xf0200108, 0x00020102, 0xbf810000, +}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::array TextureDescriptor(const std::uint8_t* texels) { + const auto address = AddressOf(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Store(AgcDriver::VulkanDevice& device, const std::uint8_t* texels) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + const std::span code(StoreCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::vector storage(BlockBytes + 0x10000u); + auto* block = reinterpret_cast((reinterpret_cast(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}); + auto* source = block; + auto* destination = block + SurfaceBytes; + auto* label = reinterpret_cast(block + LabelOffset); + for (std::size_t at = 0; at < SurfaceBytes; ++at) source[at] = static_cast(0x31u + at % 67u); + std::memset(destination, 0xa5, SurfaceBytes); + *label = 0; + { + GuestAllocations::Mutation mutation; + mutation.Add(block, BlockBytes, true, true, true); + } + const auto surfaceBytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(source))).guestBytes; + Require(surfaceBytes == SurfaceBytes, "the 256x256 32-bit surface must cover exactly its 256 KiB block"); + + Store(*device, source); + Require(StorageTexture::FindPending(AddressOf(source), SurfaceBytes) != nullptr, "the compute store left no pending image over the source"); + + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto outcome = device->CopyBuffer(AddressOf(destination), AddressOf(source), SurfaceBytes, 0, std::numeric_limits::max(), 0, 0, 0, [](std::span, std::uint64_t) {}); + if (outcome.path != 3) { + std::printf("skipped, the copy took path %d instead of the image copy\n", outcome.path); + return VulkanTestSkipped; + } + const std::uint32_t value = LabelValue; + const int reason = device->WriteLabelOnGpu(AddressOf(const_cast(label)), std::as_bytes(std::span(&value, 1)), 1, 0); + if (reason != 0) { + std::printf("skipped, the label was not recorded on the GPU (reason %d)\n", reason); + return VulkanTestSkipped; + } + device->SubmitRecorded(true); + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (*label != LabelValue) { + Require(std::chrono::steady_clock::now() < deadline, "the label after the image copy never landed"); + std::this_thread::yield(); + } + std::vector seen(destination, destination + SurfaceBytes); + + std::vector expected(SurfaceBytes); + AgcDriver::GuestMemory::Read(AddressOf(source), expected); + std::size_t differences = 0; + for (std::size_t at = 0; at < SurfaceBytes; ++at) differences += static_cast(seen[at]) != expected[at]; + Require(differences == 0, "a CPU read after the label that followed the image copy found " + std::to_string(differences) + " of " + std::to_string(SurfaceBytes) + " destination bytes still unwritten"); + + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::FlushCachedTextures(device->Device()); + } + device->WaitIdle(); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + std::puts("alias copy label 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/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index 81807e14b2..5c662f4b2d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" @@ -38,6 +39,16 @@ #include #include +#ifndef _WIN32 +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} +#endif + namespace { using AgcDriver::Graphics::ColorTargetLayout; @@ -447,12 +458,481 @@ 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); + const auto expectRange = [](std::string_view message, const char* plane, std::uint64_t address, std::size_t bytes, std::uint32_t samples) { + char detail[160]; + std::snprintf(detail, sizeof(detail), "(%s range 0x%llx+0x%zx, %u samples)", plane, static_cast(address), bytes, samples); + Require(message.find(detail) != std::string_view::npos, "a resident multisampled DCC refusal did not identify the affected plane, range and sample count"); + }; + 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; + if (refused) expectRange(error.what(), "color", source.Address(), AgcDriver::Graphics::MultisampledColorBytes(target), samples); + } + 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; + if (refused) expectRange(error.what(), "DCC", metadata.Address(), AgcDriver::Graphics::MultisampledDccBytes(target), samples); + } + Require(refused, "a same-value CPU DCC store did not invalidate multisampled DCC residency"); + } + if (code == 0x40u && alphaOnMsb && cmaskMode == 0xccu) { + reinterpret_cast(target.cmaskAddress)[0] = 0xccu; + 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; + if (refused) expectRange(error.what(), "CMASK", target.cmaskAddress, target.cmaskBytes, samples); + } + Require(refused, "a same-value CPU CMASK 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; + if (refused) expectRange(error.what(), "DCC", metadata.Address(), AgcDriver::Graphics::MultisampledDccBytes(target), samples); + } + 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; + blocks.push_back(std::make_unique(layout.Bytes() * samples)); + auto& source = *blocks.back(); + std::memset(source.data, 0, source.bytes); + blocks.push_back(std::make_unique(BlockBytes)); + auto& metadata = *blocks.back(); + std::memset(metadata.data, 0, BlockBytes); + auto first = DecodeTarget(source, samples); + first.dccAddress = metadata.Address(); + first.dccPipeAligned = true; + const Color written{0.25f, 0.25f, 0.25f, 0.25f}; + Draw(device, first, false, Float32Export, Program(written, Float32Export, 1u), false, true); + const std::vector ones(AgcDriver::Graphics::MultisampledDccBytes(first), std::byte{0xc0}); + AgcDriver::GuestMemory::Write(metadata.Address(), ones); + auto second = first; + second.pipeBankXor = 0x4000u; + Draw(device, second, false, Float32Export, Program(written, Float32Export, 1u), false, true); + blocks.push_back(std::make_unique(layout.Bytes())); + auto& resolved = *blocks.back(); + std::memset(resolved.data, 0, resolved.bytes); + AgcDriver::Graphics::ColorMetadataPass pass{}; + pass.mode = AgcDriver::Graphics::ColorMetadataPass::Mode::Resolve; + pass.source = second; + pass.targets = {DecodeTarget(resolved)}; + device.ColorMetadataPass(pass); + const auto stored = ReadBack(device, resolved); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto expected = x < ScissorWidth ? 1.0f + (written[channel] - 1.0f) / static_cast(samples) : 1.0f; + Require(FromHalf(stored[layout.Offset(x, y) / 2u + channel]) == expected, "a multisampled DCC target recreated with another pipe-bank XOR did not start from its new fast clear"); + } + } + } + auto third = second; + third.dccAlphaOnMsb = true; + bool refused = false; + try { + Draw(device, third, false, Float32Export, Program(written, Float32Export, 1u), false, true); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("layout or metadata changed") != std::string_view::npos; + } + Require(refused, "a resident multisampled DCC target accepted a use with other DCC metadata"); + programCount = 0; + } + for (const auto samples : {2u, 4u, 8u}) { + if (selectedSamples != 0 && samples != selectedSamples) continue; + for (const auto mode : {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u}) { + 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; + } + } +} + +} + +void DccRefillTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSamples) { + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8); + std::vector> blocks; + struct ClearTargets { + AgcDriver::VulkanDevice& device; + ~ClearTargets() { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(device.Device()); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); + } + } clearTargets{device}; +#ifndef _WIN32 + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + const auto decided = AgcDriver::Graphics::PrepareImportWatch(context); + struct Restore { + const AgcDriver::Graphics::Context& context; + AgcDriver::Graphics::ImportWatch decided; + ~Restore() { AgcDriver::Graphics::SetImportWatch(context, decided); } + } restore{context, decided}; +#endif + const auto fill = [](const Block& block, std::size_t bytes, std::uint32_t pattern) { + for (std::size_t offset = 0; offset + sizeof(pattern) <= bytes; offset += sizeof(pattern)) std::memcpy(block.data + offset, &pattern, sizeof(pattern)); + AgcDriver::Graphics::NoteColorMetadataFill(block.Address(), bytes, pattern); + }; + const Color written{0.25f, 0.25f, 0.25f, 0.25f}; + for (const auto samples : {2u, 4u, 8u}) { + if (selectedSamples != 0 && samples != selectedSamples) continue; + for (const auto mode : {0u, 1u, 2u, 3u, 4u, 5u, 6u}) { + for (const auto cmasked : {false, true}) { + if (mode == 4 && !cmasked) continue; +#ifdef _WIN32 + if (mode == 6) continue; +#else + AgcDriver::Graphics::SetImportWatch(context, mode == 5 ? AgcDriver::Graphics::ImportWatch::Watch : mode == 6 ? AgcDriver::Graphics::ImportWatch::Unwatch : decided); +#endif + blocks.push_back(std::make_unique(layout.Bytes() * samples)); + auto& source = *blocks.back(); + std::memset(source.data, 0, source.bytes); + blocks.push_back(std::make_unique(BlockBytes)); + auto& metadata = *blocks.back(); + std::memset(metadata.data, 0, BlockBytes); + auto target = DecodeTarget(source, samples); + target.dccAddress = metadata.Address(); + target.dccPipeAligned = true; + target.dccAlphaOnMsb = true; + Block* cmask = nullptr; + if (cmasked) { + blocks.push_back(std::make_unique(BlockBytes)); + cmask = blocks.back().get(); + std::memset(cmask->data, 0xcc, BlockBytes); + target.cmaskAddress = cmask->Address(); + target.cmaskBytes = AgcDriver::Graphics::CmaskBytes(Width, Height); + target.cmaskFastClear = true; + device.ColorMetadataPass({AgcDriver::Graphics::ColorMetadataPass::Mode::FmaskDecompress, {target}}); + } + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + const auto dccBytes = AgcDriver::Graphics::MultisampledDccBytes(target); + std::string_view expected; + bool rewatchable = false; + if (mode == 0) { + fill(metadata, dccBytes, 0xc0c0c0c0u); + if (cmask != nullptr) fill(*cmask, target.cmaskBytes, 0xccccccccu); + } else if (mode == 1) { + fill(metadata, dccBytes, 0xc0c0c0c0u); + } else if (mode >= 5) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto record = [&](const Block& block, std::size_t bytes, std::uint32_t value) { + const std::array pattern{value, value, value, value}; + Require(device.FillBuffer(block.Address(), bytes, pattern), "the metadata fill was not recorded on the GPU"); + AgcDriver::Graphics::NoteColorMetadataFill(block.Address(), bytes, value); + }; + record(metadata, dccBytes, 0xc0c0c0c0u); + if (cmask != nullptr) record(*cmask, target.cmaskBytes, 0xccccccccu); + if (!AgcDriver::GuestMemory::Watched(metadata.Address(), dccBytes) || (cmask != nullptr && !AgcDriver::GuestMemory::Watched(cmask->Address(), target.cmaskBytes))) expected = "requires watched guest memory"; +#ifdef _WIN32 + rewatchable = !expected.empty(); +#else + Require(mode == 6 || expected.empty(), "a GPU fill of multisampled DCC metadata left it unwatched although host imports stay watched"); +#endif + } else if (mode == 2) { + fill(metadata, dccBytes / 2u, 0xc0c0c0c0u); + expected = "writes to a resident multisampled DCC"; + } else if (mode == 3) { + fill(metadata, dccBytes, 0xffffffffu); + expected = "not a supported uniform fast clear"; + } else { + fill(*cmask, target.cmaskBytes, 0xccccccccu); + expected = "writes to a resident multisampled DCC"; + } + bool drawn = false; + if (!expected.empty()) { + bool refused = false; + try { + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + drawn = true; + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find(expected) != std::string_view::npos; + } + Require(refused || (drawn && rewatchable), "a write to resident multisampled DCC metadata that is not a full fast-clear fill was not refused"); + if (refused) { + programCount = 0; + continue; + } + } + if (!drawn) Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + blocks.push_back(std::make_unique(layout.Bytes())); + auto& resolved = *blocks.back(); + std::memset(resolved.data, 0, resolved.bytes); + AgcDriver::Graphics::ColorMetadataPass pass{}; + pass.mode = AgcDriver::Graphics::ColorMetadataPass::Mode::Resolve; + pass.source = target; + pass.targets = {DecodeTarget(resolved)}; + device.ColorMetadataPass(pass); + const auto stored = ReadBack(device, resolved); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto expectedValue = x < ScissorWidth ? 1.0f + (written[channel] - 1.0f) / static_cast(samples) : 1.0f; + const auto actual = FromHalf(stored[layout.Offset(x, y) / 2u + channel]); + Require(actual == expectedValue, "a full DCC fast-clear fill of a resident multisampled target did not clear it to the new color before the next draw"); + } + } + } + programCount = 0; + } + } + } +} + +void DccImportWindowTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSamples) { + if (!AgcDriver::GuestMemory::WriteWatched()) return; + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8); + std::vector> blocks; + struct ClearTargets { + AgcDriver::VulkanDevice& device; + ~ClearTargets() { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(device.Device()); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); + } + } clearTargets{device}; + const Color written{0.25f, 0.25f, 0.25f, 0.25f}; + for (const auto samples : {2u, 4u, 8u}) { + if (selectedSamples != 0 && samples != selectedSamples) continue; + blocks.push_back(std::make_unique(layout.Bytes() * samples)); + auto& source = *blocks.back(); + std::memset(source.data, 0, source.bytes); + blocks.push_back(std::make_unique(BlockBytes)); + auto& metadata = *blocks.back(); + std::memset(metadata.data, 0, BlockBytes); + auto target = DecodeTarget(source, samples); + target.dccAddress = metadata.Address(); + target.dccPipeAligned = true; + target.dccAlphaOnMsb = true; + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + Require(AgcDriver::GuestMemory::ImportWatched(metadata.Address(), BlockBytes, [&] { + auto* first = reinterpret_cast(metadata.data); + *first = *first; + return true; + }), "the import over resident multisampled DCC metadata failed"); + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + Require(AgcDriver::GuestMemory::ImportWatched(metadata.Address(), BlockBytes, [&] { + auto* first = reinterpret_cast(metadata.data); + *first = static_cast(*first ^ 0xffu); + return true; + }), "the second import over resident multisampled DCC metadata failed"); + bool refused = false; + try { + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("writes to a resident multisampled DCC") != std::string_view::npos; + } + Require(refused, "a store of new bytes to resident multisampled DCC metadata within an import window was not refused"); + programCount = 0; + } } int main(int argc, char** argv) { try { const auto device = OpenVulkanTestDevice(); 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); + DccRefillTests(*device, samples); + DccImportWindowTests(*device, samples); + } catch (const std::runtime_error& error) { + if (std::string_view(error.what()) != "AGC graphics: the device cannot render color with the target's sample count") throw; + std::printf("skipped, the device lacks the requested multisample color attachments: %s\n", error.what()); + 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/ArrayLayerTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp index 18dcef360c..aed0ffc37a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp @@ -68,6 +68,8 @@ alignas(256) std::array PixelCode{}; alignas(256) std::array Output{}; void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode); +void OverlappingArrays(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode, bool cpuWrite = false); +void OverlappingArrayLastSlice(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode); std::uint64_t AddressOf(const void* data) { return static_cast(reinterpret_cast(data)); @@ -81,27 +83,29 @@ std::string Hex(std::uint32_t value) { class GuestBlock { public: - GuestBlock() { + explicit GuestBlock(std::size_t registeredBytes = SurfaceBytes, std::size_t registeredOffset = 0, bool commitAll = true) : registeredOffset(registeredOffset) { #ifdef _WIN32 block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, LayerBytes)); - if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block + (commitAll ? 0 : registeredOffset), commitAll ? SurfaceBytes : registeredBytes, PAGE_READWRITE, commitAll ? SurfaceBytes : registeredBytes); #else - void* mapped = mmap(nullptr, SurfaceBytes + LayerBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + void* mapped = mmap(nullptr, SurfaceBytes + LayerBytes, commitAll ? PROT_READ | PROT_WRITE : PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (mapped != MAP_FAILED) { const auto begin = reinterpret_cast(mapped); const auto aligned = (begin + LayerBytes - 1) & ~(static_cast(LayerBytes) - 1); if (aligned != begin) munmap(mapped, aligned - begin); if (aligned + SurfaceBytes != begin + SurfaceBytes + LayerBytes) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + LayerBytes - aligned); block = reinterpret_cast(aligned); - if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes); + if (!commitAll) Require(mprotect(block + registeredOffset, registeredBytes, PROT_READ | PROT_WRITE) == 0, "cannot commit the active array slice"); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block + (commitAll ? 0 : registeredOffset), commitAll ? SurfaceBytes : registeredBytes); } #endif Require(block != nullptr, "array layer target: cannot allocate the guest block"); - GuestAllocations::Mutation().Add(block, SurfaceBytes, true, true, true); + if (!commitAll) GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(block), SurfaceBytes); + GuestAllocations::Mutation().Add(block + registeredOffset, registeredBytes, true, true, true); } ~GuestBlock() { - GuestAllocations::Mutation().Remove(block); + if (registered) GuestAllocations::Mutation().Remove(block + registeredOffset); #ifdef _WIN32 GuestArena::GuestArenaReset_nid_postfix(block, SurfaceBytes); GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); @@ -115,9 +119,15 @@ public: GuestBlock& operator=(const GuestBlock&) = delete; std::uint8_t* Data() { return block; } + void Unregister() { + GuestAllocations::Mutation().Remove(block + registeredOffset); + registered = false; + } private: std::uint8_t* block = nullptr; + std::size_t registeredOffset = 0; + bool registered = true; }; std::array SurfaceDescriptor(std::uint64_t address, std::uint32_t swizzleMode, std::uint32_t type, std::uint32_t lastLayer) { @@ -164,10 +174,10 @@ void Load(AgcDriver::VulkanDevice& device, std::span code, Dispatch(device, code, userData); } -void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer, std::uint32_t swizzleMode) { +void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer, std::uint32_t swizzleMode, std::uint32_t lastLayer = Layers - 1u) { auto& code = LoadCodes[layer]; code = {0x7e020280, 0x7e040280u | layer, 0xf0001128, 0x00010300, 0x34080082, 0xbf8c3f70, 0xe0701000, 0x80000304, 0xbf810000}; - Load(device, code, SurfaceDescriptor(surface, swizzleMode, Type2DArray, Layers - 1u)); + Load(device, code, SurfaceDescriptor(surface, swizzleMode, Type2DArray, lastLayer)); } void Sample2D(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t swizzleMode) { @@ -181,23 +191,23 @@ void RequireOutput(std::uint32_t inside, std::uint32_t outside, const std::strin } } -AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) { +AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed, std::uint32_t lastSlice) { AgcDriver::Registers cx; cx[0x318] = static_cast(address >> 8u); cx[0x31b] = slice | (slice << 13u); cx[0x31c] = Color32FloatInfo; cx[0x31d] = 0; cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); - cx[0x3b8] = (TiledAttrib3 & ~(0x1fu << 14u)) | (swizzleMode << 14u) | (collapsed ? 0u : Layers - 1u); + cx[0x3b8] = (TiledAttrib3 & ~(0x1fu << 14u)) | (swizzleMode << 14u) | (collapsed ? 0u : lastSlice); cx[0x390] = static_cast(address >> 40u); const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); Require(color.format == VK_FORMAT_R32_SFLOAT && color.bytes == LayerBytes && color.address == address + slice * LayerBytes, "the COLOR_32 float view did not decode as a 128x128 R32 slice of the array"); if (collapsed) Require(color.arrayAddress == 0 && color.arrayLastSlice == 0, "a collapsed slice view unexpectedly describes a whole array"); - else Require(color.arrayAddress == address && color.arrayLastSlice == Layers - 1u && color.arraySlice == slice, "the color view did not keep its array and slice"); + else Require(color.arrayAddress == address && color.arrayLastSlice == lastSlice && color.arraySlice == slice, "the color view did not keep its array and slice"); return color; } -void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) { +void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed, std::uint32_t lastSlice = Layers - 1u, std::uint32_t width = ScissorWidth) { const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; std::size_t at = 0; @@ -224,7 +234,7 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint const auto vertexResult = ShaderRecompiler::Recompile(vertex); const auto vertexPush = static_cast(vertexResult.pushConstants.size()); - const auto color = DecodeTarget(surface, slice, swizzleMode, collapsed); + const auto color = DecodeTarget(surface, slice, swizzleMode, collapsed, lastSlice); VkPipelineColorBlendAttachmentState blend{}; blend.colorWriteMask = 0xf; AgcDriver::Graphics::State state{}; @@ -236,7 +246,7 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; state.viewport = {0, static_cast(Side), static_cast(Side), -static_cast(Side), 0, 1}; state.negativeOneToOne = false; - state.scissor = {{0, 0}, {ScissorWidth, Side}}; + state.scissor = {{0, 0}, {width, Side}}; state.cullMode = VK_CULL_MODE_NONE; state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; state.blends = {blend}; @@ -290,6 +300,9 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t sw RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " uploaded again from guest memory"); AgcDriver::Graphics::ClearCachedTextures(device.Device()); SampleSliceZero(device, texels, swizzleMode); + OverlappingArrays(device, texels, swizzleMode); + OverlappingArrays(device, texels, swizzleMode, true); + OverlappingArrayLastSlice(device, texels, swizzleMode); } void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) { @@ -309,6 +322,140 @@ void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std: AgcDriver::Graphics::ClearCachedTextures(device.Device()); } +void OverlappingArrayLastSlice(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) { + const auto neighbourAddress = AddressOf(texels); + const auto surface = neighbourAddress + LayerBytes; + const auto cpuValue = std::bit_cast(0.125f); + const auto drawn = std::bit_cast(Drawn); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / sizeof(std::uint32_t), Initial); + AgcDriver::GuestMemory::CollectWrites(neighbourAddress, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + DrawSlice(device, surface, 1u, swizzleMode, false, 1u); + DrawSlice(device, neighbourAddress, 1u, swizzleMode, false, 1u, Side); + const auto neighbour = StorageTexture::FindPending(neighbourAddress, 2u * LayerBytes); + Require(neighbour != nullptr && neighbour->Descriptor().baseAddress == neighbourAddress, "the preceding target has no pending results"); + std::fill_n(reinterpret_cast(texels + 2u * LayerBytes), LayerBytes / sizeof(std::uint32_t), cpuValue); + AgcDriver::GuestMemory::CollectWrites(surface + LayerBytes, LayerBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + DrawSlice(device, surface, 1u, swizzleMode, false, 1u); + Require(StorageTexture::FindPending(neighbourAddress, 2u * LayerBytes) == neighbour, "CPU refresh of the last slice stored the preceding target's results"); + LoadLayer(device, surface, 1u, swizzleMode, 1u); + RequireOutput(drawn, cpuValue, "the refreshed last slice keeps the CPU's untouched pixels"); + LoadLayer(device, surface, 0u, swizzleMode, 1u); + RequireOutput(drawn, drawn, "the first slice sees the preceding target's pending results"); + StorageTexture::FlushPending(neighbourAddress, SurfaceBytes, nullptr, "test"); + device.WaitIdle(); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); +} + +void UnregisteredUnusedLayer(AgcDriver::VulkanDevice& device, std::uint32_t slice) { + GuestBlock block(LayerBytes, slice * LayerBytes); + auto* texels = block.Data(); + const auto surface = AddressOf(texels); + const auto active = surface + slice * LayerBytes; + std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Initial); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + DrawSlice(device, surface, slice, TileR64KBX, false); + Require(StorageTexture::FindPending(surface, SurfaceBytes) != nullptr, "the registered slice was not pending before its CPU write-back"); + StorageTexture::FlushPending(active, LayerBytes, nullptr, "test"); + device.WaitIdle(); + const auto* first = reinterpret_cast(active); + const auto* last = reinterpret_cast(active + LayerBytes); + const auto unchanged = [](std::uint32_t value) { return value == Initial; }; + const auto drawn = std::count(first, last, std::bit_cast(Drawn)); + const bool untouched = std::all_of(reinterpret_cast(texels), first, unchanged) && std::all_of(last, reinterpret_cast(texels + SurfaceBytes), unchanged); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); + Require(drawn == ScissorWidth * Side, "CPU write-back discarded the registered slice because an unused array slice was unregistered"); + Require(untouched, "CPU write-back modified an unregistered unused slice"); +} + +void RetiredArrayLayer(AgcDriver::VulkanDevice& device) { + GuestBlock block(LayerBytes); + auto* texels = block.Data(); + const auto surface = AddressOf(texels); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Initial); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + DrawSlice(device, surface, 0u, TileR64KBX, false); + Require(StorageTexture::FindPending(surface, SurfaceBytes) != nullptr, "the retired slice was not pending before it was unregistered"); + block.Unregister(); + StorageTexture::FlushPending(surface, LayerBytes, nullptr, "test"); + device.WaitIdle(); + const bool untouched = std::all_of(reinterpret_cast(texels), reinterpret_cast(texels + SurfaceBytes), [](std::uint32_t value) { return value == Initial; }); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); + Require(untouched, "CPU write-back stored results over a retired allocation"); +} + +void UncommittedUnusedLayers(AgcDriver::VulkanDevice& device, std::uint32_t slice) { + if (!AgcDriver::GuestMemory::WriteWatched()) return; + GuestBlock block(LayerBytes, slice * LayerBytes, false); + const auto surface = AddressOf(block.Data()); + const auto active = surface + slice * LayerBytes; + auto* texels = reinterpret_cast(active); + std::fill_n(texels, LayerBytes / sizeof(std::uint32_t), Initial); + Require(AgcDriver::GuestMemory::CollectWrites(active, LayerBytes) != 0, "the committed array slice has no write history"); + Require(AgcDriver::GuestMemory::CollectWritesUncached(surface, SurfaceBytes) == 0, "the uncommitted array fixture unexpectedly has complete backend write history"); + Require(AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes) == 0, "the unused array slices unexpectedly have complete write history"); + AgcDriver::GuestMemory::BumpCollectEpoch(); + DrawSlice(device, surface, slice, TileR64KBX, false); + const auto pending = StorageTexture::FindPending(surface, SurfaceBytes); + Require(pending != nullptr, "the committed array slice has no pending draw results"); + Require(pending->GenerationFor(active, LayerBytes) != 0, "an uncommitted unused slice erased the active slice's write history"); + const auto version = pending->Version(); + for (unsigned repeat = 0; repeat < 3; ++repeat) { + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(pending->RefreshLayers(slice, 1), "an unchanged committed slice was uploaded again"); + Require(pending->GenerationFor(active, LayerBytes) != 0 && pending->Version() == version, "repeated refresh erased the committed slice's write history"); + Require(StorageTexture::FindPending(surface, SurfaceBytes) == pending, "refresh settled unchanged GPU results for an uncommitted unused slice"); + } + const auto cpu = std::bit_cast(0.125f); + std::fill_n(texels, LayerBytes / sizeof(std::uint32_t), cpu); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(!pending->RefreshLayers(slice, 1), "the active array slice ignored a genuine CPU write"); + LoadLayer(device, surface, slice, TileR64KBX); + RequireOutput(cpu, cpu, "a committed array slice after a genuine CPU write"); + DrawSlice(device, surface, slice, TileR64KBX, false); + StorageTexture::FlushPending(active, LayerBytes, nullptr, "test"); + device.WaitIdle(); + Require(std::count(texels, texels + LayerBytes / sizeof(std::uint32_t), std::bit_cast(Drawn)) == ScissorWidth * Side, "write-back lost results in the committed array slice"); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); +} + +void OverlappingArrays(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode, bool cpuWrite) { + const auto surface = AddressOf(texels); + const auto next = surface + LayerBytes; + const auto drawn = std::bit_cast(Drawn); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Initial); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + DrawSlice(device, surface, 0, swizzleMode, false, 1u); + DrawSlice(device, next, 0, swizzleMode, false, 1u, Side); + const auto neighbour = StorageTexture::FindPending(next, 2u * LayerBytes); + Require(neighbour != nullptr && neighbour->Descriptor().baseAddress == next && neighbour->Descriptor().dimension == TextureDimension::k2DArray, "the next target did not render into the image of its own array"); + if (cpuWrite) { + std::fill_n(reinterpret_cast(texels), LayerBytes / sizeof(std::uint32_t), std::bit_cast(0.125f)); + AgcDriver::GuestMemory::CollectWrites(surface, LayerBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + } + DrawSlice(device, surface, 0, swizzleMode, false, 1u); + Require(StorageTexture::FindPending(next, 2u * LayerBytes) == neighbour, "a draw into slice 0 of the first array stored the next target's results, which only its unused second slice overlaps"); + Sample2D(device, surface, swizzleMode); + RequireOutput(drawn, cpuWrite ? std::bit_cast(0.125f) : Initial, "slice 0 of the first array sampled as a 2D texture after both draws"); + Require(StorageTexture::FindPending(next, 2u * LayerBytes) == neighbour, "a 2D texture over slice 0 of the first array stored the next target's results, which only its unused second slice overlaps"); + LoadLayer(device, next, 0, swizzleMode, 1u); + RequireOutput(drawn, drawn, "slice 0 of the next array after the draw into the first"); + LoadLayer(device, surface, 1, swizzleMode, 1u); + RequireOutput(drawn, drawn, "slice 1 of the first array (the next target's slice 0) read while the next target's results were pending"); + LoadLayer(device, surface, 0, swizzleMode, 1u); + RequireOutput(drawn, cpuWrite ? std::bit_cast(0.125f) : Initial, "slice 0 of the first array after both draws"); + LoadLayer(device, next, 1, swizzleMode, 1u); + RequireOutput(Initial, Initial, "slice 1 of the next array, which nothing drew"); + StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test"); + device.WaitIdle(); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); +} + } int main() { @@ -319,6 +466,11 @@ int main() { Run(*device, block.Data(), TileR64KBX); Run(*device, block.Data(), static_cast(AgcDriver::Graphics::ColorTileMode::D64KBX)); for (std::uint32_t slice = 1; slice < Layers; ++slice) Run(*device, block.Data(), TileR64KBX, true, slice); + UnregisteredUnusedLayer(*device, 0u); + UnregisteredUnusedLayer(*device, Layers - 1u); + RetiredArrayLayer(*device); + UncommittedUnusedLayers(*device, 0u); + UncommittedUnusedLayers(*device, Layers - 1u); std::puts("array layer target tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BdaAnchorReads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BdaAnchorReads.cpp new file mode 100644 index 0000000000..9bd219d267 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BdaAnchorReads.cpp @@ -0,0 +1,164 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t InputDwords = 1024; +constexpr std::uint32_t Fill = 0xcdcdcdcdu; +constexpr std::uint32_t OutputBase = 0x8000; +constexpr std::uint32_t OutputDwords = 8; + +alignas(256) constexpr std::array Code{ + 0x34040082, 0x34060085, 0x4a0606ff, OutputBase, + 0xdc308010, 0x04000002, 0xdc308014, 0x05000002, 0xdc308018, 0x06000002, + 0xdc308403, 0x07000002, + 0xf4000100, 0xfa000203, 0xf4000140, 0xfa0002f6, + 0xbf8c3f70, 0xbf8cc07f, 0x7e100204, 0x7e120205, + 0xdc708000, 0x00000403, 0xdc708004, 0x00000503, 0xdc708008, 0x00000603, 0xdc70800c, 0x00000703, + 0xdc708010, 0x00000803, 0xdc708014, 0x00000903, 0xbf810000}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "BDA anchor reads: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::vector Initial() { + std::vector memory(BlockBytes / 4u, Fill); + for (std::uint32_t dword = 0; dword < InputDwords; ++dword) memory[dword] = 0x61000000u + dword * 0x00010307u; + return memory; +} + +std::uint32_t Bytes(const std::vector& memory, std::uint32_t address) { + std::uint32_t value = 0; + for (std::uint32_t byte = 0; byte < 4u; ++byte) { + const std::uint32_t at = address + byte; + value |= ((memory[at / 4u] >> (at % 4u * 8u)) & 0xffu) << (byte * 8u); + } + return value; +} + +std::vector Expected() { + const auto initial = Initial(); + auto memory = initial; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + auto* out = &memory[OutputBase / 4u + tid * OutputDwords]; + out[0] = Bytes(initial, tid * 4u + 0x10u); + out[1] = Bytes(initial, tid * 4u + 0x14u); + out[2] = Bytes(initial, tid * 4u + 0x18u); + out[3] = Bytes(initial, tid * 4u + 0x403u); + out[4] = Bytes(initial, 0x200u); + out[5] = Bytes(initial, 0x2f4u); + } + return memory; +} + +ShaderRecompiler::RecompileResult Recompile(std::uint32_t waveSize, std::span userData, std::span guest, const ShaderRecompiler::SpirvTarget& target) { + const std::span code(Code); + const std::array regions{{{reinterpret_cast(code.data()), std::as_bytes(code)}, {reinterpret_cast(guest.data()), guest}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, regions}, + target, + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +bool SearchesAnchor(const ShaderRecompiler::SharedSpirv& spirv) { + const std::string_view text(reinterpret_cast(spirv.data()), spirv.size() * sizeof(std::uint32_t)); + return text.find("bda_anchor_range") != std::string_view::npos; +} + +void Run(AgcDriver::VulkanDevice& device, GuestBlock& guest, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target, const std::string& name) { + auto memory = Initial(); + std::memcpy(guest.Data(), memory.data(), BlockBytes); + const auto address = static_cast(reinterpret_cast(guest.Data())); + const std::vector userData{static_cast(address), static_cast(address >> 32u)}; + const auto result = Recompile(waveSize, userData, std::span(reinterpret_cast(guest.Data()), InputDwords * sizeof(std::uint32_t)), target); + Require(SearchesAnchor(result.spirv), "BDA anchor reads: " + name + " reads of one user-data base do not share an anchor"); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); + std::memcpy(memory.data(), guest.Data(), BlockBytes); + const auto expected = Expected(); + for (std::size_t dword = 0; dword < memory.size(); ++dword) { + Require(memory[dword] == expected[dword], "BDA anchor reads: " + name + " byte " + std::to_string(dword * 4u) + " is " + Hex(memory[dword]) + ", expected " + Hex(expected[dword])); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock guest; + const auto native = device->Target(); + const auto split = device->ComputeTarget(32); + Run(*device, guest, 32, native, "wave32"); + Run(*device, guest, 64, native, "wave64"); + if (split.subgroupSize != native.subgroupSize) Run(*device, guest, 64, split, "wave64 split"); + std::puts("BDA anchor read 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/BindlessImages.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BindlessImages.cpp index a990a210d7..3268ff39d8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BindlessImages.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BindlessImages.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "Recompiler.hpp" +#include "ImageTableAbi.hpp" #include "VulkanTestDevice.hpp" #include #include @@ -61,17 +62,16 @@ void Run(AgcDriver::VulkanDevice& device) { else Require(shader.cacheHit && shader.variantId == artifact.variantId, "bindless table changed the compiled artifact"); if (iteration == 1u) { auto invalid = shader; - auto binding = std::ranges::find_if(invalid.bindings, [](const DescriptorBinding& value) { return value.role == DescriptorRole::ShaderData; }); - Require(binding != invalid.bindings.end(), "bindless shader has no runtime metadata"); - const auto offset = invalid.imageMetadataDword + invalid.runtimeImageResources.at(0) * (sizeof(RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t)) + offsetof(RuntimeAbi::ResourceMetadata, firstElement) / sizeof(std::uint32_t); - binding->guestDescriptor.at(offset) = RuntimeAbi::SampledHeapCapacity; + auto binding = std::ranges::find_if(invalid.bindings, [](const DescriptorBinding& value) { return value.role == DescriptorRole::ImageTableMap; }); + Require(binding != invalid.bindings.end(), "bindless shader has no image table map"); + binding->guestDescriptor.at(ImageTableAbi::HeaderElementCount) += 1u; bool rejected = false; try { device.Dispatch(invalid, 1u, 1u, 1u); } catch (const std::exception& error) { - rejected = std::string_view(error.what()).find("runtime metadata exceeds its bound heap") != std::string_view::npos; + rejected = std::string_view(error.what()).find("image table map disagrees with its image binding") != std::string_view::npos; } - Require(rejected, "driver accepted an out-of-range runtime heap index"); + Require(rejected, "driver accepted an image table map naming more elements than its bound table"); } device.Dispatch(shader, 1u, 1u, 1u); device.SubmitRecorded(false); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp new file mode 100644 index 0000000000..0cfcfe9c2f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp @@ -0,0 +1,92 @@ +#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 Threads = 32; +constexpr std::uint32_t Sentinel = 0x5eed0001u; +constexpr std::size_t OffsetWord = 2; +alignas(256) std::array Buffer{}; +alignas(256) std::array Output{}; + +alignas(256) auto Code = std::to_array({ + 0x34020082, + 0x7e040281, + 0xe0c85000, 0x80000201, + 0xbf8c3f70, + 0xe0701000, 0x80010201, + 0xbf810000, +}); + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t offset) { + for (std::uint32_t lane = 0; lane < Threads; ++lane) Buffer[lane] = lane * 100u; + Buffer[Threads] = Sentinel; + Output.fill(0xdeadbeefu); + Code[OffsetWord] = 0xe0c85000u | offset; + std::vector userData(8, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(buffer.begin(), buffer.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; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(std::uint32_t offset) { + const auto what = "buffer_atomic_add at byte offset " + std::to_string(offset) + ": lane "; + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(Output[lane] == lane * 100u, what + std::to_string(lane) + " returned " + std::to_string(Output[lane]) + ", expected " + std::to_string(lane * 100u)); + Require(Buffer[lane] == lane * 100u + 1u, what + std::to_string(lane) + " left dword " + std::to_string(Buffer[lane]) + ", expected " + std::to_string(lane * 100u + 1u)); + } + Require(Buffer[Threads] == Sentinel, what + "the dword after the last lane changed"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityInt64)) { + std::puts("skipped, the device has no shaderInt64"); + return VulkanTestSkipped; + } + for (std::uint32_t offset = 0; offset < 4u; ++offset) { + Run(*device, offset); + Check(offset); + } + std::puts("buffer atomic unaligned tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp index bf04d4c6f0..51fa4f8630 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp @@ -125,6 +125,9 @@ void ScalarReads(AgcDriver::VulkanDevice& device) { static_cast(ShaderRecompiler::Recompile(request)); } catch (const std::runtime_error& error) { Require(std::string(error.what()).find("buffer descriptor uses an unsupported type") != std::string::npos, "vector buffer descriptor failed for another reason"); + char words[64]; + std::snprintf(words, sizeof(words), "(buffer 0 V# %08x %08x %08x %08x)", userData[0], userData[1], userData[2], userData[3]); + Require(std::string(error.what()).find(words) != std::string::npos, std::string("unsupported buffer descriptor error does not name ") + words + ": " + error.what()); continue; } throw std::runtime_error("a vector or mixed buffer access accepted a non-buffer descriptor type"); @@ -133,16 +136,19 @@ void ScalarReads(AgcDriver::VulkanDevice& device) { request.shader.code = code; request.shader.codeAddress = reinterpret_cast(code.data()); auto input = BufferDescriptor(Input.data(), InputBytes); + std::copy(input.begin(), input.end(), userData.begin()); + Output.fill(Fill); + const auto baselineResult = ShaderRecompiler::Recompile(request); + device.Dispatch(baselineResult, 1, 1, 1); + device.WaitIdle(); + const auto baseline = Output; input[1] |= 0x40000000u; std::copy(input.begin(), input.end(), userData.begin()); + Output.fill(Fill); const auto result = ShaderRecompiler::Recompile(request); - try { - device.Dispatch(result, 1, 1, 1); - } catch (const std::runtime_error& error) { - Require(std::string(error.what()).find("buffer descriptor has reserved bits set") != std::string::npos, "vector or mixed buffer descriptor failed for another reason"); - continue; - } - throw std::runtime_error("a vector or mixed buffer access accepted reserved descriptor bits"); + device.Dispatch(result, 1, 1, 1); + device.WaitIdle(); + Require(Output == baseline, "vector or mixed buffer read changed with reserved bit 30 set"); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferResourceVcc.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferResourceVcc.cpp new file mode 100644 index 0000000000..159fd6e259 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferResourceVcc.cpp @@ -0,0 +1,87 @@ +#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; +constexpr std::uint32_t Threads = 64u; +constexpr std::uint32_t Guard = 0xdeadbeefu; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +std::array Descriptor(const void* bytes, std::uint32_t size, std::uint32_t format) { + const auto address = reinterpret_cast(bytes); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), size, format}; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t wave, std::uint32_t opcode, std::uint32_t bytes, std::uint32_t format, bool store = false) { + Output.fill(Guard); + alignas(256) const std::array code{ + 0x34020082u, 0xbee80304u, 0xbee90305u, 0xbeea0306u, 0xbeeb0307u, + store ? 0xe0301000u : opcode, store ? 0x80000201u : 0x801a0201u, 0xbf8c3f70u, + store ? opcode : 0xe0701000u, store ? 0x801a0201u : 0x80000201u, 0xbf810000u}; + std::array userData{}; + const auto output = Descriptor(Output.data() + 4u, store ? bytes : Threads * 4u, format); + const auto input = Descriptor(Input.data(), store ? Threads * 4u : bytes, format); + const auto ordinary = store ? input : output; + const auto mixed = store ? output : input; + std::copy(ordinary.begin(), ordinary.end(), userData.begin()); + std::copy(mixed.begin(), mixed.end(), userData.begin() + 4u); + const std::span words(code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), words, 0u, {}}, + {wave, 0u, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), {0u, 0u, 0u, 128u}}; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1u, 1u, 1u, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::size_t index = 0u; index < 4u; ++index) { + Require(Output[index] == Guard && Output[Threads + 4u + index] == Guard, "buffer VCC tuple writes passed the output bounds"); + } + for (std::uint32_t lane = 0u; lane < Threads; ++lane) { + std::uint32_t expected = lane * 4u < bytes ? Input[lane] : store ? Guard : 0u; + if (!store && format == 0x31038facu && opcode == 0xe0001000u && lane * 4u < bytes) { + expected = std::bit_cast(static_cast(Input[lane] & 0xffu) / 255.0f); + } + if (Output[lane + 4u] != expected) { + std::fprintf(stderr, "wave=%u opcode=%08x store=%d bytes=%u format=%08x lane=%u actual=%08x expected=%08x\n", wave, opcode, store, bytes, format, lane, Output[lane + 4u], expected); + } + Require(Output[lane + 4u] == expected, "buffer VCC descriptor data differs from its scalar words"); + } +} +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (std::uint32_t lane = 0u; lane < Threads; ++lane) Input[lane] = 0x3f800000u | (lane * 0x10001u); + for (const auto wave : {32u, 64u}) { + for (const auto load : {0xe8b01000u, 0xe0301000u, 0xe0001000u}) { + Run(*device, wave, load, Threads * 4u, 0x31016facu); + Run(*device, wave, load, 12u, 0x31016facu); + } + Run(*device, wave, 0xe0001000u, Threads * 4u, 0x31038facu); + for (const auto store : {0xe0701000u, 0xe8b41000u}) { + Run(*device, wave, store, Threads * 4u, 0x31016facu, true); + Run(*device, wave, store, 12u, 0x31016facu, true); + } + } + std::puts("buffer mixed SGPR/VCC descriptor loads/stores, both VCC words, wave32/wave64 and output guards pass"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp index bbd38f1f91..bc07a0cdbf 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,36 @@ 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 RunAtomicContainingDword(AgcDriver::VulkanDevice& device) { + DispatchAtomic(device, 2u, 0u); + for (std::uint32_t word = 0; word < 128u; ++word) { + const auto expected = word >= 1u && word < Lanes ? Fill + word + 1u : Fill; + Require(Output[word] == expected, "buffer unaligned base atomic, view +2, offset lane * 4: dword " + std::to_string(word) + " is " + Hex(Output[word]) + ", expected " + Hex(expected) + ": a 32-bit atomic off a DWORD boundary acts on its containing dword"); + } + for (std::uint32_t lane = 0; lane < Lanes; ++lane) Require(Output[128u + lane] == Fill, "buffer unaligned base atomic, view +2, offset lane * 4: lane " + std::to_string(lane) + " returned " + Hex(Output[128u + lane]) + ", expected " + Hex(Fill)); } } @@ -246,7 +265,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 +276,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); } + RunAtomicContainingDword(*device); std::puts("buffer unaligned base tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp new file mode 100644 index 0000000000..c60b857816 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp @@ -0,0 +1,248 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#include "ImageTableAbi.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using namespace AgcDriver::Graphics; + +constexpr std::uint32_t Side = 64; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t AllocationBytes = 2 * BlockBytes; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + data = GuestArena::GuestArenaAllocate_nid_postfix(AllocationBytes, BlockBytes); + if (data != nullptr) GuestArena::GuestArenaCommit_nid_postfix(data, AllocationBytes, PAGE_READWRITE, AllocationBytes); +#else + void* mapped = mmap(nullptr, AllocationBytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + AllocationBytes != begin + AllocationBytes + BlockBytes) munmap(reinterpret_cast(aligned + AllocationBytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, AllocationBytes); + } +#endif + Require(data != nullptr, "cannot allocate tracked image memory"); + std::memset(data, 0, AllocationBytes); + GuestAllocations::Mutation().Add(data, AllocationBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(data); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(data, AllocationBytes); + GuestArena::GuestArenaRelease_nid_postfix(data, AllocationBytes); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, AllocationBytes); + munmap(data, AllocationBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + +private: + void* data = nullptr; +}; + +template +void RequireColorGuard(TCall&& call, const char* failure) { + bool guarded = false; + try { + call(); + } catch (const std::runtime_error& error) { + guarded = std::string_view(error.what()).find("multisampled color target") != std::string_view::npos; + if (!guarded) throw; + } + Require(guarded, failure); +} + +void CheckCachedImage(AgcDriver::VulkanDevice& device, bool sampled, bool draw) { + std::printf("checking %s %s image reuse\n", draw ? "draw" : "compute", sampled ? "sampled" : "storage"); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + GuestBlock block; + struct Cleanup { + VkDevice device; + ~Cleanup() { + ClearMultisampleTargets(device); + ClearCachedTextures(device); + } + } cleanup{context.device}; + const auto address = block.Address(); + ShaderRecompiler::DescriptorBinding binding{}; + binding.kind = sampled ? ShaderRecompiler::DescriptorKind::SampledImage : ShaderRecompiler::DescriptorKind::StorageImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.binding = (sampled ? ShaderRecompiler::RuntimeAbi::FirstImageBinding : ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding) + PlaceholderDimensionsPerClass + 2u; + binding.count = 1; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageWritten = {false}; + binding.imageSamplers = {0u}; + binding.guestDescriptor = {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (20u << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u}; + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult program; + program.bindings.push_back(binding); + const CompiledShader shader{draw ? ShaderRecompiler::ShaderStage::Fragment : ShaderRecompiler::ShaderStage::Compute, &program, 0}; + const std::array stages{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, shader}}; + const auto build = [&] { + if (draw) return std::make_unique(context, stages, ColorTarget{}, 0, 0, 0); + return std::make_unique(context, shader); + }; + auto resources = build(); + Require(resources->Reusable(), "the ordinary image resources are not reusable"); + const auto revalidate = [&](ShaderResources::ProofReport* report = nullptr) { + return draw ? resources->Revalidate(stages, report) : resources->Revalidate(shader, report); + }; + ShaderResources::ProofReport proof; + Require(revalidate(&proof), "the unchanged image resources failed their initial proof"); + if (std::getenv("APS5_NO_FAST_REVALIDATE") == nullptr) { + if (AgcDriver::GuestMemory::Watched(address, Side * Side * 4u)) Require(proof.path == ShaderResources::ProofPath::Fast, "the fixture did not exercise the fast image proof"); + else std::puts("image memory is compared, not watched: fast image proof not tested; checking the image guards through the full proof"); + } + ColorTarget target{}; + target.address = address + BlockBytes; + target.surfaceAddress = target.address; + target.extent = {Side, Side}; + target.surfaceExtent = target.extent; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.tileMode = ColorTileMode::RenderTarget; + target.elementBytes = 4; + target.bytes = Side * Side * 4u; + target.samples = 4; + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the disjoint multisample target has no view"); + device.SubmitRecorded(true); + device.WaitIdle(); + Require(revalidate(), "a disjoint multisample target prevented image reuse"); + target.address = address; + target.surfaceAddress = address; + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the overlapping multisample target has no view"); + device.SubmitRecorded(true); + device.WaitIdle(); + RequireColorGuard([&] { revalidate(); }, "a cached image access accepted memory owned by a multisampled color target"); + RequireColorGuard([&] { build(); }, "a fresh image access accepted memory owned by a multisampled color target"); + ClearMultisampleTargets(context.device); + Require(revalidate(), "retiring the multisample targets did not restore image reuse"); +} + +void CheckMultisampleTable(AgcDriver::VulkanDevice& device) { + namespace Abi = ShaderRecompiler::ImageTableAbi; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (context.imageTableCapacity == 0) { + std::puts("skipped, native multisample table check needs image table support"); + return; + } + GuestBlock block; + struct Cleanup { + VkDevice device; + ~Cleanup() { ClearMultisampleTargets(device); } + } cleanup{context.device}; + ColorTarget target{}; + target.address = block.Address(); + target.surfaceAddress = target.address; + target.extent = {Side, Side}; + target.surfaceExtent = target.extent; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.tileMode = ColorTileMode::RenderTarget; + target.elementBytes = 4; + target.bytes = Side * Side * 4u; + target.samples = 4; + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the native multisample table target has no view"); + device.SubmitRecorded(true); + device.WaitIdle(); + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding map{}; + map.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + map.role = ShaderRecompiler::DescriptorRole::ImageTableMap; + map.binding = static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap); + map.count = 1; + map.guestDescriptor.resize(Abi::HeaderWords + 1u); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderElementCount] = 1u; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords + 1u; + program.bindings.push_back(map); + ShaderRecompiler::DescriptorBinding image{}; + image.kind = ShaderRecompiler::DescriptorKind::SampledImage; + image.role = ShaderRecompiler::DescriptorRole::ImageTable; + image.binding = ShaderRecompiler::RuntimeAbi::ImageTableBinding; + image.count = 1; + image.imageShapes = {ShaderRecompiler::DescriptorImageShape::Image2DMultisample}; + image.imageDepthCompare = {false}; + image.imageWritten = {false}; + image.imageSamplers = {0u}; + const auto address = block.Address(); + image.guestDescriptor = {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (56u << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), 0xfacu | (2u << 16u) | (0x1bu << 20u) | (14u << 28u), 0u, 2u << 4u, 0u, 0u}; + program.bindings.push_back(image); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + ShaderResources resources(context, shader); + Require(resources.SampledTextures().size() == 1u, "the native multisample table entry was rejected during construction"); + Require(resources.Reusable(), "the native multisample table did not reach resource validation"); + static_cast(resources.Revalidate(shader)); + device.SubmitRecorded(true); + device.WaitIdle(); + std::puts("native multisample table validation did not reject its color target"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (!AgcDriver::GuestMemory::WriteWatched() || (context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "cached image guard tests require write tracking and 4x color targets"); + std::puts("skipped, cached image guard tests require write tracking and 4x color targets"); + return VulkanTestSkipped; + } + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + for (const bool draw : {false, true}) { + for (const bool sampled : {false, true}) CheckCachedImage(*device, sampled, draw); + } + CheckMultisampleTable(*device); + std::puts("cached image multisample guard tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ClampedGlobalFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ClampedGlobalFill.cpp new file mode 100644 index 0000000000..49a656173f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ClampedGlobalFill.cpp @@ -0,0 +1,269 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.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 + +namespace { + +using AgcDriver::Graphics::DccKeys; +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using AgcDriver::DriverDetail::MatchClampedGlobalFill; + +alignas(256) constexpr std::array Code{0xd7460000u, 0x04010c06u, 0x7e080205u, 0x7e0a0205u, 0x7e0c0205u, 0x7e0e0205u, 0x34000084u, 0x26000004u, 0xdc788000u, 0x00020400u, 0xbf810000u}; +alignas(256) constexpr std::array WriteCode{0x7e020280u, 0x7e0402f2u, 0x7e0602f2u, 0x7e0802f2u, 0x7e0a02f2u, 0xf0201f08u, 0x00010200u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t WriteThreads = 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 LabelOffset = SurfaceBytes + 4096; +constexpr std::size_t BlockBytes = 2 * 65536; +constexpr std::size_t OutputDwords = 1028; +alignas(256) std::array Output{}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(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, "clamped global fill: 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::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::vector UserData(void* output, std::uint32_t maxOffset, std::uint32_t value) { + const auto address = reinterpret_cast(output); + return {0u, 0u, static_cast(address), static_cast(address >> 32u), maxOffset, value}; +} + +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), + }; +} + +void MatchTests() { + const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1}; + const std::array packet{0xc0031500u, 1, 1, 1, 0x41u}; + const auto data = UserData(Output.data(), 1008, 0x5a5a5a5au); + const auto fill = MatchClampedGlobalFill(packet, Code, data, compute); + Require(fill && fill->base == reinterpret_cast(Output.data()) && fill->bytes == 1024 && fill->pattern == std::array{0x5a5a5a5au, 0x5a5a5a5au, 0x5a5a5a5au, 0x5a5a5a5au}, "the clamped global fill kernel was not recognized as a fill of its value"); + for (std::size_t word = 0; word < Code.size(); ++word) { + auto altered = Code; + altered[word] ^= 0x10000u; + Require(!MatchClampedGlobalFill(packet, altered, data, compute), "a different shader matched the clamped global fill kernel"); + } + Require(!MatchClampedGlobalFill(packet, std::span(Code).first(10), data, compute), "a truncated shader matched"); + auto extra = data; + extra.push_back(0); + Require(!MatchClampedGlobalFill(packet, Code, extra, compute), "a different user SGPR count matched (the group id moves)"); + auto threads = packet; + threads[4] |= 0x20u; + Require(!MatchClampedGlobalFill(threads, Code, data, compute), "a dispatch in thread dimensions matched"); + auto rows = packet; + rows[2] = 2; + Require(!MatchClampedGlobalFill(rows, Code, data, compute), "a two-dimensional dispatch matched"); + auto noGroup = compute; + noGroup.groupIdEnable[0] = false; + Require(!MatchClampedGlobalFill(packet, Code, data, noGroup), "a dispatch without the group id matched"); + Require(!MatchClampedGlobalFill(packet, Code, UserData(Output.data() + 1, 1008, 0), compute), "an unaligned destination matched"); + Require(!MatchClampedGlobalFill(packet, Code, UserData(Output.data(), 1000, 0), compute), "a largest offset off the 16-byte grid matched"); + const auto clamped = MatchClampedGlobalFill(packet, Code, UserData(Output.data(), 240, 0), compute); + Require(clamped && clamped->bytes == 256, "the fill reached past its largest store offset"); + std::array four = packet; + four[1] = 4; + const auto launched = MatchClampedGlobalFill(four, Code, UserData(Output.data(), 0x100000u, 0), compute); + Require(launched && launched->bytes == 4096, "the fill reached past its last thread's store"); +} + +std::vector Commands(std::span code, const std::vector& data, std::uint32_t groups, std::uint32_t threads) { + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{threads, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (1u << 7u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u}); + return words; +} + +void Submit(const std::vector& words) { + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); +} + +void RangeTests(GuestBlock& block) { + struct Case { + std::uint32_t groups, maxOffset, value; + }; + constexpr std::array cases{{ + {1, 1008, 0u}, + {4, 1008, 0x5a5a5a5au}, + {4, 240, 0xffffffffu}, + {1, 0x100000u, 0x01020304u}, + {0, 1008, 0x77777777u}, + }}; + const std::span output(reinterpret_cast(block.Data()), OutputDwords); + for (const auto& item : cases) { + std::fill(output.begin(), output.end(), Untouched); + Submit(Commands(Code, UserData(output.data(), item.maxOffset, item.value), item.groups, 64)); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(output.data()), output.size_bytes()); + const auto filledDwords = std::min(item.groups * 64u * 4u, item.maxOffset / 4u + 4u); + for (std::uint32_t i = 0; i < output.size(); ++i) { + const auto expected = i < filledDwords ? item.value : Untouched; + Require(output[i] == expected, "a flush right after the fill read dword " + std::to_string(i) + " as " + std::to_string(output[i]) + ", expected " + std::to_string(expected) + " (groups " + std::to_string(item.groups) + ", largest offset " + std::to_string(item.maxOffset) + "): the flush did not wait for the fill's range"); + } + } +} + +void KeyFillOrderTests(GuestBlock& block) { + auto* texels = block.Data(); + auto* keys = texels + KeysOffset; + auto* label = reinterpret_cast(texels + LabelOffset); + const auto surface = AddressOf(texels); + std::memset(texels, 0x55, SurfaceBytes); + std::memset(keys, 0xff, KeyBytes); + *label = 0; + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto texture = TextureDescriptor(texels, keys); + std::vector writeData(16, 0u); + std::copy(texture.begin(), texture.end(), writeData.begin() + 4); + const auto write = Commands(WriteCode, writeData, 1, WriteThreads); + Submit(write); + auto words = Commands(Code, UserData(keys, static_cast(KeyBytes - 16), 0u), 1, 64); + words.insert(words.end(), write.begin(), write.end()); + const auto labelAddress = AddressOf(label); + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(labelAddress), static_cast(labelAddress >> 32u), 1u, 0u, 0u}); + Submit(words); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (std::atomic_ref(*label).load() != 1 && std::chrono::steady_clock::now() < deadline) AgcDriver::GuestMemory::FlushGpuWrites(labelAddress, sizeof(*label)); + Require(std::atomic_ref(*label).load() == 1, "the label after the key fill and the write did not land"); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto image = StorageTexture::FindPending(surface, SurfaceBytes); + Require(image != nullptr && image->Descriptor().dccAddress == AddressOf(keys), "the surface written after the key fill has no pending image under its keys"); + AgcDriver::GuestMemory::CollectWritesUncached(AddressOf(keys), KeyBytes); + const auto read = AgcDriver::Graphics::ProvedClearKeys(image->Descriptor(), image->GuestBytes(), image->KeyProof()); + char message[320]; + std::snprintf(message, sizeof(message), "a write recorded after RADV's key fill: once the fill landed, the keys read %s against uploaded %s, so the next key read drops the write", AgcDriver::Graphics::DccKeysName(read), AgcDriver::Graphics::DccKeysName(image->UploadedKeys())); + Require(read == image->UploadedKeys() && read == DccKeys::Uncompressed, message); + } + AgcDriver::GuestMemory::FlushGpuWrites(surface, SurfaceBytes); + const auto written = static_cast(std::count(texels, texels + SurfaceBytes, std::uint8_t{0xff})); + const auto cleared = static_cast(std::count(texels, texels + SurfaceBytes, std::uint8_t{0x00})); + char message[320]; + std::snprintf(message, sizeof(message), "the write after RADV's key fill stored %zu written and %zu cleared bytes of %zu, expected %u written over the clear", written, cleared, SurfaceBytes, WriteThreads * 4u); + Require(written == WriteThreads * 4u && cleared == SurfaceBytes - written, message); +} + +} + +int main() { + try { + if (std::getenv("APS5_NO_FILL_HLE") == nullptr) MatchTests(); + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + { + GuestBlock block; + KeyFillOrderTests(block); + } + { + GuestBlock block; + RangeTests(block); + } + AgcDriverShutdown_nid_postfix(); + std::puts("clamped global fill tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/ComputeFsTableCalls.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp new file mode 100644 index 0000000000..a5395d5661 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp @@ -0,0 +1,119 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "SceShaders.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t Marker = 0x55u; +constexpr std::uint64_t FsTable = 0xfe0040000ull; + +alignas(256) constexpr std::array Caller{ + 0x34040082u, 0x7e0202ffu, 0x00000055u, 0xbeea03ffu, 0xe0040000u, 0xbeeb038fu, 0xf4080235u, 0xfa000000u, 0xbf8cc07fu, 0xf4240384u, + 0xfa000020u, 0xbf8cc07fu, 0xbf130e80u, 0xbf840006u, 0xf4240404u, 0xfa000028u, 0xbf8cc07fu, 0xbe8e210eu, 0xe0701000u, 0x80010102u, + 0xbf810000u, 0xbf9f0000u}; +alignas(256) std::array Output{}; + +std::vector Commands() { + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{Threads, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(Caller.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{8u << 1u}; + registers(0x213, resource); + const auto output = reinterpret_cast(Output.data()); + const std::array data{0u, 0u, 0u, 0u, static_cast(output), static_cast((output >> 32u) & 0xffffu), + static_cast(sizeof(Output)), 0x31016facu}; + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u}); + return words; +} + +std::string Dispatch() { + Output.fill(Untouched); + const auto words = Commands(); + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + std::string failure; + try { + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + } catch (const std::exception& error) { + failure = error.what(); + } + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); + return failure; +} + +void CheckTable() { + AgcDriverWaitIdle_nid_postfix(); + Require(AgcDriver::GuestMemory::Accessible(reinterpret_cast(FsTable), 0x60u), "the FS table at 0xfe0040000 is not mapped"); + std::array slots{}; + std::memcpy(slots.data(), reinterpret_cast(FsTable), sizeof(slots)); + const auto records = slots[0] & 0xffffffffffffull; + Require(slots[0] >> 48u == 16u, "FS table slot 0 does not have a stride of 16"); + Require(slots[1] == 0x0000520400000008ull, "FS table slot 0 does not describe eight records with dword3 0x5204"); + for (std::size_t slot = 2; slot < slots.size(); ++slot) Require(slots[slot] == 0u, "an FS table slot other than the first is not zero"); + Require(records % 16u == 0u && AgcDriver::GuestMemory::Accessible(reinterpret_cast(records), 128u), "the FS table records are not readable"); + std::array table{}; + std::memcpy(table.data(), reinterpret_cast(records), sizeof(table)); + for (std::size_t record = 0; record < 8u; ++record) { + Require(table[record * 2u] != 0u && table[record * 2u] == table[2u] && table[record * 2u + 1u] == 0u, "an FS table record does not hold the routine and a zero"); + } + std::array routine{}; + Require(AgcDriver::GuestMemory::Accessible(reinterpret_cast(table[2]), sizeof(routine)), "the FS routine is not readable"); + std::memcpy(routine.data(), reinterpret_cast(table[2]), sizeof(routine)); + Require(routine[0] == 0xbe80200eu, "the FS routine does not start with s_setpc_b64 s[14:15]"); +} + +} + +int main() { +#ifdef ANYPS5_ENABLE_ASAN + std::puts("skipped: the shader calls through the FS table at the PS5's fixed 0xfe0040000, which an ANYPS5_ENABLE_ASAN build cannot map"); + return VulkanTestSkipped; +#endif + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + CheckTable(); + const auto failure = Dispatch(); + Require(failure.find("not statically resolvable") == std::string::npos, "the call through the FS table did not link: " + failure); + Require(failure.empty(), "the dispatch failed: " + failure); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(Output[lane] == Marker, "lane " + std::to_string(lane) + " is " + std::to_string(Output[lane]) + " after the call through the FS table, expected " + std::to_string(Marker)); + } + AgcDriverShutdown_nid_postfix(); + std::puts("compute FS table call tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception&) { + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ComputePipelineOutsideGpuMutex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputePipelineOutsideGpuMutex.cpp new file mode 100644 index 0000000000..dd464ab921 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputePipelineOutsideGpuMutex.cpp @@ -0,0 +1,123 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array ReaderCode{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{}; + alignas(256) std::array table{}; + alignas(256) std::array output{}; + alignas(256) std::array outputDescriptor{}; + alignas(256) volatile std::uint32_t done = 0; +}; + +std::array Slots; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Dispatch(std::vector& words, const void* code, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (const std::uint32_t offset : {0x207u, 0x208u, 0x209u}) SetShaderRegister(words, offset, 1); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); +} + +void WriteData(std::vector& words, volatile std::uint32_t* target, std::uint32_t value) { + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(target), High(target), value}); +} + +void Submit(std::uint32_t queue, Slot& slot, const void* code, std::uint32_t value) { + slot.value[0] = value; + slot.table = {Low(slot.value.data()), High(slot.value.data())}; + slot.outputDescriptor = {Low(slot.output.data()), High(slot.output.data()) & 0xffffu, static_cast(sizeof(slot.output)), 0x31016facu}; + std::vector words; + Dispatch(words, code, {Low(slot.table.data()), High(slot.table.data()), Low(slot.outputDescriptor.data()), High(slot.outputDescriptor.data())}); + WriteData(words, &slot.done, 1); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(queue, &packet) == 0, "compute submit failed"); +} + +void WaitDone(const Slot& slot, const char* what) { + const auto start = std::chrono::steady_clock::now(); + while (slot.done == 0) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(60), what); + std::this_thread::sleep_for(std::chrono::microseconds(200)); + } +} + +} + +int main() { + using AgcDriver::VulkanDevice; + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + Submit(0x28, Slots[0], ReaderCode.data(), 0x1234u); + WaitDone(Slots[0], "the first dispatch did not finish"); + Require(Slots[0].output[0] == 0x1234u, "the first dispatch stored " + std::to_string(Slots[0].output[0])); + + const auto before = VulkanDevice::ComputePipelineCounts(); + { + std::unique_lock gpuLock(AgcDriver::GuestMemory::GpuMutex()); + Submit(0x20, Slots[1], NewReaderCode.data(), 0x5678u); + const auto start = std::chrono::steady_clock::now(); + while (VulkanDevice::ComputePipelineCounts().created == before.created) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(60), "a dispatch of a new program did not create its pipeline while another thread held the GPU mutex"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + 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 && 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"; + AgcDriverWaitIdle_nid_postfix(); + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ComputeTableCalls.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeTableCalls.cpp new file mode 100644 index 0000000000..d9a6a8aa1d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeTableCalls.cpp @@ -0,0 +1,170 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "SceShaders.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Untouched = 0xcafebabeu; + +alignas(256) constexpr std::array Caller{ + 0x7e020280u, 0xf4080400u, 0xfa000000u, 0x8f148402u, 0xbf8cc07fu, 0xf4280208u, 0x28000000u, 0xbf8cc07fu, + 0xbe8e0308u, 0xbe8f0309u, 0xbf068103u, 0xbe8e210eu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u, + 0xbf9f0000u, 0x30306c73u, 0x00000040u, 0x00000008u}; +alignas(256) constexpr std::array HoistedCaller{ + 0x7e020280u, 0xf4080400u, 0xfa000000u, 0xbf8cc07fu, 0xbf068203u, 0xbf850001u, 0x7e020280u, 0x8f148402u, + 0xf4280208u, 0x28000000u, 0xbf8cc07fu, 0xbe8e0308u, 0xbe8f0309u, 0xbf068103u, 0xbe8e210eu, 0x34040082u, + 0xe0701000u, 0x80010102u, 0xbf810000u, 0xbf9f0000u, 0x30306c73u, 0x00000040u, 0x00000008u}; +alignas(256) constexpr std::array SelectSixteen{0x85159091u, 0x7e020215u, 0xbe80200eu}; +alignas(256) constexpr std::array SelectThirtyTwo{0x8515a0a1u, 0x7e020215u, 0xbe80200eu}; +alignas(16) std::array Table{}; +alignas(16) std::array Descriptors{}; +alignas(256) std::array Output{}; + +void Fill() { + const auto first = reinterpret_cast(SelectSixteen.data()); + const auto second = reinterpret_cast(SelectThirtyTwo.data()); + Table = {static_cast(first), static_cast(first >> 32u), 0u, 0u, static_cast(second), static_cast(second >> 32u), 0u, 0u}; + const auto table = reinterpret_cast(Table.data()); + Descriptors = {static_cast(table), static_cast((table >> 32u) & 0xffffu) | (16u << 16u), 2u, 0x31016facu}; +} + +std::array UserData(std::uint32_t index, bool scc) { + const auto descriptors = reinterpret_cast(Descriptors.data()); + const auto output = reinterpret_cast(Output.data()); + return {static_cast(descriptors), static_cast(descriptors >> 32u), index, scc ? 1u : 0u, + static_cast(output), static_cast((output >> 32u) & 0xffffu), static_cast(sizeof(Output)), 0x31016facu}; +} + +std::vector Commands(const std::array& data, bool wave32, const std::uint32_t* caller) { + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{Threads, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(caller); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{static_cast(data.size()) << 1u}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u | (wave32 ? 0x8000u : 0u)}); + return words; +} + +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + Require(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + _dup2(_fileno(file), _fileno(stderr)); +#else + saved = dup(fileno(stderr)); + dup2(fileno(file), fileno(stderr)); +#endif + } + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + +std::string Dispatch(std::uint32_t index, bool scc, bool wave32, const std::uint32_t* caller = Caller.data()) { + Output.fill(Untouched); + const auto words = Commands(UserData(index, scc), wave32, caller); + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + StderrCapture log; + std::string failure; + try { + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + } catch (const std::exception& error) { + failure = error.what(); + } + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); + return log.Finish() + failure; +} + +void Check(std::uint32_t index, bool scc, bool wave32, std::uint32_t expected, const std::string& what, const std::uint32_t* caller = Caller.data()) { + const auto log = Dispatch(index, scc, wave32, caller); + Require(log.find("no linked callee") == std::string::npos, what + ": a linked callee was reported missing: " + log); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(Output[lane] == expected, what + (wave32 ? " (wave32)" : " (wave64)") + ": lane " + std::to_string(lane) + " is " + std::to_string(Output[lane]) + ", expected " + std::to_string(expected)); + } +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + Fill(); + for (const bool wave32 : {false, true}) { + Check(0u, false, wave32, 16u, "the first record's callee with SCC 0"); + Check(0u, true, wave32, 17u, "the first record's callee with SCC 1"); + Check(1u, false, wave32, 32u, "the second record's callee with SCC 0"); + Check(1u, true, wave32, 33u, "the second record's callee with SCC 1"); + Check(0u, false, wave32, 16u, "the first record's callee through a descriptor loaded before a branch", HoistedCaller.data()); + Check(1u, true, wave32, 33u, "the second record's callee through a descriptor loaded before a branch", HoistedCaller.data()); + const auto log = Dispatch(2u, false, wave32); + Require(log.find("called address 0x0 at program counter") != std::string::npos && log.find("which no linked callee of that call matches") != std::string::npos, + std::string("a record past the table did not report an unlinked call") + (wave32 ? " (wave32)" : " (wave64)") + ": " + log); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(Output[lane] == Untouched, "a lane stored after the unlinked call: lane " + std::to_string(lane) + " is " + std::to_string(Output[lane])); + } + } + AgcDriverShutdown_nid_postfix(); + std::puts("compute table call tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception&) { + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ConstantDwordFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ConstantDwordFill.cpp new file mode 100644 index 0000000000..7a13cea9dc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ConstantDwordFill.cpp @@ -0,0 +1,161 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::MatchConstantDwordFill; + +alignas(256) constexpr std::array Code{0xd7460000u, 0x04010c04u, 0x7e020280u, 0xe0102000u, 0x80000100u, 0xbf810000u}; +alignas(256) constexpr std::array OneCode{0xd7460000u, 0x04010c04u, 0x7e020281u, 0xe0102000u, 0x80000100u, 0xbf810000u}; +alignas(256) constexpr std::array SixtyFourCode{0xd7460000u, 0x04010c04u, 0x7e0202c0u, 0xe0102000u, 0x80000100u, 0xbf810000u}; +alignas(256) constexpr std::array MinusOneCode{0xd7460000u, 0x04010c04u, 0x7e0202c1u, 0xe0102000u, 0x80000100u, 0xbf810000u}; +alignas(256) constexpr std::array MinusSixteenCode{0xd7460000u, 0x04010c04u, 0x7e0202d0u, 0xe0102000u, 0x80000100u, 0xbf810000u}; +alignas(256) constexpr std::array FloatHalfCode{0xd7460000u, 0x04010c04u, 0x7e0202f0u, 0xe0102000u, 0x80000100u, 0xbf810000u}; +alignas(256) constexpr std::array VgprCode{0xd7460000u, 0x04010c04u, 0x7e020300u, 0xe0102000u, 0x80000100u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +alignas(256) std::array Output{}; + +std::vector UserData(void* output, std::uint32_t records, std::uint32_t stride = 4, std::uint32_t format = 0x00014204u) { + const auto address = reinterpret_cast(output); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), records, format}; +} + +void MatchTests() { + const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1}; + const std::array packet{0xc0031500u, 4, 1, 1, 0x41u}; + const auto data = UserData(Output.data(), 1024); + const auto fill = MatchConstantDwordFill(packet, Code, data, compute); + Require(fill && fill->base == reinterpret_cast(Output.data()) && fill->bytes == 1024 && fill->pattern == std::array{}, "the zero dword fill kernel was not recognized as a zero fill"); + for (std::size_t word = 0; word < Code.size(); ++word) { + auto altered = Code; + altered[word] ^= 0x10000u; + Require(!MatchConstantDwordFill(packet, altered, data, compute), "a different shader matched the zero dword fill kernel"); + } + Require(!MatchConstantDwordFill(packet, std::span(Code).first(5), data, compute), "a truncated shader matched"); + Require(!MatchConstantDwordFill(packet, Code, UserData(Output.data(), 1024, 8), compute), "a strided descriptor matched a contiguous fill"); + Require(!MatchConstantDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x00814204u), compute), "an add-tid descriptor matched"); + Require(!MatchConstantDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x30014204u), compute), "a raw out-of-bounds mode matched"); + Require(!MatchConstantDwordFill(packet, Code, UserData(Output.data() + 1, 1024), compute), "an unaligned destination matched"); + Require(!MatchConstantDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x00011204u), compute), "an 8-bit output format matched"); + Require(!MatchConstantDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x00017204u), compute), "an output format above 32_FLOAT matched"); + Require(MatchConstantDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x00015204u), compute).has_value(), "32_SINT output did not match"); + Require(MatchConstantDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x00016204u), compute).has_value(), "32_FLOAT output did not match"); + Require(!MatchConstantDwordFill(packet, Code, UserData(Output.data(), 102), compute), "a record count not filling 16-byte units matched"); + auto extra = data; + extra.push_back(0); + Require(!MatchConstantDwordFill(packet, Code, extra, compute), "a different user SGPR count matched (the group id moves)"); + auto partial = packet; + partial[4] |= 0x20u; + Require(!MatchConstantDwordFill(partial, Code, data, compute), "a partial dispatch matched"); + auto noGroup = compute; + noGroup.groupIdEnable[0] = false; + Require(!MatchConstantDwordFill(packet, Code, data, noGroup), "a dispatch without the group id matched"); + const auto clamped = MatchConstantDwordFill(packet, Code, UserData(Output.data(), 64), compute); + Require(clamped && clamped->bytes == 256, "the fill exceeded the descriptor's records"); + struct Constant { std::span code; std::uint32_t value; }; + for (const auto& item : {Constant{OneCode, 1u}, Constant{SixtyFourCode, 64u}, Constant{MinusOneCode, 0xffffffffu}, Constant{MinusSixteenCode, 0xfffffff0u}}) { + const auto constant = MatchConstantDwordFill(packet, item.code, data, compute); + Require(constant && constant->bytes == 1024 && constant->pattern == std::array{item.value, item.value, item.value, item.value}, "an inline constant dword fill did not match with its value"); + } + Require(!MatchConstantDwordFill(packet, FloatHalfCode, data, compute), "a float inline constant matched"); + Require(!MatchConstantDwordFill(packet, VgprCode, data, compute), "a fill of a vector register matched"); +} + +std::vector Commands(std::span code, const std::vector& data, std::uint32_t groups, bool partial, std::uint32_t threads) { + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{64, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (1u << 7u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, partial ? threads : groups, 1, 1, 0x41u | (partial ? 0x20u : 0u)}); + return words; +} + +void ExecuteTests() { + struct Case { + std::uint32_t groups, records, stride; + bool partial; + std::uint32_t threads; + }; + constexpr std::array cases{{ + {4, 1024, 4, false, 0}, + {16, 1024, 4, false, 0}, + {4, 64, 4, false, 0}, + {4, 100, 4, false, 0}, + {4, 102, 4, false, 0}, + {2, 1024, 8, false, 0}, + {0, 1024, 4, true, 70}, + {0, 1024, 4, false, 0}, + }}; + for (const auto& item : cases) { + Output.fill(Untouched); + const auto words = Commands(Code, UserData(Output.data(), item.records, item.stride), item.groups, item.partial, item.threads); + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); + const auto launched = item.partial ? item.threads : item.groups * 64u; + const auto filled = std::min(launched, item.records); + const auto step = item.stride / 4u; + for (std::uint32_t i = 0; i < Output.size(); ++i) { + const bool written = i % step == 0 && i / step < filled; + const auto expected = written ? 0u : Untouched; + Require(Output[i] == expected, "zero dword fill mismatch at " + std::to_string(i) + " (groups " + std::to_string(item.groups) + ", records " + std::to_string(item.records) + ", stride " + std::to_string(item.stride) + "): got " + std::to_string(Output[i]) + ", expected " + std::to_string(expected)); + } + } + struct ConstantCase { std::span code; std::uint32_t value, groups, records; }; + for (const auto& item : {ConstantCase{OneCode, 1u, 4, 256}, ConstantCase{SixtyFourCode, 64u, 2, 1000}, ConstantCase{MinusOneCode, 0xffffffffu, 4, 255}, ConstantCase{MinusSixteenCode, 0xfffffff0u, 4, 256}}) { + Output.fill(Untouched); + const auto words = Commands(item.code, UserData(Output.data(), item.records), item.groups, false, 0); + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); + const auto filled = std::min(item.groups * 64u, item.records); + for (std::uint32_t i = 0; i < Output.size(); ++i) { + const auto expected = i < filled ? item.value : Untouched; + Require(Output[i] == expected, "inline constant dword fill mismatch at " + std::to_string(i) + " (value " + std::to_string(item.value) + "): got " + std::to_string(Output[i]) + ", expected " + std::to_string(expected)); + } + } +} + +} + +int main() { + try { + if (std::getenv("APS5_NO_FILL_HLE") == nullptr) MatchTests(); + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ExecuteTests(); + AgcDriverShutdown_nid_postfix(); + std::puts("constant dword fill tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/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..f3c8208133 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthClearHle.cpp @@ -0,0 +1,571 @@ +#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/Recorder.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; +} + +Graphics::Recorder* testRecorder = nullptr; +Graphics::Recorder* driverRecorder = nullptr; + +void UseTestDevice(AgcDriver::VulkanDevice& device) { + testRecorder = AgcDriver::VulkanDeviceTestAccess::ContextOf(device).recorder; + Require(testRecorder != nullptr, "depth clear: the test device has no recorder"); + testRecorder->Activate(); +} + +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, {}}; + if (driverRecorder != nullptr) driverRecorder->Activate(); + Require(sceAgcDriverSubmitDcb(&dcb) == 0, "depth clear: DCB submission failed"); + AgcDriverWaitIdle_nid_postfix(); + if (driverRecorder == nullptr) { + driverRecorder = Graphics::Recorder::Active(); + Require(driverRecorder != nullptr && driverRecorder != testRecorder, "depth clear: the driver's device did not activate its own recorder"); + } +} + +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) { + UseTestDevice(device); + 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) { + UseTestDevice(device); + 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) { + UseTestDevice(device); + 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) { + UseTestDevice(device); + 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) { + UseTestDevice(device); + 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)"); +} + +void PlaneFillCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, int mode) { + UseTestDevice(device); + Graphics::SetDepthPlaneClearForTest(mode != 0); + const auto before = Graphics::DepthClearCounts(); + const std::string label = "plane fill, 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 planeBytes = static_cast(Graphics::DepthSliceBytes({Width, Height}, 4u)); + 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"); + Require(drawLess() == 0u, label + "the plane does not hold the 0.5 the producer wrote"); + + Graphics::NoteDepthMetadataFill(plane.Address(), planeBytes, std::bit_cast(0.125f)); + Require(drawGreater() == Covered, label + "a plane fill of 0.125 did not clear the depth"); + Graphics::NoteDepthMetadataFill(plane.Address(), planeBytes, std::bit_cast(1.0f)); + SetColor(color, 0.25f); + RunClear(device, code, plane, color, Format32Float, 8, 4); + Require(drawLess() == 0u, label + "a clear to 0.25 after a plane fill of 1.0 left the fill in the depth"); + Require(drawGreater() == Covered, label + "a clear to 0.25 after a plane fill of 1.0 did not leave 0.25 everywhere"); + + SetColor(color, 1.0f); + RunClear(device, code, plane, color, Format32Float, 8, 4); + Graphics::NoteDepthMetadataFill(plane.Address(), planeBytes, std::bit_cast(0.125f)); + Require(drawLess() == 0u, label + "a plane fill of 0.125 after a clear to 1.0 left the clear in the depth"); + Require(drawGreater() == Covered, label + "a plane fill of 0.125 after a clear to 1.0 did not leave 0.125 everywhere"); + 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 == 2u, label + "the lever took " + std::to_string(difference.clears) + " clears, expected 2"); +} + +} + +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 ? "plane fill, switch off" : "plane fill, lever", [&] { PlaneFillCase(*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/DepthFastClear.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClear.cpp index 7d119f12f3..181a10cebc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClear.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClear.cpp @@ -17,7 +17,7 @@ using DepthFastClearHarness::Surface; using DepthFastClearHarness::Width; alignas(4096) std::array Depth{}; -alignas(256) std::array Htile{}; +alignas(256) std::array Htile{}; alignas(16) std::array FillValue{}; constexpr std::array MemoryValueFillKernel{0xd7460000u, 0x04010c08u, 0xf4201a82u, 0xfa000000u, 0xbf8cc07fu, 0x7e02026au, 0xe0102000u, 0x80000100u, 0xbf810000u}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp index 1e4e5bcb3d..e1d74d89e9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp @@ -39,6 +39,9 @@ struct Surface { std::uint64_t htile; bool htileStencil; VkFormat format; + std::uint32_t slice = 0; + std::uint8_t clearStencil = 0; + std::uint32_t swizzle = 0; }; struct DrawOptions { @@ -47,6 +50,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) { @@ -67,11 +72,11 @@ struct Shaders { std::uint32_t vertexPush = 0; }; -inline Shaders Compile(AgcDriver::VulkanDevice& device) { +inline Shaders Compile(AgcDriver::VulkanDevice& device, const void* vertices = Triangles().data()) { const auto target = device.Target(); Shaders shaders; std::vector vertexUserData(4, 0u); - const auto vertexBuffer = BufferDescriptor(Triangles().data(), 16u, static_cast(Triangles().size())); + const auto vertexBuffer = BufferDescriptor(vertices, 16u, static_cast(Triangles().size())); std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; ShaderRecompiler::RecompileRequest vertex{ @@ -104,29 +109,31 @@ inline Shaders Compile(AgcDriver::VulkanDevice& device) { return shaders; } -inline std::uint32_t Draw(AgcDriver::VulkanDevice& device, const Shaders& compiled, const Surface& surface, const DrawOptions& options) { - Pixels.fill(std::byte{0}); +inline std::uint32_t Draw(AgcDriver::VulkanDevice& device, const Shaders& compiled, const Surface& surface, const DrawOptions& options, VkExtent2D extent = {Width, Height}, std::span pixels = Pixels) { + std::fill(pixels.begin(), pixels.end(), std::byte{0}); const std::array shaders{{ {ShaderStage::Vertex, &compiled.vertex, 0}, {ShaderStage::Fragment, &compiled.pixel, PixelPushOffset(compiled.vertexPush, device.Target())} }}; AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; - state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.color = {reinterpret_cast(pixels.data()), extent, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; state.colors = {state.color}; state.hasColorTarget = true; - state.depth = AgcDriver::Graphics::DepthTarget{surface.depth, surface.stencil, {Width, Height}, surface.format, 1.0f, 0, surface.htile, surface.htileStencil}; + state.depth = AgcDriver::Graphics::DepthTarget{surface.depth, surface.stencil, extent, surface.format, 1.0f, surface.clearStencil, surface.htile, surface.htileStencil}; + state.depth->firstSlice = surface.slice; + state.depth->depthSwizzle = surface.swizzle; state.depthTest = options.depthTest; state.depthWrite = options.depthWrite; state.depthCompare = options.depthCompare; state.stencilTest = options.stencilTest; state.stencilFront = options.stencil; state.stencilBack = options.stencil; - state.renderExtent = {Width, Height}; + state.renderExtent = extent; 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(extent.height), static_cast(extent.width), -static_cast(extent.height), options.minDepth, options.maxDepth}; state.negativeOneToOne = false; - state.scissor = {{0, 0}, {Width, Height}}; + state.scissor = {{0, 0}, extent}; state.cullMode = VK_CULL_MODE_NONE; state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; state.blend.colorWriteMask = 15; @@ -136,8 +143,8 @@ inline std::uint32_t Draw(AgcDriver::VulkanDevice& device, const Shaders& compil device.Draw(state, draw, shaders); device.WaitIdle(); std::uint32_t covered = 0; - for (std::size_t pixel = 0; pixel < Pixels.size() / 4u; ++pixel) { - if (std::to_integer(Pixels[pixel * 4u]) == 255u) ++covered; + for (std::size_t pixel = 0; pixel < pixels.size() / 4u; ++pixel) { + if (std::to_integer(pixels[pixel * 4u]) == 255u) ++covered; } return covered; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp index 0ecf904715..f535dcfc10 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/Shutdown.hpp" @@ -11,6 +12,7 @@ #endif #include #endif +#include #include #include #include @@ -20,6 +22,7 @@ #include #include #include +#include #include namespace { @@ -113,6 +116,8 @@ void DispatchFill(const GuestBlock& code, const GuestBlock& htile, std::uint32_t }); } +constexpr std::array UnknownFillKernel{0xbf800000u, 0xd7460004u, 0x04010c08u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0xe01c2000u, 0x80000004u, 0xbf810000u}; + bool StoresFills(AgcDriver::VulkanDevice& device, const GuestBlock& probe) { std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); std::array pattern{}; @@ -162,6 +167,108 @@ void DispatchBufferPatternFillClearsDepth(AgcDriver::VulkanDevice& device, const Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, "a compute fill with the buffer pattern 0 over the HTILE did not clear the depth to DB_DEPTH_CLEAR (1.0)"); } +void DrawAfterFastClearExpandsHtile(AgcDriver::VulkanDevice& device, const Shaders& shaders) { + for (const bool stencil : {false, true}) { + auto& htile = NewBlock(); + const Surface surface{NewBlock().Address(), stencil ? NewBlock().Address() : 0, htile.Address(), stencil, stencil ? VK_FORMAT_D32_SFLOAT_S8_UINT : VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, "a new depth surface was not cleared to DB_DEPTH_CLEAR (1.0)"); + DmaFill(htile, HtileFillBytes, stencil ? 0xf0u : 0u); + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, "a fast clear through the HTILE did not clear the depth to DB_DEPTH_CLEAR (1.0)"); + std::vector words(HtileFillBytes / 4u); + AgcDriver::GuestMemory::Read(htile.Address(), std::as_writable_bytes(std::span(words)), 4); + const auto expanded = stencil ? 0xfffff3ffu : 0xfffc000fu; + for (const auto word : words) Require(word == expanded, stencil ? "a draw after a depth and stencil fast clear left HTILE words other than expanded (0xfffff3ff)" : "a draw after a depth fast clear left HTILE words other than expanded (0xfffc000f): a kernel decoding the depth through ZMask 0 reads the clear value, not the drawn depth"); + Require(Draw(device, shaders, surface, {}) == 0u, "the depth drawn after the fast clear was cleared again"); + } +} + +void UnknownKernelClearsTwice(AgcDriver::VulkanDevice& device, const Shaders& shaders) { + const auto& htile = NewBlock(); + auto& code = NewBlock(); + std::memcpy(code.Data(), UnknownFillKernel.data(), sizeof(UnknownFillKernel)); + const Surface surface{NewBlock().Address(), 0, htile.Address(), false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, "a new depth surface was not cleared to DB_DEPTH_CLEAR (1.0)"); + for (const char* frame : {"first", "second"}) { + DispatchFill(code, htile, HtileFillBytes); + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, std::string("the ") + frame + " clear an unrecognized kernel wrote into the HTILE was not taken"); + Require(Draw(device, shaders, surface, {}) == 0u, std::string("the depth written after the ") + frame + " kernel clear was lost"); + } +} + +std::uint32_t* WordsAt(GuestBlock& block, std::size_t offset) { return reinterpret_cast(block.Data() + offset); } + +bool AllWords(const std::uint32_t* words, std::uint32_t value) { return std::all_of(words, words + HtileFillBytes / 4u, [&](std::uint32_t word) { return word == value; }); } + +void ExpandedTileKeepsSlice(AgcDriver::VulkanDevice& device, const Shaders& shaders) { + auto& htile = NewBlock(); + const Surface first{NewBlock().Address(), 0, htile.Address(), false, VK_FORMAT_D32_SFLOAT, 0}; + const Surface second{NewBlock().Address(), 0, htile.Address() + HtileFillBytes, false, VK_FORMAT_D32_SFLOAT, 1}; + Require(Draw(device, shaders, first, {.depthWrite = true}) == Covered, "slice 0 was not cleared to DB_DEPTH_CLEAR (1.0)"); + Require(Draw(device, shaders, second, {.depthWrite = true}) == Covered, "slice 1 was not cleared to DB_DEPTH_CLEAR (1.0)"); + auto* sliceOne = WordsAt(htile, HtileFillBytes); + std::fill(sliceOne, sliceOne + HtileFillBytes / 4u, 0u); + sliceOne[0] = 0xffffffffu; + AgcDriver::GuestMemory::MarkWritten(htile.Address() + HtileFillBytes, HtileFillBytes); + Require(Draw(device, shaders, second, {}) == 0u, "slice 1 took a clear from HTILE words with one tile left expanded"); + Require(sliceOne[0] == 0xffffffffu && std::all_of(sliceOne + 1, sliceOne + HtileFillBytes / 4u, [](std::uint32_t word) { return word == 0u; }), "a bind of slice 1 stored back HTILE words with one tile left expanded"); + Require(AllWords(WordsAt(htile, 0), 0xfffc000fu), "a bind of slice 1 changed slice 0's HTILE"); +} + +void UniformSliceDecodes(AgcDriver::VulkanDevice& device, const Shaders& shaders) { + auto& htile = NewBlock(); + const Surface first{NewBlock().Address(), 0, htile.Address(), false, VK_FORMAT_D32_SFLOAT, 0}; + const Surface second{NewBlock().Address(), 0, htile.Address() + HtileFillBytes, false, VK_FORMAT_D32_SFLOAT, 1}; + Require(Draw(device, shaders, first, {.depthWrite = true}) == Covered, "slice 0 was not cleared to DB_DEPTH_CLEAR (1.0)"); + Require(Draw(device, shaders, second, {.depthWrite = true}) == Covered, "slice 1 was not cleared to DB_DEPTH_CLEAR (1.0)"); + Require(Draw(device, shaders, second, {}) == 0u, "the depth drawn into slice 1 was lost"); + auto* sliceOne = WordsAt(htile, HtileFillBytes); + std::fill(sliceOne, sliceOne + HtileFillBytes / 4u, 0u); + AgcDriver::GuestMemory::MarkWritten(htile.Address() + HtileFillBytes, HtileFillBytes); + Require(Draw(device, shaders, second, {.depthWrite = true}) == Covered, "slice 1 did not take the clear from HTILE words that are all equal"); + Require(AllWords(sliceOne, 0xfffc000fu), "slice 1's bind did not store its cleared HTILE back as expanded"); + Require(Draw(device, shaders, second, {}) == 0u, "the depth drawn into slice 1 after the clear was lost"); +} + +void MixedClearedSliceClears(AgcDriver::VulkanDevice& device, const Shaders& shaders) { + auto& htile = NewBlock(); + const Surface first{NewBlock().Address(), 0, htile.Address(), false, VK_FORMAT_D32_SFLOAT, 0}; + const Surface second{NewBlock().Address(), 0, htile.Address() + HtileFillBytes, false, VK_FORMAT_D32_SFLOAT, 1}; + Require(Draw(device, shaders, first, {.depthWrite = true}) == Covered, "slice 0 was not cleared to DB_DEPTH_CLEAR (1.0)"); + Require(Draw(device, shaders, second, {.depthWrite = true}) == Covered, "slice 1 was not cleared to DB_DEPTH_CLEAR (1.0)"); + Require(Draw(device, shaders, second, {}) == 0u, "the depth drawn into slice 1 was lost"); + auto* sliceOne = WordsAt(htile, HtileFillBytes); + for (std::uint32_t word = 0; word < HtileFillBytes / 4u; ++word) sliceOne[word] = (word / 7u) % 2u == 0u ? 0u : 0xfffffff0u; + AgcDriver::GuestMemory::MarkWritten(htile.Address() + HtileFillBytes, HtileFillBytes); + Require(Draw(device, shaders, second, {.depthWrite = true}) == Covered, "slice 1 did not take the clear from HTILE words that all have ZMask 0 but different Min/Max Z"); + Require(AllWords(sliceOne, 0xfffc000fu), "slice 1's bind did not store its cleared HTILE back as expanded"); + Require(AllWords(WordsAt(htile, 0), 0xfffc000fu), "a bind of slice 1 changed slice 0's HTILE"); + Require(Draw(device, shaders, second, {}) == 0u, "the depth drawn into slice 1 after the clear was lost"); +} + +void RecordedFillClearsWithoutWait(AgcDriver::VulkanDevice& device, const Shaders& shaders) { + const VkStencilOpState writeFive{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 5u}; + const VkStencilOpState equalZero{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, 0u}; + for (const bool stencil : {false, true}) { + auto& htile = NewBlock(); + const Surface surface{NewBlock().Address(), stencil ? NewBlock().Address() : 0, htile.Address(), stencil, stencil ? VK_FORMAT_D32_SFLOAT_S8_UINT : VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, surface, {.depthWrite = true, .stencilTest = stencil, .stencil = writeFive}) == Covered, "a new depth surface was not cleared to DB_DEPTH_CLEAR (1.0)"); + Require(Draw(device, shaders, surface, {}) == 0u, "a draw at depth 0.5 passed LESS against the 0.5 written before it"); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto word = stencil ? 0xf0u : 0u; + const std::array pattern{word, word, word, word}; + Require(device.FillBuffer(htile.Address(), HtileFillBytes, pattern), stencil ? "the depth and stencil fast clear fill (SMem 0) over the HTILE was not recorded on the GPU" : "the fill of 0 over the HTILE was not recorded on the GPU"); + } + const auto waits = AgcDriver::Graphics::Recorder::ThreadHookWaits(); + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, stencil ? "a depth and stencil fast clear fill recorded over the HTILE did not clear the depth to DB_DEPTH_CLEAR (1.0)" : "a fill of 0 recorded over the HTILE did not clear the depth to DB_DEPTH_CLEAR (1.0)"); + Require(AgcDriver::Graphics::Recorder::ThreadHookWaits() == waits, "the bind of the depth target waited for the GPU to run the fill recorded over its HTILE"); + const auto expanded = stencil ? 0xfffff3ffu : 0xfffc000fu; + Require(AllWords(WordsAt(htile, 0), expanded), stencil ? "the bind after a depth and stencil fast clear fill recorded over the HTILE did not store it back as expanded (0xfffff3ff)" : "the bind after a fill recorded over the HTILE did not store it back as expanded (0xfffc000f)"); + if (stencil) Require(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = equalZero}) == Covered, "a depth and stencil fast clear fill recorded over the HTILE did not clear the stencil to 0"); + Require(Draw(device, shaders, surface, {}) == 0u, "the depth drawn after the clear from the recorded fill was cleared again"); + } +} + void DepthOnlyFillKeepsStencil(AgcDriver::VulkanDevice& device, const Shaders& shaders) { const auto& htile = NewBlock(); const Surface surface{NewBlock().Address(), NewBlock().Address(), htile.Address(), true, VK_FORMAT_D32_SFLOAT_S8_UINT}; @@ -185,10 +292,16 @@ int main() { const auto shaders = Compile(*device); DmaFillClearsDepth(*device, shaders); DepthOnlyFillKeepsStencil(*device, shaders); + DrawAfterFastClearExpandsHtile(*device, shaders); const bool computeRan = StoresFills(*device, NewBlock()); + ExpandedTileKeepsSlice(*device, shaders); + UniformSliceDecodes(*device, shaders); + MixedClearedSliceClears(*device, shaders); if (computeRan) { + RecordedFillClearsWithoutWait(*device, shaders); DispatchFillClearsDepth(*device, shaders); DispatchBufferPatternFillClearsDepth(*device, shaders); + UnknownKernelClearsTwice(*device, shaders); } else std::printf("compute fill not run: the device does not store fills into imported guest memory\n"); LibcRunShutdown_nid_postfix(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthHtileWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthHtileWrite.cpp new file mode 100644 index 0000000000..965645083f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthHtileWrite.cpp @@ -0,0 +1,119 @@ +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include +#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 Tiles = HtileBytes / 4u; +constexpr VkExtent2D InitialExtent{1024, 1024}; +constexpr std::size_t InitialPixels = static_cast(InitialExtent.width) * InitialExtent.height; +constexpr std::size_t InitialHtileBytes = 2u * 32768u; +constexpr std::size_t PlaneBytes = 64u * 1024u; + +alignas(4096) std::array InitialDepth{}; +alignas(4096) std::array InitialStencil{}; +alignas(4096) std::array InitialColor{}; +alignas(256) std::array InitialHtile{}; + + +alignas(4096) std::array Depth{}; +alignas(4096) std::array StencilDepth{}; +alignas(4096) std::array Stencil{}; +alignas(256) std::array Htile{}; + +void Fill(std::uint32_t word, std::size_t count = Tiles) { + std::fill_n(Htile.begin(), count, word); +} + +bool Holds(std::uint32_t word) { + return std::all_of(Htile.begin(), Htile.begin() + Tiles, [&](std::uint32_t value) { return value == word; }); +} + +DrawOptions StencilDraw(VkCompareOp compare, std::uint32_t reference, VkStencilOp op) { + DrawOptions options; + options.depthTest = false; + options.stencilTest = true; + options.stencil = {op, op, op, compare, 0xffu, op == VK_STENCIL_OP_KEEP ? 0u : 0xffu, reference}; + return options; +} + +} + +int main() { + try { + Require(AgcDriver::Graphics::DepthSliceBytes({Width, Height}, sizeof(float)) <= sizeof(Depth) && + AgcDriver::Graphics::DepthSliceBytes({Width, Height}, sizeof(float)) <= sizeof(StencilDepth) && + AgcDriver::Graphics::DepthSliceBytes({Width, Height}, sizeof(std::uint8_t)) <= sizeof(Stencil), + "depth fixtures do not cover their tiled footprints"); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto shaders = Compile(*device); + + Fill(0xfffc000fu); + const Surface depth{reinterpret_cast(Depth.data()), 0, reinterpret_cast(Htile.data()), false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(*device, shaders, depth, {.depthWrite = true}) == Covered, "a new depth surface was not cleared to DB_DEPTH_CLEAR (1.0)"); + Require(Draw(*device, shaders, depth, {}) == 0u, "a draw at depth 0.5 passed LESS against the 0.5 written before it"); + Fill(0u, Tiles - 1); + Require(Draw(*device, shaders, depth, {}) == 0u, "an HTILE with one tile left expanded cleared the depth"); + Fill(0u); + Require(Draw(*device, shaders, depth, {.depthWrite = true}) == Covered, "an HTILE written with ZMask 0 on every tile did not clear the depth to DB_DEPTH_CLEAR (1.0)"); + Require(Holds(0xfffc000fu), "the HTILE of the cleared depth was not stored back as expanded"); + Require(Draw(*device, shaders, depth, {}) == 0u, "the depth written after the clear was cleared again"); + + Fill(0xfffff3ffu); + const Surface stencil{reinterpret_cast(StencilDepth.data()), reinterpret_cast(Stencil.data()), reinterpret_cast(Htile.data()), true, VK_FORMAT_D32_SFLOAT_S8_UINT}; + Require(Draw(*device, shaders, stencil, StencilDraw(VK_COMPARE_OP_ALWAYS, 5u, VK_STENCIL_OP_REPLACE)) == Covered, "a stencil write of 5 did not pass ALWAYS"); + Require(Draw(*device, shaders, stencil, StencilDraw(VK_COMPARE_OP_EQUAL, 5u, VK_STENCIL_OP_KEEP)) == Covered, "the stencil plane does not hold the 5 written before"); + Fill(0xfffff00fu); + Require(Draw(*device, shaders, stencil, StencilDraw(VK_COMPARE_OP_EQUAL, 0u, VK_STENCIL_OP_KEEP)) == Covered, "an HTILE written with SMem 0 on every tile did not clear the stencil to DB_STENCIL_CLEAR (0)"); + Require(Holds(0xfffff3ffu), "the HTILE of the cleared stencil was not stored back as expanded"); + Require(Draw(*device, shaders, stencil, StencilDraw(VK_COMPARE_OP_ALWAYS, 9u, VK_STENCIL_OP_REPLACE)) == Covered, "a stencil write of 9 did not pass ALWAYS"); + Require(Draw(*device, shaders, stencil, StencilDraw(VK_COMPARE_OP_EQUAL, 9u, VK_STENCIL_OP_KEEP)) == Covered, "the stencil written after the clear was cleared again"); + const Surface depthOnlyStencil{stencil.depth, stencil.stencil, stencil.htile, false, stencil.format}; + Fill(0xfffffff0u); + Require(Draw(*device, shaders, depthOnlyStencil, StencilDraw(VK_COMPARE_OP_EQUAL, 9u, VK_STENCIL_OP_KEEP)) == Covered, "a depth-only HTILE clear changed the stencil plane"); + + Fill(0xfffffff0u); + const Surface inheritedStencil{reinterpret_cast(InitialDepth.data()), stencil.stencil, stencil.htile, false, stencil.format, 0, 1}; + auto inheritedStencilDraw = StencilDraw(VK_COMPARE_OP_EQUAL, 9u, VK_STENCIL_OP_KEEP); + inheritedStencilDraw.depthTest = true; + Require(Draw(*device, shaders, inheritedStencil, inheritedStencilDraw) == Covered, "a fresh depth-only HTILE clear lost its initial depth or inherited stencil data"); + + Fill(0xfffff3ffu); + Require(Draw(*device, shaders, stencil, {.depthWrite = true}) == Covered, "the inherited-depth fixture did not write depth 0.5"); + Fill(0u); + const Surface inherited{stencil.depth, reinterpret_cast(InitialStencil.data()), stencil.htile, true, stencil.format}; + Require(Draw(*device, shaders, inherited, {}) == Covered, "a new attachment inheriting depth did not take its HTILE clear"); + + Require(AgcDriver::Graphics::HtileMetadataBytes(InitialExtent) == InitialHtileBytes, "the initial-clear fixture has the wrong metadata size"); + std::fill(InitialHtile.begin(), InitialHtile.end(), 0xfffffff0u); + const Surface initial{reinterpret_cast(InitialDepth.data()), reinterpret_cast(InitialStencil.data()), reinterpret_cast(InitialHtile.data()), false, VK_FORMAT_D32_SFLOAT_S8_UINT, 0, 1}; + Require(Draw(*device, shaders, initial, StencilDraw(VK_COMPARE_OP_EQUAL, 1u, VK_STENCIL_OP_KEEP), InitialExtent, InitialColor) == InitialPixels, "a depth-only HTILE clear discarded the initial stencil clear"); + Require(std::all_of(InitialHtile.begin(), InitialHtile.end(), [](std::uint32_t word) { return word == 0xfffc000fu; }), "the initial HTILE was not stored back as expanded"); + Require(Draw(*device, shaders, initial, {.depthWrite = true}, InitialExtent, InitialColor) == InitialPixels, "the reused initial-clear fixture did not write depth 0.5"); + Require(Draw(*device, shaders, initial, StencilDraw(VK_COMPARE_OP_ALWAYS, 9u, VK_STENCIL_OP_REPLACE), InitialExtent, InitialColor) == InitialPixels, "the reused initial-clear fixture did not write stencil 9"); + std::fill(InitialHtile.begin(), InitialHtile.end(), 0xfffffff0u); + auto reused = StencilDraw(VK_COMPARE_OP_EQUAL, 9u, VK_STENCIL_OP_KEEP); + reused.depthTest = true; + Require(Draw(*device, shaders, initial, reused, InitialExtent, InitialColor) == InitialPixels, "a reused attachment lost its depth-only clear or stencil plane"); + std::puts("depth HTILE write 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/DepthPlaneBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneBufferMemory.cpp new file mode 100644 index 0000000000..fcaadb3722 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneBufferMemory.cpp @@ -0,0 +1,238 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using DepthFastClearHarness::Compile; +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::HtileBytes; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Surface; +using DepthFastClearHarness::Width; + +constexpr std::size_t Block = 65536; +alignas(256) std::array Htile{}; +alignas(256) std::array ViewedHtile{}; +alignas(256) constexpr std::array LoadCode{ + 0x7e02020du, 0x7e04020cu, 0xf0001108u, 0x00010300u, 0xbf8c3f70u, 0x34080082u, 0xe0701000u, 0x80000304u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +constexpr std::uint32_t LoadThreads = 32; +constexpr std::size_t ColorBytes = static_cast(Width) * Height * 4u; + +void* AllocateWatched(std::size_t bytes) { +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the depth plane block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + return block; +} + +std::array LoadRow(AgcDriver::VulkanDevice& device, std::uint64_t address, std::uint32_t width, std::uint32_t height, std::uint32_t swizzle) { + alignas(256) static std::array output{}; + std::vector userData(16, 0u); + const auto out = reinterpret_cast(output.data()); + const std::array buffer{static_cast(out), static_cast((out >> 32u) & 0xffffu), static_cast(sizeof(output)), 0x31016facu}; + const std::array texture{static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (22u << 20u) | (((width - 1u) & 3u) << 30u), ((width - 1u) >> 2u) | ((height - 1u) << 14u), 0xfacu | (swizzle << 20u) | (9u << 28u), 0u, 0u, 0u, 0u}; + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::span code(LoadCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{LoadThreads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + output.fill(0xdeadbeefu); + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + return output; +} + +bool RowHolds(const std::array& row, float depth) { + return std::all_of(row.begin(), row.end(), [&](std::uint32_t bits) { return bits == std::bit_cast(depth); }); +} + +std::size_t Holding(std::span plane, float depth) { + return static_cast(std::count(plane.begin(), plane.end(), depth)); +} + +void DrawBlendKeepingMemory(AgcDriver::VulkanDevice& device, const DepthFastClearHarness::Shaders& compiled, std::uint64_t address) { + const std::array shaders{{ + {ShaderRecompiler::ShaderStage::Vertex, &compiled.vertex, 0}, + {ShaderRecompiler::ShaderStage::Fragment, &compiled.pixel, PixelPushOffset(compiled.vertexPush, device.Target())} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {address, {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, ColorBytes, 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0.0f, 1.0f}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend = {VK_TRUE, VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, 15}; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(DepthFastClearHarness::Triangles().size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto planeBytes = static_cast(AgcDriver::Graphics::DepthSliceBytes({Width, Height}, 4u)); + Require(planeBytes <= Block, "the depth plane does not fit one block"); + auto* block = static_cast(AllocateWatched(Block)); + const std::span plane(block, planeBytes / sizeof(float)); + const auto address = reinterpret_cast(block); + const auto shaders = Compile(*device); + const auto reader = Compile(*device, block); + const Surface depth{address, 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, AgcDriver::Graphics::XorSwizzleMode(AgcDriver::Graphics::TextureTileMode::kZ64KBX)}; + + Require(Draw(*device, shaders, depth, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 did not cover the target"); + Draw(*device, reader, depth, {.depthTest = false}); + Require(Holding(plane, 0.5f) == static_cast(Width) * Height, "a vertex fetch from the depth plane did not see the depth 0.5 a draw wrote into it"); + + if (!AgcDriver::GuestMemory::WriteWatched() || !AgcDriver::GuestMemory::Watched(address, Block)) { + std::puts("guest memory is not write-watched here: a buffer write into the depth plane is not exercised"); + std::puts("depth plane buffer memory tests passed"); + return 0; + } + std::fill(plane.begin(), plane.end(), 0.75f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(Draw(*device, shaders, depth, {}) == Covered, "a draw at depth 0.5 failed LESS against the 0.75 written into the depth plane's memory"); + Require(Draw(*device, shaders, depth, {.depthWrite = true}) == Covered, "a draw at depth 0.5 writing depth failed LESS against 0.75"); + Draw(*device, reader, depth, {.depthTest = false}); + Require(Holding(plane, 0.5f) == static_cast(Width) * Height, "a vertex fetch from the depth plane did not see the depth 0.5 written after the plane was reloaded"); + + auto* fresh = static_cast(AllocateWatched(Block)); + const std::span freshPlane(fresh, planeBytes / sizeof(float)); + const auto freshReader = Compile(*device, fresh); + const Surface unstored{reinterpret_cast(fresh), 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + Require(Draw(*device, shaders, unstored, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 into a new plane did not cover the target"); + std::fill(freshPlane.begin(), freshPlane.end(), 0.75f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Draw(*device, freshReader, depth, {.depthTest = false}); + Require(Holding(freshPlane, 0.75f) == freshPlane.size(), "the depth 0.5 a draw wrote before a guest write into the plane was stored over that write"); + Require(Draw(*device, shaders, unstored, {}) == Covered, "a draw at depth 0.5 failed LESS against the 0.75 written over depth the surface had not stored"); + std::fill(freshPlane.begin(), freshPlane.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(Draw(*device, shaders, unstored, {}) == 0u, "a draw at depth 0.5 passed LESS against the 0.25 written into the plane before it"); + Require(Draw(*device, shaders, unstored, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 over the reloaded plane did not cover the target"); + Draw(*device, freshReader, depth, {.depthTest = false}); + Require(Holding(freshPlane, 0.5f) == static_cast(Width) * Height, "the depth 0.5 a draw wrote after a guest write into the plane did not reach memory"); + + Htile.fill(0xffffffffu); + auto* cleared = static_cast(AllocateWatched(Block)); + const std::span clearedPlane(cleared, planeBytes / sizeof(float)); + const auto clearedReader = Compile(*device, cleared); + const Surface compressed{reinterpret_cast(cleared), 0, reinterpret_cast(Htile.data()), false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + Require(Draw(*device, shaders, compressed, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 into a plane with HTILE did not cover the target"); + Draw(*device, clearedReader, depth, {.depthTest = false}); + Require(Holding(clearedPlane, 0.5f) == static_cast(Width) * Height, "a vertex fetch from a plane with HTILE did not see the depth 0.5 a draw wrote into it"); + std::fill(clearedPlane.begin(), clearedPlane.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + AgcDriver::Graphics::NoteDepthMetadataFill(compressed.htile, HtileBytes, 0u); + Require(Draw(*device, shaders, compressed, {}) == Covered, "a guest write into the depth plane won over a later HTILE fast clear of it"); + + auto* viewed = static_cast(AllocateWatched(Block)); + const std::span viewedBlock(viewed, Block / sizeof(float)); + std::fill(viewedBlock.begin(), viewedBlock.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + ViewedHtile.fill(0xffffffffu); + const Surface sampled{reinterpret_cast(viewed), 0, reinterpret_cast(ViewedHtile.data()), false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + const auto viewedReader = Compile(*device, viewed); + constexpr std::size_t ViewOffset = 0x100; + const auto view = sampled.depth + ViewOffset; + const auto matchesMemory = [&](const std::array& row) { + Draw(*device, viewedReader, depth, {.depthTest = false}); + return std::memcmp(row.data(), reinterpret_cast(viewed) + ViewOffset, sizeof(row)) == 0; + }; + Require(Draw(*device, shaders, sampled, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 into a viewed plane did not cover the target"); + Require(matchesMemory(LoadRow(*device, view, Width, Height / 2u, 0u)), "a linear texture at an offset into a depth plane did not read the depth drawn into it"); + AgcDriver::Graphics::NoteDepthMetadataFill(sampled.htile, HtileBytes, 0u); + Draw(*device, shaders, sampled, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_NEVER}); + Require(matchesMemory(LoadRow(*device, view, Width, Height / 2u, 0u)), "a cached linear texture at an offset into a depth plane did not read the depth cleared after it was cached"); + Require(Draw(*device, shaders, sampled, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 over the cleared viewed plane did not cover the target"); + Require(matchesMemory(LoadRow(*device, view, Width, Height / 2u, 0u)), "a linear texture at an offset into a depth plane, viewed through its cached storage image, did not read the depth drawn after the image was made"); + + auto* blended = static_cast(AllocateWatched(Block)); + const std::span blendedBlock(blended, Block / sizeof(float)); + std::fill(blendedBlock.begin(), blendedBlock.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const Surface underBlend{reinterpret_cast(blended), 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + Require(Draw(*device, shaders, underBlend, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 under a later blended color target did not cover the target"); + DrawBlendKeepingMemory(*device, shaders, underBlend.depth); + Draw(*device, Compile(*device, blended), depth, {.depthTest = false}); + Require(Holding(blendedBlock, 0.5f) == static_cast(Width) * Height, "a color target blending over a depth plane did not load the depth drawn into the plane"); + Htile.fill(0xffffffffu); + auto* reused = static_cast(AllocateWatched(Block)); + const std::span reusedPlane(reused, planeBytes / sizeof(float)); + const auto reusedReader = Compile(*device, reused); + const Surface recleared{reinterpret_cast(reused), 0, reinterpret_cast(Htile.data()), false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + Require(Draw(*device, shaders, recleared, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 into a plane that is cleared each frame did not cover the target"); + Draw(*device, reusedReader, depth, {.depthTest = false}); + Require(Holding(reusedPlane, 0.5f) == static_cast(Width) * Height, "a vertex fetch did not see the depth 0.5 drawn into a plane that is cleared each frame"); + std::fill(reusedPlane.begin(), reusedPlane.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + for (int frame = 0; frame < 2; ++frame) { + const float value = 0.75f - 0.125f * static_cast(frame); + AgcDriver::Graphics::NoteDepthMetadataFill(recleared.htile, HtileBytes, 0u); + Require(Draw(*device, shaders, recleared, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS, .minDepth = value, .maxDepth = value}) == Covered, "the draw after the HTILE clear did not cover the target"); + Draw(*device, reusedReader, depth, {.depthTest = false}); + Require(Holding(reusedPlane, value) == static_cast(Width) * Height, "after a guest write into a stored depth plane, the depth drawn behind an HTILE clear did not reach memory (frame " + std::to_string(frame) + ")"); + } + std::puts("depth plane buffer memory tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneFill.cpp new file mode 100644 index 0000000000..fb67e951b8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneFill.cpp @@ -0,0 +1,110 @@ +#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(4096) std::array AliasDepth{}; +alignas(4096) std::array AliasStencil{}; +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)"); + Htile.fill(0xfffff3ffu); + AgcDriver::Graphics::NoteDepthMetadataFill(stencil, StencilPlaneBytes, 0x07070707u); + const Surface stencilAlias{reinterpret_cast(AliasDepth.data()), stencil, htiled.htile, false, VK_FORMAT_D32_SFLOAT_S8_UINT}; + Require(Draw(*device, shaders, stencilAlias, StencilEquals(7u)) == Covered, "a new attachment lost its inherited pending stencil fill"); + + AgcDriver::Graphics::NoteDepthMetadataFill(stencilAlias.depth, DepthPlaneBytes, 0u); + const Surface depthAlias{stencilAlias.depth, reinterpret_cast(AliasStencil.data()), htiled.htile, false, VK_FORMAT_D32_SFLOAT_S8_UINT}; + Require(Draw(*device, shaders, depthAlias, {}) == 0u, "a new attachment lost its inherited pending depth fill"); + 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/DepthPlaneGuestLayers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp index 797f086287..cdb94c5363 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp @@ -313,6 +313,76 @@ void RecordedUploads(const Context& context, const WatchedBlock& block) { recorder.Sync(); } +void RunCubeGather(const Context& base, const WatchedBlock& block) { + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.width = Side; + resource.height = Side; + resource.depthOrLastArray = 5; + resource.mipCount = 2; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::kCube; + resource.format = Format32Float; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + Require(geometry.layerBytes != LayerBytes, "the cube fixture's per-layer footprint must not match one host depth slice"); + const auto address = block.Address(); + resource.baseAddress = address; + ClearDepthSurfaces(context.device); + for (std::uint32_t layer = 0; layer < 6; ++layer) { + DepthSurfaceView(context, DepthTarget{address + geometry.GuestLayerOffset(layer), 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + } + imageToBufferCopies = 0; + bufferToImageCopies = 0; + const auto texture = DepthSurfaceTexture(context, words, resource, components); + Require(texture != nullptr, "a cube over six depth surfaces at the descriptor's layer offsets was not sampled through their depth planes"); + if (imageToBufferCopies.load() != 6 || bufferToImageCopies.load() != 1) { + throw std::runtime_error("sampling a cube over six depth surfaces recorded " + std::to_string(imageToBufferCopies.load()) + " depth surface copies and " + std::to_string(bufferToImageCopies.load()) + " plane copy fills, expected 6 and 1"); + } + ClearDepthSurfaces(context.device); +} + +void RunCubeTiledSlice(const Context& base, const WatchedBlock& block) { + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + constexpr VkExtent2D extent{64, 64}; + GuestTextureResource resource{}; + resource.width = extent.width; + resource.height = extent.height; + resource.depthOrLastArray = 5; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::kCube; + resource.format = Format32Float; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + const auto slice = DepthSliceBytes(extent, 4u); + Require(geometry.layerBytes == slice && geometry.sliceLinearBytes != slice, "the tiled cube fixture must have a layer footprint equal to a depth slice but a different linear footprint"); + const auto address = block.Address(); + resource.baseAddress = address; + ClearDepthSurfaces(context.device); + for (std::uint32_t layer = 0; layer < 6; ++layer) { + DepthSurfaceView(context, DepthTarget{address + geometry.GuestLayerOffset(layer), 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + } + imageToBufferCopies = 0; + bufferToImageCopies = 0; + const auto texture = DepthSurfaceTexture(context, words, resource, components); + Require(texture != nullptr, "a tiled cube whose linear layer footprint differs from a depth slice was not sampled through its depth planes"); + if (imageToBufferCopies.load() != 6 || bufferToImageCopies.load() != 1) { + throw std::runtime_error("sampling a tiled cube over six depth surfaces recorded " + std::to_string(imageToBufferCopies.load()) + " depth surface copies and " + std::to_string(bufferToImageCopies.load()) + " plane copy fills, expected 6 and 1"); + } + ClearDepthSurfaces(context.device); +} + } int main() { @@ -337,6 +407,8 @@ int main() { Require(AgcDriver::GuestMemory::Watched(block.Address(), BlockBytes), "depth plane guest layers: the guest block is not write-watched"); Run(context, block); RecordedUploads(context, block); + RunCubeGather(context, block); + RunCubeTiledSlice(context, block); } device.reset(); std::puts("depth plane guest layer tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlanePartialStore.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlanePartialStore.cpp new file mode 100644 index 0000000000..43b8815b56 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlanePartialStore.cpp @@ -0,0 +1,234 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::DepthPlaneStoreStatistics; +using DepthFastClearHarness::Compile; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::DrawOptions; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Surface; + +constexpr std::uint32_t PlaneWidth = 384; +constexpr std::uint32_t PlaneHeight = 200; +constexpr VkExtent2D PlaneExtent{PlaneWidth, PlaneHeight}; +constexpr std::size_t Block = 65536; +constexpr std::size_t Units = 6; +constexpr std::size_t PlaneBytes = Units * Block; +constexpr std::size_t UnitTexels = Block / sizeof(float); +constexpr std::size_t EdgeTexels = 128u * (PlaneHeight - 128u); +constexpr std::size_t PlaneTexels = static_cast(PlaneWidth) * PlaneHeight; +constexpr VkExtent2D SmallExtent{64, 32}; +constexpr std::size_t SmallColorBytes = static_cast(SmallExtent.width) * SmallExtent.height * 4u; +constexpr std::uint64_t ReadBefore = 4096; +constexpr std::size_t ReadBytes = 7u * 1024u; +alignas(256) std::array Colors{}; + +void* AllocateBlocks(std::size_t bytes, bool watched) { +#ifdef _WIN32 + void* block = nullptr; + if (watched) { + block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); + } else { + block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + } + Require(block != nullptr, "cannot allocate the depth plane blocks"); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the depth plane blocks"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + if (watched) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + return block; +} + +void Fill(float* plane, float value) { + std::fill(plane, plane + PlaneBytes / sizeof(float), value); + AgcDriver::GuestMemory::BumpCollectEpoch(); +} + +void FillUnit(float* plane, std::size_t unit, float value) { + std::fill(plane + unit * UnitTexels, plane + (unit + 1u) * UnitTexels, value); + AgcDriver::GuestMemory::BumpCollectEpoch(); +} + +std::size_t Texels(std::size_t unit) { + return unit < 3u ? UnitTexels : EdgeTexels; +} + +void RequireUnit(const float* plane, std::size_t unit, float depth, std::size_t count, const std::string& what) { + const auto* first = plane + unit * UnitTexels; + const auto held = static_cast(std::count(first, first + UnitTexels, depth)); + Require(held == count, what + ": unit " + std::to_string(unit) + " holds " + std::to_string(held) + " texels of " + std::to_string(depth) + " instead of " + std::to_string(count)); +} + +DepthPlaneStoreStatistics Since(const DepthPlaneStoreStatistics& before) { + const auto now = AgcDriver::Graphics::DepthPlaneStoreCounts(); + DepthPlaneStoreStatistics difference; + difference.stores = now.stores - before.stores; + difference.units = now.units - before.units; + difference.bytes = now.bytes - before.bytes; + return difference; +} + +std::vector> ReadStart(AgcDriver::VulkanDevice& device, std::uint64_t base) { + auto stored = AgcDriver::Graphics::FlushDepthPlanes(base - ReadBefore, ReadBytes); + device.WaitIdle(); + return stored; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Require(AgcDriver::Graphics::DepthSliceBytes(PlaneExtent, 4u) == PlaneBytes, "the 384x200 depth plane is not six 64 KiB tiles"); + auto* blocks = static_cast(AllocateBlocks(Block + PlaneBytes, true)); + auto* plane = reinterpret_cast(blocks + Block); + const auto base = reinterpret_cast(plane); + const auto shaders = Compile(*device); + const auto swizzle = AgcDriver::Graphics::XorSwizzleMode(AgcDriver::Graphics::TextureTileMode::kZ64KBX); + const Surface depth{base, 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, swizzle}; + const auto draw = [&](const Surface& surface, const DrawOptions& options) { return Draw(*device, shaders, surface, options, PlaneExtent, Colors); }; + const DrawOptions writeHalf{.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}; + + Fill(plane, 1.0f); + Require(draw(depth, writeHalf) == PlaneTexels, "the draw writing depth 0.5 did not cover the plane"); + Require(AgcDriver::Graphics::DepthPlanesStale(), "the draw writing depth left the plane unregistered"); + auto before = AgcDriver::Graphics::DepthPlaneStoreCounts(); + const auto first = ReadStart(*device, base); + auto stored = Since(before); + Require(first.size() == 1 && first[0].first == base && first[0].second == Block, "a read ending 3 KiB into the stale plane did not store exactly its first 64 KiB unit"); + Require(stored.stores == 1 && stored.units == 1 && stored.bytes == Block, "a read ending 3 KiB into the stale plane did not count one store of one unit"); + RequireUnit(plane, 0, 0.5f, UnitTexels, "after a read of the first unit"); + for (std::size_t unit = 1; unit < Units; ++unit) RequireUnit(plane, unit, 1.0f, UnitTexels, "after a read of the first unit, a unit it does not overlap"); + Require(AgcDriver::Graphics::DepthPlanesStale(), "a store of one unit left no stale plane"); + + before = AgcDriver::Graphics::DepthPlaneStoreCounts(); + Require(ReadStart(*device, base).empty(), "the same read again stored the first unit again"); + stored = Since(before); + Require(stored.stores == 0 && stored.units == 0 && stored.bytes == 0, "the same read again counted a store"); + + before = AgcDriver::Graphics::DepthPlaneStoreCounts(); + const auto rest = AgcDriver::Graphics::FlushDepthPlanes(base, PlaneBytes); + device->WaitIdle(); + stored = Since(before); + Require(rest.size() == 1 && rest[0].first == base + Block && rest[0].second == PlaneBytes - Block, "a read of the whole plane did not store the five units still stale"); + Require(stored.stores == 1 && stored.units == Units - 1u && stored.bytes == PlaneBytes - Block, "a read of the whole plane did not count one store of five units"); + for (std::size_t unit = 0; unit < Units; ++unit) RequireUnit(plane, unit, 0.5f, Texels(unit), "after a read of the whole plane"); + for (std::size_t unit = 3; unit < Units; ++unit) RequireUnit(plane, unit, 1.0f, UnitTexels - EdgeTexels, "after a read of the whole plane, the padding below the last row"); + Require(!AgcDriver::Graphics::DepthPlanesStale(), "a read of the whole plane left it stale"); + + if (!AgcDriver::GuestMemory::WriteWatched() || !AgcDriver::GuestMemory::Watched(base, PlaneBytes)) { + std::puts("guest memory is not write-watched here: guest writes into a partly stored plane are not exercised"); + } else { + Fill(plane, 1.0f); + Require(draw(depth, writeHalf) == PlaneTexels, "the draw writing depth 0.5 over the reloaded plane did not cover it"); + Require(!ReadStart(*device, base).empty(), "a read of the first unit of the drawn plane stored nothing"); + FillUnit(plane, 4, 0.75f); + draw(depth, {.depthTest = false}); + before = AgcDriver::Graphics::DepthPlaneStoreCounts(); + Require(!AgcDriver::Graphics::FlushDepthPlanes(base, PlaneBytes).empty(), "a read of the whole plane after a guest write into unit 4 stored nothing"); + device->WaitIdle(); + stored = Since(before); + Require(stored.units == 4 && stored.bytes == 4u * Block, "a read of the whole plane after a guest write into unit 4 did not store the four units still stale"); + for (const std::size_t unit : {0u, 1u, 2u, 3u, 5u}) RequireUnit(plane, unit, 0.5f, Texels(unit), "a guest write into unit 4 after unit 0 was stored, then a read of the whole plane"); + RequireUnit(plane, 4, 0.75f, UnitTexels, "a guest write into unit 4 after unit 0 was stored, then a read of the whole plane, the written unit"); + Require(draw(depth, {.minDepth = 0.6f, .maxDepth = 0.6f}) == EdgeTexels, "a draw at depth 0.6 did not pass LESS exactly over the unit the guest wrote 0.75 into"); + + Fill(plane, 1.0f); + Require(draw(depth, writeHalf) == PlaneTexels, "the draw writing depth 0.5 before a guest write and an unmatched view did not cover the plane"); + Require(!ReadStart(*device, base).empty(), "a read of the first unit before a guest write and an unmatched view stored nothing"); + FillUnit(plane, 2, 0.75f); + const std::array words{0, 0, 0, 0, PlaneWidth, PlaneHeight, 1, 0}; + AgcDriver::Graphics::GuestTextureResource levels{}; + levels.baseAddress = base; + levels.width = PlaneWidth; + levels.height = PlaneHeight; + levels.mipCount = 2; + levels.lastLevel = 1; + levels.dimension = AgcDriver::Graphics::TextureDimension::k2D; + levels.tileMode = AgcDriver::Graphics::TextureTileMode::kZ64KBX; + levels.format = 22; + Require(AgcDriver::Graphics::DepthSurfaceTexture(AgcDriver::VulkanDeviceTestAccess::ContextOf(*device), words, levels, {}) == nullptr, "a view with two levels of a plane the guest wrote was served by the depth surface"); + device->WaitIdle(); + Require(!AgcDriver::Graphics::DepthSurfaceAt(base), "a view with two levels of a plane the guest wrote left the depth surface live"); + for (const std::size_t unit : {0u, 1u, 3u, 4u, 5u}) RequireUnit(plane, unit, 0.5f, Texels(unit), "the retire of a partly stored plane the guest wrote into"); + RequireUnit(plane, 2, 0.75f, UnitTexels, "the retire of a partly stored plane the guest wrote into, the written unit"); + } + + Fill(plane, 1.0f); + Require(draw(depth, writeHalf) == PlaneTexels, "the draw writing depth 0.5 under a later color target did not cover the plane"); + Require(!ReadStart(*device, base).empty(), "a read of the first unit before a color target stored nothing"); + auto* smallPlane = AllocateBlocks(Block, true); + const Surface smallDepth{reinterpret_cast(smallPlane), 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, swizzle}; + const std::span overLastUnit(reinterpret_cast(plane + 5u * UnitTexels), SmallColorBytes); + Draw(*device, shaders, smallDepth, {.depthTest = false}, SmallExtent, overLastUnit); + Require(!AgcDriver::Graphics::DepthSurfaceAt(base), "a color target over the last unit left the depth surface live"); + for (std::size_t unit = 0; unit < 5u; ++unit) RequireUnit(plane, unit, 0.5f, Texels(unit), "a color target over the last unit of a partly stored plane"); + + auto* looseBlocks = static_cast(AllocateBlocks(Block + PlaneBytes, false)); + auto* loose = reinterpret_cast(looseBlocks + Block); + const auto looseBase = reinterpret_cast(loose); + if (AgcDriver::GuestMemory::Watched(looseBase, PlaneBytes)) { + std::puts("memory outside the guest arena is write-watched here: a partial store over unwatched memory is not exercised"); + } else { + const Surface unwatched{looseBase, 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, swizzle}; + Fill(loose, 1.0f); + Require(draw(unwatched, writeHalf) == PlaneTexels, "the draw writing depth 0.5 into an unwatched plane did not cover it"); + before = AgcDriver::Graphics::DepthPlaneStoreCounts(); + Require(ReadStart(*device, looseBase).size() == 1, "a read of the first unit of an unwatched plane did not store it"); + stored = Since(before); + Require(stored.units == 1 && stored.bytes == Block, "a read of the first unit of an unwatched plane did not count one unit"); + RequireUnit(loose, 0, 0.5f, UnitTexels, "unwatched plane after a read of the first unit"); + for (std::size_t unit = 1; unit < Units; ++unit) RequireUnit(loose, unit, 1.0f, UnitTexels, "unwatched plane after a read of the first unit, a unit it does not overlap"); + Require(draw(unwatched, {.minDepth = 0.6f, .maxDepth = 0.6f}) == 0u, "a draw at depth 0.6 passed LESS over unwatched units still holding depth 0.5 on the GPU"); + } + std::puts("depth plane partial store 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 2cdf03f889..33c6cfdb6b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp @@ -1,9 +1,13 @@ +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" #include "DepthFastClearHarness.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "Recompiler.hpp" #include "VulkanTestDevice.hpp" #ifdef _WIN32 @@ -16,6 +20,7 @@ #endif #include #include +#include #include #include #include @@ -26,6 +31,14 @@ #include #include +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + namespace { using AgcDriver::Graphics::Require; @@ -35,7 +48,9 @@ using DepthFastClearHarness::VertexCode; 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; @@ -43,7 +58,7 @@ constexpr std::uint32_t Type2DArray = 13; constexpr std::uint32_t Swizzle64KbS = 9; constexpr std::uint32_t Swizzle64KbZX = 24; constexpr std::uint64_t SliceBytes = 65536; -constexpr std::size_t BlockBytes = 2u << 20u; +constexpr std::size_t BlockBytes = 8u << 20u; constexpr std::size_t OutputOffset = 0; constexpr std::size_t ArrayOffset = 0x10000; constexpr std::size_t CubeOffset = 0x40000; @@ -51,17 +66,19 @@ constexpr std::size_t IntegerOffset = 0xa0000; constexpr std::size_t DepthBitsOffset = 0xb0000; constexpr std::size_t OneSliceOffset = 0xd0000; constexpr std::size_t FirstSliceOffset = 0xe0000; +constexpr 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; -// image_load of row 0, one texel per thread (x = thread id), slice / face from user data, stored as a dword per -// thread into the buffer at s[0:3]. The T# is s[4:11], the slice s12. constexpr std::array LoadDims{1u, 3u, 5u}; std::array LoadCode(std::uint32_t dim) { return { - 0x7e020280, // v_mov_b32 v1, 0 (y) + 0x7e02020d, 0x7e04020c, // v_mov_b32 v2, s12 (slice or face) 0xf0001100u | (dim << 3u), // image_load v3, v[0:2], s[4:11] dmask:0x1 unorm, dim 2D / CUBE / 2D_ARRAY 0x00010300, @@ -79,7 +96,7 @@ alignas(256) std::array Load2D = LoadCode(LoadDims[0]); alignas(256) std::array LoadCube = LoadCode(LoadDims[1]); alignas(256) std::array LoadArray = LoadCode(LoadDims[2]); -alignas(256) std::array Pixels{}; +alignas(256) std::array Pixels{}; class GuestBlock { public: @@ -196,7 +213,7 @@ void DrawDepth(AgcDriver::VulkanDevice& device, std::uint64_t address, VkExtent2 }}; AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; - state.color = {reinterpret_cast(Pixels.data()), extent, VK_FORMAT_R8G8B8A8_UNORM, static_cast(extent.width) * extent.height * 4u, 0xe4u}; + state.color = {reinterpret_cast(Pixels.data()), extent, VK_FORMAT_R8G8B8A8_UNORM, AgcDriver::Graphics::ColorTargetLayout(extent.width, extent.height, AgcDriver::Graphics::ColorTileMode::Linear).Bytes(), 0xe4u}; state.colors = {state.color}; state.hasColorTarget = true; state.depth = AgcDriver::Graphics::DepthTarget{address, 0, extent, format, 1.0f, 0}; @@ -218,9 +235,7 @@ void DrawDepth(AgcDriver::VulkanDevice& device, std::uint64_t address, VkExtent2 device.WaitIdle(); } -// Loads row 0 of `slice` of the texture `shape` describes at `texels` into `output` (one dword per thread). With -// `shaderImage2D` the shader declares a 2D image whatever the T#'s type is. -void Load(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& shape, std::uint32_t slice, std::span output, bool shaderImage2D = false) { +void Load(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& shape, std::uint32_t slice, std::span output, bool shaderImage2D = false, std::uint32_t row = 0) { const auto& code = shaderImage2D ? Load2D : shape.type == TypeCube ? LoadCube : shape.type == Type2DArray ? LoadArray : Load2D; std::vector userData(16, 0u); const auto out = std::array{static_cast(reinterpret_cast(output.data())), static_cast((reinterpret_cast(output.data()) >> 32u) & 0xffffu), static_cast(output.size_bytes()), 0x31016facu}; @@ -228,6 +243,7 @@ void Load(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& sh std::copy(out.begin(), out.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); userData[12] = slice; + userData[13] = row; const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(code))}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ @@ -303,6 +319,25 @@ void DepthBitsArrayCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std: // A 2D shader image over a 2D-array T# reads the array's first slice: a one-slice array, which samples the depth // image itself, and a 3-slice array, which samples the plane copy. +void DepthBitsBaseSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + Shape shape{Format16Uint, 256, 128, Type2DArray, 1, Swizzle64KbZX}; + shape.baseArray = 1; + Load(device, block.At(DepthBitsOffset), shape, 0, output); + 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); @@ -316,20 +351,87 @@ void FirstSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::spa Expect(output, Bits(DrawnDepth), "3-slice D32 2D array through a 2D shader image"); } -// A plane array from a later base slice is still refused (its slice offsets are not modeled here). -void RefusalCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { - bool refused = false; - try { - Shape shape{Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}; - shape.baseArray = 1; - Load(device, block.At(ArrayOffset), shape, 0, output); - } catch (const std::runtime_error& error) { - refused = std::string(error.what()).find("sampling the depth planes of depth surface") != std::string::npos; - if (!refused) throw; +void PaddedCubeCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* base = block.At(PaddedCubeOffset); + DrawDepth(device, reinterpret_cast(base), {128, 128}, VK_FORMAT_D16_UNORM, DrawnD16Depth); + for (std::uint32_t face = 1; face < 6u; ++face) FillSlice(base + face * SliceBytes, SliceBytes, 0x1000u + face, 2u); + const Shape shape{Format16Uint, 128, 128, TypeCube, 0, Swizzle64KbZX}; + for (std::uint32_t face = 0; face < 6u; ++face) { + Load(device, base, shape, face, output); + Expect(output, face == 0 ? DrawnD16Bits : 0x1000u + face, "D16 cube with padded rows, face " + std::to_string(face)); } - Require(refused, "a depth plane array from base slice 1 was not refused"); } +void SmallD16CubeCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* base = block.At(SmallCubeOffset); + DrawDepth(device, reinterpret_cast(base), {32, 32}, VK_FORMAT_D16_UNORM, DrawnD16Depth); + for (std::uint32_t face = 1; face < 6u; ++face) FillSlice(base + face * SliceBytes, SliceBytes, 0x1230u + face, 2u); + const Shape shape{Format16Uint, 32, 32, TypeCube, 0, Swizzle64KbZX}; + for (std::uint32_t face = 0; face < 6u; ++face) { + Load(device, base, shape, face, output); + Expect(output, face == 0 ? DrawnD16Bits : 0x1230u + face, "small D16 cube face " + std::to_string(face)); + } +} + +void PaddedD16CubeCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + constexpr std::uint32_t side = 320; + const auto mips = AgcDriver::Graphics::ComputeElementMipLayout(AgcDriver::Graphics::TextureTileMode::kZ64KBX, 2u, side, side, 1u); + Require(mips.size() == 1 && mips.front().pitchBytes == 1024 && mips.front().linearSize == 327680 && mips.front().tiledSize == 393216, "the 320 D16 regression must have a padded row pitch and face stride"); + const auto stride = AgcDriver::Graphics::DepthSliceBytes({side, side}, 2u); + Require(stride == 393216 && LargeCubeOffset + 6u * stride <= BlockBytes, "the 320 D16 cube must fit its actual tiled face stride"); + auto* base = block.At(LargeCubeOffset); + DrawDepth(device, reinterpret_cast(base), {side, side}, VK_FORMAT_D16_UNORM, DrawnD16Depth); + for (std::uint32_t face = 1; face < 6; ++face) FillSlice(base + face * stride, stride, 0x1000u + face * 0x1000u, 2u); + const Shape shape{Format16Unorm, side, side, TypeCube, 5u, Swizzle64KbZX}; + const auto check = [&](std::uint32_t face, std::uint32_t row, bool rewritten) { + Load(device, base, shape, face, output, false, row); + const auto bits = face == 0 ? DrawnD16Bits : rewritten && face == 3 ? 0x7000u : 0x1000u + face * 0x1000u; + const auto expected = static_cast(bits) / 65535.0f; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + float actual = 0; + std::memcpy(&actual, &output[tid], sizeof actual); + char message[160]; + std::snprintf(message, sizeof message, "320 D16 cube face %u row %u texel %u: got %.8f, expected %.8f", face, row, tid, actual, expected); + Require(std::abs(actual - expected) <= 2.0f / 65535.0f, message); + } + }; + for (std::uint32_t face = 0; face < 6; ++face) { + for (const auto row : {0u, 1u, 127u, 319u}) check(face, row, false); + } + FillSlice(base + 3u * stride, stride, 0x7000u, 2u); + for (std::uint32_t face = 0; face < 6; ++face) check(face, 319u, true); +} + +void BaseSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* base = block.At(ArrayOffset); + Shape shape{Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}; + shape.baseArray = 1; + Load(device, base, shape, 0, output); + Expect(output, Bits(0.5f), "D32 2D array from base slice 1, slice 0"); + Load(device, base, shape, 1, output); + Expect(output, Bits(0.75f), "D32 2D array from base slice 1, slice 1"); + Load(device, base, shape, 0, output, true); + Expect(output, Bits(0.5f), "D32 2D array from base slice 1 through a 2D shader image"); +} + +void ExpectRefusal(AgcDriver::VulkanDevice& device, GuestBlock& block, std::size_t offset, const Shape& shape, std::uint32_t baseArray, const std::string& expected, const std::string& what) { + const auto words = TextureDescriptor(block.At(offset), shape); + auto resource = AgcDriver::Graphics::DecodeTextureResource(words); + resource.baseArray = baseArray; + try { + static_cast(AgcDriver::Graphics::DepthSurfaceTexture(AgcDriver::VulkanDeviceTestAccess::ContextOf(device), words, resource, {})); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find(expected) != std::string::npos, what + " was refused with an unexpected message: " + error.what()); + return; + } + Require(false, what + " was not refused"); +} + +void RefusalCase(AgcDriver::VulkanDevice& device, GuestBlock& block) { + ExpectRefusal(device, block, ArrayOffset, {Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}, 3, "base slice 3 is not implemented", "a depth plane array whose base slice is past its last slice"); + ExpectRefusal(device, block, CubeOffset, {Format32Float, 128, 128, TypeCube, 5, Swizzle64KbZX}, 1, "base slice 1 is not implemented", "a depth plane cube from base slice 1"); + ExpectRefusal(device, block, ArrayOffset, {Format32Float, 128, 128, Type2D, 0, Swizzle64KbZX}, 1, "slice 1 is not implemented", "a 2D depth plane view with a base slice"); +} } int main() { @@ -342,8 +444,14 @@ int main() { CubeCase(*device, block, output); IntegerCase(*device, block, output); DepthBitsArrayCase(*device, block, output); + DepthBitsBaseSliceCase(*device, block, output); + GuestDepthBitsCase(*device, block, output); FirstSliceCase(*device, block, output); - RefusalCase(*device, block, output); + PaddedCubeCase(*device, block, output); + SmallD16CubeCase(*device, block, output); + PaddedD16CubeCase(*device, block, output); + BaseSliceCase(*device, block, output); + RefusalCase(*device, block); std::puts("depth plane sampling tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp new file mode 100644 index 0000000000..831ec0b751 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthRangeRemapDraw.cpp @@ -0,0 +1,257 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +namespace Harness = DepthFastClearHarness; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +struct Transform { + bool negativeOneToOne; + float scale; + float offset; + float clampMin; + float clampMax; +}; + +struct Case { + Transform transform; + float z; + bool drawn; + float probe; + bool covered; + bool remapOnly; + const char* what; +}; + +constexpr Transform Plain{false, 1.0f, 0.0f, 0.0f, 1.0f}; + +alignas(4096) std::array, 32> Depths{}; +alignas(256) std::array, 3>, 128> VertexSets{}; +std::size_t vertexSets = 0; + +// A vertex shader that reads no user data: the fullscreen triangle comes from the vertex index (v5), and the +// clip z is the literal at index 9. Its program carries no push data of its own. +alignas(256) std::array FetchlessCode{ + 0x36020a81u, 0x34020282u, 0x4a0202c1u, 0x7e020b01u, 0x2c040a81u, 0x34040482u, 0x4a0404c1u, 0x7e040b02u, + 0x7e0602ffu, 0u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, +}; + +std::uint32_t Draw(AgcDriver::VulkanDevice& device, std::uint64_t depth, const Transform& transform, bool remap, float z, bool write, bool fetchless = false, bool targetRemap = false, bool prepared = false) { + Require(vertexSets < VertexSets.size(), "the test ran out of vertex sets"); + auto& vertices = VertexSets[vertexSets++]; + vertices = {{{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}}; + Harness::Pixels.fill(std::byte{0}); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Harness::Pixels.data()), {Harness::Width, Harness::Height}, VK_FORMAT_R8G8B8A8_UNORM, Harness::Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = AgcDriver::Graphics::DepthTarget{depth, 0, {Harness::Width, Harness::Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + state.depthTest = true; + state.depthWrite = write; + state.depthCompare = write ? VK_COMPARE_OP_ALWAYS : VK_COMPARE_OP_LESS; + state.renderExtent = {Harness::Width, Harness::Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + const float minDepth = transform.negativeOneToOne ? transform.offset - transform.scale : transform.offset; + state.viewport = {0, static_cast(Harness::Height), static_cast(Harness::Width), -static_cast(Harness::Height), minDepth, transform.offset + transform.scale}; + state.negativeOneToOne = transform.negativeOneToOne; + state.depthScale = transform.scale; + state.depthOffset = transform.offset; + state.depthClampMin = transform.clampMin; + state.depthClampMax = transform.clampMax; + state.scissor = {{0, 0}, {Harness::Width, Harness::Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + auto context = device.graphicsContext(); + context.depthRangeUnrestricted = !remap; + AgcDriver::Graphics::ApplyDepthRemap(context, state); + const bool inRange = minDepth >= 0.0f && transform.offset + transform.scale <= 1.0f; + Require((state.depthRemap.mode != 0u) == (remap && !inRange), "the draw did not take the depth path the test asked for"); + + auto target = device.Target(); + target.depthRangeRemap = state.depthRemap.mode != 0u || targetRemap; + ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; + vertexInfo.depthRemapMode = state.depthRemap.mode; + std::vector vertexUserData(4, 0u); + if (fetchless) { + FetchlessCode[9] = std::bit_cast(z); + } else { + const auto vertexBuffer = Harness::BufferDescriptor(vertices.data(), 16u, static_cast(vertices.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + } + const std::span vertexCode = fetchless ? std::span(FetchlessCode) : std::span(Harness::VertexCode); + const std::array vertexMemory{{{reinterpret_cast(vertexCode.data()), std::as_bytes(vertexCode)}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(vertexCode.data()), vertexCode, 0, {}}, + {32, 0, vertexUserData, std::nullopt, std::nullopt, vertexInfo, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = prepared; + std::shared_ptr vertexHolder; + if (prepared) { + const auto handle = ShaderRecompiler::PrepareShader(vertex); + const auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(vertex, handle); + Require(invocation.has_value(), "the prepared vertex shader did not match its own request"); + vertexHolder = invocation->Materialize(*invocation->Capture(ShaderRecompiler::SrtRuntime{std::span(vertexUserData)})); + } else { + vertexHolder = std::make_shared(ShaderRecompiler::Recompile(vertex)); + } + const auto& vertexResult = *vertexHolder; + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + if (fetchless) Require(vertexPush == 0u, "the fetchless vertex shader carries push data of its own"); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = true; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + std::vector pixelUserData(4, 0u); + const std::array pixelMemory{{{reinterpret_cast(Harness::PixelCode.data()), std::as_bytes(std::span(Harness::PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(Harness::PixelCode.data()), Harness::PixelCode, 0, {}}, + {32, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); + std::uint32_t covered = 0; + for (std::size_t index = 0; index < Harness::Pixels.size() / 4u; ++index) { + if (std::to_integer(Harness::Pixels[index * 4u]) == 255u) ++covered; + } + return covered; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto context = device->graphicsContext(); + if (!context.depthClamp) { + std::puts("skipped, the device has no depth clamping"); + return VulkanTestSkipped; + } + constexpr float Step = 1.0f / 64.0f; + constexpr Transform minecraft{true, 1.0f, 0.0f, -1.0f, 1.0f}; + constexpr Transform widened{false, 2.0f, -0.5f, 0.0f, 1.0f}; + constexpr Transform bounded{false, 2.0f, -0.5f, 0.25f, 0.75f}; + constexpr std::array cases{{ + {minecraft, 0.5f - Step, true, 0.5f, false, false, "negative-one-to-one depth just below 0.5"}, + {minecraft, 0.5f + Step, true, 0.5f, true, false, "negative-one-to-one depth just above 0.5"}, + {minecraft, -1.5f, false, 0.5f, true, false, "negative-one-to-one clip z in front of the near plane"}, + {minecraft, 1.5f, false, 0.5f, true, false, "negative-one-to-one clip z behind the far plane"}, + {widened, 0.5f - Step / 2.0f, true, 0.5f, false, false, "scaled zero-to-one depth just below 0.5"}, + {widened, 0.5f + Step / 2.0f, true, 0.5f, true, false, "scaled zero-to-one depth just above 0.5"}, + {widened, -0.125f, false, 0.5f, true, false, "zero-to-one clip z in front of the near plane"}, + {widened, 1.125f, false, 0.5f, true, false, "zero-to-one clip z behind the far plane"}, + {bounded, 0.4f, true, 0.35f, false, false, "depth 0.3 inside clamp bounds [0.25, 0.75], probed at 0.35"}, + {bounded, 0.4f, true, 0.25f, true, false, "depth 0.3 inside clamp bounds [0.25, 0.75], probed at 0.25"}, + {bounded, 0.6f, true, 0.65f, true, false, "depth 0.7 inside clamp bounds [0.25, 0.75], probed at 0.65"}, + {bounded, 0.2f, true, 0.2f, true, true, "depth -0.1 clamped to 0.25, probed at 0.2"}, + {bounded, 0.8f, true, 0.8f, false, true, "depth 1.1 clamped to 0.75, probed at 0.8"}, + }}; + std::size_t surfaces = 0; + std::size_t ran = 0; + for (const bool remap : {true, false}) { + if (!remap && !context.depthRangeUnrestricted) continue; + for (const auto& test : cases) { + if (!remap && (test.remapOnly || (test.transform.negativeOneToOne && !context.depthClipControl))) continue; + Require(surfaces < Depths.size(), "the test ran out of depth surfaces"); + const auto depth = static_cast(reinterpret_cast(Depths[surfaces++].data())); + const auto drawn = Draw(*device, depth, test.transform, remap, test.z, true); + const auto passed = Draw(*device, depth, Plain, false, test.probe, false); + const auto path = remap ? "remapped" : "unrestricted"; + if (drawn != (test.drawn ? Harness::Covered : 0u) || passed != (test.covered ? Harness::Covered : 0u)) { + char detail[256]; + std::snprintf(detail, sizeof(detail), "%s (%s path): the draw covered %u of %u pixels (expected %s) and the probe passed %u (expected %s)", test.what, path, drawn, Harness::Covered, test.drawn ? "all" : "none", passed, test.covered ? "all" : "none"); + throw std::runtime_error(detail); + } + ++ran; + } + } + // Scale and offset reach the vertex shader through the draw's push block, so two draws whose depth remap + // differs only in them build one pipeline between them: the second finds it in the store, and both + // store the depth they should. + { + constexpr Transform widerScale{false, 4.0f, -1.5f, 0.0f, 1.0f}; + Require(surfaces + 2u <= Depths.size(), "the test ran out of depth surfaces"); + const auto depthA = static_cast(reinterpret_cast(Depths[surfaces++].data())); + const auto depthB = static_cast(reinterpret_cast(Depths[surfaces++].data())); + const auto first = Draw(*device, depthA, widened, true, 0.5f + Step / 2.0f, true); + const auto before = AgcDriver::Graphics::PipelineStoreTotals(); + const auto second = Draw(*device, depthB, widerScale, true, 0.5f + Step / 4.0f, true); + const auto after = AgcDriver::Graphics::PipelineStoreTotals(); + Require(first == Harness::Covered && second == Harness::Covered, "a draw with a remapped depth did not cover the target"); + Require(after.misses == before.misses && after.hits > before.hits, "a draw that differs only in the depth scale and offset built another pipeline"); + for (const auto depth : {depthA, depthB}) { + Require(Draw(*device, depth, Plain, false, 0.5f, false) == Harness::Covered, "a depth remapped by a shared pipeline was stored below 0.5 + 1/64"); + Require(Draw(*device, depth, Plain, false, 0.5f + 2.0f * Step, false) == 0u, "a depth remapped by a shared pipeline was stored above 0.5 + 1/64"); + } + ++ran; + } + // A remapped vertex shader with no push data of its own, and a fragment shader with none: the pipeline's + // push stages must still include the vertex stage (a non-GPL layout names them), or the scale and offset + // reach no one. + { + Require(surfaces < Depths.size(), "the test ran out of depth surfaces"); + const auto depth = static_cast(reinterpret_cast(Depths[surfaces++].data())); + const auto drawn = Draw(*device, depth, widened, true, 0.5f + Step / 2.0f, true, true); + Require(drawn == Harness::Covered, "a fetchless remapped vertex shader did not cover the target"); + Require(Draw(*device, depth, Plain, false, 0.5f, false) == Harness::Covered, "a fetchless remapped vertex shader stored a depth below 0.5 + 1/64, so its scale and offset did not arrive"); + Require(Draw(*device, depth, Plain, false, 0.5f + 2.0f * Step, false) == 0u, "a fetchless remapped vertex shader stored a depth above 0.5 + 1/64"); + ++ran; + } + // On a device without VK_EXT_depth_range_unrestricted the target remaps every vertex shader, so an ordinary + // in-range draw (mode 0) with no push data of its own still carries the remap's push block: it must validate + // and draw, from a direct build and from a prepared one. + for (const bool prepared : {false, true}) { + Require(surfaces < Depths.size(), "the test ran out of depth surfaces"); + const auto depth = static_cast(reinterpret_cast(Depths[surfaces++].data())); + const auto drawn = Draw(*device, depth, Plain, false, 0.5f + Step / 2.0f, true, true, true, prepared); + Require(drawn == Harness::Covered, prepared ? "a fetchless mode 0 vertex shader on a remapping target, prepared, did not cover the target" : "a fetchless mode 0 vertex shader on a remapping target did not cover the target"); + Require(Draw(*device, depth, Plain, false, 0.5f, false) == Harness::Covered && Draw(*device, depth, Plain, false, 0.5f + 2.0f * Step, false) == 0u, "a fetchless mode 0 vertex shader on a remapping target stored the wrong depth"); + ++ran; + } + std::printf("depth range remap draw tests passed (%zu cases, unrestricted path %s)\n", ran, context.depthRangeUnrestricted ? "compared" : "absent"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp index ec58a19d3a..27236f4199 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -1,6 +1,9 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" @@ -29,6 +32,7 @@ using namespace AgcDriver::Graphics; constexpr std::uint32_t Side = 64; constexpr std::size_t BlockBytes = 65536; alignas(4096) std::array Storage{}; +alignas(4096) std::array CopyStorage{}; constexpr std::uint32_t Format32Float = 22; constexpr std::uint32_t Format8Uint = 5; @@ -109,6 +113,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 +204,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); @@ -244,9 +255,117 @@ void Run(Context context, std::uint8_t* block) { ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredStencil, 0)); }, "storage image access to depth/stencil surface", "a two-layer 2D array view of the stencil plane was accepted"); layeredDepth.depthOrLastArray = 1; ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "with a layer pitch of", "a two-layer 2D array view of the depth plane with a layer pitch other than the depth slice size was accepted"); - layeredDepth.depthOrLastArray = 0; layeredDepth.baseArray = 1; - ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "through a 2D array view of slices 1-0", "a 2D array view of layer 1 of the depth plane was accepted"); + ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "with a layer pitch of", "a 2D array view of layer 1 of the depth plane with a layer pitch other than the depth slice size was accepted"); + const auto own = StorageTexture::Create(context, *context.detiler, PlaneResource(address, Format32Float), 0); + const std::array eighth{std::bit_cast(0.125f), std::bit_cast(0.125f), std::bit_cast(0.125f), std::bit_cast(0.125f)}; + Require(own->FillClear(eighth, StorageTexture::WholeImage, refusal), refusal != nullptr ? refusal : "the storage image could not be filled"); + Require(DepthSurfaceStoreBack(context, *own), "a storage image over the depth plane that the depth surface did not hand out was not stored back"); + submit(recorder); + const auto reread = DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0); + Require(reread != nullptr, "the depth plane has no storage image"); + submit(recorder); + readBack(*reread); + Require(CountFloats(block, 0.125f) == Side * Side, "a storage image written over the depth plane did not reach the depth surface"); + Require(!DepthSurfaceStoreBack(context, *StorageTexture::Create(context, *context.detiler, PlaneResource(address, Format32Float, Side / 2), 0)), "a storage image of another extent over the depth memory was stored into the depth surface"); +} + +void RunPairings(Context context, std::uint8_t* block) { + const auto address = reinterpret_cast(block); + const auto submit = [&](Recorder& recorder) { + recorder.Submit(); + Check(context.Function("vkQueueWaitIdle")(context.queue), "vkQueueWaitIdle"); + }; + Recorder recorder(context); + recorder.Activate(); + const auto readBack = [&](StorageTexture& texture) { + texture.MarkDirty(); + texture.WriteBack(); + submit(recorder); + }; + const DepthTarget first{address, address + BlockBytes / 2, {Side, Side}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.25f, 7}; + const DepthTarget second{address, address + BlockBytes / 2 + 8192, {Side, Side}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.5f, 3}; + Require(DepthSurfaceView(context, first) != VK_NULL_HANDLE && DepthSurfaceView(context, second) != VK_NULL_HANDLE, "the two pairings of the depth plane were not created"); + const auto depth = DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0); + Require(depth != nullptr, "the depth plane has no storage image"); + submit(recorder); + const std::array pattern{std::bit_cast(0.75f), std::bit_cast(0.75f), std::bit_cast(0.75f), std::bit_cast(0.75f)}; + const char* refusal = nullptr; + Require(depth->FillClear(pattern, StorageTexture::WholeImage, refusal), refusal != nullptr ? refusal : "the storage image could not be filled"); + Require(DepthSurfaceStoreBack(context, *depth), "the newest pairing's storage image was not stored back"); + submit(recorder); + const auto reread = DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0); + Require(reread != nullptr, "the depth plane has no storage image"); + submit(recorder); + readBack(*reread); + Require(CountFloats(block, 0.75f) == Side * Side, "a storage image stored back went into an older pairing instead of the pairing that handed it out"); + auto layered = PlaneResource(address, Format32Float); + layered.dimension = TextureDimension::k2DArray; + Require(DepthSurfaceStorage(context, {}, layered, 0) != nullptr, "a one-layer 2D array view of the depth plane was not taken"); + ExpectFailure([&] { static_cast(DepthSurfaceStoreBack(context, *depth)); }, "through a view that another view of the plane", "a store back through a replaced view was taken by an older pairing that covers the plane"); +} + +void DepthCopyCase(Context context, std::uint8_t* block) { + const auto colorAddress = reinterpret_cast(block); + const auto depthAddress = colorAddress + BlockBytes / 4; + Recorder recorder(context); + recorder.Activate(); + const auto submit = [&] { + recorder.Submit(); + Check(context.Function("vkQueueWaitIdle")(context.queue), "vkQueueWaitIdle"); + }; + ColorTarget color{}; + color.address = colorAddress; + color.surfaceAddress = colorAddress; + color.extent = {Side, Side}; + color.surfaceExtent = color.extent; + color.format = VK_FORMAT_R32_SFLOAT; + color.elementBytes = 4; + color.tileMode = ColorTileMode::RenderTarget; + color.bytes = ColorTargetLayout(Side, Side, ColorTileMode::RenderTarget, 4).Bytes(); + const auto copy = [&](std::uint32_t samples, std::uint32_t sample, VkExtent2D extent) { + RunDepthCopyPass(context, DepthCopyPass{depthAddress, extent, samples, sample, color}); + submit(); + const auto resident = CachedStorageSurface(context, SurfaceForTarget(color)); + resident->MarkDirty(); + resident->WriteBack(); + submit(); + }; + const auto countBlock = [&](float value) { + std::size_t count = 0; + for (std::size_t offset = 0; offset < BlockBytes; offset += sizeof(float)) { + float texel = 0; + std::memcpy(&texel, block + offset, sizeof(texel)); + if (texel == value) ++count; + } + return count; + }; + std::fill(block, block + BlockBytes, std::uint8_t{0}); + ExpectFailure([&] { copy(1, 0, {Side, Side}); }, "no draw rendered", "a DB depth copy from memory without a depth surface was accepted"); + const auto sampledCounts = context.limits.sampledImageDepthSampleCounts & context.limits.framebufferDepthSampleCounts; + const std::uint32_t samples = (sampledCounts & VK_SAMPLE_COUNT_2_BIT) != 0 ? 2u : (sampledCounts & VK_SAMPLE_COUNT_4_BIT) != 0 ? 4u : 0u; + if (samples != 0) { + Require(DepthSurfaceView(context, DepthTarget{depthAddress, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 0.375f, 0, 0, false, samples}) != VK_NULL_HANDLE, "the multisample depth surface was not created"); + for (std::uint32_t sample = 0; sample < samples; ++sample) { + std::fill(block, block + BlockBytes, std::uint8_t{0}); + copy(samples, sample, {Side, Side}); + Require(countBlock(0.375f) == Side * Side, "the DB depth copy of sample " + std::to_string(sample) + " of " + std::to_string(samples) + " did not store the cleared depth in every texel of the R32 float target"); + } + ExpectFailure([&] { copy(samples, samples, {Side, Side}); }, "does not have", "a DB depth copy of a sample past the last of a multisample surface was accepted"); + ExpectFailure([&] { copy(samples, 0, {Side / 2, Side / 2}); }, "of another size", "a DB depth copy of another extent than the depth surface was accepted"); + ClearDepthSurfaces(context.device); + } + if ((context.limits.sampledImageDepthSampleCounts & VK_SAMPLE_COUNT_2_BIT) == 0) { + Require(DepthSurfaceView(context, DepthTarget{depthAddress, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 0.375f, 0, 0, false, 2}) != VK_NULL_HANDLE, "the unsampleable two-sample depth surface was not created"); + ExpectFailure([&] { copy(2, 0, {Side, Side}); }, "cannot sample at its sample count", "a DB depth copy from an unsampleable two-sample depth surface was accepted"); + ClearDepthSurfaces(context.device); + std::puts("DB depth copy: unsupported two-sample source refused cleanly"); + } + Require(DepthSurfaceView(context, DepthTarget{depthAddress, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 0.625f, 0, 0, false, 1}) != VK_NULL_HANDLE, "the one-sample depth surface was not created"); + std::fill(block, block + BlockBytes, std::uint8_t{0}); + copy(1, 0, {Side, Side}); + Require(countBlock(0.625f) == Side * Side, "the DB depth copy of a one-sample depth surface did not store the cleared depth in every texel of the R32 float target"); + ExpectFailure([&] { copy(1, 1, {Side, Side}); }, "does not have", "a DB depth copy of a second sample of a one-sample surface was accepted"); } } @@ -269,6 +388,13 @@ int main() { { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); Run(context, block); + auto* copyBlock = CopyStorage.data() + (BlockBytes - reinterpret_cast(CopyStorage.data()) % BlockBytes) % BlockBytes; + GuestAllocations::Mutation().Add(copyBlock, BlockBytes, true, true, true); + DepthCopyCase(context, copyBlock); + ClearCachedTextures(context.device); + GuestAllocations::Mutation().Remove(copyBlock); + ClearDepthSurfaces(context.device); + RunPairings(context, block); ClearDepthSurfaces(context.device); } GuestAllocations::Mutation().Remove(block); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp index cf2517f292..4c2e019b69 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp @@ -124,14 +124,14 @@ std::array BufferDescriptor(const void* data, std::uint32_t by return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; } -std::array TextureDescriptor(const void* data, std::uint32_t format, std::uint32_t type = Type2D, std::uint32_t tile = TileLinear, std::uint32_t lastArray = 0) { +std::array TextureDescriptor(const void* data, std::uint32_t format, std::uint32_t type = Type2D, std::uint32_t tile = TileLinear, std::uint32_t lastArray = 0, std::uint32_t baseArray = 0) { const auto address = static_cast(reinterpret_cast(data)); return { static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), 0xfacu | (tile << 20u) | (type << 28u), - lastArray, 0u, 0u, 0u, + lastArray | (baseArray << 16u), 0u, 0u, 0u, }; } @@ -252,10 +252,10 @@ void ArrayViewCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, std: RequireRow(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = StencilEqual(9u)}), "stencil test against the one-layer 2D array view's stencil"); } -ShaderRecompiler::RecompileResult LayerProgram(AgcDriver::VulkanDevice& device, std::span code, std::span output, const std::uint8_t* layers, std::uint32_t layer) { +ShaderRecompiler::RecompileResult LayerProgram(AgcDriver::VulkanDevice& device, std::span code, std::span output, const std::uint8_t* layers, std::uint32_t layer, std::uint32_t baseArray) { std::vector userData(16, 0u); const auto buffer = BufferDescriptor(output.data(), static_cast(output.size_bytes())); - const auto texture = TextureDescriptor(layers, Format32Float, Type2DArray, TileZ64KBX, ArrayLayers - 1u); + const auto texture = TextureDescriptor(layers, Format32Float, Type2DArray, TileZ64KBX, ArrayLayers - 1u, baseArray); std::copy(buffer.begin(), buffer.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); userData[12] = layer; @@ -263,8 +263,8 @@ ShaderRecompiler::RecompileResult LayerProgram(AgcDriver::VulkanDevice& device, return Compile(device, code, userData); } -void StoreLayer(AgcDriver::VulkanDevice& device, std::span layers, std::span output, std::uint32_t layer) { - const auto store = LayerProgram(device, StoreLayerCode, output, layers.data(), layer); +void StoreLayer(AgcDriver::VulkanDevice& device, std::span layers, std::span output, std::uint32_t layer, std::uint32_t baseArray = 0) { + const auto store = LayerProgram(device, StoreLayerCode, output, layers.data(), layer, baseArray); device.Dispatch(store, 1, 1, 1, {}, reinterpret_cast(StoreLayerCode.data())); device.WaitIdle(); } @@ -313,8 +313,8 @@ void LayeredArrayViewCase(AgcDriver::VulkanDevice& device, const Shaders& shader StoreLayer(device, layers, output, 1); RequireRow(Draw(device, shaders, second, {.depthCompare = VK_COMPARE_OP_GREATER}), "depth test against the values a 2D array storage view stored to the second layer's surface"); Require(Draw(device, shaders, first, {.depthCompare = VK_COMPARE_OP_GREATER}) == 0, "a store to the second layer changed the first layer's surface"); - StoreLayer(device, layers, output, 2); - RequireRow(Draw(device, shaders, second, {.depthCompare = VK_COMPARE_OP_GREATER}), "depth test against the second layer's surface after a store to the third layer"); + StoreLayer(device, layers, output, 0, 2); + RequireRow(Draw(device, shaders, second, {.depthCompare = VK_COMPARE_OP_GREATER}), "depth test against the second layer's surface after a store to the third layer through a view from base slice 2"); AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(layers.data()), layers.size(), nullptr, "test"); device.WaitIdle(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp index 91db433d86..dbd774f671 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp @@ -17,6 +17,10 @@ using DepthFastClearHarness::Width; alignas(4096) std::array Depth{}; alignas(256) std::array Output{}; + +std::uintptr_t DepthAddress() { + return (reinterpret_cast(Depth.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}; +} alignas(256) constexpr std::array LoadCode{ 0x7e3c0280u, 0x7e3e0280u, 0x7e140280u, 0xf0001108u, 0x00010a1eu, 0xbf8c3f70u, 0xf0201108u, 0x00010a1eu, 0x34060082u, 0xe0701000u, 0x80000a03u, 0xbf810000u, 0xbf810000u, @@ -26,14 +30,14 @@ alignas(256) constexpr std::array SampleCode{ 0x00610a1eu, 0x34060082u, 0xe0701000u, 0x80000a03u, 0xbf810000u, }; -void Read(AgcDriver::VulkanDevice& device, std::uint32_t format, std::uint32_t width, bool sampled, float expected) { +void Read(AgcDriver::VulkanDevice& device, std::uint32_t format, std::uint32_t width, bool sampled, float expected, std::uint32_t height = Height, std::uint32_t swizzle = 0u) { Output.fill(0xdeadbeefu); const auto output = reinterpret_cast(Output.data()); - const auto depth = reinterpret_cast(Depth.data()); + const auto depth = DepthAddress(); const std::array data{ static_cast(output), static_cast(output >> 32u), sizeof(Output), 0x31016facu, static_cast(depth >> 8u), static_cast(depth >> 40u) | (format << 20u) | (((width - 1u) & 3u) << 30u), - ((width - 1u) >> 2u) | ((Height - 1u) << 14u), 0x90000facu, 0u, 0u, 0u, 0u, + ((width - 1u) >> 2u) | ((height - 1u) << 14u), 0x90000facu | (swizzle << 20u), 0u, 0u, 0u, 0u, 0x92u, 0u, 0u, 0u, }; const auto code = sampled ? std::span(SampleCode) : std::span(LoadCode); @@ -60,7 +64,7 @@ int main() { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; const auto shaders = DepthFastClearHarness::Compile(*device); - const auto address = reinterpret_cast(Depth.data()); + const auto address = DepthAddress(); for (const auto format : {VK_FORMAT_D32_SFLOAT, VK_FORMAT_D16_UNORM}) { AgcDriver::Graphics::ClearDepthSurfaces(device->Device()); Depth.fill(0.25f); @@ -68,6 +72,14 @@ int main() { Require(DepthFastClearHarness::Draw(*device, shaders, surface, {.depthWrite = true}) == DepthFastClearHarness::Covered, "depth producer did not cover the surface"); Read(*device, format == VK_FORMAT_D16_UNORM ? 7u : 22u, Width, true, 0.5f); Read(*device, 22u, format == VK_FORMAT_D16_UNORM ? Width : Width / 2u, false, 0.25f); + if (format != VK_FORMAT_D32_SFLOAT) continue; + Read(*device, 22u, Width, true, 0.5f); + Read(*device, 20u, Width, false, 0.25f, Height, 25u); + Read(*device, 22u, Width, true, 0.25f); + Require(DepthFastClearHarness::Draw(*device, shaders, surface, {.depthTest = false, .depthWrite = true}) == DepthFastClearHarness::Covered, "depth producer did not cover the surface"); + Read(*device, 22u, Width, true, 0.5f); + Read(*device, 22u, 128u, false, 0.25f, 128u); + Read(*device, 22u, Width, true, 0.25f); } std::puts("depth storage reuse GPU tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceFromStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceFromStorage.cpp new file mode 100644 index 0000000000..fdd4bbe36a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceFromStorage.cpp @@ -0,0 +1,205 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.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 "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::HtileBytes; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Width; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Format16Unorm = 7; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t TileDepth64KB = 0x18; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Type2DArray = 13; +constexpr std::size_t PlaneBytes = 65536; +constexpr std::size_t BlockBytes = 9 * PlaneBytes; +constexpr std::uint32_t RowPasses = 15; + +alignas(256) constexpr std::array StoreRowCode{ + 0x7e280d00, 0x102828ff, 0x3cf80000, 0x7e3c0300, 0x7e3e0280, 0xf0201108, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array StoreRowLayerCode{ + 0x7e280d00, 0x102828ff, 0x3cf80000, 0x7e3c0300, 0x7e3e0280, 0x7e400280, 0xf0201128, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "depth surface from storage: cannot allocate the guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(block), BlockBytes); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Plane(std::size_t index) { return block + index * PlaneBytes; } + +private: + std::uint8_t* block = nullptr; +}; + +std::uint64_t Address(const std::uint8_t* plane) { + return static_cast(reinterpret_cast(plane)); +} + +std::array StorageView(std::uint64_t address, std::uint32_t format, std::uint32_t layers) { + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (TileDepth64KB << 20u) | ((layers > 1u ? Type2DArray : Type2D) << 28u), + layers - 1u, 0u, 0u, 0u, + }; +} + +void Fill(std::uint8_t* plane, VkFormat format) { + if (format == VK_FORMAT_D16_UNORM) { + const std::uint16_t quarter = 0x4000u; + for (std::size_t offset = 0; offset < PlaneBytes; offset += sizeof(quarter)) std::memcpy(plane + offset, &quarter, sizeof(quarter)); + return; + } + const float quarter = 0.25f; + for (std::size_t offset = 0; offset < PlaneBytes; offset += sizeof(quarter)) std::memcpy(plane + offset, &quarter, sizeof(quarter)); +} + +void StoreRow(AgcDriver::VulkanDevice& device, std::uint8_t* plane, VkFormat format, std::uint32_t layers) { + std::vector userData(16, 0u); + const auto view = StorageView(Address(plane), format == VK_FORMAT_D16_UNORM ? Format16Unorm : Format32Float, layers); + std::copy(view.begin(), view.end(), userData.begin() + 4); + const std::span code = layers > 1u ? std::span(StoreRowLayerCode) : std::span(StoreRowCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto program = ShaderRecompiler::Recompile(request); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +DepthFastClearHarness::Surface Plane(std::uint8_t* plane, VkFormat format, std::uint64_t htile = 0) { + return {Address(plane), 0, htile, false, format}; +} + +std::vector failures; + +void RequireCount(std::uint32_t covered, std::uint32_t expected, const std::string& what) { + if (covered != expected) failures.push_back(what + ": " + std::to_string(covered) + " pixels passed, expected " + std::to_string(expected)); +} + +void NewSurfaceCase(AgcDriver::VulkanDevice& device, const DepthFastClearHarness::Shaders& shaders, std::uint8_t* plane, VkFormat format, bool flushed, const std::string& what, std::uint32_t layers = 1) { + for (std::uint32_t layer = 0; layer < layers; ++layer) Fill(plane + layer * PlaneBytes, format); + StoreRow(device, plane, format, layers); + if (layers > 1u) { + Require(AgcDriver::Graphics::StorageTexture::FindPending(Address(plane), PlaneBytes) != nullptr && AgcDriver::Graphics::StorageTexture::FindLive(Address(plane), PlaneBytes) == nullptr, what + ": the stored row is not pending in a storage image larger than the plane, or a cached image of the plane's size holds it"); + } else if (flushed) { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(plane), PlaneBytes, nullptr, "test"); + device.WaitIdle(); + Require(AgcDriver::Graphics::StorageTexture::FindPending(Address(plane), PlaneBytes) == nullptr && AgcDriver::Graphics::StorageTexture::FindLive(Address(plane), PlaneBytes) != nullptr, what + ": the stored row is not held by a flushed, live storage image"); + } else { + Require(AgcDriver::Graphics::StorageTexture::FindPending(Address(plane), PlaneBytes) != nullptr, what + ": the stored row is not pending in a storage image"); + } + RequireCount(DepthFastClearHarness::Draw(device, shaders, Plane(plane, format), {}), RowPasses, what); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto shaders = DepthFastClearHarness::Compile(*device); + GuestBlock block; + NewSurfaceCase(*device, shaders, block.Plane(0), VK_FORMAT_D32_SFLOAT, false, "a new D32 depth surface over a pending R32F storage write"); + NewSurfaceCase(*device, shaders, block.Plane(1), VK_FORMAT_D16_UNORM, false, "a new D16 depth surface over a pending R16 unorm storage write"); + NewSurfaceCase(*device, shaders, block.Plane(2), VK_FORMAT_D32_SFLOAT, true, "a new D32 depth surface over a flushed R32F storage write"); + NewSurfaceCase(*device, shaders, block.Plane(7), VK_FORMAT_D32_SFLOAT, false, "a new D32 depth surface over a pending R32F storage write through a two-layer 2D array view", 2); + + auto* cleared = block.Plane(3); + const auto htile = Address(block.Plane(4)); + Fill(cleared, VK_FORMAT_D32_SFLOAT); + StoreRow(*device, cleared, VK_FORMAT_D32_SFLOAT, 1); + AgcDriver::Graphics::NoteHtileDepthClear(htile, DepthFastClearHarness::HtileBytes); + RequireCount(DepthFastClearHarness::Draw(*device, shaders, Plane(cleared, VK_FORMAT_D32_SFLOAT, htile), {}), Covered, "a new D32 depth surface whose HTILE was cleared after a storage write"); + + auto* stenciled = block.Plane(5); + const DepthFastClearHarness::Surface depthStencil{Address(stenciled), Address(block.Plane(6)), 0, false, VK_FORMAT_D32_SFLOAT_S8_UINT}; + constexpr VkStencilOpState equalZero{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, 0u}; + Fill(stenciled, VK_FORMAT_D32_SFLOAT); + StoreRow(*device, stenciled, VK_FORMAT_D32_SFLOAT, 1); + RequireCount(DepthFastClearHarness::Draw(*device, shaders, depthStencil, {}), RowPasses, "a new D32S8 depth surface over a pending R32F storage write"); + RequireCount(DepthFastClearHarness::Draw(*device, shaders, depthStencil, {.depthTest = false, .stencilTest = true, .stencil = equalZero}), Covered, "the stencil plane of a new D32S8 depth surface that took a storage write, tested EQUAL to its clear value 0"); + + if (!failures.empty()) { + std::string text; + for (const auto& failure : failures) text += failure + "\n"; + throw std::runtime_error(text); + } + std::puts("depth surface from storage 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/DepthSurfaceLastWriter.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceLastWriter.cpp new file mode 100644 index 0000000000..b7f809f6ee --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceLastWriter.cpp @@ -0,0 +1,245 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +constexpr std::uint64_t DepthAddress = 0x7f0000000000ull; +constexpr VkExtent2D Extent{64, 64}; +constexpr std::uint32_t Format32Float = 22; + +class Device { +public: + Device() { +#ifdef _WIN32 + library = SDL_LoadObject("vulkan-1.dll"); +#else + library = SDL_LoadObject("libvulkan.so.1"); +#endif + Require(library != nullptr, "cannot load Vulkan"); + try { + instanceProc = reinterpret_cast(SDL_LoadFunction(library, "vkGetInstanceProcAddr")); + Require(instanceProc != nullptr, "missing Vulkan instance resolver"); + VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + application.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + info.pApplicationInfo = &application; + Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); + std::uint32_t count = 0; + const auto enumerate = function("vkEnumeratePhysicalDevices"); + Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices"); + Require(count != 0, "no Vulkan device"); + std::vector devices(count); + Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); + context.physical = devices.front(); + const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); + queues(context.physical, &count, nullptr); + std::vector families(count); + queues(context.physical, &count, families.data()); + std::uint32_t family = 0; + while (family < count && (families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0) ++family; + Require(family < count, "no Vulkan graphics queue"); + const float priority = 1; + VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue.queueFamilyIndex = family; + queue.queueCount = 1; + queue.pQueuePriorities = &priority; + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + device.queueCreateInfoCount = 1; + device.pQueueCreateInfos = &queue; + Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + context.deviceProc = function("vkGetDeviceProcAddr"); + context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); + function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + context.limits = properties.limits; + context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); + VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + pool.queueFamilyIndex = family; + Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); + } catch (...) { + release(); + throw; + } + } + + ~Device() { release(); } + const Context& GetContext() const { return context; } + +private: + template + TFunction function(const char* name) const { + const auto result = reinterpret_cast(instanceProc(instance, name)); + Require(result != nullptr, name); + return result; + } + + void release() noexcept { + if (context.device != VK_NULL_HANDLE) ClearDepthSurfaces(context.device); + if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); + context.bufferPool.reset(); + if (context.device != VK_NULL_HANDLE) function("vkDestroyDevice")(context.device, nullptr); + if (instance != VK_NULL_HANDLE) function("vkDestroyInstance")(instance, nullptr); + if (library != nullptr) SDL_UnloadObject(library); + } + + void* library = nullptr; + PFN_vkGetInstanceProcAddr instanceProc = nullptr; + VkInstance instance = VK_NULL_HANDLE; + Context context{}; +}; + +constexpr std::size_t Block = 65536; + +void* AllocateWatched(std::size_t bytes) { +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the watched block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + Require(AgcDriver::GuestMemory::Watched(reinterpret_cast(block), bytes), "the test block is not watched"); + return block; +} + +DepthTarget Depth(std::uint64_t address = DepthAddress, VkExtent2D extent = Extent) { + return {address, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; +} + +GuestTextureResource View(std::uint32_t width, std::uint32_t height, std::uint64_t address = DepthAddress, std::uint32_t levels = 1) { + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = width; + resource.height = height; + resource.mipCount = levels; + resource.lastLevel = levels - 1; + resource.dimension = TextureDimension::k2D; + resource.format = Format32Float; + return resource; +} + +constexpr VkComponentMapping Identity{VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + +std::shared_ptr Sample(const Context& context, const GuestTextureResource& resource) { + const std::array words{0, 0, 0, 0, resource.width, resource.height, resource.lastLevel, 0}; + return DepthSurfaceTexture(context, words, resource, Identity); +} + +bool Refused(const Context& context, const GuestTextureResource& resource) { + try { + Sample(context, resource); + } catch (const std::runtime_error& error) { + return std::string(error.what()).find("is not implemented") != std::string::npos; + } + return false; +} + +void Run(const Context& context) { + const auto mips = View(Extent.width, Extent.height, DepthAddress, 2); + const auto exact = View(Extent.width, Extent.height); + const auto depthBytes = DepthSliceBytes(Extent, 4); + + DepthSurfaceView(context, Depth()); + Require(DepthSurfaceAt(DepthAddress), "a bound depth surface is not found at its address"); + Require(Refused(context, mips), "depth, then a view with two levels: the live depth surface did not refuse it"); + Require(Sample(context, exact) != nullptr, "depth, then its own view: the depth surface did not serve it"); + + RetireDepthSurfaces(context.device, DepthAddress + depthBytes, 0x10000); + Require(Refused(context, mips), "a write past the depth plane retired the depth surface"); + + RetireDepthSurfaces(context.device, DepthAddress + 0x100, 0x1000); + Require(!DepthSurfaceAt(DepthAddress), "a color target over the depth plane left the depth surface live"); + Require(Sample(context, mips) == nullptr, "depth, then a color target, then a view with two levels: the view was not left to guest memory"); + Require(Sample(context, exact) == nullptr, "depth, then a color target, then its own view: the retired depth surface served it"); + + DepthSurfaceView(context, Depth()); + Require(DepthSurfaceAt(DepthAddress), "binding the depth target again did not bring the depth surface back"); + Require(Refused(context, mips), "depth, color target, depth again, then a view with two levels: the live depth surface did not refuse it"); + + if (!AgcDriver::GuestMemory::WriteWatched()) { + Require(Refused(context, mips), "without write watching, a view with two levels was not refused"); + std::puts("guest memory is not write-watched here: the CPU write order is not exercised"); + return; + } + + auto* memory = static_cast(AllocateWatched(2 * Block)); + const auto watched = reinterpret_cast(memory); + Require(depthBytes <= 2 * Block, "the depth plane does not fit the watched block"); + const auto watchedMips = View(Extent.width, Extent.height, watched, 2); + const auto watchedExact = View(Extent.width, Extent.height, watched); + AgcDriver::GuestMemory::CollectWrites(watched, 2 * Block); + + DepthSurfaceView(context, Depth(watched)); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(Refused(context, watchedMips), "depth, then a view with two levels: the live depth surface over watched memory did not refuse it"); + + memory[8] = 0x5a; + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(Sample(context, watchedExact) != nullptr, "depth, then a CPU write, then its own view: the depth surface did not serve it"); + Require(Sample(context, watchedMips) == nullptr, "depth, then a CPU write, then a view with two levels: the view was not left to guest memory"); + Require(!DepthSurfaceAt(watched), "depth, then a CPU write, then a view with two levels: the depth surface stayed live"); + + memory[16] = 0xa5; + AgcDriver::GuestMemory::BumpCollectEpoch(); + DepthSurfaceView(context, Depth(watched)); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(Refused(context, watchedMips), "a CPU write, then depth, then a view with two levels: the live depth surface did not refuse it"); + + AgcDriver::GuestMemory::MarkWritten(watched + Block, 4); + Require(Refused(context, watchedMips), "a driver store past the depth plane retired the depth surface"); + AgcDriver::GuestMemory::MarkWritten(watched + 0x100, 4); + Require(Sample(context, watchedMips) == nullptr, "depth, then a driver store, then a view with two levels: the view was not left to guest memory"); +} + +} + +int main() { + try { + std::unique_ptr device; + try { + device = std::make_unique(); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return VulkanTestSkipped; + } + Run(device->GetContext()); + std::puts("depth surface last writer tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDataHitWords.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDataHitWords.cpp new file mode 100644 index 0000000000..c0d7cc48b9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDataHitWords.cpp @@ -0,0 +1,152 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint32_t Lanes = 32; +constexpr std::uint32_t BufferBytes = Lanes * 4u; +constexpr std::uint32_t ComputeQueue = 0x20; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Unwritten = 0xcafebabeu; + +std::uint32_t* Constants = nullptr; +std::uint32_t* Input = nullptr; +std::uint32_t* Output = nullptr; + +alignas(256) constexpr std::array AddressCode{0xf4000200u, 0xfa000000u, 0x34020082u, 0xdc308000u, 0x03020001u, 0xbf8cc07fu, 0x7e040208u, 0x4a040481u, 0xbf8c3f70u, 0x4a040503u, 0xe0701000u, 0x80010201u, 0xbf810000u}; + +std::uint32_t* NewBlock() { +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + if (block == nullptr) throw std::runtime_error("dispatch data hit words: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + return block; +} + +void check(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("dispatch data hit words: " + what); +} + +template +void WaitUntil(Ready ready, const std::string& what) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (!ready()) { + if (std::chrono::steady_clock::now() >= deadline) throw std::runtime_error("dispatch data hit words: timed out waiting for " + what); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::vector Dispatch() { + const auto codeAddress = reinterpret_cast(AddressCode.data()); + const auto constants = reinterpret_cast(Constants); + const auto input = reinterpret_cast(Input); + const auto output = BufferDescriptor(Output, BufferBytes); + return { + 0xc0027600u, 0x20cu, static_cast(codeAddress >> 8u), static_cast(codeAddress >> 40u), + 0xc0037600u, 0x207u, Lanes, 1u, 1u, + 0xc0017600u, 0x213u, 8u << 1u, + 0xc0087600u, 0x240u, static_cast(constants), static_cast(constants >> 32u), static_cast(input), static_cast(input >> 32u), output[0], output[1], output[2], output[3], + 0xc0031500u, 1u, 1u, 1u, 0x8041u, + }; +} + +void SetConstant(std::uint32_t value) { + for (std::uint32_t lane = 0; lane < Lanes; ++lane) Constants[lane] = value; + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Constants), BufferBytes); +} + +void CheckOutput(std::uint32_t expected, const std::string& what) { + WaitUntil([] { + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output), BufferBytes); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + if (std::atomic_ref(Output[lane]).load(std::memory_order_acquire) == Unwritten) return false; + } + return true; + }, what + " to store"); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) check(Output[lane] == expected, what + ": lane " + std::to_string(lane) + " is " + std::to_string(Output[lane]) + ", expected " + std::to_string(expected)); +} + +void Round(std::uint32_t value, const std::string& what) { + SetConstant(value); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) Output[lane] = Unwritten; + auto words = Dispatch(); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + check(sceAgcDriverSubmitAcb(ComputeQueue, &packet) == 0, "submission failed"); + AgcDriverWaitIdle_nid_postfix(); + CheckOutput(value + 1u, what); +} + +} + +int main() { + try { + { + const auto probe = OpenVulkanTestDevice(); + if (!probe) return VulkanTestSkipped; + } + + Shader shader{}; + shader.file_header = 0x34333231; + shader.version = 0x18; + shader.header_size = sizeof(Shader); + shader.shader_size = sizeof(AddressCode); + shader.code = AddressCode.data(); + AgcDriverRegisterShader_nid_postfix(&shader); + + Constants = NewBlock(); + Input = NewBlock(); + Output = NewBlock(); + + Round(0x11112222u, "the first dispatch"); + const std::array values{0x33334444u, 0x55556666u, 0x7a7b7c7du, 0x0f1e2d3cu}; + for (std::size_t round = 0; round < values.size(); ++round) { + const auto mismatches = AgcDriver::Graphics::SnapshotMismatches(); + const auto what = "data hit " + std::to_string(round + 1) + ", resolved ahead on the compute queue"; + Round(values[round], what); + const auto added = AgcDriver::Graphics::SnapshotMismatches() - mismatches; + check(added == 0, what + ": its snapshots differ from the registered memory serving them (" + std::to_string(added) + " snapshot mismatches)"); + } + std::printf("dispatch data hit words: %zu data hits resolved ahead read their own live words\n", values.size()); + + LibcRunShutdown_nid_postfix(); + std::puts("dispatch data hit words tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { LibcRunShutdown_nid_postfix(); } catch (...) {} + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDifferingValueSets.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDifferingValueSets.cpp new file mode 100644 index 0000000000..374159ffe1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDifferingValueSets.cpp @@ -0,0 +1,141 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::Driver; + +constexpr std::array Code{0xf4080100u, 0xfa000000u, 0xbf8cc07fu, 0x7e020280u, 0xe0702000u, 0x80010100u, 0xbf810000u}; +constexpr std::size_t SrtOffset = 4096; +constexpr std::size_t OutputOffset = 8192; +constexpr std::size_t BlockBytes = 65536; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + void* mapped = mmap(nullptr, BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + block = static_cast(mapped); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "dispatch differing value sets: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint64_t At(std::size_t offset) const { return static_cast(reinterpret_cast(block + offset)); } + std::uint8_t* Bytes(std::size_t offset) const { return block + offset; } + +private: + std::uint8_t* block = nullptr; +}; + +std::vector Pm4(std::uint32_t opcode, std::initializer_list body) { + std::vector words{0xc0000000u | (static_cast(body.size() - 1) << 16u) | (opcode << 8u)}; + words.insert(words.end(), body); + return words; +} + +void WriteSrt(const GuestBlock& block, std::uint32_t records) { + const auto address = block.At(OutputOffset); + const std::array words{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), records, 0x00014204u}; + std::memcpy(block.Bytes(SrtOffset), words.data(), sizeof(words)); +} + +void Dispatch(const GuestBlock& block) { + const auto kernel = block.At(0); + const auto srt = block.At(SrtOffset); + std::vector words; + for (const auto& packet : {Pm4(0x76, {0x20c, static_cast(kernel >> 8u)}), Pm4(0x76, {0x20d, static_cast(kernel >> 40u) & 0xffu}), Pm4(0x76, {0x207, 64}), Pm4(0x76, {0x208, 1}), Pm4(0x76, {0x209, 1}), Pm4(0x76, {0x213, 2u << 1u}), Pm4(0x76, {0x240, static_cast(srt)}), Pm4(0x76, {0x241, static_cast(srt >> 32u)}), Pm4(0x15, {1, 1, 1, 0x41})}) { + words.insert(words.end(), packet.begin(), packet.end()); + } + Packet dcb{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&dcb) == 0, "dispatch differing value sets: the dispatch submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +struct Totals { + std::uint64_t classified = 0, matched = 0; + std::uint64_t lastClassified = 0, lastMatched = 0; + + void Add() { + const auto counters = Driver::Get().DispatchCacheCounters(); + classified += counters.differingClassified >= lastClassified ? counters.differingClassified - lastClassified : counters.differingClassified; + matched += counters.differingMatchedPrior >= lastMatched ? counters.differingMatchedPrior - lastMatched : counters.differingMatchedPrior; + lastClassified = counters.differingClassified; + lastMatched = counters.differingMatchedPrior; + } +}; + +void AlternatingValueSets(const GuestBlock& block) { + Totals totals; + totals.Add(); + for (std::uint32_t dispatch = 0; dispatch < 5; ++dispatch) { + WriteSrt(block, dispatch % 2u == 0u ? 64u : 65u); + Dispatch(block); + totals.Add(); + } + std::printf("alternating value sets: %llu differing dispatches classified, %llu matched one of the last 3 value sets\n", static_cast(totals.classified), static_cast(totals.matched)); + Require(totals.classified == 4, "the four dispatches after the first were not all classified as differing (" + std::to_string(totals.classified) + ")"); + Require(totals.matched == 3, "the three dispatches bringing a value set seen before did not all match it (" + std::to_string(totals.matched) + ")"); +} + +} + +int main() { + try { +#ifdef _WIN32 + _putenv_s("APS5_PROFILE_DRAW", "1"); + _putenv_s("APS5_DISPATCH_VARIANTS", "1"); +#else + setenv("APS5_PROFILE_DRAW", "1", 1); + setenv("APS5_DISPATCH_VARIANTS", "1", 1); +#endif + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + GuestBlock block; + std::memcpy(block.Bytes(0), Code.data(), sizeof(Code)); + AlternatingValueSets(block); + AgcDriverShutdown_nid_postfix(); + std::puts("dispatch differing value sets: the differing statistics recognize a value set they saw before"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { AgcDriverShutdown_nid_postfix(); } catch (...) {} + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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..a82e34a36a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp @@ -0,0 +1,252 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp" +#include "Optimization/SrtWalker.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 SplitRunsNameTheirSharedWords() { + alignas(16) static std::array data{1u, 2u, 3u, 4u}; + auto table = std::make_shared(); + table->words = {5u, 6u, 7u, 8u}; + const auto dataAddress = static_cast(reinterpret_cast(data.data())); + const auto tableAddress = dataAddress + 0x1000u; + const std::array regions{{{dataAddress, std::as_bytes(std::span(data))}, {tableAddress, std::as_bytes(std::span(table->words))}}}; + ShaderRecompiler::SrtUserDataUse use; + use.asBase = 0x3u; + use.reads = {{dataAddress, 0}, {dataAddress + 8u, 1}}; + const std::array, 1> supplied{table}; + AgcDriver::DriverDetail::DispatchVariant variant; + AgcDriver::DriverDetail::Driver::tagVariantRuns(variant, regions, use, true, 0, supplied); + Require(variant.runs.size() > regions.size(), "two user-data bases into one region did not split it into several runs"); + Require(variant.shared.size() == 1u && variant.shared.front().regions == 1u, "the table region's words were not recorded as one shared run"); + const auto& shared = variant.shared.front(); + std::size_t first = 0; + for (std::size_t run = 0; run < shared.region && run < variant.runs.size(); ++run) first += static_cast((variant.runs[run].second - variant.runs[run].first) / sizeof(std::uint32_t)); + Require(shared.region < variant.runs.size() && variant.runs[shared.region].first == tableAddress && first == shared.first, "the shared words of a variant whose runs were split for relocation name run " + std::to_string(shared.region) + " of " + std::to_string(variant.runs.size()) + ", not the table's run " + std::to_string(variant.runs.size() - 1u)); +} + +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 { + SplitRunsNameTheirSharedWords(); + 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 23fbd3392b..ab9818c6ba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp @@ -1,6 +1,9 @@ #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 #include #include #include @@ -21,8 +24,14 @@ using AgcDriver::DriverDetail::DrawMiss; using AgcDriver::DriverDetail::DrawKeySightings; alignas(256) constexpr std::array VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array TriangleVertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, +}; alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array ConstantPixelCode{0xf4000080u, 0xfa000000u, 0xbf8cc07fu, 0x7e000202u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u}; alignas(256) std::array ColorMemory{}; +alignas(256) std::array PixelConstant{}; struct Header { Shader shader{}; @@ -31,6 +40,8 @@ struct Header { alignas(8) Header VertexHeader{}; alignas(8) Header PixelHeader{}; +alignas(8) Header TriangleVertexHeader{}; +alignas(8) Header ConstantPixelHeader{}; const Shader* Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { header.shader.file_header = 0x34333231u; @@ -49,10 +60,10 @@ void Append(std::vector& commands, std::uint32_t opcode, std::ini commands.insert(commands.end(), payload); } -std::vector DrawThrice() { +std::vector Draws(float scale, const std::uint32_t* vertexCode, std::uint32_t vertexUserSgprs, const std::uint32_t* pixelCode, std::uint32_t pixelUserSgprs, const std::vector>& pixelWords) { const auto color = reinterpret_cast(ColorMemory.data()); - const auto vertex = reinterpret_cast(VertexCode.data()); - const auto pixel = reinterpret_cast(PixelCode.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}, @@ -69,33 +80,57 @@ std::vector DrawThrice() { {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)}, + {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}, }; const std::vector> shader{ - {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0}, - {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, vertexUserSgprs << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, pixelUserSgprs << 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}); - Append(commands, 0x2d, {3, 2}); - Append(commands, 0x2d, {3, 2}); - Append(commands, 0x2d, {3, 2}); + for (const auto& words : pixelWords) { + for (std::uint32_t k = 0; k < words.size(); ++k) Append(commands, 0x76, {0x00c + k, words[k]}); + Append(commands, 0x2d, {3, 2}); + } return commands; } +AgcDriver::DriverDetail::DrawEntryCounters Submit(std::vector commands) { + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); +} + +AgcDriver::DriverDetail::DrawEntryCounters Submit(float scale, std::initializer_list pixelWords) { + std::vector> words; + for (const auto word : pixelWords) words.push_back({std::bit_cast(word)}); + return Submit(Draws(scale, VertexCode.data(), 1, PixelCode.data(), 1, words)); +} + +AgcDriver::DriverDetail::DrawEntryCounters SubmitConstantDraws(std::size_t draws) { + const auto constant = reinterpret_cast(PixelConstant.data()); + const std::vector> words(draws, {static_cast(constant), static_cast(constant >> 32u)}); + return Submit(Draws(32.0f, TriangleVertexCode.data(), 0, ConstantPixelCode.data(), 2, words)); +} + +unsigned BrightestColorByte() { + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(ColorMemory.data()), ColorMemory.size()); + unsigned brightest = 0; + for (const auto value : ColorMemory) brightest = std::max(brightest, static_cast(value)); + return brightest; +} + void ThirdDrawHitsTheCache(bool libraries) { Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); - auto commands = DrawThrice(); - Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; - Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); - AgcDriverWaitIdle_nid_postfix(); - const auto counters = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + const auto counters = Submit(32.0f, {1.0f, 1.0f, 1.0f}); const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; const auto layout = counters.misses[static_cast(DrawMiss::Layout)]; Require(counters.lookups == 3 && counters.absent == 2, mode + ": the three draws made " + std::to_string(counters.lookups) + " draw-cache lookups (" + std::to_string(counters.absent) + " without an entry), expected 3 lookups with 2 absent"); @@ -104,6 +139,84 @@ void ThirdDrawHitsTheCache(bool libraries) { Require(counters.hits == 1, mode + ": the repeated draw did not reuse the cached draw (" + std::to_string(counters.hits) + " hits)"); } +void UserDataDrawsReuseTheirShape(bool libraries) { + const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; + const bool verify = std::getenv("APS5_VERIFY_DRAW_DATA_HITS") != nullptr; + const auto before = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + const auto known = Submit(32.0f, {2.0f, 3.0f, 4.0f}); + Require(known.lookups - before.lookups == 3 && known.absent - before.absent == 3, mode + ": three draws with new user words did not miss the draw key three times"); + Require(known.dataHits - before.dataHits == 3, mode + ": " + std::to_string(known.dataHits - before.dataHits) + " of three draws that differ from a cached draw only in user words reused its decode, expected 3"); + Require(known.inserts == before.inserts, mode + ": a new key of a shape that has an entry was cached at its first sight"); + const auto stages = known.dataStagesReused + known.dataStagesCompiled - before.dataStagesReused - before.dataStagesCompiled; + Require(stages == 6, mode + ": the three data-only hits accounted " + std::to_string(stages) + " stages, expected 6"); + Require(verify ? known.dataVerified - before.dataVerified == 3 : known.dataStagesReused - before.dataStagesReused == 3, mode + ": the data-only hits reused " + std::to_string(known.dataStagesReused - before.dataStagesReused) + " stages and verified " + std::to_string(known.dataVerified - before.dataVerified) + ", expected the 3 vertex stages, whose user words did not change"); + const auto fresh = Submit(16.0f, {5.0f, 6.0f, 7.0f}); + Require(fresh.inserts - known.inserts == 1, mode + ": three keys of a new shape inserted " + std::to_string(fresh.inserts - known.inserts) + " draw entries, expected 1: the shape's second key"); + Require(fresh.dataHits - known.dataHits == 1, mode + ": the third key of a new shape made " + std::to_string(fresh.dataHits - known.dataHits) + " data-only hits, expected 1"); +} + +void StageDataHitsRefreshTheirWords(bool libraries) { + const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; + const bool verify = std::getenv("APS5_VERIFY_DRAW_DATA_HITS") != nullptr; + const bool enabled = std::getenv("APS5_NO_DRAW_STAGE_DATA_HITS") == nullptr; + Registered(TriangleVertexHeader, TriangleVertexCode.data(), sizeof(TriangleVertexCode), 2); + Registered(ConstantPixelHeader, ConstantPixelCode.data(), sizeof(ConstantPixelCode), 1); + ColorMemory.fill(std::byte{0}); + PixelConstant[0] = 0.25f; + const auto before = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + const auto cached = SubmitConstantDraws(3); + Require(cached.inserts - before.inserts == 1 && cached.hits - before.hits == 1, mode + ": three draws of a pixel shader reading a constant through its user words did not insert once and hit once"); + Require(!enabled || cached.stageDataInserts - before.stageDataInserts == 1, mode + ": the inserted draw recorded " + std::to_string(cached.stageDataInserts - before.stageDataInserts) + " stages with data positions, expected the fragment stage"); + Require(BrightestColorByte() == 64, mode + ": the draws of constant 0.25 wrote " + std::to_string(BrightestColorByte()) + ", expected 64"); + PixelConstant[0] = 1.0f; + const auto refreshed = SubmitConstantDraws(1); + const auto fragmentMisses = refreshed.misses[static_cast(DrawMiss::FragmentDiffering)] - cached.misses[static_cast(DrawMiss::FragmentDiffering)]; + if (!enabled) { + Require(fragmentMisses == 1, mode + ": with stage data hits off, a changed constant did not miss on the fragment stage"); + } else if (verify) { + Require(refreshed.stageDataVerified - cached.stageDataVerified == 1, mode + ": the stage data hit was verified " + std::to_string(refreshed.stageDataVerified - cached.stageDataVerified) + " times, expected once"); + } else { + Require(refreshed.hits - cached.hits == 1 && fragmentMisses == 0, mode + ": a draw whose constant changed missed the cached draw"); + Require(refreshed.stageDataHits - cached.stageDataHits == 1 && refreshed.stageDataWords - cached.stageDataWords == 1, mode + ": the hit refreshed " + std::to_string(refreshed.stageDataWords - cached.stageDataWords) + " words in " + std::to_string(refreshed.stageDataHits - cached.stageDataHits) + " stages, expected 1 word in the fragment stage"); + Require(refreshed.inserts == cached.inserts, mode + ": a stage data hit inserted a draw entry"); + } + Require(BrightestColorByte() == 255, mode + ": the draw of constant 1.0 wrote " + std::to_string(BrightestColorByte()) + ", expected 255"); +} + +void RetainedPixelCacheTransitions() { + auto commands = Draws(32.0f, VertexCode.data(), 1, PixelCode.data(), 1, {}); + Append(commands, 0x76, {0x00c, std::bit_cast(1.0f)}); + const auto draw = [&] { + for (unsigned i = 0; i < 3; ++i) Append(commands, 0x2d, {3, 2}); + }; + const auto skipped = [&] { + Append(commands, 0x69, {0x8e, 0, 0}); + Append(commands, 0x69, {0x203, 0}); + Append(commands, 0x69, {0x2dc, 0xaa00}); + draw(); + }; + Append(commands, 0x69, {0x1c4, 1}); + skipped(); + Append(commands, 0x69, {0x8e, 0xf, 0xf}); + draw(); + skipped(); + Append(commands, 0x69, {0x203, 0x400}); + draw(); + skipped(); + Append(commands, 0x69, {0x8e, 0xf, 0xf}); + Append(commands, 0x69, {0x2dc, 0xaa01}); + draw(); + skipped(); + const auto before = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "retained pixel cache transitions failed to submit"); + AgcDriverWaitIdle_nid_postfix(); + const auto after = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(after.lookups - before.lookups == 21 && after.absent - before.absent == 8 && after.inserts - before.inserts == 4 && after.hits - before.hits == 13, "color, execution or alpha-to-coverage transitions reused the wrong cached draw"); + if (std::getenv("APS5_VERIFY_DRAW_RECIPE") != nullptr) Require(after.verifyDecodes > before.verifyDecodes, "pixel cache transitions did not verify any cached decode"); + Require(after.verifyDecodeMismatches == before.verifyDecodeMismatches && after.facadeMismatches == before.facadeMismatches, "cached pixel transitions disagreed with fresh decoding"); +} + void SightingsKeepCollidingKeys() { DrawKeySightings sightings(DrawKeySightings::Ways); Require(!sightings.SeenBefore(0) && sightings.SeenBefore(0), "draw key 0 was not admitted at its second sight"); @@ -127,7 +240,11 @@ int main() { } if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; ThirdDrawHitsTheCache(libraries); + UserDataDrawsReuseTheirShape(libraries); + 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/DrawDataHitUserWords.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp new file mode 100644 index 0000000000..13d67731a9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp @@ -0,0 +1,233 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array PixelCode{0xf4080100u, 0xfa000000u, 0xbf8cc07fu, 0xe0300000u, 0x80010000u, 0xbf8c3f70u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array TriangleVertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, +}; +alignas(256) constexpr std::array ArithmeticPixelCode{0xf4000100u, 0xfa000000u, 0xbf8cc07fu, 0x7e000204u, 0x06000002u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array PixelConstant{}; +alignas(256) std::array ColorMemory{}; + +constexpr std::size_t BlockBytes = 65536; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; +alignas(8) Header ArithmeticPixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +std::uint32_t* GuestBlock() { +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "data-only hit: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + return block; +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +void PixelUserData(std::vector& commands, const std::uint32_t* pointer, std::uint32_t unread) { + const auto address = reinterpret_cast(pointer); + Append(commands, 0x76, {0x00c, static_cast(address)}); + Append(commands, 0x76, {0x00d, static_cast(address >> 32u)}); + Append(commands, 0x76, {0x00e, unread}); +} + +std::vector Setup(bool triangle = false) { + const auto color = reinterpret_cast(ColorMemory.data()); + const auto vertex = reinterpret_cast(triangle ? TriangleVertexCode.data() : VertexCode.data()); + const auto pixel = reinterpret_cast(triangle ? ArithmeticPixelCode.data() : PixelCode.data()); + std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, triangle ? 0u : 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 3u << 1u}, + }; + if (triangle) context.insert(context.end(), {{0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}}); + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + return commands; +} + +AgcDriver::DriverDetail::DrawEntryCounters Submit(std::vector commands) { + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); +} + +std::string Counts(const AgcDriver::DriverDetail::DrawEntryCounters& counters) { + return std::to_string(counters.lookups) + " lookups, " + std::to_string(counters.dataHits) + " data-only hits, stages " + std::to_string(counters.dataStagesReused) + " reused / " + std::to_string(counters.dataStagesCompiled) + " compiled / " + std::to_string(counters.dataVerified) + " verified"; +} + +void UnreadUserWordsKeepTheStage(bool verify) { + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + auto* first = GuestBlock(); + auto* second = GuestBlock(); + const auto describe = [](std::uint32_t* table, std::uint32_t* data, float value) { + const auto address = reinterpret_cast(data); + data[0] = std::bit_cast(value); + table[0] = static_cast(address); + table[1] = static_cast(address >> 32u) & 0xffffu; + table[2] = 16; + table[3] = 0x4bfacu; + }; + describe(first, first + 1024, 1.0f); + describe(second, second + 1024, 0.5f); + + auto commands = Setup(); + PixelUserData(commands, first, 0x11); + Append(commands, 0x2d, {3, 2}); + auto counters = Submit(std::move(commands)); + Require(counters.lookups == 1 && counters.dataHits == 0, "the first draw: " + Counts(counters)); + commands.clear(); + PixelUserData(commands, first, 0x11); + Append(commands, 0x2d, {3, 2}); + counters = Submit(std::move(commands)); + Require(counters.inserts != 0, "the repeated register key was not admitted: " + Counts(counters)); + + commands.clear(); + PixelUserData(commands, first, 0x22); + Append(commands, 0x2d, {3, 2}); + const auto before = counters; + counters = Submit(std::move(commands)); + const auto reused = counters.dataStagesReused - before.dataStagesReused; + const auto compiled = counters.dataStagesCompiled - before.dataStagesCompiled; + Require(counters.dataHits == before.dataHits + 1, "a draw that changed only a user word no stage reads was not a data-only hit: " + Counts(counters)); + if (verify) Require(compiled == 2 && counters.dataVerified - before.dataVerified == 2, "verify mode: a draw that changed only an unread user word did not build and compare both stages: " + Counts(counters)); + else Require(reused == 2 && compiled == 0, "a draw that changed only a user word no stage reads did not reuse both stages: " + Counts(counters)); + + commands.clear(); + PixelUserData(commands, second, 0x22); + Append(commands, 0x2d, {3, 2}); + const auto moved = counters; + counters = Submit(std::move(commands)); + Require(counters.dataHits == moved.dataHits + 1, "a draw that pointed the pixel stage at another buffer descriptor was not a data-only hit: " + Counts(counters)); + Require(counters.dataStagesCompiled - moved.dataStagesCompiled >= 1, "a draw that pointed the pixel stage at another buffer descriptor reused the pixel stage: " + Counts(counters)); + if (!verify) Require(counters.dataStagesReused - moved.dataStagesReused == 1, "a draw that changed only the pixel stage's descriptor pointer did not reuse the vertex stage: " + Counts(counters)); +} + +unsigned BrightestColorByte() { + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(ColorMemory.data()), ColorMemory.size()); + unsigned brightest = 0; + for (const auto value : ColorMemory) brightest = std::max(brightest, static_cast(std::to_integer(value))); + return brightest; +} + +void ReadUserWordsChangeTheOutput() { + Registered(VertexHeader, TriangleVertexCode.data(), sizeof(TriangleVertexCode), 2); + Registered(ArithmeticPixelHeader, ArithmeticPixelCode.data(), sizeof(ArithmeticPixelCode), 1); + PixelConstant[0] = 0.25f; + auto* table = reinterpret_cast(PixelConstant.data()); + const auto draw = [&](float word, bool setup) { + auto commands = setup ? Setup(true) : std::vector{}; + PixelUserData(commands, table, std::bit_cast(word)); + Append(commands, 0x2d, {3, 2}); + Submit(std::move(commands)); + return BrightestColorByte(); + }; + draw(0.25f, true); + const auto first = draw(0.25f, false); + const auto before = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + const auto second = draw(0.5f, false); + const auto after = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + const auto closeTo = [](unsigned value, unsigned expected) { return value + 2 >= expected && value <= expected + 2; }; + Require(closeTo(first, 128), "a pixel stage adding user word 2 (0.25) to a loaded 0.25 wrote " + std::to_string(first) + ", expected 128"); + Require(closeTo(second, 191), "a pixel stage adding user word 2 changed to 0.5 wrote " + std::to_string(second) + ", expected 191 (" + Counts(after) + ", before: " + Counts(before) + ")"); +} + +} + +int main() { + try { + bool libraries = false; + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + libraries = device->GraphicsPipelineLibraries(); + } + if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; + const bool verify = std::getenv("APS5_VERIFY_DRAW_DATA_HITS") != nullptr; + UnreadUserWordsKeepTheStage(verify); + ReadUserWordsChangeTheOutput(); + AgcDriverShutdown_nid_postfix(); + std::cout << "Data-only hits " << (verify ? "verified" : "reused") << " the stages whose read user words did not change\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp index 6754f0c84d..9413952247 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp @@ -69,6 +69,15 @@ alignas(256) constexpr std::array PostAddCode{ 0xbf810000, }; +alignas(256) constexpr auto NoCarryCode = [] { + auto code = PostAddCode; + code[2] = 0x4a0a0a10u; + code[3] = 0xbf800000u; + code[4] = 0x4a101011u; + code[5] = 0xbf800000u; + return code; +}(); + alignas(256) constexpr std::array CarryCode{ 0x7e020280, 0x7e040280, 0xd70f7d05, 0x00020a10, @@ -184,6 +193,88 @@ alignas(256) constexpr std::array OtherVgprCode{ 0xbf810000, }; +alignas(256) constexpr std::array SadCode{ + 0x7e020280, 0x7e040280, + 0xd55d0005, 0x04150010, + 0xd70f7d08, 0x00021011, + 0xf4080506, 0xfa000000, + 0xf4080606, 0xfa000010, + 0x02060b08, + 0xbf8cc07f, + 0xe0002000, 0x80050a03, + 0xe0002000, 0x80060b03, + 0xbf8c3f70, + 0x4a08170a, 0x34080884, + 0x7e180301, 0x7e1a0302, 0x7e1c0305, 0x7e1e0308, + 0xe0781000, 0x80020c04, + 0x7e200280, 0x7e2202f2, + 0xf80008cf, 0x11101010, + 0xbf810000, +}; + +alignas(256) constexpr std::array ExecAllCode{ + 0xbefe04c1, 0x7e040280, + 0xd70f7d05, 0x00020a10, + 0xd70f7d08, 0x00021011, + 0xf4080506, 0xfa000000, + 0xf4080606, 0xfa000010, + 0x02060b08, + 0xbf8cc07f, + 0xe0002000, 0x80050a03, + 0xe0002000, 0x80060b03, + 0xbf8c3f70, + 0x4a08170a, 0x34080884, + 0x7e180301, 0x7e1a0302, 0x7e1c0305, 0x7e1e0308, + 0xe0781000, 0x80020c04, + 0x7e200280, 0x7e2202f2, + 0xf80008cf, 0x11101010, + 0xbf810000, +}; + +alignas(256) constexpr std::array ReloadCode{ + 0x7e020280, 0x7e040280, + 0xd70f7d05, 0x00020a10, + 0xd70f7d08, 0x00021011, + 0xf4080506, 0xfa000000, + 0xf4080606, 0xfa000010, + 0x02060b08, + 0xbf8cc07f, + 0xe0002000, 0x80050a03, + 0xe0002000, 0x80060b03, + 0xbf8c3f70, + 0x4a08170a, 0x34080884, + 0x7e180301, 0x7e1a0302, 0x7e1c0305, 0x7e1e0308, + 0xe0781000, 0x80020c04, + 0x7e200280, 0x7e2202f2, + 0xf80008cf, 0x11101010, + 0xf4040407, 0xfa000000, + 0xbf8cc07f, + 0x80121110, + 0xbf810000, +}; + +alignas(256) constexpr std::array SkippedReloadCode{ + 0x7e020280, 0x7e040280, + 0xd70f7d05, 0x00020a10, + 0xd70f7d08, 0x00021011, + 0xf4080506, 0xfa000000, + 0xf4080606, 0xfa000010, + 0x02060b08, + 0xbf8cc07f, + 0xe0002000, 0x80050a03, + 0xe0002000, 0x80060b03, + 0xbf8c3f70, + 0x4a08170a, 0x34080884, + 0x7e180301, 0x7e1a0302, 0x7e1c0305, 0x7e1e0308, + 0xe0781000, 0x80020c04, + 0x7e200280, 0x7e2202f2, + 0xf80008cf, 0x11101010, + 0xbf840001, + 0xbe900380, + 0x80121110, + 0xbf810000, +}; + alignas(256) constexpr std::array PixelCode{0x7e0002f2u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u}; constexpr std::uint32_t Width = 64; @@ -262,14 +353,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 +438,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'; @@ -362,7 +454,7 @@ std::string Analysis(Fixture& fixture) { bool vertex; bool instance; }; - const std::array cases{{ + const std::array cases{{ {"a program that copies v5 and v8 before its fetch adds", VertexCode, true, true}, {"a program whose fetch adds are the only readers of v5, v8, s16 and s17", PostAddCode, false, false}, {"a program that reads the instance add's carry", CarryCode, false, true}, @@ -371,6 +463,10 @@ std::string Analysis(Fixture& fixture) { {"a program that writes EXEC before its fetch adds", ExecCode, true, true}, {"a program that writes another add's carry to s17 before its fetch adds", CarryWriteCode, false, true}, {"a program whose s16 add writes v0, not v5", OtherVgprCode, true, false}, + {"a program whose vertex add is an unclamped v_sad_u32", SadCode, false, false}, + {"a program that sets every EXEC bit with s_mov_b64 before its fetch adds", ExecAllCode, false, false}, + {"a program that reloads s16 and s17 with s_load_dwordx2 from s[14:15] after its fetch adds and reads them", ReloadCode, false, false}, + {"a program that reads s16 and s17 after a branch that can skip the write of s16", SkippedReloadCode, true, true}, }}; const auto userData = fixture.UserData(0x51u, 0x62u); std::string failures; @@ -451,15 +547,16 @@ std::string Run(Fixture& fixture) { failures += fixture.Check(what + ", on the CPU path", autoIndices, arguments.firstVertexOrIndex, arguments.firstInstance, true); } if (fixture.device->DrawIndirectSupport().firstInstance) { - { - const std::string what = "indexed indirect record whose fetch adds are the only readers of v5/v8, user data s16 0x51, s17 0x62"; - const auto vertex = fixture.Vertex(PostAddCode, staleUserData); + for (const auto* code : {&PostAddCode, &NoCarryCode}) { + const std::string what = std::string(code == &NoCarryCode ? "no-carry " : "carry ") + "indexed indirect record whose fetch adds are the only readers of v5/v8, user data s16 0x51, s17 0x62"; + const auto vertex = fixture.Vertex(*code, staleUserData); AgcDriver::Pm4::DrawParameters draw{indexAddress, static_cast(Indices.size()), 2, 0, 0, true, 0, 0}; draw.indirect = indexedIndirect; failures += Classified(fixture, what, vertex, staleUserData, draw, std::nullopt, Rule::InPlace); fixture.Place(indexedRecord[3], bufferIndices, indexedRecord[4]); fixture.Draw(vertex, draw); failures += fixture.Check(what + ", drawn in place on the GPU", bufferIndices, indexedRecord[3], indexedRecord[4], false); + if (code == &NoCarryCode) std::cout << "no-carry indexed indirect GPU readback checked\n"; } { const std::string what = "indexed indirect record whose packet names no register for its offsets, user data s16 0x51, s17 0x62"; @@ -475,6 +572,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/DrawPipelineTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawPipelineTrace.cpp new file mode 100644 index 0000000000..31456ae51f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawPipelineTrace.cpp @@ -0,0 +1,48 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include +#include +#include +#include +#include +#include + +int main(int argc, char** argv) { + try { + using namespace AgcDriver; + using DriverDetail::DrawPipeline; + if (DrawPipeline::Depth() == 0) return 77; + const bool disabled = argc > 1 && std::string_view(argv[1]) == "disabled"; + if (PerformanceTrace::Configured() == disabled) throw std::runtime_error("unexpected trace configuration"); + if (!disabled) PerformanceTrace::Present(1, 1, nullptr); + bool checked = false; + const char* failure = nullptr; + DrawPipeline::Queue0().Enqueue([&] { + auto* trace = PerformanceTrace::Thread(); + if (disabled) { + if (trace != nullptr) { failure = "unconfigured draw committer allocated a trace buffer"; return; } + checked = true; + return; + } + if (trace == nullptr) { failure = "draw committer CPU spans are absent from the trace"; return; } + { + PerformanceTimer timer("DrawPipeline.TraceRegression"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + const auto snapshot = trace->Collect(); + if (snapshot.name != "draw committer" || snapshot.dropped) { failure = "draw committer trace identity or capacity is incorrect"; return; } + const auto found = [](const auto& event) { return event.scope != nullptr && std::string_view(event.scope) == "DrawPipeline.TraceRegression" && event.stage == nullptr && event.length > std::chrono::steady_clock::duration::zero(); }; + if (std::none_of(snapshot.slices.begin(), snapshot.slices.end(), found) || std::none_of(snapshot.totals.begin(), snapshot.totals.end(), found)) + { failure = "committed work was missing from trace slices or totals"; return; } + checked = true; + }, {}); + DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission); + if (failure != nullptr) throw std::runtime_error(failure); + if (!checked) throw std::runtime_error("draw committer did not run the trace check"); + std::cout << "Draw committer tracing checked\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp new file mode 100644 index 0000000000..c0a50d0476 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp @@ -0,0 +1,131 @@ +#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 + +namespace { + +using AgcDriver::Graphics::Require; +alignas(256) std::array Pixels{}; +alignas(256) std::array Source{}; +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{ + 0x7e020280u, 0xe0301000u, 0x80000201u, 0xbf8c3f70u, 0x7e000280u, 0x7e0602f2u, 0xf800180fu, 0x03010002u, 0xbf810000u, +}; + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +void Draw(const std::array& descriptor, std::uint32_t* label = nullptr) { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 0u}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 4u << 1u}, + {0x00c, descriptor[0]}, {0x00d, descriptor[1]}, {0x00e, descriptor[2]}, {0x00f, descriptor[3]}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + const auto color = reinterpret_cast(Pixels.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + Append(commands, 0x2d, {3, 2}); + if (label != nullptr) { + const auto address = reinterpret_cast(label); + Append(commands, 0x37, {0x00100200u, static_cast(address), static_cast(address >> 32u), 7u}); + } + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "unsupported buffer type draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +void NamesShaderAndDescriptor() { + const auto address = reinterpret_cast(Source.data()); + const std::array descriptor{static_cast(address), static_cast((address >> 32u) & 0xffffu), sizeof(Source), 0xa1b00facu}; + try { + Draw(descriptor); + } catch (const std::exception& error) { + const std::string message = error.what(); + char shader[64]; + std::snprintf(shader, sizeof(shader), "shader 0x%llx stage %u", static_cast(reinterpret_cast(PixelCode.data())), static_cast(ShaderRecompiler::ShaderStage::Fragment)); + char words[64]; + std::snprintf(words, sizeof(words), "(buffer 0 V# %08x %08x %08x %08x)", descriptor[0], descriptor[1], descriptor[2], descriptor[3]); + Require(message.find("buffer descriptor uses an unsupported type") != std::string::npos, "draw failed for another reason: " + message); + Require(message.find(shader) != std::string::npos, std::string("draw error does not name ") + shader + ": " + message); + Require(message.find(words) != std::string::npos, std::string("draw error does not name ") + words + ": " + message); + return; + } + throw std::runtime_error("a pixel shader read a buffer through a descriptor of type 2"); +} + +void SkipsRefusedDraw() { + const auto address = reinterpret_cast(Source.data()); + const std::array descriptor{static_cast(address), static_cast((address >> 32u) & 0xffffu), sizeof(Source), 0xa1b00facu}; + alignas(8) static std::uint32_t label = 0; + for (int submission = 0; submission < 2; ++submission) { + label = 0; + Draw(descriptor, &label); + Require(label == 7u, "the packet after a skipped refused draw was not executed"); + } + for (const auto byte : Pixels) Require(byte == std::byte{0}, "a skipped refused draw wrote its color target"); +} + +} + +int main(int argc, char** argv) { + try { + Require(argc == 1 || (argc == 2 && std::string_view(argv[1]) == "--skip-refused"), "invalid test arguments"); + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + if (argc == 2) SkipsRefusedDraw(); + else NamesShaderAndDescriptor(); + try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {} + std::puts("draw unsupported buffer type tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {} + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp new file mode 100644 index 0000000000..d45a4cc045 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp @@ -0,0 +1,137 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Range = std::pair; +using Ranges = std::vector; + +void Require(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("draw write ranges: " + what); +} + +bool Contains(const Ranges& ranges, std::uint64_t begin, std::uint64_t end) { + return std::any_of(ranges.begin(), ranges.end(), [&](const Range& range) { return range.first <= begin && end <= range.second; }); +} + +bool Touches(const Ranges& ranges, std::uint64_t begin, std::uint64_t end) { + return std::any_of(ranges.begin(), ranges.end(), [&](const Range& range) { return range.first < end && begin < range.second; }); +} + +std::array ImageWords(std::uint64_t base, std::uint32_t width, std::uint32_t height) { + std::array words{}; + const auto base40 = base >> 8u; + words[0] = static_cast(base40); + words[1] = static_cast((base40 >> 32u) & 0xffu) | (56u << 20u) | (((width - 1u) & 0x3u) << 30u); + words[2] = ((width - 1u) >> 2u) | ((height - 1u) << 14u); + words[3] = 4u | (5u << 3u) | (6u << 6u) | (7u << 9u) | (9u << 28u); + return words; +} + +} + +int main() { + try { + using AgcDriver::DriverDetail::DrawWriteRanges; + constexpr std::uint64_t written = 0x100000000ull; + constexpr std::uint64_t readOnly = 0x200000000ull; + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding images; + images.kind = ShaderRecompiler::DescriptorKind::StorageImage; + images.role = ShaderRecompiler::DescriptorRole::GuestImages; + images.count = 2; + for (const auto base : {written, readOnly}) { + const auto words = ImageWords(base, 256, 128); + images.guestDescriptor.insert(images.guestDescriptor.end(), words.begin(), words.end()); + } + images.imageWritten = {true, false}; + program.bindings.push_back(images); + const std::array stages{{{ShaderRecompiler::ShaderStage::Fragment, &program, 0}}}; + AgcDriver::Graphics::State state{}; + const auto resource = AgcDriver::Graphics::DecodeTextureResource(std::span(images.guestDescriptor).subspan(0, 8)); + const auto surfaceBytes = AgcDriver::Graphics::DescribeSurface(resource).guestBytes; + Require(resource.baseAddress == written && surfaceBytes >= 256u * 128u * 4u, "the test T# does not decode as meant"); + for (const bool exact : {false, true}) { + const auto ranges = DrawWriteRanges(state, stages, exact); + Require(Contains(ranges, written, written + surfaceBytes), "a storage image the draw stores to is not a write range"); + Require(!Touches(ranges, readOnly, readOnly + surfaceBytes), "a storage image the draw only reads is a write range"); + } + program.bindings[0].imageWritten.clear(); + Require(Contains(DrawWriteRanges(state, stages, true), readOnly, readOnly + surfaceBytes), "an image without written flags is not a write range"); + auto nullImage = ImageWords(0, 256, 128); + nullImage[0] = 0; + nullImage[1] &= ~0xffu; + std::copy(nullImage.begin(), nullImage.end(), program.bindings[0].guestDescriptor.begin() + 8); + program.bindings[0].imageWritten = {true, true}; + for (const bool exact : {false, true}) { + const auto ranges = DrawWriteRanges(state, stages, exact); + Require(Contains(ranges, written, written + surfaceBytes), "a written image next to a null one is not a write range"); + Require(!Touches(ranges, 0, surfaceBytes), "a null storage image is a write range"); + } + bool bounded = false; + const auto dispatchRanges = AgcDriver::DriverDetail::DispatchWriteRanges(program, bounded); + Require(bounded, "a dispatch whose written images are a real one and a null one is not bounded"); + Require(Contains(dispatchRanges, written, written + surfaceBytes), "a written image next to a null one is not a dispatch write range"); + Require(!Touches(dispatchRanges, 0, surfaceBytes), "a null storage image is a dispatch write range"); + AgcDriver::Graphics::State targets{}; + targets.hasColorTarget = true; + auto& color = targets.color; + color.address = 0x300000000ull; + color.surfaceAddress = color.address; + color.extent = {1920, 1080}; + color.tileMode = AgcDriver::Graphics::ColorTileMode::RenderTarget; + color.elementBytes = 4; + const AgcDriver::Graphics::ColorTargetLayout layout(1920, 1080, color.tileMode, 4); + color.bytes = layout.Bytes(); + color.dccAddress = 0x380000000ull; + color.cmaskAddress = 0x3c0000000ull; + color.cmaskBytes = AgcDriver::Graphics::CmaskBytes(1920, 1080); + AgcDriver::Graphics::DepthTarget depth{}; + depth.address = 0x400000000ull; + depth.stencilAddress = 0x480000000ull; + depth.extent = {1920, 1080}; + depth.format = VK_FORMAT_D32_SFLOAT_S8_UINT; + targets.depth = depth; + const std::span none; + const auto exact = DrawWriteRanges(targets, none, true); + const auto estimated = DrawWriteRanges(targets, none, false); + std::size_t last = 0; + for (std::uint32_t y = 0; y < 1080; y += 7) { + for (std::uint32_t x = 0; x < 1920; x += 5) last = std::max(last, layout.Offset(x, y)); + } + last = std::max(last, layout.Offset(1919, 1079)); + Require(last + 4 <= color.bytes, "the color layout addresses past its bytes"); + Require(Contains(exact, color.address, color.address + color.bytes), "the exact color range misses the layout"); + Require(!Touches(exact, color.address + color.bytes, color.address + color.bytes + 1), "the exact color range runs past the layout"); + Require(Contains(exact, color.dccAddress, color.dccAddress + color.bytes / 256) && !Touches(exact, color.dccAddress + (color.bytes + 255) / 256, color.dccAddress + (color.bytes + 255) / 256 + 1), "the exact DCC range is not one key byte per 256 surface bytes"); + for (const auto& ranges : {exact, estimated}) { + Require(Contains(ranges, color.cmaskAddress, color.cmaskAddress + color.cmaskBytes), "the CMASK of the color target is not a write range"); + } + Require(!Touches(exact, color.cmaskAddress + color.cmaskBytes, color.cmaskAddress + color.cmaskBytes + 1), "the exact CMASK range runs past CmaskBytes"); + const auto depthBytes = AgcDriver::Graphics::DepthSliceBytes(depth.extent, 4); + const auto stencilBytes = AgcDriver::Graphics::DepthSliceBytes(depth.extent, 1); + Require(Contains(exact, depth.address, depth.address + depthBytes) && !Touches(exact, depth.address + depthBytes, depth.address + depthBytes + 1), "the exact depth range is not DepthSliceBytes"); + Require(Contains(exact, depth.stencilAddress, depth.stencilAddress + stencilBytes) && !Touches(exact, depth.stencilAddress + stencilBytes, depth.stencilAddress + stencilBytes + 1), "the exact stencil range is not DepthSliceBytes"); + for (const auto& [begin, end] : exact) Require(Contains(estimated, begin, end), "an exact range is not inside the estimates"); + std::printf("draw write ranges: color %zu bytes (estimate %llu), depth %llu, stencil %llu\n", color.bytes, static_cast(2 * color.bytes + 65536), static_cast(depthBytes), static_cast(stencilBytes)); + std::puts("draw write ranges 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/DwordPatternFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DwordPatternFill.cpp index 1629b16458..f7a18fada0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DwordPatternFill.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DwordPatternFill.cpp @@ -26,6 +26,13 @@ alignas(256) constexpr std::array Code{ 0x7e000207u, 0xe0102000u, 0x80000002u, 0x8afe7e6au, 0xbf880003u, 0x7e000206u, 0xe0102000u, 0x80000002u, 0xbefe046au, 0x8afe7e0au, 0xbf880003u, 0x7e000205u, 0xe0102000u, 0x80000002u, 0xbefe040au, 0x8afe7e08u, 0xbf880003u, 0x7e000204u, 0xe0102000u, 0x80000002u, 0xbf810000u}; +constexpr std::size_t ComparesAt = 27; +constexpr std::array WithCompares(std::array compares) { + auto code = Code; + for (std::size_t word = 0; word < compares.size(); ++word) code[ComparesAt + word] = compares[word]; + return code; +} +alignas(256) constexpr std::array SwappedCode = WithCompares({0x7d8c02f9u, 0x06068c02u, 0x7d860609u}); constexpr std::array Pattern{0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; constexpr std::uint32_t Untouched = 0xcafebabeu; alignas(256) std::array Output{}; @@ -35,6 +42,35 @@ std::vector UserData(void* output, std::uint32_t records, std::ui return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), records, format, Pattern[0], Pattern[1], Pattern[2], Pattern[3], count, period}; } +void SwappedComparesTests() { + const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1}; + const std::array packet{0xc0031500u, 4, 1, 1, 0x41u}; + for (const auto period : {1u, 2u, 4u}) { + const auto data = UserData(Output.data(), 1024, 256, period); + const auto listed = MatchDwordPatternFill(packet, Code, data, compute); + const auto swapped = MatchDwordPatternFill(packet, SwappedCode, data, compute); + Require(listed && swapped && swapped->base == listed->base && swapped->bytes == listed->bytes && swapped->pattern == listed->pattern, "the kernel with its two compares in the other order was not recognized as the same fill"); + } + const auto data = UserData(Output.data(), 1024, 256, 4); + for (std::size_t word = 0; word < SwappedCode.size(); ++word) { + auto altered = SwappedCode; + altered[word] ^= 1u; + Require(!MatchDwordPatternFill(packet, altered, data, compute), "a different shader matched the dword pattern-fill kernel with swapped compares"); + } + Require(!MatchDwordPatternFill(packet, std::span(SwappedCode).first(68), data, compute), "a truncated shader with swapped compares matched"); + constexpr std::array, 5> otherOrders{{ + {0x7d8c02f9u, 0x7d860609u, 0x06068c02u}, + {0x06068c02u, 0x7d8c02f9u, 0x7d860609u}, + {0x7d860609u, 0x06068c02u, 0x7d8c02f9u}, + {0x7d860609u, 0x7d860609u, 0x7d8c02f9u}, + {0x7d8c02f9u, 0x06068c02u, 0x7d8c02f9u}, + }}; + for (const auto& order : otherOrders) { + const auto code = WithCompares(order); + Require(!MatchDwordPatternFill(packet, code, data, compute), "a kernel whose compare words are not one of the two orders matched"); + } +} + void MatchTests() { const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1}; const std::array packet{0xc0031500u, 4, 1, 1, 0x41u}; @@ -74,7 +110,7 @@ void MatchTests() { Require(clamped && clamped->bytes == 256, "the fill exceeded the descriptor's records"); } -std::vector Commands(const std::vector& data, std::uint32_t groups, bool partial, std::uint32_t threads) { +std::vector Commands(std::span code, const std::vector& data, std::uint32_t groups, bool partial, std::uint32_t threads) { std::vector words; const auto registers = [&](std::uint32_t first, std::span values) { words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); @@ -83,7 +119,7 @@ std::vector Commands(const std::vector& data, std: }; const std::array shape{64, 1, 1}; registers(0x207, shape); - const auto address = reinterpret_cast(Code.data()); + const auto address = reinterpret_cast(code.data()); const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; registers(0x20c, program); const std::array resource{(static_cast(data.size()) << 1u) | (1u << 7u)}; @@ -111,18 +147,20 @@ void ExecuteTests() { {4, 1024, 256, 5, false, 0}, {0, 1024, 256, 4, true, 70}, }}; - for (const auto& item : cases) { - Output.fill(Untouched); - const auto words = Commands(UserData(Output.data(), item.records, item.count, item.period), item.groups, item.partial, item.threads); - Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; - AgcDriver::Submit(&packet, 0); - AgcDriverWaitIdle_nid_postfix(); - AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); - const auto launched = item.partial ? item.threads : item.groups * 64u; - const auto filled = std::min({launched, item.records, item.count}); - for (std::uint32_t i = 0; i < Output.size(); ++i) { - const auto expected = i < filled ? Pattern[std::min(i % item.period, 3u)] : Untouched; - Require(Output[i] == expected, "dword pattern fill mismatch at " + std::to_string(i) + " (period " + std::to_string(item.period) + ", count " + std::to_string(item.count) + ", records " + std::to_string(item.records) + "): got " + std::to_string(Output[i]) + ", expected " + std::to_string(expected)); + for (const std::span code : {std::span(Code), std::span(SwappedCode)}) { + for (const auto& item : cases) { + Output.fill(Untouched); + const auto words = Commands(code, UserData(Output.data(), item.records, item.count, item.period), item.groups, item.partial, item.threads); + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); + const auto launched = item.partial ? item.threads : item.groups * 64u; + const auto filled = std::min({launched, item.records, item.count}); + for (std::uint32_t i = 0; i < Output.size(); ++i) { + const auto expected = i < filled ? Pattern[std::min(i % item.period, 3u)] : Untouched; + Require(Output[i] == expected, "dword pattern fill mismatch at " + std::to_string(i) + " (" + (code.data() == SwappedCode.data() ? "swapped" : "listed") + " compares, period " + std::to_string(item.period) + ", count " + std::to_string(item.count) + ", records " + std::to_string(item.records) + "): got " + std::to_string(Output[i]) + ", expected " + std::to_string(expected)); + } } } } @@ -131,7 +169,10 @@ void ExecuteTests() { int main() { try { - if (std::getenv("APS5_NO_FILL_HLE") == nullptr) MatchTests(); + if (std::getenv("APS5_NO_FILL_HLE") == nullptr) { + MatchTests(); + SwappedComparesTests(); + } { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index 276ea252d6..b656cca503 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -27,6 +27,7 @@ constexpr std::uint32_t Side = 2; constexpr std::uint32_t Format8888UNorm = 56; constexpr std::uint32_t Format8UNorm = 1; constexpr std::uint32_t Format8_8UNorm = 14; +constexpr std::uint32_t Format16UNorm = 7; constexpr std::uint32_t Format1616UNorm = 23; constexpr std::uint32_t Format9995Float = 132; constexpr std::uint32_t Format8Srgb = 128; @@ -178,45 +179,6 @@ void FillInput() { Extra[0] = 0x00001f20u; } -std::int32_t OffsetComponent(std::uint32_t tid, std::uint32_t component) { - const auto field = (Extra[tid * 3u] >> (component * 8u)) & 0x3fu; - return static_cast(field ^ 0x20u) - 0x20; -} - -float ReferenceTexel(int x, int y, float reference, const Sampler& sampler) { - const std::uint32_t clamp = sampler.clamp; - const int size = static_cast(Side); - if (clamp == ClampBorder && (x < 0 || x >= size || y < 0 || y >= size)) { - const float border = sampler.border == BorderWhite ? 1.0f : 0.0f; - return reference <= border ? 1.0f : 0.0f; - } - const auto address = [&](int value) { - if (clamp == ClampEdge) return std::clamp(value, 0, static_cast(Side) - 1); - const int size = static_cast(Side); - return ((value % size) + size) % size; - }; - const float red = static_cast(Red[address(y) * Side + address(x)]) / 255.0f; - return reference <= red ? 1.0f : 0.0f; -} - -float Expected(std::uint32_t tid, const Sampler& sampler, bool offsets) { - const float u = Input[tid * 3u + 1u] * static_cast(Side); - const float v = Input[tid * 3u + 2u] * static_cast(Side); - const float reference = std::clamp(Input[tid * 3u + 0u], 0.0f, 1.0f); - const int offsetX = offsets ? OffsetComponent(tid, 0u) : 0; - const int offsetY = offsets ? OffsetComponent(tid, 1u) : 0; - if (sampler.filter == FilterPoint) return ReferenceTexel(static_cast(std::floor(u)) + offsetX, static_cast(std::floor(v)) + offsetY, reference, sampler); - const float cu = u - 0.5f; - const float cv = v - 0.5f; - const int x = static_cast(std::floor(cu)) + offsetX; - const int y = static_cast(std::floor(cv)) + offsetY; - const float a = cu - std::floor(cu); - const float b = cv - std::floor(cv); - const float top = ReferenceTexel(x, y, reference, sampler) * (1.0f - a) + ReferenceTexel(x + 1, y, reference, sampler) * a; - const float bottom = ReferenceTexel(x, y + 1, reference, sampler) * (1.0f - a) + ReferenceTexel(x + 1, y + 1, reference, sampler) * a; - return top * (1.0f - b) + bottom * b; -} - ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, std::span code = Code, bool useCache = false, bool nativeSampleOffsets = true, bool cube = false) { std::vector userData(24, 0u); const auto input = cube ? BufferDescriptor(CubeInput.data(), static_cast(sizeof(CubeInput))) : BufferDescriptor(Input.data(), static_cast(sizeof(Input))); @@ -254,9 +216,7 @@ std::uint64_t Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); for (std::uint32_t tid = 0; tid < Threads; ++tid) { - const float expected = Expected(tid, sampler, offsets); - const float tolerance = sampler.filter == FilterPoint ? 0.0f : 1e-4f; - Require(std::fabs(Output[tid] - expected) <= tolerance, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected)); + Require(Output[tid] == 0.0f, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected 0"); } return result.variantId; } @@ -274,12 +234,11 @@ void RunChannels(AgcDriver::VulkanDevice& device, std::span device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); for (std::uint32_t tid = 0; tid < Threads; ++tid) { - const float compared = Expected(tid, sampler, false); for (std::uint32_t reg = 0; reg < 4u; ++reg) { float expected = -1.0f; if (reg < registers) { const auto select = channels[firstChannel + reg]; - expected = select == 0u ? 0.0f : select == 1u ? 1.0f : compared; + expected = select == 0u ? 0.0f : select == 1u ? 1.0f : 0.0f; } const float actual = Output[tid * 4u + reg]; Require(std::fabs(actual - expected) <= 1e-4f, std::string(name) + ": thread " + std::to_string(tid) + " register " + std::to_string(reg) + " returned " + std::to_string(actual) + ", expected " + std::to_string(expected)); @@ -304,7 +263,7 @@ void RunCube(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(CubeCode.data())); device.WaitIdle(); for (std::uint32_t tid = 0; tid < Threads; ++tid) { - Require(Output[tid] == 1.0f, std::string(name) + ": +X cube face comparison returned " + std::to_string(Output[tid]) + ", expected 1"); + Require(Output[tid] == 0.0f, std::string(name) + ": +X cube face comparison returned " + std::to_string(Output[tid]) + ", expected 0"); } } @@ -314,7 +273,7 @@ bool BindsDepthCompare(const ShaderRecompiler::RecompileResult& result) { }); } -void RunSrgbCubeZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name) { +void RunZeroCube(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name) { FillCubeTexels(); for (std::uint32_t tid = 0; tid < Threads; ++tid) { CubeInput[tid * 4u + 0u] = 0.5f; @@ -332,7 +291,7 @@ void RunSrgbCubeZero(AgcDriver::VulkanDevice& device, std::uint32_t format, cons } } -void RunSrgbZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name, bool useCache, std::uint64_t expectedVariant) { +void RunZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name, bool useCache, std::uint64_t expectedVariant) { Output.fill(-1.0f); const auto result = Compile(device, format, sampler, Code, useCache); if (useCache) Require(result.cacheHit && result.variantId == expectedVariant, std::string(name) + " did not reuse the compiled comparison artifact"); @@ -360,30 +319,44 @@ 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(); FillInput(); RunCube(*device, {ClampEdge, FilterPoint}, "point cube, clamp to edge"); - Reject(*device, Format1616UNorm, {ClampEdge, FilterBilinear}, "bilinear cube comparison requires DISABLE_CUBE_WRAP", CubeCode, true, true); + RunCube(*device, {ClampEdge, FilterBilinear}, "bilinear cube without DISABLE_CUBE_WRAP"); for (std::uint32_t function = 0; function < 8u; ++function) { const auto name = "bilinear 8_8_8_8 srgb cube function " + std::to_string(function); - RunSrgbCubeZero(*device, Format8888Srgb, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str()); + RunZeroCube(*device, Format8888Srgb, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str()); } - RunSrgbCubeZero(*device, Format8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8 srgb cube without DISABLE_CUBE_WRAP"); - RunSrgbCubeZero(*device, Format8_8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8_8 srgb cube without DISABLE_CUBE_WRAP"); + RunZeroCube(*device, Format8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8 srgb cube without DISABLE_CUBE_WRAP"); + RunZeroCube(*device, Format8_8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8_8 srgb cube without DISABLE_CUBE_WRAP"); RunCube(*device, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, true}, "bilinear cube with DISABLE_CUBE_WRAP"); - Reject(*device, Format9995Float, {ClampEdge, FilterPoint}, "implemented only for float, unorm and snorm formats"); + Reject(*device, Format9995Float, {ClampEdge, FilterPoint}, "implemented only for floating-point, UNORM and SNORM formats"); for (std::uint32_t function = 0; function < 8u; ++function) { const auto name = "bilinear 8_8_8_8 unorm cube function " + std::to_string(function); - RunSrgbCubeZero(*device, Format8888UNorm, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str()); + RunZeroCube(*device, Format8888UNorm, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str()); } - RunSrgbCubeZero(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear, BorderBlack, 0u, 0u, false, GreaterEqual}, "anisotropic bilinear 8_8_8_8 unorm cube without DISABLE_CUBE_WRAP"); - Reject(*device, Format1616UNorm, {ClampEdge, FilterAnisoBilinear}, "point or bilinear"); - Reject(*device, Format1616UNorm, {ClampEdge, FilterAnisoPoint}, "point or bilinear"); + RunZeroCube(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear, BorderBlack, 0u, 0u, false, GreaterEqual}, "anisotropic bilinear 8_8_8_8 unorm cube without DISABLE_CUBE_WRAP"); + RunZero(*device, Format1616UNorm, {ClampEdge, FilterAnisoBilinear}, "anisotropic bilinear 16_16 unorm", false, 0u); + RunZero(*device, Format1616UNorm, {ClampEdge, FilterAnisoPoint}, "anisotropic point 16_16 unorm", false, 0u); FillTexels(); + Require(BindsDepthCompare(Compile(*device, Format16UNorm, {ClampEdge, FilterPoint})), "an R16 unorm texture left the native comparison path"); Require(BindsDepthCompare(Compile(*device, Format32Float, {ClampEdge, FilterBilinear})), "an R32 float texture left the native comparison path"); Require(!BindsDepthCompare(Compile(*device, Format8888UNorm, {ClampEdge, FilterBilinear})), "a color texture kept a depth-compare binding"); + for (const auto format : {1u, 2u, 8u, 13u, 14u, 15u, 23u, 24u, 29u, 36u, 50u, 51u, 56u, 57u, 65u, 66u, 71u, 77u}) { + if (format == 51u && !device->SupportsImageFormat(VK_FORMAT_A2B10G10R10_SNORM_PACK32, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT)) { + std::puts("measured GFX10 color format 51 returns zero: skipped, Vulkan device does not support the image format"); + continue; + } + const auto name = "measured GFX10 color format " + std::to_string(format) + " returns zero"; + RunZero(*device, format, {ClampEdge, FilterPoint}, name.c_str(), false, 0u); + } + for (std::uint32_t function = 0; function < 8u; ++function) { + const auto name = "16_16 unorm compare function " + std::to_string(function) + " returns zero"; + RunZero(*device, Format1616UNorm, {ClampEdge, FilterPoint, BorderBlack, 0u, 0u, false, function}, name.c_str(), false, 0u); + } for (const bool useCache : {false, true}) { const auto variant = useCache ? Run(*device, {ClampEdge, FilterPoint}, "cached UNORM before sRGB", false, true) : 0u; if (useCache) Require(variant != 0, "the cached comparison has no compiled artifact identity"); @@ -391,7 +364,7 @@ int main() { for (std::uint32_t function = 0; function < 8u; ++function) { for (const auto filter : {FilterPoint, FilterBilinear}) { const auto name = std::string(useCache ? "cached" : "uncached") + " sRGB format " + std::to_string(format) + " function " + std::to_string(function) + (filter == FilterPoint ? " point" : " bilinear"); - RunSrgbZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), useCache, variant); + RunZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), useCache, variant); } } } @@ -402,13 +375,14 @@ int main() { for (const auto filter : {FilterPoint, FilterBilinear, FilterAnisoBilinear}) { if (format != Format8888UNorm && function != LessEqual) continue; const auto name = "8-bit unorm format " + std::to_string(format) + " function " + std::to_string(function) + " filter " + std::to_string(filter); - RunSrgbZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), false, 0u); + RunZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), false, 0u); } } } + RunZero(*device, Format1616UNorm, {ClampEdge, FilterPoint}, "16_16 unorm comparison returns zero", false, 0u); Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge"); Run(*device, {ClampWrap, FilterPoint}, "point, wrap"); - Run(*device, {ClampWrap, FilterPoint}, "point, wrap with negative offsets", true); + 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 +391,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 +419,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/F16FmaInvalidNan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F16FmaInvalidNan.cpp new file mode 100644 index 0000000000..da5cdec6ce --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F16FmaInvalidNan.cpp @@ -0,0 +1,141 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 10; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 2; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array FmaCode{ + 0x34020084u, 0x34040083u, 0xe03c1000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0xd74b000au, 0x041a0b04u, + 0x7e160306u, 0x6c160b04u, 0xe0741000u, 0x80010a02u, 0xbf810000u, +}; +alignas(256) constexpr std::array MixCode{ + 0x34020084u, 0x34040083u, 0xe03c1000u, 0x80000401u, 0xbf8c3f70u, 0xcc20600au, 0x1c120b04u, 0xcc20600bu, + 0x0c120d04u, 0xe0741000u, 0x80010a02u, 0xbf810000u, +}; + +using Rows = std::array, Threads>; + +constexpr Rows FmaRows{{ + {0x7c00u, 0x0000u, 0x0000u}, + {0x0000u, 0xfc00u, 0x3c00u}, + {0x8000u, 0x7c00u, 0xfc00u}, + {0x8000u, 0xfc00u, 0x3c00u}, + {0x7c00u, 0xbc00u, 0x7c00u}, + {0x7c00u, 0x8000u, 0xfc00u}, + {0x7c00u, 0x3c00u, 0xfc00u}, + {0xfc00u, 0x3c00u, 0x7c00u}, + {0x7c00u, 0x0000u, 0x7e00u}, + {0x8000u, 0xfc00u, 0x7d00u}, +}}; +constexpr Rows MixRows{{ + {0x00007c00u, 0x00000000u, 0x00000000u}, + {0x3c00fc00u, 0x00000000u, 0x80000000u}, + {0x7e007c00u, 0x00008000u, 0x80000000u}, + {0x7d00fc00u, 0x00000000u, 0x00000000u}, + {0x7c007c00u, 0x0000bc00u, 0xbf800000u}, + {0xfc007c00u, 0x00003c00u, 0x3f800000u}, + {0x7c00fc00u, 0x00004000u, 0x40000000u}, + {0xfc00fc00u, 0x0000b800u, 0xbf000000u}, + {0x7e00fc00u, 0x00008000u, 0x00000000u}, + {0x7fff7c00u, 0x00000000u, 0x80000000u}, +}}; + +struct Mode { + const char* name; + std::optional value; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const Rows& rows, const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + std::copy(rows[tid].begin(), rows[tid].end(), &Input[tid * Inputs]); + Input[tid * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const Mode& mode, const Rows& rows, const char* const (&names)[Results], std::uint32_t expected) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t form = 0; form < Results; ++form) { + const std::uint32_t actual = Output[tid * Results + form]; + Require(actual == expected, std::string(names[form]) + " " + mode.name + " of " + Hex(rows[tid][0]) + ", " + Hex(rows[tid][1]) + ", " + Hex(rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const Mode modes[] = { + {"without a known float mode", std::nullopt}, + {"with denormals flushed, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x00u, true, false, false}}, + {"with denormals flushed, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x00u, true, true, false}}, + {"with denormals kept, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}}, + {"with denormals kept, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}}, + }; + constexpr const char* FmaNames[Results] = {"v_fma_f16", "v_fmac_f16"}; + constexpr const char* MixNames[Results] = {"v_fma_mix_f32 of three f16 sources", "v_fma_mix_f32 of an f16, an f32 and an f16 source"}; + for (const Mode& mode : modes) { + Run(*device, FmaCode, FmaRows, mode); + Check(mode, FmaRows, FmaNames, 0xfe00u); + if (!mode.value) continue; + Run(*device, MixCode, MixRows, mode); + Check(mode, MixRows, MixNames, 0xffc00000u); + } + std::puts("f16 fma invalid operation NaN tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/F32DenormalFlush.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp index 374eaa63a3..19beb4db4c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp @@ -13,6 +13,7 @@ #include #include #include +#include namespace { @@ -389,6 +390,31 @@ bool IsNan(std::uint32_t value) { return (value & 0x7fffffffu) > 0x7f800000u; } +bool HardwareFlush = false; + +std::size_t CompiledWords(AgcDriver::VulkanDevice& device, bool hardwareFlush, bool& declaresFlush) { + std::vector userData(8, 0u); + const std::array memory{{{reinterpret_cast(Code.data()), std::as_bytes(std::span(Code))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(Code.data()), Code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.target.float32FlushToZero = hardwareFlush; + const auto result = ShaderRecompiler::Recompile(request); + const auto words = result.spirv.Words(); + declaresFlush = false; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + if ((words[cursor] & 0xffffu) == spv::OpExecutionMode && count == 4u && words[cursor + 2u] == spv::ExecutionModeDenormFlushToZero && words[cursor + 3u] == 32u) declaresFlush = true; + cursor += count; + } + return words.size(); +} + void Run(AgcDriver::VulkanDevice& device, const std::optional& floatMode, std::span code = Code, const std::uint32_t (*rows)[Inputs] = Rows) { for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy(std::begin(rows[tid]), std::end(rows[tid]), &Input[tid * Inputs]); Output.fill(0xdeadbeefu); @@ -407,6 +433,7 @@ void Run(AgcDriver::VulkanDevice& device, const std::optional(code.data())); device.WaitIdle(); @@ -477,44 +504,60 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device, std::nullopt); - Check("no float mode"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); - Check("IEEE=0 f32 denormals flushed"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0x00u, false, true, false}); - Check("IEEE=1 all denormals flushed"); - Run(*device, std::nullopt, MixedTinyCode, MixedTinyRows); - CheckMixedTiny("no float mode"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, MixedTinyCode, MixedTinyRows); - CheckMixedTiny("IEEE=0 mixed sources"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0x00u, false, true, false}, MixedTinyCode, MixedTinyRows); - CheckMixedTiny("IEEE=1 mixed sources"); - for (std::uint32_t round = 1u; round < 4u; ++round) { - Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u | round, true, false, false}, MixedTinyCode, MixedTinyRows); - CheckMixedTiny("mixed sources round mode " + std::to_string(round), round); + const auto capabilities = device->Target().supportedCapabilities; + const bool declared = std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityDenormFlushToZero)) != capabilities.end(); + const bool native = !declared && device->Target().float32FlushToZero; + const bool hardware = declared || native; + if (hardware) { + bool softwareDeclares = true; + bool hardwareDeclares = false; + const auto software = CompiledWords(*device, false, softwareDeclares); + const auto hardwareWords = CompiledWords(*device, true, hardwareDeclares); + Require(!softwareDeclares && hardwareDeclares == declared, declared ? "f32 denormal flush: only the hardware-flush module declares DenormFlushToZero 32" : "f32 denormal flush: a module flushed natively declares DenormFlushToZero 32 without the device capability"); + Require(hardwareWords < software, "f32 denormal flush: the hardware-flush module is " + std::to_string(hardwareWords) + " words, the emulated one " + std::to_string(software)); } - Run(*device, std::nullopt, TinyCode, TinyRows); - CheckTinyLegacy("no float mode"); - CheckTiny("no float mode"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, TinyCode, TinyRows); - CheckTinyLegacy("IEEE=0 f32 denormals flushed"); - CheckTiny("IEEE=0 f32 denormals flushed"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0x00u, false, true, false}, TinyCode, TinyRows); - CheckTinyLegacy("IEEE=1 all denormals flushed"); - CheckTiny("IEEE=1 all denormals flushed"); - for (std::uint32_t round = 1u; round < 4u; ++round) { - Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u | round, true, false, false}, TinyCode, TinyRows); - CheckTinyLegacy("round mode " + std::to_string(round)); - CheckTinyDirected(round); - } - for (const std::uint32_t mode : {0xd0u, 0xe0u}) { - bool refused = false; - try { - Run(*device, ShaderRecompiler::ShaderFloatMode{mode, true, false, false}); - } catch (const std::runtime_error& error) { - refused = std::string(error.what()).find("f32 denormal mode") != std::string::npos; + for (const bool flush : {false, true}) { + if (flush && !hardware) continue; + HardwareFlush = flush; + Run(*device, std::nullopt); + Check("no float mode"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + Check("IEEE=0 f32 denormals flushed"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0x00u, false, true, false}); + Check("IEEE=1 all denormals flushed"); + Run(*device, std::nullopt, MixedTinyCode, MixedTinyRows); + CheckMixedTiny("no float mode"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, MixedTinyCode, MixedTinyRows); + CheckMixedTiny("IEEE=0 mixed sources"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0x00u, false, true, false}, MixedTinyCode, MixedTinyRows); + CheckMixedTiny("IEEE=1 mixed sources"); + for (std::uint32_t round = 1u; round < 4u; ++round) { + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u | round, true, false, false}, MixedTinyCode, MixedTinyRows); + CheckMixedTiny("mixed sources round mode " + std::to_string(round), round); + } + Run(*device, std::nullopt, TinyCode, TinyRows); + CheckTinyLegacy("no float mode"); + CheckTiny("no float mode"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, TinyCode, TinyRows); + CheckTinyLegacy("IEEE=0 f32 denormals flushed"); + CheckTiny("IEEE=0 f32 denormals flushed"); + Run(*device, ShaderRecompiler::ShaderFloatMode{0x00u, false, true, false}, TinyCode, TinyRows); + CheckTinyLegacy("IEEE=1 all denormals flushed"); + CheckTiny("IEEE=1 all denormals flushed"); + for (std::uint32_t round = 1u; round < 4u; ++round) { + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u | round, true, false, false}, TinyCode, TinyRows); + CheckTinyLegacy("round mode " + std::to_string(round)); + CheckTinyDirected(round); + } + for (const std::uint32_t mode : {0xd0u, 0xe0u}) { + bool refused = false; + try { + Run(*device, ShaderRecompiler::ShaderFloatMode{mode, true, false, false}); + } catch (const std::runtime_error& error) { + refused = std::string(error.what()).find("f32 denormal mode") != std::string::npos; + } + Require(refused, "f32 denormal flush: FLOAT_MODE " + Hex(mode) + " was not refused"); } - Require(refused, "f32 denormal flush: FLOAT_MODE " + Hex(mode) + " was not refused"); } std::puts("f32 denormal flush tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F64AddMulDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64AddMulDenormalModes.cpp new file mode 100644 index 0000000000..afacd844cb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64AddMulDenormalModes.cpp @@ -0,0 +1,224 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 16; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xd564000au, 0x00020d04u, 0xd565000cu, + 0x00020d04u, 0xe0781000u, 0x80010a02u, 0xbf810000u, +}; + +constexpr std::uint64_t Pairs[Threads][2] = { + {0x0000000000000001ull, 0x0000000000000000ull}, + {0x8000000000000001ull, 0x0000000000000001ull}, + {0x000fffffffffffffull, 0x0000000000000001ull}, + {0x0010000000000000ull, 0x8000000000000001ull}, + {0x0010000000000001ull, 0x8010000000000000ull}, + {0x0000000000000001ull, 0x3ff0000000000000ull}, + {0x000fffffffffffffull, 0x3ff0000000000001ull}, + {0x0010000000000001ull, 0x3feffffffffffffeull}, + {0x2000000000000001ull, 0x1ffffffffffffffeull}, + {0x0010000000000000ull, 0x3fe0000000000000ull}, + {0xbff0000000000000ull, 0x800fffffffffffffull}, + {0x3ca0000000000000ull, 0x0020000000000000ull}, + {0x4330000000000000ull, 0x0000000000000001ull}, + {0x8000000000000000ull, 0x8000000000000000ull}, + {0x0000000000000000ull, 0x800fffffffffffffull}, + {0x3fe0000000000000ull, 0x0010000000000001ull}, +}; +constexpr std::uint64_t Expected[4][Threads][2] = { + { + {0x0000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x0000000000000000ull, 0x0000000000000000ull}, + {0x0010000000000000ull, 0x8000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x3ff0000000000000ull, 0x0000000000000000ull}, + {0x3ff0000000000001ull, 0x0000000000000000ull}, + {0x3feffffffffffffeull, 0x0010000000000000ull}, + {0x2010000000000000ull, 0x0010000000000000ull}, + {0x3fe0000000000000ull, 0x0000000000000000ull}, + {0xbff0000000000000ull, 0x0000000000000000ull}, + {0x3ca0000000000000ull, 0x0000000000000000ull}, + {0x4330000000000000ull, 0x0000000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x3fe0000000000000ull, 0x0000000000000000ull}, + }, + { + {0x0000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x0010000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x3ff0000000000000ull, 0x0000000000000000ull}, + {0x3ff0000000000001ull, 0x0010000000000000ull}, + {0x3feffffffffffffeull, 0x0010000000000000ull}, + {0x2010000000000000ull, 0x0010000000000000ull}, + {0x3fe0000000000000ull, 0x0000000000000000ull}, + {0xbff0000000000000ull, 0x0000000000000000ull}, + {0x3ca0000000000000ull, 0x0000000000000000ull}, + {0x4330000000000000ull, 0x0010000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull}, + {0x8000000000000000ull, 0x8000000000000000ull}, + {0x3fe0000000000000ull, 0x0000000000000000ull}, + }, + { + {0x0000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x0000000000000000ull, 0x0000000000000000ull}, + {0x0010000000000000ull, 0x8000000000000000ull}, + {0x0000000000000001ull, 0x8000000000000000ull}, + {0x3ff0000000000000ull, 0x0000000000000000ull}, + {0x3ff0000000000001ull, 0x0000000000000000ull}, + {0x3feffffffffffffeull, 0x0010000000000000ull}, + {0x2010000000000000ull, 0x0010000000000000ull}, + {0x3fe0000000000000ull, 0x0008000000000000ull}, + {0xbff0000000000000ull, 0x0000000000000000ull}, + {0x3ca0000000000000ull, 0x0000000000000001ull}, + {0x4330000000000000ull, 0x0000000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x3fe0000000000000ull, 0x0008000000000000ull}, + }, + { + {0x0000000000000001ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x0010000000000000ull, 0x0000000000000000ull}, + {0x000fffffffffffffull, 0x8000000000000000ull}, + {0x0000000000000001ull, 0x8000000000000000ull}, + {0x3ff0000000000000ull, 0x0000000000000001ull}, + {0x3ff0000000000001ull, 0x0010000000000000ull}, + {0x3feffffffffffffeull, 0x0010000000000000ull}, + {0x2010000000000000ull, 0x0010000000000000ull}, + {0x3fe0000000000000ull, 0x0008000000000000ull}, + {0xbff0000000000000ull, 0x000fffffffffffffull}, + {0x3ca0000000000000ull, 0x0000000000000001ull}, + {0x4330000000000000ull, 0x0010000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull}, + {0x800fffffffffffffull, 0x8000000000000000ull}, + {0x3fe0000000000000ull, 0x0008000000000000ull}, + }, +}; + +struct Mode { + const char* name; + std::optional value; + std::uint32_t field; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%016llx", static_cast(value)); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * Inputs] = static_cast(Pairs[tid][0]); + Input[tid * Inputs + 1u] = static_cast(Pairs[tid][0] >> 32u); + Input[tid * Inputs + 2u] = static_cast(Pairs[tid][1]); + Input[tid * Inputs + 3u] = static_cast(Pairs[tid][1] >> 32u); + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +std::uint32_t Check(const Mode& mode, bool keepsDenormals) { + constexpr const char* Names[2] = {"v_add_f64", "v_mul_f64"}; + const std::array deviceFlushes{mode.field & 2u, mode.field & 1u, 0u}; + std::uint32_t skipped = 0; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t form = 0; form < 2u; ++form) { + const std::uint64_t actual = Output[tid * Results + form * 2u] | (static_cast(Output[tid * Results + form * 2u + 1u]) << 32u); + const std::uint64_t expected = Expected[mode.field][tid][form]; + if (actual == expected) continue; + const std::string message = std::string(Names[form]) + " " + mode.name + " of " + Hex(Pairs[tid][0]) + " and " + Hex(Pairs[tid][1]) + " is " + Hex(actual) + ", expected " + Hex(expected); + const bool deviceDependent = std::any_of(deviceFlushes.begin(), deviceFlushes.end(), [&](std::uint32_t flushed) { return Expected[flushed][tid][form] != expected; }); + Require(deviceDependent && !keepsDenormals, message); + std::printf("skipped %s, the device flushes f64 denormals without DenormPreserve support\n", message.c_str()); + ++skipped; + } + } + return skipped; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityFloat64)) { + std::puts("skipped, the device has no shaderFloat64"); + return VulkanTestSkipped; + } + const Mode modes[] = { + {"without a known float mode", std::nullopt, 3u}, + {"f64 denormal mode 0, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, 0u}, + {"f64 denormal mode 0, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}, 0u}, + {"f64 denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x70u, true, true, false}, 1u}, + {"f64 denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x70u, true, false, false}, 1u}, + {"f64 denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xb0u, true, true, false}, 2u}, + {"f64 denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xb0u, true, false, false}, 2u}, + {"f64 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, 3u}, + {"f64 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, 3u}, + }; + bool keepsDenormals = device->Target().float64DenormPreserve; + std::uint32_t skipped = 0; + for (const Mode& mode : modes) { + Run(*device, mode); + const std::uint32_t modeSkipped = Check(mode, keepsDenormals); + keepsDenormals = keepsDenormals || (!mode.value.has_value() && modeSkipped == 0u); + skipped += modeSkipped; + } + std::printf("f64 add and mul denormal mode tests passed, %u device-dependent results skipped\n", skipped); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F64Arithmetic.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64Arithmetic.cpp index 38ff1defd3..b4db75b187 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F64Arithmetic.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64Arithmetic.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -154,15 +155,13 @@ void Expect(std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, con Require(actual == expected, std::string(Ieee ? "f64 arithmetic, IEEE mode: lane " : "f64 arithmetic: lane ") + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected)); } -void Run(AgcDriver::VulkanDevice& device) { - for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); +void Run(AgcDriver::VulkanDevice& device, std::span code, std::optional 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{ @@ -172,7 +171,7 @@ void Run(AgcDriver::VulkanDevice& device) { {0, 0, 0, 128} }; request.useCache = false; - if (Ieee) request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}; + request.context.floatMode = mode; const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); @@ -190,6 +189,88 @@ void Check() { } } +std::uint64_t MinMaxReference(std::uint64_t lhs, std::uint64_t rhs, bool maximum, bool ieee) { + constexpr std::uint64_t magnitude = 0x7fffffffffffffffull; + constexpr std::uint64_t infinity = 0x7ff0000000000000ull; + constexpr std::uint64_t quiet = 0x0008000000000000ull; + const bool lhsNan = (lhs & magnitude) > infinity; + const bool rhsNan = (rhs & magnitude) > infinity; + if (ieee && lhsNan && (lhs & quiet) == 0u) return lhs | quiet; + if (ieee && rhsNan && (rhs & quiet) == 0u) return rhs | quiet; + if (rhsNan) return lhs; + if (lhsNan) return rhs; + if ((lhs & magnitude) == 0u && (rhs & magnitude) == 0u) return maximum ? lhs & rhs : lhs | rhs; + return maximum ? (std::bit_cast(lhs) > std::bit_cast(rhs) ? lhs : rhs) : (std::bit_cast(lhs) < std::bit_cast(rhs) ? lhs : rhs); +} + +void CheckMinMaxDenormals(AgcDriver::VulkanDevice& device) { + const bool canPreserveDenormals = device.Target().float64DenormPreserve; + constexpr std::array, Threads> rows{{ + {0x0000000000000001ull, 0x0000000000000000ull}, {0x0000000000000000ull, 0x0000000000000001ull}, + {0x8000000000000001ull, 0x0000000000000000ull}, {0x0000000000000000ull, 0x8000000000000001ull}, + {0x0000000000000001ull, 0x8000000000000001ull}, {0x8000000000000001ull, 0x0000000000000001ull}, + {0x000fffffffffffffull, 0x800fffffffffffffull}, {0x800fffffffffffffull, 0x000fffffffffffffull}, + {0x000fffffffffffffull, 0x0010000000000000ull}, {0x0010000000000000ull, 0x000fffffffffffffull}, + {0x800fffffffffffffull, 0x8010000000000000ull}, {0x8010000000000000ull, 0x800fffffffffffffull}, + {0x0000000000000001ull, 0x000fffffffffffffull}, {0x000fffffffffffffull, 0x0000000000000001ull}, + {0x8000000000000001ull, 0x800fffffffffffffull}, {0x800fffffffffffffull, 0x8000000000000001ull}, + {0x7ff8000000000123ull, 0x0000000000000001ull}, {0x0000000000000001ull, 0x7ff8000000000123ull}, + {0xfff8000000000123ull, 0x800fffffffffffffull}, {0x800fffffffffffffull, 0xfff8000000000123ull}, + {0x7ff0000000000123ull, 0x0000000000000001ull}, {0x0000000000000001ull, 0x7ff0000000000123ull}, + {0xfff0000000000123ull, 0x800fffffffffffffull}, {0x800fffffffffffffull, 0xfff0000000000123ull}, + {0x7ff0000000000000ull, 0x000fffffffffffffull}, {0x000fffffffffffffull, 0x7ff0000000000000ull}, + {0xfff0000000000000ull, 0x800fffffffffffffull}, {0x800fffffffffffffull, 0xfff0000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, {0x8000000000000000ull, 0x0000000000000000ull}, + {0x0010000000000000ull, 0x8010000000000000ull}, {0x8010000000000000ull, 0x0010000000000000ull}, + }}; + constexpr std::array, 4> modifiers{{{0u, 0u}, {1u, 2u}, {2u, 1u}, {3u, 3u}}}; + for (const auto& modifier : modifiers) { + alignas(256) auto code = Code; + for (const auto word : {13u, 15u}) { + code[word] |= modifier[0] << 8u; + code[word + 1u] |= modifier[1] << 29u; + } + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t source = 0; source < 2u; ++source) { + Input[tid * Inputs + source * 2u] = static_cast(rows[tid][source]); + Input[tid * Inputs + source * 2u + 1u] = static_cast(rows[tid][source] >> 32u); + } + } + for (const int denormals : {-1, 0, 2, 3}) { + for (const bool ieee : {false, true}) { + if (denormals < 0 && ieee) continue; + std::optional mode; + if (denormals >= 0) mode = ShaderRecompiler::ShaderFloatMode{0x30u | (static_cast(denormals) << 6u), true, ieee, false}; + Run(device, code, mode); + const bool keepsDenormals = denormals < 0 || (denormals & 1) != 0; + std::uint32_t skippedWords = 0u; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + auto pair = rows[tid]; + for (std::uint32_t source = 0; source < 2u; ++source) { + if (((modifier[0] >> source) & 1u) != 0u) pair[source] &= 0x7fffffffffffffffull; + if (((modifier[1] >> source) & 1u) != 0u) pair[source] ^= 0x8000000000000000ull; + if ((denormals == 0 || denormals == 2) && (pair[source] & 0x7ff0000000000000ull) == 0u) pair[source] &= 0x8000000000000000ull; + } + for (std::uint32_t operation = 0; operation < 2u; ++operation) { + const auto expected = MinMaxReference(pair[0], pair[1], operation != 0u, ieee); + for (std::uint32_t word = 0; word < 2u; ++word) { + const auto actual = Output[tid * Results + 8u + operation * 2u + word]; + const auto expectedWord = static_cast(expected >> (word * 32u)); + if (actual != expectedWord && keepsDenormals && !canPreserveDenormals) { + ++skippedWords; + continue; + } + Require(actual == expectedWord, "f64 min/max denormal mode " + std::to_string(denormals) + " IEEE " + std::to_string(ieee) + " abs " + std::to_string(modifier[0]) + " neg " + std::to_string(modifier[1]) + " lane " + std::to_string(tid) + " " + Names[8u + operation * 2u + word] + " is " + Hex(actual) + ", expected " + Hex(expectedWord)); + } + } + } + if (skippedWords != 0u) std::printf("skipped %u f64 min/max result words in denormal mode %d IEEE %u abs %u neg %u: device has no f64 DenormPreserve\n", skippedWords, denormals, ieee ? 1u : 0u, modifier[0], modifier[1]); + } + } + } + std::puts("f64 min/max denormal input checks completed"); +} + } int main() { @@ -202,9 +283,11 @@ int main() { } for (const bool ieee : {false, true}) { Ieee = ieee; - Run(*device); + for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); + Run(*device, Code, ieee ? std::optional{ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}} : std::nullopt); Check(); } + CheckMinMaxDenormals(*device); std::puts("f64 arithmetic tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F64RoundingDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64RoundingDenormalModes.cpp new file mode 100644 index 0000000000..e64de0f152 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64RoundingDenormalModes.cpp @@ -0,0 +1,169 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040086u, 0xe0341000u, 0x80000401u, 0xbf8c3f70u, 0x7e143504u, 0x7e183104u, 0x7e1c7d04u, + 0x7e207b04u, 0x7e247904u, 0xe0781000u, 0x80010a02u, 0xe0781010u, 0x80010e02u, 0xe0701020u, 0x80011202u, + 0xbf810000u, +}; + +constexpr std::array Values{ + 0x0000000000000000ull, 0x8000000000000000ull, 0x0000000000000001ull, 0x8000000000000001ull, 0x000fffffffffffffull, 0x800fffffffffffffull, + 0x0008000000000000ull, 0x8008000000000000ull, 0x0010000000000000ull, 0x8010000000000000ull, 0x3fe0000000000000ull, 0xbff8000000000000ull, + 0x4006000000000000ull, 0x3fefffffffffffffull, 0xbfefffffffffffffull, 0x7fefffffffffffffull, 0xffefffffffffffffull, 0x4330000000000001ull, +}; + +struct Mode { + const char* name; + std::optional value; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%016llx", static_cast(value)); + return text; +} + +std::uint64_t ValueAt(std::uint32_t tid) { + return tid < Values.size() ? Values[tid] : 0u; +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * Inputs] = static_cast(ValueAt(tid)); + Input[tid * Inputs + 1u] = static_cast(ValueAt(tid) >> 32u); + Input[tid * Inputs + 2u] = 0u; + Input[tid * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +std::uint64_t Flush(std::uint64_t bits, bool flushed) { + return flushed && (bits & 0x7ff0000000000000ull) == 0u ? bits & 0x8000000000000000ull : bits; +} + +std::uint64_t Bits(double value) { + return std::bit_cast(value); +} + +std::array Expected(std::uint64_t source, std::uint32_t field) { + const double value = std::bit_cast(Flush(source, (field & 1u) == 0u)); + const double below = std::bit_cast(0x3fefffffffffffffull); + int exponent = 0; + const double mantissa = std::frexp(value, &exponent); + return { + Bits(std::floor(value)), + Bits(std::ceil(value)), + Flush(Bits(std::min(value - std::floor(value), below)), (field & 2u) == 0u), + Bits(mantissa), + static_cast(exponent), + }; +} + +std::uint32_t Check(const Mode& mode, bool keepsDenormals) { + const std::uint32_t field = mode.value.has_value() ? mode.value->floatMode >> 6u : 3u; + const std::array deviceFlushes{field & 2u, field & 1u, 0u}; + constexpr const char* Names[5] = {"v_floor_f64", "v_ceil_f64", "v_fract_f64", "v_frexp_mant_f64", "v_frexp_exp_i32_f64"}; + std::uint32_t skipped = 0; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint64_t source = ValueAt(tid); + const auto expected = Expected(source, field); + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t index = 0; index < expected.size(); ++index) { + const std::uint64_t actual = index < 4u ? out[index * 2u] | (static_cast(out[index * 2u + 1u]) << 32u) : out[8]; + if (actual == expected[index]) continue; + const auto text = [index](std::uint64_t bits) { return index < 4u ? Hex(bits) : std::to_string(static_cast(bits)); }; + const std::string message = std::string(Names[index]) + " " + mode.name + " of " + Hex(source) + " is " + text(actual) + ", expected " + text(expected[index]); + const bool deviceDependent = std::any_of(deviceFlushes.begin(), deviceFlushes.end(), [&](std::uint32_t flushed) { return Expected(source, flushed)[index] != expected[index]; }); + Require(deviceDependent && !keepsDenormals, message); + std::printf("skipped %s, the device flushes f64 denormals without DenormPreserve support\n", message.c_str()); + ++skipped; + } + } + return skipped; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityFloat64)) { + std::puts("skipped, the device has no shaderFloat64"); + return VulkanTestSkipped; + } + const Mode modes[] = { + {"without a known float mode", std::nullopt}, + {"f64 denormal mode 0, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}}, + {"f64 denormal mode 0, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}}, + {"f64 denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x70u, true, true, false}}, + {"f64 denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x70u, true, false, false}}, + {"f64 denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xb0u, true, true, false}}, + {"f64 denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xb0u, true, false, false}}, + {"f64 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}}, + {"f64 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}}, + }; + bool keepsDenormals = device->Target().float64DenormPreserve; + std::uint32_t skipped = 0; + for (const Mode& mode : modes) { + Run(*device, mode); + const std::uint32_t modeSkipped = Check(mode, keepsDenormals); + keepsDenormals = keepsDenormals || (!mode.value.has_value() && modeSkipped == 0u); + skipped += modeSkipped; + } + std::printf("f64 rounding denormal mode tests passed, %u device-dependent results skipped\n", skipped); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp new file mode 100644 index 0000000000..0382270371 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FailedCaptureDump.cpp @@ -0,0 +1,143 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "ControlFlow/RequestSerializer.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array Code{ + 0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e020280u, 0xe0702000u, 0x80020100u, 0xbf810000u, 0u, +}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +alignas(256) std::array Output{}; +alignas(16) std::array Valid{}; +alignas(16) std::array Unsupported{}; + +std::array BufferDescriptor(std::uint32_t word3) { + const auto address = reinterpret_cast(Output.data()); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), static_cast(Output.size()), word3}; +} + +std::array Pointer(const void* descriptor) { + const auto address = reinterpret_cast(descriptor); + return {static_cast(address), static_cast(address >> 32u)}; +} + +void Dispatch(const std::array& userData) { + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{64, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(Code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{static_cast(userData.size()) << 1u}; + registers(0x213, resource); + registers(0x240, userData); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); +} + +std::string Read(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + Require(file.is_open(), "shader request " + path.string() + " was not written"); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +std::string Replay(const std::string& text) { + const auto replay = ShaderRecompiler::RequestSerializer{}.Deserialize(text); + try { + static_cast(ShaderRecompiler::Recompile(replay.request)); + } catch (const std::exception& error) { + return error.what(); + } + return {}; +} + +std::vector UserData(const std::string& text) { + const auto replay = ShaderRecompiler::RequestSerializer{}.Deserialize(text); + return {replay.request.context.userData.begin(), replay.request.context.userData.end()}; +} + +void Run() { + char prefix[64]; + std::snprintf(prefix, sizeof(prefix), "shader_%llx", static_cast(reinterpret_cast(Code.data()))); + const std::string first = std::string(prefix) + ".req"; + const std::string failed = std::string(prefix) + "_failed.req"; + Valid = BufferDescriptor(0x00014204u); + Unsupported = BufferDescriptor(0xa1b00facu); + Output.fill(Untouched); + const auto valid = Pointer(Valid.data()); + Dispatch(valid); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); + for (const auto word : Output) Require(word == 0u, "the dispatch through the valid descriptor stored nothing"); + const auto unsupported = Pointer(Unsupported.data()); + std::string message; + try { + Dispatch(unsupported); + } catch (const std::exception& error) { + message = error.what(); + } + Require(message.find("unsupported type") != std::string::npos, "the dispatch through a type 2 descriptor failed for another reason: " + message); + Require(message.find("(" + failed + ")") != std::string::npos, "the failure does not name the failing dispatch's request: " + message); + const auto firstText = Read(first); + const auto failedText = Read(failed); + Require(UserData(firstText) == std::vector(valid.begin(), valid.end()), "the first dispatch's request lost its user data"); + Require(UserData(failedText) == std::vector(unsupported.begin(), unsupported.end()), "the failed request holds another dispatch's user data"); + Require(Replay(firstText).empty(), "the first dispatch's request does not replay: " + Replay(firstText)); + const auto replayed = Replay(failedText); + Require(replayed.find("unsupported type") != std::string::npos, "the failed request does not replay its failure: " + replayed); +} + +} + +int main() { + const auto directory = std::filesystem::temp_directory_path() / ("agc_failed_capture_dump-" + std::to_string(std::random_device{}())); + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + std::filesystem::remove_all(directory); + std::filesystem::create_directories(directory); + std::filesystem::current_path(directory); +#ifdef _WIN32 + Require(_putenv_s("APS5_DUMP_SHADERS", "1") == 0, "cannot enable shader capture"); +#else + Require(setenv("APS5_DUMP_SHADERS", "1", 1) == 0, "cannot enable shader capture"); +#endif + Run(); + try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {} + std::filesystem::current_path(directory.parent_path()); + std::filesystem::remove_all(directory); + std::puts("failed capture dump tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {} + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/FlipStoresSmallResults.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlipStoresSmallResults.cpp new file mode 100644 index 0000000000..34f0951dc6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlipStoresSmallResults.cpp @@ -0,0 +1,155 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +constexpr std::uint32_t Threads = 8; +constexpr std::uint32_t Side = 16; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t OutputHandle = 7; +constexpr std::size_t BlockBytes = 0x20000; +constexpr std::uint32_t Untouched = 0xdeadbeefu; +constexpr std::uint32_t Written = 0x12345678u; + +alignas(256) constexpr std::array StoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, Written, 0xf0200108, 0x00020102, 0xbf810000, +}; + +alignas(4) std::atomic Gate = 0; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::uint32_t Low(const void* data) { + return static_cast(AddressOf(data)); +} + +std::uint32_t High(const void* data) { + return static_cast(AddressOf(data) >> 32u); +} + +std::array TextureDescriptor(const std::uint32_t* texels) { + const auto address = AddressOf(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +std::vector Frame(const std::uint32_t* texels) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::vector words; + const auto registers = [&](const std::uint32_t first, const std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{Threads, 1, 1}; + registers(0x207, shape); + const std::array program{static_cast(AddressOf(StoreCode.data()) >> 8u), static_cast(AddressOf(StoreCode.data()) >> 40u)}; + registers(0x20c, program); + const std::array resource{static_cast(userData.size()) << 1u}; + registers(0x213, resource); + registers(0x240, userData); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u}); + words.insert(words.end(), {AgcDriver::FlipPacketHeader, OutputHandle, 0xfffffffeu, 1, 0, 0}); + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(&Gate), High(&Gate), 1u, 0xffffffffu, 0x19u}); + return words; +} + +class Request final : public AgcDriver::IFlipRequest { +public: + explicit Request(std::atomic& ready) : ready(ready) {} + void GpuReady(const std::shared_ptr&) override { ++ready; } + void Fail(std::exception_ptr) noexcept override {} +private: + std::atomic& ready; +}; + +class Output final : public AgcDriver::IVideoOutput { +public: + std::shared_ptr Reserve(const AgcDriver::FlipInfo&) override { return std::make_shared(ready); } + void Fail(std::exception_ptr) noexcept override {} + std::atomic ready = 0; +}; + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + std::vector storage((BlockBytes + 0x10000u) / 4u); + auto* texels = reinterpret_cast((reinterpret_cast(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}); + std::fill(texels, texels + BlockBytes / 4u, Untouched); + { + GuestAllocations::Mutation mutation; + mutation.Add(texels, BlockBytes, true, true, true); + } + const auto bytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels))).guestBytes; + Require(bytes <= AgcDriver::Graphics::StorageTexture::SmallStoreBytes, "the test image is larger than an image the flip stores"); + const volatile std::uint32_t* cpu = texels; + const auto stored = [&] { + for (std::size_t index = 0; index < bytes / 4u; ++index) { + if (cpu[index] == Written) return true; + } + return false; + }; + auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(OutputHandle, output); + + auto words = Frame(texels); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while ((output->ready.load() == 0 || !stored()) && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const bool flipped = output->ready.load() == 1; + const bool seen = stored(); + Gate.store(1, std::memory_order_release); + + AgcDriverWaitIdle_nid_postfix(); + AgcDriverUnregisterVideoOutput_nid_postfix(OutputHandle, output); + AgcDriverShutdown_nid_postfix(); + { + GuestAllocations::Mutation mutation; + mutation.Remove(texels); + } + Require(flipped, "the flip did not reach the GPU"); + Require(seen, "a small image written before a flip did not reach guest memory while the queue kept working"); + std::puts("flip stores small results tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp index 8c6b30f646..a70e3086fd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp @@ -85,7 +85,6 @@ const std::array PackedChecks{{ struct Mode { const char* name; std::optional floatMode; - bool packed = true; }; const std::array Modes{{ @@ -94,8 +93,8 @@ const std::array Modes{{ {"dx10", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}}, {"ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, false}}, {"none", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, false}}, - {"f16 denormals flushed, dx10 ieee", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, false}, - {"f16 input denormals flushed", ShaderRecompiler::ShaderFloatMode{0xb0u, false, false, false}, false}, + {"f16 denormals flushed, dx10 ieee", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}}, + {"f16 input denormals flushed", ShaderRecompiler::ShaderFloatMode{0xb0u, false, false, false}}, }}; std::uint64_t Seed = 0x2545f4914f6cdd1dull; @@ -214,14 +213,7 @@ struct Model { bits[index] = (registers[source.reg] >> (selectHigh ? 16u : 0u)) & 0xffffu; if (high != 0u ? source.negateHi : source.negate) bits[index] ^= 0x8000u; } - std::uint32_t lane = Numeric(check.kind == Kind::Max, Read(bits[0]), Read(bits[1])).bits; - lane = IsNan(bits[0]) ? bits[1] : lane; - lane = IsNan(bits[1]) ? bits[0] : lane; - if (ieee) { - lane = IsSnan(bits[1]) ? Quiet(bits[1]) : lane; - lane = IsSnan(bits[0]) ? Quiet(bits[0]) : lane; - } - result |= Clamp(check.clamp, lane) << (16u * high); + result |= Clamp(check.clamp, Pair(check.kind == Kind::Max, Read(bits[0]), Read(bits[1])).bits) << (16u * high); } return result; } @@ -267,8 +259,7 @@ int main() { const bool flushInputs = mode.floatMode.has_value() && (mode.floatMode->floatMode & 0x40u) == 0u; const Model model{mode.floatMode.has_value() && mode.floatMode->ieeeMode, !mode.floatMode.has_value() || mode.floatMode->dx10Clamp, flushInputs}; const auto scalar = compile(ScalarCode, mode); - std::optional packed; - if (mode.packed) packed = compile(PackedCode, mode); + const auto packed = compile(PackedCode, mode); for (std::uint32_t batch = 0; batch < Batches; ++batch) { for (std::uint32_t tid = 0; tid < Threads; ++tid) { std::uint32_t* row = &Input[tid * Inputs]; @@ -293,7 +284,7 @@ int main() { } }; run(scalar, ScalarCode, ScalarChecks, false); - if (packed) run(*packed, PackedCode, PackedChecks, true); + run(packed, PackedCode, PackedChecks, true); } } std::printf("f16 min/max tests passed (%llu results)\n", static_cast(compared)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp new file mode 100644 index 0000000000..959ceebe7b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp @@ -0,0 +1,238 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +constexpr std::uint32_t Stride = 16; +constexpr std::uint32_t Batches = 32; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, + 0x64140b04, + 0x66160b04, + 0x68180b04, + 0x6a1a0b04, + 0xd74b000e, 0x041a0b04, + 0xd74b980f, 0x041a0b04, + 0xd74b3110, 0xc4160d04, + 0x7e220306, 0x6c220b04, + 0x6e240b04, 0x00003d55, + 0x70260b04, 0x00000001, + 0xd74ba214, 0x241a0b04, + 0x662a08f2, + 0xd5328116, 0x20020b04, + 0xd5350217, 0x40020d04, + 0xd5338018, 0x00020b04, + 0xd5340019, 0x20020b04, + 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xbf810000, +}; + +constexpr const char* Names[Results] = { + "v_add_f16 v10, v4, v5", + "v_sub_f16 v11, v4, v5", + "v_subrev_f16 v12, v4, v5", + "v_mul_f16 v13, v4, v5", + "v_fma_f16 v14, v4, v5, v6", + "v_fma_f16 v15, v4, v5, v6 op_sel:[1,1,0,0] clamp", + "v_fma_f16 v16, |v4|, -v6, -v5 op_sel:[0,1,1,0]", + "v_fmac_f16 v17, v4, v5", + "v_fmamk_f16 v18, v4, 0x3d55, v5", + "v_fmaak_f16 v19, v4, v5, 0x0001", + "v_fma_f16 v20, -v4, |v5|, v6 op_sel:[0,0,1,0] clamp", + "v_sub_f16 v21, 1.0, v4", + "v_add_f16 v22, -|v4|, v5 clamp", + "v_mul_f16 v23, v4, -|v6|", + "v_sub_f16 v24, v4, v5 clamp", + "v_subrev_f16 v25, -v4, v5", +}; + +struct Mode { + const char* name; + std::optional floatMode; + bool native = true; +}; + +const std::array Modes{{ + {"no FLOAT_MODE", std::nullopt}, + {"dx10 ieee ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, true}}, + {"dx10 ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}}, + {"dx10 ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, true}}, + {"dx10", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}}, + {"ieee ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, true}}, + {"ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, false}}, + {"ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, true}}, + {"none", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, false}}, + {"f16 denormals flushed", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}, false}, +}}; + +std::uint64_t Seed = 0x9e3779b97f4a7c15ull; + +std::uint32_t Next() { + Seed += 0x9e3779b97f4a7c15ull; + std::uint64_t z = Seed; + z = (z ^ (z >> 30u)) * 0xbf58476d1ce4e5b9ull; + z = (z ^ (z >> 27u)) * 0x94d049bb133111ebull; + return static_cast(z ^ (z >> 31u)); +} + +std::uint32_t Half() { + const std::uint32_t bits = Next(); + const std::uint32_t sign = (bits >> 31u) << 15u; + const std::uint32_t mantissa = bits & 0x3ffu; + switch ((bits >> 16u) % 10u) { + case 0u: return sign; + case 1u: return sign | std::max(mantissa, 1u); + case 2u: return sign | 0x7c00u; + case 3u: return sign | 0x7c00u | std::max(mantissa, 1u); + case 4u: return sign | ((0x1du + (bits >> 20u) % 2u) << 10u) | mantissa; + case 5u: return sign | ((1u + (bits >> 20u) % 4u) << 10u) | mantissa; + case 6u: return sign | ((0x0du + (bits >> 20u) % 5u) << 10u) | mantissa; + default: return bits & 0xffffu; + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +struct F16x2Math { + bool add = false; + bool mul = false; + bool fma = false; +}; + +F16x2Math FindF16x2Math(std::span spirv) { + std::optional half; + std::optional pair; + F16x2Math math; + for (std::size_t offset = 5u; offset < spirv.size();) { + const std::uint32_t words = spirv[offset] >> 16u; + const std::uint32_t opcode = spirv[offset] & 0xffffu; + Require(words != 0u && offset + words <= spirv.size(), "f16 native: malformed SPIR-V"); + if (opcode == spv::OpTypeFloat) { + Require(words >= 3u, "f16 native: malformed float type"); + if (spirv[offset + 2u] == 16u) half = spirv[offset + 1u]; + } + if (opcode == spv::OpTypeVector) { + Require(words >= 4u, "f16 native: malformed vector type"); + if (half && spirv[offset + 2u] == *half && spirv[offset + 3u] == 2u) pair = spirv[offset + 1u]; + } + if (opcode == spv::OpFAdd || opcode == spv::OpFMul || opcode == spv::OpExtInst) { + Require(words >= 5u, "f16 native: malformed arithmetic instruction"); + if (pair && spirv[offset + 1u] == *pair) { + math.add = math.add || opcode == spv::OpFAdd; + math.mul = math.mul || opcode == spv::OpFMul; + math.fma = math.fma || opcode == spv::OpExtInst; + } + } + offset += words; + } + return math; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->Target(); + const auto supports = [&](spv::Capability capability) { + return std::ranges::find(target.supportedCapabilities, static_cast(capability)) != target.supportedCapabilities.end(); + }; + if (!supports(spv::CapabilityFloat16) || !supports(spv::CapabilityRoundingModeRTE) || !supports(spv::CapabilityDenormPreserve)) { + std::puts("f16 native: skipped, the device has no native f16 with RTE rounding and kept denormals"); + return VulkanTestSkipped; + } + std::vector withoutFloat16; + std::ranges::copy_if(target.supportedCapabilities, std::back_inserter(withoutFloat16), [](std::uint32_t capability) { return capability != spv::CapabilityFloat16; }); + auto emulatedTarget = target; + emulatedTarget.supportedCapabilities = withoutFloat16; + + const std::span code(Code); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + const auto compile = [&](const ShaderRecompiler::SpirvTarget& shaderTarget, const Mode& mode) { + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + shaderTarget, + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.floatMode; + return ShaderRecompiler::Recompile(request); + }; + const auto run = [&](const auto& result) { + Output.fill(0xdeadbeefu); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device->WaitIdle(); + return Output; + }; + + std::uint64_t compared = 0u; + std::uint64_t nanPayloads = 0u; + const auto isNan16 = [](std::uint32_t value) { return (value & 0x7fffu) > 0x7c00u; }; + for (const auto& mode : Modes) { + const auto native = compile(target, mode); + const auto emulated = compile(emulatedTarget, mode); + const F16x2Math nativeMath = FindF16x2Math(native.spirv.Words()); + const F16x2Math emulatedMath = FindF16x2Math(emulated.spirv.Words()); + Require(nativeMath.add == mode.native && nativeMath.mul == mode.native, std::string("f16 native: ") + mode.name + (mode.native ? " does not use" : " uses") + " f16vec2 add and mul"); + Require(!nativeMath.fma, std::string("f16 native: ") + mode.name + " uses an f16vec2 Fma, which Vulkan does not require to be fused"); + Require(!emulatedMath.add && !emulatedMath.mul && !emulatedMath.fma, std::string("f16 native: ") + mode.name + " uses f16vec2 math without Float16"); + for (std::uint32_t batch = 0; batch < Batches; ++batch) { + for (auto& word : Input) word = Half() | (Half() << 16u); + const auto nativeOutput = run(native); + const auto emulatedOutput = run(emulated); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t index = 0; index < Results; ++index) { + const std::uint32_t actual = nativeOutput[tid * Stride + index]; + const std::uint32_t expected = emulatedOutput[tid * Stride + index]; + const std::uint32_t* row = &Input[tid * Inputs]; + const bool bothNan = (actual >> 16u) == (expected >> 16u) && isNan16(actual) && isNan16(expected); + if (actual != expected && bothNan) ++nanPayloads; + Require(actual == expected || bothNan, std::string("f16 native: ") + mode.name + ", " + Names[index] + " with v4=" + Hex(row[0]) + " v5=" + Hex(row[1]) + " v6=" + Hex(row[2]) + " v7=" + Hex(row[3]) + " is " + Hex(actual) + ", f32 path gives " + Hex(expected)); + } + } + compared += Threads * Results; + } + } + std::printf("f16 native tests passed (%llu results, %llu NaN results with another payload)\n", static_cast(compared), static_cast(nanPayloads)); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FmaMixF16DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FmaMixF16DenormalModes.cpp new file mode 100644 index 0000000000..c772a521e1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FmaMixF16DenormalModes.cpp @@ -0,0 +1,221 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 16; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 2; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040083u, 0xe03c1000u, 0x80000401u, 0xbf8c3f70u, 0xcc20600au, 0x1c120b04u, 0xcc20600bu, + 0x0c120d04u, 0xe0741000u, 0x80010a02u, 0xbf810000u, +}; +constexpr std::uint32_t Rows[Threads][3] = { + {0x00010000u, 0x00000000u, 0x3f800000u}, + {0x80010000u, 0x00000000u, 0x00800000u}, + {0x00003c00u, 0x00000001u, 0x4b000000u}, + {0x80000001u, 0x00003c00u, 0xbf800000u}, + {0x000003ffu, 0x00003c00u, 0x33800000u}, + {0x000183ffu, 0x00004000u, 0x00000001u}, + {0x00000200u, 0x00000200u, 0x3e800000u}, + {0x00013c00u, 0x00003c00u, 0x7f7fffffu}, + {0x00010400u, 0x00003800u, 0x3f800000u}, + {0x00007c00u, 0x00000001u, 0x3f800000u}, + {0x00010001u, 0x00000001u, 0x40000000u}, + {0x81554400u, 0x00000155u, 0xc0800000u}, + {0x83ff3c00u, 0x00000400u, 0x3f000000u}, + {0x00007bffu, 0x00000001u, 0x2f800000u}, + {0x80000000u, 0x00008000u, 0x80000000u}, + {0x00008001u, 0x00008001u, 0x3f800000u}, +}; +constexpr std::uint32_t Expected[2][2][Threads][2] = { + { + { + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x4b000000u}, + {0x00000000u, 0x80000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x3f800000u, 0x7f7fffffu}, + {0x38000000u, 0x38800000u}, + {0xffc00000u, 0x7f800000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0xc1800000u}, + {0x38800000u, 0x3f000000u}, + {0x00000000u, 0x377fe000u}, + {0x80000000u, 0x80000000u}, + {0x00000000u, 0x00000000u}, + }, + { + {0x33800000u, 0x33800000u}, + {0xb3800000u, 0xb3800000u}, + {0x33800000u, 0x4b000000u}, + {0x33800000u, 0xb3800000u}, + {0x387fc000u, 0x2c7fc000u}, + {0xb8ffa000u, 0x33800000u}, + {0x30800000u, 0x37000000u}, + {0x3f800000u, 0x7f7fffffu}, + {0x38004000u, 0x38802000u}, + {0x7f800000u, 0x7f800000u}, + {0x33800000u, 0x34400000u}, + {0x387fc000u, 0xc180000bu}, + {0x33800000u, 0x3efff802u}, + {0x3b7fe000u, 0x377fe000u}, + {0x80000000u, 0x80000000u}, + {0x27800000u, 0xb3800000u}, + }, + }, + { + { + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x4b000000u}, + {0x00000000u, 0x80000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x3f800000u, 0x7f7fffffu}, + {0x38000000u, 0x38800000u}, + {0xffc00000u, 0x7f800000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0xc1800000u}, + {0x38800000u, 0x3f000000u}, + {0x00000000u, 0x377fe000u}, + {0x80000000u, 0x80000000u}, + {0x00000000u, 0x00000000u}, + }, + { + {0x33800000u, 0x33800000u}, + {0xb3800000u, 0xb3800000u}, + {0x33800000u, 0x4b000000u}, + {0x33800000u, 0xb3800000u}, + {0x387fc000u, 0x2c7fc000u}, + {0xb8ffa000u, 0x33800000u}, + {0x30800000u, 0x37000000u}, + {0x3f800000u, 0x7f7fffffu}, + {0x38004000u, 0x38802000u}, + {0x7f800000u, 0x7f800000u}, + {0x33800000u, 0x34400000u}, + {0x387fc000u, 0xc180000bu}, + {0x33800000u, 0x3efff802u}, + {0x3b7fe000u, 0x377fe000u}, + {0x80000000u, 0x80000000u}, + {0x27800000u, 0xb3800000u}, + }, + }, +}; + +struct Mode { + const char* name; + ShaderRecompiler::ShaderFloatMode value; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), &Input[tid * Inputs]); + Input[tid * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +bool Matches(std::uint32_t actual, std::uint32_t expected) { + const auto nan = [](std::uint32_t single) { return (single & 0x7fffffffu) > 0x7f800000u; }; + return actual == expected || (nan(expected) && nan(actual)); +} + +void Check(const Mode& mode) { + constexpr const char* Names[Results] = {"v_fma_mix_f32 of three f16 sources", "v_fma_mix_f32 of an f16, an f32 and an f16 source"}; + const std::uint32_t f32Kept = (mode.value.floatMode >> 4u) & 1u; + const std::uint32_t f16InputsKept = (mode.value.floatMode >> 6u) & 1u; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t form = 0; form < Results; ++form) { + const std::uint32_t actual = Output[tid * Results + form]; + const std::uint32_t expected = Expected[f32Kept][f16InputsKept][tid][form]; + Require(Matches(actual, expected), std::string(Names[form]) + " " + mode.name + " of " + Hex(Rows[tid][0]) + ", " + Hex(Rows[tid][1]) + ", " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::vector modes; + static const char* const Names[2][4][2] = { + {{"f32 denormals flushed, f16 denormal mode 0, IEEE mode off", "f32 denormals flushed, f16 denormal mode 0, IEEE mode on"}, + {"f32 denormals flushed, f16 denormal mode 1, IEEE mode off", "f32 denormals flushed, f16 denormal mode 1, IEEE mode on"}, + {"f32 denormals flushed, f16 denormal mode 2, IEEE mode off", "f32 denormals flushed, f16 denormal mode 2, IEEE mode on"}, + {"f32 denormals flushed, f16 denormal mode 3, IEEE mode off", "f32 denormals flushed, f16 denormal mode 3, IEEE mode on"}}, + {{"f32 denormals kept, f16 denormal mode 0, IEEE mode off", "f32 denormals kept, f16 denormal mode 0, IEEE mode on"}, + {"f32 denormals kept, f16 denormal mode 1, IEEE mode off", "f32 denormals kept, f16 denormal mode 1, IEEE mode on"}, + {"f32 denormals kept, f16 denormal mode 2, IEEE mode off", "f32 denormals kept, f16 denormal mode 2, IEEE mode on"}, + {"f32 denormals kept, f16 denormal mode 3, IEEE mode off", "f32 denormals kept, f16 denormal mode 3, IEEE mode on"}}, + }; + for (std::uint32_t f32 = 0; f32 < 2u; ++f32) { + for (std::uint32_t f16 = 0; f16 < 4u; ++f16) { + for (std::uint32_t ieee = 0; ieee < 2u; ++ieee) { + modes.push_back({Names[f32][f16][ieee], ShaderRecompiler::ShaderFloatMode{(f16 << 6u) | (f32 != 0u ? 0x30u : 0u), true, ieee != 0u, false}}); + } + } + } + for (const Mode& mode : modes) { + Run(*device, mode); + Check(mode); + } + std::puts("v_fma_mix_f32 f16 denormal mode tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FmaskImageReads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FmaskImageReads.cpp new file mode 100644 index 0000000000..b76ffe2a6d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FmaskImageReads.cpp @@ -0,0 +1,160 @@ +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +constexpr std::uint32_t Width = 128; +constexpr std::uint32_t Height = 64; +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Sentinel = 0xdeadbeef; +alignas(4096) std::array TexelStorage{}; +const auto Texels = std::span(TexelStorage).subspan((0u - reinterpret_cast(TexelStorage.data())) & 0xffffu, 3 * 65536); +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(256) constexpr std::array Code{0x34020084u, 0xe0381000u, 0x80000201u, 0xbf8c3f70u, 0xf0001f08u, 0x00020602u, 0xbf8c3f70u, 0xe0781000u, 0x80010601u, 0xbf810000u}; + +std::array Buffer(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 Image(bool layered, std::uint32_t pipeXor, std::uint32_t swizzle = 4u) { + const auto address = reinterpret_cast(Texels.data()) + pipeXor; + return {static_cast(address >> 8u), static_cast(address >> 40u) | (166u << 20u) | (3u << 30u), 31u | ((Height - 1u) << 14u), swizzle | (24u << 20u) | ((layered ? 13u : 9u) << 28u), layered ? 2u | (1u << 16u) : 0u, 0u, 0u, 0u}; +} + +std::uint32_t Mapping(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t round) { + if (round == 0) return 0u; + if (round == 1) return 0x11111111u; + if (round == 2) return 0x11111100u; + std::uint32_t word = 0; + for (std::uint32_t sample = 0; sample < 8; ++sample) word |= ((x + 3u * y + 5u * layer + sample + round) & 7u) << (sample * 4u); + return word; +} + +std::uint64_t EquationOffset(const TextureSwizzleEquation& equation, std::uint32_t x, std::uint32_t y, std::uint32_t layer) { + 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 << 12u) & (mask & 0xfff000u)) ^ ((layer << 24u) & (mask & 0xff000000u)); + offset |= static_cast(std::popcount(selected) & 1u) << bit; + } + return offset; +} + +void Fill(bool layered, std::uint32_t pipeXor, std::uint32_t round) { + auto words = Image(layered, pipeXor); + words[1] = (words[1] & ~(0x1ffu << 20u)) | (20u << 20u); + const auto resource = DecodeTextureResource(words); + const auto geometry = DescribeSurface(resource); + Require(geometry.guestBytes <= Texels.size(), "FMASK fixture exceeds its allocation"); + const auto& mip = geometry.mips.at(0); + const auto block = ThinBlockLayout(resource.tileMode, 4u); + const auto* equation = FindTextureSwizzleEquation(24u, 4u); + Require(equation != nullptr, "FMASK fixture has no Z swizzle equation"); + std::ranges::fill(Texels, std::byte{0}); + for (std::uint32_t layer = 0; layer < geometry.layers; ++layer) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + auto offset = geometry.GuestLayerOffset(layer) + mip.tiledOffset; + if (mip.tail) offset += EquationOffset(*equation, x + mip.tailX, y + mip.tailY, layer) ^ pipeXor; + else offset += (static_cast(y / block[2]) * mip.blocksPerRow + x / block[1]) * block[0] + (EquationOffset(*equation, x, y, layer) ^ pipeXor); + const auto value = Mapping(x, y, layer, round); + Require(offset + sizeof(value) <= Texels.size(), "FMASK fixture addresses outside its allocation"); + std::memcpy(Texels.data() + offset, &value, sizeof(value)); + } + } + } +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::span image, bool registration) { + std::vector userData(16u); + const auto input = Buffer(Input.data(), sizeof(Input)); + const auto output = Buffer(Output.data(), Threads * 16u); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4u); + std::copy(image.begin(), image.end(), userData.begin() + 8u); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + auto target = device.Target(); + target.registrationModules = registration; + ShaderRecompiler::RecompileRequest request{{ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0u, {}}, {32u, 0u, userData, compute, std::nullopt, std::nullopt, memory}, target, {0u, 0u, 0u, 128u}}; + return ShaderRecompiler::Recompile(request); +} + +void Run(AgcDriver::VulkanDevice& device, bool layered, bool registration, std::uint32_t pipeXor) { + auto code = Code; + if (layered) code[4] = 0xf0001f28u; + ShaderRecompiler::CompiledShaderArtifact first; + for (std::uint32_t round = 0; round < 5; ++round) { + Fill(layered, pipeXor, round); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Input[lane * 4u] = (lane * 37u + 11u) % Width; + Input[lane * 4u + 1u] = (lane * 23u + 5u) % Height; + Input[lane * 4u + 2u] = layered ? lane & 1u : 0u; + Input[lane * 4u + 3u] = 0u; + } + Output.fill(Sentinel); + const auto swizzle = round == 4u ? 4u | (1u << 3u) | (4u << 6u) : 4u; + const auto shader = Compile(device, code, Image(layered, pipeXor, swizzle), registration); + if (first.variantId == 0u) first = shader; + else Require(shader.cacheHit && shader.variantId == first.variantId, "FMASK data or channel selectors changed the compiled shader artifact"); + device.Dispatch(shader, 1u, 1u, 1u); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const auto value = Mapping(Input[lane * 4u], Input[lane * 4u + 1u], layered ? Input[lane * 4u + 2u] + 1u : 0u, round); + const std::array expected{value, round == 4u ? 1u : 0u, round == 4u ? value : 0u, 0u}; + for (std::uint32_t component = 0; component < 4u; ++component) Require(Output[lane * 4u + component] == expected[component], "FMASK load did not read its mapping: round " + std::to_string(round) + ", lane " + std::to_string(lane) + ", component " + std::to_string(component)); + } + Require(std::all_of(Output.begin() + Threads * 4u, Output.end(), [](auto value) { return value == Sentinel; }), "FMASK load wrote outside its output"); + if (round == 3u) { + auto compressed = Image(layered, pipeXor); + compressed[6] = 1u << 21u; + Output.fill(Sentinel); + bool rejected = false; + try { + const auto invalid = Compile(device, code, compressed, registration); + device.Dispatch(invalid, 1u, 1u, 1u); + device.WaitIdle(); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("FMASK CMASK compression is not implemented") != std::string::npos, "compressed FMASK failed for another reason: " + std::string(error.what())); + rejected = true; + } + Require(rejected, "a cached FMASK shader accepted compression without decoding CMASK"); + Require(std::all_of(Output.begin(), Output.end(), [](auto value) { return value == Sentinel; }), "the rejected compressed FMASK load wrote output"); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const bool registration : {false, true}) { + for (const bool layered : {false, true}) { + for (const auto pipeXor : {0u, 0xf00u}) Run(*device, layered, registration, pipeXor); + } + } + std::cout << "FMASK image reads passed: 40 dispatches, 5120 component checks, 8 compression refusals\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FragmentUserDataTail.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FragmentUserDataTail.cpp new file mode 100644 index 0000000000..0c735d500d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FragmentUserDataTail.cpp @@ -0,0 +1,143 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +constexpr std::byte Kept{0x40}; +using Image = std::array; +alignas(256) Image Pixels{}; +alignas(256) std::array Body{}; +alignas(256) std::array Reference{}; +alignas(256) std::array Epilog{0xbf850001u, 0x7e000280u, 0x7e040208u, 0x7e0602f2u, 0xf800180fu, 0x03010002u, 0xbf810000u}; +alignas(256) const std::array OtherEpilog{0xbf850001u, 0x7e000280u, 0x7e040201u, 0x7e0602f2u, 0xf800180fu, 0x03010002u, 0xbf810000u}; +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, +}; + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +Image Draw(const std::uint32_t* code, const std::uint32_t* epilog, std::uint32_t userCount) { + Pixels.fill(Kept); + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(code); + const auto next = reinterpret_cast(epilog); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 0u}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, userCount << 1u}, {0x00c, std::bit_cast(1.0f)}, + {0x00d, std::bit_cast(0.5f)}, {0x010, static_cast(next)}, {0x011, 0x00803fc1u}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + const auto color = reinterpret_cast(Pixels.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + Append(commands, 0x2d, {3, 2}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "fragment tail draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(color, Pixels.size()); + return Pixels; +} + +Image Check(std::span prefix, std::span epilog, std::uint32_t userCount = 6u) { + Body.fill(0xbf9f0000u); + Reference.fill(0xbf9f0000u); + std::copy(prefix.begin(), prefix.end(), Body.begin()); + Body[prefix.size()] = 0xbe802004u; + std::copy(prefix.begin(), prefix.end(), Reference.begin()); + std::copy_n(epilog.begin(), 7u, Reference.begin() + prefix.size()); + const auto expected = Draw(Reference.data(), epilog.data(), userCount); + for (unsigned repeat = 0; repeat < 2u; ++repeat) { + const auto actual = Draw(Body.data(), epilog.data(), userCount); + Require(actual == expected, "linked fragment tail pixels differ from the monolithic shader"); + } + return expected; +} + +void FragmentTailDraws() { + const auto high = static_cast(reinterpret_cast(Epilog.data()) >> 32u); + std::vector prefix{ + 0xbe880300u, 0x7e000201u, 0x7e020280u, 0xbf060505u, 0xbe8503ffu, high, + }; + const auto original = Check(prefix, Epilog); + const auto kept = std::count(original.begin(), original.end(), Kept); + Require(kept != 0 && kept != static_cast(original.size()), "reference triangle did not cross the render target"); + Require(std::count(original.begin(), original.end(), std::byte{0xff}) > 0, "fragment continuation did not export its carried scalar color"); + Require(Check(prefix, Epilog, 5u) == original, "constant high tail required a second user-data word"); + prefix.back() = static_cast(reinterpret_cast(OtherEpilog.data()) >> 32u); + const auto changedAddress = Check(prefix, OtherEpilog); + Require(changedAddress != original, "changing the epilog did not change the reference pixels"); + prefix.back() = high; + Epilog[2] = OtherEpilog[2]; + Require(Check(prefix, Epilog) == changedAddress, "rewritten epilog kept the old pixels"); + Epilog[2] = 0x7e040208u; + Require(Check(prefix, Epilog) == original, "restored epilog did not restore the pixels"); + prefix[3] = 0xbf070505u; + Require(Check(prefix, Epilog) != original, "SCC was not preserved across the fragment tail jump"); + prefix[3] = 0xbf060505u; + prefix.push_back(0xbefe0480u); + const auto emptyExec = Check(prefix, Epilog); + Require(std::ranges::all_of(emptyExec, [](std::byte value) { return value == Kept; }), "EXEC was not preserved across the fragment tail jump"); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + FragmentTailDraws(); + AgcDriverShutdown_nid_postfix(); + std::puts("fragment user-data tail tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {} + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FrontFaceAllBits.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FrontFaceAllBits.cpp new file mode 100644 index 0000000000..409e71f12c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FrontFaceAllBits.cpp @@ -0,0 +1,134 @@ +#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 Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t WaveSize = 64; +constexpr std::uint32_t Kept = 0x12345678u; +alignas(256) std::array Pixels{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0xf8001801, 0x00000000, 0xbf810000, +}; + +constexpr std::array, 6> Corners{{ + {1.0f, 1.0f}, {1.0f, 15.0f}, {15.0f, 1.0f}, + {17.0f, 1.0f}, {31.0f, 1.0f}, {17.0f, 15.0f}, +}}; + +alignas(256) std::array, Corners.size()> Vertices{}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void Draw(AgcDriver::VulkanDevice& device, bool allBits) { + Pixels.fill(Kept); + for (std::size_t index = 0; index < Corners.size(); ++index) { + Vertices[index] = {Corners[index][0] * 2.0f / Width - 1.0f, 1.0f - Corners[index][1] * 2.0f / Height, 0.5f, 1.0f}; + } + const auto target = device.Target(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Vertices.data(), 16u, static_cast(Vertices.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::FrontFace); + pixel.frontFace = true; + pixel.frontFaceAllBits = allBits; + pixel.targetOutputMode[0] = 7; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R32_UINT, sizeof(Pixels), 0xe4u}; + state.color.uintExport = true; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(std::uint32_t front, std::uint32_t back, std::string_view what) { + for (const auto& [x, y, isFront] : std::array, 2>{{{4, 4, true}, {20, 4, false}}}) { + const auto expected = isFront ? front : back; + const auto actual = Pixels[y * Width + x]; + char text[160]; + std::snprintf(text, sizeof(text), "%.*s: the %s-facing triangle read FRONT_FACE as 0x%08x, expected 0x%08x", static_cast(what.size()), what.data(), isFront ? "front" : "back", actual, expected); + Require(actual == expected, text); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Draw(*device, false); + Check(0x3f800000u, 0xbf800000u, "without FRONT_FACE_ALL_BITS"); + Draw(*device, true); + Check(0xffffffffu, 0u, "with FRONT_FACE_ALL_BITS"); + std::puts("front face all-bits tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GdsAppendConsume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GdsAppendConsume.cpp index 0ee7d0c786..08d1c78777 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GdsAppendConsume.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GdsAppendConsume.cpp @@ -25,7 +25,14 @@ constexpr std::uint32_t CounterOffset = 0x10; constexpr std::array Starts{1000, 2000, 3000, 4000}; constexpr std::uint32_t UntouchedAppend = 0x5eed0001u; constexpr std::uint32_t UntouchedConsume = 0x5eed0002u; +constexpr std::uint32_t UntouchedOrdered = 0x5eed0003u; constexpr std::size_t ActiveLanesLiteral = 12; +constexpr std::size_t OrderedWaveMaskLiteral = 5; +constexpr std::size_t OrderedM0Literal = 7; +constexpr std::size_t OrderedFirstLaneLiteral = 9; +constexpr std::size_t OrderedOperationLiteral = 10; +constexpr std::uint32_t OrderedAdd = 0xd8fe0310u; +constexpr std::uint32_t OrderedSwap = 0xd8fe1310u; alignas(256) std::array Output{}; alignas(16) std::array CounterReadback{}; @@ -44,6 +51,27 @@ alignas(256) auto Code = std::to_array({ 0xbf810000, }); +alignas(256) auto OrderedCode = std::to_array({ + 0x34060082, + 0x4a020083, + 0x7e0a02ff, UntouchedOrdered, + 0x360400ff, 0x0000003f, + 0xbefc03ff, 0x0000ffff, + 0x7da604ff, 0x00000000, + OrderedAdd, 0x05000001, + 0xbefe04c1, + 0xbf8cc07f, + 0xe0702000, 0x80000503, + 0xbf810000, +}); + +struct OrderedRun { + std::uint32_t m0; + std::uint32_t firstLaneEighths; +}; + +constexpr std::array OrderedRuns{{{0, 0}, {7, 0}, {7, 2}, {7, 5}}}; + struct Workgroup { std::uint32_t threads; std::uint32_t waveSize; @@ -83,13 +111,11 @@ std::array ReadCounters(AgcDriver::QueueState& queue) { return CounterReadback; } -void Run(AgcDriver::VulkanDevice& device, const Workgroup& workgroup) { +void Run(AgcDriver::VulkanDevice& device, const Workgroup& workgroup, std::span code) { Output.fill(0xdeadbeefu); - Code[ActiveLanesLiteral] = workgroup.activeLanes; std::vector userData(4, 0u); const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); std::copy(output.begin(), output.end(), userData.begin()); - const std::span code(Code); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{workgroup.threads, 1, 1}, 0, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ @@ -134,6 +160,56 @@ void CheckWaves(const Workgroup& workgroup, std::uint32_t result, std::uint32_t } } +void CheckOrdered(const Workgroup& workgroup, std::uint32_t m0, std::uint32_t firstLane, const std::array& counters) { + const auto waves = workgroup.threads / workgroup.waveSize; + const auto what = workgroup.Name() + " ds_ordered_count with M0 " + std::to_string(m0) + " from lane " + std::to_string(firstLane) + " of each wave"; + std::vector values(waves); + std::vector order(waves); + for (std::uint32_t wave = 0; wave < waves; ++wave) { + order[wave] = wave; + const auto first = wave * workgroup.waveSize + firstLane; + values[wave] = Output[first * Results]; + for (std::uint32_t lane = wave * workgroup.waveSize; lane < (wave + 1u) * workgroup.waveSize; ++lane) { + const auto actual = Output[lane * Results]; + if (lane < first) { + Require(actual == UntouchedOrdered, what + ": inactive lane " + std::to_string(lane) + " was written " + std::to_string(actual)); + continue; + } + Require(actual == values[wave], what + ": lane " + std::to_string(lane) + " returned " + std::to_string(actual) + ", its wave's first active lane " + std::to_string(values[wave])); + } + } + std::sort(order.begin(), order.end(), [&](std::uint32_t left, std::uint32_t right) { return values[left] < values[right]; }); + auto expected = Starts[0]; + for (const auto wave : order) { + Require(values[wave] == expected, what + ": wave " + std::to_string(wave) + " returned " + std::to_string(values[wave]) + ", expected " + std::to_string(expected)); + expected += wave * workgroup.waveSize + firstLane + 3u; + } + Require(counters[0] == expected, what + ": GDS counter 0 is " + std::to_string(counters[0]) + ", expected " + std::to_string(expected)); +} + +void CheckOrderedSwap(const Workgroup& workgroup, std::uint32_t m0, std::uint32_t firstLane, const std::array& counters) { + const auto waves = workgroup.threads / workgroup.waveSize; + const auto what = workgroup.Name() + " ds_ordered_count swap with M0 " + std::to_string(m0) + " from lane " + std::to_string(firstLane) + " of each wave"; + std::vector seen{counters[0]}; + std::vector expected{Starts[0]}; + for (std::uint32_t wave = 0; wave < waves; ++wave) { + const auto first = wave * workgroup.waveSize + firstLane; + for (std::uint32_t lane = wave * workgroup.waveSize; lane < (wave + 1u) * workgroup.waveSize; ++lane) { + const auto actual = Output[lane * Results]; + if (lane < first) { + Require(actual == UntouchedOrdered, what + ": inactive lane " + std::to_string(lane) + " was written " + std::to_string(actual)); + continue; + } + Require(actual == Output[first * Results], what + ": lane " + std::to_string(lane) + " returned " + std::to_string(actual) + ", its wave's first active lane " + std::to_string(Output[first * Results])); + } + seen.push_back(Output[first * Results]); + expected.push_back(first + 3u); + } + std::sort(seen.begin(), seen.end()); + std::sort(expected.begin(), expected.end()); + Require(seen == expected, what + ": the returned values and GDS counter 0 (" + std::to_string(counters[0]) + ") are not the start value and each wave's first active lane value"); +} + void Check(const Workgroup& workgroup, std::uint32_t pass, const std::array& counters) { const auto active = std::min(workgroup.activeLanes, workgroup.threads); const std::array steps{workgroup.threads, workgroup.threads, active, active}; @@ -164,10 +240,25 @@ int main() { for (const auto& workgroup : Workgroups) { SetCounters(queue); Require(ReadCounters(queue) == Starts, workgroup.Name() + ": the GDS counters did not read back as set"); + Code[ActiveLanesLiteral] = workgroup.activeLanes; for (std::uint32_t pass = 0; pass < 2; ++pass) { - Run(*device, workgroup); + Run(*device, workgroup, Code); Check(workgroup, pass, ReadCounters(queue)); } + for (const auto& run : OrderedRuns) { + const auto firstLane = workgroup.waveSize * run.firstLaneEighths / 8u; + SetCounters(queue); + OrderedCode[OrderedWaveMaskLiteral] = workgroup.waveSize - 1u; + OrderedCode[OrderedM0Literal] = run.m0; + OrderedCode[OrderedFirstLaneLiteral] = firstLane; + OrderedCode[OrderedOperationLiteral] = OrderedAdd; + Run(*device, workgroup, OrderedCode); + CheckOrdered(workgroup, run.m0, firstLane, ReadCounters(queue)); + SetCounters(queue); + OrderedCode[OrderedOperationLiteral] = OrderedSwap; + Run(*device, workgroup, OrderedCode); + CheckOrderedSwap(workgroup, run.m0, firstLane, ReadCounters(queue)); + } } std::puts("gds append consume tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoads.cpp index 0c288dbb13..b13c0f3fb7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoads.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "Recompiler.hpp" @@ -16,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -143,7 +145,15 @@ void Dispatch(AgcDriver::VulkanDevice& device, std::span co }; request.useCache = false; const auto result = ShaderRecompiler::Recompile(request); - device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + const auto before = AgcDriver::VulkanDevice::ComputePipelineCounts(); + auto prepared = device.PrepareDispatch(result, {}, Threads, reinterpret_cast(code.data())); + const auto after = AgcDriver::VulkanDevice::ComputePipelineCounts(); + Require(after.created == before.created + 1 && after.underGpuMutex == before.underGpuMutex, "global loads: the address-table program's pipeline was not created before the GPU mutex"); + { + std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data()), std::move(prepared)); + } + Require(AgcDriver::VulkanDevice::ComputePipelineCounts().created == after.created, "global loads: the dispatch created a second pipeline"); device.WaitIdle(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GpuBufferFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GpuBufferFormat.cpp new file mode 100644 index 0000000000..423fc3d9a6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GpuBufferFormat.cpp @@ -0,0 +1,678 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Lanes = 2; +constexpr std::uint32_t Results = 12; +constexpr std::uint32_t OutputStride = 16; +constexpr std::uint32_t Untouched = 0xdeadbeefu; +constexpr std::size_t BlockBytes = 65536; + +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, "gpu buffer format: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + const std::array initialData{ + 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0xc0490fdbu, 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0x00000001u + }; + std::copy(initialData.begin(), initialData.end(), Data().begin()); + } + + ~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::span Descriptor() { + return std::span(reinterpret_cast(block), 4); + } + + std::span Data() { + return std::span(reinterpret_cast(block + 256), 4 * Lanes); + } + + std::span Output() { + return std::span(reinterpret_cast(block + 512), OutputStride * Lanes); + } + +private: + std::uint8_t* block = nullptr; +}; + +alignas(256) constexpr std::array Code{ + 0xe0340000u, 0x80010200u, 0xbf8c3f70u, 0x7e140502u, 0x7e160503u, 0x34020084u, 0x34280086u, 0xe00c1000u, + 0x80020401u, 0xe0001000u, 0x80020801u, 0xe0041000u, 0x80020901u, 0xe0081000u, 0x80020b01u, 0xe20c1000u, + 0x80020e01u, 0xbf8c3f70u, 0xe0781000u, 0x80030414u, 0xe0701010u, 0x80030814u, 0xe0741014u, 0x80030914u, + 0xe07c101cu, 0x80030b14u, 0xe0741028u, 0x80030e14u, 0xbf810000u, +}; + +struct Case { + std::uint32_t records; + std::uint32_t word3; + std::array expected; +}; +constexpr std::array Cases{{ + {32u, 0x31001facu, { + 0x3b808081u, 0x3b808081u, 0x3b808081u, 0x3b808081u, 0x3b808081u, 0x3b808081u, + 0x3b808081u, 0x3b808081u, 0x3b808081u, 0x3b808081u, 0x1c041c04u, 0x1c041c04u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + }}, + {32u, 0x31002facu, { + 0x3c010204u, 0x3c010204u, 0x3c010204u, 0x3c010204u, 0x3c010204u, 0x3c010204u, + 0x3c010204u, 0x3c010204u, 0x3c010204u, 0x3c010204u, 0x20082008u, 0x20082008u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + }}, + {32u, 0x31003facu, { + 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, + 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3c003c00u, 0x3c003c00u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + }}, + {32u, 0x31004facu, { + 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, + 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3c003c00u, 0x3c003c00u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + }}, + {32u, 0x31005facu, { + 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, + 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00010001u, 0x00010001u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + }}, + {32u, 0x31006facu, { + 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, + 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00010001u, 0x00010001u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + }}, + {32u, 0x31007facu, { + 0x3f7f01ffu, 0x3f7f01ffu, 0x3f7f01ffu, 0x3f7f01ffu, 0x3f7f01ffu, 0x3f7f01ffu, + 0x3f7f01ffu, 0x3f7f01ffu, 0x3f7f01ffu, 0x3f7f01ffu, 0x3bf83bf8u, 0x3bf83bf8u, + 0x3ef800f8u, 0x3ef800f8u, 0x3ef800f8u, 0x3ef800f8u, 0x3ef800f8u, 0x3ef800f8u, + 0x3ef800f8u, 0x3ef800f8u, 0x3ef800f8u, 0x3ef800f8u, 0x37c037c0u, 0x37c037c0u, + }}, + {32u, 0x31008facu, { + 0xbbff01feu, 0xbbff01feu, 0xbbff01feu, 0xbbff01feu, 0xbbff01feu, 0xbbff01feu, + 0xbbff01feu, 0xbbff01feu, 0xbbff01feu, 0xbbff01feu, 0x9ff89ff8u, 0x9ff89ff8u, + 0x3f7801f0u, 0x3f7801f0u, 0x3f7801f0u, 0x3f7801f0u, 0x3f7801f0u, 0x3f7801f0u, + 0x3f7801f0u, 0x3f7801f0u, 0x3f7801f0u, 0x3f7801f0u, 0x3bc03bc0u, 0x3bc03bc0u, + }}, + {32u, 0x31009facu, { + 0x477f0100u, 0x477f0100u, 0x477f0100u, 0x477f0100u, 0x477f0100u, 0x477f0100u, + 0x477f0100u, 0x477f0100u, 0x477f0100u, 0x477f0100u, 0x7bf87bf8u, 0x7bf87bf8u, + 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x46f80000u, + 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x77c077c0u, 0x77c077c0u, + }}, + {32u, 0x3100afacu, { + 0xc37f0000u, 0xc37f0000u, 0xc37f0000u, 0xc37f0000u, 0xc37f0000u, 0xc37f0000u, + 0xc37f0000u, 0xc37f0000u, 0xc37f0000u, 0xc37f0000u, 0xdbf8dbf8u, 0xdbf8dbf8u, + 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x46f80000u, + 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x77c077c0u, 0x77c077c0u, + }}, + {32u, 0x3100bfacu, { + 0x0000ff01u, 0x0000ff01u, 0x0000ff01u, 0x0000ff01u, 0x0000ff01u, 0x0000ff01u, + 0x0000ff01u, 0x0000ff01u, 0x0000ff01u, 0x0000ff01u, 0xff01ff01u, 0xff01ff01u, + 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, + 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x7c007c00u, 0x7c007c00u, + }}, + {32u, 0x3100cfacu, { + 0xffffff01u, 0xffffff01u, 0xffffff01u, 0xffffff01u, 0xffffff01u, 0xffffff01u, + 0xffffff01u, 0xffffff01u, 0xffffff01u, 0xffffff01u, 0xff01ff01u, 0xff01ff01u, + 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, + 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x7c007c00u, 0x7c007c00u, + }}, + {32u, 0x3100dfacu, { + 0xffe02000u, 0xffe02000u, 0xffe02000u, 0xffe02000u, 0xffe02000u, 0xffe02000u, + 0xffe02000u, 0xffe02000u, 0xffe02000u, 0xffe02000u, 0xff01ff01u, 0xff01ff01u, + 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, + 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7c007c00u, 0x7c007c00u, + }}, + {32u, 0x3100efacu, { + 0x3b808081u, 0x3f800000u, 0x3b808081u, 0x3f800000u, 0x3b808081u, 0x3b808081u, + 0x3f800000u, 0x3b808081u, 0x3f800000u, 0x3b808081u, 0x3c001c04u, 0x3c001c04u, + 0x00000000u, 0x3ef8f8f9u, 0x00000000u, 0x3ef8f8f9u, 0x00000000u, 0x00000000u, + 0x3ef8f8f9u, 0x00000000u, 0x3ef8f8f9u, 0x00000000u, 0x37c80000u, 0x37c80000u, + }}, + {32u, 0x3100ffacu, { + 0x3c010204u, 0xbc010204u, 0x3c010204u, 0xbc010204u, 0x3c010204u, 0x3c010204u, + 0xbc010204u, 0x3c010204u, 0xbc010204u, 0x3c010204u, 0xa0082008u, 0xa0082008u, + 0x00000000u, 0x3f79f3e8u, 0x00000000u, 0x3f79f3e8u, 0x00000000u, 0x00000000u, + 0x3f79f3e8u, 0x00000000u, 0x3f79f3e8u, 0x00000000u, 0x3bd00000u, 0x3bd00000u, + }}, + {32u, 0x31010facu, { + 0x3f800000u, 0x437f0000u, 0x3f800000u, 0x437f0000u, 0x3f800000u, 0x3f800000u, + 0x437f0000u, 0x3f800000u, 0x437f0000u, 0x3f800000u, 0x5bf83c00u, 0x5bf83c00u, + 0x00000000u, 0x42f80000u, 0x00000000u, 0x42f80000u, 0x00000000u, 0x00000000u, + 0x42f80000u, 0x00000000u, 0x42f80000u, 0x00000000u, 0x57c00000u, 0x57c00000u, + }}, + {32u, 0x31011facu, { + 0x3f800000u, 0xbf800000u, 0x3f800000u, 0xbf800000u, 0x3f800000u, 0x3f800000u, + 0xbf800000u, 0x3f800000u, 0xbf800000u, 0x3f800000u, 0xbc003c00u, 0xbc003c00u, + 0x00000000u, 0x42f80000u, 0x00000000u, 0x42f80000u, 0x00000000u, 0x00000000u, + 0x42f80000u, 0x00000000u, 0x42f80000u, 0x00000000u, 0x57c00000u, 0x57c00000u, + }}, + {32u, 0x31012facu, { + 0x00000001u, 0x000000ffu, 0x00000001u, 0x000000ffu, 0x00000001u, 0x00000001u, + 0x000000ffu, 0x00000001u, 0x000000ffu, 0x00000001u, 0x00ff0001u, 0x00ff0001u, + 0x00000000u, 0x0000007cu, 0x00000000u, 0x0000007cu, 0x00000000u, 0x00000000u, + 0x0000007cu, 0x00000000u, 0x0000007cu, 0x00000000u, 0x007c0000u, 0x007c0000u, + }}, + {32u, 0x31013facu, { + 0x00000001u, 0xffffffffu, 0x00000001u, 0xffffffffu, 0x00000001u, 0x00000001u, + 0xffffffffu, 0x00000001u, 0xffffffffu, 0x00000001u, 0xffff0001u, 0xffff0001u, + 0x00000000u, 0x0000007cu, 0x00000000u, 0x0000007cu, 0x00000000u, 0x00000000u, + 0x0000007cu, 0x00000000u, 0x0000007cu, 0x00000000u, 0x007c0000u, 0x007c0000u, + }}, + {32u, 0x31014facu, { + 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, + 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0xffffffffu, 0xffffffffu, + 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, + 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xffffffffu, 0xffffffffu, + }}, + {32u, 0x31015facu, { + 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, + 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x80008000u, 0x80008000u, + 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, + 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0x80008000u, 0x80008000u, + }}, + {32u, 0x31016facu, { + 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, + 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x80008000u, 0x80008000u, + 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, + 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfbfffbffu, 0xfbfffbffu, + }}, + {32u, 0x31017facu, { + 0x3f7f01ffu, 0x3f007f80u, 0x3f7f01ffu, 0x3f007f80u, 0x3f7f01ffu, 0x3f7f01ffu, + 0x3f007f80u, 0x3f7f01ffu, 0x3f007f80u, 0x3f7f01ffu, 0x38043bf8u, 0x38043bf8u, + 0x3ef800f8u, 0x3f7c00fcu, 0x3ef800f8u, 0x3f7c00fcu, 0x3ef800f8u, 0x3ef800f8u, + 0x3f7c00fcu, 0x3ef800f8u, 0x3f7c00fcu, 0x3ef800f8u, 0x3be037c0u, 0x3be037c0u, + }}, + {32u, 0x31018facu, { + 0xbbff01feu, 0xbf7f03feu, 0xbbff01feu, 0xbf7f03feu, 0xbbff01feu, 0xbbff01feu, + 0xbf7f03feu, 0xbbff01feu, 0xbf7f03feu, 0xbbff01feu, 0xbbf89ff8u, 0xbbf89ff8u, + 0x3f7801f0u, 0xbd000100u, 0x3f7801f0u, 0xbd000100u, 0x3f7801f0u, 0x3f7801f0u, + 0xbd000100u, 0x3f7801f0u, 0xbd000100u, 0x3f7801f0u, 0xa8003bc0u, 0xa8003bc0u, + }}, + {32u, 0x31019facu, { + 0x477f0100u, 0x47007f00u, 0x477f0100u, 0x47007f00u, 0x477f0100u, 0x477f0100u, + 0x47007f00u, 0x477f0100u, 0x47007f00u, 0x477f0100u, 0x78047bf8u, 0x78047bf8u, + 0x46f80000u, 0x477c0000u, 0x46f80000u, 0x477c0000u, 0x46f80000u, 0x46f80000u, + 0x477c0000u, 0x46f80000u, 0x477c0000u, 0x46f80000u, 0x7be077c0u, 0x7be077c0u, + }}, + {32u, 0x3101afacu, { + 0xc37f0000u, 0xc6ff0200u, 0xc37f0000u, 0xc6ff0200u, 0xc37f0000u, 0xc37f0000u, + 0xc6ff0200u, 0xc37f0000u, 0xc6ff0200u, 0xc37f0000u, 0xf7f8dbf8u, 0xf7f8dbf8u, + 0x46f80000u, 0xc4800000u, 0x46f80000u, 0xc4800000u, 0x46f80000u, 0x46f80000u, + 0xc4800000u, 0x46f80000u, 0xc4800000u, 0x46f80000u, 0xe40077c0u, 0xe40077c0u, + }}, + {32u, 0x3101bfacu, { + 0x0000ff01u, 0x0000807fu, 0x0000ff01u, 0x0000807fu, 0x0000ff01u, 0x0000ff01u, + 0x0000807fu, 0x0000ff01u, 0x0000807fu, 0x0000ff01u, 0x807fff01u, 0x807fff01u, + 0x00007c00u, 0x0000fc00u, 0x00007c00u, 0x0000fc00u, 0x00007c00u, 0x00007c00u, + 0x0000fc00u, 0x00007c00u, 0x0000fc00u, 0x00007c00u, 0xfc007c00u, 0xfc007c00u, + }}, + {32u, 0x3101cfacu, { + 0xffffff01u, 0xffff807fu, 0xffffff01u, 0xffff807fu, 0xffffff01u, 0xffffff01u, + 0xffff807fu, 0xffffff01u, 0xffff807fu, 0xffffff01u, 0x807fff01u, 0x807fff01u, + 0x00007c00u, 0xfffffc00u, 0x00007c00u, 0xfffffc00u, 0x00007c00u, 0x00007c00u, + 0xfffffc00u, 0x00007c00u, 0xfffffc00u, 0x00007c00u, 0xfc007c00u, 0xfc007c00u, + }}, + {32u, 0x3101dfacu, { + 0xffe02000u, 0xb6fe0000u, 0xffe02000u, 0xb6fe0000u, 0xffe02000u, 0xffe02000u, + 0xb6fe0000u, 0xffe02000u, 0xb6fe0000u, 0xffe02000u, 0x807fff01u, 0x807fff01u, + 0x7f800000u, 0xff800000u, 0x7f800000u, 0xff800000u, 0x7f800000u, 0x7f800000u, + 0xff800000u, 0x7f800000u, 0xff800000u, 0x7f800000u, 0xfc007c00u, 0xfc007c00u, + }}, + {32u, 0x3101efacu, { + 0x3f603c08u, 0x3f800000u, 0x3f006018u, 0x3f603c08u, 0x3f603c08u, 0x3f603c08u, + 0x3f800000u, 0x3f603c08u, 0x3f800000u, 0x3f006018u, 0x3c003b02u, 0x3b023803u, + 0x3f001002u, 0x3bf01e04u, 0x3f7c3f10u, 0x3f001002u, 0x3f001002u, 0x3f001002u, + 0x3bf01e04u, 0x3f001002u, 0x3bf01e04u, 0x3f7c3f10u, 0x1f813801u, 0x38013be2u, + }}, + {32u, 0x3101ffacu, { + 0xbe7f3fd0u, 0xba802008u, 0xbf800000u, 0xbe7f3fd0u, 0xbe7f3fd0u, 0xbe7f3fd0u, + 0xba802008u, 0xbe7f3fd0u, 0xba802008u, 0xbf800000u, 0x9401b3fau, 0xb3fabc00u, + 0xbf800000u, 0x3c703c0fu, 0xbd004020u, 0xbf800000u, 0xbf800000u, 0xbf800000u, + 0x3c703c0fu, 0xbf800000u, 0x3c703c0fu, 0xbd004020u, 0x2382bc00u, 0xbc00a802u, + }}, + {32u, 0x31020facu, { + 0x44e02000u, 0x44ffe000u, 0x44004000u, 0x44e02000u, 0x44e02000u, 0x44e02000u, + 0x44ffe000u, 0x44e02000u, 0x44ffe000u, 0x44004000u, 0x67ff6701u, 0x67016002u, + 0x44800000u, 0x41700000u, 0x447c0000u, 0x44800000u, 0x44800000u, 0x44800000u, + 0x41700000u, 0x44800000u, 0x41700000u, 0x447c0000u, 0x4b806400u, 0x640063e0u, + }}, + {32u, 0x31021facu, { + 0xc37f0000u, 0xbf800000u, 0xc3ff8000u, 0xc37f0000u, 0xc37f0000u, 0xc37f0000u, + 0xbf800000u, 0xc37f0000u, 0xbf800000u, 0xc3ff8000u, 0xbc00dbf8u, 0xdbf8dffcu, + 0xc4800000u, 0x41700000u, 0xc1800000u, 0xc4800000u, 0xc4800000u, 0xc4800000u, + 0x41700000u, 0xc4800000u, 0x41700000u, 0xc1800000u, 0x4b80e400u, 0xe400cc00u, + }}, + {32u, 0x31022facu, { + 0x00000701u, 0x000007ffu, 0x00000201u, 0x00000701u, 0x00000701u, 0x00000701u, + 0x000007ffu, 0x00000701u, 0x000007ffu, 0x00000201u, 0x07ff0701u, 0x07010201u, + 0x00000400u, 0x0000000fu, 0x000003f0u, 0x00000400u, 0x00000400u, 0x00000400u, + 0x0000000fu, 0x00000400u, 0x0000000fu, 0x000003f0u, 0x000f0400u, 0x040003f0u, + }}, + {32u, 0x31023facu, { + 0xffffff01u, 0xffffffffu, 0xfffffe01u, 0xffffff01u, 0xffffff01u, 0xffffff01u, + 0xffffffffu, 0xffffff01u, 0xffffffffu, 0xfffffe01u, 0xffffff01u, 0xff01fe01u, + 0xfffffc00u, 0x0000000fu, 0xfffffff0u, 0xfffffc00u, 0xfffffc00u, 0xfffffc00u, + 0x0000000fu, 0xfffffc00u, 0x0000000fu, 0xfffffff0u, 0x000ffc00u, 0xfc00fff0u, + }}, + {32u, 0x31024facu, { + 0x46020000u, 0x7ffe0000u, 0x40040000u, 0x46020000u, 0x46020000u, 0x46020000u, + 0x7ffe0000u, 0x46020000u, 0x7ffe0000u, 0x40040000u, 0x7ff07010u, 0x70104020u, + 0x40000000u, 0x37700000u, 0x7fc00000u, 0x40000000u, 0x40000000u, 0x40000000u, + 0x37700000u, 0x40000000u, 0x37700000u, 0x7fc00000u, 0x00f04000u, 0x40007e00u, + }}, + {32u, 0x31025facu, { + 0x3f40701cu, 0x3f800000u, 0x3f00700eu, 0x3f40701cu, 0x3f40701cu, 0x3f40701cu, + 0x3f800000u, 0x3f40701cu, 0x3f800000u, 0x3f00700eu, 0x3c003a04u, 0x3a043804u, + 0x00000000u, 0x3c781f04u, 0x3f7c1f84u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x3c781f04u, 0x00000000u, 0x3c781f04u, 0x3f7c1f84u, 0x23c10000u, 0x00003be1u, + }}, + {32u, 0x31026facu, { + 0xbeff7fc0u, 0xba802008u, 0xbf7f7fe0u, 0xbeff7fc0u, 0xbeff7fc0u, 0xbeff7fc0u, + 0xba802008u, 0xbeff7fc0u, 0xba802008u, 0xbf7f7fe0u, 0x9401b7fcu, 0xb7fcbbfcu, + 0x00000000u, 0x3cf83e10u, 0xbd002008u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x3cf83e10u, 0x00000000u, 0x3cf83e10u, 0xbd002008u, 0x27c20000u, 0x0000a801u, + }}, + {32u, 0x31027facu, { + 0x44404000u, 0x44ffe000u, 0x44806000u, 0x44404000u, 0x44404000u, 0x44404000u, + 0x44ffe000u, 0x44404000u, 0x44ffe000u, 0x44806000u, 0x67ff6202u, 0x62026403u, + 0x00000000u, 0x41f80000u, 0x44fc0000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x41f80000u, 0x00000000u, 0x41f80000u, 0x44fc0000u, 0x4fc00000u, 0x000067e0u, + }}, + {32u, 0x31028facu, { + 0xc37f0000u, 0xbf800000u, 0xc47f4000u, 0xc37f0000u, 0xc37f0000u, 0xc37f0000u, + 0xbf800000u, 0xc37f0000u, 0xbf800000u, 0xc47f4000u, 0xbc00dbf8u, 0xdbf8e3fau, + 0x00000000u, 0x41f80000u, 0xc2000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x41f80000u, 0x00000000u, 0x41f80000u, 0xc2000000u, 0x4fc00000u, 0x0000d000u, + }}, + {32u, 0x31029facu, { + 0x00000301u, 0x000007ffu, 0x00000403u, 0x00000301u, 0x00000301u, 0x00000301u, + 0x000007ffu, 0x00000301u, 0x000007ffu, 0x00000403u, 0x07ff0301u, 0x03010403u, + 0x00000000u, 0x0000001fu, 0x000007e0u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x0000001fu, 0x00000000u, 0x0000001fu, 0x000007e0u, 0x001f0000u, 0x000007e0u, + }}, + {32u, 0x3102afacu, { + 0xffffff01u, 0xffffffffu, 0xfffffc03u, 0xffffff01u, 0xffffff01u, 0xffffff01u, + 0xffffffffu, 0xffffff01u, 0xffffffffu, 0xfffffc03u, 0xffffff01u, 0xff01fc03u, + 0x00000000u, 0x0000001fu, 0xffffffe0u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x0000001fu, 0x00000000u, 0x0000001fu, 0xffffffe0u, 0x001f0000u, 0x0000ffe0u, + }}, + {32u, 0x3102bfacu, { + 0x44040000u, 0x7ffe0000u, 0x40060000u, 0x44040000u, 0x44040000u, 0x44040000u, + 0x7ffe0000u, 0x44040000u, 0x7ffe0000u, 0x40060000u, 0x7ff06020u, 0x60204030u, + 0x00000000u, 0x37f80000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x37f80000u, 0x00000000u, 0x37f80000u, 0x7fc00000u, 0x01f00000u, 0x00007e00u, + }}, + {32u, 0x3102cfacu, { + 0x3eaaaaabu, 0x3f703c0fu, 0x3f800000u, 0x3f006018u, 0x3eaaaaabu, 0x3eaaaaabu, + 0x3f703c0fu, 0x3eaaaaabu, 0x3f703c0fu, 0x3f800000u, 0x3b823555u, 0x38033c00u, + 0x00000000u, 0x3f40300cu, 0x3be0380eu, 0x3f7c3f10u, 0x00000000u, 0x00000000u, + 0x3f40300cu, 0x00000000u, 0x3f40300cu, 0x3be0380eu, 0x3a020000u, 0x3be21f02u, + }}, + {32u, 0x3102dfacu, { + 0x3f800000u, 0xbe004020u, 0xbb004020u, 0xbf800000u, 0x3f800000u, 0x3f800000u, + 0xbe004020u, 0x3f800000u, 0xbe004020u, 0xbb004020u, 0xb0023c00u, 0xbc009802u, + 0x00000000u, 0xbf004020u, 0x3c607038u, 0xbd004020u, 0x00000000u, 0x00000000u, + 0xbf004020u, 0x00000000u, 0xbf004020u, 0x3c607038u, 0xb8020000u, 0xa8022304u, + }}, + {32u, 0x3102efacu, { + 0x3f800000u, 0x44700000u, 0x447fc000u, 0x44004000u, 0x3f800000u, 0x3f800000u, + 0x44700000u, 0x3f800000u, 0x44700000u, 0x447fc000u, 0x63803c00u, 0x600263feu, + 0x00000000u, 0x44400000u, 0x40e00000u, 0x447c0000u, 0x00000000u, 0x00000000u, + 0x44400000u, 0x00000000u, 0x44400000u, 0x40e00000u, 0x62000000u, 0x63e04700u, + }}, + {32u, 0x3102ffacu, { + 0x3f800000u, 0xc2800000u, 0xbf800000u, 0xc3ff8000u, 0x3f800000u, 0x3f800000u, + 0xc2800000u, 0x3f800000u, 0xc2800000u, 0xbf800000u, 0xd4003c00u, 0xdffcbc00u, + 0x00000000u, 0xc3800000u, 0x40e00000u, 0xc1800000u, 0x00000000u, 0x00000000u, + 0xc3800000u, 0x00000000u, 0xc3800000u, 0x40e00000u, 0xdc000000u, 0xcc004700u, + }}, + {32u, 0x31030facu, { + 0x00000001u, 0x000003c0u, 0x000003ffu, 0x00000201u, 0x00000001u, 0x00000001u, + 0x000003c0u, 0x00000001u, 0x000003c0u, 0x000003ffu, 0x03c00001u, 0x020103ffu, + 0x00000000u, 0x00000300u, 0x00000007u, 0x000003f0u, 0x00000000u, 0x00000000u, + 0x00000300u, 0x00000000u, 0x00000300u, 0x00000007u, 0x03000000u, 0x03f00007u, + }}, + {32u, 0x31031facu, { + 0x00000001u, 0xffffffc0u, 0xffffffffu, 0xfffffe01u, 0x00000001u, 0x00000001u, + 0xffffffc0u, 0x00000001u, 0xffffffc0u, 0xffffffffu, 0xffc00001u, 0xfe01ffffu, + 0x00000000u, 0xffffff00u, 0x00000007u, 0xfffffff0u, 0x00000000u, 0x00000000u, + 0xffffff00u, 0x00000000u, 0xffffff00u, 0x00000007u, 0xff000000u, 0xfff00007u, + }}, + {32u, 0x31032facu, { + 0x3f40701cu, 0x3f800000u, 0x3be0380eu, 0x3f2aaaabu, 0x3f40701cu, 0x3f40701cu, + 0x3f800000u, 0x3f40701cu, 0x3f800000u, 0x3be0380eu, 0x3c003a04u, 0x39551f02u, + 0x00000000u, 0x3cf83e10u, 0x3f703c0fu, 0x3f800000u, 0x00000000u, 0x00000000u, + 0x3cf83e10u, 0x00000000u, 0x3cf83e10u, 0x3f703c0fu, 0x27c20000u, 0x3c003b82u, + }}, + {32u, 0x31033facu, { + 0xbeff7fc0u, 0xbb004020u, 0x3c607038u, 0xbf800000u, 0xbeff7fc0u, 0xbeff7fc0u, + 0xbb004020u, 0xbeff7fc0u, 0xbb004020u, 0x3c607038u, 0x9802b7fcu, 0xbc002304u, + 0x00000000u, 0x3d787c3eu, 0xbe004020u, 0xbf800000u, 0x00000000u, 0x00000000u, + 0x3d787c3eu, 0x00000000u, 0x3d787c3eu, 0xbe004020u, 0x2bc40000u, 0xbc00b002u, + }}, + {32u, 0x31034facu, { + 0x44404000u, 0x447fc000u, 0x40e00000u, 0x40000000u, 0x44404000u, 0x44404000u, + 0x447fc000u, 0x44404000u, 0x447fc000u, 0x40e00000u, 0x63fe6202u, 0x40004700u, + 0x00000000u, 0x41f80000u, 0x44700000u, 0x40400000u, 0x00000000u, 0x00000000u, + 0x41f80000u, 0x00000000u, 0x41f80000u, 0x44700000u, 0x4fc00000u, 0x42006380u, + }}, + {32u, 0x31035facu, { + 0xc37f0000u, 0xbf800000u, 0x40e00000u, 0xc0000000u, 0xc37f0000u, 0xc37f0000u, + 0xbf800000u, 0xc37f0000u, 0xbf800000u, 0x40e00000u, 0xbc00dbf8u, 0xc0004700u, + 0x00000000u, 0x41f80000u, 0xc2800000u, 0xbf800000u, 0x00000000u, 0x00000000u, + 0x41f80000u, 0x00000000u, 0x41f80000u, 0xc2800000u, 0x4fc00000u, 0xbc00d400u, + }}, + {32u, 0x31036facu, { + 0x00000301u, 0x000003ffu, 0x00000007u, 0x00000002u, 0x00000301u, 0x00000301u, + 0x000003ffu, 0x00000301u, 0x000003ffu, 0x00000007u, 0x03ff0301u, 0x00020007u, + 0x00000000u, 0x0000001fu, 0x000003c0u, 0x00000003u, 0x00000000u, 0x00000000u, + 0x0000001fu, 0x00000000u, 0x0000001fu, 0x000003c0u, 0x001f0000u, 0x000303c0u, + }}, + {32u, 0x31037facu, { + 0xffffff01u, 0xffffffffu, 0x00000007u, 0xfffffffeu, 0xffffff01u, 0xffffff01u, + 0xffffffffu, 0xffffff01u, 0xffffffffu, 0x00000007u, 0xffffff01u, 0xfffe0007u, + 0x00000000u, 0x0000001fu, 0xffffffc0u, 0xffffffffu, 0x00000000u, 0x00000000u, + 0x0000001fu, 0x00000000u, 0x0000001fu, 0xffffffc0u, 0x001f0000u, 0xffffffc0u, + }}, + {32u, 0x31038facu, { + 0x3b808081u, 0x3f800000u, 0x3efefeffu, 0x3f008081u, 0x3b808081u, 0x3b808081u, + 0x3f800000u, 0x3b808081u, 0x3f800000u, 0x3efefeffu, 0x3c001c04u, 0x380437f8u, + 0x00000000u, 0x3ef8f8f9u, 0x00000000u, 0x3f7cfcfdu, 0x00000000u, 0x00000000u, + 0x3ef8f8f9u, 0x00000000u, 0x3ef8f8f9u, 0x00000000u, 0x37c80000u, 0x3be80000u, + }}, + {32u, 0x31039facu, { + 0x3c010204u, 0xbc010204u, 0x3f800000u, 0xbf800000u, 0x3c010204u, 0x3c010204u, + 0xbc010204u, 0x3c010204u, 0xbc010204u, 0x3f800000u, 0xa0082008u, 0xbc003c00u, + 0x00000000u, 0x3f79f3e8u, 0x00000000u, 0xbd010204u, 0x00000000u, 0x00000000u, + 0x3f79f3e8u, 0x00000000u, 0x3f79f3e8u, 0x00000000u, 0x3bd00000u, 0xa8080000u, + }}, + {32u, 0x3103afacu, { + 0x3f800000u, 0x437f0000u, 0x42fe0000u, 0x43000000u, 0x3f800000u, 0x3f800000u, + 0x437f0000u, 0x3f800000u, 0x437f0000u, 0x42fe0000u, 0x5bf83c00u, 0x580057f0u, + 0x00000000u, 0x42f80000u, 0x00000000u, 0x437c0000u, 0x00000000u, 0x00000000u, + 0x42f80000u, 0x00000000u, 0x42f80000u, 0x00000000u, 0x57c00000u, 0x5be00000u, + }}, + {32u, 0x3103bfacu, { + 0x3f800000u, 0xbf800000u, 0x42fe0000u, 0xc3000000u, 0x3f800000u, 0x3f800000u, + 0xbf800000u, 0x3f800000u, 0xbf800000u, 0x42fe0000u, 0xbc003c00u, 0xd80057f0u, + 0x00000000u, 0x42f80000u, 0x00000000u, 0xc0800000u, 0x00000000u, 0x00000000u, + 0x42f80000u, 0x00000000u, 0x42f80000u, 0x00000000u, 0x57c00000u, 0xc4000000u, + }}, + {32u, 0x3103cfacu, { + 0x00000001u, 0x000000ffu, 0x0000007fu, 0x00000080u, 0x00000001u, 0x00000001u, + 0x000000ffu, 0x00000001u, 0x000000ffu, 0x0000007fu, 0x00ff0001u, 0x0080007fu, + 0x00000000u, 0x0000007cu, 0x00000000u, 0x000000fcu, 0x00000000u, 0x00000000u, + 0x0000007cu, 0x00000000u, 0x0000007cu, 0x00000000u, 0x007c0000u, 0x00fc0000u, + }}, + {32u, 0x3103dfacu, { + 0x00000001u, 0xffffffffu, 0x0000007fu, 0xffffff80u, 0x00000001u, 0x00000001u, + 0xffffffffu, 0x00000001u, 0xffffffffu, 0x0000007fu, 0xffff0001u, 0xff80007fu, + 0x00000000u, 0x0000007cu, 0x00000000u, 0xfffffffcu, 0x00000000u, 0x00000000u, + 0x0000007cu, 0x00000000u, 0x0000007cu, 0x00000000u, 0x007c0000u, 0xfffc0000u, + }}, + {32u, 0x3103efacu, { + 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0xffffffffu, 0xffffffffu, + 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0xffffffffu, 0xffffffffu, + }}, + {32u, 0x3103ffacu, { + 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x7fff8000u, 0x7fff8000u, + 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0x7fff8000u, + }}, + {32u, 0x31040facu, { + 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x20058000u, 0x20058000u, + 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0x7ffcfbffu, 0x7ffcfbffu, + }}, + {32u, 0x31041facu, { + 0x3f7f01ffu, 0x3f007f80u, 0x3f3c00bcu, 0x3e7000f0u, 0x3f7f01ffu, 0x3f7f01ffu, + 0x3f007f80u, 0x3f7f01ffu, 0x3f007f80u, 0x3f3c00bcu, 0x38043bf8u, 0x338039e0u, + 0x3ef800f8u, 0x3f7c00fcu, 0x3f000080u, 0x3effff00u, 0x3ef800f8u, 0x3ef800f8u, + 0x3f7c00fcu, 0x3ef800f8u, 0x3f7c00fcu, 0x3f000080u, 0x3be037c0u, 0x38003800u, + }}, + {32u, 0x31042facu, { + 0xbbff01feu, 0xbf7f03feu, 0xbf080110u, 0x3ef001e0u, 0xbbff01feu, 0xbbff01feu, + 0xbf7f03feu, 0xbbff01feu, 0xbf7f03feu, 0xbf080110u, 0xbbf89ff8u, 0x3780b840u, + 0x3f7801f0u, 0xbd000100u, 0xbf800000u, 0x3f800000u, 0x3f7801f0u, 0x3f7801f0u, + 0xbd000100u, 0x3f7801f0u, 0xbd000100u, 0xbf800000u, 0xa8003bc0u, 0x3c00bc00u, + }}, + {32u, 0x31043facu, { + 0x477f0100u, 0x47007f00u, 0x473c0000u, 0x46700000u, 0x477f0100u, 0x477f0100u, + 0x47007f00u, 0x477f0100u, 0x47007f00u, 0x473c0000u, 0x78047bf8u, 0x738079e0u, + 0x46f80000u, 0x477c0000u, 0x47000000u, 0x46fffe00u, 0x46f80000u, 0x46f80000u, + 0x477c0000u, 0x46f80000u, 0x477c0000u, 0x47000000u, 0x7be077c0u, 0x78007800u, + }}, + {32u, 0x31044facu, { + 0xc37f0000u, 0xc6ff0200u, 0xc6880000u, 0x46700000u, 0xc37f0000u, 0xc37f0000u, + 0xc6ff0200u, 0xc37f0000u, 0xc6ff0200u, 0xc6880000u, 0xf7f8dbf8u, 0x7380f440u, + 0x46f80000u, 0xc4800000u, 0xc7000000u, 0x46fffe00u, 0x46f80000u, 0x46f80000u, + 0xc4800000u, 0x46f80000u, 0xc4800000u, 0xc7000000u, 0xe40077c0u, 0x7800f800u, + }}, + {32u, 0x31045facu, { + 0x0000ff01u, 0x0000807fu, 0x0000bc00u, 0x00003c00u, 0x0000ff01u, 0x0000ff01u, + 0x0000807fu, 0x0000ff01u, 0x0000807fu, 0x0000bc00u, 0x807fff01u, 0x3c00bc00u, + 0x00007c00u, 0x0000fc00u, 0x00008000u, 0x00007fffu, 0x00007c00u, 0x00007c00u, + 0x0000fc00u, 0x00007c00u, 0x0000fc00u, 0x00008000u, 0xfc007c00u, 0x7fff8000u, + }}, + {32u, 0x31046facu, { + 0xffffff01u, 0xffff807fu, 0xffffbc00u, 0x00003c00u, 0xffffff01u, 0xffffff01u, + 0xffff807fu, 0xffffff01u, 0xffff807fu, 0xffffbc00u, 0x807fff01u, 0x3c00bc00u, + 0x00007c00u, 0xfffffc00u, 0xffff8000u, 0x00007fffu, 0x00007c00u, 0x00007c00u, + 0xfffffc00u, 0x00007c00u, 0xfffffc00u, 0xffff8000u, 0xfc007c00u, 0x7fff8000u, + }}, + {32u, 0x31047facu, { + 0xffe02000u, 0xb6fe0000u, 0xbf800000u, 0x3f800000u, 0xffe02000u, 0xffe02000u, + 0xb6fe0000u, 0xffe02000u, 0xb6fe0000u, 0xbf800000u, 0x807fff01u, 0x3c00bc00u, + 0x7f800000u, 0xff800000u, 0x80000000u, 0x7fffe000u, 0x7f800000u, 0x7f800000u, + 0xff800000u, 0x7f800000u, 0xff800000u, 0x80000000u, 0xfc007c00u, 0x7fff8000u, + }}, + {32u, 0x31048facu, { + 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0x807fff01u, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0xffffffffu, 0xffffffffu, + 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0xffffffffu, 0xffffffffu, + }}, + {32u, 0x31049facu, { + 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0x807fff01u, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0x7fff8000u, 0x80007fffu, + 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0x7fff8000u, 0x80007fffu, + }}, + {32u, 0x3104afacu, { + 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0x807fff01u, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0x20058000u, 0x80003c00u, + 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0x7ffcfbffu, 0xfbff7e00u, + }}, + {32u, 0x3104bfacu, { + 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0xc0490fdbu, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0xffffffffu, 0xffffffffu, + 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0x00000001u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0xffffffffu, 0x0001ffffu, + }}, + {32u, 0x3104cfacu, { + 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0xc0490fdbu, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0x7fff8000u, 0x80007fffu, + 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0x00000001u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0x7fff8000u, 0x00017fffu, + }}, + {32u, 0x3104dfacu, { + 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0xc0490fdbu, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0x20058000u, 0xc2483c00u, + 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0x00000001u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0x7ffcfbffu, 0x00007e00u, + }}, + {32u, 0x310389c8u, { + 0x00000000u, 0x3f800000u, 0x3f008081u, 0x3b808081u, 0x00000000u, 0x00000000u, + 0x3f800000u, 0x00000000u, 0x3f800000u, 0x3f008081u, 0x3c000000u, 0x1c043804u, + 0x00000000u, 0x3f800000u, 0x3f7cfcfdu, 0x00000000u, 0x00000000u, 0x00000000u, + 0x3f800000u, 0x00000000u, 0x3f800000u, 0x3f7cfcfdu, 0x3c000000u, 0x00003be8u, + }}, + {32u, 0x3103c9c8u, { + 0x00000000u, 0x00000001u, 0x00000080u, 0x00000001u, 0x00000000u, 0x00000000u, + 0x00000001u, 0x00000000u, 0x00000001u, 0x00000080u, 0x00010000u, 0x00010080u, + 0x00000000u, 0x00000001u, 0x000000fcu, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000001u, 0x00000000u, 0x00000001u, 0x000000fcu, 0x00010000u, 0x000000fcu, + }}, + {32u, 0x31040977u, { + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x3c00bc00u, + 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x80002005u, 0x80002005u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0x7fff8000u, + 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0xfbff7ffcu, 0xfbff7ffcu, + }}, + {32u, 0x31016977u, { + 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, + 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x80008000u, 0x80008000u, + 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, + 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfbfffbffu, 0xfbfffbffu, + }}, + {22u, 0x3104dfacu, { + 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0xc0490fdbu, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0x20058000u, 0xc2483c00u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xfc007c00u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + }}, + {22u, 0x31047facu, { + 0xffe02000u, 0xb6fe0000u, 0xbf800000u, 0x3f800000u, 0xffe02000u, 0xffe02000u, + 0xb6fe0000u, 0xffe02000u, 0xb6fe0000u, 0xbf800000u, 0x807fff01u, 0x3c00bc00u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7f800000u, 0x7f800000u, + 0xff800000u, 0x7f800000u, 0xff800000u, 0x80000000u, 0x00000000u, 0x00000000u, + }}, + {22u, 0x31040facu, { + 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x20058000u, 0x20058000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xfc007c00u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + }}, + {22u, 0x31038facu, { + 0x3b808081u, 0x3f800000u, 0x3efefeffu, 0x3f008081u, 0x3b808081u, 0x3b808081u, + 0x3f800000u, 0x3b808081u, 0x3f800000u, 0x3efefeffu, 0x3c001c04u, 0x380437f8u, + 0x00000000u, 0x3ef8f8f9u, 0x00000000u, 0x3f7cfcfdu, 0x00000000u, 0x00000000u, + 0x3ef8f8f9u, 0x00000000u, 0x3ef8f8f9u, 0x00000000u, 0x37c80000u, 0x3be80000u, + }}, +}}; + +constexpr std::array InvalidFormats{0x31000facu, 0x3104efacu}; + +constexpr std::array Names{ + "buffer_load_format_xyzw x", "buffer_load_format_xyzw y", "buffer_load_format_xyzw z", "buffer_load_format_xyzw w", + "buffer_load_format_x x", "buffer_load_format_xy x", "buffer_load_format_xy y", "buffer_load_format_xyz x", + "buffer_load_format_xyz y", "buffer_load_format_xyz z", "buffer_load_format_d16_xyzw xy", "buffer_load_format_d16_xyzw zw", +}; + +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, GuestBlock& guest) { + const auto descriptor = reinterpret_cast(guest.Descriptor().data()); + const auto data = reinterpret_cast(guest.Data().data()); + const auto output = reinterpret_cast(guest.Output().data()); + const std::array userData{ + 0u, 0u, 0u, 0u, + static_cast(descriptor), static_cast((descriptor >> 32u) & 0xffffu), static_cast(guest.Descriptor().size_bytes()), 0x31016facu, + static_cast(data), static_cast((data >> 32u) & 0xffffu), 0u, 0u, + static_cast(output), static_cast((output >> 32u) & 0xffffu), static_cast(guest.Output().size_bytes()), 0x31016facu, + }; + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 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& shader, GuestBlock& guest, std::uint32_t records, std::uint32_t word3) { + auto descriptor = guest.Descriptor(); + descriptor[0] = records; + descriptor[1] = word3; + descriptor[2] = 0u; + descriptor[3] = 0u; + std::fill(guest.Output().begin(), guest.Output().end(), Untouched); + device.Dispatch(shader, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock guest; + const auto shader = Compile(*device, guest); + for (const auto& test : Cases) { + Run(*device, shader, guest, test.records, test.word3); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + for (std::uint32_t index = 0; index < Results; ++index) { + const auto actual = guest.Output()[lane * OutputStride + index]; + const auto expected = test.expected[lane * Results + index]; + Require(actual == expected, "gpu buffer format: format " + std::to_string((test.word3 >> 12u) & 0x7fu) + " word3 " + Hex(test.word3) + " records " + std::to_string(test.records) + " lane " + std::to_string(lane) + " " + Names[index] + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } + } + for (const auto word3 : InvalidFormats) { + Run(*device, shader, guest, 32u, word3); + const auto output = guest.Output(); + Require(std::all_of(output.begin(), output.end(), [](std::uint32_t word) { return word == Untouched; }), "gpu buffer format: format " + std::to_string((word3 >> 12u) & 0x7fu) + " did not stop the invocation before its stores"); + } + std::puts("gpu buffer format tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsMisalignedWriterAhead.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsMisalignedWriterAhead.cpp new file mode 100644 index 0000000000..12eb8b6c06 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsMisalignedWriterAhead.cpp @@ -0,0 +1,123 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array WriterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000202u, 0x7e020203u, 0x7e040204u, 0xe07c0000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array CounterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000281u, 0xe0c80000u, 0x80020000u, 0xbf810000u}; + +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t ArgumentsOffset = 4; +std::array BlockStorage{}; +const std::span Block{reinterpret_cast((reinterpret_cast(BlockStorage.data()) + BlockBytes - 1) & ~(BlockBytes - 1)), BlockBytes}; +alignas(256) std::array WriterDescriptor{}; +alignas(256) std::array Counter{}; +alignas(256) std::array CounterDescriptor{}; +alignas(256) volatile std::uint32_t Gate = 1; +alignas(256) volatile std::uint32_t Done = 0; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +std::array Descriptor(const void* pointer, std::uint32_t bytes) { + return {Low(pointer), High(pointer) & 0xffffu, bytes, 0x31016facu}; +} + +std::uint32_t Argument(std::size_t index) { + std::uint32_t value = 0; + std::memcpy(&value, Block.data() + ArgumentsOffset + index * sizeof(value), sizeof(value)); + return value; +} + +void SetArguments(std::uint32_t x, std::uint32_t y, std::uint32_t z) { + const std::array values{x, y, z}; + std::memcpy(Block.data() + ArgumentsOffset, values.data(), sizeof(values)); +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Program(std::vector& words, const void* code, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (const std::uint32_t offset : {0x207u, 0x208u, 0x209u}) SetShaderRegister(words, offset, 1); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); +} + +} + +int main() { + try { + Require(reinterpret_cast(Block.data()) % BlockBytes == 0, "the indirect arguments block is not 64 KiB aligned"); + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + GuestAllocations::Mutation().Add(Block.data(), BlockBytes, true, true, true); + SetArguments(1, 1, 1); + const auto* arguments = Block.data() + ArgumentsOffset; + WriterDescriptor = Descriptor(arguments, 16); + CounterDescriptor = Descriptor(Counter.data(), sizeof(Counter)); + std::vector warmUp; + Program(warmUp, CounterCode.data(), {Low(CounterDescriptor.data()), High(CounterDescriptor.data())}); + warmUp.insert(warmUp.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); + warmUp.insert(warmUp.end(), {0xc0033700u, 0x00100200u, Low(&Done), High(&Done), 2u}); + Packet first{warmUp.data(), static_cast(warmUp.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &first) == 0, "warm-up submit failed"); + AgcDriverWaitIdle_nid_postfix(); + Require(Done == 2 && Counter[0] == 1, "the warm-up dispatch did not run"); + Counter[0] = 0; + const auto adoptedBefore = AgcDriverDispatchesAdoptedAhead_nid_postfix(); + std::vector words; + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(&Gate), High(&Gate), 1u, 0xffffffffu, 0x19u}); + Program(words, WriterCode.data(), {Low(WriterDescriptor.data()), High(WriterDescriptor.data()), 2u, 3u, 4u}); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); + Program(words, CounterCode.data(), {Low(CounterDescriptor.data()), High(CounterDescriptor.data())}); + words.insert(words.end(), {0xc0021600u, Low(arguments), High(arguments), 0x8041u}); + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(&Done), High(&Done), 1u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "compute submit failed"); + AgcDriverWaitIdle_nid_postfix(); + Require(Done == 1, "the submission did not finish"); + Require(Argument(0) == 2 && Argument(1) == 3 && Argument(2) == 4, "the first dispatch did not write the indirect arguments through its misaligned binding"); + Require(Counter[0] == 24, "an indirect dispatch whose arguments the dispatch before it writes through a misaligned binding ran " + std::to_string(Counter[0]) + " groups, expected 24"); + const auto adopted = AgcDriverDispatchesAdoptedAhead_nid_postfix() - adoptedBefore; + Require(adopted == 2, "the gated group adopted " + std::to_string(adopted) + " dispatches resolved at the gate, expected 2: a GPU-read indirect dispatch whose registered arguments an earlier dispatch writes through a misaligned binding must still be captured at the gate"); + GuestAllocations::Mutation().Remove(Block.data()); + std::cout << "gpu indirect arguments misaligned writer ahead test passed\n"; + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsWrittenAhead.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsWrittenAhead.cpp new file mode 100644 index 0000000000..7f0640e1fc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsWrittenAhead.cpp @@ -0,0 +1,101 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array WriterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000202u, 0x7e020203u, 0x7e040204u, 0xe07c0000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array CounterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000281u, 0xe0c80000u, 0x80020000u, 0xbf810000u}; + +alignas(256) std::array Arguments{1, 1, 1, 0}; +alignas(256) std::array ArgumentsDescriptor{}; +alignas(256) std::array Counter{}; +alignas(256) std::array CounterDescriptor{}; +alignas(256) volatile std::uint32_t Gate = 1; +alignas(256) volatile std::uint32_t Done = 0; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +std::array Descriptor(const void* pointer, std::uint32_t bytes) { + return {Low(pointer), High(pointer) & 0xffffu, bytes, 0x31016facu}; +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Program(std::vector& words, const void* code, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (const std::uint32_t offset : {0x207u, 0x208u, 0x209u}) SetShaderRegister(words, offset, 1); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + ArgumentsDescriptor = Descriptor(Arguments.data(), sizeof(Arguments)); + CounterDescriptor = Descriptor(Counter.data(), sizeof(Counter)); + std::vector warmUp; + Program(warmUp, CounterCode.data(), {Low(CounterDescriptor.data()), High(CounterDescriptor.data())}); + warmUp.insert(warmUp.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); + warmUp.insert(warmUp.end(), {0xc0033700u, 0x00100200u, Low(&Done), High(&Done), 2u}); + Packet first{warmUp.data(), static_cast(warmUp.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &first) == 0, "warm-up submit failed"); + AgcDriverWaitIdle_nid_postfix(); + Require(Done == 2 && Counter[0] == 1, "the warm-up dispatch did not run"); + Counter[0] = 0; + const auto adoptedBefore = AgcDriverDispatchesAdoptedAhead_nid_postfix(); + std::vector words; + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(&Gate), High(&Gate), 1u, 0xffffffffu, 0x19u}); + Program(words, WriterCode.data(), {Low(ArgumentsDescriptor.data()), High(ArgumentsDescriptor.data()), 2u, 3u, 4u}); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); + Program(words, CounterCode.data(), {Low(CounterDescriptor.data()), High(CounterDescriptor.data())}); + words.insert(words.end(), {0xc0021600u, Low(Arguments.data()), High(Arguments.data()), 0x8041u}); + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(&Done), High(&Done), 1u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "compute submit failed"); + AgcDriverWaitIdle_nid_postfix(); + Require(Done == 1, "the submission did not finish"); + Require(Arguments[0] == 2 && Arguments[1] == 3 && Arguments[2] == 4, "the first dispatch did not write the indirect arguments"); + Require(Counter[0] == 24, "an indirect dispatch whose arguments the dispatch before it writes ran " + std::to_string(Counter[0]) + " groups, expected 24"); + const auto adopted = AgcDriverDispatchesAdoptedAhead_nid_postfix() - adoptedBefore; + Require(adopted == 2, "the gated group adopted " + std::to_string(adopted) + " dispatches resolved at the gate, expected 2: a GPU-read indirect dispatch whose arguments an earlier dispatch writes must still be captured at the gate"); + std::cout << "gpu indirect arguments written ahead test passed\n"; + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp index f415d1e5e8..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/GuardedNullPointer.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GuardedNullPointer.cpp index d9ad01a30a..172c6a26aa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GuardedNullPointer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GuardedNullPointer.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #ifdef _WIN32 @@ -52,6 +53,12 @@ alignas(256) constexpr std::array ExecCode{ 0x7e02020cu, 0xbefe04c1u, 0xe0701000u, 0x80010102u, 0xbf810000u, }; +alignas(256) constexpr std::array DirectRootCode{ + 0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xbe8a0302u, 0xbe8b0303u, 0xbe8c03ffu, 0x11111111u, + 0xbf8cc07fu, 0xbf068008u, 0xbf850003u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, + 0xe0701000u, 0x80010102u, 0xbf810000u, +}; + alignas(256) constexpr std::array EmptyExecCode{ 0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0x7da80008u, 0xbf800000u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu, @@ -64,6 +71,12 @@ alignas(256) constexpr std::array ChainCode{ 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u, }; +alignas(256) constexpr std::array TwoGuardCode{ + 0xf4080100u, 0xfa000000u, 0xf4040280u, 0xfa000018u, 0xf4040380u, 0xfa000020u, 0xbe8c03ffu, 0x11111111u, + 0xbf8cc07fu, 0xbf068002u, 0xbf840003u, 0xf4000305u, 0xfa000000u, 0xbf820002u, 0xf4000307u, 0xfa000000u, + 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u, +}; + alignas(256) constexpr std::array DescriptorCode{ 0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0x7e0202ffu, 0x11111111u, 0xbf8cc07fu, 0xbf068008u, 0xbf850006u, 0xf4080405u, 0xfa000010u, 0xbf8cc07fu, 0xe0300000u, 0x80040100u, @@ -117,7 +130,7 @@ struct Outcome { }; template -Outcome Run(AgcDriver::VulkanDevice& device, const std::array& code, std::uint64_t pointer, std::uint32_t guard) { +Outcome Run(AgcDriver::VulkanDevice& device, const std::array& code, std::uint64_t pointer, std::uint32_t guard, std::uint32_t groups = 1u, std::uint64_t second = 0u, bool direct = false) { const auto output = static_cast(reinterpret_cast(Output.data())); Root = {}; Root[0] = static_cast(output); @@ -127,11 +140,14 @@ Outcome Run(AgcDriver::VulkanDevice& device, const std::array(pointer); Root[7] = static_cast(pointer >> 32u); + Root[8] = static_cast(second); + Root[9] = static_cast(second >> 32u); std::fill(Output.begin(), Output.end(), 0xdeadbeefu); const auto root = static_cast(reinterpret_cast(Root.data())); - const std::array userData{static_cast(root), static_cast(root >> 32u)}; + const std::array words{static_cast(root), static_cast(root >> 32u), static_cast(pointer), static_cast(pointer >> 32u)}; + const std::span userData(words.data(), direct ? 4u : 2u); const std::span span(code); - const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {false, false, false}, false, 1}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {groups > 1u, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ {ShaderStage::Compute, reinterpret_cast(span.data()), span, 0, {}}, {64, 0, userData, compute, std::nullopt, std::nullopt, {}}, @@ -150,7 +166,7 @@ Outcome Run(AgcDriver::VulkanDevice& device, const std::array(span.data())); + device.Dispatch(*result, groups, 1, 1, {}, reinterpret_cast(span.data())); device.WaitIdle(); } catch (...) { static_cast(log.Finish()); @@ -178,6 +194,47 @@ void RequireWords(const Outcome& outcome, std::uint32_t lanes, std::uint32_t ins } } +void RunDirectRootTests(AgcDriver::VulkanDevice& device) { + const auto valid = static_cast(reinterpret_cast(Data.data())); + void* reserved = nullptr; +#ifdef _WIN32 + reserved = VirtualAlloc(nullptr, 65536, MEM_RESERVE, PAGE_NOACCESS); +#else + reserved = mmap(nullptr, 65536, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (reserved == MAP_FAILED) reserved = nullptr; +#endif + Require(reserved != nullptr, "direct root: cannot reserve an inaccessible range"); + const auto unmapped = static_cast(reinterpret_cast(reserved)); + const auto taken = Run(device, DirectRootCode, valid, 1u, 1u, 0u, true); + Require(!Faulted(taken) && taken.poisoned == 0u && taken.cacheable, "direct root: a mapped taken load faulted, was poisoned or left the caches:\n" + taken.log); + RequireWords(taken, Lanes, Payload, "direct root: a mapped taken load"); + std::puts("direct root: mapped taken load wrote the payload"); + const auto skippedMapped = Run(device, DirectRootCode, valid, 0u, 1u, 0u, true); + Require(!Faulted(skippedMapped) && skippedMapped.poisoned == 0u && skippedMapped.cacheable, "direct root: a mapped skipped load faulted, was poisoned or left the caches:\n" + skippedMapped.log); + RequireWords(skippedMapped, 0u, 0u, "direct root: a mapped skipped load"); + std::puts("direct root: mapped skipped load wrote the default"); + bool refused = false; + try { + static_cast(Run(device, DirectRootCode, unmapped, 1u, 1u, 0u, true)); + } catch (const std::runtime_error& error) { + char expected[96]; + std::snprintf(expected, sizeof(expected), "guest memory is not readable at 0x%llx", static_cast(unmapped)); + Require(std::string(error.what()).find(expected) != std::string::npos, "direct root: the invalid taken load failed for another reason: " + std::string(error.what())); + refused = true; + } + Require(refused, "direct root: an invalid taken load was accepted"); + std::puts("direct root: inaccessible taken load was rejected"); + const auto skipped = Run(device, DirectRootCode, unmapped, 0u, 1u, 0u, true); + Require(!Faulted(skipped) && skipped.poisoned == 0u && skipped.cacheable, "direct root: an inaccessible skipped load faulted, was poisoned or left the caches:\n" + skipped.log); + RequireWords(skipped, 0u, 0u, "direct root: an inaccessible skipped load"); + std::puts("direct root: inaccessible skipped load wrote the default"); +#ifdef _WIN32 + VirtualFree(reserved, 0, MEM_RELEASE); +#else + munmap(reserved, 65536); +#endif +} + void RunTests(AgcDriver::VulkanDevice& device) { const auto valid = static_cast(reinterpret_cast(Data.data())); void* reserved = nullptr; @@ -194,7 +251,7 @@ void RunTests(AgcDriver::VulkanDevice& device) { Require(!Faulted(taken) && taken.poisoned == 0u && taken.cacheable, "guarded pointer: a mapped pointer faulted, was poisoned or left the caches:\n" + taken.log); RequireWords(taken, Lanes, Payload, "guarded pointer: a mapped pointer under a taken branch"); const auto skippedNull = Run(device, BranchCode, 0u, 0u); - Require(!Faulted(skippedNull) && skippedNull.poisoned == 1u && !skippedNull.cacheable, "guarded pointer: a null pointer behind a branch that skips its read faulted or stayed cacheable:\n" + skippedNull.log); + Require(!Faulted(skippedNull) && skippedNull.poisoned == 0u && skippedNull.cacheable, "guarded pointer: a null pointer behind a branch that skips its read faulted, was poisoned or left the caches:\n" + skippedNull.log); RequireWords(skippedNull, 0u, 0u, "guarded pointer: a skipped read through a null pointer"); const auto takenNull = Run(device, BranchCode, 0u, 1u); Require(takenNull.pipelineVariantId == skippedNull.pipelineVariantId && takenNull.pipelineVariantId == taken.pipelineVariantId && takenNull.variantId == taken.variantId, "guarded pointer: the poison or the branch condition changed the pipeline variant"); @@ -204,6 +261,11 @@ void RunTests(AgcDriver::VulkanDevice& device) { Require(Faulted(takenUnmapped) && takenUnmapped.log.find(Fault(unmapped, 0x2cu)) != std::string::npos, "guarded pointer: a read through an unmapped pointer that runs did not fault at its address:\n" + takenUnmapped.log); Require(takenUnmapped.pipelineVariantId == taken.pipelineVariantId, "guarded pointer: an unmapped pointer changed the pipeline variant"); + const auto twoGuards = Run(device, TwoGuardCode, 0u, 0u, 2u, unmapped); + const bool nullRecord = twoGuards.log.find(Fault(0u, 0x2cu)) != std::string::npos; + const bool unmappedRecord = twoGuards.log.find(Fault(unmapped, 0x38u)) != std::string::npos; + Require(twoGuards.poisoned == 2u && Faulted(twoGuards) && nullRecord != unmappedRecord, "guarded pointer: two workgroups faulting through different guards did not report exactly one of their two records:\n" + twoGuards.log); + const auto someLanes = Run(device, ExecCode, valid, 5u); Require(!Faulted(someLanes), "guarded pointer: a mapped pointer under EXEC faulted:\n" + someLanes.log); RequireWords(someLanes, 5u, Payload, "guarded pointer: a mapped pointer read under EXEC"); @@ -220,7 +282,7 @@ void RunTests(AgcDriver::VulkanDevice& device) { Require(noLanes.pipelineVariantId == someLanes.pipelineVariantId && oneLane.pipelineVariantId == someLanes.pipelineVariantId && emptyNull.pipelineVariantId == emptyMapped.pipelineVariantId, "guarded pointer: the poison changed an EXEC program's pipeline variant"); const auto chainSkipped = Run(device, ChainCode, 0u, 0u); - Require(chainSkipped.poisoned == 3u && Faulted(chainSkipped) && chainSkipped.log.find(Fault(0u, 0x24u)) != std::string::npos, "guarded pointer: a pointer read through a null pointer, used only by a skipped read, did not fault at its own pc:\n" + chainSkipped.log); + Require(chainSkipped.poisoned == 2u && Faulted(chainSkipped) && chainSkipped.log.find(Fault(0u, 0x24u)) != std::string::npos, "guarded pointer: a pointer read through a null pointer, used only by a skipped read, did not fault at its own pc:\n" + chainSkipped.log); const auto chainTaken = Run(device, ChainCode, 0u, 1u); Require(Faulted(chainTaken) && chainTaken.log.find(Fault(0u, 0x24u)) != std::string::npos, "guarded pointer: a read through a pointer read through a null pointer did not fault at the first read:\n" + chainTaken.log); Indirect[0] = valid; @@ -234,7 +296,7 @@ void RunTests(AgcDriver::VulkanDevice& device) { Require(!Faulted(descriptorTaken) && descriptorTaken.poisoned == 0u && descriptorTaken.cacheable, "guarded pointer: a V# behind a mapped pointer faulted, was poisoned or left the caches:\n" + descriptorTaken.log); RequireWords(descriptorTaken, Lanes, Payload, "guarded pointer: a buffer read through a V# behind a mapped pointer"); const auto descriptorSkipped = Run(device, DescriptorCode, 0u, 0u); - Require(!Faulted(descriptorSkipped) && descriptorSkipped.poisoned == 4u && !descriptorSkipped.cacheable, "guarded pointer: a V# behind a branch that skips its null pointer faulted or stayed cacheable:\n" + descriptorSkipped.log); + Require(!Faulted(descriptorSkipped) && descriptorSkipped.poisoned == 0u && descriptorSkipped.cacheable, "guarded pointer: a V# behind a branch that skips its null pointer faulted, was poisoned or left the caches:\n" + descriptorSkipped.log); RequireWords(descriptorSkipped, 0u, 0u, "guarded pointer: a skipped V# load through a null pointer"); const auto descriptorNull = Run(device, DescriptorCode, 0u, 1u); Require(descriptorNull.pipelineVariantId == descriptorSkipped.pipelineVariantId && Faulted(descriptorNull) && descriptorNull.log.find(Fault(0x10u, 0x2cu)) != std::string::npos, "guarded pointer: a V# load through a null pointer that runs did not fault at its pc and address:\n" + descriptorNull.log); @@ -254,12 +316,18 @@ void RunTests(AgcDriver::VulkanDevice& device) { } -int main() { +int main(int argc, char** argv) { try { + Require(argc == 1 || (argc == 2 && std::string(argv[1]) == "--direct-root-only"), "unknown guarded-pointer test arguments"); const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - RunTests(*device); - std::puts("guarded null pointer tests passed"); + if (argc == 2) { + RunDirectRootTests(*device); + std::puts("guarded direct root tests passed"); + } else { + RunTests(*device); + std::puts("guarded null pointer tests passed"); + } return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp new file mode 100644 index 0000000000..2d737ebc4e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp @@ -0,0 +1,272 @@ +#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, std::size_t bytes = FillBytes) { + const auto* data = static_cast(pointer); + for (std::size_t offset = 0; offset < bytes; 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::size_t bytes = FillBytes) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(device.FillBuffer(Address(pointer), bytes, Pattern), "Vulkan device did not import the guest allocation"); + Require(Imported(device, pointer, bytes), "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, 3 * BlockBytes); + auto* middle = static_cast(mapping) + BlockBytes; + Require(mprotect_nid_postfix(middle, BlockBytes, 1) == 0, "guest partial protection failed"); + Require(!Imported(device, mapping, 3 * BlockBytes), "whole host import survived partial protection change"); + CheckPattern(mapping, 3 * BlockBytes); + Require(munmap_nid_postfix(middle, BlockBytes) == 0, "guest middle unmap after protection change failed"); + Require(!Imported(device, mapping, 3 * BlockBytes), "host import still covered the protected and unmapped middle range"); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "guest prefix unmap after protection change failed"); + Require(munmap_nid_postfix(middle + BlockBytes, BlockBytes) == 0, "guest suffix unmap after protection change failed"); + + mapping = Map(3 * BlockBytes); + Fill(device, mapping, 3 * BlockBytes); + Require(Imported(device, mapping, 3 * BlockBytes), "whole host import did not cover the three-block mapping before middle unmap"); + middle = static_cast(mapping) + BlockBytes; + Require(munmap_nid_postfix(middle, BlockBytes) == 0, "guest middle unmap failed"); + Require(!Imported(device, mapping, 3 * BlockBytes), "middle unmap left a host import spanning the unmapped range"); + Require(!Imported(device, mapping), "middle unmap retained the prefix of the retired host import"); + Fill(device, mapping); + Require(Imported(device, mapping), "the retained prefix was not imported again after 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* span = static_cast(Map(3 * BlockBytes)); + Require(munmap_nid_postfix(span + BlockBytes, BlockBytes) == 0, "guest copy gap unmap failed"); + void* source = span; + void* destination = span + 2 * BlockBytes; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Fill(device, source); + 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/HostImportWindows.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp index 57396daef2..e6d9bd6b30 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "VulkanTestDevice.hpp" #ifdef _WIN32 #ifndef NOMINMAX @@ -124,6 +125,44 @@ Outcome WindowsFollowTheirRange() { return Outcome::Passed; } +Outcome ResidentMappingsImportAfterCollection() { +#ifdef _WIN32 + constexpr std::size_t bytes = MiB; + auto* data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, 0x200000)); + Require(data != nullptr, "cannot allocate resident import address"); + const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE | SEC_RESERVE, 0, bytes, nullptr); + Require(section != nullptr, "cannot reserve resident import backing"); + GuestArena::GuestArenaMapDirect_nid_postfix(data, bytes, section, 0, PAGE_READWRITE); + std::memset(data, 0x37, bytes); + GuestAllocations::Mutation().Add(data, bytes, true, true, true); + const auto address = reinterpret_cast(data); + Require(AgcDriver::GuestMemory::CollectWritesUncached(address, bytes) != 0, "resident import memory is not watched"); + { + auto device = OpenVulkanTestDevice(); + Require(device != nullptr, "resident import test lost its Vulkan device"); + const auto context = Probe(*device); + for (unsigned int pass = 0; pass < 2; ++pass) { + Require(AgcDriver::GuestMemory::CollectWritesUncached(address, bytes) != 0, "resident import collection failed"); + Require(Fill(*device, address), "resident memory could not be filled after collecting its writes"); + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(Graphics::HostImportCovers(context, address, ProbeBytes), "resident import did not cover its data"); + Graphics::ClearHostImports(device->Device()); + } + Require(Fill(*device, address), "resident memory could not be filled after collecting its writes"); + std::uint32_t first = 0; + std::memcpy(&first, data, sizeof(first)); + Require(first == 0x11111111u && data[ProbeBytes] == 0x37 && data[bytes - 1] == 0x37, "resident import lost the GPU fill or neighboring bytes"); + Graphics::ClearHostImports(device->Device()); + } + GuestAllocations::Mutation().Remove(data); + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + GuestArena::GuestArenaForgetSection_nid_postfix(section); + CloseHandle(section); +#endif + return Outcome::Passed; +} + } int main() { @@ -136,6 +175,7 @@ int main() { } const auto windows = WindowsFollowTheirRange(); Require(windows == Outcome::Passed, "host import windows: the device stopped importing guest memory between two scenarios"); + Require(ResidentMappingsImportAfterCollection() == Outcome::Passed, "resident import test failed"); std::puts("host import windows tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/IdleStoreOverUnmap.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IdleStoreOverUnmap.cpp new file mode 100644 index 0000000000..21fbc192ec --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IdleStoreOverUnmap.cpp @@ -0,0 +1,231 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t Block = 0x10000; +constexpr std::size_t SurfaceBytes = 4 * Block; +constexpr std::uint32_t Untouched = 0xdeadbeefu; +constexpr std::uint32_t Stored = 0x12345678u; +constexpr std::uint32_t StoredAgain = 0x0badf00du; +constexpr auto IdleStoreWindow = std::chrono::milliseconds(500); + +alignas(256) constexpr std::array StoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, Stored, 0xf0200108, 0x00020102, 0xbf810000, +}; + +alignas(256) constexpr std::array StoreAgainCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, StoredAgain, 0xf0200108, 0x00020102, 0xbf810000, +}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::array TextureDescriptor(const std::uint32_t* texels) { + const auto address = AddressOf(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Store(const std::uint32_t* texels, std::span code) { + std::array userData{}; + const auto texture = TextureDescriptor(texels); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::vector words; + const auto registers = [&](const std::uint32_t first, const std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{8, 1, 1}; + registers(0x207, shape); + const auto address = AddressOf(code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array layout{static_cast(userData.size()) << 1u}; + registers(0x213, layout); + registers(0x240, userData); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); +} + +std::uint32_t* MapSurface() { +#ifdef _WIN32 + auto* texels = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, Block)); + if (texels != nullptr) GuestArena::GuestArenaCommit_nid_postfix(texels, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); + return texels; +#else + void* mapped = mmap(nullptr, SurfaceBytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped == MAP_FAILED) return nullptr; + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + Block - 1) & ~std::uintptr_t{Block - 1}; + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned != begin + Block) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + Block - aligned); + auto* texels = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(texels, SurfaceBytes); + return texels; +#endif +} + +void ReleaseSurface(std::uint32_t* texels) { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(texels, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(texels, SurfaceBytes); +#else + munmap(texels, SurfaceBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(texels, SurfaceBytes); +#endif +} + +bool Holds(const std::uint32_t* texels, std::uint32_t value) { + const auto* words = static_cast(texels); + for (std::size_t index = 0; index < SurfaceBytes / 4u; ++index) { + if (words[index] == value) return true; + } + return false; +} + +bool HoldLive(std::unique_lock& hold, const std::uint32_t* texels, std::uint32_t result) { + const auto address = AddressOf(texels); + 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; + hold.unlock(); + if (Holds(texels, result)) return false; + Require(std::chrono::steady_clock::now() < deadline, "idle store over unmap: the result neither became pending nor reached guest memory"); + std::this_thread::yield(); + } +} + +enum class Outcome { Settled, Missed, NotRun }; + +Outcome Run() { + auto* texels = MapSurface(); + Require(texels != nullptr, "idle store over unmap: cannot allocate the guest surface"); + std::fill(texels, texels + SurfaceBytes / 4u, Untouched); + GuestAllocations::Mutation().Add(texels, SurfaceBytes, true, true, true); + bool mapped = true; + struct Unmapped { + std::uint32_t* texels; + bool& mapped; + ~Unmapped() { + if (!mapped) return; + AgcDriverWaitIdle_nid_postfix(); + GuestAllocations::Mutation().Remove(texels); + ReleaseSurface(texels); + } + } unmapped{texels, mapped}; + Require(AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels))).guestBytes == SurfaceBytes, "idle store over unmap: unexpected surface size"); + const auto address = AddressOf(texels); + if (!AgcDriver::GuestMemory::Watched(address, SurfaceBytes)) { + std::printf("skipped, the surface is not write-watched\n"); + return Outcome::NotRun; + } + AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes); + Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes), "idle store over unmap: the surface is still write-watched"); + std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex(), std::defer_lock); + Store(texels, StoreCode); + if (!HoldLive(hold, texels, Stored)) return Outcome::Missed; + StorageTexture::FlushAllPending("idle store over unmap fixture"); + hold.unlock(); + { + std::vector first(SurfaceBytes); + AgcDriver::GuestMemory::Read(address, first); + } + Require(static_cast(texels)[0] == Stored, "idle store over unmap: the first result was not stored"); + hold.lock(); + const auto image = StorageTexture::FindLive(address, SurfaceBytes); + const bool saved = image != nullptr && image->GuestSnapshotValid(); + hold.unlock(); + Require(image != nullptr, "idle store over unmap: the surface has no storage image"); + if (!saved) { + std::printf("skipped, the first store left no saved guest bytes to compare with\n"); + return Outcome::NotRun; + } + Store(texels, StoreAgainCode); + if (!HoldLive(hold, texels, StoredAgain)) return Outcome::Missed; + { + GuestAllocations::Mutation mutation; + mutation.Unmap(texels, SurfaceBytes, [](const void* piece, std::size_t, const void*, bool) { + ReleaseSurface(static_cast(const_cast(piece))); + }); + mapped = false; + hold.unlock(); + std::this_thread::sleep_for(IdleStoreWindow); + Require(StorageTexture::AnyPending(), "idle store over unmap: the queue's idle store ran while a guest unmap of the surface was in progress"); + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (StorageTexture::AnyPending() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(!StorageTexture::AnyPending(), "idle store over unmap: the queue's idle store did not settle the results of the unmapped surface"); + AgcDriverWaitIdle_nid_postfix(); + return Outcome::Settled; +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + struct ShutdownGuard { + ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } + } shutdown; + auto outcome = Outcome::Missed; + for (std::uint32_t attempt = 0; attempt < 64u && outcome == Outcome::Missed; ++attempt) outcome = Run(); + Require(outcome != Outcome::Missed, "idle store over unmap: the result was never held pending"); + if (outcome == Outcome::NotRun) return VulkanTestSkipped; + std::puts("idle store over unmap 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/ImageArraySampleOnVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArraySampleOnVolume.cpp new file mode 100644 index 0000000000..958a05d7ba --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArraySampleOnVolume.cpp @@ -0,0 +1,224 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type3D = 10; +constexpr std::uint32_t Depth = 4; +constexpr std::uint32_t SampleResult = 8; +constexpr std::uint32_t LoadResult = 12; +alignas(256) std::array Buffer{}; +alignas(256) std::array Volume{}; + +alignas(256) constexpr std::array ArraySampleCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf09c0f28, 0x00610c02, + 0xf0001f10, 0x00011005, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, + 0x80000e01, 0xe070102c, 0x80000f01, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, + 0x80001201, 0xe070103c, 0x80001301, 0xbf810000, +}; + +alignas(256) constexpr std::array VolumeSampleCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf09c0f10, 0x00610c02, + 0xf0001f10, 0x00011005, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, + 0x80000e01, 0xe070102c, 0x80000f01, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, + 0x80001201, 0xe070103c, 0x80001301, 0xbf810000, +}; + +alignas(256) constexpr std::array ArrayExplicitLodCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xbf8c3f70, 0xf0900f28, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, + 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, +}; + +alignas(256) constexpr std::array VolumeExplicitLodCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xbf8c3f70, 0xf0900f10, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, + 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, +}; + +alignas(256) constexpr std::array ArrayBiasCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xbf8c3f70, 0xf0940f28, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, + 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, +}; + +struct Lane { + std::uint32_t x; + std::uint32_t y; + std::uint32_t z; + float r; +}; + +Lane LaneOf(std::uint32_t tid) { + const auto z = tid % Depth; + const auto wraps = static_cast((tid / Depth) % 4u) - 1.0f; + return {(tid * 7u + 3u) % Size, (tid * 5u + 1u) % Size, z, (static_cast(z) + 0.5f) / static_cast(Depth) + wraps}; +} + +std::uint32_t Bits(float value) { + return std::bit_cast(value); +} + +void FillInput(bool explicitLod, float lod) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto lane = LaneOf(tid); + const auto u = Bits((static_cast(lane.x) + 0.5f) / static_cast(Size)); + const auto v = Bits((static_cast(lane.y) + 0.5f) / static_cast(Size)); + const std::array words{u, v, Bits(lane.r), explicitLod ? Bits(lod) : lane.x, lane.y, lane.z}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } +} + +std::uint8_t Marker(std::uint32_t level, std::uint32_t z) { + return static_cast(level * 25u + z * 50u + 7u); +} + +std::array VolumeDescriptor(std::uint32_t levels) { + const auto address = static_cast(reinterpret_cast(Volume.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | ((levels - 1u) << 16u) | (Type3D << 28u), + Depth - 1u, + (levels - 1u) << 4u, + 0u, + 0u, + }; +} + +void FillVolume(std::uint32_t levels) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(VolumeDescriptor(levels))); + Require(geometry.guestBytes <= Volume.size(), "image array sample on a volume: the texture does not fit the texel storage"); + std::fill(Volume.begin(), Volume.end(), 0xeeu); + for (std::uint32_t level = 0; level < levels; ++level) { + const auto& mip = geometry.mips.at(level); + for (std::uint32_t z = 0; z < Depth; ++z) { + if (!geometry.HasLayer(level, z)) continue; + for (std::uint32_t y = 0; y < mip.height; ++y) { + for (std::uint32_t x = 0; x < mip.width; ++x) { + auto* texel = &Volume[geometry.GuestLayerOffset(z) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; + texel[0] = Marker(level, z); + texel[1] = static_cast(x); + texel[2] = static_cast(y); + texel[3] = 255u; + } + } + } + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint32_t levels) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto texture = VolumeDescriptor(levels); + const auto sampler = SamplerDescriptor(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Expect(std::uint32_t tid, std::uint32_t result, const std::array& expected, const std::string& name) { + for (std::uint32_t component = 0; component < 4u; ++component) { + const float value = std::bit_cast(Buffer[tid * Words + result + component]) * 255.0f; + Require(std::lround(value) == static_cast(expected[component]), name + ": thread " + std::to_string(tid) + " (r " + std::to_string(LaneOf(tid).r) + ") component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + } +} + +std::array Texel(std::uint32_t tid, std::uint32_t level) { + const auto lane = LaneOf(tid); + return {Marker(level, lane.z >> level), lane.x >> level, lane.y >> level, 255u}; +} + +void CheckSamples(const std::string& name, std::uint32_t level, bool load) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Expect(tid, SampleResult, Texel(tid, level), name); + if (load) Expect(tid, LoadResult, Texel(tid, 0u), "image_load 3d on a 3D texture"); + } +} + +void RequireRefused(AgcDriver::VulkanDevice& device) { + std::string refusal; + try { + Run(device, ArrayBiasCode, 1u); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find("incompatible with the static runtime image interface") != std::string::npos, "image_sample_b 2d_array on a 3D texture was not refused: " + refusal); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillVolume(1u); + FillInput(false, 0.0f); + Run(*device, VolumeSampleCode, 1u); + CheckSamples("image_sample_lz 3d on a 3D texture", 0u, true); + FillInput(false, 0.0f); + Run(*device, ArraySampleCode, 1u); + CheckSamples("image_sample_lz 2d_array on a 3D texture", 0u, true); + FillInput(true, 0.0f); + Run(*device, ArrayExplicitLodCode, 1u); + CheckSamples("image_sample_l 2d_array on a 3D texture", 0u, false); + FillVolume(2u); + FillInput(true, 1.0f); + Run(*device, VolumeExplicitLodCode, 2u); + CheckSamples("image_sample_l 3d with LOD 1 on a two-level 3D texture", 1u, false); + FillInput(true, 1.0f); + Run(*device, ArrayExplicitLodCode, 2u); + CheckSamples("image_sample_l 2d_array with LOD 1 on a two-level 3D texture", 1u, false); + RequireRefused(*device); + std::puts("image array sample on volume tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp new file mode 100644 index 0000000000..d6a5e73941 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp @@ -0,0 +1,151 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Width = 8; +constexpr std::uint32_t Layers = 3; +constexpr std::uint32_t BaseArray = 8100; +constexpr std::uint32_t LastArray = BaseArray + Layers - 1u; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type1DArray = 12; +constexpr std::uint32_t ClampEdge = 2; + +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +constexpr std::size_t TexelBytes = 4u << 20u; +constexpr std::size_t TexelAlignment = 65536; +std::array TexelStorage{}; +const std::span Texels{reinterpret_cast((reinterpret_cast(TexelStorage.data()) + TexelAlignment - 1) & ~(TexelAlignment - 1)), TexelBytes}; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf09c0f20, 0x00820602, 0xf0001f20, 0x00020a04, + 0x34020085, 0xbf8c3f70, 0xe0781000, 0x80010601, 0xe0781010, 0x80010a01, 0xbf810000, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(std::uint32_t baseArray = BaseArray) { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Width - 1u) & 3u) << 30u), + (Width - 1u) >> 2u, + 0xfacu | (Type1DArray << 28u), + LastArray | (baseArray << 16u), 0u, 0u, 0u, + }; +} + +std::array SamplerDescriptor() { + return {ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, 0u, 0u}; +} + +std::array Texel(std::uint32_t x, std::uint32_t guestLayer) { + return {static_cast((guestLayer * Width + x) * 9u + 3u), static_cast(x * 30u + guestLayer % 7u), static_cast(guestLayer), 0xff}; +} + +void FillTexels() { + std::ranges::fill(Texels, std::uint8_t{0}); + const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor())); + const auto& mip = surface.mips.at(0); + Require(surface.layers == LastArray + 1u && surface.guestBytes <= Texels.size(), "the test array does not fit its storage"); + for (std::uint32_t layer = 0; layer <= LastArray; ++layer) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto texel = Texel(x, layer); + std::copy(texel.begin(), texel.end(), Texels.begin() + static_cast(surface.GuestLayerOffset(layer) + mip.tiledOffset + x * 4u)); + } + } +} + +void FillInput() { + constexpr std::array layers{0.0f, 1.0f, 2.0f, 0.4f, 1.6f, 2.4f, -1.0f, 5.0f}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * 4u + 0u] = std::bit_cast(-0.2f + 0.045f * static_cast(tid)); + Input[tid * 4u + 1u] = std::bit_cast(layers[tid % layers.size()]); + Input[tid * 4u + 2u] = tid % Width; + Input[tid * 4u + 3u] = (tid / Width) % Layers; + } +} + +void Expect(std::uint32_t tid, const char* name, const float* actual, const std::array& expected) { + for (std::uint32_t component = 0; component < 4u; ++component) { + const float scaled = actual[component] * 255.0f; + Require(std::fabs(scaled - std::round(scaled)) < 1e-3f && std::lround(scaled) == expected[component], std::string(name) + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(scaled) + "/255, expected " + std::to_string(expected[component]) + "/255"); + } +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t baseArray = BaseArray) { + Output.fill(-1.0f); + std::vector userData(20, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); + const auto texture = TextureDescriptor(baseArray); + const auto sampler = SamplerDescriptor(); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float u = std::bit_cast(Input[tid * 4u + 0u]); + const float layer = std::clamp(std::nearbyint(std::bit_cast(Input[tid * 4u + 1u])), 0.0f, static_cast(LastArray - baseArray)); + const auto x = static_cast(std::clamp(static_cast(std::floor(u * static_cast(Width))), 0, static_cast(Width) - 1)); + Expect(tid, "image_sample_lz on a window of a 1D array past maxImageArrayLayers", &Output[tid * 8u], Texel(x, baseArray + static_cast(layer))); + Expect(tid, "image_load on a window of a 1D array past maxImageArrayLayers", &Output[tid * 8u + 4u], Texel(Input[tid * 4u + 2u], baseArray + Input[tid * 4u + 3u])); + } +} + +} + +int main() { + try { + Require(reinterpret_cast(Texels.data()) % TexelAlignment == 0, "the test array is not 64 KiB aligned"); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexels(); + FillInput(); + Run(*device); + try { + Run(*device, 0); + } catch (const std::exception& error) { + if (std::string(error.what()).find("an image view of more array slices than maxImageArrayLayers") == std::string::npos) throw; + } + std::puts("a window of a 1D array past maxImageArrayLayers was sampled and loaded from its viewed layers"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp index 6c036012f0..94de1a8025 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp @@ -2,6 +2,8 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "Optimization/ResourceMaterializer.hpp" +#include "Optimization/ResourceProgram.hpp" #include "Recompiler.hpp" #include "VulkanTestDevice.hpp" #ifdef _WIN32 @@ -48,6 +50,15 @@ constexpr std::size_t BlockBytes = 65536; constexpr std::size_t RoundTripOffset = 16384; constexpr std::size_t StoreOffset = 32768; constexpr std::array PointSampler{0x00000092u, 0x00fff000u, 0u, 0u}; +constexpr std::array BilinearSampler{0x00000092u, 0x00fff000u, 0x00500000u, 0u}; +constexpr std::array WrapBilinearSampler{0x00000090u, 0x00fff000u, 0x00500000u, 0u}; +constexpr std::array WrapPointSampler{0x00000090u, 0x00fff000u, 0u, 0u}; +constexpr std::array BorderSampler{0x000000b6u, 0x00fff000u, 0u, 0u}; +constexpr std::uint32_t SampleStep = 0x3a200000u; +constexpr std::uint32_t OffGridStep = 0x3a206000u; +constexpr std::uint32_t SampleStart = 0x39c00000u; +constexpr std::uint32_t WrappedStart = 0xbac00000u; +constexpr double SampleTolerance = 1.0e-5; alignas(256) std::array Output{}; alignas(256) std::array Input{}; @@ -73,6 +84,13 @@ constexpr std::array SamplerCode(std::uint32_t word0) { }; } +constexpr std::array SampleAtCode(std::uint32_t step, std::uint32_t start) { + return { + 0x34060084u, 0x7e280d00u, 0x102828ffu, step, 0x062828ffu, start, 0x7e2a0280u, 0xf09c0f08u, + 0x00610a14u, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, 0xbf810000u, + }; +} + alignas(256) constexpr std::array RoundTrip{ 0x7e080218u, 0x343c0885u, 0x4a3c3d00u, 0x7e3e0280u, 0xf0001708u, 0x00010a1eu, 0xbf8c3f70u, 0xf0201708u, 0x00040a1eu, 0xbf810000u, @@ -88,6 +106,10 @@ alignas(256) constexpr auto StoreD16 = StoreCode(0x7u, D16); alignas(256) constexpr auto SampleLz = SamplerCode(0xf09c0f08u); alignas(256) constexpr auto Gather4Lz = SamplerCode(0xf11c0108u); alignas(256) constexpr auto GetLod = SamplerCode(0xf1800308u); +alignas(256) constexpr auto SampleAt = SampleAtCode(SampleStep, SampleStart); +alignas(256) constexpr auto SampleWrapped = SampleAtCode(SampleStep, WrappedStart); +alignas(256) constexpr auto SampleOffGrid = SampleAtCode(OffGridStep, SampleStart); +alignas(256) constexpr auto SampleWrappedOffGrid = SampleAtCode(OffGridStep, WrappedStart); constexpr std::array StoreValues{ 0x00000000u, 0x3f800000u, 0x3f000000u, 0x3eaaaaabu, 0x80000000u, 0xbf800000u, @@ -322,6 +344,94 @@ void CheckFloatStore(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { } } +void CheckFloatSample(AgcDriver::VulkanDevice& device, const std::uint8_t* texels, const std::array& expected, const std::string& what) { + Run(device, SampleLz, TextureDescriptor(texels, FloatFormat, SwizzleXYZ1, Threads), PointSampler, 1); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto actual = Output[tid * 4u + component]; + const bool nan = (expected[component] & 0x7f800000u) == 0x7f800000u && (expected[component] & 0x7fffffu) != 0u; + Require(nan ? std::isnan(std::bit_cast(actual)) : actual == expected[component], what + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(expected[component])); + } + } +} + +double ChannelValue(std::uint32_t texel, std::uint32_t swizzle, std::uint32_t component) { + const auto selector = (swizzle >> (component * 3u)) & 7u; + if (selector == 0u) return 0.0; + if (selector == 1u) return 1.0; + return static_cast(Field(texel, selector - 4u)) / (selector == 6u ? 1023.0 : 2047.0); +} + +void CheckSample(AgcDriver::VulkanDevice& device, const std::uint8_t* texels, std::span code, std::uint32_t step, std::uint32_t start, std::uint32_t swizzle, const std::array& sampler, bool linear, bool wrap, const std::string& what) { + Run(device, code, TextureDescriptor(texels, UnormFormat, swizzle, LoadWidth), sampler, 1); + const auto texelAt = [&](std::int64_t index) { + const auto width = static_cast(LoadWidth); + const auto addressed = wrap ? ((index % width) + width) % width : std::clamp(index, 0, width - 1); + std::uint32_t texel = 0; + std::memcpy(&texel, texels + addressed * 4u, 4u); + return texel; + }; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = (static_cast(tid) * static_cast(std::bit_cast(step)) + static_cast(std::bit_cast(start))) * LoadWidth; + for (std::uint32_t component = 0; component < 4u; ++component) { + double expected = 0.0; + if (linear) { + const auto centre = x - 0.5; + const auto first = static_cast(std::floor(centre)); + const auto weight = std::nearbyint((centre - std::floor(centre)) * 256.0) / 256.0; + const auto left = ChannelValue(texelAt(first), swizzle, component); + const auto right = ChannelValue(texelAt(first + 1), swizzle, component); + expected = left + (right - left) * weight; + } else { + expected = ChannelValue(texelAt(static_cast(std::floor(x))), swizzle, component); + } + const auto actual = static_cast(std::bit_cast(Output[tid * 4u + component])); + Require(std::fabs(actual - expected) <= SampleTolerance * std::max(1.0, std::fabs(expected)), what + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected)); + } + } +} + +void CheckConvertedPreparation(AgcDriver::VulkanDevice& device, const std::uint8_t* texels) { + std::vector userData(24, 0u); + const auto texture = TextureDescriptor(texels, UnormFormat, SwizzleXYZ1, LoadWidth); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(PointSampler.begin(), PointSampler.end(), userData.begin() + 12); + const std::span code = SampleAt; + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {true, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + auto converted = request; + converted.context.convertedSamples = true; + const auto convertedMode = [](const ShaderRecompiler::ImageResource& mode) { return mode.conversionFormat == ShaderRecompiler::IrBufferFormat::Format11_11_10UNorm; }; + const auto plain = ShaderRecompiler::GetResourcePlan(request); + Require(plain->info.images.size() == 1u && std::ranges::none_of(plain->info.runtimeImageModes.at(0), convertedMode), "a sampled image is prepared with converted-sample modes before a converted descriptor was seen"); + const auto prepared = ShaderRecompiler::GetResourcePlan(converted); + Require(prepared->info.images.size() == 1u && std::ranges::count_if(prepared->info.runtimeImageModes.at(0), convertedMode) == 1, "a sampled image is not prepared with the converted unorm sample mode once a converted descriptor was seen"); + const auto handle = ShaderRecompiler::PrepareShader(request); + Require(!ShaderRecompiler::PreparedShaderInvocation::TryCreate(converted, handle).has_value(), "an artifact prepared without converted-sample modes matches a request that asks for them"); + const auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(request, handle); + Require(invocation.has_value(), "the prepared artifact does not match its own request"); + ShaderRecompiler::SrtRuntime runtime; + runtime.userData = userData; + runtime.shaderBase = request.shader.codeAddress; + std::string reason; + try { + static_cast(invocation->Capture(runtime)); + } catch (const ShaderRecompiler::ConvertedSampleRequired& error) { + reason = error.what(); + } + Require(!reason.empty(), "capturing a converted descriptor through an artifact prepared without converted-sample modes does not ask for them"); + const auto convertedHandle = ShaderRecompiler::PrepareShader(converted); + const auto convertedInvocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(converted, convertedHandle); + Require(convertedInvocation.has_value(), "the artifact prepared with converted-sample modes does not match its request"); + static_cast(convertedInvocation->Capture(runtime)); +} + void RequireRefused(AgcDriver::VulkanDevice& device, std::span code, const std::uint8_t* texels, std::uint32_t swizzle, const std::array& extra, const std::string& reason, const std::string& what, std::uint32_t format = UnormFormat) { std::string refusal; try { @@ -353,9 +463,18 @@ int main() { CheckStore(*device, texels + StoreOffset, StoreXyz, 0x7u, "image_store dmask:0x7"); CheckStore(*device, texels + StoreOffset, StoreXz, 0x5u, "image_store dmask:0x5"); CheckFloatStore(*device, texels + FloatOffset); + Run(*device, StoreXyz, TextureDescriptor(texels + FloatOffset, FloatFormat, SwizzleXYZ1, Threads), BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)), 1); + CheckFloatSample(*device, texels + FloatOffset, {0u, 0u, 0x7fc00000u, 0x3f800000u}, "image_sample_lz of R10_G11_B11_FLOAT results still on the GPU"); CheckFloatLoad(*device, texels + FloatOffset); - RequireRefused(*device, SampleLz, texels + FloatOffset, SwizzleXYZ1, PointSampler, "samples or gathers a converted float image", "image_sample_lz of R10_G11_B11_FLOAT", FloatFormat); - RequireRefused(*device, SampleLz, texels, SwizzleXYZ1, PointSampler, "sampling, gathering or querying LOD of a converted unorm image", "image_sample_lz"); + CheckFloatSample(*device, texels + FloatOffset, {0x7f800000u, 0x7f800000u, 0x387c0000u, 0x3f800000u}, "image_sample_lz of R10_G11_B11_FLOAT"); + CheckSample(*device, texels, SampleAt, SampleStep, SampleStart, SwizzleXYZ1, PointSampler, false, false, "point image_sample_lz X Y Z 1"); + CheckSample(*device, texels, SampleAt, SampleStep, SampleStart, SwizzleZYX1, BilinearSampler, true, false, "bilinear image_sample_lz Z Y X 1"); + CheckSample(*device, texels, SampleWrapped, SampleStep, WrappedStart, SwizzleX011, WrapPointSampler, false, true, "wrapped point image_sample_lz X 0 1 1"); + CheckSample(*device, texels, SampleWrapped, SampleStep, WrappedStart, SwizzleXYZ1, WrapBilinearSampler, true, true, "wrapped bilinear image_sample_lz X Y Z 1"); + CheckSample(*device, texels, SampleOffGrid, OffGridStep, SampleStart, SwizzleZYX1, BilinearSampler, true, false, "off-grid bilinear image_sample_lz Z Y X 1"); + CheckSample(*device, texels, SampleWrappedOffGrid, OffGridStep, WrappedStart, SwizzleXYZ1, WrapBilinearSampler, true, true, "off-grid wrapped bilinear image_sample_lz X Y Z 1"); + CheckConvertedPreparation(*device, texels); + RequireRefused(*device, SampleLz, texels, SwizzleXYZ1, BorderSampler, "implemented only with wrap or clamp-to-edge addressing", "image_sample_lz with clamp-to-border addressing"); RequireRefused(*device, Gather4Lz, texels, SwizzleXYZ1, PointSampler, "sampling, gathering or querying LOD of a converted unorm image", "image_gather4_lz"); RequireRefused(*device, GetLod, texels, SwizzleXYZ1, PointSampler, "sampling, gathering or querying LOD of a converted unorm image", "image_get_lod"); RequireRefused(*device, LoadD16, texels, SwizzleXYZ1, {}, "converted unorm image with 16-bit data", "image_load d16"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageMsaaAddressOnPlane.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageMsaaAddressOnPlane.cpp new file mode 100644 index 0000000000..a2d0b12593 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageMsaaAddressOnPlane.cpp @@ -0,0 +1,115 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +using AgcDriver::Graphics::Require; +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Words = 8; +alignas(256) std::array buffer{}; +alignas(256) std::array texture{}; +constexpr std::array Coordinates{0u, 1u, 3u, 4u}; +constexpr std::array Samples{0u, 1u, 7u, 0xffffffffu}; +constexpr std::array UnormExpected{0x0u, 0x3b808081u, 0x3c40c0c1u, 0x3e808081u, 0x3e828283u, 0x3e868687u, 0x3f40c0c1u, 0x3f41c1c2u, 0x3f43c3c4u}; +constexpr std::array SrgbExpected{0x0u, 0x399f0000u, 0x3a6f0000u, 0x3d520000u, 0x3d590000u, 0x3d660000u, 0x3f070000u, 0x3f090000u, 0x3f0c0000u}; +alignas(256) constexpr std::array MsaaCode{ + 0x34020085u, 0xe0301000u, 0x80000201u, 0xe0301004u, 0x80000301u, 0xe0301008u, 0x80000401u, + 0xbf8c3f70u, 0xf0000130u, 0x00010802u, 0xbf8c3f70u, 0xe0701010u, 0x80000801u, 0xbf810000u +}; +alignas(256) constexpr auto PlaneCode = [] { + auto code = MsaaCode; + code[8] = 0xf0000108u; + return code; +}(); + +std::array TextureDescriptor(std::uint32_t format) { + const auto address = reinterpret_cast(texture.data()); + return {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (format << 20u) | 0xc0000000u, 0xc000u, 0x90000facu, 0u, 0u, 0u, 0u}; +} + +void FillTexture(const std::array& descriptor) { + const auto layout = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(descriptor)); + Require(layout.guestBytes <= texture.size(), "MSAA-address texture exceeds its storage"); + texture.fill(std::byte{0xee}); + const auto& mip = layout.mips.at(0); + for (std::uint32_t y = 0; y < 4; ++y) { + for (std::uint32_t x = 0; x < 4; ++x) { + const auto value = 0x12340000u + y * 64u + x; + std::memcpy(texture.data() + mip.tiledOffset + y * mip.pitchBytes + x * 4u, &value, sizeof(value)); + } + } +} + +std::array Load(AgcDriver::VulkanDevice& device, const std::array& image, std::span code, std::uint32_t wave) { + buffer.fill(0xdeadbeefu); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + buffer[lane * Words] = Coordinates[lane % 4u]; + buffer[lane * Words + 1u] = Coordinates[(lane / 4u) % 4u]; + buffer[lane * Words + 2u] = Samples[lane / 16u]; + } + const auto address = reinterpret_cast(buffer.data()); + std::vector data{static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(buffer.size() * 4u), 0x31016facu}; + data.insert(data.end(), image.begin(), image.end()); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {wave, 0, data, compute, std::nullopt, std::nullopt, memory}, device.Target(), {0, 0, 0, 128} + }; + request.useCache = false; + const auto compiled = ShaderRecompiler::Recompile(request); + device.Dispatch(compiled, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + std::array result{}; + for (std::uint32_t lane = 0; lane < Threads; ++lane) result[lane] = buffer[lane * Words + 4u]; + return result; +} + +void Run(AgcDriver::VulkanDevice& device) { + for (const auto format : {20u, 22u, 56u, 130u}) { + const auto descriptor = TextureDescriptor(format); + FillTexture(descriptor); + for (const auto wave : {32u, 64u}) { + const auto plain = Load(device, descriptor, PlaneCode, wave); + const auto msaa = Load(device, descriptor, MsaaCode, wave); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(msaa[lane] == plain[lane], "MSAA-address load differs from the measured 2D control at lane " + std::to_string(lane)); + const auto x = Coordinates[lane % 4u]; + const auto y = Coordinates[(lane / 4u) % 4u]; + if (x == 4u || y == 4u) Require(msaa[lane] == 0u, "MSAA-address out-of-range load did not read zero"); + else if (format == 20u || format == 22u) Require(msaa[lane] == 0x12340000u + y * 64u + x, "MSAA-address load lost the oracle texel marker"); + else { + const auto index = ((lane / 4u) % 4u) * 3u + lane % 4u; + const auto expected = std::bit_cast((format == 56u ? UnormExpected : SrgbExpected)[index]); + const auto actual = std::bit_cast(msaa[lane]); + if (format == 130u) { + const auto encoded = [](float linear) { return std::lround((linear <= 0.0031308f ? linear * 12.92f : 1.055f * std::pow(linear, 1.0f / 2.4f) - 0.055f) * 255.0f); }; + Require(std::isfinite(actual) && encoded(actual) == encoded(expected), "MSAA-address sRGB load changed the oracle's encoded texel"); + } else Require(std::abs(actual - expected) < 0.0001f, "MSAA-address normalized load differs from the Radeon oracle"); + } + } + } + } +} +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageOneDimensionalCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageOneDimensionalCompare.cpp new file mode 100644 index 0000000000..b119ee834f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageOneDimensionalCompare.cpp @@ -0,0 +1,365 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Layers = 3; +constexpr std::uint32_t Format16UNorm = 7; +constexpr std::uint32_t Type1D = 8; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Type1DArray = 12; +constexpr std::uint32_t ReferenceWord = 6; +constexpr std::uint32_t Result = 8; +constexpr std::uint32_t Step1D = 4000; +constexpr std::uint32_t StepArray = 1200; +constexpr std::uint32_t Step2D = 200; +constexpr std::uint32_t Row2D = 2; +constexpr std::uint32_t CompareLess = 1; + +alignas(256) std::array Buffer{}; +alignas(256) std::array Line{}; +alignas(4096) std::array ArrayTexels{}; +alignas(4096) std::array PlaneTexels{}; + +alignas(256) constexpr std::array Code1D_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0xf0bc0100, 0x00610c14, 0xbf8c3f70, 0xe0701020, + 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1D_ImplicitLod{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0xf0a00100, 0x00610c14, 0xbf8c3f70, 0xe0701020, + 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1D_ExplicitLod{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0280, 0xf0b00100, 0x00610c14, 0xbf8c3f70, + 0xe0701020, 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1D_Lz_Offset{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e2802ff, 0x00000f00, 0x7e2a0308, 0x7e2c0302, 0xf0fc0100, 0x00610c14, + 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1DArray_Lz_Offset{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e2802ff, 0x00000f00, 0x7e2a0308, 0x7e2c0302, 0x7e2e0303, 0xf0fc0120, 0x00610c14, + 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1DArray_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0bc0120, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array Code1DArray_ImplicitLod{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0a00120, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array Code2D_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0bc0108, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array Code2D_ImplicitLod{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0a00108, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array Code1D_Gather_C_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0xf13c0100, 0x00610c14, 0xbf8c3f70, 0xe0701020, + 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1DArray_Gather_C_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf13c0120, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +std::uint32_t TexelOf(std::uint32_t tid) { + return (tid * 7u + 3u) % Size; +} + +bool ReferenceBelow(std::uint32_t tid) { + return (tid & 1u) == 0u; +} + +std::uint32_t Bits(float value) { + return std::bit_cast(value); +} + +void FillInput1D() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const float texel = static_cast((x + 1u) * Step1D) / 65535.0f; + const float half = static_cast(Step1D / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +void FillInput1DArray() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const auto layer = (tid / 7u) % Layers; + const float texel = static_cast((layer * Size + x + 1u) * StepArray) / 65535.0f; + const float half = static_cast(StepArray / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + 1u] = Bits(static_cast(layer)); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +void FillInput2D() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const float texel = static_cast((Row2D * Size + x + 1u) * Step2D) / 65535.0f; + const float half = static_cast(Step2D / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + 1u] = Bits((static_cast(Row2D) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +void FillInputArrayFirstLayer() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const float texel = static_cast((0u * Size + x + 1u) * StepArray) / 65535.0f; + const float half = static_cast(StepArray / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +std::array LineDescriptor() { + const auto address = static_cast(reinterpret_cast(Line.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + (Size - 1u) >> 2u, + 0xfacu | (Type1D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::array ArrayDescriptor() { + const auto address = static_cast(reinterpret_cast(ArrayTexels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + (Size - 1u) >> 2u, + 0xfacu | (Type1DArray << 28u), + Layers - 1u, 0u, 0u, 0u, + }; +} + +std::array PlaneDescriptor() { + const auto address = static_cast(reinterpret_cast(PlaneTexels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +void FillLine() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(LineDescriptor())); + Require(geometry.guestBytes <= Line.size(), "1D texture does not fit texel storage"); + std::fill(Line.begin(), Line.end(), 0xeeu); + for (std::uint32_t x = 0; x < Size; ++x) { + const auto value = static_cast((x + 1u) * Step1D); + auto* texel = &Line[geometry.GuestLayerOffset(0) + geometry.mips.at(0).tiledOffset + x * 2u]; + texel[0] = static_cast(value); + texel[1] = static_cast(value >> 8u); + } +} + +void FillArray() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(ArrayDescriptor())); + Require(geometry.guestBytes <= ArrayTexels.size(), "1D array texture does not fit texel storage"); + std::fill(ArrayTexels.begin(), ArrayTexels.end(), 0xeeu); + for (std::uint32_t layer = 0; layer < Layers; ++layer) { + for (std::uint32_t x = 0; x < Size; ++x) { + const auto value = static_cast((layer * Size + x + 1u) * StepArray); + auto* texel = &ArrayTexels[geometry.GuestLayerOffset(layer) + geometry.mips.at(0).tiledOffset + x * 2u]; + texel[0] = static_cast(value); + texel[1] = static_cast(value >> 8u); + } + } +} + +void FillPlane() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(PlaneDescriptor())); + Require(geometry.guestBytes <= PlaneTexels.size(), "2D texture does not fit texel storage"); + std::fill(PlaneTexels.begin(), PlaneTexels.end(), 0xeeu); + for (std::uint32_t y = 0; y < Size; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + const auto value = static_cast((y * Size + x + 1u) * Step2D); + auto* texel = &PlaneTexels[geometry.GuestLayerOffset(0) + geometry.mips.at(0).tiledOffset + static_cast(y) * geometry.mips.at(0).pitchBytes + x * 2u]; + texel[0] = static_cast(value); + texel[1] = static_cast(value >> 8u); + } + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array SamplerDescriptor() { + return {CompareLess << 12u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::array& textureDesc) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto sampler = SamplerDescriptor(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(textureDesc.begin(), textureDesc.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const std::string& op) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float value = std::bit_cast(Buffer[tid * Words + Result]); + const float expected = ReferenceBelow(tid) ? 1.0f : 0.0f; + Require(value == expected, op + ": thread " + std::to_string(tid) + " is " + std::to_string(value) + ", expected " + std::to_string(expected)); + } +} + +void ExpectGatherReject(AgcDriver::VulkanDevice& device, std::span code, const std::array& textureDesc) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(sizeof(Buffer))); + const auto sampler = SamplerDescriptor(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(textureDesc.begin(), textureDesc.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + try { + (void)ShaderRecompiler::Recompile(request); + Require(false, "remapped 1D depth gather was accepted unexpectedly"); + } catch (const std::exception& error) { + Require(std::string(error.what()).find("has an unsupported 1D gather variant") != std::string::npos, "unexpected error for 1D depth gather: " + std::string(error.what())); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillLine(); + FillArray(); + FillPlane(); + + FillInput1D(); + Run(*device, Code1D_Lz, LineDescriptor()); + Check("1D image_sample_c_lz"); + + FillInput1D(); + Run(*device, Code1D_ImplicitLod, LineDescriptor()); + Check("1D image_sample_c"); + + FillInput1D(); + Run(*device, Code1D_ExplicitLod, LineDescriptor()); + Check("1D image_sample_c_l"); + + FillInput1D(); + Run(*device, Code1D_Lz_Offset, LineDescriptor()); + Check("1D image_sample_c_lz_o"); + + FillInput1DArray(); + Run(*device, Code1DArray_Lz, ArrayDescriptor()); + Check("1D array image_sample_c_lz"); + + FillInput1DArray(); + Run(*device, Code1DArray_Lz_Offset, ArrayDescriptor()); + Check("1D array image_sample_c_lz_o"); + + FillInput1DArray(); + Run(*device, Code1DArray_ImplicitLod, ArrayDescriptor()); + Check("1D array image_sample_c"); + + FillInputArrayFirstLayer(); + Run(*device, Code1D_Lz, ArrayDescriptor()); + Check("1D image_sample_c_lz on 1D array first layer"); + + FillInputArrayFirstLayer(); + Run(*device, Code1D_ImplicitLod, ArrayDescriptor()); + Check("1D image_sample_c on 1D array first layer"); + + FillInput2D(); + Run(*device, Code2D_Lz, PlaneDescriptor()); + Check("2D image_sample_c_lz"); + + FillInput2D(); + Run(*device, Code2D_ImplicitLod, PlaneDescriptor()); + Check("2D image_sample_c"); + + ExpectGatherReject(*device, Code1D_Gather_C_Lz, LineDescriptor()); + ExpectGatherReject(*device, Code1DArray_Gather_C_Lz, ArrayDescriptor()); + + std::puts("1D, 1D-array, and 2D comparison tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePackedHorizontalGather.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePackedHorizontalGather.cpp index 336a066bf8..9621ea1f12 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePackedHorizontalGather.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePackedHorizontalGather.cpp @@ -16,6 +16,12 @@ namespace { +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; @@ -156,7 +162,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t dmask) { const auto expected = Expected(tid, dmask); for (std::uint32_t component = 0; component < static_cast(std::popcount(dmask)); ++component) { const std::uint32_t actual = Output[tid * 4u + component]; - Require(actual == expected[component], std::string("packed horizontal gather: dmask ") + std::to_string(dmask) + " thread " + std::to_string(tid) + " register " + std::to_string(component) + " is 0x" + std::to_string(actual) + ", expected 0x" + std::to_string(expected[component])); + Require(actual == expected[component], std::string("packed horizontal gather: dmask ") + std::to_string(dmask) + " thread " + std::to_string(tid) + " register " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(expected[component])); } } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnCube.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnCube.cpp new file mode 100644 index 0000000000..1dedf777c9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnCube.cpp @@ -0,0 +1,151 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t Size = 8; +constexpr std::uint32_t Faces = 6; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t TypeCube = 11; +constexpr std::uint32_t Result = 8; +alignas(256) std::array Buffer{}; +alignas(256) std::array Cube{}; + +alignas(256) constexpr std::array Code{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xbf8c3f70, 0xf09c0108, 0x00610c02, 0xf0900108, 0x00610d02, + 0xf0800108, 0x00610e02, 0xf0001108, 0x00010f05, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, + 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, +}; + +struct Lane { + std::uint32_t x; + std::uint32_t y; + std::uint32_t loadX; + std::uint32_t loadY; +}; + +Lane LaneOf(std::uint32_t tid) { + return {tid % Size, (tid * 3u) % Size, (tid * 5u) % Size, (tid * 7u + 1u) % Size}; +} + +float TexelValue(std::uint32_t face, std::uint32_t x, std::uint32_t y) { + return static_cast(face * 1000u + y * 16u + x); +} + +std::array CubeDescriptor() { + const auto address = static_cast(reinterpret_cast(Cube.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32Float << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | (TypeCube << 28u), + Faces - 1u, + 0u, + 0u, + 0u, + }; +} + +void FillCube() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(CubeDescriptor())); + Require(geometry.guestBytes <= Cube.size(), "image plane address on a cube: the texture does not fit the texel storage"); + Require(geometry.layers == Faces, "image plane address on a cube: the cube does not have six faces"); + const auto& mip = geometry.mips.at(0); + std::fill(Cube.begin(), Cube.end(), 0u); + for (std::uint32_t face = 0; face < Faces; ++face) { + for (std::uint32_t y = 0; y < Size; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + const float value = TexelValue(face, x, y); + std::memcpy(&Cube[geometry.GuestLayerOffset(face) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u], &value, 4u); + } + } + } +} + +void FillInput() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto lane = LaneOf(tid); + const float u = 1.0f + (static_cast(lane.x) + 0.5f) / static_cast(Size); + const float v = 1.0f + (static_cast(lane.y) + 0.5f) / static_cast(Size); + const std::array words{std::bit_cast(u), std::bit_cast(v), 0u, lane.loadX, lane.loadY}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void Run(AgcDriver::VulkanDevice& device) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto texture = CubeDescriptor(); + const std::array sampler{0u, 0xfffu << 12u, 0u, 0u}; + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(Code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(Code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); +} + +void Check() { + constexpr std::array names{"image_sample_lz", "image_sample_l", "image_sample", "image_load"}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto lane = LaneOf(tid); + const std::array expected{TexelValue(0u, lane.x, lane.y), TexelValue(0u, lane.x, lane.y), TexelValue(0u, lane.x, lane.y), TexelValue(0u, lane.loadX, lane.loadY)}; + for (std::uint32_t op = 0; op < expected.size(); ++op) { + const float value = std::bit_cast(Buffer[tid * Words + Result + op]); + Require(value == expected[op], std::string("image plane address on a cube: ") + names[op] + " 2d on a cube texture, thread " + std::to_string(tid) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[op])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillCube(); + FillInput(); + Run(*device); + Check(); + std::puts("image plane address on cube 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/ImagePlaneAddressOnLine.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp index 4614998ce7..6e3186841c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp @@ -22,6 +22,7 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Words = 32; constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Mips = 5; constexpr std::uint32_t Format8888UNorm = 56; constexpr std::uint32_t Type1D = 8; constexpr std::uint32_t Type2D = 9; @@ -30,6 +31,7 @@ constexpr std::uint32_t LoadResult = 12; alignas(256) std::array Buffer{}; alignas(256) std::array Line{}; alignas(256) std::array Plane{}; +alignas(256) std::array MippedLine{}; alignas(256) constexpr std::array Code{ 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, @@ -39,11 +41,56 @@ alignas(256) constexpr std::array Code{ 0x80001201, 0xe070103c, 0x80001301, 0xbf810000, }; -alignas(256) constexpr std::array ExplicitLodCode{ +alignas(256) constexpr std::array ExplicitLodCode{ 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, - 0x80000501, 0xbf8c3f70, 0xf0900f08, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xbf810000, + 0x80000501, 0xbf8c3f70, 0xf0900f08, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, + 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, +}; + +struct SamplerCase { + const char* name; + std::uint32_t clamp; + std::uint32_t filter; + std::uint32_t mipFilter; +}; + +constexpr std::uint32_t ClampWrap = 0; +constexpr std::uint32_t ClampEdge = 2; +constexpr std::uint32_t ClampBorder = 6; +constexpr std::uint32_t FilterPoint = 0; +constexpr std::uint32_t FilterLinear = 1; +constexpr std::uint32_t MipPoint = 1; +constexpr std::uint32_t MipLinear = 2; + +constexpr std::array SamplerCases{{ + {"point filtering, point mips, clamp to edge", ClampEdge, FilterPoint, MipPoint}, + {"point filtering, point mips, wrap", ClampWrap, FilterPoint, MipPoint}, + {"linear filtering, linear mips, clamp to edge", ClampEdge, FilterLinear, MipLinear}, + {"linear filtering, linear mips, wrap", ClampWrap, FilterLinear, MipLinear}, + {"linear filtering, linear mips, clamp to border", ClampBorder, FilterLinear, MipLinear}, +}}; + +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}; @@ -81,6 +128,20 @@ std::array LineDescriptor() { }; } +std::array MippedLineDescriptor() { + const auto address = static_cast(reinterpret_cast(MippedLine.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + (Size - 1u) >> 2u, + 0xfacu | ((Mips - 1u) << 16u) | (Type1D << 28u), + 0u, + (Mips - 1u) << 4u, + 0u, + 0u, + }; +} + std::array PlaneDescriptor() { const auto address = static_cast(reinterpret_cast(Plane.data())); return { @@ -111,6 +172,25 @@ void FillTexture(std::span storage, const std::array MipTexel(std::uint32_t level, std::uint32_t x) { + return {10u + 40u * level, x * 12u + 3u, 200u - 30u * level, 255u - 7u * x - 11u * level}; +} + +void FillMippedLine() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(MippedLineDescriptor())); + Require(geometry.guestBytes <= MippedLine.size(), "image plane address on a line: the mipped texture does not fit the texel storage"); + Require(geometry.mips.size() == Mips, "image plane address on a line: the mipped texture has " + std::to_string(geometry.mips.size()) + " mips"); + std::fill(MippedLine.begin(), MippedLine.end(), 0xeeu); + for (std::uint32_t level = 0; level < Mips; ++level) { + const auto& mip = geometry.mips[level]; + for (std::uint32_t x = 0; x < (Size >> level); ++x) { + const auto value = MipTexel(level, x); + auto* texel = &MippedLine[geometry.GuestLayerOffset(0) + mip.tiledOffset + x * 4u]; + for (std::uint32_t component = 0; component < 4u; ++component) texel[component] = static_cast(value[component]); + } + } +} + 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}; @@ -120,10 +200,13 @@ std::array SamplerDescriptor() { return {0u, 0xfffu << 12u, 1u << 26u, 0u}; } -void Run(AgcDriver::VulkanDevice& device, std::span code, const std::array& texture) { +std::array SamplerDescriptor(const SamplerCase& sampler) { + return {sampler.clamp | (sampler.clamp << 3u) | (sampler.clamp << 6u), 0xfffu << 12u, (sampler.filter << 20u) | (sampler.filter << 22u) | (sampler.mipFilter << 26u), 0u}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::array& texture, const std::array& sampler = SamplerDescriptor()) { std::vector userData(16, 0u); const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); - const auto sampler = SamplerDescriptor(); std::copy(buffer.begin(), buffer.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); @@ -164,16 +247,108 @@ void CheckPlane() { } } -void RequireRefused(AgcDriver::VulkanDevice& device) { - std::string refusal; - try { - Run(device, ExplicitLodCode, LineDescriptor()); - } catch (const std::exception& error) { - refusal = error.what(); - } - Require(refusal.find("incompatible with the static runtime image interface") != std::string::npos, "image_sample_l 2d on a 1D texture was not refused: " + refusal); +float LaneU(std::uint32_t tid) { + return LodU[(tid + tid / 8u) % LodU.size()]; } +float LaneLod(std::uint32_t tid) { + return Lods[tid / 4u]; +} + +void FillLodInput() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::array words{Bits(LaneU(tid)), Bits(LineV[(tid / 2u) % LineV.size()]), Bits(LaneLod(tid)), 0u}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } +} + +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]); + return results; +} + +std::uint32_t PointLevel(float lod) { + return static_cast(std::clamp(std::lround(lod), 0L, static_cast(Mips - 1u))); +} + +std::uint32_t PointTexel(float u, std::uint32_t level, std::uint32_t clamp) { + const auto width = static_cast(Size >> level); + const auto texel = static_cast(std::floor(u * static_cast(width))); + if (clamp == ClampWrap) return static_cast((texel % width + width) % width); + return static_cast(std::clamp(texel, 0, width - 1)); +} + +void CheckExplicitLod(AgcDriver::VulkanDevice& device) { + FillMippedLine(); + for (const auto& sampler : SamplerCases) { + const auto samplerWords = SamplerDescriptor(sampler); + FillLodInput(); + Run(device, LineLodCode, MippedLineDescriptor(), samplerWords); + const auto reference = SampleResults(); + FillLodInput(); + Run(device, ExplicitLodCode, MippedLineDescriptor(), samplerWords); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto u = LaneU(tid); + const auto lod = LaneLod(tid); + const auto what = std::string("image_sample_l 2d on a 1D texture with ") + sampler.name + ", u " + std::to_string(u) + ", v " + std::to_string(LineV[(tid / 2u) % LineV.size()]) + ", LOD " + std::to_string(lod); + 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)); + } + if (sampler.filter == FilterPoint) { + const auto level = PointLevel(lod); + Expect(tid, SampleResult, MipTexel(level, PointTexel(u, level, sampler.clamp)), what); + } else if (sampler.clamp != ClampBorder) { + const auto mark = std::bit_cast(Buffer[tid * Words + SampleResult]) * 255.0f; + const auto expected = 10.0f + 40.0f * std::clamp(lod, 0.0f, static_cast(Mips - 1u)); + Require(std::fabs(mark - expected) <= 1.0f, what + ": thread " + std::to_string(tid) + " blends the levels to " + std::to_string(mark) + ", expected " + std::to_string(expected)); + } + } + } +} + +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() { @@ -188,7 +363,8 @@ int main() { FillInput(false); Run(*device, Code, PlaneDescriptor()); CheckPlane(); - RequireRefused(*device); + 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/ImagePlaneBiasOnSingleLevelVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneBiasOnSingleLevelVolume.cpp new file mode 100644 index 0000000000..29875f7792 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneBiasOnSingleLevelVolume.cpp @@ -0,0 +1,244 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Mips = 1; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type3D = 10; +constexpr float SecondCoordinateScale = 1.0f / static_cast(Size); +alignas(256) std::array Pixels{}; +alignas(256) std::array Volume{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array BiasPixelCode{ + 0x10040010, 0x06060300, 0x10060611, 0xf0940f08, 0x00610401, 0xbf8c3f70, 0xf800180f, 0x07060504, 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}}}; + +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::array VolumeDescriptor() { + const auto address = static_cast(reinterpret_cast(Volume.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | ((Mips - 1u) << 16u) | (Type3D << 28u), + 0u, + (Mips - 1u) << 4u, + 0u, + 0u, + }; +} + +std::array SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void FillTexture() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(VolumeDescriptor())); + Require(geometry.guestBytes <= Volume.size(), "single-level volume does not fit the texel storage"); + Require(geometry.mips.size() == 1u && geometry.imageDepth == 1u, "single-level volume geometry differs"); + std::fill(Volume.begin(), Volume.end(), 0xeeu); + const auto& mip = geometry.mips.front(); + for (std::uint32_t y = 0; y < Size; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + auto* texel = &Volume[mip.tiledOffset + y * mip.pitchBytes + x * 4u]; + texel[0] = static_cast(x * 8u); + texel[1] = static_cast(y * 8u); + texel[2] = 99u; + texel[3] = 255u; + } + } +} + +void Draw(AgcDriver::VulkanDevice& device, std::array texture, std::array sampler, float bias, bool mixedSamplers = false) { + const auto& target = device.Target(); + alignas(256) std::array code{}; + std::copy_n(BiasPixelCode.begin(), 3u, code.begin()); + code[3] = 0x7e020212u; + std::copy(BiasPixelCode.begin() + 3, BiasPixelCode.end(), code.begin() + 4); + if (mixedSamplers) { + code[6] = 0xf0940f08u; + code[7] = 0x00a10801u; + code[8] = 0xbf8c3f70u; + code[9] = 0x06081104u; + code[10] = 0xf800180fu; + code[11] = 0x07060504u; + code[12] = 0xbf810000u; + } + const auto pixelCode = std::span(code).first(mixedSamplers ? 13u : 10u); + Pixels.fill(std::byte{0}); + + 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, {}}, + {64, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = false; + 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(24, 0u); + std::copy(texture.begin(), texture.end(), pixelUserData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), pixelUserData.begin() + 12); + pixelUserData[16] = std::bit_cast(1.0f / static_cast(Size)); + pixelUserData[17] = std::bit_cast(SecondCoordinateScale); + pixelUserData[18] = std::bit_cast(bias); + auto secondSampler = sampler; + secondSampler[0] &= ~(1u << 20u); + std::copy(secondSampler.begin(), secondSampler.end(), pixelUserData.begin() + 20); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(std::span(pixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {64, 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(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(bool degamma = false) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + std::array expected{(x % Size) * 8u, ((x + y + 1u) % Size) * 8u, 99u, 255u}; + if (degamma) { + for (std::uint32_t component = 0; component < 3u; ++component) { + const float encoded = static_cast(expected[component]) / 255.0f; + const float linear = encoded <= 0.04045f ? encoded / 12.92f : std::pow((encoded + 0.055f) / 1.055f, 2.4f); + expected[component] = static_cast(std::lround(linear * 255.0f)); + } + } + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto value = std::to_integer(Pixels[(y * Width + x) * 4u + component]); + Require(std::abs(static_cast(value) - static_cast(expected[component])) <= (degamma ? 1 : 0), "single-level volume bias: pixel " + std::to_string(x) + "," + std::to_string(y) + " component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + } + } + } +} + +void RequireRefused(AgcDriver::VulkanDevice& device, std::array texture, std::array sampler, const char* expected) { + std::string refusal; + try { + Draw(device, texture, sampler, 0.0f); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find(expected) != std::string::npos, "single-level volume refusal differs: " + refusal); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexture(); + for (const float bias : {-16.0f, -0.5f, 0.0f, 0.5f, 16.0f}) { + Draw(*device, VolumeDescriptor(), SamplerDescriptor(), bias); + Check(); + } + for (const std::uint32_t reduction : {1u, 2u}) { + auto sampler = SamplerDescriptor(); + sampler[0] |= reduction << 29u; + Draw(*device, VolumeDescriptor(), sampler, 8.0f); + Check(); + } + auto integerTexture = VolumeDescriptor(); + integerTexture[1] = (integerTexture[1] & ~(0x1ffu << 20u)) | (60u << 20u); + auto forcedSampler = SamplerDescriptor(); + forcedSampler[0] |= 1u << 20u; + for (const float bias : {-16.0f, 0.0f, 16.0f}) { + Draw(*device, integerTexture, forcedSampler, bias); + Check(true); + } + std::string mixedRefusal; + try { + Draw(*device, integerTexture, forcedSampler, 0.0f, true); + } catch (const std::exception& error) { + mixedRefusal = error.what(); + } + Require(mixedRefusal.find("mixed sampler states") != std::string::npos, "mixed FORCE_DEGAMMA samplers were not refused: " + mixedRefusal); + auto texture = VolumeDescriptor(); + texture[4] = 1u; + RequireRefused(*device, texture, SamplerDescriptor(), "one slice and mip level zero only"); + texture = VolumeDescriptor(); + texture[3] |= 1u << 16u; + texture[5] |= 1u << 4u; + RequireRefused(*device, texture, SamplerDescriptor(), "one slice and mip level zero only"); + auto sampler = SamplerDescriptor(); + sampler[2] |= (1u << 20u) | (1u << 22u); + RequireRefused(*device, VolumeDescriptor(), sampler, "normalized point sampler"); + sampler = SamplerDescriptor(); + sampler[0] |= 1u << 15u; + RequireRefused(*device, VolumeDescriptor(), sampler, "normalized point sampler"); + std::puts("image plane bias on single-level volume tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneCompareOnLine.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneCompareOnLine.cpp new file mode 100644 index 0000000000..56717c07df --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneCompareOnLine.cpp @@ -0,0 +1,155 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Format16UNorm = 7; +constexpr std::uint32_t Type1D = 8; +constexpr std::uint32_t ReferenceWord = 6; +constexpr std::uint32_t Result = 8; +constexpr std::uint32_t Step = 4000; +constexpr std::uint32_t CompareLess = 1; +alignas(256) std::array Buffer{}; +alignas(256) std::array Line{}; + +alignas(256) constexpr std::array LevelZeroCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0bc0108, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array ImplicitLodCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0a00108, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +constexpr std::array LineV{0.0f, 0.5f, 1.0f, 1.5f, -0.25f, 3.0f, 0.25f, 0.75f}; + +std::uint32_t TexelOf(std::uint32_t tid) { + return (tid * 7u + 3u) % Size; +} + +bool ReferenceBelow(std::uint32_t tid) { + return (tid & 1u) == 0u; +} + +std::uint32_t Bits(float value) { + return std::bit_cast(value); +} + +void FillInput() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const float texel = static_cast((x + 1u) * Step) / 65535.0f; + const float half = static_cast(Step / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + 1u] = Bits(LineV[tid % LineV.size()]); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +std::array LineDescriptor() { + const auto address = static_cast(reinterpret_cast(Line.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + (Size - 1u) >> 2u, + 0xfacu | (Type1D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void FillLine() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(LineDescriptor())); + Require(geometry.guestBytes <= Line.size(), "image plane compare on a line: the texture does not fit the texel storage"); + std::fill(Line.begin(), Line.end(), 0xeeu); + for (std::uint32_t x = 0; x < Size; ++x) { + const auto value = static_cast((x + 1u) * Step); + auto* texel = &Line[geometry.GuestLayerOffset(0) + geometry.mips.at(0).tiledOffset + x * 2u]; + texel[0] = static_cast(value); + texel[1] = static_cast(value >> 8u); + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array SamplerDescriptor() { + return {CompareLess << 12u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto texture = LineDescriptor(); + const auto sampler = SamplerDescriptor(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const std::string& op) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float value = std::bit_cast(Buffer[tid * Words + Result]); + const float expected = ReferenceBelow(tid) ? 1.0f : 0.0f; + Require(value == expected, op + " 2d on a 1D texture with v " + std::to_string(LineV[tid % LineV.size()]) + ": thread " + std::to_string(tid) + " is " + std::to_string(value) + ", expected " + std::to_string(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillLine(); + FillInput(); + Run(*device, LevelZeroCode); + Check("image_sample_c_lz"); + FillInput(); + Run(*device, ImplicitLodCode); + Check("image_sample_c"); + std::puts("image plane compare on line 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/ImageRepackedFloat.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageRepackedFloat.cpp new file mode 100644 index 0000000000..a42baf7fca --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageRepackedFloat.cpp @@ -0,0 +1,219 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#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 Words = 32; +constexpr std::uint32_t MipSize = 16; +constexpr std::uint32_t MipLevels = 5; +constexpr std::uint32_t CubeSize = 8; +constexpr std::uint32_t Faces = 6; +constexpr std::uint32_t RepackedFormat = 43; +constexpr std::uint32_t NativeFormat = 36; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t TypeCube = 11; +constexpr std::uint32_t Result = 8; +constexpr std::array PointMipSampler{0u, 0xfffu << 12u, 1u << 26u, 0u}; +constexpr std::array BilinearSampler{0u, 0xfffu << 12u, (1u << 20u) | (1u << 22u), 0u}; +alignas(256) std::array Buffer{}; +alignas(256) std::array Texels{}; + +alignas(256) constexpr std::array Code{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xbf8c3f70, 0xf09c0108, 0x00610c02, 0xf0900108, 0x00610d02, + 0xf0800108, 0x00610e02, 0xf0001108, 0x00010f05, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, + 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, +}; + +std::uint32_t GuestWord(std::uint32_t seed) { + seed ^= seed >> 16u; + seed *= 0x7feb352du; + seed ^= seed >> 15u; + seed *= 0x846ca68bu; + return seed ^ (seed >> 16u); +} + +float SmallFloat(std::uint32_t bits, std::uint32_t mantissaBits) { + const auto exponent = bits >> mantissaBits; + const auto mantissa = bits & ((1u << mantissaBits) - 1u); + const auto scale = static_cast(1u << mantissaBits); + if (exponent == 31u) return mantissa == 0u ? std::numeric_limits::infinity() : std::numeric_limits::quiet_NaN(); + if (exponent == 0u) return std::ldexp(static_cast(mantissa) / scale, -14); + return std::ldexp(1.0f + static_cast(mantissa) / scale, static_cast(exponent) - 15); +} + +float GuestComponent(std::uint32_t word, std::uint32_t component) { + if (component == 0u) return SmallFloat(word & 0x3ffu, 5u); + if (component == 1u) return SmallFloat((word >> 10u) & 0x7ffu, 6u); + return SmallFloat(word >> 21u, 6u); +} + +bool Same(float actual, float expected) { + return (std::isnan(actual) && std::isnan(expected)) || std::bit_cast(actual) == std::bit_cast(expected); +} + +std::uint32_t Swizzle(std::uint32_t component) { + return (4u + component) | (5u << 3u) | (6u << 6u) | (7u << 9u); +} + +std::array Descriptor(std::uint32_t format, std::uint32_t size, std::uint32_t type, std::uint32_t swizzle, std::uint32_t lastLevel, std::uint32_t lastArray) { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((size - 1u) & 3u) << 30u), + ((size - 1u) >> 2u) | ((size - 1u) << 14u), + swizzle | (lastLevel << 16u) | (type << 28u), + lastArray, + lastLevel << 4u, + 0u, + 0u, + }; +} + +template +void Fill(const std::array& descriptor, TWord word) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(descriptor)); + Require(geometry.guestBytes <= Texels.size(), "image repacked float: the texture does not fit the texel storage"); + std::fill(Texels.begin(), Texels.end(), 0u); + for (std::uint32_t layer = 0; layer < geometry.layers; ++layer) { + for (std::uint32_t level = 0; level < geometry.mips.size(); ++level) { + const auto& mip = geometry.mips[level]; + for (std::uint32_t y = 0; y < mip.height; ++y) { + for (std::uint32_t x = 0; x < mip.width; ++x) { + const std::uint32_t value = word(layer, level, x, y); + std::memcpy(&Texels[geometry.GuestLayerOffset(layer) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u], &value, 4u); + } + } + } + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void Run(AgcDriver::VulkanDevice& device, const std::array& texture, const std::array& sampler) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(Code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(Code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); +} + +void Input(std::uint32_t size, float base, std::uint32_t lod) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = tid % size; + const auto y = (tid * 3u) % size; + const float u = base + (static_cast(x) + 0.5f) / static_cast(size); + const float v = base + (static_cast(y) + 0.5f) / static_cast(size); + const std::array words{std::bit_cast(u), std::bit_cast(v), std::bit_cast(static_cast(lod)), x, y}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } +} + +std::uint32_t MipWord(std::uint32_t, std::uint32_t level, std::uint32_t x, std::uint32_t y) { + return GuestWord(level * 4096u + y * 64u + x); +} + +void CheckMipLevels(AgcDriver::VulkanDevice& device) { + for (std::uint32_t lod = 1; lod < MipLevels; ++lod) { + const auto size = MipSize >> lod; + for (std::uint32_t component = 0; component < 3u; ++component) { + const auto descriptor = Descriptor(RepackedFormat, MipSize, Type2D, Swizzle(component), MipLevels - 1u, 0u); + Fill(descriptor, MipWord); + Input(size, 0.0f, lod); + Run(device, descriptor, PointMipSampler); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = tid % size; + const auto y = (tid * 3u) % size; + const float sampled = std::bit_cast(Buffer[tid * Words + Result + 1u]); + const float expected = GuestComponent(MipWord(0u, lod, x, y), component); + Require(Same(sampled, expected), "image repacked float: image_sample_l at LOD " + std::to_string(lod) + ", thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(sampled) + ", expected " + std::to_string(expected)); + const auto loaded = std::bit_cast(Buffer[tid * Words + Result + 3u]); + Require(Same(loaded, GuestComponent(MipWord(0u, 0u, x, y), component)), "image repacked float: image_load of mip 0, thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(loaded)); + } + } + } +} + +std::uint32_t CubeWord(std::uint32_t face, std::uint32_t, std::uint32_t x, std::uint32_t y) { + const auto exponent = 12u + (face + x + y) % 6u; + return ((exponent << 5u) | ((x * 7u + y) & 31u)) | (((exponent + 1u) << 6u | ((y * 5u + face) & 63u)) << 10u) | (((exponent + 2u) << 6u | ((x * 3u + face * 11u) & 63u)) << 21u); +} + +void CheckCubeFace(AgcDriver::VulkanDevice& device) { + for (std::uint32_t component = 0; component < 3u; ++component) { + const auto repacked = Descriptor(RepackedFormat, CubeSize, TypeCube, Swizzle(component), 0u, Faces - 1u); + Fill(repacked, CubeWord); + Input(CubeSize, 1.0f, 0u); + for (std::uint32_t tid = 0; tid < Threads; ++tid) Buffer[tid * Words] = std::bit_cast(std::bit_cast(Buffer[tid * Words]) + 0.3f / CubeSize); + const auto input = Buffer; + Run(device, repacked, BilinearSampler); + const auto repackedResults = Buffer; + constexpr std::array nativeChannel{2u, 1u, 0u}; + const auto native = Descriptor(NativeFormat, CubeSize, TypeCube, Swizzle(nativeChannel[component]), 0u, Faces - 1u); + Fill(native, [](std::uint32_t face, std::uint32_t level, std::uint32_t x, std::uint32_t y) { return AgcDriver::Graphics::RepackTextureWord(CubeWord(face, level, x, y)); }); + Buffer = input; + Run(device, native, BilinearSampler); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t op = 0; op < 4u; ++op) { + const float actual = std::bit_cast(repackedResults[tid * Words + Result + op]); + const float expected = std::bit_cast(Buffer[tid * Words + Result + op]); + Require(Same(actual, expected), "image repacked float: cube face sample " + std::to_string(op) + ", thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(actual) + ", the native B10G11R11 cube gives " + std::to_string(expected)); + } + const float face = std::bit_cast(repackedResults[tid * Words + Result + 3u]); + Require(Same(face, GuestComponent(CubeWord(0u, 0u, tid % CubeSize, (tid * 3u) % CubeSize), component)), "image repacked float: image_load of cube face 0, thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(face)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + CheckMipLevels(*device); + CheckCubeFace(*device); + std::puts("image repacked float 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/ImageSampleForceDegamma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp index bab71fa850..331882d14a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp @@ -25,6 +25,8 @@ constexpr std::uint32_t Size = 4; constexpr std::uint32_t Format8888UNorm = 56; constexpr std::uint32_t Format8888Srgb = 130; constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Format8888SNorm = 57; +constexpr std::uint32_t FormatBc4UNorm = 175; constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t FilterPoint = 0; constexpr std::uint32_t FilterBilinear = 1; @@ -173,7 +175,9 @@ int main() { Require(unormPlain[0] != unormPoint[0] || unormPlain[4] != unormPoint[4], "8_8_8_8_UNORM without FORCE_DEGAMMA sampled the decoded values"); FillInput(false); RequireSame(Sample(*device, Format8888UNorm, FilterBilinear, ForceDegamma), forcedBilinear, "8_8_8_8_UNORM, bilinear", "through FORCE_DEGAMMA", "as 8_8_8_8_SRGB"); - Reject(*device, Format32Float, "guest format 22, which is not sRGB and has no sRGB view, is sampled through a sampler that forces sRGB decoding"); + RequireSame(Sample(*device, Format32Float, FilterPoint, ForceDegamma), Sample(*device, Format32Float, FilterPoint, 0u), "32_FLOAT, point"); + RequireSame(Sample(*device, FormatBc4UNorm, FilterPoint, ForceDegamma), Sample(*device, FormatBc4UNorm, FilterPoint, 0u), "BC4_UNORM, point"); + Reject(*device, Format8888SNorm, "guest format 57, which is not sRGB and has no sRGB view, is sampled through a sampler that forces sRGB decoding"); std::puts("image sample force degamma tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/ImageSampleUnnormalized.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp index 88a589493d..4bf2fffbce 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp @@ -335,8 +335,11 @@ int main() { ExpectFailure(*device, Offsets, chain, Samplers[0].words, "single-level", "a 4-level view read with texel offsets"); ExpectFailure(*device, Gradient, chain, Samplers[0].words, "single-level", "a 4-level view read by image_sample_d"); ExpectFailure(*device, LevelZero, chain, Samplers[2].words, "single-level", "a 4-level view through an S# with a minimum LOD and a LOD bias"); - ExpectFailure(*device, Plain, single, {0x00008092u, 0x00fff000u, 0x05100000u, 0u}, "different minification", "unequal minification and magnification filters"); - ExpectFailure(*device, Plain, single, {0x00008090u, 0x00fff000u, 0x05500000u, 0u}, "clamp mode 0", "wrap on X"); + const auto magFilterReference = Run(*device, Plain, single, {0x00008092u, 0x00fff000u, 0x05500000u, 0u}, coordinates); + const auto unequalFilterSamples = Run(*device, Plain, single, {0x00008092u, 0x00fff000u, 0x05100000u, 0u}, coordinates); + Require(unequalFilterSamples == magFilterReference, "unnormalized sampler with unequal filters does not sample with its magnification filter"); + const auto clampModeZeroSamples = Run(*device, Plain, single, {0x00008090u, 0x00fff000u, 0x05500000u, 0u}, coordinates); + Require(clampModeZeroSamples == magFilterReference, "unnormalized sampler with clamp mode 0 does not sample with clamp mode 2"); const auto constantView = [&](std::uint32_t type, std::uint32_t format, std::uint32_t swizzle) { auto descriptor = TextureDescriptor(SingleLevel.data(), 0u, 0u); descriptor[1] = (descriptor[1] & ~(0x1ffu << 20u)) | (format << 20u); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleVccSampler.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleVccSampler.cpp new file mode 100644 index 0000000000..d5f05cabc7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleVccSampler.cpp @@ -0,0 +1,166 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Size = 8; +constexpr std::uint32_t Format32SInt = 21; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t SwizzleX111 = 0x24cu; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::uint32_t SampleX = 3; +constexpr std::uint32_t SampleY = 5; +constexpr float Tolerance = 1.0f / 64.0f; +constexpr std::array LinearSampler{0x00000092u, 0x00fff000u, 0x00500000u, 0u}; + +alignas(256) std::array Output{}; +alignas(256) std::array Integers{}; +alignas(256) std::array Colors{}; + +alignas(256) constexpr std::array Code{ + 0x34060085u, 0x7e2802ffu, 0x3ef00000u, 0x7e2a02ffu, 0x3f300000u, 0xf09c0f08u, 0x00610a14u, 0xf09c0f08u, + 0x00640e14u, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, 0xe0781010u, 0x80000e03u, 0xbf810000u, 0xbf810000u, +}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t PitchBytes(std::uint32_t format, std::size_t storage) { + const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format, Size, Size, 1u); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= storage, "image sample VCC sampler: a texture does not fit its storage"); + return mips[0].pitchBytes; +} + +std::int32_t Integer(std::uint32_t x, std::uint32_t y) { + return -static_cast(100u * x + 7u * y + 1u); +} + +void Fill() { + const auto integerPitch = PitchBytes(Format32SInt, Integers.size()); + const auto colorPitch = PitchBytes(Format8888UNorm, Colors.size()); + Integers.fill(0x5au); + Colors.fill(0x5au); + for (std::uint32_t y = 0; y < Size; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + const auto bits = static_cast(Integer(x, y)); + auto* integer = &Integers[static_cast(y) * integerPitch + x * 4u]; + for (std::uint32_t byte = 0; byte < 4u; ++byte) integer[byte] = static_cast(bits >> (byte * 8u)); + auto* color = &Colors[static_cast(y) * colorPitch + x * 4u]; + color[0] = x > SampleX ? 255u : 0u; + color[1] = 0u; + color[2] = 0u; + color[3] = 255u; + } + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(const void* texels, std::uint32_t format, std::uint32_t swizzle) { + const auto address = static_cast(reinterpret_cast(texels)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + swizzle | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, bool border) { + Output.fill(0xdeadbeefu); + std::vector userData(24, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + const auto integers = TextureDescriptor(Integers.data(), Format32SInt, SwizzleX111); + const auto colors = TextureDescriptor(Colors.data(), Format8888UNorm, IdentitySwizzle); + std::copy(output.begin(), output.end(), userData.begin()); + std::copy(integers.begin(), integers.end(), userData.begin() + 4); + auto sampler = LinearSampler; + if (border) { sampler[0] = 0x1b6u; sampler[3] = 2u << 30u; } + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + std::copy(colors.begin(), colors.end(), userData.begin() + 16); + alignas(256) std::array words{}; + words[0] = 0xbee8030cu; + words[1] = 0xbee9030du; + words[2] = 0xbeea030eu; + words[3] = 0xbeeb030fu; + std::copy(Code.begin(), Code.end(), words.begin() + 4); + for (const auto index : {10u, 12u}) words[index] = (words[index] & ~(31u << 21u)) | (26u << 21u); + if (border) { words[6] = 0xbf800000u; words[8] = 0xbf800000u; } + const std::span code(words); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {true, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, bool border) { + Fill(); + Run(device, waveSize, border); + const std::array integer{border ? 1u : static_cast(Integer(SampleX, SampleY)), 1u, 1u, 1u}; + const std::array color = border ? std::array{1.0f, 1.0f, 1.0f, 1.0f} : std::array{0.25f, 0.0f, 0.0f, 1.0f}; + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto actual = Output[lane * 8u + component]; + Require(actual == integer[component], "32_SINT image_sample_lz through a bilinear sampler shared with 8_8_8_8_UNORM: lane " + std::to_string(lane) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(integer[component])); + } + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto bits = Output[lane * 8u + 4u + component]; + const auto actual = std::bit_cast(bits); + Require(std::fabs(actual - color[component]) <= Tolerance, "8_8_8_8_UNORM image_sample_lz through a bilinear sampler shared with 32_SINT: lane " + std::to_string(lane) + " component " + std::to_string(component) + " is " + Hex(bits) + ", expected " + std::to_string(color[component])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const auto waveSize : {32u, 64u}) { + Check(*device, waveSize, false); + Check(*device, waveSize, true); + } + std::puts("image sample VCC sampler 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/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/ImageSrgbSampleCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp new file mode 100644 index 0000000000..4ea7cc7525 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp @@ -0,0 +1,352 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Size = 4; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Srgb8 = 128; +constexpr std::uint32_t Srgb8_8 = 129; +constexpr std::uint32_t SwizzleX = 0xfacu; +constexpr std::uint32_t SwizzleYAsX = 0xfadu; +constexpr std::uint32_t FilterPoint = 0; +constexpr std::uint32_t FilterBilinear = 1; +constexpr std::array Codes{0, 255, 128, 64}; +constexpr std::size_t BlockBytes = 2u << 20u; +constexpr std::uint32_t StoreSize = 1024; + +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(4096) std::array Texels{}; + +alignas(256) constexpr std::array SampleCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xbf8c3f70, + 0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) std::array StoreCode{ + 0x7e280d00, 0x102828ff, 0x3d000000, 0x062a28ff, 0x41000000, 0x7e3c0300, 0x7e3e0280, 0xf0201108, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array GatherCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf11c0108, 0x00820401, 0xbf8c3f70, + 0x34160084, 0xe0781000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + + +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, "image sRGB sample copy: cannot allocate the guest block"); + Require(AgcDriver::GuestMemory::Watched(reinterpret_cast(block), BlockBytes), "image sRGB sample copy: the guest block is not write-watched"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(block), BlockBytes); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(std::uint32_t format, std::uint32_t swizzle, const void* data = Texels.data(), std::uint32_t extent = Size) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((extent - 1u) & 3u) << 30u), + ((extent - 1u) >> 2u) | ((extent - 1u) << 14u), + swizzle | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +std::array SamplerDescriptor(std::uint32_t filter) { + return {0x92u, 0x00fff000u, (filter << 20u) | (filter << 22u) | (1u << 24u), 0u}; +} + +double Decode(std::uint32_t code) { + const double encoded = code / 255.0; + return encoded <= 0.04045 ? encoded / 12.92 : std::pow((encoded + 0.055) / 1.055, 2.4); +} + +std::uint32_t Code(std::uint32_t format, bool second, std::uint32_t x) { + const std::uint32_t code = Codes.at(std::min(x, Size - 1u)); + return second && format == Srgb8_8 ? 255u - code : code; +} + +void FillTexels(std::uint32_t format) { + const std::uint32_t bytes = format == Srgb8 ? 1u : 2u; + const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format, Size, Size, 1); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Texels.size(), "image sRGB sample copy: the texture does not fit the texel storage"); + Texels.fill(0); + for (std::uint32_t y = 0; y < Size; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + auto* texel = &Texels[mips[0].tiledOffset + static_cast(y) * mips[0].pitchBytes + x * bytes]; + texel[0] = static_cast(Code(format, false, x)); + if (bytes == 2u) texel[1] = static_cast(Code(format, true, x)); + } + } +} + +void FillInput(bool gather, bool between) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t base = tid % (Size - 1u); + float u = (static_cast(tid % Size) + 0.5f) / static_cast(Size); + if (gather) u = (static_cast(base) + 0.6f) / static_cast(Size); + else if (between) u = static_cast(base + 1u) / static_cast(Size); + Input[tid * 3u + 0u] = u; + Input[tid * 3u + 1u] = 0.6f / static_cast(Size); + Input[tid * 3u + 2u] = 0.0f; + } +} + +bool Close(double actual, double expected) { + return std::fabs(actual - expected) <= 0.01 + std::fabs(expected) * 0.04; +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, float scale) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(Srgb8, SwizzleX, texels, StoreSize); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::uint32_t bits = 0; + std::memcpy(&bits, &scale, sizeof(bits)); + StoreCode[2] = bits; + const std::array memory{{{reinterpret_cast(StoreCode.data()), std::as_bytes(std::span(StoreCode))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(StoreCode.data()), std::span(StoreCode), 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(StoreCode.data())); + device.WaitIdle(); +} + +void CheckStored(AgcDriver::VulkanDevice& device, std::uint8_t* texels, float scale, const std::string& what) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * 3u + 0u] = (static_cast(tid) + 0.5f) / static_cast(StoreSize); + Input[tid * 3u + 1u] = 0.5f / static_cast(StoreSize); + Input[tid * 3u + 2u] = 0.0f; + } + std::vector userData(28, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); + const auto texture = TextureDescriptor(Srgb8, SwizzleX, texels, StoreSize); + const auto sampler = SamplerDescriptor(FilterPoint); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::array memory{{{reinterpret_cast(SampleCode.data()), std::as_bytes(std::span(SampleCode))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(SampleCode.data()), std::span(SampleCode), 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); + Output.fill(-1.0f); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(SampleCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const double code = std::round(std::min(1.0, static_cast(tid) * scale) * 255.0); + const double expected = Decode(static_cast(code)); + Require(Close(Output[tid], expected), what + ": thread " + std::to_string(tid) + " sampled " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected)); + } +} + +void CheckStorageSource(AgcDriver::VulkanDevice& device) { + GuestBlock block; + auto* texels = block.Data(); + const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(Srgb8, SwizzleX, texels, StoreSize))); + Require(surface.guestBytes >= (1u << 20u) && surface.guestBytes <= BlockBytes, "8_SRGB storage source: the surface does not fit the guest block or is below the storage image floor"); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), surface.guestBytes, nullptr, "test"); + AgcDriver::Graphics::FlushCachedTextures(device.Device()); + Store(device, texels, 1.0f / 32.0f); + CheckStored(device, texels, 1.0f / 32.0f, "8_SRGB sample of a pending storage image"); + const auto before = AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(texels), surface.guestBytes); + Require(before != nullptr, "8_SRGB storage source: no storage image holds the surface's results"); + const auto version = before->Version(); + Store(device, texels, 1.0f / 8.0f); + const auto after = AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(texels), surface.guestBytes); + Require(after != nullptr && after->Version() > version, "8_SRGB storage source: the second store did not advance the storage image"); + CheckStored(device, texels, 1.0f / 8.0f, "8_SRGB sample of a pending storage image after a second store"); +} + +std::string ForcedMode() { + const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); + return forced == nullptr ? std::string{} : std::string{forced}; +} + +void VKAPI_PTR NativeSrgbFormatProperties(VkPhysicalDevice, VkFormat, VkFormatProperties* properties) { + *properties = {}; + properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT | VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT; +} + +void VKAPI_PTR NoSrgbFormatProperties(VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) { + *properties = {}; + if (format == VK_FORMAT_R8_SRGB || format == VK_FORMAT_R8G8_SRGB) return; + properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT | VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT; +} + +std::uint32_t SrgbBits() { + return AgcDriver::Graphics::SrgbDecodeFormats(NoSrgbFormatProperties, VK_NULL_HANDLE); +} + +void CheckSelection() { + const auto mode = ForcedMode(); + Require(SrgbBits() != 0u, "a device that cannot sample R8_SRGB or R8G8_SRGB selected no sRGB decode formats"); + const auto decode = AgcDriver::Graphics::SrgbDecodeFormats(NativeSrgbFormatProperties, VK_NULL_HANDLE); + const auto copy = AgcDriver::Graphics::SrgbCopyFormats(NativeSrgbFormatProperties, VK_NULL_HANDLE, decode); + const std::uint32_t expected = mode == "copy" ? SrgbBits() : 0u; + Require(copy == expected, "a device that samples R8_SRGB and R8G8_SRGB selected sRGB copy formats " + std::to_string(copy) + " with APS5_SRGB_SHADER_DECODE='" + mode + "', expected " + std::to_string(expected)); +} + +void Check(AgcDriver::VulkanDevice& device, std::uint32_t format, bool second, std::uint32_t filter, bool gather, const std::string& what) { + FillTexels(format); + const bool between = filter == FilterBilinear; + FillInput(gather, between); + const auto& code = gather ? GatherCode : SampleCode; + std::vector userData(28, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); + const auto texture = TextureDescriptor(format, second ? SwizzleYAsX : SwizzleX); + const auto sampler = SamplerDescriptor(filter); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(code))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), std::span(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); + Output.fill(-1.0f); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t base = tid % (Size - 1u); + if (gather) { + const double low = Decode(Code(format, second, base)); + const double high = Decode(Code(format, second, base + 1u)); + const std::array expected{low, high, high, low}; + for (std::uint32_t component = 0; component < 4u; ++component) { + const double actual = Output[tid * 4u + component]; + Require(Close(actual, expected[component]), what + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " gathered " + std::to_string(actual) + ", expected " + std::to_string(expected[component])); + } + continue; + } + double expected = Decode(Code(format, second, tid % Size)); + if (between) expected = (Decode(Code(format, second, base)) + Decode(Code(format, second, base + 1u))) / 2.0; + const double actual = Output[tid]; + Require(Close(actual, expected), what + ": thread " + std::to_string(tid) + " sampled " + std::to_string(actual) + ", expected " + std::to_string(expected)); + } +} + +} + +int main() { + try { + CheckSelection(); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (ForcedMode() == "copy") Require((device->Target().srgbDecodeFormats & SrgbBits()) == 0u, "APS5_SRGB_SHADER_DECODE=copy: the device cannot blit R8 or RG8 to RGBA8, so 8 and 8_8 sRGB samples have no copy"); + for (const auto format : {Srgb8, Srgb8_8}) { + const std::string name = format == Srgb8 ? "8_SRGB" : "8_8_SRGB"; + Check(*device, format, false, FilterPoint, false, name + " point sample"); + Check(*device, format, false, FilterBilinear, false, name + " bilinear sample, decoded before filtering"); + Check(*device, format, false, FilterPoint, true, name + " gather4"); + if (format == Srgb8_8) { + Check(*device, format, true, FilterPoint, false, name + " point sample of the second channel"); + Check(*device, format, true, FilterBilinear, false, name + " bilinear sample of the second channel, decoded before filtering"); + Check(*device, format, true, FilterPoint, true, name + " gather4 of the second channel"); + } + } + CheckStorageSource(*device); + std::puts("image sRGB sample copy tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFaceOnCube.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFaceOnCube.cpp new file mode 100644 index 0000000000..148b6e45c7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFaceOnCube.cpp @@ -0,0 +1,195 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Size = 8; +constexpr std::uint32_t Threads = Size * Size; +constexpr std::uint32_t Faces = 12; +constexpr std::uint32_t TypeCube = 11; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::size_t TexelBytes = 32768; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) std::array Input{}; + +alignas(256) constexpr std::array StoreCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x363c0087, 0x2c3e0083, 0xf0201f08, 0x0001141e, 0xbf810000, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "image store face on a cube: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +struct Format { + const char* name; + std::uint32_t format; +}; + +constexpr std::array Formats{{ + {"32_UINT", 20u}, + {"32_FLOAT", 22u}, +}}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Value(std::uint32_t tid) { + return 0x3f000000u + tid * 0x1357u; +} + +std::array CubeDescriptor(const void* data, std::uint32_t format, std::uint32_t baseArray) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + IdentitySwizzle | (TypeCube << 28u), + (Faces - 1u) | (baseArray << 16u), + 0u, + 0u, + 0u, + }; +} + +AgcDriver::Graphics::SurfaceGeometry Geometry(const void* data, std::uint32_t format) { + return AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(CubeDescriptor(data, format, 0u))); +} + +std::uint64_t TexelOffset(const AgcDriver::Graphics::SurfaceGeometry& geometry, std::uint32_t face, std::uint32_t x, std::uint32_t y) { + const auto& mip = geometry.mips.at(0); + return geometry.GuestLayerOffset(face) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u; +} + +std::vector Initial() { + std::vector bytes(TexelBytes); + for (std::size_t index = 0; index < bytes.size(); ++index) bytes[index] = static_cast(0x80u + 37u * index + 11u * (index >> 4u)); + return bytes; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Format& format, std::uint32_t baseArray) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) Input[tid * 4u] = Value(tid); + const auto initial = Initial(); + std::copy(initial.begin(), initial.end(), texels); + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto texture = CubeDescriptor(texels, format.format, baseArray); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::span code(StoreCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), TexelBytes, nullptr, "test"); + device.WaitIdle(); +} + +void CheckTexels(const std::uint8_t* texels, const Format& format, std::uint32_t baseArray) { + const auto geometry = Geometry(texels, format.format); + Require(geometry.layers == Faces && geometry.guestBytes <= TexelBytes, "image store face on a cube: the cube has " + std::to_string(geometry.layers) + " layers in " + std::to_string(geometry.guestBytes) + " bytes, expected " + std::to_string(Faces) + " in at most " + std::to_string(TexelBytes)); + const auto initial = Initial(); + std::vector written(TexelBytes, false); + const auto label = std::string("image_store 2d through a ") + format.name + " cube T# with BASE_ARRAY " + std::to_string(baseArray); + for (std::uint32_t y = 0; y < Size; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + const auto offset = TexelOffset(geometry, baseArray, x, y); + std::uint32_t actual = 0; + std::memcpy(&actual, texels + offset, 4u); + const auto wanted = Value(y * Size + x); + Require(actual == wanted, label + ": texel (" + std::to_string(x) + ", " + std::to_string(y) + ") of face " + std::to_string(baseArray) + " is " + Hex(actual) + ", expected " + Hex(wanted)); + std::fill(written.begin() + static_cast(offset), written.begin() + static_cast(offset + 4u), true); + } + } + for (std::size_t index = 0; index < TexelBytes; ++index) { + Require(written[index] || texels[index] == initial[index], label + ": byte " + std::to_string(index) + " outside face " + std::to_string(baseArray) + " changed"); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + auto* texels = block.Data(); + for (const auto& format : Formats) { + for (const auto baseArray : {0u, 6u}) { + Dispatch(*device, texels, format, baseArray); + CheckTexels(texels, format, baseArray); + } + } + std::puts("image store face on cube 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/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/ImageStoreUnormRounding.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreUnormRounding.cpp new file mode 100644 index 0000000000..bb6c6f25cf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreUnormRounding.cpp @@ -0,0 +1,264 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Width = 32; +constexpr std::uint32_t Height = 8; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::uint32_t TwoChannelSwizzle = 0x22cu; +constexpr std::size_t TexelBytes = 8192; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) std::array Input{}; + +alignas(256) constexpr std::array StoreCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x0001141e, + 0x7e3e0281, 0xf0201508, 0x0001141e, 0xbf810000, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "image store unorm rounding: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + + +struct Format { + const char* name; + std::uint32_t format; + std::uint32_t components; + std::uint32_t bits; + bool snorm; +}; + +constexpr std::array Formats{{ + {"8_UNORM", 1u, 1u, 8u, false}, + {"16_16_UNORM", 23u, 2u, 16u, false}, + {"8_8_8_8_UNORM", 56u, 4u, 8u, false}, + {"8_8_8_8_SNORM", 57u, 4u, 8u, true}, + {"16_16_16_16_UNORM", 65u, 4u, 16u, false}, +}}; + +struct Write { + std::uint32_t row; + std::uint32_t dmask; +}; + +constexpr std::array StoreWrites{{{0u, 0xfu}, {1u, 0x5u}}}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +float Scale(const Format& format) { + return static_cast((1u << (format.snorm ? format.bits - 1u : format.bits)) - 1u); +} + +float Value(const Format& format, std::uint32_t tid, std::uint32_t index) { + constexpr std::array Edges{0.0f, -0.0f, 1.0f, 0.5f, -0.25f, 1.5f, -1.5f, 1e-30f, -1e-30f, 0.999f, 0.0019607844f, 0.9980392f, 0.33333334f, -0.6666667f, 2e-5f, 0.75f}; + const auto slot = tid * 4u + index; + if (slot < Edges.size()) return Edges[slot]; + if (slot == Edges.size()) return std::numeric_limits::quiet_NaN(); + if (slot == Edges.size() + 1u) return std::numeric_limits::infinity(); + if (slot == Edges.size() + 2u) return -std::numeric_limits::infinity(); + const auto scale = Scale(format); + const auto level = static_cast((slot * 37u) % static_cast(scale)); + auto value = (level + 0.5f) / scale; + for (std::uint32_t step = 0; step < slot % 5u; ++step) value = std::nextafter(value, slot % 2u == 0u ? 2.0f : -2.0f); + return format.snorm && slot % 3u == 0u ? -value : value; +} + +std::uint32_t Level(const Format& format, float value) { + const auto lower = format.snorm ? -1.0f : 0.0f; + const auto clamped = std::isnan(value) ? 0.0f : std::clamp(value, lower, 1.0f); + const auto product = static_cast(clamped) * static_cast(Scale(format)); + const auto level = static_cast(std::nearbyint(product)); + return static_cast(level) & ((1u << format.bits) - 1u); +} + +std::uint32_t TexelBytesOf(const Format& format) { + return format.components * format.bits / 8u; +} + +std::uint32_t SwizzleOf(const Format& format) { + return format.components == 2u ? TwoChannelSwizzle : IdentitySwizzle; +} + +AgcDriver::Graphics::TileMipLayout Mip(const Format& format) { + return AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format.format, Width, Height, 1u).at(0); +} + +std::uint64_t TexelOffset(const Format& format, std::uint32_t x, std::uint32_t y) { + const auto mip = Mip(format); + return mip.tiledOffset + static_cast(y) * mip.pitchBytes + static_cast(x) * TexelBytesOf(format); +} + +std::vector Initial() { + std::vector bytes(TexelBytes); + for (std::size_t index = 0; index < bytes.size(); ++index) { + bytes[index] = static_cast(0x80u + 37u * index + 11u * (index >> 4u)); + } + return bytes; +} + +std::uint32_t StoredComponent(const Format& format, std::uint32_t tid, std::uint32_t dmask, std::uint32_t component) { + if (((dmask >> component) & 1u) == 0u) return 0u; + std::uint32_t index = 0; + for (std::uint32_t lower = 0; lower < component; ++lower) index += (dmask >> lower) & 1u; + return Level(format, Value(format, tid, index)); +} + +std::vector Expected(const Format& format) { + auto bytes = Initial(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (const auto& write : StoreWrites) { + auto* texel = bytes.data() + TexelOffset(format, tid, write.row); + for (std::uint32_t component = 0; component < format.components; ++component) { + const auto value = StoredComponent(format, tid, write.dmask, component); + std::memcpy(texel + component * format.bits / 8u, &value, format.bits / 8u); + } + } + } + return bytes; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(const void* data, std::uint32_t format, std::uint32_t swizzle) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + swizzle | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span code, std::uint32_t format, std::uint32_t swizzle, const std::array& buffer) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels, format, swizzle); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), TexelBytes, nullptr, "test"); + device.WaitIdle(); +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Format& format) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t index = 0; index < 4u; ++index) Input[tid * 4u + index] = std::bit_cast(Value(format, tid, index)); + } + const auto initial = Initial(); + std::copy(initial.begin(), initial.end(), texels); + Dispatch(device, texels, StoreCode, format.format, SwizzleOf(format), BufferDescriptor(Input.data(), static_cast(Input.size() * 4u))); +} + +void CheckTexels(const std::uint8_t* texels, const Format& format) { + const auto expected = Expected(format); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = TexelOffset(format, x, y); + for (std::uint32_t component = 0; component < format.components; ++component) { + std::uint32_t actual = 0; + std::uint32_t wanted = 0; + std::memcpy(&actual, texels + offset + component * format.bits / 8u, format.bits / 8u); + std::memcpy(&wanted, expected.data() + offset + component * format.bits / 8u, format.bits / 8u); + Require(actual == wanted, std::string("image_store of ") + format.name + ": texel (" + std::to_string(x) + ", " + std::to_string(y) + ") component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(wanted) + " (level nearest to the exact product)"); + } + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + auto* texels = block.Data(); + for (const auto& format : Formats) { + Store(*device, texels, format); + CheckTexels(texels, format); + } + std::puts("image store unorm rounding tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreVolumeThroughArray.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreVolumeThroughArray.cpp new file mode 100644 index 0000000000..b82fa4ba07 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreVolumeThroughArray.cpp @@ -0,0 +1,173 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Size = 4; +constexpr std::uint32_t Depth = 2; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t Type2DArray = 13; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::size_t TexelBytes = 16384; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) std::array Input{}; + +alignas(256) constexpr std::array StoreCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0xf0201110, 0x00011714, 0x7e2c0282, 0xf0201110, + 0x00011714, 0xbf810000, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "image store volume through array: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Value(std::uint32_t tid) { + return (tid + 1u) * 0x9e3779b1u; +} + +std::vector Initial() { + std::vector bytes(TexelBytes); + for (std::size_t index = 0; index < bytes.size(); ++index) bytes[index] = static_cast(0x80u + 37u * index + 11u * (index >> 4u)); + return bytes; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array ArrayDescriptor(const void* data) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + IdentitySwizzle | (Type2DArray << 28u), + Depth - 1u, + 0u, + 0u, + 0u, + }; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::size_t bytes) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::array words{tid % Size, (tid / Size) % Size, tid / (Size * Size), Value(tid)}; + std::copy(words.begin(), words.end(), Input.begin() + tid * 4u); + } + const auto initial = Initial(); + std::copy(initial.begin(), initial.end(), texels); + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto texture = ArrayDescriptor(texels); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::span code(StoreCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), bytes, nullptr, "test"); + device.WaitIdle(); +} + +void Check(const std::uint8_t* texels) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(ArrayDescriptor(texels))); + const auto& mip = geometry.mips.at(0); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = tid % Size; + const auto y = (tid / Size) % Size; + const auto z = tid / (Size * Size); + std::uint32_t actual = 0; + std::memcpy(&actual, texels + geometry.GuestLayerOffset(z) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u, 4u); + Require(actual == Value(tid), "image_store 3d through a 2D-array T#: texel (" + std::to_string(x) + ", " + std::to_string(y) + ", " + std::to_string(z) + ") is " + Hex(actual) + ", expected " + Hex(Value(tid))); + } + const auto initial = Initial(); + Require(std::equal(texels + geometry.guestBytes, texels + TexelBytes, initial.begin() + static_cast(geometry.guestBytes)), "image_store 3d through a 2D-array T#: a slice past the array changed bytes after it"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + auto* texels = block.Data(); + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(ArrayDescriptor(texels))); + Require(geometry.guestBytes <= TexelBytes, "image store volume through array: the array does not fit the texel storage"); + Dispatch(*device, texels, geometry.guestBytes); + Check(texels); + std::puts("image store volume through array tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp new file mode 100644 index 0000000000..d353e82b93 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -0,0 +1,1525 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "ImageTableAbi.hpp" +#include "VulkanTestDevice.hpp" +#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 +#include +#else +#include +#include +#endif + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 4; +constexpr std::uint32_t Stride = 48; +constexpr std::uint32_t Records = 12; +constexpr std::uint32_t Lanes = 64; +constexpr std::uint32_t MaxGroups = 8; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t Format32SInt = 21; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Format11_11_10UInt = 34; +constexpr std::uint32_t Format8_8Srgb = 129; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Type3D = 10; +constexpr std::uint32_t DepthSwizzleMode = 0x18; + +alignas(256) std::array FloatTexels{}; +alignas(256) std::array OtherFloatTexels{}; +alignas(256) std::array UintTexels{}; +alignas(256) std::array SintTexels{}; +alignas(4096) std::array DepthStorage{}; +alignas(256) std::array Table{}; +alignas(256) std::array Keys{}; +alignas(256) std::array Output{}; +alignas(256) std::array Srt{}; +constexpr std::uint32_t PaletteOffset = 0x40; +constexpr std::uint32_t PaletteBytes = PaletteOffset + 256u * 32u; +std::vector Root(0x100000u); + +alignas(256) constexpr std::array TableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0x8f108202u, 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 GuardedTableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, + 0xf4040a00u, 0xfa000070u, 0xbf8cc07fu, 0xf4000a94u, 0xfa000000u, 0xbf8cc07fu, + 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, + 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, + 0x3e000000u, 0xf09c8f08u, 0x01090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, + 0x34101084u, 0x7e08022au, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array TableDirectSamplerCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf4280902u, 0x24000010u, 0x360200bfu, 0x7e020d01u, + 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c8f08u, 0x01090401u, 0x7e120202u, + 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array TableSamplerCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080900u, 0xfa000040u, + 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf4280802u, 0x24000000u, 0x360200bfu, 0x7e020d01u, + 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c8f08u, 0x01090401u, 0x7e120202u, + 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array TableCutCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0x8f108202u, 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 TableWave32Code{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0x7e200500u, 0x90108510u, + 0x8f118202u, 0x81101110u, 0x8f108210u, 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, + 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c8f08u, + 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, + 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array ReadTableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0x8f108202u, 0xf4200444u, + 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0xf0008f08u, 0x00090401u, + 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array ReadTableCutCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0x8f108202u, 0xf4200444u, + 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7da400ffu, 0x00001000u, + 0xf0008f08u, 0x00090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, + 0xbf810000u, +}; + +alignas(256) constexpr std::array ReadTableD16Code{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0x8f108202u, 0xf4200444u, + 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0xf000a388u, 0x80090401u, + 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0701000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array ReadTableWave32Code{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0x7e200500u, 0x90108510u, + 0x8f118202u, 0x81101110u, 0x8f108210u, 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, + 0x360200bfu, 0x7e040280u, 0xf0008f08u, 0x00090401u, 0x7e120202u, 0x34101287u, 0x4a101100u, 0x34101084u, + 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array ReadTableCounterLoopCode{ + 0xf4080100u, 0xfa000000u, 0xf4080700u, 0xfa000020u, 0x360200bfu, 0x7e040280u, 0xbea80380u, 0x9312b028u, + 0xf42c0802u, 0x24000000u, 0xf0008f08u, 0x00090401u, 0x7e120228u, 0x34101286u, 0x4a101100u, 0x34101084u, + 0xe0781000u, 0x80070408u, 0x80288128u, 0xbf0a8328u, 0xbf85fff2u, 0xbf810000u, +}; + +alignas(256) constexpr std::array ReadTableSetBitLoopCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4200a44u, 0xfa000000u, + 0x360200bfu, 0x7e040280u, 0xbea80380u, 0xbeaa1329u, 0x9312b02au, 0xf42c0802u, 0x24000000u, 0xf0008f08u, + 0x00090401u, 0x7e120228u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0x80288128u, + 0xbea91b2au, 0xbf078029u, 0xbf85fff0u, 0xbf810000u, +}; + +alignas(256) constexpr std::array ReadTable256Code{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0x8f108202u, 0xf4200444u, + 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0xf0000f08u, 0x00080401u, + 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array ReadDirectCode{ + 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, 0xf4080900u, 0xfa000040u, 0x360200bfu, 0x7e040280u, + 0xf0008f08u, 0x00090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, + 0xbf810000u, +}; + +alignas(256) constexpr std::array ReadDirectD16Code{ + 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, 0xf4080900u, 0xfa000040u, 0x360200bfu, 0x7e040280u, + 0xf000a388u, 0x80090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0701000u, 0x80070408u, + 0xbf810000u, +}; + +alignas(256) constexpr std::array ReadDirect256Code{ + 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, 0xf40c0900u, 0xfa000040u, 0x360200bfu, 0x7e040280u, + 0xf0000f08u, 0x00090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, + 0xbf810000u, +}; + +alignas(256) constexpr std::array StoreTableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, + 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, + 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c0f08u, 0x01090401u, 0x7e120202u, + 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array LoadedPointerCode{ + 0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0xf4040500u, 0xfa000030u, + 0x8f108202u, 0xf4200444u, 0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0xf40c090au, 0x241fff00u, + 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c0f08u, + 0x01090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array PointerStorageWriteCode{ + 0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0xf4040500u, 0xfa000030u, + 0x8f108202u, 0xf4200444u, 0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0xf40c090au, 0x241fff00u, + 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c0f08u, + 0x01090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, + 0xf40c0b00u, 0xfa000040u, 0x7e3c0300u, 0x7e3e0280u, 0xf0201f08u, 0x000b041eu, 0xbf810000u, +}; + +alignas(256) constexpr std::array PointerStorageIsolationCode{ + 0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0xf4040500u, 0xfa000030u, + 0x8f108202u, 0xf4200444u, 0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0xf40c090au, 0x241fff00u, + 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, + 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xf4000600u, 0xfa000060u, 0x7da80018u, + 0xf09c0f08u, 0x01090401u, 0xe0781000u, 0x80070408u, 0xbefe04c1u, + 0xf4040500u, 0xfa000038u, 0xf40c090au, 0x241fff00u, 0x7da60018u, + 0xf09c0f08u, 0x01090401u, 0xe0781000u, 0x80070408u, 0xbefe04c1u, + 0xf40c0b00u, 0xfa000040u, 0x7e3c0300u, 0x7e3e0280u, 0xf0201f08u, 0x000b041eu, 0xbf810000u, +}; + +alignas(256) constexpr std::array UnboundedPointerCode{ + 0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x8f108202u, 0xf4200444u, + 0x20000000u, 0x8f128511u, 0xf40c0900u, 0x24000040u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, + 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c0f08u, 0x01090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, + 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array ReadPointerCode{ + 0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x8f108202u, 0xf4200444u, + 0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0xf40c0900u, 0x24000040u, 0x360200bfu, 0x7e040280u, + 0xf0000f08u, 0x00090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, + 0xbf810000u, +}; + +alignas(256) constexpr std::array SplitRecordCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0x8f108202u, 0xf4200444u, + 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0xf4280a02u, 0x24000020u, 0x360200bfu, 0x7e020d01u, + 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c0f08u, 0x01090401u, 0x7e120202u, + 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array IndirectRecordCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf4240b02u, 0x24000008u, 0xf40c0916u, 0xfa000020u, + 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c0f08u, + 0x01090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array PointerChainRecordCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf4240b02u, 0x24000008u, 0xf4040b96u, 0xfa000010u, + 0xf40c0917u, 0xfa000020u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, + 0x3e000000u, 0xf09c0f08u, 0x01090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, + 0x80070408u, 0xbf810000u, +}; + +alignas(256) std::array Indirect{}; +alignas(256) std::array Links{}; + +constexpr std::array PointClamp{0x92u, (4u * 256u) << 12u, 0u, 0u}; +constexpr std::array LinearWrap{0u, (4u * 256u) << 12u, (1u << 20u) | (1u << 22u), 0u}; +constexpr std::array PointWrap{0u, (4u * 256u) << 12u, 1u << 24u, 0u}; + +std::array Image(const void* texels, std::uint32_t format, std::uint32_t type = Type2D, std::uint32_t swizzleMode = 0u) { + const auto address = static_cast(reinterpret_cast(texels)); + return {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), ((Width - 1u) >> 2u) | ((Height - 1u) << 14u) | (1u << 31u), 0xfacu | (swizzleMode << 20u) | (type << 28u)}; +} + +std::array Buffer(const void* base, std::uint32_t bytes) { + const auto address = static_cast(reinterpret_cast(base)); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void SetRecord(std::uint32_t record, const std::array& sampler, const std::array& image) { + auto* words = Table.data() + record * (Stride / 4u); + std::fill(words, words + Stride / 4u, 0u); + std::copy(sampler.begin(), sampler.end(), words); + std::copy(image.begin(), image.end(), words + 4); +} + +void SetWideRecord(std::uint32_t record, const std::array& image) { + auto* words = Table.data() + record * (Stride / 4u); + std::fill(words, words + Stride / 4u, 0u); + std::copy(image.begin(), image.end(), words); +} + +std::uint32_t Half(float value) { + const auto bits = std::bit_cast(value); + return (((bits >> 23u) - 112u) << 10u) | ((bits >> 13u) & 0x3ffu); +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + Require(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + _dup2(_fileno(file), _fileno(stderr)); +#else + saved = dup(fileno(stderr)); + dup2(fileno(file), fileno(stderr)); +#endif + } + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + +struct Outcome { + std::vector words; + std::string log; + std::uint64_t variantId = 0; + std::uint64_t artifactId = 0; + std::uint32_t imageTableCount = 0; + std::uint32_t imageTableFaults = 0; + std::uint32_t poisonedSrtReads = 0; + bool cacheable = false; + std::vector> samplers; + std::vector> samplerTable; + std::vector>> imageTableReadRanges; +}; + +std::vector> SamplerWords(const std::vector& words) { + std::vector> result; + for (std::size_t first = 0; first + 4u <= words.size(); first += 4u) result.push_back({words[first], words[first + 1u], words[first + 2u], words[first + 3u]}); + return result; +} + +Outcome Execute(AgcDriver::VulkanDevice& device, std::span span, const void* root, std::span memory, std::uint32_t groups, std::uint32_t threads, std::uint32_t waveSize) { + const auto rootAddress = reinterpret_cast(root); + const std::array userData{static_cast(rootAddress), static_cast(rootAddress >> 32u)}; + std::fill(Output.begin(), Output.end(), 0xdeadbeefu); + const ShaderRecompiler::ShaderComputeStageInfo compute{{threads, 1, 1}, 0u, {true, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(span.data()), span, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + const auto result = ShaderRecompiler::Recompile(request); + Outcome outcome; + outcome.variantId = result.PipelineVariantId(); + outcome.artifactId = result.variantId; + outcome.imageTableFaults = result.imageTableFaults; + outcome.poisonedSrtReads = result.poisonedSrtReads; + outcome.cacheable = AgcDriver::DriverDetail::CacheableResult(result); + outcome.imageTableReadRanges = result.imageTableReadRanges; + for (const auto& binding : result.bindings) { + 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); + } + StderrCapture capture; + try { + device.Dispatch(result, groups, 1, 1, {}, reinterpret_cast(span.data())); + device.WaitIdle(); + } catch (...) { + static_cast(capture.Finish()); + throw; + } + outcome.log = capture.Finish(); + outcome.words.assign(Output.begin(), Output.begin() + groups * threads * 4u); + return outcome; +} + +template +Outcome Run(AgcDriver::VulkanDevice& device, const std::array& code, std::uint32_t groups, std::uint32_t threads, std::uint32_t waveSize, bool outputIntoTable = false) { + const auto table = Buffer(Table.data(), static_cast(sizeof(Table))); + const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); + const auto output = outputIntoTable ? Buffer(Table.data(), static_cast(sizeof(Table))) : Buffer(Output.data(), static_cast(sizeof(Output))); + std::copy(table.begin(), table.end(), Srt.begin()); + std::copy(keys.begin(), keys.end(), Srt.begin() + 4); + std::copy(output.begin(), output.end(), Srt.begin() + 8); + const std::span span(code); + const std::array memory{{ + {reinterpret_cast(span.data()), std::as_bytes(span)}, + {reinterpret_cast(Srt.data()), std::as_bytes(std::span(Srt))}, + {reinterpret_cast(Table.data()), std::as_bytes(std::span(Table))}, + {reinterpret_cast(Keys.data()), std::as_bytes(std::span(Keys))}, + }}; + return Execute(device, span, Srt.data(), memory, groups, threads, waveSize); +} + +template +Outcome RunPointer(AgcDriver::VulkanDevice& device, const std::array& code, std::uint32_t groups) { + auto* root = Root.data(); + const auto palette = static_cast(reinterpret_cast(root)) + PaletteOffset + 0x100u; + const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); + const auto output = Buffer(Output.data(), static_cast(sizeof(Output))); + std::copy(keys.begin(), keys.end(), root); + std::copy(output.begin(), output.end(), root + 4); + std::copy(PointClamp.begin(), PointClamp.end(), root + 8); + root[12] = static_cast(palette); + root[13] = static_cast(palette >> 32u); + const std::span span(code); + const std::array memory{{ + {reinterpret_cast(span.data()), std::as_bytes(span)}, + {reinterpret_cast(root), std::as_bytes(std::span(root, PaletteBytes / 4u))}, + {reinterpret_cast(Keys.data()), std::as_bytes(std::span(Keys))}, + }}; + return Execute(device, span, root, memory, groups, Lanes, 64); +} + +template +Outcome RunIndirectRecords(AgcDriver::VulkanDevice& device, const std::array& code, std::uint32_t groups) { + const auto table = Buffer(Table.data(), static_cast(sizeof(Table))); + const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); + const auto output = Buffer(Output.data(), static_cast(sizeof(Output))); + std::copy(table.begin(), table.end(), Srt.begin()); + std::copy(keys.begin(), keys.end(), Srt.begin() + 4); + std::copy(output.begin(), output.end(), Srt.begin() + 8); + std::copy(PointClamp.begin(), PointClamp.end(), Srt.begin() + 12); + const std::span span(code); + const std::array memory{{ + {reinterpret_cast(span.data()), std::as_bytes(span)}, + {reinterpret_cast(Srt.data()), std::as_bytes(std::span(Srt))}, + {reinterpret_cast(Table.data()), std::as_bytes(std::span(Table))}, + {reinterpret_cast(Keys.data()), std::as_bytes(std::span(Keys))}, + {reinterpret_cast(Indirect.data()), std::as_bytes(std::span(Indirect))}, + {reinterpret_cast(Links.data()), std::as_bytes(std::span(Links))}, + }}; + return Execute(device, span, Srt.data(), memory, groups, Lanes, 64); +} + +void SetEntry(std::uint32_t entry, const std::array& image) { + auto* words = Root.data() + (PaletteOffset + entry * 32u) / 4u; + std::fill(words, words + 8, 0u); + std::copy(image.begin(), image.end(), words); +} + +template +Outcome RunDirectCode(AgcDriver::VulkanDevice& device, const std::array& code, const std::array& sampler, std::span image) { + std::copy(sampler.begin(), sampler.end(), Srt.begin() + 12); + std::fill(Srt.begin() + 16, Srt.begin() + 24, 0u); + std::copy(image.begin(), image.end(), Srt.begin() + 16); + return Run(device, code, 1, Lanes, 64); +} + +Outcome RunDirect(AgcDriver::VulkanDevice& device, const std::array& sampler, const std::array& image) { + return RunDirectCode(device, DirectCode, sampler, image); +} + +std::vector Group(const Outcome& outcome, std::uint32_t group, std::uint32_t threads = Lanes) { + return std::vector(outcome.words.begin() + group * threads * 4u, outcome.words.begin() + (group + 1u) * threads * 4u); +} + +bool Faulted(const Outcome& outcome) { + return outcome.log.find("image table:") != std::string::npos; +} + +bool Holds(const std::vector>& samplers, const std::array& sampler) { + return std::find(samplers.begin(), samplers.end(), sampler) != samplers.end(); +} + +bool TablePointCopy(const Outcome& outcome, const std::array& sampler, const std::array& point) { + bool found = false; + for (std::size_t entry = 0; entry + ShaderRecompiler::ImageTableAbi::SamplerEntryElements <= outcome.samplerTable.size(); entry += ShaderRecompiler::ImageTableAbi::SamplerEntryElements) { + if (outcome.samplerTable[entry] != sampler) continue; + found = true; + if (outcome.samplerTable[entry + 1u] != point) return false; + } + return found; +} + +void Fill() { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = y * Width + x; + FloatTexels[index] = static_cast(x) + 100.0f * static_cast(y) + 0.25f; + OtherFloatTexels[index] = -static_cast(x) - 0.5f; + UintTexels[index] = 0x1000u + x + 0x100u * y; + SintTexels[index] = -3 * static_cast(x + 1u) - 1000 * static_cast(y); + } + } + for (std::uint32_t index = 0; index < DepthStorage.size(); ++index) DepthStorage[index] = 0x3f000000u + index * 0x1234u; +} + +void RequireRed(const Outcome& outcome, std::uint32_t group, std::uint32_t threads, const std::function& expected, const std::string& what) { + for (std::uint32_t lane = 0; lane < threads; ++lane) { + const auto actual = outcome.words[(group * threads + lane) * 4u]; + Require(actual == expected(lane % Width), what + ": lane " + std::to_string(lane) + " is " + Hex(actual) + ", expected " + Hex(expected(lane % Width))); + } +} + +bool Zeros(const Outcome& outcome) { + return std::all_of(outcome.words.begin(), outcome.words.end(), [](std::uint32_t word) { return word == 0u || word == 0xdeadbeefu; }); +} + +template +void RunWaveKeys(AgcDriver::VulkanDevice& device, const std::array& code, const std::string& what) { + if (device.Target().subgroupSize != 32u) { + std::printf("%s: per-wave key case skipped, subgroup size %u does not hold exactly one wave32\n", what.c_str(), device.Target().subgroupSize); + return; + } + for (std::uint32_t wave = 0; wave < 8; ++wave) Keys[wave] = wave % 3u; + const auto waves = Run(device, code, 2, 128, 32); + Require(!Faulted(waves), what + ": per-wave keys faulted:\n" + waves.log); + for (std::uint32_t group = 0; group < 2; ++group) { + for (std::uint32_t wave = 0; wave < 4; ++wave) { + const auto key = (group * 4u + wave) % 3u; + for (std::uint32_t lane = 0; lane < 32; ++lane) { + const auto thread = wave * 32u + lane; + const auto actual = waves.words[(group * 128u + thread) * 4u]; + const auto x = thread % Width; + const auto expected = key == 0u ? std::bit_cast(FloatTexels[x]) : key == 1u ? UintTexels[x] : static_cast(SintTexels[x]); + Require(actual == expected, what + ": wave " + std::to_string(wave) + " of group " + std::to_string(group) + " read the wrong entry at lane " + std::to_string(lane)); + } + } + } +} + +void SetClassRecords() { + const auto floatImage = Image(FloatTexels.data(), Format32Float); + const auto uintImage = Image(UintTexels.data(), Format32UInt); + const auto sintImage = Image(SintTexels.data(), Format32SInt); + SetRecord(0, PointClamp, floatImage); + SetRecord(1, PointClamp, uintImage); + SetRecord(2, PointClamp, sintImage); + SetRecord(3, LinearWrap, floatImage); + SetRecord(4, LinearWrap, uintImage); + SetRecord(5, LinearWrap, sintImage); + SetRecord(6, {0u, 0u, 0u, 0u}, {0u, 0u, 0u, 0u}); + SetRecord(7, PointClamp, {0x1234u, 0x5678u, 0x9abcu, 0x1000ffacu}); + for (std::uint32_t record = 8; record < Records; ++record) SetRecord(record, PointClamp, floatImage); +} + +void RunBorderSwizzleTests(AgcDriver::VulkanDevice& device) { + constexpr std::array opaqueBlack{0x1b6u, (4u * 256u) << 12u, 0u, 1u << 30u}; + Fill(); + Table = {}; + Keys = {}; + Srt = {}; + const auto floatImage = Image(FloatTexels.data(), Format32Float); + auto alphaImage = floatImage; + alphaImage[3] |= 1u << 25u; + const auto alphaControl = RunDirect(device, PointClamp, alphaImage); + Require(!Faulted(alphaControl), "image table border: an alpha-moving BC swizzle without an opaque black border faulted"); + RequireRed(alphaControl, 0, Lanes, [](std::uint32_t x) { return std::bit_cast(FloatTexels[x]); }, "alpha-moving BC swizzle without a border"); + std::string refusal; + try { + static_cast(RunDirect(device, opaqueBlack, alphaImage)); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find("moves the alpha channel is sampled through an opaque black border") != std::string::npos, "image table border: a direct T#/S# alpha-moving opaque-black pair was not refused: " + refusal); + const auto requireFault = [](const Outcome& outcome, const std::string& reason, const std::string& what) { + Require(Faulted(outcome) && outcome.log.find(reason) != std::string::npos, what + " did not report its entry rejection:\n" + outcome.log); + if (!Zeros(outcome)) { + const auto unexpected = std::find_if(outcome.words.begin(), outcome.words.end(), [](std::uint32_t word) { return word != 0u && word != 0xdeadbeefu; }); + Require(false, what + ": faulted output word " + std::to_string(unexpected - outcome.words.begin()) + " is " + Hex(*unexpected) + ", expected zero or untouched 0xdeadbeef"); + } + }; + for (const auto bcSwizzle : {0u, 4u}) { + auto image = floatImage; + image[3] |= bcSwizzle << 25u; + const auto name = "image table border BC" + std::to_string(bcSwizzle); + const auto control = RunDirect(device, PointClamp, image); + const auto directOpaque = RunDirect(device, opaqueBlack, image); + Require(!Faulted(directOpaque) && Group(directOpaque, 0) == Group(control, 0), name + ": an allowed direct T#/S# opaque-black pair changed the result"); + RequireRed(directOpaque, 0, Lanes, [](std::uint32_t x) { return std::bit_cast(FloatTexels[x]); }, name + " direct opaque-black texels"); + + SetRecord(0, PointClamp, image); + SetRecord(1, PointClamp, alphaImage); + Keys[0] = 0; + Keys[1] = 1; + const auto inactiveImage = RunDirectCode(device, TableDirectSamplerCode, opaqueBlack, image); + Require(inactiveImage.imageTableCount == 2u, name + ": the inactive alpha-moving T# was not included in the snapshot"); + Require(!Faulted(inactiveImage) && Group(inactiveImage, 0) == Group(directOpaque, 0), name + ": an inactive rejected table T# poisoned the active T#/direct S# pair:\n" + inactiveImage.log); + Keys[0] = 1; + Keys[1] = 0; + const auto activeImage = RunDirectCode(device, TableDirectSamplerCode, opaqueBlack, image); + requireFault(activeImage, "moves the alpha channel is sampled through an opaque black border", name + " table T#/direct S#"); + + SetRecord(0, PointClamp, image); + SetRecord(1, opaqueBlack, image); + for (const bool tableImage : {false, true}) { + const auto pair = name + (tableImage ? " table T#/table S#" : " direct T#/table S#"); + Keys[0] = 0; + Keys[1] = 1; + const auto inactiveSampler = tableImage ? RunDirectCode(device, TableCode, PointClamp, image) : RunDirectCode(device, TableSamplerCode, PointClamp, image); + Require(Holds(inactiveSampler.samplerTable, opaqueBlack), pair + ": the inactive opaque-black S# was not included in the snapshot"); + Require(!Faulted(inactiveSampler) && Group(inactiveSampler, 0) == Group(control, 0), pair + ": an inactive rejected S# poisoned the active pair:\n" + inactiveSampler.log); + Keys[0] = 1; + Keys[1] = 0; + const auto activeSampler = tableImage ? RunDirectCode(device, TableCode, PointClamp, image) : RunDirectCode(device, TableSamplerCode, PointClamp, image); + requireFault(activeSampler, "sampler table entry with an opaque black border", pair); + } + } + const auto cut = Run(device, TableCutCode, 1, Lanes, 64); + Require(Holds(cut.samplerTable, opaqueBlack), "image table border: the EXEC-clear opaque-black S# was not included in the snapshot"); + Require(!Faulted(cut), "image table border: an opaque-black S# selected with EXEC clear faulted:\n" + cut.log); + Require(std::all_of(cut.words.begin(), cut.words.end(), [](std::uint32_t word) { return word == 0xdeadbeefu; }), "image table border: the EXEC-clear shader wrote output"); + for (const std::uint32_t border : {0u, 2u}) { + auto sampler = opaqueBlack; + sampler[3] = border << 30u; + const auto name = "image table border numeric class " + std::to_string(border); + const auto direct = RunDirect(device, sampler, floatImage); + Require(!Faulted(direct), name + ": the direct float-border control faulted"); + RequireRed(direct, 0, Lanes, [](std::uint32_t x) { return std::bit_cast(FloatTexels[x]); }, name + " direct float texels"); + SetRecord(0, PointClamp, floatImage); + SetRecord(1, PointClamp, Image(UintTexels.data(), Format32UInt)); + Keys[0] = 0; + Keys[1] = 1; + const auto inactiveImage = RunDirectCode(device, TableDirectSamplerCode, sampler, floatImage); + Require(inactiveImage.imageTableCount == 2u, name + ": the inactive integer T# was not included in the snapshot"); + Require(!Faulted(inactiveImage) && Group(inactiveImage, 0) == Group(direct, 0), name + ": an inactive integer T# poisoned the float-border pair:\n" + inactiveImage.log); + Keys[0] = 1; + Keys[1] = 0; + requireFault(RunDirectCode(device, TableDirectSamplerCode, sampler, floatImage), "border color numeric class disagrees", name + " active integer T#"); + SetRecord(0, PointClamp, floatImage); + SetRecord(1, sampler, floatImage); + for (const bool tableImage : {false, true}) { + const auto pair = name + (tableImage ? " table T#/table S#" : " direct T#/table S#"); + Keys[0] = 0; + Keys[1] = 1; + const auto inactiveSampler = tableImage ? RunDirectCode(device, TableCode, PointClamp, floatImage) : RunDirectCode(device, TableSamplerCode, PointClamp, floatImage); + Require(Holds(inactiveSampler.samplerTable, sampler), pair + ": the inactive border S# was not included in the snapshot"); + Require(!Faulted(inactiveSampler) && Group(inactiveSampler, 0) == Group(direct, 0), pair + ": an inactive rejected S# poisoned the active pair:\n" + inactiveSampler.log); + Keys[0] = 1; + Keys[1] = 0; + const auto activeSampler = tableImage ? RunDirectCode(device, TableCode, PointClamp, floatImage) : RunDirectCode(device, TableSamplerCode, PointClamp, floatImage); + requireFault(activeSampler, "sampler table entry with a border needs dynamic texture numeric-class pairing", pair); + } + const auto inactiveExec = Run(device, TableCutCode, 1, Lanes, 64); + Require(Holds(inactiveExec.samplerTable, sampler), name + ": the EXEC-clear border S# was not included in the snapshot"); + Require(!Faulted(inactiveExec) && std::all_of(inactiveExec.words.begin(), inactiveExec.words.end(), [](std::uint32_t word) { return word == 0xdeadbeefu; }), name + ": the EXEC-clear border S# faulted or wrote output:\n" + inactiveExec.log); + } +} + +void RunAnisoOverrideTests(AgcDriver::VulkanDevice& device) { + constexpr std::uint32_t overrideBit = 1u << 29u; + constexpr std::uint32_t anisoFilters = (2u << 20u) | (2u << 22u); + Fill(); + Table = {}; + Keys = {}; + Srt = {}; + const auto image = Image(FloatTexels.data(), Format32Float); + const auto control = RunDirect(device, PointClamp, image); + const auto requireFault = [](const Outcome& outcome, const std::string& what) { + Require(Faulted(outcome) && outcome.log.find("anisotropic ANISO_OVERRIDE needs dynamic texture mip-level pairing") != std::string::npos, what + " did not report its entry rejection:\n" + outcome.log); + Require(Zeros(outcome), what + " returned values other than zero or the untouched sentinel"); + }; + for (const auto filters : {overrideBit, anisoFilters, overrideBit | anisoFilters}) { + auto sampler = PointClamp; + sampler[0] |= 2u << 9u; + sampler[2] |= filters; + const bool rejected = filters == (overrideBit | anisoFilters); + const auto direct = RunDirect(device, sampler, image); + Require(!Faulted(direct) && Group(direct, 0) == Group(control, 0), "image table ANISO_OVERRIDE: a direct pair changed its texels"); + SetRecord(0, PointClamp, image); + SetRecord(1, sampler, image); + Keys[0] = 0; + Keys[1] = 1; + const auto tableImage = RunDirectCode(device, TableDirectSamplerCode, sampler, image); + if (rejected) requireFault(tableImage, "image table ANISO_OVERRIDE table T#/direct S#"); + else Require(!Faulted(tableImage) && Group(tableImage, 0) == Group(direct, 0), "image table ANISO_OVERRIDE: an unaffected direct sampler changed its table result"); + for (const bool tableImages : {false, true}) { + const auto pair = std::string("image table ANISO_OVERRIDE ") + (tableImages ? "table T#/table S#" : "direct T#/table S#"); + Keys[0] = 0; + Keys[1] = 1; + const auto inactive = tableImages ? RunDirectCode(device, TableCode, PointClamp, image) : RunDirectCode(device, TableSamplerCode, PointClamp, image); + Require(Holds(inactive.samplerTable, sampler), pair + ": the inactive sampler was not captured"); + Require(!Faulted(inactive) && Group(inactive, 0) == Group(control, 0), pair + ": the inactive entry changed the selected texels"); + Keys[0] = 1; + Keys[1] = 0; + const auto active = tableImages ? RunDirectCode(device, TableCode, PointClamp, image) : RunDirectCode(device, TableSamplerCode, PointClamp, image); + if (rejected) requireFault(active, pair); + else Require(!Faulted(active) && Group(active, 0) == Group(direct, 0), pair + ": an unaffected sampler changed its texels"); + } + if (rejected) { + alignas(256) std::array cutCode{}; + std::copy_n(TableDirectSamplerCode.begin(), 21u, cutCode.begin()); + cutCode[21] = 0x7da400ffu; + cutCode[22] = 0x00001000u; + std::copy(TableDirectSamplerCode.begin() + 21u, TableDirectSamplerCode.end(), cutCode.begin() + 23u); + const auto cut = RunDirectCode(device, cutCode, sampler, image); + Require(cut.imageTableCount == 1u && !Faulted(cut), "image table ANISO_OVERRIDE: an EXEC-clear table T#/direct S# access faulted"); + Require(std::all_of(cut.words.begin(), cut.words.end(), [](std::uint32_t word) { return word == 0xdeadbeefu; }), "image table ANISO_OVERRIDE: EXEC-clear sampling wrote output"); + } + } +} + +void RunTests(AgcDriver::VulkanDevice& device) { + Fill(); + const auto floatImage = Image(FloatTexels.data(), Format32Float); + const auto uintImage = Image(UintTexels.data(), Format32UInt); + const auto sintImage = Image(SintTexels.data(), Format32SInt); + SetClassRecords(); + + Keys = {}; + for (std::uint32_t group = 0; group < 6; ++group) Keys[group] = group; + const auto classes = Run(device, TableCode, 6, Lanes, 64); + Require(!Faulted(classes), "image table: valid entries faulted:\n" + classes.log); + Require(classes.imageTableCount == 3u, "image table: the table binding does not hold one element per distinct T# (" + std::to_string(classes.imageTableCount) + ")"); + + const auto invariantPhiCode = [] { + std::array code{}; + std::copy_n(TableCode.begin(), 19, code.begin()); + code[19] = 0xbea80380u; + std::copy_n(TableCode.begin() + 19, 8, code.begin() + 20); + const std::array tail{0xbf068028u, 0xbf850003u, 0xf42c0802u, 0x24000000u, 0xbf820002u, 0xf42c0802u, 0x24000000u, 0x80288128u, 0xbf0a8228u, 0xbf85ffeeu, 0xbf810000u}; + std::copy(tail.begin(), tail.end(), code.begin() + 28); + return code; + }(); + const auto controlPhiCode = [&] { + std::array code{}; + std::copy_n(invariantPhiCode.begin(), 28, code.begin()); + const std::array tail{0x80288128u, 0xbf0a8228u, 0xbf85fff5u, 0xbf810000u}; + std::copy(tail.begin(), tail.end(), code.begin() + 28); + return code; + }(); + const auto controlPhi = Run(device, controlPhiCode, 6, Lanes, 64); + const auto invariantPhi = Run(device, invariantPhiCode, 6, Lanes, 64); + Require(!Faulted(invariantPhi) && !Faulted(controlPhi), "image table: invariant descriptor Phis faulted"); + Require(invariantPhi.imageTableCount == classes.imageTableCount, "image table: invariant descriptor Phis changed the distinct table elements"); + Require(invariantPhi.words == controlPhi.words, "image table: invariant descriptor Phis changed GPU sample results"); + for (std::uint32_t group = 0; group < 6; ++group) { + if (group == 4u) continue; + const auto* words = Table.data() + group * (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(classes, group) == Group(control, 0), "image table: record " + std::to_string(group) + " differs from the direct binding of the same words"); + } + RequireRed(classes, 0, Lanes, [](std::uint32_t x) { return std::bit_cast(FloatTexels[x]); }, "float texels"); + RequireRed(classes, 1, Lanes, [](std::uint32_t x) { return UintTexels[x]; }, "uint texels"); + RequireRed(classes, 2, Lanes, [](std::uint32_t x) { return static_cast(SintTexels[x]); }, "sint texels"); + RequireRed(classes, 4, Lanes, [](std::uint32_t x) { return UintTexels[x]; }, "uint texels through a linear sampler"); + + Keys[0] = 6; + Keys[1] = 1000; + Keys[2] = 0x10000000u; + const auto zeros = Run(device, TableCode, 3, Lanes, 64); + Require(!Faulted(zeros), "image table: a null entry or an out-of-range key faulted:\n" + zeros.log); + for (std::size_t index = 0; index < 2 * Lanes * 4; ++index) { + Require(zeros.words[index] == 0u, "image table: a null entry or an out-of-range key did not sample zeros (word " + std::to_string(index) + " is " + Hex(zeros.words[index]) + ")"); + } + Require(Group(zeros, 2) == Group(classes, 0), "image table: a key that wraps onto record 0 did not sample record 0"); + Require(zeros.artifactId == classes.artifactId && zeros.variantId == classes.variantId, "image table: other keys changed the module"); + + Keys[0] = 0x05555556u; + const auto wrapped = Run(device, TableCode, 1, Lanes, 64); + Require(Faulted(wrapped) && wrapped.log.find("outside the snapshot") != std::string::npos, "image table: a mid-record key did not fault as outside the snapshot:\n" + wrapped.log); + + Keys = {}; + const auto unused = Run(device, TableCode, 1, Lanes, 64); + Require(!Faulted(unused) && Group(unused, 0) == Group(classes, 0), "image table: an unreferenced invalid entry changed the result"); + Require(unused.imageTableCount == 1u, "image table: a narrowed table bound records its keys cannot select"); + Keys[0] = 7; + const auto cut = Run(device, TableCutCode, 1, Lanes, 64); + Require(!Faulted(cut), "image table: an invalid entry selected with EXEC clear faulted:\n" + cut.log); + const auto selected = Run(device, TableCode, 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(selected) && selected.log.find("not an image") != std::string::npos && selected.log.find(address) != std::string::npos && selected.log.find("00001234") != std::string::npos, "image table: an invalid entry selected with EXEC set did not report its address, words and reason:\n" + selected.log); + Require(Zeros(selected), "image table: a faulting access did not sample zeros"); + + void* reserved = nullptr; +#ifdef _WIN32 + reserved = VirtualAlloc(nullptr, 65536, MEM_RESERVE, PAGE_NOACCESS); +#else + reserved = mmap(nullptr, 65536, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (reserved == MAP_FAILED) reserved = nullptr; +#endif + Require(reserved != nullptr, "cannot reserve an inaccessible range"); + const auto unmappedImage = Image(reserved, Format32Float); + SetRecord(8, PointClamp, unmappedImage); + Keys[0] = 0; + const auto unmappedUnused = Run(device, TableCode, 1, Lanes, 64); + Require(!Faulted(unmappedUnused) && Group(unmappedUnused, 0) == Group(classes, 0), "image table: an unreferenced unmapped entry changed the result:\n" + unmappedUnused.log); + Keys[0] = 8; + const auto unmappedUsed = Run(device, TableCode, 1, Lanes, 64); + Require(Faulted(unmappedUsed) && unmappedUsed.log.find("rejected by the driver") != std::string::npos && unmappedUsed.log.find("is not mapped") != std::string::npos && Zeros(unmappedUsed), "image table: an entry whose surface is not mapped was bound instead of faulting when selected:\n" + unmappedUsed.log); + SetRecord(8, PointClamp, floatImage); +#ifdef _WIN32 + VirtualFree(reserved, 0, MEM_RELEASE); +#else + munmap(reserved, 65536); +#endif + + const auto srgbImage = Image(UintTexels.data(), Format8_8Srgb); + SetRecord(9, PointClamp, srgbImage); + Keys[0] = 0; + const auto srgbUnused = Run(device, TableCode, 1, Lanes, 64); + Require(!Faulted(srgbUnused) && Group(srgbUnused, 0) == Group(classes, 0), "image table: an unreferenced 8_8_SRGB entry changed the result:\n" + srgbUnused.log); + Keys[0] = 9; + const auto srgbUsed = Run(device, TableCode, 1, Lanes, 64); + Require(Faulted(srgbUsed) && srgbUsed.log.find("sRGB decode") != std::string::npos, "image table: an 8_8_SRGB entry sampled with shader decode did not fault as sRGB decode:\n" + srgbUsed.log); + Require(Zeros(srgbUsed), "image table: a sampled 8_8_SRGB entry decoded in the shader did not sample zeros"); + std::string refusal; + try { + static_cast(RunDirect(device, PointClamp, srgbImage)); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find("samples or gathers an sRGB image the device cannot sample") != std::string::npos, "image table: the direct binding of the 8_8_SRGB entry was not refused: " + refusal); + SetRecord(9, PointClamp, floatImage); + + Keys[0] = 0; + const auto before = Run(device, TableCode, 1, Lanes, 64); + SetRecord(0, PointClamp, Image(OtherFloatTexels.data(), Format32Float)); + const auto after = Run(device, TableCode, 1, Lanes, 64); + Require(after.variantId == before.variantId && after.artifactId == before.artifactId, "image table: other table contents specialized another module"); + RequireRed(after, 0, Lanes, [](std::uint32_t x) { return std::bit_cast(OtherFloatTexels[x]); }, "changed table contents"); + OtherFloatTexels[5] = 1234.5f; + const auto texels = Run(device, TableCode, 1, Lanes, 64); + RequireRed(texels, 0, Lanes, [](std::uint32_t x) { return std::bit_cast(OtherFloatTexels[x]); }, "changed texels"); + SetRecord(0, PointClamp, floatImage); + + std::copy(LinearWrap.begin(), LinearWrap.end(), Srt.begin() + 12); + Keys[0] = 2; + Keys[1] = 0; + Keys[2] = 1; + const auto integerLinear = Run(device, TableDirectSamplerCode, 3, Lanes, 64); + Require(!Faulted(integerLinear), "image table: a direct sampler with a table T# faulted:\n" + integerLinear.log); + const auto sintControl = RunDirect(device, LinearWrap, sintImage); + const auto floatControl = RunDirect(device, LinearWrap, floatImage); + const auto uintControl = RunDirect(device, LinearWrap, uintImage); + Require(floatControl.samplers == std::vector{LinearWrap} && uintControl.samplers == std::vector{PointWrap} && sintControl.samplers == std::vector{PointWrap}, "image table: the direct bindings under a linear S# changed their sampler copies"); + Require(Group(integerLinear, 0) == Group(sintControl, 0) && Group(integerLinear, 1) == Group(floatControl, 0), "image table: an entry under a linear direct S# differs from the direct binding"); + RequireRed(integerLinear, 0, Lanes, [](std::uint32_t x) { return static_cast(SintTexels[x]); }, "sint texels through a linear direct sampler"); + RequireRed(integerLinear, 2, Lanes, [](std::uint32_t x) { return UintTexels[x]; }, "uint texels through a linear direct sampler"); + + Keys = {}; + Keys[0] = 1; + const auto uintLinear = Run(device, TableDirectSamplerCode, 2, Lanes, 64); + Require(!Faulted(uintLinear), "image table: a direct sampler with float and uint table T#s faulted:\n" + uintLinear.log); + Require(Holds(uintLinear.samplers, LinearWrap) && Holds(uintLinear.samplers, PointWrap), "image table: a linear direct S# with float and uint table T#s has no point-filtered copy"); + Require(Group(uintLinear, 1) == Group(floatControl, 0), "image table: a float entry under a linear direct S# differs from the direct binding"); + RequireRed(uintLinear, 0, Lanes, [](std::uint32_t x) { return UintTexels[x]; }, "uint texels beside float texels through a linear direct sampler"); + + Keys[0] = 4; + const auto uintTable = Run(device, TableCode, 1, Lanes, 64); + Require(!Faulted(uintTable), "image table: a table S# with float and uint table T#s faulted:\n" + uintTable.log); + Require(TablePointCopy(uintTable, LinearWrap, PointWrap), "image table: a linear table S# has no point-filtered copy"); + RequireRed(uintTable, 0, Lanes, [](std::uint32_t x) { return UintTexels[x]; }, "uint texels through a linear table sampler"); + + std::copy(uintImage.begin(), uintImage.end(), Srt.begin() + 16); + Keys[0] = 4; + Keys[1] = 1; + const auto tableSampler = Run(device, TableSamplerCode, 2, Lanes, 64); + Require(!Faulted(tableSampler), "image table: a table sampler with a direct T# faulted:\n" + tableSampler.log); + Require(TablePointCopy(tableSampler, LinearWrap, PointWrap), "image table: a linear table S# with a direct uint T# has no point-filtered copy"); + RequireRed(tableSampler, 0, Lanes, [](std::uint32_t x) { return UintTexels[x]; }, "direct uint texels through a linear table sampler"); + RequireRed(tableSampler, 1, Lanes, [](std::uint32_t x) { return UintTexels[x]; }, "direct uint texels through a point table sampler"); + + RunWaveKeys(device, TableWave32Code, "image table"); + + Keys = {}; + const auto aliased = Run(device, TableCode, 1, Lanes, 64, true); + Require(Faulted(aliased) && aliased.log.find("is written by its own dispatch") != std::string::npos, "image table: a dispatch storing into its own table did not fault on use:\n" + aliased.log); + SetClassRecords(); +} + +void RunReadTests(AgcDriver::VulkanDevice& device) { + SetClassRecords(); + Keys = {}; + const std::array keys{0u, 1u, 2u, 3u, 6u}; + std::copy(keys.begin(), keys.end(), Keys.begin()); + const auto reads = Run(device, ReadTableCode, 5, Lanes, 64); + Require(!Faulted(reads), "image table read: valid entries faulted:\n" + reads.log); + for (std::uint32_t group = 0; group < 4; ++group) { + const auto* words = Table.data() + keys[group] * (Stride / 4u); + const auto control = RunDirectCode(device, ReadDirectCode, PointClamp, std::span(words + 4, 4)); + Require(Group(reads, group) == Group(control, 0), "image table read: record " + std::to_string(keys[group]) + " differs from the direct binding of the same words"); + } + RequireRed(reads, 0, Lanes, [](std::uint32_t x) { return std::bit_cast(FloatTexels[x]); }, "image table read: float texels"); + RequireRed(reads, 1, Lanes, [](std::uint32_t x) { return UintTexels[x]; }, "image table read: uint texels"); + RequireRed(reads, 2, Lanes, [](std::uint32_t x) { return static_cast(SintTexels[x]); }, "image table read: sint texels"); + const auto nullGroup = Group(reads, 4); + Require(std::all_of(nullGroup.begin(), nullGroup.end(), [](std::uint32_t word) { return word == 0u; }), "image table read: a null entry did not read zeros"); + + Keys[0] = 7; + const auto cut = Run(device, ReadTableCutCode, 1, Lanes, 64); + Require(!Faulted(cut), "image table read: an invalid entry read with EXEC clear faulted:\n" + cut.log); + const auto selected = Run(device, ReadTableCode, 1, Lanes, 64); + Require(Faulted(selected) && selected.log.find("not an image") != std::string::npos && selected.log.find("00001234") != std::string::npos, "image table read: an invalid entry read with EXEC set did not report its words and reason:\n" + selected.log); + + Keys = {}; + Keys[1] = 1u; + const auto halves = Run(device, ReadTableD16Code, 2, Lanes, 64); + Require(!Faulted(halves), "image table d16 read: valid entries faulted:\n" + halves.log); + for (std::uint32_t group = 0; group < 2; ++group) { + const auto* words = Table.data() + group * (Stride / 4u); + const auto control = RunDirectCode(device, ReadDirectD16Code, PointClamp, std::span(words + 4, 4)); + Require(Group(halves, group) == Group(control, 0), "image table d16 read: record " + std::to_string(group) + " differs from the direct binding of the same words"); + } + RequireRed(halves, 0, Lanes, [](std::uint32_t x) { return Half(FloatTexels[x]); }, "image table d16 read: float texels"); + RequireRed(halves, 1, Lanes, [](std::uint32_t x) { return UintTexels[x] & 0xffffu; }, "image table d16 read: uint texels"); + + RunWaveKeys(device, ReadTableWave32Code, "image table read"); +} + +void RequireIterations(const Outcome& outcome, std::span records, const std::string& what) { + Require(!Faulted(outcome) && outcome.imageTableCount != 0u, what + ": the loop did not read through the table:\n" + outcome.log); + for (std::uint32_t iteration = 0; iteration < records.size(); ++iteration) { + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + const auto* texel = Output.data() + (iteration * Lanes + lane) * 4u; + const auto record = records[iteration]; + const auto expected = record == 0u ? std::bit_cast(FloatTexels[lane]) : record == 1u ? UintTexels[lane] : record == 2u ? static_cast(SintTexels[lane]) : 0u; + Require(texel[0] == expected && (record != 6u || std::all_of(texel, texel + 4, [](std::uint32_t word) { return word == 0u; })), what + ": iteration " + std::to_string(iteration) + " read " + Hex(texel[0]) + " at lane " + std::to_string(lane) + ", expected record " + std::to_string(record) + "'s " + Hex(expected)); + } + } +} + +void RunLoopKeyTests(AgcDriver::VulkanDevice& device) { + SetClassRecords(); + Keys = {}; + const std::array counted{0u, 1u, 2u}; + RequireIterations(Run(device, ReadTableCounterLoopCode, 1, Lanes, 64), counted, "image table loop counter key"); + Keys[0] = 0x45u; + const std::array setBits{0u, 2u, 6u}; + RequireIterations(Run(device, ReadTableSetBitLoopCode, 1, Lanes, 64), setBits, "image table set-bit key"); + Keys[0] = 0x6u; + const std::array otherBits{1u, 2u}; + RequireIterations(Run(device, ReadTableSetBitLoopCode, 1, Lanes, 64), otherBits, "image table set-bit key after the mask changed"); +} + +void RunGuardedTableTests(AgcDriver::VulkanDevice& device) { + Keys = {}; + const auto image = Image(FloatTexels.data(), Format32Float); + for (std::uint32_t record = 0; record < Records; ++record) SetRecord(record, PointClamp, image); + const auto pointer = reinterpret_cast(&Srt[30]); + Srt[28] = static_cast(pointer); + Srt[29] = static_cast(pointer >> 32u); + Srt[30] = 0x12345678u; + const auto mapped = Run(device, GuardedTableCode, 1, Lanes, 64); + Require(mapped.imageTableCount != 0u && mapped.imageTableFaults == 0u && mapped.poisonedSrtReads == 0u && mapped.cacheable && mapped.log.find("BDA access failed") == std::string::npos, "image table guard: a mapped nested read faulted or left the caches:\n" + mapped.log); + RequireRed(mapped, 0, Lanes, [](std::uint32_t) { return 0x12345678u; }, "image table guard: mapped nested payload"); + Srt[28] = 0u; + Srt[29] = 0u; + const auto poisoned = Run(device, GuardedTableCode, 1, Lanes, 64); + Require(poisoned.imageTableCount != 0u && poisoned.imageTableFaults == 0u && poisoned.poisonedSrtReads == 1u && !poisoned.cacheable, "image table guard: a poisoned nested read was not captured or stayed cacheable"); + Require(poisoned.artifactId == mapped.artifactId && poisoned.variantId == mapped.variantId, "image table guard: a poisoned nested read changed the pipeline"); + Require(poisoned.log.find("BDA access failed") != std::string::npos && poisoned.log.find("address=0x0 instruction=0x24 bytes=4") != std::string::npos, "image table guard: a valid table suppressed the nested read's completion fault:\n" + poisoned.log); + 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 = {}; + Keys[0] = record; + const auto table = Run(device, tableCode, 1, Lanes, 64); + std::string refusal; + Outcome control; + try { + control = RunDirectCode(device, directCode, PointClamp, image); + } catch (const std::exception& error) { + refusal = error.what(); + } + if (!refusal.empty()) { + Require(Faulted(table) && Zeros(table), what + ": the direct binding was refused (" + refusal + ") but the table entry did not fault:\n" + table.log); + std::printf("%s: refused directly and faulted through the table: %s\n", what.c_str(), refusal.c_str()); + return; + } + Require(!Faulted(table), what + ": the table entry faulted:\n" + table.log); + Require(Group(table, 0) == Group(control, 0), what + ": the table entry differs from the direct binding of the same words"); +} + +void RunModeTests(AgcDriver::VulkanDevice& device) { + const auto rgb = Image(UintTexels.data(), Format11_11_10UInt); + const auto depthBase = (reinterpret_cast(DepthStorage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}; + const auto depth = Image(reinterpret_cast(depthBase), Format32UInt, Type2D, DepthSwizzleMode); + const auto srgb = Image(UintTexels.data(), Format8_8Srgb); + SetRecord(0, PointClamp, rgb); + SetRecord(1, PointClamp, depth); + SetRecord(2, PointClamp, srgb); + SetRecord(3, PointClamp, Image(FloatTexels.data(), Format32Float)); + RequireModeEntry(device, ReadTableCode, ReadDirectCode, 0, rgb, "image table mode: 11_11_10 UINT image_load"); + RequireModeEntry(device, TableCode, DirectCode, 0, rgb, "image table mode: 11_11_10 UINT image_sample"); + RequireModeEntry(device, ReadTableCode, ReadDirectCode, 1, depth, "image table mode: depth bits image_load"); + RequireModeEntry(device, TableCode, DirectCode, 1, depth, "image table mode: depth bits image_sample"); + if (device.Target().srgbDecodeFormats != 0u) { + RequireModeEntry(device, ReadTableCode, ReadDirectCode, 2, srgb, "image table mode: 8_8_SRGB image_load decoded in the shader"); + } else { + std::printf("image table mode: the sRGB decode case skipped, the device samples 8_8_SRGB natively\n"); + } + + std::array volume{}; + const auto volumeWords = Image(FloatTexels.data(), Format32Float, Type3D); + std::copy(volumeWords.begin(), volumeWords.end(), volume.begin()); + std::array plain{}; + const auto plainWords = Image(FloatTexels.data(), Format32Float); + std::copy(plainWords.begin(), plainWords.end(), plain.begin()); + SetWideRecord(0, {volume[0], volume[1], volume[2], volume[3]}); + SetWideRecord(1, {plain[0], plain[1], plain[2], plain[3]}); + RequireModeEntry(device, ReadTable256Code, ReadDirect256Code, 0, volume, "image table mode: a 3D T# read by a 2D image_load"); + RequireModeEntry(device, ReadTable256Code, ReadDirect256Code, 1, plain, "image table mode: an r256 2D T#"); + SetClassRecords(); +} + +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 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); + 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) { + const auto floatImage = Image(FloatTexels.data(), Format32Float); + const auto uintImage = Image(UintTexels.data(), Format32UInt); + const auto sintImage = Image(SintTexels.data(), Format32SInt); + const std::array invalid{0x1234u, 0x5678u, 0x9abcu, 0x1000ffacu}; + std::fill(Root.begin(), Root.end(), 0u); + SetEntry(0, floatImage); + SetEntry(1, uintImage); + SetEntry(2, sintImage); + SetEntry(7, invalid); + SetEntry(255, uintImage); + const std::array keys{0u, 1u, 2u, 3u, 0x1ffu, 0x102u}; + Keys = {}; + std::copy(keys.begin(), keys.end(), Keys.begin()); + const auto palette = RunPointer(device, PointerCode, 6); + Require(!Faulted(palette), "pointer image table: valid entries faulted:\n" + palette.log); + for (std::uint32_t group = 0; group < keys.size(); ++group) { + const auto* words = Root.data() + (PaletteOffset + (keys[group] & 0xffu) * 32u) / 4u; + if (std::all_of(words, words + 8, [](std::uint32_t word) { return word == 0u; })) { + const auto zeros = Group(palette, group); + Require(std::all_of(zeros.begin(), zeros.end(), [](std::uint32_t word) { return word == 0u; }), "pointer image table: a null entry did not sample zeros"); + continue; + } + if (keys[group] == 0x1ffu) continue; + const auto control = RunDirect(device, PointClamp, {words[0], words[1], words[2], words[3]}); + Require(Group(palette, group) == Group(control, 0), "pointer image table: key " + Hex(keys[group]) + " differs from the direct binding of its masked entry"); + } + RequireRed(palette, 0, Lanes, [](std::uint32_t x) { return std::bit_cast(FloatTexels[x]); }, "pointer image table float texels"); + RequireRed(palette, 2, Lanes, [](std::uint32_t x) { return static_cast(SintTexels[x]); }, "pointer image table sint texels"); + RequireRed(palette, 4, Lanes, [](std::uint32_t x) { return UintTexels[x]; }, "pointer image table key 0x1ff masked to entry 255"); + + const auto loaded = RunPointer(device, LoadedPointerCode, 6); + Require(!Faulted(loaded) && loaded.words == palette.words, "pointer image table: a base loaded from the SRT with a negative immediate differs:\n" + loaded.log); + Require(loaded.artifactId != 0u && palette.artifactId != loaded.artifactId, "pointer image table: two programs share an artifact"); + + Keys[0] = 7; + const auto selected = RunPointer(device, PointerCode, 1); + char address[32]; + std::snprintf(address, sizeof(address), "0x%llx", static_cast(reinterpret_cast(Root.data()) + PaletteOffset + 7u * 32u)); + Require(Faulted(selected) && selected.log.find("not an image") != std::string::npos && selected.log.find(address) != std::string::npos && selected.log.find("00001234") != std::string::npos, "pointer image table: an invalid entry did not report its address, words and reason:\n" + selected.log); + Require(Zeros(selected), "pointer image table: a faulting access did not sample zeros"); + Require(selected.variantId == palette.variantId, "pointer image table: other keys specialized another module"); + + Keys[0] = 2; + Keys[1] = 0x08000000u; + const auto unbounded = RunPointer(device, UnboundedPointerCode, 2); + Require(!Faulted(unbounded) && Group(unbounded, 0) == Group(palette, 2) && Group(unbounded, 1) == Group(palette, 0), "pointer image table: an unmasked key, or one whose offset wraps to entry 0, sampled the wrong entry:\n" + unbounded.log); + Keys[0] = 300; + const auto past = RunPointer(device, UnboundedPointerCode, 1); + Require(Faulted(past) && past.log.find("unmapped") != std::string::npos, "pointer image table: an entry past the captured palette did not fault as unmapped:\n" + past.log); + Keys[0] = 0x10000u; + const auto capped = RunPointer(device, UnboundedPointerCode, 1); + Require(Faulted(capped) && capped.log.find("outside the snapshot") != std::string::npos, "pointer image table: a key past the key cap did not fault as outside the snapshot:\n" + capped.log); + + const std::array readKeys{0u, 1u, 2u, 3u, 0x1ffu}; + Keys = {}; + std::copy(readKeys.begin(), readKeys.end(), Keys.begin()); + const auto reads = RunPointer(device, ReadPointerCode, 5); + Require(!Faulted(reads), "pointer image table read: valid entries faulted:\n" + reads.log); + for (std::uint32_t group = 0; group < readKeys.size(); ++group) { + const auto* words = Root.data() + (PaletteOffset + (readKeys[group] & 0xffu) * 32u) / 4u; + if (std::all_of(words, words + 8, [](std::uint32_t word) { return word == 0u; })) { + const auto zeros = Group(reads, group); + Require(std::all_of(zeros.begin(), zeros.end(), [](std::uint32_t word) { return word == 0u; }), "pointer image table read: a null entry did not read zeros"); + continue; + } + const auto control = RunDirectCode(device, ReadDirectCode, PointClamp, std::span(words, 4)); + Require(Group(reads, group) == Group(control, 0), "pointer image table read: key " + Hex(readKeys[group]) + " differs from the direct binding of its masked entry"); + } + RequireRed(reads, 4, Lanes, [](std::uint32_t x) { return UintTexels[x]; }, "pointer image table read: key 0x1ff masked to entry 255"); +} + +void RunPointerRecordTests(AgcDriver::VulkanDevice& device) { + Fill(); + const auto floatImage = Image(FloatTexels.data(), Format32Float); + const auto uintImage = Image(UintTexels.data(), Format32UInt); + const auto sintImage = Image(SintTexels.data(), Format32SInt); + const std::array, 3> images{floatImage, uintImage, sintImage}; + const std::array, 3> texels{ + [](std::uint32_t x) { return std::bit_cast(FloatTexels[x]); }, + [](std::uint32_t x) { return UintTexels[x]; }, + [](std::uint32_t x) { return static_cast(SintTexels[x]); }, + }; + const std::array names{"float", "uint", "sint"}; + + Table = {}; + Indirect = {}; + Keys = {}; + for (std::uint32_t group = 0; group < 3; ++group) Keys[group] = 2u - group; + for (std::uint32_t record = 0; record < 3; ++record) { + const std::uint32_t entry = 2u - record; + std::copy(images[record].begin(), images[record].end(), Indirect.data() + entry * 16u + 8u); + const auto pointer = static_cast(reinterpret_cast(Indirect.data() + entry * 16u)); + Table[record * (Stride / 4u) + 2u] = static_cast(pointer); + Table[record * (Stride / 4u) + 3u] = static_cast(pointer >> 32u); + } + const auto indirect = RunIndirectRecords(device, IndirectRecordCode, 3); + Require(!Faulted(indirect), "image table: a T# behind a pointer in its record faulted:\n" + indirect.log); + Require(indirect.imageTableCount == 3u, "image table: T#s behind pointers in their records do not hold one element per distinct T# (" + std::to_string(indirect.imageTableCount) + ")"); + for (std::uint32_t group = 0; group < 3; ++group) RequireRed(indirect, group, Lanes, texels[2u - group], std::string(names[2u - group]) + " texels of a T# behind a pointer in its record"); + + const auto entry = Indirect.data() + 16u; + std::copy(floatImage.begin(), floatImage.end(), entry + 8u); + const auto changed = RunIndirectRecords(device, IndirectRecordCode, 3); + RequireRed(changed, 1, Lanes, texels[0], "float texels after the T# behind record 1's pointer changed"); +} + +void RunPointerChainRecordTests(AgcDriver::VulkanDevice& device) { + Fill(); + const auto floatImage = Image(FloatTexels.data(), Format32Float); + const auto uintImage = Image(UintTexels.data(), Format32UInt); + const auto sintImage = Image(SintTexels.data(), Format32SInt); + const std::array, 3> images{floatImage, uintImage, sintImage}; + const std::array, 3> texels{ + [](std::uint32_t x) { return std::bit_cast(FloatTexels[x]); }, + [](std::uint32_t x) { return UintTexels[x]; }, + [](std::uint32_t x) { return static_cast(SintTexels[x]); }, + }; + const std::array names{"float", "uint", "sint"}; + const auto store = [](std::uint32_t* at, const void* pointer) { + const auto address = static_cast(reinterpret_cast(pointer)); + at[0] = static_cast(address); + at[1] = static_cast(address >> 32u); + }; + + Table = {}; + Indirect = {}; + Links = {}; + Keys = {}; + for (std::uint32_t group = 0; group < 3; ++group) Keys[group] = 2u - group; + for (std::uint32_t record = 0; record < 3; ++record) { + const std::uint32_t entry = 2u - record; + std::copy(images[record].begin(), images[record].end(), Indirect.data() + entry * 16u + 8u); + store(Links.data() + record * 8u + 4u, Indirect.data() + entry * 16u); + store(Table.data() + record * (Stride / 4u) + 2u, Links.data() + record * 8u); + } + const auto chained = RunIndirectRecords(device, PointerChainRecordCode, 3); + Require(!Faulted(chained), "image table: a T# behind two pointers from its record faulted:\n" + chained.log); + Require(chained.imageTableCount == 3u, "image table: T#s behind two pointers from their records do not hold one element per distinct T# (" + std::to_string(chained.imageTableCount) + ")"); + for (std::uint32_t group = 0; group < 3; ++group) RequireRed(chained, group, Lanes, texels[2u - group], std::string(names[2u - group]) + " texels of a T# behind two pointers from its record"); + + store(Links.data() + 8u + 4u, Indirect.data() + 2u * 16u); + const auto relinked = RunIndirectRecords(device, PointerChainRecordCode, 3); + RequireRed(relinked, 1, Lanes, texels[0], "float texels after record 1's second pointer moved to another T#"); +} + +void RunSplitRecordTests(AgcDriver::VulkanDevice& device) { + Fill(); + const auto floatImage = Image(FloatTexels.data(), Format32Float); + const auto uintImage = Image(UintTexels.data(), Format32UInt); + const auto sintImage = Image(SintTexels.data(), Format32SInt); + const std::array, 3> images{floatImage, uintImage, sintImage}; + const std::array, 3> texels{ + [](std::uint32_t x) { return std::bit_cast(FloatTexels[x]); }, + [](std::uint32_t x) { return UintTexels[x]; }, + [](std::uint32_t x) { return static_cast(SintTexels[x]); }, + }; + const std::array names{"float", "uint", "sint"}; + + Table = {}; + for (std::uint32_t record = 0; record < 3; ++record) SetRecord(record, PointClamp, images[record]); + Keys = {}; + for (std::uint32_t group = 0; group < 3; ++group) Keys[group] = 2u - group; + const auto split = Run(device, SplitRecordCode, 3, Lanes, 64); + Require(!Faulted(split), "image table: a T# read by two loads of its record faulted:\n" + split.log); + Require(split.imageTableCount == 3u, "image table: a T# read by two loads of its record does not hold one element per distinct T# (" + std::to_string(split.imageTableCount) + ")"); + for (std::uint32_t group = 0; group < 3; ++group) RequireRed(split, group, Lanes, texels[2u - group], std::string(names[2u - group]) + " texels of a T# read by two loads of its record"); +} + +void RunPointerStorageWriteTests(AgcDriver::VulkanDevice& device) { + alignas(256) static std::array records{}; + alignas(256) static std::array otherRecords{}; + alignas(256) static std::array isolatedTexels{}; + 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); } + } registeredRecords(records.data(), sizeof(records)), registeredOtherTexels(OtherFloatTexels.data(), sizeof(OtherFloatTexels)); + for (std::uint32_t x = 0; x < isolatedTexels.size(); ++x) isolatedTexels[x] = 1024.0f + static_cast(x); + const auto run = [&](std::span code, bool alias, std::uint32_t waveSize = 64u, std::uint32_t selectedLanes = 0u) { + records.fill(0u); + otherRecords.fill(0u); + const auto sampled = Image(FloatTexels.data(), Format32Float); + std::copy(sampled.begin(), sampled.end(), records.begin()); + const auto isolated = Image(isolatedTexels.data(), Format32Float); + std::copy(isolated.begin(), isolated.end(), otherRecords.begin()); + const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); + const auto output = Buffer(Output.data(), static_cast(sizeof(Output))); + const auto* storageData = alias ? static_cast(records.data()) : OtherFloatTexels.data(); + const auto storage = Image(storageData, Format32Float); + Srt.fill(0u); + std::copy(keys.begin(), keys.end(), Srt.begin()); + std::copy(output.begin(), output.end(), Srt.begin() + 4); + std::copy(PointClamp.begin(), PointClamp.end(), Srt.begin() + 8); + const auto pointer = static_cast(reinterpret_cast(records.data())) + 0x100u; + Srt[12] = static_cast(pointer); + Srt[13] = static_cast(pointer >> 32u); + const auto otherPointer = static_cast(reinterpret_cast(otherRecords.data())) + 0x100u; + Srt[14] = static_cast(otherPointer); + Srt[15] = static_cast(otherPointer >> 32u); + std::copy(storage.begin(), storage.end(), Srt.begin() + 16); + Srt[24] = selectedLanes; + Keys.fill(0u); + const std::array memory{{ + {reinterpret_cast(code.data()), std::as_bytes(code)}, + {reinterpret_cast(Srt.data()), std::as_bytes(std::span(Srt))}, + {reinterpret_cast(records.data()), std::as_bytes(std::span(records))}, + {reinterpret_cast(otherRecords.data()), std::as_bytes(std::span(otherRecords))}, + {reinterpret_cast(Keys.data()), std::as_bytes(std::span(Keys))}, + }}; + const auto outcome = Execute(device, code, Srt.data(), memory, 1u, waveSize, waveSize); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(storageData), sizeof(FloatTexels), nullptr, "test"); + device.WaitIdle(); + return outcome; + }; + const auto separate = run(PointerStorageWriteCode, false); + Require(!Faulted(separate), "pointer image table: a separate storage image write faulted:\n" + separate.log); + RequireRed(separate, 0, Lanes, [](std::uint32_t x) { return std::bit_cast(FloatTexels[x]); }, "pointer image table with a separate storage image write"); + const auto aliased = run(PointerStorageWriteCode, true); + Require(Faulted(aliased) && aliased.log.find("is written by its own dispatch") != std::string::npos, "pointer image table: an overlapping storage image write did not fault:\n" + aliased.log); + Require(Zeros(aliased), "pointer image table: an overlapping storage image write did not suppress the sampled value"); + const auto isolated = run(PointerStorageIsolationCode, true, 32u); + Require(isolated.imageTableReadRanges.size() == 2u && !isolated.imageTableReadRanges[0].empty() && !isolated.imageTableReadRanges[1].empty(), "pointer image table isolation: both tables were not captured"); + for (const auto& [first, firstBytes] : isolated.imageTableReadRanges[0]) { + for (const auto& [second, secondBytes] : isolated.imageTableReadRanges[1]) { + Require(first <= second ? second - first >= firstBytes : first - second >= secondBytes, "pointer image table isolation: captured table ranges overlap"); + } + } + Require(!Faulted(isolated), "pointer image table isolation: writing the table selected with EXEC clear faulted the other table:\n" + isolated.log); + RequireRed(isolated, 0, 32u, [](std::uint32_t x) { return std::bit_cast(isolatedTexels[x]); }, "pointer image table isolation"); + for (std::uint32_t x = 0; x < 32u; ++x) { + Require(records[x] == std::bit_cast(isolatedTexels[x]), "pointer image table isolation: the storage image did not write the inactive table"); + } + const auto selected = run(PointerStorageIsolationCode, true, 32u, 32u); + Require(selected.imageTableReadRanges == isolated.imageTableReadRanges, "pointer image table isolation: selecting the written table changed the captured ranges"); + Require(Faulted(selected) && selected.log.find("is written by its own dispatch") != std::string::npos, "pointer image table isolation: selecting the written table did not fault:\n" + selected.log); + Require(Zeros(selected), "pointer image table isolation: selecting the written table did not suppress the sampled value"); +} + +std::array VertexBuffer(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}; +} + +alignas(256) constexpr std::array VertexTableCode{ + 0x8f098408u, 0xf4280302u, 0x12000000u, 0x7e140280u, 0x7e160280u, 0xbf8cc07fu, 0xf0008f08u, 0x0003100au, + 0xbf8c3f70u, 0xe0702000u, 0x80041005u, 0xe0382000u, 0x80000005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, + 0xbf810000u, +}; + +alignas(256) constexpr std::array PixelTableCode{ + 0x8f058404u, 0xf4280300u, 0x0a000000u, 0x7e140280u, 0x7e160280u, 0xbf8cc07fu, 0xf0008f08u, 0x0003100au, + 0xbf8c3f70u, 0xe0700000u, 0x80021000u, 0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u, +}; + +constexpr std::uint32_t TargetWidth = 64; +constexpr std::uint32_t TargetHeight = 32; +alignas(256) std::array Pixels{}; +alignas(256) std::array VertexResults{}; +alignas(256) std::array PixelResults{}; +alignas(256) std::array GraphicsTable{}; +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}}}; + +std::pair DrawTables(AgcDriver::VulkanDevice& device, std::uint32_t vertexKey, std::uint32_t pixelKey) { + const auto target = device.Target(); + Pixels.fill(std::byte{0}); + VertexResults.fill(0xdeadbeefu); + PixelResults.fill(0xdeadbeefu); + const auto table = Buffer(GraphicsTable.data(), static_cast(sizeof(GraphicsTable))); + std::vector vertexUserData(20, 0u); + const auto vertices = VertexBuffer(Triangle.data(), 16u, static_cast(Triangle.size())); + const auto vertexOutput = VertexBuffer(VertexResults.data(), 16u, 4u); + std::copy(vertices.begin(), vertices.end(), vertexUserData.begin()); + std::copy(table.begin(), table.end(), vertexUserData.begin() + 4); + vertexUserData[8] = vertexKey; + std::copy(vertexOutput.begin(), vertexOutput.end(), vertexUserData.begin() + 16); + const std::array vertexMemory{{{reinterpret_cast(VertexTableCode.data()), std::as_bytes(std::span(VertexTableCode))}, {reinterpret_cast(GraphicsTable.data()), std::as_bytes(std::span(GraphicsTable))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexTableCode.data()), VertexTableCode, 0, {}}, + {64, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = false; + 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(12, 0u); + const auto pixelOutput = Buffer(PixelResults.data(), static_cast(sizeof(PixelResults))); + std::copy(table.begin(), table.end(), pixelUserData.begin()); + pixelUserData[4] = pixelKey; + std::copy(pixelOutput.begin(), pixelOutput.end(), pixelUserData.begin() + 8); + const std::array pixelMemory{{{reinterpret_cast(PixelTableCode.data()), std::as_bytes(std::span(PixelTableCode))}, {reinterpret_cast(GraphicsTable.data()), std::as_bytes(std::span(GraphicsTable))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelTableCode.data()), PixelTableCode, 0, {}}, + {64, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + 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()), {TargetWidth, TargetHeight}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {TargetWidth, TargetHeight}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(TargetHeight), static_cast(TargetWidth), -static_cast(TargetHeight), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {TargetWidth, TargetHeight}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); + return {vertexResult.PipelineVariantId(), pixelResult.PipelineVariantId()}; +} + +void RunGraphicsTests(AgcDriver::VulkanDevice& device) { + const auto floatImage = Image(FloatTexels.data(), Format32Float); + const auto uintImage = Image(UintTexels.data(), Format32UInt); + const auto sintImage = Image(SintTexels.data(), Format32SInt); + GraphicsTable = {}; + std::copy(floatImage.begin(), floatImage.end(), GraphicsTable.begin()); + std::copy(uintImage.begin(), uintImage.end(), GraphicsTable.begin() + 4); + std::copy(sintImage.begin(), sintImage.end(), GraphicsTable.begin() + 8); + const auto first = DrawTables(device, 1u, 2u); + for (std::uint32_t vertex = 0; vertex < 3u; ++vertex) Require(VertexResults[vertex * 4u] == UintTexels[0], "image table graphics: the vertex stage read the wrong entry (" + Hex(VertexResults[vertex * 4u]) + ")"); + Require(PixelResults[0] == static_cast(SintTexels[0]), "image table graphics: the pixel stage read the wrong entry (" + Hex(PixelResults[0]) + ")"); + const auto second = DrawTables(device, 2u, 0u); + for (std::uint32_t vertex = 0; vertex < 3u; ++vertex) Require(VertexResults[vertex * 4u] == static_cast(SintTexels[0]), "image table graphics: the vertex stage read the wrong entry after the keys changed"); + Require(PixelResults[0] == std::bit_cast(FloatTexels[0]), "image table graphics: the pixel stage read the wrong entry after the keys changed"); + Require(first == second, "image table graphics: other table keys built another pipeline"); +} + +} + +int main() { + try { +#ifdef _WIN32 + _putenv_s("APS5_SRGB_SHADER_DECODE", "1"); +#else + setenv("APS5_SRGB_SHADER_DECODE", "1", 1); +#endif + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + RunTests(*device); + RunBorderSwizzleTests(*device); + RunAnisoOverrideTests(*device); + RunReadTests(*device); + RunLoopKeyTests(*device); + RunGuardedTableTests(*device); + RunSelectedRecordTests(*device); + RunModeTests(*device); + RunStoreTests(*device); + RunSelectedStoreTests(*device); + RunPointerTests(*device); + RunPointerRecordTests(*device); + RunPointerChainRecordTests(*device); + RunSplitRecordTests(*device); + RunPointerStorageWriteTests(*device); + RunGraphicsTests(*device); + std::puts("image table 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/ImageTableCache.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCache.cpp new file mode 100644 index 0000000000..2e2bf16ed9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCache.cpp @@ -0,0 +1,195 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp" +#include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp" +#include "Recompiler.hpp" +#include "BdaAbi.hpp" +#include "ImageTableAbi.hpp" +#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 AgcDriver::Graphics::Require; +namespace Abi = ShaderRecompiler::ImageTableAbi; + +constexpr std::uint32_t PageBytes = 4096; +constexpr std::uint32_t ReservedBytes = 65536; +constexpr std::uint32_t Stride = 48; +constexpr std::uint32_t Records = 4; +constexpr std::uint32_t TableBytes = Records * Stride; +constexpr std::uint32_t TableOffset = PageBytes - 2u * Stride; +constexpr std::uint32_t ImageOffset = 16; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type2D = 9; + +struct alignas(256) Texture { + std::array bytes{}; +}; + +Texture Texels; +alignas(256) std::array Srt{}; +alignas(256) std::array Output{}; + +const std::vector TableCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x9310b010u, + 0xf4280502u, 0x20000010u, 0xf09c8f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + +std::uint64_t Address(const void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::array Image(const void* base) { + const auto address = Address(base); + return {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u)}; +} + +std::array Buffer(const void* base, std::uint32_t bytes) { + const auto address = Address(base); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::byte* Reserve() { +#ifdef _WIN32 + auto* base = static_cast(VirtualAlloc(nullptr, ReservedBytes, MEM_RESERVE, PAGE_NOACCESS)); +#else + auto* mapped = mmap(nullptr, ReservedBytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + auto* base = mapped == MAP_FAILED ? nullptr : static_cast(mapped); +#endif + Require(base != nullptr, "image table cache: cannot reserve an inaccessible range"); + return base; +} + +void Commit(std::byte* page) { +#ifdef _WIN32 + const bool committed = VirtualAlloc(page, PageBytes, MEM_COMMIT, PAGE_READWRITE) == page; +#else + const bool committed = mprotect(page, PageBytes, PROT_READ | PROT_WRITE) == 0; +#endif + Require(committed, "image table cache: cannot commit a page"); +} + +void Release(std::byte* base) { +#ifdef _WIN32 + VirtualFree(base, 0, MEM_RELEASE); +#else + munmap(base, ReservedBytes); +#endif +} + +struct Captured { + std::unique_ptr memory; + std::shared_ptr capture; + std::vector regions; + std::shared_ptr compiled; +}; + +Captured Capture(std::byte* table) { + using namespace ShaderRecompiler; + static const std::array capabilities{29u, 5302u, 1u}; + static const std::array extensions{"SPV_EXT_descriptor_indexing"}; + const auto srtAddress = Address(Srt.data()); + const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x40000u + TableCode.size() * 4u, TableCode, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + const auto tableV = Buffer(table, TableBytes); + const auto outputV = Buffer(Output.data(), sizeof(Output)); + Srt = {}; + std::copy(tableV.begin(), tableV.end(), Srt.begin()); + std::copy(outputV.begin(), outputV.end(), Srt.begin() + 12); + + Captured result; + result.memory = std::make_unique(std::span{}); + result.capture = result.memory->Capture(request); + result.regions = result.memory->TakeRecentRegions(); + request.context.memory = result.regions; + result.compiled = Recompile(request, *result.capture); + return result; +} + +std::uint32_t Code(const Captured& captured, std::uint32_t record) { + const auto& bindings = captured.compiled->bindings; + const auto binding = std::find_if(bindings.begin(), bindings.end(), [](const ShaderRecompiler::DescriptorBinding& candidate) { return candidate.role == ShaderRecompiler::DescriptorRole::ImageTableMap; }); + Require(binding != bindings.end(), "image table cache: the result has no image table map"); + const auto& map = binding->guestDescriptor; + Require(record < map.at(Abi::TableHeader(0) + Abi::TableKeyCount), "image table cache: the record is outside the map"); + return map.at(map.at(Abi::TableHeader(0) + Abi::TableMapStart) + record); +} + +void SetRecord(std::byte* table, std::uint32_t record) { + const auto words = Image(Texels.bytes.data()); + std::memcpy(table + record * Stride + ImageOffset, words.data(), sizeof(words)); +} + +void RunTests() { + using AgcDriver::DriverDetail::CacheableResult; + auto* reserved = Reserve(); + Commit(reserved); + auto* table = reserved + TableOffset; + SetRecord(table, 0); + SetRecord(table, 1); + + const auto unmapped = Capture(table); + const auto& poison = unmapped.compiled->imageTablePoison; + const auto unmappedCode = Code(unmapped, 2); + Require((unmappedCode & Abi::PoisonFlag) != 0u && poison.at(unmappedCode & ~Abi::PoisonFlag).reason == static_cast(Abi::PoisonReason::Unmapped) && Code(unmapped, 3) == unmappedCode, "image table cache: records on an uncommitted page are not Unmapped poison"); + Require((Code(unmapped, 0) & Abi::PoisonFlag) == 0u && Code(unmapped, 0) == Code(unmapped, 1), "image table cache: a record on the committed page is poison"); + Require(!CacheableResult(*unmapped.compiled), "image table cache: a result with Unmapped entries is cacheable"); + + Commit(reserved + PageBytes); + SetRecord(table, 2); + SetRecord(table, 3); + for (const auto& region : unmapped.regions) { + Require(std::memcmp(reinterpret_cast(region.guestAddress), region.bytes.data(), region.bytes.size()) == 0, "image table cache: a word the Unmapped capture read changed"); + } + + const auto mapped = Capture(table); + Require(CacheableResult(*mapped.compiled), "image table cache: a result without Unmapped entries is not cacheable"); + Require(Code(mapped, 2) == Code(mapped, 0) && (Code(mapped, 2) & Abi::PoisonFlag) == 0u, "image table cache: a record on a newly committed page is not an image"); + Require(mapped.compiled->variantId == unmapped.compiled->variantId && mapped.compiled->PipelineVariantId() == unmapped.compiled->PipelineVariantId(), "image table cache: the committed records changed the compiled module"); + const auto& ranges = mapped.compiled->imageTableRanges; + Require(std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.first == Address(table) && range.second == TableBytes; }), "image table cache: the result does not name the table range"); + Release(reserved); +} + +} + +int main() { + try { + RunTests(); + std::puts("image table cache 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/ImageTableCacheBounds.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp new file mode 100644 index 0000000000..765947dc99 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp @@ -0,0 +1,409 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp" +#include "Optimization/ResourceMaterializer.hpp" +#include "Optimization/SrtWalker.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 +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Records = 65536; +constexpr std::uint32_t Stride = 32; +constexpr std::uint32_t Elements = 4096; +constexpr std::uint32_t Spare = 64; +constexpr std::uint32_t TextureBytes = 2048; +constexpr std::uint32_t Aliases = 256; +constexpr std::uint32_t Surfaces = (Elements + Spare + Aliases - 1) / Aliases; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t SrtBytes = 256; +constexpr std::size_t SrtSlots = 16; +constexpr std::size_t SrtOffset = 4096; +constexpr std::size_t OutputOffset = SrtOffset + SrtSlots * SrtBytes; +constexpr std::size_t HeapOffset = OutputOffset + 4096; +constexpr std::size_t TexturesOffset = HeapOffset + static_cast(Records) * Stride; +constexpr std::size_t BlockBytes = (TexturesOffset + static_cast(Surfaces) * TextureBytes + 65535) & ~static_cast(65535); + +constexpr std::uint32_t WideStride = 48; +constexpr std::uint32_t WideRecords = 32768; +constexpr std::size_t WideKernelOffset = 2048; +constexpr std::size_t WideHeapOffset = 65536; +constexpr std::array WideSlots{SrtSlots - 1u, SrtSlots - 2u, SrtSlots - 3u}; +constexpr std::array WideTableCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x9310b010u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; +constexpr std::array TableCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 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, "image table cache bounds: 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::array V(std::uint64_t address, std::uint32_t stride, std::uint32_t records) { + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), records, stride == 0u ? 0x31016facu : 0x01016facu}; +} + +void SetRecord(const GuestBlock& block, std::uint32_t record, std::uint32_t element) { + const auto address = block.At(TexturesOffset + static_cast(element / Aliases) * TextureBytes); + const std::array words{static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 7u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, element % Aliases, 0u}; + std::memcpy(block.Bytes(HeapOffset + static_cast(record) * Stride), words.data(), sizeof(words)); +} + +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 Dispatch(const GuestBlock& block, std::size_t slot, std::size_t kernelOffset = 0, std::vector words = {}) { + const auto kernel = block.At(kernelOffset); + const auto srt = block.At(SrtOffset + slot * 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, "image table cache bounds: the dispatch submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +void Prepare(const GuestBlock& block) { + std::memcpy(block.Bytes(0), TableCode.data(), sizeof(TableCode)); + for (std::uint32_t surface = 0; surface < Surfaces; ++surface) std::memset(block.Bytes(TexturesOffset + static_cast(surface) * TextureBytes), static_cast(surface), TextureBytes); + for (std::uint32_t record = 0; record < Records; ++record) SetRecord(block, record, record % Elements); + for (std::size_t slot = 0; slot < SrtSlots; ++slot) { + auto* srt = reinterpret_cast(block.Bytes(SrtOffset + slot * SrtBytes)); + std::fill(srt, srt + SrtBytes / 4u, 0u); + const auto heap = V(block.At(HeapOffset), Stride, Records); + const auto output = V(block.At(OutputOffset), 0u, 4096u); + std::copy(heap.begin(), heap.end(), srt); + std::copy(output.begin(), output.end(), srt + 12); + } +} + +void TablesChangingPerDispatch(const GuestBlock& block) { + auto& cache = AgcDriver::Graphics::SharedResourceCache(); + std::uint64_t most = 0; + for (std::uint32_t dispatch = 0; dispatch < 6; ++dispatch) { + SetRecord(block, dispatch * 977u % Records, Elements + dispatch % Spare); + Dispatch(block, 0); + most = std::max(most, cache.TableElements()); + } + Require(most >= Elements, "image table cache bounds: no dispatch kept its table in the resource cache"); + Require(most <= AgcDriver::Graphics::ResourceCache::TableElementBudget(), "image table cache bounds: tables that changed every dispatch kept " + std::to_string(most) + " table elements in the resource cache, over its budget of " + std::to_string(AgcDriver::Graphics::ResourceCache::TableElementBudget())); +} + +void SrtMovingPerDispatch(const GuestBlock& block) { + using AgcDriver::DriverDetail::Driver; + for (std::uint32_t record = 0; record < Records; ++record) SetRecord(block, record, record % 4u); + std::uint64_t most = 0; + std::size_t entries = 0; + for (std::size_t slot = 0; slot < SrtSlots; ++slot) { + Dispatch(block, slot); + const auto size = Driver::Get().DispatchCacheSize(); + most = std::max(most, size.variantBytes); + entries = std::max(entries, size.entries); + } + Require(entries >= 2, "image table cache bounds: the dispatches were not kept in the dispatch cache"); + Require(most <= Driver::DispatchCacheBytes(), "image table cache bounds: dispatches reading a 2 MiB table through " + std::to_string(SrtSlots) + " SRTs kept " + std::to_string(most >> 20u) + " MiB of captured words in the dispatch cache, over its budget of " + std::to_string(Driver::DispatchCacheBytes() >> 20u) + " MiB"); +} + +std::array TextureWords(const GuestBlock& block, std::uint32_t element) { + const auto address = block.At(TexturesOffset + static_cast(element / Aliases) * TextureBytes); + return {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 7u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, element % Aliases, 0u}; +} + +std::uint64_t WideRecord(const GuestBlock& block, std::uint32_t record) { + return block.At(WideHeapOffset + static_cast(record) * WideStride); +} + +void SetWideRecord(const GuestBlock& block, std::uint32_t record, std::uint32_t element) { + const auto words = TextureWords(block, element); + std::memcpy(block.Bytes(WideHeapOffset + static_cast(record) * WideStride), words.data(), sizeof(words)); +} + +void VouchedLookups(const GuestBlock& block) { + using AgcDriver::ShaderMemory; + using ShaderRecompiler::ResourceMaterializer; + std::memcpy(block.Bytes(WideKernelOffset), WideTableCode.data(), sizeof(WideTableCode)); + for (std::uint32_t record = 0; record < WideRecords; ++record) { + SetWideRecord(block, record, (1u + record % 15u) * Aliases); + std::memset(block.Bytes(WideHeapOffset + static_cast(record) * WideStride + 32u), 0x5a, WideStride - 32u); + } + for (const auto slot : WideSlots) { + auto* srt = reinterpret_cast(block.Bytes(SrtOffset + slot * SrtBytes)); + std::fill(srt, srt + SrtBytes / 4u, 0u); + const auto heap = V(block.At(WideHeapOffset), WideStride, WideRecords); + const auto output = V(block.At(OutputOffset), 0u, 4096u); + std::copy(heap.begin(), heap.end(), srt); + std::copy(output.begin(), output.end(), srt + 12); + } + struct Step { + std::uint64_t vouched = 0; + std::uint64_t refused = 0; + std::uint64_t columns = 0; + std::uint64_t reused = 0; + }; + const auto dispatch = [&](std::size_t slot, std::vector before = {}) { + const auto vouches = ShaderMemory::Vouches(); + const auto reuse = ResourceMaterializer::TableReuse(); + Dispatch(block, slot, WideKernelOffset, std::move(before)); + const auto after = ShaderMemory::Vouches(); + const auto reused = ResourceMaterializer::TableReuse(); + return Step{after.blocks - vouches.blocks, after.refused - vouches.refused, reused.columns - reuse.columns, (reused.unwritten - reuse.unwritten) + (reused.compared - reuse.compared)}; + }; + const auto text = [](const Step& step) { return " (" + std::to_string(step.vouched) + " vouched, " + std::to_string(step.refused) + " refused, " + std::to_string(step.columns) + " columns read, " + std::to_string(step.reused) + " reused)"; }; + const auto read = dispatch(WideSlots[0]); + Require(read.columns == 1u, "image table vouched lookups: the first dispatch did not read the strided table" + text(read)); + const auto shared = dispatch(WideSlots[1]); + Require(shared.columns == 0u && shared.reused == 1u, "image table vouched lookups: a second dispatch over the unwritten table did not reuse its column" + text(shared)); + const auto hit = dispatch(WideSlots[1]); + Require(hit.vouched >= 1u && hit.refused == 0u && hit.columns == 0u && hit.reused == 0u, "image table vouched lookups: a cached dispatch over an unwritten table was not validated by its table's vouch" + text(hit)); + + std::size_t vouchedSlot = WideSlots[1]; + std::size_t otherSlot = WideSlots[0]; + const auto recapture = [&](const std::string& what, std::vector before = {}) { + const auto changed = dispatch(vouchedSlot, std::move(before)); + Require(changed.refused >= 1u && changed.columns == 1u, "image table vouched lookups: " + what + " was hidden by the cached dispatch's vouch" + text(changed)); + const auto other = dispatch(otherSlot); + Require(other.columns == 0u && other.reused == 1u, "image table vouched lookups: the other dispatch after " + what + " did not reuse the column read again" + text(other)); + const auto again = dispatch(otherSlot); + Require(again.vouched >= 1u && again.refused == 0u && again.columns == 0u && again.reused == 0u, "image table vouched lookups: the dispatch after " + what + " was not vouched for again" + text(again)); + std::swap(vouchedSlot, otherSlot); + }; + SetWideRecord(block, 0u, 9u * Aliases); + recapture("a CPU store into a record"); + + const auto words = TextureWords(block, 10u * Aliases); + const auto target = WideRecord(block, 0u); + std::vector cpWrite{0xc0000000u | ((3u + static_cast(words.size()) - 1u) << 16u) | (0x37u << 8u), 0x00100200u, static_cast(target), static_cast(target >> 32u)}; + cpWrite.insert(cpWrite.end(), words.begin(), words.end()); + recapture("a GPU write into a record pending ahead of the dispatch", cpWrite); + Require(std::memcmp(block.Bytes(WideHeapOffset), words.data(), sizeof(words)) == 0, "image table vouched lookups: the GPU write did not land in the record"); + + const auto driverWords = TextureWords(block, 11u * Aliases); + AgcDriver::GuestMemory::Write(WideRecord(block, 0u), std::as_bytes(std::span(driverWords)), 4); + recapture("a driver store into a record"); + + *reinterpret_cast(block.Bytes(WideHeapOffset + 36u)) ^= 0x10u; + const auto gap = dispatch(vouchedSlot); + Require(gap.refused >= 1u && gap.columns == 0u && gap.reused == 0u, "image table vouched lookups: a store between two records did not refuse the vouch, or the table was captured again" + text(gap)); + const auto compared = dispatch(WideSlots[2]); + Require(compared.columns == 0u && compared.reused == 1u, "image table vouched lookups: a new dispatch after a store between two records did not compare and keep the column" + text(compared)); + const auto restamped = dispatch(vouchedSlot); + Require(restamped.vouched >= 1u && restamped.refused == 0u && restamped.columns == 0u, "image table vouched lookups: a dispatch sharing a column compared after a store between two records was not vouched for again" + text(restamped)); + std::puts("image table vouched lookups passed"); +} + +struct QueuedDraw { + std::atomic release{false}; + + void Enqueue(std::uint64_t begin, std::uint64_t end) { + AgcDriver::DriverDetail::DrawPipeline::Queue0().Enqueue([this] { + while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }, {{begin, end}}); + } +}; + +void VouchUnderQueuedDraws(const GuestBlock& block) { + using AgcDriver::DriverDetail::DrawPipeline; + using AgcDriver::DriverDetail::Driver; + Require(DrawPipeline::Depth() >= 3u, "image table vouch under queued draws: the draw pipeline is off"); + for (std::uint32_t record = 0; record < WideRecords; ++record) SetWideRecord(block, record, (1u + record % 15u) * Aliases); + const auto low = WideRecord(block, 0u); + const auto bytes = static_cast(WideRecord(block, WideRecords - 1u) + 32u - low); + static_cast(Driver::Get()); + const ShaderRecompiler::SrtWriteStamp stamp{AgcDriver::GuestMemory::CollectWrites(low, bytes), GuestAllocations::GuestAllocationsGeneration_nid_postfix()}; + Require(stamp.generation != 0u, "image table vouch under queued draws: the table is not write-watched"); + auto& pipeline = DrawPipeline::Queue0(); + QueuedDraw elsewhere; + QueuedDraw between; + QueuedDraw into; + const auto releaseAll = [&] { + elsewhere.release = true; + between.release = true; + into.release = true; + }; + DrawPipeline::Active() = true; + const auto vouch = [&](const std::string& what, bool expected) { + const auto outstanding = pipeline.Outstanding(); + std::atomic finished{false}; + std::atomic waited{false}; + std::thread watchdog([&] { + const auto start = std::chrono::steady_clock::now(); + while (!finished.load()) { + if (std::chrono::steady_clock::now() - start > std::chrono::seconds(5)) { + waited = true; + releaseAll(); + return; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + }); + const bool vouched = AgcDriver::ShaderMemory::Vouched(&Driver::QueryPendingSpan, low, bytes, stamp); + finished = true; + watchdog.join(); + const bool queued = !waited.load() && pipeline.Outstanding() == outstanding; + if (!queued || vouched != expected) releaseAll(); + Require(queued, "image table vouch under queued draws: " + what + ": the vouch waited for the queued draws"); + Require(vouched == expected, "image table vouch under queued draws: " + what + (expected ? ": the table was not vouched for" : ": the table was still vouched for")); + }; + const auto settle = [&](std::size_t outstanding) { + const auto start = std::chrono::steady_clock::now(); + while (pipeline.Outstanding() != outstanding) { + if (std::chrono::steady_clock::now() - start > std::chrono::seconds(20)) { + releaseAll(); + Require(false, "image table vouch under queued draws: the released draws were never committed"); + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + }; + vouch("an idle pipeline", true); + elsewhere.Enqueue(block.At(OutputOffset), block.At(OutputOffset) + 4096u); + vouch("a queued draw writing outside the table", true); + between.Enqueue(low + 32u, low + WideStride); + vouch("a queued draw writing between two records", false); + into.Enqueue(WideRecord(block, 7u), WideRecord(block, 7u) + 32u); + elsewhere.release = true; + between.release = true; + settle(1u); + vouch("a queued draw writing into a record", false); + into.release = true; + pipeline.Drain(DrawPipeline::DrainReason::Capture); + vouch("committed draws that wrote nothing", true); + DrawPipeline::Active() = false; + std::puts("image table vouch under queued draws passed"); +} + +void SetBudgets() { +#ifdef _WIN32 + _putenv_s("APS5_RESOURCE_CACHE_TABLE_ELEMENTS", "8192"); + _putenv_s("APS5_DISPATCH_CACHE_BYTES", "16777216"); +#else + setenv("APS5_RESOURCE_CACHE_TABLE_ELEMENTS", "8192", 1); + setenv("APS5_DISPATCH_CACHE_BYTES", "16777216", 1); +#endif +} + +#ifdef _WIN32 +bool WindowsImportsStayWatchedThroughReads(const AgcDriver::Graphics::Context& context) { + if (context.hostImportAlignment == 0) return true; + const auto probe = AgcDriver::Graphics::ProbeImportWriteProtection(context); + if (probe.failure == nullptr && probe.writtenAfterSubmit == 0) return true; + if (probe.failure != nullptr) { + std::printf("skipped, the host import probe failed at %s (%d)\n", probe.failure, static_cast(probe.result)); + } else { + std::printf( + "skipped, host imports on this device report %u of %u pages written at the import and " + "%u after a GPU read, so a watched imported table cannot be vouched for\n", + probe.writtenAtImport, probe.pages, probe.writtenAfterSubmit); + } + return false; +} +#else +bool ImportsStayWatchedThroughReads(const AgcDriver::Graphics::Context& context) { + if (context.hostImportAlignment == 0) return true; + const auto probe = AgcDriver::Graphics::ProbeImportWriteProtection(context); + if (probe.failure == nullptr && probe.writtenAfterSubmit == 0) return true; + if (probe.failure != nullptr) std::printf("skipped, the device reports no host import probe result: it failed at %s (%d)\n", probe.failure, static_cast(probe.result)); + else std::printf("skipped, the device reports %u of %u host-imported pages written after a GPU read, so a watched imported table cannot be vouched for\n", probe.writtenAfterSubmit, probe.pages); + return false; +} +#endif + +} + +int main(int argc, char** argv) { + SetBudgets(); + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; +#ifdef _WIN32 + if (argc == 2 && std::string(argv[1]) == "--vouched-lookups" && + !WindowsImportsStayWatchedThroughReads(device->graphicsContext())) return VulkanTestSkipped; +#else + if (argc == 2 && std::string(argv[1]) == "--vouched-lookups" && !ImportsStayWatchedThroughReads(device->graphicsContext())) return VulkanTestSkipped; +#endif + } + GuestBlock block; + Prepare(block); + if (argc == 2 && std::string(argv[1]) == "--vouched-lookups") { + VouchedLookups(block); + AgcDriverShutdown_nid_postfix(); + return 0; + } + if (argc == 2 && std::string(argv[1]) == "--queued-draws") { + VouchUnderQueuedDraws(block); + AgcDriverShutdown_nid_postfix(); + return 0; + } + TablesChangingPerDispatch(block); + SrtMovingPerDispatch(block); + AgcDriverShutdown_nid_postfix(); + std::puts("image table cache bounds 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/ImageTableReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableReuse.cpp new file mode 100644 index 0000000000..a46aeb00be --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableReuse.cpp @@ -0,0 +1,364 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 4; +constexpr std::uint32_t TextureBytes = Width * Height * 4; +constexpr std::uint32_t Stride = 48; +constexpr std::uint32_t Records = 48; +constexpr std::uint32_t Moved = 64; +constexpr std::uint32_t Lanes = 64; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t TexturesOffset = 65536; +constexpr std::size_t BlockBytes = TexturesOffset + 2 * 65536; +constexpr std::size_t UnimportableBytes = BlockBytes - 256; +constexpr std::uint64_t CpuWriteResolutions = 1; + +alignas(256) std::array Keys{}; +alignas(256) std::array Output{}; +alignas(256) std::array Srt{}; + +alignas(256) constexpr std::array TableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0x8f108202u, 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 StoreCode{ + 0x7e020d00u, 0x082802f7u, 0x7e3c0300u, 0x7e3e0280u, 0xf0201108u, 0x0001141eu, 0xbf810000u, +}; + +constexpr std::array PointClamp{0x92u, (4u * 256u) << 12u, 0u, 0u}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::array Image(const void* texels) { + const auto address = AddressOf(texels); + return {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (Format32Float << 20u) | (((Width - 1u) & 3u) << 30u), ((Width - 1u) >> 2u) | ((Height - 1u) << 14u) | (1u << 31u), 0xfacu | (Type2D << 28u)}; +} + +std::array Buffer(const void* base, std::uint32_t bytes) { + const auto address = AddressOf(base); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +class GuestBlock { +public: + explicit GuestBlock(std::size_t registered = BlockBytes) { +#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, "image table reuse: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, registered, 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 + } + + std::uint32_t* Table() const { return reinterpret_cast(block); } + float* Texels(std::uint32_t texture) const { return reinterpret_cast(block + TexturesOffset + static_cast(texture) * TextureBytes); } + +private: + std::uint8_t* block = nullptr; +}; + +void SetRecord(const GuestBlock& block, std::uint32_t record, std::uint32_t texture) { + auto* words = block.Table() + record * (Stride / 4u); + std::fill(words, words + Stride / 4u, 0u); + std::copy(PointClamp.begin(), PointClamp.end(), words); + const auto image = Image(block.Texels(texture)); + std::copy(image.begin(), image.end(), words + 4); +} + +void Fill(const GuestBlock& block, std::uint32_t texture, float bias) { + for (std::uint32_t texel = 0; texel < Width * Height; ++texel) block.Texels(texture)[texel] = bias + static_cast(texture) * 100.0f + static_cast(texel); +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, const GuestBlock& block) { + const auto srtAddress = AddressOf(Srt.data()); + const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; + const std::span code(TableCode); + const std::span table(block.Table(), Records * Stride / 4u); + const std::array memory{{ + {AddressOf(code.data()), std::as_bytes(code)}, + {srtAddress, std::as_bytes(std::span(Srt))}, + {AddressOf(table.data()), std::as_bytes(table)}, + {AddressOf(Keys.data()), std::as_bytes(std::span(Keys))}, + }}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {true, false, false}, false, 1}; + const ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, AddressOf(code.data()), code, 0, {}}, + {64, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + auto result = ShaderRecompiler::Recompile(request); + const auto binding = std::find_if(result.bindings.begin(), result.bindings.end(), [](const ShaderRecompiler::DescriptorBinding& candidate) { return candidate.role == ShaderRecompiler::DescriptorRole::ImageTable; }); + Require(binding != result.bindings.end() && binding->guestDescriptor.size() == static_cast(Records) * 8u, "image table reuse: the shader does not bind the whole table"); + return result; +} + +void SetOutput(const void* output) { + const auto view = Buffer(output, static_cast(sizeof(Output))); + std::copy(view.begin(), view.end(), Srt.begin() + 8); +} + +void Store(AgcDriver::VulkanDevice& device, const GuestBlock& block, std::uint32_t texture) { + std::vector userData(16, 0u); + const auto image = Image(block.Texels(texture)); + std::copy(image.begin(), image.end(), userData.begin() + 4); + const std::span code(StoreCode); + const std::array memory{{{AddressOf(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, AddressOf(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, {}, AddressOf(code.data())); + device.WaitIdle(); +} + +std::vector Dispatch(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& result, std::uint32_t record) { + Keys[0] = record; + std::fill(Output.begin(), Output.end(), 0xdeadbeefu); + device.Dispatch(result, 1, 1, 1, {}, AddressOf(TableCode.data())); + device.WaitIdle(); + std::vector red(Lanes); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) red[lane] = Output[lane * 4u]; + return red; +} + +std::vector Row(const float* texels) { + std::vector row(Lanes); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) row[lane] = std::bit_cast(texels[lane % Width]); + return row; +} + +struct Step { + std::uint64_t reused; + std::uint64_t resolved; +}; + +Step Since(const AgcDriver::Graphics::ImageTableReuseCounts& before) { + const auto now = AgcDriver::Graphics::ImageTableReuses(); + return {now.reused - before.reused, now.resolved - before.resolved}; +} + +void ExpectWrites(const AgcDriver::Graphics::ImageTableReuseCounts& before, std::uint64_t kept, std::uint64_t made, const std::string& what) { + static const bool keptSets = std::getenv("APS5_KEPT_DESCRIPTOR_SETS") == nullptr || std::string(std::getenv("APS5_KEPT_DESCRIPTOR_SETS")) != "0"; + const auto now = AgcDriver::Graphics::ImageTableReuses(); + const auto keptNow = now.writesKept - before.writesKept; + const auto madeNow = now.writesMade - before.writesMade; + if (!keptSets) { + Require(keptNow == 0u && madeNow == kept + made, "image table reuse: " + what + ": table descriptors were kept with kept sets turned off"); + return; + } + Require(keptNow == kept && madeNow == made, "image table reuse: " + what + ": " + std::to_string(keptNow) + " table descriptor writes kept and " + std::to_string(madeNow) + " made, expected " + std::to_string(kept) + " and " + std::to_string(made)); +} + +void Expect(const AgcDriver::Graphics::ImageTableReuseCounts& before, std::uint64_t reused, std::uint64_t resolved, const std::string& what) { + const auto step = Since(before); + Require(step.reused == reused && step.resolved == resolved, "image table reuse: " + what + ": " + std::to_string(step.reused) + " reused and " + std::to_string(step.resolved) + " resolved tables, expected " + std::to_string(reused) + " and " + std::to_string(resolved)); +} + +void RunImageStore(AgcDriver::VulkanDevice& device, const GuestBlock& block) { + for (std::uint32_t texture = 0; texture < Records; ++texture) { + Fill(block, texture, 0.125f); + SetRecord(block, texture, texture); + } + const auto srt = Buffer(block.Table(), Records * Stride); + std::copy(srt.begin(), srt.end(), Srt.begin()); + const auto result = Compile(device, block); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto before = AgcDriver::Graphics::ImageTableReuses(); + Require(Dispatch(device, result, 25) == Row(block.Texels(25)), "image table reuse: the table before an image store sampled the wrong entry"); + Require(Dispatch(device, result, 27) == Row(block.Texels(27)), "image table reuse: the table resolved again before an image store sampled the wrong entry"); + Require(Dispatch(device, result, 23) == Row(block.Texels(23)), "image table reuse: the table reused before an image store sampled the wrong entry"); + Store(device, block, 23); + std::vector stored(Width); + for (std::uint32_t x = 0; x < Width; ++x) stored[x] = -4.0f - static_cast(x); + Require(Dispatch(device, result, 23) == Row(stored.data()), "image table reuse: an entry an image store left results pending over kept its old texels"); + Expect(before, 3 - CpuWriteResolutions, CpuWriteResolutions + 1, "CPU writes to a new table's 64 KiB block, then an image store leaving results pending over an entry"); + AgcDriver::Graphics::StorageTexture::FlushPending(AddressOf(block.Texels(23)), TextureBytes, nullptr, "test"); + device.WaitIdle(); + Require(Row(block.Texels(23)) == Row(stored.data()), "image table reuse: the image store wrote the wrong values"); +} + +void RunTests(AgcDriver::VulkanDevice& device, const GuestBlock& block, const GuestBlock& stores) { + for (std::uint32_t texture = 0; texture < Records; ++texture) { + Fill(block, texture, 0.25f); + SetRecord(block, texture, texture); + } + Fill(block, Moved, 0.5f); + for (std::uint32_t key = 0; key < Records; ++key) Keys[key] = key; + const auto srt = Buffer(block.Table(), Records * Stride); + const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); + std::copy(srt.begin(), srt.end(), Srt.begin()); + std::copy(keys.begin(), keys.end(), Srt.begin() + 4); + SetOutput(Output.data()); + const auto result = Compile(device, block); + const bool watched = AgcDriver::GuestMemory::WriteWatched(); + + std::vector unbumped[2]; + auto before = AgcDriver::Graphics::ImageTableReuses(); + std::thread thread([&] { + unbumped[0] = Dispatch(device, result, 3); + unbumped[1] = Dispatch(device, result, 3); + }); + thread.join(); + Require(unbumped[0] == Row(block.Texels(3)) && unbumped[1] == Row(block.Texels(3)), "image table reuse: a thread without a collect epoch sampled the wrong entry"); + Expect(before, 0, 2, "a thread without a collect epoch"); + + AgcDriver::GuestMemory::BumpCollectEpoch(); + before = AgcDriver::Graphics::ImageTableReuses(); + Require(Dispatch(device, result, 5) == Row(block.Texels(5)), "image table reuse: the first dispatch of the epoch sampled the wrong entry"); + Require(Dispatch(device, result, 9) == Row(block.Texels(9)), "image table reuse: a dispatch reusing the table sampled the wrong entry"); + Require(Dispatch(device, result, 5) == Row(block.Texels(5)), "image table reuse: the third dispatch of the epoch sampled the wrong entry"); + if (!watched) { + Expect(before, 0, 3, "guest memory without a write watch"); + std::puts("guest memory has no write watch: image table reuse is not tested"); + return; + } + Expect(before, 2, 1, "three dispatches of one table in one epoch"); + if (std::getenv("APS5_NO_TABLE_REPLAY") == nullptr) Require(AgcDriver::Graphics::ImageTableReuses().replayed - before.replayed == 2u, "image table reuse: three dispatches of one table in one epoch did not replay the bound table twice"); + ExpectWrites(before, 3, 0, "three dispatches of one table in one epoch"); + + before = AgcDriver::Graphics::ImageTableReuses(); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); + Require(Dispatch(device, result, 7) == Row(block.Texels(7)), "image table reuse: the table after the texture cache was cleared sampled the wrong entry"); + Require(Dispatch(device, result, 9) == Row(block.Texels(9)), "image table reuse: the table resolved after the texture cache was cleared sampled the wrong entry"); + Expect(before, 1, 1, "a kept table whose textures the texture cache dropped"); + ExpectWrites(before, 1, 1, "a kept table whose textures the texture cache dropped"); + + before = AgcDriver::Graphics::ImageTableReuses(); + std::vector stored(Width * Height); + for (std::uint32_t texel = 0; texel < Width * Height; ++texel) stored[texel] = -1.0f - static_cast(texel); + AgcDriver::GuestMemory::Write(AddressOf(block.Texels(11)), std::as_bytes(std::span(stored))); + Require(Dispatch(device, result, 11) == Row(stored.data()), "image table reuse: an entry the driver stored over in the epoch kept its old texels"); + Require(Dispatch(device, result, 11) == Row(stored.data()), "image table reuse: the table resolved after a driver store sampled the wrong entry"); + Expect(before, 1, 1, "a driver store over an entry"); + + before = AgcDriver::Graphics::ImageTableReuses(); + Fill(block, 17, 0.75f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(Dispatch(device, result, 17) == Row(block.Texels(17)), "image table reuse: an entry the CPU wrote before the epoch kept its old texels"); + Require(Dispatch(device, result, 3) == Row(block.Texels(3)), "image table reuse: an entry stamped by another entry's collect sampled the wrong texels"); + Require(Dispatch(device, result, 17) == Row(block.Texels(17)), "image table reuse: the table of the new epoch sampled the wrong entry"); + Expect(before, 3 - CpuWriteResolutions, CpuWriteResolutions, "a CPU write collected in a new epoch, then two dispatches"); + + before = AgcDriver::Graphics::ImageTableReuses(); + SetRecord(block, 19, Moved); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto moved = Compile(device, block); + Require(Dispatch(device, moved, 19) == Row(block.Texels(Moved)), "image table reuse: a record pointed at another texture sampled the old one"); + Require(Dispatch(device, result, 19) == Row(block.Texels(19)), "image table reuse: the original table sampled the moved record"); + Require(Dispatch(device, moved, 19) == Row(block.Texels(Moved)), "image table reuse: the moved table sampled the wrong entry when reused"); + Require(Dispatch(device, result, 21) == Row(block.Texels(21)), "image table reuse: the original table sampled the wrong entry when reused"); + Expect(before, 2, 2, "two tables that differ in one record"); + ExpectWrites(before, 0, 4, "two tables that differ in one record, alternating"); + + SetOutput(block.Texels(13)); + const auto writer = Compile(device, block); + SetOutput(Output.data()); + before = AgcDriver::Graphics::ImageTableReuses(); + static_cast(Dispatch(device, writer, 7)); + const auto sampled = Dispatch(device, result, 13); + const std::vector written(block.Texels(13), block.Texels(13) + Width * Height); + Require(std::bit_cast(written[0]) == Row(block.Texels(7))[0], "image table reuse: the dispatch writing into an entry's texels stored the wrong values"); + Require(sampled == Row(written.data()), "image table reuse: an entry a dispatch wrote in the epoch kept its old texels"); + Expect(before, 1, 1, "a dispatch writing an entry's texels"); + Require(Dispatch(device, result, 15) == Row(block.Texels(15)), "image table reuse: the table after a dispatch wrote an entry sampled the wrong entry"); + + RunImageStore(device, stores); + + AgcDriver::Graphics::ReleaseTextureCaches(~std::uint64_t{0}); + Require(AgcDriver::Graphics::SampledTextureMemory() == 0u, "image table reuse: " + std::to_string(AgcDriver::Graphics::SampledTextureMemory()) + " bytes of sampled textures outlived a full release of the texture caches after the kept-table steps"); + + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(Dispatch(device, result, 29) == Row(block.Texels(29)), "image table reuse: the table before its textures were destroyed sampled the wrong entry"); + before = AgcDriver::Graphics::ImageTableReuses(); + Require(Dispatch(device, result, 29) == Row(block.Texels(29)), "image table reuse: the table built again before its textures were destroyed sampled the wrong entry"); + ExpectWrites(before, 1, 0, "a kept table built again before its textures were destroyed"); + AgcDriver::Graphics::ReleaseTextureCaches(~std::uint64_t{0}); + Require(AgcDriver::Graphics::SampledTextureMemory() == 0u, "image table reuse: " + std::to_string(AgcDriver::Graphics::SampledTextureMemory()) + " bytes of sampled textures outlived a full release of the texture caches after a table's set was kept"); + before = AgcDriver::Graphics::ImageTableReuses(); + Require(Dispatch(device, result, 29) == Row(block.Texels(29)), "image table reuse: a kept table whose textures were destroyed sampled the wrong entry"); + ExpectWrites(before, 0, 1, "a kept table whose textures were destroyed"); +} + +} + +int main() { + try { + GuestBlock block; + GuestBlock stores(UnimportableBytes); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + RunTests(*device, block, stores); + std::puts("image table reuse 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/ImageVolumeAddressOnLine.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageVolumeAddressOnLine.cpp new file mode 100644 index 0000000000..b9027457bf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageVolumeAddressOnLine.cpp @@ -0,0 +1,233 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type1D = 8; +constexpr std::uint32_t Type3D = 10; +constexpr std::uint32_t Depth = 2; +constexpr std::uint32_t SampleResult = 8; +constexpr std::uint32_t LoadResult = 12; +alignas(256) std::array Buffer{}; +alignas(256) std::array Line{}; +alignas(256) std::array Volume{}; + +alignas(256) constexpr std::array Code{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf09c0f10, 0x00610c02, + 0xf0001f10, 0x00011005, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, + 0x80000e01, 0xe070102c, 0x80000f01, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, + 0x80001201, 0xe070103c, 0x80001301, 0xbf810000, +}; + +alignas(256) constexpr std::array ImplicitLodCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf0800f10, 0x00610c02, + 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, + 0x80000f01, 0xbf810000, +}; + +alignas(256) constexpr std::array ExplicitLodCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xbf8c3f70, 0xf0900f10, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xbf810000, +}; + +constexpr std::array LineV{0.0f, 0.5f, 1.0f, 1.5f, -0.25f, 3.0f, 0.25f, 0.75f}; +constexpr std::array LineR{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}; +constexpr std::array LineZ{0u, 1u, 0u, 2u, 3u, 0u, 0xffffffffu}; + +struct Lane { + std::uint32_t x; + std::uint32_t y; +}; + +Lane LaneOf(std::uint32_t tid) { + return {(tid * 7u + 3u) % Size, (tid * 5u + 1u) % Size}; +} + +std::uint32_t Bits(float value) { + return std::bit_cast(value); +} + +void FillInput(bool volume) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto lane = LaneOf(tid); + const auto u = Bits((static_cast(lane.x) + 0.5f) / static_cast(Size)); + const auto v = volume ? Bits((static_cast(lane.y) + 0.5f) / static_cast(Size)) : Bits(LineV[tid % LineV.size()]); + const auto r = volume ? Bits((static_cast(tid % Depth) + 0.5f) / static_cast(Depth)) : Bits(LineR[(tid + 3u) % LineR.size()]); + const auto y = volume ? lane.y : LineY[tid % LineY.size()]; + const auto z = volume ? tid % Depth : LineZ[tid % LineZ.size()]; + const std::array words{u, v, r, lane.x, y, z}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } +} + +std::uint8_t SliceMarker(std::uint32_t z) { + return static_cast(z * 100u + 7u); +} + +std::array LineDescriptor() { + const auto address = static_cast(reinterpret_cast(Line.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + (Size - 1u) >> 2u, + 0xfacu | (Type1D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +std::array VolumeDescriptor() { + const auto address = static_cast(reinterpret_cast(Volume.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | (Type3D << 28u), + Depth - 1u, + 0u, + 0u, + 0u, + }; +} + +void FillTexture(std::span storage, const std::array& descriptor, std::uint32_t slices, std::uint32_t rows) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(descriptor)); + Require(geometry.guestBytes <= storage.size(), "image volume address on a line: the texture does not fit the texel storage"); + const auto& mip = geometry.mips.at(0); + std::fill(storage.begin(), storage.end(), 0xeeu); + for (std::uint32_t z = 0; z < slices; ++z) { + for (std::uint32_t y = 0; y < rows; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + auto* texel = &storage[geometry.GuestLayerOffset(z) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; + texel[0] = SliceMarker(z); + texel[1] = static_cast(x); + texel[2] = static_cast(y); + texel[3] = 255u; + } + } + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::array& texture) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto sampler = SamplerDescriptor(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Expect(std::uint32_t tid, std::uint32_t result, const std::array& expected, const std::string& name) { + for (std::uint32_t component = 0; component < 4u; ++component) { + const float value = std::bit_cast(Buffer[tid * Words + result + component]) * 255.0f; + Require(std::lround(value) == static_cast(expected[component]), name + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + } +} + +void CheckLine(const std::string& op) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::array texel{SliceMarker(0), LaneOf(tid).x, 0u, 255u}; + Expect(tid, SampleResult, texel, op + " 3d on a 1D texture with v " + std::to_string(LineV[tid % LineV.size()]) + " and r " + std::to_string(LineR[(tid + 3u) % LineR.size()])); + } +} + +void CheckLineLoad() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto z = LineZ[tid % LineZ.size()]; + const std::array texel = z == 0u ? std::array{SliceMarker(0), LaneOf(tid).x, 0u, 255u} : std::array{}; + Expect(tid, LoadResult, texel, "image_load 3d on a 1D texture with y " + std::to_string(static_cast(LineY[tid % LineY.size()])) + " and z " + std::to_string(static_cast(z))); + } +} + +void CheckVolume() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto lane = LaneOf(tid); + const std::array texel{SliceMarker(tid % Depth), lane.x, lane.y, 255u}; + Expect(tid, SampleResult, texel, "image_sample_lz 3d on a 3D texture"); + Expect(tid, LoadResult, texel, "image_load 3d on a 3D texture"); + } +} + +void RequireRefused(AgcDriver::VulkanDevice& device, std::span code, const std::string& name) { + std::string refusal; + try { + Run(device, code, LineDescriptor()); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find("incompatible with the static runtime image interface") != std::string::npos, name + " on a 1D texture was not refused: " + refusal); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexture(Line, LineDescriptor(), 1u, 1u); + FillInput(false); + Run(*device, Code, LineDescriptor()); + CheckLine("image_sample_lz"); + CheckLineLoad(); + FillInput(false); + Run(*device, ImplicitLodCode, LineDescriptor()); + CheckLine("image_sample"); + FillTexture(Volume, VolumeDescriptor(), Depth, Size); + FillInput(true); + Run(*device, Code, VolumeDescriptor()); + CheckVolume(); + RequireRefused(*device, ExplicitLodCode, "image_sample_l 3d"); + std::puts("image volume address on line 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/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/IndirectArgumentsPendingProducer.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectArgumentsPendingProducer.cpp new file mode 100644 index 0000000000..306f35cbd0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectArgumentsPendingProducer.cpp @@ -0,0 +1,377 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array AddressWriterCode{0x7e000202u, 0x7e020203u, 0x7e040204u, 0x7e060280u, 0xdc7c8000u, 0x00000003u, 0xbf810000u}; +alignas(256) std::array CounterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000281u, 0xe0c80000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array SpinCode{0xf4080200u, 0xfa000000u, 0xbe840381u, 0x9304ff04u, 0x0019660du, 0x8004ff04u, 0x3c6ef35fu, 0x80828102u, 0xbf078002u, 0xbf85fff9u, 0xbf8cc07fu, 0x7e000204u, 0xe0700000u, 0x80020000u, 0xbf810000u}; + +constexpr std::uint32_t Groups = 5; +constexpr std::array Stale{3u, 288u, 0xffffffffu}; +constexpr std::uint32_t SpinIterations = 1u << 20u; +constexpr std::uint32_t SpinDispatches = 16; +constexpr std::uint32_t ThreadDimensions = 0x8061u; +constexpr std::uint32_t GroupCounts = 0x8041u; +constexpr std::uint32_t Compute = 0x20u; +constexpr std::uint32_t OtherCompute = 0x28u; +constexpr std::uint32_t Graphics = 0u; +constexpr std::size_t GuestBytes = 1u << 20u; + +struct Slot { + alignas(256) std::array arguments{}; + alignas(256) std::array counter{}; + alignas(256) std::array counterDescriptor{}; + alignas(256) std::array spin{}; + alignas(256) std::array spinDescriptor{}; + alignas(256) std::uint32_t label = 0; + alignas(256) std::uint32_t done = 0; +}; + +enum class Order { SeparateSubmissions, OneSubmission, LabelWaitInOneSubmission, LabelWaitBetweenSubmissions, LabelWaitAcrossQueues, ArgumentLabelInEarlierBatch }; + +struct Variant { + const char* name; + std::uint32_t initiator; + Order order; +}; + +constexpr std::array Variants{{ + {"thread dimensions, separate submissions", ThreadDimensions, Order::SeparateSubmissions}, + {"thread dimensions, one submission", ThreadDimensions, Order::OneSubmission}, + {"thread dimensions, label wait in one submission", ThreadDimensions, Order::LabelWaitInOneSubmission}, + {"thread dimensions, label wait between submissions", ThreadDimensions, Order::LabelWaitBetweenSubmissions}, + {"thread dimensions, label wait across compute queues", ThreadDimensions, Order::LabelWaitAcrossQueues}, + {"group counts read on the CPU behind an argument label of an earlier graphics batch", GroupCounts, Order::ArgumentLabelInEarlierBatch}, +}}; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +std::array Descriptor(const void* pointer, std::uint32_t bytes) { + return {Low(pointer), High(pointer) & 0xffffu, bytes, 0x31016facu}; +} + +std::uint32_t Load(const std::uint32_t& value) { + return reinterpret_cast(value); +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Program(std::vector& words, const void* code, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (const std::uint32_t offset : {0x207u, 0x208u, 0x209u}) SetShaderRegister(words, offset, 1); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); +} + +void WriteData(std::vector& words, std::uint32_t* target, std::uint32_t value) { + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(target), High(target), value}); +} + +void WaitEqual(std::vector& words, std::uint32_t* target, std::uint32_t value) { + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(target), High(target), value, 0xffffffffu, 0x19u}); +} + +void Spin(std::vector& words, Slot& slot, std::uint32_t dispatches) { + for (std::uint32_t index = 0; index < dispatches; ++index) { + Program(words, SpinCode.data(), {Low(slot.spinDescriptor.data()), High(slot.spinDescriptor.data()), SpinIterations}); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, GroupCounts}); + } +} + +void Produce(std::vector& words, Slot& slot) { + Program(words, AddressWriterCode.data(), {Low(slot.arguments.data()), High(slot.arguments.data()), Groups, 1u, 1u}); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, GroupCounts}); +} + +void Consume(std::vector& words, Slot& slot, std::uint32_t initiator) { + Program(words, CounterCode.data(), {Low(slot.counterDescriptor.data()), High(slot.counterDescriptor.data())}); + words.insert(words.end(), {0xc0021102u, 1u, Low(slot.arguments.data()), High(slot.arguments.data())}); + words.insert(words.end(), {0xc0011600u, 0u, initiator}); +} + +void Submit(std::uint32_t queue, std::vector& words) { + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + if (queue == Graphics) Require(sceAgcDriverSubmitDcb(&packet) == 0, "graphics submit failed"); + else Require(sceAgcDriverSubmitAcb(queue, &packet) == 0, "compute submit failed"); +} + +ShaderRecompiler::RecompileResult Recompile(AgcDriver::VulkanDevice& device, Slot& slot, std::span code, std::initializer_list userData) { + const std::array memory{{ + {reinterpret_cast(code.data()), std::as_bytes(code)}, + {reinterpret_cast(slot.counterDescriptor.data()), std::as_bytes(std::span(slot.counterDescriptor))}, + }}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{1, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, std::span(userData.begin(), userData.size()), compute, std::nullopt, std::nullopt, memory}, + device.ComputeTarget(32), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +class ProducerSubmissionGate { +public: + explicit ProducerSubmissionGate(AgcDriver::VulkanDevice& device) : device(device) { +#ifdef _WIN32 + constexpr const char* libraryName = "vulkan-1.dll"; +#else + constexpr const char* libraryName = "libvulkan.so.1"; +#endif + library.reset(SDL_LoadObject(libraryName)); + Require(library != nullptr, "cannot load Vulkan for the producer submission gate"); + resolve = reinterpret_cast(SDL_LoadFunction(library.get(), "vkGetDeviceProcAddr")); + Require(resolve != nullptr, "cannot resolve vkGetDeviceProcAddr for the producer submission gate"); + const auto createSemaphore = function("vkCreateSemaphore"); + destroySemaphore = function("vkDestroySemaphore"); + signalSemaphore = reinterpret_cast(resolve(device.Device(), "vkSignalSemaphoreKHR")); + if (signalSemaphore == nullptr) signalSemaphore = function("vkSignalSemaphore"); + VkSemaphoreTypeCreateInfo type{VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO}; + type.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE; + VkSemaphoreCreateInfo info{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, &type}; + Require(createSemaphore(device.Device(), &info, nullptr, &semaphore) == VK_SUCCESS, "cannot create the producer submission gate"); + } + + ~ProducerSubmissionGate() { + if (worker.joinable()) { + worker.request_stop(); + worker.join(); + } + Release(); + try { + device.WaitIdle(); + } catch (...) { + } + destroySemaphore(device.Device(), semaphore, nullptr); + } + + void Arm(AgcDriver::Graphics::Recorder& recorder) { + recorder.Commands(); + recorder.HoldNextSubmit(semaphore, 1); + } + + void ReleaseAfterSubmission(AgcDriver::Graphics::Recorder& recorder, std::uint64_t serial) { + worker = std::jthread([this, &recorder, serial](std::stop_token stop) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!stop.stop_requested() && recorder.NewestSubmitted() < serial && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::microseconds(100)); + producerSubmitted = recorder.NewestSubmitted() >= serial; + Release(); + if (!producerSubmitted && !stop.stop_requested()) std::fprintf(stderr, "pending producer gate: watchdog expired with producer batch %llu unsubmitted (vkSignalSemaphore %d)\n", static_cast(serial), static_cast(releaseResult)); + }); + } + + void Finish() { + worker.join(); + Require(releaseResult == VK_SUCCESS, "cannot release the producer submission gate"); + Require(producerSubmitted, "the CPU argument read did not submit the later leased producer before waiting for its earlier argument label"); + device.WaitIdle(); + } + +private: + template + TFunction function(const char* name) const { + const auto function = reinterpret_cast(resolve(device.Device(), name)); + Require(function != nullptr, std::string("cannot resolve ") + name + " for the producer submission gate"); + return function; + } + + void Release() { + if (signaled.exchange(true)) return; + VkSemaphoreSignalInfo info{VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO}; + info.semaphore = semaphore; + info.value = 1; + releaseResult = signalSemaphore(device.Device(), &info); + } + + AgcDriver::VulkanDevice& device; + std::unique_ptr library{nullptr, &SDL_UnloadObject}; + PFN_vkGetDeviceProcAddr resolve = nullptr; + PFN_vkDestroySemaphore destroySemaphore = nullptr; + PFN_vkSignalSemaphore signalSemaphore = nullptr; + VkSemaphore semaphore = VK_NULL_HANDLE; + std::jthread worker; + std::atomic signaled{false}; + bool producerSubmitted = false; + VkResult releaseResult = VK_NOT_READY; +}; + +void RunArgumentLabel(Slot& slot) { + AgcDriverShutdown_nid_postfix(); + slot.arguments = {3, 1, 1, 0}; + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, "cannot open the argument-label test device"); + const auto producer = Recompile(*device, slot, AddressWriterCode, {Low(slot.arguments.data()), High(slot.arguments.data()), Groups, 1, 1}); + const auto consumer = Recompile(*device, slot, CounterCode, {Low(slot.counterDescriptor.data()), High(slot.counterDescriptor.data())}); + const auto preparedProducer = device->PrepareDispatch(producer, {}, 1, reinterpret_cast(AddressWriterCode.data())); + const auto preparedConsumer = device->PrepareDispatch(consumer, {}, 0, reinterpret_cast(CounterCode.data())); + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "the argument-label test has no active recorder"); + ProducerSubmissionGate gate(*device); + gate.Arm(*recorder); + const std::uint32_t labelValue = 2; + const auto labelAddress = reinterpret_cast(&slot.arguments[1]); + Require(device->WriteLabelOnGpu(labelAddress, std::as_bytes(std::span(&labelValue, 1)), 1, Graphics, false) == 0, "the argument label was not recorded on the GPU"); + device->SubmitRecorded(false); + const auto producerSerial = recorder->OpenSerial(); + gate.ReleaseAfterSubmission(*recorder, producerSerial); + const auto deferredBefore = AgcDriver::Graphics::LeaseCounters().deferred; + device->Dispatch(producer, 1, 1, 1, {}, reinterpret_cast(AddressWriterCode.data()), preparedProducer); + Require(AgcDriver::Graphics::LeaseCounters().deferred > deferredBefore, "the address-based producer did not defer its registry lease"); + Require(recorder->Submissions() < producerSerial && recorder->Recording(), "the producer was submitted before the CPU argument decision"); + Require(recorder->PendingLabel(labelAddress, 4, 0).has_value(), "the earlier argument label retired before the CPU argument decision"); + const auto outcome = device->DispatchIndirect(consumer, reinterpret_cast(slot.arguments.data()), {}, reinterpret_cast(CounterCode.data()), preparedConsumer); + Require(outcome.cpuReason == 2, "the indirect dispatch did not take the pending-label CPU fallback (reason " + std::to_string(outcome.cpuReason) + ")"); + std::fprintf(stderr, "pending producer CPU fallback: cpuReason %d\n", outcome.cpuReason); + gate.Finish(); + slot.done = 1; +} + +void Run(Slot& slot, const Variant& variant) { + slot.arguments = {Stale[0], Stale[1], Stale[2], 0}; + slot.counter = {}; + slot.counterDescriptor = Descriptor(slot.counter.data(), sizeof(slot.counter)); + slot.spinDescriptor = Descriptor(slot.spin.data(), sizeof(slot.spin)); + slot.label = 0; + slot.done = 0; + std::vector first; + std::vector second; + switch (variant.order) { + case Order::SeparateSubmissions: + Spin(first, slot, SpinDispatches); + Produce(first, slot); + Consume(second, slot, variant.initiator); + WriteData(second, &slot.done, 1); + Submit(Compute, first); + Submit(Compute, second); + break; + case Order::OneSubmission: + Spin(first, slot, SpinDispatches); + Produce(first, slot); + Consume(first, slot, variant.initiator); + WriteData(first, &slot.done, 1); + Submit(Compute, first); + break; + case Order::LabelWaitInOneSubmission: + Spin(first, slot, SpinDispatches); + Produce(first, slot); + WriteData(first, &slot.label, 1); + WaitEqual(first, &slot.label, 1); + Consume(first, slot, variant.initiator); + WriteData(first, &slot.done, 1); + Submit(Compute, first); + break; + case Order::LabelWaitBetweenSubmissions: + Spin(first, slot, SpinDispatches); + Produce(first, slot); + WriteData(first, &slot.label, 1); + WaitEqual(second, &slot.label, 1); + Consume(second, slot, variant.initiator); + WriteData(second, &slot.done, 1); + Submit(Compute, first); + Submit(Compute, second); + break; + case Order::LabelWaitAcrossQueues: + Spin(first, slot, SpinDispatches); + Produce(first, slot); + WriteData(first, &slot.label, 1); + WaitEqual(second, &slot.label, 1); + Consume(second, slot, variant.initiator); + WriteData(second, &slot.done, 1); + Submit(OtherCompute, first); + Submit(Compute, second); + break; + case Order::ArgumentLabelInEarlierBatch: + RunArgumentLabel(slot); + break; + } + if (variant.order != Order::ArgumentLabelInEarlierBatch) AgcDriverWaitIdle_nid_postfix(); + const auto start = std::chrono::steady_clock::now(); + while (Load(slot.done) == 0) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(10), std::string(variant.name) + ": the consumer's completion label did not land"); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + Require(Load(slot.arguments[0]) == Groups && Load(slot.arguments[1]) == 1 && Load(slot.arguments[2]) == 1, std::string(variant.name) + ": the producer's address-based store did not write the indirect arguments"); + Require(Load(slot.counter[0]) == Groups, std::string(variant.name) + ": the indirect dispatch ran " + std::to_string(Load(slot.counter[0])) + " threads, expected the " + std::to_string(Groups) + " its producer's address-based store wrote"); +} + +} + +int main(int argc, char** argv) { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, GuestBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* block = static_cast(std::aligned_alloc(GuestBytes, GuestBytes)); +#endif + Require(block != nullptr, "cannot allocate guest memory"); + GuestAllocations::Mutation().Add(block, GuestBytes, true, true, true); + auto* slots = new (block) Slot[Variants.size()]; + for (std::size_t index = 0; index < Variants.size(); ++index) { + if (argc > 1 && std::to_string(index) != argv[1]) continue; + Run(slots[index], Variants[index]); + std::cout << Variants[index].name << ": passed\n"; + } + std::cout << "indirect arguments pending producer tests passed\n"; + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/IndirectDispatchLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectDispatchLimit.cpp new file mode 100644 index 0000000000..54aba72adc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectDispatchLimit.cpp @@ -0,0 +1,103 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.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::size_t BlockBytes = 65536; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array StoreCode{0x34020082u, 0x7e040287u, 0xe0701000u, 0x80010201u, 0xbf810000u}; + +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), count * 4u, 0x31016facu}; +} + +std::optional> OverLimit(const std::uint32_t* limit) { + for (std::size_t axis = 0; axis < 3; ++axis) { + if (limit[axis] == 0xffffffffu) continue; + std::array groups{1u, 1u, 1u}; + groups[axis] = limit[axis] + 1u; + return groups; + } + return std::nullopt; +} + +int Run(AgcDriver::VulkanDevice& device, std::uint32_t* block, std::array groups) { + Output.fill(0xdeadbeefu); + std::memcpy(block, groups.data(), sizeof(groups)); + std::vector userData(8, 0u); + const auto output = BufferDescriptor(Output.data(), Threads); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(StoreCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {64, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + const auto outcome = device.DispatchIndirect(result, reinterpret_cast(block), {}, reinterpret_cast(code.data())); + device.WaitIdle(); + return outcome.cpuReason; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + auto* block = static_cast(::operator new(BlockBytes, std::align_val_t{BlockBytes})); + std::memset(block, 0, BlockBytes); + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, BlockBytes, true, true, true); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + int failures = 0; + try { + const auto over = OverLimit(device->WorkGroupCountLimit()); + const int invalidPath = over ? Run(*device, block, *over) : 0; + const auto overText = over ? std::to_string((*over)[0]) + "x" + std::to_string((*over)[1]) + "x" + std::to_string((*over)[2]) : std::string(); + if (!over) { + std::puts("skipped, every group count is within the device limits"); + } else if (invalidPath != 0) { + std::printf("skipped, the group counts were read on the CPU (reason %d): the device cannot import the block\n", invalidPath); + } else { + for (std::uint32_t tid = 0; tid < Threads; ++tid) Require(Output[tid] == 0xdeadbeefu, "indirect dispatch limit: thread " + std::to_string(tid) + " ran with " + overText + " groups"); + Require(Run(*device, block, {1u, 1u, 1u}) == 0, "indirect dispatch limit: valid counts left the GPU path"); + for (std::uint32_t tid = 0; tid < Threads; ++tid) Require(Output[tid] == 7u, "indirect dispatch limit: thread " + std::to_string(tid) + " stored " + std::to_string(Output[tid]) + " with one group, expected 7"); + } + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + failures = 1; + } + mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + ::operator delete(block, std::align_val_t{BlockBytes}); + if (failures != 0) return 1; + std::puts("indirect dispatch limit 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/IndirectThreadDimensions.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectThreadDimensions.cpp new file mode 100644 index 0000000000..37573087cf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectThreadDimensions.cpp @@ -0,0 +1,214 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array AddressWriterCode{0x7e000202u, 0x7e020203u, 0x7e040204u, 0x7e060280u, 0xdc7c8000u, 0x00000003u, 0xbf810000u}; +alignas(256) std::array CounterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000281u, 0xe0c80000u, 0x80020000u, 0xbf810000u}; + +constexpr std::uint32_t ThreadDimensions = 0x8061u; +constexpr std::uint32_t GroupCounts = 0x8041u; +constexpr std::uint32_t Compute = 0x20u; +constexpr std::size_t GuestBytes = 1u << 20u; + +struct Slot { + alignas(256) std::array arguments{}; + alignas(256) std::array counter{}; + alignas(256) std::array counterDescriptor{}; + alignas(256) std::uint32_t done = 0; +}; + +struct Shape { + const char* name; + std::array threads; + std::array group; +}; + +constexpr std::array Shapes{{ + {"100x1x1 threads in groups of 64x1x1", {100u, 1u, 1u}, {64u, 1u, 1u}}, + {"5x3x2 threads in groups of 4x2x2", {5u, 3u, 2u}, {4u, 2u, 2u}}, + {"64x2x1 threads in groups of 32x1x1", {64u, 2u, 1u}, {32u, 1u, 1u}}, + {"0x7x1 threads in groups of 8x1x1", {0u, 7u, 1u}, {8u, 1u, 1u}}, +}}; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +std::array Descriptor(const void* pointer, std::uint32_t bytes) { + return {Low(pointer), High(pointer) & 0xffffu, bytes, 0x31016facu}; +} + +std::uint32_t Load(const std::uint32_t& value) { + return reinterpret_cast(value); +} + +std::uint32_t Expected(const Shape& shape) { + return shape.threads[0] * shape.threads[1] * shape.threads[2]; +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Program(std::vector& words, const void* code, std::array group, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (std::uint32_t axis = 0; axis < 3; ++axis) SetShaderRegister(words, 0x207u + axis, group[axis]); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); +} + +void Reset(Slot& slot) { + slot.arguments = {0xffffffffu, 0xffffffffu, 0xffffffffu, 0}; + slot.counter = {}; + slot.counterDescriptor = Descriptor(slot.counter.data(), sizeof(slot.counter)); + slot.done = 0; +} + +void Check(const Slot& slot, const Shape& shape, const std::string& name) { + const auto start = std::chrono::steady_clock::now(); + while (Load(slot.done) == 0) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(10), name + ": the completion label did not land"); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + Require(Load(slot.counter[0]) == Expected(shape), name + ": " + std::to_string(Load(slot.counter[0])) + " threads ran, expected " + std::to_string(Expected(shape))); +} + +void RunSubmitted(Slot& slot, const Shape& shape, bool producedOnGpu) { + Reset(slot); + if (!producedOnGpu) { + for (std::size_t axis = 0; axis < 3; ++axis) slot.arguments[axis] = shape.threads[axis]; + } + std::vector words; + if (producedOnGpu) { + Program(words, AddressWriterCode.data(), {1u, 1u, 1u}, {Low(slot.arguments.data()), High(slot.arguments.data()), shape.threads[0], shape.threads[1], shape.threads[2]}); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, GroupCounts}); + } + Program(words, CounterCode.data(), shape.group, {Low(slot.counterDescriptor.data()), High(slot.counterDescriptor.data())}); + words.insert(words.end(), {0xc0021102u, 1u, Low(slot.arguments.data()), High(slot.arguments.data())}); + words.insert(words.end(), {0xc0011600u, 0u, ThreadDimensions}); + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(&slot.done), High(&slot.done), 1u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(Compute, &packet) == 0, "compute submit failed"); + AgcDriverWaitIdle_nid_postfix(); + Check(slot, shape, std::string(shape.name) + (producedOnGpu ? ", arguments stored by the GPU" : ", arguments stored by the CPU")); +} + +ShaderRecompiler::RecompileResult Recompile(AgcDriver::VulkanDevice& device, std::span code, std::span userData, std::span memory, std::array group, std::array partialThreads, std::uint32_t pushBytes) { + const ShaderRecompiler::ShaderComputeStageInfo compute{group, 0, {false, false, false}, false, 1, partialThreads}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.ComputeTarget(32), + {0, 0, 0, pushBytes} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void RunCpuFallback(Slot& slot, const Shape& shape) { + AgcDriverShutdown_nid_postfix(); + Reset(slot); + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, "cannot open the CPU fallback test device"); + const std::array producerData{Low(slot.arguments.data()), High(slot.arguments.data()), shape.threads[0], shape.threads[1], shape.threads[2]}; + const std::array producerMemory{{{reinterpret_cast(AddressWriterCode.data()), std::as_bytes(std::span(AddressWriterCode))}}}; + const auto producer = Recompile(*device, AddressWriterCode, producerData, producerMemory, {1u, 1u, 1u}, {}, 128u); + const std::array consumerData{Low(slot.counterDescriptor.data()), High(slot.counterDescriptor.data())}; + const std::array consumerMemory{{ + {reinterpret_cast(CounterCode.data()), std::as_bytes(std::span(CounterCode))}, + {reinterpret_cast(slot.counterDescriptor.data()), std::as_bytes(std::span(slot.counterDescriptor))}, + }}; + const auto consumer = Recompile(*device, CounterCode, consumerData, consumerMemory, shape.group, {~0u, ~0u, ~0u}, 0u); + const auto preparedProducer = device->PrepareDispatch(producer, {}, 1, reinterpret_cast(AddressWriterCode.data())); + const auto preparedConsumer = device->PrepareDispatch(consumer, {}, 0, reinterpret_cast(CounterCode.data())); + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "the CPU fallback test has no active recorder"); + device->Dispatch(producer, 1, 1, 1, {}, reinterpret_cast(AddressWriterCode.data()), preparedProducer); + const auto labelAddress = reinterpret_cast(&slot.arguments[2]); + Require(device->WriteLabelOnGpu(labelAddress, std::as_bytes(std::span(&shape.threads[2], 1)), 1, Compute, false) == 0, "the argument label was not recorded on the GPU"); + const auto outcome = device->DispatchIndirect(consumer, reinterpret_cast(slot.arguments.data()), {}, reinterpret_cast(CounterCode.data()), preparedConsumer, nullptr, shape.group); + Require(outcome.cpuReason == 2, std::string(shape.name) + ": the indirect dispatch did not take the pending-label CPU fallback (reason " + std::to_string(outcome.cpuReason) + ")"); + recorder->Sync(); + slot.done = 1; + Check(slot, shape, std::string(shape.name) + ", read on the CPU behind a pending argument label"); +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, GuestBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* block = static_cast(std::aligned_alloc(GuestBytes, GuestBytes)); +#endif + Require(block != nullptr, "cannot allocate guest memory"); + GuestAllocations::Mutation().Add(block, GuestBytes, true, true, true); + auto* slot = new (block) Slot{}; + const bool gpuThreadDimensions = std::getenv("APS5_NO_GPU_THREAD_DIMENSIONS") == nullptr; + for (const auto& shape : Shapes) { + if (gpuThreadDimensions) RunSubmitted(*slot, shape, true); + RunSubmitted(*slot, shape, false); + std::cout << shape.name << ": passed\n"; + } + if (gpuThreadDimensions) { + for (const auto& shape : Shapes) { + RunCpuFallback(*slot, shape); + std::cout << shape.name << ", CPU fallback: passed\n"; + } + } + std::cout << "indirect thread dimension tests passed\n"; + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/InterpolationModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationModes.cpp index cb363ba335..9c332e8aaa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationModes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationModes.cpp @@ -6,9 +6,13 @@ #include "VulkanTestDevice.hpp" #include #include +#include +#include #include #include #include +#include +#include #include namespace { @@ -65,15 +69,19 @@ constexpr std::array Rows{{ constexpr std::uint32_t Words = 12; constexpr std::uint32_t Sentinel = 0xdeadbeefu; -alignas(256) std::array Data{}; +constexpr std::uint32_t RandomRows = 2048; +constexpr std::uint32_t RandomModes = 32; +constexpr std::uint32_t RandomWords = 9 + RandomModes * 3; +alignas(256) std::array Data{}; -RecompileResult Compile(std::uint32_t mode) { +RecompileResult Compile(std::span modes, bool integer, bool raw, std::uint32_t words) { IrProgram program; const ShaderStageInputInfo inputs{}; SpirvEmitterState state(program, inputs); + state.integerInterpolation = integer; auto& module = state.module; module.EmitCapability(spv::CapabilityShader); - module.EmitCapability(spv::CapabilityFloat64); + module.EmitCapability(integer ? spv::CapabilityInt64 : spv::CapabilityFloat64); module.EmitCapability(spv::CapabilitySignedZeroInfNanPreserve); module.EmitExtension("SPV_KHR_float_controls"); module.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450); @@ -92,14 +100,15 @@ RecompileResult Compile(std::uint32_t mode) { const auto voidType = module.Type(spv::OpTypeVoid); module.EmitEntryPoint(spv::ExecutionModelGLCompute, main, "main", {invocation}); module.AddExecutionMode(main, spv::ExecutionModeLocalSize, static_cast(Rows.size()), 1u, 1u); - for (const auto width : {32u, 64u}) module.AddExecutionMode(main, spv::ExecutionModeSignedZeroInfNanPreserve, width); + module.AddExecutionMode(main, spv::ExecutionModeSignedZeroInfNanPreserve, 32u); + if (!integer) module.AddExecutionMode(main, spv::ExecutionModeSignedZeroInfNanPreserve, 64u); module.AddFunction(spv::OpFunction, voidType, main, spv::FunctionControlMaskNone, module.Type(spv::OpTypeFunction, voidType)); module.AddFunction(spv::OpLabel, module.AllocateId()); const auto id = module.AllocateId(); module.AddFunction(spv::OpLoad, TypeU32Vector(state, 3u), id, invocation); const auto lane = module.AllocateId(); module.AddFunction(spv::OpCompositeExtract, u32, lane, id, 0u); - const auto rowBase = Binary(state, spv::OpIMul, u32, lane, ConstantU32(state, Words)); + const auto rowBase = Binary(state, spv::OpIMul, u32, lane, ConstantU32(state, words)); const auto address = [&](std::uint32_t offset) { const auto pointer = module.AllocateId(); module.AddFunction(spv::OpAccessChain, element, pointer, buffer, ConstantU32(state, 0u), Binary(state, spv::OpIAdd, u32, rowBase, ConstantU32(state, offset))); @@ -113,14 +122,18 @@ RecompileResult Compile(std::uint32_t mode) { IrProgram constants; IrBuilder builder(constants); builder.SetInsertionPoint(constants.CreateBlock()); - const auto* flags = &builder.Constant(mode); - for (std::uint32_t op = 0; op < 3u; ++op) { - const auto delta = load(op * 3u); - const auto coordinate = load(op * 3u + 1u); - const auto base = load(op * 3u + 2u); - const auto value = op == 1u ? EmitFPInterpolateF16(state, delta, coordinate, base, flags) : EmitFPInterpolateF32(state, delta, coordinate, base, flags); - const auto result = op == 1u ? EmitConvertF16F32(state, value) : Unary(state, spv::OpBitcast, u32, value); - module.AddFunction(spv::OpStore, address(9u + op), result); + std::array operands{}; + for (std::uint32_t index = 0; index < operands.size(); ++index) operands[index] = load(index); + for (std::uint32_t slot = 0; slot < modes.size(); ++slot) { + const auto* flags = &builder.Constant(modes[slot]); + for (std::uint32_t op = 0; op < 3u; ++op) { + const auto delta = operands[op * 3u]; + const auto coordinate = operands[op * 3u + 1u]; + const auto base = operands[op * 3u + 2u]; + const auto value = op == 1u ? EmitFPInterpolateF16(state, delta, coordinate, base, flags) : EmitFPInterpolateF32(state, delta, coordinate, base, flags); + const auto result = op == 1u && !raw ? EmitConvertF16F32(state, value) : Unary(state, spv::OpBitcast, u32, value); + module.AddFunction(spv::OpStore, address(9u + slot * 3u + op), result); + } } module.AddFunction(spv::OpReturn); module.AddFunction(spv::OpFunctionEnd); @@ -132,10 +145,11 @@ RecompileResult Compile(std::uint32_t mode) { return result; } -void Run(AgcDriver::VulkanDevice& device, std::uint32_t mode) { +void Run(AgcDriver::VulkanDevice& device, std::uint32_t mode, bool integer) { Data.fill(Sentinel); for (std::uint32_t row = 0; row < Rows.size(); ++row) std::copy(Rows[row].input.begin(), Rows[row].input.end(), Data.begin() + row * Words); - device.Dispatch(Compile(ModeFlags[mode]), 1u, 1u, 1u); + const std::array modes{ModeFlags[mode]}; + device.Dispatch(Compile(modes, integer, false, Words), 1u, 1u, 1u); device.WaitIdle(); for (std::uint32_t row = 0; row < Rows.size(); ++row) { for (std::uint32_t input = 0; input < 9u; ++input) Require(Data[row * Words + input] == Rows[row].input[input], "interpolation modes changed an input"); @@ -143,7 +157,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t mode) { const auto actual = Data[row * Words + 9u + op]; const auto expected = Rows[row].expected[mode][op]; char message[200]; - std::snprintf(message, sizeof(message), "%s, %s, row %u: got 0x%08x, expected 0x%08x", OpNames[op], ModeNames[mode], row, actual, expected); + std::snprintf(message, sizeof(message), "%s (%s), %s, row %u: got 0x%08x, expected 0x%08x", OpNames[op], integer ? "integer" : "Float64", ModeNames[mode], row, actual, expected); Require(actual == expected, message); } } @@ -152,17 +166,155 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t mode) { } +std::uint32_t Flush(std::uint32_t bits, bool flush, std::uint32_t smallestNormal) { + const auto magnitude = bits & 0x7fffffffu; + return flush && magnitude != 0u && magnitude < smallestNormal ? bits & 0x80000000u : bits; +} + +bool NonFinite(std::uint32_t bits) { return (bits & 0x7f800000u) == 0x7f800000u; } +bool Nan(std::uint32_t bits) { return (bits & 0x7fffffffu) > 0x7f800000u; } +bool Infinite(std::uint32_t bits) { return (bits & 0x7fffffffu) == 0x7f800000u; } +bool Zero(std::uint32_t bits) { return (bits & 0x7fffffffu) == 0u; } + +std::uint32_t Reference(std::uint32_t delta, std::uint32_t coordinate, std::uint32_t base, bool half, std::uint32_t mode) { + const bool quiet = half || (mode & InterpolationQuiet) != 0u; + const bool flush32 = (mode & InterpolationFlush32) != 0u; + const bool flush16 = (mode & InterpolationFlush16) != 0u; + const std::uint32_t rounding = half ? (mode >> InterpolationRound16Shift) & 3u : 0u; + const auto d = Flush(delta, flush16, 0x38800000u); + const auto c = Flush(coordinate, flush32, 0x00800000u); + const auto b = half ? Flush(base, flush32, 0x00800000u) : Flush(base, flush16, 0x38800000u); + const double fd = std::bit_cast(d); + const double fc = std::bit_cast(c); + const double fb = std::bit_cast(b); + std::uint32_t result = 0; + if (NonFinite(d) || NonFinite(c) || NonFinite(b)) { + const double unbounded = fd * fc + fb; + result = std::isnan(unbounded) ? 0xffc00000u : (std::signbit(unbounded) ? 0xff800000u : 0x7f800000u); + const auto quieted = [&](std::uint32_t bits) { return quiet ? bits | 0x00400000u : bits; }; + const bool invalid = (Infinite(d) && Zero(c)) || (Zero(d) && Infinite(c)); + if (Nan(b) && !invalid) result = quieted(b); + if (Nan(c)) result = quieted(c); + if (Nan(d)) result = quieted(d); + } else { + const double product = fd * fc; + double sum = product + fb; + const double back = sum - product; + const double error = (product - (sum - back)) + (fb - back); + if (error != 0.0 && (std::bit_cast(sum) & 1u) == 0u) sum = std::nextafter(sum, error > 0.0 ? std::numeric_limits::infinity() : -std::numeric_limits::infinity()); + if (sum == 0.0 && rounding == 2u && !(product == 0.0 && !std::signbit(product) && fb == 0.0 && !std::signbit(fb))) sum = -0.0; + double rounded = sum; + if (sum != 0.0) { + const int precision = half ? 11 : 24; + const int smallest = half ? -24 : -149; + const int step = std::max(std::ilogb(sum) - (precision - 1), smallest); + const double scaled = std::ldexp(sum, -step); + const double integral = rounding == 1u ? std::ceil(scaled) : rounding == 2u ? std::floor(scaled) : rounding == 3u ? std::trunc(scaled) : std::nearbyint(scaled); + rounded = std::ldexp(integral, step); + } + if (half) { + const double magnitude = std::fabs(rounded); + const bool negative = std::signbit(rounded); + if (rounding == 0u && magnitude > 65536.0) rounded = std::copysign(65536.0, rounded); + if (rounding != 0u && magnitude > 65504.0) { + const bool larger = (rounding == 1u && !negative) || (rounding == 2u && negative); + rounded = std::copysign(larger ? 65536.0 : 65504.0, rounded); + } + } + result = std::bit_cast(static_cast(rounded)); + } + return half ? Flush(result, flush16, 0x38800000u) : Flush(result, flush32, 0x00800000u); +} + +struct Generator { + std::uint64_t state; + std::uint64_t Next() { + state += 0x9e3779b97f4a7c15ull; + auto value = state; + value = (value ^ (value >> 30u)) * 0xbf58476d1ce4e5b9ull; + value = (value ^ (value >> 27u)) * 0x94d049bb133111ebull; + return value ^ (value >> 31u); + } + std::uint32_t Pick(std::uint32_t count) { return static_cast(Next() % count); } + std::uint32_t Half() { + constexpr std::array special{0x0000u, 0x8000u, 0x0001u, 0x8001u, 0x03ffu, 0x0400u, 0x3c00u, 0xbc00u, 0x7bffu, 0xfbffu, 0x7c00u, 0xfc00u}; + const auto bits = Pick(4u) == 0u ? special[Pick(static_cast(special.size()))] : static_cast(Next() & 0xffffu); + const auto sign = (bits & 0x8000u) << 16u; + const auto exponent = (bits >> 10u) & 0x1fu; + const auto fraction = bits & 0x3ffu; + if (exponent == 31u) return sign | 0x7f800000u | (fraction << 13u); + if (exponent != 0u) return sign | ((exponent + 112u) << 23u) | (fraction << 13u); + return sign | std::bit_cast(static_cast(fraction) * 0x1p-24f); + } + std::uint32_t Single() { + constexpr std::array special{0x00000000u, 0x80000000u, 0x00000001u, 0x807fffffu, 0x00800000u, 0x3f800000u, 0xbf800000u, 0x7f7fffffu, 0xff7fffffu, 0x7f800000u, 0xff800000u, 0x7fc00000u, 0xffa00001u, 0x33800000u}; + switch (Pick(4u)) { + case 0: return special[Pick(static_cast(special.size()))]; + case 1: return static_cast(Next()); + default: return (Pick(2u) << 31u) | ((Pick(254u) + 1u) << 23u) | static_cast(Next() & 0x7fffffu); + } + } + std::uint32_t Cancelling(std::uint32_t delta, std::uint32_t coordinate, bool half) { + const double product = static_cast(std::bit_cast(delta)) * static_cast(std::bit_cast(coordinate)); + if (!std::isfinite(product)) return half ? Single() : Half(); + auto bits = std::bit_cast(static_cast(-product)); + if (Pick(2u) == 0u) bits += Pick(5u) - 2u; + if (half) return bits; + const auto value = std::bit_cast(bits); + if (!std::isfinite(value) || std::fabs(value) > 65504.0f) return Half(); + const auto exponent = std::max(std::ilogb(value) - 10, -24); + return std::bit_cast(static_cast(std::ldexp(std::nearbyint(std::ldexp(value, -exponent)), exponent))); + } +}; + +void RunRandom(AgcDriver::VulkanDevice& device, bool integer) { + std::array modes{}; + for (std::uint32_t slot = 0; slot < RandomModes; ++slot) modes[slot] = ModeFlags[slot % ModeFlags.size()] | ((slot / ModeFlags.size()) << InterpolationRound16Shift); + Generator generator{0x6b5f00000000ull}; + Data.fill(Sentinel); + for (std::uint32_t row = 0; row < RandomRows; ++row) { + for (std::uint32_t op = 0; op < 3u; ++op) { + const bool half = op == 1u; + const auto delta = generator.Half(); + const auto coordinate = generator.Single(); + const auto base = generator.Pick(3u) == 0u ? generator.Cancelling(delta, coordinate, half) : half ? generator.Single() : generator.Half(); + Data[row * RandomWords + op * 3u] = delta; + Data[row * RandomWords + op * 3u + 1u] = coordinate; + Data[row * RandomWords + op * 3u + 2u] = base; + } + } + device.Dispatch(Compile(modes, integer, true, RandomWords), RandomRows / static_cast(Rows.size()), 1u, 1u); + device.WaitIdle(); + for (std::uint32_t row = 0; row < RandomRows; ++row) { + for (std::uint32_t slot = 0; slot < RandomModes; ++slot) { + for (std::uint32_t op = 0; op < 3u; ++op) { + const auto* input = &Data[row * RandomWords + op * 3u]; + const auto expected = Reference(input[0], input[1], input[2], op == 1u, modes[slot]); + const auto actual = Data[row * RandomWords + 9u + slot * 3u + op]; + char message[240]; + std::snprintf(message, sizeof(message), "%s (%s), mode 0x%x, random row %u: 0x%08x * 0x%08x + 0x%08x gave 0x%08x, reference 0x%08x", + OpNames[op], integer ? "integer" : "Float64", modes[slot], row, input[0], input[1], input[2], actual, expected); + Require(actual == expected, message); + } + } + } + for (std::size_t index = RandomRows * RandomWords; index < Data.size(); ++index) Require(Data[index] == Sentinel, "random interpolation rows wrote beyond the rows"); +} + int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; const auto target = device->Target(); - if (std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityFloat64) == target.supportedCapabilities.end()) { - std::puts("skipped, interpolation modes require Float64"); - return VulkanTestSkipped; + const bool float64 = std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityFloat64) != target.supportedCapabilities.end(); + for (std::uint32_t mode = 0; mode < ModeFlags.size(); ++mode) { + Run(*device, mode, true); + if (float64) Run(*device, mode, false); } - for (std::uint32_t mode = 0; mode < ModeFlags.size(); ++mode) Run(*device, mode); - std::puts("interpolation modes passed: 32 hardware rows, 3 ops, 8 float modes"); + RunRandom(*device, true); + if (float64) RunRandom(*device, false); + std::puts(float64 ? "interpolation modes passed: 32 hardware rows and 2048 random rows against the reference in 32 float modes, 3 ops, integer and Float64 paths" + : "interpolation modes passed: 32 hardware rows and 2048 random rows against the reference in 32 float modes, 3 ops, integer path (the device has no Float64)"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; 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/LayeredArrayColorDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredArrayColorDraw.cpp new file mode 100644 index 0000000000..8105de38e5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredArrayColorDraw.cpp @@ -0,0 +1,256 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Layered draws (a vertex shader exporting the render target index) into a color view of several +// slices: (a) slices 1 to 3 of a four-slice 2D array, (b) slices 0 to 2 of a surface of one slice. +// Each layer is filled with its own value; after the write-back every slice of guest memory must hold +// its layer's value, and the slices outside the view their initial bytes. + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Side = 128; +constexpr std::uint32_t Slices = 4; +constexpr std::size_t SliceBytes = 65536; +constexpr std::size_t SurfaceBytes = SliceBytes * Slices; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Color32FloatInfo = (4u << 2u) | (7u << 8u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint8_t Float32Export = 9; +constexpr std::uint32_t LayerExport = (1u << 18u) | (1u << 21u); +constexpr std::uint32_t ShaderViewportIndexLayerCapability = 5254; +constexpr std::uint32_t Kept = 0x5a5a5a5au; + +// Position from the buffer at offset 0, the layer from offset 28 into the render target index. +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xe030201c, 0x80000405, 0xbf8c3f70, 0xf80000cf, 0x03020100, 0xf80008d4, 0x00040000, 0xbf810000, +}; + +// 1 + layer / 2, the layer read from the render target index field of the ancillary word. +alignas(256) constexpr std::array LayerValuePixelCode{ + 0x2c020090, 0x7e020d01, 0x100202f0, 0x060202f2, 0xf800180f, 0x01010101, 0xbf810000, +}; + +struct Vertex { + std::array position; + std::array color; + std::uint32_t layer; +}; + +alignas(256) std::array Vertices{}; + +float LayerValue(std::uint32_t layer) { + return 1.0f + static_cast(layer) / 2.0f; +} + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, SliceBytes)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes + SliceBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + SliceBytes - 1) & ~(static_cast(SliceBytes) - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + SurfaceBytes != begin + SurfaceBytes + SliceBytes) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + SliceBytes - aligned); + block = reinterpret_cast(aligned); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes); + } +#endif + Require(block != nullptr, "layered array color draw: cannot allocate the color array"); + GuestAllocations::Mutation().Add(block, SurfaceBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); +#else + munmap(block, SurfaceBytes); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, SurfaceBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +void BuildQuads(std::uint32_t layers) { + Vertices = {}; + const std::array, 6> corners{{{-1.0f, -1.0f}, {1.0f, -1.0f}, {-1.0f, 1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f}}}; + for (std::uint32_t layer = 0; layer < layers; ++layer) { + for (std::uint32_t k = 0; k < corners.size(); ++k) Vertices[6u * layer + k] = {{corners[k][0], corners[k][1], 0.5f, 1.0f}, {1.0f, 1.0f, 1.0f}, layer}; + } +} + +// A CB_COLOR_VIEW of slices first..first + layers - 1 of a surface whose last array slice is lastArray. +AgcDriver::Graphics::ColorTarget DecodeColor(std::uint64_t address, std::uint32_t lastArray, std::uint32_t first, std::uint32_t layers) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(address >> 8u); + cx[0x31b] = first | ((first + layers - 1u) << 13u); + cx[0x31c] = Color32FloatInfo; + cx[0x31d] = 0; + cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = (TiledAttrib3 & ~((0x1fu << 14u) | 0x1fffu)) | (TileR64KBX << 14u) | lastArray; + cx[0x390] = static_cast(address >> 40u); + cx[0x207] = LayerExport; + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + const auto arrayLast = std::max(lastArray, first + layers - 1u); + Require(color.layers == layers && color.arraySlice == first && color.arrayLastSlice == arrayLast && color.arrayAddress == address, "the color view did not decode as " + std::to_string(layers) + " layers from slice " + std::to_string(first) + " of an array whose last slice is " + std::to_string(arrayLast)); + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + const auto vertexCount = 6u * color.layers; + BuildQuads(color.layers); + std::vector vertexUserData(4, 0u); + const auto address = AddressOf(Vertices.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (static_cast(sizeof(Vertex)) << 16u), vertexCount, 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; + vertexInfo.paClVsOutCntl = LayerExport; + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, vertexInfo, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Side, Side}; + state.renderLayers = color.layers; + state.layerExports = LayerExport; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Side), static_cast(Side), -static_cast(Side), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Side, Side}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.targetOutputMode[0] = Float32Export; + pixel.targetExportMapping = AgcDriver::Graphics::ExportMappings(state); + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + pixel.ancillary = true; + pixel.layerExported = true; + const std::span pixelCode(LayerValuePixelCode); + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, vertexCount, 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +// Draws into slices first..first + layers - 1 and checks every slice of the four in guest memory. +void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t lastArray, std::uint32_t first, std::uint32_t layers, const std::string& what) { + const auto surface = AddressOf(texels); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Kept); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Draw(device, DecodeColor(surface, lastArray, first, layers)); + AgcDriver::Graphics::StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "layered array color draw test"); + device.WaitIdle(); + for (std::uint32_t slice = 0; slice < Slices; ++slice) { + const bool drawn = slice >= first && slice < first + layers; + const auto expected = drawn ? std::bit_cast(LayerValue(slice - first)) : Kept; + const auto* words = reinterpret_cast(texels + slice * SliceBytes); + for (std::size_t word = 0; word < SliceBytes / 4u; ++word) { + if (words[word] == expected) continue; + char actual[16]; + std::snprintf(actual, sizeof(actual), "0x%08x", words[word]); + Require(false, what + ": slice " + std::to_string(slice) + " word " + std::to_string(word) + " is " + actual + ", expected " + (drawn ? std::to_string(LayerValue(slice - first)) + " (layer " + std::to_string(slice - first) + ")" : "its initial bytes (outside the color view)")); + } + } +} + +} + +int main() { + try { + GuestBlock array; + GuestBlock single; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->Target(); + if (std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), ShaderViewportIndexLayerCapability) == target.supportedCapabilities.end()) { + std::puts("skipped, the device has no VK_EXT_shader_viewport_index_layer"); + return VulkanTestSkipped; + } + Run(*device, array.Data(), Slices - 1u, 1, 3, "a layered draw into slices 1 to 3 of a four-slice 2D array"); + Run(*device, single.Data(), 0, 0, 3, "a layered draw into slices 0 to 2 of a surface of one slice"); + std::puts("layered array color draw tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/LdsAddF32.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsAddF32.cpp new file mode 100644 index 0000000000..abd04c9da1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsAddF32.cpp @@ -0,0 +1,184 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Results = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020083, 0xe0341000, 0x80000201, 0x34080082, 0xbf8c3f70, 0xd8340000, 0x00000204, 0xd8340400, + 0x00000204, 0xbf8cc07f, 0xd9540000, 0x05000304, 0xd8540400, 0x00000304, 0xbf8cc07f, 0xd8d80000, + 0x06000004, 0xd8d80400, 0x07000004, 0x34020084, 0xbf8cc07f, 0xe07c1000, 0x80010501, 0xbf810000, +}; + +constexpr std::array, Threads> Operands{{ + {0x7f800000, 0xff800000}, {0xff800000, 0x7f800000}, {0x3f800000, 0xbf800000}, {0x80000000, 0x80000000}, + {0x00000000, 0x80000000}, {0x7f7fffff, 0x7f7fffff}, {0x7fc00000, 0xffc00001}, {0x7f800001, 0x3f800000}, + {0x3f800000, 0xff812345}, {0xffc00001, 0x7f800001}, {0x00000001, 0x80000001}, {0x00800000, 0x80000001}, + {0x8110c65e, 0x8074b225}, {0x00d42774, 0x80d0391a}, {0x80800000, 0x00800001}, {0x80431a89, 0x01bcb83b}, + {0x01bec9b9, 0x81bab7f2}, {0x007fffff, 0x00400000}, {0x80000000, 0x80000001}, {0x80f9f2ca, 0x00cd2d23}, + {0x005921e2, 0x8187bc63}, {0x014e8a46, 0x0060b9ff}, {0x81cf89be, 0x805706c5}, {0x00e62d65, 0x80e62d68}, + {0x00a66021, 0x800b6944}, {0x802976a1, 0x0156a967}, {0x0197eca0, 0x803c270f}, {0x8027bc33, 0x00f88e02}, + {0x0104a791, 0x80c6fa02}, {0x001f9fb3, 0x01e94ee7}, {0x80000000, 0x007fffff}, {0x007fffff, 0x007fffff}, + {0x010d380e, 0x0076291d}, {0x80000001, 0x80000001}, {0x81dd408a, 0x8023bed7}, {0x00000001, 0x00400000}, + {0x8033deaa, 0x001a010b}, {0x003da089, 0x01a8d6f4}, {0x002eafb3, 0x01f74c53}, {0x01fd86b4, 0x00195d7b}, + {0x80476063, 0x00b49438}, {0x00800001, 0x80000001}, {0xe34217a6, 0x5b100f9a}, {0xff7fffff, 0x00800000}, + {0x00400000, 0xcb000001}, {0x4b000000, 0x807fffff}, {0xa63b9449, 0x2d91f2c3}, {0x819c564f, 0x802c240f}, + {0x81b5b8a2, 0x81a1681d}, {0xe640b291, 0x5766d41e}, {0x334bd7d6, 0x3d01a33b}, {0x01eeda5d, 0x80e7c8f6}, + {0x7f800001, 0x80000001}, {0xff812345, 0x80000000}, {0xffc00001, 0x7f7fffff}, {0xff812345, 0x7f7fffff}, + {0xffc00001, 0x00800001}, {0xff812345, 0x80800000}, {0x6ef2f0aa, 0xeef2feaa}, {0x019a87f3, 0x80efc545}, + {0x71223030, 0xf122303c}, {0xff7fffff, 0xff7fffff}, {0x80ec8133, 0x005f7c8c}, {0xe914c8da, 0x6914c8db}, +}}; + +constexpr std::array, 5> Measured{{ + { + 0xffc00000, 0xffc00000, 0x00000000, 0x80000000, 0x00000000, 0x7f800000, 0xffc00001, 0x7fc00001, + 0xffc12345, 0x7fc00001, 0x00000000, 0x00800000, 0x8110c65e, 0x00000000, 0x00000000, 0x01bcb83b, + 0x00000000, 0x00000000, 0x80000000, 0x80000000, 0x8187bc63, 0x014e8a46, 0x81cf89be, 0x80000000, + 0x00a66021, 0x0156a967, 0x0197eca0, 0x00f88e02, 0x00000000, 0x01e94ee7, 0x00000000, 0x00000000, + 0x010d380e, 0x80000000, 0x81dd408a, 0x00000000, 0x00000000, 0x01a8d6f4, 0x01f74c53, 0x01fd86b4, + 0x00b49438, 0x00800001, 0xe3421716, 0xff7fffff, 0xcb000001, 0x4b000000, 0x2d91f14c, 0x819c564f, + 0x822b9060, 0xe640b291, 0x3d01a348, 0x01b4e820, 0x7fc00001, 0xffc12345, 0xffc00001, 0xffc12345, + 0xffc00001, 0xffc12345, 0xe8e00000, 0x013d2d44, 0xe7400000, 0xff800000, 0x80ec8133, 0x5d800000, + }, + { + 0xffc00000, 0xffc00000, 0x00000000, 0x80000000, 0x00000000, 0x7f800000, 0xffc00001, 0x7fc00001, + 0xffc12345, 0x7fc00001, 0x00000000, 0x00000000, 0x814b1f70, 0x00000000, 0x00000000, 0x01abf199, + 0x00000000, 0x00bfffff, 0x80000000, 0x80000000, 0x8162e7d5, 0x017ee746, 0x81e54b6f, 0x80000000, + 0x009af6dd, 0x0141ee16, 0x0188e2dc, 0x00d0d1cf, 0x00000000, 0x01f136d4, 0x00000000, 0x00fffffe, + 0x01484c9c, 0x80000000, 0x81e63040, 0x00000000, 0x80000000, 0x01b83f16, 0x02017c20, 0x0201ef09, + 0x00000000, 0x00800000, 0xe3421716, 0xff7fffff, 0xcb000001, 0x4b000000, 0x2d91f14c, 0x81a75f53, + 0x822b9060, 0xe640b291, 0x3d01a348, 0x01b4e820, 0x7fc00001, 0xffc12345, 0xffc00001, 0xffc12345, + 0xffc00001, 0xffc12345, 0xe8e00000, 0x013d2d44, 0xe7400000, 0xff800000, 0x808d04a7, 0x5d800000, + }, + { + 0xffc00000, 0xffc00000, 0x00000000, 0x80000000, 0x00000000, 0x7f800000, 0xffc00001, 0x7fc00001, + 0xffc12345, 0x7fc00001, 0x00000000, 0x00800000, 0x8110c65e, 0x0003ee5a, 0x00000001, 0x01bcb83b, + 0x0010471c, 0x00000000, 0x80000000, 0x802cc5a7, 0x8187bc63, 0x014e8a46, 0x81cf89be, 0x80000003, + 0x00a66021, 0x0156a967, 0x0197eca0, 0x00f88e02, 0x00425520, 0x01e94ee7, 0x00000000, 0x00000000, + 0x010d380e, 0x80000000, 0x81dd408a, 0x00000000, 0x00000000, 0x01a8d6f4, 0x01f74c53, 0x01fd86b4, + 0x00b49438, 0x00800001, 0xe3421716, 0xff7fffff, 0xcb000001, 0x4b000000, 0x2d91f14c, 0x819c564f, + 0x822b9060, 0xe640b291, 0x3d01a348, 0x01b4e820, 0x7fc00001, 0xffc12345, 0xffc00001, 0xffc12345, + 0xffc00001, 0xffc12345, 0xe8e00000, 0x013d2d44, 0xe7400000, 0xff800000, 0x80ec8133, 0x5d800000, + }, + { + 0xffc00000, 0xffc00000, 0x00000000, 0x80000000, 0x00000000, 0x7f800000, 0xffc00001, 0x7fc00001, + 0xffc12345, 0x7fc00001, 0x00000000, 0x007fffff, 0x814b1f70, 0x0003ee5a, 0x00000001, 0x01abf199, + 0x0010471c, 0x00bfffff, 0x80000001, 0x802cc5a7, 0x8162e7d5, 0x017ee746, 0x81e54b6f, 0x80000003, + 0x009af6dd, 0x0141ee16, 0x0188e2dc, 0x00d0d1cf, 0x00425520, 0x01f136d4, 0x007fffff, 0x00fffffe, + 0x01484c9c, 0x80000002, 0x81e63040, 0x00400001, 0x8019dd9f, 0x01b83f16, 0x02017c20, 0x0201ef09, + 0x006d33d5, 0x00800000, 0xe3421716, 0xff7fffff, 0xcb000001, 0x4b000000, 0x2d91f14c, 0x81a75f53, + 0x822b9060, 0xe640b291, 0x3d01a348, 0x01b4e820, 0x7fc00001, 0xffc12345, 0xffc00001, 0xffc12345, + 0xffc00001, 0xffc12345, 0xe8e00000, 0x013d2d44, 0xe7400000, 0xff800000, 0x808d04a7, 0x5d800000, + }, + { + 0xffc00000, 0xffc00000, 0x00000000, 0x80000000, 0x00000000, 0x7f800000, 0xffc00001, 0x7fc00001, + 0xffc12345, 0x7fc00001, 0x00000000, 0x007fffff, 0x814b1f70, 0x0003ee5a, 0x00000001, 0x01abf199, + 0x0010471c, 0x00bfffff, 0x80000001, 0x802cc5a7, 0x8162e7d5, 0x017ee746, 0x81e54b6f, 0x80000003, + 0x009af6dd, 0x0141ee16, 0x0188e2dc, 0x00d0d1cf, 0x00425520, 0x01f136d4, 0x007fffff, 0x00fffffe, + 0x01484c9c, 0x80000002, 0x81e63040, 0x00400001, 0x8019dd9f, 0x01b83f16, 0x02017c20, 0x0201ef09, + 0x006d33d5, 0x00800000, 0xe3421716, 0xff7fffff, 0xcb000001, 0x4b000000, 0x2d91f14c, 0x81a75f53, + 0x822b9060, 0xe640b291, 0x3d01a348, 0x01b4e820, 0x7fc00001, 0xffc12345, 0xffc00001, 0xffc12345, + 0xffc00001, 0xffc12345, 0xe8e00000, 0x013d2d44, 0xe7400000, 0xff800000, 0x808d04a7, 0x5d800000, + }, +}}; + +constexpr std::array FloatModes{0x00u | (3u << 6u), 0x10u | (3u << 6u), 0x20u | (3u << 6u), 0x30u | (3u << 6u), 0x33u | (3u << 6u)}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t floatMode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * 2u] = Operands[tid][0]; + Input[tid * 2u + 1u] = Operands[tid][1]; + } + 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}, 2048u, {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{floatMode, true, false, false}; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); +} + +bool Denormal(std::uint32_t value) { + return (value & 0x7f800000u) == 0u && (value & 0x007fffffu) != 0u; +} + +bool HostDependent(std::uint32_t mode, std::uint32_t tid) { + const bool keepInputs = ((FloatModes[mode] >> 4u) & 1u) != 0u; + const bool keepResult = ((FloatModes[mode] >> 5u) & 1u) != 0u; + return (keepInputs && (Denormal(Operands[tid][0]) || Denormal(Operands[tid][1]))) || (keepResult && Denormal(Measured[mode][tid])); +} + +std::uint32_t Check(std::uint32_t mode) { + std::uint32_t checked = 0; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + if (HostDependent(mode, tid)) continue; + ++checked; + const std::uint32_t* out = &Output[tid * Results]; + const std::string row = "FLOAT_MODE " + Hex(FloatModes[mode]) + " lane " + std::to_string(tid) + " (" + Hex(Operands[tid][0]) + " + " + Hex(Operands[tid][1]) + ")"; + Require(out[0] == Operands[tid][0], "lds add f32: ds_add_rtn_f32 " + row + " returned " + Hex(out[0])); + Require(out[1] == Measured[mode][tid], "lds add f32: ds_add_rtn_f32 " + row + " stored " + Hex(out[1]) + ", expected " + Hex(Measured[mode][tid])); + Require(out[2] == Measured[mode][tid], "lds add f32: ds_add_f32 " + row + " stored " + Hex(out[2]) + ", expected " + Hex(Measured[mode][tid])); + } + return checked; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::uint32_t checked = 0; + for (std::uint32_t mode = 0; mode < FloatModes.size(); ++mode) { + Run(*device, FloatModes[mode]); + checked += Check(mode); + } + Require(checked >= 200u, "lds add f32: only " + std::to_string(checked) + " rows were independent of host denormal preservation"); + std::puts("lds add f32 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/LdsBeyondDeviceLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsBeyondDeviceLimit.cpp index 8b37c4591f..fb6fd2d583 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LdsBeyondDeviceLimit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsBeyondDeviceLimit.cpp @@ -148,7 +148,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const ShaderRe Require(within.workgroupMemoryDwords == 0u, "lds beyond the device limit " + run + ": LDS below the device limit left workgroup memory"); fits(within, "LDS below the device limit"); const auto atLimit = Compile(waveSize, limit, target); - Require(atLimit.workgroupMemoryDwords == limit / 4u + 1u, "lds beyond the device limit " + run + ": LDS at the device limit gave " + std::to_string(atLimit.workgroupMemoryDwords) + " device memory dwords per workgroup"); + Require(atLimit.workgroupMemoryDwords == limit / 4u + 2u, "lds beyond the device limit " + run + ": LDS at the device limit gave " + std::to_string(atLimit.workgroupMemoryDwords) + " device memory dwords per workgroup"); fits(atLimit, "LDS at the device limit"); const auto bytes = limit + AboveLimitBytes; for (std::uint32_t tid = 0; tid < Threads; ++tid) { @@ -159,7 +159,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const ShaderRe } Output.fill(Untouched); const auto result = Compile(waveSize, bytes, target); - Require(result.workgroupMemoryDwords == bytes / 4u + 1u, "lds beyond the device limit " + run + ": " + std::to_string(bytes) + " bytes of LDS gave " + std::to_string(result.workgroupMemoryDwords) + " device memory dwords per workgroup"); + Require(result.workgroupMemoryDwords == bytes / 4u + 2u, "lds beyond the device limit " + run + ": " + std::to_string(bytes) + " bytes of LDS gave " + std::to_string(result.workgroupMemoryDwords) + " device memory dwords per workgroup"); fits(result, "LDS above the device limit"); if (indirect) { IndirectGroups = {groupsX, groupsY, groupsZ, 0u}; 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..1f8032e93c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp @@ -6,16 +6,69 @@ #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 +#include +#else +#include +#endif namespace { using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + Require(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + _dup2(_fileno(file), _fileno(stderr)); +#else + saved = dup(fileno(stderr)); + dup2(fileno(file), fileno(stderr)); +#endif + } + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + constexpr std::uint32_t Threads = 256; constexpr std::uint32_t Iterations = 16; constexpr std::uint32_t Repeats = 32; @@ -163,6 +216,32 @@ alignas(256) constexpr auto LoadedParityCode = Unrolled<1>( 0xbf810000, })); +alignas(256) constexpr auto MisalignedCode = Unrolled<1>( + std::to_array({ + 0x34100083, + 0x7e020280, + 0x7e0402c1, + 0x7e060280, + 0x7e080281, + 0x7e0c0280, + 0x7e0e0280, + 0xd9340000, 0x00000601, + 0xbf8cc07f, + 0xbf8a0000, + }), + std::to_array({ + 0xd9000004, 0x00000201, + 0xbf8cc07f, + }), + std::to_array({ + 0xbf8cc07f, + 0xbf8a0000, + 0xd9d80000, 0x0a000001, + 0xbf8cc07f, + 0xe0741000, 0x80010a08, + 0xbf810000, + })); + std::array BufferDescriptor(const void* data, std::uint32_t bytes) { const auto address = reinterpret_cast(data); return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; @@ -182,14 +261,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; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::uint32_t ldsDwords) { 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}, @@ -197,21 +303,68 @@ void Run(AgcDriver::VulkanDevice& device, std::span code, c {0, 0, 0, 128} }; request.useCache = false; - const auto result = ShaderRecompiler::Recompile(request); + return ShaderRecompiler::Recompile(request); +} + +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) { + const auto result = Compile(device, code, ldsDwords); + if (expectedDeviceDwords) { + const auto bytes = WorkgroupBytes(result.spirv.Words()); + const auto limit = device.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 Misaligned(AgcDriver::VulkanDevice& device, std::uint32_t ldsDwords) { + const auto result = Compile(device, MisalignedCode, ldsDwords); + StderrCapture capture; + try { + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(MisalignedCode.data())); + device.WaitIdle(); + } catch (...) { + static_cast(capture.Finish()); + throw; + } + const auto log = capture.Finish(); + Require(log.find("BDA access failed: address=0x4 instruction=0x2c bytes=8 ") != std::string::npos && log.find("reason=7") != std::string::npos, "lds lock budget: misaligned ds_add_u64 on device-backed LDS did not fault"); + std::puts("lds lock budget: misaligned ds_add_u64 faulted"); +} + +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 + 2u) & ~1u); + const auto& capabilities = target.supportedCapabilities; + if (std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityInt64Atomics)) != capabilities.end()) Misaligned(device, limit / 4u); + 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/LdsTreeReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsTreeReduction.cpp new file mode 100644 index 0000000000..5188455a21 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsTreeReduction.cpp @@ -0,0 +1,84 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 256; +constexpr std::uint32_t Rounds = 16; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array TreeCode{ + 0x34040082, 0x4a020081, 0xd8340000, 0x00000102, 0xbf8cc07f, 0xbf8a0000, 0x7da800ff, 0x00000080, + 0xbf880006, 0xd8d80200, 0x03000002, 0xbf8cc07f, 0x4a020701, 0xd8340000, 0x00000102, 0xbefe03c1, + 0xbf8cc07f, 0xbf8a0000, 0x7da800c0, 0xbf880006, 0xd8d80100, 0x03000002, 0xbf8cc07f, 0x4a020701, + 0xd8340000, 0x00000102, 0xbefe03c1, 0xbf8cc07f, 0xbf8a0000, 0x7da800a0, 0xbf880006, 0xd8d80080, + 0x03000002, 0xbf8cc07f, 0x4a020701, 0xd8340000, 0x00000102, 0xbefe03c1, 0xbf8cc07f, 0xbf8a0000, + 0x7da80090, 0xbf880006, 0xd8d80040, 0x03000002, 0xbf8cc07f, 0x4a020701, 0xd8340000, 0x00000102, + 0xbefe03c1, 0xbf8cc07f, 0x7da80088, 0xbf880006, 0xd8d80020, 0x03000002, 0xbf8cc07f, 0x4a020701, + 0xd8340000, 0x00000102, 0xbefe03c1, 0xbf8cc07f, 0x7da80084, 0xbf880006, 0xd8d80010, 0x03000002, + 0xbf8cc07f, 0x4a020701, 0xd8340000, 0x00000102, 0xbefe03c1, 0xbf8cc07f, 0x7da80082, 0xbf880006, + 0xd8d80008, 0x03000002, 0xbf8cc07f, 0x4a020701, 0xd8340000, 0x00000102, 0xbefe03c1, 0xbf8cc07f, + 0x7da80081, 0xbf880006, 0xd8d80004, 0x03000002, 0xbf8cc07f, 0x4a020701, 0xd8340000, 0x00000102, + 0xbefe03c1, 0xbf8cc07f, 0xbf8a0000, 0x7e060280, 0xd8d80000, 0x03000003, 0xbf8cc07f, 0xe0701000, + 0x80000302, 0xbf810000, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->ComputeTarget(32); + if (target.subgroupSize != 32u) { + std::printf("skipped, wave32 programs do not run on 32-lane subgroups (%u lanes)\n", target.subgroupSize); + return VulkanTestSkipped; + } + std::vector userData(4, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(output.begin(), output.end(), userData.begin()); + const std::span code(TreeCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, Threads, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + target, + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + constexpr std::uint32_t Total = Threads * (Threads + 1u) / 2u; + for (std::uint32_t round = 0; round < Rounds; ++round) { + Output.fill(0xdeadbeefu); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device->WaitIdle(); + for (std::uint32_t thread = 0; thread < Threads; ++thread) { + Require(Output[thread] == Total, "LDS tree reduction over eight wave32 waves, round " + std::to_string(round) + ": thread " + std::to_string(thread) + " (wave " + std::to_string(thread / 32u) + ") read " + std::to_string(Output[thread]) + ", expected " + std::to_string(Total)); + } + } + std::puts("LDS tree reduction tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/LookBackScan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp index 210d8e681d..8ec6a58bd2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp @@ -24,6 +24,13 @@ alignas(256) constexpr std::array ScanCode{ 0xe0701000, 0x80030802, 0xbf810000, }; +constexpr auto StoreScanCode = [] { + auto code = ScanCode; + code[16] = 0xe0745000; + code[38] = 0xe0745000; + return code; +}(); + constexpr std::uint32_t Lanes = 64; constexpr std::uint32_t Blocks = 4096; @@ -41,9 +48,17 @@ std::array BufferDescriptor(const void* data, std::uint32_t by return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; } -void Run(AgcDriver::VulkanDevice& device, std::uint32_t groups) { - alignas(256) static std::array code{}; - code = ScanCode; +struct Variant { + const char* name; + const std::array& words; +}; + +constexpr std::array Variants{{{"buffer_atomic_swap_x2", ScanCode}, {"buffer_store_dwordx2", StoreScanCode}}}; + +void Run(AgcDriver::VulkanDevice& device, std::size_t variant, std::uint32_t groups) { + alignas(256) static std::array, Variants.size()> programs{}; + auto& code = programs[variant]; + code = Variants[variant].words; std::vector userData; for (const auto& descriptor : {BufferDescriptor(Counter.data(), sizeof(Counter)), BufferDescriptor(Aggregates.data(), sizeof(Aggregates)), BufferDescriptor(Pairs.data(), sizeof(Pairs)), BufferDescriptor(Prefixes.data(), sizeof(Prefixes))}) { userData.insert(userData.end(), descriptor.begin(), descriptor.end()); @@ -64,8 +79,8 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t groups) { device.WaitIdle(); } -void CheckLookBack(AgcDriver::VulkanDevice& device, std::uint32_t block) { - const auto name = "look-back from block " + std::to_string(block); +void CheckLookBack(AgcDriver::VulkanDevice& device, std::size_t variant, std::uint32_t block) { + const auto name = std::string(Variants[variant].name) + " look-back from block " + std::to_string(block); Pairs.fill(0u); std::uint32_t prefix = 0; for (std::uint32_t b = 0; b < Blocks; ++b) Aggregates[b] = Value(b, block); @@ -77,20 +92,20 @@ void CheckLookBack(AgcDriver::VulkanDevice& device, std::uint32_t block) { Counter.fill(0u); Counter[0] = block; Prefixes.fill(0xdeadbeefu); - Run(device, 1); + Run(device, variant, 1); Require(Counter[0] == block + 1, name + ": the counter is " + std::to_string(Counter[0])); Require(Prefixes[block] == prefix, name + ": its prefix is " + std::to_string(Prefixes[block]) + ", expected " + std::to_string(prefix)); Require(Pairs[2 * block] == 2u, name + ": its flag is " + std::to_string(Pairs[2 * block]) + ", expected 2"); Require(Pairs[2 * block + 1] == prefix + Aggregates[block], name + ": it published " + std::to_string(Pairs[2 * block + 1]) + ", expected " + std::to_string(prefix + Aggregates[block])); } -void CheckWhole(AgcDriver::VulkanDevice& device, std::uint32_t seed) { - const auto name = "whole scan " + std::to_string(seed); +void CheckWhole(AgcDriver::VulkanDevice& device, std::size_t variant, std::uint32_t seed) { + const auto name = std::string(Variants[variant].name) + " whole scan " + std::to_string(seed); for (std::uint32_t b = 0; b < Blocks; ++b) Aggregates[b] = Value(b, seed); Counter.fill(0u); Pairs.fill(0u); Prefixes.fill(0xdeadbeefu); - Run(device, Blocks); + Run(device, variant, Blocks); std::uint32_t running = 0; for (std::uint32_t b = 0; b < Blocks; ++b) { Require(Prefixes[b] == running, name + ": block " + std::to_string(b) + "'s prefix is " + std::to_string(Prefixes[b]) + ", expected " + std::to_string(running)); @@ -113,8 +128,10 @@ int main() { std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); return VulkanTestSkipped; } - for (const std::uint32_t block : {0u, 1u, 2u, 63u, 64u, 1000u, Blocks - 1}) CheckLookBack(*device, block); - for (std::uint32_t seed = 0; seed < 50; ++seed) CheckWhole(*device, seed); + for (std::size_t variant = 0; variant < Variants.size(); ++variant) { + for (const std::uint32_t block : {0u, 1u, 2u, 63u, 64u, 1000u, Blocks - 1}) CheckLookBack(*device, variant, block); + for (std::uint32_t seed = 0; seed < 50; ++seed) CheckWhole(*device, variant, seed); + } std::puts("look-back scan tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LookupLogFitsBuild.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LookupLogFitsBuild.cpp new file mode 100644 index 0000000000..5ca84cfaf6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LookupLogFitsBuild.cpp @@ -0,0 +1,214 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::CompiledShader; +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::ShaderResources; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Lanes = 64; +constexpr std::uint32_t Textures = 1500; +constexpr std::size_t LogBound = 1024; +constexpr std::uint32_t Stride = 32; +constexpr std::size_t SurfaceBytes = 256; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t SrtOffset = 4096; +constexpr std::size_t InputOffset = 8192; +constexpr std::size_t OutputOffset = 12288; +constexpr std::size_t HeapOffset = 65536; +constexpr std::size_t BlockBytes = (HeapOffset + std::size_t{Textures} * Stride + 65535u) & ~std::size_t{65535}; +constexpr std::size_t SurfaceBlockBytes = (std::size_t{Textures} * SurfaceBytes + 65535u) & ~std::size_t{65535}; + +constexpr std::array Code{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, + 0x34080082u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xbf8cc07fu, + 0xe0301000u, 0x80030504u, 0xf09c0f08u, 0x00450000u, 0xbf8c3f70u, 0x4a000105u, + 0xe0701000u, 0x80070004u, 0xbf810000u, 0u, 0u}; + +class GuestBlock { +public: + GuestBlock(std::size_t bytes, bool gpu) { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, bytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + bytes != begin + bytes + alignment) munmap(reinterpret_cast(aligned + bytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); + } +#endif + Require(block != nullptr, "lookup log: cannot allocate a guest block"); + std::memset(block, 0, bytes); + GuestAllocations::Mutation().Add(block, bytes, true, true, gpu); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(block), bytes); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint64_t At(std::size_t offset) const { return reinterpret_cast(block + offset); } + std::uint8_t* Bytes(std::size_t offset) const { return block + offset; } + std::uint32_t* Words(std::size_t offset) const { return reinterpret_cast(block + offset); } + +private: + std::uint8_t* block = nullptr; +}; + +std::array BufferDescriptor(std::uint64_t address, std::uint32_t stride, std::uint32_t records) { + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), records, stride == 0u ? 0x31016facu : 0x01016facu}; +} + +void Prepare(const GuestBlock& block, const GuestBlock& surfaces) { + std::memcpy(block.Words(0), Code.data(), sizeof(Code)); + for (std::uint32_t texture = 0; texture < Textures; ++texture) { + const auto offset = static_cast(texture) * SurfaceBytes; + std::memset(surfaces.Bytes(offset), static_cast(texture & 0xffu), SurfaceBytes); + const auto surface = surfaces.At(offset); + const std::array words{static_cast(surface >> 8u), static_cast((surface >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 1u | (7u << 14u), 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + std::memcpy(block.Words(HeapOffset + static_cast(texture) * Stride), words.data(), sizeof(words)); + } + auto* srt = block.Words(SrtOffset); + const auto heap = BufferDescriptor(block.At(HeapOffset), Stride, Textures); + const auto input = BufferDescriptor(block.At(InputOffset), 0u, Lanes * 4u); + const auto output = BufferDescriptor(block.At(OutputOffset), 0u, Lanes * 4u); + std::copy(heap.begin(), heap.end(), srt); + std::copy(input.begin(), input.end(), srt + 8); + std::copy(output.begin(), output.end(), srt + 12); + AgcDriver::GuestMemory::CollectWritesUncached(block.At(0), BlockBytes); + AgcDriver::GuestMemory::CollectWritesUncached(surfaces.At(0), SurfaceBlockBytes); +} + +ShaderRecompiler::RecompileResult Compile(const AgcDriver::VulkanDevice& device, const GuestBlock& block) { + const auto srt = block.At(SrtOffset); + const std::array userData{static_cast(srt), static_cast(srt >> 32u)}; + const std::span code(block.Words(0), Code.size()); + const std::array memory{{ + {block.At(0), std::as_bytes(code)}, + {srt, std::as_bytes(std::span(block.Words(SrtOffset), 64))}, + {block.At(HeapOffset), std::as_bytes(std::span(block.Words(HeapOffset), static_cast(Textures) * Stride / 4u))}, + }}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, block.At(0), code, 0, {}}, + {Lanes, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + return ShaderRecompiler::Recompile(request); +} + +ShaderResources::ProofReport Prove(ShaderResources& resources, const CompiledShader& shader, bool& current) { + ShaderResources::ProofReport report; + current = resources.Revalidate(shader, &report); + return report; +} + +std::string Path(const ShaderResources::ProofReport& report) { + switch (report.path) { + case ShaderResources::ProofPath::Fast: return "fast"; + case ShaderResources::ProofPath::OwnRefreshed: return "own refresh"; + default: return "full walk"; + } +} + +void RunTests(const AgcDriver::VulkanDevice& device) { + GuestBlock block(BlockBytes, true); + GuestBlock surfaces(SurfaceBlockBytes, false); + Prepare(block, surfaces); + const auto result = Compile(device, block); + const auto table = std::find_if(result.bindings.begin(), result.bindings.end(), [](const auto& binding) { return binding.role == ShaderRecompiler::DescriptorRole::ImageTable; }); + Require(table != result.bindings.end() && table->count == Textures, "lookup log: the program does not bind an image table of " + std::to_string(Textures) + " textures"); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + const CompiledShader shader{ShaderStage::Compute, &result, 0}; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + auto resources = std::make_shared(context, shader); + Require(resources->Reusable() && resources->SampledTextures().size() == Textures, "lookup log: the set over " + std::to_string(Textures) + " textures is not reusable"); + const auto capacity = ShaderResources::LookupLogCapacity(); + Require(capacity <= LogBound, "lookup log: the thread's log kept a capacity of " + std::to_string(capacity) + " records after a build of " + std::to_string(Textures) + " textures, over the bound of " + std::to_string(LogBound)); + std::printf("the thread's log capacity is %zu records after the build\n", capacity); + bool current = false; + auto report = Prove(*resources, shader, current); + Require(current && report.path == ShaderResources::ProofPath::Fast, "lookup log: a set over " + std::to_string(Textures) + " textures took the " + Path(report) + " with its memory unchanged after the build"); + std::puts("unchanged memory proved on the fast path after the build"); + const auto target = static_cast(Textures / 3u) * SurfaceBytes; + auto* first = reinterpret_cast(surfaces.Bytes(target)); + const std::uint8_t saved = *first; + *first = saved; + report = Prove(*resources, shader, current); + Require(report.path == ShaderResources::ProofPath::Full, "lookup log: a store over texture " + std::to_string(Textures / 3u) + " took the " + Path(report) + ", not the full walk"); + Require(ShaderResources::LookupLogCapacity() <= LogBound, "lookup log: the thread's log kept a capacity of " + std::to_string(ShaderResources::LookupLogCapacity()) + " records after a full walk over " + std::to_string(Textures) + " textures"); + if (current) { + report = Prove(*resources, shader, current); + Require(current && report.path == ShaderResources::ProofPath::Fast, "lookup log: a set over " + std::to_string(Textures) + " textures took the " + Path(report) + " with its memory unchanged after a full walk"); + std::puts("unchanged memory proved on the fast path after a full walk"); + } + *first = static_cast(saved ^ 0xffu); + report = Prove(*resources, shader, current); + Require(report.path == ShaderResources::ProofPath::Full && !current, "lookup log: a set whose texture " + std::to_string(Textures / 3u) + " changed was proved current on the " + Path(report)); + std::puts("a changed texture fails the proof"); +} + +} + +int main() { +#ifdef _WIN32 + if (std::getenv("APS5_TEMPLATE_AUDIT_MS") == nullptr) _putenv_s("APS5_TEMPLATE_AUDIT_MS", "0"); +#else + setenv("APS5_TEMPLATE_AUDIT_MS", "0", 0); +#endif + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + RunTests(*device); + std::puts("lookup log fits build tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MeshBarycentricEmulation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MeshBarycentricEmulation.cpp new file mode 100644 index 0000000000..58f20d44f9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MeshBarycentricEmulation.cpp @@ -0,0 +1,306 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::PixelInput; +using ShaderRecompiler::PixelInputBit; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 48; +constexpr float Unwritten = -7.0f; +alignas(256) std::array Pixels{}; + +alignas(256) constexpr std::array MeshCode{ + 0x8f6a9003, 0x94fe6ac1, 0xbf88000b, 0xd7650006, 0x000100c1, 0xd7660006, 0x00020cc1, 0x93ebff03, + 0x00040018, 0xd7460006, 0x04190c6b, 0x340c0c82, 0xd8340000, 0x00000506, 0xbf8cc07f, 0xbefe04c1, + 0xbf8a0000, 0x938dff02, 0x00090016, 0x9382ff03, 0x00040018, 0x938cff03, 0x00080008, 0xd7650009, + 0x000100c1, 0xd7660009, 0x000212c1, 0xd746000a, 0x04250c02, 0x7da8120c, 0xbf88002e, 0x361600ff, + 0x0000ffff, 0x2c180090, 0x361a02ff, 0x0000ffff, 0xd8d80000, 0x0b00000b, 0xd8d80000, 0x0c00000c, + 0xd8d80000, 0x0d00000d, 0xbf8cc07f, 0xe0382000, 0x8002100b, 0xe0382010, 0x8002140b, 0xe0382000, + 0x8002180c, 0xe0382010, 0x80021c0c, 0xe0382000, 0x8002200d, 0xe0382010, 0x8002240d, 0x161c1483, + 0x161e14ff, 0x00000060, 0xbf8c3f70, 0xdb7c0400, 0x0000100f, 0xdb7c0410, 0x0000140f, 0xdb7c0420, + 0x0000180f, 0xdb7c0430, 0x00001c0f, 0xdb7c0440, 0x0000200f, 0xdb7c0450, 0x0000240f, 0x4a501c81, + 0x4a521c82, 0x3450508a, 0x34525294, 0xd772002a, 0x04a6510e, 0xbf8cc07f, 0xbefe04c1, 0xbf8a0000, + 0x930e830d, 0xbf078002, 0xbf850003, 0x8f0f8c0d, 0x887c0f0e, 0xbf900009, 0x7da8140d, 0xbf880002, + 0xf8000941, 0x0000002a, 0xbefe04c1, 0x7da8140e, 0xbf88000a, 0x34561485, 0xdbfc0400, 0x2c00002b, + 0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80008cf, 0x2f2e2d2c, 0xf800020f, 0x33323130, 0xbf810000, +}; + +alignas(256) constexpr std::array BarycentricPixelCode{ + 0xf800180f, 0x01000100, 0xbf810000, +}; + +alignas(256) constexpr std::array VertexValuePixelCode{ + 0xc80a0000, 0xc80e0001, 0xc8120002, 0xf800180f, 0x04040302, 0xbf810000, +}; + +struct Vertex { + std::array position; + std::array color; +}; + +struct Corner { + float x; + float y; + float w; +}; + +constexpr std::array VertexValues{0.25f, -3.5f, 7.75f}; + +constexpr std::array Corners{{{-0.8f, -0.8f, 1.0f}, {0.9f, -0.6f, 2.5f}, {-0.5f, 0.85f, 0.5f}}}; + +alignas(256) std::array Vertices{}; + +std::array VertexBufferDescriptor(const void* vertices, std::uint32_t count) { + const auto address = reinterpret_cast(vertices); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (32u << 16u), count, 0x01016facu}; +} + +constexpr ShaderRecompiler::MeshConfiguration SmallSubgroup{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}; + +template +void ExpectRejected(TAction action, const std::string& reason) { + try { + action(); + } catch (const std::exception& error) { + Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string("the validator rejected the draw for another reason than \"") + reason + "\": " + error.what()); + return; + } + Require(false, "the validator accepted a draw it must reject (" + reason + ")"); +} + +std::uint32_t DecorationWord(const ShaderRecompiler::RecompileResult& result, spv::Decoration decoration) { + const auto& words = result.spirv.Words(); + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + if ((words[cursor] & 0xffffu) == spv::OpDecorate && count == 3u && words[cursor + 2] == decoration) return static_cast(cursor + 2); + cursor += count; + } + return 0u; +} + +void CheckValidator(AgcDriver::VulkanDevice& device, const AgcDriver::Pm4::DrawParameters& draw, const AgcDriver::Graphics::State& state, const ShaderRecompiler::RecompileResult& meshResult, const ShaderRecompiler::RecompileResult& pixelResult, std::uint32_t pixelOffset) { + const auto rejected = [&](const ShaderRecompiler::RecompileResult& mesh, const ShaderRecompiler::RecompileResult& pixel, const std::string& reason) { + const std::array shaders{{{ShaderStage::Mesh, &mesh, 0}, {ShaderStage::Fragment, &pixel, pixelOffset}}}; + ExpectRejected([&] { device.Draw(state, draw, shaders); }, reason); + }; + auto unreserved = pixelResult; + unreserved.barycentricEmulation.active = false; + rejected(meshResult, unreserved, "per-primitive"); + auto shifted = pixelResult; + shifted.fragmentParameters.push_back({0u, 0u, false, false}); + rejected(meshResult, shifted, "per-primitive"); + auto plain = pixelResult; + const auto decorate = DecorationWord(plain, spv::DecorationPerPrimitiveEXT); + Require(decorate != 0u, "the pixel module has no per-primitive input"); + plain.spirv[decorate] = spv::DecorationFlat; + rejected(meshResult, plain, "mesh shading"); +} + +ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint32_t inputs) { + Pixels.fill(Unwritten); + auto target = device.Target(); + target.fragmentShaderBarycentricEnabled = false; + + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Vertices.size()), 0, 1, 0, false}; + std::vector meshUserData(12, 0u); + const auto index = AgcDriver::Graphics::MeshIndexBufferDescriptor(draw); + std::copy(index.begin(), index.end(), meshUserData.begin() + ShaderRecompiler::MeshIndexBufferUserWord); + const auto vertexBuffer = VertexBufferDescriptor(Vertices.data(), static_cast(Vertices.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), meshUserData.begin() + 8); + const std::array meshMemory{{{reinterpret_cast(MeshCode.data()), std::as_bytes(std::span(MeshCode))}}}; + ShaderRecompiler::RecompileRequest mesh{ + {ShaderStage::Mesh, reinterpret_cast(MeshCode.data()), MeshCode, 0, {}}, + {64u, 0, meshUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, meshMemory}, + target, + {0, 0, 0, ShaderRecompiler::MeshDrawPushOffsetBytes}, + ShaderRecompiler::GraphicsCompileContext{0, {}, SmallSubgroup, std::nullopt, {draw.indexAddress, draw.indexCount, draw.indexSize, draw.instanceCount}} + }; + mesh.useCache = false; + const auto meshResult = ShaderRecompiler::Recompile(mesh); + const auto meshPush = static_cast(meshResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.interpolatorCount = 1; + pixel.inputAddr = inputs; + pixel.hasPerspectiveCenterVgpr = (inputs & PixelInputBit(PixelInput::PerspectiveCenter)) != 0u; + pixel.noPerspective = (inputs & PixelInputBit(PixelInput::LinearCenter)) != 0u; + pixel.executeOnNoop = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData; + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {64u, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(meshPush, target), + ShaderRecompiler::GraphicsCompileContext{0, {}, SmallSubgroup, std::nullopt, {draw.indexAddress, draw.indexCount, draw.indexSize, draw.instanceCount}} + }; + fragment.useCache = false; + auto pixelResult = ShaderRecompiler::Recompile(fragment); + Require(pixelResult.barycentricEmulation.active, "the pixel shader did not emulate barycentrics"); + mesh.graphics->meshBarycentrics = ShaderRecompiler::MeshBarycentricLayoutOf(pixelResult); + const auto pairedMesh = ShaderRecompiler::Recompile(mesh); + if (pixelCode.data() == BarycentricPixelCode.data()) { + auto last = mesh; + last.graphics->mesh->provokingVertex = 1u; + ExpectRejected([&] { static_cast(ShaderRecompiler::Recompile(last)); }, "first provoking vertex"); + auto wide = mesh; + wide.graphics->mesh->maxVertices = target.mesh->maxSharedMemoryBytes / 16u + 1u; + ExpectRejected([&] { static_cast(ShaderRecompiler::Recompile(wide)); }, "mesh shared memory"); + } + Require(pairedMesh.meshBarycentrics == mesh.graphics->meshBarycentrics, "the paired mesh does not record the layout it was compiled for"); + Require(pairedMesh.pushConstants.size() == meshPush && pairedMesh.parameterExports == meshResult.parameterExports, "the paired mesh changed the push constants or exports the pixel shader was built for"); + + const std::array shaders{{ + {ShaderStage::Mesh, &pairedMesh, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(meshPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, 64u, 64u, SmallSubgroup, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R32G32B32A32_SFLOAT, sizeof(Pixels), 0xe4u, AgcDriver::Graphics::ColorTileMode::Linear, 16u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + if (pixelCode.data() == BarycentricPixelCode.data()) CheckValidator(device, draw, state, pairedMesh, pixelResult, PixelPushOffset(meshPush, target)); + device.Draw(state, draw, shaders); + device.WaitIdle(); + return pixelResult; +} + +struct Expected { + bool inside; + std::array linear; + std::array perspective; +}; + +Expected Barycentrics(std::uint32_t x, std::uint32_t y, bool snapped) { + std::array, 3> window{}; + for (std::size_t i = 0; i < Corners.size(); ++i) window[i] = {(Corners[i].x + 1.0) * Width / 2.0, (1.0 - Corners[i].y) * Height / 2.0}; + if (snapped) { + for (auto& corner : window) { + for (auto& coordinate : corner) coordinate = std::round(coordinate * 256.0) / 256.0; + } + } + const double px = x + 0.5; + const double py = y + 0.5; + const auto edge = [](const std::array& a, const std::array& b, double cx, double cy) { return (b[0] - a[0]) * (cy - a[1]) - (b[1] - a[1]) * (cx - a[0]); }; + const double area = edge(window[0], window[1], window[2][0], window[2][1]); + const std::array b{edge(window[1], window[2], px, py) / area, edge(window[2], window[0], px, py) / area, edge(window[0], window[1], px, py) / area}; + Expected expected{std::min({b[0], b[1], b[2]}) > 0.02, {}, {}}; + double sum = 0.0; + for (std::size_t i = 0; i < 3; ++i) sum += b[i] / Corners[i].w; + for (std::size_t i = 0; i < 3; ++i) { + expected.linear[i] = static_cast(b[i]); + expected.perspective[i] = static_cast(b[i] / Corners[i].w / sum); + } + return expected; +} + +bool MatchesEither(float value, float unsnapped, float snapped) { + return std::fabs(value - unsnapped) <= 1e-4f || std::fabs(value - snapped) <= 1e-4f; +} + +float Pixel(std::uint32_t x, std::uint32_t y, std::uint32_t channel) { + return Pixels[(y * Width + x) * 4u + channel]; +} + +std::uint32_t CheckVertexValues() { + const std::array expected{VertexValues[1] - VertexValues[0], VertexValues[2] - VertexValues[0], VertexValues[0]}; + std::uint32_t checked = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + if (!Barycentrics(x, y, false).inside) continue; + for (std::uint32_t channel = 0; channel < 3; ++channel) { + Require(Pixel(x, y, channel) == expected[channel], "v_interp_mov: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected " + std::to_string(expected[channel])); + } + ++checked; + } + } + Require(checked > 200u, "v_interp_mov: too few interior pixels were checked"); + return checked; +} + +std::uint32_t CheckBarycentrics(const std::string& what, bool perspective) { + std::uint32_t checked = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = Barycentrics(x, y, false); + if (!expected.inside) continue; + const auto snapped = Barycentrics(x, y, true); + const auto& weights = perspective ? expected.perspective : expected.linear; + const auto& snappedWeights = perspective ? snapped.perspective : snapped.linear; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto value = Pixel(x, y, channel); + const auto reference = weights[1u + (channel & 1u)]; + const auto snappedReference = snappedWeights[1u + (channel & 1u)]; + Require(MatchesEither(value, reference, snappedReference), what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(value) + ", expected " + std::to_string(reference) + " or " + std::to_string(snappedReference)); + } + ++checked; + } + } + Require(checked > 200u, what + ": too few interior pixels were checked"); + return checked; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!device->Target().mesh.has_value()) { + std::puts("skipped, the device has no VK_EXT_mesh_shader"); + return VulkanTestSkipped; + } + if (device->DeviceName().starts_with("llvmpipe")) { + std::puts("skipped, the device has no mesh shader compiler that handles these programs (llvmpipe)"); + return VulkanTestSkipped; + } + for (std::size_t i = 0; i < Corners.size(); ++i) { + const auto& corner = Corners[i]; + Vertices[i] = {{corner.x * corner.w, corner.y * corner.w, 0.5f * corner.w, corner.w}, {VertexValues[i], 0.0f, 0.0f, 1.0f}}; + } + const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter); + const auto linear = PixelInputBit(PixelInput::LinearCenter); + auto result = Draw(*device, BarycentricPixelCode, perspective); + Require(result.barycentricEmulation.smooth && !result.barycentricEmulation.linear, "reading PERSP_CENTER I/J did not emulate perspective barycentrics"); + const auto smooth = CheckBarycentrics("mesh PERSP_CENTER I/J", true); + result = Draw(*device, BarycentricPixelCode, linear); + Require(!result.barycentricEmulation.smooth && result.barycentricEmulation.linear, "reading LINEAR_CENTER I/J did not emulate linear barycentrics"); + const auto noPerspective = CheckBarycentrics("mesh LINEAR_CENTER I/J", false); + result = Draw(*device, VertexValuePixelCode, perspective); + Require(result.barycentricEmulation.active && result.fragmentParameters.size() == 1 && result.fragmentParameters[0].perVertex, "v_interp_mov of P10 and P20 did not emulate per-vertex parameters"); + const auto vertexValues = CheckVertexValues(); + std::printf("mesh barycentric emulation tests passed: %u perspective and %u linear I/J pixels, %u raw vertex value pixels\n", smooth, noPerspective, vertexValues); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/MeshProvokingLayer.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MeshProvokingLayer.cpp new file mode 100644 index 0000000000..0431a9279e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MeshProvokingLayer.cpp @@ -0,0 +1,259 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 32; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t Layers = 2; +constexpr std::uint8_t Kept = 0x40; +constexpr std::uint32_t LayerExport = (1u << 18u) | (1u << 21u); +constexpr std::uint32_t ShaderViewportIndexLayerCapability = 5254; + +alignas(256) constexpr std::array GeometryCode{ + 0x8f6a9003, 0x94fe6ac1, 0xbf88000b, 0xd7650006, 0x000100c1, 0xd7660006, 0x00020cc1, 0x93ebff03, + 0x00040018, 0xd7460006, 0x04190c6b, 0x340c0c82, 0xd8340000, 0x00000506, 0xbf8cc07f, 0xbefe04c1, + 0xbf8a0000, 0x938dff02, 0x00090016, 0x9382ff03, 0x00040018, 0x938cff03, 0x00080008, 0xd7650009, + 0x000100c1, 0xd7660009, 0x000212c1, 0xd746000a, 0x04250c02, 0x7da8120c, 0xbf88002e, 0x361600ff, + 0x0000ffff, 0x2c180090, 0x361a02ff, 0x0000ffff, 0xd8d80000, 0x0b00000b, 0xd8d80000, 0x0c00000c, + 0xd8d80000, 0x0d00000d, 0xbf8cc07f, 0xe0382000, 0x8002100b, 0xe0382010, 0x8002140b, 0xe0382000, + 0x8002180c, 0xe0382010, 0x80021c0c, 0xe0382000, 0x8002200d, 0xe0382010, 0x8002240d, 0x161c1483, + 0x161e14ff, 0x00000060, 0xbf8c3f70, 0xdb7c0400, 0x0000100f, 0xdb7c0410, 0x0000140f, 0xdb7c0420, + 0x0000180f, 0xdb7c0430, 0x00001c0f, 0xdb7c0440, 0x0000200f, 0xdb7c0450, 0x0000240f, 0x4a501c81, + 0x4a521c82, 0x3450508a, 0x34525294, 0xd772002a, 0x04a6510e, 0xbf8cc07f, 0xbefe04c1, 0xbf8a0000, + 0x930e830d, 0xbf078002, 0xbf850003, 0x8f0f8c0d, 0x887c0f0e, 0xbf900009, 0x7da8140d, 0xbf880002, + 0xf8000941, 0x0000002a, 0xbefe04c1, 0x7da8140e, 0xbf88000c, 0x34561485, 0xdbfc0400, 0x2c00002b, + 0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80000cf, 0x2f2e2d2c, 0xf80008d4, 0x00330000, 0xf800020f, + 0x33323130, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array LayerFieldPixelCode{ + 0x2c020090, 0x7e020d01, 0x100202f0, 0xf800180f, 0x01010101, 0xbf810000, +}; + +struct Vertex { + std::array position; + std::array color; + std::uint32_t layer; +}; + +alignas(256) std::array Vertices{}; + +void BuildQuad() { + const std::array, 6> corners{{{-1.0f, -1.0f}, {1.0f, -1.0f}, {-1.0f, 1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f}}}; + for (std::uint32_t k = 0; k < corners.size(); ++k) Vertices[k] = {{corners[k][0], corners[k][1], 0.5f, 1.0f}, {1.0f, 1.0f, 1.0f}, k % 3u == 2u ? 1u : 0u}; +} + +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}; +} + +AgcDriver::Graphics::ColorTarget LayeredTarget(std::byte* memory) { + AgcDriver::Graphics::ColorTarget color{reinterpret_cast(memory), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, 0, 0xe4u}; + color.tileMode = ColorTileMode::RenderTarget; + color.depth = Layers; + color.depthSlice = 0; + color.layers = Layers; + color.bytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::SurfaceForTarget(color)).guestBytes; + return color; +} + +class GuestVolume { +public: + explicit GuestVolume(std::size_t bytes) { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(65536, bytes)); +#endif + Require(block != nullptr, "cannot allocate the layered target"); + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + std::memset(block, Kept, bytes); + } + + ~GuestVolume() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestVolume(const GuestVolume&) = delete; + GuestVolume& operator=(const GuestVolume&) = delete; + std::byte* Data() const { return block; } + +private: + std::byte* block = nullptr; +}; + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, bool last, std::span pixelCode, bool layerField) { + const auto target = device.Target(); + const auto vertexBuffer = BufferDescriptor(Vertices.data(), sizeof(Vertex), static_cast(Vertices.size())); + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Vertices.size()), 0, 1, 0, false}; + ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; + vertexInfo.paClVsOutCntl = LayerExport; + std::vector userData(12u, 0u); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), userData.begin() + 8); + const ShaderRecompiler::MeshConfiguration mesh{4u, 4u, 12u, 12u, 4u, 64u, 1024u, last ? 2u : 0u, 4u}; + const std::array geometryMemory{{{reinterpret_cast(GeometryCode.data()), std::as_bytes(std::span(GeometryCode))}}}; + ShaderRecompiler::RecompileRequest geometry{ + {ShaderStage::Mesh, reinterpret_cast(GeometryCode.data()), GeometryCode, 0, {}}, + {64, 0, userData, std::nullopt, std::nullopt, vertexInfo, geometryMemory}, + target, + {0, 0, 0, ShaderRecompiler::MeshDrawPushOffsetBytes} + }; + geometry.graphics = ShaderRecompiler::GraphicsCompileContext{0, {}, mesh, std::nullopt, {draw.indexAddress, draw.indexCount, draw.indexSize, draw.instanceCount}}; + geometry.useCache = false; + const auto geometryResult = ShaderRecompiler::Recompile(geometry); + const auto geometryPush = static_cast(geometryResult.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + if (layerField) { + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + pixel.ancillary = true; + pixel.layerExported = true; + } + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {64, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(geometryPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Mesh, &geometryResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(geometryPush, target)} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, 64, 64, mesh, std::nullopt}; + state.provokingVertexMode = last ? VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT : VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; + state.color = color; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.renderLayers = Layers; + state.layerExports = LayerExport; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + device.Draw(state, draw, shaders); + device.WaitIdle(); + AgcDriver::Graphics::StorageTexture::FlushAllPending("mesh provoking layer test"); + device.WaitIdle(); +} + +std::uint64_t BlockOffset(const AgcDriver::Graphics::TextureSwizzleEquation& equation, std::uint32_t x, std::uint32_t y, std::uint32_t z) { + std::uint64_t offset = 0; + for (std::uint32_t bit = 0; bit < equation.bits.size(); ++bit) { + const auto mask = equation.bits[bit]; + const auto selected = (x & (mask & 0xfffu)) ^ ((y << 12u) & (mask & 0xfff000u)) ^ ((z << 24u) & (mask & 0xff000000u)); + offset |= static_cast(std::popcount(selected) & 1u) << bit; + } + return offset; +} + +void Check(const std::byte* memory, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t drawnLayer, std::uint8_t drawn, const std::string& what) { + using namespace AgcDriver::Graphics; + const auto geometry = DescribeSurface(SurfaceForTarget(color)); + const auto tileMode = ColorTextureTileMode(color.tileMode); + const auto thin = ThinBlockLayout(tileMode, 4); + const auto& mip = geometry.mips.at(0); + const auto* equation = FindTextureSwizzleEquation(static_cast(color.tileMode), 4); + Require(equation != nullptr, "missing the target's swizzle equation"); + for (std::uint32_t layer = 0; layer < Layers; ++layer) { + const auto* base = memory + geometry.GuestLayerOffset(layer); + const auto expected = layer == drawnLayer ? drawn : Kept; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto* texel = base + (static_cast(y / thin[2]) * mip.blocksPerRow + x / thin[1]) * thin[0] + BlockOffset(*equation, x, y, layer); + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto actual = std::to_integer(texel[channel]); + Require(actual == expected || (layer == drawnLayer && expected == 128 && actual == 127), what + ": layer " + std::to_string(layer) + " texel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected) + (layer == drawnLayer ? " (drawn)" : " (untouched)")); + } + } + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->Target(); + if (!target.mesh.has_value() || std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), ShaderViewportIndexLayerCapability) == target.supportedCapabilities.end()) { + std::puts("skipped, the device has no VK_EXT_mesh_shader or VK_EXT_shader_viewport_index_layer"); + 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; + } + if (!device->ProvokingVertexLast()) { + std::puts("skipped, the device has no last provoking vertex mode"); + return VulkanTestSkipped; + } + BuildQuad(); + const auto bytes = (LayeredTarget(nullptr).bytes + 65535u) & ~std::size_t{65535u}; + for (const bool layerField : {false, true}) { + for (const bool last : {false, true}) { + const GuestVolume memory(bytes); + const auto color = LayeredTarget(memory.Data()); + Draw(*device, color, last, layerField ? std::span(LayerFieldPixelCode) : std::span(PixelCode), layerField); + const auto drawn = static_cast(layerField ? (last ? 128u : 0u) : 255u); + Check(memory.Data(), color, last ? 1u : 0u, drawn, std::string(last ? "last provoking vertex: the layer of each triangle's third vertex" : "first provoking vertex: the layer of each triangle's first vertex") + (layerField ? ", read by the pixel shader" : "")); + } + } + std::puts("mesh provoking layer tests passed: first mode draws each triangle into its first vertex's layer and last mode into its third vertex's"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MeshProvokingVertex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MeshProvokingVertex.cpp new file mode 100644 index 0000000000..1cb6bea34a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MeshProvokingVertex.cpp @@ -0,0 +1,203 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.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 +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 64; +using Image = std::array; +alignas(256) Image Pixels{}; +alignas(256) constexpr std::array Indices{0, 1, 2, 3}; +alignas(256) constexpr std::array, 4> Vertices{{ + {-0.9f, -0.8f, 0, 1, 1, 0, 0, 1}, {-0.1f, -0.8f, 0, 1, 0, 1, 0, 1}, {-0.5f, 0.8f, 0, 1, 0, 0, 1, 1}, {0.3f, 0.8f, 0, 1, 1, 1, 0, 1} +}}; +alignas(256) constexpr std::array FlatPixelCode{0xc8120002u, 0xc8160102u, 0xc81a0202u, 0xc81e0302u, 0xf800180fu, 0x07060504u, 0xbf810000u}; +alignas(256) constexpr std::array SmoothPixelCode{0xc8100000u, 0xc8110001u, 0xc8140100u, 0xc8150101u, 0xc8180200u, 0xc8190201u, 0xc81c0300u, 0xc81d0301u, 0xf800180fu, 0x07060504u, 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 Descriptor{}; + +struct Header { + Shader shader{}; + ShaderUserData users{}; +}; + +Header GeometryHeader{}; +Header FlatPixelHeader{}; +Header SmoothPixelHeader{}; + +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}); +} + +Image Draw(const std::uint32_t* pixelCode, bool flat, bool last, bool strip = false) { + Pixels.fill(std::byte{0}); + const auto color = reinterpret_cast(Pixels.data()); + const auto geometry = reinterpret_cast(GeometryCode.data()); + const auto pixel = reinterpret_cast(pixelCode); + const auto table = reinterpret_cast(Descriptor.data()); + const std::vector> context{ + {0x2d5, 0x20}, {0x1ff, 12}, {0x2ce, 3}, {0x29b, 2}, {0x2ab, 4}, {0x260, 0}, + {0x1b6, 1}, {0x1b3, 2}, {0x1b4, 2}, {0x191, flat ? 0x400u : 0u}, + {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, 0x240u | (last ? 1u << 19u : 0u)}, + {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, strip ? 6u : 4u); + 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}); + const auto address = reinterpret_cast(Indices.data()); + const std::uint32_t count = strip ? 4u : 3u; + 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(); + AgcDriver::GuestMemory::FlushGpuWrites(color, Pixels.size()); + return Pixels; +} + +unsigned Channel(const Image& image, std::uint32_t x, std::uint32_t y, std::uint32_t channel) { + return std::to_integer(image[(y * Width + x) * 4u + channel]); +} + +constexpr std::array, 5> Inside{{{12, 32}, {16, 32}, {20, 32}, {16, 28}, {16, 36}}}; + +void FlatInputs() { + for (const auto last : {false, true, false}) { + const auto image = Draw(FlatPixelCode.data(), true, last); + const std::array expected{last ? 0u : 255u, 0u, last ? 255u : 0u, 255u}; + const std::string mode = last ? "last" : "first"; + for (const auto& [x, y] : Inside) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + Require(Channel(image, x, y, channel) == expected[channel], "flat input, " + mode + " provoking vertex: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Channel(image, x, y, channel)) + ", expected " + std::to_string(expected[channel]) + " (the color of vertex " + (last ? "2" : "0") + ")"); + } + } + Require(Channel(image, 48, 48, 3) == 0, "flat input, " + mode + " provoking vertex: the draw wrote outside its triangle"); + } +} + +constexpr std::array, 4> SecondInside{{{26, 48}, {32, 48}, {29, 44}, {29, 52}}}; + +void CheckFlat(const Image& image, std::span> pixels, const std::array& expected, const std::string& what) { + for (const auto& [x, y] : pixels) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + Require(Channel(image, x, y, channel) == expected[channel], what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Channel(image, x, y, channel)) + ", expected " + std::to_string(expected[channel])); + } + } +} + +void StripFlatInputs() { + const auto last = Draw(FlatPixelCode.data(), true, true, true); + CheckFlat(last, Inside, {0u, 0u, 255u, 255u}, "triangle strip, last provoking vertex, triangle 0 (the color of vertex 2)"); + CheckFlat(last, SecondInside, {255u, 255u, 0u, 255u}, "triangle strip, last provoking vertex, triangle 1 (the color of vertex 3)"); + Require(Channel(last, 48, 48, 3) == 0, "triangle strip, last provoking vertex: the draw wrote outside its triangles"); + const auto first = Draw(FlatPixelCode.data(), true, false, true); + CheckFlat(first, Inside, {255u, 0u, 0u, 255u}, "triangle strip, first provoking vertex, triangle 0 (the color of vertex 0)"); +} + +void SmoothInputs() { + const auto first = Draw(SmoothPixelCode.data(), false, false); + for (const auto& [x, y] : Inside) { + for (std::uint32_t channel = 0; channel < 3; ++channel) Require(Channel(first, x, y, channel) > 16u, "smooth input: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Channel(first, x, y, channel)) + ", not a blend of the three vertex colors"); + } + const auto last = Draw(SmoothPixelCode.data(), false, true); + 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) Require(Channel(last, x, y, channel) == Channel(first, x, y, channel), "smooth input: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Channel(last, x, y, channel)) + " with the last provoking vertex and " + std::to_string(Channel(first, x, y, channel)) + " with the first"); + } + } +} + +} + +int main() { + try { + bool smooth = false; + { + 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; + } + if (!device->ProvokingVertexLast()) { + std::puts("skipped, the device has no last provoking vertex mode"); + return VulkanTestSkipped; + } + smooth = TargetHasCapability(device->Target(), spv::CapabilityFragmentBarycentricKHR); + } + const auto vertices = reinterpret_cast(Vertices.data()); + Descriptor = {static_cast(vertices), static_cast(vertices >> 32u) | (32u << 16u), static_cast(Vertices.size()), 0x01016facu}; + Register(GeometryHeader, GeometryCode.data(), sizeof(GeometryCode), 2); + Register(FlatPixelHeader, FlatPixelCode.data(), sizeof(FlatPixelCode), 1); + Register(SmoothPixelHeader, SmoothPixelCode.data(), sizeof(SmoothPixelCode), 1); + FlatInputs(); + StripFlatInputs(); + if (smooth) SmoothInputs(); + AgcDriverShutdown_nid_postfix(); + std::cout << "geometry-program triangles take flat inputs from vertex 0 with the first provoking vertex and from vertex 2 with the last, and the second triangle of a strip takes them from vertex 3 with the last" << (smooth ? ", and interpolate smooth inputs the same in both modes\n" : "; smooth inputs skipped without fragment shader barycentrics\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/MixPrecision.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp index f7d68a606b..972108e80a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp @@ -18,7 +18,6 @@ namespace { using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; -constexpr std::uint32_t Float64Capability = 10; constexpr std::uint32_t Threads = 64; constexpr std::uint32_t Inputs = 4; constexpr std::uint32_t Results = 16; @@ -286,11 +285,6 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - const auto capabilities = device->Target().supportedCapabilities; - if (std::find(capabilities.begin(), capabilities.end(), Float64Capability) == capabilities.end()) { - std::puts("skipped, v_fma_mixlo_f16 and v_fma_mixhi_f16 are computed in f64 and the device has no shaderFloat64"); - return VulkanTestSkipped; - } FillInput(); Run(*device, MixCode); Check(); 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/MultisampleCenteredLines.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleCenteredLines.cpp new file mode 100644 index 0000000000..a5e2dfc7f6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleCenteredLines.cpp @@ -0,0 +1,330 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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 Side = 16; +constexpr std::uint32_t Samples = 4; +constexpr std::uint32_t SampleShift = 2; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t WaveSize = 64; +constexpr std::uint32_t Float16x4Info = 0x0730u; +constexpr std::uint32_t MultisampledAttrib = (SampleShift << 12u) | (SampleShift << 15u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t AaConfig4x = 0x0020c002u; +constexpr std::uint32_t MsaaEnableClear = 2; +constexpr std::uint32_t StandardLocations4x = 0x622ae6aeu; +constexpr std::uint32_t Format16161616Float = 71; +constexpr std::uint32_t Type2DMsaa = 14; +constexpr std::uint32_t TileRenderTarget = 0x1b; +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t LoadResult = 12; +constexpr std::array Primitives{2u, 3u}; +constexpr std::array Drawn{0.25f, 0.5f, 0.125f, 1.0f}; +constexpr std::array DrawnBytes{std::byte{64}, std::byte{128}, std::byte{32}, std::byte{255}}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0802ff, 0x3e800000, 0x7e0a02ff, 0x3f000000, 0x7e0c02ff, 0x3e000000, 0x7e0e02ff, 0x3f800000, 0xf800180f, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array Unorm8PixelCode{ + 0x7e0802ff, std::bit_cast(64.0f / 255.0f), 0x7e0a02ff, std::bit_cast(128.0f / 255.0f), 0x7e0c02ff, std::bit_cast(32.0f / 255.0f), 0x7e0e02ff, std::bit_cast(1.0f), 0xf800180f, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array LoadCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf0001f30, 0x00011005, + 0xbf8c3f70, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, 0x80001201, 0xe070103c, + 0x80001301, 0xbf810000, +}; + +alignas(256) constexpr std::array, 4> Lines{{ + {-0.828125f, -0.4375f, 0.5f, 1.0f}, {0.90625f, 0.140625f, 0.5f, 1.0f}, + {-0.265625f, 0.90625f, 0.5f, 1.0f}, {0.34375f, -0.921875f, 0.5f, 1.0f}, +}}; + +alignas(256) std::array Buffer{}; +alignas(256) std::array, Primitives.size()> LinePixels{}; + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes) { +#ifdef _WIN32 + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); +#endif + Require(data != nullptr, "cannot allocate the multisampled color target"); + std::memset(data, 0, bytes); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + VirtualFree(data, 0, MEM_RELEASE); +#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; + std::uint8_t* data = nullptr; +}; + +AgcDriver::QueueState LineQueue(const Block& block, std::uint32_t primitive) { + const auto half = std::bit_cast(static_cast(Side) / 2.0f); + const auto extent = (Side << 16u) | Side; + AgcDriver::QueueState queue; + queue.userConfig[0x242] = primitive; + queue.context = { + {0x2d5, 0x2000}, {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, AaConfig4x}, {0x292, MsaaEnableClear}, {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}, + {0x318, static_cast(block.Address() >> 8u)}, {0x390, static_cast(block.Address() >> 40u)}, + {0x31b, 0}, {0x31c, Float16x4Info}, {0x31d, MultisampledAttrib}, + {0x3b0, ((Side - 1u) << 14u) | (Side - 1u)}, {0x3b8, TiledAttrib3}, {0x1e0, 0}, + {0xc, 0}, {0xd, extent}, {0x81, 0x80000000}, {0x82, extent}, + {0x90, 0x80000000}, {0x91, extent}, {0x94, 0x80000000}, {0x95, extent}, + {0x10f, half}, {0x110, half}, {0x111, std::bit_cast(-static_cast(Side) / 2.0f)}, {0x112, half}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, {0xb4, 0}, {0xb5, std::bit_cast(1.0f)} + }; + for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) queue.context[offset] = (offset - 0x2feu) % 4u == 0 ? StandardLocations4x : 0u; + queue.shader[0x008] = 0x100; + queue.shader[0x009] = 0; + return queue; +} + +AgcDriver::Graphics::State DecodeLines(const Block& block, std::uint32_t primitive) { + const auto state = AgcDriver::Graphics::DecodeState(LineQueue(block, primitive)); + const auto& color = state.color; + Require(state.topology == (primitive == 2u ? VK_PRIMITIVE_TOPOLOGY_LINE_LIST : VK_PRIMITIVE_TOPOLOGY_LINE_STRIP), "VGT_PRIMITIVE_TYPE " + std::to_string(primitive) + " did not decode as a line list or strip"); + Require(state.colors.size() == 1 && color.address == block.Address() && color.format == VK_FORMAT_R16G16B16A16_SFLOAT && color.samples == Samples && color.extent.width == Side && color.extent.height == Side, "the target did not decode as a 16x16 RGBA16F surface of 4 samples"); + Require(state.samples == Samples && !state.depth && !state.perSampleShading && !state.alphaToCoverage, "the lines did not decode as a 4x draw without depth, sample shading or alpha-to-coverage"); + const auto locations = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < Samples; ++sample) Require(locations[sample].x == 0.5f && locations[sample].y == 0.5f, "the decoded 4x line state does not put every sample at the pixel center"); + Require(state.viewport.x == 0.0f && state.viewport.y == static_cast(Side) && state.viewport.width == static_cast(Side) && state.viewport.height == -static_cast(Side) && state.scissor.offset.x == 0 && state.scissor.offset.y == 0 && state.scissor.extent.width == Side && state.scissor.extent.height == Side, "the decoded viewport or scissor does not cover the 16x16 target"); + return state; +} + +AgcDriver::Graphics::State OneSample(AgcDriver::Graphics::State state, std::span pixels, const AgcDriver::Graphics::ColorTargetLayout& linear) { + state.color = {reinterpret_cast(pixels.data()), {Side, Side}, VK_FORMAT_R8G8B8A8_UNORM, linear.Bytes(), 0xe4u}; + state.colors = {state.color}; + state.samples = 1; + state.customSampleLocations = false; + state.sampleLocations.fill(0); + state.sampleLocationGrid = false; + return state; +} + +void DrawLines(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state, std::span pixelCode) { + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Lines.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Lines.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()); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Lines.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +std::vector> LoadSamples(AgcDriver::VulkanDevice& device, const Block& block) { + const auto address = block.Address(); + const std::array texture{ + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16161616Float << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (SampleShift << 16u) | (TileRenderTarget << 20u) | (Type2DMsaa << 28u), + 0u, + SampleShift << 4u, + 0u, + 0u, + }; + const auto bufferAddress = reinterpret_cast(Buffer.data()); + const std::array buffer{static_cast(bufferAddress), static_cast((bufferAddress >> 32u) & 0xffffu), static_cast(Buffer.size() * 4u), 0x31016facu}; + std::vector userData(16, 0u); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(LoadCode.data()), std::as_bytes(std::span(LoadCode))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(LoadCode.data()), LoadCode, 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::vector> loaded(Side * Side * Samples); + for (std::uint32_t first = 0; first < loaded.size(); first += Threads) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto index = first + tid; + const auto pixel = index / Samples; + const std::array words{0u, 0u, 0u, pixel % Side, pixel / Side, index % Samples}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(LoadCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t component = 0; component < 4u; ++component) loaded[first + tid][component] = std::bit_cast(Buffer[tid * Words + LoadResult + component]); + } + } + return loaded; +} + +bool NearEndpoint(std::uint32_t x, std::uint32_t y) { + return std::any_of(Lines.begin(), Lines.end(), [&](const std::array& vertex) { + const double px = (static_cast(vertex[0]) + 1.0) * 0.5 * Side; + const double py = Side - (static_cast(vertex[1]) + 1.0) * 0.5 * Side; + return std::fabs(x + 0.5 - px) < 1.5 && std::fabs(y + 0.5 - py) < 1.5; + }); +} + +void CompareLines(AgcDriver::VulkanDevice& device, const Block& block, std::uint32_t primitive, std::span pixels) { + const auto decoded = DecodeLines(block, primitive); + DrawLines(device, decoded, PixelCode); + const auto loaded = LoadSamples(device, block); + const AgcDriver::Graphics::ColorTargetLayout linear(Side, Side, AgcDriver::Graphics::ColorTileMode::Linear, 4u); + Require(linear.Bytes() <= pixels.size(), "the one-sample line target does not fit its memory"); + std::fill(pixels.begin(), pixels.end(), std::byte{0}); + DrawLines(device, OneSample(decoded, pixels, linear), Unorm8PixelCode); + std::uint32_t covered = 0; + std::uint32_t single = 0; + std::uint32_t compared = 0; + std::uint32_t differing = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + char where[80]; + std::snprintf(where, sizeof(where), "VGT_PRIMITIVE_TYPE %u: pixel (%u, %u)", primitive, x, y); + std::uint32_t drawn = 0; + for (std::uint32_t sample = 0; sample < Samples; ++sample) { + const auto& texel = loaded[(y * Side + x) * Samples + sample]; + Require(texel == Drawn || texel == std::array{}, std::string(where) + " of the 4x target holds neither the line color nor the cleared value"); + drawn += texel == Drawn ? 1u : 0u; + } + Require(drawn == 0u || drawn == Samples, std::string(where) + " has " + std::to_string(drawn) + " of its 4 samples covered by a line with every sample at the pixel center"); + const auto bytes = pixels.begin() + static_cast(linear.Offset(x, y)); + const bool oneSample = std::equal(DrawnBytes.begin(), DrawnBytes.end(), bytes); + Require(oneSample || std::all_of(bytes, bytes + 4, [](std::byte value) { return value == std::byte{0}; }), std::string(where) + " of the one-sample target holds neither the line color nor zero"); + covered += drawn == Samples ? 1u : 0u; + single += oneSample ? 1u : 0u; + if (NearEndpoint(x, y)) continue; + ++compared; + differing += (drawn == Samples) != oneSample ? 1u : 0u; + Require(!oneSample || drawn == Samples, std::string(where) + " is drawn by the one-sample line but not covered in the 4x target"); + } + } + Require(covered >= 20u && covered <= 2u * single && compared >= 200u, "VGT_PRIMITIVE_TYPE " + std::to_string(primitive) + ": the 4x lines covered " + std::to_string(covered) + " pixels and the one-sample lines " + std::to_string(single) + ", with " + std::to_string(compared) + " pixels compared"); + std::printf("VGT_PRIMITIVE_TYPE %u: 4x lines with every sample at the pixel center covered %u pixels in full and none in part; the one-sample lines drew %u; %u of the %u pixels away from the endpoints differ\n", primitive, covered, single, differing, compared); +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).storageImageMultisample) { + std::puts("skipped, the device has no shaderStorageImageMultisample to load the samples of a 4x target"); + return VulkanTestSkipped; + } + if ((AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).sampleLocationSampleCounts & Samples) == 0u) { + std::puts("skipped, the device has no VK_EXT_sample_locations at 4 samples to put every sample at the pixel center"); + return VulkanTestSkipped; + } + Block listTarget(4u * BlockBytes); + Block stripTarget(4u * BlockBytes); + struct ReleaseDevice { + std::unique_ptr& value; + ~ReleaseDevice() { value.reset(); } + } releaseDevice{device}; + const std::array targets{&listTarget, &stripTarget}; + for (std::size_t index = 0; index < Primitives.size(); ++index) CompareLines(*device, *targets[index], Primitives[index], LinePixels[index]); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleFillHostOnly.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleFillHostOnly.cpp index 1f4a518a9c..354b880d73 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleFillHostOnly.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleFillHostOnly.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" @@ -23,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -154,6 +156,10 @@ bool FillsTakenAsMultisampledClearsAreNotStored(AgcDriver::VulkanDevice& device) VkDevice handle; ~ClearTargets() { std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + if (auto* active = AgcDriver::Graphics::Recorder::Active(); active != nullptr && active->Device() != handle) { + active->Sync(); + active->FlushDeferredReleases(); + } device.SubmitRecorded(true); device.WaitIdle(); AgcDriver::Graphics::ClearMultisampleTargets(handle); @@ -179,7 +185,12 @@ bool FillsTakenAsMultisampledClearsAreNotStored(AgcDriver::VulkanDevice& device) Require(AgcDriver::Graphics::MultisampledColorBytes(target) == BlockBytes, "the 4x 64x64 RGBA8 target does not take one 64 KiB block"); { std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); - Require(AgcDriver::Graphics::MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the multisampled target has no view"); + auto* active = AgcDriver::Graphics::Recorder::Active(); + Require(active != nullptr && active->Device() != context.device, "the fill test requires a second device's active recorder"); + const auto keptBytes = active->OpenKeptBytes(); + std::shared_ptr owner; + Require(AgcDriver::Graphics::MultisampleTargetView(context, target, owner) != VK_NULL_HANDLE && owner != nullptr, "the multisampled target has no view"); + Require(active->OpenKeptBytes() == keptBytes, "the multisampled target was charged to another device's recorder"); device.SubmitRecorded(true); device.WaitIdle(); } @@ -205,7 +216,11 @@ bool FillsTakenAsMultisampledClearsAreNotStored(AgcDriver::VulkanDevice& device) srgb.format = VK_FORMAT_R8G8B8A8_SRGB; { std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + auto* active = AgcDriver::Graphics::Recorder::Active(); + Require(active != nullptr && active->Device() != context.device, "the sRGB fill test requires a second device's active recorder"); + const auto keptBytes = active->OpenKeptBytes(); Require(AgcDriver::Graphics::MultisampleTargetView(context, srgb) != VK_NULL_HANDLE, "the sRGB multisampled target has no view"); + Require(active->OpenKeptBytes() == keptBytes, "the sRGB multisampled target was charged to another device's recorder"); device.SubmitRecorded(true); device.WaitIdle(); } @@ -226,6 +241,17 @@ bool FillsTakenAsMultisampledClearsAreNotStored(AgcDriver::VulkanDevice& device) DispatchFill(code, color.Address(), BlockBytes, 0); Settle(code, sentinel); Require(color.Holds(std::byte{}, 0, BlockBytes), "a compute fill over a retired target's color memory was not stored"); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + std::shared_ptr owner; + Require(AgcDriver::Graphics::MultisampleTargetView(context, target, owner) != VK_NULL_HANDLE && owner != nullptr, "the replacement multisampled target has no view"); + const std::weak_ptr targetOwner = owner; + owner.reset(); + device.SubmitRecorded(true); + device.WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(context.device); + Require(targetOwner.expired(), "another device's recorder retained a multisampled target after its device's work completed"); + } return true; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleReadback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleReadback.cpp new file mode 100644 index 0000000000..a6c84ad27f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleReadback.cpp @@ -0,0 +1,575 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTextureLayout.hpp" +#include "VulkanTestDevice.hpp" +#include "MsaaReadbackVertex_spv.h" +#include "MsaaReadbackUint_spv.h" +#include "MsaaReadbackDepth_spv.h" +#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 namespace AgcDriver::Graphics; + +PFN_vkGetDeviceProcAddr originalDeviceProc = nullptr; +PipelineCacheSynchronization* pipelineCacheSynchronization = nullptr; +std::uint32_t checkedPipelineCacheUses = 0u; + +VKAPI_ATTR VkResult VKAPI_CALL CheckedCreateComputePipelines(VkDevice device, VkPipelineCache cache, std::uint32_t count, const VkComputePipelineCreateInfo* infos, const VkAllocationCallbacks* allocator, VkPipeline* pipelines) { + bool snapshotAvailable = false; + std::thread probe([&] { + const auto snapshot = pipelineCacheSynchronization->TryLockForSnapshot(); + snapshotAvailable = snapshot.owns_lock(); + }); + probe.join(); + Require(!snapshotAvailable, "sample reader pipeline creation did not hold the cache use lock"); + ++checkedPipelineCacheUses; + const auto create = reinterpret_cast(originalDeviceProc(device, "vkCreateComputePipelines")); + Require(create != nullptr, "readback fixture cannot resolve compute pipeline creation"); + return create(device, cache, count, infos, allocator, pipelines); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL CheckedDeviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateComputePipelines") == 0) return reinterpret_cast(CheckedCreateComputePipelines); + return originalDeviceProc(device, name); +} + +constexpr bool SurfaceFits(const VkImageFormatProperties& supported, std::uint32_t samples, std::uint32_t width = 17u, std::uint32_t height = 5u, std::uint32_t layers = 2u) { + return (supported.sampleCounts & samples) != 0u && width <= supported.maxExtent.width && height <= supported.maxExtent.height && layers <= supported.maxArrayLayers; +} + +void CheckSupportSelection() { + constexpr VkImageFormatProperties supported{{128u, 64u, 1u}, 1u, 2u, VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_4_BIT | VK_SAMPLE_COUNT_8_BIT, 0u}; + static_assert(!SurfaceFits(supported, 2u)); + static_assert(SurfaceFits(supported, 4u) && SurfaceFits(supported, 8u)); + static_assert(SurfaceFits(supported, 4u, 128u, 64u, 1u)); + static_assert(!SurfaceFits(supported, 4u, 129u, 64u, 1u)); + static_assert(!SurfaceFits(supported, 4u, 128u, 65u, 1u)); + static_assert(!SurfaceFits(supported, 4u, 17u, 5u, 3u)); + static_assert(!SurfaceFits(VkImageFormatProperties{}, 4u)); +} + +bool SurfaceSupported(const Context& context, VkFormat format, std::uint32_t samples, std::uint32_t width = 17u, std::uint32_t height = 5u, std::uint32_t layers = 2u, bool writable = false) { + const VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | (format == VK_FORMAT_D32_SFLOAT ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) | (writable ? VK_IMAGE_USAGE_STORAGE_BIT : 0u); + VkImageFormatProperties supported{}; + const auto result = context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0u, &supported); + if (result != VK_ERROR_FORMAT_NOT_SUPPORTED) Check(result, "readback fixture format query"); + if (result == VK_SUCCESS && SurfaceFits(supported, samples, width, height, layers)) return true; + std::printf("skipped, the device does not support readback fixture format=%u samples=%u extent=%ux%u layers=%u\n", static_cast(format), samples, width, height, layers); + return false; +} + +struct Surface { + Context context; + VkFormat format; + std::uint32_t elementBytes; + std::uint32_t samples; + std::uint32_t width; + std::uint32_t height; + std::uint32_t layers; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView arrayView = VK_NULL_HANDLE; + VkRenderPass pass = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; + std::array modules{}; + std::vector views; + std::vector framebuffers; + + Surface(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples, std::uint32_t width = 17u, std::uint32_t height = 5u, std::uint32_t layers = 2u, bool writable = false) : context(context), format(format), elementBytes(elementBytes), samples(samples), width(width), height(height), layers(layers) { + try { + const bool depth = format == VK_FORMAT_D32_SFLOAT; + const auto aspect = depth ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT; + const VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | (depth ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) | (writable ? VK_IMAGE_USAGE_STORAGE_BIT : 0u); + VkImageFormatProperties supported{}; + Check(context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0u, &supported), "readback fixture format query"); + Require(SurfaceFits(supported, samples, width, height, layers), "readback fixture format does not support its sample count or extent"); + Require(context.sampleRateShading, "readback fixture needs per-sample shading"); + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {width, height, 1u}; + info.mipLevels = 1u; + info.arrayLayers = layers; + info.samples = static_cast(samples); + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = usage; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "readback fixture image"); + 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), "readback fixture memory"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0u), "readback fixture bind"); + VkImageViewCreateInfo view{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + view.image = image; + view.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY; + view.format = format; + view.subresourceRange = {aspect, 0u, 1u, 0u, layers}; + Check(context.Function("vkCreateImageView")(context.device, &view, nullptr, &arrayView), "readback fixture array view"); + VkAttachmentDescription attachment{}; + attachment.format = format; + attachment.samples = info.samples; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + 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 reference{0u, VK_IMAGE_LAYOUT_GENERAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + if (depth) subpass.pDepthStencilAttachment = &reference; + else { subpass.colorAttachmentCount = 1u; subpass.pColorAttachments = &reference; } + VkRenderPassCreateInfo renderPass{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + renderPass.attachmentCount = 1u; + renderPass.pAttachments = &attachment; + renderPass.subpassCount = 1u; + renderPass.pSubpasses = &subpass; + Check(context.Function("vkCreateRenderPass")(context.device, &renderPass, nullptr, &pass), "readback fixture render pass"); + views.resize(layers); + framebuffers.resize(layers); + for (std::uint32_t layer = 0u; layer < layers; ++layer) { + view.viewType = VK_IMAGE_VIEW_TYPE_2D; + view.subresourceRange.baseArrayLayer = layer; + view.subresourceRange.layerCount = 1u; + Check(context.Function("vkCreateImageView")(context.device, &view, nullptr, &views[layer]), "readback fixture layer view"); + VkFramebufferCreateInfo framebuffer{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebuffer.renderPass = pass; + framebuffer.attachmentCount = 1u; + framebuffer.pAttachments = &views[layer]; + framebuffer.width = width; + framebuffer.height = height; + framebuffer.layers = 1u; + Check(context.Function("vkCreateFramebuffer")(context.device, &framebuffer, nullptr, &framebuffers[layer]), "readback fixture framebuffer"); + } + const auto makeModule = [&](std::span words, VkShaderModule& module) { + VkShaderModuleCreateInfo shader{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + shader.codeSize = words.size_bytes(); + shader.pCode = words.data(); + Check(context.Function("vkCreateShaderModule")(context.device, &shader, nullptr, &module), "readback fixture shader"); + }; + makeModule(MSAA_READBACK_VERTEX_SPV, modules[0]); + makeModule(depth ? std::span(MSAA_READBACK_DEPTH_SPV) : std::span(MSAA_READBACK_UINT_SPV), modules[1]); + const VkPushConstantRange push{VK_SHADER_STAGE_FRAGMENT_BIT, 0u, 5u * sizeof(std::uint32_t)}; + VkPipelineLayoutCreateInfo layout{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layout.pushConstantRangeCount = 1u; + layout.pPushConstantRanges = &push; + Check(context.Function("vkCreatePipelineLayout")(context.device, &layout, nullptr, &pipelineLayout), "readback fixture pipeline layout"); + std::array stages{}; + for (std::uint32_t index = 0; index < stages.size(); ++index) { + stages[index] = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + stages[index].stage = index == 0u ? VK_SHADER_STAGE_VERTEX_BIT : VK_SHADER_STAGE_FRAGMENT_BIT; + stages[index].module = modules[index]; + stages[index].pName = "main"; + } + const VkPipelineVertexInputStateCreateInfo vertices{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + const VkViewport viewport{0.0f, 0.0f, static_cast(width), static_cast(height), 0.0f, 1.0f}; + const VkRect2D scissor{{0, 0}, {width, height}}; + VkPipelineViewportStateCreateInfo viewportState{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewportState.viewportCount = viewportState.scissorCount = 1u; + viewportState.pViewports = &viewport; + viewportState.pScissors = &scissor; + VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + raster.polygonMode = VK_POLYGON_MODE_FILL; + raster.cullMode = VK_CULL_MODE_NONE; + raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + raster.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = info.samples; + multisample.sampleShadingEnable = VK_TRUE; + multisample.minSampleShading = 1.0f; + VkPipelineDepthStencilStateCreateInfo depthState{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; + depthState.depthTestEnable = depthState.depthWriteEnable = depth ? VK_TRUE : VK_FALSE; + depthState.depthCompareOp = VK_COMPARE_OP_ALWAYS; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 15u; + VkPipelineColorBlendStateCreateInfo color{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + color.attachmentCount = depth ? 0u : 1u; + color.pAttachments = &blend; + VkGraphicsPipelineCreateInfo graphics{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + graphics.stageCount = static_cast(stages.size()); + graphics.pStages = stages.data(); + graphics.pVertexInputState = &vertices; + graphics.pInputAssemblyState = &assembly; + graphics.pViewportState = &viewportState; + graphics.pRasterizationState = &raster; + graphics.pMultisampleState = &multisample; + graphics.pDepthStencilState = &depthState; + graphics.pColorBlendState = &color; + graphics.layout = pipelineLayout; + graphics.renderPass = pass; + Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1u, &graphics, nullptr, &pipeline), "readback fixture graphics pipeline"); + } catch (...) { Release(); throw; } + } + + ~Surface() { Release(); } + Surface(const Surface&) = delete; + Surface& operator=(const Surface&) = delete; + + void Release() noexcept { + if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + if (pipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, pipelineLayout, nullptr); + for (const auto module : modules) if (module) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + for (const auto framebuffer : framebuffers) if (framebuffer) context.Function("vkDestroyFramebuffer")(context.device, framebuffer, nullptr); + if (pass) context.Function("vkDestroyRenderPass")(context.device, pass, nullptr); + for (const auto view : views) if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); + if (arrayView) context.Function("vkDestroyImageView")(context.device, arrayView, nullptr); + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + } + + void Render() { + CommandBatch batch(context); + for (std::uint32_t layer = 0; layer < layers; ++layer) { + const VkClearValue clear{}; + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = pass; + begin.framebuffer = framebuffers[layer]; + begin.renderArea = {{0, 0}, {width, height}}; + begin.clearValueCount = 1u; + begin.pClearValues = &clear; + const std::array push{layer, width, height, samples, elementBytes}; + context.Function("vkCmdBeginRenderPass")(batch.Handle(), &begin, VK_SUBPASS_CONTENTS_INLINE); + context.Function("vkCmdBindPipeline")(batch.Handle(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + context.Function("vkCmdPushConstants")(batch.Handle(), pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0u, sizeof(push), push.data()); + context.Function("vkCmdDraw")(batch.Handle(), 3u, 1u, 0u, 0u); + context.Function("vkCmdEndRenderPass")(batch.Handle()); + } + batch.SubmitAndWait(); + } + + std::vector Expected(std::uint32_t first, std::uint32_t elements) const { + std::vector result(static_cast(elements) * elementBytes); + for (std::uint32_t index = 0u; index < elements; ++index) { + const auto element = first + index; + const auto sample = element % samples; + const auto pixel = element / samples; + const auto x = pixel % width; + const auto y = (pixel / width) % height; + const auto layer = pixel / (width * height); + const auto value = x + y * width + layer * width * height + sample * 31u; + std::array words{0x91352700u ^ value, 0x45219b00u ^ (value * 3u), 0xc7391d00u ^ (value * 5u), 0x6b43e100u ^ (value * 7u)}; + if (format == VK_FORMAT_D32_SFLOAT) words[0] = std::bit_cast(0.125f + static_cast(element + 1u) / 1048576.0f); + std::memcpy(result.data() + static_cast(index) * elementBytes, words.data(), elementBytes); + } + return result; + } +}; + +std::vector CheckReadback(TextureDetiler& reader, const Surface& surface, std::uint32_t first, std::uint32_t elements, std::uint32_t repetitions, std::size_t extraBytes = 0u, std::span supplied = {}) { + const auto expected = supplied.empty() ? surface.Expected(first, elements) : std::vector(supplied.begin(), supplied.end()); + const auto bytes = ((expected.size() + 3u) & ~std::size_t{3u}) + extraBytes; + const auto alignment = std::max(4u, surface.context.limits.minStorageBufferOffsetAlignment); + const auto offset = (16u + alignment - 1u) / alignment * alignment; + Buffer output(surface.context, offset + bytes + 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::fill(output.Bytes().begin(), output.Bytes().end(), std::byte{0xa5}); + reader.BeginBatch(); + CommandBatch batch(surface.context); + RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + for (std::uint32_t repeat = 0u; repeat < repetitions; ++repeat) { + reader.DispatchMultisampleReadback(batch.Handle(), surface.format, output.Handle(), offset, bytes, surface.arrayView, surface.width, surface.height, surface.layers, surface.samples, first, elements); + if (repeat + 1u < repetitions) RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + } + RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + output.Invalidate(); + const auto data = output.Bytes(); + const auto mismatch = std::mismatch(expected.begin(), expected.end(), data.begin() + offset); + if (mismatch.first != expected.end()) std::printf("readback mismatch: format=%u first=%u byte=%zu expected=0x%02x actual=0x%02x\n", static_cast(surface.format), first, static_cast(mismatch.first - expected.begin()), std::to_integer(*mismatch.first), std::to_integer(*mismatch.second)); + Require(std::equal(expected.begin(), expected.end(), data.begin() + offset), "multisample readback changed a rendered sample, component, layer or pixel"); + for (std::size_t index = 0u; index < data.size(); ++index) Require((index >= offset && index < offset + expected.size()) || data[index] == std::byte{0xa5}, "multisample readback changed a buffer sentinel or packed tail"); + return {data.begin() + offset, data.begin() + offset + expected.size()}; +} + +void CheckRetile(TextureDetiler& reader, const Surface& surface) { + const auto elements = surface.width * surface.height * surface.layers * surface.samples; + const auto linear = CheckReadback(reader, surface, 0u, elements, 1u); + const auto expected = surface.Expected(0u, elements); + constexpr std::array, 7> coordinates{{ + {0u, 0u, 0u}, {128u, 66u, 16u}, {64u, 32u, 1u}, {32u, 64u, 2u}, + {63u, 31u, 7u}, {127u, 65u, 15u}, {1u, 1u, 3u} + }}; + constexpr std::array modes{TextureTileMode::kR64KBX, TextureTileMode::kZ64KBX}; + constexpr std::array, 2> references{{ + {0ull, 6621216ull, 493568ull, 1016832ull, 2766588ull, 6170780ull, 1215508ull}, + {0ull, 6637700ull, 460808ull, 1000460ull, 2815728ull, 6219892ull, 1182776ull} + }}; + for (std::size_t mode = 0u; mode < modes.size(); ++mode) { + for (const auto pipeBankXor : {0u, 0x8100u}) { + const MultisampleTextureLayout layout(modes[mode], surface.elementBytes, surface.samples, surface.width, surface.height, surface.layers, pipeBankXor); + std::vector tiled(static_cast(layout.Bytes()) + 32u, std::byte{0xa5}); + auto payload = std::span(tiled).subspan(16u, layout.Bytes()); + layout.Tile(linear, payload); + std::vector covered(payload.size()); + std::size_t position = 0u; + for (std::uint32_t layer = 0u; layer < surface.layers; ++layer) { + for (std::uint32_t y = 0u; y < surface.height; ++y) { + for (std::uint32_t x = 0u; x < surface.width; ++x) { + for (std::uint32_t fragment = 0u; fragment < surface.samples; ++fragment) { + const auto address = layout.Offset(x, y, layer, fragment); + Require(address + surface.elementBytes <= payload.size(), "readback retile exceeds its allocation"); + for (std::uint32_t byte = 0u; byte < surface.elementBytes; ++byte) { + Require(!covered[address + byte], "readback retile elements overlap"); + covered[address + byte] = true; + Require(payload[address + byte] == expected[position++], "readback and retile disagree on layer, pixel or fragment order"); + } + } + } + } + } + for (std::size_t index = 0u; index < covered.size(); ++index) Require(covered[index] || payload[index] == std::byte{0xa5}, "readback retile changed tile padding"); + for (std::size_t index = 0u; index < coordinates.size(); ++index) { + const auto& coordinate = coordinates[index]; + const auto fragment = static_cast(index) % surface.samples; + const auto source = (((coordinate[2] * surface.height + coordinate[1]) * surface.width + coordinate[0]) * surface.samples + fragment) * surface.elementBytes; + const auto reference = references[mode][index]; + const auto address = (reference & ~65535ull) | ((reference & 65535ull) ^ pipeBankXor); + Require(address + surface.elementBytes <= payload.size() && std::equal(expected.begin() + source, expected.begin() + source + surface.elementBytes, payload.begin() + address), "rendered readback differs at an AMD reference fragment address"); + } + std::vector restored(expected.size()); + layout.Detile(payload, restored); + Require(restored == expected && linear == expected, "readback retile roundtrip changed rendered element bits"); + for (std::size_t index = 0u; index < tiled.size(); ++index) Require((index >= 16u && index < 16u + payload.size()) || tiled[index] == std::byte{0xa5}, "readback retile changed an allocation sentinel"); + std::printf("multisample readback retile: format=%u mode=%s xor=0x%x samples=%u layers=%u elements=%u reference-points=7 padding/sentinels/roundtrip=passed\n", static_cast(surface.format), mode == 0u ? "R_X" : "Z_X", pipeBankXor, surface.samples, surface.layers, elements); + } + } +} + +template +void Refused(TAction action, const char* expected) { + std::string reason; + try { action(); } catch (const std::exception& error) { reason = error.what(); } + Require(reason.find(expected) != std::string::npos, "multisample readback missed an exact invalid-input refusal: " + reason); +} + +void CheckRefusals(TextureDetiler& reader, const Surface& surface) { + Buffer output(surface.context, 4096u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + CommandBatch batch(surface.context); + const auto call = [&](VkFormat format, std::uint64_t offset, std::uint64_t bytes, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t fragments, std::uint32_t first, std::uint32_t elements) { + reader.DispatchMultisampleReadback(batch.Handle(), format, output.Handle(), offset, bytes, surface.arrayView, width, height, layers, fragments, first, elements); + }; + for (auto format : {VK_FORMAT_UNDEFINED, VK_FORMAT_R32_SFLOAT, VK_FORMAT_D16_UNORM, VK_FORMAT_D32_SFLOAT_S8_UINT}) Refused([&] { call(format, 0u, 4u, 1u, 1u, 1u, 4u, 0u, 1u); }, "requires an unsigned element view or D32 depth"); + for (auto fragments : {0u, 1u, 3u, 16u}) Refused([&] { call(surface.format, 0u, 4u, 1u, 1u, 1u, fragments, 0u, 1u); }, "fragment count is unsupported"); + for (auto extent : {std::array{0u, 1u, 1u}, std::array{1u, 0u, 1u}, std::array{1u, 1u, 0u}, std::array{16385u, 1u, 1u}, std::array{1u, 16385u, 1u}, std::array{1u, 1u, 8193u}}) Refused([&] { call(surface.format, 0u, 4u, extent[0], extent[1], extent[2], 4u, 0u, 1u); }, "extent is unsupported"); + for (auto range : {std::array{0u, 0u}, std::array{4u, 1u}, std::array{3u, 2u}, std::array{0xffffffffu, 2u}}) Refused([&] { call(surface.format, 0u, 4u, 1u, 1u, 1u, 4u, range[0], range[1]); }, "element range is invalid"); + for (auto range : {std::array{1u, 4u}, std::array{0u, 0u}, std::array{0u, 3u}, std::array{0u, static_cast(surface.context.limits.maxStorageBufferRange) + 4u}, std::array{~std::uint64_t{0} - 3u, 8u}}) Refused([&] { call(surface.format, range[0], range[1], 1u, 1u, 1u, 4u, 0u, 1u); }, "buffer range is invalid"); + Refused([&] { reader.DispatchMultisampleReadback(VK_NULL_HANDLE, surface.format, output.Handle(), 0u, 4u, surface.arrayView, 1u, 1u, 1u, 4u, 0u, 1u); }, "requires commands, buffer and array view"); + Refused([&] { reader.DispatchMultisampleReadback(batch.Handle(), surface.format, VK_NULL_HANDLE, 0u, 4u, surface.arrayView, 1u, 1u, 1u, 4u, 0u, 1u); }, "requires commands, buffer and array view"); + Refused([&] { reader.DispatchMultisampleReadback(batch.Handle(), surface.format, output.Handle(), 0u, 4u, VK_NULL_HANDLE, 1u, 1u, 1u, 4u, 0u, 1u); }, "requires commands, buffer and array view"); + Refused([&] { call(surface.format, 0u, 4u, 16384u, 16384u, 8192u, 8u, 0u, 1u); }, "element range is invalid"); + std::puts("multisample readback invalid-input refusals passed"); +} + +std::vector Upload(TextureDetiler& transfer, TextureDetiler& reader, const Surface& surface, std::vector& expected, std::uint32_t first, std::uint32_t elements, std::uint32_t repetitions = 1u, std::span supplied = {}) { + const auto bytes = static_cast(elements) * surface.elementBytes; + const auto padded = (bytes + 3u) & ~std::size_t{3}; + const auto alignment = std::max({4u, surface.elementBytes, surface.context.limits.minStorageBufferOffsetAlignment}); + const auto offset = (16u + alignment - 1u) / alignment * alignment; + Buffer source(surface.context, offset + padded + 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::fill(source.Bytes().begin(), source.Bytes().end(), std::byte{0x5a}); + Require(supplied.empty() || supplied.size() == bytes, "multisample upload fixture source size mismatch"); + for (std::size_t index = 0; index < bytes; ++index) source.Bytes()[offset + index] = supplied.empty() ? static_cast((index * 29u + first * 17u + elements * 7u) & 255u) : supplied[index]; + const std::vector retained(source.Bytes().begin(), source.Bytes().end()); + std::copy_n(retained.begin() + offset, bytes, expected.begin() + static_cast(first) * surface.elementBytes); + const auto stride = 256u / alignment * alignment; + Require(stride != 0u, "upload fixture cannot hold an aligned chunk"); + transfer.BeginBatch(); + CommandBatch batch(surface.context); + RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + for (std::uint32_t repeat = 0; repeat < repetitions; ++repeat) { + for (std::size_t done = 0; done < bytes; done += stride) { + const auto count = std::min(stride, bytes - done); + transfer.DispatchMultisampleImage(batch.Handle(), surface.elementBytes, source.Handle(), offset + done, (count + 3u) & ~std::size_t{3}, surface.arrayView, surface.width, surface.height, surface.layers, surface.samples, first + static_cast(done / surface.elementBytes), static_cast(count / surface.elementBytes)); + } + RecordMemoryBarrier(surface.context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + } + batch.SubmitAndWait(); + source.Invalidate(); + Require(std::equal(retained.begin(), retained.end(), source.Bytes().begin()), "multisample upload changed source bytes or sentinels"); + return CheckReadback(reader, surface, 0u, surface.width * surface.height * surface.layers * surface.samples, 1u, 0u, expected); +} + +void CheckUploadRefusals(TextureDetiler& transfer, const Surface& surface) { + Buffer source(surface.context, 256u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + CommandBatch batch(surface.context); + const auto call = [&](std::uint32_t bytes, std::uint32_t fragments, std::uint32_t first, std::uint32_t elements, std::uint64_t offset = 0u, std::uint64_t range = 4u) { + transfer.DispatchMultisampleImage(batch.Handle(), bytes, source.Handle(), offset, range, surface.arrayView, 1u, 1u, 1u, fragments, first, elements); + }; + for (const auto bytes : {0u, 3u, 32u}) Refused([&] { call(bytes, 4u, 0u, 1u); }, "element size is unsupported"); + for (const auto fragments : {0u, 1u, 3u, 16u}) Refused([&] { call(4u, fragments, 0u, 1u); }, "fragment count is unsupported"); + for (const auto range : {std::array{0u, 0u}, std::array{4u, 1u}, std::array{3u, 2u}, std::array{0xffffffffu, 2u}}) Refused([&] { call(4u, 4u, range[0], range[1]); }, "element range is invalid"); + for (const auto range : {std::array{1u, 4u}, std::array{0u, 0u}, std::array{0u, 3u}, std::array{0u, 260u}, std::array{~std::uint64_t{0} - 3u, 8u}}) Refused([&] { call(4u, 4u, 0u, 1u, range[0], range[1]); }, "buffer range is invalid"); + Refused([&] { transfer.DispatchMultisampleImage(VK_NULL_HANDLE, 4u, source.Handle(), 0u, 4u, surface.arrayView, 1u, 1u, 1u, 4u, 0u, 1u); }, "requires commands, buffer and array view"); + Refused([&] { transfer.DispatchMultisampleImage(batch.Handle(), 4u, VK_NULL_HANDLE, 0u, 4u, surface.arrayView, 1u, 1u, 1u, 4u, 0u, 1u); }, "requires commands, buffer and array view"); + Refused([&] { transfer.DispatchMultisampleImage(batch.Handle(), 4u, source.Handle(), 0u, 4u, VK_NULL_HANDLE, 1u, 1u, 1u, 4u, 0u, 1u); }, "requires commands, buffer and array view"); + for (const auto extent : {std::array{0u, 1u, 1u}, std::array{1u, 0u, 1u}, std::array{1u, 1u, 0u}, std::array{16385u, 1u, 1u}, std::array{1u, 16385u, 1u}, std::array{1u, 1u, 8193u}}) Refused([&] { transfer.DispatchMultisampleImage(batch.Handle(), 4u, source.Handle(), 0u, 4u, surface.arrayView, extent[0], extent[1], extent[2], 4u, 0u, 1u); }, "extent is unsupported"); + Refused([&] { transfer.DispatchMultisampleImage(batch.Handle(), 4u, source.Handle(), 0u, 4u, surface.arrayView, 16384u, 16384u, 8192u, 8u, 0u, 1u); }, "element range is invalid"); + for (const bool multisample : {false, true}) { + auto unsupported = surface.context; + if (multisample) unsupported.storageImageMultisample = false; + else unsupported.storageImageWriteWithoutFormat = false; + TextureDetiler unavailable(unsupported); + Refused([&] { unavailable.DispatchMultisampleImage(batch.Handle(), 4u, source.Handle(), 0u, 4u, surface.arrayView, 1u, 1u, 1u, 4u, 0u, 1u); }, "does not enable multisampled storage image writes without format"); + } +} + +void CheckUpload(const Context& context) { + Require(context.storageImageMultisample && context.storageImageWriteWithoutFormat, "upload fixture requires enabled multisampled formatless storage writes"); + auto limited = context; + limited.limits.maxComputeWorkGroupCount[0] = 1u; + limited.limits.maxStorageBufferRange = 256u; + TextureDetiler transfer(limited); + TextureDetiler reader(context); + std::uint32_t exercised = 0u; + for (const auto bytes : {1u, 2u, 4u, 8u, 16u}) { + const auto format = TextureDetiler::ImageElementFormat(bytes); + for (const auto samples : {2u, 4u, 8u}) { + if (!SurfaceSupported(context, format, samples, 17u, 5u, 2u, true)) continue; + Surface surface(context, format, bytes, samples, 17u, 5u, 2u, true); + surface.Render(); + const auto elements = surface.width * surface.height * surface.layers * samples; + auto expected = surface.Expected(0u, elements); + Upload(transfer, reader, surface, expected, 3u, 1u); + Upload(transfer, reader, surface, expected, 1u, elements - 3u); + Upload(transfer, reader, surface, expected, 0u, elements); + Upload(transfer, reader, surface, expected, elements - 1u, 1u, samples == 4u ? 65u : 1u); + if (bytes == 4u && samples == 4u) CheckUploadRefusals(transfer, surface); + std::printf("multisample upload: bytes=%u samples=%u layers=2 full/partial/packed/source-sentinels/chunks/reuse=passed\n", bytes, samples); + ++exercised; + } + } + Require(exercised != 0u, "no supported multisample upload subcase was exercised"); + const MultisampleTextureLayout active(TextureTileMode::kR64KBX, 8u, 4u, 17u, 5u, 2u); + if (SurfaceSupported(context, VK_FORMAT_R32G32_UINT, 4u, active.Pitch(), active.PaddedHeight(), 2u, true)) { + for (const auto xorValue : {0u, 0x8100u}) { + const MultisampleTextureLayout layout(TextureTileMode::kR64KBX, 8u, 4u, active.Pitch(), active.PaddedHeight(), 2u, xorValue); + std::vector tiled(static_cast(layout.Bytes())); + for (std::size_t byte = 0; byte < tiled.size(); ++byte) tiled[byte] = static_cast((byte * 37u + (byte >> 8u) * 13u + (byte >> 15u) * 71u + (byte >> 16u) * 101u) & 255u); + std::vector linear(static_cast(layout.LinearBytes())); + layout.Detile(tiled, linear); + Surface surface(context, VK_FORMAT_R32G32_UINT, 8u, 4u, layout.Pitch(), layout.PaddedHeight(), 2u, true); + surface.Render(); + const auto elements = surface.width * surface.height * surface.layers * surface.samples; + auto expected = surface.Expected(0u, elements); + const auto actual = Upload(transfer, reader, surface, expected, 0u, elements, 1u, linear); + std::vector retiled(tiled.size(), std::byte{0xa5}); + layout.Tile(actual, retiled); + Require(retiled == tiled, "multisample padded initialization changed color bytes, fragments, layers, XOR or padding"); + std::printf("multisample padded upload: active=17x5 backing=%ux%u layers=2 fragments=4 bytes=%zu xor=0x%x exact roundtrip passed\n", surface.width, surface.height, retiled.size(), xorValue); + } + } +} + +} + +int main(int argc, char** argv) { + try { + CheckSupportSelection(); + const std::string mode = argc == 1 ? "uint32" : argv[1]; + VkFormat format = VK_FORMAT_R32_UINT; + std::uint32_t bytes = 4u; + if (mode == "uint8") { format = VK_FORMAT_R8_UINT; bytes = 1u; } + else if (mode == "uint16") { format = VK_FORMAT_R16_UINT; bytes = 2u; } + else if (mode == "uint64") { format = VK_FORMAT_R32G32_UINT; bytes = 8u; } + else if (mode == "uint128") { format = VK_FORMAT_R32G32B32A32_UINT; bytes = 16u; } + else Require(mode == "uint32" || mode == "depth" || mode == "upload" || mode == "retile" || mode == "depth_retile", "unknown multisample readback test mode"); + if (mode == "depth" || mode == "depth_retile") format = VK_FORMAT_D32_SFLOAT; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + originalDeviceProc = context.deviceProc; + pipelineCacheSynchronization = context.pipelineCacheSynchronization; + Require(originalDeviceProc != nullptr && pipelineCacheSynchronization != nullptr, "readback fixture requires device functions and pipeline cache synchronization"); + context.deviceProc = CheckedDeviceProc; + if (!context.sampleRateShading) { + std::puts("skipped, the device does not support per-sample shading"); + return VulkanTestSkipped; + } + if (mode == "upload") { + if (!context.storageImageMultisample || !context.storageImageWriteWithoutFormat) { + std::puts("skipped, the device does not enable multisampled formatless storage writes"); + return VulkanTestSkipped; + } + CheckUpload(context); + Require(checkedPipelineCacheUses != 0u, "upload fixture did not check pipeline cache use"); + std::printf("multisample upload guarded pipeline creation checks=%u passed\n", checkedPipelineCacheUses); + return 0; + } + auto limited = context; + limited.limits.maxComputeWorkGroupCount[0] = 1u; + TextureDetiler reader(limited); + if (mode == "retile" || mode == "depth_retile") { + if (!SurfaceSupported(context, format, 4u, 129u, 67u, 17u)) return VulkanTestSkipped; + Surface surface(context, format, bytes, 4u, 129u, 67u, 17u); + surface.Render(); + CheckRetile(reader, surface); + Require(checkedPipelineCacheUses != 0u, "retile fixture did not check pipeline cache use"); + std::printf("multisample retile guarded pipeline creation checks=%u passed\n", checkedPipelineCacheUses); + return 0; + } + std::uint32_t checked = 0u; + for (const auto samples : {2u, 4u, 8u}) { + if (!SurfaceSupported(context, format, samples)) continue; + Surface surface(context, format, bytes, samples); + surface.Render(); + const auto elements = surface.width * surface.height * surface.layers * samples; + CheckReadback(reader, surface, 1u, 1u, 1u, 64u * std::max(4u, bytes)); + CheckReadback(reader, surface, 0u, elements, samples == 4u ? 65u : 1u); + CheckReadback(reader, surface, 1u, elements - 3u, 1u); + CheckReadback(reader, surface, 0u, elements, 1u); + if (samples == 4u && bytes == 4u) CheckRefusals(reader, surface); + std::printf("multisample readback %s: samples=%u elements=%u layers=%u full/partial/sentinels/reuse/2d=passed storage-usage=0\n", mode.c_str(), samples, elements, surface.layers); + ++checked; + } + if (checked == 0u) return VulkanTestSkipped; + if (mode == "depth" && SurfaceSupported(context, format, 4u, 128u, 64u, 1u)) { + Surface surface(context, format, bytes, 4u, 128u, 64u, 1u); + surface.Render(); + CheckReadback(reader, surface, 0u, 128u * 64u * 4u, 1u); + std::puts("multisample readback D32 128x64 4x: 32768 distinct D32 sample values passed"); + } + if (mode == "uint32" && SurfaceSupported(context, VK_FORMAT_D32_SFLOAT, 4u) && SurfaceSupported(context, VK_FORMAT_R32_UINT, 4u)) { + Surface depth(context, VK_FORMAT_D32_SFLOAT, 4u, 4u); + depth.Render(); + CheckReadback(reader, depth, 0u, depth.width * depth.height * depth.layers * depth.samples, 1u); + Surface color(context, VK_FORMAT_R32_UINT, 4u, 4u); + color.Render(); + CheckReadback(reader, color, 0u, color.width * color.height * color.layers * color.samples, 1u); + std::puts("multisample readback uint/depth pipeline transitions passed"); + } + Require(checkedPipelineCacheUses != 0u, "readback fixture did not check pipeline cache use"); + std::printf("multisample reader guarded pipeline creation checks=%u passed\n", checkedPipelineCacheUses); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp new file mode 100644 index 0000000000..27f7d4ae49 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp @@ -0,0 +1,585 @@ +#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/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using namespace AgcDriver::Graphics; + +constexpr std::size_t BlockBytes = 1u << 20u; +constexpr std::uint32_t Side = 256; + +struct Image { + std::uint64_t generation; + VkDeviceMemory memory; +}; + +std::mutex countsMutex; +std::condition_variable countsChanged; +std::map images; +std::map views; +std::map allocations; +std::vector> pendingImages; +std::uint64_t created = 0, destroyed = 0; +PFN_vkGetDeviceProcAddr realProc = nullptr; +PFN_vkAllocateMemory realAllocate = nullptr; +PFN_vkFreeMemory realFree = nullptr; +PFN_vkCreateImage realCreateImage = nullptr; +PFN_vkDestroyImage realDestroyImage = nullptr; +PFN_vkBindImageMemory realBindImageMemory = nullptr; +PFN_vkCreateImageView realCreateImageView = nullptr; +PFN_vkDestroyImageView realDestroyImageView = nullptr; +PFN_vkCmdClearColorImage realClearColorImage = nullptr; +PFN_vkCmdCopyImage realCopyImage = nullptr; + +[[noreturn]] void FailViewOutlivesImage() { + std::fputs("an MSAA image would be destroyed before one of its views; stopping\n", stderr); + std::fflush(stderr); +#ifdef _WIN32 + TerminateProcess(GetCurrentProcess(), 1); +#endif + std::_Exit(1); +} + +[[noreturn]] void FailPendingImage() { + std::fputs("an unsubmitted MSAA image would be destroyed; stopping without submitting it\n", stderr); + std::fflush(stderr); +#ifdef _WIN32 + TerminateProcess(GetCurrentProcess(), 1); +#endif + std::_Exit(1); +} + +VKAPI_ATTR VkResult VKAPI_CALL CountAllocate(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* callbacks, VkDeviceMemory* memory) { + std::lock_guard lock(countsMutex); + const auto result = realAllocate(device, info, callbacks, memory); + if (result == VK_SUCCESS) allocations.emplace(*memory, info->allocationSize); + return result; +} + +VKAPI_ATTR void VKAPI_CALL CountFree(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* callbacks) { + { + std::lock_guard lock(countsMutex); + realFree(device, memory, callbacks); + allocations.erase(memory); + } + countsChanged.notify_all(); +} + +VKAPI_ATTR VkResult VKAPI_CALL CountCreateImage(VkDevice device, const VkImageCreateInfo* info, const VkAllocationCallbacks* callbacks, VkImage* image) { + std::lock_guard lock(countsMutex); + const auto result = realCreateImage(device, info, callbacks, image); + if (result == VK_SUCCESS) images.emplace(*image, Image{++created, VK_NULL_HANDLE}); + return result; +} + +VKAPI_ATTR void VKAPI_CALL CountDestroyImage(VkDevice device, VkImage image, const VkAllocationCallbacks* callbacks) { + { + std::lock_guard lock(countsMutex); + for (const auto& [pending, generation] : pendingImages) { + if (pending == image && images.at(image).generation == generation) FailPendingImage(); + } + if (std::any_of(views.begin(), views.end(), [&](const auto& entry) { return entry.second == image; })) FailViewOutlivesImage(); + realDestroyImage(device, image, callbacks); + images.erase(image); + ++destroyed; + } + countsChanged.notify_all(); +} + +VKAPI_ATTR VkResult VKAPI_CALL CountBindImageMemory(VkDevice device, VkImage image, VkDeviceMemory memory, VkDeviceSize offset) { + std::lock_guard lock(countsMutex); + const auto result = realBindImageMemory(device, image, memory, offset); + if (result == VK_SUCCESS) images.at(image).memory = memory; + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL CountCreateImageView(VkDevice device, const VkImageViewCreateInfo* info, const VkAllocationCallbacks* callbacks, VkImageView* view) { + std::lock_guard lock(countsMutex); + const auto result = realCreateImageView(device, info, callbacks, view); + if (result == VK_SUCCESS) views.emplace(*view, info->image); + return result; +} + +VKAPI_ATTR void VKAPI_CALL CountDestroyImageView(VkDevice device, VkImageView view, const VkAllocationCallbacks* callbacks) { + std::lock_guard lock(countsMutex); + realDestroyImageView(device, view, callbacks); + views.erase(view); +} + +VKAPI_ATTR void VKAPI_CALL CountClearColorImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, const VkClearColorValue* clear, std::uint32_t count, const VkImageSubresourceRange* ranges) { + { + std::lock_guard lock(countsMutex); + pendingImages.emplace_back(image, images.at(image).generation); + } + realClearColorImage(commands, image, layout, clear, count, ranges); +} + +VKAPI_ATTR void VKAPI_CALL CountCopyImage(VkCommandBuffer commands, VkImage source, VkImageLayout sourceLayout, VkImage destination, VkImageLayout destinationLayout, std::uint32_t count, const VkImageCopy* regions) { + { + std::lock_guard lock(countsMutex); + pendingImages.emplace_back(source, images.at(source).generation); + pendingImages.emplace_back(destination, images.at(destination).generation); + } + realCopyImage(commands, source, sourceLayout, destination, destinationLayout, count, regions); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL CountingDeviceProc(VkDevice device, const char* name) { + const auto function = realProc(device, name); + if (function == nullptr) return nullptr; + if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast(CountAllocate); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast(CountFree); + if (std::strcmp(name, "vkCreateImage") == 0) return reinterpret_cast(CountCreateImage); + if (std::strcmp(name, "vkDestroyImage") == 0) return reinterpret_cast(CountDestroyImage); + if (std::strcmp(name, "vkBindImageMemory") == 0) return reinterpret_cast(CountBindImageMemory); + if (std::strcmp(name, "vkCreateImageView") == 0) return reinterpret_cast(CountCreateImageView); + if (std::strcmp(name, "vkDestroyImageView") == 0) return reinterpret_cast(CountDestroyImageView); + if (std::strcmp(name, "vkCmdClearColorImage") == 0) return reinterpret_cast(CountClearColorImage); + if (std::strcmp(name, "vkCmdCopyImage") == 0) return reinterpret_cast(CountCopyImage); + return function; +} + +void CheckPendingImages() { + std::lock_guard lock(countsMutex); + for (const auto& [image, generation] : pendingImages) { + const auto found = images.find(image); + if (found == images.end() || found->second.generation != generation) { + FailPendingImage(); + } + } +} + +Image ImageFor(VkImageView view, VkImage* image = nullptr) { + std::lock_guard lock(countsMutex); + const auto handle = views.at(view); + if (image) *image = handle; + return images.at(handle); +} + +bool Alive(VkImage image, const Image& expected) { + std::lock_guard lock(countsMutex); + const auto found = images.find(image); + return found != images.end() && found->second.generation == expected.generation && allocations.contains(expected.memory); +} + +VkDeviceSize AllocationBytesOf(const Image& image) { + std::lock_guard lock(countsMutex); + return allocations.at(image.memory); +} + +void AwaitImages(std::size_t limit, const std::string& what) { + std::unique_lock lock(countsMutex); + const bool released = countsChanged.wait_for(lock, std::chrono::seconds(2), [&] { return images.size() <= limit && allocations.size() <= limit; }); + Require(released, what + ": " + std::to_string(images.size()) + " images and " + std::to_string(allocations.size()) + " allocations remain"); +} + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + data = VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE, PAGE_NOACCESS); + Require(data != nullptr, "cannot reserve the MSAA guest block"); +#else + data = mmap(nullptr, BlockBytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(data != MAP_FAILED, "cannot reserve the MSAA guest block"); +#endif + Map(); + } + + ~GuestBlock() { + if (mapped) Unmap(); +#ifdef _WIN32 + VirtualFree(data, 0, MEM_RELEASE); +#else + munmap(data, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + void Map() { + Require(!mapped, "the MSAA guest block is already mapped"); +#ifdef _WIN32 + Require(VirtualAlloc(data, BlockBytes, MEM_COMMIT, PAGE_READWRITE) == data, "cannot commit the MSAA guest block"); +#else + Require(mmap(data, BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == data, "cannot map the MSAA guest block"); +#endif + GuestAllocations::Mutation().Add(data, BlockBytes, true, true, true); + mapped = true; + } + + void Unmap() { + Require(mapped, "the MSAA guest block is not mapped"); + GuestAllocations::Mutation().Unmap(data, BlockBytes, [&](const void* begin, std::size_t bytes, const void*, bool last) { + Require(begin == data && bytes == BlockBytes && last, "the MSAA allocation was not released as one range"); +#ifdef _WIN32 + Require(VirtualFree(data, BlockBytes, MEM_DECOMMIT) != 0, "cannot decommit the MSAA guest block"); +#else + Require(mmap(data, BlockBytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == data, "cannot unmap the MSAA guest block"); +#endif + }); + mapped = false; + RequireNoMultisampleTarget(Address(), BlockBytes, "the released guest range"); + } + + std::uint64_t Address() const { return reinterpret_cast(data); } + +private: + void* data = nullptr; + bool mapped = false; +}; + +ColorTarget Target(const GuestBlock& block) { + ColorTarget target{}; + target.address = target.surfaceAddress = block.Address(); + target.extent = target.surfaceExtent = {Side, Side}; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.elementBytes = 4; + target.samples = 4; + target.tileMode = ColorTileMode::RenderTarget; + target.bytes = ColorTargetLayout(Side, Side, target.tileMode, target.elementBytes).Bytes(); + Require(MultisampledColorBytes(target) == BlockBytes, "the MSAA guest footprint differs from its allocation"); + return target; +} + +GuestTextureResource Resource(const GuestBlock& block) { + GuestTextureResource resource{}; + resource.baseAddress = block.Address(); + resource.width = resource.height = Side; + resource.mipCount = resource.allocatedMipCount = 1; + resource.dimension = TextureDimension::k2DMsaa; + resource.tileMode = TextureTileMode::kR64KBX; + resource.format = 56; + resource.samples = 4; + return resource; +} + +class Run { +public: + explicit Run(AgcDriver::VulkanDevice& device) : device(device), context(AgcDriver::VulkanDeviceTestAccess::ContextOf(device)) { + realProc = context.deviceProc; + realAllocate = context.Function("vkAllocateMemory"); + realFree = context.Function("vkFreeMemory"); + realCreateImage = context.Function("vkCreateImage"); + realDestroyImage = context.Function("vkDestroyImage"); + realBindImageMemory = context.Function("vkBindImageMemory"); + realCreateImageView = context.Function("vkCreateImageView"); + realDestroyImageView = context.Function("vkDestroyImageView"); + realClearColorImage = context.Function("vkCmdClearColorImage"); + realCopyImage = context.Function("vkCmdCopyImage"); + context.deviceProc = CountingDeviceProc; + context.functions = nullptr; + } + + ~Run() { Clear(); } + + void Sync() { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + CheckPendingImages(); + { + std::lock_guard countsLock(countsMutex); + pendingImages.clear(); + } + device.SubmitRecorded(true); + device.WaitIdle(); + context.recorder->FlushDeferredReleases(); + } + + void Clear() { + Sync(); + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + ClearMultisampleTargets(context.device); + } + + void IdleRemaps() { + GuestBlock block; + const auto target = Target(block); + for (unsigned cycle = 0; cycle < 32; ++cycle) { + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "an idle MSAA target has no view"); + } + Sync(); + block.Unmap(); + if (cycle != 31) block.Map(); + } + { + std::lock_guard lock(countsMutex); + std::printf("32 idle MSAA remaps: created=%llu destroyed=%llu live=%zu\n", static_cast(created), static_cast(destroyed), images.size()); + } + AwaitImages(1, "unmapped idle MSAA targets accumulate"); + Clear(); + AwaitImages(0, "clearing idle MSAA targets did not release their allocations"); + } + + void RecordedRemap() { + GuestBlock block; + const auto target = Target(block); + VkImage oldImage = VK_NULL_HANDLE; + Image old{}; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto keptBefore = context.recorder->OpenKeptBytes(); + std::shared_ptr lookupOwner; + VkImage lookupImage = VK_NULL_HANDLE; + old = ImageFor(MultisampleTargetView(context, target, lookupOwner, &lookupImage), &oldImage); + Require(lookupOwner != nullptr && lookupImage == oldImage, "the MSAA lookup did not return its owner and hazard image together"); + Require(context.recorder->Recording(), "MSAA initialization was not recorded"); + const auto kept = context.recorder->OpenKeptBytes(); + Require(kept >= keptBefore + AllocationBytesOf(old), "the pending MSAA allocation is missing from the recorder budget"); + Require(ImageFor(MultisampleTargetView(context, target)).generation == old.generation, "the repeated MSAA lookup changed its owner"); + Require(context.recorder->OpenKeptBytes() == kept, "a repeated MSAA lookup double-counts its allocation in one batch"); + } + block.Unmap(); + block.Map(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto keptBefore = context.recorder->OpenKeptBytes(); + const auto replacement = ImageFor(MultisampleTargetView(context, target)); + CheckPendingImages(); + Require(replacement.generation != old.generation && Alive(oldImage, old), "a pending MSAA clear lost its image during remap"); + Require(context.recorder->OpenKeptBytes() >= keptBefore + AllocationBytesOf(replacement), "the replacement MSAA allocation is missing from the recorder budget"); + } + Sync(); + AwaitImages(1, "the completed retired MSAA target was not released"); + Require(!Alive(oldImage, old), "the retired recorded MSAA allocation remains live"); + block.Unmap(); + Clear(); + AwaitImages(0, "recorded MSAA targets were not fully released"); + } + + void SampledRemap() { + GuestBlock block; + const auto target = Target(block); + std::shared_ptr held; + VkImage oldImage = VK_NULL_HANDLE; + Image old{}; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + MultisampleTargetView(context, target); + held = MultisampleTargetTexture(context, Resource(block), {}); + old = ImageFor(held->View(), &oldImage); + } + const std::weak_ptr lifetime = held; + Sync(); + block.Unmap(); + block.Map(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto replacement = ImageFor(MultisampleTargetView(context, target)); + CheckPendingImages(); + Require(replacement.generation != old.generation, "a remapped sampled MSAA target reused its retired owner"); + Require(Alive(oldImage, old) && ImageFor(held->View()).generation == old.generation, "a held sampled MSAA view lost its parent allocation"); + context.recorder->Keep(held); + held.reset(); + Require(!lifetime.expired() && Alive(oldImage, old), "a recorded sampled MSAA view lost its last alias"); + } + Sync(); + AwaitImages(1, "a sampled MSAA owner or cached view forms a retention cycle"); + Require(lifetime.expired() && !Alive(oldImage, old), "a retired sampled MSAA parent survives its last alias and batch"); + block.Unmap(); + Clear(); + AwaitImages(0, "sampled MSAA targets were not fully released"); + } + + void OrderRemap() { + const auto locations = context.sampleLocationSampleCounts; + const auto grid = context.sampleLocationGridSampleCounts; + context.sampleLocationSampleCounts |= VK_SAMPLE_COUNT_4_BIT; + context.sampleLocationGridSampleCounts &= ~static_cast(VK_SAMPLE_COUNT_4_BIT); + GuestBlock block; + const auto target = Target(block); + if (!MayReorderSamples(context, target.samples) || SampleOrderStorageFormat(context, target.format, target.elementBytes, target.samples) == VK_FORMAT_UNDEFINED) { + context.sampleLocationSampleCounts = locations; + context.sampleLocationGridSampleCounts = grid; + std::puts("the device has no multisampled storage view for the sample-order target; its retirement case is skipped"); + return; + } + SampleOrderTarget order; + VkImage oldImage = VK_NULL_HANDLE; + Image old{}; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + order = MultisampleTargetOrderTarget(context, target); + old = ImageFor(MultisampleTargetView(context, target), &oldImage); + Require(order.image == oldImage, "the sample-order target does not name its target's image"); + } + Sync(); + block.Unmap(); + block.Map(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto replacement = ImageFor(MultisampleTargetView(context, target)); + CheckPendingImages(); + Require(replacement.generation != old.generation, "a remapped reordered MSAA target reused its retired owner"); + Require(Alive(oldImage, old), "a held sample-order target lost its image when its MSAA target retired"); + context.recorder->Keep(order.keep); + order = {}; + Require(Alive(oldImage, old), "recorded sample-order work lost its image when its MSAA target retired"); + } + Sync(); + AwaitImages(1, "a retired sample-order target survives its last batch"); + Require(!Alive(oldImage, old), "a retired reordered MSAA parent survives its last batch"); + block.Unmap(); + Clear(); + context.sampleLocationSampleCounts = locations; + context.sampleLocationGridSampleCounts = grid; + AwaitImages(0, "reordered MSAA targets were not fully released"); + } + + void StorageRemap() { + GuestBlock block; + const auto target = Target(block); + const auto resource = Resource(block); + std::shared_ptr held; + VkImageView sampledView = VK_NULL_HANDLE, storageView = VK_NULL_HANDLE; + VkImage oldImage = VK_NULL_HANDLE, promotedImage = VK_NULL_HANDLE; + Image old{}, promoted{}; + VkDeviceSize totalBytes = 0; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + MultisampleTargetView(context, target); + held = MultisampleTargetTexture(context, resource, {}); + sampledView = held->View(); + old = ImageFor(sampledView, &oldImage); + } + const std::weak_ptr lifetime = held; + Sync(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto keptBefore = context.recorder->OpenKeptBytes(); + storageView = MultisampleTargetStorageView(context, resource); + promoted = ImageFor(storageView, &promotedImage); + Require(promoted.generation != old.generation && Alive(oldImage, old), "MSAA storage promotion did not retain its source image"); + Require(ImageFor(sampledView).generation == old.generation, "MSAA storage promotion destroyed a held sampled view"); + Require(held->View() == sampledView, "MSAA storage promotion changed a held sampled alias"); + totalBytes = AllocationBytesOf(old) + AllocationBytesOf(promoted); + const auto kept = context.recorder->OpenKeptBytes(); + Require(kept == keptBefore + totalBytes, "MSAA storage promotion did not account for exactly both allocations"); + Require(MultisampleTargetStorageView(context, resource) == storageView, "a repeated MSAA storage lookup changed its view"); + Require(context.recorder->OpenKeptBytes() == kept, "a repeated MSAA storage lookup double-counts its allocations"); + } + Sync(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(Alive(oldImage, old) && ImageFor(sampledView).generation == old.generation, "a held sampled MSAA view did not survive storage promotion completion"); + Require(held->View() == sampledView, "a held sampled MSAA alias changed after promotion completion"); + held.reset(); + const auto keptBefore = context.recorder->OpenKeptBytes(); + Require(MultisampleTargetStorageView(context, resource) == storageView, "a later MSAA storage lookup changed its promoted image"); + const auto kept = context.recorder->OpenKeptBytes(); + Require(kept == keptBefore + totalBytes, "an already-storable MSAA target is missing allocations from the new batch budget"); + Require(MultisampleTargetStorageView(context, resource) == storageView, "a repeated promoted MSAA lookup changed its view"); + Require(context.recorder->OpenKeptBytes() == kept, "an already-storable MSAA target double-counts its allocations in one batch"); + } + block.Unmap(); + block.Map(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto replacement = ImageFor(MultisampleTargetView(context, target)); + CheckPendingImages(); + Require(replacement.generation != promoted.generation, "a remapped MSAA target reused its promoted storage owner"); + Require(!lifetime.expired() && Alive(oldImage, old) && Alive(promotedImage, promoted), "a pending MSAA storage use lost its promoted or retired allocation"); + } + Sync(); + AwaitImages(1, "the completed promoted MSAA owner was not released"); + Require(lifetime.expired() && !Alive(oldImage, old) && !Alive(promotedImage, promoted), "a retired MSAA storage owner retained its allocations after completion"); + block.Unmap(); + Clear(); + AwaitImages(0, "promoted MSAA targets were not fully released"); + } + + void StorageTwin() { + GuestBlock block; + const auto target = Target(block); + auto twin = target; + twin.format = VK_FORMAT_R8G8B8A8_SRGB; + const auto resource = Resource(block); + VkImage renderedImage = VK_NULL_HANDLE, twinImage = VK_NULL_HANDLE, promotedImage = VK_NULL_HANDLE, promotedTwinImage = VK_NULL_HANDLE; + Image rendered{}, promoted{}; + VkImageView twinView = VK_NULL_HANDLE; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + rendered = ImageFor(MultisampleTargetView(context, target), &renderedImage); + twinView = MultisampleTargetView(context, twin); + ImageFor(twinView, &twinImage); + Require(twinImage == renderedImage, "an sRGB use of an MSAA surface rendered as UNORM does not reach its samples"); + } + Sync(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + promoted = ImageFor(MultisampleTargetStorageView(context, resource), &promotedImage); + Require(promoted.generation != rendered.generation, "MSAA storage promotion kept the rendered image"); + const auto promotedTwin = MultisampleTargetView(context, twin); + ImageFor(promotedTwin, &promotedTwinImage); + Require(promotedTwinImage == promotedImage, "after storage promotion an sRGB use of the MSAA surface still views the retired image"); + } + Sync(); + block.Unmap(); + Clear(); + AwaitImages(0, "MSAA targets with twin views were not fully released"); + } + +private: + AgcDriver::VulkanDevice& device; + Context context; +}; + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + Require((context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) != 0, "the retirement fixture needs 4x color targets"); + Run run(*device); + run.IdleRemaps(); + run.RecordedRemap(); + run.SampledRemap(); + run.StorageRemap(); + run.OrderRemap(); + if (context.storageImageMultisample) run.StorageTwin(); + std::puts("retired MSAA allocations stay bounded, recorded clears and storage promotions retain their images, sampled aliases release their parents after the last batch, and sample-order targets keep their retired images until their batch completes"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleSrgbTwin.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleSrgbTwin.cpp new file mode 100644 index 0000000000..a28966b839 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleSrgbTwin.cpp @@ -0,0 +1,247 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.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/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 AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::ColorTarget; +using AgcDriver::Graphics::Require; + +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Side = 64; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "multisampled sRGB twin: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::byte* block = nullptr; +}; + +GuestBlock& NewBlock() { + static auto* blocks = new std::deque(); + return blocks->emplace_back(); +} + +ColorTarget MultisampledTarget(const GuestBlock& color, VkFormat format) { + ColorTarget target{}; + target.address = color.Address(); + target.surfaceAddress = target.address; + target.extent = {Side, Side}; + target.surfaceExtent = target.extent; + target.format = format; + target.tileMode = AgcDriver::Graphics::ColorTileMode::RenderTarget; + target.elementBytes = 4; + target.bytes = BlockBytes / 4; + target.samples = 4; + return target; +} + +struct Acquired { + VkImageView view = VK_NULL_HANDLE; + VkImage image = VK_NULL_HANDLE; + std::shared_ptr owner; +}; + +Acquired Acquire(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::Context& context, const ColorTarget& target) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Acquired acquired; + acquired.view = AgcDriver::Graphics::MultisampleTargetView(context, target, acquired.owner, &acquired.image); + Require(acquired.view != VK_NULL_HANDLE && acquired.image != VK_NULL_HANDLE && acquired.owner != nullptr, "the multisampled target has no view"); + device.SubmitRecorded(true); + device.WaitIdle(); + return acquired; +} + +std::array UniformTexel(const AgcDriver::Graphics::Context& context, VkImage multisampled, VkFormat format) { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {Side, Side, 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_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + VkImage resolved = VK_NULL_HANDLE; + AgcDriver::Graphics::Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &resolved), "vkCreateImage resolved texels"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, resolved, &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); + VkDeviceMemory memory = VK_NULL_HANDLE; + AgcDriver::Graphics::Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory resolved texels"); + AgcDriver::Graphics::Check(context.Function("vkBindImageMemory")(context.device, resolved, memory, 0), "vkBindImageMemory resolved texels"); + AgcDriver::Graphics::Buffer readback(context, static_cast(Side) * Side * 4u, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + { + AgcDriver::Graphics::CommandBatch batch(context); + const auto commands = batch.Handle(); + std::array barriers{}; + for (auto& barrier : barriers) { + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + } + barriers[0].image = multisampled; + barriers[0].srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; + barriers[0].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barriers[0].oldLayout = barriers[0].newLayout = VK_IMAGE_LAYOUT_GENERAL; + barriers[1].image = resolved; + barriers[1].dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barriers[1].oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barriers[1].newLayout = VK_IMAGE_LAYOUT_GENERAL; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, static_cast(barriers.size()), barriers.data()); + VkImageResolve region{}; + region.srcSubresource = region.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.extent = {Side, Side, 1}; + context.Function("vkCmdResolveImage")(commands, multisampled, VK_IMAGE_LAYOUT_GENERAL, resolved, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion); + VkMemoryBarrier toCopy{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + toCopy.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toCopy.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &toCopy, 0, nullptr, 0, nullptr); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {Side, Side, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, resolved, VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©); + VkMemoryBarrier toHost{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + toHost.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toHost.dstAccessMask = VK_ACCESS_HOST_READ_BIT; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &toHost, 0, nullptr, 0, nullptr); + batch.SubmitAndWait(); + } + readback.Invalidate(); + const auto bytes = readback.Bytes(); + std::array texel{}; + std::copy_n(bytes.begin(), texel.size(), texel.begin()); + for (std::size_t offset = 0; offset < bytes.size(); offset += texel.size()) Require(std::equal(texel.begin(), texel.end(), bytes.begin() + static_cast(offset)), "a cleared multisampled target holds different texels"); + context.Function("vkDestroyImage")(context.device, resolved, nullptr); + context.Function("vkFreeMemory")(context.device, memory, nullptr); + return texel; +} + +std::array Bytes(std::uint32_t pattern) { + std::array bytes{}; + std::memcpy(bytes.data(), &pattern, bytes.size()); + return bytes; +} + +void FillTakenThroughTwin(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::Context& context, VkFormat created, VkFormat used, std::uint32_t pattern) { + auto& color = NewBlock(); + const auto target = MultisampledTarget(color, created); + const auto rendered = Acquire(device, context, target); + const auto bytes = static_cast(AgcDriver::Graphics::MultisampledColorBytes(target)); + Require(AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address(), bytes, pattern), "the color fill is not taken as the target's clear in its creation format"); + AgcDriver::Graphics::NoteColorMetadataFill(color.Address(), bytes, pattern); + const auto twin = Acquire(device, context, MultisampledTarget(color, used)); + Require(twin.image == rendered.image, "the twin use of the filled target does not reach its samples"); + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(UniformTexel(context, twin.image, created) == Bytes(pattern), "a fill taken as a clear through the twin format does not store the guest's bytes"); +} + +bool TwinFormatsShareTheirSamples(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if ((context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0) { + std::cout << "skipped, the device has no 4x color targets\n"; + return false; + } + struct ClearTargets { + AgcDriver::VulkanDevice& device; + VkDevice handle; + ~ClearTargets() { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(handle); + } + } clearTargets{device, context.device}; + for (const auto [first, second] : {std::pair{VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_B8G8R8A8_UNORM}, std::pair{VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_SRGB}}) { + if (AgcDriver::Graphics::SrgbTwinFormat(context, first) != second && AgcDriver::Graphics::SrgbTwinFormat(context, second) != first) continue; + auto& color = NewBlock(); + const auto rendered = Acquire(device, context, MultisampledTarget(color, first)); + const auto twin = Acquire(device, context, MultisampledTarget(color, second)); + Require(twin.image == rendered.image && twin.owner == rendered.owner, "a use of a multisampled surface in its sRGB twin format does not reach the samples rendered in the other format"); + Require(twin.view != rendered.view, "a use of a multisampled surface in its sRGB twin format is given the view of the other format"); + const auto again = Acquire(device, context, MultisampledTarget(color, first)); + Require(again.image == rendered.image && again.view == rendered.view, "returning to the first format does not give back the first format's view"); + } + if (AgcDriver::Graphics::SrgbTwinFormat(context, VK_FORMAT_R8G8B8A8_UNORM) == VK_FORMAT_R8G8B8A8_SRGB) { + FillTakenThroughTwin(device, context, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_SRGB, 0x80808080u); + FillTakenThroughTwin(device, context, VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_R8G8B8A8_UNORM, 0xff0000ffu); + } + auto& other = NewBlock(); + const auto unorm = Acquire(device, context, MultisampledTarget(other, VK_FORMAT_R8G8B8A8_UNORM)); + const auto floating = Acquire(device, context, MultisampledTarget(other, VK_FORMAT_R16G16_SFLOAT)); + Require(floating.image != unorm.image, "a multisampled surface used in an unrelated format of the same size shares the other format's samples"); + return true; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const bool tested = TwinFormatsShareTheirSamples(*device); + LibcRunShutdown_nid_postfix(); + if (!tested) return VulkanTestSkipped; + std::cout << "multisampled sRGB twin tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + LibcRunShutdown_nid_postfix(); + } catch (...) { + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampledPassOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampledPassOrder.cpp new file mode 100644 index 0000000000..2075a2e2c8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampledPassOrder.cpp @@ -0,0 +1,149 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using namespace AgcDriver::Graphics; +using DepthFastClearHarness::Compile; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::Shaders; +using DepthFastClearHarness::Triangles; +using DepthFastClearHarness::Width; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Samples = 4; + +alignas(4096) std::array Multisampled{}; +alignas(4096) std::array DepthA{}; +alignas(4096) std::array DepthB{}; + +struct Barrier { + VkPipelineStageFlags source; + VkPipelineStageFlags destination; + std::vector memory; + std::vector images; +}; + +class Capture { +public: + static Context Install(const Context& base) { + Require(resolver == nullptr, "nested barrier capture"); + resolver = base.deviceProc; + device = base.device; + Context captured = base; + captured.bufferPool.reset(); + captured.deviceProc = resolve; + captured.functions = nullptr; + return captured; + } + static std::vector Take() { return std::exchange(barriers, {}); } + +private: + static PFN_vkVoidFunction VKAPI_CALL resolve(VkDevice handle, const char* name) { + if (std::string_view(name) == "vkCmdPipelineBarrier") return reinterpret_cast(pipelineBarrier); + return resolver(handle, name); + } + static VKAPI_ATTR void VKAPI_CALL pipelineBarrier(VkCommandBuffer commands, VkPipelineStageFlags source, VkPipelineStageFlags destination, VkDependencyFlags dependencies, std::uint32_t memoryCount, const VkMemoryBarrier* memory, std::uint32_t bufferCount, const VkBufferMemoryBarrier* buffers, std::uint32_t imageCount, const VkImageMemoryBarrier* images) { + barriers.push_back({source, destination, std::vector(memory, memory + memoryCount), std::vector(images, images + imageCount)}); + reinterpret_cast(resolver(device, "vkCmdPipelineBarrier"))(commands, source, destination, dependencies, memoryCount, memory, bufferCount, buffers, imageCount, images); + } + + static inline PFN_vkGetDeviceProcAddr resolver = nullptr; + static inline VkDevice device = VK_NULL_HANDLE; + static inline std::vector barriers; +}; + +bool OrdersColorWrites(const std::vector& barriers) { + return std::any_of(barriers.begin(), barriers.end(), [](const Barrier& barrier) { + return barrier.source == VK_PIPELINE_STAGE_ALL_COMMANDS_BIT && barrier.destination == VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT && barrier.images.empty() && barrier.memory.size() == 1 && (barrier.memory[0].srcAccessMask & VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT) != 0 && (barrier.memory[0].dstAccessMask & VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT) != 0; + }); +} + +void Draw(const Context& context, AgcDriver::VulkanDevice& device, const Shaders& compiled, std::uint64_t depth) { + const std::array shaders{{ + {ShaderStage::Vertex, &compiled.vertex, 0}, + {ShaderStage::Fragment, &compiled.pixel, PixelPushOffset(compiled.vertexPush, device.Target())} + }}; + State state{}; + state.stages = {ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Multisampled.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Multisampled.size(), 0xe4u}; + state.color.samples = Samples; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = DepthTarget{depth, 0, {Width, Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0, false, Samples}; + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_LESS; + state.samples = Samples; + 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}; + AgcDriver::Graphics::Draw(context, state, draw, shaders); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto base = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if ((base.limits.framebufferColorSampleCounts & Samples) == 0 || (base.limits.framebufferDepthSampleCounts & Samples) == 0) { + std::puts("skipped, the device cannot render four-sample color and depth"); + return VulkanTestSkipped; + } + if (!Recorder::MergeBarriers()) { + std::puts("skipped, barrier elision is off"); + return VulkanTestSkipped; + } + const auto shaders = Compile(*device); + device->WaitIdle(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto captured = Capture::Install(base); + Recorder recorder(captured); + recorder.Activate(); + const auto depthA = reinterpret_cast(DepthA.data()); + const auto depthB = reinterpret_cast(DepthB.data()); + Draw(captured, *device, shaders, depthA); + Draw(captured, *device, shaders, depthB); + Capture::Take(); + Draw(captured, *device, shaders, depthA); + const auto between = Capture::Take(); + Require(OrdersColorWrites(between), "multisampled pass order: two draws sharing a multisampled color target while switching depth targets recorded no barrier ordering the first draw's color writes before the second"); + recorder.Submit(); + recorder.Sync(); + std::puts("multisampled pass order tests passed (ordering asserted on the recorded barriers; the multisampled image is not resolved to guest memory)"); + 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..6df1dfa361 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp @@ -0,0 +1,213 @@ +#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 +#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 +}; + +enum class ClearConsumer { Owner, Texture, Attachment }; + +int RunNativeClear(AgcDriver::VulkanDevice& device, ClearConsumer consumer) { + 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"); + std::optional> htileClear; + if (NoteHtileDepthClear(target.htileAddress, metadataBytes, metadataBytes)) htileClear.emplace(target.htileAddress, metadataBytes); + Require(htileClear.has_value(), "the full BDA clear was not registered for completion"); + const auto generation = Memory::TrackerGeneration(); + userData[4] = 0xfffffff0u; + device.Dispatch(compile(), groups, 1u, 1u, {}, reinterpret_cast(code.data()), nullptr, nullptr, htileClear); + std::puts("Native full BDA clear: checking sampled owner before explicit WaitIdle"); + Require(context.recorder->PendingWriteOverlaps(target.htileAddress, metadataBytes), "the recognized HTILE clear is missing its pending write range"); + switch (consumer) { + case ClearConsumer::Owner: + Require(DepthSurfaceHolds(context, DecodeTextureResource(words), sampled.get()), "a pending full BDA clear lost its native sampled owner"); + break; + case ClearConsumer::Texture: + Require(CachedSampledTexture(context, words) == sampled, "a pending full BDA clear lost its native sampled view"); + break; + case ClearConsumer::Attachment: + Require(DepthSurfaceView(context, target) == view, "a pending full BDA clear lost its native attachment"); + break; + } + std::printf("Native full BDA clear: consumer=%u settled the recognized clear before WaitIdle\n", static_cast(consumer)); + Require(!context.recorder->PendingWriteOverlaps(target.htileAddress, metadataBytes), "the native HTILE consumer did not settle its clear's pending write"); + Require(Memory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "the native HTILE consumer did not complete its recognized clear"); + 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(Memory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "the completed recognized native clear did not record exact coverage"); + 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; + for (const auto consumer : {ClearConsumer::Owner, ClearConsumer::Texture, ClearConsumer::Attachment}) { + const auto result = RunNativeClear(*device, consumer); + if (result != 0) return result; + } + return 0; + } 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 new file mode 100644 index 0000000000..641eaf93b0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -0,0 +1,1371 @@ +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "SpirvBackend/SpirvModule.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#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 { +struct DriverTestAccess { + static std::shared_ptr GetDevice() { return Driver::Get().device.Load(); } +}; +} + +namespace { + +using namespace AgcDriver::Graphics; + +class Device { +public: + Device() { +#ifdef _WIN32 + library = SDL_LoadObject("vulkan-1.dll"); +#else + library = SDL_LoadObject("libvulkan.so.1"); +#endif + Require(library != nullptr, "cannot load Vulkan"); + proc = reinterpret_cast(SDL_LoadFunction(library, "vkGetInstanceProcAddr")); + Require(proc != nullptr, "Vulkan instance procedure is missing"); + try { + VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + app.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + info.pApplicationInfo = &app; + Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); + std::uint32_t count = 0u; + const auto enumerate = function("vkEnumeratePhysicalDevices"); + Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices"); + std::vector devices(count); + Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); + int selected = -1; + for (const auto physical : devices) { + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(physical, &properties); + if (const auto* wanted = std::getenv("ANYPS5_GPU"); wanted != nullptr && std::string(properties.deviceName).find(wanted) == std::string::npos) continue; + const int rank = properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU ? 3 : properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU ? 2 : 1; + if (properties.apiVersion < VK_API_VERSION_1_1 || rank <= selected) continue; + context.physical = physical; + context.limits = properties.limits; + selected = rank; + } + Require(context.physical != VK_NULL_HANDLE, "no Vulkan 1.1 device"); + VkPhysicalDeviceProperties selectedProperties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &selectedProperties); + std::printf("Selected GPU name=%s vendor=0x%x device=0x%x driver=%u\n", selectedProperties.deviceName, selectedProperties.vendorID, selectedProperties.deviceID, selectedProperties.driverVersion); + const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); + queues(context.physical, &count, nullptr); + std::vector families(count); + queues(context.physical, &count, families.data()); + std::uint32_t family = 0u; + constexpr auto queueFlags = VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT; + while (family < count && (families[family].queueFlags & queueFlags) != queueFlags) ++family; + Require(family < count, "no graphics and compute queue"); + VkPhysicalDeviceFeatures available{}; + function("vkGetPhysicalDeviceFeatures")(context.physical, &available); + VkPhysicalDeviceFeatures enabled{}; + enabled.shaderStorageImageReadWithoutFormat = available.shaderStorageImageReadWithoutFormat; + const float priority = 1.0f; + VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue.queueFamilyIndex = family; + queue.queueCount = 1u; + queue.pQueuePriorities = &priority; + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + device.queueCreateInfoCount = 1u; + device.pQueueCreateInfos = &queue; + device.pEnabledFeatures = &enabled; + Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + context.deviceProc = function("vkGetDeviceProcAddr"); + context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); + context.imageFormatProperties = function("vkGetPhysicalDeviceImageFormatProperties"); + 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; } + } + ~Device() { release(); } + Context GetContext() const { return context; } +private: + template TFunction function(const char* name) const { + const auto result = reinterpret_cast(proc(instance, name)); + Require(result != nullptr, std::string("missing Vulkan function ") + name); + return result; + } + void release() noexcept { + if (context.device != VK_NULL_HANDLE) { + context.Function("vkDeviceWaitIdle")(context.device); + ClearCachedTextures(context.device); + ClearDepthSurfaces(context.device); + context.bufferPool.reset(); + if (context.pool) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); + function("vkDestroyDevice")(context.device, nullptr); + } + if (instance) function("vkDestroyInstance")(instance, nullptr); + if (library) SDL_UnloadObject(library); + } + void* library = nullptr; + PFN_vkGetInstanceProcAddr proc = nullptr; + VkInstance instance = VK_NULL_HANDLE; + Context context{}; +}; + +class TargetMemory { +public: + explicit TargetMemory(std::size_t bytes, bool registerAllocation = false) : bytes(bytes), registered(registerAllocation) { +#ifdef _WIN32 + pointer = GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536u); + GuestArena::GuestArenaCommit_nid_postfix(pointer, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + 65536u, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot allocate multisample target fixture"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + 65535u) & ~std::uintptr_t{65535u}; + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + 65536u) munmap(reinterpret_cast(aligned + bytes), begin + 65536u - aligned); + pointer = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(pointer, bytes); +#endif + Require(pointer != nullptr, "multisample target fixture allocation failed"); + std::memset(pointer, 0xa5, bytes); + if (registered) GuestAllocations::Mutation().Add(pointer, bytes, true, true, true); + } + ~TargetMemory() { + if (registered) GuestAllocations::Mutation().Remove(pointer); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(pointer, bytes); + GuestArena::GuestArenaRelease_nid_postfix(pointer, bytes); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(pointer, bytes); + munmap(pointer, bytes); +#endif + } + std::byte* Data() const { return static_cast(pointer); } + std::uint64_t Address() const { return reinterpret_cast(pointer); } +private: + void* pointer = nullptr; + std::size_t bytes; + bool registered = false; +}; + +alignas(256) constexpr std::array HtileMaskKernel{ + 0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u, + 0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u, + 0xbf810000u, +}; + +std::vector DriverPm4Packet(std::uint32_t opcode, std::initializer_list body) { + std::vector words{0xc0000000u | (static_cast(body.size() - 1u) << 16u) | (opcode << 8u)}; + words.insert(words.end(), body); + return words; +} + +Context SubmitDriverHtileClear(std::uint64_t address, std::unique_lock& gpuLock) { + const auto shader = reinterpret_cast(HtileMaskKernel.data()); + std::vector commands; + const auto append = [&commands](std::vector packet) { commands.insert(commands.end(), packet.begin(), packet.end()); }; + append(DriverPm4Packet(0x76u, {0x20cu, static_cast(shader >> 8u)})); + append(DriverPm4Packet(0x76u, {0x20du, static_cast(shader >> 40u) & 0xffu})); + append(DriverPm4Packet(0x76u, {0x207u, 64u})); + append(DriverPm4Packet(0x76u, {0x208u, 1u})); + append(DriverPm4Packet(0x76u, {0x209u, 1u})); + append(DriverPm4Packet(0x76u, {0x213u, (6u << 1u) | 0x80u})); + append(DriverPm4Packet(0x76u, {0x240u, 0u})); + append(DriverPm4Packet(0x76u, {0x241u, 0u})); + append(DriverPm4Packet(0x76u, {0x242u, static_cast(address)})); + append(DriverPm4Packet(0x76u, {0x243u, static_cast(address >> 32u)})); + append(DriverPm4Packet(0x76u, {0x244u, 0xfffc00f0u})); + append(DriverPm4Packet(0x76u, {0x245u, 0u})); + append(DriverPm4Packet(0x15u, {32u, 1u, 1u, 0x41u})); + Packet packet{commands.data(), static_cast(commands.size()), 0u, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "native depth HTILE clear: PM4 submission failed"); + gpuLock.unlock(); + try { AgcDriverWaitIdle_nid_postfix(); } + catch (...) { gpuLock.lock(); throw; } + gpuLock.lock(); + const auto driverDevice = AgcDriver::DriverDetail::DriverTestAccess::GetDevice(); + Require(driverDevice != nullptr, "HTILE clear driver device is missing"); + return driverDevice->graphicsContext(); +} + +template +void TargetRefuses(Action action, std::string_view expected) { + std::string message; + try { action(); } + catch (const std::exception& error) { message = error.what(); } + Require(message.find(expected) != std::string::npos, "multisample target refusal did not match " + std::string(expected) + ": " + message); +} + +void QueueTargetByteWrite(const Context& context, Recorder& recorder, std::uint64_t address, std::byte value) { + auto buffer = std::make_shared(context, 4u, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const auto commands = recorder.Commands(); + const auto byte = std::to_integer(value); + context.Function("vkCmdFillBuffer")(commands, buffer->Handle(), 0u, 4u, byte * 0x01010101u); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + recorder.OnComplete([buffer, address] { + buffer->Invalidate(); + AgcDriver::GuestMemory::StoreOwnBytes(address, buffer->Bytes().first(1u)); + }); + recorder.Keep(buffer); + recorder.NotePendingWrite(address, 1u); +} + +std::vector FetchShader(std::uint32_t width, std::uint32_t height, std::uint32_t fragments, bool arrayed, bool floating) { + ShaderRecompiler::SpirvModule module; + module.EmitCapability(spv::CapabilityShader); + module.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450); + const auto u32 = module.Type(spv::OpTypeInt, 32u, 0u); + const auto i32 = module.Type(spv::OpTypeInt, 32u, 1u); + const auto scalar = floating ? module.Type(spv::OpTypeFloat, 32u) : u32; + const auto v4 = module.Type(spv::OpTypeVector, scalar, 4u); + const auto v3 = module.Type(spv::OpTypeVector, u32, 3u); + const auto coordinateType = module.Type(spv::OpTypeVector, i32, arrayed ? 3u : 2u); + const auto voidType = module.Type(spv::OpTypeVoid); + const auto constant = [&](std::uint32_t value) { return module.Constant(spv::OpConstant, u32, value); }; + const auto array = module.Type(spv::OpTypeRuntimeArray, v4); + module.AddAnnotation(spv::OpDecorate, array, spv::DecorationArrayStride, 16u); + const auto block = module.Type(spv::OpTypeStruct, array); + module.AddAnnotation(spv::OpDecorate, block, spv::DecorationBlock); + module.AddAnnotation(spv::OpMemberDecorate, block, 0u, spv::DecorationOffset, 0u); + const auto buffer = module.DefineGlobalVariable(module.Type(spv::OpTypePointer, spv::StorageClassStorageBuffer, block), spv::StorageClassStorageBuffer); + module.AddAnnotation(spv::OpDecorate, buffer, spv::DecorationDescriptorSet, 0u); + module.AddAnnotation(spv::OpDecorate, buffer, spv::DecorationBinding, 1u); + const auto imageType = module.Type(spv::OpTypeImage, scalar, spv::Dim2D, 0u, arrayed ? 1u : 0u, 1u, 1u, spv::ImageFormatUnknown); + const auto image = module.DefineGlobalVariable(module.Type(spv::OpTypePointer, spv::StorageClassUniformConstant, imageType), spv::StorageClassUniformConstant); + module.AddAnnotation(spv::OpDecorate, image, spv::DecorationDescriptorSet, 0u); + module.AddAnnotation(spv::OpDecorate, image, spv::DecorationBinding, 0u); + const auto group = module.DefineGlobalVariable(module.Type(spv::OpTypePointer, spv::StorageClassInput, v3), spv::StorageClassInput); + module.AddAnnotation(spv::OpDecorate, group, spv::DecorationBuiltIn, spv::BuiltInWorkgroupId); + const auto main = module.AllocateId(); + module.AddFunction(spv::OpFunction, voidType, main, spv::FunctionControlMaskNone, module.Type(spv::OpTypeFunction, voidType)); + module.AddFunction(spv::OpLabel, module.AllocateId()); + const auto loaded = module.AllocateId(); + module.AddFunction(spv::OpLoad, v3, loaded, group); + const auto index = module.AllocateId(); + module.AddFunction(spv::OpCompositeExtract, u32, index, loaded, 0u); + const auto binary = [&](spv::Op op, std::uint32_t a, std::uint32_t b) { + const auto result = module.AllocateId(); + module.AddFunction(op, u32, result, a, b); + return result; + }; + const auto pixel = binary(spv::OpUDiv, index, constant(fragments)); + const auto x = binary(spv::OpUMod, pixel, constant(width)); + const auto y = binary(spv::OpUMod, binary(spv::OpUDiv, pixel, constant(width)), constant(height)); + const auto z = binary(spv::OpUDiv, pixel, constant(width * height)); + const auto signedValue = [&](std::uint32_t value) { + const auto result = module.AllocateId(); + module.AddFunction(spv::OpBitcast, i32, result, value); + return result; + }; + const auto coordinate = module.AllocateId(); + if (arrayed) module.AddFunction(spv::OpCompositeConstruct, coordinateType, coordinate, signedValue(x), signedValue(y), signedValue(z)); + else module.AddFunction(spv::OpCompositeConstruct, coordinateType, coordinate, signedValue(x), signedValue(y)); + const auto object = module.AllocateId(); + module.AddFunction(spv::OpLoad, imageType, object, image); + const auto value = module.AllocateId(); + module.AddFunction(spv::OpImageFetch, v4, value, object, coordinate, spv::ImageOperandsSampleMask, binary(spv::OpUMod, index, constant(fragments))); + const auto pointer = module.AllocateId(); + module.AddFunction(spv::OpAccessChain, module.Type(spv::OpTypePointer, spv::StorageClassStorageBuffer, v4), pointer, buffer, constant(0u), binary(spv::OpIAdd, index, constant(1u))); + module.AddFunction(spv::OpStore, pointer, value); + module.AddFunction(spv::OpReturn); + module.AddFunction(spv::OpFunctionEnd); + module.EmitEntryPoint(spv::ExecutionModelGLCompute, main, "main", {group}); + module.AddExecutionMode(main, spv::ExecutionModeLocalSize, 1u, 1u, 1u); + return module.Finalize(); +} + +class Fetch { +public: + Fetch(const Context& context, const Texture& texture, Buffer& output, std::span code, bool firstLayer = false) : context(context) { + try { + const std::array bindings{{{0u, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1u, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, {1u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1u, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}}}; + VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + info.bindingCount = 2u; + info.pBindings = bindings.data(); + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &info, nullptr, &setLayout), "fetch descriptor layout"); + const std::array sizes{{{VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1u}, {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1u}}}; + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = 1u; + poolInfo.poolSizeCount = 2u; + poolInfo.pPoolSizes = sizes.data(); + Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &pool), "fetch descriptor pool"); + const VkDescriptorSetAllocateInfo allocate{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, nullptr, pool, 1u, &setLayout}; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocate, &set), "fetch descriptor set"); + const VkDescriptorImageInfo image{VK_NULL_HANDLE, firstLayer ? texture.FirstLayerView() : texture.View(), texture.Layout()}; + const VkDescriptorBufferInfo buffer{output.Handle(), 0u, output.Bytes().size()}; + const std::array writes{{{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, set, 0u, 0u, 1u, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, &image, nullptr, nullptr}, {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, set, 1u, 0u, 1u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, nullptr, &buffer, nullptr}}}; + context.Function("vkUpdateDescriptorSets")(context.device, 2u, writes.data(), 0u, nullptr); + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = 1u; + layoutInfo.pSetLayouts = &setLayout; + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &layout), "fetch pipeline layout"); + VkShaderModuleCreateInfo shaderInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + shaderInfo.codeSize = code.size_bytes(); + shaderInfo.pCode = code.data(); + Check(context.Function("vkCreateShaderModule")(context.device, &shaderInfo, nullptr, &module), "fetch shader module"); + VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + pipelineInfo.layout = layout; + pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0u, VK_SHADER_STAGE_COMPUTE_BIT, module, "main", nullptr}; + Check(context.Function("vkCreateComputePipelines")(context.device, VK_NULL_HANDLE, 1u, &pipelineInfo, nullptr, &pipeline), "fetch pipeline"); + } catch (...) { release(); throw; } + } + ~Fetch() { release(); } + void Run(VkCommandBuffer commands, std::uint32_t groups) const { + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); + context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, layout, 0u, 1u, &set, 0u, nullptr); + context.Function("vkCmdDispatch")(commands, groups, 1u, 1u); + } +private: + void release() noexcept { + if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + if (module) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + if (layout) context.Function("vkDestroyPipelineLayout")(context.device, layout, nullptr); + if (pool) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); + if (setLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, setLayout, nullptr); + } + Context context; + VkDescriptorSetLayout setLayout = VK_NULL_HANDLE; + VkDescriptorPool pool = VK_NULL_HANDLE; + VkDescriptorSet set = VK_NULL_HANDLE; + VkPipelineLayout layout = VK_NULL_HANDLE; + VkShaderModule module = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; +}; + +std::vector DepthVertexShader() { + ShaderRecompiler::SpirvModule module; + module.EmitCapability(spv::CapabilityShader); + module.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450); + const auto f32 = module.Type(spv::OpTypeFloat, 32u); + const auto i32 = module.Type(spv::OpTypeInt, 32u, 1u); + const auto boolean = module.Type(spv::OpTypeBool); + const auto v4 = module.Type(spv::OpTypeVector, f32, 4u); + const auto voidType = module.Type(spv::OpTypeVoid); + const auto index = module.DefineGlobalVariable(module.Type(spv::OpTypePointer, spv::StorageClassInput, i32), spv::StorageClassInput); + const auto position = module.DefineGlobalVariable(module.Type(spv::OpTypePointer, spv::StorageClassOutput, v4), spv::StorageClassOutput); + module.AddAnnotation(spv::OpDecorate, index, spv::DecorationBuiltIn, spv::BuiltInVertexIndex); + module.AddAnnotation(spv::OpDecorate, position, spv::DecorationBuiltIn, spv::BuiltInPosition); + const auto main = module.AllocateId(); + module.AddFunction(spv::OpFunction, voidType, main, spv::FunctionControlMaskNone, module.Type(spv::OpTypeFunction, voidType)); + module.AddFunction(spv::OpLabel, module.AllocateId()); + const auto loaded = module.AllocateId(); + module.AddFunction(spv::OpLoad, i32, loaded, index); + const auto coordinate = [&](std::uint32_t vertex) { + const auto equal = module.AllocateId(), value = module.AllocateId(); + module.AddFunction(spv::OpIEqual, boolean, equal, loaded, module.Constant(spv::OpConstant, i32, vertex)); + module.AddFunction(spv::OpSelect, f32, value, equal, module.Constant(spv::OpConstant, f32, std::bit_cast(3.0f)), module.Constant(spv::OpConstant, f32, std::bit_cast(-1.0f))); + return value; + }; + const auto value = module.AllocateId(); + module.AddFunction(spv::OpCompositeConstruct, v4, value, coordinate(1u), coordinate(2u), module.Constant(spv::OpConstant, f32, 0u), module.Constant(spv::OpConstant, f32, std::bit_cast(1.0f))); + module.AddFunction(spv::OpStore, position, value); + module.AddFunction(spv::OpReturn); + module.AddFunction(spv::OpFunctionEnd); + module.EmitEntryPoint(spv::ExecutionModelVertex, main, "main", {index, position}); + return module.Finalize(); +} + +std::vector DepthFragmentShader() { + ShaderRecompiler::SpirvModule module; + module.EmitCapability(spv::CapabilityShader); + module.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450); + const auto f32 = module.Type(spv::OpTypeFloat, 32u); + const auto u32 = module.Type(spv::OpTypeInt, 32u, 0u); + const auto voidType = module.Type(spv::OpTypeVoid); + const auto block = module.Type(spv::OpTypeStruct, f32); + module.AddAnnotation(spv::OpDecorate, block, spv::DecorationBlock); + module.AddAnnotation(spv::OpMemberDecorate, block, 0u, spv::DecorationOffset, 0u); + const auto push = module.DefineGlobalVariable(module.Type(spv::OpTypePointer, spv::StorageClassPushConstant, block), spv::StorageClassPushConstant); + const auto depth = module.DefineGlobalVariable(module.Type(spv::OpTypePointer, spv::StorageClassOutput, f32), spv::StorageClassOutput); + module.AddAnnotation(spv::OpDecorate, depth, spv::DecorationBuiltIn, spv::BuiltInFragDepth); + const auto main = module.AllocateId(); + module.AddFunction(spv::OpFunction, voidType, main, spv::FunctionControlMaskNone, module.Type(spv::OpTypeFunction, voidType)); + module.AddFunction(spv::OpLabel, module.AllocateId()); + const auto pointer = module.AllocateId(), value = module.AllocateId(); + module.AddFunction(spv::OpAccessChain, module.Type(spv::OpTypePointer, spv::StorageClassPushConstant, f32), pointer, push, module.Constant(spv::OpConstant, u32, 0u)); + module.AddFunction(spv::OpLoad, f32, value, pointer); + module.AddFunction(spv::OpStore, depth, value); + module.AddFunction(spv::OpReturn); + module.AddFunction(spv::OpFunctionEnd); + module.EmitEntryPoint(spv::ExecutionModelFragment, main, "main", {depth}); + module.AddExecutionMode(main, spv::ExecutionModeOriginUpperLeft); + module.AddExecutionMode(main, spv::ExecutionModeDepthReplacing); + return module.Finalize(); +} + +std::vector DepthFeedbackFragmentShader(std::uint32_t binding, bool sampling) { + ShaderRecompiler::SpirvModule module; + module.EmitCapability(spv::CapabilityShader); + module.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450); + const auto f32 = module.Type(spv::OpTypeFloat, 32u); + const auto i32 = module.Type(spv::OpTypeInt, 32u, 1u); + const auto v4 = module.Type(spv::OpTypeVector, f32, 4u); + const auto v2 = module.Type(spv::OpTypeVector, i32, 2u); + const auto voidType = module.Type(spv::OpTypeVoid); + const auto imageType = module.Type(spv::OpTypeImage, f32, spv::Dim2D, 0u, 0u, 1u, 1u, spv::ImageFormatUnknown); + const auto imageArray = module.Type(spv::OpTypeArray, imageType, module.Constant(spv::OpConstant, i32, 1u)); + const auto image = sampling ? module.DefineGlobalVariable(module.Type(spv::OpTypePointer, spv::StorageClassUniformConstant, imageArray), spv::StorageClassUniformConstant) : 0u; + if (sampling) { + module.AddAnnotation(spv::OpDecorate, image, spv::DecorationDescriptorSet, ShaderRecompiler::RuntimeAbi::DescriptorSet); + module.AddAnnotation(spv::OpDecorate, image, spv::DecorationBinding, binding); + } + const auto depth = module.DefineGlobalVariable(module.Type(spv::OpTypePointer, spv::StorageClassOutput, f32), spv::StorageClassOutput); + module.AddAnnotation(spv::OpDecorate, depth, spv::DecorationBuiltIn, spv::BuiltInFragDepth); + const auto zero = module.Constant(spv::OpConstant, i32, 0u); + const auto coordinate = module.Constant(spv::OpConstantComposite, v2, zero, zero); + const auto main = module.AllocateId(); + module.AddFunction(spv::OpFunction, voidType, main, spv::FunctionControlMaskNone, module.Type(spv::OpTypeFunction, voidType)); + module.AddFunction(spv::OpLabel, module.AllocateId()); + auto value = module.Constant(spv::OpConstant, f32, 0u); + if (sampling) { + const auto element = module.AllocateId(), object = module.AllocateId(), fetched = module.AllocateId(); + value = module.AllocateId(); + module.AddFunction(spv::OpAccessChain, module.Type(spv::OpTypePointer, spv::StorageClassUniformConstant, imageType), element, image, zero); + module.AddFunction(spv::OpLoad, imageType, object, element); + module.AddFunction(spv::OpImageFetch, v4, fetched, object, coordinate, spv::ImageOperandsSampleMask, zero); + module.AddFunction(spv::OpCompositeExtract, f32, value, fetched, 0u); + } + module.AddFunction(spv::OpStore, depth, value); + module.AddFunction(spv::OpReturn); + module.AddFunction(spv::OpFunctionEnd); + module.EmitEntryPoint(spv::ExecutionModelFragment, main, "main", {depth}); + module.AddExecutionMode(main, spv::ExecutionModeOriginUpperLeft); + module.AddExecutionMode(main, spv::ExecutionModeDepthReplacing); + return module.Finalize(); +} + +class DepthRaster { +public: + DepthRaster(const Context& context, const DepthTarget& target, VkImageView view) : context(context), extent(target.extent), samples(target.samples) { + try { + const VkAttachmentDescription attachment{0u, target.format, static_cast(samples), VK_ATTACHMENT_LOAD_OP_LOAD, VK_ATTACHMENT_STORE_OP_STORE, VK_ATTACHMENT_LOAD_OP_DONT_CARE, VK_ATTACHMENT_STORE_OP_DONT_CARE, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL}; + const VkAttachmentReference reference{0u, VK_IMAGE_LAYOUT_GENERAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.pDepthStencilAttachment = &reference; + VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + passInfo.attachmentCount = 1u; + passInfo.pAttachments = &attachment; + passInfo.subpassCount = 1u; + passInfo.pSubpasses = &subpass; + Check(context.Function("vkCreateRenderPass")(context.device, &passInfo, nullptr, &pass), "depth sample render pass"); + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = pass; + framebufferInfo.attachmentCount = 1u; + framebufferInfo.pAttachments = &view; + framebufferInfo.width = extent.width; + framebufferInfo.height = extent.height; + framebufferInfo.layers = 1u; + Check(context.Function("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "depth sample framebuffer"); + const VkPushConstantRange push{VK_SHADER_STAGE_FRAGMENT_BIT, 0u, 4u}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.pushConstantRangeCount = 1u; + layoutInfo.pPushConstantRanges = &push; + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &layout), "depth sample pipeline layout"); + std::array stages{}; + const std::array codes{DepthVertexShader(), DepthFragmentShader()}; + for (std::uint32_t i = 0u; i < 2u; ++i) { + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = codes[i].size() * 4u; + info.pCode = codes[i].data(); + Check(context.Function("vkCreateShaderModule")(context.device, &info, nullptr, &modules[i]), "depth sample shader"); + stages[i] = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0u, i == 0u ? VK_SHADER_STAGE_VERTEX_BIT : VK_SHADER_STAGE_FRAGMENT_BIT, modules[i], "main", nullptr}; + } + VkPipelineVertexInputStateCreateInfo input{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + const VkViewport viewport{0.0f, 0.0f, static_cast(extent.width), static_cast(extent.height), 0.0f, 1.0f}; + const VkRect2D scissor{{0, 0}, extent}; + VkPipelineViewportStateCreateInfo viewportInfo{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewportInfo.viewportCount = viewportInfo.scissorCount = 1u; + viewportInfo.pViewports = &viewport; + viewportInfo.pScissors = &scissor; + VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + raster.polygonMode = VK_POLYGON_MODE_FILL; + raster.cullMode = VK_CULL_MODE_NONE; + raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + raster.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = static_cast(samples); + VkPipelineDepthStencilStateCreateInfo depth{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; + depth.depthTestEnable = depth.depthWriteEnable = VK_TRUE; + depth.depthCompareOp = VK_COMPARE_OP_ALWAYS; + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + info.stageCount = 2u; + info.pStages = stages.data(); + info.pVertexInputState = &input; + info.pInputAssemblyState = &assembly; + info.pViewportState = &viewportInfo; + info.pRasterizationState = &raster; + info.pMultisampleState = &multisample; + info.pDepthStencilState = &depth; + info.pColorBlendState = &blend; + info.layout = layout; + info.renderPass = pass; + pipelines.resize(samples, VK_NULL_HANDLE); + for (std::uint32_t sample = 0u; sample < samples; ++sample) { + const auto mask = 1u << sample; + multisample.pSampleMask = &mask; + Check(context.Function("vkCreateGraphicsPipelines")(context.device, VK_NULL_HANDLE, 1u, &info, nullptr, &pipelines[sample]), "depth sample pipeline"); + } + } catch (...) { release(); throw; } + } + ~DepthRaster() { release(); } + void Run(VkCommandBuffer commands, float offset) const { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = pass; + begin.framebuffer = framebuffer; + begin.renderArea = {{0, 0}, extent}; + context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + for (std::uint32_t sample = 0u; sample < samples; ++sample) { + const auto value = offset + static_cast(sample + 1u) / 16.0f; + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines[sample]); + context.Function("vkCmdPushConstants")(commands, layout, VK_SHADER_STAGE_FRAGMENT_BIT, 0u, 4u, &value); + context.Function("vkCmdDraw")(commands, 3u, 1u, 0u, 0u); + } + context.Function("vkCmdEndRenderPass")(commands); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + } +private: + void release() noexcept { + for (const auto pipeline : pipelines) if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + for (const auto module : modules) if (module) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + if (layout) context.Function("vkDestroyPipelineLayout")(context.device, layout, nullptr); + if (framebuffer) context.Function("vkDestroyFramebuffer")(context.device, framebuffer, nullptr); + if (pass) context.Function("vkDestroyRenderPass")(context.device, pass, nullptr); + } + Context context; + VkExtent2D extent; + std::uint32_t samples; + VkRenderPass pass = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + VkPipelineLayout layout = VK_NULL_HANDLE; + std::array modules{}; + std::vector pipelines; +}; + +std::array DepthSampleWords(const DepthTarget& target) { + const auto base = (target.address | target.pipeBankXor) >> 8u; + const auto metadata = target.htileAddress >> 8u; + const auto format = target.format == VK_FORMAT_D16_UNORM ? 7u : 22u; + const auto count = static_cast(std::countr_zero(target.samples)); + const auto width = target.extent.width - 1u, height = target.extent.height - 1u; + return {static_cast(base), static_cast((base >> 32u) & 255u) | (format << 20u) | ((width & 3u) << 30u), (width >> 2u) | (height << 14u), (14u << 28u) | (24u << 20u) | (count << 16u) | 0x204u, 0u, count << 4u, (target.iterate256 ? 1u << 10u : 0u) | (1u << 19u) | (1u << 21u) | (static_cast(metadata & 255u) << 24u), static_cast(metadata >> 8u)}; +} + +bool DepthSupported(const Context& context, VkFormat format, std::uint32_t fragments) { + 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_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, 0u, &properties); + return result == VK_SUCCESS && (properties.sampleCounts & fragments) != 0u && (context.limits.framebufferDepthSampleCounts & fragments) != 0u; +} + +void CheckDrawFeedback() { + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, "native depth draw feedback requires Vulkan"); + constexpr VkExtent2D extent{5u, 3u}; + constexpr std::uint32_t samples = 4u; + const auto depthBytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, 4u, samples)); + TargetMemory memory(depthBytes), metadata(65536u); + std::memset(metadata.Data(), 0, 65536u); + DepthTarget depth{memory.Address(), 0u, extent, VK_FORMAT_D32_SFLOAT, 0.0f, 0u, metadata.Address(), false, samples}; + depth.depthSwizzle = 24u; + depth.iterate256 = depth.htilePipeAligned = true; + struct Cleanup { + AgcDriver::VulkanDevice& device; + ~Cleanup() { device.WaitIdle(); ClearCachedTextures(device.Device()); ClearDepthSurfaces(device.Device()); } + } cleanup{*device}; + State state{}; + state.stages = {ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.depth = depth; + state.depthTest = state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + state.renderExtent = extent; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0.0f, 0.0f, static_cast(extent.width), static_cast(extent.height), 0.0f, 1.0f}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, extent}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.samples = samples; + ShaderRecompiler::RecompileResult vertex, producer, reader; + vertex.spirv = DepthVertexShader(); + vertex.variantId = 1u; + constexpr auto binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding; + producer.spirv = DepthFeedbackFragmentShader(binding, false); + producer.variantId = 2u; + const auto pixelOffset = PixelPushOffset(0u, device->Target()); + const AgcDriver::Pm4::DrawParameters draw{0u, 3u, 0u, 1u, 0u, false}; + const std::array initial{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0u}, {ShaderRecompiler::ShaderStage::Fragment, &producer, pixelOffset}}}; + device->Draw(state, draw, initial); + device->WaitIdle(); + std::array probe{}; + TargetRefuses([&] { AgcDriver::GuestMemory::Read(depth.address, probe); }, "sample-preserving guest publication"); + Require(DepthSurfaceAt(depth.address), "a refused guest read retired a native depth surface a draw wrote"); + reader.spirv = DepthFeedbackFragmentShader(binding, true); + reader.variantId = 3u; + const auto words = DepthSampleWords(depth); + ShaderRecompiler::DescriptorBinding image{}; + image.kind = ShaderRecompiler::DescriptorKind::SampledImage; + image.role = ShaderRecompiler::DescriptorRole::GuestImages; + image.descriptorSet = ShaderRecompiler::RuntimeAbi::DescriptorSet; + image.binding = binding; + image.count = 1u; + image.guestDescriptor.assign(words.begin(), words.end()); + image.imageShape = ShaderRecompiler::DescriptorImageShape::Image2DMultisample; + image.imageSamplers = {0u}; + reader.bindings.push_back(std::move(image)); + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0u}, {ShaderRecompiler::ShaderStage::Fragment, &reader, pixelOffset}}}; + TargetRefuses([&] { device->Draw(state, draw, shaders); }, "sampling a native multisampled depth target the draw attaches"); + device->WaitIdle(); + TargetMemory testedMemory(depthBytes), testedMetadata(65536u); + std::memset(testedMetadata.Data(), 0, 65536u); + const auto guestByte = [](std::size_t offset) { return static_cast(offset * 7u + 3u); }; + for (std::size_t offset = 0u; offset < depthBytes; ++offset) testedMemory.Data()[offset] = guestByte(offset); + auto tested = depth; + tested.address = testedMemory.Address(); + tested.htileAddress = testedMetadata.Address(); + state.depth = tested; + state.depthWrite = false; + device->Draw(state, draw, initial); + device->WaitIdle(); + Require(DepthSurfaceAt(tested.address), "a depth-test-only draw left no resident native depth surface"); + AgcDriver::GuestMemory::Read(tested.address + depthBytes - 1u, probe); + Require(probe[0] == guestByte(depthBytes - 1u), "a guest read after a depth-test-only draw did not return its guest bytes"); + Require(!DepthSurfaceAt(tested.address), "a guest read left a native depth surface only depth-tested resident"); + std::puts("Native depth feedback refused through the draw resource path; a written surface refused and a depth-tested one retired on guest read"); +} + +enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap, PossibleStore, CompletedPossibleStore, DriverDispatch, ImportWindowWrite }; + +void CheckClearDispatch() { + const std::array code{ + 0xd7460000, 0x04010c06, 0x34000084, 0xdc388000, 0x04020000, + 0xbf8c3f70, 0xd7710008, 0x00120805, 0xd7710009, 0x00120a05, + 0xd771000a, 0x00120c05, 0xd771000b, 0x00120e05, + 0xdc788000, 0x00020800, 0xbf810000, + }; + const std::array userData{0u, 0u, 65536u, 0u, 0xfffc00f0u, 0u}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{64u, 1u, 1u}, 0u, {true, false, false}, false, 1u}; + const std::array packet{0u, 32u, 1u, 1u, 1u}; + Require(HtileDepthClearBytes_nid_no_patch(code, userData, compute, packet, 0u) == 32768u, "complete HTILE mask dispatch was not recognized"); + for (const auto groups : {0x3fffffu, 0x400000u, 0x400001u}) { + auto boundary = packet; + boundary[1] = groups; + const auto expected = groups > 0x400000u ? 0u : static_cast(groups) * 1024u; + Require(HtileDepthClearBytes_nid_no_patch(code, userData, compute, boundary, 0u) == expected, "HTILE mask extent crossed its 32-bit byte-offset bound"); + } + Require(HtileDepthClearBytes_nid_no_patch(code, userData, compute, packet, 65536u) == 0u, "indirect HTILE clear was accepted"); + Require(HtileDepthClearBytes_nid_no_patch(code, userData, compute, std::span(packet).first(4u), 0u) == 0u, "short HTILE dispatch packet was accepted"); + Require(HtileDepthClearBytes_nid_no_patch(code, std::span(userData).first(5u), compute, packet, 0u) == 0u, "short HTILE user data was accepted"); + auto changedCode = code; + changedCode[0] ^= 1u; + Require(HtileDepthClearBytes_nid_no_patch(changedCode, userData, compute, packet, 0u) == 0u, "another HTILE producer was accepted"); + for (std::uint32_t fault = 0u; fault < 14u; ++fault) { + auto changedCompute = compute; + auto changedPacket = packet; + auto changedUserData = userData; + switch (fault) { + case 0u: changedCompute.partialThreads[0] = 1u; break; + case 1u: changedCompute.groupIdEnable[0] = false; break; + case 2u: changedCompute.threadIdComponentCount = 0u; break; + case 3u: changedCompute.threadIdComponentCount = 4u; break; + case 4u: changedCompute.numThreads[0] = 32u; break; + case 5u: changedCompute.numThreads[1] = 2u; break; + case 6u: changedPacket[1] = 0u; break; + case 7u: changedPacket[2] = 2u; break; + case 8u: changedPacket[3] = 2u; break; + case 9u: changedPacket[4] = 0u; break; + case 10u: changedPacket[4] = 0x21u; break; + case 11u: changedUserData[2] = 65537u; break; + case 12u: changedUserData[4] = 1u; break; + case 13u: changedUserData[2] = 0xfffffff0u; changedUserData[3] = 0xffffffffu; break; + } + Require(HtileDepthClearBytes_nid_no_patch(code, changedUserData, changedCompute, changedPacket, 0u) == 0u, "unproven HTILE mask dispatch was accepted"); + } +} + +void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing, std::optional depthOnlyOverride, ClearFault fault, std::unique_lock& gpuLock, bool iterate256 = true) { + const VkExtent2D extent{5u, 3u}; + const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); + TargetMemory memory(bytes), metadata(65536u, fault == ClearFault::DriverDispatch); + const auto metadataBytes = HtileMetadataBytes(extent); + const bool depthOnly = depthOnlyOverride.value_or(one); + const std::uint32_t pattern = one ? (depthOnly ? 0xfffffff0u : 0xfffc00f0u) : 0u; + for (std::size_t i = 0u; i < metadataBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &pattern, 4u); + DepthTarget target{memory.Address(), 0u, extent, format, one ? 1.0f : 0.0f, 0u, metadata.Address(), false, fragments}; + target.depthSwizzle = 24u; + target.pipeBankXor = one ? 0x8c00u : 0u; + target.htileDepthOnly = depthOnly; + target.iterate256 = iterate256; + target.htilePipeAligned = true; + TargetMemory otherMemory(static_cast(DepthSliceBytes(extent, 4u))); + DepthTarget otherTarget{otherMemory.Address(), 0u, extent, VK_FORMAT_D32_SFLOAT, 0.0f, 0u, 0u, false, 1u}; + Recorder recorder(context); + recorder.Activate(); + struct Cleanup { + Recorder& recorder; + Context context; + ~Cleanup() { recorder.Sync(); ClearCachedTextures(context.device); ClearDepthSurfaces(context.device); } + } cleanup{recorder, context}; + if (!one) { + metadata.Data()[metadataBytes - 1u] = std::byte{0x7f}; + QueueTargetByteWrite(context, recorder, target.htileAddress + metadataBytes - 1u, std::byte{0}); + Require(recorder.PendingWriteOverlaps(target.htileAddress, metadataBytes), "initial depth producer was not registered"); + } + const auto view = DepthSurfaceView(context, target); + Require(DepthSurfaceView(context, target) == view, "native depth attachment did not reuse its image"); + NoteDepthSurfaceWrite(context, target); + std::array beforeSampling{}; + TargetRefuses([&] { AgcDriver::GuestMemory::Read(target.address + bytes - 1u, beforeSampling); }, "sample-preserving guest publication"); + TargetRefuses([&] { RequireNoMultisampleTarget(target.htileAddress + metadataBytes - 1u, 1u, "a DMA copy"); }, "sample-preserving guest publication"); + const auto words = DepthSampleWords(target); + auto texture = CachedSampledTexture(context, words); + auto arrayTexture = CachedSampledTexture(context, words, VK_IMAGE_VIEW_TYPE_2D_ARRAY); + Require(texture != nullptr && CachedSampledTexture(context, words) == texture && texture->SampledViewRange(false).type == VK_IMAGE_VIEW_TYPE_2D, "native depth sampled view was not reused"); + Require(arrayTexture != nullptr && arrayTexture != texture && CachedSampledTexture(context, words, VK_IMAGE_VIEW_TYPE_2D_ARRAY) == arrayTexture && arrayTexture->SampledViewRange(false).type == VK_IMAGE_VIEW_TYPE_2D_ARRAY, "native depth arrayed sampled view was not created as VK_IMAGE_VIEW_TYPE_2D_ARRAY or reused"); + Require(DepthSurfaceHolds(context, DecodeTextureResource(words), texture.get()) && DepthSurfaceHolds(context, DecodeTextureResource(words), arrayTexture.get()), "native depth sampled view is not held by its attachment"); + const std::array, 1> sampled{texture}; + TargetRefuses([&] { RequireNoNativeDepthFeedback_nid_no_patch(context, target, sampled); }, "sampling a native multisampled depth target the draw attaches"); + const std::array, 1> unused{}; + RequireNoNativeDepthFeedback_nid_no_patch(context, target, unused); + auto unattached = target; + unattached.address += bytes; + RequireNoNativeDepthFeedback_nid_no_patch(context, unattached, sampled); + auto singleSampled = target; + singleSampled.samples = 1u; + RequireNoNativeDepthFeedback_nid_no_patch(context, singleSampled, sampled); + auto otherDevice = context; + otherDevice.device = VK_NULL_HANDLE; + RequireNoNativeDepthFeedback_nid_no_patch(otherDevice, target, sampled); + recorder.Sync(); + const auto count = extent.width * extent.height * fragments; + Buffer output(context, (count + 2u) * 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const Fetch fetch(context, *texture, output, FetchShader(extent.width, extent.height, fragments, false, true)); + const Fetch arrayFetch(context, *arrayTexture, output, FetchShader(extent.width, extent.height, fragments, true, true)); + const auto check = [&](bool rendered, float offset) { + output.Invalidate(); + for (std::uint32_t index = 0u; index < count; ++index) { + std::array result{}; + std::memcpy(result.data(), output.Bytes().data() + (index + 1u) * 16u, 16u); + const auto expected = rendered ? offset + static_cast(index % fragments + 1u) / 16.0f : target.clearDepth; + const auto tolerance = format == VK_FORMAT_D16_UNORM && rendered ? 1.0f / 65535.0f : 0.0f; + Require(std::isfinite(result[0]) && std::abs(result[0] - expected) <= tolerance && result[1] == 0.0f && result[2] == 0.0f && result[3] == 1.0f, "native depth sampled fetch lost a raster sample or its component mapping"); + } + Require(std::all_of(output.Bytes().begin(), output.Bytes().begin() + 16u, [](auto byte) { return byte == std::byte{0xa5}; }) && std::all_of(output.Bytes().end() - 16u, output.Bytes().end(), [](auto byte) { return byte == std::byte{0xa5}; }), "native depth sampled fetch changed output guards"); + }; + std::memset(output.Bytes().data(), 0xa5, output.Bytes().size()); + { + CommandBatch batch(context); + fetch.Run(batch.Handle(), count); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + check(false, 0.0f); + } + std::memset(output.Bytes().data(), 0xa5, output.Bytes().size()); + { + CommandBatch batch(context); + arrayFetch.Run(batch.Handle(), count); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + check(false, 0.0f); + } + { + const DepthRaster raster(context, target, view); + for (const auto offset : {0.0f, 0.25f}) { + Require(DepthSurfaceView(context, target) == view, "rebinding the native depth attachment replaced its image"); + Require(CachedSampledTexture(context, words) == texture && CachedSampledTexture(context, words, VK_IMAGE_VIEW_TYPE_2D_ARRAY) == arrayTexture, "native sampled view changed between raster writes"); + std::memset(output.Bytes().data(), 0xa5, output.Bytes().size()); + { + CommandBatch batch(context); + raster.Run(batch.Handle(), offset); + batch.SubmitAndWait(); + } + if (offset == 0.0f) DepthSurfaceView(context, otherTarget); + Require(CachedSampledTexture(context, words) == texture, "native depth shadow view changed after a raster write"); + recorder.Sync(); + CommandBatch batch(context); + fetch.Run(batch.Handle(), count); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + check(true, offset); + std::memset(output.Bytes().data(), 0xa5, output.Bytes().size()); + CommandBatch arrayBatch(context); + arrayFetch.Run(arrayBatch.Handle(), count); + RecordMemoryBarrier(context, arrayBatch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + arrayBatch.SubmitAndWait(); + check(true, offset); + } + if (clearing) { + for (std::uint32_t iteration = 0u; iteration < 3u; ++iteration) { + { + CommandBatch batch(context); + raster.Run(batch.Handle(), static_cast(iteration) / 16.0f); + batch.SubmitAndWait(); + } + const bool clearOne = iteration == 1u ? !one : one; + target.clearDepth = clearOne ? 1.0f : 0.0f; + Require(DepthSurfaceView(context, target) == view, "updating the native clear value replaced its attachment"); + const std::uint32_t clearPattern = clearOne ? (depthOnly ? 0xfffffff0u : 0xfffc00f0u) : 0u; + if (fault == ClearFault::DriverDispatch && clearOne) { + struct ShutdownDriver { + ~ShutdownDriver() { + try { AgcDriverShutdown_nid_postfix(); } catch (...) {} + } + } shutdownDriver; + const auto generation = AgcDriver::GuestMemory::TrackerGeneration(); + const auto driverContext = SubmitDriverHtileClear(target.htileAddress, gpuLock); + recorder.Activate(); + Require(Recorder::Active() == &recorder, "driver idle did not restore the native depth fixture recorder"); + if (!AgcDriver::GuestMemory::Watched(target.htileAddress, metadataBytes)) { + Require(PrepareImportWatch(driverContext) == ImportWatch::Unwatch, + "the HTILE metadata range lost write-watch coverage on a device whose host imports stay watched"); + std::puts("skipped native HTILE write-coverage assertion: the write tracker does not cover the imported metadata range; the production depth-sampling path may still decline this clear"); + TargetRefuses([&] { Require(DepthSurfaceHolds(context, DecodeTextureResource(words), texture.get()), "native depth owner was not retained"); }, + "native multisample depth clear requires tracked HTILE generations"); + return; + } else { + const bool storedAll = AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation); + Require(storedAll, "driver idle returned before recording the completed HTILE dispatch"); + } + } else { + const auto nativeBytes = fault == ClearFault::UnprovenDispatch ? 0u : static_cast(metadataBytes); + if (fault == ClearFault::PartialStore || fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) { + for (std::size_t i = 0u; i < metadataBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u); + } + const bool completionCandidate = NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); + if (fault == ClearFault::ImportWindowWrite) { + const auto beforeImport = AgcDriver::GuestMemory::TrackerGeneration(); + Require(AgcDriver::GuestMemory::ImportWatched(target.htileAddress, static_cast(metadataBytes), [&] { + metadata.Data()[0] ^= std::byte{0x01}; + return true; + }), "HTILE host import simulation failed"); + Require(AgcDriver::GuestMemory::CollectWritesUncached(target.htileAddress, static_cast(metadataBytes)) > beforeImport, "HTILE import window did not stamp its changed page"); + } + if (completionCandidate) ArmHtileDepthClear(target.htileAddress, static_cast(metadataBytes)); + if (fault == ClearFault::Overlap) NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); + const auto storedBytes = fault == ClearFault::PartialStore ? 4u : static_cast(metadataBytes); + if (fault == ClearFault::PossibleStore || fault == ClearFault::CompletedPossibleStore) { + const auto generation = AgcDriver::GuestMemory::TrackerGeneration(); + AgcDriver::GuestMemory::MarkPossiblyWritten(target.htileAddress, metadataBytes); + Require(!AgcDriver::GuestMemory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation), "a possible HTILE range proved complete coverage"); + if (fault == ClearFault::CompletedPossibleStore) CompleteHtileDepthClear(target.htileAddress, static_cast(metadataBytes)); + } else AgcDriver::GuestMemory::StoreOwnBytes(target.htileAddress, storedBytes, [&] { + for (std::size_t i = 0u; i < storedBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u); + if (fault == ClearFault::BadMetadata) metadata.Data()[0] = std::byte{0x0f}; + }); + if (fault == ClearFault::CpuWrite) metadata.Data()[metadataBytes - 1u] ^= std::byte{0x01}; + if (fault == ClearFault::DepthWrite) memory.Data()[bytes - 1u] ^= std::byte{0x01}; + if (fault == ClearFault::PendingWrite) { + QueueTargetByteWrite(context, recorder, target.htileAddress, std::byte{0}); + std::string pendingRefusal; + try { CachedSampledTexture(context, words); } + catch (const std::exception& error) { pendingRefusal = error.what(); } + const bool refused = pendingRefusal.find("external GPU writes") != std::string::npos; + if (!refused) std::fprintf(stderr, "Pending HTILE texture refusal: %s\n", pendingRefusal.c_str()); + Require(refused, "an unrecognized pending HTILE write was accepted or refused for another reason"); + } + } + Require(DepthSurfaceHolds(context, DecodeTextureResource(words), texture.get()), "completed native depth clear lost its sampled view"); + recorder.Sync(); + Require(DepthSurfaceView(context, target) == view && CachedSampledTexture(context, words) == texture, "completed native depth clear replaced an attachment or cached view"); + std::memset(output.Bytes().data(), 0xa5, output.Bytes().size()); + CommandBatch batch(context); + fetch.Run(batch.Handle(), count); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + check(false, 0.0f); + } + std::printf("Native depth completed clear format=%d samples=%u depthOnly=%u value=%u cycles=3\n", format, fragments, depthOnly, one); + } + } + Require(std::all_of(memory.Data(), memory.Data() + bytes, [](auto value) { return value == std::byte{0xa5}; }), "native depth sampling silently published guest memory"); + std::array probe{}; + for (const auto address : {target.address, target.address + bytes - 1u, target.htileAddress, target.htileAddress + metadataBytes - 1u}) { + TargetRefuses([&] { RequireNoResidentDepthRead_nid_no_patch(address, 1u); }, "sample-preserving guest publication"); + TargetRefuses([&] { AgcDriver::GuestMemory::Read(address, probe); }, "sample-preserving guest publication"); + TargetRefuses([&] { RequireNoMultisampleTarget(address, 1u, "a DMA copy"); }, "sample-preserving guest publication"); + } + TargetMemory unrelated(65536u); + AgcDriver::GuestMemory::Read(unrelated.Address(), probe); + Require(probe[0] == std::byte{0xa5}, "resident depth guard blocked an unrelated read"); + auto alias = target; + alias.htileAddress += 65536u; + TargetRefuses([&] { DepthSurfaceView(context, alias); }, "layout or metadata changed"); + alias = target; + alias.htileDepthOnly = !target.htileDepthOnly; + TargetRefuses([&] { DepthSurfaceView(context, alias); }, "layout or metadata changed"); + auto resource = DecodeTextureResource(words); + for (std::uint32_t field = 0u; field < 18u; ++field) { + auto changed = resource; + switch (field) { + case 0u: changed.samples = fragments == 2u ? 4u : 2u; break; + case 1u: ++changed.width; break; + case 2u: ++changed.height; break; + case 3u: changed.format = 20u; break; + case 4u: changed.dimension = TextureDimension::k2DArray; break; + case 5u: changed.depthOrLastArray = 1u; break; + case 6u: changed.baseArray = 1u; break; + case 7u: changed.minLod = 1u; break; + case 8u: changed.pipeBankXor ^= 0x100u; break; + case 9u: changed.dccAddress += 256u; break; + case 10u: changed.dccPipeAligned = false; break; + case 11u: changed.iterate256 = !changed.iterate256; break; + case 12u: changed.allocatedMipCount = 2u; break; + case 13u: changed.mipCount = 2u; break; + case 14u: changed.baseLevel = 1u; break; + case 15u: changed.lastLevel = 1u; break; + case 16u: changed.dccAlphaOnMsb = true; break; + case 17u: changed.tileMode = TextureTileMode::kR64KBX; break; + } + TargetRefuses([&] { DepthSurfaceTexture(context, words, changed, VkComponentMapping{}); }, "native multisample depth sampled"); + } + memory.Data()[bytes - 1u] = std::byte{0x5a}; + TargetRefuses([&] { CachedSampledTexture(context, words); }, "guest writes"); + TargetRefuses([&] { DepthSurfaceHolds(context, resource, texture.get()); }, "guest writes"); + texture.reset(); + arrayTexture.reset(); + std::printf("Native depth format=%u samples=%u clear=%u iterate256=%u: imported, rasterized twice, sampled and refused conflicts\n", static_cast(format), fragments, one, iterate256); +} + +void CheckUnwrittenGuestAccess(Context context, VkFormat format, std::uint32_t fragments) { + const VkExtent2D extent{5u, 3u}; + const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); + const auto metadataBytes = static_cast(HtileMetadataBytes(extent)); + TargetMemory memory(bytes), metadata(65536u), unrelated(65536u); + const auto guestByte = [](std::size_t offset) { return static_cast(offset * 7u + 3u); }; + for (std::size_t offset = 0u; offset < bytes; ++offset) memory.Data()[offset] = guestByte(offset); + std::memset(metadata.Data(), 0, metadataBytes); + DepthTarget target{memory.Address(), 0u, extent, format, 0.0f, 0u, metadata.Address(), false, fragments}; + target.depthSwizzle = 24u; + target.iterate256 = target.htilePipeAligned = target.htileDepthOnly = true; + Recorder recorder(context); + recorder.Activate(); + struct Cleanup { + Recorder& recorder; + Context context; + ~Cleanup() { recorder.Sync(); ClearCachedTextures(context.device); ClearDepthSurfaces(context.device); } + } cleanup{recorder, context}; + const auto resident = [&] { return HasResidentDepthGuards_nid_no_patch() && DepthSurfaceAt(target.address); }; + auto view = DepthSurfaceView(context, target); + Require(view != VK_NULL_HANDLE && resident(), "unwritten native depth fixture has no resident owner"); + auto otherDevice = context; + otherDevice.device = VK_NULL_HANDLE; + NoteDepthSurfaceWrite(otherDevice, target); + auto unattached = target; + unattached.address += 65536u; + NoteDepthSurfaceWrite(context, unattached); + std::array probe{}; + AgcDriver::GuestMemory::Read(unrelated.Address(), probe); + Require(resident(), "an unrelated guest read retired a native depth surface"); + for (const bool readMetadata : {false, true}) { + if (readMetadata) RequireNoMultisampleTarget(target.htileAddress + metadataBytes - 1u, 1u, "a DMA copy"); + else { + AgcDriver::GuestMemory::Read(target.address + bytes - 1u, probe); + Require(probe[0] == guestByte(bytes - 1u), "a guest read over an unwritten native depth surface did not return its guest bytes"); + } + Require(!resident(), "a guest access left an unwritten native depth surface resident"); + RequireNoResidentDepthRead_nid_no_patch(target.address, bytes); + RequireNoResidentDepthRead_nid_no_patch(target.htileAddress, metadataBytes); + recorder.Sync(); + const auto rebound = DepthSurfaceView(context, target); + Require(rebound != VK_NULL_HANDLE && rebound != view && resident(), "rebinding after a guest access did not import a new native depth surface"); + view = rebound; + } + NoteDepthSurfaceWrite(context, target); + TargetRefuses([&] { AgcDriver::GuestMemory::Read(target.address, probe); }, "sample-preserving guest publication"); + TargetRefuses([&] { RequireNoMultisampleTarget(target.htileAddress, 1u, "a DMA copy"); }, "sample-preserving guest publication"); + Require(resident(), "a refused guest access retired a written native depth surface"); + for (std::size_t offset = 0u; offset < bytes; ++offset) Require(memory.Data()[offset] == guestByte(offset), "retiring an unwritten native depth surface changed its guest bytes"); + Require(std::all_of(metadata.Data(), metadata.Data() + metadataBytes, [](std::byte value) { return value == std::byte{0}; }), "retiring an unwritten native depth surface changed its HTILE"); + std::printf("Native depth format=%u samples=%u: guest access retired the unwritten surface and refused the written one\n", static_cast(format), fragments); +} + +void CheckHtileRouting(Context context, VkFormat format, std::uint32_t fragments) { + const VkExtent2D extent{5u, 3u}; + const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); + TargetMemory memory(bytes), metadata(65536u), single(65536u); + const auto metadataBytes = HtileMetadataBytes(extent); + std::memset(metadata.Data(), 0, metadataBytes); + const std::vector cleared(metadata.Data(), metadata.Data() + metadataBytes); + const auto untouched = [&] { return std::equal(cleared.begin(), cleared.end(), metadata.Data()); }; + DepthTarget target{memory.Address(), 0u, extent, format, 0.0f, 0u, metadata.Address(), false, fragments}; + target.depthSwizzle = 24u; + target.iterate256 = target.htilePipeAligned = true; + Recorder recorder(context); + recorder.Activate(); + struct Cleanup { + Recorder& recorder; + Context context; + ~Cleanup() { recorder.Sync(); ClearCachedTextures(context.device); ClearDepthSurfaces(context.device); } + } cleanup{recorder, context}; + const auto view = DepthSurfaceView(context, target); + Require(HasResidentDepthGuards_nid_no_patch(), "the native depth owner installed no guest guard"); + Require(untouched(), "the first bind of a native depth target stored expanded HTILE words back"); + Require(DepthSurfaceView(context, target) == view && untouched(), "rebinding a native-owned depth target went through the generic HTILE store-back"); + auto alias = target; + alias.samples = 1u; + alias.address = single.Address(); + TargetRefuses([&] { DepthSurfaceView(context, alias); }, "layout or metadata changed"); + Require(untouched(), "a single-sample bind over a native owner's HTILE stored expanded words into it"); + std::printf("Native depth format=%u samples=%u: binds over the owner's HTILE left its words alone\n", static_cast(format), fragments); +} + +void CheckEligibility(Context context) { + constexpr VkExtent2D extent{129u, 67u}; + constexpr std::uint32_t samples = 4u; + const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, 4u, samples)); + TargetMemory memory(bytes), metadata(65536u), stencil(bytes); + std::memset(metadata.Data(), 0, 65536u); + DepthTarget target{memory.Address(), 0u, extent, VK_FORMAT_D32_SFLOAT, 0.0f, 0u, metadata.Address(), false, samples}; + target.depthSwizzle = 24u; + target.iterate256 = target.htilePipeAligned = true; + Recorder recorder(context); + recorder.Activate(); + const auto lookup = [&](const DepthTarget& use) { + const auto words = DepthSampleWords(use); + return DepthSurfaceTexture(context, words, DecodeTextureResource(words), VkComponentMapping{}); + }; + const auto clear = [&] { + recorder.Sync(); + ClearCachedTextures(context.device); + ClearDepthSurfaces(context.device); + }; + struct Cleanup { + decltype(clear)& action; + ~Cleanup() { action(); } + } cleanup{clear}; + for (std::uint32_t field = 0u; field < 19u; ++field) { + auto invalid = target; + std::string_view reason; + switch (field) { + case 0u: invalid.depthSwizzle = 0u; reason = "non-Z_X"; break; + case 1u: invalid.firstSlice = 1u; reason = "arrays"; break; + case 2u: invalid.lastSlice = 1u; reason = "arrays"; break; + case 3u: invalid.pipeBankXor = 0x10u; reason = "XOR"; break; + case 4u: invalid.htileAddress = 0u; reason = "HTILE"; break; + case 5u: invalid.htileAddress += 16u; reason = "HTILE"; break; + case 6u: invalid.htilePipeAligned = false; reason = "HTILE"; break; + case 7u: invalid.htileVrsEncoding = 1u; reason = "VRS"; break; + case 8u: invalid.clearDepth = 0.5f; reason = "clear value"; break; + case 9u: invalid.htileAddress = invalid.address; reason = "overlap"; break; + case 10u: metadata.Data()[HtileMetadataBytes(extent) - 1u] = std::byte{0x40}; reason = "clear value"; break; + case 11u: invalid.format = VK_FORMAT_D32_SFLOAT_S8_UINT; invalid.stencilAddress = stencil.Address(); reason = "stencil"; break; + case 12u: invalid.htileDepthOnly = true; metadata.Data()[0] = std::byte{0x30}; reason = "clear value"; break; + case 13u: invalid.clearDepth = 1.0f; { const std::uint32_t word = 0xfffffff0u; for (std::size_t offset = 0u; offset < HtileMetadataBytes(extent); offset += 4u) std::memcpy(metadata.Data() + offset, &word, 4u); } reason = "clear value"; break; + case 14u: metadata.Data()[0] = std::byte{0xf}; reason = "clear value"; break; + case 15u: invalid.clearDepth = 1.0f; reason = "clear value"; break; + case 16u: invalid.clearDepth = 1.0f; invalid.htileDepthOnly = true; reason = "clear value"; break; + case 17u: { const std::uint32_t word = 0xfffc0000u; for (std::size_t offset = 0u; offset < HtileMetadataBytes(extent); offset += 4u) std::memcpy(metadata.Data() + offset, &word, 4u); } reason = "clear value"; break; + case 18u: invalid.htileDepthOnly = true; { const std::uint32_t word = 0xfffffff0u; for (std::size_t offset = 0u; offset < HtileMetadataBytes(extent); offset += 4u) std::memcpy(metadata.Data() + offset, &word, 4u); } reason = "clear value"; break; + } + Require(DepthSurfaceView(context, invalid) != VK_NULL_HANDLE, "sampling ineligibility must preserve attachment rendering"); + TargetRefuses([&] { lookup(invalid); }, reason); + if (field == 11u) { + GuestTextureResource alias{}; + alias.baseAddress = invalid.stencilAddress; + alias.width = extent.width; + alias.height = extent.height; + alias.dimension = TextureDimension::k2D; + alias.samples = 1u; + alias.format = 36u; + Require(DepthSurfaceTexture(context, {}, alias, VkComponentMapping{}) == nullptr, "single-sample color at a native stencil address must not be claimed as a depth reader"); + alias.format = 5u; + TargetRefuses([&] { DepthSurfaceTexture(context, {}, alias, VkComponentMapping{}); }, "stencil"); + } + if (field == 10u) { + std::memset(metadata.Data(), 0, 65536u); + TargetRefuses([&] { lookup(invalid); }, reason); + } + clear(); + std::memset(metadata.Data(), 0, 65536u); + } + { + TargetMemory unwatched(bytes); + AgcDriver::GuestMemory::Unwatch(unwatched.Address(), bytes); + auto invalid = target; + invalid.address = unwatched.Address(); + Require(DepthSurfaceView(context, invalid) != VK_NULL_HANDLE, "unwatched sampling memory must preserve attachment rendering"); + TargetRefuses([&] { lookup(invalid); }, "watched accessible guest memory"); + NoteDepthMetadataFill(invalid.htileAddress, HtileMetadataBytes(extent), 0u); + TargetRefuses([&] { lookup(invalid); }, "watched accessible guest memory"); + clear(); + } + DepthSurfaceView(context, target); + metadata.Data()[HtileMetadataBytes(extent) - 1u] = std::byte{0x40}; + TargetRefuses([&] { lookup(target); }, "guest writes"); + clear(); + std::memset(metadata.Data(), 0, 65536u); + DepthSurfaceView(context, target); + QueueTargetByteWrite(context, recorder, target.address + bytes - 1u, std::byte{0x5a}); + TargetRefuses([&] { lookup(target); }, "external GPU writes"); + clear(); + DepthSurfaceView(context, target); + NoteDepthMetadataFill(target.htileAddress, HtileMetadataBytes(extent), 0u); + TargetRefuses([&] { lookup(target); }, "metadata fill transition"); + clear(); + const auto initialView = DepthSurfaceView(context, target); + auto changedClearRegister = target; + changedClearRegister.clearDepth = 1.0f; + Require(DepthSurfaceView(context, changedClearRegister) == initialView, "changing only DB_DEPTH_CLEAR must reuse the native attachment without recertifying its initial contents"); + auto texture = lookup(target); + recorder.Sync(); + ClearDepthSurfaces(context.device); + Require(texture != nullptr && texture->View() != VK_NULL_HANDLE, "a held depth view lost its native image owner"); + Buffer output(context, (extent.width * extent.height * samples + 2u) * 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::memset(output.Bytes().data(), 0xa5, output.Bytes().size()); + { + const Fetch fetch(context, *texture, output, FetchShader(extent.width, extent.height, samples, false, true)); + CommandBatch batch(context); + fetch.Run(batch.Handle(), extent.width * extent.height * samples); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + output.Invalidate(); + for (std::uint32_t i = 0u; i < extent.width * extent.height * samples; ++i) { + std::array value{}; + std::memcpy(value.data(), output.Bytes().data() + (i + 1u) * 16u, 16u); + Require(value == std::array{0.0f, 0.0f, 0.0f, 1.0f}, "a held native depth view lost its image contents when the surface cache was cleared"); + } + } + texture.reset(); + std::puts("Native depth eligibility, guest writes, queued writes, metadata transitions and held views checked"); +} + +void CheckRetirement(Context context, VkFormat format, std::uint32_t samples, std::uint32_t mode) { + constexpr VkExtent2D extent{5u, 3u}; + const auto depthBytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, samples)); + const auto metadataBytes = static_cast(HtileMetadataBytes(extent)); + const auto allocationBytes = depthBytes + 65536u; + TargetMemory memory(allocationBytes), colorMemory(65536u); + const auto release = [](const void*, std::size_t, const void*, bool) {}; + const auto add = [&](TargetMemory& allocation, std::size_t bytes) { + GuestAllocations::Mutation mutation; + mutation.Add(allocation.Data(), bytes, true, true, true); + }; + const auto remove = [&](TargetMemory& allocation, std::size_t bytes) { + GuestAllocations::Mutation mutation; + if (mutation.Overlaps(allocation.Data(), bytes)) mutation.Unmap(allocation.Data(), bytes, release); + }; + Recorder recorder(context); + recorder.Activate(); + const auto cleanup = [&] { + recorder.Sync(); + ClearCachedTextures(context.device); + ClearDepthSurfaces(context.device); + ClearMultisampleTargets(context.device); + remove(memory, allocationBytes); + remove(colorMemory, 65536u); + }; + struct Cleanup { + decltype(cleanup)& action; + ~Cleanup() { action(); } + } cleanupScope{cleanup}; + add(memory, allocationBytes); + add(colorMemory, 65536u); + std::memset(memory.Data() + depthBytes, 0, metadataBytes); + DepthTarget target{memory.Address(), 0u, extent, format, 0.0f, 0u, memory.Address() + depthBytes, false, samples}; + target.depthSwizzle = 24u; + target.iterate256 = target.htilePipeAligned = target.htileDepthOnly = true; + const auto words = DepthSampleWords(target); + const auto resource = DecodeTextureResource(words); + const auto firstView = DepthSurfaceView(context, target); + NoteDepthSurfaceWrite(context, target); + auto firstTexture = DepthSurfaceTexture(context, words, resource, VkComponentMapping{}); + Require(firstTexture != nullptr, "retirement fixture has no native sampled view"); + ColorTarget color{colorMemory.Address(), extent, VK_FORMAT_R8G8B8A8_UNORM, extent.width * extent.height * 4u, 0u}; + color.tileMode = ColorTileMode::RenderTarget; + color.samples = 4u; + color.surfaceAddress = color.address; + Require(MultisampleTargetView(context, color) != VK_NULL_HANDLE, "retirement fixture has no color attachment"); + recorder.Sync(); + const auto guarded = [&] { + TargetRefuses([&] { RequireNoResidentDepthRead_nid_no_patch(target.address, 1u); }, "sample-preserving"); + }; + { + GuestAllocations::Mutation mutation; + mutation.Protect(memory.Data(), allocationBytes, true, true, true, [] {}); + } + guarded(); + { + GuestAllocations::Mutation mutation; + TargetRefuses([&] { mutation.Unmap(memory.Data(), allocationBytes, [](const void*, std::size_t, const void*, bool) { throw std::runtime_error("retirement fixture failed host release"); }); }, "failed host release"); + Require(mutation.Covers(memory.Data(), allocationBytes), "failed host release changed registry coverage"); + } + guarded(); + if ((mode & 1u) == 0u) ClearMultisampleTargets(context.device); + const auto unmap = [&](std::size_t offset, std::size_t bytes) { + GuestAllocations::Mutation mutation; + mutation.Unmap(memory.Data() + offset, bytes, release); + }; + if (mode == 0u) { + GuestAllocations::Mutation mutation; + mutation.Remove(memory.Data()); + } else if (mode == 1u) unmap(0u, allocationBytes); + else if (mode == 2u) { + unmap(0u, depthBytes / 2u); + guarded(); + unmap(depthBytes / 2u, depthBytes / 2u); + guarded(); + unmap(depthBytes, metadataBytes / 2u); + guarded(); + unmap(depthBytes + metadataBytes / 2u, metadataBytes / 2u); + } else { + unmap(depthBytes / 2u, depthBytes / 2u + metadataBytes / 2u); + guarded(); + unmap(0u, depthBytes / 2u); + guarded(); + unmap(depthBytes + metadataBytes / 2u, metadataBytes / 2u); + } + RequireNoResidentDepthRead_nid_no_patch(target.address, 1u); + RequireNoResidentDepthRead_nid_no_patch(target.htileAddress, metadataBytes); + Require(!DepthSurfaceHolds(context, resource, firstTexture.get()) && !DepthSurfaceAt(target.address), "a fully released depth owner remains discoverable"); + if ((mode & 1u) != 0u) { + ClearDepthSurfaces(context.device); + TargetRefuses([&] { RequireNoMultisampleTarget(color.address, 1u, "a CPU read"); }, "multisampled"); + remove(colorMemory, 65536u); + RequireNoMultisampleTarget(color.address, 1u, "a CPU read"); + } + remove(memory, allocationBytes); + add(memory, allocationBytes); + target.clearDepth = 1.0f; + const std::uint32_t one = 0xfffffff0u; + for (std::size_t offset = 0u; offset < metadataBytes; offset += 4u) std::memcpy(memory.Data() + depthBytes + offset, &one, 4u); + const auto remappedView = DepthSurfaceView(context, target); + Require(remappedView != VK_NULL_HANDLE && ((mode & 1u) != 0u || remappedView != firstView), "a remapped allocation reused its old native depth attachment"); + auto fresh = DepthSurfaceTexture(context, words, resource, VkComponentMapping{}); + Require(fresh != nullptr && fresh->View() != firstTexture->View(), "a remapped allocation reused its old sampled view"); + recorder.Sync(); + const auto count = extent.width * extent.height * samples; + Buffer output(context, (count + 2u) * 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + for (const auto& [texture, expected] : {std::pair{firstTexture, 0.0f}, std::pair{fresh, 1.0f}}) { + std::memset(output.Bytes().data(), 0xa5, output.Bytes().size()); + const Fetch fetch(context, *texture, output, FetchShader(extent.width, extent.height, samples, false, true)); + CommandBatch batch(context); + fetch.Run(batch.Handle(), count); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + output.Invalidate(); + for (std::uint32_t i = 0u; i < count; ++i) { + std::array value{}; + std::memcpy(value.data(), output.Bytes().data() + (i + 1u) * 16u, 16u); + if (value != std::array{expected, 0.0f, 0.0f, 1.0f}) std::printf("Retirement sample format=%d samples=%u mode=%u index=%u expected=%g actual=(%g,%g,%g,%g)\n", format, samples, mode, i, expected, value[0], value[1], value[2], value[3]); + Require(value == std::array{expected, 0.0f, 0.0f, 1.0f}, "retired or remapped depth image lost its exact contents"); + } + Require(std::all_of(output.Bytes().begin(), output.Bytes().begin() + 16u, [](std::byte value) { return value == std::byte{0xa5}; }) && std::all_of(output.Bytes().end() - 16u, output.Bytes().end(), [](std::byte value) { return value == std::byte{0xa5}; }), "retirement fetch wrote beyond its output"); + } +} + +} + +int main(int argc, char** argv) { + try { + std::unique_ptr device; + try { device = std::make_unique(); } + catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return VulkanTestSkipped; + } + CheckDrawFeedback(); + const auto context = device->GetContext(); + std::unique_lock lock(AgcDriver::GuestMemory::GpuMutex()); + Require(DepthSupported(context, VK_FORMAT_D32_SFLOAT, 4u), "the native depth regression requires sampled D32 with four samples"); + if (argc == 2 && std::string_view(argv[1]) == "--clear-refusals") { + for (const auto& [fault, expected] : std::array{ + std::pair{ClearFault::PartialStore, "no complete tracked HTILE store"}, + std::pair{ClearFault::CpuWrite, "CPU writes raced"}, + std::pair{ClearFault::DepthWrite, "guest writes"}, + std::pair{ClearFault::BadMetadata, "does not prove its clear value"}, + std::pair{ClearFault::PossibleStore, "no complete tracked HTILE store"}, + std::pair{ClearFault::UnprovenDispatch, "incomplete or unproven"}, + std::pair{ClearFault::PendingWrite, "external GPU writes"}, + std::pair{ClearFault::Overlap, "overlapping native multisample depth"}}) { + TargetRefuses([&] { CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, fault, lock); }, expected); + std::printf("Native depth clear fault=%u refused: %s\n", static_cast(fault), expected); + } + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--clears") { + CheckClearDispatch(); + std::uint32_t checked = 0u, unavailable = 0u; + for (const auto format : {VK_FORMAT_D16_UNORM, VK_FORMAT_D32_SFLOAT}) { + for (const auto samples : {2u, 4u, 8u}) { + if (!DepthSupported(context, format, samples)) { unavailable += 4u; continue; } + for (const bool depthOnly : {false, true}) { + for (const bool one : {false, true}) { CheckDepthTarget(context, format, samples, one, true, depthOnly, ClearFault::None, lock); ++checked; } + } + } + } + CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::CompletedPossibleStore, lock); + ++checked; + CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::ImportWindowWrite, lock); + ++checked; + CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, ClearFault::DriverDispatch, lock); + ++checked; + std::printf("Native depth completed clear cases checked=%u unavailable=%u\n", checked, unavailable); + return 0; + } + Require(argc == 1 || (argc == 2 && std::string_view(argv[1]) == "--retirement"), "invalid native depth fixture arguments"); + if (argc == 2) { + std::uint32_t checked = 0u, unavailable = 0u; + for (const auto format : {VK_FORMAT_D16_UNORM, VK_FORMAT_D32_SFLOAT}) { + for (const auto samples : {2u, 4u, 8u}) { + if (!DepthSupported(context, format, samples)) { unavailable += 4u; continue; } + for (std::uint32_t mode = 0u; mode < 4u; ++mode) { CheckRetirement(context, format, samples, mode); ++checked; } + } + } + std::printf("Native depth retirement cases checked=%u unavailable=%u\n", checked, unavailable); + return 0; + } + std::uint32_t checked = 0u, unavailable = 0u; + for (const auto format : {VK_FORMAT_D16_UNORM, VK_FORMAT_D32_SFLOAT}) { + for (const auto samples : {2u, 4u, 8u}) { + if (!DepthSupported(context, format, samples)) { unavailable += 4u; continue; } + for (const bool one : {false, true}) { + for (const bool iterate256 : {true, false}) { CheckDepthTarget(context, format, samples, one, false, std::nullopt, ClearFault::None, lock, iterate256); ++checked; } + } + CheckHtileRouting(context, format, samples); + CheckUnwrittenGuestAccess(context, format, samples); + } + } + CheckEligibility(context); + std::printf("Native multisampled depth cases checked=%u unavailable=%u\n", checked, unavailable); + return 0; + } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); return 1; } +} 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/OversizedDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp index 068e0f31cd..b31f2c597f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp @@ -18,12 +18,19 @@ #include #include #include +#include #include #include #include namespace { +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + using AgcDriver::Graphics::Require; constexpr std::uint32_t Threads = 64; @@ -94,7 +101,7 @@ void CheckSplitCoverage(AgcDriver::VulkanDevice& device) { device.WaitIdle(); AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(guest.Data()), guest.Bytes); for (std::uint32_t i = 0; i < Stores; ++i) { - Require(guest.Data()[i] == Pattern[i % Period], "oversized dispatch: dword " + std::to_string(i) + " is 0x" + std::to_string(guest.Data()[i]) + ", expected 0x" + std::to_string(Pattern[i % Period])); + Require(guest.Data()[i] == Pattern[i % Period], "oversized dispatch: dword " + std::to_string(i) + " is " + Hex(guest.Data()[i]) + ", expected " + Hex(Pattern[i % Period])); } } @@ -135,38 +142,50 @@ void CheckLdsSplitThrows(AgcDriver::VulkanDevice& device) { request.useCache = false; const auto result = ShaderRecompiler::Recompile(request); Require(result.workgroupMemoryDwords != 0u, "oversized dispatch: the LDS shader did not take the device-memory path"); - bool threw = false; - try { - device.Dispatch(result, 1, device.WorkGroupCountLimit()[1] + 1u, 1, {}, reinterpret_cast(code.data())); - } catch (const std::runtime_error&) { - threw = true; + for (const auto z : {1u, std::numeric_limits::max()}) { + bool threw = false; + try { + device.Dispatch(result, 1, device.WorkGroupCountLimit()[1] + 1u, z, {}, reinterpret_cast(code.data())); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("a split aliases the device-memory LDS slots") != std::string::npos, "oversized dispatch: wrong LDS split refusal: " + std::string(error.what())); + threw = true; + } + device.WaitIdle(); + Require(threw, "oversized dispatch: a Y count over the limit with device-memory LDS did not throw"); } - device.WaitIdle(); - Require(threw, "oversized dispatch: a Y count over the limit with device-memory LDS did not throw"); } -void CheckNvidiaSplitThrows(AgcDriver::VulkanDevice& device) { - const GuestBlock guest(Threads); +alignas(256) constexpr std::array GroupYCode{ + 0x7e000204u, 0x34020082u, 0x4a040081u, 0xe0701000u, 0x80000201u, 0xbf810000u}; +constexpr std::uint32_t GroupYSplitCap = 1u << 20u; +constexpr std::uint32_t GroupYTail = 4097u; + +bool CheckGroupYSplitCoverage(AgcDriver::VulkanDevice& device) { + const auto limit = device.WorkGroupCountLimit()[1]; + if (limit >= GroupYSplitCap) { + std::printf("oversized dispatch Y split: skipped, the device reports a %u Y workgroup limit\n", limit); + return false; + } + const auto groups = limit + GroupYTail; + const GuestBlock guest(groups); const auto address = static_cast(reinterpret_cast(guest.Data())); const std::vector userData{ - static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), - Threads, 0x00014004u, Pattern[0], Pattern[1], Pattern[2], Pattern[3], Threads, Period}; - const std::span code(Code); + static_cast(address), static_cast((address >> 32u) & 0xffffu), groups * 4u, 0x31016facu}; + const std::span code(GroupYCode); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; - const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {true, false, false}, false, 1}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, true, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, {64, 0, userData, compute, std::nullopt, std::nullopt, memory}, device.Target(), {0, 0, 0, 128}, std::nullopt, false}; const auto result = ShaderRecompiler::Recompile(request); - bool threw = false; - try { - device.Dispatch(result, 1, device.WorkGroupCountLimit()[1] + 1u, 1, {}, reinterpret_cast(code.data())); - } catch (const std::runtime_error&) { - threw = true; - } + device.Dispatch(result, 1, groups, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); - Require(threw, "oversized dispatch: NVIDIA did not throw for a count over the workgroup limit"); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(guest.Data()), guest.Bytes); + for (std::uint32_t group = 0; group < groups; ++group) { + Require(guest.Data()[group] == group + 1u, "oversized dispatch Y split: dword " + std::to_string(group) + " is " + std::to_string(guest.Data()[group]) + ", expected " + std::to_string(group + 1u)); + } + return true; } } @@ -176,12 +195,15 @@ int main() { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; CheckLdsSplitThrows(*device); - if (device->VendorId() == 0x10de) { - CheckNvidiaSplitThrows(*device); - } else if (device->WorkGroupCountLimit()[0] < Groups) { + bool split = CheckGroupYSplitCoverage(*device); + if (device->WorkGroupCountLimit()[0] < Groups) { CheckSplitCoverage(*device); + split = true; } else { - std::printf("oversized dispatch split: skipped, the device reports a %u workgroup limit, which never splits a %ux1x1 dispatch\n", device->WorkGroupCountLimit()[0], Groups); + std::printf("oversized dispatch X split: skipped, the device reports a %u workgroup limit, which never splits a %ux1x1 dispatch\n", device->WorkGroupCountLimit()[0], Groups); + } + if (!split) { + AgcDriverShutdown_nid_postfix(); return VulkanTestSkipped; } AgcDriverShutdown_nid_postfix(); 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..238cb5ce89 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp @@ -0,0 +1,342 @@ +#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 StandardOrderInfo = Float11_11_10Info & ~0x1800u; +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, 64> 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, bool standardOrder) { + if (standardOrder) return SmallFloat(rgb[0], 5) | (SmallFloat(rgb[1], 6) << 10u) | (SmallFloat(rgb[2], 6) << 21u); + 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, bool standardOrder) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = standardOrder ? StandardOrderInfo : 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 == (standardOrder ? 0xe4u : 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, bool standardOrder) { + 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, standardOrder); + 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, standardOrder), 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, standardOrder), 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()); + for (const bool standardOrder : {false, true}) { + TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, standardOrder ? "tiled standard-order" : "tiled", standardOrder); + TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, standardOrder ? "linear standard-order" : "linear", standardOrder); + } + 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 4bdf081efe..0afb33266f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp @@ -45,6 +45,8 @@ constexpr std::uint32_t TileR64KBX = 0x1b; constexpr std::uint32_t Type2D = 9; constexpr std::size_t BlockBytes = 65536; constexpr std::size_t SurfaceBytes = std::size_t{Side} * Side * 4u; +constexpr std::uint32_t LargeSide = 512; +constexpr std::size_t LargeSurfaceBytes = std::size_t{LargeSide} * LargeSide * 4u; constexpr int Skipped = 77; class Device { @@ -110,6 +112,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 (...) { @@ -148,16 +151,16 @@ private: class ReservedSurface { public: - ReservedSurface() { - block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, BlockBytes)); + explicit ReservedSurface(std::size_t bytes) : size(bytes) { + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(size, BlockBytes)); Require(block != nullptr, "partially resident snapshot: cannot reserve the guest surface"); - GuestAllocations::Mutation().Add(block, SurfaceBytes, true, true, true); + GuestAllocations::Mutation().Add(block, size, true, true, true); } ~ReservedSurface() { GuestAllocations::Mutation().Remove(block); - Decommit(0, SurfaceBytes); - GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); + Decommit(0, size); + GuestArena::GuestArenaRelease_nid_postfix(block, size); } ReservedSurface(const ReservedSurface&) = delete; @@ -188,6 +191,7 @@ public: std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } private: + std::size_t size = 0; std::uint8_t* block = nullptr; }; @@ -199,11 +203,11 @@ void CountReads(std::uint64_t address, std::size_t bytes) { if (address < surfaceEnd.load() && address + bytes > surfaceBegin.load()) surfaceReads.fetch_add(1); } -std::array Descriptor(std::uint64_t address) { +std::array Descriptor(std::uint64_t address, std::uint32_t side = Side) { return { static_cast(address >> 8u), - static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), - ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((side - 1u) & 3u) << 30u), + ((side - 1u) >> 2u) | ((side - 1u) << 14u), 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), 0u, 0u, @@ -262,6 +266,93 @@ void Run(const Context& context, ReservedSurface& surface) { AgcDriver::GuestMemory::SetFlushHook(nullptr); } +void LargeTracked(const Context& context, ReservedSurface& surface, std::size_t committedBytes) { + const auto address = surface.Address(); + const auto words = Descriptor(address, LargeSide); + const auto layout = committedBytes == LargeSurfaceBytes ? std::string("committed") : std::string("partially resident"); + Require(DescribeSurface(DecodeTextureResource(words)).guestBytes == LargeSurfaceBytes, "the large test descriptor does not describe the reserved surface"); + surface.Commit(0, committedBytes); + std::memset(surface.Data(), 0x5a, committedBytes); + surfaceBegin = address; + surfaceEnd = address + LargeSurfaceBytes; + std::shared_ptr previous; + const auto sample = [&](const std::string& when, bool uploaded) { + AgcDriver::GuestMemory::BumpCollectEpoch(); + surfaceReads = 0; + auto texture = CachedSampledTexture(context, words); + Require(texture != nullptr && !texture->ViewsStorageImage(), "the large " + layout + " surface was not sampled through a snapshot " + when); + const auto reads = surfaceReads.load(); + if (uploaded && (reads == 0 || texture == previous)) throw std::runtime_error("sampling the large " + layout + " surface " + when + " kept the texture uploaded before it"); + if (!uploaded && (reads != 0 || texture != previous)) throw std::runtime_error("sampling the large " + layout + " surface " + when + " uploaded it again although the write tracker saw no write (" + std::to_string(reads) + " reads)"); + previous = texture; + }; + AgcDriver::GuestMemory::SetFlushHook(&CountReads); + try { + sample("at its first use", true); + sample("again", false); + surface.Data()[0x100] = 0x11; + sample("after a CPU write", true); + sample("after the upload of the written surface", false); + surface.Data()[committedBytes - 0x40] = 0x22; + sample("after a CPU write to its last committed block", true); + sample("after the upload of the written block", false); + AgcDriver::GuestMemory::MarkWritten(address + 0x200, 4); + sample("after a driver store of the bytes already there, which a snapshot it did not keep cannot tell from a change", true); + sample("after the upload that followed the driver store", false); + } catch (...) { + AgcDriver::GuestMemory::SetFlushHook(nullptr); + throw; + } + AgcDriver::GuestMemory::SetFlushHook(nullptr); + ClearCachedTextures(context.device); + surface.Decommit(0, committedBytes); +} + +#ifdef _WIN32 +void UntrackedHint(const Context& context, ReservedSurface& surface) { + const auto address = surface.Address(); + const auto words = Descriptor(address); + surface.Commit(0, SurfaceBytes); + std::memset(surface.Data(), 0x5a, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes); + Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes) && AgcDriver::GuestMemory::UntrackedHint(address, SurfaceBytes) != 0, "the surface did not leave the write watch with an untracked hint"); + surfaceBegin = address; + surfaceEnd = address + SurfaceBytes; + std::shared_ptr previous; + const auto sample = [&](const std::string& when, bool compared, bool same) { + AgcDriver::GuestMemory::BumpCollectEpoch(); + surfaceReads = 0; + auto texture = CachedSampledTexture(context, words); + Require(texture != nullptr && !texture->ViewsStorageImage(), "the untracked surface was not sampled through a snapshot " + when); + const auto reads = surfaceReads.load(); + if (compared && reads == 0) throw std::runtime_error("the cached snapshot of the untracked surface was not compared with guest memory " + when); + if (!compared && reads != 0) throw std::runtime_error("the cached snapshot of the untracked surface was compared with guest memory " + when + " although its untracked hint did not move (" + std::to_string(reads) + " reads)"); + if (previous != nullptr && same != (texture == previous)) throw std::runtime_error(std::string("sampling the untracked surface ") + when + (same ? " replaced the snapshot" : " kept the stale snapshot")); + previous = texture; + }; + AgcDriver::GuestMemory::SetFlushHook(&CountReads); + try { + sample("at its first use", true, false); + for (int epoch = 0; epoch < 50; ++epoch) sample("in a collect epoch without a store", false, true); + surface.Data()[0x100] = 0x11; + sample("after a CPU write", true, false); + sample("after the re-read of the written block", false, true); + auto* stored = static_cast(surface.Data()) + 2 * BlockBytes + 0x40; + *stored = *stored; + sample("after a CPU store of the bytes already there", true, true); + sample("after the compare that followed that store", false, true); + AgcDriver::GuestMemory::MarkWritten(address + 0x200, 4); + sample("after a driver store", true, true); + sample("after the compare that followed the driver store", false, true); + } catch (...) { + AgcDriver::GuestMemory::SetFlushHook(nullptr); + throw; + } + AgcDriver::GuestMemory::SetFlushHook(nullptr); +} +#endif + void PendingStores(const Context& context, ReservedSurface& surface) { const auto address = surface.Address(); const auto words = Descriptor(address); @@ -323,10 +414,21 @@ int main(int argc, char** argv) { auto context = device->GetContext(); TextureDetiler detiler(context); context.detiler = &detiler; - ReservedSurface surface; + const bool large = argc == 2 && std::string_view(argv[1]) == "--large-tracked"; + ReservedSurface surface(large ? LargeSurfaceBytes : SurfaceBytes); std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); - if (argc == 2 && std::string_view(argv[1]) == "--pending-stores") PendingStores(context, surface); - else Run(context, surface); + if (large) { + LargeTracked(context, surface, LargeSurfaceBytes); + LargeTracked(context, surface, BlockBytes); + } else if (argc == 2 && std::string_view(argv[1]) == "--pending-stores") PendingStores(context, surface); + else if (argc == 2 && std::string_view(argv[1]) == "--untracked-hint") { +#ifdef _WIN32 + UntrackedHint(context, surface); +#else + std::puts("skipped, only Windows host imports leave the write watch with a hint"); + return Skipped; +#endif + } else Run(context, surface); ClearCachedTextures(context.device); } device.reset(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PassInputHazard.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PassInputHazard.cpp new file mode 100644 index 0000000000..cbe0f54f42 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PassInputHazard.cpp @@ -0,0 +1,241 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.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 "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using DepthFastClearHarness::BufferDescriptor; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Triangles; +using DepthFastClearHarness::VertexCode; +using DepthFastClearHarness::Width; +using ShaderRecompiler::ShaderStage; + +constexpr std::size_t BlockBytes = 65536; +const ColorTargetLayout TargetLayout(Width, Height, ColorTileMode::RenderTarget, 4); + +alignas(4096) std::array FirstDepth{}; +alignas(4096) std::array SecondDepth{}; + +alignas(256) constexpr std::array CoverCode{0x7e0002f2, 0xf800180f, 0x00000000, 0xbf810000}; +alignas(256) constexpr std::array StoreCode{0x7e0a0280, 0x7e0c0280, 0xe0702000, 0x80010506, 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000}; + +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, "pass input hazard: cannot map a 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, "pass input hazard: cannot allocate a block"); + std::memset(data, 0, bytes); + 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; +}; + +struct Program { + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult pixel; + std::uint32_t vertexPush = 0; +}; + +Program Compile(AgcDriver::VulkanDevice& device, const void* vertices, std::span pixelCode, const void* stored) { + const auto target = device.Target(); + Program program; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(vertices, 16u, static_cast(Triangles().size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {32, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + program.vertex = ShaderRecompiler::Recompile(vertex); + program.vertexPush = static_cast(program.vertex.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = true; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + std::vector pixelUserData(8, 0u); + if (stored != nullptr) { + const auto storeBuffer = BufferDescriptor(stored, 4u, 1u); + std::copy(storeBuffer.begin(), storeBuffer.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, {}}, + {32, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(program.vertexPush, target) + }; + fragment.useCache = false; + program.pixel = ShaderRecompiler::Recompile(fragment); + return program; +} + +void Draw(AgcDriver::VulkanDevice& device, const Program& program, const Block& target, const float* depth, const std::uint32_t* indices) { + const std::array shaders{{ + {ShaderStage::Vertex, &program.vertex, 0}, + {ShaderStage::Fragment, &program.pixel, PixelPushOffset(program.vertexPush, device.Target())} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {target.Address(), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, TargetLayout.Bytes(), 0xe4u, ColorTileMode::RenderTarget}; + state.colors = {state.color}; + state.hasColorTarget = true; + if (depth != nullptr) { + state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(depth), 0, {Width, Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + } + 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{reinterpret_cast(indices), static_cast(Triangles().size()), indices != nullptr ? 4u : 0u, 1, 0, indices != nullptr}; + device.Draw(state, draw, shaders); +} + +bool WriterFenced(std::uint64_t address, std::size_t bytes) { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "pass input hazard: no active recorder"); + const std::array, 1> writes{{{address, address + bytes}}}; + return recorder->StartDrawPass(0x5eed, false, false, AgcDriver::Graphics::PassAccess{{}, writes, {}, {}, false, false}).barrier; +} + +std::size_t Covered(AgcDriver::VulkanDevice& device, const Block& target) { + AgcDriver::Graphics::StorageTexture::FlushPending(target.Address(), target.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(TargetLayout.Bytes()); + AgcDriver::GuestMemory::Read(target.Address(), std::as_writable_bytes(std::span(stored)), 1); + return static_cast(std::count(stored.begin(), stored.end(), std::uint8_t{0xff})); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const Block inputs(BlockBytes); + auto* indices = reinterpret_cast(inputs.data); + constexpr std::array triangle{0u, 1u, 2u}; + const auto reader = Compile(*device, Triangles().data(), CoverCode, nullptr); + const auto writer = Compile(*device, Triangles().data(), StoreCode, indices + 1); + constexpr std::size_t all = std::size_t{Width} * Height * 4u; + const std::array, 3> depths{{{nullptr, nullptr}, {FirstDepth.data(), nullptr}, {FirstDepth.data(), SecondDepth.data()}}}; + for (const auto& [readDepth, writeDepth] : depths) { + const Block read(TargetLayout.Bytes()); + const Block written(TargetLayout.Bytes()); + std::memcpy(indices, triangle.data(), sizeof(triangle)); + const auto variant = std::string(readDepth == nullptr ? "without depth" : writeDepth == nullptr ? "with depth on the reader" : "with a depth target each"); + Draw(*device, reader, read, readDepth, indices); + if (device->DrawInputsInPlace()) Require(WriterFenced(inputs.Address(), sizeof(triangle)), "pass input hazard " + variant + ": a pass writing the indices an open pass reads in place took no barrier"); + Draw(*device, reader, read, readDepth, indices); + Draw(*device, writer, written, writeDepth, nullptr); + Require(Covered(*device, read) == all, "pass input hazard " + variant + ": the draw reading its indices in place did not cover its target, a later pass overwrote them first"); + Require(Covered(*device, written) == all, "pass input hazard " + variant + ": the storing draw did not cover its target"); + std::array stored{}; + AgcDriver::GuestMemory::Read(inputs.Address(), std::as_writable_bytes(std::span(stored)), 4); + Require(stored == std::array{0u, 0u, 2u}, "pass input hazard " + variant + ": the storing draw's write did not reach the indices"); + } + std::puts("pass input hazard tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineJournal.cpp new file mode 100644 index 0000000000..16d3742761 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineJournal.cpp @@ -0,0 +1,431 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "../NarrowConstantStoreFixture.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 32; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Color1555Info = 0x8044u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32Export = 9; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array PixelProgram{}; +std::size_t pixelProgramWords = 0; + +std::span Program(const std::array& color) { + std::size_t at = 0; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + PixelProgram[at++] = 0x7e0002ffu | ((4u + channel) << 17u); + PixelProgram[at++] = std::bit_cast(color[channel]); + } + PixelProgram[at++] = 0xf800180fu; + PixelProgram[at++] = 0x07060504u; + PixelProgram[at++] = 0xbf810000u; + pixelProgramWords = at; + return std::span(PixelProgram.data(), at); +} + +class Block { +public: + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the guest block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the guest block"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Color1555Info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_A1R5G5B5_UNORM_PACK16 && color.bytes == block.bytes, "the test target did not decode as an A1R5G5B5 surface"); + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment); + const auto pixelResult = invocation.Materialize(*invocation.Capture(runtime)); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +void ExpectTexel(AgcDriver::VulkanDevice& device, const Block& block, std::uint16_t texel, const char* what) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes / 2u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + const ColorTargetLayout layout(Width, Height, ColorTileMode::Linear, 2); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + if (stored[index] == texel) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) holds 0x%04x, expected 0x%04x", x, y, stored[index], texel); + throw std::runtime_error(std::string(what) + detail); + } + } +} + +// The constant-store compute program with its two stored literals replaced: `salt` 0 is the +// fixture's program, every other value a program of its own with its own module. +std::vector ComputeCode(std::uint32_t salt) { + std::vector code(NarrowConstantStoreFixture::Code.begin(), NarrowConstantStoreFixture::Code.end()); + if (salt != 0) { + code[3] = 0x00a10200u ^ (salt * 0x01010101u); + code[5] = 0x0000fec3u ^ (salt * 0x00010001u); + } + return code; +} + +ShaderRecompiler::RecompileResult CompileCompute(AgcDriver::VulkanDevice& device, const std::vector& code, std::span userData, std::uint32_t waveSize) { + using namespace ShaderRecompiler; + const std::span words(code); + const std::array memory{{{reinterpret_cast(words.data()), std::as_bytes(words)}}}; + const ShaderComputeStageInfo compute{{NarrowConstantStoreFixture::Threads, 1, 1}, 0, {false, false, false}, false, 1, {}}; + RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(words.data()), words, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), {0, 0, 0, 128}, std::nullopt, false, + }; + return Recompile(request); +} + +void ExpectConstantStore(const Block& block, const char* what) { + using namespace NarrowConstantStoreFixture; + for (std::size_t offset = 0; offset < Threads * LaneBytes; ++offset) { + const auto expected = ExpectedLane[offset % LaneBytes]; + Require(block.data[offset] == expected, std::string(what) + ": byte " + std::to_string(offset) + " is " + std::to_string(block.data[offset]) + ", expected " + std::to_string(expected)); + } +} + +std::array AddressWords(const Block& block) { + const auto address = block.Address(); + return {static_cast(address), static_cast(address >> 32u)}; +} + +void SetEnvironment(const char* name, const std::string& value) { +#ifdef _WIN32 + _putenv_s(name, value.c_str()); +#else + setenv(name, value.c_str(), 1); +#endif +} + +struct Totals { + AgcDriver::Graphics::PipelineJournalCounters journal; + AgcDriver::Graphics::PipelineStoreCounters store; +}; + +Totals Snapshot() { + return {AgcDriver::Graphics::PipelineJournalTotals(), AgcDriver::Graphics::PipelineStoreTotals()}; +} + +void JournalTests() { + using namespace NarrowConstantStoreFixture; + Require(AgcDriver::Graphics::PipelineJournalEnabled(), "the pipeline journal is disabled although the shader cache directory is set"); + const auto journalPath = AgcDriver::Graphics::PipelineJournalPath(); + Block compute(BlockBytes); + Block target(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 2).Bytes()); + const auto computeCode = ComputeCode(0); + const std::array red{1.0f, 0.0f, 0.0f, 1.0f}; + constexpr std::uint16_t redTexel = 0x801f; + { + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, "the first device is required once the second opened"); + device->WaitForPipelineJournalReplay(); + const auto before = Snapshot(); + Require(before.journal.loadedRecipes == 0 && before.journal.replayedGraphics == 0 && before.journal.replayedCompute == 0, "an empty journal replayed pipelines"); + std::memset(compute.data, Fill, compute.bytes); + const auto words = AddressWords(compute); + const auto result = CompileCompute(*device, computeCode, words, 32); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(computeCode.data())); + device->WaitIdle(); + ExpectConstantStore(compute, "first run: the constant store dispatch"); + std::memset(target.data, 0, target.bytes); + Draw(*device, DecodeTarget(target), Program(red)); + ExpectTexel(*device, target, redTexel, "first run: the draw"); + Require(device->ComputePipelinesCreated() == 1, "the first run did not build exactly one compute pipeline"); + AgcDriver::Graphics::FlushPipelineJournal(); + const auto after = Snapshot(); + Require(after.store.misses == before.store.misses + 1 && after.store.hits == before.store.hits, "the first draw did not miss the pipeline store exactly once"); + Require(after.journal.recordedGraphics == 1 && after.journal.recordedCompute == 1 && after.journal.recordedModules == 3 && after.journal.writeFailures == 0, "the first run did not record one graphics and one compute pipeline with three modules"); + Draw(*device, DecodeTarget(target), Program(red)); + device->WaitIdle(); + AgcDriver::Graphics::FlushPipelineJournal(); + const auto repeated = Snapshot(); + Require(repeated.store.hits == after.store.hits + 1 && repeated.store.misses == after.store.misses, "a repeated draw did not hit the pipeline store"); + Require(repeated.journal.recordedGraphics == 1 && repeated.journal.recordedModules == 3, "a repeated draw recorded its pipeline again"); + } + Require(std::filesystem::exists(journalPath) && std::filesystem::file_size(journalPath) > 0, "the journal file was not written"); + { + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, "the second device could not be opened"); + device->WaitForPipelineJournalReplay(); + const auto replayed = Snapshot(); + Require(replayed.journal.loadedRecipes == 2 && replayed.journal.loadedModules == 3 && replayed.journal.droppedRecords == 0, "the second run did not load the two recorded pipelines and their three modules"); + Require(replayed.journal.replayedGraphics == 1 && replayed.journal.replayedCompute == 1 && replayed.journal.replayRejected == 0 && replayed.journal.replayFailed == 0 && replayed.journal.replayPresent == 0, "the replay did not build one graphics and one compute pipeline"); + Require(device->ComputePipelinesCreated() == 1, "the replay did not build the compute pipeline on the new device"); + std::memset(compute.data, Fill, compute.bytes); + const auto words = AddressWords(compute); + const auto result = CompileCompute(*device, computeCode, words, 32); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(computeCode.data())); + device->WaitIdle(); + ExpectConstantStore(compute, "second run: the constant store dispatch"); + Require(device->ComputePipelinesCreated() == 1, "the second run's dispatch built a compute pipeline the replay had built"); + std::memset(target.data, 0, target.bytes); + Draw(*device, DecodeTarget(target), Program(red)); + ExpectTexel(*device, target, redTexel, "second run: the draw through the replayed pipeline"); + const auto drawn = Snapshot(); + Require(drawn.store.hits == replayed.store.hits + 1 && drawn.store.misses == replayed.store.misses, "the second run's draw did not find the replayed pipeline"); + AgcDriver::Graphics::FlushPipelineJournal(); + const auto final = Snapshot(); + Require(final.journal.recordedGraphics == 1 && final.journal.recordedCompute == 1 && final.journal.recordedModules == 3, "the second run recorded pipelines the journal already held"); + } +} + +// --benchmark [count]: `count` distinct compute programs, dispatched for the first time on three +// devices in turn. The first device starts without the driver's pipeline cache file (a driver with +// an on-disk shader cache of its own, lavapipe's Mesa cache here, still serves from it unless that +// is disabled too, MESA_SHADER_CACHE_DISABLE=true, where the driver's compile dominates); it leaves +// the driver's cache and the journal warm. The second replays the journal before its dispatches. +// The third dispatches with the driver's cache warm but the journal file removed: what a warm run +// pays at each first use without the journal. Wall times only; no pass threshold. +void Benchmark(std::uint32_t count) { + using namespace NarrowConstantStoreFixture; + Require(AgcDriver::Graphics::PipelineJournalEnabled(), "the pipeline journal is disabled"); + Block compute(BlockBytes); + const auto words = AddressWords(compute); + std::vector> codes; + for (std::uint32_t salt = 1; salt <= count; ++salt) codes.push_back(ComputeCode(salt)); + struct Run { + double openMs = 0; + double dispatchMs = 0; + std::uint64_t created = 0; + AgcDriver::Graphics::PipelineJournalCounters journal; + }; + const auto run = [&](const char* what) { + Run measured; + const auto openedAt = std::chrono::steady_clock::now(); + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, std::string("no device for the benchmark's ") + what); + device->WaitForPipelineJournalReplay(); + measured.openMs = std::chrono::duration(std::chrono::steady_clock::now() - openedAt).count(); + measured.journal = AgcDriver::Graphics::PipelineJournalTotals(); + std::vector results; + for (const auto& code : codes) results.push_back(CompileCompute(*device, code, words, 32)); + const auto createdBefore = device->ComputePipelinesCreated(); + const auto started = std::chrono::steady_clock::now(); + for (std::size_t index = 0; index < results.size(); ++index) device->Dispatch(results[index], 1, 1, 1, {}, reinterpret_cast(codes[index].data())); + device->WaitIdle(); + measured.dispatchMs = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); + measured.created = device->ComputePipelinesCreated() - createdBefore; + AgcDriver::Graphics::FlushPipelineJournal(); + return measured; + }; + const auto cold = run("cold run"); + const auto replayed = run("replayed run"); + std::error_code error; + std::filesystem::remove(AgcDriver::Graphics::PipelineJournalPath(), error); + Require(!error, "cannot remove the journal for the benchmark's last run"); + const auto warm = run("warm run without the journal"); + std::printf("pipeline journal benchmark: %u compute programs, %u replay threads\n", count, replayed.journal.replayThreads); + std::printf(" cold driver cache, no journal: %llu pipelines built at their dispatches, %u dispatches in %.1f ms (%.2f ms each)\n", static_cast(cold.created), count, cold.dispatchMs, cold.dispatchMs / count); + std::printf(" warm driver cache, no journal: %llu pipelines built at their dispatches, %u dispatches in %.1f ms (%.2f ms each)\n", static_cast(warm.created), count, warm.dispatchMs, warm.dispatchMs / count); + std::printf(" warm driver cache, journal: %llu pipelines built by the replay in %.1f ms of replay work (%.1f ms from the device opening), then %llu built at their dispatches, %u dispatches in %.1f ms (%.2f ms each)\n", static_cast(replayed.journal.replayedCompute), replayed.journal.replayMs, replayed.openMs, static_cast(replayed.created), count, replayed.dispatchMs, replayed.dispatchMs / count); +} + +} + +int main(int argc, char** argv) { + try { + const auto directory = std::filesystem::temp_directory_path() / ("aps5-pipeline-journal-test-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(directory); + SetEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); + SetEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + SetEnvironment("APS5_NO_PIPELINE_JOURNAL", "0"); + SetEnvironment("APS5_VERIFY_SET_LAYOUTS", "1"); + SetEnvironment("APS5_PIPELINE_JOURNAL_THREADS", "2"); + struct Cleanup { + std::filesystem::path directory; + ~Cleanup() { + std::error_code error; + std::filesystem::remove_all(directory, error); + } + } cleanup{directory}; + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + if (argc >= 2 && std::string(argv[1]) == "--benchmark") { + Benchmark(argc >= 3 ? static_cast(std::strtoul(argv[2], nullptr, 10)) : 24u); + return 0; + } + JournalTests(); + std::puts("pipeline journal record and replay passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp index e4e66f07f3..f2b56411c0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp @@ -268,6 +268,9 @@ int main(int argc, char** argv) { OptimizationDrain drain{context.device}; std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); Run(*device, context, trace, mode == "color32" ? 32u : 64u, masked ? VK_FORMAT_UNDEFINED : (mode == "d16" ? VK_FORMAT_D16_UNORM : VK_FORMAT_D32_SFLOAT), optimize); + if (optimize) Require(AgcDriver::Graphics::PipelineLibraryWorkers() == 1, "the optimized pipeline was not built on the pipeline library worker"); + AgcDriver::Graphics::StopPipelineLibraryWorker(); + Require(AgcDriver::Graphics::PipelineLibraryWorkers() == 0, "stopping the pipeline library left its worker running"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp new file mode 100644 index 0000000000..8cc143e3c5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp @@ -0,0 +1,268 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::DriverDetail::DrawPipeline; + +constexpr std::uint32_t Lanes = 32; +constexpr std::uint32_t BufferBytes = Lanes * 4u; +constexpr std::uint32_t DispatchOpcode = 0x15; + +constexpr std::size_t BlockBytes = 65536; + +std::uint32_t* NewBlock() { +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + if (block == nullptr) throw std::runtime_error("pipelined dispatch: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, false); + return block; +} + +std::uint32_t* Input = nullptr; +std::uint32_t* Output = nullptr; +std::uint32_t* Scratch = nullptr; +std::uint32_t* Table = nullptr; +std::uint32_t* TargetA = nullptr; +std::uint32_t* TargetB = nullptr; + +alignas(256) constexpr std::array Code{0x34020082u, 0xe0301000u, 0x80000201u, 0xbf8c3f70u, 0x4a060481u, 0xe0701000u, 0x80010301u, 0xbf810000u}; + +alignas(256) constexpr std::array ScalarCode{0xf4000200u, 0xfa000000u, 0xbf8cc07fu, 0x34020082u, 0x7e040208u, 0x4a040481u, 0xe0701000u, 0x80010201u, 0xbf810000u}; + +alignas(256) constexpr std::array TableCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x34020082u, 0xe0301000u, 0x80020201u, 0xbf8c3f70u, 0x4a060481u, 0xe0701000u, 0x80010301u, 0xbf810000u}; + +void check(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("pipelined dispatch: " + what); +} + +template +void WaitUntil(Ready ready, const std::string& what) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (!ready()) { + if (std::chrono::steady_clock::now() >= deadline) throw std::runtime_error("pipelined dispatch: timed out waiting for " + what); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +std::array FillPacket(std::uint32_t value, const void* destination, std::uint32_t bytes) { + const auto address = reinterpret_cast(destination); + return {0xc0055000u, 2u << 29u, value, 0u, static_cast(address), static_cast(address >> 32u), bytes}; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::vector Round(const void* destination, std::uint32_t value, const void* code) { + const auto fill = FillPacket(value, destination, BufferBytes); + std::vector words(fill.begin(), fill.end()); + const auto codeAddress = reinterpret_cast(code); + const auto input = BufferDescriptor(Input, BufferBytes); + const auto output = BufferDescriptor(Output, BufferBytes); + const std::vector commands{ + 0xc0027600u, 0x20cu, static_cast(codeAddress >> 8u), static_cast(codeAddress >> 40u), + 0xc0037600u, 0x207u, Lanes, 1u, 1u, + 0xc0017600u, 0x213u, 8u << 1u, + 0xc0087600u, 0x240u, input[0], input[1], input[2], input[3], output[0], output[1], output[2], output[3], + 0xc0031500u, 1u, 1u, 1u, 0x8041u, + }; + words.insert(words.end(), commands.begin(), commands.end()); + return words; +} + +std::vector TableFills(const void* target, std::uint32_t value) { + const auto targetFill = FillPacket(value, target, BufferBytes); + const auto tableFill = FillPacket(static_cast(reinterpret_cast(target)), Table, 4u); + std::vector words(targetFill.begin(), targetFill.end()); + words.insert(words.end(), tableFill.begin(), tableFill.end()); + return words; +} + +std::vector TableDispatch(const void* code) { + const auto codeAddress = reinterpret_cast(code); + const auto tableAddress = reinterpret_cast(Table); + const auto output = BufferDescriptor(Output, BufferBytes); + const std::vector commands{ + 0xc0027600u, 0x20cu, static_cast(codeAddress >> 8u), static_cast(codeAddress >> 40u), + 0xc0037600u, 0x207u, Lanes, 1u, 1u, + 0xc0017600u, 0x213u, 8u << 1u, + 0xc0087600u, 0x240u, static_cast(tableAddress), static_cast(tableAddress >> 32u), 0u, 0u, output[0], output[1], output[2], output[3], + 0xc0031500u, 1u, 1u, 1u, 0x8041u, + }; + return commands; +} + +void Submit(std::vector& words) { + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed"); +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_PIPELINED_DRAWS", "1"); + _putenv_s("APS5_PIPELINE_DMA", "1"); +#else + setenv("APS5_PIPELINED_DRAWS", "1", 1); + setenv("APS5_PIPELINE_DMA", "1", 1); +#endif + try { + { + const auto probe = OpenVulkanTestDevice(); + if (!probe) return VulkanTestSkipped; + } + const char* dispatchSwitch = std::getenv("APS5_PIPELINE_DISPATCH"); + const bool pipelinedDispatch = dispatchSwitch != nullptr && dispatchSwitch[0] != '\0' && dispatchSwitch[0] != '0'; + + Shader shader{}; + shader.file_header = 0x34333231; + shader.version = 0x18; + shader.header_size = sizeof(Shader); + shader.shader_size = sizeof(Code); + shader.code = Code.data(); + AgcDriverRegisterShader_nid_postfix(&shader); + Shader scalarShader{}; + scalarShader.file_header = 0x34333231; + scalarShader.version = 0x18; + scalarShader.header_size = sizeof(Shader); + scalarShader.shader_size = sizeof(ScalarCode); + scalarShader.code = ScalarCode.data(); + AgcDriverRegisterShader_nid_postfix(&scalarShader); + Shader tableShader{}; + tableShader.file_header = 0x34333231; + tableShader.version = 0x18; + tableShader.header_size = sizeof(Shader); + tableShader.shader_size = sizeof(TableCode); + tableShader.code = TableCode.data(); + AgcDriverRegisterShader_nid_postfix(&tableShader); + + Input = NewBlock(); + Output = NewBlock(); + Scratch = NewBlock(); + Table = NewBlock(); + TargetA = NewBlock(); + TargetB = NewBlock(); + check((reinterpret_cast(TargetA) >> 32u) == (reinterpret_cast(TargetB) >> 32u), "the two targets do not share their high address dword"); + const auto initialDescriptor = BufferDescriptor(TargetA, BufferBytes); + std::copy(initialDescriptor.begin(), initialDescriptor.end(), Table); + const std::array values{0x0a0b0c0du, 0x11112222u, 0x33334444u, 0x55556666u, 0x77778888u}; + for (std::size_t round = 0; round < values.size(); ++round) { + const auto value = values[round]; + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode); + auto words = Round(Input, value, Code.data()); + Submit(words); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output), BufferBytes); + const auto drains = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode) - drainsBefore; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + check(Input[lane] == value, "the immediate fill did not land in lane " + std::to_string(lane)); + check(Output[lane] == value + 1u, "round " + std::to_string(round) + ": lane " + std::to_string(lane) + " of the output is " + std::to_string(Output[lane]) + ", expected " + std::to_string(value + 1u) + " (the dispatch did not see the fill before it)"); + } + std::printf("pipelined dispatch: input fill round %zu, fill 0x%08x, dispatch drains %llu\n", round, value, static_cast(drains)); + if (!pipelinedDispatch) check(drains >= 1, "a dispatch did not drain the pipeline, and APS5_PIPELINE_DISPATCH is off"); + if (pipelinedDispatch) check(drains <= 1, "a dispatch drained the pipeline more than once (" + std::to_string(drains) + ")"); + } + for (std::size_t round = 0; round < 3; ++round) { + const auto value = 0xc0de0000u + static_cast(round); + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode); + auto words = Round(Scratch, value, Code.data()); + Submit(words); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Scratch), BufferBytes); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output), BufferBytes); + const auto drains = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode) - drainsBefore; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + check(Scratch[lane] == value, "the scratch fill did not land in lane " + std::to_string(lane)); + check(Output[lane] == Input[lane] + 1u, "scratch round " + std::to_string(round) + ": lane " + std::to_string(lane) + " of the output is " + std::to_string(Output[lane]) + ", expected " + std::to_string(Input[lane] + 1u)); + } + std::printf("pipelined dispatch: scratch fill round %zu, fill 0x%08x, dispatch drains %llu\n", round, value, static_cast(drains)); + if (!pipelinedDispatch) check(drains >= 1, "a dispatch did not drain the pipeline, and APS5_PIPELINE_DISPATCH is off"); + if (pipelinedDispatch) check(drains == 0, "a dispatch that reads nothing pending drained the pipeline (" + std::to_string(drains) + ")"); + } + const std::array tableValues{0x6a6a0001u, 0x6b6b0002u, 0x6c6c0003u, 0x6d6d0004u}; + for (std::size_t round = 0; round < tableValues.size(); ++round) { + auto* target = round % 2 == 0 ? TargetA : TargetB; + const auto value = tableValues[round]; + std::atomic released{false}; + struct Release { + std::atomic& flag; + ~Release() { flag.store(true, std::memory_order_release); } + } release{released}; + WaitUntil([] { return DrawPipeline::QueuedItems() == 0; }, "the pipeline to empty before descriptor round " + std::to_string(round)); + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode); + DrawPipeline::Queue0().Enqueue([&released] { + while (!released.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }, {}); + auto fills = TableFills(target, value); + Submit(fills); + WaitUntil([] { return DrawPipeline::QueuedItems() >= 4; }, "the target and table fills queued behind the gate"); + auto dispatch = TableDispatch(TableCode.data()); + Submit(dispatch); + WaitUntil([&drainsBefore] { return DrawPipeline::QueuedItems() >= 5 || DrawPipeline::DrainRequestsByOpcode(DispatchOpcode) > drainsBefore; }, "the dispatch handled while the table fill is pending"); + released.store(true, std::memory_order_release); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output), BufferBytes); + const auto drains = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode) - drainsBefore; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + check(Output[lane] == value + 1u, "descriptor round " + std::to_string(round) + ": lane " + std::to_string(lane) + " of the output is " + std::to_string(Output[lane]) + ", expected " + std::to_string(value + 1u) + " (the dispatch read the buffer the table named before the pending fill of the table)"); + } + std::printf("pipelined dispatch: descriptor round %zu, fill 0x%08x, dispatch drains %llu\n", round, value, static_cast(drains)); + if (!pipelinedDispatch) check(drains >= 1, "a dispatch did not drain the pipeline, and APS5_PIPELINE_DISPATCH is off"); + if (pipelinedDispatch) check(drains == 1, "a dispatch whose table entry is pending a write drained " + std::to_string(drains) + " times, expected once"); + } + const std::array scalarValues{0x13572468u, 0x2468ace0u, 0x0f1e2d3cu, 0x9abcdef0u, 0x31415926u}; + for (std::size_t round = 0; round < scalarValues.size(); ++round) { + const auto value = scalarValues[round]; + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode); + auto words = Round(Input, value, ScalarCode.data()); + Submit(words); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output), BufferBytes); + const auto drains = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode) - drainsBefore; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + check(Output[lane] == value + 1u, "scalar round " + std::to_string(round) + ": lane " + std::to_string(lane) + " of the output is " + std::to_string(Output[lane]) + ", expected " + std::to_string(value + 1u) + " (a stale input)"); + } + std::printf("pipelined dispatch: scalar read round %zu, fill 0x%08x, dispatch drains %llu\n", round, value, static_cast(drains)); + if (!pipelinedDispatch) check(drains >= 1, "a dispatch did not drain the pipeline, and APS5_PIPELINE_DISPATCH is off"); + if (pipelinedDispatch) check(drains <= 1, "a dispatch drained the pipeline more than once (" + std::to_string(drains) + ")"); + } + LibcRunShutdown_nid_postfix(); + std::puts("pipelined dispatch tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { LibcRunShutdown_nid_postfix(); } catch (...) {} + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp new file mode 100644 index 0000000000..d43e135c35 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp @@ -0,0 +1,177 @@ +#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/Submit/include/Dcb.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::DriverDetail::DrawPipeline; + +void check(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("pipelined DMA: " + what); +} + +std::array DmaData(bool immediate, std::uint64_t source, std::uint32_t data, std::uint64_t destination, std::uint32_t bytes) { + return {0xc0055000u, immediate ? (2u << 29u) : 0u, immediate ? data : static_cast(source), immediate ? 0u : static_cast(source >> 32u), static_cast(destination), static_cast(destination >> 32u), bytes}; +} + +std::array DmaSelect(std::uint32_t destinationSelect, std::uint64_t source, std::uint64_t destination, std::uint32_t bytes) { + return {0xc0055000u, destinationSelect << 20u, static_cast(source), static_cast(source >> 32u), static_cast(destination), static_cast(destination >> 32u), bytes}; +} + +template +void Append(std::vector& words, const std::array& packet) { + words.insert(words.end(), packet.begin(), packet.end()); +} + +void Submit(std::vector& words) { + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed"); +} + +struct Gate { + std::atomic release{false}; + ~Gate() { release.store(true, std::memory_order_release); } +}; + +void Hold(Gate& gate, std::uint64_t address, std::size_t bytes) { + DrawPipeline::Queue0().Enqueue([&gate] { + while (!gate.release.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }, {{address, address + bytes}}); +} + +void ExpectDrains(std::vector& words, std::uint32_t opcode, Gate& gate, const char* what) { + const auto before = DrawPipeline::DrainRequestsByOpcode(opcode); + std::thread releaser([&] { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (DrawPipeline::DrainRequestsByOpcode(opcode) == before && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + gate.release.store(true, std::memory_order_release); + }); + Submit(words); + releaser.join(); + check(DrawPipeline::DrainRequestsByOpcode(opcode) == before + 1, what); +} + +void ExpectNoDrain(std::uint32_t opcode, std::uint64_t before, const char* what) { + check(DrawPipeline::DrainRequestsByOpcode(opcode) == before, what); +} + +} + +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 { + alignas(256) static std::array first{}; + alignas(256) static std::array second{}; + alignas(256) static std::array pending{}; + alignas(256) static std::array elsewhere{}; + first.fill(0); + second.fill(0); + pending.fill(0); + elsewhere.fill(0); + using namespace AgcDriver::DriverDetail; + const auto drainedBefore = storesDrained.load(); + const auto cpuBefore = storesOnCpu.load() + storesOnGpu.load() + storesBehindCompletions.load(); + std::vector words; + const auto fill = DmaData(true, 0, 0x5eed1234u, reinterpret_cast(first.data()), static_cast(sizeof(first))); + const auto copy = DmaData(false, reinterpret_cast(first.data()), 0, reinterpret_cast(second.data()), 512); + words.insert(words.end(), fill.begin(), fill.end()); + words.insert(words.end(), copy.begin(), copy.end()); + Submit(words); + AgcDriverWaitIdle_nid_postfix(); + for (const auto word : first) check(word == 0x5eed1234u, "the immediate fill did not land"); + for (std::size_t i = 0; i < second.size(); ++i) check(second[i] == (i < 128 ? 0x5eed1234u : 0u), "the copy did not see the fill before it, or wrote past its bytes"); + const auto stores = storesOnCpu.load() + storesOnGpu.load() + storesBehindCompletions.load() - cpuBefore; + check(stores == 2, "the DMA_DATA packets were not committed (" + std::to_string(stores) + " committed stores)"); + check(storesDrained.load() == drainedBefore, "a DMA_DATA packet still drained"); + + const auto source = reinterpret_cast(first.data()); + const auto held = reinterpret_cast(pending.data()); + const auto other = reinterpret_cast(elsewhere.data()); + { + Gate gate; + Hold(gate, held, sizeof(pending)); + const auto dmaBefore = DrawPipeline::DrainRequestsByOpcode(0x50); + const auto predicationBefore = DrawPipeline::DrainRequestsByOpcode(0x20); + const auto nopBefore = DrawPipeline::DrainRequestsByOpcode(0x10); + words.clear(); + Append(words, DmaSelect(2, source, 0, 64)); + Append(words, DmaData(false, source, 0, other, 0)); + Append(words, DmaSelect(1, source, 0, 4)); + Append(words, DmaData(false, source, 0, other, 2)); + Append(words, std::array{0xc0022000u, 0x31100u, static_cast(held), static_cast(held >> 32u)}); + Append(words, std::array{0xc000102cu, 0x00636261u}); + Append(words, std::array{0xc0001030u, 0u}); + Submit(words); + ExpectNoDrain(0x50, dmaBefore, "a prefetch, a zero-byte copy, a GDS copy or an unaligned copy off the pending range drained"); + ExpectNoDrain(0x20, predicationBefore, "SET_PREDICATION drained the pipeline"); + ExpectNoDrain(0x10, nopBefore, "a marker push or pop drained the pipeline"); + } + AgcDriverWaitIdle_nid_postfix(); + + { + Gate gate; + Hold(gate, held, sizeof(pending)); + words.clear(); + Append(words, DmaData(false, source, 0, held, 2)); + ExpectDrains(words, 0x50, gate, "a copy onto an uncommitted pipeline write did not drain"); + } + AgcDriverWaitIdle_nid_postfix(); + + { + Gate gate; + Hold(gate, held, sizeof(pending)); + words.clear(); + Append(words, DmaData(false, held, 0, other, 2)); + ExpectDrains(words, 0x50, gate, "an unaligned copy of an uncommitted pipeline write did not drain"); + } + AgcDriverWaitIdle_nid_postfix(); + + { + Gate gate; + Hold(gate, held, sizeof(pending)); + words.clear(); + Append(words, std::array{0xc0022000u, 0x31100u, static_cast(held), static_cast(held >> 32u)}); + Append(words, std::array{0xc0001301u, 32u}); + ExpectDrains(words, 0x13, gate, "a predicated packet whose predicate overlaps an uncommitted write did not drain"); + } + AgcDriverWaitIdle_nid_postfix(); + + { + Gate gate; + Hold(gate, held, sizeof(pending)); + const auto resetBefore = DrawPipeline::DrainRequestsByOpcode(0x10); + words.clear(); + Append(words, std::array{0xc0001024u, 0u}); + Submit(words); + ExpectNoDrain(0x10, resetBefore, "DISPATCH_RESET drained the pipeline"); + } + + LibcRunShutdown_nid_postfix(); + std::puts("pipelined DMA 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/PipelinedFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp new file mode 100644 index 0000000000..c501698a57 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp @@ -0,0 +1,271 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferFill.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using namespace AgcDriver::DriverDetail; +alignas(256) constexpr auto Code = PatternFillKernel; +alignas(256) std::array Pattern{}; +alignas(256) std::array Control{}; +alignas(256) std::array Output{}; +alignas(256) std::array Labels{}; +alignas(256) std::array ScratchPattern{0x0f0f0f0fu, 0, 0, 0}; +alignas(256) std::array ScratchControl{256, 0, 0, 0}; +alignas(256) std::array Scratch{}; + +std::array UserData(const void* pattern, const void* control, void* output) { + std::array data{}; + const auto descriptor = [&](std::size_t first, const void* pointer, std::uint32_t stride, std::uint32_t count, std::uint32_t format) { + const auto address = reinterpret_cast(pointer); + data[first] = static_cast(address); + data[first + 1] = static_cast(address >> 32u) | (stride << 16u); + data[first + 2] = count; + data[first + 3] = format; + }; + descriptor(0, pattern, 4, 4, 0x14004u); + descriptor(4, output, 4, 256, 0x14004u); + descriptor(8, control, 16, 1, 0x4dfacu); + return data; +} + +void AppendFill(std::vector& words, const void* pattern, const void* control, void* output) { + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{64, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(Code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{(12u << 1u) | (1u << 7u)}; + registers(0x213, resource); + registers(0x240, UserData(pattern, control, output)); + words.insert(words.end(), {0xc0031500u, 4, 1, 1, 0x41u | 0x8000u}); +} + +void AppendScratchFill(std::vector& words) { + AppendFill(words, ScratchPattern.data(), ScratchControl.data(), Scratch.data()); +} + +void AppendOutputFill(std::vector& words) { + AppendFill(words, Pattern.data(), Control.data(), Output.data()); +} + +void AppendLabel(std::vector& words, std::uint32_t* target, std::uint32_t value) { + const auto address = reinterpret_cast(target); + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(address), static_cast(address >> 32u), value, 0, 0}); +} + +struct Blocker { + struct State { + std::atomic release{false}; + std::atomic done{false}; + }; + std::shared_ptr state = std::make_shared(); + std::uint32_t* target = nullptr; + ~Blocker() { Release(); } + void Release() { state->release = true; } + void Enqueue(std::vector writes = {}) { + DrawPipeline::Queue0().Enqueue([state = state, target = target] { + while (!state->release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (target != nullptr) std::atomic_ref(*target).store(1); + { std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); } + state->done = true; + }, std::move(writes)); + } + void Join() const { + const auto start = std::chrono::steady_clock::now(); + while (!state->done.load()) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(20), "the blocker never ran"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } +}; + +void SubmitBehind(Blocker& blocker, std::vector& words) { + std::unique_lock gpuLock(AgcDriver::GuestMemory::GpuMutex()); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + blocker.Enqueue(); +} + +void ReleaseWhen(Blocker& blocker, std::size_t reached, std::size_t passed) { + const auto start = std::chrono::steady_clock::now(); + auto reachedAt = std::chrono::steady_clock::time_point{}; + for (;;) { + const auto outstanding = DrawPipeline::Queue0().Outstanding(); + const auto now = std::chrono::steady_clock::now(); + if (outstanding >= passed) break; + if (outstanding >= reached && reachedAt == std::chrono::steady_clock::time_point{}) reachedAt = now; + if (reachedAt != std::chrono::steady_clock::time_point{} && now - reachedAt > std::chrono::seconds(2)) break; + Require(now - start < std::chrono::seconds(20), "the queue worker never reached the fill"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + blocker.Release(); +} + +void Finish() { + auto idle = std::async(std::launch::async, [] { AgcDriverWaitIdle_nid_postfix(); }); + if (idle.wait_for(std::chrono::seconds(20)) != std::future_status::ready) { + std::fputs("pipelined fill: the queue worker and the draw pipeline deadlocked\n", stderr); + std::fflush(stderr); + std::_Exit(1); + } + idle.get(); + const auto start = std::chrono::steady_clock::now(); + while (DrawPipeline::Queue0().Outstanding() != 0) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(20), "the draw pipeline kept an item after the queue went idle"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Scratch.data()), sizeof(Scratch)); +} + +void InputsAfterQueuedLabels() { + Output.fill(0xcafebabeu); + Pattern = {0x40404040u, 0, 0, 0}; + Control = {256, 0, 0, 0}; + std::vector words; + AppendScratchFill(words); + AppendLabel(words, &Control[0], 128); + AppendLabel(words, &Pattern[0], 0xababababu); + AppendOutputFill(words); + Blocker blocker; + SubmitBehind(blocker, words); + ReleaseWhen(blocker, 4, 5); + Finish(); + blocker.Join(); + for (std::size_t i = 0; i < Output.size(); ++i) { + const auto expected = i < 128 ? 0xababababu : 0xcafebabeu; + Require(Output[i] == expected, "a fill read its control or pattern before the queued labels that write them, at " + std::to_string(i)); + } +} + +void FillIntoFullPipeline() { + Output.fill(0xcafebabeu); + Pattern = {0x5a5a5a5au, 0, 0, 0}; + Control = {256, 0, 0, 0}; + Labels.fill(0); + const auto depth = DrawPipeline::Depth(); + Require(depth >= 3 && depth - 2 <= Labels.size(), "unexpected draw pipeline depth " + std::to_string(depth)); + std::vector words; + AppendScratchFill(words); + for (std::size_t i = 0; i + 2 < depth; ++i) AppendLabel(words, &Labels[i], static_cast(i + 1)); + AppendOutputFill(words); + Blocker blocker; + SubmitBehind(blocker, words); + ReleaseWhen(blocker, depth, depth + 1); + Finish(); + blocker.Join(); + for (std::size_t i = 0; i + 2 < depth; ++i) Require(Labels[i] == i + 1, "a queued label was not stored"); + for (std::size_t i = 0; i < Output.size(); ++i) Require(Output[i] == (i < 256 ? 0x5a5a5a5au : 0xcafebabeu), "a fill behind a full draw pipeline did not land at " + std::to_string(i)); +} + +void SatisfiedWaitKeepsUnrelatedQueuedWork() { + alignas(8) static std::uint32_t gate = 1; + alignas(8) static std::uint32_t marker = 0; + marker = 0; + + Blocker blocker; + blocker.Enqueue(); + + const auto address = reinterpret_cast(&gate); + std::vector words{ + 0xc0053c00u, 0x13u, static_cast(address), static_cast(address >> 32u), + 1u, 0xffffffffu, 0x19u}; + AppendLabel(words, &marker, 1); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (DrawPipeline::Queue0().Outstanding() < 2 && std::chrono::steady_clock::now() < deadline) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const bool keptQueued = DrawPipeline::Queue0().Outstanding() >= 2; + blocker.Release(); + Finish(); + blocker.Join(); + Require(keptQueued, "a satisfied WAIT_REG_MEM drained unrelated queued work"); + Require(marker == 1, "a label after a satisfied WAIT_REG_MEM did not execute"); +} + +void SatisfiedWaitDrainsOverlappingQueuedWork() { + alignas(8) static std::uint32_t gate = 1; + alignas(8) static std::uint32_t marker = 0; + std::atomic_ref(gate).store(1); + marker = 0; + + Blocker blocker; + blocker.target = &gate; + const auto address = reinterpret_cast(&gate); + blocker.Enqueue({{address, address + sizeof(gate)}}); + + std::vector words{ + 0xc0053c00u, 0x13u, static_cast(address), static_cast(address >> 32u), + 1u, 0xffffffffu, 0x19u}; + AppendLabel(words, &marker, 1); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(0x3c); + auto submitted = std::async(std::launch::async, [&] { AgcDriver::Submit(&packet, 0); }); + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (DrawPipeline::DrainRequestsByOpcode(0x3c) == drainsBefore && + DrawPipeline::Queue0().Outstanding() < 2 && std::chrono::steady_clock::now() < deadline) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const bool drained = DrawPipeline::DrainRequestsByOpcode(0x3c) > drainsBefore; + const bool labelQueuedEarly = DrawPipeline::Queue0().Outstanding() >= 2; + blocker.Release(); + Require(submitted.wait_for(std::chrono::seconds(20)) == std::future_status::ready, + "WAIT_REG_MEM submission did not finish after the overlapping write"); + submitted.get(); + Finish(); + blocker.Join(); + Require(drained && !labelQueuedEarly, "a satisfied WAIT_REG_MEM skipped an overlapping queued write"); + Require(marker == 1, "a label after an overlapping WAIT_REG_MEM did not execute"); +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_PIPELINED_DRAWS", "1"); +#else + setenv("APS5_PIPELINED_DRAWS", "1", 1); +#endif + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + InputsAfterQueuedLabels(); + FillIntoFullPipeline(); + SatisfiedWaitKeepsUnrelatedQueuedWork(); + SatisfiedWaitDrainsOverlappingQueuedWork(); + AgcDriverShutdown_nid_postfix(); + std::puts("pipelined fill tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedIndirectDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedIndirectDraw.cpp new file mode 100644 index 0000000000..93abe602a5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedIndirectDraw.cpp @@ -0,0 +1,276 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::DrawPipeline; + +constexpr std::byte Kept{0x40}; +constexpr std::uint32_t BaseVertexLocation = 0x8c; +constexpr std::uint32_t Clipped = 0x40000000u; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array DirectMemory{}; +alignas(256) std::array FirstMemory{}; +alignas(256) std::array SecondMemory{}; +alignas(256) std::array WrittenMemory{}; +alignas(256) std::array Records{3, 1, 0, 0}; +alignas(256) std::array LateRecords{0, 1, 0, 0}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +void AppendState(std::vector& commands) { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0x01200000}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + Append(commands, 0x79, {0x242, 4}); + Append(commands, 0x79, {0x24a, 0}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); +} + +void AppendTarget(std::vector& commands, std::array& memory, std::uint32_t userWord) { + const auto color = reinterpret_cast(memory.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + Append(commands, 0x76, {BaseVertexLocation, userWord}); +} + +void AppendRecords(std::vector& commands, const void* records) { + const auto address = reinterpret_cast(records); + Append(commands, 0x11, {1, static_cast(address), static_cast(address >> 32u)}); +} + +void AppendIndirect(std::vector& commands) { + Append(commands, 0x24, {0, BaseVertexLocation, 0, 2}); +} + +void AppendLabel(std::vector& commands, std::uint32_t* target, std::uint32_t value) { + const auto address = reinterpret_cast(target); + commands.insert(commands.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(address), static_cast(address >> 32u), value, 0, 0}); +} + +std::size_t Covered(const std::array& memory) { + return static_cast(std::count_if(memory.begin(), memory.end(), [](std::byte value) { return value != Kept; })); +} + +void SameAsDirect(const std::array& memory, const std::string& what) { + for (std::size_t index = 0; index < DirectMemory.size(); ++index) { + Require(memory[index] == DirectMemory[index], what + ": byte " + std::to_string(index) + " is " + std::to_string(static_cast(memory[index])) + ", the direct draw wrote " + std::to_string(static_cast(DirectMemory[index]))); + } +} + +void Flush(const std::array& memory) { + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(memory.data()), memory.size()); +} + +struct Blocker { + struct State { + std::atomic release{false}; + std::atomic done{false}; + }; + std::shared_ptr state = std::make_shared(); + void Release() { state->release = true; } + void Enqueue() { + DrawPipeline::Queue0().Enqueue([state = state] { + while (!state->release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + { std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); } + state->done = true; + }, {}); + } + void Join() const { + const auto start = std::chrono::steady_clock::now(); + while (!state->done.load()) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(20), "the blocker never ran"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } +}; + +void SubmitBehind(Blocker& blocker, std::vector& commands, std::size_t queuedBefore) { + std::unique_lock gpuLock(AgcDriver::GuestMemory::GpuMutex()); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + const auto start = std::chrono::steady_clock::now(); + while (DrawPipeline::Queue0().Outstanding() < queuedBefore && std::chrono::steady_clock::now() - start < std::chrono::seconds(60)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + blocker.Enqueue(); +} + +void Finish() { + auto idle = std::async(std::launch::async, [] { AgcDriverWaitIdle_nid_postfix(); }); + if (idle.wait_for(std::chrono::seconds(20)) != std::future_status::ready) { + std::fputs("pipelined indirect draw: the queue worker and the draw pipeline deadlocked\n", stderr); + std::fflush(stderr); + std::_Exit(1); + } + idle.get(); +} + +void DrawDirectReference() { + DirectMemory.fill(Kept); + std::vector commands; + AppendState(commands); + AppendTarget(commands, DirectMemory, 0); + Append(commands, 0x2d, {3, 2}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the direct draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + Flush(DirectMemory); + const auto direct = Covered(DirectMemory); + Require(direct != 0 && direct != DirectMemory.size(), "the direct draw covered " + std::to_string(direct) + " of " + std::to_string(DirectMemory.size()) + " bytes, expected an edge through the target"); +} + +void IndirectDrawsQueueBehindTheCommitter() { + FirstMemory.fill(Kept); + SecondMemory.fill(Kept); + std::vector commands; + AppendState(commands); + AppendRecords(commands, Records.data()); + AppendTarget(commands, FirstMemory, Clipped); + AppendIndirect(commands); + AppendTarget(commands, SecondMemory, Clipped); + AppendIndirect(commands); + Blocker blocker; + SubmitBehind(blocker, commands, 3); + const auto start = std::chrono::steady_clock::now(); + while (DrawPipeline::Queue0().Outstanding() < 3) { + if (std::chrono::steady_clock::now() - start > std::chrono::seconds(10)) { + const auto outstanding = DrawPipeline::Queue0().Outstanding(); + blocker.Release(); + Finish(); + Require(false, "the indirect draws drained the pipeline instead of queuing behind it (" + std::to_string(outstanding) + " items outstanding, expected the blocker and two draws)"); + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + blocker.Release(); + Finish(); + blocker.Join(); + Flush(FirstMemory); + Flush(SecondMemory); + SameAsDirect(FirstMemory, "the first queued indirect draw"); + SameAsDirect(SecondMemory, "the second queued indirect draw"); +} + +void IndirectDrawWaitsForItsQueuedRecordWrite() { + WrittenMemory.fill(Kept); + LateRecords = {0, 1, 0, 0}; + std::vector commands; + AppendState(commands); + AppendLabel(commands, &LateRecords[0], 3); + AppendRecords(commands, LateRecords.data()); + AppendTarget(commands, WrittenMemory, Clipped); + AppendIndirect(commands); + Blocker blocker; + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(0x100); + SubmitBehind(blocker, commands, 1); + const auto start = std::chrono::steady_clock::now(); + while (DrawPipeline::DrainRequestsByOpcode(0x100) == drainsBefore && std::chrono::steady_clock::now() - start < std::chrono::seconds(10)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + blocker.Release(); + Finish(); + blocker.Join(); + Flush(WrittenMemory); + Require(LateRecords[0] == 3, "the queued label did not store the record's vertex count"); + SameAsDirect(WrittenMemory, "the indirect draw whose record a queued label wrote"); +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_PIPELINED_DRAWS", "1"); +#else + setenv("APS5_PIPELINED_DRAWS", "1", 1); +#endif + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + DrawDirectReference(); + IndirectDrawsQueueBehindTheCommitter(); + IndirectDrawWaitsForItsQueuedRecordWrite(); + AgcDriverShutdown_nid_postfix(); + std::puts("pipelined indirect draw tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedPredicatedLabelOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedPredicatedLabelOrder.cpp new file mode 100644 index 0000000000..3f29450f0e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedPredicatedLabelOrder.cpp @@ -0,0 +1,55 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void check(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("pipelined predicated label order: " + what); +} + +alignas(256) std::array Predicate{}; +alignas(256) std::array Target{}; + +void WriteData(std::vector& words, const void* destination, std::uint32_t value, bool predicated) { + const auto pointer = reinterpret_cast(destination); + words.insert(words.end(), {0xc0033700u | (predicated ? 1u : 0u), 0x00100200u, static_cast(pointer), static_cast(pointer >> 32u), value}); +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_PIPELINED_DRAWS", "1"); +#else + setenv("APS5_PIPELINED_DRAWS", "1", 1); +#endif + try { + Predicate = {1, 0, 0, 0}; + Target = {0, 0, 0, 0}; + const auto predicate = reinterpret_cast(Predicate.data()); + std::vector words; + words.insert(words.end(), {0xc0022000u, (4u << 16u) | 0x100u, static_cast(predicate), static_cast(predicate >> 32u)}); + WriteData(words, Target.data(), 1, true); + WriteData(words, Target.data(), 2, false); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed"); + AgcDriverWaitIdle_nid_postfix(); + check(Target[0] == 2, "a predicated WRITE_DATA landed after the unpredicated WRITE_DATA that follows it (target " + std::to_string(Target[0]) + ", expected 2)"); + LibcRunShutdown_nid_postfix(); + std::puts("pipelined predicated label order test 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/PipelinedPredicationRead.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedPredicationRead.cpp new file mode 100644 index 0000000000..5b400d5036 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedPredicationRead.cpp @@ -0,0 +1,106 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array Code{0xd7460000u, 0x04010c04u, 0x7e020280u, 0xe0102000u, 0x80000100u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +alignas(256) std::array Output{}; +alignas(256) std::array Predicate{}; + +void Registers(std::vector& words, std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); +} + +void SetPredication(std::vector& words, const void* address, bool executeWhenSet) { + const auto pointer = reinterpret_cast(address); + words.insert(words.end(), {0xc0022000u, (4u << 16u) | (executeWhenSet ? 0x100u : 0u), static_cast(pointer), static_cast(pointer >> 32u)}); +} + +void ClearPredication(std::vector& words) { + words.insert(words.end(), {0xc0022000u, 0, 0, 0}); +} + +void WritePredicate(std::vector& words, std::uint32_t value) { + const auto pointer = reinterpret_cast(Predicate.data()); + words.insert(words.end(), {0xc0033700u, 0x00100200u, static_cast(pointer), static_cast(pointer >> 32u), value}); +} + +void PredicatedFill(std::vector& words) { + const std::array shape{64, 1, 1}; + Registers(words, 0x207, shape); + const auto address = reinterpret_cast(Code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + Registers(words, 0x20c, program); + const std::array resource{(4u << 1u) | (1u << 7u)}; + Registers(words, 0x213, resource); + const auto output = reinterpret_cast(Output.data()); + const std::array descriptor{static_cast(output), static_cast((output >> 32u) & 0xffffu) | (4u << 16u), static_cast(Output.size()), 0x00014204u}; + Registers(words, 0x240, descriptor); + words.insert(words.end(), {0xc0031501u, 1, 1, 1, 0x41u}); +} + +void SubmitBehindCommits(std::vector& words) { + std::unique_lock gpuLock(AgcDriver::GuestMemory::GpuMutex()); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); +} + +void Run(std::uint32_t before, std::uint32_t written, bool expectFill) { + Output.fill(Untouched); + Predicate = {before, 0, 0, 0}; + std::vector words; + SetPredication(words, Predicate.data(), true); + WritePredicate(words, written); + PredicatedFill(words); + ClearPredication(words); + SubmitBehindCommits(words); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); + Require(Predicate[0] == written, "the WRITE_DATA to the predicate did not land"); + for (std::size_t i = 0; i < Output.size(); ++i) { + const auto expected = expectFill ? 0u : Untouched; + Require(Output[i] == expected, std::string("a predicated dispatch read a predicate that a queued WRITE_DATA had not yet stored (predicate ") + std::to_string(before) + " -> " + std::to_string(written) + ", word " + std::to_string(i) + ")"); + } +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_PIPELINED_DRAWS", "1"); +#else + setenv("APS5_PIPELINED_DRAWS", "1", 1); +#endif + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + Run(0, 1, true); + Run(1, 0, false); + AgcDriverShutdown_nid_postfix(); + std::puts("pipelined predication read 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/PipelinedPrefetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedPrefetch.cpp new file mode 100644 index 0000000000..cf3574966c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedPrefetch.cpp @@ -0,0 +1,121 @@ +#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/Shutdown.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint32_t DmaDataOpcode = 0x50; + +using AgcDriver::DriverDetail::DrawPipeline; + +void check(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("pipelined prefetch: " + what); +} + +alignas(256) std::array Source{}; +alignas(256) std::array Destination{}; +alignas(64) std::array Label{}; + +std::array DmaData(std::uint32_t destinationSelect, std::uint64_t source, std::uint64_t destination, std::uint32_t bytes) { + return {0xc0055000u, destinationSelect << 20u, static_cast(source), static_cast(source >> 32u), static_cast(destination), static_cast(destination >> 32u), bytes}; +} + +void AppendLabel(std::vector& words, std::uint32_t value) { + const auto address = reinterpret_cast(Label.data()); + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(address), static_cast(address >> 32u), value, 0, 0}); +} + +struct Blocker { + std::atomic release{false}; + void Enqueue() { + DrawPipeline::Queue0().Enqueue([this] { + while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }, {}); + } +}; + +void Finish() { + auto idle = std::async(std::launch::async, [] { AgcDriverWaitIdle_nid_postfix(); }); + if (idle.wait_for(std::chrono::seconds(20)) != std::future_status::ready) { + std::fputs("pipelined prefetch: the queue worker and the draw pipeline deadlocked\n", stderr); + std::fflush(stderr); + std::_Exit(1); + } + idle.get(); +} + +struct Behind { + std::size_t outstanding; + std::uint64_t drains; +}; + +Behind SubmitBehindBusyPipeline(const std::array& dma, std::uint32_t labelValue, bool expectQueued) { + Label.fill(0); + const auto before = DrawPipeline::DrainRequestsByOpcode(DmaDataOpcode); + Blocker blocker; + blocker.Enqueue(); + std::vector words(dma.begin(), dma.end()); + AppendLabel(words, labelValue); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed"); + const auto start = std::chrono::steady_clock::now(); + std::size_t outstanding = 0; + for (;;) { + outstanding = DrawPipeline::Queue0().Outstanding(); + const auto waited = std::chrono::steady_clock::now() - start; + if (expectQueued ? outstanding >= 2 : waited > std::chrono::milliseconds(1500)) break; + if (waited > std::chrono::seconds(10)) break; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + blocker.release = true; + Finish(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Label.data()), sizeof(Label)); + check(Label[0] == labelValue, "the label behind the DMA_DATA did not land"); + return {outstanding, DrawPipeline::DrainRequestsByOpcode(DmaDataOpcode) - before}; +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_PIPELINED_DRAWS", "1"); +#else + setenv("APS5_PIPELINED_DRAWS", "1", 1); +#endif + try { + for (std::size_t i = 0; i < Source.size(); ++i) Source[i] = static_cast(i * 2654435761u); + Destination.fill(0); + const auto source = reinterpret_cast(Source.data()); + const auto destination = reinterpret_cast(Destination.data()); + const auto prefetch = DmaData(2u, source, 0, static_cast(sizeof(Source))); + const auto copy = DmaData(0u, source, destination, 512); + const auto prefetched = SubmitBehindBusyPipeline(prefetch, 0x11111111u, true); + check(prefetched.outstanding >= 2, "a DMA_DATA prefetch drained the draw pipeline: the label behind it never queued behind the held commit (" + std::to_string(prefetched.outstanding) + " outstanding)"); + check(prefetched.drains == 0, "a DMA_DATA prefetch drained the draw pipeline (" + std::to_string(prefetched.drains) + " packet drains)"); + const auto copied = SubmitBehindBusyPipeline(copy, 0x22222222u, false); + check(copied.outstanding == 1, "a DMA_DATA copy did not hold the queue worker (" + std::to_string(copied.outstanding) + " outstanding)"); + check(copied.drains == 1, "a DMA_DATA copy did not drain the draw pipeline (" + std::to_string(copied.drains) + " packet drains)"); + for (std::size_t i = 0; i < Destination.size(); ++i) check(Destination[i] == (i < 128 ? Source[i] : 0u), "the copy did not land, or wrote past its bytes"); + LibcRunShutdown_nid_postfix(); + std::puts("a DMA_DATA prefetch keeps the draw pipeline, a copy drains it"); + 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..f364c83039 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp @@ -35,6 +35,10 @@ alignas(256) constexpr std::array VertexValuePixelCode{ 0xc80a0000, 0xc80e0001, 0xc8120002, 0xf800180f, 0x04040302, 0xbf810000, }; +alignas(256) constexpr std::array FlatBarycentricPixelCode{ + 0xc8120002, 0xf800180f, 0x04040100, 0xbf810000, +}; + alignas(256) constexpr std::array InterpolatedPixelCode{ 0xc8080000, 0xc8090001, 0xf800180f, 0x02020202, 0xbf810000, }; @@ -53,16 +57,31 @@ std::array, 3> ClipPositions() { return positions; } +constexpr std::array StripCorners{{{-0.8f, -0.8f, 1.0f}, {0.8f, -0.8f, 1.0f}, {-0.8f, 0.8f, 1.0f}, {0.8f, 0.8f, 1.0f}}}; + +std::array, 4> StripPositions() { + std::array, 4> positions{}; + for (std::size_t i = 0; i < StripCorners.size(); ++i) positions[i] = {StripCorners[i].x, StripCorners[i].y, 0.5f, 1.0f}; + return positions; +} + 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::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, bool flat = false, VkProvokingVertexModeEXT provoking = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, bool strip = false) { Pixels.fill(Unwritten); auto target = device.Target(); target.fragmentShaderBarycentricEnabled = false; - static const auto positions = ClipPositions(); + static const auto trianglePositions = ClipPositions(); + static const auto stripPositions = StripPositions(); + const auto positions = strip ? std::span>(stripPositions) : std::span>(trianglePositions); std::vector vertexUserData(4, 0u); const auto vertexBuffer = BufferDescriptor(positions.data(), 16u, static_cast(positions.size())); @@ -80,6 +99,7 @@ ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, std::spa ShaderRecompiler::ShaderPixelStageInfo pixel{}; pixel.interpolatorCount = 1; + if (flat) pixel.interpolatorSettings[0] = 0x400u; pixel.inputAddr = inputs; pixel.hasPerspectiveCenterVgpr = (inputs & PixelInputBit(PixelInput::PerspectiveCenter)) != 0u; pixel.noPerspective = (inputs & PixelInputBit(PixelInput::LinearCenter)) != 0u; @@ -98,9 +118,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(), provoking == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT ? 2u : 0u); + shaders.push_back({ShaderStage::TessellationControl, &patches.control, 0}); + shaders.push_back({ShaderStage::TessellationEvaluation, &patches.evaluation, 0}); } shaders.push_back({ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}); @@ -110,11 +135,12 @@ ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, std::spa state.colors = {state.color}; state.hasColorTarget = true; state.renderExtent = {Width, Height}; - state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.topology = strip ? VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP : 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.cullMode = cullMode; + state.provokingVertexMode = provoking; state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; state.blend.colorWriteMask = 15; state.blends = {state.blend}; @@ -232,31 +258,166 @@ void CheckInterpolated() { Require(checked > 200u, "v_interp_p1/p2: too few interior pixels were checked"); } +void CheckFlat(const std::string& what, std::size_t provoking) { + const auto positions = ClipPositions(); + std::uint32_t checked = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = BarycentricsUnsnapped(x, y); + if (!expected.inside) continue; + const auto snapped = BarycentricsSnapped(x, y); + for (std::uint32_t channel = 0; channel < 2; ++channel) { + Require(MatchesEither(Pixel(x, y, channel), expected.perspective[1u + channel], snapped.perspective[1u + channel]), what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") weight " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel))); + } + for (std::uint32_t channel = 2; channel < 4; ++channel) { + Require(Pixel(x, y, channel) == positions[provoking][0], what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") flat channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected vertex " + std::to_string(provoking) + "'s " + std::to_string(positions[provoking][0])); + } + ++checked; + } + } + Require(checked > 200u, what + ": too few interior pixels were checked"); +} + +bool Drawn() { + return std::any_of(Pixels.begin(), Pixels.end(), [](float value) { return value != Unwritten; }); +} + +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"); + } +} + +struct StripPixel { + bool inside; + std::array order; + std::array weights; +}; + +StripPixel StripBarycentrics(std::uint32_t x, std::uint32_t y, bool snapped) { + std::array, 4> window{}; + for (std::size_t i = 0; i < StripCorners.size(); ++i) { + window[i] = {(StripCorners[i].x + 1.0) * Width / 2.0, (1.0 - StripCorners[i].y) * Height / 2.0}; + if (snapped) { + for (auto& coordinate : window[i]) coordinate = std::round(coordinate * 256.0) / 256.0; + } + } + const double px = x + 0.5; + const double py = y + 0.5; + const auto edge = [](const std::array& a, const std::array& b, double cx, double cy) { return (b[0] - a[0]) * (cy - a[1]) - (b[1] - a[1]) * (cx - a[0]); }; + for (const std::array order : {std::array{0, 1, 2}, std::array{1, 3, 2}}) { + const auto& a = window[order[0]]; + const auto& b = window[order[1]]; + const auto& c = window[order[2]]; + const double area = edge(a, b, c[0], c[1]); + const std::array weights{edge(b, c, px, py) / area, edge(c, a, px, py) / area, edge(a, b, px, py) / area}; + if (std::min({weights[0], weights[1], weights[2]}) > 0.02) return {true, order, weights}; + } + return {false, {}, {}}; +} + +void CheckStrip(AgcDriver::VulkanDevice& device, Emulation emulation, const std::string& name) { + Draw(device, emulation, BarycentricPixelCode, PixelInputBit(PixelInput::LinearCenter), VK_CULL_MODE_NONE, false, VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, true); + std::array checked{}; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = StripBarycentrics(x, y, false); + if (!expected.inside) continue; + const auto snapped = StripBarycentrics(x, y, true); + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto reference = static_cast(expected.weights[1u + (channel & 1u)]); + const auto snappedReference = snapped.inside && snapped.order == expected.order ? static_cast(snapped.weights[1u + (channel & 1u)]) : reference; + Require(MatchesEither(Pixel(x, y, channel), reference, snappedReference), name + " triangle strip: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected " + std::to_string(reference)); + } + ++checked[expected.order[0]]; + } + } + Require(checked[0] > 200u && checked[1] > 200u, name + " triangle strip: too few pixels of each triangle were checked"); + static const auto positions = StripPositions(); + Draw(device, emulation, VertexValuePixelCode, PixelInputBit(PixelInput::PerspectiveCenter), VK_CULL_MODE_NONE, false, VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, true); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = StripBarycentrics(x, y, false); + if (!expected.inside) continue; + const auto first = positions[expected.order[0]][0]; + const std::array values{positions[expected.order[1]][0] - first, positions[expected.order[2]][0] - first, first}; + for (std::uint32_t channel = 0; channel < 3; ++channel) { + Require(Pixel(x, y, channel) == values[channel], name + " triangle strip v_interp_mov: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected " + std::to_string(values[channel])); + } + } + } + std::printf("barycentric emulation through the %s passed for a triangle strip: %u and %u pixels of its even and odd triangle\n", name.c_str(), checked[0], checked[1]); +} + +void CheckStripFlatLast(AgcDriver::VulkanDevice& device, Emulation emulation, const std::string& name) { + Draw(device, emulation, FlatBarycentricPixelCode, PixelInputBit(PixelInput::PerspectiveCenter), VK_CULL_MODE_NONE, true, VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT, true); + const auto positions = StripPositions(); + std::array checked{}; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = StripBarycentrics(x, y, false); + if (!expected.inside) continue; + const auto triangle = expected.order[0]; + const auto provoking = triangle == 0u ? 2u : 3u; + for (std::uint32_t channel = 2; channel < 4; ++channel) { + Require(Pixel(x, y, channel) == positions[provoking][0], name + " flat last-provoking strip: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") flat channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected vertex " + std::to_string(provoking) + " value " + std::to_string(positions[provoking][0])); + } + ++checked[triangle]; + } + } + Require(checked[0] > 200u && checked[1] > 200u, name + " flat last-provoking strip: too few pixels of each triangle were checked"); + std::printf("flat last-provoking strip through %s passed: %u and %u pixels of its even and odd triangle\n", name.c_str(), checked[0], checked[1]); +} + +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); + result = Draw(device, emulation, FlatBarycentricPixelCode, perspective, VK_CULL_MODE_NONE, true); + Require(result.barycentricEmulation.active && std::any_of(result.fragmentParameters.begin(), result.fragmentParameters.end(), [](const auto& parameter) { return parameter.flat && !parameter.perVertex; }), "a flat P0 read beside raw I/J did not pass a flat parameter through the emulation"); + CheckFlat(name + " flat input, first provoking vertex", 0u); + if (emulation == Emulation::Tessellation) { + Draw(device, emulation, FlatBarycentricPixelCode, perspective, VK_CULL_MODE_NONE, true, VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT); + CheckFlat(name + " flat input, last provoking vertex", 2u); + CheckStripFlatLast(device, emulation, name); + } + CheckStrip(device, emulation, name); + std::printf("barycentric emulation through the %s passed: %u perspective and %u linear I/J pixels, raw vertex values, host interpolation, face culling, flat inputs and triangle strips\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/PixelImplicitDerivatives.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelImplicitDerivatives.cpp new file mode 100644 index 0000000000..db376c9567 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelImplicitDerivatives.cpp @@ -0,0 +1,164 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 192; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Levels = 5; +alignas(256) std::array Pixels{}; +alignas(256) std::array Texels{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf800020f, 0x03020100, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0xbefe0a7e, 0xbe80047e, 0xbefe0400, 0xc8080000, 0xc8090001, 0xc80c0100, 0xc80d0101, 0x101000ff, 0x358637bd, 0x06041102, 0x10040410, 0x10060611, + 0xf0800f08, 0x00610402, 0xbf8c3f70, 0xf800180f, 0x07060504, 0xbf810000, +}; + +std::vector> Triangles() { + std::vector> vertices; + for (std::uint32_t cell = 0; cell < 24; ++cell) { + const float x = -1.0f + (2.0f / 6.0f) * static_cast(cell % 6); + const float y = -1.0f + 0.5f * static_cast(cell / 6); + const float skew = 0.07f * static_cast(cell % 5); + vertices.push_back({x + (0.05f + skew) / 3.0f, y + 0.025f, 0.5f, 1.0f}); + vertices.push_back({x + 0.31f, y + (0.11f + skew) * 0.5f, 0.5f, 1.0f}); + vertices.push_back({x + (0.37f - skew * 0.5f) / 3.0f, y + 0.455f, 0.5f, 1.0f}); + } + return vertices; +} + +std::array BufferDescriptor(const void* data, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32) & 0xffffu) | (16u << 16), count, 0x01016facu}; +} + +std::array TextureDescriptor() { + const auto address = reinterpret_cast(Texels.data()); + return {static_cast(address >> 8), static_cast((address >> 40) & 0xffu) | (56u << 20) | (3u << 30), + ((Size - 1u) >> 2) | ((Size - 1u) << 14), 0xfacu | ((Levels - 1u) << 16) | (9u << 28), 0u, (Levels - 1u) << 4, 0u, 0u}; +} + +void FillTexture() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor())); + Require(geometry.guestBytes <= Texels.size(), "implicit derivative texture does not fit"); + Texels.fill(0xeeu); + for (std::uint32_t level = 0; level < Levels; ++level) { + const auto& mip = geometry.mips[level]; + for (std::uint32_t y = 0; y < (Size >> level); ++y) for (std::uint32_t x = 0; x < (Size >> level); ++x) { + auto* texel = &Texels[mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; + texel[0] = static_cast(10u + 40u * level); + texel[1] = 77; + texel[2] = 99; + texel[3] = 255; + } + } +} + +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t wave, std::uint32_t level, bool emulate) { + auto target = device.Target(); + if (emulate) target.fragmentShaderBarycentricEnabled = false; + static const auto triangles = Triangles(); + Pixels.fill(std::byte{0}); + const auto vertexDescriptor = BufferDescriptor(triangles.data(), static_cast(triangles.size())); + const std::vector vertexUserData(vertexDescriptor.begin(), vertexDescriptor.end()); + 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, {}}, + {wave, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, target, {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = wave == 32; + pixel.interpolatorCount = 1; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCenter); + pixel.hasPerspectiveCenterVgpr = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + std::vector pixelUserData(18); + const auto texture = TextureDescriptor(); + std::copy(texture.begin(), texture.end(), pixelUserData.begin() + 4); + const std::array sampler{0u, 0xfffu << 12, 1u << 26, 0u}; + std::copy(sampler.begin(), sampler.end(), pixelUserData.begin() + 12); + pixelUserData[16] = std::bit_cast(std::exp2(static_cast(level) - 0.25f) * Width / (2.0f * Size)); + pixelUserData[17] = std::bit_cast(std::exp2(static_cast(level) - 0.25f) * Height / (2.0f * Size)); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {wave, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + std::vector shaders{{ShaderStage::Vertex, &vertexResult, 0}}; + ShaderRecompiler::RecompileResult geometry; + if (pixelResult.barycentricEmulation.active) { + geometry = ShaderRecompiler::BuildBarycentricGeometryShader(vertexResult, pixelResult, target, device.GeometryLimits()); + shaders.push_back({ShaderStage::Geometry, &geometry, 0}); + } + shaders.push_back({ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}); + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, wave, wave, 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}; + device.Draw(state, {0, static_cast(triangles.size()), 0, 1, 0, false}, shaders); + device.WaitIdle(); + std::uint32_t covered = 0; + std::uint32_t bad = 0; + for (std::uint32_t index = 0; index < Width * Height; ++index) { + if (Pixels[index * 4 + 3] == std::byte{0}) continue; + ++covered; + bad += std::to_integer(Pixels[index * 4]) != 10u + 40u * level; + } + std::printf("emulate%u wave%u mip%u covered=%u incorrect=%u\n", emulate, wave, level, covered, bad); + Require(covered > 1000, "implicit derivative triangles covered too few pixels"); + Require(bad == 0, "implicit derivatives selected the wrong mip at " + std::to_string(bad) + " pixels"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!device->GeometryLimits()) { + std::puts("skipped, the device lacks geometryShader"); + return VulkanTestSkipped; + } + FillTexture(); + for (const auto emulate : {false, true}) for (const auto wave : {64u, 32u}) for (const auto level : {0u, 1u, 2u}) Draw(*device, wave, level, emulate); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} 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/PixelSampleCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp new file mode 100644 index 0000000000..ce79a9b375 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleCoverage.cpp @@ -0,0 +1,396 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context Context(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; + +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} +}}; + +constexpr std::array, 3> HalfTriangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {1.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 1.0f, 0.5f, 1.0f} +}}; + +constexpr std::array, 3> OtherHalfTriangle{{ + {1.0f, -1.0f, 0.5f, 1.0f}, {1.0f, 1.0f, 0.5f, 1.0f}, {-1.0f, 1.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 8> Programs{}; +alignas(256) std::array DepthMemory{}; +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 coverageVgpr) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + coverageVgpr); + code[at++] = 0x7e000280u | (21u << 17u); + code[at++] = 0x7e000280u | (22u << 17u); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +std::span SampleIndexProgram() { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + code[at++] = 0xd5480018u; + code[at++] = 0x02111100u; + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 24u); + code[at++] = 0x7e000280u | (21u << 17u); + code[at++] = 0x7e000280u | (22u << 17u); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t info) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = info; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t inputAddr, std::span pixelCode, std::span, 3> triangle = Triangle, std::uint32_t rasterSamples = 1, const AgcDriver::Graphics::DepthTarget* depth = nullptr, bool perSample = false) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(triangle.data(), 16u, static_cast(triangle.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = inputAddr; + pixel.ancillary = (inputAddr & ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary)) != 0u; + pixel.hasPerspectiveCenterVgpr = (inputAddr & ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCenter)) != 0u; + pixel.sampleShading = perSample; + pixel.iteratesSamples = perSample; + 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 = {}; + state.samples = rasterSamples; + state.mergedCoverage = rasterSamples > 1 && depth == nullptr; + state.truncatedCoverage = rasterSamples > 1 && depth != nullptr; + state.perSampleShading = perSample; + if (depth != nullptr) { + state.depth = *depth; + state.depthTest = true; + state.depthWrite = false; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + } + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, std::uint64_t address, std::size_t bytes) { + AgcDriver::Graphics::StorageTexture::FlushPending(address, bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(bytes); + AgcDriver::GuestMemory::Read(address, std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +void Expect(AgcDriver::VulkanDevice& device, std::uint32_t inputAddr, std::uint32_t coverageVgpr, const std::string& what) { + constexpr std::size_t bytes = std::size_t{Width} * Height * 16u; + Require(AgcDriver::Graphics::ColorTargetLayout(Width, Height, AgcDriver::Graphics::ColorTileMode::Linear, 16).Bytes() == bytes, what + ": the linear target is padded"); + Block block(bytes); + std::fill(block.data, block.data + bytes, std::uint8_t{0xff}); + const auto color = DecodeTarget(block, Rgba32FloatInfo); + Require(color.format == VK_FORMAT_R32G32B32A32_SFLOAT && color.elementBytes == 16 && color.bytes == bytes, what + ": the target decoded to the wrong format"); + Draw(device, color, inputAddr, Program(coverageVgpr)); + const auto stored = ReadBack(device, block.Address(), bytes); + 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{1.0f, 0.0f, 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 ExpectMerged(AgcDriver::VulkanDevice& device) { + 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 color = DecodeTarget(block, Rgba32FloatInfo); + try { + Draw(device, color, ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage), Program(0u), HalfTriangle, 4u); + } catch (const std::exception& error) { + if (std::string_view(error.what()).find("VK_NV_framebuffer_mixed_samples") == std::string_view::npos) throw; + return false; + } + const auto stored = ReadBack(device, block.Address(), bytes); + std::uint32_t full = 0; + std::uint32_t partial = 0; + std::uint32_t untouched = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto* texel = stored.data() + (static_cast(y) * Width + x) * 16u; + if (std::all_of(texel, texel + 16, [](std::uint8_t byte) { return byte == 0xffu; })) { + ++untouched; + continue; + } + std::array value{}; + std::memcpy(value.data(), texel, sizeof(value)); + const auto mask = static_cast(value[0]); + Require(value[0] == static_cast(mask) && mask >= 1u && mask <= 15u && value[1] == 0.0f && value[2] == 0.0f && value[3] == 1.0f, "merged coverage: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(value[0]) + ", " + std::to_string(value[1]) + ", " + std::to_string(value[2]) + ", " + std::to_string(value[3]) + ")"); + if (mask == 15u) ++full; + else ++partial; + } + } + Require(full != 0 && partial != 0 && untouched != 0, "merged coverage: a half-covering triangle wrote " + std::to_string(full) + " fully covered, " + std::to_string(partial) + " partly covered and left " + std::to_string(untouched) + " pixels"); + return true; +} + +std::string ExpectTruncated(AgcDriver::VulkanDevice& device) { + if ((AgcDriver::VulkanDeviceTestAccess::Context(device).limits.framebufferDepthSampleCounts & VK_SAMPLE_COUNT_2_BIT) == 0) return "the device has no two-sample depth targets (framebufferDepthSampleCounts)"; + constexpr std::size_t bytes = std::size_t{Width} * Height * 16u; + AgcDriver::Graphics::DepthTarget depth{reinterpret_cast(DepthMemory.data()), 0u, {Width, Height}, VK_FORMAT_D32_SFLOAT, 0.0f, 0}; + depth.samples = 2; + std::array written{}; + std::uint32_t untouched = 0; + for (const auto* triangle : {&HalfTriangle, &OtherHalfTriangle}) { + Block block(bytes); + std::fill(block.data, block.data + bytes, std::uint8_t{0xff}); + const auto color = DecodeTarget(block, Rgba32FloatInfo); + try { + Draw(device, color, ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage), Program(0u), *triangle, 2u, &depth); + } catch (const std::exception& error) { + if (std::string_view(error.what()).find("truncated coverage reduction") == std::string_view::npos) throw; + return "the device lacks VK_AMD_mixed_attachment_samples and VK_NV_framebuffer_mixed_samples with truncated coverage reduction"; + } + const auto stored = ReadBack(device, block.Address(), bytes); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto* texel = stored.data() + (static_cast(y) * Width + x) * 16u; + if (std::all_of(texel, texel + 16, [](std::uint8_t byte) { return byte == 0xffu; })) { + ++untouched; + continue; + } + std::array value{}; + std::memcpy(value.data(), texel, sizeof(value)); + const auto mask = static_cast(value[0]); + Require(value[0] == static_cast(mask) && mask >= 1u && mask <= 3u && value[1] == 0.0f && value[2] == 0.0f && value[3] == 1.0f, "truncated coverage: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(value[0]) + ", " + std::to_string(value[1]) + ", " + std::to_string(value[2]) + ", " + std::to_string(value[3]) + ")"); + ++written[mask]; + } + } + } + Require(written[2] == 0, "truncated coverage: " + std::to_string(written[2]) + " pixels that cover only sample 1 were written into the one-sample target"); + Require(written[1] != 0 && written[3] != 0 && untouched != 0, "truncated coverage: two half-covering triangles wrote " + std::to_string(written[3]) + " fully covered and " + std::to_string(written[1]) + " sample-0-only pixels and left " + std::to_string(untouched)); + std::uint32_t perSampleWritten = 0; + std::uint32_t perSampleUntouched = 0; + for (const auto* triangle : {&HalfTriangle, &OtherHalfTriangle}) { + Block block(bytes); + std::fill(block.data, block.data + bytes, std::uint8_t{0xff}); + const auto color = DecodeTarget(block, Rgba32FloatInfo); + Draw(device, color, ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary), SampleIndexProgram(), *triangle, 2u, &depth, true); + const auto stored = ReadBack(device, block.Address(), bytes); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto* texel = stored.data() + (static_cast(y) * Width + x) * 16u; + if (std::all_of(texel, texel + 16, [](std::uint8_t byte) { return byte == 0xffu; })) { + ++perSampleUntouched; + continue; + } + std::array value{}; + std::memcpy(value.data(), texel, sizeof(value)); + Require(value == std::array{0.0f, 0.0f, 0.0f, 1.0f}, "truncated coverage per sample: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(value[0]) + ", " + std::to_string(value[1]) + ", " + std::to_string(value[2]) + ", " + std::to_string(value[3]) + "), not sample 0's output"); + ++perSampleWritten; + } + } + } + Require(perSampleWritten == written[1] + written[3] && perSampleUntouched == untouched, "truncated coverage per sample: the draws wrote " + std::to_string(perSampleWritten) + " pixels where one invocation per pixel wrote " + std::to_string(written[1] + written[3])); + return {}; +} + +} + +int main() { + try { + using ShaderRecompiler::PixelInput; + using ShaderRecompiler::PixelInputBit; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Expect(*device, PixelInputBit(PixelInput::SampleCoverage), 0u, "SAMPLE_COVERAGE alone"); + Expect(*device, PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::SampleCoverage), 2u, "SAMPLE_COVERAGE after the perspective center"); + Expect(*device, PixelInputBit(PixelInput::Ancillary) | PixelInputBit(PixelInput::SampleCoverage), 1u, "SAMPLE_COVERAGE after ANCILLARY"); + if (!ExpectMerged(*device)) std::puts("merged coverage not tested: the device lacks VK_NV_framebuffer_mixed_samples with merged coverage reduction"); + if (const auto skipped = ExpectTruncated(*device); !skipped.empty()) std::printf("truncated coverage not tested: %s\n", skipped.c_str()); + std::puts("pixel sample coverage 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/PixelSampleIteration.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleIteration.cpp new file mode 100644 index 0000000000..072b3ca41b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleIteration.cpp @@ -0,0 +1,630 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::PixelInput; +using ShaderRecompiler::PixelInputBit; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t Slots = 4; +constexpr std::uint32_t WaveSize = 64; +constexpr std::uint32_t PositionAtSample = 0x20000u; +constexpr std::uint32_t PositionUpperLeft = 0x100000u; +constexpr std::uint32_t Positions = PixelInputBit(PixelInput::PositionX) | PixelInputBit(PixelInput::PositionY); +constexpr std::uint32_t Ancillary = PixelInputBit(PixelInput::Ancillary); +constexpr std::uint32_t PerspectiveSample = PixelInputBit(PixelInput::PerspectiveSample); +constexpr std::uint32_t PerspectiveCenter = PixelInputBit(PixelInput::PerspectiveCenter); +constexpr std::uint32_t LinearSample = PixelInputBit(PixelInput::LinearSample); +constexpr std::uint32_t Village = PerspectiveSample | Positions | Ancillary; + +struct Record { + std::uint32_t invocations; + float x; + float y; + float i; + float j; + std::uint32_t ancillary; + std::array unused; +}; +static_assert(sizeof(Record) == 32); +alignas(256) std::array Records{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array AncillaryStoreCode{ + 0x7e0a0f02, 0x7e0c0f03, 0x340c0c86, 0x4a0a0d05, 0x340a0a82, 0xd5480007, 0x02111104, 0x4a0a0f05, + 0x340a0a85, 0x7e100281, 0xe0c81000, 0x80000805, 0xe0701004, 0x80000205, 0xe0701008, 0x80000305, + 0xe070100c, 0x80000005, 0xe0701010, 0x80000105, 0xe0701014, 0x80000405, 0xbf8c3f70, 0xf8001890, + 0x00000000, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelStoreCode{ + 0x7e0a0f02, 0x7e0c0f03, 0x340c0c86, 0x4a0a0d05, 0x340a0a87, 0x7e100281, 0xe0c81000, 0x80000805, + 0xe0701004, 0x80000205, 0xe0701008, 0x80000305, 0xe070100c, 0x80000005, 0xe0701010, 0x80000105, + 0xbf8c3f70, 0xf8001890, 0x00000000, 0xbf810000, +}; + +alignas(256) constexpr std::array ExportCode{0xf800180f, 0x04020100, 0xbf810000}; + +struct Corner { + float x; + float y; + float w; +}; + +constexpr std::array Corners{{{-1.0f, -1.0f, 1.0f}, {1.0f, -1.0f, 2.0f}, {-1.0f, 1.0f, 0.5f}}}; + +std::array, 3> ClipPositions() { + std::array, 3> positions{}; + for (std::size_t i = 0; i < Corners.size(); ++i) positions[i] = {Corners[i].x * Corners[i].w, Corners[i].y * Corners[i].w, 0.5f * Corners[i].w, Corners[i].w}; + return positions; +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, stride == 0u ? 0x31016facu : 0x01016facu}; +} + +std::array, 4> StandardSamples(std::uint32_t samples) { + if (samples == 2) return {{{0.25, 0.25}, {-0.25, -0.25}, {}, {}}}; + if (samples == 4) return {{{-0.125, -0.375}, {0.375, -0.125}, {-0.375, 0.125}, {0.125, 0.375}}}; + return {}; +} + +struct Pass { + std::uint32_t samples = 4; + std::uint32_t inputs = Village; + bool iterated = false; + std::uint32_t barycentricControl = 0; + std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; + bool customLocations = false; + AgcDriver::Graphics::SampleOrder order{}; +}; + +std::uint8_t CustomLocation(std::uint32_t pixel, std::uint32_t sample) { + return static_cast(sample | (pixel << 4u)); +} + +AgcDriver::Graphics::State PassState(const Pass& pass) { + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.hasColorTarget = false; + 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.blendConstants = {}; + state.samples = pass.samples; + state.perSampleShading = pass.iterated; + state.quadSampleMask = pass.quadSampleMask; + state.customSampleLocations = pass.customLocations; + state.sampleLocationGrid = pass.customLocations; + for (std::uint32_t pixel = 0; pass.customLocations && pixel < 4u; ++pixel) { + for (std::uint32_t sample = 0; sample < pass.samples; ++sample) state.sampleLocations[pixel * 8u + sample] = CustomLocation(pixel, sample); + } + return state; +} + +ShaderRecompiler::ShaderPixelStageInfo PassPixel(const Pass& pass, const AgcDriver::Graphics::State& state) { + const AgcDriver::Registers context{{0x1b3, pass.inputs}, {0x1b4, pass.inputs}, {0x1b6, 0u}, {0x1b8, pass.barycentricControl}, {0x1c5, 0u}, {0x203, 0u}}; + auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + pixel.quadPixelMask = AgcDriver::Graphics::QuadPixelMask(state); + pixel.quadSampleMask = AgcDriver::Graphics::QuadSampleMasks(state, pass.order); + pixel.iteratesSamples = state.perSampleShading; + pixel.sampleLocations = AgcDriver::Graphics::PixelSampleLocations(state, pixel); + pixel.sampleOrder = pass.order; + return pixel; +} + +void Draw(AgcDriver::VulkanDevice& device, const Pass& pass) { + Records.fill({}); + const auto target = device.Target(); + static const auto positions = ClipPositions(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(positions.data(), 16u, static_cast(positions.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()); + + const auto state = PassState(pass); + const auto pixel = PassPixel(pass, state); + Require(ShaderRecompiler::PixelInputVgpr(pass.inputs, PixelInput::PositionX) == 2u && ShaderRecompiler::PixelInputVgpr(pass.inputs, PixelInput::PositionY) == 3u, "the test inputs moved the positions"); + std::vector pixelUserData(4, 0u); + const auto records = BufferDescriptor(Records.data(), 0u, static_cast(sizeof(Records))); + std::copy(records.begin(), records.end(), pixelUserData.begin()); + const std::span code = (pass.inputs & Ancillary) != 0u ? std::span(AncillaryStoreCode) : std::span(PixelStoreCode); + const std::array pixelMemory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(code.data()), code, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + std::vector shaders{{ShaderStage::Vertex, &vertexResult, 0}}; + ShaderRecompiler::RecompileResult geometry; + if (pixelResult.barycentricEmulation.active) { + geometry = ShaderRecompiler::BuildBarycentricGeometryShader(vertexResult, pixelResult, target, device.GeometryLimits()); + shaders.push_back({ShaderStage::Geometry, &geometry, 0}); + } + shaders.push_back({ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}); + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(positions.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +std::array Barycentrics(double x, double y, bool perspective) { + const double b1 = x / Width; + const double b2 = (Height - y) / Height; + const std::array linear{1.0 - b1 - b2, b1, b2}; + if (!perspective) return {linear[1], linear[2]}; + double sum = 0.0; + for (std::size_t i = 0; i < Corners.size(); ++i) sum += linear[i] / Corners[i].w; + return {linear[1] / Corners[1].w / sum, linear[2] / Corners[2].w / sum}; +} + +std::string Point(double x, double y) { + return "(" + std::to_string(x) + ", " + std::to_string(y) + ")"; +} + +void Check(const Pass& pass, const std::string& what) { + const bool ancillary = (pass.inputs & Ancillary) != 0u; + const bool samplePair = (pass.inputs & (PerspectiveSample | LinearSample)) != 0u; + const bool perspective = (pass.inputs & (PerspectiveSample | PerspectiveCenter)) != 0u; + const auto samples = StandardSamples(pass.samples); + const auto full = (1u << pass.samples) - 1u; + const auto location = (pass.barycentricControl & PositionUpperLeft) != 0u ? 3u : (pass.barycentricControl >> 16u) & 3u; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto mask = pass.quadSampleMask[(x & 1u) | ((y & 1u) << 1u)] & full; + std::uint32_t inside = 0; + std::uint32_t outside = 0; + for (std::uint32_t sample = 0; sample < pass.samples; ++sample) { + const auto weights = Barycentrics(x + 0.5 + samples[sample][0], y + 0.5 + samples[sample][1], false); + const double first = 1.0 - weights[0] - weights[1]; + inside += first > 1e-3 ? 1u : 0u; + outside += first < -1e-3 ? 1u : 0u; + } + if (inside != pass.samples && outside != pass.samples) continue; + for (std::uint32_t slot = 0; slot < Slots; ++slot) { + const auto& record = Records[(y * Width + x) * Slots + slot]; + std::uint32_t expected = 0; + if (inside == 0u) expected = 0; + else if (pass.iterated) expected = ancillary ? (slot < pass.samples ? (mask >> slot) & 1u : 0u) : (slot == 0u ? static_cast(std::popcount(mask)) : 0u); + else expected = slot == 0u && mask != 0u ? 1u : 0u; + const auto where = what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") slot " + std::to_string(slot); + Require(record.invocations == expected, where + " ran " + std::to_string(record.invocations) + " time(s), expected " + std::to_string(expected)); + if (expected == 0u) continue; + const bool known = !pass.iterated || ancillary; + const auto sample = pass.iterated ? slot : 0u; + if (ancillary) Require(((record.ancillary >> 8u) & 0xfu) == sample, where + " read sample number " + std::to_string((record.ancillary >> 8u) & 0xfu)); + const double centerX = x + 0.5; + const double centerY = y + 0.5; + double positionX = centerX; + double positionY = centerY; + if (location == 3u) { + positionX = x; + positionY = y; + } else if (location == 2u) { + positionX = centerX + samples[sample][0]; + positionY = centerY + samples[sample][1]; + } + if (known || location == 0u || location == 3u) { + Require(record.x == static_cast(positionX) && record.y == static_cast(positionY), where + " read the position " + Point(record.x, record.y) + ", expected " + Point(positionX, positionY)); + } + if (samplePair && !known) continue; + const double evaluatedX = samplePair ? centerX + samples[sample][0] : centerX; + const double evaluatedY = samplePair ? centerY + samples[sample][1] : centerY; + const auto weights = Barycentrics(evaluatedX, evaluatedY, perspective); + Require(std::fabs(record.i - weights[0]) <= 1e-5 && std::fabs(record.j - weights[1]) <= 1e-5, where + " read the barycentrics " + Point(record.i, record.j) + ", expected " + Point(weights[0], weights[1]) + " at " + Point(evaluatedX, evaluatedY)); + } + } + } +} + +bool Run(AgcDriver::VulkanDevice& device, const Pass& pass, const std::string& what) { + try { + Draw(device, pass); + } catch (const std::exception& error) { + if (std::string(error.what()).find("device does not support rendering without attachments at the draw's sample count") == std::string::npos) throw; + std::printf("%s: skipped, %s\n", what.c_str(), error.what()); + return false; + } + Check(pass, what); + return true; +} + +struct Module { + std::vector builtins; + std::vector capabilities; + std::vector> offsets; + std::vector>> tables; + bool variableOffset = false; + std::map decorations; + std::map constants; + std::map>> instructions; +}; + +Module PixelModule(const Pass& pass, bool interpolationFunctions) { + const auto state = PassState(pass); + auto pixel = PassPixel(pass, state); + pixel.targetOutputMode[0] = 9u; + pixel.targetExportMapping.fill(0xe4u); + static constexpr std::array withOffsets{spv::CapabilityInterpolationFunction}; + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderStage::Fragment, reinterpret_cast(ExportCode.data()), ExportCode, 0, {}}; + request.context.waveSize = WaveSize; + request.context.pixel = pixel; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.target.fragmentShaderBarycentricEnabled = true; + if (interpolationFunctions) request.target.supportedCapabilities = withOffsets; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + const auto& words = result.spirv.Words(); + std::map floats; + std::map> vectors; + Module module; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + const auto count = static_cast(words[at] >> 16u); + if (op == spv::OpCapability) module.capabilities.push_back(static_cast(words[at + 1])); + if (op == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn) module.decorations[words[at + 1]] = static_cast(words[at + 3]); + if (op == spv::OpConstant && count == 4u) { + floats[words[at + 2]] = std::bit_cast(words[at + 3]); + module.constants[words[at + 2]] = words[at + 3]; + } + switch (op) { + case spv::OpConstant: + case spv::OpLoad: + case spv::OpAccessChain: + case spv::OpBitcast: + case spv::OpConvertFToU: + case spv::OpBitwiseAnd: + case spv::OpBitwiseOr: + case spv::OpShiftLeftLogical: + case spv::OpShiftRightLogical: + case spv::OpIAdd: + case spv::OpIEqual: + case spv::OpSelect: + case spv::OpCompositeExtract: + case spv::OpExtInst: + if (count >= 3u) module.instructions[words[at + 2]] = {op, std::vector(words.begin() + static_cast(at) + 3, words.begin() + static_cast(at) + static_cast(count))}; + break; + default: + break; + } + if (op == spv::OpConstantComposite && (words[at] >> 16u) == 5u && floats.contains(words[at + 3]) && floats.contains(words[at + 4])) vectors[words[at + 2]] = {floats[words[at + 3]], floats[words[at + 4]]}; + if (op == spv::OpConstantComposite && (words[at] >> 16u) > 5u && vectors.contains(words[at + 3])) { + std::vector> table; + for (std::size_t element = at + 3; element < at + (words[at] >> 16u) && vectors.contains(words[element]); ++element) table.push_back(vectors[words[element]]); + module.tables.push_back(table); + } + if (op == spv::OpExtInst && words[at + 4] == GLSLstd450InterpolateAtOffset) { + const auto found = vectors.find(words[at + 6]); + if (found == vectors.end()) module.variableOffset = true; + else module.offsets.push_back(found->second); + } + if (op != spv::OpLoad && op != spv::OpAccessChain && op != spv::OpExtInst) continue; + const auto pointer = op == spv::OpExtInst ? words[at + 5] : words[at + 3]; + const auto found = module.decorations.find(pointer); + if (found != module.decorations.end() && std::ranges::find(module.builtins, found->second) == module.builtins.end()) module.builtins.push_back(found->second); + } + return module; +} + +bool Has(const Module& module, spv::BuiltIn builtin) { + return std::ranges::find(module.builtins, builtin) != module.builtins.end(); +} + +bool Has(const Module& module, spv::Capability capability) { + return std::ranges::find(module.capabilities, capability) != module.capabilities.end(); +} + +void ExpectModules() { + for (const std::uint32_t samples : {2u, 4u}) { + const auto name = std::to_string(samples) + "x"; + const auto first = StandardSamples(samples)[0]; + Pass pass; + pass.samples = samples; + auto module = PixelModule(pass, true); + Require(!Has(module, spv::BuiltInSampleId) && !Has(module, spv::CapabilitySampleRateShading), name + ": a pixel shader the guest runs once per pixel reads SampleId"); + Require(!module.offsets.empty() && !module.variableOffset && std::ranges::all_of(module.offsets, [&](const auto& offset) { return offset[0] == static_cast(first[0]) && offset[1] == static_cast(first[1]); }), name + ": PERSP_SAMPLE is not interpolated at sample 0's offset"); + Require(Has(module, spv::CapabilityInterpolationFunction) && Has(module, spv::BuiltInBaryCoordKHR) && Has(module, spv::BuiltInFragCoord), name + ": the once-per-pixel module lacks its inputs"); + pass.iterated = true; + module = PixelModule(pass, true); + Require(Has(module, spv::BuiltInSampleId) && Has(module, spv::CapabilitySampleRateShading), name + ": an iterated pixel shader lost its per-sample inputs"); + const auto standard = StandardSamples(samples); + const auto guestTable = [&](const std::vector>& table) { + if (table.size() != 8u) return false; + for (std::uint32_t sample = 0; sample < samples; ++sample) { + if (table[sample][0] != static_cast(standard[sample][0]) || table[sample][1] != static_cast(standard[sample][1])) return false; + } + return true; + }; + Require(module.offsets.empty() && module.variableOffset && std::ranges::any_of(module.tables, guestTable), name + ": an iterated PERSP_SAMPLE is not interpolated at the guest's sample locations"); + pass.customLocations = true; + module = PixelModule(pass, true); + const auto customTable = [&](std::uint32_t carried) { + return [&, carried](const std::vector>& table) { + if (table.size() != 32u) return false; + for (std::uint32_t index = 0; index < table.size(); ++index) { + const auto location = index % 8u < carried ? CustomLocation(index / 8u, index % 8u) : std::uint8_t{0u}; + if (table[index][0] != ShaderRecompiler::SampleLocationOffset(location, 0u) || table[index][1] != ShaderRecompiler::SampleLocationOffset(location, 1u)) return false; + } + return true; + }; + }; + Require(module.variableOffset && Has(module, spv::BuiltInSampleId) && Has(module, spv::BuiltInFragCoord) && std::ranges::any_of(module.tables, customTable(samples)), name + ": an iterated PERSP_SAMPLE is not interpolated at each pixel's custom sample locations"); + pass.iterated = false; + module = PixelModule(pass, true); + Require(module.variableOffset && !Has(module, spv::BuiltInSampleId) && Has(module, spv::BuiltInFragCoord) && std::ranges::any_of(module.tables, customTable(1u)), name + ": a once-per-pixel PERSP_SAMPLE is not interpolated at each pixel's custom sample 0"); + pass.customLocations = false; + pass.iterated = true; + pass.inputs = PerspectiveCenter | Positions | Ancillary; + module = PixelModule(pass, true); + Require(!module.offsets.empty() && !module.variableOffset && std::ranges::all_of(module.offsets, [](const auto& offset) { return offset[0] == 0.0f && offset[1] == 0.0f; }), name + ": an iterated PERSP_CENTER is not interpolated at the pixel center"); + pass.iterated = false; + module = PixelModule(pass, true); + Require(module.offsets.empty() && !Has(module, spv::BuiltInSampleId) && !Has(module, spv::CapabilityInterpolationFunction), name + ": a once-per-pixel PERSP_CENTER changed"); + pass.inputs = Village; + module = PixelModule(pass, false); + Require(module.offsets.empty() && !Has(module, spv::BuiltInSampleId) && !Has(module, spv::CapabilityInterpolationFunction), name + ": a device without sampleRateShading got offset interpolation"); + } + Pass single; + single.samples = 1; + const auto module = PixelModule(single, true); + Require(module.offsets.empty() && !Has(module, spv::BuiltInSampleId), "1x: a single-sample pixel shader changed"); +} + +const std::pair>* Instruction(const Module& module, std::uint32_t id) { + const auto found = module.instructions.find(id); + return found == module.instructions.end() ? nullptr : &found->second; +} + +bool ConstantIs(const Module& module, std::uint32_t id, std::uint32_t value) { + const auto found = module.constants.find(id); + return found != module.constants.end() && found->second == value; +} + +std::set BuiltinTrace(const Module& module, std::uint32_t id) { + std::set found; + std::set seen; + std::vector pending{id}; + while (!pending.empty()) { + const auto cursor = pending.back(); + pending.pop_back(); + if (!seen.insert(cursor).second) continue; + const auto* instruction = Instruction(module, cursor); + if (instruction == nullptr) continue; + if ((instruction->first == spv::OpLoad || instruction->first == spv::OpAccessChain) && !instruction->second.empty()) { + const auto decoration = module.decorations.find(instruction->second.front()); + if (decoration != module.decorations.end()) found.insert(decoration->second); + } + for (const auto argument : instruction->second) pending.push_back(argument); + } + return found; +} + +std::uint32_t GuestIndexOrderWord(const Module& module, std::uint32_t id) { + const auto* masked = Instruction(module, id); + if (masked == nullptr || masked->first != spv::OpBitwiseAnd || masked->second.size() != 2u || !ConstantIs(module, masked->second[1], 7u)) return 0u; + const auto* shifted = Instruction(module, masked->second[0]); + if (shifted == nullptr || shifted->first != spv::OpShiftRightLogical || shifted->second.size() != 2u) return 0u; + const auto* shift = Instruction(module, shifted->second[1]); + if (shift == nullptr || shift->first != spv::OpShiftLeftLogical || shift->second.size() != 2u || !ConstantIs(module, shift->second[1], 2u)) return 0u; + const auto* low = Instruction(module, shift->second[0]); + if (low == nullptr || low->first != spv::OpBitwiseAnd || low->second.size() != 2u || !ConstantIs(module, low->second[1], 7u)) return 0u; + const auto* bits = Instruction(module, low->second[0]); + if (bits == nullptr || bits->first != spv::OpBitcast || bits->second.empty()) return 0u; + const auto* loaded = Instruction(module, bits->second[0]); + if (loaded == nullptr || loaded->first != spv::OpLoad || loaded->second.empty()) return 0u; + const auto decoration = module.decorations.find(loaded->second[0]); + if (decoration == module.decorations.end() || decoration->second != spv::BuiltInSampleId) return 0u; + return shifted->second[0]; +} + +bool OrderWordValues(const Module& module, std::uint32_t id, std::set& values) { + const auto constant = module.constants.find(id); + if (constant != module.constants.end()) { + values.insert(constant->second); + return true; + } + const auto* instruction = Instruction(module, id); + if (instruction == nullptr || instruction->first != spv::OpSelect || instruction->second.size() != 3u) return false; + return OrderWordValues(module, instruction->second[1], values) && OrderWordValues(module, instruction->second[2], values); +} + +void CheckOrderIndex(const Module& module, const AgcDriver::Graphics::SampleOrder& order, const std::string& what) { + std::vector indexes; + for (const auto& [id, instruction] : module.instructions) { + if (instruction.first != spv::OpExtInst || instruction.second.size() < 4u || instruction.second[1] != GLSLstd450InterpolateAtOffset) continue; + const auto* loaded = Instruction(module, instruction.second[3]); + if (loaded == nullptr || loaded->first != spv::OpLoad || loaded->second.empty()) continue; + const auto* chain = Instruction(module, loaded->second[0]); + if (chain == nullptr || chain->first != spv::OpAccessChain || chain->second.size() < 2u) continue; + indexes.push_back(chain->second[1]); + } + Require(!indexes.empty(), what + ": the off-center sample read does not go through the per-sample location table"); + for (const auto index : indexes) { + const auto* joined = Instruction(module, index); + Require(joined != nullptr && joined->first == spv::OpBitwiseOr && joined->second.size() == 2u, what + ": the location table index is not guest | (quad << 3)"); + std::uint32_t quad = 0u; + std::uint32_t guest = 0u; + for (const auto operand : joined->second) { + const auto* instruction = Instruction(module, operand); + if (instruction != nullptr && instruction->first == spv::OpShiftLeftLogical && instruction->second.size() == 2u && ConstantIs(module, instruction->second[1], 3u)) quad = operand; + else guest = operand; + } + Require(quad != 0u && guest != 0u, what + ": the location table index is not guest | (quad << 3)"); + Require(BuiltinTrace(module, quad).contains(spv::BuiltInFragCoord), what + ": the table index's quad term does not come from the position input"); + const auto word = GuestIndexOrderWord(module, guest); + Require(word != 0u, what + ": the table index's low bits are not the order word's nibble at the shader's SampleId"); + std::set values; + Require(OrderWordValues(module, word, values), what + ": the order word in the table index is not selected per 2x2 pixel"); + for (const auto value : values) { + Require(std::ranges::any_of(order, [&](std::uint32_t word32) { return value == (word32 == 0u ? AgcDriver::Graphics::KeptSampleOrder : word32); }), what + ": the table index selects an order word the pass did not carry"); + } + } +} + +void ExpectOrderModules() { + const AgcDriver::Graphics::SampleOrder order{0x76543201u, AgcDriver::Graphics::KeptSampleOrder, 0x76543201u, AgcDriver::Graphics::KeptSampleOrder}; + for (const std::uint32_t samples : {2u, 4u}) { + const auto name = std::to_string(samples) + "x"; + Pass pass; + pass.samples = samples; + pass.inputs = Village; + pass.iterated = true; + pass.customLocations = true; + pass.order = order; + const auto module = PixelModule(pass, true); + Require(Has(module, spv::BuiltInSampleId) && Has(module, spv::BuiltInFragCoord), name + ": the reordered offset read lost the per-sample index or the position input"); + const auto customTable = [&](const std::vector>& table) { + if (table.size() != 32u) return false; + for (std::uint32_t index = 0; index < table.size(); ++index) { + const auto location = index % 8u < samples ? CustomLocation(index / 8u, index % 8u) : std::uint8_t{0u}; + if (table[index][0] != ShaderRecompiler::SampleLocationOffset(location, 0u) || table[index][1] != ShaderRecompiler::SampleLocationOffset(location, 1u)) return false; + } + return true; + }; + Require(std::ranges::any_of(module.tables, customTable), name + ": no 32-entry table of every pixel's custom sample locations"); + CheckOrderIndex(module, order, name + ": an iterated PERSP_SAMPLE on a reordered pattern varying across the 2x2 grid"); + } + for (const std::uint32_t samples : {2u, 4u}) { + const auto name = std::to_string(samples) + "x"; + for (const bool iterated : {false, true}) { + Pass pass; + pass.samples = samples; + pass.inputs = Ancillary; + pass.iterated = iterated; + pass.order = order; + Require(Has(PixelModule(pass, true), spv::BuiltInFragCoord), name + ": a pixel shader that only reorders its samples was not given the position input"); + pass.order = {}; + Require(!Has(PixelModule(pass, true), spv::BuiltInFragCoord), name + ": a pixel shader with nothing needing a position got one anyway"); + } + } + std::puts("recompiled modules index each pixel's location table by the guest sample index and the 2x2 quad term, and request the position input for the sample order alone"); +} + +void ExpectSampleLocations() { + Pass pass; + for (const std::uint32_t samples : {1u, 2u, 4u, 8u}) { + pass.samples = samples; + const auto state = PassState(pass); + const auto locations = AgcDriver::Graphics::PixelSampleLocations(state, PassPixel(pass, state)); + const std::uint8_t expected = samples == 1u ? 0x00u : samples == 2u ? 0x44u : samples == 4u ? 0xaeu : 0xd1u; + for (std::uint32_t index = 0; index < locations.size(); ++index) Require(locations[index] == (index % 8u == 0u ? expected : 0u), std::to_string(samples) + "x: the once-per-pixel locations are not sample 0's standard location"); + } + pass.samples = 4; + auto state = PassState(pass); + state.customSampleLocations = true; + state.sampleLocationGrid = true; + for (std::uint32_t pixel = 0; pixel < 4u; ++pixel) state.sampleLocations[pixel * 8u] = static_cast(0x10u * pixel + 1u); + auto pixel = PassPixel(pass, state); + for (std::uint32_t index = 0; index < 4u; ++index) Require(pixel.sampleLocations[index * 8u] == 0x10u * index + 1u, "custom per-pixel sample 0 locations were not carried"); + pass.iterated = true; + state = PassState(pass); + pixel = PassPixel(pass, state); + for (std::uint32_t index = 0; index < pixel.sampleLocations.size(); ++index) { + const std::array standard{0xaeu, 0xe6u, 0x2au, 0x62u}; + Require(pixel.sampleLocations[index] == (index % 8u < 4u ? standard[index % 8u] : 0u), "an iterated draw did not carry every sample's location"); + } + pass.iterated = false; + pass.inputs = PerspectiveCenter | Positions | Ancillary; + state = PassState(pass); + Require(std::ranges::all_of(PassPixel(pass, state).sampleLocations, [](std::uint8_t location) { return location == 0u; }), "a draw without sample inputs carried sample locations"); + pass.barycentricControl = PositionAtSample; + state = PassState(pass); + Require(PassPixel(pass, state).sampleLocations[0] == 0xaeu, "positions at the sample did not carry sample 0's location"); +} + +} + +int main() { + try { + ExpectSampleLocations(); + ExpectModules(); + ExpectOrderModules(); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Require(TargetHasCapability(device->Target(), spv::CapabilityInterpolationFunction), "the test device lacks sampleRateShading"); + std::uint32_t ran = 0; + for (const std::uint32_t samples : {4u, 2u}) { + const auto name = std::to_string(samples) + "x "; + Pass pass; + pass.samples = samples; + ran += Run(*device, pass, name + "PERSP_SAMPLE, POS and ANCILLARY run once per pixel") ? 1u : 0u; + pass.inputs = PerspectiveSample | Positions; + ran += Run(*device, pass, name + "PERSP_SAMPLE without ANCILLARY at sample 0") ? 1u : 0u; + pass.inputs = LinearSample | Positions | Ancillary; + ran += Run(*device, pass, name + "LINEAR_SAMPLE at sample 0") ? 1u : 0u; + pass.inputs = PerspectiveCenter | Positions | Ancillary; + ran += Run(*device, pass, name + "PERSP_CENTER once per pixel") ? 1u : 0u; + pass.inputs = Village; + pass.barycentricControl = PositionUpperLeft; + ran += Run(*device, pass, name + "positions at the upper-left corner") ? 1u : 0u; + pass.inputs = PerspectiveSample | Positions; + pass.barycentricControl = PositionAtSample; + ran += Run(*device, pass, name + "positions at sample 0") ? 1u : 0u; + pass.inputs = Village; + pass.barycentricControl = 0; + pass.quadSampleMask = {0x1u, 0x2u, 0x0u, 0x3u}; + ran += Run(*device, pass, name + "a partial quad sample mask once per pixel") ? 1u : 0u; + pass.quadSampleMask = {0xffffu, 0xffffu, 0xffffu, 0xffffu}; + pass.iterated = true; + pass.barycentricControl = PositionAtSample; + ran += Run(*device, pass, name + "an iterated shader per sample") ? 1u : 0u; + pass.inputs = PerspectiveCenter | Positions | Ancillary; + pass.barycentricControl = 0; + ran += Run(*device, pass, name + "an iterated shader with PERSP_CENTER and positions at the center") ? 1u : 0u; + pass.inputs = PerspectiveSample | Positions; + ran += Run(*device, pass, name + "an iterated shader without ANCILLARY") ? 1u : 0u; + } + Pass single; + single.samples = 1; + ran += Run(*device, single, "1x PERSP_SAMPLE, POS and ANCILLARY") ? 1u : 0u; + Require(ran >= 11u, "fewer than eleven draws ran"); + std::printf("pixel sample iteration tests passed (%u draws)\n", ran); + 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/PolygonModeDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PolygonModeDraw.cpp new file mode 100644 index 0000000000..db8de1f769 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PolygonModeDraw.cpp @@ -0,0 +1,184 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t WaveSize = 64; +constexpr std::byte Kept{0x40}; +constexpr std::byte Written{0xff}; +alignas(256) std::array Pixels{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +using Pixel = std::pair; + +constexpr std::array, 3> Corners{{{4.5f, 2.5f}, {40.5f, 2.5f}, {4.5f, 12.5f}}}; + +const std::set CornerPixels{{4, 2}, {40, 2}, {4, 12}}; +const std::set Interior{{10, 4}, {20, 4}, {15, 5}, {8, 8}}; +const std::set Outside{{50, 10}, {60, 2}, {30, 12}, {2, 2}, {4, 14}}; + +alignas(256) std::array, Corners.size()> Vertices{}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void Draw(AgcDriver::VulkanDevice& device, VkPolygonMode mode) { + Pixels.fill(Kept); + for (std::size_t index = 0; index < Corners.size(); ++index) { + Vertices[index] = {Corners[index][0] * 2.0f / Width - 1.0f, 1.0f - Corners[index][1] * 2.0f / Height, 0.5f, 1.0f}; + } + const auto target = device.Target(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Vertices.data(), 16u, static_cast(Vertices.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.polygonMode = mode; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +bool IsWritten(std::uint32_t x, std::uint32_t y) { + bool written = true; + bool kept = true; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto value = Pixels[(y * Width + x) * 4u + channel]; + written = written && value == Written; + kept = kept && value == Kept; + } + Require(written || kept, "pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is neither written nor kept"); + return written; +} + +void Expect(const std::set& pixels, bool written, std::string_view what) { + for (const auto& [x, y] : pixels) { + Require(IsWritten(x, y) == written, std::string(what) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + (written ? "kept" : "written") + ", expected " + (written ? "written" : "kept")); + } +} + +void ExpectExactly(const std::set& pixels, std::string_view what) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const bool expected = pixels.contains({x, y}); + Require(IsWritten(x, y) == expected, std::string(what) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + (expected ? "kept" : "written") + ", expected " + (expected ? "written" : "kept")); + } + } +} + +std::set StraightEdges() { + std::set pixels; + for (std::uint32_t x = 5; x <= 39; ++x) pixels.insert({x, 2}); + for (std::uint32_t y = 3; y <= 11; ++y) pixels.insert({4, y}); + return pixels; +} + +bool Unsupported(AgcDriver::VulkanDevice& device, VkPolygonMode mode) { + try { + Draw(device, mode); + return false; + } catch (const std::exception& error) { + const std::string_view message = error.what(); + if (message.find("needs the fillModeNonSolid feature") != std::string_view::npos || message.find("needs VK_KHR_maintenance5") != std::string_view::npos) { + std::printf("skipped, %s\n", error.what()); + return true; + } + throw; + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Draw(*device, VK_POLYGON_MODE_FILL); + Expect(Interior, true, "filled triangle"); + Expect(Outside, false, "filled triangle"); + if (Unsupported(*device, VK_POLYGON_MODE_POINT)) return VulkanTestSkipped; + ExpectExactly(CornerPixels, "point polygon mode"); + if (Unsupported(*device, VK_POLYGON_MODE_LINE)) return VulkanTestSkipped; + Expect(StraightEdges(), true, "line polygon mode"); + Expect(Interior, false, "line polygon mode"); + Expect(Outside, false, "line polygon mode"); + Draw(*device, VK_POLYGON_MODE_FILL); + Expect(Interior, true, "filled triangle after the point and line modes"); + std::puts("point and line polygon modes drew the triangle's corners and edges"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp index 42ac67075a..785886aa84 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp @@ -1,7 +1,10 @@ +#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 "../SlabReturn.hpp" #include #include #include @@ -12,6 +15,7 @@ #include #include #include +#include #include #include @@ -21,6 +25,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 +35,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 +116,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 +162,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 +176,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 +203,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 +240,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 +258,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); @@ -322,7 +544,7 @@ void ExactSizes(const Context& context) { } -int main() { +int main(int argc, char** argv) { try { std::unique_ptr device; try { @@ -332,8 +554,33 @@ int main() { std::printf("skipped, no usable Vulkan device: %s\n", error.what()); return VulkanTestSkipped; } + const auto& context = device->GetContext(); + const bool onlyReturns = argc == 2; + const bool hostOnly = onlyReturns && std::strcmp(argv[1], "--slab-return-host") == 0; + Require(!onlyReturns || hostOnly || std::strcmp(argv[1], "--slab-return-device") == 0, "unknown slab return test mode"); + bool host = false; + bool local = false; + for (std::uint32_t type = 0; type < context.memory.memoryTypeCount; ++type) { + const auto flags = context.memory.memoryTypes[type].propertyFlags; + const bool isHost = (flags & HostMemory) == HostMemory; + const bool isLocal = (flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0 && (flags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0; + if ((!host && isHost && (!onlyReturns || hostOnly)) || (!local && isLocal && (!onlyReturns || !hostOnly))) { + const auto before = LiveBytes; + AgcDriver::Tests::SlabReturnWithoutAllocation(context, type); + Require(LiveBytes == before && AllocationErrors == 0, "slab returns leaked or invalidly freed native Vulkan memory"); + host |= isHost; + local |= isLocal; + } + } + Require(onlyReturns ? (hostOnly ? host : local) : host, "no requested memory type for the slab return test"); + if (onlyReturns) return 0; + SlabMetadataFailures(context); RobustReadPastEnd(device->GetContext()); + DeviceBindingFailures(device->GetContext()); ExactSizes(device->GetContext()); + 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 new file mode 100644 index 0000000000..b67059d036 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp @@ -0,0 +1,163 @@ +#include "VulkanTestDevice.hpp" +#include "SceShaders.hpp" +#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 +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps); + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) const std::array VertexCode{0x7e0002ffu, 0, 0x7e0202ffu, 0, 0x7e0402ffu, 0, 0x7e0602ffu, 0x3f800000u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; +alignas(256) const std::array PixelCode{0xbf810000u}; + +struct VertexHeader { + Shader shader{}; + std::array registers{}; + ShaderSpecialRegs specials{}; + ShaderUserData users{}; + std::array context{{{0x2d5, 0x2000}}}; + + VertexHeader() { + const auto address = reinterpret_cast(VertexCode.data()); + shader.file_header = 0x34333231u; + shader.version = 0x18; + shader.header_size = sizeof(VertexHeader); + shader.shader_size = sizeof(VertexCode); + shader.code = VertexCode.data(); + shader.type = 2; + registers = {{{0xc8, static_cast(address >> 8u)}, {0xc9, static_cast(address >> 40u)}, {0x8b, 0}, {0x8a, 0}, {0xca, 0}, {0xcb, 0}, {0xcc, 0}}}; + shader.sh_registers = registers.data(); + shader.num_sh_registers = registers.size(); + shader.cx_registers = context.data(); + shader.num_cx_registers = context.size(); + shader.specials = &specials; + shader.user_data = &users; + specials.dispatch_modifier = 0x8000; + } +}; + +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(); + } +}; + +class HeldPreparationThreads { +public: + HeldPreparationThreads() { + 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() { + Release(); + } + + void Release() { + { + std::lock_guard lock(state->mutex); + state->released = true; + } + state->changed.notify_all(); + } + +private: + struct State { + std::mutex mutex; + std::condition_variable changed; + unsigned active = 0; + bool released = false; + }; + std::shared_ptr state = std::make_shared(); +}; + +void RegistrationBesidePendingLink() { + VertexHeader vertex; + VertexHeader other; + PixelHeader pixel; + AgcDriverRegisterShader_nid_postfix(&pixel.shader); + std::future link; + std::future registration; + bool registeredBeside = false; + std::array mapping{}; + { + HeldPreparationThreads held; + AgcDriverRegisterShader_nid_postfix(&vertex.shader); + link = std::async(std::launch::async, [&] { return sceAgcCreateInterpolantMapping_0100(mapping.data(), &vertex.shader, &pixel.shader); }); + std::this_thread::sleep_for(std::chrono::seconds(1)); + Require(link.wait_for(std::chrono::seconds(0)) == std::future_status::timeout, "the link did not wait for the vertex shader's registration preparation"); + registration = std::async(std::launch::async, [&] { AgcDriverRegisterShader_nid_postfix(&other.shader); }); + registeredBeside = registration.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + held.Release(); + } + registration.get(); + Require(link.get() == 0, "the interpolant mapping failed"); + Require(registeredBeside, "a link waiting for a registration preparation blocked the registration of another shader"); + for (std::uint32_t index = 0; index < mapping.size(); ++index) Require(mapping[index].offset == 0x191u + index, "the interpolant mapping wrote another register"); +} + +} + +int main() { + try { + if (!OpenVulkanTestDevice()) return VulkanTestSkipped; + RegistrationBesidePendingLink(); + AgcDriverShutdown_nid_postfix(); + std::cout << "preparation scope settle tests 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/PreparedGraphics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp index e405f2c168..56ea345d1f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp @@ -4,17 +4,22 @@ #include "SceShaders.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "Optimization/ResourceProgram.hpp" +#include "CompiledVariant.hpp" +#include "Optimization/RequestMemoryView.hpp" #include #include #include #include #include +#include #include #include #include #include #include +#include namespace { @@ -300,7 +305,13 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path registry.erase(late.snapshot->codeAddress); } ShaderRegistry invalidRegistry; - invalidRegistry[1] = {std::make_shared(ShaderSnapshot{1, 0, 2, {}, {}})}; + for (const auto& [address, snapshots] : registry) { + for (const auto& snapshot : snapshots) { + auto compute = *snapshot; + compute.type = 0; + invalidRegistry[address].push_back(std::make_shared(std::move(compute))); + } + } DrawDecode invalid{}; invalid.state = prepared.state; Reject([&] { DecodeGraphicsPrograms(invalid, queue, invalidRegistry, true, true); }, "registered"); @@ -309,6 +320,359 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path Reject([&] { DecodeGraphicsPrograms(invalid, queue, registry, true, true); }, "reserved graphics program address"); } +void PrebuildPolicyReadback(AgcDriver::VulkanDevice& device, bool disabled) { + using namespace AgcDriver::DriverDetail; + using namespace ShaderRecompiler; + constexpr std::uint32_t extent = 64u; + alignas(256) static std::array pixels{}; + alignas(256) static constexpr std::array, 3> positions{{{-1, -1, 0, 1}, {3, -1, 0, 1}, {-1, 3, 0, 1}}}; + constexpr std::array vertexCode{0xe0382000u, 0x80000005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; + constexpr std::array pixelCode{0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u}; + DrawDecode decoded{}; + decoded.state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, {}, {}}; + decoded.pixel.wave32 = true; + decoded.pixel.targetOutputMode[0] = 9u; + decoded.pixel.targetExportMapping.fill(0xe4u); + const auto append = [&](ShaderStage stage, std::span code, std::vector users) { + auto snapshot = std::make_shared(); + snapshot->codeAddress = reinterpret_cast(code.data()); + snapshot->headerAddress = 0u; + snapshot->type = stage == ShaderStage::Vertex ? 2u : 1u; + snapshot->code.assign(code.begin(), code.end()); + DrawProgram program{{stage, snapshot->codeAddress, snapshot->code, 0u, {}}, 0u, 0u, std::move(users), {{{snapshot->codeAddress, std::as_bytes(std::span(snapshot->code))}, {0u, {}}}}, snapshot, 0u}; + decoded.programs.push_back(std::move(program)); + decoded.roles.push_back(stage == ShaderStage::Vertex ? ProgramRole::Main : ProgramRole::Fragment); + }; + const auto address = reinterpret_cast(positions.data()); + append(ShaderStage::Vertex, vertexCode, {static_cast(address), static_cast(address >> 32u) | (16u << 16u), 3u, 0x01016facu}); + append(ShaderStage::Fragment, pixelCode, {}); + const auto target = device.Target(); + const auto stages = PrepareGraphicsStages(decoded, target); + Require(stages.size() == 2u, "prebuild policy lost a required graphics stage"); + for (const auto& stage : stages) { + Require(stage.entry.handle != nullptr && stage.snapshot != nullptr, "prebuild policy lost a required source handle"); + Require(disabled ? stage.registration == nullptr : stage.registration != nullptr, "prebuild policy retained or skipped registration-default materialization"); + stage.snapshot->prepared->entries.push_back(stage.entry); + } + std::vector linked; + std::vector memory; + for (std::size_t index = 0u; index < decoded.programs.size(); ++index) { + const auto& program = decoded.programs[index]; + linked.push_back({decoded.roles[index], program.binary, program.userDataBase, program.firstUserSgpr, program.userData}); + memory.insert(memory.end(), program.memory.begin(), program.memory.end()); + } + std::vector> owners; + std::vector shaders; + for (std::size_t index = 0u; index < decoded.programs.size(); ++index) { + const auto& program = decoded.programs[index]; + const auto& stage = stages[index]; + const bool pixel = stage.stage == ShaderStage::Fragment; + std::optional vertex; + if (!pixel) { + vertex = AgcDriver::Graphics::DecodeVertexStageInfo({}, 0u, program.userData, nullptr, true, 8u); + vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), 0u, program.binary.code, 0u); + } + const auto room = AgcDriver::Graphics::PipelinePushSlotBytes - stage.pushOffset % AgcDriver::Graphics::PipelinePushSlotBytes; + Require(stage.pushOffset % sizeof(std::uint32_t) == 0u && stage.entry.handle->artifact->bindings.pushConstantSizeBytes <= room, "prebuild policy changed the prepared push constant range"); + RecompileRequest request{program.binary, {32u, 0u, program.userData, {}, pixel ? std::optional(decoded.pixel) : std::nullopt, vertex, memory}, target, {0u, 0u, stage.pushOffset, room}, GraphicsCompileContext{0u, linked, {}, {}, {0u, 3u, 4u, 1u}}}; + const auto invocation = InvocationFor(*program.snapshot, 0u, request); + const auto capture = invocation.Capture(SrtRuntime{program.userData, program.binary.codeAddress}); + Require(capture->source == stage.entry.handle->source && program.snapshot->prepared->entries.front().handle == stage.entry.handle, "prebuild policy replaced required prepared source ownership"); + owners.push_back(invocation.Materialize(*capture)); + Require(owners.back()->variantId == GetPreparedArtifact(*stage.entry.handle).variantId && program.snapshot->prepared->entries.size() == 1u && owners.back()->pushConstants.size() <= room, "prebuild policy changed the prepared shader ABI or ownership"); + shaders.push_back({stage.stage, owners.back().get(), stage.pushOffset}); + } + auto state = decoded.state; + pixels.fill(0x40404040u); + state.color = {reinterpret_cast(pixels.data()), {extent, extent}, VK_FORMAT_R8G8B8A8_UNORM, sizeof(pixels), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {extent, extent}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(extent), static_cast(extent), -static_cast(extent), 0, 1}; + state.scissor = {{0u, 0u}, {extent, extent}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.negativeOneToOne = false; + state.blend.colorWriteMask = 15u; + state.blends = {state.blend}; + device.Draw(state, {0u, 3u, 0u, 1u, 0u, false}, shaders); + device.WaitIdle(); + Require(std::ranges::all_of(pixels, [](const auto value) { return value == 0xffffffffu; }), "prebuild policy changed native prepared-shader readback"); +} + +void RetainedPixelMatchesNullProgram(AgcDriver::VulkanDevice& device) { + using namespace AgcDriver::DriverDetail; + Fixture front; + Fixture retained; + front.Initialize(2u); + retained.Initialize(1u); + const auto null = std::make_shared(PrepareNullPixelProgram(device)); + ShaderRegistry registry; + for (const auto& snapshot : {front.snapshot, retained.snapshot, null}) registry.emplace(snapshot->codeAddress, std::vector>{snapshot}); + AgcDriver::QueueState queue{}; + queue.context[0x8e] = queue.context[0x8f] = queue.context[0x203] = 0; + queue.context[0x2dc] = 0xaa00; + queue.context[0x1c4] = 1; + front.Bind(queue, 0xc8u, 0x8bu); + queue.shader[0x8bu] = 0; + retained.Bind(queue, 0x008u, 0x00bu); + queue.shader[0x00bu] = (31u << 1u) | (1u << 27u); + for (std::uint32_t i = 0; i < 30; ++i) queue.shader[0x00cu + i] = 0xdead0000u + i; + queue.context[0x1b6] = queue.context[0x1b3] = queue.context[0x1b4] = queue.context[0x1c5] = 0xffffffffu; + auto captured = queue; + captured.shader[0x00bu] = 20u << 1u; + DrawDecode capturedDraw{}; + capturedDraw.state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + DecodeGraphicsPrograms(capturedDraw, captured, registry, false, true); + Require(capturedDraw.programs.back().snapshot == null && capturedDraw.programs.back().userData.empty(), "a retained pixel program with a valid 20-word resource count consumed unrelated userdata"); + for (const auto wave : {64u, 32u}) { + DrawDecode draw{}; + draw.state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + draw.state.stages.vertexWaveSize = 32u; + draw.state.stages.fragmentWaveSize = wave; + std::vector reads; + AgcDriver::Graphics::RegisterReadLog() = &reads; + try { + DecodeGraphicsPrograms(draw, queue, registry, false, true); + draw.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, {}, AgcDriver::Graphics::PixelProgramSkipped(queue)); + } catch (...) { + AgcDriver::Graphics::RegisterReadLog() = nullptr; + throw; + } + AgcDriver::Graphics::RegisterReadLog() = nullptr; + Require(draw.roles.size() == 2 && draw.roles.back() == ShaderRecompiler::ProgramRole::Fragment, "null substitution changed stage linking"); + Require(draw.programs.back().snapshot == null && draw.programs.back().userData.empty(), "an unused retained pixel program consumed unrelated userdata"); + Require(draw.pixel.interpolatorCount == 0 && !draw.pixel.depthExportEnable && !draw.pixel.stencilRefExportEnable && !draw.pixel.sampleMaskExportEnable && !draw.pixel.pixelKillEnable, "the substituted pixel program inherited stale inputs or exports"); + for (const auto mode : draw.pixel.targetOutputMode) Require(mode == 0, "the substituted pixel program inherited stale color formats"); + for (const auto read : reads) { + Require(AgcDriver::Graphics::DrawKeyCovers(read), "a pixel substitution dependency is absent from the draw key"); + Require(read.bank != AgcDriver::Graphics::RegisterBank::Shader || read.offset < 0x00bu || read.offset > 0x02bu, "the substituted program read fragment resources or userdata"); + } + const auto stages = PrepareGraphicsStages(draw, device.Target()); + Require(stages.size() == 2 && stages.back().entry.handle != nullptr, "a retained-format null program was not prepared"); + std::vector linked; + std::vector memory; + for (std::size_t i = 0; i < draw.programs.size(); ++i) { + const auto& program = draw.programs[i]; + linked.push_back({draw.roles[i], program.binary, program.userDataBase, program.firstUserSgpr, program.userData}); + memory.insert(memory.end(), program.memory.begin(), program.memory.end()); + } + const auto& fragment = draw.programs.back(); + const auto offset = stages.back().pushOffset; + const auto room = AgcDriver::Graphics::PipelinePushSlotBytes - offset % AgcDriver::Graphics::PipelinePushSlotBytes; + ShaderRecompiler::RecompileRequest request{fragment.binary, {wave, 0, {}, {}, draw.pixel, {}, memory, RegisteredFloatMode(*null)}, device.Target(), {0, 0, offset, room}, ShaderRecompiler::GraphicsCompileContext{0, linked, {}, {}, {}}}; + static_cast(InvocationFor(*null, fragment.codeOffset, request)); + const auto handle = SourceHandleFor(*null, fragment.codeOffset, request); + const auto entries = null->prepared->entries.size(); + static_cast(InvocationFor(*null, fragment.codeOffset, request)); + Require(null->prepared->entries.size() == entries && SourceHandleFor(*null, fragment.codeOffset, request) == handle, "a repeated null program did not reuse its prepared variant"); + } + queue.shader[0x00bu] = 2; + queue.shader[0x00cu] = 0x12345678u; + for (const auto trigger : {0u, 1u, 2u}) { + auto active = queue; + if (trigger == 0) active.context[0x8e] = active.context[0x8f] = 0xf; + if (trigger == 1) active.context[0x203] = 0x400; + if (trigger == 2) active.context[0x2dc] = 0xaa01; + DrawDecode draw{}; + draw.state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + DecodeGraphicsPrograms(draw, active, registry, false, true); + Require(draw.programs.back().snapshot == retained.snapshot && draw.programs.back().userData == std::vector{0x12345678u}, "an enabled consumer did not restore the retained pixel program"); + } +} + +void CheckMergedHull(AgcDriver::VulkanDevice& device) { + using namespace AgcDriver::DriverDetail; + using namespace ShaderRecompiler; + Fixture merged; + Fixture domain; + Fixture fragment; + merged.Initialize(3u); + domain.Initialize(2u); + fragment.Initialize(1u); + ShaderRegistry registry; + for (const auto* fixture : {&merged, &domain, &fragment}) registry.emplace(fixture->snapshot->codeAddress, std::vector>{fixture->snapshot}); + AgcDriver::QueueState queue{}; + queue.context[0x8e] = 0xfu; + queue.context[0x8f] = 0xfu; + merged.Bind(queue, 0x148u, 0x10bu); + domain.Bind(queue, 0xc8u, 0x8bu); + fragment.Bind(queue, 0x8u, 0xbu); + const auto unset = queue; + const auto null = [&] { + auto state = queue; + state.shader[0x102u] = 0u; + state.shader[0x103u] = 0u; + return state; + }(); + alignas(8) const std::array pointer{}; + const auto pointerAddress = reinterpret_cast(pointer.data()); + queue.shader[0x102u] = static_cast(pointerAddress); + queue.shader[0x103u] = static_cast(pointerAddress >> 32u); + DrawDecode prepared{}; + prepared.state.stages.path = AgcDriver::Graphics::ShaderPath::Tessellation; + prepared.state.stages.vertexWaveSize = 64u; + prepared.state.stages.fragmentWaveSize = 32u; + prepared.state.stages.tessellation = TessellationConfiguration{3, 4, 1, 2, 2}; + prepared.pixel.wave32 = true; + prepared.pixel.targetOutputMode[0] = 9; + prepared.pixel.targetExportMapping.fill(0xe4u); + DecodeGraphicsPrograms(prepared, queue, registry, true, true); + DrawDecode draw{}; + draw.state = prepared.state; + draw.state.stages.tessellation->mergedHull = false; + draw.pixel = prepared.pixel; + DecodeGraphicsPrograms(draw, queue, registry, false, true); + for (const auto* decoded : {&prepared, &draw}) { + Require(decoded->state.stages.tessellation->mergedHull, "a registered type-3 hull program was not decoded as a merged hull"); + Require(decoded->roles == std::vector{ProgramRole::Local, ProgramRole::Hull, ProgramRole::Domain, ProgramRole::Fragment}, "a merged hull program did not give the local and hull roles"); + const auto& local = decoded->programs[0]; + const auto& hull = decoded->programs[1]; + Require(local.snapshot == merged.snapshot && hull.snapshot == merged.snapshot && local.codeOffset == 64u && hull.codeOffset == 64u, "the local and hull stages of a merged hull program refer to another program"); + Require(local.binary.stage == ShaderStage::Local && hull.binary.stage == ShaderStage::TessellationControl && local.firstUserSgpr == 8u && hull.firstUserSgpr == 0u && hull.userData.size() == local.userData.size() + 8u, "a merged hull program's stages lost the merged user SGPR layout"); + } + Require(draw.programs[1].userData[0] == static_cast(pointerAddress) && draw.programs[1].userData[1] == static_cast(pointerAddress >> 32u), "a merged hull draw did not pass its user-data address"); + Require(UserDataWords(draw.programs, draw.roles, 0x10eu) == std::vector>{{0u, 2u}, {1u, 10u}}, "an indirect draw's user-data register does not reach both stages of a merged hull program"); + std::vector programMemory; + AppendProgramMemory(programMemory, draw.programs); + Require(programMemory.size() == 6u, "a merged hull draw's program memory does not list each code and header region once"); + try { + const RequestMemoryView view(programMemory); + } catch (const std::exception& error) { + throw std::runtime_error(std::string("a merged hull draw's program memory is not a valid request memory: ") + error.what()); + } + for (const auto* state : {&unset, &null}) { + DrawDecode without{}; + without.state = prepared.state; + without.pixel = prepared.pixel; + DecodeGraphicsPrograms(without, *state, registry, false, true); + Require(without.roles == draw.roles && without.programs[1].userData.size() == draw.programs[1].userData.size() && without.programs[1].userData[0] == 0u && without.programs[1].userData[1] == 0u, "a merged hull draw without a user-data address was not decoded with zero s[0:1]"); + } + auto unmapped = queue; + unmapped.shader[0x102u] = 0x10u; + unmapped.shader[0x103u] = 0u; + DrawDecode stale{}; + stale.state = prepared.state; + stale.pixel = prepared.pixel; + DecodeGraphicsPrograms(stale, unmapped, registry, false, true); + Require(stale.programs[1].userData[0] == 0x10u && stale.programs[1].userData[1] == 0u, "a merged hull draw did not pass a user-data address it does not need unchecked"); + const auto stages = PrepareGraphicsStages(prepared, device.Target()); + Require(stages.size() == 4u && stages[0].entry.handle != stages[1].entry.handle, "a merged hull program was not prepared as separate local and hull stages"); + for (const auto& stage : stages) stage.snapshot->prepared->entries.push_back(stage.entry); + std::vector linked; + std::vector memory; + AppendProgramMemory(memory, draw.programs); + for (std::size_t index = 0; index < draw.programs.size(); ++index) { + const auto& program = draw.programs[index]; + linked.push_back({draw.roles[index], program.binary, program.userDataBase, program.firstUserSgpr, program.userData}); + } + const auto predicted = merged.snapshot->prepared->entries.size(); + for (std::size_t index = 0; index < 2u; ++index) { + const auto& program = draw.programs[index]; + const auto vertex = AgcDriver::Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true); + RecompileRequest request{program.binary, {64u, program.firstUserSgpr, program.userData, {}, std::nullopt, vertex, memory}, device.Target(), {0, 0, 0, 128u}, GraphicsCompileContext{program.firstUserSgpr, linked, std::nullopt, draw.state.stages.tessellation, {0, 3, 4, 1}}}; + static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); + Require(merged.snapshot->prepared->entries.size() == predicted, "a merged hull stage prepared at ABI resolution was prepared again at its draw"); + request.graphics->tessellation->mergedHull = false; + Require(!MatchesPreparedShader(request, *stages[index].entry.handle), "a separate hull configuration matched a merged hull artifact"); + } + Fixture front; + front.Initialize(5u); + registry.emplace(front.snapshot->codeAddress, std::vector>{front.snapshot}); + Fixture back; + back.Initialize(7u); + registry.emplace(back.snapshot->codeAddress, std::vector>{back.snapshot}); + auto separate = queue; + front.Bind(separate, 0x148u, 0x10bu); + back.Bind(separate, 0x108u, 0x10bu); + DrawDecode halves{}; + halves.state = prepared.state; + halves.pixel = prepared.pixel; + DecodeGraphicsPrograms(halves, separate, registry, false, true); + Require(!halves.state.stages.tessellation->mergedHull && halves.programs[0].snapshot == front.snapshot && halves.programs[1].snapshot == back.snapshot, "separate hull halves were decoded as a merged hull program"); + auto halvesUnset = unset; + front.Bind(halvesUnset, 0x148u, 0x10bu); + back.Bind(halvesUnset, 0x108u, 0x10bu); + DrawDecode halvesWithout{}; + halvesWithout.state = prepared.state; + halvesWithout.pixel = prepared.pixel; + Reject([&] { DecodeGraphicsPrograms(halvesWithout, halvesUnset, registry, false, true); }, "merged shader user-data address is null"); +} + +void CheckMergedHullVertexTables(AgcDriver::VulkanDevice& device) { + using namespace AgcDriver::DriverDetail; + using namespace ShaderRecompiler; + struct Header { + Shader shader{}; + ShaderUserData users{}; + std::array direct{}; + ShaderSemantic semantic{}; + } header; + alignas(256) std::array code{}; + code.fill(0xffffffffu); + code.back() = 0xbf810000u; + header.shader.type = 3; + header.shader.user_data = &header.users; + header.direct.fill(0xffffu); + header.direct[8] = 10u; + header.direct[10] = 12u; + header.users.direct_resource_offset = header.direct.data(); + header.users.direct_resource_count = static_cast(header.direct.size()); + header.semantic.size_in_elements = 4u; + header.shader.input_semantics = &header.semantic; + header.shader.num_input_semantics = 1u; + auto merged = std::make_shared(); + merged->type = 3; + merged->codeAddress = reinterpret_cast(code.data()); + merged->headerAddress = reinterpret_cast(&header); + merged->code.assign(code.begin(), code.end()); + merged->header.resize(sizeof(header)); + std::memcpy(merged->header.data(), &header, sizeof(header)); + Fixture domain; + Fixture fragment; + domain.Initialize(2u); + fragment.Initialize(1u); + ShaderRegistry registry; + registry.emplace(merged->codeAddress, std::vector>{merged}); + for (const auto* fixture : {&domain, &fragment}) registry.emplace(fixture->snapshot->codeAddress, std::vector>{fixture->snapshot}); + AgcDriver::QueueState queue{}; + queue.context[0x8e] = 0xfu; + queue.context[0x8f] = 0xfu; + const auto entry = merged->codeAddress + 256u; + queue.shader[0x148u] = static_cast(entry >> 8u); + queue.shader[0x149u] = static_cast(entry >> 40u); + queue.shader[0x10bu] = 16u << 1u; + domain.Bind(queue, 0xc8u, 0x8bu); + fragment.Bind(queue, 0x8u, 0xbu); + alignas(16) const std::array bufferTable{}; + alignas(16) const std::array attributeTable{}; + const auto bufferAddress = reinterpret_cast(bufferTable.data()); + const auto attributeAddress = reinterpret_cast(attributeTable.data()); + for (std::uint32_t slot = 2u; slot < 6u; ++slot) queue.shader[0x10cu + slot] = slot % 2u == 0u ? 0x12345600u : 0x00000077u; + queue.shader[0x10cu + 10u] = static_cast(bufferAddress); + queue.shader[0x10cu + 11u] = static_cast(bufferAddress >> 32u); + queue.shader[0x10cu + 12u] = static_cast(attributeAddress); + queue.shader[0x10cu + 13u] = static_cast(attributeAddress >> 32u); + DrawDecode draw{}; + draw.state.stages.path = AgcDriver::Graphics::ShaderPath::Tessellation; + draw.state.stages.vertexWaveSize = 64u; + draw.state.stages.fragmentWaveSize = 32u; + draw.state.stages.tessellation = TessellationConfiguration{3, 3, 1, 2, 2}; + draw.pixel.wave32 = true; + draw.pixel.targetOutputMode[0] = 9; + draw.pixel.targetExportMapping.fill(0xe4u); + DecodeGraphicsPrograms(draw, queue, registry, false, true); + Require(draw.state.stages.tessellation->mergedHull && draw.programs[1].firstUserSgpr == 0u, "the merged hull draw with vertex tables was not decoded as a merged hull"); + for (std::size_t index = 0; index < 2u; ++index) { + const auto& program = draw.programs[index]; + std::vector reads; + const auto vertex = AgcDriver::Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &reads, false, 8u - program.firstUserSgpr); + Require(vertex.fetchEmbedded && vertex.fetchAttribReg == 12u && vertex.fetchBufferReg == 10u && vertex.resourcesNum == 1u, "a merged hull stage did not decode its vertex tables"); + Require(reads.size() == 2u && reads[0].address == attributeAddress && reads[1].address == bufferAddress, "a merged hull stage read its vertex tables from user-data words other than its slots"); + } +} + void NullPixelMatchesRegistration(AgcDriver::VulkanDevice& device) { using namespace AgcDriver::DriverDetail; Fixture front; @@ -354,11 +718,23 @@ int main(int argc, char** argv) { auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; Require(argc <= 2, "invalid test arguments"); + if (argc == 2 && std::string_view(argv[1]).starts_with("--prebuild-")) { + const auto mode = std::string_view(argv[1]).substr(11u); + const auto* value = std::getenv("APS5_NO_LIBRARY_PREBUILD"); + Require(mode == "unset" ? value == nullptr : (mode == "0" || mode == "1") && value != nullptr && mode == value, "prebuild policy test has the wrong startup environment"); + if (!device->Target().registrationModules) return VulkanTestSkipped; + PrebuildPolicyReadback(*device, mode == "1"); + std::cout << "prebuild policy native readback passed\n"; + return 0; + } const auto dump = argc == 2 ? std::filesystem::path(argv[1]) : std::filesystem::path{}; + RetainedPixelMatchesNullProgram(*device); NullPixelMatchesRegistration(*device); Check(*device, AgcDriver::Graphics::ShaderPath::Vertex, dump); Check(*device, AgcDriver::Graphics::ShaderPath::Geometry, dump); Check(*device, AgcDriver::Graphics::ShaderPath::Tessellation, dump); + CheckMergedHull(*device); + CheckMergedHullVertexTables(*device); std::cout << "prepared graphics ABI tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp index d0a96ffef6..beaed11d60 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -152,6 +153,73 @@ void HeaderSelection() { Reject([&] { static_cast(RegisteredProgram(*registry, 0x10000, {2, 4})); }); } +void RegistryVersions() { + using Expected = std::map>; + const auto matches = [](const ShaderRegistry& current, const Expected& expected) { + const auto same = [](const auto& entry, const auto& wanted) { return entry.first == wanted.first && entry.second.size() == 1u && entry.second.front() == wanted.second; }; + if (current.size() != expected.size() || !std::equal(current.begin(), current.end(), expected.begin(), expected.end(), same)) return false; + auto backward = current.end(); + for (auto wanted = expected.rbegin(); wanted != expected.rend(); ++wanted) { + if (backward == current.begin() || !same(*--backward, *wanted)) return false; + } + if (backward != current.begin()) return false; + for (const auto& entry : expected) { + for (const auto probe : {std::uint64_t{0}, entry.first - 1u, entry.first, entry.first + 4u, ~std::uint64_t{0}}) { + const auto found = current.upper_bound(probe); + const auto wanted = expected.upper_bound(probe); + if ((found == current.end()) != (wanted == expected.end()) || (found != current.end() && found->first != wanted->first)) return false; + if (current.contains(probe) != expected.contains(probe) || (current.find(probe) == current.end()) == expected.contains(probe)) return false; + } + } + return true; + }; + const auto makeSnapshot = [](std::uint64_t codeAddress) { + auto snapshot = std::make_shared(); + snapshot->codeAddress = codeAddress; + snapshot->headerAddress = codeAddress + 0x80u; + snapshot->code = {0xbf810000u}; + return snapshot; + }; + std::vector> snapshots; + Expected expected; + std::shared_ptr registry; + std::uint64_t state = 1; + while (snapshots.size() < 4096u) { + state = state * 6364136223846793005ull + 1442695040888963407ull; + const auto address = 0x100000u + ((state >> 33u) % 0x100000u) * 0x100u; + if (expected.contains(address)) continue; + const auto snapshot = makeSnapshot(address); + expected.emplace(address, snapshot); + snapshots.push_back(snapshot); + PublishRegisteredShader(registry, snapshot); + } + Require(matches(*registry, expected), "the shader registry did not keep its code addresses in order"); + std::shared_ptr earlier = registry; + const auto previous = expected; + const auto added = makeSnapshot(0x100040u); + expected.emplace(added->codeAddress, added); + std::vector uses; + for (const auto& snapshot : snapshots) uses.push_back(snapshot.use_count()); + PublishRegisteredShader(registry, added); + std::size_t copied = 0; + for (std::size_t index = 0; index < snapshots.size(); ++index) copied += snapshots[index].use_count() != uses[index] ? 1u : 0u; + Require(copied <= 128u, "publishing a shader copied the registry's entries for other code addresses"); + Require(matches(*registry, expected) && matches(*earlier, previous), "publishing a shader changed an earlier registry or lost an entry"); + ShaderRegistry edited = *registry; + auto remaining = expected; + std::size_t index = 0; + for (const auto& entry : expected) { + if (index++ % 3u != 0u) continue; + Require(edited.erase(entry.first) == 1u && edited.erase(entry.first) == 0u, "the shader registry did not erase a code address once"); + remaining.erase(entry.first); + } + Require(matches(edited, remaining) && matches(*registry, expected), "erasing from a copy of the registry changed the original or lost an entry"); + for (const auto& entry : remaining) edited.erase(entry.first); + Require(edited.empty() && edited.begin() == edited.end() && edited.upper_bound(0) == edited.end(), "an emptied shader registry kept entries"); + Require(edited.emplace(added->codeAddress, ShaderRegistry::mapped_type{added}).second && !edited.emplace(added->codeAddress, ShaderRegistry::mapped_type{}).second, "an emptied shader registry did not take a new entry once"); + Require(edited.size() == 1u && edited.at(added->codeAddress).size() == 1u && edited.at(added->codeAddress).front() == added, "an emptied shader registry lost its new entry"); +} + void FusedHullHeaderSelection() { struct Hull { Shader shader{}; @@ -344,6 +412,7 @@ int main() { try { Transactions(); HeaderSelection(); + RegistryVersions(); FusedHullHeaderSelection(); TransactionLock(); PreparationPriority(false); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index c4fb6eba1f..734a7c6825 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -1,10 +1,17 @@ #include "VulkanTestDevice.hpp" #include "Optimization/ResourceProgram.hpp" #include "ControlFlow/RequestSerializer.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #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/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp" +#include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/AsyncLog.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 +34,15 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#else +#include +#endif namespace { @@ -193,8 +209,10 @@ void FailureCapture(AgcDriver::VulkanDevice& device) { const auto expected = failure([&] { static_cast(PrepareShader(request)); }); const char* previous = std::getenv("APS5_DUMP_SHADERS"); const std::string saved = previous != nullptr ? previous : ""; - const std::string path = "shader_12345cafe.req"; + const std::string path = "shader_12345cafe.prepare.req"; + const std::string capturePath = "shader_12345cafe.req"; std::remove(path.c_str()); + std::remove(capturePath.c_str()); #ifdef _WIN32 Require(_putenv_s("APS5_DUMP_SHADERS", "") == 0, "cannot disable shader capture"); #else @@ -215,6 +233,14 @@ void FailureCapture(AgcDriver::VulkanDevice& device) { std::ostringstream dumpedPrefix; dumpedPrefix << "shader 0x" << std::hex << request.shader.codeAddress << " stage " << static_cast(request.shader.stage) << " (" << path << "): "; Require(actual == dumpedPrefix.str() + expected, "enabled shader capture changed the diagnosed failure"); + Require(!std::ifstream(capturePath, std::ios::binary).is_open(), "a preparation failure was dumped as a dispatch request"); + Require(AgcDriver::DriverDetail::Driver::dumpRequest(request.shader.codeAddress, request) == capturePath, "a dispatch request dump has the wrong name"); + std::ifstream capture(capturePath, std::ios::binary); + std::ostringstream captureText; + captureText << capture.rdbuf(); + capture.close(); + std::remove(capturePath.c_str()); + Require(captureText.str() == RequestSerializer{}.Serialize(request), "an earlier preparation failure dump suppressed the dispatch request dump"); #ifdef _WIN32 Require(_putenv_s("APS5_DUMP_SHADERS", saved.c_str()) == 0, "cannot restore shader capture"); #else @@ -304,6 +330,31 @@ std::exception_ptr InjectedFailure() { } } +void AwaitedRegistrationPreparation() { + std::atomic ran{false}; + AgcDriver::DriverDetail::SubmitRegistrationPreparation([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + ran.store(true, std::memory_order_release); + }); + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); + Require(ran.load(std::memory_order_acquire), "a submission did not wait for the registration preparation queued before it"); + std::atomic slow{false}; + std::atomic fast{false}; + AgcDriver::DriverDetail::SubmitRegistrationPreparation([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + slow.store(true, std::memory_order_release); + }); + auto waiter = std::async(std::launch::async, [&] { + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); + return slow.load(std::memory_order_acquire); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + AgcDriver::DriverDetail::SubmitRegistrationPreparation([&] { fast.store(true, std::memory_order_release); }); + Require(waiter.get(), "a submission stopped waiting for an earlier preparation once a later one finished"); + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); + Require(fast.load(std::memory_order_acquire), "the later preparation did not run"); +} + void PendingRegistrationPreparation(AgcDriver::VulkanDevice& device) { alignas(256) const std::array code{0xbf810000u}; std::array users{}; @@ -575,7 +626,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'; @@ -587,6 +638,107 @@ 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); + AgcDriverResolveShaderAbi_nid_postfix(&header.shader, {}, {}); + { + AgcDriver::DriverDetail::ShaderPreparationTransaction transaction; + const auto& state = transaction.Read(*registered()); + Require(state.deferred && state.entries.empty(), "the pipeline ABI of a lazily registered shader it cannot prepare did not leave it to its dispatch"); + } + 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 { @@ -643,11 +795,498 @@ void DeferredUndecodableRegistration() { ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, "unsupported MIMG opcode"); } +class StderrCapture { +public: + StderrCapture() { + AgcDriver::FlushAsyncLog_nid_no_patch(); + std::fflush(stderr); + file = std::tmpfile(); + Require(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + _dup2(_fileno(file), _fileno(stderr)); +#else + saved = dup(fileno(stderr)); + dup2(fileno(file), fileno(stderr)); +#endif + } + std::string Finish() { + AgcDriver::FlushAsyncLog_nid_no_patch(); + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + +template +std::string Logged(TAction action) { + StderrCapture capture; + try { + action(); + } catch (...) { + static_cast(capture.Finish()); + throw; + } + return capture.Finish(); +} + +std::size_t Deferrals(const std::string& log) { + constexpr std::string_view line = "could not be prepared when the title created its pipeline"; + std::size_t count = 0; + for (auto found = log.find(line); found != std::string::npos; found = log.find(line, found + line.size())) ++count; + return count; +} + +struct AbiHeader { + Shader shader{}; + std::array registers{}; + std::array context{}; + ShaderSpecialRegs specials{}; + ShaderUserData users{}; +}; + +std::uint32_t ProgramLow(const void* code) { + return static_cast(reinterpret_cast(code) >> 8u); +} + +std::uint32_t ProgramHigh(const void* code) { + return static_cast(reinterpret_cast(code) >> 40u); +} + +template +void DescribeAbi(AbiHeader& header, const std::array& code, std::uint8_t type, std::initializer_list registers, std::initializer_list context) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18; + header.shader.header_size = sizeof(AbiHeader); + header.shader.shader_size = sizeof(code); + header.shader.code = code.data(); + header.shader.type = type; + header.shader.user_data = &header.users; + std::copy(registers.begin(), registers.end(), header.registers.begin()); + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = static_cast(registers.size()); + std::copy(context.begin(), context.end(), header.context.begin()); + header.shader.cx_registers = header.context.data(); + header.shader.num_cx_registers = static_cast(context.size()); +} + +alignas(256) constexpr std::array UserDataCaller{ + 0x7e020280u, 0xbe940383u, 0xbe8e0300u, 0xbe8f0301u, 0xbe8e210eu, 0xbe8e0302u, 0xbe8f0303u, 0xbe8e210eu, + 0x80948114u, 0xbf078014u, 0xbf85fff7u, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u, 0xbf9f0000u, + 0x30306c73u, 0x00000040u, 0x00000008u}; +alignas(256) constexpr std::array AddFiveCallee{0x4a020285u, 0xbe80200eu}; +alignas(256) constexpr std::array AddSixtyFourCallee{0x4a0202c0u, 0xbe80200eu}; +alignas(256) std::array CallerOutput{}; +alignas(256) constexpr std::array WaterfallStoreCode{ + 0x7e020280u, 0x7e040280u, 0x7e080280u, 0x7e0a0280u, 0x7e0c0280u, 0x7e0e0280u, 0xbe86047eu, 0xf40c0200u, + 0xfa000000u, 0xd4c10000u, 0x00020083u, 0xbf8cc07fu, 0xf0200f08u, 0x00020401u, 0x8afe007eu, 0xbf89fff7u, + 0xbefe0406u, 0xbf810000u}; +alignas(256) constexpr std::array WaterfallPixelCode{ + 0xc80a0002u, 0x7e000280u, 0x7e020280u, 0xbe86047eu, 0xf40c0200u, 0xfa000000u, 0xd4c10000u, 0x00020483u, + 0xbf8cc07fu, 0xf0000f08u, 0x00020400u, 0x8afe007eu, 0xbf89fff7u, 0xbefe0406u, 0xbf8c3f70u, 0xf800180fu, + 0x07060504u, 0xbf810000u}; +alignas(256) constexpr std::array WaterfallVertexCode{ + 0x7e020280u, 0x7e040280u, 0xbe86037eu, 0xf40c0404u, 0xfa000000u, 0xd4c10008u, 0x00020083u, 0xbf8cc07fu, + 0xf0000f08u, 0x00040401u, 0x8a7e087eu, 0xbf89fff7u, 0xbefe0306u, 0xbf8a0000u, 0xbf8c3f70u, 0xf80008cfu, + 0x07060504u, 0xbf810000u}; +alignas(256) constexpr std::array OtherWaterfallVertexCode = WaterfallVertexCode; +alignas(256) constexpr std::array BarrierFreeWaterfallVertexCode{ + 0x7e020280u, 0x7e040280u, 0xbe86037eu, 0xf40c0404u, 0xfa000000u, 0xd4c10008u, 0x00020083u, 0xbf8cc07fu, + 0xf0000f08u, 0x00040401u, 0x8a7e087eu, 0xbf89fff7u, 0xbefe0306u, 0xbf800000u, 0xbf8c3f70u, 0xf80008cfu, + 0x07060504u, 0xbf810000u}; +alignas(256) constexpr std::array FrontCode{0x7e0002ffu, 0, 0x7e0202ffu, 0, 0x7e0402ffu, 0, 0x7e0602ffu, 0x3f800000u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; +alignas(256) constexpr std::array UndecodablePixelCode{0xf1949f05u, 0x00060018u, 0xbf810000u}; +alignas(256) constexpr std::array PassthroughPixelCode{0xc8020002u, 0xf800180fu, 0x00000000u, 0xbf810000u}; + +void DescribeWaterfallPixel(AbiHeader& pixel) { + DescribeAbi(pixel, WaterfallPixelCode, 1, {{0x8, ProgramLow(WaterfallPixelCode.data())}, {0x9, ProgramHigh(WaterfallPixelCode.data())}, {0xa, 0}, {0xb, 2u << 1u}}, {{0x1b6, 1}, {0x1b3, 2}, {0x1b4, 2}, {0x191, 0}, {0x1c5, 4}}); +} + +void DescribeFront(AbiHeader& vertex) { + DescribeAbi(vertex, FrontCode, 2, {{0xc8, ProgramLow(FrontCode.data())}, {0xc9, ProgramHigh(FrontCode.data())}, {0x8b, 0}, {0x8a, 0}, {0xca, 0}, {0xcb, 0}, {0xcc, 0}}, {{0x2d5, 0x2000}}); + vertex.shader.specials = &vertex.specials; + vertex.specials.dispatch_modifier = 0x8000; +} + +void DescribeWaterfallVertex(AbiHeader& vertex, const std::array& code) { + DescribeAbi(vertex, code, 2, {{0xc8, ProgramLow(code.data())}, {0xc9, ProgramHigh(code.data())}, {0x8a, 0}, {0x8b, 2u << 1u}}, {{0x1ff, 64}, {0x2ab, 4}, {0x2ce, 0}}); + vertex.shader.specials = &vertex.specials; + vertex.specials = {{0x25b, 0x8040}, {0x2d5, 0x00402000}, 0, {}, {}, {0x29b, 2}, {0x260, 0}}; +} + +struct FetchingHeader { + AbiHeader abi; + std::array directOffsets{}; + ShaderSemantic semantic{}; +}; + +void DescribeStaticFetch(FetchingHeader& vertex) { + vertex.abi.shader.header_size = sizeof(FetchingHeader); + for (auto& reg : vertex.abi.registers) { + if (reg.offset == 0x8b) reg.value = 4u << 1u; + } + vertex.directOffsets.fill(0xffffu); + vertex.directOffsets[8] = 0; + vertex.directOffsets[10] = 2; + vertex.abi.users.direct_resource_offset = vertex.directOffsets.data(); + vertex.abi.users.direct_resource_count = static_cast(vertex.directOffsets.size()); + vertex.semantic.size_in_elements = 4; + vertex.semantic.static_vb_index = 1; + vertex.abi.shader.input_semantics = &vertex.semantic; + vertex.abi.shader.num_input_semantics = 1; +} + +void AbiDeferredCompute() { + static AbiHeader caller; + DescribeAbi(caller, UserDataCaller, 0, {{0x20c, ProgramLow(UserDataCaller.data())}, {0x20d, ProgramHigh(UserDataCaller.data())}, {0x207, 64}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 8u << 1u}}, {}); + caller.shader.specials = &caller.specials; + caller.specials.dispatch_modifier = 0x8000u; + AgcDriverRegisterShader_nid_postfix(&caller.shader); + const auto before = ShaderRecompiler::FrontEndBuilds(); + AgcDriverResolveShaderAbi_nid_postfix(&caller.shader, {}, {}); + const auto attempted = ShaderRecompiler::FrontEndBuilds(); + AgcDriverResolveShaderAbi_nid_postfix(&caller.shader, {}, {}); + Require(attempted > before && ShaderRecompiler::FrontEndBuilds() == attempted, "a compute static ABI deferred to its dispatch was prepared again for the same state"); + const auto first = reinterpret_cast(AddFiveCallee.data()); + const auto second = reinterpret_cast(AddSixtyFourCallee.data()); + const auto output = reinterpret_cast(CallerOutput.data()); + const std::array users{static_cast(first), static_cast(first >> 32u), static_cast(second), static_cast(second >> 32u), + static_cast(output), static_cast((output >> 32u) & 0xffffu), static_cast(sizeof(CallerOutput)), 0x31016facu}; + std::vector commands; + for (std::size_t index = 0; index < caller.shader.num_sh_registers; ++index) commands.insert(commands.end(), {0xc0017600u, caller.registers[index].offset, caller.registers[index].value}); + for (std::uint32_t index = 0; index < users.size(); ++index) commands.insert(commands.end(), {0xc0017600u, 0x240u + index, users[index]}); + commands.insert(commands.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); + CallerOutput.fill(0xcafebabeu); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + sceAgcDriverSubmitAcb(0x20, &packet); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(output, sizeof(CallerOutput)); + Require(std::ranges::all_of(CallerOutput, [](std::uint32_t lane) { return lane == 3u * (5u + 64u); }), "a compute shader its static ABI deferred did not run its user-data callees at its dispatch"); + AgcDriverShutdown_nid_postfix(); +} + +void AbiDeferredRegistrationFailure() { + static AbiHeader store; + DescribeAbi(store, WaterfallStoreCode, 0, {{0x20c, ProgramLow(WaterfallStoreCode.data())}, {0x20d, ProgramHigh(WaterfallStoreCode.data())}, {0x207, 64}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 2u << 1u}}, {}); + store.shader.specials = &store.specials; + static AbiHeader front; + DescribeFront(front); + static AbiHeader pixel; + DescribeWaterfallPixel(pixel); + pixel.context[0] = {0x1b6, 0}; + static AbiHeader vertex; + DescribeWaterfallVertex(vertex, BarrierFreeWaterfallVertexCode); + static AbiHeader interpolated; + DescribeWaterfallPixel(interpolated); + for (auto* header : {&store, &front, &pixel, &vertex, &interpolated}) AgcDriverRegisterShader_nid_postfix(&header->shader); + const std::array primitive{{{0x242, 4}}}; + const std::array frontStages{&front.shader, &pixel.shader}; + const std::array vertexStages{&vertex.shader, &interpolated.shader}; + const auto resolve = [&] { + AgcDriver::ShaderPreparationScope scope({&front.shader, &pixel.shader, &vertex.shader}); + AgcDriverResolveShaderAbi_nid_postfix(&store.shader, {}, {}); + AgcDriverResolveShaderAbi_nid_postfix(&pixel.shader, {}, {}); + AgcDriverResolveGraphicsAbi_nid_postfix(&front.shader, &pixel.shader, 17); + AgcDriverResolveGraphicsStagesAbi_nid_postfix(frontStages, {}, primitive); + AgcDriverResolveGraphicsStagesAbi_nid_postfix(vertexStages, {}, primitive); + AgcDriverResolveGraphicsAbi_nid_postfix(&vertex.shader, &interpolated.shader, 17); + scope.Commit(); + }; + auto log = Logged(resolve); + Require(Deferrals(log) == 3 && log.find("is not a valid runtime value") != std::string::npos, "stages whose registration preparation failed were not deferred once each, with the stored reason, when the title created their pipelines"); + log = Logged(resolve); + Require(Deferrals(log) == 0, "stages whose registration preparation failed were logged again for the same pipelines"); + std::vector commands; + for (std::size_t index = 0; index < store.shader.num_sh_registers; ++index) commands.insert(commands.end(), {0xc0017600u, store.registers[index].offset, store.registers[index].value}); + commands.insert(commands.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + sceAgcDriverSubmitAcb(0x20, &packet); + ExpectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, "is not a valid runtime value"); + ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, "is not a valid runtime value"); +} + +void AbiDeferredPixel() { + static AbiHeader pixel; + DescribeWaterfallPixel(pixel); + AgcDriverRegisterShader_nid_postfix(&pixel.shader); + const auto before = ShaderRecompiler::FrontEndBuilds(); + auto log = Logged([&] { AgcDriverResolveShaderAbi_nid_postfix(&pixel.shader, {}, {}); }); + const auto attempted = ShaderRecompiler::FrontEndBuilds(); + log += Logged([&] { AgcDriverResolveShaderAbi_nid_postfix(&pixel.shader, {}, {}); }); + Require(attempted > before && ShaderRecompiler::FrontEndBuilds() == attempted, "a pixel static ABI deferred to its draw was prepared again for the same state"); + Require(Deferrals(log) == 1 && log.find("is not a valid runtime value") != std::string::npos, "a pixel shader its static ABI cannot prepare was not logged once with its reason"); + { + AgcDriver::ShaderPreparationScope scope({&pixel.shader}); + const std::array interpolant{{{0x191, 0x20}}}; + log = Logged([&] { AgcDriverResolveShaderAbi_nid_postfix(&pixel.shader, interpolant, {}); }); + scope.Commit(); + } + Require(ShaderRecompiler::FrontEndBuilds() > attempted && Deferrals(log) == 0, "a deferred pixel shader was not prepared once more for another state, or was logged again"); + static AbiHeader front; + DescribeFront(front); + AgcDriverRegisterShader_nid_postfix(&front.shader); + std::vector commands; + for (const auto* header : {&front, &pixel}) { + for (std::size_t index = 0; index < header->shader.num_sh_registers; ++index) commands.insert(commands.end(), {0xc0017600u, header->registers[index].offset, header->registers[index].value}); + for (std::size_t index = 0; index < header->shader.num_cx_registers; ++index) commands.insert(commands.end(), {0xc0016900u, header->context[index].offset, header->context[index].value}); + } + commands.insert(commands.end(), {0xc0016900u, 0x8e, 0xfu, 0xc0016900u, 0x8f, 0xfu, 0xc0016900u, 0xd, 0x00100010u, 0xc0016900u, 0x204, 0x80000u, 0xc0016900u, 0x1c3, 4, 0xc0017900u, 0x242, 4, 0xc0012d00u, 3, 2}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw of a deferred pixel shader was not submitted"); + ExpectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, "is not a valid runtime value"); + ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(&pixel.shader, {}, {}); }, "is not a valid runtime value"); + ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, "is not a valid runtime value"); +} + +void AbiDeferredStages() { + static AbiHeader nested; + static AbiHeader direct; + DescribeWaterfallVertex(nested, WaterfallVertexCode); + DescribeWaterfallVertex(direct, OtherWaterfallVertexCode); + AgcDriverRegisterShader_nid_postfix(&nested.shader); + AgcDriverRegisterShader_nid_postfix(&direct.shader); + const std::array primitive{{{0x242, 4}}}; + const auto before = ShaderRecompiler::FrontEndBuilds(); + std::string log; + { + AgcDriver::ShaderPreparationScope scope({&nested.shader}); + log = Logged([&] { AgcDriverResolveShaderAbi_nid_postfix(&nested.shader, {}, primitive); }); + scope.Commit(); + } + const auto nestedAttempt = ShaderRecompiler::FrontEndBuilds(); + Require(nestedAttempt > before && Deferrals(log) == 1, "a vertex shader the static ABI resolves through its graphics stages was not deferred to its draw"); + log = Logged([&] { AgcDriverResolveShaderAbi_nid_postfix(&nested.shader, {}, primitive); }); + Require(ShaderRecompiler::FrontEndBuilds() == nestedAttempt && Deferrals(log) == 0, "a deferred vertex static ABI was prepared or logged again for the same state"); + const std::array stages{&direct.shader}; + log = Logged([&] { AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, {}, primitive); }); + const auto directAttempt = ShaderRecompiler::FrontEndBuilds(); + Require(directAttempt > nestedAttempt && Deferrals(log) == 1, "a graphics stage its ABI cannot prepare was not deferred to its draw"); + log = Logged([&] { AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, {}, primitive); }); + Require(ShaderRecompiler::FrontEndBuilds() == directAttempt && Deferrals(log) == 0, "a deferred graphics stage ABI was prepared or logged again for the same state"); + AgcDriverShutdown_nid_postfix(); +} + +void AbiDeferredRectangle() { + static AbiHeader front; + static AbiHeader undecodable; + static AbiHeader pixel; + DescribeFront(front); + DescribeAbi(undecodable, UndecodablePixelCode, 1, {{0x8, ProgramLow(UndecodablePixelCode.data())}, {0x9, ProgramHigh(UndecodablePixelCode.data())}, {0xa, 0}, {0xb, 0}}, {}); + DescribeWaterfallPixel(pixel); + for (auto* header : {&front, &undecodable, &pixel}) AgcDriverRegisterShader_nid_postfix(&header->shader); + AgcDriverResolveGraphicsAbi_nid_postfix(&front.shader, &undecodable.shader, 17); + AgcDriverResolveShaderAbi_nid_postfix(&pixel.shader, {}, {}); + { + AgcDriver::ShaderPreparationScope scope({&front.shader, &pixel.shader}); + AgcDriverResolveGraphicsAbi_nid_postfix(&front.shader, &pixel.shader, 17); + scope.Commit(); + } + AgcDriverShutdown_nid_postfix(); +} + +void AbiDeferredUndecodableState() { + static AbiHeader front; + DescribeFront(front); + front.context[0] = {0x2ce, 3}; + front.specials = {{0x25b, 0x8040}, {0x2d5, 0x2030}, 0x8000, {}, {}, {0x29b, 1}, {0x262, 0}}; + AgcDriverRegisterShader_nid_postfix(&front.shader); + const std::array stages{&front.shader}; + const std::array context{front.specials.vgt_shader_stages_en, front.specials.vgt_gs_out_prim_type}; + const std::array primitive{front.specials.ge_cntl, front.specials.ge_user_vgpr_en, ShaderRegister{0x242, 4}}; + const auto before = ShaderRecompiler::FrontEndBuilds(); + auto log = Logged([&] { AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, context, primitive); }); + Require(Deferrals(log) == 1 && log.find("VGT_SHADER_STAGES_EN=0x2030: ") != std::string::npos, "a graphics stage whose pipeline state does not decode was not deferred with the decode's reason"); + log = Logged([&] { AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, context, primitive); }); + Require(ShaderRecompiler::FrontEndBuilds() == before && Deferrals(log) == 0, "a graphics stage deferred for a pipeline state that does not decode was prepared or logged again"); + std::vector commands; + for (std::size_t index = 0; index < front.shader.num_sh_registers; ++index) commands.insert(commands.end(), {0xc0017600u, front.registers[index].offset, front.registers[index].value}); + commands.insert(commands.end(), {0xc0016900u, 0x2d5, 0x2000u, 0xc0017600u, 0x8, 0, 0xc0016900u, 0xd, 0x00100010u, 0xc0016900u, 0x204, 0x80000u, 0xc0016900u, 0x1c3, 4, 0xc0017900u, 0x242, 4, 0xc0012d00u, 3, 2}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + log = Logged([&] { + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw of a graphics stage deferred at its pipeline ABI was not submitted"); + AgcDriverWaitIdle_nid_postfix(); + }); + Require(ShaderRecompiler::FrontEndBuilds() > before && log.find("has no artifact prepared at registration") == std::string::npos, "a graphics stage deferred at its pipeline ABI was not prepared at its first draw, or was reported there as missing a registration artifact"); + AgcDriverShutdown_nid_postfix(); +} + +void AbiDeferredVertexInputs() { + static FetchingHeader fetching; + DescribeWaterfallVertex(fetching.abi, WaterfallVertexCode); + DescribeStaticFetch(fetching); + AgcDriverRegisterShader_nid_postfix(&fetching.abi.shader); + const std::array stages{&fetching.abi.shader}; + const std::array primitive{{{0x242, 4}}}; + const auto resolve = [&] { + AgcDriver::ShaderPreparationScope scope({&fetching.abi.shader}); + AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, {}, primitive); + scope.Commit(); + }; + const auto before = ShaderRecompiler::FrontEndBuilds(); + auto log = Logged(resolve); + Require(Deferrals(log) == 1 && log.find("statically bound vertex attributes are not implemented") != std::string::npos, "a graphics stage whose vertex inputs its pipeline ABI cannot decode was not deferred with the decode's reason"); + log = Logged(resolve); + Require(ShaderRecompiler::FrontEndBuilds() == before && Deferrals(log) == 0, "a graphics stage deferred for vertex inputs that do not decode was prepared or logged again"); + alignas(16) static std::array tables{}; + const auto address = reinterpret_cast(tables.data()); + std::vector commands; + for (std::size_t index = 0; index < fetching.abi.shader.num_sh_registers; ++index) commands.insert(commands.end(), {0xc0017600u, fetching.abi.registers[index].offset, fetching.abi.registers[index].value}); + for (std::uint32_t index = 0; index < 4; ++index) commands.insert(commands.end(), {0xc0017600u, 0x8cu + index, static_cast(index % 2u == 0u ? address : address >> 32u)}); + for (std::size_t index = 0; index < fetching.abi.shader.num_cx_registers; ++index) commands.insert(commands.end(), {0xc0016900u, fetching.abi.context[index].offset, fetching.abi.context[index].value}); + for (const auto reg : {fetching.abi.specials.vgt_shader_stages_en, fetching.abi.specials.vgt_gs_out_prim_type}) commands.insert(commands.end(), {0xc0016900u, reg.offset, reg.value}); + for (const auto reg : {fetching.abi.specials.ge_cntl, fetching.abi.specials.ge_user_vgpr_en}) commands.insert(commands.end(), {0xc0017900u, reg.offset, reg.value}); + commands.insert(commands.end(), {0xc0017600u, 0x8, 0, 0xc0016900u, 0xd, 0x00100010u, 0xc0016900u, 0x204, 0x80000u, 0xc0016900u, 0x1c3, 4, 0xc0017900u, 0x242, 4, 0xc0012d00u, 3, 2}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw of a graphics stage deferred for its vertex inputs was not submitted"); + ExpectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, "statically bound vertex attributes are not implemented"); + ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, "statically bound vertex attributes are not implemented"); +} + +void AbiDeferredRectanglePairs() { + static AbiHeader front; + DescribeFront(front); + static AbiHeader pixel; + DescribeAbi(pixel, PassthroughPixelCode, 1, {{0x8, ProgramLow(PassthroughPixelCode.data())}, {0x9, ProgramHigh(PassthroughPixelCode.data())}, {0xa, 0}, {0xb, 0}}, {{0x1b6, 1}, {0x1b3, 2}, {0x1b4, 2}, {0x191, 0x420}, {0x1c5, 4}}); + static AbiHeader unresolved; + DescribeWaterfallPixel(unresolved); + for (auto* header : {&front, &pixel, &unresolved}) AgcDriverRegisterShader_nid_postfix(&header->shader); + const std::array passthrough{pixel.context[3]}; + const auto resolve = [&] { + AgcDriver::ShaderPreparationScope scope({&front.shader, &pixel.shader}); + AgcDriverResolveGraphicsAbi_nid_postfix(&front.shader, nullptr, 17); + AgcDriverResolveGraphicsAbi_nid_postfix(&front.shader, &pixel.shader, 17); + AgcDriverResolveGraphicsAbi_nid_postfix(&front.shader, &unresolved.shader, 17); + AgcDriverResolveShaderAbi_nid_postfix(&pixel.shader, passthrough, {}); + AgcDriverResolveGraphicsAbi_nid_postfix(&front.shader, &pixel.shader, 17); + scope.Commit(); + }; + auto log = Logged(resolve); + Require(Deferrals(log) == 1 && log.find("custom per-vertex interpolation is unsupported") != std::string::npos, "a rectangle pair its pipeline ABI cannot build was not deferred once with the builder's reason"); + log = Logged(resolve); + Require(Deferrals(log) == 0, "a rectangle pair deferred at its pipeline ABI was logged again"); + const auto draw = [&](bool withPixel) { + std::vector commands; + for (const auto* header : {&front, &pixel}) { + if (header == &pixel && !withPixel) continue; + for (std::size_t index = 0; index < header->shader.num_sh_registers; ++index) commands.insert(commands.end(), {0xc0017600u, header->registers[index].offset, header->registers[index].value}); + for (std::size_t index = 0; index < header->shader.num_cx_registers; ++index) commands.insert(commands.end(), {0xc0016900u, header->context[index].offset, header->context[index].value}); + } + if (!withPixel) commands.insert(commands.end(), {0xc0017600u, 0x8, 0}); + commands.insert(commands.end(), {0xc0016900u, 0x8e, 0xfu, 0xc0016900u, 0x8f, 0xfu, 0xc0016900u, 0xd, 0x00100010u, 0xc0016900u, 0x204, 0x80000u, 0xc0016900u, 0x1c3, 4, 0xc0017900u, 0x242, 17, 0xc0012d00u, 3, 2}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "a rect-list draw was not submitted"); + AgcDriverWaitIdle_nid_postfix(); + }; + draw(false); + ExpectFailure([&] { draw(true); }, "custom per-vertex interpolation is unsupported"); + ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, "custom per-vertex interpolation is unsupported"); +} + +void RelocatedHeaders() { + alignas(256) static const std::array code{0x7e0002ffu, 0, 0x7e0202ffu, 0, 0x7e0402ffu, 0, 0x7e0602ffu, 0x3f800000u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; + struct Header { + Shader shader{}; + std::array registers{}; + ShaderSpecialRegs specials{}; + ShaderUserData users{}; + std::array context{{{0x2d5, 0x2000}}}; + }; + static Header registered; + const auto address = reinterpret_cast(code.data()); + const auto fill = [&](Header& header) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = sizeof(code); + header.shader.code = code.data(); + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = header.registers.size(); + header.shader.cx_registers = header.context.data(); + header.shader.num_cx_registers = header.context.size(); + header.shader.specials = &header.specials; + header.shader.user_data = &header.users; + header.specials.dispatch_modifier = 0x8000; + header.shader.type = 2; + header.registers = {{{0xc8, static_cast(address >> 8u)}, {0xc9, static_cast(address >> 40u)}, {0x8b, 0}, {0x8a, 0}, {0xca, 0}, {0xcb, 0}, {0xcc, 0}}}; + }; + fill(registered); + const std::array primitive{{{0x242, 4}}}; + AgcDriverRegisterShader_nid_postfix(®istered.shader); + AgcDriverResolveShaderAbi_nid_postfix(®istered.shader, {}, primitive); + std::vector commands; + for (const auto reg : registered.registers) commands.insert(commands.end(), {0xc0017600u, reg.offset, reg.value}); + for (const auto reg : registered.context) commands.insert(commands.end(), {0xc0016900u, reg.offset, reg.value}); + commands.insert(commands.end(), {0xc0017600u, 0x8, 0, 0xc0016900u, 0xd, 0x00100010u, 0xc0016900u, 0x204, 0x80000u, 0xc0016900u, 0x1c3, 4, 0xc0017900u, 0x242, 4, 0xc0012d00u, 3, 2}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + // A title that relocates a registered header and its arrays into memory of its own resolves the ABI with the copy, + // which was never registered. Every array it names holds the registered bytes, so the registered header decodes it. + static Header relocated; + fill(relocated); + AgcDriverResolveShaderAbi_nid_postfix(&relocated.shader, {}, primitive); + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the relocated header's draw was not submitted"); + AgcDriverWaitIdle_nid_postfix(); + // A copy that moves only its user data pointer beside the registered arrays keeps resolving. + static Header movedUserData; + movedUserData = registered; + movedUserData.shader.user_data = &movedUserData.users; + AgcDriverResolveShaderAbi_nid_postfix(&movedUserData.shader, {}, primitive); + // A copy whose registers, specials or semantics differ is still a replaced header. + static Header patched; + fill(patched); + patched.registers[2].value ^= 2u; + ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(&patched.shader, {}, primitive); }, "replaced shader header"); + static Header otherSpecials; + fill(otherSpecials); + otherSpecials.specials.dispatch_modifier = 0x4000; + ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(&otherSpecials.shader, {}, primitive); }, "replaced shader header"); + static Header otherContext; + fill(otherContext); + otherContext.context[0].value = 0x2001; + ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(&otherContext.shader, {}, primitive); }, "replaced shader header"); +} + } int main(int argc, char** argv) { 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"); + const std::string_view abiMode = argc == 2 ? argv[1] : ""; + const bool abiDeferred = abiMode == "--abi-deferred-compute" || abiMode == "--abi-deferred-pixel" || abiMode == "--abi-deferred-stages" || abiMode == "--abi-deferred-rectangle" || abiMode == "--abi-deferred-undecodable-state" || abiMode == "--abi-deferred-registration-failure" || abiMode == "--abi-deferred-vertex-inputs" || abiMode == "--abi-deferred-rectangle-pairs"; + Require(argc == 1 || abiDeferred || (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 (abiDeferred) { + if (!OpenVulkanTestDevice()) return VulkanTestSkipped; + if (abiMode == "--abi-deferred-compute") AbiDeferredCompute(); + else if (abiMode == "--abi-deferred-pixel") AbiDeferredPixel(); + else if (abiMode == "--abi-deferred-stages") AbiDeferredStages(); + else if (abiMode == "--abi-deferred-undecodable-state") AbiDeferredUndecodableState(); + else if (abiMode == "--abi-deferred-registration-failure") AbiDeferredRegistrationFailure(); + else if (abiMode == "--abi-deferred-vertex-inputs") AbiDeferredVertexInputs(); + else if (abiMode == "--abi-deferred-rectangle-pairs") AbiDeferredRectanglePairs(); + else AbiDeferredRectangle(); + std::cout << "shader ABI deferral tests passed: " << abiMode << '\n'; + return 0; + } + 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; @@ -673,10 +1312,12 @@ int main(int argc, char** argv) { RegisteredVertexWorkgroup(device->Target()); FailureCapture(*device); PendingRegistrationPreparation(*device); + AwaitedRegistrationPreparation(); Require(argc != 2 || std::string_view(argv[1]) != "--fail-before-registration", "injected failure before registration"); PrepareMultisampledStorage(*device); device.reset(); SharedCodeHeaders(); + RelocatedHeaders(); RegistrationWithoutSpecials(); Registration(argc == 2); std::cout << "prepared shader and transactional registration tests passed\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedUnregisteredGraphics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedUnregisteredGraphics.cpp index 1915467395..df68d87509 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedUnregisteredGraphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedUnregisteredGraphics.cpp @@ -51,13 +51,13 @@ void Check(AgcDriver::VulkanDevice& device) { Require(draw.programs.size() == 2 && draw.roles.back() == ProgramRole::Fragment, "unregistered graphics programs did not decode as a vertex and a fragment stage"); for (const auto& program : draw.programs) Require(program.snapshot != nullptr && program.snapshot->header.empty() && program.snapshot->prepared->entries.empty(), "an unregistered graphics program was not read as raw code"); Require(RawGraphicsDrawn(), "a draw of unregistered graphics programs was not noted"); - bool refused = false; - try { - Driver::Get().RegisterShader(nullptr); - } catch (const std::exception& error) { - refused = std::string(error.what()).find("registered after graphics programs were drawn") != std::string::npos; + for (const auto& program : draw.programs) { + const auto begin = program.snapshot->codeAddress; + Require(RawGraphicsDrawnIn(begin, begin + 4u) && RawGraphicsDrawnIn(begin - 4u, begin + 1u), "a raw-drawn program's code is not recorded"); + Require(!RawGraphicsDrawnIn(begin - 4u, begin), "the raw-drawn range starts before the program"); } - Require(refused, "a shader registration after unregistered graphics draws was accepted"); + alignas(256) static std::array Elsewhere{0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; + Require(!RawGraphicsDrawnIn(reinterpret_cast(Elsewhere.data()), reinterpret_cast(Elsewhere.data()) + sizeof(Elsewhere)), "unrelated code reads as drawn raw"); const auto target = device.Target(); std::vector linked; std::vector memory; 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/QuadPixelMask.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QuadPixelMask.cpp new file mode 100644 index 0000000000..613daa2001 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QuadPixelMask.cpp @@ -0,0 +1,189 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 32; +constexpr std::uint32_t TextureWidth = Width * 4u; +constexpr std::uint32_t TextureHeight = Height * 4u; +constexpr std::uint32_t TextureLevels = 9; +constexpr std::uint32_t SampledLevel = 2; +constexpr std::uint32_t Format8888Unorm = 56; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t DemoteToHelperInvocation = 5379; +alignas(256) std::array Pixels{}; +alignas(4096) std::array Texels{}; +alignas(256) 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) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array ConstantPixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array SampledPixelCode{ + 0xbe8c047e, 0xbefe0a7e, 0x100000ff, 0x3c800000, 0x100202ff, 0x3d000000, 0xf0800f08, 0x00400200, + 0xbf8c3f70, 0x87fe0c7e, 0xf800180f, 0x05040302, 0xbf810000, +}; + +std::array LevelColor(std::uint32_t level) { + return {static_cast(32u * (level + 1u)), static_cast(255u - 16u * level), static_cast(0x40u + level), 255u}; +} + +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::array TextureDescriptor() { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888Unorm << 20u) | (((TextureWidth - 1u) & 3u) << 30u), + ((TextureWidth - 1u) >> 2u) | ((TextureHeight - 1u) << 14u), + 0xfacu | ((TextureLevels - 1u) << 16u) | (Type2D << 28u), + 0u, (TextureLevels - 1u) << 4u, 0u, 0u, + }; +} + +std::array SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void FillTexture() { + const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Format8888Unorm, TextureWidth, TextureHeight, TextureLevels); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Texels.size(), "the mipmapped test texture does not fit its storage"); + for (std::uint32_t level = 0; level < TextureLevels; ++level) { + const auto color = LevelColor(level); + for (std::uint64_t offset = mips[level].tiledOffset; offset < mips[level].tiledOffset + mips[level].tiledSize; offset += 4u) { + std::memcpy(Texels.data() + offset, color.data(), color.size()); + } + } +} + +void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint8_t quadPixelMask) { + Pixels.fill(std::byte{0}); + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {64, 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); + pixel.quadPixelMask = quadPixelMask; + std::vector pixelUserData(12, 0u); + const auto texture = TextureDescriptor(); + const auto sampler = SamplerDescriptor(); + std::copy(texture.begin(), texture.end(), pixelUserData.begin()); + std::copy(sampler.begin(), sampler.end(), pixelUserData.begin() + 8); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {64, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(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(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(const std::string& what, std::uint8_t quadPixelMask, std::array color) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const bool covered = ((quadPixelMask >> ((x & 1u) | ((y & 1u) << 1u))) & 1u) != 0; + const auto* written = &Pixels[(y * Width + x) * 4u]; + for (std::uint32_t channel = 0; channel < 4u; ++channel) { + const auto expected = covered ? color[channel] : 0u; + const auto value = std::to_integer(written[channel]); + Require(value == expected, what + " with quad pixel mask " + std::to_string(quadPixelMask) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(value) + ", expected " + std::to_string(expected)); + } + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto capabilities = device->Target().supportedCapabilities; + const bool demote = std::find(capabilities.begin(), capabilities.end(), DemoteToHelperInvocation) != capabilities.end(); + FillTexture(); + for (const std::uint8_t mask : {std::uint8_t{0xf}, std::uint8_t{0x2}, std::uint8_t{0x9}}) { + Draw(*device, ConstantPixelCode, mask); + Check("constant color", mask, {255u, 255u, 255u, 255u}); + if (mask == 0xf || demote) { + Draw(*device, SampledPixelCode, mask); + Check("implicit-LOD sample", mask, LevelColor(SampledLevel)); + continue; + } + try { + Draw(*device, SampledPixelCode, mask); + } catch (const std::exception& error) { + Require(std::string(error.what()).find("shaderDemoteToHelperInvocation") != std::string::npos, std::string("an implicit-LOD sample under a quad pixel mask failed for another reason: ") + error.what()); + continue; + } + Require(false, "an implicit-LOD sample under quad pixel mask " + std::to_string(mask) + " was drawn without demote to helper invocation"); + } + std::printf("quad pixel mask tests passed (%s)\n", demote ? "demote to helper invocation" : "kill, derivatives refused"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroIdleWait.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroIdleWait.cpp new file mode 100644 index 0000000000..07f2a58a6b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroIdleWait.cpp @@ -0,0 +1,40 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + struct ShutdownGuard { + ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } + } shutdown; + + std::array nop{0xc0001000u, 0u}; + Packet packet{nop.data(), static_cast(nop.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + + const auto dormant = [] { return AgcDriver::DriverDetail::queue0Dormant.load(std::memory_order_relaxed); }; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!dormant() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + AgcDriver::Graphics::Require(!AgcDriver::Graphics::StorageTexture::AnyPending(), "a no-op submission left storage results pending"); + AgcDriver::Graphics::Require(dormant(), "queue 0 kept polling with every queue idle and no storage results to store instead of waiting"); + + AgcDriverShutdown_nid_postfix(); + std::puts("queue zero idle wait 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/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/QueuedIndexReadOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp new file mode 100644 index 0000000000..acc2b1617a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp @@ -0,0 +1,432 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 192; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t DistanceVector = 0x00400000u; +constexpr std::uint32_t SetContextReg = 0x69u; +constexpr std::uint32_t SetShReg = 0x76u; +constexpr std::uint32_t SetUconfigReg = 0x79u; +constexpr std::uint32_t DrawIndexOffset2Header = 0xc0033500u; + +constexpr std::size_t Block = 65536; +constexpr std::size_t PoolBytes = 2 * Block; +constexpr std::size_t ColorBytes = Width * Height * 4; +constexpr std::uint32_t FormatRgba16Float = 71; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Standard64KB = 0x09; +constexpr std::uint32_t ImageWidth = 128; +constexpr std::uint32_t ImageHeight = 120; +constexpr std::uint8_t Sentinel = 0xc7; +constexpr std::array Indices{0, 1, 2}; +constexpr std::size_t VertexOffset = 0x2000; + +std::byte* ColorMemory = nullptr; +std::byte* Pool = nullptr; +std::byte* Copied = nullptr; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80020005, 0xe0382010, 0x80020405, 0xbf8c3f70, + 0xf80000cf, 0x03020100, 0xf80008df, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0002f2, 0x7e0202f2, 0x7e0402f2, 0x7e0602f2, 0xf800080f, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array ImageWriteCode{ + 0xd7460004u, 0x0401060eu, 0xd7460005u, 0x0405060fu, 0x7e100d04u, 0x7e120d05u, + 0x7e140c0du, 0x7e1602f2u, 0xf0200f08u, 0x00000804u, 0xbf810000u, +}; + +struct Vertex { + std::array position; + std::array distances; +}; + +std::array Vertices{}; + +constexpr std::array PositionX{-3.0f, 1.0f, 1.0f}; +constexpr std::array PositionY{-1.0f, -1.0f, 3.0f}; +constexpr std::array Negative{-1.0f, -1.0f, -1.0f}; + +struct RegisteredShader { + Shader shader; + ShaderUserData userData; +}; +RegisteredShader VertexShader{}; +RegisteredShader PixelShader{}; + +const std::vector> ContextBaseline{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, + {0x81, 0x80000000}, {0x90, 0x80000000}, {0x94, 0x80000000}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, {0x114, 0}, {0x113, std::bit_cast(1.0f)}, +}; + +std::byte* AllocateWatched(std::size_t bytes) { +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (raw == MAP_FAILED) throw std::runtime_error("queued index read: cannot map the watched block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + return static_cast(block); +} + +void Register(RegisteredShader& registered, const void* code, std::uint32_t bytes, std::uint8_t type) { + registered.shader.file_header = 0x34333231u; + registered.shader.version = 0x18u; + registered.shader.user_data = ®istered.userData; + registered.shader.code = code; + registered.shader.header_size = sizeof(RegisteredShader); + registered.shader.shader_size = bytes; + registered.shader.type = type; + AgcDriverRegisterShader_nid_postfix(®istered.shader); +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void SetRegister(std::vector& words, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { + words.push_back(0xc0010000u | (opcode << 8u)); + words.push_back(offset); + words.push_back(value); +} + +std::array ImageDescriptor(const void* texels) { + const auto address = reinterpret_cast(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (FormatRgba16Float << 20u) | (((ImageWidth - 1u) & 3u) << 30u), + ((ImageWidth - 1u) >> 2u) | ((ImageHeight - 1u) << 14u), + 0xfacu | (Standard64KB << 20u) | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::vector ImageWrite() { + const auto descriptor = ImageDescriptor(Pool); + std::vector data(descriptor.begin(), descriptor.end()); + data.resize(14, 0u); + data[13] = 7u; + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{8, 8, 1}; + registers(0x207, shape); + const auto code = reinterpret_cast(ImageWriteCode.data()); + const std::array program{static_cast(code >> 8u), static_cast(code >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (7u << 7u) | (1u << 11u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, shape[0], shape[1], shape[2], 0x41u}); + return words; +} + +std::vector Draw(const void* vertices, std::size_t count) { + const auto colorAddress = reinterpret_cast(ColorMemory); + const auto poolAddress = reinterpret_cast(Pool); + const auto vertexAddress = reinterpret_cast(VertexCode.data()); + const auto pixelAddress = reinterpret_cast(PixelCode.data()); + std::vector words; + for (const auto& [offset, value] : ContextBaseline) SetRegister(words, SetContextReg, offset, value); + SetRegister(words, SetContextReg, 0x207, DistanceVector); + SetRegister(words, SetContextReg, 0x318, static_cast(colorAddress >> 8u)); + SetRegister(words, SetContextReg, 0x390, static_cast(colorAddress >> 40u)); + SetRegister(words, SetContextReg, 0x3b0, ((Width - 1u) << 14u) | (Height - 1u)); + SetRegister(words, SetContextReg, 0x10f, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x110, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x111, std::bit_cast(-(Height / 2.0f))); + SetRegister(words, SetContextReg, 0x112, std::bit_cast(Height / 2.0f)); + const auto extent = (Height << 16u) | Width; + SetRegister(words, SetContextReg, 0xd, extent); + SetRegister(words, SetContextReg, 0x82, extent); + SetRegister(words, SetContextReg, 0x91, extent); + SetRegister(words, SetContextReg, 0x95, extent); + SetRegister(words, SetUconfigReg, 0x242, 4u); + SetRegister(words, SetShReg, 0x008, static_cast(pixelAddress >> 8u)); + SetRegister(words, SetShReg, 0x009, static_cast(pixelAddress >> 40u)); + SetRegister(words, SetShReg, 0x00b, 0u); + SetRegister(words, SetShReg, 0x0c8, static_cast(vertexAddress >> 8u)); + SetRegister(words, SetShReg, 0x0c9, static_cast(vertexAddress >> 40u)); + SetRegister(words, SetShReg, 0x08b, 4u << 1u); + const auto descriptor = BufferDescriptor(vertices, sizeof(Vertex), static_cast(count)); + for (std::uint32_t i = 0; i < descriptor.size(); ++i) SetRegister(words, SetShReg, 0x08c + i, descriptor[i]); + words.insert(words.end(), {0xc0012600u, static_cast(poolAddress), static_cast(poolAddress >> 32u)}); + words.insert(words.end(), {0xc0001300u, static_cast(Indices.size())}); + SetRegister(words, SetUconfigReg, 0x243, 0x401u); + words.insert(words.end(), {DrawIndexOffset2Header, static_cast(Indices.size()), 0u, static_cast(Indices.size()), 0u}); + return words; +} + +void Submit(const std::vector& words, const char* what) { + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, std::string("queued index read: the submission failed for ") + what); +} + +std::vector CopyPool(std::size_t bytes) { + const auto source = reinterpret_cast(Pool); + const auto destination = reinterpret_cast(Copied); + return {0xc0055000u, 0u, static_cast(source), static_cast(source >> 32u), static_cast(destination), static_cast(destination >> 32u), static_cast(bytes)}; +} + +void RequireCopiedImage(std::size_t bytes, const char* phase) { + std::size_t differing = 0; + for (std::size_t i = 0; i < bytes; ++i) differing += Copied[i] != Pool[i] ? 1u : 0u; + Require(differing == 0, std::string("queued index read: ") + phase + ": " + std::to_string(differing) + " of " + std::to_string(bytes) + " copied bytes miss the image's results over the copy source"); +} + +std::vector WaitRegMem(std::uint64_t address, std::uint32_t reference) { + return {0xc0053c00u, 0x13u, static_cast(address), static_cast(address >> 32u), reference, 0xffffffffu, 0u}; +} + +bool IndicesIntact() { + return std::equal(Indices.begin(), Indices.end(), reinterpret_cast(Pool)); +} + +bool VerticesIntact() { + return std::memcmp(Pool + VertexOffset, Vertices.data(), sizeof(Vertex) * Vertices.size()) == 0; +} + +std::uint32_t PoolWord(std::size_t offset) { + return *reinterpret_cast(Pool + offset); +} + +void ResetPool() { + std::memset(Pool, Sentinel, PoolBytes); + std::memcpy(Pool, Indices.data(), Indices.size() * sizeof(std::uint32_t)); + std::memcpy(Pool + VertexOffset, Vertices.data(), sizeof(Vertex) * Vertices.size()); +} + +std::size_t WrittenPixels() { + std::vector colors(ColorBytes); + AgcDriver::GuestMemory::Read(reinterpret_cast(reinterpret_cast(ColorMemory)), colors); + std::size_t written = 0; + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const auto value = std::to_integer(colors[pixel * 4u]); + Require(value == 0u || value == 255u, "queued index read: pixel " + std::to_string(pixel) + " stored " + std::to_string(value)); + written += value == 255u ? 1u : 0u; + } + return written; +} + +void RequireTriangle(const char* phase) { + const auto written = WrittenPixels(); + Require(written == Width * Height, std::string("queued index read: ") + phase + ": the draw left " + std::to_string(Width * Height - written) + " pixels cleared, expected the triangle its indices and vertices name"); +} + +void LandPool() { + const auto landBy = std::chrono::steady_clock::now() + std::chrono::seconds(5); + do { + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(reinterpret_cast(Pool)), PoolBytes); + AgcDriverWaitIdle_nid_postfix(); + } while (IndicesIntact() && std::chrono::steady_clock::now() < landBy); + Require(!IndicesIntact(), "queued index read: the image's results never reached the memory they cover, so the run above proves nothing"); +} + +void WaitForWord(std::uint64_t address, std::uint32_t value, const char* what) { + const auto start = std::chrono::steady_clock::now(); + const auto deadline = start + std::chrono::seconds(10); + while (*reinterpret_cast(address) != value) { + if (std::chrono::steady_clock::now() >= deadline) { + char message[256]; + std::snprintf(message, sizeof(message), "queued index read: %s: 0x%08llx held 0x%08x after %.1f s, expected 0x%08x", what, static_cast(address), *reinterpret_cast(address), std::chrono::duration(std::chrono::steady_clock::now() - start).count(), value); + Require(false, message); + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +std::atomic GateOpen{true}; + +void GateUntilQueued() { + AgcDriver::DriverDetail::DrawPipeline::Queue0().Enqueue([] { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (AgcDriver::DriverDetail::DrawPipeline::QueuedItems() < 2 || !GateOpen.load()) { + if (std::chrono::steady_clock::now() >= deadline) return; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + }, {}); +} + +void WaitForCommits() { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (AgcDriver::DriverDetail::DrawPipeline::QueuedItems() != 0) { + Require(std::chrono::steady_clock::now() < deadline, "queued index read: the draw commit never ran"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +void WaitForHeldRange(std::uint64_t address, std::size_t bytes, const char* what) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (!AgcDriver::DriverDetail::DrawPipeline::HeldReads(address, bytes) && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const bool held = AgcDriver::DriverDetail::DrawPipeline::HeldReads(address, bytes); + GateOpen = true; + Require(held, std::string("queued index read: the queued draw holds no read of its ") + what + " range"); +} + +void CopyPendingImage(std::size_t bytes, const char* phase) { + const auto poolAddress = reinterpret_cast(reinterpret_cast(Pool)); + ResetPool(); + std::memset(Copied, 0, PoolBytes); + Submit(ImageWrite(), phase); + AgcDriverWaitIdle_nid_postfix(); + Require(AgcDriver::Graphics::StorageTexture::AnyPending(), std::string("queued index read: ") + phase + ": the image write left no pending result to order against"); + GateOpen = false; + GateUntilQueued(); + Submit(CopyPool(bytes), phase); + WaitForHeldRange(poolAddress, bytes, "copy source"); + WaitForCommits(); + AgcDriverWaitIdle_nid_postfix(); + LandPool(); + RequireCopiedImage(bytes, phase); +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_PIPELINED_DRAWS", "1"); + _putenv_s("APS5_PIPELINE_DMA", "1"); +#else + setenv("APS5_PIPELINED_DRAWS", "1", 1); + setenv("APS5_PIPELINE_DMA", "1", 1); +#endif + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ColorMemory = AllocateWatched(2 * Block); + Pool = AllocateWatched(PoolBytes); + Copied = AllocateWatched(PoolBytes); + GuestAllocations::Mutation().Add(Pool, PoolBytes, true, true, true); + GuestAllocations::Mutation().Add(Copied, PoolBytes, true, true, true); + Register(VertexShader, VertexCode.data(), sizeof(VertexCode), 2); + Register(PixelShader, PixelCode.data(), sizeof(PixelCode), 1); + std::memset(Pool, Sentinel, PoolBytes); + std::memcpy(Pool, Indices.data(), Indices.size() * sizeof(std::uint32_t)); + for (std::size_t i = 0; i < Vertices.size(); ++i) Vertices[i] = {{PositionX[i], PositionY[i], 0.5f, 1.0f}, {Negative[i], Negative[i], Negative[i], 0.0f}}; + std::memcpy(Pool + VertexOffset, Vertices.data(), sizeof(Vertex) * Vertices.size()); + + const auto poolAddress = reinterpret_cast(reinterpret_cast(Pool)); + const auto vertexAddress = poolAddress + VertexOffset; + const std::array, 1> poolRange{{ + {poolAddress, static_cast(PoolBytes)}}}; + Submit(ImageWrite(), "the image write"); + AgcDriverWaitIdle_nid_postfix(); + Require(AgcDriver::Graphics::StorageTexture::AnyPending(), "queued index read: the image write left no pending result to order against"); + Require(IndicesIntact(), "queued index read: the image's results reached guest memory before any consumer asked"); + GateUntilQueued(); + std::fill_n(ColorMemory, ColorBytes, std::byte{0}); + Submit(Draw(Vertices.data(), Vertices.size()), "the indexed draw"); + WaitForCommits(); + Require(IndicesIntact(), "queued index read: the queued draw's commit stored the image's results over its index buffer"); + AgcDriverWaitIdle_nid_postfix(); + RequireTriangle("the index read"); + LandPool(); + + ResetPool(); + Submit(ImageWrite(), "the second image write"); + AgcDriverWaitIdle_nid_postfix(); + Require(AgcDriver::Graphics::StorageTexture::AnyPending(), "queued index read: the second image write left no pending result to order against"); + GateOpen = false; + GateUntilQueued(); + std::fill_n(ColorMemory, ColorBytes, std::byte{0}); + Submit(Draw(Pool + VertexOffset, Vertices.size()), "the draw whose vertices the image covers"); + WaitForHeldRange(vertexAddress, sizeof(Vertex) * Vertices.size(), "vertex"); + WaitForCommits(); + AgcDriverWaitIdle_nid_postfix(); + RequireTriangle("the vertex read"); + LandPool(); + + std::array texels{}; + for (std::size_t i = 0; i < texels.size(); ++i) texels[i] = PoolWord(i * sizeof(std::uint32_t)); + const auto sentinelWord = static_cast(Sentinel) * 0x01010101u; + std::size_t awaited = texels.size(); + for (std::size_t i = 0; i < texels.size(); ++i) { + if (texels[i] != Indices[i] && texels[i] != sentinelWord) { + awaited = i; + break; + } + } + Require(awaited != texels.size(), "queued index read: the image's first texels repeat the indices and the sentinel, so a wait on them proves nothing"); + const auto awaitedAddress = poolAddress + awaited * sizeof(std::uint32_t); + ResetPool(); + + Submit(ImageWrite(), "the third image write"); + AgcDriverWaitIdle_nid_postfix(); + Require(AgcDriver::Graphics::StorageTexture::AnyPending(), "queued index read: the third image write left no pending result to order against"); + std::fill_n(ColorMemory, ColorBytes, std::byte{0}); + Submit(Draw(Pool + VertexOffset, Vertices.size()), "the draw a wait behind it reads the held range of"); + Submit(WaitRegMem(awaitedAddress, texels[awaited]), "the wait over the held range"); + WaitForWord(awaitedAddress, texels[awaited], "the wait over the held range was not satisfied"); + AgcDriverWaitIdle_nid_postfix(); + RequireTriangle("the wait over the held range"); + Require(!VerticesIntact(), "queued index read: the image's results never reached the memory the wait covered"); + CopyPendingImage(4096, "the queued copy of the image"); + std::puts("queued index read vs. deferred storage write-back tests passed"); + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QueuedRawShader.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedRawShader.cpp new file mode 100644 index 0000000000..703f7523d7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedRawShader.cpp @@ -0,0 +1,66 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void QueuedCode(bool cached) { + using namespace AgcDriver::DriverDetail; + alignas(256) static std::array, 2> programs{}; + auto& code = programs[cached ? 0 : 1]; + code.fill(0xbf810000u); + code[0] = 0x7e000200u; + const auto address = reinterpret_cast(code.data()); + const auto before = cached ? ReadRawShader(address) : nullptr; + std::atomic finished{false}; + std::atomic started{false}; + struct Restore { + std::atomic& finished; + bool active = DrawPipeline::Active(); + ~Restore() { + finished.store(true, std::memory_order_release); + DrawPipeline::Active() = active; + DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission); + } + } restore{finished}; + DrawPipeline::Queue0().Enqueue([&, address] { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(200); + started.store(true, std::memory_order_release); + while (!finished.load(std::memory_order_acquire) && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + const std::uint32_t replacement = 0x7e0002f2u; + AgcDriver::GuestMemory::Write(address, std::as_bytes(std::span(&replacement, 1)), alignof(std::uint32_t)); + }, {{address, address + sizeof(std::uint32_t)}}); + while (!started.load(std::memory_order_acquire)) std::this_thread::yield(); + DrawPipeline::Active() = true; + const auto after = ReadRawShader(address); + finished.store(true, std::memory_order_release); + DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission); + if (after->code.front() != 0x7e0002f2u || (before != nullptr && (before->code.front() != 0x7e000200u || before == after))) { + throw std::runtime_error(std::string(cached ? "cached" : "fresh") + " raw shader read ignored a queued write to its code"); + } +} + +} + +int main(int argc, char** argv) { + try { + if (AgcDriver::DriverDetail::DrawPipeline::Depth() == 0) return 77; + if (argc == 1 || std::string(argv[1]) == "cached") QueuedCode(true); + if (argc == 1 || std::string(argv[1]) == "fresh") QueuedCode(false); + std::cout << "Raw shader reads observed queued code writes\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RectListCorner.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RectListCorner.cpp new file mode 100644 index 0000000000..0657f99bca --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RectListCorner.cpp @@ -0,0 +1,205 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "SpirvBackend/SpirvModule.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Extent = 64; + +alignas(256) std::array pixels{}; + +class Emitter { +public: + explicit Emitter(std::uint32_t version) : builder(version) { + builder.EmitCapability(spv::CapabilityShader); + builder.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450); + voidType = type(spv::OpTypeVoid); + boolType = type(spv::OpTypeBool); + intType = type(spv::OpTypeInt, 32u, 1u); + floatType = type(spv::OpTypeFloat, 32u); + vec4Type = type(spv::OpTypeVector, floatType, 4u); + functionType = type(spv::OpTypeFunction, voidType); + } + + std::vector Vertex() { + const auto main = result(spv::OpFunction, voidType, spv::FunctionControlMaskNone, functionType); + const auto indexPointer = pointer(spv::StorageClassInput, intType); + const auto vectorPointer = pointer(spv::StorageClassOutput, vec4Type); + const auto vertexIndex = builder.DefineGlobalVariable(indexPointer, spv::StorageClassInput); + const auto position = builder.DefineGlobalVariable(vectorPointer, spv::StorageClassOutput); + const auto identity = builder.DefineGlobalVariable(vectorPointer, spv::StorageClassOutput); + decorate(vertexIndex, spv::DecorationBuiltIn, spv::BuiltInVertexIndex); + decorate(position, spv::DecorationBuiltIn, spv::BuiltInPosition); + decorate(identity, spv::DecorationLocation, 0u); + builder.EmitEntryPoint(spv::ExecutionModelVertex, main, "main", {vertexIndex, position, identity}); + result(spv::OpLabel); + const auto index = result(spv::OpLoad, intType, vertexIndex); + const auto first = result(spv::OpIEqual, boolType, index, constant(intType, 0u)); + const auto second = result(spv::OpIEqual, boolType, index, constant(intType, 1u)); + const auto positionValue = result(spv::OpSelect, vec4Type, first, vector(-1.0f, -1.0f, 0.0f, 1.0f), result(spv::OpSelect, vec4Type, second, vector(1.0f, -1.0f, 0.0f, 1.0f), vector(-1.0f, 1.0f, 0.0f, 1.0f))); + const auto identityValue = result(spv::OpSelect, vec4Type, first, vector(0.0f, 0.0f, 0.0f, 1.0f), result(spv::OpSelect, vec4Type, second, vector(1.0f, 0.0f, 0.0f, 1.0f), vector(0.0f, 1.0f, 0.0f, 1.0f))); + emit(spv::OpStore, position, positionValue); + emit(spv::OpStore, identity, identityValue); + emit(spv::OpReturn); + emit(spv::OpFunctionEnd); + return builder.Finalize(); + } + + std::vector Fragment() { + const auto main = result(spv::OpFunction, voidType, spv::FunctionControlMaskNone, functionType); + const auto vectorPointerIn = pointer(spv::StorageClassInput, vec4Type); + const auto vectorPointerOut = pointer(spv::StorageClassOutput, vec4Type); + const auto identity = builder.DefineGlobalVariable(vectorPointerIn, spv::StorageClassInput); + const auto color = builder.DefineGlobalVariable(vectorPointerOut, spv::StorageClassOutput); + decorate(identity, spv::DecorationLocation, 0u); + decorate(color, spv::DecorationLocation, 0u); + builder.EmitEntryPoint(spv::ExecutionModelFragment, main, "main", {identity, color}); + builder.AddExecutionMode(main, spv::ExecutionModeOriginUpperLeft); + result(spv::OpLabel); + emit(spv::OpStore, color, result(spv::OpLoad, vec4Type, identity)); + emit(spv::OpReturn); + emit(spv::OpFunctionEnd); + return builder.Finalize(); + } + +private: + template + std::uint32_t type(spv::Op opcode, TArgs... operands) { + return builder.Type(opcode, operands...); + } + + std::uint32_t pointer(spv::StorageClass storage, std::uint32_t valueType) { return type(spv::OpTypePointer, storage, valueType); } + + std::uint32_t constant(std::uint32_t constantType, std::uint32_t value) { return builder.Constant(spv::OpConstant, constantType, value); } + + std::uint32_t vector(float x, float y, float z, float w) { + return builder.Constant(spv::OpConstantComposite, vec4Type, constant(floatType, std::bit_cast(x)), constant(floatType, std::bit_cast(y)), constant(floatType, std::bit_cast(z)), constant(floatType, std::bit_cast(w))); + } + + template + std::uint32_t result(spv::Op opcode, std::uint32_t resultType, TArgs... operands) { + const auto id = builder.AllocateId(); + builder.AddFunction(opcode, resultType, id, operands...); + return id; + } + + std::uint32_t result(spv::Op opcode) { + const auto id = builder.AllocateId(); + builder.AddFunction(opcode, id); + return id; + } + + template + void emit(spv::Op opcode, TArgs... operands) { + builder.AddFunction(opcode, operands...); + } + + void decorate(std::uint32_t target, spv::Decoration decoration, std::uint32_t value) { + builder.AddAnnotation(spv::OpDecorate, target, decoration, value); + } + + ShaderRecompiler::SpirvModule builder; + std::uint32_t voidType = 0; + std::uint32_t boolType = 0; + std::uint32_t intType = 0; + std::uint32_t floatType = 0; + std::uint32_t vec4Type = 0; + std::uint32_t functionType = 0; +}; + +std::uint32_t VertexOf(std::uint32_t x, std::uint32_t y) { + const auto offset = (static_cast(y) * Extent + x) * 4u; + const auto red = std::to_integer(pixels[offset]); + const auto green = std::to_integer(pixels[offset + 1]); + const auto alpha = std::to_integer(pixels[offset + 3]); + Require(alpha >= 128u, "rect-list corner pixel was not drawn"); + return (red >= 128u ? 1u : 0u) + (green >= 128u ? 2u : 0u); +} + +int Run(AgcDriver::VulkanDevice& device) { + std::cout << "device: " << device.DeviceName() << "\n"; + auto target = device.Target(); + if (!target.tessellation.has_value() || !TargetHasCapability(target, spv::CapabilityTessellation)) { + std::cout << "skipped, device has no tessellation\n"; + return VulkanTestSkipped; + } + if (target.spirvVersion > 0x00010400u) target.spirvVersion = 0x00010400u; + Require(target.spirvVersion >= 0x00010300u, "rect-list corner draw needs SPIR-V 1.3"); + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + vertex.spirv = ShaderRecompiler::SharedSpirv(Emitter(target.spirvVersion).Vertex()); + fragment.spirv = ShaderRecompiler::SharedSpirv(Emitter(target.spirvVersion).Fragment()); + vertex.parameterExports = {0u}; + fragment.fragmentParameters = {{0u, 0u, false, false}}; + const auto rectangle = ShaderRecompiler::BuildRectListShaders(vertex, fragment, target); + const std::array shaders{{ + {ShaderStage::Vertex, &vertex, 0u}, + {ShaderStage::TessellationControl, &rectangle.control, 0u}, + {ShaderStage::TessellationEvaluation, &rectangle.evaluation, 0u}, + {ShaderStage::Fragment, &fragment, PixelPushOffset(0u, device.Target())}, + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.rectList = true; + state.renderExtent = {Extent, Extent}; + state.topology = VK_PRIMITIVE_TOPOLOGY_PATCH_LIST; + state.viewport = {0.0f, static_cast(Extent), static_cast(Extent), -static_cast(Extent), 0.0f, 1.0f}; + state.scissor = {{0, 0}, {Extent, Extent}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15u; + state.blends = {state.blend}; + pixels.fill(std::byte{0}); + device.Draw(state, {0u, 3u, 0u, 1u, 0u, false}, shaders); + device.WaitIdle(); + + const std::array, 4> corners{{ + {0u, Extent - 1u, 0u}, + {Extent - 1u, Extent - 1u, 1u}, + {0u, 0u, 2u}, + {Extent - 1u, 0u, 3u}, + }}; + for (const auto& corner : corners) { + const auto selected = VertexOf(corner[0], corner[1]); + std::cout << "corner (" << corner[0] << ", " << corner[1] << ") vertex " << selected << "\n"; + Require(selected == corner[2], "rect-list corner selected vertex " + std::to_string(selected) + ", expected " + std::to_string(corner[2])); + } + std::uint32_t covered = 0; + for (std::size_t offset = 3; offset < pixels.size(); offset += 4) { + if (pixels[offset] != std::byte{0}) ++covered; + } + std::cout << "covered " << covered << " of " << Extent * Extent << "\n"; + Require(covered * 5u > Extent * Extent * 4u, "rect-list rectangle covered " + std::to_string(covered) + " pixels"); + return 0; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto result = Run(*device); + if (result != 0) return result; + std::cout << "rect-list corner 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/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/RegistrationPrepareStack.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationPrepareStack.cpp new file mode 100644 index 0000000000..20c3fd9389 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationPrepareStack.cpp @@ -0,0 +1,112 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif + +namespace { + +constexpr std::uint64_t PoolStackBytes = 64ull << 20u; +constexpr std::uint64_t MostCommittedBytes = 8ull << 20u; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error("registration prepare stack: " + message); +} + +struct StackUse { + std::thread::id thread; + std::uint64_t reserved = 0; + std::uint64_t committed = 0; +}; + +StackUse MeasureCurrentStack() { + StackUse use; + use.thread = std::this_thread::get_id(); +#ifdef _WIN32 + MEMORY_BASIC_INFORMATION here{}; + const int local = 0; + Require(VirtualQuery(&local, &here, sizeof(here)) != 0, "VirtualQuery of the stack failed"); + auto cursor = reinterpret_cast(here.AllocationBase); + for (;;) { + MEMORY_BASIC_INFORMATION info{}; + if (VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == 0 || info.AllocationBase != here.AllocationBase) break; + use.reserved += info.RegionSize; + if (info.State == MEM_COMMIT) use.committed += info.RegionSize; + cursor = reinterpret_cast(info.BaseAddress) + info.RegionSize; + } +#elif defined(__APPLE__) + use.reserved = pthread_get_stacksize_np(pthread_self()); +#else + pthread_attr_t attributes; + Require(pthread_getattr_np(pthread_self(), &attributes) == 0, "pthread_getattr_np failed"); + void* base = nullptr; + std::size_t size = 0; + pthread_attr_getstack(&attributes, &base, &size); + pthread_attr_destroy(&attributes); + use.reserved = size; +#endif + return use; +} + +StackUse MeasureOn(const std::function)>& submit) { + std::mutex mutex; + std::condition_variable done; + bool measured = false; + StackUse use; + submit([&] { + const auto result = MeasureCurrentStack(); + std::lock_guard lock(mutex); + use = result; + measured = true; + done.notify_all(); + }); + std::unique_lock lock(mutex); + done.wait(lock, [&] { return measured; }); + return use; +} + +void CheckPool(const char* name, const std::function)>& submit) { + const auto use = MeasureOn(submit); + Require(use.thread != std::this_thread::get_id(), std::string(name) + " ran the task on the caller's thread"); + Require(use.reserved >= PoolStackBytes, std::string(name) + " thread reserves " + std::to_string(use.reserved >> 20u) + " MiB of stack, less than 64 MiB"); +#ifdef _WIN32 + Require(use.committed <= MostCommittedBytes, std::string(name) + " thread commits " + std::to_string(use.committed >> 20u) + " MiB of its " + std::to_string(use.reserved >> 20u) + " MiB stack up front"); +#endif +} + +} + +int main() { + try { + AgcDriver::DriverDetail::Driver::Get(); + CheckPool("registration preparation", [](std::function task) { AgcDriver::DriverDetail::SubmitRegistrationPreparation(std::move(task)); }); + CheckPool("library prebuild", [](std::function task) { AgcDriver::DriverDetail::SubmitLibraryPrebuild(std::move(task)); }); + const char* prebuildThreads = std::getenv("APS5_PREBUILD_THREADS"); + const auto started = std::max(2u, std::thread::hardware_concurrency() / 2u) + (prebuildThreads != nullptr ? std::max(1u, static_cast(std::strtoul(prebuildThreads, nullptr, 10))) : 1u); + const auto live = AgcDriver::DriverDetail::RegistrationPreparationThreads(); + Require(live == started, "the preparation and prebuild pools count " + std::to_string(live) + " live threads of the " + std::to_string(started) + " they started"); + AgcDriver::DriverDetail::CloseRegistrationPreparation(); + Require(AgcDriver::DriverDetail::RegistrationPreparationThreads() == 0, "closing the preparation and prebuild pools left " + std::to_string(AgcDriver::DriverDetail::RegistrationPreparationThreads()) + " live threads"); + std::puts("registration prepare stack tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/RelativeIndexingKnownBits.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexingKnownBits.cpp new file mode 100644 index 0000000000..37a26d0c34 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexingKnownBits.cpp @@ -0,0 +1,123 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t MaxThreads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Outputs = 16; +constexpr std::uint32_t Results = 11; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0x34060086, 0x34040088, 0xe0381000, 0x80000401, 0xf4280200, 0xfa000000, 0x4a1404ff, + 0x00001000, 0x4a1604ff, 0x00001001, 0x4a1804ff, 0x00001002, 0x4a1a04ff, 0x00001003, 0x4a1c04ff, + 0x00001004, 0x4a1e04ff, 0x00001005, 0x4a2004ff, 0x00001006, 0x4a2204ff, 0x00001007, 0x4a2404ff, + 0x00001008, 0x4a2604ff, 0x00001009, 0x4a2804ff, 0x0000100a, 0x4a2a04ff, 0x0000100b, 0x4a2c04ff, + 0x0000100c, 0x4a2e04ff, 0x0000100d, 0x4a3004ff, 0x0000100e, 0x4a3204ff, 0x0000100f, 0x4a5004ff, + 0x00002000, 0x4a5204ff, 0x00002001, 0x4a5404ff, 0x00002002, 0x4a5604ff, 0x00002003, 0x4a5804ff, + 0x00002004, 0x4a5a04ff, 0x00002005, 0x4a5c04ff, 0x00002006, 0x4a5e04ff, 0x00002007, 0xbe9403ff, + 0x00003000, 0xbe9503ff, 0x00003001, 0xbe9603ff, 0x00003002, 0xbe9703ff, 0x00003003, 0xbe9803ff, + 0x00003004, 0xbe9903ff, 0x00003005, 0xbe9a03ff, 0x00003006, 0xbe9b03ff, 0x00003007, 0x7e3402ff, + 0x00000077, 0x7e3602ff, 0x00000088, 0xbf8c0070, 0x8f0c8208, 0x887c810c, 0x7e3c870a, 0x34100882, + 0x4a101083, 0x7e1a0508, 0xbefc030d, 0x7e3e870a, 0x97fc8109, 0x7e50851a, 0x34120a82, 0x7e1a0509, + 0x887c820d, 0x7e50851a, 0x97fc810a, 0x7e50851b, 0x97fc810b, 0xbe8e2e14, 0x8f0c8209, 0x887c820c, + 0xbe9430ff, 0x00000099, 0x7e40020e, 0x7e420216, 0x7e440215, 0xe0701000, 0x80011e03, 0xe0701004, + 0x80011f03, 0xe0701008, 0x80012803, 0xe070100c, 0x80012903, 0xe0701010, 0x80012a03, 0xe0701014, + 0x80012c03, 0xe0701018, 0x80012d03, 0xe070101c, 0x80012e03, 0xe0701020, 0x80012003, 0xe0701024, + 0x80012103, 0xe0701028, 0x80012203, 0xbf810000, +}; + +constexpr const char* Names[Results] = {"movrels_shifted_or", "movrels_readfirstlane_add", "movreld_base", "movreld_past_range_1", "movreld_past_range_2", "movreld_neighbour", "movreld_wrapped_shift", "movreld_past_range_6", "s_movrels_shift_add", "s_movreld_past_range_2", "s_movreld_neighbour"}; + +void Fill(std::uint32_t tid, std::uint32_t* words) { + words[0] = tid + 2u; + words[1] = tid + 1000u; + words[2] = 0x40000001u; + words[3] = 1u; +} + +std::array Expected(std::uint32_t tid) { + const std::uint32_t lane = tid << 8u; + return {0x1009u + lane, 0x100bu + lane, 0x2000u + lane, 0x2001u + lane, 0x2002u + lane, 0x2004u + lane, 0x00000088u, 0x2006u + lane, 0x3005u, 0x3002u, 0x3001u}; +} + +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 Expect(std::uint32_t waveSize, std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, const char* name) { + Require(actual == expected, std::string("relative indexing with known M0 bits: wave") + std::to_string(waveSize) + " lane " + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected)); +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::uint32_t threads) { + for (std::uint32_t tid = 0; tid < threads; ++tid) Fill(tid, &Input[tid * Inputs]); + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(std::uint32_t waveSize, std::uint32_t threads) { + for (std::uint32_t tid = 0; tid < threads; ++tid) { + const auto expected = Expected(tid); + for (std::uint32_t index = 0; index < Results; ++index) Expect(waveSize, tid, Output[tid * Outputs + index], expected[index], Names[index]); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < 32u) { + std::printf("relative indexing with known M0 bits skipped, subgroup size %u holds less than a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + Run(*device, 32, 32); + Check(32, 32); + Run(*device, 64, 64); + Check(64, 64); + std::puts("relative indexing with known M0 bits tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/ReversedOrderBlend.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ReversedOrderBlend.cpp new file mode 100644 index 0000000000..eb88d15ef2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ReversedOrderBlend.cpp @@ -0,0 +1,524 @@ +#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; +using Color = std::array; + +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 TiledAttrib3 = 0x4dc6c000u; +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} +}}; + +constexpr std::array, 4> UnormDestinations{{ + {64, 128, 32, 200}, {255, 0, 16, 128}, {32, 224, 96, 0}, {128, 64, 192, 255} +}}; + +constexpr std::array FloatDestinations{{ + {0.5f, 0.25f, 0.75f, 0.5f}, {1.0f, 0.0f, 0.125f, 0.25f}, {0.0625f, 0.875f, 0.5f, 1.0f}, {0.25f, 0.5f, 0.375f, 0.75f} +}}; + +struct Format { + const char* name; + std::uint32_t format; + std::uint32_t number; + std::uint32_t elementBytes; + VkFormat vkFormat; + bool normalized; +}; + +constexpr std::array Formats{{ + {"8_8_8_8 UNORM", 10u, 0u, 4u, VK_FORMAT_R8G8B8A8_UNORM, true}, + {"16_16_16_16 FLOAT", 12u, 7u, 8u, VK_FORMAT_R16G16B16A16_SFLOAT, false}, +}}; + +struct Swap { + const char* name; + std::uint32_t value; + std::uint8_t mapping; + std::array memoryChannels; +}; + +constexpr Swap Standard{"SWAP_STD", 0u, 0xe4u, {0u, 1u, 2u, 3u}}; + +constexpr std::array Swaps{{ + {"SWAP_STD_REV", 2u, 0x1bu, {3u, 2u, 1u, 0u}}, + {"SWAP_ALT_REV", 3u, 0x93u, {3u, 0u, 1u, 2u}}, +}}; + +alignas(256) std::array, 8> Programs{}; +std::size_t programCount = 0; + +std::uint16_t Half(float value) { + Require(value == 0.0f || (value >= 0.0625f && value <= 1.0f), "the test's half values are normal and at most 1"); + 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 static_cast(((((bits >> 23u) & 0xffu) - 112u) << 10u) | ((bits >> 13u) & 0x3ffu)); +} + +float FromHalf(std::uint16_t half) { + const auto exponent = (half >> 10u) & 0x1fu; + const auto mantissa = half & 0x3ffu; + Require(exponent != 31u, "a blended texel is infinite or NaN"); + const auto magnitude = exponent == 0 ? std::ldexp(static_cast(mantissa), -24) : std::ldexp(1.0f + static_cast(mantissa) / 1024.0f, static_cast(exponent) - 15); + return (half & 0x8000u) != 0 ? -magnitude : magnitude; +} + +std::span Program(const Color& exported, bool compressed) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + const auto move = [&](std::uint32_t vgpr, std::uint32_t value) { + code[at++] = 0x7e0002ffu | (vgpr << 17u); + code[at++] = value; + }; + if (compressed) { + move(4, Half(exported[0]) | (Half(exported[1]) << 16u)); + move(5, Half(exported[2]) | (Half(exported[3]) << 16u)); + code[at++] = 0xf8001c0fu; + code[at++] = 0x00000504u; + } else { + for (std::uint32_t channel = 0; channel < 4; ++channel) move(4 + channel, std::bit_cast(exported[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::QueueState Queue(const Block& block, const Format& format, const Swap& swap, std::uint32_t control, std::uint32_t targetMask, std::uint32_t exportFormat) { + AgcDriver::QueueState queue; + queue.userConfig[0x242] = 4; + queue.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, targetMask}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, exportFormat}, {0x1c3, 4}, + {0x318, static_cast(block.Address() >> 8u)}, {0x390, static_cast(block.Address() >> 40u)}, + {0x31b, 0}, {0x31c, (format.format << 2u) | (format.number << 8u) | (swap.value << 11u) | (format.normalized ? 0x8000u : 0u)}, {0x31d, 0}, + {0x3b0, ((Width - 1u) << 14u) | (Height - 1u)}, {0x3b8, TiledAttrib3}, {0x1e0, control}, + {0x105, 0}, {0x106, 0}, {0x107, 0}, {0x108, 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}, + {0x10f, std::bit_cast(static_cast(Width) / 2.0f)}, {0x110, std::bit_cast(static_cast(Width) / 2.0f)}, + {0x111, std::bit_cast(-static_cast(Height) / 2.0f)}, {0x112, std::bit_cast(static_cast(Height) / 2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)} + }; + queue.shader[0x008] = 0x100; + queue.shader[0x009] = 0; + return queue; +} + +struct Renderer { + AgcDriver::VulkanDevice& device; + ShaderRecompiler::RecompileResult vertex; + std::uint32_t vertexPush = 0; +}; + +ShaderRecompiler::RecompileResult CompileVertex(AgcDriver::VulkanDevice& device) { + 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, {}}, + {64, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + device.Target(), + {0, 0, 0, 64} + }; + vertex.useCache = false; + return ShaderRecompiler::Recompile(vertex); +} + +void Draw(Renderer& renderer, const AgcDriver::Graphics::State& decoded, std::uint32_t exportFormat, std::span pixelCode) { + const auto target = renderer.device.Target(); + constexpr std::uint32_t waveSize = 64; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = decoded.colors; + state.color = decoded.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; + state.blends = decoded.blends; + state.blend = decoded.blend; + state.blendConstants = decoded.blendConstants; + state.dualSourceBlend = decoded.dualSourceBlend; + state.reversedBlend = decoded.reversedBlend; + + 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(renderer.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->reversedBlend = state.reversedBlend; + 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"); + if (state.reversedBlend != 0u) { + auto plain = fragment; + plain.context.pixel->reversedBlend = 0u; + const auto unblended = materialize(plain); + Require(unblended->variantId == pixelResult->variantId && unblended->PipelineVariantId() != pixelResult->PipelineVariantId(), "the reversed-order blend code did not key its own specialization of the registered shader"); + } + const std::array shaders{{ + {ShaderStage::Vertex, &renderer.vertex, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(renderer.vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + renderer.device.Draw(state, draw, shaders); +} + +Color Destination(const Format& format, std::uint32_t pick) { + if (!format.normalized) return FloatDestinations[pick]; + Color color{}; + for (std::uint32_t channel = 0; channel < 4; ++channel) color[channel] = static_cast(UnormDestinations[pick][channel]) / 255.0f; + return color; +} + +std::vector Fill(Block& block, const ColorTargetLayout& layout, const Format& format, const Swap& swap) { + std::vector bytes(block.bytes); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto pick = (x + y) % 4u; + const auto index = layout.Offset(x, y); + for (std::uint32_t component = 0; component < 4; ++component) { + const auto channel = swap.memoryChannels[component]; + if (format.normalized) { + bytes[index + component] = UnormDestinations[pick][channel]; + } else { + const auto half = Half(FloatDestinations[pick][channel]); + bytes[index + 2u * component] = static_cast(half); + bytes[index + 2u * component + 1u] = static_cast(half >> 8u); + } + } + } + } + std::memcpy(block.data, bytes.data(), block.bytes); + return bytes; +} + +float Factor(std::uint32_t factor, std::uint32_t channel, const Color& source, const Color& destination) { + const bool alpha = channel == 3u; + switch (factor) { + case 0: return 0.0f; + case 1: return 1.0f; + case 2: return source[channel]; + case 3: return 1.0f - source[channel]; + case 4: return source[3]; + case 5: return 1.0f - source[3]; + case 6: return destination[3]; + case 7: return 1.0f - destination[3]; + case 8: return destination[channel]; + case 9: return 1.0f - destination[channel]; + case 10: return alpha ? 1.0f : std::min(source[3], 1.0f - destination[3]); + default: throw std::runtime_error("the reference blend has no factor " + std::to_string(factor)); + } +} + +Color Blend(std::uint32_t control, const Color& exported, const Color& destination, bool normalized) { + Color source = exported; + if (normalized) { + for (auto& value : source) value = std::clamp(value, 0.0f, 1.0f); + } + const auto alphaControl = (control & 0x20000000u) != 0 ? control >> 16u : control; + Color result{}; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto fields = channel == 3u ? alphaControl : control; + const auto weighted = source[channel] * Factor(fields & 0x1fu, channel, source, destination); + const auto kept = destination[channel] * Factor((fields >> 8u) & 0x1fu, channel, source, destination); + float value = 0.0f; + switch ((fields >> 5u) & 7u) { + case 0: value = weighted + kept; break; + case 1: value = weighted - kept; break; + case 2: value = std::min(weighted, kept); break; + case 3: value = std::max(weighted, kept); break; + case 4: value = kept - weighted; break; + default: throw std::runtime_error("the reference blend has no combine function " + std::to_string((fields >> 5u) & 7u)); + } + result[channel] = normalized ? std::clamp(value, 0.0f, 1.0f) : value; + } + return result; +} + +void Check(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const Format& format, const Swap& swap, std::uint32_t targetMask, const std::vector& before, std::uint32_t control, const Color& exported, const std::string& what) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto pick = (x + y) % 4u; + const auto index = layout.Offset(x, y); + const auto expected = Blend(control, exported, Destination(format, pick), format.normalized); + for (std::uint32_t component = 0; component < 4; ++component) { + const auto channel = swap.memoryChannels[component]; + const bool written = x < ScissorWidth && ((targetMask >> channel) & 1u) != 0; + bool match = false; + double held = 0.0; + double want = 0.0; + if (format.normalized) { + held = stored[index + component]; + want = written ? expected[channel] * 255.0 : before[index + component]; + match = written ? std::fabs(held - want) <= 1.0 : stored[index + component] == before[index + component]; + } else { + const auto at = index + 2u * component; + const auto half = static_cast(stored[at] | (stored[at + 1u] << 8u)); + const auto previous = static_cast(before[at] | (before[at + 1u] << 8u)); + held = FromHalf(half); + want = written ? expected[channel] : FromHalf(previous); + match = written ? std::fabs(held - want) <= std::ldexp(std::max(1.0, std::fabs(want)), -11) : half == previous; + } + if (match) continue; + char detail[192]; + std::snprintf(detail, sizeof(detail), ": memory component %u (channel %u) of pixel (%u, %u) %s holds %.6g, expected %.6g", component, channel, x, y, written ? "inside the scissor" : "outside the written area", held, want); + throw std::runtime_error(what + detail); + } + } + } +} + +std::vector Stored(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const Format& format, const Swap& swap, std::vector& bits) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + std::vector values(static_cast(Width) * Height * 4u); + bits.assign(values.size(), 0u); + 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); + for (std::uint32_t component = 0; component < 4; ++component) { + const auto at = (static_cast(y) * Width + x) * 4u + swap.memoryChannels[component]; + bits[at] = format.normalized ? stored[index + component] : static_cast(stored[index + 2u * component] | (stored[index + 2u * component + 1u] << 8u)); + values[at] = format.normalized ? static_cast(bits[at]) : static_cast(FromHalf(bits[at])); + } + } + } + return values; +} + +void CompareWithStandardOrder(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const Format& format, const Swap& swap, const std::vector& standard, const std::string& what) { + std::vector reversed; + const auto values = Stored(device, block, layout, format, swap, reversed); + for (std::size_t at = 0; at < reversed.size(); ++at) { + if (reversed[at] == standard[at]) continue; + const auto pixel = at / 4u; + char detail[224]; + std::snprintf(detail, sizeof(detail), ": channel %u of pixel (%u, %u) holds %.6g (bits 0x%04x), while the same draw into a SWAP_STD target, which Vulkan blends alone, holds bits 0x%04x", static_cast(at % 4u), static_cast(pixel % Width), static_cast(pixel / Width), values[at], reversed[at], standard[at]); + throw std::runtime_error(what + detail); + } +} + +struct BlendCase { + const char* name; + std::uint32_t control; + std::uint32_t targetMask; + bool shaded; +}; + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const bool dualSource = device->DualSrcBlend(); + Renderer renderer{*device, CompileVertex(*device)}; + renderer.vertexPush = static_cast(renderer.vertex.pushConstants.size()); + const Color exported{0.25f, 0.75f, 0.125f, 0.5f}; + const Color outOfRange{1.5f, -0.25f, 0.5f, 1.25f}; + const Color strongAlpha{0.25f, 0.75f, 0.125f, 1.5f}; + const auto wide = Program(exported, false); + const auto compressed = Program(exported, true); + const auto clamped = Program(outOfRange, false); + const auto unclamped = Program(strongAlpha, false); + const std::array cases{{ + {"SRC_ALPHA + ONE_MINUS_SRC_ALPHA, alpha ONE + ONE_MINUS_SRC_ALPHA", 0x65010504u, 0xfu, true}, + {"SRC_ALPHA + ONE_MINUS_SRC_ALPHA, alpha ONE + ONE_MINUS_SRC_ALPHA, red, green and blue written", 0x65010504u, 0x7u, true}, + {"SRC_ALPHA reverse-subtract ONE_MINUS_SRC_ALPHA, alpha ONE reverse-subtract ONE_MINUS_SRC_ALPHA", 0x65810584u, 0xfu, true}, + {"ONE + ONE_MINUS_SRC_ALPHA", 0x40000501u, 0xfu, true}, + {"SRC_ALPHA + ONE, alpha ZERO + ONE", 0x61000104u, 0xfu, true}, + {"SRC_COLOR + ONE_MINUS_SRC_COLOR, alpha SRC_COLOR + ONE", 0x61020302u, 0xfu, true}, + {"SRC_ALPHA + ONE_MINUS_SRC_ALPHA, alpha ONE + ONE", 0x61010504u, 0xfu, true}, + {"DST_COLOR + ZERO, alpha DST_ALPHA + ZERO", 0x60060008u, 0xfu, false}, + {"ONE + ONE", 0x40000101u, 0xfu, false}, + {"ONE MIN ONE", 0x40000141u, 0xfu, false}, + }}; + bool skipped = false; + for (const auto& format : Formats) { + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, format.elementBytes); + Block block(layout.Bytes()); + for (const auto& swap : Swaps) { + const auto run = [&](const BlendCase& test, std::uint32_t exportFormat, std::span program, const Color& values, const char* exports) { + const auto what = std::string(format.name) + " " + swap.name + ", " + test.name + ", " + exports; + Fill(block, layout, format, Standard); + const auto plain = AgcDriver::Graphics::DecodeState(Queue(block, format, Standard, test.control, test.targetMask, exportFormat)); + Require(plain.colors.size() == 1 && plain.colors[0].componentMapping == Standard.mapping && plain.reversedBlend == 0u && !plain.dualSourceBlend, what + ": the SWAP_STD reference target did not decode as a plain Vulkan blend"); + Draw(renderer, plain, exportFormat, program); + std::vector standard; + Stored(*device, block, layout, format, Standard, standard); + const auto before = Fill(block, layout, format, swap); + const auto decoded = AgcDriver::Graphics::DecodeState(Queue(block, format, swap, test.control, test.targetMask, exportFormat)); + Require(decoded.colors.size() == 1 && decoded.colors[0].format == format.vkFormat && decoded.colors[0].componentMapping == swap.mapping && decoded.colors[0].bytes == block.bytes, what + ": the target did not decode as a reversed 256x128 surface"); + Require((decoded.reversedBlend != 0u) == test.shaded && !decoded.dualSourceBlend, what + ": the blend was not split between Vulkan and the pixel shader as expected"); + if (test.shaded && !dualSource) { + skipped = true; + return; + } + Draw(renderer, decoded, exportFormat, program); + Check(*device, block, layout, format, swap, test.targetMask, before, test.control, values, what); + CompareWithStandardOrder(*device, block, layout, format, swap, standard, what); + }; + for (const auto& test : cases) run(test, Float32Export, wide, exported, "32_ABGR exports"); + run(cases[0], Float16Export, compressed, exported, "FP16_ABGR exports"); + if (format.normalized) { + run(cases[0], Float32Export, clamped, outOfRange, "32_ABGR exports outside [0, 1]"); + run(cases[5], Float32Export, clamped, outOfRange, "32_ABGR exports outside [0, 1]"); + } else { + run(cases[0], Float32Export, unclamped, strongAlpha, "32_ABGR exports with an alpha of 1.5"); + } + } + } + if (skipped) { + std::puts("skipped, the device has no dual-source blending, so only the blends Vulkan applies alone ran"); + return VulkanTestSkipped; + } + std::puts("reversed component order blend 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/SampleMaskDepthStencil.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskDepthStencil.cpp new file mode 100644 index 0000000000..412ceb0a5d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskDepthStencil.cpp @@ -0,0 +1,215 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "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 Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t WaveSize = 64; +constexpr std::uint32_t EarlyZThenLateZ = 0x10u; +constexpr std::uint8_t Reference = 0x5au; +constexpr std::byte Kept{0x40}; +alignas(256) std::array Pixels{}; +alignas(256) std::array DepthMemory{}; +alignas(256) std::array StencilMemory{}; +alignas(256) std::array Stores{}; + +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 StoringPixelCode{ + 0x7e040f00, 0x7e060f01, 0x34060686, 0x4a040702, 0x34040482, 0x7e080281, 0xe0705000, 0x80000402, + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +using Triangle = std::array, 3>; +alignas(256) constexpr Triangle Middle{{{-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) constexpr Triangle Near{{{-1.0f, -1.0f, 0.25f, 1.0f}, {3.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 3.0f, 0.25f, 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, stride == 0u ? 0x31016facu : 0x01016facu}; +} + +struct Pass { + std::array quadSampleMask{1, 1, 1, 1}; + const Triangle* triangle = &Middle; + std::span pixelCode = PlainPixelCode; + std::uint32_t shaderControl = EarlyZThenLateZ; + bool depthTest = false; + bool depthWrite = false; + VkCompareOp depthCompare = VK_COMPARE_OP_ALWAYS; + VkStencilOpState stencil{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0u, 0u}; +}; + +void Draw(AgcDriver::VulkanDevice& device, const Pass& pass) { + Pixels.fill(Kept); + Stores.fill(0); + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(pass.triangle->data(), 16u, static_cast(pass.triangle->size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.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, 0.0f, 0}; + state.depthTest = pass.depthTest; + state.depthWrite = pass.depthWrite; + state.depthCompare = pass.depthCompare; + state.stencilTest = true; + state.stencilFront = pass.stencil; + state.stencilBack = pass.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 = {}; + state.quadSampleMask = pass.quadSampleMask; + state.pixelEarlyZ = AgcDriver::Graphics::PixelEarlyZ(pass.shaderControl); + + const AgcDriver::Registers context{{0x1b3, 0x300u}, {0x1b4, 0x300u}, {0x1b6, 0u}, {0x1c5, 9u}, {0x203, pass.shaderControl}}; + auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + Require(pixel.earlyZ == state.pixelEarlyZ, "the pixel stage and the decoded state disagree on early Z"); + pixel.quadPixelMask = AgcDriver::Graphics::QuadPixelMask(state); + std::vector pixelUserData(4, 0u); + const auto stores = BufferDescriptor(Stores.data(), 0u, static_cast(sizeof(Stores))); + std::copy(stores.begin(), stores.end(), pixelUserData.begin()); + const std::array pixelMemory{{{reinterpret_cast(pass.pixelCode.data()), std::as_bytes(pass.pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pass.pixelCode.data()), pass.pixelCode, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(pass.triangle->size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(const std::function& drawn, const std::string& 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, what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + std::to_string(static_cast(actual)) + ", expected " + (drawn(x, y) ? "drawn" : "kept")); + } + } +} + +bool Covered(std::uint8_t quadPixelMask, std::uint32_t x, std::uint32_t y) { + return ((quadPixelMask >> ((x & 1u) | ((y & 1u) << 1u))) & 1u) != 0u; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const VkStencilOpState replace{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, Reference}; + const VkStencilOpState equal{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, Reference}; + const VkStencilOpState always{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0u, 0u}; + const VkStencilOpState zero{VK_STENCIL_OP_ZERO, VK_STENCIL_OP_ZERO, VK_STENCIL_OP_ZERO, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 0u}; + for (const std::uint8_t quadPixelMask : {std::uint8_t{0x1}, std::uint8_t{0x2}, std::uint8_t{0x9}}) { + const auto mask = "quad pixel mask " + std::to_string(quadPixelMask); + Pass reset; + reset.triangle = &Near; + reset.depthTest = true; + reset.depthWrite = true; + reset.stencil = zero; + Draw(*device, reset); + Check([](std::uint32_t, std::uint32_t) { return true; }, mask + ", the full-screen draw that resets depth to 0.25 and stencil to 0"); + Pass gbuffer; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) gbuffer.quadSampleMask[pixel] = static_cast((quadPixelMask >> pixel) & 1u); + gbuffer.depthTest = true; + gbuffer.depthWrite = true; + gbuffer.depthCompare = VK_COMPARE_OP_GREATER_OR_EQUAL; + gbuffer.stencil = replace; + Draw(*device, gbuffer); + Check([&](std::uint32_t x, std::uint32_t y) { return Covered(quadPixelMask, x, y); }, mask + ", the early-Z GEQUAL draw with depth writes and stencil REPLACE"); + Pass stencilProbe; + stencilProbe.stencil = equal; + Draw(*device, stencilProbe); + Check([&](std::uint32_t x, std::uint32_t y) { return Covered(quadPixelMask, x, y); }, mask + ", stencil equal to the replaced reference"); + Pass depthProbe; + depthProbe.triangle = &Near; + depthProbe.depthTest = true; + depthProbe.depthCompare = VK_COMPARE_OP_LESS; + depthProbe.stencil = always; + Draw(*device, depthProbe); + Check([&](std::uint32_t x, std::uint32_t y) { return Covered(quadPixelMask, x, y); }, mask + ", depth 0.25 less than the stored depth"); + depthProbe.depthCompare = VK_COMPARE_OP_GREATER_OR_EQUAL; + Draw(*device, depthProbe); + Check([&](std::uint32_t x, std::uint32_t y) { return !Covered(quadPixelMask, x, y); }, mask + ", depth 0.25 at least the stored depth"); + } + Pass storing; + storing.quadSampleMask = {1, 0, 0, 0}; + storing.pixelCode = StoringPixelCode; + storing.depthTest = true; + storing.depthCompare = VK_COMPARE_OP_ALWAYS; + storing.stencil = always; + try { + Draw(*device, storing); + } catch (const std::exception& error) { + Require(std::string(error.what()).find("differs between the pixels of a quad") != std::string::npos, std::string("a storing early-Z pixel program under a quad mask failed for another reason: ") + error.what()); + storing.shaderControl = 0u; + Draw(*device, storing); + Check([](std::uint32_t x, std::uint32_t y) { return Covered(0x1, x, y); }, "the storing late-Z pixel program under quad pixel mask 1"); + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const auto expected = Covered(0x1, pixel % Width, pixel / Width) ? 1u : 0u; + Require(Stores[pixel] == expected, "the storing pixel program under quad pixel mask 1 stored " + std::to_string(Stores[pixel]) + " at pixel " + std::to_string(pixel) + ", expected " + std::to_string(expected)); + } + std::puts("quad sample mask depth and stencil tests passed"); + return 0; + } + Require(false, "a quad-masked early-Z pixel program that stores to memory was drawn with depth tests"); + return 1; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskDraw.cpp new file mode 100644 index 0000000000..867590f0cf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskDraw.cpp @@ -0,0 +1,213 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t SetContextReg = 0x69u; +constexpr std::uint32_t SetShReg = 0x76u; +constexpr std::uint32_t SetUconfigReg = 0x79u; +constexpr std::uint32_t DrawIndexAuto = 0x2du; +constexpr std::size_t Block = 65536; +constexpr std::size_t ColorBytes = Width * Height * 4; +std::byte* ColorMemory = nullptr; +alignas(64) volatile std::uint32_t EndOfPipe = 0; + +std::byte* AllocateWatched(std::size_t bytes) { +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (raw == MAP_FAILED) throw std::runtime_error("cannot map the watched block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + return static_cast(block); +} + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80020005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0002f2, 0x7e0202f2, 0x7e0402f2, 0x7e0602f2, 0xf800080f, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array, 3> Vertices{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}, +}}; + +struct Step { + const char* name; + std::uint32_t top; + std::uint32_t bottom; + std::uint32_t shaderControl; + std::uint8_t quadPixels; +}; + +const std::array Steps{{ + {"PA_SC_AA_MASK 0x0000ffff/0 with early Z (GTA V, X0Y0)", 0x0000ffffu, 0u, 0x10u, 0x1u}, + {"PA_SC_AA_MASK 0x0000ffff/0", 0x0000ffffu, 0u, 0x800u, 0x1u}, + {"PA_SC_AA_MASK 0xffff0000/0 (X1Y0)", 0xffff0000u, 0u, 0x800u, 0x2u}, + {"PA_SC_AA_MASK 0/0xffff0000 (X1Y1)", 0u, 0xffff0000u, 0x800u, 0x8u}, + {"PA_SC_AA_MASK 0xfffe0001/0x0001fffe (X0Y0 and X1Y1 by bit 0)", 0xfffe0001u, 0x0001fffeu, 0x800u, 0x9u}, + {"PA_SC_AA_MASK 0x00010001/0x00010001 (full at one sample)", 0x00010001u, 0x00010001u, 0x800u, 0xfu}, + {"PA_SC_AA_MASK 0/0 (empty)", 0u, 0u, 0x800u, 0x0u}, + {"full PA_SC_AA_MASK", 0xffffffffu, 0xffffffffu, 0x800u, 0xfu}, +}}; + +struct RegisteredShader { + Shader shader; + ShaderUserData userData; +}; +RegisteredShader VertexShader{}; +RegisteredShader PixelShader{}; + +const std::vector> ContextBaseline{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, + {0x81, 0x80000000}, {0x90, 0x80000000}, {0x94, 0x80000000}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, {0x114, 0}, {0x113, std::bit_cast(1.0f)}, +}; + +void Register(RegisteredShader& registered, const void* code, std::uint32_t bytes, std::uint8_t type) { + registered.shader.file_header = 0x34333231u; + registered.shader.version = 0x18u; + registered.shader.user_data = ®istered.userData; + registered.shader.code = code; + registered.shader.header_size = sizeof(RegisteredShader); + registered.shader.shader_size = bytes; + registered.shader.type = type; + AgcDriverRegisterShader_nid_postfix(®istered.shader); +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void SetRegister(std::vector& words, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { + words.push_back(0xc0010000u | (opcode << 8u)); + words.push_back(offset); + words.push_back(value); +} + +std::vector Frame(const Step& step) { + const auto colorAddress = reinterpret_cast(ColorMemory); + const auto vertexAddress = reinterpret_cast(VertexCode.data()); + const auto pixelAddress = reinterpret_cast(PixelCode.data()); + std::vector words; + for (const auto& [offset, value] : ContextBaseline) SetRegister(words, SetContextReg, offset, value); + SetRegister(words, SetContextReg, 0x203, step.shaderControl); + SetRegister(words, SetContextReg, 0x30e, step.top); + SetRegister(words, SetContextReg, 0x30f, step.bottom); + SetRegister(words, SetContextReg, 0x318, static_cast(colorAddress >> 8u)); + SetRegister(words, SetContextReg, 0x390, static_cast(colorAddress >> 40u)); + SetRegister(words, SetContextReg, 0x3b0, ((Width - 1u) << 14u) | (Height - 1u)); + SetRegister(words, SetContextReg, 0x10f, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x110, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x111, std::bit_cast(-(Height / 2.0f))); + SetRegister(words, SetContextReg, 0x112, std::bit_cast(Height / 2.0f)); + const auto extent = (Height << 16u) | Width; + SetRegister(words, SetContextReg, 0xd, extent); + SetRegister(words, SetContextReg, 0x82, extent); + SetRegister(words, SetContextReg, 0x91, extent); + SetRegister(words, SetContextReg, 0x95, extent); + SetRegister(words, SetUconfigReg, 0x242, 4u); + SetRegister(words, SetShReg, 0x008, static_cast(pixelAddress >> 8u)); + SetRegister(words, SetShReg, 0x009, static_cast(pixelAddress >> 40u)); + SetRegister(words, SetShReg, 0x00b, 0u); + SetRegister(words, SetShReg, 0x0c8, static_cast(vertexAddress >> 8u)); + SetRegister(words, SetShReg, 0x0c9, static_cast(vertexAddress >> 40u)); + SetRegister(words, SetShReg, 0x08b, 4u << 1u); + const auto descriptor = BufferDescriptor(Vertices.data(), 16u, static_cast(Vertices.size())); + for (std::uint32_t i = 0; i < descriptor.size(); ++i) SetRegister(words, SetShReg, 0x08c + i, descriptor[i]); + words.insert(words.end(), {0xc0000000u | (1u << 16u) | (DrawIndexAuto << 8u), static_cast(Vertices.size()), 2u}); + const auto labelAddress = reinterpret_cast(&EndOfPipe); + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(labelAddress), static_cast(static_cast(labelAddress) >> 32u), 1u, 0u, 0u}); + return words; +} + +void Check(const Step& step, const std::vector& colors) { + const auto what = std::string("sample mask through the driver, ") + step.name; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto value = std::to_integer(colors[(y * Width + x) * 4u]); + const bool expected = ((step.quadPixels >> ((x & 1u) | ((y & 1u) << 1u))) & 1u) != 0u; + Require(value == (expected ? 255u : 0u), what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") stored " + std::to_string(value) + ", expected " + (expected ? "written" : "clear")); + } + } +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ColorMemory = AllocateWatched(Block); + Register(VertexShader, VertexCode.data(), sizeof(VertexCode), 2); + Register(PixelShader, PixelCode.data(), sizeof(PixelCode), 1); + for (int round = 0; round < 2; ++round) { + for (const auto& step : Steps) { + std::fill_n(ColorMemory, ColorBytes, std::byte{0}); + EndOfPipe = 0; + auto words = Frame(step); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, std::string("DCB submission failed for ") + step.name); + AgcDriverWaitIdle_nid_postfix(); + Require(EndOfPipe == 1u, std::string("the end-of-pipe label was not written for ") + step.name); + std::vector colors(ColorBytes); + AgcDriver::GuestMemory::Read(static_cast(reinterpret_cast(ColorMemory)), colors); + Check(step, colors); + } + } + AgcDriverShutdown_nid_postfix(); + std::puts("sample mask draws through the driver passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskMultisample.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskMultisample.cpp new file mode 100644 index 0000000000..6daebdbc82 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskMultisample.cpp @@ -0,0 +1,212 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::PixelInput; +using ShaderRecompiler::PixelInputBit; +using ShaderRecompiler::ShaderStage; +using Masks = std::array; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t Samples = 4; +constexpr std::uint32_t WaveSize = 64; +constexpr std::uint32_t EarlyZThenLateZ = 0x10u; +constexpr Masks Full{0xf, 0xf, 0xf, 0xf}; +alignas(256) std::array Invocations{}; +alignas(256) std::array DepthMemory{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array SampleStoreCode{ + 0x7e100f04, 0x7e120f05, 0x34121286, 0x4a101308, 0x34101082, 0xd548000a, 0x02111106, 0x4a101508, + 0x34101082, 0x7e160281, 0xe0705000, 0x80000b08, 0xbf8c3f70, 0xf8001890, 0x00000000, 0xbf810000, + 0xbf810000, +}; + +alignas(256) constexpr std::array NullExportCode{0xf8001890, 0x00000000, 0xbf810000}; + +using Triangle = std::array, 3>; +alignas(256) constexpr Triangle Far{{{-1.0f, -1.0f, 0.25f, 1.0f}, {3.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 3.0f, 0.25f, 1.0f}}}; +alignas(256) constexpr Triangle Probe{{{-1.0f, -1.0f, 0.375f, 1.0f}, {3.0f, -1.0f, 0.375f, 1.0f}, {-1.0f, 3.0f, 0.375f, 1.0f}}}; +alignas(256) constexpr Triangle Middle{{{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 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, stride == 0u ? 0x31016facu : 0x01016facu}; +} + +struct Pass { + Masks quadSampleMask = Full; + bool perSample = true; + std::uint32_t shaderControl = 0u; + const Triangle* triangle = &Middle; + bool depth = false; + bool depthWrite = false; + VkCompareOp depthCompare = VK_COMPARE_OP_ALWAYS; +}; + +void Draw(AgcDriver::VulkanDevice& device, const Pass& pass) { + Invocations.fill(0); + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(pass.triangle->data(), 16u, static_cast(pass.triangle->size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.hasColorTarget = false; + 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.blendConstants = {}; + state.samples = Samples; + state.perSampleShading = pass.perSample; + state.quadSampleMask = pass.quadSampleMask; + state.pixelEarlyZ = AgcDriver::Graphics::PixelEarlyZ(pass.shaderControl); + if (pass.depth) { + state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(DepthMemory.data()), 0u, {Width, Height}, VK_FORMAT_D32_SFLOAT, 0.0f, 0}; + state.depth->samples = Samples; + state.depthTest = true; + state.depthWrite = pass.depthWrite; + state.depthCompare = pass.depthCompare; + } + + const std::uint32_t inputs = pass.perSample ? PixelInputBit(PixelInput::PerspectiveSample) | PixelInputBit(PixelInput::LinearSample) | PixelInputBit(PixelInput::PositionX) | PixelInputBit(PixelInput::PositionY) | PixelInputBit(PixelInput::Ancillary) : PixelInputBit(PixelInput::PerspectiveCenter); + const AgcDriver::Registers context{{0x1b3, inputs}, {0x1b4, inputs}, {0x1b6, 0u}, {0x1c5, 0u}, {0x203, pass.shaderControl}}; + auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + Require(pixel.sampleShading == pass.perSample, "the pixel inputs did not select the requested shading rate"); + pixel.quadPixelMask = AgcDriver::Graphics::QuadPixelMask(state); + pixel.quadSampleMask = AgcDriver::Graphics::QuadSampleMasks(state); + pixel.iteratesSamples = state.perSampleShading; + pixel.sampleLocations = AgcDriver::Graphics::PixelSampleLocations(state, pixel); + std::vector pixelUserData(4, 0u); + const auto invocations = BufferDescriptor(Invocations.data(), 0u, static_cast(sizeof(Invocations))); + std::copy(invocations.begin(), invocations.end(), pixelUserData.begin()); + const std::span code = pass.perSample ? std::span(SampleStoreCode) : std::span(NullExportCode); + const std::array pixelMemory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(code.data()), code, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(pass.triangle->size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(const Masks& quadSampleMask, const std::string& what) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto mask = quadSampleMask[(x & 1u) | ((y & 1u) << 1u)]; + for (std::uint32_t sample = 0; sample < Samples; ++sample) { + const auto expected = (mask >> sample) & 1u; + const auto ran = Invocations[(y * Width + x) * Samples + sample]; + Require(ran == expected, what + ": sample " + std::to_string(sample) + " of pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") " + (ran != 0u ? "stored" : "did not store") + " (" + std::to_string(ran) + "), the quad pixel's mask is " + std::to_string(mask)); + } + } + } +} + +void DepthWritten(AgcDriver::VulkanDevice& device, const Masks& mask, const std::string& what) { + Pass reset; + reset.perSample = false; + reset.triangle = &Far; + reset.depth = true; + reset.depthWrite = true; + Draw(device, reset); + Pass masked = reset; + masked.quadSampleMask = mask; + masked.triangle = &Middle; + Draw(device, masked); + Pass probe; + probe.shaderControl = EarlyZThenLateZ; + probe.triangle = &Probe; + probe.depth = true; + probe.depthCompare = VK_COMPARE_OP_LESS; + Draw(device, probe); + Check(mask, "the samples a pixel-rate draw with " + what + " wrote depth 0.5 into, probed by early-Z per-sample stores"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + struct Case { + const char* name; + Masks mask; + }; + const std::array cases{{ + {"no sample mask", Full}, + {"samples 0 and 2 of every pixel", {0x5, 0x5, 0x5, 0x5}}, + {"sample 3 of every pixel", {0x8, 0x8, 0x8, 0x8}}, + {"no sample of any pixel", {0x0, 0x0, 0x0, 0x0}}, + {"every sample of the even-x, even-y pixel of each quad", {0xf, 0x0, 0x0, 0x0}}, + {"every sample of the odd-x pixels of each quad", {0x0, 0xf, 0x0, 0xf}}, + {"sample q of quad pixel q", {0x1, 0x2, 0x4, 0x8}}, + {"all, none, none and samples 0 and 1", {0xf, 0x0, 0x0, 0x3}}, + {"samples 1 and 3, 0 and 2, 2 and 3, and 0", {0xa, 0x5, 0xc, 0x1}}, + }}; + for (const auto& test : cases) { + Pass pass; + pass.quadSampleMask = test.mask; + Draw(*device, pass); + Check(test.mask, std::string("4x per-sample shading with ") + test.name); + } + for (const auto& test : cases) DepthWritten(*device, test.mask, test.name); + Pass early; + early.quadSampleMask = {0x6, 0x6, 0x6, 0x6}; + early.shaderControl = EarlyZThenLateZ; + try { + Draw(*device, early); + } catch (const std::exception& error) { + Require(std::string(error.what()).find("earlyFragmentSampleMaskTestBeforeSampleCounting") != std::string::npos, std::string("a sample mask with early fragment tests failed for another reason: ") + error.what()); + std::puts("4x sample mask tests passed (early fragment tests refused: the device does not order the sample mask test before sample counting)"); + return 0; + } + Check(early.quadSampleMask, "4x per-sample shading with early fragment tests and samples 1 and 2 of every pixel"); + std::puts("4x sample mask 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/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp new file mode 100644 index 0000000000..abe81b5503 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -0,0 +1,971 @@ +#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/DepthSurface.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" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.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 +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Side = 16; +constexpr std::uint32_t Samples = 4; +constexpr std::uint32_t SampleShift = 2; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Float16x4Info = 0x0730u; +constexpr std::uint32_t MultisampledAttrib = (SampleShift << 12u) | (SampleShift << 15u); +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 TileZ64KBX = 0x18; +constexpr std::uint32_t TileS64KBX = 0x19; +constexpr std::uint32_t TileD64KBX = 0x1a; +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t LoadResult = 12; +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, +}; + +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, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf0001f30, 0x00011005, + 0xbf8c3f70, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, 0x80001201, 0xe070103c, + 0x80001301, 0xbf810000, +}; + +alignas(256) constexpr std::array ArrayLoadCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf0001f38, 0x00011004, + 0xbf8c3f70, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, 0x80001201, 0xe070103c, + 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} +}}; + +alignas(256) std::array Buffer{}; + +struct Block { + explicit Block(std::size_t bytes, bool watched = false) : bytes(bytes), watched(watched) { +#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"); + std::memset(data, 0, bytes); + 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() { + { + 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] = Float16x4Info; + 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_R16G16B16A16_SFLOAT && color.samples == Samples && color.extent.width == Side && color.extent.height == Side, "the target did not decode as a 16x16 RGBA16F surface of 4 samples"); + 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; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Side, Side}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Side), static_cast(Side), -static_cast(Side), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Side, Side}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + state.samples = color.samples; + return 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); + 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()); + 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{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::uint32_t format = Format16161616Float, std::uint32_t tileMode = TileRenderTarget) { + const auto address = block.Address(); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (sampleShift << 16u) | (tileMode << 20u) | (Type2DMsaa << 28u), + 0u, + sampleShift << 4u, + 0u, + 0u, + }; +} + +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::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); +} + +ShaderRecompiler::RecompileResult CompileLoad(AgcDriver::VulkanDevice& device, const std::array& texture, std::vector& userData, bool arrayed = false) { + return CompileProgram(device, arrayed ? ArrayLoadCode : LoadCode, texture, userData); +} + +void StoreSamples(AgcDriver::VulkanDevice& device, const std::array& texture, const std::vector& writes) { + std::vector userData; + const auto program = CompileProgram(device, StoreCode, texture, userData); + for (std::size_t first = 0; first < writes.size(); first += Threads) { + Buffer.fill(0u); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto& write = writes[std::min(first + tid, writes.size() - 1)]; + const std::array words{0u, 0u, 0u, write.x, write.y, write.sample, std::bit_cast(Written[0]), std::bit_cast(Written[1]), std::bit_cast(Written[2]), std::bit_cast(Written[3])}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(StoreCode.data())); + device.WaitIdle(); + } +} + +std::vector SampleWrites(bool (*selected)(std::uint32_t x, std::uint32_t y, std::uint32_t sample)) { + std::vector writes; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + for (std::uint32_t sample = 0; sample < Samples; ++sample) { + if (selected(x, y, sample)) writes.push_back({x, y, sample}); + } + } + } + return writes; +} + +void CheckWritten(const std::vector>& before, const std::vector>& after, bool (*selected)(std::uint32_t x, std::uint32_t y, std::uint32_t sample), const std::string& what) { + std::uint32_t written = 0; + for (std::uint32_t index = 0; index < after.size(); ++index) { + const auto pixel = index / Samples; + const auto x = pixel % Side; + const auto y = pixel / Side; + const auto sample = index % Samples; + const bool chosen = selected(x, y, sample); + char where[96]; + std::snprintf(where, sizeof(where), "sample %u of pixel (%u, %u)", sample, x, y); + Require(after[index] == (chosen ? Written : before[index]), std::string(where) + (chosen ? " does not hold the stored color " : " changed although it was not stored to ") + what); + written += chosen ? 1u : 0u; + } + Require(written != 0 && written != after.size(), "the stores covered every sample or none " + what); +} + +void RequireStoreRefused(AgcDriver::VulkanDevice& device, const std::array& texture, const std::string& expected, const std::string& what) { + std::string refusal; + try { + StoreSamples(device, texture, {{0u, 0u, 0u}}); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find(expected) != std::string::npos, what + " was not refused: " + refusal); +} + +std::vector> RunLoad(AgcDriver::VulkanDevice& device, const std::array& texture, bool arrayed, std::uint32_t fragments) { + std::vector userData; + const auto program = CompileLoad(device, texture, userData, arrayed); + std::vector> loaded(Side * Side * fragments); + const auto codeAddress = reinterpret_cast((arrayed ? ArrayLoadCode : LoadCode).data()); + for (std::uint32_t first = 0; first < loaded.size(); first += Threads) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto index = first + tid; + const auto pixel = index / fragments; + const std::array words = arrayed + ? std::array{0u, 0u, pixel % Side, pixel / Side, 0u, index % fragments} + : std::array{0u, 0u, 0u, pixel % Side, pixel / Side, index % fragments}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } + device.Dispatch(program, 1, 1, 1, {}, codeAddress); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t component = 0; component < 4u; ++component) loaded[first + tid][component] = std::bit_cast(Buffer[tid * Words + LoadResult + component]); + } + } + return loaded; +} + +std::vector> LoadSamples(AgcDriver::VulkanDevice& device, const std::array& texture, std::uint32_t fragments = Samples) { + auto loaded = RunLoad(device, texture, false, fragments); + const auto loadedArray = RunLoad(device, texture, true, fragments); + Require(loadedArray == loaded, "Dim2DMsaaArray image_load disagreed with Dim2DMsaa image_load on the multisampled color target"); + return loaded; +} + +std::vector> LoadSamples(AgcDriver::VulkanDevice& device, const Block& block, std::uint32_t format = Format16161616Float, std::uint32_t tileMode = TileRenderTarget) { + return LoadSamples(device, TargetDescriptor(block, SampleShift, format, tileMode)); +} + +std::array CompressedDescriptor(const Block& block, std::uint32_t tileMode, std::uint64_t htile) { + auto texture = TargetDescriptor(block, SampleShift, Format16161616Float, tileMode); + const auto meta = htile >> 8u; + texture[6] = (1u << 20u) | (1u << 21u) | (static_cast(meta & 0xffu) << 24u); + texture[7] = static_cast(meta >> 8u); + return texture; +} + +double EdgeDistance(double x, double y, std::size_t from) { + const auto pixels = [](const std::array& vertex) { + return std::array{(static_cast(vertex[0]) + 1.0) * 0.5 * Side, Side - (static_cast(vertex[1]) + 1.0) * 0.5 * Side}; + }; + const auto a = pixels(Triangle[from]); + const auto b = pixels(Triangle[(from + 1u) % Triangle.size()]); + const auto c = pixels(Triangle[(from + 2u) % Triangle.size()]); + const auto length = std::hypot(b[0] - a[0], b[1] - a[1]); + const auto side = [&](double px, double py) { return ((b[0] - a[0]) * (py - a[1]) - (b[1] - a[1]) * (px - a[0])) / length; }; + return side(c[0], c[1]) < 0 ? -side(x, y) : side(x, y); +} + +void CheckSamples(const AgcDriver::Graphics::State& state, const std::vector>& loaded, const std::array& background) { + const auto locations = AgcDriver::Graphics::SampleLocations(state); + std::array pixelsByCoverage{}; + std::array coveredBySample{}; + std::uint32_t checked = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + std::uint32_t covered = 0; + for (std::uint32_t sample = 0; sample < Samples; ++sample) { + const auto& texel = loaded[(y * Side + x) * Samples + sample]; + const bool drawn = texel == Drawn; + const bool empty = texel == background; + char where[160]; + std::snprintf(where, sizeof(where), "sample %u of pixel (%u, %u) loaded (%g, %g, %g, %g)", sample, x, y, texel[0], texel[1], texel[2], texel[3]); + Require(drawn || empty, std::string(where) + ", neither the drawn color nor the cleared target"); + covered += drawn ? 1u : 0u; + coveredBySample[sample] += drawn ? 1u : 0u; + const auto px = x + static_cast(locations[sample].x); + const auto py = y + static_cast(locations[sample].y); + const auto distance = std::min({EdgeDistance(px, py, 0), EdgeDistance(px, py, 1), EdgeDistance(px, py, 2)}); + if (std::fabs(distance) < EdgeMargin) continue; + ++checked; + Require(drawn == (distance > 0), std::string(where) + (distance > 0 ? ", which the triangle covers" : ", which the triangle does not cover")); + } + ++pixelsByCoverage[covered]; + } + } + Require(checked >= Side * Side * Samples * 9u / 10u, "fewer than nine in ten samples are clear of the triangle's edges"); + Require(pixelsByCoverage[0] != 0 && pixelsByCoverage[Samples] != 0, "the triangle covers every pixel or none"); + Require(pixelsByCoverage[1] + pixelsByCoverage[2] + pixelsByCoverage[3] != 0, "no pixel is partly covered, so the samples of a pixel were not told apart"); + Require(std::adjacent_find(coveredBySample.begin(), coveredBySample.end(), std::not_equal_to<>()) != coveredBySample.end(), "every sample index has the same coverage, so the sample index was not told apart"); +} + +void CheckUniform(const std::vector>& loaded, const std::array& expected, const std::string& what) { + for (std::size_t index = 0; index < loaded.size(); ++index) { + const auto& texel = loaded[index]; + char where[160]; + std::snprintf(where, sizeof(where), "sample %zu of pixel (%zu, %zu) loaded (%g, %g, %g, %g)", index % Samples, index / Samples % Side, index / Samples / Side, texel[0], texel[1], texel[2], texel[3]); + Require(texel == expected, std::string(where) + " " + what); + } +} + +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)); +} + +void RequireRefused(AgcDriver::VulkanDevice& device, const std::array& texture, const std::string& expected, const std::string& what) { + std::string refusal; + try { + std::vector userData; + const auto program = CompileLoad(device, texture, userData); + Buffer.fill(0u); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(LoadCode.data())); + device.WaitIdle(); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find(expected) != std::string::npos, what + " was not refused: " + refusal); +} + +alignas(256) 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(); +} + +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; +} + +alignas(256) constexpr std::array NativeFmaskCode{0x34020084u, 0xe0381000u, 0x80000201u, 0xbf8c3f70u, 0xf0001f08u, 0x00020602u, 0xbf8c3f70u, 0xe0781000u, 0x80010601u, 0xbf810000u}; +alignas(256) std::array NativeFmaskInput{}; +alignas(256) std::array NativeFmaskOutput{}; + +std::array NativeFmaskDescriptor(const Block& fmask, const Block& cmask, std::uint32_t pipeXor) { + const auto address = fmask.Address() | pipeXor; + const auto metadata = cmask.Address(); + return {static_cast(address >> 8u), static_cast(address >> 40u) | (166u << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), 4u | (1u << 3u) | (4u << 6u) | (24u << 20u) | (9u << 28u), 0u, 0u, (1u << 19u) | (1u << 21u) | (static_cast(metadata >> 8u) << 24u), static_cast(metadata >> 16u)}; +} + +ShaderRecompiler::RecompileResult CompileNativeFmask(AgcDriver::VulkanDevice& device, const std::array& image, bool registration) { + std::vector userData(16u); + const auto buffer = [](const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return std::array{static_cast(address), static_cast(address >> 32u) & 0xffffu, bytes, 0x31016facu}; + }; + const auto input = buffer(NativeFmaskInput.data(), sizeof(NativeFmaskInput)); + const auto output = buffer(NativeFmaskOutput.data(), Threads * 16u); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4u); + std::copy(image.begin(), image.end(), userData.begin() + 8u); + const std::array memory{{{reinterpret_cast(NativeFmaskCode.data()), std::as_bytes(std::span(NativeFmaskCode))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + auto target = device.Target(); + target.registrationModules = registration; + ShaderRecompiler::RecompileRequest request{{ShaderStage::Compute, reinterpret_cast(NativeFmaskCode.data()), NativeFmaskCode, 0u, {}}, {32u, 0u, userData, compute, std::nullopt, std::nullopt, memory}, target, {0u, 0u, 0u, 128u}}; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void DispatchNativeFmask(AgcDriver::VulkanDevice& device, const std::array& image, bool registration) { + device.Dispatch(CompileNativeFmask(device, image, registration), 1u, 1u, 1u); + device.WaitIdle(); +} + +void NativeFmask(std::unique_ptr& device) { + using namespace AgcDriver::Graphics; + Block colorMemory(4u * BlockBytes, true); + Block dcc(BlockBytes, true); + Block cmask(BlockBytes, true); + Block fmask(BlockBytes, true); + Block unrelated(BlockBytes, true); + struct ReleaseDevice { + std::unique_ptr& value; + ~ReleaseDevice() { value.reset(); } + } releaseDevice{device}; + constexpr std::uint32_t sentinel = 0xdeadbeefu; + for (const auto pipeXor : {0u, 0xf00u}) { + std::memset(colorMemory.data, 0, colorMemory.bytes); + std::memset(dcc.data, 0, dcc.bytes); + std::memset(cmask.data, 0xcc, cmask.bytes); + std::memset(fmask.data, 0x11, fmask.bytes); + AgcDriver::Registers cx{{0x318, static_cast(colorMemory.Address() >> 8u)}, {0x31b, 0u}, {0x31c, Float16x4Info | 0x10006000u}, {0x31d, (3u << 12u) | (3u << 15u)}, {0x31e, 0u}, {0x31f, static_cast(cmask.Address() >> 8u)}, {0x321, static_cast((fmask.Address() | pipeXor) >> 8u)}, {0x325, static_cast(dcc.Address() >> 8u)}, {0x390, static_cast(colorMemory.Address() >> 40u)}, {0x398, static_cast(cmask.Address() >> 40u)}, {0x3a0, static_cast(fmask.Address() >> 40u)}, {0x3a8, static_cast(dcc.Address() >> 40u)}, {0x3b0, ((Side - 1u) << 14u) | (Side - 1u)}, {0x3b8, TiledAttrib3}}; + const auto color = DecodeColorBuffer(cx, 0u); + Require(color.samples == 8u && color.cmaskAddress == cmask.Address(), "native FMASK fixture lost its decoded target association"); + std::array, 8> expected{}; + for (std::uint32_t sample = 0u; sample < 8u; ++sample) { + expected[sample] = {static_cast(sample + 1u) / 8.0f, 0.25f, 0.5f, 1.0f}; + auto pixel = PixelCode; + pixel[1] = std::bit_cast(expected[sample][0]); + pixel[3] = std::bit_cast(expected[sample][1]); + pixel[5] = std::bit_cast(expected[sample][2]); + pixel[7] = std::bit_cast(expected[sample][3]); + auto state = TargetState(color); + state.quadSampleMask.fill(static_cast(1u << sample)); + Draw(*device, state, 0u, 2u, pixel); + } + const auto native = LoadSamples(*device, TargetDescriptor(colorMemory, 3u), 8u); + std::vector covered; + for (std::uint32_t pixel = 0; pixel < Side * Side; ++pixel) { + bool all = true; + for (std::size_t sample = 0; sample < 8u; ++sample) all = all && native[pixel * 8u + sample] == expected[sample]; + if (all) covered.push_back(pixel); + } + Require(!covered.empty(), "native FMASK fixture did not render eight distinct sample colors"); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const auto pixel = covered[lane % covered.size()]; + NativeFmaskInput[lane * 4u] = pixel % Side; + NativeFmaskInput[lane * 4u + 1u] = pixel / Side; + NativeFmaskInput[lane * 4u + 2u] = 0u; + NativeFmaskInput[lane * 4u + 3u] = 0u; + } + const auto image = NativeFmaskDescriptor(fmask, cmask, pipeXor); + const auto check = [&] { + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const std::array mapping{0x76543210u, 1u, 0x76543210u, 0u}; + for (std::size_t component = 0; component < 4u; ++component) Require(NativeFmaskOutput[lane * 4u + component] == mapping[component], "native FMASK load did not preserve its expanded sample indices and selectors"); + for (std::uint32_t sample = 0; sample < 8u; ++sample) { + const auto fragment = NativeFmaskOutput[lane * 4u] >> (4u * sample) & 15u; + const auto pixel = NativeFmaskInput[lane * 4u + 1u] * Side + NativeFmaskInput[lane * 4u]; + Require(native[pixel * 8u + fragment] == expected[sample], "native FMASK remapped a sampled color to another fragment"); + } + } + Require(std::all_of(NativeFmaskOutput.begin() + Threads * 4u, NativeFmaskOutput.end(), [](auto word) { return word == sentinel; }), "native FMASK wrote beyond its output"); + }; + for (const auto registration : {false, true}) { + for (std::uint32_t repeat = 0; repeat < 2u; ++repeat) { + NativeFmaskOutput.fill(sentinel); + DispatchNativeFmask(*device, image, registration); + check(); + } + const auto program = CompileNativeFmask(*device, image, registration); + const auto codeAddress = reinterpret_cast(NativeFmaskCode.data()); + std::shared_ptr recipe; + NativeFmaskOutput.fill(sentinel); + device->Dispatch(program, 1u, 1u, 1u, {}, codeAddress, nullptr, &recipe); + device->WaitIdle(); + check(); + Require(recipe == nullptr, "native FMASK emitted a reusable recipe without an ownership proof"); + const auto prepared = device->PrepareDispatch(program, {}, Threads, codeAddress); + Require(prepared != nullptr, "native FMASK did not prepare a deferred dispatch"); + NativeFmaskOutput.fill(sentinel); + device->Dispatch(program, 1u, 1u, 1u, {}, codeAddress, prepared); + device->WaitIdle(); + check(); + } + Require(LoadSamples(*device, TargetDescriptor(colorMemory, 3u), 8u) == native, "FMASK sampling changed the native samples"); + Require(std::all_of(fmask.data, fmask.data + fmask.bytes, [](auto byte) { return byte == 0x11u; }) && std::all_of(cmask.data, cmask.data + cmask.bytes, [](auto byte) { return byte == 0xccu; }), "native FMASK sampling published or changed guest metadata"); + const auto refused = [&](const auto& descriptor, std::string_view reason) { + NativeFmaskOutput.fill(sentinel); + std::string error; + try { DispatchNativeFmask(*device, descriptor, false); } catch (const std::runtime_error& e) { error = e.what(); } + Require(error.find(reason) != std::string::npos, "native FMASK missing refusal: " + error); + Require(std::all_of(NativeFmaskOutput.begin(), NativeFmaskOutput.end(), [sentinel](auto word) { return word == sentinel; }), "rejected native FMASK wrote output"); + }; + refused(NativeFmaskDescriptor(unrelated, cmask, pipeXor), "descriptor does not match"); + refused(NativeFmaskDescriptor(fmask, unrelated, pipeXor), "descriptor does not match"); + refused(NativeFmaskDescriptor(fmask, cmask, pipeXor ^ 0x100u), "descriptor does not match"); + auto resized = image; + resized[2] += 1u; + refused(resized, "extent or subresource does not match"); + const auto deferredProgram = CompileNativeFmask(*device, image, false); + const auto codeAddress = reinterpret_cast(NativeFmaskCode.data()); + const auto preparedBeforeStore = device->PrepareDispatch(deferredProgram, {}, Threads, codeAddress); + Require(preparedBeforeStore != nullptr, "native FMASK did not prepare its metadata invalidation probe"); + std::array same{}; + same.fill(std::byte{0x11}); + AgcDriver::GuestMemory::Write(fmask.Address(), same); + refused(image, "writes to a resident native FMASK"); + NativeFmaskOutput.fill(sentinel); + std::string deferredError; + try { + device->Dispatch(deferredProgram, 1u, 1u, 1u, {}, codeAddress, preparedBeforeStore); + device->WaitIdle(); + } catch (const std::runtime_error& error) { + deferredError = error.what(); + } + Require(deferredError.find("writes to a resident native FMASK") != std::string::npos, "deferred native FMASK missed its metadata store: " + deferredError); + Require(std::all_of(NativeFmaskOutput.begin(), NativeFmaskOutput.end(), [](auto word) { return word == sentinel; }), "rejected deferred native FMASK wrote output"); + device->SubmitRecorded(true); + device->WaitIdle(); + ClearMultisampleTargets(device->Device()); + ClearCachedTextures(device->Device()); + cx[0x31c] &= ~0x2000u; + cx[0x31f] = 0u; + cx[0x398] = 0u; + const auto withoutCmask = DecodeColorBuffer(cx, 0u); + Require(withoutCmask.cmaskAddress == 0u, "the no-CMASK control retained metadata"); + Draw(*device, TargetState(withoutCmask)); + const auto uncompressed = LoadSamples(*device, TargetDescriptor(colorMemory, 3u), 8u); + Require(std::find(uncompressed.begin(), uncompressed.end(), Drawn) != uncompressed.end(), "tracking native FMASK broke ordinary DCC rendering without CMASK"); + device->SubmitRecorded(true); + device->WaitIdle(); + ClearMultisampleTargets(device->Device()); + ClearCachedTextures(device->Device()); + Draw(*device, TargetState(color)); + auto recreated = color; + recreated.pipeBankXor ^= 0x4000u; + recreated.fmaskAddress = 0u; + recreated.fmaskPipeBankXor = 0u; + Draw(*device, TargetState(recreated)); + std::array formerFmask{}; + formerFmask.fill(std::byte{0x22}); + AgcDriver::GuestMemory::Write(fmask.Address(), formerFmask); + Draw(*device, TargetState(recreated)); + auto recreatedDescriptor = TargetDescriptor(colorMemory, 3u); + recreatedDescriptor[0] |= recreated.pipeBankXor >> 8u; + const auto recreatedSamples = LoadSamples(*device, recreatedDescriptor, 8u); + Require(std::find(recreatedSamples.begin(), recreatedSamples.end(), Drawn) != recreatedSamples.end(), "former FMASK metadata affected the recreated native target"); + device->SubmitRecorded(true); + device->WaitIdle(); + ClearMultisampleTargets(device->Device()); + ClearCachedTextures(device->Device()); + std::printf("native FMASK: XOR=0x%x, eight distinct samples, both registration modes, cached reuse, association/extent/write refusals and unchanged guest metadata passed\n", pipeXor); + } +} + +} + +int main(int argc, char** argv) { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (argc == 2 && std::string_view(argv[1]) == "--native-fmask") { + { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (!context.storageImageMultisample || (context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_8_BIT) == 0) { + std::puts("native FMASK skipped, the device does not render or store 8-sample color"); + return VulkanTestSkipped; + } + } + NativeFmask(device); + return 0; + } + 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); + Require(colorBytes != 0 && colorBytes <= block.bytes, "the 4x target's color memory does not fit its block"); + AgcDriver::Graphics::NoteColorMetadataFill(block.Address(), static_cast(colorBytes), FillOnes); + CheckUniform(LoadSamples(*device, block), Ones, "after a fill of the target's color memory with half-float ones"); + RequireRefused(*device, TargetDescriptor(block, 3u), "texture of 8 samples", "loading a 4x target as an 8x texture"); + Require(colorBytes <= other.bytes, "the 4x target's color memory does not fit one block"); + CheckUniform(LoadSamples(*device, other), {}, "from zeroed color memory no draw rendered to"); + Block stored(BlockBytes); + StoreWords(stored, colorBytes, FillOnes); + CheckUniform(LoadSamples(*device, stored), Ones, "from color memory of half-float ones no draw rendered to"); + const auto storedState = TargetState(DecodeTarget(stored)); + Draw(*device, storedState); + CheckSamples(storedState, LoadSamples(*device, stored), Ones); + Block mixed(BlockBytes); + StoreWords(mixed, colorBytes, FillOnes); + 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"); + for (const auto mode : {TileZ64KBX, TileD64KBX}) { + Block swizzled(BlockBytes); + StoreWords(swizzled, colorBytes, FillOnes); + CheckUniform(LoadSamples(*device, swizzled, Format16161616Float, mode), Ones, "from the uniform memory of a " + std::to_string(mode) + " swizzle surface no draw rendered to"); + } + const auto htileBytes = AgcDriver::Graphics::HtileMetadataBytes({Side, Side}); + Require(htileBytes <= BlockBytes, "the HTILE of the 4x texture does not fit one block"); + Block cleared(2u * BlockBytes); + StoreWords(cleared, colorBytes, FillOnes); + const auto clearedHtile = cleared.Address() + BlockBytes; + CheckUniform(LoadSamples(*device, CompressedDescriptor(cleared, TileZ64KBX, clearedHtile)), {}, "from a Z swizzle surface whose HTILE is all cleared to 0.0 over memory of other values"); + AgcDriver::Graphics::NoteColorMetadataFill(clearedHtile, sizeof(std::uint32_t), 0u); + RequireRefused(*device, CompressedDescriptor(cleared, TileZ64KBX, clearedHtile), "over the HTILE", "loading a Z swizzle texture after a fill over part of its HTILE"); + AgcDriver::Graphics::NoteColorMetadataFill(clearedHtile, static_cast(htileBytes), 0u); + CheckUniform(LoadSamples(*device, CompressedDescriptor(cleared, TileZ64KBX, clearedHtile)), {}, "from a Z swizzle surface after a fill of 0 over its whole HTILE"); + Block expanded(2u * BlockBytes); + StoreWords(expanded, colorBytes, FillOnes); + std::fill_n(reinterpret_cast(expanded.data + BlockBytes), htileBytes / sizeof(std::uint32_t), 0xffffffffu); + CheckUniform(LoadSamples(*device, CompressedDescriptor(expanded, TileZ64KBX, expanded.Address() + BlockBytes)), Ones, "from the uniform memory of a Z swizzle surface whose HTILE is all expanded"); + Block partial(2u * BlockBytes); + StoreWords(partial, colorBytes, FillOnes); + const std::uint32_t expandedTile = 0xfu; + std::memcpy(partial.data + BlockBytes + htileBytes - sizeof(expandedTile), &expandedTile, sizeof(expandedTile)); + RequireRefused(*device, CompressedDescriptor(partial, TileZ64KBX, partial.Address() + BlockBytes), "neither all cleared to 0.0 nor all expanded", "loading a Z swizzle texture whose HTILE mixes cleared and expanded tiles"); + Block display(2u * BlockBytes); + StoreWords(display, colorBytes, FillOnes); + RequireRefused(*device, CompressedDescriptor(display, TileD64KBX, display.Address() + BlockBytes), "compression metadata", "loading a D swizzle multisampled texture no draw rendered to with compression metadata"); + Block standard(BlockBytes); + StoreWords(standard, colorBytes, FillOnes); + RequireRefused(*device, TargetDescriptor(standard, SampleShift, Format16161616Float, TileS64KBX), "has no modeled multisampled color footprint", "loading an S swizzle multisampled texture no draw rendered to"); + 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'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleOrder.cpp new file mode 100644 index 0000000000..9e534056ab --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleOrder.cpp @@ -0,0 +1,1528 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include "SampleOrderTestFillDepth_spv.h" +#include "SampleOrderTestFillStencil_spv.h" +#include "SampleOrderTestFillVertex_spv.h" +#include "SampleOrderTestR16_spv.h" +#include "SampleOrderTestR32_spv.h" +#include "SampleOrderTestR8_spv.h" +#include "SampleOrderTestReadColor_spv.h" +#include "SampleOrderTestReadDepth_spv.h" +#include "SampleOrderTestReadStencil_spv.h" +#include "SampleOrderTestRG32_spv.h" +#include "SampleOrderTestRGBA32_spv.h" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Check; +using AgcDriver::Graphics::Context; +using AgcDriver::Graphics::KeptSampleOrder; +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::SampleOrder; +using AgcDriver::Graphics::SampleOrderMethod; +using AgcDriver::Graphics::SampleOrderPass; +using AgcDriver::Graphics::SampleOrderTarget; +using ShaderRecompiler::PixelInput; +using ShaderRecompiler::PixelInputBit; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t WaveSize = 64; +constexpr VkImageUsageFlags DepthUsage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; +constexpr VkImageUsageFlags ColorUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT; + +SampleOrder OrderFor(std::uint32_t samples) { + if (samples == 2u) return {KeptSampleOrder, KeptSampleOrder, 0x76543201u, 0x76543201u}; + if (samples == 4u) return {KeptSampleOrder, 0x76542301u, 0x76540132u, 0x76541023u}; + return {0x01234567u, KeptSampleOrder, 0x10325476u, 0x45670123u}; +} + +std::uint32_t Pick(std::uint32_t word, std::uint32_t index) { + return (word >> (index * 4u)) & 7u; +} + +std::uint32_t Quad(std::uint32_t x, std::uint32_t y) { + return (x & 1u) | ((y & 1u) << 1u); +} + +std::string At(std::uint32_t x, std::uint32_t y, std::uint32_t sample) { + return "sample " + std::to_string(sample) + " of pixel (" + std::to_string(x) + ", " + std::to_string(y) + ")"; +} + +void FullBarrier(const Context& context, VkCommandBuffer commands) { + AgcDriver::Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); +} + +void HostBarrier(const Context& context, VkCommandBuffer commands) { + AgcDriver::Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); +} + +class Image { +public: + Image(const Context& context, VkFormat format, std::uint32_t samples, VkExtent2D extent, VkImageUsageFlags usage, VkImageCreateFlags flags) : context(context) { + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.flags = flags; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {extent.width, extent.height, 1u}; + info.mipLevels = 1u; + info.arrayLayers = 1u; + info.samples = static_cast(samples); + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = usage; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage sample order test"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory sample order test"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0u), "vkBindImageMemory sample order test"); + } catch (...) { + release(); + throw; + } + } + ~Image() { release(); } + Image(const Image&) = delete; + Image& operator=(const Image&) = delete; + VkImage Handle() const { return image; } + VkImageView View(VkFormat format, VkImageAspectFlags aspect, VkImageUsageFlags usage) { + VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + viewUsage.usage = usage; + VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + info.pNext = &viewUsage; + info.image = image; + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.format = format; + info.subresourceRange = {aspect, 0u, 1u, 0u, 1u}; + VkImageView view = VK_NULL_HANDLE; + Check(context.Function("vkCreateImageView")(context.device, &info, nullptr, &view), "vkCreateImageView sample order test"); + views.push_back(view); + return view; + } + void ToGeneral(VkCommandBuffer commands, VkImageAspectFlags aspect) const { + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange = {aspect, 0u, 1u, 0u, 1u}; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0u, 0u, nullptr, 0u, nullptr, 1u, &barrier); + } + +private: + void release() noexcept { + for (const auto view : views) context.Function("vkDestroyImageView")(context.device, view, nullptr); + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + } + Context context; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + std::vector views; +}; + +struct Binding { + std::uint32_t binding; + VkDescriptorType type; +}; + +class Program { +public: + Program(const Context& context, std::span compute, std::span bindings, std::uint32_t pushBytes, VkShaderStageFlags stages = VK_SHADER_STAGE_COMPUTE_BIT) : context(context), stages(stages), pushBytes(pushBytes) { + try { + std::vector layoutBindings; + std::vector sizes; + for (const auto& binding : bindings) { + layoutBindings.push_back({binding.binding, binding.type, 1u, stages, nullptr}); + sizes.push_back({binding.type, 1u}); + types.push_back(binding); + } + VkDescriptorSetLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + layoutInfo.bindingCount = static_cast(layoutBindings.size()); + layoutInfo.pBindings = layoutBindings.data(); + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &layoutInfo, nullptr, &setLayout), "sample order test descriptor layout"); + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = 1u; + poolInfo.poolSizeCount = static_cast(sizes.size()); + poolInfo.pPoolSizes = sizes.data(); + Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &pool), "sample order test descriptor pool"); + const VkDescriptorSetAllocateInfo allocate{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, nullptr, pool, 1u, &setLayout}; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocate, &set), "sample order test descriptor set"); + const VkPushConstantRange range{stages, 0u, pushBytes}; + VkPipelineLayoutCreateInfo layoutCreate{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutCreate.setLayoutCount = 1u; + layoutCreate.pSetLayouts = &setLayout; + layoutCreate.pushConstantRangeCount = 1u; + layoutCreate.pPushConstantRanges = ⦥ + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutCreate, nullptr, &layout), "sample order test pipeline layout"); + if (compute.empty()) return; + const auto shader = Module(compute); + VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0u, VK_SHADER_STAGE_COMPUTE_BIT, shader, "main", nullptr}; + info.layout = layout; + const auto created = context.Function("vkCreateComputePipelines")(context.device, VK_NULL_HANDLE, 1u, &info, nullptr, &pipeline); + context.Function("vkDestroyShaderModule")(context.device, shader, nullptr); + Check(created, "sample order test compute pipeline"); + } catch (...) { + release(); + throw; + } + } + ~Program() { release(); } + Program(const Program&) = delete; + Program& operator=(const Program&) = delete; + + VkShaderModule Module(std::span code) const { + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = code.size_bytes(); + info.pCode = code.data(); + VkShaderModule module = VK_NULL_HANDLE; + Check(context.Function("vkCreateShaderModule")(context.device, &info, nullptr, &module), "sample order test shader module"); + return module; + } + void Image(std::uint32_t binding, VkImageView view) const { + const VkDescriptorImageInfo image{VK_NULL_HANDLE, view, VK_IMAGE_LAYOUT_GENERAL}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = set; + write.dstBinding = binding; + write.descriptorCount = 1u; + write.descriptorType = typeOf(binding); + write.pImageInfo = ℑ + context.Function("vkUpdateDescriptorSets")(context.device, 1u, &write, 0u, nullptr); + } + void Storage(std::uint32_t binding, VkBuffer buffer) const { + const VkDescriptorBufferInfo info{buffer, 0u, VK_WHOLE_SIZE}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = set; + write.dstBinding = binding; + write.descriptorCount = 1u; + write.descriptorType = typeOf(binding); + write.pBufferInfo = &info; + context.Function("vkUpdateDescriptorSets")(context.device, 1u, &write, 0u, nullptr); + } + void Bind(VkCommandBuffer commands, VkPipelineBindPoint point, const void* push) const { + context.Function("vkCmdBindDescriptorSets")(commands, point, layout, 0u, 1u, &set, 0u, nullptr); + context.Function("vkCmdPushConstants")(commands, layout, stages, 0u, pushBytes, push); + } + void Dispatch(VkCommandBuffer commands, const void* push, VkExtent2D extent) const { + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); + Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, push); + context.Function("vkCmdDispatch")(commands, (extent.width + 7u) / 8u, (extent.height + 7u) / 8u, 1u); + } + VkPipelineLayout Layout() const { return layout; } + +private: + VkDescriptorType typeOf(std::uint32_t binding) const { + for (const auto& known : types) { + if (known.binding == binding) return known.type; + } + throw std::runtime_error("the sample order test program has no binding " + std::to_string(binding)); + } + void release() noexcept { + if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + if (layout) context.Function("vkDestroyPipelineLayout")(context.device, layout, nullptr); + if (pool) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); + if (setLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, setLayout, nullptr); + } + Context context; + VkShaderStageFlags stages; + std::uint32_t pushBytes; + std::vector types; + VkDescriptorSetLayout setLayout = VK_NULL_HANDLE; + VkDescriptorPool pool = VK_NULL_HANDLE; + VkDescriptorSet set = VK_NULL_HANDLE; + VkPipelineLayout layout = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; +}; + +class DepthFill { +public: + DepthFill(const Context& context, VkFormat format, std::uint32_t samples, VkExtent2D extent, VkImageView attachment, bool stencil, VkBuffer values) : context(context), extent(extent), samples(samples) { + const std::array bindings{{{0u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER}}}; + program = std::make_unique(context, std::span{}, bindings, 8u, VK_SHADER_STAGE_FRAGMENT_BIT); + program->Storage(0u, values); + try { + const bool hasStencil = AgcDriver::Graphics::HasStencil(format); + VkAttachmentDescription description{}; + description.format = format; + description.samples = static_cast(samples); + description.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + description.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + description.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + description.stencilStoreOp = hasStencil ? VK_ATTACHMENT_STORE_OP_STORE : VK_ATTACHMENT_STORE_OP_DONT_CARE; + description.initialLayout = VK_IMAGE_LAYOUT_GENERAL; + description.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + const VkAttachmentReference reference{0u, VK_IMAGE_LAYOUT_GENERAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.pDepthStencilAttachment = &reference; + VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + passInfo.attachmentCount = 1u; + passInfo.pAttachments = &description; + passInfo.subpassCount = 1u; + passInfo.pSubpasses = &subpass; + Check(context.Function("vkCreateRenderPass")(context.device, &passInfo, nullptr, &pass), "sample order test fill pass"); + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = pass; + framebufferInfo.attachmentCount = 1u; + framebufferInfo.pAttachments = &attachment; + framebufferInfo.width = extent.width; + framebufferInfo.height = extent.height; + framebufferInfo.layers = 1u; + Check(context.Function("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "sample order test fill framebuffer"); + const auto vertex = program->Module(SAMPLE_ORDER_TEST_FILLVERTEX_SPV); + const auto fragment = stencil ? program->Module(SAMPLE_ORDER_TEST_FILLSTENCIL_SPV) : program->Module(SAMPLE_ORDER_TEST_FILLDEPTH_SPV); + const std::array stages{{ + {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0u, VK_SHADER_STAGE_VERTEX_BIT, vertex, "main", nullptr}, + {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0u, VK_SHADER_STAGE_FRAGMENT_BIT, fragment, "main", nullptr}, + }}; + VkPipelineVertexInputStateCreateInfo input{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + const VkViewport viewport{0.0f, 0.0f, static_cast(extent.width), static_cast(extent.height), 0.0f, 1.0f}; + const VkRect2D scissor{{0, 0}, extent}; + VkPipelineViewportStateCreateInfo viewports{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewports.viewportCount = 1u; + viewports.pViewports = &viewport; + viewports.scissorCount = 1u; + viewports.pScissors = &scissor; + VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + raster.polygonMode = VK_POLYGON_MODE_FILL; + raster.cullMode = VK_CULL_MODE_NONE; + raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + raster.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = static_cast(samples); + multisample.sampleShadingEnable = VK_TRUE; + multisample.minSampleShading = 1.0f; + VkStencilOpState replace{}; + replace.failOp = VK_STENCIL_OP_REPLACE; + replace.passOp = VK_STENCIL_OP_REPLACE; + replace.depthFailOp = VK_STENCIL_OP_REPLACE; + replace.compareOp = VK_COMPARE_OP_ALWAYS; + replace.compareMask = 0xffu; + replace.writeMask = 0xffu; + VkPipelineDepthStencilStateCreateInfo depth{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; + depth.depthTestEnable = VK_TRUE; + depth.depthWriteEnable = VK_TRUE; + depth.depthCompareOp = VK_COMPARE_OP_ALWAYS; + depth.stencilTestEnable = stencil ? VK_TRUE : VK_FALSE; + depth.front = replace; + depth.back = replace; + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + info.stageCount = static_cast(stages.size()); + info.pStages = stages.data(); + info.pVertexInputState = &input; + info.pInputAssemblyState = &assembly; + info.pViewportState = &viewports; + info.pRasterizationState = &raster; + info.pMultisampleState = &multisample; + info.pDepthStencilState = &depth; + info.pColorBlendState = &blend; + info.layout = program->Layout(); + info.renderPass = pass; + const auto created = context.Function("vkCreateGraphicsPipelines")(context.device, VK_NULL_HANDLE, 1u, &info, nullptr, &pipeline); + context.Function("vkDestroyShaderModule")(context.device, fragment, nullptr); + context.Function("vkDestroyShaderModule")(context.device, vertex, nullptr); + Check(created, "sample order test fill pipeline"); + } catch (...) { + release(); + throw; + } + } + ~DepthFill() { release(); } + DepthFill(const DepthFill&) = delete; + DepthFill& operator=(const DepthFill&) = delete; + void Run(VkCommandBuffer commands) const { + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = pass; + begin.framebuffer = framebuffer; + begin.renderArea = {{0, 0}, extent}; + context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + const std::array push{extent.width, samples}; + program->Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, push.data()); + context.Function("vkCmdDraw")(commands, 3u, 1u, 0u, 0u); + context.Function("vkCmdEndRenderPass")(commands); + } + +private: + void release() noexcept { + if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + if (framebuffer) context.Function("vkDestroyFramebuffer")(context.device, framebuffer, nullptr); + if (pass) context.Function("vkDestroyRenderPass")(context.device, pass, nullptr); + } + Context context; + VkExtent2D extent; + std::uint32_t samples; + std::unique_ptr program; + VkRenderPass pass = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; +}; + +using Samples = std::vector>; + +template +void Submit(const Context& context, Record record) { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::CommandBatch batch(context); + record(batch.Handle()); + batch.SubmitAndWait(); +} + +Samples ReadValues(AgcDriver::Graphics::Buffer& buffer, std::size_t count) { + buffer.Invalidate(); + Samples values(count); + std::memcpy(values.data(), buffer.Bytes().data(), count * sizeof(values[0])); + return values; +} + +void CheckOrdered(const Samples& before, const Samples& after, VkExtent2D extent, VkExtent2D region, std::uint32_t samples, const SampleOrder& order, bool toHost, const std::string& what) { + std::uint32_t moved = 0; + std::string first; + std::array wrong{}; + for (std::uint32_t y = 0; y < extent.height; ++y) { + for (std::uint32_t x = 0; x < extent.width; ++x) { + const bool inside = x < region.width && y < region.height; + const auto word = order[Quad(x, y)]; + const auto map = toHost ? word : AgcDriver::Graphics::InvertSampleOrder(word); + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto base = (y * extent.width + x) * samples; + const auto source = inside ? Pick(map, sample) : sample; + if (after[base + sample] != before[base + source]) { + if (first.empty()) { + char held[96]; + std::snprintf(held, sizeof(held), " holds 0x%08x 0x%08x, expected 0x%08x 0x%08x (sample %u)", after[base + sample][0], after[base + sample][1], before[base + source][0], before[base + source][1], source); + first = At(x, y, sample) + held; + } + ++wrong[Quad(x, y)]; + } + if (source != sample) ++moved; + } + } + } + if (!first.empty()) { + std::string classes; + for (std::uint32_t quad = 0; quad < wrong.size(); ++quad) { + const bool kept = order[quad] == 0u || order[quad] == KeptSampleOrder; + classes += (quad == 0u ? "" : ", ") + std::to_string(wrong[quad]) + " in pixels with x " + ((quad & 1u) != 0u ? "odd" : "even") + " and y " + ((quad & 2u) != 0u ? "odd" : "even") + (kept ? " (kept)" : " (reordered)"); + } + throw std::runtime_error(what + ": " + first + "; wrong samples: " + classes); + } + Require(moved != 0, what + ": the order moved no sample"); +} + +const std::array, 5> ColorFormats{{ + {VK_FORMAT_R8_UNORM, 1u}, {VK_FORMAT_R16_SFLOAT, 2u}, {VK_FORMAT_R8G8B8A8_SRGB, 4u}, {VK_FORMAT_R16G16B16A16_SFLOAT, 8u}, {VK_FORMAT_R32G32B32A32_SFLOAT, 16u} +}}; + +std::span TexelCode(VkFormat alias) { + switch (alias) { + case VK_FORMAT_R8_UINT: return SAMPLE_ORDER_TEST_R8_SPV; + case VK_FORMAT_R16_UINT: return SAMPLE_ORDER_TEST_R16_SPV; + case VK_FORMAT_R32_UINT: return SAMPLE_ORDER_TEST_R32_SPV; + case VK_FORMAT_R32G32_UINT: return SAMPLE_ORDER_TEST_RG32_SPV; + case VK_FORMAT_R32G32B32A32_UINT: return SAMPLE_ORDER_TEST_RGBA32_SPV; + default: throw std::runtime_error("the sample order test has no texel program for this alias"); + } +} + +std::uint32_t AliasMask(std::uint32_t elementBytes, std::uint32_t component) { + if (elementBytes == 1u) return component == 0u ? 0xffu : 0u; + if (elementBytes == 2u) return component == 0u ? 0xffffu : 0u; + if (elementBytes == 4u) return component == 0u ? ~0u : 0u; + if (elementBytes == 8u) return component < 2u ? ~0u : 0u; + return ~0u; +} + +Samples Masked(Samples values, std::uint32_t elementBytes) { + for (auto& value : values) { + for (std::uint32_t component = 0; component < 4u; ++component) value[component] &= AliasMask(elementBytes, component); + } + return values; +} + +constexpr VkExtent2D SwapExtent{20u, 12u}; +constexpr VkExtent2D SwapRegion{16u, 10u}; + +bool ColorSwaps(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples) { + const auto alias = AgcDriver::Graphics::SampleOrderStorageFormat(context, format, elementBytes, samples); + const auto name = "the " + std::to_string(samples) + "x color format " + std::to_string(format); + if (alias == VK_FORMAT_UNDEFINED) { + std::printf("%s has no %u-sample storage alias here, not swapped\n", name.c_str(), samples); + return false; + } + const auto order = OrderFor(samples); + Image image(context, format, samples, SwapExtent, ColorUsage, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT); + const auto view = image.View(alias, VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_USAGE_STORAGE_BIT); + const auto count = static_cast(SwapExtent.width) * SwapExtent.height * samples; + AgcDriver::Graphics::Buffer values(context, count * 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Samples before(count); + for (std::size_t index = 0; index < count; ++index) { + for (std::uint32_t component = 0; component < 4u; ++component) before[index][component] = static_cast(index * 0x9e3779b1u + component * 0x85ebca6bu + 0x1000u * (index % samples)) & AliasMask(elementBytes, component); + before[index][0] = (before[index][0] & ~7u & AliasMask(elementBytes, 0u)) | static_cast(index % samples); + } + std::memcpy(values.Bytes().data(), before.data(), count * sizeof(before[0])); + const std::array bindings{{{0u, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}, {1u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER}}}; + Program texels(context, TexelCode(alias), bindings, 16u); + texels.Image(0u, view); + texels.Storage(1u, values.Handle()); + const std::array store{SwapExtent.width, SwapExtent.height, samples, 1u}; + const std::array load{SwapExtent.width, SwapExtent.height, samples, 0u}; + const SampleOrderTarget target{image.Handle(), view, alias, SwapExtent, samples, SampleOrderMethod::Storage, nullptr}; + const SampleOrderPass pass{order, SwapRegion, {}, false}; + std::vector> kept; + Submit(context, [&](VkCommandBuffer commands) { + image.ToGeneral(commands, VK_IMAGE_ASPECT_COLOR_BIT); + texels.Dispatch(commands, store.data(), SwapExtent); + FullBarrier(context, commands); + texels.Dispatch(commands, load.data(), SwapExtent); + HostBarrier(context, commands); + }); + Require(Masked(ReadValues(values, count), elementBytes) == before, name + ": the stored samples did not read back"); + Submit(context, [&](VkCommandBuffer commands) { + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(&target, 1u), true, kept); + FullBarrier(context, commands); + texels.Dispatch(commands, load.data(), SwapExtent); + HostBarrier(context, commands); + }); + CheckOrdered(before, Masked(ReadValues(values, count), elementBytes), SwapExtent, SwapRegion, samples, order, true, name + " put in the host's order"); + Submit(context, [&](VkCommandBuffer commands) { + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(&target, 1u), false, kept); + FullBarrier(context, commands); + texels.Dispatch(commands, load.data(), SwapExtent); + HostBarrier(context, commands); + }); + Require(Masked(ReadValues(values, count), elementBytes) == before, name + ": putting the samples back in the guest's order did not restore every sample bit for bit"); + std::printf("%s round-trips through its %u-byte storage alias\n", name.c_str(), elementBytes); + return true; +} + +bool DepthSampleable(const Context& context, VkFormat format, std::uint32_t samples) { + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + constexpr VkFormatFeatureFlags needed = VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; + if ((properties.optimalTilingFeatures & needed) != needed) return false; + VkImageFormatProperties supported{}; + return context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, DepthUsage, 0u, &supported) == VK_SUCCESS && (supported.sampleCounts & samples) != 0u; +} + +std::string MethodName(SampleOrderMethod method) { + switch (method) { + case SampleOrderMethod::Depth: return "a depth pass"; + case SampleOrderMethod::DepthStencilExport: return "a depth pass exporting stencil"; + case SampleOrderMethod::DepthStencilBits: return "a depth pass and eight stencil bit passes"; + default: return "storage"; + } +} + +bool DepthSwaps(const Context& context, VkFormat format, std::uint32_t samples, SampleOrderMethod method) { + const bool stencil = method != SampleOrderMethod::Depth; + const auto name = "the " + std::to_string(samples) + "x depth format " + std::to_string(format) + " through " + MethodName(method); + if (!DepthSampleable(context, format, samples)) { + std::printf("%s: the format cannot be sampled at this sample count here, not swapped\n", name.c_str()); + return false; + } + if (stencil && !context.shaderStencilExport) { + std::printf("%s: the device has no stencil export to fill the test's stencil, not swapped\n", name.c_str()); + return false; + } + const auto order = OrderFor(samples); + const bool hasStencil = AgcDriver::Graphics::HasStencil(format); + const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (hasStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); + Image image(context, format, samples, SwapExtent, DepthUsage, 0u); + const auto attachment = image.View(format, aspects, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + const auto depthView = image.View(format, VK_IMAGE_ASPECT_DEPTH_BIT, VK_IMAGE_USAGE_SAMPLED_BIT); + const auto stencilView = stencil ? image.View(format, VK_IMAGE_ASPECT_STENCIL_BIT, VK_IMAGE_USAGE_SAMPLED_BIT) : VK_NULL_HANDLE; + const auto count = static_cast(SwapExtent.width) * SwapExtent.height * samples; + AgcDriver::Graphics::Buffer fill(context, count * 8u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + AgcDriver::Graphics::Buffer values(context, count * 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::vector> filled(count); + const bool unorm16 = format == VK_FORMAT_D16_UNORM || format == VK_FORMAT_D16_UNORM_S8_UINT; + for (std::size_t index = 0; index < count; ++index) { + const auto key = static_cast(index + 1u); + filled[index] = {std::bit_cast(unorm16 ? static_cast(key) / 65535.0f : static_cast(key) / 4096.0f), (key * 37u) & 0xffu}; + } + std::memcpy(fill.Bytes().data(), filled.data(), count * sizeof(filled[0])); + DepthFill raster(context, format, samples, SwapExtent, attachment, stencil, fill.Handle()); + const std::array bindings{{{0u, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, {1u, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, {2u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER}}}; + Program read(context, stencil ? std::span(SAMPLE_ORDER_TEST_READSTENCIL_SPV) : std::span(SAMPLE_ORDER_TEST_READDEPTH_SPV), bindings, 12u); + read.Image(0u, depthView); + if (stencil) read.Image(1u, stencilView); + read.Storage(2u, values.Handle()); + const std::array push{SwapExtent.width, SwapExtent.height, samples}; + const SampleOrderTarget target{image.Handle(), attachment, format, SwapExtent, samples, method, nullptr}; + const SampleOrderPass pass{order, SwapRegion, {}, false}; + std::vector> kept; + Submit(context, [&](VkCommandBuffer commands) { + image.ToGeneral(commands, aspects); + raster.Run(commands); + FullBarrier(context, commands); + read.Dispatch(commands, push.data(), SwapExtent); + HostBarrier(context, commands); + }); + const auto before = ReadValues(values, count); + for (std::size_t index = 0; index < count; ++index) { + const auto key = static_cast(index + 1u); + const auto depth = std::bit_cast(before[index][0]); + const bool depthHeld = unorm16 ? static_cast(depth * 65535.0f + 0.5f) == key : before[index][0] == filled[index][0]; + Require(depthHeld && (!stencil || before[index][1] == filled[index][1]), name + ": the filled sample " + std::to_string(index) + " did not read back"); + } + Submit(context, [&](VkCommandBuffer commands) { + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(&target, 1u), true, kept); + FullBarrier(context, commands); + read.Dispatch(commands, push.data(), SwapExtent); + HostBarrier(context, commands); + }); + CheckOrdered(before, ReadValues(values, count), SwapExtent, SwapRegion, samples, order, true, name + " put in the host's order"); + Submit(context, [&](VkCommandBuffer commands) { + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(&target, 1u), false, kept); + FullBarrier(context, commands); + read.Dispatch(commands, push.data(), SwapExtent); + HostBarrier(context, commands); + }); + Require(ReadValues(values, count) == before, name + ": putting the samples back in the guest's order did not restore every sample bit for bit"); + std::printf("%s round-trips\n", name.c_str()); + return true; +} + +std::vector SwapSampleCounts(const Context& context) { + std::vector counts; + for (const std::uint32_t samples : {2u, 4u, 8u}) { + if ((context.limits.framebufferColorSampleCounts & samples) != 0u && (context.limits.framebufferDepthSampleCounts & samples) != 0u) counts.push_back(samples); + } + return counts; +} + +std::uint32_t SwapPasses(const Context& context) { + std::uint32_t swapped = 0; + for (const auto samples : SwapSampleCounts(context)) { + for (const auto& [format, bytes] : ColorFormats) swapped += ColorSwaps(context, format, bytes, samples) ? 1u : 0u; + swapped += DepthSwaps(context, VK_FORMAT_D32_SFLOAT, samples, SampleOrderMethod::Depth) ? 1u : 0u; + swapped += DepthSwaps(context, VK_FORMAT_D16_UNORM, samples, SampleOrderMethod::Depth) ? 1u : 0u; + for (const auto format : {VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D16_UNORM_S8_UINT}) { + swapped += DepthSwaps(context, format, samples, SampleOrderMethod::Depth) ? 1u : 0u; + swapped += DepthSwaps(context, format, samples, SampleOrderMethod::DepthStencilExport) ? 1u : 0u; + swapped += DepthSwaps(context, format, samples, SampleOrderMethod::DepthStencilBits) ? 1u : 0u; + } + } + return swapped; +} + +using Vertices = std::vector>; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array NullExportCode{0xf8001890, 0x00000000, 0xbf810000}; + +constexpr std::uint32_t RemapWidth = 64; +constexpr std::uint32_t RemapHeight = 8; +alignas(256) std::array Invocations{}; +alignas(256) std::array, 4> StoreCodes{}; +alignas(256) std::array, 8> MaskCodes{}; +alignas(256) constexpr std::array SampleColorCode{0xd5480007, 0x02111106, 0x7e0e0d07, 0xf800180f, 0x07070100, 0xbf810000, 0xbf810000}; +alignas(256) std::array DepthMemory{}; + +std::span StoreCode(std::uint32_t samples) { + const auto shift = static_cast(std::countr_zero(samples)); + auto& code = StoreCodes[shift]; + code = {0x7e100f04, 0x7e120f05, 0x34121286, 0x4a101308, 0x34101080u | shift, 0xd548000a, 0x02111106, 0x4a101508, 0x34101082, 0x7e160281, 0xe0705000, 0x80000b08, 0xbf8c3f70, 0xf8001890, 0x00000000, 0xbf810000, 0xbf810000}; + return code; +} + +std::span MaskCode(std::size_t slot, std::uint32_t mask) { + auto& code = MaskCodes[slot]; + code = {0x7e1002ffu, mask, 0xf8001884u, 0x00080000u, 0xbf810000u}; + return code; +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, stride == 0u ? 0x31016facu : 0x01016facu}; +} + +struct Pass { + const Vertices* vertices = nullptr; + const void* vertexMemory = nullptr; + std::span code; + bool perSample = false; + std::uint32_t shaderControl = 0u; + std::uint32_t colorFormat = 0u; + std::uint32_t samples = 4u; + VkExtent2D extent{RemapWidth, RemapHeight}; + std::optional depth; + bool depthWrite = false; + VkCompareOp depthCompare = VK_COMPARE_OP_ALWAYS; + std::optional color; + std::optional> locations; + std::optional order; + std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; + bool storesInvocations = false; + std::function beforeReplay; + bool buildsNoRecipe = false; +}; + +void Draw(AgcDriver::VulkanDevice& device, const Context& context, const Pass& pass) { + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(pass.vertexMemory != nullptr ? pass.vertexMemory : pass.vertices->data(), 16u, static_cast(pass.vertices->size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.hasColorTarget = pass.color.has_value(); + if (pass.color) { + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + state.colors = {*pass.color}; + state.color = *pass.color; + state.blends = {blend}; + state.blend = blend; + } + state.renderExtent = pass.extent; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(pass.extent.height), static_cast(pass.extent.width), -static_cast(pass.extent.height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, pass.extent}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blendConstants = {}; + state.samples = pass.samples; + state.perSampleShading = pass.perSample; + state.quadSampleMask = pass.quadSampleMask; + state.pixelEarlyZ = AgcDriver::Graphics::PixelEarlyZ(pass.shaderControl); + if (pass.locations) { + state.customSampleLocations = true; + state.sampleLocations = *pass.locations; + for (std::uint32_t pixel = 1; pixel < 4u; ++pixel) { + for (std::uint32_t sample = 0; sample < pass.samples; ++sample) state.sampleLocationGrid |= state.sampleLocations[pixel * 8u + sample] != state.sampleLocations[sample]; + } + } + if (pass.depth) { + state.depth = pass.depth; + state.depthTest = true; + state.depthWrite = pass.depthWrite; + state.depthCompare = pass.depthCompare; + } + + const std::uint32_t inputs = pass.perSample ? PixelInputBit(PixelInput::PerspectiveSample) | PixelInputBit(PixelInput::LinearSample) | PixelInputBit(PixelInput::PositionX) | PixelInputBit(PixelInput::PositionY) | PixelInputBit(PixelInput::Ancillary) : 0u; + const AgcDriver::Registers registers{{0x1b3, inputs}, {0x1b4, inputs}, {0x1b6, 0u}, {0x1c5, pass.colorFormat}, {0x203, pass.shaderControl}}; + auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(registers, AgcDriver::Graphics::ExportMappings(state)); + Require(pixel.sampleShading == pass.perSample, "the pixel inputs did not select the requested shading rate"); + pixel.iteratesSamples = state.perSampleShading; + pixel.sampleLocations = AgcDriver::Graphics::PixelSampleLocations(state, pixel); + const auto order = pass.order ? *pass.order : state.sampleLocationGrid ? AgcDriver::Graphics::HostSampleLocationsFor(context, state).order : SampleOrder{}; + pixel.sampleOrder = order; + pixel.quadPixelMask = AgcDriver::Graphics::QuadPixelMask(state); + pixel.quadSampleMask = AgcDriver::Graphics::QuadSampleMasks(state, order); + std::vector pixelUserData(4, 0u); + if (pass.storesInvocations) { + const auto invocations = BufferDescriptor(Invocations.data(), 0u, static_cast(sizeof(Invocations))); + std::copy(invocations.begin(), invocations.end(), pixelUserData.begin()); + } + const std::array pixelMemory{{{reinterpret_cast(pass.code.data()), std::as_bytes(pass.code)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pass.code.data()), pass.code, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + std::vector shaders{{ShaderStage::Vertex, &vertexResult, 0}}; + ShaderRecompiler::RecompileResult geometry; + if (pixelResult.barycentricEmulation.active) { + geometry = ShaderRecompiler::BuildBarycentricGeometryShader(vertexResult, pixelResult, target, device.GeometryLimits()); + shaders.push_back({ShaderStage::Geometry, &geometry, 0}); + } + shaders.push_back({ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}); + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(pass.vertices->size()), 0, 1, 0, false}; + if (!pass.beforeReplay && !pass.buildsNoRecipe) { + AgcDriver::Graphics::Draw(context, state, draw, shaders); + return; + } + std::shared_ptr recipe; + AgcDriver::Graphics::Draw(context, state, draw, shaders, {}, &recipe); + if (pass.buildsNoRecipe) { + Require(recipe == nullptr, "a draw that reorders its samples built a recipe"); + return; + } + Require(recipe != nullptr, "the recorded draw built no recipe"); + pass.beforeReplay(); + const auto replayed = AgcDriver::Graphics::DrawWithRecipe(context, state, draw, shaders, {}, *recipe); + Require(replayed.recorded, std::string("the draw did not replay its recipe: ") + AgcDriver::Graphics::DrawRecipeMissName(replayed.miss)); +} + +void Settle(AgcDriver::VulkanDevice& device) { + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + } + device.WaitIdle(); +} + +Vertices Rectangle(float left, float right, float z) { + return {{left, -1.0f, z, 1.0f}, {right, -1.0f, z, 1.0f}, {right, 1.0f, z, 1.0f}, {left, -1.0f, z, 1.0f}, {right, 1.0f, z, 1.0f}, {left, 1.0f, z, 1.0f}}; +} + +Vertices Screen(float z) { + return {{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}; +} + +float StandardX(std::uint32_t samples, std::uint32_t sample) { + static constexpr std::array two{0.75f, 0.25f}; + static constexpr std::array four{0.375f, 0.875f, 0.125f, 0.625f}; + static constexpr std::array eight{0.5625f, 0.4375f, 0.8125f, 0.3125f, 0.1875f, 0.0625f, 0.6875f, 0.9375f}; + return samples == 2u ? two[sample] : samples == 4u ? four[sample] : eight[sample]; +} + +void CheckStored(std::uint32_t samples, const std::string& what, const std::function& expected) { + for (std::uint32_t y = 0; y < RemapHeight; ++y) { + for (std::uint32_t x = 0; x < RemapWidth; ++x) { + const auto mask = expected(x, y); + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto stored = Invocations[(y * RemapWidth + x) * samples + sample]; + Require(stored == ((mask >> sample) & 1u), what + ": guest " + At(x, y, sample) + (stored != 0u ? " ran" : " did not run") + ", expected guest mask " + std::to_string(mask)); + } + } + } +} + +std::uint32_t GuestBits(std::uint32_t hostBits, std::uint32_t word, std::uint32_t samples) { + std::uint32_t guest = 0; + for (std::uint32_t host = 0; host < samples; ++host) { + if (((hostBits >> host) & 1u) != 0u) guest |= 1u << Pick(word, host); + } + return guest; +} + +void SampleIndexRemap(AgcDriver::VulkanDevice& device, const Context& context, std::uint32_t samples) { + const auto order = OrderFor(samples); + const auto toNdc = [](float pixel) { return pixel * 2.0f / static_cast(RemapWidth) - 1.0f; }; + const auto rectangle = Rectangle(toNdc(2.5f), toNdc(5.5f), 0.5f); + for (const bool remapped : {false, true}) { + Invocations.fill(0u); + Pass pass; + pass.vertices = &rectangle; + pass.code = StoreCode(samples); + pass.perSample = true; + pass.samples = samples; + pass.order = remapped ? order : SampleOrder{}; + pass.storesInvocations = true; + Draw(device, context, pass); + Settle(device); + CheckStored(samples, std::to_string(samples) + "x SampleID " + (remapped ? "under the test's sample order" : "in the host's order"), [&](std::uint32_t x, std::uint32_t y) { + std::uint32_t host = 0; + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto at = static_cast(x) + StandardX(samples, sample); + if (at > 2.5f && at < 5.5f) host |= 1u << sample; + } + return remapped ? GuestBits(host, order[Quad(x, y)], samples) : host; + }); + } + std::printf("%ux SampleID reads the guest's sample index in all four pixels of the 2x2 grid\n", samples); +} + +void CoverageRemap(AgcDriver::VulkanDevice& device, const Context& context, std::uint32_t samples) { + const auto order = OrderFor(samples); + const auto back = Screen(0.25f), middle = Screen(0.5f), probe = Screen(0.375f); + const AgcDriver::Graphics::DepthTarget depth = [&] { + AgcDriver::Graphics::DepthTarget target{reinterpret_cast(DepthMemory.data()) + 256u * 1024u * static_cast(std::countr_zero(samples)), 0u, {RemapWidth, RemapHeight}, VK_FORMAT_D32_SFLOAT, 0.0f, 0}; + target.samples = samples; + return target; + }(); + const std::array masks{1u, samples == 2u ? 2u : samples == 4u ? 6u : 0x96u}; + std::size_t slot = 0; + for (const auto mask : masks) { + Pass reset; + reset.vertices = &back; + reset.code = NullExportCode; + reset.samples = samples; + reset.depth = depth; + reset.depthWrite = true; + Draw(device, context, reset); + Pass masked = reset; + masked.vertices = &middle; + masked.code = MaskCode(slot++ + (samples == 2u ? 0u : samples == 4u ? 2u : 4u), mask); + masked.shaderControl = 0x100u; + masked.order = order; + Draw(device, context, masked); + Settle(device); + Invocations.fill(0u); + Pass check; + check.vertices = &probe; + check.code = StoreCode(samples); + check.perSample = true; + check.shaderControl = 0x10u; + check.samples = samples; + check.depth = depth; + check.depthCompare = VK_COMPARE_OP_LESS; + check.order = order; + check.storesInvocations = true; + Draw(device, context, check); + Settle(device); + CheckStored(samples, std::to_string(samples) + "x samples a pixel shader's exported guest mask " + std::to_string(mask) + " wrote depth into", [&](std::uint32_t, std::uint32_t) { return mask; }); + } + std::printf("%ux exported sample masks reach the host samples at the guest samples' locations\n", samples); +} + +void CoverageOrder(AgcDriver::VulkanDevice& device, const Context& context, std::uint32_t samples) { + const auto order = OrderFor(samples); + const auto toNdc = [](float pixel) { return pixel * 2.0f / static_cast(RemapWidth) - 1.0f; }; + const auto rectangle = Rectangle(toNdc(2.5f), toNdc(5.5f), 0.5f); + const auto back = Screen(0.25f), probe = Screen(0.375f); + const AgcDriver::Graphics::DepthTarget depth = [&] { + AgcDriver::Graphics::DepthTarget target{reinterpret_cast(DepthMemory.data()) + 256u * 1024u * (static_cast(std::countr_zero(samples)) + 4u), 0u, {RemapWidth, RemapHeight}, VK_FORMAT_D32_SFLOAT, 0.0f, 0}; + target.samples = samples; + return target; + }(); + const std::array mask{2u, 2u, 2u, 4u}; + const auto full = (1u << samples) - 1u; + const auto covered = [&](std::uint32_t x, std::uint32_t y) { + std::uint32_t host = 0u; + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto at = static_cast(x) + StandardX(samples, sample); + if (at > 2.5f && at < 5.5f) host |= 1u << sample; + } + return GuestBits(host, order[Quad(x, y)], samples) & (mask[Quad(x, y)] & full); + }; + Pass reset; + reset.vertices = &back; + reset.code = NullExportCode; + reset.samples = samples; + reset.depth = depth; + reset.depthWrite = true; + Draw(device, context, reset); + Invocations.fill(0u); + Pass masked; + masked.vertices = &rectangle; + masked.code = StoreCode(samples); + masked.perSample = true; + masked.samples = samples; + masked.depth = depth; + masked.depthWrite = true; + masked.order = order; + masked.quadSampleMask = mask; + masked.storesInvocations = true; + Draw(device, context, masked); + Settle(device); + CheckStored(samples, std::to_string(samples) + "x a quad sample mask reached the pixel shader in the guest's order", covered); + Invocations.fill(0u); + Pass check; + check.vertices = &probe; + check.code = StoreCode(samples); + check.perSample = true; + check.shaderControl = 0x10u; + check.samples = samples; + check.depth = depth; + check.depthCompare = VK_COMPARE_OP_LESS; + check.order = order; + check.storesInvocations = true; + Draw(device, context, check); + Settle(device); + CheckStored(samples, std::to_string(samples) + "x a quad sample mask exported through the sample order landed on the host's samples", covered); + std::printf("%ux a quad sample mask reaches the pixel shader in the guest's order and exports coverage in the host's\n", samples); +} + +std::uint32_t ShaderRemaps(AgcDriver::VulkanDevice& device, const Context& context) { + std::uint32_t checked = 0; + for (const std::uint32_t samples : {2u, 4u, 8u}) { + if ((context.limits.framebufferDepthSampleCounts & samples) == 0u || (context.limits.framebufferNoAttachmentsSampleCounts & samples) == 0u) continue; + if (context.limits.standardSampleLocations) { + SampleIndexRemap(device, context, samples); + ++checked; + } else { + std::printf("skipped the %ux SampleID check: the device has no standard sample locations\n", samples); + } + CoverageRemap(device, context, samples); + CoverageOrder(device, context, samples); + ++checked; + } + return checked; +} + +constexpr std::size_t BlockBytes = 65536; + +class GuestBlock { +public: + explicit GuestBlock(std::size_t bytes) : bytes((bytes + BlockBytes - 1u) & ~(BlockBytes - 1u)) { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, this->bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, this->bytes)); +#endif + Require(block != nullptr, "sample order test: cannot allocate a guest block"); + std::memset(block, 0, this->bytes); + GuestAllocations::Mutation().Add(block, this->bytes, true, true, true); + } + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::size_t bytes; + std::byte* block = nullptr; +}; + +GuestBlock& NewBlock(std::size_t bytes) { + static auto* blocks = new std::deque(); + return blocks->emplace_back(bytes); +} + +constexpr VkExtent2D ReferenceExtent{64u, 32u}; +constexpr std::array VillageLocations{0x04, 0x0c, 0, 0, 0, 0, 0, 0, 0x04, 0x0c, 0, 0, 0, 0, 0, 0, 0x0c, 0x04, 0, 0, 0, 0, 0, 0, 0x0c, 0x04, 0, 0, 0, 0, 0, 0}; +constexpr float ClearDepth = 0.9375f; +constexpr std::size_t SceneSlots = 6u; +const Vertices Slopes{{-1.0f, -1.0f, 0.125f, 1.0f}, {1.0f, -0.8125f, 0.875f, 1.0f}, {-0.5625f, 1.0f, 0.5f, 1.0f}, {1.0f, -1.0f, 0.75f, 1.0f}, {0.6875f, 1.0f, 0.0625f, 1.0f}, {-1.0f, 0.4375f, 0.3125f, 1.0f}}; + +struct Rendered { + Samples color; + Samples depth; +}; + +double SlopeDepth(std::size_t triangle, double x, double y) { + std::array, 3> corner{}; + for (std::size_t vertex = 0; vertex < corner.size(); ++vertex) { + const auto& at = Slopes[triangle * 3u + vertex]; + corner[vertex] = {(at[0] + 1.0) * ReferenceExtent.width / 2.0, (1.0 - at[1]) * ReferenceExtent.height / 2.0, at[2]}; + } + const auto edge = [](const std::array& from, const std::array& to, double px, double py) { return (to[0] - from[0]) * (py - from[1]) - (to[1] - from[1]) * (px - from[0]); }; + const auto area = edge(corner[0], corner[1], corner[2][0], corner[2][1]); + return (edge(corner[1], corner[2], x, y) * corner[0][2] + edge(corner[2], corner[0], x, y) * corner[1][2] + edge(corner[0], corner[1], x, y) * corner[2][2]) / area; +} + +struct DepthLocations { + std::size_t sloped = 0; + std::size_t guest = 0; + std::size_t standard = 0; + std::array slopedByRow{}; + std::array guestByRow{}; +}; + +DepthLocations LocateDepth(const Samples& depth, const std::array& locations = VillageLocations) { + constexpr std::uint32_t samples = 2u; + constexpr std::array, 2> standard{{{0.75, 0.75}, {0.25, 0.25}}}; + DepthLocations located; + for (std::size_t index = 0; index < depth.size(); ++index) { + const auto value = static_cast(std::bit_cast(depth[index][0])); + if (value == ClearDepth) continue; + ++located.sloped; + const auto pixel = static_cast(index / samples); + const auto sample = static_cast(index % samples); + const auto x = pixel % ReferenceExtent.width; + const auto y = pixel / ReferenceExtent.width; + ++located.slopedByRow[y & 1u]; + const auto bits = static_cast(locations[Quad(x, y) * 8u + sample]); + const std::array guest{0.5 + (static_cast(bits & 7u) - static_cast(bits & 8u)) / 16.0, 0.5 + (static_cast((bits >> 4u) & 7u) - static_cast((bits >> 4u) & 8u)) / 16.0}; + const auto holds = [&](const std::array& at) { + for (std::size_t triangle = 0; triangle < 2u; ++triangle) { + if (std::abs(SlopeDepth(triangle, x + at[0], y + at[1]) - value) < 1.0e-5) return true; + } + return false; + }; + if (holds(guest)) { + ++located.guest; + ++located.guestByRow[y & 1u]; + } else if (holds(standard[sample])) { + ++located.standard; + } + } + return located; +} + +DepthLocations PrintDepthLocations(const std::string& what, const DepthLocations& located) { + std::printf("%s: of %zu sloped depth samples, %zu hold the slope's depth at the guest's sample locations (%zu of %zu on even rows, %zu of %zu on odd rows) and %zu at the standard 2x locations\n", what.c_str(), located.sloped, located.guest, located.guestByRow[0], located.slopedByRow[0], located.guestByRow[1], located.slopedByRow[1], located.standard); + return located; +} + +DepthLocations ReportDepthLocations(const std::string& what, const Rendered& rendered, const std::array& locations = VillageLocations) { + return PrintDepthLocations(what, LocateDepth(rendered.depth, locations)); +} + +Samples ReadTarget(const Context& context, VkImageView view, bool color, std::uint32_t samples) { + const auto count = static_cast(ReferenceExtent.width) * ReferenceExtent.height * samples; + AgcDriver::Graphics::Buffer values(context, count * 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const std::array bindings{{{0u, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, {2u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER}}}; + Program read(context, color ? std::span(SAMPLE_ORDER_TEST_READCOLOR_SPV) : std::span(SAMPLE_ORDER_TEST_READDEPTH_SPV), bindings, 12u); + read.Image(0u, view); + read.Storage(2u, values.Handle()); + const std::array push{ReferenceExtent.width, ReferenceExtent.height, samples}; + Submit(context, [&](VkCommandBuffer commands) { + FullBarrier(context, commands); + read.Dispatch(commands, push.data(), ReferenceExtent); + HostBarrier(context, commands); + }); + return ReadValues(values, count); +} + +Rendered RenderCheckerboard(AgcDriver::VulkanDevice& device, const Context& context, std::uint32_t samples, std::size_t run, const std::optional>& locations) { + AgcDriver::Graphics::ColorTarget color{}; + color.extent = ReferenceExtent; + color.surfaceExtent = ReferenceExtent; + color.format = VK_FORMAT_R16G16B16A16_SFLOAT; + color.componentMapping = 0xe4u; + color.tileMode = AgcDriver::Graphics::ColorTileMode::RenderTarget; + color.elementBytes = 8u; + color.samples = samples; + color.bytes = AgcDriver::Graphics::ColorTargetLayout(ReferenceExtent.width, ReferenceExtent.height, color.tileMode, color.elementBytes).Bytes(); + auto& block = NewBlock(AgcDriver::Graphics::MultisampledColorBytes(color)); + color.address = block.Address(); + color.surfaceAddress = color.address; + AgcDriver::Graphics::DepthTarget depth{reinterpret_cast(DepthMemory.data()) + 256u * 1024u * run, 0u, ReferenceExtent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + depth.samples = samples; + const Vertices clear = Screen(ClearDepth); + const Vertices shade{{-0.9375f, -0.875f, 0.25f, 1.0f}, {0.9375f, -0.625f, 0.6875f, 1.0f}, {0.0625f, 0.9375f, 0.4375f, 1.0f}}; + Pass depthOnly; + depthOnly.vertices = &clear; + depthOnly.code = NullExportCode; + depthOnly.samples = samples; + depthOnly.extent = ReferenceExtent; + depthOnly.depth = depth; + depthOnly.depthWrite = true; + depthOnly.locations = locations; + Draw(device, context, depthOnly); + depthOnly.vertices = &Slopes; + depthOnly.depthCompare = VK_COMPARE_OP_LESS; + Draw(device, context, depthOnly); + Pass colorPass; + colorPass.vertices = &clear; + colorPass.code = SampleColorCode; + colorPass.perSample = true; + colorPass.colorFormat = 9u; + colorPass.samples = samples; + colorPass.extent = ReferenceExtent; + colorPass.depth = depth; + colorPass.depthCompare = VK_COMPARE_OP_ALWAYS; + colorPass.color = color; + colorPass.locations = locations; + Draw(device, context, colorPass); + colorPass.vertices = &shade; + colorPass.depthCompare = VK_COMPARE_OP_LESS_OR_EQUAL; + Draw(device, context, colorPass); + Settle(device); + Rendered rendered; + rendered.color = ReadTarget(context, AgcDriver::Graphics::MultisampleTargetView(context, color), true, samples); + rendered.depth = ReadTarget(context, AgcDriver::Graphics::DepthSurfaceOrderTarget(context, depth).view, false, samples); + return rendered; +} + +std::size_t Differences(const Samples& reference, const Samples& rendered, std::uint32_t samples, const std::string& what, bool report) { + std::size_t differences = 0; + for (std::size_t index = 0; index < reference.size(); ++index) { + if (reference[index] == rendered[index]) continue; + if (report && differences < 4u) { + const auto pixel = static_cast(index / samples); + std::printf("%s: %s is 0x%08x 0x%08x 0x%08x 0x%08x, the reference's is 0x%08x 0x%08x 0x%08x 0x%08x\n", what.c_str(), At(pixel % ReferenceExtent.width, pixel / ReferenceExtent.width, static_cast(index % samples)).c_str(), rendered[index][0], rendered[index][1], rendered[index][2], rendered[index][3], reference[index][0], reference[index][1], reference[index][2], reference[index][3]); + } + ++differences; + } + return differences; +} + +struct ClearTargets { + AgcDriver::VulkanDevice& device; + VkDevice handle; + ~ClearTargets() { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(handle); + AgcDriver::Graphics::ClearCachedTextures(handle); + } +}; + +void CheckScene(const Rendered& rendered, std::uint32_t samples, const std::string& what) { + std::size_t cleared = 0; + for (std::size_t index = 0; index < rendered.depth.size(); ++index) { + const auto sample = static_cast(index % samples); + const auto pixel = static_cast(index / samples); + const auto at = At(pixel % ReferenceExtent.width, pixel / ReferenceExtent.width, sample); + const auto id = static_cast(sample); + const auto z = std::bit_cast(rendered.color[index][2]); + const auto w = std::bit_cast(rendered.color[index][3]); + Require(z == id && w == id, what + ": " + at + " does not hold its own sample index in color (z " + std::to_string(z) + ", w " + std::to_string(w) + ")"); + const auto depth = std::bit_cast(rendered.depth[index][0]); + Require(depth >= 0.0f && depth <= ClearDepth, what + ": " + at + " holds depth " + std::to_string(depth)); + if (depth == ClearDepth) ++cleared; + } + Require(cleared != 0u && cleared != rendered.depth.size(), what + ": the slopes did not leave depth both at and below the clear value"); +} + +std::uint32_t SceneChecks(AgcDriver::VulkanDevice& device, const Context& context) { + ClearTargets clearTargets{device, context.device}; + std::uint32_t checked = 0; + for (const std::uint32_t samples : {2u, 4u, 8u}) { + if ((context.limits.framebufferColorSampleCounts & samples) == 0u || (context.limits.framebufferDepthSampleCounts & samples) == 0u || !DepthSampleable(context, VK_FORMAT_D32_SFLOAT, samples)) continue; + CheckScene(RenderCheckerboard(device, context, samples, SceneSlots + static_cast(std::countr_zero(samples)), std::nullopt), samples, std::to_string(samples) + "x scene with the standard locations"); + ++checked; + } + return checked; +} + +std::array UniformLocations(const std::array& pattern, std::uint32_t pixel) { + std::array uniform{}; + for (std::uint32_t target = 0; target < 4u; ++target) std::copy_n(pattern.begin() + 8u * pixel, 8u, uniform.begin() + 8u * target); + return uniform; +} + +Rendered QuadComposite(AgcDriver::VulkanDevice& device, const Context& context, std::uint32_t samples, std::size_t run, const std::array& pattern) { + std::array parts; + for (std::uint32_t pixel = 0; pixel < parts.size(); ++pixel) parts[pixel] = RenderCheckerboard(device, context, samples, run + pixel, UniformLocations(pattern, pixel)); + auto composite = parts[0]; + for (std::size_t index = 0; index < composite.color.size(); ++index) { + const auto pixel = static_cast(index / samples); + const auto& part = parts[Quad(pixel % ReferenceExtent.width, pixel / ReferenceExtent.width)]; + composite.color[index] = part.color[index]; + composite.depth[index] = part.depth[index]; + } + return composite; +} + +int GridReference(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + ClearTargets clearTargets{device, context.device}; + constexpr std::uint32_t samples = 2u; + if ((context.limits.framebufferColorSampleCounts & samples) == 0u || (context.limits.framebufferDepthSampleCounts & samples) == 0u) { + std::printf("skipped, the device has no 2x color and depth targets\n"); + return VulkanTestSkipped; + } + if ((context.sampleLocationSampleCounts & samples) == 0u) { + std::printf("skipped, the device has no custom sample locations at 2 samples\n"); + return VulkanTestSkipped; + } + const auto largest = context.sampleLocationGridSizes[1]; + std::printf("the device's largest sample location grid at 2 samples is %ux%u pixels (vkGetPhysicalDeviceMultisamplePropertiesEXT)\n", largest.width, largest.height); + auto emulated = context; + emulated.sampleLocationGridSampleCounts = 0u; + auto fallback = emulated; + fallback.storageImageMultisample = false; + AgcDriver::Graphics::State probe{}; + probe.samples = samples; + probe.customSampleLocations = true; + probe.sampleLocationGrid = true; + probe.sampleLocations = VillageLocations; + AgcDriver::Graphics::ColorTarget color{}; + color.format = VK_FORMAT_R16G16B16A16_SFLOAT; + color.elementBytes = 8u; + color.samples = samples; + probe.colors = {color}; + probe.depth = AgcDriver::Graphics::DepthTarget{0x100000u, 0u, ReferenceExtent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + probe.depth->samples = samples; + const auto refusal = AgcDriver::Graphics::SampleOrderRefusal(emulated, probe); + if (!refusal.empty()) { + std::printf("skipped, the device does not put the checkerboard's samples in the guest's order: %s\n", refusal.c_str()); + return VulkanTestSkipped; + } + Require(AgcDriver::Graphics::ReordersSamples(AgcDriver::Graphics::HostSampleLocationsFor(emulated, probe).order) && !AgcDriver::Graphics::ReordersSamples(AgcDriver::Graphics::HostSampleLocationsFor(fallback, probe).order), "the emulated and fallback contexts did not take the exact and approximate paths"); + const auto reference = QuadComposite(device, context, samples, 3u, VillageLocations); + const auto exact = RenderCheckerboard(device, emulated, samples, 1u, VillageLocations); + const auto approximate = RenderCheckerboard(device, fallback, samples, 2u, VillageLocations); + CheckScene(reference, samples, "the composite of uniform renders"); + CheckScene(exact, samples, "the exact path"); + CheckScene(approximate, samples, "pixel (0,0)'s locations alone"); + const bool grid = (context.sampleLocationGridSampleCounts & samples) != 0u; + std::optional native; + if (grid) { + native = RenderCheckerboard(device, context, samples, 0u, VillageLocations); + CheckScene(*native, samples, "the 2x2 grid"); + } + const auto referenceDepth = ReportDepthLocations("the composite of uniform renders", reference); + ReportDepthLocations("the exact path", exact); + ReportDepthLocations("pixel (0,0)'s locations alone", approximate); + const auto nativeDepth = native ? std::optional(ReportDepthLocations("the device's 2x2 grid", *native)) : std::nullopt; + const bool gridDepthCompared = nativeDepth && nativeDepth->guest == nativeDepth->sloped; + if (referenceDepth.guest != referenceDepth.sloped) { + std::printf("skipped, the device does not keep depth drawn at custom sample locations at those locations, even after a transition that names them: the composite of uniform renders holds the guest's depth in %zu of %zu sloped samples, so no path can be compared with it bit for bit\n", referenceDepth.guest, referenceDepth.sloped); + return VulkanTestSkipped; + } + std::size_t gridDifferences = 0; + if (native) { + gridDifferences = Differences(reference.color, native->color, samples, "color through the device's 2x2 grid", true) + (gridDepthCompared ? Differences(reference.depth, native->depth, samples, "depth through the device's 2x2 grid", true) : 0u); + if (!gridDepthCompared) std::printf("the device's 2x2 grid holds the guest's depth in %zu of %zu sloped samples as the device reads it back, so only its color is compared with the composite\n", nativeDepth->guest, nativeDepth->sloped); + const auto exactGridDifferences = Differences(native->color, exact.color, samples, "color", false) + Differences(native->depth, exact.depth, samples, "depth", false); + std::printf("2x checkerboard at %ux%u: the device's 2x2 grid differs from the composite of four uniform renders in %zu samples and from the exact path in %zu samples\n", ReferenceExtent.width, ReferenceExtent.height, gridDifferences, exactGridDifferences); + } else { + std::printf("the device has no 2x2 sample location grid at 2 samples: the composite of four uniform renders is the reference\n"); + } + const auto colorDifferences = Differences(reference.color, exact.color, samples, "color with the samples put in the guest's order", true); + const auto depthDifferences = Differences(reference.depth, exact.depth, samples, "depth with the samples put in the guest's order", true); + const auto approximateDifferences = Differences(reference.color, approximate.color, samples, "color", false) + Differences(reference.depth, approximate.depth, samples, "depth", false); + std::printf("2x checkerboard at %ux%u: the exact path differs from the reference in %zu color and %zu depth samples; pixel (0,0)'s locations alone differ in %zu samples\n", ReferenceExtent.width, ReferenceExtent.height, colorDifferences, depthDifferences, approximateDifferences); + Require(approximateDifferences != 0u, "pixel (0,0)'s locations alone rendered the checkerboard like the reference: the scene does not tell the paths apart"); + Require(gridDifferences == 0u, "the composite of four uniform renders does not render the checkerboard like the device's 2x2 grid"); + Require(colorDifferences == 0u && depthDifferences == 0u, "the exact path did not render the checkerboard bit for bit like the reference"); + std::puts(grid && gridDepthCompared ? "the exact path renders Village's 2x checkerboard like the device's 2x2 sample location grid, bit for bit" : "the exact path renders Village's 2x checkerboard like four uniform renders composed by pixel parity, bit for bit"); + return 0; +} + +void CheckRecipeDepth(AgcDriver::VulkanDevice& device, const Context& context, const std::array& locations, std::size_t run, const std::string& what, bool reordered) { + constexpr std::uint32_t samples = 2u; + AgcDriver::Graphics::DepthTarget depth{reinterpret_cast(DepthMemory.data()) + 256u * 1024u * run, 0u, ReferenceExtent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + depth.samples = samples; + const Vertices clear = Screen(ClearDepth); + Pass clearPass; + clearPass.vertices = &clear; + clearPass.code = NullExportCode; + clearPass.samples = samples; + clearPass.extent = ReferenceExtent; + clearPass.depth = depth; + clearPass.depthWrite = true; + clearPass.locations = locations; + auto slopes = clearPass; + slopes.vertices = &Slopes; + auto& vertexBlock = NewBlock(Slopes.size() * sizeof(Slopes[0])); + std::memcpy(reinterpret_cast(static_cast(vertexBlock.Address())), Slopes.data(), Slopes.size() * sizeof(Slopes[0])); + slopes.vertexMemory = reinterpret_cast(static_cast(vertexBlock.Address())); + if (reordered) { + slopes.buildsNoRecipe = true; + Draw(device, context, clearPass); + Draw(device, context, slopes); + Settle(device); + std::printf("%s: builds no recipe, since a draw that reorders its samples is built in full each time, with its sample swaps\n", what.c_str()); + return; + } + const auto read = [&] { return ReadTarget(context, AgcDriver::Graphics::DepthSurfaceOrderTarget(context, depth).view, false, samples); }; + Samples recorded; + slopes.beforeReplay = [&] { + Settle(device); + recorded = read(); + Draw(device, context, clearPass); + Settle(device); + const auto cleared = read(); + Require(std::all_of(cleared.begin(), cleared.end(), [](const std::array& value) { return std::bit_cast(value[0]) == ClearDepth; }), what + ": the clear between the recorded draw and its replay left depth other than the clear value"); + }; + Draw(device, context, clearPass); + Draw(device, context, slopes); + Settle(device); + const auto replayed = read(); + const auto first = PrintDepthLocations(what + ", the recorded draw", LocateDepth(recorded, locations)); + PrintDepthLocations(what + ", the recipe's replay as the first draw of a pass", LocateDepth(replayed, locations)); + Require(first.sloped != 0u, what + ": the recorded draw left no sloped depth"); + const bool readAtGuestLocations = first.guest == first.sloped; + if (!readAtGuestLocations) std::printf("%s: the device reads depth drawn at custom sample locations back at other locations, even after a transition that names them: the replay is compared with the recorded draw as the device reads both back\n", what.c_str()); + const auto differences = Differences(recorded, replayed, samples, what + ", depth replayed from the recipe", true); + std::printf("%s, %ux%u: the recipe's replay differs from the recorded draw in %zu of %zu samples\n", what.c_str(), ReferenceExtent.width, ReferenceExtent.height, differences, recorded.size()); + Require(differences == 0u, what + (readAtGuestLocations ? ": the recipe's replay did not leave the depth the recorded draw left at the guest's sample locations" : ": the recipe's replay did not leave the depth the recorded draw left, as the device reads both back")); + std::printf("%s: a depth-only 2x draw replayed from its recipe as the first draw of a pass leaves the depth the recorded draw left%s, bit for bit\n", what.c_str(), readAtGuestLocations ? "" : " as the device reads both back"); +} + +int RecipeDepthLocations(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + ClearTargets clearTargets{device, context.device}; + constexpr std::uint32_t samples = 2u; + if ((context.limits.framebufferDepthSampleCounts & samples) == 0u || !DepthSampleable(context, VK_FORMAT_D32_SFLOAT, samples)) { + std::printf("skipped, the device has no sampleable 2x depth targets\n"); + return VulkanTestSkipped; + } + if ((context.sampleLocationSampleCounts & samples) == 0u || !AgcDriver::Graphics::SampleLocationsCompatibleDepth(context, VK_FORMAT_D32_SFLOAT, samples)) { + std::printf("skipped, the device has no custom sample locations at 2 samples\n"); + return VulkanTestSkipped; + } + if (!AgcDriver::Graphics::DrawRecipes()) { + std::printf("skipped, draw recipes are off (APS5_NO_DRAW_RECIPE)\n"); + return VulkanTestSkipped; + } + CheckRecipeDepth(device, context, UniformLocations(VillageLocations, 0u), 0u, "pixel (0,0)'s locations in every pixel", false); + auto emulated = context; + emulated.sampleLocationGridSampleCounts = 0u; + AgcDriver::Graphics::State probe{}; + probe.samples = samples; + probe.customSampleLocations = true; + probe.sampleLocationGrid = true; + probe.sampleLocations = VillageLocations; + probe.depth = AgcDriver::Graphics::DepthTarget{0x100000u, 0u, ReferenceExtent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + probe.depth->samples = samples; + const auto refusal = AgcDriver::Graphics::SampleOrderRefusal(emulated, probe); + if (!refusal.empty()) { + std::printf("the checkerboard through the exact path is not checked: the device does not put its samples in the guest's order: %s\n", refusal.c_str()); + return 0; + } + Require(AgcDriver::Graphics::ReordersSamples(AgcDriver::Graphics::HostSampleLocationsFor(emulated, probe).order), "the context without a 2x2 grid did not take the exact path"); + CheckRecipeDepth(device, emulated, UniformLocations(VillageLocations, 0u), 1u, "pixel (0,0)'s locations in every pixel, without a 2x2 grid", false); + CheckRecipeDepth(device, emulated, VillageLocations, 2u, "the 2x checkerboard through the exact path", true); + return 0; +} + +int Bench(AgcDriver::VulkanDevice& device, VkExtent2D extent, std::uint32_t samples, std::uint32_t frames) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if ((context.limits.framebufferColorSampleCounts & samples) == 0u || (context.limits.framebufferDepthSampleCounts & samples) == 0u) { + std::printf("skipped, the device has no %ux color and depth targets\n", samples); + return VulkanTestSkipped; + } + const auto alias = AgcDriver::Graphics::SampleOrderStorageFormat(context, VK_FORMAT_R16G16B16A16_SFLOAT, 8u, samples); + if (alias == VK_FORMAT_UNDEFINED || !DepthSampleable(context, VK_FORMAT_D32_SFLOAT, samples) || !context.limits.timestampComputeAndGraphics) { + std::printf("skipped, the device has no %ux storage alias for RGBA16F, sampleable D32F or timestamps\n", samples); + return VulkanTestSkipped; + } + const bool located = (context.sampleLocationSampleCounts & samples) != 0u && AgcDriver::Graphics::SampleLocationsCompatibleDepth(context, VK_FORMAT_D32_SFLOAT, samples); + Image color(context, VK_FORMAT_R16G16B16A16_SFLOAT, samples, extent, ColorUsage, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT); + Image depth(context, VK_FORMAT_D32_SFLOAT, samples, extent, DepthUsage, located ? VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT : 0u); + const auto storage = color.View(alias, VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_USAGE_STORAGE_BIT); + const auto attachment = depth.View(VK_FORMAT_D32_SFLOAT, VK_IMAGE_ASPECT_DEPTH_BIT, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + const std::array targets{{ + {color.Handle(), storage, alias, extent, samples, SampleOrderMethod::Storage, nullptr}, + {depth.Handle(), attachment, VK_FORMAT_D32_SFLOAT, extent, samples, SampleOrderMethod::Depth, nullptr}, + }}; + VkQueryPoolCreateInfo queryInfo{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO}; + queryInfo.queryType = VK_QUERY_TYPE_TIMESTAMP; + queryInfo.queryCount = 5u; + VkQueryPool queries = VK_NULL_HANDLE; + Check(context.Function("vkCreateQueryPool")(context.device, &queryInfo, nullptr, &queries), "vkCreateQueryPool sample order bench"); + Submit(context, [&](VkCommandBuffer commands) { + color.ToGeneral(commands, VK_IMAGE_ASPECT_COLOR_BIT); + depth.ToGeneral(commands, VK_IMAGE_ASPECT_DEPTH_BIT); + }); + const auto measure = [&](const SampleOrderPass& pass) { + std::vector> phases; + std::vector> kept; + for (std::uint32_t frame = 0; frame <= frames; ++frame) { + kept.clear(); + Submit(context, [&](VkCommandBuffer commands) { + context.Function("vkCmdResetQueryPool")(commands, queries, 0u, 5u); + const auto stamp = [&](std::uint32_t query) { + context.Function("vkCmdWriteTimestamp")(commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queries, query); + }; + stamp(0u); + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(targets).subspan(1u), true, kept); + stamp(1u); + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(targets).subspan(1u), false, kept); + stamp(2u); + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, targets, true, kept); + stamp(3u); + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, targets, false, kept); + stamp(4u); + }); + std::array ticks{}; + Check(context.Function("vkGetQueryPoolResults")(context.device, queries, 0u, 5u, sizeof(ticks), ticks.data(), sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "vkGetQueryPoolResults sample order bench"); + if (frame == 0u) continue; + std::array phase{}; + for (std::size_t index = 0; index < phase.size(); ++index) phase[index] = static_cast(ticks[index + 1u] - ticks[index]) * context.limits.timestampPeriod / 1.0e6; + phases.push_back(phase); + } + std::array median{}; + for (std::size_t index = 0; index < median.size(); ++index) { + std::vector values; + for (const auto& phase : phases) values.push_back(phase[index]); + std::nth_element(values.begin(), values.begin() + static_cast(values.size() / 2u), values.end()); + median[index] = values[values.size() / 2u]; + } + return median; + }; + const auto report = [&](const std::array& median, const char* how) { + std::printf("sample order swaps at %ux%u, %ux, %s, median of %u frames (GPU ms): depth-only pass in %.3f, out %.3f; color+depth pass in %.3f, out %.3f; per frame with one pass of each %.3f\n", extent.width, extent.height, samples, how, frames, median[0], median[1], median[2], median[3], median[0] + median[1] + median[2] + median[3]); + }; + report(measure(SampleOrderPass{OrderFor(samples), extent, {}, false}), "without sample locations"); + if (located) { + AgcDriver::Graphics::State standard{}; + standard.samples = samples; + report(measure(SampleOrderPass{OrderFor(samples), extent, AgcDriver::Graphics::SampleLocations(standard), true}), "naming the standard sample locations at the depth copy's transitions"); + } else { + std::printf("the device draws no %ux depth at custom sample locations: no swap names them\n", samples); + } + context.Function("vkDestroyQueryPool")(context.device, queries, nullptr); + return 0; +} + +} + +int main(int argc, char** argv) { + try { + const std::string_view mode = argc > 1 ? std::string_view(argv[1]) : std::string_view(); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (mode == "--grid-reference") return GridReference(*device); + if (mode == "--recipe-depth-locations") return RecipeDepthLocations(*device); + if (mode == "--bench") { + VkExtent2D extent{1920u, 2160u}; + if (argc > 2) Require(std::sscanf(argv[2], "%ux%u", &extent.width, &extent.height) == 2, "--bench takes WIDTHxHEIGHT"); + const auto samples = argc > 3 ? static_cast(std::strtoul(argv[3], nullptr, 10)) : 2u; + const auto frames = argc > 4 ? static_cast(std::strtoul(argv[4], nullptr, 10)) : 50u; + return Bench(*device, extent, samples, frames); + } + Require(mode.empty(), "unknown mode " + std::string(mode)); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (!context.sampleRateShading) { + std::printf("skipped, the device lacks sampleRateShading\n"); + return VulkanTestSkipped; + } + const auto swapped = SwapPasses(context); + const auto remapped = ShaderRemaps(*device, context); + const auto scenes = SceneChecks(*device, context); + if (swapped == 0u && remapped == 0u && scenes == 0u) { + std::printf("skipped, the device has no multisampled targets the sample order tests can use\n"); + return VulkanTestSkipped; + } + std::printf("sample order tests passed: %u swap round trips, %u shader remap checks, %u scene checks\n", swapped, remapped, scenes); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 9d1978dbb2..46e3184ea6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -2,12 +2,15 @@ #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" #include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include "VulkanTestDevice.hpp" #include #ifdef _WIN32 @@ -15,8 +18,12 @@ #define NOMINMAX #endif #include +#else +#include #endif +#include #include +#include #include #include #include @@ -25,8 +32,11 @@ #include #include #include +#include +#include #include #include +#include #include #include #include @@ -47,9 +57,70 @@ constexpr std::size_t AllocationBytes = BlockBytes + RegistrationGapBytes; constexpr std::uint64_t Budget = 1ull << 20u; constexpr std::uint64_t Ceiling = Budget + Budget / 4u; +std::optional selectDevice(std::span devices, std::string_view request) { + const auto rankDeviceType = [](VkPhysicalDeviceType type) { + switch (type) { + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3; + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 2; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 1; + default: return 0; + } + }; + std::optional selected; + int selectedRank = -1; + for (std::size_t index = 0; index < devices.size(); ++index) { + const auto& candidate = devices[index]; + if (candidate.apiVersion < VK_API_VERSION_1_1) continue; + const std::string_view name(candidate.deviceName); + if (!request.empty() && std::search(name.begin(), name.end(), request.begin(), request.end(), [](char left, char right) { + return std::tolower(static_cast(left)) == std::tolower(static_cast(right)); + }) == name.end()) continue; + const int rank = request.empty() ? rankDeviceType(candidate.deviceType) : 4; + if (rank <= selectedRank) continue; + selected = index; + selectedRank = rank; + } + return selected; +} + +void deviceSelectionTests() { + const auto properties = [](const char* name, VkPhysicalDeviceType type) { + VkPhysicalDeviceProperties result{}; + result.apiVersion = VK_API_VERSION_1_1; + result.deviceType = type; + std::memcpy(result.deviceName, name, std::strlen(name) + 1u); + return result; + }; + std::array devices{ + properties("AMD Radeon Graphics", VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU), + properties("NVIDIA GeForce RTX 5080", VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU), + properties("Virtual GPU", VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU), + properties("CPU", VK_PHYSICAL_DEVICE_TYPE_CPU), + }; + Require(!selectDevice({}, {}).has_value(), "an empty device list selected a device"); + Require(selectDevice(devices, {}) == 1u, "default selection did not prefer the discrete GPU"); + Require(selectDevice(devices, "AMD") == 0u, "the AMD request did not select the integrated GPU"); + Require(selectDevice(devices, "NVIDIA") == 1u, "the NVIDIA request did not select the discrete GPU"); + Require(selectDevice(devices, "aMd rAdEoN") == 0u, "a requested integrated GPU lost to an unrelated discrete GPU"); + Require(selectDevice(devices, "rtx 5080") == 1u, "a partial GPU name did not match without regard to case"); + Require(selectDevice(devices, "Virtual") == 2u, "a requested virtual GPU was not selected"); + Require(selectDevice(devices, "cpu") == 3u, "a requested CPU device was not selected"); + Require(!selectDevice(devices, "missing").has_value(), "an unmatched request selected another GPU"); + Require(!selectDevice(devices, "NVIDIA GeForce RTX 5080 with a longer name").has_value(), "a request longer than the device name matched"); + Require(selectDevice(std::span(devices).subspan(2), {}) == 0u, "default selection did not prefer the virtual GPU to the CPU"); + devices[1].apiVersion = VK_API_VERSION_1_0; + Require(selectDevice(devices, {}) == 0u, "default selection did not prefer the integrated GPU to the virtual GPU"); + Require(!selectDevice(devices, "NVIDIA").has_value(), "a requested device without Vulkan 1.1 was selected"); + devices[1] = devices[0]; + Require(selectDevice(devices, {}) == 0u && selectDevice(devices, "AMD") == 0u, "equally ranked matches did not retain enumeration order"); + for (auto& device : devices) device.apiVersion = VK_API_VERSION_1_0; + Require(!selectDevice(devices, {}).has_value(), "a list without Vulkan 1.1 selected a device"); + std::puts("sampled-cache device selection CPU tests passed"); +} + class Device { public: - explicit Device(bool resourceImages = false) { + explicit Device(bool enableStorageImageMultisample = true) { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); #else @@ -70,7 +141,13 @@ public: Require(count != 0, "no Vulkan device"); std::vector devices(count); Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); - context.physical = devices.front(); + const auto physicalProperties = function("vkGetPhysicalDeviceProperties"); + std::vector deviceProperties(devices.size()); + for (std::size_t index = 0; index < devices.size(); ++index) physicalProperties(devices[index], &deviceProperties[index]); + const char* requestedName = std::getenv("ANYPS5_GPU"); + const auto selectedIndex = selectDevice(deviceProperties, requestedName != nullptr ? requestedName : ""); + Require(selectedIndex.has_value(), requestedName != nullptr && *requestedName != '\0' ? std::string("no Vulkan 1.1 device matching ANYPS5_GPU=\"") + requestedName + "\"" : "no Vulkan 1.1 device"); + context.physical = devices[*selectedIndex]; const auto extensions = function("vkEnumerateDeviceExtensionProperties"); Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); std::vector available(count); @@ -95,13 +172,12 @@ public: queue.queueFamilyIndex = family; queue.queueCount = 1; queue.pQueuePriorities = &priority; + VkPhysicalDeviceFeatures supported{}; + function("vkGetPhysicalDeviceFeatures")(context.physical, &supported); VkPhysicalDeviceFeatures enabled{}; enabled.shaderInt64 = VK_TRUE; - if (resourceImages) { - VkPhysicalDeviceFeatures supported{}; - function("vkGetPhysicalDeviceFeatures")(context.physical, &supported); - enabled.shaderStorageImageMultisample = supported.shaderStorageImageMultisample; - } + enabled.shaderStorageImageMultisample = enableStorageImageMultisample && supported.shaderStorageImageMultisample; + context.storageImageMultisample = enabled.shaderStorageImageMultisample == VK_TRUE; address.pNext = &bytes; std::vector extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; if (hasExtension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME)) { @@ -122,6 +198,7 @@ public: function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); VkPhysicalDeviceProperties properties{}; function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + std::printf("sampled-cache fixture Vulkan device: %s vendor=0x%04x device=0x%04x\n", properties.deviceName, properties.vendorID, properties.deviceID); context.limits = properties.limits; context.bufferDeviceAddress = true; context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); @@ -129,6 +206,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 (...) { @@ -140,6 +218,14 @@ public: ~Device() { release(); } const Context& GetContext() const { return context; } + void VerifySelection() const { + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + const char* request = std::getenv("ANYPS5_GPU"); + Require(selectDevice(std::span(&properties, 1), request != nullptr ? request : "").has_value(), "the sampled-cache fixture physical device does not match ANYPS5_GPU"); + std::puts("sampled-cache fixture physical device selection verified"); + } + private: template TFunction function(const char* name) const { @@ -193,6 +279,53 @@ private: void* memory = nullptr; }; +class WatchedSurface { +public: + explicit WatchedSurface(const Context& context) : context(context) { +#ifdef _WIN32 + memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, 65536)); + if (memory != nullptr) GuestArena::GuestArenaCommit_nid_postfix(memory, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, SurfaceBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + SurfaceBytes != begin + SurfaceBytes + alignment) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + alignment - aligned); + memory = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory, SurfaceBytes); + } +#endif + Require(memory != nullptr, "cannot allocate the watched surface"); + Require(AgcDriver::GuestMemory::Watched(Address(), SurfaceBytes), "the watched surface is not write-watched"); + std::memset(memory, 0x5a, SurfaceBytes); + GuestAllocations::Mutation().Add(memory, SurfaceBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(Address(), SurfaceBytes); + } + + ~WatchedSurface() { + ClearCachedTextures(context.device); + GuestAllocations::Mutation().Remove(memory); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, SurfaceBytes); +#else + munmap(memory, SurfaceBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, SurfaceBytes); +#endif + } + + WatchedSurface(const WatchedSurface&) = delete; + WatchedSurface& operator=(const WatchedSurface&) = delete; + + std::uint64_t Address() const { return reinterpret_cast(memory); } + +private: + const Context& context; + std::uint8_t* memory = nullptr; +}; + std::array Descriptor(std::uint64_t address, std::uint32_t side = Side) { return { static_cast(address >> 8u), @@ -236,6 +369,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 +452,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 +483,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 +509,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 +517,115 @@ 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"); +} + +ShaderRecompiler::RecompileResult SampledProgram(std::uint64_t address) { + ShaderRecompiler::DescriptorBinding binding{}; + binding.kind = ShaderRecompiler::DescriptorKind::SampledImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.descriptorSet = 0; + binding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding + PlaceholderDimensionsPerClass + 2u; + binding.count = 1; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageWritten.assign(1, false); + binding.imageSamplers.assign(1, 0u); + const auto words = Descriptor(address); + binding.guestDescriptor.assign(words.begin(), words.end()); + ShaderRecompiler::RecompileResult program; + program.bindings.push_back(std::move(binding)); + return program; +} + +void replacedEntryTests(Context context) { + PlaceholderImages placeholders(context); + context.placeholderImages = &placeholders; + TextureCache textures(context); + context.textureCache = &textures; + WatchedSurface surface(context); + ClearCachedTextures(context.device); + const auto program = SampledProgram(surface.Address()); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + auto first = std::make_shared(context, shader); + Require(first->Reusable(), "the sampled snapshot's descriptor set is not eligible for resource caching"); + Require(first->SampledTextures().size() == 1, "the sampled descriptor set binds no texture"); + const auto bound = first->SampledTextures().front(); + Require(!bound->ViewsStorageImage(), "the replaced-entry fixture is not a snapshot"); + Require(Sampled(context, surface.Address()) == bound, "the descriptor set does not bind the cache's texture"); + ShaderResources::ProofReport report{}; + Require(first->Revalidate(shader), "a template over an unchanged, still cached texture did not prove current"); + Require(first->Revalidate(shader, &report), "a template over an unchanged, still cached texture did not prove current twice"); + Require(report.path == ShaderResources::ProofPath::Fast, "the fixture's template did not take the fast proof"); + Require(first->SampledTextures().front() == bound, "the proof changed the bound texture"); + ClearCachedTextures(context.device); + Require(TextureCacheUsage().sampledEntries == 0, "clearing the sampled cache left entries"); + Require(!first->Revalidate(shader, &report), "a template binding a texture whose cache entry left still proved current"); + const auto replacement = Sampled(context, surface.Address()); + Require(replacement != bound, "the lookup after the clear handed back the old texture"); + Require(!first->Revalidate(shader), "a template binding the replaced texture proved current against the new entry"); + auto rebuilt = std::make_shared(context, shader); + Require(rebuilt->SampledTextures().front() == replacement, "the rebuilt descriptor set does not bind the cache's new texture"); + Require(rebuilt->Revalidate(shader), "the rebuilt template did not prove current over its cached texture"); + first.reset(); + rebuilt.reset(); + ClearCachedTextures(context.device); + std::puts("template over a replaced sampled cache entry passed"); +} + +bool lookupChangedTests(Context context) { + PlaceholderImages placeholders(context); + context.placeholderImages = &placeholders; + TextureCache textures(context); + context.textureCache = &textures; + if (context.hostImportAlignment == 0) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "the lookup-changed test requires host imports"); + std::puts("skipped, host imports are unavailable"); + return false; + } + WatchedSurface surface(context); + ClearCachedTextures(context.device); + const auto program = SampledProgram(surface.Address()); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + auto first = std::make_shared(context, shader); + Require(first->Reusable() && first->SampledTextures().size() == 1, "the lookup-changed template is not reusable"); + const auto bound = first->SampledTextures().front(); + Require(!bound->ViewsStorageImage(), "the lookup-changed fixture is not a snapshot before the import"); + ShaderResources::ProofReport report{}; + Require(first->Revalidate(shader), "the lookup-changed template did not prove current"); + Require(first->Revalidate(shader, &report) && report.path == ShaderResources::ProofPath::Fast, "the lookup-changed template did not take the fast proof"); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + Require(first->Revalidate(shader, &report), "the audit refused an unchanged template"); + Require(report.path == ShaderResources::ProofPath::Full, "a proof past the audit interval did not take the full walk"); + Require(first->SampledTextures().front() == bound, "the audit changed the bound texture"); + Require(first->Revalidate(shader, &report) && report.path == ShaderResources::ProofPath::Fast, "the proof right after an audit did not take the fast proof"); + if (HostImportFor(context, surface.Address(), SurfaceBytes) == nullptr) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "the host import of the watched surface was refused"); + std::puts("skipped, host imports are unavailable"); + return false; + } + Require(!bound->Released(), "the snapshot left the cache without a lookup"); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + Require(!first->Revalidate(shader, &report), "a template binding a snapshot of a surface its lookup now views through a storage image still proved current past the audit interval"); + const auto view = Sampled(context, surface.Address()); + Require(view != bound && view->ViewsStorageImage(), "the lookup of the imported surface does not view its storage image"); + auto rebuilt = std::make_shared(context, shader); + Require(rebuilt->SampledTextures().front() == view, "the rebuilt template does not bind the storage view"); + first.reset(); + rebuilt.reset(); + ClearCachedTextures(context.device); + std::puts("template over a changed sampled lookup passed"); + return true; } void reportedBudgetTests(const Context& context) { @@ -424,6 +697,48 @@ void heldTests(const Context& context) { Require(TextureCacheUsage().sampledEntries == capacity && TextureCacheUsage().sampledBytes <= Budget, "released textures were not evicted down to the budget"); } +void releasedTests(Context context) { + PlaceholderImages placeholders(context); + context.placeholderImages = &placeholders; + TextureCache textures(context); + context.textureCache = &textures; + ClearCachedTextures(context.device); + auto& cache = SharedResourceCache(); + cache.Clear(); + try { + Block block(context); + auto pinned = ImageResources(context, block, 0, 1, true); + Require(pinned->SampledTextures().size() == 1 && !pinned->SampledTextures().front()->ViewsStorageImage(), "the pinned descriptor set does not hold a sampled snapshot"); + const std::weak_ptr texture = pinned->SampledTextures().front(); + const std::weak_ptr pinnedSet = pinned; + cache.Insert({1}, pinned); + Require(cache.Find({1}) == pinned, "the resource cache rejected a descriptor set whose texture is cached"); + pinned.reset(); + ClearCachedTextures(context.device); + Require(!texture.expired() && texture.lock()->Released(), "the sampled texture cache dropped a texture the resource cache holds without marking it released"); + auto live = ImageResources(context, block, 1, 1, true); + std::vector> displaced; + cache.Insert({2}, live, &displaced); + Require(cache.Find({1}) == nullptr && cache.Find({2}) == live && cache.Size() == 1, "the resource cache kept an entry holding a released texture"); + Require(displaced.size() == 1 && displaced.front() == pinnedSet.lock(), "the entry holding a released texture was not handed back to the caller"); + displaced.clear(); + Require(pinnedSet.expired() && texture.expired(), "the resource cache kept a released texture alive"); + auto stale = ImageResources(context, block, 2, 1, true); + ClearCachedTextures(context.device); + Require(stale->SampledTextures().front()->Released(), "a held texture the sampled cache dropped is not marked released"); + cache.Insert({3}, stale, &displaced); + Require(cache.Find({3}) == nullptr, "a descriptor set holding a released texture entered the resource cache"); + Require(cache.Size() == 0 && displaced.size() == 1 && displaced.front() == live, "the live entry whose texture was released stayed in the resource cache"); + displaced.clear(); + } catch (...) { + cache.Clear(); + throw; + } + cache.Clear(); + ClearCachedTextures(context.device); + std::puts("resource cache released texture tests passed"); +} + void rescanTests(const Context& context) { constexpr std::uint32_t SmallSide = 128; constexpr std::size_t SmallBytes = std::size_t{SmallSide} * SmallSide * 4u; @@ -477,10 +792,10 @@ void frameTests(const Context& context) { hits += again == made[surface].lock(); } Require(hits == capacity, "the next frame over the same " + std::to_string(frame) + " textures found " + std::to_string(hits) + " of them cached, not the " + std::to_string(capacity) + " the cache kept"); - auto admitted = Sampled(context, block.Surface(frame - 1u)); - Require(Sampled(context, block.Surface(frame - 1u)) == admitted, "a texture used a second time in the frame was not cached"); - Require(TextureCacheUsage().sampledEntries == capacity && TextureCacheUsage().sampledBytes <= Budget, "the texture admitted on its second use left the cache over its budget"); - admitted.reset(); + auto refused = Sampled(context, block.Surface(frame - 1u)); + Require(Sampled(context, block.Surface(frame - 1u)) != refused, "a texture used a second time in the frame was cached in place of one of the frame's textures"); + Require(TextureCacheUsage().sampledEntries == capacity && TextureCacheUsage().sampledBytes <= Budget, "the texture used a second time left the cache over its budget"); + refused.reset(); Recorder::CountPresent(); std::vector> moved(frame); for (std::size_t surface = 0; surface < frame; ++surface) moved[surface] = Sampled(context, block.Surface(frame + surface)); @@ -492,6 +807,71 @@ void frameTests(const Context& context) { Require(TextureCacheUsage().sampledEntries == capacity, "a second frame over other textures left " + std::to_string(TextureCacheUsage().sampledEntries) + " entries cached"); } +void scanTests(const Context& context) { + Block block(context); + ClearCachedTextures(context.device); + Recorder::CountPresent(); + const auto bytes = Sampled(context, block.Surface(0))->AllocationBytes(); + const auto capacity = static_cast(Budget / bytes); + const auto working = 2u * capacity; + Require(capacity >= 2u && working <= Surfaces && TextureCacheUsage().sampledEntries == 1u, "a snapshot of the 256 KiB surface holds " + std::to_string(bytes) + " bytes, which the test's 1 MiB budget does not fit as planned"); + constexpr std::size_t Presents = 6; + constexpr std::size_t Uses = 2; + std::vector> last(working); + std::size_t hits = 0; + std::size_t lookups = 0; + for (std::size_t present = 0; present < Presents; ++present) { + Recorder::CountPresent(); + for (std::size_t surface = 0; surface < working; ++surface) { + for (std::size_t use = 0; use < Uses; ++use) { + const auto texture = Sampled(context, block.Surface(surface)); + if (present != 0u) { + hits += texture == last[surface].lock(); + ++lookups; + } + last[surface] = texture; + } + } + } + Require(TextureCacheUsage().sampledBytes <= Budget, "the sampled texture cache holds more than its budget after a scan"); + const auto wanted = (Presents - 1u) * Uses * capacity / 2u; + Require(hits >= wanted, "a cyclic scan using each of " + std::to_string(working) + " textures twice in a row, twice the " + std::to_string(capacity) + " the budget holds, found " + std::to_string(hits) + " of " + std::to_string(lookups) + " lookups cached, fewer than " + std::to_string(wanted)); +} + +void shrinkTests(const Context& context) { + Block block(context); + ClearCachedTextures(context.device); + Recorder::CountPresent(); + OverrideCacheBudgetForTest(2u * Budget); + const auto bytes = Sampled(context, block.Surface(0))->AllocationBytes(); + const auto capacity = static_cast((2u * Budget) / bytes); + Require(capacity >= 4u && capacity + 1u <= Surfaces, "a snapshot of the 256 KiB surface holds " + std::to_string(bytes) + " bytes, which the test's 2 MiB budget does not fit as planned"); + for (std::size_t surface = 1; surface < capacity; ++surface) Sampled(context, block.Surface(surface)); + Require(TextureCacheUsage().sampledEntries == capacity, "the 2 MiB budget did not keep " + std::to_string(capacity) + " snapshots"); + Recorder::CountPresent(); + OverrideCacheBudgetForTest(Budget); + for (std::size_t surface = 0; surface < capacity; ++surface) Sampled(context, block.Surface(surface)); + Sampled(context, block.Surface(capacity)); + Require(TextureCacheUsage().sampledBytes <= Ceiling, "after the budget fell to 1 MiB, one insert left " + std::to_string(TextureCacheUsage().sampledBytes) + " bytes cached with every entry in use this frame, past a quarter over the budget"); + OverrideCacheBudgetForTest(0); +} + +void secondUseTests(const Context& context) { + Block block(context); + ClearCachedTextures(context.device); + Recorder::CountPresent(); + const auto bytes = Sampled(context, block.Surface(0))->AllocationBytes(); + const auto capacity = static_cast(Budget / bytes); + Require(capacity >= 2u && capacity < Surfaces && TextureCacheUsage().sampledEntries == 1u, "a snapshot of the 256 KiB surface holds " + std::to_string(bytes) + " bytes, which the test's 1 MiB budget does not fit as planned"); + for (std::size_t surface = 1; surface < capacity; ++surface) Sampled(context, block.Surface(surface)); + Recorder::CountPresent(); + auto first = Sampled(context, block.Surface(capacity)); + Require(TextureCacheUsage().sampledEntries == capacity && first.use_count() == 1, "the first use in a frame of a texture the cache does not hold was cached in place of one the last frame used"); + const auto second = Sampled(context, block.Surface(capacity)); + Require(Sampled(context, block.Surface(capacity)) == second, "a texture used a second time in a frame whose least recently used entry is from the last frame was not cached"); + Require(TextureCacheUsage().sampledEntries == capacity && TextureCacheUsage().sampledBytes <= Budget, "the texture admitted on its second use left the cache over its budget"); +} + void earlyMissTests(const Context& context) { Block block(context); ClearCachedTextures(context.device); @@ -580,6 +960,21 @@ std::uint64_t ReadBudget(const Context& context) { return SampledTextureCacheBudget(context); } +constexpr std::uint64_t PressedUsage = ReportedBudget - 9ull * (1ull << 30u); + +void VKAPI_PTR ReportPressure(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { + auto* reported = static_cast(properties->pNext); + for (std::uint32_t heap = 0; heap < VK_MAX_MEMORY_HEAPS; ++heap) { + reported->heapBudget[heap] = ReportedBudget; + reported->heapUsage[heap] = PressedUsage; + } +} + +std::uint64_t ReadStorageBudget(const Context& context) { + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + return StorageTextureCacheBudget(context); +} + void memoryTests(Context context) { context.memoryProperties2 = &ReportBudget; const auto before = SampledTextureMemory(); @@ -597,27 +992,68 @@ 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 + TextureCacheUsage().storageBytes; + Require(pressed == expected, "55 GiB in use of a 64 GiB budget leaves the storage image cache " + std::to_string(pressed >> 20u) + " MiB, not the " + std::to_string(expected >> 20u) + " MiB left after the use outside the texture caches, the sampled textures and 8 GiB of headroom"); + std::printf("reported heap budget %llu MiB with %llu MiB in use: storage image cache budget %llu MiB\n", static_cast(ReportedBudget >> 20u), static_cast(PressedUsage >> 20u), static_cast(pressed >> 20u)); +} + } int main(int argc, char** argv) { try { const bool memory = argc > 1 && std::string(argv[1]) == "memory"; const std::string mode = argc > 1 ? argv[1] : ""; + if (mode == "device-selection-cpu") { + deviceSelectionTests(); + return 0; + } const bool resources = mode == "resource-admission" || mode == "resource-oversized" || mode == "resource-clear"; + const bool disableStorageImageMultisample = mode == "without-storage-image-multisample"; #ifdef _WIN32 if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", "1"); + if (std::getenv("APS5_TEMPLATE_AUDIT_MS") == nullptr) _putenv_s("APS5_TEMPLATE_AUDIT_MS", "250"); #else if (!memory) setenv("APS5_TEXTURE_CACHE_MIB", "1", 1); + setenv("APS5_TEMPLATE_AUDIT_MS", "250", 0); #endif std::unique_ptr device; try { - device = std::make_unique(resources); + device = std::make_unique(!disableStorageImageMultisample); } catch (const std::exception& error) { - if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + const char* requestedName = std::getenv("ANYPS5_GPU"); + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr || (requestedName != nullptr && *requestedName != '\0')) throw; std::printf("skipped, no usable Vulkan device: %s\n", error.what()); return VulkanTestSkipped; } + if (mode == "device-selection") { + device->VerifySelection(); + return 0; + } auto context = device->GetContext(); + if (disableStorageImageMultisample) { + PlaceholderImages placeholders(context); + for (std::uint32_t heap = ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding; + heap < ShaderRecompiler::RuntimeAbi::FirstImageBinding + ShaderRecompiler::RuntimeAbi::ImageBindingCount; ++heap) { + const auto shape = PlaceholderImageShapeOf(heap); + if (shape.samples == VK_SAMPLE_COUNT_1_BIT) continue; + bool refused = false; + try { + placeholders.View(heap); + } catch (const std::runtime_error&) { + refused = true; + } + Require(refused, "a device without storage-image MSAA returned a placeholder for typed multisample storage heap " + std::to_string(heap)); + } + } TextureDetiler detiler(context); context.detiler = &detiler; if (resources) { @@ -626,9 +1062,22 @@ int main(int argc, char** argv) { std::puts("resource cache tests passed"); return 0; } + if (mode == "resource-replaced") { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + replacedEntryTests(context); + std::puts("resource cache replaced entry tests passed"); + return 0; + } + if (mode == "resource-lookup-changed") { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + if (!lookupChangedTests(context)) return VulkanTestSkipped; + std::puts("resource cache lookup-changed tests passed"); + return 0; + } if (memory) { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); memoryTests(context); + storageMemoryTests(context); ClearCachedTextures(context.device); std::puts("sampled texture memory budget tests passed"); return 0; @@ -638,10 +1087,14 @@ int main(int argc, char** argv) { reportedBudgetTests(context); snapshotTests(context); heldTests(context); + releasedTests(context); rescanTests(context); viewEvictionTests(context); viewTests(context); + scanTests(context); frameTests(context); + shrinkTests(context); + secondUseTests(context); earlyMissTests(context); collidingKeyTests(context); ClearCachedTextures(context.device); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSetpc.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSetpc.cpp index 669e125986..41069b2013 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSetpc.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSetpc.cpp @@ -274,6 +274,22 @@ void Run(AgcDriver::VulkanDevice& device, std::span code, s device.WaitIdle(); } +std::vector Scheduled(std::span original) { + std::vector code(original.begin(), original.end()); + code.insert(code.begin() + 25, 0x7e3c0280u); + code.insert(code.begin() + 24, 0x7e3e0280u); + const auto relocated = [](std::int32_t word) { return word + (word >= 24 ? 1 : 0) + (word >= 25 ? 1 : 0); }; + for (std::int32_t word = 0; word < static_cast(original.size()); ++word) { + const auto instruction = original[static_cast(word)]; + const auto opcode = instruction & 0xffff0000u; + if (opcode != 0xbf820000u && opcode != 0xbf850000u) continue; + const auto target = word + 1 + static_cast(instruction & 0xffffu); + const auto offset = relocated(target) - relocated(word) - 1; + code[static_cast(relocated(word))] = opcode | (static_cast(offset) & 0xffffu); + } + return code; +} + void CheckReversedSubtractRefused(AgcDriver::VulkanDevice& device) { std::string refusal; try { @@ -306,6 +322,14 @@ int main() { Check(Expected64); Run(*device, Wave64Code, 64, device->ComputeTarget(32)); Check(Expected64); + const auto scheduled32 = Scheduled(Wave32Code); + const auto scheduled64 = Scheduled(Wave64Code); + Run(*device, scheduled32, 32, device->ComputeTarget(32)); + Check(Expected32); + Run(*device, scheduled64, 64, device->Target()); + Check(Expected64); + Run(*device, scheduled64, 64, device->ComputeTarget(32)); + Check(Expected64); CheckReversedSubtractRefused(*device); std::puts("setpc tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp new file mode 100644 index 0000000000..c6dd1e4ca1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp @@ -0,0 +1,234 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::DriverDetail::MatchSgprPatternFill; +using AgcDriver::Graphics::DccKeys; +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array Code{ + 0xbfa00001u, 0xd7460004u, 0x04010c09u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0x7da80808u, + 0xbf88000du, 0x360a0883u, 0xbe84047eu, 0xbe86047eu, 0x7e100505u, 0x7da40a08u, 0xbefc0308u, 0x7e0c8700u, + 0x8a867e06u, 0xbefe0406u, 0xbf85fff9u, 0xbefe0404u, 0xe0102000u, 0x80000604u, 0xbf810000u}; +constexpr std::uint32_t Dwords = 1024; +constexpr std::size_t KeyBytes = Dwords * 4; +constexpr std::uint64_t SurfaceBytes = KeyBytes * 256; +alignas(256) std::array Keys{}; + +using Pattern = std::array; + +constexpr Pattern Uniform(std::uint8_t key) { + const std::uint32_t word = key * 0x01010101u; + return {word, word, word, word}; +} + +std::uint64_t KeysAddress(std::size_t offset = 0) { + return static_cast(reinterpret_cast(Keys.data())) + offset; +} + +std::vector UserData(std::uint32_t records, std::uint32_t count, const Pattern& pattern, std::uint32_t format = 0x00014004u) { + const auto address = KeysAddress(); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), records, format, pattern[0], pattern[1], pattern[2], pattern[3], count}; +} + +void MatchTests() { + const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1}; + const std::array packet{0xc0031500u, 16, 1, 1, 0x41u}; + const Pattern pattern{0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; + const auto data = UserData(Dwords, Dwords, pattern); + const auto fill = MatchSgprPatternFill(packet, Code, data, compute); + Require(fill && fill->base == KeysAddress() && fill->bytes == KeyBytes && fill->pattern == pattern, "the SGPR pattern-fill kernel was not recognized"); + for (std::size_t word = 0; word < Code.size(); ++word) { + auto altered = Code; + altered[word] ^= 1u; + Require(!MatchSgprPatternFill(packet, altered, data, compute), "a different shader matched the SGPR pattern-fill kernel"); + } + Require(!MatchSgprPatternFill(packet, std::span(Code).first(22), data, compute), "a truncated shader matched"); + const auto bounded = MatchSgprPatternFill(packet, Code, UserData(Dwords, 100, pattern), compute); + Require(bounded && bounded->bytes == 400, "the fill exceeded the kernel's count"); + const auto clamped = MatchSgprPatternFill(packet, Code, UserData(64, Dwords, pattern), compute); + Require(clamped && clamped->bytes == 256, "the fill exceeded the descriptor's records"); + auto fewer = packet; + fewer[1] = 2; + const auto launched = MatchSgprPatternFill(fewer, Code, data, compute); + Require(launched && launched->bytes == 512, "the fill exceeded the launched threads"); + Require(!MatchSgprPatternFill(packet, Code, UserData(Dwords, 0, pattern), compute), "an empty fill matched"); + for (const std::uint32_t format : {0x00015004u, 0x00016004u, 0x10014004u}) { + Require(MatchSgprPatternFill(packet, Code, UserData(Dwords, Dwords, pattern, format), compute).has_value(), "a 32-bit output format with an index bound was refused"); + } + for (const std::uint32_t format : {0x00005004u, 0x0000d004u, 0x0004b004u, 0x00814004u, 0x20014004u, 0x30014004u, 0x40014004u}) { + Require(!MatchSgprPatternFill(packet, Code, UserData(Dwords, Dwords, pattern, format), compute), "a descriptor whose elements are not one bounded 32-bit value matched"); + } + auto wide = data; + wide[1] = (wide[1] & 0xffffu) | (8u << 16u); + Require(!MatchSgprPatternFill(packet, Code, wide, compute), "a stride other than one dword matched"); + auto swizzled = data; + swizzled[1] |= 1u << 31u; + Require(!MatchSgprPatternFill(packet, Code, swizzled, compute), "a swizzled descriptor matched"); + auto extra = data; + extra.push_back(0); + Require(!MatchSgprPatternFill(packet, Code, extra, compute), "a different user SGPR count matched (the group id moves)"); + auto partial = packet; + partial[4] |= 0x20u; + Require(!MatchSgprPatternFill(partial, Code, data, compute), "a partial dispatch matched"); + auto rows = packet; + rows[2] = 2; + Require(!MatchSgprPatternFill(rows, Code, data, compute), "a dispatch with more than one group row matched"); + auto noGroup = compute; + noGroup.groupIdEnable[0] = false; + Require(!MatchSgprPatternFill(packet, Code, data, noGroup), "a dispatch without the group id matched"); +} + +alignas(8) std::uint32_t Finished = 0; +std::uint32_t Submitted = 0; +constexpr std::uint32_t MaxResubmits = 16; + +void Submit(const std::vector& data, std::uint32_t groups) { + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{64, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(Code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (1u << 7u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u}); + const auto finished = reinterpret_cast(&Finished); + words.insert(words.end(), {0xc0033700u, 0x00100200u, static_cast(finished), static_cast(finished >> 32u), ++Submitted}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); +} + +struct KeyRead { + DccKeys keys; + bool waited; + std::uint32_t resubmits; +}; + +std::uint64_t lastNote = 0; + +void Settle() { + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(KeysAddress(), KeyBytes); +} + +KeyRead SubmitAndRead(const std::vector& data, std::uint32_t groups, std::uint64_t surfaceBytes = SurfaceBytes, bool retireFirst = false) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + Submit(data, groups); + if (retireFirst) Settle(); + std::uint32_t resubmits = 0; + for (;;) { + bool retired = false; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + if (const auto noted = recorder != nullptr ? recorder->NewestWriteNote(KeysAddress(), KeyBytes) : 0; noted > lastNote) { + lastNote = noted; + const auto keys = AgcDriver::Graphics::CurrentDccKeys(KeysAddress(), surfaceBytes); + return {keys, !recorder->PendingWriteOverlaps(KeysAddress(), KeyBytes), resubmits}; + } + retired = std::atomic_ref(Finished).load() == Submitted; + } + if (retired) { + Require(resubmits < MaxResubmits, "the dispatch completed " + std::to_string(resubmits + 1) + " times without a pending write over the keys"); + ++resubmits; + Submit(data, groups); + } + Require(std::chrono::steady_clock::now() < deadline, "the submitted dispatch never became a pending write over the keys"); + std::this_thread::yield(); + } +} + +void RequireStored(std::uint32_t count, const Pattern& pattern, std::uint32_t before, const std::string& what) { + Settle(); + for (std::uint32_t i = 0; i < Dwords; ++i) { + Require(Keys[i] == (i < count ? pattern[i % 4] : before), what + ": dword " + std::to_string(i) + " holds " + std::to_string(Keys[i])); + } +} + +std::string Describe(const char* what, const KeyRead& read) { + return std::string(what) + ": the keys read " + AgcDriver::Graphics::DccKeysName(read.keys) + " and the read " + (read.waited ? "waited for the GPU" : "did not wait"); +} + +void ExecuteTests() { + const std::array, 6> codes{{{0x00, DccKeys::Clear0000}, {0x40, DccKeys::Clear0001}, {0x80, DccKeys::Clear1110}, {0xc0, DccKeys::Clear1111}, {0x20, DccKeys::ClearRegister}, {0xff, DccKeys::Uncompressed}}}; + for (const auto& [key, keys] : codes) { + Settle(); + const auto read = SubmitAndRead(UserData(Dwords, Dwords, Uniform(key)), Dwords / 64); + Require(read.keys == keys && !read.waited, Describe("a pending key code fill by the kernel", read)); + RequireStored(Dwords, Uniform(key), 0, "the kernel's key code fill"); + } + + Settle(); + const auto retired = SubmitAndRead(UserData(Dwords, Dwords, Uniform(0x40)), Dwords / 64, SurfaceBytes, true); + Require(retired.resubmits > 0, "a fill that retired before the read was not submitted again"); + Require(retired.keys == DccKeys::Clear0001 && !retired.waited, Describe("a pending 0001 fill submitted again after it retired before the read", retired)); + RequireStored(Dwords, Uniform(0x40), 0, "the 0001 fill submitted again"); + + const auto other = SubmitAndRead(UserData(Dwords, Dwords, Uniform(0x5a)), Dwords / 64); + Require(other.keys == DccKeys::Mixed && other.waited, Describe("a pending fill of a byte that is not a key code", other)); + RequireStored(Dwords, Uniform(0x5a), 0, "the kernel's fill of another byte"); + + const Pattern several{0x40404040u, 0x40404040u, 0x40404040u, 0x80808080u}; + const auto mixed = SubmitAndRead(UserData(Dwords, Dwords, several), Dwords / 64); + Require(mixed.keys == DccKeys::Mixed && mixed.waited, Describe("a pending fill of several key codes", mixed)); + RequireStored(Dwords, several, 0, "the kernel's fill of several key codes"); + + const auto first = SubmitAndRead(UserData(Dwords, Dwords, Uniform(0x40)), Dwords / 64); + Require(first.keys == DccKeys::Clear0001 && !first.waited, Describe("a pending 0001 fill by the kernel", first)); + const auto later = SubmitAndRead(UserData(Dwords, Dwords / 2, Uniform(0x5a)), Dwords / 64); + Require(later.keys == DccKeys::Mixed && later.waited, Describe("a later writer over half of a pending 0001 fill", later)); + Settle(); + for (std::uint32_t i = 0; i < Dwords; ++i) Require(Keys[i] == (i < Dwords / 2 ? 0x5a5a5a5au : 0x40404040u), "the later writer over the 0001 fill: dword " + std::to_string(i) + " holds " + std::to_string(Keys[i])); + + const auto half = SubmitAndRead(UserData(Dwords, Dwords / 2, Uniform(0xc0)), Dwords / 64); + Require(half.keys == DccKeys::Mixed, Describe("a pending 1111 fill of half the keys over 0001 keys", half)); + Settle(); + const auto inside = SubmitAndRead(UserData(Dwords, Dwords / 2, Uniform(0xc0)), Dwords / 64, SurfaceBytes / 2); + Require(inside.keys == DccKeys::Clear1111, Describe("the half of the keys a pending 1111 fill covers", inside)); + Settle(); + for (std::uint32_t i = 0; i < Dwords; ++i) Require(Keys[i] == (i < Dwords / 2 ? 0xc0c0c0c0u : 0x40404040u), "the 1111 fill of half the keys: dword " + std::to_string(i) + " holds " + std::to_string(Keys[i])); +} + +} + +int main() { + try { + MatchTests(); + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ExecuteTests(); + AgcDriverShutdown_nid_postfix(); + std::puts("SGPR pattern fill key tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp new file mode 100644 index 0000000000..82832ba244 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp @@ -0,0 +1,220 @@ +#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 ClosingDropsQueuedPrebuilds() { + using namespace AgcDriver::DriverDetail; + std::promise started; + auto running = started.get_future(); + std::promise gate; + auto opened = gate.get_future().share(); + bool queuedRan = false; + SubmitLibraryPrebuild([&] { started.set_value(); opened.wait(); }); + for (int index = 0; index < 3; ++index) SubmitLibraryPrebuild([&] { queuedRan = true; }); + running.wait(); + auto entering = std::make_shared>(); + auto entered = entering->get_future(); + auto awaited = std::make_shared>(); + auto woken = awaited->get_future(); + std::thread([entering, awaited] { + entering->set_value(); + AwaitLibraryPrebuilds(); + awaited->set_value(); + }).detach(); + entered.wait(); + const bool waiting = woken.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout; + 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(); + const bool returned = closing.wait_for(std::chrono::seconds(10)) == std::future_status::ready; + const bool wokeUp = woken.wait_for(std::chrono::seconds(10)) == std::future_status::ready; + closing.get(); + Require(waiting, "a waiter for the library prebuilds returned while one was still running"); + Require(waited, "closing returned while a library prebuild was still running"); + Require(returned, "closing did not return once the running library prebuild ended"); + Require(wokeUp, "a waiter for the library prebuilds did not return after closing dropped the queued ones"); + Require(!queuedRan, "closing ran the queued library prebuilds"); + Require(RegistrationPreparationThreads() == 0, "closing the library prebuilds left their thread running"); +} + +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 == "--queued-prebuilds") ClosingDropsQueuedPrebuilds(); + 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..64835ea370 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 (...) { @@ -324,7 +325,7 @@ void Run(const Context& context, const CopyPipeline& pipeline, bool writable, co std::memset(readback.Bytes().data(), 0xcc, BufferBytes); VkBuffer primed = VK_NULL_HANDLE; { - Buffer earlier(context, BufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer earlier(context, BufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); primed = earlier.Handle(); std::memset(earlier.Bytes().data(), 0xa5, BufferBytes); } 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/SparseIndirectIndices.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseIndirectIndices.cpp new file mode 100644 index 0000000000..809321d1b5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseIndirectIndices.cpp @@ -0,0 +1,248 @@ +#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 +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#include +#endif + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::byte Kept{0x40}; +constexpr std::uint32_t DeclaredIndices = 0x10000000u; +constexpr std::size_t ReservationBytes = std::size_t{DeclaredIndices} * 4; +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array WriterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000202u, 0x7e020203u, 0x7e040204u, 0xe07c0000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array Reference{}; +alignas(256) std::array Actual{}; +alignas(256) std::array Records{}; +alignas(256) std::array RecordDescriptor{}; +alignas(256) std::array Count{}; +alignas(256) std::array CountDescriptor{}; + +class SparseIndices { +public: + SparseIndices() { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + page = info.dwPageSize; + memory = static_cast(VirtualAlloc(nullptr, ReservationBytes, MEM_RESERVE, PAGE_NOACCESS)); + Require(memory != nullptr, "index reservation failed"); + Require(VirtualAlloc(memory, page, MEM_COMMIT, PAGE_READWRITE) == memory, "first index page commit failed"); + Require(VirtualAlloc(memory + 2 * page, page, MEM_COMMIT, PAGE_READWRITE) == memory + 2 * page, "last index page commit failed"); +#else + const auto hostPage = sysconf(_SC_PAGESIZE); + Require(hostPage > 0, "host page size is unavailable"); + page = static_cast(hostPage); + auto* allocation = mmap(nullptr, ReservationBytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(allocation != MAP_FAILED, "index reservation failed"); + memory = static_cast(allocation); + Require(mprotect(memory, page, PROT_READ | PROT_WRITE) == 0, "first index page commit failed"); + Require(mprotect(memory + 2 * page, page, PROT_READ | PROT_WRITE) == 0, "last index page commit failed"); +#endif + } + + ~SparseIndices() { +#ifdef _WIN32 + VirtualFree(memory, 0, MEM_RELEASE); +#else + munmap(memory, ReservationBytes); +#endif + } + + SparseIndices(const SparseIndices&) = delete; + SparseIndices& operator=(const SparseIndices&) = delete; + + std::byte* Base() const { return memory + page - 16; } + std::uint32_t AfterGap(std::uint32_t size) const { return static_cast((page + 16) / size); } + + void Fill(std::uint32_t size) { + const std::array small{0, 1, 2}; + const std::array large{0, 1, 2}; + const void* source = size == 2 ? static_cast(small.data()) : static_cast(large.data()); + std::memcpy(Base(), source, 3 * size); + std::memcpy(memory + 2 * page, source, 3 * size); + } + +private: + std::byte* memory = nullptr; + std::size_t page = 0; +}; + +std::uint32_t Low(const void* pointer) { return static_cast(reinterpret_cast(pointer)); } +std::uint32_t High(const void* pointer) { return static_cast(reinterpret_cast(pointer) >> 32u); } + +void Append(std::vector& words, std::uint32_t opcode, std::initializer_list payload) { + words.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + words.insert(words.end(), payload); +} + +void WriteArguments(std::vector& words, const std::uint32_t* descriptor, std::array values) { + const auto address = reinterpret_cast(WriterCode.data()); + Append(words, 0x76, {0x20c, static_cast(address >> 8u)}); + Append(words, 0x76, {0x20d, static_cast(address >> 40u)}); + for (const auto offset : {0x207u, 0x208u, 0x209u}) Append(words, 0x76, {offset, 1}); + Append(words, 0x76, {0x212, 0}); + Append(words, 0x76, {0x213, 10}); + Append(words, 0x76, {0x240, Low(descriptor), High(descriptor), values[0], values[1], values[2]}); + Append(words, 0x15, {1, 1, 1, 0x8041}); +} + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +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); +} + +std::vector State() { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const std::vector> context{ + {0x2d5, 0x2000}, {0x1b6, 0}, {0x207, 0x01200000}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, {0xc, 0}, {0xd, 0x40040}, {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + std::vector words; + Append(words, 0x79, {0x242, 4}); + Append(words, 0x79, {0x24a, 0}); + for (const auto& [offset, value] : context) Append(words, 0x69, {offset, value}); + Append(words, 0x76, {0xc8, static_cast(vertex >> 8u)}); + Append(words, 0x76, {0xc9, static_cast(vertex >> 40u)}); + Append(words, 0x76, {0x8b, 2}); + Append(words, 0x76, {0x8c, 0}); + Append(words, 0x76, {0x008, static_cast(pixel >> 8u)}); + Append(words, 0x76, {0x009, static_cast(pixel >> 40u)}); + Append(words, 0x76, {0x00b, 2}); + Append(words, 0x76, {0x00c, std::bit_cast(1.0f)}); + return words; +} + +void Target(std::vector& words, void* pointer) { + const auto address = reinterpret_cast(pointer); + Append(words, 0x69, {0x318, static_cast(address >> 8u)}); + Append(words, 0x69, {0x390, static_cast(address >> 40u)}); +} + +void Run(SparseIndices& indices, std::uint32_t size, bool multi, bool afterGap, bool unreadable) { + indices.Fill(size); + Reference.fill(Kept); + Actual.fill(Kept); + Records.fill(0); + Count.fill(0); + const auto first = unreadable ? 16u / size : afterGap ? indices.AfterGap(size) : 0u; + Records[8] = 3; + Records[9] = 1; + Records[10] = DeclaredIndices; + Records[16] = 3; + Records[17] = 1; + Records[18] = 16u / size; + RecordDescriptor = {Low(Records.data()), High(Records.data()) & 0xffffu, sizeof(Records), 0x31016facu}; + CountDescriptor = {Low(Count.data()), High(Count.data()) & 0xffffu, sizeof(Count), 0x31016facu}; + auto words = State(); + Target(words, Reference.data()); + Append(words, 0x2a, {1}); + Append(words, 0x2d, {3, 2}); + WriteArguments(words, RecordDescriptor.data(), {3, 1, first}); + if (multi) WriteArguments(words, CountDescriptor.data(), {2, 0, 0}); + Target(words, Actual.data()); + Append(words, 0x2a, {size == 2 ? 0u : 1u}); + Append(words, 0x26, {Low(indices.Base()), High(indices.Base())}); + Append(words, 0x13, {DeclaredIndices}); + Append(words, 0x11, {1, Low(Records.data()), High(Records.data())}); + if (multi) Append(words, 0x38, {0, 0, 0, 0x40000280u, 3, Low(Count.data()), High(Count.data()), 32, 0}); + else Append(words, 0x25, {0, 0, 0, 0}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "sparse indexed draw submit failed"); + AgcDriverWaitIdle_nid_postfix(); + Require(Records[0] == 3 && Records[1] == 1 && Records[2] == first, "the GPU did not write the current indirect record"); + Require(!multi || Count[0] == 2, "the GPU did not write the indirect draw count"); + const auto covered = std::count_if(Reference.begin(), Reference.end(), [](std::byte value) { return value != Kept; }); + Require(covered != 0, "the reference triangle did not draw"); + Require(Actual == Reference, "the sparse indexed indirect draw differs from the reference triangle"); + std::cout << "sparse indices " << size << " bytes, " << (multi ? "counted multi" : "single") << ", " << (afterGap ? "after gap" : "before gap") << " passed\n"; +} + +} + +int main(int argc, char** argv) { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + Register(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Register(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + SparseIndices indices; + if (argc == 2 && std::string(argv[1]) == "--unreadable") { + bool refused = false; + try { + Run(indices, 2, false, false, true); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + refused = message.find("guest memory is not readable") != std::string::npos && message.find("+0x6)") != std::string::npos; + Require(refused, "the unreadable record did not fail on its actual six-byte index range: " + message); + } + Require(refused, "an indirect record reading the unmapped index page was accepted"); + try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {} + std::cout << "the unreadable indirect index range was refused\n"; + return 0; + } + for (const auto size : {2u, 4u}) { + for (const auto multi : {false, true}) { + Run(indices, size, multi, false, false); + Run(indices, size, multi, true, false); + } + } + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {} + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StagedClearSampledRead.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StagedClearSampledRead.cpp new file mode 100644 index 0000000000..f6016f8995 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StagedClearSampledRead.cpp @@ -0,0 +1,208 @@ +#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/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::DccKeys; +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +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 SampleTexelBytes = 4; +constexpr std::size_t UnalignedBlockBytes = 2 * 65536 + 2048; +constexpr std::size_t ArenaBytes = 3 * 65536; + +alignas(256) constexpr std::array WriteRowCode{ + 0x7e020281, 0x7e0402f2, 0x7e0602f2, 0x7e0802f2, 0x7e0a02f2, 0xf0201f08, 0x00010200, 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(GuestArena::GuestArenaAllocate_nid_postfix(ArenaBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, ArenaBytes, PAGE_READWRITE, ArenaBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, ArenaBytes + 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 + ArenaBytes != begin + ArenaBytes + alignment) munmap(reinterpret_cast(aligned + ArenaBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, ArenaBytes); + } +#endif + Require(block != nullptr, "staged clear sampled read: cannot allocate the guest block"); + Require(AgcDriver::GuestMemory::Watched(AddressOf(block), UnalignedBlockBytes), "staged clear sampled read: the guest block is not write-watched"); + GuestAllocations::Mutation().Add(block, UnalignedBlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, ArenaBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, ArenaBytes); +#else + munmap(block, ArenaBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, ArenaBytes); +#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}; +} + +void Dispatch(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; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +std::size_t Count(const std::uint8_t* bytes, std::size_t size, std::uint8_t value) { + return static_cast(std::count(bytes, bytes + size, value)); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + auto* texels = block.Data(); + auto* keys = texels + KeysOffset; + std::memset(texels, 0x00, SurfaceBytes); + const AgcDriver::Graphics::ColorTargetLayout layout(Side, Side, AgcDriver::Graphics::ColorTileMode::RenderTarget, 4); + std::memset(texels + layout.Offset(1, 0), 0xff, SampleTexelBytes); + std::memset(keys, 0xff, KeyBytes); + const auto surface = AddressOf(texels); + const auto texture = TextureDescriptor(texels, keys); + const auto resource = AgcDriver::Graphics::DecodeTextureResource(texture); + AgcDriver::GuestMemory::BumpCollectEpoch(); + std::vector userData(24, 0u); + std::vector outputData(Threads * 4u, 0xdeadbeefu); + const auto results = BufferDescriptor(outputData.data(), Threads * 16u); + std::copy(results.begin(), results.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(PointSampler.begin(), PointSampler.end(), userData.begin() + 12); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + std::vector writeData(16, 0u); + std::copy(texture.begin(), texture.end(), writeData.begin() + 4); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Dispatch(*device, WriteRowCode, writeData); + const auto image = StorageTexture::FindPending(surface, SurfaceBytes); + Require(image != nullptr, "the pass left no pending results at the surface"); + Require(image->UploadedKeys() == DccKeys::Uncompressed, "the pass did not draw under the settled keys"); + std::memset(keys, 0x00, KeyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(AgcDriver::Graphics::TextureClearKeys(resource, SurfaceBytes) == DccKeys::Clear0000, "the key bytes do not read a staged 0000 clear"); + Dispatch(*device, SampleSecondTexelCode, userData); + Require(StorageTexture::FindPending(surface, SurfaceBytes) == nullptr, "the sampled read did not store the pending results: the lookup serves without them in guest memory"); + const auto written = Count(texels, SurfaceBytes, 0xff); + const auto cleared = Count(texels, SurfaceBytes, 0x00); + char message[320]; + std::snprintf(message, sizeof(message), "the sampled read stored %zu written and %zu cleared bytes of %zu, expected %zu written", written, cleared, SurfaceBytes, Threads * 4u + SampleTexelBytes); + Require(written == Threads * 4u + SampleTexelBytes && cleared == SurfaceBytes - written, message); + for (std::uint32_t index = 0; index < Threads * 4u; ++index) { + std::snprintf(message, sizeof(message), "component %u of thread %u is 0x%08x, expected the drawn 0x3f800000 the staged clear does not cover", index % 4u, index / 4u, outputData[index]); + Require(outputData[index] == 0x3f800000u, message); + } + } + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::FlushCachedTextures(device->Device()); + } + device->WaitIdle(); + std::puts("staged clear sampled read 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/StencilKnownReplace.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilKnownReplace.cpp new file mode 100644 index 0000000000..284eb2e7d6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilKnownReplace.cpp @@ -0,0 +1,191 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::byte Kept{0x40}; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array WhitePixelCode{0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u}; +alignas(256) std::array ColorMemory{}; +alignas(256) std::array DepthMemory{}; +alignas(256) std::array StencilMemory{}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; + std::array registers{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + if (type == 1) { + const auto address = reinterpret_cast(code); + header.registers = {{{0x008, static_cast(address >> 8u)}, {0x009, static_cast(address >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}}}; + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = header.registers.size(); + } + 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); +} + +struct StencilState { + std::uint32_t depthControl; + std::uint32_t operations; + std::uint32_t refMask; +}; + +std::vector Draw(const StencilState& stencilState) { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(WhitePixelCode.data()); + const auto color = reinterpret_cast(ColorMemory.data()); + const auto depth = reinterpret_cast(DepthMemory.data()); + const auto stencil = reinterpret_cast(StencilMemory.data()); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b3, 0}, {0x1b4, 0}, {0x1b6, 0}, {0x207, 0}, {0x200, stencilState.depthControl}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {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, 9}, {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}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, {0x292, 2}, + {0x000, 0}, {0x002, 0}, {0x007, (3u << 16u) | 63u}, + {0x008, 0}, {0x009, std::bit_cast(1.0f)}, {0x00a, 0}, {0x00b, 0}, + {0x010, 3}, {0x011, 1}, + {0x012, static_cast(depth >> 8u)}, {0x014, static_cast(depth >> 8u)}, + {0x013, static_cast(stencil >> 8u)}, {0x015, static_cast(stencil >> 8u)}, + {0x01a, static_cast(depth >> 40u)}, {0x01c, static_cast(depth >> 40u)}, + {0x01b, static_cast(stencil >> 40u)}, {0x01d, static_cast(stencil >> 40u)}, + {0x10b, stencilState.operations}, {0x10c, stencilState.refMask}, {0x10d, stencilState.refMask}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + Append(commands, 0x2d, {3, 2}); + return commands; +} + +void Submit(const StencilState& stencilState) { + ColorMemory.fill(Kept); + auto commands = Draw(stencilState); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the stencil draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +constexpr std::uint32_t StencilEnable = 1u; +constexpr std::uint32_t Equal = 2u << 8u; +constexpr std::uint32_t LessEqual = 3u << 8u; +constexpr std::uint32_t Greater = 4u << 8u; +constexpr std::uint32_t NotEqual = 5u << 8u; +constexpr std::uint32_t Always = 7u << 8u; + +std::uint32_t RefMask(std::uint32_t test, std::uint32_t compareMask, std::uint32_t writeMask, std::uint32_t opValue) { + return test | (compareMask << 8u) | (writeMask << 16u) | (opValue << 24u); +} + +std::array Fill(std::uint32_t value) { + Submit({StencilEnable | Always, 0x30u, RefMask(value, 0xff, 0xff, 0)}); + const auto coverage = ColorMemory; + Require(std::count(coverage.begin(), coverage.end(), Kept) != static_cast(coverage.size()), "the stencil fill covered no pixels"); + return coverage; +} + +void Expect(const std::array& coverage, std::uint32_t value, const std::string& what) { + Submit({StencilEnable | Equal, 0u, RefMask(value, 0xff, 0, 0)}); + std::size_t drawn = 0; + for (std::size_t pixel = 0; pixel < coverage.size(); pixel += 4u) { + const bool covered = coverage[pixel] != Kept; + drawn += covered; + for (std::size_t channel = 0; channel < 4u; ++channel) Require(ColorMemory[pixel + channel] == (covered ? std::byte{255} : Kept), what + ": the stencil is not " + std::to_string(value) + " exactly where the draw covered"); + } + Require(drawn != 0, what + ": the probe covered no pixels"); +} + +void Case(const std::string& what, std::uint32_t initial, const StencilState& stencilState, std::uint32_t expected, bool testPasses = true) { + const auto coverage = Fill(initial); + Submit(stencilState); + if (testPasses) Require(ColorMemory == coverage, what + ": the draw under test did not color exactly the filled pixels"); + else Require(std::all_of(ColorMemory.begin(), ColorMemory.end(), [](auto value) { return value == Kept; }), what + ": a draw whose stencil test fails wrote color"); + Expect(coverage, expected, what); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, WhitePixelCode.data(), sizeof(WhitePixelCode), 1); + const std::array pixelContext{{{0x203, 0x800}, {0x1c4, 9}, {0x1b6, 0}}}; + AgcDriverResolveShaderAbi_nid_postfix(&PixelHeader.shader, pixelContext, {}); + Case("an equal test replaced by the operation value", 0xff, {StencilEnable | Equal, 0x40u, RefMask(0xff, 0xff, 0xff, 0x40)}, 0x40); + Case("an equal test replaced by the operation value on pass and depth fail", 0xff, {StencilEnable | Equal, 0x440u, RefMask(0xff, 0xff, 0xff, 0x40)}, 0x40); + Case("an equal test replaced by 0xff", 0x25, {StencilEnable | Equal, 0x20u, RefMask(0x25, 0xff, 0xff, 0)}, 0xff); + Case("an equal test replaced through a partial write mask", 0xff, {StencilEnable | Equal, 0x40u, RefMask(0xff, 0xff, 0xf0, 0x40)}, 0x4f); + Case("a failed not-equal test replaced by the operation value", 0xff, {StencilEnable | NotEqual, 0x4u, RefMask(0xff, 0xff, 0xff, 0x40)}, 0x40, false); + Case("a less-or-equal test against 0xff replaced by the operation value", 0xff, {StencilEnable | LessEqual, 0x40u, RefMask(0xff, 0xff, 0xff, 0x40)}, 0x40); + Case("a failed greater test against 0xff replaced by the operation value", 0xff, {StencilEnable | Greater, 0x4u, RefMask(0xff, 0xff, 0xff, 0x40)}, 0x40, false); + AgcDriverShutdown_nid_postfix(); + std::puts("stencil replacements with a known stencil value passed"); + 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/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/StencilRefExportUnused.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilRefExportUnused.cpp new file mode 100644 index 0000000000..34bcd4728b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilRefExportUnused.cpp @@ -0,0 +1,256 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::byte Kept{0x40}; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e040285u, 0xf8000082u, 0x00000200u, 0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u}; +alignas(256) constexpr std::array WhitePixelCode{0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u}; +alignas(256) std::array ColorMemory{}; +alignas(256) std::array DepthMemory{}; +alignas(256) std::array StencilMemory{}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; + std::array registers{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + if (type == 1) { + const auto address = reinterpret_cast(code); + header.registers = {{{0x008, static_cast(address >> 8u)}, {0x009, static_cast(address >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}}}; + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = header.registers.size(); + } + 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); +} + +constexpr std::array PixelContext{{{0x203, 0x802}, {0x1c4, 9}, {0x1b6, 0}}}; + +struct DrawOptions { + bool depth = false; + std::uint32_t depthControl = 0x701u; + std::uint32_t operations = 0; + std::uint32_t front = 0x00ffff25u; + std::uint32_t back = 0x00ffff25u; + std::uint32_t shaderControl = 0x802u; + std::uint32_t pixelInputs = 0; +}; + +std::vector Draw(std::span pixelCode = PixelCode, const DrawOptions& options = {}) { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(pixelCode.data()); + const auto color = reinterpret_cast(ColorMemory.data()); + std::vector> context{ + {0x2d5, 0x2000}, + {0x1b3, options.pixelInputs}, {0x1b4, options.pixelInputs}, {0x1b6, 0}, {0x207, 0}, {0x200, options.depth ? options.depthControl : 0}, {0x203, options.shaderControl}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {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, 9}, {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}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, {0x292, 2}, + }; + if (options.depth) { + const auto depth = reinterpret_cast(DepthMemory.data()); + const auto stencil = reinterpret_cast(StencilMemory.data()); + const std::vector> depthContext{ + {0x000, 0}, {0x002, 0}, {0x007, (3u << 16u) | 63u}, + {0x008, 0}, {0x009, std::bit_cast(1.0f)}, {0x00a, 0}, {0x00b, 0}, + {0x010, 3}, {0x011, 1}, + {0x012, static_cast(depth >> 8u)}, {0x014, static_cast(depth >> 8u)}, + {0x013, static_cast(stencil >> 8u)}, {0x015, static_cast(stencil >> 8u)}, + {0x01a, static_cast(depth >> 40u)}, {0x01c, static_cast(depth >> 40u)}, + {0x01b, static_cast(stencil >> 40u)}, {0x01d, static_cast(stencil >> 40u)}, + {0x10b, options.operations}, {0x10c, options.front}, {0x10d, options.back}, + }; + context.insert(context.end(), depthContext.begin(), depthContext.end()); + } + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + Append(commands, 0x2d, {3, 2}); + return commands; +} + +void UnusedStencilRefExport() { + ColorMemory.fill(Kept); + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + AgcDriverResolveShaderAbi_nid_postfix(&PixelHeader.shader, PixelContext, {}); + auto commands = Draw(); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + const auto kept = static_cast(std::count(ColorMemory.begin(), ColorMemory.end(), Kept)); + Require(kept != ColorMemory.size(), "the draw whose pixel shader exports an unused stencil test value wrote no pixels"); +} + +void SubmitDraw(std::span code, const DrawOptions& options) { + ColorMemory.fill(Kept); + Registered(PixelHeader, code.data(), code.size_bytes(), 1); + const std::array context{{{0x203, options.shaderControl}, {0x1c4, 9}, {0x1b6, 0}, {0x1b3, options.pixelInputs}, {0x1b4, options.pixelInputs}}}; + AgcDriverResolveShaderAbi_nid_postfix(&PixelHeader.shader, context, {}); + auto commands = Draw(code, options); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the stencil reference regression submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +std::array InitializeStencil() { + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + SubmitDraw(PixelCode, {.depth = true, .operations = 0x30u, .shaderControl = 0x800u}); + const auto coverage = ColorMemory; + Require(std::count(coverage.begin(), coverage.end(), Kept) != coverage.size(), "the stencil fixture has no covered pixels"); + return coverage; +} + +template +void CheckWhitePixels(const std::array& coverage, TPredicate selected, const char* message) { + std::size_t selectedPixels = 0; + for (std::uint32_t y = 0; y < 4u; ++y) { + for (std::uint32_t x = 0; x < 64u; ++x) { + const auto pixel = (y * 64u + x) * 4u; + const bool drawn = coverage[pixel] != Kept && selected(x, y); + selectedPixels += drawn; + for (std::uint32_t channel = 0; channel < 4u; ++channel) Require(ColorMemory[pixel + channel] == (drawn ? std::byte{255} : Kept), message); + } + } + Require(selectedPixels != 0, "the stencil readback probe selected no covered pixels"); +} + +void ProbeStencil(std::uint32_t reference) { + SubmitDraw(WhitePixelCode, {.depth = true, .depthControl = 0x201u, .front = 0x0000ff00u | reference, .back = 0x0000ff00u | reference, .shaderControl = 0x800u}); +} + +void IndependentReference(bool invert) { + const auto coverage = InitializeStencil(); + const auto operations = invert ? 0x00070070u : 0u; + SubmitDraw(PixelCode, {.depth = true, .depthControl = 0x007007e7u, .operations = operations, .shaderControl = 0x800u}); + const auto expected = ColorMemory; + Require(expected == coverage, "the reference-independent baseline changed color coverage"); + static_cast(InitializeStencil()); + SubmitDraw(PixelCode, {.depth = true, .depthControl = 0x007007e7u, .operations = operations}); + Require(ColorMemory == expected, "dropping an unused reference changed the full color readback"); + ProbeStencil(invert ? (37u ^ 255u) : 37u); + CheckWhitePixels(coverage, [](auto, auto) { return true; }, "dropping an unused reference changed the stencil readback"); +} + +void DependentReference(std::string_view name) { + static_cast(InitializeStencil()); + DrawOptions options{.depth = true}; + if (name == "compare") options.depthControl = 0x201u; + else if (name == "back-compare") options.depthControl = 0x200781u; + else if (name == "replace") options.operations = 0x30u; + else if (name == "fail-replace") { options.depthControl = 1u; options.operations = 3u; } + else if (name == "depth-fail-replace") { options.depthControl = 0x703u; options.operations = 0x300u; } + else if (name == "partial-replace") { options.operations = 0x30u; options.front = options.back = 0x0080ff25u; } + else throw std::runtime_error("unknown dependent stencil reference case"); + const auto stencil = StencilMemory; + bool refused = false; + try { SubmitDraw(PixelCode, options); } + catch (const std::exception& error) { + refused = std::string_view(error.what()).find("a pixel shader that exports the stencil reference needs VK_EXT_shader_stencil_export, which the device lacks") != std::string_view::npos; + if (!refused) throw; + } + Require(refused, "a dependent reference without native stencil export was admitted"); + Require(std::all_of(ColorMemory.begin(), ColorMemory.end(), [](auto value) { return value == Kept; }), "a refused stencil draw wrote color"); + Require(StencilMemory == stencil, "a refused stencil draw changed its guest stencil memory"); +} + +} + +int main(int argc, char** argv) { + try { + bool nativeStencilExport = false; + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + nativeStencilExport = TargetHasCapability(device->Target(), spv::CapabilityStencilExportEXT); + } + bool expectedRefusal = false; + if (argc == 3 && std::string_view(argv[1]) == "--case") { + const std::string_view name = argv[2]; + if (name == "keep" || name == "invert") IndependentReference(name == "invert"); + else { + if (nativeStencilExport) { + std::puts("skipped: a dependent reference refusal requires a device without native stencil export"); + return VulkanTestSkipped; + } + DependentReference(name); + expectedRefusal = true; + } + } else { + Require(argc == 1, "expected --case keep, invert, compare, back-compare, replace, fail-replace, depth-fail-replace, or partial-replace"); + UnusedStencilRefExport(); + IndependentReference(false); + IndependentReference(true); + } + try { AgcDriverShutdown_nid_postfix(); } + catch (const std::exception& error) { + if (!expectedRefusal || std::string_view(error.what()).find("a pixel shader that exports the stencil reference needs VK_EXT_shader_stencil_export, which the device lacks") == std::string_view::npos) throw; + } + std::puts(expectedRefusal ? "a dependent stencil reference retained the native extension refusal" : "unused stencil reference draws and full color/stencil readbacks passed"); + 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/StencilReplay.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp new file mode 100644 index 0000000000..3612fae6ef --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp @@ -0,0 +1,378 @@ +#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 + +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 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}); +} + +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 = {}, const std::function& alterVertex = {}) { + 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; + auto vertexResult = ShaderRecompiler::Recompile(vertex); + if (alterVertex) alterVertex(vertexResult); + 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; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + 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 + }; + constexpr std::array ordered{ + 0x7c000000u | (4u << 17u) | (2u << 9u) | 0xf0u, + 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; }; + bool rejectedContract = false; + try { + Draw(*device, alternating, exported, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, {}, {}, [&](ShaderRecompiler::RecompileResult& result) { result.hostSubgroupSize = device->Target().subgroupSize == 64u ? 32u : 64u; }); + } catch (const std::exception& error) { + rejectedContract = std::string_view(error.what()).find("subgroup contract differs") != std::string_view::npos; + if (!rejectedContract) throw; + } + Require(rejectedContract, "a stale vertex subgroup-size contract was accepted"); + rejectedContract = false; + try { + Draw(*device, alternating, exported, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, {}, {}, [](ShaderRecompiler::RecompileResult& result) { auto words = result.spirv.Copy(); words[1] = 0x00010600u; result.spirv = std::move(words); }); + } catch (const std::exception& error) { + rejectedContract = std::string_view(error.what()).find("subgroup contract differs") != std::string_view::npos; + if (!rejectedContract) throw; + } + Require(rejectedContract, "a varying-size module contract was accepted"); + Draw(*device, alternating, exported, replace()); + Check(all, "finite stencil export"); + Draw(*device, PlainPixelCode, plain, equal(255u)); + Check(even, "even columns exported 255"); + Draw(*device, PlainPixelCode, plain, equal(0u)); + Check(odd, "odd columns exported zero"); + Draw(*device, alternating, exported, equal(91u)); + Check(all, "exported references replace compare reference"); + Draw(*device, PlainPixelCode, plain, replace(0u)); + Draw(*device, ordered, exported, replace(), 6u); + Draw(*device, PlainPixelCode, plain, equal(0u)); + Check(all, "overlapping primitives retain original order"); + Draw(*device, PlainPixelCode, plain, replace(0u)); + const VkStencilOpState instanceOrder{VK_STENCIL_OP_INCREMENT_AND_WRAP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_GREATER, 0xffu, 0xffu, 0u}; + Draw(*device, ordered, exported, instanceOrder, 6u, 2u); + Draw(*device, PlainPixelCode, plain, equal(0u)); + Check(all, "primitive order within every instance"); + alignas(256) constexpr std::array indices{0u, 1u, 2u, 3u, 4u, 5u}; + alignas(256) std::array, 7> indexedVertices{}; + std::copy(Vertices.begin(), Vertices.end(), indexedVertices.begin() + 1); + Draw(*device, PlainPixelCode, plain, replace(0u)); + Draw(*device, ordered, exported, instanceOrder, 6u, 2u, false, {}, [&](AgcDriver::Pm4::DrawParameters& draw) { + draw.indexed = true; + draw.indexAddress = reinterpret_cast(indices.data()); + draw.indexSize = 4u; + draw.firstVertex = 1u; + draw.firstInstance = 7u; + }, indexedVertices); + Draw(*device, PlainPixelCode, plain, equal(0u)); + Check(all, "indexed primitives preserve base vertex and nonzero first instance"); + alignas(256) std::array, 72> instanceVertices{}; + std::copy(Vertices.begin(), Vertices.end(), instanceVertices.begin() + 56); + std::copy(Vertices.begin() + 3, Vertices.end(), instanceVertices.begin() + 64); + std::copy(Vertices.begin(), Vertices.begin() + 3, instanceVertices.begin() + 67); + constexpr std::array instanceVertexCode{0x34101083u, 0x4a0a0b08u, 0xe0382000u, 0x80000005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; + Draw(*device, PlainPixelCode, plain, replace(0u)); + Draw(*device, ordered, exported, instanceOrder, 6u, 2u, false, {}, [](AgcDriver::Pm4::DrawParameters& draw) { draw.firstInstance = 7u; }, instanceVertices, instanceVertexCode); + Check(all, "nonzero instance indices select live vertices"); + Draw(*device, PlainPixelCode, plain, equal(255u)); + Check(all, "each replayed instance preserves its observable instance index"); + Draw(*device, PlainPixelCode, plain, replace(0x55u)); + Draw(*device, alternating, exported, replace(0u, 0xaau)); + Draw(*device, PlainPixelCode, plain, equal(0xffu)); + Check(even, "stencil write mask preserves even columns"); + Draw(*device, PlainPixelCode, plain, equal(0x55u)); + Check(odd, "stencil write mask preserves odd columns"); + auto highBits = alternating; + highBits[4] = 0x1234ffu; + Draw(*device, highBits, exported, replace()); + Draw(*device, PlainPixelCode, plain, equal(255u)); + Check(even, "stencil reference truncates to eight attachment bits"); + Draw(*device, PlainPixelCode, plain, replace(0u)); + Draw(*device, PlainPixelCode, plain, replace(0u), 3u, 1u, false, [](AgcDriver::Graphics::State& state) { state.cullMode = VK_CULL_MODE_BACK_BIT; }); + const auto facing = Pixels[0] == std::byte{255} ? VK_FRONT_FACE_COUNTER_CLOCKWISE : VK_FRONT_FACE_CLOCKWISE; + for (const bool back : {false, true}) { + Draw(*device, PlainPixelCode, plain, replace(0u)); + Draw(*device, alternating, exported, replace(), 3u, 1u, false, [&](AgcDriver::Graphics::State& state) { + state.frontFace = back ? (facing == VK_FRONT_FACE_COUNTER_CLOCKWISE ? VK_FRONT_FACE_CLOCKWISE : VK_FRONT_FACE_COUNTER_CLOCKWISE) : facing; + state.stencilFront.writeMask = 0xaau; + state.stencilBack.writeMask = 0x55u; + state.stencilBack.passOp = VK_STENCIL_OP_INVERT; + }); + Draw(*device, PlainPixelCode, plain, equal(back ? 0x55u : 0xaau)); + Check(back ? all : even, "distinct front and back stencil operations and write masks"); + } + Draw(*device, PlainPixelCode, plain, replace(0x0fu)); + Draw(*device, alternating, exported, equal(1u, 0x0fu)); + Check(even, "stencil compare masks exported reference"); + const VkStencilOpState depthFailure{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 0u}; + Draw(*device, alternating, exported, depthFailure, 3u, 1u, true); + Check([](std::uint32_t, std::uint32_t) { return false; }, "depth failure rejects colors"); + Draw(*device, PlainPixelCode, plain, equal(255u)); + Check(even, "depth-fail replace uses exported reference"); + auto depthStencil = std::vector(alternating.begin(), alternating.end()); + const auto mrtz = std::find(depthStencil.begin(), depthStencil.end(), 0xf8000082u); + const auto mrtzOffset = static_cast(mrtz - depthStencil.begin()); + depthStencil.insert(mrtz, {0x7e0c02ffu, 0x3f400000u}); + depthStencil[mrtzOffset + 2u] = 0xf8000083u; + depthStencil[mrtzOffset + 3u] = (5u << 8u) | 6u; + Draw(*device, depthStencil, 0x53u, replace(), 3u, 1u, false, [](AgcDriver::Graphics::State& state) { + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + }); + auto midDepth = Vertices; + for (auto& vertex : midDepth) vertex[2] = 0.5f; + Draw(*device, PlainPixelCode, plain, replace(), 3u, 1u, false, [](AgcDriver::Graphics::State& state) { + state.depthTest = true; + state.depthCompare = VK_COMPARE_OP_LESS; + }, {}, midDepth); + Check(all, "simultaneous depth and stencil exports preserve shader depth replacement"); + for (const bool restart : {false, true}) { + bool refused = false; + try { + Draw(*device, alternating, exported, replace(), 3u, 1u, false, [&](AgcDriver::Graphics::State& state) { if (restart) state.primitiveRestart = true; else state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; }); + } catch (const std::exception& error) { + refused = std::string(error.what()).find("direct triangle lists") != std::string::npos; + } + Require(refused, "unsafe primitive routing was not refused"); + } + Draw(*device, ColumnPixelCode, exported, replace()); + Check(all, "unbounded stencil export"); + Draw(*device, PlainPixelCode, plain, equal(5u)); + Check([](std::uint32_t x, std::uint32_t) { return x == 5u; }, "unbounded stencil export replaces each column's own value"); + bool earlyRefused = false; + try { Draw(*device, alternating, 0x1012u, replace()); } catch (const std::exception& error) { earlyRefused = std::string(error.what()).find("late tests") != std::string::npos; } + Require(earlyRefused, "early stencil testing was not refused"); + bool orderedRefused = false; + try { Draw(*device, alternating, 0x10052u, replace()); } catch (const std::exception& error) { orderedRefused = std::string(error.what()).find("late tests") != std::string::npos; } + Require(orderedRefused, "ordered fragment execution was not refused"); + bool indirectRefused = false; + try { Draw(*device, alternating, exported, replace(), 3u, 1u, false, {}, [](AgcDriver::Pm4::DrawParameters& draw) { draw.indirect.emplace(); }); } catch (const std::exception& error) { indirectRefused = std::string(error.what()).find("direct triangle lists") != std::string::npos; } + Require(indirectRefused, "an indirect replay draw was not refused"); + auto killed = std::vector(alternating.begin(), alternating.end()); + killed.insert(std::find(killed.begin(), killed.end(), 0xf800180fu), 0x7c000000u | (0xd2u << 17u) | (3u << 9u) | 0x80u); + Draw(*device, killed, exported, replace()); + Check(even, "shader kill remains effective before reference filtering"); + auto skippedExport = std::vector(alternating.begin(), alternating.end()); + skippedExport.insert(skippedExport.begin(), 0xbe94037eu); + skippedExport.insert(skippedExport.begin() + 7, 0x7c000000u | (0xd2u << 17u) | (3u << 9u) | 0x80u); + skippedExport.insert(std::find(skippedExport.begin(), skippedExport.end(), 0xf800180fu), 0xbefe0314u); + bool coverageRefused = false; + try { Draw(*device, skippedExport, exported, replace()); } catch (const std::exception& error) { coverageRefused = std::string(error.what()).find("every kept path") != std::string::npos; } + Require(coverageRefused, "a stencil export skipped by kept fragments was not refused"); + auto userColor = std::vector(alternating.begin(), alternating.end()); + *std::find(userColor.begin(), userColor.end(), 0x7e0e02f2u) = 0x7e0e0200u; + const std::array colorData{0x3f800000u}; + Draw(*device, userColor, exported, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, colorData); + Check(all, "dynamic reference preserves the adjacent shader-data push payload"); + auto guarded = std::vector(alternating.begin(), alternating.end()); + *std::find(guarded.begin(), guarded.end(), 0x7e0e02f2u) = 0x7e0e020cu; + guarded.insert(guarded.begin(), {0xf4040280u, 0xfa000018u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu}); + alignas(256) std::array root{}; + const std::uint32_t payload = 0x3f800000u; + const auto rootAddress = static_cast(reinterpret_cast(root.data())); + const std::array rootData{static_cast(rootAddress), static_cast(rootAddress >> 32u)}; + const auto payloadAddress = static_cast(reinterpret_cast(&payload)); + root[6] = static_cast(payloadAddress); + root[7] = static_cast(payloadAddress >> 32u); + const std::array guardedMemory{{{rootAddress, std::as_bytes(std::span(root))}, {payloadAddress, std::as_bytes(std::span(&payload, 1))}}}; + Draw(*device, guarded, exported, replace(), 6u, 2u, false, {}, {}, Vertices, VertexCode, rootData, guardedMemory); + Check(all, "readonly guarded SRT survives repeated references, primitives and instances"); + root[6] = root[7] = 0u; + bool faultThrown = false; + try { Draw(*device, guarded, exported, replace(), 6u, 2u, false, {}, {}, Vertices, VertexCode, rootData, std::span(guardedMemory).first(1)); } + catch (const std::exception& error) { faultThrown = std::string(error.what()).find("BDA") != std::string::npos; } + Require(faultThrown, "poisoned readonly SRT replay did not throw a host fault"); + Require(std::ranges::all_of(Pixels, [](std::byte value) { return value == Kept; }), "poisoned replay published guest color results before its fault"); + std::puts("stencil replay 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..a3a273ff50 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp @@ -1,4 +1,6 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "Recompiler.hpp" @@ -10,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +20,7 @@ namespace { using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::Recorder; using AgcDriver::Graphics::StorageTexture; using ShaderRecompiler::ShaderStage; @@ -51,8 +55,7 @@ std::array TextureDescriptor(std::uint64_t address) { }; } -std::shared_ptr Store(AgcDriver::VulkanDevice& device, std::uint64_t base, std::uint32_t surface) { - const auto address = base + surface * Stride; +void Record(AgcDriver::VulkanDevice& device, std::uint64_t address) { std::vector userData(16, 0u); const auto texture = TextureDescriptor(address); std::copy(texture.begin(), texture.end(), userData.begin() + 4); @@ -67,17 +70,71 @@ std::shared_ptr Store(AgcDriver::VulkanDevice& device, std::uint }; request.useCache = true; const auto result = ShaderRecompiler::Recompile(request); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); +} + +std::shared_ptr Store(AgcDriver::VulkanDevice& device, std::uint64_t base, std::uint32_t surface, bool flush = true) { + const auto address = base + surface * Stride; + Record(device, address); device.WaitIdle(); + if (!flush) return StorageTexture::FindLive(address, SurfaceBytes); StorageTexture::FlushPending(address, static_cast(SurfaceBytes), nullptr, "test"); device.WaitIdle(); return StorageTexture::FindLive(address, SurfaceBytes); } +int RetainedBatch() { +#ifdef _WIN32 + Require(_putenv_s("APS5_TEXTURE_CACHE_MIB", "32") == 0, "cannot set the test cache budget"); + Require(_putenv_s("APS5_NO_RESOURCE_CACHE", "1") == 0, "cannot disable the resource cache for the retained-batch test"); +#else + Require(setenv("APS5_TEXTURE_CACHE_MIB", "32", 1) == 0, "cannot set the test cache budget"); + Require(setenv("APS5_NO_RESOURCE_CACHE", "1", 1) == 0, "cannot disable the resource cache for the retained-batch test"); +#endif + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::vector storage((SurfaceBytes + MaxSurfaces * Stride + Stride) / 4u, 0u); + auto* texels = reinterpret_cast((reinterpret_cast(storage.data()) + Stride - 1u) & ~std::uintptr_t{Stride - 1u}); + const auto base = static_cast(reinterpret_cast(texels)); + const auto blockBytes = static_cast(SurfaceBytes + (MaxSurfaces - 1u) * Stride); + { + GuestAllocations::Mutation mutation; + mutation.Add(texels, blockBytes, true, true, true); + } + const auto release = [&] { + device->WaitIdle(); + AgcDriver::Graphics::ClearCachedTextures(device->Device()); + GuestAllocations::Mutation mutation; + mutation.Remove(texels); + }; + try { + Record(*device, base); + auto retained = StorageTexture::FindLive(base, SurfaceBytes); + Require(retained != nullptr, "surface 0 has no cached storage image"); + const auto retainedBytes = retained->BudgetBytes(); + auto* recorder = Recorder::Active(); + Require(recorder != nullptr && recorder->Recording(), "the surface dispatch was not left in an open batch"); + const auto beforeEviction = recorder->OpenKeptBytes(); + const auto capacity = static_cast(TestBudget / retainedBytes); + Require(capacity + 1u < MaxSurfaces, "the eviction set exceeds the test storage"); + retained.reset(); + for (std::uint32_t surface = 1; surface <= capacity; ++surface) Record(*device, base + surface * Stride); + Require(StorageTexture::FindLive(base, SurfaceBytes) == nullptr, "surface 0 remained cached after the storage budget was exceeded"); + Require(recorder->OpenKeptBytes() >= beforeEviction + retainedBytes, "an evicted storage image held by a batch without a shared resource-cache entry was not charged to that batch"); + } catch (...) { + release(); + throw; + } + release(); + return 0; } -int main() { +} + +int main(int argc, char** argv) { try { + if (argc == 2 && std::string(argv[1]) == "--retained-batch") return RetainedBatch(); #ifdef _WIN32 Require(_putenv_s("APS5_TEXTURE_CACHE_MIB", "32") == 0, "cannot set the test cache budget"); #else @@ -126,6 +183,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/StorageFileWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageFileWrite.cpp new file mode 100644 index 0000000000..6fd7ebae81 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageFileWrite.cpp @@ -0,0 +1,375 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif + +class FileStream; +struct GuestIovec { void* base; std::size_t length; }; + +extern "C" { +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelWrite(int, const void*, std::size_t); +std::int64_t APS5_VABI sceKernelPwrite(int, const void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI sceKernelWritev(int, const GuestIovec*, int); +std::int64_t APS5_VABI sceKernelPwritev(int, const GuestIovec*, int, std::int64_t); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +std::int64_t APS5_VABI _write_nid_postfix(int, const void*, std::size_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 writev_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI pwritev_nid_postfix(int, const GuestIovec*, int, std::int64_t); +int APS5_VABI sceKernelAioSubmitWriteCommands(KernelAioRwRequest*, std::int32_t, std::int32_t, std::int32_t*); +int APS5_VABI sceKernelAioSubmitWriteCommandsMultiple(KernelAioRwRequest*, std::int32_t, std::int32_t, std::int32_t*); +int APS5_VABI sceKernelAioDeleteRequest(std::int32_t, std::int32_t*); +FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +std::size_t APS5_VABI fwrite_nid_postfix(const void*, std::size_t, std::size_t, FileStream*); +int APS5_VABI fclose_nid_postfix(FileStream*); +} + +namespace { + +using AgcDriver::Graphics::Require; +constexpr std::uint32_t Width = 128; +constexpr std::uint32_t Height = 120; +constexpr std::size_t SurfaceBytes = 0x20000; +constexpr std::size_t SourceOffset = 13; +constexpr std::size_t TransferBytes = SurfaceBytes - 37; +constexpr std::int64_t FileOffset = 17; +alignas(256) constexpr std::array WriteCode{ + 0xd7460004u, 0x0401060eu, 0xd7460005u, 0x0405060fu, 0x7e100d04u, 0x7e120d05u, + 0x7e140c0du, 0x7e1602f2u, 0xf0200f08u, 0x00000804u, 0xbf810000u, +}; + +class GuestSurface { +public: + GuestSurface() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, SurfaceBytes)); + GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(mapped != MAP_FAILED, "file write: cannot allocate the guest surface"); + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + SurfaceBytes - 1) & ~(std::uintptr_t{SurfaceBytes - 1}); + if (aligned != begin) munmap(mapped, aligned - begin); + const auto tail = begin + SurfaceBytes * 2 - aligned - SurfaceBytes; + if (tail != 0) munmap(reinterpret_cast(aligned + SurfaceBytes), tail); + block = reinterpret_cast(aligned); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes); +#endif + GuestAllocations::Mutation mutation; + mutation.Add(block, SurfaceBytes, true, true, true); + } + + ~GuestSurface() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); +#else + munmap(block, SurfaceBytes); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, SurfaceBytes); +#endif + } + + GuestSurface(const GuestSurface&) = delete; + GuestSurface& operator=(const GuestSurface&) = delete; + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +void Produce(std::uint8_t* texels) { + std::memset(texels, 0, SurfaceBytes); + const auto address = reinterpret_cast(texels); + const std::array data{ + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (71u << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (0x09u << 20u) | (9u << 28u), + 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 7u, + }; + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{8, 8, 1}; + registers(0x207, shape); + const auto code = reinterpret_cast(WriteCode.data()); + const std::array program{static_cast(code >> 8u), static_cast(code >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (7u << 7u) | (1u << 11u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, Width / 8u, Height / 8u, 1u, 0x41u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverSuspendPoint_nid_postfix(); + AgcDriverWaitIdle_nid_postfix(); + Require(AgcDriver::Graphics::StorageTexture::AnyPending(), "file write: the GPU producer left no pending image"); +} + +class OutputFile { +public: + OutputFile() : name("anyps5-storage-file-write-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".bin") {} + ~OutputFile() { + std::error_code error; + std::filesystem::remove(name, error); + } + const std::string& Name() const { return name; } + int Open() const { + const auto descriptor = sceKernelOpen(name.c_str(), SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_TRUNC, 0600); + Require(descriptor >= 0, "file write: cannot open the output"); + return descriptor; + } + std::vector Read(std::size_t offset) const { + std::ifstream file(name, std::ios::binary | std::ios::ate); + Require(file.tellg() == static_cast(offset + TransferBytes), "file write: incorrect output length"); + file.seekg(static_cast(offset)); + std::vector result(TransferBytes); + Require(static_cast(file.read(reinterpret_cast(result.data()), result.size())), "file write: short output readback"); + return result; + } + +private: + std::string name; +}; + +bool Pending(GuestSurface& surface) { + return AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(surface.Data()), SurfaceBytes) != nullptr; +} + +std::unique_lock ProduceHeld(GuestSurface& surface, GuestSurface& unrelated) { + for (int attempt = 0; attempt < 10; ++attempt) { + if (!Pending(unrelated)) Produce(unrelated.Data()); + Produce(surface.Data()); + std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex()); + if (Pending(surface) && Pending(unrelated)) return hold; + } + throw std::runtime_error("file write: the queue-idle store kept taking the pending images"); +} + +enum class Api { Write, PosixWrite, CrtWrite, Pwrite, PosixPwrite, Writev, PosixWritev, Pwritev, PosixPwritev, Aio, AioMultiple, Fwrite }; + +bool CheckWrite(Api api, const char* name, GuestSurface& surface, GuestSurface& unrelated) { + const auto hold = ProduceHeld(surface, unrelated); + OutputFile file; + auto* source = surface.Data() + SourceOffset; + const std::array vectors{{{source, 111}, {nullptr, 0}, {source + 111, 65537}, {source + 65648, TransferBytes - 65648}}}; + const bool positioned = api == Api::Pwrite || api == Api::PosixPwrite || api == Api::Pwritev || api == Api::PosixPwritev || api == Api::Aio || api == Api::AioMultiple; + if (api == Api::Fwrite) { + auto* stream = fopen_nid_postfix(file.Name().c_str(), "wb"); + Require(stream != nullptr, "file write: cannot open the stdio output"); + Require(fwrite_nid_postfix(source, 0, 7, stream) == 0, "file write: zero-sized fwrite failed"); + Require(fwrite_nid_postfix(source, 5, 0, stream) == 0, "file write: zero-count fwrite failed"); + bool overflow = false; + try { fwrite_nid_postfix(source, std::numeric_limits::max(), 2, stream); } + catch (const std::overflow_error&) { overflow = true; } + Require(overflow, "file write: fwrite byte count overflow was accepted"); + Require(AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(surface.Data()), SurfaceBytes) != nullptr, "file write: an empty or overflowing fwrite flushed its source"); + const auto written = fwrite_nid_postfix(source, 5, TransferBytes / 5, stream); + Require(fclose_nid_postfix(stream) == 0, "file write: cannot close the stdio output"); + Require(written == TransferBytes / 5, "file write: fwrite returned a short element count"); + } else { + const auto descriptor = file.Open(); + if (api == Api::Write) { + Require(sceKernelWrite(descriptor, source, 0) == 0, "file write: zero length failed"); + Require(sceKernelPwrite(descriptor, nullptr, 0, 0) == 0, "file write: zero-length pwrite failed"); + Require(sceKernelWritev(descriptor, nullptr, 0) == 0, "file write: empty writev failed"); + Require(sceKernelPwrite(descriptor, source, 1, -1) == static_cast(static_cast(0x80020016u)), "file write: negative offset was accepted"); + Require(AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(surface.Data()), SurfaceBytes) != nullptr, "file write: an empty or invalid write flushed its source"); + const char cpu = 'x'; + Require(sceKernelWrite(descriptor, &cpu, 1) == 1, "file write: unrelated CPU write failed"); + Require(AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(surface.Data()), SurfaceBytes) != nullptr, "file write: an unrelated source flushed the GPU image"); + Require(sceKernelClose(descriptor) == 0, "file write: cannot close the scope-control output"); + } + const auto output = api == Api::Write ? file.Open() : descriptor; + std::int64_t written = -1; + switch (api) { + case Api::Write: written = sceKernelWrite(output, source, TransferBytes); break; + case Api::PosixWrite: written = write_nid_postfix(output, reinterpret_cast(source), TransferBytes); break; + case Api::CrtWrite: written = _write_nid_postfix(output, source, TransferBytes); break; + case Api::Pwrite: written = sceKernelPwrite(output, source, TransferBytes, FileOffset); break; + case Api::PosixPwrite: written = pwrite_nid_disambig1_nid_postfix(output, source, TransferBytes, FileOffset); break; + case Api::Writev: written = sceKernelWritev(output, vectors.data(), vectors.size()); break; + case Api::PosixWritev: written = writev_nid_postfix(output, vectors.data(), vectors.size()); break; + case Api::Pwritev: written = sceKernelPwritev(output, vectors.data(), vectors.size(), FileOffset); break; + case Api::PosixPwritev: written = pwritev_nid_postfix(output, vectors.data(), vectors.size(), FileOffset); break; + case Api::Aio: + case Api::AioMultiple: { + KernelAioResult result{}; + KernelAioRwRequest request{FileOffset, TransferBytes, source, &result, output}; + std::int32_t id = 0; + Require((api == Api::Aio ? sceKernelAioSubmitWriteCommands(&request, 1, 0, &id) : sceKernelAioSubmitWriteCommandsMultiple(&request, 1, 0, &id)) == 0, "file write: AIO submission failed"); + std::int32_t removed = -1; + Require(sceKernelAioDeleteRequest(id, &removed) == 0 && removed == 0, "file write: AIO deletion failed"); + Require(result.state == 3, "file write: AIO did not complete"); + written = result.return_value; + break; + } + case Api::Fwrite: break; + } + Require(sceKernelClose(output) == 0, "file write: cannot close the output"); + Require(written == TransferBytes, "file write: the transfer returned a short byte count"); + } + const auto stored = file.Read(positioned ? FileOffset : 0); + std::vector expected(TransferBytes); + AgcDriver::GuestMemory::Read(reinterpret_cast(source), expected); + Require(std::any_of(expected.begin(), expected.end(), [](std::byte value) { return value != std::byte{}; }), "file write: the GPU producer wrote only zeros"); + std::size_t differences = 0; + for (std::size_t index = 0; index < stored.size(); ++index) differences += stored[index] != expected[index]; + std::printf("%s: %zu incorrect file bytes out of %zu\n", name, differences, stored.size()); + Require(Pending(unrelated), "file write: an unrelated pending GPU image was flushed"); + return differences == 0; +} + +void CheckConcurrentFlushWait(GuestSurface& surface) { + Produce(surface.Data()); + const auto address = reinterpret_cast(surface.Data()); + Require(AgcDriver::Graphics::StorageTexture::FindPending(address, SurfaceBytes) != nullptr, "file write: concurrent flush setup has no pending image"); + std::mutex mutex; + std::condition_variable changed; + bool gpuLocked = false; + bool releaseGpu = false; + std::thread holder([&] { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + { + std::lock_guard lock(mutex); + gpuLocked = true; + } + changed.notify_all(); + std::unique_lock lock(mutex); + changed.wait(lock, [&] { return releaseGpu; }); + }); + { + std::unique_lock lock(mutex); + changed.wait(lock, [&] { return gpuLocked; }); + } + std::exception_ptr flushFailure; + std::thread flusher([&] { + try { AgcDriver::Graphics::StorageTexture::FlushPending(address, SurfaceBytes, nullptr, "concurrent flush test"); } + catch (...) { flushFailure = std::current_exception(); } + }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (AgcDriver::Graphics::StorageTexture::FindPending(address, SurfaceBytes) != nullptr && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + const bool taken = AgcDriver::Graphics::StorageTexture::FindPending(address, SurfaceBytes) == nullptr; + std::vector seen(SurfaceBytes); + std::atomic readDone = false; + std::exception_ptr readFailure; + std::thread reader; + if (taken) { + reader = std::thread([&] { + try { + AgcDriver::Graphics::StorageTexture::FlushPending(address, SurfaceBytes, nullptr, "concurrent read test"); + AgcDriver::GuestMemory::Read(address, seen); + } catch (...) { + readFailure = std::current_exception(); + } + readDone.store(true, std::memory_order_release); + changed.notify_all(); + }); + std::unique_lock lock(mutex); + changed.wait_for(lock, std::chrono::milliseconds(50), [&] { return readDone.load(std::memory_order_acquire); }); + releaseGpu = true; + } else { + std::lock_guard lock(mutex); + releaseGpu = true; + } + changed.notify_all(); + holder.join(); + flusher.join(); + if (reader.joinable()) reader.join(); + Require(taken, "file write: concurrent flush did not take the pending image"); + if (flushFailure != nullptr) std::rethrow_exception(flushFailure); + if (readFailure != nullptr) std::rethrow_exception(readFailure); + Require(std::any_of(seen.begin(), seen.end(), [](std::byte value) { return value != std::byte{}; }), "file write: concurrent guest read returned before an overlapping image was stored"); +} + +void CheckShutdownAndGuard() { + OutputFile file; + std::vector source(TransferBytes, std::byte{0x5a}); + AgcDriver::GuestMemory::SetFlushHook(nullptr); + AgcDriver::GuestMemory::SetReadGuard([](std::uint64_t, std::size_t) { throw std::runtime_error("file source read guard"); }); + const auto descriptor = file.Open(); + bool refused = false; + try { sceKernelWrite(descriptor, source.data(), source.size()); } + catch (const std::runtime_error& error) { refused = std::string(error.what()) == "file source read guard"; } + AgcDriver::GuestMemory::SetReadGuard(nullptr); + Require(refused && std::filesystem::file_size(file.Name()) == 0, "file write: source read guard was bypassed before the file changed"); + Require(sceKernelWrite(descriptor, source.data(), source.size()) == static_cast(source.size()), "file write: ordinary source failed after shutdown and hook reset"); + Require(sceKernelClose(descriptor) == 0, "file write: cannot close the shutdown output"); + Require(file.Read(0) == source, "file write: ordinary output changed after shutdown and hook reset"); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + GuestSurface surface; + GuestSurface unrelated; + CheckConcurrentFlushWait(surface); + Produce(unrelated.Data()); + bool correct = true; + const std::array, 12> cases{{ + {Api::Write, "sceKernelWrite"}, {Api::PosixWrite, "write"}, {Api::CrtWrite, "_write"}, + {Api::Pwrite, "sceKernelPwrite"}, {Api::PosixPwrite, "pwrite"}, + {Api::Writev, "sceKernelWritev"}, {Api::PosixWritev, "writev"}, + {Api::Pwritev, "sceKernelPwritev"}, {Api::PosixPwritev, "pwritev"}, + {Api::Aio, "AIO"}, {Api::AioMultiple, "AIO multiple"}, {Api::Fwrite, "fwrite"}, + }}; + for (const auto& [api, name] : cases) { + correct = CheckWrite(api, name, surface, unrelated) && correct; + } + Require(correct, "file write: pending GPU results were not stored before the host read"); + std::vector unused(SurfaceBytes); + AgcDriver::GuestMemory::Read(reinterpret_cast(unrelated.Data()), unused); + AgcDriverShutdown_nid_postfix(); + CheckShutdownAndGuard(); + 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/StorageFlushOrdering.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageFlushOrdering.cpp new file mode 100644 index 0000000000..ec90020045 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageFlushOrdering.cpp @@ -0,0 +1,173 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.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 { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::size_t BlockBytes = 1u << 20u; +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::size_t ImageBytes = Width * Height * 4u; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Initial = 0x3f800000u; +constexpr std::uint32_t Stored = 0x11111111u; +constexpr std::uint32_t Overwritten = 0x22222222u; +constexpr std::size_t OverwrittenDword = 0x100u / 4u; +constexpr std::size_t FarDword = 100u * Width; + +alignas(256) constexpr std::array StoreImageCode{0x7e000280u, 0x7e020280u, 0x7e0402ffu, Stored, 0xf0200108u, 0x00010200u, 0x7e0202ffu, 0x00000064u, 0xf0200108u, 0x00010200u, 0xbf810000u}; +alignas(256) constexpr std::array StoreGlobalCode{0x7e000208u, 0x7e020209u, 0x7e0402ffu, Overwritten, 0xdc708100u, 0x007d0200u, 0xbf810000u}; + +class GuestBlock { +public: + explicit GuestBlock(bool watched) : watched(watched) { +#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); + if (watched) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "storage flush ordering: cannot allocate the guest block"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), BlockBytes), "storage flush ordering: the guest block is not write-watched"); + std::fill(Words(), Words() + BlockBytes / 4u, Initial); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(Address(), BlockBytes); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); +#else + munmap(block, BlockBytes); + if (watched) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint32_t* Words() const { return reinterpret_cast(block); } + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + bool watched; + std::uint8_t* block = nullptr; +}; + +ShaderRecompiler::RecompileResult Compile(std::span code, std::span userData, const ShaderRecompiler::SpirvTarget& target) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {64u, 0, userData, compute, std::nullopt, std::nullopt, memory}, + target, + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +std::vector ImageUserData(std::uint64_t base) { + std::vector userData(12, 0u); + userData[4] = static_cast(base >> 8u); + userData[5] = static_cast((base >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Width - 1u) & 3u) << 30u); + userData[6] = ((Width - 1u) >> 2u) | ((Height - 1u) << 14u); + userData[7] = 0xfacu | (Type2D << 28u); + return userData; +} + +std::vector GlobalUserData(std::uint64_t base) { + std::vector userData(10, 0u); + userData[8] = static_cast(base); + userData[9] = static_cast(base >> 32u); + return userData; +} + +bool AddressBasedStorePending(std::uint64_t image, bool open) { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto* recorder = AgcDriver::Graphics::Recorder::Active(); + const auto read = recorder != nullptr ? recorder->DescribePendingRead(image + ImageBytes, 4u) : std::nullopt; + return read.has_value() && read->kind == AgcDriver::Graphics::Recorder::ReadKind::AddressBased && read->open == open; +} + +void Run(AgcDriver::VulkanDevice& device, const GuestBlock& guest, std::size_t offset, bool submit, const char* name) { + const auto* words = guest.Words() + offset / 4u; + const auto image = guest.Address() + offset; + const auto target = device.Target(); + const auto storeImage = Compile(StoreImageCode, ImageUserData(image), target); + const auto storeGlobal = Compile(StoreGlobalCode, GlobalUserData(image), target); + device.Dispatch(storeImage, 1u, 1u, 1u, {}, reinterpret_cast(StoreImageCode.data())); + device.Dispatch(storeGlobal, 1u, 1u, 1u, {}, reinterpret_cast(StoreGlobalCode.data())); + if (submit) { + std::uint64_t serial = 0; + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + serial = device.SubmitAndEpoch(); + } + device.WaitRecorded(serial); + } + const std::string what = std::string("storage flush ordering, ") + name + ": "; + Require(AddressBasedStorePending(image, !submit), what + "the address-based store was not pending in the expected batch at the flush"); + Require(AgcDriver::Graphics::StorageTexture::FlushPending(image, ImageBytes, nullptr, "test"), what + "the image store left no results pending"); + device.WaitIdle(); + Require(words[FarDword] == Stored, what + "the pending image's texel away from the newer write was not stored"); + Require(words[OverwrittenDword] == Overwritten, what + "the pending image's store put its older texels over a newer address-based store"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const bool watched = AgcDriver::GuestMemory::WriteWatched(); + GuestBlock guest(watched); + if (!watched) std::puts("guest memory has no write watch: the writer-in-flight case is skipped"); + else if (!device->CanWaitUnlocked()) std::puts("the recorder cannot wait for the GPU without running completions: the writer-in-flight case is skipped"); + else Run(*device, guest, 0, true, "writer in flight"); + Run(*device, guest, BlockBytes / 2u, false, "writer in the open batch"); + std::puts("storage flush ordering 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/StorageReadDuringIdleStore.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageReadDuringIdleStore.cpp new file mode 100644 index 0000000000..ece9a4e874 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageReadDuringIdleStore.cpp @@ -0,0 +1,159 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +constexpr std::uint32_t Width = 128; +constexpr std::uint32_t Height = 120; +constexpr std::size_t SurfaceBytes = 0x20000; +constexpr int Rounds = 20; +alignas(256) constexpr std::array WriteCode{ + 0xd7460004u, 0x0401060eu, 0xd7460005u, 0x0405060fu, 0x7e100d04u, 0x7e120d05u, + 0x7e140c0du, 0x7e1602f2u, 0xf0200f08u, 0x00000804u, 0xbf810000u, +}; + +class GuestSurface { +public: + GuestSurface() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, SurfaceBytes)); + GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(mapped != MAP_FAILED, "idle store read: cannot allocate the guest surface"); + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + SurfaceBytes - 1) & ~(std::uintptr_t{SurfaceBytes - 1}); + if (aligned != begin) munmap(mapped, aligned - begin); + const auto tail = begin + SurfaceBytes * 2 - aligned - SurfaceBytes; + if (tail != 0) munmap(reinterpret_cast(aligned + SurfaceBytes), tail); + block = reinterpret_cast(aligned); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes); +#endif + GuestAllocations::Mutation mutation; + mutation.Add(block, SurfaceBytes, true, true, true); + } + + ~GuestSurface() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); +#else + munmap(block, SurfaceBytes); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, SurfaceBytes); +#endif + } + + GuestSurface(const GuestSurface&) = delete; + GuestSurface& operator=(const GuestSurface&) = delete; + std::uint8_t* Data() { return block; } + std::uint64_t Address() const { return reinterpret_cast(block); } + +private: + std::uint8_t* block = nullptr; +}; + +void Produce(GuestSurface& surface) { + std::memset(surface.Data(), 0, SurfaceBytes); + const auto address = surface.Address(); + const std::array data{ + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (71u << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (0x09u << 20u) | (9u << 28u), + 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 7u, + }; + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{8, 8, 1}; + registers(0x207, shape); + const auto code = reinterpret_cast(WriteCode.data()); + const std::array program{static_cast(code >> 8u), static_cast(code >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (7u << 7u) | (1u << 11u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, Width / 8u, Height / 8u, 1u, 0x41u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverSuspendPoint_nid_postfix(); + AgcDriverWaitIdle_nid_postfix(); + Require(StorageTexture::FindPending(surface.Address(), SurfaceBytes) != nullptr, "idle store read: the GPU producer left no pending image"); +} + +bool AllZero(std::span bytes) { + return std::none_of(bytes.begin(), bytes.end(), [](std::byte value) { return value != std::byte{}; }); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + GuestSurface surface; + int stale = 0; + for (int round = 0; round < Rounds; ++round) { + Produce(surface); + std::atomic started{false}; + std::thread idle([&] { + started = true; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + StorageTexture::FlushAllPending("idle"); + }); + while (!started) {} + while (StorageTexture::FindPending(surface.Address(), SurfaceBytes) != nullptr) {} + std::vector seen(SurfaceBytes); + AgcDriver::GuestMemory::Read(surface.Address(), seen); + idle.join(); + stale += AllZero(seen); + std::vector stored(SurfaceBytes); + AgcDriver::GuestMemory::Read(surface.Address(), stored); + Require(!AllZero(stored), "idle store read: the GPU producer wrote only zeros"); + } + std::printf("idle store read: %d of %d host reads saw the memory before the store\n", stale, Rounds); + Require(stale == 0, "idle store read: a host read did not wait for the image another thread was storing"); + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << "\n"; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackNoWindow.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackNoWindow.cpp new file mode 100644 index 0000000000..de8bb3a7d9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackNoWindow.cpp @@ -0,0 +1,179 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::DescribeSurface; +using AgcDriver::Graphics::GuestTextureResource; +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using AgcDriver::Graphics::TextureDetiler; +using AgcDriver::Graphics::TextureDimension; +using AgcDriver::Graphics::TextureTileMode; + +// A thin 3D surface (SW_64KB_R_X, the branch a volume keeps its depth slices in) whose mip tail +// starts at level 1: with 16 depth slices, level 1's blocks exist for the first 8 slices only +// (HasLayer's taper) while the flat 64 KiB unit grid spans all 16. The first 64 KiB unit of every +// slice past the taper start therefore lies outside every (slice, level) window the write-back +// retiles through, and slices 0..7 have both their units inside windows. +constexpr std::uint32_t Width = 128; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t Depth = 16; +constexpr std::uint32_t MipCount = 5; +constexpr std::uint32_t Format32Float = 22; +constexpr std::size_t UnitBytes = 65536; +constexpr std::uint8_t Sentinel = 0xa5; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); + return text; +} + +class GuestBlock { +public: + explicit GuestBlock(std::size_t bytes) : bytes(bytes) { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, UnitBytes)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* mapped = mmap(nullptr, bytes + UnitBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + UnitBytes - 1) & ~(static_cast(UnitBytes) - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + bytes != begin + bytes + UnitBytes) munmap(reinterpret_cast(aligned + bytes), begin + UnitBytes - aligned); + block = reinterpret_cast(aligned); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); + } +#endif + Require(block != nullptr, "storage write-back no-window: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, bytes); + GuestArena::GuestArenaRelease_nid_postfix(block, bytes); +#else + munmap(block, bytes); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, bytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; + std::size_t bytes = 0; +}; + +void Run(AgcDriver::VulkanDevice& device) { + GuestTextureResource resource{}; + resource.width = Width; + resource.height = Height; + resource.depthOrLastArray = Depth - 1u; + resource.mipCount = MipCount; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k3D; + resource.format = Format32Float; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + Require(geometry.guestBytes >= Depth * UnitBytes, "storage write-back no-window: the fixture's slices are smaller than a 64 KiB unit"); + + GuestBlock block(static_cast(geometry.guestBytes)); + resource.baseAddress = AddressOf(block.Data()); + Require(resource.baseAddress % UnitBytes == 0, "storage write-back no-window: the guest block is not 64 KiB aligned"); + std::memset(block.Data(), Sentinel, static_cast(geometry.guestBytes)); + AgcDriver::GuestMemory::CollectWrites(resource.baseAddress, static_cast(geometry.guestBytes)); + AgcDriver::GuestMemory::BumpCollectEpoch(); + + auto context = device.graphicsContext(); + TextureDetiler detiler(context); + context.detiler = &detiler; + const auto texture = StorageTexture::Create(context, detiler, resource, 0); + texture->MarkDirty(); + Require(StorageTexture::FindPending(resource.baseAddress, geometry.guestBytes) != nullptr, "storage write-back no-window: the marked image is not pending"); + + // The (slice, level) windows sliceWindows would retile a kept unit through: the mip levels of + // every depth slice HasLayer keeps, each at its tiled offset in the slice. + std::vector> windows; + for (std::uint32_t layer = 0; layer < geometry.layers; ++layer) { + for (std::uint32_t level = 0; level < geometry.mips.size(); ++level) { + if (!geometry.HasLayer(level, layer)) continue; + const auto begin = resource.baseAddress + geometry.GuestLayerOffset(layer) + geometry.mips[level].tiledOffset; + windows.emplace_back(begin, begin + geometry.mips[level].tiledSize); + } + } + Require(!windows.empty(), "storage write-back no-window: the fixture has no slice windows"); + + // Every 64 KiB unit of the flat tracking grid is stored on its own: a unit inside a window + // stores through the retiler, one outside every window must store nothing and leave the guest + // bytes alone instead of sizing the retiler's buffers at zero. + std::size_t units = 0, outside = 0; + for (auto begin = resource.baseAddress; begin < resource.baseAddress + geometry.guestBytes; begin += UnitBytes) { + const auto end = std::min(begin + UnitBytes, resource.baseAddress + geometry.guestBytes); + const auto bytes = static_cast(end - begin); + const bool inWindow = std::any_of(windows.begin(), windows.end(), [&](const std::pair& window) { return begin < window.second && window.first < end; }); + Require(StorageTexture::FlushPending(begin, bytes, nullptr, "test"), "storage write-back no-window: the unit at +" + Hex(begin - resource.baseAddress) + " was not listed by its flush"); + device.WaitIdle(); + Require(!StorageTexture::FlushPending(begin, bytes, nullptr, "test"), "storage write-back no-window: the unit at +" + Hex(begin - resource.baseAddress) + " stayed pending after its flush"); + ++units; + if (inWindow) continue; + ++outside; + const bool untouched = std::all_of(block.Data() + (begin - resource.baseAddress), block.Data() + (end - resource.baseAddress), [](std::uint8_t byte) { return byte == Sentinel; }); + Require(untouched, "storage write-back no-window: the flush of the unit at +" + Hex(begin - resource.baseAddress) + ", which lies outside every slice window, wrote to its guest bytes"); + } + Require(outside != 0, "storage write-back no-window: the thin volume fixture has no unit outside every slice window"); + Require(!texture->Dirty(), "storage write-back no-window: flushing every unit left the image pending"); + std::printf("storage write-back no-window tests passed (%zu units, %zu outside every window)\n", units, outside); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp new file mode 100644 index 0000000000..9dd13a3b30 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp @@ -0,0 +1,171 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 128; +constexpr std::uint32_t Height = 120; +constexpr std::uint32_t FormatRgba16Float = 71; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Standard64KB = 0x09; +constexpr std::size_t SurfaceBytes = 0x20000; +constexpr std::size_t AllocationBytes = std::size_t{Width} * Height * 8u; +constexpr std::uint8_t Sentinel = 0xa5; + +alignas(256) constexpr std::array WriteCode{0xd7460004u, 0x0401060eu, 0xd7460005u, 0x0405060fu, 0x7e100d04u, 0x7e120d05u, 0x7e140c0du, 0x7e1602f2u, 0xf0200f08u, 0x00000804u, 0xbf810000u}; + +std::array TextureDescriptor(const void* texels) { + const auto address = reinterpret_cast(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (FormatRgba16Float << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (Standard64KB << 20u) | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +void Write(std::uint8_t* texels) { + std::vector data; + const auto texture = TextureDescriptor(texels); + data.insert(data.end(), texture.begin(), texture.end()); + data.resize(14, 0u); + data[13] = 7u; + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{8, 8, 1}; + registers(0x207, shape); + const auto code = reinterpret_cast(WriteCode.data()); + const std::array program{static_cast(code >> 8u), static_cast(code >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (7u << 7u) | (1u << 11u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, Width / 8u, Height / 8u, 1u, 0x41u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::Graphics::StorageTexture::FlushAllPending("test"); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(texels), SurfaceBytes); +} + +class GuestSurface { +public: + explicit GuestSurface(bool split) { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, Alignment)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes + Alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + Alignment - 1) & ~(static_cast(Alignment) - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + SurfaceBytes != begin + SurfaceBytes + Alignment) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + Alignment - aligned); + block = reinterpret_cast(aligned); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes); + } +#endif + Require(block != nullptr, "storage write-back overhang: cannot allocate the guest surface"); + std::memset(block, 0, AllocationBytes); + std::memset(block + AllocationBytes, Sentinel, SurfaceBytes - AllocationBytes); + GuestAllocations::Mutation mutation; + if (split) { + mutation.Add(block, AllocationBytes, true, true, true); + mutation.Add(block + AllocationBytes, SurfaceBytes - AllocationBytes, true, true, true); + } else { + mutation.Add(block, SurfaceBytes, true, true, true); + } + this->split = split; + } + + ~GuestSurface() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + if (split) mutation.Remove(block + AllocationBytes); + } +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); +#else + munmap(block, SurfaceBytes); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, SurfaceBytes); +#endif + } + + GuestSurface(const GuestSurface&) = delete; + GuestSurface& operator=(const GuestSurface&) = delete; + + std::uint8_t* Data() { return block; } + +private: + static constexpr std::size_t Alignment = 65536; + std::uint8_t* block = nullptr; + bool split = false; +}; + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + GuestSurface whole(false); + GuestSurface split(true); + Write(whole.Data()); + Write(split.Data()); + const auto* expected = whole.Data(); + const auto* stored = split.Data(); + const auto texel = [](const std::uint8_t* at) { return !std::all_of(at, at + 8, [](std::uint8_t byte) { return byte == 0 || byte == Sentinel; }); }; + std::size_t written = 0; + std::size_t past = 0; + for (std::size_t at = 0; at < SurfaceBytes; at += 8) { + written += texel(expected + at); + past += at >= AllocationBytes && texel(expected + at); + } + Require(written != 0, "storage write-back overhang: the fully registered surface received no texels"); + Require(past != 0, "storage write-back overhang: no texel of the fully registered surface lies past pitch x height"); + const auto differs = std::mismatch(expected, expected + SurfaceBytes, stored); + Require(differs.first == expected + SurfaceBytes, "storage write-back overhang: byte 0x" + [&] { char text[32]; std::snprintf(text, sizeof(text), "%zx", static_cast(differs.first - expected)); return std::string(text); }() + " differs from the fully registered surface (" + std::to_string(written) + " texels, " + std::to_string(past) + " past pitch x height)"); + Require(std::any_of(stored + AllocationBytes, stored + SurfaceBytes, [](std::uint8_t byte) { return byte == Sentinel; }), "storage write-back overhang: the padding past the image's rows was overwritten"); + AgcDriverShutdown_nid_postfix(); + std::puts("storage write-back overhang 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/StoreOverUntracked.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp index 99e31a350a..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 @@ -37,6 +38,8 @@ using AgcDriver::Graphics::StorageTexture; constexpr std::uint32_t Side = 256; constexpr std::uint32_t Format32UInt = 20; constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t TileLinear = 0; +std::uint32_t surfaceTile = TileR64KBX; constexpr std::uint32_t Type2D = 9; constexpr std::size_t Page = 0x1000; constexpr std::size_t Block = 0x10000; @@ -44,15 +47,24 @@ constexpr std::size_t SurfaceBytes = 4 * Block; constexpr std::uint32_t Untouched = 0xdeadbeefu; constexpr std::uint32_t Stored = 0x12345678u; constexpr std::uint32_t Filled = 0x9abcdef0u; +constexpr std::uint32_t StoredAgain = 0x0badf00du; alignas(256) constexpr std::array StoreCode{ 0x7e040300, 0x7e060280, 0x7e0202ff, Stored, 0xf0200108, 0x00020102, 0xbf810000, }; +alignas(256) constexpr std::array StoreAgainCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, StoredAgain, 0xf0200108, 0x00020102, 0xbf810000, +}; + alignas(256) constexpr std::array FillCode{ 0xd7460004, 0x04010c08, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xbf810000, }; +alignas(256) constexpr std::array ReadCode{ + 0x7e020280, 0x7e04020c, 0xf0001108, 0x00010300, 0xbf8c3f70, 0x34080082, 0xe0701000, 0x80000304, 0xbf810000, +}; + std::uint64_t AddressOf(const void* data) { return static_cast(reinterpret_cast(data)); } @@ -63,7 +75,7 @@ std::array TextureDescriptor(const std::uint32_t* texels, std: static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((side - 1u) & 3u) << 30u), ((side - 1u) >> 2u) | ((side - 1u) << 14u), - 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0xfacu | (surfaceTile << 20u) | (Type2D << 28u), 0u, 0u, 0u, @@ -91,11 +103,11 @@ void Dispatch(std::span code, std::uint32_t threads, std::u AgcDriver::Submit(&packet, 0); } -void Store(const std::uint32_t* texels, std::uint32_t side = Side) { +void Store(const std::uint32_t* texels, std::uint32_t side = Side, std::span code = StoreCode) { std::array userData{}; const auto texture = TextureDescriptor(texels, side); std::copy(texture.begin(), texture.end(), userData.begin() + 8); - Dispatch(StoreCode, 8, static_cast(userData.size()) << 1u, userData, 1); + Dispatch(code, 8, static_cast(userData.size()) << 1u, userData, 1); } void Fill(const std::uint32_t* texels) { @@ -113,42 +125,58 @@ void Fill(const std::uint32_t* texels) { Dispatch(FillCode, 64, (static_cast(userData.size()) << 1u) | 0x80u, userData, static_cast(SurfaceBytes / 1024u)); } +std::array BufferDescriptor(const void* data, std::size_t bytes) { + const auto address = AddressOf(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(bytes), 0x31016facu}; +} + +void ReadImage(const std::uint32_t* texels, std::uint32_t* output) { + std::array userData{}; + const auto buffer = BufferDescriptor(output, SurfaceBytes); + const auto texture = TextureDescriptor(texels); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + Dispatch(ReadCode, 32, static_cast(userData.size()) << 1u, userData, 1); +} + class Surface { public: - explicit Surface(bool watched = false) : watched(watched) { + explicit Surface(bool watched = false, std::size_t lead = 0) : watched(watched), regionBytes(lead == 0 ? SurfaceBytes : SurfaceBytes + Block) { if (watched) { #ifdef _WIN32 - texels = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, Block)); - if (texels != nullptr) GuestArena::GuestArenaCommit_nid_postfix(texels, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); + region = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(regionBytes, Block)); + if (region != nullptr) GuestArena::GuestArenaCommit_nid_postfix(region, regionBytes, PAGE_READWRITE, regionBytes); #else - void* mapped = mmap(nullptr, SurfaceBytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + void* mapped = mmap(nullptr, regionBytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (mapped != MAP_FAILED) { const auto begin = reinterpret_cast(mapped); const auto aligned = (begin + Block - 1) & ~std::uintptr_t{Block - 1}; if (aligned != begin) munmap(mapped, aligned - begin); - if (aligned != begin + Block) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + Block - aligned); - texels = reinterpret_cast(aligned); - GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(texels, SurfaceBytes); + if (aligned != begin + Block) munmap(reinterpret_cast(aligned + regionBytes), begin + Block - aligned); + region = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(region, regionBytes); } #endif + if (region != nullptr) texels = reinterpret_cast(region + lead); } else { storage.resize((SurfaceBytes + Block) / 4u); texels = reinterpret_cast((reinterpret_cast(storage.data()) + Block - 1) & ~std::uintptr_t{Block - 1}); } Require(texels != nullptr, "store over untracked: cannot allocate the guest surface"); std::fill(texels, texels + SurfaceBytes / 4u, Untouched); - GuestAllocations::Mutation().Add(texels, SurfaceBytes, true, true, true); + allocation = region != nullptr ? static_cast(region) : static_cast(texels); + GuestAllocations::Mutation().Add(allocation, region != nullptr ? regionBytes : SurfaceBytes, true, true, true); } ~Surface() { - GuestAllocations::Mutation().Remove(texels); + GuestAllocations::Mutation().Remove(allocation); if (!watched) return; #ifdef _WIN32 - GuestArena::GuestArenaReset_nid_postfix(texels, SurfaceBytes); - GuestArena::GuestArenaRelease_nid_postfix(texels, SurfaceBytes); + GuestArena::GuestArenaReset_nid_postfix(region, regionBytes); + GuestArena::GuestArenaRelease_nid_postfix(region, regionBytes); #else - munmap(texels, SurfaceBytes); - GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(texels, SurfaceBytes); + munmap(region, regionBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(region, regionBytes); #endif } @@ -161,7 +189,10 @@ public: private: bool watched; + std::size_t regionBytes; std::vector storage; + std::uint8_t* region = nullptr; + void* allocation = nullptr; std::uint32_t* texels = nullptr; }; @@ -190,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"); @@ -205,7 +237,35 @@ void Settle(THold& hold, const Surface& surface, std::uint32_t result) { hold.unlock(); } -enum class Kind { ReadAfter, ReadBefore, Filled, Copied, LeftWatch, WrittenThenLeftWatch, ImportLeftWatch, NeighbourCopied }; +void PendingIdentity() { + Surface pending; + Surface completed; + std::shared_ptr image; + struct WaitForWork { + ~WaitForWork() { AgcDriverWaitIdle_nid_postfix(); } + } waitForWork; + Store(pending.Texels()); + AgcDriverWaitIdle_nid_postfix(); + std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex()); + image = StorageTexture::FindLive(pending.Address(), SurfaceBytes); + Require(image != nullptr, "store over untracked: the identity test has no live image"); + StorageTexture::FlushAllPending("pending identity fixture"); + image->MarkDirty(); + Require(StorageTexture::FindPending(pending.Address(), SurfaceBytes) == image, "store over untracked: the identity test did not make its image pending"); + Require(StorageTexture::FindLive(completed.Address(), SurfaceBytes) == nullptr && StorageTexture::FindPending(completed.Address(), SurfaceBytes) == nullptr, "store over untracked: the completed surface unexpectedly has an image"); + hold.unlock(); + Require(HoldPending(hold, pending, Stored), "store over untracked: the wait ignored its own pending image"); + Require(hold.owns_lock(), "store over untracked: the pending image was not held under the GPU mutex"); + std::fill(completed.Texels(), completed.Texels() + SurfaceBytes / 4u, Stored); + hold.unlock(); + const bool held = HoldPending(hold, completed, Stored); + if (!hold.owns_lock()) hold.lock(); + StorageTexture::FlushAllPending("pending identity fixture cleanup"); + hold.unlock(); + Require(!held, "store over untracked: the wait accepted another surface's pending image"); +} + +enum class Kind { ReadAfter, ReadBefore, Filled, Copied, LeftWatch, WrittenThenLeftWatch, ImportLeftWatch, NeighbourCopied, NeighbourLeftWatch, UntrackedRepeat }; // One 64 KiB block of the surface: the image of its last block, and the source copied into it. constexpr std::uint32_t BlockSide = 128; @@ -219,11 +279,79 @@ struct Case { enum class Outcome { Kept, Lost, Missed }; +// The surface is outside the write watch from the start. Its first results are stored, which +// leaves their bytes saved for the next store to compare with; the CPU writes another block, then +// the next results land and are stored: the CPU's block was written after the first store, so it +// keeps the CPU's bytes as the write watch would keep them. +Outcome RunSecondStore(const char* name) { + Surface surface(true); + struct WaitForWork { + ~WaitForWork() { AgcDriverWaitIdle_nid_postfix(); } + } waitForWork; + const auto address = surface.Address(); + if (!AgcDriver::GuestMemory::Watched(address, SurfaceBytes)) { + std::fprintf(stderr, "[store over untracked] %s: not run, the surface is not write-watched\n", name); + return Outcome::Kept; + } + AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes); + Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes), "store over untracked: the surface is still write-watched"); + std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex(), std::defer_lock); + const auto heldLive = [&](std::uint32_t result) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (true) { + hold.lock(); + 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"); + std::this_thread::yield(); + } + }; + Store(surface.Texels()); + if (!heldLive(Stored)) return Outcome::Missed; + StorageTexture::FlushAllPending("store over untracked fixture"); + hold.unlock(); + { + std::vector first(SurfaceBytes); + AgcDriver::GuestMemory::Read(address, first); + } + Require(static_cast(surface.Texels())[0] == Stored, "store over untracked: the first result was not stored"); + hold.lock(); + const auto image = StorageTexture::FindLive(address, SurfaceBytes); + const bool saved = image != nullptr && image->GuestSnapshotValid(); + hold.unlock(); + Require(image != nullptr, "store over untracked: the surface has no storage image"); + if (!saved) { + std::fprintf(stderr, "[store over untracked] %s: the store left no saved guest bytes\n", name); + return Outcome::Lost; + } + auto* written = surface.Bytes() + 2 * Block; + for (std::size_t offset = 0; offset < Page; ++offset) static_cast(written)[offset] = Pattern(offset); + Store(surface.Texels(), Side, StoreAgainCode); + if (!heldLive(StoredAgain)) return Outcome::Missed; + StorageTexture::FlushAllPending("store over untracked fixture"); + hold.unlock(); + std::vector whole(SurfaceBytes); + AgcDriver::GuestMemory::Read(address, whole); + Require(static_cast(surface.Texels())[0] == StoredAgain, "store over untracked: the second result did not reach a block the CPU left alone"); + const auto lost = LostBytes(written); + if (lost == 0) return Outcome::Kept; + std::fprintf(stderr, "[store over untracked] %s: of 0x%zx bytes the CPU wrote between the two stores, %zu were lost\n", name, Page, lost); + return Outcome::Lost; +} + Outcome Run(const Case& test) { + std::optional output; std::optional source; if (test.kind == Kind::Copied || test.kind == Kind::NeighbourCopied) source.emplace(); - const bool leaves = test.kind == Kind::LeftWatch || test.kind == Kind::WrittenThenLeftWatch || test.kind == Kind::ImportLeftWatch; - Surface surface(leaves); + const bool neighbour = test.kind == Kind::NeighbourLeftWatch; + const bool leaves = test.kind == Kind::LeftWatch || test.kind == Kind::WrittenThenLeftWatch || test.kind == Kind::ImportLeftWatch || neighbour; + surfaceTile = neighbour ? TileLinear : TileR64KBX; + Surface surface(leaves, neighbour ? Page : 0); + struct WaitForWork { + ~WaitForWork() { AgcDriverWaitIdle_nid_postfix(); } + } waitForWork; auto* bytes = surface.Bytes(); const auto address = surface.Address(); std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex(), std::defer_lock); @@ -234,6 +362,16 @@ Outcome Run(const Case& test) { Settle(hold, *source, Filled); } Store(surface.Texels()); + if (test.kind == Kind::UntrackedRepeat) { + Settle(hold, surface, Stored); + output.emplace(); + ReadImage(surface.Texels(), output->Texels()); + hold.lock(); + const auto image = StorageTexture::FindLive(surface.Address(), SurfaceBytes); + Require(image != nullptr, "store over untracked: the sampled surface has no live storage image"); + image->MarkDirty(); + hold.unlock(); + } if (test.kind == Kind::Filled || leaves) { Settle(hold, surface, Stored); if (leaves) { @@ -276,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); @@ -315,8 +451,9 @@ Outcome Run(const Case& test) { Require(image != nullptr, "store over untracked: the surface has no storage image"); Require(!image->GuestSnapshotValid(), "store over untracked: the pending result had saved guest bytes while its memory was write-watched"); if (test.kind == Kind::ImportLeftWatch) Require(AgcDriver::GuestMemory::ImportWatched(address, SurfaceBytes, [] { return true; }, false), "store over untracked: the import that leaves the write watch failed"); + else if (neighbour) Require(AgcDriver::GuestMemory::ImportWatched(address - Page, Page, [] { return true; }, false), "store over untracked: the import of the page before the surface failed"); else AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes); - Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes), "store over untracked: the surface is still write-watched"); + if (!neighbour) Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes), "store over untracked: the surface is still write-watched"); } std::array read{}; if (test.kind == Kind::ReadBefore) AgcDriver::GuestMemory::Read(address + Block + Page, read); @@ -335,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; @@ -344,11 +482,17 @@ int main() { struct ShutdownGuard { ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } } shutdown; + if (argc == 2) { + PendingIdentity(); + AgcDriverShutdown_nid_postfix(); + std::puts("pending surface identity tests passed"); + return 0; + } { Surface probe; Require(AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(probe.Texels()))).guestBytes == SurfaceBytes, "store over untracked: unexpected surface size"); } - constexpr std::array cases{{ + constexpr std::array cases{{ {"another block read after the CPU write", Kind::ReadAfter, Block + Page}, {"another block read before the CPU write", Kind::ReadBefore, 0xe000}, {"a fill of the surface", Kind::Filled, 0xe000}, @@ -357,6 +501,8 @@ int main() { {"a surface written before it left the write watch", Kind::WrittenThenLeftWatch, 0xe000}, {"a surface whose import left the write watch", Kind::ImportLeftWatch, 0xe000}, {"an image copy into another block of the surface", Kind::NeighbourCopied, 0xe000}, + {"a surface whose first block left the write watch with the page before it", Kind::NeighbourLeftWatch, 0xe000}, + {"untracked storage results written again after an image read", Kind::UntrackedRepeat, 0xe000}, }}; bool kept = true; for (const auto& test : cases) { @@ -365,6 +511,13 @@ int main() { Require(outcome != Outcome::Missed, "store over untracked: the result was never held pending"); kept = outcome == Outcome::Kept && kept; } + { + constexpr auto name = "a second store over a surface outside the write watch"; + auto outcome = Outcome::Missed; + for (std::uint32_t attempt = 0; attempt < 256u && outcome == Outcome::Missed; ++attempt) outcome = RunSecondStore(name); + Require(outcome != Outcome::Missed, "store over untracked: the result was never held pending"); + kept = outcome == Outcome::Kept && kept; + } Require(kept, "a pending storage result was stored over bytes the CPU wrote to untracked memory"); AgcDriverShutdown_nid_postfix(); std::puts("store over untracked tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StreamCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StreamCopy.cpp new file mode 100644 index 0000000000..65bbde4890 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StreamCopy.cpp @@ -0,0 +1,177 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::MatchesStreamCopyKernel; +using AgcDriver::DriverDetail::StreamCopyRange; + +alignas(256) constexpr std::array Code{0xd7460008u, 0x04010e08u, 0x8f6a8708u, 0xd76d0009u, 0x0401806au, 0xe00c2000u, 0x80000008u, 0xe00c2000u, 0x80000409u, 0xbf8c3f71u, 0xe01c2000u, 0x80010008u, 0xbf8c3f70u, 0xe01c2000u, 0x80010409u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t Format = 0x0004bfacu; +constexpr std::uint32_t LargeGroups = 8193; +constexpr std::size_t Words = (std::size_t{LargeGroups} * 128u + 64u) * 4u; +alignas(256) std::array Source{}; +alignas(256) std::array Destination{}; + +std::vector UserData(const std::uint32_t* source, std::uint32_t sourceRecords, const std::uint32_t* destination, std::uint32_t destinationRecords, std::uint32_t sourceStride = 16, std::uint32_t destinationStride = 16) { + const auto from = reinterpret_cast(source); + const auto to = reinterpret_cast(destination); + return {static_cast(from), static_cast((from >> 32u) & 0xffffu) | (sourceStride << 16u), sourceRecords, Format, + static_cast(to), static_cast((to >> 32u) & 0xffffu) | (destinationStride << 16u), destinationRecords, Format}; +} + +void MatchTests() { + const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1}; + const auto data = UserData(Source.data(), 512, Destination.data(), 512); + Require(MatchesStreamCopyKernel(Code, data, compute), "the stream copy kernel was not recognized"); + for (std::size_t word = 0; word < Code.size(); ++word) { + auto altered = Code; + altered[word] ^= 1u; + Require(!MatchesStreamCopyKernel(altered, data, compute), "a different shader matched the stream copy kernel (word " + std::to_string(word) + ")"); + } + Require(!MatchesStreamCopyKernel(std::span(Code).first(15), data, compute), "a truncated shader matched"); + Require(!MatchesStreamCopyKernel(Code, UserData(Source.data(), 512, Destination.data(), 512, 4), compute), "a source stride other than 16 matched"); + Require(!MatchesStreamCopyKernel(Code, UserData(Source.data(), 512, Destination.data(), 512, 16, 4), compute), "a destination stride other than 16 matched"); + auto swizzled = data; + swizzled[5] |= 0x80000000u; + Require(!MatchesStreamCopyKernel(Code, swizzled, compute), "a swizzled destination matched"); + auto formats = data; + formats[7] ^= 0x1000u; + Require(!MatchesStreamCopyKernel(Code, formats, compute), "descriptors of different formats matched"); + const auto both = [&](std::uint32_t word3) { + auto changed = data; + changed[3] = changed[7] = word3; + return MatchesStreamCopyKernel(Code, changed, compute); + }; + Require(!both((Format & ~0x7f000u) | (0x14u << 12u)), "a 32-bit format matched a copy of 16-byte elements"); + Require(!both((Format & ~0xfffu) | 0xc6cu), "a swapped destination select matched"); + Require(!both(Format | 0x00800000u), "an add-tid descriptor matched"); + Require(!both(Format | 0x30000000u), "a raw out-of-bounds mode matched"); + Require(!both(Format | 0x40000000u), "a descriptor of another type matched"); + auto extra = data; + extra.push_back(0); + Require(!MatchesStreamCopyKernel(Code, extra, compute), "a different user SGPR count matched (the group id moves)"); + auto wave32 = compute; + wave32.numThreads[0] = 32; + Require(!MatchesStreamCopyKernel(Code, data, wave32), "a group of 32 threads matched"); + auto noGroup = compute; + noGroup.groupIdEnable[0] = false; + Require(!MatchesStreamCopyKernel(Code, data, noGroup), "a dispatch without the group id matched"); + + const auto source = reinterpret_cast(Source.data()); + const auto destination = reinterpret_cast(Destination.data()); + const auto copy = StreamCopyRange(data, 4); + Require(copy && copy->source == source && copy->destination == destination && copy->bytes == 8192, "4 groups were not taken as a copy of 512 elements"); + const auto clamped = StreamCopyRange(UserData(Source.data(), 512, Destination.data(), 300), 4); + Require(clamped && clamped->bytes == 300u * 16u, "the copy was not clamped to the destination's records"); + const auto launched = StreamCopyRange(UserData(Source.data(), 600, Destination.data(), 600), 4); + Require(launched && launched->bytes == 8192, "the copy exceeded the launched threads"); + Require(!StreamCopyRange(UserData(Source.data(), 400, Destination.data(), 512), 4), "a source with fewer records than the copy was taken"); + Require(!StreamCopyRange(UserData(Source.data(), 512, Destination.data(), 0), 4), "a copy of 0 bytes was taken"); + Require(!StreamCopyRange(data, 0), "a copy of 0 groups was taken"); + Require(StreamCopyRange(UserData(Source.data(), 8192u * 128u, Destination.data(), 8192u * 128u), 8192).has_value(), "a copy of 16 MiB was refused"); + Require(!StreamCopyRange(UserData(Source.data(), LargeGroups * 128u, Destination.data(), LargeGroups * 128u), LargeGroups), "a copy above 16 MiB was taken"); + Require(!StreamCopyRange(UserData(Source.data() + 1, 512, Destination.data(), 512), 4), "a source not aligned to 16 bytes was taken"); + Require(!StreamCopyRange(UserData(Source.data(), 512, Destination.data() + 1, 512), 4), "a destination not aligned to 16 bytes was taken"); +} + +std::vector Commands(const std::vector& data, std::uint32_t groups) { + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{64, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(Code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (1u << 7u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u}); + return words; +} + +std::uint64_t Taken() { + using namespace AgcDriver::DriverDetail; + return copyPathCounts[CopyCpu].load() + copyPathCounts[CopyGpu].load() + copyPathCounts[CopyAliased].load(); +} + +void ExecuteTests() { + using AgcDriver::DriverDetail::copyPathCounts; + using AgcDriver::DriverDetail::CopyShape; + struct Case { + const char* name; + std::uint32_t groups, sourceRecords, destinationRecords, sourceOffset, destinationOffset; + bool hle; + }; + constexpr std::array cases{{ + {"4 groups", 4, 512, 512, 0, 0, true}, + {"16 groups", 16, 2048, 2048, 0, 0, true}, + {"destination records below the launch", 4, 512, 300, 0, 0, true}, + {"records above the launch", 4, 600, 600, 0, 0, true}, + {"source records below the copy", 4, 400, 512, 0, 0, false}, + {"0 destination records", 4, 512, 0, 0, 0, false}, + {"above 16 MiB", LargeGroups, LargeGroups * 128u, LargeGroups * 128u, 0, 0, false}, + {"source not aligned to 16 bytes", 4, 512, 512, 1, 0, false}, + {"destination not aligned to 16 bytes", 4, 512, 512, 0, 1, false}, + }}; + const bool hleEnabled = std::getenv("APS5_NO_COPY_HLE") == nullptr; + for (const auto& item : cases) { + const bool hle = item.hle && hleEnabled; + for (std::size_t i = 0; i < Source.size(); ++i) Source[i] = static_cast(i) * 2654435761u + 1u; + Destination.fill(Untouched); + const auto taken = Taken(); + const auto refused = copyPathCounts[CopyShape].load(); + const auto words = Commands(UserData(Source.data() + item.sourceOffset, item.sourceRecords, Destination.data() + item.destinationOffset, item.destinationRecords), item.groups); + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Destination.data()), sizeof(Destination)); + const std::string where = std::string(" (") + item.name + ")"; + Require(Taken() - taken == (hle ? 1u : 0u), (hle ? "the copy did not take the HLE path" : "the copy took the HLE path") + where); + Require(copyPathCounts[CopyShape].load() - refused == (hle || !hleEnabled ? 0u : 1u), (hle ? "the copy was refused for its shape" : "the copy was not refused for its shape") + where); + const auto elements = std::min(item.groups * 128u, item.destinationRecords); + for (std::size_t i = 0; i < Destination.size(); ++i) { + const bool inside = i >= item.destinationOffset && (i - item.destinationOffset) / 4u < elements; + const auto element = (i - item.destinationOffset) / 4u; + const auto expected = !inside ? Untouched : element < item.sourceRecords ? Source[item.sourceOffset + i - item.destinationOffset] : 0u; + Require(Destination[i] == expected, "stream copy mismatch at word " + std::to_string(i) + where + ": got " + std::to_string(Destination[i]) + ", expected " + std::to_string(expected)); + } + } +} + +} + +int main() { + try { + if (std::getenv("APS5_NO_COPY_HLE") == nullptr) MatchTests(); + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ExecuteTests(); + AgcDriverShutdown_nid_postfix(); + std::puts("stream copy 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/SubvectorLoop.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp index 4944237df7..bb378bac87 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "../SubvectorLoopFixture.hpp" #include "Recompiler.hpp" #include "VulkanTestDevice.hpp" #include @@ -17,6 +18,7 @@ namespace { using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; +using SubvectorLoopFixture::ExecKeptCode; constexpr std::uint32_t Threads = 64; constexpr std::uint32_t Columns = 9; @@ -25,16 +27,6 @@ constexpr std::uint32_t Marker = 0x5a5a5a5au; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array ExecKeptCode{ - 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, - 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000a, 0x80148114, 0x4a141481, - 0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850001, 0xbe26fff6, - 0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, 0x80010a01, - 0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, 0x80010e01, - 0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, 0x80011201, - 0xbf810000, -}; - alignas(256) constexpr std::array ExecResetCode{ 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000b, 0x80148114, 0x4a141481, diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index c016c7423a..8eee7b840b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -22,7 +22,9 @@ #include #include #include +#include #include +#include namespace { @@ -117,6 +119,21 @@ alignas(256) constexpr std::array DomainCode{ 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, }; +alignas(256) constexpr std::array DomainComputedPoint{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xbf8c3f70, 0x7c0214ff, + 0x7f7fffff, 0xd5010002, 0x01a90280, 0xd7460002, 0x04050902, 0xe0341010, 0x04030c02, 0xe0341020, + 0x04030e02, 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, 0x10221705, + 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, 0x40400000, 0x7e260280, 0x7e2802f2, + 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, +}; + +alignas(256) constexpr std::array DomainLoweredInterior{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, + 0xe0341020, 0x04030e01, 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, + 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, 0x40400000, 0x08222511, + 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, +}; + alignas(256) constexpr std::array DomainRelativeData{ 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0x4a2a0e81, 0x7e2a0d15, 0x102a2aff, 0x3e000000, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0xbf8c3f70, 0x08040af2, @@ -318,6 +335,90 @@ alignas(256) constexpr std::array QuadDomainCode{ 0x7e3402f2, 0xf80008cf, 0x1a191716, 0xf800020f, 0x1a19181a, 0xbf810000, }; +alignas(256) constexpr std::array MergedHullCode{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001c, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, 0x000100c1, + 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, + 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, 0x162c2890, + 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, 0xbe94047e, + 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80048, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, + 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, + 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array MergedHullRegisters{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001c, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, 0x000100c1, + 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, + 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, 0x162c2890, + 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0741000, 0x02020816, 0x7da82881, 0xbe94047e, + 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80048, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, + 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, + 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array MergedHullForeignRecord{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001c, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340020, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, 0x000100c1, + 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, + 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, 0x162c2890, + 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, 0xbe94047e, + 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80048, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, + 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, + 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array MergedHullDivergentBarrier{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001d, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8a0000, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, + 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, + 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, + 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, + 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80048, 0x06000013, 0xd8dc1008, 0x07000013, + 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, + 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array MergedHullOutsidePatch{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001c, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, 0x000100c1, + 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, + 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, 0x162c2890, + 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, 0xbe94047e, + 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80060, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, + 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, + 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array MergedHullReadLane{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001c, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, 0x000100c1, + 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, + 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, 0x162c2890, + 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, 0xbe94047e, + 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80048, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, + 0xd7600010, 0x02010106, 0x7e300210, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, + 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + alignas(256) constexpr std::array PixelCode{ 0xc8020002, 0xc8060102, 0xc80a0202, 0xc80e0302, 0xf800180f, 0x03020100, 0xbf810000, }; @@ -325,6 +426,7 @@ alignas(256) constexpr std::array PixelCode{ using ShaderRecompiler::TessellationConfiguration; constexpr TessellationConfiguration Patches{3u, 3u, 1u, 2u, 2u}; +constexpr TessellationConfiguration MergedPatches{3u, 3u, 1u, 2u, 2u, 256u, 0u, false, 0u, false, true}; constexpr std::array PatchLevels{1u, 0u, 5u}; constexpr std::uint16_t Corner(std::uint32_t patch, std::uint32_t x, std::uint32_t y) { @@ -359,6 +461,8 @@ struct PatchDraw { std::span indices; float minLevel = 1.0f; float maxLevel = 64.0f; + std::uint32_t instances = 1; + std::uint32_t firstInstance = 0; }; alignas(256) constexpr std::array NextInputIndices{ @@ -369,6 +473,7 @@ alignas(256) constexpr std::array NextInputIndices{ const PatchDraw Triangles{Patches, Indices}; const PatchDraw NextInputTriangles{{4u, 3u, 1u, 2u, 2u}, NextInputIndices}; +const PatchDraw MergedTriangles{MergedPatches, Indices}; struct Stages { ShaderRecompiler::RecompileResult local; @@ -394,7 +499,7 @@ ShaderRecompiler::RecompileResult Compile(const ShaderRecompiler::SpirvTarget& t pixel = info; } else { vertex = ShaderRecompiler::ShaderVertexStageInfo{}; - graphics = ShaderRecompiler::GraphicsCompileContext{hull ? 0u : 8u, {}, std::nullopt, patches.configuration, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, 1}}; + graphics = ShaderRecompiler::GraphicsCompileContext{hull ? 0u : 8u, {}, std::nullopt, patches.configuration, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, patches.instances}}; } auto layout = stage == ShaderStage::Fragment ? PixelPushLayout(pushCursor, target) : ShaderRecompiler::BindingLayout{0, 0, pushCursor, AgcDriver::Graphics::PipelinePushSlotBytes - pushCursor}; if (layout.pushConstantOffsetBytes < AgcDriver::Graphics::PipelinePushSlotBytes) layout.pushConstantSizeBytes = ShaderRecompiler::TessellationLevelPushOffsetBytes - layout.pushConstantOffsetBytes; @@ -477,7 +582,7 @@ void Draw(AgcDriver::VulkanDevice& device, const Stages& stages, VkCullModeFlags state.blend.colorWriteMask = 15; state.blends = {state.blend}; state.blendConstants = {}; - device.Draw(state, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, 1, 0, true}, shaders); + device.Draw(state, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, patches.instances, 0, true, 0, patches.firstInstance}, shaders); device.WaitIdle(); } @@ -494,12 +599,12 @@ std::uint32_t CoveredPixels(std::uint32_t patch, bool interior) { return count; } -void RequirePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target, std::span hull, const std::string& program, const PatchDraw& patches = Triangles, std::span local = LocalCode) { +void RequirePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target, std::span hull, const std::string& program, const PatchDraw& patches = Triangles, std::span local = LocalCode, std::span domain = DomainCode) { std::uint32_t cursor = 0; const Stages stages{ Compile(target, ShaderStage::Local, local, cursor, patches), Compile(target, ShaderStage::TessellationControl, hull, cursor, patches), - Compile(target, ShaderStage::TessellationEvaluation, DomainCode, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, domain, cursor, patches), Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), }; const auto center = Row(-0.5f + 1.0f / 3.0f); @@ -621,6 +726,33 @@ void RequireRelativePatches(AgcDriver::VulkanDevice& device, const ShaderRecompi } } +void RequireInstancedRelativePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + const auto center = Row(-0.5f + 1.0f / 3.0f); + TessellationConfiguration restarting{3u, 3u, 1u, 2u, 2u, 4u, 8u}; + restarting.breakAtInstance = true; + for (const auto& [patches, first, last] : {std::tuple{PatchDraw{{3u, 3u, 1u, 2u, 2u, 4u, 8u}, Indices, 1.0f, 64.0f, 2u, 1u}, 128, 64}, std::tuple{PatchDraw{{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices, 1.0f, 64.0f, 2u, 1u}, 32, 96}, std::tuple{PatchDraw{restarting, Indices, 1.0f, 64.0f, 2u, 1u}, 32, 96}}) { + std::uint32_t cursor = 0; + const Stages stages{ + Compile(target, ShaderStage::Local, LocalCode, cursor, patches), + Compile(target, ShaderStage::TessellationControl, HullRelativeData, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, DomainPatchRead, cursor, patches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), + }; + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + for (const auto& [patch, expected] : {std::pair{0u, first}, std::pair{2u, last}}) { + const auto pixel = PixelAt(Column(patch, 0.25f), center); + Require(pixel[0] == 255 && std::abs(pixel[2] - expected) <= 1, "the patch in column " + std::to_string(patch) + " of an instanced draw with " + std::to_string(patches.configuration.patchesPerGroup) + " patches per group and " + std::to_string(patches.configuration.primitiveGroupSize) + " primitives per group" + (patches.configuration.breakAtInstance ? " and BREAK_WAVE_AT_EOI" : "") + " did not take its blue channel from the hull's relative patch index across instances, " + PixelText(pixel)); + } + } + const PatchDraw shared{{3u, 3u, 1u, 2u, 2u, 4u, 8u}, Indices, 1.0f, 64.0f, 2u, 1u}; + ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "domain program uses its relative patch index as data in an instanced draw", shared); + std::vector capabilities(target.supportedCapabilities.begin(), target.supportedCapabilities.end()); + std::erase(capabilities, static_cast(spv::CapabilityDrawParameters)); + auto withoutDrawParameters = target; + withoutDrawParameters.supportedCapabilities = capabilities; + ExpectRefusal(withoutDrawParameters, ShaderStage::TessellationControl, HullRelativeData, "shaderDrawParameters", shared); +} + class GuestBlock { public: GuestBlock() { @@ -689,6 +821,32 @@ void RequireHullBdaReads(AgcDriver::VulkanDevice& device, const ShaderRecompiler address(0u); } +Stages CompileLoweredInterior(const ShaderRecompiler::SpirvTarget& target, const PatchDraw& patches) { + std::uint32_t cursor = 0; + return { + Compile(target, ShaderStage::Local, LocalCode, cursor, patches), + Compile(target, ShaderStage::TessellationControl, HullCode, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, DomainLoweredInterior, cursor, patches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), + }; +} + +void RequireFractionalEvenPartitioning(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + const auto below = Row(-0.75f); + const PatchDraw integer{{3u, 3u, 1u, 0u, 2u}, Indices}; + Draw(device, CompileLoweredInterior(target, integer), VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, integer); + Require(CoveredPixels(0, false) > 100 && PixelAt(Column(0, 0.25f), below) == Background, "integer partitioning: the level-1 patch has a domain point inside it, " + PixelText(PixelAt(Column(0, 0.25f), below))); + Require(PixelAt(Column(2, 0.25f), below) != Background, "integer partitioning: the level-5 patch has no domain point inside it"); + const PatchDraw patches{{3u, 3u, 1u, 3u, 2u}, Indices}; + const auto stages = CompileLoweredInterior(target, patches); + Require(HasSpacing(stages.domain, spv::ExecutionModeSpacingFractionalEven) && !HasSpacing(stages.domain, spv::ExecutionModeSpacingFractionalOdd) && !HasSpacing(stages.domain, spv::ExecutionModeSpacingEqual), "fractional-even partitioning did not compile to fractional-even spacing"); + Require(HasLevelBound(stages.hull, 64.0f) && !HasLevelBound(stages.hull, 63.0f), "fractional-even partitioning did not bound its tessellation factors at 64"); + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + Require(CoveredPixels(0, false) > 100 && PixelAt(Column(0, 0.25f), below) != Background, "fractional-even partitioning: the level-1 patch was not raised to level 2 with a domain point inside it"); + Require(CoveredPixels(1, false) == 0 && PixelAt(Column(1, 0.25f), below) == Background, "fractional-even partitioning: the patch with level 0 was not culled"); + Require(CoveredPixels(2, false) > 100 && PixelAt(Column(2, 0.25f), below) != Background, "fractional-even partitioning: the level-5 patch has no domain point inside it"); +} + void RequireQuads(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { const auto center = Row(0.0f); std::array frontFacing{}; @@ -779,6 +937,53 @@ void RequireSingleControlPointHulls(const ShaderRecompiler::SpirvTarget& target) Require(!CompileHull(target, HullSingleOutput, {1u, 1u, 1u, 2u, 2u}).spirv.empty(), "a hull with one output control point and unguarded patch outputs was not compiled"); } +void RequireMergedHullWithoutSubgroups(const ShaderRecompiler::SpirvTarget& target) { + std::uint32_t cursor = 0; + const auto hull = Compile(target, ShaderStage::TessellationControl, MergedHullCode, cursor, MergedTriangles); + const auto words = hull.spirv.Words(); + for (std::size_t at = 5; at + 1u < words.size(); at += words[at] >> 16u) { + Require((words[at] >> 16u) != 0u, "the merged hull program compiled to a SPIR-V instruction without words"); + Require((words[at] & 0xffffu) != spv::OpCapability || words[at + 1u] != spv::CapabilityGroupNonUniform, "the hull stage of a merged hull program declares subgroup operations, which a device without tessellation control subgroups rejects"); + } +} + +void RequireMergedHullConstantIndices(const ShaderRecompiler::SpirvTarget& target) { + std::uint32_t cursor = 0; + const auto hull = Compile(target, ShaderStage::TessellationControl, MergedHullCode, cursor, MergedTriangles); + const auto words = hull.spirv.Words(); + std::unordered_set constants; + std::unordered_set folded; + for (std::size_t at = 5; at + 1u < words.size(); at += words[at] >> 16u) { + const auto count = words[at] >> 16u; + Require(count != 0u && at + count <= words.size(), "the merged hull program compiled to a malformed SPIR-V instruction"); + const auto opcode = words[at] & 0xffffu; + if (opcode == spv::OpConstant && count >= 3u) constants.insert(words[at + 2u]); + const bool integer = opcode == spv::OpIAdd || opcode == spv::OpIMul || opcode == spv::OpUDiv || opcode == spv::OpUMod || opcode == spv::OpShiftRightLogical || opcode == spv::OpBitwiseAnd; + const auto operand = [&](std::uint32_t id) { return constants.contains(id) || folded.contains(id); }; + if (integer && count == 5u && operand(words[at + 3u]) && operand(words[at + 4u])) folded.insert(words[at + 2u]); + if (opcode == spv::OpAccessChain) { + for (std::uint32_t index = 4u; index < count; ++index) { + Require(!folded.contains(words[at + index]), "the merged hull program indexes a tessellation attribute with arithmetic on constants instead of a constant, which AMD's Windows driver reads wrongly"); + } + } + } +} + +void RequireRegisteredMergedStages() { + const auto low = [](const auto& code) { return static_cast(reinterpret_cast(code.data()) >> 8u); }; + const auto high = [](const auto& code) { return static_cast(reinterpret_cast(code.data()) >> 40u); }; + RegisteredStage merged; + RegisteredStage domain; + Describe(merged, MergedHullCode, 3u, {{0x148u, low(MergedHullCode)}, {0x149u, high(MergedHullCode)}, {0x10au, 0u}, {0x10bu, 16u}}, {{0x2dbu, 0x40049u}, {0x286u, 0x42800000u}, {0x287u, 0x3f800000u}, {0x2d6u, 0xc355u}}, 0x105u); + Describe(domain, DomainCode, 2u, {{0xc8u, low(DomainCode)}, {0xc9u, high(DomainCode)}, {0x8au, 0u}, {0x8bu, 16u}}, {}, 0x02002008u); + for (const auto* stage : {&merged, &domain}) AgcDriverRegisterShader_nid_postfix(&stage->shader); + const std::array stages{&merged.shader, &domain.shader}; + const std::array context{{{0x2d5u, 0x0200210du}, {0x29bu, 0u}}}; + const std::array primitive{{{0x25bu, 0u}, {0x262u, 0u}, {0x242u, 9u}}}; + AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, context, primitive); + AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, context, primitive); +} + } int main() { @@ -802,8 +1007,6 @@ int main() { ExpectRefusal(target, ShaderStage::Local, LocalIndexAsData, "outside a tessellation ring or LDS address"); ExpectRefusal(target, ShaderStage::TessellationControl, HullRelativeData, "GE_CNTL.PRIM_GRP_SIZE is 0", RecordPatches); ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "GE_CNTL.PRIM_GRP_SIZE is 0", RecordPatches); - ExpectRefusal(target, ShaderStage::TessellationControl, HullRelativeData, "GE_CNTL.BREAK_WAVE_AT_EOI is clear", {{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices}); - ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "GE_CNTL.BREAK_WAVE_AT_EOI is clear", {{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices}); ExpectRefusal(target, ShaderStage::TessellationControl, HullCode, "outside its patch's quad factors", {{3u, 3u, 2u, 2u, 2u}, Indices}); ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine"); ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine", {{3u, 3u, 1u, 2u, 2u, 158u}, Indices}); @@ -813,18 +1016,31 @@ int main() { RequirePatches(*device, target, HullRestoredExecSaved, "hull program saving its restored exec"); RequirePatches(*device, target, HullNextInput, "hull program reading the next of 4 input control points of 24 bytes", NextInputTriangles, LocalCodeStride24); RequirePatches(*device, target, HullDynamicIndex, "hull program reading a control point at a computed index"); + RequirePatches(*device, target, HullCode, "domain program reading control points at a computed index", Triangles, LocalCode, DomainComputedPoint); RequirePatches(*device, target, HullAndSaveexec, "hull program narrowing exec with s_and_saveexec_b64"); RequirePatches(*device, target, HullSaveexecLoop, "hull program restoring exec through loop phis"); RequirePatches(*device, target, HullDescendingRecords, "hull program with descending patch records in 128-patch groups", {{3u, 3u, 1u, 2u, 2u, 128u}, Indices}); + ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullCode, "more input than output control points", {{4u, 3u, 1u, 2u, 2u, 256u, 0u, false, 0u, false, true}, Indices}); + ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullForeignRecord, "outside its own control point's record", MergedTriangles); + ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullDivergentBarrier, "s_barrier that some invocations do not reach", MergedTriangles); + ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullOutsidePatch, "reads LDS outside its patch's control point records", MergedTriangles); + ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullReadLane, "another lane's register", MergedTriangles); + RequireMergedHullWithoutSubgroups(target); + RequireMergedHullConstantIndices(target); + RequirePatches(*device, target, MergedHullCode, "merged hull program", MergedTriangles, MergedHullCode); + RequirePatches(*device, target, MergedHullRegisters, "merged hull program storing its local part's registers", MergedTriangles, MergedHullRegisters); RequireCounterClockwise(*device, target); RequireLevelClamps(*device, target); RequireIntegerPartitioning(*device, target); + RequireFractionalEvenPartitioning(*device, target); RequireQuads(*device, target); RequirePatchConstants(*device, target); RequireRelativePatches(*device, target); + RequireInstancedRelativePatches(*device, target); RequireHullBdaReads(*device, target); RequireCompareBranches(target); device.reset(); + RequireRegisteredMergedStages(); RequireRegisteredStages(); std::puts("Tessellation tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/TgSizeWaveId.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/TgSizeWaveId.cpp new file mode 100644 index 0000000000..79d60e331f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/TgSizeWaveId.cpp @@ -0,0 +1,94 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Threads = 256; + +alignas(256) constexpr std::array TgSizeWaveIdCode{ + 0x9384ff03, 0x00060006, 0x34020082, 0x7e040204, 0xdc708000, 0x00000201, 0xbf810000, 0xbf810000, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "TG_SIZE wave id: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + ~GuestBlock() { GuestAllocations::Mutation().Remove(block); } + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +void Run(AgcDriver::VulkanDevice& device, GuestBlock& guest, std::uint32_t waveSize) { + std::memset(guest.Data(), 0xff, BlockBytes); + const auto address = static_cast(reinterpret_cast(guest.Data())); + const std::vector userData{static_cast(address), static_cast(address >> 32u)}; + const std::span code(TgSizeWaveIdCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {true, false, false}, true, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + std::array waves{}; + std::memcpy(waves.data(), guest.Data(), sizeof(waves)); + for (std::uint32_t thread = 0; thread < Threads; ++thread) { + if (waves[thread] == thread / waveSize) continue; + Require(false, "wave" + std::to_string(waveSize) + " thread " + std::to_string(thread) + " read wave " + std::to_string(waves[thread]) + " from TG_SIZE[11:6], expected " + std::to_string(thread / waveSize)); + } + std::printf("wave%u: TG_SIZE[11:6] holds the wave index for all %u threads\n", waveSize, Threads); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock guest; + Run(*device, guest, 64); + Run(*device, guest, 32); + std::puts("TG_SIZE wave id 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/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/VCmpF32Denormals.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp index 54e132b6c6..22568a2f9a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp @@ -95,17 +95,9 @@ std::string Hex(std::uint32_t value) { return text; } -std::uint32_t Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode mode, std::uint32_t compare, std::uint32_t capture, std::uint32_t group) { +ShaderRecompiler::RecompileResult Compile(const AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode mode, std::uint32_t compare, std::uint32_t capture) { Code[6] = compare; Code[7] = capture; - for (std::uint32_t lane = 0; lane < Threads; ++lane) { - const std::uint32_t pair = group * Threads + lane; - Input[lane * Inputs] = Values[pair / ValueCount]; - Input[lane * Inputs + 1u] = Values[pair % ValueCount]; - Input[lane * Inputs + 2u] = 0u; - Input[lane * 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)); @@ -122,8 +114,19 @@ std::uint32_t Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloat }; request.useCache = false; request.context.floatMode = mode; - const auto result = ShaderRecompiler::Recompile(request); - device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + return ShaderRecompiler::Recompile(request); +} + +std::uint32_t Run(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& program, std::uint32_t group) { + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const std::uint32_t pair = group * Threads + lane; + Input[lane * Inputs] = Values[pair / ValueCount]; + Input[lane * Inputs + 1u] = Values[pair % ValueCount]; + Input[lane * Inputs + 2u] = 0u; + Input[lane * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(Code.data())); device.WaitIdle(); std::uint32_t mask = 0u; for (std::uint32_t lane = 0; lane < Threads; ++lane) { @@ -135,8 +138,9 @@ std::uint32_t Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloat void Check(AgcDriver::VulkanDevice& device, const char* modeName, ShaderRecompiler::ShaderFloatMode mode, const Row& row, bool exec, const std::uint32_t* expected) { const std::string name = std::string(exec ? "v_cmpx_" : "v_cmp_") + row.condition + "_f32"; + const auto program = Compile(device, mode, exec ? row.cmpx : row.cmp, exec ? CaptureExec : CaptureVcc); for (std::uint32_t group = 0; group < Groups; ++group) { - const std::uint32_t actual = Run(device, mode, exec ? row.cmpx : row.cmp, exec ? CaptureExec : CaptureVcc, group); + const std::uint32_t actual = Run(device, program, group); if (actual == expected[group]) continue; const std::uint32_t diff = actual ^ expected[group]; for (std::uint32_t lane = 0; lane < Threads; ++lane) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowLiveness.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowLiveness.cpp new file mode 100644 index 0000000000..54193f052e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowLiveness.cpp @@ -0,0 +1,93 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t MaxThreads = 64; +constexpr std::uint32_t Results = 4; +alignas(256) std::array Output{}; +alignas(256) std::array Unused{}; + +alignas(256) constexpr std::array Code{ + 0x34060084, 0x4a080081, 0xbf068008, 0xbf850001, 0x4a080890, 0xbefc0380, 0x7e028704, 0xe0701000, + 0x80010103, 0x4a080081, 0xbe890383, 0x7e0a0280, 0xbefc0380, 0x7e0c8704, 0x4a0a0b06, 0x4a080890, + 0x80898109, 0xbf078009, 0xbf85fff9, 0xe0701004, 0x80010503, 0xbf810000, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::uint32_t threads, std::uint32_t overwrite) { + Output.fill(0xdeadbeefu); + std::vector userData(9, 0u); + const auto input = BufferDescriptor(Unused.data(), static_cast(Unused.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + userData[8] = overwrite; + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(std::uint32_t waveSize, std::uint32_t threads, std::uint32_t overwrite) { + for (std::uint32_t tid = 0; tid < threads; ++tid) { + const std::uint32_t* out = &Output[tid * Results]; + const std::uint32_t written = tid + 1u; + const std::uint32_t branch = overwrite != 0u ? written + 16u : written; + const std::uint32_t loop = 3u * written + 48u; + const auto where = std::string("vector window liveness: wave") + std::to_string(waveSize) + " lane " + std::to_string(tid) + (overwrite != 0u ? " (overwritten before the read)" : " (overwrite branched over)"); + Require(out[0] == branch, where + ": the window register written before the branch read " + Hex(out[0]) + ", expected " + Hex(branch)); + Require(out[1] == loop, where + ": the window register carried around the loop read " + Hex(out[1]) + ", expected " + Hex(loop)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const std::uint32_t waveSize : {32u, 64u}) { + for (const std::uint32_t overwrite : {0u, 1u}) { + Run(*device, waveSize, waveSize, overwrite); + Check(waveSize, waveSize, overwrite); + } + } + std::puts("vector window liveness tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowNarrowing.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowNarrowing.cpp new file mode 100644 index 0000000000..73b15eb93a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowNarrowing.cpp @@ -0,0 +1,120 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t MaxThreads = 64; +constexpr std::uint32_t Results = 16; +constexpr std::uint32_t Checked = 6; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020086, 0x34060088, 0x4a140680, 0x4a160681, 0x4a180682, 0x4a1a0683, 0x4a1c0684, 0x4a1e0685, + 0x4a200686, 0x4a220687, 0x4a240688, 0x4a260689, 0x4a28068a, 0x4a2a068b, 0x4a2c068c, 0x4a2e068d, + 0x4a30068e, 0x4a32068f, 0x4a340690, 0x4a360691, 0x4a380692, 0x4a3a0693, 0x4a3c0694, 0x4a3e0695, + 0x4a400696, 0x4a420697, 0x4a440698, 0x4a460699, 0x4a48069a, 0x4a4a069b, 0x4a4c069c, 0x4a4e069d, + 0x4a50069e, 0x4a52069f, 0x877c8308, 0x7e08870a, 0xbefc0309, 0x7e0a870a, 0x7e148504, 0x877c8308, + 0x7e148505, 0xe0701000, 0x80010401, 0xe0701004, 0x80010501, 0xe0701008, 0x80010c01, 0xe070100c, + 0x80010d01, 0xe0701010, 0x80011e01, 0xe0701014, 0x80012901, 0xbf810000, +}; + +constexpr const char* Names[Checked] = {"v4", "v5", "v12", "v13", "v30", "v41"}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t FunctionVariables(std::span words) { + std::uint32_t count = 0; + for (std::size_t at = 5; at < words.size();) { + const auto length = words[at] >> 16u; + if (length == 0u) break; + if ((words[at] & 0xffffu) == spv::OpVariable && at + 3u < words.size() && words[at + 3u] == spv::StorageClassFunction) ++count; + at += length; + } + return count; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::uint32_t threads, std::uint32_t bounded, std::uint32_t data) { + Output.fill(0xdeadbeefu); + std::vector userData(10, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(output.begin(), output.end(), userData.begin() + 4); + userData[8] = bounded; + userData[9] = data; + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + Require(FunctionVariables(result.spirv.Words()) > 8u, "vector window narrowing: wave" + std::to_string(waveSize) + " window was not split into separate slot variables"); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(std::uint32_t waveSize, std::uint32_t threads, std::uint32_t bounded, std::uint32_t data) { + for (std::uint32_t tid = 0; tid < threads; ++tid) { + std::array window{}; + for (std::uint32_t k = 0; k < window.size(); ++k) window[k] = (tid << 8u) + k; + const std::uint32_t v4 = window[bounded & 3u]; + const std::uint32_t v5 = window[data]; + window[data] = v4; + window[bounded & 3u] = v5; + const std::array expected{v4, v5, window[2], window[3], window[20], window[31]}; + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t index = 0; index < Checked; ++index) { + Require(out[index] == expected[index], "vector window narrowing: wave" + std::to_string(waveSize) + " data index " + std::to_string(data) + " lane " + std::to_string(tid) + " " + Names[index] + " is " + Hex(out[index]) + ", expected " + Hex(expected[index])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < 32u) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + for (const std::uint32_t data : {20u, 2u, 31u}) { + Run(*device, 32, 32, 7u, data); + Check(32, 32, 7u, data); + Run(*device, 64, 64, 7u, data); + Check(64, 64, 7u, data); + } + std::puts("vector window narrowing tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp index 5b5a66615f..08adb9fde4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp @@ -129,9 +129,10 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const Row& ro pixel.targetOutputMode[0] = 9; pixel.targetExportMapping.fill(0xe4u); const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + const std::vector pixelUserData(4, 0u); ShaderRecompiler::RecompileRequest fragment{ {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, - {waveSize, 0, std::vector(4, 0u), std::nullopt, pixel, std::nullopt, pixelMemory}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, device.Target(), PixelPushLayout(vertexPush, device.Target()) }; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp index 84b788a199..076e006c49 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp @@ -201,6 +201,8 @@ void PositionExportLayoutTests() { for (int use = 0; use < 2; ++use) Require(PositionExportLayout(&snapshot, 0, snapshot.code, 0x0040000fu) == 0u, "a cached pos0-only program kept the position export bits"); snapshot.code.assign(VertexCode.begin(), VertexCode.end()); Require(PositionExportLayout(&snapshot, 2, std::span(snapshot.code).subspan(2), 0x0040000fu) == 0x0040000fu, "a program at an offset was answered from the cache of its snapshot"); + constexpr std::array PaddedTail{0u}; + Require(PositionExportLayout(nullptr, 0, PaddedTail, 0x0040000fu) == 0x0040000fu, "an undecodable registered tail lost the guest's position export bits"); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexPointSize.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexPointSize.cpp new file mode 100644 index 0000000000..253201b452 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexPointSize.cpp @@ -0,0 +1,165 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Extent = 64u; +constexpr std::uint32_t MiscVector = 1u << 21u; +constexpr std::uint32_t VertexPointSize = 1u << 16u; +alignas(256) std::array pixels{}; +alignas(256) std::array vertices{}; +constexpr std::array positionCode{0xf4080100u, 0xfa000000u, 0x4a0a0a02u, 0x4a0a0b08u, 0x4a0a0a03u, 0xe00c2000u, 0x80010005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; +constexpr std::array pixelCode{0x7e0002f2u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u}; + +std::vector SizeExportCode(float size) { + std::vector code(positionCode.begin(), positionCode.begin() + 8); + code.insert(code.end(), {0x7e0802ffu, std::bit_cast(size), 0xf80000cfu, 0x03020100u, 0xf80008d1u, 0x00000004u, 0xbf810000u}); + return code; +} + +struct PointSize { + float size = 1.0f; + float minimum = 0.0f; + float maximum = 0.0f; + bool fromVertex = false; +}; + +struct Square { + unsigned pixels = 0; + std::uint32_t left = Extent, top = Extent, right = 0, bottom = 0; +}; + +std::vector RunCase(AgcDriver::VulkanDevice& device, const char* name, std::span vertexCode, const PointSize& points, float offset) { + const std::array, 4> centres{{{16.0f, 16.0f}, {48.0f, 16.0f}, {16.0f, 48.0f}, {48.0f, 48.0f}}}; + vertices.fill(std::byte{0}); + constexpr std::uint32_t stride = 16u; + for (std::size_t index = 0; index < centres.size(); ++index) { + const std::array position{(centres[index][0] + offset) / Extent * 2.0f - 1.0f, (centres[index][1] + offset) / Extent * 2.0f - 1.0f, 0.0f, 1.0f}; + std::memcpy(vertices.data() + index * stride, position.data(), sizeof(position)); + } + std::array descriptor{}; + const auto tableAddress = reinterpret_cast(descriptor.data()); + std::array userData{static_cast(tableAddress), static_cast(tableAddress >> 32u), 0u, 0u}; + const std::array memory{{{tableAddress, std::as_bytes(std::span(descriptor))}}}; + ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; + vertexInfo.fetchEmbedded = true; + vertexInfo.resourcesNum = 1u; + vertexInfo.resourcesDst[0] = {0, 4, 0, 0}; + vertexInfo.paClVsOutCntl = points.fromVertex ? MiscVector | VertexPointSize : 0u; + vertexInfo.pointList = true; + vertexInfo.pointSize = points.size; + vertexInfo.pointSizeMin = points.minimum; + vertexInfo.pointSizeMax = points.maximum; + ShaderRecompiler::RecompileRequest request{{ShaderStage::Vertex, reinterpret_cast(vertexCode.data()), vertexCode, 0, {}}, {32u, 0u, userData, std::nullopt, std::nullopt, vertexInfo, memory}, device.Target(), {0u, 0u, 0u, 96u}}; + const auto address = reinterpret_cast(vertices.data()); + descriptor = {static_cast(address), static_cast(address >> 32u) | (stride << 16u), static_cast(centres.size()), 0x01000facu | (77u << 12u)}; + request.context.vertex->resources[0].fields = descriptor; + const auto vertex = ShaderRecompiler::Recompile(request); + const auto pushBytes = static_cast(vertex.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixelInfo{}; + pixelInfo.wave32 = true; + pixelInfo.targetOutputMode[0] = 9u; + pixelInfo.targetExportMapping.fill(0xe4u); + pixelInfo.rasterPrimitive = ShaderRecompiler::RasterPrimitive::Point; + ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), PixelPushLayout(pushBytes, device.Target())}; + const auto pixel = ShaderRecompiler::Recompile(pixelRequest); + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(pushBytes, device.Target())}}}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Extent, Extent}; + state.topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST; + state.paClVsOutCntl = vertexInfo.paClVsOutCntl; + state.vertexPoints = true; + state.pointSize = points.size; + state.pointSizeMin = points.minimum; + state.pointSizeMax = points.maximum; + state.viewport = {0.0f, static_cast(Extent), static_cast(Extent), -static_cast(Extent), 0.0f, 1.0f}; + state.scissor = {{0, 0}, {Extent, Extent}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15u; + state.blends = {state.blend}; + AgcDriver::Pm4::DrawParameters draw{0u, static_cast(centres.size()), 0u, 1u, 0u, false}; + draw.firstInstance = 0u; + pixels.fill(std::byte{0}); + device.Draw(state, draw, shaders); + device.WaitIdle(); + std::vector squares(centres.size()); + for (std::uint32_t y = 0; y < Extent; ++y) { + for (std::uint32_t x = 0; x < Extent; ++x) { + const auto index = (y * Extent + x) * 4u; + if (pixels[index] == std::byte{0} && pixels[index + 3u] == std::byte{0}) continue; + auto& square = squares[(y >= Extent / 2u ? 2u : 0u) + (x >= Extent / 2u ? 1u : 0u)]; + ++square.pixels; + square.left = std::min(square.left, x); + square.top = std::min(square.top, y); + square.right = std::max(square.right, x + 1u); + square.bottom = std::max(square.bottom, y + 1u); + } + } + std::cout << "case " << name << ":"; + for (const auto& square : squares) std::cout << " " << square.pixels << " [" << square.left << "," << square.top << ")-(" << square.right << "," << square.bottom << ")"; + std::cout << "\n"; + return squares; +} + +void RequireSquares(const std::vector& squares, std::uint32_t side, const char* name) { + for (const auto& square : squares) { + Require(square.pixels == side * side && square.right - square.left == side && square.bottom - square.top == side, std::string(name) + ": a point is not a " + std::to_string(side) + "x" + std::to_string(side) + " square"); + } +} + +void RequireAtMostOnePixel(const std::vector& squares, const char* name) { + for (const auto& square : squares) Require(square.pixels <= 1u, std::string(name) + ": a point with no positive size covers more than one pixel"); +} + +void Run(AgcDriver::VulkanDevice& device) { + std::cout << "device: " << device.DeviceName() << "\n"; + RequireSquares(RunCase(device, "register size 1", positionCode, {1.0f, 1.0f, 1.0f, false}, 0.5f), 1u, "register size 1"); + RequireSquares(RunCase(device, "register size 4", positionCode, {4.0f, 1.0f, 1.0f, false}, 0.0f), 4u, "register size 4"); + RequireSquares(RunCase(device, "register size 3 ignores the clamp", positionCode, {3.0f, 6.0f, 8.0f, false}, 0.5f), 3u, "register size 3 ignores the clamp"); + const auto two = SizeExportCode(2.0f); + RequireSquares(RunCase(device, "vertex size 2 within [1, 8]", two, {1.0f, 1.0f, 8.0f, true}, 0.0f), 2u, "vertex size 2 within [1, 8]"); + const auto six = SizeExportCode(6.0f); + RequireSquares(RunCase(device, "vertex size 6 clamped to 4", six, {1.0f, 0.5f, 4.0f, true}, 0.0f), 4u, "vertex size 6 clamped to 4"); + const auto one = SizeExportCode(1.0f); + RequireSquares(RunCase(device, "vertex size 1 clamped to 5", one, {1.0f, 5.0f, 8.0f, true}, 0.5f), 5u, "vertex size 1 clamped to 5"); + RequireSquares(RunCase(device, "vertex size over a register size", six, {1.0f, 1.0f, 8.0f, true}, 0.0f), 6u, "vertex size over a register size"); + const auto zero = SizeExportCode(0.0f); + RequireAtMostOnePixel(RunCase(device, "vertex size 0 within [0, 4]", zero, {0.0f, 0.0f, 4.0f, true}, 0.5f), "vertex size 0 within [0, 4]"); + const auto negative = SizeExportCode(-3.0f); + RequireAtMostOnePixel(RunCase(device, "vertex size -3 within [0, 4]", negative, {1.0f, 0.0f, 4.0f, true}, 0.5f), "vertex size -3 within [0, 4]"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp index 10857e6d31..a26753bd63 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp @@ -155,9 +155,135 @@ constexpr const char* Names[16] = { "v_cubema_f32 v25, -v4, |v5|, v6 clamp", }; -void Fill(std::uint32_t tid, std::uint32_t* words) { - std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), words); -} +alignas(256) constexpr std::array OmodCode{ + 0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, 0xd544000a, 0x0c1a0b04, 0xd544000b, + 0x1c1a0b04, 0xd545000c, 0x0c1a0b04, 0xd545000d, 0x141a0b04, 0xd545000e, 0x1c1a0b04, 0xd546000f, + 0x0c1a0b04, 0xd5460010, 0x141a0b04, 0xd5460011, 0x1c1a0b04, 0xd5470012, 0x0c1a0b04, 0xd5470013, + 0x141a0b04, 0xd5470014, 0x1c1a0b04, 0xd5440015, 0xdc1a0b04, 0xd5458216, 0x341a0b04, 0xd5468117, + 0x5c1a0b04, 0xd5478018, 0x8c1a0b04, 0xd5440019, 0x141a0b04, 0xe0781000, 0x80010a03, 0xe0781010, + 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xbf810000, +}; + +constexpr std::uint32_t OmodRows[32][4] = { + {0x3f800000u, 0x3f000000u, 0x3e800000u, 0x00000000u}, + {0xbf800000u, 0x3f000000u, 0x3e800000u, 0x00000000u}, + {0x3f000000u, 0x3f800000u, 0x3e800000u, 0x00000000u}, + {0x3f000000u, 0xbf800000u, 0xbe800000u, 0x00000000u}, + {0x3e800000u, 0x3f000000u, 0x3f800000u, 0x00000000u}, + {0x3e800000u, 0xbf000000u, 0xbf800000u, 0x00000000u}, + {0x40400000u, 0xc0e00000u, 0x40000000u, 0x00000000u}, + {0xc0200000u, 0x3fc00000u, 0x40c00000u, 0x00000000u}, + {0x7f7fffffu, 0x7f000000u, 0xff000000u, 0x00000000u}, + {0xff7fffffu, 0x7e800000u, 0x7effffffu, 0x00000000u}, + {0x3f800000u, 0x00800000u, 0x80ffffffu, 0x00000000u}, + {0x3f800000u, 0x01000000u, 0x81000000u, 0x00000000u}, + {0x3f800000u, 0x00000007u, 0x80000007u, 0x00000000u}, + {0x3f800000u, 0x80000000u, 0x00000000u, 0x00000000u}, + {0x00800000u, 0x00000000u, 0x80000000u, 0x00000000u}, + {0x00000007u, 0x00000003u, 0x00000001u, 0x00000000u}, + {0x7f800000u, 0x3f800000u, 0x40000000u, 0x00000000u}, + {0xff800000u, 0x7f800000u, 0x3f800000u, 0x00000000u}, + {0x7fc00000u, 0x3f800000u, 0x3f000000u, 0x00000000u}, + {0x3f800000u, 0xffc00000u, 0x3f000000u, 0x00000000u}, + {0x40000000u, 0x3f800000u, 0x7fc00000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u}, + {0x7149f2cau, 0xf149f2cau, 0x71c9f2cau, 0x00000000u}, + {0x0da24260u, 0x0e224260u, 0x8e736390u, 0x00000000u}, + {0x7e800000u, 0x7e7fffffu, 0x3f800000u, 0x00000000u}, + {0x40000000u, 0x7e800000u, 0x7effffffu, 0x00000000u}, + {0x3f000000u, 0xbf000000u, 0x3f000000u, 0x00000000u}, + {0xc0c5edadu, 0xc08852a3u, 0xc0c18eacu, 0x00000000u}, + {0xbf3d7ad8u, 0xc0737ab3u, 0xc0e5e3c1u, 0x00000000u}, + {0x3f000000u, 0x3e800000u, 0xff812345u, 0x00000000u}, + {0x7f812345u, 0x3f000000u, 0x3e800000u, 0x00000000u} +}; +constexpr std::uint32_t OmodExpected[32][16] = { + {0x00000000u, 0x00000000u, 0xbf000000u, 0xbf800000u, 0xbe000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x3e800000u, 0x3f800000u, 0x00000000u}, + {0x40000000u, 0x3f000000u, 0x3f000000u, 0x3f800000u, 0x3e000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0x3f000000u, 0x00000000u, 0x3e800000u, 0x00000000u, 0x40800000u}, + {0x40800000u, 0x3f800000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0x3f000000u, 0x3f800000u, 0x3e000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x3fc00000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x41000000u}, + {0x40c00000u, 0x3fc00000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0x3f000000u, 0x3f800000u, 0x3e000000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41400000u}, + {0x41000000u, 0x40000000u, 0x3f000000u, 0x3f800000u, 0x3e000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x40200000u, 0x00000000u, 0x3e800000u, 0x00000000u, 0x41800000u}, + {0x41200000u, 0x40200000u, 0xbf000000u, 0xbf800000u, 0xbe000000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0x40000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x41a00000u}, + {0x40c00000u, 0x3fc00000u, 0x40c00000u, 0x41400000u, 0x3fc00000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0xc1e00000u, 0xc2600000u, 0xc0e00000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x41400000u}, + {0x41000000u, 0x40000000u, 0xc0a00000u, 0xc1200000u, 0xbfa00000u, 0xc0400000u, 0xc0c00000u, 0xbf400000u, 0x41c00000u, 0x42400000u, 0x40c00000u, 0x40200000u, 0x3f800000u, 0x3f400000u, 0x00000000u, 0x41800000u}, + {0x00000000u, 0x00000000u, 0x7f800000u, 0x7f800000u, 0x7e800000u, 0xff800000u, 0xff800000u, 0xfe800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x40000000u, 0x3f000000u, 0x7f7fffffu, 0x7f800000u, 0x7e7fffffu, 0xff000000u, 0xff800000u, 0xfe000000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x3f000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x40800000u}, + {0x00000000u, 0x00000000u, 0x017fffffu, 0x01ffffffu, 0x00000000u, 0x81000000u, 0x81800000u, 0x80000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x01800000u, 0x02000000u, 0x00800000u, 0x81800000u, 0x82000000u, 0x80800000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00800000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01800000u, 0x02000000u, 0x00800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01800000u, 0x00000000u}, + {0x41000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x00000000u, 0x00000000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0xc0000000u, 0xc0800000u, 0xbf000000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x3f800000u, 0x00000000u}, + {0x40800000u, 0x3f800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x40000000u, 0x40800000u, 0x3f000000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x3fc00000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x41000000u}, + {0x00000000u, 0x00000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0xc0000000u, 0xc0800000u, 0xbf000000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xc0000000u, 0xc0800000u, 0xbf000000u, 0x41000000u, 0x41800000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x3f000000u, 0x3f800000u, 0x00000000u}, + {0x41000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x41000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x41000000u, 0x40000000u, 0x71c9f2cau, 0x7249f2cau, 0x70c9f2cau, 0x71c9f2cau, 0x7249f2cau, 0x70c9f2cau, 0x72c9f2cau, 0x7349f2cau, 0x71c9f2cau, 0x40200000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x41200000u, 0x40200000u, 0x8e224260u, 0x8ea24260u, 0x8d224260u, 0x8ea24260u, 0x8f224260u, 0x8da24260u, 0x8f736390u, 0x8ff36390u, 0x8e736390u, 0x40000000u, 0x0ea24260u, 0x0da24260u, 0x0f736390u, 0x41a00000u}, + {0x00000000u, 0x00000000u, 0xc0000000u, 0xc0800000u, 0xbf000000u, 0xfeffffffu, 0xff7fffffu, 0xfdffffffu, 0x7f800000u, 0x7f800000u, 0x7e800000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x41000000u, 0x40000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0xff000000u, 0xff800000u, 0xfe000000u, 0x7f800000u, 0x7f800000u, 0x7effffffu, 0x40200000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x41800000u}, + {0x41000000u, 0x40000000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0x40000000u, 0x40800000u, 0x3f000000u, 0x40200000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x40000000u, 0x3f000000u, 0xc1418eacu, 0xc1c18eacu, 0xc0418eacu, 0x410852a3u, 0x418852a3u, 0x400852a3u, 0xc1c5edadu, 0xc245edadu, 0xc0c5edadu, 0x3f000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x41200000u, 0x40200000u, 0x3fbd7ad8u, 0x403d7ad8u, 0x3ebd7ad8u, 0x40f37ab3u, 0x41737ab3u, 0x3ff37ab3u, 0xc1e5e3c1u, 0xc265e3c1u, 0xc0e5e3c1u, 0x40000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x41a00000u}, + {0x00000000u, 0x00000000u, 0x7f812345u, 0x7f812345u, 0x7f812345u, 0xbf000000u, 0xbf800000u, 0xbe000000u, 0x40000000u, 0x40800000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0x3e000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xbf000000u, 0xbf800000u, 0xbe000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0x7f812345u, 0x7f812345u, 0x7f812345u, 0x00000000u, 0x00000000u, 0x3e800000u, 0x00000000u, 0x00000000u} +}; +constexpr std::uint32_t OmodExpectedIeee[32][16] = { + {0x00000000u, 0x00000000u, 0xbe800000u, 0xbe800000u, 0xbe800000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x3e800000u, 0x3f000000u, 0x3f800000u, 0x00000000u}, + {0x3f800000u, 0x3f800000u, 0x3e800000u, 0x3e800000u, 0x3e800000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0x3f800000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x3f800000u}, + {0x40000000u, 0x40000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3e800000u, 0x3e800000u, 0x3e800000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x40400000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x40000000u}, + {0x40400000u, 0x40400000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3e800000u, 0x3e800000u, 0x3e800000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40400000u}, + {0x40800000u, 0x40800000u, 0x3e800000u, 0x3e800000u, 0x3e800000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x40a00000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x40800000u}, + {0x40a00000u, 0x40a00000u, 0xbe800000u, 0xbe800000u, 0xbe800000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0x40800000u, 0x3e800000u, 0x00000000u, 0x3f800000u, 0x40a00000u}, + {0x40400000u, 0x40400000u, 0x40400000u, 0x40400000u, 0x40400000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0xc1600000u, 0xc1600000u, 0xc1600000u, 0x40000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x40400000u}, + {0x40800000u, 0x40800000u, 0xc0200000u, 0xc0200000u, 0xc0200000u, 0xbfc00000u, 0xbfc00000u, 0xbfc00000u, 0x41400000u, 0x41400000u, 0x41400000u, 0x40a00000u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x40800000u}, + {0x00000000u, 0x00000000u, 0x7f000000u, 0x7f000000u, 0x7f000000u, 0xff000000u, 0xff000000u, 0xff000000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x3f800000u, 0x3f800000u, 0x7effffffu, 0x7effffffu, 0x7effffffu, 0xfe800000u, 0xfe800000u, 0xfe800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x3f800000u}, + {0x00000000u, 0x00000000u, 0x00ffffffu, 0x00ffffffu, 0x00ffffffu, 0x80800000u, 0x80800000u, 0x80800000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00800000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x01000000u, 0x01000000u, 0x01000000u, 0x81000000u, 0x81000000u, 0x81000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000007u, 0x00000007u, 0x00000007u, 0x80000007u, 0x80000007u, 0x80000007u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000007u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x01000000u, 0x01000000u, 0x01000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00000000u}, + {0x40800000u, 0x40800000u, 0x00000007u, 0x00000007u, 0x00000007u, 0x80000003u, 0x80000003u, 0x80000003u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x00000000u, 0x00000003u, 0x00000000u, 0x40800000u}, + {0x00000000u, 0x00000000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x40000000u, 0x40000000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x40400000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x40000000u}, + {0x00000000u, 0x00000000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0x40800000u, 0x40800000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x40800000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x40800000u, 0x40800000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x40800000u, 0x40800000u, 0x7149f2cau, 0x7149f2cau, 0x7149f2cau, 0x7149f2cau, 0x7149f2cau, 0x7149f2cau, 0x7249f2cau, 0x7249f2cau, 0x7249f2cau, 0x40a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x40a00000u, 0x40a00000u, 0x8da24260u, 0x8da24260u, 0x8da24260u, 0x8e224260u, 0x8e224260u, 0x8e224260u, 0x8ef36390u, 0x8ef36390u, 0x8ef36390u, 0x40800000u, 0x0da24260u, 0x0e224260u, 0x0ef36390u, 0x40a00000u}, + {0x00000000u, 0x00000000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0xfe7fffffu, 0xfe7fffffu, 0xfe7fffffu, 0x7f000000u, 0x7f000000u, 0x7f000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x40800000u, 0x40800000u, 0x40000000u, 0x40000000u, 0x40000000u, 0xfe800000u, 0xfe800000u, 0xfe800000u, 0x7f7fffffu, 0x7f7fffffu, 0x7f7fffffu, 0x40a00000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x40800000u}, + {0x40800000u, 0x40800000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x40a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x3f800000u, 0x3f800000u, 0xc0c18eacu, 0xc0c18eacu, 0xc0c18eacu, 0x408852a3u, 0x408852a3u, 0x408852a3u, 0xc145edadu, 0xc145edadu, 0xc145edadu, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3f800000u}, + {0x40a00000u, 0x40a00000u, 0x3f3d7ad8u, 0x3f3d7ad8u, 0x3f3d7ad8u, 0x40737ab3u, 0x40737ab3u, 0x40737ab3u, 0xc165e3c1u, 0xc165e3c1u, 0xc165e3c1u, 0x40800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x40a00000u}, + {0x00000000u, 0x00000000u, 0x7fc12345u, 0x7fc12345u, 0x7fc12345u, 0xbe800000u, 0xbe800000u, 0xbe800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3e800000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xbe800000u, 0xbe800000u, 0xbe800000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0x7fc12345u, 0x7fc12345u, 0x7fc12345u, 0x00000000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x00000000u} +}; +constexpr const char* OmodNames[16] = { + "v_cubeid_f32 v10, v4, v5, v6 mul:2", + "v_cubeid_f32 v11, v4, v5, v6 div:2", + "v_cubesc_f32 v12, v4, v5, v6 mul:2", + "v_cubesc_f32 v13, v4, v5, v6 mul:4", + "v_cubesc_f32 v14, v4, v5, v6 div:2", + "v_cubetc_f32 v15, v4, v5, v6 mul:2", + "v_cubetc_f32 v16, v4, v5, v6 mul:4", + "v_cubetc_f32 v17, v4, v5, v6 div:2", + "v_cubema_f32 v18, v4, v5, v6 mul:2", + "v_cubema_f32 v19, v4, v5, v6 mul:4", + "v_cubema_f32 v20, v4, v5, v6 div:2", + "v_cubeid_f32 v21, v4, -v5, -v6 div:2", + "v_cubesc_f32 v22, -v4, |v5|, v6 clamp mul:4", + "v_cubetc_f32 v23, |v4|, -v5, v6 clamp div:2", + "v_cubema_f32 v24, v4, v5, -v6 clamp mul:2", + "v_cubeid_f32 v25, v4, v5, v6 mul:4", +}; std::array BufferDescriptor(const void* data, std::uint32_t bytes) { const auto address = reinterpret_cast(data); @@ -193,33 +319,19 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std:: return ShaderRecompiler::Recompile(request); } -void Run(AgcDriver::VulkanDevice& device, const std::optional& floatMode) { - for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::uint32_t (&rows)[32][4], const std::optional& floatMode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy(std::begin(rows[tid]), std::end(rows[tid]), &Input[tid * Inputs]); Output.fill(0xdeadbeefu); - const std::span code(Code); const auto result = Compile(device, code, floatMode); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); } -void CheckOmodRefused(AgcDriver::VulkanDevice& device) { - for (const std::uint32_t opcode : {0x144u, 0x145u, 0x146u, 0x147u}) { - alignas(256) const std::array code{0xd400000au | (opcode << 16u), 0x0c1a0b04u, 0xbf810000u}; - std::string refusal; - try { - static_cast(Compile(device, code)); - } catch (const std::exception& error) { - refusal = error.what(); - } - Require(refusal.find("VOP3 source modifiers are not implemented") != std::string::npos, "VOP3 opcode " + Hex(opcode) + " with omod was not refused"); - } -} - -void Check(const std::uint32_t (&expected)[32][16], const char* mode) { +void Check(const std::uint32_t (&expected)[32][16], const char* const (&names)[16], const char* mode) { for (std::uint32_t tid = 0; tid < Threads; ++tid) { const std::uint32_t* out = &Output[tid * Results]; for (std::uint32_t i = 0; i < 16; ++i) { - Expect(tid, out[i], expected[tid][i], (std::string(mode) + " " + Names[i]).c_str()); + Expect(tid, out[i], expected[tid][i], (std::string(mode) + " " + names[i]).c_str()); } } } @@ -230,13 +342,22 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device, std::nullopt); - Check(Expected, "no float mode"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); - Check(Expected, "IEEE=0"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); - Check(ExpectedIeee, "IEEE=1"); - CheckOmodRefused(*device); + Run(*device, Code, Rows, std::nullopt); + Check(Expected, Names, "no float mode"); + Run(*device, Code, Rows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + Check(Expected, Names, "IEEE=0"); + Run(*device, Code, Rows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); + Check(ExpectedIeee, Names, "IEEE=1"); + Run(*device, OmodCode, OmodRows, std::nullopt); + Check(OmodExpected, OmodNames, "no float mode"); + Run(*device, OmodCode, OmodRows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + Check(OmodExpected, OmodNames, "IEEE=0 f32 denormals flushed"); + Run(*device, OmodCode, OmodRows, ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}); + Check(OmodExpected, OmodNames, "IEEE=0 f32 denormals kept"); + Run(*device, OmodCode, OmodRows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); + Check(OmodExpectedIeee, OmodNames, "IEEE=1 f32 denormals flushed"); + Run(*device, OmodCode, OmodRows, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}); + Check(OmodExpectedIeee, OmodNames, "IEEE=1 f32 denormals kept"); std::puts("vop3 cube clamp tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VopcCompareF64.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VopcCompareF64.cpp index 064d6a8bf5..b4e9e68d34 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VopcCompareF64.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VopcCompareF64.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -22,6 +23,11 @@ constexpr std::uint32_t Inputs = 8; constexpr std::uint32_t Results = 4; constexpr std::uint32_t Compares = 41; constexpr std::uint32_t RandomRounds = 4; +struct Mode { + const char* name; + std::optional value; + bool flushed; +}; alignas(256) std::array Input{}; alignas(256) std::array Output{}; @@ -62,11 +68,12 @@ alignas(256) constexpr std::array CompareCode{ 0xd76f000b, 0x042d1114, 0xe0702000, 0x80010a03, 0xe0702004, 0x80010b03, 0xbf810000, }; -constexpr std::array Edges{ +constexpr std::array Edges{ 0x7ff8000000000000ull, 0x7ff0000000000001ull, 0xfff8000000000001ull, 0x7ff0000100000000ull, 0x7ff0000000000000ull, 0xfff0000000000000ull, 0x0000000000000000ull, 0x8000000000000000ull, 0x0000000000000001ull, 0x0000000000000002ull, 0x800fffffffffffffull, 0x0010000000000000ull, 0x3ff0000000000000ull, 0x3ff0000000000001ull, 0xbff0000000000000ull, 0x4008000000000000ull, 0xc008000000000000ull, 0x3fc45f306dc9c882ull, 0x3fc45f3060000000ull, 0x7fefffffffffffffull, + 0x8000000000000001ull, 0x000fffffffffffffull, }; constexpr std::array Masks{ @@ -113,8 +120,7 @@ std::array BufferDescriptor(const void* data, std::uint32_t co return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x11016facu}; } -void Run(AgcDriver::VulkanDevice& device) { - Output.fill(0xdeadbeefu); +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, const Mode& mode) { 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())); @@ -130,9 +136,8 @@ void Run(AgcDriver::VulkanDevice& device) { {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(); + request.context.floatMode = mode.value; + return ShaderRecompiler::Recompile(request); } std::uint32_t ClassBit(std::uint64_t bits) { @@ -150,14 +155,18 @@ bool ClassMatch(std::uint64_t bits, std::uint32_t mask) { return ((mask >> ClassBit(bits)) & 1u) != 0u; } -void Check(std::uint32_t round) { +void Check(std::uint32_t round, const Mode& mode) { for (std::uint32_t tid = 0; tid < Threads; ++tid) { const auto* in = &Input[tid * Inputs]; const std::uint64_t abits = in[0] | (static_cast(in[1]) << 32u); const std::uint64_t bbits = in[2] | (static_cast(in[3]) << 32u); const std::uint32_t mask = in[4]; - const double a = std::bit_cast(abits); - const double b = std::bit_cast(bbits); + const auto value = [&](std::uint64_t bits) { + const double result = std::bit_cast(bits); + return mode.flushed && std::fpclassify(result) == FP_SUBNORMAL ? std::copysign(0.0, result) : result; + }; + const double a = value(abits); + const double b = value(bbits); const bool unordered = std::isnan(a) || std::isnan(b); const std::array base{ false, @@ -186,13 +195,13 @@ void Check(std::uint32_t round) { expected[34] = 3.0 < a; expected[35] = 1.0 >= a; expected[36] = std::bit_cast(0x3fc45f306dc9c882ull) == a; - expected[37] = std::numeric_limits::denorm_min() < a; + expected[37] = (mode.flushed ? 0.0 : std::numeric_limits::denorm_min()) < a; expected[38] = std::fabs(a) < b; expected[39] = ClassMatch(abits & 0x7fffffffffffffffull, mask); expected[40] = !(std::fabs(a) > std::fabs(b)); for (std::uint32_t k = 0; k < expected.size(); ++k) { const bool actual = ((Output[tid * Results + k / 32u] >> (k % 32u)) & 1u) != 0u; - Require(actual == expected[k], "vopc f64 compares: round " + std::to_string(round) + " thread " + std::to_string(tid) + " compare " + std::to_string(k) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[k])); + Require(actual == expected[k], "vopc f64 compares: " + std::string(mode.name) + " round " + std::to_string(round) + " thread " + std::to_string(tid) + " compare " + std::to_string(k) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[k])); } } } @@ -203,10 +212,26 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - for (std::uint32_t round = 0; round < EdgeRounds + RandomRounds; ++round) { - FillInput(round); - Run(*device); - Check(round); + const std::array modes{{ + {"unknown mode", std::nullopt, false}, + {"mode 0, IEEE off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}, true}, + {"mode 1, IEEE off", ShaderRecompiler::ShaderFloatMode{0x60u, true, false, false}, false}, + {"mode 2, IEEE off", ShaderRecompiler::ShaderFloatMode{0x90u, true, false, false}, true}, + {"mode 3, IEEE off", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, false}, + {"mode 0, IEEE on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, true}, + {"mode 1, IEEE on", ShaderRecompiler::ShaderFloatMode{0x60u, true, true, false}, false}, + {"mode 2, IEEE on", ShaderRecompiler::ShaderFloatMode{0x90u, true, true, false}, true}, + {"mode 3, IEEE on", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}, false}, + }}; + for (const auto& mode : modes) { + const auto result = Compile(*device, mode); + for (std::uint32_t round = 0; round < EdgeRounds + RandomRounds; ++round) { + FillInput(round); + Output.fill(0xdeadbeefu); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(CompareCode.data())); + device->WaitIdle(); + Check(round, mode); + } } std::puts("vopc f64 compare tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/WindowLaneSpill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/WindowLaneSpill.cpp new file mode 100644 index 0000000000..f597fc79ee --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/WindowLaneSpill.cpp @@ -0,0 +1,82 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x7e020300, 0xbefc030c, 0x7e048500, 0xd7610003, 0x00010004, 0xd7610003, 0x00010205, 0xd7610003, 0x00010406, + 0xd7610003, 0x00010607, 0xbe840380, 0xbe850380, 0xbe860380, 0xbe870380, 0xd7600008, 0x00010103, 0xd7600009, + 0x00010303, 0xd760000a, 0x00010503, 0xd760000b, 0x00010703, 0xe0702000, 0x80020001, 0xbf810000, +}; + +alignas(256) constexpr std::array IndexedStoreCode{ + 0x7e020300, 0xbefc030c, 0x7e048500, 0xd7610003, 0x00010004, 0xd7610003, 0x00010205, 0xd7610003, 0x00010406, + 0xd7610003, 0x00010607, 0x7e048500, 0xbe840380, 0xbe850380, 0xbe860380, 0xbe870380, 0xd7600008, 0x00010103, + 0xd7600009, 0x00010303, 0xd760000a, 0x00010503, 0xd760000b, 0x00010703, 0xe0702000, 0x80020001, 0xbf810000, +}; + +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(16, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + 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(); +} + +void Check(const std::string& name) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Require(Output[tid] == tid, name + ": lane " + std::to_string(tid) + " stored " + std::to_string(Output[tid]) + " through the V# read back from a window register"); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < Threads) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + Run(*device, Code); + Check("window lane spill"); + Run(*device, IndexedStoreCode); + Check("window lane spill past an indexed store"); + std::puts("window lane spill 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/ZeroDwordFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroDwordFill.cpp deleted file mode 100644 index e291be51a4..0000000000 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroDwordFill.cpp +++ /dev/null @@ -1,133 +0,0 @@ -#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" -#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp" -#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" -#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" -#include "VulkanTestDevice.hpp" -#include -#include -#include -#include -#include -#include -#include - -namespace { - -using AgcDriver::Graphics::Require; -using AgcDriver::DriverDetail::MatchZeroDwordFill; - -alignas(256) constexpr std::array Code{0xd7460000u, 0x04010c04u, 0x7e020280u, 0xe0102000u, 0x80000100u, 0xbf810000u}; -constexpr std::uint32_t Untouched = 0xcafebabeu; -alignas(256) std::array Output{}; - -std::vector UserData(void* output, std::uint32_t records, std::uint32_t stride = 4, std::uint32_t format = 0x00014204u) { - const auto address = reinterpret_cast(output); - return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), records, format}; -} - -void MatchTests() { - const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1}; - const std::array packet{0xc0031500u, 4, 1, 1, 0x41u}; - const auto data = UserData(Output.data(), 1024); - const auto fill = MatchZeroDwordFill(packet, Code, data, compute); - Require(fill && fill->base == reinterpret_cast(Output.data()) && fill->bytes == 1024 && fill->pattern == std::array{}, "the zero dword fill kernel was not recognized as a zero fill"); - for (std::size_t word = 0; word < Code.size(); ++word) { - auto altered = Code; - altered[word] ^= 1u; - Require(!MatchZeroDwordFill(packet, altered, data, compute), "a different shader matched the zero dword fill kernel"); - } - Require(!MatchZeroDwordFill(packet, std::span(Code).first(5), data, compute), "a truncated shader matched"); - Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data(), 1024, 8), compute), "a strided descriptor matched a contiguous fill"); - Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x00814204u), compute), "an add-tid descriptor matched"); - Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x30014204u), compute), "a raw out-of-bounds mode matched"); - Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data() + 1, 1024), compute), "an unaligned destination matched"); - Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x00011204u), compute), "an 8-bit output format matched"); - Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x00017204u), compute), "an output format above 32_FLOAT matched"); - Require(MatchZeroDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x00015204u), compute).has_value(), "32_SINT output did not match"); - Require(MatchZeroDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x00016204u), compute).has_value(), "32_FLOAT output did not match"); - Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data(), 102), compute), "a record count not filling 16-byte units matched"); - auto extra = data; - extra.push_back(0); - Require(!MatchZeroDwordFill(packet, Code, extra, compute), "a different user SGPR count matched (the group id moves)"); - auto partial = packet; - partial[4] |= 0x20u; - Require(!MatchZeroDwordFill(partial, Code, data, compute), "a partial dispatch matched"); - auto noGroup = compute; - noGroup.groupIdEnable[0] = false; - Require(!MatchZeroDwordFill(packet, Code, data, noGroup), "a dispatch without the group id matched"); - const auto clamped = MatchZeroDwordFill(packet, Code, UserData(Output.data(), 64), compute); - Require(clamped && clamped->bytes == 256, "the fill exceeded the descriptor's records"); -} - -std::vector Commands(const std::vector& data, std::uint32_t groups, bool partial, std::uint32_t threads) { - std::vector words; - const auto registers = [&](std::uint32_t first, std::span values) { - words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); - words.push_back(first); - words.insert(words.end(), values.begin(), values.end()); - }; - const std::array shape{64, 1, 1}; - registers(0x207, shape); - const auto address = reinterpret_cast(Code.data()); - const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; - registers(0x20c, program); - const std::array resource{(static_cast(data.size()) << 1u) | (1u << 7u)}; - registers(0x213, resource); - registers(0x240, data); - words.insert(words.end(), {0xc0031500u, partial ? threads : groups, 1, 1, 0x41u | (partial ? 0x20u : 0u)}); - return words; -} - -void ExecuteTests() { - struct Case { - std::uint32_t groups, records, stride; - bool partial; - std::uint32_t threads; - }; - constexpr std::array cases{{ - {4, 1024, 4, false, 0}, - {16, 1024, 4, false, 0}, - {4, 64, 4, false, 0}, - {4, 100, 4, false, 0}, - {4, 102, 4, false, 0}, - {2, 1024, 8, false, 0}, - {0, 1024, 4, true, 70}, - {0, 1024, 4, false, 0}, - }}; - for (const auto& item : cases) { - Output.fill(Untouched); - const auto words = Commands(UserData(Output.data(), item.records, item.stride), item.groups, item.partial, item.threads); - Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; - AgcDriver::Submit(&packet, 0); - AgcDriverWaitIdle_nid_postfix(); - AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); - const auto launched = item.partial ? item.threads : item.groups * 64u; - const auto filled = std::min(launched, item.records); - const auto step = item.stride / 4u; - for (std::uint32_t i = 0; i < Output.size(); ++i) { - const bool written = i % step == 0 && i / step < filled; - const auto expected = written ? 0u : Untouched; - Require(Output[i] == expected, "zero dword fill mismatch at " + std::to_string(i) + " (groups " + std::to_string(item.groups) + ", records " + std::to_string(item.records) + ", stride " + std::to_string(item.stride) + "): got " + std::to_string(Output[i]) + ", expected " + std::to_string(expected)); - } - } -} - -} - -int main() { - try { - if (std::getenv("APS5_NO_FILL_HLE") == nullptr) MatchTests(); - { - const auto device = OpenVulkanTestDevice(); - if (!device) return VulkanTestSkipped; - } - ExecuteTests(); - AgcDriverShutdown_nid_postfix(); - std::puts("zero dword fill tests passed"); - return 0; - } catch (const std::exception& error) { - std::cerr << error.what() << '\n'; - AgcDriverShutdown_nid_postfix(); - return 1; - } -} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp index 5715c39d27..e5804bf70a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp @@ -142,14 +142,28 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - constexpr std::array cases{{ - {2u, 1u, true, "OOB_SELECT 2, one record"}, + constexpr std::uint32_t Wzyx = 7u | (6u << 3u) | (1u << 6u) | (0u << 9u); + constexpr std::array cases{{ + {0u, 0u, false, "OOB_SELECT 0, no records"}, + {0u, 1u, false, "OOB_SELECT 0, one record"}, + {0u, 16u, false, "OOB_SELECT 0, records cover the element's bytes"}, + {0u, 0xffffffffu, false, "OOB_SELECT 0, NUM_RECORDS 0xffffffff"}, + {0u, 0xffffffffu, false, "OOB_SELECT 0, NUM_RECORDS 0xffffffff, selectors W Z 1 0", Wzyx}, + {1u, 0u, false, "OOB_SELECT 1, no records"}, + {1u, 3u, true, "OOB_SELECT 1, NUM_RECORDS counts the three vertices' records"}, + {1u, 4u, true, "OOB_SELECT 1, NUM_RECORDS past the vertices"}, + {1u, 0xffffffffu, true, "OOB_SELECT 1, NUM_RECORDS 0xffffffff"}, + {1u, 3u, true, "OOB_SELECT 1, NUM_RECORDS 3, selectors W Z 1 0", Wzyx}, {2u, 0u, false, "OOB_SELECT 2, no records"}, - {3u, 16u, true, "OOB_SELECT 3, whole element in range"}, + {2u, 1u, true, "OOB_SELECT 2, one record"}, + {2u, 16u, true, "OOB_SELECT 2, whole element in range"}, {3u, 0u, false, "OOB_SELECT 3, no records"}, - {0u, 16u, true, "OOB_SELECT 0, whole element in range"}, - {3u, 16u, true, "OOB_SELECT 3, selectors W Z 1 0", 7u | (6u << 3u) | (1u << 6u) | (0u << 9u)}, - {3u, 0u, false, "OOB_SELECT 3, no records, selectors W Z 1 0", 7u | (6u << 3u) | (1u << 6u) | (0u << 9u)}, + {3u, 8u, false, "OOB_SELECT 3, the element only partly fits"}, + {3u, 15u, false, "OOB_SELECT 3, the element misses its last byte"}, + {3u, 16u, true, "OOB_SELECT 3, whole element in range"}, + {3u, 0xffffffffu, true, "OOB_SELECT 3, NUM_RECORDS 0xffffffff"}, + {3u, 16u, true, "OOB_SELECT 3, selectors W Z 1 0", Wzyx}, + {3u, 8u, false, "OOB_SELECT 3, partly fits, selectors W Z 1 0", Wzyx}, }}; for (const auto& value : cases) { Draw(*device, value); diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/MultisampleReadback.frag b/core/libs/prx/libSceAgcDriver/tests/shaders/MultisampleReadback.frag new file mode 100644 index 0000000000..2f0e104c90 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/MultisampleReadback.frag @@ -0,0 +1,20 @@ +#version 450 + +layout(push_constant) uniform Push { uint layer; uint width; uint height; uint samples; uint elementBytes; } params; +#if !READ_DEPTH +layout(location = 0) out uvec4 color; +#endif + +void main() { + uint x = uint(gl_FragCoord.x); + uint y = uint(gl_FragCoord.y); +#if READ_DEPTH + uint element = (x + y * params.width + params.layer * params.width * params.height) * params.samples + uint(gl_SampleID); + gl_FragDepth = 0.125 + float(element + 1u) / 1048576.0; +#else + uint value = x + y * params.width + params.layer * params.width * params.height + uint(gl_SampleID) * 31u; + color = uvec4(0x91352700u ^ value, 0x45219b00u ^ (value * 3u), 0xc7391d00u ^ (value * 5u), 0x6b43e100u ^ (value * 7u)); + if (params.elementBytes == 1u) color.x &= 255u; + else if (params.elementBytes == 2u) color.x &= 65535u; +#endif +} diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/MultisampleReadback.vert b/core/libs/prx/libSceAgcDriver/tests/shaders/MultisampleReadback.vert new file mode 100644 index 0000000000..b240e1901b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/MultisampleReadback.vert @@ -0,0 +1,6 @@ +#version 450 + +void main() { + vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + gl_Position = vec4(positions[gl_VertexIndex], 0.5, 1.0); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.frag b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.frag new file mode 100644 index 0000000000..0f0df0cae8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.frag @@ -0,0 +1,22 @@ +#version 450 +#ifdef STENCIL +#extension GL_ARB_shader_stencil_export : require +#endif + +layout(std430, binding = 0) readonly buffer Values { + uvec2 values[]; +}; + +layout(push_constant) uniform Fill { + uint width; + uint count; +} fill; + +void main() { + const uvec2 at = uvec2(gl_FragCoord.xy); + const uint slot = (at.y * fill.width + at.x) * fill.count + uint(gl_SampleID); + gl_FragDepth = uintBitsToFloat(values[slot].x); +#ifdef STENCIL + gl_FragStencilRefARB = int(values[slot].y); +#endif +} diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.vert b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.vert new file mode 100644 index 0000000000..05606ecec4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.vert @@ -0,0 +1,6 @@ +#version 450 + +void main() { + const vec2 corner = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2)); + gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderRead.comp b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderRead.comp new file mode 100644 index 0000000000..89a0fed927 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderRead.comp @@ -0,0 +1,39 @@ +#version 450 +#extension GL_EXT_samplerless_texture_functions : require + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +#ifdef COLOR +layout(binding = 0) uniform texture2DMS colors; +#else +layout(binding = 0) uniform texture2DMS depths; +#ifdef STENCIL +layout(binding = 1) uniform utexture2DMS stencils; +#endif +#endif + +layout(std430, binding = 2) buffer Values { + uvec4 values[]; +}; + +layout(push_constant) uniform Read { + uvec2 extent; + uint count; +} read; + +void main() { + const uvec2 at = gl_GlobalInvocationID.xy; + if (at.x >= read.extent.x || at.y >= read.extent.y) return; + for (uint index = 0u; index < read.count; ++index) { + const uint slot = (at.y * read.extent.x + at.x) * read.count + index; +#ifdef COLOR + values[slot] = floatBitsToUint(texelFetch(colors, ivec2(at), int(index))); +#else + uvec4 value = uvec4(floatBitsToUint(texelFetch(depths, ivec2(at), int(index)).r), 0u, 0u, 0u); +#ifdef STENCIL + value.y = texelFetch(stencils, ivec2(at), int(index)).r; +#endif + values[slot] = value; +#endif + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderTexels.comp b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderTexels.comp new file mode 100644 index 0000000000..b169405ba9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderTexels.comp @@ -0,0 +1,25 @@ +#version 450 + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +layout(binding = 0, TEXEL_FORMAT) uniform restrict uimage2DMS texels; + +layout(std430, binding = 1) buffer Values { + uvec4 values[]; +}; + +layout(push_constant) uniform Access { + uvec2 extent; + uint count; + uint store; +} access; + +void main() { + const uvec2 at = gl_GlobalInvocationID.xy; + if (at.x >= access.extent.x || at.y >= access.extent.y) return; + for (uint index = 0u; index < access.count; ++index) { + const uint slot = (at.y * access.extent.x + at.x) * access.count + index; + if (access.store != 0u) imageStore(texels, ivec2(at), int(index), values[slot]); + else values[slot] = imageLoad(texels, ivec2(at), int(index)); + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/test_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('= 270 + root = native(root) + current = root / version + current.mkdir(parents=True) + old = time.time() - 30 * 24 * 3600 + for parent in (root, outside): + for index, name in enumerate(siblings): + folder = parent / name + folder.mkdir() + (folder / 'sentinel').write_bytes(b'preserve outside the cache root') + if index != 1: + (folder / 'last-used').write_bytes(b'') + os.utime(folder / 'last-used', (old if index == 0 else time.time(),) * 2) + os.utime(folder, (old, old)) + (outside / 'outside.tmp.1').write_bytes(b'not a cache temporary') + os.utime(outside / 'outside.tmp.1', (old, old)) + for name in ('shader.bin', 'old.tmp.1', 'fresh.tmp.1', 'last-used'): + (current / name).write_bytes(b'cached bytes') + for name in ('shader.bin', 'old.tmp.1', 'last-used'): + os.utime(current / name, (old, old)) + before = snapshot(outside) + if mode.startswith('extended'): + environment['ANYPS5_SHADER_CACHE_DIR'] = str(root) + elif mode == 'device': + environment['ANYPS5_SHADER_CACHE_DIR'] = '\\\\.\\' + str(root)[4:] + else: + environment['ANYPS5_SHADER_CACHE_DIR'] = str(root)[4:] if os.name == 'nt' else str(root) + started = time.time() + try: + result = run() + except subprocess.TimeoutExpired as error: + assert snapshot(outside) == before, 'housekeeping changed files outside its root' + raise AssertionError('housekeeping did not return within five seconds; child terminated') from error + assert snapshot(outside) == before, 'housekeeping changed files outside its root' + assert result.returncode == 0, result.stdout + assert 'housekeeping version ' + version in result.stdout, result.stdout + assert (current / 'last-used').stat().st_mtime >= started - 2, 'active cache stamp was not refreshed' + skipped = mode.startswith('extended') or mode == 'device' + for name in siblings[:2]: + assert (root / name).exists() == skipped, 'unexpected stale cache directory eviction' + assert (root / siblings[2] / 'sentinel').read_bytes() == b'preserve outside the cache root' + assert (current / 'old.tmp.1').exists() == skipped, 'unexpected old temporary removal' + assert (current / 'fresh.tmp.1').read_bytes() == b'cached bytes' + assert (current / 'shader.bin').read_bytes() == b'cached bytes' + print(mode + ': returned, active stamp refreshed, outside tree intact, eviction policy verified') + finally: + assert sandbox.parent == temporary and sandbox.name.startswith('aps5-housekeeping-') + shutil.rmtree(native(sandbox)) + + +if __name__ == '__main__': + main() diff --git a/core/libs/prx/libSceAgcDriver/tests/test_shader_cache_prebuilt_discovery.py b/core/libs/prx/libSceAgcDriver/tests/test_shader_cache_prebuilt_discovery.py new file mode 100644 index 0000000000..2b7523f8ab --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/test_shader_cache_prebuilt_discovery.py @@ -0,0 +1,74 @@ +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + + +def native(path): + return Path('\\\\?\\' + str(path)) if os.name == 'nt' else path + + +def snapshot(directory): + return {str(path.relative_to(directory)): None if path.is_dir() else path.read_bytes() + for path in directory.rglob('*')} + + +def main(): + executable = str(Path(sys.argv[1]).resolve()) + mode = sys.argv[2] + assert mode in ('plain', 'relative', 'extended', 'extended-forward', 'device', 'plain-long', 'extended-long') + assert os.name == 'nt' or mode in ('plain', 'relative', 'plain-long') + temporary = Path(tempfile.gettempdir()).resolve() + sandbox = Path(tempfile.mkdtemp(prefix='aps5-prebuilt-discovery-')).resolve() + try: + outside = sandbox / 'outside' + outside.mkdir() + (outside / 'z.pack').write_bytes(b'outside directory canary') + (outside / 'sentinel').write_bytes(b'unchanged outside the cache root') + original = snapshot(outside) + for layout in ('pack', 'loose', 'versioned', 'file', 'empty', 'missing'): + root = sandbox / (layout + '-\u00e4\u6f22') + if mode.endswith('long'): + while len(str(root)) < 270: + root /= 'd' * 40 + assert len(str(root / 'prebuilt')) >= 278 + root = native(root) + root.mkdir(parents=True) + plain = str(root)[4:] if os.name == 'nt' else str(root) + alias = plain + if mode.startswith('extended'): + alias = str(root) + if mode == 'extended-forward': + alias = '\\\\?\\' + plain.replace('\\', '/') + elif mode == 'device': + alias = '\\\\.\\' + plain + elif mode == 'relative': + alias = os.path.join('.', os.path.relpath(plain, outside)) + environment = dict(os.environ, ANYPS5_NO_SHADER_CACHE='0', ANYPS5_SHADER_CACHE_DIR=alias, + APS5_NO_PIPELINE_JOURNAL='1') + + def run(action): + try: + result = subprocess.run([executable, '--' + action + '-prebuilt-discovery', layout], cwd=outside, + env=environment, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, errors='replace', timeout=5) + except subprocess.TimeoutExpired as error: + raise AssertionError('prebuilt discovery did not return within five seconds; child terminated') from error + assert snapshot(outside) == original, 'prebuilt discovery changed files outside its root' + assert result.returncode == 0, mode + ' ' + layout + ':\n' + result.stdout + assert 'prebuilt discovery ' + ('seeded' if action == 'seed' else 'verified') + ' ' + layout in result.stdout + + run('seed') + before = snapshot(root) + run('verify') + assert snapshot(root) == before, 'prebuilt discovery modified its read-only input' + print(mode + ': six prebuilt layouts verified; cache and outside trees unchanged') + finally: + assert sandbox.parent == temporary and sandbox.name.startswith('aps5-prebuilt-discovery-') + shutil.rmtree(native(sandbox)) + + +if __name__ == '__main__': + main() diff --git a/core/libs/prx/libSceAgcDriver/tools/Pm4Analysis.cpp b/core/libs/prx/libSceAgcDriver/tools/Pm4Analysis.cpp new file mode 100644 index 0000000000..e16f862818 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tools/Pm4Analysis.cpp @@ -0,0 +1,765 @@ +#include "Pm4Analysis.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/LabelStores.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Pm4Analysis { +namespace { + +constexpr std::uint64_t SmallStoreBytes = 4096; +constexpr std::size_t ShownValues = 4; +constexpr std::size_t MarkerCharacters = 48; +constexpr std::size_t ListedWaits = 32; +constexpr std::size_t LastSubmissionsPerQueue = 2; +constexpr std::size_t ShownPackets = 48; +constexpr std::size_t ContextBefore = 8; +constexpr std::size_t ContextAfter = 4; +constexpr const char* CompareNames[8] = {"always", "<", "<=", "==", "!=", ">=", ">", "reserved"}; + +std::string format(const char* text, ...) { + va_list arguments; + va_start(arguments, text); + va_list measured; + va_copy(measured, arguments); + const int length = std::vsnprintf(nullptr, 0, text, measured); + va_end(measured); + std::string result(length < 0 ? 0 : static_cast(length), '\0'); + const int written = length < 0 ? length : std::vsnprintf(result.data(), result.size() + 1, text, arguments); + va_end(arguments); + if (length < 0 || written != length) throw std::runtime_error("PM4 analysis: cannot format a line"); + return result; +} + +void require(bool condition, const std::string& reason) { + if (!condition) throw std::runtime_error("PM4 analysis: " + reason); +} + +std::uint64_t address(std::uint32_t low, std::uint32_t high) { + return low | (static_cast(high) << 32u); +} + +std::string hex(std::uint64_t value) { + return format("0x%llx", static_cast(value)); +} + +std::string values(std::span words) { + std::string text; + for (std::size_t i = 0; i < words.size() && i < ShownValues; ++i) text += format(i == 0 ? "0x%x" : " 0x%x", words[i]); + if (words.size() > ShownValues) text += format(" (+%zu)", words.size() - ShownValues); + return text; +} + +std::string marker(std::span payload) { + std::string text; + for (const auto byte : std::as_bytes(payload)) { + const auto character = std::to_integer(byte); + if (character == 0 || text.size() == MarkerCharacters) break; + text.push_back(character >= 0x20 && character < 0x7f ? static_cast(character) : '?'); + } + return "\"" + text + "\""; +} + +std::string describeNop(std::span p) { + const auto at = [&](std::size_t index) { return index < p.size() ? p[index] : 0u; }; + switch ((p[0] >> 2u) & 0x3fu) { + case 0: return format("%zu dwords", p.size() - 1); + case 0x06: return format("output %u index %d", at(1), static_cast(at(2))); + case 0x0b: return marker(p.subspan(1)); + case 0x17: return format("output %u index %d mode %u argument %s", at(1), static_cast(at(2)), at(3), hex(address(at(4), at(5))).c_str()); + case 0x1a: return format("operation %u", at(1)); + case 0x09: case 0x0c: return {}; + default: return values(p.subspan(1)); + } +} + +std::string describeDmaEnd(std::uint32_t select, std::uint32_t low, std::uint32_t high, bool destination) { + if (select == Pm4::DmaSelectGds) return format("GDS offset 0x%x", low); + if (destination && select == Pm4::DmaDestinationNowhere) return "nothing (prefetch)"; + return hex(address(low, high)); +} + +std::string describe(std::span p) { + const auto opcode = (p[0] >> 8u) & 0xffu; + const auto at = [&](std::size_t index) { return index < p.size() ? p[index] : 0u; }; + switch (opcode) { + case 0x10: return describeNop(p); + case 0x3c: case 0x93: { + const auto wait = Pm4::DecodeWait(p); + return format("[%s] (%zu bytes) %s 0x%llx mask 0x%llx poll %u", hex(wait.address).c_str(), wait.bytes, CompareNames[wait.function], static_cast(wait.reference), static_cast(wait.mask), wait.poll); + } + case 0x49: return format("event 0x%x data select %u interrupt %u address %s data %s", p[1] & 0x3fu, p[2] >> 29u, (p[2] >> 24u) & 7u, hex(address(p[3], p[4])).c_str(), hex(address(p[5], p[6])).c_str()); + case 0x37: return format("dst %u address %s%s: %s", (p[1] >> 8u) & 0xfu, hex(address(p[2], p[3])).c_str(), (p[1] & 0x10000u) != 0 ? " (one address)" : "", values(p.subspan(4)).c_str()); + case 0x40: { + const auto source = Pm4::CopyDataSource(p); + if (Pm4::CopyDataToRegister(p)) return format("source select %u source %s to register 0x%x", source, hex(address(p[2], p[3])).c_str(), p[4]); + return format("source select %u source %s to %s (%zu bytes)", source, hex(address(p[2], p[3])).c_str(), hex(address(p[4], p[5])).c_str(), Pm4::CopyDataBytes(p)); + } + case 0x50: { + const auto from = Pm4::DmaSource(p) == Pm4::DmaSourceData ? format("fill 0x%08x", p[2]) : "copy from " + describeDmaEnd(Pm4::DmaSource(p), p[2], p[3], false); + return format("%s to %s (%zu bytes)", from.c_str(), describeDmaEnd(Pm4::DmaDestination(p), p[4], p[5], true).c_str(), Pm4::DmaBytes(p)); + } + case 0x46: { + auto text = format("event 0x%x index %u", p[1] & 0x3fu, (p[1] >> 8u) & 7u); + if (Pm4::SampleDump(p)) text += " samples to " + hex(Pm4::SampleDumpAddress(p)); + return text; + } + case 0x45: { + const auto write = Pm4::DecodeConditionalWrite(p); + return format("[%s] %s 0x%x mask 0x%x: store 0x%x to %s", hex(write.compared).c_str(), CompareNames[p[1] & 7u], p[4], p[5], write.value, hex(write.destination).c_str()); + } + case 0x83: { + const auto dump = Pm4::DecodeConstantRamDump(p); + return format("offset 0x%x %u dwords to %s", dump.offset, dump.words, hex(dump.destination).c_str()); + } + case 0x59: return (p[1] & Pm4::RewindValid) != 0 ? "valid" : "not valid"; + case 0x68: case 0x69: case 0x76: case 0x78: case 0x79: case 0x7a: + if (Pm4::IsTagMarker(p)) return format("tag 0x%08x", p[2]); + return format("0x%x = %s", p[1], values(p.subspan(2)).c_str()); + case 0x11: return format("index %u base %s", at(1), hex(address(at(2), at(3))).c_str()); + case 0x12: case 0x13: case 0x2a: case 0x2f: case 0x42: return format("%u", at(1)); + case 0x15: return format("groups %ux%ux%u initiator 0x%x", at(1), at(2), at(3), at(4)); + case 0x16: return p.size() == 4 ? format("arguments %s initiator 0x%x", hex(address(at(1), at(2))).c_str(), at(3)) : format("offset 0x%x initiator 0x%x", at(1), at(2)); + case 0x20: return format("control 0x%x address %s", at(1), hex(address(at(2), at(3))).c_str()); + case 0x22: return format("[%s] guards %zu dwords", hex(address(at(1), at(2))).c_str(), Pm4::ConditionalWords(p)); + case 0x24: case 0x25: return format("offset 0x%x locations 0x%x 0x%x initiator 0x%x", at(1), at(2), at(3), at(4)); + case 0x2c: case 0x38: return format("offset 0x%x control 0x%x count %u count address %s stride %u initiator 0x%x", at(1), at(4), at(5), hex(address(at(6), at(7))).c_str(), at(8), at(9)); + case 0x26: return format("address %s", hex(address(at(1), at(2))).c_str()); + case 0x27: return format("%u indices at %s max %u initiator 0x%x", at(4), hex(address(at(2), at(3))).c_str(), at(1), at(5)); + case 0x2d: return format("%u vertices initiator 0x%x", at(1), at(2)); + case 0x35: return format("%u indices from %u max %u initiator 0x%x", at(3), at(2), at(1), at(4)); + case 0x3f: return format("predicated, address %s %u dwords", hex(address(at(1), at(2) & 0xffffu)).c_str(), at(3) & 0xfffffu); + case 0x58: { + const auto base = ((static_cast(at(5)) << 32u) | at(4)) << 8u; + const auto bytes = ((static_cast(at(3)) << 32u) | at(2)) << 8u; + return format("control 0x%x base %s bytes %s", at(1), hex(base).c_str(), hex(bytes).c_str()); + } + case 0x63: case 0x64: case 0x9f: + return at(3) == 0x80000000u ? format("%u pairs at %s", at(4), hex(address(at(1), at(2))).c_str()) : format("0x%x x%u from %s", at(3), at(4), hex(address(at(1), at(2))).c_str()); + case 0x81: return format("offset 0x%x %zu dwords", at(1), p.size() > 2 ? p.size() - 2 : 0); + default: return values(p.subspan(1)); + } +} + +bool conditionalStart(std::uint32_t header) { + const auto opcode = (header >> 8u) & 0xffu; + return opcode == 0x22 || (opcode == 0x3f && Pm4::Predicated(header)); +} + +struct Located { + std::size_t submission = 0; + std::size_t offset = 0; + std::uint32_t header = 0; +}; + +struct Store { + Located at; + MemoryStore store; +}; + +struct Waiter { + Located at; + MemoryWait wait; +}; + +using Key = std::tuple; + +struct QueueStores { + std::vector byExecution; + std::vector byEntry; +}; + +struct RangeStores { + std::map queues; + std::size_t count = 0; +}; + +struct Accepting { + std::map> prefix; + std::size_t total = 0; +}; + +using CompareKey = std::tuple; +using Listed = std::vector>; + +class Matcher { +public: + Matcher(const std::vector& submissions, const std::vector& stores) : submissions(submissions), stores(stores) { + for (std::size_t index = 0; index < stores.size(); ++index) (stores[index].store.store.bytes <= SmallStoreBytes ? small : large).push_back(index); + std::sort(small.begin(), small.end(), [&](std::size_t left, std::size_t right) { return stores[left].store.store.address < stores[right].store.store.address; }); + } + + void Match(WaitReport& report, const Waiter& waiter) { + const auto& fields = report.wait.fields; + const auto& range = rangeFor(fields.address, fields.bytes); + const auto& accepting = acceptingFor(range, report.wait); + report.writerCount = range.count; + const auto queue = submissions[waiter.at.submission].queue; + const auto found = range.queues.find(queue); + const bool stored = found != range.queues.end(); + const auto& own = stored ? found->second.byExecution : empty; + const auto before = lowerExecution(own, execution(waiter.at)); + const auto others = range.count - own.size(); + const auto ownAccepting = stored ? accepting.prefix.at(queue)[before] : 0; + const auto otherAccepting = accepting.total - (stored ? accepting.prefix.at(queue).back() : 0); + Listed listed; + + std::optional lastValue; + bool lastAccepts = false; + if (before != 0) { + lastValue = settledValue(own[before - 1], fields); + lastAccepts = lastValue.has_value() && Pm4::WaitComparesValue(report.wait.packet, *lastValue); + if (lastAccepts && others == 0) { + setReleased(report, WaitReport::Route::CurrentValue, + format("released: %s, the last store before it on its queue, leaves 0x%llx at %s, which the compare accepts, and no other queue stores to the awaited bytes", where(own[before - 1]).c_str(), static_cast(*lastValue), hex(fields.address).c_str())); + listed.emplace_back(own[before - 1], WriterMatch::Effect::CurrentValue); + report.writers = writers(listed, waiter); + return; + } + } + + std::optional kept; + std::size_t newer = 0; + for (auto position = before; position != 0;) { + const auto index = own[--position]; + if (stores[index].at.submission != waiter.at.submission) break; + if (!keptAccepting(index, report.wait)) continue; + kept = index; + newer = before - 1 - position + others; + break; + } + if (kept.has_value() && newer < DriverDetail::LabelStoreHistory) { + const auto value = storedBytes(stores[*kept].store.store, fields.address, fields.bytes); + setReleased(report, WaitReport::Route::LabelHistory, + format("released: %s stores 0x%llx after the waiting submission entered the driver, and at most %zu other stores to the awaited bytes can run between it and the wait, so the driver's label history, which keeps the last %zu stores per dword over all queues, still holds it when the wait runs", + where(*kept).c_str(), static_cast(*value), newer, DriverDetail::LabelStoreHistory)); + listed.emplace_back(*kept, WriterMatch::Effect::LabelHistory); + report.writers = writers(listed, waiter); + return; + } + + if (kept.has_value()) listed.emplace_back(*kept, WriterMatch::Effect::Evicted); + if (before != 0 && own[before - 1] != kept) listed.emplace_back(own[before - 1], lastAccepts ? WriterMatch::Effect::Overwritten : lastValue.has_value() ? WriterMatch::Effect::Rejected : WriterMatch::Effect::Undecided); + if (stored) { + const auto& prefix = accepting.prefix.at(queue); + const auto end = before == 0 ? 0 : before - 1; + if (const auto earlier = lastAcceptingBefore(prefix, end); earlier.has_value() && own[*earlier] != kept) { + listed.emplace_back(own[*earlier], settledValue(own[*earlier], fields).has_value() ? WriterMatch::Effect::Overwritten : WriterMatch::Effect::Undecided); + } + if (const auto later = firstAcceptingFrom(prefix, before); later.has_value()) listed.emplace_back(own[*later], WriterMatch::Effect::After); + } + const auto queues = noteOthers(range, queue, submissions[waiter.at.submission].ordinal, listed); + + const auto awaited = format("%s+%zu", hex(fields.address).c_str(), fields.bytes); + if (ownAccepting + otherAccepting == 0) { + report.verdict = WaitReport::Verdict::Unreleased; + report.reason = unreleasedReason(range, awaited, own.size() - before); + if (!submissions[waiter.at.submission].heldKnown) { + report.verdict = WaitReport::Verdict::Unknown; + report.reason = "unknown: the index has no held column, so its value may have passed the compare at submit; otherwise " + report.reason; + } + } else if (others != 0) { + report.verdict = WaitReport::Verdict::Unknown; + report.crossQueue = true; + report.reason = format("unknown: depends on cross-queue timing (stores on %s): %zu of the %zu stores to %s on other queues write a value the compare may accept, and the capture does not fix when a store of another queue runs relative to the wait", + queues.c_str(), otherAccepting, others, awaited.c_str()); + if (lastAccepts) report.reason += format("; %s, the last store before it on its queue, leaves a value the compare accepts, but a store of another queue can replace it", where(own[before - 1]).c_str()); + if (kept.has_value()) report.reason += format("; %s, a label earlier in the waiting submission that the compare accepts, can be pushed out of the driver's label history by %zu stores to the awaited bytes, as many as it keeps per dword", where(*kept).c_str(), newer); + } else { + report.verdict = WaitReport::Verdict::Unknown; + report.reason = "unknown: " + sameQueueReason(own[before - 1], fields, kept, newer); + } + report.writers = writers(listed, waiter); + } + +private: + static constexpr std::size_t ListedPerQueue = 4; + + const std::vector& submissions; + const std::vector& stores; + const std::vector empty; + std::vector small; + std::vector large; + std::map, RangeStores> ranges; + std::map accepting; + + Key execution(const Located& at) const { + const auto& submission = submissions[at.submission]; + return {submission.serial, submission.ordinal, at.offset}; + } + + Key entry(const Located& at) const { + return {submissions[at.submission].ordinal, at.offset, 0}; + } + + std::size_t lowerExecution(const std::vector& list, const Key& key) const { + return static_cast(std::lower_bound(list.begin(), list.end(), key, [&](std::size_t index, const Key& value) { return execution(stores[index].at) < value; }) - list.begin()); + } + + static bool covers(const Store& store, std::uint64_t at, std::size_t bytes) { + return at >= store.store.store.address && at - store.store.store.address <= store.store.store.bytes && bytes <= store.store.store.bytes - (at - store.store.store.address); + } + + static std::optional storedBytes(const Pm4::PacketStore& store, std::uint64_t at, std::size_t bytes) { + if (store.value == Pm4::PacketStore::Value::RunTime || at < store.address || at - store.address > store.bytes || bytes > store.bytes - (at - store.address)) return std::nullopt; + std::uint64_t value = 0; + for (std::size_t i = 0; i < bytes; ++i) { + const auto index = static_cast(at - store.address) + i; + const auto byte = store.value == Pm4::PacketStore::Value::Bytes ? store.data.at(index) : store.data.at(index % store.data.size()); + value |= std::to_integer(byte) << (8u * i); + } + return value; + } + + std::optional settledValue(std::size_t index, const Pm4::WaitFields& fields) const { + if (stores[index].store.conditional) return std::nullopt; + return storedBytes(stores[index].store.store, fields.address, fields.bytes); + } + + bool mayAccept(std::size_t index, const MemoryWait& wait) const { + const auto value = storedBytes(stores[index].store.store, wait.fields.address, wait.fields.bytes); + return !value.has_value() || Pm4::WaitComparesValue(wait.packet, *value); + } + + bool keptAccepting(std::size_t index, const MemoryWait& wait) const { + const auto& store = stores[index]; + const auto& fields = wait.fields; + if (!store.store.label || store.store.conditional || !DriverDetail::KeepsLabelStore(store.store.store.address, store.store.store.bytes)) return false; + if (!covers(store, fields.address, fields.bytes) || (fields.address - store.store.store.address) % 4 != 0) return false; + const auto value = storedBytes(store.store.store, fields.address, fields.bytes); + return value.has_value() && Pm4::WaitComparesValue(wait.packet, *value); + } + + static std::optional lastAcceptingBefore(const std::vector& prefix, std::size_t end) { + if (prefix[end] == 0) return std::nullopt; + return static_cast(std::lower_bound(prefix.begin(), prefix.begin() + static_cast(end) + 1, prefix[end]) - prefix.begin()) - 1; + } + + static std::optional firstAcceptingFrom(const std::vector& prefix, std::size_t from) { + if (prefix.back() == prefix[from]) return std::nullopt; + return static_cast(std::upper_bound(prefix.begin() + static_cast(from), prefix.end(), prefix[from]) - prefix.begin()) - 1; + } + + std::string where(std::size_t index) const { + const auto& at = stores[index].at; + return format("#%llu +0x%04zx %s", static_cast(submissions[at.submission].ordinal), at.offset, Pm4::Name(at.header).c_str()); + } + + static void setReleased(WaitReport& report, WaitReport::Route route, std::string reason) { + report.verdict = WaitReport::Verdict::Released; + report.route = route; + report.reason = std::move(reason); + } + + std::string noteOthers(const RangeStores& range, std::uint32_t queue, std::uint64_t entered, Listed& listed) const { + std::string text; + for (const auto& [storeQueue, lists] : range.queues) { + if (storeQueue == queue) continue; + const auto& list = lists.byEntry; + const auto ordinal = [&](std::size_t position) { return submissions[stores[list[position]].at.submission].ordinal; }; + auto high = static_cast(std::lower_bound(list.begin(), list.end(), entered, [&](std::size_t index, std::uint64_t value) { return submissions[stores[index].at.submission].ordinal < value; }) - list.begin()); + auto low = high; + std::vector nearest; + while (nearest.size() < ListedPerQueue && (low != 0 || high != list.size())) { + const bool takeLow = high == list.size() || (low != 0 && entered - ordinal(low - 1) <= ordinal(high) - entered); + nearest.push_back(takeLow ? list[--low] : list[high++]); + } + std::sort(nearest.begin(), nearest.end(), [&](std::size_t left, std::size_t right) { return entry(stores[left].at) < entry(stores[right].at); }); + std::vector ordinals; + for (const auto index : nearest) { + listed.emplace_back(index, WriterMatch::Effect::OtherQueue); + const auto at = submissions[stores[index].at.submission].ordinal; + if (ordinals.empty() || ordinals.back() != at) ordinals.push_back(at); + } + text += format("%squeue 0x%x at", text.empty() ? "" : "; ", storeQueue); + for (std::size_t i = 0; i < ordinals.size(); ++i) text += format("%s #%llu", i == 0 ? "" : ",", static_cast(ordinals[i])); + if (list.size() > nearest.size()) text += format(" (+%zu more)", list.size() - nearest.size()); + } + return text; + } + + std::string sameQueueReason(std::size_t last, const Pm4::WaitFields& fields, const std::optional& kept, std::size_t newer) const { + const auto& store = stores[last]; + const auto lastStore = where(last) + ", the last store before it on its queue,"; + if (store.store.conditional) return lastStore + " is conditional (COND_WRITE, inside a COND_EXEC range or a predicated packet), so the value the wait reads depends on run time"; + if (store.store.store.value == Pm4::PacketStore::Value::RunTime) return lastStore + " stores a value known only when the GPU runs it"; + const auto value = storedBytes(store.store.store, fields.address, fields.bytes); + if (!value.has_value()) return lastStore + " writes only part of the awaited bytes"; + auto text = format("%s leaves 0x%llx, which the compare rejects, and the capture does not prove that the driver's label history still holds an earlier store the compare accepts when the wait runs", lastStore.c_str(), static_cast(*value)); + if (kept.has_value()) return text + format(": %s, a label earlier in the waiting submission that the compare accepts, can be pushed out of it by %zu newer stores to the awaited bytes", where(*kept).c_str(), newer); + return text + ": no store the compare accepts is a RELEASE_MEM or WRITE_DATA label the history keeps earlier in the waiting submission, and whether an earlier one ran after the submission entered the driver depends on when it ran"; + } + + static std::string unreleasedReason(const RangeStores& range, const std::string& awaited, std::size_t later) { + const std::string prefix = "unreleased by the GPU; a CPU store could release it (CPU and shader stores are not captured): its value at submit fails the compare and "; + if (range.count == 0) return prefix + "no packet in the capture writes " + awaited; + if (range.count == later) return prefix + format("the %zu stores to %s in the capture are later on its queue and run after it", range.count, awaited.c_str()); + auto text = prefix + format("none of the %zu stores to %s in the capture that can run before it writes a value the compare accepts", range.count - later, awaited.c_str()); + if (later != 0) text += format(" (%zu more are later on its queue and run after it)", later); + return text; + } + + std::vector writers(Listed& listed, const Waiter& waiter) const { + std::stable_sort(listed.begin(), listed.end(), [](const auto& left, const auto& right) { return left.second < right.second; }); + std::vector result; + std::set seen; + const auto& waiting = submissions[waiter.at.submission]; + for (const auto& [index, effect] : listed) { + if (result.size() == Analysis::ListedWriters) break; + if (!seen.insert(index).second) continue; + const auto& at = stores[index].at; + const auto& submission = submissions[at.submission]; + WriterMatch match; + match.at = {submission.ordinal, submission.queue, submission.serial, at.offset, Pm4::Name(at.header)}; + if (submission.queue == waiting.queue) match.order = execution(at) < execution(waiter.at) ? WriterMatch::Order::EarlierOnQueue : WriterMatch::Order::LaterOnQueue; + match.effect = effect; + match.stored = storedBytes(stores[index].store.store, waiter.wait.fields.address, waiter.wait.fields.bytes); + match.accepted = mayAccept(index, waiter.wait); + result.push_back(std::move(match)); + } + return result; + } + + const Accepting& acceptingFor(const RangeStores& range, const MemoryWait& wait) { + const auto& fields = wait.fields; + const auto [found, inserted] = accepting.try_emplace({fields.address, fields.bytes, fields.function, fields.reference, fields.mask}); + if (!inserted) return found->second; + auto& result = found->second; + for (const auto& [queue, lists] : range.queues) { + auto& prefix = result.prefix[queue]; + prefix.push_back(0); + for (const auto index : lists.byExecution) prefix.push_back(prefix.back() + (mayAccept(index, wait) ? 1 : 0)); + result.total += prefix.back(); + } + return result; + } + + const RangeStores& rangeFor(std::uint64_t at, std::size_t bytes) { + const auto [found, inserted] = ranges.try_emplace({at, bytes}); + if (!inserted) return found->second; + auto& range = found->second; + const auto overlaps = [&](std::size_t index) { + const auto& store = stores[index].store.store; + return store.address < at + bytes && at < store.address + store.bytes; + }; + std::vector matches; + const auto first = std::lower_bound(small.begin(), small.end(), at >= SmallStoreBytes ? at - SmallStoreBytes + 1 : 0, [&](std::size_t index, std::uint64_t value) { return stores[index].store.store.address < value; }); + for (auto it = first; it != small.end() && stores[*it].store.store.address < at + bytes; ++it) { + if (overlaps(*it)) matches.push_back(*it); + } + for (const auto index : large) { + if (overlaps(index)) matches.push_back(index); + } + range.count = matches.size(); + for (const auto index : matches) { + auto& lists = range.queues[submissions[stores[index].at.submission].queue]; + lists.byExecution.push_back(index); + lists.byEntry.push_back(index); + } + for (auto& [queue, lists] : range.queues) { + std::sort(lists.byExecution.begin(), lists.byExecution.end(), [&](std::size_t left, std::size_t right) { return execution(stores[left].at) < execution(stores[right].at); }); + std::sort(lists.byEntry.begin(), lists.byEntry.end(), [&](std::size_t left, std::size_t right) { return entry(stores[left].at) < entry(stores[right].at); }); + } + return range; + } +}; + +void matchRewind(WaitReport& report, const std::map, std::vector>& segments) { + const auto& ordinals = segments.at({report.at.queue, report.at.serial}); + const auto self = std::find(ordinals.begin(), ordinals.end(), report.at.ordinal); + if (self != ordinals.end() && self + 1 != ordinals.end()) { + report.verdict = WaitReport::Verdict::Released; + report.route = WaitReport::Route::RewindTail; + report.reason = format("released: its tail entered the driver as #%llu", static_cast(*(self + 1))); + return; + } + if (report.wait.released) { + report.verdict = WaitReport::Verdict::Unknown; + report.route = WaitReport::Route::None; + report.reason = "unknown: the valid bit was set when the submission entered the driver, but the driver reads it again when the queue reaches the REWIND and the capture has no tail"; + return; + } + report.verdict = WaitReport::Verdict::Unreleased; + report.reason = "unreleased: no rewind tail in the capture, so the title had not set the REWIND valid bit when the capture ended"; +} + +const char* verdictName(WaitReport::Verdict verdict) { + switch (verdict) { + case WaitReport::Verdict::Released: return "released"; + case WaitReport::Verdict::Unknown: return "UNKNOWN"; + case WaitReport::Verdict::Unreleased: return "UNRELEASED"; + } + throw std::runtime_error("PM4 analysis: unknown verdict"); +} + +std::string describeWriter(const WriterMatch& match) { + const char* order = match.order == WriterMatch::Order::EarlierOnQueue ? "earlier on this queue" : match.order == WriterMatch::Order::LaterOnQueue ? "later on this queue" : "on another queue"; + std::string text = format("#%llu queue 0x%x serial %llu +0x%04zx %s, %s", static_cast(match.at.ordinal), match.at.queue, static_cast(match.at.serial), match.at.offset, match.at.name.c_str(), order); + text += match.stored.has_value() ? format(", stores 0x%llx", static_cast(*match.stored)) : ", stores part of the awaited bytes or a value known only at run time"; + switch (match.effect) { + case WriterMatch::Effect::CurrentValue: return text + ": the value the wait reads, releases it"; + case WriterMatch::Effect::LabelHistory: return text + ": still in the driver's label history when the wait runs, releases it"; + case WriterMatch::Effect::OtherQueue: return text + (match.accepted ? ": the compare may accept it" : ": the compare rejects it") + ", and the capture does not fix when it runs relative to the wait"; + case WriterMatch::Effect::Undecided: return text + ": conditional, or its value is known only at run time"; + case WriterMatch::Effect::Overwritten: return text + ": the compare accepts it, but another store to the awaited bytes can replace it before the wait"; + case WriterMatch::Effect::Evicted: return text + ": the compare accepts it, but newer stores to the awaited bytes can push it out of the driver's label history"; + case WriterMatch::Effect::Rejected: return text + ": the compare rejects it"; + case WriterMatch::Effect::After: return text + (match.accepted ? ": the compare may accept it" : "") + ", but it runs after the wait"; + } + throw std::runtime_error("PM4 analysis: unknown writer effect"); +} + +std::string waitLine(const WaitReport& report) { + std::string text = format("#%llu queue 0x%x serial %llu +0x%04zx %s", static_cast(report.at.ordinal), report.at.queue, static_cast(report.at.serial), report.at.offset, report.at.name.c_str()); + if (report.wait.kind == MemoryWait::Kind::Memory) { + const auto& fields = report.wait.fields; + text += format(" [%s] (%zu bytes) %s 0x%llx mask 0x%llx", hex(fields.address).c_str(), fields.bytes, CompareNames[fields.function], static_cast(fields.reference), static_cast(fields.mask)); + } + if (report.wait.conditional) text += " (conditional)"; + return text; +} + +std::string submissionLine(const CapturedSubmission& submission) { + const char* kind = submission.kind == SubmissionKind::Rewind ? " (rewind tail)" : submission.kind == SubmissionKind::Suspend ? " (suspend point)" : ""; + return format("#%llu queue 0x%x %s serial %llu at %llu.%03llu ms: %zu dwords%s", static_cast(submission.ordinal), submission.queue, SubmissionCapture::QueueType(submission.queue).data(), static_cast(submission.serial), + static_cast(submission.timeUs / 1000), static_cast(submission.timeUs % 1000), submission.dwords, kind); +} + +void printPackets(std::FILE* output, const std::vector& packets, std::size_t from, std::size_t to, std::size_t marked) { + for (std::size_t i = from; i < to && i < packets.size(); ++i) { + const auto& packet = packets[i]; + std::fprintf(output, " %s+0x%04zx %-26s %s%s\n", packet.offset == marked ? "> " : " ", packet.offset, packet.name.c_str(), packet.operands.c_str(), packet.conditional && (!packet.stores.empty() || packet.wait.has_value()) ? " (conditional)" : ""); + } +} + +std::vector decodePackets(const CapturedSubmission& submission, bool text) { + const std::span commands = submission.commands; + require(commands.size() == submission.dwords, "the commands do not hold the dword count of the index"); + std::vector packets; + std::vector boundaries; + std::vector guarded; + auto range = submission.conditionalRanges.begin(); + auto held = submission.heldAtSubmit.begin(); + for (std::size_t cursor = 0; cursor < commands.size();) { + std::erase_if(guarded, [cursor](std::size_t end) { return end <= cursor; }); + boundaries.push_back(cursor); + const auto header = commands[cursor]; + DecodedPacket packet; + packet.offset = cursor; + packet.header = header; + if (text) packet.name = Pm4::Name(header); + if (Pm4::FillerPacket(header)) { + std::size_t run = 1; + while (cursor + run < commands.size() && Pm4::FillerPacket(commands[cursor + run])) ++run; + for (std::size_t inner = 1; inner < run; ++inner) boundaries.push_back(cursor + inner); + packet.words = run; + if (text) packet.operands = format("x%zu", run); + packets.push_back(std::move(packet)); + cursor += run; + continue; + } + if ((header >> 30u) != 3u) require(false, format("header 0x%08x at DWORD %zu is not a type-3 packet", header, cursor)); + const auto words = Pm4::PacketWords(header); + if (words > commands.size() - cursor) require(false, format("%s at DWORD %zu needs %zu dwords, %zu remain", Pm4::Name(header).c_str(), cursor, words, commands.size() - cursor)); + const auto span = commands.subspan(cursor, words); + try { + Pm4::Validate(span, submission.queue); + } catch (const std::exception& error) { + throw std::runtime_error(format("PM4 analysis: %s at DWORD %zu is a packet the driver rejects, so the capture is corrupt: ", Pm4::Name(header).c_str(), cursor) + error.what()); + } + packet.words = words; + packet.conditional = !guarded.empty() || Pm4::Predicated(header); + if (conditionalStart(header) && !submission.conditionalKnown) { + guarded.push_back(commands.size()); + } else if (conditionalStart(header)) { + if (range == submission.conditionalRanges.end() || range->first != cursor) require(false, format("%s at DWORD %zu has no conditional range in the index", Pm4::Name(header).c_str(), cursor)); + if (range->second < cursor + words) require(false, format("the conditional range of %s at DWORD %zu ends inside it", Pm4::Name(header).c_str(), cursor)); + guarded.push_back(range->second); + ++range; + } + if (range != submission.conditionalRanges.end() && range->first <= cursor) require(false, format("a conditional range of the index starts at DWORD %zu, which is no COND_EXEC or predicated INDIRECT_BUFFER", range->first)); + const auto opcode = (header >> 8u) & 0xffu; + if (text) packet.operands = describe(span); + for (auto& store : Pm4::DecodeStores(span)) { + const bool conditional = packet.conditional || store.conditional; + packet.stores.push_back({std::move(store), conditional, opcode == 0x49 || opcode == 0x37}); + } + const bool heldHere = held != submission.heldAtSubmit.end() && *held == cursor; + if (opcode == 0x3c || opcode == 0x93) { + MemoryWait wait; + wait.fields = Pm4::DecodeWait(span); + wait.heldAtSubmit = heldHere; + wait.conditional = packet.conditional; + wait.packet.assign(span.begin(), span.end()); + packet.wait = std::move(wait); + } else if (opcode == 0x59) { + MemoryWait wait; + wait.kind = MemoryWait::Kind::Rewind; + wait.released = (span[1] & Pm4::RewindValid) != 0; + wait.packet.assign(span.begin(), span.end()); + packet.wait = std::move(wait); + } + if (heldHere && (!packet.wait.has_value() || packet.wait->kind != MemoryWait::Kind::Memory)) require(false, format("the index marks DWORD %zu as a wait held at submit, but %s is no WAIT_REG_MEM", cursor, Pm4::Name(header).c_str())); + if (heldHere) ++held; + if (held != submission.heldAtSubmit.end() && *held <= cursor) require(false, format("the index marks DWORD %zu as a wait held at submit, which is inside a packet", *held)); + packets.push_back(std::move(packet)); + cursor += words; + } + require(range == submission.conditionalRanges.end(), "a conditional range of the index starts past the commands"); + require(held == submission.heldAtSubmit.end(), "a wait held at submit lies past the commands"); + for (const auto& [start, end] : submission.conditionalRanges) { + if (end != commands.size() && !std::binary_search(boundaries.begin(), boundaries.end(), end)) require(false, format("the conditional range at DWORD %zu ends inside a packet", start)); + } + return packets; +} + +std::vector decodeSubmission(const std::filesystem::path& directory, CapturedSubmission submission, bool text) { + try { + submission.commands = SubmissionCapture::ReadCommands(directory, submission); + return decodePackets(submission, text); + } catch (const std::exception& error) { + throw std::runtime_error(format("submission #%llu: ", static_cast(submission.ordinal)) + error.what()); + } +} + +} + +std::vector Decode(const CapturedSubmission& submission) { + return decodePackets(submission, true); +} + +std::size_t Analysis::Count(WaitReport::Verdict verdict) const { + return static_cast(std::count_if(waits.begin(), waits.end(), [verdict](const WaitReport& report) { return report.verdict == verdict; })); +} + +std::size_t Analysis::CrossQueue() const { + return static_cast(std::count_if(waits.begin(), waits.end(), [](const WaitReport& report) { return report.crossQueue; })); +} + +Analysis Analyze(const std::filesystem::path& directory) { + Analysis analysis; + analysis.submissions = SubmissionCapture::ReadIndex(directory); + const auto& submissions = analysis.submissions; + std::map, std::vector> segments; + for (const auto& submission : submissions) { + if (submission.kind != SubmissionKind::Suspend) segments[{submission.queue, submission.serial}].push_back(submission.ordinal); + } + std::vector stores; + std::vector waiters; + for (std::size_t index = 0; index < submissions.size(); ++index) { + if (submissions[index].dwords == 0) continue; + auto packets = decodeSubmission(directory, submissions[index], false); + analysis.packets += packets.size(); + for (auto& packet : packets) { + for (auto& store : packet.stores) stores.push_back({{index, packet.offset, packet.header}, std::move(store)}); + if (packet.wait.has_value()) waiters.push_back({{index, packet.offset, packet.header}, std::move(*packet.wait)}); + } + } + analysis.stores = stores.size(); + Matcher matcher(submissions, stores); + for (const auto& waiter : waiters) { + WaitReport report; + const auto& submission = submissions[waiter.at.submission]; + report.at = {submission.ordinal, submission.queue, submission.serial, waiter.at.offset, Pm4::Name(waiter.at.header)}; + report.wait = waiter.wait; + if (report.wait.kind == MemoryWait::Kind::Rewind) { + matchRewind(report, segments); + } else if (report.wait.fields.function == 0) { + report.verdict = WaitReport::Verdict::Released; + report.route = WaitReport::Route::Always; + report.reason = "released: the compare function always passes"; + } else if (report.wait.heldAtSubmit) { + report.verdict = WaitReport::Verdict::Released; + report.route = WaitReport::Route::HeldAtSubmit; + report.reason = "released: its value passed the compare when the submission entered the driver (held at submit)"; + } else { + matcher.Match(report, waiter); + } + analysis.waits.push_back(std::move(report)); + } + return analysis; +} + +int Report(const std::filesystem::path& directory, bool full, std::FILE* output) { + const auto analysis = Analyze(directory); + const auto& submissions = analysis.submissions; + std::map perQueue; + std::size_t tails = 0, suspends = 0, dwords = 0; + for (const auto& submission : submissions) { + dwords += submission.dwords; + if (submission.kind == SubmissionKind::Suspend) ++suspends; + else ++perQueue[submission.queue]; + if (submission.kind == SubmissionKind::Rewind) ++tails; + } + std::fprintf(output, "capture %s: submissions %zu (rewind tails %zu, suspend points %zu), dwords %zu, packets %zu, memory stores %zu\n", directory.string().c_str(), submissions.size(), tails, suspends, dwords, analysis.packets, analysis.stores); + for (const auto& [queue, count] : perQueue) std::fprintf(output, " queue 0x%x %s: submissions %zu\n", queue, SubmissionCapture::QueueType(queue).data(), count); + std::map routes; + for (const auto& report : analysis.waits) { + if (report.verdict == WaitReport::Verdict::Released) ++routes[report.route]; + } + const auto unreleased = analysis.Count(WaitReport::Verdict::Unreleased); + const auto unknown = analysis.Count(WaitReport::Verdict::Unknown); + std::fprintf(output, "waits %zu: released %zu (compare always %zu, held at submit %zu, current value %zu, label history %zu, rewind tail %zu), unknown %zu (cross-queue timing %zu), unreleased %zu\n", analysis.waits.size(), analysis.Count(WaitReport::Verdict::Released), + routes[WaitReport::Route::Always], routes[WaitReport::Route::HeldAtSubmit], routes[WaitReport::Route::CurrentValue], routes[WaitReport::Route::LabelHistory], routes[WaitReport::Route::RewindTail], unknown, analysis.CrossQueue(), unreleased); + std::fprintf(output, "(released only where the capture proves it; the tool does not model cross-queue timing, so a wait that stores of another queue decide is unknown)\n"); + std::fprintf(output, "(stores are the PM4 packets of the capture; CPU and shader stores are not captured)\n"); + if (std::any_of(submissions.begin(), submissions.end(), [](const CapturedSubmission& submission) { return !submission.heldKnown || !submission.conditionalKnown; })) { + std::fprintf(output, "(the index has no held and conditional columns: a wait no store of the capture can release is unknown instead of unreleased, and every packet after a COND_EXEC or predicated INDIRECT_BUFFER is conditional to the end of its submission)\n"); + } + + std::map byOrdinal; + for (std::size_t index = 0; index < submissions.size(); ++index) byOrdinal.emplace(submissions[index].ordinal, index); + std::vector shown; + if (full) { + for (const auto& report : analysis.waits) shown.push_back(&report); + } else { + for (const auto verdict : {WaitReport::Verdict::Unreleased, WaitReport::Verdict::Unknown}) { + for (auto it = analysis.waits.rbegin(); it != analysis.waits.rend() && shown.size() < ListedWaits; ++it) { + if (it->verdict == verdict) shown.push_back(&*it); + } + } + if (!shown.empty()) std::fprintf(output, "\nunreleased waits, then unknown waits, newest first:\n"); + } + for (const auto* report : shown) { + std::fprintf(output, "\n%s %s\n %s\n", verdictName(report->verdict), waitLine(*report).c_str(), report->reason.c_str()); + for (const auto& writer : report->writers) std::fprintf(output, " writer %s\n", describeWriter(writer).c_str()); + if (report->writerCount > report->writers.size()) std::fprintf(output, " (%zu stores to the awaited bytes in the capture)\n", report->writerCount); + if (full) continue; + const auto& submission = submissions[byOrdinal.at(report->at.ordinal)]; + const auto packets = decodeSubmission(directory, submission, true); + const auto at = static_cast(std::find_if(packets.begin(), packets.end(), [&](const DecodedPacket& packet) { return packet.offset == report->at.offset; }) - packets.begin()); + std::fprintf(output, " in %s\n", submissionLine(submission).c_str()); + printPackets(output, packets, at > ContextBefore ? at - ContextBefore : 0, at + ContextAfter + 1, report->at.offset); + } + if (!full && unreleased + unknown > shown.size()) std::fprintf(output, "\n(+%zu more unreleased and unknown waits; --full lists every wait)\n", unreleased + unknown - shown.size()); + + std::vector order; + for (std::size_t index = 0; index < submissions.size(); ++index) { + if (submissions[index].kind != SubmissionKind::Suspend) order.push_back(index); + } + std::sort(order.begin(), order.end(), [&](std::size_t left, std::size_t right) { + return std::tie(submissions[left].queue, submissions[left].serial, submissions[left].ordinal) < std::tie(submissions[right].queue, submissions[right].serial, submissions[right].ordinal); + }); + std::fprintf(output, full ? "\nsubmissions in execution order per queue:\n" : "\nlast submissions of each queue (--full prints every submission and packet):\n"); + for (std::size_t begin = 0; begin < order.size();) { + auto end = begin; + while (end < order.size() && submissions[order[end]].queue == submissions[order[begin]].queue) ++end; + for (auto index = full || end - begin <= LastSubmissionsPerQueue ? begin : end - LastSubmissionsPerQueue; index < end; ++index) { + const auto& submission = submissions[order[index]]; + std::fprintf(output, "%s\n", submissionLine(submission).c_str()); + const auto packets = submission.dwords == 0 ? std::vector{} : decodeSubmission(directory, submission, true); + const auto limit = full ? packets.size() : std::min(packets.size(), ShownPackets); + printPackets(output, packets, 0, limit, std::numeric_limits::max()); + if (limit < packets.size()) std::fprintf(output, " (+%zu packets)\n", packets.size() - limit); + } + begin = end; + } + return unreleased == 0 ? 0 : 1; +} + +} diff --git a/core/libs/prx/libSceAgcDriver/tools/Pm4Analysis.hpp b/core/libs/prx/libSceAgcDriver/tools/Pm4Analysis.hpp new file mode 100644 index 0000000000..cff6c6da5c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tools/Pm4Analysis.hpp @@ -0,0 +1,91 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TOOLS_PM4ANALYSIS_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TOOLS_PM4ANALYSIS_HPP + +#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" +#include "prx/libSceAgcDriver/Execution/include/SubmissionCapture.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Pm4Analysis { + +struct MemoryStore { + Pm4::PacketStore store; + bool conditional = false; + bool label = false; +}; + +struct MemoryWait { + enum class Kind : std::uint8_t { Memory, Rewind }; + Kind kind = Kind::Memory; + Pm4::WaitFields fields{}; + bool heldAtSubmit = false; + bool conditional = false; + bool released = false; + std::vector packet; +}; + +struct DecodedPacket { + std::size_t offset = 0; + std::size_t words = 0; + std::uint32_t header = 0; + std::string name; + std::string operands; + bool conditional = false; + std::vector stores; + std::optional wait; +}; + +std::vector Decode(const CapturedSubmission& submission); + +struct PacketLocation { + std::uint64_t ordinal = 0; + std::uint32_t queue = 0; + std::uint64_t serial = 0; + std::size_t offset = 0; + std::string name; +}; + +struct WriterMatch { + enum class Order : std::uint8_t { EarlierOnQueue, LaterOnQueue, OtherQueue }; + enum class Effect : std::uint8_t { CurrentValue, LabelHistory, Evicted, Overwritten, Undecided, OtherQueue, Rejected, After }; + PacketLocation at; + Order order = Order::OtherQueue; + Effect effect = Effect::OtherQueue; + std::optional stored; + bool accepted = false; +}; + +struct WaitReport { + enum class Verdict : std::uint8_t { Released, Unknown, Unreleased }; + enum class Route : std::uint8_t { None, Always, HeldAtSubmit, CurrentValue, LabelHistory, RewindTail }; + PacketLocation at; + MemoryWait wait; + Verdict verdict = Verdict::Unreleased; + Route route = Route::None; + bool crossQueue = false; + std::string reason; + std::size_t writerCount = 0; + std::vector writers; +}; + +struct Analysis { + static constexpr std::size_t ListedWriters = 8; + std::vector submissions; + std::size_t packets = 0; + std::size_t stores = 0; + std::vector waits; + std::size_t Count(WaitReport::Verdict verdict) const; + std::size_t CrossQueue() const; +}; + +Analysis Analyze(const std::filesystem::path& directory); +int Report(const std::filesystem::path& directory, bool full, std::FILE* output); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tools/Pm4Replay.cpp b/core/libs/prx/libSceAgcDriver/tools/Pm4Replay.cpp new file mode 100644 index 0000000000..742fab2a5c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tools/Pm4Replay.cpp @@ -0,0 +1,48 @@ +#include "Pm4Analysis.hpp" +#include +#include +#include +#include +#include + +namespace { + +int usage() { + std::fprintf(stderr, "usage: agc_pm4_replay [--full]\n" + " decodes the submissions APS5_DUMP_SUBMITS= captured and reports each wait as released when\n" + " the capture alone proves it, unreleased when no store in the capture can release it and unknown\n" + " otherwise (cross-queue timing is not modelled); exit code 1 when a wait is unreleased, 2 on errors\n"); + return 2; +} + +template +int run(int argc, TChar** argv) { + constexpr std::string_view Full = "--full"; + std::filesystem::path directory; + bool full = false; + for (int index = 1; index < argc; ++index) { + const std::basic_string_view argument = argv[index]; + if (std::ranges::equal(argument, Full)) full = true; + else if (!argument.starts_with(TChar('-')) && directory.empty()) directory = argument; + else return usage(); + } + if (directory.empty()) return usage(); + try { + return AgcDriver::Pm4Analysis::Report(directory, full, stdout); + } catch (const std::exception& error) { + std::fprintf(stderr, "agc_pm4_replay: %s\n", error.what()); + return 2; + } +} + +} + +#ifdef _WIN32 +int wmain(int argc, wchar_t** argv) { + return run(argc, argv); +} +#else +int main(int argc, char** argv) { + return run(argc, argv); +} +#endif diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index 524575eefd..e4eeec9174 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -5,6 +5,7 @@ #include "ControlFlow/Structurizer.hpp" #include "Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" +#include "Optimization/RequestMemoryView.hpp" #include "Recompiler.hpp" #include "RdnaDecoder/RdnaInstructionDecoder.hpp" @@ -16,6 +17,7 @@ #include #include #include +#include #include namespace { @@ -42,8 +44,11 @@ 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_waveLdsMemoryBarriers = false; bool g_code = false; bool Replay(const char* path) { @@ -106,26 +111,44 @@ 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; + if (g_waveLdsMemoryBarriers) request.request.target.waveLdsMemoryBarriers = true; const auto result = ShaderRecompiler::Recompile(request.request); std::printf(" recompiled: %zu SPIR-V words\n", result.spirv.size()); if (g_spirv) { @@ -147,7 +170,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 +186,7 @@ bool Replay(const char* path) { int main(int argc, char** argv) { if (argc < 2) { - std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--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] [--wave-lds-memory-barriers] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set; shader_
.prepare.req holds a request whose preparation failed before any resource capture, so replay it with --prepare\n"); return 2; } int failures = 0; @@ -184,6 +207,14 @@ 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]) == "--wave-lds-memory-barriers") { + g_waveLdsMemoryBarriers = true; + continue; + } if (std::string(argv[i]) == "--mem") { g_memory = true; continue; @@ -196,6 +227,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/libSceAgcResourceRegistration/Export.cpp b/core/libs/prx/libSceAgcResourceRegistration/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAgcResourceRegistration/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAgcVshDebug/Export.cpp b/core/libs/prx/libSceAgcVshDebug/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAgcVshDebug/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAjm.native/AjmLibrary.cmake b/core/libs/prx/libSceAjm.native/AjmLibrary.cmake new file mode 100644 index 0000000000..ec9f5b9a81 --- /dev/null +++ b/core/libs/prx/libSceAjm.native/AjmLibrary.cmake @@ -0,0 +1,15 @@ +function(add_sce_ajm_library target) + set(ajmDir ${CMAKE_CURRENT_FUNCTION_LIST_DIR}) + add_library(${target} SHARED EXCLUDE_FROM_ALL + ${ajmDir}/Export.cpp + ${ajmDir}/src/Ajm.cpp + ${ARGN} + ) + target_include_directories(${target} PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src + ${CMAKE_SOURCE_DIR}/3rdparty/vgmstream/src/coding/libs + ) + target_link_libraries(${target} PRIVATE atrac9 atrac9_vibration ffmpeg libc libkernel) + configure_windows_unwind(${target}) +endfunction() diff --git a/core/libs/prx/libSceAjm.native/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt index 0119f36b99..20f6cf1f20 100644 --- a/core/libs/prx/libSceAjm.native/CMakeLists.txt +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -2,13 +2,13 @@ enable_language(C) set(LIBATRAC9_DIR ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) set(LIBATRAC9_PATCHED_DIR ${CMAKE_CURRENT_BINARY_DIR}/atrac9) -set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${LIBATRAC9_DIR}/unpack.c) +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${LIBATRAC9_DIR}/unpack.c ${LIBATRAC9_DIR}/bit_reader.c ${LIBATRAC9_DIR}/decinit.c) include(${CMAKE_CURRENT_SOURCE_DIR}/PatchLibAtrac9.cmake) add_library(atrac9 STATIC EXCLUDE_FROM_ALL ${LIBATRAC9_DIR}/band_extension.c ${LIBATRAC9_DIR}/bit_allocation.c - ${LIBATRAC9_DIR}/bit_reader.c - ${LIBATRAC9_DIR}/decinit.c + ${LIBATRAC9_PATCHED_DIR}/bit_reader.c + ${LIBATRAC9_PATCHED_DIR}/decinit.c ${LIBATRAC9_DIR}/decoder.c ${LIBATRAC9_DIR}/huffCodes.c ${LIBATRAC9_DIR}/imdct.c @@ -22,21 +22,20 @@ add_library(atrac9 STATIC EXCLUDE_FROM_ALL target_include_directories(atrac9 PRIVATE ${LIBATRAC9_DIR}) set_target_properties(atrac9 PROPERTIES POSITION_INDEPENDENT_CODE ON) -add_library(libSceAjm.native SHARED EXCLUDE_FROM_ALL - ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/Ajm.cpp +set(VGMSTREAM_ATRAC9_DIR ${CMAKE_SOURCE_DIR}/3rdparty/vgmstream/src/coding/libs) +add_library(atrac9_vibration STATIC EXCLUDE_FROM_ALL + ${VGMSTREAM_ATRAC9_DIR}/atrac9_dec.c + ${VGMSTREAM_ATRAC9_DIR}/atrac9_internal.c ) +set_target_properties(atrac9_vibration PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) -target_include_directories(libSceAjm.native PRIVATE - ${LIBS_INCLUDE_DIR} - ${LIBATRAC9_DIR} -) -target_link_libraries(libSceAjm.native PRIVATE atrac9 ffmpeg libc libkernel) -configure_windows_unwind(libSceAjm.native) +include(${CMAKE_CURRENT_SOURCE_DIR}/AjmLibrary.cmake) +add_sce_ajm_library(libSceAjm.native) if(BUILD_TESTING) add_test_executable(atrac9_gradient_tests tests/Atrac9Gradient.c ${LIBATRAC9_DIR}/bit_reader.c) target_include_directories(atrac9_gradient_tests PRIVATE ${LIBATRAC9_PATCHED_DIR} ${LIBATRAC9_DIR}) + target_link_libraries(atrac9_gradient_tests PRIVATE atrac9) target_compile_options(atrac9_gradient_tests PRIVATE -ffunction-sections -fdata-sections) if(APPLE) target_link_options(atrac9_gradient_tests PRIVATE LINKER:-dead_strip) @@ -45,11 +44,29 @@ if(BUILD_TESTING) endif() add_test(NAME atrac9_gradient COMMAND atrac9_gradient_tests) + add_test_executable(atrac9_config_data_tests tests/Atrac9ConfigData.c) + target_include_directories(atrac9_config_data_tests PRIVATE ${LIBATRAC9_DIR}) + target_link_libraries(atrac9_config_data_tests PRIVATE atrac9) + if(NOT WIN32) + target_link_libraries(atrac9_config_data_tests PRIVATE m) + endif() + add_test(NAME atrac9_config_data COMMAND atrac9_config_data_tests) + add_test_executable(guest_ajm_tests ${LIBS_INCLUDE_DIR}/tests/GuestAjm.cpp) target_include_directories(guest_ajm_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ajm_tests PRIVATE libSceAjm.native libc) configure_windows_unwind(guest_ajm_tests) add_test(NAME guest_ajm COMMAND guest_ajm_tests) + add_test_executable(guest_ajm_vibration_tests ${LIBS_INCLUDE_DIR}/tests/GuestAjmVibration.cpp) + target_include_directories(guest_ajm_vibration_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ajm_vibration_tests PRIVATE libSceAjm.native libc) + configure_windows_unwind(guest_ajm_vibration_tests) + add_test(NAME guest_ajm_vibration COMMAND guest_ajm_vibration_tests) + add_test_executable(guest_ajm_wvorbis_tests ${LIBS_INCLUDE_DIR}/tests/GuestAjmWVorbis.cpp) + target_include_directories(guest_ajm_wvorbis_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ajm_wvorbis_tests PRIVATE libSceAjm.native libc) + configure_windows_unwind(guest_ajm_wvorbis_tests) + add_test(NAME guest_ajm_wvorbis COMMAND guest_ajm_wvorbis_tests) if(UNIX) add_test_executable(guest_ajm_teardown_tests ${LIBS_INCLUDE_DIR}/tests/GuestAjmTeardown.cpp) target_include_directories(guest_ajm_teardown_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -58,7 +75,7 @@ if(BUILD_TESTING) add_test(NAME guest_ajm_teardown COMMAND guest_ajm_teardown_tests $) endif() if(WIN32) - set_tests_properties(guest_ajm PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_ajm guest_ajm_vibration guest_ajm_wvorbis PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index e6c8fcbd75..3b0b5e4a8a 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -4,6 +4,7 @@ #include "prx/libc/include/General.hpp" #include #include +#include // Entry points this library does not implement: reported before the unimplemented-call exception so an // APS5_TRACE_AJM run shows which one a title reached. @@ -12,6 +13,69 @@ static void AjmStub(const char* name) { NotImplemented_nid_no_patch(name); } +struct WVorbisHeaderPacketParameter { + std::size_t size; + std::int32_t channels; + std::uint32_t sampleRate; + std::int32_t bytesPerSecond; + std::int32_t shortBlockExponent; + std::int32_t longBlockExponent; + std::uint32_t reserved; + const void* setupPacket; + std::size_t setupPacketSize; + void* output; + std::size_t outputCapacity; + std::size_t outputSize; +}; + +struct WVorbisSetupPacketParameter { + std::size_t size; + std::int32_t channels; + std::uint32_t reserved; + const void* input; + std::size_t inputSize; + void* output; + std::size_t outputCapacity; + std::size_t outputSize; +}; + +static_assert(sizeof(WVorbisHeaderPacketParameter) == 72); +static_assert(offsetof(WVorbisHeaderPacketParameter, setupPacket) == 32); +static_assert(sizeof(WVorbisSetupPacketParameter) == 56); +static_assert(offsetof(WVorbisSetupPacketParameter, input) == 16); + +namespace { + +constexpr int AJM_WVORBIS_ERROR_INVALID_PARAMETER = static_cast(0x8A6E0001); +constexpr int AJM_WVORBIS_ERROR_INVALID_SIZE = static_cast(0x8A6E0002); +constexpr int AJM_WVORBIS_ERROR_OUTPUT_TOO_SMALL = static_cast(0x8A6E0003); +constexpr int AJM_WVORBIS_ERROR_INVALID_SETUP = static_cast(0x8A6E0004); +constexpr std::size_t WVORBIS_LENGTH_TABLE_SIZE = 10; +constexpr std::size_t VORBIS_IDENTIFICATION_SIZE = 30; +constexpr std::size_t VORBIS_COMMENT_SIZE = 16; +constexpr std::size_t WVORBIS_SETUP_MIN_SIZE = 9; +constexpr std::size_t WVORBIS_SETUP_PART1_LENGTH_OFFSET = 7; +constexpr std::int32_t VORBIS_BLOCK_SIZE_MIN = 64; +constexpr std::int32_t VORBIS_BLOCK_SIZE_MAX = 8192; +constexpr std::uint8_t VORBIS_MAGIC[] {'v', 'o', 'r', 'b', 'i', 's'}; + +std::int32_t WVorbisBlockSize(std::int32_t log2) { + return static_cast(1u << (static_cast(log2) & 31u)); +} + +std::uint8_t* PutLittle(std::uint8_t* out, std::uint32_t value, std::size_t bytes) { + for (std::size_t i = 0; i < bytes; ++i) out[i] = static_cast(value >> (8 * i)); + return out + bytes; +} + +std::uint8_t* PutVorbisHeaderStart(std::uint8_t* out, std::uint8_t type) { + *out++ = type; + std::memcpy(out, VORBIS_MAGIC, sizeof(VORBIS_MAGIC)); + return out + sizeof(VORBIS_MAGIC); +} + +} + extern "C" { int APS5_VABI sceAjmBatchJobEncode(AjmBatchInfo* info, uint32_t instance, const void* pcm_input, size_t pcm_input_size, void* bitstream_output, size_t bitstream_output_size, void* result) { @@ -32,12 +96,73 @@ const char* APS5_VABI sceAjmStrError(int error) { return nullptr; } -int APS5_VABI sceAjmDecWVorbisCreateHeaderPacket(void) { +int APS5_VABI sceAjmDecWVorbisCreateHeaderPacket(WVorbisHeaderPacketParameter* param) { + if (param == nullptr) return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + if (param->size != sizeof(WVorbisHeaderPacketParameter)) return AJM_WVORBIS_ERROR_INVALID_SIZE; + if (param->bytesPerSecond < 0) return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + if (WVorbisBlockSize(param->shortBlockExponent) < VORBIS_BLOCK_SIZE_MIN) return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + if (param->longBlockExponent < param->shortBlockExponent || WVorbisBlockSize(param->longBlockExponent) > VORBIS_BLOCK_SIZE_MAX) + return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + if (param->setupPacket == nullptr) return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + if (param->setupPacketSize < WVORBIS_SETUP_MIN_SIZE) return AJM_WVORBIS_ERROR_INVALID_SETUP; + if (param->output == nullptr) return AJM_WVORBIS_ERROR_INVALID_PARAMETER; + constexpr std::size_t headers = WVORBIS_LENGTH_TABLE_SIZE + VORBIS_IDENTIFICATION_SIZE + VORBIS_COMMENT_SIZE; + if (param->outputCapacity < headers || param->setupPacketSize > param->outputCapacity - headers) return AJM_WVORBIS_ERROR_OUTPUT_TOO_SMALL; + + const auto* setup = static_cast(param->setupPacket); + const auto part1 = static_cast(setup[WVORBIS_SETUP_PART1_LENGTH_OFFSET] | setup[WVORBIS_SETUP_PART1_LENGTH_OFFSET + 1] << 8); + auto* out = static_cast(param->output); + out = PutLittle(out, 0, 2); + out = PutLittle(out, VORBIS_IDENTIFICATION_SIZE, 2); + out = PutLittle(out, VORBIS_COMMENT_SIZE, 2); + out = PutLittle(out, part1, 2); + out = PutLittle(out, static_cast(param->setupPacketSize) - part1, 2); + + out = PutVorbisHeaderStart(out, 1); + out = PutLittle(out, 0, 4); + out = PutLittle(out, static_cast(param->channels), 1); + out = PutLittle(out, param->sampleRate, 4); + out = PutLittle(out, 0, 4); + out = PutLittle(out, static_cast(param->bytesPerSecond) * 8u, 4); + out = PutLittle(out, 0, 4); + *out++ = static_cast((param->shortBlockExponent & 0xF) | (param->longBlockExponent & 0xF) << 4); + *out++ = 1; + + out = PutVorbisHeaderStart(out, 3); + out = PutLittle(out, 0, 4); + out = PutLittle(out, 0, 4); + *out++ = 1; + + std::memcpy(out, setup, param->setupPacketSize); + param->outputSize = headers + param->setupPacketSize; + return 0; +} + +int APS5_VABI sceAjmDecWVorbisCreateSetupPacket(WVorbisSetupPacketParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAjmDecWVorbisCreateSetupPacket(void) { +int APS5_VABI sceAjmBatchGetLog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmInstanceAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +uint32_t APS5_VABI sceAjmInstanceCodecType(uint32_t instance_id) { + return instance_id >> 14; +} + +int APS5_VABI sceAjmInstanceExtend(uint32_t context, uint32_t codec, uint64_t flags, uint32_t instance) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmInstanceSwitch(uint32_t context, uint32_t codec, uint64_t flags, uint32_t instance) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceAjm.native/PatchLibAtrac9.cmake b/core/libs/prx/libSceAjm.native/PatchLibAtrac9.cmake index e795b3fba8..6c1b91c006 100644 --- a/core/libs/prx/libSceAjm.native/PatchLibAtrac9.cmake +++ b/core/libs/prx/libSceAjm.native/PatchLibAtrac9.cmake @@ -8,3 +8,23 @@ endif() string(REPLACE "${libatrac9OldGuard}" "${libatrac9NewGuard}" libatrac9Unpack "${libatrac9Unpack}") file(MAKE_DIRECTORY ${LIBATRAC9_PATCHED_DIR}) file(WRITE ${LIBATRAC9_PATCHED_DIR}/unpack.c "${libatrac9Unpack}") + +file(READ ${LIBATRAC9_DIR}/bit_reader.c libatrac9BitReader) +set(libatrac9OldPeek "\tconst int byteIndex = br->Position / 8;\n\tconst int bitIndex = br->Position % 8;\n\tconst int numBytes = (bitIndex + bits - 1) / 8 + 1;\n\tconst unsigned char* buffer = br->Buffer;\n\n\tint value;\n\tswitch (numBytes) {") +string(FIND "${libatrac9BitReader}" "${libatrac9OldPeek}" libatrac9PeekPosition) +if(libatrac9PeekPosition EQUAL -1) + message(FATAL_ERROR "LibAtrac9 PeekInt has changed; review the local fast path") +endif() +set(libatrac9NewPeek "\tconst int byteIndex = br->Position >> 3;\n\tconst int bitIndex = br->Position & 7;\n\tconst unsigned char* buffer = br->Buffer;\n\tif (bits > 0 && bits <= 24 && byteIndex + 4 <= (br->LengthBits >> 3)) {\n\t\tconst unsigned raw = ((unsigned)buffer[byteIndex] << 24) | ((unsigned)buffer[byteIndex + 1] << 16) | ((unsigned)buffer[byteIndex + 2] << 8) | (unsigned)buffer[byteIndex + 3];\n\t\treturn (int)((raw << bitIndex) >> (32 - bits));\n\t}\n\tconst int numBytes = (bitIndex + bits - 1) / 8 + 1;\n\tint value;\n\tswitch (numBytes) {") +string(REPLACE "${libatrac9OldPeek}" "${libatrac9NewPeek}" libatrac9BitReader "${libatrac9BitReader}") +file(WRITE ${LIBATRAC9_PATCHED_DIR}/bit_reader.c "${libatrac9BitReader}") + +file(READ ${LIBATRAC9_DIR}/decinit.c libatrac9Decinit) +set(libatrac9OldConfigGuard "header != 0xFE || validationBit != 0") +set(libatrac9NewConfigGuard "header != 0xFE || validationBit != 0 || config->ChannelConfigIndex > 5") +string(FIND "${libatrac9Decinit}" "${libatrac9OldConfigGuard}" libatrac9ConfigGuardPosition) +if(libatrac9ConfigGuardPosition EQUAL -1) + message(FATAL_ERROR "LibAtrac9 config data guard has changed; review the local bounds fix") +endif() +string(REPLACE "${libatrac9OldConfigGuard}" "${libatrac9NewConfigGuard}" libatrac9Decinit "${libatrac9Decinit}") +file(WRITE ${LIBATRAC9_PATCHED_DIR}/decinit.c "${libatrac9Decinit}") diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 15cc83db67..1a27be97b1 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -4,11 +4,14 @@ #include "prx/libc/include/General.hpp" #include "libatrac9.h" extern "C" { +#include "atrac9_dec.h" #include +#include #include #include } #include +#include #include #include #include @@ -17,7 +20,6 @@ extern "C" { #include #include #include -#include #include #include #include @@ -40,11 +42,21 @@ constexpr std::int32_t AJM_RESULT_INVALID_PARAMETER = 0x00000004; constexpr std::int32_t AJM_RESULT_PARTIAL_INPUT = 0x00000008; constexpr std::int32_t AJM_RESULT_NOT_ENOUGH_ROOM = 0x00000010; constexpr std::int32_t AJM_RESULT_CODEC_ERROR = 0x40000000; +constexpr std::int32_t AJM_RESULT_FATAL = static_cast(0x80000000u); constexpr std::uint32_t CODEC_MP3 = 0; constexpr std::uint32_t CODEC_AT9 = 1; +constexpr std::uint32_t CODEC_M4AAC = 2; +constexpr std::uint32_t CODEC_PCM = 23; constexpr std::uint32_t CODEC_OPUS = 24; +constexpr std::uint32_t M4AAC_CONFIG_ADTS = 1; +constexpr std::uint32_t M4AAC_CONFIG_RAW = 2; +constexpr std::uint64_t M4AAC_ENABLE_SBR_DECODE = 1ull << 32; +constexpr std::uint64_t M4AAC_ENABLE_NONDELAY_OUTPUT = 1ull << 33; +constexpr std::uint64_t M4AAC_SURROUND_INTERLEAVE_ORDERS = 3ull << 34; +constexpr std::uint32_t M4AAC_DELAY_FRAMES = 2; + // APS5_TRACE_AJM=1 logs every job the title submits and what the library writes back. bool TraceEnabled() { static const bool enabled = std::getenv("APS5_TRACE_AJM") != nullptr; @@ -102,6 +114,11 @@ struct SidebandAt9CodecInfo { std::uint32_t frameSamples; }; +struct SidebandM4aacCodecInfo { + std::uint32_t heaac; + std::uint32_t reserved; +}; + struct SidebandResampleInfo { float ratio; std::int32_t samples; @@ -110,6 +127,7 @@ struct SidebandResampleInfo { struct Resampler { float ratio = 1.0f; + float change = 0.0f; std::vector frames; double position = 1.0; }; @@ -118,6 +136,7 @@ struct Instance { std::uint32_t codec = 0; std::uint64_t flags = 0; void* decoder = nullptr; + atrac9_handle_t* vibrationDecoder = nullptr; std::vector channelDecoders; std::vector channelBytes; std::vector blockPcm; @@ -136,15 +155,32 @@ struct Instance { AVCodecContext* opus = nullptr; std::uint32_t opusChannels = 0; std::uint32_t opusMappingFamily = 0; + std::uint32_t opusStreams = 0; + std::uint32_t opusCoupled = 0; + bool opusLayoutPending = false; std::uint32_t opusSampleRate = 0; std::vector opusPending; + AVCodecContext* aac = nullptr; + std::uint32_t aacConfig = 0; + std::uint32_t aacFrequencyIndex = 0; + std::uint32_t aacChannels = 0; + std::uint32_t aacSampleRate = 0; + std::uint32_t aacBitrate = 0; + std::uint32_t aacFrameSamples = 0; + std::uint32_t aacDelayFrames = 0; + bool aacHeaac = false; + std::uint32_t pcmChannels = 0; + std::uint32_t pcmSampleRate = 0; + bool pcmResampled = false; Resampler resampler; ~Instance() { if (decoder) Atrac9ReleaseHandle(decoder); + atrac9_free(vibrationDecoder); for (void* channelDecoder : channelDecoders) Atrac9ReleaseHandle(channelDecoder); if (mp3) avcodec_free_context(&mp3); if (opus) avcodec_free_context(&opus); + if (aac) avcodec_free_context(&aac); } }; @@ -154,7 +190,7 @@ std::atomic g_nextContext{1}; std::atomic g_nextInstance{1}; std::atomic g_nextBatch{1}; std::mutex& g_batchLock = *new std::mutex(); -std::set& g_batches = *new std::set(); +std::map& g_batches = *new std::map(); enum class JobKind : std::uint32_t { Initialize = 1, @@ -269,7 +305,23 @@ bool ParseMultichannelConfig(const std::uint8_t* config, At9MultichannelConfig& return true; } +bool IsAt9Vibration(const std::uint8_t* config) { + return config[0] == 0xFE && ((config[1] >> 1u) & 7u) >= 6u; +} + bool DescribeAt9Config(const std::uint8_t* configData, Atrac9CodecInfo& info) { + if (IsAt9Vibration(configData)) { + atrac9_info_t vibration{}; + if (atrac9_get_config_info(configData, &vibration) != 0 || vibration.sample_rate <= 0) return false; + info.channels = vibration.channels; + info.channelConfigIndex = (configData[1] >> 1u) & 7u; + info.samplingRate = vibration.sample_rate; + info.superframeSize = vibration.superframe_size; + info.framesInSuperframe = vibration.frames_per_superframe; + info.frameSamples = vibration.frame_samples; + std::memcpy(info.configData, configData, ATRAC9_CONFIG_DATA_SIZE); + return true; + } At9MultichannelConfig multichannel{}; const bool isMultichannel = ParseMultichannelConfig(configData, multichannel); unsigned char config[ATRAC9_CONFIG_DATA_SIZE]; @@ -292,12 +344,18 @@ bool DescribeAt9Config(const std::uint8_t* configData, Atrac9CodecInfo& info) { bool ResetDecoder(Instance& instance) { if (instance.decoder) Atrac9ReleaseHandle(instance.decoder); instance.decoder = nullptr; + atrac9_free(instance.vibrationDecoder); + instance.vibrationDecoder = nullptr; for (void* channelDecoder : instance.channelDecoders) Atrac9ReleaseHandle(channelDecoder); instance.channelDecoders.clear(); instance.superframeRemaining = 0; instance.frameInSuperframe = 0; unsigned char config[ATRAC9_CONFIG_DATA_SIZE]; std::memcpy(config, instance.info.configData, sizeof(config)); + if (IsAt9Vibration(config)) { + instance.vibrationDecoder = atrac9_init(config); + return instance.vibrationDecoder != nullptr; + } At9MultichannelConfig multichannel{}; if (!ParseMultichannelConfig(config, multichannel)) { instance.decoder = Atrac9GetHandle(); @@ -312,7 +370,7 @@ bool ResetDecoder(Instance& instance) { } bool HasAt9Decoder(const Instance& instance) { - return instance.decoder || !instance.channelDecoders.empty(); + return instance.decoder || instance.vibrationDecoder || !instance.channelDecoders.empty(); } int DecodeAt9Block(void* decoder, std::uint32_t encoding, const std::uint8_t* data, int size, std::uint8_t* pcm, int* used) { @@ -326,6 +384,25 @@ int DecodeAt9Block(void* decoder, std::uint32_t encoding, const std::uint8_t* da constexpr int AT9_MULTICHANNEL_BLOCK_OVERRUN = 0x7F000001; int DecodeAt9Frame(Instance& instance, const std::uint8_t* data, int size, std::uint8_t* pcm, std::uint32_t encoding, std::size_t sampleBytes, int* used) { + if (instance.vibrationDecoder) { + if (encoding > 2) NotImplemented_nid_no_patch("AJM ATRAC9 vibration PCM encoding other than PCM16, PCM32 and float"); + int samples = 0; + if (encoding == 0) samples = atrac9_decode_pcm16(instance.vibrationDecoder, data, size, reinterpret_cast(pcm), used); + else if (encoding == 2) samples = atrac9_decode_float(instance.vibrationDecoder, data, size, reinterpret_cast(pcm), used); + else { + std::array decoded{}; + samples = atrac9_decode(instance.vibrationDecoder, data, size, decoded.data(), used); + if (samples > 0) { + const auto count = static_cast(samples) * instance.info.channels; + if (count > decoded.size()) return AT9_MULTICHANNEL_BLOCK_OVERRUN; + for (std::size_t index = 0; index < count; ++index) { + const auto value = static_cast(std::clamp(static_cast(decoded[index]) * 65536.0, -2147483648.0, 2147483647.0)); + std::memcpy(pcm + index * sizeof(value), &value, sizeof(value)); + } + } + } + return samples < 0 ? samples : samples == instance.info.frameSamples ? 0 : AT9_MULTICHANNEL_BLOCK_OVERRUN; + } if (instance.channelDecoders.empty()) return DecodeAt9Block(instance.decoder, encoding, data, size, pcm, used); const std::size_t channels = instance.channelDecoders.size(); const auto frameSamples = static_cast(instance.info.frameSamples); @@ -356,14 +433,32 @@ int DecodeAt9Frame(Instance& instance, const std::uint8_t* data, int size, std:: } void OpenOpus(Instance& instance); +std::int32_t InitializeAac(Instance& instance, const std::uint8_t* parameters, std::uint64_t size); void ClearResampler(Instance& instance) { instance.resampler.frames.clear(); instance.resampler.position = 1.0; } +std::int32_t InitializePcm(Instance& instance, const std::uint8_t* parameters, std::uint64_t size) { + if (size != 12) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (codec 23 parameters other than 12 bytes)"); + std::uint32_t words[3] = {}; + std::memcpy(words, parameters, sizeof(words)); + if (words[0] != 0) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (codec 23 input format other than 0, interleaved s16)"); + if (words[1] == 0 || words[1] > 8 || words[2] == 0) return AJM_RESULT_INVALID_PARAMETER; + instance.pcmChannels = words[1]; + instance.pcmSampleRate = words[2]; + instance.pcmResampled = false; + instance.initialized = true; + instance.totalDecodedSamples = 0; + instance.segmentSamples = 0; + instance.gapless = {}; + return 0; +} + std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* parameters, std::uint64_t size) { ClearResampler(instance); + if (instance.codec == CODEC_PCM) return InitializePcm(instance, parameters, size); if (instance.codec == CODEC_OPUS) { if (size < 8) return AJM_RESULT_INVALID_PARAMETER; std::uint32_t channels = 0; @@ -374,12 +469,13 @@ std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* paramete std::uint32_t mappingFamily = 0; if (size >= 12) std::memcpy(&mappingFamily, parameters + 8, sizeof(mappingFamily)); if (sampleRate != 48000) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus sample rate other than 48000)"); - if (mappingFamily > 1 && (mappingFamily != 255 || channels != 1)) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus mapping family other than 0, 1 or a mono stream's 255)"); + if (mappingFamily > 1 && mappingFamily != 255) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus mapping family other than 0, 1 or 255)"); if (mappingFamily == 0 && channels > 2) return AJM_RESULT_INVALID_PARAMETER; instance.opusChannels = channels; instance.opusMappingFamily = mappingFamily; instance.opusSampleRate = sampleRate; - OpenOpus(instance); + instance.opusLayoutPending = mappingFamily == 255 && channels > 1; + if (!instance.opusLayoutPending) OpenOpus(instance); instance.opusPending.clear(); instance.initialized = true; instance.totalDecodedSamples = 0; @@ -387,6 +483,7 @@ std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* paramete instance.gapless = {}; return 0; } + if (instance.codec == CODEC_M4AAC) return InitializeAac(instance, parameters, size); if (instance.codec != CODEC_AT9) { instance.initialized = true; return 0; @@ -515,7 +612,7 @@ void StorePcm(double value, std::uint8_t* out, std::uint32_t encoding) { } bool Resampling(const Instance& instance) { - return instance.resampler.ratio != 1.0f || !instance.resampler.frames.empty(); + return instance.resampler.ratio != 1.0f || instance.resampler.change != 0.0f || !instance.resampler.frames.empty(); } std::size_t ResamplerHeld(const Instance& instance, std::size_t channels) { @@ -533,6 +630,10 @@ void ResamplerPush(Instance& instance, const double* samples, std::size_t frames stored.insert(stored.end(), samples, samples + frames * channels); } +double CatmullRom(double p0, double p1, double p2, double p3, double t) { + return p1 + 0.5 * t * (p2 - p0 + t * (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3 + t * (3.0 * (p1 - p2) + p3 - p0))); +} + void ResamplerProduce(Instance& instance, PcmOutputs& outputs, std::size_t channels, std::uint32_t encoding, std::size_t sampleBytes) { auto& resampler = instance.resampler; if (resampler.frames.empty() || channels == 0) return; @@ -548,11 +649,13 @@ void ResamplerProduce(Instance& instance, PcmOutputs& outputs, std::size_t chann const double p1 = resampler.frames[index * channels + channel]; const double p2 = resampler.frames[(index + 1) * channels + channel]; const double p3 = resampler.frames[(index + 2) * channels + channel]; - const double value = p1 + 0.5 * t * (p2 - p0 + t * (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3 + t * (3.0 * (p1 - p2) + p3 - p0))); - StorePcm(value, frame.data() + channel * sampleBytes, encoding); + StorePcm(CatmullRom(p0, p1, p2, p3, t), frame.data() + channel * sampleBytes, encoding); } outputs.Emit(frame.data(), frameBytes); resampler.position += resampler.ratio; + if (resampler.change != 0.0f) { + resampler.ratio = std::max(0.0f, resampler.ratio + resampler.change); + } } const auto index = std::min(static_cast(resampler.position), available); if (index > 1) { @@ -592,6 +695,8 @@ SidebandFormat CurrentFormat(const Instance& instance) { case CODEC_AT9: return {static_cast(instance.info.channels), ChannelMask(static_cast(instance.info.channels)), static_cast(instance.info.samplingRate), PcmEncoding(instance), 0, 0}; case CODEC_MP3: return {instance.mp3Channels, ChannelMask(instance.mp3Channels), instance.mp3SampleRate, PcmEncoding(instance), instance.mp3Bitrate, 0}; case CODEC_OPUS: return {instance.opusChannels, ChannelMask(instance.opusChannels), instance.opusSampleRate, PcmEncoding(instance), 0, 0}; + case CODEC_M4AAC: return {instance.aacChannels, ChannelMask(instance.aacChannels), instance.aacSampleRate, PcmEncoding(instance), instance.aacBitrate, 0}; + case CODEC_PCM: return {instance.pcmChannels, ChannelMask(instance.pcmChannels), instance.pcmSampleRate, PcmEncoding(instance), 0, 0}; default: throw std::runtime_error("AJM: the format of codec " + std::to_string(instance.codec) + " is not implemented"); } } @@ -617,7 +722,10 @@ void WriteRunSideband(const JobHeader& job, const Instance& instance, std::int32 } if (job.flags & SIDEBAND_FORMAT) write(&format, sizeof(format)); if (job.flags & SIDEBAND_GAPLESS_DECODE) write(&instance.gapless, sizeof(instance.gapless)); - if (job.flags & RUN_GET_CODEC_INFO) { + if ((job.flags & RUN_GET_CODEC_INFO) && instance.codec == CODEC_M4AAC) { + const SidebandM4aacCodecInfo codecInfo{instance.aacHeaac ? 1u : 0u, 0}; + write(&codecInfo, sizeof(codecInfo)); + } else if (job.flags & RUN_GET_CODEC_INFO) { const SidebandAt9CodecInfo codecInfo = At9CodecInfo(instance); write(&codecInfo, sizeof(codecInfo)); } @@ -743,14 +851,15 @@ std::uint32_t ReadBigEndian(const std::uint8_t* data, std::size_t bytes) { int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int parseOfl, AjmDecMp3ParseFrame* frame); -void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint32_t header, AjmDecMp3ParseFrame* frame) { +void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint32_t header, AjmDecMp3ParseFrame* frame, int parseOfl) { const std::size_t end = std::min(streamSize, frame->frame_size); - if (end <= 4) return; - const std::size_t available = end - 4; + const std::size_t sideInfoStart = 4 + (((header >> 16u) & 1u) == 0 ? 2u : 0u); + if (end <= sideInfoStart) return; + const std::size_t available = end - sideInfoStart; std::size_t bit = 0; const auto readBits = [&](std::uint32_t count) { std::uint32_t value = 0; - for (std::uint32_t i = 0; i < count; ++i, ++bit) value = (value << 1u) | (bit / 8 < available ? (stream[4 + bit / 8] >> (7 - bit % 8)) & 1u : 0u); + for (std::uint32_t i = 0; i < count; ++i, ++bit) value = (value << 1u) | (bit / 8 < available ? (stream[sideInfoStart + bit / 8] >> (7 - bit % 8)) & 1u : 0u); return value; }; const bool mpeg1 = ((header >> 19u) & 3u) == 3u; @@ -764,11 +873,20 @@ void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint } } bit += mainDataBits; - std::size_t at = 4 + std::min((bit + 7) / 8, available); + std::size_t at = sideInfoStart + std::min((bit + 7) / 8, available); - if (at + 8 <= end && (std::memcmp(stream + at, "Xing", 4) == 0 || std::memcmp(stream + at, "Info", 4) == 0)) { - const std::uint8_t flags = stream[at + 7]; - std::size_t field = at + 8; + std::size_t tagAt = at; + if (tagAt + 8 > end || (std::memcmp(stream + tagAt, "Xing", 4) != 0 && std::memcmp(stream + tagAt, "Info", 4) != 0)) { + const std::size_t fixedOffset = mpeg1 ? (mono ? 21u : 36u) : (mono ? 13u : 21u); + tagAt = 0; + if (fixedOffset + 8 <= end && (std::memcmp(stream + fixedOffset, "Xing", 4) == 0 || std::memcmp(stream + fixedOffset, "Info", 4) == 0)) { + tagAt = fixedOffset; + } + } + + if (tagAt != 0) { + const std::uint8_t flags = stream[tagAt + 7]; + std::size_t field = tagAt + 8; std::uint32_t frames = 0; if (flags & 0x01u) { if (field + 4 > end) return; @@ -795,30 +913,34 @@ void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint frame->encoder_delay = frame->samples_per_channel + delay + 529; frame->ofl_type = MP3_OFL_LAME; } - } else if (at != 36 && end >= 40 && std::memcmp(stream + 36, "VBRI", 4) == 0) { - NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (VBRI header outside MPEG-1 stereo)"); - } else if (at + 26 <= end && std::memcmp(stream + at, "VBRI", 4) == 0) { - frame->encoder_delay = ReadBigEndian(stream + at + 6, 2); + return; + } + + const std::size_t vbriAt = end >= 40 && std::memcmp(stream + 36, "VBRI", 4) == 0 ? 36 : at; + if (vbriAt + 26 <= end && std::memcmp(stream + vbriAt, "VBRI", 4) == 0) { + frame->encoder_delay = ReadBigEndian(stream + vbriAt + 6, 2); frame->ofl_type = MP3_OFL_VBRI; - if (frame->frame_size <= streamSize) { + if (parseOfl == 1 && frame->frame_size <= streamSize) { AjmDecMp3ParseFrame next{}; - if (ParseMp3Header(stream + frame->frame_size, streamSize - static_cast(frame->frame_size), 1, &next) == 0 && next.ofl_type == MP3_OFL_FGH) { + if (ParseMp3Header(stream + frame->frame_size, streamSize - static_cast(frame->frame_size), 2, &next) == 0 && next.ofl_type == MP3_OFL_FGH) { frame->encoder_delay += next.encoder_delay; frame->total_samples = next.total_samples; frame->ofl_type = MP3_OFL_VBRI_AND_FGH; } } - } else if (at + 10 <= end) { - while (at + 9 < end && stream[at] != 0xB4u) ++at; - if (at + 9 >= end) return; - std::uint8_t crc = 0xFF; - for (std::size_t i = 0; i < 9; ++i) { - for (int shift = 7; shift >= 0; --shift) { - const bool same = ((crc >> 7u) & 1u) == ((stream[at + i] >> shift) & 1u); - crc = static_cast(same ? (crc << 1u) ^ 0x45u : crc << 1u); - } + return; + } + + while (at + 9 < end && stream[at] != 0xB4u) ++at; + if (at + 9 >= end) return; + std::uint8_t crc = 0xFF; + for (std::size_t i = 0; i < 9; ++i) { + for (int shift = 7; shift >= 0; --shift) { + const bool same = ((crc >> 7u) & 1u) == ((stream[at + i] >> shift) & 1u); + crc = static_cast(same ? (crc << 1u) ^ 0x45u : crc << 1u); } - if (stream[at + 9] != crc) return; + } + if (stream[at + 9] == crc) { frame->encoder_delay = ReadBigEndian(stream + at + 1, 2); frame->total_samples = ReadBigEndian(stream + at + 3, 4); frame->ofl_type = MP3_OFL_FGH; @@ -836,13 +958,13 @@ int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int par }; const std::uint32_t header = (std::uint32_t{stream[0]} << 24u) | (std::uint32_t{stream[1]} << 16u) | (std::uint32_t{stream[2]} << 8u) | stream[3]; const std::uint32_t version = (header >> 19u) & 3u; + const std::uint32_t layer = (header >> 17u) & 3u; const std::uint32_t sampleRate = sampleRates[version][(header >> 10u) & 3u]; const std::uint32_t bitrate = kbps[version][(header >> 12u) & 15u] * 1000u; - const bool valid = (header >> 21u) == 0x7FFu && sampleRate != 0 && bitrate != 0; + const bool valid = (header >> 21u) == 0x7FFu && layer != 0 && sampleRate != 0 && bitrate != 0; AJM_TRACE("[ajm] parse mp3 frame %02x %02x %02x %02x (%u bytes, parse ofl %d) -> %s, %u Hz, %u bps\n", stream[0], stream[1], stream[2], stream[3], streamSize, parseOfl, valid ? "ok" : "invalid", sampleRate, bitrate); if (!valid) return SCE_AJM_ERROR_INVALID_PARAMETER; - if (parseOfl && ((header >> 17u) & 3u) != 1u) NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (original file length lookup outside layer III)"); - if (parseOfl && ((header >> 16u) & 1u) == 0) NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (original file length lookup in a CRC-protected frame)"); + if (parseOfl && layer != 1u) return SCE_AJM_ERROR_INVALID_PARAMETER; const bool mpeg1 = version == 3; frame->frame_size = (mpeg1 ? 144u : 72u) * bitrate / sampleRate + ((header >> 9u) & 1u); frame->num_channels = ((header >> 6u) & 3u) == 3u ? 1u : 2u; @@ -853,7 +975,7 @@ int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int par frame->num_frames = 0; frame->total_samples = 0; frame->ofl_type = MP3_OFL_NONE; - if (parseOfl) ParseMp3Ofl(stream, streamSize, header, frame); + if (parseOfl) ParseMp3Ofl(stream, streamSize, header, frame, parseOfl); return 0; } @@ -953,6 +1075,21 @@ void OpenOpus(Instance& instance) { if (!instance.opus) throw std::runtime_error("AJM: cannot allocate the FFmpeg Opus decoder"); av_channel_layout_default(&instance.opus->ch_layout, static_cast(instance.opusChannels)); instance.opus->sample_rate = 48000; + if (instance.opusMappingFamily == 255 && instance.opusChannels > 1) { + instance.opus->extradata_size = static_cast(21 + instance.opusChannels); + instance.opus->extradata = static_cast(av_mallocz(instance.opus->extradata_size + AV_INPUT_BUFFER_PADDING_SIZE)); + if (!instance.opus->extradata) throw std::bad_alloc(); + auto* header = instance.opus->extradata; + std::memcpy(header, "OpusHead", 8); + header[8] = 1; + header[9] = static_cast(instance.opusChannels); + header[12] = 0x80; + header[13] = 0xbb; + header[18] = 255; + header[19] = static_cast(instance.opusStreams); + header[20] = static_cast(instance.opusCoupled); + for (std::uint32_t channel = 0; channel < instance.opusChannels; ++channel) header[21 + channel] = static_cast(channel); + } if (instance.opusMappingFamily == 1) { static constexpr std::uint8_t streams[8] = {1, 1, 2, 2, 3, 4, 4, 5}; static constexpr std::uint8_t coupled[8] = {0, 1, 1, 2, 2, 2, 3, 3}; @@ -999,6 +1136,118 @@ std::size_t OpusPacketSamples(const std::uint8_t* packet, std::size_t size) { return frames * frameSamples; } + +std::size_t OpusLength(const std::uint8_t* data, std::size_t size, std::size_t& at) { + if (at >= size) return SIZE_MAX; + const std::size_t first = data[at++]; + if (first < 252) return first; + if (at >= size) return SIZE_MAX; + return first + 4u * data[at++]; +} + +std::size_t OpusSelfDelimitedBytes(const std::uint8_t* data, std::size_t size) { + if (size < 1) return SIZE_MAX; + const std::uint32_t code = data[0] & 3u; + std::size_t at = 1; + if (code == 0) { + const std::size_t frame = OpusLength(data, size, at); + return frame == SIZE_MAX ? SIZE_MAX : at + frame; + } + if (code == 1) { + const std::size_t frame = OpusLength(data, size, at); + return frame == SIZE_MAX ? SIZE_MAX : at + 2 * frame; + } + if (code == 2) { + const std::size_t first = OpusLength(data, size, at); + const std::size_t second = OpusLength(data, size, at); + return first == SIZE_MAX || second == SIZE_MAX ? SIZE_MAX : at + first + second; + } + if (at >= size) return SIZE_MAX; + const std::uint8_t count = data[at++]; + const std::size_t frames = count & 0x3fu; + if (frames == 0) return SIZE_MAX; + std::size_t padding = 0; + if ((count & 0x40u) != 0) { + for (;;) { + if (at >= size) return SIZE_MAX; + const std::uint8_t value = data[at++]; + padding += value == 255 ? 254 : value; + if (value != 255) break; + } + } + std::size_t total = 0; + if ((count & 0x80u) != 0) { + for (std::size_t frame = 0; frame < frames; ++frame) { + const std::size_t bytes = OpusLength(data, size, at); + if (bytes == SIZE_MAX) return SIZE_MAX; + total += bytes; + } + } else { + const std::size_t bytes = OpusLength(data, size, at); + if (bytes == SIZE_MAX) return SIZE_MAX; + total = bytes * frames; + } + return at + total + padding; +} + +void OpenOpusFromPacket(Instance& instance, const std::uint8_t* data, std::size_t size) { + std::uint32_t streams = 0; + std::uint32_t coupled = 0; + std::size_t offset = 0; + while (offset < size) { + if ((data[offset] & 0x04u) != 0) ++coupled; + ++streams; + const std::uint32_t channels = coupled * 2 + (streams - coupled); + if (channels >= instance.opusChannels) break; + const std::size_t bytes = OpusSelfDelimitedBytes(data + offset, size - offset); + if (bytes == SIZE_MAX || bytes >= size - offset) break; + offset += bytes; + } + if (coupled * 2 + (streams - coupled) != instance.opusChannels) throw std::runtime_error("AJM Opus: a mapping family 255 packet's " + std::to_string(streams) + " streams (" + std::to_string(coupled) + " stereo) do not make its " + std::to_string(instance.opusChannels) + " channels"); + instance.opusStreams = streams; + instance.opusCoupled = coupled; + instance.opusLayoutPending = false; + OpenOpus(instance); +} + +void RunPcm(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { + if (job.flags & ~SIDEBAND_STREAM) NotImplemented_nid_no_patch("AJM codec 23 run with flags other than SIDEBAND_STREAM"); + const auto encoding = PcmEncoding(instance); + if (encoding > 2) throw std::runtime_error("AJM codec 23: PCM encoding " + std::to_string(encoding) + " is not implemented"); + if (instance.resampler.change != 0.0f) NotImplemented_nid_no_patch("AJM codec 23 run with a resample ratio change per sample"); + if (instance.gapless.totalSamples != 0 || instance.gapless.skipSamples != 0) NotImplemented_nid_no_patch("AJM codec 23 run with gapless decode counts"); + if (instance.pcmResampled) NotImplemented_nid_no_patch("AJM codec 23 run after a resampled run without an initialize or context clear in between"); + const JobInput input(job, inputs); + PcmOutputs pcmOutputs(outputs, job.outputCount); + const std::size_t channels = instance.pcmChannels; + const std::size_t inputFrameBytes = channels * sizeof(std::int16_t); + const std::size_t sampleBytes = encoding == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); + if (input.empty()) NotImplemented_nid_no_patch("AJM codec 23 run with an empty input"); + if (input.size() % inputFrameBytes != 0) NotImplemented_nid_no_patch("AJM codec 23 run with an input that is not a whole number of frames"); + const std::size_t frames = input.size() / inputFrameBytes; + const double ratio = instance.resampler.ratio; + const std::size_t room = pcmOutputs.Room() / (channels * sampleBytes); + std::size_t outputFrames = 0; + while (outputFrames < room && static_cast(outputFrames) * ratio < static_cast(frames)) ++outputFrames; + if (static_cast(outputFrames) * ratio < static_cast(frames)) NotImplemented_nid_no_patch("AJM codec 23 run whose output cannot hold the whole resampled input"); + const auto sample = [&](std::size_t index, std::size_t channel) { return PcmValue(input.data() + (std::min(index, frames - 1) * channels + channel) * sizeof(std::int16_t), 0); }; + std::vector frame(channels * sampleBytes); + for (std::size_t output = 0; output < outputFrames; ++output) { + const double position = static_cast(output) * ratio; + const auto index = static_cast(position); + const double t = position - static_cast(index); + for (std::size_t channel = 0; channel < channels; ++channel) { + const double value = CatmullRom(sample(index == 0 ? 0 : index - 1, channel), sample(index, channel), sample(index + 1, channel), sample(index + 2, channel), t); + StorePcm(value, frame.data() + channel * sampleBytes, encoding); + } + pcmOutputs.Emit(frame.data(), frame.size()); + } + if (ratio != 1.0) instance.pcmResampled = true; + instance.totalDecodedSamples += frames; + AJM_TRACE("[ajm] instance %u pcm run flags 0x%llx: %zu input bytes consumed, %zu output bytes, ratio %f -> produced %zu, %zu channels, total samples %llu\n", job.instance, static_cast(job.flags), input.size(), pcmOutputs.capacity, ratio, pcmOutputs.produced, channels, static_cast(instance.totalDecodedSamples)); + WriteRunSideband(job, instance, 0, input.size(), pcmOutputs.produced, 1, CurrentFormat(instance)); +} + void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { const JobInput input(job, inputs); PcmOutputs pcmOutputs(outputs, job.outputCount); @@ -1048,6 +1297,7 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } + if (instance.opusLayoutPending) OpenOpusFromPacket(instance, data, bytes); packet->data = const_cast(data); packet->size = static_cast(bytes); if (avcodec_send_packet(instance.opus, packet.get()) < 0) { @@ -1076,9 +1326,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); @@ -1110,6 +1361,193 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, CurrentFormat(instance)); } +std::uint32_t InstanceChannels(const Instance& instance) { + return static_cast((instance.flags >> 3u) & 0xFu); +} + +std::uint32_t AacRawChannels(const Instance& instance) { + const std::uint32_t channels = InstanceChannels(instance); + return channels == 0 ? 2 : channels; +} + +std::uint32_t AacDelayFrames(const Instance& instance) { + return (instance.flags & M4AAC_ENABLE_NONDELAY_OUTPUT) ? 0 : M4AAC_DELAY_FRAMES; +} + +void OpenAac(Instance& instance) { + static const bool quiet = (av_log_set_level(TraceEnabled() ? AV_LOG_WARNING : AV_LOG_QUIET), true); + (void)quiet; + const AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_AAC); + if (instance.aac) avcodec_free_context(&instance.aac); + instance.aac = codec ? avcodec_alloc_context3(codec) : nullptr; + if (!instance.aac) throw std::runtime_error("AJM: cannot allocate the FFmpeg AAC decoder"); + if (instance.aacConfig == M4AAC_CONFIG_RAW) { + const std::uint32_t config = (2u << 11u) | (instance.aacFrequencyIndex << 7u) | (AacRawChannels(instance) << 3u); + instance.aac->extradata = static_cast(av_mallocz(2 + AV_INPUT_BUFFER_PADDING_SIZE)); + if (!instance.aac->extradata) throw std::bad_alloc(); + instance.aac->extradata[0] = static_cast(config >> 8u); + instance.aac->extradata[1] = static_cast(config); + instance.aac->extradata_size = 2; + } + if (avcodec_open2(instance.aac, codec, nullptr) < 0) throw std::runtime_error("AJM: cannot open the FFmpeg AAC decoder"); +} + +std::int32_t InitializeAac(Instance& instance, const std::uint8_t* parameters, std::uint64_t size) { + if (size < 8) return AJM_RESULT_INVALID_PARAMETER; + std::uint32_t config = 0; + std::uint32_t frequencyIndex = 0; + std::memcpy(&config, parameters, sizeof(config)); + std::memcpy(&frequencyIndex, parameters + 4, sizeof(frequencyIndex)); + if (config != M4AAC_CONFIG_ADTS && config != M4AAC_CONFIG_RAW) return AJM_RESULT_INVALID_PARAMETER; + if (config == M4AAC_CONFIG_RAW && frequencyIndex >= 12) return AJM_RESULT_INVALID_PARAMETER; + const std::uint32_t rawChannels = AacRawChannels(instance); + if (config == M4AAC_CONFIG_RAW && rawChannels != 1 && rawChannels != 2 && rawChannels != 6) throw std::runtime_error("AJM M4AAC: raw initialization of an instance created for " + std::to_string(rawChannels) + " channels is not implemented (only 1, 2 and 6)"); + instance.aacConfig = config; + instance.aacFrequencyIndex = frequencyIndex; + OpenAac(instance); + instance.aacChannels = 0; + instance.aacSampleRate = 0; + instance.aacBitrate = 0; + instance.aacFrameSamples = 0; + instance.aacHeaac = false; + instance.aacDelayFrames = AacDelayFrames(instance); + instance.initialized = true; + instance.totalDecodedSamples = 0; + instance.segmentSamples = 0; + instance.gapless = {}; + return 0; +} + +struct AdtsHeader { + std::size_t frameBytes; + std::uint32_t profile; + std::uint32_t frequencyIndex; + std::uint32_t channelConfig; + std::uint32_t rawBlocks; +}; + +constexpr std::size_t ADTS_HEADER_BYTES = 7; + +bool ParseAdtsHeader(const std::uint8_t* data, AdtsHeader& header) { + if (data[0] != 0xFFu || (data[1] & 0xF6u) != 0xF0u) return false; + header.profile = data[2] >> 6u; + header.frequencyIndex = (data[2] >> 2u) & 0xFu; + header.channelConfig = ((data[2] & 1u) << 2u) | (data[3] >> 6u); + header.frameBytes = ((std::size_t{data[3]} & 3u) << 11u) | (std::size_t{data[4]} << 3u) | (data[5] >> 5u); + header.rawBlocks = data[6] & 3u; + return header.frameBytes >= ((data[1] & 1u) ? ADTS_HEADER_BYTES : ADTS_HEADER_BYTES + 2); +} + +std::size_t EmittedSamples(const Instance& instance, std::size_t samples) { + if (instance.gapless.skippedSamples < instance.gapless.skipSamples) samples -= std::min(samples, instance.gapless.skipSamples - instance.gapless.skippedSamples); + if (instance.gapless.totalSamples != 0) { + const std::uint64_t remaining = instance.gapless.totalSamples > instance.segmentSamples ? instance.gapless.totalSamples - instance.segmentSamples : 0; + samples = static_cast(std::min(samples, remaining)); + } + return samples; +} + +bool AacLayoutImplemented(const AVChannelLayout& layout) { + const AVChannelLayout mono = AV_CHANNEL_LAYOUT_MONO; + const AVChannelLayout stereo = AV_CHANNEL_LAYOUT_STEREO; + const AVChannelLayout surround = AV_CHANNEL_LAYOUT_5POINT1_BACK; + return av_channel_layout_compare(&layout, &mono) == 0 || av_channel_layout_compare(&layout, &stereo) == 0 || av_channel_layout_compare(&layout, &surround) == 0; +} + +std::size_t AdtsFrameBytes(const Instance& instance, const std::uint8_t* data) { + AdtsHeader header{}; + if (!ParseAdtsHeader(data, header)) throw std::runtime_error("AJM M4AAC: the input at the consumed offset is not an ADTS frame header (resynchronization is not implemented)"); + if (header.profile != 1) throw std::runtime_error("AJM M4AAC: ADTS profile " + std::to_string(header.profile) + " is not implemented (only AAC LC)"); + if (header.rawBlocks != 0) throw std::runtime_error("AJM M4AAC: ADTS frames with more than one raw data block are not implemented"); + if (header.frequencyIndex >= 12) throw std::runtime_error("AJM M4AAC: ADTS sampling frequency index " + std::to_string(header.frequencyIndex) + " is not implemented"); + if (header.channelConfig != 1 && header.channelConfig != 2 && header.channelConfig != 6) throw std::runtime_error("AJM M4AAC: ADTS channel configuration " + std::to_string(header.channelConfig) + " is not implemented (only 1, 2 and 6)"); + const std::uint32_t limit = InstanceChannels(instance); + if (limit != 0 && header.channelConfig > limit) throw std::runtime_error("AJM M4AAC: an ADTS stream of " + std::to_string(header.channelConfig) + " channels on an instance created for " + std::to_string(limit) + " is not implemented"); + return header.frameBytes; +} + +void RunAac(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { + const bool raw = instance.aacConfig == M4AAC_CONFIG_RAW; + if (raw && (job.flags & RUN_MULTIPLE_FRAMES)) throw std::runtime_error("AJM M4AAC: RUN_MULTIPLE_FRAMES on raw input is not implemented (a raw access unit carries no length)"); + const JobInput input(job, inputs); + PcmOutputs pcmOutputs(outputs, job.outputCount); + const auto encoding = PcmEncoding(instance); + if (encoding > 2) throw std::runtime_error("AJM M4AAC: PCM encoding " + std::to_string(encoding) + " is not implemented"); + const std::size_t sampleBytes = encoding == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); + std::unique_ptr packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); }); + std::unique_ptr decoded(av_frame_alloc(), [](AVFrame* f) { av_frame_free(&f); }); + if (!packet || !decoded) throw std::bad_alloc(); + std::vector bitstream; + std::vector pcm; + + std::int32_t result = 0; + std::size_t consumed = 0; + std::uint32_t frames = 0; + if (Resampling(instance)) ResamplerProduce(instance, pcmOutputs, instance.aacChannels, encoding, sampleBytes); + while (consumed < input.size()) { + const std::uint8_t* data = input.data() + consumed; + const std::size_t available = input.size() - consumed; + if (!raw && available < ADTS_HEADER_BYTES) { + result |= AJM_RESULT_PARTIAL_INPUT; + break; + } + const std::size_t frameBytes = raw ? available : AdtsFrameBytes(instance, data); + if (available < frameBytes) { + result |= AJM_RESULT_PARTIAL_INPUT; + break; + } + const std::size_t frameChannelBytes = instance.aacChannels * sampleBytes; + if (pcmOutputs.Room() < (Resampling(instance) ? frameChannelBytes : EmittedSamples(instance, instance.aacFrameSamples) * frameChannelBytes)) { + if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; + break; + } + bitstream.assign(data, data + frameBytes); + bitstream.resize(frameBytes + AV_INPUT_BUFFER_PADDING_SIZE, 0); + packet->data = bitstream.data(); + packet->size = static_cast(frameBytes); + int status = avcodec_send_packet(instance.aac, packet.get()); + std::size_t received = 0; + while (status >= 0 && (status = avcodec_receive_frame(instance.aac, decoded.get())) == 0) { + if (++received > 1) throw std::runtime_error("AJM M4AAC: one input frame decoded to more than one output frame (raw input holding several access units is not implemented)"); + const auto format = static_cast(decoded->format); + if (format != AV_SAMPLE_FMT_FLTP) throw std::runtime_error("AJM M4AAC: FFmpeg sample format " + std::to_string(decoded->format) + " is not converted"); + if (!AacLayoutImplemented(decoded->ch_layout)) throw std::runtime_error("AJM M4AAC: a decoded layout of " + std::to_string(decoded->ch_layout.nb_channels) + " channels is not implemented (only mono, stereo and 5.1)"); + const bool heaac = instance.aac->profile == AV_PROFILE_AAC_HE || instance.aac->profile == AV_PROFILE_AAC_HE_V2; + if (heaac && (instance.flags & M4AAC_ENABLE_SBR_DECODE) == 0) throw std::runtime_error("AJM M4AAC: an HE-AAC (SBR or PS) stream on an instance without ENABLE_SBR_DECODE is not implemented"); + const auto channels = static_cast(decoded->ch_layout.nb_channels); + const auto samples = static_cast(decoded->nb_samples); + instance.aacChannels = static_cast(channels); + instance.aacSampleRate = static_cast(decoded->sample_rate); + instance.aacFrameSamples = static_cast(samples); + instance.aacHeaac = heaac; + instance.aacBitrate = samples == 0 ? 0 : static_cast(std::uint64_t{frameBytes} * 8u * instance.aacSampleRate / samples); + if (instance.aacDelayFrames == 0) { + pcm.resize(samples * channels * sampleBytes); + for (std::size_t sample = 0; sample < samples; ++sample) { + for (std::size_t channel = 0; channel < channels; ++channel) StorePcm(reinterpret_cast(decoded->extended_data[channel])[sample], pcm.data() + (sample * channels + channel) * sampleBytes, encoding); + } + if (!Resampling(instance) && EmittedSamples(instance, samples) * channels * sampleBytes > pcmOutputs.Room()) throw std::runtime_error("AJM M4AAC: a decoded frame of " + std::to_string(samples) + " samples does not fit the " + std::to_string(pcmOutputs.Room()) + " output bytes left (a change of the frame length is not implemented)"); + EmitFrame(instance, pcmOutputs, pcm.data(), samples, channels, sampleBytes); + } + av_frame_unref(decoded.get()); + } + if (status != AVERROR(EAGAIN)) { + avcodec_flush_buffers(instance.aac); + result |= AJM_RESULT_CODEC_ERROR | AJM_RESULT_FATAL; + break; + } + if (instance.aacDelayFrames > 0) --instance.aacDelayFrames; + consumed += frameBytes; + ++frames; + if ((job.flags & RUN_MULTIPLE_FRAMES) == 0) break; + } + if (consumed == input.size() && frames == 0 && pcmOutputs.produced == 0 && result == 0) result |= AJM_RESULT_PARTIAL_INPUT; + + AJM_TRACE("[ajm] instance %u m4aac run flags 0x%llx: %zu input bytes, %zu output bytes -> result 0x%x, %u frames, consumed %zu, produced %zu, %u channels, %u Hz, %u bps, heaac %d, total samples %llu\n", job.instance, static_cast(job.flags), input.size(), pcmOutputs.capacity, static_cast(result), frames, consumed, pcmOutputs.produced, + instance.aacChannels, instance.aacSampleRate, instance.aacBitrate, instance.aacHeaac ? 1 : 0, static_cast(instance.totalDecodedSamples)); + WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, CurrentFormat(instance)); +} + void ClearContext(Instance& instance) { instance.totalDecodedSamples = 0; instance.segmentSamples = 0; @@ -1117,16 +1555,26 @@ void ClearContext(Instance& instance) { if (HasAt9Decoder(instance) && instance.initialized) ResetDecoder(instance); if (instance.mp3) avcodec_flush_buffers(instance.mp3); if (instance.opus) avcodec_flush_buffers(instance.opus); + if (instance.aac) { + avcodec_flush_buffers(instance.aac); + instance.aacDelayFrames = AacDelayFrames(instance); + } instance.opusPending.clear(); + instance.pcmResampled = false; ClearResampler(instance); } +void CheckAacFlags(std::uint64_t flags) { + if ((flags >> 32u) & ~((M4AAC_ENABLE_SBR_DECODE | M4AAC_ENABLE_NONDELAY_OUTPUT | M4AAC_SURROUND_INTERLEAVE_ORDERS) >> 32u)) NotImplemented_nid_no_patch("sceAjmInstanceCreate (M4AAC codec flags other than ENABLE_SBR_DECODE, ENABLE_NONDELAY_OUTPUT and the surround interleave orders)"); +} + std::size_t ControlInitializeSize(std::uint32_t codec) { switch (codec) { case CODEC_MP3: return 0; case CODEC_AT9: return 8; + case CODEC_M4AAC: return 8; case CODEC_OPUS: return 12; - default: NotImplemented_nid_no_patch("sceAjmBatchJobControl (INITIALIZE for a codec other than MP3, ATRAC9 and Opus)"); return 0; + default: NotImplemented_nid_no_patch("sceAjmBatchJobControl (INITIALIZE for a codec other than MP3, ATRAC9, M4AAC and Opus)"); return 0; } } @@ -1150,7 +1598,7 @@ void Control(Instance& instance, const JobHeader& job, const AjmBuffer* inputs) } std::size_t HeldSamples(const Instance& instance) { - const std::size_t channels = instance.codec == CODEC_OPUS ? instance.opusChannels : instance.codec == CODEC_MP3 ? instance.mp3Channels : static_cast(std::max(0, instance.info.channels)); + const std::size_t channels = instance.codec == CODEC_PCM ? instance.pcmChannels : instance.codec == CODEC_OPUS ? instance.opusChannels : instance.codec == CODEC_MP3 ? instance.mp3Channels : instance.codec == CODEC_M4AAC ? instance.aacChannels : static_cast(std::max(0, instance.info.channels)); if (Resampling(instance)) return ResamplerHeld(instance, channels); const std::size_t sampleBytes = PcmEncoding(instance) == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); return channels == 0 ? 0 : instance.opusPending.size() / (channels * sampleBytes); @@ -1197,11 +1645,15 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out case JobKind::Control: Control(*instance, job, inputs); break; - case JobKind::SetResampleParameters: - std::memcpy(&instance->resampler.ratio, job.parameters, sizeof(instance->resampler.ratio)); - AJM_TRACE("[ajm] instance %u set resample ratio %f\n", job.instance, static_cast(instance->resampler.ratio)); + case JobKind::SetResampleParameters: { + float params[2] = {1.0f, 0.0f}; + std::memcpy(params, job.parameters, std::min(job.parameterSize, sizeof(params))); + instance->resampler.ratio = params[0]; + instance->resampler.change = job.parameterSize >= sizeof(float) * 2 ? params[1] : 0.0f; + AJM_TRACE("[ajm] instance %u set resample ratio %f change %f\n", job.instance, static_cast(instance->resampler.ratio), static_cast(instance->resampler.change)); WriteResult(job.sideband, job.sidebandSize, 0); break; + } case JobKind::GetResampleInfo: { const SidebandResampleInfo resample{instance->resampler.ratio, static_cast(HeldSamples(*instance)), {}}; WriteResult(job.sideband, job.sidebandSize, 0); @@ -1212,12 +1664,12 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out if (!instance->initialized) { AJM_TRACE("[ajm] instance %u run before initialize\n", job.instance); WriteResult(job.sideband, job.sidebandSize, AJM_RESULT_NOT_INITIALIZED); - } else if (instance->codec != CODEC_AT9 && instance->codec != CODEC_MP3 && instance->codec != CODEC_OPUS) { + } else if (instance->codec != CODEC_AT9 && instance->codec != CODEC_MP3 && instance->codec != CODEC_M4AAC && instance->codec != CODEC_PCM && instance->codec != CODEC_OPUS) { throw std::runtime_error("AJM: decoding codec " + std::to_string(instance->codec) + " is not implemented"); } else if ((job.flags & RUN_GET_CODEC_INFO) && (job.flags & RUN_MULTIPLE_FRAMES)) { NotImplemented_nid_no_patch("AJM run job with both RUN_GET_CODEC_INFO and RUN_MULTIPLE_FRAMES (sideband order)"); - } else if ((job.flags & RUN_GET_CODEC_INFO) && instance->codec != CODEC_AT9) { - NotImplemented_nid_no_patch("AJM RUN_GET_CODEC_INFO for a codec other than ATRAC9"); + } else if ((job.flags & RUN_GET_CODEC_INFO) && instance->codec != CODEC_AT9 && instance->codec != CODEC_M4AAC) { + NotImplemented_nid_no_patch("AJM RUN_GET_CODEC_INFO for a codec other than ATRAC9 and M4AAC"); } else if (job.inputCount == 0 && job.outputCount == 0) { AJM_TRACE("[ajm] instance %u run flags 0x%llx without buffers, sideband %llu bytes\n", job.instance, static_cast(job.flags), static_cast(job.sidebandSize)); WriteRunSideband(job, *instance, 0, 0, 0, 0, CurrentFormat(*instance)); @@ -1225,6 +1677,10 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out RunAt9(*instance, job, inputs, outputs); } else if (instance->codec == CODEC_MP3) { RunMp3(*instance, job, inputs, outputs); + } else if (instance->codec == CODEC_M4AAC) { + RunAac(*instance, job, inputs, outputs); + } else if (instance->codec == CODEC_PCM) { + RunPcm(*instance, job, inputs, outputs); } else { RunOpus(*instance, job, inputs, outputs); } @@ -1288,12 +1744,17 @@ int APS5_VABI sceAjmInstanceCreate(uint32_t context, uint32_t codec, uint64_t fl OpenMp3(*created); created->initialized = true; } - const std::uint32_t id = (codec << 14) | (g_nextInstance.fetch_add(1, std::memory_order_relaxed) & 0x3FFF); + if (codec == CODEC_M4AAC) CheckAacFlags(flags); 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) { @@ -1387,8 +1848,13 @@ int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo* info, uint32_t i } int APS5_VABI sceAjmBatchJobSetResampleParametersEx(AjmBatchInfo* info, uint32_t instance, float ratio_start, float ratio_change_per_sample, uint32_t flags, void* result) { - if (ratio_change_per_sample != 0.0f) throw std::runtime_error("sceAjmBatchJobSetResampleParametersEx: a ratio change per sample is unsupported"); - return sceAjmBatchJobSetResampleParameters(info, instance, ratio_start, flags, result); + (void)flags; + if (!std::isfinite(ratio_start) || ratio_start <= 0.0f || !std::isfinite(ratio_change_per_sample)) return SCE_AJM_ERROR_INVALID_PARAMETER; + auto header = MakeHeader(JobKind::SetResampleParameters, instance, result, sizeof(SidebandResult)); + const float parameters[2] = {ratio_start, ratio_change_per_sample}; + std::memcpy(header.parameters, parameters, sizeof(parameters)); + header.parameterSize = sizeof(parameters); + return Append(info, header, nullptr, nullptr); } int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo* info, uint32_t instance, void* result) { @@ -1425,7 +1891,7 @@ int APS5_VABI sceAjmBatchJobGetStatistics(AjmBatchInfo* info, float interval, vo } int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int priority, AjmBatchError* error, uint32_t* batch) { - (void)context; + if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; (void)priority; if (!info || !batch) return SCE_AJM_ERROR_INVALID_PARAMETER; AJM_TRACE("[ajm] batch start: context %u, priority %d, %llu of %llu buffer bytes used\n", context, priority, static_cast(info->offset), static_cast(info->size)); @@ -1455,18 +1921,20 @@ int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int p const auto id = g_nextBatch.fetch_add(1, std::memory_order_relaxed); { std::lock_guard lock(g_batchLock); - g_batches.insert(id); + g_batches.emplace(id, context); } *batch = id; return 0; } int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout, AjmBatchError* error) { - (void)context; + if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; (void)timeout; { std::lock_guard lock(g_batchLock); - if (g_batches.erase(batch) == 0) return SCE_AJM_ERROR_INVALID_BATCH; + const auto found = g_batches.find(batch); + if (found == g_batches.end() || found->second != context) return SCE_AJM_ERROR_INVALID_BATCH; + g_batches.erase(found); } if (error) std::memset(error, 0, sizeof(*error)); return 0; @@ -1475,7 +1943,8 @@ int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout int APS5_VABI sceAjmBatchCancel(uint32_t context, uint32_t batch) { if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; std::lock_guard lock(g_batchLock); - return g_batches.contains(batch) ? 0 : SCE_AJM_ERROR_INVALID_BATCH; + const auto found = g_batches.find(batch); + return found != g_batches.end() && found->second == context ? 0 : SCE_AJM_ERROR_INVALID_BATCH; } int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* error) { diff --git a/core/libs/prx/libSceAjm.native/tests/Atrac9ConfigData.c b/core/libs/prx/libSceAjm.native/tests/Atrac9ConfigData.c new file mode 100644 index 0000000000..8482f3d6b6 --- /dev/null +++ b/core/libs/prx/libSceAjm.native/tests/Atrac9ConfigData.c @@ -0,0 +1,27 @@ +#include "libatrac9.h" +#include + +static int ChannelCount(int sampleRateIndex, int channelConfigIndex) +{ + unsigned char config[ATRAC9_CONFIG_DATA_SIZE] = {0xFE, (unsigned char)(sampleRateIndex << 4 | channelConfigIndex << 1), 0x0B, 0xE0}; + Atrac9CodecInfo info = {0}; + void* decoder = Atrac9GetHandle(); + if (decoder == NULL) exit(EXIT_FAILURE); + int channels = -1; + if (Atrac9InitDecoder(decoder, config) == 0 && Atrac9GetCodecInfo(decoder, &info) == 0) channels = info.channels; + Atrac9ReleaseHandle(decoder); + return channels; +} + +int main(void) +{ + static const int channels[8] = {1, 2, 2, 6, 8, 4, -1, -1}; + for (int sampleRateIndex = 0; sampleRateIndex < 16; ++sampleRateIndex) + { + for (int channelConfigIndex = 0; channelConfigIndex < 8; ++channelConfigIndex) + { + if (ChannelCount(sampleRateIndex, channelConfigIndex) != channels[channelConfigIndex]) return EXIT_FAILURE; + } + } + return EXIT_SUCCESS; +} diff --git a/core/libs/prx/libSceAjm/CMakeLists.txt b/core/libs/prx/libSceAjm/CMakeLists.txt new file mode 100644 index 0000000000..7a9c708eae --- /dev/null +++ b/core/libs/prx/libSceAjm/CMakeLists.txt @@ -0,0 +1,2 @@ +include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceAjm.native/AjmLibrary.cmake) +add_sce_ajm_library(libSceAjm ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) diff --git a/core/libs/prx/libSceAjm/Export.cpp b/core/libs/prx/libSceAjm/Export.cpp index fb34206c21..508f721ede 100644 --- a/core/libs/prx/libSceAjm/Export.cpp +++ b/core/libs/prx/libSceAjm/Export.cpp @@ -5,6 +5,19 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceAjmBatchJobControlBufferRa() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmBatchJobRunSplitBufferRa() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmBatchStartBuffer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceAjmi/Export.cpp b/core/libs/prx/libSceAjmi/Export.cpp index fb34206c21..83e8ac5ffa 100644 --- a/core/libs/prx/libSceAjmi/Export.cpp +++ b/core/libs/prx/libSceAjmi/Export.cpp @@ -7,4 +7,14 @@ extern "C" { APS5_DUMMY_FUN +int APS5_VABI sceAjmBatchJobInlineBuffer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmBatchJobRunBufferRa(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index fba6d9efbe..ca51ae70f2 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -7,7 +7,6 @@ #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include -#include #include #include #include @@ -15,7 +14,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 +25,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,10 +92,10 @@ 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); std::memcpy(destination, &header, sizeof(header)); if (data) std::memcpy(destination + sizeof(header), data, std::uint64_t{dwords} * 4u); buffer->offset += static_cast(bytes); @@ -116,7 +115,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); @@ -224,18 +224,6 @@ static int RecordWriteAddressFromCounterPair(Apr::CommandBufferObject* buffer, v return AppendCommand(buffer, Apr::Opcode::WriteAddressFromCounterPair, Apr::WriteAddressFromCounterCommand{{}, reinterpret_cast(address), counter, counter + 1u}); } -static constexpr int ThrdSuccess = 0; -static constexpr int ThrdNomem = 1; -static constexpr int ThrdTimedout = 2; -static constexpr int ThrdBusy = 3; -static constexpr int ThrdError = 4; - -static constexpr int CppDeviceOrResourceBusy = 0; -static constexpr int CppInvalidArgument = 1; -static constexpr int CppResourceUnavailableTryAgain = 6; - -static constexpr int MtxPlain = 0x1; - static std::atomic_flag sharedPtrSpinLock; struct AmmUsageStatsData { @@ -250,54 +238,10 @@ static_assert(sizeof(AmmUsageStatsData) == 0x18); static constexpr std::uint16_t AmmPageTablePoolEntries = 512; static constexpr std::uint32_t AmmRingIdleFlags = 0x7; -struct GuestPad { - unsigned (APS5_VABI* const* vtable)(GuestPad*); - void* condition; - void* mutex; - bool started; -}; - -static_assert(offsetof(GuestPad, condition) == 0x8 && offsetof(GuestPad, mutex) == 0x10 && offsetof(GuestPad, started) == 0x18 && sizeof(GuestPad) == 0x20); - extern "C" { -int APS5_VABI pthread_create_nid_postfix(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg); -int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value); -int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); -void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); -int APS5_VABI _Mtx_lock_nid_postfix(void** handle); -int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); int APS5_VABI _Cnd_init_nid_postfix(void** handle); -void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); -int APS5_VABI _Cnd_wait_nid_postfix(void** condition, void** mutex); -int APS5_VABI _Cnd_signal_nid_postfix(void** handle); -int APS5_VABI wcsrtombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t** source, std::size_t limit, void* state); -int APS5_VABI _Cnd_init_nid_postfix(void** handle); -int SignalCondition_nid_no_patch(void** handle); -[[noreturn]] void ThrowCppError_nid_no_patch(int code); -} - -static void RequireThrd(int result, const char* what) { - switch (result) { - case ThrdSuccess: - return; - case ThrdNomem: - case ThrdTimedout: - ThrowCppError_nid_no_patch(CppResourceUnavailableTryAgain); - case ThrdBusy: - ThrowCppError_nid_no_patch(CppDeviceOrResourceBusy); - case ThrdError: - ThrowCppError_nid_no_patch(CppInvalidArgument); - } - throw std::runtime_error(std::string(what) + " failed with " + std::to_string(result)); -} - -static void* APS5_VABI PadEntry_nid_no_patch(void* argument) { - auto* pad = static_cast(argument); - const auto go = pad->vtable[0]; - return reinterpret_cast(static_cast(go(pad))); -} - -extern "C" { +int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit); +[[noreturn]] void APS5_VABI __cxa_call_terminate_nid_postfix(void* exception); int APS5_VABI sceAmprCommandBufferWriteAddressOnCompletion(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint64_t value) { if (!ValidWriteAddress(address)) return SCE_KERNEL_ERROR_EINVAL; @@ -344,6 +288,7 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWaitOnCounter() { return sizeof std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteKernelEventQueueOnCompletion() { return sizeof(Apr::WriteKernelEventQueueCommand); } int APS5_VABI sceAmprAprCommandBufferConstructor(Apr::CommandBufferObject* buffer, uint64_t* gatherState, uint64_t* scatterState) { + if (!buffer || !gatherState || !scatterState) return SCE_KERNEL_ERROR_EINVAL; buffer->type = Apr::BufferType::Apr; *gatherState = 0; *scatterState = 0; @@ -449,6 +394,7 @@ int APS5_VABI sceAmprCommandBufferConstructNop(Apr::CommandBufferObject* buffer, } int APS5_VABI sceAmprCommandBufferConstructor(Apr::CommandBufferObject* buffer) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; *buffer = {Apr::BufferType::Generic, 0, 0, 0, 0, nullptr}; return 0; } @@ -485,7 +431,6 @@ int APS5_VABI sceAmprCommandBufferNop(Apr::CommandBufferObject* buffer, std::uin int APS5_VABI sceAmprCommandBufferNopWithData(Apr::CommandBufferObject* buffer, std::uint32_t dwords, const std::uint32_t* data) { if (dwords > 15u) return SCE_KERNEL_ERROR_EINVAL; - if (dwords != 0u && !data) throw std::invalid_argument("sceAmprCommandBufferNopWithData: null data"); return AppendNop(buffer, dwords, data); } @@ -503,12 +448,14 @@ int APS5_VABI sceAmprCommandBufferPushMarkerWithColor(Apr::CommandBufferObject* } int APS5_VABI sceAmprCommandBufferReset(Apr::CommandBufferObject* buffer) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; buffer->offset = 0; buffer->numCommands = 0; return 0; } int APS5_VABI sceAmprCommandBufferSetBuffer(Apr::CommandBufferObject* buffer, void* memory, uint32_t size) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; buffer->base = static_cast(memory); buffer->size = size; buffer->offset = 0; @@ -898,64 +845,6 @@ int APS5_VABI sceAmprAmmWaitCommandBufferCompletion(std::uint32_t id) { return AmmSubmitted_nid_no_patch(id) ? 0 : SCE_KERNEL_ERROR_ESRCH; } -[[noreturn]] void APS5_VABI _ZSt16_Throw_Cpp_errori_nid_postfix(int code) { - ThrowCppError_nid_no_patch(code); -} -int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* code) { - void* result = nullptr; - if (pthread_join_nid_postfix(thread, &result) != 0) return ThrdError; - if (code) *code = static_cast(reinterpret_cast(result)); - return ThrdSuccess; -} - -void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad) { - if (!pad) APS5_INVALID_ARG_EX; - RequireThrd(_Mtx_lock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_lock"); - pad->started = true; - RequireThrd(_Cnd_signal_nid_postfix(&pad->condition), "_Pad::_Release: _Cnd_signal"); - RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_unlock"); -} - -void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad) { - if (!pad) APS5_INVALID_ARG_EX; - RequireThrd(_Cnd_init_nid_postfix(&pad->condition), "_Pad::_Pad: _Cnd_init"); - const int created = _Mtx_init_nid_postfix(&pad->mutex, MtxPlain); - if (created != ThrdSuccess) { - _Cnd_destroy_nid_postfix(&pad->condition); - RequireThrd(created, "_Pad::_Pad: _Mtx_init"); - } - pad->started = false; - const int locked = _Mtx_lock_nid_postfix(&pad->mutex); - if (locked != ThrdSuccess) { - _Mtx_destroy_nid_postfix(&pad->mutex); - _Cnd_destroy_nid_postfix(&pad->condition); - RequireThrd(locked, "_Pad::_Pad: _Mtx_lock"); - } -} - -void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad) { - if (!pad) APS5_INVALID_ARG_EX; - RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::~_Pad: _Mtx_unlock"); - _Mtx_destroy_nid_postfix(&pad->mutex); - _Cnd_destroy_nid_postfix(&pad->condition); -} - -int APS5_VABI _Thrd_id_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, Pthread* thread) { - if (!pad || !thread) APS5_INVALID_ARG_EX; - const int created = pthread_create_nid_postfix(thread, nullptr, PadEntry_nid_no_patch, pad); - if (created != 0) ThrowCppError_nid_no_patch(CppResourceUnavailableTryAgain); - while (!pad->started) RequireThrd(_Cnd_wait_nid_postfix(&pad->condition, &pad->mutex), "_Pad::_Launch: _Cnd_wait"); -} -int APS5_VABI _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - void APS5_VABI _Unlock_shared_ptr_spin_lock_nid_postfix(void) { sharedPtrSpinLock.clear(std::memory_order_release); } @@ -964,21 +853,11 @@ int APS5_VABI _Cnd_init_with_name_nid_postfix(void** handle, const char*) { return _Cnd_init_nid_postfix(handle); } -int APS5_VABI _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZNKSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE3getES3_S3_RSt8ios_baseRNSt5_IosbIiE8_IostateEP2tmPKcSE__nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -989,14 +868,8 @@ void APS5_VABI _Lock_shared_ptr_spin_lock_nid_postfix(void) { } -int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit) { - std::array state{}; - return wcsrtombs_s_nid_postfix(result, destination, capacity, &source, limit, state.data()); -} - -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); @@ -1044,6 +917,11 @@ void APS5_VABI sceAmprIgnoreHandlerS(const char* message, void* pointer, int err SonyIgnoreHandlerS(message, pointer, error); } +APS5_EXPORT("sZLrjx-yEx4", sceAmprWcstombsS); +int APS5_VABI sceAmprWcstombsS(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit) { + return wcstombs_s_nid_postfix(result, destination, capacity, source, limit); +} + APS5_EXPORT("VUzjXknPPBs", sceAmprMbstowcs); std::size_t APS5_VABI sceAmprMbstowcs(std::uint16_t* dest, const char* src, std::size_t size) { return SonyMbstowcs(dest, src, size); diff --git a/core/libs/prx/libSceAppChecker/Export.cpp b/core/libs/prx/libSceAppChecker/Export.cpp new file mode 100644 index 0000000000..b50753644e --- /dev/null +++ b/core/libs/prx/libSceAppChecker/Export.cpp @@ -0,0 +1,23 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAppCheckerAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppCheckerExecute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppCheckerExecuteEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index c3b44b3d75..6854c950da 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -2,14 +2,18 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/RemoveTree.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include +#include #include #include #include #include #include +static constexpr int SCE_APP_CONTENT_ERROR_NOT_INITIALIZED = static_cast(0x80D90001); static constexpr int SCE_APP_CONTENT_ERROR_PARAMETER = static_cast(0x80D90002); static constexpr int SCE_APP_CONTENT_ERROR_BUSY = static_cast(0x80D90003); static constexpr int SCE_APP_CONTENT_ERROR_NOT_MOUNTED = static_cast(0x80D90004); @@ -29,6 +33,20 @@ struct AppContentAddcontInfo { namespace { +std::atomic_bool initialized{false}; +std::mutex initializationMutex; + +std::uint32_t compiledSdkVersion() { + const auto* process = static_cast(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t version; + std::memcpy(&version, process + 16, sizeof(version)); + return version; +} + +bool validEntitlementLabel(const NpUnifiedEntitlementLabel& label, std::uint32_t minimumSdkVersion) { + return compiledSdkVersion() < minimumSdkVersion || std::all_of(std::begin(label.padding), std::end(label.padding), [](char value) { return value == 0; }); +} + struct TemporaryData { std::mutex mutex; const std::filesystem::path directory = ResolvePath_nid_no_patch(TEMPORARY_MOUNT_POINT); @@ -49,37 +67,44 @@ bool IsTemporaryMountPoint(const AppContentMountPoint* mountPoint) { } static void ClearDirectory(const std::filesystem::path& directory) { - for (const auto& entry : std::filesystem::directory_iterator(directory)) std::filesystem::remove_all(entry.path()); + for (const auto& entry : std::filesystem::directory_iterator(directory)) RemoveTree(entry.path()); RecordWrittenPath_nid_no_patch(directory); } extern "C" { int APS5_VABI sceAppContentAddcontMount(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, AppContentMountPoint* mount_point) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; (void)service_label; if (!entitlement_label || !mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlement_label, 0x01500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_NOT_FOUND; } int APS5_VABI sceAppContentAddcontUnmount(const AppContentMountPoint* mount_point) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; if (!mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_NOT_FOUND; } int APS5_VABI sceAppContentGetAddcontInfo(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, AppContentAddcontInfo* info) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; (void)service_label; if (!entitlement_label || !info) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlement_label, 0x01500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_DRM_NO_ENTITLEMENT; } -int APS5_VABI sceAppContentGetAddcontInfoList(uint32_t service_label, AppContentAddcontInfo* list, uint32_t list_num, uint32_t* hit_num) { - (void)service_label; - if ((!list || list_num == 0) && !hit_num) return SCE_APP_CONTENT_ERROR_PARAMETER; - if (hit_num) *hit_num = 0; +int APS5_VABI sceAppContentGetAddcontInfoList(uint32_t serviceLabel, AppContentAddcontInfo* list, uint32_t listNum, uint32_t* hitNum) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + (void)serviceLabel; + if (!hitNum) return SCE_APP_CONTENT_ERROR_PARAMETER; + *hitNum = 0; return 0; } int APS5_VABI sceAppContentAppParamGetInt(uint32_t param_id, int32_t* value) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; if (!value) return SCE_APP_CONTENT_ERROR_PARAMETER; switch (param_id) { case APPPARAM_ID_SKU_FLAG: @@ -94,20 +119,22 @@ int APS5_VABI sceAppContentAppParamGetInt(uint32_t param_id, int32_t* value) { } int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint* mount_point) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; if (!mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER; - if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; + if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED; const auto directory = ResolvePath_nid_no_patch(DOWNLOAD_MOUNT_POINT); if (std::filesystem::create_directories(directory)) RecordWrittenPath_nid_no_patch(directory); ClearDirectory(directory); return 0; } -int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint* mount_point, size_t* available_space_kb) { - if (!available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER; - if (!mount_point || std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; +int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint* mountPoint, size_t* availableSpaceKb) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + if (!mountPoint || !availableSpaceKb) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (std::strncmp(mountPoint->data, DOWNLOAD_MOUNT_POINT, sizeof(mountPoint->data)) != 0) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED; const std::uint64_t quotaKb = GetAppDownloadDataSizeMiB_nid_postfix() * 1024u; if (quotaKb == 0) { - *available_space_kb = 0; + *availableSpaceKb = 0; return 0; } const auto directory = ResolvePath_nid_no_patch(DOWNLOAD_MOUNT_POINT); @@ -117,19 +144,27 @@ int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMount if (entry.is_regular_file()) usedKb += (entry.file_size() + 1023u) / 1024u; } const std::uint64_t hostKb = std::filesystem::space(directory).available / 1024u; - *available_space_kb = static_cast(std::min(quotaKb - std::min(quotaKb, usedKb), hostKb)); + *availableSpaceKb = static_cast(std::min(quotaKb - std::min(quotaKb, usedKb), hostKb)); return 0; } -int APS5_VABI sceAppContentInitialize(const AppContentInitParam* init_param, AppContentBootParam* boot_param) { - (void)init_param; - if (!boot_param) return SCE_APP_CONTENT_ERROR_PARAMETER; +int APS5_VABI sceAppContentInitialize(const AppContentInitParam* initParam, AppContentBootParam* bootParam) { + std::lock_guard lock(initializationMutex); + const auto sdkVersion = compiledSdkVersion(); + if (sdkVersion >= 0x01500000) { + if (initialized.load()) return SCE_APP_CONTENT_ERROR_BUSY; + if (initParam == nullptr || bootParam == nullptr) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!std::all_of(std::begin(initParam->reserved), std::end(initParam->reserved), [](char value) { return value == 0; }) || !std::all_of(std::begin(bootParam->reserved1), std::end(bootParam->reserved1), [](char value) { return value == 0; }) || !std::all_of(std::begin(bootParam->reserved2), std::end(bootParam->reserved2), [](char value) { return value == 0; })) return SCE_APP_CONTENT_ERROR_PARAMETER; + } + if (bootParam == nullptr) return SCE_APP_CONTENT_ERROR_PARAMETER; Temporary(); - std::memset(boot_param, 0, sizeof(*boot_param)); + std::memset(bootParam, 0, sizeof(*bootParam)); + initialized = true; return 0; } int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint* mountPoint) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; if (!mountPoint) return SCE_APP_CONTENT_ERROR_PARAMETER; auto& state = Temporary(); std::lock_guard lock(state.mutex); @@ -139,6 +174,7 @@ int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint* mount } int APS5_VABI sceAppContentTemporaryDataGetAvailableSpaceKb(const AppContentMountPoint* mountPoint, size_t* availableSpaceKb) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; if (!mountPoint || !availableSpaceKb) return SCE_APP_CONTENT_ERROR_PARAMETER; auto& state = Temporary(); std::lock_guard lock(state.mutex); @@ -148,6 +184,7 @@ int APS5_VABI sceAppContentTemporaryDataGetAvailableSpaceKb(const AppContentMoun } int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t option, AppContentMountPoint* mountPoint) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; if (!mountPoint || option > TEMPORARY_DATA_OPTION_FORMAT) return SCE_APP_CONTENT_ERROR_PARAMETER; auto& state = Temporary(); std::lock_guard lock(state.mutex); @@ -162,6 +199,7 @@ int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t option, AppContentMountP } int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint* mountPoint) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; if (!mountPoint) return SCE_APP_CONTENT_ERROR_PARAMETER; auto& state = Temporary(); std::lock_guard lock(state.mutex); @@ -173,19 +211,110 @@ int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint* moun int APS5_VABI sceAppContentAddcontEnqueueDownload(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; (void)service_label; if (!entitlement_label) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlement_label, 0x01500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_NOT_FOUND; } int APS5_VABI sceAppContentAddcontDelete(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; (void)service_label; if (!entitlement_label) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlement_label, 0x03500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_NOT_FOUND; } -int APS5_VABI sceAppContentGetAddcontDownloadProgress(void) { +int APS5_VABI sceAppContentGetAddcontDownloadProgress(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, void* progress) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + (void)service_label; + if (!entitlement_label || !progress) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlement_label, 0x03500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_NOT_FOUND; +} +int APS5_VABI sceAppContentAddcontEnqueueDownloadByEntitlemetId(void) { NotImplemented_nid_no_patch(__func__); return 0; } + +int APS5_VABI sceAppContentAddcontEnqueueDownloadSp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentAddcontMountByEntitlemetId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentAddcontShrink(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentAppParamGetString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentDownload0Expand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentDownload0Shrink(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentDownload1Expand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentDownload1Shrink(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentGetAddcontInfoByEntitlementId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentGetAddcontInfoListByIroTag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentGetDownloadedStoreCountry(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentGetEntitlementKey(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentGetPftFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentGetRegion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentRequestPatchInstall(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentTemporaryDataMount(AppContentMountPoint* mountPoint) { + return sceAppContentTemporaryDataMount2(TEMPORARY_DATA_OPTION_FORMAT, mountPoint); +} + } diff --git a/core/libs/prx/libSceAppDbShellCoreClient/Export.cpp b/core/libs/prx/libSceAppDbShellCoreClient/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAppDbShellCoreClient/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAppDbShellCoreClientForSystemService/Export.cpp b/core/libs/prx/libSceAppDbShellCoreClientForSystemService/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAppDbShellCoreClientForSystemService/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAppInstUtil/Export.cpp b/core/libs/prx/libSceAppInstUtil/Export.cpp new file mode 100644 index 0000000000..5925f41ddd --- /dev/null +++ b/core/libs/prx/libSceAppInstUtil/Export.cpp @@ -0,0 +1,87 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +struct AppInstallProgressInfo; + +extern "C" { + +int APS5_VABI sceAppInstUtilInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppInstallAll(void* reserved) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppInstallTitleDir(const char* titleId, const char* titleDir, void* reserved) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppIsInInstalling(int32_t* installing) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppIsInUpdating(const char* titleId, bool* updating) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppExists(const char* titleId, int32_t* exists) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppGetSize(const char* titleId, std::uint64_t* size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppInstallPkg(const char* packagePath, std::uint64_t serviceArgument) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppPrepareOverwritePkg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppRecoverApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppUnInstall(const char* titleId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppUnInstallAddcont(const char* titleId, const char* contentLabel) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppUnInstallPat(const char* titleId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppUnInstallTheme() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilAppUnInstallTypes(const char* titleId, std::uint32_t types) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilGetInstallProgressInfo(const char* titleId, AppInstallProgressInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilGetInstallProgress(const char* titleId, void* progress) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAppInstUtilGetPrimaryAppSlot(const char* titleId, void* slot) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAsyncStorageInternal/Export.cpp b/core/libs/prx/libSceAsyncStorageInternal/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAsyncStorageInternal/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAt9Enc/CMakeLists.txt b/core/libs/prx/libSceAt9Enc/CMakeLists.txt new file mode 100644 index 0000000000..89880a7b92 --- /dev/null +++ b/core/libs/prx/libSceAt9Enc/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceAt9Enc SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceAt9Enc PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceAt9Enc) + +if(BUILD_TESTING) + add_test_executable(guest_at9_enc_tests ${LIBS_INCLUDE_DIR}/tests/GuestAt9Enc.cpp) + target_include_directories(guest_at9_enc_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_at9_enc_tests PRIVATE libSceAt9Enc libc) + configure_windows_unwind(guest_at9_enc_tests) + add_test(NAME guest_at9_enc COMMAND guest_at9_enc_tests) + if(WIN32) + set_tests_properties(guest_at9_enc PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAt9Enc/Export.cpp b/core/libs/prx/libSceAt9Enc/Export.cpp new file mode 100644 index 0000000000..77fee077ec --- /dev/null +++ b/core/libs/prx/libSceAt9Enc/Export.cpp @@ -0,0 +1,34 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + + +extern "C" { + +int32_t APS5_VABI sceAt9EncClearContext(void *pAt9EncWorkArea, int32_t *pInternalResult) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceAt9EncCreateEncoder(void *pAt9EncWorkArea, void *pAt9EncParam, int32_t memSize, int32_t *pInternalResult) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceAt9EncEncode(void *pAt9EncWorkArea, const void *pInputBuf, void *pOutputBuf, uint32_t inputBufSize, uint32_t outputBufSize, uint32_t *pOutputDataByte, uint32_t *pUsedBufSize, int32_t *pInternalResult) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceAt9EncFlush(void *pAt9EncWorkArea, void *pOutputBuf, uint32_t outputBufSize, uint32_t *pOutputDataByte, int32_t *pInternalResult) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceAt9EncQueryMemSize(void *pAt9EncParam, int32_t *pMemSize, int32_t *pInternalResult) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAudio3d/CMakeLists.txt b/core/libs/prx/libSceAudio3d/CMakeLists.txt index 1ed8b159bb..2d4e024734 100644 --- a/core/libs/prx/libSceAudio3d/CMakeLists.txt +++ b/core/libs/prx/libSceAudio3d/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceAudio3d SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceAudio3d PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceAudio3d PRIVATE libSceAudioOut) configure_windows_unwind(libSceAudio3d) if(BUILD_TESTING) @@ -21,4 +22,23 @@ if(BUILD_TESTING) set_tests_properties(guest_audio3d_port PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_audio3d_speaker_array_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudio3dSpeakerArray.cpp) + target_include_directories(guest_audio3d_speaker_array_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_audio3d_speaker_array_tests PRIVATE libSceAudio3d libSceAudioOut libc) + configure_windows_unwind(guest_audio3d_speaker_array_tests) + add_test(NAME guest_audio3d_speaker_array COMMAND guest_audio3d_speaker_array_tests) + if(WIN32) + set_tests_properties(guest_audio3d_speaker_array PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_audio3d_mixer_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudio3dMixer.cpp) + target_include_directories(guest_audio3d_mixer_tests PRIVATE + ${LIBS_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_audio3d_mixer_tests PRIVATE libc) + configure_windows_unwind(guest_audio3d_mixer_tests) + add_test(NAME guest_audio3d_mixer COMMAND guest_audio3d_mixer_tests) + if(WIN32) + set_tests_properties(guest_audio3d_mixer PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index b366da3067..eed6da479e 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -1,21 +1,31 @@ -#include +#include #include #include #include #include +#include +#include #include #include #include #include +#include +#include "Mixer.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -#include "prx/libc/include/PreciseWait.hpp" + +extern "C" { +int APS5_VABI sceAudioOutOpen(int user_id, int type, int index, std::uint32_t len, std::uint32_t freq, std::uint32_t param); +int APS5_VABI sceAudioOutOutput(int handle, const void* ptr); +int APS5_VABI sceAudioOutClose(int handle); +} namespace { constexpr int AUDIO3D_ERROR_INVALID_PORT = static_cast(0x80EA0002); constexpr int AUDIO3D_ERROR_INVALID_OBJECT = static_cast(0x80EA0003); constexpr int AUDIO3D_ERROR_INVALID_PARAMETER = static_cast(0x80EA0004); +constexpr int AUDIO3D_ERROR_NOT_SUPPORTED = static_cast(0x80EA0008); constexpr int AUDIO3D_ERROR_OUT_OF_RESOURCES = static_cast(0x80EA0006); constexpr int AUDIO3D_ERROR_NOT_READY = static_cast(0x80EA0007); @@ -25,24 +35,44 @@ constexpr std::uint32_t AUDIO3D_OBJECT_INVALID = 0xFFFFFFFF; constexpr std::uint32_t AUDIO3D_RATE_48000 = 0; constexpr std::uint32_t AUDIO3D_SAMPLE_RATE = 48000; constexpr std::uint32_t AUDIO3D_BUFFER_ADVANCE_AND_PUSH = 2; +constexpr int AUDIO3D_AUDIO_OUT_PORT_TYPE = 126; +constexpr std::uint32_t AUDIO3D_AUDIO_OUT_FORMAT_STEREO = 1; constexpr std::uint32_t AUDIO3D_BLOCKING_ASYNC = 0; constexpr std::uint32_t AUDIO3D_BLOCKING_SYNC = 1; constexpr std::uint32_t AUDIO3D_ATTRIBUTE_RESET_STATE = 0x20000; constexpr std::uint32_t AUDIO3D_ATTRIBUTE_LATE_REVERB_LEVEL = 0x10001; constexpr std::uint32_t AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS = 0x10002; constexpr std::uint32_t AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE = 0x10003; +constexpr std::uint32_t AUDIO3D_FORMAT_S16 = 0; +constexpr std::uint32_t AUDIO3D_FORMAT_FLOAT = 1; +constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_BOTH = 0; +constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_HMU_ONLY = 1; +constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_TV_ONLY = 2; -using Clock = std::chrono::steady_clock; +struct Audio3dAudioOut { + std::size_t buffer_bytes = 0; + std::deque> pending; +}; + +struct Audio3dBed { + std::vector pcm; + std::uint32_t channels = 0; + std::uint32_t format = 0; + std::uint32_t samples = 0; +}; struct Audio3dPort { bool open = false; std::uint32_t queue_depth = 0; - Clock::duration frame_duration{}; - std::uint32_t advanced = 0; - std::deque playing; + std::uint32_t granularity = 0; + std::uint32_t buffer_mode = 0; std::uint32_t max_objects = 0; std::uint32_t next_object_id = 0; std::set objects; + std::deque beds; + std::deque> mixed; + int audio_out_handle = 0; + std::map audio_out_ports; }; std::mutex g_mutex; @@ -57,9 +87,39 @@ bool PortIsOpen(std::uint32_t port_id) { return port_id == AUDIO3D_PORT_ID && g_port.open; } -std::uint32_t QueueLevel(Clock::time_point now) { - while (!g_port.playing.empty() && g_port.playing.front() <= now) g_port.playing.pop_front(); - return g_port.advanced + static_cast(g_port.playing.size()); +std::uint32_t QueueLevel() { + return static_cast(g_port.mixed.size()); +} + +std::size_t AudioOutBufferBytes(std::uint32_t len, std::uint32_t param) { + static constexpr std::uint32_t channels[] = {1, 2, 8, 1, 2, 8, 8, 8}; + static constexpr std::uint32_t sample_size[] = {2, 2, 2, 4, 4, 4, 2, 4}; + const std::uint32_t format = param & 0xFF; + if (format >= std::size(channels)) return 0; + return static_cast(len) * channels[format] * sample_size[format]; +} + +int DrainAssociatedAudioOuts() { + while (true) { + int handle = 0; + std::vector frame; + { + std::lock_guard lock(g_mutex); + const auto entry = std::find_if(g_port.audio_out_ports.begin(), g_port.audio_out_ports.end(), + [](const auto& candidate) { return !candidate.second.pending.empty(); }); + if (entry == g_port.audio_out_ports.end()) return 0; + handle = entry->first; + frame = std::move(entry->second.pending.front()); + entry->second.pending.pop_front(); + } + const int result = sceAudioOutOutput(handle, frame.data()); + if (result < 0) { + std::lock_guard lock(g_mutex); + const auto entry = g_port.audio_out_ports.find(handle); + if (entry != g_port.audio_out_ports.end()) entry->second.pending.push_front(std::move(frame)); + return result; + } + } } } @@ -70,6 +130,7 @@ void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* p) { if (p == nullptr) APS5_INVALID_ARG_EX; constexpr Audio3dOpenParameters defaults{0x20, 256, 0, 512, 2, 2, 0, 0}; static_assert(offsetof(Audio3dOpenParameters, num_beds) == 0x20); + static_assert(sizeof(Audio3dOpenParameters) == 0x28); std::memcpy(p, &defaults, defaults.size_this); } @@ -91,15 +152,40 @@ int APS5_VABI sceAudio3dTerminate() { int APS5_VABI sceAudio3dPortAdvance(uint32_t port_id) { std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; - if (QueueLevel(Clock::now()) >= g_port.queue_depth) Unsupported(__func__, "queue is full"); - ++g_port.advanced; + if (g_port.buffer_mode != AUDIO3D_BUFFER_ADVANCE_AND_PUSH) return AUDIO3D_ERROR_NOT_SUPPORTED; + if (QueueLevel() >= g_port.queue_depth) return AUDIO3D_ERROR_NOT_READY; + std::vector frame; + if (g_port.beds.empty()) { + Audio3dInternal::MixFrame(nullptr, 0, g_port.granularity, frame); + } else { + const Audio3dBed& bed = g_port.beds.front(); + const Audio3dInternal::Source source{ + bed.pcm.data(), + bed.samples, + bed.channels, + bed.format == AUDIO3D_FORMAT_FLOAT, + 1.0f, + 0.0f, + }; + Audio3dInternal::MixFrame(&source, 1, g_port.granularity, frame); + g_port.beds.pop_front(); + } + g_port.mixed.push_back(std::move(frame)); return 0; } int APS5_VABI sceAudio3dPortClose(uint32_t port_id) { - std::lock_guard lock(g_mutex); - if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; - g_port = Audio3dPort{}; + int handle = 0; + std::vector associated; + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + handle = g_port.audio_out_handle; + for (const auto& entry : g_port.audio_out_ports) associated.push_back(entry.first); + g_port = Audio3dPort{}; + } + for (const int port : associated) sceAudioOutClose(port); + if (handle != 0) sceAudioOutClose(handle); return 0; } @@ -107,12 +193,26 @@ int APS5_VABI sceAudio3dPortGetQueueLevel(uint32_t port_id, uint32_t* queue_leve std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; if (queue_level == nullptr && queue_available == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; - const std::uint32_t level = QueueLevel(Clock::now()); + const std::uint32_t level = QueueLevel(); if (queue_level != nullptr) *queue_level = level; if (queue_available != nullptr) *queue_available = g_port.queue_depth - level; return 0; } +int APS5_VABI sceAudio3dPortGetAttributesSupported(uint32_t portId, uint32_t* attributes, uint32_t* numAttributes) { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(portId)) return AUDIO3D_ERROR_INVALID_PORT; + if (numAttributes == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; + constexpr std::uint32_t supported[]{ + AUDIO3D_ATTRIBUTE_LATE_REVERB_LEVEL, + AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS, + AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE, + }; + *numAttributes = std::size(supported); + if (attributes != nullptr) std::copy(std::begin(supported), std::end(supported), attributes); + 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; @@ -143,33 +243,53 @@ int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* param g_port = Audio3dPort{}; g_port.open = true; g_port.queue_depth = parameters->queue_depth; + g_port.granularity = parameters->granularity; + g_port.buffer_mode = parameters->buffer_mode; g_port.max_objects = parameters->max_objects; - g_port.frame_duration = std::chrono::duration_cast( - std::chrono::nanoseconds(std::uint64_t{parameters->granularity} * 1000000000u / AUDIO3D_SAMPLE_RATE)); *id = AUDIO3D_PORT_ID; return 0; } int APS5_VABI sceAudio3dPortPush(uint32_t port_id, uint32_t blocking) { - Clock::time_point wait_until; + int handle = 0; + std::uint32_t granularity = 0; { std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; if (blocking != AUDIO3D_BLOCKING_ASYNC && blocking != AUDIO3D_BLOCKING_SYNC) { Unsupported(__func__, "blocking mode " + std::to_string(blocking)); } - const Clock::time_point now = Clock::now(); - const std::uint32_t level = QueueLevel(now); - Clock::time_point end = g_port.playing.empty() ? now : g_port.playing.back(); - for (; g_port.advanced != 0; --g_port.advanced) { - end += g_port.frame_duration; - g_port.playing.push_back(end); - } - if (blocking == AUDIO3D_BLOCKING_ASYNC || level < g_port.queue_depth) return 0; - wait_until = g_port.playing[level - g_port.queue_depth]; + handle = g_port.audio_out_handle; + granularity = g_port.granularity; } - PreciseSleepUntil(wait_until); - return 0; + if (handle == 0) { + const int opened = sceAudioOutOpen(AUDIO3D_USER_ID_SYSTEM, AUDIO3D_AUDIO_OUT_PORT_TYPE, 0, + granularity, AUDIO3D_SAMPLE_RATE, AUDIO3D_AUDIO_OUT_FORMAT_STEREO); + if (opened < 0) return AUDIO3D_ERROR_OUT_OF_RESOURCES; + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) { + sceAudioOutClose(opened); + return AUDIO3D_ERROR_INVALID_PORT; + } + if (g_port.audio_out_handle == 0) g_port.audio_out_handle = opened; + else sceAudioOutClose(opened); + } + std::deque> frames; + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + handle = g_port.audio_out_handle; + const std::size_t target = blocking == AUDIO3D_BLOCKING_SYNC ? 0 : g_port.queue_depth - 1; + while (g_port.mixed.size() > target) { + frames.push_back(std::move(g_port.mixed.front())); + g_port.mixed.pop_front(); + } + } + for (const std::vector& frame : frames) { + const int result = sceAudioOutOutput(handle, frame.data()); + if (result < 0) return result; + } + return DrainAssociatedAudioOuts(); } int APS5_VABI sceAudio3dPortSetAttribute(uint32_t port_id, uint32_t attribute_id, const void* attribute, size_t attribute_size) { @@ -221,11 +341,173 @@ int APS5_VABI sceAudio3dObjectSetAttributes(uint32_t port_id, uint32_t object_id } int APS5_VABI sceAudio3dPortFlush(uint32_t port_id) { - std::lock_guard lock(g_mutex); - if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; - g_port.advanced = 0; - g_port.playing.clear(); + int handle = 0; + std::vector associated; + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + g_port.mixed.clear(); + handle = g_port.audio_out_handle; + for (auto& entry : g_port.audio_out_ports) { + entry.second.pending.clear(); + associated.push_back(entry.first); + } + } + for (const int port : associated) sceAudioOutOutput(port, nullptr); + if (handle != 0) sceAudioOutOutput(handle, nullptr); return 0; } +int APS5_VABI sceAudio3dBedWrite2(uint32_t port_id, uint32_t num_channels, uint32_t format, const void* buffer, uint32_t num_samples, uint32_t output_route, bool) { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (num_channels != 2 && num_channels != 6 && num_channels != 8) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (format > AUDIO3D_FORMAT_FLOAT) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (output_route != AUDIO3D_OUTPUT_ROUTE_BOTH && output_route != AUDIO3D_OUTPUT_ROUTE_HMU_ONLY && output_route != AUDIO3D_OUTPUT_ROUTE_TV_ONLY) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (buffer == nullptr || num_samples == 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + const std::size_t sample_size = format == AUDIO3D_FORMAT_S16 ? 2u : 4u; + if (reinterpret_cast(buffer) % sample_size != 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + const std::uint32_t samples = std::min(num_samples, g_port.granularity); + const std::size_t bytes = static_cast(samples) * num_channels * sample_size; + if (g_port.beds.size() >= g_port.queue_depth) return AUDIO3D_ERROR_NOT_READY; + const auto* first = static_cast(buffer); + Audio3dBed bed; + bed.pcm.assign(first, first + bytes); + bed.channels = num_channels; + bed.format = format; + bed.samples = samples; + g_port.beds.push_back(std::move(bed)); + return 0; +} + +int APS5_VABI sceAudio3dBedWrite(uint32_t port_id, uint32_t num_channels, uint32_t format, const void* buffer, uint32_t num_samples) { + return sceAudio3dBedWrite2(port_id, num_channels, format, buffer, num_samples, AUDIO3D_OUTPUT_ROUTE_BOTH, false); +} + +int APS5_VABI sceAudio3dAudioOutOpen(uint32_t port_id, int user_id, std::int32_t type, std::int32_t index, uint32_t len, uint32_t freq, uint32_t param) { + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (len != g_port.granularity) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (index != 0 || (param & 0xFF) > 7) return AUDIO3D_ERROR_INVALID_PARAMETER; + } + const int handle = sceAudioOutOpen(user_id, type, index, len, freq, param); + if (handle < 0) return AUDIO3D_ERROR_OUT_OF_RESOURCES; + bool registered = false; + { + std::lock_guard lock(g_mutex); + if (PortIsOpen(port_id)) { + g_port.audio_out_ports.emplace(handle, Audio3dAudioOut{AudioOutBufferBytes(len, param)}); + registered = true; + } + } + if (!registered) { + sceAudioOutClose(handle); + return AUDIO3D_ERROR_INVALID_PORT; + } + return handle; +} + +int APS5_VABI sceAudio3dAudioOutClose(std::int32_t handle) { + { + std::lock_guard lock(g_mutex); + if (g_port.audio_out_ports.erase(handle) == 0) return AUDIO3D_ERROR_INVALID_PORT; + } + sceAudioOutClose(handle); + return 0; +} + +int APS5_VABI sceAudio3dAudioOutOutput(std::int32_t handle, const void* ptr) { + std::lock_guard lock(g_mutex); + const auto port = g_port.audio_out_ports.find(handle); + if (port == g_port.audio_out_ports.end()) return AUDIO3D_ERROR_INVALID_PORT; + if (ptr == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (port->second.pending.size() >= g_port.queue_depth) return AUDIO3D_ERROR_NOT_READY; + const auto* first = static_cast(ptr); + port->second.pending.emplace_back(first, first + port->second.buffer_bytes); + return static_cast(g_port.granularity); +} + +int APS5_VABI sceAudio3dAudioOutOutputs(AudioOutOutputParam* param, uint32_t num) { + if (param == nullptr || num == 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + for (std::uint32_t i = 0; i < num; ++i) { + const int result = sceAudio3dAudioOutOutput(param[i].handle, param[i].ptr); + if (result < 0) return result; + } + return 0; +} + +int APS5_VABI sceAudio3dStrError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dPortOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dPortCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dPortDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dPortGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dReportRegisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dReportUnregisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +size_t APS5_VABI sceAudioOut2GetSpeakerArrayMemorySize(uint32_t num_speakers, uint8_t is_3d, uint8_t is_ambisonics); +int APS5_VABI sceAudioOut2SpeakerArrayCreate(AudioOut2SpeakerArrayHandle* handle, const void* vbap_params, const void* ambi_params); +int APS5_VABI sceAudioOut2SpeakerArrayDestroy(AudioOut2SpeakerArrayHandle handle); +int APS5_VABI sceAudioOut2GetSpeakerArrayCoefficients(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, uint32_t num_coefficients, uint8_t height_aware, float downmix_spread_radius); + +struct Audio3dSpeakerArrayParameters { + const AudioOut2Position* positions; + std::uint32_t numSpeakers; + std::uint8_t is3d; + void* memory; + std::size_t memorySize; +}; + +static_assert(sizeof(Audio3dSpeakerArrayParameters) == 0x20); + +size_t APS5_VABI sceAudio3dGetSpeakerArrayMemorySize(uint32_t num_speakers, uint8_t is_3d) { + return sceAudioOut2GetSpeakerArrayMemorySize(num_speakers, is_3d, 0); +} + +int APS5_VABI sceAudio3dCreateSpeakerArray(AudioOut2SpeakerArrayHandle* handle, const Audio3dSpeakerArrayParameters* parameters) { + if (!handle || !parameters) return AUDIO3D_ERROR_INVALID_PARAMETER; + unsigned char extended[0x28] = {}; + std::memcpy(extended, parameters, sizeof(*parameters)); + return sceAudioOut2SpeakerArrayCreate(handle, extended, nullptr); +} + +int APS5_VABI sceAudio3dDeleteSpeakerArray(AudioOut2SpeakerArrayHandle handle) { + return sceAudioOut2SpeakerArrayDestroy(handle); +} + +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, uint32_t num_coefficients) { + return sceAudioOut2GetSpeakerArrayCoefficients(handle, pos, spread, coefficients, num_coefficients, 0, 0.1f); +} + +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients2(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, uint32_t num_coefficients, uint8_t height_aware, float downmix_spread_radius) { + return sceAudioOut2GetSpeakerArrayCoefficients(handle, pos, spread, coefficients, num_coefficients, height_aware, downmix_spread_radius); +} + } diff --git a/core/libs/prx/libSceAudio3d/Mixer.hpp b/core/libs/prx/libSceAudio3d/Mixer.hpp new file mode 100644 index 0000000000..569d653e19 --- /dev/null +++ b/core/libs/prx/libSceAudio3d/Mixer.hpp @@ -0,0 +1,70 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAUDIO3D_MIXER_HPP +#define CORE_LIBS_PRX_LIBSCEAUDIO3D_MIXER_HPP + +#include +#include +#include +#include +#include + +namespace Audio3dInternal { + +struct Source { + const void* samples; + std::uint32_t num_samples; + std::uint32_t channels; + bool is_float; + float gain; + float pan; +}; + +constexpr float kSurroundFold = 0.7071f; +constexpr std::uint32_t kChannelL = 0; +constexpr std::uint32_t kChannelR = 1; +constexpr std::uint32_t kChannelCenter = 2; +[[maybe_unused]] constexpr std::uint32_t kChannelLfe = 3; +constexpr std::uint32_t kChannelLs = 4; +constexpr std::uint32_t kChannelRs = 5; +constexpr std::uint32_t kChannelLrs = 6; +constexpr std::uint32_t kChannelRrs = 7; + +inline float ReadSample(const Source& source, std::uint32_t frame, std::uint32_t channel) { + const std::size_t index = static_cast(frame) * source.channels + channel; + if (source.is_float) return static_cast(source.samples)[index]; + return static_cast(static_cast(source.samples)[index]) / 32768.0f; +} + +inline void MixFrame(const Source* sources, std::size_t count, std::uint32_t granularity, std::vector& out) { + std::vector mix(static_cast(granularity) * 2, 0.0f); + for (std::size_t source_index = 0; source_index < count; ++source_index) { + const Source& src = sources[source_index]; + if (src.samples == nullptr || src.num_samples == 0) continue; + const float pan = std::clamp(src.pan, -1.0f, 1.0f); + const float gl = (pan <= 0.0f ? 1.0f : 1.0f - pan) * src.gain; + const float gr = (pan >= 0.0f ? 1.0f : 1.0f + pan) * src.gain; + for (std::uint32_t frame = 0; frame < granularity && frame < src.num_samples; ++frame) { + const std::size_t left_index = static_cast(frame) * 2; + float left = ReadSample(src, frame, kChannelL); + float right = src.channels == 1 ? left : ReadSample(src, frame, kChannelR); + if (src.channels > kChannelCenter) { + const float center = ReadSample(src, frame, kChannelCenter) * kSurroundFold; + left += center; + right += center; + } + if (src.channels > kChannelLs) left += ReadSample(src, frame, kChannelLs) * kSurroundFold; + if (src.channels > kChannelRs) right += ReadSample(src, frame, kChannelRs) * kSurroundFold; + if (src.channels > kChannelLrs) left += ReadSample(src, frame, kChannelLrs) * kSurroundFold; + if (src.channels > kChannelRrs) right += ReadSample(src, frame, kChannelRrs) * kSurroundFold; + mix[left_index] += left * gl; + mix[left_index + 1] += right * gr; + } + } + out.resize(static_cast(granularity) * 2); + for (std::size_t i = 0; i < out.size(); ++i) { + out[i] = static_cast(std::lround(std::clamp(mix[i], -1.0f, 1.0f) * 32767.0f)); + } +} + +} + +#endif diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index b07ad21506..0a1712b5da 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace { @@ -16,12 +17,12 @@ constexpr int AUDIO_IN_ERROR_INVALID_HANDLE = static_cast(0x80260101); constexpr int AUDIO_IN_ERROR_INVALID_SIZE = static_cast(0x80260102); constexpr int AUDIO_IN_ERROR_INVALID_FREQ = static_cast(0x80260103); constexpr int AUDIO_IN_ERROR_INVALID_TYPE = static_cast(0x80260104); +constexpr int AUDIO_IN_ERROR_INVALID_POINTER = static_cast(0x80260105); constexpr int AUDIO_IN_ERROR_INVALID_PARAM = static_cast(0x80260106); constexpr int AUDIO_IN_ERROR_PORT_FULL = static_cast(0x80260107); constexpr int AUDIO_IN_ERROR_BUSY = static_cast(0x8026010A); constexpr int AUDIO_IN_SILENT_STATE_DEVICE_NONE = 1; constexpr std::uint32_t MAX_QUEUED_BLOCKS = 4; -constexpr std::uint32_t MAX_ASYNC_LEN = 384; using Clock = std::chrono::steady_clock; @@ -48,6 +49,7 @@ bool formatOf(std::uint32_t param, Format& format) { switch (param) { case 1: format = {AUDIO_S16SYS, 1}; return true; case 2: format = {AUDIO_S16SYS, 2}; return true; + case 0x10: case 0x11: format = {AUDIO_F32SYS, 1}; return true; case 0x12: format = {AUDIO_F32SYS, 2}; return true; default: return false; @@ -84,11 +86,7 @@ int checkArguments(std::uint32_t type, std::uint32_t index, std::uint32_t len, s return 0; } -int openPort(std::uint32_t len, std::uint32_t freq, std::uint32_t param, const char* function) { - if (param == 0x10) { - NotImplemented_nid_no_patch(function); - return 0; - } +int openPort(std::uint32_t len, std::uint32_t freq, std::uint32_t param) { Format format{}; if (!formatOf(param, format)) return AUDIO_IN_ERROR_INVALID_PARAM; std::lock_guard lock(g_mutex); @@ -164,7 +162,7 @@ int APS5_VABI sceAudioInInput(int handle, void* dest) { int APS5_VABI sceAudioInOpen(int user_id, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { (void)user_id; if (const int error = checkArguments(type, index, len, freq); error != 0) return error; - return openPort(len, freq, param, __func__); + return openPort(len, freq, param); } int32_t APS5_VABI sceAudioInClose(int32_t handle) { @@ -177,31 +175,195 @@ int32_t APS5_VABI sceAudioInClose(int32_t handle) { return 0; } -int32_t APS5_VABI sceAudioInHqOpen(int32_t user_id, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { - (void)user_id; +int32_t APS5_VABI sceAudioInHqOpen(int32_t userId, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { + if (freq != 48000) return AUDIO_IN_ERROR_INVALID_FREQ; + if (len != 128) return AUDIO_IN_ERROR_INVALID_SIZE; + if (param != 2) return AUDIO_IN_ERROR_INVALID_PARAM; if (const int error = checkArguments(type, index, len, freq); error != 0) return error; - if (len != 128 || freq != 48000) { - NotImplemented_nid_no_patch(__func__); - return 0; - } - return openPort(len, freq, param, __func__); + return openPort(len, freq, param); } -int32_t APS5_VABI sceAudioInAsyncOpen(int32_t user_id, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { - (void)user_id; - const int error = checkArguments(type, index, len, freq); - if (error == AUDIO_IN_ERROR_INVALID_SIZE && len <= MAX_ASYNC_LEN) { - NotImplemented_nid_no_patch(__func__); - return 0; - } - if (error != 0) return error; - return openPort(len, freq, param, __func__); +int32_t APS5_VABI sceAudioInAsyncOpen(int32_t userId, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { + if (freq != 48000) return AUDIO_IN_ERROR_INVALID_FREQ; + if (len != 128) return AUDIO_IN_ERROR_INVALID_SIZE; + if (const int error = checkArguments(type, index, len, freq); error != 0) return error; + return openPort(len, freq, param); } int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t handle) { - (void)handle; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!find(handle)) return AUDIO_IN_ERROR_INVALID_HANDLE; + return 0; +} + +int32_t APS5_VABI sceAudioInGetGain(int32_t handle, float* gain) { + if (!gain) return AUDIO_IN_ERROR_INVALID_POINTER; + *gain = 1.0f; + std::lock_guard lock(g_mutex); + return find(handle) ? 0 : AUDIO_IN_ERROR_INVALID_HANDLE; +} + +int32_t APS5_VABI sceAudioInGetRerouteCount(int32_t handle) { + std::lock_guard lock(g_mutex); + return find(handle) ? 0 : AUDIO_IN_ERROR_INVALID_HANDLE; +} + +int APS5_VABI sceAudioInDeviceIdOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInDeviceIdAsyncOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetUsbGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInIsSharedDevice(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInDeviceIdHqOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInDeviceHqOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetPortConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetPortStatuses(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInGetHandleStatusInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInOpenEx(int user_id, uint32_t type, uint32_t index, uint32_t extra, uint32_t len, uint32_t freq, uint32_t param) { + (void)extra; + return sceAudioInOpen(user_id, type, index, len, freq, param); +} + +int APS5_VABI sceAudioInHqOpenEx(int user_id, uint32_t type, uint32_t index, uint32_t extra, uint32_t len, uint32_t freq, uint32_t param) { + (void)extra; + return sceAudioInOpen(user_id, type, index, len, freq, param); +} + +int APS5_VABI sceAudioInDeviceOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInChangeAppModuleState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetSparkParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetSparkSideTone(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetCompressorPreGain(void) { + return 0; +} + +int APS5_VABI sceAudioInSetNoiseGateThreshold(void) { + return 0; +} + +int APS5_VABI sceAudioInSetMicLevel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetDeviceMuteStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetDevConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInCountPorts(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetUserMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicWrite(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicSetMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetMode2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInExtCtrl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInInputs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; } } diff --git a/core/libs/prx/libSceAudioLatencyEstimation/Export.cpp b/core/libs/prx/libSceAudioLatencyEstimation/Export.cpp new file mode 100644 index 0000000000..71b2edeba4 --- /dev/null +++ b/core/libs/prx/libSceAudioLatencyEstimation/Export.cpp @@ -0,0 +1,28 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAudioLatencyEstimationAddData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioLatencyEstimationGetDelayTime() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioLatencyEstimationInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioLatencyEstimationRelease() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index acc471f504..d13571aad4 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -1,23 +1,100 @@ add_library(libSceAudioOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOutSdl.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOutShutdown.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Context.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Low.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2PadMix.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Port.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2SpeakerArray.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2User.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Mastering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2System.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Unimplemented.cpp ) target_include_directories(libSceAudioOut PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR} ) -target_link_libraries(libSceAudioOut PRIVATE libc libkernel SDL2-static) +target_link_libraries(libSceAudioOut PRIVATE libc libkernel libScePad SDL2-static) configure_windows_unwind(libSceAudioOut) if(BUILD_TESTING) + add_test_executable(audio_out_vibration_timeline_tests tests/AudioOutVibrationTimeline.cpp) + target_include_directories(audio_out_vibration_timeline_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_vibration_timeline_tests PRIVATE libc) + configure_windows_unwind(audio_out_vibration_timeline_tests) + add_test(NAME audio_out_vibration_timeline COMMAND audio_out_vibration_timeline_tests) + set_tests_properties(audio_out_vibration_timeline PROPERTIES TIMEOUT 5) + if(WIN32) + set_tests_properties(audio_out_vibration_timeline PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(audio_out_vibration_device_tests tests/AudioOutVibrationDevice.cpp src/AudioOut.cpp src/AudioOut2PadMix.cpp src/AudioOutShutdown.cpp) + target_compile_definitions(audio_out_vibration_device_tests PRIVATE SDL_MAIN_HANDLED) + target_include_directories(audio_out_vibration_device_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(audio_out_vibration_device_tests PRIVATE libc libkernel libScePad) + configure_windows_unwind(audio_out_vibration_device_tests) + foreach(vibrationDeviceMode IN ITEMS quad pulse open_failure timing cadence discovery_failure) + set(vibrationDeviceTest "audio_out_vibration_device_${vibrationDeviceMode}") + add_test(NAME ${vibrationDeviceTest} COMMAND audio_out_vibration_device_tests ${vibrationDeviceMode}) + set_tests_properties(${vibrationDeviceTest} PROPERTIES TIMEOUT 5) + if(WIN32) + set_tests_properties(${vibrationDeviceTest} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + + add_test_executable(audio_out_buffer_size_tests tests/AudioOutBufferSize.cpp src/AudioOut.cpp src/AudioOut2PadMix.cpp src/AudioOutShutdown.cpp) + target_compile_definitions(audio_out_buffer_size_tests PRIVATE SDL_MAIN_HANDLED) + target_include_directories(audio_out_buffer_size_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(audio_out_buffer_size_tests PRIVATE libc libkernel libScePad) + configure_windows_unwind(audio_out_buffer_size_tests) + foreach(bufferSizeMode IN ITEMS pass_through wrap mix threshold) + set(bufferSizeTest "audio_out_buffer_size_${bufferSizeMode}") + add_test(NAME ${bufferSizeTest} COMMAND audio_out_buffer_size_tests ${bufferSizeMode}) + set_tests_properties(${bufferSizeTest} PROPERTIES TIMEOUT 5) + if(WIN32) + set_tests_properties(${bufferSizeTest} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + + add_test_executable(audio_out_vibration_tests tests/AudioOutVibration.cpp) + target_include_directories(audio_out_vibration_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_vibration_tests PRIVATE libSceAudioOut libScePad libc) + configure_windows_unwind(audio_out_vibration_tests) + foreach(vibrationMode IN ITEMS s16 f32 batch_s16 batch_f32 mono_s16 mono_f32 gain_s16 gain_f32 muted silence drain ex pad_control) + set(vibrationTest "audio_out_vibration_${vibrationMode}") + add_test(NAME ${vibrationTest} COMMAND audio_out_vibration_tests --${vibrationMode}) + set_tests_properties(${vibrationTest} PROPERTIES TIMEOUT 5 ENVIRONMENT "SDL_AUDIODRIVER=dummy") + if(WIN32) + set_tests_properties(${vibrationTest} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + + 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) + set_target_properties(audio_out_resampling_tests PROPERTIES ENABLE_EXPORTS ON) + add_test(NAME audio_out_resampling COMMAND audio_out_resampling_tests) + set_tests_properties(audio_out_resampling PROPERTIES TIMEOUT 10) + + endif() + add_test_executable(audio_out_initialization_tests tests/AudioOutInitialization.cpp src/AudioOutSdl.cpp) target_include_directories(audio_out_initialization_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) target_link_libraries(audio_out_initialization_tests PRIVATE libc) @@ -69,6 +146,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out_sys_tests tests/AudioOutSys.cpp) + target_include_directories(audio_out_sys_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_sys_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out_sys_tests) + add_test(NAME audio_out_sys COMMAND audio_out_sys_tests) + if(WIN32) + set_tests_properties(audio_out_sys PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(audio_out_sys_hdmi_monitor_info_tests tests/AudioOutSysHdmiMonitorInfo.cpp) target_include_directories(audio_out_sys_hdmi_monitor_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out_sys_hdmi_monitor_info_tests PRIVATE libSceAudioOut libc) @@ -117,6 +204,15 @@ if(BUILD_TESTING) set_tests_properties(audio_out2_port_buffers PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out2_device_state_tests tests/AudioOut2DeviceState.cpp) + target_include_directories(audio_out2_device_state_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out2_device_state_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out2_device_state_tests) + add_test(NAME audio_out2_device_state COMMAND audio_out2_device_state_tests) + if(WIN32) + set_tests_properties(audio_out2_device_state PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(audio_out2_timing_tests tests/AudioOut2Timing.cpp) target_include_directories(audio_out2_timing_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out2_timing_tests PRIVATE libSceAudioOut libc) @@ -126,4 +222,17 @@ if(BUILD_TESTING) set_tests_properties(audio_out2_timing PROPERTIES RUN_SERIAL TRUE ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out_shutdown_tests tests/AudioOutShutdown.cpp) + target_include_directories(audio_out_shutdown_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_shutdown_tests PRIVATE libSceAudioOut libkernel libc) + configure_windows_unwind(audio_out_shutdown_tests) + add_test(NAME audio_out2_shutdown COMMAND audio_out_shutdown_tests --audio-out2) + add_test(NAME audio_out2_waiting_push_shutdown COMMAND audio_out_shutdown_tests --audio-out2-waiting-push) + add_test(NAME audio_out2_advance_shutdown COMMAND audio_out_shutdown_tests --audio-out2-advance) + add_test(NAME audio_out_shutdown COMMAND audio_out_shutdown_tests --audio-out) + set_tests_properties(audio_out2_shutdown audio_out2_waiting_push_shutdown audio_out2_advance_shutdown audio_out_shutdown PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(audio_out2_shutdown audio_out2_waiting_push_shutdown audio_out2_advance_shutdown audio_out_shutdown PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index fcd7d359f8..9faeaef957 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 @@ -15,14 +13,20 @@ #include "SDL.h" #include "SceTypes.hpp" #include "prx/libc/include/PreciseWait.hpp" +#include "prx/libc/include/Shutdown.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include "prx/libSceAudioOut/include/AudioOutSdl.hpp" +#include "AudioOutShutdown.hpp" +#include "AudioOut2PadMix.hpp" +#include "prx/libScePad/include/PadState.hpp" static constexpr int PORT_TYPE_MAIN = 0; static constexpr int PORT_TYPE_BGM = 1; static constexpr int PORT_TYPE_VOICE = 2; static constexpr int PORT_TYPE_PERSONAL = 3; static constexpr int PORT_TYPE_PADSPK = 4; +static constexpr int PORT_TYPE_EX = 6; +static constexpr int PORT_TYPE_SYS = 7; static constexpr int PORT_TYPE_VIBRATION = 10; static constexpr int PORT_TYPE_AUDIO3D = 126; static constexpr int PORT_TYPE_AUX = 127; @@ -36,7 +40,18 @@ static constexpr std::uint64_t DRAIN_TIMEOUT_US = 200000; static constexpr std::uint64_t DRAIN_SLEEP_US = 1000; static constexpr int MONITOR_TYPE_HDMI = 1; static constexpr std::uint32_t HDMI_MONITOR_INFO_SIZE = 0x180; -static constexpr std::uint32_t EX_MONITOR_INFO_MIN_SIZE = 0xA0; +static constexpr std::uint32_t EX_MONITOR_INFO_SIZE = 0x400; +static constexpr std::uint32_t EX_OPEN_MODE_MAX = 10; +static constexpr std::uint32_t EX_OPEN_MODES = 0x61F; +static constexpr std::uint32_t EX_CONFIGURE_MODE_DEFAULT = 0xFF; +static constexpr std::uint32_t EX_CONFIGURE_TARGET_DEFAULT = 0xFF; +static constexpr int ERROR_EX_INVALID_MODE = -2144993259; +static constexpr int ERROR_EX_INVALID_CONTROL = -2144960503; +static constexpr std::uint32_t SYS_OPEN_MODE_TRUEHD = 5; +static constexpr std::uint32_t SYS_CONFIGURE_TYPE_HDMI = 1; +static constexpr std::uint32_t SYS_CONFIGURE_TARGET_HDMI = 1; +static constexpr std::uint32_t SYS_PORT_SAMPLES = 1024; +static constexpr std::uint32_t SYS_PORT_FRAME_RATE = 192000; enum class Format { Unknown, @@ -85,6 +100,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; @@ -96,12 +119,13 @@ struct Port { int mixLevel = DEFAULT_VOLUME; std::uint64_t lastDataOutputTime = 0; std::uint64_t virtualQueueEnd = 0; + AudioOut2PadLayout padLayout; SDL_AudioDeviceID device = 0; SDL_AudioSpec spec = {}; }; 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; @@ -127,7 +151,7 @@ static bool openDevice(Port& port) { desired.samples = static_cast(port.samplesNum); desired.callback = nullptr; SDL_AudioSpec obtained{}; - port.device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, SDL_AUDIO_ALLOW_ANY_CHANGE); + port.device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, 0); if (port.device == 0) { return false; } @@ -136,6 +160,30 @@ static bool openDevice(Port& port) { return true; } +static bool openVibrationDevice(Port& port) { + if (!ensureSdlAudio()) throw std::runtime_error(std::string("AudioOut: could not initialize controller audio: ") + SDL_GetError()); + const int count = SDL_GetNumAudioDevices(0); + if (count < 0) throw std::runtime_error(std::string("AudioOut: could not enumerate controller sound cards: ") + SDL_GetError()); + for (int index = 0; index < count; ++index) { + const char* name = SDL_GetAudioDeviceName(index, 0); + if (!AudioOut2IsPadAudioDevice(name)) continue; + const std::string controllerName(name); + SDL_AudioSpec native{}; + if (SDL_GetAudioDeviceSpec(index, 0, &native) == 0 && native.channels != 0 && native.channels != AUDIO_OUT2_PAD_CHANNELS) continue; + port.padLayout = AudioOut2PadLayoutForDriver(SDL_GetCurrentAudioDriver()); + SDL_AudioSpec desired{}; + desired.freq = static_cast(port.freq); + desired.format = AUDIO_F32SYS; + desired.channels = static_cast(port.padLayout.channels); + desired.samples = static_cast(port.samplesNum); + port.device = SDL_OpenAudioDevice(controllerName.c_str(), 0, &desired, &port.spec, 0); + if (port.device == 0) throw std::runtime_error(std::string("AudioOut: could not open the controller sound card: ") + SDL_GetError()); + SDL_PauseAudioDevice(port.device, 0); + return true; + } + return false; +} + static void closeDevice(Port& port) { if (port.device != 0 && SDL_WasInit(SDL_INIT_AUDIO) != 0) { SDL_ClearQueuedAudio(port.device); @@ -151,7 +199,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++) { @@ -197,6 +245,23 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector return buf.data(); } +static std::vector prepareVibration(const Port& port, const void* data) { + std::vector prepared; + const auto* samples = prepareBuffer(port, data, prepared); + std::vector pad(static_cast(port.samplesNum) * AUDIO_OUT2_PAD_CHANNELS, 0.0f); + constexpr float unity[2] = {1.0f, 1.0f}; + for (std::uint32_t frame = 0; frame < port.samplesNum; ++frame) { + float input[2]{}; + for (int channel = 0; channel < port.channels; ++channel) { + const auto index = static_cast(frame) * port.channels + channel; + input[channel] = formatIsFloat(port.format) ? static_cast(samples)[index] : static_cast(samples)[index] / 32768.0f; + } + AudioOut2AccumulatePadFrame(AudioOut2Route::PadVibration, input, port.channels, unity, pad.data() + static_cast(frame) * AUDIO_OUT2_PAD_CHANNELS); + } + AudioOut2FinishPadMix(pad.data(), port.samplesNum); + return pad; +} + static void sleepUs(std::unique_lock& lock, std::uint64_t us) { lock.unlock(); PreciseSleepUs(us); @@ -217,34 +282,18 @@ 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); - - SDL_AudioCVT cvt{}; - const int cvtResult = SDL_BuildAudioCVT( - &cvt, - sdlFormat(port.format), static_cast(port.channels), static_cast(port.freq), - port.spec.format, port.spec.channels, port.spec.freq); - - if (cvtResult < 0) { - throw std::runtime_error(std::string("SDL_BuildAudioCVT: ") + SDL_GetError()); - } - - const void* queueData = prepared; - std::uint32_t queueSize = preparedSize; - std::vector convertBuf; - - if (cvtResult > 0) { - convertBuf.resize(static_cast(preparedSize) * cvt.len_mult); - std::memcpy(convertBuf.data(), prepared, preparedSize); - cvt.buf = convertBuf.data(); - cvt.len = static_cast(preparedSize); - if (SDL_ConvertAudio(&cvt) < 0) { - throw std::runtime_error(std::string("SDL_ConvertAudio: ") + SDL_GetError()); - } - queueData = cvt.buf; - queueSize = static_cast(cvt.len_cvt); + std::vector padFrames; + const void* queueData = nullptr; + std::uint32_t queueSize = 0; + if (port.type == PORT_TYPE_VIBRATION) { + queueSize = checkedBufferSize(port.samplesNum, port.padLayout.channels, sizeof(float)); + const auto pad = prepareVibration(port, data); + padFrames.resize(static_cast(port.samplesNum) * port.padLayout.channels); + AudioOut2WritePadFrames(pad.data(), port.padLayout, padFrames.data(), port.samplesNum); + queueData = padFrames.data(); + } else { + queueData = prepareBuffer(port, data, prepareBuf); + queueSize = checkedBufferSize(port.samplesNum, static_cast(port.channels), bytesPerSample(port.format)); } const std::uint64_t bufferUs = port.freq != 0 @@ -282,6 +331,25 @@ static void paceVirtualPort(Port& port, bool hasData, std::unique_lock& lock) { + const auto pad = port.type == PORT_TYPE_VIBRATION && data != nullptr ? prepareVibration(port, data) : std::vector{}; + paceVirtualPort(port, data != nullptr, lock); + const auto source = static_cast(&port - g_ports) + 1; + if (port.type == PORT_TYPE_VIBRATION && data == nullptr) PadClearLegacyAudioHaptics_nid_postfix(source); + if (!pad.empty()) { + constexpr std::uint32_t grainFrames = 256; + const auto begin = port.virtualQueueEnd - (1000000ULL * port.samplesNum) / port.freq; + for (std::uint32_t frame = 0; frame < port.samplesNum;) { + const auto count = std::min(grainFrames, port.samplesNum - frame); + const auto start = begin + (1000000ULL * frame) / port.freq; + const auto end = begin + (1000000ULL * (frame + count)) / port.freq; + const auto rumble = AudioOut2RumbleFromPadFrames(pad.data() + static_cast(frame) * AUDIO_OUT2_PAD_CHANNELS, count); + PadQueueLegacyAudioHaptics_nid_postfix(source, start, end, rumble[0], rumble[1]); + frame += count; + } + } +} + static bool portTypeValid(int type) { return (type >= PORT_TYPE_MAIN && type <= PORT_TYPE_PADSPK) || type == PORT_TYPE_VIBRATION || @@ -297,6 +365,42 @@ static Port* getPort(int handle) { return &g_ports[idx]; } +static int allocatePort(int type, std::uint32_t len, std::uint32_t freq, Format format) { + std::lock_guard lock(g_mutex); + for (int i = 0; i < PORTS_MAX; i++) { + if (!g_ports[i].used) { + Port& port = g_ports[i]; + port.used = true; + port.type = type; + port.samplesNum = len; + port.freq = freq; + port.format = format; + port.channels = channelsForFormat(format); + port.lastDataOutputTime = 0; + port.virtualQueueEnd = sceKernelGetProcessTime(); + port.mixLevel = type == PORT_TYPE_PADSPK ? DEFAULT_PADSPK_MIX_LEVEL : DEFAULT_VOLUME; + if (type == PORT_TYPE_VIBRATION) { + try { + openVibrationDevice(port); + } catch (...) { + port = Port{}; + throw; + } + } + for (int c = 0; c < port.channels; c++) { + port.volume[c] = DEFAULT_VOLUME; + } + if (type != PORT_TYPE_VIBRATION && type != PORT_TYPE_EX && type != PORT_TYPE_SYS) { + openDevice(port); + } + static const bool trace = std::getenv("APS5_TRACE_AUDIOOUT") != nullptr; + if (trace) std::fprintf(stderr, "[audioout] port %d: type %d, %u samples at %u Hz, format %d, device %u\n", i + 1, type, len, freq, static_cast(format), static_cast(port.device)); + return i + 1; + } + } + return -2144993275; +} + extern "C" { int APS5_VABI sceAudioOutInit() { @@ -333,47 +437,28 @@ int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len throw std::runtime_error("sceAudioOutOpen: unknown format param"); } - std::lock_guard lock(g_mutex); - for (int i = 0; i < PORTS_MAX; i++) { - if (!g_ports[i].used) { - Port& port = g_ports[i]; - port.used = true; - port.type = type; - port.samplesNum = len; - port.freq = freq; - port.format = format; - port.channels = channelsForFormat(format); - port.lastDataOutputTime = 0; - port.virtualQueueEnd = sceKernelGetProcessTime(); - port.mixLevel = type == PORT_TYPE_PADSPK ? DEFAULT_PADSPK_MIX_LEVEL : DEFAULT_VOLUME; - for (int c = 0; c < port.channels; c++) { - port.volume[c] = DEFAULT_VOLUME; - } - if (type != PORT_TYPE_VIBRATION) { - openDevice(port); - } - static const bool trace = std::getenv("APS5_TRACE_AUDIOOUT") != nullptr; - if (trace) std::fprintf(stderr, "[audioout] port %d: type %d, %u samples at %u Hz, format %d, device %u\n", i + 1, type, len, freq, static_cast(format), static_cast(port.device)); - return i + 1; - } - } - return -2144993275; + if (type == PORT_TYPE_VIBRATION && channelsForFormat(format) > 2) throw std::runtime_error("sceAudioOutOpen: vibration layouts above stereo are not implemented"); + return allocatePort(type, len, freq, format); } 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) { return -2144993277; } closeDevice(*port); + if (port->type == PORT_TYPE_VIBRATION) PadClearLegacyAudioHaptics_nid_postfix(handle); *port = Port{}; return 0; } -int APS5_VABI sceAudioOutOutput(int handle, const void* ptr) { - std::lock_guard outputLock(g_outputMutex); +int APS5_VABI sceAudioOutOutput(int handle, const void* ptr) try { + const AudioOutCall call; + 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) { @@ -381,20 +466,31 @@ int APS5_VABI sceAudioOutOutput(int handle, const void* ptr) { } if (port->device == 0) { - paceVirtualPort(*port, ptr != nullptr, lock); + outputVirtualPort(*port, ptr, lock); } else { queueAudio(*port, ptr, lock); } if (ptr != nullptr) port->lastDataOutputTime = sceKernelGetProcessTime(); return static_cast(port->samplesNum); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num) { +int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num) try { + const AudioOutCall call; if (param == nullptr || num == 0) { return -2144993276; } - 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++) { @@ -407,7 +503,7 @@ int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num) for (std::uint32_t i = 0; i < num; i++) { Port& port = *getPort(param[i].handle); if (port.device == 0) { - paceVirtualPort(port, param[i].ptr != nullptr, lock); + outputVirtualPort(port, param[i].ptr, lock); } else { queueAudio(port, param[i].ptr, lock); } @@ -419,6 +515,8 @@ int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num) } return static_cast(first.samplesNum); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceAudioOutSetVolume(int handle, std::uint32_t flag, int* vol) { @@ -501,86 +599,119 @@ int APS5_VABI sceAudioOutGetPortState(int handle, AudioOutPortState* state) { return 0; } -int APS5_VABI sceAudioOutDeviceIdOpen() { +int APS5_VABI sceAudioOutDeviceIdOpen(std::uint64_t deviceId, std::int32_t type, std::int32_t index, std::uint32_t length, std::int32_t frequency, std::uint32_t parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAudioOutSysClose() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAudioOutSysClose(int handle) { + if (handle <= 0 || handle > PORTS_MAX) { + throw std::runtime_error("sceAudioOutSysClose: handle " + std::to_string(handle) + " is not an open Sys 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_SYS) { + throw std::runtime_error("sceAudioOutSysClose: handle " + std::to_string(handle) + " is not an open Sys port"); + } + *port = Port{}; return 0; } -int APS5_VABI sceAudioOutSysConfigureOutput() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAudioOutSysConfigureOutput(int type, std::uint64_t flags, std::uint32_t mode, std::uint32_t target, std::uint64_t option) { + if (type != SYS_CONFIGURE_TYPE_HDMI) { + throw std::runtime_error("sceAudioOutSysConfigureOutput: output type " + std::to_string(type) + " not supported"); + } + if (flags != 0 || option != 0) { + throw std::runtime_error("sceAudioOutSysConfigureOutput: flags " + std::to_string(flags) + " and option " + std::to_string(option) + " not supported"); + } + if (mode != SYS_OPEN_MODE_TRUEHD) { + throw std::runtime_error("sceAudioOutSysConfigureOutput: mode " + std::to_string(mode) + " not supported"); + } + if (target != SYS_CONFIGURE_TARGET_HDMI) { + throw std::runtime_error("sceAudioOutSysConfigureOutput: target " + std::to_string(target) + " not supported"); + } return 0; } int APS5_VABI sceAudioOutSysGetHdmiMonitorInfo(int type, void* info, std::uint32_t size) { - if (type != MONITOR_TYPE_HDMI) { - throw std::runtime_error("sceAudioOutSysGetHdmiMonitorInfo: monitor type " + std::to_string(type) + " not supported"); - } - if (size != HDMI_MONITOR_INFO_SIZE) { - throw std::runtime_error("sceAudioOutSysGetHdmiMonitorInfo: size " + std::to_string(size) + " not supported"); - } - if (info == nullptr) { - return -2144993276; - } + if (info == nullptr) return static_cast(0x80260004); + if (size != HDMI_MONITOR_INFO_SIZE) return static_cast(0x80260006); + if (type != MONITOR_TYPE_HDMI && type != 0x10) return static_cast(0x8026000C); + if (type == 0x10) throw std::runtime_error("sceAudioOutSysGetHdmiMonitorInfo: monitor type 16 not supported"); std::memset(info, 0, HDMI_MONITOR_INFO_SIZE); return 0; } -int APS5_VABI sceAudioOutSysOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudioOutSysOpen(int userId, std::uint32_t mode) { + (void)userId; + if (mode != SYS_OPEN_MODE_TRUEHD) { + throw std::runtime_error("sceAudioOutSysOpen: mode " + std::to_string(mode) + " not supported"); + } + return allocatePort(PORT_TYPE_SYS, SYS_PORT_SAMPLES, SYS_PORT_FRAME_RATE, Format::S16_8Ch); } -int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, int mixLevel) { +int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, std::uint32_t mixLevel) { std::lock_guard lock(g_mutex); Port* port = getPort(handle); if (port == nullptr) { return -2144993277; } - if (port->type != PORT_TYPE_PADSPK) { - return -2144993270; - } if (mixLevel > DEFAULT_VOLUME) { return -2144993260; } - if (mixLevel < 0) { - throw std::runtime_error("sceAudioOutSetMixLevelPadSpk: negative mix level not supported"); + if (port->type != PORT_TYPE_PADSPK) { + return -2144993270; } port->mixLevel = mixLevel; return 0; } -int APS5_VABI sceAudioOutExClose() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAudioOutExClose(int handle) { + 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) { + throw std::runtime_error("sceAudioOutExClose: handle " + std::to_string(handle) + " is not an open Ex port"); + } + *port = Port{}; return 0; } -int APS5_VABI sceAudioOutExConfigureOutput() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAudioOutExConfigureOutput(int control, std::uint64_t flags, std::uint32_t mode, std::uint32_t target, std::uint64_t option) { + if (control != 0) { + return ERROR_EX_INVALID_CONTROL; + } + if (flags != 0 || option != 0) { + throw std::runtime_error("sceAudioOutExConfigureOutput: flags " + std::to_string(flags) + " and option " + std::to_string(option) + " not supported"); + } + if (mode > EX_OPEN_MODE_MAX && mode != EX_CONFIGURE_MODE_DEFAULT) { + throw std::runtime_error("sceAudioOutExConfigureOutput: mode " + std::to_string(mode) + " not supported"); + } + if ((target < 1 || target > 3) && target != EX_CONFIGURE_TARGET_DEFAULT) { + throw std::runtime_error("sceAudioOutExConfigureOutput: target " + std::to_string(target) + " not supported"); + } return 0; } int APS5_VABI sceAudioOutExGetMonitorInfo(int type, int index, void* info, std::uint32_t size) { - if (type != 0 || index != 0) { - throw std::runtime_error("sceAudioOutExGetMonitorInfo: monitor " + std::to_string(type) + ", " + std::to_string(index) + " not supported"); - } - if (size < EX_MONITOR_INFO_MIN_SIZE) { - throw std::runtime_error("sceAudioOutExGetMonitorInfo: size " + std::to_string(size) + " not supported"); - } - if (info == nullptr) { - return -2144993276; - } - std::memset(info, 0, size); + if (type != 0) return static_cast(0x80260003); + if (info == nullptr) return static_cast(0x80260004); + if (size != EX_MONITOR_INFO_SIZE) return static_cast(0x80260006); + std::memset(info, 0, EX_MONITOR_INFO_SIZE); return 0; } -int APS5_VABI sceAudioOutExOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudioOutExOpen(int userId, std::uint32_t mode) { + (void)userId; + if (mode > EX_OPEN_MODE_MAX || ((EX_OPEN_MODES >> mode) & 1) == 0) { + return ERROR_EX_INVALID_MODE; + } + const bool highRate = mode == 3 || mode == 4 || mode == 10; + return allocatePort(PORT_TYPE_EX, highRate ? 1024 : 256, highRate ? 192000 : 48000, Format::S16Stereo); } } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 3f88277444..e11ee633e5 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -13,15 +13,11 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/PreciseWait.hpp" +#include "prx/libc/include/Shutdown.hpp" #include "AudioOut2Internal.hpp" +#include "AudioOutShutdown.hpp" #include "prx/libSceAudioOut/include/AudioOutSdl.hpp" - -// An AudioOut2 context is the hardware output queue: every push appends a grain (num_grains samples) -// to a queue of queue_depth grains that plays in real time. Titles pace their mixer on that queue, so -// the level must follow the output clock. With an SDL device open, a grain's port buffers are read at -// the next push, the device callback plays the mixed grains behind a cushion and the level counts the -// grains beyond that cushion; otherwise each push is mixed at once and a wall-clock model of the -// queue stands in. +#include "prx/libScePad/include/PadState.hpp" using Clock = std::chrono::steady_clock; @@ -175,7 +171,7 @@ static void ClosePadDevice(AudioOut2Context& context) { static void UpdatePadDevice(AudioOut2Context& context, Clock::time_point now) { if (context.padDevice != 0 && SDL_GetAudioDeviceStatus(context.padDevice) == SDL_AUDIO_STOPPED) { - APS5_LOG_CHARS_OUT("AudioOut2: the controller sound card went away; its ports play in the main mix"); + APS5_LOG_CHARS_OUT("AudioOut2: the controller sound card went away; speaker audio falls back to the main mix, haptic audio stays muted"); ClosePadDevice(context); context.nextPadProbe = now + PAD_PROBE_INTERVAL; } @@ -201,7 +197,8 @@ static void QueuePadGrain(AudioOut2Context& context) { static void Render(AudioOut2Context& context, const AudioOut2Grain& grain) { float* pad = nullptr; - if (context.padDevice != 0) { + if (context.padDevice != 0 || AudioOut2HasPadPorts(context)) { + context.padMix.resize(static_cast(context.grain) * AUDIO_OUT2_PAD_CHANNELS); std::fill(context.padMix.begin(), context.padMix.end(), 0.0f); pad = context.padMix.data(); } @@ -213,7 +210,11 @@ static void Render(AudioOut2Context& context, const AudioOut2Grain& grain) { } if (pad != nullptr) { AudioOut2FinishPadMix(pad, context.grain); - QueuePadGrain(context); + if (context.padDevice != 0) QueuePadGrain(context); + else { + const auto rumble = AudioOut2RumbleFromPadFrames(pad, context.grain); + PadSubmitAudioHaptics_nid_postfix(rumble[0], rumble[1]); + } } } @@ -256,16 +257,30 @@ static void TraceSummary(AudioOut2Context& context, Clock::time_point now) { extern "C" { -// The title's per-tick step between setting the ports' data and pushing. A push records the buffers the -// ports point at and the next push reads them, and the push paces the clock, so nothing is due here. -int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) { +int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) try { + const AudioOutCall call; auto* context = FromHandle(ctx); if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; std::lock_guard lock(context->lock); + if (context->device != 0) { + if (!context->advancedMode) { + while (!context->pendingGrains.empty()) { + Render(*context, context->pendingGrains.front()); + context->output.insert(context->output.end(), context->mix.begin(), context->mix.end()); + context->pendingGrains.pop_front(); + context->pendingSince.pop_front(); + } + context->advancedMode = true; + } + context->advancedGrains.push_back(AudioOut2CaptureGrain(*context, true)); + context->grainReady.notify_one(); + } context->advances++; context->summaryAdvances++; if (context->advances <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f Advance ctx %p (pushes so far %llu)\n", AudioOut2TraceSeconds(), static_cast(context), static_cast(context->pushes)); return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam* params, void* buffer, size_t buffer_size, AudioOut2ContextHandle* ctx) { @@ -283,7 +298,8 @@ int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam* params, voi return 0; } -int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle ctx) { +int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle ctx) try { + const AudioOutCall call; auto* context = FromHandle(ctx); if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; AUDIOOUT2_TRACE("t=%.3f ContextDestroy ctx %p after %llu pushes\n", AudioOut2TraceSeconds(), static_cast(context), static_cast(context->pushes)); @@ -297,13 +313,15 @@ int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle ctx) { AudioOut2ReleasePorts(*context); delete context; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint32_t* queue_level, uint32_t* available_queue) { 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++; @@ -312,7 +330,8 @@ int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint3 return 0; } -int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t blocking) { +int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t blocking) try { + const AudioOutCall call; auto* context = FromHandle(ctx); if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; std::unique_lock lock(context->lock); @@ -330,13 +349,24 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block lock.lock(); now = Clock::now(); } + if (context->advancedMode && context->advancedGrains.empty()) { + if (!blocking) throw std::runtime_error("AudioOut2: a non-blocking push without a rendered grain is not implemented"); + if (!context->grainReady.wait(lock, LibcShutdownToken_nid_postfix(), [&] { return !context->advancedGrains.empty(); })) throw ProcessShutdown{}; + now = Clock::now(); + Drain(*context, now); + } context->queued++; context->playHead += GrainDuration(*context); UpdatePadDevice(*context, now); - auto grain = AudioOut2CaptureGrain(*context); + auto grain = context->advancedMode ? std::move(context->advancedGrains.front()) : AudioOut2CaptureGrain(*context); + if (context->advancedMode) context->advancedGrains.pop_front(); const auto mixed = static_cast(grain.size()); if (context->device == 0) { Render(*context, grain); + } else if (context->advancedMode) { + Render(*context, grain); + if (PendingMs(*context) > MAX_QUEUED_MS) context->dropped++; + else context->output.insert(context->output.end(), context->mix.begin(), context->mix.end()); } else { context->pendingGrains.push_back(std::move(grain)); context->pendingSince.push_back(Clock::now()); @@ -367,6 +397,8 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block } TraceSummary(*context, now); return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceAudioOut2ContextQueryMemory(const AudioOut2ContextParam* params, size_t* memory_size) { diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp index 1a6f8cc7e4..c58bd4c542 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -1,12 +1,15 @@ #ifndef CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2INTERNAL_HPP #define CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2INTERNAL_HPP +#include +#include #include #include #include #include #include #include +#include #include #include @@ -47,6 +50,8 @@ struct AudioOut2CapturedPort { std::size_t index = 0; std::uint64_t generation = 0; const void* data = nullptr; + std::vector pcm; + std::array volume{}; }; using AudioOut2Grain = std::vector; @@ -86,6 +91,9 @@ struct AudioOut2Context { // Mixed stereo frames the device has not played yet; it plays silence until the cushion is mixed. std::deque output; bool priming = true; + bool advancedMode = false; + std::condition_variable_any grainReady; + std::deque advancedGrains; std::deque pendingGrains; std::deque pendingSince; SDL_AudioDeviceID padDevice = 0; @@ -112,7 +120,7 @@ struct AudioOut2Context { // Mixes every port of the context that carries PCM data into out (stereo float, frames frames), summing // onto the zeroed buffer. Returns the number of ports mixed. std::uint32_t AudioOut2MixPorts(const AudioOut2Context& context, const AudioOut2Grain& grain, float* out, float* padOut, std::uint32_t frames); -AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context); +AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context, bool snapshot = false); bool AudioOut2HasPadPorts(const AudioOut2Context& context); // Forgets the ports of a context being destroyed. void AudioOut2ReleasePorts(const AudioOut2Context& context); diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp new file mode 100644 index 0000000000..eac5204b6d --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp @@ -0,0 +1,73 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAudioOut2Initialize(void); +int APS5_VABI sceAudioOut2ContextQueryMemory(const AudioOut2ContextParam* params, size_t* memory_size); +int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam* params, void* buffer, size_t buffer_size, AudioOut2ContextHandle* ctx); +int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle ctx); +int APS5_VABI sceAudioOut2ContextSetAttributes(AudioOut2ContextHandle ctx, const AudioOut2Attribute* attributes, uint32_t num); +int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx); +int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t blocking); +int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint32_t* queue_level, uint32_t* available_queue); +int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2PortParam* params, AudioOut2PortHandle* port); +int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle port); +int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle port, const AudioOut2Attribute* attributes, uint32_t num); +int APS5_VABI sceAudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortState* state); + +int APS5_VABI sceAudioOut2LoInit(void) { + return sceAudioOut2Initialize(); +} + +int APS5_VABI sceAudioOut2LoTerminate(void) { + return 0; +} + +int APS5_VABI sceAudioOut2LoContextQueryMemory(const AudioOut2ContextParam* params, size_t* memory_size) { + return sceAudioOut2ContextQueryMemory(params, memory_size); +} + +int APS5_VABI sceAudioOut2LoContextCreate(const AudioOut2ContextParam* params, void* buffer, size_t buffer_size, AudioOut2ContextHandle* ctx) { + return sceAudioOut2ContextCreate(params, buffer, buffer_size, ctx); +} + +int APS5_VABI sceAudioOut2LoContextDestroy(AudioOut2ContextHandle ctx) { + return sceAudioOut2ContextDestroy(ctx); +} + +int APS5_VABI sceAudioOut2LoContextSetAttributes(AudioOut2ContextHandle ctx, const AudioOut2Attribute* attributes, uint32_t num) { + return sceAudioOut2ContextSetAttributes(ctx, attributes, num); +} + +int APS5_VABI sceAudioOut2LoContextAdvance(AudioOut2ContextHandle ctx) { + return sceAudioOut2ContextAdvance(ctx); +} + +int APS5_VABI sceAudioOut2LoContextPush(AudioOut2ContextHandle ctx, uint32_t blocking) { + return sceAudioOut2ContextPush(ctx, blocking); +} + +int APS5_VABI sceAudioOut2LoContextGetQueueLevel(AudioOut2ContextHandle ctx, uint32_t* queue_level, uint32_t* available_queue) { + return sceAudioOut2ContextGetQueueLevel(ctx, queue_level, available_queue); +} + +int APS5_VABI sceAudioOut2LoPortCreate(AudioOut2ContextHandle ctx, const AudioOut2PortParam* params, AudioOut2PortHandle* port) { + return sceAudioOut2PortCreate(ctx, params, port); +} + +int APS5_VABI sceAudioOut2LoPortDestroy(AudioOut2PortHandle port) { + return sceAudioOut2PortDestroy(port); +} + +int APS5_VABI sceAudioOut2LoPortSetAttributes(AudioOut2PortHandle port, const AudioOut2Attribute* attributes, uint32_t num) { + return sceAudioOut2PortSetAttributes(port, attributes, num); +} + +int APS5_VABI sceAudioOut2LoPortGetState(AudioOut2PortHandle port, AudioOut2PortState* state) { + return sceAudioOut2PortGetState(port, state); +} + +} diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp index 0d437bbb81..fa03043577 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp @@ -1,7 +1,20 @@ #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int SCE_AUDIO_OUT2_ERROR_MASTERING_INVALID_API_PARAM = static_cast(0x80268201); +constexpr int SCE_AUDIO_OUT2_ERROR_MASTERING_INVALID_STATES_ID = static_cast(0x80268204); +constexpr std::uint32_t AUDIO_OUT2_MASTERING_STATES_ID_DEFAULT = 1; +constexpr std::uint32_t AUDIO_OUT2_MASTERING_STATES_ID_V2 = 2; +constexpr std::uint32_t AUDIO_OUT2_MASTERING_STRUCT_ID_COMPRESSOR_DEFAULT = 0x01010001; +constexpr std::uint32_t AUDIO_OUT2_MASTERING_STRUCT_ID_COMPRESSOR_V2 = 0x01020001; +constexpr std::uint32_t AUDIO_OUT2_MASTERING_STRUCT_ID_LIMITER_DEFAULT = 0x01010003; + +} + extern "C" { int APS5_VABI sceAudioOut2MasteringInit(uint32_t flags) { @@ -9,22 +22,49 @@ int APS5_VABI sceAudioOut2MasteringInit(uint32_t flags) { return 0; } +int APS5_VABI sceAudioOut2MasteringInitEx(uint32_t flags) { + if ((flags & 0x7FFFFFFFu) != 0) NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceAudioOut2MasteringTerm(void) { return 0; } int APS5_VABI sceAudioOut2MasteringGetState(AudioOut2MasteringStatesHeader* state, uint32_t output, AudioOut2UserHandle user) { - (void)state; (void)output; (void)user; - NotImplemented_nid_no_patch(__func__); - return 0; + if (state == nullptr) return SCE_AUDIO_OUT2_ERROR_MASTERING_INVALID_API_PARAM; + switch (state->states_id) { + case AUDIO_OUT2_MASTERING_STATES_ID_DEFAULT: { + auto* full = static_cast(static_cast(state)); + std::memset(full, 0, sizeof(*full)); + full->states_header.states_id = AUDIO_OUT2_MASTERING_STATES_ID_DEFAULT; + full->compressor_states.descriptor.id = AUDIO_OUT2_MASTERING_STRUCT_ID_COMPRESSOR_DEFAULT; + full->compressor_states.descriptor.size = sizeof(AudioOut2MasteringCompressorStates); + full->limiter_states.descriptor.id = AUDIO_OUT2_MASTERING_STRUCT_ID_LIMITER_DEFAULT; + full->limiter_states.descriptor.size = sizeof(AudioOut2MasteringLimiterStates); + return 0; + } + case AUDIO_OUT2_MASTERING_STATES_ID_V2: { + auto* full = static_cast(static_cast(state)); + std::memset(full, 0, sizeof(*full)); + full->states_header.states_id = AUDIO_OUT2_MASTERING_STATES_ID_V2; + full->compressor_states.descriptor.id = AUDIO_OUT2_MASTERING_STRUCT_ID_COMPRESSOR_V2; + full->compressor_states.descriptor.size = sizeof(AudioOut2MasteringCompressorStatesV2); + full->limiter_states.descriptor.id = AUDIO_OUT2_MASTERING_STRUCT_ID_LIMITER_DEFAULT; + full->limiter_states.descriptor.size = sizeof(AudioOut2MasteringLimiterStates); + return 0; + } + default: + return SCE_AUDIO_OUT2_ERROR_MASTERING_INVALID_STATES_ID; + } } int APS5_VABI sceAudioOut2MasteringSetParam(const AudioOut2MasteringParamsHeader* param, uint32_t output, uint32_t flags) { (void)output; (void)flags; - if (param == nullptr) NotImplemented_nid_no_patch(__func__); + if (param == nullptr) return SCE_AUDIO_OUT2_ERROR_MASTERING_INVALID_API_PARAM; return 0; } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2PadMix.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2PadMix.cpp index e1c7c049e0..5d1a0f387d 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2PadMix.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2PadMix.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include AudioOut2Route AudioOut2RouteForPort(std::uint16_t type, std::uint32_t channels) { @@ -37,6 +38,21 @@ void AudioOut2FinishPadMix(float* out, std::uint32_t frames) { for (std::size_t index = 0; index < samples; index++) out[index] = std::clamp(out[index], -1.0f, 1.0f); } +std::array AudioOut2RumbleFromPadFrames(const float* pad, std::uint32_t frames) { + std::array result{}; + if (pad == nullptr || frames == 0) return result; + for (std::uint32_t channel = 0; channel < 2; ++channel) { + double energy = 0.0; + for (std::uint32_t frame = 0; frame < frames; ++frame) { + const auto sample = pad[static_cast(frame) * AUDIO_OUT2_PAD_CHANNELS + AUDIO_OUT2_PAD_VIBRATION_LEFT + channel]; + if (std::isfinite(sample)) energy += static_cast(sample) * sample; + } + const auto rms = std::sqrt(energy / frames); + if (rms >= 0.01) result[channel] = static_cast(std::lround(std::min(1.0, rms * 2.0) * 255)); + } + return result; +} + AudioOut2PadLayout AudioOut2PadLayoutForDriver(const char* driver) { AudioOut2PadLayout layout; if (driver != nullptr && std::strcmp(driver, "pulseaudio") == 0) { diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2PadMix.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2PadMix.hpp index 501a1af8eb..1d4663c039 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2PadMix.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2PadMix.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2PADMIX_HPP #include +#include static constexpr std::uint16_t AUDIO_OUT2_PORT_TYPE_PAD_SPEAKER = 0x3; static constexpr std::uint16_t AUDIO_OUT2_PORT_TYPE_PAD_VIBRATION = 0x6; @@ -28,6 +29,7 @@ AudioOut2Route AudioOut2RouteForPort(std::uint16_t type, std::uint32_t channels) bool AudioOut2IsPadAudioDevice(const char* name); void AudioOut2AccumulatePadFrame(AudioOut2Route route, const float* in, std::uint32_t channels, const float* volume, float* pad); void AudioOut2FinishPadMix(float* out, std::uint32_t frames); +std::array AudioOut2RumbleFromPadFrames(const float* pad, std::uint32_t frames); AudioOut2PadLayout AudioOut2PadLayoutForDriver(const char* driver); void AudioOut2WritePadFrames(const float* pad, const AudioOut2PadLayout& layout, float* out, std::uint32_t frames); diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp index 0deb7d563b..14c9bc2c63 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp @@ -6,11 +6,14 @@ #include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/Shutdown.hpp" #include "AudioOut2Internal.hpp" #include "AudioOut2PadMix.hpp" +#include "AudioOutShutdown.hpp" static constexpr std::uint16_t OUTPUT_MAIN = 1; static constexpr std::int16_t VOLUME_MAX = 127; @@ -74,15 +77,15 @@ static void ReadFrame(const AudioOut2Port& port, const void* data, std::uint32_t } } -static void AccumulatePadPort(const AudioOut2Port& port, const void* data, AudioOut2Route route, float* out, std::uint32_t frames) { +static void AccumulatePadPort(const AudioOut2Port& port, const void* data, AudioOut2Route route, float* out, std::uint32_t frames, const float* volume) { float in[AUDIO_OUT2_PORT_CHANNELS_MAX]; for (std::uint32_t frame = 0; frame < frames; frame++) { ReadFrame(port, data, frame, in); - AudioOut2AccumulatePadFrame(route, in, port.channels, port.volume, out + static_cast(frame) * AUDIO_OUT2_PAD_CHANNELS); + AudioOut2AccumulatePadFrame(route, in, port.channels, volume, out + static_cast(frame) * AUDIO_OUT2_PAD_CHANNELS); } } -static void AccumulatePort(const AudioOut2Port& port, const void* data, float* out, std::uint32_t frames) { +static void AccumulatePort(const AudioOut2Port& port, const void* data, float* out, std::uint32_t frames, const float* volume) { const auto& fold = *port.fold; float in[AUDIO_OUT2_PORT_CHANNELS_MAX]; for (std::uint32_t frame = 0; frame < frames; frame++) { @@ -90,7 +93,7 @@ static void AccumulatePort(const AudioOut2Port& port, const void* data, float* o float left = 0.0f; float right = 0.0f; for (std::uint32_t c = 0; c < port.channels; c++) { - const float sample = in[c] * port.volume[c]; + const float sample = in[c] * volume[c]; if (fold.left[c] != 0.0f) left += sample * fold.left[c]; if (fold.right[c] != 0.0f) right += sample * fold.right[c]; } @@ -99,7 +102,7 @@ static void AccumulatePort(const AudioOut2Port& port, const void* data, float* o } } -AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context) { +AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context, bool snapshot) { std::lock_guard lock(g_portsLock); AudioOut2Grain grain; for (std::size_t index = 0; index < g_ports.size(); index++) { @@ -110,7 +113,15 @@ AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context) { data = port.queuedData.front(); port.queuedData.pop_front(); } - if (data != nullptr) grain.push_back({index, port.generation, data}); + if (data != nullptr) { + AudioOut2CapturedPort captured{index, port.generation, data}; + if (snapshot) { + captured.pcm.resize(static_cast(context.grain) * port.channels * (port.int16 ? sizeof(std::int16_t) : sizeof(float))); + std::memcpy(captured.pcm.data(), data, captured.pcm.size()); + std::copy_n(port.volume, port.channels, captured.volume.begin()); + } + grain.push_back(std::move(captured)); + } } return grain; } @@ -118,17 +129,17 @@ AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context) { std::uint32_t AudioOut2MixPorts(const AudioOut2Context& context, const AudioOut2Grain& grain, float* out, float* padOut, std::uint32_t frames) { std::lock_guard lock(g_portsLock); std::uint32_t mixed = 0; - for (const auto& [index, generation, data] : grain) { + for (const auto& captured : grain) { + const auto index = captured.index; + const auto generation = captured.generation; + const void* data = captured.pcm.empty() ? captured.data : captured.pcm.data(); if (index >= g_ports.size()) continue; const auto& port = g_ports[index]; if (!port.used || port.generation != generation || port.context != &context || port.channels == 0) continue; - auto route = AudioOut2RouteForPort(port.type, port.channels); - if (padOut == nullptr) { - if (route == AudioOut2Route::PadVibration) continue; - route = AudioOut2Route::Main; - } - if (route == AudioOut2Route::Main) AccumulatePort(port, data, out, frames); - else AccumulatePadPort(port, data, route, padOut, frames); + const auto route = AudioOut2RouteForPort(port.type, port.channels); + const float* volume = captured.pcm.empty() ? port.volume : captured.volume.data(); + if (route == AudioOut2Route::Main || (context.padDevice == 0 && route == AudioOut2Route::PadSpeaker)) AccumulatePort(port, data, out, frames, volume); + else if (padOut != nullptr) AccumulatePadPort(port, data, route, padOut, frames, volume); mixed++; } return mixed; @@ -169,7 +180,8 @@ static void TraceAttribute(AudioOut2PortHandle handle, const AudioOut2Port& port extern "C" { -int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2PortParam* params, AudioOut2PortHandle* port) { +int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2PortParam* params, AudioOut2PortHandle* port) try { + const AudioOutCall call; if (!ctx) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; if (!params || !port) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT; const auto channels = (params->data_format >> FORMAT_CHANNELS_SHIFT) & FORMAT_CHANNELS_MASK; @@ -210,30 +222,39 @@ int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2 AudioOut2TraceSeconds(), static_cast(ctx), static_cast(*port), params->port_type, params->data_format, entry.channels, params->sampling_freq, params->flags, static_cast(params->user_handle)); return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle port) { +int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle port) try { + const AudioOutCall call; std::lock_guard lock(g_portsLock); auto* entry = FromHandle(port); if (!entry) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; AUDIOOUT2_TRACE("t=%.3f PortDestroy port %llu (data sets %llu)\n", AudioOut2TraceSeconds(), static_cast(port), static_cast(entry->dataSets)); *entry = AudioOut2Port{}; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceAudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortState* state) { +int APS5_VABI sceAudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortState* state) try { + const AudioOutCall call; std::lock_guard lock(g_portsLock); - if (!FromHandle(port)) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; + const auto* entry = FromHandle(port); + if (!entry) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; if (!state) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT; + *state = AudioOut2PortState{}; state->output = OUTPUT_MAIN; - state->num_channels = AUDIO_OUT2_OUTPUT_CHANNELS; + state->num_channels = static_cast(entry->channels); state->volume = VOLUME_MAX; - state->reroute_counter = 0; - state->flags = 0; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle port, const AudioOut2Attribute* attributes, uint32_t num) { +int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle port, const AudioOut2Attribute* attributes, uint32_t num) try { + const AudioOutCall call; std::lock_guard lock(g_portsLock); auto* entry = FromHandle(port); if (!entry) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; @@ -259,6 +280,8 @@ int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle port, const Audi if (traces < ATTRIBUTE_TRACE_FULL || traces % ATTRIBUTE_TRACE_EVERY == 0) TraceAttribute(port, *entry, attribute, verdict); } return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp index 9185d192e0..7b6a07017e 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp @@ -68,7 +68,7 @@ extern "C" { int APS5_VABI sceAudioOut2SpeakerArrayCreate(AudioOut2SpeakerArrayHandle* handle, const void* vbap_params, const void* ambi_params) { const auto* params = static_cast(vbap_params); - if (handle == nullptr || params == nullptr || ambi_params == nullptr || params->positions == nullptr || + if (handle == nullptr || params == nullptr || params->positions == nullptr || params->numSpeakers == 0 || params->numSpeakers > MaxSpeakers || params->memory == nullptr || params->memorySize < sizeof(SpeakerArray) || reinterpret_cast(params->memory) % alignof(SpeakerArray) != 0) { APS5_INVALID_ARG_EX; @@ -100,14 +100,15 @@ int APS5_VABI sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients(AudioOut2Speaker } int APS5_VABI sceAudioOut2GetSpeakerArrayCoefficients(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, uint32_t num_coefficients, uint8_t height_aware, float downmix_spread_radius) { - (void)handle; (void)pos; (void)spread; - (void)coefficients; - (void)num_coefficients; (void)height_aware; (void)downmix_spread_radius; - NotImplemented_nid_no_patch(__func__); + const SpeakerArray& array = Array(handle); + if (coefficients == nullptr || num_coefficients != array.numSpeakers) APS5_INVALID_ARG_EX; + std::fill_n(coefficients, array.numSpeakers, 0.0f); + if (array.numSpeakers > 0) coefficients[0] = 1.0f; + if (array.numSpeakers > 1) coefficients[1] = 1.0f; return 0; } @@ -121,13 +122,15 @@ size_t APS5_VABI sceAudioOut2GetSpeakerArrayMemorySize(uint32_t num_speakers, ui int APS5_VABI sceAudioOut2GetSpeakerInfo(AudioOut2SpeakerInfo* info, uint32_t flags) { (void)flags; if (!info) return static_cast(0x80260502); - constexpr std::uint8_t SpeakerTypeStereo = 0; + constexpr std::uint8_t SpeakerTypeTelevision = 0; constexpr std::uint32_t FrontLeftAndRight = 0x3; + constexpr AudioOut2SpeakerAngle NoSpeaker = {-360, -360}; *info = AudioOut2SpeakerInfo{}; - info->type = SpeakerTypeStereo; + info->type = SpeakerTypeTelevision; info->available_bits = FrontLeftAndRight; - info->speaker_angle[0] = {-30, 0}; - info->speaker_angle[1] = {30, 0}; + for (auto& angle : info->speaker_angle) angle = NoSpeaker; + info->speaker_angle[0] = {30, 0}; + info->speaker_angle[1] = {-30, 0}; return 0; } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp index 93afb2dabf..89c681ad9c 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -1,12 +1,12 @@ #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "AudioOut2Internal.hpp" -static constexpr int USER_ID_SYSTEM = 0xFF; -static constexpr std::uint32_t MIN_SUPPORTED_3D_LATENCY = 1; -static constexpr std::uint32_t MAX_SUPPORTED_3D_LATENCY = 2; +static constexpr std::uint32_t MAX_SUPPORTED_3D_OUTPUT = 2; +static constexpr int SCE_AUDIO_OUT2_ERROR_INVALID_PARAM = static_cast(0x80268001); static std::atomic g_chatEnabled{false}; @@ -16,8 +16,14 @@ int APS5_VABI sceAudioOut2Initialize(void) { return 0; } -int APS5_VABI sceAudioOut2Set3DLatency(int userId, std::uint32_t latency) { - if (userId != USER_ID_SYSTEM || latency < MIN_SUPPORTED_3D_LATENCY || latency > MAX_SUPPORTED_3D_LATENCY) NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAudioOut2InitializeEx(std::uint32_t param) { + (void)param; + return sceAudioOut2Initialize(); +} + +int APS5_VABI sceAudioOut2Set3DLatency(int userId, std::uint32_t output) { + if (output > MAX_SUPPORTED_3D_OUTPUT) return SCE_AUDIO_OUT2_ERROR_INVALID_PARAM; + if (userId != 0 && (userId != 0xFF || output == 0)) return SCE_AUDIO_OUT2_ERROR_INVALID_PARAM; return 0; } @@ -28,8 +34,16 @@ int APS5_VABI sceAudioOut2GetSystemState(AudioOut2SystemState* state) { } int APS5_VABI sceAudioOut2SetSystemDebugState(const AudioOut2SystemDebugStateParam* param) { - (void)param; - NotImplemented_nid_no_patch(__func__); + constexpr int ErrorInvalidPointer = static_cast(0x8026800C); + constexpr std::uint32_t DebugStateIdLoudness = 1; + constexpr std::uint32_t DebugStateIdUnmodelled = 0x20; + constexpr std::size_t LoudnessParamSize = 8; + constexpr std::size_t UnmodelledParamSize = 4; + if (param == nullptr) return ErrorInvalidPointer; + if (param->debug_state_id == DebugStateIdUnmodelled && param->param_size == UnmodelledParamSize) NotImplemented_nid_no_patch(__func__); + if (param->debug_state_id != DebugStateIdLoudness || param->param_size != LoudnessParamSize) return SCE_AUDIO_OUT2_ERROR_INVALID_PARAM; + if (param->param == nullptr) NotImplemented_nid_no_patch(__func__); + AUDIOOUT2_TRACE("t=%.3f SetSystemDebugState: debug state %u recorded; the host has no system mix to apply it to\n", AudioOut2TraceSeconds(), param->debug_state_id); return 0; } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp index a8c42717bf..1a514be246 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp @@ -32,13 +32,13 @@ int APS5_VABI sceAudioOut2UserDestroy(AudioOut2UserHandle handle) { } int APS5_VABI sceAudioOut2UserGetSupportedAttributes(AudioOut2UserHandle handle, uint32_t* contextAttributes, uint32_t* portAttributes) { - if (!contextAttributes || !portAttributes) - throw std::runtime_error("sceAudioOut2UserGetSupportedAttributes: null output"); + if (handle == 0 || handle == std::numeric_limits::max()) return static_cast(0x80268010); + if (!contextAttributes && !portAttributes) return static_cast(0x80268001); std::lock_guard lock(g_usersLock); if (!g_users.contains(handle)) throw std::runtime_error("sceAudioOut2UserGetSupportedAttributes: unknown user handle"); - *contextAttributes = 0; - *portAttributes = (1u << 0) | (1u << 1); + if (contextAttributes) *contextAttributes = 0; + if (portAttributes) *portAttributes = (1u << 0) | (1u << 1); return 0; } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.cpp b/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.cpp new file mode 100644 index 0000000000..d980fd22fc --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.cpp @@ -0,0 +1,34 @@ +#include +#include +#include "prx/libc/include/Shutdown.hpp" +#include "AudioOutShutdown.hpp" + +namespace { + +std::atomic inside{0}; +std::atomic stopping{false}; + +void Leave() { + if (inside.fetch_sub(1) == 1) inside.notify_all(); +} + +void Stop() { + stopping.store(true); + for (auto count = inside.load(); count != 0; count = inside.load()) inside.wait(count); +} + +} + +AudioOutCall::AudioOutCall() { + static const bool registered = (LibcRegisterShutdown_nid_postfix(Stop), true); + static_cast(registered); + inside.fetch_add(1); + if (stopping.load() || LibcShutdownToken_nid_postfix().stop_requested()) { + Leave(); + throw ProcessShutdown{}; + } +} + +AudioOutCall::~AudioOutCall() { + Leave(); +} diff --git a/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.hpp b/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.hpp new file mode 100644 index 0000000000..39af8943ab --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.hpp @@ -0,0 +1,12 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUTSHUTDOWN_HPP +#define CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUTSHUTDOWN_HPP + +class AudioOutCall final { +public: + AudioOutCall(); + ~AudioOutCall(); + AudioOutCall(const AudioOutCall&) = delete; + AudioOutCall& operator=(const AudioOutCall&) = delete; +}; + +#endif diff --git a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp new file mode 100644 index 0000000000..24829f4035 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp @@ -0,0 +1,413 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAudioOut2UserCreateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetSystemDebugState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetHandleStatusInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetSimulatedHandleStatusInfo2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutChangeAppModuleState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetSimulatedHandleStatusInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetSimulatedBusUsableStatusByBusType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutAttachToApplicationByPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutDetachFromApplicationByPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutPtOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutPtClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutPtGetLastOutputTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExPtOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExPtClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExPtGetLastOutputTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExGetLastOutputTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetInfoOpenNum(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutMasteringInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutMasteringSetParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutMasteringGetState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutMasteringTerm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetPadSpkVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetPadJackVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetUsbVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetCaesarVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetHeadphoneOutMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutGetFocusEnablePid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetMainOutput(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetVolumeDown(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetChatBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetRecMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetAllMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetSparkEQ(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutArbitrationInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2ArbitrationInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnable3DAudioByArbitration(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutAssignHRTF(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnable3DAudioForUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableMonoHpOutput(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableTVVirtual(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableTVCorrection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableVrControllerLowPowerMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutEnableCinematicVrRotation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutCustomHrtfIsExist(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExConfigureOutputMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExSystemInfoIsSupportedAudioOutExMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExGetSystemInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutExGetOutputInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysConfigureOutputMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysGetMonitorInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysHdmiMonitorInfoIsSupportedAudioOutMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysGetSystemInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSysGetOutputInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetPortConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetPortStatuses(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetHeadphoneOutMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusUserLoginFor3DAudio(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetDeviceConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetMorpheusParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetCaesarParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2MbusSetSparkParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2AllAudioMainToHeadphoneForDiag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2LoPortGetStateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2ReportRegisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2ReportUnregisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetTvCorrectionInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetIpcHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetSystemStateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2SetDebugHeadphoneMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2DebugStateCtrl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2ContextBedWrite(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2PortCreateEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2Terminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2PortRegister(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2PortUnregister(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetAmbisonicsEncodeCoefficients(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOut2GetHrtfIdForCronos(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2DeviceState.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2DeviceState.cpp new file mode 100644 index 0000000000..404037d497 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2DeviceState.cpp @@ -0,0 +1,137 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam*); +int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); +int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle); +int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle, const AudioOut2PortParam*, AudioOut2PortHandle*); +int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle); +int APS5_VABI sceAudioOut2PortGetState(AudioOut2PortHandle, AudioOut2PortState*); +int APS5_VABI sceAudioOut2GetSpeakerInfo(AudioOut2SpeakerInfo*, std::uint32_t); +int APS5_VABI sceAudioOut2GetSystemState(AudioOut2SystemState*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +namespace { + +constexpr int errorInvalidArgument = static_cast(0x80260502); +constexpr int errorInvalidHandle = static_cast(0x80260503); +constexpr std::uint16_t portTypeMain = 0; +constexpr std::uint16_t outputMain = 1; +constexpr std::int16_t volumeMax = 127; +constexpr std::uint32_t formatFloat = 0; +constexpr std::uint32_t formatS16 = 1; +constexpr std::uint32_t frequency = 48000; + +std::uint32_t Format(std::uint32_t channels, std::uint32_t type) { + return channels << 8 | type; +} + +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 +} + +AudioOut2PortState StateOfPort(AudioOut2ContextHandle context, std::uint32_t format) { + AudioOut2PortParam params{}; + params.port_type = portTypeMain; + params.data_format = format; + params.sampling_freq = frequency; + AudioOut2PortHandle port = 0; + Require(sceAudioOut2PortCreate(context, ¶ms, &port) == 0); + AudioOut2PortState state; + std::memset(&state, 0xAB, sizeof(state)); + Require(sceAudioOut2PortGetState(port, &state) == 0); + Require(sceAudioOut2PortDestroy(port) == 0); + return state; +} + +void PortStateFollowsTheDataFormat(AudioOut2ContextHandle context) { + struct Case { + std::uint32_t channels; + std::uint32_t sampleType; + }; + for (const Case entry : {Case{1, formatFloat}, Case{2, formatFloat}, Case{2, formatS16}, Case{8, formatFloat}, Case{12, formatS16}}) { + const auto state = StateOfPort(context, Format(entry.channels, entry.sampleType)); + Require(state.output == outputMain); + Require(state.num_channels == entry.channels); + Require(state.volume == volumeMax); + Require(state.reroute_counter == 0); + Require(state.flags == 0); + Require(state.pad1 == 0 && state.pad2 == 0); + for (const auto word : state.reserved) Require(word == 0); + } +} + +void PortStateRejectsBadArguments(AudioOut2ContextHandle context) { + AudioOut2PortState state{}; + Require(sceAudioOut2PortGetState(0, &state) == errorInvalidHandle); + Require(sceAudioOut2PortGetState(0x7fff, &state) == errorInvalidHandle); + + AudioOut2PortParam params{}; + params.port_type = portTypeMain; + params.data_format = Format(2, formatFloat); + params.sampling_freq = frequency; + AudioOut2PortHandle port = 0; + Require(sceAudioOut2PortCreate(context, ¶ms, &port) == 0); + Require(sceAudioOut2PortGetState(port, nullptr) == errorInvalidArgument); + Require(sceAudioOut2PortDestroy(port) == 0); + Require(sceAudioOut2PortGetState(port, &state) == errorInvalidHandle); +} + +void SpeakerInfoReportsTheFrontPair() { + AudioOut2SpeakerInfo info; + std::memset(&info, 0xAB, sizeof(info)); + Require(sceAudioOut2GetSpeakerInfo(&info, 0) == 0); + Require(info.type == 0); + Require(info.available_bits == 0x3); + Require(info.flags == 0); + Require(info.speaker_angle[0].azimuth == 30 && info.speaker_angle[0].elevation == 0); + Require(info.speaker_angle[1].azimuth == -30 && info.speaker_angle[1].elevation == 0); + for (int index = 2; index < 16; index++) Require(info.speaker_angle[index].azimuth == -360 && info.speaker_angle[index].elevation == -360); + Require(sceAudioOut2GetSpeakerInfo(nullptr, 0) == errorInvalidArgument); +} + +void SystemStateIsSilentAndRejectsNull() { + AudioOut2SystemState state; + std::memset(&state, 0xAB, sizeof(state)); + Require(sceAudioOut2GetSystemState(&state) == 0); + Require(state.loudness == 0.0f); + Require(sceAudioOut2GetSystemState(nullptr) == errorInvalidArgument); +} + +} + +int main() { + const auto path = std::filesystem::temp_directory_path() / ("anyps5_audio_out2_device_state-" + std::to_string(std::random_device{}()) + ".raw"); + std::filesystem::remove(path); + SetEnvironment("SDL_DISKAUDIOFILE", path.string()); + + AudioOut2ContextParam params{}; + Require(sceAudioOut2ContextResetParam(¶ms) == 0); + params.num_grains = 256; + params.queue_depth = 1; + AudioOut2ContextHandle context = 0; + Require(sceAudioOut2ContextCreate(¶ms, nullptr, 0, &context) == 0); + + PortStateFollowsTheDataFormat(context); + PortStateRejectsBadArguments(context); + SpeakerInfoReportsTheFrontPair(); + SystemStateIsSilentAndRejectsNull(); + + Require(sceAudioOut2ContextDestroy(context) == 0); + std::filesystem::remove(path); + return 0; +} diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp index b91b5d29df..09822f5c74 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp @@ -1,5 +1,7 @@ +#include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -7,10 +9,16 @@ extern "C" { int APS5_VABI sceAudioOut2Initialize(); +int APS5_VABI sceAudioOut2InitializeEx(std::uint32_t); +int APS5_VABI sceAudioOut2LoInit(); +int APS5_VABI sceAudioOut2LoTerminate(); int APS5_VABI sceAudioOut2Set3DLatency(int, std::uint32_t); int APS5_VABI sceAudioOut2MasteringInit(std::uint32_t); +int APS5_VABI sceAudioOut2MasteringInitEx(std::uint32_t); int APS5_VABI sceAudioOut2MasteringTerm(); int APS5_VABI sceAudioOut2MasteringSetParam(const void*, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioOut2MasteringGetState(void*, std::uint32_t, std::uint64_t); +int APS5_VABI sceAudioOut2SetSystemDebugState(const void*); int APS5_VABI sceAudioOut2EnableChat(); int APS5_VABI sceAudioOut2DisableChat(); } @@ -33,21 +41,39 @@ static bool ThrowsRuntimeError(TFunction function) { namespace { +static_assert(sizeof(AudioOut2MasteringCompressorStates) == 208); +static_assert(sizeof(AudioOut2MasteringCompressorStatesV2) == 272); +static_assert(sizeof(AudioOut2MasteringLimiterStates) == 112); +static_assert(sizeof(AudioOut2MasteringStates) == 336); +static_assert(sizeof(AudioOut2MasteringStatesV2) == 400); + constexpr int systemUser = 0xFF; constexpr int user = 0x10000000; +constexpr int errorInvalidParam = static_cast(0x80268001); +constexpr int errorInvalidPointer = static_cast(0x8026800C); +constexpr int errorMasteringInvalidApiParam = static_cast(0x80268201); +constexpr int errorMasteringInvalidStatesId = static_cast(0x80268204); void TestSet3DLatency() { - Require(sceAudioOut2Set3DLatency(systemUser, 2) == 0, "latency 2 for the system user must be accepted"); - Require(sceAudioOut2Set3DLatency(systemUser, 2) == 0, "latency 2 must be accepted again"); - Require(sceAudioOut2Set3DLatency(systemUser, 1) == 0, "latency 1 for the system user must be accepted"); - Require(ThrowsRuntimeError([] { sceAudioOut2Set3DLatency(systemUser, 0); }), "latency 0 must throw"); - Require(ThrowsRuntimeError([] { sceAudioOut2Set3DLatency(systemUser, 3); }), "latency 3 must throw"); - Require(ThrowsRuntimeError([] { sceAudioOut2Set3DLatency(user, 2); }), "a user other than the system user must throw"); + Require(sceAudioOut2Set3DLatency(systemUser, 0) == errorInvalidParam, "output 0 for selector 255 must be refused"); + for (std::uint32_t output = 0; output <= 2; ++output) { + Require(sceAudioOut2Set3DLatency(0, output) == 0, "selector 0 must accept each supported output"); + } + Require(sceAudioOut2Set3DLatency(systemUser, 1) == 0, "output 1 for the system user must be accepted"); + Require(sceAudioOut2Set3DLatency(systemUser, 2) == 0, "output 2 for the system user must be accepted"); + Require(sceAudioOut2Set3DLatency(systemUser, 2) == 0, "output 2 must be accepted again"); + Require(sceAudioOut2Set3DLatency(user, 1) == errorInvalidParam, "a user id is not a valid latency selector"); + Require(sceAudioOut2Set3DLatency(systemUser, 3) == errorInvalidParam, "output 3 must be refused"); + Require(sceAudioOut2Set3DLatency(systemUser, 0xFFFFFFFFu) == errorInvalidParam, "a large output must be refused"); } void TestMasteringInit() { Require(sceAudioOut2MasteringInit(0) == 0, "flags 0 must be accepted"); Require(ThrowsRuntimeError([] { sceAudioOut2MasteringInit(1); }), "flags 1 must throw"); + Require(sceAudioOut2MasteringInitEx(0) == 0, "InitEx flags 0 must be accepted"); + Require(sceAudioOut2MasteringInitEx(0x80000000u) == 0, "InitEx flag bit 31 must be accepted"); + Require(ThrowsRuntimeError([] { sceAudioOut2MasteringInitEx(1); }), "InitEx flag bit 0 must throw"); + Require(ThrowsRuntimeError([] { sceAudioOut2MasteringInitEx(0x40000000u); }), "InitEx flag bit 30 must throw"); } void TestMasteringTerm() { @@ -59,7 +85,54 @@ void TestMasteringTerm() { void TestMasteringSetParam() { const std::uint32_t params[4] = {1u, 0u, 0u, 0u}; Require(sceAudioOut2MasteringSetParam(params, 0, 0) == 0, "mastering parameters must be accepted"); - Require(ThrowsRuntimeError([] { sceAudioOut2MasteringSetParam(nullptr, 0, 0); }), "null mastering parameters must throw"); + Require(sceAudioOut2MasteringSetParam(params, 2, 0) == 0, "mastering parameters for output 2 must be accepted"); + Require(sceAudioOut2MasteringSetParam(nullptr, 0, 0) == errorMasteringInvalidApiParam, "null mastering parameters return the invalid API parameter error"); +} + +void TestMasteringGetState() { + alignas(16) std::uint8_t storage[512]; + Require(sceAudioOut2MasteringGetState(nullptr, 0, 0) == errorMasteringInvalidApiParam, "a null state returns the invalid API parameter error"); + auto* header = reinterpret_cast(storage); + auto* words = reinterpret_cast(storage); + *header = 1u; + Require(sceAudioOut2MasteringGetState(storage, 0, 0) == 0, "states id 1 must be accepted"); + Require(*header == 1u, "states id 1 is kept"); + Require(words[4] == 0x01010001u && words[5] == 8u + 8u + 3u * 8u * 4u * 2u, "the compressor states descriptor is filled"); + Require(words[56] == 0x01010003u && words[57] == 8u + 8u + 8u * 4u * 3u, "the limiter states descriptor is filled"); + std::fill(storage, storage + sizeof(storage), 0xA5); + *header = 2u; + Require(sceAudioOut2MasteringGetState(storage, 2, 1) == 0, "states id 2 must be accepted"); + Require(*header == 2u, "states id 2 is kept"); + Require(words[4] == 0x01020001u && words[5] == 8u + 8u + 4u * 8u * 4u * 2u, "the V2 compressor states descriptor is filled"); + Require(words[72] == 0x01010003u && words[73] == 8u + 8u + 8u * 4u * 3u, "the V2 limiter states descriptor is filled"); + Require(std::all_of(storage + 8, storage + 16, [](std::uint8_t byte) { return byte == 0; }), "the reserved words are zeroed"); + Require(std::all_of(storage + 24, storage + 288, [](std::uint8_t byte) { return byte == 0; }), "the V2 compressor states are zeroed"); + *header = 3u; + Require(sceAudioOut2MasteringGetState(storage, 0, 0) == errorMasteringInvalidStatesId, "an unknown states id is refused"); + *header = 0u; + Require(sceAudioOut2MasteringGetState(storage, 0, 0) == errorMasteringInvalidStatesId, "states id 0 is refused"); +} + +void TestSetSystemDebugState() { + std::uint32_t loudness[2] = {1, 0}; + const auto set = [&loudness](std::uint32_t id, void* data, std::size_t size) { + const AudioOut2SystemDebugStateParam param{id, 0, data, size}; + return sceAudioOut2SetSystemDebugState(¶m); + }; + Require(set(1, loudness, 8) == 0, "the loudness debug state must be accepted"); + Require(set(1, loudness, 8) == 0, "the loudness debug state must be accepted again"); + Require(set(1, loudness, 0) == errorInvalidParam, "a loudness debug state of size 0 is refused"); + Require(set(1, loudness, 4) == errorInvalidParam, "a loudness debug state of size 4 is refused"); + Require(set(1, loudness, 16) == errorInvalidParam, "a loudness debug state of size 16 is refused"); + Require(set(0, loudness, 8) == errorInvalidParam, "debug state id 0 is refused"); + Require(set(2, loudness, 8) == errorInvalidParam, "debug state id 2 is refused"); + Require(set(0x21, loudness, 8) == errorInvalidParam, "debug state id 0x21 is refused"); + Require(set(0xFFFFFFFFu, loudness, 8) == errorInvalidParam, "a large debug state id is refused"); + Require(set(0x20, loudness, 8) == errorInvalidParam, "debug state id 0x20 of size 8 is refused"); + Require(set(0x20, loudness, 0) == errorInvalidParam, "debug state id 0x20 of size 0 is refused"); + Require(ThrowsRuntimeError([&] { set(0x20, loudness, 4); }), "debug state id 0x20 of size 4 must throw"); + Require(ThrowsRuntimeError([&] { set(1, nullptr, 8); }), "a loudness debug state without a param must throw"); + Require(sceAudioOut2SetSystemDebugState(nullptr) == errorInvalidPointer, "a null debug state is refused"); } void TestEnableChat() { @@ -70,6 +143,14 @@ void TestEnableChat() { Require(sceAudioOut2EnableChat() == 0, "enabling chat after disabling it must be accepted"); } +void TestLowLatencyService() { + Require(sceAudioOut2LoInit() == 0, "starting the low-latency service must be accepted"); + Require(sceAudioOut2LoInit() == 0, "starting the low-latency service again must be accepted"); + Require(sceAudioOut2LoTerminate() == 0, "stopping the low-latency service returns 0"); + Require(sceAudioOut2LoTerminate() == 0, "stopping a stopped low-latency service returns 0"); + Require(sceAudioOut2InitializeEx(0) == 0 && sceAudioOut2InitializeEx(1) == 0, "the extended initialization succeeds"); +} + } int main() { @@ -78,6 +159,9 @@ int main() { TestMasteringInit(); TestMasteringTerm(); TestMasteringSetParam(); + TestMasteringGetState(); + TestSetSystemDebugState(); TestEnableChat(); + TestLowLatencyService(); return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2PadMix.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2PadMix.cpp index a7e6f32527..5c68579719 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2PadMix.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2PadMix.cpp @@ -29,6 +29,14 @@ static void Accumulate(AudioOut2Route route, const float* data, std::uint32_t ch } int main() { + { + const float pad[] = {1.0f, -1.0f, 0.25f, -0.5f, 1.0f, -1.0f, -0.25f, 0.5f}; + const auto rumble = AudioOut2RumbleFromPadFrames(pad, 2); + Require(rumble[0] == 128 && rumble[1] == 255, "haptic energy drives separate motors, not speaker samples"); + const float speakersOnly[] = {1.0f, 1.0f, 0.0f, 0.0f}; + Require(AudioOut2RumbleFromPadFrames(speakersOnly, 1) == std::array{}, "speaker sound does not rumble"); + Require(AudioOut2RumbleFromPadFrames(nullptr, 0) == std::array{}, "empty input does not rumble"); + } Require(AudioOut2RouteForPort(0x3, 1) == AudioOut2Route::PadSpeaker, "mono type 0x3 is the pad speaker"); Require(AudioOut2RouteForPort(0x3, 2) == AudioOut2Route::PadSpeaker, "stereo type 0x3 is the pad speaker"); Require(AudioOut2RouteForPort(0x6, 2) == AudioOut2Route::PadVibration, "stereo type 0x6 is the vibration"); diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp index 54f467b334..b1d5725eca 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp @@ -1,5 +1,8 @@ #include "SceTypes.hpp" +#include +#include +#include #include #include #include @@ -7,15 +10,19 @@ #include #include #include +#include #include #include #include +#include #include extern "C" { 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); @@ -49,7 +56,7 @@ void SetData(AudioOut2PortHandle port, const void* data) { Require(sceAudioOut2PortSetAttributes(port, &attribute, 1) == 0); } -std::vector Play(const std::function& feed) { +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()); @@ -57,7 +64,7 @@ std::vector Play(const std::function Play(const std::function volume{0.5f, 0.5f}; + const AudioOut2Attribute attribute{1, 0, volume.data(), sizeof(volume)}; + Require(sceAudioOut2PortSetAttributes(port, &attribute, 1) == 0); + SetData(port, buffer.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + }, true); + RequirePlayed(played, {&buffer, &reduced}); +} + +void TestAdvanceBurstRetainsEveryGrain() { + std::vector> 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); + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + SetData(port, first.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + std::promise entered; + std::promise done; + auto completed = done.get_future(); + std::thread pusher([&] { + entered.set_value(); + done.set_value(sceAudioOut2ContextPush(context, 1)); + }); + entered.get_future().wait(); + const bool waited = completed.wait_for(std::chrono::milliseconds(10)) == std::future_status::timeout; + SetData(port, second.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + Require(completed.get() == 0); + pusher.join(); + Require(waited); + }, true); + 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/AudioOut2PortLayouts.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortLayouts.cpp index 1cad3a7422..7fcfa50334 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortLayouts.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortLayouts.cpp @@ -77,11 +77,17 @@ void TestUserAttributes() { } Require(threw && context.value == 0xFFFFFFFFu && port.value == 0xFFFFFFFFu); }; - rejects(first, true, false); - rejects(first, false, true); - rejects(first, true, true); - rejects(0, false, false); - rejects(std::numeric_limits::max(), false, false); + context.value = port.value = 0xFFFFFFFFu; + Require(sceAudioOut2UserGetSupportedAttributes(first, nullptr, &port.value) == 0); + Require(context.value == 0xFFFFFFFFu && port.value == 3); + Require(sceAudioOut2UserGetSupportedAttributes(first, &context.value, nullptr) == 0 && context.value == 0); + context.value = port.value = 0xFFFFFFFFu; + Require(sceAudioOut2UserGetSupportedAttributes(first, nullptr, nullptr) == static_cast(0x80268001)); + for (const AudioOut2UserHandle handle : {AudioOut2UserHandle{0}, std::numeric_limits::max()}) { + Require(sceAudioOut2UserGetSupportedAttributes(handle, &context.value, &port.value) == static_cast(0x80268010)); + Require(sceAudioOut2UserGetSupportedAttributes(handle, nullptr, nullptr) == static_cast(0x80268010)); + Require(context.value == 0xFFFFFFFFu && port.value == 0xFFFFFFFFu); + } Require(sceAudioOut2UserDestroy(first) == 0); rejects(first, false, false); std::thread([&] { diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp index c16ffebbfb..36074daece 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,7 @@ extern "C" { int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam*); +int APS5_VABI sceAudioOut2ContextQueryMemory(const AudioOut2ContextParam*, std::size_t*); int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle); int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle, std::uint32_t); @@ -22,6 +24,17 @@ int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle, std::uint int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle, const AudioOut2PortParam*, AudioOut2PortHandle*); int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle); int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle, const AudioOut2Attribute*, std::uint32_t); +int APS5_VABI sceAudioOut2LoContextQueryMemory(const AudioOut2ContextParam*, std::size_t*); +int APS5_VABI sceAudioOut2LoContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); +int APS5_VABI sceAudioOut2LoContextDestroy(AudioOut2ContextHandle); +int APS5_VABI sceAudioOut2LoContextSetAttributes(AudioOut2ContextHandle, const AudioOut2Attribute*, std::uint32_t); +int APS5_VABI sceAudioOut2LoContextAdvance(AudioOut2ContextHandle); +int APS5_VABI sceAudioOut2LoContextPush(AudioOut2ContextHandle, std::uint32_t); +int APS5_VABI sceAudioOut2LoContextGetQueueLevel(AudioOut2ContextHandle, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceAudioOut2LoPortCreate(AudioOut2ContextHandle, const AudioOut2PortParam*, AudioOut2PortHandle*); +int APS5_VABI sceAudioOut2LoPortDestroy(AudioOut2PortHandle); +int APS5_VABI sceAudioOut2LoPortSetAttributes(AudioOut2PortHandle, const AudioOut2Attribute*, std::uint32_t); +int APS5_VABI sceAudioOut2LoPortGetState(AudioOut2PortHandle, AudioOut2PortState*); } static void Require(bool value) { if (!value) std::abort(); } @@ -44,6 +57,8 @@ constexpr std::uint32_t formatMonoFloat = 1u << 8; constexpr std::uint32_t formatStereoFloat = 2u << 8; constexpr std::uint32_t attributeData = 0; constexpr int queueFull = static_cast(0x80260507); +constexpr int errorInvalidArgument = static_cast(0x80260502); +constexpr int errorInvalidHandle = static_cast(0x80260503); constexpr float masterGain = 0.5f; constexpr float tolerance = 1e-5f; @@ -53,24 +68,26 @@ struct Session { AudioOut2PortHandle port = 0; std::vector buffer = std::vector(static_cast(grain) * 2, 0.0f); - explicit Session(const char* name) : path(std::filesystem::temp_directory_path() / (std::to_string(std::random_device{}()) + "-" + name)) { + bool low; + + explicit Session(const char* name, bool lowLatency = false) : path(std::filesystem::temp_directory_path() / (std::to_string(std::random_device{}()) + "-" + name)), low(lowLatency) { std::filesystem::remove(path); SetEnvironment("SDL_DISKAUDIOFILE", path.string()); AudioOut2ContextParam params{}; Require(sceAudioOut2ContextResetParam(¶ms) == 0); params.num_grains = grain; params.queue_depth = queueDepth; - Require(sceAudioOut2ContextCreate(¶ms, nullptr, 0, &context) == 0); + Require((low ? sceAudioOut2LoContextCreate : sceAudioOut2ContextCreate)(¶ms, nullptr, 0, &context) == 0); AudioOut2PortParam port{}; port.port_type = 0; port.data_format = formatStereoFloat; port.sampling_freq = frequency; - Require(sceAudioOut2PortCreate(context, &port, &this->port) == 0); + Require((low ? sceAudioOut2LoPortCreate : sceAudioOut2PortCreate)(context, &port, &this->port) == 0); } void Point(const void* data) { const AudioOut2Attribute attribute{attributeData, 0, &data, sizeof(data)}; - Require(sceAudioOut2PortSetAttributes(port, &attribute, 1) == 0); + Require((low ? sceAudioOut2LoPortSetAttributes : sceAudioOut2PortSetAttributes)(port, &attribute, 1) == 0); } void Fill(float value) { @@ -78,8 +95,8 @@ struct Session { } std::vector Close() { - Require(sceAudioOut2PortDestroy(port) == 0); - Require(sceAudioOut2ContextDestroy(context) == 0); + Require((low ? sceAudioOut2LoPortDestroy : sceAudioOut2PortDestroy)(port) == 0); + Require((low ? sceAudioOut2LoContextDestroy : sceAudioOut2ContextDestroy)(context) == 0); std::ifstream file(path, std::ios::binary); const std::vector bytes{std::istreambuf_iterator(file), std::istreambuf_iterator()}; file.close(); @@ -188,6 +205,132 @@ void TestPrimingAfterRunningDry() { Require(Constant(played, second, burst, -0.5f * masterGain)); } +void TestLowLatencyCallsRejectBadArguments() { + AudioOut2ContextParam params{}; + Require(sceAudioOut2ContextResetParam(¶ms) == 0); + params.num_grains = grain; + params.queue_depth = queueDepth; + const std::size_t sizeSentinel = 0x1234; + std::size_t size = sizeSentinel; + Require(sceAudioOut2LoContextQueryMemory(nullptr, &size) == errorInvalidArgument); + Require(sceAudioOut2LoContextQueryMemory(¶ms, nullptr) == errorInvalidArgument); + Require(size == sizeSentinel); + Require(sceAudioOut2LoContextQueryMemory(¶ms, &size) == 0); + std::size_t standardSize = sizeSentinel; + Require(sceAudioOut2ContextQueryMemory(¶ms, &standardSize) == 0); + Require(size == standardSize && size != sizeSentinel); + + const AudioOut2ContextHandle contextSentinel = 0x5A5A; + AudioOut2ContextHandle context = contextSentinel; + Require(sceAudioOut2LoContextCreate(nullptr, nullptr, 0, &context) == errorInvalidArgument); + Require(context == contextSentinel); + Require(sceAudioOut2LoContextCreate(¶ms, nullptr, 0, nullptr) == errorInvalidArgument); + + Require(sceAudioOut2LoContextDestroy(0) == errorInvalidHandle); + Require(sceAudioOut2LoContextAdvance(0) == errorInvalidHandle); + Require(sceAudioOut2LoContextPush(0, 0) == errorInvalidHandle); + Require(sceAudioOut2LoContextPush(0, 1) == errorInvalidHandle); + std::uint32_t level = 77; + std::uint32_t available = 78; + Require(sceAudioOut2LoContextGetQueueLevel(0, &level, &available) == errorInvalidHandle); + Require(level == 77 && available == 78); + const AudioOut2Attribute attribute{0, 0, nullptr, 0}; + Require(sceAudioOut2LoContextSetAttributes(0, &attribute, 1) == errorInvalidHandle); + + AudioOut2PortParam portParams{}; + portParams.port_type = 0; + portParams.data_format = formatStereoFloat; + portParams.sampling_freq = frequency; + const AudioOut2PortHandle portSentinel = 0x6B6B; + AudioOut2PortHandle port = portSentinel; + Require(sceAudioOut2LoPortCreate(0, &portParams, &port) == errorInvalidHandle); + Require(port == portSentinel); + Require(sceAudioOut2LoPortDestroy(0) == errorInvalidHandle); + Require(sceAudioOut2LoPortSetAttributes(0, &attribute, 1) == errorInvalidHandle); + AudioOut2PortState state; + Require(sceAudioOut2LoPortGetState(0, &state) == errorInvalidHandle); + + Session session("anyps5_audio_out2_low_arguments.raw", true); + Require(sceAudioOut2LoPortCreate(session.context, nullptr, &port) == errorInvalidArgument); + Require(port == portSentinel); + Require(sceAudioOut2LoPortCreate(session.context, &portParams, nullptr) == errorInvalidArgument); + Require(sceAudioOut2LoContextSetAttributes(session.context, nullptr, 1) == errorInvalidArgument); + Require(sceAudioOut2LoContextSetAttributes(session.context, nullptr, 0) == 0); + Require(sceAudioOut2LoContextSetAttributes(session.context, &attribute, 1) == 0); + Require(sceAudioOut2LoPortSetAttributes(session.port, nullptr, 1) == errorInvalidArgument); + Require(sceAudioOut2LoPortSetAttributes(session.port, nullptr, 0) == 0); + Require(sceAudioOut2LoPortGetState(session.port, nullptr) == errorInvalidArgument); + Require(sceAudioOut2LoContextGetQueueLevel(session.context, nullptr, nullptr) == 0); + std::memset(&state, 0xAB, sizeof(state)); + Require(sceAudioOut2LoPortGetState(session.port, &state) == 0); + Require(state.output == 1 && state.num_channels == 2 && state.volume == 127 && state.reroute_counter == 0 && state.flags == 0); + const auto destroyed = session.port; + Require(sceAudioOut2LoPortDestroy(destroyed) == 0); + Require(sceAudioOut2LoPortDestroy(destroyed) == errorInvalidHandle); + Require(sceAudioOut2LoPortGetState(destroyed, &state) == errorInvalidHandle); + Require(sceAudioOut2LoPortSetAttributes(destroyed, &attribute, 1) == errorInvalidHandle); + Require(sceAudioOut2LoContextDestroy(session.context) == 0); + std::filesystem::remove(session.path); +} + +void TestLowLatencyReadsAtNextPush() { + Session session("anyps5_audio_out2_low_next_push.raw", true); + session.Fill(0.25f); + session.Point(session.buffer.data()); + Require(sceAudioOut2LoContextPush(session.context, 1) == 0); + session.Fill(0.5f); + Require(sceAudioOut2LoContextPush(session.context, 1) == 0); + session.Fill(0.75f); + Require(sceAudioOut2LoContextPush(session.context, 1) == 0); + session.Fill(0.0f); + session.Point(nullptr); + for (std::uint32_t push = 0; push < cushionGrains * 4; push++) Require(sceAudioOut2LoContextPush(session.context, 1) == 0); + const auto played = session.Close(); + const auto first = FirstNonZero(played); + const std::size_t samples = static_cast(grain) * 2; + Require(Constant(played, first, samples, 0.5f * masterGain)); + Require(Constant(played, first + samples, samples, 0.75f * masterGain)); + Require(FirstNonZero(played, first + 2 * samples) == played.size()); +} + +void TestLowLatencyAdvanceKeepsTheAdvancedGrain() { + Session session("anyps5_audio_out2_low_advance.raw", true); + session.Fill(0.25f); + session.Point(session.buffer.data()); + Require(sceAudioOut2LoContextAdvance(session.context) == 0); + session.Fill(0.75f); + session.Point(nullptr); + for (std::uint32_t push = 0; push < cushionGrains * 4; push++) { + Require(sceAudioOut2LoContextPush(session.context, 1) == 0); + Require(sceAudioOut2LoContextAdvance(session.context) == 0); + } + const auto played = session.Close(); + const auto first = FirstNonZero(played); + const std::size_t samples = static_cast(grain) * 2; + Require(Constant(played, first, samples, 0.25f * masterGain)); + Require(FirstNonZero(played, first + samples) == played.size()); +} + +void TestLowLatencyQueueLevelFillsToTheDepth() { + Session session("anyps5_audio_out2_low_level.raw", true); + session.Point(nullptr); + std::uint32_t level = 99; + std::uint32_t available = 99; + Require(sceAudioOut2LoContextGetQueueLevel(session.context, &level, &available) == 0); + Require(level == 0 && available == queueDepth); + std::uint32_t accepted = 0; + int result = 0; + while ((result = sceAudioOut2LoContextPush(session.context, 0)) == 0) { + accepted++; + Require(accepted < 1000); + } + Require(result == queueFull); + Require(accepted >= cushionGrains + queueDepth); + Require(sceAudioOut2LoContextGetQueueLevel(session.context, &level, &available) == 0); + Require(level <= queueDepth && level + available == queueDepth); + session.Close(); +} + } int main() { @@ -196,5 +339,9 @@ int main() { TestRecreatedPort(); TestLevelExcludesCushion(); TestPrimingAfterRunningDry(); + TestLowLatencyCallsRejectBadArguments(); + TestLowLatencyReadsAtNextPush(); + TestLowLatencyAdvanceKeepsTheAdvancedGrain(); + TestLowLatencyQueueLevelFillsToTheDepth(); return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutBufferSize.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutBufferSize.cpp new file mode 100644 index 0000000000..d51eda399a --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutBufferSize.cpp @@ -0,0 +1,100 @@ +#include "SDL.h" +#include "prx/libSceAudioOut/include/AudioOutSdl.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#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 sceAudioOutClose(int); +int APS5_VABI sceAudioOutOutput(int, const void*); +} + +namespace { +constexpr SDL_AudioDeviceID device = 7; +int opened = 0; +int closed = 0; +int queued = 0; +std::uint32_t queuedBytes = 0; +const void* supplied = nullptr; +std::uint32_t suppliedBytes = 0; + +void Require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +void RunPassThrough() { + std::vector samples(512u * 8u); + for (std::size_t i = 0; i < samples.size(); ++i) samples[i] = static_cast(i % 97u) / 97.0f; + const int handle = sceAudioOutOpen(0, 0, 0, 512, 48000, 5); + Require(handle > 0 && opened == 1, "a 512-frame F32 8-channel port did not open its device"); + supplied = samples.data(); + suppliedBytes = static_cast(samples.size() * sizeof(float)); + Require(sceAudioOutOutput(handle, samples.data()) == 512, "a 512-frame output did not return 512"); + Require(queued == 1 && queuedBytes == 16384, "a 512-frame F32 8-channel output did not queue 16384 bytes"); + Require(sceAudioOutClose(handle) == 0 && closed == 1, "the port did not close its device"); +} + +void RunOverflow(std::uint32_t frames, std::uint32_t format, std::uint32_t frameBytes) { + std::vector frame(frameBytes); + const int handle = sceAudioOutOpen(0, 0, 0, frames, 48000, format); + Require(handle > 0 && opened == 1, "a port with an oversized length did not open its device"); + supplied = frame.data(); + suppliedBytes = frameBytes; + bool threw = false; + try { + static_cast(sceAudioOutOutput(handle, frame.data())); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "an output whose buffer size overflows did not throw"); + Require(queued == 0, "an output whose buffer size overflows queued audio"); + Require(sceAudioOutClose(handle) == 0 && closed == 1, "the port did not close after the refused output"); +} +} + +int InitializeSdlAudio_nid_no_patch() { return 0; } +extern "C" Uint32 SDLCALL SDL_WasInit(Uint32) { return SDL_INIT_AUDIO; } +extern "C" int SDLCALL SDL_GetNumAudioDevices(int) { return 1; } +extern "C" const char* SDLCALL SDL_GetAudioDeviceName(int, int) { return "Main speakers"; } +extern "C" int SDLCALL SDL_GetAudioDeviceSpec(int, int, SDL_AudioSpec* spec) { *spec = {}; spec->channels = 2; return 0; } +extern "C" const char* SDLCALL SDL_GetCurrentAudioDriver() { return "dummy"; } +extern "C" const char* SDLCALL SDL_GetError() { return "test error"; } +extern "C" SDL_AudioDeviceID SDLCALL SDL_OpenAudioDevice(const char* name, int capture, const SDL_AudioSpec* desired, SDL_AudioSpec* obtained, int) { + Require(name == nullptr && capture == 0 && desired->callback == nullptr, "the main port did not open the default device"); + ++opened; + *obtained = *desired; + return device; +} +extern "C" void SDLCALL SDL_PauseAudioDevice(SDL_AudioDeviceID id, int) { Require(id == device, "wrong device paused"); } +extern "C" Uint32 SDLCALL SDL_GetQueuedAudioSize(SDL_AudioDeviceID id) { Require(id == device, "wrong queue queried"); return 0; } +extern "C" int SDLCALL SDL_QueueAudio(SDL_AudioDeviceID id, const void* data, Uint32 bytes) { + Require(id == device, "wrong queue fed"); + ++queued; + queuedBytes = bytes; + if (bytes <= suppliedBytes) Require(std::memcmp(data, supplied, bytes) == 0, "the queued audio is not the supplied samples"); + return 0; +} +extern "C" void SDLCALL SDL_ClearQueuedAudio(SDL_AudioDeviceID id) { Require(id == device, "wrong queue cleared"); } +extern "C" void SDLCALL SDL_CloseAudioDevice(SDL_AudioDeviceID id) { Require(id == device, "wrong device closed"); ++closed; } + +int main(int argc, char** argv) { + try { + Require(argc == 2, "invalid test arguments"); + const std::string_view mode = argv[1]; + if (mode == "pass_through") RunPassThrough(); + else if (mode == "wrap") RunOverflow(0x80000000u, 5, 8u * sizeof(float)); + else if (mode == "mix") RunOverflow(0x80000000u, 7, 8u * sizeof(float)); + else if (mode == "threshold") RunOverflow(0x40000000u, 0, sizeof(std::int16_t)); + else Require(false, "invalid buffer size mode"); + std::puts("audio out buffer size tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp index 22f70c9128..b96f9dd570 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp @@ -1,32 +1,66 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include -#include +#include extern "C" { -int APS5_VABI sceAudioOutExClose(); -int APS5_VABI sceAudioOutExConfigureOutput(); -int APS5_VABI sceAudioOutExOpen(); +int APS5_VABI sceAudioOutExOpen(int userId, std::uint32_t mode); +int APS5_VABI sceAudioOutExConfigureOutput(int control, std::uint64_t flags, std::uint32_t mode, std::uint32_t target, std::uint64_t option); +int APS5_VABI sceAudioOutExClose(int handle); +int APS5_VABI sceAudioOutOutput(int handle, const void* ptr); } -using ExFunction = int (APS5_VABI *)(); +static constexpr int SYSTEM_USER = 0xFF; +static constexpr int ERROR_EX_INVALID_MODE = static_cast(0x80260015u); +static constexpr int ERROR_EX_INVALID_CONTROL = static_cast(0x80268009u); -static bool ReportsNotImplemented(ExFunction function, const char* name) { +static bool failed = false; + +static void Require(bool condition, const char* what) { + if (!condition) { + std::fprintf(stderr, "failed: %s\n", what); + failed = true; + } +} + +template +static bool Throws(Call call) { 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; + call(); + } catch (const std::runtime_error&) { + return true; } - std::fprintf(stderr, "%s returned instead of reporting that it is not implemented\n", name); return false; } int main() { - bool passed = ReportsNotImplemented(sceAudioOutExClose, "sceAudioOutExClose"); - passed = ReportsNotImplemented(sceAudioOutExConfigureOutput, "sceAudioOutExConfigureOutput") && passed; - passed = ReportsNotImplemented(sceAudioOutExOpen, "sceAudioOutExOpen") && passed; - return passed ? 0 : 1; + Require(sceAudioOutExOpen(SYSTEM_USER, 5) == ERROR_EX_INVALID_MODE, "mode 5 (Linear PCM) is refused"); + Require(sceAudioOutExOpen(SYSTEM_USER, 7) == ERROR_EX_INVALID_MODE, "mode 7 is outside mask 0x61F"); + Require(sceAudioOutExOpen(SYSTEM_USER, 11) == ERROR_EX_INVALID_MODE, "mode 11 is above 10"); + + const int ac3 = sceAudioOutExOpen(SYSTEM_USER, 0); + Require(ac3 > 0, "mode 0 opens a port"); + const std::vector burst(256 * 2, 0x1234); + Require(sceAudioOutOutput(ac3, burst.data()) == 256, "a mode 0 port takes 256 frames"); + Require(sceAudioOutOutput(ac3, nullptr) == 256, "a mode 0 port drains"); + + const int eac3 = sceAudioOutExOpen(SYSTEM_USER, 4); + Require(eac3 > 0 && eac3 != ac3, "mode 4 opens a second port"); + const std::vector wide(1024 * 2, 0); + Require(sceAudioOutOutput(eac3, wide.data()) == 1024, "a mode 4 port takes 1024 frames"); + + Require(sceAudioOutExConfigureOutput(1, 0, 0, 1, 0) == ERROR_EX_INVALID_CONTROL, "a nonzero first argument is refused"); + Require(sceAudioOutExConfigureOutput(0, 0, 0, 1, 0) == 0, "mode 0 to HDMI"); + Require(sceAudioOutExConfigureOutput(0, 0, 0, 0xFF, 0) == 0, "mode 0 to the default target"); + Require(sceAudioOutExConfigureOutput(0, 0, 0xFF, 0xFF, 0) == 0, "the reset"); + Require(Throws([] { sceAudioOutExConfigureOutput(0, 0, 11, 1, 0); }), "an unknown mode throws"); + Require(Throws([] { sceAudioOutExConfigureOutput(0, 0, 0, 4, 0); }), "an unknown target throws"); + Require(Throws([] { sceAudioOutExConfigureOutput(0, 1, 0, 1, 0); }), "nonzero flags throw"); + + Require(sceAudioOutExClose(ac3) == 0, "close the mode 0 port"); + Require(Throws([ac3] { sceAudioOutExClose(ac3); }), "a closed port throws"); + Require(sceAudioOutExClose(eac3) == 0, "close the mode 4 port"); + return failed ? 1 : 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutExMonitorInfo.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutExMonitorInfo.cpp index fecdf065e1..6fcced4ffb 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutExMonitorInfo.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutExMonitorInfo.cpp @@ -5,8 +5,6 @@ #include #include #include -#include -#include extern "C" { int APS5_VABI sceAudioOutExGetMonitorInfo(int, int, void*, std::uint32_t); @@ -21,28 +19,16 @@ static void Require(bool value, const char* message) { namespace { constexpr std::uint32_t titleSize = 0x400; -constexpr std::uint32_t minimumSize = 0xA0; constexpr std::size_t descriptorCountOffset = 0x98; constexpr std::size_t guardSize = 16; constexpr std::uint8_t untouched = 0xA5; constexpr int invalidPointer = static_cast(0x80260004); -bool Throws(int type, int index, std::uint32_t size, const std::string& expected) { - std::array info{}; - try { - sceAudioOutExGetMonitorInfo(type, index, info.data(), size); - } catch (const std::runtime_error& error) { - if (std::string(error.what()) == expected) return true; - std::fprintf(stderr, "threw: %s\n", error.what()); - return false; - } - return false; -} - -void CheckEmpty(std::uint32_t size) { +void CheckEmpty(int index) { + constexpr auto size = titleSize; std::array buffer{}; buffer.fill(untouched); - Require(sceAudioOutExGetMonitorInfo(0, 0, buffer.data(), size) == 0, "the monitor query must succeed"); + Require(sceAudioOutExGetMonitorInfo(0, index, buffer.data(), size) == 0, "the monitor query must succeed"); for (std::uint32_t i = 0; i < size; i++) { Require(buffer[i] == 0, "the monitor info must describe a sink without audio descriptors"); } @@ -57,15 +43,14 @@ void CheckEmpty(std::uint32_t size) { } int main() { - CheckEmpty(titleSize); - CheckEmpty(minimumSize); - Require(sceAudioOutExGetMonitorInfo(0, 0, nullptr, titleSize) == invalidPointer, - "a null buffer must report an invalid pointer"); - Require(Throws(0, 0, minimumSize - 1, "sceAudioOutExGetMonitorInfo: size 159 not supported"), - "a buffer without room for the descriptor count must throw"); - Require(Throws(1, 0, titleSize, "sceAudioOutExGetMonitorInfo: monitor 1, 0 not supported"), - "a nonzero first argument must throw"); - Require(Throws(0, 1, titleSize, "sceAudioOutExGetMonitorInfo: monitor 0, 1 not supported"), - "a nonzero second argument must throw"); + for (const int index : {0, 1, -1, 100}) CheckEmpty(index); + std::array buffer; + buffer.fill(untouched); + for (const std::uint32_t size : {0u, 0xA0u, titleSize - 1, titleSize + 1, 0xffffffffu}) { + Require(sceAudioOutExGetMonitorInfo(0, 0, buffer.data(), size) == static_cast(0x80260006), "exact size required"); + Require(sceAudioOutExGetMonitorInfo(0, 0, nullptr, size) == invalidPointer, "pointer checked before size"); + Require(sceAudioOutExGetMonitorInfo(1, 0, nullptr, size) == static_cast(0x80260003), "type checked before pointer"); + for (const auto value : buffer) Require(value == untouched, "failure preserves output"); + } 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/AudioOutMixLevelPadSpk.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp index 9c7ac4e9af..ca25acb711 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp @@ -16,7 +16,7 @@ int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std:: int APS5_VABI sceAudioOutClose(int); int APS5_VABI sceAudioOutOutput(int, const void*); int APS5_VABI sceAudioOutSetVolume(int, std::uint32_t, int*); -int APS5_VABI sceAudioOutSetMixLevelPadSpk(int, int); +int APS5_VABI sceAudioOutSetMixLevelPadSpk(int, std::uint32_t); } static void Require(bool value) { if (!value) std::abort(); } @@ -153,7 +153,7 @@ void TestOtherPortType() { Require(Play(portTypeMain, formatS16Mono, block, [](int) {}) == block); Require(Play(portTypeMain, formatS16Mono, block, [](int handle) { Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == invalidPortType); - Require(sceAudioOutSetMixLevelPadSpk(handle, unity + 1) == invalidPortType); + Require(sceAudioOutSetMixLevelPadSpk(handle, unity + 1) == invalidMixLevel); }) == block); } @@ -164,7 +164,7 @@ void TestRejectedLevels() { closed = handle; Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == 0); Require(sceAudioOutSetMixLevelPadSpk(handle, unity + 1) == invalidMixLevel); - Require(ThrowsRuntimeError([handle] { sceAudioOutSetMixLevelPadSpk(handle, -1); })); + Require(sceAudioOutSetMixLevelPadSpk(handle, UINT32_MAX) == invalidMixLevel); Require(sceAudioOutSetMixLevelPadSpk(0, unity) == invalidPort); Require(sceAudioOutSetMixLevelPadSpk(handle + 1, unity) == invalidPort); }) == Scaled(block, unity / 2)); 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/libSceAudioOut/tests/AudioOutResampling.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutResampling.cpp new file mode 100644 index 0000000000..c56be14970 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutResampling.cpp @@ -0,0 +1,103 @@ +#include "SDL.h" +#include "prx/libc/include/general/VabiMacros.hpp" + +#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; +constexpr int hostRate = 48000; +SDL_AudioStream* conversion = nullptr; +std::vector played; + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +void Collect() { + const int bytes = SDL_AudioStreamAvailable(conversion); + Require(bytes >= 0 && bytes % static_cast(sizeof(float)) == 0, "invalid converted size"); + const auto first = played.size(); + played.resize(first + bytes / sizeof(float)); + if (bytes != 0) Require(SDL_AudioStreamGet(conversion, played.data() + first, bytes) == bytes, "stream read failed"); +} + +void TestBoundary(std::uint32_t sourceRate) { + played.clear(); + const int port = sceAudioOutOpen(0, 0, 0, grain, sourceRate, 4); + Require(port > 0, "port creation failed"); + std::vector data(grain * 2, 0.0f); + data[(grain - 1) * 2] = 0.5f; + data[(grain - 1) * 2 + 1] = 0.25f; + Require(sceAudioOutOutput(port, data.data()) == static_cast(grain), "first output failed"); + data.assign(grain * 2, 0.0f); + for (int block = 0; block < 8; block++) Require(sceAudioOutOutput(port, data.data()) == static_cast(grain), "trailing output failed"); + Require(sceAudioOutOutput(port, nullptr) == static_cast(grain), "drain failed"); + Require(sceAudioOutClose(port) == 0, "close failed"); + const auto boundary = static_cast(std::ceil(static_cast(grain) * hostRate / sourceRate)); + Require(played.size() / 2 > boundary + 16, "resampled tail missing"); + double tailEnergy = 0.0; + for (std::size_t frame = boundary; frame < boundary + 16; frame++) { + const float left = played[frame * 2]; + const float right = played[frame * 2 + 1]; + Require(std::isfinite(left) && std::isfinite(right), "nonfinite resampled output"); + Require(std::abs(left - 2.0f * right) < 1e-6f, "stereo channels changed during conversion"); + tailEnergy += static_cast(left) * left; + } + Require(tailEnergy > 1e-7, "resampling discarded the impulse response at the grain boundary"); +} + +} + +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" Uint32 SDLCALL SDL_GetQueuedAudioSize(SDL_AudioDeviceID) { return 0; } +extern "C" void SDLCALL SDL_ClearQueuedAudio(SDL_AudioDeviceID) {} + +extern "C" SDL_AudioDeviceID SDLCALL SDL_OpenAudioDevice(const char*, int, const SDL_AudioSpec* desired, SDL_AudioSpec* obtained, int changes) { + *obtained = *desired; + if ((changes & SDL_AUDIO_ALLOW_FREQUENCY_CHANGE) != 0) obtained->freq = hostRate; + else { + conversion = SDL_NewAudioStream(desired->format, desired->channels, desired->freq, AUDIO_F32SYS, 2, hostRate); + Require(conversion != nullptr, "host conversion stream creation failed"); + } + return 2; +} + +extern "C" int SDLCALL SDL_QueueAudio(SDL_AudioDeviceID, const void* data, Uint32 size) { + if (conversion != nullptr) { + if (SDL_AudioStreamPut(conversion, data, static_cast(size)) < 0) return -1; + Collect(); + } else { + const auto* samples = static_cast(data); + played.insert(played.end(), samples, samples + size / sizeof(float)); + } + return 0; +} + +extern "C" void SDLCALL SDL_CloseAudioDevice(SDL_AudioDeviceID) { + if (conversion == nullptr) return; + Require(SDL_AudioStreamFlush(conversion) == 0, "host conversion flush failed"); + Collect(); + SDL_FreeAudioStream(conversion); + conversion = nullptr; +} + +int main() { + TestBoundary(32000); + TestBoundary(44100); + return 0; +} diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutShutdown.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutShutdown.cpp new file mode 100644 index 0000000000..dd5390753d --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutShutdown.cpp @@ -0,0 +1,155 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam*); +int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); +int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle); +int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle, std::uint32_t); +int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle, const AudioOut2PortParam*, AudioOut2PortHandle*); +int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle, const AudioOut2Attribute*, std::uint32_t); +int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioOutOutput(int, const void*); +} + +namespace { + +constexpr std::uint32_t grain = 256; +constexpr std::uint32_t frequency = 48000; +constexpr std::uint16_t portTypeMain = 0; +constexpr std::uint32_t attributeData = 0; +constexpr std::uint32_t formatStereoFloat = 2u << 8; +constexpr int user = 0x10000000; +constexpr std::uint32_t formatS16Stereo = 1; +constexpr std::uint64_t startedOutputs = 3; +constexpr auto settle = std::chrono::milliseconds(100); +constexpr auto exitLimit = std::chrono::seconds(3); + +std::atomic outputs{0}; +std::vector pcm(static_cast(grain) * 2, 0.25f); + +void Fail(const char* message) { + std::fprintf(stderr, "%s\n", message); + std::fflush(stderr); + std::_Exit(1); +} + +void RequireParked() { + const auto before = outputs.load(); + std::this_thread::sleep_for(settle); + if (outputs.load() != before) Fail("the mixer thread kept outputting audio after the audio shutdown"); + std::puts("the mixer thread was parked before the audio tables were destroyed"); + std::fflush(stdout); +} + +void DelayExit() { + std::this_thread::sleep_for(settle); +} + +void ReportExit() { + std::puts("the exit finished while a push waited for an advance"); + std::fflush(stdout); +} + +void SetData(AudioOut2PortHandle port) { + const void* pointer = pcm.data(); + const AudioOut2Attribute attribute{attributeData, 0, &pointer, sizeof(pointer)}; + if (sceAudioOut2PortSetAttributes(port, &attribute, 1) != 0) Fail("a port attribute set returned an error"); +} + +void CreatePort(AudioOut2ContextHandle& context, AudioOut2PortHandle& port) { + AudioOut2ContextParam params{}; + if (sceAudioOut2ContextResetParam(¶ms) != 0) Fail("context parameters were not reset"); + params.num_grains = grain; + if (sceAudioOut2ContextCreate(¶ms, nullptr, 0, &context) != 0) Fail("context was not created"); + AudioOut2PortParam portParams{}; + portParams.port_type = portTypeMain; + portParams.data_format = formatStereoFloat; + portParams.sampling_freq = frequency; + if (sceAudioOut2PortCreate(context, &portParams, &port) != 0) Fail("port was not created"); +} + +void PushAudioOut2() { + AudioOut2ContextHandle context = 0; + AudioOut2PortHandle port = 0; + CreatePort(context, port); + std::thread([context, port] { + for (;;) { + SetData(port); + if (sceAudioOut2ContextPush(context, 1) != 0) Fail("a push during exit returned an error"); + outputs++; + } + }).detach(); +} + +void WaitInPushAudioOut2() { + AudioOut2ContextHandle context = 0; + AudioOut2PortHandle port = 0; + CreatePort(context, port); + SetData(port); + if (sceAudioOut2ContextAdvance(context) != 0) Fail("an advance returned an error"); + if (sceAudioOut2ContextPush(context, 1) != 0) Fail("a push returned an error"); + std::thread([context] { + outputs++; + sceAudioOut2ContextPush(context, 1); + Fail("a push waiting for an advance returned during exit"); + }).detach(); +} + +void AdvanceAtExit() { + AudioOut2ContextHandle context = 0; + AudioOut2PortHandle port = 0; + CreatePort(context, port); + SetData(port); + LibcRegisterShutdown_nid_postfix(DelayExit); + LibcRequestExit_nid_postfix(0); + sceAudioOut2ContextAdvance(context); + Fail("an advance after the exit request returned"); +} + +void OutputAudioOut() { + const int port = sceAudioOutOpen(user, 0, 0, grain, frequency, formatS16Stereo); + if (port <= 0) Fail("port was not opened"); + static std::vector data(static_cast(grain) * 2, 0x1000); + std::thread([port] { + for (;;) { + if (sceAudioOutOutput(port, data.data()) < 0) Fail("an output during exit returned an error"); + outputs++; + } + }).detach(); +} + +} + +int main(int argc, char** argv) { +#ifdef _WIN32 + _putenv_s("SDL_AUDIODRIVER", "dummy"); +#else + setenv("SDL_AUDIODRIVER", "dummy", 1); +#endif + const bool waiting = argc == 2 && std::strcmp(argv[1], "--audio-out2-waiting-push") == 0; + if (argc == 2 && std::strcmp(argv[1], "--audio-out2") == 0) PushAudioOut2(); + else if (argc == 2 && std::strcmp(argv[1], "--audio-out") == 0) OutputAudioOut(); + else if (waiting) WaitInPushAudioOut2(); + else if (argc == 2 && std::strcmp(argv[1], "--audio-out2-advance") == 0) AdvanceAtExit(); + else Fail("usage: audio_out_shutdown_tests --audio-out2 | --audio-out2-waiting-push | --audio-out2-advance | --audio-out"); + while (outputs.load() < (waiting ? 1 : startedOutputs)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (waiting) std::this_thread::sleep_for(settle); + std::thread([] { + std::this_thread::sleep_for(exitLimit); + Fail("the exit did not finish"); + }).detach(); + if (std::atexit(waiting ? ReportExit : RequireParked) != 0) Fail("atexit registration failed"); + LibcRequestExit_nid_postfix(0); + LibcAwaitExit_nid_postfix(); +} diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutSys.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutSys.cpp new file mode 100644 index 0000000000..eb5878fc18 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutSys.cpp @@ -0,0 +1,65 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOutSysOpen(int userId, std::uint32_t mode); +int APS5_VABI sceAudioOutSysConfigureOutput(int type, std::uint64_t flags, std::uint32_t mode, std::uint32_t target, std::uint64_t option); +int APS5_VABI sceAudioOutSysClose(int handle); +int APS5_VABI sceAudioOutExOpen(int userId, std::uint32_t mode); +int APS5_VABI sceAudioOutOutput(int handle, const void* ptr); +} + +static constexpr int SYSTEM_USER = 0xFF; + +static bool failed = false; + +static void Require(bool condition, const char* what) { + if (!condition) { + std::fprintf(stderr, "failed: %s\n", what); + failed = true; + } +} + +template +static bool Throws(Call call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +int main() { + Require(Throws([] { sceAudioOutSysOpen(SYSTEM_USER, 0); }), "mode 0 throws: linear PCM opens through the Ex path"); + Require(Throws([] { sceAudioOutSysOpen(SYSTEM_USER, 6); }), "mode 6 throws"); + + const int truehd = sceAudioOutSysOpen(SYSTEM_USER, 5); + Require(truehd > 0, "mode 5 opens a TrueHD MAT port"); + const std::vector grain(1024 * 8, 0x1234); + Require(sceAudioOutOutput(truehd, grain.data()) == 1024, "a mode 5 port takes 1024 eight-channel frames"); + Require(sceAudioOutOutput(truehd, nullptr) == 1024, "a mode 5 port drains"); + + const int second = sceAudioOutSysOpen(SYSTEM_USER, 5); + Require(second > 0 && second != truehd, "a second mode 5 port opens"); + + Require(sceAudioOutSysConfigureOutput(1, 0, 5, 1, 0) == 0, "the HDMI MAT switch is accepted"); + Require(Throws([] { sceAudioOutSysConfigureOutput(0, 0, 5, 1, 0); }), "another output type throws"); + Require(Throws([] { sceAudioOutSysConfigureOutput(1, 1, 5, 1, 0); }), "nonzero flags throw"); + Require(Throws([] { sceAudioOutSysConfigureOutput(1, 0, 4, 1, 0); }), "another mode throws"); + Require(Throws([] { sceAudioOutSysConfigureOutput(1, 0, 5, 2, 0); }), "another target throws"); + + Require(sceAudioOutSysClose(truehd) == 0, "close the first Sys port"); + Require(Throws([truehd] { sceAudioOutSysClose(truehd); }), "a closed Sys port throws"); + const int ex = sceAudioOutExOpen(SYSTEM_USER, 0); + Require(ex > 0, "an Ex port opens"); + Require(Throws([ex] { sceAudioOutSysClose(ex); }), "an Ex port is not a Sys port"); + Require(Throws([] { sceAudioOutSysClose(0); }), "handle 0 is not a Sys port"); + Require(Throws([] { sceAudioOutSysClose(1000); }), "an out-of-range handle is not a Sys port"); + Require(sceAudioOutSysClose(second) == 0, "close the second Sys port"); + return failed ? 1 : 0; +} diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp index ce23a664d7..7eb1547ce7 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp @@ -59,13 +59,14 @@ int main() { Require(sceAudioOutSysGetHdmiMonitorInfo(monitorTypeHdmi, nullptr, infoSize) == invalidPointer, "a null buffer must report an invalid pointer"); - Require(Throws(monitorTypeHdmi, infoSize - 1, - "sceAudioOutSysGetHdmiMonitorInfo: size 383 not supported"), "a short buffer must throw"); - Require(Throws(monitorTypeHdmi, infoSize + 1, - "sceAudioOutSysGetHdmiMonitorInfo: size 385 not supported"), "a long buffer must throw"); + buffer.fill(untouched); + for (const std::uint32_t size : {0u, infoSize - 1, infoSize + 1, 0xffffffffu}) { + Require(sceAudioOutSysGetHdmiMonitorInfo(monitorTypeHdmi, buffer.data(), size) == static_cast(0x80260006), "exact size required"); + Require(sceAudioOutSysGetHdmiMonitorInfo(0, nullptr, size) == invalidPointer, "pointer checked before size and type"); + for (const auto value : buffer) Require(value == untouched, "failure preserves output"); + } + for (const int type : {0, 2, -1, 17}) Require(sceAudioOutSysGetHdmiMonitorInfo(type, buffer.data(), infoSize) == static_cast(0x8026000C), "unsupported type"); Require(Throws(0x10, infoSize, - "sceAudioOutSysGetHdmiMonitorInfo: monitor type 16 not supported"), "monitor type 0x10 must throw"); - Require(Throws(0, infoSize, - "sceAudioOutSysGetHdmiMonitorInfo: monitor type 0 not supported"), "monitor type 0 must throw"); + "sceAudioOutSysGetHdmiMonitorInfo: monitor type 16 not supported"), "unimplemented device query"); return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutVibration.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutVibration.cpp new file mode 100644 index 0000000000..4f27e0bf7d --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutVibration.cpp @@ -0,0 +1,99 @@ +#include "prx/libScePad/include/PadState.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#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 sceAudioOutExOpen(int, std::uint32_t); +int APS5_VABI sceAudioOutClose(int); +int APS5_VABI sceAudioOutOutput(int, const void*); +int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam*, std::uint32_t); +int APS5_VABI sceAudioOutSetVolume(int, std::uint32_t, int*); +} + +static void Require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +struct Case { + std::string_view name; + std::uint32_t format = 1; + bool batch = false; + std::array volume{32768, 32768}; + std::array expected{128, 255}; + bool silence = false; + bool drain = false; + bool ex = false; +}; + +int main(int argc, char** argv) { + try { + const std::string_view mode = argc == 2 ? argv[1] : "--s16"; + const Case cases[] = { + {"--s16"}, {"--f32", 4}, {"--batch_s16", 1, true}, {"--batch_f32", 4, true}, + {"--mono_s16", 0, false, {32768, 32768}, {128, 128}}, + {"--mono_f32", 3, false, {32768, 32768}, {128, 128}}, + {"--gain_s16", 1, false, {16384, 8192}, {64, 64}}, + {"--gain_f32", 4, true, {16384, 8192}, {64, 64}}, + {"--muted", 4, false, {0, 0}, {0, 0}}, + {"--silence", 1, true, {32768, 32768}, {0, 0}, true}, + {"--drain", 4, false, {32768, 32768}, {0, 0}, false, true}, + {"--ex", 1, true, {32768, 32768}, {0, 0}, false, false, true}, + {"--pad_control"} + }; + const Case* selected = nullptr; + for (const auto& entry : cases) if (entry.name == mode) selected = &entry; + Require(argc <= 2 && selected != nullptr, "invalid test arguments"); + const auto& test = *selected; + Pad::Initialize(); + Pad::SetVibrationMode(1); + std::uint32_t seen = 0; + PadOutputState output{}; + static_cast(PadFetchOutput_nid_postfix(&seen, &output)); + struct Port { + int handle = -1; + ~Port() { if (handle > 0) sceAudioOutClose(handle); } + } port; + if (mode == "--pad_control") { + PadSubmitAudioHaptics_nid_postfix(128, 255); + } else { + const std::uint32_t frames = test.ex ? 256u : 48000u; + const bool floating = test.format == 3 || test.format == 4; + const std::uint32_t channels = test.format == 0 || test.format == 3 ? 1u : 2u; + std::vector integers(frames * 2); + std::vector floats(frames * 2); + for (std::uint32_t frame = 0; frame < frames && !test.silence; ++frame) { + integers[frame * channels] = 8192; + floats[frame * channels] = 0.25f; + if (channels == 2) { + integers[frame * channels + 1] = -16384; + floats[frame * channels + 1] = -0.5f; + } + } + port.handle = test.ex ? sceAudioOutExOpen(0x10000000, 0) : sceAudioOutOpen(0x10000000, 10, 0, frames, 48000, test.format); + Require(port.handle > 0, "legacy vibration port did not open"); + auto volume = test.volume; + Require(sceAudioOutSetVolume(port.handle, 3, volume.data()) == 0, "legacy vibration volume failed"); + const void* samples = test.drain ? nullptr : floating ? static_cast(floats.data()) : static_cast(integers.data()); + AudioOutOutputParam param{}; + param.handle = port.handle; + param.ptr = samples; + const int result = test.batch ? sceAudioOutOutputs(¶m, 1) : sceAudioOutOutput(port.handle, samples); + Require(result == static_cast(frames), "legacy vibration output returned the wrong frame count"); + } + const bool changed = PadFetchOutput_nid_postfix(&seen, &output); + Require(changed == (test.expected[0] != 0 || test.expected[1] != 0), "legacy vibration output published the wrong controller state"); + Require(output.audioVibrationLarge == test.expected[0] && output.audioVibrationSmall == test.expected[1], "legacy vibration PCM did not reach the existing motor bridge"); + Require(output.vibrationLarge == 0 && output.vibrationSmall == 0, "audio vibration replaced regular motor state"); + std::puts("legacy vibration bridge test passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutVibrationDevice.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutVibrationDevice.cpp new file mode 100644 index 0000000000..6d0a7ca4f1 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutVibrationDevice.cpp @@ -0,0 +1,290 @@ +#include "SDL.h" +#include "prx/libSceAudioOut/include/AudioOutSdl.hpp" +#include "prx/libScePad/include/PadState.hpp" +#include "prx/libScePad/include/PadAudioHapticQueue.hpp" +#include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#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 sceAudioOutClose(int); +int APS5_VABI sceAudioOutOutput(int, const void*); +int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam*, std::uint32_t); +int APS5_VABI sceAudioOutSetVolume(int, std::uint32_t, int*); +int APS5_VABI sceAudioOutGetLastOutputTime(int, std::uint64_t*); +} + +namespace { +constexpr std::uint32_t frames = 256; +bool available = true; +bool failOpen = false; +bool failInitialize = false; +bool failEnumeration = false; +bool pulse = false; +std::uint32_t frequency = 48000; +int opened = 0; +int closed = 0; +int queued = 0; +std::vector played; +struct Block { + int source; + std::uint64_t begin; + std::uint64_t end; + std::array rumble; +}; +std::vector blocks; +std::vector clears; +PadAudioHapticQueue timeline; + +void Require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +float Expected(std::int16_t sample, int volume, bool floating) { + const float value = sample / 32768.0f; + if (floating) return std::clamp(value * (volume / 32768.0f), -1.0f, 1.0f); + return static_cast(std::clamp(static_cast(sample) * volume / 32768, -32768, 32767)) / 32768.0f; +} + +void Run(std::uint32_t format, bool batch, int volume) { + const bool floating = format >= 3; + const std::uint32_t channels = format == 0 || format == 3 ? 1u : 2u; + const int handle = sceAudioOutOpen(0x10000000, 10, 0, frames, frequency, format); + Require(handle == 1, "a closed or failed vibration port remained allocated"); + std::array levels{volume, volume / 2}; + Require(sceAudioOutSetVolume(handle, 3, levels.data()) == 0, "vibration gains failed"); + std::array integers{}; + std::array floats{}; + for (std::uint32_t frame = 0; frame < frames; ++frame) { + integers[frame * channels] = 8192; + floats[frame * channels] = 0.25f; + if (channels == 2) { + integers[frame * channels + 1] = -16384; + floats[frame * channels + 1] = -0.5f; + } + } + const void* data = floating ? static_cast(floats.data()) : static_cast(integers.data()); + AudioOutOutputParam outputs[2]{}; + outputs[0].handle = handle; + outputs[0].ptr = data; + outputs[1].handle = 9999; + outputs[1].ptr = data; + const int before = queued; + Require(sceAudioOutOutputs(outputs, 2) < 0 && queued == before, "an invalid batch queued a valid prefix"); + Require((batch ? sceAudioOutOutputs(outputs, 1) : sceAudioOutOutput(handle, data)) == static_cast(frames), "vibration frame count changed"); + Require(queued == before + 1, "native vibration did not reach the named controller device"); + const std::uint32_t deviceChannels = pulse ? 6u : 4u; + Require(played.size() == frames * deviceChannels, "native vibration frame layout changed"); + const auto left = Expected(8192, levels[0], floating); + const auto right = channels == 1 ? left : Expected(-16384, levels[1], floating); + for (std::uint32_t frame = 0; frame < frames; ++frame) { + for (std::uint32_t channel = 0; channel < deviceChannels; ++channel) { + const auto expected = channel == deviceChannels - 2 ? left : channel == deviceChannels - 1 ? right : 0.0f; + Require(played[frame * deviceChannels + channel] == expected, "vibration PCM or gain leaked into a speaker channel"); + } + } + std::uint64_t time = 0; + Require(sceAudioOutGetLastOutputTime(handle, &time) == 0 && time != 0, "vibration acceptance time was not recorded"); + Require(sceAudioOutOutput(handle, nullptr) == static_cast(frames) && queued == before + 1, "draining vibration queued samples"); + std::uint64_t after = 0; + Require(sceAudioOutGetLastOutputTime(handle, &after) == 0 && after == time, "a drain changed the last data time"); + Require(sceAudioOutClose(handle) == 0, "native vibration close failed"); +} + +int Submit(std::uint32_t count, std::uint32_t rate, bool split = false) { + const int handle = sceAudioOutOpen(0x10000000, 10, 0, count, rate, 1); + Require(handle > 0, "timed vibration port did not open"); + std::vector samples(static_cast(count) * 2); + for (std::uint32_t frame = 0; frame < count; ++frame) { + samples[frame * 2] = split && frame >= 256 ? 16384 : 8192; + samples[frame * 2 + 1] = split && frame >= 256 ? -8192 : -16384; + } + Require(sceAudioOutOutput(handle, samples.data()) == static_cast(count), "timed vibration output failed"); + return handle; +} + +void RunTiming() { + available = false; + const int shortPort = Submit(256, 48000); + Require(blocks.size() == 1 && blocks[0].end - blocks[0].begin == 5333 && blocks[0].rumble == std::array{128, 255}, "short block timing changed"); + const int longPort = Submit(4800, 24000); + Require(blocks.size() == 20 && blocks.back().end - blocks[1].begin == 200000, "long block duration did not follow frame count/rate"); + for (std::size_t index = 2; index < blocks.size(); ++index) Require(blocks[index].begin == blocks[index - 1].end, "successive grains overlap or leave a gap"); + auto model = timeline; + Require(model.Read(blocks[1].begin + 81000) == std::array{128, 255}, "a long block inherited the AudioOut2 timeout"); + Require(sceAudioOutOutput(shortPort, nullptr) == 256 && clears == std::vector{shortPort}, "a drain cleared another port"); + Require(sceKernelGetProcessTime() >= blocks[0].end, "a drain returned before the submitted block ended"); + Require(sceAudioOutClose(shortPort) == 0 && sceAudioOutClose(longPort) == 0, "timed port close failed"); + Require(clears == std::vector{shortPort, shortPort, longPort}, "close did not clear its own source"); + Require(timeline.Read(sceKernelGetProcessTime()) == std::array{}, "close retained a queued source"); + + const auto firstLong = blocks.size(); + const int drained = Submit(4800, 24000); + const auto end = blocks.back().end; + Require(blocks.size() == firstLong + 19, "long PCM was not divided into grains"); + Require(sceAudioOutOutput(drained, nullptr) == 4800 && sceKernelGetProcessTime() >= end, "long drain did not wait for its frame duration"); + Require(sceAudioOutClose(drained) == 0, "drained port close failed"); + + const auto firstSplit = blocks.size(); + const int split = Submit(512, 48000, true); + Require(blocks.size() == firstSplit + 2, "two-grain PCM was reduced to one value"); + Require(blocks[firstSplit].rumble == std::array{128, 255} && blocks.back().rumble == std::array{255, 128}, "grain-local energy was replaced by whole-block RMS"); + Require(sceAudioOutClose(split) == 0, "split port close failed"); + Require(opened == 0 && queued == 0, "virtual vibration opened a speaker device"); +} + +void RunCadence() { + available = false; + const std::array ports{ + sceAudioOutOpen(0x10000000, 10, 0, frames, frequency, 1), + sceAudioOutOpen(0x10000000, 10, 0, frames, frequency, 1) + }; + Require(ports[0] > 0 && ports[1] > 0 && ports[0] != ports[1], "repeated vibration ports did not open independently"); + constexpr std::array, 4> patterns{{{8192, -16384}, {0, 0}, {-16384, 8192}, {16384, -8192}}}; + constexpr std::array, 4> expected{{{128, 255}, {0, 0}, {255, 128}, {255, 128}}}; + std::array ends{}; + std::array samples{}; + std::vector boundaries; + for (std::size_t call = 0; call < 32; ++call) { + for (std::size_t port = 0; port < ports.size(); ++port) { + const auto pattern = (call + port) % patterns.size(); + for (std::size_t frame = 0; frame < frames; ++frame) { + samples[frame * 2] = patterns[pattern][0]; + samples[frame * 2 + 1] = patterns[pattern][1]; + } + const auto count = blocks.size(); + Require(sceAudioOutOutput(ports[port], samples.data()) == static_cast(frames), "repeated vibration output rejected 256 frames"); + Require(blocks.size() == count + 1, "repeated vibration output did not queue one grain per call"); + const auto& block = blocks.back(); + Require(block.source == ports[port] && block.rumble == expected[pattern], "repeated vibration output used the wrong source or PCM energy"); + Require(block.end - block.begin == 5333 && block.begin >= ends[port], "repeated vibration interval has the wrong duration or overlaps its preceding call"); + ends[port] = block.end; + boundaries.insert(boundaries.end(), {block.begin, block.end - 1, block.end}); + } + } + Require(blocks.size() == 64 && clears.empty() && opened == 0 && queued == 0, "repeated virtual output changed routes or cleared a live port"); + std::sort(boundaries.begin(), boundaries.end()); + boundaries.erase(std::unique(boundaries.begin(), boundaries.end()), boundaries.end()); + const auto check = [&](PadAudioHapticQueue model, int removed) { + for (const auto time : boundaries) { + std::array value{}; + for (const auto& block : blocks) { + if (block.source == removed || time < block.begin || time >= block.end) continue; + for (std::size_t channel = 0; channel < value.size(); ++channel) value[channel] = std::max(value[channel], block.rumble[channel]); + } + Require(model.Read(time) == value, "repeated vibration timeline lost a grain, silence, boundary or independent source"); + } + }; + check(timeline, 0); + Require(sceAudioOutOutput(ports[0], nullptr) == static_cast(frames) && clears == std::vector{ports[0]}, "repeated vibration drain cleared the wrong source"); + check(timeline, ports[0]); + Require(sceAudioOutClose(ports[0]) == 0 && sceAudioOutClose(ports[1]) == 0, "repeated vibration port close failed"); + Require(clears == std::vector{ports[0], ports[0], ports[1]}, "repeated vibration close did not clear only its source"); + Require(timeline.Read(ends[0]) == std::array{}, "repeated vibration close retained queued output"); + std::puts("64 repeated legacy stereo S16/48 kHz outputs: 256 frames per call, 32 calls per port; PCM, interval boundaries, source mixing and drain isolation passed"); +} +} + +int InitializeSdlAudio_nid_no_patch() { return failInitialize ? -1 : 0; } +extern "C" void PadQueueLegacyAudioHaptics_nid_postfix(int source, std::uint64_t begin, std::uint64_t end, std::uint8_t large, std::uint8_t small) { + blocks.push_back({source, begin, end, {large, small}}); + timeline.Queue(source, begin, end, large, small); +} +extern "C" void PadClearLegacyAudioHaptics_nid_postfix(int source) { + clears.push_back(source); + timeline.Clear(source); +} +extern "C" Uint32 SDLCALL SDL_WasInit(Uint32) { return SDL_INIT_AUDIO; } +extern "C" int SDLCALL SDL_GetNumAudioDevices(int) { return failEnumeration ? -1 : available ? 2 : 1; } +extern "C" const char* SDLCALL SDL_GetAudioDeviceName(int index, int) { return index == 0 ? "Main speakers" : "DualSense Wireless Controller"; } +extern "C" int SDLCALL SDL_GetAudioDeviceSpec(int index, int, SDL_AudioSpec* spec) { *spec = {}; spec->channels = index == 0 ? 2 : 4; return 0; } +extern "C" const char* SDLCALL SDL_GetCurrentAudioDriver() { return pulse ? "pulseaudio" : "dummy"; } +extern "C" const char* SDLCALL SDL_GetError() { return "test controller open failure"; } +extern "C" SDL_AudioDeviceID SDLCALL SDL_OpenAudioDevice(const char* name, int capture, const SDL_AudioSpec* desired, SDL_AudioSpec* obtained, int flags) { + Require(name != nullptr && std::string_view(name) == "DualSense Wireless Controller", "vibration opened the default speakers"); + Require(capture == 0 && flags == 0 && desired->callback == nullptr && desired->freq == static_cast(frequency) && desired->format == AUDIO_F32SYS && desired->channels == (pulse ? 6 : 4) && desired->samples == frames, "native controller request changed"); + if (failOpen) return 0; + ++opened; + *obtained = *desired; + return 42; +} +extern "C" void SDLCALL SDL_PauseAudioDevice(SDL_AudioDeviceID device, int) { Require(device == 42, "wrong native device paused"); } +extern "C" Uint32 SDLCALL SDL_GetQueuedAudioSize(SDL_AudioDeviceID device) { Require(device == 42, "wrong native queue queried"); return 0; } +extern "C" int SDLCALL SDL_QueueAudio(SDL_AudioDeviceID device, const void* data, Uint32 bytes) { + Require(device == 42 && bytes % sizeof(float) == 0, "wrong vibration queue"); + const auto* samples = static_cast(data); + const std::size_t sampleCount = bytes / sizeof(float); + played.assign(samples, samples + sampleCount); + ++queued; + return 0; +} +extern "C" void SDLCALL SDL_ClearQueuedAudio(SDL_AudioDeviceID device) { Require(device == 42, "wrong native queue cleared"); } +extern "C" void SDLCALL SDL_CloseAudioDevice(SDL_AudioDeviceID device) { Require(device == 42, "wrong native device closed"); ++closed; } + +int main(int argc, char** argv) { + try { + Require(argc == 2, "invalid test arguments"); + const std::string_view mode = argv[1]; + Require(mode == "quad" || mode == "pulse" || mode == "open_failure" || mode == "timing" || mode == "cadence" || mode == "discovery_failure", "invalid device mode"); + pulse = mode == "pulse"; + Pad::Initialize(); + Pad::SetVibrationMode(1); + std::uint32_t seen = 0; + PadOutputState state{}; + static_cast(PadFetchOutput_nid_postfix(&seen, &state)); + if (mode == "cadence") { + RunCadence(); + return 0; + } + if (mode == "timing") { + RunTiming(); + std::puts("legacy vibration device tests passed"); + return 0; + } + if (mode == "open_failure") { + failOpen = true; + bool threw = false; + try { static_cast(sceAudioOutOpen(0x10000000, 10, 0, frames, frequency, 1)); } + catch (const std::runtime_error&) { threw = true; } + Require(threw && opened == 0 && queued == 0, "a failed native open silently changed routes"); + failOpen = false; + } + if (mode == "discovery_failure") { + for (bool* failure : {&failInitialize, &failEnumeration}) { + *failure = true; + bool threw = false; + try { static_cast(sceAudioOutOpen(0x10000000, 10, 0, frames, frequency, 1)); } + catch (const std::runtime_error&) { threw = true; } + Require(threw && opened == 0 && queued == 0, "a discovery failure silently changed routes"); + *failure = false; + } + } + for (auto rate : {24000u, 48000u}) { + frequency = rate; + for (auto format : {0u, 1u, 3u, 4u}) + for (bool batch : {false, true}) + for (int volume : {0, 16384, 32768, 131072}) Run(format, batch, volume); + } + for (auto format : {2u, 5u, 6u, 7u}) { + bool threw = false; + try { static_cast(sceAudioOutOpen(0x10000000, 10, 0, frames, frequency, format)); } + catch (const std::runtime_error&) { threw = true; } + Require(threw && opened == 64, "an unknown multichannel vibration layout was accepted"); + } + Require(opened == closed && opened == 64, "native vibration device ownership changed"); + Require(!PadFetchOutput_nid_postfix(&seen, &state), "native waveform output also sent motor approximation"); + std::puts("legacy vibration device tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutVibrationTimeline.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutVibrationTimeline.cpp new file mode 100644 index 0000000000..75f961ebfc --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutVibrationTimeline.cpp @@ -0,0 +1,70 @@ +#include "prx/libScePad/include/PadAudioHapticQueue.hpp" +#include + +static void Require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +template +static bool Throws(TAction action) { + try { action(); } + catch (const std::invalid_argument&) { return true; } + return false; +} + +int main() { + try { + { + PadAudioHapticQueue queue; + queue.Queue(1, 1000, 6333, 128, 255); + Require(queue.Read(999) == std::array{}, "a queued block began early"); + Require(queue.Read(1000) == std::array{128, 255}, "a block did not begin at its queued start"); + Require(queue.Read(6332) == std::array{128, 255}, "a block ended early"); + Require(queue.Read(6333) == std::array{}, "a short block inherited an 80 ms hold"); + } + { + PadAudioHapticQueue queue; + queue.Queue(1, 1000, 201000, 200, 100); + Require(queue.Read(81000) == std::array{200, 100}, "a long block expired after 80 ms"); + Require(queue.Read(200999) == std::array{200, 100}, "a long block ended before its frame duration"); + Require(queue.Read(201000) == std::array{}, "a long block outlived its frame duration"); + } + { + PadAudioHapticQueue queue; + queue.Queue(1, 100, 200, 200, 10); + queue.Queue(1, 200, 300, 0, 0); + queue.Queue(1, 400, 500, 20, 30); + Require(queue.Read(199) == std::array{200, 10}, "the first grain changed"); + Require(queue.Read(200) == std::array{}, "silent PCM retained an earlier peak"); + Require(queue.Read(350) == std::array{}, "a gap between blocks vibrated"); + Require(queue.Read(400) == std::array{20, 30}, "a later grain did not start"); + } + { + PadAudioHapticQueue queue; + queue.Queue(1, 0, 1000, 200, 10); + queue.Queue(2, 0, 1000, 20, 220); + Require(queue.Read(10) == std::array{200, 220}, "independent sources did not combine"); + queue.Clear(1); + Require(queue.Read(10) == std::array{20, 220}, "draining one source cleared another"); + queue.Clear(1); + queue.Clear(99); + Require(queue.Read(10) == std::array{20, 220}, "clearing an absent source changed output"); + queue.Clear(2); + Require(queue.Read(10) == std::array{}, "closing the final source retained output"); + } + { + PadAudioHapticQueue queue; + Require(Throws([&] { queue.Queue(0, 0, 1, 1, 1); }), "an invalid source was accepted"); + Require(Throws([&] { queue.Queue(1, 1, 1, 1, 1); }), "a zero-length interval was accepted"); + Require(Throws([&] { queue.Queue(1, 2, 1, 1, 1); }), "a reversed interval was accepted"); + queue.Queue(1, 0, 100, 10, 20); + Require(Throws([&] { queue.Queue(1, 99, 200, 1, 1); }), "overlapping source blocks were accepted"); + Require(queue.Read(50) == std::array{10, 20}, "a refused interval changed the active block"); + } + std::puts("legacy vibration timeline tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp index 602d531cfa..8323b813c9 100644 --- a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp +++ b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp @@ -9,6 +9,10 @@ namespace AudioPropagation { +inline constexpr std::int32_t ErrorInvalidArgument = static_cast(0x8A700001); +inline constexpr std::int32_t ErrorInvalidHandle = static_cast(0x8A700002); +inline constexpr std::int32_t ErrorNullPointer = static_cast(0x8A700003); + inline constexpr std::uint32_t MaterialId = 0x010107d1; inline constexpr std::size_t MaterialSize = 0x40; inline constexpr std::uint32_t SystemMemoryId = 0x010107d4; @@ -23,6 +27,107 @@ inline constexpr std::size_t PortalParamsSize = 0x60; inline constexpr std::uint32_t PathEntryId = 0x010107d9; inline constexpr std::size_t PathEntrySize = 0x28; +inline constexpr std::uint32_t SystemOptionsId = 0x010107d5; +inline constexpr std::size_t SystemOptionsSize = 56; + +struct SystemOptions { + AudioPropagationStructDescriptor desc; + std::uint32_t maxSources; + std::uint32_t maxMaterials; + std::uint32_t parameter24; + std::uint32_t reflectionOrder; + std::uint32_t frameSamples; + float propagationSpeed; + float maxPathLength; + std::uint32_t flags; + std::uint8_t opaque[8]; +}; + +static_assert(sizeof(SystemOptions) == SystemOptionsSize); +static_assert(offsetof(SystemOptions, frameSamples) == 32); +static_assert(offsetof(SystemOptions, flags) == 44); + +struct SourceInfo { + AudioPropagationHandle system; + float position[3]; + std::uint32_t reserved20; + float direction[3]; + std::uint32_t reserved36; + std::uint32_t attribute2Mode; + float attribute2Values[3]; + float attribute3Values[4]; + std::uint32_t pathCapacity; + std::uint32_t parameter76; +}; + +struct SystemInfo { + void* cpuMemory; + std::size_t cpuMemorySize; + std::uint32_t index; + std::uint32_t frameSamples; + std::uint32_t parameter24; + std::uint32_t maxSources; + std::uint32_t maxMaterials; + std::uint32_t pathCapacityPerSource; + float propagationSpeed; + std::uint32_t reserved44; +}; + +static_assert(sizeof(SourceInfo) == 80); +static_assert(offsetof(SourceInfo, position) == 8); +static_assert(offsetof(SourceInfo, direction) == 24); +static_assert(offsetof(SourceInfo, attribute2Mode) == 40); +static_assert(offsetof(SourceInfo, attribute2Values) == 44); +static_assert(offsetof(SourceInfo, attribute3Values) == 56); +static_assert(offsetof(SourceInfo, pathCapacity) == 72); +static_assert(offsetof(SourceInfo, parameter76) == 76); +static_assert(sizeof(SystemInfo) == 48); +static_assert(offsetof(SystemInfo, cpuMemorySize) == 8); +static_assert(offsetof(SystemInfo, index) == 16); +static_assert(offsetof(SystemInfo, frameSamples) == 20); +static_assert(offsetof(SystemInfo, parameter24) == 24); +static_assert(offsetof(SystemInfo, maxSources) == 28); +static_assert(offsetof(SystemInfo, maxMaterials) == 32); +static_assert(offsetof(SystemInfo, pathCapacityPerSource) == 36); +static_assert(offsetof(SystemInfo, propagationSpeed) == 40); +struct ReportRegistration; + +struct ReportArguments { + std::uint32_t generalOffset; + std::uint32_t floatingOffset; + void* overflowArguments; + void* savedRegisters; +}; + +struct ReportInfo { + std::uint64_t size; + AudioPropagationHandle handle; + std::uint32_t category; + std::int32_t result; + std::int32_t operation; + std::uint32_t reserved; + const char* function; + const char* format; + ReportArguments arguments; +}; + +using ReportHandler = void (APS5_VABI*)(const ReportInfo* report, void* userData); + +static_assert(sizeof(ReportArguments) == 24); +static_assert(offsetof(ReportArguments, generalOffset) == 0); +static_assert(offsetof(ReportArguments, floatingOffset) == 4); +static_assert(offsetof(ReportArguments, overflowArguments) == 8); +static_assert(offsetof(ReportArguments, savedRegisters) == 16); +static_assert(sizeof(ReportInfo) == 72); +static_assert(offsetof(ReportInfo, size) == 0); +static_assert(offsetof(ReportInfo, handle) == 8); +static_assert(offsetof(ReportInfo, category) == 16); +static_assert(offsetof(ReportInfo, result) == 20); +static_assert(offsetof(ReportInfo, operation) == 24); +static_assert(offsetof(ReportInfo, function) == 32); +static_assert(offsetof(ReportInfo, format) == 40); +static_assert(offsetof(ReportInfo, arguments) == 48); + struct Attribute { std::uint32_t id; std::uint32_t reserved0; @@ -32,13 +137,38 @@ struct Attribute { std::uint32_t reserved2; }; +struct PathPoint { + AudioPropagationStructDescriptor desc; + AudioPropagationHandle room; + float gain; + float position[3]; + std::byte opaque[8]; +}; + +struct AudioPath { + AudioPropagationStructDescriptor desc; + PathPoint points[4]; + std::uint32_t pointCount; + std::uint32_t flags; + std::byte opaque[8]; +}; + +static_assert(sizeof(PathPoint) == 48); +static_assert(offsetof(PathPoint, room) == 16); +static_assert(offsetof(PathPoint, gain) == 24); +static_assert(offsetof(PathPoint, position) == 28); +static_assert(sizeof(AudioPath) == 224); +static_assert(offsetof(AudioPath, points) == 16); +static_assert(offsetof(AudioPath, pointCount) == 208); +static_assert(offsetof(AudioPath, flags) == 212); + struct RenderInfo { AudioPropagationStructDescriptor desc; AudioPropagationHandle source; - void* output; + float* output; std::size_t outputSize; std::uint32_t format; - std::uint32_t reserved; + std::uint32_t channelCount; }; struct PortalParams { @@ -53,6 +183,7 @@ static_assert(sizeof(Attribute) == 0x20); static_assert(sizeof(RenderInfo) == RenderInfoSize); static_assert(offsetof(RenderInfo, source) == 0x10); static_assert(offsetof(RenderInfo, format) == 0x28); +static_assert(offsetof(RenderInfo, channelCount) == 0x2c); static_assert(sizeof(PortalParams) == PortalParamsSize); static_assert(offsetof(PortalParams, rooms) == 0x50); @@ -60,8 +191,8 @@ static_assert(offsetof(PortalParams, rooms) == 0x50); extern "C" { -std::int32_t APS5_VABI sceAudioPropagationSystemQueryMemory(const void* options, AudioPropagationSystemMemory* outMemory); -std::int32_t APS5_VABI sceAudioPropagationSystemCreate(const void* options, const AudioPropagationSystemMemory* memory, AudioPropagationHandle* outSystem); +std::int32_t APS5_VABI sceAudioPropagationSystemQueryMemory(const AudioPropagation::SystemOptions* options, AudioPropagationSystemMemory* outMemory); +std::int32_t APS5_VABI sceAudioPropagationSystemCreate(const AudioPropagation::SystemOptions* options, const AudioPropagationSystemMemory* memory, AudioPropagationHandle* outSystem); std::int32_t APS5_VABI sceAudioPropagationSystemDestroy(AudioPropagationHandle system); std::int32_t APS5_VABI sceAudioPropagationSystemSetAttributes(AudioPropagationHandle system, const AudioPropagation::Attribute* attributes, std::uint32_t count); std::int32_t APS5_VABI sceAudioPropagationSystemRegisterMaterial(AudioPropagationHandle system, const AudioPropagationStructDescriptor* material, AudioPropagationHandle* outMaterial); @@ -79,10 +210,10 @@ std::int32_t APS5_VABI sceAudioPropagationSourceSetAttributes(AudioPropagationHa std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPathCount(AudioPropagationHandle source, std::uint32_t* count); std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPath(AudioPropagationHandle source, std::uint32_t index, AudioPropagationHandle* outPath); std::int32_t APS5_VABI sceAudioPropagationSourceGetRays(AudioPropagationHandle source, void* rays, std::uint32_t* count); -std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropagationHandle source, const void* rays, std::uint32_t rayCount, std::uint32_t flags, void* paths, std::uint32_t pathCount); +std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropagationHandle source, const void* rays, std::uint32_t rayCount, std::uint32_t reflectionOrder, void* paths, std::uint32_t pathCount); std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHandle source, const void* entries, std::uint32_t count); -std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const void* data, float gain); -std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle system, const AudioPropagation::RenderInfo* infos, std::uint32_t count); +std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const AudioPropagation::AudioPath* data, float gain); +std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle system, AudioPropagation::RenderInfo* infos, std::uint32_t count); } diff --git a/core/libs/prx/libSceAudioPropagation/CMakeLists.txt b/core/libs/prx/libSceAudioPropagation/CMakeLists.txt index 29417b8962..0575687fe3 100644 --- a/core/libs/prx/libSceAudioPropagation/CMakeLists.txt +++ b/core/libs/prx/libSceAudioPropagation/CMakeLists.txt @@ -5,7 +5,7 @@ configure_windows_unwind(libSceAudioPropagation) if(BUILD_TESTING) add_test_executable(guest_audio_propagation_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudioPropagation.cpp) target_include_directories(guest_audio_propagation_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_audio_propagation_tests PRIVATE libSceAudioPropagation libc) + target_link_libraries(guest_audio_propagation_tests PRIVATE libSceAudioPropagation libc ${CMAKE_DL_LIBS}) configure_windows_unwind(guest_audio_propagation_tests) add_test(NAME guest_audio_propagation COMMAND guest_audio_propagation_tests) if(WIN32) diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index c3cdff194d..2b461aa9cc 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -1,6 +1,8 @@ #include "AudioPropagation.hpp" +#include "prx/libc/include/General.hpp" #include +#include #include #include #include @@ -21,7 +23,8 @@ constexpr std::uint64_t SystemMagic = [] { struct SystemState { std::uint64_t magic; - std::uint64_t reserved; + std::uint32_t frameSamples; + std::uint32_t maxSources; }; enum class Kind { Room, Portal, Source, Material }; @@ -114,23 +117,49 @@ void requireNoPropagation(bool none, const char* function) { extern "C" { -std::int32_t APS5_VABI sceAudioPropagationSystemQueryMemory(const void* options, AudioPropagationSystemMemory* outMemory) { - require(options != nullptr, __func__); - requireDescriptor(outMemory != nullptr ? &outMemory->desc : nullptr, SystemMemoryId, SystemMemorySize, __func__); - outMemory->size_cpu_mem = sizeof(SystemState); +std::int32_t APS5_VABI sceAudioPropagationSystemQueryMemory(const SystemOptions* options, AudioPropagationSystemMemory* outMemory) { + if (!options || !outMemory) return ErrorNullPointer; + if (outMemory->desc.id != SystemMemoryId || outMemory->desc.size != SystemMemorySize) return static_cast(0x8A700007); + outMemory->p_cpu_mem = nullptr; + outMemory->size_cpu_mem = 0; + outMemory->p_gpu_mem = nullptr; outMemory->size_gpu_mem = 0; + if (options->desc.id != SystemOptionsId || options->desc.size != SystemOptionsSize) return static_cast(0x8A700007); + if (options->maxSources == 0 || options->maxSources > 64 || options->maxMaterials == 0 || options->maxMaterials > 64 || options->parameter24 != 6 || options->reflectionOrder == 0 || options->reflectionOrder > 2) return ErrorInvalidArgument; + const auto frameSamples = options->frameSamples; + if (frameSamples != 256 && frameSamples != 512 && frameSamples != 1024 && frameSamples != 2048 && frameSamples != 4096) return ErrorInvalidArgument; + if (!(options->propagationSpeed > 0x1p-23f) || options->propagationSpeed > 1372.0f || !(options->maxPathLength > 0x1p-23f) || options->maxPathLength > 1372.0f || options->flags > 31 || ((options->flags & 1) && frameSamples != 512)) return ErrorInvalidArgument; + if (options->flags & 1) throw std::runtime_error(std::string(__func__) + ": GPU memory requirements depend on an unsupported native runtime query"); + const std::uint64_t sources = options->maxSources; + const std::uint64_t paths = sources * (options->reflectionOrder == 1 ? 6 : 36); + const float delaySeconds = options->maxPathLength / options->propagationSpeed; + const float delaySamples = delaySeconds * 48000.0f; + const auto delayCapacity = static_cast(std::ceil(delaySamples)); + std::uint64_t bytes = sources * 1024 + paths * (32 * (options->reflectionOrder + 2) + 8928); + bytes += frameSamples == 256 ? 2048 : 4 * frameSamples; + bytes += 4 * delayCapacity * sources; + bytes += bytes & 15; + bytes += 160 * paths + 12352 + 4208 * options->maxMaterials; + if (frameSamples == 256) bytes += sources * ((options->flags & 2) ? 36896 : 16416); + if (options->flags & 4) bytes += 2000656; + if (options->flags & 8) bytes += 288 * paths; + if (options->flags & 16) bytes += 144 * paths; + outMemory->size_cpu_mem = bytes + 5368 - ((bytes & 15) ^ 8); return 0; } -std::int32_t APS5_VABI sceAudioPropagationSystemCreate(const void* options, const AudioPropagationSystemMemory* memory, AudioPropagationHandle* outSystem) { +std::int32_t APS5_VABI sceAudioPropagationSystemCreate(const SystemOptions* options, const AudioPropagationSystemMemory* memory, AudioPropagationHandle* outSystem) { require(options != nullptr && outSystem != nullptr, __func__); requireDescriptor(memory != nullptr ? &memory->desc : nullptr, SystemMemoryId, SystemMemorySize, __func__); + auto requiredMemory = *memory; + const auto result = sceAudioPropagationSystemQueryMemory(options, &requiredMemory); + if (result != 0) return result; const auto address = reinterpret_cast(memory->p_cpu_mem); - require(address != 0 && address % alignof(SystemState) == 0 && memory->size_cpu_mem >= sizeof(SystemState) && memory->size_gpu_mem == 0, __func__); + require(address != 0 && address % alignof(SystemState) == 0 && memory->size_cpu_mem >= requiredMemory.size_cpu_mem && memory->size_gpu_mem == 0, __func__); auto& state = registry(); std::lock_guard lock(state.mutex); require(!state.systems.contains(address), __func__); - *static_cast(memory->p_cpu_mem) = SystemState{SystemMagic, 0}; + *static_cast(memory->p_cpu_mem) = SystemState{SystemMagic, options->frameSamples, options->maxSources}; state.systems.insert(address); *outSystem = address; return 0; @@ -257,9 +286,11 @@ std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPathCount(AudioPropagati std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPath(AudioPropagationHandle source, std::uint32_t index, AudioPropagationHandle* outPath) { auto& state = registry(); std::lock_guard lock(state.mutex); - requireObject(state, source, Kind::Source, __func__); - static_cast(outPath); - throw std::invalid_argument(std::string(__func__) + ": index " + std::to_string(index) + " out of range (0 audio paths)"); + const auto found = state.objects.find(source); + if (found == state.objects.end() || found->second.kind != Kind::Source || !liveSystem(state, found->second.system)) return ErrorInvalidHandle; + if (!outPath) return ErrorNullPointer; + static_cast(index); + return ErrorInvalidArgument; } std::int32_t APS5_VABI sceAudioPropagationSourceGetRays(AudioPropagationHandle source, void* rays, std::uint32_t* count) { @@ -271,15 +302,14 @@ std::int32_t APS5_VABI sceAudioPropagationSourceGetRays(AudioPropagationHandle s return 0; } -std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropagationHandle source, const void* rays, std::uint32_t rayCount, std::uint32_t flags, void* paths, std::uint32_t pathCount) { +std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropagationHandle source, const void* rays, std::uint32_t rayCount, std::uint32_t reflectionOrder, void* paths, std::uint32_t pathCount) { auto& state = registry(); std::lock_guard lock(state.mutex); - requireObject(state, source, Kind::Source, __func__); - static_cast(rays); - static_cast(flags); - static_cast(paths); - requireNoPropagation(rayCount == 0 && pathCount == 0, __func__); - return 0; + const auto found = state.objects.find(source); + if (found == state.objects.end() || found->second.kind != Kind::Source || !liveSystem(state, found->second.system)) return ErrorInvalidHandle; + if (!rays || !paths) return ErrorNullPointer; + if (rayCount != 6 || reflectionOrder == 0 || pathCount == 0) return ErrorInvalidArgument; + throw std::runtime_error(__func__); } std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHandle source, const void* entries, std::uint32_t count) { @@ -291,7 +321,7 @@ std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHa return 0; } -std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const void* data, float gain) { +std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const AudioPath* data, float gain) { static_cast(path); static_cast(data); static_cast(gain); @@ -299,19 +329,74 @@ std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHan return 0; } -std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle system, const RenderInfo* infos, std::uint32_t count) { +std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle system, RenderInfo* infos, std::uint32_t count) { require(infos != nullptr && count != 0, __func__); auto& state = registry(); std::lock_guard lock(state.mutex); requireSystem(state, system, __func__); + const auto& configuration = *reinterpret_cast(system); + require(count <= configuration.maxSources, __func__); + const auto requiredBytes = static_cast(configuration.frameSamples) * 16 * sizeof(float); for (std::uint32_t index = 0; index < count; ++index) { const auto& info = infos[index]; requireDescriptor(&info.desc, RenderInfoId, RenderInfoSize, __func__); require(requireObject(state, info.source, Kind::Source, __func__).system == system, __func__); - require(info.output != nullptr && info.outputSize != 0, __func__); + require(info.output != nullptr && info.outputSize >= requiredBytes, __func__); if (info.format != RenderFormat) throw std::runtime_error(std::string(__func__) + ": render format " + std::to_string(info.format) + " is unknown"); } - for (std::uint32_t index = 0; index < count; ++index) std::memset(infos[index].output, 0, infos[index].outputSize); + for (std::uint32_t index = 0; index < count; ++index) { + infos[index].channelCount = 0; + } + return 0; +} + +int APS5_VABI sceAudioPropagationPathGetNumPoints(AudioPropagationHandle path, std::uint32_t* count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationReportApi(std::uint32_t category, AudioPropagationHandle handle, std::int32_t result, std::int32_t operation, const char* function, const char* format, ...) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationResetAttributes(Attribute* attributes, std::uint32_t count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationSourceQueryInfo(AudioPropagationHandle source, SourceInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationSystemLock(AudioPropagationHandle system) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationSystemQueryInfo(AudioPropagationHandle system, SystemInfo* info, std::size_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationReportRegisterHandler(std::uint32_t category, ReportHandler handler, void* userData, ReportRegistration** registration) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationReportUnregisterHandler(ReportRegistration* registration) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationSystemEnumHandles(AudioPropagationHandle* handles, std::uint32_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioPropagationSystemUnlock(AudioPropagationHandle system) { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceAudioSystem/Export.cpp b/core/libs/prx/libSceAudioSystem/Export.cpp new file mode 100644 index 0000000000..d5c4291d01 --- /dev/null +++ b/core/libs/prx/libSceAudioSystem/Export.cpp @@ -0,0 +1,18 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAudioSystemSystemResume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioSystemSystemSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAudiodReport/Export.cpp b/core/libs/prx/libSceAudiodReport/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAudiodReport/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAudiodec.native/Export.cpp b/core/libs/prx/libSceAudiodec.native/Export.cpp new file mode 100644 index 0000000000..4f00015020 --- /dev/null +++ b/core/libs/prx/libSceAudiodec.native/Export.cpp @@ -0,0 +1,78 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAudiodecClearContextEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecCreateDecoderEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDecode2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDecode2Ex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDecode2WithPriority(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDecode2WithPriorityEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDecodeEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDecodeWithPriority(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDecodeWithPriorityEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDeleteDecoderEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecGetContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecRegisterCodec(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAudiodec/Export.cpp b/core/libs/prx/libSceAudiodec/Export.cpp index 76722d3554..5e2b911800 100644 --- a/core/libs/prx/libSceAudiodec/Export.cpp +++ b/core/libs/prx/libSceAudiodec/Export.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include #include #include "SceTypes.hpp" @@ -66,9 +68,9 @@ std::int32_t wordSizeOf(const void* param) { return static_cast(param)->i_bw_pcm; } -std::int32_t validateWordSize(std::int32_t wordSize) { - if (wordSize == Audiodec::WORD_SIZE_16BIT || wordSize == Audiodec::WORD_SIZE_FLOAT) return 0; - if (wordSize == 0) NotImplemented_nid_no_patch("libSceAudiodec 24-bit PCM output"); +std::int32_t validateWordSize(std::int32_t wordSize, std::uint32_t codecType) { + if (wordSize == Audiodec::WORD_SIZE_16BIT) return 0; + if (wordSize == Audiodec::WORD_SIZE_FLOAT && codecType != TYPE_MP3) return 0; return ERROR_INVALID_WORD_LENGTH; } @@ -88,11 +90,11 @@ std::int32_t validateCtrl(const AudiodecCtrl* ctrl, std::uint32_t codecType) { if (infoSize != sizeof(AudiodecMp3Info)) return ERROR_INVALID_BSI_INFO_SIZE; break; default: - if (paramSize < sizeof(AudiodecParamM4aac)) return ERROR_INVALID_PARAM_SIZE; + if (paramSize != sizeof(AudiodecParamM4aac) && paramSize != 28 && paramSize != 32) return ERROR_INVALID_PARAM_SIZE; if (infoSize != sizeof(AudiodecM4aacInfo)) return ERROR_INVALID_BSI_INFO_SIZE; break; } - return validateWordSize(wordSizeOf(ctrl->pParam)); + return validateWordSize(wordSizeOf(ctrl->pParam), codecType); } std::int32_t validateBuffers(const AudiodecCtrl* ctrl) { @@ -133,13 +135,21 @@ void fillMp3Info(AudiodecMp3Info* info, const Audiodec::Mp3Header& header) { } std::int32_t createAac(const AudiodecParamM4aac* param, AudiodecM4aacInfo* info, Instance& instance) { + std::uint32_t extensionMode = 0; + std::uint32_t outputMode = 0; + const auto* bytes = reinterpret_cast(param); + if (param->ui_size >= 28) std::memcpy(&extensionMode, bytes + 24, sizeof(extensionMode)); + if (extensionMode > 1) return static_cast(0x807F0305); + if (param->ui_size == 32) std::memcpy(&outputMode, bytes + 28, sizeof(outputMode)); + if (outputMode > 2) return static_cast(0x807F0306); + if (extensionMode != 0 || outputMode != 0) throw std::runtime_error("M4AAC extended decoding modes are not implemented"); if (param->ui_config_number != M4AAC_CONFIG_ADTS && param->ui_config_number != M4AAC_CONFIG_RAW) return ERROR_M4AAC_INVALID_CONFIG_NUMBER; const std::uint32_t sampleRate = Audiodec::AacSampleRate(param->ui_sampling_freq_index); if (param->ui_config_number == M4AAC_CONFIG_RAW && sampleRate == 0) return ERROR_M4AAC_INVALID_SAMPLING_FREQ; - if (param->ui_max_channels > 8) return ERROR_M4AAC_INVALID_MAX_CHANNELS; - if (param->ui_enable_heaac > 1) return ERROR_M4AAC_INVALID_ENABLE_HEAAC; - const std::uint32_t channels = param->ui_max_channels == 0 ? 2 : param->ui_max_channels; - instance.decoder = Audiodec::CreateAac(param->i_bw_pcm, param->ui_config_number == M4AAC_CONFIG_ADTS, param->ui_sampling_freq_index, channels); + if (param->ui_max_channels != 1 && param->ui_max_channels != 2 && param->ui_max_channels != 6 && param->ui_max_channels != 8) return ERROR_M4AAC_INVALID_MAX_CHANNELS; + if (param->ui_enable_heaac > 1 || (param->ui_enable_heaac == 1 && param->ui_max_channels > 2)) return ERROR_M4AAC_INVALID_ENABLE_HEAAC; + const std::uint32_t channels = param->ui_max_channels; + instance.decoder = Audiodec::CreateAac(param->i_bw_pcm, param->ui_config_number == M4AAC_CONFIG_ADTS, param->ui_sampling_freq_index, channels, param->ui_enable_heaac == 1); instance.channels = channels; instance.sampleRate = sampleRate; info->ui_sampling_freq = sampleRate; diff --git a/core/libs/prx/libSceAudiodec/src/Codecs.cpp b/core/libs/prx/libSceAudiodec/src/Codecs.cpp index aca46d88d1..c3324c3b1a 100644 --- a/core/libs/prx/libSceAudiodec/src/Codecs.cpp +++ b/core/libs/prx/libSceAudiodec/src/Codecs.cpp @@ -122,35 +122,103 @@ public: } }; +struct AacLayout { + std::uint32_t objectType; + std::uint32_t samplingIndex; + std::uint32_t channelConfiguration; + bool operator==(const AacLayout&) const = default; +}; + +enum class Sbr { + Implicit, + Absent, + WithoutParametricStereo, +}; + +std::vector audioSpecificConfig(const AacLayout& layout, Sbr sbr, std::uint32_t sbrSampleRate) { + std::vector bytes; + std::size_t bits = 0; + const auto put = [&](std::uint32_t value, int width) { + for (int bit = width - 1; bit >= 0; --bit, ++bits) { + if (bits % 8 == 0) bytes.push_back(0); + bytes.back() |= static_cast((value >> bit & 1) << (7 - bits % 8)); + } + }; + put(layout.objectType, 5); + put(layout.samplingIndex, 4); + put(layout.channelConfiguration, 4); + put(0, 3); + if (sbr == Sbr::Implicit) return bytes; + put(0x2B7, 11); + put(5, 5); + put(sbr == Sbr::WithoutParametricStereo, 1); + if (sbr == Sbr::WithoutParametricStereo) { + const auto* rate = std::find(std::begin(AAC_SAMPLE_RATES), std::end(AAC_SAMPLE_RATES), sbrSampleRate); + if (rate == std::end(AAC_SAMPLE_RATES)) { + put(15, 4); + put(sbrSampleRate, 24); + } else { + put(static_cast(rate - std::begin(AAC_SAMPLE_RATES)), 4); + } + put(0x548, 11); + put(0, 1); + } + return bytes; +} + class AacDecoder final : public FfmpegDecoder { public: - AacDecoder(std::int32_t wordSize, bool adts, std::vector> configs) - : FfmpegDecoder(AV_CODEC_ID_AAC, wordSize, configs.empty() ? std::vector{} : configs.front()), adts(adts), configs(std::move(configs)) {} + AacDecoder(std::int32_t wordSize, bool adts, bool highEfficiency, std::vector layouts) + : FfmpegDecoder(AV_CODEC_ID_AAC, wordSize, layouts.empty() ? std::vector{} : audioSpecificConfig(layouts.front(), highEfficiency ? Sbr::Implicit : Sbr::Absent, 0)), + adts(adts), highEfficiency(highEfficiency), sbr(highEfficiency ? Sbr::Implicit : Sbr::Absent), layouts(std::move(layouts)) {} DecodeResult Decode(const std::uint8_t* data, std::size_t size, std::uint8_t* pcm, std::size_t pcmSize) override { - std::size_t frameBytes = size; - if (adts) { - if (size < 7 || data[0] != 0xFF || (data[1] & 0xF6) != 0xF0) return {DecodeStatus::InvalidData}; - frameBytes = static_cast((data[3] & 3) << 11 | data[4] << 3 | data[5] >> 5); - if (frameBytes < 7) return {DecodeStatus::InvalidData}; - if (frameBytes > size) return {DecodeStatus::PartialInput}; - } - if (configs.size() <= 1) return decodePacket(data, frameBytes, pcm, pcmSize); - for (std::size_t index = 0; index < configs.size(); ++index) { - if (index != 0) reopen(configs[index]); - const DecodeResult result = decodePacket(data, frameBytes, pcm, pcmSize); - if (result.status != DecodeStatus::InvalidData) { - configs = {configs[index]}; - return result; + if (!adts) return decodeAccessUnit(data, size, pcm, pcmSize); + if (size < 7 || data[0] != 0xFF || (data[1] & 0xF6) != 0xF0) return {DecodeStatus::InvalidData}; + const auto frameBytes = static_cast((data[3] & 3) << 11 | data[4] << 3 | data[5] >> 5); + const std::size_t headerBytes = (data[1] & 1) != 0 ? 7 : 9; + if (frameBytes <= headerBytes) return {DecodeStatus::InvalidData}; + if (frameBytes > size) return {DecodeStatus::PartialInput}; + const AacLayout layout{(data[2] >> 6) + 1u, data[2] >> 2 & 15u, (data[2] & 1u) << 2 | data[3] >> 6}; + if (layout.channelConfiguration == 0) { + if (!layouts.empty()) { + layouts.clear(); + reopen({}); } + return decodePacket(data, frameBytes, pcm, pcmSize); } - reopen(configs.front()); - return {DecodeStatus::InvalidData}; + if (layouts.empty() || layouts.front() != layout) { + layouts = {layout}; + sbr = highEfficiency ? Sbr::Implicit : Sbr::Absent; + reopen(audioSpecificConfig(layout, sbr, 0)); + } + DecodeResult result = decodeAccessUnit(data + headerBytes, frameBytes - headerBytes, pcm, pcmSize); + result.consumed = frameBytes; + return result; } private: + DecodeResult decodeAccessUnit(const std::uint8_t* data, std::size_t size, std::uint8_t* pcm, std::size_t pcmSize) { + for (std::size_t index = 0; index < layouts.size(); ++index) { + if (index != 0) reopen(audioSpecificConfig(layouts[index], sbr, 0)); + DecodeResult result = decodePacket(data, size, pcm, pcmSize); + if (result.status == DecodeStatus::InvalidData) continue; + layouts = {layouts[index]}; + if (sbr == Sbr::Implicit && layouts.front().channelConfiguration == 1 && result.channels > 1) { + sbr = Sbr::WithoutParametricStereo; + reopen(audioSpecificConfig(layouts.front(), sbr, result.sampleRate)); + result = decodePacket(data, size, pcm, pcmSize); + } + return result; + } + if (layouts.size() > 1) reopen(audioSpecificConfig(layouts.front(), sbr, 0)); + return {DecodeStatus::InvalidData}; + } + bool adts; - std::vector> configs; + bool highEfficiency; + Sbr sbr; + std::vector layouts; }; class At9Decoder final : public Decoder { @@ -239,20 +307,16 @@ std::unique_ptr CreateMp3(std::int32_t wordSize) { return std::make_unique(wordSize); } -std::unique_ptr CreateAac(std::int32_t wordSize, bool adts, std::uint32_t samplingFreqIndex, std::uint32_t channels) { - std::vector> configs; +std::unique_ptr CreateAac(std::int32_t wordSize, bool adts, std::uint32_t samplingFreqIndex, std::uint32_t channels, bool highEfficiency) { + std::vector layouts; if (!adts) { const std::uint32_t lowComplexity = 2; - const auto config = [&](std::uint32_t channelConfiguration) { - return std::vector{static_cast(lowComplexity << 3 | samplingFreqIndex >> 1), - static_cast((samplingFreqIndex & 1) << 7 | channelConfiguration << 3)}; - }; - configs.push_back(config(channels < 2 ? 1 : 2)); + layouts.push_back({lowComplexity, samplingFreqIndex, channels < 2 ? 1u : 2u}); for (std::uint32_t channelConfiguration = 3; channelConfiguration <= 7; ++channelConfiguration) { - if ((channelConfiguration == 7 ? 8 : channelConfiguration) <= channels) configs.push_back(config(channelConfiguration)); + if ((channelConfiguration == 7 ? 8 : channelConfiguration) <= channels) layouts.push_back({lowComplexity, samplingFreqIndex, channelConfiguration}); } } - return std::make_unique(wordSize, adts, std::move(configs)); + return std::make_unique(wordSize, adts, highEfficiency, std::move(layouts)); } std::unique_ptr CreateAt9(std::int32_t wordSize, const std::uint8_t (&config)[4], At9Format& format) { diff --git a/core/libs/prx/libSceAudiodec/src/Codecs.hpp b/core/libs/prx/libSceAudiodec/src/Codecs.hpp index ebf8434eb2..6d43c67e6b 100644 --- a/core/libs/prx/libSceAudiodec/src/Codecs.hpp +++ b/core/libs/prx/libSceAudiodec/src/Codecs.hpp @@ -57,7 +57,7 @@ bool ParseMp3Header(const std::uint8_t* data, std::size_t size, Mp3Header& heade std::uint32_t AacSampleRate(std::uint32_t index); bool ValidAt9Config(const std::uint8_t (&config)[4]); std::unique_ptr CreateMp3(std::int32_t wordSize); -std::unique_ptr CreateAac(std::int32_t wordSize, bool adts, std::uint32_t samplingFreqIndex, std::uint32_t channels); +std::unique_ptr CreateAac(std::int32_t wordSize, bool adts, std::uint32_t samplingFreqIndex, std::uint32_t channels, bool highEfficiency); std::unique_ptr CreateAt9(std::int32_t wordSize, const std::uint8_t (&config)[4], At9Format& format); } diff --git a/core/libs/prx/libSceAudiodecCpu/Export.cpp b/core/libs/prx/libSceAudiodecCpu/Export.cpp new file mode 100644 index 0000000000..c0c2f83744 --- /dev/null +++ b/core/libs/prx/libSceAudiodecCpu/Export.cpp @@ -0,0 +1,48 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAudiodecCpuClearContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecCpuDecode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecCpuInitDecoder(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecCpuInternalClearContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecCpuInternalDecode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecCpuInternalInitDecoder(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecCpuInternalQueryMemSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecCpuQueryMemSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAudiodecCpuAlac/Export.cpp b/core/libs/prx/libSceAudiodecCpuAlac/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceAudiodecCpuAlac/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAudiodecCpuDdp/Export.cpp b/core/libs/prx/libSceAudiodecCpuDdp/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAudiodecCpuDdp/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAudiodecCpuDts/Export.cpp b/core/libs/prx/libSceAudiodecCpuDts/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAudiodecCpuDts/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAudiodecCpuDtsHdLbr/Export.cpp b/core/libs/prx/libSceAudiodecCpuDtsHdLbr/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAudiodecCpuDtsHdLbr/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAudiodecCpuDtsHdMa/Export.cpp b/core/libs/prx/libSceAudiodecCpuDtsHdMa/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAudiodecCpuDtsHdMa/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAudiodecCpuFlac/Export.cpp b/core/libs/prx/libSceAudiodecCpuFlac/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAudiodecCpuFlac/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAudiodecCpuLpcm/Export.cpp b/core/libs/prx/libSceAudiodecCpuLpcm/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAudiodecCpuLpcm/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAudiodecCpuM4aac/Export.cpp b/core/libs/prx/libSceAudiodecCpuM4aac/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAudiodecCpuM4aac/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAudiodecCpuTrhd/Export.cpp b/core/libs/prx/libSceAudiodecCpuTrhd/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAudiodecCpuTrhd/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAutoMounterClient/Export.cpp b/core/libs/prx/libSceAutoMounterClient/Export.cpp new file mode 100644 index 0000000000..77af95c0ff --- /dev/null +++ b/core/libs/prx/libSceAutoMounterClient/Export.cpp @@ -0,0 +1,56 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +struct AutoMounterUsbDeviceInfo { + char deviceName[128]; + std::byte metadata[22]; + std::byte data0[256]; + std::byte padding[2]; + std::uint64_t data0Size; + std::byte data1[256]; + std::uint64_t data1Size; + std::byte data2[256]; + std::uint64_t data2Size; +}; + +static_assert(sizeof(AutoMounterUsbDeviceInfo) == 944); +static_assert(offsetof(AutoMounterUsbDeviceInfo, data0) == 150); +static_assert(offsetof(AutoMounterUsbDeviceInfo, data0Size) == 408); +static_assert(offsetof(AutoMounterUsbDeviceInfo, data1) == 416); +static_assert(offsetof(AutoMounterUsbDeviceInfo, data1Size) == 672); +static_assert(offsetof(AutoMounterUsbDeviceInfo, data2) == 680); +static_assert(offsetof(AutoMounterUsbDeviceInfo, data2Size) == 936); + +extern "C" { + +int APS5_VABI sceAutoMounterClientGetUsbDeviceInfo(const char* deviceName, AutoMounterUsbDeviceInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAutoMounterClientGetUsbDeviceList(char (*deviceNames)[64], std::int32_t* count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceAutoMounterClientInit(PthreadAttr* threadAttr) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAutoMounterClientRegisterCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAutoMounterClientTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAutoMounterClientUnregisterCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAvPlayer.native/Export.cpp b/core/libs/prx/libSceAvPlayer.native/Export.cpp new file mode 100644 index 0000000000..2584dda091 --- /dev/null +++ b/core/libs/prx/libSceAvPlayer.native/Export.cpp @@ -0,0 +1,18 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAvPlayerPrintf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvPlayerVprintf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAvPlayer/CMakeLists.txt b/core/libs/prx/libSceAvPlayer/CMakeLists.txt index bdc296e7eb..d8a652d7ff 100644 --- a/core/libs/prx/libSceAvPlayer/CMakeLists.txt +++ b/core/libs/prx/libSceAvPlayer/CMakeLists.txt @@ -11,11 +11,24 @@ 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) + add_test(NAME guest_avplayer_paused_first_video COMMAND guest_avplayer_tests --paused-first-video) + set_tests_properties(guest_avplayer_paused_first_video PROPERTIES + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/guest_avplayer_paused_first_video + TIMEOUT 20) + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/guest_avplayer_paused_first_video) set_tests_properties(guest_avplayer PROPERTIES WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/guest_avplayer TIMEOUT 60) + 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_paused_first_video PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_avplayer_callback_close PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAvPlayer/Export.cpp b/core/libs/prx/libSceAvPlayer/Export.cpp index fc78bf88b5..93d3d8fb0f 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; } @@ -175,10 +175,9 @@ int APS5_VABI sceAvPlayerStart(AvPlayerInternal* h) { return ToPlayer(h)->Start(); } -int APS5_VABI sceAvPlayerStartEx(AvPlayerInternal* h, const void* start_info_ex) { - (void)start_info_ex; - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->Start(); +int APS5_VABI sceAvPlayerStartEx(AvPlayerInternal* handle, const StartInfo* startInfo) { + if (handle == nullptr || startInfo == nullptr || startInfo->size != sizeof(*startInfo)) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; + return ToPlayer(handle)->Start(startInfo->startTime); } int APS5_VABI sceAvPlayerStop(AvPlayerInternal* h) { @@ -191,5 +190,13 @@ int APS5_VABI sceAvPlayerStreamCount(AvPlayerInternal* h) { return ToPlayer(h)->StreamCount(); } +} + +namespace AvPlayer { + +void SetAvPlayerClockForTest_nid_no_patch(AvPlayerInternal* h, double milliseconds) { + if (h) ToPlayer(h)->SetClockTimeForTest(milliseconds); +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp index 740f552d2e..1c82453073 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; @@ -50,6 +51,13 @@ public: virtual void OnError() = 0; }; +struct StartInfo { + std::uint64_t size; + std::uint64_t startTime; +}; + +static_assert(sizeof(StartInfo) == 16); + struct SourceSettings { AvPlayerMemAllocator memory; AvPlayerFileReplacement file; @@ -65,7 +73,7 @@ public: virtual bool EnableStream(std::uint32_t index) = 0; virtual bool DisableStream(std::uint32_t index) = 0; virtual bool ChangeStream(std::uint32_t from, std::uint32_t to) = 0; - virtual int Start() = 0; + virtual int Start(std::uint64_t startTime) = 0; virtual void Stop() = 0; virtual void Pause() = 0; virtual void Resume() = 0; @@ -78,15 +86,18 @@ public: virtual bool GetAudioData(AvPlayerFrameInfo& info) = 0; virtual std::uint64_t CurrentTime() = 0; virtual bool Finished() = 0; + virtual void SetClockTimeForTest(double milliseconds) = 0; }; std::unique_ptr OpenSource(const SourceSettings& settings, const std::string& path, ISourceEvents& events); std::uint32_t DetectSourceType(std::string_view path); +void SetAvPlayerClockForTest_nid_no_patch(AvPlayerInternal* h, double milliseconds); + class Player final : public AvPlayerInternal, private ISourceEvents { public: - Player(const AvPlayerInitData& init, std::uint32_t bufferCount); + Player(const AvPlayerInitData& init, std::uint32_t bufferCount, bool ps5Layout = false); ~Player() override; Player(const Player&) = delete; Player& operator=(const Player&) = delete; @@ -99,7 +110,7 @@ public: int EnableStream(std::uint32_t index); int DisableStream(std::uint32_t index); int ChangeStream(std::uint32_t from, std::uint32_t to); - int Start(); + int Start(std::uint64_t startTime = 0); int Stop(); int Pause(); int Resume(); @@ -112,6 +123,7 @@ public: bool GetAudioData(AvPlayerFrameInfo& info); bool IsActive(); std::uint64_t CurrentTime(); + void SetClockTimeForTest(double milliseconds); private: enum class State { Initial, Ready, Play, Pause, Stop, EndOfFile, Error }; @@ -126,18 +138,19 @@ 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(); bool readyLocked() const; - int startLocked(); + int startLocked(std::uint64_t startTime = 0); int stopLocked(); AvPlayerFileReplacement file{}; AvPlayerMemAllocator memory{}; AvPlayerEventReplacement callback{}; bool autoStartEnabled = false; + bool ps5Layout = false; char language[4]{}; std::uint32_t videoBuffers = 2; @@ -154,6 +167,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..90c3b57dba 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; @@ -257,15 +265,15 @@ int Player::ChangeStream(std::uint32_t from, std::uint32_t to) { return SCE_OK; } -int Player::Start() { +int Player::Start(std::uint64_t startTime) { std::lock_guard lock(mutex); - return startLocked(); + return startLocked(startTime); } -int Player::startLocked() { +int Player::startLocked(std::uint64_t startTime) { if (!source) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; if (state != State::Ready && state != State::Stop && stopLocked() != SCE_OK) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; - const int result = source->Start(); + const int result = source->Start(startTime); if (result != SCE_OK) { previous = state; state = State::Error; @@ -378,4 +386,9 @@ std::uint64_t Player::CurrentTime() { return source ? source->CurrentTime() : 0; } +void Player::SetClockTimeForTest(double milliseconds) { + std::lock_guard lock(mutex); + if (source) source->SetClockTimeForTest(milliseconds); +} + } diff --git a/core/libs/prx/libSceAvPlayer/src/Source.cpp b/core/libs/prx/libSceAvPlayer/src/Source.cpp index 7778568ddc..4d31c38f1d 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) { @@ -137,11 +137,16 @@ private: class Clock { public: double Now() const { + if (overrideTime >= 0.0) return overrideTime; if (!running || paused) return base; const std::chrono::duration elapsed = std::chrono::steady_clock::now() - wall; return std::max(0.0, base + elapsed.count() * speed / NormalSpeed); } + void SetOverrideForTest(double milliseconds) { + overrideTime = milliseconds; + } + void Rebase(double milliseconds) { base = milliseconds; wall = std::chrono::steady_clock::now(); @@ -176,6 +181,7 @@ private: std::int32_t speed = NormalSpeed; bool running = false; bool paused = false; + double overrideTime = -1.0; }; enum class PacketKind { Data, Flush, Drain, End, Reopen }; @@ -321,7 +327,8 @@ public: return true; } - int Start() override { + int Start(std::uint64_t startTime) override { + if (duration != 0 && startTime > duration) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; if (video.stream < 0 && audio.stream < 0) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; for (auto* decoder : {&video, &audio}) { if (decoder->stream < 0) continue; @@ -332,7 +339,7 @@ public: } } addVideoDecodeAheadBuffers(); - const auto start = startOffset; + const auto start = startTime != 0 ? startTime : startOffset; startOffset = 0; seek(start); { @@ -343,6 +350,7 @@ public: demuxEpoch = 1; minEpoch = 1; clockEpoch = 0; + loopEpochs.clear(); audioDriving = false; presented = false; lastPresented = 0; @@ -431,6 +439,11 @@ public: condition.notify_all(); } + void SetClockTimeForTest(double milliseconds) override { + std::lock_guard lock(mutex); + presentationClock.SetOverrideForTest(milliseconds); + } + bool GetVideoData(AvPlayerFrameInfoEx& info) override { bool taken = false; { @@ -506,7 +519,7 @@ private: while (!frames.empty() && frames.front().epoch < stale) recycleFront(video); if (frames.empty()) return false; const auto& front = frames.front(); - if (paused && front.epoch <= clockEpoch) return false; + if (paused && presented && front.epoch <= clockEpoch) return false; if (front.epoch > clockEpoch) { if (synced && audioDriving && speed == NormalSpeed) return false; if (synced && front.seamless && presented && presentationClock.Now() < static_cast(lastPresented + frameDuration())) return false; @@ -515,7 +528,8 @@ private: } if (synced || audioDriving) { const auto now = presentationClock.Now(); - if (synced && static_cast(front.info.timestamp) > now) return false; + const double syncTolerance = static_cast(frameDuration()) * 0.75; + if (synced && static_cast(front.info.timestamp) > now + syncTolerance) return false; while (frames.size() > 1 && frames[1].epoch == frames.front().epoch && static_cast(frames[1].info.timestamp) <= now) recycleFront(video); } present(video, info); @@ -579,6 +593,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 +732,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 +778,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 +811,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 +1003,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 +1096,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/libSceAvPlayerStreaming/Export.cpp b/core/libs/prx/libSceAvPlayerStreaming/Export.cpp new file mode 100644 index 0000000000..b484bc1af2 --- /dev/null +++ b/core/libs/prx/libSceAvPlayerStreaming/Export.cpp @@ -0,0 +1,174 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN +int APS5_VABI MvpHttpAbort_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpDeInit_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpGet_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpGetLastError_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpHead_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpHeadCleanup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpInit_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpPost_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpSetCert_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpSetRecvTimeout_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpSetRedirectCb_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsCloseFile(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsDisableRepresentation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsEnableRepresentation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsEnableStream(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetAvailableBW(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetLiveStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetNextAuInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetNextUnitData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetRepresentationCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetRepresentationInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetSeekWindow(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetStreamInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetUrlEncryptionType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsJumpPTS(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsOpenFile(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsOpenFileEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsReleaseBuffer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsRepresentationIsEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsSetAvailableBW(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsSetStartingBW(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsStartFileStreaming(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsStopFileStreaming(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAvSetting/Export.cpp b/core/libs/prx/libSceAvSetting/Export.cpp new file mode 100644 index 0000000000..e53788bea3 --- /dev/null +++ b/core/libs/prx/libSceAvSetting/Export.cpp @@ -0,0 +1,130 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +int APS5_VABI sceAvControlGetPuTypeByMonitorInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsVideoOutModeSupportedByMonitorInfoWithOptions(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetDitherModeForDiag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetLayoutModeForDiag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetMonitorFakeMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetCurrentOutputSysMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetNativeHdmiMonitorInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetRawMonitorInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetKsvList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetRawEdid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlRemoveProcessOutputMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlUpdateDriverStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlNotifyUmdEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlAddCallbacksForLnc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlAddCallbacksForResourceArbitrator(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlRemoveCallbacks(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlCheckCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlWaitCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetColorMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlCheckMonitorInfoCapability(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlCheckVideoOutMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetCheckoutputMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetSystemVideoMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlDebugAddCallbacks(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAvcap2/Export.cpp b/core/libs/prx/libSceAvcap2/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAvcap2/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceAvcap2mvr/Export.cpp b/core/libs/prx/libSceAvcap2mvr/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceAvcap2mvr/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceBackupRestoreUtil/Export.cpp b/core/libs/prx/libSceBackupRestoreUtil/Export.cpp new file mode 100644 index 0000000000..77695475b9 --- /dev/null +++ b/core/libs/prx/libSceBackupRestoreUtil/Export.cpp @@ -0,0 +1,88 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceBackupRestoreUtilBackupData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBackupRestoreUtilCancelBackupData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBackupRestoreUtilCancelGetRequiredSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBackupRestoreUtilCancelGetTitles() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBackupRestoreUtilCancelRestoreData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBackupRestoreUtilDeleteBackupData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBackupRestoreUtilGetBackupDataInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBackupRestoreUtilGetBackupParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBackupRestoreUtilGetBackupProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBackupRestoreUtilGetRequiredSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBackupRestoreUtilGetRestoreProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBackupRestoreUtilGetTitles() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBackupRestoreUtilInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBackupRestoreUtilPrepareBackup() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBackupRestoreUtilRestoreData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBackupRestoreUtilTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceBeisobmf/Export.cpp b/core/libs/prx/libSceBeisobmf/Export.cpp new file mode 100644 index 0000000000..a2eee1a93d --- /dev/null +++ b/core/libs/prx/libSceBeisobmf/Export.cpp @@ -0,0 +1,99 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN +int APS5_VABI isobmf_box_getchildbox_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_box_getdiskoffset_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_box_getflags32b_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_box_getmember_length_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_box_getmember_ptr_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_box_getmember_type_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_box_getnextbox_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_box_getstate_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_box_gettype_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_decoder_clear_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_decoder_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_decoder_decode_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_decoder_destroy_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_decoder_get_decoder_tree_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_decoder_getquerydata_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_decoder_next_disk_offset_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_decoder_query_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_memmanager_set_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceBemp2sys/Export.cpp b/core/libs/prx/libSceBemp2sys/Export.cpp new file mode 100644 index 0000000000..3bf12ad835 --- /dev/null +++ b/core/libs/prx/libSceBemp2sys/Export.cpp @@ -0,0 +1,164 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN +int APS5_VABI bemp2sys_logprintf_getlevel_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_logprintf_setfunc_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_logprintf_setlevel_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_memmanager_set_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_packetizer_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_packetizer_destroy_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_packetizer_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_packetizer_release_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_pesparser_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_pesparser_destroy_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_pesparser_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_pesparser_parse_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_pesparser_release_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_pesparser_sendmessage_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_psdecoder_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_psdecoder_destroy_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_psdecoder_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_psdecoder_readpacket_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_psdecoder_release_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_psdecoder_sendmessage_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsdecoder_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsdecoder_destroy_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsdecoder_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsdecoder_readpacket_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsdecoder_release_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsdecoder_sendmessage_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsfilter_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsfilter_destroy_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsfilter_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsfilter_release_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsfilter_sendmessage_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceBgft/Export.cpp b/core/libs/prx/libSceBgft/Export.cpp new file mode 100644 index 0000000000..483969400b --- /dev/null +++ b/core/libs/prx/libSceBgft/Export.cpp @@ -0,0 +1,376 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +struct BgftInitParam { + void* heap; + uint64_t heapSize; +}; + +struct BgftProgress; +struct BgftStorageTaskParam { + std::uint32_t value0; + std::uint32_t value4; + const char* contentId; + const char* text16; + std::byte unused24[8]; + const char* text32; + const char* text40; + const char* text48; + std::uint32_t value56; + std::byte padding60[4]; + const char* text64; + const char* text72; + const char* text80; + const char* text88; + std::uint64_t value96; + std::uint32_t value104; +}; + +static_assert(sizeof(BgftStorageTaskParam) == 112); +static_assert(offsetof(BgftStorageTaskParam, contentId) == 8); +static_assert(offsetof(BgftStorageTaskParam, text32) == 32); +static_assert(offsetof(BgftStorageTaskParam, value56) == 56); +static_assert(offsetof(BgftStorageTaskParam, text64) == 64); +static_assert(offsetof(BgftStorageTaskParam, value96) == 96); +static_assert(offsetof(BgftStorageTaskParam, value104) == 104); + +static_assert(sizeof(BgftInitParam) == 16); +static_assert(offsetof(BgftInitParam, heapSize) == 8); + +extern "C" { + +int APS5_VABI sceBgftServiceInit(const BgftInitParam* params) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceTerm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceDownloadPauseTaskAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceDownloadResumeTaskAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceDownloadStartTaskAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceDownloadStopTaskAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceDownloadFindActivePatchTask(const char* contentId, int32_t* taskId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceDownloadFindActivePupTask(int32_t* taskId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceDownloadFindTaskByContentId(const char* contentId, int32_t subType, int32_t* taskId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceDownloadGetProgress(int32_t taskId, BgftProgress* progress) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceDownloadPauseTask(int32_t taskId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceDownloadResumeTask(int32_t taskId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceDownloadStartTask(int32_t taskId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceDownloadStopTask(int32_t taskId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadFindActiveGameAndGameAcTask(const char* contentId, int32_t* taskId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadFindActiveTask(const char* contentId, int32_t subType, int32_t* taskId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadGetGameAndGameAcProgress(const char* contentId, BgftProgress* progress) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadGetPatchProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadGetPlayGoProgress(const char* contentId, BgftProgress* progress) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadGetProgress(int32_t taskId, BgftProgress* progress) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadGetTaskInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadPauseTask(int32_t taskId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadRegisterTaskByStorageEx(const BgftStorageTaskParam* params, int32_t* taskId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadResumeTask(int32_t taskId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadStopTask(int32_t taskId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadStartTask(int32_t taskId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntDownloadUnregisterTask(int32_t taskId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceIntInit(const BgftInitParam* params) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceBgftServiceDownloadQueryTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugDownloadCorruptPkg(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugDownloadRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugPlayGoGetPlayGoStatusString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugPlayGoIsPaused(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugPlayGoResume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugPlayGoRevertToFullState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugPlayGoRevertToInitialState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugPlayGoRevertToSnapshot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugPlayGoSnapshotByTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugPlayGoSuspend(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadDebugDeleteBgftEnvFile(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadDebugDownloadBgftEnvFile(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadDebugGetBgftEnvInfoString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadDebugGetStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadGetExtUsbUserStorageSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadGetUserStorageSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadPauseTaskAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadQueryTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadRegisterTaskByStorage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadRestartTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadResumeTaskAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadStartTaskAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadStopTaskAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadUnregisterTaskAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntExtUsbCheckActiveDownloadTasks(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntGetTaskInfoValueInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntGetTaskInfoValueString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntGetTaskInfoValueStringIndex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntPlayGoFindTaskId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntPlayGoGetDiscIniChunksInstallProgress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntPlayGoIsPaused(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntPlayGoResume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntTerm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadGetProgress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadQueryTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadRegisterTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadRestartTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadStartTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadStartTaskAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadStopTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadStopTaskAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadUnregisterTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadUnregisterTaskAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceBgsStorage/Export.cpp b/core/libs/prx/libSceBgsStorage/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceBgsStorage/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceBluetoothHid/Export.cpp b/core/libs/prx/libSceBluetoothHid/Export.cpp index 46e728e4fd..63c76ddd55 100644 --- a/core/libs/prx/libSceBluetoothHid/Export.cpp +++ b/core/libs/prx/libSceBluetoothHid/Export.cpp @@ -4,47 +4,159 @@ #include "prx/libc/include/General.hpp" #include +#include + +struct BluetoothHidThreadParam { + std::int32_t priority; + std::uint32_t padding; + std::uint64_t affinityMask; + std::uint32_t reserved; + std::uint32_t tailPadding; +}; + +struct BluetoothHidParam { + const BluetoothHidThreadParam* threadParam; + std::uint32_t reserved; + std::uint32_t padding; +}; + +struct BluetoothHidEvent { + std::uint64_t deviceHandle; + std::uint16_t vendorId; + std::uint16_t productId; + std::uint8_t type; + std::uint8_t status; + std::uint16_t reportSize; + std::uint8_t reserved[8]; + std::uint8_t report[78]; + std::uint8_t padding[2]; +}; + +struct BluetoothHidDeviceInfo; +struct BluetoothHidDeviceName; +using BluetoothHidCallback = void (APS5_VABI *)(const BluetoothHidEvent*, std::int32_t, void*); + +static_assert(sizeof(BluetoothHidThreadParam) == 24); +static_assert(offsetof(BluetoothHidThreadParam, affinityMask) == 8); +static_assert(offsetof(BluetoothHidThreadParam, reserved) == 16); +static_assert(sizeof(BluetoothHidParam) == 16); +static_assert(sizeof(BluetoothHidEvent) == 104); +static_assert(offsetof(BluetoothHidEvent, reportSize) == 14); +static_assert(offsetof(BluetoothHidEvent, report) == 24); + +namespace { +std::mutex stateMutex; +bool initialized = false; +bool callbackRegistered = false; +constexpr std::int32_t InvalidArgument = static_cast(0x81540001u); +constexpr std::int32_t NotInitialized = static_cast(0x81540004u); +constexpr std::int32_t NoCallback = static_cast(0x81540006u); +} extern "C" { std::int32_t APS5_VABI sceBluetoothHidInit() { + std::lock_guard lock(stateMutex); + initialized = true; return 0; } -std::int32_t APS5_VABI sceBluetoothHidRegisterCallback(void* callback, std::int32_t flags, void* param) { - (void)flags; - (void)param; - if (callback == nullptr) throw std::invalid_argument(std::string(__func__) + ": null callback"); +std::int32_t APS5_VABI sceBluetoothHidRegisterCallback(BluetoothHidCallback callback, void* userData, const BluetoothHidParam* param) { + std::lock_guard lock(stateMutex); + if (!initialized) return NotInitialized; + if (callback == nullptr) return InvalidArgument; + if (param != nullptr) { + if (param->reserved != 0) return InvalidArgument; + if (param->threadParam != nullptr) { + if (param->threadParam->reserved != 0) return InvalidArgument; + throw std::runtime_error("sceBluetoothHidRegisterCallback: custom worker scheduling is not implemented"); + } + } + (void)userData; + callbackRegistered = true; return 0; } std::int32_t APS5_VABI sceBluetoothHidUnregisterCallback() { + std::lock_guard lock(stateMutex); + if (!initialized) return NotInitialized; + if (!callbackRegistered) return NoCallback; + callbackRegistered = false; return 0; } std::int32_t APS5_VABI sceBluetoothHidRegisterDevice(std::uint16_t vendorId, std::uint16_t productId) { + std::lock_guard lock(stateMutex); + if (!initialized) return NotInitialized; + if (!callbackRegistered) return NoCallback; (void)vendorId; (void)productId; return 0; } std::int32_t APS5_VABI sceBluetoothHidUnregisterDevice(std::uint16_t vendorId, std::uint16_t productId) { + std::lock_guard lock(stateMutex); + if (!initialized) return NotInitialized; (void)vendorId; (void)productId; return 0; } -int APS5_VABI sceBluetoothHidGetDeviceName() { +int APS5_VABI sceBluetoothHidGetDeviceName(std::uint64_t deviceHandle, BluetoothHidDeviceName* name) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBluetoothHidGetInputReport() { +int APS5_VABI sceBluetoothHidGetInputReport(std::uint64_t deviceHandle, std::uint8_t reportId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceBluetoothHidSetOutputReport() { +int APS5_VABI sceBluetoothHidSetOutputReport(std::uint64_t deviceHandle, std::uint8_t reportId, const void* data, std::size_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidDebugGetVersion() { + return 0x1e; +} + +int APS5_VABI sceBluetoothHidDisconnectDevice(std::int32_t deviceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidGetDeviceInfo(std::int32_t deviceId, BluetoothHidDeviceInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidGetFeatureReport(std::uint64_t deviceHandle, std::uint8_t reportId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidGetReportDescriptor(std::uint64_t deviceHandle, void* descriptor, std::size_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidInterruptOutput(std::uint64_t deviceHandle, std::uint8_t reportId, const void* data, std::size_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidParamInitialize(BluetoothHidParam* param, const BluetoothHidThreadParam* threadParam) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidSetFeatureReport(std::uint64_t deviceHandle, std::uint8_t reportId, const void* data, std::size_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBluetoothHidThreadParamInitialize(BluetoothHidThreadParam* param, std::int32_t priority, std::uint64_t affinityMask) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceCdlgG2P/CMakeLists.txt b/core/libs/prx/libSceCdlgG2P/CMakeLists.txt index 9809109f69..f15414c733 100644 --- a/core/libs/prx/libSceCdlgG2P/CMakeLists.txt +++ b/core/libs/prx/libSceCdlgG2P/CMakeLists.txt @@ -1,11 +1,12 @@ add_library(libSceCdlgG2P SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceCdlgG2P PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceCdlgG2P) +target_link_libraries(libSceCdlgG2P PRIVATE libSceUserService) if(BUILD_TESTING) add_test_executable(guest_g2p_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestG2PDialog.cpp) target_include_directories(guest_g2p_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_g2p_dialog_tests PRIVATE libSceCdlgG2P) + target_link_libraries(guest_g2p_dialog_tests PRIVATE libSceCdlgG2P libSceUserService) add_test(NAME guest_g2p_dialog COMMAND guest_g2p_dialog_tests) if(WIN32) set_tests_properties(guest_g2p_dialog PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceCdlgG2P/Export.cpp b/core/libs/prx/libSceCdlgG2P/Export.cpp index d772b29003..43abff189e 100644 --- a/core/libs/prx/libSceCdlgG2P/Export.cpp +++ b/core/libs/prx/libSceCdlgG2P/Export.cpp @@ -1,10 +1,14 @@ +#include "SceTypes.hpp" #include #include #include #include -#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" +extern "C" int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* users); + namespace { constexpr int COMMON_DIALOG_STATUS_NONE = 0; @@ -12,9 +16,10 @@ 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_INVALID_ARGUMENT = static_cast(0x80B8000Au); 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); constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; @@ -25,46 +30,51 @@ std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; extern "C" { int APS5_VABI sceG2PDialogInitialize(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 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 sceG2PDialogOpen(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 sceG2PDialogOpen(const G2PDialogParam* param) { + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + 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 (!ValidateCommonDialogBaseParam(param->base) || param->size != sizeof(*param) || param->userId == -1 || !std::all_of(std::begin(param->reserved), std::end(param->reserved), [](std::uint8_t value) { return value == 0; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + UserServiceLoginUserIdList users{}; + if (sceUserServiceGetLoginUserIdList(&users) != 0 || std::none_of(std::begin(users.user_id), std::end(users.user_id), [param](std::int32_t userId) { return userId != -1 && userId == param->userId; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; } int APS5_VABI sceG2PDialogUpdateStatus(void) { - return g_status.load(); + return g_status.load(); } int APS5_VABI sceG2PDialogGetStatus(void) { - return g_status.load(); + return g_status.load(); } -int APS5_VABI sceG2PDialogGetResult(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 sceG2PDialogGetResult(CommonDialogResultPrefix* result) { + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + return 0; } int APS5_VABI sceG2PDialogClose(void) { - if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - return 0; + 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_NOT_RUNNING; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; } int APS5_VABI sceG2PDialogTerminate(void) { - if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - return 0; + 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/libSceCdlgPlayerReview/CMakeLists.txt b/core/libs/prx/libSceCdlgPlayerReview/CMakeLists.txt index 7b4466af1a..f3aa2fabdc 100644 --- a/core/libs/prx/libSceCdlgPlayerReview/CMakeLists.txt +++ b/core/libs/prx/libSceCdlgPlayerReview/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(libSceCdlgPlayerReview SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceCdlgPlayerReview PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceCdlgPlayerReview) +target_link_libraries(libSceCdlgPlayerReview PRIVATE libSceUserService) if(MINGW) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) @@ -11,7 +12,7 @@ endif() if(BUILD_TESTING) add_test_executable(guest_player_review_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestPlayerReviewDialog.cpp) target_include_directories(guest_player_review_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_player_review_dialog_tests PRIVATE libSceCdlgPlayerReview) + target_link_libraries(guest_player_review_dialog_tests PRIVATE libSceCdlgPlayerReview libSceUserService) add_test(NAME guest_player_review_dialog COMMAND guest_player_review_dialog_tests) if(WIN32) set_tests_properties(guest_player_review_dialog PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp index b19768c25a..4db4e1760e 100644 --- a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp +++ b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp @@ -1,9 +1,12 @@ +#include "SceTypes.hpp" #include #include -#include -#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" +extern "C" int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* users); + namespace { std::atomic g_status{0}; @@ -12,8 +15,10 @@ 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_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_INVALID_ARGUMENT = static_cast(0x80B8000Au); 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 +26,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,21 +34,28 @@ 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) { +int APS5_VABI scePlayerReviewDialogOpen(const PlayerReviewDialogParam* param) { + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; 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; + if (!ValidateCommonDialogBaseParam(param->base) || param->size != sizeof(*param) || param->userId == -1 || !std::all_of(std::begin(param->reserved), std::end(param->reserved), [](std::uint8_t value) { return value == 0; }) || param->mode > 1) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + UserServiceLoginUserIdList users{}; + if (sceUserServiceGetLoginUserIdList(&users) != 0 || std::none_of(std::begin(users.user_id), std::end(users.user_id), [param](std::int32_t userId) { return userId != -1 && userId == param->userId; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } int APS5_VABI scePlayerReviewDialogClose(void) { - if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + 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_NOT_RUNNING; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } @@ -55,13 +67,13 @@ int APS5_VABI scePlayerReviewDialogUpdateStatus(void) { return g_status.load(); } -int APS5_VABI scePlayerReviewDialogGetResult(void* result) { +int APS5_VABI scePlayerReviewDialogGetResult(CommonDialogResultPrefix* result) { + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; 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; - *static_cast(result) = COMMON_DIALOG_RESULT_USER_CANCELED; - return 0; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + return COMMON_DIALOG_RESULT_USER_CANCELED; } } diff --git a/core/libs/prx/libSceCdlgUtilServer/Export.cpp b/core/libs/prx/libSceCdlgUtilServer/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceCdlgUtilServer/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceCesCs/CMakeLists.txt b/core/libs/prx/libSceCesCs/CMakeLists.txt index 674db3453e..c9d9076534 100644 --- a/core/libs/prx/libSceCesCs/CMakeLists.txt +++ b/core/libs/prx/libSceCesCs/CMakeLists.txt @@ -10,8 +10,33 @@ if(BUILD_TESTING) target_include_directories(guest_ces_module_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ces_module_tests PRIVATE libSceCesCs-module) add_test(NAME guest_ces_module COMMAND guest_ces_module_tests) + add_test_executable(guest_ces_dbcs_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesDbcs.cpp) + target_include_directories(guest_ces_dbcs_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ces_dbcs_tests PRIVATE libSceCesCs) + add_test(NAME guest_ces_dbcs COMMAND guest_ces_dbcs_tests) + add_test_executable(guest_ces_dbcs_module_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesDbcs.cpp) + target_include_directories(guest_ces_dbcs_module_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ces_dbcs_module_tests PRIVATE libSceCesCs-module) + add_test(NAME guest_ces_dbcs_module COMMAND guest_ces_dbcs_module_tests) if(WIN32) - set_tests_properties(guest_ces guest_ces_module PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_ces guest_ces_module guest_ces_dbcs guest_ces_dbcs_module PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +if(BUILD_TESTING) + foreach(variant IN ITEMS unicode unicode_module) + add_test_executable(guest_ces_${variant}_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesUnicode.cpp) + target_include_directories(guest_ces_${variant}_tests PRIVATE ${LIBS_INCLUDE_DIR}) + if(variant STREQUAL "unicode_module") + target_link_libraries(guest_ces_${variant}_tests PRIVATE libSceCesCs-module) + else() + target_link_libraries(guest_ces_${variant}_tests PRIVATE libSceCesCs) + endif() + add_test(NAME guest_ces_${variant} COMMAND guest_ces_${variant}_tests) + endforeach() + if(WIN32) + set_tests_properties(guest_ces_unicode guest_ces_unicode_module PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceCesCs/CesLibrary.cmake b/core/libs/prx/libSceCesCs/CesLibrary.cmake index dcad1c7bd6..d18971f49b 100644 --- a/core/libs/prx/libSceCesCs/CesLibrary.cmake +++ b/core/libs/prx/libSceCesCs/CesLibrary.cmake @@ -1,5 +1,8 @@ function(add_sce_ces_library target) - add_library(${target} SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Export.cpp) + add_library(${target} SHARED EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Export.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Unicode.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/PendingExports.cpp) target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(${target}) endfunction() diff --git a/core/libs/prx/libSceCesCs/Cp949.hpp b/core/libs/prx/libSceCesCs/Cp949.hpp new file mode 100644 index 0000000000..ec0166ce13 --- /dev/null +++ b/core/libs/prx/libSceCesCs/Cp949.hpp @@ -0,0 +1,136 @@ +#pragma once + +#include + +namespace Cp949 { + +inline constexpr uint16_t TO_UCS2[126 * 190] = { + 0xAC02, 0xAC03, 0xAC05, 0xAC06, 0xAC0B, 0xAC0C, 0xAC0D, 0xAC0E, 0xAC0F, 0xAC18, 0xAC1E, 0xAC1F, 0xAC21, 0xAC22, 0xAC23, 0xAC25, 0xAC26, 0xAC27, 0xAC28, 0xAC29, 0xAC2A, 0xAC2B, 0xAC2E, 0xAC32, 0xAC33, 0xAC34, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xAC35, 0xAC36, 0xAC37, 0xAC3A, 0xAC3B, 0xAC3D, 0xAC3E, 0xAC3F, 0xAC41, 0xAC42, 0xAC43, 0xAC44, 0xAC45, 0xAC46, 0xAC47, 0xAC48, 0xAC49, 0xAC4A, 0xAC4C, 0xAC4E, 0xAC4F, 0xAC50, 0xAC51, 0xAC52, 0xAC53, 0xAC55, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xAC56, 0xAC57, 0xAC59, 0xAC5A, 0xAC5B, 0xAC5D, 0xAC5E, 0xAC5F, 0xAC60, 0xAC61, 0xAC62, 0xAC63, 0xAC64, 0xAC65, 0xAC66, 0xAC67, 0xAC68, 0xAC69, 0xAC6A, 0xAC6B, 0xAC6C, 0xAC6D, 0xAC6E, 0xAC6F, 0xAC72, 0xAC73, 0xAC75, 0xAC76, 0xAC79, 0xAC7B, 0xAC7C, 0xAC7D, 0xAC7E, 0xAC7F, 0xAC82, 0xAC87, 0xAC88, 0xAC8D, 0xAC8E, 0xAC8F, 0xAC91, 0xAC92, 0xAC93, 0xAC95, 0xAC96, 0xAC97, 0xAC98, 0xAC99, 0xAC9A, 0xAC9B, 0xAC9E, 0xACA2, 0xACA3, 0xACA4, 0xACA5, 0xACA6, 0xACA7, 0xACAB, 0xACAD, 0xACAE, 0xACB1, 0xACB2, 0xACB3, 0xACB4, 0xACB5, 0xACB6, 0xACB7, 0xACBA, 0xACBE, 0xACBF, 0xACC0, 0xACC2, 0xACC3, 0xACC5, 0xACC6, 0xACC7, 0xACC9, 0xACCA, 0xACCB, 0xACCD, 0xACCE, 0xACCF, 0xACD0, 0xACD1, 0xACD2, 0xACD3, 0xACD4, 0xACD6, 0xACD8, 0xACD9, 0xACDA, 0xACDB, 0xACDC, 0xACDD, 0xACDE, 0xACDF, 0xACE2, 0xACE3, 0xACE5, 0xACE6, 0xACE9, 0xACEB, 0xACED, 0xACEE, 0xACF2, 0xACF4, 0xACF7, 0xACF8, 0xACF9, 0xACFA, 0xACFB, 0xACFE, 0xACFF, 0xAD01, 0xAD02, 0xAD03, 0xAD05, 0xAD07, 0xAD08, 0xAD09, 0xAD0A, 0xAD0B, 0xAD0E, 0xAD10, 0xAD12, 0xAD13, + 0xAD14, 0xAD15, 0xAD16, 0xAD17, 0xAD19, 0xAD1A, 0xAD1B, 0xAD1D, 0xAD1E, 0xAD1F, 0xAD21, 0xAD22, 0xAD23, 0xAD24, 0xAD25, 0xAD26, 0xAD27, 0xAD28, 0xAD2A, 0xAD2B, 0xAD2E, 0xAD2F, 0xAD30, 0xAD31, 0xAD32, 0xAD33, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xAD36, 0xAD37, 0xAD39, 0xAD3A, 0xAD3B, 0xAD3D, 0xAD3E, 0xAD3F, 0xAD40, 0xAD41, 0xAD42, 0xAD43, 0xAD46, 0xAD48, 0xAD4A, 0xAD4B, 0xAD4C, 0xAD4D, 0xAD4E, 0xAD4F, 0xAD51, 0xAD52, 0xAD53, 0xAD55, 0xAD56, 0xAD57, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xAD59, 0xAD5A, 0xAD5B, 0xAD5C, 0xAD5D, 0xAD5E, 0xAD5F, 0xAD60, 0xAD62, 0xAD64, 0xAD65, 0xAD66, 0xAD67, 0xAD68, 0xAD69, 0xAD6A, 0xAD6B, 0xAD6E, 0xAD6F, 0xAD71, 0xAD72, 0xAD77, 0xAD78, 0xAD79, 0xAD7A, 0xAD7E, 0xAD80, 0xAD83, 0xAD84, 0xAD85, 0xAD86, 0xAD87, 0xAD8A, 0xAD8B, 0xAD8D, 0xAD8E, 0xAD8F, 0xAD91, 0xAD92, 0xAD93, 0xAD94, 0xAD95, 0xAD96, 0xAD97, 0xAD98, 0xAD99, 0xAD9A, 0xAD9B, 0xAD9E, 0xAD9F, 0xADA0, 0xADA1, 0xADA2, 0xADA3, 0xADA5, 0xADA6, 0xADA7, 0xADA8, 0xADA9, 0xADAA, 0xADAB, 0xADAC, 0xADAD, 0xADAE, 0xADAF, 0xADB0, 0xADB1, 0xADB2, 0xADB3, 0xADB4, 0xADB5, 0xADB6, 0xADB8, 0xADB9, 0xADBA, 0xADBB, 0xADBC, 0xADBD, 0xADBE, 0xADBF, 0xADC2, 0xADC3, 0xADC5, 0xADC6, 0xADC7, 0xADC9, 0xADCA, 0xADCB, 0xADCC, 0xADCD, 0xADCE, 0xADCF, 0xADD2, 0xADD4, 0xADD5, 0xADD6, 0xADD7, 0xADD8, 0xADD9, 0xADDA, 0xADDB, 0xADDD, 0xADDE, 0xADDF, 0xADE1, 0xADE2, 0xADE3, 0xADE5, 0xADE6, 0xADE7, 0xADE8, 0xADE9, 0xADEA, 0xADEB, 0xADEC, 0xADED, 0xADEE, 0xADEF, 0xADF0, 0xADF1, 0xADF2, 0xADF3, 0xADF4, 0xADF5, 0xADF6, 0xADF7, + 0xADFA, 0xADFB, 0xADFD, 0xADFE, 0xAE02, 0xAE03, 0xAE04, 0xAE05, 0xAE06, 0xAE07, 0xAE0A, 0xAE0C, 0xAE0E, 0xAE0F, 0xAE10, 0xAE11, 0xAE12, 0xAE13, 0xAE15, 0xAE16, 0xAE17, 0xAE18, 0xAE19, 0xAE1A, 0xAE1B, 0xAE1C, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xAE1D, 0xAE1E, 0xAE1F, 0xAE20, 0xAE21, 0xAE22, 0xAE23, 0xAE24, 0xAE25, 0xAE26, 0xAE27, 0xAE28, 0xAE29, 0xAE2A, 0xAE2B, 0xAE2C, 0xAE2D, 0xAE2E, 0xAE2F, 0xAE32, 0xAE33, 0xAE35, 0xAE36, 0xAE39, 0xAE3B, 0xAE3C, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xAE3D, 0xAE3E, 0xAE3F, 0xAE42, 0xAE44, 0xAE47, 0xAE48, 0xAE49, 0xAE4B, 0xAE4F, 0xAE51, 0xAE52, 0xAE53, 0xAE55, 0xAE57, 0xAE58, 0xAE59, 0xAE5A, 0xAE5B, 0xAE5E, 0xAE62, 0xAE63, 0xAE64, 0xAE66, 0xAE67, 0xAE6A, 0xAE6B, 0xAE6D, 0xAE6E, 0xAE6F, 0xAE71, 0xAE72, 0xAE73, 0xAE74, 0xAE75, 0xAE76, 0xAE77, 0xAE7A, 0xAE7E, 0xAE7F, 0xAE80, 0xAE81, 0xAE82, 0xAE83, 0xAE86, 0xAE87, 0xAE88, 0xAE89, 0xAE8A, 0xAE8B, 0xAE8D, 0xAE8E, 0xAE8F, 0xAE90, 0xAE91, 0xAE92, 0xAE93, 0xAE94, 0xAE95, 0xAE96, 0xAE97, 0xAE98, 0xAE99, 0xAE9A, 0xAE9B, 0xAE9C, 0xAE9D, 0xAE9E, 0xAE9F, 0xAEA0, 0xAEA1, 0xAEA2, 0xAEA3, 0xAEA4, 0xAEA5, 0xAEA6, 0xAEA7, 0xAEA8, 0xAEA9, 0xAEAA, 0xAEAB, 0xAEAC, 0xAEAD, 0xAEAE, 0xAEAF, 0xAEB0, 0xAEB1, 0xAEB2, 0xAEB3, 0xAEB4, 0xAEB5, 0xAEB6, 0xAEB7, 0xAEB8, 0xAEB9, 0xAEBA, 0xAEBB, 0xAEBF, 0xAEC1, 0xAEC2, 0xAEC3, 0xAEC5, 0xAEC6, 0xAEC7, 0xAEC8, 0xAEC9, 0xAECA, 0xAECB, 0xAECE, 0xAED2, 0xAED3, 0xAED4, 0xAED5, 0xAED6, 0xAED7, 0xAEDA, 0xAEDB, 0xAEDD, 0xAEDE, 0xAEDF, 0xAEE0, 0xAEE1, 0xAEE2, 0xAEE3, 0xAEE4, 0xAEE5, + 0xAEE6, 0xAEE7, 0xAEE9, 0xAEEA, 0xAEEC, 0xAEEE, 0xAEEF, 0xAEF0, 0xAEF1, 0xAEF2, 0xAEF3, 0xAEF5, 0xAEF6, 0xAEF7, 0xAEF9, 0xAEFA, 0xAEFB, 0xAEFD, 0xAEFE, 0xAEFF, 0xAF00, 0xAF01, 0xAF02, 0xAF03, 0xAF04, 0xAF05, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xAF06, 0xAF09, 0xAF0A, 0xAF0B, 0xAF0C, 0xAF0E, 0xAF0F, 0xAF11, 0xAF12, 0xAF13, 0xAF14, 0xAF15, 0xAF16, 0xAF17, 0xAF18, 0xAF19, 0xAF1A, 0xAF1B, 0xAF1C, 0xAF1D, 0xAF1E, 0xAF1F, 0xAF20, 0xAF21, 0xAF22, 0xAF23, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xAF24, 0xAF25, 0xAF26, 0xAF27, 0xAF28, 0xAF29, 0xAF2A, 0xAF2B, 0xAF2E, 0xAF2F, 0xAF31, 0xAF33, 0xAF35, 0xAF36, 0xAF37, 0xAF38, 0xAF39, 0xAF3A, 0xAF3B, 0xAF3E, 0xAF40, 0xAF44, 0xAF45, 0xAF46, 0xAF47, 0xAF4A, 0xAF4B, 0xAF4C, 0xAF4D, 0xAF4E, 0xAF4F, 0xAF51, 0xAF52, 0xAF53, 0xAF54, 0xAF55, 0xAF56, 0xAF57, 0xAF58, 0xAF59, 0xAF5A, 0xAF5B, 0xAF5E, 0xAF5F, 0xAF60, 0xAF61, 0xAF62, 0xAF63, 0xAF66, 0xAF67, 0xAF68, 0xAF69, 0xAF6A, 0xAF6B, 0xAF6C, 0xAF6D, 0xAF6E, 0xAF6F, 0xAF70, 0xAF71, 0xAF72, 0xAF73, 0xAF74, 0xAF75, 0xAF76, 0xAF77, 0xAF78, 0xAF7A, 0xAF7B, 0xAF7C, 0xAF7D, 0xAF7E, 0xAF7F, 0xAF81, 0xAF82, 0xAF83, 0xAF85, 0xAF86, 0xAF87, 0xAF89, 0xAF8A, 0xAF8B, 0xAF8C, 0xAF8D, 0xAF8E, 0xAF8F, 0xAF92, 0xAF93, 0xAF94, 0xAF96, 0xAF97, 0xAF98, 0xAF99, 0xAF9A, 0xAF9B, 0xAF9D, 0xAF9E, 0xAF9F, 0xAFA0, 0xAFA1, 0xAFA2, 0xAFA3, 0xAFA4, 0xAFA5, 0xAFA6, 0xAFA7, 0xAFA8, 0xAFA9, 0xAFAA, 0xAFAB, 0xAFAC, 0xAFAD, 0xAFAE, 0xAFAF, 0xAFB0, 0xAFB1, 0xAFB2, 0xAFB3, 0xAFB4, 0xAFB5, 0xAFB6, 0xAFB7, 0xAFBA, 0xAFBB, 0xAFBD, 0xAFBE, + 0xAFBF, 0xAFC1, 0xAFC2, 0xAFC3, 0xAFC4, 0xAFC5, 0xAFC6, 0xAFCA, 0xAFCC, 0xAFCF, 0xAFD0, 0xAFD1, 0xAFD2, 0xAFD3, 0xAFD5, 0xAFD6, 0xAFD7, 0xAFD8, 0xAFD9, 0xAFDA, 0xAFDB, 0xAFDD, 0xAFDE, 0xAFDF, 0xAFE0, 0xAFE1, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xAFE2, 0xAFE3, 0xAFE4, 0xAFE5, 0xAFE6, 0xAFE7, 0xAFEA, 0xAFEB, 0xAFEC, 0xAFED, 0xAFEE, 0xAFEF, 0xAFF2, 0xAFF3, 0xAFF5, 0xAFF6, 0xAFF7, 0xAFF9, 0xAFFA, 0xAFFB, 0xAFFC, 0xAFFD, 0xAFFE, 0xAFFF, 0xB002, 0xB003, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB005, 0xB006, 0xB007, 0xB008, 0xB009, 0xB00A, 0xB00B, 0xB00D, 0xB00E, 0xB00F, 0xB011, 0xB012, 0xB013, 0xB015, 0xB016, 0xB017, 0xB018, 0xB019, 0xB01A, 0xB01B, 0xB01E, 0xB01F, 0xB020, 0xB021, 0xB022, 0xB023, 0xB024, 0xB025, 0xB026, 0xB027, 0xB029, 0xB02A, 0xB02B, 0xB02C, 0xB02D, 0xB02E, 0xB02F, 0xB030, 0xB031, 0xB032, 0xB033, 0xB034, 0xB035, 0xB036, 0xB037, 0xB038, 0xB039, 0xB03A, 0xB03B, 0xB03C, 0xB03D, 0xB03E, 0xB03F, 0xB040, 0xB041, 0xB042, 0xB043, 0xB046, 0xB047, 0xB049, 0xB04B, 0xB04D, 0xB04F, 0xB050, 0xB051, 0xB052, 0xB056, 0xB058, 0xB05A, 0xB05B, 0xB05C, 0xB05E, 0xB05F, 0xB060, 0xB061, 0xB062, 0xB063, 0xB064, 0xB065, 0xB066, 0xB067, 0xB068, 0xB069, 0xB06A, 0xB06B, 0xB06C, 0xB06D, 0xB06E, 0xB06F, 0xB070, 0xB071, 0xB072, 0xB073, 0xB074, 0xB075, 0xB076, 0xB077, 0xB078, 0xB079, 0xB07A, 0xB07B, 0xB07E, 0xB07F, 0xB081, 0xB082, 0xB083, 0xB085, 0xB086, 0xB087, 0xB088, 0xB089, 0xB08A, 0xB08B, 0xB08E, 0xB090, 0xB092, 0xB093, 0xB094, 0xB095, 0xB096, 0xB097, 0xB09B, 0xB09D, 0xB09E, 0xB0A3, 0xB0A4, + 0xB0A5, 0xB0A6, 0xB0A7, 0xB0AA, 0xB0B0, 0xB0B2, 0xB0B6, 0xB0B7, 0xB0B9, 0xB0BA, 0xB0BB, 0xB0BD, 0xB0BE, 0xB0BF, 0xB0C0, 0xB0C1, 0xB0C2, 0xB0C3, 0xB0C6, 0xB0CA, 0xB0CB, 0xB0CC, 0xB0CD, 0xB0CE, 0xB0CF, 0xB0D2, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB0D3, 0xB0D5, 0xB0D6, 0xB0D7, 0xB0D9, 0xB0DA, 0xB0DB, 0xB0DC, 0xB0DD, 0xB0DE, 0xB0DF, 0xB0E1, 0xB0E2, 0xB0E3, 0xB0E4, 0xB0E6, 0xB0E7, 0xB0E8, 0xB0E9, 0xB0EA, 0xB0EB, 0xB0EC, 0xB0ED, 0xB0EE, 0xB0EF, 0xB0F0, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB0F1, 0xB0F2, 0xB0F3, 0xB0F4, 0xB0F5, 0xB0F6, 0xB0F7, 0xB0F8, 0xB0F9, 0xB0FA, 0xB0FB, 0xB0FC, 0xB0FD, 0xB0FE, 0xB0FF, 0xB100, 0xB101, 0xB102, 0xB103, 0xB104, 0xB105, 0xB106, 0xB107, 0xB10A, 0xB10D, 0xB10E, 0xB10F, 0xB111, 0xB114, 0xB115, 0xB116, 0xB117, 0xB11A, 0xB11E, 0xB11F, 0xB120, 0xB121, 0xB122, 0xB126, 0xB127, 0xB129, 0xB12A, 0xB12B, 0xB12D, 0xB12E, 0xB12F, 0xB130, 0xB131, 0xB132, 0xB133, 0xB136, 0xB13A, 0xB13B, 0xB13C, 0xB13D, 0xB13E, 0xB13F, 0xB142, 0xB143, 0xB145, 0xB146, 0xB147, 0xB149, 0xB14A, 0xB14B, 0xB14C, 0xB14D, 0xB14E, 0xB14F, 0xB152, 0xB153, 0xB156, 0xB157, 0xB159, 0xB15A, 0xB15B, 0xB15D, 0xB15E, 0xB15F, 0xB161, 0xB162, 0xB163, 0xB164, 0xB165, 0xB166, 0xB167, 0xB168, 0xB169, 0xB16A, 0xB16B, 0xB16C, 0xB16D, 0xB16E, 0xB16F, 0xB170, 0xB171, 0xB172, 0xB173, 0xB174, 0xB175, 0xB176, 0xB177, 0xB17A, 0xB17B, 0xB17D, 0xB17E, 0xB17F, 0xB181, 0xB183, 0xB184, 0xB185, 0xB186, 0xB187, 0xB18A, 0xB18C, 0xB18E, 0xB18F, 0xB190, 0xB191, 0xB195, 0xB196, 0xB197, 0xB199, 0xB19A, 0xB19B, 0xB19D, + 0xB19E, 0xB19F, 0xB1A0, 0xB1A1, 0xB1A2, 0xB1A3, 0xB1A4, 0xB1A5, 0xB1A6, 0xB1A7, 0xB1A9, 0xB1AA, 0xB1AB, 0xB1AC, 0xB1AD, 0xB1AE, 0xB1AF, 0xB1B0, 0xB1B1, 0xB1B2, 0xB1B3, 0xB1B4, 0xB1B5, 0xB1B6, 0xB1B7, 0xB1B8, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB1B9, 0xB1BA, 0xB1BB, 0xB1BC, 0xB1BD, 0xB1BE, 0xB1BF, 0xB1C0, 0xB1C1, 0xB1C2, 0xB1C3, 0xB1C4, 0xB1C5, 0xB1C6, 0xB1C7, 0xB1C8, 0xB1C9, 0xB1CA, 0xB1CB, 0xB1CD, 0xB1CE, 0xB1CF, 0xB1D1, 0xB1D2, 0xB1D3, 0xB1D5, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB1D6, 0xB1D7, 0xB1D8, 0xB1D9, 0xB1DA, 0xB1DB, 0xB1DE, 0xB1E0, 0xB1E1, 0xB1E2, 0xB1E3, 0xB1E4, 0xB1E5, 0xB1E6, 0xB1E7, 0xB1EA, 0xB1EB, 0xB1ED, 0xB1EE, 0xB1EF, 0xB1F1, 0xB1F2, 0xB1F3, 0xB1F4, 0xB1F5, 0xB1F6, 0xB1F7, 0xB1F8, 0xB1FA, 0xB1FC, 0xB1FE, 0xB1FF, 0xB200, 0xB201, 0xB202, 0xB203, 0xB206, 0xB207, 0xB209, 0xB20A, 0xB20D, 0xB20E, 0xB20F, 0xB210, 0xB211, 0xB212, 0xB213, 0xB216, 0xB218, 0xB21A, 0xB21B, 0xB21C, 0xB21D, 0xB21E, 0xB21F, 0xB221, 0xB222, 0xB223, 0xB224, 0xB225, 0xB226, 0xB227, 0xB228, 0xB229, 0xB22A, 0xB22B, 0xB22C, 0xB22D, 0xB22E, 0xB22F, 0xB230, 0xB231, 0xB232, 0xB233, 0xB235, 0xB236, 0xB237, 0xB238, 0xB239, 0xB23A, 0xB23B, 0xB23D, 0xB23E, 0xB23F, 0xB240, 0xB241, 0xB242, 0xB243, 0xB244, 0xB245, 0xB246, 0xB247, 0xB248, 0xB249, 0xB24A, 0xB24B, 0xB24C, 0xB24D, 0xB24E, 0xB24F, 0xB250, 0xB251, 0xB252, 0xB253, 0xB254, 0xB255, 0xB256, 0xB257, 0xB259, 0xB25A, 0xB25B, 0xB25D, 0xB25E, 0xB25F, 0xB261, 0xB262, 0xB263, 0xB264, 0xB265, 0xB266, 0xB267, 0xB26A, 0xB26B, 0xB26C, 0xB26D, 0xB26E, + 0xB26F, 0xB270, 0xB271, 0xB272, 0xB273, 0xB276, 0xB277, 0xB278, 0xB279, 0xB27A, 0xB27B, 0xB27D, 0xB27E, 0xB27F, 0xB280, 0xB281, 0xB282, 0xB283, 0xB286, 0xB287, 0xB288, 0xB28A, 0xB28B, 0xB28C, 0xB28D, 0xB28E, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB28F, 0xB292, 0xB293, 0xB295, 0xB296, 0xB297, 0xB29B, 0xB29C, 0xB29D, 0xB29E, 0xB29F, 0xB2A2, 0xB2A4, 0xB2A7, 0xB2A8, 0xB2A9, 0xB2AB, 0xB2AD, 0xB2AE, 0xB2AF, 0xB2B1, 0xB2B2, 0xB2B3, 0xB2B5, 0xB2B6, 0xB2B7, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB2B8, 0xB2B9, 0xB2BA, 0xB2BB, 0xB2BC, 0xB2BD, 0xB2BE, 0xB2BF, 0xB2C0, 0xB2C1, 0xB2C2, 0xB2C3, 0xB2C4, 0xB2C5, 0xB2C6, 0xB2C7, 0xB2CA, 0xB2CB, 0xB2CD, 0xB2CE, 0xB2CF, 0xB2D1, 0xB2D3, 0xB2D4, 0xB2D5, 0xB2D6, 0xB2D7, 0xB2DA, 0xB2DC, 0xB2DE, 0xB2DF, 0xB2E0, 0xB2E1, 0xB2E3, 0xB2E7, 0xB2E9, 0xB2EA, 0xB2F0, 0xB2F1, 0xB2F2, 0xB2F6, 0xB2FC, 0xB2FD, 0xB2FE, 0xB302, 0xB303, 0xB305, 0xB306, 0xB307, 0xB309, 0xB30A, 0xB30B, 0xB30C, 0xB30D, 0xB30E, 0xB30F, 0xB312, 0xB316, 0xB317, 0xB318, 0xB319, 0xB31A, 0xB31B, 0xB31D, 0xB31E, 0xB31F, 0xB320, 0xB321, 0xB322, 0xB323, 0xB324, 0xB325, 0xB326, 0xB327, 0xB328, 0xB329, 0xB32A, 0xB32B, 0xB32C, 0xB32D, 0xB32E, 0xB32F, 0xB330, 0xB331, 0xB332, 0xB333, 0xB334, 0xB335, 0xB336, 0xB337, 0xB338, 0xB339, 0xB33A, 0xB33B, 0xB33C, 0xB33D, 0xB33E, 0xB33F, 0xB340, 0xB341, 0xB342, 0xB343, 0xB344, 0xB345, 0xB346, 0xB347, 0xB348, 0xB349, 0xB34A, 0xB34B, 0xB34C, 0xB34D, 0xB34E, 0xB34F, 0xB350, 0xB351, 0xB352, 0xB353, 0xB357, 0xB359, 0xB35A, 0xB35D, 0xB360, 0xB361, 0xB362, 0xB363, + 0xB366, 0xB368, 0xB36A, 0xB36C, 0xB36D, 0xB36F, 0xB372, 0xB373, 0xB375, 0xB376, 0xB377, 0xB379, 0xB37A, 0xB37B, 0xB37C, 0xB37D, 0xB37E, 0xB37F, 0xB382, 0xB386, 0xB387, 0xB388, 0xB389, 0xB38A, 0xB38B, 0xB38D, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB38E, 0xB38F, 0xB391, 0xB392, 0xB393, 0xB395, 0xB396, 0xB397, 0xB398, 0xB399, 0xB39A, 0xB39B, 0xB39C, 0xB39D, 0xB39E, 0xB39F, 0xB3A2, 0xB3A3, 0xB3A4, 0xB3A5, 0xB3A6, 0xB3A7, 0xB3A9, 0xB3AA, 0xB3AB, 0xB3AD, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB3AE, 0xB3AF, 0xB3B0, 0xB3B1, 0xB3B2, 0xB3B3, 0xB3B4, 0xB3B5, 0xB3B6, 0xB3B7, 0xB3B8, 0xB3B9, 0xB3BA, 0xB3BB, 0xB3BC, 0xB3BD, 0xB3BE, 0xB3BF, 0xB3C0, 0xB3C1, 0xB3C2, 0xB3C3, 0xB3C6, 0xB3C7, 0xB3C9, 0xB3CA, 0xB3CD, 0xB3CF, 0xB3D1, 0xB3D2, 0xB3D3, 0xB3D6, 0xB3D8, 0xB3DA, 0xB3DC, 0xB3DE, 0xB3DF, 0xB3E1, 0xB3E2, 0xB3E3, 0xB3E5, 0xB3E6, 0xB3E7, 0xB3E9, 0xB3EA, 0xB3EB, 0xB3EC, 0xB3ED, 0xB3EE, 0xB3EF, 0xB3F0, 0xB3F1, 0xB3F2, 0xB3F3, 0xB3F4, 0xB3F5, 0xB3F6, 0xB3F7, 0xB3F8, 0xB3F9, 0xB3FA, 0xB3FB, 0xB3FD, 0xB3FE, 0xB3FF, 0xB400, 0xB401, 0xB402, 0xB403, 0xB404, 0xB405, 0xB406, 0xB407, 0xB408, 0xB409, 0xB40A, 0xB40B, 0xB40C, 0xB40D, 0xB40E, 0xB40F, 0xB411, 0xB412, 0xB413, 0xB414, 0xB415, 0xB416, 0xB417, 0xB419, 0xB41A, 0xB41B, 0xB41D, 0xB41E, 0xB41F, 0xB421, 0xB422, 0xB423, 0xB424, 0xB425, 0xB426, 0xB427, 0xB42A, 0xB42C, 0xB42D, 0xB42E, 0xB42F, 0xB430, 0xB431, 0xB432, 0xB433, 0xB435, 0xB436, 0xB437, 0xB438, 0xB439, 0xB43A, 0xB43B, 0xB43C, 0xB43D, 0xB43E, 0xB43F, 0xB440, 0xB441, 0xB442, 0xB443, 0xB444, + 0xB445, 0xB446, 0xB447, 0xB448, 0xB449, 0xB44A, 0xB44B, 0xB44C, 0xB44D, 0xB44E, 0xB44F, 0xB452, 0xB453, 0xB455, 0xB456, 0xB457, 0xB459, 0xB45A, 0xB45B, 0xB45C, 0xB45D, 0xB45E, 0xB45F, 0xB462, 0xB464, 0xB466, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB467, 0xB468, 0xB469, 0xB46A, 0xB46B, 0xB46D, 0xB46E, 0xB46F, 0xB470, 0xB471, 0xB472, 0xB473, 0xB474, 0xB475, 0xB476, 0xB477, 0xB478, 0xB479, 0xB47A, 0xB47B, 0xB47C, 0xB47D, 0xB47E, 0xB47F, 0xB481, 0xB482, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB483, 0xB484, 0xB485, 0xB486, 0xB487, 0xB489, 0xB48A, 0xB48B, 0xB48C, 0xB48D, 0xB48E, 0xB48F, 0xB490, 0xB491, 0xB492, 0xB493, 0xB494, 0xB495, 0xB496, 0xB497, 0xB498, 0xB499, 0xB49A, 0xB49B, 0xB49C, 0xB49E, 0xB49F, 0xB4A0, 0xB4A1, 0xB4A2, 0xB4A3, 0xB4A5, 0xB4A6, 0xB4A7, 0xB4A9, 0xB4AA, 0xB4AB, 0xB4AD, 0xB4AE, 0xB4AF, 0xB4B0, 0xB4B1, 0xB4B2, 0xB4B3, 0xB4B4, 0xB4B6, 0xB4B8, 0xB4BA, 0xB4BB, 0xB4BC, 0xB4BD, 0xB4BE, 0xB4BF, 0xB4C1, 0xB4C2, 0xB4C3, 0xB4C5, 0xB4C6, 0xB4C7, 0xB4C9, 0xB4CA, 0xB4CB, 0xB4CC, 0xB4CD, 0xB4CE, 0xB4CF, 0xB4D1, 0xB4D2, 0xB4D3, 0xB4D4, 0xB4D6, 0xB4D7, 0xB4D8, 0xB4D9, 0xB4DA, 0xB4DB, 0xB4DE, 0xB4DF, 0xB4E1, 0xB4E2, 0xB4E5, 0xB4E7, 0xB4E8, 0xB4E9, 0xB4EA, 0xB4EB, 0xB4EE, 0xB4F0, 0xB4F2, 0xB4F3, 0xB4F4, 0xB4F5, 0xB4F6, 0xB4F7, 0xB4F9, 0xB4FA, 0xB4FB, 0xB4FC, 0xB4FD, 0xB4FE, 0xB4FF, 0xB500, 0xB501, 0xB502, 0xB503, 0xB504, 0xB505, 0xB506, 0xB507, 0xB508, 0xB509, 0xB50A, 0xB50B, 0xB50C, 0xB50D, 0xB50E, 0xB50F, 0xB510, 0xB511, 0xB512, 0xB513, 0xB516, 0xB517, 0xB519, 0xB51A, 0xB51D, + 0xB51E, 0xB51F, 0xB520, 0xB521, 0xB522, 0xB523, 0xB526, 0xB52B, 0xB52C, 0xB52D, 0xB52E, 0xB52F, 0xB532, 0xB533, 0xB535, 0xB536, 0xB537, 0xB539, 0xB53A, 0xB53B, 0xB53C, 0xB53D, 0xB53E, 0xB53F, 0xB542, 0xB546, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB547, 0xB548, 0xB549, 0xB54A, 0xB54E, 0xB54F, 0xB551, 0xB552, 0xB553, 0xB555, 0xB556, 0xB557, 0xB558, 0xB559, 0xB55A, 0xB55B, 0xB55E, 0xB562, 0xB563, 0xB564, 0xB565, 0xB566, 0xB567, 0xB568, 0xB569, 0xB56A, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB56B, 0xB56C, 0xB56D, 0xB56E, 0xB56F, 0xB570, 0xB571, 0xB572, 0xB573, 0xB574, 0xB575, 0xB576, 0xB577, 0xB578, 0xB579, 0xB57A, 0xB57B, 0xB57C, 0xB57D, 0xB57E, 0xB57F, 0xB580, 0xB581, 0xB582, 0xB583, 0xB584, 0xB585, 0xB586, 0xB587, 0xB588, 0xB589, 0xB58A, 0xB58B, 0xB58C, 0xB58D, 0xB58E, 0xB58F, 0xB590, 0xB591, 0xB592, 0xB593, 0xB594, 0xB595, 0xB596, 0xB597, 0xB598, 0xB599, 0xB59A, 0xB59B, 0xB59C, 0xB59D, 0xB59E, 0xB59F, 0xB5A2, 0xB5A3, 0xB5A5, 0xB5A6, 0xB5A7, 0xB5A9, 0xB5AC, 0xB5AD, 0xB5AE, 0xB5AF, 0xB5B2, 0xB5B6, 0xB5B7, 0xB5B8, 0xB5B9, 0xB5BA, 0xB5BE, 0xB5BF, 0xB5C1, 0xB5C2, 0xB5C3, 0xB5C5, 0xB5C6, 0xB5C7, 0xB5C8, 0xB5C9, 0xB5CA, 0xB5CB, 0xB5CE, 0xB5D2, 0xB5D3, 0xB5D4, 0xB5D5, 0xB5D6, 0xB5D7, 0xB5D9, 0xB5DA, 0xB5DB, 0xB5DC, 0xB5DD, 0xB5DE, 0xB5DF, 0xB5E0, 0xB5E1, 0xB5E2, 0xB5E3, 0xB5E4, 0xB5E5, 0xB5E6, 0xB5E7, 0xB5E8, 0xB5E9, 0xB5EA, 0xB5EB, 0xB5ED, 0xB5EE, 0xB5EF, 0xB5F0, 0xB5F1, 0xB5F2, 0xB5F3, 0xB5F4, 0xB5F5, 0xB5F6, 0xB5F7, 0xB5F8, 0xB5F9, 0xB5FA, 0xB5FB, 0xB5FC, 0xB5FD, 0xB5FE, 0xB5FF, + 0xB600, 0xB601, 0xB602, 0xB603, 0xB604, 0xB605, 0xB606, 0xB607, 0xB608, 0xB609, 0xB60A, 0xB60B, 0xB60C, 0xB60D, 0xB60E, 0xB60F, 0xB612, 0xB613, 0xB615, 0xB616, 0xB617, 0xB619, 0xB61A, 0xB61B, 0xB61C, 0xB61D, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB61E, 0xB61F, 0xB620, 0xB621, 0xB622, 0xB623, 0xB624, 0xB626, 0xB627, 0xB628, 0xB629, 0xB62A, 0xB62B, 0xB62D, 0xB62E, 0xB62F, 0xB630, 0xB631, 0xB632, 0xB633, 0xB635, 0xB636, 0xB637, 0xB638, 0xB639, 0xB63A, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB63B, 0xB63C, 0xB63D, 0xB63E, 0xB63F, 0xB640, 0xB641, 0xB642, 0xB643, 0xB644, 0xB645, 0xB646, 0xB647, 0xB649, 0xB64A, 0xB64B, 0xB64C, 0xB64D, 0xB64E, 0xB64F, 0xB650, 0xB651, 0xB652, 0xB653, 0xB654, 0xB655, 0xB656, 0xB657, 0xB658, 0xB659, 0xB65A, 0xB65B, 0xB65C, 0xB65D, 0xB65E, 0xB65F, 0xB660, 0xB661, 0xB662, 0xB663, 0xB665, 0xB666, 0xB667, 0xB669, 0xB66A, 0xB66B, 0xB66C, 0xB66D, 0xB66E, 0xB66F, 0xB670, 0xB671, 0xB672, 0xB673, 0xB674, 0xB675, 0xB676, 0xB677, 0xB678, 0xB679, 0xB67A, 0xB67B, 0xB67C, 0xB67D, 0xB67E, 0xB67F, 0xB680, 0xB681, 0xB682, 0xB683, 0xB684, 0xB685, 0xB686, 0xB687, 0xB688, 0xB689, 0xB68A, 0xB68B, 0xB68C, 0xB68D, 0xB68E, 0xB68F, 0xB690, 0xB691, 0xB692, 0xB693, 0xB694, 0xB695, 0xB696, 0xB697, 0xB698, 0xB699, 0xB69A, 0xB69B, 0xB69E, 0xB69F, 0xB6A1, 0xB6A2, 0xB6A3, 0xB6A5, 0xB6A6, 0xB6A7, 0xB6A8, 0xB6A9, 0xB6AA, 0xB6AD, 0xB6AE, 0xB6AF, 0xB6B0, 0xB6B2, 0xB6B3, 0xB6B4, 0xB6B5, 0xB6B6, 0xB6B7, 0xB6B8, 0xB6B9, 0xB6BA, 0xB6BB, 0xB6BC, 0xB6BD, 0xB6BE, 0xB6BF, 0xB6C0, 0xB6C1, 0xB6C2, + 0xB6C3, 0xB6C4, 0xB6C5, 0xB6C6, 0xB6C7, 0xB6C8, 0xB6C9, 0xB6CA, 0xB6CB, 0xB6CC, 0xB6CD, 0xB6CE, 0xB6CF, 0xB6D0, 0xB6D1, 0xB6D2, 0xB6D3, 0xB6D5, 0xB6D6, 0xB6D7, 0xB6D8, 0xB6D9, 0xB6DA, 0xB6DB, 0xB6DC, 0xB6DD, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB6DE, 0xB6DF, 0xB6E0, 0xB6E1, 0xB6E2, 0xB6E3, 0xB6E4, 0xB6E5, 0xB6E6, 0xB6E7, 0xB6E8, 0xB6E9, 0xB6EA, 0xB6EB, 0xB6EC, 0xB6ED, 0xB6EE, 0xB6EF, 0xB6F1, 0xB6F2, 0xB6F3, 0xB6F5, 0xB6F6, 0xB6F7, 0xB6F9, 0xB6FA, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB6FB, 0xB6FC, 0xB6FD, 0xB6FE, 0xB6FF, 0xB702, 0xB703, 0xB704, 0xB706, 0xB707, 0xB708, 0xB709, 0xB70A, 0xB70B, 0xB70C, 0xB70D, 0xB70E, 0xB70F, 0xB710, 0xB711, 0xB712, 0xB713, 0xB714, 0xB715, 0xB716, 0xB717, 0xB718, 0xB719, 0xB71A, 0xB71B, 0xB71C, 0xB71D, 0xB71E, 0xB71F, 0xB720, 0xB721, 0xB722, 0xB723, 0xB724, 0xB725, 0xB726, 0xB727, 0xB72A, 0xB72B, 0xB72D, 0xB72E, 0xB731, 0xB732, 0xB733, 0xB734, 0xB735, 0xB736, 0xB737, 0xB73A, 0xB73C, 0xB73D, 0xB73E, 0xB73F, 0xB740, 0xB741, 0xB742, 0xB743, 0xB745, 0xB746, 0xB747, 0xB749, 0xB74A, 0xB74B, 0xB74D, 0xB74E, 0xB74F, 0xB750, 0xB751, 0xB752, 0xB753, 0xB756, 0xB757, 0xB758, 0xB759, 0xB75A, 0xB75B, 0xB75C, 0xB75D, 0xB75E, 0xB75F, 0xB761, 0xB762, 0xB763, 0xB765, 0xB766, 0xB767, 0xB769, 0xB76A, 0xB76B, 0xB76C, 0xB76D, 0xB76E, 0xB76F, 0xB772, 0xB774, 0xB776, 0xB777, 0xB778, 0xB779, 0xB77A, 0xB77B, 0xB77E, 0xB77F, 0xB781, 0xB782, 0xB783, 0xB785, 0xB786, 0xB787, 0xB788, 0xB789, 0xB78A, 0xB78B, 0xB78E, 0xB793, 0xB794, 0xB795, 0xB79A, 0xB79B, 0xB79D, 0xB79E, + 0xB79F, 0xB7A1, 0xB7A2, 0xB7A3, 0xB7A4, 0xB7A5, 0xB7A6, 0xB7A7, 0xB7AA, 0xB7AE, 0xB7AF, 0xB7B0, 0xB7B1, 0xB7B2, 0xB7B3, 0xB7B6, 0xB7B7, 0xB7B9, 0xB7BA, 0xB7BB, 0xB7BC, 0xB7BD, 0xB7BE, 0xB7BF, 0xB7C0, 0xB7C1, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB7C2, 0xB7C3, 0xB7C4, 0xB7C5, 0xB7C6, 0xB7C8, 0xB7CA, 0xB7CB, 0xB7CC, 0xB7CD, 0xB7CE, 0xB7CF, 0xB7D0, 0xB7D1, 0xB7D2, 0xB7D3, 0xB7D4, 0xB7D5, 0xB7D6, 0xB7D7, 0xB7D8, 0xB7D9, 0xB7DA, 0xB7DB, 0xB7DC, 0xB7DD, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB7DE, 0xB7DF, 0xB7E0, 0xB7E1, 0xB7E2, 0xB7E3, 0xB7E4, 0xB7E5, 0xB7E6, 0xB7E7, 0xB7E8, 0xB7E9, 0xB7EA, 0xB7EB, 0xB7EE, 0xB7EF, 0xB7F1, 0xB7F2, 0xB7F3, 0xB7F5, 0xB7F6, 0xB7F7, 0xB7F8, 0xB7F9, 0xB7FA, 0xB7FB, 0xB7FE, 0xB802, 0xB803, 0xB804, 0xB805, 0xB806, 0xB80A, 0xB80B, 0xB80D, 0xB80E, 0xB80F, 0xB811, 0xB812, 0xB813, 0xB814, 0xB815, 0xB816, 0xB817, 0xB81A, 0xB81C, 0xB81E, 0xB81F, 0xB820, 0xB821, 0xB822, 0xB823, 0xB826, 0xB827, 0xB829, 0xB82A, 0xB82B, 0xB82D, 0xB82E, 0xB82F, 0xB830, 0xB831, 0xB832, 0xB833, 0xB836, 0xB83A, 0xB83B, 0xB83C, 0xB83D, 0xB83E, 0xB83F, 0xB841, 0xB842, 0xB843, 0xB845, 0xB846, 0xB847, 0xB848, 0xB849, 0xB84A, 0xB84B, 0xB84C, 0xB84D, 0xB84E, 0xB84F, 0xB850, 0xB852, 0xB854, 0xB855, 0xB856, 0xB857, 0xB858, 0xB859, 0xB85A, 0xB85B, 0xB85E, 0xB85F, 0xB861, 0xB862, 0xB863, 0xB865, 0xB866, 0xB867, 0xB868, 0xB869, 0xB86A, 0xB86B, 0xB86E, 0xB870, 0xB872, 0xB873, 0xB874, 0xB875, 0xB876, 0xB877, 0xB879, 0xB87A, 0xB87B, 0xB87D, 0xB87E, 0xB87F, 0xB880, 0xB881, 0xB882, 0xB883, 0xB884, + 0xB885, 0xB886, 0xB887, 0xB888, 0xB889, 0xB88A, 0xB88B, 0xB88C, 0xB88E, 0xB88F, 0xB890, 0xB891, 0xB892, 0xB893, 0xB894, 0xB895, 0xB896, 0xB897, 0xB898, 0xB899, 0xB89A, 0xB89B, 0xB89C, 0xB89D, 0xB89E, 0xB89F, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB8A0, 0xB8A1, 0xB8A2, 0xB8A3, 0xB8A4, 0xB8A5, 0xB8A6, 0xB8A7, 0xB8A9, 0xB8AA, 0xB8AB, 0xB8AC, 0xB8AD, 0xB8AE, 0xB8AF, 0xB8B1, 0xB8B2, 0xB8B3, 0xB8B5, 0xB8B6, 0xB8B7, 0xB8B9, 0xB8BA, 0xB8BB, 0xB8BC, 0xB8BD, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB8BE, 0xB8BF, 0xB8C2, 0xB8C4, 0xB8C6, 0xB8C7, 0xB8C8, 0xB8C9, 0xB8CA, 0xB8CB, 0xB8CD, 0xB8CE, 0xB8CF, 0xB8D1, 0xB8D2, 0xB8D3, 0xB8D5, 0xB8D6, 0xB8D7, 0xB8D8, 0xB8D9, 0xB8DA, 0xB8DB, 0xB8DC, 0xB8DE, 0xB8E0, 0xB8E2, 0xB8E3, 0xB8E4, 0xB8E5, 0xB8E6, 0xB8E7, 0xB8EA, 0xB8EB, 0xB8ED, 0xB8EE, 0xB8EF, 0xB8F1, 0xB8F2, 0xB8F3, 0xB8F4, 0xB8F5, 0xB8F6, 0xB8F7, 0xB8FA, 0xB8FC, 0xB8FE, 0xB8FF, 0xB900, 0xB901, 0xB902, 0xB903, 0xB905, 0xB906, 0xB907, 0xB908, 0xB909, 0xB90A, 0xB90B, 0xB90C, 0xB90D, 0xB90E, 0xB90F, 0xB910, 0xB911, 0xB912, 0xB913, 0xB914, 0xB915, 0xB916, 0xB917, 0xB919, 0xB91A, 0xB91B, 0xB91C, 0xB91D, 0xB91E, 0xB91F, 0xB921, 0xB922, 0xB923, 0xB924, 0xB925, 0xB926, 0xB927, 0xB928, 0xB929, 0xB92A, 0xB92B, 0xB92C, 0xB92D, 0xB92E, 0xB92F, 0xB930, 0xB931, 0xB932, 0xB933, 0xB934, 0xB935, 0xB936, 0xB937, 0xB938, 0xB939, 0xB93A, 0xB93B, 0xB93E, 0xB93F, 0xB941, 0xB942, 0xB943, 0xB945, 0xB946, 0xB947, 0xB948, 0xB949, 0xB94A, 0xB94B, 0xB94D, 0xB94E, 0xB950, 0xB952, 0xB953, 0xB954, 0xB955, 0xB956, 0xB957, + 0xB95A, 0xB95B, 0xB95D, 0xB95E, 0xB95F, 0xB961, 0xB962, 0xB963, 0xB964, 0xB965, 0xB966, 0xB967, 0xB96A, 0xB96C, 0xB96E, 0xB96F, 0xB970, 0xB971, 0xB972, 0xB973, 0xB976, 0xB977, 0xB979, 0xB97A, 0xB97B, 0xB97D, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB97E, 0xB97F, 0xB980, 0xB981, 0xB982, 0xB983, 0xB986, 0xB988, 0xB98B, 0xB98C, 0xB98F, 0xB990, 0xB991, 0xB992, 0xB993, 0xB994, 0xB995, 0xB996, 0xB997, 0xB998, 0xB999, 0xB99A, 0xB99B, 0xB99C, 0xB99D, 0xB99E, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xB99F, 0xB9A0, 0xB9A1, 0xB9A2, 0xB9A3, 0xB9A4, 0xB9A5, 0xB9A6, 0xB9A7, 0xB9A8, 0xB9A9, 0xB9AA, 0xB9AB, 0xB9AE, 0xB9AF, 0xB9B1, 0xB9B2, 0xB9B3, 0xB9B5, 0xB9B6, 0xB9B7, 0xB9B8, 0xB9B9, 0xB9BA, 0xB9BB, 0xB9BE, 0xB9C0, 0xB9C2, 0xB9C3, 0xB9C4, 0xB9C5, 0xB9C6, 0xB9C7, 0xB9CA, 0xB9CB, 0xB9CD, 0xB9D3, 0xB9D4, 0xB9D5, 0xB9D6, 0xB9D7, 0xB9DA, 0xB9DC, 0xB9DF, 0xB9E0, 0xB9E2, 0xB9E6, 0xB9E7, 0xB9E9, 0xB9EA, 0xB9EB, 0xB9ED, 0xB9EE, 0xB9EF, 0xB9F0, 0xB9F1, 0xB9F2, 0xB9F3, 0xB9F6, 0xB9FB, 0xB9FC, 0xB9FD, 0xB9FE, 0xB9FF, 0xBA02, 0xBA03, 0xBA04, 0xBA05, 0xBA06, 0xBA07, 0xBA09, 0xBA0A, 0xBA0B, 0xBA0C, 0xBA0D, 0xBA0E, 0xBA0F, 0xBA10, 0xBA11, 0xBA12, 0xBA13, 0xBA14, 0xBA16, 0xBA17, 0xBA18, 0xBA19, 0xBA1A, 0xBA1B, 0xBA1C, 0xBA1D, 0xBA1E, 0xBA1F, 0xBA20, 0xBA21, 0xBA22, 0xBA23, 0xBA24, 0xBA25, 0xBA26, 0xBA27, 0xBA28, 0xBA29, 0xBA2A, 0xBA2B, 0xBA2C, 0xBA2D, 0xBA2E, 0xBA2F, 0xBA30, 0xBA31, 0xBA32, 0xBA33, 0xBA34, 0xBA35, 0xBA36, 0xBA37, 0xBA3A, 0xBA3B, 0xBA3D, 0xBA3E, 0xBA3F, 0xBA41, 0xBA43, 0xBA44, 0xBA45, 0xBA46, + 0xBA47, 0xBA4A, 0xBA4C, 0xBA4F, 0xBA50, 0xBA51, 0xBA52, 0xBA56, 0xBA57, 0xBA59, 0xBA5A, 0xBA5B, 0xBA5D, 0xBA5E, 0xBA5F, 0xBA60, 0xBA61, 0xBA62, 0xBA63, 0xBA66, 0xBA6A, 0xBA6B, 0xBA6C, 0xBA6D, 0xBA6E, 0xBA6F, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xBA72, 0xBA73, 0xBA75, 0xBA76, 0xBA77, 0xBA79, 0xBA7A, 0xBA7B, 0xBA7C, 0xBA7D, 0xBA7E, 0xBA7F, 0xBA80, 0xBA81, 0xBA82, 0xBA86, 0xBA88, 0xBA89, 0xBA8A, 0xBA8B, 0xBA8D, 0xBA8E, 0xBA8F, 0xBA90, 0xBA91, 0xBA92, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xBA93, 0xBA94, 0xBA95, 0xBA96, 0xBA97, 0xBA98, 0xBA99, 0xBA9A, 0xBA9B, 0xBA9C, 0xBA9D, 0xBA9E, 0xBA9F, 0xBAA0, 0xBAA1, 0xBAA2, 0xBAA3, 0xBAA4, 0xBAA5, 0xBAA6, 0xBAA7, 0xBAAA, 0xBAAD, 0xBAAE, 0xBAAF, 0xBAB1, 0xBAB3, 0xBAB4, 0xBAB5, 0xBAB6, 0xBAB7, 0xBABA, 0xBABC, 0xBABE, 0xBABF, 0xBAC0, 0xBAC1, 0xBAC2, 0xBAC3, 0xBAC5, 0xBAC6, 0xBAC7, 0xBAC9, 0xBACA, 0xBACB, 0xBACC, 0xBACD, 0xBACE, 0xBACF, 0xBAD0, 0xBAD1, 0xBAD2, 0xBAD3, 0xBAD4, 0xBAD5, 0xBAD6, 0xBAD7, 0xBADA, 0xBADB, 0xBADC, 0xBADD, 0xBADE, 0xBADF, 0xBAE0, 0xBAE1, 0xBAE2, 0xBAE3, 0xBAE4, 0xBAE5, 0xBAE6, 0xBAE7, 0xBAE8, 0xBAE9, 0xBAEA, 0xBAEB, 0xBAEC, 0xBAED, 0xBAEE, 0xBAEF, 0xBAF0, 0xBAF1, 0xBAF2, 0xBAF3, 0xBAF4, 0xBAF5, 0xBAF6, 0xBAF7, 0xBAF8, 0xBAF9, 0xBAFA, 0xBAFB, 0xBAFD, 0xBAFE, 0xBAFF, 0xBB01, 0xBB02, 0xBB03, 0xBB05, 0xBB06, 0xBB07, 0xBB08, 0xBB09, 0xBB0A, 0xBB0B, 0xBB0C, 0xBB0E, 0xBB10, 0xBB12, 0xBB13, 0xBB14, 0xBB15, 0xBB16, 0xBB17, 0xBB19, 0xBB1A, 0xBB1B, 0xBB1D, 0xBB1E, 0xBB1F, 0xBB21, 0xBB22, 0xBB23, 0xBB24, 0xBB25, 0xBB26, 0xBB27, + 0xBB28, 0xBB2A, 0xBB2C, 0xBB2D, 0xBB2E, 0xBB2F, 0xBB30, 0xBB31, 0xBB32, 0xBB33, 0xBB37, 0xBB39, 0xBB3A, 0xBB3F, 0xBB40, 0xBB41, 0xBB42, 0xBB43, 0xBB46, 0xBB48, 0xBB4A, 0xBB4B, 0xBB4C, 0xBB4E, 0xBB51, 0xBB52, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xBB53, 0xBB55, 0xBB56, 0xBB57, 0xBB59, 0xBB5A, 0xBB5B, 0xBB5C, 0xBB5D, 0xBB5E, 0xBB5F, 0xBB60, 0xBB62, 0xBB64, 0xBB65, 0xBB66, 0xBB67, 0xBB68, 0xBB69, 0xBB6A, 0xBB6B, 0xBB6D, 0xBB6E, 0xBB6F, 0xBB70, 0xBB71, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xBB72, 0xBB73, 0xBB74, 0xBB75, 0xBB76, 0xBB77, 0xBB78, 0xBB79, 0xBB7A, 0xBB7B, 0xBB7C, 0xBB7D, 0xBB7E, 0xBB7F, 0xBB80, 0xBB81, 0xBB82, 0xBB83, 0xBB84, 0xBB85, 0xBB86, 0xBB87, 0xBB89, 0xBB8A, 0xBB8B, 0xBB8D, 0xBB8E, 0xBB8F, 0xBB91, 0xBB92, 0xBB93, 0xBB94, 0xBB95, 0xBB96, 0xBB97, 0xBB98, 0xBB99, 0xBB9A, 0xBB9B, 0xBB9C, 0xBB9D, 0xBB9E, 0xBB9F, 0xBBA0, 0xBBA1, 0xBBA2, 0xBBA3, 0xBBA5, 0xBBA6, 0xBBA7, 0xBBA9, 0xBBAA, 0xBBAB, 0xBBAD, 0xBBAE, 0xBBAF, 0xBBB0, 0xBBB1, 0xBBB2, 0xBBB3, 0xBBB5, 0xBBB6, 0xBBB8, 0xBBB9, 0xBBBA, 0xBBBB, 0xBBBC, 0xBBBD, 0xBBBE, 0xBBBF, 0xBBC1, 0xBBC2, 0xBBC3, 0xBBC5, 0xBBC6, 0xBBC7, 0xBBC9, 0xBBCA, 0xBBCB, 0xBBCC, 0xBBCD, 0xBBCE, 0xBBCF, 0xBBD1, 0xBBD2, 0xBBD4, 0xBBD5, 0xBBD6, 0xBBD7, 0xBBD8, 0xBBD9, 0xBBDA, 0xBBDB, 0xBBDC, 0xBBDD, 0xBBDE, 0xBBDF, 0xBBE0, 0xBBE1, 0xBBE2, 0xBBE3, 0xBBE4, 0xBBE5, 0xBBE6, 0xBBE7, 0xBBE8, 0xBBE9, 0xBBEA, 0xBBEB, 0xBBEC, 0xBBED, 0xBBEE, 0xBBEF, 0xBBF0, 0xBBF1, 0xBBF2, 0xBBF3, 0xBBF4, 0xBBF5, 0xBBF6, 0xBBF7, 0xBBFA, 0xBBFB, 0xBBFD, 0xBBFE, 0xBC01, + 0xBC03, 0xBC04, 0xBC05, 0xBC06, 0xBC07, 0xBC0A, 0xBC0E, 0xBC10, 0xBC12, 0xBC13, 0xBC19, 0xBC1A, 0xBC20, 0xBC21, 0xBC22, 0xBC23, 0xBC26, 0xBC28, 0xBC2A, 0xBC2B, 0xBC2C, 0xBC2E, 0xBC2F, 0xBC32, 0xBC33, 0xBC35, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xBC36, 0xBC37, 0xBC39, 0xBC3A, 0xBC3B, 0xBC3C, 0xBC3D, 0xBC3E, 0xBC3F, 0xBC42, 0xBC46, 0xBC47, 0xBC48, 0xBC4A, 0xBC4B, 0xBC4E, 0xBC4F, 0xBC51, 0xBC52, 0xBC53, 0xBC54, 0xBC55, 0xBC56, 0xBC57, 0xBC58, 0xBC59, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xBC5A, 0xBC5B, 0xBC5C, 0xBC5E, 0xBC5F, 0xBC60, 0xBC61, 0xBC62, 0xBC63, 0xBC64, 0xBC65, 0xBC66, 0xBC67, 0xBC68, 0xBC69, 0xBC6A, 0xBC6B, 0xBC6C, 0xBC6D, 0xBC6E, 0xBC6F, 0xBC70, 0xBC71, 0xBC72, 0xBC73, 0xBC74, 0xBC75, 0xBC76, 0xBC77, 0xBC78, 0xBC79, 0xBC7A, 0xBC7B, 0xBC7C, 0xBC7D, 0xBC7E, 0xBC7F, 0xBC80, 0xBC81, 0xBC82, 0xBC83, 0xBC86, 0xBC87, 0xBC89, 0xBC8A, 0xBC8D, 0xBC8F, 0xBC90, 0xBC91, 0xBC92, 0xBC93, 0xBC96, 0xBC98, 0xBC9B, 0xBC9C, 0xBC9D, 0xBC9E, 0xBC9F, 0xBCA2, 0xBCA3, 0xBCA5, 0xBCA6, 0xBCA9, 0xBCAA, 0xBCAB, 0xBCAC, 0xBCAD, 0xBCAE, 0xBCAF, 0xBCB2, 0xBCB6, 0xBCB7, 0xBCB8, 0xBCB9, 0xBCBA, 0xBCBB, 0xBCBE, 0xBCBF, 0xBCC1, 0xBCC2, 0xBCC3, 0xBCC5, 0xBCC6, 0xBCC7, 0xBCC8, 0xBCC9, 0xBCCA, 0xBCCB, 0xBCCC, 0xBCCE, 0xBCD2, 0xBCD3, 0xBCD4, 0xBCD6, 0xBCD7, 0xBCD9, 0xBCDA, 0xBCDB, 0xBCDD, 0xBCDE, 0xBCDF, 0xBCE0, 0xBCE1, 0xBCE2, 0xBCE3, 0xBCE4, 0xBCE5, 0xBCE6, 0xBCE7, 0xBCE8, 0xBCE9, 0xBCEA, 0xBCEB, 0xBCEC, 0xBCED, 0xBCEE, 0xBCEF, 0xBCF0, 0xBCF1, 0xBCF2, 0xBCF3, 0xBCF7, 0xBCF9, 0xBCFA, 0xBCFB, 0xBCFD, + 0xBCFE, 0xBCFF, 0xBD00, 0xBD01, 0xBD02, 0xBD03, 0xBD06, 0xBD08, 0xBD0A, 0xBD0B, 0xBD0C, 0xBD0D, 0xBD0E, 0xBD0F, 0xBD11, 0xBD12, 0xBD13, 0xBD15, 0xBD16, 0xBD17, 0xBD18, 0xBD19, 0xBD1A, 0xBD1B, 0xBD1C, 0xBD1D, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xBD1E, 0xBD1F, 0xBD20, 0xBD21, 0xBD22, 0xBD23, 0xBD25, 0xBD26, 0xBD27, 0xBD28, 0xBD29, 0xBD2A, 0xBD2B, 0xBD2D, 0xBD2E, 0xBD2F, 0xBD30, 0xBD31, 0xBD32, 0xBD33, 0xBD34, 0xBD35, 0xBD36, 0xBD37, 0xBD38, 0xBD39, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xBD3A, 0xBD3B, 0xBD3C, 0xBD3D, 0xBD3E, 0xBD3F, 0xBD41, 0xBD42, 0xBD43, 0xBD44, 0xBD45, 0xBD46, 0xBD47, 0xBD4A, 0xBD4B, 0xBD4D, 0xBD4E, 0xBD4F, 0xBD51, 0xBD52, 0xBD53, 0xBD54, 0xBD55, 0xBD56, 0xBD57, 0xBD5A, 0xBD5B, 0xBD5C, 0xBD5D, 0xBD5E, 0xBD5F, 0xBD60, 0xBD61, 0xBD62, 0xBD63, 0xBD65, 0xBD66, 0xBD67, 0xBD69, 0xBD6A, 0xBD6B, 0xBD6C, 0xBD6D, 0xBD6E, 0xBD6F, 0xBD70, 0xBD71, 0xBD72, 0xBD73, 0xBD74, 0xBD75, 0xBD76, 0xBD77, 0xBD78, 0xBD79, 0xBD7A, 0xBD7B, 0xBD7C, 0xBD7D, 0xBD7E, 0xBD7F, 0xBD82, 0xBD83, 0xBD85, 0xBD86, 0xBD8B, 0xBD8C, 0xBD8D, 0xBD8E, 0xBD8F, 0xBD92, 0xBD94, 0xBD96, 0xBD97, 0xBD98, 0xBD9B, 0xBD9D, 0xBD9E, 0xBD9F, 0xBDA0, 0xBDA1, 0xBDA2, 0xBDA3, 0xBDA5, 0xBDA6, 0xBDA7, 0xBDA8, 0xBDA9, 0xBDAA, 0xBDAB, 0xBDAC, 0xBDAD, 0xBDAE, 0xBDAF, 0xBDB1, 0xBDB2, 0xBDB3, 0xBDB4, 0xBDB5, 0xBDB6, 0xBDB7, 0xBDB9, 0xBDBA, 0xBDBB, 0xBDBC, 0xBDBD, 0xBDBE, 0xBDBF, 0xBDC0, 0xBDC1, 0xBDC2, 0xBDC3, 0xBDC4, 0xBDC5, 0xBDC6, 0xBDC7, 0xBDC8, 0xBDC9, 0xBDCA, 0xBDCB, 0xBDCC, 0xBDCD, 0xBDCE, 0xBDCF, 0xBDD0, 0xBDD1, + 0xBDD2, 0xBDD3, 0xBDD6, 0xBDD7, 0xBDD9, 0xBDDA, 0xBDDB, 0xBDDD, 0xBDDE, 0xBDDF, 0xBDE0, 0xBDE1, 0xBDE2, 0xBDE3, 0xBDE4, 0xBDE5, 0xBDE6, 0xBDE7, 0xBDE8, 0xBDEA, 0xBDEB, 0xBDEC, 0xBDED, 0xBDEE, 0xBDEF, 0xBDF1, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xBDF2, 0xBDF3, 0xBDF5, 0xBDF6, 0xBDF7, 0xBDF9, 0xBDFA, 0xBDFB, 0xBDFC, 0xBDFD, 0xBDFE, 0xBDFF, 0xBE01, 0xBE02, 0xBE04, 0xBE06, 0xBE07, 0xBE08, 0xBE09, 0xBE0A, 0xBE0B, 0xBE0E, 0xBE0F, 0xBE11, 0xBE12, 0xBE13, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xBE15, 0xBE16, 0xBE17, 0xBE18, 0xBE19, 0xBE1A, 0xBE1B, 0xBE1E, 0xBE20, 0xBE21, 0xBE22, 0xBE23, 0xBE24, 0xBE25, 0xBE26, 0xBE27, 0xBE28, 0xBE29, 0xBE2A, 0xBE2B, 0xBE2C, 0xBE2D, 0xBE2E, 0xBE2F, 0xBE30, 0xBE31, 0xBE32, 0xBE33, 0xBE34, 0xBE35, 0xBE36, 0xBE37, 0xBE38, 0xBE39, 0xBE3A, 0xBE3B, 0xBE3C, 0xBE3D, 0xBE3E, 0xBE3F, 0xBE40, 0xBE41, 0xBE42, 0xBE43, 0xBE46, 0xBE47, 0xBE49, 0xBE4A, 0xBE4B, 0xBE4D, 0xBE4F, 0xBE50, 0xBE51, 0xBE52, 0xBE53, 0xBE56, 0xBE58, 0xBE5C, 0xBE5D, 0xBE5E, 0xBE5F, 0xBE62, 0xBE63, 0xBE65, 0xBE66, 0xBE67, 0xBE69, 0xBE6B, 0xBE6C, 0xBE6D, 0xBE6E, 0xBE6F, 0xBE72, 0xBE76, 0xBE77, 0xBE78, 0xBE79, 0xBE7A, 0xBE7E, 0xBE7F, 0xBE81, 0xBE82, 0xBE83, 0xBE85, 0xBE86, 0xBE87, 0xBE88, 0xBE89, 0xBE8A, 0xBE8B, 0xBE8E, 0xBE92, 0xBE93, 0xBE94, 0xBE95, 0xBE96, 0xBE97, 0xBE9A, 0xBE9B, 0xBE9C, 0xBE9D, 0xBE9E, 0xBE9F, 0xBEA0, 0xBEA1, 0xBEA2, 0xBEA3, 0xBEA4, 0xBEA5, 0xBEA6, 0xBEA7, 0xBEA9, 0xBEAA, 0xBEAB, 0xBEAC, 0xBEAD, 0xBEAE, 0xBEAF, 0xBEB0, 0xBEB1, 0xBEB2, 0xBEB3, 0xBEB4, 0xBEB5, 0xBEB6, 0xBEB7, + 0xBEB8, 0xBEB9, 0xBEBA, 0xBEBB, 0xBEBC, 0xBEBD, 0xBEBE, 0xBEBF, 0xBEC0, 0xBEC1, 0xBEC2, 0xBEC3, 0xBEC4, 0xBEC5, 0xBEC6, 0xBEC7, 0xBEC8, 0xBEC9, 0xBECA, 0xBECB, 0xBECC, 0xBECD, 0xBECE, 0xBECF, 0xBED2, 0xBED3, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xBED5, 0xBED6, 0xBED9, 0xBEDA, 0xBEDB, 0xBEDC, 0xBEDD, 0xBEDE, 0xBEDF, 0xBEE1, 0xBEE2, 0xBEE6, 0xBEE7, 0xBEE8, 0xBEE9, 0xBEEA, 0xBEEB, 0xBEED, 0xBEEE, 0xBEEF, 0xBEF0, 0xBEF1, 0xBEF2, 0xBEF3, 0xBEF4, 0xBEF5, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xBEF6, 0xBEF7, 0xBEF8, 0xBEF9, 0xBEFA, 0xBEFB, 0xBEFC, 0xBEFD, 0xBEFE, 0xBEFF, 0xBF00, 0xBF02, 0xBF03, 0xBF04, 0xBF05, 0xBF06, 0xBF07, 0xBF0A, 0xBF0B, 0xBF0C, 0xBF0D, 0xBF0E, 0xBF0F, 0xBF10, 0xBF11, 0xBF12, 0xBF13, 0xBF14, 0xBF15, 0xBF16, 0xBF17, 0xBF1A, 0xBF1E, 0xBF1F, 0xBF20, 0xBF21, 0xBF22, 0xBF23, 0xBF24, 0xBF25, 0xBF26, 0xBF27, 0xBF28, 0xBF29, 0xBF2A, 0xBF2B, 0xBF2C, 0xBF2D, 0xBF2E, 0xBF2F, 0xBF30, 0xBF31, 0xBF32, 0xBF33, 0xBF34, 0xBF35, 0xBF36, 0xBF37, 0xBF38, 0xBF39, 0xBF3A, 0xBF3B, 0xBF3C, 0xBF3D, 0xBF3E, 0xBF3F, 0xBF42, 0xBF43, 0xBF45, 0xBF46, 0xBF47, 0xBF49, 0xBF4A, 0xBF4B, 0xBF4C, 0xBF4D, 0xBF4E, 0xBF4F, 0xBF52, 0xBF53, 0xBF54, 0xBF56, 0xBF57, 0xBF58, 0xBF59, 0xBF5A, 0xBF5B, 0xBF5C, 0xBF5D, 0xBF5E, 0xBF5F, 0xBF60, 0xBF61, 0xBF62, 0xBF63, 0xBF64, 0xBF65, 0xBF66, 0xBF67, 0xBF68, 0xBF69, 0xBF6A, 0xBF6B, 0xBF6C, 0xBF6D, 0xBF6E, 0xBF6F, 0xBF70, 0xBF71, 0xBF72, 0xBF73, 0xBF74, 0xBF75, 0xBF76, 0xBF77, 0xBF78, 0xBF79, 0xBF7A, 0xBF7B, 0xBF7C, 0xBF7D, 0xBF7E, 0xBF7F, 0xBF80, 0xBF81, 0xBF82, + 0xBF83, 0xBF84, 0xBF85, 0xBF86, 0xBF87, 0xBF88, 0xBF89, 0xBF8A, 0xBF8B, 0xBF8C, 0xBF8D, 0xBF8E, 0xBF8F, 0xBF90, 0xBF91, 0xBF92, 0xBF93, 0xBF95, 0xBF96, 0xBF97, 0xBF98, 0xBF99, 0xBF9A, 0xBF9B, 0xBF9C, 0xBF9D, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xBF9E, 0xBF9F, 0xBFA0, 0xBFA1, 0xBFA2, 0xBFA3, 0xBFA4, 0xBFA5, 0xBFA6, 0xBFA7, 0xBFA8, 0xBFA9, 0xBFAA, 0xBFAB, 0xBFAC, 0xBFAD, 0xBFAE, 0xBFAF, 0xBFB1, 0xBFB2, 0xBFB3, 0xBFB4, 0xBFB5, 0xBFB6, 0xBFB7, 0xBFB8, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xBFB9, 0xBFBA, 0xBFBB, 0xBFBC, 0xBFBD, 0xBFBE, 0xBFBF, 0xBFC0, 0xBFC1, 0xBFC2, 0xBFC3, 0xBFC4, 0xBFC6, 0xBFC7, 0xBFC8, 0xBFC9, 0xBFCA, 0xBFCB, 0xBFCE, 0xBFCF, 0xBFD1, 0xBFD2, 0xBFD3, 0xBFD5, 0xBFD6, 0xBFD7, 0xBFD8, 0xBFD9, 0xBFDA, 0xBFDB, 0xBFDD, 0xBFDE, 0xBFE0, 0xBFE2, 0xBFE3, 0xBFE4, 0xBFE5, 0xBFE6, 0xBFE7, 0xBFE8, 0xBFE9, 0xBFEA, 0xBFEB, 0xBFEC, 0xBFED, 0xBFEE, 0xBFEF, 0xBFF0, 0xBFF1, 0xBFF2, 0xBFF3, 0xBFF4, 0xBFF5, 0xBFF6, 0xBFF7, 0xBFF8, 0xBFF9, 0xBFFA, 0xBFFB, 0xBFFC, 0xBFFD, 0xBFFE, 0xBFFF, 0xC000, 0xC001, 0xC002, 0xC003, 0xC004, 0xC005, 0xC006, 0xC007, 0xC008, 0xC009, 0xC00A, 0xC00B, 0xC00C, 0xC00D, 0xC00E, 0xC00F, 0xC010, 0xC011, 0xC012, 0xC013, 0xC014, 0xC015, 0xC016, 0xC017, 0xC018, 0xC019, 0xC01A, 0xC01B, 0xC01C, 0xC01D, 0xC01E, 0xC01F, 0xC020, 0xC021, 0xC022, 0xC023, 0xC024, 0xC025, 0xC026, 0xC027, 0xC028, 0xC029, 0xC02A, 0xC02B, 0xC02C, 0xC02D, 0xC02E, 0xC02F, 0xC030, 0xC031, 0xC032, 0xC033, 0xC034, 0xC035, 0xC036, 0xC037, 0xC038, 0xC039, 0xC03A, 0xC03B, 0xC03D, 0xC03E, 0xC03F, + 0xC040, 0xC041, 0xC042, 0xC043, 0xC044, 0xC045, 0xC046, 0xC047, 0xC048, 0xC049, 0xC04A, 0xC04B, 0xC04C, 0xC04D, 0xC04E, 0xC04F, 0xC050, 0xC052, 0xC053, 0xC054, 0xC055, 0xC056, 0xC057, 0xC059, 0xC05A, 0xC05B, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC05D, 0xC05E, 0xC05F, 0xC061, 0xC062, 0xC063, 0xC064, 0xC065, 0xC066, 0xC067, 0xC06A, 0xC06B, 0xC06C, 0xC06D, 0xC06E, 0xC06F, 0xC070, 0xC071, 0xC072, 0xC073, 0xC074, 0xC075, 0xC076, 0xC077, 0xC078, 0xC079, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC07A, 0xC07B, 0xC07C, 0xC07D, 0xC07E, 0xC07F, 0xC080, 0xC081, 0xC082, 0xC083, 0xC084, 0xC085, 0xC086, 0xC087, 0xC088, 0xC089, 0xC08A, 0xC08B, 0xC08C, 0xC08D, 0xC08E, 0xC08F, 0xC092, 0xC093, 0xC095, 0xC096, 0xC097, 0xC099, 0xC09A, 0xC09B, 0xC09C, 0xC09D, 0xC09E, 0xC09F, 0xC0A2, 0xC0A4, 0xC0A6, 0xC0A7, 0xC0A8, 0xC0A9, 0xC0AA, 0xC0AB, 0xC0AE, 0xC0B1, 0xC0B2, 0xC0B7, 0xC0B8, 0xC0B9, 0xC0BA, 0xC0BB, 0xC0BE, 0xC0C2, 0xC0C3, 0xC0C4, 0xC0C6, 0xC0C7, 0xC0CA, 0xC0CB, 0xC0CD, 0xC0CE, 0xC0CF, 0xC0D1, 0xC0D2, 0xC0D3, 0xC0D4, 0xC0D5, 0xC0D6, 0xC0D7, 0xC0DA, 0xC0DE, 0xC0DF, 0xC0E0, 0xC0E1, 0xC0E2, 0xC0E3, 0xC0E6, 0xC0E7, 0xC0E9, 0xC0EA, 0xC0EB, 0xC0ED, 0xC0EE, 0xC0EF, 0xC0F0, 0xC0F1, 0xC0F2, 0xC0F3, 0xC0F6, 0xC0F8, 0xC0FA, 0xC0FB, 0xC0FC, 0xC0FD, 0xC0FE, 0xC0FF, 0xC101, 0xC102, 0xC103, 0xC105, 0xC106, 0xC107, 0xC109, 0xC10A, 0xC10B, 0xC10C, 0xC10D, 0xC10E, 0xC10F, 0xC111, 0xC112, 0xC113, 0xC114, 0xC116, 0xC117, 0xC118, 0xC119, 0xC11A, 0xC11B, 0xC121, 0xC122, 0xC125, 0xC128, 0xC129, 0xC12A, 0xC12B, 0xC12E, + 0xC132, 0xC133, 0xC134, 0xC135, 0xC137, 0xC13A, 0xC13B, 0xC13D, 0xC13E, 0xC13F, 0xC141, 0xC142, 0xC143, 0xC144, 0xC145, 0xC146, 0xC147, 0xC14A, 0xC14E, 0xC14F, 0xC150, 0xC151, 0xC152, 0xC153, 0xC156, 0xC157, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC159, 0xC15A, 0xC15B, 0xC15D, 0xC15E, 0xC15F, 0xC160, 0xC161, 0xC162, 0xC163, 0xC166, 0xC16A, 0xC16B, 0xC16C, 0xC16D, 0xC16E, 0xC16F, 0xC171, 0xC172, 0xC173, 0xC175, 0xC176, 0xC177, 0xC179, 0xC17A, 0xC17B, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC17C, 0xC17D, 0xC17E, 0xC17F, 0xC180, 0xC181, 0xC182, 0xC183, 0xC184, 0xC186, 0xC187, 0xC188, 0xC189, 0xC18A, 0xC18B, 0xC18F, 0xC191, 0xC192, 0xC193, 0xC195, 0xC197, 0xC198, 0xC199, 0xC19A, 0xC19B, 0xC19E, 0xC1A0, 0xC1A2, 0xC1A3, 0xC1A4, 0xC1A6, 0xC1A7, 0xC1AA, 0xC1AB, 0xC1AD, 0xC1AE, 0xC1AF, 0xC1B1, 0xC1B2, 0xC1B3, 0xC1B4, 0xC1B5, 0xC1B6, 0xC1B7, 0xC1B8, 0xC1B9, 0xC1BA, 0xC1BB, 0xC1BC, 0xC1BE, 0xC1BF, 0xC1C0, 0xC1C1, 0xC1C2, 0xC1C3, 0xC1C5, 0xC1C6, 0xC1C7, 0xC1C9, 0xC1CA, 0xC1CB, 0xC1CD, 0xC1CE, 0xC1CF, 0xC1D0, 0xC1D1, 0xC1D2, 0xC1D3, 0xC1D5, 0xC1D6, 0xC1D9, 0xC1DA, 0xC1DB, 0xC1DC, 0xC1DD, 0xC1DE, 0xC1DF, 0xC1E1, 0xC1E2, 0xC1E3, 0xC1E5, 0xC1E6, 0xC1E7, 0xC1E9, 0xC1EA, 0xC1EB, 0xC1EC, 0xC1ED, 0xC1EE, 0xC1EF, 0xC1F2, 0xC1F4, 0xC1F5, 0xC1F6, 0xC1F7, 0xC1F8, 0xC1F9, 0xC1FA, 0xC1FB, 0xC1FE, 0xC1FF, 0xC201, 0xC202, 0xC203, 0xC205, 0xC206, 0xC207, 0xC208, 0xC209, 0xC20A, 0xC20B, 0xC20E, 0xC210, 0xC212, 0xC213, 0xC214, 0xC215, 0xC216, 0xC217, 0xC21A, 0xC21B, 0xC21D, 0xC21E, 0xC221, 0xC222, 0xC223, + 0xC224, 0xC225, 0xC226, 0xC227, 0xC22A, 0xC22C, 0xC22E, 0xC230, 0xC233, 0xC235, 0xC236, 0xC237, 0xC238, 0xC239, 0xC23A, 0xC23B, 0xC23C, 0xC23D, 0xC23E, 0xC23F, 0xC240, 0xC241, 0xC242, 0xC243, 0xC244, 0xC245, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC246, 0xC247, 0xC249, 0xC24A, 0xC24B, 0xC24C, 0xC24D, 0xC24E, 0xC24F, 0xC252, 0xC253, 0xC255, 0xC256, 0xC257, 0xC259, 0xC25A, 0xC25B, 0xC25C, 0xC25D, 0xC25E, 0xC25F, 0xC261, 0xC262, 0xC263, 0xC264, 0xC266, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC267, 0xC268, 0xC269, 0xC26A, 0xC26B, 0xC26E, 0xC26F, 0xC271, 0xC272, 0xC273, 0xC275, 0xC276, 0xC277, 0xC278, 0xC279, 0xC27A, 0xC27B, 0xC27E, 0xC280, 0xC282, 0xC283, 0xC284, 0xC285, 0xC286, 0xC287, 0xC28A, 0xC28B, 0xC28C, 0xC28D, 0xC28E, 0xC28F, 0xC291, 0xC292, 0xC293, 0xC294, 0xC295, 0xC296, 0xC297, 0xC299, 0xC29A, 0xC29C, 0xC29E, 0xC29F, 0xC2A0, 0xC2A1, 0xC2A2, 0xC2A3, 0xC2A6, 0xC2A7, 0xC2A9, 0xC2AA, 0xC2AB, 0xC2AE, 0xC2AF, 0xC2B0, 0xC2B1, 0xC2B2, 0xC2B3, 0xC2B6, 0xC2B8, 0xC2BA, 0xC2BB, 0xC2BC, 0xC2BD, 0xC2BE, 0xC2BF, 0xC2C0, 0xC2C1, 0xC2C2, 0xC2C3, 0xC2C4, 0xC2C5, 0xC2C6, 0xC2C7, 0xC2C8, 0xC2C9, 0xC2CA, 0xC2CB, 0xC2CC, 0xC2CD, 0xC2CE, 0xC2CF, 0xC2D0, 0xC2D1, 0xC2D2, 0xC2D3, 0xC2D4, 0xC2D5, 0xC2D6, 0xC2D7, 0xC2D8, 0xC2D9, 0xC2DA, 0xC2DB, 0xC2DE, 0xC2DF, 0xC2E1, 0xC2E2, 0xC2E5, 0xC2E6, 0xC2E7, 0xC2E8, 0xC2E9, 0xC2EA, 0xC2EE, 0xC2F0, 0xC2F2, 0xC2F3, 0xC2F4, 0xC2F5, 0xC2F7, 0xC2FA, 0xC2FD, 0xC2FE, 0xC2FF, 0xC301, 0xC302, 0xC303, 0xC304, 0xC305, 0xC306, 0xC307, 0xC30A, 0xC30B, 0xC30E, 0xC30F, + 0xC310, 0xC311, 0xC312, 0xC316, 0xC317, 0xC319, 0xC31A, 0xC31B, 0xC31D, 0xC31E, 0xC31F, 0xC320, 0xC321, 0xC322, 0xC323, 0xC326, 0xC327, 0xC32A, 0xC32B, 0xC32C, 0xC32D, 0xC32E, 0xC32F, 0xC330, 0xC331, 0xC332, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC333, 0xC334, 0xC335, 0xC336, 0xC337, 0xC338, 0xC339, 0xC33A, 0xC33B, 0xC33C, 0xC33D, 0xC33E, 0xC33F, 0xC340, 0xC341, 0xC342, 0xC343, 0xC344, 0xC346, 0xC347, 0xC348, 0xC349, 0xC34A, 0xC34B, 0xC34C, 0xC34D, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC34E, 0xC34F, 0xC350, 0xC351, 0xC352, 0xC353, 0xC354, 0xC355, 0xC356, 0xC357, 0xC358, 0xC359, 0xC35A, 0xC35B, 0xC35C, 0xC35D, 0xC35E, 0xC35F, 0xC360, 0xC361, 0xC362, 0xC363, 0xC364, 0xC365, 0xC366, 0xC367, 0xC36A, 0xC36B, 0xC36D, 0xC36E, 0xC36F, 0xC371, 0xC373, 0xC374, 0xC375, 0xC376, 0xC377, 0xC37A, 0xC37B, 0xC37E, 0xC37F, 0xC380, 0xC381, 0xC382, 0xC383, 0xC385, 0xC386, 0xC387, 0xC389, 0xC38A, 0xC38B, 0xC38D, 0xC38E, 0xC38F, 0xC390, 0xC391, 0xC392, 0xC393, 0xC394, 0xC395, 0xC396, 0xC397, 0xC398, 0xC399, 0xC39A, 0xC39B, 0xC39C, 0xC39D, 0xC39E, 0xC39F, 0xC3A0, 0xC3A1, 0xC3A2, 0xC3A3, 0xC3A4, 0xC3A5, 0xC3A6, 0xC3A7, 0xC3A8, 0xC3A9, 0xC3AA, 0xC3AB, 0xC3AC, 0xC3AD, 0xC3AE, 0xC3AF, 0xC3B0, 0xC3B1, 0xC3B2, 0xC3B3, 0xC3B4, 0xC3B5, 0xC3B6, 0xC3B7, 0xC3B8, 0xC3B9, 0xC3BA, 0xC3BB, 0xC3BC, 0xC3BD, 0xC3BE, 0xC3BF, 0xC3C1, 0xC3C2, 0xC3C3, 0xC3C4, 0xC3C5, 0xC3C6, 0xC3C7, 0xC3C8, 0xC3C9, 0xC3CA, 0xC3CB, 0xC3CC, 0xC3CD, 0xC3CE, 0xC3CF, 0xC3D0, 0xC3D1, 0xC3D2, 0xC3D3, 0xC3D4, 0xC3D5, 0xC3D6, 0xC3D7, 0xC3DA, + 0xC3DB, 0xC3DD, 0xC3DE, 0xC3E1, 0xC3E3, 0xC3E4, 0xC3E5, 0xC3E6, 0xC3E7, 0xC3EA, 0xC3EB, 0xC3EC, 0xC3EE, 0xC3EF, 0xC3F0, 0xC3F1, 0xC3F2, 0xC3F3, 0xC3F6, 0xC3F7, 0xC3F9, 0xC3FA, 0xC3FB, 0xC3FC, 0xC3FD, 0xC3FE, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC3FF, 0xC400, 0xC401, 0xC402, 0xC403, 0xC404, 0xC405, 0xC406, 0xC407, 0xC409, 0xC40A, 0xC40B, 0xC40C, 0xC40D, 0xC40E, 0xC40F, 0xC411, 0xC412, 0xC413, 0xC414, 0xC415, 0xC416, 0xC417, 0xC418, 0xC419, 0xC41A, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC41B, 0xC41C, 0xC41D, 0xC41E, 0xC41F, 0xC420, 0xC421, 0xC422, 0xC423, 0xC425, 0xC426, 0xC427, 0xC428, 0xC429, 0xC42A, 0xC42B, 0xC42D, 0xC42E, 0xC42F, 0xC431, 0xC432, 0xC433, 0xC435, 0xC436, 0xC437, 0xC438, 0xC439, 0xC43A, 0xC43B, 0xC43E, 0xC43F, 0xC440, 0xC441, 0xC442, 0xC443, 0xC444, 0xC445, 0xC446, 0xC447, 0xC449, 0xC44A, 0xC44B, 0xC44C, 0xC44D, 0xC44E, 0xC44F, 0xC450, 0xC451, 0xC452, 0xC453, 0xC454, 0xC455, 0xC456, 0xC457, 0xC458, 0xC459, 0xC45A, 0xC45B, 0xC45C, 0xC45D, 0xC45E, 0xC45F, 0xC460, 0xC461, 0xC462, 0xC463, 0xC466, 0xC467, 0xC469, 0xC46A, 0xC46B, 0xC46D, 0xC46E, 0xC46F, 0xC470, 0xC471, 0xC472, 0xC473, 0xC476, 0xC477, 0xC478, 0xC47A, 0xC47B, 0xC47C, 0xC47D, 0xC47E, 0xC47F, 0xC481, 0xC482, 0xC483, 0xC484, 0xC485, 0xC486, 0xC487, 0xC488, 0xC489, 0xC48A, 0xC48B, 0xC48C, 0xC48D, 0xC48E, 0xC48F, 0xC490, 0xC491, 0xC492, 0xC493, 0xC495, 0xC496, 0xC497, 0xC498, 0xC499, 0xC49A, 0xC49B, 0xC49D, 0xC49E, 0xC49F, 0xC4A0, 0xC4A1, 0xC4A2, 0xC4A3, 0xC4A4, 0xC4A5, 0xC4A6, 0xC4A7, 0xC4A8, 0xC4A9, + 0xC4AA, 0xC4AB, 0xC4AC, 0xC4AD, 0xC4AE, 0xC4AF, 0xC4B0, 0xC4B1, 0xC4B2, 0xC4B3, 0xC4B4, 0xC4B5, 0xC4B6, 0xC4B7, 0xC4B9, 0xC4BA, 0xC4BB, 0xC4BD, 0xC4BE, 0xC4BF, 0xC4C0, 0xC4C1, 0xC4C2, 0xC4C3, 0xC4C4, 0xC4C5, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC4C6, 0xC4C7, 0xC4C8, 0xC4C9, 0xC4CA, 0xC4CB, 0xC4CC, 0xC4CD, 0xC4CE, 0xC4CF, 0xC4D0, 0xC4D1, 0xC4D2, 0xC4D3, 0xC4D4, 0xC4D5, 0xC4D6, 0xC4D7, 0xC4D8, 0xC4D9, 0xC4DA, 0xC4DB, 0xC4DC, 0xC4DD, 0xC4DE, 0xC4DF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC4E0, 0xC4E1, 0xC4E2, 0xC4E3, 0xC4E4, 0xC4E5, 0xC4E6, 0xC4E7, 0xC4E8, 0xC4EA, 0xC4EB, 0xC4EC, 0xC4ED, 0xC4EE, 0xC4EF, 0xC4F2, 0xC4F3, 0xC4F5, 0xC4F6, 0xC4F7, 0xC4F9, 0xC4FB, 0xC4FC, 0xC4FD, 0xC4FE, 0xC502, 0xC503, 0xC504, 0xC505, 0xC506, 0xC507, 0xC508, 0xC509, 0xC50A, 0xC50B, 0xC50D, 0xC50E, 0xC50F, 0xC511, 0xC512, 0xC513, 0xC515, 0xC516, 0xC517, 0xC518, 0xC519, 0xC51A, 0xC51B, 0xC51D, 0xC51E, 0xC51F, 0xC520, 0xC521, 0xC522, 0xC523, 0xC524, 0xC525, 0xC526, 0xC527, 0xC52A, 0xC52B, 0xC52D, 0xC52E, 0xC52F, 0xC531, 0xC532, 0xC533, 0xC534, 0xC535, 0xC536, 0xC537, 0xC53A, 0xC53C, 0xC53E, 0xC53F, 0xC540, 0xC541, 0xC542, 0xC543, 0xC546, 0xC547, 0xC54B, 0xC54F, 0xC550, 0xC551, 0xC552, 0xC556, 0xC55A, 0xC55B, 0xC55C, 0xC55F, 0xC562, 0xC563, 0xC565, 0xC566, 0xC567, 0xC569, 0xC56A, 0xC56B, 0xC56C, 0xC56D, 0xC56E, 0xC56F, 0xC572, 0xC576, 0xC577, 0xC578, 0xC579, 0xC57A, 0xC57B, 0xC57E, 0xC57F, 0xC581, 0xC582, 0xC583, 0xC585, 0xC586, 0xC588, 0xC589, 0xC58A, 0xC58B, 0xC58E, 0xC590, 0xC592, 0xC593, 0xC594, + 0xC596, 0xC599, 0xC59A, 0xC59B, 0xC59D, 0xC59E, 0xC59F, 0xC5A1, 0xC5A2, 0xC5A3, 0xC5A4, 0xC5A5, 0xC5A6, 0xC5A7, 0xC5A8, 0xC5AA, 0xC5AB, 0xC5AC, 0xC5AD, 0xC5AE, 0xC5AF, 0xC5B0, 0xC5B1, 0xC5B2, 0xC5B3, 0xC5B6, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC5B7, 0xC5BA, 0xC5BF, 0xC5C0, 0xC5C1, 0xC5C2, 0xC5C3, 0xC5CB, 0xC5CD, 0xC5CF, 0xC5D2, 0xC5D3, 0xC5D5, 0xC5D6, 0xC5D7, 0xC5D9, 0xC5DA, 0xC5DB, 0xC5DC, 0xC5DD, 0xC5DE, 0xC5DF, 0xC5E2, 0xC5E4, 0xC5E6, 0xC5E7, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC5E8, 0xC5E9, 0xC5EA, 0xC5EB, 0xC5EF, 0xC5F1, 0xC5F2, 0xC5F3, 0xC5F5, 0xC5F8, 0xC5F9, 0xC5FA, 0xC5FB, 0xC602, 0xC603, 0xC604, 0xC609, 0xC60A, 0xC60B, 0xC60D, 0xC60E, 0xC60F, 0xC611, 0xC612, 0xC613, 0xC614, 0xC615, 0xC616, 0xC617, 0xC61A, 0xC61D, 0xC61E, 0xC61F, 0xC620, 0xC621, 0xC622, 0xC623, 0xC626, 0xC627, 0xC629, 0xC62A, 0xC62B, 0xC62F, 0xC631, 0xC632, 0xC636, 0xC638, 0xC63A, 0xC63C, 0xC63D, 0xC63E, 0xC63F, 0xC642, 0xC643, 0xC645, 0xC646, 0xC647, 0xC649, 0xC64A, 0xC64B, 0xC64C, 0xC64D, 0xC64E, 0xC64F, 0xC652, 0xC656, 0xC657, 0xC658, 0xC659, 0xC65A, 0xC65B, 0xC65E, 0xC65F, 0xC661, 0xC662, 0xC663, 0xC664, 0xC665, 0xC666, 0xC667, 0xC668, 0xC669, 0xC66A, 0xC66B, 0xC66D, 0xC66E, 0xC670, 0xC672, 0xC673, 0xC674, 0xC675, 0xC676, 0xC677, 0xC67A, 0xC67B, 0xC67D, 0xC67E, 0xC67F, 0xC681, 0xC682, 0xC683, 0xC684, 0xC685, 0xC686, 0xC687, 0xC68A, 0xC68C, 0xC68E, 0xC68F, 0xC690, 0xC691, 0xC692, 0xC693, 0xC696, 0xC697, 0xC699, 0xC69A, 0xC69B, 0xC69D, 0xC69E, 0xC69F, 0xC6A0, 0xC6A1, 0xC6A2, 0xC6A3, 0xC6A6, + 0xC6A8, 0xC6AA, 0xC6AB, 0xC6AC, 0xC6AD, 0xC6AE, 0xC6AF, 0xC6B2, 0xC6B3, 0xC6B5, 0xC6B6, 0xC6B7, 0xC6BB, 0xC6BC, 0xC6BD, 0xC6BE, 0xC6BF, 0xC6C2, 0xC6C4, 0xC6C6, 0xC6C7, 0xC6C8, 0xC6C9, 0xC6CA, 0xC6CB, 0xC6CE, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC6CF, 0xC6D1, 0xC6D2, 0xC6D3, 0xC6D5, 0xC6D6, 0xC6D7, 0xC6D8, 0xC6D9, 0xC6DA, 0xC6DB, 0xC6DE, 0xC6DF, 0xC6E2, 0xC6E3, 0xC6E4, 0xC6E5, 0xC6E6, 0xC6E7, 0xC6EA, 0xC6EB, 0xC6ED, 0xC6EE, 0xC6EF, 0xC6F1, 0xC6F2, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC6F3, 0xC6F4, 0xC6F5, 0xC6F6, 0xC6F7, 0xC6FA, 0xC6FB, 0xC6FC, 0xC6FE, 0xC6FF, 0xC700, 0xC701, 0xC702, 0xC703, 0xC706, 0xC707, 0xC709, 0xC70A, 0xC70B, 0xC70D, 0xC70E, 0xC70F, 0xC710, 0xC711, 0xC712, 0xC713, 0xC716, 0xC718, 0xC71A, 0xC71B, 0xC71C, 0xC71D, 0xC71E, 0xC71F, 0xC722, 0xC723, 0xC725, 0xC726, 0xC727, 0xC729, 0xC72A, 0xC72B, 0xC72C, 0xC72D, 0xC72E, 0xC72F, 0xC732, 0xC734, 0xC736, 0xC738, 0xC739, 0xC73A, 0xC73B, 0xC73E, 0xC73F, 0xC741, 0xC742, 0xC743, 0xC745, 0xC746, 0xC747, 0xC748, 0xC749, 0xC74B, 0xC74E, 0xC750, 0xC759, 0xC75A, 0xC75B, 0xC75D, 0xC75E, 0xC75F, 0xC761, 0xC762, 0xC763, 0xC764, 0xC765, 0xC766, 0xC767, 0xC769, 0xC76A, 0xC76C, 0xC76D, 0xC76E, 0xC76F, 0xC770, 0xC771, 0xC772, 0xC773, 0xC776, 0xC777, 0xC779, 0xC77A, 0xC77B, 0xC77F, 0xC780, 0xC781, 0xC782, 0xC786, 0xC78B, 0xC78C, 0xC78D, 0xC78F, 0xC792, 0xC793, 0xC795, 0xC799, 0xC79B, 0xC79C, 0xC79D, 0xC79E, 0xC79F, 0xC7A2, 0xC7A7, 0xC7A8, 0xC7A9, 0xC7AA, 0xC7AB, 0xC7AE, 0xC7AF, 0xC7B1, 0xC7B2, 0xC7B3, 0xC7B5, 0xC7B6, 0xC7B7, + 0xC7B8, 0xC7B9, 0xC7BA, 0xC7BB, 0xC7BE, 0xC7C2, 0xC7C3, 0xC7C4, 0xC7C5, 0xC7C6, 0xC7C7, 0xC7CA, 0xC7CB, 0xC7CD, 0xC7CF, 0xC7D1, 0xC7D2, 0xC7D3, 0xC7D4, 0xC7D5, 0xC7D6, 0xC7D7, 0xC7D9, 0xC7DA, 0xC7DB, 0xC7DC, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC7DE, 0xC7DF, 0xC7E0, 0xC7E1, 0xC7E2, 0xC7E3, 0xC7E5, 0xC7E6, 0xC7E7, 0xC7E9, 0xC7EA, 0xC7EB, 0xC7ED, 0xC7EE, 0xC7EF, 0xC7F0, 0xC7F1, 0xC7F2, 0xC7F3, 0xC7F4, 0xC7F5, 0xC7F6, 0xC7F7, 0xC7F8, 0xC7F9, 0xC7FA, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC7FB, 0xC7FC, 0xC7FD, 0xC7FE, 0xC7FF, 0xC802, 0xC803, 0xC805, 0xC806, 0xC807, 0xC809, 0xC80B, 0xC80C, 0xC80D, 0xC80E, 0xC80F, 0xC812, 0xC814, 0xC817, 0xC818, 0xC819, 0xC81A, 0xC81B, 0xC81E, 0xC81F, 0xC821, 0xC822, 0xC823, 0xC825, 0xC826, 0xC827, 0xC828, 0xC829, 0xC82A, 0xC82B, 0xC82E, 0xC830, 0xC832, 0xC833, 0xC834, 0xC835, 0xC836, 0xC837, 0xC839, 0xC83A, 0xC83B, 0xC83D, 0xC83E, 0xC83F, 0xC841, 0xC842, 0xC843, 0xC844, 0xC845, 0xC846, 0xC847, 0xC84A, 0xC84B, 0xC84E, 0xC84F, 0xC850, 0xC851, 0xC852, 0xC853, 0xC855, 0xC856, 0xC857, 0xC858, 0xC859, 0xC85A, 0xC85B, 0xC85C, 0xC85D, 0xC85E, 0xC85F, 0xC860, 0xC861, 0xC862, 0xC863, 0xC864, 0xC865, 0xC866, 0xC867, 0xC868, 0xC869, 0xC86A, 0xC86B, 0xC86C, 0xC86D, 0xC86E, 0xC86F, 0xC872, 0xC873, 0xC875, 0xC876, 0xC877, 0xC879, 0xC87B, 0xC87C, 0xC87D, 0xC87E, 0xC87F, 0xC882, 0xC884, 0xC888, 0xC889, 0xC88A, 0xC88E, 0xC88F, 0xC890, 0xC891, 0xC892, 0xC893, 0xC895, 0xC896, 0xC897, 0xC898, 0xC899, 0xC89A, 0xC89B, 0xC89C, 0xC89E, 0xC8A0, 0xC8A2, 0xC8A3, 0xC8A4, + 0xC8A5, 0xC8A6, 0xC8A7, 0xC8A9, 0xC8AA, 0xC8AB, 0xC8AC, 0xC8AD, 0xC8AE, 0xC8AF, 0xC8B0, 0xC8B1, 0xC8B2, 0xC8B3, 0xC8B4, 0xC8B5, 0xC8B6, 0xC8B7, 0xC8B8, 0xC8B9, 0xC8BA, 0xC8BB, 0xC8BE, 0xC8BF, 0xC8C0, 0xC8C1, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC8C2, 0xC8C3, 0xC8C5, 0xC8C6, 0xC8C7, 0xC8C9, 0xC8CA, 0xC8CB, 0xC8CD, 0xC8CE, 0xC8CF, 0xC8D0, 0xC8D1, 0xC8D2, 0xC8D3, 0xC8D6, 0xC8D8, 0xC8DA, 0xC8DB, 0xC8DC, 0xC8DD, 0xC8DE, 0xC8DF, 0xC8E2, 0xC8E3, 0xC8E5, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC8E6, 0xC8E7, 0xC8E8, 0xC8E9, 0xC8EA, 0xC8EB, 0xC8EC, 0xC8ED, 0xC8EE, 0xC8EF, 0xC8F0, 0xC8F1, 0xC8F2, 0xC8F3, 0xC8F4, 0xC8F6, 0xC8F7, 0xC8F8, 0xC8F9, 0xC8FA, 0xC8FB, 0xC8FE, 0xC8FF, 0xC901, 0xC902, 0xC903, 0xC907, 0xC908, 0xC909, 0xC90A, 0xC90B, 0xC90E, 0x3000, 0x3001, 0x3002, 0x00B7, 0x2025, 0x2026, 0x00A8, 0x3003, 0x00AD, 0x2015, 0x2225, 0xFF3C, 0x223C, 0x2018, 0x2019, 0x201C, 0x201D, 0x3014, 0x3015, 0x3008, 0x3009, 0x300A, 0x300B, 0x300C, 0x300D, 0x300E, 0x300F, 0x3010, 0x3011, 0x00B1, 0x00D7, 0x00F7, 0x2260, 0x2264, 0x2265, 0x221E, 0x2234, 0x00B0, 0x2032, 0x2033, 0x2103, 0x212B, 0xFFE0, 0xFFE1, 0xFFE5, 0x2642, 0x2640, 0x2220, 0x22A5, 0x2312, 0x2202, 0x2207, 0x2261, 0x2252, 0x00A7, 0x203B, 0x2606, 0x2605, 0x25CB, 0x25CF, 0x25CE, 0x25C7, 0x25C6, 0x25A1, 0x25A0, 0x25B3, 0x25B2, 0x25BD, 0x25BC, 0x2192, 0x2190, 0x2191, 0x2193, 0x2194, 0x3013, 0x226A, 0x226B, 0x221A, 0x223D, 0x221D, 0x2235, 0x222B, 0x222C, 0x2208, 0x220B, 0x2286, 0x2287, 0x2282, 0x2283, 0x222A, 0x2229, 0x2227, 0x2228, 0xFFE2, + 0xC910, 0xC912, 0xC913, 0xC914, 0xC915, 0xC916, 0xC917, 0xC919, 0xC91A, 0xC91B, 0xC91C, 0xC91D, 0xC91E, 0xC91F, 0xC920, 0xC921, 0xC922, 0xC923, 0xC924, 0xC925, 0xC926, 0xC927, 0xC928, 0xC929, 0xC92A, 0xC92B, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC92D, 0xC92E, 0xC92F, 0xC930, 0xC931, 0xC932, 0xC933, 0xC935, 0xC936, 0xC937, 0xC938, 0xC939, 0xC93A, 0xC93B, 0xC93C, 0xC93D, 0xC93E, 0xC93F, 0xC940, 0xC941, 0xC942, 0xC943, 0xC944, 0xC945, 0xC946, 0xC947, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC948, 0xC949, 0xC94A, 0xC94B, 0xC94C, 0xC94D, 0xC94E, 0xC94F, 0xC952, 0xC953, 0xC955, 0xC956, 0xC957, 0xC959, 0xC95A, 0xC95B, 0xC95C, 0xC95D, 0xC95E, 0xC95F, 0xC962, 0xC964, 0xC965, 0xC966, 0xC967, 0xC968, 0xC969, 0xC96A, 0xC96B, 0xC96D, 0xC96E, 0xC96F, 0x21D2, 0x21D4, 0x2200, 0x2203, 0x00B4, 0xFF5E, 0x02C7, 0x02D8, 0x02DD, 0x02DA, 0x02D9, 0x00B8, 0x02DB, 0x00A1, 0x00BF, 0x02D0, 0x222E, 0x2211, 0x220F, 0x00A4, 0x2109, 0x2030, 0x25C1, 0x25C0, 0x25B7, 0x25B6, 0x2664, 0x2660, 0x2661, 0x2665, 0x2667, 0x2663, 0x2299, 0x25C8, 0x25A3, 0x25D0, 0x25D1, 0x2592, 0x25A4, 0x25A5, 0x25A8, 0x25A7, 0x25A6, 0x25A9, 0x2668, 0x260F, 0x260E, 0x261C, 0x261E, 0x00B6, 0x2020, 0x2021, 0x2195, 0x2197, 0x2199, 0x2196, 0x2198, 0x266D, 0x2669, 0x266A, 0x266C, 0x327F, 0x321C, 0x2116, 0x33C7, 0x2122, 0x33C2, 0x33D8, 0x2121, 0x20AC, 0x00AE, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0xC971, 0xC972, 0xC973, 0xC975, 0xC976, 0xC977, 0xC978, 0xC979, 0xC97A, 0xC97B, 0xC97D, 0xC97E, 0xC97F, 0xC980, 0xC981, 0xC982, 0xC983, 0xC984, 0xC985, 0xC986, 0xC987, 0xC98A, 0xC98B, 0xC98D, 0xC98E, 0xC98F, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC991, 0xC992, 0xC993, 0xC994, 0xC995, 0xC996, 0xC997, 0xC99A, 0xC99C, 0xC99E, 0xC99F, 0xC9A0, 0xC9A1, 0xC9A2, 0xC9A3, 0xC9A4, 0xC9A5, 0xC9A6, 0xC9A7, 0xC9A8, 0xC9A9, 0xC9AA, 0xC9AB, 0xC9AC, 0xC9AD, 0xC9AE, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xC9AF, 0xC9B0, 0xC9B1, 0xC9B2, 0xC9B3, 0xC9B4, 0xC9B5, 0xC9B6, 0xC9B7, 0xC9B8, 0xC9B9, 0xC9BA, 0xC9BB, 0xC9BC, 0xC9BD, 0xC9BE, 0xC9BF, 0xC9C2, 0xC9C3, 0xC9C5, 0xC9C6, 0xC9C9, 0xC9CB, 0xC9CC, 0xC9CD, 0xC9CE, 0xC9CF, 0xC9D2, 0xC9D4, 0xC9D7, 0xC9D8, 0xC9DB, 0xFF01, 0xFF02, 0xFF03, 0xFF04, 0xFF05, 0xFF06, 0xFF07, 0xFF08, 0xFF09, 0xFF0A, 0xFF0B, 0xFF0C, 0xFF0D, 0xFF0E, 0xFF0F, 0xFF10, 0xFF11, 0xFF12, 0xFF13, 0xFF14, 0xFF15, 0xFF16, 0xFF17, 0xFF18, 0xFF19, 0xFF1A, 0xFF1B, 0xFF1C, 0xFF1D, 0xFF1E, 0xFF1F, 0xFF20, 0xFF21, 0xFF22, 0xFF23, 0xFF24, 0xFF25, 0xFF26, 0xFF27, 0xFF28, 0xFF29, 0xFF2A, 0xFF2B, 0xFF2C, 0xFF2D, 0xFF2E, 0xFF2F, 0xFF30, 0xFF31, 0xFF32, 0xFF33, 0xFF34, 0xFF35, 0xFF36, 0xFF37, 0xFF38, 0xFF39, 0xFF3A, 0xFF3B, 0xFFE6, 0xFF3D, 0xFF3E, 0xFF3F, 0xFF40, 0xFF41, 0xFF42, 0xFF43, 0xFF44, 0xFF45, 0xFF46, 0xFF47, 0xFF48, 0xFF49, 0xFF4A, 0xFF4B, 0xFF4C, 0xFF4D, 0xFF4E, 0xFF4F, 0xFF50, 0xFF51, 0xFF52, 0xFF53, 0xFF54, 0xFF55, 0xFF56, 0xFF57, 0xFF58, 0xFF59, 0xFF5A, 0xFF5B, 0xFF5C, 0xFF5D, 0xFFE3, + 0xC9DE, 0xC9DF, 0xC9E1, 0xC9E3, 0xC9E5, 0xC9E6, 0xC9E8, 0xC9E9, 0xC9EA, 0xC9EB, 0xC9EE, 0xC9F2, 0xC9F3, 0xC9F4, 0xC9F5, 0xC9F6, 0xC9F7, 0xC9FA, 0xC9FB, 0xC9FD, 0xC9FE, 0xC9FF, 0xCA01, 0xCA02, 0xCA03, 0xCA04, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCA05, 0xCA06, 0xCA07, 0xCA0A, 0xCA0E, 0xCA0F, 0xCA10, 0xCA11, 0xCA12, 0xCA13, 0xCA15, 0xCA16, 0xCA17, 0xCA19, 0xCA1A, 0xCA1B, 0xCA1C, 0xCA1D, 0xCA1E, 0xCA1F, 0xCA20, 0xCA21, 0xCA22, 0xCA23, 0xCA24, 0xCA25, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCA26, 0xCA27, 0xCA28, 0xCA2A, 0xCA2B, 0xCA2C, 0xCA2D, 0xCA2E, 0xCA2F, 0xCA30, 0xCA31, 0xCA32, 0xCA33, 0xCA34, 0xCA35, 0xCA36, 0xCA37, 0xCA38, 0xCA39, 0xCA3A, 0xCA3B, 0xCA3C, 0xCA3D, 0xCA3E, 0xCA3F, 0xCA40, 0xCA41, 0xCA42, 0xCA43, 0xCA44, 0xCA45, 0xCA46, 0x3131, 0x3132, 0x3133, 0x3134, 0x3135, 0x3136, 0x3137, 0x3138, 0x3139, 0x313A, 0x313B, 0x313C, 0x313D, 0x313E, 0x313F, 0x3140, 0x3141, 0x3142, 0x3143, 0x3144, 0x3145, 0x3146, 0x3147, 0x3148, 0x3149, 0x314A, 0x314B, 0x314C, 0x314D, 0x314E, 0x314F, 0x3150, 0x3151, 0x3152, 0x3153, 0x3154, 0x3155, 0x3156, 0x3157, 0x3158, 0x3159, 0x315A, 0x315B, 0x315C, 0x315D, 0x315E, 0x315F, 0x3160, 0x3161, 0x3162, 0x3163, 0x3164, 0x3165, 0x3166, 0x3167, 0x3168, 0x3169, 0x316A, 0x316B, 0x316C, 0x316D, 0x316E, 0x316F, 0x3170, 0x3171, 0x3172, 0x3173, 0x3174, 0x3175, 0x3176, 0x3177, 0x3178, 0x3179, 0x317A, 0x317B, 0x317C, 0x317D, 0x317E, 0x317F, 0x3180, 0x3181, 0x3182, 0x3183, 0x3184, 0x3185, 0x3186, 0x3187, 0x3188, 0x3189, 0x318A, 0x318B, 0x318C, 0x318D, 0x318E, + 0xCA47, 0xCA48, 0xCA49, 0xCA4A, 0xCA4B, 0xCA4E, 0xCA4F, 0xCA51, 0xCA52, 0xCA53, 0xCA55, 0xCA56, 0xCA57, 0xCA58, 0xCA59, 0xCA5A, 0xCA5B, 0xCA5E, 0xCA62, 0xCA63, 0xCA64, 0xCA65, 0xCA66, 0xCA67, 0xCA69, 0xCA6A, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCA6B, 0xCA6C, 0xCA6D, 0xCA6E, 0xCA6F, 0xCA70, 0xCA71, 0xCA72, 0xCA73, 0xCA74, 0xCA75, 0xCA76, 0xCA77, 0xCA78, 0xCA79, 0xCA7A, 0xCA7B, 0xCA7C, 0xCA7E, 0xCA7F, 0xCA80, 0xCA81, 0xCA82, 0xCA83, 0xCA85, 0xCA86, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCA87, 0xCA88, 0xCA89, 0xCA8A, 0xCA8B, 0xCA8C, 0xCA8D, 0xCA8E, 0xCA8F, 0xCA90, 0xCA91, 0xCA92, 0xCA93, 0xCA94, 0xCA95, 0xCA96, 0xCA97, 0xCA99, 0xCA9A, 0xCA9B, 0xCA9C, 0xCA9D, 0xCA9E, 0xCA9F, 0xCAA0, 0xCAA1, 0xCAA2, 0xCAA3, 0xCAA4, 0xCAA5, 0xCAA6, 0xCAA7, 0x2170, 0x2171, 0x2172, 0x2173, 0x2174, 0x2175, 0x2176, 0x2177, 0x2178, 0x2179, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x2160, 0x2161, 0x2162, 0x2163, 0x2164, 0x2165, 0x2166, 0x2167, 0x2168, 0x2169, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, 0x03A0, 0x03A1, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, 0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF, 0x03C0, 0x03C1, 0x03C3, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, 0x03C9, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0xCAA8, 0xCAA9, 0xCAAA, 0xCAAB, 0xCAAC, 0xCAAD, 0xCAAE, 0xCAAF, 0xCAB0, 0xCAB1, 0xCAB2, 0xCAB3, 0xCAB4, 0xCAB5, 0xCAB6, 0xCAB7, 0xCAB8, 0xCAB9, 0xCABA, 0xCABB, 0xCABE, 0xCABF, 0xCAC1, 0xCAC2, 0xCAC3, 0xCAC5, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCAC6, 0xCAC7, 0xCAC8, 0xCAC9, 0xCACA, 0xCACB, 0xCACE, 0xCAD0, 0xCAD2, 0xCAD4, 0xCAD5, 0xCAD6, 0xCAD7, 0xCADA, 0xCADB, 0xCADC, 0xCADD, 0xCADE, 0xCADF, 0xCAE1, 0xCAE2, 0xCAE3, 0xCAE4, 0xCAE5, 0xCAE6, 0xCAE7, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCAE8, 0xCAE9, 0xCAEA, 0xCAEB, 0xCAED, 0xCAEE, 0xCAEF, 0xCAF0, 0xCAF1, 0xCAF2, 0xCAF3, 0xCAF5, 0xCAF6, 0xCAF7, 0xCAF8, 0xCAF9, 0xCAFA, 0xCAFB, 0xCAFC, 0xCAFD, 0xCAFE, 0xCAFF, 0xCB00, 0xCB01, 0xCB02, 0xCB03, 0xCB04, 0xCB05, 0xCB06, 0xCB07, 0xCB09, 0xCB0A, 0x2500, 0x2502, 0x250C, 0x2510, 0x2518, 0x2514, 0x251C, 0x252C, 0x2524, 0x2534, 0x253C, 0x2501, 0x2503, 0x250F, 0x2513, 0x251B, 0x2517, 0x2523, 0x2533, 0x252B, 0x253B, 0x254B, 0x2520, 0x252F, 0x2528, 0x2537, 0x253F, 0x251D, 0x2530, 0x2525, 0x2538, 0x2542, 0x2512, 0x2511, 0x251A, 0x2519, 0x2516, 0x2515, 0x250E, 0x250D, 0x251E, 0x251F, 0x2521, 0x2522, 0x2526, 0x2527, 0x2529, 0x252A, 0x252D, 0x252E, 0x2531, 0x2532, 0x2535, 0x2536, 0x2539, 0x253A, 0x253D, 0x253E, 0x2540, 0x2541, 0x2543, 0x2544, 0x2545, 0x2546, 0x2547, 0x2548, 0x2549, 0x254A, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0xCB0B, 0xCB0C, 0xCB0D, 0xCB0E, 0xCB0F, 0xCB11, 0xCB12, 0xCB13, 0xCB15, 0xCB16, 0xCB17, 0xCB19, 0xCB1A, 0xCB1B, 0xCB1C, 0xCB1D, 0xCB1E, 0xCB1F, 0xCB22, 0xCB23, 0xCB24, 0xCB25, 0xCB26, 0xCB27, 0xCB28, 0xCB29, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCB2A, 0xCB2B, 0xCB2C, 0xCB2D, 0xCB2E, 0xCB2F, 0xCB30, 0xCB31, 0xCB32, 0xCB33, 0xCB34, 0xCB35, 0xCB36, 0xCB37, 0xCB38, 0xCB39, 0xCB3A, 0xCB3B, 0xCB3C, 0xCB3D, 0xCB3E, 0xCB3F, 0xCB40, 0xCB42, 0xCB43, 0xCB44, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCB45, 0xCB46, 0xCB47, 0xCB4A, 0xCB4B, 0xCB4D, 0xCB4E, 0xCB4F, 0xCB51, 0xCB52, 0xCB53, 0xCB54, 0xCB55, 0xCB56, 0xCB57, 0xCB5A, 0xCB5B, 0xCB5C, 0xCB5E, 0xCB5F, 0xCB60, 0xCB61, 0xCB62, 0xCB63, 0xCB65, 0xCB66, 0xCB67, 0xCB68, 0xCB69, 0xCB6A, 0xCB6B, 0xCB6C, 0x3395, 0x3396, 0x3397, 0x2113, 0x3398, 0x33C4, 0x33A3, 0x33A4, 0x33A5, 0x33A6, 0x3399, 0x339A, 0x339B, 0x339C, 0x339D, 0x339E, 0x339F, 0x33A0, 0x33A1, 0x33A2, 0x33CA, 0x338D, 0x338E, 0x338F, 0x33CF, 0x3388, 0x3389, 0x33C8, 0x33A7, 0x33A8, 0x33B0, 0x33B1, 0x33B2, 0x33B3, 0x33B4, 0x33B5, 0x33B6, 0x33B7, 0x33B8, 0x33B9, 0x3380, 0x3381, 0x3382, 0x3383, 0x3384, 0x33BA, 0x33BB, 0x33BC, 0x33BD, 0x33BE, 0x33BF, 0x3390, 0x3391, 0x3392, 0x3393, 0x3394, 0x2126, 0x33C0, 0x33C1, 0x338A, 0x338B, 0x338C, 0x33D6, 0x33C5, 0x33AD, 0x33AE, 0x33AF, 0x33DB, 0x33A9, 0x33AA, 0x33AB, 0x33AC, 0x33DD, 0x33D0, 0x33D3, 0x33C3, 0x33C9, 0x33DC, 0x33C6, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0xCB6D, 0xCB6E, 0xCB6F, 0xCB70, 0xCB71, 0xCB72, 0xCB73, 0xCB74, 0xCB75, 0xCB76, 0xCB77, 0xCB7A, 0xCB7B, 0xCB7C, 0xCB7D, 0xCB7E, 0xCB7F, 0xCB80, 0xCB81, 0xCB82, 0xCB83, 0xCB84, 0xCB85, 0xCB86, 0xCB87, 0xCB88, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCB89, 0xCB8A, 0xCB8B, 0xCB8C, 0xCB8D, 0xCB8E, 0xCB8F, 0xCB90, 0xCB91, 0xCB92, 0xCB93, 0xCB94, 0xCB95, 0xCB96, 0xCB97, 0xCB98, 0xCB99, 0xCB9A, 0xCB9B, 0xCB9D, 0xCB9E, 0xCB9F, 0xCBA0, 0xCBA1, 0xCBA2, 0xCBA3, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCBA4, 0xCBA5, 0xCBA6, 0xCBA7, 0xCBA8, 0xCBA9, 0xCBAA, 0xCBAB, 0xCBAC, 0xCBAD, 0xCBAE, 0xCBAF, 0xCBB0, 0xCBB1, 0xCBB2, 0xCBB3, 0xCBB4, 0xCBB5, 0xCBB6, 0xCBB7, 0xCBB9, 0xCBBA, 0xCBBB, 0xCBBC, 0xCBBD, 0xCBBE, 0xCBBF, 0xCBC0, 0xCBC1, 0xCBC2, 0xCBC3, 0xCBC4, 0x00C6, 0x00D0, 0x00AA, 0x0126, 0x0000, 0x0132, 0x0000, 0x013F, 0x0141, 0x00D8, 0x0152, 0x00BA, 0x00DE, 0x0166, 0x014A, 0x0000, 0x3260, 0x3261, 0x3262, 0x3263, 0x3264, 0x3265, 0x3266, 0x3267, 0x3268, 0x3269, 0x326A, 0x326B, 0x326C, 0x326D, 0x326E, 0x326F, 0x3270, 0x3271, 0x3272, 0x3273, 0x3274, 0x3275, 0x3276, 0x3277, 0x3278, 0x3279, 0x327A, 0x327B, 0x24D0, 0x24D1, 0x24D2, 0x24D3, 0x24D4, 0x24D5, 0x24D6, 0x24D7, 0x24D8, 0x24D9, 0x24DA, 0x24DB, 0x24DC, 0x24DD, 0x24DE, 0x24DF, 0x24E0, 0x24E1, 0x24E2, 0x24E3, 0x24E4, 0x24E5, 0x24E6, 0x24E7, 0x24E8, 0x24E9, 0x2460, 0x2461, 0x2462, 0x2463, 0x2464, 0x2465, 0x2466, 0x2467, 0x2468, 0x2469, 0x246A, 0x246B, 0x246C, 0x246D, 0x246E, 0x00BD, 0x2153, 0x2154, 0x00BC, 0x00BE, 0x215B, 0x215C, 0x215D, 0x215E, + 0xCBC5, 0xCBC6, 0xCBC7, 0xCBC8, 0xCBC9, 0xCBCA, 0xCBCB, 0xCBCC, 0xCBCD, 0xCBCE, 0xCBCF, 0xCBD0, 0xCBD1, 0xCBD2, 0xCBD3, 0xCBD5, 0xCBD6, 0xCBD7, 0xCBD8, 0xCBD9, 0xCBDA, 0xCBDB, 0xCBDC, 0xCBDD, 0xCBDE, 0xCBDF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCBE0, 0xCBE1, 0xCBE2, 0xCBE3, 0xCBE5, 0xCBE6, 0xCBE8, 0xCBEA, 0xCBEB, 0xCBEC, 0xCBED, 0xCBEE, 0xCBEF, 0xCBF0, 0xCBF1, 0xCBF2, 0xCBF3, 0xCBF4, 0xCBF5, 0xCBF6, 0xCBF7, 0xCBF8, 0xCBF9, 0xCBFA, 0xCBFB, 0xCBFC, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCBFD, 0xCBFE, 0xCBFF, 0xCC00, 0xCC01, 0xCC02, 0xCC03, 0xCC04, 0xCC05, 0xCC06, 0xCC07, 0xCC08, 0xCC09, 0xCC0A, 0xCC0B, 0xCC0E, 0xCC0F, 0xCC11, 0xCC12, 0xCC13, 0xCC15, 0xCC16, 0xCC17, 0xCC18, 0xCC19, 0xCC1A, 0xCC1B, 0xCC1E, 0xCC1F, 0xCC20, 0xCC23, 0xCC24, 0x00E6, 0x0111, 0x00F0, 0x0127, 0x0131, 0x0133, 0x0138, 0x0140, 0x0142, 0x00F8, 0x0153, 0x00DF, 0x00FE, 0x0167, 0x014B, 0x0149, 0x3200, 0x3201, 0x3202, 0x3203, 0x3204, 0x3205, 0x3206, 0x3207, 0x3208, 0x3209, 0x320A, 0x320B, 0x320C, 0x320D, 0x320E, 0x320F, 0x3210, 0x3211, 0x3212, 0x3213, 0x3214, 0x3215, 0x3216, 0x3217, 0x3218, 0x3219, 0x321A, 0x321B, 0x249C, 0x249D, 0x249E, 0x249F, 0x24A0, 0x24A1, 0x24A2, 0x24A3, 0x24A4, 0x24A5, 0x24A6, 0x24A7, 0x24A8, 0x24A9, 0x24AA, 0x24AB, 0x24AC, 0x24AD, 0x24AE, 0x24AF, 0x24B0, 0x24B1, 0x24B2, 0x24B3, 0x24B4, 0x24B5, 0x2474, 0x2475, 0x2476, 0x2477, 0x2478, 0x2479, 0x247A, 0x247B, 0x247C, 0x247D, 0x247E, 0x247F, 0x2480, 0x2481, 0x2482, 0x00B9, 0x00B2, 0x00B3, 0x2074, 0x207F, 0x2081, 0x2082, 0x2083, 0x2084, + 0xCC25, 0xCC26, 0xCC2A, 0xCC2B, 0xCC2D, 0xCC2F, 0xCC31, 0xCC32, 0xCC33, 0xCC34, 0xCC35, 0xCC36, 0xCC37, 0xCC3A, 0xCC3F, 0xCC40, 0xCC41, 0xCC42, 0xCC43, 0xCC46, 0xCC47, 0xCC49, 0xCC4A, 0xCC4B, 0xCC4D, 0xCC4E, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCC4F, 0xCC50, 0xCC51, 0xCC52, 0xCC53, 0xCC56, 0xCC5A, 0xCC5B, 0xCC5C, 0xCC5D, 0xCC5E, 0xCC5F, 0xCC61, 0xCC62, 0xCC63, 0xCC65, 0xCC67, 0xCC69, 0xCC6A, 0xCC6B, 0xCC6C, 0xCC6D, 0xCC6E, 0xCC6F, 0xCC71, 0xCC72, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCC73, 0xCC74, 0xCC76, 0xCC77, 0xCC78, 0xCC79, 0xCC7A, 0xCC7B, 0xCC7C, 0xCC7D, 0xCC7E, 0xCC7F, 0xCC80, 0xCC81, 0xCC82, 0xCC83, 0xCC84, 0xCC85, 0xCC86, 0xCC87, 0xCC88, 0xCC89, 0xCC8A, 0xCC8B, 0xCC8C, 0xCC8D, 0xCC8E, 0xCC8F, 0xCC90, 0xCC91, 0xCC92, 0xCC93, 0x3041, 0x3042, 0x3043, 0x3044, 0x3045, 0x3046, 0x3047, 0x3048, 0x3049, 0x304A, 0x304B, 0x304C, 0x304D, 0x304E, 0x304F, 0x3050, 0x3051, 0x3052, 0x3053, 0x3054, 0x3055, 0x3056, 0x3057, 0x3058, 0x3059, 0x305A, 0x305B, 0x305C, 0x305D, 0x305E, 0x305F, 0x3060, 0x3061, 0x3062, 0x3063, 0x3064, 0x3065, 0x3066, 0x3067, 0x3068, 0x3069, 0x306A, 0x306B, 0x306C, 0x306D, 0x306E, 0x306F, 0x3070, 0x3071, 0x3072, 0x3073, 0x3074, 0x3075, 0x3076, 0x3077, 0x3078, 0x3079, 0x307A, 0x307B, 0x307C, 0x307D, 0x307E, 0x307F, 0x3080, 0x3081, 0x3082, 0x3083, 0x3084, 0x3085, 0x3086, 0x3087, 0x3088, 0x3089, 0x308A, 0x308B, 0x308C, 0x308D, 0x308E, 0x308F, 0x3090, 0x3091, 0x3092, 0x3093, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0xCC94, 0xCC95, 0xCC96, 0xCC97, 0xCC9A, 0xCC9B, 0xCC9D, 0xCC9E, 0xCC9F, 0xCCA1, 0xCCA2, 0xCCA3, 0xCCA4, 0xCCA5, 0xCCA6, 0xCCA7, 0xCCAA, 0xCCAE, 0xCCAF, 0xCCB0, 0xCCB1, 0xCCB2, 0xCCB3, 0xCCB6, 0xCCB7, 0xCCB9, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCCBA, 0xCCBB, 0xCCBD, 0xCCBE, 0xCCBF, 0xCCC0, 0xCCC1, 0xCCC2, 0xCCC3, 0xCCC6, 0xCCC8, 0xCCCA, 0xCCCB, 0xCCCC, 0xCCCD, 0xCCCE, 0xCCCF, 0xCCD1, 0xCCD2, 0xCCD3, 0xCCD5, 0xCCD6, 0xCCD7, 0xCCD8, 0xCCD9, 0xCCDA, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCCDB, 0xCCDC, 0xCCDD, 0xCCDE, 0xCCDF, 0xCCE0, 0xCCE1, 0xCCE2, 0xCCE3, 0xCCE5, 0xCCE6, 0xCCE7, 0xCCE8, 0xCCE9, 0xCCEA, 0xCCEB, 0xCCED, 0xCCEE, 0xCCEF, 0xCCF1, 0xCCF2, 0xCCF3, 0xCCF4, 0xCCF5, 0xCCF6, 0xCCF7, 0xCCF8, 0xCCF9, 0xCCFA, 0xCCFB, 0xCCFC, 0xCCFD, 0x30A1, 0x30A2, 0x30A3, 0x30A4, 0x30A5, 0x30A6, 0x30A7, 0x30A8, 0x30A9, 0x30AA, 0x30AB, 0x30AC, 0x30AD, 0x30AE, 0x30AF, 0x30B0, 0x30B1, 0x30B2, 0x30B3, 0x30B4, 0x30B5, 0x30B6, 0x30B7, 0x30B8, 0x30B9, 0x30BA, 0x30BB, 0x30BC, 0x30BD, 0x30BE, 0x30BF, 0x30C0, 0x30C1, 0x30C2, 0x30C3, 0x30C4, 0x30C5, 0x30C6, 0x30C7, 0x30C8, 0x30C9, 0x30CA, 0x30CB, 0x30CC, 0x30CD, 0x30CE, 0x30CF, 0x30D0, 0x30D1, 0x30D2, 0x30D3, 0x30D4, 0x30D5, 0x30D6, 0x30D7, 0x30D8, 0x30D9, 0x30DA, 0x30DB, 0x30DC, 0x30DD, 0x30DE, 0x30DF, 0x30E0, 0x30E1, 0x30E2, 0x30E3, 0x30E4, 0x30E5, 0x30E6, 0x30E7, 0x30E8, 0x30E9, 0x30EA, 0x30EB, 0x30EC, 0x30ED, 0x30EE, 0x30EF, 0x30F0, 0x30F1, 0x30F2, 0x30F3, 0x30F4, 0x30F5, 0x30F6, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0xCCFE, 0xCCFF, 0xCD00, 0xCD02, 0xCD03, 0xCD04, 0xCD05, 0xCD06, 0xCD07, 0xCD0A, 0xCD0B, 0xCD0D, 0xCD0E, 0xCD0F, 0xCD11, 0xCD12, 0xCD13, 0xCD14, 0xCD15, 0xCD16, 0xCD17, 0xCD1A, 0xCD1C, 0xCD1E, 0xCD1F, 0xCD20, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCD21, 0xCD22, 0xCD23, 0xCD25, 0xCD26, 0xCD27, 0xCD29, 0xCD2A, 0xCD2B, 0xCD2D, 0xCD2E, 0xCD2F, 0xCD30, 0xCD31, 0xCD32, 0xCD33, 0xCD34, 0xCD35, 0xCD36, 0xCD37, 0xCD38, 0xCD3A, 0xCD3B, 0xCD3C, 0xCD3D, 0xCD3E, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCD3F, 0xCD40, 0xCD41, 0xCD42, 0xCD43, 0xCD44, 0xCD45, 0xCD46, 0xCD47, 0xCD48, 0xCD49, 0xCD4A, 0xCD4B, 0xCD4C, 0xCD4D, 0xCD4E, 0xCD4F, 0xCD50, 0xCD51, 0xCD52, 0xCD53, 0xCD54, 0xCD55, 0xCD56, 0xCD57, 0xCD58, 0xCD59, 0xCD5A, 0xCD5B, 0xCD5D, 0xCD5E, 0xCD5F, 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0401, 0x0416, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0451, 0x0436, 0x0437, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0xCD61, 0xCD62, 0xCD63, 0xCD65, 0xCD66, 0xCD67, 0xCD68, 0xCD69, 0xCD6A, 0xCD6B, 0xCD6E, 0xCD70, 0xCD72, 0xCD73, 0xCD74, 0xCD75, 0xCD76, 0xCD77, 0xCD79, 0xCD7A, 0xCD7B, 0xCD7C, 0xCD7D, 0xCD7E, 0xCD7F, 0xCD80, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCD81, 0xCD82, 0xCD83, 0xCD84, 0xCD85, 0xCD86, 0xCD87, 0xCD89, 0xCD8A, 0xCD8B, 0xCD8C, 0xCD8D, 0xCD8E, 0xCD8F, 0xCD90, 0xCD91, 0xCD92, 0xCD93, 0xCD96, 0xCD97, 0xCD99, 0xCD9A, 0xCD9B, 0xCD9D, 0xCD9E, 0xCD9F, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCDA0, 0xCDA1, 0xCDA2, 0xCDA3, 0xCDA6, 0xCDA8, 0xCDAA, 0xCDAB, 0xCDAC, 0xCDAD, 0xCDAE, 0xCDAF, 0xCDB1, 0xCDB2, 0xCDB3, 0xCDB4, 0xCDB5, 0xCDB6, 0xCDB7, 0xCDB8, 0xCDB9, 0xCDBA, 0xCDBB, 0xCDBC, 0xCDBD, 0xCDBE, 0xCDBF, 0xCDC0, 0xCDC1, 0xCDC2, 0xCDC3, 0xCDC5, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0xCDC6, 0xCDC7, 0xCDC8, 0xCDC9, 0xCDCA, 0xCDCB, 0xCDCD, 0xCDCE, 0xCDCF, 0xCDD1, 0xCDD2, 0xCDD3, 0xCDD4, 0xCDD5, 0xCDD6, 0xCDD7, 0xCDD8, 0xCDD9, 0xCDDA, 0xCDDB, 0xCDDC, 0xCDDD, 0xCDDE, 0xCDDF, 0xCDE0, 0xCDE1, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCDE2, 0xCDE3, 0xCDE4, 0xCDE5, 0xCDE6, 0xCDE7, 0xCDE9, 0xCDEA, 0xCDEB, 0xCDED, 0xCDEE, 0xCDEF, 0xCDF1, 0xCDF2, 0xCDF3, 0xCDF4, 0xCDF5, 0xCDF6, 0xCDF7, 0xCDFA, 0xCDFC, 0xCDFE, 0xCDFF, 0xCE00, 0xCE01, 0xCE02, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCE03, 0xCE05, 0xCE06, 0xCE07, 0xCE09, 0xCE0A, 0xCE0B, 0xCE0D, 0xCE0E, 0xCE0F, 0xCE10, 0xCE11, 0xCE12, 0xCE13, 0xCE15, 0xCE16, 0xCE17, 0xCE18, 0xCE1A, 0xCE1B, 0xCE1C, 0xCE1D, 0xCE1E, 0xCE1F, 0xCE22, 0xCE23, 0xCE25, 0xCE26, 0xCE27, 0xCE29, 0xCE2A, 0xCE2B, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0xCE2C, 0xCE2D, 0xCE2E, 0xCE2F, 0xCE32, 0xCE34, 0xCE36, 0xCE37, 0xCE38, 0xCE39, 0xCE3A, 0xCE3B, 0xCE3C, 0xCE3D, 0xCE3E, 0xCE3F, 0xCE40, 0xCE41, 0xCE42, 0xCE43, 0xCE44, 0xCE45, 0xCE46, 0xCE47, 0xCE48, 0xCE49, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCE4A, 0xCE4B, 0xCE4C, 0xCE4D, 0xCE4E, 0xCE4F, 0xCE50, 0xCE51, 0xCE52, 0xCE53, 0xCE54, 0xCE55, 0xCE56, 0xCE57, 0xCE5A, 0xCE5B, 0xCE5D, 0xCE5E, 0xCE62, 0xCE63, 0xCE64, 0xCE65, 0xCE66, 0xCE67, 0xCE6A, 0xCE6C, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCE6E, 0xCE6F, 0xCE70, 0xCE71, 0xCE72, 0xCE73, 0xCE76, 0xCE77, 0xCE79, 0xCE7A, 0xCE7B, 0xCE7D, 0xCE7E, 0xCE7F, 0xCE80, 0xCE81, 0xCE82, 0xCE83, 0xCE86, 0xCE88, 0xCE8A, 0xCE8B, 0xCE8C, 0xCE8D, 0xCE8E, 0xCE8F, 0xCE92, 0xCE93, 0xCE95, 0xCE96, 0xCE97, 0xCE99, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0xCE9A, 0xCE9B, 0xCE9C, 0xCE9D, 0xCE9E, 0xCE9F, 0xCEA2, 0xCEA6, 0xCEA7, 0xCEA8, 0xCEA9, 0xCEAA, 0xCEAB, 0xCEAE, 0xCEAF, 0xCEB0, 0xCEB1, 0xCEB2, 0xCEB3, 0xCEB4, 0xCEB5, 0xCEB6, 0xCEB7, 0xCEB8, 0xCEB9, 0xCEBA, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCEBB, 0xCEBC, 0xCEBD, 0xCEBE, 0xCEBF, 0xCEC0, 0xCEC2, 0xCEC3, 0xCEC4, 0xCEC5, 0xCEC6, 0xCEC7, 0xCEC8, 0xCEC9, 0xCECA, 0xCECB, 0xCECC, 0xCECD, 0xCECE, 0xCECF, 0xCED0, 0xCED1, 0xCED2, 0xCED3, 0xCED4, 0xCED5, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCED6, 0xCED7, 0xCED8, 0xCED9, 0xCEDA, 0xCEDB, 0xCEDC, 0xCEDD, 0xCEDE, 0xCEDF, 0xCEE0, 0xCEE1, 0xCEE2, 0xCEE3, 0xCEE6, 0xCEE7, 0xCEE9, 0xCEEA, 0xCEED, 0xCEEE, 0xCEEF, 0xCEF0, 0xCEF1, 0xCEF2, 0xCEF3, 0xCEF6, 0xCEFA, 0xCEFB, 0xCEFC, 0xCEFD, 0xCEFE, 0xCEFF, 0xAC00, 0xAC01, 0xAC04, 0xAC07, 0xAC08, 0xAC09, 0xAC0A, 0xAC10, 0xAC11, 0xAC12, 0xAC13, 0xAC14, 0xAC15, 0xAC16, 0xAC17, 0xAC19, 0xAC1A, 0xAC1B, 0xAC1C, 0xAC1D, 0xAC20, 0xAC24, 0xAC2C, 0xAC2D, 0xAC2F, 0xAC30, 0xAC31, 0xAC38, 0xAC39, 0xAC3C, 0xAC40, 0xAC4B, 0xAC4D, 0xAC54, 0xAC58, 0xAC5C, 0xAC70, 0xAC71, 0xAC74, 0xAC77, 0xAC78, 0xAC7A, 0xAC80, 0xAC81, 0xAC83, 0xAC84, 0xAC85, 0xAC86, 0xAC89, 0xAC8A, 0xAC8B, 0xAC8C, 0xAC90, 0xAC94, 0xAC9C, 0xAC9D, 0xAC9F, 0xACA0, 0xACA1, 0xACA8, 0xACA9, 0xACAA, 0xACAC, 0xACAF, 0xACB0, 0xACB8, 0xACB9, 0xACBB, 0xACBC, 0xACBD, 0xACC1, 0xACC4, 0xACC8, 0xACCC, 0xACD5, 0xACD7, 0xACE0, 0xACE1, 0xACE4, 0xACE7, 0xACE8, 0xACEA, 0xACEC, 0xACEF, 0xACF0, 0xACF1, 0xACF3, 0xACF5, 0xACF6, 0xACFC, 0xACFD, 0xAD00, 0xAD04, 0xAD06, + 0xCF02, 0xCF03, 0xCF05, 0xCF06, 0xCF07, 0xCF09, 0xCF0A, 0xCF0B, 0xCF0C, 0xCF0D, 0xCF0E, 0xCF0F, 0xCF12, 0xCF14, 0xCF16, 0xCF17, 0xCF18, 0xCF19, 0xCF1A, 0xCF1B, 0xCF1D, 0xCF1E, 0xCF1F, 0xCF21, 0xCF22, 0xCF23, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCF25, 0xCF26, 0xCF27, 0xCF28, 0xCF29, 0xCF2A, 0xCF2B, 0xCF2E, 0xCF32, 0xCF33, 0xCF34, 0xCF35, 0xCF36, 0xCF37, 0xCF39, 0xCF3A, 0xCF3B, 0xCF3C, 0xCF3D, 0xCF3E, 0xCF3F, 0xCF40, 0xCF41, 0xCF42, 0xCF43, 0xCF44, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCF45, 0xCF46, 0xCF47, 0xCF48, 0xCF49, 0xCF4A, 0xCF4B, 0xCF4C, 0xCF4D, 0xCF4E, 0xCF4F, 0xCF50, 0xCF51, 0xCF52, 0xCF53, 0xCF56, 0xCF57, 0xCF59, 0xCF5A, 0xCF5B, 0xCF5D, 0xCF5E, 0xCF5F, 0xCF60, 0xCF61, 0xCF62, 0xCF63, 0xCF66, 0xCF68, 0xCF6A, 0xCF6B, 0xCF6C, 0xAD0C, 0xAD0D, 0xAD0F, 0xAD11, 0xAD18, 0xAD1C, 0xAD20, 0xAD29, 0xAD2C, 0xAD2D, 0xAD34, 0xAD35, 0xAD38, 0xAD3C, 0xAD44, 0xAD45, 0xAD47, 0xAD49, 0xAD50, 0xAD54, 0xAD58, 0xAD61, 0xAD63, 0xAD6C, 0xAD6D, 0xAD70, 0xAD73, 0xAD74, 0xAD75, 0xAD76, 0xAD7B, 0xAD7C, 0xAD7D, 0xAD7F, 0xAD81, 0xAD82, 0xAD88, 0xAD89, 0xAD8C, 0xAD90, 0xAD9C, 0xAD9D, 0xADA4, 0xADB7, 0xADC0, 0xADC1, 0xADC4, 0xADC8, 0xADD0, 0xADD1, 0xADD3, 0xADDC, 0xADE0, 0xADE4, 0xADF8, 0xADF9, 0xADFC, 0xADFF, 0xAE00, 0xAE01, 0xAE08, 0xAE09, 0xAE0B, 0xAE0D, 0xAE14, 0xAE30, 0xAE31, 0xAE34, 0xAE37, 0xAE38, 0xAE3A, 0xAE40, 0xAE41, 0xAE43, 0xAE45, 0xAE46, 0xAE4A, 0xAE4C, 0xAE4D, 0xAE4E, 0xAE50, 0xAE54, 0xAE56, 0xAE5C, 0xAE5D, 0xAE5F, 0xAE60, 0xAE61, 0xAE65, 0xAE68, 0xAE69, 0xAE6C, 0xAE70, 0xAE78, + 0xCF6D, 0xCF6E, 0xCF6F, 0xCF72, 0xCF73, 0xCF75, 0xCF76, 0xCF77, 0xCF79, 0xCF7A, 0xCF7B, 0xCF7C, 0xCF7D, 0xCF7E, 0xCF7F, 0xCF81, 0xCF82, 0xCF83, 0xCF84, 0xCF86, 0xCF87, 0xCF88, 0xCF89, 0xCF8A, 0xCF8B, 0xCF8D, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCF8E, 0xCF8F, 0xCF90, 0xCF91, 0xCF92, 0xCF93, 0xCF94, 0xCF95, 0xCF96, 0xCF97, 0xCF98, 0xCF99, 0xCF9A, 0xCF9B, 0xCF9C, 0xCF9D, 0xCF9E, 0xCF9F, 0xCFA0, 0xCFA2, 0xCFA3, 0xCFA4, 0xCFA5, 0xCFA6, 0xCFA7, 0xCFA9, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCFAA, 0xCFAB, 0xCFAC, 0xCFAD, 0xCFAE, 0xCFAF, 0xCFB1, 0xCFB2, 0xCFB3, 0xCFB4, 0xCFB5, 0xCFB6, 0xCFB7, 0xCFB8, 0xCFB9, 0xCFBA, 0xCFBB, 0xCFBC, 0xCFBD, 0xCFBE, 0xCFBF, 0xCFC0, 0xCFC1, 0xCFC2, 0xCFC3, 0xCFC5, 0xCFC6, 0xCFC7, 0xCFC8, 0xCFC9, 0xCFCA, 0xCFCB, 0xAE79, 0xAE7B, 0xAE7C, 0xAE7D, 0xAE84, 0xAE85, 0xAE8C, 0xAEBC, 0xAEBD, 0xAEBE, 0xAEC0, 0xAEC4, 0xAECC, 0xAECD, 0xAECF, 0xAED0, 0xAED1, 0xAED8, 0xAED9, 0xAEDC, 0xAEE8, 0xAEEB, 0xAEED, 0xAEF4, 0xAEF8, 0xAEFC, 0xAF07, 0xAF08, 0xAF0D, 0xAF10, 0xAF2C, 0xAF2D, 0xAF30, 0xAF32, 0xAF34, 0xAF3C, 0xAF3D, 0xAF3F, 0xAF41, 0xAF42, 0xAF43, 0xAF48, 0xAF49, 0xAF50, 0xAF5C, 0xAF5D, 0xAF64, 0xAF65, 0xAF79, 0xAF80, 0xAF84, 0xAF88, 0xAF90, 0xAF91, 0xAF95, 0xAF9C, 0xAFB8, 0xAFB9, 0xAFBC, 0xAFC0, 0xAFC7, 0xAFC8, 0xAFC9, 0xAFCB, 0xAFCD, 0xAFCE, 0xAFD4, 0xAFDC, 0xAFE8, 0xAFE9, 0xAFF0, 0xAFF1, 0xAFF4, 0xAFF8, 0xB000, 0xB001, 0xB004, 0xB00C, 0xB010, 0xB014, 0xB01C, 0xB01D, 0xB028, 0xB044, 0xB045, 0xB048, 0xB04A, 0xB04C, 0xB04E, 0xB053, 0xB054, 0xB055, 0xB057, 0xB059, + 0xCFCC, 0xCFCD, 0xCFCE, 0xCFCF, 0xCFD0, 0xCFD1, 0xCFD2, 0xCFD3, 0xCFD4, 0xCFD5, 0xCFD6, 0xCFD7, 0xCFD8, 0xCFD9, 0xCFDA, 0xCFDB, 0xCFDC, 0xCFDD, 0xCFDE, 0xCFDF, 0xCFE2, 0xCFE3, 0xCFE5, 0xCFE6, 0xCFE7, 0xCFE9, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xCFEA, 0xCFEB, 0xCFEC, 0xCFED, 0xCFEE, 0xCFEF, 0xCFF2, 0xCFF4, 0xCFF6, 0xCFF7, 0xCFF8, 0xCFF9, 0xCFFA, 0xCFFB, 0xCFFD, 0xCFFE, 0xCFFF, 0xD001, 0xD002, 0xD003, 0xD005, 0xD006, 0xD007, 0xD008, 0xD009, 0xD00A, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD00B, 0xD00C, 0xD00D, 0xD00E, 0xD00F, 0xD010, 0xD012, 0xD013, 0xD014, 0xD015, 0xD016, 0xD017, 0xD019, 0xD01A, 0xD01B, 0xD01C, 0xD01D, 0xD01E, 0xD01F, 0xD020, 0xD021, 0xD022, 0xD023, 0xD024, 0xD025, 0xD026, 0xD027, 0xD028, 0xD029, 0xD02A, 0xD02B, 0xD02C, 0xB05D, 0xB07C, 0xB07D, 0xB080, 0xB084, 0xB08C, 0xB08D, 0xB08F, 0xB091, 0xB098, 0xB099, 0xB09A, 0xB09C, 0xB09F, 0xB0A0, 0xB0A1, 0xB0A2, 0xB0A8, 0xB0A9, 0xB0AB, 0xB0AC, 0xB0AD, 0xB0AE, 0xB0AF, 0xB0B1, 0xB0B3, 0xB0B4, 0xB0B5, 0xB0B8, 0xB0BC, 0xB0C4, 0xB0C5, 0xB0C7, 0xB0C8, 0xB0C9, 0xB0D0, 0xB0D1, 0xB0D4, 0xB0D8, 0xB0E0, 0xB0E5, 0xB108, 0xB109, 0xB10B, 0xB10C, 0xB110, 0xB112, 0xB113, 0xB118, 0xB119, 0xB11B, 0xB11C, 0xB11D, 0xB123, 0xB124, 0xB125, 0xB128, 0xB12C, 0xB134, 0xB135, 0xB137, 0xB138, 0xB139, 0xB140, 0xB141, 0xB144, 0xB148, 0xB150, 0xB151, 0xB154, 0xB155, 0xB158, 0xB15C, 0xB160, 0xB178, 0xB179, 0xB17C, 0xB180, 0xB182, 0xB188, 0xB189, 0xB18B, 0xB18D, 0xB192, 0xB193, 0xB194, 0xB198, 0xB19C, 0xB1A8, 0xB1CC, 0xB1D0, 0xB1D4, 0xB1DC, 0xB1DD, + 0xD02E, 0xD02F, 0xD030, 0xD031, 0xD032, 0xD033, 0xD036, 0xD037, 0xD039, 0xD03A, 0xD03B, 0xD03D, 0xD03E, 0xD03F, 0xD040, 0xD041, 0xD042, 0xD043, 0xD046, 0xD048, 0xD04A, 0xD04B, 0xD04C, 0xD04D, 0xD04E, 0xD04F, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD051, 0xD052, 0xD053, 0xD055, 0xD056, 0xD057, 0xD059, 0xD05A, 0xD05B, 0xD05C, 0xD05D, 0xD05E, 0xD05F, 0xD061, 0xD062, 0xD063, 0xD064, 0xD065, 0xD066, 0xD067, 0xD068, 0xD069, 0xD06A, 0xD06B, 0xD06E, 0xD06F, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD071, 0xD072, 0xD073, 0xD075, 0xD076, 0xD077, 0xD078, 0xD079, 0xD07A, 0xD07B, 0xD07E, 0xD07F, 0xD080, 0xD082, 0xD083, 0xD084, 0xD085, 0xD086, 0xD087, 0xD088, 0xD089, 0xD08A, 0xD08B, 0xD08C, 0xD08D, 0xD08E, 0xD08F, 0xD090, 0xD091, 0xD092, 0xD093, 0xD094, 0xB1DF, 0xB1E8, 0xB1E9, 0xB1EC, 0xB1F0, 0xB1F9, 0xB1FB, 0xB1FD, 0xB204, 0xB205, 0xB208, 0xB20B, 0xB20C, 0xB214, 0xB215, 0xB217, 0xB219, 0xB220, 0xB234, 0xB23C, 0xB258, 0xB25C, 0xB260, 0xB268, 0xB269, 0xB274, 0xB275, 0xB27C, 0xB284, 0xB285, 0xB289, 0xB290, 0xB291, 0xB294, 0xB298, 0xB299, 0xB29A, 0xB2A0, 0xB2A1, 0xB2A3, 0xB2A5, 0xB2A6, 0xB2AA, 0xB2AC, 0xB2B0, 0xB2B4, 0xB2C8, 0xB2C9, 0xB2CC, 0xB2D0, 0xB2D2, 0xB2D8, 0xB2D9, 0xB2DB, 0xB2DD, 0xB2E2, 0xB2E4, 0xB2E5, 0xB2E6, 0xB2E8, 0xB2EB, 0xB2EC, 0xB2ED, 0xB2EE, 0xB2EF, 0xB2F3, 0xB2F4, 0xB2F5, 0xB2F7, 0xB2F8, 0xB2F9, 0xB2FA, 0xB2FB, 0xB2FF, 0xB300, 0xB301, 0xB304, 0xB308, 0xB310, 0xB311, 0xB313, 0xB314, 0xB315, 0xB31C, 0xB354, 0xB355, 0xB356, 0xB358, 0xB35B, 0xB35C, 0xB35E, 0xB35F, 0xB364, 0xB365, + 0xD095, 0xD096, 0xD097, 0xD098, 0xD099, 0xD09A, 0xD09B, 0xD09C, 0xD09D, 0xD09E, 0xD09F, 0xD0A0, 0xD0A1, 0xD0A2, 0xD0A3, 0xD0A6, 0xD0A7, 0xD0A9, 0xD0AA, 0xD0AB, 0xD0AD, 0xD0AE, 0xD0AF, 0xD0B0, 0xD0B1, 0xD0B2, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD0B3, 0xD0B6, 0xD0B8, 0xD0BA, 0xD0BB, 0xD0BC, 0xD0BD, 0xD0BE, 0xD0BF, 0xD0C2, 0xD0C3, 0xD0C5, 0xD0C6, 0xD0C7, 0xD0CA, 0xD0CB, 0xD0CC, 0xD0CD, 0xD0CE, 0xD0CF, 0xD0D2, 0xD0D6, 0xD0D7, 0xD0D8, 0xD0D9, 0xD0DA, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD0DB, 0xD0DE, 0xD0DF, 0xD0E1, 0xD0E2, 0xD0E3, 0xD0E5, 0xD0E6, 0xD0E7, 0xD0E8, 0xD0E9, 0xD0EA, 0xD0EB, 0xD0EE, 0xD0F2, 0xD0F3, 0xD0F4, 0xD0F5, 0xD0F6, 0xD0F7, 0xD0F9, 0xD0FA, 0xD0FB, 0xD0FC, 0xD0FD, 0xD0FE, 0xD0FF, 0xD100, 0xD101, 0xD102, 0xD103, 0xD104, 0xB367, 0xB369, 0xB36B, 0xB36E, 0xB370, 0xB371, 0xB374, 0xB378, 0xB380, 0xB381, 0xB383, 0xB384, 0xB385, 0xB38C, 0xB390, 0xB394, 0xB3A0, 0xB3A1, 0xB3A8, 0xB3AC, 0xB3C4, 0xB3C5, 0xB3C8, 0xB3CB, 0xB3CC, 0xB3CE, 0xB3D0, 0xB3D4, 0xB3D5, 0xB3D7, 0xB3D9, 0xB3DB, 0xB3DD, 0xB3E0, 0xB3E4, 0xB3E8, 0xB3FC, 0xB410, 0xB418, 0xB41C, 0xB420, 0xB428, 0xB429, 0xB42B, 0xB434, 0xB450, 0xB451, 0xB454, 0xB458, 0xB460, 0xB461, 0xB463, 0xB465, 0xB46C, 0xB480, 0xB488, 0xB49D, 0xB4A4, 0xB4A8, 0xB4AC, 0xB4B5, 0xB4B7, 0xB4B9, 0xB4C0, 0xB4C4, 0xB4C8, 0xB4D0, 0xB4D5, 0xB4DC, 0xB4DD, 0xB4E0, 0xB4E3, 0xB4E4, 0xB4E6, 0xB4EC, 0xB4ED, 0xB4EF, 0xB4F1, 0xB4F8, 0xB514, 0xB515, 0xB518, 0xB51B, 0xB51C, 0xB524, 0xB525, 0xB527, 0xB528, 0xB529, 0xB52A, 0xB530, 0xB531, 0xB534, 0xB538, + 0xD105, 0xD106, 0xD107, 0xD108, 0xD109, 0xD10A, 0xD10B, 0xD10C, 0xD10E, 0xD10F, 0xD110, 0xD111, 0xD112, 0xD113, 0xD114, 0xD115, 0xD116, 0xD117, 0xD118, 0xD119, 0xD11A, 0xD11B, 0xD11C, 0xD11D, 0xD11E, 0xD11F, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD120, 0xD121, 0xD122, 0xD123, 0xD124, 0xD125, 0xD126, 0xD127, 0xD128, 0xD129, 0xD12A, 0xD12B, 0xD12C, 0xD12D, 0xD12E, 0xD12F, 0xD132, 0xD133, 0xD135, 0xD136, 0xD137, 0xD139, 0xD13B, 0xD13C, 0xD13D, 0xD13E, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD13F, 0xD142, 0xD146, 0xD147, 0xD148, 0xD149, 0xD14A, 0xD14B, 0xD14E, 0xD14F, 0xD151, 0xD152, 0xD153, 0xD155, 0xD156, 0xD157, 0xD158, 0xD159, 0xD15A, 0xD15B, 0xD15E, 0xD160, 0xD162, 0xD163, 0xD164, 0xD165, 0xD166, 0xD167, 0xD169, 0xD16A, 0xD16B, 0xD16D, 0xB540, 0xB541, 0xB543, 0xB544, 0xB545, 0xB54B, 0xB54C, 0xB54D, 0xB550, 0xB554, 0xB55C, 0xB55D, 0xB55F, 0xB560, 0xB561, 0xB5A0, 0xB5A1, 0xB5A4, 0xB5A8, 0xB5AA, 0xB5AB, 0xB5B0, 0xB5B1, 0xB5B3, 0xB5B4, 0xB5B5, 0xB5BB, 0xB5BC, 0xB5BD, 0xB5C0, 0xB5C4, 0xB5CC, 0xB5CD, 0xB5CF, 0xB5D0, 0xB5D1, 0xB5D8, 0xB5EC, 0xB610, 0xB611, 0xB614, 0xB618, 0xB625, 0xB62C, 0xB634, 0xB648, 0xB664, 0xB668, 0xB69C, 0xB69D, 0xB6A0, 0xB6A4, 0xB6AB, 0xB6AC, 0xB6B1, 0xB6D4, 0xB6F0, 0xB6F4, 0xB6F8, 0xB700, 0xB701, 0xB705, 0xB728, 0xB729, 0xB72C, 0xB72F, 0xB730, 0xB738, 0xB739, 0xB73B, 0xB744, 0xB748, 0xB74C, 0xB754, 0xB755, 0xB760, 0xB764, 0xB768, 0xB770, 0xB771, 0xB773, 0xB775, 0xB77C, 0xB77D, 0xB780, 0xB784, 0xB78C, 0xB78D, 0xB78F, 0xB790, 0xB791, 0xB792, 0xB796, 0xB797, + 0xD16E, 0xD16F, 0xD170, 0xD171, 0xD172, 0xD173, 0xD174, 0xD175, 0xD176, 0xD177, 0xD178, 0xD179, 0xD17A, 0xD17B, 0xD17D, 0xD17E, 0xD17F, 0xD180, 0xD181, 0xD182, 0xD183, 0xD185, 0xD186, 0xD187, 0xD189, 0xD18A, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD18B, 0xD18C, 0xD18D, 0xD18E, 0xD18F, 0xD190, 0xD191, 0xD192, 0xD193, 0xD194, 0xD195, 0xD196, 0xD197, 0xD198, 0xD199, 0xD19A, 0xD19B, 0xD19C, 0xD19D, 0xD19E, 0xD19F, 0xD1A2, 0xD1A3, 0xD1A5, 0xD1A6, 0xD1A7, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD1A9, 0xD1AA, 0xD1AB, 0xD1AC, 0xD1AD, 0xD1AE, 0xD1AF, 0xD1B2, 0xD1B4, 0xD1B6, 0xD1B7, 0xD1B8, 0xD1B9, 0xD1BB, 0xD1BD, 0xD1BE, 0xD1BF, 0xD1C1, 0xD1C2, 0xD1C3, 0xD1C4, 0xD1C5, 0xD1C6, 0xD1C7, 0xD1C8, 0xD1C9, 0xD1CA, 0xD1CB, 0xD1CC, 0xD1CD, 0xD1CE, 0xD1CF, 0xB798, 0xB799, 0xB79C, 0xB7A0, 0xB7A8, 0xB7A9, 0xB7AB, 0xB7AC, 0xB7AD, 0xB7B4, 0xB7B5, 0xB7B8, 0xB7C7, 0xB7C9, 0xB7EC, 0xB7ED, 0xB7F0, 0xB7F4, 0xB7FC, 0xB7FD, 0xB7FF, 0xB800, 0xB801, 0xB807, 0xB808, 0xB809, 0xB80C, 0xB810, 0xB818, 0xB819, 0xB81B, 0xB81D, 0xB824, 0xB825, 0xB828, 0xB82C, 0xB834, 0xB835, 0xB837, 0xB838, 0xB839, 0xB840, 0xB844, 0xB851, 0xB853, 0xB85C, 0xB85D, 0xB860, 0xB864, 0xB86C, 0xB86D, 0xB86F, 0xB871, 0xB878, 0xB87C, 0xB88D, 0xB8A8, 0xB8B0, 0xB8B4, 0xB8B8, 0xB8C0, 0xB8C1, 0xB8C3, 0xB8C5, 0xB8CC, 0xB8D0, 0xB8D4, 0xB8DD, 0xB8DF, 0xB8E1, 0xB8E8, 0xB8E9, 0xB8EC, 0xB8F0, 0xB8F8, 0xB8F9, 0xB8FB, 0xB8FD, 0xB904, 0xB918, 0xB920, 0xB93C, 0xB93D, 0xB940, 0xB944, 0xB94C, 0xB94F, 0xB951, 0xB958, 0xB959, 0xB95C, 0xB960, 0xB968, 0xB969, + 0xD1D0, 0xD1D1, 0xD1D2, 0xD1D3, 0xD1D4, 0xD1D5, 0xD1D6, 0xD1D7, 0xD1D9, 0xD1DA, 0xD1DB, 0xD1DC, 0xD1DD, 0xD1DE, 0xD1DF, 0xD1E0, 0xD1E1, 0xD1E2, 0xD1E3, 0xD1E4, 0xD1E5, 0xD1E6, 0xD1E7, 0xD1E8, 0xD1E9, 0xD1EA, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD1EB, 0xD1EC, 0xD1ED, 0xD1EE, 0xD1EF, 0xD1F0, 0xD1F1, 0xD1F2, 0xD1F3, 0xD1F5, 0xD1F6, 0xD1F7, 0xD1F9, 0xD1FA, 0xD1FB, 0xD1FC, 0xD1FD, 0xD1FE, 0xD1FF, 0xD200, 0xD201, 0xD202, 0xD203, 0xD204, 0xD205, 0xD206, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD208, 0xD20A, 0xD20B, 0xD20C, 0xD20D, 0xD20E, 0xD20F, 0xD211, 0xD212, 0xD213, 0xD214, 0xD215, 0xD216, 0xD217, 0xD218, 0xD219, 0xD21A, 0xD21B, 0xD21C, 0xD21D, 0xD21E, 0xD21F, 0xD220, 0xD221, 0xD222, 0xD223, 0xD224, 0xD225, 0xD226, 0xD227, 0xD228, 0xD229, 0xB96B, 0xB96D, 0xB974, 0xB975, 0xB978, 0xB97C, 0xB984, 0xB985, 0xB987, 0xB989, 0xB98A, 0xB98D, 0xB98E, 0xB9AC, 0xB9AD, 0xB9B0, 0xB9B4, 0xB9BC, 0xB9BD, 0xB9BF, 0xB9C1, 0xB9C8, 0xB9C9, 0xB9CC, 0xB9CE, 0xB9CF, 0xB9D0, 0xB9D1, 0xB9D2, 0xB9D8, 0xB9D9, 0xB9DB, 0xB9DD, 0xB9DE, 0xB9E1, 0xB9E3, 0xB9E4, 0xB9E5, 0xB9E8, 0xB9EC, 0xB9F4, 0xB9F5, 0xB9F7, 0xB9F8, 0xB9F9, 0xB9FA, 0xBA00, 0xBA01, 0xBA08, 0xBA15, 0xBA38, 0xBA39, 0xBA3C, 0xBA40, 0xBA42, 0xBA48, 0xBA49, 0xBA4B, 0xBA4D, 0xBA4E, 0xBA53, 0xBA54, 0xBA55, 0xBA58, 0xBA5C, 0xBA64, 0xBA65, 0xBA67, 0xBA68, 0xBA69, 0xBA70, 0xBA71, 0xBA74, 0xBA78, 0xBA83, 0xBA84, 0xBA85, 0xBA87, 0xBA8C, 0xBAA8, 0xBAA9, 0xBAAB, 0xBAAC, 0xBAB0, 0xBAB2, 0xBAB8, 0xBAB9, 0xBABB, 0xBABD, 0xBAC4, 0xBAC8, 0xBAD8, 0xBAD9, 0xBAFC, + 0xD22A, 0xD22B, 0xD22E, 0xD22F, 0xD231, 0xD232, 0xD233, 0xD235, 0xD236, 0xD237, 0xD238, 0xD239, 0xD23A, 0xD23B, 0xD23E, 0xD240, 0xD242, 0xD243, 0xD244, 0xD245, 0xD246, 0xD247, 0xD249, 0xD24A, 0xD24B, 0xD24C, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD24D, 0xD24E, 0xD24F, 0xD250, 0xD251, 0xD252, 0xD253, 0xD254, 0xD255, 0xD256, 0xD257, 0xD258, 0xD259, 0xD25A, 0xD25B, 0xD25D, 0xD25E, 0xD25F, 0xD260, 0xD261, 0xD262, 0xD263, 0xD265, 0xD266, 0xD267, 0xD268, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD269, 0xD26A, 0xD26B, 0xD26C, 0xD26D, 0xD26E, 0xD26F, 0xD270, 0xD271, 0xD272, 0xD273, 0xD274, 0xD275, 0xD276, 0xD277, 0xD278, 0xD279, 0xD27A, 0xD27B, 0xD27C, 0xD27D, 0xD27E, 0xD27F, 0xD282, 0xD283, 0xD285, 0xD286, 0xD287, 0xD289, 0xD28A, 0xD28B, 0xD28C, 0xBB00, 0xBB04, 0xBB0D, 0xBB0F, 0xBB11, 0xBB18, 0xBB1C, 0xBB20, 0xBB29, 0xBB2B, 0xBB34, 0xBB35, 0xBB36, 0xBB38, 0xBB3B, 0xBB3C, 0xBB3D, 0xBB3E, 0xBB44, 0xBB45, 0xBB47, 0xBB49, 0xBB4D, 0xBB4F, 0xBB50, 0xBB54, 0xBB58, 0xBB61, 0xBB63, 0xBB6C, 0xBB88, 0xBB8C, 0xBB90, 0xBBA4, 0xBBA8, 0xBBAC, 0xBBB4, 0xBBB7, 0xBBC0, 0xBBC4, 0xBBC8, 0xBBD0, 0xBBD3, 0xBBF8, 0xBBF9, 0xBBFC, 0xBBFF, 0xBC00, 0xBC02, 0xBC08, 0xBC09, 0xBC0B, 0xBC0C, 0xBC0D, 0xBC0F, 0xBC11, 0xBC14, 0xBC15, 0xBC16, 0xBC17, 0xBC18, 0xBC1B, 0xBC1C, 0xBC1D, 0xBC1E, 0xBC1F, 0xBC24, 0xBC25, 0xBC27, 0xBC29, 0xBC2D, 0xBC30, 0xBC31, 0xBC34, 0xBC38, 0xBC40, 0xBC41, 0xBC43, 0xBC44, 0xBC45, 0xBC49, 0xBC4C, 0xBC4D, 0xBC50, 0xBC5D, 0xBC84, 0xBC85, 0xBC88, 0xBC8B, 0xBC8C, 0xBC8E, 0xBC94, 0xBC95, 0xBC97, + 0xD28D, 0xD28E, 0xD28F, 0xD292, 0xD293, 0xD294, 0xD296, 0xD297, 0xD298, 0xD299, 0xD29A, 0xD29B, 0xD29D, 0xD29E, 0xD29F, 0xD2A1, 0xD2A2, 0xD2A3, 0xD2A5, 0xD2A6, 0xD2A7, 0xD2A8, 0xD2A9, 0xD2AA, 0xD2AB, 0xD2AD, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD2AE, 0xD2AF, 0xD2B0, 0xD2B2, 0xD2B3, 0xD2B4, 0xD2B5, 0xD2B6, 0xD2B7, 0xD2BA, 0xD2BB, 0xD2BD, 0xD2BE, 0xD2C1, 0xD2C3, 0xD2C4, 0xD2C5, 0xD2C6, 0xD2C7, 0xD2CA, 0xD2CC, 0xD2CD, 0xD2CE, 0xD2CF, 0xD2D0, 0xD2D1, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD2D2, 0xD2D3, 0xD2D5, 0xD2D6, 0xD2D7, 0xD2D9, 0xD2DA, 0xD2DB, 0xD2DD, 0xD2DE, 0xD2DF, 0xD2E0, 0xD2E1, 0xD2E2, 0xD2E3, 0xD2E6, 0xD2E7, 0xD2E8, 0xD2E9, 0xD2EA, 0xD2EB, 0xD2EC, 0xD2ED, 0xD2EE, 0xD2EF, 0xD2F2, 0xD2F3, 0xD2F5, 0xD2F6, 0xD2F7, 0xD2F9, 0xD2FA, 0xBC99, 0xBC9A, 0xBCA0, 0xBCA1, 0xBCA4, 0xBCA7, 0xBCA8, 0xBCB0, 0xBCB1, 0xBCB3, 0xBCB4, 0xBCB5, 0xBCBC, 0xBCBD, 0xBCC0, 0xBCC4, 0xBCCD, 0xBCCF, 0xBCD0, 0xBCD1, 0xBCD5, 0xBCD8, 0xBCDC, 0xBCF4, 0xBCF5, 0xBCF6, 0xBCF8, 0xBCFC, 0xBD04, 0xBD05, 0xBD07, 0xBD09, 0xBD10, 0xBD14, 0xBD24, 0xBD2C, 0xBD40, 0xBD48, 0xBD49, 0xBD4C, 0xBD50, 0xBD58, 0xBD59, 0xBD64, 0xBD68, 0xBD80, 0xBD81, 0xBD84, 0xBD87, 0xBD88, 0xBD89, 0xBD8A, 0xBD90, 0xBD91, 0xBD93, 0xBD95, 0xBD99, 0xBD9A, 0xBD9C, 0xBDA4, 0xBDB0, 0xBDB8, 0xBDD4, 0xBDD5, 0xBDD8, 0xBDDC, 0xBDE9, 0xBDF0, 0xBDF4, 0xBDF8, 0xBE00, 0xBE03, 0xBE05, 0xBE0C, 0xBE0D, 0xBE10, 0xBE14, 0xBE1C, 0xBE1D, 0xBE1F, 0xBE44, 0xBE45, 0xBE48, 0xBE4C, 0xBE4E, 0xBE54, 0xBE55, 0xBE57, 0xBE59, 0xBE5A, 0xBE5B, 0xBE60, 0xBE61, 0xBE64, + 0xD2FB, 0xD2FC, 0xD2FD, 0xD2FE, 0xD2FF, 0xD302, 0xD304, 0xD306, 0xD307, 0xD308, 0xD309, 0xD30A, 0xD30B, 0xD30F, 0xD311, 0xD312, 0xD313, 0xD315, 0xD317, 0xD318, 0xD319, 0xD31A, 0xD31B, 0xD31E, 0xD322, 0xD323, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD324, 0xD326, 0xD327, 0xD32A, 0xD32B, 0xD32D, 0xD32E, 0xD32F, 0xD331, 0xD332, 0xD333, 0xD334, 0xD335, 0xD336, 0xD337, 0xD33A, 0xD33E, 0xD33F, 0xD340, 0xD341, 0xD342, 0xD343, 0xD346, 0xD347, 0xD348, 0xD349, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD34A, 0xD34B, 0xD34C, 0xD34D, 0xD34E, 0xD34F, 0xD350, 0xD351, 0xD352, 0xD353, 0xD354, 0xD355, 0xD356, 0xD357, 0xD358, 0xD359, 0xD35A, 0xD35B, 0xD35C, 0xD35D, 0xD35E, 0xD35F, 0xD360, 0xD361, 0xD362, 0xD363, 0xD364, 0xD365, 0xD366, 0xD367, 0xD368, 0xD369, 0xBE68, 0xBE6A, 0xBE70, 0xBE71, 0xBE73, 0xBE74, 0xBE75, 0xBE7B, 0xBE7C, 0xBE7D, 0xBE80, 0xBE84, 0xBE8C, 0xBE8D, 0xBE8F, 0xBE90, 0xBE91, 0xBE98, 0xBE99, 0xBEA8, 0xBED0, 0xBED1, 0xBED4, 0xBED7, 0xBED8, 0xBEE0, 0xBEE3, 0xBEE4, 0xBEE5, 0xBEEC, 0xBF01, 0xBF08, 0xBF09, 0xBF18, 0xBF19, 0xBF1B, 0xBF1C, 0xBF1D, 0xBF40, 0xBF41, 0xBF44, 0xBF48, 0xBF50, 0xBF51, 0xBF55, 0xBF94, 0xBFB0, 0xBFC5, 0xBFCC, 0xBFCD, 0xBFD0, 0xBFD4, 0xBFDC, 0xBFDF, 0xBFE1, 0xC03C, 0xC051, 0xC058, 0xC05C, 0xC060, 0xC068, 0xC069, 0xC090, 0xC091, 0xC094, 0xC098, 0xC0A0, 0xC0A1, 0xC0A3, 0xC0A5, 0xC0AC, 0xC0AD, 0xC0AF, 0xC0B0, 0xC0B3, 0xC0B4, 0xC0B5, 0xC0B6, 0xC0BC, 0xC0BD, 0xC0BF, 0xC0C0, 0xC0C1, 0xC0C5, 0xC0C8, 0xC0C9, 0xC0CC, 0xC0D0, 0xC0D8, 0xC0D9, 0xC0DB, 0xC0DC, 0xC0DD, 0xC0E4, + 0xD36A, 0xD36B, 0xD36C, 0xD36D, 0xD36E, 0xD36F, 0xD370, 0xD371, 0xD372, 0xD373, 0xD374, 0xD375, 0xD376, 0xD377, 0xD378, 0xD379, 0xD37A, 0xD37B, 0xD37E, 0xD37F, 0xD381, 0xD382, 0xD383, 0xD385, 0xD386, 0xD387, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD388, 0xD389, 0xD38A, 0xD38B, 0xD38E, 0xD392, 0xD393, 0xD394, 0xD395, 0xD396, 0xD397, 0xD39A, 0xD39B, 0xD39D, 0xD39E, 0xD39F, 0xD3A1, 0xD3A2, 0xD3A3, 0xD3A4, 0xD3A5, 0xD3A6, 0xD3A7, 0xD3AA, 0xD3AC, 0xD3AE, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD3AF, 0xD3B0, 0xD3B1, 0xD3B2, 0xD3B3, 0xD3B5, 0xD3B6, 0xD3B7, 0xD3B9, 0xD3BA, 0xD3BB, 0xD3BD, 0xD3BE, 0xD3BF, 0xD3C0, 0xD3C1, 0xD3C2, 0xD3C3, 0xD3C6, 0xD3C7, 0xD3CA, 0xD3CB, 0xD3CC, 0xD3CD, 0xD3CE, 0xD3CF, 0xD3D1, 0xD3D2, 0xD3D3, 0xD3D4, 0xD3D5, 0xD3D6, 0xC0E5, 0xC0E8, 0xC0EC, 0xC0F4, 0xC0F5, 0xC0F7, 0xC0F9, 0xC100, 0xC104, 0xC108, 0xC110, 0xC115, 0xC11C, 0xC11D, 0xC11E, 0xC11F, 0xC120, 0xC123, 0xC124, 0xC126, 0xC127, 0xC12C, 0xC12D, 0xC12F, 0xC130, 0xC131, 0xC136, 0xC138, 0xC139, 0xC13C, 0xC140, 0xC148, 0xC149, 0xC14B, 0xC14C, 0xC14D, 0xC154, 0xC155, 0xC158, 0xC15C, 0xC164, 0xC165, 0xC167, 0xC168, 0xC169, 0xC170, 0xC174, 0xC178, 0xC185, 0xC18C, 0xC18D, 0xC18E, 0xC190, 0xC194, 0xC196, 0xC19C, 0xC19D, 0xC19F, 0xC1A1, 0xC1A5, 0xC1A8, 0xC1A9, 0xC1AC, 0xC1B0, 0xC1BD, 0xC1C4, 0xC1C8, 0xC1CC, 0xC1D4, 0xC1D7, 0xC1D8, 0xC1E0, 0xC1E4, 0xC1E8, 0xC1F0, 0xC1F1, 0xC1F3, 0xC1FC, 0xC1FD, 0xC200, 0xC204, 0xC20C, 0xC20D, 0xC20F, 0xC211, 0xC218, 0xC219, 0xC21C, 0xC21F, 0xC220, 0xC228, 0xC229, 0xC22B, 0xC22D, + 0xD3D7, 0xD3D9, 0xD3DA, 0xD3DB, 0xD3DC, 0xD3DD, 0xD3DE, 0xD3DF, 0xD3E0, 0xD3E2, 0xD3E4, 0xD3E5, 0xD3E6, 0xD3E7, 0xD3E8, 0xD3E9, 0xD3EA, 0xD3EB, 0xD3EE, 0xD3EF, 0xD3F1, 0xD3F2, 0xD3F3, 0xD3F5, 0xD3F6, 0xD3F7, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD3F8, 0xD3F9, 0xD3FA, 0xD3FB, 0xD3FE, 0xD400, 0xD402, 0xD403, 0xD404, 0xD405, 0xD406, 0xD407, 0xD409, 0xD40A, 0xD40B, 0xD40C, 0xD40D, 0xD40E, 0xD40F, 0xD410, 0xD411, 0xD412, 0xD413, 0xD414, 0xD415, 0xD416, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD417, 0xD418, 0xD419, 0xD41A, 0xD41B, 0xD41C, 0xD41E, 0xD41F, 0xD420, 0xD421, 0xD422, 0xD423, 0xD424, 0xD425, 0xD426, 0xD427, 0xD428, 0xD429, 0xD42A, 0xD42B, 0xD42C, 0xD42D, 0xD42E, 0xD42F, 0xD430, 0xD431, 0xD432, 0xD433, 0xD434, 0xD435, 0xD436, 0xD437, 0xC22F, 0xC231, 0xC232, 0xC234, 0xC248, 0xC250, 0xC251, 0xC254, 0xC258, 0xC260, 0xC265, 0xC26C, 0xC26D, 0xC270, 0xC274, 0xC27C, 0xC27D, 0xC27F, 0xC281, 0xC288, 0xC289, 0xC290, 0xC298, 0xC29B, 0xC29D, 0xC2A4, 0xC2A5, 0xC2A8, 0xC2AC, 0xC2AD, 0xC2B4, 0xC2B5, 0xC2B7, 0xC2B9, 0xC2DC, 0xC2DD, 0xC2E0, 0xC2E3, 0xC2E4, 0xC2EB, 0xC2EC, 0xC2ED, 0xC2EF, 0xC2F1, 0xC2F6, 0xC2F8, 0xC2F9, 0xC2FB, 0xC2FC, 0xC300, 0xC308, 0xC309, 0xC30C, 0xC30D, 0xC313, 0xC314, 0xC315, 0xC318, 0xC31C, 0xC324, 0xC325, 0xC328, 0xC329, 0xC345, 0xC368, 0xC369, 0xC36C, 0xC370, 0xC372, 0xC378, 0xC379, 0xC37C, 0xC37D, 0xC384, 0xC388, 0xC38C, 0xC3C0, 0xC3D8, 0xC3D9, 0xC3DC, 0xC3DF, 0xC3E0, 0xC3E2, 0xC3E8, 0xC3E9, 0xC3ED, 0xC3F4, 0xC3F5, 0xC3F8, 0xC408, 0xC410, 0xC424, 0xC42C, 0xC430, + 0xD438, 0xD439, 0xD43A, 0xD43B, 0xD43C, 0xD43D, 0xD43E, 0xD43F, 0xD441, 0xD442, 0xD443, 0xD445, 0xD446, 0xD447, 0xD448, 0xD449, 0xD44A, 0xD44B, 0xD44C, 0xD44D, 0xD44E, 0xD44F, 0xD450, 0xD451, 0xD452, 0xD453, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD454, 0xD455, 0xD456, 0xD457, 0xD458, 0xD459, 0xD45A, 0xD45B, 0xD45D, 0xD45E, 0xD45F, 0xD461, 0xD462, 0xD463, 0xD465, 0xD466, 0xD467, 0xD468, 0xD469, 0xD46A, 0xD46B, 0xD46C, 0xD46E, 0xD470, 0xD471, 0xD472, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD473, 0xD474, 0xD475, 0xD476, 0xD477, 0xD47A, 0xD47B, 0xD47D, 0xD47E, 0xD481, 0xD483, 0xD484, 0xD485, 0xD486, 0xD487, 0xD48A, 0xD48C, 0xD48E, 0xD48F, 0xD490, 0xD491, 0xD492, 0xD493, 0xD495, 0xD496, 0xD497, 0xD498, 0xD499, 0xD49A, 0xD49B, 0xD49C, 0xD49D, 0xC434, 0xC43C, 0xC43D, 0xC448, 0xC464, 0xC465, 0xC468, 0xC46C, 0xC474, 0xC475, 0xC479, 0xC480, 0xC494, 0xC49C, 0xC4B8, 0xC4BC, 0xC4E9, 0xC4F0, 0xC4F1, 0xC4F4, 0xC4F8, 0xC4FA, 0xC4FF, 0xC500, 0xC501, 0xC50C, 0xC510, 0xC514, 0xC51C, 0xC528, 0xC529, 0xC52C, 0xC530, 0xC538, 0xC539, 0xC53B, 0xC53D, 0xC544, 0xC545, 0xC548, 0xC549, 0xC54A, 0xC54C, 0xC54D, 0xC54E, 0xC553, 0xC554, 0xC555, 0xC557, 0xC558, 0xC559, 0xC55D, 0xC55E, 0xC560, 0xC561, 0xC564, 0xC568, 0xC570, 0xC571, 0xC573, 0xC574, 0xC575, 0xC57C, 0xC57D, 0xC580, 0xC584, 0xC587, 0xC58C, 0xC58D, 0xC58F, 0xC591, 0xC595, 0xC597, 0xC598, 0xC59C, 0xC5A0, 0xC5A9, 0xC5B4, 0xC5B5, 0xC5B8, 0xC5B9, 0xC5BB, 0xC5BC, 0xC5BD, 0xC5BE, 0xC5C4, 0xC5C5, 0xC5C6, 0xC5C7, 0xC5C8, 0xC5C9, 0xC5CA, 0xC5CC, 0xC5CE, + 0xD49E, 0xD49F, 0xD4A0, 0xD4A1, 0xD4A2, 0xD4A3, 0xD4A4, 0xD4A5, 0xD4A6, 0xD4A7, 0xD4A8, 0xD4AA, 0xD4AB, 0xD4AC, 0xD4AD, 0xD4AE, 0xD4AF, 0xD4B0, 0xD4B1, 0xD4B2, 0xD4B3, 0xD4B4, 0xD4B5, 0xD4B6, 0xD4B7, 0xD4B8, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD4B9, 0xD4BA, 0xD4BB, 0xD4BC, 0xD4BD, 0xD4BE, 0xD4BF, 0xD4C0, 0xD4C1, 0xD4C2, 0xD4C3, 0xD4C4, 0xD4C5, 0xD4C6, 0xD4C7, 0xD4C8, 0xD4C9, 0xD4CA, 0xD4CB, 0xD4CD, 0xD4CE, 0xD4CF, 0xD4D1, 0xD4D2, 0xD4D3, 0xD4D5, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD4D6, 0xD4D7, 0xD4D8, 0xD4D9, 0xD4DA, 0xD4DB, 0xD4DD, 0xD4DE, 0xD4E0, 0xD4E1, 0xD4E2, 0xD4E3, 0xD4E4, 0xD4E5, 0xD4E6, 0xD4E7, 0xD4E9, 0xD4EA, 0xD4EB, 0xD4ED, 0xD4EE, 0xD4EF, 0xD4F1, 0xD4F2, 0xD4F3, 0xD4F4, 0xD4F5, 0xD4F6, 0xD4F7, 0xD4F9, 0xD4FA, 0xD4FC, 0xC5D0, 0xC5D1, 0xC5D4, 0xC5D8, 0xC5E0, 0xC5E1, 0xC5E3, 0xC5E5, 0xC5EC, 0xC5ED, 0xC5EE, 0xC5F0, 0xC5F4, 0xC5F6, 0xC5F7, 0xC5FC, 0xC5FD, 0xC5FE, 0xC5FF, 0xC600, 0xC601, 0xC605, 0xC606, 0xC607, 0xC608, 0xC60C, 0xC610, 0xC618, 0xC619, 0xC61B, 0xC61C, 0xC624, 0xC625, 0xC628, 0xC62C, 0xC62D, 0xC62E, 0xC630, 0xC633, 0xC634, 0xC635, 0xC637, 0xC639, 0xC63B, 0xC640, 0xC641, 0xC644, 0xC648, 0xC650, 0xC651, 0xC653, 0xC654, 0xC655, 0xC65C, 0xC65D, 0xC660, 0xC66C, 0xC66F, 0xC671, 0xC678, 0xC679, 0xC67C, 0xC680, 0xC688, 0xC689, 0xC68B, 0xC68D, 0xC694, 0xC695, 0xC698, 0xC69C, 0xC6A4, 0xC6A5, 0xC6A7, 0xC6A9, 0xC6B0, 0xC6B1, 0xC6B4, 0xC6B8, 0xC6B9, 0xC6BA, 0xC6C0, 0xC6C1, 0xC6C3, 0xC6C5, 0xC6CC, 0xC6CD, 0xC6D0, 0xC6D4, 0xC6DC, 0xC6DD, 0xC6E0, 0xC6E1, 0xC6E8, + 0xD4FE, 0xD4FF, 0xD500, 0xD501, 0xD502, 0xD503, 0xD505, 0xD506, 0xD507, 0xD509, 0xD50A, 0xD50B, 0xD50D, 0xD50E, 0xD50F, 0xD510, 0xD511, 0xD512, 0xD513, 0xD516, 0xD518, 0xD519, 0xD51A, 0xD51B, 0xD51C, 0xD51D, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD51E, 0xD51F, 0xD520, 0xD521, 0xD522, 0xD523, 0xD524, 0xD525, 0xD526, 0xD527, 0xD528, 0xD529, 0xD52A, 0xD52B, 0xD52C, 0xD52D, 0xD52E, 0xD52F, 0xD530, 0xD531, 0xD532, 0xD533, 0xD534, 0xD535, 0xD536, 0xD537, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD538, 0xD539, 0xD53A, 0xD53B, 0xD53E, 0xD53F, 0xD541, 0xD542, 0xD543, 0xD545, 0xD546, 0xD547, 0xD548, 0xD549, 0xD54A, 0xD54B, 0xD54E, 0xD550, 0xD552, 0xD553, 0xD554, 0xD555, 0xD556, 0xD557, 0xD55A, 0xD55B, 0xD55D, 0xD55E, 0xD55F, 0xD561, 0xD562, 0xD563, 0xC6E9, 0xC6EC, 0xC6F0, 0xC6F8, 0xC6F9, 0xC6FD, 0xC704, 0xC705, 0xC708, 0xC70C, 0xC714, 0xC715, 0xC717, 0xC719, 0xC720, 0xC721, 0xC724, 0xC728, 0xC730, 0xC731, 0xC733, 0xC735, 0xC737, 0xC73C, 0xC73D, 0xC740, 0xC744, 0xC74A, 0xC74C, 0xC74D, 0xC74F, 0xC751, 0xC752, 0xC753, 0xC754, 0xC755, 0xC756, 0xC757, 0xC758, 0xC75C, 0xC760, 0xC768, 0xC76B, 0xC774, 0xC775, 0xC778, 0xC77C, 0xC77D, 0xC77E, 0xC783, 0xC784, 0xC785, 0xC787, 0xC788, 0xC789, 0xC78A, 0xC78E, 0xC790, 0xC791, 0xC794, 0xC796, 0xC797, 0xC798, 0xC79A, 0xC7A0, 0xC7A1, 0xC7A3, 0xC7A4, 0xC7A5, 0xC7A6, 0xC7AC, 0xC7AD, 0xC7B0, 0xC7B4, 0xC7BC, 0xC7BD, 0xC7BF, 0xC7C0, 0xC7C1, 0xC7C8, 0xC7C9, 0xC7CC, 0xC7CE, 0xC7D0, 0xC7D8, 0xC7DD, 0xC7E4, 0xC7E8, 0xC7EC, 0xC800, 0xC801, 0xC804, 0xC808, 0xC80A, + 0xD564, 0xD566, 0xD567, 0xD56A, 0xD56C, 0xD56E, 0xD56F, 0xD570, 0xD571, 0xD572, 0xD573, 0xD576, 0xD577, 0xD579, 0xD57A, 0xD57B, 0xD57D, 0xD57E, 0xD57F, 0xD580, 0xD581, 0xD582, 0xD583, 0xD586, 0xD58A, 0xD58B, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD58C, 0xD58D, 0xD58E, 0xD58F, 0xD591, 0xD592, 0xD593, 0xD594, 0xD595, 0xD596, 0xD597, 0xD598, 0xD599, 0xD59A, 0xD59B, 0xD59C, 0xD59D, 0xD59E, 0xD59F, 0xD5A0, 0xD5A1, 0xD5A2, 0xD5A3, 0xD5A4, 0xD5A6, 0xD5A7, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD5A8, 0xD5A9, 0xD5AA, 0xD5AB, 0xD5AC, 0xD5AD, 0xD5AE, 0xD5AF, 0xD5B0, 0xD5B1, 0xD5B2, 0xD5B3, 0xD5B4, 0xD5B5, 0xD5B6, 0xD5B7, 0xD5B8, 0xD5B9, 0xD5BA, 0xD5BB, 0xD5BC, 0xD5BD, 0xD5BE, 0xD5BF, 0xD5C0, 0xD5C1, 0xD5C2, 0xD5C3, 0xD5C4, 0xD5C5, 0xD5C6, 0xD5C7, 0xC810, 0xC811, 0xC813, 0xC815, 0xC816, 0xC81C, 0xC81D, 0xC820, 0xC824, 0xC82C, 0xC82D, 0xC82F, 0xC831, 0xC838, 0xC83C, 0xC840, 0xC848, 0xC849, 0xC84C, 0xC84D, 0xC854, 0xC870, 0xC871, 0xC874, 0xC878, 0xC87A, 0xC880, 0xC881, 0xC883, 0xC885, 0xC886, 0xC887, 0xC88B, 0xC88C, 0xC88D, 0xC894, 0xC89D, 0xC89F, 0xC8A1, 0xC8A8, 0xC8BC, 0xC8BD, 0xC8C4, 0xC8C8, 0xC8CC, 0xC8D4, 0xC8D5, 0xC8D7, 0xC8D9, 0xC8E0, 0xC8E1, 0xC8E4, 0xC8F5, 0xC8FC, 0xC8FD, 0xC900, 0xC904, 0xC905, 0xC906, 0xC90C, 0xC90D, 0xC90F, 0xC911, 0xC918, 0xC92C, 0xC934, 0xC950, 0xC951, 0xC954, 0xC958, 0xC960, 0xC961, 0xC963, 0xC96C, 0xC970, 0xC974, 0xC97C, 0xC988, 0xC989, 0xC98C, 0xC990, 0xC998, 0xC999, 0xC99B, 0xC99D, 0xC9C0, 0xC9C1, 0xC9C4, 0xC9C7, 0xC9C8, 0xC9CA, 0xC9D0, 0xC9D1, 0xC9D3, + 0xD5CA, 0xD5CB, 0xD5CD, 0xD5CE, 0xD5CF, 0xD5D1, 0xD5D3, 0xD5D4, 0xD5D5, 0xD5D6, 0xD5D7, 0xD5DA, 0xD5DC, 0xD5DE, 0xD5DF, 0xD5E0, 0xD5E1, 0xD5E2, 0xD5E3, 0xD5E6, 0xD5E7, 0xD5E9, 0xD5EA, 0xD5EB, 0xD5ED, 0xD5EE, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD5EF, 0xD5F0, 0xD5F1, 0xD5F2, 0xD5F3, 0xD5F6, 0xD5F8, 0xD5FA, 0xD5FB, 0xD5FC, 0xD5FD, 0xD5FE, 0xD5FF, 0xD602, 0xD603, 0xD605, 0xD606, 0xD607, 0xD609, 0xD60A, 0xD60B, 0xD60C, 0xD60D, 0xD60E, 0xD60F, 0xD612, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD616, 0xD617, 0xD618, 0xD619, 0xD61A, 0xD61B, 0xD61D, 0xD61E, 0xD61F, 0xD621, 0xD622, 0xD623, 0xD625, 0xD626, 0xD627, 0xD628, 0xD629, 0xD62A, 0xD62B, 0xD62C, 0xD62E, 0xD62F, 0xD630, 0xD631, 0xD632, 0xD633, 0xD634, 0xD635, 0xD636, 0xD637, 0xD63A, 0xD63B, 0xC9D5, 0xC9D6, 0xC9D9, 0xC9DA, 0xC9DC, 0xC9DD, 0xC9E0, 0xC9E2, 0xC9E4, 0xC9E7, 0xC9EC, 0xC9ED, 0xC9EF, 0xC9F0, 0xC9F1, 0xC9F8, 0xC9F9, 0xC9FC, 0xCA00, 0xCA08, 0xCA09, 0xCA0B, 0xCA0C, 0xCA0D, 0xCA14, 0xCA18, 0xCA29, 0xCA4C, 0xCA4D, 0xCA50, 0xCA54, 0xCA5C, 0xCA5D, 0xCA5F, 0xCA60, 0xCA61, 0xCA68, 0xCA7D, 0xCA84, 0xCA98, 0xCABC, 0xCABD, 0xCAC0, 0xCAC4, 0xCACC, 0xCACD, 0xCACF, 0xCAD1, 0xCAD3, 0xCAD8, 0xCAD9, 0xCAE0, 0xCAEC, 0xCAF4, 0xCB08, 0xCB10, 0xCB14, 0xCB18, 0xCB20, 0xCB21, 0xCB41, 0xCB48, 0xCB49, 0xCB4C, 0xCB50, 0xCB58, 0xCB59, 0xCB5D, 0xCB64, 0xCB78, 0xCB79, 0xCB9C, 0xCBB8, 0xCBD4, 0xCBE4, 0xCBE7, 0xCBE9, 0xCC0C, 0xCC0D, 0xCC10, 0xCC14, 0xCC1C, 0xCC1D, 0xCC21, 0xCC22, 0xCC27, 0xCC28, 0xCC29, 0xCC2C, 0xCC2E, 0xCC30, 0xCC38, 0xCC39, 0xCC3B, + 0xD63D, 0xD63E, 0xD63F, 0xD641, 0xD642, 0xD643, 0xD644, 0xD646, 0xD647, 0xD64A, 0xD64C, 0xD64E, 0xD64F, 0xD650, 0xD652, 0xD653, 0xD656, 0xD657, 0xD659, 0xD65A, 0xD65B, 0xD65D, 0xD65E, 0xD65F, 0xD660, 0xD661, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD662, 0xD663, 0xD664, 0xD665, 0xD666, 0xD668, 0xD66A, 0xD66B, 0xD66C, 0xD66D, 0xD66E, 0xD66F, 0xD672, 0xD673, 0xD675, 0xD676, 0xD677, 0xD678, 0xD679, 0xD67A, 0xD67B, 0xD67C, 0xD67D, 0xD67E, 0xD67F, 0xD680, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD681, 0xD682, 0xD684, 0xD686, 0xD687, 0xD688, 0xD689, 0xD68A, 0xD68B, 0xD68E, 0xD68F, 0xD691, 0xD692, 0xD693, 0xD695, 0xD696, 0xD697, 0xD698, 0xD699, 0xD69A, 0xD69B, 0xD69C, 0xD69E, 0xD6A0, 0xD6A2, 0xD6A3, 0xD6A4, 0xD6A5, 0xD6A6, 0xD6A7, 0xD6A9, 0xD6AA, 0xCC3C, 0xCC3D, 0xCC3E, 0xCC44, 0xCC45, 0xCC48, 0xCC4C, 0xCC54, 0xCC55, 0xCC57, 0xCC58, 0xCC59, 0xCC60, 0xCC64, 0xCC66, 0xCC68, 0xCC70, 0xCC75, 0xCC98, 0xCC99, 0xCC9C, 0xCCA0, 0xCCA8, 0xCCA9, 0xCCAB, 0xCCAC, 0xCCAD, 0xCCB4, 0xCCB5, 0xCCB8, 0xCCBC, 0xCCC4, 0xCCC5, 0xCCC7, 0xCCC9, 0xCCD0, 0xCCD4, 0xCCE4, 0xCCEC, 0xCCF0, 0xCD01, 0xCD08, 0xCD09, 0xCD0C, 0xCD10, 0xCD18, 0xCD19, 0xCD1B, 0xCD1D, 0xCD24, 0xCD28, 0xCD2C, 0xCD39, 0xCD5C, 0xCD60, 0xCD64, 0xCD6C, 0xCD6D, 0xCD6F, 0xCD71, 0xCD78, 0xCD88, 0xCD94, 0xCD95, 0xCD98, 0xCD9C, 0xCDA4, 0xCDA5, 0xCDA7, 0xCDA9, 0xCDB0, 0xCDC4, 0xCDCC, 0xCDD0, 0xCDE8, 0xCDEC, 0xCDF0, 0xCDF8, 0xCDF9, 0xCDFB, 0xCDFD, 0xCE04, 0xCE08, 0xCE0C, 0xCE14, 0xCE19, 0xCE20, 0xCE21, 0xCE24, 0xCE28, 0xCE30, 0xCE31, 0xCE33, 0xCE35, + 0xD6AB, 0xD6AD, 0xD6AE, 0xD6AF, 0xD6B1, 0xD6B2, 0xD6B3, 0xD6B4, 0xD6B5, 0xD6B6, 0xD6B7, 0xD6B8, 0xD6BA, 0xD6BC, 0xD6BD, 0xD6BE, 0xD6BF, 0xD6C0, 0xD6C1, 0xD6C2, 0xD6C3, 0xD6C6, 0xD6C7, 0xD6C9, 0xD6CA, 0xD6CB, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD6CD, 0xD6CE, 0xD6CF, 0xD6D0, 0xD6D2, 0xD6D3, 0xD6D5, 0xD6D6, 0xD6D8, 0xD6DA, 0xD6DB, 0xD6DC, 0xD6DD, 0xD6DE, 0xD6DF, 0xD6E1, 0xD6E2, 0xD6E3, 0xD6E5, 0xD6E6, 0xD6E7, 0xD6E9, 0xD6EA, 0xD6EB, 0xD6EC, 0xD6ED, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD6EE, 0xD6EF, 0xD6F1, 0xD6F2, 0xD6F3, 0xD6F4, 0xD6F6, 0xD6F7, 0xD6F8, 0xD6F9, 0xD6FA, 0xD6FB, 0xD6FE, 0xD6FF, 0xD701, 0xD702, 0xD703, 0xD705, 0xD706, 0xD707, 0xD708, 0xD709, 0xD70A, 0xD70B, 0xD70C, 0xD70D, 0xD70E, 0xD70F, 0xD710, 0xD712, 0xD713, 0xD714, 0xCE58, 0xCE59, 0xCE5C, 0xCE5F, 0xCE60, 0xCE61, 0xCE68, 0xCE69, 0xCE6B, 0xCE6D, 0xCE74, 0xCE75, 0xCE78, 0xCE7C, 0xCE84, 0xCE85, 0xCE87, 0xCE89, 0xCE90, 0xCE91, 0xCE94, 0xCE98, 0xCEA0, 0xCEA1, 0xCEA3, 0xCEA4, 0xCEA5, 0xCEAC, 0xCEAD, 0xCEC1, 0xCEE4, 0xCEE5, 0xCEE8, 0xCEEB, 0xCEEC, 0xCEF4, 0xCEF5, 0xCEF7, 0xCEF8, 0xCEF9, 0xCF00, 0xCF01, 0xCF04, 0xCF08, 0xCF10, 0xCF11, 0xCF13, 0xCF15, 0xCF1C, 0xCF20, 0xCF24, 0xCF2C, 0xCF2D, 0xCF2F, 0xCF30, 0xCF31, 0xCF38, 0xCF54, 0xCF55, 0xCF58, 0xCF5C, 0xCF64, 0xCF65, 0xCF67, 0xCF69, 0xCF70, 0xCF71, 0xCF74, 0xCF78, 0xCF80, 0xCF85, 0xCF8C, 0xCFA1, 0xCFA8, 0xCFB0, 0xCFC4, 0xCFE0, 0xCFE1, 0xCFE4, 0xCFE8, 0xCFF0, 0xCFF1, 0xCFF3, 0xCFF5, 0xCFFC, 0xD000, 0xD004, 0xD011, 0xD018, 0xD02D, 0xD034, 0xD035, 0xD038, 0xD03C, + 0xD715, 0xD716, 0xD717, 0xD71A, 0xD71B, 0xD71D, 0xD71E, 0xD71F, 0xD721, 0xD722, 0xD723, 0xD724, 0xD725, 0xD726, 0xD727, 0xD72A, 0xD72C, 0xD72E, 0xD72F, 0xD730, 0xD731, 0xD732, 0xD733, 0xD736, 0xD737, 0xD739, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD73A, 0xD73B, 0xD73D, 0xD73E, 0xD73F, 0xD740, 0xD741, 0xD742, 0xD743, 0xD745, 0xD746, 0xD748, 0xD74A, 0xD74B, 0xD74C, 0xD74D, 0xD74E, 0xD74F, 0xD752, 0xD753, 0xD755, 0xD75A, 0xD75B, 0xD75C, 0xD75D, 0xD75E, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD75F, 0xD762, 0xD764, 0xD766, 0xD767, 0xD768, 0xD76A, 0xD76B, 0xD76D, 0xD76E, 0xD76F, 0xD771, 0xD772, 0xD773, 0xD775, 0xD776, 0xD777, 0xD778, 0xD779, 0xD77A, 0xD77B, 0xD77E, 0xD77F, 0xD780, 0xD782, 0xD783, 0xD784, 0xD785, 0xD786, 0xD787, 0xD78A, 0xD78B, 0xD044, 0xD045, 0xD047, 0xD049, 0xD050, 0xD054, 0xD058, 0xD060, 0xD06C, 0xD06D, 0xD070, 0xD074, 0xD07C, 0xD07D, 0xD081, 0xD0A4, 0xD0A5, 0xD0A8, 0xD0AC, 0xD0B4, 0xD0B5, 0xD0B7, 0xD0B9, 0xD0C0, 0xD0C1, 0xD0C4, 0xD0C8, 0xD0C9, 0xD0D0, 0xD0D1, 0xD0D3, 0xD0D4, 0xD0D5, 0xD0DC, 0xD0DD, 0xD0E0, 0xD0E4, 0xD0EC, 0xD0ED, 0xD0EF, 0xD0F0, 0xD0F1, 0xD0F8, 0xD10D, 0xD130, 0xD131, 0xD134, 0xD138, 0xD13A, 0xD140, 0xD141, 0xD143, 0xD144, 0xD145, 0xD14C, 0xD14D, 0xD150, 0xD154, 0xD15C, 0xD15D, 0xD15F, 0xD161, 0xD168, 0xD16C, 0xD17C, 0xD184, 0xD188, 0xD1A0, 0xD1A1, 0xD1A4, 0xD1A8, 0xD1B0, 0xD1B1, 0xD1B3, 0xD1B5, 0xD1BA, 0xD1BC, 0xD1C0, 0xD1D8, 0xD1F4, 0xD1F8, 0xD207, 0xD209, 0xD210, 0xD22C, 0xD22D, 0xD230, 0xD234, 0xD23C, 0xD23D, 0xD23F, 0xD241, 0xD248, 0xD25C, + 0xD78D, 0xD78E, 0xD78F, 0xD791, 0xD792, 0xD793, 0xD794, 0xD795, 0xD796, 0xD797, 0xD79A, 0xD79C, 0xD79E, 0xD79F, 0xD7A0, 0xD7A1, 0xD7A2, 0xD7A3, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD264, 0xD280, 0xD281, 0xD284, 0xD288, 0xD290, 0xD291, 0xD295, 0xD29C, 0xD2A0, 0xD2A4, 0xD2AC, 0xD2B1, 0xD2B8, 0xD2B9, 0xD2BC, 0xD2BF, 0xD2C0, 0xD2C2, 0xD2C8, 0xD2C9, 0xD2CB, 0xD2D4, 0xD2D8, 0xD2DC, 0xD2E4, 0xD2E5, 0xD2F0, 0xD2F1, 0xD2F4, 0xD2F8, 0xD300, 0xD301, 0xD303, 0xD305, 0xD30C, 0xD30D, 0xD30E, 0xD310, 0xD314, 0xD316, 0xD31C, 0xD31D, 0xD31F, 0xD320, 0xD321, 0xD325, 0xD328, 0xD329, 0xD32C, 0xD330, 0xD338, 0xD339, 0xD33B, 0xD33C, 0xD33D, 0xD344, 0xD345, 0xD37C, 0xD37D, 0xD380, 0xD384, 0xD38C, 0xD38D, 0xD38F, 0xD390, 0xD391, 0xD398, 0xD399, 0xD39C, 0xD3A0, 0xD3A8, 0xD3A9, 0xD3AB, 0xD3AD, 0xD3B4, 0xD3B8, 0xD3BC, 0xD3C4, 0xD3C5, 0xD3C8, 0xD3C9, 0xD3D0, 0xD3D8, 0xD3E1, 0xD3E3, 0xD3EC, 0xD3ED, 0xD3F0, 0xD3F4, 0xD3FC, 0xD3FD, 0xD3FF, 0xD401, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD408, 0xD41D, 0xD440, 0xD444, 0xD45C, 0xD460, 0xD464, 0xD46D, 0xD46F, 0xD478, 0xD479, 0xD47C, 0xD47F, 0xD480, 0xD482, 0xD488, 0xD489, 0xD48B, 0xD48D, 0xD494, 0xD4A9, 0xD4CC, 0xD4D0, 0xD4D4, 0xD4DC, 0xD4DF, 0xD4E8, 0xD4EC, 0xD4F0, 0xD4F8, 0xD4FB, 0xD4FD, 0xD504, 0xD508, 0xD50C, 0xD514, 0xD515, 0xD517, 0xD53C, 0xD53D, 0xD540, 0xD544, 0xD54C, 0xD54D, 0xD54F, 0xD551, 0xD558, 0xD559, 0xD55C, 0xD560, 0xD565, 0xD568, 0xD569, 0xD56B, 0xD56D, 0xD574, 0xD575, 0xD578, 0xD57C, 0xD584, 0xD585, 0xD587, 0xD588, 0xD589, 0xD590, 0xD5A5, 0xD5C8, 0xD5C9, 0xD5CC, 0xD5D0, 0xD5D2, 0xD5D8, 0xD5D9, 0xD5DB, 0xD5DD, 0xD5E4, 0xD5E5, 0xD5E8, 0xD5EC, 0xD5F4, 0xD5F5, 0xD5F7, 0xD5F9, 0xD600, 0xD601, 0xD604, 0xD608, 0xD610, 0xD611, 0xD613, 0xD614, 0xD615, 0xD61C, 0xD620, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xD624, 0xD62D, 0xD638, 0xD639, 0xD63C, 0xD640, 0xD645, 0xD648, 0xD649, 0xD64B, 0xD64D, 0xD651, 0xD654, 0xD655, 0xD658, 0xD65C, 0xD667, 0xD669, 0xD670, 0xD671, 0xD674, 0xD683, 0xD685, 0xD68C, 0xD68D, 0xD690, 0xD694, 0xD69D, 0xD69F, 0xD6A1, 0xD6A8, 0xD6AC, 0xD6B0, 0xD6B9, 0xD6BB, 0xD6C4, 0xD6C5, 0xD6C8, 0xD6CC, 0xD6D1, 0xD6D4, 0xD6D7, 0xD6D9, 0xD6E0, 0xD6E4, 0xD6E8, 0xD6F0, 0xD6F5, 0xD6FC, 0xD6FD, 0xD700, 0xD704, 0xD711, 0xD718, 0xD719, 0xD71C, 0xD720, 0xD728, 0xD729, 0xD72B, 0xD72D, 0xD734, 0xD735, 0xD738, 0xD73C, 0xD744, 0xD747, 0xD749, 0xD750, 0xD751, 0xD754, 0xD756, 0xD757, 0xD758, 0xD759, 0xD760, 0xD761, 0xD763, 0xD765, 0xD769, 0xD76C, 0xD770, 0xD774, 0xD77C, 0xD77D, 0xD781, 0xD788, 0xD789, 0xD78C, 0xD790, 0xD798, 0xD799, 0xD79B, 0xD79D, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x4F3D, 0x4F73, 0x5047, 0x50F9, 0x52A0, 0x53EF, 0x5475, 0x54E5, 0x5609, 0x5AC1, 0x5BB6, 0x6687, 0x67B6, 0x67B7, 0x67EF, 0x6B4C, 0x73C2, 0x75C2, 0x7A3C, 0x82DB, 0x8304, 0x8857, 0x8888, 0x8A36, 0x8CC8, 0x8DCF, 0x8EFB, 0x8FE6, 0x99D5, 0x523B, 0x5374, 0x5404, 0x606A, 0x6164, 0x6BBC, 0x73CF, 0x811A, 0x89BA, 0x89D2, 0x95A3, 0x4F83, 0x520A, 0x58BE, 0x5978, 0x59E6, 0x5E72, 0x5E79, 0x61C7, 0x63C0, 0x6746, 0x67EC, 0x687F, 0x6F97, 0x764E, 0x770B, 0x78F5, 0x7A08, 0x7AFF, 0x7C21, 0x809D, 0x826E, 0x8271, 0x8AEB, 0x9593, 0x4E6B, 0x559D, 0x66F7, 0x6E34, 0x78A3, 0x7AED, 0x845B, 0x8910, 0x874E, 0x97A8, 0x52D8, 0x574E, 0x582A, 0x5D4C, 0x611F, 0x61BE, 0x6221, 0x6562, 0x67D1, 0x6A44, 0x6E1B, 0x7518, 0x75B3, 0x76E3, 0x77B0, 0x7D3A, 0x90AF, 0x9451, 0x9452, 0x9F95, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x5323, 0x5CAC, 0x7532, 0x80DB, 0x9240, 0x9598, 0x525B, 0x5808, 0x59DC, 0x5CA1, 0x5D17, 0x5EB7, 0x5F3A, 0x5F4A, 0x6177, 0x6C5F, 0x757A, 0x7586, 0x7CE0, 0x7D73, 0x7DB1, 0x7F8C, 0x8154, 0x8221, 0x8591, 0x8941, 0x8B1B, 0x92FC, 0x964D, 0x9C47, 0x4ECB, 0x4EF7, 0x500B, 0x51F1, 0x584F, 0x6137, 0x613E, 0x6168, 0x6539, 0x69EA, 0x6F11, 0x75A5, 0x7686, 0x76D6, 0x7B87, 0x82A5, 0x84CB, 0xF900, 0x93A7, 0x958B, 0x5580, 0x5BA2, 0x5751, 0xF901, 0x7CB3, 0x7FB9, 0x91B5, 0x5028, 0x53BB, 0x5C45, 0x5DE8, 0x62D2, 0x636E, 0x64DA, 0x64E7, 0x6E20, 0x70AC, 0x795B, 0x8DDD, 0x8E1E, 0xF902, 0x907D, 0x9245, 0x92F8, 0x4E7E, 0x4EF6, 0x5065, 0x5DFE, 0x5EFA, 0x6106, 0x6957, 0x8171, 0x8654, 0x8E47, 0x9375, 0x9A2B, 0x4E5E, 0x5091, 0x6770, 0x6840, 0x5109, 0x528D, 0x5292, 0x6AA2, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x77BC, 0x9210, 0x9ED4, 0x52AB, 0x602F, 0x8FF2, 0x5048, 0x61A9, 0x63ED, 0x64CA, 0x683C, 0x6A84, 0x6FC0, 0x8188, 0x89A1, 0x9694, 0x5805, 0x727D, 0x72AC, 0x7504, 0x7D79, 0x7E6D, 0x80A9, 0x898B, 0x8B74, 0x9063, 0x9D51, 0x6289, 0x6C7A, 0x6F54, 0x7D50, 0x7F3A, 0x8A23, 0x517C, 0x614A, 0x7B9D, 0x8B19, 0x9257, 0x938C, 0x4EAC, 0x4FD3, 0x501E, 0x50BE, 0x5106, 0x52C1, 0x52CD, 0x537F, 0x5770, 0x5883, 0x5E9A, 0x5F91, 0x6176, 0x61AC, 0x64CE, 0x656C, 0x666F, 0x66BB, 0x66F4, 0x6897, 0x6D87, 0x7085, 0x70F1, 0x749F, 0x74A5, 0x74CA, 0x75D9, 0x786C, 0x78EC, 0x7ADF, 0x7AF6, 0x7D45, 0x7D93, 0x8015, 0x803F, 0x811B, 0x8396, 0x8B66, 0x8F15, 0x9015, 0x93E1, 0x9803, 0x9838, 0x9A5A, 0x9BE8, 0x4FC2, 0x5553, 0x583A, 0x5951, 0x5B63, 0x5C46, 0x60B8, 0x6212, 0x6842, 0x68B0, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x68E8, 0x6EAA, 0x754C, 0x7678, 0x78CE, 0x7A3D, 0x7CFB, 0x7E6B, 0x7E7C, 0x8A08, 0x8AA1, 0x8C3F, 0x968E, 0x9DC4, 0x53E4, 0x53E9, 0x544A, 0x5471, 0x56FA, 0x59D1, 0x5B64, 0x5C3B, 0x5EAB, 0x62F7, 0x6537, 0x6545, 0x6572, 0x66A0, 0x67AF, 0x69C1, 0x6CBD, 0x75FC, 0x7690, 0x777E, 0x7A3F, 0x7F94, 0x8003, 0x80A1, 0x818F, 0x82E6, 0x82FD, 0x83F0, 0x85C1, 0x8831, 0x88B4, 0x8AA5, 0xF903, 0x8F9C, 0x932E, 0x96C7, 0x9867, 0x9AD8, 0x9F13, 0x54ED, 0x659B, 0x66F2, 0x688F, 0x7A40, 0x8C37, 0x9D60, 0x56F0, 0x5764, 0x5D11, 0x6606, 0x68B1, 0x68CD, 0x6EFE, 0x7428, 0x889E, 0x9BE4, 0x6C68, 0xF904, 0x9AA8, 0x4F9B, 0x516C, 0x5171, 0x529F, 0x5B54, 0x5DE5, 0x6050, 0x606D, 0x62F1, 0x63A7, 0x653B, 0x73D9, 0x7A7A, 0x86A3, 0x8CA2, 0x978F, 0x4E32, 0x5BE1, 0x6208, 0x679C, 0x74DC, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x79D1, 0x83D3, 0x8A87, 0x8AB2, 0x8DE8, 0x904E, 0x934B, 0x9846, 0x5ED3, 0x69E8, 0x85FF, 0x90ED, 0xF905, 0x51A0, 0x5B98, 0x5BEC, 0x6163, 0x68FA, 0x6B3E, 0x704C, 0x742F, 0x74D8, 0x7BA1, 0x7F50, 0x83C5, 0x89C0, 0x8CAB, 0x95DC, 0x9928, 0x522E, 0x605D, 0x62EC, 0x9002, 0x4F8A, 0x5149, 0x5321, 0x58D9, 0x5EE3, 0x66E0, 0x6D38, 0x709A, 0x72C2, 0x73D6, 0x7B50, 0x80F1, 0x945B, 0x5366, 0x639B, 0x7F6B, 0x4E56, 0x5080, 0x584A, 0x58DE, 0x602A, 0x6127, 0x62D0, 0x69D0, 0x9B41, 0x5B8F, 0x7D18, 0x80B1, 0x8F5F, 0x4EA4, 0x50D1, 0x54AC, 0x55AC, 0x5B0C, 0x5DA0, 0x5DE7, 0x652A, 0x654E, 0x6821, 0x6A4B, 0x72E1, 0x768E, 0x77EF, 0x7D5E, 0x7FF9, 0x81A0, 0x854E, 0x86DF, 0x8F03, 0x8F4E, 0x90CA, 0x9903, 0x9A55, 0x9BAB, 0x4E18, 0x4E45, 0x4E5D, 0x4EC7, 0x4FF1, 0x5177, 0x52FE, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x5340, 0x53E3, 0x53E5, 0x548E, 0x5614, 0x5775, 0x57A2, 0x5BC7, 0x5D87, 0x5ED0, 0x61FC, 0x62D8, 0x6551, 0x67B8, 0x67E9, 0x69CB, 0x6B50, 0x6BC6, 0x6BEC, 0x6C42, 0x6E9D, 0x7078, 0x72D7, 0x7396, 0x7403, 0x77BF, 0x77E9, 0x7A76, 0x7D7F, 0x8009, 0x81FC, 0x8205, 0x820A, 0x82DF, 0x8862, 0x8B33, 0x8CFC, 0x8EC0, 0x9011, 0x90B1, 0x9264, 0x92B6, 0x99D2, 0x9A45, 0x9CE9, 0x9DD7, 0x9F9C, 0x570B, 0x5C40, 0x83CA, 0x97A0, 0x97AB, 0x9EB4, 0x541B, 0x7A98, 0x7FA4, 0x88D9, 0x8ECD, 0x90E1, 0x5800, 0x5C48, 0x6398, 0x7A9F, 0x5BAE, 0x5F13, 0x7A79, 0x7AAE, 0x828E, 0x8EAC, 0x5026, 0x5238, 0x52F8, 0x5377, 0x5708, 0x62F3, 0x6372, 0x6B0A, 0x6DC3, 0x7737, 0x53A5, 0x7357, 0x8568, 0x8E76, 0x95D5, 0x673A, 0x6AC3, 0x6F70, 0x8A6D, 0x8ECC, 0x994B, 0xF906, 0x6677, 0x6B78, 0x8CB4, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x9B3C, 0xF907, 0x53EB, 0x572D, 0x594E, 0x63C6, 0x69FB, 0x73EA, 0x7845, 0x7ABA, 0x7AC5, 0x7CFE, 0x8475, 0x898F, 0x8D73, 0x9035, 0x95A8, 0x52FB, 0x5747, 0x7547, 0x7B60, 0x83CC, 0x921E, 0xF908, 0x6A58, 0x514B, 0x524B, 0x5287, 0x621F, 0x68D8, 0x6975, 0x9699, 0x50C5, 0x52A4, 0x52E4, 0x61C3, 0x65A4, 0x6839, 0x69FF, 0x747E, 0x7B4B, 0x82B9, 0x83EB, 0x89B2, 0x8B39, 0x8FD1, 0x9949, 0xF909, 0x4ECA, 0x5997, 0x64D2, 0x6611, 0x6A8E, 0x7434, 0x7981, 0x79BD, 0x82A9, 0x887E, 0x887F, 0x895F, 0xF90A, 0x9326, 0x4F0B, 0x53CA, 0x6025, 0x6271, 0x6C72, 0x7D1A, 0x7D66, 0x4E98, 0x5162, 0x77DC, 0x80AF, 0x4F01, 0x4F0E, 0x5176, 0x5180, 0x55DC, 0x5668, 0x573B, 0x57FA, 0x57FC, 0x5914, 0x5947, 0x5993, 0x5BC4, 0x5C90, 0x5D0E, 0x5DF1, 0x5E7E, 0x5FCC, 0x6280, 0x65D7, 0x65E3, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x671E, 0x671F, 0x675E, 0x68CB, 0x68C4, 0x6A5F, 0x6B3A, 0x6C23, 0x6C7D, 0x6C82, 0x6DC7, 0x7398, 0x7426, 0x742A, 0x7482, 0x74A3, 0x7578, 0x757F, 0x7881, 0x78EF, 0x7941, 0x7947, 0x7948, 0x797A, 0x7B95, 0x7D00, 0x7DBA, 0x7F88, 0x8006, 0x802D, 0x808C, 0x8A18, 0x8B4F, 0x8C48, 0x8D77, 0x9321, 0x9324, 0x98E2, 0x9951, 0x9A0E, 0x9A0F, 0x9A65, 0x9E92, 0x7DCA, 0x4F76, 0x5409, 0x62EE, 0x6854, 0x91D1, 0x55AB, 0x513A, 0xF90B, 0xF90C, 0x5A1C, 0x61E6, 0xF90D, 0x62CF, 0x62FF, 0xF90E, 0xF90F, 0xF910, 0xF911, 0xF912, 0xF913, 0x90A3, 0xF914, 0xF915, 0xF916, 0xF917, 0xF918, 0x8AFE, 0xF919, 0xF91A, 0xF91B, 0xF91C, 0x6696, 0xF91D, 0x7156, 0xF91E, 0xF91F, 0x96E3, 0xF920, 0x634F, 0x637A, 0x5357, 0xF921, 0x678F, 0x6960, 0x6E73, 0xF922, 0x7537, 0xF923, 0xF924, 0xF925, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x7D0D, 0xF926, 0xF927, 0x8872, 0x56CA, 0x5A18, 0xF928, 0xF929, 0xF92A, 0xF92B, 0xF92C, 0x4E43, 0xF92D, 0x5167, 0x5948, 0x67F0, 0x8010, 0xF92E, 0x5973, 0x5E74, 0x649A, 0x79CA, 0x5FF5, 0x606C, 0x62C8, 0x637B, 0x5BE7, 0x5BD7, 0x52AA, 0xF92F, 0x5974, 0x5F29, 0x6012, 0xF930, 0xF931, 0xF932, 0x7459, 0xF933, 0xF934, 0xF935, 0xF936, 0xF937, 0xF938, 0x99D1, 0xF939, 0xF93A, 0xF93B, 0xF93C, 0xF93D, 0xF93E, 0xF93F, 0xF940, 0xF941, 0xF942, 0xF943, 0x6FC3, 0xF944, 0xF945, 0x81BF, 0x8FB2, 0x60F1, 0xF946, 0xF947, 0x8166, 0xF948, 0xF949, 0x5C3F, 0xF94A, 0xF94B, 0xF94C, 0xF94D, 0xF94E, 0xF94F, 0xF950, 0xF951, 0x5AE9, 0x8A25, 0x677B, 0x7D10, 0xF952, 0xF953, 0xF954, 0xF955, 0xF956, 0xF957, 0x80FD, 0xF958, 0xF959, 0x5C3C, 0x6CE5, 0x533F, 0x6EBA, 0x591A, 0x8336, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x4E39, 0x4EB6, 0x4F46, 0x55AE, 0x5718, 0x58C7, 0x5F56, 0x65B7, 0x65E6, 0x6A80, 0x6BB5, 0x6E4D, 0x77ED, 0x7AEF, 0x7C1E, 0x7DDE, 0x86CB, 0x8892, 0x9132, 0x935B, 0x64BB, 0x6FBE, 0x737A, 0x75B8, 0x9054, 0x5556, 0x574D, 0x61BA, 0x64D4, 0x66C7, 0x6DE1, 0x6E5B, 0x6F6D, 0x6FB9, 0x75F0, 0x8043, 0x81BD, 0x8541, 0x8983, 0x8AC7, 0x8B5A, 0x931F, 0x6C93, 0x7553, 0x7B54, 0x8E0F, 0x905D, 0x5510, 0x5802, 0x5858, 0x5E62, 0x6207, 0x649E, 0x68E0, 0x7576, 0x7CD6, 0x87B3, 0x9EE8, 0x4EE3, 0x5788, 0x576E, 0x5927, 0x5C0D, 0x5CB1, 0x5E36, 0x5F85, 0x6234, 0x64E1, 0x73B3, 0x81FA, 0x888B, 0x8CB8, 0x968A, 0x9EDB, 0x5B85, 0x5FB7, 0x60B3, 0x5012, 0x5200, 0x5230, 0x5716, 0x5835, 0x5857, 0x5C0E, 0x5C60, 0x5CF6, 0x5D8B, 0x5EA6, 0x5F92, 0x60BC, 0x6311, 0x6389, 0x6417, 0x6843, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x68F9, 0x6AC2, 0x6DD8, 0x6E21, 0x6ED4, 0x6FE4, 0x71FE, 0x76DC, 0x7779, 0x79B1, 0x7A3B, 0x8404, 0x89A9, 0x8CED, 0x8DF3, 0x8E48, 0x9003, 0x9014, 0x9053, 0x90FD, 0x934D, 0x9676, 0x97DC, 0x6BD2, 0x7006, 0x7258, 0x72A2, 0x7368, 0x7763, 0x79BF, 0x7BE4, 0x7E9B, 0x8B80, 0x58A9, 0x60C7, 0x6566, 0x65FD, 0x66BE, 0x6C8C, 0x711E, 0x71C9, 0x8C5A, 0x9813, 0x4E6D, 0x7A81, 0x4EDD, 0x51AC, 0x51CD, 0x52D5, 0x540C, 0x61A7, 0x6771, 0x6850, 0x68DF, 0x6D1E, 0x6F7C, 0x75BC, 0x77B3, 0x7AE5, 0x80F4, 0x8463, 0x9285, 0x515C, 0x6597, 0x675C, 0x6793, 0x75D8, 0x7AC7, 0x8373, 0xF95A, 0x8C46, 0x9017, 0x982D, 0x5C6F, 0x81C0, 0x829A, 0x9041, 0x906F, 0x920D, 0x5F97, 0x5D9D, 0x6A59, 0x71C8, 0x767B, 0x7B49, 0x85E4, 0x8B04, 0x9127, 0x9A30, 0x5587, 0x61F6, 0xF95B, 0x7669, 0x7F85, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x863F, 0x87BA, 0x88F8, 0x908F, 0xF95C, 0x6D1B, 0x70D9, 0x73DE, 0x7D61, 0x843D, 0xF95D, 0x916A, 0x99F1, 0xF95E, 0x4E82, 0x5375, 0x6B04, 0x6B12, 0x703E, 0x721B, 0x862D, 0x9E1E, 0x524C, 0x8FA3, 0x5D50, 0x64E5, 0x652C, 0x6B16, 0x6FEB, 0x7C43, 0x7E9C, 0x85CD, 0x8964, 0x89BD, 0x62C9, 0x81D8, 0x881F, 0x5ECA, 0x6717, 0x6D6A, 0x72FC, 0x7405, 0x746F, 0x8782, 0x90DE, 0x4F86, 0x5D0D, 0x5FA0, 0x840A, 0x51B7, 0x63A0, 0x7565, 0x4EAE, 0x5006, 0x5169, 0x51C9, 0x6881, 0x6A11, 0x7CAE, 0x7CB1, 0x7CE7, 0x826F, 0x8AD2, 0x8F1B, 0x91CF, 0x4FB6, 0x5137, 0x52F5, 0x5442, 0x5EEC, 0x616E, 0x623E, 0x65C5, 0x6ADA, 0x6FFE, 0x792A, 0x85DC, 0x8823, 0x95AD, 0x9A62, 0x9A6A, 0x9E97, 0x9ECE, 0x529B, 0x66C6, 0x6B77, 0x701D, 0x792B, 0x8F62, 0x9742, 0x6190, 0x6200, 0x6523, 0x6F23, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x7149, 0x7489, 0x7DF4, 0x806F, 0x84EE, 0x8F26, 0x9023, 0x934A, 0x51BD, 0x5217, 0x52A3, 0x6D0C, 0x70C8, 0x88C2, 0x5EC9, 0x6582, 0x6BAE, 0x6FC2, 0x7C3E, 0x7375, 0x4EE4, 0x4F36, 0x56F9, 0xF95F, 0x5CBA, 0x5DBA, 0x601C, 0x73B2, 0x7B2D, 0x7F9A, 0x7FCE, 0x8046, 0x901E, 0x9234, 0x96F6, 0x9748, 0x9818, 0x9F61, 0x4F8B, 0x6FA7, 0x79AE, 0x91B4, 0x96B7, 0x52DE, 0xF960, 0x6488, 0x64C4, 0x6AD3, 0x6F5E, 0x7018, 0x7210, 0x76E7, 0x8001, 0x8606, 0x865C, 0x8DEF, 0x8F05, 0x9732, 0x9B6F, 0x9DFA, 0x9E75, 0x788C, 0x797F, 0x7DA0, 0x83C9, 0x9304, 0x9E7F, 0x9E93, 0x8AD6, 0x58DF, 0x5F04, 0x6727, 0x7027, 0x74CF, 0x7C60, 0x807E, 0x5121, 0x7028, 0x7262, 0x78CA, 0x8CC2, 0x8CDA, 0x8CF4, 0x96F7, 0x4E86, 0x50DA, 0x5BEE, 0x5ED6, 0x6599, 0x71CE, 0x7642, 0x77AD, 0x804A, 0x84FC, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x907C, 0x9B27, 0x9F8D, 0x58D8, 0x5A41, 0x5C62, 0x6A13, 0x6DDA, 0x6F0F, 0x763B, 0x7D2F, 0x7E37, 0x851E, 0x8938, 0x93E4, 0x964B, 0x5289, 0x65D2, 0x67F3, 0x69B4, 0x6D41, 0x6E9C, 0x700F, 0x7409, 0x7460, 0x7559, 0x7624, 0x786B, 0x8B2C, 0x985E, 0x516D, 0x622E, 0x9678, 0x4F96, 0x502B, 0x5D19, 0x6DEA, 0x7DB8, 0x8F2A, 0x5F8B, 0x6144, 0x6817, 0xF961, 0x9686, 0x52D2, 0x808B, 0x51DC, 0x51CC, 0x695E, 0x7A1C, 0x7DBE, 0x83F1, 0x9675, 0x4FDA, 0x5229, 0x5398, 0x540F, 0x550E, 0x5C65, 0x60A7, 0x674E, 0x68A8, 0x6D6C, 0x7281, 0x72F8, 0x7406, 0x7483, 0xF962, 0x75E2, 0x7C6C, 0x7F79, 0x7FB8, 0x8389, 0x88CF, 0x88E1, 0x91CC, 0x91D0, 0x96E2, 0x9BC9, 0x541D, 0x6F7E, 0x71D0, 0x7498, 0x85FA, 0x8EAA, 0x96A3, 0x9C57, 0x9E9F, 0x6797, 0x6DCB, 0x7433, 0x81E8, 0x9716, 0x782C, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x7ACB, 0x7B20, 0x7C92, 0x6469, 0x746A, 0x75F2, 0x78BC, 0x78E8, 0x99AC, 0x9B54, 0x9EBB, 0x5BDE, 0x5E55, 0x6F20, 0x819C, 0x83AB, 0x9088, 0x4E07, 0x534D, 0x5A29, 0x5DD2, 0x5F4E, 0x6162, 0x633D, 0x6669, 0x66FC, 0x6EFF, 0x6F2B, 0x7063, 0x779E, 0x842C, 0x8513, 0x883B, 0x8F13, 0x9945, 0x9C3B, 0x551C, 0x62B9, 0x672B, 0x6CAB, 0x8309, 0x896A, 0x977A, 0x4EA1, 0x5984, 0x5FD8, 0x5FD9, 0x671B, 0x7DB2, 0x7F54, 0x8292, 0x832B, 0x83BD, 0x8F1E, 0x9099, 0x57CB, 0x59B9, 0x5A92, 0x5BD0, 0x6627, 0x679A, 0x6885, 0x6BCF, 0x7164, 0x7F75, 0x8CB7, 0x8CE3, 0x9081, 0x9B45, 0x8108, 0x8C8A, 0x964C, 0x9A40, 0x9EA5, 0x5B5F, 0x6C13, 0x731B, 0x76F2, 0x76DF, 0x840C, 0x51AA, 0x8993, 0x514D, 0x5195, 0x52C9, 0x68C9, 0x6C94, 0x7704, 0x7720, 0x7DBF, 0x7DEC, 0x9762, 0x9EB5, 0x6EC5, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8511, 0x51A5, 0x540D, 0x547D, 0x660E, 0x669D, 0x6927, 0x6E9F, 0x76BF, 0x7791, 0x8317, 0x84C2, 0x879F, 0x9169, 0x9298, 0x9CF4, 0x8882, 0x4FAE, 0x5192, 0x52DF, 0x59C6, 0x5E3D, 0x6155, 0x6478, 0x6479, 0x66AE, 0x67D0, 0x6A21, 0x6BCD, 0x6BDB, 0x725F, 0x7261, 0x7441, 0x7738, 0x77DB, 0x8017, 0x82BC, 0x8305, 0x8B00, 0x8B28, 0x8C8C, 0x6728, 0x6C90, 0x7267, 0x76EE, 0x7766, 0x7A46, 0x9DA9, 0x6B7F, 0x6C92, 0x5922, 0x6726, 0x8499, 0x536F, 0x5893, 0x5999, 0x5EDF, 0x63CF, 0x6634, 0x6773, 0x6E3A, 0x732B, 0x7AD7, 0x82D7, 0x9328, 0x52D9, 0x5DEB, 0x61AE, 0x61CB, 0x620A, 0x62C7, 0x64AB, 0x65E0, 0x6959, 0x6B66, 0x6BCB, 0x7121, 0x73F7, 0x755D, 0x7E46, 0x821E, 0x8302, 0x856A, 0x8AA3, 0x8CBF, 0x9727, 0x9D61, 0x58A8, 0x9ED8, 0x5011, 0x520E, 0x543B, 0x554F, 0x6587, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x6C76, 0x7D0A, 0x7D0B, 0x805E, 0x868A, 0x9580, 0x96EF, 0x52FF, 0x6C95, 0x7269, 0x5473, 0x5A9A, 0x5C3E, 0x5D4B, 0x5F4C, 0x5FAE, 0x672A, 0x68B6, 0x6963, 0x6E3C, 0x6E44, 0x7709, 0x7C73, 0x7F8E, 0x8587, 0x8B0E, 0x8FF7, 0x9761, 0x9EF4, 0x5CB7, 0x60B6, 0x610D, 0x61AB, 0x654F, 0x65FB, 0x65FC, 0x6C11, 0x6CEF, 0x739F, 0x73C9, 0x7DE1, 0x9594, 0x5BC6, 0x871C, 0x8B10, 0x525D, 0x535A, 0x62CD, 0x640F, 0x64B2, 0x6734, 0x6A38, 0x6CCA, 0x73C0, 0x749E, 0x7B94, 0x7C95, 0x7E1B, 0x818A, 0x8236, 0x8584, 0x8FEB, 0x96F9, 0x99C1, 0x4F34, 0x534A, 0x53CD, 0x53DB, 0x62CC, 0x642C, 0x6500, 0x6591, 0x69C3, 0x6CEE, 0x6F58, 0x73ED, 0x7554, 0x7622, 0x76E4, 0x76FC, 0x78D0, 0x78FB, 0x792C, 0x7D46, 0x822C, 0x87E0, 0x8FD4, 0x9812, 0x98EF, 0x52C3, 0x62D4, 0x64A5, 0x6E24, 0x6F51, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x767C, 0x8DCB, 0x91B1, 0x9262, 0x9AEE, 0x9B43, 0x5023, 0x508D, 0x574A, 0x59A8, 0x5C28, 0x5E47, 0x5F77, 0x623F, 0x653E, 0x65B9, 0x65C1, 0x6609, 0x678B, 0x699C, 0x6EC2, 0x78C5, 0x7D21, 0x80AA, 0x8180, 0x822B, 0x82B3, 0x84A1, 0x868C, 0x8A2A, 0x8B17, 0x90A6, 0x9632, 0x9F90, 0x500D, 0x4FF3, 0xF963, 0x57F9, 0x5F98, 0x62DC, 0x6392, 0x676F, 0x6E43, 0x7119, 0x76C3, 0x80CC, 0x80DA, 0x88F4, 0x88F5, 0x8919, 0x8CE0, 0x8F29, 0x914D, 0x966A, 0x4F2F, 0x4F70, 0x5E1B, 0x67CF, 0x6822, 0x767D, 0x767E, 0x9B44, 0x5E61, 0x6A0A, 0x7169, 0x71D4, 0x756A, 0xF964, 0x7E41, 0x8543, 0x85E9, 0x98DC, 0x4F10, 0x7B4F, 0x7F70, 0x95A5, 0x51E1, 0x5E06, 0x68B5, 0x6C3E, 0x6C4E, 0x6CDB, 0x72AF, 0x7BC4, 0x8303, 0x6CD5, 0x743A, 0x50FB, 0x5288, 0x58C1, 0x64D8, 0x6A97, 0x74A7, 0x7656, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x78A7, 0x8617, 0x95E2, 0x9739, 0xF965, 0x535E, 0x5F01, 0x8B8A, 0x8FA8, 0x8FAF, 0x908A, 0x5225, 0x77A5, 0x9C49, 0x9F08, 0x4E19, 0x5002, 0x5175, 0x5C5B, 0x5E77, 0x661E, 0x663A, 0x67C4, 0x68C5, 0x70B3, 0x7501, 0x75C5, 0x79C9, 0x7ADD, 0x8F27, 0x9920, 0x9A08, 0x4FDD, 0x5821, 0x5831, 0x5BF6, 0x666E, 0x6B65, 0x6D11, 0x6E7A, 0x6F7D, 0x73E4, 0x752B, 0x83E9, 0x88DC, 0x8913, 0x8B5C, 0x8F14, 0x4F0F, 0x50D5, 0x5310, 0x535C, 0x5B93, 0x5FA9, 0x670D, 0x798F, 0x8179, 0x832F, 0x8514, 0x8907, 0x8986, 0x8F39, 0x8F3B, 0x99A5, 0x9C12, 0x672C, 0x4E76, 0x4FF8, 0x5949, 0x5C01, 0x5CEF, 0x5CF0, 0x6367, 0x68D2, 0x70FD, 0x71A2, 0x742B, 0x7E2B, 0x84EC, 0x8702, 0x9022, 0x92D2, 0x9CF3, 0x4E0D, 0x4ED8, 0x4FEF, 0x5085, 0x5256, 0x526F, 0x5426, 0x5490, 0x57E0, 0x592B, 0x5A66, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x5B5A, 0x5B75, 0x5BCC, 0x5E9C, 0xF966, 0x6276, 0x6577, 0x65A7, 0x6D6E, 0x6EA5, 0x7236, 0x7B26, 0x7C3F, 0x7F36, 0x8150, 0x8151, 0x819A, 0x8240, 0x8299, 0x83A9, 0x8A03, 0x8CA0, 0x8CE6, 0x8CFB, 0x8D74, 0x8DBA, 0x90E8, 0x91DC, 0x961C, 0x9644, 0x99D9, 0x9CE7, 0x5317, 0x5206, 0x5429, 0x5674, 0x58B3, 0x5954, 0x596E, 0x5FFF, 0x61A4, 0x626E, 0x6610, 0x6C7E, 0x711A, 0x76C6, 0x7C89, 0x7CDE, 0x7D1B, 0x82AC, 0x8CC1, 0x96F0, 0xF967, 0x4F5B, 0x5F17, 0x5F7F, 0x62C2, 0x5D29, 0x670B, 0x68DA, 0x787C, 0x7E43, 0x9D6C, 0x4E15, 0x5099, 0x5315, 0x532A, 0x5351, 0x5983, 0x5A62, 0x5E87, 0x60B2, 0x618A, 0x6249, 0x6279, 0x6590, 0x6787, 0x69A7, 0x6BD4, 0x6BD6, 0x6BD7, 0x6BD8, 0x6CB8, 0xF968, 0x7435, 0x75FA, 0x7812, 0x7891, 0x79D5, 0x79D8, 0x7C83, 0x7DCB, 0x7FE1, 0x80A5, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x813E, 0x81C2, 0x83F2, 0x871A, 0x88E8, 0x8AB9, 0x8B6C, 0x8CBB, 0x9119, 0x975E, 0x98DB, 0x9F3B, 0x56AC, 0x5B2A, 0x5F6C, 0x658C, 0x6AB3, 0x6BAF, 0x6D5C, 0x6FF1, 0x7015, 0x725D, 0x73AD, 0x8CA7, 0x8CD3, 0x983B, 0x6191, 0x6C37, 0x8058, 0x9A01, 0x4E4D, 0x4E8B, 0x4E9B, 0x4ED5, 0x4F3A, 0x4F3C, 0x4F7F, 0x4FDF, 0x50FF, 0x53F2, 0x53F8, 0x5506, 0x55E3, 0x56DB, 0x58EB, 0x5962, 0x5A11, 0x5BEB, 0x5BFA, 0x5C04, 0x5DF3, 0x5E2B, 0x5F99, 0x601D, 0x6368, 0x659C, 0x65AF, 0x67F6, 0x67FB, 0x68AD, 0x6B7B, 0x6C99, 0x6CD7, 0x6E23, 0x7009, 0x7345, 0x7802, 0x793E, 0x7940, 0x7960, 0x79C1, 0x7BE9, 0x7D17, 0x7D72, 0x8086, 0x820D, 0x838E, 0x84D1, 0x86C7, 0x88DF, 0x8A50, 0x8A5E, 0x8B1D, 0x8CDC, 0x8D66, 0x8FAD, 0x90AA, 0x98FC, 0x99DF, 0x9E9D, 0x524A, 0xF969, 0x6714, 0xF96A, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x5098, 0x522A, 0x5C71, 0x6563, 0x6C55, 0x73CA, 0x7523, 0x759D, 0x7B97, 0x849C, 0x9178, 0x9730, 0x4E77, 0x6492, 0x6BBA, 0x715E, 0x85A9, 0x4E09, 0xF96B, 0x6749, 0x68EE, 0x6E17, 0x829F, 0x8518, 0x886B, 0x63F7, 0x6F81, 0x9212, 0x98AF, 0x4E0A, 0x50B7, 0x50CF, 0x511F, 0x5546, 0x55AA, 0x5617, 0x5B40, 0x5C19, 0x5CE0, 0x5E38, 0x5E8A, 0x5EA0, 0x5EC2, 0x60F3, 0x6851, 0x6A61, 0x6E58, 0x723D, 0x7240, 0x72C0, 0x76F8, 0x7965, 0x7BB1, 0x7FD4, 0x88F3, 0x89F4, 0x8A73, 0x8C61, 0x8CDE, 0x971C, 0x585E, 0x74BD, 0x8CFD, 0x55C7, 0xF96C, 0x7A61, 0x7D22, 0x8272, 0x7272, 0x751F, 0x7525, 0xF96D, 0x7B19, 0x5885, 0x58FB, 0x5DBC, 0x5E8F, 0x5EB6, 0x5F90, 0x6055, 0x6292, 0x637F, 0x654D, 0x6691, 0x66D9, 0x66F8, 0x6816, 0x68F2, 0x7280, 0x745E, 0x7B6E, 0x7D6E, 0x7DD6, 0x7F72, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x80E5, 0x8212, 0x85AF, 0x897F, 0x8A93, 0x901D, 0x92E4, 0x9ECD, 0x9F20, 0x5915, 0x596D, 0x5E2D, 0x60DC, 0x6614, 0x6673, 0x6790, 0x6C50, 0x6DC5, 0x6F5F, 0x77F3, 0x78A9, 0x84C6, 0x91CB, 0x932B, 0x4ED9, 0x50CA, 0x5148, 0x5584, 0x5B0B, 0x5BA3, 0x6247, 0x657E, 0x65CB, 0x6E32, 0x717D, 0x7401, 0x7444, 0x7487, 0x74BF, 0x766C, 0x79AA, 0x7DDA, 0x7E55, 0x7FA8, 0x817A, 0x81B3, 0x8239, 0x861A, 0x87EC, 0x8A75, 0x8DE3, 0x9078, 0x9291, 0x9425, 0x994D, 0x9BAE, 0x5368, 0x5C51, 0x6954, 0x6CC4, 0x6D29, 0x6E2B, 0x820C, 0x859B, 0x893B, 0x8A2D, 0x8AAA, 0x96EA, 0x9F67, 0x5261, 0x66B9, 0x6BB2, 0x7E96, 0x87FE, 0x8D0D, 0x9583, 0x965D, 0x651D, 0x6D89, 0x71EE, 0xF96E, 0x57CE, 0x59D3, 0x5BAC, 0x6027, 0x60FA, 0x6210, 0x661F, 0x665F, 0x7329, 0x73F9, 0x76DB, 0x7701, 0x7B6C, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8056, 0x8072, 0x8165, 0x8AA0, 0x9192, 0x4E16, 0x52E2, 0x6B72, 0x6D17, 0x7A05, 0x7B39, 0x7D30, 0xF96F, 0x8CB0, 0x53EC, 0x562F, 0x5851, 0x5BB5, 0x5C0F, 0x5C11, 0x5DE2, 0x6240, 0x6383, 0x6414, 0x662D, 0x68B3, 0x6CBC, 0x6D88, 0x6EAF, 0x701F, 0x70A4, 0x71D2, 0x7526, 0x758F, 0x758E, 0x7619, 0x7B11, 0x7BE0, 0x7C2B, 0x7D20, 0x7D39, 0x852C, 0x856D, 0x8607, 0x8A34, 0x900D, 0x9061, 0x90B5, 0x92B7, 0x97F6, 0x9A37, 0x4FD7, 0x5C6C, 0x675F, 0x6D91, 0x7C9F, 0x7E8C, 0x8B16, 0x8D16, 0x901F, 0x5B6B, 0x5DFD, 0x640D, 0x84C0, 0x905C, 0x98E1, 0x7387, 0x5B8B, 0x609A, 0x677E, 0x6DDE, 0x8A1F, 0x8AA6, 0x9001, 0x980C, 0x5237, 0xF970, 0x7051, 0x788E, 0x9396, 0x8870, 0x91D7, 0x4FEE, 0x53D7, 0x55FD, 0x56DA, 0x5782, 0x58FD, 0x5AC2, 0x5B88, 0x5CAB, 0x5CC0, 0x5E25, 0x6101, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x620D, 0x624B, 0x6388, 0x641C, 0x6536, 0x6578, 0x6A39, 0x6B8A, 0x6C34, 0x6D19, 0x6F31, 0x71E7, 0x72E9, 0x7378, 0x7407, 0x74B2, 0x7626, 0x7761, 0x79C0, 0x7A57, 0x7AEA, 0x7CB9, 0x7D8F, 0x7DAC, 0x7E61, 0x7F9E, 0x8129, 0x8331, 0x8490, 0x84DA, 0x85EA, 0x8896, 0x8AB0, 0x8B90, 0x8F38, 0x9042, 0x9083, 0x916C, 0x9296, 0x92B9, 0x968B, 0x96A7, 0x96A8, 0x96D6, 0x9700, 0x9808, 0x9996, 0x9AD3, 0x9B1A, 0x53D4, 0x587E, 0x5919, 0x5B70, 0x5BBF, 0x6DD1, 0x6F5A, 0x719F, 0x7421, 0x74B9, 0x8085, 0x83FD, 0x5DE1, 0x5F87, 0x5FAA, 0x6042, 0x65EC, 0x6812, 0x696F, 0x6A53, 0x6B89, 0x6D35, 0x6DF3, 0x73E3, 0x76FE, 0x77AC, 0x7B4D, 0x7D14, 0x8123, 0x821C, 0x8340, 0x84F4, 0x8563, 0x8A62, 0x8AC4, 0x9187, 0x931E, 0x9806, 0x99B4, 0x620C, 0x8853, 0x8FF0, 0x9265, 0x5D07, 0x5D27, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x5D69, 0x745F, 0x819D, 0x8768, 0x6FD5, 0x62FE, 0x7FD2, 0x8936, 0x8972, 0x4E1E, 0x4E58, 0x50E7, 0x52DD, 0x5347, 0x627F, 0x6607, 0x7E69, 0x8805, 0x965E, 0x4F8D, 0x5319, 0x5636, 0x59CB, 0x5AA4, 0x5C38, 0x5C4E, 0x5C4D, 0x5E02, 0x5F11, 0x6043, 0x65BD, 0x662F, 0x6642, 0x67BE, 0x67F4, 0x731C, 0x77E2, 0x793A, 0x7FC5, 0x8494, 0x84CD, 0x8996, 0x8A66, 0x8A69, 0x8AE1, 0x8C55, 0x8C7A, 0x57F4, 0x5BD4, 0x5F0F, 0x606F, 0x62ED, 0x690D, 0x6B96, 0x6E5C, 0x7184, 0x7BD2, 0x8755, 0x8B58, 0x8EFE, 0x98DF, 0x98FE, 0x4F38, 0x4F81, 0x4FE1, 0x547B, 0x5A20, 0x5BB8, 0x613C, 0x65B0, 0x6668, 0x71FC, 0x7533, 0x795E, 0x7D33, 0x814E, 0x81E3, 0x8398, 0x85AA, 0x85CE, 0x8703, 0x8A0A, 0x8EAB, 0x8F9B, 0xF971, 0x8FC5, 0x5931, 0x5BA4, 0x5BE6, 0x6089, 0x5BE9, 0x5C0B, 0x5FC3, 0x6C81, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xF972, 0x6DF1, 0x700B, 0x751A, 0x82AF, 0x8AF6, 0x4EC0, 0x5341, 0xF973, 0x96D9, 0x6C0F, 0x4E9E, 0x4FC4, 0x5152, 0x555E, 0x5A25, 0x5CE8, 0x6211, 0x7259, 0x82BD, 0x83AA, 0x86FE, 0x8859, 0x8A1D, 0x963F, 0x96C5, 0x9913, 0x9D09, 0x9D5D, 0x580A, 0x5CB3, 0x5DBD, 0x5E44, 0x60E1, 0x6115, 0x63E1, 0x6A02, 0x6E25, 0x9102, 0x9354, 0x984E, 0x9C10, 0x9F77, 0x5B89, 0x5CB8, 0x6309, 0x664F, 0x6848, 0x773C, 0x96C1, 0x978D, 0x9854, 0x9B9F, 0x65A1, 0x8B01, 0x8ECB, 0x95BC, 0x5535, 0x5CA9, 0x5DD6, 0x5EB5, 0x6697, 0x764C, 0x83F4, 0x95C7, 0x58D3, 0x62BC, 0x72CE, 0x9D28, 0x4EF0, 0x592E, 0x600F, 0x663B, 0x6B83, 0x79E7, 0x9D26, 0x5393, 0x54C0, 0x57C3, 0x5D16, 0x611B, 0x66D6, 0x6DAF, 0x788D, 0x827E, 0x9698, 0x9744, 0x5384, 0x627C, 0x6396, 0x6DB2, 0x7E0A, 0x814B, 0x984D, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x6AFB, 0x7F4C, 0x9DAF, 0x9E1A, 0x4E5F, 0x503B, 0x51B6, 0x591C, 0x60F9, 0x63F6, 0x6930, 0x723A, 0x8036, 0xF974, 0x91CE, 0x5F31, 0xF975, 0xF976, 0x7D04, 0x82E5, 0x846F, 0x84BB, 0x85E5, 0x8E8D, 0xF977, 0x4F6F, 0xF978, 0xF979, 0x58E4, 0x5B43, 0x6059, 0x63DA, 0x6518, 0x656D, 0x6698, 0xF97A, 0x694A, 0x6A23, 0x6D0B, 0x7001, 0x716C, 0x75D2, 0x760D, 0x79B3, 0x7A70, 0xF97B, 0x7F8A, 0xF97C, 0x8944, 0xF97D, 0x8B93, 0x91C0, 0x967D, 0xF97E, 0x990A, 0x5704, 0x5FA1, 0x65BC, 0x6F01, 0x7600, 0x79A6, 0x8A9E, 0x99AD, 0x9B5A, 0x9F6C, 0x5104, 0x61B6, 0x6291, 0x6A8D, 0x81C6, 0x5043, 0x5830, 0x5F66, 0x7109, 0x8A00, 0x8AFA, 0x5B7C, 0x8616, 0x4FFA, 0x513C, 0x56B4, 0x5944, 0x63A9, 0x6DF9, 0x5DAA, 0x696D, 0x5186, 0x4E88, 0x4F59, 0xF97F, 0xF980, 0xF981, 0x5982, 0xF982, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xF983, 0x6B5F, 0x6C5D, 0xF984, 0x74B5, 0x7916, 0xF985, 0x8207, 0x8245, 0x8339, 0x8F3F, 0x8F5D, 0xF986, 0x9918, 0xF987, 0xF988, 0xF989, 0x4EA6, 0xF98A, 0x57DF, 0x5F79, 0x6613, 0xF98B, 0xF98C, 0x75AB, 0x7E79, 0x8B6F, 0xF98D, 0x9006, 0x9A5B, 0x56A5, 0x5827, 0x59F8, 0x5A1F, 0x5BB4, 0xF98E, 0x5EF6, 0xF98F, 0xF990, 0x6350, 0x633B, 0xF991, 0x693D, 0x6C87, 0x6CBF, 0x6D8E, 0x6D93, 0x6DF5, 0x6F14, 0xF992, 0x70DF, 0x7136, 0x7159, 0xF993, 0x71C3, 0x71D5, 0xF994, 0x784F, 0x786F, 0xF995, 0x7B75, 0x7DE3, 0xF996, 0x7E2F, 0xF997, 0x884D, 0x8EDF, 0xF998, 0xF999, 0xF99A, 0x925B, 0xF99B, 0x9CF6, 0xF99C, 0xF99D, 0xF99E, 0x6085, 0x6D85, 0xF99F, 0x71B1, 0xF9A0, 0xF9A1, 0x95B1, 0x53AD, 0xF9A2, 0xF9A3, 0xF9A4, 0x67D3, 0xF9A5, 0x708E, 0x7130, 0x7430, 0x8276, 0x82D2, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xF9A6, 0x95BB, 0x9AE5, 0x9E7D, 0x66C4, 0xF9A7, 0x71C1, 0x8449, 0xF9A8, 0xF9A9, 0x584B, 0xF9AA, 0xF9AB, 0x5DB8, 0x5F71, 0xF9AC, 0x6620, 0x668E, 0x6979, 0x69AE, 0x6C38, 0x6CF3, 0x6E36, 0x6F41, 0x6FDA, 0x701B, 0x702F, 0x7150, 0x71DF, 0x7370, 0xF9AD, 0x745B, 0xF9AE, 0x74D4, 0x76C8, 0x7A4E, 0x7E93, 0xF9AF, 0xF9B0, 0x82F1, 0x8A60, 0x8FCE, 0xF9B1, 0x9348, 0xF9B2, 0x9719, 0xF9B3, 0xF9B4, 0x4E42, 0x502A, 0xF9B5, 0x5208, 0x53E1, 0x66F3, 0x6C6D, 0x6FCA, 0x730A, 0x777F, 0x7A62, 0x82AE, 0x85DD, 0x8602, 0xF9B6, 0x88D4, 0x8A63, 0x8B7D, 0x8C6B, 0xF9B7, 0x92B3, 0xF9B8, 0x9713, 0x9810, 0x4E94, 0x4F0D, 0x4FC9, 0x50B2, 0x5348, 0x543E, 0x5433, 0x55DA, 0x5862, 0x58BA, 0x5967, 0x5A1B, 0x5BE4, 0x609F, 0xF9B9, 0x61CA, 0x6556, 0x65FF, 0x6664, 0x68A7, 0x6C5A, 0x6FB3, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x70CF, 0x71AC, 0x7352, 0x7B7D, 0x8708, 0x8AA4, 0x9C32, 0x9F07, 0x5C4B, 0x6C83, 0x7344, 0x7389, 0x923A, 0x6EAB, 0x7465, 0x761F, 0x7A69, 0x7E15, 0x860A, 0x5140, 0x58C5, 0x64C1, 0x74EE, 0x7515, 0x7670, 0x7FC1, 0x9095, 0x96CD, 0x9954, 0x6E26, 0x74E6, 0x7AA9, 0x7AAA, 0x81E5, 0x86D9, 0x8778, 0x8A1B, 0x5A49, 0x5B8C, 0x5B9B, 0x68A1, 0x6900, 0x6D63, 0x73A9, 0x7413, 0x742C, 0x7897, 0x7DE9, 0x7FEB, 0x8118, 0x8155, 0x839E, 0x8C4C, 0x962E, 0x9811, 0x66F0, 0x5F80, 0x65FA, 0x6789, 0x6C6A, 0x738B, 0x502D, 0x5A03, 0x6B6A, 0x77EE, 0x5916, 0x5D6C, 0x5DCD, 0x7325, 0x754F, 0xF9BA, 0xF9BB, 0x50E5, 0x51F9, 0x582F, 0x592D, 0x5996, 0x59DA, 0x5BE5, 0xF9BC, 0xF9BD, 0x5DA2, 0x62D7, 0x6416, 0x6493, 0x64FE, 0xF9BE, 0x66DC, 0xF9BF, 0x6A48, 0xF9C0, 0x71FF, 0x7464, 0xF9C1, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x7A88, 0x7AAF, 0x7E47, 0x7E5E, 0x8000, 0x8170, 0xF9C2, 0x87EF, 0x8981, 0x8B20, 0x9059, 0xF9C3, 0x9080, 0x9952, 0x617E, 0x6B32, 0x6D74, 0x7E1F, 0x8925, 0x8FB1, 0x4FD1, 0x50AD, 0x5197, 0x52C7, 0x57C7, 0x5889, 0x5BB9, 0x5EB8, 0x6142, 0x6995, 0x6D8C, 0x6E67, 0x6EB6, 0x7194, 0x7462, 0x7528, 0x752C, 0x8073, 0x8338, 0x84C9, 0x8E0A, 0x9394, 0x93DE, 0xF9C4, 0x4E8E, 0x4F51, 0x5076, 0x512A, 0x53C8, 0x53CB, 0x53F3, 0x5B87, 0x5BD3, 0x5C24, 0x611A, 0x6182, 0x65F4, 0x725B, 0x7397, 0x7440, 0x76C2, 0x7950, 0x7991, 0x79B9, 0x7D06, 0x7FBD, 0x828B, 0x85D5, 0x865E, 0x8FC2, 0x9047, 0x90F5, 0x91EA, 0x9685, 0x96E8, 0x96E9, 0x52D6, 0x5F67, 0x65ED, 0x6631, 0x682F, 0x715C, 0x7A36, 0x90C1, 0x980A, 0x4E91, 0xF9C5, 0x6A52, 0x6B9E, 0x6F90, 0x7189, 0x8018, 0x82B8, 0x8553, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x904B, 0x9695, 0x96F2, 0x97FB, 0x851A, 0x9B31, 0x4E90, 0x718A, 0x96C4, 0x5143, 0x539F, 0x54E1, 0x5713, 0x5712, 0x57A3, 0x5A9B, 0x5AC4, 0x5BC3, 0x6028, 0x613F, 0x63F4, 0x6C85, 0x6D39, 0x6E72, 0x6E90, 0x7230, 0x733F, 0x7457, 0x82D1, 0x8881, 0x8F45, 0x9060, 0xF9C6, 0x9662, 0x9858, 0x9D1B, 0x6708, 0x8D8A, 0x925E, 0x4F4D, 0x5049, 0x50DE, 0x5371, 0x570D, 0x59D4, 0x5A01, 0x5C09, 0x6170, 0x6690, 0x6E2D, 0x7232, 0x744B, 0x7DEF, 0x80C3, 0x840E, 0x8466, 0x853F, 0x875F, 0x885B, 0x8918, 0x8B02, 0x9055, 0x97CB, 0x9B4F, 0x4E73, 0x4F91, 0x5112, 0x516A, 0xF9C7, 0x552F, 0x55A9, 0x5B7A, 0x5BA5, 0x5E7C, 0x5E7D, 0x5EBE, 0x60A0, 0x60DF, 0x6108, 0x6109, 0x63C4, 0x6538, 0x6709, 0xF9C8, 0x67D4, 0x67DA, 0xF9C9, 0x6961, 0x6962, 0x6CB9, 0x6D27, 0xF9CA, 0x6E38, 0xF9CB, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x6FE1, 0x7336, 0x7337, 0xF9CC, 0x745C, 0x7531, 0xF9CD, 0x7652, 0xF9CE, 0xF9CF, 0x7DAD, 0x81FE, 0x8438, 0x88D5, 0x8A98, 0x8ADB, 0x8AED, 0x8E30, 0x8E42, 0x904A, 0x903E, 0x907A, 0x9149, 0x91C9, 0x936E, 0xF9D0, 0xF9D1, 0x5809, 0xF9D2, 0x6BD3, 0x8089, 0x80B2, 0xF9D3, 0xF9D4, 0x5141, 0x596B, 0x5C39, 0xF9D5, 0xF9D6, 0x6F64, 0x73A7, 0x80E4, 0x8D07, 0xF9D7, 0x9217, 0x958F, 0xF9D8, 0xF9D9, 0xF9DA, 0xF9DB, 0x807F, 0x620E, 0x701C, 0x7D68, 0x878D, 0xF9DC, 0x57A0, 0x6069, 0x6147, 0x6BB7, 0x8ABE, 0x9280, 0x96B1, 0x4E59, 0x541F, 0x6DEB, 0x852D, 0x9670, 0x97F3, 0x98EE, 0x63D6, 0x6CE3, 0x9091, 0x51DD, 0x61C9, 0x81BA, 0x9DF9, 0x4F9D, 0x501A, 0x5100, 0x5B9C, 0x610F, 0x61FF, 0x64EC, 0x6905, 0x6BC5, 0x7591, 0x77E3, 0x7FA9, 0x8264, 0x858F, 0x87FB, 0x8863, 0x8ABC, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8B70, 0x91AB, 0x4E8C, 0x4EE5, 0x4F0A, 0xF9DD, 0xF9DE, 0x5937, 0x59E8, 0xF9DF, 0x5DF2, 0x5F1B, 0x5F5B, 0x6021, 0xF9E0, 0xF9E1, 0xF9E2, 0xF9E3, 0x723E, 0x73E5, 0xF9E4, 0x7570, 0x75CD, 0xF9E5, 0x79FB, 0xF9E6, 0x800C, 0x8033, 0x8084, 0x82E1, 0x8351, 0xF9E7, 0xF9E8, 0x8CBD, 0x8CB3, 0x9087, 0xF9E9, 0xF9EA, 0x98F4, 0x990C, 0xF9EB, 0xF9EC, 0x7037, 0x76CA, 0x7FCA, 0x7FCC, 0x7FFC, 0x8B1A, 0x4EBA, 0x4EC1, 0x5203, 0x5370, 0xF9ED, 0x54BD, 0x56E0, 0x59FB, 0x5BC5, 0x5F15, 0x5FCD, 0x6E6E, 0xF9EE, 0xF9EF, 0x7D6A, 0x8335, 0xF9F0, 0x8693, 0x8A8D, 0xF9F1, 0x976D, 0x9777, 0xF9F2, 0xF9F3, 0x4E00, 0x4F5A, 0x4F7E, 0x58F9, 0x65E5, 0x6EA2, 0x9038, 0x93B0, 0x99B9, 0x4EFB, 0x58EC, 0x598A, 0x59D9, 0x6041, 0xF9F4, 0xF9F5, 0x7A14, 0xF9F6, 0x834F, 0x8CC3, 0x5165, 0x5344, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xF9F7, 0xF9F8, 0xF9F9, 0x4ECD, 0x5269, 0x5B55, 0x82BF, 0x4ED4, 0x523A, 0x54A8, 0x59C9, 0x59FF, 0x5B50, 0x5B57, 0x5B5C, 0x6063, 0x6148, 0x6ECB, 0x7099, 0x716E, 0x7386, 0x74F7, 0x75B5, 0x78C1, 0x7D2B, 0x8005, 0x81EA, 0x8328, 0x8517, 0x85C9, 0x8AEE, 0x8CC7, 0x96CC, 0x4F5C, 0x52FA, 0x56BC, 0x65AB, 0x6628, 0x707C, 0x70B8, 0x7235, 0x7DBD, 0x828D, 0x914C, 0x96C0, 0x9D72, 0x5B71, 0x68E7, 0x6B98, 0x6F7A, 0x76DE, 0x5C91, 0x66AB, 0x6F5B, 0x7BB4, 0x7C2A, 0x8836, 0x96DC, 0x4E08, 0x4ED7, 0x5320, 0x5834, 0x58BB, 0x58EF, 0x596C, 0x5C07, 0x5E33, 0x5E84, 0x5F35, 0x638C, 0x66B2, 0x6756, 0x6A1F, 0x6AA3, 0x6B0C, 0x6F3F, 0x7246, 0xF9FA, 0x7350, 0x748B, 0x7AE0, 0x7CA7, 0x8178, 0x81DF, 0x81E7, 0x838A, 0x846C, 0x8523, 0x8594, 0x85CF, 0x88DD, 0x8D13, 0x91AC, 0x9577, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x969C, 0x518D, 0x54C9, 0x5728, 0x5BB0, 0x624D, 0x6750, 0x683D, 0x6893, 0x6E3D, 0x6ED3, 0x707D, 0x7E21, 0x88C1, 0x8CA1, 0x8F09, 0x9F4B, 0x9F4E, 0x722D, 0x7B8F, 0x8ACD, 0x931A, 0x4F47, 0x4F4E, 0x5132, 0x5480, 0x59D0, 0x5E95, 0x62B5, 0x6775, 0x696E, 0x6A17, 0x6CAE, 0x6E1A, 0x72D9, 0x732A, 0x75BD, 0x7BB8, 0x7D35, 0x82E7, 0x83F9, 0x8457, 0x85F7, 0x8A5B, 0x8CAF, 0x8E87, 0x9019, 0x90B8, 0x96CE, 0x9F5F, 0x52E3, 0x540A, 0x5AE1, 0x5BC2, 0x6458, 0x6575, 0x6EF4, 0x72C4, 0xF9FB, 0x7684, 0x7A4D, 0x7B1B, 0x7C4D, 0x7E3E, 0x7FDF, 0x837B, 0x8B2B, 0x8CCA, 0x8D64, 0x8DE1, 0x8E5F, 0x8FEA, 0x8FF9, 0x9069, 0x93D1, 0x4F43, 0x4F7A, 0x50B3, 0x5168, 0x5178, 0x524D, 0x526A, 0x5861, 0x587C, 0x5960, 0x5C08, 0x5C55, 0x5EDB, 0x609B, 0x6230, 0x6813, 0x6BBF, 0x6C08, 0x6FB1, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x714E, 0x7420, 0x7530, 0x7538, 0x7551, 0x7672, 0x7B4C, 0x7B8B, 0x7BAD, 0x7BC6, 0x7E8F, 0x8A6E, 0x8F3E, 0x8F49, 0x923F, 0x9293, 0x9322, 0x942B, 0x96FB, 0x985A, 0x986B, 0x991E, 0x5207, 0x622A, 0x6298, 0x6D59, 0x7664, 0x7ACA, 0x7BC0, 0x7D76, 0x5360, 0x5CBE, 0x5E97, 0x6F38, 0x70B9, 0x7C98, 0x9711, 0x9B8E, 0x9EDE, 0x63A5, 0x647A, 0x8776, 0x4E01, 0x4E95, 0x4EAD, 0x505C, 0x5075, 0x5448, 0x59C3, 0x5B9A, 0x5E40, 0x5EAD, 0x5EF7, 0x5F81, 0x60C5, 0x633A, 0x653F, 0x6574, 0x65CC, 0x6676, 0x6678, 0x67FE, 0x6968, 0x6A89, 0x6B63, 0x6C40, 0x6DC0, 0x6DE8, 0x6E1F, 0x6E5E, 0x701E, 0x70A1, 0x738E, 0x73FD, 0x753A, 0x775B, 0x7887, 0x798E, 0x7A0B, 0x7A7D, 0x7CBE, 0x7D8E, 0x8247, 0x8A02, 0x8AEA, 0x8C9E, 0x912D, 0x914A, 0x91D8, 0x9266, 0x92CC, 0x9320, 0x9706, 0x9756, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x975C, 0x9802, 0x9F0E, 0x5236, 0x5291, 0x557C, 0x5824, 0x5E1D, 0x5F1F, 0x608C, 0x63D0, 0x68AF, 0x6FDF, 0x796D, 0x7B2C, 0x81CD, 0x85BA, 0x88FD, 0x8AF8, 0x8E44, 0x918D, 0x9664, 0x969B, 0x973D, 0x984C, 0x9F4A, 0x4FCE, 0x5146, 0x51CB, 0x52A9, 0x5632, 0x5F14, 0x5F6B, 0x63AA, 0x64CD, 0x65E9, 0x6641, 0x66FA, 0x66F9, 0x671D, 0x689D, 0x68D7, 0x69FD, 0x6F15, 0x6F6E, 0x7167, 0x71E5, 0x722A, 0x74AA, 0x773A, 0x7956, 0x795A, 0x79DF, 0x7A20, 0x7A95, 0x7C97, 0x7CDF, 0x7D44, 0x7E70, 0x8087, 0x85FB, 0x86A4, 0x8A54, 0x8ABF, 0x8D99, 0x8E81, 0x9020, 0x906D, 0x91E3, 0x963B, 0x96D5, 0x9CE5, 0x65CF, 0x7C07, 0x8DB3, 0x93C3, 0x5B58, 0x5C0A, 0x5352, 0x62D9, 0x731D, 0x5027, 0x5B97, 0x5F9E, 0x60B0, 0x616B, 0x68D5, 0x6DD9, 0x742E, 0x7A2E, 0x7D42, 0x7D9C, 0x7E31, 0x816B, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8E2A, 0x8E35, 0x937E, 0x9418, 0x4F50, 0x5750, 0x5DE6, 0x5EA7, 0x632B, 0x7F6A, 0x4E3B, 0x4F4F, 0x4F8F, 0x505A, 0x59DD, 0x80C4, 0x546A, 0x5468, 0x55FE, 0x594F, 0x5B99, 0x5DDE, 0x5EDA, 0x665D, 0x6731, 0x67F1, 0x682A, 0x6CE8, 0x6D32, 0x6E4A, 0x6F8D, 0x70B7, 0x73E0, 0x7587, 0x7C4C, 0x7D02, 0x7D2C, 0x7DA2, 0x821F, 0x86DB, 0x8A3B, 0x8A85, 0x8D70, 0x8E8A, 0x8F33, 0x9031, 0x914E, 0x9152, 0x9444, 0x99D0, 0x7AF9, 0x7CA5, 0x4FCA, 0x5101, 0x51C6, 0x57C8, 0x5BEF, 0x5CFB, 0x6659, 0x6A3D, 0x6D5A, 0x6E96, 0x6FEC, 0x710C, 0x756F, 0x7AE3, 0x8822, 0x9021, 0x9075, 0x96CB, 0x99FF, 0x8301, 0x4E2D, 0x4EF2, 0x8846, 0x91CD, 0x537D, 0x6ADB, 0x696B, 0x6C41, 0x847A, 0x589E, 0x618E, 0x66FE, 0x62EF, 0x70DD, 0x7511, 0x75C7, 0x7E52, 0x84B8, 0x8B49, 0x8D08, 0x4E4B, 0x53EA, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x54AB, 0x5730, 0x5740, 0x5FD7, 0x6301, 0x6307, 0x646F, 0x652F, 0x65E8, 0x667A, 0x679D, 0x67B3, 0x6B62, 0x6C60, 0x6C9A, 0x6F2C, 0x77E5, 0x7825, 0x7949, 0x7957, 0x7D19, 0x80A2, 0x8102, 0x81F3, 0x829D, 0x82B7, 0x8718, 0x8A8C, 0xF9FC, 0x8D04, 0x8DBE, 0x9072, 0x76F4, 0x7A19, 0x7A37, 0x7E54, 0x8077, 0x5507, 0x55D4, 0x5875, 0x632F, 0x6422, 0x6649, 0x664B, 0x686D, 0x699B, 0x6B84, 0x6D25, 0x6EB1, 0x73CD, 0x7468, 0x74A1, 0x755B, 0x75B9, 0x76E1, 0x771E, 0x778B, 0x79E6, 0x7E09, 0x7E1D, 0x81FB, 0x852F, 0x8897, 0x8A3A, 0x8CD1, 0x8EEB, 0x8FB0, 0x9032, 0x93AD, 0x9663, 0x9673, 0x9707, 0x4F84, 0x53F1, 0x59EA, 0x5AC9, 0x5E19, 0x684E, 0x74C6, 0x75BE, 0x79E9, 0x7A92, 0x81A3, 0x86ED, 0x8CEA, 0x8DCC, 0x8FED, 0x659F, 0x6715, 0xF9FD, 0x57F7, 0x6F57, 0x7DDD, 0x8F2F, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x93F6, 0x96C6, 0x5FB5, 0x61F2, 0x6F84, 0x4E14, 0x4F98, 0x501F, 0x53C9, 0x55DF, 0x5D6F, 0x5DEE, 0x6B21, 0x6B64, 0x78CB, 0x7B9A, 0xF9FE, 0x8E49, 0x8ECA, 0x906E, 0x6349, 0x643E, 0x7740, 0x7A84, 0x932F, 0x947F, 0x9F6A, 0x64B0, 0x6FAF, 0x71E6, 0x74A8, 0x74DA, 0x7AC4, 0x7C12, 0x7E82, 0x7CB2, 0x7E98, 0x8B9A, 0x8D0A, 0x947D, 0x9910, 0x994C, 0x5239, 0x5BDF, 0x64E6, 0x672D, 0x7D2E, 0x50ED, 0x53C3, 0x5879, 0x6158, 0x6159, 0x61FA, 0x65AC, 0x7AD9, 0x8B92, 0x8B96, 0x5009, 0x5021, 0x5275, 0x5531, 0x5A3C, 0x5EE0, 0x5F70, 0x6134, 0x655E, 0x660C, 0x6636, 0x66A2, 0x69CD, 0x6EC4, 0x6F32, 0x7316, 0x7621, 0x7A93, 0x8139, 0x8259, 0x83D6, 0x84BC, 0x50B5, 0x57F0, 0x5BC0, 0x5BE8, 0x5F69, 0x63A1, 0x7826, 0x7DB5, 0x83DC, 0x8521, 0x91C7, 0x91F5, 0x518A, 0x67F5, 0x7B56, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8CAC, 0x51C4, 0x59BB, 0x60BD, 0x8655, 0x501C, 0xF9FF, 0x5254, 0x5C3A, 0x617D, 0x621A, 0x62D3, 0x64F2, 0x65A5, 0x6ECC, 0x7620, 0x810A, 0x8E60, 0x965F, 0x96BB, 0x4EDF, 0x5343, 0x5598, 0x5929, 0x5DDD, 0x64C5, 0x6CC9, 0x6DFA, 0x7394, 0x7A7F, 0x821B, 0x85A6, 0x8CE4, 0x8E10, 0x9077, 0x91E7, 0x95E1, 0x9621, 0x97C6, 0x51F8, 0x54F2, 0x5586, 0x5FB9, 0x64A4, 0x6F88, 0x7DB4, 0x8F1F, 0x8F4D, 0x9435, 0x50C9, 0x5C16, 0x6CBE, 0x6DFB, 0x751B, 0x77BB, 0x7C3D, 0x7C64, 0x8A79, 0x8AC2, 0x581E, 0x59BE, 0x5E16, 0x6377, 0x7252, 0x758A, 0x776B, 0x8ADC, 0x8CBC, 0x8F12, 0x5EF3, 0x6674, 0x6DF8, 0x807D, 0x83C1, 0x8ACB, 0x9751, 0x9BD6, 0xFA00, 0x5243, 0x66FF, 0x6D95, 0x6EEF, 0x7DE0, 0x8AE6, 0x902E, 0x905E, 0x9AD4, 0x521D, 0x527F, 0x54E8, 0x6194, 0x6284, 0x62DB, 0x68A2, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x6912, 0x695A, 0x6A35, 0x7092, 0x7126, 0x785D, 0x7901, 0x790E, 0x79D2, 0x7A0D, 0x8096, 0x8278, 0x82D5, 0x8349, 0x8549, 0x8C82, 0x8D85, 0x9162, 0x918B, 0x91AE, 0x4FC3, 0x56D1, 0x71ED, 0x77D7, 0x8700, 0x89F8, 0x5BF8, 0x5FD6, 0x6751, 0x90A8, 0x53E2, 0x585A, 0x5BF5, 0x60A4, 0x6181, 0x6460, 0x7E3D, 0x8070, 0x8525, 0x9283, 0x64AE, 0x50AC, 0x5D14, 0x6700, 0x589C, 0x62BD, 0x63A8, 0x690E, 0x6978, 0x6A1E, 0x6E6B, 0x76BA, 0x79CB, 0x82BB, 0x8429, 0x8ACF, 0x8DA8, 0x8FFD, 0x9112, 0x914B, 0x919C, 0x9310, 0x9318, 0x939A, 0x96DB, 0x9A36, 0x9C0D, 0x4E11, 0x755C, 0x795D, 0x7AFA, 0x7B51, 0x7BC9, 0x7E2E, 0x84C4, 0x8E59, 0x8E74, 0x8EF8, 0x9010, 0x6625, 0x693F, 0x7443, 0x51FA, 0x672E, 0x9EDC, 0x5145, 0x5FE0, 0x6C96, 0x87F2, 0x885D, 0x8877, 0x60B4, 0x81B5, 0x8403, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8D05, 0x53D6, 0x5439, 0x5634, 0x5A36, 0x5C31, 0x708A, 0x7FE0, 0x805A, 0x8106, 0x81ED, 0x8DA3, 0x9189, 0x9A5F, 0x9DF2, 0x5074, 0x4EC4, 0x53A0, 0x60FB, 0x6E2C, 0x5C64, 0x4F88, 0x5024, 0x55E4, 0x5CD9, 0x5E5F, 0x6065, 0x6894, 0x6CBB, 0x6DC4, 0x71BE, 0x75D4, 0x75F4, 0x7661, 0x7A1A, 0x7A49, 0x7DC7, 0x7DFB, 0x7F6E, 0x81F4, 0x86A9, 0x8F1C, 0x96C9, 0x99B3, 0x9F52, 0x5247, 0x52C5, 0x98ED, 0x89AA, 0x4E03, 0x67D2, 0x6F06, 0x4FB5, 0x5BE2, 0x6795, 0x6C88, 0x6D78, 0x741B, 0x7827, 0x91DD, 0x937C, 0x87C4, 0x79E4, 0x7A31, 0x5FEB, 0x4ED6, 0x54A4, 0x553E, 0x58AE, 0x59A5, 0x60F0, 0x6253, 0x62D6, 0x6736, 0x6955, 0x8235, 0x9640, 0x99B1, 0x99DD, 0x502C, 0x5353, 0x5544, 0x577C, 0xFA01, 0x6258, 0xFA02, 0x64E2, 0x666B, 0x67DD, 0x6FC1, 0x6FEF, 0x7422, 0x7438, 0x8A17, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x9438, 0x5451, 0x5606, 0x5766, 0x5F48, 0x619A, 0x6B4E, 0x7058, 0x70AD, 0x7DBB, 0x8A95, 0x596A, 0x812B, 0x63A2, 0x7708, 0x803D, 0x8CAA, 0x5854, 0x642D, 0x69BB, 0x5B95, 0x5E11, 0x6E6F, 0xFA03, 0x8569, 0x514C, 0x53F0, 0x592A, 0x6020, 0x614B, 0x6B86, 0x6C70, 0x6CF0, 0x7B1E, 0x80CE, 0x82D4, 0x8DC6, 0x90B0, 0x98B1, 0xFA04, 0x64C7, 0x6FA4, 0x6491, 0x6504, 0x514E, 0x5410, 0x571F, 0x8A0E, 0x615F, 0x6876, 0xFA05, 0x75DB, 0x7B52, 0x7D71, 0x901A, 0x5806, 0x69CC, 0x817F, 0x892A, 0x9000, 0x9839, 0x5078, 0x5957, 0x59AC, 0x6295, 0x900F, 0x9B2A, 0x615D, 0x7279, 0x95D6, 0x5761, 0x5A46, 0x5DF4, 0x628A, 0x64AD, 0x64FA, 0x6777, 0x6CE2, 0x6D3E, 0x722C, 0x7436, 0x7834, 0x7F77, 0x82AD, 0x8DDB, 0x9817, 0x5224, 0x5742, 0x677F, 0x7248, 0x74E3, 0x8CA9, 0x8FA6, 0x9211, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x962A, 0x516B, 0x53ED, 0x634C, 0x4F69, 0x5504, 0x6096, 0x6557, 0x6C9B, 0x6D7F, 0x724C, 0x72FD, 0x7A17, 0x8987, 0x8C9D, 0x5F6D, 0x6F8E, 0x70F9, 0x81A8, 0x610E, 0x4FBF, 0x504F, 0x6241, 0x7247, 0x7BC7, 0x7DE8, 0x7FE9, 0x904D, 0x97AD, 0x9A19, 0x8CB6, 0x576A, 0x5E73, 0x67B0, 0x840D, 0x8A55, 0x5420, 0x5B16, 0x5E63, 0x5EE2, 0x5F0A, 0x6583, 0x80BA, 0x853D, 0x9589, 0x965B, 0x4F48, 0x5305, 0x530D, 0x530F, 0x5486, 0x54FA, 0x5703, 0x5E03, 0x6016, 0x629B, 0x62B1, 0x6355, 0xFA06, 0x6CE1, 0x6D66, 0x75B1, 0x7832, 0x80DE, 0x812F, 0x82DE, 0x8461, 0x84B2, 0x888D, 0x8912, 0x900B, 0x92EA, 0x98FD, 0x9B91, 0x5E45, 0x66B4, 0x66DD, 0x7011, 0x7206, 0xFA07, 0x4FF5, 0x527D, 0x5F6A, 0x6153, 0x6753, 0x6A19, 0x6F02, 0x74E2, 0x7968, 0x8868, 0x8C79, 0x98C7, 0x98C4, 0x9A43, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x54C1, 0x7A1F, 0x6953, 0x8AF7, 0x8C4A, 0x98A8, 0x99AE, 0x5F7C, 0x62AB, 0x75B2, 0x76AE, 0x88AB, 0x907F, 0x9642, 0x5339, 0x5F3C, 0x5FC5, 0x6CCC, 0x73CC, 0x7562, 0x758B, 0x7B46, 0x82FE, 0x999D, 0x4E4F, 0x903C, 0x4E0B, 0x4F55, 0x53A6, 0x590F, 0x5EC8, 0x6630, 0x6CB3, 0x7455, 0x8377, 0x8766, 0x8CC0, 0x9050, 0x971E, 0x9C15, 0x58D1, 0x5B78, 0x8650, 0x8B14, 0x9DB4, 0x5BD2, 0x6068, 0x608D, 0x65F1, 0x6C57, 0x6F22, 0x6FA3, 0x701A, 0x7F55, 0x7FF0, 0x9591, 0x9592, 0x9650, 0x97D3, 0x5272, 0x8F44, 0x51FD, 0x542B, 0x54B8, 0x5563, 0x558A, 0x6ABB, 0x6DB5, 0x7DD8, 0x8266, 0x929C, 0x9677, 0x9E79, 0x5408, 0x54C8, 0x76D2, 0x86E4, 0x95A4, 0x95D4, 0x965C, 0x4EA2, 0x4F09, 0x59EE, 0x5AE6, 0x5DF7, 0x6052, 0x6297, 0x676D, 0x6841, 0x6C86, 0x6E2F, 0x7F38, 0x809B, 0x822A, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFA08, 0xFA09, 0x9805, 0x4EA5, 0x5055, 0x54B3, 0x5793, 0x595A, 0x5B69, 0x5BB3, 0x61C8, 0x6977, 0x6D77, 0x7023, 0x87F9, 0x89E3, 0x8A72, 0x8AE7, 0x9082, 0x99ED, 0x9AB8, 0x52BE, 0x6838, 0x5016, 0x5E78, 0x674F, 0x8347, 0x884C, 0x4EAB, 0x5411, 0x56AE, 0x73E6, 0x9115, 0x97FF, 0x9909, 0x9957, 0x9999, 0x5653, 0x589F, 0x865B, 0x8A31, 0x61B2, 0x6AF6, 0x737B, 0x8ED2, 0x6B47, 0x96AA, 0x9A57, 0x5955, 0x7200, 0x8D6B, 0x9769, 0x4FD4, 0x5CF4, 0x5F26, 0x61F8, 0x665B, 0x6CEB, 0x70AB, 0x7384, 0x73B9, 0x73FE, 0x7729, 0x774D, 0x7D43, 0x7D62, 0x7E23, 0x8237, 0x8852, 0xFA0A, 0x8CE2, 0x9249, 0x986F, 0x5B51, 0x7A74, 0x8840, 0x9801, 0x5ACC, 0x4FE0, 0x5354, 0x593E, 0x5CFD, 0x633E, 0x6D79, 0x72F9, 0x8105, 0x8107, 0x83A2, 0x92CF, 0x9830, 0x4EA8, 0x5144, 0x5211, 0x578B, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x5F62, 0x6CC2, 0x6ECE, 0x7005, 0x7050, 0x70AF, 0x7192, 0x73E9, 0x7469, 0x834A, 0x87A2, 0x8861, 0x9008, 0x90A2, 0x93A3, 0x99A8, 0x516E, 0x5F57, 0x60E0, 0x6167, 0x66B3, 0x8559, 0x8E4A, 0x91AF, 0x978B, 0x4E4E, 0x4E92, 0x547C, 0x58D5, 0x58FA, 0x597D, 0x5CB5, 0x5F27, 0x6236, 0x6248, 0x660A, 0x6667, 0x6BEB, 0x6D69, 0x6DCF, 0x6E56, 0x6EF8, 0x6F94, 0x6FE0, 0x6FE9, 0x705D, 0x72D0, 0x7425, 0x745A, 0x74E0, 0x7693, 0x795C, 0x7CCA, 0x7E1E, 0x80E1, 0x82A6, 0x846B, 0x84BF, 0x864E, 0x865F, 0x8774, 0x8B77, 0x8C6A, 0x93AC, 0x9800, 0x9865, 0x60D1, 0x6216, 0x9177, 0x5A5A, 0x660F, 0x6DF7, 0x6E3E, 0x743F, 0x9B42, 0x5FFD, 0x60DA, 0x7B0F, 0x54C4, 0x5F18, 0x6C5E, 0x6CD3, 0x6D2A, 0x70D8, 0x7D05, 0x8679, 0x8A0C, 0x9D3B, 0x5316, 0x548C, 0x5B05, 0x6A3A, 0x706B, 0x7575, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x798D, 0x79BE, 0x82B1, 0x83EF, 0x8A71, 0x8B41, 0x8CA8, 0x9774, 0xFA0B, 0x64F4, 0x652B, 0x78BA, 0x78BB, 0x7A6B, 0x4E38, 0x559A, 0x5950, 0x5BA6, 0x5E7B, 0x60A3, 0x63DB, 0x6B61, 0x6665, 0x6853, 0x6E19, 0x7165, 0x74B0, 0x7D08, 0x9084, 0x9A69, 0x9C25, 0x6D3B, 0x6ED1, 0x733E, 0x8C41, 0x95CA, 0x51F0, 0x5E4C, 0x5FA8, 0x604D, 0x60F6, 0x6130, 0x614C, 0x6643, 0x6644, 0x69A5, 0x6CC1, 0x6E5F, 0x6EC9, 0x6F62, 0x714C, 0x749C, 0x7687, 0x7BC1, 0x7C27, 0x8352, 0x8757, 0x9051, 0x968D, 0x9EC3, 0x532F, 0x56DE, 0x5EFB, 0x5F8A, 0x6062, 0x6094, 0x61F7, 0x6666, 0x6703, 0x6A9C, 0x6DEE, 0x6FAE, 0x7070, 0x736A, 0x7E6A, 0x81BE, 0x8334, 0x86D4, 0x8AA8, 0x8CC4, 0x5283, 0x7372, 0x5B96, 0x6A6B, 0x9404, 0x54EE, 0x5686, 0x5B5D, 0x6548, 0x6585, 0x66C9, 0x689F, 0x6D8D, 0x6DC6, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x723B, 0x80B4, 0x9175, 0x9A4D, 0x4FAF, 0x5019, 0x539A, 0x540E, 0x543C, 0x5589, 0x55C5, 0x5E3F, 0x5F8C, 0x673D, 0x7166, 0x73DD, 0x9005, 0x52DB, 0x52F3, 0x5864, 0x58CE, 0x7104, 0x718F, 0x71FB, 0x85B0, 0x8A13, 0x6688, 0x85A8, 0x55A7, 0x6684, 0x714A, 0x8431, 0x5349, 0x5599, 0x6BC1, 0x5F59, 0x5FBD, 0x63EE, 0x6689, 0x7147, 0x8AF1, 0x8F1D, 0x9EBE, 0x4F11, 0x643A, 0x70CB, 0x7566, 0x8667, 0x6064, 0x8B4E, 0x9DF8, 0x5147, 0x51F6, 0x5308, 0x6D36, 0x80F8, 0x9ED1, 0x6615, 0x6B23, 0x7098, 0x75D5, 0x5403, 0x5C79, 0x7D07, 0x8A16, 0x6B20, 0x6B3D, 0x6B46, 0x5438, 0x6070, 0x6D3D, 0x7FD5, 0x8208, 0x50D6, 0x51DE, 0x559C, 0x566B, 0x56CD, 0x59EC, 0x5B09, 0x5E0C, 0x6199, 0x6198, 0x6231, 0x665E, 0x66E6, 0x7199, 0x71B9, 0x71BA, 0x72A7, 0x79A7, 0x7A00, 0x7FB2, 0x8A70, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, +}; + +} diff --git a/core/libs/prx/libSceCesCs/Cp950.hpp b/core/libs/prx/libSceCesCs/Cp950.hpp new file mode 100644 index 0000000000..ad0e018b29 --- /dev/null +++ b/core/libs/prx/libSceCesCs/Cp950.hpp @@ -0,0 +1,99 @@ +#pragma once + +#include + +namespace Cp950 { + +inline constexpr uint16_t TO_UCS2[89 * 157] = { + 0x3000, 0xFF0C, 0x3001, 0x3002, 0xFF0E, 0x2027, 0xFF1B, 0xFF1A, 0xFF1F, 0xFF01, 0xFE30, 0x2026, 0x2025, 0xFE50, 0xFE51, 0xFE52, 0x00B7, 0xFE54, 0xFE55, 0xFE56, 0xFE57, 0xFF5C, 0x2013, 0xFE31, 0x2014, 0xFE33, 0x2574, 0xFE34, 0xFE4F, 0xFF08, 0xFF09, 0xFE35, 0xFE36, 0xFF5B, 0xFF5D, 0xFE37, 0xFE38, 0x3014, 0x3015, 0xFE39, 0xFE3A, 0x3010, 0x3011, 0xFE3B, 0xFE3C, 0x300A, 0x300B, 0xFE3D, 0xFE3E, 0x3008, 0x3009, 0xFE3F, 0xFE40, 0x300C, 0x300D, 0xFE41, 0xFE42, 0x300E, 0x300F, 0xFE43, 0xFE44, 0xFE59, 0xFE5A, 0xFE5B, 0xFE5C, 0xFE5D, 0xFE5E, 0x2018, 0x2019, 0x201C, 0x201D, 0x301D, 0x301E, 0x2035, 0x2032, 0xFF03, 0xFF06, 0xFF0A, 0x203B, 0x00A7, 0x3003, 0x25CB, 0x25CF, 0x25B3, 0x25B2, 0x25CE, 0x2606, 0x2605, 0x25C7, 0x25C6, 0x25A1, 0x25A0, 0x25BD, 0x25BC, 0x32A3, 0x2105, 0x00AF, 0xFFE3, 0xFF3F, 0x02CD, 0xFE49, 0xFE4A, 0xFE4D, 0xFE4E, 0xFE4B, 0xFE4C, 0xFE5F, 0xFE60, 0xFE61, 0xFF0B, 0xFF0D, 0x00D7, 0x00F7, 0x00B1, 0x221A, 0xFF1C, 0xFF1E, 0xFF1D, 0x2266, 0x2267, 0x2260, 0x221E, 0x2252, 0x2261, 0xFE62, 0xFE63, 0xFE64, 0xFE65, 0xFE66, 0xFF5E, 0x2229, 0x222A, 0x22A5, 0x2220, 0x221F, 0x22BF, 0x33D2, 0x33D1, 0x222B, 0x222E, 0x2235, 0x2234, 0x2640, 0x2642, 0x2295, 0x2299, 0x2191, 0x2193, 0x2190, 0x2192, 0x2196, 0x2197, 0x2199, 0x2198, 0x2225, 0x2223, 0xFF0F, + 0xFF3C, 0x2215, 0xFE68, 0xFF04, 0xFFE5, 0x3012, 0xFFE0, 0xFFE1, 0xFF05, 0xFF20, 0x2103, 0x2109, 0xFE69, 0xFE6A, 0xFE6B, 0x33D5, 0x339C, 0x339D, 0x339E, 0x33CE, 0x33A1, 0x338E, 0x338F, 0x33C4, 0x00B0, 0x5159, 0x515B, 0x515E, 0x515D, 0x5161, 0x5163, 0x55E7, 0x74E9, 0x7CCE, 0x2581, 0x2582, 0x2583, 0x2584, 0x2585, 0x2586, 0x2587, 0x2588, 0x258F, 0x258E, 0x258D, 0x258C, 0x258B, 0x258A, 0x2589, 0x253C, 0x2534, 0x252C, 0x2524, 0x251C, 0x2594, 0x2500, 0x2502, 0x2595, 0x250C, 0x2510, 0x2514, 0x2518, 0x256D, 0x256E, 0x2570, 0x256F, 0x2550, 0x255E, 0x256A, 0x2561, 0x25E2, 0x25E3, 0x25E5, 0x25E4, 0x2571, 0x2572, 0x2573, 0xFF10, 0xFF11, 0xFF12, 0xFF13, 0xFF14, 0xFF15, 0xFF16, 0xFF17, 0xFF18, 0xFF19, 0x2160, 0x2161, 0x2162, 0x2163, 0x2164, 0x2165, 0x2166, 0x2167, 0x2168, 0x2169, 0x3021, 0x3022, 0x3023, 0x3024, 0x3025, 0x3026, 0x3027, 0x3028, 0x3029, 0x5341, 0x5344, 0x5345, 0xFF21, 0xFF22, 0xFF23, 0xFF24, 0xFF25, 0xFF26, 0xFF27, 0xFF28, 0xFF29, 0xFF2A, 0xFF2B, 0xFF2C, 0xFF2D, 0xFF2E, 0xFF2F, 0xFF30, 0xFF31, 0xFF32, 0xFF33, 0xFF34, 0xFF35, 0xFF36, 0xFF37, 0xFF38, 0xFF39, 0xFF3A, 0xFF41, 0xFF42, 0xFF43, 0xFF44, 0xFF45, 0xFF46, 0xFF47, 0xFF48, 0xFF49, 0xFF4A, 0xFF4B, 0xFF4C, 0xFF4D, 0xFF4E, 0xFF4F, 0xFF50, 0xFF51, 0xFF52, 0xFF53, 0xFF54, 0xFF55, 0xFF56, + 0xFF57, 0xFF58, 0xFF59, 0xFF5A, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, 0x03A0, 0x03A1, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, 0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF, 0x03C0, 0x03C1, 0x03C3, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, 0x03C9, 0x3105, 0x3106, 0x3107, 0x3108, 0x3109, 0x310A, 0x310B, 0x310C, 0x310D, 0x310E, 0x310F, 0x3110, 0x3111, 0x3112, 0x3113, 0x3114, 0x3115, 0x3116, 0x3117, 0x3118, 0x3119, 0x311A, 0x311B, 0x311C, 0x311D, 0x311E, 0x311F, 0x3120, 0x3121, 0x3122, 0x3123, 0x3124, 0x3125, 0x3126, 0x3127, 0x3128, 0x3129, 0x02D9, 0x02C9, 0x02CA, 0x02C7, 0x02CB, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x20AC, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x4E00, 0x4E59, 0x4E01, 0x4E03, 0x4E43, 0x4E5D, 0x4E86, 0x4E8C, 0x4EBA, 0x513F, 0x5165, 0x516B, 0x51E0, 0x5200, 0x5201, 0x529B, 0x5315, 0x5341, 0x535C, 0x53C8, 0x4E09, 0x4E0B, 0x4E08, 0x4E0A, 0x4E2B, 0x4E38, 0x51E1, 0x4E45, 0x4E48, 0x4E5F, 0x4E5E, 0x4E8E, 0x4EA1, 0x5140, 0x5203, 0x52FA, 0x5343, 0x53C9, 0x53E3, 0x571F, 0x58EB, 0x5915, 0x5927, 0x5973, 0x5B50, 0x5B51, 0x5B53, 0x5BF8, 0x5C0F, 0x5C22, 0x5C38, 0x5C71, 0x5DDD, 0x5DE5, 0x5DF1, 0x5DF2, 0x5DF3, 0x5DFE, 0x5E72, 0x5EFE, 0x5F0B, 0x5F13, 0x624D, 0x4E11, 0x4E10, 0x4E0D, 0x4E2D, 0x4E30, 0x4E39, 0x4E4B, 0x5C39, 0x4E88, 0x4E91, 0x4E95, 0x4E92, 0x4E94, 0x4EA2, 0x4EC1, 0x4EC0, 0x4EC3, 0x4EC6, 0x4EC7, 0x4ECD, 0x4ECA, 0x4ECB, 0x4EC4, 0x5143, 0x5141, 0x5167, 0x516D, 0x516E, 0x516C, 0x5197, 0x51F6, 0x5206, 0x5207, 0x5208, 0x52FB, 0x52FE, 0x52FF, 0x5316, 0x5339, 0x5348, 0x5347, 0x5345, 0x535E, 0x5384, 0x53CB, 0x53CA, 0x53CD, 0x58EC, 0x5929, 0x592B, 0x592A, 0x592D, 0x5B54, 0x5C11, 0x5C24, 0x5C3A, 0x5C6F, 0x5DF4, 0x5E7B, 0x5EFF, 0x5F14, 0x5F15, 0x5FC3, 0x6208, 0x6236, 0x624B, 0x624E, 0x652F, 0x6587, 0x6597, 0x65A4, 0x65B9, 0x65E5, 0x66F0, 0x6708, 0x6728, 0x6B20, 0x6B62, 0x6B79, 0x6BCB, 0x6BD4, 0x6BDB, 0x6C0F, 0x6C34, 0x706B, 0x722A, 0x7236, 0x723B, 0x7247, 0x7259, 0x725B, 0x72AC, 0x738B, 0x4E19, + 0x4E16, 0x4E15, 0x4E14, 0x4E18, 0x4E3B, 0x4E4D, 0x4E4F, 0x4E4E, 0x4EE5, 0x4ED8, 0x4ED4, 0x4ED5, 0x4ED6, 0x4ED7, 0x4EE3, 0x4EE4, 0x4ED9, 0x4EDE, 0x5145, 0x5144, 0x5189, 0x518A, 0x51AC, 0x51F9, 0x51FA, 0x51F8, 0x520A, 0x52A0, 0x529F, 0x5305, 0x5306, 0x5317, 0x531D, 0x4EDF, 0x534A, 0x5349, 0x5361, 0x5360, 0x536F, 0x536E, 0x53BB, 0x53EF, 0x53E4, 0x53F3, 0x53EC, 0x53EE, 0x53E9, 0x53E8, 0x53FC, 0x53F8, 0x53F5, 0x53EB, 0x53E6, 0x53EA, 0x53F2, 0x53F1, 0x53F0, 0x53E5, 0x53ED, 0x53FB, 0x56DB, 0x56DA, 0x5916, 0x592E, 0x5931, 0x5974, 0x5976, 0x5B55, 0x5B83, 0x5C3C, 0x5DE8, 0x5DE7, 0x5DE6, 0x5E02, 0x5E03, 0x5E73, 0x5E7C, 0x5F01, 0x5F18, 0x5F17, 0x5FC5, 0x620A, 0x6253, 0x6254, 0x6252, 0x6251, 0x65A5, 0x65E6, 0x672E, 0x672C, 0x672A, 0x672B, 0x672D, 0x6B63, 0x6BCD, 0x6C11, 0x6C10, 0x6C38, 0x6C41, 0x6C40, 0x6C3E, 0x72AF, 0x7384, 0x7389, 0x74DC, 0x74E6, 0x7518, 0x751F, 0x7528, 0x7529, 0x7530, 0x7531, 0x7532, 0x7533, 0x758B, 0x767D, 0x76AE, 0x76BF, 0x76EE, 0x77DB, 0x77E2, 0x77F3, 0x793A, 0x79BE, 0x7A74, 0x7ACB, 0x4E1E, 0x4E1F, 0x4E52, 0x4E53, 0x4E69, 0x4E99, 0x4EA4, 0x4EA6, 0x4EA5, 0x4EFF, 0x4F09, 0x4F19, 0x4F0A, 0x4F15, 0x4F0D, 0x4F10, 0x4F11, 0x4F0F, 0x4EF2, 0x4EF6, 0x4EFB, 0x4EF0, 0x4EF3, 0x4EFD, 0x4F01, 0x4F0B, 0x5149, 0x5147, 0x5146, 0x5148, 0x5168, + 0x5171, 0x518D, 0x51B0, 0x5217, 0x5211, 0x5212, 0x520E, 0x5216, 0x52A3, 0x5308, 0x5321, 0x5320, 0x5370, 0x5371, 0x5409, 0x540F, 0x540C, 0x540A, 0x5410, 0x5401, 0x540B, 0x5404, 0x5411, 0x540D, 0x5408, 0x5403, 0x540E, 0x5406, 0x5412, 0x56E0, 0x56DE, 0x56DD, 0x5733, 0x5730, 0x5728, 0x572D, 0x572C, 0x572F, 0x5729, 0x5919, 0x591A, 0x5937, 0x5938, 0x5984, 0x5978, 0x5983, 0x597D, 0x5979, 0x5982, 0x5981, 0x5B57, 0x5B58, 0x5B87, 0x5B88, 0x5B85, 0x5B89, 0x5BFA, 0x5C16, 0x5C79, 0x5DDE, 0x5E06, 0x5E76, 0x5E74, 0x5F0F, 0x5F1B, 0x5FD9, 0x5FD6, 0x620E, 0x620C, 0x620D, 0x6210, 0x6263, 0x625B, 0x6258, 0x6536, 0x65E9, 0x65E8, 0x65EC, 0x65ED, 0x66F2, 0x66F3, 0x6709, 0x673D, 0x6734, 0x6731, 0x6735, 0x6B21, 0x6B64, 0x6B7B, 0x6C16, 0x6C5D, 0x6C57, 0x6C59, 0x6C5F, 0x6C60, 0x6C50, 0x6C55, 0x6C61, 0x6C5B, 0x6C4D, 0x6C4E, 0x7070, 0x725F, 0x725D, 0x767E, 0x7AF9, 0x7C73, 0x7CF8, 0x7F36, 0x7F8A, 0x7FBD, 0x8001, 0x8003, 0x800C, 0x8012, 0x8033, 0x807F, 0x8089, 0x808B, 0x808C, 0x81E3, 0x81EA, 0x81F3, 0x81FC, 0x820C, 0x821B, 0x821F, 0x826E, 0x8272, 0x827E, 0x866B, 0x8840, 0x884C, 0x8863, 0x897F, 0x9621, 0x4E32, 0x4EA8, 0x4F4D, 0x4F4F, 0x4F47, 0x4F57, 0x4F5E, 0x4F34, 0x4F5B, 0x4F55, 0x4F30, 0x4F50, 0x4F51, 0x4F3D, 0x4F3A, 0x4F38, 0x4F43, 0x4F54, 0x4F3C, 0x4F46, 0x4F63, + 0x4F5C, 0x4F60, 0x4F2F, 0x4F4E, 0x4F36, 0x4F59, 0x4F5D, 0x4F48, 0x4F5A, 0x514C, 0x514B, 0x514D, 0x5175, 0x51B6, 0x51B7, 0x5225, 0x5224, 0x5229, 0x522A, 0x5228, 0x52AB, 0x52A9, 0x52AA, 0x52AC, 0x5323, 0x5373, 0x5375, 0x541D, 0x542D, 0x541E, 0x543E, 0x5426, 0x544E, 0x5427, 0x5446, 0x5443, 0x5433, 0x5448, 0x5442, 0x541B, 0x5429, 0x544A, 0x5439, 0x543B, 0x5438, 0x542E, 0x5435, 0x5436, 0x5420, 0x543C, 0x5440, 0x5431, 0x542B, 0x541F, 0x542C, 0x56EA, 0x56F0, 0x56E4, 0x56EB, 0x574A, 0x5751, 0x5740, 0x574D, 0x5747, 0x574E, 0x573E, 0x5750, 0x574F, 0x573B, 0x58EF, 0x593E, 0x599D, 0x5992, 0x59A8, 0x599E, 0x59A3, 0x5999, 0x5996, 0x598D, 0x59A4, 0x5993, 0x598A, 0x59A5, 0x5B5D, 0x5B5C, 0x5B5A, 0x5B5B, 0x5B8C, 0x5B8B, 0x5B8F, 0x5C2C, 0x5C40, 0x5C41, 0x5C3F, 0x5C3E, 0x5C90, 0x5C91, 0x5C94, 0x5C8C, 0x5DEB, 0x5E0C, 0x5E8F, 0x5E87, 0x5E8A, 0x5EF7, 0x5F04, 0x5F1F, 0x5F64, 0x5F62, 0x5F77, 0x5F79, 0x5FD8, 0x5FCC, 0x5FD7, 0x5FCD, 0x5FF1, 0x5FEB, 0x5FF8, 0x5FEA, 0x6212, 0x6211, 0x6284, 0x6297, 0x6296, 0x6280, 0x6276, 0x6289, 0x626D, 0x628A, 0x627C, 0x627E, 0x6279, 0x6273, 0x6292, 0x626F, 0x6298, 0x626E, 0x6295, 0x6293, 0x6291, 0x6286, 0x6539, 0x653B, 0x6538, 0x65F1, 0x66F4, 0x675F, 0x674E, 0x674F, 0x6750, 0x6751, 0x675C, 0x6756, 0x675E, 0x6749, 0x6746, 0x6760, + 0x6753, 0x6757, 0x6B65, 0x6BCF, 0x6C42, 0x6C5E, 0x6C99, 0x6C81, 0x6C88, 0x6C89, 0x6C85, 0x6C9B, 0x6C6A, 0x6C7A, 0x6C90, 0x6C70, 0x6C8C, 0x6C68, 0x6C96, 0x6C92, 0x6C7D, 0x6C83, 0x6C72, 0x6C7E, 0x6C74, 0x6C86, 0x6C76, 0x6C8D, 0x6C94, 0x6C98, 0x6C82, 0x7076, 0x707C, 0x707D, 0x7078, 0x7262, 0x7261, 0x7260, 0x72C4, 0x72C2, 0x7396, 0x752C, 0x752B, 0x7537, 0x7538, 0x7682, 0x76EF, 0x77E3, 0x79C1, 0x79C0, 0x79BF, 0x7A76, 0x7CFB, 0x7F55, 0x8096, 0x8093, 0x809D, 0x8098, 0x809B, 0x809A, 0x80B2, 0x826F, 0x8292, 0x828B, 0x828D, 0x898B, 0x89D2, 0x8A00, 0x8C37, 0x8C46, 0x8C55, 0x8C9D, 0x8D64, 0x8D70, 0x8DB3, 0x8EAB, 0x8ECA, 0x8F9B, 0x8FB0, 0x8FC2, 0x8FC6, 0x8FC5, 0x8FC4, 0x5DE1, 0x9091, 0x90A2, 0x90AA, 0x90A6, 0x90A3, 0x9149, 0x91C6, 0x91CC, 0x9632, 0x962E, 0x9631, 0x962A, 0x962C, 0x4E26, 0x4E56, 0x4E73, 0x4E8B, 0x4E9B, 0x4E9E, 0x4EAB, 0x4EAC, 0x4F6F, 0x4F9D, 0x4F8D, 0x4F73, 0x4F7F, 0x4F6C, 0x4F9B, 0x4F8B, 0x4F86, 0x4F83, 0x4F70, 0x4F75, 0x4F88, 0x4F69, 0x4F7B, 0x4F96, 0x4F7E, 0x4F8F, 0x4F91, 0x4F7A, 0x5154, 0x5152, 0x5155, 0x5169, 0x5177, 0x5176, 0x5178, 0x51BD, 0x51FD, 0x523B, 0x5238, 0x5237, 0x523A, 0x5230, 0x522E, 0x5236, 0x5241, 0x52BE, 0x52BB, 0x5352, 0x5354, 0x5353, 0x5351, 0x5366, 0x5377, 0x5378, 0x5379, 0x53D6, 0x53D4, 0x53D7, 0x5473, 0x5475, + 0x5496, 0x5478, 0x5495, 0x5480, 0x547B, 0x5477, 0x5484, 0x5492, 0x5486, 0x547C, 0x5490, 0x5471, 0x5476, 0x548C, 0x549A, 0x5462, 0x5468, 0x548B, 0x547D, 0x548E, 0x56FA, 0x5783, 0x5777, 0x576A, 0x5769, 0x5761, 0x5766, 0x5764, 0x577C, 0x591C, 0x5949, 0x5947, 0x5948, 0x5944, 0x5954, 0x59BE, 0x59BB, 0x59D4, 0x59B9, 0x59AE, 0x59D1, 0x59C6, 0x59D0, 0x59CD, 0x59CB, 0x59D3, 0x59CA, 0x59AF, 0x59B3, 0x59D2, 0x59C5, 0x5B5F, 0x5B64, 0x5B63, 0x5B97, 0x5B9A, 0x5B98, 0x5B9C, 0x5B99, 0x5B9B, 0x5C1A, 0x5C48, 0x5C45, 0x5C46, 0x5CB7, 0x5CA1, 0x5CB8, 0x5CA9, 0x5CAB, 0x5CB1, 0x5CB3, 0x5E18, 0x5E1A, 0x5E16, 0x5E15, 0x5E1B, 0x5E11, 0x5E78, 0x5E9A, 0x5E97, 0x5E9C, 0x5E95, 0x5E96, 0x5EF6, 0x5F26, 0x5F27, 0x5F29, 0x5F80, 0x5F81, 0x5F7F, 0x5F7C, 0x5FDD, 0x5FE0, 0x5FFD, 0x5FF5, 0x5FFF, 0x600F, 0x6014, 0x602F, 0x6035, 0x6016, 0x602A, 0x6015, 0x6021, 0x6027, 0x6029, 0x602B, 0x601B, 0x6216, 0x6215, 0x623F, 0x623E, 0x6240, 0x627F, 0x62C9, 0x62CC, 0x62C4, 0x62BF, 0x62C2, 0x62B9, 0x62D2, 0x62DB, 0x62AB, 0x62D3, 0x62D4, 0x62CB, 0x62C8, 0x62A8, 0x62BD, 0x62BC, 0x62D0, 0x62D9, 0x62C7, 0x62CD, 0x62B5, 0x62DA, 0x62B1, 0x62D8, 0x62D6, 0x62D7, 0x62C6, 0x62AC, 0x62CE, 0x653E, 0x65A7, 0x65BC, 0x65FA, 0x6614, 0x6613, 0x660C, 0x6606, 0x6602, 0x660E, 0x6600, 0x660F, 0x6615, 0x660A, + 0x6607, 0x670D, 0x670B, 0x676D, 0x678B, 0x6795, 0x6771, 0x679C, 0x6773, 0x6777, 0x6787, 0x679D, 0x6797, 0x676F, 0x6770, 0x677F, 0x6789, 0x677E, 0x6790, 0x6775, 0x679A, 0x6793, 0x677C, 0x676A, 0x6772, 0x6B23, 0x6B66, 0x6B67, 0x6B7F, 0x6C13, 0x6C1B, 0x6CE3, 0x6CE8, 0x6CF3, 0x6CB1, 0x6CCC, 0x6CE5, 0x6CB3, 0x6CBD, 0x6CBE, 0x6CBC, 0x6CE2, 0x6CAB, 0x6CD5, 0x6CD3, 0x6CB8, 0x6CC4, 0x6CB9, 0x6CC1, 0x6CAE, 0x6CD7, 0x6CC5, 0x6CF1, 0x6CBF, 0x6CBB, 0x6CE1, 0x6CDB, 0x6CCA, 0x6CAC, 0x6CEF, 0x6CDC, 0x6CD6, 0x6CE0, 0x7095, 0x708E, 0x7092, 0x708A, 0x7099, 0x722C, 0x722D, 0x7238, 0x7248, 0x7267, 0x7269, 0x72C0, 0x72CE, 0x72D9, 0x72D7, 0x72D0, 0x73A9, 0x73A8, 0x739F, 0x73AB, 0x73A5, 0x753D, 0x759D, 0x7599, 0x759A, 0x7684, 0x76C2, 0x76F2, 0x76F4, 0x77E5, 0x77FD, 0x793E, 0x7940, 0x7941, 0x79C9, 0x79C8, 0x7A7A, 0x7A79, 0x7AFA, 0x7CFE, 0x7F54, 0x7F8C, 0x7F8B, 0x8005, 0x80BA, 0x80A5, 0x80A2, 0x80B1, 0x80A1, 0x80AB, 0x80A9, 0x80B4, 0x80AA, 0x80AF, 0x81E5, 0x81FE, 0x820D, 0x82B3, 0x829D, 0x8299, 0x82AD, 0x82BD, 0x829F, 0x82B9, 0x82B1, 0x82AC, 0x82A5, 0x82AF, 0x82B8, 0x82A3, 0x82B0, 0x82BE, 0x82B7, 0x864E, 0x8671, 0x521D, 0x8868, 0x8ECB, 0x8FCE, 0x8FD4, 0x8FD1, 0x90B5, 0x90B8, 0x90B1, 0x90B6, 0x91C7, 0x91D1, 0x9577, 0x9580, 0x961C, 0x9640, 0x963F, 0x963B, 0x9644, + 0x9642, 0x96B9, 0x96E8, 0x9752, 0x975E, 0x4E9F, 0x4EAD, 0x4EAE, 0x4FE1, 0x4FB5, 0x4FAF, 0x4FBF, 0x4FE0, 0x4FD1, 0x4FCF, 0x4FDD, 0x4FC3, 0x4FB6, 0x4FD8, 0x4FDF, 0x4FCA, 0x4FD7, 0x4FAE, 0x4FD0, 0x4FC4, 0x4FC2, 0x4FDA, 0x4FCE, 0x4FDE, 0x4FB7, 0x5157, 0x5192, 0x5191, 0x51A0, 0x524E, 0x5243, 0x524A, 0x524D, 0x524C, 0x524B, 0x5247, 0x52C7, 0x52C9, 0x52C3, 0x52C1, 0x530D, 0x5357, 0x537B, 0x539A, 0x53DB, 0x54AC, 0x54C0, 0x54A8, 0x54CE, 0x54C9, 0x54B8, 0x54A6, 0x54B3, 0x54C7, 0x54C2, 0x54BD, 0x54AA, 0x54C1, 0x54C4, 0x54C8, 0x54AF, 0x54AB, 0x54B1, 0x54BB, 0x54A9, 0x54A7, 0x54BF, 0x56FF, 0x5782, 0x578B, 0x57A0, 0x57A3, 0x57A2, 0x57CE, 0x57AE, 0x5793, 0x5955, 0x5951, 0x594F, 0x594E, 0x5950, 0x59DC, 0x59D8, 0x59FF, 0x59E3, 0x59E8, 0x5A03, 0x59E5, 0x59EA, 0x59DA, 0x59E6, 0x5A01, 0x59FB, 0x5B69, 0x5BA3, 0x5BA6, 0x5BA4, 0x5BA2, 0x5BA5, 0x5C01, 0x5C4E, 0x5C4F, 0x5C4D, 0x5C4B, 0x5CD9, 0x5CD2, 0x5DF7, 0x5E1D, 0x5E25, 0x5E1F, 0x5E7D, 0x5EA0, 0x5EA6, 0x5EFA, 0x5F08, 0x5F2D, 0x5F65, 0x5F88, 0x5F85, 0x5F8A, 0x5F8B, 0x5F87, 0x5F8C, 0x5F89, 0x6012, 0x601D, 0x6020, 0x6025, 0x600E, 0x6028, 0x604D, 0x6070, 0x6068, 0x6062, 0x6046, 0x6043, 0x606C, 0x606B, 0x606A, 0x6064, 0x6241, 0x62DC, 0x6316, 0x6309, 0x62FC, 0x62ED, 0x6301, 0x62EE, 0x62FD, 0x6307, 0x62F1, 0x62F7, + 0x62EF, 0x62EC, 0x62FE, 0x62F4, 0x6311, 0x6302, 0x653F, 0x6545, 0x65AB, 0x65BD, 0x65E2, 0x6625, 0x662D, 0x6620, 0x6627, 0x662F, 0x661F, 0x6628, 0x6631, 0x6624, 0x66F7, 0x67FF, 0x67D3, 0x67F1, 0x67D4, 0x67D0, 0x67EC, 0x67B6, 0x67AF, 0x67F5, 0x67E9, 0x67EF, 0x67C4, 0x67D1, 0x67B4, 0x67DA, 0x67E5, 0x67B8, 0x67CF, 0x67DE, 0x67F3, 0x67B0, 0x67D9, 0x67E2, 0x67DD, 0x67D2, 0x6B6A, 0x6B83, 0x6B86, 0x6BB5, 0x6BD2, 0x6BD7, 0x6C1F, 0x6CC9, 0x6D0B, 0x6D32, 0x6D2A, 0x6D41, 0x6D25, 0x6D0C, 0x6D31, 0x6D1E, 0x6D17, 0x6D3B, 0x6D3D, 0x6D3E, 0x6D36, 0x6D1B, 0x6CF5, 0x6D39, 0x6D27, 0x6D38, 0x6D29, 0x6D2E, 0x6D35, 0x6D0E, 0x6D2B, 0x70AB, 0x70BA, 0x70B3, 0x70AC, 0x70AF, 0x70AD, 0x70B8, 0x70AE, 0x70A4, 0x7230, 0x7272, 0x726F, 0x7274, 0x72E9, 0x72E0, 0x72E1, 0x73B7, 0x73CA, 0x73BB, 0x73B2, 0x73CD, 0x73C0, 0x73B3, 0x751A, 0x752D, 0x754F, 0x754C, 0x754E, 0x754B, 0x75AB, 0x75A4, 0x75A5, 0x75A2, 0x75A3, 0x7678, 0x7686, 0x7687, 0x7688, 0x76C8, 0x76C6, 0x76C3, 0x76C5, 0x7701, 0x76F9, 0x76F8, 0x7709, 0x770B, 0x76FE, 0x76FC, 0x7707, 0x77DC, 0x7802, 0x7814, 0x780C, 0x780D, 0x7946, 0x7949, 0x7948, 0x7947, 0x79B9, 0x79BA, 0x79D1, 0x79D2, 0x79CB, 0x7A7F, 0x7A81, 0x7AFF, 0x7AFD, 0x7C7D, 0x7D02, 0x7D05, 0x7D00, 0x7D09, 0x7D07, 0x7D04, 0x7D06, 0x7F38, 0x7F8E, 0x7FBF, 0x8004, + 0x8010, 0x800D, 0x8011, 0x8036, 0x80D6, 0x80E5, 0x80DA, 0x80C3, 0x80C4, 0x80CC, 0x80E1, 0x80DB, 0x80CE, 0x80DE, 0x80E4, 0x80DD, 0x81F4, 0x8222, 0x82E7, 0x8303, 0x8305, 0x82E3, 0x82DB, 0x82E6, 0x8304, 0x82E5, 0x8302, 0x8309, 0x82D2, 0x82D7, 0x82F1, 0x8301, 0x82DC, 0x82D4, 0x82D1, 0x82DE, 0x82D3, 0x82DF, 0x82EF, 0x8306, 0x8650, 0x8679, 0x867B, 0x867A, 0x884D, 0x886B, 0x8981, 0x89D4, 0x8A08, 0x8A02, 0x8A03, 0x8C9E, 0x8CA0, 0x8D74, 0x8D73, 0x8DB4, 0x8ECD, 0x8ECC, 0x8FF0, 0x8FE6, 0x8FE2, 0x8FEA, 0x8FE5, 0x8FED, 0x8FEB, 0x8FE4, 0x8FE8, 0x90CA, 0x90CE, 0x90C1, 0x90C3, 0x914B, 0x914A, 0x91CD, 0x9582, 0x9650, 0x964B, 0x964C, 0x964D, 0x9762, 0x9769, 0x97CB, 0x97ED, 0x97F3, 0x9801, 0x98A8, 0x98DB, 0x98DF, 0x9996, 0x9999, 0x4E58, 0x4EB3, 0x500C, 0x500D, 0x5023, 0x4FEF, 0x5026, 0x5025, 0x4FF8, 0x5029, 0x5016, 0x5006, 0x503C, 0x501F, 0x501A, 0x5012, 0x5011, 0x4FFA, 0x5000, 0x5014, 0x5028, 0x4FF1, 0x5021, 0x500B, 0x5019, 0x5018, 0x4FF3, 0x4FEE, 0x502D, 0x502A, 0x4FFE, 0x502B, 0x5009, 0x517C, 0x51A4, 0x51A5, 0x51A2, 0x51CD, 0x51CC, 0x51C6, 0x51CB, 0x5256, 0x525C, 0x5254, 0x525B, 0x525D, 0x532A, 0x537F, 0x539F, 0x539D, 0x53DF, 0x54E8, 0x5510, 0x5501, 0x5537, 0x54FC, 0x54E5, 0x54F2, 0x5506, 0x54FA, 0x5514, 0x54E9, 0x54ED, 0x54E1, 0x5509, 0x54EE, 0x54EA, + 0x54E6, 0x5527, 0x5507, 0x54FD, 0x550F, 0x5703, 0x5704, 0x57C2, 0x57D4, 0x57CB, 0x57C3, 0x5809, 0x590F, 0x5957, 0x5958, 0x595A, 0x5A11, 0x5A18, 0x5A1C, 0x5A1F, 0x5A1B, 0x5A13, 0x59EC, 0x5A20, 0x5A23, 0x5A29, 0x5A25, 0x5A0C, 0x5A09, 0x5B6B, 0x5C58, 0x5BB0, 0x5BB3, 0x5BB6, 0x5BB4, 0x5BAE, 0x5BB5, 0x5BB9, 0x5BB8, 0x5C04, 0x5C51, 0x5C55, 0x5C50, 0x5CED, 0x5CFD, 0x5CFB, 0x5CEA, 0x5CE8, 0x5CF0, 0x5CF6, 0x5D01, 0x5CF4, 0x5DEE, 0x5E2D, 0x5E2B, 0x5EAB, 0x5EAD, 0x5EA7, 0x5F31, 0x5F92, 0x5F91, 0x5F90, 0x6059, 0x6063, 0x6065, 0x6050, 0x6055, 0x606D, 0x6069, 0x606F, 0x6084, 0x609F, 0x609A, 0x608D, 0x6094, 0x608C, 0x6085, 0x6096, 0x6247, 0x62F3, 0x6308, 0x62FF, 0x634E, 0x633E, 0x632F, 0x6355, 0x6342, 0x6346, 0x634F, 0x6349, 0x633A, 0x6350, 0x633D, 0x632A, 0x632B, 0x6328, 0x634D, 0x634C, 0x6548, 0x6549, 0x6599, 0x65C1, 0x65C5, 0x6642, 0x6649, 0x664F, 0x6643, 0x6652, 0x664C, 0x6645, 0x6641, 0x66F8, 0x6714, 0x6715, 0x6717, 0x6821, 0x6838, 0x6848, 0x6846, 0x6853, 0x6839, 0x6842, 0x6854, 0x6829, 0x68B3, 0x6817, 0x684C, 0x6851, 0x683D, 0x67F4, 0x6850, 0x6840, 0x683C, 0x6843, 0x682A, 0x6845, 0x6813, 0x6818, 0x6841, 0x6B8A, 0x6B89, 0x6BB7, 0x6C23, 0x6C27, 0x6C28, 0x6C26, 0x6C24, 0x6CF0, 0x6D6A, 0x6D95, 0x6D88, 0x6D87, 0x6D66, 0x6D78, 0x6D77, 0x6D59, 0x6D93, + 0x6D6C, 0x6D89, 0x6D6E, 0x6D5A, 0x6D74, 0x6D69, 0x6D8C, 0x6D8A, 0x6D79, 0x6D85, 0x6D65, 0x6D94, 0x70CA, 0x70D8, 0x70E4, 0x70D9, 0x70C8, 0x70CF, 0x7239, 0x7279, 0x72FC, 0x72F9, 0x72FD, 0x72F8, 0x72F7, 0x7386, 0x73ED, 0x7409, 0x73EE, 0x73E0, 0x73EA, 0x73DE, 0x7554, 0x755D, 0x755C, 0x755A, 0x7559, 0x75BE, 0x75C5, 0x75C7, 0x75B2, 0x75B3, 0x75BD, 0x75BC, 0x75B9, 0x75C2, 0x75B8, 0x768B, 0x76B0, 0x76CA, 0x76CD, 0x76CE, 0x7729, 0x771F, 0x7720, 0x7728, 0x77E9, 0x7830, 0x7827, 0x7838, 0x781D, 0x7834, 0x7837, 0x7825, 0x782D, 0x7820, 0x781F, 0x7832, 0x7955, 0x7950, 0x7960, 0x795F, 0x7956, 0x795E, 0x795D, 0x7957, 0x795A, 0x79E4, 0x79E3, 0x79E7, 0x79DF, 0x79E6, 0x79E9, 0x79D8, 0x7A84, 0x7A88, 0x7AD9, 0x7B06, 0x7B11, 0x7C89, 0x7D21, 0x7D17, 0x7D0B, 0x7D0A, 0x7D20, 0x7D22, 0x7D14, 0x7D10, 0x7D15, 0x7D1A, 0x7D1C, 0x7D0D, 0x7D19, 0x7D1B, 0x7F3A, 0x7F5F, 0x7F94, 0x7FC5, 0x7FC1, 0x8006, 0x8018, 0x8015, 0x8019, 0x8017, 0x803D, 0x803F, 0x80F1, 0x8102, 0x80F0, 0x8105, 0x80ED, 0x80F4, 0x8106, 0x80F8, 0x80F3, 0x8108, 0x80FD, 0x810A, 0x80FC, 0x80EF, 0x81ED, 0x81EC, 0x8200, 0x8210, 0x822A, 0x822B, 0x8228, 0x822C, 0x82BB, 0x832B, 0x8352, 0x8354, 0x834A, 0x8338, 0x8350, 0x8349, 0x8335, 0x8334, 0x834F, 0x8332, 0x8339, 0x8336, 0x8317, 0x8340, 0x8331, 0x8328, 0x8343, + 0x8654, 0x868A, 0x86AA, 0x8693, 0x86A4, 0x86A9, 0x868C, 0x86A3, 0x869C, 0x8870, 0x8877, 0x8881, 0x8882, 0x887D, 0x8879, 0x8A18, 0x8A10, 0x8A0E, 0x8A0C, 0x8A15, 0x8A0A, 0x8A17, 0x8A13, 0x8A16, 0x8A0F, 0x8A11, 0x8C48, 0x8C7A, 0x8C79, 0x8CA1, 0x8CA2, 0x8D77, 0x8EAC, 0x8ED2, 0x8ED4, 0x8ECF, 0x8FB1, 0x9001, 0x9006, 0x8FF7, 0x9000, 0x8FFA, 0x8FF4, 0x9003, 0x8FFD, 0x9005, 0x8FF8, 0x9095, 0x90E1, 0x90DD, 0x90E2, 0x9152, 0x914D, 0x914C, 0x91D8, 0x91DD, 0x91D7, 0x91DC, 0x91D9, 0x9583, 0x9662, 0x9663, 0x9661, 0x965B, 0x965D, 0x9664, 0x9658, 0x965E, 0x96BB, 0x98E2, 0x99AC, 0x9AA8, 0x9AD8, 0x9B25, 0x9B32, 0x9B3C, 0x4E7E, 0x507A, 0x507D, 0x505C, 0x5047, 0x5043, 0x504C, 0x505A, 0x5049, 0x5065, 0x5076, 0x504E, 0x5055, 0x5075, 0x5074, 0x5077, 0x504F, 0x500F, 0x506F, 0x506D, 0x515C, 0x5195, 0x51F0, 0x526A, 0x526F, 0x52D2, 0x52D9, 0x52D8, 0x52D5, 0x5310, 0x530F, 0x5319, 0x533F, 0x5340, 0x533E, 0x53C3, 0x66FC, 0x5546, 0x556A, 0x5566, 0x5544, 0x555E, 0x5561, 0x5543, 0x554A, 0x5531, 0x5556, 0x554F, 0x5555, 0x552F, 0x5564, 0x5538, 0x552E, 0x555C, 0x552C, 0x5563, 0x5533, 0x5541, 0x5557, 0x5708, 0x570B, 0x5709, 0x57DF, 0x5805, 0x580A, 0x5806, 0x57E0, 0x57E4, 0x57FA, 0x5802, 0x5835, 0x57F7, 0x57F9, 0x5920, 0x5962, 0x5A36, 0x5A41, 0x5A49, 0x5A66, 0x5A6A, 0x5A40, + 0x5A3C, 0x5A62, 0x5A5A, 0x5A46, 0x5A4A, 0x5B70, 0x5BC7, 0x5BC5, 0x5BC4, 0x5BC2, 0x5BBF, 0x5BC6, 0x5C09, 0x5C08, 0x5C07, 0x5C60, 0x5C5C, 0x5C5D, 0x5D07, 0x5D06, 0x5D0E, 0x5D1B, 0x5D16, 0x5D22, 0x5D11, 0x5D29, 0x5D14, 0x5D19, 0x5D24, 0x5D27, 0x5D17, 0x5DE2, 0x5E38, 0x5E36, 0x5E33, 0x5E37, 0x5EB7, 0x5EB8, 0x5EB6, 0x5EB5, 0x5EBE, 0x5F35, 0x5F37, 0x5F57, 0x5F6C, 0x5F69, 0x5F6B, 0x5F97, 0x5F99, 0x5F9E, 0x5F98, 0x5FA1, 0x5FA0, 0x5F9C, 0x607F, 0x60A3, 0x6089, 0x60A0, 0x60A8, 0x60CB, 0x60B4, 0x60E6, 0x60BD, 0x60C5, 0x60BB, 0x60B5, 0x60DC, 0x60BC, 0x60D8, 0x60D5, 0x60C6, 0x60DF, 0x60B8, 0x60DA, 0x60C7, 0x621A, 0x621B, 0x6248, 0x63A0, 0x63A7, 0x6372, 0x6396, 0x63A2, 0x63A5, 0x6377, 0x6367, 0x6398, 0x63AA, 0x6371, 0x63A9, 0x6389, 0x6383, 0x639B, 0x636B, 0x63A8, 0x6384, 0x6388, 0x6399, 0x63A1, 0x63AC, 0x6392, 0x638F, 0x6380, 0x637B, 0x6369, 0x6368, 0x637A, 0x655D, 0x6556, 0x6551, 0x6559, 0x6557, 0x555F, 0x654F, 0x6558, 0x6555, 0x6554, 0x659C, 0x659B, 0x65AC, 0x65CF, 0x65CB, 0x65CC, 0x65CE, 0x665D, 0x665A, 0x6664, 0x6668, 0x6666, 0x665E, 0x66F9, 0x52D7, 0x671B, 0x6881, 0x68AF, 0x68A2, 0x6893, 0x68B5, 0x687F, 0x6876, 0x68B1, 0x68A7, 0x6897, 0x68B0, 0x6883, 0x68C4, 0x68AD, 0x6886, 0x6885, 0x6894, 0x689D, 0x68A8, 0x689F, 0x68A1, 0x6882, 0x6B32, 0x6BBA, + 0x6BEB, 0x6BEC, 0x6C2B, 0x6D8E, 0x6DBC, 0x6DF3, 0x6DD9, 0x6DB2, 0x6DE1, 0x6DCC, 0x6DE4, 0x6DFB, 0x6DFA, 0x6E05, 0x6DC7, 0x6DCB, 0x6DAF, 0x6DD1, 0x6DAE, 0x6DDE, 0x6DF9, 0x6DB8, 0x6DF7, 0x6DF5, 0x6DC5, 0x6DD2, 0x6E1A, 0x6DB5, 0x6DDA, 0x6DEB, 0x6DD8, 0x6DEA, 0x6DF1, 0x6DEE, 0x6DE8, 0x6DC6, 0x6DC4, 0x6DAA, 0x6DEC, 0x6DBF, 0x6DE6, 0x70F9, 0x7109, 0x710A, 0x70FD, 0x70EF, 0x723D, 0x727D, 0x7281, 0x731C, 0x731B, 0x7316, 0x7313, 0x7319, 0x7387, 0x7405, 0x740A, 0x7403, 0x7406, 0x73FE, 0x740D, 0x74E0, 0x74F6, 0x74F7, 0x751C, 0x7522, 0x7565, 0x7566, 0x7562, 0x7570, 0x758F, 0x75D4, 0x75D5, 0x75B5, 0x75CA, 0x75CD, 0x768E, 0x76D4, 0x76D2, 0x76DB, 0x7737, 0x773E, 0x773C, 0x7736, 0x7738, 0x773A, 0x786B, 0x7843, 0x784E, 0x7965, 0x7968, 0x796D, 0x79FB, 0x7A92, 0x7A95, 0x7B20, 0x7B28, 0x7B1B, 0x7B2C, 0x7B26, 0x7B19, 0x7B1E, 0x7B2E, 0x7C92, 0x7C97, 0x7C95, 0x7D46, 0x7D43, 0x7D71, 0x7D2E, 0x7D39, 0x7D3C, 0x7D40, 0x7D30, 0x7D33, 0x7D44, 0x7D2F, 0x7D42, 0x7D32, 0x7D31, 0x7F3D, 0x7F9E, 0x7F9A, 0x7FCC, 0x7FCE, 0x7FD2, 0x801C, 0x804A, 0x8046, 0x812F, 0x8116, 0x8123, 0x812B, 0x8129, 0x8130, 0x8124, 0x8202, 0x8235, 0x8237, 0x8236, 0x8239, 0x838E, 0x839E, 0x8398, 0x8378, 0x83A2, 0x8396, 0x83BD, 0x83AB, 0x8392, 0x838A, 0x8393, 0x8389, 0x83A0, 0x8377, 0x837B, 0x837C, + 0x8386, 0x83A7, 0x8655, 0x5F6A, 0x86C7, 0x86C0, 0x86B6, 0x86C4, 0x86B5, 0x86C6, 0x86CB, 0x86B1, 0x86AF, 0x86C9, 0x8853, 0x889E, 0x8888, 0x88AB, 0x8892, 0x8896, 0x888D, 0x888B, 0x8993, 0x898F, 0x8A2A, 0x8A1D, 0x8A23, 0x8A25, 0x8A31, 0x8A2D, 0x8A1F, 0x8A1B, 0x8A22, 0x8C49, 0x8C5A, 0x8CA9, 0x8CAC, 0x8CAB, 0x8CA8, 0x8CAA, 0x8CA7, 0x8D67, 0x8D66, 0x8DBE, 0x8DBA, 0x8EDB, 0x8EDF, 0x9019, 0x900D, 0x901A, 0x9017, 0x9023, 0x901F, 0x901D, 0x9010, 0x9015, 0x901E, 0x9020, 0x900F, 0x9022, 0x9016, 0x901B, 0x9014, 0x90E8, 0x90ED, 0x90FD, 0x9157, 0x91CE, 0x91F5, 0x91E6, 0x91E3, 0x91E7, 0x91ED, 0x91E9, 0x9589, 0x966A, 0x9675, 0x9673, 0x9678, 0x9670, 0x9674, 0x9676, 0x9677, 0x966C, 0x96C0, 0x96EA, 0x96E9, 0x7AE0, 0x7ADF, 0x9802, 0x9803, 0x9B5A, 0x9CE5, 0x9E75, 0x9E7F, 0x9EA5, 0x9EBB, 0x50A2, 0x508D, 0x5085, 0x5099, 0x5091, 0x5080, 0x5096, 0x5098, 0x509A, 0x6700, 0x51F1, 0x5272, 0x5274, 0x5275, 0x5269, 0x52DE, 0x52DD, 0x52DB, 0x535A, 0x53A5, 0x557B, 0x5580, 0x55A7, 0x557C, 0x558A, 0x559D, 0x5598, 0x5582, 0x559C, 0x55AA, 0x5594, 0x5587, 0x558B, 0x5583, 0x55B3, 0x55AE, 0x559F, 0x553E, 0x55B2, 0x559A, 0x55BB, 0x55AC, 0x55B1, 0x557E, 0x5589, 0x55AB, 0x5599, 0x570D, 0x582F, 0x582A, 0x5834, 0x5824, 0x5830, 0x5831, 0x5821, 0x581D, 0x5820, 0x58F9, 0x58FA, 0x5960, + 0x5A77, 0x5A9A, 0x5A7F, 0x5A92, 0x5A9B, 0x5AA7, 0x5B73, 0x5B71, 0x5BD2, 0x5BCC, 0x5BD3, 0x5BD0, 0x5C0A, 0x5C0B, 0x5C31, 0x5D4C, 0x5D50, 0x5D34, 0x5D47, 0x5DFD, 0x5E45, 0x5E3D, 0x5E40, 0x5E43, 0x5E7E, 0x5ECA, 0x5EC1, 0x5EC2, 0x5EC4, 0x5F3C, 0x5F6D, 0x5FA9, 0x5FAA, 0x5FA8, 0x60D1, 0x60E1, 0x60B2, 0x60B6, 0x60E0, 0x611C, 0x6123, 0x60FA, 0x6115, 0x60F0, 0x60FB, 0x60F4, 0x6168, 0x60F1, 0x610E, 0x60F6, 0x6109, 0x6100, 0x6112, 0x621F, 0x6249, 0x63A3, 0x638C, 0x63CF, 0x63C0, 0x63E9, 0x63C9, 0x63C6, 0x63CD, 0x63D2, 0x63E3, 0x63D0, 0x63E1, 0x63D6, 0x63ED, 0x63EE, 0x6376, 0x63F4, 0x63EA, 0x63DB, 0x6452, 0x63DA, 0x63F9, 0x655E, 0x6566, 0x6562, 0x6563, 0x6591, 0x6590, 0x65AF, 0x666E, 0x6670, 0x6674, 0x6676, 0x666F, 0x6691, 0x667A, 0x667E, 0x6677, 0x66FE, 0x66FF, 0x671F, 0x671D, 0x68FA, 0x68D5, 0x68E0, 0x68D8, 0x68D7, 0x6905, 0x68DF, 0x68F5, 0x68EE, 0x68E7, 0x68F9, 0x68D2, 0x68F2, 0x68E3, 0x68CB, 0x68CD, 0x690D, 0x6912, 0x690E, 0x68C9, 0x68DA, 0x696E, 0x68FB, 0x6B3E, 0x6B3A, 0x6B3D, 0x6B98, 0x6B96, 0x6BBC, 0x6BEF, 0x6C2E, 0x6C2F, 0x6C2C, 0x6E2F, 0x6E38, 0x6E54, 0x6E21, 0x6E32, 0x6E67, 0x6E4A, 0x6E20, 0x6E25, 0x6E23, 0x6E1B, 0x6E5B, 0x6E58, 0x6E24, 0x6E56, 0x6E6E, 0x6E2D, 0x6E26, 0x6E6F, 0x6E34, 0x6E4D, 0x6E3A, 0x6E2C, 0x6E43, 0x6E1D, 0x6E3E, 0x6ECB, + 0x6E89, 0x6E19, 0x6E4E, 0x6E63, 0x6E44, 0x6E72, 0x6E69, 0x6E5F, 0x7119, 0x711A, 0x7126, 0x7130, 0x7121, 0x7136, 0x716E, 0x711C, 0x724C, 0x7284, 0x7280, 0x7336, 0x7325, 0x7334, 0x7329, 0x743A, 0x742A, 0x7433, 0x7422, 0x7425, 0x7435, 0x7436, 0x7434, 0x742F, 0x741B, 0x7426, 0x7428, 0x7525, 0x7526, 0x756B, 0x756A, 0x75E2, 0x75DB, 0x75E3, 0x75D9, 0x75D8, 0x75DE, 0x75E0, 0x767B, 0x767C, 0x7696, 0x7693, 0x76B4, 0x76DC, 0x774F, 0x77ED, 0x785D, 0x786C, 0x786F, 0x7A0D, 0x7A08, 0x7A0B, 0x7A05, 0x7A00, 0x7A98, 0x7A97, 0x7A96, 0x7AE5, 0x7AE3, 0x7B49, 0x7B56, 0x7B46, 0x7B50, 0x7B52, 0x7B54, 0x7B4D, 0x7B4B, 0x7B4F, 0x7B51, 0x7C9F, 0x7CA5, 0x7D5E, 0x7D50, 0x7D68, 0x7D55, 0x7D2B, 0x7D6E, 0x7D72, 0x7D61, 0x7D66, 0x7D62, 0x7D70, 0x7D73, 0x5584, 0x7FD4, 0x7FD5, 0x800B, 0x8052, 0x8085, 0x8155, 0x8154, 0x814B, 0x8151, 0x814E, 0x8139, 0x8146, 0x813E, 0x814C, 0x8153, 0x8174, 0x8212, 0x821C, 0x83E9, 0x8403, 0x83F8, 0x840D, 0x83E0, 0x83C5, 0x840B, 0x83C1, 0x83EF, 0x83F1, 0x83F4, 0x8457, 0x840A, 0x83F0, 0x840C, 0x83CC, 0x83FD, 0x83F2, 0x83CA, 0x8438, 0x840E, 0x8404, 0x83DC, 0x8407, 0x83D4, 0x83DF, 0x865B, 0x86DF, 0x86D9, 0x86ED, 0x86D4, 0x86DB, 0x86E4, 0x86D0, 0x86DE, 0x8857, 0x88C1, 0x88C2, 0x88B1, 0x8983, 0x8996, 0x8A3B, 0x8A60, 0x8A55, 0x8A5E, 0x8A3C, 0x8A41, + 0x8A54, 0x8A5B, 0x8A50, 0x8A46, 0x8A34, 0x8A3A, 0x8A36, 0x8A56, 0x8C61, 0x8C82, 0x8CAF, 0x8CBC, 0x8CB3, 0x8CBD, 0x8CC1, 0x8CBB, 0x8CC0, 0x8CB4, 0x8CB7, 0x8CB6, 0x8CBF, 0x8CB8, 0x8D8A, 0x8D85, 0x8D81, 0x8DCE, 0x8DDD, 0x8DCB, 0x8DDA, 0x8DD1, 0x8DCC, 0x8DDB, 0x8DC6, 0x8EFB, 0x8EF8, 0x8EFC, 0x8F9C, 0x902E, 0x9035, 0x9031, 0x9038, 0x9032, 0x9036, 0x9102, 0x90F5, 0x9109, 0x90FE, 0x9163, 0x9165, 0x91CF, 0x9214, 0x9215, 0x9223, 0x9209, 0x921E, 0x920D, 0x9210, 0x9207, 0x9211, 0x9594, 0x958F, 0x958B, 0x9591, 0x9593, 0x9592, 0x958E, 0x968A, 0x968E, 0x968B, 0x967D, 0x9685, 0x9686, 0x968D, 0x9672, 0x9684, 0x96C1, 0x96C5, 0x96C4, 0x96C6, 0x96C7, 0x96EF, 0x96F2, 0x97CC, 0x9805, 0x9806, 0x9808, 0x98E7, 0x98EA, 0x98EF, 0x98E9, 0x98F2, 0x98ED, 0x99AE, 0x99AD, 0x9EC3, 0x9ECD, 0x9ED1, 0x4E82, 0x50AD, 0x50B5, 0x50B2, 0x50B3, 0x50C5, 0x50BE, 0x50AC, 0x50B7, 0x50BB, 0x50AF, 0x50C7, 0x527F, 0x5277, 0x527D, 0x52DF, 0x52E6, 0x52E4, 0x52E2, 0x52E3, 0x532F, 0x55DF, 0x55E8, 0x55D3, 0x55E6, 0x55CE, 0x55DC, 0x55C7, 0x55D1, 0x55E3, 0x55E4, 0x55EF, 0x55DA, 0x55E1, 0x55C5, 0x55C6, 0x55E5, 0x55C9, 0x5712, 0x5713, 0x585E, 0x5851, 0x5858, 0x5857, 0x585A, 0x5854, 0x586B, 0x584C, 0x586D, 0x584A, 0x5862, 0x5852, 0x584B, 0x5967, 0x5AC1, 0x5AC9, 0x5ACC, 0x5ABE, 0x5ABD, 0x5ABC, + 0x5AB3, 0x5AC2, 0x5AB2, 0x5D69, 0x5D6F, 0x5E4C, 0x5E79, 0x5EC9, 0x5EC8, 0x5F12, 0x5F59, 0x5FAC, 0x5FAE, 0x611A, 0x610F, 0x6148, 0x611F, 0x60F3, 0x611B, 0x60F9, 0x6101, 0x6108, 0x614E, 0x614C, 0x6144, 0x614D, 0x613E, 0x6134, 0x6127, 0x610D, 0x6106, 0x6137, 0x6221, 0x6222, 0x6413, 0x643E, 0x641E, 0x642A, 0x642D, 0x643D, 0x642C, 0x640F, 0x641C, 0x6414, 0x640D, 0x6436, 0x6416, 0x6417, 0x6406, 0x656C, 0x659F, 0x65B0, 0x6697, 0x6689, 0x6687, 0x6688, 0x6696, 0x6684, 0x6698, 0x668D, 0x6703, 0x6994, 0x696D, 0x695A, 0x6977, 0x6960, 0x6954, 0x6975, 0x6930, 0x6982, 0x694A, 0x6968, 0x696B, 0x695E, 0x6953, 0x6979, 0x6986, 0x695D, 0x6963, 0x695B, 0x6B47, 0x6B72, 0x6BC0, 0x6BBF, 0x6BD3, 0x6BFD, 0x6EA2, 0x6EAF, 0x6ED3, 0x6EB6, 0x6EC2, 0x6E90, 0x6E9D, 0x6EC7, 0x6EC5, 0x6EA5, 0x6E98, 0x6EBC, 0x6EBA, 0x6EAB, 0x6ED1, 0x6E96, 0x6E9C, 0x6EC4, 0x6ED4, 0x6EAA, 0x6EA7, 0x6EB4, 0x714E, 0x7159, 0x7169, 0x7164, 0x7149, 0x7167, 0x715C, 0x716C, 0x7166, 0x714C, 0x7165, 0x715E, 0x7146, 0x7168, 0x7156, 0x723A, 0x7252, 0x7337, 0x7345, 0x733F, 0x733E, 0x746F, 0x745A, 0x7455, 0x745F, 0x745E, 0x7441, 0x743F, 0x7459, 0x745B, 0x745C, 0x7576, 0x7578, 0x7600, 0x75F0, 0x7601, 0x75F2, 0x75F1, 0x75FA, 0x75FF, 0x75F4, 0x75F3, 0x76DE, 0x76DF, 0x775B, 0x776B, 0x7766, 0x775E, 0x7763, + 0x7779, 0x776A, 0x776C, 0x775C, 0x7765, 0x7768, 0x7762, 0x77EE, 0x788E, 0x78B0, 0x7897, 0x7898, 0x788C, 0x7889, 0x787C, 0x7891, 0x7893, 0x787F, 0x797A, 0x797F, 0x7981, 0x842C, 0x79BD, 0x7A1C, 0x7A1A, 0x7A20, 0x7A14, 0x7A1F, 0x7A1E, 0x7A9F, 0x7AA0, 0x7B77, 0x7BC0, 0x7B60, 0x7B6E, 0x7B67, 0x7CB1, 0x7CB3, 0x7CB5, 0x7D93, 0x7D79, 0x7D91, 0x7D81, 0x7D8F, 0x7D5B, 0x7F6E, 0x7F69, 0x7F6A, 0x7F72, 0x7FA9, 0x7FA8, 0x7FA4, 0x8056, 0x8058, 0x8086, 0x8084, 0x8171, 0x8170, 0x8178, 0x8165, 0x816E, 0x8173, 0x816B, 0x8179, 0x817A, 0x8166, 0x8205, 0x8247, 0x8482, 0x8477, 0x843D, 0x8431, 0x8475, 0x8466, 0x846B, 0x8449, 0x846C, 0x845B, 0x843C, 0x8435, 0x8461, 0x8463, 0x8469, 0x846D, 0x8446, 0x865E, 0x865C, 0x865F, 0x86F9, 0x8713, 0x8708, 0x8707, 0x8700, 0x86FE, 0x86FB, 0x8702, 0x8703, 0x8706, 0x870A, 0x8859, 0x88DF, 0x88D4, 0x88D9, 0x88DC, 0x88D8, 0x88DD, 0x88E1, 0x88CA, 0x88D5, 0x88D2, 0x899C, 0x89E3, 0x8A6B, 0x8A72, 0x8A73, 0x8A66, 0x8A69, 0x8A70, 0x8A87, 0x8A7C, 0x8A63, 0x8AA0, 0x8A71, 0x8A85, 0x8A6D, 0x8A62, 0x8A6E, 0x8A6C, 0x8A79, 0x8A7B, 0x8A3E, 0x8A68, 0x8C62, 0x8C8A, 0x8C89, 0x8CCA, 0x8CC7, 0x8CC8, 0x8CC4, 0x8CB2, 0x8CC3, 0x8CC2, 0x8CC5, 0x8DE1, 0x8DDF, 0x8DE8, 0x8DEF, 0x8DF3, 0x8DFA, 0x8DEA, 0x8DE4, 0x8DE6, 0x8EB2, 0x8F03, 0x8F09, 0x8EFE, 0x8F0A, + 0x8F9F, 0x8FB2, 0x904B, 0x904A, 0x9053, 0x9042, 0x9054, 0x903C, 0x9055, 0x9050, 0x9047, 0x904F, 0x904E, 0x904D, 0x9051, 0x903E, 0x9041, 0x9112, 0x9117, 0x916C, 0x916A, 0x9169, 0x91C9, 0x9237, 0x9257, 0x9238, 0x923D, 0x9240, 0x923E, 0x925B, 0x924B, 0x9264, 0x9251, 0x9234, 0x9249, 0x924D, 0x9245, 0x9239, 0x923F, 0x925A, 0x9598, 0x9698, 0x9694, 0x9695, 0x96CD, 0x96CB, 0x96C9, 0x96CA, 0x96F7, 0x96FB, 0x96F9, 0x96F6, 0x9756, 0x9774, 0x9776, 0x9810, 0x9811, 0x9813, 0x980A, 0x9812, 0x980C, 0x98FC, 0x98F4, 0x98FD, 0x98FE, 0x99B3, 0x99B1, 0x99B4, 0x9AE1, 0x9CE9, 0x9E82, 0x9F0E, 0x9F13, 0x9F20, 0x50E7, 0x50EE, 0x50E5, 0x50D6, 0x50ED, 0x50DA, 0x50D5, 0x50CF, 0x50D1, 0x50F1, 0x50CE, 0x50E9, 0x5162, 0x51F3, 0x5283, 0x5282, 0x5331, 0x53AD, 0x55FE, 0x5600, 0x561B, 0x5617, 0x55FD, 0x5614, 0x5606, 0x5609, 0x560D, 0x560E, 0x55F7, 0x5616, 0x561F, 0x5608, 0x5610, 0x55F6, 0x5718, 0x5716, 0x5875, 0x587E, 0x5883, 0x5893, 0x588A, 0x5879, 0x5885, 0x587D, 0x58FD, 0x5925, 0x5922, 0x5924, 0x596A, 0x5969, 0x5AE1, 0x5AE6, 0x5AE9, 0x5AD7, 0x5AD6, 0x5AD8, 0x5AE3, 0x5B75, 0x5BDE, 0x5BE7, 0x5BE1, 0x5BE5, 0x5BE6, 0x5BE8, 0x5BE2, 0x5BE4, 0x5BDF, 0x5C0D, 0x5C62, 0x5D84, 0x5D87, 0x5E5B, 0x5E63, 0x5E55, 0x5E57, 0x5E54, 0x5ED3, 0x5ED6, 0x5F0A, 0x5F46, 0x5F70, 0x5FB9, 0x6147, + 0x613F, 0x614B, 0x6177, 0x6162, 0x6163, 0x615F, 0x615A, 0x6158, 0x6175, 0x622A, 0x6487, 0x6458, 0x6454, 0x64A4, 0x6478, 0x645F, 0x647A, 0x6451, 0x6467, 0x6434, 0x646D, 0x647B, 0x6572, 0x65A1, 0x65D7, 0x65D6, 0x66A2, 0x66A8, 0x669D, 0x699C, 0x69A8, 0x6995, 0x69C1, 0x69AE, 0x69D3, 0x69CB, 0x699B, 0x69B7, 0x69BB, 0x69AB, 0x69B4, 0x69D0, 0x69CD, 0x69AD, 0x69CC, 0x69A6, 0x69C3, 0x69A3, 0x6B49, 0x6B4C, 0x6C33, 0x6F33, 0x6F14, 0x6EFE, 0x6F13, 0x6EF4, 0x6F29, 0x6F3E, 0x6F20, 0x6F2C, 0x6F0F, 0x6F02, 0x6F22, 0x6EFF, 0x6EEF, 0x6F06, 0x6F31, 0x6F38, 0x6F32, 0x6F23, 0x6F15, 0x6F2B, 0x6F2F, 0x6F88, 0x6F2A, 0x6EEC, 0x6F01, 0x6EF2, 0x6ECC, 0x6EF7, 0x7194, 0x7199, 0x717D, 0x718A, 0x7184, 0x7192, 0x723E, 0x7292, 0x7296, 0x7344, 0x7350, 0x7464, 0x7463, 0x746A, 0x7470, 0x746D, 0x7504, 0x7591, 0x7627, 0x760D, 0x760B, 0x7609, 0x7613, 0x76E1, 0x76E3, 0x7784, 0x777D, 0x777F, 0x7761, 0x78C1, 0x789F, 0x78A7, 0x78B3, 0x78A9, 0x78A3, 0x798E, 0x798F, 0x798D, 0x7A2E, 0x7A31, 0x7AAA, 0x7AA9, 0x7AED, 0x7AEF, 0x7BA1, 0x7B95, 0x7B8B, 0x7B75, 0x7B97, 0x7B9D, 0x7B94, 0x7B8F, 0x7BB8, 0x7B87, 0x7B84, 0x7CB9, 0x7CBD, 0x7CBE, 0x7DBB, 0x7DB0, 0x7D9C, 0x7DBD, 0x7DBE, 0x7DA0, 0x7DCA, 0x7DB4, 0x7DB2, 0x7DB1, 0x7DBA, 0x7DA2, 0x7DBF, 0x7DB5, 0x7DB8, 0x7DAD, 0x7DD2, 0x7DC7, 0x7DAC, + 0x7F70, 0x7FE0, 0x7FE1, 0x7FDF, 0x805E, 0x805A, 0x8087, 0x8150, 0x8180, 0x818F, 0x8188, 0x818A, 0x817F, 0x8182, 0x81E7, 0x81FA, 0x8207, 0x8214, 0x821E, 0x824B, 0x84C9, 0x84BF, 0x84C6, 0x84C4, 0x8499, 0x849E, 0x84B2, 0x849C, 0x84CB, 0x84B8, 0x84C0, 0x84D3, 0x8490, 0x84BC, 0x84D1, 0x84CA, 0x873F, 0x871C, 0x873B, 0x8722, 0x8725, 0x8734, 0x8718, 0x8755, 0x8737, 0x8729, 0x88F3, 0x8902, 0x88F4, 0x88F9, 0x88F8, 0x88FD, 0x88E8, 0x891A, 0x88EF, 0x8AA6, 0x8A8C, 0x8A9E, 0x8AA3, 0x8A8D, 0x8AA1, 0x8A93, 0x8AA4, 0x8AAA, 0x8AA5, 0x8AA8, 0x8A98, 0x8A91, 0x8A9A, 0x8AA7, 0x8C6A, 0x8C8D, 0x8C8C, 0x8CD3, 0x8CD1, 0x8CD2, 0x8D6B, 0x8D99, 0x8D95, 0x8DFC, 0x8F14, 0x8F12, 0x8F15, 0x8F13, 0x8FA3, 0x9060, 0x9058, 0x905C, 0x9063, 0x9059, 0x905E, 0x9062, 0x905D, 0x905B, 0x9119, 0x9118, 0x911E, 0x9175, 0x9178, 0x9177, 0x9174, 0x9278, 0x9280, 0x9285, 0x9298, 0x9296, 0x927B, 0x9293, 0x929C, 0x92A8, 0x927C, 0x9291, 0x95A1, 0x95A8, 0x95A9, 0x95A3, 0x95A5, 0x95A4, 0x9699, 0x969C, 0x969B, 0x96CC, 0x96D2, 0x9700, 0x977C, 0x9785, 0x97F6, 0x9817, 0x9818, 0x98AF, 0x98B1, 0x9903, 0x9905, 0x990C, 0x9909, 0x99C1, 0x9AAF, 0x9AB0, 0x9AE6, 0x9B41, 0x9B42, 0x9CF4, 0x9CF6, 0x9CF3, 0x9EBC, 0x9F3B, 0x9F4A, 0x5104, 0x5100, 0x50FB, 0x50F5, 0x50F9, 0x5102, 0x5108, 0x5109, 0x5105, 0x51DC, + 0x5287, 0x5288, 0x5289, 0x528D, 0x528A, 0x52F0, 0x53B2, 0x562E, 0x563B, 0x5639, 0x5632, 0x563F, 0x5634, 0x5629, 0x5653, 0x564E, 0x5657, 0x5674, 0x5636, 0x562F, 0x5630, 0x5880, 0x589F, 0x589E, 0x58B3, 0x589C, 0x58AE, 0x58A9, 0x58A6, 0x596D, 0x5B09, 0x5AFB, 0x5B0B, 0x5AF5, 0x5B0C, 0x5B08, 0x5BEE, 0x5BEC, 0x5BE9, 0x5BEB, 0x5C64, 0x5C65, 0x5D9D, 0x5D94, 0x5E62, 0x5E5F, 0x5E61, 0x5EE2, 0x5EDA, 0x5EDF, 0x5EDD, 0x5EE3, 0x5EE0, 0x5F48, 0x5F71, 0x5FB7, 0x5FB5, 0x6176, 0x6167, 0x616E, 0x615D, 0x6155, 0x6182, 0x617C, 0x6170, 0x616B, 0x617E, 0x61A7, 0x6190, 0x61AB, 0x618E, 0x61AC, 0x619A, 0x61A4, 0x6194, 0x61AE, 0x622E, 0x6469, 0x646F, 0x6479, 0x649E, 0x64B2, 0x6488, 0x6490, 0x64B0, 0x64A5, 0x6493, 0x6495, 0x64A9, 0x6492, 0x64AE, 0x64AD, 0x64AB, 0x649A, 0x64AC, 0x6499, 0x64A2, 0x64B3, 0x6575, 0x6577, 0x6578, 0x66AE, 0x66AB, 0x66B4, 0x66B1, 0x6A23, 0x6A1F, 0x69E8, 0x6A01, 0x6A1E, 0x6A19, 0x69FD, 0x6A21, 0x6A13, 0x6A0A, 0x69F3, 0x6A02, 0x6A05, 0x69ED, 0x6A11, 0x6B50, 0x6B4E, 0x6BA4, 0x6BC5, 0x6BC6, 0x6F3F, 0x6F7C, 0x6F84, 0x6F51, 0x6F66, 0x6F54, 0x6F86, 0x6F6D, 0x6F5B, 0x6F78, 0x6F6E, 0x6F8E, 0x6F7A, 0x6F70, 0x6F64, 0x6F97, 0x6F58, 0x6ED5, 0x6F6F, 0x6F60, 0x6F5F, 0x719F, 0x71AC, 0x71B1, 0x71A8, 0x7256, 0x729B, 0x734E, 0x7357, 0x7469, 0x748B, 0x7483, + 0x747E, 0x7480, 0x757F, 0x7620, 0x7629, 0x761F, 0x7624, 0x7626, 0x7621, 0x7622, 0x769A, 0x76BA, 0x76E4, 0x778E, 0x7787, 0x778C, 0x7791, 0x778B, 0x78CB, 0x78C5, 0x78BA, 0x78CA, 0x78BE, 0x78D5, 0x78BC, 0x78D0, 0x7A3F, 0x7A3C, 0x7A40, 0x7A3D, 0x7A37, 0x7A3B, 0x7AAF, 0x7AAE, 0x7BAD, 0x7BB1, 0x7BC4, 0x7BB4, 0x7BC6, 0x7BC7, 0x7BC1, 0x7BA0, 0x7BCC, 0x7CCA, 0x7DE0, 0x7DF4, 0x7DEF, 0x7DFB, 0x7DD8, 0x7DEC, 0x7DDD, 0x7DE8, 0x7DE3, 0x7DDA, 0x7DDE, 0x7DE9, 0x7D9E, 0x7DD9, 0x7DF2, 0x7DF9, 0x7F75, 0x7F77, 0x7FAF, 0x7FE9, 0x8026, 0x819B, 0x819C, 0x819D, 0x81A0, 0x819A, 0x8198, 0x8517, 0x853D, 0x851A, 0x84EE, 0x852C, 0x852D, 0x8513, 0x8511, 0x8523, 0x8521, 0x8514, 0x84EC, 0x8525, 0x84FF, 0x8506, 0x8782, 0x8774, 0x8776, 0x8760, 0x8766, 0x8778, 0x8768, 0x8759, 0x8757, 0x874C, 0x8753, 0x885B, 0x885D, 0x8910, 0x8907, 0x8912, 0x8913, 0x8915, 0x890A, 0x8ABC, 0x8AD2, 0x8AC7, 0x8AC4, 0x8A95, 0x8ACB, 0x8AF8, 0x8AB2, 0x8AC9, 0x8AC2, 0x8ABF, 0x8AB0, 0x8AD6, 0x8ACD, 0x8AB6, 0x8AB9, 0x8ADB, 0x8C4C, 0x8C4E, 0x8C6C, 0x8CE0, 0x8CDE, 0x8CE6, 0x8CE4, 0x8CEC, 0x8CED, 0x8CE2, 0x8CE3, 0x8CDC, 0x8CEA, 0x8CE1, 0x8D6D, 0x8D9F, 0x8DA3, 0x8E2B, 0x8E10, 0x8E1D, 0x8E22, 0x8E0F, 0x8E29, 0x8E1F, 0x8E21, 0x8E1E, 0x8EBA, 0x8F1D, 0x8F1B, 0x8F1F, 0x8F29, 0x8F26, 0x8F2A, 0x8F1C, 0x8F1E, + 0x8F25, 0x9069, 0x906E, 0x9068, 0x906D, 0x9077, 0x9130, 0x912D, 0x9127, 0x9131, 0x9187, 0x9189, 0x918B, 0x9183, 0x92C5, 0x92BB, 0x92B7, 0x92EA, 0x92AC, 0x92E4, 0x92C1, 0x92B3, 0x92BC, 0x92D2, 0x92C7, 0x92F0, 0x92B2, 0x95AD, 0x95B1, 0x9704, 0x9706, 0x9707, 0x9709, 0x9760, 0x978D, 0x978B, 0x978F, 0x9821, 0x982B, 0x981C, 0x98B3, 0x990A, 0x9913, 0x9912, 0x9918, 0x99DD, 0x99D0, 0x99DF, 0x99DB, 0x99D1, 0x99D5, 0x99D2, 0x99D9, 0x9AB7, 0x9AEE, 0x9AEF, 0x9B27, 0x9B45, 0x9B44, 0x9B77, 0x9B6F, 0x9D06, 0x9D09, 0x9D03, 0x9EA9, 0x9EBE, 0x9ECE, 0x58A8, 0x9F52, 0x5112, 0x5118, 0x5114, 0x5110, 0x5115, 0x5180, 0x51AA, 0x51DD, 0x5291, 0x5293, 0x52F3, 0x5659, 0x566B, 0x5679, 0x5669, 0x5664, 0x5678, 0x566A, 0x5668, 0x5665, 0x5671, 0x566F, 0x566C, 0x5662, 0x5676, 0x58C1, 0x58BE, 0x58C7, 0x58C5, 0x596E, 0x5B1D, 0x5B34, 0x5B78, 0x5BF0, 0x5C0E, 0x5F4A, 0x61B2, 0x6191, 0x61A9, 0x618A, 0x61CD, 0x61B6, 0x61BE, 0x61CA, 0x61C8, 0x6230, 0x64C5, 0x64C1, 0x64CB, 0x64BB, 0x64BC, 0x64DA, 0x64C4, 0x64C7, 0x64C2, 0x64CD, 0x64BF, 0x64D2, 0x64D4, 0x64BE, 0x6574, 0x66C6, 0x66C9, 0x66B9, 0x66C4, 0x66C7, 0x66B8, 0x6A3D, 0x6A38, 0x6A3A, 0x6A59, 0x6A6B, 0x6A58, 0x6A39, 0x6A44, 0x6A62, 0x6A61, 0x6A4B, 0x6A47, 0x6A35, 0x6A5F, 0x6A48, 0x6B59, 0x6B77, 0x6C05, 0x6FC2, 0x6FB1, 0x6FA1, + 0x6FC3, 0x6FA4, 0x6FC1, 0x6FA7, 0x6FB3, 0x6FC0, 0x6FB9, 0x6FB6, 0x6FA6, 0x6FA0, 0x6FB4, 0x71BE, 0x71C9, 0x71D0, 0x71D2, 0x71C8, 0x71D5, 0x71B9, 0x71CE, 0x71D9, 0x71DC, 0x71C3, 0x71C4, 0x7368, 0x749C, 0x74A3, 0x7498, 0x749F, 0x749E, 0x74E2, 0x750C, 0x750D, 0x7634, 0x7638, 0x763A, 0x76E7, 0x76E5, 0x77A0, 0x779E, 0x779F, 0x77A5, 0x78E8, 0x78DA, 0x78EC, 0x78E7, 0x79A6, 0x7A4D, 0x7A4E, 0x7A46, 0x7A4C, 0x7A4B, 0x7ABA, 0x7BD9, 0x7C11, 0x7BC9, 0x7BE4, 0x7BDB, 0x7BE1, 0x7BE9, 0x7BE6, 0x7CD5, 0x7CD6, 0x7E0A, 0x7E11, 0x7E08, 0x7E1B, 0x7E23, 0x7E1E, 0x7E1D, 0x7E09, 0x7E10, 0x7F79, 0x7FB2, 0x7FF0, 0x7FF1, 0x7FEE, 0x8028, 0x81B3, 0x81A9, 0x81A8, 0x81FB, 0x8208, 0x8258, 0x8259, 0x854A, 0x8559, 0x8548, 0x8568, 0x8569, 0x8543, 0x8549, 0x856D, 0x856A, 0x855E, 0x8783, 0x879F, 0x879E, 0x87A2, 0x878D, 0x8861, 0x892A, 0x8932, 0x8925, 0x892B, 0x8921, 0x89AA, 0x89A6, 0x8AE6, 0x8AFA, 0x8AEB, 0x8AF1, 0x8B00, 0x8ADC, 0x8AE7, 0x8AEE, 0x8AFE, 0x8B01, 0x8B02, 0x8AF7, 0x8AED, 0x8AF3, 0x8AF6, 0x8AFC, 0x8C6B, 0x8C6D, 0x8C93, 0x8CF4, 0x8E44, 0x8E31, 0x8E34, 0x8E42, 0x8E39, 0x8E35, 0x8F3B, 0x8F2F, 0x8F38, 0x8F33, 0x8FA8, 0x8FA6, 0x9075, 0x9074, 0x9078, 0x9072, 0x907C, 0x907A, 0x9134, 0x9192, 0x9320, 0x9336, 0x92F8, 0x9333, 0x932F, 0x9322, 0x92FC, 0x932B, 0x9304, 0x931A, + 0x9310, 0x9326, 0x9321, 0x9315, 0x932E, 0x9319, 0x95BB, 0x96A7, 0x96A8, 0x96AA, 0x96D5, 0x970E, 0x9711, 0x9716, 0x970D, 0x9713, 0x970F, 0x975B, 0x975C, 0x9766, 0x9798, 0x9830, 0x9838, 0x983B, 0x9837, 0x982D, 0x9839, 0x9824, 0x9910, 0x9928, 0x991E, 0x991B, 0x9921, 0x991A, 0x99ED, 0x99E2, 0x99F1, 0x9AB8, 0x9ABC, 0x9AFB, 0x9AED, 0x9B28, 0x9B91, 0x9D15, 0x9D23, 0x9D26, 0x9D28, 0x9D12, 0x9D1B, 0x9ED8, 0x9ED4, 0x9F8D, 0x9F9C, 0x512A, 0x511F, 0x5121, 0x5132, 0x52F5, 0x568E, 0x5680, 0x5690, 0x5685, 0x5687, 0x568F, 0x58D5, 0x58D3, 0x58D1, 0x58CE, 0x5B30, 0x5B2A, 0x5B24, 0x5B7A, 0x5C37, 0x5C68, 0x5DBC, 0x5DBA, 0x5DBD, 0x5DB8, 0x5E6B, 0x5F4C, 0x5FBD, 0x61C9, 0x61C2, 0x61C7, 0x61E6, 0x61CB, 0x6232, 0x6234, 0x64CE, 0x64CA, 0x64D8, 0x64E0, 0x64F0, 0x64E6, 0x64EC, 0x64F1, 0x64E2, 0x64ED, 0x6582, 0x6583, 0x66D9, 0x66D6, 0x6A80, 0x6A94, 0x6A84, 0x6AA2, 0x6A9C, 0x6ADB, 0x6AA3, 0x6A7E, 0x6A97, 0x6A90, 0x6AA0, 0x6B5C, 0x6BAE, 0x6BDA, 0x6C08, 0x6FD8, 0x6FF1, 0x6FDF, 0x6FE0, 0x6FDB, 0x6FE4, 0x6FEB, 0x6FEF, 0x6F80, 0x6FEC, 0x6FE1, 0x6FE9, 0x6FD5, 0x6FEE, 0x6FF0, 0x71E7, 0x71DF, 0x71EE, 0x71E6, 0x71E5, 0x71ED, 0x71EC, 0x71F4, 0x71E0, 0x7235, 0x7246, 0x7370, 0x7372, 0x74A9, 0x74B0, 0x74A6, 0x74A8, 0x7646, 0x7642, 0x764C, 0x76EA, 0x77B3, 0x77AA, 0x77B0, 0x77AC, + 0x77A7, 0x77AD, 0x77EF, 0x78F7, 0x78FA, 0x78F4, 0x78EF, 0x7901, 0x79A7, 0x79AA, 0x7A57, 0x7ABF, 0x7C07, 0x7C0D, 0x7BFE, 0x7BF7, 0x7C0C, 0x7BE0, 0x7CE0, 0x7CDC, 0x7CDE, 0x7CE2, 0x7CDF, 0x7CD9, 0x7CDD, 0x7E2E, 0x7E3E, 0x7E46, 0x7E37, 0x7E32, 0x7E43, 0x7E2B, 0x7E3D, 0x7E31, 0x7E45, 0x7E41, 0x7E34, 0x7E39, 0x7E48, 0x7E35, 0x7E3F, 0x7E2F, 0x7F44, 0x7FF3, 0x7FFC, 0x8071, 0x8072, 0x8070, 0x806F, 0x8073, 0x81C6, 0x81C3, 0x81BA, 0x81C2, 0x81C0, 0x81BF, 0x81BD, 0x81C9, 0x81BE, 0x81E8, 0x8209, 0x8271, 0x85AA, 0x8584, 0x857E, 0x859C, 0x8591, 0x8594, 0x85AF, 0x859B, 0x8587, 0x85A8, 0x858A, 0x8667, 0x87C0, 0x87D1, 0x87B3, 0x87D2, 0x87C6, 0x87AB, 0x87BB, 0x87BA, 0x87C8, 0x87CB, 0x893B, 0x8936, 0x8944, 0x8938, 0x893D, 0x89AC, 0x8B0E, 0x8B17, 0x8B19, 0x8B1B, 0x8B0A, 0x8B20, 0x8B1D, 0x8B04, 0x8B10, 0x8C41, 0x8C3F, 0x8C73, 0x8CFA, 0x8CFD, 0x8CFC, 0x8CF8, 0x8CFB, 0x8DA8, 0x8E49, 0x8E4B, 0x8E48, 0x8E4A, 0x8F44, 0x8F3E, 0x8F42, 0x8F45, 0x8F3F, 0x907F, 0x907D, 0x9084, 0x9081, 0x9082, 0x9080, 0x9139, 0x91A3, 0x919E, 0x919C, 0x934D, 0x9382, 0x9328, 0x9375, 0x934A, 0x9365, 0x934B, 0x9318, 0x937E, 0x936C, 0x935B, 0x9370, 0x935A, 0x9354, 0x95CA, 0x95CB, 0x95CC, 0x95C8, 0x95C6, 0x96B1, 0x96B8, 0x96D6, 0x971C, 0x971E, 0x97A0, 0x97D3, 0x9846, 0x98B6, 0x9935, 0x9A01, + 0x99FF, 0x9BAE, 0x9BAB, 0x9BAA, 0x9BAD, 0x9D3B, 0x9D3F, 0x9E8B, 0x9ECF, 0x9EDE, 0x9EDC, 0x9EDD, 0x9EDB, 0x9F3E, 0x9F4B, 0x53E2, 0x5695, 0x56AE, 0x58D9, 0x58D8, 0x5B38, 0x5F5D, 0x61E3, 0x6233, 0x64F4, 0x64F2, 0x64FE, 0x6506, 0x64FA, 0x64FB, 0x64F7, 0x65B7, 0x66DC, 0x6726, 0x6AB3, 0x6AAC, 0x6AC3, 0x6ABB, 0x6AB8, 0x6AC2, 0x6AAE, 0x6AAF, 0x6B5F, 0x6B78, 0x6BAF, 0x7009, 0x700B, 0x6FFE, 0x7006, 0x6FFA, 0x7011, 0x700F, 0x71FB, 0x71FC, 0x71FE, 0x71F8, 0x7377, 0x7375, 0x74A7, 0x74BF, 0x7515, 0x7656, 0x7658, 0x7652, 0x77BD, 0x77BF, 0x77BB, 0x77BC, 0x790E, 0x79AE, 0x7A61, 0x7A62, 0x7A60, 0x7AC4, 0x7AC5, 0x7C2B, 0x7C27, 0x7C2A, 0x7C1E, 0x7C23, 0x7C21, 0x7CE7, 0x7E54, 0x7E55, 0x7E5E, 0x7E5A, 0x7E61, 0x7E52, 0x7E59, 0x7F48, 0x7FF9, 0x7FFB, 0x8077, 0x8076, 0x81CD, 0x81CF, 0x820A, 0x85CF, 0x85A9, 0x85CD, 0x85D0, 0x85C9, 0x85B0, 0x85BA, 0x85B9, 0x85A6, 0x87EF, 0x87EC, 0x87F2, 0x87E0, 0x8986, 0x89B2, 0x89F4, 0x8B28, 0x8B39, 0x8B2C, 0x8B2B, 0x8C50, 0x8D05, 0x8E59, 0x8E63, 0x8E66, 0x8E64, 0x8E5F, 0x8E55, 0x8EC0, 0x8F49, 0x8F4D, 0x9087, 0x9083, 0x9088, 0x91AB, 0x91AC, 0x91D0, 0x9394, 0x938A, 0x9396, 0x93A2, 0x93B3, 0x93AE, 0x93AC, 0x93B0, 0x9398, 0x939A, 0x9397, 0x95D4, 0x95D6, 0x95D0, 0x95D5, 0x96E2, 0x96DC, 0x96D9, 0x96DB, 0x96DE, 0x9724, 0x97A3, 0x97A6, + 0x97AD, 0x97F9, 0x984D, 0x984F, 0x984C, 0x984E, 0x9853, 0x98BA, 0x993E, 0x993F, 0x993D, 0x992E, 0x99A5, 0x9A0E, 0x9AC1, 0x9B03, 0x9B06, 0x9B4F, 0x9B4E, 0x9B4D, 0x9BCA, 0x9BC9, 0x9BFD, 0x9BC8, 0x9BC0, 0x9D51, 0x9D5D, 0x9D60, 0x9EE0, 0x9F15, 0x9F2C, 0x5133, 0x56A5, 0x58DE, 0x58DF, 0x58E2, 0x5BF5, 0x9F90, 0x5EEC, 0x61F2, 0x61F7, 0x61F6, 0x61F5, 0x6500, 0x650F, 0x66E0, 0x66DD, 0x6AE5, 0x6ADD, 0x6ADA, 0x6AD3, 0x701B, 0x701F, 0x7028, 0x701A, 0x701D, 0x7015, 0x7018, 0x7206, 0x720D, 0x7258, 0x72A2, 0x7378, 0x737A, 0x74BD, 0x74CA, 0x74E3, 0x7587, 0x7586, 0x765F, 0x7661, 0x77C7, 0x7919, 0x79B1, 0x7A6B, 0x7A69, 0x7C3E, 0x7C3F, 0x7C38, 0x7C3D, 0x7C37, 0x7C40, 0x7E6B, 0x7E6D, 0x7E79, 0x7E69, 0x7E6A, 0x7F85, 0x7E73, 0x7FB6, 0x7FB9, 0x7FB8, 0x81D8, 0x85E9, 0x85DD, 0x85EA, 0x85D5, 0x85E4, 0x85E5, 0x85F7, 0x87FB, 0x8805, 0x880D, 0x87F9, 0x87FE, 0x8960, 0x895F, 0x8956, 0x895E, 0x8B41, 0x8B5C, 0x8B58, 0x8B49, 0x8B5A, 0x8B4E, 0x8B4F, 0x8B46, 0x8B59, 0x8D08, 0x8D0A, 0x8E7C, 0x8E72, 0x8E87, 0x8E76, 0x8E6C, 0x8E7A, 0x8E74, 0x8F54, 0x8F4E, 0x8FAD, 0x908A, 0x908B, 0x91B1, 0x91AE, 0x93E1, 0x93D1, 0x93DF, 0x93C3, 0x93C8, 0x93DC, 0x93DD, 0x93D6, 0x93E2, 0x93CD, 0x93D8, 0x93E4, 0x93D7, 0x93E8, 0x95DC, 0x96B4, 0x96E3, 0x972A, 0x9727, 0x9761, 0x97DC, 0x97FB, 0x985E, + 0x9858, 0x985B, 0x98BC, 0x9945, 0x9949, 0x9A16, 0x9A19, 0x9B0D, 0x9BE8, 0x9BE7, 0x9BD6, 0x9BDB, 0x9D89, 0x9D61, 0x9D72, 0x9D6A, 0x9D6C, 0x9E92, 0x9E97, 0x9E93, 0x9EB4, 0x52F8, 0x56A8, 0x56B7, 0x56B6, 0x56B4, 0x56BC, 0x58E4, 0x5B40, 0x5B43, 0x5B7D, 0x5BF6, 0x5DC9, 0x61F8, 0x61FA, 0x6518, 0x6514, 0x6519, 0x66E6, 0x6727, 0x6AEC, 0x703E, 0x7030, 0x7032, 0x7210, 0x737B, 0x74CF, 0x7662, 0x7665, 0x7926, 0x792A, 0x792C, 0x792B, 0x7AC7, 0x7AF6, 0x7C4C, 0x7C43, 0x7C4D, 0x7CEF, 0x7CF0, 0x8FAE, 0x7E7D, 0x7E7C, 0x7E82, 0x7F4C, 0x8000, 0x81DA, 0x8266, 0x85FB, 0x85F9, 0x8611, 0x85FA, 0x8606, 0x860B, 0x8607, 0x860A, 0x8814, 0x8815, 0x8964, 0x89BA, 0x89F8, 0x8B70, 0x8B6C, 0x8B66, 0x8B6F, 0x8B5F, 0x8B6B, 0x8D0F, 0x8D0D, 0x8E89, 0x8E81, 0x8E85, 0x8E82, 0x91B4, 0x91CB, 0x9418, 0x9403, 0x93FD, 0x95E1, 0x9730, 0x98C4, 0x9952, 0x9951, 0x99A8, 0x9A2B, 0x9A30, 0x9A37, 0x9A35, 0x9C13, 0x9C0D, 0x9E79, 0x9EB5, 0x9EE8, 0x9F2F, 0x9F5F, 0x9F63, 0x9F61, 0x5137, 0x5138, 0x56C1, 0x56C0, 0x56C2, 0x5914, 0x5C6C, 0x5DCD, 0x61FC, 0x61FE, 0x651D, 0x651C, 0x6595, 0x66E9, 0x6AFB, 0x6B04, 0x6AFA, 0x6BB2, 0x704C, 0x721B, 0x72A7, 0x74D6, 0x74D4, 0x7669, 0x77D3, 0x7C50, 0x7E8F, 0x7E8C, 0x7FBC, 0x8617, 0x862D, 0x861A, 0x8823, 0x8822, 0x8821, 0x881F, 0x896A, 0x896C, 0x89BD, 0x8B74, + 0x8B77, 0x8B7D, 0x8D13, 0x8E8A, 0x8E8D, 0x8E8B, 0x8F5F, 0x8FAF, 0x91BA, 0x942E, 0x9433, 0x9435, 0x943A, 0x9438, 0x9432, 0x942B, 0x95E2, 0x9738, 0x9739, 0x9732, 0x97FF, 0x9867, 0x9865, 0x9957, 0x9A45, 0x9A43, 0x9A40, 0x9A3E, 0x9ACF, 0x9B54, 0x9B51, 0x9C2D, 0x9C25, 0x9DAF, 0x9DB4, 0x9DC2, 0x9DB8, 0x9E9D, 0x9EEF, 0x9F19, 0x9F5C, 0x9F66, 0x9F67, 0x513C, 0x513B, 0x56C8, 0x56CA, 0x56C9, 0x5B7F, 0x5DD4, 0x5DD2, 0x5F4E, 0x61FF, 0x6524, 0x6B0A, 0x6B61, 0x7051, 0x7058, 0x7380, 0x74E4, 0x758A, 0x766E, 0x766C, 0x79B3, 0x7C60, 0x7C5F, 0x807E, 0x807D, 0x81DF, 0x8972, 0x896F, 0x89FC, 0x8B80, 0x8D16, 0x8D17, 0x8E91, 0x8E93, 0x8F61, 0x9148, 0x9444, 0x9451, 0x9452, 0x973D, 0x973E, 0x97C3, 0x97C1, 0x986B, 0x9955, 0x9A55, 0x9A4D, 0x9AD2, 0x9B1A, 0x9C49, 0x9C31, 0x9C3E, 0x9C3B, 0x9DD3, 0x9DD7, 0x9F34, 0x9F6C, 0x9F6A, 0x9F94, 0x56CC, 0x5DD6, 0x6200, 0x6523, 0x652B, 0x652A, 0x66EC, 0x6B10, 0x74DA, 0x7ACA, 0x7C64, 0x7C63, 0x7C65, 0x7E93, 0x7E96, 0x7E94, 0x81E2, 0x8638, 0x863F, 0x8831, 0x8B8A, 0x9090, 0x908F, 0x9463, 0x9460, 0x9464, 0x9768, 0x986F, 0x995C, 0x9A5A, 0x9A5B, 0x9A57, 0x9AD3, 0x9AD4, 0x9AD1, 0x9C54, 0x9C57, 0x9C56, 0x9DE5, 0x9E9F, 0x9EF4, 0x56D1, 0x58E9, 0x652C, 0x705E, 0x7671, 0x7672, 0x77D7, 0x7F50, 0x7F88, 0x8836, 0x8839, 0x8862, 0x8B93, 0x8B92, + 0x8B96, 0x8277, 0x8D1B, 0x91C0, 0x946A, 0x9742, 0x9748, 0x9744, 0x97C6, 0x9870, 0x9A5F, 0x9B22, 0x9B58, 0x9C5F, 0x9DF9, 0x9DFA, 0x9E7C, 0x9E7D, 0x9F07, 0x9F77, 0x9F72, 0x5EF3, 0x6B16, 0x7063, 0x7C6C, 0x7C6E, 0x883B, 0x89C0, 0x8EA1, 0x91C1, 0x9472, 0x9470, 0x9871, 0x995E, 0x9AD6, 0x9B23, 0x9ECC, 0x7064, 0x77DA, 0x8B9A, 0x9477, 0x97C9, 0x9A62, 0x9A65, 0x7E9C, 0x8B9C, 0x8EAA, 0x91C5, 0x947D, 0x947E, 0x947C, 0x9C77, 0x9C78, 0x9EF7, 0x8C54, 0x947F, 0x9E1A, 0x7228, 0x9A6A, 0x9B31, 0x9E1B, 0x9E1E, 0x7C72, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x4E42, 0x4E5C, 0x51F5, 0x531A, 0x5382, 0x4E07, 0x4E0C, 0x4E47, 0x4E8D, 0x56D7, 0xFA0C, 0x5C6E, 0x5F73, 0x4E0F, 0x5187, 0x4E0E, 0x4E2E, 0x4E93, 0x4EC2, 0x4EC9, 0x4EC8, 0x5198, 0x52FC, 0x536C, 0x53B9, 0x5720, 0x5903, 0x592C, 0x5C10, 0x5DFF, 0x65E1, 0x6BB3, 0x6BCC, 0x6C14, 0x723F, 0x4E31, 0x4E3C, 0x4EE8, 0x4EDC, 0x4EE9, 0x4EE1, 0x4EDD, 0x4EDA, 0x520C, 0x531C, 0x534C, 0x5722, 0x5723, 0x5917, 0x592F, 0x5B81, 0x5B84, 0x5C12, 0x5C3B, 0x5C74, 0x5C73, 0x5E04, 0x5E80, 0x5E82, 0x5FC9, 0x6209, 0x6250, 0x6C15, 0x6C36, 0x6C43, 0x6C3F, 0x6C3B, 0x72AE, 0x72B0, 0x738A, 0x79B8, 0x808A, 0x961E, 0x4F0E, 0x4F18, 0x4F2C, 0x4EF5, 0x4F14, 0x4EF1, 0x4F00, 0x4EF7, 0x4F08, 0x4F1D, 0x4F02, 0x4F05, 0x4F22, 0x4F13, 0x4F04, 0x4EF4, 0x4F12, 0x51B1, 0x5213, 0x5209, 0x5210, 0x52A6, 0x5322, 0x531F, 0x534D, 0x538A, 0x5407, 0x56E1, 0x56DF, 0x572E, 0x572A, 0x5734, 0x593C, 0x5980, 0x597C, 0x5985, 0x597B, 0x597E, 0x5977, 0x597F, 0x5B56, 0x5C15, 0x5C25, 0x5C7C, 0x5C7A, 0x5C7B, 0x5C7E, 0x5DDF, 0x5E75, 0x5E84, 0x5F02, 0x5F1A, 0x5F74, 0x5FD5, 0x5FD4, 0x5FCF, 0x625C, 0x625E, 0x6264, 0x6261, 0x6266, 0x6262, 0x6259, 0x6260, 0x625A, 0x6265, 0x65EF, 0x65EE, 0x673E, 0x6739, 0x6738, 0x673B, 0x673A, 0x673F, 0x673C, 0x6733, 0x6C18, 0x6C46, 0x6C52, 0x6C5C, 0x6C4F, 0x6C4A, 0x6C54, 0x6C4B, + 0x6C4C, 0x7071, 0x725E, 0x72B4, 0x72B5, 0x738E, 0x752A, 0x767F, 0x7A75, 0x7F51, 0x8278, 0x827C, 0x8280, 0x827D, 0x827F, 0x864D, 0x897E, 0x9099, 0x9097, 0x9098, 0x909B, 0x9094, 0x9622, 0x9624, 0x9620, 0x9623, 0x4F56, 0x4F3B, 0x4F62, 0x4F49, 0x4F53, 0x4F64, 0x4F3E, 0x4F67, 0x4F52, 0x4F5F, 0x4F41, 0x4F58, 0x4F2D, 0x4F33, 0x4F3F, 0x4F61, 0x518F, 0x51B9, 0x521C, 0x521E, 0x5221, 0x52AD, 0x52AE, 0x5309, 0x5363, 0x5372, 0x538E, 0x538F, 0x5430, 0x5437, 0x542A, 0x5454, 0x5445, 0x5419, 0x541C, 0x5425, 0x5418, 0x543D, 0x544F, 0x5441, 0x5428, 0x5424, 0x5447, 0x56EE, 0x56E7, 0x56E5, 0x5741, 0x5745, 0x574C, 0x5749, 0x574B, 0x5752, 0x5906, 0x5940, 0x59A6, 0x5998, 0x59A0, 0x5997, 0x598E, 0x59A2, 0x5990, 0x598F, 0x59A7, 0x59A1, 0x5B8E, 0x5B92, 0x5C28, 0x5C2A, 0x5C8D, 0x5C8F, 0x5C88, 0x5C8B, 0x5C89, 0x5C92, 0x5C8A, 0x5C86, 0x5C93, 0x5C95, 0x5DE0, 0x5E0A, 0x5E0E, 0x5E8B, 0x5E89, 0x5E8C, 0x5E88, 0x5E8D, 0x5F05, 0x5F1D, 0x5F78, 0x5F76, 0x5FD2, 0x5FD1, 0x5FD0, 0x5FED, 0x5FE8, 0x5FEE, 0x5FF3, 0x5FE1, 0x5FE4, 0x5FE3, 0x5FFA, 0x5FEF, 0x5FF7, 0x5FFB, 0x6000, 0x5FF4, 0x623A, 0x6283, 0x628C, 0x628E, 0x628F, 0x6294, 0x6287, 0x6271, 0x627B, 0x627A, 0x6270, 0x6281, 0x6288, 0x6277, 0x627D, 0x6272, 0x6274, 0x6537, 0x65F0, 0x65F4, 0x65F3, 0x65F2, 0x65F5, 0x6745, 0x6747, + 0x6759, 0x6755, 0x674C, 0x6748, 0x675D, 0x674D, 0x675A, 0x674B, 0x6BD0, 0x6C19, 0x6C1A, 0x6C78, 0x6C67, 0x6C6B, 0x6C84, 0x6C8B, 0x6C8F, 0x6C71, 0x6C6F, 0x6C69, 0x6C9A, 0x6C6D, 0x6C87, 0x6C95, 0x6C9C, 0x6C66, 0x6C73, 0x6C65, 0x6C7B, 0x6C8E, 0x7074, 0x707A, 0x7263, 0x72BF, 0x72BD, 0x72C3, 0x72C6, 0x72C1, 0x72BA, 0x72C5, 0x7395, 0x7397, 0x7393, 0x7394, 0x7392, 0x753A, 0x7539, 0x7594, 0x7595, 0x7681, 0x793D, 0x8034, 0x8095, 0x8099, 0x8090, 0x8092, 0x809C, 0x8290, 0x828F, 0x8285, 0x828E, 0x8291, 0x8293, 0x828A, 0x8283, 0x8284, 0x8C78, 0x8FC9, 0x8FBF, 0x909F, 0x90A1, 0x90A5, 0x909E, 0x90A7, 0x90A0, 0x9630, 0x9628, 0x962F, 0x962D, 0x4E33, 0x4F98, 0x4F7C, 0x4F85, 0x4F7D, 0x4F80, 0x4F87, 0x4F76, 0x4F74, 0x4F89, 0x4F84, 0x4F77, 0x4F4C, 0x4F97, 0x4F6A, 0x4F9A, 0x4F79, 0x4F81, 0x4F78, 0x4F90, 0x4F9C, 0x4F94, 0x4F9E, 0x4F92, 0x4F82, 0x4F95, 0x4F6B, 0x4F6E, 0x519E, 0x51BC, 0x51BE, 0x5235, 0x5232, 0x5233, 0x5246, 0x5231, 0x52BC, 0x530A, 0x530B, 0x533C, 0x5392, 0x5394, 0x5487, 0x547F, 0x5481, 0x5491, 0x5482, 0x5488, 0x546B, 0x547A, 0x547E, 0x5465, 0x546C, 0x5474, 0x5466, 0x548D, 0x546F, 0x5461, 0x5460, 0x5498, 0x5463, 0x5467, 0x5464, 0x56F7, 0x56F9, 0x576F, 0x5772, 0x576D, 0x576B, 0x5771, 0x5770, 0x5776, 0x5780, 0x5775, 0x577B, 0x5773, 0x5774, 0x5762, + 0x5768, 0x577D, 0x590C, 0x5945, 0x59B5, 0x59BA, 0x59CF, 0x59CE, 0x59B2, 0x59CC, 0x59C1, 0x59B6, 0x59BC, 0x59C3, 0x59D6, 0x59B1, 0x59BD, 0x59C0, 0x59C8, 0x59B4, 0x59C7, 0x5B62, 0x5B65, 0x5B93, 0x5B95, 0x5C44, 0x5C47, 0x5CAE, 0x5CA4, 0x5CA0, 0x5CB5, 0x5CAF, 0x5CA8, 0x5CAC, 0x5C9F, 0x5CA3, 0x5CAD, 0x5CA2, 0x5CAA, 0x5CA7, 0x5C9D, 0x5CA5, 0x5CB6, 0x5CB0, 0x5CA6, 0x5E17, 0x5E14, 0x5E19, 0x5F28, 0x5F22, 0x5F23, 0x5F24, 0x5F54, 0x5F82, 0x5F7E, 0x5F7D, 0x5FDE, 0x5FE5, 0x602D, 0x6026, 0x6019, 0x6032, 0x600B, 0x6034, 0x600A, 0x6017, 0x6033, 0x601A, 0x601E, 0x602C, 0x6022, 0x600D, 0x6010, 0x602E, 0x6013, 0x6011, 0x600C, 0x6009, 0x601C, 0x6214, 0x623D, 0x62AD, 0x62B4, 0x62D1, 0x62BE, 0x62AA, 0x62B6, 0x62CA, 0x62AE, 0x62B3, 0x62AF, 0x62BB, 0x62A9, 0x62B0, 0x62B8, 0x653D, 0x65A8, 0x65BB, 0x6609, 0x65FC, 0x6604, 0x6612, 0x6608, 0x65FB, 0x6603, 0x660B, 0x660D, 0x6605, 0x65FD, 0x6611, 0x6610, 0x66F6, 0x670A, 0x6785, 0x676C, 0x678E, 0x6792, 0x6776, 0x677B, 0x6798, 0x6786, 0x6784, 0x6774, 0x678D, 0x678C, 0x677A, 0x679F, 0x6791, 0x6799, 0x6783, 0x677D, 0x6781, 0x6778, 0x6779, 0x6794, 0x6B25, 0x6B80, 0x6B7E, 0x6BDE, 0x6C1D, 0x6C93, 0x6CEC, 0x6CEB, 0x6CEE, 0x6CD9, 0x6CB6, 0x6CD4, 0x6CAD, 0x6CE7, 0x6CB7, 0x6CD0, 0x6CC2, 0x6CBA, 0x6CC3, 0x6CC6, 0x6CED, 0x6CF2, + 0x6CD2, 0x6CDD, 0x6CB4, 0x6C8A, 0x6C9D, 0x6C80, 0x6CDE, 0x6CC0, 0x6D30, 0x6CCD, 0x6CC7, 0x6CB0, 0x6CF9, 0x6CCF, 0x6CE9, 0x6CD1, 0x7094, 0x7098, 0x7085, 0x7093, 0x7086, 0x7084, 0x7091, 0x7096, 0x7082, 0x709A, 0x7083, 0x726A, 0x72D6, 0x72CB, 0x72D8, 0x72C9, 0x72DC, 0x72D2, 0x72D4, 0x72DA, 0x72CC, 0x72D1, 0x73A4, 0x73A1, 0x73AD, 0x73A6, 0x73A2, 0x73A0, 0x73AC, 0x739D, 0x74DD, 0x74E8, 0x753F, 0x7540, 0x753E, 0x758C, 0x7598, 0x76AF, 0x76F3, 0x76F1, 0x76F0, 0x76F5, 0x77F8, 0x77FC, 0x77F9, 0x77FB, 0x77FA, 0x77F7, 0x7942, 0x793F, 0x79C5, 0x7A78, 0x7A7B, 0x7AFB, 0x7C75, 0x7CFD, 0x8035, 0x808F, 0x80AE, 0x80A3, 0x80B8, 0x80B5, 0x80AD, 0x8220, 0x82A0, 0x82C0, 0x82AB, 0x829A, 0x8298, 0x829B, 0x82B5, 0x82A7, 0x82AE, 0x82BC, 0x829E, 0x82BA, 0x82B4, 0x82A8, 0x82A1, 0x82A9, 0x82C2, 0x82A4, 0x82C3, 0x82B6, 0x82A2, 0x8670, 0x866F, 0x866D, 0x866E, 0x8C56, 0x8FD2, 0x8FCB, 0x8FD3, 0x8FCD, 0x8FD6, 0x8FD5, 0x8FD7, 0x90B2, 0x90B4, 0x90AF, 0x90B3, 0x90B0, 0x9639, 0x963D, 0x963C, 0x963A, 0x9643, 0x4FCD, 0x4FC5, 0x4FD3, 0x4FB2, 0x4FC9, 0x4FCB, 0x4FC1, 0x4FD4, 0x4FDC, 0x4FD9, 0x4FBB, 0x4FB3, 0x4FDB, 0x4FC7, 0x4FD6, 0x4FBA, 0x4FC0, 0x4FB9, 0x4FEC, 0x5244, 0x5249, 0x52C0, 0x52C2, 0x533D, 0x537C, 0x5397, 0x5396, 0x5399, 0x5398, 0x54BA, 0x54A1, 0x54AD, 0x54A5, 0x54CF, + 0x54C3, 0x830D, 0x54B7, 0x54AE, 0x54D6, 0x54B6, 0x54C5, 0x54C6, 0x54A0, 0x5470, 0x54BC, 0x54A2, 0x54BE, 0x5472, 0x54DE, 0x54B0, 0x57B5, 0x579E, 0x579F, 0x57A4, 0x578C, 0x5797, 0x579D, 0x579B, 0x5794, 0x5798, 0x578F, 0x5799, 0x57A5, 0x579A, 0x5795, 0x58F4, 0x590D, 0x5953, 0x59E1, 0x59DE, 0x59EE, 0x5A00, 0x59F1, 0x59DD, 0x59FA, 0x59FD, 0x59FC, 0x59F6, 0x59E4, 0x59F2, 0x59F7, 0x59DB, 0x59E9, 0x59F3, 0x59F5, 0x59E0, 0x59FE, 0x59F4, 0x59ED, 0x5BA8, 0x5C4C, 0x5CD0, 0x5CD8, 0x5CCC, 0x5CD7, 0x5CCB, 0x5CDB, 0x5CDE, 0x5CDA, 0x5CC9, 0x5CC7, 0x5CCA, 0x5CD6, 0x5CD3, 0x5CD4, 0x5CCF, 0x5CC8, 0x5CC6, 0x5CCE, 0x5CDF, 0x5CF8, 0x5DF9, 0x5E21, 0x5E22, 0x5E23, 0x5E20, 0x5E24, 0x5EB0, 0x5EA4, 0x5EA2, 0x5E9B, 0x5EA3, 0x5EA5, 0x5F07, 0x5F2E, 0x5F56, 0x5F86, 0x6037, 0x6039, 0x6054, 0x6072, 0x605E, 0x6045, 0x6053, 0x6047, 0x6049, 0x605B, 0x604C, 0x6040, 0x6042, 0x605F, 0x6024, 0x6044, 0x6058, 0x6066, 0x606E, 0x6242, 0x6243, 0x62CF, 0x630D, 0x630B, 0x62F5, 0x630E, 0x6303, 0x62EB, 0x62F9, 0x630F, 0x630C, 0x62F8, 0x62F6, 0x6300, 0x6313, 0x6314, 0x62FA, 0x6315, 0x62FB, 0x62F0, 0x6541, 0x6543, 0x65AA, 0x65BF, 0x6636, 0x6621, 0x6632, 0x6635, 0x661C, 0x6626, 0x6622, 0x6633, 0x662B, 0x663A, 0x661D, 0x6634, 0x6639, 0x662E, 0x670F, 0x6710, 0x67C1, 0x67F2, 0x67C8, 0x67BA, + 0x67DC, 0x67BB, 0x67F8, 0x67D8, 0x67C0, 0x67B7, 0x67C5, 0x67EB, 0x67E4, 0x67DF, 0x67B5, 0x67CD, 0x67B3, 0x67F7, 0x67F6, 0x67EE, 0x67E3, 0x67C2, 0x67B9, 0x67CE, 0x67E7, 0x67F0, 0x67B2, 0x67FC, 0x67C6, 0x67ED, 0x67CC, 0x67AE, 0x67E6, 0x67DB, 0x67FA, 0x67C9, 0x67CA, 0x67C3, 0x67EA, 0x67CB, 0x6B28, 0x6B82, 0x6B84, 0x6BB6, 0x6BD6, 0x6BD8, 0x6BE0, 0x6C20, 0x6C21, 0x6D28, 0x6D34, 0x6D2D, 0x6D1F, 0x6D3C, 0x6D3F, 0x6D12, 0x6D0A, 0x6CDA, 0x6D33, 0x6D04, 0x6D19, 0x6D3A, 0x6D1A, 0x6D11, 0x6D00, 0x6D1D, 0x6D42, 0x6D01, 0x6D18, 0x6D37, 0x6D03, 0x6D0F, 0x6D40, 0x6D07, 0x6D20, 0x6D2C, 0x6D08, 0x6D22, 0x6D09, 0x6D10, 0x70B7, 0x709F, 0x70BE, 0x70B1, 0x70B0, 0x70A1, 0x70B4, 0x70B5, 0x70A9, 0x7241, 0x7249, 0x724A, 0x726C, 0x7270, 0x7273, 0x726E, 0x72CA, 0x72E4, 0x72E8, 0x72EB, 0x72DF, 0x72EA, 0x72E6, 0x72E3, 0x7385, 0x73CC, 0x73C2, 0x73C8, 0x73C5, 0x73B9, 0x73B6, 0x73B5, 0x73B4, 0x73EB, 0x73BF, 0x73C7, 0x73BE, 0x73C3, 0x73C6, 0x73B8, 0x73CB, 0x74EC, 0x74EE, 0x752E, 0x7547, 0x7548, 0x75A7, 0x75AA, 0x7679, 0x76C4, 0x7708, 0x7703, 0x7704, 0x7705, 0x770A, 0x76F7, 0x76FB, 0x76FA, 0x77E7, 0x77E8, 0x7806, 0x7811, 0x7812, 0x7805, 0x7810, 0x780F, 0x780E, 0x7809, 0x7803, 0x7813, 0x794A, 0x794C, 0x794B, 0x7945, 0x7944, 0x79D5, 0x79CD, 0x79CF, 0x79D6, 0x79CE, 0x7A80, + 0x7A7E, 0x7AD1, 0x7B00, 0x7B01, 0x7C7A, 0x7C78, 0x7C79, 0x7C7F, 0x7C80, 0x7C81, 0x7D03, 0x7D08, 0x7D01, 0x7F58, 0x7F91, 0x7F8D, 0x7FBE, 0x8007, 0x800E, 0x800F, 0x8014, 0x8037, 0x80D8, 0x80C7, 0x80E0, 0x80D1, 0x80C8, 0x80C2, 0x80D0, 0x80C5, 0x80E3, 0x80D9, 0x80DC, 0x80CA, 0x80D5, 0x80C9, 0x80CF, 0x80D7, 0x80E6, 0x80CD, 0x81FF, 0x8221, 0x8294, 0x82D9, 0x82FE, 0x82F9, 0x8307, 0x82E8, 0x8300, 0x82D5, 0x833A, 0x82EB, 0x82D6, 0x82F4, 0x82EC, 0x82E1, 0x82F2, 0x82F5, 0x830C, 0x82FB, 0x82F6, 0x82F0, 0x82EA, 0x82E4, 0x82E0, 0x82FA, 0x82F3, 0x82ED, 0x8677, 0x8674, 0x867C, 0x8673, 0x8841, 0x884E, 0x8867, 0x886A, 0x8869, 0x89D3, 0x8A04, 0x8A07, 0x8D72, 0x8FE3, 0x8FE1, 0x8FEE, 0x8FE0, 0x90F1, 0x90BD, 0x90BF, 0x90D5, 0x90C5, 0x90BE, 0x90C7, 0x90CB, 0x90C8, 0x91D4, 0x91D3, 0x9654, 0x964F, 0x9651, 0x9653, 0x964A, 0x964E, 0x501E, 0x5005, 0x5007, 0x5013, 0x5022, 0x5030, 0x501B, 0x4FF5, 0x4FF4, 0x5033, 0x5037, 0x502C, 0x4FF6, 0x4FF7, 0x5017, 0x501C, 0x5020, 0x5027, 0x5035, 0x502F, 0x5031, 0x500E, 0x515A, 0x5194, 0x5193, 0x51CA, 0x51C4, 0x51C5, 0x51C8, 0x51CE, 0x5261, 0x525A, 0x5252, 0x525E, 0x525F, 0x5255, 0x5262, 0x52CD, 0x530E, 0x539E, 0x5526, 0x54E2, 0x5517, 0x5512, 0x54E7, 0x54F3, 0x54E4, 0x551A, 0x54FF, 0x5504, 0x5508, 0x54EB, 0x5511, 0x5505, 0x54F1, + 0x550A, 0x54FB, 0x54F7, 0x54F8, 0x54E0, 0x550E, 0x5503, 0x550B, 0x5701, 0x5702, 0x57CC, 0x5832, 0x57D5, 0x57D2, 0x57BA, 0x57C6, 0x57BD, 0x57BC, 0x57B8, 0x57B6, 0x57BF, 0x57C7, 0x57D0, 0x57B9, 0x57C1, 0x590E, 0x594A, 0x5A19, 0x5A16, 0x5A2D, 0x5A2E, 0x5A15, 0x5A0F, 0x5A17, 0x5A0A, 0x5A1E, 0x5A33, 0x5B6C, 0x5BA7, 0x5BAD, 0x5BAC, 0x5C03, 0x5C56, 0x5C54, 0x5CEC, 0x5CFF, 0x5CEE, 0x5CF1, 0x5CF7, 0x5D00, 0x5CF9, 0x5E29, 0x5E28, 0x5EA8, 0x5EAE, 0x5EAA, 0x5EAC, 0x5F33, 0x5F30, 0x5F67, 0x605D, 0x605A, 0x6067, 0x6041, 0x60A2, 0x6088, 0x6080, 0x6092, 0x6081, 0x609D, 0x6083, 0x6095, 0x609B, 0x6097, 0x6087, 0x609C, 0x608E, 0x6219, 0x6246, 0x62F2, 0x6310, 0x6356, 0x632C, 0x6344, 0x6345, 0x6336, 0x6343, 0x63E4, 0x6339, 0x634B, 0x634A, 0x633C, 0x6329, 0x6341, 0x6334, 0x6358, 0x6354, 0x6359, 0x632D, 0x6347, 0x6333, 0x635A, 0x6351, 0x6338, 0x6357, 0x6340, 0x6348, 0x654A, 0x6546, 0x65C6, 0x65C3, 0x65C4, 0x65C2, 0x664A, 0x665F, 0x6647, 0x6651, 0x6712, 0x6713, 0x681F, 0x681A, 0x6849, 0x6832, 0x6833, 0x683B, 0x684B, 0x684F, 0x6816, 0x6831, 0x681C, 0x6835, 0x682B, 0x682D, 0x682F, 0x684E, 0x6844, 0x6834, 0x681D, 0x6812, 0x6814, 0x6826, 0x6828, 0x682E, 0x684D, 0x683A, 0x6825, 0x6820, 0x6B2C, 0x6B2F, 0x6B2D, 0x6B31, 0x6B34, 0x6B6D, 0x8082, 0x6B88, 0x6BE6, 0x6BE4, + 0x6BE8, 0x6BE3, 0x6BE2, 0x6BE7, 0x6C25, 0x6D7A, 0x6D63, 0x6D64, 0x6D76, 0x6D0D, 0x6D61, 0x6D92, 0x6D58, 0x6D62, 0x6D6D, 0x6D6F, 0x6D91, 0x6D8D, 0x6DEF, 0x6D7F, 0x6D86, 0x6D5E, 0x6D67, 0x6D60, 0x6D97, 0x6D70, 0x6D7C, 0x6D5F, 0x6D82, 0x6D98, 0x6D2F, 0x6D68, 0x6D8B, 0x6D7E, 0x6D80, 0x6D84, 0x6D16, 0x6D83, 0x6D7B, 0x6D7D, 0x6D75, 0x6D90, 0x70DC, 0x70D3, 0x70D1, 0x70DD, 0x70CB, 0x7F39, 0x70E2, 0x70D7, 0x70D2, 0x70DE, 0x70E0, 0x70D4, 0x70CD, 0x70C5, 0x70C6, 0x70C7, 0x70DA, 0x70CE, 0x70E1, 0x7242, 0x7278, 0x7277, 0x7276, 0x7300, 0x72FA, 0x72F4, 0x72FE, 0x72F6, 0x72F3, 0x72FB, 0x7301, 0x73D3, 0x73D9, 0x73E5, 0x73D6, 0x73BC, 0x73E7, 0x73E3, 0x73E9, 0x73DC, 0x73D2, 0x73DB, 0x73D4, 0x73DD, 0x73DA, 0x73D7, 0x73D8, 0x73E8, 0x74DE, 0x74DF, 0x74F4, 0x74F5, 0x7521, 0x755B, 0x755F, 0x75B0, 0x75C1, 0x75BB, 0x75C4, 0x75C0, 0x75BF, 0x75B6, 0x75BA, 0x768A, 0x76C9, 0x771D, 0x771B, 0x7710, 0x7713, 0x7712, 0x7723, 0x7711, 0x7715, 0x7719, 0x771A, 0x7722, 0x7727, 0x7823, 0x782C, 0x7822, 0x7835, 0x782F, 0x7828, 0x782E, 0x782B, 0x7821, 0x7829, 0x7833, 0x782A, 0x7831, 0x7954, 0x795B, 0x794F, 0x795C, 0x7953, 0x7952, 0x7951, 0x79EB, 0x79EC, 0x79E0, 0x79EE, 0x79ED, 0x79EA, 0x79DC, 0x79DE, 0x79DD, 0x7A86, 0x7A89, 0x7A85, 0x7A8B, 0x7A8C, 0x7A8A, 0x7A87, 0x7AD8, 0x7B10, + 0x7B04, 0x7B13, 0x7B05, 0x7B0F, 0x7B08, 0x7B0A, 0x7B0E, 0x7B09, 0x7B12, 0x7C84, 0x7C91, 0x7C8A, 0x7C8C, 0x7C88, 0x7C8D, 0x7C85, 0x7D1E, 0x7D1D, 0x7D11, 0x7D0E, 0x7D18, 0x7D16, 0x7D13, 0x7D1F, 0x7D12, 0x7D0F, 0x7D0C, 0x7F5C, 0x7F61, 0x7F5E, 0x7F60, 0x7F5D, 0x7F5B, 0x7F96, 0x7F92, 0x7FC3, 0x7FC2, 0x7FC0, 0x8016, 0x803E, 0x8039, 0x80FA, 0x80F2, 0x80F9, 0x80F5, 0x8101, 0x80FB, 0x8100, 0x8201, 0x822F, 0x8225, 0x8333, 0x832D, 0x8344, 0x8319, 0x8351, 0x8325, 0x8356, 0x833F, 0x8341, 0x8326, 0x831C, 0x8322, 0x8342, 0x834E, 0x831B, 0x832A, 0x8308, 0x833C, 0x834D, 0x8316, 0x8324, 0x8320, 0x8337, 0x832F, 0x8329, 0x8347, 0x8345, 0x834C, 0x8353, 0x831E, 0x832C, 0x834B, 0x8327, 0x8348, 0x8653, 0x8652, 0x86A2, 0x86A8, 0x8696, 0x868D, 0x8691, 0x869E, 0x8687, 0x8697, 0x8686, 0x868B, 0x869A, 0x8685, 0x86A5, 0x8699, 0x86A1, 0x86A7, 0x8695, 0x8698, 0x868E, 0x869D, 0x8690, 0x8694, 0x8843, 0x8844, 0x886D, 0x8875, 0x8876, 0x8872, 0x8880, 0x8871, 0x887F, 0x886F, 0x8883, 0x887E, 0x8874, 0x887C, 0x8A12, 0x8C47, 0x8C57, 0x8C7B, 0x8CA4, 0x8CA3, 0x8D76, 0x8D78, 0x8DB5, 0x8DB7, 0x8DB6, 0x8ED1, 0x8ED3, 0x8FFE, 0x8FF5, 0x9002, 0x8FFF, 0x8FFB, 0x9004, 0x8FFC, 0x8FF6, 0x90D6, 0x90E0, 0x90D9, 0x90DA, 0x90E3, 0x90DF, 0x90E5, 0x90D8, 0x90DB, 0x90D7, 0x90DC, 0x90E4, 0x9150, + 0x914E, 0x914F, 0x91D5, 0x91E2, 0x91DA, 0x965C, 0x965F, 0x96BC, 0x98E3, 0x9ADF, 0x9B2F, 0x4E7F, 0x5070, 0x506A, 0x5061, 0x505E, 0x5060, 0x5053, 0x504B, 0x505D, 0x5072, 0x5048, 0x504D, 0x5041, 0x505B, 0x504A, 0x5062, 0x5015, 0x5045, 0x505F, 0x5069, 0x506B, 0x5063, 0x5064, 0x5046, 0x5040, 0x506E, 0x5073, 0x5057, 0x5051, 0x51D0, 0x526B, 0x526D, 0x526C, 0x526E, 0x52D6, 0x52D3, 0x532D, 0x539C, 0x5575, 0x5576, 0x553C, 0x554D, 0x5550, 0x5534, 0x552A, 0x5551, 0x5562, 0x5536, 0x5535, 0x5530, 0x5552, 0x5545, 0x550C, 0x5532, 0x5565, 0x554E, 0x5539, 0x5548, 0x552D, 0x553B, 0x5540, 0x554B, 0x570A, 0x5707, 0x57FB, 0x5814, 0x57E2, 0x57F6, 0x57DC, 0x57F4, 0x5800, 0x57ED, 0x57FD, 0x5808, 0x57F8, 0x580B, 0x57F3, 0x57CF, 0x5807, 0x57EE, 0x57E3, 0x57F2, 0x57E5, 0x57EC, 0x57E1, 0x580E, 0x57FC, 0x5810, 0x57E7, 0x5801, 0x580C, 0x57F1, 0x57E9, 0x57F0, 0x580D, 0x5804, 0x595C, 0x5A60, 0x5A58, 0x5A55, 0x5A67, 0x5A5E, 0x5A38, 0x5A35, 0x5A6D, 0x5A50, 0x5A5F, 0x5A65, 0x5A6C, 0x5A53, 0x5A64, 0x5A57, 0x5A43, 0x5A5D, 0x5A52, 0x5A44, 0x5A5B, 0x5A48, 0x5A8E, 0x5A3E, 0x5A4D, 0x5A39, 0x5A4C, 0x5A70, 0x5A69, 0x5A47, 0x5A51, 0x5A56, 0x5A42, 0x5A5C, 0x5B72, 0x5B6E, 0x5BC1, 0x5BC0, 0x5C59, 0x5D1E, 0x5D0B, 0x5D1D, 0x5D1A, 0x5D20, 0x5D0C, 0x5D28, 0x5D0D, 0x5D26, 0x5D25, 0x5D0F, + 0x5D30, 0x5D12, 0x5D23, 0x5D1F, 0x5D2E, 0x5E3E, 0x5E34, 0x5EB1, 0x5EB4, 0x5EB9, 0x5EB2, 0x5EB3, 0x5F36, 0x5F38, 0x5F9B, 0x5F96, 0x5F9F, 0x608A, 0x6090, 0x6086, 0x60BE, 0x60B0, 0x60BA, 0x60D3, 0x60D4, 0x60CF, 0x60E4, 0x60D9, 0x60DD, 0x60C8, 0x60B1, 0x60DB, 0x60B7, 0x60CA, 0x60BF, 0x60C3, 0x60CD, 0x60C0, 0x6332, 0x6365, 0x638A, 0x6382, 0x637D, 0x63BD, 0x639E, 0x63AD, 0x639D, 0x6397, 0x63AB, 0x638E, 0x636F, 0x6387, 0x6390, 0x636E, 0x63AF, 0x6375, 0x639C, 0x636D, 0x63AE, 0x637C, 0x63A4, 0x633B, 0x639F, 0x6378, 0x6385, 0x6381, 0x6391, 0x638D, 0x6370, 0x6553, 0x65CD, 0x6665, 0x6661, 0x665B, 0x6659, 0x665C, 0x6662, 0x6718, 0x6879, 0x6887, 0x6890, 0x689C, 0x686D, 0x686E, 0x68AE, 0x68AB, 0x6956, 0x686F, 0x68A3, 0x68AC, 0x68A9, 0x6875, 0x6874, 0x68B2, 0x688F, 0x6877, 0x6892, 0x687C, 0x686B, 0x6872, 0x68AA, 0x6880, 0x6871, 0x687E, 0x689B, 0x6896, 0x688B, 0x68A0, 0x6889, 0x68A4, 0x6878, 0x687B, 0x6891, 0x688C, 0x688A, 0x687D, 0x6B36, 0x6B33, 0x6B37, 0x6B38, 0x6B91, 0x6B8F, 0x6B8D, 0x6B8E, 0x6B8C, 0x6C2A, 0x6DC0, 0x6DAB, 0x6DB4, 0x6DB3, 0x6E74, 0x6DAC, 0x6DE9, 0x6DE2, 0x6DB7, 0x6DF6, 0x6DD4, 0x6E00, 0x6DC8, 0x6DE0, 0x6DDF, 0x6DD6, 0x6DBE, 0x6DE5, 0x6DDC, 0x6DDD, 0x6DDB, 0x6DF4, 0x6DCA, 0x6DBD, 0x6DED, 0x6DF0, 0x6DBA, 0x6DD5, 0x6DC2, 0x6DCF, 0x6DC9, + 0x6DD0, 0x6DF2, 0x6DD3, 0x6DFD, 0x6DD7, 0x6DCD, 0x6DE3, 0x6DBB, 0x70FA, 0x710D, 0x70F7, 0x7117, 0x70F4, 0x710C, 0x70F0, 0x7104, 0x70F3, 0x7110, 0x70FC, 0x70FF, 0x7106, 0x7113, 0x7100, 0x70F8, 0x70F6, 0x710B, 0x7102, 0x710E, 0x727E, 0x727B, 0x727C, 0x727F, 0x731D, 0x7317, 0x7307, 0x7311, 0x7318, 0x730A, 0x7308, 0x72FF, 0x730F, 0x731E, 0x7388, 0x73F6, 0x73F8, 0x73F5, 0x7404, 0x7401, 0x73FD, 0x7407, 0x7400, 0x73FA, 0x73FC, 0x73FF, 0x740C, 0x740B, 0x73F4, 0x7408, 0x7564, 0x7563, 0x75CE, 0x75D2, 0x75CF, 0x75CB, 0x75CC, 0x75D1, 0x75D0, 0x768F, 0x7689, 0x76D3, 0x7739, 0x772F, 0x772D, 0x7731, 0x7732, 0x7734, 0x7733, 0x773D, 0x7725, 0x773B, 0x7735, 0x7848, 0x7852, 0x7849, 0x784D, 0x784A, 0x784C, 0x7826, 0x7845, 0x7850, 0x7964, 0x7967, 0x7969, 0x796A, 0x7963, 0x796B, 0x7961, 0x79BB, 0x79FA, 0x79F8, 0x79F6, 0x79F7, 0x7A8F, 0x7A94, 0x7A90, 0x7B35, 0x7B47, 0x7B34, 0x7B25, 0x7B30, 0x7B22, 0x7B24, 0x7B33, 0x7B18, 0x7B2A, 0x7B1D, 0x7B31, 0x7B2B, 0x7B2D, 0x7B2F, 0x7B32, 0x7B38, 0x7B1A, 0x7B23, 0x7C94, 0x7C98, 0x7C96, 0x7CA3, 0x7D35, 0x7D3D, 0x7D38, 0x7D36, 0x7D3A, 0x7D45, 0x7D2C, 0x7D29, 0x7D41, 0x7D47, 0x7D3E, 0x7D3F, 0x7D4A, 0x7D3B, 0x7D28, 0x7F63, 0x7F95, 0x7F9C, 0x7F9D, 0x7F9B, 0x7FCA, 0x7FCB, 0x7FCD, 0x7FD0, 0x7FD1, 0x7FC7, 0x7FCF, 0x7FC9, 0x801F, + 0x801E, 0x801B, 0x8047, 0x8043, 0x8048, 0x8118, 0x8125, 0x8119, 0x811B, 0x812D, 0x811F, 0x812C, 0x811E, 0x8121, 0x8115, 0x8127, 0x811D, 0x8122, 0x8211, 0x8238, 0x8233, 0x823A, 0x8234, 0x8232, 0x8274, 0x8390, 0x83A3, 0x83A8, 0x838D, 0x837A, 0x8373, 0x83A4, 0x8374, 0x838F, 0x8381, 0x8395, 0x8399, 0x8375, 0x8394, 0x83A9, 0x837D, 0x8383, 0x838C, 0x839D, 0x839B, 0x83AA, 0x838B, 0x837E, 0x83A5, 0x83AF, 0x8388, 0x8397, 0x83B0, 0x837F, 0x83A6, 0x8387, 0x83AE, 0x8376, 0x839A, 0x8659, 0x8656, 0x86BF, 0x86B7, 0x86C2, 0x86C1, 0x86C5, 0x86BA, 0x86B0, 0x86C8, 0x86B9, 0x86B3, 0x86B8, 0x86CC, 0x86B4, 0x86BB, 0x86BC, 0x86C3, 0x86BD, 0x86BE, 0x8852, 0x8889, 0x8895, 0x88A8, 0x88A2, 0x88AA, 0x889A, 0x8891, 0x88A1, 0x889F, 0x8898, 0x88A7, 0x8899, 0x889B, 0x8897, 0x88A4, 0x88AC, 0x888C, 0x8893, 0x888E, 0x8982, 0x89D6, 0x89D9, 0x89D5, 0x8A30, 0x8A27, 0x8A2C, 0x8A1E, 0x8C39, 0x8C3B, 0x8C5C, 0x8C5D, 0x8C7D, 0x8CA5, 0x8D7D, 0x8D7B, 0x8D79, 0x8DBC, 0x8DC2, 0x8DB9, 0x8DBF, 0x8DC1, 0x8ED8, 0x8EDE, 0x8EDD, 0x8EDC, 0x8ED7, 0x8EE0, 0x8EE1, 0x9024, 0x900B, 0x9011, 0x901C, 0x900C, 0x9021, 0x90EF, 0x90EA, 0x90F0, 0x90F4, 0x90F2, 0x90F3, 0x90D4, 0x90EB, 0x90EC, 0x90E9, 0x9156, 0x9158, 0x915A, 0x9153, 0x9155, 0x91EC, 0x91F4, 0x91F1, 0x91F3, 0x91F8, 0x91E4, 0x91F9, 0x91EA, + 0x91EB, 0x91F7, 0x91E8, 0x91EE, 0x957A, 0x9586, 0x9588, 0x967C, 0x966D, 0x966B, 0x9671, 0x966F, 0x96BF, 0x976A, 0x9804, 0x98E5, 0x9997, 0x509B, 0x5095, 0x5094, 0x509E, 0x508B, 0x50A3, 0x5083, 0x508C, 0x508E, 0x509D, 0x5068, 0x509C, 0x5092, 0x5082, 0x5087, 0x515F, 0x51D4, 0x5312, 0x5311, 0x53A4, 0x53A7, 0x5591, 0x55A8, 0x55A5, 0x55AD, 0x5577, 0x5645, 0x55A2, 0x5593, 0x5588, 0x558F, 0x55B5, 0x5581, 0x55A3, 0x5592, 0x55A4, 0x557D, 0x558C, 0x55A6, 0x557F, 0x5595, 0x55A1, 0x558E, 0x570C, 0x5829, 0x5837, 0x5819, 0x581E, 0x5827, 0x5823, 0x5828, 0x57F5, 0x5848, 0x5825, 0x581C, 0x581B, 0x5833, 0x583F, 0x5836, 0x582E, 0x5839, 0x5838, 0x582D, 0x582C, 0x583B, 0x5961, 0x5AAF, 0x5A94, 0x5A9F, 0x5A7A, 0x5AA2, 0x5A9E, 0x5A78, 0x5AA6, 0x5A7C, 0x5AA5, 0x5AAC, 0x5A95, 0x5AAE, 0x5A37, 0x5A84, 0x5A8A, 0x5A97, 0x5A83, 0x5A8B, 0x5AA9, 0x5A7B, 0x5A7D, 0x5A8C, 0x5A9C, 0x5A8F, 0x5A93, 0x5A9D, 0x5BEA, 0x5BCD, 0x5BCB, 0x5BD4, 0x5BD1, 0x5BCA, 0x5BCE, 0x5C0C, 0x5C30, 0x5D37, 0x5D43, 0x5D6B, 0x5D41, 0x5D4B, 0x5D3F, 0x5D35, 0x5D51, 0x5D4E, 0x5D55, 0x5D33, 0x5D3A, 0x5D52, 0x5D3D, 0x5D31, 0x5D59, 0x5D42, 0x5D39, 0x5D49, 0x5D38, 0x5D3C, 0x5D32, 0x5D36, 0x5D40, 0x5D45, 0x5E44, 0x5E41, 0x5F58, 0x5FA6, 0x5FA5, 0x5FAB, 0x60C9, 0x60B9, 0x60CC, 0x60E2, 0x60CE, 0x60C4, 0x6114, + 0x60F2, 0x610A, 0x6116, 0x6105, 0x60F5, 0x6113, 0x60F8, 0x60FC, 0x60FE, 0x60C1, 0x6103, 0x6118, 0x611D, 0x6110, 0x60FF, 0x6104, 0x610B, 0x624A, 0x6394, 0x63B1, 0x63B0, 0x63CE, 0x63E5, 0x63E8, 0x63EF, 0x63C3, 0x649D, 0x63F3, 0x63CA, 0x63E0, 0x63F6, 0x63D5, 0x63F2, 0x63F5, 0x6461, 0x63DF, 0x63BE, 0x63DD, 0x63DC, 0x63C4, 0x63D8, 0x63D3, 0x63C2, 0x63C7, 0x63CC, 0x63CB, 0x63C8, 0x63F0, 0x63D7, 0x63D9, 0x6532, 0x6567, 0x656A, 0x6564, 0x655C, 0x6568, 0x6565, 0x658C, 0x659D, 0x659E, 0x65AE, 0x65D0, 0x65D2, 0x667C, 0x666C, 0x667B, 0x6680, 0x6671, 0x6679, 0x666A, 0x6672, 0x6701, 0x690C, 0x68D3, 0x6904, 0x68DC, 0x692A, 0x68EC, 0x68EA, 0x68F1, 0x690F, 0x68D6, 0x68F7, 0x68EB, 0x68E4, 0x68F6, 0x6913, 0x6910, 0x68F3, 0x68E1, 0x6907, 0x68CC, 0x6908, 0x6970, 0x68B4, 0x6911, 0x68EF, 0x68C6, 0x6914, 0x68F8, 0x68D0, 0x68FD, 0x68FC, 0x68E8, 0x690B, 0x690A, 0x6917, 0x68CE, 0x68C8, 0x68DD, 0x68DE, 0x68E6, 0x68F4, 0x68D1, 0x6906, 0x68D4, 0x68E9, 0x6915, 0x6925, 0x68C7, 0x6B39, 0x6B3B, 0x6B3F, 0x6B3C, 0x6B94, 0x6B97, 0x6B99, 0x6B95, 0x6BBD, 0x6BF0, 0x6BF2, 0x6BF3, 0x6C30, 0x6DFC, 0x6E46, 0x6E47, 0x6E1F, 0x6E49, 0x6E88, 0x6E3C, 0x6E3D, 0x6E45, 0x6E62, 0x6E2B, 0x6E3F, 0x6E41, 0x6E5D, 0x6E73, 0x6E1C, 0x6E33, 0x6E4B, 0x6E40, 0x6E51, 0x6E3B, 0x6E03, 0x6E2E, 0x6E5E, + 0x6E68, 0x6E5C, 0x6E61, 0x6E31, 0x6E28, 0x6E60, 0x6E71, 0x6E6B, 0x6E39, 0x6E22, 0x6E30, 0x6E53, 0x6E65, 0x6E27, 0x6E78, 0x6E64, 0x6E77, 0x6E55, 0x6E79, 0x6E52, 0x6E66, 0x6E35, 0x6E36, 0x6E5A, 0x7120, 0x711E, 0x712F, 0x70FB, 0x712E, 0x7131, 0x7123, 0x7125, 0x7122, 0x7132, 0x711F, 0x7128, 0x713A, 0x711B, 0x724B, 0x725A, 0x7288, 0x7289, 0x7286, 0x7285, 0x728B, 0x7312, 0x730B, 0x7330, 0x7322, 0x7331, 0x7333, 0x7327, 0x7332, 0x732D, 0x7326, 0x7323, 0x7335, 0x730C, 0x742E, 0x742C, 0x7430, 0x742B, 0x7416, 0x741A, 0x7421, 0x742D, 0x7431, 0x7424, 0x7423, 0x741D, 0x7429, 0x7420, 0x7432, 0x74FB, 0x752F, 0x756F, 0x756C, 0x75E7, 0x75DA, 0x75E1, 0x75E6, 0x75DD, 0x75DF, 0x75E4, 0x75D7, 0x7695, 0x7692, 0x76DA, 0x7746, 0x7747, 0x7744, 0x774D, 0x7745, 0x774A, 0x774E, 0x774B, 0x774C, 0x77DE, 0x77EC, 0x7860, 0x7864, 0x7865, 0x785C, 0x786D, 0x7871, 0x786A, 0x786E, 0x7870, 0x7869, 0x7868, 0x785E, 0x7862, 0x7974, 0x7973, 0x7972, 0x7970, 0x7A02, 0x7A0A, 0x7A03, 0x7A0C, 0x7A04, 0x7A99, 0x7AE6, 0x7AE4, 0x7B4A, 0x7B3B, 0x7B44, 0x7B48, 0x7B4C, 0x7B4E, 0x7B40, 0x7B58, 0x7B45, 0x7CA2, 0x7C9E, 0x7CA8, 0x7CA1, 0x7D58, 0x7D6F, 0x7D63, 0x7D53, 0x7D56, 0x7D67, 0x7D6A, 0x7D4F, 0x7D6D, 0x7D5C, 0x7D6B, 0x7D52, 0x7D54, 0x7D69, 0x7D51, 0x7D5F, 0x7D4E, 0x7F3E, 0x7F3F, 0x7F65, + 0x7F66, 0x7FA2, 0x7FA0, 0x7FA1, 0x7FD7, 0x8051, 0x804F, 0x8050, 0x80FE, 0x80D4, 0x8143, 0x814A, 0x8152, 0x814F, 0x8147, 0x813D, 0x814D, 0x813A, 0x81E6, 0x81EE, 0x81F7, 0x81F8, 0x81F9, 0x8204, 0x823C, 0x823D, 0x823F, 0x8275, 0x833B, 0x83CF, 0x83F9, 0x8423, 0x83C0, 0x83E8, 0x8412, 0x83E7, 0x83E4, 0x83FC, 0x83F6, 0x8410, 0x83C6, 0x83C8, 0x83EB, 0x83E3, 0x83BF, 0x8401, 0x83DD, 0x83E5, 0x83D8, 0x83FF, 0x83E1, 0x83CB, 0x83CE, 0x83D6, 0x83F5, 0x83C9, 0x8409, 0x840F, 0x83DE, 0x8411, 0x8406, 0x83C2, 0x83F3, 0x83D5, 0x83FA, 0x83C7, 0x83D1, 0x83EA, 0x8413, 0x83C3, 0x83EC, 0x83EE, 0x83C4, 0x83FB, 0x83D7, 0x83E2, 0x841B, 0x83DB, 0x83FE, 0x86D8, 0x86E2, 0x86E6, 0x86D3, 0x86E3, 0x86DA, 0x86EA, 0x86DD, 0x86EB, 0x86DC, 0x86EC, 0x86E9, 0x86D7, 0x86E8, 0x86D1, 0x8848, 0x8856, 0x8855, 0x88BA, 0x88D7, 0x88B9, 0x88B8, 0x88C0, 0x88BE, 0x88B6, 0x88BC, 0x88B7, 0x88BD, 0x88B2, 0x8901, 0x88C9, 0x8995, 0x8998, 0x8997, 0x89DD, 0x89DA, 0x89DB, 0x8A4E, 0x8A4D, 0x8A39, 0x8A59, 0x8A40, 0x8A57, 0x8A58, 0x8A44, 0x8A45, 0x8A52, 0x8A48, 0x8A51, 0x8A4A, 0x8A4C, 0x8A4F, 0x8C5F, 0x8C81, 0x8C80, 0x8CBA, 0x8CBE, 0x8CB0, 0x8CB9, 0x8CB5, 0x8D84, 0x8D80, 0x8D89, 0x8DD8, 0x8DD3, 0x8DCD, 0x8DC7, 0x8DD6, 0x8DDC, 0x8DCF, 0x8DD5, 0x8DD9, 0x8DC8, 0x8DD7, 0x8DC5, 0x8EEF, 0x8EF7, 0x8EFA, + 0x8EF9, 0x8EE6, 0x8EEE, 0x8EE5, 0x8EF5, 0x8EE7, 0x8EE8, 0x8EF6, 0x8EEB, 0x8EF1, 0x8EEC, 0x8EF4, 0x8EE9, 0x902D, 0x9034, 0x902F, 0x9106, 0x912C, 0x9104, 0x90FF, 0x90FC, 0x9108, 0x90F9, 0x90FB, 0x9101, 0x9100, 0x9107, 0x9105, 0x9103, 0x9161, 0x9164, 0x915F, 0x9162, 0x9160, 0x9201, 0x920A, 0x9225, 0x9203, 0x921A, 0x9226, 0x920F, 0x920C, 0x9200, 0x9212, 0x91FF, 0x91FD, 0x9206, 0x9204, 0x9227, 0x9202, 0x921C, 0x9224, 0x9219, 0x9217, 0x9205, 0x9216, 0x957B, 0x958D, 0x958C, 0x9590, 0x9687, 0x967E, 0x9688, 0x9689, 0x9683, 0x9680, 0x96C2, 0x96C8, 0x96C3, 0x96F1, 0x96F0, 0x976C, 0x9770, 0x976E, 0x9807, 0x98A9, 0x98EB, 0x9CE6, 0x9EF9, 0x4E83, 0x4E84, 0x4EB6, 0x50BD, 0x50BF, 0x50C6, 0x50AE, 0x50C4, 0x50CA, 0x50B4, 0x50C8, 0x50C2, 0x50B0, 0x50C1, 0x50BA, 0x50B1, 0x50CB, 0x50C9, 0x50B6, 0x50B8, 0x51D7, 0x527A, 0x5278, 0x527B, 0x527C, 0x55C3, 0x55DB, 0x55CC, 0x55D0, 0x55CB, 0x55CA, 0x55DD, 0x55C0, 0x55D4, 0x55C4, 0x55E9, 0x55BF, 0x55D2, 0x558D, 0x55CF, 0x55D5, 0x55E2, 0x55D6, 0x55C8, 0x55F2, 0x55CD, 0x55D9, 0x55C2, 0x5714, 0x5853, 0x5868, 0x5864, 0x584F, 0x584D, 0x5849, 0x586F, 0x5855, 0x584E, 0x585D, 0x5859, 0x5865, 0x585B, 0x583D, 0x5863, 0x5871, 0x58FC, 0x5AC7, 0x5AC4, 0x5ACB, 0x5ABA, 0x5AB8, 0x5AB1, 0x5AB5, 0x5AB0, 0x5ABF, 0x5AC8, 0x5ABB, 0x5AC6, + 0x5AB7, 0x5AC0, 0x5ACA, 0x5AB4, 0x5AB6, 0x5ACD, 0x5AB9, 0x5A90, 0x5BD6, 0x5BD8, 0x5BD9, 0x5C1F, 0x5C33, 0x5D71, 0x5D63, 0x5D4A, 0x5D65, 0x5D72, 0x5D6C, 0x5D5E, 0x5D68, 0x5D67, 0x5D62, 0x5DF0, 0x5E4F, 0x5E4E, 0x5E4A, 0x5E4D, 0x5E4B, 0x5EC5, 0x5ECC, 0x5EC6, 0x5ECB, 0x5EC7, 0x5F40, 0x5FAF, 0x5FAD, 0x60F7, 0x6149, 0x614A, 0x612B, 0x6145, 0x6136, 0x6132, 0x612E, 0x6146, 0x612F, 0x614F, 0x6129, 0x6140, 0x6220, 0x9168, 0x6223, 0x6225, 0x6224, 0x63C5, 0x63F1, 0x63EB, 0x6410, 0x6412, 0x6409, 0x6420, 0x6424, 0x6433, 0x6443, 0x641F, 0x6415, 0x6418, 0x6439, 0x6437, 0x6422, 0x6423, 0x640C, 0x6426, 0x6430, 0x6428, 0x6441, 0x6435, 0x642F, 0x640A, 0x641A, 0x6440, 0x6425, 0x6427, 0x640B, 0x63E7, 0x641B, 0x642E, 0x6421, 0x640E, 0x656F, 0x6592, 0x65D3, 0x6686, 0x668C, 0x6695, 0x6690, 0x668B, 0x668A, 0x6699, 0x6694, 0x6678, 0x6720, 0x6966, 0x695F, 0x6938, 0x694E, 0x6962, 0x6971, 0x693F, 0x6945, 0x696A, 0x6939, 0x6942, 0x6957, 0x6959, 0x697A, 0x6948, 0x6949, 0x6935, 0x696C, 0x6933, 0x693D, 0x6965, 0x68F0, 0x6978, 0x6934, 0x6969, 0x6940, 0x696F, 0x6944, 0x6976, 0x6958, 0x6941, 0x6974, 0x694C, 0x693B, 0x694B, 0x6937, 0x695C, 0x694F, 0x6951, 0x6932, 0x6952, 0x692F, 0x697B, 0x693C, 0x6B46, 0x6B45, 0x6B43, 0x6B42, 0x6B48, 0x6B41, 0x6B9B, 0xFA0D, 0x6BFB, 0x6BFC, + 0x6BF9, 0x6BF7, 0x6BF8, 0x6E9B, 0x6ED6, 0x6EC8, 0x6E8F, 0x6EC0, 0x6E9F, 0x6E93, 0x6E94, 0x6EA0, 0x6EB1, 0x6EB9, 0x6EC6, 0x6ED2, 0x6EBD, 0x6EC1, 0x6E9E, 0x6EC9, 0x6EB7, 0x6EB0, 0x6ECD, 0x6EA6, 0x6ECF, 0x6EB2, 0x6EBE, 0x6EC3, 0x6EDC, 0x6ED8, 0x6E99, 0x6E92, 0x6E8E, 0x6E8D, 0x6EA4, 0x6EA1, 0x6EBF, 0x6EB3, 0x6ED0, 0x6ECA, 0x6E97, 0x6EAE, 0x6EA3, 0x7147, 0x7154, 0x7152, 0x7163, 0x7160, 0x7141, 0x715D, 0x7162, 0x7172, 0x7178, 0x716A, 0x7161, 0x7142, 0x7158, 0x7143, 0x714B, 0x7170, 0x715F, 0x7150, 0x7153, 0x7144, 0x714D, 0x715A, 0x724F, 0x728D, 0x728C, 0x7291, 0x7290, 0x728E, 0x733C, 0x7342, 0x733B, 0x733A, 0x7340, 0x734A, 0x7349, 0x7444, 0x744A, 0x744B, 0x7452, 0x7451, 0x7457, 0x7440, 0x744F, 0x7450, 0x744E, 0x7442, 0x7446, 0x744D, 0x7454, 0x74E1, 0x74FF, 0x74FE, 0x74FD, 0x751D, 0x7579, 0x7577, 0x6983, 0x75EF, 0x760F, 0x7603, 0x75F7, 0x75FE, 0x75FC, 0x75F9, 0x75F8, 0x7610, 0x75FB, 0x75F6, 0x75ED, 0x75F5, 0x75FD, 0x7699, 0x76B5, 0x76DD, 0x7755, 0x775F, 0x7760, 0x7752, 0x7756, 0x775A, 0x7769, 0x7767, 0x7754, 0x7759, 0x776D, 0x77E0, 0x7887, 0x789A, 0x7894, 0x788F, 0x7884, 0x7895, 0x7885, 0x7886, 0x78A1, 0x7883, 0x7879, 0x7899, 0x7880, 0x7896, 0x787B, 0x797C, 0x7982, 0x797D, 0x7979, 0x7A11, 0x7A18, 0x7A19, 0x7A12, 0x7A17, 0x7A15, 0x7A22, 0x7A13, + 0x7A1B, 0x7A10, 0x7AA3, 0x7AA2, 0x7A9E, 0x7AEB, 0x7B66, 0x7B64, 0x7B6D, 0x7B74, 0x7B69, 0x7B72, 0x7B65, 0x7B73, 0x7B71, 0x7B70, 0x7B61, 0x7B78, 0x7B76, 0x7B63, 0x7CB2, 0x7CB4, 0x7CAF, 0x7D88, 0x7D86, 0x7D80, 0x7D8D, 0x7D7F, 0x7D85, 0x7D7A, 0x7D8E, 0x7D7B, 0x7D83, 0x7D7C, 0x7D8C, 0x7D94, 0x7D84, 0x7D7D, 0x7D92, 0x7F6D, 0x7F6B, 0x7F67, 0x7F68, 0x7F6C, 0x7FA6, 0x7FA5, 0x7FA7, 0x7FDB, 0x7FDC, 0x8021, 0x8164, 0x8160, 0x8177, 0x815C, 0x8169, 0x815B, 0x8162, 0x8172, 0x6721, 0x815E, 0x8176, 0x8167, 0x816F, 0x8144, 0x8161, 0x821D, 0x8249, 0x8244, 0x8240, 0x8242, 0x8245, 0x84F1, 0x843F, 0x8456, 0x8476, 0x8479, 0x848F, 0x848D, 0x8465, 0x8451, 0x8440, 0x8486, 0x8467, 0x8430, 0x844D, 0x847D, 0x845A, 0x8459, 0x8474, 0x8473, 0x845D, 0x8507, 0x845E, 0x8437, 0x843A, 0x8434, 0x847A, 0x8443, 0x8478, 0x8432, 0x8445, 0x8429, 0x83D9, 0x844B, 0x842F, 0x8442, 0x842D, 0x845F, 0x8470, 0x8439, 0x844E, 0x844C, 0x8452, 0x846F, 0x84C5, 0x848E, 0x843B, 0x8447, 0x8436, 0x8433, 0x8468, 0x847E, 0x8444, 0x842B, 0x8460, 0x8454, 0x846E, 0x8450, 0x870B, 0x8704, 0x86F7, 0x870C, 0x86FA, 0x86D6, 0x86F5, 0x874D, 0x86F8, 0x870E, 0x8709, 0x8701, 0x86F6, 0x870D, 0x8705, 0x88D6, 0x88CB, 0x88CD, 0x88CE, 0x88DE, 0x88DB, 0x88DA, 0x88CC, 0x88D0, 0x8985, 0x899B, 0x89DF, 0x89E5, 0x89E4, + 0x89E1, 0x89E0, 0x89E2, 0x89DC, 0x89E6, 0x8A76, 0x8A86, 0x8A7F, 0x8A61, 0x8A3F, 0x8A77, 0x8A82, 0x8A84, 0x8A75, 0x8A83, 0x8A81, 0x8A74, 0x8A7A, 0x8C3C, 0x8C4B, 0x8C4A, 0x8C65, 0x8C64, 0x8C66, 0x8C86, 0x8C84, 0x8C85, 0x8CCC, 0x8D68, 0x8D69, 0x8D91, 0x8D8C, 0x8D8E, 0x8D8F, 0x8D8D, 0x8D93, 0x8D94, 0x8D90, 0x8D92, 0x8DF0, 0x8DE0, 0x8DEC, 0x8DF1, 0x8DEE, 0x8DD0, 0x8DE9, 0x8DE3, 0x8DE2, 0x8DE7, 0x8DF2, 0x8DEB, 0x8DF4, 0x8F06, 0x8EFF, 0x8F01, 0x8F00, 0x8F05, 0x8F07, 0x8F08, 0x8F02, 0x8F0B, 0x9052, 0x903F, 0x9044, 0x9049, 0x903D, 0x9110, 0x910D, 0x910F, 0x9111, 0x9116, 0x9114, 0x910B, 0x910E, 0x916E, 0x916F, 0x9248, 0x9252, 0x9230, 0x923A, 0x9266, 0x9233, 0x9265, 0x925E, 0x9283, 0x922E, 0x924A, 0x9246, 0x926D, 0x926C, 0x924F, 0x9260, 0x9267, 0x926F, 0x9236, 0x9261, 0x9270, 0x9231, 0x9254, 0x9263, 0x9250, 0x9272, 0x924E, 0x9253, 0x924C, 0x9256, 0x9232, 0x959F, 0x959C, 0x959E, 0x959B, 0x9692, 0x9693, 0x9691, 0x9697, 0x96CE, 0x96FA, 0x96FD, 0x96F8, 0x96F5, 0x9773, 0x9777, 0x9778, 0x9772, 0x980F, 0x980D, 0x980E, 0x98AC, 0x98F6, 0x98F9, 0x99AF, 0x99B2, 0x99B0, 0x99B5, 0x9AAD, 0x9AAB, 0x9B5B, 0x9CEA, 0x9CED, 0x9CE7, 0x9E80, 0x9EFD, 0x50E6, 0x50D4, 0x50D7, 0x50E8, 0x50F3, 0x50DB, 0x50EA, 0x50DD, 0x50E4, 0x50D3, 0x50EC, 0x50F0, 0x50EF, 0x50E3, 0x50E0, + 0x51D8, 0x5280, 0x5281, 0x52E9, 0x52EB, 0x5330, 0x53AC, 0x5627, 0x5615, 0x560C, 0x5612, 0x55FC, 0x560F, 0x561C, 0x5601, 0x5613, 0x5602, 0x55FA, 0x561D, 0x5604, 0x55FF, 0x55F9, 0x5889, 0x587C, 0x5890, 0x5898, 0x5886, 0x5881, 0x587F, 0x5874, 0x588B, 0x587A, 0x5887, 0x5891, 0x588E, 0x5876, 0x5882, 0x5888, 0x587B, 0x5894, 0x588F, 0x58FE, 0x596B, 0x5ADC, 0x5AEE, 0x5AE5, 0x5AD5, 0x5AEA, 0x5ADA, 0x5AED, 0x5AEB, 0x5AF3, 0x5AE2, 0x5AE0, 0x5ADB, 0x5AEC, 0x5ADE, 0x5ADD, 0x5AD9, 0x5AE8, 0x5ADF, 0x5B77, 0x5BE0, 0x5BE3, 0x5C63, 0x5D82, 0x5D80, 0x5D7D, 0x5D86, 0x5D7A, 0x5D81, 0x5D77, 0x5D8A, 0x5D89, 0x5D88, 0x5D7E, 0x5D7C, 0x5D8D, 0x5D79, 0x5D7F, 0x5E58, 0x5E59, 0x5E53, 0x5ED8, 0x5ED1, 0x5ED7, 0x5ECE, 0x5EDC, 0x5ED5, 0x5ED9, 0x5ED2, 0x5ED4, 0x5F44, 0x5F43, 0x5F6F, 0x5FB6, 0x612C, 0x6128, 0x6141, 0x615E, 0x6171, 0x6173, 0x6152, 0x6153, 0x6172, 0x616C, 0x6180, 0x6174, 0x6154, 0x617A, 0x615B, 0x6165, 0x613B, 0x616A, 0x6161, 0x6156, 0x6229, 0x6227, 0x622B, 0x642B, 0x644D, 0x645B, 0x645D, 0x6474, 0x6476, 0x6472, 0x6473, 0x647D, 0x6475, 0x6466, 0x64A6, 0x644E, 0x6482, 0x645E, 0x645C, 0x644B, 0x6453, 0x6460, 0x6450, 0x647F, 0x643F, 0x646C, 0x646B, 0x6459, 0x6465, 0x6477, 0x6573, 0x65A0, 0x66A1, 0x66A0, 0x669F, 0x6705, 0x6704, 0x6722, 0x69B1, 0x69B6, 0x69C9, + 0x69A0, 0x69CE, 0x6996, 0x69B0, 0x69AC, 0x69BC, 0x6991, 0x6999, 0x698E, 0x69A7, 0x698D, 0x69A9, 0x69BE, 0x69AF, 0x69BF, 0x69C4, 0x69BD, 0x69A4, 0x69D4, 0x69B9, 0x69CA, 0x699A, 0x69CF, 0x69B3, 0x6993, 0x69AA, 0x69A1, 0x699E, 0x69D9, 0x6997, 0x6990, 0x69C2, 0x69B5, 0x69A5, 0x69C6, 0x6B4A, 0x6B4D, 0x6B4B, 0x6B9E, 0x6B9F, 0x6BA0, 0x6BC3, 0x6BC4, 0x6BFE, 0x6ECE, 0x6EF5, 0x6EF1, 0x6F03, 0x6F25, 0x6EF8, 0x6F37, 0x6EFB, 0x6F2E, 0x6F09, 0x6F4E, 0x6F19, 0x6F1A, 0x6F27, 0x6F18, 0x6F3B, 0x6F12, 0x6EED, 0x6F0A, 0x6F36, 0x6F73, 0x6EF9, 0x6EEE, 0x6F2D, 0x6F40, 0x6F30, 0x6F3C, 0x6F35, 0x6EEB, 0x6F07, 0x6F0E, 0x6F43, 0x6F05, 0x6EFD, 0x6EF6, 0x6F39, 0x6F1C, 0x6EFC, 0x6F3A, 0x6F1F, 0x6F0D, 0x6F1E, 0x6F08, 0x6F21, 0x7187, 0x7190, 0x7189, 0x7180, 0x7185, 0x7182, 0x718F, 0x717B, 0x7186, 0x7181, 0x7197, 0x7244, 0x7253, 0x7297, 0x7295, 0x7293, 0x7343, 0x734D, 0x7351, 0x734C, 0x7462, 0x7473, 0x7471, 0x7475, 0x7472, 0x7467, 0x746E, 0x7500, 0x7502, 0x7503, 0x757D, 0x7590, 0x7616, 0x7608, 0x760C, 0x7615, 0x7611, 0x760A, 0x7614, 0x76B8, 0x7781, 0x777C, 0x7785, 0x7782, 0x776E, 0x7780, 0x776F, 0x777E, 0x7783, 0x78B2, 0x78AA, 0x78B4, 0x78AD, 0x78A8, 0x787E, 0x78AB, 0x789E, 0x78A5, 0x78A0, 0x78AC, 0x78A2, 0x78A4, 0x7998, 0x798A, 0x798B, 0x7996, 0x7995, 0x7994, 0x7993, + 0x7997, 0x7988, 0x7992, 0x7990, 0x7A2B, 0x7A4A, 0x7A30, 0x7A2F, 0x7A28, 0x7A26, 0x7AA8, 0x7AAB, 0x7AAC, 0x7AEE, 0x7B88, 0x7B9C, 0x7B8A, 0x7B91, 0x7B90, 0x7B96, 0x7B8D, 0x7B8C, 0x7B9B, 0x7B8E, 0x7B85, 0x7B98, 0x5284, 0x7B99, 0x7BA4, 0x7B82, 0x7CBB, 0x7CBF, 0x7CBC, 0x7CBA, 0x7DA7, 0x7DB7, 0x7DC2, 0x7DA3, 0x7DAA, 0x7DC1, 0x7DC0, 0x7DC5, 0x7D9D, 0x7DCE, 0x7DC4, 0x7DC6, 0x7DCB, 0x7DCC, 0x7DAF, 0x7DB9, 0x7D96, 0x7DBC, 0x7D9F, 0x7DA6, 0x7DAE, 0x7DA9, 0x7DA1, 0x7DC9, 0x7F73, 0x7FE2, 0x7FE3, 0x7FE5, 0x7FDE, 0x8024, 0x805D, 0x805C, 0x8189, 0x8186, 0x8183, 0x8187, 0x818D, 0x818C, 0x818B, 0x8215, 0x8497, 0x84A4, 0x84A1, 0x849F, 0x84BA, 0x84CE, 0x84C2, 0x84AC, 0x84AE, 0x84AB, 0x84B9, 0x84B4, 0x84C1, 0x84CD, 0x84AA, 0x849A, 0x84B1, 0x84D0, 0x849D, 0x84A7, 0x84BB, 0x84A2, 0x8494, 0x84C7, 0x84CC, 0x849B, 0x84A9, 0x84AF, 0x84A8, 0x84D6, 0x8498, 0x84B6, 0x84CF, 0x84A0, 0x84D7, 0x84D4, 0x84D2, 0x84DB, 0x84B0, 0x8491, 0x8661, 0x8733, 0x8723, 0x8728, 0x876B, 0x8740, 0x872E, 0x871E, 0x8721, 0x8719, 0x871B, 0x8743, 0x872C, 0x8741, 0x873E, 0x8746, 0x8720, 0x8732, 0x872A, 0x872D, 0x873C, 0x8712, 0x873A, 0x8731, 0x8735, 0x8742, 0x8726, 0x8727, 0x8738, 0x8724, 0x871A, 0x8730, 0x8711, 0x88F7, 0x88E7, 0x88F1, 0x88F2, 0x88FA, 0x88FE, 0x88EE, 0x88FC, 0x88F6, 0x88FB, + 0x88F0, 0x88EC, 0x88EB, 0x899D, 0x89A1, 0x899F, 0x899E, 0x89E9, 0x89EB, 0x89E8, 0x8AAB, 0x8A99, 0x8A8B, 0x8A92, 0x8A8F, 0x8A96, 0x8C3D, 0x8C68, 0x8C69, 0x8CD5, 0x8CCF, 0x8CD7, 0x8D96, 0x8E09, 0x8E02, 0x8DFF, 0x8E0D, 0x8DFD, 0x8E0A, 0x8E03, 0x8E07, 0x8E06, 0x8E05, 0x8DFE, 0x8E00, 0x8E04, 0x8F10, 0x8F11, 0x8F0E, 0x8F0D, 0x9123, 0x911C, 0x9120, 0x9122, 0x911F, 0x911D, 0x911A, 0x9124, 0x9121, 0x911B, 0x917A, 0x9172, 0x9179, 0x9173, 0x92A5, 0x92A4, 0x9276, 0x929B, 0x927A, 0x92A0, 0x9294, 0x92AA, 0x928D, 0x92A6, 0x929A, 0x92AB, 0x9279, 0x9297, 0x927F, 0x92A3, 0x92EE, 0x928E, 0x9282, 0x9295, 0x92A2, 0x927D, 0x9288, 0x92A1, 0x928A, 0x9286, 0x928C, 0x9299, 0x92A7, 0x927E, 0x9287, 0x92A9, 0x929D, 0x928B, 0x922D, 0x969E, 0x96A1, 0x96FF, 0x9758, 0x977D, 0x977A, 0x977E, 0x9783, 0x9780, 0x9782, 0x977B, 0x9784, 0x9781, 0x977F, 0x97CE, 0x97CD, 0x9816, 0x98AD, 0x98AE, 0x9902, 0x9900, 0x9907, 0x999D, 0x999C, 0x99C3, 0x99B9, 0x99BB, 0x99BA, 0x99C2, 0x99BD, 0x99C7, 0x9AB1, 0x9AE3, 0x9AE7, 0x9B3E, 0x9B3F, 0x9B60, 0x9B61, 0x9B5F, 0x9CF1, 0x9CF2, 0x9CF5, 0x9EA7, 0x50FF, 0x5103, 0x5130, 0x50F8, 0x5106, 0x5107, 0x50F6, 0x50FE, 0x510B, 0x510C, 0x50FD, 0x510A, 0x528B, 0x528C, 0x52F1, 0x52EF, 0x5648, 0x5642, 0x564C, 0x5635, 0x5641, 0x564A, 0x5649, 0x5646, 0x5658, + 0x565A, 0x5640, 0x5633, 0x563D, 0x562C, 0x563E, 0x5638, 0x562A, 0x563A, 0x571A, 0x58AB, 0x589D, 0x58B1, 0x58A0, 0x58A3, 0x58AF, 0x58AC, 0x58A5, 0x58A1, 0x58FF, 0x5AFF, 0x5AF4, 0x5AFD, 0x5AF7, 0x5AF6, 0x5B03, 0x5AF8, 0x5B02, 0x5AF9, 0x5B01, 0x5B07, 0x5B05, 0x5B0F, 0x5C67, 0x5D99, 0x5D97, 0x5D9F, 0x5D92, 0x5DA2, 0x5D93, 0x5D95, 0x5DA0, 0x5D9C, 0x5DA1, 0x5D9A, 0x5D9E, 0x5E69, 0x5E5D, 0x5E60, 0x5E5C, 0x7DF3, 0x5EDB, 0x5EDE, 0x5EE1, 0x5F49, 0x5FB2, 0x618B, 0x6183, 0x6179, 0x61B1, 0x61B0, 0x61A2, 0x6189, 0x619B, 0x6193, 0x61AF, 0x61AD, 0x619F, 0x6192, 0x61AA, 0x61A1, 0x618D, 0x6166, 0x61B3, 0x622D, 0x646E, 0x6470, 0x6496, 0x64A0, 0x6485, 0x6497, 0x649C, 0x648F, 0x648B, 0x648A, 0x648C, 0x64A3, 0x649F, 0x6468, 0x64B1, 0x6498, 0x6576, 0x657A, 0x6579, 0x657B, 0x65B2, 0x65B3, 0x66B5, 0x66B0, 0x66A9, 0x66B2, 0x66B7, 0x66AA, 0x66AF, 0x6A00, 0x6A06, 0x6A17, 0x69E5, 0x69F8, 0x6A15, 0x69F1, 0x69E4, 0x6A20, 0x69FF, 0x69EC, 0x69E2, 0x6A1B, 0x6A1D, 0x69FE, 0x6A27, 0x69F2, 0x69EE, 0x6A14, 0x69F7, 0x69E7, 0x6A40, 0x6A08, 0x69E6, 0x69FB, 0x6A0D, 0x69FC, 0x69EB, 0x6A09, 0x6A04, 0x6A18, 0x6A25, 0x6A0F, 0x69F6, 0x6A26, 0x6A07, 0x69F4, 0x6A16, 0x6B51, 0x6BA5, 0x6BA3, 0x6BA2, 0x6BA6, 0x6C01, 0x6C00, 0x6BFF, 0x6C02, 0x6F41, 0x6F26, 0x6F7E, 0x6F87, 0x6FC6, 0x6F92, + 0x6F8D, 0x6F89, 0x6F8C, 0x6F62, 0x6F4F, 0x6F85, 0x6F5A, 0x6F96, 0x6F76, 0x6F6C, 0x6F82, 0x6F55, 0x6F72, 0x6F52, 0x6F50, 0x6F57, 0x6F94, 0x6F93, 0x6F5D, 0x6F00, 0x6F61, 0x6F6B, 0x6F7D, 0x6F67, 0x6F90, 0x6F53, 0x6F8B, 0x6F69, 0x6F7F, 0x6F95, 0x6F63, 0x6F77, 0x6F6A, 0x6F7B, 0x71B2, 0x71AF, 0x719B, 0x71B0, 0x71A0, 0x719A, 0x71A9, 0x71B5, 0x719D, 0x71A5, 0x719E, 0x71A4, 0x71A1, 0x71AA, 0x719C, 0x71A7, 0x71B3, 0x7298, 0x729A, 0x7358, 0x7352, 0x735E, 0x735F, 0x7360, 0x735D, 0x735B, 0x7361, 0x735A, 0x7359, 0x7362, 0x7487, 0x7489, 0x748A, 0x7486, 0x7481, 0x747D, 0x7485, 0x7488, 0x747C, 0x7479, 0x7508, 0x7507, 0x757E, 0x7625, 0x761E, 0x7619, 0x761D, 0x761C, 0x7623, 0x761A, 0x7628, 0x761B, 0x769C, 0x769D, 0x769E, 0x769B, 0x778D, 0x778F, 0x7789, 0x7788, 0x78CD, 0x78BB, 0x78CF, 0x78CC, 0x78D1, 0x78CE, 0x78D4, 0x78C8, 0x78C3, 0x78C4, 0x78C9, 0x799A, 0x79A1, 0x79A0, 0x799C, 0x79A2, 0x799B, 0x6B76, 0x7A39, 0x7AB2, 0x7AB4, 0x7AB3, 0x7BB7, 0x7BCB, 0x7BBE, 0x7BAC, 0x7BCE, 0x7BAF, 0x7BB9, 0x7BCA, 0x7BB5, 0x7CC5, 0x7CC8, 0x7CCC, 0x7CCB, 0x7DF7, 0x7DDB, 0x7DEA, 0x7DE7, 0x7DD7, 0x7DE1, 0x7E03, 0x7DFA, 0x7DE6, 0x7DF6, 0x7DF1, 0x7DF0, 0x7DEE, 0x7DDF, 0x7F76, 0x7FAC, 0x7FB0, 0x7FAD, 0x7FED, 0x7FEB, 0x7FEA, 0x7FEC, 0x7FE6, 0x7FE8, 0x8064, 0x8067, 0x81A3, 0x819F, + 0x819E, 0x8195, 0x81A2, 0x8199, 0x8197, 0x8216, 0x824F, 0x8253, 0x8252, 0x8250, 0x824E, 0x8251, 0x8524, 0x853B, 0x850F, 0x8500, 0x8529, 0x850E, 0x8509, 0x850D, 0x851F, 0x850A, 0x8527, 0x851C, 0x84FB, 0x852B, 0x84FA, 0x8508, 0x850C, 0x84F4, 0x852A, 0x84F2, 0x8515, 0x84F7, 0x84EB, 0x84F3, 0x84FC, 0x8512, 0x84EA, 0x84E9, 0x8516, 0x84FE, 0x8528, 0x851D, 0x852E, 0x8502, 0x84FD, 0x851E, 0x84F6, 0x8531, 0x8526, 0x84E7, 0x84E8, 0x84F0, 0x84EF, 0x84F9, 0x8518, 0x8520, 0x8530, 0x850B, 0x8519, 0x852F, 0x8662, 0x8756, 0x8763, 0x8764, 0x8777, 0x87E1, 0x8773, 0x8758, 0x8754, 0x875B, 0x8752, 0x8761, 0x875A, 0x8751, 0x875E, 0x876D, 0x876A, 0x8750, 0x874E, 0x875F, 0x875D, 0x876F, 0x876C, 0x877A, 0x876E, 0x875C, 0x8765, 0x874F, 0x877B, 0x8775, 0x8762, 0x8767, 0x8769, 0x885A, 0x8905, 0x890C, 0x8914, 0x890B, 0x8917, 0x8918, 0x8919, 0x8906, 0x8916, 0x8911, 0x890E, 0x8909, 0x89A2, 0x89A4, 0x89A3, 0x89ED, 0x89F0, 0x89EC, 0x8ACF, 0x8AC6, 0x8AB8, 0x8AD3, 0x8AD1, 0x8AD4, 0x8AD5, 0x8ABB, 0x8AD7, 0x8ABE, 0x8AC0, 0x8AC5, 0x8AD8, 0x8AC3, 0x8ABA, 0x8ABD, 0x8AD9, 0x8C3E, 0x8C4D, 0x8C8F, 0x8CE5, 0x8CDF, 0x8CD9, 0x8CE8, 0x8CDA, 0x8CDD, 0x8CE7, 0x8DA0, 0x8D9C, 0x8DA1, 0x8D9B, 0x8E20, 0x8E23, 0x8E25, 0x8E24, 0x8E2E, 0x8E15, 0x8E1B, 0x8E16, 0x8E11, 0x8E19, 0x8E26, 0x8E27, + 0x8E14, 0x8E12, 0x8E18, 0x8E13, 0x8E1C, 0x8E17, 0x8E1A, 0x8F2C, 0x8F24, 0x8F18, 0x8F1A, 0x8F20, 0x8F23, 0x8F16, 0x8F17, 0x9073, 0x9070, 0x906F, 0x9067, 0x906B, 0x912F, 0x912B, 0x9129, 0x912A, 0x9132, 0x9126, 0x912E, 0x9185, 0x9186, 0x918A, 0x9181, 0x9182, 0x9184, 0x9180, 0x92D0, 0x92C3, 0x92C4, 0x92C0, 0x92D9, 0x92B6, 0x92CF, 0x92F1, 0x92DF, 0x92D8, 0x92E9, 0x92D7, 0x92DD, 0x92CC, 0x92EF, 0x92C2, 0x92E8, 0x92CA, 0x92C8, 0x92CE, 0x92E6, 0x92CD, 0x92D5, 0x92C9, 0x92E0, 0x92DE, 0x92E7, 0x92D1, 0x92D3, 0x92B5, 0x92E1, 0x92C6, 0x92B4, 0x957C, 0x95AC, 0x95AB, 0x95AE, 0x95B0, 0x96A4, 0x96A2, 0x96D3, 0x9705, 0x9708, 0x9702, 0x975A, 0x978A, 0x978E, 0x9788, 0x97D0, 0x97CF, 0x981E, 0x981D, 0x9826, 0x9829, 0x9828, 0x9820, 0x981B, 0x9827, 0x98B2, 0x9908, 0x98FA, 0x9911, 0x9914, 0x9916, 0x9917, 0x9915, 0x99DC, 0x99CD, 0x99CF, 0x99D3, 0x99D4, 0x99CE, 0x99C9, 0x99D6, 0x99D8, 0x99CB, 0x99D7, 0x99CC, 0x9AB3, 0x9AEC, 0x9AEB, 0x9AF3, 0x9AF2, 0x9AF1, 0x9B46, 0x9B43, 0x9B67, 0x9B74, 0x9B71, 0x9B66, 0x9B76, 0x9B75, 0x9B70, 0x9B68, 0x9B64, 0x9B6C, 0x9CFC, 0x9CFA, 0x9CFD, 0x9CFF, 0x9CF7, 0x9D07, 0x9D00, 0x9CF9, 0x9CFB, 0x9D08, 0x9D05, 0x9D04, 0x9E83, 0x9ED3, 0x9F0F, 0x9F10, 0x511C, 0x5113, 0x5117, 0x511A, 0x5111, 0x51DE, 0x5334, 0x53E1, 0x5670, 0x5660, 0x566E, + 0x5673, 0x5666, 0x5663, 0x566D, 0x5672, 0x565E, 0x5677, 0x571C, 0x571B, 0x58C8, 0x58BD, 0x58C9, 0x58BF, 0x58BA, 0x58C2, 0x58BC, 0x58C6, 0x5B17, 0x5B19, 0x5B1B, 0x5B21, 0x5B14, 0x5B13, 0x5B10, 0x5B16, 0x5B28, 0x5B1A, 0x5B20, 0x5B1E, 0x5BEF, 0x5DAC, 0x5DB1, 0x5DA9, 0x5DA7, 0x5DB5, 0x5DB0, 0x5DAE, 0x5DAA, 0x5DA8, 0x5DB2, 0x5DAD, 0x5DAF, 0x5DB4, 0x5E67, 0x5E68, 0x5E66, 0x5E6F, 0x5EE9, 0x5EE7, 0x5EE6, 0x5EE8, 0x5EE5, 0x5F4B, 0x5FBC, 0x619D, 0x61A8, 0x6196, 0x61C5, 0x61B4, 0x61C6, 0x61C1, 0x61CC, 0x61BA, 0x61BF, 0x61B8, 0x618C, 0x64D7, 0x64D6, 0x64D0, 0x64CF, 0x64C9, 0x64BD, 0x6489, 0x64C3, 0x64DB, 0x64F3, 0x64D9, 0x6533, 0x657F, 0x657C, 0x65A2, 0x66C8, 0x66BE, 0x66C0, 0x66CA, 0x66CB, 0x66CF, 0x66BD, 0x66BB, 0x66BA, 0x66CC, 0x6723, 0x6A34, 0x6A66, 0x6A49, 0x6A67, 0x6A32, 0x6A68, 0x6A3E, 0x6A5D, 0x6A6D, 0x6A76, 0x6A5B, 0x6A51, 0x6A28, 0x6A5A, 0x6A3B, 0x6A3F, 0x6A41, 0x6A6A, 0x6A64, 0x6A50, 0x6A4F, 0x6A54, 0x6A6F, 0x6A69, 0x6A60, 0x6A3C, 0x6A5E, 0x6A56, 0x6A55, 0x6A4D, 0x6A4E, 0x6A46, 0x6B55, 0x6B54, 0x6B56, 0x6BA7, 0x6BAA, 0x6BAB, 0x6BC8, 0x6BC7, 0x6C04, 0x6C03, 0x6C06, 0x6FAD, 0x6FCB, 0x6FA3, 0x6FC7, 0x6FBC, 0x6FCE, 0x6FC8, 0x6F5E, 0x6FC4, 0x6FBD, 0x6F9E, 0x6FCA, 0x6FA8, 0x7004, 0x6FA5, 0x6FAE, 0x6FBA, 0x6FAC, 0x6FAA, 0x6FCF, 0x6FBF, 0x6FB8, + 0x6FA2, 0x6FC9, 0x6FAB, 0x6FCD, 0x6FAF, 0x6FB2, 0x6FB0, 0x71C5, 0x71C2, 0x71BF, 0x71B8, 0x71D6, 0x71C0, 0x71C1, 0x71CB, 0x71D4, 0x71CA, 0x71C7, 0x71CF, 0x71BD, 0x71D8, 0x71BC, 0x71C6, 0x71DA, 0x71DB, 0x729D, 0x729E, 0x7369, 0x7366, 0x7367, 0x736C, 0x7365, 0x736B, 0x736A, 0x747F, 0x749A, 0x74A0, 0x7494, 0x7492, 0x7495, 0x74A1, 0x750B, 0x7580, 0x762F, 0x762D, 0x7631, 0x763D, 0x7633, 0x763C, 0x7635, 0x7632, 0x7630, 0x76BB, 0x76E6, 0x779A, 0x779D, 0x77A1, 0x779C, 0x779B, 0x77A2, 0x77A3, 0x7795, 0x7799, 0x7797, 0x78DD, 0x78E9, 0x78E5, 0x78EA, 0x78DE, 0x78E3, 0x78DB, 0x78E1, 0x78E2, 0x78ED, 0x78DF, 0x78E0, 0x79A4, 0x7A44, 0x7A48, 0x7A47, 0x7AB6, 0x7AB8, 0x7AB5, 0x7AB1, 0x7AB7, 0x7BDE, 0x7BE3, 0x7BE7, 0x7BDD, 0x7BD5, 0x7BE5, 0x7BDA, 0x7BE8, 0x7BF9, 0x7BD4, 0x7BEA, 0x7BE2, 0x7BDC, 0x7BEB, 0x7BD8, 0x7BDF, 0x7CD2, 0x7CD4, 0x7CD7, 0x7CD0, 0x7CD1, 0x7E12, 0x7E21, 0x7E17, 0x7E0C, 0x7E1F, 0x7E20, 0x7E13, 0x7E0E, 0x7E1C, 0x7E15, 0x7E1A, 0x7E22, 0x7E0B, 0x7E0F, 0x7E16, 0x7E0D, 0x7E14, 0x7E25, 0x7E24, 0x7F43, 0x7F7B, 0x7F7C, 0x7F7A, 0x7FB1, 0x7FEF, 0x802A, 0x8029, 0x806C, 0x81B1, 0x81A6, 0x81AE, 0x81B9, 0x81B5, 0x81AB, 0x81B0, 0x81AC, 0x81B4, 0x81B2, 0x81B7, 0x81A7, 0x81F2, 0x8255, 0x8256, 0x8257, 0x8556, 0x8545, 0x856B, 0x854D, 0x8553, 0x8561, 0x8558, + 0x8540, 0x8546, 0x8564, 0x8541, 0x8562, 0x8544, 0x8551, 0x8547, 0x8563, 0x853E, 0x855B, 0x8571, 0x854E, 0x856E, 0x8575, 0x8555, 0x8567, 0x8560, 0x858C, 0x8566, 0x855D, 0x8554, 0x8565, 0x856C, 0x8663, 0x8665, 0x8664, 0x879B, 0x878F, 0x8797, 0x8793, 0x8792, 0x8788, 0x8781, 0x8796, 0x8798, 0x8779, 0x8787, 0x87A3, 0x8785, 0x8790, 0x8791, 0x879D, 0x8784, 0x8794, 0x879C, 0x879A, 0x8789, 0x891E, 0x8926, 0x8930, 0x892D, 0x892E, 0x8927, 0x8931, 0x8922, 0x8929, 0x8923, 0x892F, 0x892C, 0x891F, 0x89F1, 0x8AE0, 0x8AE2, 0x8AF2, 0x8AF4, 0x8AF5, 0x8ADD, 0x8B14, 0x8AE4, 0x8ADF, 0x8AF0, 0x8AC8, 0x8ADE, 0x8AE1, 0x8AE8, 0x8AFF, 0x8AEF, 0x8AFB, 0x8C91, 0x8C92, 0x8C90, 0x8CF5, 0x8CEE, 0x8CF1, 0x8CF0, 0x8CF3, 0x8D6C, 0x8D6E, 0x8DA5, 0x8DA7, 0x8E33, 0x8E3E, 0x8E38, 0x8E40, 0x8E45, 0x8E36, 0x8E3C, 0x8E3D, 0x8E41, 0x8E30, 0x8E3F, 0x8EBD, 0x8F36, 0x8F2E, 0x8F35, 0x8F32, 0x8F39, 0x8F37, 0x8F34, 0x9076, 0x9079, 0x907B, 0x9086, 0x90FA, 0x9133, 0x9135, 0x9136, 0x9193, 0x9190, 0x9191, 0x918D, 0x918F, 0x9327, 0x931E, 0x9308, 0x931F, 0x9306, 0x930F, 0x937A, 0x9338, 0x933C, 0x931B, 0x9323, 0x9312, 0x9301, 0x9346, 0x932D, 0x930E, 0x930D, 0x92CB, 0x931D, 0x92FA, 0x9325, 0x9313, 0x92F9, 0x92F7, 0x9334, 0x9302, 0x9324, 0x92FF, 0x9329, 0x9339, 0x9335, 0x932A, 0x9314, 0x930C, + 0x930B, 0x92FE, 0x9309, 0x9300, 0x92FB, 0x9316, 0x95BC, 0x95CD, 0x95BE, 0x95B9, 0x95BA, 0x95B6, 0x95BF, 0x95B5, 0x95BD, 0x96A9, 0x96D4, 0x970B, 0x9712, 0x9710, 0x9799, 0x9797, 0x9794, 0x97F0, 0x97F8, 0x9835, 0x982F, 0x9832, 0x9924, 0x991F, 0x9927, 0x9929, 0x999E, 0x99EE, 0x99EC, 0x99E5, 0x99E4, 0x99F0, 0x99E3, 0x99EA, 0x99E9, 0x99E7, 0x9AB9, 0x9ABF, 0x9AB4, 0x9ABB, 0x9AF6, 0x9AFA, 0x9AF9, 0x9AF7, 0x9B33, 0x9B80, 0x9B85, 0x9B87, 0x9B7C, 0x9B7E, 0x9B7B, 0x9B82, 0x9B93, 0x9B92, 0x9B90, 0x9B7A, 0x9B95, 0x9B7D, 0x9B88, 0x9D25, 0x9D17, 0x9D20, 0x9D1E, 0x9D14, 0x9D29, 0x9D1D, 0x9D18, 0x9D22, 0x9D10, 0x9D19, 0x9D1F, 0x9E88, 0x9E86, 0x9E87, 0x9EAE, 0x9EAD, 0x9ED5, 0x9ED6, 0x9EFA, 0x9F12, 0x9F3D, 0x5126, 0x5125, 0x5122, 0x5124, 0x5120, 0x5129, 0x52F4, 0x5693, 0x568C, 0x568D, 0x5686, 0x5684, 0x5683, 0x567E, 0x5682, 0x567F, 0x5681, 0x58D6, 0x58D4, 0x58CF, 0x58D2, 0x5B2D, 0x5B25, 0x5B32, 0x5B23, 0x5B2C, 0x5B27, 0x5B26, 0x5B2F, 0x5B2E, 0x5B7B, 0x5BF1, 0x5BF2, 0x5DB7, 0x5E6C, 0x5E6A, 0x5FBE, 0x5FBB, 0x61C3, 0x61B5, 0x61BC, 0x61E7, 0x61E0, 0x61E5, 0x61E4, 0x61E8, 0x61DE, 0x64EF, 0x64E9, 0x64E3, 0x64EB, 0x64E4, 0x64E8, 0x6581, 0x6580, 0x65B6, 0x65DA, 0x66D2, 0x6A8D, 0x6A96, 0x6A81, 0x6AA5, 0x6A89, 0x6A9F, 0x6A9B, 0x6AA1, 0x6A9E, 0x6A87, 0x6A93, 0x6A8E, + 0x6A95, 0x6A83, 0x6AA8, 0x6AA4, 0x6A91, 0x6A7F, 0x6AA6, 0x6A9A, 0x6A85, 0x6A8C, 0x6A92, 0x6B5B, 0x6BAD, 0x6C09, 0x6FCC, 0x6FA9, 0x6FF4, 0x6FD4, 0x6FE3, 0x6FDC, 0x6FED, 0x6FE7, 0x6FE6, 0x6FDE, 0x6FF2, 0x6FDD, 0x6FE2, 0x6FE8, 0x71E1, 0x71F1, 0x71E8, 0x71F2, 0x71E4, 0x71F0, 0x71E2, 0x7373, 0x736E, 0x736F, 0x7497, 0x74B2, 0x74AB, 0x7490, 0x74AA, 0x74AD, 0x74B1, 0x74A5, 0x74AF, 0x7510, 0x7511, 0x7512, 0x750F, 0x7584, 0x7643, 0x7648, 0x7649, 0x7647, 0x76A4, 0x76E9, 0x77B5, 0x77AB, 0x77B2, 0x77B7, 0x77B6, 0x77B4, 0x77B1, 0x77A8, 0x77F0, 0x78F3, 0x78FD, 0x7902, 0x78FB, 0x78FC, 0x78F2, 0x7905, 0x78F9, 0x78FE, 0x7904, 0x79AB, 0x79A8, 0x7A5C, 0x7A5B, 0x7A56, 0x7A58, 0x7A54, 0x7A5A, 0x7ABE, 0x7AC0, 0x7AC1, 0x7C05, 0x7C0F, 0x7BF2, 0x7C00, 0x7BFF, 0x7BFB, 0x7C0E, 0x7BF4, 0x7C0B, 0x7BF3, 0x7C02, 0x7C09, 0x7C03, 0x7C01, 0x7BF8, 0x7BFD, 0x7C06, 0x7BF0, 0x7BF1, 0x7C10, 0x7C0A, 0x7CE8, 0x7E2D, 0x7E3C, 0x7E42, 0x7E33, 0x9848, 0x7E38, 0x7E2A, 0x7E49, 0x7E40, 0x7E47, 0x7E29, 0x7E4C, 0x7E30, 0x7E3B, 0x7E36, 0x7E44, 0x7E3A, 0x7F45, 0x7F7F, 0x7F7E, 0x7F7D, 0x7FF4, 0x7FF2, 0x802C, 0x81BB, 0x81C4, 0x81CC, 0x81CA, 0x81C5, 0x81C7, 0x81BC, 0x81E9, 0x825B, 0x825A, 0x825C, 0x8583, 0x8580, 0x858F, 0x85A7, 0x8595, 0x85A0, 0x858B, 0x85A3, 0x857B, 0x85A4, 0x859A, 0x859E, + 0x8577, 0x857C, 0x8589, 0x85A1, 0x857A, 0x8578, 0x8557, 0x858E, 0x8596, 0x8586, 0x858D, 0x8599, 0x859D, 0x8581, 0x85A2, 0x8582, 0x8588, 0x8585, 0x8579, 0x8576, 0x8598, 0x8590, 0x859F, 0x8668, 0x87BE, 0x87AA, 0x87AD, 0x87C5, 0x87B0, 0x87AC, 0x87B9, 0x87B5, 0x87BC, 0x87AE, 0x87C9, 0x87C3, 0x87C2, 0x87CC, 0x87B7, 0x87AF, 0x87C4, 0x87CA, 0x87B4, 0x87B6, 0x87BF, 0x87B8, 0x87BD, 0x87DE, 0x87B2, 0x8935, 0x8933, 0x893C, 0x893E, 0x8941, 0x8952, 0x8937, 0x8942, 0x89AD, 0x89AF, 0x89AE, 0x89F2, 0x89F3, 0x8B1E, 0x8B18, 0x8B16, 0x8B11, 0x8B05, 0x8B0B, 0x8B22, 0x8B0F, 0x8B12, 0x8B15, 0x8B07, 0x8B0D, 0x8B08, 0x8B06, 0x8B1C, 0x8B13, 0x8B1A, 0x8C4F, 0x8C70, 0x8C72, 0x8C71, 0x8C6F, 0x8C95, 0x8C94, 0x8CF9, 0x8D6F, 0x8E4E, 0x8E4D, 0x8E53, 0x8E50, 0x8E4C, 0x8E47, 0x8F43, 0x8F40, 0x9085, 0x907E, 0x9138, 0x919A, 0x91A2, 0x919B, 0x9199, 0x919F, 0x91A1, 0x919D, 0x91A0, 0x93A1, 0x9383, 0x93AF, 0x9364, 0x9356, 0x9347, 0x937C, 0x9358, 0x935C, 0x9376, 0x9349, 0x9350, 0x9351, 0x9360, 0x936D, 0x938F, 0x934C, 0x936A, 0x9379, 0x9357, 0x9355, 0x9352, 0x934F, 0x9371, 0x9377, 0x937B, 0x9361, 0x935E, 0x9363, 0x9367, 0x9380, 0x934E, 0x9359, 0x95C7, 0x95C0, 0x95C9, 0x95C3, 0x95C5, 0x95B7, 0x96AE, 0x96B0, 0x96AC, 0x9720, 0x971F, 0x9718, 0x971D, 0x9719, 0x979A, 0x97A1, 0x979C, + 0x979E, 0x979D, 0x97D5, 0x97D4, 0x97F1, 0x9841, 0x9844, 0x984A, 0x9849, 0x9845, 0x9843, 0x9925, 0x992B, 0x992C, 0x992A, 0x9933, 0x9932, 0x992F, 0x992D, 0x9931, 0x9930, 0x9998, 0x99A3, 0x99A1, 0x9A02, 0x99FA, 0x99F4, 0x99F7, 0x99F9, 0x99F8, 0x99F6, 0x99FB, 0x99FD, 0x99FE, 0x99FC, 0x9A03, 0x9ABE, 0x9AFE, 0x9AFD, 0x9B01, 0x9AFC, 0x9B48, 0x9B9A, 0x9BA8, 0x9B9E, 0x9B9B, 0x9BA6, 0x9BA1, 0x9BA5, 0x9BA4, 0x9B86, 0x9BA2, 0x9BA0, 0x9BAF, 0x9D33, 0x9D41, 0x9D67, 0x9D36, 0x9D2E, 0x9D2F, 0x9D31, 0x9D38, 0x9D30, 0x9D45, 0x9D42, 0x9D43, 0x9D3E, 0x9D37, 0x9D40, 0x9D3D, 0x7FF5, 0x9D2D, 0x9E8A, 0x9E89, 0x9E8D, 0x9EB0, 0x9EC8, 0x9EDA, 0x9EFB, 0x9EFF, 0x9F24, 0x9F23, 0x9F22, 0x9F54, 0x9FA0, 0x5131, 0x512D, 0x512E, 0x5698, 0x569C, 0x5697, 0x569A, 0x569D, 0x5699, 0x5970, 0x5B3C, 0x5C69, 0x5C6A, 0x5DC0, 0x5E6D, 0x5E6E, 0x61D8, 0x61DF, 0x61ED, 0x61EE, 0x61F1, 0x61EA, 0x61F0, 0x61EB, 0x61D6, 0x61E9, 0x64FF, 0x6504, 0x64FD, 0x64F8, 0x6501, 0x6503, 0x64FC, 0x6594, 0x65DB, 0x66DA, 0x66DB, 0x66D8, 0x6AC5, 0x6AB9, 0x6ABD, 0x6AE1, 0x6AC6, 0x6ABA, 0x6AB6, 0x6AB7, 0x6AC7, 0x6AB4, 0x6AAD, 0x6B5E, 0x6BC9, 0x6C0B, 0x7007, 0x700C, 0x700D, 0x7001, 0x7005, 0x7014, 0x700E, 0x6FFF, 0x7000, 0x6FFB, 0x7026, 0x6FFC, 0x6FF7, 0x700A, 0x7201, 0x71FF, 0x71F9, 0x7203, 0x71FD, 0x7376, + 0x74B8, 0x74C0, 0x74B5, 0x74C1, 0x74BE, 0x74B6, 0x74BB, 0x74C2, 0x7514, 0x7513, 0x765C, 0x7664, 0x7659, 0x7650, 0x7653, 0x7657, 0x765A, 0x76A6, 0x76BD, 0x76EC, 0x77C2, 0x77BA, 0x78FF, 0x790C, 0x7913, 0x7914, 0x7909, 0x7910, 0x7912, 0x7911, 0x79AD, 0x79AC, 0x7A5F, 0x7C1C, 0x7C29, 0x7C19, 0x7C20, 0x7C1F, 0x7C2D, 0x7C1D, 0x7C26, 0x7C28, 0x7C22, 0x7C25, 0x7C30, 0x7E5C, 0x7E50, 0x7E56, 0x7E63, 0x7E58, 0x7E62, 0x7E5F, 0x7E51, 0x7E60, 0x7E57, 0x7E53, 0x7FB5, 0x7FB3, 0x7FF7, 0x7FF8, 0x8075, 0x81D1, 0x81D2, 0x81D0, 0x825F, 0x825E, 0x85B4, 0x85C6, 0x85C0, 0x85C3, 0x85C2, 0x85B3, 0x85B5, 0x85BD, 0x85C7, 0x85C4, 0x85BF, 0x85CB, 0x85CE, 0x85C8, 0x85C5, 0x85B1, 0x85B6, 0x85D2, 0x8624, 0x85B8, 0x85B7, 0x85BE, 0x8669, 0x87E7, 0x87E6, 0x87E2, 0x87DB, 0x87EB, 0x87EA, 0x87E5, 0x87DF, 0x87F3, 0x87E4, 0x87D4, 0x87DC, 0x87D3, 0x87ED, 0x87D8, 0x87E3, 0x87A4, 0x87D7, 0x87D9, 0x8801, 0x87F4, 0x87E8, 0x87DD, 0x8953, 0x894B, 0x894F, 0x894C, 0x8946, 0x8950, 0x8951, 0x8949, 0x8B2A, 0x8B27, 0x8B23, 0x8B33, 0x8B30, 0x8B35, 0x8B47, 0x8B2F, 0x8B3C, 0x8B3E, 0x8B31, 0x8B25, 0x8B37, 0x8B26, 0x8B36, 0x8B2E, 0x8B24, 0x8B3B, 0x8B3D, 0x8B3A, 0x8C42, 0x8C75, 0x8C99, 0x8C98, 0x8C97, 0x8CFE, 0x8D04, 0x8D02, 0x8D00, 0x8E5C, 0x8E62, 0x8E60, 0x8E57, 0x8E56, 0x8E5E, 0x8E65, 0x8E67, + 0x8E5B, 0x8E5A, 0x8E61, 0x8E5D, 0x8E69, 0x8E54, 0x8F46, 0x8F47, 0x8F48, 0x8F4B, 0x9128, 0x913A, 0x913B, 0x913E, 0x91A8, 0x91A5, 0x91A7, 0x91AF, 0x91AA, 0x93B5, 0x938C, 0x9392, 0x93B7, 0x939B, 0x939D, 0x9389, 0x93A7, 0x938E, 0x93AA, 0x939E, 0x93A6, 0x9395, 0x9388, 0x9399, 0x939F, 0x938D, 0x93B1, 0x9391, 0x93B2, 0x93A4, 0x93A8, 0x93B4, 0x93A3, 0x93A5, 0x95D2, 0x95D3, 0x95D1, 0x96B3, 0x96D7, 0x96DA, 0x5DC2, 0x96DF, 0x96D8, 0x96DD, 0x9723, 0x9722, 0x9725, 0x97AC, 0x97AE, 0x97A8, 0x97AB, 0x97A4, 0x97AA, 0x97A2, 0x97A5, 0x97D7, 0x97D9, 0x97D6, 0x97D8, 0x97FA, 0x9850, 0x9851, 0x9852, 0x98B8, 0x9941, 0x993C, 0x993A, 0x9A0F, 0x9A0B, 0x9A09, 0x9A0D, 0x9A04, 0x9A11, 0x9A0A, 0x9A05, 0x9A07, 0x9A06, 0x9AC0, 0x9ADC, 0x9B08, 0x9B04, 0x9B05, 0x9B29, 0x9B35, 0x9B4A, 0x9B4C, 0x9B4B, 0x9BC7, 0x9BC6, 0x9BC3, 0x9BBF, 0x9BC1, 0x9BB5, 0x9BB8, 0x9BD3, 0x9BB6, 0x9BC4, 0x9BB9, 0x9BBD, 0x9D5C, 0x9D53, 0x9D4F, 0x9D4A, 0x9D5B, 0x9D4B, 0x9D59, 0x9D56, 0x9D4C, 0x9D57, 0x9D52, 0x9D54, 0x9D5F, 0x9D58, 0x9D5A, 0x9E8E, 0x9E8C, 0x9EDF, 0x9F01, 0x9F00, 0x9F16, 0x9F25, 0x9F2B, 0x9F2A, 0x9F29, 0x9F28, 0x9F4C, 0x9F55, 0x5134, 0x5135, 0x5296, 0x52F7, 0x53B4, 0x56AB, 0x56AD, 0x56A6, 0x56A7, 0x56AA, 0x56AC, 0x58DA, 0x58DD, 0x58DB, 0x5912, 0x5B3D, 0x5B3E, 0x5B3F, 0x5DC3, 0x5E70, + 0x5FBF, 0x61FB, 0x6507, 0x6510, 0x650D, 0x6509, 0x650C, 0x650E, 0x6584, 0x65DE, 0x65DD, 0x66DE, 0x6AE7, 0x6AE0, 0x6ACC, 0x6AD1, 0x6AD9, 0x6ACB, 0x6ADF, 0x6ADC, 0x6AD0, 0x6AEB, 0x6ACF, 0x6ACD, 0x6ADE, 0x6B60, 0x6BB0, 0x6C0C, 0x7019, 0x7027, 0x7020, 0x7016, 0x702B, 0x7021, 0x7022, 0x7023, 0x7029, 0x7017, 0x7024, 0x701C, 0x702A, 0x720C, 0x720A, 0x7207, 0x7202, 0x7205, 0x72A5, 0x72A6, 0x72A4, 0x72A3, 0x72A1, 0x74CB, 0x74C5, 0x74B7, 0x74C3, 0x7516, 0x7660, 0x77C9, 0x77CA, 0x77C4, 0x77F1, 0x791D, 0x791B, 0x7921, 0x791C, 0x7917, 0x791E, 0x79B0, 0x7A67, 0x7A68, 0x7C33, 0x7C3C, 0x7C39, 0x7C2C, 0x7C3B, 0x7CEC, 0x7CEA, 0x7E76, 0x7E75, 0x7E78, 0x7E70, 0x7E77, 0x7E6F, 0x7E7A, 0x7E72, 0x7E74, 0x7E68, 0x7F4B, 0x7F4A, 0x7F83, 0x7F86, 0x7FB7, 0x7FFD, 0x7FFE, 0x8078, 0x81D7, 0x81D5, 0x8264, 0x8261, 0x8263, 0x85EB, 0x85F1, 0x85ED, 0x85D9, 0x85E1, 0x85E8, 0x85DA, 0x85D7, 0x85EC, 0x85F2, 0x85F8, 0x85D8, 0x85DF, 0x85E3, 0x85DC, 0x85D1, 0x85F0, 0x85E6, 0x85EF, 0x85DE, 0x85E2, 0x8800, 0x87FA, 0x8803, 0x87F6, 0x87F7, 0x8809, 0x880C, 0x880B, 0x8806, 0x87FC, 0x8808, 0x87FF, 0x880A, 0x8802, 0x8962, 0x895A, 0x895B, 0x8957, 0x8961, 0x895C, 0x8958, 0x895D, 0x8959, 0x8988, 0x89B7, 0x89B6, 0x89F6, 0x8B50, 0x8B48, 0x8B4A, 0x8B40, 0x8B53, 0x8B56, 0x8B54, 0x8B4B, 0x8B55, + 0x8B51, 0x8B42, 0x8B52, 0x8B57, 0x8C43, 0x8C77, 0x8C76, 0x8C9A, 0x8D06, 0x8D07, 0x8D09, 0x8DAC, 0x8DAA, 0x8DAD, 0x8DAB, 0x8E6D, 0x8E78, 0x8E73, 0x8E6A, 0x8E6F, 0x8E7B, 0x8EC2, 0x8F52, 0x8F51, 0x8F4F, 0x8F50, 0x8F53, 0x8FB4, 0x9140, 0x913F, 0x91B0, 0x91AD, 0x93DE, 0x93C7, 0x93CF, 0x93C2, 0x93DA, 0x93D0, 0x93F9, 0x93EC, 0x93CC, 0x93D9, 0x93A9, 0x93E6, 0x93CA, 0x93D4, 0x93EE, 0x93E3, 0x93D5, 0x93C4, 0x93CE, 0x93C0, 0x93D2, 0x93E7, 0x957D, 0x95DA, 0x95DB, 0x96E1, 0x9729, 0x972B, 0x972C, 0x9728, 0x9726, 0x97B3, 0x97B7, 0x97B6, 0x97DD, 0x97DE, 0x97DF, 0x985C, 0x9859, 0x985D, 0x9857, 0x98BF, 0x98BD, 0x98BB, 0x98BE, 0x9948, 0x9947, 0x9943, 0x99A6, 0x99A7, 0x9A1A, 0x9A15, 0x9A25, 0x9A1D, 0x9A24, 0x9A1B, 0x9A22, 0x9A20, 0x9A27, 0x9A23, 0x9A1E, 0x9A1C, 0x9A14, 0x9AC2, 0x9B0B, 0x9B0A, 0x9B0E, 0x9B0C, 0x9B37, 0x9BEA, 0x9BEB, 0x9BE0, 0x9BDE, 0x9BE4, 0x9BE6, 0x9BE2, 0x9BF0, 0x9BD4, 0x9BD7, 0x9BEC, 0x9BDC, 0x9BD9, 0x9BE5, 0x9BD5, 0x9BE1, 0x9BDA, 0x9D77, 0x9D81, 0x9D8A, 0x9D84, 0x9D88, 0x9D71, 0x9D80, 0x9D78, 0x9D86, 0x9D8B, 0x9D8C, 0x9D7D, 0x9D6B, 0x9D74, 0x9D75, 0x9D70, 0x9D69, 0x9D85, 0x9D73, 0x9D7B, 0x9D82, 0x9D6F, 0x9D79, 0x9D7F, 0x9D87, 0x9D68, 0x9E94, 0x9E91, 0x9EC0, 0x9EFC, 0x9F2D, 0x9F40, 0x9F41, 0x9F4D, 0x9F56, 0x9F57, 0x9F58, 0x5337, 0x56B2, + 0x56B5, 0x56B3, 0x58E3, 0x5B45, 0x5DC6, 0x5DC7, 0x5EEE, 0x5EEF, 0x5FC0, 0x5FC1, 0x61F9, 0x6517, 0x6516, 0x6515, 0x6513, 0x65DF, 0x66E8, 0x66E3, 0x66E4, 0x6AF3, 0x6AF0, 0x6AEA, 0x6AE8, 0x6AF9, 0x6AF1, 0x6AEE, 0x6AEF, 0x703C, 0x7035, 0x702F, 0x7037, 0x7034, 0x7031, 0x7042, 0x7038, 0x703F, 0x703A, 0x7039, 0x7040, 0x703B, 0x7033, 0x7041, 0x7213, 0x7214, 0x72A8, 0x737D, 0x737C, 0x74BA, 0x76AB, 0x76AA, 0x76BE, 0x76ED, 0x77CC, 0x77CE, 0x77CF, 0x77CD, 0x77F2, 0x7925, 0x7923, 0x7927, 0x7928, 0x7924, 0x7929, 0x79B2, 0x7A6E, 0x7A6C, 0x7A6D, 0x7AF7, 0x7C49, 0x7C48, 0x7C4A, 0x7C47, 0x7C45, 0x7CEE, 0x7E7B, 0x7E7E, 0x7E81, 0x7E80, 0x7FBA, 0x7FFF, 0x8079, 0x81DB, 0x81D9, 0x820B, 0x8268, 0x8269, 0x8622, 0x85FF, 0x8601, 0x85FE, 0x861B, 0x8600, 0x85F6, 0x8604, 0x8609, 0x8605, 0x860C, 0x85FD, 0x8819, 0x8810, 0x8811, 0x8817, 0x8813, 0x8816, 0x8963, 0x8966, 0x89B9, 0x89F7, 0x8B60, 0x8B6A, 0x8B5D, 0x8B68, 0x8B63, 0x8B65, 0x8B67, 0x8B6D, 0x8DAE, 0x8E86, 0x8E88, 0x8E84, 0x8F59, 0x8F56, 0x8F57, 0x8F55, 0x8F58, 0x8F5A, 0x908D, 0x9143, 0x9141, 0x91B7, 0x91B5, 0x91B2, 0x91B3, 0x940B, 0x9413, 0x93FB, 0x9420, 0x940F, 0x9414, 0x93FE, 0x9415, 0x9410, 0x9428, 0x9419, 0x940D, 0x93F5, 0x9400, 0x93F7, 0x9407, 0x940E, 0x9416, 0x9412, 0x93FA, 0x9409, 0x93F8, 0x940A, 0x93FF, + 0x93FC, 0x940C, 0x93F6, 0x9411, 0x9406, 0x95DE, 0x95E0, 0x95DF, 0x972E, 0x972F, 0x97B9, 0x97BB, 0x97FD, 0x97FE, 0x9860, 0x9862, 0x9863, 0x985F, 0x98C1, 0x98C2, 0x9950, 0x994E, 0x9959, 0x994C, 0x994B, 0x9953, 0x9A32, 0x9A34, 0x9A31, 0x9A2C, 0x9A2A, 0x9A36, 0x9A29, 0x9A2E, 0x9A38, 0x9A2D, 0x9AC7, 0x9ACA, 0x9AC6, 0x9B10, 0x9B12, 0x9B11, 0x9C0B, 0x9C08, 0x9BF7, 0x9C05, 0x9C12, 0x9BF8, 0x9C40, 0x9C07, 0x9C0E, 0x9C06, 0x9C17, 0x9C14, 0x9C09, 0x9D9F, 0x9D99, 0x9DA4, 0x9D9D, 0x9D92, 0x9D98, 0x9D90, 0x9D9B, 0x9DA0, 0x9D94, 0x9D9C, 0x9DAA, 0x9D97, 0x9DA1, 0x9D9A, 0x9DA2, 0x9DA8, 0x9D9E, 0x9DA3, 0x9DBF, 0x9DA9, 0x9D96, 0x9DA6, 0x9DA7, 0x9E99, 0x9E9B, 0x9E9A, 0x9EE5, 0x9EE4, 0x9EE7, 0x9EE6, 0x9F30, 0x9F2E, 0x9F5B, 0x9F60, 0x9F5E, 0x9F5D, 0x9F59, 0x9F91, 0x513A, 0x5139, 0x5298, 0x5297, 0x56C3, 0x56BD, 0x56BE, 0x5B48, 0x5B47, 0x5DCB, 0x5DCF, 0x5EF1, 0x61FD, 0x651B, 0x6B02, 0x6AFC, 0x6B03, 0x6AF8, 0x6B00, 0x7043, 0x7044, 0x704A, 0x7048, 0x7049, 0x7045, 0x7046, 0x721D, 0x721A, 0x7219, 0x737E, 0x7517, 0x766A, 0x77D0, 0x792D, 0x7931, 0x792F, 0x7C54, 0x7C53, 0x7CF2, 0x7E8A, 0x7E87, 0x7E88, 0x7E8B, 0x7E86, 0x7E8D, 0x7F4D, 0x7FBB, 0x8030, 0x81DD, 0x8618, 0x862A, 0x8626, 0x861F, 0x8623, 0x861C, 0x8619, 0x8627, 0x862E, 0x8621, 0x8620, 0x8629, 0x861E, 0x8625, + 0x8829, 0x881D, 0x881B, 0x8820, 0x8824, 0x881C, 0x882B, 0x884A, 0x896D, 0x8969, 0x896E, 0x896B, 0x89FA, 0x8B79, 0x8B78, 0x8B45, 0x8B7A, 0x8B7B, 0x8D10, 0x8D14, 0x8DAF, 0x8E8E, 0x8E8C, 0x8F5E, 0x8F5B, 0x8F5D, 0x9146, 0x9144, 0x9145, 0x91B9, 0x943F, 0x943B, 0x9436, 0x9429, 0x943D, 0x943C, 0x9430, 0x9439, 0x942A, 0x9437, 0x942C, 0x9440, 0x9431, 0x95E5, 0x95E4, 0x95E3, 0x9735, 0x973A, 0x97BF, 0x97E1, 0x9864, 0x98C9, 0x98C6, 0x98C0, 0x9958, 0x9956, 0x9A39, 0x9A3D, 0x9A46, 0x9A44, 0x9A42, 0x9A41, 0x9A3A, 0x9A3F, 0x9ACD, 0x9B15, 0x9B17, 0x9B18, 0x9B16, 0x9B3A, 0x9B52, 0x9C2B, 0x9C1D, 0x9C1C, 0x9C2C, 0x9C23, 0x9C28, 0x9C29, 0x9C24, 0x9C21, 0x9DB7, 0x9DB6, 0x9DBC, 0x9DC1, 0x9DC7, 0x9DCA, 0x9DCF, 0x9DBE, 0x9DC5, 0x9DC3, 0x9DBB, 0x9DB5, 0x9DCE, 0x9DB9, 0x9DBA, 0x9DAC, 0x9DC8, 0x9DB1, 0x9DAD, 0x9DCC, 0x9DB3, 0x9DCD, 0x9DB2, 0x9E7A, 0x9E9C, 0x9EEB, 0x9EEE, 0x9EED, 0x9F1B, 0x9F18, 0x9F1A, 0x9F31, 0x9F4E, 0x9F65, 0x9F64, 0x9F92, 0x4EB9, 0x56C6, 0x56C5, 0x56CB, 0x5971, 0x5B4B, 0x5B4C, 0x5DD5, 0x5DD1, 0x5EF2, 0x6521, 0x6520, 0x6526, 0x6522, 0x6B0B, 0x6B08, 0x6B09, 0x6C0D, 0x7055, 0x7056, 0x7057, 0x7052, 0x721E, 0x721F, 0x72A9, 0x737F, 0x74D8, 0x74D5, 0x74D9, 0x74D7, 0x766D, 0x76AD, 0x7935, 0x79B4, 0x7A70, 0x7A71, 0x7C57, 0x7C5C, 0x7C59, 0x7C5B, 0x7C5A, + 0x7CF4, 0x7CF1, 0x7E91, 0x7F4F, 0x7F87, 0x81DE, 0x826B, 0x8634, 0x8635, 0x8633, 0x862C, 0x8632, 0x8636, 0x882C, 0x8828, 0x8826, 0x882A, 0x8825, 0x8971, 0x89BF, 0x89BE, 0x89FB, 0x8B7E, 0x8B84, 0x8B82, 0x8B86, 0x8B85, 0x8B7F, 0x8D15, 0x8E95, 0x8E94, 0x8E9A, 0x8E92, 0x8E90, 0x8E96, 0x8E97, 0x8F60, 0x8F62, 0x9147, 0x944C, 0x9450, 0x944A, 0x944B, 0x944F, 0x9447, 0x9445, 0x9448, 0x9449, 0x9446, 0x973F, 0x97E3, 0x986A, 0x9869, 0x98CB, 0x9954, 0x995B, 0x9A4E, 0x9A53, 0x9A54, 0x9A4C, 0x9A4F, 0x9A48, 0x9A4A, 0x9A49, 0x9A52, 0x9A50, 0x9AD0, 0x9B19, 0x9B2B, 0x9B3B, 0x9B56, 0x9B55, 0x9C46, 0x9C48, 0x9C3F, 0x9C44, 0x9C39, 0x9C33, 0x9C41, 0x9C3C, 0x9C37, 0x9C34, 0x9C32, 0x9C3D, 0x9C36, 0x9DDB, 0x9DD2, 0x9DDE, 0x9DDA, 0x9DCB, 0x9DD0, 0x9DDC, 0x9DD1, 0x9DDF, 0x9DE9, 0x9DD9, 0x9DD8, 0x9DD6, 0x9DF5, 0x9DD5, 0x9DDD, 0x9EB6, 0x9EF0, 0x9F35, 0x9F33, 0x9F32, 0x9F42, 0x9F6B, 0x9F95, 0x9FA2, 0x513D, 0x5299, 0x58E8, 0x58E7, 0x5972, 0x5B4D, 0x5DD8, 0x882F, 0x5F4F, 0x6201, 0x6203, 0x6204, 0x6529, 0x6525, 0x6596, 0x66EB, 0x6B11, 0x6B12, 0x6B0F, 0x6BCA, 0x705B, 0x705A, 0x7222, 0x7382, 0x7381, 0x7383, 0x7670, 0x77D4, 0x7C67, 0x7C66, 0x7E95, 0x826C, 0x863A, 0x8640, 0x8639, 0x863C, 0x8631, 0x863B, 0x863E, 0x8830, 0x8832, 0x882E, 0x8833, 0x8976, 0x8974, 0x8973, 0x89FE, + 0x8B8C, 0x8B8E, 0x8B8B, 0x8B88, 0x8C45, 0x8D19, 0x8E98, 0x8F64, 0x8F63, 0x91BC, 0x9462, 0x9455, 0x945D, 0x9457, 0x945E, 0x97C4, 0x97C5, 0x9800, 0x9A56, 0x9A59, 0x9B1E, 0x9B1F, 0x9B20, 0x9C52, 0x9C58, 0x9C50, 0x9C4A, 0x9C4D, 0x9C4B, 0x9C55, 0x9C59, 0x9C4C, 0x9C4E, 0x9DFB, 0x9DF7, 0x9DEF, 0x9DE3, 0x9DEB, 0x9DF8, 0x9DE4, 0x9DF6, 0x9DE1, 0x9DEE, 0x9DE6, 0x9DF2, 0x9DF0, 0x9DE2, 0x9DEC, 0x9DF4, 0x9DF3, 0x9DE8, 0x9DED, 0x9EC2, 0x9ED0, 0x9EF2, 0x9EF3, 0x9F06, 0x9F1C, 0x9F38, 0x9F37, 0x9F36, 0x9F43, 0x9F4F, 0x9F71, 0x9F70, 0x9F6E, 0x9F6F, 0x56D3, 0x56CD, 0x5B4E, 0x5C6D, 0x652D, 0x66ED, 0x66EE, 0x6B13, 0x705F, 0x7061, 0x705D, 0x7060, 0x7223, 0x74DB, 0x74E5, 0x77D5, 0x7938, 0x79B7, 0x79B6, 0x7C6A, 0x7E97, 0x7F89, 0x826D, 0x8643, 0x8838, 0x8837, 0x8835, 0x884B, 0x8B94, 0x8B95, 0x8E9E, 0x8E9F, 0x8EA0, 0x8E9D, 0x91BE, 0x91BD, 0x91C2, 0x946B, 0x9468, 0x9469, 0x96E5, 0x9746, 0x9743, 0x9747, 0x97C7, 0x97E5, 0x9A5E, 0x9AD5, 0x9B59, 0x9C63, 0x9C67, 0x9C66, 0x9C62, 0x9C5E, 0x9C60, 0x9E02, 0x9DFE, 0x9E07, 0x9E03, 0x9E06, 0x9E05, 0x9E00, 0x9E01, 0x9E09, 0x9DFF, 0x9DFD, 0x9E04, 0x9EA0, 0x9F1E, 0x9F46, 0x9F74, 0x9F75, 0x9F76, 0x56D4, 0x652E, 0x65B8, 0x6B18, 0x6B19, 0x6B17, 0x6B1A, 0x7062, 0x7226, 0x72AA, 0x77D8, 0x77D9, 0x7939, 0x7C69, 0x7C6B, 0x7CF6, 0x7E9A, + 0x7E98, 0x7E9B, 0x7E99, 0x81E0, 0x81E1, 0x8646, 0x8647, 0x8648, 0x8979, 0x897A, 0x897C, 0x897B, 0x89FF, 0x8B98, 0x8B99, 0x8EA5, 0x8EA4, 0x8EA3, 0x946E, 0x946D, 0x946F, 0x9471, 0x9473, 0x9749, 0x9872, 0x995F, 0x9C68, 0x9C6E, 0x9C6D, 0x9E0B, 0x9E0D, 0x9E10, 0x9E0F, 0x9E12, 0x9E11, 0x9EA1, 0x9EF5, 0x9F09, 0x9F47, 0x9F78, 0x9F7B, 0x9F7A, 0x9F79, 0x571E, 0x7066, 0x7C6F, 0x883C, 0x8DB2, 0x8EA6, 0x91C3, 0x9474, 0x9478, 0x9476, 0x9475, 0x9A60, 0x9C74, 0x9C73, 0x9C71, 0x9C75, 0x9E14, 0x9E13, 0x9EF6, 0x9F0A, 0x9FA4, 0x7068, 0x7065, 0x7CF7, 0x866A, 0x883E, 0x883D, 0x883F, 0x8B9E, 0x8C9C, 0x8EA9, 0x8EC9, 0x974B, 0x9873, 0x9874, 0x98CC, 0x9961, 0x99AB, 0x9A64, 0x9A66, 0x9A67, 0x9B24, 0x9E15, 0x9E17, 0x9F48, 0x6207, 0x6B1E, 0x7227, 0x864C, 0x8EA8, 0x9482, 0x9480, 0x9481, 0x9A69, 0x9A68, 0x9B2E, 0x9E19, 0x7229, 0x864B, 0x8B9F, 0x9483, 0x9C79, 0x9EB7, 0x7675, 0x9A6B, 0x9C7A, 0x9E1D, 0x7069, 0x706A, 0x9EA4, 0x9F7E, 0x9F49, 0x9F98, 0x7881, 0x92B9, 0x88CF, 0x58BB, 0x6052, 0x7CA7, 0x5AFA, 0x2554, 0x2566, 0x2557, 0x2560, 0x256C, 0x2563, 0x255A, 0x2569, 0x255D, 0x2552, 0x2564, 0x2555, 0x255E, 0x256A, 0x2561, 0x2558, 0x2567, 0x255B, 0x2553, 0x2565, 0x2556, 0x255F, 0x256B, 0x2562, 0x2559, 0x2568, 0x255C, 0x2551, 0x2550, 0x256D, 0x256E, 0x2570, 0x256F, 0x2593, +}; + +} diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 215e941d22..7f9956a54c 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -1,7 +1,16 @@ #include #include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "JisX0208.hpp" +#include "JisX0212.hpp" +#include "Cp949.hpp" +#include "Cp950.hpp" +#include "SingleByte.hpp" namespace { @@ -13,9 +22,10 @@ constexpr int CES_ERROR_UNASSIGNED_CODE = static_cast(0x805C0020); constexpr int CES_ERROR_INVALID_DST_BUFFER = static_cast(0x805C0030); constexpr int CES_ERROR_DST_BUFFER_END = static_cast(0x805C0031); -constexpr uint8_t PROFILES[2] = {0, 1}; -constexpr const uint8_t* CP1252_PROFILE = &PROFILES[0]; -constexpr const uint8_t* CP1251_PROFILE = &PROFILES[1]; +constexpr uint8_t PROFILES[3 + SingleByte::Count][8] = {{0}, {1}, {2}}; +constexpr const uint8_t* CP1252_PROFILE = PROFILES[0]; +constexpr const uint8_t* CP1251_PROFILE = PROFILES[1]; +constexpr const uint8_t* CP1250_PROFILE = PROFILES[2]; constexpr uint16_t CP1252_HIGH[32] = { 0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F, @@ -33,19 +43,49 @@ constexpr uint16_t CP1251_HIGH[128] = { 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F, }; +constexpr uint16_t CP1250_HIGH[128] = { + 0x20AC, 0x0081, 0x201A, 0x0083, 0x201E, 0x2026, 0x2020, 0x2021, 0x0088, 0x2030, 0x0160, 0x2039, 0x015A, 0x0164, 0x017D, 0x0179, + 0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x0098, 0x2122, 0x0161, 0x203A, 0x015B, 0x0165, 0x017E, 0x017A, + 0x00A0, 0x02C7, 0x02D8, 0x0141, 0x00A4, 0x0104, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x015E, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x017B, + 0x00B0, 0x00B1, 0x02DB, 0x0142, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x0105, 0x015F, 0x00BB, 0x013D, 0x02DD, 0x013E, 0x017C, + 0x0154, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x0139, 0x0106, 0x00C7, 0x010C, 0x00C9, 0x0118, 0x00CB, 0x011A, 0x00CD, 0x00CE, 0x010E, + 0x0110, 0x0143, 0x0147, 0x00D3, 0x00D4, 0x0150, 0x00D6, 0x00D7, 0x0158, 0x016E, 0x00DA, 0x0170, 0x00DC, 0x00DD, 0x0162, 0x00DF, + 0x0155, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x013A, 0x0107, 0x00E7, 0x010D, 0x00E9, 0x0119, 0x00EB, 0x011B, 0x00ED, 0x00EE, 0x010F, + 0x0111, 0x0144, 0x0148, 0x00F3, 0x00F4, 0x0151, 0x00F6, 0x00F7, 0x0159, 0x016F, 0x00FA, 0x0171, 0x00FC, 0x00FD, 0x0163, 0x02D9, +}; + +constexpr uint16_t CP932_IBM_HEAD[28] = { + 0x2170, 0x2171, 0x2172, 0x2173, 0x2174, 0x2175, 0x2176, 0x2177, 0x2178, 0x2179, + 0x2160, 0x2161, 0x2162, 0x2163, 0x2164, 0x2165, 0x2166, 0x2167, 0x2168, 0x2169, + 0xFFE2, 0xFFE4, 0xFF07, 0xFF02, 0x3231, 0x2116, 0x2121, 0x2235, +}; + +const uint8_t* singleByteProfile(SingleByte::Table table) { + return PROFILES[3 + table]; +} + +const uint16_t* sbcHighTable(const void* profile) { + if (profile == CP1251_PROFILE) return CP1251_HIGH; + if (profile == CP1250_PROFILE) return CP1250_HIGH; + for (std::size_t table = 0; table < SingleByte::Count; ++table) { + if (profile == singleByteProfile(static_cast(table))) return SingleByte::HIGH[table]; + } + return nullptr; +} + uint32_t sbcToUnicode(const uint8_t* profile, uint8_t sbc) { - if (profile == CP1251_PROFILE) return sbc >= 0x80 ? CP1251_HIGH[sbc - 0x80] : sbc; + if (const uint16_t* high = sbcHighTable(profile)) return sbc >= 0x80 ? high[sbc - 0x80] : sbc; return sbc >= 0x80 && sbc < 0xA0 ? CP1252_HIGH[sbc - 0x80] : sbc; } bool unicodeToSbc(const uint8_t* profile, uint32_t code, uint8_t& sbc) { - if (profile == CP1251_PROFILE) { + if (const uint16_t* high = sbcHighTable(profile)) { if (code < 0x80) { sbc = static_cast(code); return true; } for (uint32_t i = 0; i < 128; ++i) { - if (CP1251_HIGH[i] == code) { + if (high[i] == code) { sbc = static_cast(0x80 + i); return true; } @@ -103,6 +143,391 @@ uint32_t encodeUtf8(uint32_t code, uint8_t* utf8) { return 3; } +constexpr uint8_t EUC_JP_TAG[8] = {'E', 'U', 'C', 'J', 'P', '9', '3', '2'}; +constexpr uint8_t SJIS_TAG[8] = {'S', 'J', 'I', 'S', '1', '9', '9', '7'}; + +bool isEucJp(const void* profile) { + return profile != nullptr && std::memcmp(profile, EUC_JP_TAG, sizeof(EUC_JP_TAG)) == 0; +} + +bool isSjis(const void* profile) { + return profile != nullptr && std::memcmp(profile, SJIS_TAG, sizeof(SJIS_TAG)) == 0; +} + +bool isSbc(const void* profile) { + return profile == CP1252_PROFILE || sbcHighTable(profile) != nullptr; +} + +const void* contextProfile(const void* context) { + const void* profile = nullptr; + std::memcpy(&profile, context, sizeof(profile)); + return profile; +} + +constexpr uint8_t EUC_KR_TAG[8] = {'E', 'U', 'C', 'K', 'R', '9', '4', '9'}; +constexpr uint8_t UHC_TAG[8] = {'U', 'H', 'C', 'C', 'P', '9', '4', '9'}; +constexpr uint8_t BIG5_TAG[8] = {'B', 'I', 'G', '5', 'C', '9', '5', '0'}; +constexpr uint16_t BIG5_LAST_CELL[6] = {0x2550, 0x255E, 0x2561, 0x256A, 0x5341, 0x5345}; + +bool isEucKr(const void* profile) { + return profile != nullptr && std::memcmp(profile, EUC_KR_TAG, sizeof(EUC_KR_TAG)) == 0; +} + +bool isUhc(const void* profile) { + return profile != nullptr && std::memcmp(profile, UHC_TAG, sizeof(UHC_TAG)) == 0; +} + +bool isBig5(const void* profile) { + return profile != nullptr && std::memcmp(profile, BIG5_TAG, sizeof(BIG5_TAG)) == 0; +} + +bool isMbcs(const void* profile) { + return isEucJp(profile) || isSjis(profile) || isSbc(profile) || isEucKr(profile) || isUhc(profile) || isBig5(profile); +} + +int decodeCp949(bool unified, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_t& code) { + const uint8_t lead = mbcs[0]; + if (lead < 0x80) { + length = 1; + code = lead; + return 0; + } + if (lead < (unified ? 0x81 : 0xA1) || lead == 0xFF) return CES_ERROR_INVALID_ENCODE; + if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; + const uint8_t trail = mbcs[1]; + const bool extended = unified && ((trail >= 0x41 && trail <= 0x5A) || (trail >= 0x61 && trail <= 0x7A) || (trail >= 0x81 && trail <= 0xA0)); + if (!extended && (trail < 0xA1 || trail == 0xFF)) return CES_ERROR_INVALID_ENCODE; + length = 2; + code = Cp949::TO_UCS2[(lead - 0x81) * 190 + (trail - 0x41)]; + return code != 0 ? 0 : CES_ERROR_UNASSIGNED_CODE; +} + +int decodeCp950(const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_t& code) { + const uint8_t lead = mbcs[0]; + if (lead < 0x80) { + length = 1; + code = lead; + return 0; + } + if (lead == 0x80 || lead == 0xFF) return CES_ERROR_INVALID_ENCODE; + if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; + const uint8_t trail = mbcs[1]; + if (trail < 0x40 || (trail > 0x7E && trail < 0xA1) || trail == 0xFF) return CES_ERROR_INVALID_ENCODE; + if (lead < 0xA1 || lead > 0xF9) return CES_ERROR_UNASSIGNED_CODE; + length = 2; + code = Cp950::TO_UCS2[(lead - 0xA1) * 157 + (trail < 0xA1 ? trail - 0x40 : trail - 0xA1 + 63)]; + return code != 0 ? 0 : CES_ERROR_UNASSIGNED_CODE; +} + +struct DbcsCell { + uint16_t code; + uint16_t bytes; +}; + +bool byCode(const DbcsCell& left, const DbcsCell& right) { + return left.code < right.code; +} + +const std::vector& cp949Reverse() { + static const auto reverse = [] { + std::vector entries; + for (uint32_t cell = 0; cell < 126 * 190; ++cell) { + if (Cp949::TO_UCS2[cell] != 0) entries.push_back({Cp949::TO_UCS2[cell], static_cast((0x81 + cell / 190) << 8 | (0x41 + cell % 190))}); + } + std::sort(entries.begin(), entries.end(), byCode); + return entries; + }(); + return reverse; +} + +const std::vector& cp950Reverse() { + static const auto reverse = [] { + std::vector entries; + for (uint32_t cell = 0; cell < 89 * 157; ++cell) { + const uint32_t column = cell % 157; + const uint32_t trail = column < 63 ? 0x40 + column : 0xA1 + column - 63; + if (Cp950::TO_UCS2[cell] != 0) entries.push_back({Cp950::TO_UCS2[cell], static_cast((0xA1 + cell / 157) << 8 | trail)}); + } + std::stable_sort(entries.begin(), entries.end(), byCode); + std::vector unique; + for (const DbcsCell& entry : entries) { + if (unique.empty() || unique.back().code != entry.code) unique.push_back(entry); + else if (std::find(std::begin(BIG5_LAST_CELL), std::end(BIG5_LAST_CELL), entry.code) != std::end(BIG5_LAST_CELL)) unique.back() = entry; + } + return unique; + }(); + return reverse; +} + +int encodeDbcs(const std::vector& reverse, bool euc, uint32_t code, uint8_t* mbcs, uint32_t& length) { + if (code < 0x80) { + length = 1; + mbcs[0] = static_cast(code); + return 0; + } + const auto found = std::lower_bound(reverse.begin(), reverse.end(), code, [](const DbcsCell& entry, uint32_t value) { return entry.code < value; }); + if (found == reverse.end() || found->code != code) return CES_ERROR_UNASSIGNED_CODE; + if (euc && ((found->bytes >> 8) < 0xA1 || (found->bytes & 0xFF) < 0xA1)) return CES_ERROR_UNASSIGNED_CODE; + length = 2; + mbcs[0] = static_cast(found->bytes >> 8); + mbcs[1] = static_cast(found->bytes & 0xFF); + return 0; +} + +int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_t& code) { + if (isEucKr(profile) || isUhc(profile)) return decodeCp949(isUhc(profile), mbcs, mbcsMax, length, code); + if (isBig5(profile)) return decodeCp950(mbcs, mbcsMax, length, code); + const uint8_t lead = mbcs[0]; + if (isSbc(profile)) { + length = 1; + code = sbcToUnicode(static_cast(profile), lead); + return 0; + } + if (isSjis(profile)) { + if (lead < 0x80) { + length = 1; + code = lead; + return 0; + } + if (lead >= 0xA1 && lead <= 0xDF) { + length = 1; + code = 0xFF61 + (lead - 0xA1); + return 0; + } + if ((lead >= 0x81 && lead <= 0x9F) || (lead >= 0xE0 && lead <= 0xFC)) { + if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; + const uint8_t trail = mbcs[1]; + if (trail < 0x40 || trail == 0x7F || trail > 0xFC) return CES_ERROR_INVALID_ENCODE; + if (lead >= 0xFA) { + const uint32_t trailIdx = trail < 0x7F ? trail - 0x40 : trail - 0x41; + const uint32_t offset = (lead - 0xFA) * 188 + trailIdx; + if (offset <= 387) { + if (offset < 28) { + code = CP932_IBM_HEAD[offset]; + length = 2; + return 0; + } + const uint32_t kanjiIdx = offset - 28; + const int row = 89 + kanjiIdx / 94; + const int cell = 1 + kanjiIdx % 94; + code = JisX0208::TO_UCS2[(row - 1) * 94 + (cell - 1)]; + if (code == 0) return CES_ERROR_UNASSIGNED_CODE; + length = 2; + return 0; + } + return CES_ERROR_UNASSIGNED_CODE; + } + int row = (lead <= 0x9F) ? (lead - 0x81) * 2 + 1 : (lead - 0xE0) * 2 + 63; + int cell = 0; + if (trail >= 0x9F) { + row += 1; + cell = trail - 0x9E; + } else { + cell = (trail > 0x7E) ? trail - 0x40 : trail - 0x3F; + } + if (row < 1 || row > 94 || cell < 1 || cell > 94) return CES_ERROR_UNASSIGNED_CODE; + code = JisX0208::TO_UCS2[(row - 1) * 94 + (cell - 1)]; + if (code == 0) return CES_ERROR_UNASSIGNED_CODE; + length = 2; + return 0; + } + return CES_ERROR_INVALID_ENCODE; + } + if (lead < 0x80) { + length = 1; + code = lead; + return 0; + } + if (lead == 0x8E) { + if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; + if (mbcs[1] < 0xA1 || mbcs[1] > 0xDF) return CES_ERROR_INVALID_ENCODE; + length = 2; + code = 0xFF61 + (mbcs[1] - 0xA1); + return 0; + } + if (lead >= 0xA1 && lead <= 0xFE) { + if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; + if (mbcs[1] < 0xA1 || mbcs[1] > 0xFE) return CES_ERROR_INVALID_ENCODE; + length = 2; + code = JisX0208::TO_UCS2[(lead - 0xA1) * 94 + (mbcs[1] - 0xA1)]; + return code != 0 ? 0 : CES_ERROR_UNASSIGNED_CODE; + } + if (lead == 0x8F) { + if (mbcsMax < 3) return CES_ERROR_SRC_BUFFER_END; + if (mbcs[1] < 0xA1 || mbcs[1] > 0xFE || mbcs[2] < 0xA1 || mbcs[2] > 0xFE) return CES_ERROR_INVALID_ENCODE; + length = 3; + code = JisX0212::TO_UCS2[(mbcs[1] - 0xA1) * 94 + (mbcs[2] - 0xA1)]; + return code != 0 ? 0 : CES_ERROR_UNASSIGNED_CODE; + } + return CES_ERROR_INVALID_ENCODE; +} + +struct JisCell { + uint16_t code; + uint16_t cell; + bool supplementary; +}; + +const std::vector& jisReverse() { + static const auto reverse = [] { + std::vector entries; + entries.reserve(2 * 94 * 94); + for (uint32_t cell = 0; cell < 94 * 94; ++cell) { + if (JisX0208::TO_UCS2[cell] != 0) entries.push_back({JisX0208::TO_UCS2[cell], static_cast(cell), false}); + } + for (uint32_t cell = 0; cell < 94 * 94; ++cell) { + if (JisX0212::TO_UCS2[cell] != 0) entries.push_back({JisX0212::TO_UCS2[cell], static_cast(cell), true}); + } + std::stable_sort(entries.begin(), entries.end(), [](const JisCell& left, const JisCell& right) { return left.code < right.code; }); + entries.erase(std::unique(entries.begin(), entries.end(), [](const JisCell& left, const JisCell& right) { return left.code == right.code; }), entries.end()); + return entries; + }(); + return reverse; +} + +int encodeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& length) { + if (isEucKr(profile) || isUhc(profile)) return encodeDbcs(cp949Reverse(), isEucKr(profile), code, mbcs, length); + if (isBig5(profile)) return encodeDbcs(cp950Reverse(), false, code, mbcs, length); + if (isSbc(profile)) { + length = 1; + return unicodeToSbc(static_cast(profile), code, mbcs[0]) ? 0 : CES_ERROR_UNASSIGNED_CODE; + } + if (isSjis(profile)) { + if (code < 0x80) { + length = 1; + mbcs[0] = static_cast(code); + return 0; + } + if (code >= 0xFF61 && code <= 0xFF9F) { + length = 1; + mbcs[0] = static_cast(0xA1 + (code - 0xFF61)); + return 0; + } + if (code > 0xFFFF) return CES_ERROR_UNASSIGNED_CODE; + const auto& reverse = jisReverse(); + const auto found = std::lower_bound(reverse.begin(), reverse.end(), static_cast(code), [](const JisCell& entry, uint16_t value) { return entry.code < value; }); + if (found == reverse.end() || found->code != code || found->supplementary) return CES_ERROR_UNASSIGNED_CODE; + const int row = found->cell / 94 + 1; + const int cell = found->cell % 94 + 1; + if (row % 2 != 0) { + mbcs[0] = static_cast(row < 63 ? 0x81 + (row - 1) / 2 : 0xE0 + (row - 63) / 2); + mbcs[1] = static_cast(cell <= 63 ? cell + 0x3F : cell + 0x40); + } else { + mbcs[0] = static_cast(row < 63 ? 0x81 + (row - 2) / 2 : 0xE0 + (row - 64) / 2); + mbcs[1] = static_cast(cell + 0x9E); + } + length = 2; + return 0; + } + if (code < 0x80) { + length = 1; + mbcs[0] = static_cast(code); + return 0; + } + if (code >= 0xFF61 && code <= 0xFF9F) { + length = 2; + mbcs[0] = 0x8E; + mbcs[1] = static_cast(0xA1 + (code - 0xFF61)); + return 0; + } + if (code > 0xFFFF) return CES_ERROR_UNASSIGNED_CODE; + const auto& reverse = jisReverse(); + const auto found = std::lower_bound(reverse.begin(), reverse.end(), static_cast(code), [](const JisCell& entry, uint16_t value) { return entry.code < value; }); + if (found == reverse.end() || found->code != code) return CES_ERROR_UNASSIGNED_CODE; + if (found->supplementary) { + length = 3; + mbcs[0] = 0x8F; + mbcs[1] = static_cast(0xA1 + found->cell / 94); + mbcs[2] = static_cast(0xA1 + found->cell % 94); + return 0; + } + length = 2; + mbcs[0] = static_cast(0xA1 + found->cell / 94); + mbcs[1] = static_cast(0xA1 + found->cell % 94); + return 0; +} + +int decodeUcs2(const uint16_t* ucs2, uint32_t, uint32_t& length, uint32_t& code) { + length = 1; + code = ucs2[0]; + return code >= 0xD800 && code <= 0xDFFF ? CES_ERROR_INVALID_ENCODE : 0; +} + +uint32_t encodeUcs2(uint32_t code, uint16_t* ucs2) { + ucs2[0] = static_cast(code); + return 1; +} + +template +int toMbcs(const void* context, const Source* source, uint32_t sourceMax, uint32_t* sourceLen, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, Decode decode) { + if (!context) return CES_ERROR_INVALID_PARAMETER; + const void* profile = contextProfile(context); + if (!isMbcs(profile)) return CES_ERROR_INVALID_PARAMETER; + if (!source) return CES_ERROR_INVALID_SRC_BUFFER; + uint32_t read = 0; + uint32_t written = 0; + while (read < sourceMax && source[read] != 0) { + uint32_t length = 0; + uint32_t code = 0; + if (const int result = decode(source + read, sourceMax - read, length, code); result != 0) return result; + uint8_t encoded[3]; + uint32_t size = 0; + if (const int result = encodeMbcs(profile, code, encoded, size); result != 0) return result; + if (mbcs) { + if (written + size > mbcsMax) return CES_ERROR_DST_BUFFER_END; + std::memcpy(mbcs + written, encoded, size); + } + read += length; + written += size; + } + if (sourceLen) *sourceLen = read; + if (mbcsLen) *mbcsLen = written; + return 0; +} + +template +int fromMbcs(const void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, Target* target, uint32_t targetMax, uint32_t* targetLen, Encode encode) { + if (!context) return CES_ERROR_INVALID_PARAMETER; + const void* profile = contextProfile(context); + if (!isMbcs(profile)) return CES_ERROR_INVALID_PARAMETER; + if (!mbcs) return CES_ERROR_INVALID_SRC_BUFFER; + uint32_t read = 0; + uint32_t written = 0; + while (read < mbcsMax && mbcs[read] != 0) { + uint32_t length = 0; + uint32_t code = 0; + if (const int result = decodeMbcs(profile, mbcs + read, mbcsMax - read, length, code); result != 0) return result; + Target encoded[4]; + const uint32_t size = encode(code, encoded); + if (target) { + if (written + size > targetMax) return CES_ERROR_DST_BUFFER_END; + std::memcpy(target + written, encoded, size * sizeof(Target)); + } + read += length; + written += size; + } + if (mbcsLen) *mbcsLen = read; + if (targetLen) *targetLen = written; + return 0; +} + +int mbcsToUtf8(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint8_t* utf8, uint32_t utf8Max, uint32_t* utf8Len) { + if (!accepted) return CES_ERROR_INVALID_PARAMETER; + if (!mbcs) return CES_ERROR_INVALID_SRC_BUFFER; + if (mbcsMax == 0) return CES_ERROR_SRC_BUFFER_END; + if (!utf8) return CES_ERROR_INVALID_DST_BUFFER; + uint32_t consumed = 0; + uint32_t code = 0; + if (const int result = decodeMbcs(profile, mbcs, mbcsMax, consumed, code); result != 0) return result; + uint8_t encoded[3]; + const uint32_t produced = encodeUtf8(code, encoded); + if (utf8Max < produced) return CES_ERROR_DST_BUFFER_END; + std::memcpy(utf8, encoded, produced); + if (mbcsLen) *mbcsLen = consumed; + if (utf8Len) *utf8Len = produced; + return 0; +} + } extern "C" { @@ -115,19 +540,205 @@ const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1251(void) { return CP1251_PROFILE; } +const void* APS5_VABI sceCesRefersUcsProfileCp1250(void) { + return CP1250_PROFILE; +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_1(void) { + return singleByteProfile(SingleByte::Iso8859_1); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_2(void) { + return singleByteProfile(SingleByte::Iso8859_2); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_4(void) { + return singleByteProfile(SingleByte::Iso8859_4); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_5(void) { + return singleByteProfile(SingleByte::Iso8859_5); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_9(void) { + return singleByteProfile(SingleByte::Iso8859_9); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_10(void) { + return singleByteProfile(SingleByte::Iso8859_10); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_13(void) { + return singleByteProfile(SingleByte::Iso8859_13); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_14(void) { + return singleByteProfile(SingleByte::Iso8859_14); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_15(void) { + return singleByteProfile(SingleByte::Iso8859_15); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_16(void) { + return singleByteProfile(SingleByte::Iso8859_16); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp437(void) { + return singleByteProfile(SingleByte::Cp437); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp737(void) { + return singleByteProfile(SingleByte::Cp737); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp775(void) { + return singleByteProfile(SingleByte::Cp775); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp850(void) { + return singleByteProfile(SingleByte::Cp850); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp852(void) { + return singleByteProfile(SingleByte::Cp852); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp855(void) { + return singleByteProfile(SingleByte::Cp855); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp860(void) { + return singleByteProfile(SingleByte::Cp860); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp861(void) { + return singleByteProfile(SingleByte::Cp861); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp862(void) { + return singleByteProfile(SingleByte::Cp862); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp863(void) { + return singleByteProfile(SingleByte::Cp863); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp865(void) { + return singleByteProfile(SingleByte::Cp865); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp866(void) { + return singleByteProfile(SingleByte::Cp866); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileKoi8R(void) { + return singleByteProfile(SingleByte::Koi8R); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileKoi8U(void) { + return singleByteProfile(SingleByte::Koi8U); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1254(void) { + return singleByteProfile(SingleByte::Cp1254); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1256(void) { + return singleByteProfile(SingleByte::Cp1256); +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1258(void) { + return singleByteProfile(SingleByte::Cp1258); +} + void* APS5_VABI sceCesUcsProfileInitSJis1997Cp932(void* sheet) { if (!sheet) return nullptr; + std::memcpy(sheet, SJIS_TAG, sizeof(SJIS_TAG)); return sheet; } int APS5_VABI sceCesMbcsUcsContextInit(void* context, const void* profile) { - (void)profile; if (!context) return CES_ERROR_INVALID_PARAMETER; + std::memcpy(context, &profile, sizeof(profile)); + return 0; +} + +void* APS5_VABI sceCesUcsProfileInitEucJpCp51932(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, EUC_JP_TAG, sizeof(EUC_JP_TAG)); + return sheet; +} + +void* APS5_VABI sceCesUcsProfileInitEucKr(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, EUC_KR_TAG, sizeof(EUC_KR_TAG)); + return sheet; +} + +void* APS5_VABI sceCesUcsProfileInitUhc(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, UHC_TAG, sizeof(UHC_TAG)); + return sheet; +} + +void* APS5_VABI sceCesUcsProfileInitBig5Cp950(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, BIG5_TAG, sizeof(BIG5_TAG)); + return sheet; +} + +int APS5_VABI sceCesUtf8StrToMbcsStr(void* context, const uint8_t* utf8, uint32_t utf8max, uint32_t* utf8Len, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen) { + if (!mbcs) return CES_ERROR_INVALID_DST_BUFFER; + return toMbcs(context, utf8, utf8max, utf8Len, mbcs, mbcsMax, mbcsLen, decodeUtf8); +} + +int APS5_VABI sceCesUtf8StrGetMbcsLen(void* context, const uint8_t* utf8, uint32_t utf8max, uint32_t* utf8Len, uint32_t* mbcsLen) { + return toMbcs(context, utf8, utf8max, utf8Len, static_cast(nullptr), 0, mbcsLen, decodeUtf8); +} + +int APS5_VABI sceCesUcs2StrToMbcsStr(void* context, const uint16_t* ucs2, uint32_t ucs2max, uint32_t* ucs2Len, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen) { + if (!mbcs) return CES_ERROR_INVALID_DST_BUFFER; + return toMbcs(context, ucs2, ucs2max, ucs2Len, mbcs, mbcsMax, mbcsLen, decodeUcs2); +} + +int APS5_VABI sceCesUcs2StrGetMbcsLen(void* context, const uint16_t* ucs2, uint32_t ucs2max, uint32_t* ucs2Len, uint32_t* mbcsLen) { + return toMbcs(context, ucs2, ucs2max, ucs2Len, static_cast(nullptr), 0, mbcsLen, decodeUcs2); +} + +int APS5_VABI sceCesMbcsStrToUtf8Str(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint8_t* utf8, uint32_t utf8max, uint32_t* utf8Len) { + if (!utf8) return CES_ERROR_INVALID_DST_BUFFER; + return fromMbcs(context, mbcs, mbcsMax, mbcsLen, utf8, utf8max, utf8Len, encodeUtf8); +} + +int APS5_VABI sceCesMbcsStrGetUtf8Len(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint32_t* utf8Len) { + return fromMbcs(context, mbcs, mbcsMax, mbcsLen, static_cast(nullptr), 0, utf8Len, encodeUtf8); +} + +int APS5_VABI sceCesMbcsStrToUcs2Str(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint16_t* ucs2, uint32_t ucs2max, uint32_t* ucs2Len) { + if (!ucs2) return CES_ERROR_INVALID_DST_BUFFER; + return fromMbcs(context, mbcs, mbcsMax, mbcsLen, ucs2, ucs2max, ucs2Len, encodeUcs2); +} + +int APS5_VABI sceCesMbcsStrGetUcs2Len(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint32_t* ucs2Len) { + return fromMbcs(context, mbcs, mbcsMax, mbcsLen, static_cast(nullptr), 0, ucs2Len, encodeUcs2); +} + +int APS5_VABI sceCesEucJpToUcs2(const void* profile, const uint8_t* euc, uint32_t eucMax, uint32_t* eucSize, uint16_t* ucs2) { + if (!isEucJp(profile)) return CES_ERROR_INVALID_PARAMETER; + if (!euc) return CES_ERROR_INVALID_SRC_BUFFER; + if (eucMax == 0) return CES_ERROR_SRC_BUFFER_END; + if (!ucs2) return CES_ERROR_INVALID_DST_BUFFER; + uint32_t length = 0; + uint32_t code = 0; + if (const int result = decodeMbcs(profile, euc, eucMax, length, code); result != 0) return result; + *ucs2 = static_cast(code); + if (eucSize) *eucSize = length; return 0; } int APS5_VABI sceCesSbcToUtf8(const uint8_t* profile, uint8_t sbc, uint8_t* utf8, uint32_t utf8max, uint32_t* utf8_len) { - if (profile != CP1252_PROFILE && profile != CP1251_PROFILE) return CES_ERROR_INVALID_PARAMETER; + if (!isSbc(profile)) return CES_ERROR_INVALID_PARAMETER; if (!utf8) return CES_ERROR_INVALID_DST_BUFFER; uint8_t encoded[3]; @@ -139,7 +750,7 @@ int APS5_VABI sceCesSbcToUtf8(const uint8_t* profile, uint8_t sbc, uint8_t* utf8 } int APS5_VABI sceCesUtf8ToSbc(const uint8_t* utf8, uint32_t utf8max, uint32_t* utf8_len, const uint8_t* profile, uint8_t* sbc) { - if (profile != CP1252_PROFILE && profile != CP1251_PROFILE) return CES_ERROR_INVALID_PARAMETER; + if (!isSbc(profile)) return CES_ERROR_INVALID_PARAMETER; if (!utf8) return CES_ERROR_INVALID_SRC_BUFFER; if (utf8max == 0) return CES_ERROR_SRC_BUFFER_END; if (!sbc) return CES_ERROR_INVALID_DST_BUFFER; @@ -152,4 +763,33 @@ int APS5_VABI sceCesUtf8ToSbc(const uint8_t* utf8, uint32_t utf8max, uint32_t* u return 0; } +int APS5_VABI sceCesEucJpToUtf8(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucLength, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { + if (!isEucJp(profile)) return CES_ERROR_INVALID_PARAMETER; + if (!eucBuffer) return CES_ERROR_INVALID_SRC_BUFFER; + if (eucMax == 0) return CES_ERROR_SRC_BUFFER_END; + if (!utf8Buffer) return CES_ERROR_INVALID_DST_BUFFER; + uint32_t consumed = 0; + uint32_t code = 0; + if (const int result = decodeMbcs(profile, eucBuffer, eucMax, consumed, code); result != 0) return result; + uint8_t encoded[3]; + const uint32_t produced = encodeUtf8(code, encoded); + if (utf8Max < produced) return CES_ERROR_DST_BUFFER_END; + std::memcpy(utf8Buffer, encoded, produced); + if (eucLength) *eucLength = consumed; + if (utf8Length) *utf8Length = produced; + return 0; +} + +int APS5_VABI sceCesEucKrToUtf8(void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucLength, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { + return mbcsToUtf8(isEucKr(profile), profile, eucBuffer, eucMax, eucLength, utf8Buffer, utf8Max, utf8Length); +} + +int APS5_VABI sceCesBig5ToUtf8(void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Length, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { + return mbcsToUtf8(isBig5(profile), profile, big5Buffer, big5Max, big5Length, utf8Buffer, utf8Max, utf8Length); +} + +int APS5_VABI sceCesUhcToUtf8(void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcLength, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { + return mbcsToUtf8(isUhc(profile), profile, uhcBuffer, uhcMax, uhcLength, utf8Buffer, utf8Max, utf8Length); +} + } diff --git a/core/libs/prx/libSceCesCs/JisX0208.hpp b/core/libs/prx/libSceCesCs/JisX0208.hpp new file mode 100644 index 0000000000..e37ae52535 --- /dev/null +++ b/core/libs/prx/libSceCesCs/JisX0208.hpp @@ -0,0 +1,104 @@ +#pragma once + +#include + +namespace JisX0208 { + +inline constexpr uint16_t TO_UCS2[94 * 94] = { + 0x3000, 0x3001, 0x3002, 0xFF0C, 0xFF0E, 0x30FB, 0xFF1A, 0xFF1B, 0xFF1F, 0xFF01, 0x309B, 0x309C, 0x00B4, 0xFF40, 0x00A8, 0xFF3E, 0xFFE3, 0xFF3F, 0x30FD, 0x30FE, 0x309D, 0x309E, 0x3003, 0x4EDD, 0x3005, 0x3006, 0x3007, 0x30FC, 0x2015, 0x2010, 0xFF0F, 0xFF3C, 0xFF5E, 0x2225, 0xFF5C, 0x2026, 0x2025, 0x2018, 0x2019, 0x201C, 0x201D, 0xFF08, 0xFF09, 0x3014, 0x3015, 0xFF3B, 0xFF3D, 0xFF5B, 0xFF5D, 0x3008, 0x3009, 0x300A, 0x300B, 0x300C, 0x300D, 0x300E, 0x300F, 0x3010, 0x3011, 0xFF0B, 0xFF0D, 0x00B1, 0x00D7, 0x00F7, 0xFF1D, 0x2260, 0xFF1C, 0xFF1E, 0x2266, 0x2267, 0x221E, 0x2234, 0x2642, 0x2640, 0x00B0, 0x2032, 0x2033, 0x2103, 0xFFE5, 0xFF04, 0xFFE0, 0xFFE1, 0xFF05, 0xFF03, 0xFF06, 0xFF0A, 0xFF20, 0x00A7, 0x2606, 0x2605, 0x25CB, 0x25CF, 0x25CE, 0x25C7, + 0x25C6, 0x25A1, 0x25A0, 0x25B3, 0x25B2, 0x25BD, 0x25BC, 0x203B, 0x3012, 0x2192, 0x2190, 0x2191, 0x2193, 0x3013, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x2208, 0x220B, 0x2286, 0x2287, 0x2282, 0x2283, 0x222A, 0x2229, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x2227, 0x2228, 0xFFE2, 0x21D2, 0x21D4, 0x2200, 0x2203, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x2220, 0x22A5, 0x2312, 0x2202, 0x2207, 0x2261, 0x2252, 0x226A, 0x226B, 0x221A, 0x223D, 0x221D, 0x2235, 0x222B, 0x222C, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x212B, 0x2030, 0x266F, 0x266D, 0x266A, 0x2020, 0x2021, 0x00B6, 0x0000, 0x0000, 0x0000, 0x0000, 0x25EF, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFF10, 0xFF11, 0xFF12, 0xFF13, 0xFF14, 0xFF15, 0xFF16, 0xFF17, 0xFF18, 0xFF19, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFF21, 0xFF22, 0xFF23, 0xFF24, 0xFF25, 0xFF26, 0xFF27, 0xFF28, 0xFF29, 0xFF2A, 0xFF2B, 0xFF2C, 0xFF2D, 0xFF2E, 0xFF2F, 0xFF30, 0xFF31, 0xFF32, 0xFF33, 0xFF34, 0xFF35, 0xFF36, 0xFF37, 0xFF38, 0xFF39, 0xFF3A, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFF41, 0xFF42, 0xFF43, 0xFF44, 0xFF45, 0xFF46, 0xFF47, 0xFF48, 0xFF49, 0xFF4A, 0xFF4B, 0xFF4C, 0xFF4D, 0xFF4E, 0xFF4F, 0xFF50, 0xFF51, 0xFF52, 0xFF53, 0xFF54, 0xFF55, 0xFF56, 0xFF57, 0xFF58, 0xFF59, 0xFF5A, 0x0000, 0x0000, 0x0000, 0x0000, + 0x3041, 0x3042, 0x3043, 0x3044, 0x3045, 0x3046, 0x3047, 0x3048, 0x3049, 0x304A, 0x304B, 0x304C, 0x304D, 0x304E, 0x304F, 0x3050, 0x3051, 0x3052, 0x3053, 0x3054, 0x3055, 0x3056, 0x3057, 0x3058, 0x3059, 0x305A, 0x305B, 0x305C, 0x305D, 0x305E, 0x305F, 0x3060, 0x3061, 0x3062, 0x3063, 0x3064, 0x3065, 0x3066, 0x3067, 0x3068, 0x3069, 0x306A, 0x306B, 0x306C, 0x306D, 0x306E, 0x306F, 0x3070, 0x3071, 0x3072, 0x3073, 0x3074, 0x3075, 0x3076, 0x3077, 0x3078, 0x3079, 0x307A, 0x307B, 0x307C, 0x307D, 0x307E, 0x307F, 0x3080, 0x3081, 0x3082, 0x3083, 0x3084, 0x3085, 0x3086, 0x3087, 0x3088, 0x3089, 0x308A, 0x308B, 0x308C, 0x308D, 0x308E, 0x308F, 0x3090, 0x3091, 0x3092, 0x3093, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x30A1, 0x30A2, 0x30A3, 0x30A4, 0x30A5, 0x30A6, 0x30A7, 0x30A8, 0x30A9, 0x30AA, 0x30AB, 0x30AC, 0x30AD, 0x30AE, 0x30AF, 0x30B0, 0x30B1, 0x30B2, 0x30B3, 0x30B4, 0x30B5, 0x30B6, 0x30B7, 0x30B8, 0x30B9, 0x30BA, 0x30BB, 0x30BC, 0x30BD, 0x30BE, 0x30BF, 0x30C0, 0x30C1, 0x30C2, 0x30C3, 0x30C4, 0x30C5, 0x30C6, 0x30C7, 0x30C8, 0x30C9, 0x30CA, 0x30CB, 0x30CC, 0x30CD, 0x30CE, 0x30CF, 0x30D0, 0x30D1, 0x30D2, 0x30D3, 0x30D4, 0x30D5, 0x30D6, 0x30D7, 0x30D8, 0x30D9, 0x30DA, 0x30DB, 0x30DC, 0x30DD, 0x30DE, 0x30DF, 0x30E0, 0x30E1, 0x30E2, 0x30E3, 0x30E4, 0x30E5, 0x30E6, 0x30E7, 0x30E8, 0x30E9, 0x30EA, 0x30EB, 0x30EC, 0x30ED, 0x30EE, 0x30EF, 0x30F0, 0x30F1, 0x30F2, 0x30F3, 0x30F4, 0x30F5, 0x30F6, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, 0x03A0, 0x03A1, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, 0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF, 0x03C0, 0x03C1, 0x03C3, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, 0x03C9, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0401, 0x0416, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0451, 0x0436, 0x0437, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x2500, 0x2502, 0x250C, 0x2510, 0x2518, 0x2514, 0x251C, 0x252C, 0x2524, 0x2534, 0x253C, 0x2501, 0x2503, 0x250F, 0x2513, 0x251B, 0x2517, 0x2523, 0x2533, 0x252B, 0x253B, 0x254B, 0x2520, 0x252F, 0x2528, 0x2537, 0x253F, 0x251D, 0x2530, 0x2525, 0x2538, 0x2542, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x2460, 0x2461, 0x2462, 0x2463, 0x2464, 0x2465, 0x2466, 0x2467, 0x2468, 0x2469, 0x246A, 0x246B, 0x246C, 0x246D, 0x246E, 0x246F, 0x2470, 0x2471, 0x2472, 0x2473, 0x2160, 0x2161, 0x2162, 0x2163, 0x2164, 0x2165, 0x2166, 0x2167, 0x2168, 0x2169, 0x0000, 0x3349, 0x3314, 0x3322, 0x334D, 0x3318, 0x3327, 0x3303, 0x3336, 0x3351, 0x3357, 0x330D, 0x3326, 0x3323, 0x332B, 0x334A, 0x333B, 0x339C, 0x339D, 0x339E, 0x338E, 0x338F, 0x33C4, 0x33A1, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x337B, 0x301D, 0x301F, 0x2116, 0x33CD, 0x2121, 0x32A4, 0x32A5, 0x32A6, 0x32A7, 0x32A8, 0x3231, 0x3232, 0x3239, 0x337E, 0x337D, 0x337C, 0x2252, 0x2261, 0x222B, 0x222E, 0x2211, 0x221A, 0x22A5, 0x2220, 0x221F, 0x22BF, 0x2235, 0x2229, 0x222A, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x4E9C, 0x5516, 0x5A03, 0x963F, 0x54C0, 0x611B, 0x6328, 0x59F6, 0x9022, 0x8475, 0x831C, 0x7A50, 0x60AA, 0x63E1, 0x6E25, 0x65ED, 0x8466, 0x82A6, 0x9BF5, 0x6893, 0x5727, 0x65A1, 0x6271, 0x5B9B, 0x59D0, 0x867B, 0x98F4, 0x7D62, 0x7DBE, 0x9B8E, 0x6216, 0x7C9F, 0x88B7, 0x5B89, 0x5EB5, 0x6309, 0x6697, 0x6848, 0x95C7, 0x978D, 0x674F, 0x4EE5, 0x4F0A, 0x4F4D, 0x4F9D, 0x5049, 0x56F2, 0x5937, 0x59D4, 0x5A01, 0x5C09, 0x60DF, 0x610F, 0x6170, 0x6613, 0x6905, 0x70BA, 0x754F, 0x7570, 0x79FB, 0x7DAD, 0x7DEF, 0x80C3, 0x840E, 0x8863, 0x8B02, 0x9055, 0x907A, 0x533B, 0x4E95, 0x4EA5, 0x57DF, 0x80B2, 0x90C1, 0x78EF, 0x4E00, 0x58F1, 0x6EA2, 0x9038, 0x7A32, 0x8328, 0x828B, 0x9C2F, 0x5141, 0x5370, 0x54BD, 0x54E1, 0x56E0, 0x59FB, 0x5F15, 0x98F2, 0x6DEB, 0x80E4, 0x852D, + 0x9662, 0x9670, 0x96A0, 0x97FB, 0x540B, 0x53F3, 0x5B87, 0x70CF, 0x7FBD, 0x8FC2, 0x96E8, 0x536F, 0x9D5C, 0x7ABA, 0x4E11, 0x7893, 0x81FC, 0x6E26, 0x5618, 0x5504, 0x6B1D, 0x851A, 0x9C3B, 0x59E5, 0x53A9, 0x6D66, 0x74DC, 0x958F, 0x5642, 0x4E91, 0x904B, 0x96F2, 0x834F, 0x990C, 0x53E1, 0x55B6, 0x5B30, 0x5F71, 0x6620, 0x66F3, 0x6804, 0x6C38, 0x6CF3, 0x6D29, 0x745B, 0x76C8, 0x7A4E, 0x9834, 0x82F1, 0x885B, 0x8A60, 0x92ED, 0x6DB2, 0x75AB, 0x76CA, 0x99C5, 0x60A6, 0x8B01, 0x8D8A, 0x95B2, 0x698E, 0x53AD, 0x5186, 0x5712, 0x5830, 0x5944, 0x5BB4, 0x5EF6, 0x6028, 0x63A9, 0x63F4, 0x6CBF, 0x6F14, 0x708E, 0x7114, 0x7159, 0x71D5, 0x733F, 0x7E01, 0x8276, 0x82D1, 0x8597, 0x9060, 0x925B, 0x9D1B, 0x5869, 0x65BC, 0x6C5A, 0x7525, 0x51F9, 0x592E, 0x5965, 0x5F80, 0x5FDC, + 0x62BC, 0x65FA, 0x6A2A, 0x6B27, 0x6BB4, 0x738B, 0x7FC1, 0x8956, 0x9D2C, 0x9D0E, 0x9EC4, 0x5CA1, 0x6C96, 0x837B, 0x5104, 0x5C4B, 0x61B6, 0x81C6, 0x6876, 0x7261, 0x4E59, 0x4FFA, 0x5378, 0x6069, 0x6E29, 0x7A4F, 0x97F3, 0x4E0B, 0x5316, 0x4EEE, 0x4F55, 0x4F3D, 0x4FA1, 0x4F73, 0x52A0, 0x53EF, 0x5609, 0x590F, 0x5AC1, 0x5BB6, 0x5BE1, 0x79D1, 0x6687, 0x679C, 0x67B6, 0x6B4C, 0x6CB3, 0x706B, 0x73C2, 0x798D, 0x79BE, 0x7A3C, 0x7B87, 0x82B1, 0x82DB, 0x8304, 0x8377, 0x83EF, 0x83D3, 0x8766, 0x8AB2, 0x5629, 0x8CA8, 0x8FE6, 0x904E, 0x971E, 0x868A, 0x4FC4, 0x5CE8, 0x6211, 0x7259, 0x753B, 0x81E5, 0x82BD, 0x86FE, 0x8CC0, 0x96C5, 0x9913, 0x99D5, 0x4ECB, 0x4F1A, 0x89E3, 0x56DE, 0x584A, 0x58CA, 0x5EFB, 0x5FEB, 0x602A, 0x6094, 0x6062, 0x61D0, 0x6212, 0x62D0, 0x6539, + 0x9B41, 0x6666, 0x68B0, 0x6D77, 0x7070, 0x754C, 0x7686, 0x7D75, 0x82A5, 0x87F9, 0x958B, 0x968E, 0x8C9D, 0x51F1, 0x52BE, 0x5916, 0x54B3, 0x5BB3, 0x5D16, 0x6168, 0x6982, 0x6DAF, 0x788D, 0x84CB, 0x8857, 0x8A72, 0x93A7, 0x9AB8, 0x6D6C, 0x99A8, 0x86D9, 0x57A3, 0x67FF, 0x86CE, 0x920E, 0x5283, 0x5687, 0x5404, 0x5ED3, 0x62E1, 0x64B9, 0x683C, 0x6838, 0x6BBB, 0x7372, 0x78BA, 0x7A6B, 0x899A, 0x89D2, 0x8D6B, 0x8F03, 0x90ED, 0x95A3, 0x9694, 0x9769, 0x5B66, 0x5CB3, 0x697D, 0x984D, 0x984E, 0x639B, 0x7B20, 0x6A2B, 0x6A7F, 0x68B6, 0x9C0D, 0x6F5F, 0x5272, 0x559D, 0x6070, 0x62EC, 0x6D3B, 0x6E07, 0x6ED1, 0x845B, 0x8910, 0x8F44, 0x4E14, 0x9C39, 0x53F6, 0x691B, 0x6A3A, 0x9784, 0x682A, 0x515C, 0x7AC3, 0x84B2, 0x91DC, 0x938C, 0x565B, 0x9D28, 0x6822, 0x8305, 0x8431, + 0x7CA5, 0x5208, 0x82C5, 0x74E6, 0x4E7E, 0x4F83, 0x51A0, 0x5BD2, 0x520A, 0x52D8, 0x52E7, 0x5DFB, 0x559A, 0x582A, 0x59E6, 0x5B8C, 0x5B98, 0x5BDB, 0x5E72, 0x5E79, 0x60A3, 0x611F, 0x6163, 0x61BE, 0x63DB, 0x6562, 0x67D1, 0x6853, 0x68FA, 0x6B3E, 0x6B53, 0x6C57, 0x6F22, 0x6F97, 0x6F45, 0x74B0, 0x7518, 0x76E3, 0x770B, 0x7AFF, 0x7BA1, 0x7C21, 0x7DE9, 0x7F36, 0x7FF0, 0x809D, 0x8266, 0x839E, 0x89B3, 0x8ACC, 0x8CAB, 0x9084, 0x9451, 0x9593, 0x9591, 0x95A2, 0x9665, 0x97D3, 0x9928, 0x8218, 0x4E38, 0x542B, 0x5CB8, 0x5DCC, 0x73A9, 0x764C, 0x773C, 0x5CA9, 0x7FEB, 0x8D0B, 0x96C1, 0x9811, 0x9854, 0x9858, 0x4F01, 0x4F0E, 0x5371, 0x559C, 0x5668, 0x57FA, 0x5947, 0x5B09, 0x5BC4, 0x5C90, 0x5E0C, 0x5E7E, 0x5FCC, 0x63EE, 0x673A, 0x65D7, 0x65E2, 0x671F, 0x68CB, 0x68C4, + 0x6A5F, 0x5E30, 0x6BC5, 0x6C17, 0x6C7D, 0x757F, 0x7948, 0x5B63, 0x7A00, 0x7D00, 0x5FBD, 0x898F, 0x8A18, 0x8CB4, 0x8D77, 0x8ECC, 0x8F1D, 0x98E2, 0x9A0E, 0x9B3C, 0x4E80, 0x507D, 0x5100, 0x5993, 0x5B9C, 0x622F, 0x6280, 0x64EC, 0x6B3A, 0x72A0, 0x7591, 0x7947, 0x7FA9, 0x87FB, 0x8ABC, 0x8B70, 0x63AC, 0x83CA, 0x97A0, 0x5409, 0x5403, 0x55AB, 0x6854, 0x6A58, 0x8A70, 0x7827, 0x6775, 0x9ECD, 0x5374, 0x5BA2, 0x811A, 0x8650, 0x9006, 0x4E18, 0x4E45, 0x4EC7, 0x4F11, 0x53CA, 0x5438, 0x5BAE, 0x5F13, 0x6025, 0x6551, 0x673D, 0x6C42, 0x6C72, 0x6CE3, 0x7078, 0x7403, 0x7A76, 0x7AAE, 0x7B08, 0x7D1A, 0x7CFE, 0x7D66, 0x65E7, 0x725B, 0x53BB, 0x5C45, 0x5DE8, 0x62D2, 0x62E0, 0x6319, 0x6E20, 0x865A, 0x8A31, 0x8DDD, 0x92F8, 0x6F01, 0x79A6, 0x9B5A, 0x4EA8, 0x4EAB, 0x4EAC, + 0x4F9B, 0x4FA0, 0x50D1, 0x5147, 0x7AF6, 0x5171, 0x51F6, 0x5354, 0x5321, 0x537F, 0x53EB, 0x55AC, 0x5883, 0x5CE1, 0x5F37, 0x5F4A, 0x602F, 0x6050, 0x606D, 0x631F, 0x6559, 0x6A4B, 0x6CC1, 0x72C2, 0x72ED, 0x77EF, 0x80F8, 0x8105, 0x8208, 0x854E, 0x90F7, 0x93E1, 0x97FF, 0x9957, 0x9A5A, 0x4EF0, 0x51DD, 0x5C2D, 0x6681, 0x696D, 0x5C40, 0x66F2, 0x6975, 0x7389, 0x6850, 0x7C81, 0x50C5, 0x52E4, 0x5747, 0x5DFE, 0x9326, 0x65A4, 0x6B23, 0x6B3D, 0x7434, 0x7981, 0x79BD, 0x7B4B, 0x7DCA, 0x82B9, 0x83CC, 0x887F, 0x895F, 0x8B39, 0x8FD1, 0x91D1, 0x541F, 0x9280, 0x4E5D, 0x5036, 0x53E5, 0x533A, 0x72D7, 0x7396, 0x77E9, 0x82E6, 0x8EAF, 0x99C6, 0x99C8, 0x99D2, 0x5177, 0x611A, 0x865E, 0x55B0, 0x7A7A, 0x5076, 0x5BD3, 0x9047, 0x9685, 0x4E32, 0x6ADB, 0x91E7, 0x5C51, 0x5C48, + 0x6398, 0x7A9F, 0x6C93, 0x9774, 0x8F61, 0x7AAA, 0x718A, 0x9688, 0x7C82, 0x6817, 0x7E70, 0x6851, 0x936C, 0x52F2, 0x541B, 0x85AB, 0x8A13, 0x7FA4, 0x8ECD, 0x90E1, 0x5366, 0x8888, 0x7941, 0x4FC2, 0x50BE, 0x5211, 0x5144, 0x5553, 0x572D, 0x73EA, 0x578B, 0x5951, 0x5F62, 0x5F84, 0x6075, 0x6176, 0x6167, 0x61A9, 0x63B2, 0x643A, 0x656C, 0x666F, 0x6842, 0x6E13, 0x7566, 0x7A3D, 0x7CFB, 0x7D4C, 0x7D99, 0x7E4B, 0x7F6B, 0x830E, 0x834A, 0x86CD, 0x8A08, 0x8A63, 0x8B66, 0x8EFD, 0x981A, 0x9D8F, 0x82B8, 0x8FCE, 0x9BE8, 0x5287, 0x621F, 0x6483, 0x6FC0, 0x9699, 0x6841, 0x5091, 0x6B20, 0x6C7A, 0x6F54, 0x7A74, 0x7D50, 0x8840, 0x8A23, 0x6708, 0x4EF6, 0x5039, 0x5026, 0x5065, 0x517C, 0x5238, 0x5263, 0x55A7, 0x570F, 0x5805, 0x5ACC, 0x5EFA, 0x61B2, 0x61F8, 0x62F3, 0x6372, + 0x691C, 0x6A29, 0x727D, 0x72AC, 0x732E, 0x7814, 0x786F, 0x7D79, 0x770C, 0x80A9, 0x898B, 0x8B19, 0x8CE2, 0x8ED2, 0x9063, 0x9375, 0x967A, 0x9855, 0x9A13, 0x9E78, 0x5143, 0x539F, 0x53B3, 0x5E7B, 0x5F26, 0x6E1B, 0x6E90, 0x7384, 0x73FE, 0x7D43, 0x8237, 0x8A00, 0x8AFA, 0x9650, 0x4E4E, 0x500B, 0x53E4, 0x547C, 0x56FA, 0x59D1, 0x5B64, 0x5DF1, 0x5EAB, 0x5F27, 0x6238, 0x6545, 0x67AF, 0x6E56, 0x72D0, 0x7CCA, 0x88B4, 0x80A1, 0x80E1, 0x83F0, 0x864E, 0x8A87, 0x8DE8, 0x9237, 0x96C7, 0x9867, 0x9F13, 0x4E94, 0x4E92, 0x4F0D, 0x5348, 0x5449, 0x543E, 0x5A2F, 0x5F8C, 0x5FA1, 0x609F, 0x68A7, 0x6A8E, 0x745A, 0x7881, 0x8A9E, 0x8AA4, 0x8B77, 0x9190, 0x4E5E, 0x9BC9, 0x4EA4, 0x4F7C, 0x4FAF, 0x5019, 0x5016, 0x5149, 0x516C, 0x529F, 0x52B9, 0x52FE, 0x539A, 0x53E3, 0x5411, + 0x540E, 0x5589, 0x5751, 0x57A2, 0x597D, 0x5B54, 0x5B5D, 0x5B8F, 0x5DE5, 0x5DE7, 0x5DF7, 0x5E78, 0x5E83, 0x5E9A, 0x5EB7, 0x5F18, 0x6052, 0x614C, 0x6297, 0x62D8, 0x63A7, 0x653B, 0x6602, 0x6643, 0x66F4, 0x676D, 0x6821, 0x6897, 0x69CB, 0x6C5F, 0x6D2A, 0x6D69, 0x6E2F, 0x6E9D, 0x7532, 0x7687, 0x786C, 0x7A3F, 0x7CE0, 0x7D05, 0x7D18, 0x7D5E, 0x7DB1, 0x8015, 0x8003, 0x80AF, 0x80B1, 0x8154, 0x818F, 0x822A, 0x8352, 0x884C, 0x8861, 0x8B1B, 0x8CA2, 0x8CFC, 0x90CA, 0x9175, 0x9271, 0x783F, 0x92FC, 0x95A4, 0x964D, 0x9805, 0x9999, 0x9AD8, 0x9D3B, 0x525B, 0x52AB, 0x53F7, 0x5408, 0x58D5, 0x62F7, 0x6FE0, 0x8C6A, 0x8F5F, 0x9EB9, 0x514B, 0x523B, 0x544A, 0x56FD, 0x7A40, 0x9177, 0x9D60, 0x9ED2, 0x7344, 0x6F09, 0x8170, 0x7511, 0x5FFD, 0x60DA, 0x9AA8, 0x72DB, 0x8FBC, + 0x6B64, 0x9803, 0x4ECA, 0x56F0, 0x5764, 0x58BE, 0x5A5A, 0x6068, 0x61C7, 0x660F, 0x6606, 0x6839, 0x68B1, 0x6DF7, 0x75D5, 0x7D3A, 0x826E, 0x9B42, 0x4E9B, 0x4F50, 0x53C9, 0x5506, 0x5D6F, 0x5DE6, 0x5DEE, 0x67FB, 0x6C99, 0x7473, 0x7802, 0x8A50, 0x9396, 0x88DF, 0x5750, 0x5EA7, 0x632B, 0x50B5, 0x50AC, 0x518D, 0x6700, 0x54C9, 0x585E, 0x59BB, 0x5BB0, 0x5F69, 0x624D, 0x63A1, 0x683D, 0x6B73, 0x6E08, 0x707D, 0x91C7, 0x7280, 0x7815, 0x7826, 0x796D, 0x658E, 0x7D30, 0x83DC, 0x88C1, 0x8F09, 0x969B, 0x5264, 0x5728, 0x6750, 0x7F6A, 0x8CA1, 0x51B4, 0x5742, 0x962A, 0x583A, 0x698A, 0x80B4, 0x54B2, 0x5D0E, 0x57FC, 0x7895, 0x9DFA, 0x4F5C, 0x524A, 0x548B, 0x643E, 0x6628, 0x6714, 0x67F5, 0x7A84, 0x7B56, 0x7D22, 0x932F, 0x685C, 0x9BAD, 0x7B39, 0x5319, 0x518A, 0x5237, + 0x5BDF, 0x62F6, 0x64AE, 0x64E6, 0x672D, 0x6BBA, 0x85A9, 0x96D1, 0x7690, 0x9BD6, 0x634C, 0x9306, 0x9BAB, 0x76BF, 0x6652, 0x4E09, 0x5098, 0x53C2, 0x5C71, 0x60E8, 0x6492, 0x6563, 0x685F, 0x71E6, 0x73CA, 0x7523, 0x7B97, 0x7E82, 0x8695, 0x8B83, 0x8CDB, 0x9178, 0x9910, 0x65AC, 0x66AB, 0x6B8B, 0x4ED5, 0x4ED4, 0x4F3A, 0x4F7F, 0x523A, 0x53F8, 0x53F2, 0x55E3, 0x56DB, 0x58EB, 0x59CB, 0x59C9, 0x59FF, 0x5B50, 0x5C4D, 0x5E02, 0x5E2B, 0x5FD7, 0x601D, 0x6307, 0x652F, 0x5B5C, 0x65AF, 0x65BD, 0x65E8, 0x679D, 0x6B62, 0x6B7B, 0x6C0F, 0x7345, 0x7949, 0x79C1, 0x7CF8, 0x7D19, 0x7D2B, 0x80A2, 0x8102, 0x81F3, 0x8996, 0x8A5E, 0x8A69, 0x8A66, 0x8A8C, 0x8AEE, 0x8CC7, 0x8CDC, 0x96CC, 0x98FC, 0x6B6F, 0x4E8B, 0x4F3C, 0x4F8D, 0x5150, 0x5B57, 0x5BFA, 0x6148, 0x6301, 0x6642, + 0x6B21, 0x6ECB, 0x6CBB, 0x723E, 0x74BD, 0x75D4, 0x78C1, 0x793A, 0x800C, 0x8033, 0x81EA, 0x8494, 0x8F9E, 0x6C50, 0x9E7F, 0x5F0F, 0x8B58, 0x9D2B, 0x7AFA, 0x8EF8, 0x5B8D, 0x96EB, 0x4E03, 0x53F1, 0x57F7, 0x5931, 0x5AC9, 0x5BA4, 0x6089, 0x6E7F, 0x6F06, 0x75BE, 0x8CEA, 0x5B9F, 0x8500, 0x7BE0, 0x5072, 0x67F4, 0x829D, 0x5C61, 0x854A, 0x7E1E, 0x820E, 0x5199, 0x5C04, 0x6368, 0x8D66, 0x659C, 0x716E, 0x793E, 0x7D17, 0x8005, 0x8B1D, 0x8ECA, 0x906E, 0x86C7, 0x90AA, 0x501F, 0x52FA, 0x5C3A, 0x6753, 0x707C, 0x7235, 0x914C, 0x91C8, 0x932B, 0x82E5, 0x5BC2, 0x5F31, 0x60F9, 0x4E3B, 0x53D6, 0x5B88, 0x624B, 0x6731, 0x6B8A, 0x72E9, 0x73E0, 0x7A2E, 0x816B, 0x8DA3, 0x9152, 0x9996, 0x5112, 0x53D7, 0x546A, 0x5BFF, 0x6388, 0x6A39, 0x7DAC, 0x9700, 0x56DA, 0x53CE, 0x5468, + 0x5B97, 0x5C31, 0x5DDE, 0x4FEE, 0x6101, 0x62FE, 0x6D32, 0x79C0, 0x79CB, 0x7D42, 0x7E4D, 0x7FD2, 0x81ED, 0x821F, 0x8490, 0x8846, 0x8972, 0x8B90, 0x8E74, 0x8F2F, 0x9031, 0x914B, 0x916C, 0x96C6, 0x919C, 0x4EC0, 0x4F4F, 0x5145, 0x5341, 0x5F93, 0x620E, 0x67D4, 0x6C41, 0x6E0B, 0x7363, 0x7E26, 0x91CD, 0x9283, 0x53D4, 0x5919, 0x5BBF, 0x6DD1, 0x795D, 0x7E2E, 0x7C9B, 0x587E, 0x719F, 0x51FA, 0x8853, 0x8FF0, 0x4FCA, 0x5CFB, 0x6625, 0x77AC, 0x7AE3, 0x821C, 0x99FF, 0x51C6, 0x5FAA, 0x65EC, 0x696F, 0x6B89, 0x6DF3, 0x6E96, 0x6F64, 0x76FE, 0x7D14, 0x5DE1, 0x9075, 0x9187, 0x9806, 0x51E6, 0x521D, 0x6240, 0x6691, 0x66D9, 0x6E1A, 0x5EB6, 0x7DD2, 0x7F72, 0x66F8, 0x85AF, 0x85F7, 0x8AF8, 0x52A9, 0x53D9, 0x5973, 0x5E8F, 0x5F90, 0x6055, 0x92E4, 0x9664, 0x50B7, 0x511F, + 0x52DD, 0x5320, 0x5347, 0x53EC, 0x54E8, 0x5546, 0x5531, 0x5617, 0x5968, 0x59BE, 0x5A3C, 0x5BB5, 0x5C06, 0x5C0F, 0x5C11, 0x5C1A, 0x5E84, 0x5E8A, 0x5EE0, 0x5F70, 0x627F, 0x6284, 0x62DB, 0x638C, 0x6377, 0x6607, 0x660C, 0x662D, 0x6676, 0x677E, 0x68A2, 0x6A1F, 0x6A35, 0x6CBC, 0x6D88, 0x6E09, 0x6E58, 0x713C, 0x7126, 0x7167, 0x75C7, 0x7701, 0x785D, 0x7901, 0x7965, 0x79F0, 0x7AE0, 0x7B11, 0x7CA7, 0x7D39, 0x8096, 0x83D6, 0x848B, 0x8549, 0x885D, 0x88F3, 0x8A1F, 0x8A3C, 0x8A54, 0x8A73, 0x8C61, 0x8CDE, 0x91A4, 0x9266, 0x937E, 0x9418, 0x969C, 0x9798, 0x4E0A, 0x4E08, 0x4E1E, 0x4E57, 0x5197, 0x5270, 0x57CE, 0x5834, 0x58CC, 0x5B22, 0x5E38, 0x60C5, 0x64FE, 0x6761, 0x6756, 0x6D44, 0x72B6, 0x7573, 0x7A63, 0x84B8, 0x8B72, 0x91B8, 0x9320, 0x5631, 0x57F4, 0x98FE, + 0x62ED, 0x690D, 0x6B96, 0x71ED, 0x7E54, 0x8077, 0x8272, 0x89E6, 0x98DF, 0x8755, 0x8FB1, 0x5C3B, 0x4F38, 0x4FE1, 0x4FB5, 0x5507, 0x5A20, 0x5BDD, 0x5BE9, 0x5FC3, 0x614E, 0x632F, 0x65B0, 0x664B, 0x68EE, 0x699B, 0x6D78, 0x6DF1, 0x7533, 0x75B9, 0x771F, 0x795E, 0x79E6, 0x7D33, 0x81E3, 0x82AF, 0x85AA, 0x89AA, 0x8A3A, 0x8EAB, 0x8F9B, 0x9032, 0x91DD, 0x9707, 0x4EBA, 0x4EC1, 0x5203, 0x5875, 0x58EC, 0x5C0B, 0x751A, 0x5C3D, 0x814E, 0x8A0A, 0x8FC5, 0x9663, 0x976D, 0x7B25, 0x8ACF, 0x9808, 0x9162, 0x56F3, 0x53A8, 0x9017, 0x5439, 0x5782, 0x5E25, 0x63A8, 0x6C34, 0x708A, 0x7761, 0x7C8B, 0x7FE0, 0x8870, 0x9042, 0x9154, 0x9310, 0x9318, 0x968F, 0x745E, 0x9AC4, 0x5D07, 0x5D69, 0x6570, 0x67A2, 0x8DA8, 0x96DB, 0x636E, 0x6749, 0x6919, 0x83C5, 0x9817, 0x96C0, 0x88FE, + 0x6F84, 0x647A, 0x5BF8, 0x4E16, 0x702C, 0x755D, 0x662F, 0x51C4, 0x5236, 0x52E2, 0x59D3, 0x5F81, 0x6027, 0x6210, 0x653F, 0x6574, 0x661F, 0x6674, 0x68F2, 0x6816, 0x6B63, 0x6E05, 0x7272, 0x751F, 0x76DB, 0x7CBE, 0x8056, 0x58F0, 0x88FD, 0x897F, 0x8AA0, 0x8A93, 0x8ACB, 0x901D, 0x9192, 0x9752, 0x9759, 0x6589, 0x7A0E, 0x8106, 0x96BB, 0x5E2D, 0x60DC, 0x621A, 0x65A5, 0x6614, 0x6790, 0x77F3, 0x7A4D, 0x7C4D, 0x7E3E, 0x810A, 0x8CAC, 0x8D64, 0x8DE1, 0x8E5F, 0x78A9, 0x5207, 0x62D9, 0x63A5, 0x6442, 0x6298, 0x8A2D, 0x7A83, 0x7BC0, 0x8AAC, 0x96EA, 0x7D76, 0x820C, 0x8749, 0x4ED9, 0x5148, 0x5343, 0x5360, 0x5BA3, 0x5C02, 0x5C16, 0x5DDD, 0x6226, 0x6247, 0x64B0, 0x6813, 0x6834, 0x6CC9, 0x6D45, 0x6D17, 0x67D3, 0x6F5C, 0x714E, 0x717D, 0x65CB, 0x7A7F, 0x7BAD, 0x7DDA, + 0x7E4A, 0x7FA8, 0x817A, 0x821B, 0x8239, 0x85A6, 0x8A6E, 0x8CCE, 0x8DF5, 0x9078, 0x9077, 0x92AD, 0x9291, 0x9583, 0x9BAE, 0x524D, 0x5584, 0x6F38, 0x7136, 0x5168, 0x7985, 0x7E55, 0x81B3, 0x7CCE, 0x564C, 0x5851, 0x5CA8, 0x63AA, 0x66FE, 0x66FD, 0x695A, 0x72D9, 0x758F, 0x758E, 0x790E, 0x7956, 0x79DF, 0x7C97, 0x7D20, 0x7D44, 0x8607, 0x8A34, 0x963B, 0x9061, 0x9F20, 0x50E7, 0x5275, 0x53CC, 0x53E2, 0x5009, 0x55AA, 0x58EE, 0x594F, 0x723D, 0x5B8B, 0x5C64, 0x531D, 0x60E3, 0x60F3, 0x635C, 0x6383, 0x633F, 0x63BB, 0x64CD, 0x65E9, 0x66F9, 0x5DE3, 0x69CD, 0x69FD, 0x6F15, 0x71E5, 0x4E89, 0x75E9, 0x76F8, 0x7A93, 0x7CDF, 0x7DCF, 0x7D9C, 0x8061, 0x8349, 0x8358, 0x846C, 0x84BC, 0x85FB, 0x88C5, 0x8D70, 0x9001, 0x906D, 0x9397, 0x971C, 0x9A12, 0x50CF, 0x5897, 0x618E, + 0x81D3, 0x8535, 0x8D08, 0x9020, 0x4FC3, 0x5074, 0x5247, 0x5373, 0x606F, 0x6349, 0x675F, 0x6E2C, 0x8DB3, 0x901F, 0x4FD7, 0x5C5E, 0x8CCA, 0x65CF, 0x7D9A, 0x5352, 0x8896, 0x5176, 0x63C3, 0x5B58, 0x5B6B, 0x5C0A, 0x640D, 0x6751, 0x905C, 0x4ED6, 0x591A, 0x592A, 0x6C70, 0x8A51, 0x553E, 0x5815, 0x59A5, 0x60F0, 0x6253, 0x67C1, 0x8235, 0x6955, 0x9640, 0x99C4, 0x9A28, 0x4F53, 0x5806, 0x5BFE, 0x8010, 0x5CB1, 0x5E2F, 0x5F85, 0x6020, 0x614B, 0x6234, 0x66FF, 0x6CF0, 0x6EDE, 0x80CE, 0x817F, 0x82D4, 0x888B, 0x8CB8, 0x9000, 0x902E, 0x968A, 0x9EDB, 0x9BDB, 0x4EE3, 0x53F0, 0x5927, 0x7B2C, 0x918D, 0x984C, 0x9DF9, 0x6EDD, 0x7027, 0x5353, 0x5544, 0x5B85, 0x6258, 0x629E, 0x62D3, 0x6CA2, 0x6FEF, 0x7422, 0x8A17, 0x9438, 0x6FC1, 0x8AFE, 0x8338, 0x51E7, 0x86F8, 0x53EA, + 0x53E9, 0x4F46, 0x9054, 0x8FB0, 0x596A, 0x8131, 0x5DFD, 0x7AEA, 0x8FBF, 0x68DA, 0x8C37, 0x72F8, 0x9C48, 0x6A3D, 0x8AB0, 0x4E39, 0x5358, 0x5606, 0x5766, 0x62C5, 0x63A2, 0x65E6, 0x6B4E, 0x6DE1, 0x6E5B, 0x70AD, 0x77ED, 0x7AEF, 0x7BAA, 0x7DBB, 0x803D, 0x80C6, 0x86CB, 0x8A95, 0x935B, 0x56E3, 0x58C7, 0x5F3E, 0x65AD, 0x6696, 0x6A80, 0x6BB5, 0x7537, 0x8AC7, 0x5024, 0x77E5, 0x5730, 0x5F1B, 0x6065, 0x667A, 0x6C60, 0x75F4, 0x7A1A, 0x7F6E, 0x81F4, 0x8718, 0x9045, 0x99B3, 0x7BC9, 0x755C, 0x7AF9, 0x7B51, 0x84C4, 0x9010, 0x79E9, 0x7A92, 0x8336, 0x5AE1, 0x7740, 0x4E2D, 0x4EF2, 0x5B99, 0x5FE0, 0x62BD, 0x663C, 0x67F1, 0x6CE8, 0x866B, 0x8877, 0x8A3B, 0x914E, 0x92F3, 0x99D0, 0x6A17, 0x7026, 0x732A, 0x82E7, 0x8457, 0x8CAF, 0x4E01, 0x5146, 0x51CB, 0x558B, 0x5BF5, + 0x5E16, 0x5E33, 0x5E81, 0x5F14, 0x5F35, 0x5F6B, 0x5FB4, 0x61F2, 0x6311, 0x66A2, 0x671D, 0x6F6E, 0x7252, 0x753A, 0x773A, 0x8074, 0x8139, 0x8178, 0x8776, 0x8ABF, 0x8ADC, 0x8D85, 0x8DF3, 0x929A, 0x9577, 0x9802, 0x9CE5, 0x52C5, 0x6357, 0x76F4, 0x6715, 0x6C88, 0x73CD, 0x8CC3, 0x93AE, 0x9673, 0x6D25, 0x589C, 0x690E, 0x69CC, 0x8FFD, 0x939A, 0x75DB, 0x901A, 0x585A, 0x6802, 0x63B4, 0x69FB, 0x4F43, 0x6F2C, 0x67D8, 0x8FBB, 0x8526, 0x7DB4, 0x9354, 0x693F, 0x6F70, 0x576A, 0x58F7, 0x5B2C, 0x7D2C, 0x722A, 0x540A, 0x91E3, 0x9DB4, 0x4EAD, 0x4F4E, 0x505C, 0x5075, 0x5243, 0x8C9E, 0x5448, 0x5824, 0x5B9A, 0x5E1D, 0x5E95, 0x5EAD, 0x5EF7, 0x5F1F, 0x608C, 0x62B5, 0x633A, 0x63D0, 0x68AF, 0x6C40, 0x7887, 0x798E, 0x7A0B, 0x7DE0, 0x8247, 0x8A02, 0x8AE6, 0x8E44, 0x9013, + 0x90B8, 0x912D, 0x91D8, 0x9F0E, 0x6CE5, 0x6458, 0x64E2, 0x6575, 0x6EF4, 0x7684, 0x7B1B, 0x9069, 0x93D1, 0x6EBA, 0x54F2, 0x5FB9, 0x64A4, 0x8F4D, 0x8FED, 0x9244, 0x5178, 0x586B, 0x5929, 0x5C55, 0x5E97, 0x6DFB, 0x7E8F, 0x751C, 0x8CBC, 0x8EE2, 0x985B, 0x70B9, 0x4F1D, 0x6BBF, 0x6FB1, 0x7530, 0x96FB, 0x514E, 0x5410, 0x5835, 0x5857, 0x59AC, 0x5C60, 0x5F92, 0x6597, 0x675C, 0x6E21, 0x767B, 0x83DF, 0x8CED, 0x9014, 0x90FD, 0x934D, 0x7825, 0x783A, 0x52AA, 0x5EA6, 0x571F, 0x5974, 0x6012, 0x5012, 0x515A, 0x51AC, 0x51CD, 0x5200, 0x5510, 0x5854, 0x5858, 0x5957, 0x5B95, 0x5CF6, 0x5D8B, 0x60BC, 0x6295, 0x642D, 0x6771, 0x6843, 0x68BC, 0x68DF, 0x76D7, 0x6DD8, 0x6E6F, 0x6D9B, 0x706F, 0x71C8, 0x5F53, 0x75D8, 0x7977, 0x7B49, 0x7B54, 0x7B52, 0x7CD6, 0x7D71, 0x5230, + 0x8463, 0x8569, 0x85E4, 0x8A0E, 0x8B04, 0x8C46, 0x8E0F, 0x9003, 0x900F, 0x9419, 0x9676, 0x982D, 0x9A30, 0x95D8, 0x50CD, 0x52D5, 0x540C, 0x5802, 0x5C0E, 0x61A7, 0x649E, 0x6D1E, 0x77B3, 0x7AE5, 0x80F4, 0x8404, 0x9053, 0x9285, 0x5CE0, 0x9D07, 0x533F, 0x5F97, 0x5FB3, 0x6D9C, 0x7279, 0x7763, 0x79BF, 0x7BE4, 0x6BD2, 0x72EC, 0x8AAD, 0x6803, 0x6A61, 0x51F8, 0x7A81, 0x6934, 0x5C4A, 0x9CF6, 0x82EB, 0x5BC5, 0x9149, 0x701E, 0x5678, 0x5C6F, 0x60C7, 0x6566, 0x6C8C, 0x8C5A, 0x9041, 0x9813, 0x5451, 0x66C7, 0x920D, 0x5948, 0x90A3, 0x5185, 0x4E4D, 0x51EA, 0x8599, 0x8B0E, 0x7058, 0x637A, 0x934B, 0x6962, 0x99B4, 0x7E04, 0x7577, 0x5357, 0x6960, 0x8EDF, 0x96E3, 0x6C5D, 0x4E8C, 0x5C3C, 0x5F10, 0x8FE9, 0x5302, 0x8CD1, 0x8089, 0x8679, 0x5EFF, 0x65E5, 0x4E73, 0x5165, + 0x5982, 0x5C3F, 0x97EE, 0x4EFB, 0x598A, 0x5FCD, 0x8A8D, 0x6FE1, 0x79B0, 0x7962, 0x5BE7, 0x8471, 0x732B, 0x71B1, 0x5E74, 0x5FF5, 0x637B, 0x649A, 0x71C3, 0x7C98, 0x4E43, 0x5EFC, 0x4E4B, 0x57DC, 0x56A2, 0x60A9, 0x6FC3, 0x7D0D, 0x80FD, 0x8133, 0x81BF, 0x8FB2, 0x8997, 0x86A4, 0x5DF4, 0x628A, 0x64AD, 0x8987, 0x6777, 0x6CE2, 0x6D3E, 0x7436, 0x7834, 0x5A46, 0x7F75, 0x82AD, 0x99AC, 0x4FF3, 0x5EC3, 0x62DD, 0x6392, 0x6557, 0x676F, 0x76C3, 0x724C, 0x80CC, 0x80BA, 0x8F29, 0x914D, 0x500D, 0x57F9, 0x5A92, 0x6885, 0x6973, 0x7164, 0x72FD, 0x8CB7, 0x58F2, 0x8CE0, 0x966A, 0x9019, 0x877F, 0x79E4, 0x77E7, 0x8429, 0x4F2F, 0x5265, 0x535A, 0x62CD, 0x67CF, 0x6CCA, 0x767D, 0x7B94, 0x7C95, 0x8236, 0x8584, 0x8FEB, 0x66DD, 0x6F20, 0x7206, 0x7E1B, 0x83AB, 0x99C1, 0x9EA6, + 0x51FD, 0x7BB1, 0x7872, 0x7BB8, 0x8087, 0x7B48, 0x6AE8, 0x5E61, 0x808C, 0x7551, 0x7560, 0x516B, 0x9262, 0x6E8C, 0x767A, 0x9197, 0x9AEA, 0x4F10, 0x7F70, 0x629C, 0x7B4F, 0x95A5, 0x9CE9, 0x567A, 0x5859, 0x86E4, 0x96BC, 0x4F34, 0x5224, 0x534A, 0x53CD, 0x53DB, 0x5E06, 0x642C, 0x6591, 0x677F, 0x6C3E, 0x6C4E, 0x7248, 0x72AF, 0x73ED, 0x7554, 0x7E41, 0x822C, 0x85E9, 0x8CA9, 0x7BC4, 0x91C6, 0x7169, 0x9812, 0x98EF, 0x633D, 0x6669, 0x756A, 0x76E4, 0x78D0, 0x8543, 0x86EE, 0x532A, 0x5351, 0x5426, 0x5983, 0x5E87, 0x5F7C, 0x60B2, 0x6249, 0x6279, 0x62AB, 0x6590, 0x6BD4, 0x6CCC, 0x75B2, 0x76AE, 0x7891, 0x79D8, 0x7DCB, 0x7F77, 0x80A5, 0x88AB, 0x8AB9, 0x8CBB, 0x907F, 0x975E, 0x98DB, 0x6A0B, 0x7C38, 0x5099, 0x5C3E, 0x5FAE, 0x6787, 0x6BD8, 0x7435, 0x7709, 0x7F8E, + 0x9F3B, 0x67CA, 0x7A17, 0x5339, 0x758B, 0x9AED, 0x5F66, 0x819D, 0x83F1, 0x8098, 0x5F3C, 0x5FC5, 0x7562, 0x7B46, 0x903C, 0x6867, 0x59EB, 0x5A9B, 0x7D10, 0x767E, 0x8B2C, 0x4FF5, 0x5F6A, 0x6A19, 0x6C37, 0x6F02, 0x74E2, 0x7968, 0x8868, 0x8A55, 0x8C79, 0x5EDF, 0x63CF, 0x75C5, 0x79D2, 0x82D7, 0x9328, 0x92F2, 0x849C, 0x86ED, 0x9C2D, 0x54C1, 0x5F6C, 0x658C, 0x6D5C, 0x7015, 0x8CA7, 0x8CD3, 0x983B, 0x654F, 0x74F6, 0x4E0D, 0x4ED8, 0x57E0, 0x592B, 0x5A66, 0x5BCC, 0x51A8, 0x5E03, 0x5E9C, 0x6016, 0x6276, 0x6577, 0x65A7, 0x666E, 0x6D6E, 0x7236, 0x7B26, 0x8150, 0x819A, 0x8299, 0x8B5C, 0x8CA0, 0x8CE6, 0x8D74, 0x961C, 0x9644, 0x4FAE, 0x64AB, 0x6B66, 0x821E, 0x8461, 0x856A, 0x90E8, 0x5C01, 0x6953, 0x98A8, 0x847A, 0x8557, 0x4F0F, 0x526F, 0x5FA9, 0x5E45, 0x670D, + 0x798F, 0x8179, 0x8907, 0x8986, 0x6DF5, 0x5F17, 0x6255, 0x6CB8, 0x4ECF, 0x7269, 0x9B92, 0x5206, 0x543B, 0x5674, 0x58B3, 0x61A4, 0x626E, 0x711A, 0x596E, 0x7C89, 0x7CDE, 0x7D1B, 0x96F0, 0x6587, 0x805E, 0x4E19, 0x4F75, 0x5175, 0x5840, 0x5E63, 0x5E73, 0x5F0A, 0x67C4, 0x4E26, 0x853D, 0x9589, 0x965B, 0x7C73, 0x9801, 0x50FB, 0x58C1, 0x7656, 0x78A7, 0x5225, 0x77A5, 0x8511, 0x7B86, 0x504F, 0x5909, 0x7247, 0x7BC7, 0x7DE8, 0x8FBA, 0x8FD4, 0x904D, 0x4FBF, 0x52C9, 0x5A29, 0x5F01, 0x97AD, 0x4FDD, 0x8217, 0x92EA, 0x5703, 0x6355, 0x6B69, 0x752B, 0x88DC, 0x8F14, 0x7A42, 0x52DF, 0x5893, 0x6155, 0x620A, 0x66AE, 0x6BCD, 0x7C3F, 0x83E9, 0x5023, 0x4FF8, 0x5305, 0x5446, 0x5831, 0x5949, 0x5B9D, 0x5CF0, 0x5CEF, 0x5D29, 0x5E96, 0x62B1, 0x6367, 0x653E, 0x65B9, 0x670B, + 0x6CD5, 0x6CE1, 0x70F9, 0x7832, 0x7E2B, 0x80DE, 0x82B3, 0x840C, 0x84EC, 0x8702, 0x8912, 0x8A2A, 0x8C4A, 0x90A6, 0x92D2, 0x98FD, 0x9CF3, 0x9D6C, 0x4E4F, 0x4EA1, 0x508D, 0x5256, 0x574A, 0x59A8, 0x5E3D, 0x5FD8, 0x5FD9, 0x623F, 0x66B4, 0x671B, 0x67D0, 0x68D2, 0x5192, 0x7D21, 0x80AA, 0x81A8, 0x8B00, 0x8C8C, 0x8CBF, 0x927E, 0x9632, 0x5420, 0x982C, 0x5317, 0x50D5, 0x535C, 0x58A8, 0x64B2, 0x6734, 0x7267, 0x7766, 0x7A46, 0x91E6, 0x52C3, 0x6CA1, 0x6B86, 0x5800, 0x5E4C, 0x5954, 0x672C, 0x7FFB, 0x51E1, 0x76C6, 0x6469, 0x78E8, 0x9B54, 0x9EBB, 0x57CB, 0x59B9, 0x6627, 0x679A, 0x6BCE, 0x54E9, 0x69D9, 0x5E55, 0x819C, 0x6795, 0x9BAA, 0x67FE, 0x9C52, 0x685D, 0x4EA6, 0x4FE3, 0x53C8, 0x62B9, 0x672B, 0x6CAB, 0x8FC4, 0x4FAD, 0x7E6D, 0x9EBF, 0x4E07, 0x6162, 0x6E80, + 0x6F2B, 0x8513, 0x5473, 0x672A, 0x9B45, 0x5DF3, 0x7B95, 0x5CAC, 0x5BC6, 0x871C, 0x6E4A, 0x84D1, 0x7A14, 0x8108, 0x5999, 0x7C8D, 0x6C11, 0x7720, 0x52D9, 0x5922, 0x7121, 0x725F, 0x77DB, 0x9727, 0x9D61, 0x690B, 0x5A7F, 0x5A18, 0x51A5, 0x540D, 0x547D, 0x660E, 0x76DF, 0x8FF7, 0x9298, 0x9CF4, 0x59EA, 0x725D, 0x6EC5, 0x514D, 0x68C9, 0x7DBF, 0x7DEC, 0x9762, 0x9EBA, 0x6478, 0x6A21, 0x8302, 0x5984, 0x5B5F, 0x6BDB, 0x731B, 0x76F2, 0x7DB2, 0x8017, 0x8499, 0x5132, 0x6728, 0x9ED9, 0x76EE, 0x6762, 0x52FF, 0x9905, 0x5C24, 0x623B, 0x7C7E, 0x8CB0, 0x554F, 0x60B6, 0x7D0B, 0x9580, 0x5301, 0x4E5F, 0x51B6, 0x591C, 0x723A, 0x8036, 0x91CE, 0x5F25, 0x77E2, 0x5384, 0x5F79, 0x7D04, 0x85AC, 0x8A33, 0x8E8D, 0x9756, 0x67F3, 0x85AE, 0x9453, 0x6109, 0x6108, 0x6CB9, 0x7652, + 0x8AED, 0x8F38, 0x552F, 0x4F51, 0x512A, 0x52C7, 0x53CB, 0x5BA5, 0x5E7D, 0x60A0, 0x6182, 0x63D6, 0x6709, 0x67DA, 0x6E67, 0x6D8C, 0x7336, 0x7337, 0x7531, 0x7950, 0x88D5, 0x8A98, 0x904A, 0x9091, 0x90F5, 0x96C4, 0x878D, 0x5915, 0x4E88, 0x4F59, 0x4E0E, 0x8A89, 0x8F3F, 0x9810, 0x50AD, 0x5E7C, 0x5996, 0x5BB9, 0x5EB8, 0x63DA, 0x63FA, 0x64C1, 0x66DC, 0x694A, 0x69D8, 0x6D0B, 0x6EB6, 0x7194, 0x7528, 0x7AAF, 0x7F8A, 0x8000, 0x8449, 0x84C9, 0x8981, 0x8B21, 0x8E0A, 0x9065, 0x967D, 0x990A, 0x617E, 0x6291, 0x6B32, 0x6C83, 0x6D74, 0x7FCC, 0x7FFC, 0x6DC0, 0x7F85, 0x87BA, 0x88F8, 0x6765, 0x83B1, 0x983C, 0x96F7, 0x6D1B, 0x7D61, 0x843D, 0x916A, 0x4E71, 0x5375, 0x5D50, 0x6B04, 0x6FEB, 0x85CD, 0x862D, 0x89A7, 0x5229, 0x540F, 0x5C65, 0x674E, 0x68A8, 0x7406, 0x7483, + 0x75E2, 0x88CF, 0x88E1, 0x91CC, 0x96E2, 0x9678, 0x5F8B, 0x7387, 0x7ACB, 0x844E, 0x63A0, 0x7565, 0x5289, 0x6D41, 0x6E9C, 0x7409, 0x7559, 0x786B, 0x7C92, 0x9686, 0x7ADC, 0x9F8D, 0x4FB6, 0x616E, 0x65C5, 0x865C, 0x4E86, 0x4EAE, 0x50DA, 0x4E21, 0x51CC, 0x5BEE, 0x6599, 0x6881, 0x6DBC, 0x731F, 0x7642, 0x77AD, 0x7A1C, 0x7CE7, 0x826F, 0x8AD2, 0x907C, 0x91CF, 0x9675, 0x9818, 0x529B, 0x7DD1, 0x502B, 0x5398, 0x6797, 0x6DCB, 0x71D0, 0x7433, 0x81E8, 0x8F2A, 0x96A3, 0x9C57, 0x9E9F, 0x7460, 0x5841, 0x6D99, 0x7D2F, 0x985E, 0x4EE4, 0x4F36, 0x4F8B, 0x51B7, 0x52B1, 0x5DBA, 0x601C, 0x73B2, 0x793C, 0x82D3, 0x9234, 0x96B7, 0x96F6, 0x970A, 0x9E97, 0x9F62, 0x66A6, 0x6B74, 0x5217, 0x52A3, 0x70C8, 0x88C2, 0x5EC9, 0x604B, 0x6190, 0x6F23, 0x7149, 0x7C3E, 0x7DF4, 0x806F, + 0x84EE, 0x9023, 0x932C, 0x5442, 0x9B6F, 0x6AD3, 0x7089, 0x8CC2, 0x8DEF, 0x9732, 0x52B4, 0x5A41, 0x5ECA, 0x5F04, 0x6717, 0x697C, 0x6994, 0x6D6A, 0x6F0F, 0x7262, 0x72FC, 0x7BED, 0x8001, 0x807E, 0x874B, 0x90CE, 0x516D, 0x9E93, 0x7984, 0x808B, 0x9332, 0x8AD6, 0x502D, 0x548C, 0x8A71, 0x6B6A, 0x8CC4, 0x8107, 0x60D1, 0x67A0, 0x9DF2, 0x4E99, 0x4E98, 0x9C10, 0x8A6B, 0x85C1, 0x8568, 0x6900, 0x6E7E, 0x7897, 0x8155, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x5F0C, 0x4E10, 0x4E15, 0x4E2A, 0x4E31, 0x4E36, 0x4E3C, 0x4E3F, 0x4E42, 0x4E56, 0x4E58, 0x4E82, 0x4E85, 0x8C6B, 0x4E8A, 0x8212, 0x5F0D, 0x4E8E, 0x4E9E, 0x4E9F, 0x4EA0, 0x4EA2, 0x4EB0, 0x4EB3, 0x4EB6, 0x4ECE, 0x4ECD, 0x4EC4, 0x4EC6, 0x4EC2, 0x4ED7, 0x4EDE, 0x4EED, 0x4EDF, 0x4EF7, 0x4F09, 0x4F5A, 0x4F30, 0x4F5B, 0x4F5D, 0x4F57, 0x4F47, 0x4F76, 0x4F88, 0x4F8F, 0x4F98, 0x4F7B, 0x4F69, 0x4F70, 0x4F91, 0x4F6F, 0x4F86, 0x4F96, 0x5118, 0x4FD4, 0x4FDF, 0x4FCE, 0x4FD8, 0x4FDB, 0x4FD1, 0x4FDA, 0x4FD0, 0x4FE4, 0x4FE5, 0x501A, 0x5028, 0x5014, 0x502A, 0x5025, 0x5005, 0x4F1C, 0x4FF6, 0x5021, 0x5029, 0x502C, 0x4FFE, 0x4FEF, 0x5011, 0x5006, 0x5043, 0x5047, 0x6703, 0x5055, 0x5050, 0x5048, 0x505A, 0x5056, 0x506C, 0x5078, 0x5080, 0x509A, 0x5085, 0x50B4, 0x50B2, + 0x50C9, 0x50CA, 0x50B3, 0x50C2, 0x50D6, 0x50DE, 0x50E5, 0x50ED, 0x50E3, 0x50EE, 0x50F9, 0x50F5, 0x5109, 0x5101, 0x5102, 0x5116, 0x5115, 0x5114, 0x511A, 0x5121, 0x513A, 0x5137, 0x513C, 0x513B, 0x513F, 0x5140, 0x5152, 0x514C, 0x5154, 0x5162, 0x7AF8, 0x5169, 0x516A, 0x516E, 0x5180, 0x5182, 0x56D8, 0x518C, 0x5189, 0x518F, 0x5191, 0x5193, 0x5195, 0x5196, 0x51A4, 0x51A6, 0x51A2, 0x51A9, 0x51AA, 0x51AB, 0x51B3, 0x51B1, 0x51B2, 0x51B0, 0x51B5, 0x51BD, 0x51C5, 0x51C9, 0x51DB, 0x51E0, 0x8655, 0x51E9, 0x51ED, 0x51F0, 0x51F5, 0x51FE, 0x5204, 0x520B, 0x5214, 0x520E, 0x5227, 0x522A, 0x522E, 0x5233, 0x5239, 0x524F, 0x5244, 0x524B, 0x524C, 0x525E, 0x5254, 0x526A, 0x5274, 0x5269, 0x5273, 0x527F, 0x527D, 0x528D, 0x5294, 0x5292, 0x5271, 0x5288, 0x5291, 0x8FA8, + 0x8FA7, 0x52AC, 0x52AD, 0x52BC, 0x52B5, 0x52C1, 0x52CD, 0x52D7, 0x52DE, 0x52E3, 0x52E6, 0x98ED, 0x52E0, 0x52F3, 0x52F5, 0x52F8, 0x52F9, 0x5306, 0x5308, 0x7538, 0x530D, 0x5310, 0x530F, 0x5315, 0x531A, 0x5323, 0x532F, 0x5331, 0x5333, 0x5338, 0x5340, 0x5346, 0x5345, 0x4E17, 0x5349, 0x534D, 0x51D6, 0x535E, 0x5369, 0x536E, 0x5918, 0x537B, 0x5377, 0x5382, 0x5396, 0x53A0, 0x53A6, 0x53A5, 0x53AE, 0x53B0, 0x53B6, 0x53C3, 0x7C12, 0x96D9, 0x53DF, 0x66FC, 0x71EE, 0x53EE, 0x53E8, 0x53ED, 0x53FA, 0x5401, 0x543D, 0x5440, 0x542C, 0x542D, 0x543C, 0x542E, 0x5436, 0x5429, 0x541D, 0x544E, 0x548F, 0x5475, 0x548E, 0x545F, 0x5471, 0x5477, 0x5470, 0x5492, 0x547B, 0x5480, 0x5476, 0x5484, 0x5490, 0x5486, 0x54C7, 0x54A2, 0x54B8, 0x54A5, 0x54AC, 0x54C4, 0x54C8, 0x54A8, + 0x54AB, 0x54C2, 0x54A4, 0x54BE, 0x54BC, 0x54D8, 0x54E5, 0x54E6, 0x550F, 0x5514, 0x54FD, 0x54EE, 0x54ED, 0x54FA, 0x54E2, 0x5539, 0x5540, 0x5563, 0x554C, 0x552E, 0x555C, 0x5545, 0x5556, 0x5557, 0x5538, 0x5533, 0x555D, 0x5599, 0x5580, 0x54AF, 0x558A, 0x559F, 0x557B, 0x557E, 0x5598, 0x559E, 0x55AE, 0x557C, 0x5583, 0x55A9, 0x5587, 0x55A8, 0x55DA, 0x55C5, 0x55DF, 0x55C4, 0x55DC, 0x55E4, 0x55D4, 0x5614, 0x55F7, 0x5616, 0x55FE, 0x55FD, 0x561B, 0x55F9, 0x564E, 0x5650, 0x71DF, 0x5634, 0x5636, 0x5632, 0x5638, 0x566B, 0x5664, 0x562F, 0x566C, 0x566A, 0x5686, 0x5680, 0x568A, 0x56A0, 0x5694, 0x568F, 0x56A5, 0x56AE, 0x56B6, 0x56B4, 0x56C2, 0x56BC, 0x56C1, 0x56C3, 0x56C0, 0x56C8, 0x56CE, 0x56D1, 0x56D3, 0x56D7, 0x56EE, 0x56F9, 0x5700, 0x56FF, 0x5704, 0x5709, + 0x5708, 0x570B, 0x570D, 0x5713, 0x5718, 0x5716, 0x55C7, 0x571C, 0x5726, 0x5737, 0x5738, 0x574E, 0x573B, 0x5740, 0x574F, 0x5769, 0x57C0, 0x5788, 0x5761, 0x577F, 0x5789, 0x5793, 0x57A0, 0x57B3, 0x57A4, 0x57AA, 0x57B0, 0x57C3, 0x57C6, 0x57D4, 0x57D2, 0x57D3, 0x580A, 0x57D6, 0x57E3, 0x580B, 0x5819, 0x581D, 0x5872, 0x5821, 0x5862, 0x584B, 0x5870, 0x6BC0, 0x5852, 0x583D, 0x5879, 0x5885, 0x58B9, 0x589F, 0x58AB, 0x58BA, 0x58DE, 0x58BB, 0x58B8, 0x58AE, 0x58C5, 0x58D3, 0x58D1, 0x58D7, 0x58D9, 0x58D8, 0x58E5, 0x58DC, 0x58E4, 0x58DF, 0x58EF, 0x58FA, 0x58F9, 0x58FB, 0x58FC, 0x58FD, 0x5902, 0x590A, 0x5910, 0x591B, 0x68A6, 0x5925, 0x592C, 0x592D, 0x5932, 0x5938, 0x593E, 0x7AD2, 0x5955, 0x5950, 0x594E, 0x595A, 0x5958, 0x5962, 0x5960, 0x5967, 0x596C, 0x5969, + 0x5978, 0x5981, 0x599D, 0x4F5E, 0x4FAB, 0x59A3, 0x59B2, 0x59C6, 0x59E8, 0x59DC, 0x598D, 0x59D9, 0x59DA, 0x5A25, 0x5A1F, 0x5A11, 0x5A1C, 0x5A09, 0x5A1A, 0x5A40, 0x5A6C, 0x5A49, 0x5A35, 0x5A36, 0x5A62, 0x5A6A, 0x5A9A, 0x5ABC, 0x5ABE, 0x5ACB, 0x5AC2, 0x5ABD, 0x5AE3, 0x5AD7, 0x5AE6, 0x5AE9, 0x5AD6, 0x5AFA, 0x5AFB, 0x5B0C, 0x5B0B, 0x5B16, 0x5B32, 0x5AD0, 0x5B2A, 0x5B36, 0x5B3E, 0x5B43, 0x5B45, 0x5B40, 0x5B51, 0x5B55, 0x5B5A, 0x5B5B, 0x5B65, 0x5B69, 0x5B70, 0x5B73, 0x5B75, 0x5B78, 0x6588, 0x5B7A, 0x5B80, 0x5B83, 0x5BA6, 0x5BB8, 0x5BC3, 0x5BC7, 0x5BC9, 0x5BD4, 0x5BD0, 0x5BE4, 0x5BE6, 0x5BE2, 0x5BDE, 0x5BE5, 0x5BEB, 0x5BF0, 0x5BF6, 0x5BF3, 0x5C05, 0x5C07, 0x5C08, 0x5C0D, 0x5C13, 0x5C20, 0x5C22, 0x5C28, 0x5C38, 0x5C39, 0x5C41, 0x5C46, 0x5C4E, 0x5C53, + 0x5C50, 0x5C4F, 0x5B71, 0x5C6C, 0x5C6E, 0x4E62, 0x5C76, 0x5C79, 0x5C8C, 0x5C91, 0x5C94, 0x599B, 0x5CAB, 0x5CBB, 0x5CB6, 0x5CBC, 0x5CB7, 0x5CC5, 0x5CBE, 0x5CC7, 0x5CD9, 0x5CE9, 0x5CFD, 0x5CFA, 0x5CED, 0x5D8C, 0x5CEA, 0x5D0B, 0x5D15, 0x5D17, 0x5D5C, 0x5D1F, 0x5D1B, 0x5D11, 0x5D14, 0x5D22, 0x5D1A, 0x5D19, 0x5D18, 0x5D4C, 0x5D52, 0x5D4E, 0x5D4B, 0x5D6C, 0x5D73, 0x5D76, 0x5D87, 0x5D84, 0x5D82, 0x5DA2, 0x5D9D, 0x5DAC, 0x5DAE, 0x5DBD, 0x5D90, 0x5DB7, 0x5DBC, 0x5DC9, 0x5DCD, 0x5DD3, 0x5DD2, 0x5DD6, 0x5DDB, 0x5DEB, 0x5DF2, 0x5DF5, 0x5E0B, 0x5E1A, 0x5E19, 0x5E11, 0x5E1B, 0x5E36, 0x5E37, 0x5E44, 0x5E43, 0x5E40, 0x5E4E, 0x5E57, 0x5E54, 0x5E5F, 0x5E62, 0x5E64, 0x5E47, 0x5E75, 0x5E76, 0x5E7A, 0x9EBC, 0x5E7F, 0x5EA0, 0x5EC1, 0x5EC2, 0x5EC8, 0x5ED0, 0x5ECF, + 0x5ED6, 0x5EE3, 0x5EDD, 0x5EDA, 0x5EDB, 0x5EE2, 0x5EE1, 0x5EE8, 0x5EE9, 0x5EEC, 0x5EF1, 0x5EF3, 0x5EF0, 0x5EF4, 0x5EF8, 0x5EFE, 0x5F03, 0x5F09, 0x5F5D, 0x5F5C, 0x5F0B, 0x5F11, 0x5F16, 0x5F29, 0x5F2D, 0x5F38, 0x5F41, 0x5F48, 0x5F4C, 0x5F4E, 0x5F2F, 0x5F51, 0x5F56, 0x5F57, 0x5F59, 0x5F61, 0x5F6D, 0x5F73, 0x5F77, 0x5F83, 0x5F82, 0x5F7F, 0x5F8A, 0x5F88, 0x5F91, 0x5F87, 0x5F9E, 0x5F99, 0x5F98, 0x5FA0, 0x5FA8, 0x5FAD, 0x5FBC, 0x5FD6, 0x5FFB, 0x5FE4, 0x5FF8, 0x5FF1, 0x5FDD, 0x60B3, 0x5FFF, 0x6021, 0x6060, 0x6019, 0x6010, 0x6029, 0x600E, 0x6031, 0x601B, 0x6015, 0x602B, 0x6026, 0x600F, 0x603A, 0x605A, 0x6041, 0x606A, 0x6077, 0x605F, 0x604A, 0x6046, 0x604D, 0x6063, 0x6043, 0x6064, 0x6042, 0x606C, 0x606B, 0x6059, 0x6081, 0x608D, 0x60E7, 0x6083, 0x609A, + 0x6084, 0x609B, 0x6096, 0x6097, 0x6092, 0x60A7, 0x608B, 0x60E1, 0x60B8, 0x60E0, 0x60D3, 0x60B4, 0x5FF0, 0x60BD, 0x60C6, 0x60B5, 0x60D8, 0x614D, 0x6115, 0x6106, 0x60F6, 0x60F7, 0x6100, 0x60F4, 0x60FA, 0x6103, 0x6121, 0x60FB, 0x60F1, 0x610D, 0x610E, 0x6147, 0x613E, 0x6128, 0x6127, 0x614A, 0x613F, 0x613C, 0x612C, 0x6134, 0x613D, 0x6142, 0x6144, 0x6173, 0x6177, 0x6158, 0x6159, 0x615A, 0x616B, 0x6174, 0x616F, 0x6165, 0x6171, 0x615F, 0x615D, 0x6153, 0x6175, 0x6199, 0x6196, 0x6187, 0x61AC, 0x6194, 0x619A, 0x618A, 0x6191, 0x61AB, 0x61AE, 0x61CC, 0x61CA, 0x61C9, 0x61F7, 0x61C8, 0x61C3, 0x61C6, 0x61BA, 0x61CB, 0x7F79, 0x61CD, 0x61E6, 0x61E3, 0x61F6, 0x61FA, 0x61F4, 0x61FF, 0x61FD, 0x61FC, 0x61FE, 0x6200, 0x6208, 0x6209, 0x620D, 0x620C, 0x6214, 0x621B, + 0x621E, 0x6221, 0x622A, 0x622E, 0x6230, 0x6232, 0x6233, 0x6241, 0x624E, 0x625E, 0x6263, 0x625B, 0x6260, 0x6268, 0x627C, 0x6282, 0x6289, 0x627E, 0x6292, 0x6293, 0x6296, 0x62D4, 0x6283, 0x6294, 0x62D7, 0x62D1, 0x62BB, 0x62CF, 0x62FF, 0x62C6, 0x64D4, 0x62C8, 0x62DC, 0x62CC, 0x62CA, 0x62C2, 0x62C7, 0x629B, 0x62C9, 0x630C, 0x62EE, 0x62F1, 0x6327, 0x6302, 0x6308, 0x62EF, 0x62F5, 0x6350, 0x633E, 0x634D, 0x641C, 0x634F, 0x6396, 0x638E, 0x6380, 0x63AB, 0x6376, 0x63A3, 0x638F, 0x6389, 0x639F, 0x63B5, 0x636B, 0x6369, 0x63BE, 0x63E9, 0x63C0, 0x63C6, 0x63E3, 0x63C9, 0x63D2, 0x63F6, 0x63C4, 0x6416, 0x6434, 0x6406, 0x6413, 0x6426, 0x6436, 0x651D, 0x6417, 0x6428, 0x640F, 0x6467, 0x646F, 0x6476, 0x644E, 0x652A, 0x6495, 0x6493, 0x64A5, 0x64A9, 0x6488, 0x64BC, + 0x64DA, 0x64D2, 0x64C5, 0x64C7, 0x64BB, 0x64D8, 0x64C2, 0x64F1, 0x64E7, 0x8209, 0x64E0, 0x64E1, 0x62AC, 0x64E3, 0x64EF, 0x652C, 0x64F6, 0x64F4, 0x64F2, 0x64FA, 0x6500, 0x64FD, 0x6518, 0x651C, 0x6505, 0x6524, 0x6523, 0x652B, 0x6534, 0x6535, 0x6537, 0x6536, 0x6538, 0x754B, 0x6548, 0x6556, 0x6555, 0x654D, 0x6558, 0x655E, 0x655D, 0x6572, 0x6578, 0x6582, 0x6583, 0x8B8A, 0x659B, 0x659F, 0x65AB, 0x65B7, 0x65C3, 0x65C6, 0x65C1, 0x65C4, 0x65CC, 0x65D2, 0x65DB, 0x65D9, 0x65E0, 0x65E1, 0x65F1, 0x6772, 0x660A, 0x6603, 0x65FB, 0x6773, 0x6635, 0x6636, 0x6634, 0x661C, 0x664F, 0x6644, 0x6649, 0x6641, 0x665E, 0x665D, 0x6664, 0x6667, 0x6668, 0x665F, 0x6662, 0x6670, 0x6683, 0x6688, 0x668E, 0x6689, 0x6684, 0x6698, 0x669D, 0x66C1, 0x66B9, 0x66C9, 0x66BE, 0x66BC, + 0x66C4, 0x66B8, 0x66D6, 0x66DA, 0x66E0, 0x663F, 0x66E6, 0x66E9, 0x66F0, 0x66F5, 0x66F7, 0x670F, 0x6716, 0x671E, 0x6726, 0x6727, 0x9738, 0x672E, 0x673F, 0x6736, 0x6741, 0x6738, 0x6737, 0x6746, 0x675E, 0x6760, 0x6759, 0x6763, 0x6764, 0x6789, 0x6770, 0x67A9, 0x677C, 0x676A, 0x678C, 0x678B, 0x67A6, 0x67A1, 0x6785, 0x67B7, 0x67EF, 0x67B4, 0x67EC, 0x67B3, 0x67E9, 0x67B8, 0x67E4, 0x67DE, 0x67DD, 0x67E2, 0x67EE, 0x67B9, 0x67CE, 0x67C6, 0x67E7, 0x6A9C, 0x681E, 0x6846, 0x6829, 0x6840, 0x684D, 0x6832, 0x684E, 0x68B3, 0x682B, 0x6859, 0x6863, 0x6877, 0x687F, 0x689F, 0x688F, 0x68AD, 0x6894, 0x689D, 0x689B, 0x6883, 0x6AAE, 0x68B9, 0x6874, 0x68B5, 0x68A0, 0x68BA, 0x690F, 0x688D, 0x687E, 0x6901, 0x68CA, 0x6908, 0x68D8, 0x6922, 0x6926, 0x68E1, 0x690C, 0x68CD, + 0x68D4, 0x68E7, 0x68D5, 0x6936, 0x6912, 0x6904, 0x68D7, 0x68E3, 0x6925, 0x68F9, 0x68E0, 0x68EF, 0x6928, 0x692A, 0x691A, 0x6923, 0x6921, 0x68C6, 0x6979, 0x6977, 0x695C, 0x6978, 0x696B, 0x6954, 0x697E, 0x696E, 0x6939, 0x6974, 0x693D, 0x6959, 0x6930, 0x6961, 0x695E, 0x695D, 0x6981, 0x696A, 0x69B2, 0x69AE, 0x69D0, 0x69BF, 0x69C1, 0x69D3, 0x69BE, 0x69CE, 0x5BE8, 0x69CA, 0x69DD, 0x69BB, 0x69C3, 0x69A7, 0x6A2E, 0x6991, 0x69A0, 0x699C, 0x6995, 0x69B4, 0x69DE, 0x69E8, 0x6A02, 0x6A1B, 0x69FF, 0x6B0A, 0x69F9, 0x69F2, 0x69E7, 0x6A05, 0x69B1, 0x6A1E, 0x69ED, 0x6A14, 0x69EB, 0x6A0A, 0x6A12, 0x6AC1, 0x6A23, 0x6A13, 0x6A44, 0x6A0C, 0x6A72, 0x6A36, 0x6A78, 0x6A47, 0x6A62, 0x6A59, 0x6A66, 0x6A48, 0x6A38, 0x6A22, 0x6A90, 0x6A8D, 0x6AA0, 0x6A84, 0x6AA2, 0x6AA3, + 0x6A97, 0x8617, 0x6ABB, 0x6AC3, 0x6AC2, 0x6AB8, 0x6AB3, 0x6AAC, 0x6ADE, 0x6AD1, 0x6ADF, 0x6AAA, 0x6ADA, 0x6AEA, 0x6AFB, 0x6B05, 0x8616, 0x6AFA, 0x6B12, 0x6B16, 0x9B31, 0x6B1F, 0x6B38, 0x6B37, 0x76DC, 0x6B39, 0x98EE, 0x6B47, 0x6B43, 0x6B49, 0x6B50, 0x6B59, 0x6B54, 0x6B5B, 0x6B5F, 0x6B61, 0x6B78, 0x6B79, 0x6B7F, 0x6B80, 0x6B84, 0x6B83, 0x6B8D, 0x6B98, 0x6B95, 0x6B9E, 0x6BA4, 0x6BAA, 0x6BAB, 0x6BAF, 0x6BB2, 0x6BB1, 0x6BB3, 0x6BB7, 0x6BBC, 0x6BC6, 0x6BCB, 0x6BD3, 0x6BDF, 0x6BEC, 0x6BEB, 0x6BF3, 0x6BEF, 0x9EBE, 0x6C08, 0x6C13, 0x6C14, 0x6C1B, 0x6C24, 0x6C23, 0x6C5E, 0x6C55, 0x6C62, 0x6C6A, 0x6C82, 0x6C8D, 0x6C9A, 0x6C81, 0x6C9B, 0x6C7E, 0x6C68, 0x6C73, 0x6C92, 0x6C90, 0x6CC4, 0x6CF1, 0x6CD3, 0x6CBD, 0x6CD7, 0x6CC5, 0x6CDD, 0x6CAE, 0x6CB1, 0x6CBE, + 0x6CBA, 0x6CDB, 0x6CEF, 0x6CD9, 0x6CEA, 0x6D1F, 0x884D, 0x6D36, 0x6D2B, 0x6D3D, 0x6D38, 0x6D19, 0x6D35, 0x6D33, 0x6D12, 0x6D0C, 0x6D63, 0x6D93, 0x6D64, 0x6D5A, 0x6D79, 0x6D59, 0x6D8E, 0x6D95, 0x6FE4, 0x6D85, 0x6DF9, 0x6E15, 0x6E0A, 0x6DB5, 0x6DC7, 0x6DE6, 0x6DB8, 0x6DC6, 0x6DEC, 0x6DDE, 0x6DCC, 0x6DE8, 0x6DD2, 0x6DC5, 0x6DFA, 0x6DD9, 0x6DE4, 0x6DD5, 0x6DEA, 0x6DEE, 0x6E2D, 0x6E6E, 0x6E2E, 0x6E19, 0x6E72, 0x6E5F, 0x6E3E, 0x6E23, 0x6E6B, 0x6E2B, 0x6E76, 0x6E4D, 0x6E1F, 0x6E43, 0x6E3A, 0x6E4E, 0x6E24, 0x6EFF, 0x6E1D, 0x6E38, 0x6E82, 0x6EAA, 0x6E98, 0x6EC9, 0x6EB7, 0x6ED3, 0x6EBD, 0x6EAF, 0x6EC4, 0x6EB2, 0x6ED4, 0x6ED5, 0x6E8F, 0x6EA5, 0x6EC2, 0x6E9F, 0x6F41, 0x6F11, 0x704C, 0x6EEC, 0x6EF8, 0x6EFE, 0x6F3F, 0x6EF2, 0x6F31, 0x6EEF, 0x6F32, 0x6ECC, + 0x6F3E, 0x6F13, 0x6EF7, 0x6F86, 0x6F7A, 0x6F78, 0x6F81, 0x6F80, 0x6F6F, 0x6F5B, 0x6FF3, 0x6F6D, 0x6F82, 0x6F7C, 0x6F58, 0x6F8E, 0x6F91, 0x6FC2, 0x6F66, 0x6FB3, 0x6FA3, 0x6FA1, 0x6FA4, 0x6FB9, 0x6FC6, 0x6FAA, 0x6FDF, 0x6FD5, 0x6FEC, 0x6FD4, 0x6FD8, 0x6FF1, 0x6FEE, 0x6FDB, 0x7009, 0x700B, 0x6FFA, 0x7011, 0x7001, 0x700F, 0x6FFE, 0x701B, 0x701A, 0x6F74, 0x701D, 0x7018, 0x701F, 0x7030, 0x703E, 0x7032, 0x7051, 0x7063, 0x7099, 0x7092, 0x70AF, 0x70F1, 0x70AC, 0x70B8, 0x70B3, 0x70AE, 0x70DF, 0x70CB, 0x70DD, 0x70D9, 0x7109, 0x70FD, 0x711C, 0x7119, 0x7165, 0x7155, 0x7188, 0x7166, 0x7162, 0x714C, 0x7156, 0x716C, 0x718F, 0x71FB, 0x7184, 0x7195, 0x71A8, 0x71AC, 0x71D7, 0x71B9, 0x71BE, 0x71D2, 0x71C9, 0x71D4, 0x71CE, 0x71E0, 0x71EC, 0x71E7, 0x71F5, 0x71FC, + 0x71F9, 0x71FF, 0x720D, 0x7210, 0x721B, 0x7228, 0x722D, 0x722C, 0x7230, 0x7232, 0x723B, 0x723C, 0x723F, 0x7240, 0x7246, 0x724B, 0x7258, 0x7274, 0x727E, 0x7282, 0x7281, 0x7287, 0x7292, 0x7296, 0x72A2, 0x72A7, 0x72B9, 0x72B2, 0x72C3, 0x72C6, 0x72C4, 0x72CE, 0x72D2, 0x72E2, 0x72E0, 0x72E1, 0x72F9, 0x72F7, 0x500F, 0x7317, 0x730A, 0x731C, 0x7316, 0x731D, 0x7334, 0x732F, 0x7329, 0x7325, 0x733E, 0x734E, 0x734F, 0x9ED8, 0x7357, 0x736A, 0x7368, 0x7370, 0x7378, 0x7375, 0x737B, 0x737A, 0x73C8, 0x73B3, 0x73CE, 0x73BB, 0x73C0, 0x73E5, 0x73EE, 0x73DE, 0x74A2, 0x7405, 0x746F, 0x7425, 0x73F8, 0x7432, 0x743A, 0x7455, 0x743F, 0x745F, 0x7459, 0x7441, 0x745C, 0x7469, 0x7470, 0x7463, 0x746A, 0x7476, 0x747E, 0x748B, 0x749E, 0x74A7, 0x74CA, 0x74CF, 0x74D4, 0x73F1, + 0x74E0, 0x74E3, 0x74E7, 0x74E9, 0x74EE, 0x74F2, 0x74F0, 0x74F1, 0x74F8, 0x74F7, 0x7504, 0x7503, 0x7505, 0x750C, 0x750E, 0x750D, 0x7515, 0x7513, 0x751E, 0x7526, 0x752C, 0x753C, 0x7544, 0x754D, 0x754A, 0x7549, 0x755B, 0x7546, 0x755A, 0x7569, 0x7564, 0x7567, 0x756B, 0x756D, 0x7578, 0x7576, 0x7586, 0x7587, 0x7574, 0x758A, 0x7589, 0x7582, 0x7594, 0x759A, 0x759D, 0x75A5, 0x75A3, 0x75C2, 0x75B3, 0x75C3, 0x75B5, 0x75BD, 0x75B8, 0x75BC, 0x75B1, 0x75CD, 0x75CA, 0x75D2, 0x75D9, 0x75E3, 0x75DE, 0x75FE, 0x75FF, 0x75FC, 0x7601, 0x75F0, 0x75FA, 0x75F2, 0x75F3, 0x760B, 0x760D, 0x7609, 0x761F, 0x7627, 0x7620, 0x7621, 0x7622, 0x7624, 0x7634, 0x7630, 0x763B, 0x7647, 0x7648, 0x7646, 0x765C, 0x7658, 0x7661, 0x7662, 0x7668, 0x7669, 0x766A, 0x7667, 0x766C, 0x7670, + 0x7672, 0x7676, 0x7678, 0x767C, 0x7680, 0x7683, 0x7688, 0x768B, 0x768E, 0x7696, 0x7693, 0x7699, 0x769A, 0x76B0, 0x76B4, 0x76B8, 0x76B9, 0x76BA, 0x76C2, 0x76CD, 0x76D6, 0x76D2, 0x76DE, 0x76E1, 0x76E5, 0x76E7, 0x76EA, 0x862F, 0x76FB, 0x7708, 0x7707, 0x7704, 0x7729, 0x7724, 0x771E, 0x7725, 0x7726, 0x771B, 0x7737, 0x7738, 0x7747, 0x775A, 0x7768, 0x776B, 0x775B, 0x7765, 0x777F, 0x777E, 0x7779, 0x778E, 0x778B, 0x7791, 0x77A0, 0x779E, 0x77B0, 0x77B6, 0x77B9, 0x77BF, 0x77BC, 0x77BD, 0x77BB, 0x77C7, 0x77CD, 0x77D7, 0x77DA, 0x77DC, 0x77E3, 0x77EE, 0x77FC, 0x780C, 0x7812, 0x7926, 0x7820, 0x792A, 0x7845, 0x788E, 0x7874, 0x7886, 0x787C, 0x789A, 0x788C, 0x78A3, 0x78B5, 0x78AA, 0x78AF, 0x78D1, 0x78C6, 0x78CB, 0x78D4, 0x78BE, 0x78BC, 0x78C5, 0x78CA, 0x78EC, + 0x78E7, 0x78DA, 0x78FD, 0x78F4, 0x7907, 0x7912, 0x7911, 0x7919, 0x792C, 0x792B, 0x7940, 0x7960, 0x7957, 0x795F, 0x795A, 0x7955, 0x7953, 0x797A, 0x797F, 0x798A, 0x799D, 0x79A7, 0x9F4B, 0x79AA, 0x79AE, 0x79B3, 0x79B9, 0x79BA, 0x79C9, 0x79D5, 0x79E7, 0x79EC, 0x79E1, 0x79E3, 0x7A08, 0x7A0D, 0x7A18, 0x7A19, 0x7A20, 0x7A1F, 0x7980, 0x7A31, 0x7A3B, 0x7A3E, 0x7A37, 0x7A43, 0x7A57, 0x7A49, 0x7A61, 0x7A62, 0x7A69, 0x9F9D, 0x7A70, 0x7A79, 0x7A7D, 0x7A88, 0x7A97, 0x7A95, 0x7A98, 0x7A96, 0x7AA9, 0x7AC8, 0x7AB0, 0x7AB6, 0x7AC5, 0x7AC4, 0x7ABF, 0x9083, 0x7AC7, 0x7ACA, 0x7ACD, 0x7ACF, 0x7AD5, 0x7AD3, 0x7AD9, 0x7ADA, 0x7ADD, 0x7AE1, 0x7AE2, 0x7AE6, 0x7AED, 0x7AF0, 0x7B02, 0x7B0F, 0x7B0A, 0x7B06, 0x7B33, 0x7B18, 0x7B19, 0x7B1E, 0x7B35, 0x7B28, 0x7B36, 0x7B50, + 0x7B7A, 0x7B04, 0x7B4D, 0x7B0B, 0x7B4C, 0x7B45, 0x7B75, 0x7B65, 0x7B74, 0x7B67, 0x7B70, 0x7B71, 0x7B6C, 0x7B6E, 0x7B9D, 0x7B98, 0x7B9F, 0x7B8D, 0x7B9C, 0x7B9A, 0x7B8B, 0x7B92, 0x7B8F, 0x7B5D, 0x7B99, 0x7BCB, 0x7BC1, 0x7BCC, 0x7BCF, 0x7BB4, 0x7BC6, 0x7BDD, 0x7BE9, 0x7C11, 0x7C14, 0x7BE6, 0x7BE5, 0x7C60, 0x7C00, 0x7C07, 0x7C13, 0x7BF3, 0x7BF7, 0x7C17, 0x7C0D, 0x7BF6, 0x7C23, 0x7C27, 0x7C2A, 0x7C1F, 0x7C37, 0x7C2B, 0x7C3D, 0x7C4C, 0x7C43, 0x7C54, 0x7C4F, 0x7C40, 0x7C50, 0x7C58, 0x7C5F, 0x7C64, 0x7C56, 0x7C65, 0x7C6C, 0x7C75, 0x7C83, 0x7C90, 0x7CA4, 0x7CAD, 0x7CA2, 0x7CAB, 0x7CA1, 0x7CA8, 0x7CB3, 0x7CB2, 0x7CB1, 0x7CAE, 0x7CB9, 0x7CBD, 0x7CC0, 0x7CC5, 0x7CC2, 0x7CD8, 0x7CD2, 0x7CDC, 0x7CE2, 0x9B3B, 0x7CEF, 0x7CF2, 0x7CF4, 0x7CF6, 0x7CFA, 0x7D06, + 0x7D02, 0x7D1C, 0x7D15, 0x7D0A, 0x7D45, 0x7D4B, 0x7D2E, 0x7D32, 0x7D3F, 0x7D35, 0x7D46, 0x7D73, 0x7D56, 0x7D4E, 0x7D72, 0x7D68, 0x7D6E, 0x7D4F, 0x7D63, 0x7D93, 0x7D89, 0x7D5B, 0x7D8F, 0x7D7D, 0x7D9B, 0x7DBA, 0x7DAE, 0x7DA3, 0x7DB5, 0x7DC7, 0x7DBD, 0x7DAB, 0x7E3D, 0x7DA2, 0x7DAF, 0x7DDC, 0x7DB8, 0x7D9F, 0x7DB0, 0x7DD8, 0x7DDD, 0x7DE4, 0x7DDE, 0x7DFB, 0x7DF2, 0x7DE1, 0x7E05, 0x7E0A, 0x7E23, 0x7E21, 0x7E12, 0x7E31, 0x7E1F, 0x7E09, 0x7E0B, 0x7E22, 0x7E46, 0x7E66, 0x7E3B, 0x7E35, 0x7E39, 0x7E43, 0x7E37, 0x7E32, 0x7E3A, 0x7E67, 0x7E5D, 0x7E56, 0x7E5E, 0x7E59, 0x7E5A, 0x7E79, 0x7E6A, 0x7E69, 0x7E7C, 0x7E7B, 0x7E83, 0x7DD5, 0x7E7D, 0x8FAE, 0x7E7F, 0x7E88, 0x7E89, 0x7E8C, 0x7E92, 0x7E90, 0x7E93, 0x7E94, 0x7E96, 0x7E8E, 0x7E9B, 0x7E9C, 0x7F38, 0x7F3A, + 0x7F45, 0x7F4C, 0x7F4D, 0x7F4E, 0x7F50, 0x7F51, 0x7F55, 0x7F54, 0x7F58, 0x7F5F, 0x7F60, 0x7F68, 0x7F69, 0x7F67, 0x7F78, 0x7F82, 0x7F86, 0x7F83, 0x7F88, 0x7F87, 0x7F8C, 0x7F94, 0x7F9E, 0x7F9D, 0x7F9A, 0x7FA3, 0x7FAF, 0x7FB2, 0x7FB9, 0x7FAE, 0x7FB6, 0x7FB8, 0x8B71, 0x7FC5, 0x7FC6, 0x7FCA, 0x7FD5, 0x7FD4, 0x7FE1, 0x7FE6, 0x7FE9, 0x7FF3, 0x7FF9, 0x98DC, 0x8006, 0x8004, 0x800B, 0x8012, 0x8018, 0x8019, 0x801C, 0x8021, 0x8028, 0x803F, 0x803B, 0x804A, 0x8046, 0x8052, 0x8058, 0x805A, 0x805F, 0x8062, 0x8068, 0x8073, 0x8072, 0x8070, 0x8076, 0x8079, 0x807D, 0x807F, 0x8084, 0x8086, 0x8085, 0x809B, 0x8093, 0x809A, 0x80AD, 0x5190, 0x80AC, 0x80DB, 0x80E5, 0x80D9, 0x80DD, 0x80C4, 0x80DA, 0x80D6, 0x8109, 0x80EF, 0x80F1, 0x811B, 0x8129, 0x8123, 0x812F, 0x814B, + 0x968B, 0x8146, 0x813E, 0x8153, 0x8151, 0x80FC, 0x8171, 0x816E, 0x8165, 0x8166, 0x8174, 0x8183, 0x8188, 0x818A, 0x8180, 0x8182, 0x81A0, 0x8195, 0x81A4, 0x81A3, 0x815F, 0x8193, 0x81A9, 0x81B0, 0x81B5, 0x81BE, 0x81B8, 0x81BD, 0x81C0, 0x81C2, 0x81BA, 0x81C9, 0x81CD, 0x81D1, 0x81D9, 0x81D8, 0x81C8, 0x81DA, 0x81DF, 0x81E0, 0x81E7, 0x81FA, 0x81FB, 0x81FE, 0x8201, 0x8202, 0x8205, 0x8207, 0x820A, 0x820D, 0x8210, 0x8216, 0x8229, 0x822B, 0x8238, 0x8233, 0x8240, 0x8259, 0x8258, 0x825D, 0x825A, 0x825F, 0x8264, 0x8262, 0x8268, 0x826A, 0x826B, 0x822E, 0x8271, 0x8277, 0x8278, 0x827E, 0x828D, 0x8292, 0x82AB, 0x829F, 0x82BB, 0x82AC, 0x82E1, 0x82E3, 0x82DF, 0x82D2, 0x82F4, 0x82F3, 0x82FA, 0x8393, 0x8303, 0x82FB, 0x82F9, 0x82DE, 0x8306, 0x82DC, 0x8309, 0x82D9, + 0x8335, 0x8334, 0x8316, 0x8332, 0x8331, 0x8340, 0x8339, 0x8350, 0x8345, 0x832F, 0x832B, 0x8317, 0x8318, 0x8385, 0x839A, 0x83AA, 0x839F, 0x83A2, 0x8396, 0x8323, 0x838E, 0x8387, 0x838A, 0x837C, 0x83B5, 0x8373, 0x8375, 0x83A0, 0x8389, 0x83A8, 0x83F4, 0x8413, 0x83EB, 0x83CE, 0x83FD, 0x8403, 0x83D8, 0x840B, 0x83C1, 0x83F7, 0x8407, 0x83E0, 0x83F2, 0x840D, 0x8422, 0x8420, 0x83BD, 0x8438, 0x8506, 0x83FB, 0x846D, 0x842A, 0x843C, 0x855A, 0x8484, 0x8477, 0x846B, 0x84AD, 0x846E, 0x8482, 0x8469, 0x8446, 0x842C, 0x846F, 0x8479, 0x8435, 0x84CA, 0x8462, 0x84B9, 0x84BF, 0x849F, 0x84D9, 0x84CD, 0x84BB, 0x84DA, 0x84D0, 0x84C1, 0x84C6, 0x84D6, 0x84A1, 0x8521, 0x84FF, 0x84F4, 0x8517, 0x8518, 0x852C, 0x851F, 0x8515, 0x8514, 0x84FC, 0x8540, 0x8563, 0x8558, 0x8548, + 0x8541, 0x8602, 0x854B, 0x8555, 0x8580, 0x85A4, 0x8588, 0x8591, 0x858A, 0x85A8, 0x856D, 0x8594, 0x859B, 0x85EA, 0x8587, 0x859C, 0x8577, 0x857E, 0x8590, 0x85C9, 0x85BA, 0x85CF, 0x85B9, 0x85D0, 0x85D5, 0x85DD, 0x85E5, 0x85DC, 0x85F9, 0x860A, 0x8613, 0x860B, 0x85FE, 0x85FA, 0x8606, 0x8622, 0x861A, 0x8630, 0x863F, 0x864D, 0x4E55, 0x8654, 0x865F, 0x8667, 0x8671, 0x8693, 0x86A3, 0x86A9, 0x86AA, 0x868B, 0x868C, 0x86B6, 0x86AF, 0x86C4, 0x86C6, 0x86B0, 0x86C9, 0x8823, 0x86AB, 0x86D4, 0x86DE, 0x86E9, 0x86EC, 0x86DF, 0x86DB, 0x86EF, 0x8712, 0x8706, 0x8708, 0x8700, 0x8703, 0x86FB, 0x8711, 0x8709, 0x870D, 0x86F9, 0x870A, 0x8734, 0x873F, 0x8737, 0x873B, 0x8725, 0x8729, 0x871A, 0x8760, 0x875F, 0x8778, 0x874C, 0x874E, 0x8774, 0x8757, 0x8768, 0x876E, 0x8759, + 0x8753, 0x8763, 0x876A, 0x8805, 0x87A2, 0x879F, 0x8782, 0x87AF, 0x87CB, 0x87BD, 0x87C0, 0x87D0, 0x96D6, 0x87AB, 0x87C4, 0x87B3, 0x87C7, 0x87C6, 0x87BB, 0x87EF, 0x87F2, 0x87E0, 0x880F, 0x880D, 0x87FE, 0x87F6, 0x87F7, 0x880E, 0x87D2, 0x8811, 0x8816, 0x8815, 0x8822, 0x8821, 0x8831, 0x8836, 0x8839, 0x8827, 0x883B, 0x8844, 0x8842, 0x8852, 0x8859, 0x885E, 0x8862, 0x886B, 0x8881, 0x887E, 0x889E, 0x8875, 0x887D, 0x88B5, 0x8872, 0x8882, 0x8897, 0x8892, 0x88AE, 0x8899, 0x88A2, 0x888D, 0x88A4, 0x88B0, 0x88BF, 0x88B1, 0x88C3, 0x88C4, 0x88D4, 0x88D8, 0x88D9, 0x88DD, 0x88F9, 0x8902, 0x88FC, 0x88F4, 0x88E8, 0x88F2, 0x8904, 0x890C, 0x890A, 0x8913, 0x8943, 0x891E, 0x8925, 0x892A, 0x892B, 0x8941, 0x8944, 0x893B, 0x8936, 0x8938, 0x894C, 0x891D, 0x8960, 0x895E, + 0x8966, 0x8964, 0x896D, 0x896A, 0x896F, 0x8974, 0x8977, 0x897E, 0x8983, 0x8988, 0x898A, 0x8993, 0x8998, 0x89A1, 0x89A9, 0x89A6, 0x89AC, 0x89AF, 0x89B2, 0x89BA, 0x89BD, 0x89BF, 0x89C0, 0x89DA, 0x89DC, 0x89DD, 0x89E7, 0x89F4, 0x89F8, 0x8A03, 0x8A16, 0x8A10, 0x8A0C, 0x8A1B, 0x8A1D, 0x8A25, 0x8A36, 0x8A41, 0x8A5B, 0x8A52, 0x8A46, 0x8A48, 0x8A7C, 0x8A6D, 0x8A6C, 0x8A62, 0x8A85, 0x8A82, 0x8A84, 0x8AA8, 0x8AA1, 0x8A91, 0x8AA5, 0x8AA6, 0x8A9A, 0x8AA3, 0x8AC4, 0x8ACD, 0x8AC2, 0x8ADA, 0x8AEB, 0x8AF3, 0x8AE7, 0x8AE4, 0x8AF1, 0x8B14, 0x8AE0, 0x8AE2, 0x8AF7, 0x8ADE, 0x8ADB, 0x8B0C, 0x8B07, 0x8B1A, 0x8AE1, 0x8B16, 0x8B10, 0x8B17, 0x8B20, 0x8B33, 0x97AB, 0x8B26, 0x8B2B, 0x8B3E, 0x8B28, 0x8B41, 0x8B4C, 0x8B4F, 0x8B4E, 0x8B49, 0x8B56, 0x8B5B, 0x8B5A, 0x8B6B, + 0x8B5F, 0x8B6C, 0x8B6F, 0x8B74, 0x8B7D, 0x8B80, 0x8B8C, 0x8B8E, 0x8B92, 0x8B93, 0x8B96, 0x8B99, 0x8B9A, 0x8C3A, 0x8C41, 0x8C3F, 0x8C48, 0x8C4C, 0x8C4E, 0x8C50, 0x8C55, 0x8C62, 0x8C6C, 0x8C78, 0x8C7A, 0x8C82, 0x8C89, 0x8C85, 0x8C8A, 0x8C8D, 0x8C8E, 0x8C94, 0x8C7C, 0x8C98, 0x621D, 0x8CAD, 0x8CAA, 0x8CBD, 0x8CB2, 0x8CB3, 0x8CAE, 0x8CB6, 0x8CC8, 0x8CC1, 0x8CE4, 0x8CE3, 0x8CDA, 0x8CFD, 0x8CFA, 0x8CFB, 0x8D04, 0x8D05, 0x8D0A, 0x8D07, 0x8D0F, 0x8D0D, 0x8D10, 0x9F4E, 0x8D13, 0x8CCD, 0x8D14, 0x8D16, 0x8D67, 0x8D6D, 0x8D71, 0x8D73, 0x8D81, 0x8D99, 0x8DC2, 0x8DBE, 0x8DBA, 0x8DCF, 0x8DDA, 0x8DD6, 0x8DCC, 0x8DDB, 0x8DCB, 0x8DEA, 0x8DEB, 0x8DDF, 0x8DE3, 0x8DFC, 0x8E08, 0x8E09, 0x8DFF, 0x8E1D, 0x8E1E, 0x8E10, 0x8E1F, 0x8E42, 0x8E35, 0x8E30, 0x8E34, 0x8E4A, + 0x8E47, 0x8E49, 0x8E4C, 0x8E50, 0x8E48, 0x8E59, 0x8E64, 0x8E60, 0x8E2A, 0x8E63, 0x8E55, 0x8E76, 0x8E72, 0x8E7C, 0x8E81, 0x8E87, 0x8E85, 0x8E84, 0x8E8B, 0x8E8A, 0x8E93, 0x8E91, 0x8E94, 0x8E99, 0x8EAA, 0x8EA1, 0x8EAC, 0x8EB0, 0x8EC6, 0x8EB1, 0x8EBE, 0x8EC5, 0x8EC8, 0x8ECB, 0x8EDB, 0x8EE3, 0x8EFC, 0x8EFB, 0x8EEB, 0x8EFE, 0x8F0A, 0x8F05, 0x8F15, 0x8F12, 0x8F19, 0x8F13, 0x8F1C, 0x8F1F, 0x8F1B, 0x8F0C, 0x8F26, 0x8F33, 0x8F3B, 0x8F39, 0x8F45, 0x8F42, 0x8F3E, 0x8F4C, 0x8F49, 0x8F46, 0x8F4E, 0x8F57, 0x8F5C, 0x8F62, 0x8F63, 0x8F64, 0x8F9C, 0x8F9F, 0x8FA3, 0x8FAD, 0x8FAF, 0x8FB7, 0x8FDA, 0x8FE5, 0x8FE2, 0x8FEA, 0x8FEF, 0x9087, 0x8FF4, 0x9005, 0x8FF9, 0x8FFA, 0x9011, 0x9015, 0x9021, 0x900D, 0x901E, 0x9016, 0x900B, 0x9027, 0x9036, 0x9035, 0x9039, 0x8FF8, + 0x904F, 0x9050, 0x9051, 0x9052, 0x900E, 0x9049, 0x903E, 0x9056, 0x9058, 0x905E, 0x9068, 0x906F, 0x9076, 0x96A8, 0x9072, 0x9082, 0x907D, 0x9081, 0x9080, 0x908A, 0x9089, 0x908F, 0x90A8, 0x90AF, 0x90B1, 0x90B5, 0x90E2, 0x90E4, 0x6248, 0x90DB, 0x9102, 0x9112, 0x9119, 0x9132, 0x9130, 0x914A, 0x9156, 0x9158, 0x9163, 0x9165, 0x9169, 0x9173, 0x9172, 0x918B, 0x9189, 0x9182, 0x91A2, 0x91AB, 0x91AF, 0x91AA, 0x91B5, 0x91B4, 0x91BA, 0x91C0, 0x91C1, 0x91C9, 0x91CB, 0x91D0, 0x91D6, 0x91DF, 0x91E1, 0x91DB, 0x91FC, 0x91F5, 0x91F6, 0x921E, 0x91FF, 0x9214, 0x922C, 0x9215, 0x9211, 0x925E, 0x9257, 0x9245, 0x9249, 0x9264, 0x9248, 0x9295, 0x923F, 0x924B, 0x9250, 0x929C, 0x9296, 0x9293, 0x929B, 0x925A, 0x92CF, 0x92B9, 0x92B7, 0x92E9, 0x930F, 0x92FA, 0x9344, 0x932E, + 0x9319, 0x9322, 0x931A, 0x9323, 0x933A, 0x9335, 0x933B, 0x935C, 0x9360, 0x937C, 0x936E, 0x9356, 0x93B0, 0x93AC, 0x93AD, 0x9394, 0x93B9, 0x93D6, 0x93D7, 0x93E8, 0x93E5, 0x93D8, 0x93C3, 0x93DD, 0x93D0, 0x93C8, 0x93E4, 0x941A, 0x9414, 0x9413, 0x9403, 0x9407, 0x9410, 0x9436, 0x942B, 0x9435, 0x9421, 0x943A, 0x9441, 0x9452, 0x9444, 0x945B, 0x9460, 0x9462, 0x945E, 0x946A, 0x9229, 0x9470, 0x9475, 0x9477, 0x947D, 0x945A, 0x947C, 0x947E, 0x9481, 0x947F, 0x9582, 0x9587, 0x958A, 0x9594, 0x9596, 0x9598, 0x9599, 0x95A0, 0x95A8, 0x95A7, 0x95AD, 0x95BC, 0x95BB, 0x95B9, 0x95BE, 0x95CA, 0x6FF6, 0x95C3, 0x95CD, 0x95CC, 0x95D5, 0x95D4, 0x95D6, 0x95DC, 0x95E1, 0x95E5, 0x95E2, 0x9621, 0x9628, 0x962E, 0x962F, 0x9642, 0x964C, 0x964F, 0x964B, 0x9677, 0x965C, 0x965E, + 0x965D, 0x965F, 0x9666, 0x9672, 0x966C, 0x968D, 0x9698, 0x9695, 0x9697, 0x96AA, 0x96A7, 0x96B1, 0x96B2, 0x96B0, 0x96B4, 0x96B6, 0x96B8, 0x96B9, 0x96CE, 0x96CB, 0x96C9, 0x96CD, 0x894D, 0x96DC, 0x970D, 0x96D5, 0x96F9, 0x9704, 0x9706, 0x9708, 0x9713, 0x970E, 0x9711, 0x970F, 0x9716, 0x9719, 0x9724, 0x972A, 0x9730, 0x9739, 0x973D, 0x973E, 0x9744, 0x9746, 0x9748, 0x9742, 0x9749, 0x975C, 0x9760, 0x9764, 0x9766, 0x9768, 0x52D2, 0x976B, 0x9771, 0x9779, 0x9785, 0x977C, 0x9781, 0x977A, 0x9786, 0x978B, 0x978F, 0x9790, 0x979C, 0x97A8, 0x97A6, 0x97A3, 0x97B3, 0x97B4, 0x97C3, 0x97C6, 0x97C8, 0x97CB, 0x97DC, 0x97ED, 0x9F4F, 0x97F2, 0x7ADF, 0x97F6, 0x97F5, 0x980F, 0x980C, 0x9838, 0x9824, 0x9821, 0x9837, 0x983D, 0x9846, 0x984F, 0x984B, 0x986B, 0x986F, 0x9870, + 0x9871, 0x9874, 0x9873, 0x98AA, 0x98AF, 0x98B1, 0x98B6, 0x98C4, 0x98C3, 0x98C6, 0x98E9, 0x98EB, 0x9903, 0x9909, 0x9912, 0x9914, 0x9918, 0x9921, 0x991D, 0x991E, 0x9924, 0x9920, 0x992C, 0x992E, 0x993D, 0x993E, 0x9942, 0x9949, 0x9945, 0x9950, 0x994B, 0x9951, 0x9952, 0x994C, 0x9955, 0x9997, 0x9998, 0x99A5, 0x99AD, 0x99AE, 0x99BC, 0x99DF, 0x99DB, 0x99DD, 0x99D8, 0x99D1, 0x99ED, 0x99EE, 0x99F1, 0x99F2, 0x99FB, 0x99F8, 0x9A01, 0x9A0F, 0x9A05, 0x99E2, 0x9A19, 0x9A2B, 0x9A37, 0x9A45, 0x9A42, 0x9A40, 0x9A43, 0x9A3E, 0x9A55, 0x9A4D, 0x9A5B, 0x9A57, 0x9A5F, 0x9A62, 0x9A65, 0x9A64, 0x9A69, 0x9A6B, 0x9A6A, 0x9AAD, 0x9AB0, 0x9ABC, 0x9AC0, 0x9ACF, 0x9AD1, 0x9AD3, 0x9AD4, 0x9ADE, 0x9ADF, 0x9AE2, 0x9AE3, 0x9AE6, 0x9AEF, 0x9AEB, 0x9AEE, 0x9AF4, 0x9AF1, 0x9AF7, + 0x9AFB, 0x9B06, 0x9B18, 0x9B1A, 0x9B1F, 0x9B22, 0x9B23, 0x9B25, 0x9B27, 0x9B28, 0x9B29, 0x9B2A, 0x9B2E, 0x9B2F, 0x9B32, 0x9B44, 0x9B43, 0x9B4F, 0x9B4D, 0x9B4E, 0x9B51, 0x9B58, 0x9B74, 0x9B93, 0x9B83, 0x9B91, 0x9B96, 0x9B97, 0x9B9F, 0x9BA0, 0x9BA8, 0x9BB4, 0x9BC0, 0x9BCA, 0x9BB9, 0x9BC6, 0x9BCF, 0x9BD1, 0x9BD2, 0x9BE3, 0x9BE2, 0x9BE4, 0x9BD4, 0x9BE1, 0x9C3A, 0x9BF2, 0x9BF1, 0x9BF0, 0x9C15, 0x9C14, 0x9C09, 0x9C13, 0x9C0C, 0x9C06, 0x9C08, 0x9C12, 0x9C0A, 0x9C04, 0x9C2E, 0x9C1B, 0x9C25, 0x9C24, 0x9C21, 0x9C30, 0x9C47, 0x9C32, 0x9C46, 0x9C3E, 0x9C5A, 0x9C60, 0x9C67, 0x9C76, 0x9C78, 0x9CE7, 0x9CEC, 0x9CF0, 0x9D09, 0x9D08, 0x9CEB, 0x9D03, 0x9D06, 0x9D2A, 0x9D26, 0x9DAF, 0x9D23, 0x9D1F, 0x9D44, 0x9D15, 0x9D12, 0x9D41, 0x9D3F, 0x9D3E, 0x9D46, 0x9D48, + 0x9D5D, 0x9D5E, 0x9D64, 0x9D51, 0x9D50, 0x9D59, 0x9D72, 0x9D89, 0x9D87, 0x9DAB, 0x9D6F, 0x9D7A, 0x9D9A, 0x9DA4, 0x9DA9, 0x9DB2, 0x9DC4, 0x9DC1, 0x9DBB, 0x9DB8, 0x9DBA, 0x9DC6, 0x9DCF, 0x9DC2, 0x9DD9, 0x9DD3, 0x9DF8, 0x9DE6, 0x9DED, 0x9DEF, 0x9DFD, 0x9E1A, 0x9E1B, 0x9E1E, 0x9E75, 0x9E79, 0x9E7D, 0x9E81, 0x9E88, 0x9E8B, 0x9E8C, 0x9E92, 0x9E95, 0x9E91, 0x9E9D, 0x9EA5, 0x9EA9, 0x9EB8, 0x9EAA, 0x9EAD, 0x9761, 0x9ECC, 0x9ECE, 0x9ECF, 0x9ED0, 0x9ED4, 0x9EDC, 0x9EDE, 0x9EDD, 0x9EE0, 0x9EE5, 0x9EE8, 0x9EEF, 0x9EF4, 0x9EF6, 0x9EF7, 0x9EF9, 0x9EFB, 0x9EFC, 0x9EFD, 0x9F07, 0x9F08, 0x76B7, 0x9F15, 0x9F21, 0x9F2C, 0x9F3E, 0x9F4A, 0x9F52, 0x9F54, 0x9F63, 0x9F5F, 0x9F60, 0x9F61, 0x9F66, 0x9F67, 0x9F6C, 0x9F6A, 0x9F77, 0x9F72, 0x9F76, 0x9F95, 0x9F9C, 0x9FA0, + 0x582F, 0x69C7, 0x9059, 0x7464, 0x51DC, 0x7199, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x7E8A, 0x891C, 0x9348, 0x9288, 0x84DC, 0x4FC9, 0x70BB, 0x6631, 0x68C8, 0x92F9, 0x66FB, 0x5F45, 0x4E28, 0x4EE1, 0x4EFC, 0x4F00, 0x4F03, 0x4F39, 0x4F56, 0x4F92, 0x4F8A, 0x4F9A, 0x4F94, 0x4FCD, 0x5040, 0x5022, 0x4FFF, 0x501E, 0x5046, 0x5070, 0x5042, 0x5094, 0x50F4, 0x50D8, 0x514A, 0x5164, 0x519D, 0x51BE, 0x51EC, 0x5215, 0x529C, 0x52A6, 0x52C0, 0x52DB, 0x5300, 0x5307, 0x5324, 0x5372, 0x5393, 0x53B2, 0x53DD, 0xFA0E, 0x549C, 0x548A, 0x54A9, 0x54FF, 0x5586, 0x5759, 0x5765, 0x57AC, 0x57C8, 0x57C7, 0xFA0F, 0xFA10, 0x589E, 0x58B2, 0x590B, 0x5953, 0x595B, 0x595D, 0x5963, 0x59A4, 0x59BA, 0x5B56, 0x5BC0, 0x752F, 0x5BD8, 0x5BEC, 0x5C1E, 0x5CA6, 0x5CBA, 0x5CF5, 0x5D27, 0x5D53, 0xFA11, 0x5D42, 0x5D6D, 0x5DB8, 0x5DB9, 0x5DD0, 0x5F21, 0x5F34, 0x5F67, 0x5FB7, + 0x5FDE, 0x605D, 0x6085, 0x608A, 0x60DE, 0x60D5, 0x6120, 0x60F2, 0x6111, 0x6137, 0x6130, 0x6198, 0x6213, 0x62A6, 0x63F5, 0x6460, 0x649D, 0x64CE, 0x654E, 0x6600, 0x6615, 0x663B, 0x6609, 0x662E, 0x661E, 0x6624, 0x6665, 0x6657, 0x6659, 0xFA12, 0x6673, 0x6699, 0x66A0, 0x66B2, 0x66BF, 0x66FA, 0x670E, 0xF929, 0x6766, 0x67BB, 0x6852, 0x67C0, 0x6801, 0x6844, 0x68CF, 0xFA13, 0x6968, 0xFA14, 0x6998, 0x69E2, 0x6A30, 0x6A6B, 0x6A46, 0x6A73, 0x6A7E, 0x6AE2, 0x6AE4, 0x6BD6, 0x6C3F, 0x6C5C, 0x6C86, 0x6C6F, 0x6CDA, 0x6D04, 0x6D87, 0x6D6F, 0x6D96, 0x6DAC, 0x6DCF, 0x6DF8, 0x6DF2, 0x6DFC, 0x6E39, 0x6E5C, 0x6E27, 0x6E3C, 0x6EBF, 0x6F88, 0x6FB5, 0x6FF5, 0x7005, 0x7007, 0x7028, 0x7085, 0x70AB, 0x710F, 0x7104, 0x715C, 0x7146, 0x7147, 0xFA15, 0x71C1, 0x71FE, 0x72B1, + 0x72BE, 0x7324, 0xFA16, 0x7377, 0x73BD, 0x73C9, 0x73D6, 0x73E3, 0x73D2, 0x7407, 0x73F5, 0x7426, 0x742A, 0x7429, 0x742E, 0x7462, 0x7489, 0x749F, 0x7501, 0x756F, 0x7682, 0x769C, 0x769E, 0x769B, 0x76A6, 0xFA17, 0x7746, 0x52AF, 0x7821, 0x784E, 0x7864, 0x787A, 0x7930, 0xFA18, 0xFA19, 0xFA1A, 0x7994, 0xFA1B, 0x799B, 0x7AD1, 0x7AE7, 0xFA1C, 0x7AEB, 0x7B9E, 0xFA1D, 0x7D48, 0x7D5C, 0x7DB7, 0x7DA0, 0x7DD6, 0x7E52, 0x7F47, 0x7FA1, 0xFA1E, 0x8301, 0x8362, 0x837F, 0x83C7, 0x83F6, 0x8448, 0x84B4, 0x8553, 0x8559, 0x856B, 0xFA1F, 0x85B0, 0xFA20, 0xFA21, 0x8807, 0x88F5, 0x8A12, 0x8A37, 0x8A79, 0x8AA7, 0x8ABE, 0x8ADF, 0xFA22, 0x8AF6, 0x8B53, 0x8B7F, 0x8CF0, 0x8CF4, 0x8D12, 0x8D76, 0xFA23, 0x8ECF, 0xFA24, 0xFA25, 0x9067, 0x90DE, 0xFA26, 0x9115, 0x9127, 0x91DA, + 0x91D7, 0x91DE, 0x91ED, 0x91EE, 0x91E4, 0x91E5, 0x9206, 0x9210, 0x920A, 0x923A, 0x9240, 0x923C, 0x924E, 0x9259, 0x9251, 0x9239, 0x9267, 0x92A7, 0x9277, 0x9278, 0x92E7, 0x92D7, 0x92D9, 0x92D0, 0xFA27, 0x92D5, 0x92E0, 0x92D3, 0x9325, 0x9321, 0x92FB, 0xFA28, 0x931E, 0x92FF, 0x931D, 0x9302, 0x9370, 0x9357, 0x93A4, 0x93C6, 0x93DE, 0x93F8, 0x9431, 0x9445, 0x9448, 0x9592, 0xF9DC, 0xFA29, 0x969D, 0x96AF, 0x9733, 0x973B, 0x9743, 0x974D, 0x974F, 0x9751, 0x9755, 0x9857, 0x9865, 0xFA2A, 0xFA2B, 0x9927, 0xFA2C, 0x999E, 0x9A4E, 0x9AD9, 0x9ADC, 0x9B75, 0x9B72, 0x9B8F, 0x9BB1, 0x9BBB, 0x9C00, 0x9D70, 0x9D6B, 0xFA2D, 0x9E19, 0x9ED1, 0x0000, 0x0000, 0x2170, 0x2171, 0x2172, 0x2173, 0x2174, 0x2175, 0x2176, 0x2177, 0x2178, 0x2179, 0xFFE2, 0xFFE4, 0xFF07, 0xFF02, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, +}; + +} diff --git a/core/libs/prx/libSceCesCs/JisX0212.hpp b/core/libs/prx/libSceCesCs/JisX0212.hpp new file mode 100644 index 0000000000..38ddf4cc7f --- /dev/null +++ b/core/libs/prx/libSceCesCs/JisX0212.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include + +namespace JisX0212 { + +// JIS X 0212-1990 to Unicode, from the Unicode consortium's JIS0212.TXT (Format A, +// table version 2.0). The index is the EUC-JP row and cell bytes minus 0xA1, as in +// JisX0208.hpp; a three-byte EUC-JP sequence 0x8F row cell looks up TO_UCS2 +// [(row - 0xA1) * 94 + (cell - 0xA1)]. 6067 of the cells are mapped; the rest are 0. + +inline constexpr uint16_t TO_UCS2[94 * 94] = { + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x02D8, 0x02C7, 0x00B8, 0x02D9, 0x02DD, 0x00AF, 0x02DB, 0x02DA, 0x007E, 0x0384, 0x0385, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x00A1, 0x00A6, 0x00BF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x00BA, 0x00AA, 0x00A9, 0x00AE, 0x2122, 0x00A4, 0x2116, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0386, 0x0388, 0x0389, 0x038A, 0x03AA, 0x0000, 0x038C, 0x0000, 0x038E, 0x03AB, 0x0000, 0x038F, 0x0000, 0x0000, 0x0000, 0x0000, 0x03AC, 0x03AD, 0x03AE, 0x03AF, 0x03CA, 0x0390, 0x03CC, 0x03C2, 0x03CD, 0x03CB, 0x03B0, 0x03CE, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0402, 0x0403, 0x0404, 0x0405, 0x0406, 0x0407, 0x0408, 0x0409, 0x040A, 0x040B, 0x040C, 0x040E, 0x040F, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0452, 0x0453, 0x0454, 0x0455, 0x0456, 0x0457, 0x0458, 0x0459, 0x045A, 0x045B, 0x045C, 0x045E, 0x045F, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x00C6, 0x0110, 0x0000, 0x0126, 0x0000, 0x0132, 0x0000, 0x0141, 0x013F, 0x0000, 0x014A, 0x00D8, 0x0152, 0x0000, 0x0166, 0x00DE, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x00E6, 0x0111, 0x00F0, 0x0127, 0x0131, 0x0133, 0x0138, 0x0142, 0x0140, 0x0149, 0x014B, 0x00F8, 0x0153, 0x00DF, 0x0167, 0x00FE, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x00C1, 0x00C0, 0x00C4, 0x00C2, 0x0102, 0x01CD, 0x0100, 0x0104, 0x00C5, 0x00C3, 0x0106, 0x0108, 0x010C, 0x00C7, 0x010A, 0x010E, 0x00C9, 0x00C8, 0x00CB, 0x00CA, 0x011A, 0x0116, 0x0112, 0x0118, 0x0000, 0x011C, 0x011E, 0x0122, 0x0120, 0x0124, 0x00CD, 0x00CC, 0x00CF, 0x00CE, 0x01CF, 0x0130, 0x012A, 0x012E, 0x0128, 0x0134, 0x0136, 0x0139, 0x013D, 0x013B, 0x0143, 0x0147, 0x0145, 0x00D1, 0x00D3, 0x00D2, 0x00D6, 0x00D4, 0x01D1, 0x0150, 0x014C, 0x00D5, 0x0154, 0x0158, 0x0156, 0x015A, 0x015C, 0x0160, 0x015E, 0x0164, 0x0162, 0x00DA, 0x00D9, 0x00DC, 0x00DB, 0x016C, 0x01D3, 0x0170, 0x016A, 0x0172, 0x016E, 0x0168, 0x01D7, 0x01DB, 0x01D9, 0x01D5, 0x0174, 0x00DD, 0x0178, 0x0176, 0x0179, 0x017D, 0x017B, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x00E1, 0x00E0, 0x00E4, 0x00E2, 0x0103, 0x01CE, 0x0101, 0x0105, 0x00E5, 0x00E3, 0x0107, 0x0109, 0x010D, 0x00E7, 0x010B, 0x010F, 0x00E9, 0x00E8, 0x00EB, 0x00EA, 0x011B, 0x0117, 0x0113, 0x0119, 0x01F5, 0x011D, 0x011F, 0x0000, 0x0121, 0x0125, 0x00ED, 0x00EC, 0x00EF, 0x00EE, 0x01D0, 0x0000, 0x012B, 0x012F, 0x0129, 0x0135, 0x0137, 0x013A, 0x013E, 0x013C, 0x0144, 0x0148, 0x0146, 0x00F1, 0x00F3, 0x00F2, 0x00F6, 0x00F4, 0x01D2, 0x0151, 0x014D, 0x00F5, 0x0155, 0x0159, 0x0157, 0x015B, 0x015D, 0x0161, 0x015F, 0x0165, 0x0163, 0x00FA, 0x00F9, 0x00FC, 0x00FB, 0x016D, 0x01D4, 0x0171, 0x016B, 0x0173, 0x016F, 0x0169, 0x01D8, 0x01DC, 0x01DA, 0x01D6, 0x0175, 0x00FD, 0x00FF, 0x0177, 0x017A, 0x017E, 0x017C, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x4E02, 0x4E04, 0x4E05, 0x4E0C, 0x4E12, 0x4E1F, 0x4E23, 0x4E24, 0x4E28, 0x4E2B, 0x4E2E, 0x4E2F, 0x4E30, 0x4E35, 0x4E40, 0x4E41, 0x4E44, 0x4E47, 0x4E51, 0x4E5A, 0x4E5C, 0x4E63, 0x4E68, 0x4E69, 0x4E74, 0x4E75, 0x4E79, 0x4E7F, 0x4E8D, 0x4E96, 0x4E97, 0x4E9D, 0x4EAF, 0x4EB9, 0x4EC3, 0x4ED0, 0x4EDA, 0x4EDB, 0x4EE0, 0x4EE1, 0x4EE2, 0x4EE8, 0x4EEF, 0x4EF1, 0x4EF3, 0x4EF5, 0x4EFD, 0x4EFE, 0x4EFF, 0x4F00, 0x4F02, 0x4F03, 0x4F08, 0x4F0B, 0x4F0C, 0x4F12, 0x4F15, 0x4F16, 0x4F17, 0x4F19, 0x4F2E, 0x4F31, 0x4F60, 0x4F33, 0x4F35, 0x4F37, 0x4F39, 0x4F3B, 0x4F3E, 0x4F40, 0x4F42, 0x4F48, 0x4F49, 0x4F4B, 0x4F4C, 0x4F52, 0x4F54, 0x4F56, 0x4F58, 0x4F5F, 0x4F63, 0x4F6A, 0x4F6C, 0x4F6E, 0x4F71, 0x4F77, 0x4F78, 0x4F79, 0x4F7A, 0x4F7D, 0x4F7E, 0x4F81, 0x4F82, 0x4F84, + 0x4F85, 0x4F89, 0x4F8A, 0x4F8C, 0x4F8E, 0x4F90, 0x4F92, 0x4F93, 0x4F94, 0x4F97, 0x4F99, 0x4F9A, 0x4F9E, 0x4F9F, 0x4FB2, 0x4FB7, 0x4FB9, 0x4FBB, 0x4FBC, 0x4FBD, 0x4FBE, 0x4FC0, 0x4FC1, 0x4FC5, 0x4FC6, 0x4FC8, 0x4FC9, 0x4FCB, 0x4FCC, 0x4FCD, 0x4FCF, 0x4FD2, 0x4FDC, 0x4FE0, 0x4FE2, 0x4FF0, 0x4FF2, 0x4FFC, 0x4FFD, 0x4FFF, 0x5000, 0x5001, 0x5004, 0x5007, 0x500A, 0x500C, 0x500E, 0x5010, 0x5013, 0x5017, 0x5018, 0x501B, 0x501C, 0x501D, 0x501E, 0x5022, 0x5027, 0x502E, 0x5030, 0x5032, 0x5033, 0x5035, 0x5040, 0x5041, 0x5042, 0x5045, 0x5046, 0x504A, 0x504C, 0x504E, 0x5051, 0x5052, 0x5053, 0x5057, 0x5059, 0x505F, 0x5060, 0x5062, 0x5063, 0x5066, 0x5067, 0x506A, 0x506D, 0x5070, 0x5071, 0x503B, 0x5081, 0x5083, 0x5084, 0x5086, 0x508A, 0x508E, 0x508F, 0x5090, + 0x5092, 0x5093, 0x5094, 0x5096, 0x509B, 0x509C, 0x509E, 0x509F, 0x50A0, 0x50A1, 0x50A2, 0x50AA, 0x50AF, 0x50B0, 0x50B9, 0x50BA, 0x50BD, 0x50C0, 0x50C3, 0x50C4, 0x50C7, 0x50CC, 0x50CE, 0x50D0, 0x50D3, 0x50D4, 0x50D8, 0x50DC, 0x50DD, 0x50DF, 0x50E2, 0x50E4, 0x50E6, 0x50E8, 0x50E9, 0x50EF, 0x50F1, 0x50F6, 0x50FA, 0x50FE, 0x5103, 0x5106, 0x5107, 0x5108, 0x510B, 0x510C, 0x510D, 0x510E, 0x50F2, 0x5110, 0x5117, 0x5119, 0x511B, 0x511C, 0x511D, 0x511E, 0x5123, 0x5127, 0x5128, 0x512C, 0x512D, 0x512F, 0x5131, 0x5133, 0x5134, 0x5135, 0x5138, 0x5139, 0x5142, 0x514A, 0x514F, 0x5153, 0x5155, 0x5157, 0x5158, 0x515F, 0x5164, 0x5166, 0x517E, 0x5183, 0x5184, 0x518B, 0x518E, 0x5198, 0x519D, 0x51A1, 0x51A3, 0x51AD, 0x51B8, 0x51BA, 0x51BC, 0x51BE, 0x51BF, 0x51C2, + 0x51C8, 0x51CF, 0x51D1, 0x51D2, 0x51D3, 0x51D5, 0x51D8, 0x51DE, 0x51E2, 0x51E5, 0x51EE, 0x51F2, 0x51F3, 0x51F4, 0x51F7, 0x5201, 0x5202, 0x5205, 0x5212, 0x5213, 0x5215, 0x5216, 0x5218, 0x5222, 0x5228, 0x5231, 0x5232, 0x5235, 0x523C, 0x5245, 0x5249, 0x5255, 0x5257, 0x5258, 0x525A, 0x525C, 0x525F, 0x5260, 0x5261, 0x5266, 0x526E, 0x5277, 0x5278, 0x5279, 0x5280, 0x5282, 0x5285, 0x528A, 0x528C, 0x5293, 0x5295, 0x5296, 0x5297, 0x5298, 0x529A, 0x529C, 0x52A4, 0x52A5, 0x52A6, 0x52A7, 0x52AF, 0x52B0, 0x52B6, 0x52B7, 0x52B8, 0x52BA, 0x52BB, 0x52BD, 0x52C0, 0x52C4, 0x52C6, 0x52C8, 0x52CC, 0x52CF, 0x52D1, 0x52D4, 0x52D6, 0x52DB, 0x52DC, 0x52E1, 0x52E5, 0x52E8, 0x52E9, 0x52EA, 0x52EC, 0x52F0, 0x52F1, 0x52F4, 0x52F6, 0x52F7, 0x5300, 0x5303, 0x530A, 0x530B, + 0x530C, 0x5311, 0x5313, 0x5318, 0x531B, 0x531C, 0x531E, 0x531F, 0x5325, 0x5327, 0x5328, 0x5329, 0x532B, 0x532C, 0x532D, 0x5330, 0x5332, 0x5335, 0x533C, 0x533D, 0x533E, 0x5342, 0x534C, 0x534B, 0x5359, 0x535B, 0x5361, 0x5363, 0x5365, 0x536C, 0x536D, 0x5372, 0x5379, 0x537E, 0x5383, 0x5387, 0x5388, 0x538E, 0x5393, 0x5394, 0x5399, 0x539D, 0x53A1, 0x53A4, 0x53AA, 0x53AB, 0x53AF, 0x53B2, 0x53B4, 0x53B5, 0x53B7, 0x53B8, 0x53BA, 0x53BD, 0x53C0, 0x53C5, 0x53CF, 0x53D2, 0x53D3, 0x53D5, 0x53DA, 0x53DD, 0x53DE, 0x53E0, 0x53E6, 0x53E7, 0x53F5, 0x5402, 0x5413, 0x541A, 0x5421, 0x5427, 0x5428, 0x542A, 0x542F, 0x5431, 0x5434, 0x5435, 0x5443, 0x5444, 0x5447, 0x544D, 0x544F, 0x545E, 0x5462, 0x5464, 0x5466, 0x5467, 0x5469, 0x546B, 0x546D, 0x546E, 0x5474, 0x547F, + 0x5481, 0x5483, 0x5485, 0x5488, 0x5489, 0x548D, 0x5491, 0x5495, 0x5496, 0x549C, 0x549F, 0x54A1, 0x54A6, 0x54A7, 0x54A9, 0x54AA, 0x54AD, 0x54AE, 0x54B1, 0x54B7, 0x54B9, 0x54BA, 0x54BB, 0x54BF, 0x54C6, 0x54CA, 0x54CD, 0x54CE, 0x54E0, 0x54EA, 0x54EC, 0x54EF, 0x54F6, 0x54FC, 0x54FE, 0x54FF, 0x5500, 0x5501, 0x5505, 0x5508, 0x5509, 0x550C, 0x550D, 0x550E, 0x5515, 0x552A, 0x552B, 0x5532, 0x5535, 0x5536, 0x553B, 0x553C, 0x553D, 0x5541, 0x5547, 0x5549, 0x554A, 0x554D, 0x5550, 0x5551, 0x5558, 0x555A, 0x555B, 0x555E, 0x5560, 0x5561, 0x5564, 0x5566, 0x557F, 0x5581, 0x5582, 0x5586, 0x5588, 0x558E, 0x558F, 0x5591, 0x5592, 0x5593, 0x5594, 0x5597, 0x55A3, 0x55A4, 0x55AD, 0x55B2, 0x55BF, 0x55C1, 0x55C3, 0x55C6, 0x55C9, 0x55CB, 0x55CC, 0x55CE, 0x55D1, 0x55D2, + 0x55D3, 0x55D7, 0x55D8, 0x55DB, 0x55DE, 0x55E2, 0x55E9, 0x55F6, 0x55FF, 0x5605, 0x5608, 0x560A, 0x560D, 0x560E, 0x560F, 0x5610, 0x5611, 0x5612, 0x5619, 0x562C, 0x5630, 0x5633, 0x5635, 0x5637, 0x5639, 0x563B, 0x563C, 0x563D, 0x563F, 0x5640, 0x5641, 0x5643, 0x5644, 0x5646, 0x5649, 0x564B, 0x564D, 0x564F, 0x5654, 0x565E, 0x5660, 0x5661, 0x5662, 0x5663, 0x5666, 0x5669, 0x566D, 0x566F, 0x5671, 0x5672, 0x5675, 0x5684, 0x5685, 0x5688, 0x568B, 0x568C, 0x5695, 0x5699, 0x569A, 0x569D, 0x569E, 0x569F, 0x56A6, 0x56A7, 0x56A8, 0x56A9, 0x56AB, 0x56AC, 0x56AD, 0x56B1, 0x56B3, 0x56B7, 0x56BE, 0x56C5, 0x56C9, 0x56CA, 0x56CB, 0x56CF, 0x56D0, 0x56CC, 0x56CD, 0x56D9, 0x56DC, 0x56DD, 0x56DF, 0x56E1, 0x56E4, 0x56E5, 0x56E6, 0x56E7, 0x56E8, 0x56F1, 0x56EB, 0x56ED, + 0x56F6, 0x56F7, 0x5701, 0x5702, 0x5707, 0x570A, 0x570C, 0x5711, 0x5715, 0x571A, 0x571B, 0x571D, 0x5720, 0x5722, 0x5723, 0x5724, 0x5725, 0x5729, 0x572A, 0x572C, 0x572E, 0x572F, 0x5733, 0x5734, 0x573D, 0x573E, 0x573F, 0x5745, 0x5746, 0x574C, 0x574D, 0x5752, 0x5762, 0x5765, 0x5767, 0x5768, 0x576B, 0x576D, 0x576E, 0x576F, 0x5770, 0x5771, 0x5773, 0x5774, 0x5775, 0x5777, 0x5779, 0x577A, 0x577B, 0x577C, 0x577E, 0x5781, 0x5783, 0x578C, 0x5794, 0x5797, 0x5799, 0x579A, 0x579C, 0x579D, 0x579E, 0x579F, 0x57A1, 0x5795, 0x57A7, 0x57A8, 0x57A9, 0x57AC, 0x57B8, 0x57BD, 0x57C7, 0x57C8, 0x57CC, 0x57CF, 0x57D5, 0x57DD, 0x57DE, 0x57E4, 0x57E6, 0x57E7, 0x57E9, 0x57ED, 0x57F0, 0x57F5, 0x57F6, 0x57F8, 0x57FD, 0x57FE, 0x57FF, 0x5803, 0x5804, 0x5808, 0x5809, 0x57E1, + 0x580C, 0x580D, 0x581B, 0x581E, 0x581F, 0x5820, 0x5826, 0x5827, 0x582D, 0x5832, 0x5839, 0x583F, 0x5849, 0x584C, 0x584D, 0x584F, 0x5850, 0x5855, 0x585F, 0x5861, 0x5864, 0x5867, 0x5868, 0x5878, 0x587C, 0x587F, 0x5880, 0x5881, 0x5887, 0x5888, 0x5889, 0x588A, 0x588C, 0x588D, 0x588F, 0x5890, 0x5894, 0x5896, 0x589D, 0x58A0, 0x58A1, 0x58A2, 0x58A6, 0x58A9, 0x58B1, 0x58B2, 0x58C4, 0x58BC, 0x58C2, 0x58C8, 0x58CD, 0x58CE, 0x58D0, 0x58D2, 0x58D4, 0x58D6, 0x58DA, 0x58DD, 0x58E1, 0x58E2, 0x58E9, 0x58F3, 0x5905, 0x5906, 0x590B, 0x590C, 0x5912, 0x5913, 0x5914, 0x8641, 0x591D, 0x5921, 0x5923, 0x5924, 0x5928, 0x592F, 0x5930, 0x5933, 0x5935, 0x5936, 0x593F, 0x5943, 0x5946, 0x5952, 0x5953, 0x5959, 0x595B, 0x595D, 0x595E, 0x595F, 0x5961, 0x5963, 0x596B, 0x596D, + 0x596F, 0x5972, 0x5975, 0x5976, 0x5979, 0x597B, 0x597C, 0x598B, 0x598C, 0x598E, 0x5992, 0x5995, 0x5997, 0x599F, 0x59A4, 0x59A7, 0x59AD, 0x59AE, 0x59AF, 0x59B0, 0x59B3, 0x59B7, 0x59BA, 0x59BC, 0x59C1, 0x59C3, 0x59C4, 0x59C8, 0x59CA, 0x59CD, 0x59D2, 0x59DD, 0x59DE, 0x59DF, 0x59E3, 0x59E4, 0x59E7, 0x59EE, 0x59EF, 0x59F1, 0x59F2, 0x59F4, 0x59F7, 0x5A00, 0x5A04, 0x5A0C, 0x5A0D, 0x5A0E, 0x5A12, 0x5A13, 0x5A1E, 0x5A23, 0x5A24, 0x5A27, 0x5A28, 0x5A2A, 0x5A2D, 0x5A30, 0x5A44, 0x5A45, 0x5A47, 0x5A48, 0x5A4C, 0x5A50, 0x5A55, 0x5A5E, 0x5A63, 0x5A65, 0x5A67, 0x5A6D, 0x5A77, 0x5A7A, 0x5A7B, 0x5A7E, 0x5A8B, 0x5A90, 0x5A93, 0x5A96, 0x5A99, 0x5A9C, 0x5A9E, 0x5A9F, 0x5AA0, 0x5AA2, 0x5AA7, 0x5AAC, 0x5AB1, 0x5AB2, 0x5AB3, 0x5AB5, 0x5AB8, 0x5ABA, 0x5ABB, 0x5ABF, + 0x5AC4, 0x5AC6, 0x5AC8, 0x5ACF, 0x5ADA, 0x5ADC, 0x5AE0, 0x5AE5, 0x5AEA, 0x5AEE, 0x5AF5, 0x5AF6, 0x5AFD, 0x5B00, 0x5B01, 0x5B08, 0x5B17, 0x5B34, 0x5B19, 0x5B1B, 0x5B1D, 0x5B21, 0x5B25, 0x5B2D, 0x5B38, 0x5B41, 0x5B4B, 0x5B4C, 0x5B52, 0x5B56, 0x5B5E, 0x5B68, 0x5B6E, 0x5B6F, 0x5B7C, 0x5B7D, 0x5B7E, 0x5B7F, 0x5B81, 0x5B84, 0x5B86, 0x5B8A, 0x5B8E, 0x5B90, 0x5B91, 0x5B93, 0x5B94, 0x5B96, 0x5BA8, 0x5BA9, 0x5BAC, 0x5BAD, 0x5BAF, 0x5BB1, 0x5BB2, 0x5BB7, 0x5BBA, 0x5BBC, 0x5BC0, 0x5BC1, 0x5BCD, 0x5BCF, 0x5BD6, 0x5BD7, 0x5BD8, 0x5BD9, 0x5BDA, 0x5BE0, 0x5BEF, 0x5BF1, 0x5BF4, 0x5BFD, 0x5C0C, 0x5C17, 0x5C1E, 0x5C1F, 0x5C23, 0x5C26, 0x5C29, 0x5C2B, 0x5C2C, 0x5C2E, 0x5C30, 0x5C32, 0x5C35, 0x5C36, 0x5C59, 0x5C5A, 0x5C5C, 0x5C62, 0x5C63, 0x5C67, 0x5C68, 0x5C69, + 0x5C6D, 0x5C70, 0x5C74, 0x5C75, 0x5C7A, 0x5C7B, 0x5C7C, 0x5C7D, 0x5C87, 0x5C88, 0x5C8A, 0x5C8F, 0x5C92, 0x5C9D, 0x5C9F, 0x5CA0, 0x5CA2, 0x5CA3, 0x5CA6, 0x5CAA, 0x5CB2, 0x5CB4, 0x5CB5, 0x5CBA, 0x5CC9, 0x5CCB, 0x5CD2, 0x5CDD, 0x5CD7, 0x5CEE, 0x5CF1, 0x5CF2, 0x5CF4, 0x5D01, 0x5D06, 0x5D0D, 0x5D12, 0x5D2B, 0x5D23, 0x5D24, 0x5D26, 0x5D27, 0x5D31, 0x5D34, 0x5D39, 0x5D3D, 0x5D3F, 0x5D42, 0x5D43, 0x5D46, 0x5D48, 0x5D55, 0x5D51, 0x5D59, 0x5D4A, 0x5D5F, 0x5D60, 0x5D61, 0x5D62, 0x5D64, 0x5D6A, 0x5D6D, 0x5D70, 0x5D79, 0x5D7A, 0x5D7E, 0x5D7F, 0x5D81, 0x5D83, 0x5D88, 0x5D8A, 0x5D92, 0x5D93, 0x5D94, 0x5D95, 0x5D99, 0x5D9B, 0x5D9F, 0x5DA0, 0x5DA7, 0x5DAB, 0x5DB0, 0x5DB4, 0x5DB8, 0x5DB9, 0x5DC3, 0x5DC7, 0x5DCB, 0x5DD0, 0x5DCE, 0x5DD8, 0x5DD9, 0x5DE0, 0x5DE4, + 0x5DE9, 0x5DF8, 0x5DF9, 0x5E00, 0x5E07, 0x5E0D, 0x5E12, 0x5E14, 0x5E15, 0x5E18, 0x5E1F, 0x5E20, 0x5E2E, 0x5E28, 0x5E32, 0x5E35, 0x5E3E, 0x5E4B, 0x5E50, 0x5E49, 0x5E51, 0x5E56, 0x5E58, 0x5E5B, 0x5E5C, 0x5E5E, 0x5E68, 0x5E6A, 0x5E6B, 0x5E6C, 0x5E6D, 0x5E6E, 0x5E70, 0x5E80, 0x5E8B, 0x5E8E, 0x5EA2, 0x5EA4, 0x5EA5, 0x5EA8, 0x5EAA, 0x5EAC, 0x5EB1, 0x5EB3, 0x5EBD, 0x5EBE, 0x5EBF, 0x5EC6, 0x5ECC, 0x5ECB, 0x5ECE, 0x5ED1, 0x5ED2, 0x5ED4, 0x5ED5, 0x5EDC, 0x5EDE, 0x5EE5, 0x5EEB, 0x5F02, 0x5F06, 0x5F07, 0x5F08, 0x5F0E, 0x5F19, 0x5F1C, 0x5F1D, 0x5F21, 0x5F22, 0x5F23, 0x5F24, 0x5F28, 0x5F2B, 0x5F2C, 0x5F2E, 0x5F30, 0x5F34, 0x5F36, 0x5F3B, 0x5F3D, 0x5F3F, 0x5F40, 0x5F44, 0x5F45, 0x5F47, 0x5F4D, 0x5F50, 0x5F54, 0x5F58, 0x5F5B, 0x5F60, 0x5F63, 0x5F64, 0x5F67, + 0x5F6F, 0x5F72, 0x5F74, 0x5F75, 0x5F78, 0x5F7A, 0x5F7D, 0x5F7E, 0x5F89, 0x5F8D, 0x5F8F, 0x5F96, 0x5F9C, 0x5F9D, 0x5FA2, 0x5FA7, 0x5FAB, 0x5FA4, 0x5FAC, 0x5FAF, 0x5FB0, 0x5FB1, 0x5FB8, 0x5FC4, 0x5FC7, 0x5FC8, 0x5FC9, 0x5FCB, 0x5FD0, 0x5FD1, 0x5FD2, 0x5FD3, 0x5FD4, 0x5FDE, 0x5FE1, 0x5FE2, 0x5FE8, 0x5FE9, 0x5FEA, 0x5FEC, 0x5FED, 0x5FEE, 0x5FEF, 0x5FF2, 0x5FF3, 0x5FF6, 0x5FFA, 0x5FFC, 0x6007, 0x600A, 0x600D, 0x6013, 0x6014, 0x6017, 0x6018, 0x601A, 0x601F, 0x6024, 0x602D, 0x6033, 0x6035, 0x6040, 0x6047, 0x6048, 0x6049, 0x604C, 0x6051, 0x6054, 0x6056, 0x6057, 0x605D, 0x6061, 0x6067, 0x6071, 0x607E, 0x607F, 0x6082, 0x6086, 0x6088, 0x608A, 0x608E, 0x6091, 0x6093, 0x6095, 0x6098, 0x609D, 0x609E, 0x60A2, 0x60A4, 0x60A5, 0x60A8, 0x60B0, 0x60B1, 0x60B7, + 0x60BB, 0x60BE, 0x60C2, 0x60C4, 0x60C8, 0x60C9, 0x60CA, 0x60CB, 0x60CE, 0x60CF, 0x60D4, 0x60D5, 0x60D9, 0x60DB, 0x60DD, 0x60DE, 0x60E2, 0x60E5, 0x60F2, 0x60F5, 0x60F8, 0x60FC, 0x60FD, 0x6102, 0x6107, 0x610A, 0x610C, 0x6110, 0x6111, 0x6112, 0x6113, 0x6114, 0x6116, 0x6117, 0x6119, 0x611C, 0x611E, 0x6122, 0x612A, 0x612B, 0x6130, 0x6131, 0x6135, 0x6136, 0x6137, 0x6139, 0x6141, 0x6145, 0x6146, 0x6149, 0x615E, 0x6160, 0x616C, 0x6172, 0x6178, 0x617B, 0x617C, 0x617F, 0x6180, 0x6181, 0x6183, 0x6184, 0x618B, 0x618D, 0x6192, 0x6193, 0x6197, 0x6198, 0x619C, 0x619D, 0x619F, 0x61A0, 0x61A5, 0x61A8, 0x61AA, 0x61AD, 0x61B8, 0x61B9, 0x61BC, 0x61C0, 0x61C1, 0x61C2, 0x61CE, 0x61CF, 0x61D5, 0x61DC, 0x61DD, 0x61DE, 0x61DF, 0x61E1, 0x61E2, 0x61E7, 0x61E9, 0x61E5, + 0x61EC, 0x61ED, 0x61EF, 0x6201, 0x6203, 0x6204, 0x6207, 0x6213, 0x6215, 0x621C, 0x6220, 0x6222, 0x6223, 0x6227, 0x6229, 0x622B, 0x6239, 0x623D, 0x6242, 0x6243, 0x6244, 0x6246, 0x624C, 0x6250, 0x6251, 0x6252, 0x6254, 0x6256, 0x625A, 0x625C, 0x6264, 0x626D, 0x626F, 0x6273, 0x627A, 0x627D, 0x628D, 0x628E, 0x628F, 0x6290, 0x62A6, 0x62A8, 0x62B3, 0x62B6, 0x62B7, 0x62BA, 0x62BE, 0x62BF, 0x62C4, 0x62CE, 0x62D5, 0x62D6, 0x62DA, 0x62EA, 0x62F2, 0x62F4, 0x62FC, 0x62FD, 0x6303, 0x6304, 0x630A, 0x630B, 0x630D, 0x6310, 0x6313, 0x6316, 0x6318, 0x6329, 0x632A, 0x632D, 0x6335, 0x6336, 0x6339, 0x633C, 0x6341, 0x6342, 0x6343, 0x6344, 0x6346, 0x634A, 0x634B, 0x634E, 0x6352, 0x6353, 0x6354, 0x6358, 0x635B, 0x6365, 0x6366, 0x636C, 0x636D, 0x6371, 0x6374, 0x6375, + 0x6378, 0x637C, 0x637D, 0x637F, 0x6382, 0x6384, 0x6387, 0x638A, 0x6390, 0x6394, 0x6395, 0x6399, 0x639A, 0x639E, 0x63A4, 0x63A6, 0x63AD, 0x63AE, 0x63AF, 0x63BD, 0x63C1, 0x63C5, 0x63C8, 0x63CE, 0x63D1, 0x63D3, 0x63D4, 0x63D5, 0x63DC, 0x63E0, 0x63E5, 0x63EA, 0x63EC, 0x63F2, 0x63F3, 0x63F5, 0x63F8, 0x63F9, 0x6409, 0x640A, 0x6410, 0x6412, 0x6414, 0x6418, 0x641E, 0x6420, 0x6422, 0x6424, 0x6425, 0x6429, 0x642A, 0x642F, 0x6430, 0x6435, 0x643D, 0x643F, 0x644B, 0x644F, 0x6451, 0x6452, 0x6453, 0x6454, 0x645A, 0x645B, 0x645C, 0x645D, 0x645F, 0x6460, 0x6461, 0x6463, 0x646D, 0x6473, 0x6474, 0x647B, 0x647D, 0x6485, 0x6487, 0x648F, 0x6490, 0x6491, 0x6498, 0x6499, 0x649B, 0x649D, 0x649F, 0x64A1, 0x64A3, 0x64A6, 0x64A8, 0x64AC, 0x64B3, 0x64BD, 0x64BE, 0x64BF, + 0x64C4, 0x64C9, 0x64CA, 0x64CB, 0x64CC, 0x64CE, 0x64D0, 0x64D1, 0x64D5, 0x64D7, 0x64E4, 0x64E5, 0x64E9, 0x64EA, 0x64ED, 0x64F0, 0x64F5, 0x64F7, 0x64FB, 0x64FF, 0x6501, 0x6504, 0x6508, 0x6509, 0x650A, 0x650F, 0x6513, 0x6514, 0x6516, 0x6519, 0x651B, 0x651E, 0x651F, 0x6522, 0x6526, 0x6529, 0x652E, 0x6531, 0x653A, 0x653C, 0x653D, 0x6543, 0x6547, 0x6549, 0x6550, 0x6552, 0x6554, 0x655F, 0x6560, 0x6567, 0x656B, 0x657A, 0x657D, 0x6581, 0x6585, 0x658A, 0x6592, 0x6595, 0x6598, 0x659D, 0x65A0, 0x65A3, 0x65A6, 0x65AE, 0x65B2, 0x65B3, 0x65B4, 0x65BF, 0x65C2, 0x65C8, 0x65C9, 0x65CE, 0x65D0, 0x65D4, 0x65D6, 0x65D8, 0x65DF, 0x65F0, 0x65F2, 0x65F4, 0x65F5, 0x65F9, 0x65FE, 0x65FF, 0x6600, 0x6604, 0x6608, 0x6609, 0x660D, 0x6611, 0x6612, 0x6615, 0x6616, 0x661D, + 0x661E, 0x6621, 0x6622, 0x6623, 0x6624, 0x6626, 0x6629, 0x662A, 0x662B, 0x662C, 0x662E, 0x6630, 0x6631, 0x6633, 0x6639, 0x6637, 0x6640, 0x6645, 0x6646, 0x664A, 0x664C, 0x6651, 0x664E, 0x6657, 0x6658, 0x6659, 0x665B, 0x665C, 0x6660, 0x6661, 0x66FB, 0x666A, 0x666B, 0x666C, 0x667E, 0x6673, 0x6675, 0x667F, 0x6677, 0x6678, 0x6679, 0x667B, 0x6680, 0x667C, 0x668B, 0x668C, 0x668D, 0x6690, 0x6692, 0x6699, 0x669A, 0x669B, 0x669C, 0x669F, 0x66A0, 0x66A4, 0x66AD, 0x66B1, 0x66B2, 0x66B5, 0x66BB, 0x66BF, 0x66C0, 0x66C2, 0x66C3, 0x66C8, 0x66CC, 0x66CE, 0x66CF, 0x66D4, 0x66DB, 0x66DF, 0x66E8, 0x66EB, 0x66EC, 0x66EE, 0x66FA, 0x6705, 0x6707, 0x670E, 0x6713, 0x6719, 0x671C, 0x6720, 0x6722, 0x6733, 0x673E, 0x6745, 0x6747, 0x6748, 0x674C, 0x6754, 0x6755, 0x675D, + 0x6766, 0x676C, 0x676E, 0x6774, 0x6776, 0x677B, 0x6781, 0x6784, 0x678E, 0x678F, 0x6791, 0x6793, 0x6796, 0x6798, 0x6799, 0x679B, 0x67B0, 0x67B1, 0x67B2, 0x67B5, 0x67BB, 0x67BC, 0x67BD, 0x67F9, 0x67C0, 0x67C2, 0x67C3, 0x67C5, 0x67C8, 0x67C9, 0x67D2, 0x67D7, 0x67D9, 0x67DC, 0x67E1, 0x67E6, 0x67F0, 0x67F2, 0x67F6, 0x67F7, 0x6852, 0x6814, 0x6819, 0x681D, 0x681F, 0x6828, 0x6827, 0x682C, 0x682D, 0x682F, 0x6830, 0x6831, 0x6833, 0x683B, 0x683F, 0x6844, 0x6845, 0x684A, 0x684C, 0x6855, 0x6857, 0x6858, 0x685B, 0x686B, 0x686E, 0x686F, 0x6870, 0x6871, 0x6872, 0x6875, 0x6879, 0x687A, 0x687B, 0x687C, 0x6882, 0x6884, 0x6886, 0x6888, 0x6896, 0x6898, 0x689A, 0x689C, 0x68A1, 0x68A3, 0x68A5, 0x68A9, 0x68AA, 0x68AE, 0x68B2, 0x68BB, 0x68C5, 0x68C8, 0x68CC, 0x68CF, + 0x68D0, 0x68D1, 0x68D3, 0x68D6, 0x68D9, 0x68DC, 0x68DD, 0x68E5, 0x68E8, 0x68EA, 0x68EB, 0x68EC, 0x68ED, 0x68F0, 0x68F1, 0x68F5, 0x68F6, 0x68FB, 0x68FC, 0x68FD, 0x6906, 0x6909, 0x690A, 0x6910, 0x6911, 0x6913, 0x6916, 0x6917, 0x6931, 0x6933, 0x6935, 0x6938, 0x693B, 0x6942, 0x6945, 0x6949, 0x694E, 0x6957, 0x695B, 0x6963, 0x6964, 0x6965, 0x6966, 0x6968, 0x6969, 0x696C, 0x6970, 0x6971, 0x6972, 0x697A, 0x697B, 0x697F, 0x6980, 0x698D, 0x6992, 0x6996, 0x6998, 0x69A1, 0x69A5, 0x69A6, 0x69A8, 0x69AB, 0x69AD, 0x69AF, 0x69B7, 0x69B8, 0x69BA, 0x69BC, 0x69C5, 0x69C8, 0x69D1, 0x69D6, 0x69D7, 0x69E2, 0x69E5, 0x69EE, 0x69EF, 0x69F1, 0x69F3, 0x69F5, 0x69FE, 0x6A00, 0x6A01, 0x6A03, 0x6A0F, 0x6A11, 0x6A15, 0x6A1A, 0x6A1D, 0x6A20, 0x6A24, 0x6A28, 0x6A30, 0x6A32, + 0x6A34, 0x6A37, 0x6A3B, 0x6A3E, 0x6A3F, 0x6A45, 0x6A46, 0x6A49, 0x6A4A, 0x6A4E, 0x6A50, 0x6A51, 0x6A52, 0x6A55, 0x6A56, 0x6A5B, 0x6A64, 0x6A67, 0x6A6A, 0x6A71, 0x6A73, 0x6A7E, 0x6A81, 0x6A83, 0x6A86, 0x6A87, 0x6A89, 0x6A8B, 0x6A91, 0x6A9B, 0x6A9D, 0x6A9E, 0x6A9F, 0x6AA5, 0x6AAB, 0x6AAF, 0x6AB0, 0x6AB1, 0x6AB4, 0x6ABD, 0x6ABE, 0x6ABF, 0x6AC6, 0x6AC9, 0x6AC8, 0x6ACC, 0x6AD0, 0x6AD4, 0x6AD5, 0x6AD6, 0x6ADC, 0x6ADD, 0x6AE4, 0x6AE7, 0x6AEC, 0x6AF0, 0x6AF1, 0x6AF2, 0x6AFC, 0x6AFD, 0x6B02, 0x6B03, 0x6B06, 0x6B07, 0x6B09, 0x6B0F, 0x6B10, 0x6B11, 0x6B17, 0x6B1B, 0x6B1E, 0x6B24, 0x6B28, 0x6B2B, 0x6B2C, 0x6B2F, 0x6B35, 0x6B36, 0x6B3B, 0x6B3F, 0x6B46, 0x6B4A, 0x6B4D, 0x6B52, 0x6B56, 0x6B58, 0x6B5D, 0x6B60, 0x6B67, 0x6B6B, 0x6B6E, 0x6B70, 0x6B75, 0x6B7D, + 0x6B7E, 0x6B82, 0x6B85, 0x6B97, 0x6B9B, 0x6B9F, 0x6BA0, 0x6BA2, 0x6BA3, 0x6BA8, 0x6BA9, 0x6BAC, 0x6BAD, 0x6BAE, 0x6BB0, 0x6BB8, 0x6BB9, 0x6BBD, 0x6BBE, 0x6BC3, 0x6BC4, 0x6BC9, 0x6BCC, 0x6BD6, 0x6BDA, 0x6BE1, 0x6BE3, 0x6BE6, 0x6BE7, 0x6BEE, 0x6BF1, 0x6BF7, 0x6BF9, 0x6BFF, 0x6C02, 0x6C04, 0x6C05, 0x6C09, 0x6C0D, 0x6C0E, 0x6C10, 0x6C12, 0x6C19, 0x6C1F, 0x6C26, 0x6C27, 0x6C28, 0x6C2C, 0x6C2E, 0x6C33, 0x6C35, 0x6C36, 0x6C3A, 0x6C3B, 0x6C3F, 0x6C4A, 0x6C4B, 0x6C4D, 0x6C4F, 0x6C52, 0x6C54, 0x6C59, 0x6C5B, 0x6C5C, 0x6C6B, 0x6C6D, 0x6C6F, 0x6C74, 0x6C76, 0x6C78, 0x6C79, 0x6C7B, 0x6C85, 0x6C86, 0x6C87, 0x6C89, 0x6C94, 0x6C95, 0x6C97, 0x6C98, 0x6C9C, 0x6C9F, 0x6CB0, 0x6CB2, 0x6CB4, 0x6CC2, 0x6CC6, 0x6CCD, 0x6CCF, 0x6CD0, 0x6CD1, 0x6CD2, 0x6CD4, 0x6CD6, + 0x6CDA, 0x6CDC, 0x6CE0, 0x6CE7, 0x6CE9, 0x6CEB, 0x6CEC, 0x6CEE, 0x6CF2, 0x6CF4, 0x6D04, 0x6D07, 0x6D0A, 0x6D0E, 0x6D0F, 0x6D11, 0x6D13, 0x6D1A, 0x6D26, 0x6D27, 0x6D28, 0x6C67, 0x6D2E, 0x6D2F, 0x6D31, 0x6D39, 0x6D3C, 0x6D3F, 0x6D57, 0x6D5E, 0x6D5F, 0x6D61, 0x6D65, 0x6D67, 0x6D6F, 0x6D70, 0x6D7C, 0x6D82, 0x6D87, 0x6D91, 0x6D92, 0x6D94, 0x6D96, 0x6D97, 0x6D98, 0x6DAA, 0x6DAC, 0x6DB4, 0x6DB7, 0x6DB9, 0x6DBD, 0x6DBF, 0x6DC4, 0x6DC8, 0x6DCA, 0x6DCE, 0x6DCF, 0x6DD6, 0x6DDB, 0x6DDD, 0x6DDF, 0x6DE0, 0x6DE2, 0x6DE5, 0x6DE9, 0x6DEF, 0x6DF0, 0x6DF4, 0x6DF6, 0x6DFC, 0x6E00, 0x6E04, 0x6E1E, 0x6E22, 0x6E27, 0x6E32, 0x6E36, 0x6E39, 0x6E3B, 0x6E3C, 0x6E44, 0x6E45, 0x6E48, 0x6E49, 0x6E4B, 0x6E4F, 0x6E51, 0x6E52, 0x6E53, 0x6E54, 0x6E57, 0x6E5C, 0x6E5D, 0x6E5E, + 0x6E62, 0x6E63, 0x6E68, 0x6E73, 0x6E7B, 0x6E7D, 0x6E8D, 0x6E93, 0x6E99, 0x6EA0, 0x6EA7, 0x6EAD, 0x6EAE, 0x6EB1, 0x6EB3, 0x6EBB, 0x6EBF, 0x6EC0, 0x6EC1, 0x6EC3, 0x6EC7, 0x6EC8, 0x6ECA, 0x6ECD, 0x6ECE, 0x6ECF, 0x6EEB, 0x6EED, 0x6EEE, 0x6EF9, 0x6EFB, 0x6EFD, 0x6F04, 0x6F08, 0x6F0A, 0x6F0C, 0x6F0D, 0x6F16, 0x6F18, 0x6F1A, 0x6F1B, 0x6F26, 0x6F29, 0x6F2A, 0x6F2F, 0x6F30, 0x6F33, 0x6F36, 0x6F3B, 0x6F3C, 0x6F2D, 0x6F4F, 0x6F51, 0x6F52, 0x6F53, 0x6F57, 0x6F59, 0x6F5A, 0x6F5D, 0x6F5E, 0x6F61, 0x6F62, 0x6F68, 0x6F6C, 0x6F7D, 0x6F7E, 0x6F83, 0x6F87, 0x6F88, 0x6F8B, 0x6F8C, 0x6F8D, 0x6F90, 0x6F92, 0x6F93, 0x6F94, 0x6F96, 0x6F9A, 0x6F9F, 0x6FA0, 0x6FA5, 0x6FA6, 0x6FA7, 0x6FA8, 0x6FAE, 0x6FAF, 0x6FB0, 0x6FB5, 0x6FB6, 0x6FBC, 0x6FC5, 0x6FC7, 0x6FC8, 0x6FCA, + 0x6FDA, 0x6FDE, 0x6FE8, 0x6FE9, 0x6FF0, 0x6FF5, 0x6FF9, 0x6FFC, 0x6FFD, 0x7000, 0x7005, 0x7006, 0x7007, 0x700D, 0x7017, 0x7020, 0x7023, 0x702F, 0x7034, 0x7037, 0x7039, 0x703C, 0x7043, 0x7044, 0x7048, 0x7049, 0x704A, 0x704B, 0x7054, 0x7055, 0x705D, 0x705E, 0x704E, 0x7064, 0x7065, 0x706C, 0x706E, 0x7075, 0x7076, 0x707E, 0x7081, 0x7085, 0x7086, 0x7094, 0x7095, 0x7096, 0x7097, 0x7098, 0x709B, 0x70A4, 0x70AB, 0x70B0, 0x70B1, 0x70B4, 0x70B7, 0x70CA, 0x70D1, 0x70D3, 0x70D4, 0x70D5, 0x70D6, 0x70D8, 0x70DC, 0x70E4, 0x70FA, 0x7103, 0x7104, 0x7105, 0x7106, 0x7107, 0x710B, 0x710C, 0x710F, 0x711E, 0x7120, 0x712B, 0x712D, 0x712F, 0x7130, 0x7131, 0x7138, 0x7141, 0x7145, 0x7146, 0x7147, 0x714A, 0x714B, 0x7150, 0x7152, 0x7157, 0x715A, 0x715C, 0x715E, 0x7160, + 0x7168, 0x7179, 0x7180, 0x7185, 0x7187, 0x718C, 0x7192, 0x719A, 0x719B, 0x71A0, 0x71A2, 0x71AF, 0x71B0, 0x71B2, 0x71B3, 0x71BA, 0x71BF, 0x71C0, 0x71C1, 0x71C4, 0x71CB, 0x71CC, 0x71D3, 0x71D6, 0x71D9, 0x71DA, 0x71DC, 0x71F8, 0x71FE, 0x7200, 0x7207, 0x7208, 0x7209, 0x7213, 0x7217, 0x721A, 0x721D, 0x721F, 0x7224, 0x722B, 0x722F, 0x7234, 0x7238, 0x7239, 0x7241, 0x7242, 0x7243, 0x7245, 0x724E, 0x724F, 0x7250, 0x7253, 0x7255, 0x7256, 0x725A, 0x725C, 0x725E, 0x7260, 0x7263, 0x7268, 0x726B, 0x726E, 0x726F, 0x7271, 0x7277, 0x7278, 0x727B, 0x727C, 0x727F, 0x7284, 0x7289, 0x728D, 0x728E, 0x7293, 0x729B, 0x72A8, 0x72AD, 0x72AE, 0x72B1, 0x72B4, 0x72BE, 0x72C1, 0x72C7, 0x72C9, 0x72CC, 0x72D5, 0x72D6, 0x72D8, 0x72DF, 0x72E5, 0x72F3, 0x72F4, 0x72FA, 0x72FB, + 0x72FE, 0x7302, 0x7304, 0x7305, 0x7307, 0x730B, 0x730D, 0x7312, 0x7313, 0x7318, 0x7319, 0x731E, 0x7322, 0x7324, 0x7327, 0x7328, 0x732C, 0x7331, 0x7332, 0x7335, 0x733A, 0x733B, 0x733D, 0x7343, 0x734D, 0x7350, 0x7352, 0x7356, 0x7358, 0x735D, 0x735E, 0x735F, 0x7360, 0x7366, 0x7367, 0x7369, 0x736B, 0x736C, 0x736E, 0x736F, 0x7371, 0x7377, 0x7379, 0x737C, 0x7380, 0x7381, 0x7383, 0x7385, 0x7386, 0x738E, 0x7390, 0x7393, 0x7395, 0x7397, 0x7398, 0x739C, 0x739E, 0x739F, 0x73A0, 0x73A2, 0x73A5, 0x73A6, 0x73AA, 0x73AB, 0x73AD, 0x73B5, 0x73B7, 0x73B9, 0x73BC, 0x73BD, 0x73BF, 0x73C5, 0x73C6, 0x73C9, 0x73CB, 0x73CC, 0x73CF, 0x73D2, 0x73D3, 0x73D6, 0x73D9, 0x73DD, 0x73E1, 0x73E3, 0x73E6, 0x73E7, 0x73E9, 0x73F4, 0x73F5, 0x73F7, 0x73F9, 0x73FA, 0x73FB, 0x73FD, + 0x73FF, 0x7400, 0x7401, 0x7404, 0x7407, 0x740A, 0x7411, 0x741A, 0x741B, 0x7424, 0x7426, 0x7428, 0x7429, 0x742A, 0x742B, 0x742C, 0x742D, 0x742E, 0x742F, 0x7430, 0x7431, 0x7439, 0x7440, 0x7443, 0x7444, 0x7446, 0x7447, 0x744B, 0x744D, 0x7451, 0x7452, 0x7457, 0x745D, 0x7462, 0x7466, 0x7467, 0x7468, 0x746B, 0x746D, 0x746E, 0x7471, 0x7472, 0x7480, 0x7481, 0x7485, 0x7486, 0x7487, 0x7489, 0x748F, 0x7490, 0x7491, 0x7492, 0x7498, 0x7499, 0x749A, 0x749C, 0x749F, 0x74A0, 0x74A1, 0x74A3, 0x74A6, 0x74A8, 0x74A9, 0x74AA, 0x74AB, 0x74AE, 0x74AF, 0x74B1, 0x74B2, 0x74B5, 0x74B9, 0x74BB, 0x74BF, 0x74C8, 0x74C9, 0x74CC, 0x74D0, 0x74D3, 0x74D8, 0x74DA, 0x74DB, 0x74DE, 0x74DF, 0x74E4, 0x74E8, 0x74EA, 0x74EB, 0x74EF, 0x74F4, 0x74FA, 0x74FB, 0x74FC, 0x74FF, 0x7506, + 0x7512, 0x7516, 0x7517, 0x7520, 0x7521, 0x7524, 0x7527, 0x7529, 0x752A, 0x752F, 0x7536, 0x7539, 0x753D, 0x753E, 0x753F, 0x7540, 0x7543, 0x7547, 0x7548, 0x754E, 0x7550, 0x7552, 0x7557, 0x755E, 0x755F, 0x7561, 0x756F, 0x7571, 0x7579, 0x757A, 0x757B, 0x757C, 0x757D, 0x757E, 0x7581, 0x7585, 0x7590, 0x7592, 0x7593, 0x7595, 0x7599, 0x759C, 0x75A2, 0x75A4, 0x75B4, 0x75BA, 0x75BF, 0x75C0, 0x75C1, 0x75C4, 0x75C6, 0x75CC, 0x75CE, 0x75CF, 0x75D7, 0x75DC, 0x75DF, 0x75E0, 0x75E1, 0x75E4, 0x75E7, 0x75EC, 0x75EE, 0x75EF, 0x75F1, 0x75F9, 0x7600, 0x7602, 0x7603, 0x7604, 0x7607, 0x7608, 0x760A, 0x760C, 0x760F, 0x7612, 0x7613, 0x7615, 0x7616, 0x7619, 0x761B, 0x761C, 0x761D, 0x761E, 0x7623, 0x7625, 0x7626, 0x7629, 0x762D, 0x7632, 0x7633, 0x7635, 0x7638, 0x7639, + 0x763A, 0x763C, 0x764A, 0x7640, 0x7641, 0x7643, 0x7644, 0x7645, 0x7649, 0x764B, 0x7655, 0x7659, 0x765F, 0x7664, 0x7665, 0x766D, 0x766E, 0x766F, 0x7671, 0x7674, 0x7681, 0x7685, 0x768C, 0x768D, 0x7695, 0x769B, 0x769C, 0x769D, 0x769F, 0x76A0, 0x76A2, 0x76A3, 0x76A4, 0x76A5, 0x76A6, 0x76A7, 0x76A8, 0x76AA, 0x76AD, 0x76BD, 0x76C1, 0x76C5, 0x76C9, 0x76CB, 0x76CC, 0x76CE, 0x76D4, 0x76D9, 0x76E0, 0x76E6, 0x76E8, 0x76EC, 0x76F0, 0x76F1, 0x76F6, 0x76F9, 0x76FC, 0x7700, 0x7706, 0x770A, 0x770E, 0x7712, 0x7714, 0x7715, 0x7717, 0x7719, 0x771A, 0x771C, 0x7722, 0x7728, 0x772D, 0x772E, 0x772F, 0x7734, 0x7735, 0x7736, 0x7739, 0x773D, 0x773E, 0x7742, 0x7745, 0x7746, 0x774A, 0x774D, 0x774E, 0x774F, 0x7752, 0x7756, 0x7757, 0x775C, 0x775E, 0x775F, 0x7760, 0x7762, + 0x7764, 0x7767, 0x776A, 0x776C, 0x7770, 0x7772, 0x7773, 0x7774, 0x777A, 0x777D, 0x7780, 0x7784, 0x778C, 0x778D, 0x7794, 0x7795, 0x7796, 0x779A, 0x779F, 0x77A2, 0x77A7, 0x77AA, 0x77AE, 0x77AF, 0x77B1, 0x77B5, 0x77BE, 0x77C3, 0x77C9, 0x77D1, 0x77D2, 0x77D5, 0x77D9, 0x77DE, 0x77DF, 0x77E0, 0x77E4, 0x77E6, 0x77EA, 0x77EC, 0x77F0, 0x77F1, 0x77F4, 0x77F8, 0x77FB, 0x7805, 0x7806, 0x7809, 0x780D, 0x780E, 0x7811, 0x781D, 0x7821, 0x7822, 0x7823, 0x782D, 0x782E, 0x7830, 0x7835, 0x7837, 0x7843, 0x7844, 0x7847, 0x7848, 0x784C, 0x784E, 0x7852, 0x785C, 0x785E, 0x7860, 0x7861, 0x7863, 0x7864, 0x7868, 0x786A, 0x786E, 0x787A, 0x787E, 0x788A, 0x788F, 0x7894, 0x7898, 0x78A1, 0x789D, 0x789E, 0x789F, 0x78A4, 0x78A8, 0x78AC, 0x78AD, 0x78B0, 0x78B1, 0x78B2, 0x78B3, + 0x78BB, 0x78BD, 0x78BF, 0x78C7, 0x78C8, 0x78C9, 0x78CC, 0x78CE, 0x78D2, 0x78D3, 0x78D5, 0x78D6, 0x78E4, 0x78DB, 0x78DF, 0x78E0, 0x78E1, 0x78E6, 0x78EA, 0x78F2, 0x78F3, 0x7900, 0x78F6, 0x78F7, 0x78FA, 0x78FB, 0x78FF, 0x7906, 0x790C, 0x7910, 0x791A, 0x791C, 0x791E, 0x791F, 0x7920, 0x7925, 0x7927, 0x7929, 0x792D, 0x7931, 0x7934, 0x7935, 0x793B, 0x793D, 0x793F, 0x7944, 0x7945, 0x7946, 0x794A, 0x794B, 0x794F, 0x7951, 0x7954, 0x7958, 0x795B, 0x795C, 0x7967, 0x7969, 0x796B, 0x7972, 0x7979, 0x797B, 0x797C, 0x797E, 0x798B, 0x798C, 0x7991, 0x7993, 0x7994, 0x7995, 0x7996, 0x7998, 0x799B, 0x799C, 0x79A1, 0x79A8, 0x79A9, 0x79AB, 0x79AF, 0x79B1, 0x79B4, 0x79B8, 0x79BB, 0x79C2, 0x79C4, 0x79C7, 0x79C8, 0x79CA, 0x79CF, 0x79D4, 0x79D6, 0x79DA, 0x79DD, 0x79DE, + 0x79E0, 0x79E2, 0x79E5, 0x79EA, 0x79EB, 0x79ED, 0x79F1, 0x79F8, 0x79FC, 0x7A02, 0x7A03, 0x7A07, 0x7A09, 0x7A0A, 0x7A0C, 0x7A11, 0x7A15, 0x7A1B, 0x7A1E, 0x7A21, 0x7A27, 0x7A2B, 0x7A2D, 0x7A2F, 0x7A30, 0x7A34, 0x7A35, 0x7A38, 0x7A39, 0x7A3A, 0x7A44, 0x7A45, 0x7A47, 0x7A48, 0x7A4C, 0x7A55, 0x7A56, 0x7A59, 0x7A5C, 0x7A5D, 0x7A5F, 0x7A60, 0x7A65, 0x7A67, 0x7A6A, 0x7A6D, 0x7A75, 0x7A78, 0x7A7E, 0x7A80, 0x7A82, 0x7A85, 0x7A86, 0x7A8A, 0x7A8B, 0x7A90, 0x7A91, 0x7A94, 0x7A9E, 0x7AA0, 0x7AA3, 0x7AAC, 0x7AB3, 0x7AB5, 0x7AB9, 0x7ABB, 0x7ABC, 0x7AC6, 0x7AC9, 0x7ACC, 0x7ACE, 0x7AD1, 0x7ADB, 0x7AE8, 0x7AE9, 0x7AEB, 0x7AEC, 0x7AF1, 0x7AF4, 0x7AFB, 0x7AFD, 0x7AFE, 0x7B07, 0x7B14, 0x7B1F, 0x7B23, 0x7B27, 0x7B29, 0x7B2A, 0x7B2B, 0x7B2D, 0x7B2E, 0x7B2F, 0x7B30, + 0x7B31, 0x7B34, 0x7B3D, 0x7B3F, 0x7B40, 0x7B41, 0x7B47, 0x7B4E, 0x7B55, 0x7B60, 0x7B64, 0x7B66, 0x7B69, 0x7B6A, 0x7B6D, 0x7B6F, 0x7B72, 0x7B73, 0x7B77, 0x7B84, 0x7B89, 0x7B8E, 0x7B90, 0x7B91, 0x7B96, 0x7B9B, 0x7B9E, 0x7BA0, 0x7BA5, 0x7BAC, 0x7BAF, 0x7BB0, 0x7BB2, 0x7BB5, 0x7BB6, 0x7BBA, 0x7BBB, 0x7BBC, 0x7BBD, 0x7BC2, 0x7BC5, 0x7BC8, 0x7BCA, 0x7BD4, 0x7BD6, 0x7BD7, 0x7BD9, 0x7BDA, 0x7BDB, 0x7BE8, 0x7BEA, 0x7BF2, 0x7BF4, 0x7BF5, 0x7BF8, 0x7BF9, 0x7BFA, 0x7BFC, 0x7BFE, 0x7C01, 0x7C02, 0x7C03, 0x7C04, 0x7C06, 0x7C09, 0x7C0B, 0x7C0C, 0x7C0E, 0x7C0F, 0x7C19, 0x7C1B, 0x7C20, 0x7C25, 0x7C26, 0x7C28, 0x7C2C, 0x7C31, 0x7C33, 0x7C34, 0x7C36, 0x7C39, 0x7C3A, 0x7C46, 0x7C4A, 0x7C55, 0x7C51, 0x7C52, 0x7C53, 0x7C59, 0x7C5A, 0x7C5B, 0x7C5C, 0x7C5D, 0x7C5E, + 0x7C61, 0x7C63, 0x7C67, 0x7C69, 0x7C6D, 0x7C6E, 0x7C70, 0x7C72, 0x7C79, 0x7C7C, 0x7C7D, 0x7C86, 0x7C87, 0x7C8F, 0x7C94, 0x7C9E, 0x7CA0, 0x7CA6, 0x7CB0, 0x7CB6, 0x7CB7, 0x7CBA, 0x7CBB, 0x7CBC, 0x7CBF, 0x7CC4, 0x7CC7, 0x7CC8, 0x7CC9, 0x7CCD, 0x7CCF, 0x7CD3, 0x7CD4, 0x7CD5, 0x7CD7, 0x7CD9, 0x7CDA, 0x7CDD, 0x7CE6, 0x7CE9, 0x7CEB, 0x7CF5, 0x7D03, 0x7D07, 0x7D08, 0x7D09, 0x7D0F, 0x7D11, 0x7D12, 0x7D13, 0x7D16, 0x7D1D, 0x7D1E, 0x7D23, 0x7D26, 0x7D2A, 0x7D2D, 0x7D31, 0x7D3C, 0x7D3D, 0x7D3E, 0x7D40, 0x7D41, 0x7D47, 0x7D48, 0x7D4D, 0x7D51, 0x7D53, 0x7D57, 0x7D59, 0x7D5A, 0x7D5C, 0x7D5D, 0x7D65, 0x7D67, 0x7D6A, 0x7D70, 0x7D78, 0x7D7A, 0x7D7B, 0x7D7F, 0x7D81, 0x7D82, 0x7D83, 0x7D85, 0x7D86, 0x7D88, 0x7D8B, 0x7D8C, 0x7D8D, 0x7D91, 0x7D96, 0x7D97, 0x7D9D, + 0x7D9E, 0x7DA6, 0x7DA7, 0x7DAA, 0x7DB3, 0x7DB6, 0x7DB7, 0x7DB9, 0x7DC2, 0x7DC3, 0x7DC4, 0x7DC5, 0x7DC6, 0x7DCC, 0x7DCD, 0x7DCE, 0x7DD7, 0x7DD9, 0x7E00, 0x7DE2, 0x7DE5, 0x7DE6, 0x7DEA, 0x7DEB, 0x7DED, 0x7DF1, 0x7DF5, 0x7DF6, 0x7DF9, 0x7DFA, 0x7E08, 0x7E10, 0x7E11, 0x7E15, 0x7E17, 0x7E1C, 0x7E1D, 0x7E20, 0x7E27, 0x7E28, 0x7E2C, 0x7E2D, 0x7E2F, 0x7E33, 0x7E36, 0x7E3F, 0x7E44, 0x7E45, 0x7E47, 0x7E4E, 0x7E50, 0x7E52, 0x7E58, 0x7E5F, 0x7E61, 0x7E62, 0x7E65, 0x7E6B, 0x7E6E, 0x7E6F, 0x7E73, 0x7E78, 0x7E7E, 0x7E81, 0x7E86, 0x7E87, 0x7E8A, 0x7E8D, 0x7E91, 0x7E95, 0x7E98, 0x7E9A, 0x7E9D, 0x7E9E, 0x7F3C, 0x7F3B, 0x7F3D, 0x7F3E, 0x7F3F, 0x7F43, 0x7F44, 0x7F47, 0x7F4F, 0x7F52, 0x7F53, 0x7F5B, 0x7F5C, 0x7F5D, 0x7F61, 0x7F63, 0x7F64, 0x7F65, 0x7F66, 0x7F6D, + 0x7F71, 0x7F7D, 0x7F7E, 0x7F7F, 0x7F80, 0x7F8B, 0x7F8D, 0x7F8F, 0x7F90, 0x7F91, 0x7F96, 0x7F97, 0x7F9C, 0x7FA1, 0x7FA2, 0x7FA6, 0x7FAA, 0x7FAD, 0x7FB4, 0x7FBC, 0x7FBF, 0x7FC0, 0x7FC3, 0x7FC8, 0x7FCE, 0x7FCF, 0x7FDB, 0x7FDF, 0x7FE3, 0x7FE5, 0x7FE8, 0x7FEC, 0x7FEE, 0x7FEF, 0x7FF2, 0x7FFA, 0x7FFD, 0x7FFE, 0x7FFF, 0x8007, 0x8008, 0x800A, 0x800D, 0x800E, 0x800F, 0x8011, 0x8013, 0x8014, 0x8016, 0x801D, 0x801E, 0x801F, 0x8020, 0x8024, 0x8026, 0x802C, 0x802E, 0x8030, 0x8034, 0x8035, 0x8037, 0x8039, 0x803A, 0x803C, 0x803E, 0x8040, 0x8044, 0x8060, 0x8064, 0x8066, 0x806D, 0x8071, 0x8075, 0x8081, 0x8088, 0x808E, 0x809C, 0x809E, 0x80A6, 0x80A7, 0x80AB, 0x80B8, 0x80B9, 0x80C8, 0x80CD, 0x80CF, 0x80D2, 0x80D4, 0x80D5, 0x80D7, 0x80D8, 0x80E0, 0x80ED, 0x80EE, + 0x80F0, 0x80F2, 0x80F3, 0x80F6, 0x80F9, 0x80FA, 0x80FE, 0x8103, 0x810B, 0x8116, 0x8117, 0x8118, 0x811C, 0x811E, 0x8120, 0x8124, 0x8127, 0x812C, 0x8130, 0x8135, 0x813A, 0x813C, 0x8145, 0x8147, 0x814A, 0x814C, 0x8152, 0x8157, 0x8160, 0x8161, 0x8167, 0x8168, 0x8169, 0x816D, 0x816F, 0x8177, 0x8181, 0x8190, 0x8184, 0x8185, 0x8186, 0x818B, 0x818E, 0x8196, 0x8198, 0x819B, 0x819E, 0x81A2, 0x81AE, 0x81B2, 0x81B4, 0x81BB, 0x81CB, 0x81C3, 0x81C5, 0x81CA, 0x81CE, 0x81CF, 0x81D5, 0x81D7, 0x81DB, 0x81DD, 0x81DE, 0x81E1, 0x81E4, 0x81EB, 0x81EC, 0x81F0, 0x81F1, 0x81F2, 0x81F5, 0x81F6, 0x81F8, 0x81F9, 0x81FD, 0x81FF, 0x8200, 0x8203, 0x820F, 0x8213, 0x8214, 0x8219, 0x821A, 0x821D, 0x8221, 0x8222, 0x8228, 0x8232, 0x8234, 0x823A, 0x8243, 0x8244, 0x8245, 0x8246, + 0x824B, 0x824E, 0x824F, 0x8251, 0x8256, 0x825C, 0x8260, 0x8263, 0x8267, 0x826D, 0x8274, 0x827B, 0x827D, 0x827F, 0x8280, 0x8281, 0x8283, 0x8284, 0x8287, 0x8289, 0x828A, 0x828E, 0x8291, 0x8294, 0x8296, 0x8298, 0x829A, 0x829B, 0x82A0, 0x82A1, 0x82A3, 0x82A4, 0x82A7, 0x82A8, 0x82A9, 0x82AA, 0x82AE, 0x82B0, 0x82B2, 0x82B4, 0x82B7, 0x82BA, 0x82BC, 0x82BE, 0x82BF, 0x82C6, 0x82D0, 0x82D5, 0x82DA, 0x82E0, 0x82E2, 0x82E4, 0x82E8, 0x82EA, 0x82ED, 0x82EF, 0x82F6, 0x82F7, 0x82FD, 0x82FE, 0x8300, 0x8301, 0x8307, 0x8308, 0x830A, 0x830B, 0x8354, 0x831B, 0x831D, 0x831E, 0x831F, 0x8321, 0x8322, 0x832C, 0x832D, 0x832E, 0x8330, 0x8333, 0x8337, 0x833A, 0x833C, 0x833D, 0x8342, 0x8343, 0x8344, 0x8347, 0x834D, 0x834E, 0x8351, 0x8355, 0x8356, 0x8357, 0x8370, 0x8378, + 0x837D, 0x837F, 0x8380, 0x8382, 0x8384, 0x8386, 0x838D, 0x8392, 0x8394, 0x8395, 0x8398, 0x8399, 0x839B, 0x839C, 0x839D, 0x83A6, 0x83A7, 0x83A9, 0x83AC, 0x83BE, 0x83BF, 0x83C0, 0x83C7, 0x83C9, 0x83CF, 0x83D0, 0x83D1, 0x83D4, 0x83DD, 0x8353, 0x83E8, 0x83EA, 0x83F6, 0x83F8, 0x83F9, 0x83FC, 0x8401, 0x8406, 0x840A, 0x840F, 0x8411, 0x8415, 0x8419, 0x83AD, 0x842F, 0x8439, 0x8445, 0x8447, 0x8448, 0x844A, 0x844D, 0x844F, 0x8451, 0x8452, 0x8456, 0x8458, 0x8459, 0x845A, 0x845C, 0x8460, 0x8464, 0x8465, 0x8467, 0x846A, 0x8470, 0x8473, 0x8474, 0x8476, 0x8478, 0x847C, 0x847D, 0x8481, 0x8485, 0x8492, 0x8493, 0x8495, 0x849E, 0x84A6, 0x84A8, 0x84A9, 0x84AA, 0x84AF, 0x84B1, 0x84B4, 0x84BA, 0x84BD, 0x84BE, 0x84C0, 0x84C2, 0x84C7, 0x84C8, 0x84CC, 0x84CF, 0x84D3, + 0x84DC, 0x84E7, 0x84EA, 0x84EF, 0x84F0, 0x84F1, 0x84F2, 0x84F7, 0x8532, 0x84FA, 0x84FB, 0x84FD, 0x8502, 0x8503, 0x8507, 0x850C, 0x850E, 0x8510, 0x851C, 0x851E, 0x8522, 0x8523, 0x8524, 0x8525, 0x8527, 0x852A, 0x852B, 0x852F, 0x8533, 0x8534, 0x8536, 0x853F, 0x8546, 0x854F, 0x8550, 0x8551, 0x8552, 0x8553, 0x8556, 0x8559, 0x855C, 0x855D, 0x855E, 0x855F, 0x8560, 0x8561, 0x8562, 0x8564, 0x856B, 0x856F, 0x8579, 0x857A, 0x857B, 0x857D, 0x857F, 0x8581, 0x8585, 0x8586, 0x8589, 0x858B, 0x858C, 0x858F, 0x8593, 0x8598, 0x859D, 0x859F, 0x85A0, 0x85A2, 0x85A5, 0x85A7, 0x85B4, 0x85B6, 0x85B7, 0x85B8, 0x85BC, 0x85BD, 0x85BE, 0x85BF, 0x85C2, 0x85C7, 0x85CA, 0x85CB, 0x85CE, 0x85AD, 0x85D8, 0x85DA, 0x85DF, 0x85E0, 0x85E6, 0x85E8, 0x85ED, 0x85F3, 0x85F6, 0x85FC, + 0x85FF, 0x8600, 0x8604, 0x8605, 0x860D, 0x860E, 0x8610, 0x8611, 0x8612, 0x8618, 0x8619, 0x861B, 0x861E, 0x8621, 0x8627, 0x8629, 0x8636, 0x8638, 0x863A, 0x863C, 0x863D, 0x8640, 0x8642, 0x8646, 0x8652, 0x8653, 0x8656, 0x8657, 0x8658, 0x8659, 0x865D, 0x8660, 0x8661, 0x8662, 0x8663, 0x8664, 0x8669, 0x866C, 0x866F, 0x8675, 0x8676, 0x8677, 0x867A, 0x868D, 0x8691, 0x8696, 0x8698, 0x869A, 0x869C, 0x86A1, 0x86A6, 0x86A7, 0x86A8, 0x86AD, 0x86B1, 0x86B3, 0x86B4, 0x86B5, 0x86B7, 0x86B8, 0x86B9, 0x86BF, 0x86C0, 0x86C1, 0x86C3, 0x86C5, 0x86D1, 0x86D2, 0x86D5, 0x86D7, 0x86DA, 0x86DC, 0x86E0, 0x86E3, 0x86E5, 0x86E7, 0x8688, 0x86FA, 0x86FC, 0x86FD, 0x8704, 0x8705, 0x8707, 0x870B, 0x870E, 0x870F, 0x8710, 0x8713, 0x8714, 0x8719, 0x871E, 0x871F, 0x8721, 0x8723, + 0x8728, 0x872E, 0x872F, 0x8731, 0x8732, 0x8739, 0x873A, 0x873C, 0x873D, 0x873E, 0x8740, 0x8743, 0x8745, 0x874D, 0x8758, 0x875D, 0x8761, 0x8764, 0x8765, 0x876F, 0x8771, 0x8772, 0x877B, 0x8783, 0x8784, 0x8785, 0x8786, 0x8787, 0x8788, 0x8789, 0x878B, 0x878C, 0x8790, 0x8793, 0x8795, 0x8797, 0x8798, 0x8799, 0x879E, 0x87A0, 0x87A3, 0x87A7, 0x87AC, 0x87AD, 0x87AE, 0x87B1, 0x87B5, 0x87BE, 0x87BF, 0x87C1, 0x87C8, 0x87C9, 0x87CA, 0x87CE, 0x87D5, 0x87D6, 0x87D9, 0x87DA, 0x87DC, 0x87DF, 0x87E2, 0x87E3, 0x87E4, 0x87EA, 0x87EB, 0x87ED, 0x87F1, 0x87F3, 0x87F8, 0x87FA, 0x87FF, 0x8801, 0x8803, 0x8806, 0x8809, 0x880A, 0x880B, 0x8810, 0x8819, 0x8812, 0x8813, 0x8814, 0x8818, 0x881A, 0x881B, 0x881C, 0x881E, 0x881F, 0x8828, 0x882D, 0x882E, 0x8830, 0x8832, 0x8835, + 0x883A, 0x883C, 0x8841, 0x8843, 0x8845, 0x8848, 0x8849, 0x884A, 0x884B, 0x884E, 0x8851, 0x8855, 0x8856, 0x8858, 0x885A, 0x885C, 0x885F, 0x8860, 0x8864, 0x8869, 0x8871, 0x8879, 0x887B, 0x8880, 0x8898, 0x889A, 0x889B, 0x889C, 0x889F, 0x88A0, 0x88A8, 0x88AA, 0x88BA, 0x88BD, 0x88BE, 0x88C0, 0x88CA, 0x88CB, 0x88CC, 0x88CD, 0x88CE, 0x88D1, 0x88D2, 0x88D3, 0x88DB, 0x88DE, 0x88E7, 0x88EF, 0x88F0, 0x88F1, 0x88F5, 0x88F7, 0x8901, 0x8906, 0x890D, 0x890E, 0x890F, 0x8915, 0x8916, 0x8918, 0x8919, 0x891A, 0x891C, 0x8920, 0x8926, 0x8927, 0x8928, 0x8930, 0x8931, 0x8932, 0x8935, 0x8939, 0x893A, 0x893E, 0x8940, 0x8942, 0x8945, 0x8946, 0x8949, 0x894F, 0x8952, 0x8957, 0x895A, 0x895B, 0x895C, 0x8961, 0x8962, 0x8963, 0x896B, 0x896E, 0x8970, 0x8973, 0x8975, 0x897A, + 0x897B, 0x897C, 0x897D, 0x8989, 0x898D, 0x8990, 0x8994, 0x8995, 0x899B, 0x899C, 0x899F, 0x89A0, 0x89A5, 0x89B0, 0x89B4, 0x89B5, 0x89B6, 0x89B7, 0x89BC, 0x89D4, 0x89D5, 0x89D6, 0x89D7, 0x89D8, 0x89E5, 0x89E9, 0x89EB, 0x89ED, 0x89F1, 0x89F3, 0x89F6, 0x89F9, 0x89FD, 0x89FF, 0x8A04, 0x8A05, 0x8A07, 0x8A0F, 0x8A11, 0x8A12, 0x8A14, 0x8A15, 0x8A1E, 0x8A20, 0x8A22, 0x8A24, 0x8A26, 0x8A2B, 0x8A2C, 0x8A2F, 0x8A35, 0x8A37, 0x8A3D, 0x8A3E, 0x8A40, 0x8A43, 0x8A45, 0x8A47, 0x8A49, 0x8A4D, 0x8A4E, 0x8A53, 0x8A56, 0x8A57, 0x8A58, 0x8A5C, 0x8A5D, 0x8A61, 0x8A65, 0x8A67, 0x8A75, 0x8A76, 0x8A77, 0x8A79, 0x8A7A, 0x8A7B, 0x8A7E, 0x8A7F, 0x8A80, 0x8A83, 0x8A86, 0x8A8B, 0x8A8F, 0x8A90, 0x8A92, 0x8A96, 0x8A97, 0x8A99, 0x8A9F, 0x8AA7, 0x8AA9, 0x8AAE, 0x8AAF, 0x8AB3, + 0x8AB6, 0x8AB7, 0x8ABB, 0x8ABE, 0x8AC3, 0x8AC6, 0x8AC8, 0x8AC9, 0x8ACA, 0x8AD1, 0x8AD3, 0x8AD4, 0x8AD5, 0x8AD7, 0x8ADD, 0x8ADF, 0x8AEC, 0x8AF0, 0x8AF4, 0x8AF5, 0x8AF6, 0x8AFC, 0x8AFF, 0x8B05, 0x8B06, 0x8B0B, 0x8B11, 0x8B1C, 0x8B1E, 0x8B1F, 0x8B0A, 0x8B2D, 0x8B30, 0x8B37, 0x8B3C, 0x8B42, 0x8B43, 0x8B44, 0x8B45, 0x8B46, 0x8B48, 0x8B52, 0x8B53, 0x8B54, 0x8B59, 0x8B4D, 0x8B5E, 0x8B63, 0x8B6D, 0x8B76, 0x8B78, 0x8B79, 0x8B7C, 0x8B7E, 0x8B81, 0x8B84, 0x8B85, 0x8B8B, 0x8B8D, 0x8B8F, 0x8B94, 0x8B95, 0x8B9C, 0x8B9E, 0x8B9F, 0x8C38, 0x8C39, 0x8C3D, 0x8C3E, 0x8C45, 0x8C47, 0x8C49, 0x8C4B, 0x8C4F, 0x8C51, 0x8C53, 0x8C54, 0x8C57, 0x8C58, 0x8C5B, 0x8C5D, 0x8C59, 0x8C63, 0x8C64, 0x8C66, 0x8C68, 0x8C69, 0x8C6D, 0x8C73, 0x8C75, 0x8C76, 0x8C7B, 0x8C7E, 0x8C86, + 0x8C87, 0x8C8B, 0x8C90, 0x8C92, 0x8C93, 0x8C99, 0x8C9B, 0x8C9C, 0x8CA4, 0x8CB9, 0x8CBA, 0x8CC5, 0x8CC6, 0x8CC9, 0x8CCB, 0x8CCF, 0x8CD6, 0x8CD5, 0x8CD9, 0x8CDD, 0x8CE1, 0x8CE8, 0x8CEC, 0x8CEF, 0x8CF0, 0x8CF2, 0x8CF5, 0x8CF7, 0x8CF8, 0x8CFE, 0x8CFF, 0x8D01, 0x8D03, 0x8D09, 0x8D12, 0x8D17, 0x8D1B, 0x8D65, 0x8D69, 0x8D6C, 0x8D6E, 0x8D7F, 0x8D82, 0x8D84, 0x8D88, 0x8D8D, 0x8D90, 0x8D91, 0x8D95, 0x8D9E, 0x8D9F, 0x8DA0, 0x8DA6, 0x8DAB, 0x8DAC, 0x8DAF, 0x8DB2, 0x8DB5, 0x8DB7, 0x8DB9, 0x8DBB, 0x8DC0, 0x8DC5, 0x8DC6, 0x8DC7, 0x8DC8, 0x8DCA, 0x8DCE, 0x8DD1, 0x8DD4, 0x8DD5, 0x8DD7, 0x8DD9, 0x8DE4, 0x8DE5, 0x8DE7, 0x8DEC, 0x8DF0, 0x8DBC, 0x8DF1, 0x8DF2, 0x8DF4, 0x8DFD, 0x8E01, 0x8E04, 0x8E05, 0x8E06, 0x8E0B, 0x8E11, 0x8E14, 0x8E16, 0x8E20, 0x8E21, 0x8E22, + 0x8E23, 0x8E26, 0x8E27, 0x8E31, 0x8E33, 0x8E36, 0x8E37, 0x8E38, 0x8E39, 0x8E3D, 0x8E40, 0x8E41, 0x8E4B, 0x8E4D, 0x8E4E, 0x8E4F, 0x8E54, 0x8E5B, 0x8E5C, 0x8E5D, 0x8E5E, 0x8E61, 0x8E62, 0x8E69, 0x8E6C, 0x8E6D, 0x8E6F, 0x8E70, 0x8E71, 0x8E79, 0x8E7A, 0x8E7B, 0x8E82, 0x8E83, 0x8E89, 0x8E90, 0x8E92, 0x8E95, 0x8E9A, 0x8E9B, 0x8E9D, 0x8E9E, 0x8EA2, 0x8EA7, 0x8EA9, 0x8EAD, 0x8EAE, 0x8EB3, 0x8EB5, 0x8EBA, 0x8EBB, 0x8EC0, 0x8EC1, 0x8EC3, 0x8EC4, 0x8EC7, 0x8ECF, 0x8ED1, 0x8ED4, 0x8EDC, 0x8EE8, 0x8EEE, 0x8EF0, 0x8EF1, 0x8EF7, 0x8EF9, 0x8EFA, 0x8EED, 0x8F00, 0x8F02, 0x8F07, 0x8F08, 0x8F0F, 0x8F10, 0x8F16, 0x8F17, 0x8F18, 0x8F1E, 0x8F20, 0x8F21, 0x8F23, 0x8F25, 0x8F27, 0x8F28, 0x8F2C, 0x8F2D, 0x8F2E, 0x8F34, 0x8F35, 0x8F36, 0x8F37, 0x8F3A, 0x8F40, 0x8F41, + 0x8F43, 0x8F47, 0x8F4F, 0x8F51, 0x8F52, 0x8F53, 0x8F54, 0x8F55, 0x8F58, 0x8F5D, 0x8F5E, 0x8F65, 0x8F9D, 0x8FA0, 0x8FA1, 0x8FA4, 0x8FA5, 0x8FA6, 0x8FB5, 0x8FB6, 0x8FB8, 0x8FBE, 0x8FC0, 0x8FC1, 0x8FC6, 0x8FCA, 0x8FCB, 0x8FCD, 0x8FD0, 0x8FD2, 0x8FD3, 0x8FD5, 0x8FE0, 0x8FE3, 0x8FE4, 0x8FE8, 0x8FEE, 0x8FF1, 0x8FF5, 0x8FF6, 0x8FFB, 0x8FFE, 0x9002, 0x9004, 0x9008, 0x900C, 0x9018, 0x901B, 0x9028, 0x9029, 0x902F, 0x902A, 0x902C, 0x902D, 0x9033, 0x9034, 0x9037, 0x903F, 0x9043, 0x9044, 0x904C, 0x905B, 0x905D, 0x9062, 0x9066, 0x9067, 0x906C, 0x9070, 0x9074, 0x9079, 0x9085, 0x9088, 0x908B, 0x908C, 0x908E, 0x9090, 0x9095, 0x9097, 0x9098, 0x9099, 0x909B, 0x90A0, 0x90A1, 0x90A2, 0x90A5, 0x90B0, 0x90B2, 0x90B3, 0x90B4, 0x90B6, 0x90BD, 0x90CC, 0x90BE, 0x90C3, + 0x90C4, 0x90C5, 0x90C7, 0x90C8, 0x90D5, 0x90D7, 0x90D8, 0x90D9, 0x90DC, 0x90DD, 0x90DF, 0x90E5, 0x90D2, 0x90F6, 0x90EB, 0x90EF, 0x90F0, 0x90F4, 0x90FE, 0x90FF, 0x9100, 0x9104, 0x9105, 0x9106, 0x9108, 0x910D, 0x9110, 0x9114, 0x9116, 0x9117, 0x9118, 0x911A, 0x911C, 0x911E, 0x9120, 0x9125, 0x9122, 0x9123, 0x9127, 0x9129, 0x912E, 0x912F, 0x9131, 0x9134, 0x9136, 0x9137, 0x9139, 0x913A, 0x913C, 0x913D, 0x9143, 0x9147, 0x9148, 0x914F, 0x9153, 0x9157, 0x9159, 0x915A, 0x915B, 0x9161, 0x9164, 0x9167, 0x916D, 0x9174, 0x9179, 0x917A, 0x917B, 0x9181, 0x9183, 0x9185, 0x9186, 0x918A, 0x918E, 0x9191, 0x9193, 0x9194, 0x9195, 0x9198, 0x919E, 0x91A1, 0x91A6, 0x91A8, 0x91AC, 0x91AD, 0x91AE, 0x91B0, 0x91B1, 0x91B2, 0x91B3, 0x91B6, 0x91BB, 0x91BC, 0x91BD, 0x91BF, + 0x91C2, 0x91C3, 0x91C5, 0x91D3, 0x91D4, 0x91D7, 0x91D9, 0x91DA, 0x91DE, 0x91E4, 0x91E5, 0x91E9, 0x91EA, 0x91EC, 0x91ED, 0x91EE, 0x91EF, 0x91F0, 0x91F1, 0x91F7, 0x91F9, 0x91FB, 0x91FD, 0x9200, 0x9201, 0x9204, 0x9205, 0x9206, 0x9207, 0x9209, 0x920A, 0x920C, 0x9210, 0x9212, 0x9213, 0x9216, 0x9218, 0x921C, 0x921D, 0x9223, 0x9224, 0x9225, 0x9226, 0x9228, 0x922E, 0x922F, 0x9230, 0x9233, 0x9235, 0x9236, 0x9238, 0x9239, 0x923A, 0x923C, 0x923E, 0x9240, 0x9242, 0x9243, 0x9246, 0x9247, 0x924A, 0x924D, 0x924E, 0x924F, 0x9251, 0x9258, 0x9259, 0x925C, 0x925D, 0x9260, 0x9261, 0x9265, 0x9267, 0x9268, 0x9269, 0x926E, 0x926F, 0x9270, 0x9275, 0x9276, 0x9277, 0x9278, 0x9279, 0x927B, 0x927C, 0x927D, 0x927F, 0x9288, 0x9289, 0x928A, 0x928D, 0x928E, 0x9292, 0x9297, + 0x9299, 0x929F, 0x92A0, 0x92A4, 0x92A5, 0x92A7, 0x92A8, 0x92AB, 0x92AF, 0x92B2, 0x92B6, 0x92B8, 0x92BA, 0x92BB, 0x92BC, 0x92BD, 0x92BF, 0x92C0, 0x92C1, 0x92C2, 0x92C3, 0x92C5, 0x92C6, 0x92C7, 0x92C8, 0x92CB, 0x92CC, 0x92CD, 0x92CE, 0x92D0, 0x92D3, 0x92D5, 0x92D7, 0x92D8, 0x92D9, 0x92DC, 0x92DD, 0x92DF, 0x92E0, 0x92E1, 0x92E3, 0x92E5, 0x92E7, 0x92E8, 0x92EC, 0x92EE, 0x92F0, 0x92F9, 0x92FB, 0x92FF, 0x9300, 0x9302, 0x9308, 0x930D, 0x9311, 0x9314, 0x9315, 0x931C, 0x931D, 0x931E, 0x931F, 0x9321, 0x9324, 0x9325, 0x9327, 0x9329, 0x932A, 0x9333, 0x9334, 0x9336, 0x9337, 0x9347, 0x9348, 0x9349, 0x9350, 0x9351, 0x9352, 0x9355, 0x9357, 0x9358, 0x935A, 0x935E, 0x9364, 0x9365, 0x9367, 0x9369, 0x936A, 0x936D, 0x936F, 0x9370, 0x9371, 0x9373, 0x9374, 0x9376, + 0x937A, 0x937D, 0x937F, 0x9380, 0x9381, 0x9382, 0x9388, 0x938A, 0x938B, 0x938D, 0x938F, 0x9392, 0x9395, 0x9398, 0x939B, 0x939E, 0x93A1, 0x93A3, 0x93A4, 0x93A6, 0x93A8, 0x93AB, 0x93B4, 0x93B5, 0x93B6, 0x93BA, 0x93A9, 0x93C1, 0x93C4, 0x93C5, 0x93C6, 0x93C7, 0x93C9, 0x93CA, 0x93CB, 0x93CC, 0x93CD, 0x93D3, 0x93D9, 0x93DC, 0x93DE, 0x93DF, 0x93E2, 0x93E6, 0x93E7, 0x93F9, 0x93F7, 0x93F8, 0x93FA, 0x93FB, 0x93FD, 0x9401, 0x9402, 0x9404, 0x9408, 0x9409, 0x940D, 0x940E, 0x940F, 0x9415, 0x9416, 0x9417, 0x941F, 0x942E, 0x942F, 0x9431, 0x9432, 0x9433, 0x9434, 0x943B, 0x943F, 0x943D, 0x9443, 0x9445, 0x9448, 0x944A, 0x944C, 0x9455, 0x9459, 0x945C, 0x945F, 0x9461, 0x9463, 0x9468, 0x946B, 0x946D, 0x946E, 0x946F, 0x9471, 0x9472, 0x9484, 0x9483, 0x9578, 0x9579, + 0x957E, 0x9584, 0x9588, 0x958C, 0x958D, 0x958E, 0x959D, 0x959E, 0x959F, 0x95A1, 0x95A6, 0x95A9, 0x95AB, 0x95AC, 0x95B4, 0x95B6, 0x95BA, 0x95BD, 0x95BF, 0x95C6, 0x95C8, 0x95C9, 0x95CB, 0x95D0, 0x95D1, 0x95D2, 0x95D3, 0x95D9, 0x95DA, 0x95DD, 0x95DE, 0x95DF, 0x95E0, 0x95E4, 0x95E6, 0x961D, 0x961E, 0x9622, 0x9624, 0x9625, 0x9626, 0x962C, 0x9631, 0x9633, 0x9637, 0x9638, 0x9639, 0x963A, 0x963C, 0x963D, 0x9641, 0x9652, 0x9654, 0x9656, 0x9657, 0x9658, 0x9661, 0x966E, 0x9674, 0x967B, 0x967C, 0x967E, 0x967F, 0x9681, 0x9682, 0x9683, 0x9684, 0x9689, 0x9691, 0x9696, 0x969A, 0x969D, 0x969F, 0x96A4, 0x96A5, 0x96A6, 0x96A9, 0x96AE, 0x96AF, 0x96B3, 0x96BA, 0x96CA, 0x96D2, 0x5DB2, 0x96D8, 0x96DA, 0x96DD, 0x96DE, 0x96DF, 0x96E9, 0x96EF, 0x96F1, 0x96FA, 0x9702, + 0x9703, 0x9705, 0x9709, 0x971A, 0x971B, 0x971D, 0x9721, 0x9722, 0x9723, 0x9728, 0x9731, 0x9733, 0x9741, 0x9743, 0x974A, 0x974E, 0x974F, 0x9755, 0x9757, 0x9758, 0x975A, 0x975B, 0x9763, 0x9767, 0x976A, 0x976E, 0x9773, 0x9776, 0x9777, 0x9778, 0x977B, 0x977D, 0x977F, 0x9780, 0x9789, 0x9795, 0x9796, 0x9797, 0x9799, 0x979A, 0x979E, 0x979F, 0x97A2, 0x97AC, 0x97AE, 0x97B1, 0x97B2, 0x97B5, 0x97B6, 0x97B8, 0x97B9, 0x97BA, 0x97BC, 0x97BE, 0x97BF, 0x97C1, 0x97C4, 0x97C5, 0x97C7, 0x97C9, 0x97CA, 0x97CC, 0x97CD, 0x97CE, 0x97D0, 0x97D1, 0x97D4, 0x97D7, 0x97D8, 0x97D9, 0x97DD, 0x97DE, 0x97E0, 0x97DB, 0x97E1, 0x97E4, 0x97EF, 0x97F1, 0x97F4, 0x97F7, 0x97F8, 0x97FA, 0x9807, 0x980A, 0x9819, 0x980D, 0x980E, 0x9814, 0x9816, 0x981C, 0x981E, 0x9820, 0x9823, 0x9826, + 0x982B, 0x982E, 0x982F, 0x9830, 0x9832, 0x9833, 0x9835, 0x9825, 0x983E, 0x9844, 0x9847, 0x984A, 0x9851, 0x9852, 0x9853, 0x9856, 0x9857, 0x9859, 0x985A, 0x9862, 0x9863, 0x9865, 0x9866, 0x986A, 0x986C, 0x98AB, 0x98AD, 0x98AE, 0x98B0, 0x98B4, 0x98B7, 0x98B8, 0x98BA, 0x98BB, 0x98BF, 0x98C2, 0x98C5, 0x98C8, 0x98CC, 0x98E1, 0x98E3, 0x98E5, 0x98E6, 0x98E7, 0x98EA, 0x98F3, 0x98F6, 0x9902, 0x9907, 0x9908, 0x9911, 0x9915, 0x9916, 0x9917, 0x991A, 0x991B, 0x991C, 0x991F, 0x9922, 0x9926, 0x9927, 0x992B, 0x9931, 0x9932, 0x9933, 0x9934, 0x9935, 0x9939, 0x993A, 0x993B, 0x993C, 0x9940, 0x9941, 0x9946, 0x9947, 0x9948, 0x994D, 0x994E, 0x9954, 0x9958, 0x9959, 0x995B, 0x995C, 0x995E, 0x995F, 0x9960, 0x999B, 0x999D, 0x999F, 0x99A6, 0x99B0, 0x99B1, 0x99B2, 0x99B5, + 0x99B9, 0x99BA, 0x99BD, 0x99BF, 0x99C3, 0x99C9, 0x99D3, 0x99D4, 0x99D9, 0x99DA, 0x99DC, 0x99DE, 0x99E7, 0x99EA, 0x99EB, 0x99EC, 0x99F0, 0x99F4, 0x99F5, 0x99F9, 0x99FD, 0x99FE, 0x9A02, 0x9A03, 0x9A04, 0x9A0B, 0x9A0C, 0x9A10, 0x9A11, 0x9A16, 0x9A1E, 0x9A20, 0x9A22, 0x9A23, 0x9A24, 0x9A27, 0x9A2D, 0x9A2E, 0x9A33, 0x9A35, 0x9A36, 0x9A38, 0x9A47, 0x9A41, 0x9A44, 0x9A4A, 0x9A4B, 0x9A4C, 0x9A4E, 0x9A51, 0x9A54, 0x9A56, 0x9A5D, 0x9AAA, 0x9AAC, 0x9AAE, 0x9AAF, 0x9AB2, 0x9AB4, 0x9AB5, 0x9AB6, 0x9AB9, 0x9ABB, 0x9ABE, 0x9ABF, 0x9AC1, 0x9AC3, 0x9AC6, 0x9AC8, 0x9ACE, 0x9AD0, 0x9AD2, 0x9AD5, 0x9AD6, 0x9AD7, 0x9ADB, 0x9ADC, 0x9AE0, 0x9AE4, 0x9AE5, 0x9AE7, 0x9AE9, 0x9AEC, 0x9AF2, 0x9AF3, 0x9AF5, 0x9AF9, 0x9AFA, 0x9AFD, 0x9AFF, 0x9B00, 0x9B01, 0x9B02, 0x9B03, + 0x9B04, 0x9B05, 0x9B08, 0x9B09, 0x9B0B, 0x9B0C, 0x9B0D, 0x9B0E, 0x9B10, 0x9B12, 0x9B16, 0x9B19, 0x9B1B, 0x9B1C, 0x9B20, 0x9B26, 0x9B2B, 0x9B2D, 0x9B33, 0x9B34, 0x9B35, 0x9B37, 0x9B39, 0x9B3A, 0x9B3D, 0x9B48, 0x9B4B, 0x9B4C, 0x9B55, 0x9B56, 0x9B57, 0x9B5B, 0x9B5E, 0x9B61, 0x9B63, 0x9B65, 0x9B66, 0x9B68, 0x9B6A, 0x9B6B, 0x9B6C, 0x9B6D, 0x9B6E, 0x9B73, 0x9B75, 0x9B77, 0x9B78, 0x9B79, 0x9B7F, 0x9B80, 0x9B84, 0x9B85, 0x9B86, 0x9B87, 0x9B89, 0x9B8A, 0x9B8B, 0x9B8D, 0x9B8F, 0x9B90, 0x9B94, 0x9B9A, 0x9B9D, 0x9B9E, 0x9BA6, 0x9BA7, 0x9BA9, 0x9BAC, 0x9BB0, 0x9BB1, 0x9BB2, 0x9BB7, 0x9BB8, 0x9BBB, 0x9BBC, 0x9BBE, 0x9BBF, 0x9BC1, 0x9BC7, 0x9BC8, 0x9BCE, 0x9BD0, 0x9BD7, 0x9BD8, 0x9BDD, 0x9BDF, 0x9BE5, 0x9BE7, 0x9BEA, 0x9BEB, 0x9BEF, 0x9BF3, 0x9BF7, 0x9BF8, + 0x9BF9, 0x9BFA, 0x9BFD, 0x9BFF, 0x9C00, 0x9C02, 0x9C0B, 0x9C0F, 0x9C11, 0x9C16, 0x9C18, 0x9C19, 0x9C1A, 0x9C1C, 0x9C1E, 0x9C22, 0x9C23, 0x9C26, 0x9C27, 0x9C28, 0x9C29, 0x9C2A, 0x9C31, 0x9C35, 0x9C36, 0x9C37, 0x9C3D, 0x9C41, 0x9C43, 0x9C44, 0x9C45, 0x9C49, 0x9C4A, 0x9C4E, 0x9C4F, 0x9C50, 0x9C53, 0x9C54, 0x9C56, 0x9C58, 0x9C5B, 0x9C5D, 0x9C5E, 0x9C5F, 0x9C63, 0x9C69, 0x9C6A, 0x9C5C, 0x9C6B, 0x9C68, 0x9C6E, 0x9C70, 0x9C72, 0x9C75, 0x9C77, 0x9C7B, 0x9CE6, 0x9CF2, 0x9CF7, 0x9CF9, 0x9D0B, 0x9D02, 0x9D11, 0x9D17, 0x9D18, 0x9D1C, 0x9D1D, 0x9D1E, 0x9D2F, 0x9D30, 0x9D32, 0x9D33, 0x9D34, 0x9D3A, 0x9D3C, 0x9D45, 0x9D3D, 0x9D42, 0x9D43, 0x9D47, 0x9D4A, 0x9D53, 0x9D54, 0x9D5F, 0x9D63, 0x9D62, 0x9D65, 0x9D69, 0x9D6A, 0x9D6B, 0x9D70, 0x9D76, 0x9D77, 0x9D7B, + 0x9D7C, 0x9D7E, 0x9D83, 0x9D84, 0x9D86, 0x9D8A, 0x9D8D, 0x9D8E, 0x9D92, 0x9D93, 0x9D95, 0x9D96, 0x9D97, 0x9D98, 0x9DA1, 0x9DAA, 0x9DAC, 0x9DAE, 0x9DB1, 0x9DB5, 0x9DB9, 0x9DBC, 0x9DBF, 0x9DC3, 0x9DC7, 0x9DC9, 0x9DCA, 0x9DD4, 0x9DD5, 0x9DD6, 0x9DD7, 0x9DDA, 0x9DDE, 0x9DDF, 0x9DE0, 0x9DE5, 0x9DE7, 0x9DE9, 0x9DEB, 0x9DEE, 0x9DF0, 0x9DF3, 0x9DF4, 0x9DFE, 0x9E0A, 0x9E02, 0x9E07, 0x9E0E, 0x9E10, 0x9E11, 0x9E12, 0x9E15, 0x9E16, 0x9E19, 0x9E1C, 0x9E1D, 0x9E7A, 0x9E7B, 0x9E7C, 0x9E80, 0x9E82, 0x9E83, 0x9E84, 0x9E85, 0x9E87, 0x9E8E, 0x9E8F, 0x9E96, 0x9E98, 0x9E9B, 0x9E9E, 0x9EA4, 0x9EA8, 0x9EAC, 0x9EAE, 0x9EAF, 0x9EB0, 0x9EB3, 0x9EB4, 0x9EB5, 0x9EC6, 0x9EC8, 0x9ECB, 0x9ED5, 0x9EDF, 0x9EE4, 0x9EE7, 0x9EEC, 0x9EED, 0x9EEE, 0x9EF0, 0x9EF1, 0x9EF2, 0x9EF5, + 0x9EF8, 0x9EFF, 0x9F02, 0x9F03, 0x9F09, 0x9F0F, 0x9F10, 0x9F11, 0x9F12, 0x9F14, 0x9F16, 0x9F17, 0x9F19, 0x9F1A, 0x9F1B, 0x9F1F, 0x9F22, 0x9F26, 0x9F2A, 0x9F2B, 0x9F2F, 0x9F31, 0x9F32, 0x9F34, 0x9F37, 0x9F39, 0x9F3A, 0x9F3C, 0x9F3D, 0x9F3F, 0x9F41, 0x9F43, 0x9F44, 0x9F45, 0x9F46, 0x9F47, 0x9F53, 0x9F55, 0x9F56, 0x9F57, 0x9F58, 0x9F5A, 0x9F5D, 0x9F5E, 0x9F68, 0x9F69, 0x9F6D, 0x9F6E, 0x9F6F, 0x9F70, 0x9F71, 0x9F73, 0x9F75, 0x9F7A, 0x9F7D, 0x9F8F, 0x9F90, 0x9F91, 0x9F92, 0x9F94, 0x9F96, 0x9F97, 0x9F9E, 0x9FA1, 0x9FA2, 0x9FA3, 0x9FA5, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, +}; + +} // namespace JisX0212 diff --git a/core/libs/prx/libSceCesCs/PendingExports.cpp b/core/libs/prx/libSceCesCs/PendingExports.cpp new file mode 100644 index 0000000000..31f8c54b02 --- /dev/null +++ b/core/libs/prx/libSceCesCs/PendingExports.cpp @@ -0,0 +1,1115 @@ +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceCesBig5ToUcs2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesBig5ToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesBig5ToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesBig5ToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesBig5ToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesBig5ToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesBig5ToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnToUcs2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnToUtf8(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnUcsProfileSetSbcs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucJpToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucJpToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucJpToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucJpToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucJpToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucJpToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucJpUcsProfileSetSbcs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucKrToUcs2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucKrToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucKrToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucKrToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucKrToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucKrToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucKrToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucKrUcsProfileSetSbcs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbToUcs2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbToUtf8(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbUcsProfileSet4ByteCharRange(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbUcsProfileSetSbcs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbUcsProfileSetUdaMapping(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022JpUcsProfileSetSbcs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022StrGetUcs2Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022StrGetUtf16Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022StrGetUtf32Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022StrGetUtf8Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022StrToUcs2Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022StrToUtf16Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022StrToUtf32Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022StrToUtf8Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022UcsContextInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022UcsContextInitCopy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesJisGetLevel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesJisX0208ToSJisCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesJisX0213ToSJisCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesJiscsToUcs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcToUcs2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcToUtf8(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcsStrGetUtf16Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcsStrGetUtf32Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcsStrToUtf16Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcsStrToUtf32Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcsUcsContextInitCopy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcsUcsSetMbcsReplacementCharCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcsUcsSetMbcsReplacementCharUCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileAscii(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileAsciiWithKatakana(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileBs4730(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp1253(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp1255(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp1257(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp857(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp858(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp864(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp869(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp874(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileGbT1988(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileGbT1988Tilde0x7e(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_11(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_3(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_7(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_8(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileJisX0201(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileJisX0201Katakana(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileJisX0201Roman(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileJisX0201RomanTilde0x7e(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileJisX0201Tilde0x7e(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileKsX1003(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileKsX1003Tilde0x7e(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisCodeToJisX0208(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisCodeToJisX0213(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisGetCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisPutCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisToUcs2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisToUtf8(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisUcsProfileSetSbcs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSbcToUcs2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSbcToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSbcToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSbcToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSbcToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSbcToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSbcToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2StrGetIso2022Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2StrToIso2022Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToBig5(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToEucCn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToEucJp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToEucKr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToGb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToMbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToSJis(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToSbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToUhc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitEucCnGb2312(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitEucJis2004(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitEucJpX0208(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitEucJpX0208Ss2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitEucJpX0208Ss2Ss3(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitGb18030_2000(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitGb18030_2005(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitGbkCp936(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitIso2022JpCp50221(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitIso2022JpCp50222(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitIso2022JpRfc1468(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitSJis1997X0208(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitSJis2004X0213(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsToJiscs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUhcToUcs2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUhcToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUhcToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUhcToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUhcToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUhcToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUhcToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUhcUcsProfileSetSbcs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16StrGetIso2022Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16StrGetMbcsLen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16StrToIso2022Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16StrToMbcsStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToBig5(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToEucCn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToEucJp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToEucKr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToGb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToMbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToSJis(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToSbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToUhc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToBig5(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToEucCn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToEucJp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToEucKr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToGb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToMbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToSJis(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToSbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToUhc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToBig5(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToEucCn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToEucJp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToEucKr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToGb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToMbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToSJis(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToSbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToUhc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32StrGetIso2022Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32StrGetMbcsLen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32StrToIso2022Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32StrToMbcsStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToBig5(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToEucCn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToEucJp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToEucKr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToGb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToMbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToSJis(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToSbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToUhc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToBig5(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToEucCn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToEucJp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToEucKr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToGb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToMbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToSJis(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToSbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToUhc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToBig5(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToEucCn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToEucJp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToEucKr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToGb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToMbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToSJis(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToSbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToUhc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8StrGetIso2022Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8StrToIso2022Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8ToBig5(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8ToEucCn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8ToEucJp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8ToEucKr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8ToGb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8ToMbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8ToSJis(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8ToUhc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceCesCs/SingleByte.hpp b/core/libs/prx/libSceCesCs/SingleByte.hpp new file mode 100644 index 0000000000..ebe46104cd --- /dev/null +++ b/core/libs/prx/libSceCesCs/SingleByte.hpp @@ -0,0 +1,312 @@ +#pragma once + +#include +#include + +namespace SingleByte { + +enum Table : std::size_t { + Iso8859_1, + Iso8859_2, + Iso8859_4, + Iso8859_5, + Iso8859_9, + Iso8859_10, + Iso8859_13, + Iso8859_14, + Iso8859_15, + Iso8859_16, + Cp437, + Cp737, + Cp775, + Cp850, + Cp852, + Cp855, + Cp860, + Cp861, + Cp862, + Cp863, + Cp865, + Cp866, + Koi8R, + Koi8U, + Cp1254, + Cp1256, + Cp1258, + Count, +}; + +inline constexpr uint16_t HIGH[Count][128] = { + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, + 0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF, + 0x00D0, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D7, 0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x00DD, 0x00DE, 0x00DF, + 0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF, + 0x00F0, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x00FF, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x0104, 0x02D8, 0x0141, 0x00A4, 0x013D, 0x015A, 0x00A7, 0x00A8, 0x0160, 0x015E, 0x0164, 0x0179, 0x00AD, 0x017D, 0x017B, + 0x00B0, 0x0105, 0x02DB, 0x0142, 0x00B4, 0x013E, 0x015B, 0x02C7, 0x00B8, 0x0161, 0x015F, 0x0165, 0x017A, 0x02DD, 0x017E, 0x017C, + 0x0154, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x0139, 0x0106, 0x00C7, 0x010C, 0x00C9, 0x0118, 0x00CB, 0x011A, 0x00CD, 0x00CE, 0x010E, + 0x0110, 0x0143, 0x0147, 0x00D3, 0x00D4, 0x0150, 0x00D6, 0x00D7, 0x0158, 0x016E, 0x00DA, 0x0170, 0x00DC, 0x00DD, 0x0162, 0x00DF, + 0x0155, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x013A, 0x0107, 0x00E7, 0x010D, 0x00E9, 0x0119, 0x00EB, 0x011B, 0x00ED, 0x00EE, 0x010F, + 0x0111, 0x0144, 0x0148, 0x00F3, 0x00F4, 0x0151, 0x00F6, 0x00F7, 0x0159, 0x016F, 0x00FA, 0x0171, 0x00FC, 0x00FD, 0x0163, 0x02D9, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x0104, 0x0138, 0x0156, 0x00A4, 0x0128, 0x013B, 0x00A7, 0x00A8, 0x0160, 0x0112, 0x0122, 0x0166, 0x00AD, 0x017D, 0x00AF, + 0x00B0, 0x0105, 0x02DB, 0x0157, 0x00B4, 0x0129, 0x013C, 0x02C7, 0x00B8, 0x0161, 0x0113, 0x0123, 0x0167, 0x014A, 0x017E, 0x014B, + 0x0100, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x012E, 0x010C, 0x00C9, 0x0118, 0x00CB, 0x0116, 0x00CD, 0x00CE, 0x012A, + 0x0110, 0x0145, 0x014C, 0x0136, 0x00D4, 0x00D5, 0x00D6, 0x00D7, 0x00D8, 0x0172, 0x00DA, 0x00DB, 0x00DC, 0x0168, 0x016A, 0x00DF, + 0x0101, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x012F, 0x010D, 0x00E9, 0x0119, 0x00EB, 0x0117, 0x00ED, 0x00EE, 0x012B, + 0x0111, 0x0146, 0x014D, 0x0137, 0x00F4, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x0173, 0x00FA, 0x00FB, 0x00FC, 0x0169, 0x016B, 0x02D9, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x0401, 0x0402, 0x0403, 0x0404, 0x0405, 0x0406, 0x0407, 0x0408, 0x0409, 0x040A, 0x040B, 0x040C, 0x00AD, 0x040E, 0x040F, + 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, + 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, + 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F, + 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F, + 0x2116, 0x0451, 0x0452, 0x0453, 0x0454, 0x0455, 0x0456, 0x0457, 0x0458, 0x0459, 0x045A, 0x045B, 0x045C, 0x00A7, 0x045E, 0x045F, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, + 0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF, + 0x011E, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D7, 0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x0130, 0x015E, 0x00DF, + 0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF, + 0x011F, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x0131, 0x015F, 0x00FF, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x0104, 0x0112, 0x0122, 0x012A, 0x0128, 0x0136, 0x00A7, 0x013B, 0x0110, 0x0160, 0x0166, 0x017D, 0x00AD, 0x016A, 0x014A, + 0x00B0, 0x0105, 0x0113, 0x0123, 0x012B, 0x0129, 0x0137, 0x00B7, 0x013C, 0x0111, 0x0161, 0x0167, 0x017E, 0x2015, 0x016B, 0x014B, + 0x0100, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x012E, 0x010C, 0x00C9, 0x0118, 0x00CB, 0x0116, 0x00CD, 0x00CE, 0x00CF, + 0x00D0, 0x0145, 0x014C, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x0168, 0x00D8, 0x0172, 0x00DA, 0x00DB, 0x00DC, 0x00DD, 0x00DE, 0x00DF, + 0x0101, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x012F, 0x010D, 0x00E9, 0x0119, 0x00EB, 0x0117, 0x00ED, 0x00EE, 0x00EF, + 0x00F0, 0x0146, 0x014D, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x0169, 0x00F8, 0x0173, 0x00FA, 0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x0138, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x201D, 0x00A2, 0x00A3, 0x00A4, 0x201E, 0x00A6, 0x00A7, 0x00D8, 0x00A9, 0x0156, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00C6, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x201C, 0x00B5, 0x00B6, 0x00B7, 0x00F8, 0x00B9, 0x0157, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00E6, + 0x0104, 0x012E, 0x0100, 0x0106, 0x00C4, 0x00C5, 0x0118, 0x0112, 0x010C, 0x00C9, 0x0179, 0x0116, 0x0122, 0x0136, 0x012A, 0x013B, + 0x0160, 0x0143, 0x0145, 0x00D3, 0x014C, 0x00D5, 0x00D6, 0x00D7, 0x0172, 0x0141, 0x015A, 0x016A, 0x00DC, 0x017B, 0x017D, 0x00DF, + 0x0105, 0x012F, 0x0101, 0x0107, 0x00E4, 0x00E5, 0x0119, 0x0113, 0x010D, 0x00E9, 0x017A, 0x0117, 0x0123, 0x0137, 0x012B, 0x013C, + 0x0161, 0x0144, 0x0146, 0x00F3, 0x014D, 0x00F5, 0x00F6, 0x00F7, 0x0173, 0x0142, 0x015B, 0x016B, 0x00FC, 0x017C, 0x017E, 0x2019, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x1E02, 0x1E03, 0x00A3, 0x010A, 0x010B, 0x1E0A, 0x00A7, 0x1E80, 0x00A9, 0x1E82, 0x1E0B, 0x1EF2, 0x00AD, 0x00AE, 0x0178, + 0x1E1E, 0x1E1F, 0x0120, 0x0121, 0x1E40, 0x1E41, 0x00B6, 0x1E56, 0x1E81, 0x1E57, 0x1E83, 0x1E60, 0x1EF3, 0x1E84, 0x1E85, 0x1E61, + 0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF, + 0x0174, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x1E6A, 0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x00DD, 0x0176, 0x00DF, + 0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF, + 0x0175, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x1E6B, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x00FD, 0x0177, 0x00FF, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x00A1, 0x00A2, 0x00A3, 0x20AC, 0x00A5, 0x0160, 0x00A7, 0x0161, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x017D, 0x00B5, 0x00B6, 0x00B7, 0x017E, 0x00B9, 0x00BA, 0x00BB, 0x0152, 0x0153, 0x0178, 0x00BF, + 0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF, + 0x00D0, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D7, 0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x00DD, 0x00DE, 0x00DF, + 0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF, + 0x00F0, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x00FF, + }, + { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x0104, 0x0105, 0x0141, 0x20AC, 0x201E, 0x0160, 0x00A7, 0x0161, 0x00A9, 0x0218, 0x00AB, 0x0179, 0x00AD, 0x017A, 0x017B, + 0x00B0, 0x00B1, 0x010C, 0x0142, 0x017D, 0x201D, 0x00B6, 0x00B7, 0x017E, 0x010D, 0x0219, 0x00BB, 0x0152, 0x0153, 0x0178, 0x017C, + 0x00C0, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x0106, 0x00C6, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF, + 0x0110, 0x0143, 0x00D2, 0x00D3, 0x00D4, 0x0150, 0x00D6, 0x015A, 0x0170, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x0118, 0x021A, 0x00DF, + 0x00E0, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x0107, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF, + 0x0111, 0x0144, 0x00F2, 0x00F3, 0x00F4, 0x0151, 0x00F6, 0x015B, 0x0171, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x0119, 0x021B, 0x00FF, + }, + { + 0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5, + 0x00C9, 0x00E6, 0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9, 0x00FF, 0x00D6, 0x00DC, 0x00A2, 0x00A3, 0x00A5, 0x20A7, 0x0192, + 0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, 0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229, + 0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, 0x03A0, + 0x03A1, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, 0x03B8, + 0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF, 0x03C0, 0x03C1, 0x03C3, 0x03C2, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x03C9, 0x03AC, 0x03AD, 0x03AE, 0x03CA, 0x03AF, 0x03CC, 0x03CD, 0x03CB, 0x03CE, 0x0386, 0x0388, 0x0389, 0x038A, 0x038C, 0x038E, + 0x038F, 0x00B1, 0x2265, 0x2264, 0x03AA, 0x03AB, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x0106, 0x00FC, 0x00E9, 0x0101, 0x00E4, 0x0123, 0x00E5, 0x0107, 0x0142, 0x0113, 0x0156, 0x0157, 0x012B, 0x0179, 0x00C4, 0x00C5, + 0x00C9, 0x00E6, 0x00C6, 0x014D, 0x00F6, 0x0122, 0x00A2, 0x015A, 0x015B, 0x00D6, 0x00DC, 0x00F8, 0x00A3, 0x00D8, 0x00D7, 0x00A4, + 0x0100, 0x012A, 0x00F3, 0x017B, 0x017C, 0x017A, 0x201D, 0x00A6, 0x00A9, 0x00AE, 0x00AC, 0x00BD, 0x00BC, 0x0141, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x0104, 0x010C, 0x0118, 0x0116, 0x2563, 0x2551, 0x2557, 0x255D, 0x012E, 0x0160, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x0172, 0x016A, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x017D, + 0x0105, 0x010D, 0x0119, 0x0117, 0x012F, 0x0161, 0x0173, 0x016B, 0x017E, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x00D3, 0x00DF, 0x014C, 0x0143, 0x00F5, 0x00D5, 0x00B5, 0x0144, 0x0136, 0x0137, 0x013B, 0x013C, 0x0146, 0x0112, 0x0145, 0x2019, + 0x00AD, 0x00B1, 0x201C, 0x00BE, 0x00B6, 0x00A7, 0x00F7, 0x201E, 0x00B0, 0x2219, 0x00B7, 0x00B9, 0x00B3, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5, + 0x00C9, 0x00E6, 0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9, 0x00FF, 0x00D6, 0x00DC, 0x00F8, 0x00A3, 0x00D8, 0x00D7, 0x0192, + 0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x00AE, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x00C1, 0x00C2, 0x00C0, 0x00A9, 0x2563, 0x2551, 0x2557, 0x255D, 0x00A2, 0x00A5, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x00E3, 0x00C3, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x00A4, + 0x00F0, 0x00D0, 0x00CA, 0x00CB, 0x00C8, 0x0131, 0x00CD, 0x00CE, 0x00CF, 0x2518, 0x250C, 0x2588, 0x2584, 0x00A6, 0x00CC, 0x2580, + 0x00D3, 0x00DF, 0x00D4, 0x00D2, 0x00F5, 0x00D5, 0x00B5, 0x00FE, 0x00DE, 0x00DA, 0x00DB, 0x00D9, 0x00FD, 0x00DD, 0x00AF, 0x00B4, + 0x00AD, 0x00B1, 0x2017, 0x00BE, 0x00B6, 0x00A7, 0x00F7, 0x00B8, 0x00B0, 0x00A8, 0x00B7, 0x00B9, 0x00B3, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x016F, 0x0107, 0x00E7, 0x0142, 0x00EB, 0x0150, 0x0151, 0x00EE, 0x0179, 0x00C4, 0x0106, + 0x00C9, 0x0139, 0x013A, 0x00F4, 0x00F6, 0x013D, 0x013E, 0x015A, 0x015B, 0x00D6, 0x00DC, 0x0164, 0x0165, 0x0141, 0x00D7, 0x010D, + 0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x0104, 0x0105, 0x017D, 0x017E, 0x0118, 0x0119, 0x00AC, 0x017A, 0x010C, 0x015F, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x00C1, 0x00C2, 0x011A, 0x015E, 0x2563, 0x2551, 0x2557, 0x255D, 0x017B, 0x017C, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x0102, 0x0103, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x00A4, + 0x0111, 0x0110, 0x010E, 0x00CB, 0x010F, 0x0147, 0x00CD, 0x00CE, 0x011B, 0x2518, 0x250C, 0x2588, 0x2584, 0x0162, 0x016E, 0x2580, + 0x00D3, 0x00DF, 0x00D4, 0x0143, 0x0144, 0x0148, 0x0160, 0x0161, 0x0154, 0x00DA, 0x0155, 0x0170, 0x00FD, 0x00DD, 0x0163, 0x00B4, + 0x00AD, 0x02DD, 0x02DB, 0x02C7, 0x02D8, 0x00A7, 0x00F7, 0x00B8, 0x00B0, 0x00A8, 0x02D9, 0x0171, 0x0158, 0x0159, 0x25A0, 0x00A0, + }, + { + 0x0452, 0x0402, 0x0453, 0x0403, 0x0451, 0x0401, 0x0454, 0x0404, 0x0455, 0x0405, 0x0456, 0x0406, 0x0457, 0x0407, 0x0458, 0x0408, + 0x0459, 0x0409, 0x045A, 0x040A, 0x045B, 0x040B, 0x045C, 0x040C, 0x045E, 0x040E, 0x045F, 0x040F, 0x044E, 0x042E, 0x044A, 0x042A, + 0x0430, 0x0410, 0x0431, 0x0411, 0x0446, 0x0426, 0x0434, 0x0414, 0x0435, 0x0415, 0x0444, 0x0424, 0x0433, 0x0413, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x0445, 0x0425, 0x0438, 0x0418, 0x2563, 0x2551, 0x2557, 0x255D, 0x0439, 0x0419, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x043A, 0x041A, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x00A4, + 0x043B, 0x041B, 0x043C, 0x041C, 0x043D, 0x041D, 0x043E, 0x041E, 0x043F, 0x2518, 0x250C, 0x2588, 0x2584, 0x041F, 0x044F, 0x2580, + 0x042F, 0x0440, 0x0420, 0x0441, 0x0421, 0x0442, 0x0422, 0x0443, 0x0423, 0x0436, 0x0416, 0x0432, 0x0412, 0x044C, 0x042C, 0x2116, + 0x00AD, 0x044B, 0x042B, 0x0437, 0x0417, 0x0448, 0x0428, 0x044D, 0x042D, 0x0449, 0x0429, 0x0447, 0x0427, 0x00A7, 0x25A0, 0x00A0, + }, + { + 0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E3, 0x00E0, 0x00C1, 0x00E7, 0x00EA, 0x00CA, 0x00E8, 0x00CD, 0x00D4, 0x00EC, 0x00C3, 0x00C2, + 0x00C9, 0x00C0, 0x00C8, 0x00F4, 0x00F5, 0x00F2, 0x00DA, 0x00F9, 0x00CC, 0x00D5, 0x00DC, 0x00A2, 0x00A3, 0x00D9, 0x20A7, 0x00D3, + 0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x00D2, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, 0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229, + 0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00D0, 0x00F0, 0x00DE, 0x00C4, 0x00C5, + 0x00C9, 0x00E6, 0x00C6, 0x00F4, 0x00F6, 0x00FE, 0x00FB, 0x00DD, 0x00FD, 0x00D6, 0x00DC, 0x00F8, 0x00A3, 0x00D8, 0x20A7, 0x0192, + 0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00C1, 0x00CD, 0x00D3, 0x00DA, 0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, 0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229, + 0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x05D0, 0x05D1, 0x05D2, 0x05D3, 0x05D4, 0x05D5, 0x05D6, 0x05D7, 0x05D8, 0x05D9, 0x05DA, 0x05DB, 0x05DC, 0x05DD, 0x05DE, 0x05DF, + 0x05E0, 0x05E1, 0x05E2, 0x05E3, 0x05E4, 0x05E5, 0x05E6, 0x05E7, 0x05E8, 0x05E9, 0x05EA, 0x00A2, 0x00A3, 0x00A5, 0x20A7, 0x0192, + 0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, 0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229, + 0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00C2, 0x00E0, 0x00B6, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x2017, 0x00C0, 0x00A7, + 0x00C9, 0x00C8, 0x00CA, 0x00F4, 0x00CB, 0x00CF, 0x00FB, 0x00F9, 0x00A4, 0x00D4, 0x00DC, 0x00A2, 0x00A3, 0x00D9, 0x00DB, 0x0192, + 0x00A6, 0x00B4, 0x00F3, 0x00FA, 0x00A8, 0x00B8, 0x00B3, 0x00AF, 0x00CE, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00BE, 0x00AB, 0x00BB, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, 0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229, + 0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5, + 0x00C9, 0x00E6, 0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9, 0x00FF, 0x00D6, 0x00DC, 0x00F8, 0x00A3, 0x00D8, 0x20A7, 0x0192, + 0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00A4, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, 0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229, + 0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0, + }, + { + 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, + 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, + 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, + 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, + 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F, + 0x0401, 0x0451, 0x0404, 0x0454, 0x0407, 0x0457, 0x040E, 0x045E, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x2116, 0x00A4, 0x25A0, 0x00A0, + }, + { + 0x2500, 0x2502, 0x250C, 0x2510, 0x2514, 0x2518, 0x251C, 0x2524, 0x252C, 0x2534, 0x253C, 0x2580, 0x2584, 0x2588, 0x258C, 0x2590, + 0x2591, 0x2592, 0x2593, 0x2320, 0x25A0, 0x2219, 0x221A, 0x2248, 0x2264, 0x2265, 0x00A0, 0x2321, 0x00B0, 0x00B2, 0x00B7, 0x00F7, + 0x2550, 0x2551, 0x2552, 0x0451, 0x2553, 0x2554, 0x2555, 0x2556, 0x2557, 0x2558, 0x2559, 0x255A, 0x255B, 0x255C, 0x255D, 0x255E, + 0x255F, 0x2560, 0x2561, 0x0401, 0x2562, 0x2563, 0x2564, 0x2565, 0x2566, 0x2567, 0x2568, 0x2569, 0x256A, 0x256B, 0x256C, 0x00A9, + 0x044E, 0x0430, 0x0431, 0x0446, 0x0434, 0x0435, 0x0444, 0x0433, 0x0445, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, + 0x043F, 0x044F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0436, 0x0432, 0x044C, 0x044B, 0x0437, 0x0448, 0x044D, 0x0449, 0x0447, 0x044A, + 0x042E, 0x0410, 0x0411, 0x0426, 0x0414, 0x0415, 0x0424, 0x0413, 0x0425, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, + 0x041F, 0x042F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0416, 0x0412, 0x042C, 0x042B, 0x0417, 0x0428, 0x042D, 0x0429, 0x0427, 0x042A, + }, + { + 0x2500, 0x2502, 0x250C, 0x2510, 0x2514, 0x2518, 0x251C, 0x2524, 0x252C, 0x2534, 0x253C, 0x2580, 0x2584, 0x2588, 0x258C, 0x2590, + 0x2591, 0x2592, 0x2593, 0x2320, 0x25A0, 0x2219, 0x221A, 0x2248, 0x2264, 0x2265, 0x00A0, 0x2321, 0x00B0, 0x00B2, 0x00B7, 0x00F7, + 0x2550, 0x2551, 0x2552, 0x0451, 0x0454, 0x2554, 0x0456, 0x0457, 0x2557, 0x2558, 0x2559, 0x255A, 0x255B, 0x0491, 0x255D, 0x255E, + 0x255F, 0x2560, 0x2561, 0x0401, 0x0404, 0x2563, 0x0406, 0x0407, 0x2566, 0x2567, 0x2568, 0x2569, 0x256A, 0x0490, 0x256C, 0x00A9, + 0x044E, 0x0430, 0x0431, 0x0446, 0x0434, 0x0435, 0x0444, 0x0433, 0x0445, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, + 0x043F, 0x044F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0436, 0x0432, 0x044C, 0x044B, 0x0437, 0x0448, 0x044D, 0x0449, 0x0447, 0x044A, + 0x042E, 0x0410, 0x0411, 0x0426, 0x0414, 0x0415, 0x0424, 0x0413, 0x0425, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, + 0x041F, 0x042F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0416, 0x0412, 0x042C, 0x042B, 0x0417, 0x0428, 0x042D, 0x0429, 0x0427, 0x042A, + }, + { + 0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x008E, 0x008F, + 0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x009E, 0x0178, + 0x00A0, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, + 0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF, + 0x011E, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D7, 0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x0130, 0x015E, 0x00DF, + 0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF, + 0x011F, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x0131, 0x015F, 0x00FF, + }, + { + 0x20AC, 0x067E, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0679, 0x2039, 0x0152, 0x0686, 0x0698, 0x0688, + 0x06AF, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x06A9, 0x2122, 0x0691, 0x203A, 0x0153, 0x200C, 0x200D, 0x06BA, + 0x00A0, 0x060C, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x06BE, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x061B, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x061F, + 0x06C1, 0x0621, 0x0622, 0x0623, 0x0624, 0x0625, 0x0626, 0x0627, 0x0628, 0x0629, 0x062A, 0x062B, 0x062C, 0x062D, 0x062E, 0x062F, + 0x0630, 0x0631, 0x0632, 0x0633, 0x0634, 0x0635, 0x0636, 0x00D7, 0x0637, 0x0638, 0x0639, 0x063A, 0x0640, 0x0641, 0x0642, 0x0643, + 0x00E0, 0x0644, 0x00E2, 0x0645, 0x0646, 0x0647, 0x0648, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x0649, 0x064A, 0x00EE, 0x00EF, + 0x064B, 0x064C, 0x064D, 0x064E, 0x00F4, 0x064F, 0x0650, 0x00F7, 0x0651, 0x00F9, 0x0652, 0x00FB, 0x00FC, 0x200E, 0x200F, 0x06D2, + }, + { + 0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x008A, 0x2039, 0x0152, 0x008D, 0x008E, 0x008F, + 0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x009A, 0x203A, 0x0153, 0x009D, 0x009E, 0x0178, + 0x00A0, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, + 0x00C0, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x00C5, 0x00C6, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x0300, 0x00CD, 0x00CE, 0x00CF, + 0x0110, 0x00D1, 0x0309, 0x00D3, 0x00D4, 0x01A0, 0x00D6, 0x00D7, 0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x01AF, 0x0303, 0x00DF, + 0x00E0, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x00E5, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x0301, 0x00ED, 0x00EE, 0x00EF, + 0x0111, 0x00F1, 0x0323, 0x00F3, 0x00F4, 0x01A1, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x01B0, 0x20AB, 0x00FF, + }, +}; + +} diff --git a/core/libs/prx/libSceCesCs/Unicode.cpp b/core/libs/prx/libSceCesCs/Unicode.cpp new file mode 100644 index 0000000000..5a4b10399b --- /dev/null +++ b/core/libs/prx/libSceCesCs/Unicode.cpp @@ -0,0 +1,204 @@ +#include +#include +#include +#include "prx/libc/include/general/VabiMacros.hpp" + +namespace { + +constexpr int INVALID_SRC_BUFFER = static_cast(0x805C0010); +constexpr int SRC_BUFFER_END = static_cast(0x805C0011); +constexpr int INVALID_ENCODE = static_cast(0x805C0014); +constexpr int UNASSIGNED_CODE = static_cast(0x805C0020); +constexpr int INVALID_DST_BUFFER = static_cast(0x805C0030); +constexpr int DST_BUFFER_END = static_cast(0x805C0031); +constexpr bool NATIVE_BIG_ENDIAN = std::endian::native == std::endian::big; + +bool isScalar(uint32_t code) { + return code <= 0x10FFFF && (code < 0xD800 || code > 0xDFFF); +} + +int decodeUtf8(const uint8_t* source, uint32_t available, uint32_t& code, uint32_t& length) { + if (!source) return INVALID_SRC_BUFFER; + if (available == 0) return SRC_BUFFER_END; + const uint8_t lead = source[0]; + if (lead <= 0x7F) { code = lead; length = 1; return 0; } + if (lead >= 0xC2 && lead <= 0xDF) { code = lead & 0x1F; length = 2; } + else if (lead >= 0xE0 && lead <= 0xEF) { code = lead & 0x0F; length = 3; } + else if (lead >= 0xF0 && lead <= 0xF4) { code = lead & 0x07; length = 4; } + else return INVALID_ENCODE; + for (uint32_t i = 1; i < length; ++i) { + if (i >= available) return SRC_BUFFER_END; + const uint8_t next = source[i]; + if (next < 0x80 || next > 0xBF) return INVALID_ENCODE; + if (i == 1 && ((lead == 0xE0 && next < 0xA0) || (lead == 0xED && next > 0x9F) + || (lead == 0xF0 && next < 0x90) || (lead == 0xF4 && next > 0x8F))) return INVALID_ENCODE; + code = code << 6 | (next & 0x3F); + } + return 0; +} + +int encodeUtf8(uint32_t code, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + if (!isScalar(code)) return INVALID_ENCODE; + if (!target) return INVALID_DST_BUFFER; + uint8_t bytes[4]; + uint32_t length = 0; + if (code < 0x80) { bytes[0] = static_cast(code); length = 1; } + else if (code < 0x800) { + bytes[0] = static_cast(0xC0 | code >> 6); + bytes[1] = static_cast(0x80 | (code & 0x3F)); + length = 2; + } else if (code < 0x10000) { + bytes[0] = static_cast(0xE0 | code >> 12); + bytes[1] = static_cast(0x80 | (code >> 6 & 0x3F)); + bytes[2] = static_cast(0x80 | (code & 0x3F)); + length = 3; + } else { + bytes[0] = static_cast(0xF0 | code >> 18); + bytes[1] = static_cast(0x80 | (code >> 12 & 0x3F)); + bytes[2] = static_cast(0x80 | (code >> 6 & 0x3F)); + bytes[3] = static_cast(0x80 | (code & 0x3F)); + length = 4; + } + if (capacity < length) return DST_BUFFER_END; + std::memcpy(target, bytes, length); + if (targetLength) *targetLength = length; + return 0; +} + +template +uint32_t loadUnit(const Unit* source) { + const auto* bytes = reinterpret_cast(source); + uint32_t code = 0; + for (uint32_t i = 0; i < sizeof(Unit); ++i) code |= uint32_t(bytes[i]) << (8 * (BigEndian ? sizeof(Unit) - 1 - i : i)); + return code; +} + +template +void storeUnit(Unit* target, uint32_t code) { + auto* bytes = reinterpret_cast(target); + for (uint32_t i = 0; i < sizeof(Unit); ++i) bytes[i] = static_cast(code >> (8 * (BigEndian ? sizeof(Unit) - 1 - i : i))); +} + +template +int utf8ToUtf32(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint32_t* target) { + uint32_t code = 0; + uint32_t length = 0; + if (const int result = decodeUtf8(source, available, code, length); result != 0) return result; + if (!target) return INVALID_DST_BUFFER; + storeUnit(target, code); + if (sourceLength) *sourceLength = length; + return 0; +} + +template +int utf8ToUtf16(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint16_t* target, uint32_t capacity, uint32_t* targetLength) { + uint32_t code = 0; + uint32_t length = 0; + if (const int result = decodeUtf8(source, available, code, length); result != 0) return result; + if (!target) return INVALID_DST_BUFFER; + const uint32_t units = code < 0x10000 ? 1 : 2; + if (capacity < units) return DST_BUFFER_END; + if (units == 1) storeUnit(target, code); + else { + code -= 0x10000; + storeUnit(target, 0xD800 | code >> 10); + storeUnit(target + 1, 0xDC00 | (code & 0x3FF)); + } + if (sourceLength) *sourceLength = length; + if (targetLength) *targetLength = units; + return 0; +} + +template +int utf16ToUtf8(const uint16_t* source, uint32_t available, uint32_t* sourceLength, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + if (!source) return INVALID_SRC_BUFFER; + if (available == 0) return SRC_BUFFER_END; + uint32_t code = loadUnit(source); + uint32_t units = 1; + if (code >= 0xDC00 && code <= 0xDFFF) return INVALID_ENCODE; + if (code >= 0xD800 && code <= 0xDBFF) { + if (available < 2) return SRC_BUFFER_END; + const uint32_t low = loadUnit(source + 1); + if (low < 0xDC00 || low > 0xDFFF) return INVALID_ENCODE; + code = 0x10000 + ((code - 0xD800) << 10) + low - 0xDC00; + units = 2; + } + if (const int result = encodeUtf8(code, target, capacity, targetLength); result != 0) return result; + if (sourceLength) *sourceLength = units; + return 0; +} + +template +int utf32ToUtf8(const uint32_t* source, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + if (!source) return INVALID_SRC_BUFFER; + return encodeUtf8(loadUnit(source), target, capacity, targetLength); +} + +} + +extern "C" { + +int APS5_VABI sceCesUcs2ToUtf8(uint16_t source, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return encodeUtf8(source, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf8ToUcs2(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint16_t* target) { + uint32_t code = 0; + uint32_t length = 0; + if (const int result = decodeUtf8(source, available, code, length); result != 0) return result; + if (!target) return INVALID_DST_BUFFER; + if (code > 0xFFFF) return UNASSIGNED_CODE; + *target = static_cast(code); + if (sourceLength) *sourceLength = length; + return 0; +} + +int APS5_VABI sceCesUtf8ToUtf32(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint32_t* target) { + return utf8ToUtf32(source, available, sourceLength, target); +} + +int APS5_VABI sceCesUtf8ToUtf32be(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint32_t* target) { + return utf8ToUtf32(source, available, sourceLength, target); +} + +int APS5_VABI sceCesUtf8ToUtf32le(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint32_t* target) { + return utf8ToUtf32(source, available, sourceLength, target); +} + +int APS5_VABI sceCesUtf8ToUtf16(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint16_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf8ToUtf16(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf8ToUtf16be(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint16_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf8ToUtf16(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf8ToUtf16le(const uint8_t* source, uint32_t available, uint32_t* sourceLength, uint16_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf8ToUtf16(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf16ToUtf8(const uint16_t* source, uint32_t available, uint32_t* sourceLength, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf16ToUtf8(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf16beToUtf8(const uint16_t* source, uint32_t available, uint32_t* sourceLength, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf16ToUtf8(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf16leToUtf8(const uint16_t* source, uint32_t available, uint32_t* sourceLength, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf16ToUtf8(source, available, sourceLength, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf32ToUtf8(uint32_t source, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return encodeUtf8(source, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf32beToUtf8(const uint32_t* source, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf32ToUtf8(source, target, capacity, targetLength); +} + +int APS5_VABI sceCesUtf32leToUtf8(const uint32_t* source, uint8_t* target, uint32_t capacity, uint32_t* targetLength) { + return utf32ToUtf8(source, target, capacity, targetLength); +} + +} diff --git a/core/libs/prx/libSceCommonDialog/CMakeLists.txt b/core/libs/prx/libSceCommonDialog/CMakeLists.txt new file mode 100644 index 0000000000..d770a3c871 --- /dev/null +++ b/core/libs/prx/libSceCommonDialog/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceCommonDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceCommonDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceCommonDialog) + +if(BUILD_TESTING) + add_test_executable(guest_common_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestCommonDialog.cpp) + target_include_directories(guest_common_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_common_dialog_tests PRIVATE libSceCommonDialog libc) + configure_windows_unwind(guest_common_dialog_tests) + add_test(NAME guest_common_dialog COMMAND guest_common_dialog_tests) + if(WIN32) + set_tests_properties(guest_common_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp index 3f6451d34f..260eb371bc 100644 --- a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp +++ b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp @@ -1,7 +1,254 @@ #ifndef CORE_LIBS_PRX_LIBSCECOMMONDIALOG_COMMONDIALOG_HPP #define CORE_LIBS_PRX_LIBSCECOMMONDIALOG_COMMONDIALOG_HPP +#include +#include +#include +#include + +struct CommonDialogBaseParam { + std::uint64_t size; + std::uint8_t reserved[36]; + std::uint32_t magic; +}; + +struct NpAuthAuthorizedAppDialogParam { + CommonDialogBaseParam base; + std::uint64_t size; + std::int32_t userId; + std::uint32_t padding60; + const char* application; + const char* text; + std::uint32_t mode; + std::uint32_t padding84; + std::uint8_t reserved[32]; +}; + +static_assert(sizeof(NpAuthAuthorizedAppDialogParam) == 120); +static_assert(offsetof(NpAuthAuthorizedAppDialogParam, application) == 64); +static_assert(offsetof(NpAuthAuthorizedAppDialogParam, text) == 72); +static_assert(offsetof(NpAuthAuthorizedAppDialogParam, mode) == 80); +static_assert(offsetof(NpAuthAuthorizedAppDialogParam, reserved) == 88); + +struct VrSetupDialogParam { + std::uint64_t size; + std::uint8_t reserved8; + std::uint8_t reserved9; + std::uint8_t data[46]; +}; + +static_assert(sizeof(VrSetupDialogParam) == 56); + +struct UsbStorageDialogParam { + CommonDialogBaseParam base; + std::uint64_t size; + char path[1024]; + std::int32_t userId; + std::uint32_t padding; +}; + +struct UsbStorageDialogResult { + std::uint64_t size; + std::int32_t result; + std::int32_t status; + std::uint8_t reserved[64]; +}; + +static_assert(offsetof(UsbStorageDialogParam, path) == 56); +static_assert(offsetof(UsbStorageDialogParam, userId) == 1080); +static_assert(sizeof(UsbStorageDialogResult) == 80); +static_assert(offsetof(UsbStorageDialogResult, result) == 8); +static_assert(offsetof(UsbStorageDialogResult, status) == 12); + +struct LoginDialogParam { + std::uint32_t size; + std::uint32_t mode; + std::int32_t userIds[9]; + std::uint32_t reserved[5]; +}; + +struct LoginDialogResult { + std::int32_t result; + std::int32_t errorCode; + std::uint8_t data[8]; +}; + +static_assert(sizeof(LoginDialogParam) == 64); +static_assert(offsetof(LoginDialogParam, userIds) == 8); +static_assert(offsetof(LoginDialogParam, reserved) == 44); +static_assert(sizeof(LoginDialogResult) == 16); +static_assert(offsetof(LoginDialogResult, errorCode) == 4); + +struct NetCtlApDialogParam { + CommonDialogBaseParam base; + std::uint64_t size; + std::uint32_t value56; + std::uint32_t reserved60; + char option[9]; + std::uint8_t reserved[31]; +}; + +struct NetCtlApDialogResult { + std::int32_t result; + std::uint8_t reserved[36]; +}; + +static_assert(sizeof(NetCtlApDialogParam) == 104); +static_assert(offsetof(NetCtlApDialogParam, option) == 64); +static_assert(offsetof(NetCtlApDialogParam, reserved) == 73); +static_assert(sizeof(NetCtlApDialogResult) == 40); + +struct G2PDialogParam { + CommonDialogBaseParam base; + std::int32_t userId; + std::uint32_t padding; + std::uint64_t size; + std::uint8_t reserved[64]; +}; + +struct PlayerReviewDialogParam { + CommonDialogBaseParam base; + std::uint64_t size; + std::int32_t userId; + std::uint8_t target[40]; + std::uint32_t mode; + std::uint8_t reserved[64]; +}; + +struct PlayerSelectionDialogPlayer { + std::uint64_t accountId; + std::uint32_t option0; + std::uint32_t option1; + std::uint32_t mode; + std::uint8_t reserved[64]; + std::uint32_t padding; +}; + +struct PlayerSelectionDialogParam { + CommonDialogBaseParam base; + std::uint64_t size; + std::int32_t userId; + char title[64]; + std::uint32_t maxPlayers; + std::uint32_t mode; + std::uint32_t padding0; + const PlayerSelectionDialogPlayer* players; + std::uint32_t playerCount; + std::uint8_t reserved[64]; + std::uint32_t padding1; +}; + +struct PlayerSelectionDialogResult { + std::int32_t result; + std::uint32_t option; + std::uint64_t* accountIds; + std::uint32_t count; + std::uint32_t padding; +}; + +static_assert(sizeof(PlayerSelectionDialogPlayer) == 88); +static_assert(offsetof(PlayerSelectionDialogPlayer, reserved) == 20); +static_assert(sizeof(PlayerSelectionDialogParam) == 216); +static_assert(offsetof(PlayerSelectionDialogParam, maxPlayers) == 124); +static_assert(offsetof(PlayerSelectionDialogParam, players) == 136); +static_assert(offsetof(PlayerSelectionDialogParam, reserved) == 148); +static_assert(sizeof(PlayerSelectionDialogResult) == 24); +static_assert(offsetof(PlayerSelectionDialogResult, accountIds) == 8); + +struct NpCommerceDialogResult { + std::int32_t result; + std::uint8_t authorized; + std::uint8_t padding[3]; + std::uint64_t userData; + std::uint8_t reserved[32]; +}; + +static_assert(sizeof(NpCommerceDialogResult) == 48); +static_assert(offsetof(NpCommerceDialogResult, userData) == 8); + +struct NpCommerceDialogParam { + CommonDialogBaseParam base; + std::uint32_t size; + std::int32_t userId; + std::uint32_t mode; + std::uint32_t option; + const char* const* products; + std::uint32_t productCount; + std::uint32_t padding; + std::uint64_t argument; + std::uint64_t userData; + std::uint8_t reserved[32]; +}; + +static_assert(sizeof(NpCommerceDialogParam) == 128); +static_assert(offsetof(NpCommerceDialogParam, products) == 64); +static_assert(offsetof(NpCommerceDialogParam, userData) == 88); + +struct NpCommerceDialogParam2 { + CommonDialogBaseParam base; + std::uint32_t size; + std::int32_t userId; + std::uint32_t mode; + std::uint32_t option; + std::uint32_t contentMode; + std::uint32_t padding0; + const char* const* products; + std::uint32_t productCount; + std::uint32_t padding1; + std::uint64_t argument; + std::uint64_t userData; + std::uint8_t reserved[32]; +}; + +static_assert(sizeof(NpCommerceDialogParam2) == 136); +static_assert(offsetof(NpCommerceDialogParam2, products) == 72); +static_assert(offsetof(NpCommerceDialogParam2, userData) == 96); + +struct WebBrowserDialogExtendedResult { + std::uint64_t size; + std::uint32_t type; + char content[2048]; + std::uint8_t reserved0[4]; + char* extendedContent; + std::uint32_t extendedCapacity; + std::uint8_t reserved1[20]; +}; + +struct WebBrowserDialogResult { + std::int32_t result; + std::uint32_t padding; + WebBrowserDialogExtendedResult* extended; + std::uint8_t reserved[240]; +}; + +static_assert(sizeof(WebBrowserDialogExtendedResult) == 2096); +static_assert(offsetof(WebBrowserDialogExtendedResult, extendedContent) == 2064); +static_assert(sizeof(WebBrowserDialogResult) == 256); + +struct WebBrowserDialogAllowList { + std::uint64_t size; + const char* addresses[20]; + std::uint8_t reserved[256]; +}; + +static_assert(sizeof(WebBrowserDialogAllowList) == 424); + +struct CommonDialogResultPrefix { + std::uint8_t untouched[8]; + std::int32_t result; +}; + +static_assert(sizeof(CommonDialogBaseParam) == 48); +static_assert(sizeof(G2PDialogParam) == 128); +static_assert(sizeof(PlayerReviewDialogParam) == 168); +static_assert(offsetof(CommonDialogResultPrefix, result) == 8); + +inline bool ValidateCommonDialogBaseParam(const CommonDialogBaseParam& param) { + return param.size == sizeof(param) && param.magic == static_cast(reinterpret_cast(¶m)) - 1060003575u && std::all_of(std::begin(param.reserved), std::end(param.reserved), [](std::uint8_t value) { return value == 0; }); +} + constexpr int COMMON_DIALOG_OK = 0; -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80002); +constexpr int COMMON_DIALOG_ERROR_ALREADY_SYSTEM_INITIALIZED = static_cast(0x80B80002); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004); #endif diff --git a/core/libs/prx/libSceCommonDialog/Export.cpp b/core/libs/prx/libSceCommonDialog/Export.cpp index 8fb415d4eb..47f9a84bb1 100644 --- a/core/libs/prx/libSceCommonDialog/Export.cpp +++ b/core/libs/prx/libSceCommonDialog/Export.cpp @@ -1,19 +1,20 @@ +#include #include #include #include "SceTypes.hpp" #include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" -static bool g_initialized = false; +static std::atomic g_initialized{false}; extern "C" { int APS5_VABI sceCommonDialogInitialize(void) { - if (g_initialized) { - return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; - } - g_initialized = true; - return COMMON_DIALOG_OK; + bool expected = false; + if (!g_initialized.compare_exchange_strong(expected, true)) { + return COMMON_DIALOG_ERROR_ALREADY_SYSTEM_INITIALIZED; + } + return COMMON_DIALOG_OK; } bool APS5_VABI sceCommonDialogIsUsed(void) { diff --git a/core/libs/prx/libSceCompanionHttpd/Export.cpp b/core/libs/prx/libSceCompanionHttpd/Export.cpp new file mode 100644 index 0000000000..48240a3fe1 --- /dev/null +++ b/core/libs/prx/libSceCompanionHttpd/Export.cpp @@ -0,0 +1,93 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceCompanionHttpdAddHeader() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdGet2ndScreenStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdGetEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdGetUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdInitialize2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdOptParamInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdRegisterRequestBodyReceptionCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdRegisterRequestCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdRegisterRequestCallback2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdSetBody() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdSetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdStop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdUnregisterRequestBodyReceptionCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompanionHttpdUnregisterRequestCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceCompanionUtil/Export.cpp b/core/libs/prx/libSceCompanionUtil/Export.cpp new file mode 100644 index 0000000000..7a208122da --- /dev/null +++ b/core/libs/prx/libSceCompanionUtil/Export.cpp @@ -0,0 +1,28 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceCompanionUtilGetEvent() { + return static_cast(0x80AD0008); +} + +int APS5_VABI sceCompanionUtilGetRemoteOskEvent() { + return 0; +} + +int APS5_VABI sceCompanionUtilInitialize() { + return 0; +} + +int APS5_VABI sceCompanionUtilOptParamInitialize() { + return 0; +} + +int APS5_VABI sceCompanionUtilTerminate() { + return 0; +} + +} diff --git a/core/libs/prx/libSceComposite/Export.cpp b/core/libs/prx/libSceComposite/Export.cpp new file mode 100644 index 0000000000..ebf58fb725 --- /dev/null +++ b/core/libs/prx/libSceComposite/Export.cpp @@ -0,0 +1,398 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceCompositorAllocateIndex() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorCheckCrash() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorCommandGpuPerfBegin() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorCommandGpuPerfEnd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorCreateIndirectRenderTarget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorDeleteIndirectRenderTarget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorFlush() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorFlushWithRepeat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorGetAnotherProcessSystemAddress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorGetAnotherProcessSystemSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorGetAnotherProcessVideoAddress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorGetAnotherProcessVideoSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorGetCanvasHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorGetClientList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorGetDmemOffset() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorGetDummyAddress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorGetDummySize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorGetRemainedCommandNum() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorGetRenderTargetResolution() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorGetSystemAddress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorGetSystemSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorGetVideoAddress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorGetVideoSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorHandleProcessEvents() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorInitWithProcessOrder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorInsertThreadTraceMarker() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorIsDebugCaptureEnabled() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorIsReady4K() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorLockCommandBuffer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorMakeCanvasHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorMapAnotherProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorMemoryPoolDecommit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorPrivacyMaskPushArea() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorQuit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorReleaseCommandBuffer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorReleaseIndex() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorReleaseLocalStall() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorReserveCommandBuffer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorResetZoomCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetAgcCompositeCanvasCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetAgcContextCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetCompositeCanvasCommandInC() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetCursorImageAddress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetDebugPositionCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetEndOfFrameCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetEventCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetFilterType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetFlipCommand2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetGameSufaceControlCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetGnmContextCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetIndirectCanvasCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetIndirectCompositionCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetIndirectRenderTargetConfigCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetInvisibleCanvasCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetKeepDisplayBufferCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetLocalStallCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetMemoryCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetPatchCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetPostEventCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetRepeatCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetResolutionCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetScalingLayoutInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetSeparateContextCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetSystemConfigWithMask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetVideoOutMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetVirtualCanvasAgcContextCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetVirtualCanvasCommandFooter() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetVirtualCanvasPatchCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetVrMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSetZoomCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorSystemConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorWaitEndOfRendering() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorWaitEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompositorWaitPostEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompsoitorGetGpuClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompsoitorGetProcessRenderingTime() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCompsoitorGetRenderingTime() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceCompositeExt/Export.cpp b/core/libs/prx/libSceCompositeExt/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceCompositeExt/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceConnectAccountDialog/Export.cpp b/core/libs/prx/libSceConnectAccountDialog/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceConnectAccountDialog/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceContentDelete/Export.cpp b/core/libs/prx/libSceContentDelete/Export.cpp index 4c80330e30..5f886b22a4 100644 --- a/core/libs/prx/libSceContentDelete/Export.cpp +++ b/core/libs/prx/libSceContentDelete/Export.cpp @@ -8,30 +8,56 @@ namespace { std::atomic g_initialized{false}; + +constexpr int InvalidArgument = static_cast(0x809D5001u); +constexpr int NotInitialized = static_cast(0x809D5002u); +constexpr int AlreadyInitialized = static_cast(0x809D5003u); } extern "C" { -int APS5_VABI sceContentDeleteInitialize(const ContentDeleteInitParam* init_param) { - if (init_param == nullptr) APS5_INVALID_ARG_EX; +int APS5_VABI sceContentDeleteInitialize(const ContentDeleteInitParam* initParam) { + if (initParam == nullptr || initParam->heap_size != 0x4000) return InvalidArgument; bool expected = false; - if (!g_initialized.compare_exchange_strong(expected, true)) throw std::logic_error(std::string(__func__) + ": already initialized"); + if (!g_initialized.compare_exchange_strong(expected, true)) return AlreadyInitialized; return 0; } -int APS5_VABI sceContentDeleteByPath(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentDeleteByPath(const char* contentPath) { + if (!g_initialized.load()) return NotInitialized; + if (contentPath == nullptr) APS5_INVALID_ARG_EX; + if (*contentPath == 0) return InvalidArgument; + 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) { +int APS5_VABI sceContentDeleteById(std::int64_t contentId) { + if (!g_initialized.load()) return NotInitialized; + 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 sceContentDeleteByIdListForSf(const std::int64_t* contentIds, std::size_t count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentDeleteGlobalMstat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentDeleteMemoryInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentDeleteMemoryTerminate(void) { NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceContentDeleteTerminate(void) { bool expected = true; - if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); + if (!g_initialized.compare_exchange_strong(expected, false)) return NotInitialized; return 0; } 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/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index f618daeea1..0c5b205edf 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -3,18 +3,20 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include -#include #include #include #include #include namespace { -constexpr std::size_t MaxExecution = 10; +constexpr std::int32_t MaxExecution = 10; +constexpr int ErrorInvalidState = static_cast(0x809D3002u); +constexpr int ErrorBusy = static_cast(0x809D3003u); +constexpr int ErrorNotInitialized = static_cast(0x809D3004u); +constexpr int ErrorExecutionMax = static_cast(0x809D3018u); std::atomic g_initialized{false}; std::mutex g_sessionMutex; std::set g_sessions; -std::int32_t g_lastSessionId = 0; void RequireSession(const char* function, std::int32_t expId) { if (!g_initialized.load()) throw std::logic_error(std::string(function) + ": not initialized"); @@ -40,44 +42,103 @@ int APS5_VABI sceContentExportFinish(std::int32_t expId) { return 0; } -int APS5_VABI sceContentExportFromData(void) { +int APS5_VABI sceContentExportFromData(std::int32_t expId, const ContentExportParam* param, std::size_t contentLength, ContentExportDataCallback callback, void* userData, char* path, std::size_t pathBufferLength) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceContentExportFromFile(void) { +int APS5_VABI sceContentExportFromFile(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, char* path, std::size_t pathBufferLength) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceContentExportFromFileWithThumbnail(void) { +int APS5_VABI sceContentExportFromFileWithThumbnail(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, const char* thumbnailPath, char* path, std::size_t pathBufferLength) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportAddToFavorites(std::int64_t contentId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportDataProvideFunction(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFree(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFrom(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFromFileForSf(std::int32_t expId, const ContentExportParam* param, const ContentExportSfParam* sfParam, const char* sourcePath, const char* thumbnailPath, char* path, std::size_t pathBufferLength) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFromFileWithContentIdList(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, const char* thumbnailPath, const std::int64_t* contentIds, std::uint32_t contentCount, char* path, std::size_t pathBufferLength) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFromFileWithTitleIdList(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, const char* thumbnailPath, const char* titleIdList, char* path, std::size_t pathBufferLength) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportInit(const ContentExportInitParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportMalloc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportRemoveFromFavorites(std::int64_t contentId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportRenameContent(std::int64_t contentId, const char* name) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportValidateContents(const std::int64_t* contentIds, std::uint32_t count) { NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceContentExportStart(void) { std::lock_guard lock(g_sessionMutex); - if (!g_initialized.load()) throw std::logic_error(std::string(__func__) + ": not initialized"); - if (g_sessions.size() >= MaxExecution) throw std::runtime_error(std::string(__func__) + ": behaviour beyond 10 live export sessions is unknown"); - if (g_lastSessionId == std::numeric_limits::max()) throw std::runtime_error(std::string(__func__) + ": export session ids exhausted"); - g_sessions.insert(++g_lastSessionId); - return g_lastSessionId; + if (!g_initialized.load()) return ErrorNotInitialized; + for (std::int32_t expId = 0; expId < MaxExecution; ++expId) { + if (g_sessions.insert(expId).second) return expId; + } + return ErrorExecutionMax; } int APS5_VABI sceContentExportGetProgress(std::int32_t expId) { std::lock_guard lock(g_sessionMutex); RequireSession(__func__, expId); - throw std::runtime_error(std::string(__func__) + ": progress of a session with no running export is unknown"); + throw std::runtime_error(std::string(__func__) + ": progress requires the calling title SDK compatibility mode"); } int APS5_VABI sceContentExportCancel(std::int32_t expId) { std::lock_guard lock(g_sessionMutex); RequireSession(__func__, expId); - throw std::runtime_error(std::string(__func__) + ": cancelling a session with no running export is unknown"); + return ErrorInvalidState; } -int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, - const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength) { +int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength) { std::lock_guard lock(g_sessionMutex); RequireSession(__func__, expId); throw std::runtime_error(std::string(__func__) + ": the host has no media library to export into"); @@ -85,9 +146,9 @@ int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const Co int APS5_VABI sceContentExportTerm(void) { std::lock_guard lock(g_sessionMutex); - if (!g_sessions.empty()) throw std::runtime_error(std::string(__func__) + ": terminating with live export sessions is unknown"); + if (!g_sessions.empty()) return ErrorBusy; bool expected = true; - if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); + if (!g_initialized.compare_exchange_strong(expected, false)) return ErrorNotInitialized; return 0; } diff --git a/core/libs/prx/libSceContentListController/Export.cpp b/core/libs/prx/libSceContentListController/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceContentListController/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceContentSearch/Export.cpp b/core/libs/prx/libSceContentSearch/Export.cpp index 9475f46b16..ac200672de 100644 --- a/core/libs/prx/libSceContentSearch/Export.cpp +++ b/core/libs/prx/libSceContentSearch/Export.cpp @@ -10,6 +10,8 @@ namespace { std::atomic g_initialized{false}; constexpr int ErrorNotInitialized = static_cast(0x809D1001u); +constexpr int ErrorAlreadyInitialized = static_cast(0x809D1002u); +constexpr int ErrorInvalidArgument = static_cast(0x809D1003u); constexpr int ErrorLimitTooBig = static_cast(0x809D100Au); constexpr std::uint32_t MaxSearchLimit = 92; constexpr std::int64_t LibraryUpdateId = 0; @@ -63,42 +65,44 @@ void ValidateOrderBy(const char* funcName, const ContentSearchContentOrderByCond extern "C" { int APS5_VABI sceContentSearchInit(const ContentSearchInitParam* init_param) { - if (init_param == nullptr) APS5_INVALID_ARG_EX; + if (g_initialized.load()) return ErrorAlreadyInitialized; + if (init_param == nullptr || init_param->memory_size == 0 || init_param->memory_size % 16384 != 0) return ErrorInvalidArgument; bool expected = false; - if (!g_initialized.compare_exchange_strong(expected, true)) throw std::logic_error(std::string(__func__) + ": already initialized"); + if (!g_initialized.compare_exchange_strong(expected, true)) return ErrorAlreadyInitialized; return 0; } int APS5_VABI sceContentSearchCloseMetadata(std::int32_t metadataId) { if (!g_initialized.load()) return ErrorNotInitialized; - ThrowUnknownMetadataId(__func__, metadataId); + return ErrorInvalidArgument; } int APS5_VABI sceContentSearchGetMetadataFieldInfo(std::int32_t metadataId, const char* field, std::int32_t* metadataType, std::int32_t* size) { if (!g_initialized.load()) return ErrorNotInitialized; - if (field == nullptr || metadataType == nullptr || size == nullptr) APS5_INVALID_ARG_EX; + if (field == nullptr || metadataType == nullptr || size == nullptr) return ErrorInvalidArgument; ThrowUnknownMetadataId(__func__, metadataId); } int APS5_VABI sceContentSearchGetMetadataValue(std::int32_t metadataId, const char* field, void* value) { if (!g_initialized.load()) return ErrorNotInitialized; - if (field == nullptr || value == nullptr) APS5_INVALID_ARG_EX; + if (field == nullptr || value == nullptr) return ErrorInvalidArgument; ThrowUnknownMetadataId(__func__, metadataId); } int APS5_VABI sceContentSearchOpenMetadata(const char* filePath, std::int32_t* metadataId) { if (!g_initialized.load()) return ErrorNotInitialized; - if (filePath == nullptr || metadataId == nullptr) APS5_INVALID_ARG_EX; + if (filePath == nullptr || metadataId == nullptr) return ErrorInvalidArgument; throw std::logic_error(std::string(__func__) + ": " + filePath + " is not in the host content library, which is empty, and the console's error for it is unknown"); } int APS5_VABI sceContentSearchSearchContent(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, const ContentSearchContentOrderByCondition* orderByConditions, std::uint32_t orderByConditionsLength, std::uint32_t offset, std::uint32_t limit, std::int64_t* numOfContent, void* infos, std::int64_t* lastUpdateId) { (void)offset; if (!g_initialized.load()) return ErrorNotInitialized; - if (numOfContent == nullptr || lastUpdateId == nullptr || (infos == nullptr && limit != 0)) APS5_INVALID_ARG_EX; + if ((columnSet == nullptr) != (columnSetLength == 0) || (orderByConditions == nullptr) != (orderByConditionsLength == 0) || limit == 0) return ErrorInvalidArgument; + if (limit > MaxSearchLimit) return ErrorLimitTooBig; + if (numOfContent == nullptr || lastUpdateId == nullptr || infos == nullptr) return ErrorInvalidArgument; ValidateColumnSets(__func__, columnSet, columnSetLength); ValidateOrderBy(__func__, orderByConditions, orderByConditionsLength); - if (limit > MaxSearchLimit) return ErrorLimitTooBig; *numOfContent = 0; *lastUpdateId = LibraryUpdateId; return 0; @@ -106,20 +110,20 @@ int APS5_VABI sceContentSearchSearchContent(const ContentSearchContentColumnSet* int APS5_VABI sceContentSearchGetContentLastUpdateId(std::int64_t* lastUpdateId) { if (!g_initialized.load()) return ErrorNotInitialized; - if (lastUpdateId == nullptr) APS5_INVALID_ARG_EX; + if (lastUpdateId == nullptr) return ErrorInvalidArgument; *lastUpdateId = LibraryUpdateId; return 0; } int APS5_VABI sceContentSearchOpenMetadataByContentId(std::int64_t contentId, std::int32_t* metadataId) { if (!g_initialized.load()) return ErrorNotInitialized; - if (metadataId == nullptr) APS5_INVALID_ARG_EX; + if (metadataId == nullptr) return ErrorInvalidArgument; throw std::logic_error(std::string(__func__) + ": content id " + std::to_string(contentId) + " is not in the host content library, which is empty, and the console's error for it is unknown"); } int APS5_VABI sceContentSearchGetTotalContentSize(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, std::int64_t* size, std::int64_t* lastUpdateId) { if (!g_initialized.load()) return ErrorNotInitialized; - if (size == nullptr || lastUpdateId == nullptr) APS5_INVALID_ARG_EX; + if ((columnSet == nullptr) != (columnSetLength == 0) || size == nullptr || lastUpdateId == nullptr) return ErrorInvalidArgument; ValidateColumnSets(__func__, columnSet, columnSetLength); *size = 0; *lastUpdateId = LibraryUpdateId; @@ -128,7 +132,7 @@ int APS5_VABI sceContentSearchGetTotalContentSize(const ContentSearchContentColu int APS5_VABI sceContentSearchGetNumOfContent(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, std::int64_t* numOfContent, std::int64_t* lastUpdateId) { if (!g_initialized.load()) return ErrorNotInitialized; - if (numOfContent == nullptr || lastUpdateId == nullptr) APS5_INVALID_ARG_EX; + if ((columnSet == nullptr) != (columnSetLength == 0) || numOfContent == nullptr || lastUpdateId == nullptr) return ErrorInvalidArgument; ValidateColumnSets(__func__, columnSet, columnSetLength); *numOfContent = 0; *lastUpdateId = LibraryUpdateId; @@ -137,7 +141,48 @@ int APS5_VABI sceContentSearchGetNumOfContent(const ContentSearchContentColumnSe int APS5_VABI sceContentSearchTerm(void) { bool expected = true; - if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); + if (!g_initialized.compare_exchange_strong(expected, false)) return ErrorNotInitialized; + return 0; +} + +int APS5_VABI sceContentBinderEnd() { + return static_cast(0x809D000F); +} +int APS5_VABI sceContentBinderGetNumOfDifference() { + return static_cast(0x809D000F); +} +int APS5_VABI sceContentBinderInitialize() { + return static_cast(0x809D000F); +} +int APS5_VABI sceContentBinderIsUpdated() { + return static_cast(0x809D000F); +} +int APS5_VABI sceContentBinderReset() { + return static_cast(0x809D000F); +} +int APS5_VABI sceContentBinderSearchDifference() { + return static_cast(0x809D000F); +} +int APS5_VABI sceContentBinderStart() { + return static_cast(0x809D000F); +} +int APS5_VABI sceContentBinderTerminate() { + return static_cast(0x809D000F); +} +int APS5_VABI sceContentSearchGetApplicationLastUpdateId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentSearchGetMyApplicationIndex() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentSearchGetNumOfApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceContentSearchSearchApplication() { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index 29c849a7e1..a38cb0293c 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -1,24 +1,2397 @@ +#include #include #include -#include #include "SceTypes.hpp" -#include "HitLog.hpp" #include "prx/libc/include/General.hpp" namespace { constexpr int kErrInvalidAddress = static_cast(0x80BC0031u); constexpr int kErrInvalidUserId = static_cast(0x80BC0010u); +constexpr std::array BaseVirtualKeys = { + 255, 255, 255, 255, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, + 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 48, 13, 27, 8, 9, 32, 187, 219, 221, + 186, 191, 191, 192, 222, 220, 188, 190, 189, 20, 112, 113, 114, 115, 116, 117, + 118, 119, 120, 121, 122, 123, 44, 145, 19, 45, 36, 33, 46, 35, 34, 39, + 37, 40, 38, 144, 111, 106, 109, 107, 13, 97, 98, 99, 100, 101, 102, 103, + 104, 105, 96, 110, 226, 93, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 12, 255, 255, 255, 255, 255, 255, 255, + 162, 160, 164, 91, 163, 161, 165, 92, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +}; -char16_t UsCharacter(std::uint16_t keycode) { - static constexpr char16_t kPunctuation[] = u"-=[]\\\0;'`,./"; - static constexpr char16_t kKeypad[] = u"/*-+\0" u"1234567890."; - if (keycode >= 0x04 && keycode <= 0x1D) return static_cast(u'a' + (keycode - 0x04)); - if (keycode >= 0x1E && keycode <= 0x26) return static_cast(u'1' + (keycode - 0x1E)); - if (keycode == 0x27) return u'0'; - if (keycode == 0x2C) return u' '; - if (keycode >= 0x2D && keycode <= 0x38) return kPunctuation[keycode - 0x2D]; - if (keycode >= 0x54 && keycode <= 0x63) return kKeypad[keycode - 0x54]; - return 0; +constexpr auto VirtualKeys = [] { + std::array, 39> layouts{}; + for (auto& layout : layouts) layout = BaseVirtualKeys; + layouts[1][28] = 90; + layouts[1][29] = 89; + layouts[1][45] = 189; + layouts[1][46] = 221; + layouts[1][47] = 186; + layouts[1][48] = 187; + layouts[2][28] = 90; + layouts[2][29] = 89; + layouts[2][45] = 219; + layouts[2][46] = 221; + layouts[2][47] = 186; + layouts[2][48] = 192; + layouts[2][49] = 223; + layouts[2][50] = 223; + layouts[2][51] = 222; + layouts[2][52] = 220; + layouts[2][53] = 191; + layouts[3][45] = 189; + layouts[3][46] = 187; + layouts[3][47] = 219; + layouts[3][48] = 221; + layouts[3][49] = 220; + layouts[3][50] = 220; + layouts[3][51] = 186; + layouts[3][53] = 192; + layouts[3][56] = 191; + layouts[3][100] = 255; + layouts[4][45] = 189; + layouts[4][46] = 187; + layouts[4][47] = 219; + layouts[4][48] = 221; + layouts[4][49] = 222; + layouts[4][50] = 222; + layouts[4][51] = 186; + layouts[4][52] = 192; + layouts[4][53] = 223; + layouts[4][56] = 191; + layouts[5][45] = 219; + layouts[5][46] = 221; + layouts[5][47] = 186; + layouts[5][48] = 187; + layouts[6][45] = 219; + layouts[6][46] = 221; + layouts[6][47] = 186; + layouts[6][48] = 187; + layouts[6][100] = 255; + layouts[8][4] = 81; + layouts[8][16] = 188; + layouts[8][20] = 65; + layouts[8][26] = 90; + layouts[8][29] = 87; + layouts[8][45] = 219; + layouts[8][46] = 187; + layouts[8][49] = 220; + layouts[8][50] = 220; + layouts[8][51] = 77; + layouts[8][52] = 192; + layouts[8][53] = 222; + layouts[8][54] = 190; + layouts[8][55] = 191; + layouts[8][56] = 223; + layouts[9][4] = 81; + layouts[9][16] = 188; + layouts[9][20] = 65; + layouts[9][26] = 90; + layouts[9][29] = 87; + layouts[9][45] = 219; + layouts[9][46] = 189; + layouts[9][49] = 220; + layouts[9][50] = 220; + layouts[9][51] = 77; + layouts[9][52] = 192; + layouts[9][53] = 222; + layouts[9][54] = 190; + layouts[9][55] = 191; + layouts[9][56] = 187; + layouts[10][45] = 189; + layouts[10][46] = 187; + layouts[10][47] = 219; + layouts[10][48] = 221; + layouts[10][49] = 220; + layouts[10][50] = 220; + layouts[10][51] = 186; + layouts[10][52] = 192; + layouts[10][53] = 222; + layouts[10][56] = 191; + layouts[11] = layouts[2]; + layouts[12] = layouts[5]; + layouts[13][45] = 219; + layouts[13][46] = 191; + layouts[13][49] = 220; + layouts[13][50] = 220; + layouts[13][51] = 187; + layouts[13][52] = 192; + layouts[13][53] = 222; + layouts[15][46] = 191; + layouts[15][47] = 219; + layouts[15][48] = 221; + layouts[15][49] = 220; + layouts[15][50] = 220; + layouts[15][51] = 186; + layouts[15][53] = 192; + layouts[16][45] = 189; + layouts[16][46] = 187; + layouts[16][47] = 219; + layouts[16][48] = 221; + layouts[16][49] = 220; + layouts[16][50] = 220; + layouts[16][51] = 186; + layouts[16][53] = 192; + layouts[16][56] = 191; + layouts[17] = layouts[5]; + layouts[18] = layouts[3]; + layouts[20][45] = 223; + layouts[20][46] = 189; + layouts[20][47] = 219; + layouts[20][48] = 221; + layouts[20][49] = 188; + layouts[20][50] = 188; + layouts[20][51] = 186; + layouts[20][52] = 190; + layouts[20][53] = 192; + layouts[20][54] = 191; + layouts[20][55] = 220; + layouts[20][56] = 190; + layouts[21][45] = 189; + layouts[21][46] = 222; + layouts[21][47] = 192; + layouts[21][48] = 219; + layouts[21][49] = 221; + layouts[21][50] = 221; + layouts[21][51] = 187; + layouts[21][52] = 186; + layouts[21][53] = 243; + layouts[21][56] = 191; + layouts[21][100] = 255; + layouts[21][135] = 226; + layouts[21][136] = 242; + layouts[21][137] = 220; + layouts[21][138] = 28; + layouts[21][139] = 247; + layouts[22] = layouts[21]; + layouts[23][45] = 189; + layouts[23][46] = 187; + layouts[23][47] = 219; + layouts[23][48] = 221; + layouts[23][49] = 220; + layouts[23][50] = 220; + layouts[23][51] = 186; + layouts[23][53] = 192; + layouts[23][56] = 191; + layouts[23][136] = 21; + layouts[23][138] = 25; + layouts[24] = layouts[3]; + layouts[25] = layouts[3]; + layouts[26] = layouts[3]; + layouts[27] = layouts[3]; + layouts[28] = layouts[3]; + layouts[29] = layouts[16]; + layouts[30] = layouts[16]; + layouts[31][28] = 90; + layouts[31][29] = 89; + layouts[31][46] = 191; + layouts[31][47] = 219; + layouts[31][48] = 221; + layouts[31][49] = 220; + layouts[31][50] = 220; + layouts[31][51] = 186; + layouts[31][53] = 192; + layouts[32] = layouts[16]; + layouts[33] = layouts[16]; + layouts[34] = layouts[16]; + layouts[35] = layouts[16]; + layouts[36][28] = 90; + layouts[36][29] = 89; + layouts[36][39] = 192; + layouts[36][45] = 191; + layouts[36][46] = 187; + layouts[36][47] = 219; + layouts[36][48] = 221; + layouts[36][49] = 220; + layouts[36][50] = 220; + layouts[36][51] = 186; + layouts[36][53] = 48; + layouts[37] = layouts[3]; + layouts[38] = layouts[3]; + return layouts; +}(); + +constexpr int kErrInvalidKeyboardType = static_cast(0x80BC001Du); +constexpr auto CharacterTables = [] { + std::array, 111> tables{}; + tables[1][4] = 327777; + tables[1][5] = 327778; + tables[1][6] = 327779; + tables[1][7] = 327780; + tables[1][8] = 327781; + tables[1][9] = 327782; + tables[1][10] = 327783; + tables[1][11] = 327784; + tables[1][12] = 327785; + tables[1][13] = 327786; + tables[1][14] = 327787; + tables[1][15] = 327788; + tables[1][16] = 327789; + tables[1][17] = 327790; + tables[1][18] = 327791; + tables[1][19] = 327792; + tables[1][20] = 327793; + tables[1][21] = 327794; + tables[1][22] = 327795; + tables[1][23] = 327796; + tables[1][24] = 327797; + tables[1][25] = 327798; + tables[1][26] = 327799; + tables[1][27] = 327800; + tables[1][28] = 327802; + tables[1][29] = 327801; + tables[1][30] = 262193; + tables[1][31] = 262194; + tables[1][32] = 262195; + tables[1][33] = 262196; + tables[1][34] = 262197; + tables[1][35] = 262198; + tables[1][36] = 262199; + tables[1][37] = 262200; + tables[1][38] = 262201; + tables[1][39] = 262192; + tables[1][40] = 262154; + tables[1][42] = 262152; + tables[1][43] = 262153; + tables[1][44] = 262176; + tables[1][45] = 262183; + tables[1][46] = 262238; + tables[1][47] = 262396; + tables[1][48] = 262312; + tables[1][49] = 262180; + tables[1][50] = 262180; + tables[1][51] = 262390; + tables[1][52] = 262372; + tables[1][53] = 262311; + tables[1][54] = 262188; + tables[1][55] = 262190; + tables[1][56] = 262189; + tables[1][83] = 393299; + tables[1][84] = 393263; + tables[1][85] = 393258; + tables[1][86] = 393261; + tables[1][87] = 393259; + tables[1][88] = 393226; + tables[1][89] = 393293; + tables[1][90] = 393297; + tables[1][91] = 393294; + tables[1][92] = 393296; + tables[1][93] = 393309; + tables[1][94] = 393295; + tables[1][95] = 393290; + tables[1][96] = 393298; + tables[1][97] = 393291; + tables[1][98] = 393289; + tables[1][99] = 393292; + tables[1][100] = 262204; + tables[1][103] = 393277; + tables[1][133] = 393260; + tables[1][134] = 393277; + tables[1][140] = 393260; + tables[2] = tables[1]; + tables[2][4] = 327745; + tables[2][5] = 327746; + tables[2][6] = 327747; + tables[2][7] = 327748; + tables[2][8] = 327749; + tables[2][9] = 327750; + tables[2][10] = 327751; + tables[2][11] = 327752; + tables[2][12] = 327753; + tables[2][13] = 327754; + tables[2][14] = 327755; + tables[2][15] = 327756; + tables[2][16] = 327757; + tables[2][17] = 327758; + tables[2][18] = 327759; + tables[2][19] = 327760; + tables[2][20] = 327761; + tables[2][21] = 327762; + tables[2][22] = 327763; + tables[2][23] = 327764; + tables[2][24] = 327765; + tables[2][25] = 327766; + tables[2][26] = 327767; + tables[2][27] = 327768; + tables[2][28] = 327770; + tables[2][29] = 327769; + tables[2][30] = 262187; + tables[2][31] = 262178; + tables[2][32] = 262186; + tables[2][33] = 262375; + tables[2][34] = 262181; + tables[2][35] = 262182; + tables[2][36] = 262191; + tables[2][37] = 262184; + tables[2][38] = 262185; + tables[2][39] = 262205; + tables[2][45] = 262207; + tables[2][46] = 262240; + tables[2][47] = 262376; + tables[2][48] = 262177; + tables[2][49] = 262307; + tables[2][50] = 262307; + tables[2][51] = 262377; + tables[2][52] = 262368; + tables[2][53] = 262320; + tables[2][54] = 262203; + tables[2][55] = 262202; + tables[2][56] = 262239; + tables[2][89] = 393265; + tables[2][90] = 393266; + tables[2][91] = 393267; + tables[2][92] = 393268; + tables[2][93] = 393269; + tables[2][94] = 393270; + tables[2][95] = 393271; + tables[2][96] = 393272; + tables[2][97] = 393273; + tables[2][98] = 393264; + tables[2][99] = 393262; + tables[2][100] = 262206; + tables[3][8] = 336044; + tables[3][30] = 262310; + tables[3][31] = 262208; + tables[3][32] = 262179; + tables[3][33] = 262320; + tables[3][34] = 262311; + tables[3][35] = 262316; + tables[3][36] = 262268; + tables[3][37] = 262306; + tables[3][45] = 262324; + tables[3][46] = 262270; + tables[3][47] = 262235; + tables[3][48] = 262237; + tables[3][49] = 262269; + tables[3][50] = 262269; + tables[3][52] = 262267; + tables[3][100] = 262236; + tables[3][136] = 262324; + tables[3][137] = 262324; + tables[3][138] = 262324; + tables[3][146] = 262324; + tables[3][147] = 262324; + tables[4] = tables[1]; + tables[4][28] = 327801; + tables[4][29] = 327802; + tables[4][45] = 262189; + tables[4][46] = 262205; + tables[4][47] = 262235; + tables[4][48] = 262237; + tables[4][49] = 262236; + tables[4][50] = 262236; + tables[4][51] = 262203; + tables[4][52] = 262183; + tables[4][53] = 262240; + tables[4][56] = 262191; + tables[4][100] = 262236; + tables[5] = tables[2]; + tables[5][28] = 327769; + tables[5][29] = 327770; + tables[5][30] = 262177; + tables[5][31] = 262208; + tables[5][32] = 262179; + tables[5][33] = 262180; + tables[5][35] = 262238; + tables[5][36] = 262182; + tables[5][37] = 262186; + tables[5][38] = 262184; + tables[5][39] = 262185; + tables[5][45] = 262239; + tables[5][46] = 262187; + tables[5][47] = 262267; + tables[5][48] = 262269; + tables[5][49] = 262268; + tables[5][50] = 262268; + tables[5][51] = 262202; + tables[5][52] = 262178; + tables[5][53] = 262270; + tables[5][54] = 262204; + tables[5][55] = 262206; + tables[5][56] = 262207; + tables[5][100] = 262268; + tables[6][34] = 270508; + tables[7] = tables[1]; + tables[7][4] = 393025; + tables[7][5] = 393026; + tables[7][6] = 393027; + tables[7][7] = 393028; + tables[7][8] = 393029; + tables[7][9] = 393030; + tables[7][10] = 393031; + tables[7][11] = 393032; + tables[7][12] = 393033; + tables[7][13] = 393034; + tables[7][14] = 393035; + tables[7][15] = 393036; + tables[7][16] = 393037; + tables[7][17] = 393038; + tables[7][18] = 393039; + tables[7][19] = 393040; + tables[7][20] = 393041; + tables[7][21] = 393042; + tables[7][22] = 393043; + tables[7][23] = 393044; + tables[7][24] = 393045; + tables[7][25] = 393046; + tables[7][26] = 393047; + tables[7][27] = 393048; + tables[7][28] = 393049; + tables[7][29] = 393050; + tables[7][30] = 327441; + tables[7][31] = 327442; + tables[7][32] = 327443; + tables[7][33] = 327444; + tables[7][34] = 327445; + tables[7][35] = 327446; + tables[7][36] = 327447; + tables[7][37] = 327448; + tables[7][38] = 327449; + tables[7][39] = 327440; + tables[7][44] = 274432; + tables[7][45] = 327437; + tables[7][46] = 327486; + tables[7][47] = 327456; + tables[7][48] = 327483; + tables[7][49] = 327485; + tables[7][50] = 327485; + tables[7][51] = 327451; + tables[7][52] = 327450; + tables[7][53] = 0; + tables[7][54] = 327436; + tables[7][55] = 327438; + tables[7][56] = 327439; + tables[7][100] = 0; + tables[7][135] = 327653; + tables[8] = tables[2]; + tables[8][4] = 392993; + tables[8][5] = 392994; + tables[8][6] = 392995; + tables[8][7] = 392996; + tables[8][8] = 392997; + tables[8][9] = 392998; + tables[8][10] = 392999; + tables[8][11] = 393000; + tables[8][12] = 393001; + tables[8][13] = 393002; + tables[8][14] = 393003; + tables[8][15] = 393004; + tables[8][16] = 393005; + tables[8][17] = 393006; + tables[8][18] = 393007; + tables[8][19] = 393008; + tables[8][20] = 393009; + tables[8][21] = 393010; + tables[8][22] = 393011; + tables[8][23] = 393012; + tables[8][24] = 393013; + tables[8][25] = 393014; + tables[8][26] = 393015; + tables[8][27] = 393016; + tables[8][28] = 393017; + tables[8][29] = 393018; + tables[8][30] = 327425; + tables[8][31] = 270364; + tables[8][32] = 327427; + tables[8][33] = 327428; + tables[8][34] = 327429; + tables[8][35] = 327430; + tables[8][36] = 270360; + tables[8][37] = 327432; + tables[8][38] = 327433; + tables[8][39] = 0; + tables[8][45] = 327453; + tables[8][46] = 327518; + tables[8][47] = 327488; + tables[8][48] = 327515; + tables[8][49] = 327517; + tables[8][50] = 327517; + tables[8][51] = 327435; + tables[8][52] = 327434; + tables[8][53] = 0; + tables[8][54] = 327452; + tables[8][55] = 327454; + tables[8][56] = 327455; + tables[8][100] = 0; + tables[8][135] = 327487; + tables[8][137] = 327516; + tables[9] = tables[4]; + tables[9][46] = 262238; + tables[9][47] = 262208; + tables[9][48] = 262235; + tables[9][49] = 262237; + tables[9][50] = 262237; + tables[9][52] = 262202; + tables[9][53] = 0; + tables[9][100] = 0; + tables[9][135] = 262236; + tables[9][137] = 262309; + tables[10] = tables[5]; + tables[10][31] = 262178; + tables[10][35] = 262182; + tables[10][36] = 262183; + tables[10][37] = 262184; + tables[10][38] = 262185; + tables[10][39] = 0; + tables[10][45] = 262205; + tables[10][46] = 262270; + tables[10][47] = 262240; + tables[10][48] = 262267; + tables[10][49] = 262269; + tables[10][50] = 262269; + tables[10][51] = 262187; + tables[10][52] = 262186; + tables[10][53] = 0; + tables[10][100] = 0; + tables[10][135] = 262239; + tables[10][137] = 262268; + tables[11] = tables[9]; + tables[11][4] = 274625; + tables[11][5] = 274611; + tables[11][6] = 274621; + tables[11][7] = 274615; + tables[11][8] = 274596; + tables[11][9] = 274639; + tables[11][10] = 274605; + tables[11][11] = 274607; + tables[11][12] = 274635; + tables[11][13] = 274654; + tables[11][14] = 274638; + tables[11][15] = 274666; + tables[11][16] = 274658; + tables[11][17] = 274655; + tables[11][18] = 274665; + tables[11][19] = 274619; + tables[11][20] = 274623; + tables[11][21] = 274617; + tables[11][22] = 274632; + tables[11][23] = 274603; + tables[11][24] = 274634; + tables[11][25] = 274642; + tables[11][26] = 274630; + tables[11][27] = 274613; + tables[11][28] = 274675; + tables[11][29] = 274628; + tables[11][30] = 274636; + tables[11][31] = 274645; + tables[11][32] = 274594; + tables[11][33] = 274598; + tables[11][34] = 274600; + tables[11][35] = 274602; + tables[11][36] = 274660; + tables[11][37] = 274662; + tables[11][38] = 274664; + tables[11][39] = 274671; + tables[11][45] = 274651; + tables[11][46] = 274648; + tables[11][47] = 274587; + tables[11][48] = 274588; + tables[11][49] = 274656; + tables[11][50] = 274656; + tables[11][51] = 274668; + tables[11][52] = 274609; + tables[11][54] = 274637; + tables[11][55] = 274667; + tables[11][56] = 274657; + tables[11][135] = 274669; + tables[11][137] = 274684; + tables[12] = tables[11]; + tables[12][8] = 274595; + tables[12][29] = 274627; + tables[12][32] = 274593; + tables[12][33] = 274597; + tables[12][34] = 274599; + tables[12][35] = 274601; + tables[12][36] = 274659; + tables[12][37] = 274661; + tables[12][38] = 274663; + tables[12][39] = 274674; + tables[12][48] = 274444; + tables[12][49] = 274445; + tables[12][50] = 274445; + tables[12][54] = 274433; + tables[12][55] = 274434; + tables[12][56] = 274683; + tables[12][89] = 393265; + tables[12][90] = 393266; + tables[12][91] = 393267; + tables[12][92] = 393268; + tables[12][93] = 393269; + tables[12][94] = 393270; + tables[12][95] = 393271; + tables[12][96] = 393272; + tables[12][97] = 393273; + tables[12][98] = 393264; + tables[12][99] = 393262; + tables[13] = tables[9]; + tables[13][4] = 327553; + tables[13][5] = 327546; + tables[13][6] = 327551; + tables[13][7] = 327548; + tables[13][8] = 327538; + tables[13][9] = 327562; + tables[13][10] = 327543; + tables[13][11] = 327544; + tables[13][12] = 327558; + tables[13][13] = 327567; + tables[13][14] = 327561; + tables[13][15] = 327576; + tables[13][16] = 327571; + tables[13][17] = 327568; + tables[13][18] = 327575; + tables[13][19] = 327550; + tables[13][20] = 327552; + tables[13][21] = 327549; + tables[13][22] = 327556; + tables[13][23] = 327542; + tables[13][24] = 327557; + tables[13][25] = 327563; + tables[13][26] = 327555; + tables[13][27] = 327547; + tables[13][28] = 327581; + tables[13][29] = 327554; + tables[13][30] = 327559; + tables[13][31] = 327564; + tables[13][32] = 327537; + tables[13][33] = 327539; + tables[13][34] = 327540; + tables[13][35] = 327541; + tables[13][36] = 327572; + tables[13][37] = 327573; + tables[13][38] = 327574; + tables[13][39] = 327580; + tables[13][45] = 327566; + tables[13][46] = 327565; + tables[13][47] = 327582; + tables[13][48] = 327583; + tables[13][49] = 327569; + tables[13][50] = 327569; + tables[13][51] = 327578; + tables[13][52] = 327545; + tables[13][54] = 327560; + tables[13][55] = 327577; + tables[13][56] = 327570; + tables[13][135] = 327579; + tables[13][137] = 327536; + tables[14] = tables[13]; + tables[14][8] = 327528; + tables[14][29] = 327535; + tables[14][32] = 327527; + tables[14][33] = 327529; + tables[14][34] = 327530; + tables[14][35] = 327531; + tables[14][36] = 327532; + tables[14][37] = 327533; + tables[14][38] = 327534; + tables[14][39] = 327526; + tables[14][48] = 327522; + tables[14][49] = 327523; + tables[14][50] = 327523; + tables[14][54] = 327524; + tables[14][55] = 327521; + tables[14][56] = 327525; + tables[14][89] = 393265; + tables[14][90] = 393266; + tables[14][91] = 393267; + tables[14][92] = 393268; + tables[14][93] = 393269; + tables[14][94] = 393270; + tables[14][95] = 393271; + tables[14][96] = 393272; + tables[14][97] = 393273; + tables[14][98] = 393264; + tables[14][99] = 393262; + tables[15] = tables[11]; + tables[15][4] = 274529; + tables[15][5] = 274515; + tables[15][6] = 274525; + tables[15][7] = 274519; + tables[15][8] = 274500; + tables[15][9] = 274543; + tables[15][10] = 274509; + tables[15][11] = 274511; + tables[15][12] = 274539; + tables[15][13] = 274558; + tables[15][14] = 274542; + tables[15][15] = 274570; + tables[15][16] = 274562; + tables[15][17] = 274559; + tables[15][18] = 274569; + tables[15][19] = 274523; + tables[15][20] = 274527; + tables[15][21] = 274521; + tables[15][22] = 274536; + tables[15][23] = 274507; + tables[15][24] = 274538; + tables[15][25] = 274546; + tables[15][26] = 274534; + tables[15][27] = 274517; + tables[15][28] = 274579; + tables[15][29] = 274532; + tables[15][30] = 274540; + tables[15][31] = 274549; + tables[15][32] = 274498; + tables[15][33] = 274502; + tables[15][34] = 274504; + tables[15][35] = 274506; + tables[15][36] = 274564; + tables[15][37] = 274566; + tables[15][38] = 274568; + tables[15][39] = 274575; + tables[15][45] = 274555; + tables[15][46] = 274552; + tables[15][49] = 274560; + tables[15][50] = 274560; + tables[15][51] = 274572; + tables[15][52] = 274513; + tables[15][54] = 274541; + tables[15][55] = 274571; + tables[15][56] = 274561; + tables[15][135] = 274573; + tables[16] = tables[15]; + tables[16][8] = 274499; + tables[16][29] = 274531; + tables[16][32] = 274497; + tables[16][33] = 274501; + tables[16][34] = 274503; + tables[16][35] = 274505; + tables[16][36] = 274563; + tables[16][37] = 274565; + tables[16][38] = 274567; + tables[16][39] = 274578; + tables[16][48] = 274444; + tables[16][49] = 274445; + tables[16][50] = 274445; + tables[16][54] = 274433; + tables[16][55] = 274434; + tables[16][56] = 274683; + tables[16][89] = 393265; + tables[16][90] = 393266; + tables[16][91] = 393267; + tables[16][92] = 393268; + tables[16][93] = 393269; + tables[16][94] = 393270; + tables[16][95] = 393271; + tables[16][96] = 393272; + tables[16][97] = 393273; + tables[16][98] = 393264; + tables[16][99] = 393262; + tables[17] = tables[1]; + tables[17][30] = 327729; + tables[17][31] = 327730; + tables[17][32] = 327731; + tables[17][33] = 327732; + tables[17][34] = 327733; + tables[17][35] = 327734; + tables[17][36] = 327735; + tables[17][37] = 327736; + tables[17][38] = 327737; + tables[17][39] = 327728; + tables[17][45] = 327903; + tables[17][46] = 262324; + tables[17][47] = 327932; + tables[17][48] = 327723; + tables[17][49] = 327715; + tables[17][50] = 327715; + tables[17][51] = 327926; + tables[17][52] = 327908; + tables[17][53] = 262238; + tables[17][54] = 327724; + tables[17][55] = 327726; + tables[17][136] = 262238; + tables[17][137] = 262238; + tables[17][138] = 262238; + tables[17][146] = 262238; + tables[17][147] = 262238; + tables[18] = tables[2]; + tables[18][30] = 327713; + tables[18][31] = 327714; + tables[18][32] = 327847; + tables[18][33] = 327716; + tables[18][34] = 327717; + tables[18][35] = 327718; + tables[18][36] = 327727; + tables[18][37] = 327720; + tables[18][38] = 327721; + tables[18][39] = 327741; + tables[18][45] = 327743; + tables[18][47] = 327900; + tables[18][48] = 327722; + tables[18][49] = 327719; + tables[18][50] = 327719; + tables[18][51] = 327894; + tables[18][52] = 327876; + tables[18][54] = 327739; + tables[18][55] = 327738; + tables[19][8] = 336044; + tables[19][16] = 327861; + tables[19][20] = 327744; + tables[19][31] = 327858; + tables[19][32] = 327859; + tables[19][36] = 327803; + tables[19][37] = 327771; + tables[19][38] = 327773; + tables[19][39] = 327805; + tables[19][45] = 327772; + tables[19][48] = 327806; + tables[19][100] = 262268; + tables[20] = tables[1]; + tables[20][28] = 327801; + tables[20][29] = 327802; + tables[20][46] = 262305; + tables[20][47] = 262240; + tables[20][48] = 262187; + tables[20][49] = 327911; + tables[20][50] = 327911; + tables[20][51] = 327921; + tables[20][52] = 262324; + tables[20][53] = 262330; + tables[21] = tables[2]; + tables[21][28] = 327769; + tables[21][29] = 327770; + tables[21][30] = 262177; + tables[21][32] = 262327; + tables[21][33] = 262180; + tables[21][46] = 262335; + tables[21][47] = 262238; + tables[21][48] = 262186; + tables[21][49] = 327879; + tables[21][50] = 327879; + tables[21][51] = 327889; + tables[21][52] = 262312; + tables[21][53] = 262314; + tables[22] = tables[3]; + tables[22][30] = 262268; + tables[22][33] = 262270; + tables[22][34] = 262272; + tables[22][36] = 0; + tables[22][37] = 0; + tables[22][45] = 0; + tables[22][46] = 0; + tables[22][49] = 327805; + tables[22][50] = 327805; + tables[22][53] = 262236; + tables[22][100] = 0; + tables[22][136] = 262270; + tables[22][137] = 262270; + tables[22][138] = 262270; + tables[22][146] = 262270; + tables[22][147] = 262270; + tables[23] = tables[17]; + tables[23][4] = 327793; + tables[23][16] = 327724; + tables[23][20] = 327777; + tables[23][26] = 327802; + tables[23][28] = 327801; + tables[23][29] = 327799; + tables[23][30] = 327718; + tables[23][31] = 327913; + tables[23][32] = 327714; + tables[23][33] = 327719; + tables[23][34] = 327720; + tables[23][35] = 327725; + tables[23][36] = 327912; + tables[23][37] = 327775; + tables[23][38] = 327911; + tables[23][39] = 327904; + tables[23][45] = 327721; + tables[23][46] = 327741; + tables[23][47] = 327774; + tables[23][48] = 327716; + tables[23][49] = 327722; + tables[23][50] = 327722; + tables[23][51] = 327789; + tables[23][52] = 327929; + tables[23][53] = 262322; + tables[23][54] = 327739; + tables[23][55] = 327738; + tables[23][56] = 327713; + tables[24] = tables[21]; + tables[24][4] = 327761; + tables[24][16] = 327743; + tables[24][20] = 327745; + tables[24][26] = 327770; + tables[24][29] = 327767; + tables[24][30] = 327729; + tables[24][31] = 327730; + tables[24][32] = 327731; + tables[24][33] = 327732; + tables[24][34] = 327733; + tables[24][35] = 327734; + tables[24][36] = 327735; + tables[24][37] = 327736; + tables[24][38] = 327737; + tables[24][39] = 327728; + tables[24][45] = 327856; + tables[24][46] = 327723; + tables[24][47] = 327848; + tables[24][48] = 327843; + tables[24][49] = 327861; + tables[24][50] = 327861; + tables[24][51] = 327757; + tables[24][52] = 327717; + tables[24][53] = 0; + tables[24][54] = 327726; + tables[24][55] = 327727; + tables[24][56] = 327847; + tables[24][136] = 262312; + tables[24][137] = 262312; + tables[24][138] = 262312; + tables[24][146] = 262312; + tables[24][147] = 262312; + tables[25][8] = 336044; + tables[25][31] = 262270; + tables[25][32] = 327715; + tables[25][33] = 327803; + tables[25][34] = 327771; + tables[25][35] = 327804; + tables[25][36] = 262240; + tables[25][37] = 327772; + tables[25][38] = 327774; + tables[25][39] = 327744; + tables[25][45] = 327773; + tables[25][46] = 327805; + tables[25][48] = 327844; + tables[25][136] = 262270; + tables[25][137] = 262270; + tables[25][138] = 262270; + tables[25][146] = 262270; + tables[25][147] = 262270; + tables[26] = tables[20]; + tables[26][46] = 262380; + tables[26][47] = 262376; + tables[26][49] = 262393; + tables[26][50] = 262393; + tables[26][51] = 262386; + tables[26][52] = 262368; + tables[26][53] = 262236; + tables[27] = tables[21]; + tables[27][32] = 262307; + tables[27][46] = 262238; + tables[27][47] = 262377; + tables[27][49] = 262311; + tables[27][50] = 262311; + tables[27][51] = 262375; + tables[27][52] = 262320; + tables[27][53] = 262268; + tables[28] = tables[6]; + tables[28][8] = 336044; + tables[28][47] = 262235; + tables[28][48] = 262237; + tables[28][51] = 262208; + tables[28][52] = 262179; + tables[29][47] = 262267; + tables[29][48] = 262269; + tables[30] = tables[20]; + tables[30][45] = 262191; + tables[30][46] = 262320; + tables[30][47] = 262312; + tables[30][48] = 262186; + tables[30][49] = 262204; + tables[30][50] = 262204; + tables[30][51] = 262187; + tables[30][53] = 262208; + tables[30][100] = 262237; + tables[31] = tables[21]; + tables[31][32] = 262179; + tables[31][36] = 262239; + tables[31][39] = 262183; + tables[31][46] = 262270; + tables[31][48] = 262268; + tables[31][49] = 262206; + tables[31][50] = 262206; + tables[31][51] = 262321; + tables[31][52] = 262240; + tables[31][53] = 262311; + tables[31][56] = 262205; + tables[31][100] = 262235; + tables[31][136] = 262270; + tables[31][137] = 262270; + tables[31][138] = 262270; + tables[31][146] = 262270; + tables[31][147] = 262270; + tables[32][6] = 327842; + tables[32][8] = 336044; + tables[32][16] = 327861; + tables[32][21] = 327862; + tables[32][22] = 327903; + tables[32][27] = 327867; + tables[32][29] = 327851; + tables[32][30] = 262329; + tables[32][31] = 262322; + tables[32][32] = 262323; + tables[32][33] = 262332; + tables[32][34] = 262333; + tables[32][35] = 262334; + tables[32][36] = 262307; + tables[32][37] = 262267; + tables[32][38] = 262269; + tables[32][45] = 262236; + tables[32][46] = 262328; + tables[32][53] = 262316; + tables[32][55] = 262327; + tables[32][100] = 262310; + tables[32][136] = 262328; + tables[32][137] = 262328; + tables[32][138] = 262328; + tables[32][146] = 262328; + tables[32][147] = 262328; + tables[33] = tables[26]; + tables[33][46] = 262315; + tables[33][47] = 262187; + tables[33][48] = 262324; + tables[33][49] = 262270; + tables[33][50] = 262270; + tables[33][51] = 327911; + tables[33][52] = 262330; + tables[34] = tables[27]; + tables[34][32] = 262179; + tables[34][46] = 262331; + tables[34][47] = 262186; + tables[34][48] = 262240; + tables[34][49] = 262238; + tables[34][50] = 262238; + tables[34][51] = 327879; + tables[34][52] = 262314; + tables[35] = tables[6]; + tables[35][8] = 336044; + tables[35][31] = 262208; + tables[35][32] = 262307; + tables[35][33] = 262311; + tables[35][36] = 262267; + tables[35][37] = 262235; + tables[35][38] = 262237; + tables[35][39] = 262269; + tables[35][47] = 262312; + tables[35][48] = 262237; + tables[35][136] = 262312; + tables[35][137] = 262312; + tables[35][138] = 262312; + tables[35][146] = 262312; + tables[35][147] = 262312; + tables[36] = tables[4]; + tables[36][4] = 328772; + tables[36][5] = 328760; + tables[36][6] = 328769; + tables[36][7] = 328754; + tables[36][8] = 328771; + tables[36][9] = 328752; + tables[36][10] = 328767; + tables[36][11] = 328768; + tables[36][12] = 328776; + tables[36][13] = 328766; + tables[36][14] = 328763; + tables[36][15] = 328756; + tables[36][16] = 328780; + tables[36][17] = 328770; + tables[36][18] = 328777; + tables[36][19] = 328759; + tables[36][20] = 328761; + tables[36][21] = 328762; + tables[36][22] = 328779; + tables[36][23] = 328757; + tables[36][24] = 328755; + tables[36][25] = 328764; + tables[36][26] = 328774; + tables[36][27] = 328775; + tables[36][28] = 328765; + tables[36][29] = 328783; + tables[36][47] = 328773; + tables[36][48] = 328778; + tables[36][51] = 328758; + tables[36][52] = 328781; + tables[36][53] = 328785; + tables[36][54] = 328753; + tables[36][55] = 328782; + tables[36][56] = 262190; + tables[37] = tables[5]; + tables[37][4] = 328740; + tables[37][5] = 328728; + tables[37][6] = 328737; + tables[37][7] = 328722; + tables[37][8] = 328739; + tables[37][9] = 328720; + tables[37][10] = 328735; + tables[37][11] = 328736; + tables[37][12] = 328744; + tables[37][13] = 328734; + tables[37][14] = 328731; + tables[37][15] = 328724; + tables[37][16] = 328748; + tables[37][17] = 328738; + tables[37][18] = 328745; + tables[37][19] = 328727; + tables[37][20] = 328729; + tables[37][21] = 328730; + tables[37][22] = 328747; + tables[37][23] = 328725; + tables[37][24] = 328723; + tables[37][25] = 328732; + tables[37][26] = 328742; + tables[37][27] = 328743; + tables[37][28] = 328733; + tables[37][29] = 328751; + tables[37][31] = 262178; + tables[37][32] = 270614; + tables[37][33] = 262203; + tables[37][35] = 262202; + tables[37][36] = 262207; + tables[37][47] = 328741; + tables[37][48] = 328746; + tables[37][49] = 262191; + tables[37][50] = 262191; + tables[37][51] = 328726; + tables[37][52] = 328749; + tables[37][53] = 328705; + tables[37][54] = 328721; + tables[37][55] = 328750; + tables[37][56] = 262188; + tables[37][100] = 262191; + tables[38] = tables[4]; + tables[38][49] = 262179; + tables[38][50] = 262179; + tables[39] = tables[5]; + tables[39][31] = 262178; + tables[39][32] = 262307; + tables[39][49] = 262270; + tables[39][50] = 262270; + tables[39][52] = 262208; + tables[39][53] = 262316; + tables[40][4] = 327905; + tables[40][8] = 327913; + tables[40][12] = 327917; + tables[40][18] = 327923; + tables[40][24] = 327930; + tables[40][33] = 270508; + tables[40][53] = 262310; + tables[41][4] = 327873; + tables[41][8] = 327881; + tables[41][12] = 327885; + tables[41][18] = 327891; + tables[41][24] = 327898; + tables[42] = tables[4]; + tables[42][4] = 332038; + tables[42][5] = 332146; + tables[42][6] = 332046; + tables[42][7] = 332043; + tables[42][8] = 332035; + tables[42][9] = 332037; + tables[42][10] = 332050; + tables[42][11] = 332137; + tables[42][12] = 332131; + tables[42][13] = 332133; + tables[42][14] = 332129; + tables[42][15] = 332149; + tables[42][16] = 332147; + tables[42][17] = 332142; + tables[42][18] = 332130; + tables[42][19] = 332134; + tables[42][20] = 332039; + tables[42][21] = 332032; + tables[42][22] = 332034; + tables[42][23] = 332041; + tables[42][24] = 332135; + tables[42][25] = 332049; + tables[42][26] = 332044; + tables[42][27] = 332048; + tables[42][28] = 332141; + tables[42][29] = 332047; + tables[42][50] = 262237; + tables[42][52] = 270360; + tables[43] = tables[5]; + tables[43][4] = 332038; + tables[43][5] = 332146; + tables[43][6] = 332046; + tables[43][7] = 332043; + tables[43][8] = 332036; + tables[43][9] = 332037; + tables[43][10] = 332050; + tables[43][11] = 332137; + tables[43][12] = 332131; + tables[43][13] = 332133; + tables[43][14] = 332129; + tables[43][15] = 332149; + tables[43][16] = 332147; + tables[43][17] = 332142; + tables[43][18] = 332132; + tables[43][19] = 332136; + tables[43][20] = 332040; + tables[43][21] = 332033; + tables[43][22] = 332034; + tables[43][23] = 332042; + tables[43][24] = 332135; + tables[43][25] = 332049; + tables[43][26] = 332045; + tables[43][27] = 332048; + tables[43][28] = 332141; + tables[43][29] = 332047; + tables[43][50] = 262269; + tables[43][52] = 270364; + tables[44] = tables[20]; + tables[44][45] = 262187; + tables[44][46] = 262236; + tables[44][47] = 327909; + tables[44][48] = 262312; + tables[44][49] = 262183; + tables[44][50] = 262183; + tables[44][51] = 327928; + tables[44][52] = 327910; + tables[44][53] = 262268; + tables[45] = tables[34]; + tables[45][33] = 262308; + tables[45][46] = 262240; + tables[45][47] = 327877; + tables[45][48] = 262238; + tables[45][49] = 262186; + tables[45][50] = 262186; + tables[45][51] = 327896; + tables[45][52] = 327878; + tables[45][53] = 262311; + tables[45][136] = 262240; + tables[45][137] = 262240; + tables[45][138] = 262240; + tables[45][146] = 262240; + tables[45][147] = 262240; + tables[46] = tables[35]; + tables[46][16] = 327861; + tables[46][33] = 262180; + tables[46][46] = 262324; + tables[46][47] = 0; + tables[46][48] = 262270; + tables[46][136] = 262324; + tables[46][137] = 262324; + tables[46][138] = 262324; + tables[46][146] = 262324; + tables[46][147] = 262324; + tables[47] = tables[44]; + tables[47][46] = 262324; + tables[47][51] = 327926; + tables[47][52] = 327908; + tables[47][53] = 262311; + tables[47][136] = 262312; + tables[47][137] = 262312; + tables[47][138] = 262312; + tables[47][146] = 262312; + tables[47][147] = 262312; + tables[48] = tables[45]; + tables[48][51] = 327894; + tables[48][52] = 327876; + tables[48][53] = 262333; + tables[48][136] = 262238; + tables[48][137] = 262238; + tables[48][138] = 262238; + tables[48][146] = 262238; + tables[48][147] = 262238; + tables[49] = tables[46]; + tables[49][45] = 262236; + tables[49][46] = 0; + tables[49][100] = 262268; + tables[49][136] = 262270; + tables[49][137] = 262270; + tables[49][138] = 262270; + tables[49][146] = 262270; + tables[49][147] = 262270; + tables[50] = tables[47]; + tables[50][51] = 327910; + tables[50][52] = 327928; + tables[50][53] = 262333; + tables[50][136] = 262324; + tables[50][137] = 262324; + tables[50][138] = 262324; + tables[50][146] = 262324; + tables[50][147] = 262324; + tables[51] = tables[45]; + tables[51][51] = 327878; + tables[51][52] = 327896; + tables[52] = tables[46]; + tables[52][34] = 262272; + tables[52][46] = 262268; + tables[52][100] = 262236; + tables[52][136] = 0; + tables[52][137] = 0; + tables[52][138] = 0; + tables[52][146] = 0; + tables[52][147] = 0; + tables[53] = tables[38]; + tables[53][4] = 340231; + tables[53][5] = 340246; + tables[53][6] = 340239; + tables[53][7] = 340238; + tables[53][8] = 340237; + tables[53][9] = 340241; + tables[53][10] = 340245; + tables[53][11] = 340248; + tables[53][12] = 340251; + tables[53][13] = 340264; + tables[53][14] = 340252; + tables[53][15] = 340256; + tables[53][16] = 340265; + tables[53][17] = 340249; + tables[53][18] = 340255; + tables[53][19] = 340259; + tables[53][20] = 340230; + tables[53][21] = 340240; + tables[53][22] = 340235; + tables[53][23] = 340244; + tables[53][24] = 340263; + tables[53][25] = 340242; + tables[53][26] = 340234; + tables[53][27] = 340236; + tables[53][28] = 340247; + tables[53][29] = 340232; + tables[53][30] = 274693; + tables[53][31] = 274697; + tables[53][32] = 262855; + tables[53][33] = 262859; + tables[53][34] = 274707; + tables[53][35] = 262858; + tables[53][36] = 262873; + tables[53][37] = 274714; + tables[53][38] = 274718; + tables[53][39] = 274722; + tables[53][45] = 274726; + tables[53][51] = 274724; + tables[53][52] = 270360; + tables[53][54] = 274717; + tables[53][55] = 274721; + tables[53][56] = 274725; + tables[54] = tables[43]; + tables[54][4] = 327777; + tables[54][5] = 327778; + tables[54][6] = 327779; + tables[54][7] = 327780; + tables[54][8] = 327781; + tables[54][9] = 327782; + tables[54][10] = 327783; + tables[54][11] = 327784; + tables[54][12] = 327785; + tables[54][13] = 327786; + tables[54][14] = 327787; + tables[54][15] = 327788; + tables[54][16] = 327789; + tables[54][17] = 327790; + tables[54][18] = 327791; + tables[54][19] = 327792; + tables[54][20] = 327793; + tables[54][21] = 327794; + tables[54][22] = 327795; + tables[54][23] = 327796; + tables[54][24] = 327797; + tables[54][25] = 327798; + tables[54][26] = 327799; + tables[54][27] = 327800; + tables[54][28] = 327801; + tables[54][29] = 327802; + tables[54][50] = 262179; + tables[54][100] = 262236; + tables[55] = tables[5]; + tables[55][52] = 270364; + tables[56] = tables[7]; + tables[56][46] = 327453; + tables[56][47] = 327483; + tables[56][48] = 327485; + tables[56][49] = 327484; + tables[56][50] = 327484; + tables[56][52] = 270360; + tables[56][53] = 270360; + tables[56][100] = 327653; + tables[56][135] = 0; + tables[57] = tables[8]; + tables[57][31] = 327456; + tables[57][35] = 327486; + tables[57][36] = 327430; + tables[57][37] = 327434; + tables[57][38] = 327432; + tables[57][39] = 327433; + tables[57][45] = 327487; + tables[57][46] = 327435; + tables[57][47] = 327515; + tables[57][48] = 327517; + tables[57][49] = 327516; + tables[57][50] = 327516; + tables[57][51] = 327450; + tables[57][52] = 270364; + tables[57][53] = 327518; + tables[57][100] = 327516; + tables[57][135] = 0; + tables[57][137] = 0; + tables[58] = tables[42]; + tables[58][4] = 353765; + tables[58][5] = 354056; + tables[58][6] = 365009; + tables[58][7] = 354088; + tables[58][8] = 355380; + tables[58][9] = 356459; + tables[58][10] = 349983; + tables[58][11] = 359161; + tables[58][12] = 352776; + tables[58][13] = 348993; + tables[58][14] = 350503; + tables[58][15] = 347693; + tables[58][16] = 347648; + tables[58][17] = 352019; + tables[58][18] = 347834; + tables[58][19] = 352195; + tables[58][20] = 352843; + tables[58][21] = 349155; + tables[58][22] = 351288; + tables[58][23] = 351999; + tables[58][24] = 351345; + tables[58][25] = 350579; + tables[58][26] = 357680; + tables[58][27] = 366307; + tables[58][28] = 349020; + tables[58][29] = 365005; + tables[58][50] = 262179; + tables[59] = tables[1]; + tables[59][47] = 262376; + tables[59][51] = 262377; + tables[59][52] = 262368; + tables[60] = tables[2]; + tables[60][47] = 262396; + tables[60][51] = 262390; + tables[60][52] = 262372; + tables[61] = tables[4]; + tables[61][47] = 262238; + tables[61][48] = 262328; + tables[61][49] = 262204; + tables[61][50] = 262204; + tables[61][52] = 262240; + tables[61][53] = 262179; + tables[61][56] = 327913; + tables[61][100] = 262315; + tables[61][136] = 262240; + tables[61][137] = 262240; + tables[61][138] = 262240; + tables[61][146] = 262240; + tables[61][147] = 262240; + tables[62] = tables[39]; + tables[62][32] = 262191; + tables[62][35] = 262207; + tables[62][47] = 262238; + tables[62][48] = 262312; + tables[62][49] = 262206; + tables[62][50] = 262206; + tables[62][52] = 262240; + tables[62][53] = 262268; + tables[62][54] = 262183; + tables[62][55] = 262190; + tables[62][56] = 327881; + tables[62][100] = 262331; + tables[62][136] = 262240; + tables[62][137] = 262240; + tables[62][138] = 262240; + tables[62][146] = 262240; + tables[62][147] = 262240; + tables[63] = tables[52]; + tables[63][8] = 0; + tables[63][18] = 327847; + tables[63][19] = 327862; + tables[63][30] = 262321; + tables[63][33] = 262306; + tables[63][34] = 262308; + tables[63][35] = 262316; + tables[63][36] = 262310; + tables[63][37] = 262322; + tables[63][38] = 262323; + tables[63][39] = 262332; + tables[63][45] = 262333; + tables[63][46] = 262334; + tables[63][47] = 262235; + tables[63][48] = 262237; + tables[63][49] = 262269; + tables[63][50] = 262269; + tables[63][51] = 262270; + tables[63][52] = 262267; + tables[63][53] = 262236; + tables[63][54] = 262319; + tables[63][55] = 262317; + tables[63][56] = 262324; + tables[63][100] = 262320; + tables[64] = tables[23]; + tables[64][35] = 327847; + tables[64][37] = 327713; + tables[64][46] = 327725; + tables[64][49] = 327861; + tables[64][50] = 327861; + tables[64][56] = 327741; + tables[65] = tables[24]; + tables[65][46] = 327775; + tables[65][48] = 327722; + tables[65][49] = 327843; + tables[65][50] = 327843; + tables[65][53] = 262323; + tables[65][56] = 327723; + tables[66][8] = 336044; + tables[66][30] = 327804; + tables[66][31] = 327744; + tables[66][32] = 327715; + tables[66][33] = 327803; + tables[66][34] = 327771; + tables[66][35] = 327774; + tables[66][38] = 327803; + tables[66][39] = 327805; + tables[66][47] = 327771; + tables[66][48] = 327773; + tables[66][49] = 262240; + tables[66][50] = 262240; + tables[66][52] = 262324; + tables[66][56] = 262270; + tables[66][100] = 262236; + tables[67][49] = 262240; + tables[67][50] = 262240; + tables[67][52] = 262324; + tables[67][56] = 262270; + tables[67][136] = 262240; + tables[67][137] = 262240; + tables[67][138] = 262240; + tables[67][146] = 262240; + tables[67][147] = 262240; + tables[68] = tables[4]; + tables[68][49] = 262179; + tables[68][100] = 0; + tables[69] = tables[5]; + tables[69][49] = 262270; + tables[69][100] = 0; + tables[70][4] = 327941; + tables[70][6] = 327943; + tables[70][8] = 327961; + tables[70][15] = 328002; + tables[70][17] = 328004; + tables[70][18] = 327923; + tables[70][22] = 328027; + tables[70][24] = 270508; + tables[70][27] = 328058; + tables[70][29] = 328060; + tables[71][4] = 327940; + tables[71][6] = 327942; + tables[71][8] = 327960; + tables[71][15] = 328001; + tables[71][17] = 328003; + tables[71][18] = 327891; + tables[71][22] = 328026; + tables[71][27] = 328057; + tables[71][29] = 328059; + tables[72] = tables[4]; + tables[72][47] = 262324; + tables[72][48] = 262235; + tables[72][49] = 0; + tables[72][50] = 262237; + tables[72][51] = 327911; + tables[72][52] = 262270; + tables[72][53] = 262183; + tables[72][56] = 262203; + tables[72][135] = 262191; + tables[73] = tables[5]; + tables[73][35] = 262312; + tables[73][47] = 262240; + tables[73][48] = 262267; + tables[73][49] = 0; + tables[73][50] = 262269; + tables[73][51] = 327879; + tables[73][52] = 262238; + tables[73][53] = 262178; + tables[73][56] = 262202; + tables[73][135] = 262207; + tables[74][8] = 262320; + tables[74][20] = 262191; + tables[74][26] = 262207; + tables[74][30] = 262329; + tables[74][31] = 262322; + tables[74][32] = 262323; + tables[74][33] = 262307; + tables[74][34] = 262306; + tables[74][35] = 262316; + tables[74][46] = 262311; + tables[74][48] = 262314; + tables[74][50] = 262330; + tables[74][135] = 262320; + tables[75] = tables[20]; + tables[75][12] = 327985; + tables[75][45] = 262186; + tables[75][46] = 262189; + tables[75][47] = 327967; + tables[75][48] = 327932; + tables[75][49] = 0; + tables[75][50] = 262188; + tables[75][51] = 328031; + tables[75][52] = 327785; + tables[75][53] = 262178; + tables[75][54] = 327926; + tables[75][55] = 327911; + tables[75][56] = 262190; + tables[76] = tables[21]; + tables[76][31] = 262183; + tables[76][32] = 262238; + tables[76][33] = 262187; + tables[76][46] = 262239; + tables[76][47] = 327966; + tables[76][48] = 262364; + tables[76][49] = 0; + tables[76][50] = 262203; + tables[76][51] = 328030; + tables[76][52] = 327984; + tables[76][53] = 262377; + tables[76][54] = 327894; + tables[76][55] = 327879; + tables[76][56] = 262202; + tables[77] = tables[52]; + tables[77][4] = 327910; + tables[77][8] = 270508; + tables[77][12] = 327785; + tables[77][16] = 0; + tables[77][20] = 262208; + tables[77][22] = 262367; + tables[77][30] = 262206; + tables[77][31] = 262307; + tables[77][32] = 262179; + tables[77][34] = 262333; + tables[77][45] = 262236; + tables[77][47] = 262312; + tables[77][50] = 262240; + tables[77][51] = 262324; + tables[77][53] = 262204; + tables[77][100] = 262268; + tables[78][4] = 327878; + tables[78][12] = 327984; + tables[79] = tables[20]; + tables[79][41] = 262268; + tables[79][46] = 262335; + tables[79][47] = 262324; + tables[79][50] = 327805; + tables[79][52] = 262267; + tables[79][53] = 262268; + tables[80] = tables[21]; + tables[80][32] = 262179; + tables[80][46] = 262305; + tables[80][47] = 262312; + tables[80][50] = 327773; + tables[80][52] = 262235; + tables[80][53] = 262320; + tables[81][20] = 262208; + tables[81][45] = 262236; + tables[81][48] = 262270; + tables[81][50] = 262240; + tables[81][52] = 262238; + tables[81][53] = 262316; + tables[82] = tables[4]; + tables[82][4] = 263732; + tables[82][5] = 263748; + tables[82][6] = 263716; + tables[82][7] = 263754; + tables[82][8] = 263723; + tables[82][9] = 263720; + tables[82][10] = 263748; + tables[82][11] = 263719; + tables[82][12] = 263751; + tables[82][13] = 263722; + tables[82][14] = 263750; + tables[82][15] = 263749; + tables[82][16] = 263721; + tables[82][17] = 263753; + tables[82][18] = 263726; + tables[82][19] = 263725; + tables[82][20] = 263734; + tables[82][21] = 263746; + tables[82][22] = 263731; + tables[82][23] = 263745; + tables[82][24] = 263737; + tables[82][25] = 263729; + tables[82][26] = 263733; + tables[82][27] = 263713; + tables[82][28] = 263738; + tables[82][29] = 263718; + tables[82][47] = 263724; + tables[82][48] = 263727; + tables[82][51] = 263747; + tables[82][52] = 263735; + tables[82][53] = 263728; + tables[82][54] = 263752; + tables[82][55] = 263730; + tables[82][56] = 263736; + tables[83] = tables[5]; + tables[83][4] = 263760; + tables[83][5] = 263748; + tables[83][6] = 262269; + tables[83][7] = 262237; + tables[83][8] = 263759; + tables[83][9] = 262235; + tables[83][10] = 263748; + tables[83][11] = 263715; + tables[83][12] = 262391; + tables[83][13] = 263744; + tables[83][14] = 263692; + tables[83][15] = 262191; + tables[83][16] = 270361; + tables[83][17] = 263714; + tables[83][18] = 262359; + tables[83][19] = 263707; + tables[83][20] = 263758; + tables[83][21] = 263756; + tables[83][22] = 263757; + tables[83][23] = 263748; + tables[83][24] = 270360; + tables[83][25] = 262267; + tables[83][26] = 263755; + tables[83][27] = 263762; + tables[83][28] = 263717; + tables[83][29] = 262270; + tables[83][38] = 262185; + tables[83][39] = 262184; + tables[83][47] = 262204; + tables[83][48] = 262206; + tables[83][53] = 263761; + tables[83][54] = 262188; + tables[83][55] = 262190; + tables[83][56] = 263711; + tables[84] = tables[1]; + tables[84][4] = 331295; + tables[84][5] = 331316; + tables[84][6] = 331329; + tables[84][7] = 331265; + tables[84][8] = 331315; + tables[84][9] = 331284; + tables[84][10] = 331328; + tables[84][11] = 331337; + tables[84][12] = 331299; + tables[84][13] = 331336; + tables[84][14] = 331314; + tables[84][15] = 331306; + tables[84][16] = 331287; + tables[84][17] = 331319; + tables[84][18] = 331289; + tables[84][19] = 331298; + tables[84][20] = 331334; + tables[84][21] = 331294; + tables[84][22] = 331307; + tables[84][23] = 331312; + tables[84][24] = 331317; + tables[84][25] = 331309; + tables[84][26] = 331332; + tables[84][27] = 331291; + tables[84][28] = 331313; + tables[84][29] = 331292; + tables[84][30] = 331333; + tables[84][31] = 327727; + tables[84][32] = 327725; + tables[84][33] = 331296; + tables[84][34] = 331286; + tables[84][35] = 331320; + tables[84][36] = 331318; + tables[84][37] = 331268; + tables[84][38] = 331285; + tables[84][39] = 331272; + tables[84][45] = 331266; + tables[84][46] = 331274; + tables[84][47] = 331290; + tables[84][48] = 331301; + tables[84][49] = 331267; + tables[84][50] = 331267; + tables[84][51] = 331303; + tables[84][52] = 331271; + tables[84][53] = 327775; + tables[84][54] = 331297; + tables[84][55] = 331331; + tables[84][56] = 331293; + tables[84][100] = 331267; + tables[84][103] = 0; + tables[84][133] = 0; + tables[84][134] = 0; + tables[84][140] = 0; + tables[85] = tables[2]; + tables[85][4] = 331300; + tables[85][5] = 331322; + tables[85][6] = 331273; + tables[85][7] = 331279; + tables[85][8] = 331278; + tables[85][9] = 331330; + tables[85][10] = 331276; + tables[85][11] = 331335; + tables[85][12] = 331283; + tables[85][13] = 331339; + tables[85][14] = 331305; + tables[85][15] = 331304; + tables[85][16] = 327743; + tables[85][17] = 331340; + tables[85][18] = 331311; + tables[85][19] = 331277; + tables[85][20] = 331344; + tables[85][21] = 331281; + tables[85][22] = 331270; + tables[85][23] = 331288; + tables[85][24] = 331338; + tables[85][25] = 331310; + tables[85][26] = 327714; + tables[85][27] = 327721; + tables[85][28] = 331341; + tables[85][29] = 327720; + tables[85][30] = 327723; + tables[85][31] = 331345; + tables[85][32] = 331346; + tables[85][33] = 331347; + tables[85][34] = 331348; + tables[85][35] = 331321; + tables[85][36] = 331327; + tables[85][37] = 331349; + tables[85][38] = 331350; + tables[85][39] = 331351; + tables[85][45] = 331352; + tables[85][46] = 331353; + tables[85][47] = 331280; + tables[85][48] = 327724; + tables[85][49] = 331269; + tables[85][50] = 331269; + tables[85][51] = 331275; + tables[85][52] = 327726; + tables[85][53] = 327717; + tables[85][54] = 331282; + tables[85][55] = 331308; + tables[85][56] = 331302; + tables[85][100] = 331269; + tables[85][103] = 0; + tables[85][133] = 0; + tables[85][134] = 0; + tables[85][140] = 0; + tables[86] = tables[1]; + tables[86][30] = 262187; + tables[86][31] = 327963; + tables[86][32] = 328033; + tables[86][33] = 327949; + tables[86][34] = 328025; + tables[86][35] = 328062; + tables[86][36] = 327933; + tables[86][37] = 327905; + tables[86][38] = 327917; + tables[86][39] = 327913; + tables[86][45] = 262205; + tables[86][46] = 262324; + tables[86][47] = 327930; + tables[86][48] = 262185; + tables[86][49] = 262312; + tables[86][50] = 262312; + tables[86][51] = 328047; + tables[86][52] = 262311; + tables[86][53] = 262203; + tables[86][100] = 262236; + tables[86][103] = 0; + tables[86][133] = 0; + tables[86][134] = 0; + tables[86][140] = 0; + tables[87] = tables[2]; + tables[87][30] = 262193; + tables[87][31] = 262194; + tables[87][32] = 262195; + tables[87][33] = 262196; + tables[87][34] = 262197; + tables[87][35] = 262198; + tables[87][36] = 262199; + tables[87][37] = 262200; + tables[87][38] = 262201; + tables[87][39] = 262192; + tables[87][45] = 262181; + tables[87][46] = 262855; + tables[87][47] = 262191; + tables[87][48] = 262184; + tables[87][49] = 262183; + tables[87][50] = 262183; + tables[87][51] = 262178; + tables[87][52] = 262177; + tables[87][54] = 262207; + tables[87][100] = 262268; + tables[87][103] = 0; + tables[87][133] = 0; + tables[87][134] = 0; + tables[87][140] = 0; + tables[88][5] = 262267; + tables[88][6] = 262182; + tables[88][7] = 262416; + tables[88][8] = 270508; + tables[88][9] = 262235; + tables[88][10] = 262237; + tables[88][14] = 262466; + tables[88][15] = 262465; + tables[88][17] = 262269; + tables[88][20] = 262236; + tables[88][22] = 262417; + tables[88][25] = 262208; + tables[88][26] = 262268; + tables[88][27] = 262179; + tables[88][30] = 262270; + tables[88][31] = 262855; + tables[88][32] = 262238; + tables[88][33] = 262872; + tables[88][34] = 262320; + tables[88][35] = 262875; + tables[88][36] = 262240; + tables[88][37] = 262873; + tables[88][38] = 262324; + tables[88][39] = 262877; + tables[88][45] = 262312; + tables[88][46] = 262328; + tables[88][47] = 262391; + tables[88][48] = 262359; + tables[88][49] = 262308; + tables[88][50] = 262308; + tables[88][51] = 262180; + tables[88][52] = 262367; + tables[88][54] = 262204; + tables[88][55] = 262206; + tables[88][56] = 262186; + tables[89] = tables[4]; + tables[89][4] = 328625; + tables[89][5] = 328626; + tables[89][6] = 328648; + tables[89][7] = 328628; + tables[89][8] = 328629; + tables[89][9] = 328646; + tables[89][10] = 328627; + tables[89][11] = 328631; + tables[89][12] = 328633; + tables[89][13] = 328638; + tables[89][14] = 328634; + tables[89][15] = 328635; + tables[89][16] = 328636; + tables[89][17] = 328637; + tables[89][18] = 328639; + tables[89][19] = 328640; + tables[89][20] = 262203; + tables[89][21] = 328641; + tables[89][22] = 328643; + tables[89][23] = 328644; + tables[89][24] = 328632; + tables[89][25] = 328649; + tables[89][26] = 263106; + tables[89][27] = 328647; + tables[89][28] = 328645; + tables[89][29] = 328630; + tables[89][51] = 263044; + tables[89][100] = 262204; + tables[89][103] = 0; + tables[89][133] = 0; + tables[89][134] = 0; + tables[89][140] = 0; + tables[90] = tables[5]; + tables[90][4] = 328593; + tables[90][5] = 328594; + tables[90][6] = 328616; + tables[90][7] = 328596; + tables[90][8] = 328597; + tables[90][9] = 328614; + tables[90][10] = 328595; + tables[90][11] = 328599; + tables[90][12] = 328601; + tables[90][13] = 328606; + tables[90][14] = 328602; + tables[90][15] = 328603; + tables[90][16] = 328604; + tables[90][17] = 328605; + tables[90][18] = 328607; + tables[90][19] = 328608; + tables[90][20] = 262202; + tables[90][21] = 328609; + tables[90][22] = 328611; + tables[90][23] = 328612; + tables[90][24] = 328600; + tables[90][25] = 328617; + tables[90][26] = 263045; + tables[90][27] = 328615; + tables[90][28] = 328613; + tables[90][29] = 328598; + tables[90][51] = 262312; + tables[90][100] = 262206; + tables[90][103] = 0; + tables[90][133] = 0; + tables[90][134] = 0; + tables[90][140] = 0; + tables[91][6] = 262313; + tables[91][8] = 270508; + tables[91][21] = 262318; + tables[91][28] = 262309; + tables[91][31] = 262322; + tables[91][32] = 262323; + tables[91][33] = 262307; + tables[91][34] = 262311; + tables[91][35] = 262326; + tables[91][37] = 262308; + tables[91][38] = 262310; + tables[91][39] = 262320; + tables[91][45] = 262321; + tables[91][46] = 262333; + tables[91][47] = 262315; + tables[91][48] = 262331; + tables[91][50] = 262316; + tables[91][51] = 263045; + tables[92] = tables[4]; + tables[92][103] = 0; + tables[92][133] = 0; + tables[92][134] = 0; + tables[92][140] = 0; + tables[93] = tables[5]; + tables[93][103] = 0; + tables[93][133] = 0; + tables[93][134] = 0; + tables[93][140] = 0; + tables[94] = tables[92]; + tables[94][30] = 327939; + tables[94][31] = 327906; + tables[94][32] = 327914; + tables[94][33] = 327924; + tables[94][34] = 262912; + tables[94][35] = 262921; + tables[94][36] = 262915; + tables[94][37] = 262913; + tables[94][38] = 262947; + tables[94][39] = 327953; + tables[94][46] = 270507; + tables[94][47] = 328112; + tables[94][48] = 328097; + tables[95] = tables[93]; + tables[95][30] = 327938; + tables[95][31] = 327874; + tables[95][32] = 327882; + tables[95][33] = 327892; + tables[95][34] = 262912; + tables[95][35] = 262921; + tables[95][36] = 262915; + tables[95][37] = 262913; + tables[95][38] = 262947; + tables[95][39] = 327952; + tables[95][47] = 328111; + tables[95][48] = 328096; + tables[96] = tables[92]; + tables[96][40] = 0; + tables[96][42] = 0; + tables[96][43] = 0; + tables[96][44] = 0; + tables[96][83] = 0; + tables[96][84] = 0; + tables[96][85] = 0; + tables[96][86] = 0; + tables[96][87] = 0; + tables[96][88] = 0; + tables[96][89] = 0; + tables[96][90] = 0; + tables[96][91] = 0; + tables[96][92] = 0; + tables[96][93] = 0; + tables[96][94] = 0; + tables[96][95] = 0; + tables[96][96] = 0; + tables[96][97] = 0; + tables[96][98] = 0; + tables[96][99] = 0; + tables[97] = tables[93]; + tables[97][40] = 0; + tables[97][42] = 0; + tables[97][43] = 0; + tables[97][44] = 0; + tables[97][83] = 0; + tables[97][84] = 0; + tables[97][85] = 0; + tables[97][86] = 0; + tables[97][87] = 0; + tables[97][88] = 0; + tables[97][89] = 0; + tables[97][90] = 0; + tables[97][91] = 0; + tables[97][92] = 0; + tables[97][93] = 0; + tables[97][94] = 0; + tables[97][95] = 0; + tables[97][96] = 0; + tables[97][97] = 0; + tables[97][98] = 0; + tables[97][99] = 0; + tables[98] = tables[92]; + tables[98][47] = 327939; + tables[98][48] = 327918; + tables[98][49] = 327906; + tables[98][50] = 327906; + tables[98][51] = 328217; + tables[98][52] = 328219; + tables[98][53] = 270366; + tables[99] = tables[93]; + tables[99][47] = 327938; + tables[99][48] = 327886; + tables[99][49] = 327874; + tables[99][50] = 327874; + tables[99][51] = 328216; + tables[99][52] = 328218; + tables[99][53] = 270365; + tables[99][54] = 262203; + tables[99][55] = 262202; + tables[100] = tables[88]; + tables[100][5] = 0; + tables[100][6] = 262313; + tables[100][7] = 327953; + tables[100][9] = 0; + tables[100][10] = 0; + tables[100][14] = 0; + tables[100][15] = 328002; + tables[100][17] = 0; + tables[100][19] = 262311; + tables[100][20] = 0; + tables[100][22] = 262367; + tables[100][25] = 0; + tables[100][26] = 0; + tables[100][27] = 0; + tables[100][47] = 262235; + tables[100][48] = 262237; + tables[100][49] = 262236; + tables[100][50] = 262236; + tables[100][51] = 262203; + tables[100][52] = 262183; + tables[100][53] = 262240; + tables[100][56] = 0; + tables[101] = tables[29]; + tables[101][7] = 327952; + tables[101][15] = 328001; + tables[101][45] = 270355; + tables[101][46] = 262321; + tables[101][49] = 262268; + tables[101][50] = 262268; + tables[101][51] = 262202; + tables[101][52] = 262178; + tables[101][53] = 262270; + tables[101][54] = 262315; + tables[101][55] = 262331; + tables[102] = tables[92]; + tables[102][28] = 327802; + tables[102][29] = 327801; + tables[102][39] = 327926; + tables[102][45] = 327932; + tables[102][46] = 327923; + tables[102][47] = 328017; + tables[102][48] = 327930; + tables[102][49] = 328049; + tables[102][50] = 328049; + tables[102][51] = 327913; + tables[102][52] = 327905; + tables[102][53] = 262192; + tables[102][56] = 262189; + tables[102][100] = 327917; + tables[103] = tables[87]; + tables[103][30] = 262183; + tables[103][31] = 262178; + tables[103][32] = 262187; + tables[103][33] = 262177; + tables[103][34] = 262181; + tables[103][35] = 262191; + tables[103][36] = 262205; + tables[103][37] = 262184; + tables[103][38] = 262185; + tables[103][39] = 327894; + tables[103][45] = 327900; + tables[103][46] = 327891; + tables[103][47] = 328016; + tables[103][48] = 327898; + tables[103][49] = 328048; + tables[103][50] = 328048; + tables[103][51] = 327881; + tables[103][52] = 327873; + tables[103][53] = 262311; + tables[103][100] = 327885; + tables[104] = tables[88]; + tables[104][4] = 262372; + tables[104][8] = 262340; + tables[104][12] = 262349; + tables[104][13] = 262381; + tables[104][16] = 262204; + tables[104][24] = 270508; + tables[104][29] = 262206; + tables[104][54] = 262203; + tables[104][100] = 262204; + tables[105] = tables[36]; + tables[105][22] = 328790; + tables[105][48] = 328791; + tables[105][52] = 328788; + tables[105][53] = 327719; + tables[105][83] = 0; + tables[105][100] = 263313; + tables[106] = tables[37]; + tables[106][22] = 328710; + tables[106][48] = 328711; + tables[106][52] = 328708; + tables[106][53] = 0; + tables[106][100] = 263312; + tables[107][24] = 263313; + tables[108][24] = 263312; + tables[109][40] = 262154; + tables[109][42] = 262152; + tables[109][43] = 262153; + tables[109][88] = 262154; + tables[109][186] = 262152; + tables[109][187] = 262153; + tables[110][31] = 327962; + tables[110][32] = 328032; + tables[110][33] = 327948; + tables[110][34] = 328024; + tables[110][35] = 328061; + tables[110][36] = 327901; + tables[110][37] = 327873; + tables[110][38] = 327885; + tables[110][39] = 327881; + tables[110][47] = 327898; + tables[110][51] = 328046; + return tables; +}(); + +constexpr std::array, 41> CharacterLayouts = {{ + {1, 2, 3, 0}, + {4, 5, 6, 0}, + {7, 8, 0, 0}, + {9, 10, 0, 0}, + {11, 12, 0, 0}, + {13, 14, 0, 0}, + {15, 16, 0, 0}, + {17, 18, 19, 0}, + {20, 21, 22, 0}, + {23, 24, 25, 0}, + {26, 27, 28, 29}, + {30, 31, 32, 0}, + {33, 34, 35, 0}, + {36, 37, 0, 0}, + {38, 39, 40, 41}, + {42, 43, 0, 0}, + {44, 45, 46, 0}, + {47, 48, 49, 0}, + {50, 51, 52, 0}, + {47, 48, 49, 0}, + {53, 54, 0, 55}, + {56, 57, 0, 0}, + {58, 54, 0, 55}, + {59, 60, 3, 0}, + {61, 62, 63, 0}, + {64, 65, 66, 67}, + {68, 69, 70, 71}, + {72, 73, 74, 0}, + {75, 76, 77, 78}, + {79, 80, 81, 0}, + {82, 83, 0, 0}, + {84, 85, 0, 0}, + {86, 87, 88, 0}, + {89, 90, 91, 0}, + {92, 93, 0, 0}, + {94, 95, 96, 97}, + {98, 99, 100, 101}, + {102, 103, 104, 0}, + {105, 106, 107, 108}, + {109, 109, 109, 109}, + {110, 110, 110, 110}, +}}; + +constexpr std::array KoreanInitial = { + 12593, 12594, 12596, 12599, 12600, 12601, 12609, 12610, 12611, 12613, 12614, 12615, 12616, 12617, 12618, 12619, 12620, 12621, 12622 +}; + +constexpr std::array KoreanFinal = { + 12593, 12594, 12595, 12596, 12597, 12598, 12599, 12601, 12602, 12603, 12604, 12605, 12606, 12607, 12608, 12609, 12610, 12612, 12613, 12614, 12615, 12616, 12618, 12619, 12620, 12621, 12622 +}; + + +std::size_t CharacterLayout(std::uint32_t type, std::uint32_t state) { + constexpr std::array layouts = {18, 7, 0, 1, 14, 8, 29, 17, 9, 25, 24, 23, 10, 11, 16, 26, 27, 12, 1, 19, 28, 3, 3, 1, 1, 1, 1, 1, 1, 1, 14, 32, 14, 34, 35, 36, 37, 1, 1}; + const bool imeMode = (state & 0x4000000u) != 0; + const bool alternateMode = (state & 0x10000000u) != 0; + switch (type) { + case 19: return imeMode ? 13 : 1; + case 22: return alternateMode ? 2 : 3; + case 23: + if (!imeMode) return alternateMode ? 2 : 3; + if ((state & 0x8000000u) == 0) return 6; + return alternateMode ? 4 : 5; + case 24: return imeMode ? 15 : 1; + case 26: + if (imeMode) return 20; + return alternateMode ? 21 : 1; + case 29: + case 39: + if (imeMode) return 22; + return alternateMode ? 21 : 1; + case 25: + case 27: + case 28: return alternateMode ? 21 : 1; + case 30: return imeMode ? 30 : 1; + case 31: return imeMode ? 31 : 14; + case 33: return imeMode ? 33 : 14; + case 38: return imeMode ? 38 : 1; + default: return layouts[type - 1]; + } +} + +std::uint32_t CharacterEntry(std::size_t layout, std::size_t layer, std::uint16_t keycode) { + if (keycode >= 256) return 0; + return CharacterTables[CharacterLayouts[layout][layer]][keycode]; +} + +char16_t ConvertCharacter(std::uint16_t keycode, std::uint32_t state, std::uint32_t type) { + if (keycode == 83 || (keycode & 0xFFF8u) == 224) return 0; + const auto layout = CharacterLayout(type, state); + const bool shifted = (state & 0x2200u) != 0; + const bool caps = (state & 0x20000u) != 0; + if (layout == 0 && caps) { + if (keycode == 47) return shifted ? 200 : 220; + if (keycode == 52) return shifted ? 192 : 196; + if (keycode == 51) return shifted ? 201 : 214; + } + const auto& layers = CharacterLayouts[layout]; + if ((state & 0x4000u) != 0 && (layers[2] != 0 || layers[3] != 0)) { + auto entry = CharacterEntry(layout, shifted ? 3 : 2, keycode); + if (entry == 0) return static_cast(CharacterEntry(39, 0, keycode)); + if (caps && (entry & 0x10000u) != 0) { + const auto opposite = CharacterEntry(layout, shifted ? 2 : 3, keycode); + if (opposite != 0) entry = opposite; + } + return static_cast(entry); + } + auto entry = CharacterEntry(layout, shifted ? 1 : 0, keycode); + if (entry == 0) return 0; + if (caps) { + if ((type == 26 || type == 29 || type == 39) && (state & 0x4000000u) != 0) { + entry = CharacterEntry(layout, shifted ? 3 : 0, keycode); + } else if (type == 32 && !shifted && ((keycode >= 31 && keycode <= 39) || keycode == 47 || keycode == 51)) { + entry = CharacterEntry(40, 0, keycode); + } else if ((entry & 0x10000u) != 0) { + entry = CharacterEntry(layout, shifted ? 0 : 1, keycode); + } + if (entry == 0) return 0; + } + if ((entry & 0x20000u) != 0) { + const bool numLock = (state & 0x10000u) != 0; + if (!numLock && keycode >= 89 && keycode <= 99) return 0; + entry = CharacterEntry(layout, numLock ? 1 : 0, keycode); + if (entry == 0) return 0; + } + auto character = static_cast(entry); + if ((state & 0x4000000u) != 0 && type == 24) { + if ((character >= 12592 && character < 12688) || (character >= 44032 && character < 55216) || (character & 0xFF00u) == 0x1100) { + if (character >= 4352 && character <= 4370) return KoreanInitial[character - 4352]; + if (character >= 4449 && character <= 4469) return static_cast(character + 8174); + if (character >= 4520 && character <= 4546) return KoreanFinal[character - 4520]; + return 0; + } + } + if ((state & 0x4000000u) != 0 && type == 30) { + if ((state & 0x80u) == 0) { + if (keycode == 5) character = shifted ? 1570 : 1575; + if (shifted && keycode == 10) character = 1571; + if (shifted && keycode == 23) character = 1573; + } + const auto modifiers = state & 0xFFF8FFFFu; + if (modifiers == 67117312 || modifiers == 67121152 || modifiers == 67109632 || modifiers == 67113472) { + constexpr std::array formatting = {0x200D, 0x200C, 0x200E, 0x200F}; + return keycode >= 30 && keycode <= 33 ? formatting[keycode - 30] : 0; + } + if ((state & 0x1100u) != 0 && shifted) return 0; + } + return character; } } // namespace @@ -27,25 +2400,28 @@ extern "C" { int APS5_VABI sceConvertKeycodeGetImeKeyboardType(int32_t userId, uint32_t* type) { if (type == nullptr) return kErrInvalidAddress; if (userId < 0) return kErrInvalidUserId; - *type = 0; - return 0; + throw std::runtime_error("sceConvertKeycodeGetImeKeyboardType: user keyboard language and variant are unavailable"); } -int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint64_t a0, std::uint64_t a1, std::uint64_t a2, std::uint64_t a3) { - APS5_HIT("CONVERTKEYCODE", "sceConvertKeycodeGetVirtualKeycode args 0x%llx 0x%llx 0x%llx 0x%llx -> error (signature unknown)", - static_cast(a0), static_cast(a1), static_cast(a2), static_cast(a3)); - return kErrInvalidAddress; +int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint16_t keycode, std::uint32_t keyboardType, std::uint16_t* virtualKeycode) { + if (virtualKeycode == nullptr) return kErrInvalidAddress; + if (keyboardType == 0 || keyboardType > VirtualKeys.size()) return kErrInvalidKeyboardType; + if (keyboardType >= 31) throw std::runtime_error("sceConvertKeycodeGetVirtualKeycode: guest sdk version is unavailable"); + auto value = keycode < BaseVirtualKeys.size() ? VirtualKeys[keyboardType - 1][keycode] : 255; + if ((keyboardType == 2 || keyboardType == 3 || keyboardType == 12) && (value == 89 || value == 90)) throw std::runtime_error("sceConvertKeycodeGetVirtualKeycode: guest sdk version is unavailable"); + *virtualKeycode = static_cast(value); + return 0; } -int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(void) { +int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(const KeyboardData* keyData, std::int32_t index, std::uint32_t state, std::uint32_t keyboardType, char16_t* character) { NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceConvertKeycodeGetCharacter(std::uint16_t keycode, std::uint32_t state, std::uint32_t type, char16_t* character) { if (character == nullptr) return kErrInvalidAddress; - if (state != 0) throw std::runtime_error("sceConvertKeycodeGetCharacter: unsupported state " + std::to_string(state)); - if (type != 0) throw std::runtime_error("sceConvertKeycodeGetCharacter: unsupported keyboard type " + std::to_string(type)); - *character = UsCharacter(keycode); + if (type == 0 || type > 39) return kErrInvalidKeyboardType; + if (type >= 31) throw std::runtime_error("sceConvertKeycodeGetCharacter: guest sdk version is unavailable"); + *character = ConvertCharacter(keycode, state, type); return 0; } diff --git a/core/libs/prx/libSceCtrlp/Export.cpp b/core/libs/prx/libSceCtrlp/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceCtrlp/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceCustomMusicAudioOut/Export.cpp b/core/libs/prx/libSceCustomMusicAudioOut/Export.cpp new file mode 100644 index 0000000000..a383508620 --- /dev/null +++ b/core/libs/prx/libSceCustomMusicAudioOut/Export.cpp @@ -0,0 +1,33 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceCustomMusicAudioOutFinalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicAudioOutGetAllMusicVolume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicAudioOutGetMuted() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicAudioOutGetVolume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicAudioOutSetAllMusicVolume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceCustomMusicCore/Export.cpp b/core/libs/prx/libSceCustomMusicCore/Export.cpp new file mode 100644 index 0000000000..fa0c494e42 --- /dev/null +++ b/core/libs/prx/libSceCustomMusicCore/Export.cpp @@ -0,0 +1,98 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceCustomMusicCoreBgmClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmOpen2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmOutput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmSetAudioVolume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreBgmStop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreGetBgmAuthorityStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreGetHdcpStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreGetSystemAudioVolume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCorePostEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreSendEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreSendMulticastEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreSendSpMessage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreSetSystemAudioVolume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreStartOperation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreStartOperation2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCustomMusicCoreStopOperation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceCustomMusicSysCallWrapper/Export.cpp b/core/libs/prx/libSceCustomMusicSysCallWrapper/Export.cpp new file mode 100644 index 0000000000..ee9009496a --- /dev/null +++ b/core/libs/prx/libSceCustomMusicSysCallWrapper/Export.cpp @@ -0,0 +1,13 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceCustomMusicSelect() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceDataTransfer/Export.cpp b/core/libs/prx/libSceDataTransfer/Export.cpp new file mode 100644 index 0000000000..43fb2a1574 --- /dev/null +++ b/core/libs/prx/libSceDataTransfer/Export.cpp @@ -0,0 +1,308 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceDataTransferAbortFgTransfer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferAbortSearchPS4() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferCheckBgTransferRunning() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferGetFgTransferProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferGetPrepareFgTransferProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferRequestGetAppInfoPS4() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferRequestGetSavedataInfoPS4() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferRequestGetUsersPS4() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferRequestLaunchPS4() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferRequestPrepareFgTransfer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferRequestRebootAuthPS4() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferRequestSearchPS4() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferRequestStartFgTransfer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferRequestTransferTimePS4() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetAbortBindSavedata() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetAbortDeactivate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetAbortEth0() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetAbortGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetAbortGetDeviceInfoApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetAbortGetTitles() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetAbortGetUsers() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetAbortLaunch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetAbortPrepareTransfer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetAbortPwrReq() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetAbortReboot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetAbortSendSsoNew2Old() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetAbortSendSsoOld2New() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetAbortTransfer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetAbortTransferSpeed() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetEventIsAuthReady() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetEventIsIPv6Ready() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetEventIsPwrReqReady() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetGetFailedUsers() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetGetRebootData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetGetTransferProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestAbortSearch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestActivate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestAuth() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestBindSavedata() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestComplete() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestCreateRebootData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestDeactivate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestEndTransfer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestGetDeviceInfoApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestGetTitles() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestGetUsers() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestLaunch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestPrepareTransfer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestPrepareTransferProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestPwrReq() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestReboot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestSearch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestSendSsoNew2Old() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestSendSsoOld2New() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestStartTransfer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTargetRequestTransferSpeed() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDataTransferTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceDbg/Export.cpp b/core/libs/prx/libSceDbg/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceDbg/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceDbgAssist/Export.cpp b/core/libs/prx/libSceDbgAssist/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceDbgAssist/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceDeci5/Export.cpp b/core/libs/prx/libSceDeci5/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceDeci5/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceDeci5Appmp/Export.cpp b/core/libs/prx/libSceDeci5Appmp/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceDeci5Appmp/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceDeci5CpComm/Export.cpp b/core/libs/prx/libSceDeci5CpComm/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceDeci5CpComm/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceDeci5Ctrlp/Export.cpp b/core/libs/prx/libSceDeci5Ctrlp/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceDeci5Ctrlp/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceDeci5Dbgp/Export.cpp b/core/libs/prx/libSceDeci5Dbgp/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceDeci5Dbgp/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceDeci5Drfp/Export.cpp b/core/libs/prx/libSceDeci5Drfp/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceDeci5Drfp/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceDeci5Eventp/Export.cpp b/core/libs/prx/libSceDeci5Eventp/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceDeci5Eventp/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceDeci5Fsmp/Export.cpp b/core/libs/prx/libSceDeci5Fsmp/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceDeci5Fsmp/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceDeci5Pgcmp/Export.cpp b/core/libs/prx/libSceDeci5Pgcmp/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceDeci5Pgcmp/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceDeci5Rdrfp/Export.cpp b/core/libs/prx/libSceDeci5Rdrfp/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceDeci5Rdrfp/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceDeci5Tsmp/Export.cpp b/core/libs/prx/libSceDeci5Tsmp/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceDeci5Tsmp/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceDeci5Ttyp/Export.cpp b/core/libs/prx/libSceDeci5Ttyp/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceDeci5Ttyp/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceDeci5UlpCommon/Export.cpp b/core/libs/prx/libSceDeci5UlpCommon/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceDeci5UlpCommon/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceDepth/Export.cpp b/core/libs/prx/libSceDepth/Export.cpp new file mode 100644 index 0000000000..edfa2e580e --- /dev/null +++ b/core/libs/prx/libSceDepth/Export.cpp @@ -0,0 +1,202 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN +int APS5_VABI sceDepthCalculateRectificationInformation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthDisableHeteroMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthEnableExtendedMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthEnableHeteroMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthEnableHeteroModeAndGenerateInformation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthGetCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthGetImage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthGetRectificationInformation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthGetSrParameter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthHandCandidateTrackerGetResult(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthHasCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthHeadCandidateDetectorGetResults(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthHeadCandidateTrackerGetResult(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthHeadCandidateTrackerSetValidationInformation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthLoadCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthQueryMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSaveInternalBuffers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetCommands(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetPostFilterParameter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetPreFilterParameter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetRectificationInformation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetRoi(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetRoiset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetSrParameter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetSrTnrParameter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSubmit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthUpdate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthValidateCurrentCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthWaitAndExecutePostProcess(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} +extern "C" { + +int APS5_VABI _sceDepthCalcHeteroAdjustParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceDepthHeadCandidateTrackerGetInformation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceDepthHeadCandidateTrackerSetInformation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceDepthSetCameraFactoryCalibrationData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceDepthSetHeteroAdjustParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceDepthSetHeteroMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceDepth2/Export.cpp b/core/libs/prx/libSceDepth2/Export.cpp new file mode 100644 index 0000000000..d043a5728d --- /dev/null +++ b/core/libs/prx/libSceDepth2/Export.cpp @@ -0,0 +1,58 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceDepth2GetImage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2Initialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2QueryMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2SaveInternalBuffers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2SetCameraFactoryCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2SetCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2SetRoi(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2Submit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2Terminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2WaitAndExecutePostProcess(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceDipsw/Export.cpp b/core/libs/prx/libSceDipsw/Export.cpp new file mode 100644 index 0000000000..3cddf792b2 --- /dev/null +++ b/core/libs/prx/libSceDipsw/Export.cpp @@ -0,0 +1,53 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceKernelCheckDipsw() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelInitializeDipsw() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsAssistMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsDevelopmentMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsDisableRazor() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelReadDipswData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetDipsw() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelUnsetDipsw() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteDipswData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceDiscId/Export.cpp b/core/libs/prx/libSceDiscId/Export.cpp new file mode 100644 index 0000000000..ef0373a67b --- /dev/null +++ b/core/libs/prx/libSceDiscId/Export.cpp @@ -0,0 +1,21 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +struct DiscId; +struct DiscId2; + +extern "C" { + +int APS5_VABI sceDiscIdGet(DiscId* discId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDiscIdGet2(DiscId2* discId, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceDiscMap/CMakeLists.txt b/core/libs/prx/libSceDiscMap/CMakeLists.txt new file mode 100644 index 0000000000..fa84f65ac2 --- /dev/null +++ b/core/libs/prx/libSceDiscMap/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceDiscMap SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceDiscMap PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceDiscMap) + +if(BUILD_TESTING) + add_test_executable(guest_disc_map_tests ${LIBS_INCLUDE_DIR}/tests/GuestDiscMap.cpp) + target_include_directories(guest_disc_map_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_disc_map_tests PRIVATE libSceDiscMap libc) + configure_windows_unwind(guest_disc_map_tests) + add_test(NAME guest_disc_map COMMAND guest_disc_map_tests) + if(WIN32) + set_tests_properties(guest_disc_map PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceDiscMap/Export.cpp b/core/libs/prx/libSceDiscMap/Export.cpp new file mode 100644 index 0000000000..8fedde6365 --- /dev/null +++ b/core/libs/prx/libSceDiscMap/Export.cpp @@ -0,0 +1,37 @@ +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr std::int32_t SCE_DISC_MAP_ERROR_NO_BITMAP_INFO = static_cast(0x81100004); + +} + +extern "C" { + +std::int32_t APS5_VABI sceDiscMapIsRequestOnHDD(const char* path, std::int64_t offset, std::int64_t size, std::int32_t* result) { + (void)path; + (void)offset; + (void)size; + (void)result; + return SCE_DISC_MAP_ERROR_NO_BITMAP_INFO; +} + +APS5_EXPORT("ioKMruft1ek", sceDiscMapUnknown_ioKMruft1ek); +std::int32_t APS5_VABI sceDiscMapUnknown_ioKMruft1ek(const char* path, std::int64_t offset, std::int64_t size, std::uint64_t* flags, std::uint64_t* out1, std::uint64_t* out2) { + (void)path; + (void)offset; + (void)size; + (void)flags; + (void)out1; + (void)out2; + return SCE_DISC_MAP_ERROR_NO_BITMAP_INFO; +} + +int APS5_VABI sceDiscMapGetPackageSize(std::int64_t fflags, int* ret1, int* ret2) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceDolbyVision/Export.cpp b/core/libs/prx/libSceDolbyVision/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceDolbyVision/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceDseehx/Export.cpp b/core/libs/prx/libSceDseehx/Export.cpp new file mode 100644 index 0000000000..43fce06ef6 --- /dev/null +++ b/core/libs/prx/libSceDseehx/Export.cpp @@ -0,0 +1,28 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceDseehxConvert() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDseehxExtInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDseehxFlush() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDseehxInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceDtcpIp/Export.cpp b/core/libs/prx/libSceDtcpIp/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceDtcpIp/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceDtsEnc/Export.cpp b/core/libs/prx/libSceDtsEnc/Export.cpp new file mode 100644 index 0000000000..06a9bc9f32 --- /dev/null +++ b/core/libs/prx/libSceDtsEnc/Export.cpp @@ -0,0 +1,43 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceDtsEncCreateEncoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDtsEncDeleteEncoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDtsEncEncode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDtsEncEncodeAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDtsEncFlush() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDtsEncIsEncoding() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDtsEncWaitEncode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceEditMp4/Export.cpp b/core/libs/prx/libSceEditMp4/Export.cpp new file mode 100644 index 0000000000..9bafeb8142 --- /dev/null +++ b/core/libs/prx/libSceEditMp4/Export.cpp @@ -0,0 +1,53 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceEditMp4Abort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEditMp4Edit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEditMp4GetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEditMp4Initialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEditMp4InitializeParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEditMp4QueryMemSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEditMp4SetFinishCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEditMp4Terminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEditMp4Unite() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceEmbeddedTts/Export.cpp b/core/libs/prx/libSceEmbeddedTts/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceEmbeddedTts/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceEmbeddedTtsCoreG3/Export.cpp b/core/libs/prx/libSceEmbeddedTtsCoreG3/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceEmbeddedTtsCoreG3/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceErrorDialog/Export.cpp b/core/libs/prx/libSceErrorDialog/Export.cpp index 17d76ba8e4..d3c922f2b1 100644 --- a/core/libs/prx/libSceErrorDialog/Export.cpp +++ b/core/libs/prx/libSceErrorDialog/Export.cpp @@ -21,6 +21,7 @@ constexpr int kErrOk = 0; constexpr int kErrNotInitialized = static_cast(0x80ED0001u); constexpr int kErrAlreadyInitialized = static_cast(0x80ED0002u); constexpr int kErrParam = static_cast(0x80ED0003u); +constexpr int kErrUnexpectedFatal = static_cast(0x80ED0004u); constexpr int kErrInvalidState = static_cast(0x80ED0005u); constexpr std::size_t kParamSize = 16; @@ -92,4 +93,12 @@ int APS5_VABI sceErrorDialogTerminate(void) { return kErrOk; } +int APS5_VABI sceErrorDialogOpenDetail() { + return kErrUnexpectedFatal; +} + +int APS5_VABI sceErrorDialogOpenWithReport() { + return kErrUnexpectedFatal; +} + } diff --git a/core/libs/prx/libSceEsvm/Export.cpp b/core/libs/prx/libSceEsvm/Export.cpp new file mode 100644 index 0000000000..54def11467 --- /dev/null +++ b/core/libs/prx/libSceEsvm/Export.cpp @@ -0,0 +1,28 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} +extern "C" { + +int APS5_VABI sceEsvmAppAudioSyncCreateContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmAppAudioSyncFlush() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmSyncVideo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceEsvmEngine/Export.cpp b/core/libs/prx/libSceEsvmEngine/Export.cpp new file mode 100644 index 0000000000..6667c6812b --- /dev/null +++ b/core/libs/prx/libSceEsvmEngine/Export.cpp @@ -0,0 +1,103 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceEsvmEngineDestroyMediaKeySystemAccess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineMediaKeySessionClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineMediaKeySessionGenerateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineMediaKeySessionGetExpiration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineMediaKeySessionGetKeyStatuses() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineMediaKeySessionGetSessionId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineMediaKeySessionLoad() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineMediaKeySessionRemove() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineMediaKeySessionSetEventHandler() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineMediaKeySessionUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineMediaKeySystemAccessCreateMediaKeys() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineMediaKeySystemAccessDestroyMediaKeys() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineMediaKeySystemAccessGetConfiguration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineMediaKeySystemAccessSetCdmStorageConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineMediaKeysCreateSession() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineMediaKeysDestroySession() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineMediaKeysSetEventHandler() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineMediaKeysSetServerCertificate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmEngineRequestMediaKeySystemAccess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceFiber/CMakeLists.txt b/core/libs/prx/libSceFiber/CMakeLists.txt index d2ec7534ef..883b7fc154 100644 --- a/core/libs/prx/libSceFiber/CMakeLists.txt +++ b/core/libs/prx/libSceFiber/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceFiber SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceFiber PRIVATE ${LIBS_INCLUDE_DIR}) +target_compile_options(libSceFiber PRIVATE -fno-omit-frame-pointer) configure_windows_unwind(libSceFiber) if(BUILD_TESTING) @@ -7,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 a727830404..54b0930521 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -35,8 +35,9 @@ 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); +using GuestFiberEntry = FiberEntry; // A fiber that is switching away stays Suspending until its context is saved; whoever runs next // on that host thread publishes Suspended. Fibers migrate between threads, so a resumer must never @@ -166,6 +167,9 @@ Aps5FiberSwitchStack_nid_no_patch: Aps5FiberTrampoline_nid_no_patch: mov %r12, %rcx and $-16, %rsp + push $0 + push $0 + mov %rsp, %rbp sub $32, %rsp call Aps5FiberMain_nid_no_patch ud2 @@ -235,6 +239,9 @@ asm(".text\n" APS5_ASM_FUNCTION("Aps5FiberTrampoline_nid_no_patch") R"( mov %r12, %rdi and $-16, %rsp + push $0 + push $0 + mov %rsp, %rbp call )" APS5_ASM_SYMBOL("Aps5FiberMain_nid_no_patch") R"( ud2 )" APS5_ASM_FUNCTION_END("Aps5FiberTrampoline_nid_no_patch")); @@ -297,7 +304,11 @@ static void ReleaseHostStack(Fiber* fiber) { static void AllocateHostStack(Fiber* fiber) { const auto page = static_cast(sysconf(_SC_PAGESIZE)); const auto bytes = (fiber->contextSize + FIBER_HOST_STACK_MARGIN + page - 1) / page * page + page; - void* mapping = mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_STACK, -1, 0); + int flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE; +#ifdef MAP_STACK + flags |= MAP_STACK; +#endif + void* mapping = mmap(nullptr, bytes, PROT_READ | PROT_WRITE, flags, -1, 0); if (mapping == MAP_FAILED) throw std::runtime_error(std::string("sceFiberInitialize: no host stack of ") + std::to_string(bytes) + " bytes for fiber '" + fiber->name + "' (errno " + std::to_string(errno) + ")"); if (mprotect(mapping, page, PROT_NONE) != 0) { const int error = errno; @@ -327,6 +338,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}; } @@ -417,7 +429,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); @@ -503,7 +515,7 @@ int32_t APS5_VABI sceFiberGetInfo(FiberObject* object, FiberInfo* fiber_info) { auto* fiber = AsFiber(object); if (!fiber || !fiber_info) return object && fiber_info ? SCE_FIBER_ERROR_INVALID : SCE_FIBER_ERROR_NULL; if (fiber_info->size != sizeof(FiberInfo)) return SCE_FIBER_ERROR_INVALID; - fiber_info->entry = reinterpret_cast(fiber->entry); + fiber_info->entry = fiber->entry; fiber_info->arg_on_initialize = fiber->argOnInitialize; fiber_info->addr_context = SizedWindow(fiber); fiber_info->size_context = fiber->contextSize; @@ -553,5 +565,24 @@ int32_t APS5_VABI sceFiberStopContextSizeCheck(void) { return g_contextSizeCheck.compare_exchange_strong(expected, false) ? SCE_OK : SCE_FIBER_ERROR_STATE; } +int APS5_VABI _sceFiberAttachContextAndRun(FiberObject* fiber, void* addr_context, uint64_t size_context, + uint64_t arg_on_run, uint64_t* arg_on_return) { + NotImplemented_nid_no_patch(__func__); + return 0; } +int APS5_VABI _sceFiberAttachContextAndSwitch(FiberObject* fiber, void* addr_context, uint64_t size_context, + uint64_t arg_on_run, uint64_t* arg_on_run_out) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceFiberInitializeWithInternalOptionImpl(FiberObject* fiber, const char* name, GuestFiberEntry entry, + uint64_t arg_on_initialize, void* addr_context, + uint64_t size_context, const FiberOptParam* opt_param, + uint32_t flags, uint32_t build_version) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceFios2/Export.cpp b/core/libs/prx/libSceFios2/Export.cpp new file mode 100644 index 0000000000..eeedc28190 --- /dev/null +++ b/core/libs/prx/libSceFios2/Export.cpp @@ -0,0 +1,848 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceFiosArchiveGetDecompressorThreadCount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosArchiveGetMountBufferSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosArchiveGetMountBufferSizeSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosArchiveMount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosArchiveMountSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosArchiveMountWithOrder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosArchiveMountWithOrderSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosArchiveSetDecompressorThreadCount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosArchiveUnmount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosArchiveUnmountSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosCacheContainsFileRangeSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosCacheContainsFileSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosCacheFlushFileRangeSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosCacheFlushFileSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosCacheFlushSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosCachePrefetchFH() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosCachePrefetchFHRange() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosCachePrefetchFHRangeSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosCachePrefetchFHSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosCachePrefetchFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosCachePrefetchFileRange() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosCachePrefetchFileRangeSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosCachePrefetchFileSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosCancelAllOps() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosClearTimeStamps() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosCloseAllFiles() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDHClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDHCloseSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDHGetPath() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDHOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDHOpenSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDHRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDHReadSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDLLInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDLLTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDateFromComponents() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDateGetCurrent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDateToComponents() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDeallocatePassthruFH() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDebugDumpDH() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDebugDumpDate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDebugDumpError() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDebugDumpFH() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDebugDumpOp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDelete() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDeleteSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDirectoryCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDirectoryCreateSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDirectoryCreateWithMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDirectoryCreateWithModeSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDirectoryDelete() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDirectoryDeleteSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDirectoryExists() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosDirectoryExistsSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosExists() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosExistsSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHCloseSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHGetOpenParams() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHGetPath() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHGetSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHOpenSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHOpenWithMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHOpenWithModeSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHPread() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHPreadSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHPreadv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHPreadvSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHPwrite() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHPwriteSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHPwritev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHPwritevSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHReadSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHReadv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHReadvSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHSeek() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHStat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHStatSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHSyncSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHTell() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHToFileno() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHTruncate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHTruncateSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHWrite() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHWriteSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHWritev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFHWritevSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFileDelete() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFileDeleteSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFileExists() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFileExistsSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFileGetSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFileGetSizeSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFileRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFileReadSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFileTruncate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFileTruncateSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFileWrite() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFileWriteSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosFilenoToFH() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosGetAllDHs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosGetAllFHs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosGetAllOps() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosGetDefaultOpAttr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosGetGlobalDefaultOpAttr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosGetSuspendCount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosGetThreadDefaultOpAttr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosIOFilterAdd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosIOFilterCache() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosIOFilterGetInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosIOFilterPsarcDearchiver() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosIOFilterRemove() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosIsIdle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosIsInitialized() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosIsSuspended() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosIsValidHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpDelete() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpGetActualCount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpGetAttr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpGetBuffer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpGetError() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpGetOffset() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpGetPath() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpGetRequestCount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpIsCancelled() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpIsDone() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpReschedule() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpRescheduleWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpSetBuffer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpSyncWait() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpSyncWaitForIO() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpWait() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOpWaitUntil() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOverlayAdd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOverlayGetInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOverlayGetList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOverlayModify() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOverlayRemove() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosOverlayResolveSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosPathNormalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosPathcmp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosPathncmp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosPrintTimeStamps() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosPrintf() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosRename() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosRenameSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosResolve() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosResolveSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosResume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosSaveTimeStamp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosSetGlobalDefaultOpAttr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosSetThreadDefaultOpAttr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosShutdownAndCancelOps() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosStat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosStatSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosTimeGetCurrent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosTimeIntervalFromNanoseconds() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosTimeIntervalToNanoseconds() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosTraceTimestamp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosUpdateParameters() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFiosVprintf() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceFont/CMakeLists.txt b/core/libs/prx/libSceFont/CMakeLists.txt index 458e3e5fd7..cec16f879b 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}) @@ -24,4 +25,17 @@ if(BUILD_TESTING) set_tests_properties(guest_font_writing_line PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(font_face_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/FontFaceLifetime.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/State.cpp) + target_include_directories(font_face_lifetime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(font_face_lifetime_tests PRIVATE freetype libc) + configure_windows_unwind(font_face_lifetime_tests) + add_test(NAME font_face_lifetime COMMAND font_face_lifetime_tests) + add_test(NAME font_face_lifetime_replace COMMAND font_face_lifetime_tests replace) + add_test(NAME font_face_lifetime_churn COMMAND font_face_lifetime_tests churn) + set_tests_properties(font_face_lifetime font_face_lifetime_replace font_face_lifetime_churn PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(font_face_lifetime font_face_lifetime_replace font_face_lifetime_churn PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceFont/include/FontDriver.hpp b/core/libs/prx/libSceFont/include/FontDriver.hpp index 889c367dda..35b8d3d533 100644 --- a/core/libs/prx/libSceFont/include/FontDriver.hpp +++ b/core/libs/prx/libSceFont/include/FontDriver.hpp @@ -202,7 +202,7 @@ using DriverTermFunction = int (APS5_VABI*)(FontLibNative* library); using DriverSupportFunction = int (APS5_VABI*)(FontLibNative* library, std::uint32_t formats); using DriverOpenFunction = int (APS5_VABI*)(FontLibNative* library, std::uint32_t mode, const void* fontAddress, std::uint32_t fontSize, std::uint32_t subFontIndex, std::uint32_t uniqueWord, FontObj** inoutFontObj); using DriverCloseFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t flags); -using DriverScaleFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint16_t* outUnitsPerEm, float* outScale); +using DriverScaleFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t* outUnitsPerEm, float* outScale); using DriverMetricFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t metricId, std::uint16_t* outMetric); using DriverGlyphsCountFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t* outCount); using DriverGlyphIndexFunction = int (APS5_VABI*)(FontObj* fontObj, std::uint32_t codepoint, std::uint32_t* outGlyphIndex); diff --git a/core/libs/prx/libSceFont/include/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp index b2a742c291..4e057a93e1 100644 --- a/core/libs/prx/libSceFont/include/FontInternal.hpp +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -24,13 +24,20 @@ constexpr std::uint8_t STYLE_FRAME_FLAG_SLANT = 0x02; constexpr std::uint8_t STYLE_FRAME_FLAG_WEIGHT = 0x04; constexpr float POINTS_PER_INCH = 72.0f; +struct FallbackFace { + std::shared_ptr> data; + FT_Face face = nullptr; + std::uint32_t subFontIndex = 0; +}; + struct FontState { std::shared_ptr> faceData; FT_Face face = nullptr; + std::vector fallbacks; float scaleW = 16.0f; float scaleH = 16.0f; std::map scriptLanguages; - std::map typographicFeatures; + int attribute = 0; FontState() = default; FontState(const FontState&) = delete; @@ -52,6 +59,7 @@ struct GeneratedGlyph { std::vector outlineContours; bool metricsInitialized = false; bool outlineInitialized = false; + int attribute = 0; }; struct SystemFontFile { @@ -88,11 +96,15 @@ FontState* TryGetState(FontHandle handle); FontState& ResetState(FontHandle handle); void RemoveState(FontHandle handle); void LoadStateFace(FontState& state, std::shared_ptr> bytes, std::uint32_t subFontIndex); +void AddStateFallbackFace(FontState& state, std::shared_ptr> bytes, std::uint32_t subFontIndex); +FT_Face FallbackFaceForCode(const FontState& state, std::uint32_t code, FT_UInt* glyphIndex); +FT_Face FaceForCode(const FontState& state, std::uint32_t code, FT_UInt* glyphIndex); bool IsSystemFontSet(std::uint32_t fontSetType); std::vector SystemFontCandidates(std::uint32_t fontSetType); std::filesystem::path SystemFontDirectory(); std::optional FindSystemFontFile(std::uint32_t fontSetType); +std::vector FindSystemFontFallbackFiles(std::uint32_t fontSetType); void TrackGeneratedGlyph(FontGlyph glyph); bool ForgetGeneratedGlyph(FontGlyph glyph); diff --git a/core/libs/prx/libSceFont/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/Layout.cpp b/core/libs/prx/libSceFont/src/Layout.cpp index 8a067968b8..a59a3b5f2f 100644 --- a/core/libs/prx/libSceFont/src/Layout.cpp +++ b/core/libs/prx/libSceFont/src/Layout.cpp @@ -404,10 +404,10 @@ int Font::GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMe float scaleH = 0.0f; const int scaleRc = StyleStateGetScalePixel(style, &scaleW, &scaleH); if (scaleRc != SCE_FONT_OK) return finish(scaleRc); - const FT_Face face = state->face; - if (!face) return finish(SCE_FONT_ERROR_NO_SUPPORT_FUNCTION); - const FT_UInt glyphIndex = ResolveGlyphIndexWithFallback(face, code); - if (glyphIndex == 0) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + if (!state->face) return finish(SCE_FONT_ERROR_NO_SUPPORT_FUNCTION); + FT_UInt glyphIndex = 0; + const FT_Face face = FaceForCode(*state, code, &glyphIndex); + if (!face) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); const auto charW = static_cast(static_cast(scaleW * 64.0f)); const auto charH = static_cast(static_cast(scaleH * 64.0f)); FT_F26Dot6 usedW = charW; @@ -475,19 +475,23 @@ int Font::RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRe obj->shift_units_y = 0; obj->layout_seed_pair = 0; StyleStateGetScalePixel(style, &obj->scale_x_0x50, &obj->scale_y_0x54); - float outScaleX = 0.0f; - float outScaleY = 0.0f; - int rc; - if (style->scale_unit == 0) { - rc = driver->set_char_default_dpi(obj, style->scale_w, style->scale_h, &outScaleX, &outScaleY); - } else { - rc = driver->set_char_with_dpi(obj, style->dpi_x ? style->dpi_x : 0x48, style->dpi_y ? style->dpi_y : 0x48, style->scale_w, style->scale_h, &outScaleX, &outScaleY); - } - if (rc != SCE_FONT_OK) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + const auto setCharSize = [&](FontObj* target) { + float outScaleX = 0.0f; + float outScaleY = 0.0f; + if (style->scale_unit == 0) return driver->set_char_default_dpi(target, style->scale_w, style->scale_h, &outScaleX, &outScaleY); + return driver->set_char_with_dpi(target, style->dpi_x ? style->dpi_x : 0x48, style->dpi_y ? style->dpi_y : 0x48, style->scale_w, style->scale_h, &outScaleX, &outScaleY); + }; + if (setCharSize(obj) != SCE_FONT_OK) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); std::uint32_t glyphIndex = 0; - rc = driver->glyph_index(obj, code, &glyphIndex); - if (rc != SCE_FONT_OK || glyphIndex == 0) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); - return finish(RenderGlyphIndexToSurface(*obj, glyphIndex, surface, x, y, metrics, result)); + if (driver->glyph_index(obj, code, &glyphIndex) == SCE_FONT_OK && glyphIndex != 0) return finish(RenderGlyphIndexToSurface(*obj, glyphIndex, surface, x, y, metrics, result)); + const FontState* state = TryGetState(handle); + FT_UInt fallbackIndex = 0; + const FT_Face fallbackFace = state ? FallbackFaceForCode(*state, code, &fallbackIndex) : nullptr; + if (!fallbackFace) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + FontObj fallback = *obj; + fallback.ft_face = fallbackFace; + if (setCharSize(&fallback) != SCE_FONT_OK) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + return finish(RenderGlyphIndexToSurface(fallback, fallbackIndex, surface, x, y, metrics, result)); } bool Font::BuildTrueOutline(GeneratedGlyph& glyph) { @@ -495,9 +499,9 @@ bool Font::BuildTrueOutline(GeneratedGlyph& glyph) { if (!state || !state->face) return false; const auto* font = GetNativeFont(glyph.owner); if (font->magic != HANDLE_MAGIC) return false; - const FT_Face face = state->face; - const FT_UInt glyphIndex = FT_Get_Char_Index(face, glyph.codepoint); - if (glyphIndex == 0) return false; + FT_UInt glyphIndex = 0; + const FT_Face face = FaceForCode(*state, glyph.codepoint, &glyphIndex); + if (!face) return false; const auto charW = static_cast(static_cast(glyph.glyph.scale_x * 64.0f)); const auto charH = static_cast(static_cast(glyph.glyph.base_scale * 64.0f)); if (FT_Set_Char_Size(face, charW, charH, 72, 72) != 0) return false; diff --git a/core/libs/prx/libSceFont/src/Library.cpp b/core/libs/prx/libSceFont/src/Library.cpp index e872a3fa66..4d8adbac7e 100644 --- a/core/libs/prx/libSceFont/src/Library.cpp +++ b/core/libs/prx/libSceFont/src/Library.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "prx/libSceFont/include/FontInternal.hpp" @@ -298,10 +299,19 @@ int APS5_VABI sceFontRendererSetOutlineBufferPolicy(FontRenderer fontRenderer, s } int APS5_VABI sceFontGetPixelResolution(FontLibrary library, std::uint32_t* subPixelCount) { + auto* lib = static_cast(library); + std::uint32_t previous = 0; + if (!lib || !AcquireLibraryLock(lib, previous)) { + if (subPixelCount) *subPixelCount = 0; + return SCE_FONT_ERROR_INVALID_LIBRARY; + } + const auto unlock = [previous](FontLibNative* locked) { ReleaseLibraryLock(locked, previous); }; + const std::unique_ptr lock(lib, unlock); + if (!lib->sys_driver || !lib->sys_driver->pixel_resolution) { + if (subPixelCount) *subPixelCount = 0; + return SCE_FONT_ERROR_INVALID_LIBRARY; + } if (!subPixelCount) return SCE_FONT_ERROR_INVALID_PARAMETER; - *subPixelCount = 0; - const auto* lib = static_cast(library); - if (!lib || lib->magic != LIBRARY_MAGIC || !lib->sys_driver || !lib->sys_driver->pixel_resolution) return SCE_FONT_ERROR_INVALID_LIBRARY; *subPixelCount = lib->sys_driver->pixel_resolution(); return SCE_FONT_OK; } @@ -368,26 +378,37 @@ int APS5_VABI sceFontClearDeviceCache(FontLibrary library) { } int APS5_VABI sceFontDettachDeviceCacheBuffer(FontLibrary library, void** buffer, std::uint32_t* size) { + const auto clearOutputs = [&] { + if (buffer) *buffer = nullptr; + if (size) *size = 0; + }; auto* lib = static_cast(library); - if (!lib || lib->magic != LIBRARY_MAGIC) return SCE_FONT_ERROR_INVALID_LIBRARY; std::uint32_t previous = 0; - if (!AcquireLibraryLock(lib, previous)) return SCE_FONT_ERROR_INVALID_LIBRARY; + if (!lib || lib->magic != LIBRARY_MAGIC || !AcquireLibraryLock(lib, previous)) { + clearOutputs(); + return SCE_FONT_ERROR_INVALID_LIBRARY; + } std::uint32_t* current = AcquireDeviceCache(lib); - if (!current) { - ReleaseDeviceCache(lib, nullptr); - ReleaseLibraryLock(lib, previous); - return SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER; - } - if (size) *size = current[0]; - if (buffer) { - *buffer = current; - } else if ((lib->flags & 1u) != 0) { - lib->iface->dealloc(lib->alloc_ctx, current); - } - lib->flags &= ~1u; ReleaseDeviceCache(lib, nullptr); + int result = SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER; + if (current) { + const std::uint32_t capacity = current[0]; + std::memset(current + 1, 0, 5 * sizeof(std::uint32_t)); + if ((lib->flags & 1u) == 0) { + if (buffer) *buffer = current; + if (size) *size = capacity; + ReleaseLibraryLock(lib, previous); + return SCE_FONT_OK; + } + result = SCE_FONT_ERROR_FATAL; + if (lib->iface && lib->iface->dealloc) { + lib->iface->dealloc(lib->alloc_ctx, current); + result = SCE_FONT_OK; + } + } + clearOutputs(); ReleaseLibraryLock(lib, previous); - return SCE_FONT_OK; + return result; } int APS5_VABI sceFontSupportExternalFonts(FontLibrary library, std::uint32_t fontMax, std::uint32_t formats) { diff --git a/core/libs/prx/libSceFont/src/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp index ef118bd83e..36e7e2263d 100644 --- a/core/libs/prx/libSceFont/src/Open.cpp +++ b/core/libs/prx/libSceFont/src/Open.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include #include #include @@ -81,7 +82,8 @@ void InitializeOpenedHandle(FontHandleNative* font, FontLibNative* library, Font driver->metric(obj, 0xEA00, metric); font->metricB = metric[0]; float scale = 0.0f; - driver->scale(obj, metric, &scale); + std::uint32_t resolution = 0; + driver->scale(obj, &resolution, &scale); font->style = {}; font->style.dpi_x = 0x48; font->style.dpi_y = 0x48; @@ -227,6 +229,13 @@ std::shared_ptr> SystemFontBytes(const std::fil return bytes; } +void WarnMissingSystemFont(std::uint32_t fontSetType) { + static std::once_flag warned; + std::call_once(warned, [&] { + std::fprintf(stderr, "[libSceFont] no file for system font set 0x%08X in %s. Titles that lay out text in system fonts can fail far from here without them: put the console's fonts or the Noto substitutes from docs/user/USAGE.md there, or set ANYPS5_SYSTEM_FONTS to their directory.\n", fontSetType, SystemFontDirectory().string().c_str()); + }); +} + void CopyStyleToCache(FontHandleNative* font) { std::memcpy(&font->cached_style.state, &font->style, sizeof(StyleStateBlock)); } @@ -392,6 +401,7 @@ int APS5_VABI sceFontOpenFontInstance(FontHandle fontHandle, FontHandle setupFon targetState.scaleW = sourceState->scaleW; targetState.scaleH = sourceState->scaleH; if (sourceState->faceData && !sourceState->faceData->empty()) LoadStateFace(targetState, sourceState->faceData, subFontIndex); + for (const FallbackFace& fallback : sourceState->fallbacks) AddStateFallbackFace(targetState, fallback.data, fallback.subFontIndex); } if (pFontHandle) *pFontHandle = target; return SCE_FONT_OK; @@ -407,6 +417,7 @@ int APS5_VABI sceFontOpenFontSet(FontLibrary library, std::uint32_t fontSetType, if (!pFontHandle) return SCE_FONT_ERROR_INVALID_PARAMETER; const std::optional file = FindSystemFontFile(fontSetType); APS5_LOG_OUT("fontSetType=0x%08X file=%s", fontSetType, file ? file->path.string().c_str() : "(none)"); + if (!file && IsSystemFontSet(fontSetType)) WarnMissingSystemFont(fontSetType); std::uint32_t libraryLock = 0; if (!AcquireLibraryLock(lib, libraryLock)) { *pFontHandle = nullptr; @@ -432,8 +443,13 @@ int APS5_VABI sceFontOpenFontSet(FontLibrary library, std::uint32_t fontSetType, if (openRc == SCE_FONT_ERROR_FS_OPEN_FAILED || openRc == SCE_FONT_ERROR_NO_SUPPORT_FORMAT) return SCE_FONT_ERROR_FONT_OPEN_FAILED; if (openRc != SCE_FONT_OK) return openRc; GetNativeFont(*pFontHandle)->open_info.fontset_flags = fontSetType; + const std::vector fallbackFiles = FindSystemFontFallbackFiles(fontSetType); + for (const SystemFontFile& fallback : fallbackFiles) APS5_LOG_OUT("fontSetType=0x%08X fallback=%s", fontSetType, fallback.path.string().c_str()); FontState& state = ResetState(*pFontHandle); if (auto bytes = SystemFontBytes(file->path)) LoadStateFace(state, std::move(bytes), file->subFontIndex); + for (const SystemFontFile& fallback : fallbackFiles) { + if (auto bytes = SystemFontBytes(fallback.path)) AddStateFallbackFace(state, std::move(bytes), fallback.subFontIndex); + } return SCE_FONT_OK; } ReleaseLibraryLock(lib, libraryLock); @@ -477,6 +493,11 @@ int APS5_VABI sceFontGetLibrary(FontHandle fontHandle, FontLibrary* pLibrary) { } int APS5_VABI sceFontGetFontGlyphsCount(FontHandle fontHandle, std::uint32_t* glyphsCount) { + const auto* font = GetNativeFont(fontHandle); + if (!font || font->magic != HANDLE_MAGIC) { + if (glyphsCount) *glyphsCount = 0; + return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + } if (!glyphsCount) return SCE_FONT_ERROR_INVALID_PARAMETER; *glyphsCount = 0; const int rc = CallOpenFontObject(fontHandle, [&](const SysDriver& driver, FontObj* obj) { @@ -488,19 +509,31 @@ int APS5_VABI sceFontGetFontGlyphsCount(FontHandle fontHandle, std::uint32_t* gl } int APS5_VABI sceFontGetCharGlyphCode(FontHandle fontHandle, std::uint32_t code, std::uint32_t* glyphCode) { + if (glyphCode) *glyphCode = 0; + const auto* font = GetNativeFont(fontHandle); + if (!font || font->magic != HANDLE_MAGIC) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + if (code == 0) return SCE_FONT_ERROR_NO_SUPPORT_CODE; if (!glyphCode) return SCE_FONT_ERROR_INVALID_PARAMETER; - *glyphCode = 0; + if ((code & 0x80000000u) != 0 && (code & 0x3fff0000u) != 0) return SCE_FONT_ERROR_NO_SUPPORT_CODE; + std::uint32_t index = 0; const int rc = CallOpenFontObject(fontHandle, [&](const SysDriver& driver, FontObj* obj) { + if ((code & 0x80000000u) != 0) { + if (!driver.glyphs_count) return SCE_FONT_ERROR_FATAL; + std::uint32_t count = 0; + const int result = driver.glyphs_count(obj, &count); + if (result != SCE_FONT_OK) return result; + index = code & 0x3fffffffu; + return index < count ? SCE_FONT_OK : SCE_FONT_ERROR_NO_SUPPORT_GLYPH; + } if (!driver.glyph_index) return SCE_FONT_ERROR_FATAL; - if (code == 0) return SCE_FONT_ERROR_NO_SUPPORT_CODE; - return driver.glyph_index(obj, code, glyphCode); + return driver.glyph_index(obj, code, &index); }); - if (rc != SCE_FONT_OK) *glyphCode = 0; + if (rc == SCE_FONT_OK) *glyphCode = index | 0x80000000u; return rc; } int APS5_VABI sceFontGetFontResolution(FontHandle fontHandle, std::uint32_t* pResolution, float* pScalePixel) { - std::uint16_t unitsPerEm = 0; + std::uint32_t unitsPerEm = 0; float scalePixel = 0.0f; const int rc = CallOpenFontObject(fontHandle, [&](const SysDriver& driver, FontObj* obj) { if (!pResolution && !pScalePixel) return SCE_FONT_ERROR_INVALID_PARAMETER; diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp index 55d3de834d..7ed1af5165 100644 --- a/core/libs/prx/libSceFont/src/Render.cpp +++ b/core/libs/prx/libSceFont/src/Render.cpp @@ -81,10 +81,11 @@ int CachedColumnOffset(FontHandle handle, FontHandleNative* font, float& offset) return rc; } -void LoadKerning(FT_Face face, float scaleW, float scaleH, std::uint32_t preCode, std::uint32_t code, FontKerning* kerning) { - const FT_UInt previousGlyph = face ? FT_Get_Char_Index(face, preCode) : 0; - const FT_UInt glyph = face ? FT_Get_Char_Index(face, code) : 0; - if (!face || previousGlyph == 0 || glyph == 0) return; +void LoadKerning(const FontState& state, float scaleW, float scaleH, std::uint32_t preCode, std::uint32_t code, FontKerning* kerning) { + FT_UInt previousGlyph = 0; + FT_UInt glyph = 0; + const FT_Face face = FaceForCode(state, preCode, &previousGlyph); + if (!face || FaceForCode(state, code, &glyph) != face) return; const auto charW = static_cast(static_cast(scaleW * 64.0f)); const auto charH = static_cast(static_cast(scaleH * 64.0f)); FT_Set_Char_Size(face, charW, charH, 72, 72); @@ -187,7 +188,7 @@ int APS5_VABI sceFontGetKerning(FontHandle fontHandle, std::uint32_t preCode, st if (!font || font->magic != HANDLE_MAGIC || !AcquireFontLock(font, fontLock)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; *kerning = {}; const FontState* state = TryGetState(fontHandle); - if (state) LoadKerning(state->face, state->scaleW, state->scaleH, preCode, code, kerning); + if (state) LoadKerning(*state, state->scaleW, state->scaleH, preCode, code, kerning); ReleaseFontLock(font, fontLock); return SCE_FONT_OK; } @@ -209,7 +210,7 @@ int APS5_VABI sceFontGetRenderScaledKerning(FontHandle fontHandle, std::uint32_t float scaleH = 0.0f; rc = StyleStateGetScalePixel(&font->cached_style.state, &scaleW, &scaleH); const FontState* state = TryGetState(fontHandle); - if (rc == SCE_FONT_OK && state) LoadKerning(state->face, scaleW, scaleH, preCode, code, kerning); + if (rc == SCE_FONT_OK && state) LoadKerning(*state, scaleW, scaleH, preCode, code, kerning); } ReleaseCachedStyleLock(font, cachedLock); ReleaseFontLock(font, fontLock); @@ -419,10 +420,27 @@ int APS5_VABI sceFontDeleteGlyph(const FontMemory* memory, FontGlyph* pGlyph) { return SCE_FONT_OK; } -int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph glyph, std::uint32_t attribute, std::uint64_t value) { +int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph glyph, std::int32_t attribute, std::int32_t* previousAttribute) { + if (!glyph || glyph->magic != GLYPH_MAGIC) { + if (previousAttribute) *previousAttribute = 0; + return SCE_FONT_ERROR_INVALID_GLYPH; + } + if (attribute != 0x10 && attribute != 0x11) { + if (previousAttribute) *previousAttribute = 0; + return SCE_FONT_ERROR_INVALID_PARAMETER; + } + const std::int32_t previous = (glyph->flags & 2) ? 0x11 : 0x10; + glyph->flags = static_cast((glyph->flags & ~2u) | ((attribute - 0x10) << 1)); + if (previousAttribute) *previousAttribute = previous; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontGlyphGetAttribute(FontGlyph glyph, int attribute, int* nowAttribute) { (void)attribute; - (void)value; - if (!glyph || glyph->magic != GLYPH_MAGIC) return SCE_FONT_ERROR_INVALID_GLYPH; + if (!nowAttribute) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* generated = TryGetGeneratedGlyph(glyph); + if (!generated) return SCE_FONT_ERROR_INVALID_GLYPH; + *nowAttribute = (glyph->flags & 2) ? 0x11 : 0x10; return SCE_FONT_OK; } diff --git a/core/libs/prx/libSceFont/src/State.cpp b/core/libs/prx/libSceFont/src/State.cpp index f2b1bc905a..4ebd92348b 100644 --- a/core/libs/prx/libSceFont/src/State.cpp +++ b/core/libs/prx/libSceFont/src/State.cpp @@ -13,11 +13,13 @@ #include #include "prx/libc/include/PreciseWait.hpp" +#include "prx/libSceFont/include/FontFreeType.hpp" #include "prx/libSceFont/include/FontInternal.hpp" namespace { std::mutex stateMutex; +std::mutex faceMutex; std::unordered_map> fontStates; std::mutex glyphMutex; std::unordered_set generatedGlyphs; @@ -53,7 +55,9 @@ void ReleaseWord(std::uint32_t& word, std::uint32_t previous) { } Font::FontState::~FontState() { + std::lock_guard lock(faceMutex); if (face) FT_Done_Face(face); + for (const FallbackFace& fallback : fallbacks) FT_Done_Face(fallback.face); } void Font::Backoff() { @@ -249,6 +253,7 @@ void Font::RemoveState(FontHandle handle) { } void Font::LoadStateFace(FontState& state, std::shared_ptr> bytes, std::uint32_t subFontIndex) { + std::lock_guard lock(faceMutex); if (state.face) { FT_Done_Face(state.face); state.face = nullptr; @@ -262,6 +267,36 @@ void Font::LoadStateFace(FontState& state, std::shared_ptr> bytes, std::uint32_t subFontIndex) { + std::lock_guard lock(faceMutex); + const FT_Library library = HostFreeTypeLibrary(); + if (!library || !bytes || bytes->empty()) return; + FT_Face face = nullptr; + if (FT_New_Memory_Face(library, bytes->data(), static_cast(bytes->size()), static_cast(subFontIndex), &face) != 0) return; + FT_Select_Charmap(face, FT_ENCODING_UNICODE); + state.fallbacks.push_back({std::move(bytes), face, subFontIndex}); +} + +FT_Face Font::FallbackFaceForCode(const FontState& state, std::uint32_t code, FT_UInt* glyphIndex) { + for (const FallbackFace& fallback : state.fallbacks) { + const FT_UInt index = ResolveGlyphIndexWithFallback(fallback.face, code); + if (index == 0) continue; + *glyphIndex = index; + return fallback.face; + } + *glyphIndex = 0; + return nullptr; +} + +FT_Face Font::FaceForCode(const FontState& state, std::uint32_t code, FT_UInt* glyphIndex) { + const FT_UInt index = ResolveGlyphIndexWithFallback(state.face, code); + if (index != 0) { + *glyphIndex = index; + return state.face; + } + return FallbackFaceForCode(state, code, glyphIndex); +} + void Font::TrackGeneratedGlyph(FontGlyph glyph) { std::lock_guard lock(glyphMutex); generatedGlyphs.insert(glyph); diff --git a/core/libs/prx/libSceFont/src/Style.cpp b/core/libs/prx/libSceFont/src/Style.cpp index 67e7abe139..01ae245867 100644 --- a/core/libs/prx/libSceFont/src/Style.cpp +++ b/core/libs/prx/libSceFont/src/Style.cpp @@ -3,6 +3,7 @@ #include #include +#include #include "prx/libSceFont/include/FontInternal.hpp" @@ -59,20 +60,61 @@ int AccessFontSettings(FontHandle fontHandle, Access&& access) { }); } -int SetFontSetting(FontHandle fontHandle, std::map FontState::*table, int key, int value) { - return AccessFontSettings(fontHandle, [&](FontState& state) { - (state.*table)[key] = value; - return SCE_FONT_OK; - }); +bool HasTypographyRecordFeature(const void* record, int typographic) { + const auto* bytes = static_cast(record); + std::uint32_t count; + std::memcpy(&count, bytes + 32, sizeof(count)); + for (std::uint32_t index = 0; index < count; ++index) { + std::int32_t fontId; + std::memcpy(&fontId, bytes + 36 + sizeof(fontId) * index, sizeof(fontId)); + if (count == 1) { + if (typographic == 65538) return fontId == 43 || fontId == 12; + return static_cast(fontId) <= 29 && (0x30000c00u & (1u << fontId)) != 0; + } + if ((std::to_integer(bytes[100 + index]) & 0x80u) != 0 && fontId >= 0) return index == (typographic == 65538 ? 2u : 1u); + } + return false; } -int GetFontSetting(FontHandle fontHandle, std::map FontState::*table, int key, int* value) { - if (!value) return SCE_FONT_ERROR_INVALID_PARAMETER; - return AccessFontSettings(fontHandle, [&](FontState& state) { - const auto found = (state.*table).find(key); - *value = found == (state.*table).end() ? 0 : found->second; +int CheckTypography(FontHandleNative* font, int typographic, bool setting) { + const auto* record = font->open_info.fontset_record; + switch (typographic) { + case 1: + case 17: return SCE_FONT_OK; - }); + case 65537: + if (setting) return !record || HasTypographyRecordFeature(record, typographic) ? SCE_FONT_ERROR_INVALID_PARAMETER : SCE_FONT_ERROR_NO_SUPPORT_TYPOGRAPHY; + return !record || HasTypographyRecordFeature(record, typographic) ? SCE_FONT_OK : SCE_FONT_ERROR_NO_SUPPORT_TYPOGRAPHY; + case 65538: + return !record || HasTypographyRecordFeature(record, typographic) ? SCE_FONT_OK : SCE_FONT_ERROR_NO_SUPPORT_TYPOGRAPHY; + case 65539: + return record ? SCE_FONT_ERROR_NO_SUPPORT_TYPOGRAPHY : SCE_FONT_OK; + default: + return SCE_FONT_ERROR_INVALID_PARAMETER; + } +} + +bool IsScriptLanguageValid(int script, int language) { + if (script == 0x100) return language >= 0x100 && language <= 0x105; + if (script == 0x600) return language >= 0x600 && language <= 0x602; + if (script != 0x3000) return false; + switch (language) { + case 0x3000: + case 0x3011: + case 0x3012: + case 0x3013: + case 0x3014: + case 0x3016: + case 0x3017: + case 0x3018: + case 0x3021: + case 0x3022: + case 0x3023: + case 0x3041: + return true; + default: + return false; + } } void ResetScaleOutputs(float* w, float* h) { @@ -101,20 +143,59 @@ float ClampWeightDelta(float scale) { extern "C" { +int APS5_VABI sceFontDefineAttribute(FontHandle fontHandle, int attribute, int* oldAttribute) { + return AccessFontSettings(fontHandle, [&](FontState& state) { + if (oldAttribute) *oldAttribute = state.attribute; + state.attribute = attribute; + return SCE_FONT_OK; + }); +} + +int APS5_VABI sceFontGetAttribute(FontHandle fontHandle, int attribute, int* nowAttribute) { + (void)attribute; + if (!nowAttribute) return SCE_FONT_ERROR_INVALID_PARAMETER; + return AccessFontSettings(fontHandle, [&](FontState& state) { + *nowAttribute = state.attribute; + return SCE_FONT_OK; + }); +} + int APS5_VABI sceFontSetScriptLanguage(FontHandle fontHandle, int fontScript, int fontLanguage) { - return SetFontSetting(fontHandle, &FontState::scriptLanguages, fontScript, fontLanguage); + const int result = AccessFontSettings(fontHandle, [&](FontState&) { + return IsScriptLanguageValid(fontScript, fontLanguage) ? SCE_FONT_OK : SCE_FONT_ERROR_INVALID_PARAMETER; + }); + if (result != SCE_FONT_OK) return result; + throw std::logic_error("sceFontSetScriptLanguage: driver language selection is not implemented"); } int APS5_VABI sceFontGetScriptLanguage(FontHandle fontHandle, int fontScript, int* fontLanguage) { - return GetFontSetting(fontHandle, &FontState::scriptLanguages, fontScript, fontLanguage); + if (fontLanguage) *fontLanguage = 0; + const int result = AccessFontSettings(fontHandle, [&](FontState&) { + if (!fontLanguage) return SCE_FONT_ERROR_INVALID_PARAMETER; + return fontScript == 0x100 || fontScript == 0x600 || fontScript == 0x3000 ? SCE_FONT_OK : SCE_FONT_ERROR_INVALID_PARAMETER; + }); + if (result != SCE_FONT_OK) return result; + throw std::logic_error("sceFontGetScriptLanguage: driver language query is not implemented"); } int APS5_VABI sceFontSetTypographicDesign(FontHandle fontHandle, int typographic, int feature) { - return SetFontSetting(fontHandle, &FontState::typographicFeatures, typographic, feature); + const int result = ReadFontStyle(fontHandle, true, [&](FontHandleNative* font) { + const int validation = CheckTypography(font, typographic, true); + if (validation != SCE_FONT_OK) return validation; + return feature == 0 || feature == (typographic == 1 ? 1 : 32) ? SCE_FONT_OK : SCE_FONT_ERROR_INVALID_PARAMETER; + }); + if (result != SCE_FONT_OK) return result; + throw std::logic_error("sceFontSetTypographicDesign: driver typography selection is not implemented"); } int APS5_VABI sceFontGetTypographicDesign(FontHandle fontHandle, int typographic, int* feature) { - return GetFontSetting(fontHandle, &FontState::typographicFeatures, typographic, feature); + const int result = ReadFontStyle(fontHandle, true, [&](FontHandleNative* font) { + const int validation = CheckTypography(font, typographic, false); + return feature ? validation : SCE_FONT_ERROR_INVALID_PARAMETER; + }); + if (feature) *feature = 0; + if (result != SCE_FONT_OK) return result; + throw std::logic_error("sceFontGetTypographicDesign: driver typography query is not implemented"); } int APS5_VABI sceFontSetScalePixel(FontHandle fontHandle, float w, float h) { diff --git a/core/libs/prx/libSceFont/src/SystemFont.cpp b/core/libs/prx/libSceFont/src/SystemFont.cpp index f164d86462..c6e3d87339 100644 --- a/core/libs/prx/libSceFont/src/SystemFont.cpp +++ b/core/libs/prx/libSceFont/src/SystemFont.cpp @@ -37,7 +37,7 @@ constexpr std::array SYSTEM_FONT_SET_TYPES = { 0x1A0825C3u, 0x1A0825C7u, 0x1A083553u, 0x1A083557u, 0x1A0835D3u, 0x1A0835D7u, }; -enum class Family { Latin, LatinItalic, Typewriter, Vietnamese, Thai, Japanese, ChineseGb }; +enum class Family { Latin, LatinItalic, Typewriter, Vietnamese, Thai, Japanese, ChineseGb, Hangul, Arabic }; struct FamilyFiles { std::array system; @@ -45,7 +45,7 @@ struct FamilyFiles { std::uint32_t substituteSubFont; }; -constexpr std::array FAMILY_FILES = {{ +constexpr std::array FAMILY_FILES = {{ {{"SST-Light.otf", "SST-Roman.otf", "SST-Medium.otf", "SST-Bold.otf"}, {"NotoSans-Light.ttf", "NotoSans-Regular.ttf", "NotoSans-Medium.ttf", "NotoSans-Bold.ttf"}, 0}, {{"SST-LightItalic.otf", "SST-Italic.otf", "SST-MediumItalic.otf", "SST-BoldItalic.otf"}, @@ -60,6 +60,10 @@ constexpr std::array FAMILY_FILES = {{ {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 0}, {{"DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf"}, {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 2}, + {{"SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Bold.otf"}, + {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 1}, + {{"SSTArabic-Light.otf", "SSTArabic-Roman.otf", "SSTArabic-Medium.otf", "SSTArabic-Bold.otf"}, + {"NotoSansArabic-Light.ttf", "NotoSansArabic-Regular.ttf", "NotoSansArabic-Medium.ttf", "NotoSansArabic-Bold.ttf"}, 0}, }}; Family FamilyOf(std::uint32_t fontSetType) { @@ -87,6 +91,37 @@ std::size_t WeightOf(std::uint32_t fontSetType) { } } +std::vector FallbackFamilies(std::uint32_t fontSetType) { + const std::uint32_t scripts = (fontSetType >> 8) & 0xFFu; + const Family primary = FamilyOf(fontSetType); + std::vector families; + const auto add = [&](bool wanted, Family family) { + if (wanted && family != primary && std::find(families.begin(), families.end(), family) == families.end()) families.push_back(family); + }; + add((fontSetType & 0x10u) != 0, Family::Vietnamese); + add((scripts & 0x10u) != 0, Family::Thai); + add((scripts & 0x88u) == 0x88u, Family::ChineseGb); + add((scripts & 0x04u) != 0, Family::Japanese); + add((scripts & 0x80u) != 0, Family::ChineseGb); + add((scripts & 0x20u) != 0, Family::Hangul); + add((fontSetType & 0x80u) != 0, Family::Arabic); + return families; +} + +std::vector FamilyCandidates(Family family, std::size_t weight) { + const FamilyFiles& files = FAMILY_FILES[static_cast(family)]; + return {{std::filesystem::path(files.system[weight]), 0}, {std::filesystem::path(files.substitute[weight]), files.substituteSubFont}}; +} + +std::optional FirstExisting(const std::filesystem::path& directory, const std::vector& candidates) { + for (const Font::SystemFontFile& candidate : candidates) { + std::error_code error; + const std::filesystem::path path = directory / candidate.path; + if (std::filesystem::is_regular_file(path, error)) return Font::SystemFontFile{path, candidate.subFontIndex}; + } + return std::nullopt; +} + std::filesystem::path ExecutableDirectory() { #ifdef _WIN32 wchar_t module[MAX_PATH]; @@ -115,14 +150,16 @@ bool Font::IsSystemFontSet(std::uint32_t fontSetType) { std::vector Font::SystemFontCandidates(std::uint32_t fontSetType) { if (!IsSystemFontSet(fontSetType)) return {}; - const FamilyFiles& files = FAMILY_FILES[static_cast(FamilyOf(fontSetType))]; - const std::size_t weight = WeightOf(fontSetType); - return {{std::filesystem::path(files.system[weight]), 0}, {std::filesystem::path(files.substitute[weight]), files.substituteSubFont}}; + return FamilyCandidates(FamilyOf(fontSetType), WeightOf(fontSetType)); } std::filesystem::path Font::SystemFontDirectory() { +#ifdef _WIN32 + const wchar_t* configured = _wgetenv(L"ANYPS5_SYSTEM_FONTS"); +#else const char* configured = std::getenv("ANYPS5_SYSTEM_FONTS"); - if (!configured || configured[0] == '\0') return ExecutableDirectory() / "anyps5-fonts"; +#endif + if (!configured || configured[0] == 0) return ExecutableDirectory() / "anyps5-fonts"; const std::filesystem::path directory(configured); std::error_code error; if (!std::filesystem::is_directory(directory, error)) throw std::runtime_error("libSceFont: ANYPS5_SYSTEM_FONTS is not a directory: " + directory.string()); @@ -132,11 +169,22 @@ std::filesystem::path Font::SystemFontDirectory() { std::optional Font::FindSystemFontFile(std::uint32_t fontSetType) { const std::vector candidates = SystemFontCandidates(fontSetType); if (candidates.empty()) return std::nullopt; + return FirstExisting(SystemFontDirectory(), candidates); +} + +std::vector Font::FindSystemFontFallbackFiles(std::uint32_t fontSetType) { + if (!IsSystemFontSet(fontSetType)) return {}; const std::filesystem::path directory = SystemFontDirectory(); - for (const SystemFontFile& candidate : candidates) { - std::error_code error; - const std::filesystem::path path = directory / candidate.path; - if (std::filesystem::is_regular_file(path, error)) return SystemFontFile{path, candidate.subFontIndex}; + const std::size_t weight = WeightOf(fontSetType); + std::vector files; + const std::optional primary = FirstExisting(directory, SystemFontCandidates(fontSetType)); + const auto opened = [&](const SystemFontFile& file) { + const auto same = [&](const SystemFontFile& other) { return other.path == file.path && other.subFontIndex == file.subFontIndex; }; + return (primary && same(*primary)) || std::any_of(files.begin(), files.end(), same); + }; + for (const Family family : FallbackFamilies(fontSetType)) { + auto file = FirstExisting(directory, FamilyCandidates(family, weight)); + if (file && !opened(*file)) files.push_back(std::move(*file)); } - return std::nullopt; + return files; } diff --git a/core/libs/prx/libSceFont/src/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..c98c47a93e 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -2,383 +2,484 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "prx/libc/include/General.hpp" +#include "prx/libSceFont/include/FontTypes.hpp" + +struct FontGraphicsDevice; +struct FontGraphicsDeviceParam; +struct FontGraphicsService; +struct FontGraphicsServiceParam; +struct FontWords; +struct FontGlyphSupport; +struct FontOutlineProfile; +struct FontMetrics; +struct FontStyleInformation; +struct FontGlyphExpandBufferState; +struct FontGraphicsResource; +struct FontGraphicsFrame; +struct FontGraphicsDesignResource; +struct FontGraphicsDesign; +struct FontGraphicsCanvas; +struct FontGraphicsCanvasSequence; +struct FontGraphicsSurface; +struct FontGraphicsSurfaceTexture; +struct FontGraphicsFill; + +struct FontGraphicsRegion { + float x; + float y; + float width; + float height; + std::uint32_t flags; + std::uint32_t tag; + float outerRadius; + float innerRadius; +}; + +static_assert(sizeof(FontGraphicsRegion) == 32); +static_assert(offsetof(FontGraphicsRegion, x) == 0); +static_assert(offsetof(FontGraphicsRegion, y) == 4); +static_assert(offsetof(FontGraphicsRegion, width) == 8); +static_assert(offsetof(FontGraphicsRegion, height) == 12); +static_assert(offsetof(FontGraphicsRegion, flags) == 16); +static_assert(offsetof(FontGraphicsRegion, tag) == 20); +static_assert(offsetof(FontGraphicsRegion, outerRadius) == 24); +static_assert(offsetof(FontGraphicsRegion, innerRadius) == 28); + +struct FontGraphicsFillMethod { + std::int32_t mode; + std::int32_t type; + std::uint32_t flags; + std::uint32_t tag; + std::byte reserved[16]; +}; + +static_assert(sizeof(FontGraphicsFillMethod) == 32); +static_assert(offsetof(FontGraphicsFillMethod, mode) == 0); +static_assert(offsetof(FontGraphicsFillMethod, type) == 4); +static_assert(offsetof(FontGraphicsFillMethod, flags) == 8); +static_assert(offsetof(FontGraphicsFillMethod, tag) == 12); +static_assert(offsetof(FontGraphicsFillMethod, reserved) == 16); + +struct FontGraphicsFillPlot { + float mapping[4]; + const FontGraphicsRegion* region; + std::uint16_t tag; + std::uint16_t reserved26; + std::uint16_t layout; + std::uint8_t flags; + std::byte reserved[33]; +}; + +static_assert(sizeof(FontGraphicsFillPlot) == 64); +static_assert(offsetof(FontGraphicsFillPlot, mapping) == 0); +static_assert(offsetof(FontGraphicsFillPlot, region) == 16); +static_assert(offsetof(FontGraphicsFillPlot, tag) == 24); +static_assert(offsetof(FontGraphicsFillPlot, reserved26) == 26); +static_assert(offsetof(FontGraphicsFillPlot, layout) == 28); +static_assert(offsetof(FontGraphicsFillPlot, flags) == 30); +static_assert(offsetof(FontGraphicsFillPlot, reserved) == 31); + +struct FontGraphicsFillRates { + float mapping[4]; + const FontGraphicsFill* fill; + std::uint16_t tag; + std::uint16_t effect; + std::uint16_t layout; + std::uint8_t flags; + std::byte reserved[33]; +}; + +static_assert(sizeof(FontGraphicsFillRates) == 64); +static_assert(offsetof(FontGraphicsFillRates, mapping) == 0); +static_assert(offsetof(FontGraphicsFillRates, fill) == 16); +static_assert(offsetof(FontGraphicsFillRates, tag) == 24); +static_assert(offsetof(FontGraphicsFillRates, effect) == 26); +static_assert(offsetof(FontGraphicsFillRates, layout) == 28); +static_assert(offsetof(FontGraphicsFillRates, flags) == 30); +static_assert(offsetof(FontGraphicsFillRates, reserved) == 31); + +struct FontGraphicsPoolUsage { + std::uint32_t capacity; + std::uint32_t used; + std::uint32_t available; + std::uint32_t peak; + std::int32_t change; +}; + +static_assert(sizeof(FontGraphicsPoolUsage) == 20); +static_assert(offsetof(FontGraphicsPoolUsage, capacity) == 0); +static_assert(offsetof(FontGraphicsPoolUsage, used) == 4); +static_assert(offsetof(FontGraphicsPoolUsage, available) == 8); +static_assert(offsetof(FontGraphicsPoolUsage, peak) == 12); +static_assert(offsetof(FontGraphicsPoolUsage, change) == 16); + +struct FontGraphicsMemoryUsage { + FontGraphicsPoolUsage storage; + FontGraphicsPoolUsage allocations; + void* address; +}; + +static_assert(sizeof(FontGraphicsMemoryUsage) == 48); +static_assert(offsetof(FontGraphicsMemoryUsage, storage) == 0); +static_assert(offsetof(FontGraphicsMemoryUsage, allocations) == 20); +static_assert(offsetof(FontGraphicsMemoryUsage, address) == 40); + +struct FontGraphicsDeviceUsage { + FontGraphicsMemoryUsage first; + FontGraphicsMemoryUsage second; +}; + +static_assert(sizeof(FontGraphicsDeviceUsage) == 96); +static_assert(offsetof(FontGraphicsDeviceUsage, first) == 0); +static_assert(offsetof(FontGraphicsDeviceUsage, second) == 48); + #pragma GCC visibility push(default) extern "C" { -int APS5_VABI sceFontCharactersRefersTextCodes() { +int APS5_VABI sceFontControl(std::int32_t command, void* object, std::uintptr_t argument) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontControl() { +int APS5_VABI sceFontCreateGraphicsDevice(const FontMemory* memory, const FontGraphicsDeviceParam* param, FontGraphicsDevice** device) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontCreateGraphicsDevice() { +int APS5_VABI sceFontCreateGraphicsService(const FontMemory* memory, const FontGraphicsServiceParam* param, FontGraphicsService** service) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontCreateGraphicsService() { +int APS5_VABI sceFontCreateGraphicsServiceWithEdition(const FontMemory* memory, const FontGraphicsServiceParam* param, std::uint64_t unusedSlot, FontGraphicsService** service) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontCreateGraphicsServiceWithEdition() { +int APS5_VABI sceFontCreateWords(const FontMemory* memory, FontTextSource* source, std::uint64_t unusedSlot, FontWords** words) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontCreateWords() { +int APS5_VABI sceFontDestroyGraphicsDevice(FontGraphicsDevice** device) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontDefineAttribute() { +int APS5_VABI sceFontDestroyGraphicsService(FontGraphicsService** service) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontDestroyGraphicsDevice() { +int APS5_VABI sceFontDestroyWords(FontWords** words) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontDestroyGraphicsService() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontDestroyWords() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - - -int APS5_VABI sceFontGetAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGetFontGlyphsOutlineProfile() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGetFontMetrics() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGetFontStyleInformation() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceFontGetGlyphExpandBufferState() { +int APS5_VABI sceFontGetFontGlyphsOutlineProfile(FontHandle font, FontOutlineProfile* profile) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGlyphGetAttribute() { +int APS5_VABI sceFontGetFontMetrics(FontHandle font, std::uint32_t mode, FontMetrics* metrics) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGlyphRenderImage() { +int APS5_VABI sceFontGetFontStyleInformation(FontHandle font, FontStyleInformation* information) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGlyphRenderImageHorizontal() { +int APS5_VABI sceFontGetGlyphExpandBufferState(FontHandle font, FontGlyphExpandBufferState* state) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGlyphRenderImageVertical() { +int APS5_VABI sceFontGlyphRenderImage(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsBeginFrame() { +int APS5_VABI sceFontGlyphRenderImageHorizontal(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsDrawingCancel() { +int APS5_VABI sceFontGlyphRenderImageVertical(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsDrawingFinish() { +int APS5_VABI sceFontGraphicsBeginFrame(FontGraphicsFrame* frame, std::uint64_t flags, FontGraphicsResource** resource) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsEndFrame() { +int APS5_VABI sceFontGraphicsDrawingCancel(FontGraphicsFrame** frame) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsExchangeResource() { +int APS5_VABI sceFontGraphicsDrawingFinish(FontGraphicsFrame** frame, FontGraphicsDevice* device) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsFillMethodInit() { +int APS5_VABI sceFontGraphicsEndFrame(FontGraphicsFrame* frame) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsFillPlotInit() { +int APS5_VABI sceFontGraphicsExchangeResource(FontGraphicsResource* resource, FontGraphicsDesignResource* replacement, FontGraphicsDesignResource** previous) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsFillPlotSetLayout() { +int APS5_VABI sceFontGraphicsFillMethodInit(FontGraphicsFillMethod* method, std::int32_t mode, std::int32_t type) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsFillPlotSetMapping() { +int APS5_VABI sceFontGraphicsFillPlotInit(FontGraphicsFillPlot* plot, const FontGraphicsRegion* region) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsFillRatesInit() { +int APS5_VABI sceFontGraphicsFillPlotSetLayout(FontGraphicsFillPlot* plot, std::int32_t first, std::int32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsFillRatesSetFillEffect() { +int APS5_VABI sceFontGraphicsFillPlotSetMapping(FontGraphicsFillPlot* plot, float first, float second, float third, float fourth) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsFillRatesSetLayout() { +int APS5_VABI sceFontGraphicsFillRatesInit(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsFillRatesSetMapping() { +int APS5_VABI sceFontGraphicsFillRatesSetFillEffect(FontGraphicsFillRates* rates, const FontGraphicsFill* fill, std::uint32_t effect) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsGetDeviceUsage() { +int APS5_VABI sceFontGraphicsFillRatesSetLayout(FontGraphicsFillRates* rates, std::int32_t first, std::int32_t second) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsRegionInit() { +int APS5_VABI sceFontGraphicsFillRatesSetMapping(FontGraphicsFillRates* rates, float first, float second, float third, float fourth) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsRegionInitCircular() { +int APS5_VABI sceFontGraphicsGetDeviceUsage(FontGraphicsDevice* device, FontGraphicsDeviceUsage* usage) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsRegionInitRoundish() { +int APS5_VABI sceFontGraphicsRegionInit(FontGraphicsRegion* region, float x, float y, float width, float height) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsRelease() { +int APS5_VABI sceFontGraphicsRegionInitCircular(FontGraphicsRegion* region, float centerX, float centerY, float outerRadius, float innerRadius) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsRenderResource() { +int APS5_VABI sceFontGraphicsRegionInitRoundish(FontGraphicsRegion* region, float x, float y, float width, float height, float outerRadius, float innerRadius, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetFramePolicy() { +int APS5_VABI sceFontGraphicsRelease(void** object) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupClipping() { +int APS5_VABI sceFontGraphicsRenderResource(FontGraphicsFrame* frame, FontGraphicsDesignResource* design, float x, float y, FontGraphicsResource** resource) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupColorRates() { +int APS5_VABI sceFontGraphicsSetFramePolicy(FontGraphicsResource* resource, std::int32_t policy, std::uint32_t value) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupFillMethod() { +int APS5_VABI sceFontGraphicsSetupClipping(FontGraphicsResource* resource, const FontGraphicsRegion* region) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupFillRates() { +int APS5_VABI sceFontGraphicsSetupColorRates(FontGraphicsResource* resource, float red, float green, float blue, float alpha) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupGlyphFill() { +int APS5_VABI sceFontGraphicsSetupFillMethod(FontGraphicsResource* resource, const FontGraphicsFillMethod* method) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupGlyphFillPlot() { +int APS5_VABI sceFontGraphicsSetupFillRates(FontGraphicsResource* resource, const FontGraphicsFillRates* rates) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupHandleDefault() { +int APS5_VABI sceFontGraphicsSetupGlyphFill(FontGraphicsResource* resource, const FontGraphicsFill* fill) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupLocation() { +int APS5_VABI sceFontGraphicsSetupGlyphFillPlot(FontGraphicsResource* resource, const FontGraphicsFillPlot* plot) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupPositioning() { +int APS5_VABI sceFontGraphicsSetupHandleDefault(FontGraphicsResource* resource, std::uint64_t flags) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupRotation() { +int APS5_VABI sceFontGraphicsSetupLocation(FontGraphicsResource* resource, float x, float y) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupScaling() { +int APS5_VABI sceFontGraphicsSetupPositioning(FontGraphicsResource* resource, std::uint32_t flags, float x, float y) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupShapeFill() { +int APS5_VABI sceFontGraphicsSetupRotation(FontGraphicsResource* resource, float angle, float pivotX, float pivotY) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsSetupShapeFillPlot() { +int APS5_VABI sceFontGraphicsSetupScaling(FontGraphicsResource* resource, float scaleX, float scaleY, float pivotX, float pivotY) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsStructureCanvas() { +int APS5_VABI sceFontGraphicsSetupShapeFill(FontGraphicsResource* resource, const FontGraphicsFill* fill) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsStructureCanvasSequence() { +int APS5_VABI sceFontGraphicsSetupShapeFillPlot(FontGraphicsResource* resource, const FontGraphicsFillPlot* plot) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsStructureDesign() { +int APS5_VABI sceFontGraphicsStructureCanvas(FontGraphicsDevice* device, std::uint32_t width, std::uint32_t height, std::int32_t format, FontGraphicsCanvas** canvas) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsStructureDesignResource() { +int APS5_VABI sceFontGraphicsStructureCanvasSequence(FontGraphicsDevice* device, FontGraphicsCanvas* canvas, const FontGraphicsSurface* surface, std::uint64_t flags, FontGraphicsCanvasSequence** sequence) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsStructureSurfaceTexture() { +int APS5_VABI sceFontGraphicsStructureDesign(FontGraphicsDevice* device, std::int32_t format, FontGraphicsDesign** design) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsUpdateClipping() { +int APS5_VABI sceFontGraphicsStructureDesignResource(FontGraphicsDevice* device, FontGraphicsDesign* design, FontGraphicsDesignResource** resource) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsUpdateColorRates() { +int APS5_VABI sceFontGraphicsStructureSurfaceTexture(FontGraphicsDevice* device, const FontGraphicsSurface* surface, FontGraphicsSurfaceTexture** texture) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsUpdateFillMethod() { +int APS5_VABI sceFontGraphicsUpdateClipping(FontGraphicsResource* resource, const FontGraphicsRegion* region) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsUpdateFillRates() { +int APS5_VABI sceFontGraphicsUpdateColorRates(FontGraphicsResource* resource, float red, float green, float blue, float alpha) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsUpdateGlyphFill() { +int APS5_VABI sceFontGraphicsUpdateFillMethod(FontGraphicsResource* resource, const FontGraphicsFillMethod* method) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsUpdateGlyphFillPlot() { +int APS5_VABI sceFontGraphicsUpdateFillRates(FontGraphicsResource* resource, const FontGraphicsFillRates* rates) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsUpdateLocation() { +int APS5_VABI sceFontGraphicsUpdateGlyphFill(FontGraphicsResource* resource, const FontGraphicsFill* fill) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsUpdatePositioning() { +int APS5_VABI sceFontGraphicsUpdateGlyphFillPlot(FontGraphicsResource* resource, const FontGraphicsFillPlot* plot) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsUpdateRotation() { +int APS5_VABI sceFontGraphicsUpdateLocation(FontGraphicsResource* resource, float x, float y) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsUpdateScaling() { +int APS5_VABI sceFontGraphicsUpdatePositioning(FontGraphicsResource* resource, std::uint32_t flags, float x, float y) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsUpdateShapeFill() { +int APS5_VABI sceFontGraphicsUpdateRotation(FontGraphicsResource* resource, float angle, float pivotX, float pivotY) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsUpdateShapeFillPlot() { +int APS5_VABI sceFontGraphicsUpdateScaling(FontGraphicsResource* resource, float scaleX, float scaleY, float pivotX, float pivotY) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontSupportGlyphs() { +int APS5_VABI sceFontGraphicsUpdateShapeFill(FontGraphicsResource* resource, const FontGraphicsFill* fill) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontTextCodesStepBack() { +int APS5_VABI sceFontGraphicsUpdateShapeFillPlot(FontGraphicsResource* resource, const FontGraphicsFillPlot* plot) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontTextCodesStepNext() { +int APS5_VABI sceFontSupportGlyphs(const FontGlyphSupport* support) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontWordsFindWordCharacters() { +int APS5_VABI sceFontWordsFindWordCharacters(FontWords* words, FontTextCharacter* firstCharacter, FontTextCharacter* endCharacter, FontTextCharacter** lastCharacter, FontTextCharacter** nextCharacter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsDrawupFillTextureImageObject(void) { +int APS5_VABI sceFontGraphicsDrawupFillTextureImageObject(FontGraphicsFrame* frame, const void* data, std::uint32_t dataSize, float x, float y, float width, float height, FontGraphicsFill** fill) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontGraphicsDrawupFillTexturePatternObject(void) { +int APS5_VABI sceFontGraphicsDrawupFillTexturePatternObject(FontGraphicsFrame* frame, const void* data, std::uint32_t dataSize, std::uint32_t firstMode, std::uint32_t secondMode, std::int32_t firstCount, std::int32_t firstLayout, std::int32_t secondLayout, std::int32_t secondCount, FontGraphicsFill** fill) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceFont/src/WritingLine.cpp b/core/libs/prx/libSceFont/src/WritingLine.cpp index 35e014c3c8..7365c1f418 100644 --- a/core/libs/prx/libSceFont/src/WritingLine.cpp +++ b/core/libs/prx/libSceFont/src/WritingLine.cpp @@ -1,7 +1,7 @@ #include #include #include -#include +#include #include #include #include @@ -11,24 +11,19 @@ namespace { -constexpr std::int32_t WRITING_FORM_HORIZONTAL = 0x10; constexpr std::int32_t WRITING_FORM_HORIZONTAL_RTL = 0x11; constexpr std::int32_t WRITING_FORM_HORIZONTAL_LTR = 0x12; -constexpr std::uint16_t CREATE_WRITING_LINE_DETAIL_ID = 0x0FD5; constexpr std::uint64_t WRITING_ATTRIBUTE_ID_MASK = 0xFF; constexpr std::uint64_t WRITING_ATTRIBUTE_ID = 0xFA; constexpr std::uint64_t WRITING_ATTRIBUTE_SPACE_BEFORE = std::uint64_t{1} << 30; constexpr std::uint64_t WRITING_ATTRIBUTE_SPACE_AFTER = std::uint64_t{1} << 31; constexpr std::size_t WRITING_LEVEL_COUNT = 0x7F; -struct CreateWritingLineDetail { - std::uint16_t detailId; -}; - struct WritingOrder { void* orderer = nullptr; WritingOrder* prev = nullptr; WritingOrder* next = nullptr; + std::uint64_t reserved = 0; std::uint64_t attribute = 0; FontWritingMetrics metrics{}; }; @@ -39,22 +34,42 @@ struct WritingLevel { }; struct WritingLine { + std::uint16_t kind = 0xF0A; + std::uint16_t reserved = 0; std::int32_t form = 0; + const FontMemory* memory = nullptr; FontWritingMetrics renderMetrics{}; FontWritingLineStep step{}; float advanceX = 0.0f; float advanceY = 0.0f; float orderingSpace = 0.0f; float steppedSpace = 0.0f; - std::size_t lastLevel = 0; + std::uint32_t lastLevel = 0; + std::uint32_t reservedLevel = 0; + WritingOrder* first = nullptr; WritingOrder* last = nullptr; WritingOrder* head = nullptr; WritingOrder* tail = nullptr; WritingOrder* cursor = nullptr; + std::uint64_t reservedCursor = 0; std::array levels{}; - std::deque orders; }; +static_assert(sizeof(WritingOrder) == 64); +static_assert(offsetof(WritingOrder, attribute) == 32); +static_assert(offsetof(WritingOrder, metrics) == 40); +static_assert(sizeof(WritingLine) == 2208); +static_assert(offsetof(WritingLine, step) == 40); +static_assert(offsetof(WritingLine, advanceX) == 104); +static_assert(offsetof(WritingLine, last) == 136); +static_assert(offsetof(WritingLine, head) == 144); +static_assert(offsetof(WritingLine, cursor) == 160); +static_assert(offsetof(WritingLine, levels) == 176); + +bool IsMemoryValid(const FontMemory* memory) { + return memory && memory->mem_kind == 0xF00 && memory->iface && memory->iface->alloc && memory->iface->dealloc; +} + std::mutex linesMutex; std::unordered_set lines; @@ -62,13 +77,6 @@ std::unordered_set lines; throw std::runtime_error(std::string(function) + ": " + what); } -WritingLine* GetLine(const char* function, void* writingLine) { - auto* line = static_cast(writingLine); - std::lock_guard lock(linesMutex); - if (!lines.contains(line)) Unsupported(function, "unknown writing line handle (error code not verified)"); - return line; -} - bool IsRightToLeft(const WritingLine& line) { return line.form == WRITING_FORM_HORIZONTAL_RTL; } @@ -94,7 +102,13 @@ float TailSpace(const WritingLine& line, const WritingOrder& order) { } void ResetOrders(WritingLine& line) { - line.orders.clear(); + for (WritingOrder* order = line.head; order;) { + WritingOrder* next = order->next; + std::destroy_at(order); + line.memory->iface->dealloc(line.memory->mspace_handle, order); + order = next; + } + line.first = nullptr; line.levels.fill({}); line.advanceX = 0.0f; line.advanceY = 0.0f; @@ -217,50 +231,71 @@ void ExtendRenderMetrics(WritingLine& line) { extern "C" { int APS5_VABI sceFontCreateWritingLine(const FontMemory* memory, std::int32_t writingForm, const void* writingLineDetail, void** pWritingLine) { - (void)memory; - if (!pWritingLine) Unsupported(__func__, "null writing line pointer (error code not verified)"); - if (writingForm != WRITING_FORM_HORIZONTAL && writingForm != WRITING_FORM_HORIZONTAL_RTL && writingForm != WRITING_FORM_HORIZONTAL_LTR) - Unsupported(__func__, "writing form " + std::to_string(writingForm) + " is not modelled"); - if (writingLineDetail && static_cast(writingLineDetail)->detailId != CREATE_WRITING_LINE_DETAIL_ID) - Unsupported(__func__, "unknown writing line detail id"); - auto* line = new WritingLine{}; + if (!memory) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (!IsMemoryValid(memory)) return SCE_FONT_ERROR_INVALID_MEMORY; + if (!pWritingLine || writingLineDetail || (writingForm != WRITING_FORM_HORIZONTAL_RTL && writingForm != WRITING_FORM_HORIZONTAL_LTR)) return SCE_FONT_ERROR_INVALID_PARAMETER; + void* allocation = memory->iface->alloc(memory->mspace_handle, sizeof(WritingLine)); + if (!allocation) return SCE_FONT_ERROR_ALLOCATION_FAILED; + auto* line = std::construct_at(static_cast(allocation)); + line->memory = memory; line->form = writingForm; { std::lock_guard lock(linesMutex); - lines.insert(line); + try { + lines.insert(line); + } catch (...) { + std::destroy_at(line); + memory->iface->dealloc(memory->mspace_handle, allocation); + throw; + } } *pWritingLine = line; return SCE_FONT_OK; } int APS5_VABI sceFontDestroyWritingLine(void** pWritingLine) { - if (!pWritingLine || !*pWritingLine) return SCE_FONT_OK; - auto* line = GetLine(__func__, *pWritingLine); + if (!pWritingLine || !*pWritingLine) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* line = static_cast(*pWritingLine); { std::lock_guard lock(linesMutex); + if (!lines.contains(line) || !IsMemoryValid(line->memory)) return SCE_FONT_ERROR_INVALID_WRITING; lines.erase(line); } - delete line; + const FontMemory* memory = line->memory; + ResetOrders(*line); + line->kind = 0; + std::destroy_at(line); + memory->iface->dealloc(memory->mspace_handle, line); *pWritingLine = nullptr; return SCE_FONT_OK; } int APS5_VABI sceFontWritingLineClear(void* writingLine) { - auto* line = GetLine(__func__, writingLine); + if (!writingLine) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* line = static_cast(writingLine); + { + std::lock_guard lock(linesMutex); + if (!lines.contains(line)) return SCE_FONT_ERROR_INVALID_WRITING; + } + if (!IsMemoryValid(line->memory)) return SCE_FONT_ERROR_INVALID_WRITING; ResetOrders(*line); return SCE_FONT_OK; } int APS5_VABI sceFontWritingLineWritesOrder(void* writingLine, std::uint64_t writingAttribute, const FontWritingMetrics* writingMetrics, void* writingOrderer) { - auto* line = GetLine(__func__, writingLine); - if (writingAttribute == 0) writingAttribute = WRITING_ATTRIBUTE_ID; - if ((writingAttribute & WRITING_ATTRIBUTE_ID_MASK) != WRITING_ATTRIBUTE_ID) - Unsupported(__func__, "writing attribute " + std::to_string(writingAttribute) + " is not modelled"); const std::size_t level = (writingAttribute >> 8) & 0xFF; - if (level >= WRITING_LEVEL_COUNT) Unsupported(__func__, "embedding level " + std::to_string(level) + " (error code not verified)"); - if (!writingOrderer) Unsupported(__func__, "null writing orderer (error code not verified)"); + if (!writingLine || !writingOrderer || (writingAttribute & WRITING_ATTRIBUTE_ID_MASK) != WRITING_ATTRIBUTE_ID || level >= WRITING_LEVEL_COUNT) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* line = static_cast(writingLine); + { + std::lock_guard lock(linesMutex); + if (!lines.contains(line)) return SCE_FONT_ERROR_INVALID_WRITING; + } + if (!IsMemoryValid(line->memory)) return SCE_FONT_ERROR_INVALID_PARAMETER; if (line->cursor) ResetOrders(*line); - WritingOrder* order = &line->orders.emplace_back(); + void* allocation = line->memory->iface->alloc(line->memory->mspace_handle, sizeof(WritingOrder)); + if (!allocation) return SCE_FONT_ERROR_ALLOCATION_FAILED; + WritingOrder* order = std::construct_at(static_cast(allocation)); + if (!line->last) line->first = order; order->orderer = writingOrderer; order->attribute = writingAttribute; if (writingMetrics) order->metrics = *writingMetrics; @@ -271,13 +306,17 @@ int APS5_VABI sceFontWritingLineWritesOrder(void* writingLine, std::uint64_t wri line->advanceX += order->metrics.advanceX; line->advanceY += order->metrics.advanceY; line->last = order; - line->lastLevel = level; + line->lastLevel = static_cast(level); return SCE_FONT_OK; } const FontWritingLineStep* APS5_VABI sceFontWritingLineRefersRenderStep(void* writingLine) { if (!writingLine) return nullptr; - auto* line = GetLine(__func__, writingLine); + auto* line = static_cast(writingLine); + { + std::lock_guard lock(linesMutex); + if (!lines.contains(line)) return nullptr; + } if (line->cursor) { ExtendRenderMetrics(*line); } else { @@ -319,14 +358,32 @@ const FontWritingLineStep* APS5_VABI sceFontWritingLineRefersRenderStep(void* wr } int APS5_VABI sceFontWritingLineGetRenderMetrics(void* writingLine, FontWritingMetrics* writingMetrics) { - auto* line = GetLine(__func__, writingLine); - if (!writingMetrics) Unsupported(__func__, "null metrics pointer (error code not verified)"); + if (!writingMetrics) return SCE_FONT_ERROR_INVALID_PARAMETER; + *writingMetrics = {}; + if (!writingLine) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* line = static_cast(writingLine); + std::lock_guard lock(linesMutex); + if (!lines.contains(line)) return SCE_FONT_ERROR_INVALID_WRITING; *writingMetrics = line->renderMetrics; return SCE_FONT_OK; } int APS5_VABI sceFontWritingLineGetOrderingSpace(void* writingLine, float* advanceSpace, float* headSpace, float* inlineSpace, float* tailSpace) { - auto* line = GetLine(__func__, writingLine); + auto* line = static_cast(writingLine); + int error = SCE_FONT_OK; + { + std::lock_guard lock(linesMutex); + if (!line) error = SCE_FONT_ERROR_INVALID_PARAMETER; + else if (!lines.contains(line)) error = SCE_FONT_ERROR_INVALID_WRITING; + else if (line->form != WRITING_FORM_HORIZONTAL_RTL && line->form != WRITING_FORM_HORIZONTAL_LTR) error = SCE_FONT_ERROR_INVALID_PARAMETER; + } + if (error != SCE_FONT_OK) { + if (advanceSpace) *advanceSpace = 0.0f; + if (headSpace) *headSpace = 0.0f; + if (inlineSpace) *inlineSpace = 0.0f; + if (tailSpace) *tailSpace = 0.0f; + return error; + } if (headSpace && !tailSpace) Unsupported(__func__, "a head space pointer without a tail space pointer is not modelled"); if (advanceSpace) *advanceSpace = line->advanceX; if (headSpace) { diff --git a/core/libs/prx/libSceFontFt/Export.cpp b/core/libs/prx/libSceFontFt/Export.cpp index d34a3ca559..628a572b94 100644 --- a/core/libs/prx/libSceFontFt/Export.cpp +++ b/core/libs/prx/libSceFontFt/Export.cpp @@ -1,11 +1,15 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later + #include "prx/libSceFontFt/include/FontFtDriver.hpp" #include "prx/libc/include/General.hpp" #pragma GCC visibility push(default) +struct FontFtAliases; +struct FontFtGlyphSelection; + extern "C" { const Font::SysDriver* APS5_VABI sceFontSelectLibraryFt(int value) { @@ -16,97 +20,85 @@ const Font::RendererSelection* APS5_VABI sceFontSelectRendererFt(int value) { return value == 0 ? FontFt::RendererTable() : nullptr; } -int APS5_VABI sceFontFtInitAliases() { +int APS5_VABI sceFontFtInitAliases(FontFtAliases** aliases) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontFtSetAliasFont() { +int APS5_VABI sceFontFtSetAliasFont(const char* alias, std::uint32_t fontId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontFtSetAliasPath() { +int APS5_VABI sceFontFtSetAliasPath(const char* alias, const char* path) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontFtSupportBdf() { +int APS5_VABI sceFontFtSupportBdf(FontLibrary library) { + return FontFt::SupportFormats(library, {"bdf"}); +} + +int APS5_VABI sceFontFtSupportCid(FontLibrary library) { + return FontFt::SupportFormats(library, {"t1cid"}); +} + +int APS5_VABI sceFontFtSupportFontFormats(FontLibrary library, std::uint32_t formats) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontFtSupportCid() { +int APS5_VABI sceFontFtSupportOpenType(FontLibrary library) { + return FontFt::SupportFormats(library, {"cff", "truetype"}); +} + +int APS5_VABI sceFontFtSupportOpenTypeOtf(FontLibrary library) { + return FontFt::SupportFormats(library, {"cff"}); +} + +int APS5_VABI sceFontFtSupportOpenTypeTtf(FontLibrary library) { + return FontFt::SupportFormats(library, {"truetype"}); +} + +int APS5_VABI sceFontFtSupportPcf(FontLibrary library) { + return FontFt::SupportFormats(library, {"pcf"}); +} + +int APS5_VABI sceFontFtSupportPfr(FontLibrary library) { + return FontFt::SupportFormats(library, {"pfr"}); +} + +int APS5_VABI sceFontFtSupportSystemFonts(FontLibrary library) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontFtSupportFontFormats() { +int APS5_VABI sceFontFtSupportTrueType(FontLibrary library) { + return FontFt::SupportFormats(library, {"truetype"}); +} + +int APS5_VABI sceFontFtSupportTrueTypeGx(FontLibrary library) { + return FontFt::SupportFormats(library, {"truetype"}); +} + +int APS5_VABI sceFontFtSupportType1(FontLibrary library) { + return FontFt::SupportFormats(library, {"type1"}); +} + +int APS5_VABI sceFontFtSupportType42(FontLibrary library) { + return FontFt::SupportFormats(library, {"type42"}); +} + +int APS5_VABI sceFontFtSupportWinFonts(FontLibrary library) { + return FontFt::SupportFormats(library, {"winfonts"}); +} + +int APS5_VABI sceFontFtTermAliases(FontFtAliases** aliases) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceFontFtSupportOpenType() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontFtSupportOpenTypeOtf() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontFtSupportOpenTypeTtf() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontFtSupportPcf() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontFtSupportPfr() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontFtSupportSystemFonts() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontFtSupportTrueType() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontFtSupportTrueTypeGx() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontFtSupportType1() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontFtSupportType42() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontFtSupportWinFonts() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontFtTermAliases() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontSelectGlyphsFt() { +const FontFtGlyphSelection* APS5_VABI sceFontSelectGlyphsFt(std::int32_t selector) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceFontFt/include/FontFtDriver.hpp b/core/libs/prx/libSceFontFt/include/FontFtDriver.hpp index 87e7e92931..35ba74ff66 100644 --- a/core/libs/prx/libSceFontFt/include/FontFtDriver.hpp +++ b/core/libs/prx/libSceFontFt/include/FontFtDriver.hpp @@ -4,12 +4,15 @@ #ifndef CORE_LIBS_PRX_LIBSCEFONTFT_INCLUDE_FONTFTDRIVER_HPP #define CORE_LIBS_PRX_LIBSCEFONTFT_INCLUDE_FONTFTDRIVER_HPP +#include + #include "prx/libSceFont/include/FontDriver.hpp" namespace FontFt { const Font::SysDriver* DriverTable(); const Font::RendererSelection* RendererTable(); +int SupportFormats(FontLibrary library, std::initializer_list modules); } diff --git a/core/libs/prx/libSceFontFt/src/Driver.cpp b/core/libs/prx/libSceFontFt/src/Driver.cpp index 7f49954e0a..a4561830ea 100644 --- a/core/libs/prx/libSceFontFt/src/Driver.cpp +++ b/core/libs/prx/libSceFontFt/src/Driver.cpp @@ -2,12 +2,21 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include +#include +#include +#include +#include #include #include #include #include #include +#include #include +#ifdef _WIN32 +#include +#endif #include #include FT_FREETYPE_H @@ -193,6 +202,41 @@ int APS5_VABI LibrarySupport(FontLibNative* library, std::uint32_t formats) { return SCE_FONT_OK; } +#ifdef _WIN32 +void CloseMappedFile(FT_Stream stream) { + UnmapViewOfFile(stream->base); + delete stream; +} + +FT_Error OpenFileFace(FT_Library library, const char* path, FT_Long subFontIndex, FT_Face* face) { + const HANDLE file = CreateFileW(std::filesystem::path(reinterpret_cast(path)).c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) return FT_Err_Cannot_Open_Resource; + LARGE_INTEGER size{}; + if (!GetFileSizeEx(file, &size) || size.QuadPart <= 0 || size.QuadPart > std::numeric_limits::max()) { + CloseHandle(file); + return FT_Err_Cannot_Open_Stream; + } + const HANDLE mapping = CreateFileMappingW(file, nullptr, PAGE_READONLY, 0, 0, nullptr); + CloseHandle(file); + if (!mapping) return FT_Err_Cannot_Open_Stream; + void* view = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0); + CloseHandle(mapping); + if (!view) return FT_Err_Cannot_Open_Stream; + auto* stream = new FT_StreamRec{}; + stream->base = static_cast(view); + stream->size = static_cast(size.QuadPart); + stream->close = &CloseMappedFile; + FT_Open_Args args{}; + args.flags = FT_OPEN_STREAM; + args.stream = stream; + return FT_Open_Face(library, &args, subFontIndex, face); +} +#else +FT_Error OpenFileFace(FT_Library library, const char* path, FT_Long subFontIndex, FT_Face* face) { + return FT_New_Face(library, path, subFontIndex, face); +} +#endif + int APS5_VABI LibraryOpenFont(FontLibNative* library, std::uint32_t mode, const void* fontAddress, std::uint32_t fontSize, std::uint32_t subFontIndex, std::uint32_t uniqueWord, FontObj** inoutFontObj) { (void)uniqueWord; if (!library || !fontAddress || !inoutFontObj) return SCE_FONT_ERROR_INVALID_PARAMETER; @@ -217,7 +261,7 @@ int APS5_VABI LibraryOpenFont(FontLibNative* library, std::uint32_t mode, const if (fromMemory) { error = FT_New_Memory_Face(*ftLibrary, static_cast(fontAddress), static_cast(fontSize), static_cast(subFontIndex), &face); } else { - error = FT_New_Face(*ftLibrary, static_cast(fontAddress), static_cast(subFontIndex), &face); + error = OpenFileFace(*ftLibrary, static_cast(fontAddress), static_cast(subFontIndex), &face); } if (error != 0 || !face) { if (fromMemory || error == FT_Err_Unknown_File_Format) return SCE_FONT_ERROR_NO_SUPPORT_FORMAT; @@ -264,7 +308,7 @@ int APS5_VABI LibraryCloseFont(FontObj* obj, std::uint32_t flags) { return SCE_FONT_OK; } -int APS5_VABI LibraryGetFaceScale(FontObj* obj, std::uint16_t* outUnitsPerEm, float* outScale) { +int APS5_VABI LibraryGetFaceScale(FontObj* obj, std::uint32_t* outUnitsPerEm, float* outScale) { if (!obj || !outUnitsPerEm || !outScale) return SCE_FONT_ERROR_FATAL; const FT_Face face = FaceOf(obj); if (!face) return SCE_FONT_ERROR_FATAL; @@ -768,3 +812,27 @@ const Font::SysDriver* FontFt::DriverTable() { const Font::RendererSelection* FontFt::RendererTable() { return &rendererTable; } + +int FontFt::SupportFormats(FontLibrary library, std::initializer_list modules) { + auto* native = static_cast(library); + if (!native) return SCE_FONT_ERROR_INVALID_LIBRARY; + auto lockWord = std::atomic_ref(native->lock_word); + std::uint32_t previous = lockWord.load(std::memory_order_acquire); + for (;;) { + if (native->magic != Font::LIBRARY_MAGIC) return SCE_FONT_ERROR_INVALID_LIBRARY; + if ((previous & Font::LOCK_BIT) == 0 && lockWord.compare_exchange_weak(previous, previous | Font::LOCK_BIT, std::memory_order_acq_rel)) break; + std::this_thread::sleep_for(std::chrono::microseconds(30)); + previous = lockWord.load(std::memory_order_acquire); + } + const auto unlock = [previous](Font::FontLibNative* locked) { + std::atomic_ref(locked->lock_word).store(previous, std::memory_order_release); + }; + const std::unique_ptr guard(native, unlock); + if (native->sys_driver != &driverTable) throw std::runtime_error("FontFt: unsupported library driver"); + const auto* context = static_cast(native->fontset_registry); + if (!context || !context->library) return SCE_FONT_ERROR_INVALID_LIBRARY; + for (const char* module : modules) { + if (!FT_Get_Module(context->library, module)) return SCE_FONT_ERROR_NO_SUPPORT_FORMAT; + } + return SCE_FONT_OK; +} diff --git a/core/libs/prx/libSceFontGs/Export.cpp b/core/libs/prx/libSceFontGs/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceFontGs/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt new file mode 100644 index 0000000000..2292f26da8 --- /dev/null +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -0,0 +1,58 @@ +add_library(libSceFreeType SHARED EXCLUDE_FROM_ALL + Export.cpp + src/GuestObjects.cpp + src/Names.cpp +) +target_include_directories(libSceFreeType PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceFreeType PRIVATE freetype) +target_sources(libSceFreeType PRIVATE src/SizeRequest.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_freetype_request_size_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeRequestSize.cpp) + target_include_directories(guest_freetype_request_size_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_request_size_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_request_size_tests) + add_test(NAME guest_freetype_request_size COMMAND guest_freetype_request_size_tests) + if(WIN32) + set_tests_properties(guest_freetype_request_size PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(libSceFreeType PRIVATE LINKER:--exclude-libs,ALL) +endif() +configure_windows_unwind(libSceFreeType) + +if(BUILD_TESTING) + add_test_executable(guest_freetype_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeType.cpp) + target_include_directories(guest_freetype_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_tests) + add_test(NAME guest_freetype COMMAND guest_freetype_tests) + add_test_executable(guest_freetype_charmap_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeCharmap.cpp) + target_include_directories(guest_freetype_charmap_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_charmap_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_charmap_tests) + add_test(NAME guest_freetype_charmap COMMAND guest_freetype_charmap_tests) + add_test_executable(guest_freetype_names_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeNames.cpp) + target_include_directories(guest_freetype_names_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_names_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_names_tests) + add_test(NAME guest_freetype_names COMMAND guest_freetype_names_tests) + add_test_executable(guest_freetype_sfnt_names_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeSfntNames.cpp) + target_include_directories(guest_freetype_sfnt_names_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_sfnt_names_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_sfnt_names_tests) + add_test(NAME guest_freetype_sfnt_names COMMAND guest_freetype_sfnt_names_tests) + if(WIN32) + set_tests_properties(guest_freetype_sfnt_names PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_freetype_names PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_freetype PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_freetype_charmap PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceFreeType/Export.cpp b/core/libs/prx/libSceFreeType/Export.cpp new file mode 100644 index 0000000000..2962f4a0b9 --- /dev/null +++ b/core/libs/prx/libSceFreeType/Export.cpp @@ -0,0 +1,246 @@ +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/General.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +namespace { + +void FinalizeSlot(void* object) { + auto* host = static_cast(object); + auto* face = static_cast(host->generic.data); + face->rec.glyph = nullptr; + if (face->slot.generic.finalizer) face->slot.generic.finalizer(&face->slot); +} + +void FinalizeSize(void* object) { + auto* host = static_cast(object); + auto* face = static_cast(host->generic.data); + if (face->size.generic.finalizer) face->size.generic.finalizer(&face->size); + face->rec.size = nullptr; +} + +void FinalizeFace(void* object) { + auto* host = static_cast(object); + auto* face = static_cast(host->generic.data); + if (face->rec.generic.finalizer) face->rec.generic.finalizer(&face->rec); +} + +void PublishFace(GuestLibrary* library, std::unique_ptr face, FT_Face host, GuestFaceRec** aface) { + std::unique_ptr hostOwner(host, &FT_Done_Face); + face->library = library; + face->host = host; + SyncFace(*face); + auto* owner = face.get(); + library->faces.push_back(std::move(face)); + host->generic = {owner, FinalizeFace}; + if (host->glyph) host->glyph->generic = {owner, FinalizeSlot}; + if (host->size) host->size->generic = {owner, FinalizeSize}; + *aface = &owner->rec; + hostOwner.release(); +} + +bool ReadFontFile(const char* path, std::vector& bytes) { + std::filesystem::path hostPath; + if (ResolvePathError_nid_no_patch(path, hostPath) != 0) return false; + std::ifstream file(hostPath, std::ios::binary); + if (!file) return false; + bytes.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + return !file.bad(); +} + +std::int64_t OpenFaceIndex(std::int64_t faceIndex) { + if (faceIndex > 0) return faceIndex & 0x7FFFFFFF; + return -static_cast((0 - static_cast(faceIndex)) & 0x7FFFFFFFu); +} + +FT_Pos HostPosition(std::int64_t value) { + if (value < std::numeric_limits::min() || value > std::numeric_limits::max()) { + throw std::runtime_error("FT_Render_Glyph: an outline coordinate outside the host FT_Pos range is not implemented"); + } + return static_cast(value); +} + +void WriteBackOutline(GuestFace& face) { + const FT_GlyphSlot host = face.host->glyph; + if (!host || host->format != FT_GLYPH_FORMAT_OUTLINE) return; + const auto& guest = face.slot.outline; + auto& outline = host->outline; + if (guest.n_points != outline.n_points || guest.n_contours != outline.n_contours || guest.tags != outline.tags || guest.contours != outline.contours || + guest.points != (face.outlinePoints.empty() ? nullptr : face.outlinePoints.data())) { + throw std::runtime_error("FT_Render_Glyph: an outline whose arrays the guest replaced is not implemented"); + } + for (std::size_t index = 0; index < face.outlinePoints.size(); ++index) { + outline.points[index].x = HostPosition(face.outlinePoints[index].x); + outline.points[index].y = HostPosition(face.outlinePoints[index].y); + } + outline.flags = guest.flags; +} + +} + +extern "C" { + +int APS5_VABI FT_Init_FreeType_nid_postfix(GuestLibrary** alibrary) { + if (!alibrary) return FT_Err_Invalid_Argument; + auto library = std::make_unique(); + const FT_Error error = FT_Init_FreeType(&library->host); + if (error != 0) return error; + *alibrary = library.release(); + return 0; +} + +int APS5_VABI FT_Done_FreeType_nid_postfix(GuestLibrary* library) { + const FT_Error error = FT_Done_FreeType(library ? library->host : nullptr); + if (error == 0) delete library; + return error; +} + +int APS5_VABI FT_New_Face_nid_postfix(GuestLibrary* library, const char* filepathname, std::int64_t face_index, GuestFaceRec** aface) { + if (!filepathname) return FT_Err_Invalid_Argument; + const std::int64_t faceIndex = OpenFaceIndex(face_index); + if (!library) return FT_Err_Invalid_Library_Handle; + auto face = std::make_unique(); + if (!ReadFontFile(filepathname, face->bytes)) return FT_Err_Cannot_Open_Resource; + if (face->bytes.size() > static_cast(std::numeric_limits::max())) { + throw std::runtime_error("FT_New_Face: a font file larger than the host FT_Long range is not implemented"); + } + FT_Face host = nullptr; + const FT_Error error = FT_New_Memory_Face(library->host, face->bytes.data(), static_cast(face->bytes.size()), static_cast(faceIndex), aface ? &host : nullptr); + if (error != 0 || !aface) return error; + PublishFace(library, std::move(face), host, aface); + return 0; +} + +int APS5_VABI FT_New_Memory_Face_nid_postfix(GuestLibrary* library, const unsigned char* fileBase, std::int64_t fileSize, std::int64_t faceIndex, GuestFaceRec** aface) { + if (!fileBase) return FT_Err_Invalid_Argument; + if (!library) return FT_Err_Invalid_Library_Handle; + if (fileSize < std::numeric_limits::min() || fileSize > std::numeric_limits::max()) { + throw std::runtime_error("FT_New_Memory_Face: a font buffer size outside the host FT_Long range is not implemented"); + } + auto face = std::make_unique(); + FT_Face host = nullptr; + const FT_Error error = FT_New_Memory_Face(library->host, fileBase, static_cast(fileSize), static_cast(OpenFaceIndex(faceIndex)), aface ? &host : nullptr); + if (error != 0 || !aface) return error; + PublishFace(library, std::move(face), host, aface); + return 0; +} + +int APS5_VABI FT_Done_Face_nid_postfix(GuestFaceRec* face) { + GuestFace* owner = FaceOf(face); + const FT_Error error = FT_Done_Face(owner ? owner->host : nullptr); + if (error == 0) owner->library->faces.remove_if([owner](const auto& entry) { return entry.get() == owner; }); + return error; +} + +int APS5_VABI FT_Set_Pixel_Sizes_nid_postfix(GuestFaceRec* face, std::uint32_t pixel_width, std::uint32_t pixel_height) { + GuestFace* owner = FaceOf(face); + const FT_Error error = FT_Set_Pixel_Sizes(owner ? owner->host : nullptr, pixel_width, pixel_height); + if (owner) SyncSize(*owner); + return error; +} + +int APS5_VABI FT_Set_Char_Size_nid_postfix(GuestFaceRec* face, std::int64_t charWidth, std::int64_t charHeight, + std::uint32_t horizontalResolution, std::uint32_t verticalResolution) { + GuestFace* owner = FaceOf(face); + if (!owner) return FT_Err_Invalid_Face_Handle; + if (!owner->host->size) return FT_Err_Invalid_Size_Handle; + if (!charWidth) charWidth = charHeight; + else if (!charHeight) charHeight = charWidth; + if (charWidth < 64) charWidth = 64; + if (charHeight < 64) charHeight = 64; + if (!horizontalResolution) horizontalResolution = verticalResolution; + else if (!verticalResolution) verticalResolution = horizontalResolution; + if (!horizontalResolution) horizontalResolution = verticalResolution = 72; + const auto maximum = std::numeric_limits::max(); + if (charWidth > (maximum - 36) / horizontalResolution || charHeight > (maximum - 36) / verticalResolution) { + throw std::runtime_error("FT_Set_Char_Size: a character size or resolution outside the host arithmetic range is not implemented"); + } + const FT_Error error = FT_Set_Char_Size(owner->host, static_cast(charWidth), static_cast(charHeight), + horizontalResolution, verticalResolution); + SyncSize(*owner); + return error; +} + +std::uint32_t APS5_VABI FT_Get_Char_Index_nid_postfix(GuestFaceRec* face, std::uint64_t charcode) { + const GuestFace* owner = FaceOf(face); + return FT_Get_Char_Index(owner ? owner->host : nullptr, static_cast(static_cast(charcode))); +} + +int APS5_VABI FT_Load_Glyph_nid_postfix(GuestFaceRec* face, std::uint32_t glyph_index, std::int32_t load_flags) { + GuestFace* owner = FaceOf(face); + const FT_Error error = FT_Load_Glyph(owner ? owner->host : nullptr, glyph_index, load_flags); + if (owner) SyncSlot(*owner); + return error; +} + +int APS5_VABI FT_Load_Char_nid_postfix(GuestFaceRec* face, std::uint64_t charCode, std::int32_t loadFlags) { + GuestFace* owner = FaceOf(face); + const FT_Error error = FT_Load_Char(owner ? owner->host : nullptr, static_cast(static_cast(charCode)), loadFlags); + if (owner) SyncSlot(*owner); + return error; +} + +int APS5_VABI FT_Render_Glyph_nid_postfix(GuestGlyphSlotRec* slot, std::int32_t render_mode) { + GuestFace* owner = FaceOf(slot); + if (owner) WriteBackOutline(*owner); + const FT_Error error = FT_Render_Glyph(owner ? owner->host->glyph : nullptr, static_cast(render_mode)); + if (owner) SyncSlot(*owner); + return error; +} + +int APS5_VABI FT_Select_Charmap_nid_postfix(GuestFaceRec* face, std::int32_t encoding) { + GuestFace* owner = FaceOf(face); + const FT_Error error = FT_Select_Charmap(owner ? owner->host : nullptr, static_cast(encoding)); + if (error == 0) owner->rec.charmap = owner->charmapPointers.at(FT_Get_Charmap_Index(owner->host->charmap)); + return error; +} + +int APS5_VABI FT_Set_Charmap_nid_postfix(GuestFaceRec* face, GuestCharMapRec* charmap) { + GuestFace* owner = FaceOf(face); + if (!owner || !charmap || owner->charmapPointers.empty()) return FT_Set_Charmap(owner ? owner->host : nullptr, nullptr); + for (std::size_t index = 0; index < owner->charmapPointers.size(); ++index) { + if (owner->charmapPointers[index] != charmap) continue; + const FT_Error error = FT_Set_Charmap(owner->host, owner->host->charmaps[index]); + if (error == 0) owner->rec.charmap = charmap; + return error; + } + return FT_Err_Invalid_Argument; +} + +std::uint64_t APS5_VABI FT_Get_First_Char_nid_postfix(GuestFaceRec* face, std::uint32_t* glyphIndex) { + const GuestFace* owner = FaceOf(face); + return FT_Get_First_Char(owner ? owner->host : nullptr, glyphIndex); +} + +std::uint64_t APS5_VABI FT_Get_Next_Char_nid_postfix(GuestFaceRec* face, std::uint64_t charcode, std::uint32_t* glyphIndex) { + const GuestFace* owner = FaceOf(face); + return FT_Get_Next_Char(owner ? owner->host : nullptr, static_cast(static_cast(charcode)), glyphIndex); +} + +void APS5_VABI FT_Set_Transform_nid_postfix(GuestFaceRec* face, const GuestMatrix* matrix, const GuestVector* delta) { + GuestFace* owner = FaceOf(face); + if (!owner) return; + const auto component = [](std::int64_t value) { + if (value < std::numeric_limits::min() || value > std::numeric_limits::max()) { + throw std::runtime_error("FT_Set_Transform: a component outside the host FT_Long range is not implemented"); + } + return static_cast(value); + }; + FT_Matrix nativeMatrix{}; + FT_Vector nativeDelta{}; + if (matrix) nativeMatrix = {component(matrix->xx), component(matrix->xy), component(matrix->yx), component(matrix->yy)}; + if (delta) nativeDelta = {component(delta->x), component(delta->y)}; + FT_Set_Transform(owner->host, matrix ? &nativeMatrix : nullptr, delta ? &nativeDelta : nullptr); +} + +} diff --git a/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp new file mode 100644 index 0000000000..bddbb33246 --- /dev/null +++ b/core/libs/prx/libSceFreeType/include/GuestFreeType.hpp @@ -0,0 +1,199 @@ +#ifndef CORE_LIBS_PRX_LIBSCEFREETYPE_INCLUDE_GUESTFREETYPE_HPP +#define CORE_LIBS_PRX_LIBSCEFREETYPE_INCLUDE_GUESTFREETYPE_HPP + +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include + +namespace FreeType { + +struct GuestGeneric { + void* data; + void (APS5_VABI *finalizer)(void*); +}; + +struct GuestVector { + std::int64_t x; + std::int64_t y; +}; + +struct GuestMatrix { + std::int64_t xx; + std::int64_t xy; + std::int64_t yx; + std::int64_t yy; +}; + +struct GuestBBox { + std::int64_t xMin; + std::int64_t yMin; + std::int64_t xMax; + std::int64_t yMax; +}; + +struct GuestBitmapSize { + std::int16_t height; + std::int16_t width; + std::int64_t size; + std::int64_t x_ppem; + std::int64_t y_ppem; +}; + +struct GuestCharMapRec { + void* face; + std::int32_t encoding; + std::uint16_t platform_id; + std::uint16_t encoding_id; +}; + +struct GuestSfntName { + std::uint16_t platform_id; + std::uint16_t encoding_id; + std::uint16_t language_id; + std::uint16_t name_id; + unsigned char* string; + std::uint32_t string_len; +}; + +struct GuestSizeMetrics { + std::uint16_t x_ppem; + std::uint16_t y_ppem; + std::int64_t x_scale; + std::int64_t y_scale; + std::int64_t ascender; + std::int64_t descender; + std::int64_t height; + std::int64_t max_advance; +}; + +struct GuestSizeRec { + void* face; + GuestGeneric generic; + GuestSizeMetrics metrics; + void* internal; +}; + +struct GuestSizeRequestRec { + std::uint32_t type; + std::int64_t width; + std::int64_t height; + std::uint32_t horiResolution; + std::uint32_t vertResolution; +}; + +struct GuestGlyphMetrics { + std::int64_t width; + std::int64_t height; + std::int64_t horiBearingX; + std::int64_t horiBearingY; + std::int64_t horiAdvance; + std::int64_t vertBearingX; + std::int64_t vertBearingY; + std::int64_t vertAdvance; +}; + +struct GuestBitmap { + std::uint32_t rows; + std::uint32_t width; + std::int32_t pitch; + unsigned char* buffer; + std::uint16_t num_grays; + unsigned char pixel_mode; + unsigned char palette_mode; + void* palette; +}; + +struct GuestOutline { + std::uint16_t n_contours; + std::uint16_t n_points; + GuestVector* points; + unsigned char* tags; + std::uint16_t* contours; + std::int32_t flags; +}; + +struct GuestGlyphSlotRec { + void* library; + void* face; + void* next; + std::uint32_t glyph_index; + GuestGeneric generic; + GuestGlyphMetrics metrics; + std::int64_t linearHoriAdvance; + std::int64_t linearVertAdvance; + GuestVector advance; + std::uint32_t format; + GuestBitmap bitmap; + std::int32_t bitmap_left; + std::int32_t bitmap_top; + GuestOutline outline; + std::uint32_t num_subglyphs; + void* subglyphs; + void* control_data; + std::int64_t control_len; + std::int64_t lsb_delta; + std::int64_t rsb_delta; + void* other; + void* internal; +}; + +struct GuestFaceRec { + std::int64_t num_faces; + std::int64_t face_index; + std::int64_t face_flags; + std::int64_t style_flags; + std::int64_t num_glyphs; + char* family_name; + char* style_name; + std::int32_t num_fixed_sizes; + GuestBitmapSize* available_sizes; + std::int32_t num_charmaps; + GuestCharMapRec** charmaps; + GuestGeneric generic; + GuestBBox bbox; + std::uint16_t units_per_EM; + std::int16_t ascender; + std::int16_t descender; + std::int16_t height; + std::int16_t max_advance_width; + std::int16_t max_advance_height; + std::int16_t underline_position; + std::int16_t underline_thickness; + GuestGlyphSlotRec* glyph; + GuestSizeRec* size; + GuestCharMapRec* charmap; + void* driver; + void* memory; + void* stream; + void* sizes_list_head; + void* sizes_list_tail; + GuestGeneric autohint; + void* extensions; + void* internal; +}; + +static_assert(sizeof(GuestMatrix) == 32 && offsetof(GuestMatrix, xy) == 8 && offsetof(GuestMatrix, yx) == 16 && offsetof(GuestMatrix, yy) == 24); +static_assert(sizeof(GuestGeneric) == 16 && offsetof(GuestGeneric, finalizer) == 8); +static_assert(sizeof(GuestBitmapSize) == 32 && offsetof(GuestBitmapSize, size) == 8); +static_assert(sizeof(GuestCharMapRec) == 16 && offsetof(GuestCharMapRec, platform_id) == 12); +static_assert(sizeof(GuestSfntName) == 24 && offsetof(GuestSfntName, string) == 8 && offsetof(GuestSfntName, string_len) == 16); +static_assert(sizeof(GuestSizeMetrics) == 56 && offsetof(GuestSizeMetrics, x_scale) == 8); +static_assert(sizeof(GuestSizeRec) == 88 && offsetof(GuestSizeRec, metrics) == 24); +static_assert(sizeof(GuestSizeRequestRec) == 32 && offsetof(GuestSizeRequestRec, width) == 8 && offsetof(GuestSizeRequestRec, height) == 16); +static_assert(offsetof(GuestSizeRequestRec, horiResolution) == 24 && offsetof(GuestSizeRequestRec, vertResolution) == 28); +static_assert(sizeof(GuestBitmap) == 40 && offsetof(GuestBitmap, buffer) == 16 && offsetof(GuestBitmap, palette) == 32); +static_assert(sizeof(GuestOutline) == 40 && offsetof(GuestOutline, points) == 8 && offsetof(GuestOutline, flags) == 32); +static_assert(offsetof(GuestGlyphSlotRec, glyph_index) == 0x18 && offsetof(GuestGlyphSlotRec, metrics) == 0x30); +static_assert(offsetof(GuestGlyphSlotRec, advance) == 0x80 && offsetof(GuestGlyphSlotRec, format) == 0x90); +static_assert(offsetof(GuestGlyphSlotRec, bitmap) == 0x98 && offsetof(GuestGlyphSlotRec, bitmap_left) == 0xc0 && offsetof(GuestGlyphSlotRec, bitmap_top) == 0xc4); +static_assert(offsetof(GuestGlyphSlotRec, outline) == 0xc8 && offsetof(GuestGlyphSlotRec, num_subglyphs) == 0xf0); +static_assert(offsetof(GuestGlyphSlotRec, lsb_delta) == 0x110 && offsetof(GuestGlyphSlotRec, internal) == 0x128 && sizeof(GuestGlyphSlotRec) == 0x130); +static_assert(offsetof(GuestFaceRec, num_fixed_sizes) == 56 && offsetof(GuestFaceRec, available_sizes) == 64 && offsetof(GuestFaceRec, charmaps) == 80); +static_assert(offsetof(GuestFaceRec, bbox) == 104 && offsetof(GuestFaceRec, units_per_EM) == 136 && offsetof(GuestFaceRec, underline_thickness) == 150); +static_assert(offsetof(GuestFaceRec, glyph) == 0x98 && offsetof(GuestFaceRec, size) == 0xa0 && offsetof(GuestFaceRec, charmap) == 0xa8); +static_assert(offsetof(GuestFaceRec, autohint) == 216 && offsetof(GuestFaceRec, internal) == 240 && sizeof(GuestFaceRec) == 248); + +} + +#endif diff --git a/core/libs/prx/libSceFreeType/include/GuestObjects.hpp b/core/libs/prx/libSceFreeType/include/GuestObjects.hpp new file mode 100644 index 0000000000..d237147b89 --- /dev/null +++ b/core/libs/prx/libSceFreeType/include/GuestObjects.hpp @@ -0,0 +1,43 @@ +#ifndef CORE_LIBS_PRX_LIBSCEFREETYPE_INCLUDE_GUESTOBJECTS_HPP +#define CORE_LIBS_PRX_LIBSCEFREETYPE_INCLUDE_GUESTOBJECTS_HPP + +#include "prx/libSceFreeType/include/GuestFreeType.hpp" + +#include +#include FT_FREETYPE_H + +#include +#include +#include + +namespace FreeType { + +struct GuestFace; + +struct GuestLibrary { + FT_Library host = nullptr; + std::list> faces; +}; + +struct GuestFace { + GuestFaceRec rec{}; + GuestGlyphSlotRec slot{}; + GuestSizeRec size{}; + GuestLibrary* library = nullptr; + FT_Face host = nullptr; + std::vector bytes; + std::vector fixedSizes; + std::vector charmaps; + std::vector charmapPointers; + std::vector outlinePoints; +}; + +GuestFace* FaceOf(const GuestFaceRec* face); +GuestFace* FaceOf(const GuestGlyphSlotRec* slot); +void SyncFace(GuestFace& face); +void SyncSize(GuestFace& face); +void SyncSlot(GuestFace& face); + +} + +#endif diff --git a/core/libs/prx/libSceFreeType/src/GuestObjects.cpp b/core/libs/prx/libSceFreeType/src/GuestObjects.cpp new file mode 100644 index 0000000000..79662344d2 --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/GuestObjects.cpp @@ -0,0 +1,101 @@ +#include "prx/libSceFreeType/include/GuestObjects.hpp" + +#include + +namespace FreeType { + +GuestFace* FaceOf(const GuestFaceRec* face) { + return face ? static_cast(face->internal) : nullptr; +} + +GuestFace* FaceOf(const GuestGlyphSlotRec* slot) { + return slot ? static_cast(slot->internal) : nullptr; +} + +void SyncFace(GuestFace& face) { + const FT_Face host = face.host; + auto& rec = face.rec; + rec.num_faces = host->num_faces; + rec.face_index = host->face_index; + rec.face_flags = host->face_flags; + rec.style_flags = host->style_flags; + rec.num_glyphs = host->num_glyphs; + rec.family_name = host->family_name; + rec.style_name = host->style_name; + face.fixedSizes.resize(static_cast(host->num_fixed_sizes)); + for (std::size_t index = 0; index < face.fixedSizes.size(); ++index) { + const auto& size = host->available_sizes[index]; + face.fixedSizes[index] = {size.height, size.width, size.size, size.x_ppem, size.y_ppem}; + } + rec.num_fixed_sizes = host->num_fixed_sizes; + rec.available_sizes = face.fixedSizes.empty() ? nullptr : face.fixedSizes.data(); + face.charmaps.resize(static_cast(host->num_charmaps)); + face.charmapPointers.resize(face.charmaps.size()); + rec.charmap = nullptr; + for (std::size_t index = 0; index < face.charmaps.size(); ++index) { + const FT_CharMap charmap = host->charmaps[index]; + face.charmaps[index] = {&rec, static_cast(charmap->encoding), charmap->platform_id, charmap->encoding_id}; + face.charmapPointers[index] = &face.charmaps[index]; + if (charmap == host->charmap) rec.charmap = &face.charmaps[index]; + } + rec.num_charmaps = host->num_charmaps; + rec.charmaps = face.charmapPointers.empty() ? nullptr : face.charmapPointers.data(); + rec.bbox = {host->bbox.xMin, host->bbox.yMin, host->bbox.xMax, host->bbox.yMax}; + rec.units_per_EM = host->units_per_EM; + rec.ascender = host->ascender; + rec.descender = host->descender; + rec.height = host->height; + rec.max_advance_width = host->max_advance_width; + rec.max_advance_height = host->max_advance_height; + rec.underline_position = host->underline_position; + rec.underline_thickness = host->underline_thickness; + rec.glyph = host->glyph ? &face.slot : nullptr; + rec.size = host->size ? &face.size : nullptr; + rec.internal = &face; + SyncSize(face); + SyncSlot(face); +} + +void SyncSize(GuestFace& face) { + face.size.face = &face.rec; + if (!face.host->size) { + face.size.metrics = {}; + return; + } + const auto& metrics = face.host->size->metrics; + face.size.metrics = {metrics.x_ppem, metrics.y_ppem, metrics.x_scale, metrics.y_scale, metrics.ascender, metrics.descender, metrics.height, metrics.max_advance}; +} + +void SyncSlot(GuestFace& face) { + auto& slot = face.slot; + slot.library = face.library; + slot.face = &face.rec; + slot.next = nullptr; + slot.internal = &face; + const FT_GlyphSlot host = face.host->glyph; + if (!host) return; + slot.glyph_index = host->glyph_index; + const auto& metrics = host->metrics; + slot.metrics = {metrics.width, metrics.height, metrics.horiBearingX, metrics.horiBearingY, metrics.horiAdvance, metrics.vertBearingX, metrics.vertBearingY, metrics.vertAdvance}; + slot.linearHoriAdvance = host->linearHoriAdvance; + slot.linearVertAdvance = host->linearVertAdvance; + slot.advance = {host->advance.x, host->advance.y}; + slot.format = static_cast(host->format); + const auto& bitmap = host->bitmap; + slot.bitmap = {bitmap.rows, bitmap.width, bitmap.pitch, bitmap.buffer, bitmap.num_grays, bitmap.pixel_mode, bitmap.palette_mode, bitmap.palette}; + slot.bitmap_left = host->bitmap_left; + slot.bitmap_top = host->bitmap_top; + const auto& outline = host->outline; + face.outlinePoints.resize(outline.n_points); + for (std::size_t index = 0; index < face.outlinePoints.size(); ++index) face.outlinePoints[index] = {outline.points[index].x, outline.points[index].y}; + slot.outline = {outline.n_contours, outline.n_points, face.outlinePoints.empty() ? nullptr : face.outlinePoints.data(), outline.tags, outline.contours, outline.flags}; + slot.num_subglyphs = host->num_subglyphs; + slot.subglyphs = nullptr; + slot.control_data = host->control_data; + slot.control_len = host->control_len; + slot.lsb_delta = host->lsb_delta; + slot.rsb_delta = host->rsb_delta; + slot.other = host->other; +} + +} diff --git a/core/libs/prx/libSceFreeType/src/Names.cpp b/core/libs/prx/libSceFreeType/src/Names.cpp new file mode 100644 index 0000000000..23d3b8ad8f --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/Names.cpp @@ -0,0 +1,55 @@ +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include FT_SFNT_NAMES_H + +#include + +using namespace FreeType; + +extern "C" { + +int APS5_VABI FT_Get_Glyph_Name_nid_postfix(GuestFaceRec* face, std::uint32_t glyphIndex, void* buffer, std::uint32_t bufferSize) { + const GuestFace* owner = FaceOf(face); + const FT_Face host = owner ? owner->host : nullptr; + if (!host || !buffer || bufferSize == 0) return FT_Get_Glyph_Name(host, glyphIndex, buffer, bufferSize); + GuestArena::HostWrite write(buffer, bufferSize); + if (static_cast(glyphIndex) >= host->num_glyphs) { + static_cast(buffer)[0] = '\0'; + return FT_Err_Invalid_Glyph_Index; + } + return FT_Get_Glyph_Name(host, glyphIndex, buffer, bufferSize); +} + +std::uint32_t APS5_VABI FT_Get_Name_Index_nid_postfix(GuestFaceRec* face, const char* glyphName) { + const GuestFace* owner = FaceOf(face); + return FT_Get_Name_Index(owner ? owner->host : nullptr, glyphName); +} + +const char* APS5_VABI FT_Get_Postscript_Name_nid_postfix(GuestFaceRec* face) { + const GuestFace* owner = FaceOf(face); + return FT_Get_Postscript_Name(owner ? owner->host : nullptr); +} + +std::uint32_t APS5_VABI FT_Get_Sfnt_Name_Count_nid_postfix(GuestFaceRec* face) { + const GuestFace* owner = FaceOf(face); + return FT_Get_Sfnt_Name_Count(owner ? owner->host : nullptr); +} + +int APS5_VABI FT_Get_Sfnt_Name_nid_postfix(GuestFaceRec* face, std::uint32_t index, GuestSfntName* name) { + const GuestFace* owner = FaceOf(face); + FT_SfntName native{}; + const FT_Error error = FT_Get_Sfnt_Name(owner ? owner->host : nullptr, index, name ? &native : nullptr); + if (!error) { + name->platform_id = native.platform_id; + name->encoding_id = native.encoding_id; + name->language_id = native.language_id; + name->name_id = native.name_id; + name->string = native.string; + name->string_len = native.string_len; + } + return error; +} + +} diff --git a/core/libs/prx/libSceFreeType/src/SizeRequest.cpp b/core/libs/prx/libSceFreeType/src/SizeRequest.cpp new file mode 100644 index 0000000000..6ef487fa6a --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/SizeRequest.cpp @@ -0,0 +1,24 @@ +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/General.hpp" + +#include + +using namespace FreeType; + +extern "C" { + +int APS5_VABI FT_Request_Size_nid_postfix(GuestFaceRec* face, const GuestSizeRequestRec* request) { + GuestFace* owner = FaceOf(face); + if (!owner || !owner->host->size || !request) return FT_Request_Size(owner ? owner->host : nullptr, nullptr); + if (request->width < 0 || request->height < 0 || request->type >= FT_SIZE_REQUEST_TYPE_MAX) return FT_Err_Invalid_Argument; + if (request->width > std::numeric_limits::max() || request->height > std::numeric_limits::max()) { + NotImplemented_nid_no_patch(__func__); + } + FT_Size_RequestRec hostRequest{static_cast(request->type), static_cast(request->width), + static_cast(request->height), request->horiResolution, request->vertResolution}; + const FT_Error error = FT_Request_Size(owner->host, &hostRequest); + SyncSize(*owner); + return error; +} + +} diff --git a/core/libs/prx/libSceFreeTypeOl/Export.cpp b/core/libs/prx/libSceFreeTypeOl/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceFreeTypeOl/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceFreeTypeOptOl/Export.cpp b/core/libs/prx/libSceFreeTypeOptOl/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceFreeTypeOptOl/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceFreeTypeOt/Export.cpp b/core/libs/prx/libSceFreeTypeOt/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceFreeTypeOt/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceFsInternalForVsh/Export.cpp b/core/libs/prx/libSceFsInternalForVsh/Export.cpp new file mode 100644 index 0000000000..4a393cfbb7 --- /dev/null +++ b/core/libs/prx/libSceFsInternalForVsh/Export.cpp @@ -0,0 +1,888 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceBdSchedCancelBackgroundCopyRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBdSchedCancelPrioritizedBackgroundCopyRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBdSchedConfigure() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBdSchedConfigure2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBdSchedDeconfigure() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBdSchedGetBackgroundCopyRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBdSchedGetExtentMap() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBdSchedGetPrioritizedBackgroundCopyRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBdSchedGetState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBdSchedSetBackgroundCopyRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBdSchedSetDiscIdentity() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBdSchedSetPrioritizedBackgroundCopyRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBdSchedStartGame() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBdSchedStartRipping() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBdSchedStopScheduling() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsBfsCheckCleanFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsBindApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsBindPackages() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsCheckFormat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsCheckSingleUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsCreateExfatPartition() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsCreatePfsSaveDataImage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsCreatePfsTrophyDataImage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsCreatePprPfsTrophyDataImage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsDeviceAlignedPread() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsDeviceAlignedPwrite() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsDevpfsA53IocGetBuildMetadataStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsDevpfsCancelWaitEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsDevpfsCreateForVSH() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsDevpfsDeleteForVSH() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsDevpfsDeregisterEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsDevpfsEventComplete() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsDevpfsEventResponseComplete() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsDevpfsFetchMetaEntry() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsDevpfsFsck() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsDevpfsInitFetchMetaEntryCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsDevpfsMountForFSMP() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsDevpfsRegisterEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsDevpfsWaitEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsDrfsRegisterSocket() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsDrfsResumeSocket() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsErriCtl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsErriGetState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsExfatMkfsExternalHdd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsExfatfsMkfs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsExtUSBSchedDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsExtUSBSchedInsert() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsExtUSBSchedInsertForHDD() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsExternalStorageDeconfigure() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsExternalStorageGetRawDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsExternalStorageInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsExternalStorageVerifyMetadata() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsGetAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsGetClusterStatistics() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsGetDeviceSectorsize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsGetFsMetadata() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsGetFsMetadataWithProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsGetSmrInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsGschedCtl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsISSchedConfigCurrentThread() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsISSchedConfigDescriptor() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsISSchedConfigThread() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsISSchedMapStat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsISSchedResultToPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsISSchedResultToSlot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsISSchedStatCurrentThread() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsISSchedStatDescriptor() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsISSchedStatThread() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsISSchedUnmapStat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsInitCreatePfsSaveDataOpt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsInitCreatePfsTrophyDataOpt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsInitMountDownloadDataOpt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsInitMountGamePkgOpt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsInitMountLwfsCompatOpt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsInitMountLwfsOpt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsInitMountPprPkgOpt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsInitMountSaveDataOpt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsInitMountTrophyDataOpt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsInitUmountDownloadDataOpt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsInitUmountGamePkgOpt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsInitUmountLwfsCompatOpt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsInitUmountLwfsOpt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsInitUmountPprPkgOpt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsInitUmountSaveDataOpt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsInitUmountTrophyDataOpt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsInitWorkspaceFileMetadataParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsLvdAttach() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsLvdAttach2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsLvdAttachPartiallyCryptedDownloadData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsLvdAttachSingleDefaultImage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsLvdAttachSingleImage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsLvdDetach() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsLvdDisableLayer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsLvdEnableLayer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsLvdSetDiscIdentity() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsLvdWaitResumed() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsMlfsMount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsMlfsMountWithFlags() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsMlfsUmount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsMountDownloadData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsMountGamePkg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsMountLwfs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsMountLwfsCompat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsMountPartiallyCryptedDownloadData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsMountPprPkg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsMountSaveData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsMountTrophyData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsNsfsAddOverlayToNamespace() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsNsfsMountWithFlags() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsNsfsSetGlobalExclude() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsNsfsUmount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsPartitionTrim() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsReserveCluster() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsSetAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsSetClusterCacheSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsSetFileReservedTotal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsSetSystemReserveSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsSetWorkspaceFileHash() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsStatWorkspace() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsSysStorageGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsTransactionCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsTransactionSaveDataInitMountOpt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsTransactionSaveDataMount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsTransactionStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsTrophyImageError() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsAllocateAdditionalContents() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsAllocateAligned() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsAllocateDownloadData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsAllocateGameImage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsAllocatePatchImage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsAllocateSaveData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsAllocateTrophyData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsCheckCleanFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsCheckFixedCylinderGroupSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsCollectTelemetry() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsCollectTelemetry2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsDirectWrite() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsEstimateZoneCapacityForUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsExtendFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsFsck() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsFsckWithMM() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsGetExtentMap() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsGrowLwfs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsGrowfs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsHideBlocksFromUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsInitalizeSparseFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsLargewriteFsck() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsLargewriteFsckWithMM() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsMkLwfs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsMkfs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsMkfsForExternalHDD() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsMkfsForSystem() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsMkfsWithFixedCylinderGroupSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsReallocateSparseFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsRecryptBlocks() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsRecryptGetState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsRecryptIsUnfinished() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsRecryptSyncMetadata() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsSeekHoleInFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUfsStopOperation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUmountDownloadData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUmountGamePkg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUmountLwfs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUmountLwfsCompat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUmountSaveData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUmountTrophyData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUmountWorkspace() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUnbindApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsUnbindPackages() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsWsCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsWsDelete() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsWsFetchFileMetadata() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsWsFsck() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsWsGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsWsMount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFsWsUmount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceGLSlimVSH/Export.cpp b/core/libs/prx/libSceGLSlimVSH/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceGLSlimVSH/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceGameCustomDataDialog/Export.cpp b/core/libs/prx/libSceGameCustomDataDialog/Export.cpp new file mode 100644 index 0000000000..5b647ad000 --- /dev/null +++ b/core/libs/prx/libSceGameCustomDataDialog/Export.cpp @@ -0,0 +1,31 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceGameCustomDataDialogGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameCustomDataDialogInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameCustomDataDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameCustomDataDialogTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameCustomDataDialogUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceGameLiveStreaming/Export.cpp b/core/libs/prx/libSceGameLiveStreaming/Export.cpp index b28b535c8e..891a864574 100644 --- a/core/libs/prx/libSceGameLiveStreaming/Export.cpp +++ b/core/libs/prx/libSceGameLiveStreaming/Export.cpp @@ -4,6 +4,18 @@ #include "prx/libc/include/General.hpp" #include "prx/libSceUserService/UserService.hpp" #include +#include +#include +#include + +static std::mutex g_mutex; +static bool g_initialized = false; +static constexpr int ErrorInvalidArgument = static_cast(0x80A00002); +static constexpr int ErrorAlreadyInitialized = static_cast(0x80A00003); +static constexpr int ErrorNotInitialized = static_cast(0x80A00004); +static constexpr std::size_t ProgramInfoSize = 0x134C; + +struct GameLiveStreamingProgramInfo; struct GameLiveStreamingStatus2 { std::int32_t userId; @@ -19,25 +31,253 @@ static_assert(sizeof(GameLiveStreamingStatus2) == 72); extern "C" { -int APS5_VABI sceGameLiveStreamingInitialize(size_t heap_size) { - if (heap_size == 0) APS5_INVALID_ARG_EX; +int APS5_VABI sceGameLiveStreamingInitialize(size_t heapSize) { + if (heapSize != 0x4000) return ErrorInvalidArgument; + std::lock_guard lock(g_mutex); + if (g_initialized) return ErrorAlreadyInitialized; + g_initialized = true; return 0; } int APS5_VABI sceGameLiveStreamingTerminate(void) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; + g_initialized = false; return 0; } int APS5_VABI sceGameLiveStreamingGetCurrentStatus2(GameLiveStreamingStatus2* status) { - if (status == nullptr) APS5_INVALID_ARG_EX; + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; + if (status == nullptr) return ErrorInvalidArgument; std::memset(status, 0, sizeof(*status)); status->userId = USER_SERVICE_USER_ID_INVALID; return 0; } -int APS5_VABI sceGameLiveStreamingGetProgramInfo() { +int APS5_VABI sceGameLiveStreamingGetProgramInfo(GameLiveStreamingProgramInfo* info) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return ErrorNotInitialized; + if (info == nullptr) return ErrorInvalidArgument; + std::memset(info, 0, ProgramInfoSize); + return 0; +} + +int APS5_VABI sceGameLiveStreamingScreenInitialize(void) { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceGameLiveStreamingScreenInitializeSeparateModeParameter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingScreenTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingScreenCloseSeparateMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingScreenConfigureSeparateMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingScreenOpenSeparateMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingScreenSetMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingPermitLiveStreaming(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingSetMaxBitrate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingGetCurrentStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingEnableSocialFeedback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessagesCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingGetSocialFeedbackMessages(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingPermitServerSideRecording(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingSetMetadata(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingSetSpoilerTag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingClearSpoilerTag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommands(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingClearPresetSocialFeedbackCommands(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingSetPresetSocialFeedbackCommandsDescription(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingClearSocialFeedbackMessages(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingPostSocialMessage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingSetCameraFrameSetting(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingSetServiceProviderPermission(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingSetDefaultServiceProviderPermission(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingSetInvitationSessionId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingStartGenerateStandbyScreenResource(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingStopGenerateStandbyScreenResource(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingRegisterCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingUnregisterCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingCheckCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingSetStandbyScreenResource(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingGetCurrentBroadcastScreenLayout(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingLaunchLiveViewer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingLaunchLiveViewerA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingSetGuardAreas(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingStartSocialFeedbackMessageFiltering(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingStopSocialFeedbackMessageFiltering(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingApplySocialFeedbackMessageFilter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingSetLinkCommentPreset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingStartDebugBroadcast(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingStopDebugBroadcast(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingEnableLiveStreaming(bool isEnableLiveStreaming) { + std::lock_guard lock(g_mutex); + if (!g_initialized) throw std::logic_error(std::string(__func__) + ": not initialized"); + (void)isEnableLiveStreaming; + return 0; +} + } diff --git a/core/libs/prx/libSceGameUpdate/Export.cpp b/core/libs/prx/libSceGameUpdate/Export.cpp index 4c0d0bb575..f8527db000 100644 --- a/core/libs/prx/libSceGameUpdate/Export.cpp +++ b/core/libs/prx/libSceGameUpdate/Export.cpp @@ -1,68 +1,98 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -// Update checks need the network, which is not available; requests fail so the game skips the check. -static constexpr int SCE_GAME_UPDATE_ERROR_OFFLINE = static_cast(0x80D50001); static constexpr int SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED = static_cast(0x80412801); +static constexpr int SCE_GAME_UPDATE_ERROR_ALREADY_INITIALIZED = static_cast(0x80412802); static constexpr int SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT = static_cast(0x80412803); static constexpr int SCE_GAME_UPDATE_ERROR_INVALID_SIZE = static_cast(0x80412804); +static constexpr int SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND = static_cast(0x80412805); -static std::atomic g_initialized{false}; +static_assert(sizeof(GameUpdateCheckParam) == 0x30); +static_assert(sizeof(GameUpdateCheckResult) == 0x30); + +static std::atomic initialized{false}; + +static int checkRequestId(int requestId) { + if (requestId <= 0) { + return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; + } + if (!initialized.load()) { + return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; + } + return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; +} + +static bool hasNonZeroReserved(const GameUpdateCheckParam* param) { + for (std::uint32_t word : param->reserved) { + if (word != 0) { + return true; + } + } + return false; +} extern "C" { -int APS5_VABI sceGameUpdateAbortRequest(int request_id) { - (void)request_id; - return 0; +int APS5_VABI sceGameUpdateAbortRequest(int requestId) { + return checkRequestId(requestId); } -int APS5_VABI sceGameUpdateCheck(int request_id, const GameUpdateCheckParam* param, GameUpdateCheckResult* result) { - (void)request_id; - (void)param; - (void)result; - return SCE_GAME_UPDATE_ERROR_OFFLINE; +int APS5_VABI sceGameUpdateCheck(int requestId, const GameUpdateCheckParam* param, GameUpdateCheckResult* result) { + if (requestId <= 0) { + return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; + } + if (param == nullptr || result == nullptr) { + return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; + } + if (param->size != sizeof(GameUpdateCheckParam) || result->size != sizeof(GameUpdateCheckResult)) { + return SCE_GAME_UPDATE_ERROR_INVALID_SIZE; + } + if (param->option != 0 || hasNonZeroReserved(param)) { + return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; + } + return checkRequestId(requestId); } int APS5_VABI sceGameUpdateCreateRequest(void) { - return SCE_GAME_UPDATE_ERROR_OFFLINE; -} - -int APS5_VABI sceGameUpdateDeleteRequest(int request_id) { - (void)request_id; - return 0; -} - -int APS5_VABI sceGameUpdateGetAddcontLatestVersion(uint32_t service_label, const void* entitlement_label, GameUpdateAddcontVersionInfo* info) { - (void)service_label; - if (!g_initialized.load()) { + if (!initialized.load()) { return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; } + throw std::runtime_error("sceGameUpdateCreateRequest: patch service request creation is not implemented"); +} + +int APS5_VABI sceGameUpdateDeleteRequest(int requestId) { + return checkRequestId(requestId); +} + +int APS5_VABI sceGameUpdateGetAddcontLatestVersion(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, GameUpdateAddcontVersionInfo* info) { + (void)serviceLabel; if (info == nullptr) { return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } - if (entitlement_label == nullptr) { - NotImplemented_nid_no_patch(__func__); + if (entitlementLabel == nullptr) { + return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } - if (info->size < sizeof(GameUpdateAddcontVersionInfo)) { - return SCE_GAME_UPDATE_ERROR_INVALID_SIZE; + if (!initialized.load()) { + return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; } - const std::size_t size = info->size; - *info = {}; - info->size = size; - info->found = false; - return 0; + throw std::runtime_error("sceGameUpdateGetAddcontLatestVersion: entitlement version lookup is not implemented"); } int APS5_VABI sceGameUpdateInitialize(void) { - g_initialized.store(true); + if (initialized.exchange(true)) { + return SCE_GAME_UPDATE_ERROR_ALREADY_INITIALIZED; + } return 0; } int APS5_VABI sceGameUpdateTerminate(void) { - g_initialized.store(false); + if (!initialized.exchange(false)) { + return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; + } return 0; } diff --git a/core/libs/prx/libSceGic/Export.cpp b/core/libs/prx/libSceGic/Export.cpp new file mode 100644 index 0000000000..6fff8198ce --- /dev/null +++ b/core/libs/prx/libSceGic/Export.cpp @@ -0,0 +1,13 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceGicGetValue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceGifParser/Export.cpp b/core/libs/prx/libSceGifParser/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceGifParser/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceGnmDriverCompat1/Export.cpp b/core/libs/prx/libSceGnmDriverCompat1/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceGnmDriverCompat1/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp new file mode 100644 index 0000000000..9b7f2684c4 --- /dev/null +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -0,0 +1,558 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int Failure = static_cast(0x8EEE00FFu); +constexpr int NoInterface = static_cast(0x80000000u); +constexpr int ValidationNotEnabled = static_cast(0x80D13FFFu); + +} // namespace + +extern "C" { + +int APS5_VABI sceGnmBeginWorkload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmComputeWaitOnAddress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmComputeWaitSemaphore(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmCreateWorkloadStream(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDebugModuleReset(void) { + return Failure; +} + +int APS5_VABI sceGnmDebugReset(void) { + return Failure; +} + +int APS5_VABI sceGnmDestroyWorkloadStream(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDingDongForWorkload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDispatchDirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDispatchIndirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDispatchIndirectOnMec(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndexAuto(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndexIndirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndexIndirectCountMulti(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndexIndirectMulti(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndexMultiInstanced(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndexOffset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndirectCountMulti(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndirectMulti(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawInitDefaultHardwareState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawInitDefaultHardwareState200(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawInitDefaultHardwareState350(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawInitToDefaultContextState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawInitToDefaultContextState400(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawInitToDefaultContextStateInternalCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawInitToDefaultContextStateInternalSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawOpaqueAuto(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterface(void) { + return NoInterface; +} + +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForGpuDebugger(void) { + return NoInterface; +} + +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForReplay(void) { + return NoInterface; +} + +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForResourceRegistration(void) { + return NoInterface; +} + +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForValidation(void) { + return NoInterface; +} + +int APS5_VABI sceGnmDriverInternalVirtualQuery(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDriverTraceInProgress(void) { + return 0; +} + +int APS5_VABI sceGnmEndWorkload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmFindResources(void) { + return Failure; +} + +int APS5_VABI sceGnmFindResourcesPublic(void) { + return Failure; +} + +int APS5_VABI sceGnmGetCoredumpAddress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetCoredumpMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetCoredumpProtectionFaultTimestamp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetDbgGcHandle(void) { + return -1; +} + +int APS5_VABI sceGnmGetGpuBlockStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetGpuCoreClockFrequency(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetGpuInfoStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetLastWaitedAddress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetNumTcaUnits(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetOffChipTessellationBufferSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetOwnerName(void) { + return Failure; +} + +int APS5_VABI sceGnmGetPhysicalCounterFromVirtualized(void) { + return Failure; +} + +int APS5_VABI sceGnmGetProtectionFaultTimeStamp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetResourceBaseAddressAndSizeInBytes(void) { + return Failure; +} + +int APS5_VABI sceGnmGetResourceName(void) { + return Failure; +} + +int APS5_VABI sceGnmGetResourceRegistrationBuffers(void) { + return Failure; +} + +int APS5_VABI sceGnmGetResourceShaderGuid(void) { + return Failure; +} + +int APS5_VABI sceGnmGetResourceType(void) { + return Failure; +} + +int APS5_VABI sceGnmGetResourceUserData(void) { + return Failure; +} + +int APS5_VABI sceGnmGetShaderProgramBaseAddress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetShaderStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGpuPaDebugEnter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGpuPaDebugLeave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmInsertDingDongMarker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmInsertThreadTraceMarker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmIsCoredumpValid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmLogicalTcaUnitToPhysical(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmMapComputeQueueWithPriority(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmPaDisableFlipCallbacks(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmPaEnableFlipCallbacks(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmPaHeartbeat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmQueryResourceRegistrationUserMemoryRequirements(void) { + return Failure; +} + +int APS5_VABI sceGnmRaiseUserExceptionEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmRegisterGdsResource(void) { + return Failure; +} + +int APS5_VABI sceGnmRegisterGnmLiveCallbackConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmRegisterOwner(void) { + return Failure; +} + +int APS5_VABI sceGnmRegisterOwnerForSystem(void) { + return Failure; +} + +int APS5_VABI sceGnmRegisterResource(void) { + return Failure; +} + +int APS5_VABI sceGnmRequestFlipAndSubmitDoneForWorkload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmResetVgtControl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSdmaClose(void) { + return Failure; +} + +int APS5_VABI sceGnmSdmaConstFill(void) { + return Failure; +} + +int APS5_VABI sceGnmSdmaCopyLinear(void) { + return Failure; +} + +int APS5_VABI sceGnmSdmaCopyTiled(void) { + return Failure; +} + +int APS5_VABI sceGnmSdmaCopyWindow(void) { + return Failure; +} + +int APS5_VABI sceGnmSdmaFlush(void) { + return Failure; +} + +int APS5_VABI sceGnmSdmaGetMinCmdSize(void) { + return Failure; +} + +int APS5_VABI sceGnmSdmaOpen(void) { + return Failure; +} + +int APS5_VABI sceGnmSetCsShaderWithModifier(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetEmbeddedPsShader(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetEmbeddedVsShader(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetPsShader350(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetResourceRegistrationUserMemory(void) { + return Failure; +} + +int APS5_VABI sceGnmSetResourceUserData(void) { + return Failure; +} + +int APS5_VABI sceGnmSetSpiEnableSqCounters(void) { + return Failure; +} + +int APS5_VABI sceGnmSetSpiEnableSqCountersForUnitInstance(void) { + return Failure; +} + +int APS5_VABI sceGnmSetVgtControl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetWaveLimitMultiplier(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetWaveLimitMultipliers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSubmitAndFlipCommandBuffersForWorkload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSubmitCommandBuffersForWorkload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmUnregisterAllResourcesForOwner(void) { + return Failure; +} + +int APS5_VABI sceGnmUnregisterOwnerAndResources(void) { + return Failure; +} + +int APS5_VABI sceGnmUnregisterResource(void) { + return Failure; +} + +int APS5_VABI sceGnmUpdateGsShader(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmUpdateHsShader(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmUpdatePsShader(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmUpdatePsShader350(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmUpdateVsShader(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateDisableDiagnostics(void) { + return ValidationNotEnabled; +} + +int APS5_VABI sceGnmValidateDisableDiagnostics2(void) { + return ValidationNotEnabled; +} + +int APS5_VABI sceGnmValidateDispatchCommandBuffers(void) { + return ValidationNotEnabled; +} + +int APS5_VABI sceGnmValidateDrawCommandBuffers(void) { + return ValidationNotEnabled; +} + +int APS5_VABI sceGnmValidateGetDiagnosticInfo(void) { + return ValidationNotEnabled; +} + +int APS5_VABI sceGnmValidateGetDiagnostics(void) { + return ValidationNotEnabled; +} + +int APS5_VABI sceGnmValidateGetVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateOnSubmitEnabled(void) { + return 0; +} + +int APS5_VABI sceGnmValidateResetState(void) { + return ValidationNotEnabled; +} + +int APS5_VABI sceGnmValidationRegisterMemoryCheckCallback(void) { + return ValidationNotEnabled; +} + +} diff --git a/core/libs/prx/libSceGpuCapture/Export.cpp b/core/libs/prx/libSceGpuCapture/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceGpuCapture/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceGpuTrace/Export.cpp b/core/libs/prx/libSceGpuTrace/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceGpuTrace/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceGvMp4Parser/Export.cpp b/core/libs/prx/libSceGvMp4Parser/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceGvMp4Parser/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceHand/Export.cpp b/core/libs/prx/libSceHand/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceHand/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceHandTracker/Export.cpp b/core/libs/prx/libSceHandTracker/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceHandTracker/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceHdrScopes/Export.cpp b/core/libs/prx/libSceHdrScopes/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceHdrScopes/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceHeadTracker/Export.cpp b/core/libs/prx/libSceHeadTracker/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceHeadTracker/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceHidControl/Export.cpp b/core/libs/prx/libSceHidControl/Export.cpp new file mode 100644 index 0000000000..c3bfd0d3ed --- /dev/null +++ b/core/libs/prx/libSceHidControl/Export.cpp @@ -0,0 +1,210 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +int APS5_VABI sceHidControlEnableMicBeamForming(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetAllVibrationMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetVibrationTriggerEffectWeakForce(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlIsForceUpdateNeeded(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetRemoteMergeProcess(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetCollaboInputSettingOverrideProcess(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetForceAdvancedVibrationMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlPswUpdateIsUpdateNeeded(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlPswUpdateIsUpdateNeededForMultiDevice(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlPswUpdateIsReady(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlPswUpdateStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlPswUpdateGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlPswUpdateSendData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlPswUpdateSendAppendixData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlPswUpdateGetReconnectStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlPswUpdateGetDeviceFirmwareUpdateTelemetryData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetPswState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetPswBatteryState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetPswDeviceName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetPswDeviceName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetPswConsoleMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetPswSpeakerVolume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetPswSidetoneLevel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetPswSidetoneLevel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetPswEqPresetSelection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetPswEqPresetSelection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetPswEqPresetName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetPswEqPresetName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetPswEqBandValues(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetPswEqBandValues(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetPswAllEqBandValues(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetPswResetSettings(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetPswNetworkLocale(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetPswNetworkLocale(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetPswDeviceNameForNearby(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlConnectPswDeviceForNearby(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetPswDeviceInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetPswCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetPswCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetPswDeviceIdFromIdxAdd(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceHmd/CMakeLists.txt b/core/libs/prx/libSceHmd/CMakeLists.txt index d5a15e9ca8..5bf180e478 100644 --- a/core/libs/prx/libSceHmd/CMakeLists.txt +++ b/core/libs/prx/libSceHmd/CMakeLists.txt @@ -1,11 +1,13 @@ add_library(libSceHmd SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceHmd PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceHmd PRIVATE libc) configure_windows_unwind(libSceHmd) if(BUILD_TESTING) add_test_executable(guest_hmd_tests ${LIBS_INCLUDE_DIR}/tests/GuestHmd.cpp) target_include_directories(guest_hmd_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_hmd_tests PRIVATE libSceHmd) + configure_windows_unwind(guest_hmd_tests) add_test(NAME guest_hmd COMMAND guest_hmd_tests) if(WIN32) set_tests_properties(guest_hmd PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index 36c2ca3c06..d32073ea63 100644 --- a/core/libs/prx/libSceHmd/Export.cpp +++ b/core/libs/prx/libSceHmd/Export.cpp @@ -1,6 +1,62 @@ #include "Hmd.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/General.hpp" #include +#include +#include +#include +#include + +struct HmdDistortionCommandParameter; +struct HmdWideNearDistortionCommandParameter; +struct Hmd2dVrCommandParameter; + +struct HmdCommandBuffer { + const std::uint32_t* commands; + std::uint32_t dwordCount; +}; + +static_assert(sizeof(HmdCommandBuffer) == 16); +static_assert(offsetof(HmdCommandBuffer, dwordCount) == 8); + +struct HmdReprojectionFrameParameter; +struct HmdReprojectionWideNearParameter; +struct HmdReprojection2dVrParameter; +struct HmdReprojectionPose; +struct HmdReprojectionOverlayParameter; +struct HmdReprojectionSubmissionParameter; +struct HmdReprojectionInitializeParameter; +struct HmdReprojectionMultilayerParameter; +struct HmdDeviceInformation; +struct HmdDistortionParameters; +struct HmdInertialSensorData; +struct HmdStatusReport; + +struct HmdFieldOfView { + float angles[4]; +}; + +struct HmdAssemblyError { + float offsets[4]; +}; + +static_assert(sizeof(HmdFieldOfView) == 16); +static_assert(sizeof(HmdAssemblyError) == 16); + +struct HmdVr2dData; +struct HmdPuPositionParameter; +struct HmdOpticalParameter; +struct HmdCrashDumpInfo; +struct HmdScreenOutputParameter; +struct HmdScreenFadeParameter; +struct HmdScreenStatus; +struct HmdCaptureInitializeParameter; +struct HmdCaptureInfo; +struct HmdLiveCaptureParameter; +struct HmdReprojectionDebugInfo; +struct HmdReprojectionMultilayerDebugInfo; +struct HmdReprojectionCallbackParameter; namespace { @@ -13,8 +69,94 @@ std::int32_t Initialize(const Hmd::InitializeParam* param) { return initialized.exchange(true) ? Hmd::AlreadyInitialized : 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 CommandQueryHandle(std::int32_t handle, const char* query) { + if (handle != 0) return Hmd::InvalidHandle; + if (initialized.load()) NotImplemented_nid_no_patch((std::string(query) + " with handle 0 after sceHmdInitialize").c_str()); + if (CompiledSdkVersion() >= Hmd::StartupStateSdkVersion) NotImplemented_nid_no_patch((std::string(query) + " with handle 0 before sceHmdInitialize from SDK 4.50").c_str()); + return Hmd::NotInitialized; +} + +template +T ParameterWord(const Hmd2dVrCommandParameter* parameter, std::size_t offset) { + T value; + std::memcpy(&value, reinterpret_cast(parameter) + offset, sizeof(value)); + return value; +} + +std::int32_t Vr2dCommandArguments(const Hmd2dVrCommandParameter* parameter, const HmdCommandBuffer* commandBuffer) { + if (!parameter || !commandBuffer) return Hmd::ParameterNull; + for (const std::size_t offset : {0x00u, 0x08u, 0x20u}) { + if (ParameterWord(parameter, offset) == 0) return Hmd::ParameterNull; + } + if (ParameterWord(parameter, 0x34) != 0 || ParameterWord(parameter, 0x38) != 0) return Hmd::ParameterInvalid; + return 0; +} + +} + +struct HmdEyeDistances { + std::uint32_t left; + std::uint32_t right; +}; + +struct HmdEyeOffset { + float x; + float y; + float z; +}; + +struct HmdMirroringAspect { + std::uint16_t reserved0; + std::int16_t aspect; + std::uint16_t reserved1; + std::uint16_t reserved2; +}; + +struct HmdMirroringAspectDebug { + std::uint16_t values[5]; + std::uint8_t mode; +}; + +static_assert(sizeof(HmdMirroringAspectDebug) == 12); +static_assert(offsetof(HmdMirroringAspectDebug, mode) == 10); +static_assert(sizeof(HmdMirroringAspect) == 8); +static_assert(offsetof(HmdMirroringAspect, aspect) == 2); + +using HmdEventCallback = void (APS5_VABI *)(std::uint32_t events); + +struct HmdDfuData { + const void* data; + std::uint32_t size; + std::uint32_t mode; +}; + +static_assert(sizeof(HmdDfuData) == 16); +static_assert(offsetof(HmdDfuData, size) == 8); +static_assert(offsetof(HmdDfuData, mode) == 12); + +struct HmdScreenData { + const void* data; + std::int32_t size; +}; + +static_assert(sizeof(HmdScreenData) == 16); +static_assert(offsetof(HmdScreenData, size) == 8); +static_assert(sizeof(HmdEyeDistances) == 8); +static_assert(sizeof(HmdEyeOffset) == 12); + +struct HmdLedData { + std::uint8_t values[9]; +}; + +static_assert(sizeof(HmdLedData) == 9); + extern "C" { std::int32_t APS5_VABI sceHmdInitialize(const Hmd::InitializeParam* param) { @@ -68,4 +210,795 @@ std::int32_t APS5_VABI sceHmdInternalMmapIsConnect() { return 0; } +int APS5_VABI sceHmdInternalGetPUVersion(std::uint32_t* version) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalEventInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetVirtualDisplaySize(std::uint8_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSocialScreenSetOutput(std::uint32_t output, const HmdScreenOutputParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetVirtualDisplayDepth(std::uint8_t* depth) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetEyeStatus(std::int32_t handle, HmdEyeOffset* left, HmdEyeOffset* right) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionUnsetCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetCrashReportCommand(std::uint8_t command) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDebugTextMode(std::uint8_t* mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetWorkMemoryAlign(void) { + return 0x400; +} + +int APS5_VABI sceHmdInternalRegisterEventCallback(HmdEventCallback callback) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetWorkMemorySize(void) { + return 0x20000; +} + +int APS5_VABI sceHmdReprojectionStartWideNear(const HmdReprojectionWideNearParameter* parameter, const HmdReprojectionPose* pose, std::uint64_t flipArg, const void* reserved) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetPUSerialNumber(char* serialNumber) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDebugSocialScreenMode(std::uint8_t* mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetM2LedBrightness(std::int32_t handle, std::uint32_t ledMask, const std::uint8_t* brightness) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetIPD(std::int32_t mode, HmdEyeDistances distances) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMirroringModeSetAspect(const HmdMirroringAspect* aspect) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuGetStatus(std::uint64_t* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenSendVideo(const HmdScreenData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetRequiredPUPVersion(std::uint32_t* firstVersion, std::uint32_t* secondVersion) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionSetCallback(const HmdReprojectionCallbackParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuCheckAfterPvt(std::uint8_t* afterPvt) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalResetInertialSensor(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionSetUserEventStart(std::int64_t eventQueue, std::int32_t eventId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionSetOutputMinColor(float red, float green, float blue) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetVirtualDisplayDepth(std::uint8_t depth) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetDistortionWorkMemoryAlign(void) { + return 0x400; +} + +int APS5_VABI sceHmdReprojectionStartMultilayer(const HmdReprojectionMultilayerParameter* layers, std::uint32_t layerCount, const HmdReprojectionSubmissionParameter* submission, const HmdReprojectionPose* pose, std::uint64_t flipArg, const void* reserved) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalPushVr2dData(const HmdVr2dData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionClearUserEventEnd(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetIPD(HmdEyeDistances* distances) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetVRMode(std::int32_t enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportReadDataSize(std::uint32_t* dataSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetVr2dData(HmdVr2dData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenSendAudio(const HmdScreenData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetPuPositionParam(HmdPuPositionParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionInitializeCapture(const HmdCaptureInitializeParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGet2DEyeOffset(std::int32_t handle, HmdEyeOffset* leftOffset, HmdEyeOffset* rightOffset) { + (void)handle; + (void)leftOffset; + (void)rightOffset; + return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; +} + +int APS5_VABI sceHmdInternalSetDebugSocialScreenMode(std::uint8_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetHmuPowerStatusForDebug(std::uint8_t* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetDistortionWorkMemorySize(void) { + return 0x20000; +} + +int APS5_VABI sceHmdReprojectionSetDisplayBuffers(std::int32_t videoOutHandle, std::int32_t firstBufferIndex, std::int32_t secondBufferIndex, const void* reserved) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenSetFadeAndSwitch(const HmdScreenFadeParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCreateSharedMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMirroringModeSetAspectDebug(const HmdMirroringAspectDebug* aspect) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetPuBuildNumber(std::uint32_t* buildNumber) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetCompoundEyeCorrectionCommand(void) { + return static_cast(0x81110009); +} + +int APS5_VABI sceHmdInternalSetS3dPassMode(std::uint8_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStopCapture(HmdCaptureInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceHmdInternalIsCommonDlgMiniAppVr2d(void) { + return false; +} + +int APS5_VABI sceHmdInternalMmapGetSensorCalibrationData(void* buffer, std::uint32_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetCorrectionCommand(std::int32_t handle, const HmdDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { + (void)parameter; + (void)commandBuffer; + return CommandQueryHandle(handle, __func__); +} + +int APS5_VABI sceHmdReprojectionQueryOnionBuffAlign(void) { + return 0x100; +} + +int APS5_VABI sceHmdInternalGetDebugMode(std::uint8_t* mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceHmdInternalIsMiniAppVr2d(void) { + return false; +} + +bool APS5_VABI sceHmdInternalIsCommonDlgVr2d(void) { + return false; +} + +int APS5_VABI sceHmdReprojectionSetOutputMinColor(float red, float green, float blue) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetPortConnection(std::uint64_t connection) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionDebugGetLastInfo(HmdReprojectionDebugInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetM2LedOn(std::int32_t handle, std::uint32_t ledMask, std::uint32_t brightness) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetFieldOfView(std::int32_t handle, HmdFieldOfView* fieldOfView) { + (void)handle; + if (!fieldOfView) return Hmd::ParameterNull; + return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; +} + +int APS5_VABI sceHmdReprojectionAddDisplayBuffer(std::int32_t bufferIndex, const void* reserved) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetIpdSettingEnableForSystemService(std::uint8_t enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetLedOn(std::int32_t handle, std::uint32_t ledMask) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternal3dAudioSendData(std::int32_t port, const void* data, std::int32_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionInitialize(const HmdReprojectionInitializeParameter* parameter, std::uint32_t mode, const void* reserved) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDeviceInformationByHandle(std::int32_t handle, HmdDeviceInformation* information) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStopLiveCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportReadData(void* buffer, std::uint32_t* bytesRead, std::uint32_t bufferSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDeviceInformation(HmdDeviceInformation* information) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDefaultLedData(std::int32_t handle, std::uint32_t* mask, HmdLedData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionQueryGarlicBuffAlign(void) { + return 0x100; +} + +int APS5_VABI sceHmdInternalSetHmuPowerControl(std::uint8_t control) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalBindDeviceWithUserId(std::int32_t handle, std::int32_t userId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetHmuSerialNumber(std::uint8_t* serialNumber) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGetWideNearCorrectionCommand(std::int32_t handle, const HmdWideNearDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { + (void)parameter; + (void)commandBuffer; + return CommandQueryHandle(handle, __func__); +} + +int APS5_VABI sceHmdInternalCheckS3dPassModeAvailable(bool* available) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenGetAudioStatus(HmdScreenStatus* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeGetAudioStatus(HmdScreenStatus* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartLiveCapture(const HmdLiveCaptureParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceHmdInternalIsGameVr2d(void) { + return false; +} + +int APS5_VABI sceHmdGetAssyError(HmdAssemblyError* data) { + if (!data) return Hmd::ParameterNull; + return initialized.load() ? Hmd::DeviceDisconnected : Hmd::NotInitialized; +} + +int APS5_VABI sceHmdReprojectionFinalize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSocialScreenSetFadeAndSwitch(const HmdScreenFadeParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetVirtualDisplayHeight(std::uint8_t height) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionInitialize(void* workMemory) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuSetMode(std::uint32_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetForcedCrash(std::uint8_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuCheckPartialUpdateAvailable(bool* available) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDebugTextMode(std::uint8_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetHmuPowerControlForDebug(std::uint8_t control) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetVirtualDisplayHeight(std::uint8_t* height) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStart(const HmdReprojectionFrameParameter* parameter, const HmdReprojectionPose* pose, std::uint64_t flipArg, const void* reserved) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalResetLedForVsh(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDebugMode(std::uint8_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportCancel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeGetVideoStatus(HmdScreenStatus* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCheckDeviceModelMk3(bool* isMk3) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdSetVr2dEnable(std::int32_t enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetSidetone(std::uint8_t level) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMmapGetModeId(std::uint32_t* modeId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdDistortionGet2dVrCommand(std::int32_t handle, const Hmd2dVrCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { + if (const auto result = Vr2dCommandArguments(parameter, commandBuffer); result != 0) return result; + return CommandQueryHandle(handle, __func__); +} + +int APS5_VABI sceHmdInternalDfuSendSize(std::int32_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetWideNearDistortionCorrectionCommand(std::int32_t handle, const HmdWideNearDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { + (void)parameter; + (void)commandBuffer; + return CommandQueryHandle(handle, __func__); +} + +int APS5_VABI sceHmdReprojectionStartMultilayer2(const HmdReprojectionMultilayerParameter* layers, std::uint32_t layerCount, const HmdReprojectionSubmissionParameter* submission, std::uint64_t flipArg, const void* reserved) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetDistortionCorrectionCommand(std::int32_t handle, const HmdDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { + (void)parameter; + (void)commandBuffer; + return CommandQueryHandle(handle, __func__); +} + +int APS5_VABI sceHmdInternalSetDeviceConnection(std::uint64_t connection) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetVirtualDisplaySize(std::uint8_t* size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionUnsetDisplayBuffers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetHmuOpticalParam(HmdOpticalParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeSendVideo(const HmdScreenData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalResetLedForVrTracker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionQueryOnionBuffSize(void) { + return 0x810; +} + +int APS5_VABI sceHmdReprojectionStartWithOverlay(const HmdReprojectionFrameParameter* parameter, const HmdReprojectionPose* pose, std::uint64_t flipArg, const HmdReprojectionOverlayParameter* overlay, const void* reserved) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionSetUserEventEnd(std::int64_t eventQueue, std::int32_t eventId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalCrashReportOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDemoMode(std::uint8_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStartWideNearWithOverlay(const HmdReprojectionWideNearParameter* parameter, const HmdReprojectionPose* pose, std::uint64_t flipArg, const HmdReprojectionOverlayParameter* overlay, const void* reserved) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetDistortionParams(HmdDistortionParameters* parameters) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuSend(const HmdDfuData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionClearUserEventStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetUserType(std::uint8_t userType) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetDemoMode(std::uint8_t* mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMapSharedMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetCrashDumpInfo(HmdCrashDumpInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetTv4kCapability(std::uint8_t* capability) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalDfuReset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStart2dVr(const HmdReprojection2dVrParameter* parameter, std::uint64_t flipArg, const void* reserved) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalMmapGetCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDefaultLedData(std::int32_t handle, std::uint32_t mask, const HmdLedData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdGetInertialSensorData(std::int32_t handle, HmdInertialSensorData* data, std::uint32_t count) { + (void)handle; + (void)data; + (void)count; + return initialized.load() ? Hmd::InvalidHandle : Hmd::NotInitialized; +} + +int APS5_VABI sceHmdInternal3dAudioClose(std::int32_t port) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetBrightness(std::uint8_t brightness) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSocialScreenGetFadeState(std::int32_t* state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSeparateModeSendAudio(const HmdScreenData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetStatusReport(std::int32_t handle, HmdStatusReport* report) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetBrightness(std::uint8_t* brightness) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionFinalizeCapture(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetPortStatus(std::uint64_t status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenGetVideoStatus(HmdScreenStatus* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternal3dAudioOpen(std::int32_t port) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalAnotherScreenGetFadeState(std::int32_t* state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalSetDebugGpo(std::uint32_t value) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetIpdSettingEnableForSystemService(std::uint8_t* enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionDebugGetLastInfoMultilayer(HmdReprojectionMultilayerDebugInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdInternalGetPuRevision(std::uint32_t* revision) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmdReprojectionQueryGarlicBuffSize(void) { + return 0x100000; +} + +int APS5_VABI sceHmdGet2dVrCommand(std::int32_t handle, const Hmd2dVrCommandParameter* parameter, HmdCommandBuffer* commandBuffer) { + if (const auto result = Vr2dCommandArguments(parameter, commandBuffer); result != 0) return result; + return CommandQueryHandle(handle, __func__); +} + } diff --git a/core/libs/prx/libSceHmd/Hmd.hpp b/core/libs/prx/libSceHmd/Hmd.hpp index cfbb52bff6..22f812c774 100644 --- a/core/libs/prx/libSceHmd/Hmd.hpp +++ b/core/libs/prx/libSceHmd/Hmd.hpp @@ -15,6 +15,7 @@ inline constexpr std::int32_t DeviceDisconnected = static_cast(0x8 inline constexpr std::int32_t ParameterNull = static_cast(0x81110008); inline constexpr std::int32_t ParameterInvalid = static_cast(0x81110009); inline constexpr std::int32_t UnsupportedFeature = static_cast(0x81110016); +inline constexpr std::uint32_t StartupStateSdkVersion = 0x04500000; enum class DeviceStatus : std::uint32_t { Ready = 0, @@ -51,6 +52,15 @@ static_assert(offsetof(DeviceInformation, hmuMount) == 24); } +struct HmdFieldOfView; +struct HmdEyeOffset; +struct HmdInertialSensorData; +struct HmdAssemblyError; +struct HmdDistortionCommandParameter; +struct HmdWideNearDistortionCommandParameter; +struct Hmd2dVrCommandParameter; +struct HmdCommandBuffer; + extern "C" { std::int32_t APS5_VABI sceHmdInitialize(const Hmd::InitializeParam* param); std::int32_t APS5_VABI sceHmdInitialize315(const Hmd::InitializeParam* param); @@ -63,6 +73,26 @@ std::int32_t APS5_VABI sceHmdGetDeviceInformationByHandle(std::int32_t handle, Hmd::DeviceInformation* info); std::int32_t APS5_VABI sceHmdInternalGetDeviceStatus(Hmd::DeviceStatus* status); std::int32_t APS5_VABI sceHmdInternalMmapIsConnect(); +int APS5_VABI sceHmdDistortionGetWorkMemoryAlign(void); +int APS5_VABI sceHmdDistortionGetWorkMemorySize(void); +int APS5_VABI sceHmdReprojectionQueryGarlicBuffAlign(void); +int APS5_VABI sceHmdReprojectionQueryGarlicBuffSize(void); +int APS5_VABI sceHmdReprojectionQueryOnionBuffAlign(void); +int APS5_VABI sceHmdReprojectionQueryOnionBuffSize(void); +int APS5_VABI sceHmdGetFieldOfView(std::int32_t handle, HmdFieldOfView* fieldOfView); +int APS5_VABI sceHmdGet2DEyeOffset(std::int32_t handle, HmdEyeOffset* leftOffset, HmdEyeOffset* rightOffset); +int APS5_VABI sceHmdGetInertialSensorData(std::int32_t handle, HmdInertialSensorData* data, std::uint32_t count); +int APS5_VABI sceHmdGetAssyError(HmdAssemblyError* data); +int APS5_VABI sceHmdDistortionGet2dVrCommand(std::int32_t handle, const Hmd2dVrCommandParameter* parameter, HmdCommandBuffer* commandBuffer); +int APS5_VABI sceHmdDistortionGetCorrectionCommand(std::int32_t handle, const HmdDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer); +int APS5_VABI sceHmdDistortionGetWideNearCorrectionCommand(std::int32_t handle, const HmdWideNearDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer); +int APS5_VABI sceHmdGet2dVrCommand(std::int32_t handle, const Hmd2dVrCommandParameter* parameter, HmdCommandBuffer* commandBuffer); +int APS5_VABI sceHmdGetDistortionCorrectionCommand(std::int32_t handle, const HmdDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer); +int APS5_VABI sceHmdGetWideNearDistortionCorrectionCommand(std::int32_t handle, const HmdWideNearDistortionCommandParameter* parameter, HmdCommandBuffer* commandBuffer); +bool APS5_VABI sceHmdInternalIsGameVr2d(void); +bool APS5_VABI sceHmdInternalIsMiniAppVr2d(void); +bool APS5_VABI sceHmdInternalIsCommonDlgVr2d(void); +bool APS5_VABI sceHmdInternalIsCommonDlgMiniAppVr2d(void); } #endif diff --git a/core/libs/prx/libSceHmd2/CMakeLists.txt b/core/libs/prx/libSceHmd2/CMakeLists.txt index 84efff8feb..6d7565ac4a 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) @@ -13,3 +14,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_hmd2_gaze_tests ${LIBS_INCLUDE_DIR}/tests/GuestHmd2Gaze.cpp) + target_include_directories(guest_hmd2_gaze_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_hmd2_gaze_tests PRIVATE libSceHmd2 libc) + configure_windows_unwind(guest_hmd2_gaze_tests) + add_test(NAME guest_hmd2_gaze COMMAND guest_hmd2_gaze_tests) + if(WIN32) + set_tests_properties(guest_hmd2_gaze PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index 9f1c4cb8f9..d744bf2741 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -1,72 +1,359 @@ -#include -#include -#include "SceTypes.hpp" +#include "Hmd2.hpp" +#include "prx/libc/include/general/ExportMacros.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceHmd/Hmd.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include "SceTypes.hpp" + +#include +#include +#include +#include + +namespace Hmd2 { + +struct ReprojectionStatus; +struct ReprojectionOutput { + std::uint32_t textureDescriptor[8]; +}; + +struct ReprojectionInitializeParameter { + void* workMemory; + void* displayMemory; + std::int32_t threadPriority; + std::uint32_t threadAffinity; + std::uint32_t queueParameter24; + std::uint32_t queueParameter28; + std::uint32_t renderingMode; + std::uint32_t reserved36; + void* auxiliaryMemory; + std::uint32_t timingInterval; + std::uint32_t reserved52[5]; +}; + +static_assert(sizeof(ReprojectionOutput) == 32); +static_assert(sizeof(ReprojectionInitializeParameter) == 72); +static_assert(offsetof(ReprojectionInitializeParameter, threadPriority) == 16); +static_assert(offsetof(ReprojectionInitializeParameter, threadAffinity) == 20); +static_assert(offsetof(ReprojectionInitializeParameter, renderingMode) == 32); +static_assert(offsetof(ReprojectionInitializeParameter, auxiliaryMemory) == 40); +static_assert(offsetof(ReprojectionInitializeParameter, timingInterval) == 48); +struct ReprojectionLayer; +struct ReprojectionBufferedLayer; + +struct ReprojectionOption { + std::uint32_t size; + std::uint32_t flags; + std::uint32_t reserved[30]; +}; + +struct ReprojectionParameter { + const ReprojectionLayer* layers; + std::int32_t layerCount; + std::uint8_t padding[4]; + const ReprojectionOption* option; +}; + +struct ReprojectionBufferedParameter { + const ReprojectionBufferedLayer* layers; + std::int32_t layerCount; + std::uint8_t padding[4]; + const ReprojectionOption* option; +}; + +static_assert(sizeof(ReprojectionOption) == 128); +static_assert(sizeof(ReprojectionParameter) == 24); +static_assert(sizeof(ReprojectionBufferedParameter) == 24); +static_assert(offsetof(ReprojectionParameter, option) == 16); +static_assert(offsetof(ReprojectionBufferedParameter, option) == 16); + + +struct MirroringParameter { + void* workMemory; + void* displayMemory; +}; + +struct MirroringOption { + std::uint32_t mode; + std::uint32_t reserved[7]; +}; + +static_assert(sizeof(MirroringParameter) == 16); +static_assert(sizeof(MirroringOption) == 32); + +struct GazeResultParameter { + std::uint32_t size; + std::int32_t handle; + std::uint8_t reserved[8]; +}; +struct GazeResult; +struct FoveatedRenderingResult { + std::uint32_t size; + std::int32_t status; + float direction[3]; + std::uint8_t reserved[4]; +}; + +static_assert(sizeof(GazeResultParameter) == 16); +static_assert(sizeof(FoveatedRenderingResult) == 24); +static_assert(offsetof(FoveatedRenderingResult, direction) == 8); +struct GazeCalibrationData { + void* data; + std::int32_t size; +}; +struct GazeUserCalibrationData { + const void* data; + std::int32_t size; +}; +struct GazeUserCalibrationStatus { + std::int32_t status; + std::int32_t eye; + float point[3]; +}; +struct GazeRawData; +struct GazeObjectSelectionTransform { + float values[16]; +}; + +struct GazeObjectSelectionObject { + std::uint64_t id; + std::uint32_t enabled; + std::uint32_t reserved12; + GazeObjectSelectionTransform transform; + const GazeObjectSelectionTransform* boundingBoxes; + std::uint32_t boundingBoxCount; + std::uint32_t reserved92; +}; + +struct GazeObjectSelectionParameter { + std::uint32_t size; + std::uint32_t reserved4; + std::uint64_t reserved8; + std::uint64_t gazeTimestamp; + GazeObjectSelectionTransform transform; + std::uint32_t selectionMode; + std::uint32_t filterMode; + std::uint64_t timestamp; + const GazeObjectSelectionObject* objects; + std::uint32_t objectCount; + std::uint32_t reserved116; +}; + +struct GazeObjectSelectionResult { + std::uint32_t size; + std::uint32_t selected; + std::uint64_t objectId; + std::uint64_t gazeTimestamp; + std::uint32_t gazeValid; + float origin[3]; + float direction[3]; + float distance; +}; + +static_assert(sizeof(GazeObjectSelectionTransform) == 64); +static_assert(sizeof(GazeObjectSelectionObject) == 96); +static_assert(offsetof(GazeObjectSelectionObject, transform) == 16); +static_assert(offsetof(GazeObjectSelectionObject, boundingBoxes) == 80); +static_assert(offsetof(GazeObjectSelectionObject, boundingBoxCount) == 88); +static_assert(sizeof(GazeObjectSelectionParameter) == 120); +static_assert(offsetof(GazeObjectSelectionParameter, transform) == 24); +static_assert(offsetof(GazeObjectSelectionParameter, selectionMode) == 88); +static_assert(offsetof(GazeObjectSelectionParameter, timestamp) == 96); +static_assert(offsetof(GazeObjectSelectionParameter, objects) == 104); +static_assert(offsetof(GazeObjectSelectionParameter, objectCount) == 112); +static_assert(sizeof(GazeObjectSelectionResult) == 56); +static_assert(offsetof(GazeObjectSelectionResult, objectId) == 8); +static_assert(offsetof(GazeObjectSelectionResult, gazeTimestamp) == 16); +static_assert(offsetof(GazeObjectSelectionResult, origin) == 28); +static_assert(offsetof(GazeObjectSelectionResult, direction) == 40); +static_assert(offsetof(GazeObjectSelectionResult, distance) == 52); +struct GazeObjectSelectionInitializeParameter { + std::uint32_t size; + std::uint32_t maxObjects; + void* memory; + std::uint32_t memorySize; + std::uint32_t reserved; +}; +struct GazeObjectSelectionMemoryParameter { + std::uint32_t size; + std::uint32_t maxObjects; +}; +struct GazeObjectSelectionMemoryInfo { + std::uint32_t size; + std::uint32_t memorySize; + std::uint64_t alignment; +}; + +struct CalibrationPoint { + float x; + float y; + float z; +}; + +static_assert(sizeof(CalibrationPoint) == 12); +static_assert(sizeof(GazeCalibrationData) == 16); +static_assert(offsetof(GazeCalibrationData, size) == 8); +static_assert(sizeof(GazeUserCalibrationData) == 16); +static_assert(offsetof(GazeUserCalibrationData, size) == 8); +static_assert(sizeof(GazeUserCalibrationStatus) == 20); +static_assert(offsetof(GazeUserCalibrationStatus, point) == 8); +static_assert(sizeof(GazeObjectSelectionInitializeParameter) == 24); +static_assert(offsetof(GazeObjectSelectionInitializeParameter, memory) == 8); +static_assert(offsetof(GazeObjectSelectionInitializeParameter, memorySize) == 16); +static_assert(sizeof(GazeObjectSelectionMemoryParameter) == 8); +static_assert(sizeof(GazeObjectSelectionMemoryInfo) == 16); +static_assert(offsetof(GazeObjectSelectionMemoryInfo, alignment) == 8); + +} 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::int32_t BeforeInitialize(std::int32_t answer, const char* name) { + std::lock_guard lock(stateMutex); + if (initialized) NotImplemented_nid_no_patch(name); + return answer; +} + +std::uint32_t CompiledSdkVersion() { + static const auto* processParameters = static_cast(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t version; + 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 sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign() { +int APS5_VABI sceHmd2ReprojectionGetStatus(Hmd2::ReprojectionStatus* status) { NotImplemented_nid_no_patch(__func__); return 0; } +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign() { + return {0x1106000, 0x100}; +} + std::int32_t APS5_VABI sceHmd2Initialize(const void* param) { (void)param; - return SCE_HMD2_ERROR_UNSUPPORTED_FEATURE; + return Hmd::UnsupportedFeature; } -int APS5_VABI sceHmd2GetFieldOfView() { - 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 sceHmd2GetDeviceInformationByHandle() { - 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 sceHmd2Terminate() { - 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; } -int APS5_VABI sceHmd2Close() { - NotImplemented_nid_no_patch(__func__); +static std::int32_t Hmd2GazeHandle(const Hmd2::GazeResultParameter* param, const void* result, std::uint32_t resultSize, const char* name) { + if (!param || !result) return Hmd2::ParameterNull; + std::uint32_t size; + std::memcpy(&size, result, sizeof(size)); + if (param->size != sizeof(Hmd2::GazeResultParameter) || size != resultSize) return Hmd2::ParameterInvalid; + std::lock_guard lock(stateMutex); + if (openHandle == 0 || param->handle != openHandle) return Hmd2::InvalidHandle; + NotImplemented_nid_no_patch(name); return 0; } -int APS5_VABI sceHmd2GazeGetResult() { - NotImplemented_nid_no_patch(__func__); +static void Hmd2StoreInvalidGaze(std::byte* result, std::size_t offset) { + const std::int32_t invalidIndex = -1; + const float invalidPair[2] = {-0.0f, 0.0f}; + const float invalidScalar = -1.0f; + std::memcpy(result + 4, &invalidIndex, sizeof(invalidIndex)); + std::memcpy(result + offset, invalidPair, sizeof(invalidPair)); + std::memcpy(result + offset + sizeof(invalidPair), &invalidScalar, sizeof(invalidScalar)); +} + +int APS5_VABI sceHmd2GazeGetResult(const Hmd2::GazeResultParameter* param, Hmd2::GazeResult* result) { + const std::int32_t status = Hmd2GazeHandle(param, result, 0x38, __func__); + if (status != Hmd2::InvalidHandle) return status; + auto* bytes = reinterpret_cast(result); + std::memset(bytes + 4, 0, 0x38 - 4); + Hmd2StoreInvalidGaze(bytes, 0x14); + return status; +} + +int APS5_VABI sceHmd2GazeGetResultForFoveatedRendering(const Hmd2::GazeResultParameter* param, Hmd2::FoveatedRenderingResult* result) { + const std::int32_t status = Hmd2GazeHandle(param, result, 0x18, __func__); + if (status != Hmd2::InvalidHandle) return status; + Hmd2StoreInvalidGaze(reinterpret_cast(result), 8); + return status; +} + +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 sceHmd2GazeGetResultForFoveatedRendering() { - 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 sceHmd2GetDeviceInformation() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GetFieldOfViewWithoutHandle() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2Open() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionBeginFrame() { +int APS5_VABI sceHmd2ReprojectionBeginFrame(std::uint64_t frameId) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -81,72 +368,69 @@ int APS5_VABI sceHmd2ReprojectionDisableVrMode() { return 0; } -int APS5_VABI sceHmd2ReprojectionEnableMirroring() { +int APS5_VABI sceHmd2ReprojectionEnableMirroring(const Hmd2::MirroringParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionEnableVrMode() { +int APS5_VABI sceHmd2ReprojectionEnableVrMode(std::uint64_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign() { +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign() { + return {0x5FA0000, 0x10000}; +} + +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign() { + return {0x4000, 0x100}; +} + +int APS5_VABI sceHmd2ReprojectionGetPredictedDisplayTime(std::uint64_t frameId, std::uint64_t* time) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign() { +int APS5_VABI sceHmd2ReprojectionInitialize(const Hmd2::ReprojectionInitializeParameter* parameter, const void* reserved) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionGetPredictedDisplayTime() { +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQueryBufferSizeAlign() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionInitialize() { +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQueryDisplayBufferSizeAlign() { + return {0x6000000, 0x10000}; +} + +int APS5_VABI sceHmd2ReprojectionSetAllowPositionalReprojection(std::uint8_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionQueryBufferSizeAlign() { +int APS5_VABI sceHmd2ReprojectionSetMirroringOption(const Hmd2::MirroringOption* option) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionQueryDisplayBufferSizeAlign() { +int APS5_VABI sceHmd2ReprojectionSetParam(std::uint32_t bufferSetId, std::uint64_t frameId, const Hmd2::ReprojectionParameter* parameter, std::uint64_t reserved) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionSetAllowPositionalReprojection() { +int APS5_VABI sceHmd2ReprojectionSetParamWithBuffer(std::uint32_t bufferSetId, std::uint64_t frameId, const Hmd2::ReprojectionBufferedParameter* parameter, std::uint64_t reserved) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionSetMirroringOption() { +int APS5_VABI sceHmd2ReprojectionSetRenderConfig(std::uint64_t flags, std::uint64_t reserved, std::uint64_t reserved2) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2ReprojectionSetParam() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionSetParamWithBuffer() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionSetRenderConfig() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionSetTiming() { +int APS5_VABI sceHmd2ReprojectionSetTiming(std::int32_t timing) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -156,9 +440,658 @@ int APS5_VABI sceHmd2ReprojectionTerminate() { return 0; } -int APS5_VABI sceHmd2SetVibration() { +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; +} + +int APS5_VABI sceHmd2ReprojectionSeeThroughDraw() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceHmd2ReprojectionSeeThroughIC() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2ReprojectionVideoOutEq() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2ReprojectionWaitShader() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2SeeThroughCallbackGetPoseDataFunc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2ReprojectionClear() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2ReprojectionSet() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2ReprojectionSetEventGpuEnd(KernelEqueue queue, std::int32_t eventId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2ReprojectionSetEventGpuStart(KernelEqueue queue, std::int32_t eventId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2RpeorjectionSetParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + + +std::int32_t APS5_VABI Hmd2BeforeInitializeZero() { + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("vDgpdS2N9Rw", Hmd2BeforeInitializeZero); +APS5_EXPORT("Ri35gKTZpvE", Hmd2BeforeInitializeZero); +APS5_EXPORT("9oyW8puiKeI", Hmd2BeforeInitializeZero); +APS5_EXPORT("1HZNE6jhBnQ", Hmd2BeforeInitializeZero); +APS5_EXPORT("cdwAKBZLXjs", Hmd2BeforeInitializeZero); +APS5_EXPORT("Yub1MF58jfY", Hmd2BeforeInitializeZero); +APS5_EXPORT("JYXAuQFnPyw", Hmd2BeforeInitializeZero); +APS5_EXPORT("VuPKqCu3wzw", Hmd2BeforeInitializeZero); +APS5_EXPORT("GSThagVyW5U", Hmd2BeforeInitializeZero); +APS5_EXPORT("J4qE5N937MY", Hmd2BeforeInitializeZero); +APS5_EXPORT("9KnNj-dyASg", Hmd2BeforeInitializeZero); +APS5_EXPORT("Qta2bkstbFk", Hmd2BeforeInitializeZero); +APS5_EXPORT("5ZcObnDRBXE", Hmd2BeforeInitializeZero); +APS5_EXPORT("AvPNn0qx4WQ", Hmd2BeforeInitializeZero); +int APS5_VABI sceHmd2SetIpdOffset(std::uint8_t enabled, float offset) { + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("-kZT7X7OMg0", Hmd2BeforeInitializeZero); +APS5_EXPORT("1ewLtng-SVk", Hmd2BeforeInitializeZero); +APS5_EXPORT("B0b8DeIUGes", Hmd2BeforeInitializeZero); +APS5_EXPORT("HclLg4Oj5yw", Hmd2BeforeInitializeZero); +APS5_EXPORT("VbEvrQXsZyY", Hmd2BeforeInitializeZero); +APS5_EXPORT("bB6MKZi5UCQ", Hmd2BeforeInitializeZero); +APS5_EXPORT("OXQ1EDf02XU", Hmd2BeforeInitializeZero); +APS5_EXPORT("IAxfMTvFm0M", Hmd2BeforeInitializeZero); +APS5_EXPORT("qTJtAtaLeno", Hmd2BeforeInitializeZero); +APS5_EXPORT("j7Q16UX6l34", Hmd2BeforeInitializeZero); +APS5_EXPORT("pmmNM06rLqU", Hmd2BeforeInitializeZero); +APS5_EXPORT("zdQfyDK9Ods", Hmd2BeforeInitializeZero); +APS5_EXPORT("aPRwp9GnljM", Hmd2BeforeInitializeZero); +APS5_EXPORT("MjY1gBSaed0", Hmd2BeforeInitializeZero); +APS5_EXPORT("evAMjJYHqW4", Hmd2BeforeInitializeZero); +APS5_EXPORT("r93FLEZ3BBk", Hmd2BeforeInitializeZero); +APS5_EXPORT("IdYURdEdNFo", Hmd2BeforeInitializeZero); +int APS5_VABI sceHmd2GazeActivateGazeTracking(std::int32_t mode) { + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("NmKIfVxr2s0", Hmd2BeforeInitializeZero); +APS5_EXPORT("KeDLkkp62Gg", Hmd2BeforeInitializeZero); +APS5_EXPORT("+aadu6-ra+A", Hmd2BeforeInitializeZero); +APS5_EXPORT("FgnTiwwGSzk", Hmd2BeforeInitializeZero); +APS5_EXPORT("PWAUwOTHbbo", Hmd2BeforeInitializeZero); +APS5_EXPORT("WzaEApHnsP8", Hmd2BeforeInitializeZero); +APS5_EXPORT("dXzQI8QVY7Y", Hmd2BeforeInitializeZero); +APS5_EXPORT("cCvFKGqRh1k", Hmd2BeforeInitializeZero); +APS5_EXPORT("uj4-2VEAFPU", Hmd2BeforeInitializeZero); +APS5_EXPORT("sIaGStI-iLM", Hmd2BeforeInitializeZero); +APS5_EXPORT("JzwpZOemDO8", Hmd2BeforeInitializeZero); +APS5_EXPORT("IsC4R4bZFVU", Hmd2BeforeInitializeZero); + +std::int32_t APS5_VABI Hmd2BeforeInitializeNotInitialized() { + return BeforeInitialize(Hmd2::NotInitialized, __func__); +} +APS5_EXPORT("sXaPEPY-fLA", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("3F5inHhKzXg", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("sJAiB-6p55U", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("E8hDydLjsSs", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("xEQkSb2WJ7Y", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("aaP4dnR4q-0", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("lEU0TeUoGwI", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("cFVyjHBpUUU", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("zROtb46jTj4", Hmd2BeforeInitializeNotInitialized); + +std::int32_t APS5_VABI Hmd2BeforeInitializeError0004() { + return BeforeInitialize(Hmd2::Error0004, __func__); +} +APS5_EXPORT("kAMsjYHBbNQ", Hmd2BeforeInitializeError0004); +APS5_EXPORT("5m73zmLpsQ0", Hmd2BeforeInitializeError0004); +APS5_EXPORT("EPeH4xuQ2XQ", Hmd2BeforeInitializeError0004); +APS5_EXPORT("TJ8lyRsjFY4", Hmd2BeforeInitializeError0004); +APS5_EXPORT("-XQVcLnyX68", Hmd2BeforeInitializeError0004); +APS5_EXPORT("giQwQoHhYYA", Hmd2BeforeInitializeError0004); +APS5_EXPORT("zNc9IkgawMI", Hmd2BeforeInitializeError0004); +int APS5_VABI sceHmd2GazeStartUserCalibration() { + return BeforeInitialize(Hmd2::Error0004, __func__); +} +int APS5_VABI sceHmd2GazeStopUserCalibration() { + return BeforeInitialize(Hmd2::Error0004, __func__); +} +APS5_EXPORT("CKlkvNngofQ", Hmd2BeforeInitializeError0004); +int APS5_VABI sceHmd2GazeSetUserCalibrationPoint(Hmd2::CalibrationPoint point) { + return BeforeInitialize(Hmd2::Error0004, __func__); +} +APS5_EXPORT("qlzgyG3s0RY", Hmd2BeforeInitializeError0004); +APS5_EXPORT("kTIDrDLzMv0", Hmd2BeforeInitializeError0004); +APS5_EXPORT("9Gee0ef8a+U", Hmd2BeforeInitializeError0004); +int APS5_VABI sceHmd2GazeSetEnabledEye(std::int32_t eye) { + return BeforeInitialize(Hmd2::Error0004, __func__); +} + +std::int32_t APS5_VABI Hmd2BeforeInitializeParameterNull() { + return BeforeInitialize(Hmd2::ParameterNull, __func__); +} +APS5_EXPORT("MNtiJTDOPAU", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("RRy9WmqZfgU", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("uobR4m9lgwc", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("-XPk5KWBYdo", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("J44idh9izZM", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("Ps0wAvYaa1E", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("kLnMR8oQ1MY", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("kMz4rbBxvjo", Hmd2BeforeInitializeParameterNull); +int APS5_VABI sceHmd2GazeGetCalibrationData(std::int32_t type, Hmd2::GazeCalibrationData* data) { + return BeforeInitialize(Hmd2::ParameterNull, __func__); +} +int APS5_VABI sceHmd2GazeSetUserCalibrationData(const Hmd2::GazeUserCalibrationData* data) { + return BeforeInitialize(Hmd2::ParameterNull, __func__); +} +APS5_EXPORT("TJpyGGP9YEY", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("Dih+Odk+RC0", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("oaqdSJw+HVY", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("m35gtHuanXU", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("CTM7QPLJF9Q", Hmd2BeforeInitializeParameterNull); +int APS5_VABI sceHmd2GazeObjectSelectionInitialize(const Hmd2::GazeObjectSelectionInitializeParameter* parameter) { + return BeforeInitialize(Hmd2::ParameterNull, __func__); +} + +std::int32_t APS5_VABI Hmd2BeforeInitializeParameterInvalid() { + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +APS5_EXPORT("bBh5bCp9coo", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("G3PNDE2VjfY", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("nScCT0OPU0o", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("6Q0Ng7OTN9o", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("-fJKh4SnFR0", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("jUA2s-l0npY", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("+9XhyAIJVj4", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("eBGK-o6E4RQ", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("MTuFeL9184E", Hmd2BeforeInitializeParameterInvalid); + +std::int32_t APS5_VABI Hmd2BeforeInitializeReprojectionNotInitialized() { + return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); +} +APS5_EXPORT("d3SEIYTP6wc", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("EVECbTvBezo", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("ciusPdJKjSU", Hmd2BeforeInitializeReprojectionNotInitialized); +int APS5_VABI sceHmd2ReprojectionSetUserEventStart(KernelEqueue queue, std::int32_t eventId) { + return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); +} +int APS5_VABI sceHmd2ReprojectionSetUserEventEnd(KernelEqueue queue, std::int32_t eventId) { + return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); +} +int APS5_VABI sceHmd2ReprojectionClearUserEventStart() { + return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); +} +int APS5_VABI sceHmd2ReprojectionClearUserEventEnd() { + return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); +} +int APS5_VABI sceHmd2ReprojectionGetCurrentOutput(Hmd2::ReprojectionOutput* output) { + return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); +} +APS5_EXPORT("aGOWaEeg04I", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("9up+EPhw1No", Hmd2BeforeInitializeReprojectionNotInitialized); + +std::int32_t APS5_VABI Hmd2BeforeInitializeError002C() { + return BeforeInitialize(Hmd2::Error002C, __func__); +} +int APS5_VABI sceHmd2GazeObjectSelectionTerminate() { + return BeforeInitialize(Hmd2::Error002C, __func__); +} + +std::int32_t APS5_VABI Hmd2BeforeInitializeError1002() { + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("2LVD1EC5SYE", Hmd2BeforeInitializeError1002); +APS5_EXPORT("Bf87AL6zWxU", Hmd2BeforeInitializeError1002); +APS5_EXPORT("XKt7w1MODFY", Hmd2BeforeInitializeError1002); +APS5_EXPORT("Y5s22M-qUDA", Hmd2BeforeInitializeError1002); +APS5_EXPORT("JJKY-BtQJLg", Hmd2BeforeInitializeError1002); + +std::int32_t APS5_VABI Hmd2BeforeInitializeError1003() { + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("9kj9vw8UShQ", Hmd2BeforeInitializeError1003); + +std::int32_t APS5_VABI Hmd2BeforeInitializeMinusOne() { + return BeforeInitialize(-1, __func__); +} +APS5_EXPORT("EAkv8Mqt33E", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("Rv7bkmKvmz0", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("1oPZizn0SNA", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("TwCpE-nNrlo", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("jkqAeQfdtxw", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("+l0RQph81Z8", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("qsmAtGw1Ncc", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("uKhwYoMsIsA", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("WNdaigr94dI", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("P6S5ZVWf-H0", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("sun6NWE1sdg", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("--TFsJYXo04", Hmd2BeforeInitializeMinusOne); + +std::int32_t APS5_VABI Hmd2BeforeInitializeValue0400() { + return BeforeInitialize(0x400, __func__); +} +APS5_EXPORT("BWqcwjCozzI", Hmd2BeforeInitializeValue0400); + +std::int32_t APS5_VABI Hmd2BeforeInitializeValue20000() { + return BeforeInitialize(0x20000, __func__); +} +APS5_EXPORT("PRz7TdBGUI0", Hmd2BeforeInitializeValue20000); + +int APS5_VABI sceHmd2InternalSetPortStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalLedDetectorStartBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageStartBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetPortConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetCameraSeeThroughMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetFakeLensSeparationDistance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalDfuSend(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalLedDetectorOpenBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageOpenBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalLedDetectorStopBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetDeviceConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalLedDetectorCloseBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetSidetone(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGetDeviceId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetBrightness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageStopBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetVibrationMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGetVr2dData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageCloseBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int32_t APS5_VABI Hmd2CheckFirstThenError0004(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error0004, __func__); +} +APS5_EXPORT("9WeZr4kYfzk", Hmd2CheckFirstThenError0004); +APS5_EXPORT("N+5h1hCOFAg", Hmd2CheckFirstThenError0004); +APS5_EXPORT("w2vEs2y5ZmE", Hmd2CheckFirstThenError0004); +APS5_EXPORT("L5gat1pe0y0", Hmd2CheckFirstThenError0004); +APS5_EXPORT("4GnLHR9NJ1w", Hmd2CheckFirstThenError0004); +APS5_EXPORT("AHk7nGSzef0", Hmd2CheckFirstThenError0004); +APS5_EXPORT("ncjX9d3mc0M", Hmd2CheckFirstThenError0004); +APS5_EXPORT("l63QYAUuogc", Hmd2CheckFirstThenError0004); +APS5_EXPORT("mrcBySCdXSo", Hmd2CheckFirstThenError0004); +APS5_EXPORT("ieBra+td8sU", Hmd2CheckFirstThenError0004); +APS5_EXPORT("s+rzBnewgGs", Hmd2CheckFirstThenError0004); +APS5_EXPORT("Nfcm0GBdzZI", Hmd2CheckFirstThenError0004); +APS5_EXPORT("Tc9rasWfrm4", Hmd2CheckFirstThenError0004); +APS5_EXPORT("ckTbKTX4KZg", Hmd2CheckFirstThenError0004); +APS5_EXPORT("D7576PUbUjk", Hmd2CheckFirstThenError0004); +APS5_EXPORT("tTJ04SrBT8U", Hmd2CheckFirstThenError0004); +int APS5_VABI sceHmd2GazeGetDeviceStatus(std::int32_t* status) { + if (status == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error0004, __func__); +} +int APS5_VABI sceHmd2GazeGetUserCalibrationStatus(Hmd2::GazeUserCalibrationStatus* status) { + if (status == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error0004, __func__); +} +APS5_EXPORT("kozvHBNyxf0", Hmd2CheckFirstThenError0004); +APS5_EXPORT("638Tmg5y6U4", Hmd2CheckFirstThenError0004); + +std::int32_t APS5_VABI Hmd2CheckFirstThenZero(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("8ETtFtp5zqY", Hmd2CheckFirstThenZero); +APS5_EXPORT("CWPkBm3EXQA", Hmd2CheckFirstThenZero); +APS5_EXPORT("R62liQQIAaA", Hmd2CheckFirstThenZero); +APS5_EXPORT("-bcWLHsdzo0", Hmd2CheckFirstThenZero); +int APS5_VABI sceHmd2SetFieldOfView(const Hmd2::FieldOfView* fieldOfView) { + if (fieldOfView == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("xNBSz8xrGdk", Hmd2CheckFirstThenZero); +APS5_EXPORT("yUVPfN-jRIU", Hmd2CheckFirstThenZero); +APS5_EXPORT("BKPR3RZ5aO4", Hmd2CheckFirstThenZero); + +std::int32_t APS5_VABI Hmd2CheckFirstThenError1002(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("IG4y+drWAWw", Hmd2CheckFirstThenError1002); +APS5_EXPORT("rhYUml9vppE", Hmd2CheckFirstThenError1002); +APS5_EXPORT("6574xHS+jZk", Hmd2CheckFirstThenError1002); +APS5_EXPORT("mXzaY6xVoxg", Hmd2CheckFirstThenError1002); +APS5_EXPORT("6af-HIZQQn4", Hmd2CheckFirstThenError1002); +APS5_EXPORT("6qWtdTHQsTk", Hmd2CheckFirstThenError1002); +APS5_EXPORT("pUm8t8xWhFg", Hmd2CheckFirstThenError1002); +APS5_EXPORT("A0XKI-WjxK8", Hmd2CheckFirstThenError1002); + +std::int32_t APS5_VABI Hmd2CheckFirstThenMinusOne(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(-1, __func__); +} +APS5_EXPORT("A9s2j8OioPQ", Hmd2CheckFirstThenMinusOne); +APS5_EXPORT("YIgtmrL+HLY", Hmd2CheckFirstThenMinusOne); +APS5_EXPORT("Ng8zEkGxDNs", Hmd2CheckFirstThenMinusOne); +APS5_EXPORT("EtslTnqO1xU", Hmd2CheckFirstThenMinusOne); +APS5_EXPORT("hfrnRTryKQQ", Hmd2CheckFirstThenMinusOne); + +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenParameterInvalid(const void* a0, const void* a1) { + if (a0 == nullptr) return Hmd2::ParameterNull; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +APS5_EXPORT("PPX68fNToFM", Hmd2CheckFirstTwoThenParameterInvalid); +int APS5_VABI sceHmd2GazeObjectSelectionQueryMemory(const Hmd2::GazeObjectSelectionMemoryParameter* parameter, Hmd2::GazeObjectSelectionMemoryInfo* info) { + if (parameter == nullptr) return Hmd2::ParameterNull; + if (info == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +int APS5_VABI sceHmd2GazeObjectSelectionGetSelectedObject(const Hmd2::GazeObjectSelectionParameter* parameter, Hmd2::GazeObjectSelectionResult* result) { + if (parameter == nullptr) return Hmd2::ParameterNull; + if (result == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} + +std::int32_t APS5_VABI Hmd2CheckSecondThenError002C(const void* a0, const void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error002C, __func__); +} +APS5_EXPORT("tWJPX2ycRMM", Hmd2CheckSecondThenError002C); +APS5_EXPORT("5UMB9PJA70o", Hmd2CheckSecondThenError002C); +APS5_EXPORT("xQs62qCk1EI", Hmd2CheckSecondThenError002C); + +std::int32_t APS5_VABI Hmd2CheckFirstThenParameterInvalid(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +APS5_EXPORT("MMc+bSlqkzk", Hmd2CheckFirstThenParameterInvalid); +APS5_EXPORT("NpZHgvSYgEA", Hmd2CheckFirstThenParameterInvalid); + +std::int32_t APS5_VABI Hmd2CheckAllSixThenZero(const void* a0, const void* a1, const void* a2, const void* a3, const void* a4, const void* a5) { + if (a0 == nullptr) return Hmd2::ParameterNull; + if (a1 == nullptr) return Hmd2::ParameterNull; + if (a2 == nullptr) return Hmd2::ParameterNull; + if (a3 == nullptr) return Hmd2::ParameterNull; + if (a4 == nullptr) return Hmd2::ParameterNull; + if (a5 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("z-serf9vsb0", Hmd2CheckAllSixThenZero); +APS5_EXPORT("IRMUFu+uG5s", Hmd2CheckAllSixThenZero); + +std::int32_t APS5_VABI Hmd2CheckSecondThenParameterInvalid(const void* a0, const void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +APS5_EXPORT("72U64E+0z6U", Hmd2CheckSecondThenParameterInvalid); +int APS5_VABI sceHmd2GazeGetCalibrationDataMaxSize(std::int32_t type, std::uint32_t* size) { + (void)type; + if (size == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} + +std::int32_t APS5_VABI Hmd2CheckFirstThenError800d0203(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error800d0203, __func__); +} +APS5_EXPORT("Im0qL0j9Ubg", Hmd2CheckFirstThenError800d0203); + +std::int32_t APS5_VABI Hmd2CheckFirstThenError1003(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("ng6eI8uA0h4", Hmd2CheckFirstThenError1003); + +std::int32_t APS5_VABI Hmd2CheckFirstThenError1010(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1010, __func__); +} +APS5_EXPORT("mPPGgjyJrCI", Hmd2CheckFirstThenError1010); + +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenError1002(const void* a0, const void* a1) { + if (a0 == nullptr) return Hmd2::ParameterNull; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("ZupCJmV5BDI", Hmd2CheckFirstTwoThenError1002); + +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenError1003(const void* a0, const void* a1) { + if (a0 == nullptr) return Hmd2::ParameterNull; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("mAQZwOX1IHU", Hmd2CheckFirstTwoThenError1003); + +std::int32_t APS5_VABI Hmd2CheckFirstAndThirdThenZero(const void* a0, const void* a1, const void* a2) { + if (a0 == nullptr) return Hmd2::ParameterNull; + (void)a1; + if (a2 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("oacDkQkinLo", Hmd2CheckFirstAndThirdThenZero); + +std::int32_t APS5_VABI Hmd2CheckFirstAndThirdThenError1003(const void* a0, const void* a1, const void* a2) { + if (a0 == nullptr) return Hmd2::ParameterNull; + (void)a1; + if (a2 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("zn7PHGzzEyI", Hmd2CheckFirstAndThirdThenError1003); + +std::int32_t APS5_VABI Hmd2CheckSecondThenInvalidHandle(const void* a0, const void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::InvalidHandle, __func__); +} +int APS5_VABI sceHmd2GazeInsertRawDataWithHandle(std::int32_t handle, Hmd2::GazeRawData* data) { + (void)handle; + if (data == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::InvalidHandle, __func__); +} + +std::int32_t APS5_VABI Hmd2CheckSecondThenError1002(const void* a0, const void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("W-ZIlUXcrNA", Hmd2CheckSecondThenError1002); + + +std::int32_t APS5_VABI Hmd2WriteStateTwo(void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + static constexpr std::uint8_t bytes[] = {0x02, 0x00, 0x00, 0x00, 0x00}; + std::memcpy(a0, bytes, sizeof bytes); + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("omimgG5itSs", Hmd2WriteStateTwo); + +std::int32_t APS5_VABI Hmd2WriteSize0x2000Align0x100(std::uint64_t* a0, std::uint64_t* a1) { + if (a0 == nullptr || a1 == nullptr) return Hmd2::ParameterNull; + *a0 = 0x2000; + *a1 = 0x100; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("6PeHcAaKYg8", Hmd2WriteSize0x2000Align0x100); + +std::int32_t APS5_VABI Hmd2WriteFourConstants(void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + static constexpr std::uint8_t bytes[] = {0xe1, 0xf0, 0x74, 0x3c, 0xd8, 0xf0, 0x74, 0x3c, 0x1f, 0xf1, 0x74, 0x3c, 0xd8, 0xf0, 0x74, 0x3c}; + std::memcpy(a0, bytes, sizeof bytes); + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("yur+xPdWPAg", Hmd2WriteFourConstants); + +std::int32_t APS5_VABI Hmd2WriteSixHalvesThenInvalidHandle(std::uint64_t a0, void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + static constexpr std::uint8_t bytes[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3f}; + std::memcpy(a1, bytes, sizeof bytes); + return BeforeInitialize(Hmd2::InvalidHandle, __func__); +} +APS5_EXPORT("RnieDDp5ge4", Hmd2WriteSixHalvesThenInvalidHandle); +APS5_EXPORT("jM3NEJA11yw", Hmd2WriteSixHalvesThenInvalidHandle); + +std::int32_t APS5_VABI Hmd2WriteTwelveHalvesThenInvalidHandle(std::uint64_t a0, void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + static constexpr std::uint8_t bytes[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + std::memcpy(a1, bytes, sizeof bytes); + return BeforeInitialize(Hmd2::InvalidHandle, __func__); +} +APS5_EXPORT("b0H-n-tUk0U", Hmd2WriteTwelveHalvesThenInvalidHandle); + +std::int32_t APS5_VABI Hmd2SlotUpTo3CheckSecondThirdThenZero(std::uint64_t a0, const void* a1, const void* a2) { + if (a1 == nullptr || a2 == nullptr) return Hmd2::ParameterNull; + if (a0 > 3) return Hmd2::ParameterInvalid; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("MXhloJ6Iw3w", Hmd2SlotUpTo3CheckSecondThirdThenZero); + +std::int32_t APS5_VABI Hmd2SlotUpTo1ThenError1002(std::uint64_t a0) { + if (a0 > 1) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("19wrIIiqFgM", Hmd2SlotUpTo1ThenError1002); + +std::int32_t APS5_VABI Hmd2SlotUpTo1CheckSecondThenError1002(std::uint64_t a0, const void* a1) { + if (a1 == nullptr) return Hmd2::ParameterNull; + if (a0 > 1) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("Z389vAm47Ys", Hmd2SlotUpTo1CheckSecondThenError1002); + +std::int32_t APS5_VABI Hmd2SlotUpTo1ThenMinusOne(std::uint64_t a0) { + if (a0 > 1) return Hmd2::ParameterInvalid; + return BeforeInitialize(-1, __func__); +} +APS5_EXPORT("cDcrvEG8j8k", Hmd2SlotUpTo1ThenMinusOne); +APS5_EXPORT("tuqJO6O6cb4", Hmd2SlotUpTo1ThenMinusOne); +APS5_EXPORT("fd4fu1cziaI", Hmd2SlotUpTo1ThenMinusOne); + +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenMinusOne(std::uint64_t a0) { + if (a0 > 2) return Hmd2::ParameterInvalid; + return BeforeInitialize(-1, __func__); +} +APS5_EXPORT("UxMg7iL4nRs", Hmd2SlotUpTo2ThenMinusOne); +APS5_EXPORT("K1GELwlaaGc", Hmd2SlotUpTo2ThenMinusOne); + +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenError1002(std::uint64_t a0) { + if (a0 > 2) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("RFn39VCHjGY", Hmd2SlotUpTo2ThenError1002); + +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenError1003(std::uint64_t a0) { + if (a0 > 2) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("FRoSjceDv10", Hmd2SlotUpTo2ThenError1003); + +std::int32_t APS5_VABI Hmd2SlotUpTo2CheckSecondThenError002C(std::uint64_t a0, const void* a1) { + if (a1 == nullptr) return Hmd2::ParameterNull; + if (a0 > 2) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error002C, __func__); +} +APS5_EXPORT("wQ44gBwBw44", Hmd2SlotUpTo2CheckSecondThenError002C); + +std::int32_t APS5_VABI Hmd2IndexZeroCheckSecondThenZero(std::uint64_t a0, const void* a1) { + if (a1 == nullptr) return Hmd2::ParameterNull; + if (a0 != 0) return Hmd2::Error100F; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("wiv8+7ogrq0", Hmd2IndexZeroCheckSecondThenZero); + } diff --git a/core/libs/prx/libSceHmd2/Hmd2.hpp b/core/libs/prx/libSceHmd2/Hmd2.hpp new file mode 100644 index 0000000000..4623331f91 --- /dev/null +++ b/core/libs/prx/libSceHmd2/Hmd2.hpp @@ -0,0 +1,90 @@ +#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::int32_t Error0004 = static_cast(0x8a720004); +inline constexpr std::int32_t ReprojectionNotInitialized = static_cast(0x8a72000b); +inline constexpr std::int32_t Error002C = static_cast(0x8a72002c); +inline constexpr std::int32_t Error1002 = static_cast(0x8a721002); +inline constexpr std::int32_t Error1003 = static_cast(0x8a721003); +inline constexpr std::int32_t Error1010 = static_cast(0x8a721010); +inline constexpr std::int32_t Error800d0203 = static_cast(0x800d0203); +inline constexpr std::int32_t Error100F = static_cast(0x8a72100f); + +inline constexpr std::uint32_t UserIdSdkVersion = 0x07000000; + +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); + +struct SizeAlign { + std::uint64_t size; + std::uint64_t align; + + constexpr SizeAlign(std::uint64_t size = 0, std::uint64_t align = 0) noexcept : size(size), align(align) {} +}; + +static_assert(sizeof(SizeAlign) == 16); +static_assert(offsetof(SizeAlign, size) == 0); +static_assert(offsetof(SizeAlign, align) == 8); + +} + +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); +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQueryBufferSizeAlign(); +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQueryDisplayBufferSizeAlign(); +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign(); +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign(); +Hmd2::SizeAlign APS5_VABI sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign(); +} + +#endif diff --git a/core/libs/prx/libSceHmdSetupDialog/CMakeLists.txt b/core/libs/prx/libSceHmdSetupDialog/CMakeLists.txt new file mode 100644 index 0000000000..a294abe2b0 --- /dev/null +++ b/core/libs/prx/libSceHmdSetupDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceHmdSetupDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceHmdSetupDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceHmdSetupDialog) + +if(BUILD_TESTING) + add_test_executable(guest_hmd_setup_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestHmdSetupDialog.cpp) + target_include_directories(guest_hmd_setup_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_hmd_setup_dialog_tests PRIVATE libSceHmdSetupDialog) + add_test(NAME guest_hmd_setup_dialog COMMAND guest_hmd_setup_dialog_tests) + if(WIN32) + set_tests_properties(guest_hmd_setup_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceHmdSetupDialog/Export.cpp b/core/libs/prx/libSceHmdSetupDialog/Export.cpp new file mode 100644 index 0000000000..571643bf02 --- /dev/null +++ b/core/libs/prx/libSceHmdSetupDialog/Export.cpp @@ -0,0 +1,71 @@ +#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 sceHmdSetupDialogInitialize(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 sceHmdSetupDialogOpen(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 sceHmdSetupDialogUpdateStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceHmdSetupDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceHmdSetupDialogGetResult(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 sceHmdSetupDialogClose(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 sceHmdSetupDialogTerminate(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/libSceHttp/CMakeLists.txt b/core/libs/prx/libSceHttp/CMakeLists.txt index 48a098bec6..701f264762 100644 --- a/core/libs/prx/libSceHttp/CMakeLists.txt +++ b/core/libs/prx/libSceHttp/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceHttp SHARED EXCLUDE_FROM_ALL Export.cpp src/Parse.cpp src/Uri.cpp) +add_library(libSceHttp SHARED EXCLUDE_FROM_ALL Export.cpp src/Options.cpp src/Parse.cpp src/Uri.cpp) target_include_directories(libSceHttp PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceHttp) @@ -13,8 +13,13 @@ if(BUILD_TESTING) target_link_libraries(guest_http_response_header_tests PRIVATE libSceHttp libc) configure_windows_unwind(guest_http_response_header_tests) add_test(NAME guest_http_response_header COMMAND guest_http_response_header_tests) + add_test_executable(guest_http_options_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttpOptions.cpp) + target_include_directories(guest_http_options_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_http_options_tests PRIVATE libSceHttp libc) + configure_windows_unwind(guest_http_options_tests) + add_test(NAME guest_http_options COMMAND guest_http_options_tests) if(WIN32) - set_tests_properties(guest_http guest_http_response_header PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_http guest_http_response_header guest_http_options PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 86f20a8700..cce00df0ae 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -7,6 +7,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceHttp/src/HttpErrors.hpp" +#include "prx/libSceHttp/src/Options.hpp" #include #include #include @@ -23,6 +24,11 @@ // No network is emulated: contexts, templates and requests can be created, but any request // that would touch the network fails with the library's network error. static std::atomic g_nextHandle{1}; +struct HttpConnectionStat; +struct HttpRequestStat; + +using HttpCookieSendCallback = int (APS5_VABI *)(std::int32_t, const char*, const char*, void*); +using HttpSocketCreationCallback = void (APS5_VABI *)(std::int32_t, std::int32_t, void*); using HttpRequestStatusCallback = void (APS5_VABI *)(int, int, void*); struct RequestStatusRegistration { HttpRequestStatusCallback callback; @@ -30,6 +36,35 @@ struct RequestStatusRegistration { }; static std::mutex g_statusMutex; static std::map g_statusCallbacks; +struct HttpCookieStats { + std::size_t usedBytes; + std::uint32_t cookieCount; + std::uint32_t reserved; + std::size_t peakUsedBytes; + std::uint32_t peakCookieCount; + std::uint32_t evictedCookieCount; +}; + +static_assert(sizeof(HttpCookieStats) == 32); +static_assert(offsetof(HttpCookieStats, peakUsedBytes) == 16); + +struct HttpMemoryPoolStats { + std::size_t poolSize; + std::size_t peakUsedSize; + std::size_t usedSize; +}; + +struct HttpRequestHeader { + const char* name; + const char* value; +}; + +static_assert(sizeof(HttpMemoryPoolStats) == 24); +static_assert(offsetof(HttpMemoryPoolStats, peakUsedSize) == 8); +static_assert(offsetof(HttpMemoryPoolStats, usedSize) == 16); +static_assert(sizeof(HttpRequestHeader) == 16); +static_assert(offsetof(HttpRequestHeader, value) == 8); + struct HttpsCaList { void* certs; int certsNum; @@ -123,6 +158,10 @@ int APS5_VABI sceHttpAbortRequest(int request_id) { return 0; } +int APS5_VABI sceHttpAbortRequestForce(int request_id) { + return sceHttpAbortRequest(request_id); +} + int APS5_VABI sceHttpAddRequestHeader(int id, const char* name, const char* value, uint32_t mode) { (void)id; (void)name; @@ -131,20 +170,36 @@ int APS5_VABI sceHttpAddRequestHeader(int id, const char* name, const char* valu return 0; } +int APS5_VABI sceHttpAuthCacheFlush(int httpCtxId) { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!g_contexts.contains(httpCtxId)) return ERROR_INVALID_ID; + return 0; +} + +int APS5_VABI sceHttpCookieFlush(int httpCtxId) { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!g_contexts.contains(httpCtxId)) return ERROR_INVALID_ID; + return 0; +} + int APS5_VABI sceHttpCreateConnection(int tmpl_id, const char* server_name, const char* scheme, uint16_t port, int enable_keep_alive) { - (void)tmpl_id; (void)server_name; (void)scheme; (void)port; (void)enable_keep_alive; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + AddHttpChild(id, tmpl_id); + return id; } int APS5_VABI sceHttpCreateConnectionWithURL(int tmpl_id, const char* url, int enable_keep_alive) { - (void)tmpl_id; (void)url; (void)enable_keep_alive; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + AddHttpChild(id, tmpl_id); + return id; } int APS5_VABI sceHttpCreateEpoll(int http_ctx_id, HttpEpollHandle* eh) { @@ -159,42 +214,46 @@ int APS5_VABI sceHttpCreateEpoll(int http_ctx_id, HttpEpollHandle* eh) { } int APS5_VABI sceHttpCreateRequest(int conn_id, int method, const char* path, uint64_t content_length) { - (void)conn_id; (void)method; (void)path; (void)content_length; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + AddHttpChild(id, conn_id); + return id; } int APS5_VABI sceHttpCreateRequestWithURL2(int conn_id, const char* method, const char* url, uint64_t content_length) { - (void)conn_id; (void)method; (void)url; (void)content_length; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + AddHttpChild(id, conn_id); + return id; } int APS5_VABI sceHttpCreateTemplate(int http_ctx_id, const char* user_agent, int http_ver, int is_auto_proxy_conf) { - (void)http_ctx_id; (void)user_agent; (void)http_ver; (void)is_auto_proxy_conf; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + AddHttpTemplate(id, http_ctx_id); + return id; } int APS5_VABI sceHttpDeleteConnection(int conn_id) { - (void)conn_id; + RemoveHttpObject(conn_id); return 0; } int APS5_VABI sceHttpDeleteRequest(int req_id) { + RemoveHttpObject(req_id); std::lock_guard lock(g_statusMutex); g_statusCallbacks.erase(req_id); return 0; } int APS5_VABI sceHttpDeleteTemplate(int tmpl_id) { - (void)tmpl_id; + RemoveHttpObject(tmpl_id); return 0; } @@ -240,6 +299,7 @@ int APS5_VABI sceHttpInit_nid_postfix(int memid, int ssl_ctx_id, uint64_t pool_s const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); std::lock_guard lock(g_stateMutex); g_contexts.insert(id); + AddHttpContext(id); return id; } @@ -257,9 +317,8 @@ int APS5_VABI sceHttpSendRequest(int request_id, const void* post_data, size_t s } int APS5_VABI sceHttpSetAuthEnabled(int id, int enable) { - (void)id; - (void)enable; - return 0; + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + return SetHttpOption(id, &HttpOptions::authEnabled, enable != 0) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttpSetAuthInfoCallback(int id, HttpAuthInfoCallback callback, void* userArg) { @@ -277,9 +336,8 @@ int APS5_VABI sceHttpSetCookieEnabled(int id, int enable) { } int APS5_VABI sceHttpSetAutoRedirect(int id, int enable) { - (void)id; - (void)enable; - return 0; + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + return SetHttpOption(id, &HttpOptions::autoRedirect, enable != 0) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttpSetConnectTimeOut(int id, uint32_t usec) { @@ -289,16 +347,19 @@ int APS5_VABI sceHttpSetConnectTimeOut(int id, uint32_t usec) { } int APS5_VABI sceHttpSetEpoll(int id, HttpEpollHandle eh, void* user_arg) { - (void)id; - (void)eh; - (void)user_arg; - return 0; + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!FindEpoll(eh)) return ERROR_INVALID_ID; + return SetHttpEpollBinding(id, eh, user_arg) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttpSetNonblock(int id, int enable) { - (void)id; - (void)enable; - return 0; + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + return SetHttpOption(id, &HttpOptions::nonblock, enable != 0) ? 0 : ERROR_INVALID_ID; +} + +int APS5_VABI sceHttpTrySetNonblock(int id, int enable) { + return sceHttpSetNonblock(id, enable); } int APS5_VABI sceHttpSetRecvTimeOut(int id, uint32_t usec) { @@ -362,6 +423,7 @@ int APS5_VABI sceHttpTerm_nid_postfix(int http_ctx_id) { std::lock_guard lock(g_stateMutex); if (g_contexts.empty()) return ERROR_BEFORE_INIT; if (g_contexts.erase(http_ctx_id) == 0) return ERROR_INVALID_ID; + RemoveHttpContext(http_ctx_id); if (!g_contexts.empty()) return 0; for (auto& [handle, epoll] : g_epolls) epolls.push_back(std::move(epoll)); g_epolls.clear(); @@ -371,8 +433,8 @@ int APS5_VABI sceHttpTerm_nid_postfix(int http_ctx_id) { } int APS5_VABI sceHttpUnsetEpoll(int id) { - (void)id; - return 0; + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + return SetHttpEpollBinding(id, nullptr, nullptr) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttpWaitRequest(HttpEpollHandle eh, HttpNBEvent* nbev, int maxevents, int timeout) { @@ -396,11 +458,12 @@ int APS5_VABI sceHttpWaitRequest(HttpEpollHandle eh, HttpNBEvent* nbev, int maxe } int APS5_VABI sceHttpCreateRequestWithURL(int conn_id, int method, const char* url, uint64_t content_length) { - (void)conn_id; (void)method; (void)url; (void)content_length; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + AddHttpChild(id, conn_id); + return id; } int APS5_VABI sceHttpReadData(int request_id, void* data, size_t size) { @@ -467,43 +530,51 @@ int APS5_VABI sceHttpParseResponseHeader(const char* header, std::size_t headerL } if (!found) return notFound; - while (start < headerLen && isSpace(input[start])) { - if (input[start++] == '\n') break; - } - std::size_t end = headerLen; - std::size_t consumed = headerLen; - std::size_t scan = start; - while (scan < headerLen) { - const auto newline = input.find('\n', scan); - if (newline == std::string_view::npos) break; - const auto next = newline + 1; - if (next < headerLen && (input[next] == ' ' || input[next] == '\t')) { - scan = next; - continue; - } - end = newline > start && input[newline - 1] == '\r' ? newline - 1 : newline; - consumed = next; - break; - } - if (end == start) { + const auto continues = [&input, headerLen](std::size_t next) { return next < headerLen && (input[next] == ' ' || input[next] == '\t'); }; + const auto empty = [fieldValue, valueLen] { *fieldValue = nullptr; *valueLen = 0; return 0; + }; + while (start < headerLen) { + const std::size_t lineFeed = input[start] == '\r' && start + 1 < headerLen && input[start + 1] == '\n' ? start + 1 : start; + if (input[lineFeed] == '\n') { + if (!continues(lineFeed + 1)) return empty(); + start = lineFeed + 1; + } else if (isSpace(input[start])) { + ++start; + } else { + break; + } } - if (consumed > static_cast(std::numeric_limits::max())) { - throw std::out_of_range("sceHttpParseResponseHeader: consumed byte count exceeds int"); + if (start == headerLen) return empty(); + std::size_t end = headerLen; + std::size_t scan = start; + while (scan < headerLen) { + const auto newline = input.find('\n', scan); + if (newline == std::string_view::npos || newline + 1 == headerLen) break; + if (continues(newline + 1)) { + scan = newline + 1; + continue; + } + end = newline > start && input[newline - 1] == '\r' ? newline - 1 : newline; + break; + } + if (end > static_cast(std::numeric_limits::max())) { + throw std::out_of_range("sceHttpParseResponseHeader: value end exceeds int"); } *fieldValue = header + start; *valueLen = end - start; - return static_cast(consumed); + return static_cast(end); } int APS5_VABI sceHttpCreateRequest2(int conn_id, const char* method, const char* path, uint64_t content_length) { - (void)conn_id; (void)method; (void)path; (void)content_length; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + AddHttpChild(id, conn_id); + return id; } int APS5_VABI sceHttpsEnableOption(int id, uint32_t ssl_flags) { @@ -595,4 +666,200 @@ int APS5_VABI sceHttpsFreeCaList(int http_ctx_id, HttpsCaList* ca_list) { ca_list->certsNum = 0; return 0; } + +int APS5_VABI sceHttpAuthCacheImport(int contextId, const void* buffer, std::size_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAuthCacheExport(int contextId, void* buffer, std::size_t size, std::size_t* requiredSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgEnableProfile(void) { + return 0; +} + +int APS5_VABI sceHttpDbgSetPrintf(void) { + return 0; +} + +int APS5_VABI sceHttpDbgShowStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetMemoryPoolStats(int contextId, HttpMemoryPoolStats* stats) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgShowConnectionStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgShowRequestStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgShowMemoryPoolStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgGetConnectionStat(std::int32_t connectionId, HttpConnectionStat* stat) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpDbgGetRequestStat(std::int32_t requestId, HttpRequestStat* stat) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAddRequestHeaderRaw(int id, const char* header, int mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpRemoveRequestHeader(int id, const char* name) { + (void)id; + (void)name; + return 0; +} + +int APS5_VABI sceHttpGetAuthEnabled(int id, int* enable) { + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + if (!enable) return ERROR_INVALID_VALUE; + const auto options = FindHttpOptions(id); + if (!options) return ERROR_INVALID_ID; + *enable = options->authEnabled ? 1 : 0; + return 0; +} + +int APS5_VABI sceHttpSetEpollId(std::int32_t id, std::int32_t epollId, void* userArg) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetEpollId(std::int32_t id, std::int32_t* epollId, void** userArg) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetCookie(int contextId, const char* url, char* cookie, std::size_t* requiredSize, std::size_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetCookieEnabled(int id, int* enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAddCookie(int contextId, const char* url, const char* cookie, std::size_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieSendCallback(std::int32_t id, HttpCookieSendCallback callback, void* userArg) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCookieExport(int contextId, void* buffer, std::size_t size, std::size_t* requiredSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCookieImport(int contextId, const void* buffer, std::size_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieTotalMaxSize(int contextId, std::uint32_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieMaxSize(int contextId, std::uint32_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieMaxNum(int contextId, std::uint32_t count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieMaxNumPerDomain(int contextId, std::uint32_t count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetCookieStats(int id, HttpCookieStats* stats) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetPriorityOption(int id, int priority) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetPolicyOption(int id, int policy) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpRequestGetAllHeaders(int requestId, HttpRequestHeader* headers, std::uint32_t* headerCount, char* contentLength, std::size_t* contentLengthSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetDefaultAcceptEncodingGZIPEnabled(int http_ctx_id, int enable) { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + return SetHttpContextAcceptEncodingGzip(http_ctx_id, enable != 0) ? 0 : ERROR_INVALID_ID; +} + +int APS5_VABI sceHttpSetProxy(int id, int, int, const char* hostname, std::uint16_t) { + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + if (!hostname) return ERROR_INVALID_VALUE; + return FindHttpOptions(id) ? 0 : ERROR_INVALID_ID; +} + +int APS5_VABI sceHttpSetSocketCreationCallback(std::int32_t id, HttpSocketCreationCallback callback, void* userArg) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetHttp09Enabled(int id, int enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetRegisteredCtxIds(int contextId, int* netPoolId, int* sslContextId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpGetConnectionStat(std::int32_t connectionId, HttpConnectionStat* stat) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpAddQuery(int id, const char* query) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetDelayBuildRequestEnabled(int id, int enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceHttp/src/Options.cpp b/core/libs/prx/libSceHttp/src/Options.cpp new file mode 100644 index 0000000000..710b33311a --- /dev/null +++ b/core/libs/prx/libSceHttp/src/Options.cpp @@ -0,0 +1,148 @@ +#include "prx/libSceHttp/src/Options.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libSceHttp/src/HttpErrors.hpp" +#include +#include +#include + +namespace { + +constexpr std::uint32_t PrivateSslFlags = 0x2dffu; + +std::mutex g_optionsMutex; +std::unordered_map g_contextAcceptEncodingGzip; +std::unordered_map g_objectOptions; + +} + +void AddHttpContext(int contextId) { + std::lock_guard lock(g_optionsMutex); + g_contextAcceptEncodingGzip[contextId] = false; +} + +void RemoveHttpContext(int contextId) { + std::lock_guard lock(g_optionsMutex); + g_contextAcceptEncodingGzip.erase(contextId); + if (g_contextAcceptEncodingGzip.empty()) g_objectOptions.clear(); +} + +bool HttpInitialised() { + std::lock_guard lock(g_optionsMutex); + return !g_contextAcceptEncodingGzip.empty(); +} + +void AddHttpTemplate(int templateId, int contextId) { + std::lock_guard lock(g_optionsMutex); + HttpOptions options; + if (const auto context = g_contextAcceptEncodingGzip.find(contextId); context != g_contextAcceptEncodingGzip.end()) options.acceptEncodingGzip = context->second; + g_objectOptions[templateId] = options; +} + +void AddHttpChild(int id, int parentId) { + std::lock_guard lock(g_optionsMutex); + const auto parent = g_objectOptions.find(parentId); + g_objectOptions[id] = parent != g_objectOptions.end() ? parent->second : HttpOptions{}; +} + +void RemoveHttpObject(int id) { + std::lock_guard lock(g_optionsMutex); + g_objectOptions.erase(id); +} + +std::optional FindHttpOptions(int id) { + std::lock_guard lock(g_optionsMutex); + const auto found = g_objectOptions.find(id); + if (found == g_objectOptions.end()) return std::nullopt; + return found->second; +} + +bool SetHttpOption(int id, bool HttpOptions::*option, bool enable) { + std::lock_guard lock(g_optionsMutex); + const auto found = g_objectOptions.find(id); + if (found == g_objectOptions.end()) return false; + found->second.*option = enable; + return true; +} + +bool SetHttpContextAcceptEncodingGzip(int contextId, bool enable) { + std::lock_guard lock(g_optionsMutex); + const auto found = g_contextAcceptEncodingGzip.find(contextId); + if (found == g_contextAcceptEncodingGzip.end()) return false; + found->second = enable; + return true; +} + +bool SetHttpEpollBinding(int id, HttpEpollHandle epoll, void* argument) { + std::lock_guard lock(g_optionsMutex); + const auto found = g_objectOptions.find(id); + if (found == g_objectOptions.end()) return false; + found->second.epoll = epoll; + found->second.epollArgument = argument; + return true; +} + +extern "C" { + +int APS5_VABI sceHttpGetAutoRedirect(int id, int* enable) { + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + if (!enable) return ERROR_INVALID_VALUE; + const auto options = FindHttpOptions(id); + if (!options) return ERROR_INVALID_ID; + *enable = options->autoRedirect ? 1 : 0; + return 0; +} + +int APS5_VABI sceHttpGetNonblock(int id, int* enable) { + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + if (!enable) return ERROR_INVALID_VALUE; + const auto options = FindHttpOptions(id); + if (!options) return ERROR_INVALID_ID; + *enable = options->nonblock ? 1 : 0; + return 0; +} + +int APS5_VABI sceHttpTryGetNonblock(int id, int* enable) { + return sceHttpGetNonblock(id, enable); +} + +int APS5_VABI sceHttpGetAcceptEncodingGZIPEnabled(int id, int* enable) { + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + if (!enable) return ERROR_INVALID_VALUE; + const auto options = FindHttpOptions(id); + if (!options) return ERROR_INVALID_ID; + *enable = options->acceptEncodingGzip ? 1 : 0; + return 0; +} + +int APS5_VABI sceHttpSetAcceptEncodingGZIPEnabled(int id, int enable) { + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + return SetHttpOption(id, &HttpOptions::acceptEncodingGzip, enable != 0) ? 0 : ERROR_INVALID_ID; +} + +int APS5_VABI sceHttpGetEpoll(int id, HttpEpollHandle* epoll, void** argument) { + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + if (!epoll) return ERROR_INVALID_VALUE; + const auto options = FindHttpOptions(id); + if (!options) return ERROR_INVALID_ID; + *epoll = options->epoll; + if (argument) *argument = options->epollArgument; + return 0; +} + +int APS5_VABI sceHttpCacheRedirectedConnectionEnabled(int id, int enable) { + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + (void)enable; + return FindHttpOptions(id) ? 0 : ERROR_INVALID_ID; +} + +int APS5_VABI sceHttpsEnableOptionPrivate(int id, std::uint32_t sslFlags) { + if (!HttpInitialised()) return ERROR_BEFORE_INIT; + if ((sslFlags & ~PrivateSslFlags) != 0) return ERROR_INVALID_VALUE; + return FindHttpOptions(id) ? 0 : ERROR_INVALID_ID; +} + +int APS5_VABI sceHttpsDisableOptionPrivate(int id, std::uint32_t sslFlags) { + return sceHttpsEnableOptionPrivate(id, sslFlags); +} + +} diff --git a/core/libs/prx/libSceHttp/src/Options.hpp b/core/libs/prx/libSceHttp/src/Options.hpp new file mode 100644 index 0000000000..c92168167a --- /dev/null +++ b/core/libs/prx/libSceHttp/src/Options.hpp @@ -0,0 +1,27 @@ +#ifndef CORE_LIBS_PRX_LIBSCEHTTP_SRC_OPTIONS_HPP +#define CORE_LIBS_PRX_LIBSCEHTTP_SRC_OPTIONS_HPP + +#include "SceTypes.hpp" +#include + +struct HttpOptions { + bool autoRedirect = true; + bool authEnabled = false; + bool nonblock = false; + bool acceptEncodingGzip = false; + HttpEpollHandle epoll = nullptr; + void* epollArgument = nullptr; +}; + +void AddHttpContext(int contextId); +void RemoveHttpContext(int contextId); +bool HttpInitialised(); +void AddHttpTemplate(int templateId, int contextId); +void AddHttpChild(int id, int parentId); +void RemoveHttpObject(int id); +std::optional FindHttpOptions(int id); +bool SetHttpOption(int id, bool HttpOptions::*option, bool enable); +bool SetHttpContextAcceptEncodingGzip(int contextId, bool enable); +bool SetHttpEpollBinding(int id, HttpEpollHandle epoll, void* argument); + +#endif diff --git a/core/libs/prx/libSceHttp/src/Parse.cpp b/core/libs/prx/libSceHttp/src/Parse.cpp index 0732f62407..409e18cf1b 100644 --- a/core/libs/prx/libSceHttp/src/Parse.cpp +++ b/core/libs/prx/libSceHttp/src/Parse.cpp @@ -11,10 +11,10 @@ bool isAsciiDigit(char c) { return c >= '0' && c <= '9'; } -bool parseVersionNumber(const char* line, size_t lineLen, size_t& index, int32_t* value) { - if (index >= lineLen || !isAsciiDigit(line[index])) return false; +bool parseVersionNumber(const char* line, size_t& index, int32_t* value) { + if (!isAsciiDigit(line[index])) return false; uint32_t number = 0; - while (index < lineLen && isAsciiDigit(line[index])) { + while (isAsciiDigit(line[index])) { number = number * 10 + static_cast(line[index] - '0'); ++index; } @@ -36,11 +36,11 @@ int APS5_VABI sceHttpParseStatusLine(const char* statusLine, size_t lineLen, int if (lineLen < 8 || std::memcmp(statusLine, "HTTP/", 5) != 0) return ERROR_PARSE_HTTP_INVALID_RESPONSE; size_t index = 5; - if (!parseVersionNumber(statusLine, lineLen, index, httpMajorVer)) return ERROR_PARSE_HTTP_INVALID_RESPONSE; - if (index >= lineLen || statusLine[index] != '.') return ERROR_PARSE_HTTP_INVALID_RESPONSE; + if (!parseVersionNumber(statusLine, index, httpMajorVer)) return ERROR_PARSE_HTTP_INVALID_RESPONSE; + if (statusLine[index] != '.') return ERROR_PARSE_HTTP_INVALID_RESPONSE; ++index; - if (!parseVersionNumber(statusLine, lineLen, index, httpMinorVer)) return ERROR_PARSE_HTTP_INVALID_RESPONSE; - if (index >= lineLen || statusLine[index] != ' ') return ERROR_PARSE_HTTP_INVALID_RESPONSE; + if (!parseVersionNumber(statusLine, index, httpMinorVer)) return ERROR_PARSE_HTTP_INVALID_RESPONSE; + if (statusLine[index] != ' ') return ERROR_PARSE_HTTP_INVALID_RESPONSE; ++index; if (lineLen - index < 3) return ERROR_PARSE_HTTP_INVALID_RESPONSE; @@ -51,10 +51,11 @@ int APS5_VABI sceHttpParseStatusLine(const char* statusLine, size_t lineLen, int index += 3; const char* phrase = statusLine + index; - const void* lineFeed = std::memchr(phrase, '\n', lineLen - index); - if (!lineFeed) return ERROR_PARSE_HTTP_INVALID_RESPONSE; + const size_t searched = lineLen - index + 1; + size_t length = 0; + while (length != searched && phrase[length] != '\n') ++length; + if (length == searched) return ERROR_PARSE_HTTP_INVALID_RESPONSE; - size_t length = static_cast(static_cast(lineFeed) - phrase); const size_t consumed = index + length + 1; if (consumed > static_cast(INT32_MAX)) { throw std::out_of_range("sceHttpParseStatusLine: consumed byte count exceeds int"); diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index 710a665726..2cf41f6f6b 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -1,5 +1,4 @@ #include -#include #include #include #include @@ -8,8 +7,11 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libSceHttp/src/HttpErrors.hpp" +extern "C" int APS5_VABI sceHttpUriSweepPath(char* dst, const char* src, size_t srcSize); + namespace { constexpr uint32_t URI_BUILD_WITH_SCHEME = 0x01; @@ -29,56 +31,59 @@ struct UriParts { std::string_view password; std::string_view hostname; std::string path; + size_t pathSpace = 0; std::string_view query; std::string_view fragment; uint16_t port = 0; }; +bool isAsciiAlpha(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} + +bool isAsciiDigit(char c) { + return c >= '0' && c <= '9'; +} + +char toAsciiLower(char c) { + return c >= 'A' && c <= 'Z' ? static_cast(c + ('a' - 'A')) : c; +} + bool equalsIgnoreCase(std::string_view left, std::string_view right) { if (left.size() != right.size()) return false; for (size_t i = 0; i < left.size(); ++i) { - if (std::tolower(static_cast(left[i])) != std::tolower(static_cast(right[i]))) return false; + if (toAsciiLower(left[i]) != toAsciiLower(right[i])) return false; } return true; } uint16_t defaultPort(std::string_view scheme) { - if (equalsIgnoreCase(scheme, "http")) return 80; - if (equalsIgnoreCase(scheme, "https")) return 443; + if (scheme.size() >= 5 && equalsIgnoreCase(scheme.substr(0, 5), "https")) return 443; + if (scheme.size() >= 4 && equalsIgnoreCase(scheme.substr(0, 4), "http")) return 80; return 0; } bool isHostCharacter(char c) { - return std::isalnum(static_cast(c)) || c == '-' || c == '.' || c == '_'; + return isAsciiAlpha(c) || isAsciiDigit(c) || c == '-' || c == '.' || c == '_'; } -std::string removeDotSegments(std::string_view input) { - std::string output; - while (!input.empty()) { - if (input.starts_with("../")) input.remove_prefix(3); - else if (input.starts_with("./")) input.remove_prefix(2); - else if (input.starts_with("/./")) input.remove_prefix(2); - else if (input == "/.") input = "/"; - else if (input.starts_with("/../") || input == "/..") { - input = input.size() == 3 ? std::string_view("/") : input.substr(3); - const size_t slash = output.rfind('/'); - output.erase(slash == std::string::npos ? 0 : slash); - } else if (input == "." || input == "..") input = {}; - else { - const size_t end = input.find('/', 1); - const size_t length = end == std::string_view::npos ? input.size() : end; - output.append(input.substr(0, length)); - input.remove_prefix(length); - } - } - return output; +bool isUserinfoCharacter(char c) { + return isAsciiAlpha(c) || isAsciiDigit(c) || std::string_view("-._~!$&'()*+,;=%:").find(c) != std::string_view::npos; +} + +std::string sweptPath(std::string_view path) { + const std::string source(path); + std::string swept(source.size() + 1, '\0'); + sceHttpUriSweepPath(swept.data(), source.c_str(), source.size() + 1); + swept.resize(std::strlen(swept.c_str())); + return swept; } size_t schemeLength(std::string_view uri) { size_t schemeEnd = 0; - if (!uri.empty() && std::isalpha(static_cast(uri[0]))) { + if (!uri.empty() && (isAsciiAlpha(uri[0]) || uri[0] == '+' || uri[0] == '-' || uri[0] == '.')) { schemeEnd = 1; - while (schemeEnd < uri.size() && (std::isalnum(static_cast(uri[schemeEnd])) || uri[schemeEnd] == '+' + while (schemeEnd < uri.size() && (isAsciiAlpha(uri[schemeEnd]) || isAsciiDigit(uri[schemeEnd]) || uri[schemeEnd] == '+' || uri[schemeEnd] == '-' || uri[schemeEnd] == '.')) ++schemeEnd; } return schemeEnd > 0 && schemeEnd < uri.size() && uri[schemeEnd] == ':' ? schemeEnd : 0; @@ -98,49 +103,51 @@ int parseUri(std::string_view uri, UriParts& parts) { parts.opaque = !uri.starts_with("//"); if (!parts.opaque) uri.remove_prefix(2); - std::string_view authority = uri.substr(0, uri.find_first_of("/?#")); - uri.remove_prefix(authority.size()); - - const size_t at = authority.rfind('@'); - if (at != std::string_view::npos) { - const std::string_view userinfo = authority.substr(0, at); + size_t userinfoEnd = 0; + while (userinfoEnd < uri.size() && isUserinfoCharacter(uri[userinfoEnd])) ++userinfoEnd; + if (userinfoEnd < uri.size() && uri[userinfoEnd] == '@') { + const std::string_view userinfo = uri.substr(0, userinfoEnd); const size_t colon = userinfo.find(':'); parts.username = userinfo.substr(0, colon); if (colon != std::string_view::npos) parts.password = userinfo.substr(colon + 1); - authority.remove_prefix(at + 1); + uri.remove_prefix(userinfoEnd + 1); } - std::string_view port; - if (authority.starts_with('[')) { - const size_t close = authority.find(']'); + if (uri.starts_with('[')) { + const size_t close = uri.find(']'); if (close == std::string_view::npos) return ERROR_INVALID_URL; - parts.hostname = authority.substr(1, close - 1); - authority.remove_prefix(close + 1); - if (!authority.empty() && authority[0] != ':') return ERROR_INVALID_URL; - if (!authority.empty()) port = authority.substr(1); + parts.hostname = uri.substr(1, close - 1); for (const char c : parts.hostname) { if (!isHostCharacter(c) && c != ':') return ERROR_INVALID_URL; } + uri.remove_prefix(close + 1); } else { - const size_t colon = authority.find(':'); - parts.hostname = authority.substr(0, colon); - if (colon != std::string_view::npos) port = authority.substr(colon + 1); - for (const char c : parts.hostname) { - if (!isHostCharacter(c)) return ERROR_INVALID_URL; + size_t hostEnd = 0; + if (!uri.starts_with('.')) { + while (hostEnd < uri.size() && isHostCharacter(uri[hostEnd])) ++hostEnd; } + parts.hostname = uri.substr(0, hostEnd); + uri.remove_prefix(hostEnd); } - if (port.size() > 5) return ERROR_INVALID_URL; - uint32_t portValue = 0; - for (const char c : port) { - if (!std::isdigit(static_cast(c))) return ERROR_INVALID_URL; - portValue = portValue * 10 + static_cast(c - '0'); + parts.port = defaultPort(parts.scheme); + if (uri.starts_with(':')) { + uri.remove_prefix(1); + const std::string_view port = uri.substr(0, uri.find('/')); + uri.remove_prefix(port.size()); + if (port.size() > 5) return ERROR_INVALID_URL; + uint32_t portValue = 0; + for (const char c : port) { + if (!isAsciiDigit(c)) return ERROR_INVALID_URL; + portValue = portValue * 10 + static_cast(c - '0'); + } + if (portValue > 0x10000) return ERROR_INVALID_URL; + if (!port.empty()) parts.port = static_cast(portValue); } - if (portValue > 0xFFFF) return ERROR_INVALID_URL; - parts.port = port.empty() ? defaultPort(parts.scheme) : static_cast(portValue); const std::string_view path = uri.substr(0, uri.find_first_of("?#")); - parts.path = removeDotSegments(path); + parts.path = sweptPath(path); + parts.pathSpace = path.size(); uri.remove_prefix(path.size()); if (uri.starts_with('?')) { @@ -152,8 +159,8 @@ int parseUri(std::string_view uri, UriParts& parts) { } size_t poolSize(const UriParts& parts) { - const std::string_view fields[] = {parts.scheme, parts.username, parts.password, parts.hostname, parts.path, parts.query, parts.fragment}; - size_t size = 0; + const std::string_view fields[] = {parts.scheme, parts.username, parts.password, parts.hostname, parts.query, parts.fragment}; + size_t size = parts.pathSpace + 1; for (const std::string_view field : fields) size += field.size() + 1; return size; } @@ -170,31 +177,42 @@ int copyOut(const std::string& value, char* out, size_t* require, size_t prepare extern "C" { -int APS5_VABI sceHttpUriBuild(char* out, size_t* require, size_t prepare, const SceHttpUriElement* src_element, uint32_t option) { - if (!src_element) return ERROR_INVALID_URL; - if (!out && !require) return ERROR_INVALID_VALUE; +int APS5_VABI sceHttpUriBuild(char* out, size_t* requiredSize, size_t capacity, const SceHttpUriElement* element, uint32_t option) { + if (!element) return ERROR_INVALID_URL; + if (!out && !requiredSize) return ERROR_INVALID_VALUE; - auto field = [](const char* value) { return value ? std::string_view(value) : std::string_view(); }; - const std::string_view scheme = field(src_element->scheme); - const std::string_view username = option & URI_BUILD_WITH_USERNAME ? field(src_element->username) : std::string_view(); - const std::string_view password = option & URI_BUILD_WITH_PASSWORD ? field(src_element->password) : std::string_view(); - const std::string_view hostname = option & URI_BUILD_WITH_HOSTNAME ? field(src_element->hostname) : std::string_view(); + const auto field = [](const char* value, size_t limit) { + size_t length = 0; + if (value) while (length < limit && value[length] != '\0') ++length; + return value ? std::string_view(value, length) : std::string_view(); + }; + const auto scheme = option & (URI_BUILD_WITH_SCHEME | URI_BUILD_WITH_PORT) ? field(element->scheme, 32) : std::string_view(); + const auto username = option & URI_BUILD_WITH_USERNAME ? field(element->username, 256) : std::string_view(); + const auto password = option & URI_BUILD_WITH_PASSWORD ? field(element->password, 256) : std::string_view(); + const auto hostname = option & URI_BUILD_WITH_HOSTNAME ? field(element->hostname, 255) : std::string_view(); std::string uri; if (option & URI_BUILD_WITH_SCHEME && !scheme.empty()) uri.append(scheme).push_back(':'); - if (!src_element->opaque) uri.append("//"); + if (element->opaque != 1) uri.append("//"); uri.append(username); if (!password.empty()) uri.append(":").append(password); if (!username.empty() || !password.empty()) uri.push_back('@'); - if (hostname.find(':') != std::string_view::npos) uri.append("[").append(hostname).append("]"); - else uri.append(hostname); - if (option & URI_BUILD_WITH_PORT && src_element->port != 0 && src_element->port != defaultPort(scheme)) { - uri.append(":").append(std::to_string(src_element->port)); + uri.append(hostname); + if (option & URI_BUILD_WITH_PORT && element->port != 0) { + std::uint16_t port = 0; + if (scheme.size() >= 5 && equalsIgnoreCase(scheme.substr(0, 5), "https")) port = 443; + else if ((scheme.size() >= 4 && equalsIgnoreCase(scheme.substr(0, 4), "http")) || (scheme.size() >= 3 && equalsIgnoreCase(scheme.substr(0, 3), "ttp"))) port = 80; + if (element->port != port) uri.append(":").append(std::to_string(element->port)); } - if (option & URI_BUILD_WITH_PATH) uri.append(field(src_element->path)); - if (option & URI_BUILD_WITH_QUERY) uri.append(field(src_element->query)); - if (option & URI_BUILD_WITH_FRAGMENT) uri.append(field(src_element->fragment)); - return copyOut(uri, out, require, prepare); + if (option & URI_BUILD_WITH_PATH) uri.append(field(element->path, URI_MAX_LENGTH)); + if (option & URI_BUILD_WITH_QUERY) uri.append(field(element->query, URI_MAX_LENGTH)); + if (option & URI_BUILD_WITH_FRAGMENT) uri.append(field(element->fragment, URI_MAX_LENGTH)); + const size_t size = uri.size() + 1 + ((option & URI_BUILD_WITH_SCHEME) && element->scheme && scheme.empty()); + if (requiredSize) *requiredSize = size; + if (!out) return 0; + if (capacity < size) return ERROR_OUT_OF_MEMORY; + std::memcpy(out, uri.c_str(), uri.size() + 1); + return 0; } int APS5_VABI sceHttpUriEscape(char* out, size_t* require, size_t prepare, const char* in) { @@ -205,7 +223,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('%'); @@ -216,24 +235,56 @@ int APS5_VABI sceHttpUriEscape(char* out, size_t* require, size_t prepare, const return copyOut(escaped, out, require, prepare); } -int APS5_VABI sceHttpUriUnescape(char* out, size_t* require, size_t prepare, const char* in) { - if (!in) return ERROR_INVALID_VALUE; +int APS5_VABI sceHttpUriUnescape(char* out, size_t* requiredSize, size_t capacity, const char* input) { + if (!input || (!out && !requiredSize)) return ERROR_INVALID_VALUE; - const std::string_view input(in); - std::string decoded; - for (size_t i = 0; i < input.size(); ++i) { - if (input[i] == '%' && input.size() - i >= 3) { - unsigned int value = 0; - const auto result = std::from_chars(input.data() + i + 1, input.data() + i + 3, value, 16); - if (result.ec == std::errc{} && result.ptr == input.data() + i + 3) { - decoded.push_back(static_cast(value)); - i += 2; - continue; + const std::string_view source(input); + const auto decode = [](const char* digits, unsigned int& value) { + const auto result = std::from_chars(digits, digits + 2, value, 16); + return result.ec == std::errc{} && result.ptr == digits + 2; + }; + for (size_t index = 0; index < source.size(); ++index) { + if (source[index] != '%') continue; + unsigned int value = 0; + if (source.size() - index < 3 || !decode(source.data() + index + 1, value)) return ERROR_INVALID_URL; + index += 2; + } + + if (!source.empty()) { + std::uint32_t sdkVersion = 0; + const auto* parameters = static_cast(ApplicationProcessParameters_nid_no_patch()); + std::memcpy(&sdkVersion, parameters + 0x10, sizeof(sdkVersion)); + if (sdkVersion < 0x1000000) { + for (size_t index = 0; index < source.size(); ++index) { + const char value = source[index]; + if (value == '%') { + index += 2; + continue; + } + if (!isAsciiAlpha(value) && !isAsciiDigit(value) && value != '*' && value != '-' && value != '.' && value != '_' && value != '+') return ERROR_INVALID_URL; } } - decoded.push_back(input[i]); } - return copyOut(decoded, out, require, prepare); + + size_t written = 0; + for (size_t index = 0; index < source.size(); ++index) { + if (out && written >= capacity) return ERROR_OUT_OF_MEMORY; + char value = source[index]; + if (value == '%') { + unsigned int decoded = 0; + decode(source.data() + index + 1, decoded); + value = static_cast(decoded); + index += 2; + } + if (out) out[written] = value; + ++written; + } + if (out) { + if (written >= capacity) return ERROR_OUT_OF_MEMORY; + out[written] = '\0'; + } + if (requiredSize) *requiredSize = written + 1; + return 0; } int APS5_VABI sceHttpUriMerge(char* merged_url, const char* url, const char* relative_uri, size_t* require, size_t prepare, uint32_t option) { @@ -289,10 +340,12 @@ int APS5_VABI sceHttpUriParse(SceHttpUriElement* out, const char* src_url, void* if (const int result = parseUri(src_url, parts); result != 0) return result; const std::string_view fields[] = {parts.scheme, parts.username, parts.password, parts.hostname, parts.path, parts.query, parts.fragment}; + const size_t spaces[] = {parts.scheme.size(), parts.username.size(), parts.password.size(), parts.hostname.size(), parts.pathSpace, parts.query.size(), + parts.fragment.size()}; const size_t size = poolSize(parts); + if (write && prepare < size) return ERROR_OUT_OF_MEMORY; if (require) *require = size; if (!write) return 0; - if (prepare < size) return ERROR_OUT_OF_MEMORY; char* next = static_cast(pool); char* strings[7]; @@ -300,7 +353,7 @@ int APS5_VABI sceHttpUriParse(SceHttpUriElement* out, const char* src_url, void* strings[i] = next; if (!fields[i].empty()) std::memcpy(next, fields[i].data(), fields[i].size()); next[fields[i].size()] = '\0'; - next += fields[i].size() + 1; + next += spaces[i] + 1; } *out = {}; out->opaque = parts.opaque; @@ -315,12 +368,53 @@ int APS5_VABI sceHttpUriParse(SceHttpUriElement* out, const char* src_url, void* return 0; } +int APS5_VABI sceHttpUriCopy(SceHttpUriElement* out, const SceHttpUriElement* src, void* pool, size_t prepare, size_t* require) { + if (!src) return ERROR_INVALID_URL; + const bool write = out && pool; + if (!write && !require) return ERROR_INVALID_VALUE; + + if (write) { + std::memset(out, 0, sizeof(*out)); + std::memset(pool, 0, prepare); + } + const char* const fields[] = {src->scheme, src->hostname, src->path, src->username, src->password}; + const uint16_t port = src->port; + size_t lengths[5]; + size_t used = 0; + for (size_t i = 0; i < 5; ++i) { + lengths[i] = fields[i] ? std::strlen(fields[i]) : 0; + used += lengths[i] + 1; + } + if (!write) { + *require = used; + return 0; + } + if (used > prepare) return ERROR_OUT_OF_MEMORY; + + char* next = static_cast(pool); + char* strings[5]; + for (size_t i = 0; i < 5; ++i) { + strings[i] = next; + if (lengths[i] != 0) std::memcpy(next, fields[i], lengths[i]); + next[lengths[i]] = '\0'; + next += lengths[i] + 1; + } + out->scheme = strings[0]; + out->hostname = strings[1]; + out->path = strings[2]; + out->username = strings[3]; + out->password = strings[4]; + out->port = port; + if (require) *require = used; + return 0; +} + int APS5_VABI sceHttpUriSweepPath(char* dst, const char* src, size_t srcSize) { if (srcSize == 0) return 0; if (!dst || !src) return ERROR_INVALID_VALUE; const size_t length = srcSize - 1; - if (length == 0 || src[0] != '/') { + if (src[0] != '/') { std::memcpy(dst, src, length); dst[length] = '\0'; return 0; @@ -335,7 +429,7 @@ int APS5_VABI sceHttpUriSweepPath(char* dst, const char* src, size_t srcSize) { pos += 2; continue; } - if (src[pos] == '.' && pos + 2 < length && src[pos + 1] == '.' && src[pos + 2] == '/') { + if (src[pos] == '.' && pos + 2 <= length && src[pos + 1] == '.' && src[pos + 2] == '/') { if (end != 0) { dst[end] = '\0'; end = static_cast(std::strrchr(dst, '/') - dst); diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index 36dfae81ec..d97f8d6c6d 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -23,6 +25,7 @@ struct Http2Handle { Http2Kind kind; int owner; std::size_t poolSize; + std::shared_ptr cookieMemory; }; static std::atomic g_nextHandle{1}; @@ -101,6 +104,11 @@ static int CompleteAsync(const char* function, int req_id, const Http2AsyncOptio return 0; } +using Http2WebSocketTextCallback = void (APS5_VABI*)(std::int32_t requestId, const char* text, std::uint32_t flags, void* userData); +using Http2WebSocketDataCallback = void (APS5_VABI*)(std::int32_t requestId, const void* data, std::size_t dataSize, std::uint32_t flags, void* userData); +using Http2WebSocketCloseCallback = void (APS5_VABI*)(std::int32_t requestId, const std::uint16_t* status, const char* reason, std::uint32_t flags, void* userData); +using Http2WebSocketErrorCallback = void (APS5_VABI*)(std::int32_t requestId, std::int32_t error, void* userData); + extern "C" { int APS5_VABI sceHttp2AddRequestHeader(int id, const char* name, const char* value, uint32_t mode) { @@ -348,11 +356,14 @@ int APS5_VABI sceHttp2CookieFlush(int id) { return 0; } -int APS5_VABI sceHttp2CreateCookieBox(int lib_http2_ctx_id) { +int APS5_VABI sceHttp2CreateCookieBox(std::size_t poolSize) { + if (poolSize < 0x10000) return static_cast(0x817b121du); + if (poolSize > std::numeric_limits::max() - 0x3fff) throw std::overflow_error("sceHttp2CreateCookieBox: pool size overflow"); + const auto allocationSize = (poolSize + 0x3fff) & ~std::size_t{0x3fff}; + auto memory = std::make_shared(allocationSize); std::lock_guard lock(g_handlesMutex); - if (FindHandle(lib_http2_ctx_id, Http2Kind::Context) == nullptr) return ERROR_INVALID_ID; const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); - g_handles[id] = {Http2Kind::CookieBox, lib_http2_ctx_id, 0}; + g_handles[id] = {Http2Kind::CookieBox, 0, poolSize, std::move(memory)}; return id; } @@ -381,27 +392,121 @@ int APS5_VABI sceHttp2SetResolveRetry(int id, int32_t retry) { return 0; } -int APS5_VABI sceHttp2WebSocketCreateRequest() { +int APS5_VABI sceHttp2WebSocketCreateRequest(std::int32_t templateId, const char* url, Http2WebSocketTextCallback textCallback, Http2WebSocketDataCallback dataCallback, Http2WebSocketCloseCallback closeCallback, Http2WebSocketErrorCallback errorCallback, void* receiveBuffer, std::size_t receiveBufferSize, void* userData, const void* option) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttp2WebSocketCloseAsync() { +int APS5_VABI sceHttp2WebSocketCloseAsync(std::int32_t requestId, const std::uint16_t* status, const char* reason, std::uint8_t flags, const Http2AsyncOption* asyncOption, const void* option) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttp2WebSocketSendTextMessageAsync() { +int APS5_VABI sceHttp2WebSocketSendTextMessageAsync(std::int32_t requestId, const char* text, std::uint8_t flags, const Http2AsyncOption* asyncOption, const void* option) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttp2WebSocketSendDataMessageAsync() { +int APS5_VABI sceHttp2WebSocketSendDataMessageAsync(std::int32_t requestId, const void* data, std::size_t dataSize, std::uint8_t flags, const Http2AsyncOption* asyncOption, const void* option) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHttp2WebSocketSetPingInterval() { +int APS5_VABI sceHttp2WebSocketSetPingInterval(std::int32_t requestId, std::uint32_t intervalMicroseconds) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2WebSocketSetPingTimeout(std::int32_t requestId, std::uint32_t timeoutMicroseconds) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceHttp2AddCookie(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2AuthCacheFlush(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2CookieExport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2CookieImport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2GetAuthEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2GetAutoRedirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2GetCookie(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2GetCookieBox(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2GetCookieStats(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2RedirectCacheFlush(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2RemoveRequestHeader(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetAuthInfoCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetCookieMaxNum(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetCookieMaxNumPerDomain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetCookieMaxSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetCookieRecvCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetCookieSendCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetPreSendCallback(void) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceHttpCache/Export.cpp b/core/libs/prx/libSceHttpCache/Export.cpp new file mode 100644 index 0000000000..12a2497b87 --- /dev/null +++ b/core/libs/prx/libSceHttpCache/Export.cpp @@ -0,0 +1,191 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +struct HttpCacheHeader { + const char* name; + const char* value; +}; + +struct HttpCacheRequest { + std::uint32_t method; + std::uint32_t reserved; + const char* url; + std::size_t headerCount; + const HttpCacheHeader* headers; +}; + +struct HttpCacheResponseHeader { + std::uint32_t statusCode; + std::uint32_t reserved; + std::size_t headerCount; + const HttpCacheHeader* headers; +}; + +struct HttpCacheResponse { + std::uint32_t statusCode; + std::uint32_t reserved; + std::size_t headerCount; + const HttpCacheHeader* headers; + std::uint64_t contentLength; +}; + +struct HttpCacheQuotaEntry { + const char* name; + std::uint64_t size; +}; + +struct HttpCacheQuota { + std::size_t count; + const HttpCacheQuotaEntry* entries; +}; + +static_assert(sizeof(HttpCacheHeader) == 16); +static_assert(sizeof(HttpCacheRequest) == 32); +static_assert(offsetof(HttpCacheRequest, url) == 8); +static_assert(offsetof(HttpCacheRequest, headers) == 24); +static_assert(sizeof(HttpCacheResponseHeader) == 24); +static_assert(offsetof(HttpCacheResponseHeader, headers) == 16); +static_assert(sizeof(HttpCacheResponse) == 32); +static_assert(offsetof(HttpCacheResponse, contentLength) == 24); +static_assert(sizeof(HttpCacheQuotaEntry) == 16); +static_assert(sizeof(HttpCacheQuota) == 16); + +extern "C" { + +int APS5_VABI sceHttpCacheClear(std::int32_t cacheId, std::int32_t userId, const char* tag) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheClearAll(std::int32_t cacheId, std::int32_t userId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheCompleteRequest(std::int32_t requestId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheCreateRequest(std::int32_t cacheId, std::int32_t userId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheCreateRequestWithTag(std::int32_t cacheId, std::int32_t userId, const char* tag) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheDeleteRequest(std::int32_t requestId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheInit(std::size_t poolSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheReadData(std::int32_t requestId, std::size_t size, void* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRetrieve(std::int32_t requestId, const HttpCacheRequest* request, HttpCacheResponse* response, HttpCacheRequest* nextRequest, std::int32_t* result) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRetrieveWithMemoryPool(std::int32_t requestId, const HttpCacheRequest* request, HttpCacheResponse* response, HttpCacheRequest* nextRequest, std::int32_t* result, void* pool, std::size_t* requiredSize, std::size_t poolSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRevalidate(std::int32_t requestId, const HttpCacheResponseHeader* header, HttpCacheResponse* response, std::int32_t* result) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRevalidateWithMemoryPool(std::int32_t requestId, const HttpCacheResponseHeader* header, HttpCacheResponse* response, std::int32_t* result, void* pool, std::size_t* requiredSize, std::size_t poolSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSetCacheSharing(std::uint8_t enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSetData(std::int32_t requestId, const void* data, std::size_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSetQuota(std::int32_t cacheId, const HttpCacheQuota* quota) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSetResponseHeader(std::int32_t requestId, const HttpCacheResponseHeader* header) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemClearAll(const char* path) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemInit(const char* path) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemSendStatistics() { + return 0; +} + +int APS5_VABI sceHttpCacheSystemShutdown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemTerm() { + return 0; +} + +int APS5_VABI sceHttpCacheTerm(std::int32_t cacheId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + + +int APS5_VABI sceHttpCacheClearAllUserTags() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheClearCacheInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheDebugSetUrl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRegisterCacheInfoUrl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRemoveTag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceHubAppUtil/Export.cpp b/core/libs/prx/libSceHubAppUtil/Export.cpp new file mode 100644 index 0000000000..b4a5ae963d --- /dev/null +++ b/core/libs/prx/libSceHubAppUtil/Export.cpp @@ -0,0 +1,23 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceHubAppUtilDeeplinkToLiveDetailView() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHubAppUtilDeeplinkToPSSubscriptions() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHubAppUtilGetInstallationStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceIcu/Export.cpp b/core/libs/prx/libSceIcu/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceIcu/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceIdu/Export.cpp b/core/libs/prx/libSceIdu/Export.cpp new file mode 100644 index 0000000000..def1b88f9a --- /dev/null +++ b/core/libs/prx/libSceIdu/Export.cpp @@ -0,0 +1,48 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceIduGetEnv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceIduInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceIduInstallPkg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceIduIsAppInstalled() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceIduIsRifInstalled() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceIduIsStaffMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceIduReboot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceIduTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceImageUtil/Export.cpp b/core/libs/prx/libSceImageUtil/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceImageUtil/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceIme/CMakeLists.txt b/core/libs/prx/libSceIme/CMakeLists.txt index 5f45b0907c..460b1fed31 100644 --- a/core/libs/prx/libSceIme/CMakeLists.txt +++ b/core/libs/prx/libSceIme/CMakeLists.txt @@ -23,6 +23,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_ime_filter_text_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeFilterText.cpp) + target_include_directories(guest_ime_filter_text_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ime_filter_text_tests PRIVATE libSceIme libc) + configure_windows_unwind(guest_ime_filter_text_tests) + add_test(NAME guest_ime_filter_text COMMAND guest_ime_filter_text_tests) + if(WIN32) + set_tests_properties(guest_ime_filter_text PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_ime_open_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeOpen.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libSceVideoOut/src/ImeTextInput.cpp) target_include_directories(guest_ime_open_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index d686445211..9485063034 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -8,10 +9,36 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceIme/include/ImeFilterText.hpp" #include "prx/libSceIme/include/ImeInput.hpp" +struct ImeVshContext; +struct ImeVshParameter; +struct ImePreeditMetaInfo; +struct ImeDictionaryWord; + +struct ImeVshCaretGeometry { + std::int32_t x; + std::int32_t y; + std::uint32_t height; + std::uint32_t index; +}; + +struct ImeVshTextGeometry { + std::int32_t x; + std::int32_t y; + std::uint32_t width; + std::uint32_t height; +}; + +static_assert(sizeof(ImeEditText) == 64); +static_assert(offsetof(ImeEvent, param) == 8); +static_assert(sizeof(ImeVshCaretGeometry) == 16); +static_assert(sizeof(ImeVshTextGeometry) == 16); + namespace { constexpr int ErrorBusy = static_cast(0x80bc0001u); @@ -37,6 +64,7 @@ constexpr int ErrorInvalidMode = static_cast(0x80bc0024u); constexpr int ErrorInvalidParam = static_cast(0x80bc0030u); constexpr int ErrorInvalidAddress = static_cast(0x80bc0031u); constexpr int ErrorInvalidReserved = static_cast(0x80bc0032u); +constexpr int ErrorInternal = static_cast(0x80bc00ffu); constexpr uint32_t ValidKeyboardModes = 0x0000007f; constexpr uint32_t TypeNumber = 4; constexpr uint32_t ValidOptions = 0x00007bff; @@ -58,15 +86,25 @@ constexpr uint32_t CaretMoveRight = 2; struct Session { Param param{}; + ImeExtKeyboardFilter extKeyboardFilter = nullptr; std::u16string text; std::uint32_t caret = 0; std::deque events; + std::deque hostInputs; + std::uint64_t id = 0; + std::uint64_t revision = 0; +}; + +struct DeferredFilter { + std::u16string oldText; }; std::mutex g_keyboardMutex; std::set g_openKeyboards; std::mutex g_imeMutex; std::optional g_session; +std::atomic g_textSink{nullptr}; +std::uint64_t g_nextSessionId = 0; void PanelSize(const Param& param, uint32_t* width, uint32_t* height) { const uint32_t scale = (param.option & OptionUseOver2K) != 0 ? 2 : 1; @@ -155,7 +193,68 @@ void QueueCaretSteps(Session& session, uint32_t direction, std::size_t steps) { } } -void Insert(Session& session, std::u16string units) { +struct EditDelta { + std::uint32_t index = 0; + std::int32_t length = 0; +}; + +EditDelta ComputeEditDelta(const std::u16string& before, const std::u16string& after) { + std::size_t prefix = 0; + while (prefix < before.size() && prefix < after.size() && before[prefix] == after[prefix]) ++prefix; + std::size_t beforeTail = before.size(); + std::size_t afterTail = after.size(); + while (beforeTail > prefix && afterTail > prefix && before[beforeTail - 1] == after[afterTail - 1]) { + --beforeTail; + --afterTail; + } + return {static_cast(prefix), + static_cast(afterTail - prefix) - static_cast(beforeTail - prefix)}; +} + +void NormalizeCaret(Session& session) { + if (session.caret > session.text.size()) session.caret = static_cast(session.text.size()); + if (session.caret > 0 && session.caret < session.text.size() && + IsHighSurrogate(session.text[session.caret - 1]) && IsLowSurrogate(session.text[session.caret])) { + ++session.caret; + } +} + +bool RunTextFilter(ImeTextFilter filter, const std::u16string& text, std::uint32_t maxLength, bool multiline, + std::u16string* filtered) { + std::vector buffer(maxLength + 1, u'\0'); + std::uint32_t length = maxLength; + if (filter(buffer.data(), &length, text.c_str(), static_cast(text.size())) != 0) { + return false; + } + if (length > maxLength || !ValidText(buffer.data(), length, multiline)) { + return false; + } + filtered->assign(buffer.data(), buffer.data() + length); + return true; +} + +void CommitFilteredEdit(Session& session, const std::u16string* filtered, const std::u16string& oldText) { + if (filtered) { + session.text = *filtered; + NormalizeCaret(session); + } + if (session.text == oldText) return; + SyncBuffers(session); + const EditDelta edit = ComputeEditDelta(oldText, session.text); + QueueText(session, EventUpdateText, edit.index, edit.length); +} + +std::optional CommitEdit(Session& session, const std::u16string& oldText, std::uint32_t plainIndex, + std::int32_t plainDelta) { + if (session.param.filter) { + return DeferredFilter{oldText}; + } + SyncBuffers(session); + QueueText(session, EventUpdateText, plainIndex, plainDelta); + return std::nullopt; +} + +std::optional Insert(Session& session, std::u16string units) { const bool multiline = (session.param.option & OptionMultiline) != 0; std::u16string accepted; for (const char16_t unit : units) { @@ -165,12 +264,12 @@ void Insert(Session& session, std::u16string units) { const std::size_t room = session.param.max_text_length - session.text.size(); std::size_t count = std::min(accepted.size(), room); if (count > 0 && count < accepted.size() && IsHighSurrogate(accepted[count - 1])) --count; - if (count == 0) return; + if (count == 0) return std::nullopt; const uint32_t index = session.caret; + const std::u16string oldText = session.text; session.text.insert(index, accepted, 0, count); session.caret += static_cast(count); - SyncBuffers(session); - QueueText(session, EventUpdateText, index, static_cast(count)); + return CommitEdit(session, oldText, index, static_cast(count)); } std::size_t UnitsBefore(const Session& session) { @@ -185,65 +284,61 @@ std::size_t UnitsAfter(const Session& session) { return caret + 1 < session.text.size() && IsHighSurrogate(session.text[caret]) && IsLowSurrogate(session.text[caret + 1]) ? 2 : 1; } -void Apply(Session& session, const ImeHostInput& input) { +std::optional Apply(Session& session, const ImeHostInput& input) { switch (input.kind) { case ImeHostInputKind::Text: { std::size_t size = 0; while (size < sizeof(input.text) && input.text[size] != '\0') ++size; - Insert(session, DecodeUtf8(input.text, size)); - return; + return Insert(session, DecodeUtf8(input.text, size)); } case ImeHostInputKind::Backspace: { const std::size_t units = UnitsBefore(session); - if (units == 0) return; + if (units == 0) return std::nullopt; + const std::u16string oldText = session.text; session.caret -= static_cast(units); session.text.erase(session.caret, units); - SyncBuffers(session); - QueueText(session, EventUpdateText, session.caret, -static_cast(units)); - return; + return CommitEdit(session, oldText, session.caret, -static_cast(units)); } case ImeHostInputKind::Delete: { const std::size_t units = UnitsAfter(session); - if (units == 0) return; + if (units == 0) return std::nullopt; + const std::u16string oldText = session.text; session.text.erase(session.caret, units); - SyncBuffers(session); - QueueText(session, EventUpdateText, session.caret, -static_cast(units)); - return; + return CommitEdit(session, oldText, session.caret, -static_cast(units)); } case ImeHostInputKind::Left: { const std::size_t units = UnitsBefore(session); session.caret -= static_cast(units); QueueCaretSteps(session, CaretMoveLeft, units); - return; + return std::nullopt; } case ImeHostInputKind::Right: { const std::size_t units = UnitsAfter(session); session.caret += static_cast(units); QueueCaretSteps(session, CaretMoveRight, units); - return; + return std::nullopt; } case ImeHostInputKind::Home: { const std::size_t units = session.caret; session.caret = 0; QueueCaretSteps(session, CaretMoveLeft, units); - return; + return std::nullopt; } case ImeHostInputKind::End: { const std::size_t units = session.text.size() - session.caret; session.caret = static_cast(session.text.size()); QueueCaretSteps(session, CaretMoveRight, units); - return; + return std::nullopt; } case ImeHostInputKind::Enter: if ((session.param.option & OptionMultiline) != 0) { - Insert(session, u"\n"); - } else { - QueueText(session, EventPressEnter, session.caret, 0); + return Insert(session, u"\n"); } - return; + QueueText(session, EventPressEnter, session.caret, 0); + return std::nullopt; case ImeHostInputKind::Escape: QueueText(session, EventPressClose, session.caret, 0); - return; + return std::nullopt; } throw std::invalid_argument("IME host input: unknown input kind"); } @@ -284,23 +379,41 @@ int ValidateOpen(const Param* param, const ExtendedParam* extended) { extern "C" { void ImePublishInput_nid_postfix(const ImeHostInput& input) { - std::lock_guard lock(g_imeMutex); - if (g_session) Apply(*g_session, input); + { + std::lock_guard lock(g_imeMutex); + if (g_session) { + g_session->hostInputs.push_back(input); + return; + } + } + const ImeHostTextSink sink = g_textSink.load(); + if (sink == nullptr) return; + std::u16string units; + if (input.kind == ImeHostInputKind::Text) { + std::size_t size = 0; + while (size < sizeof(input.text) && input.text[size] != '\0') ++size; + units = DecodeUtf8(input.text, size); + } + sink(input.kind, units.data(), units.size()); } bool ImeCapturesKeyboard_nid_postfix() { std::lock_guard lock(g_imeMutex); - return g_session.has_value(); + return g_session.has_value() || g_textSink.load() != nullptr; +} + +void ImeSetHostTextSink_nid_no_patch(ImeHostTextSink sink) { + g_textSink = sink; } int APS5_VABI sceImeOpen_nid_postfix(const Param* param, const ExtendedParam* extended) { std::lock_guard lock(g_imeMutex); if (g_session) return ErrorBusy; if (const int error = ValidateOpen(param, extended)) return error; - if (param->filter) NotImplemented_nid_no_patch("sceImeOpen: text filter"); - if (extended && extended->ext_keyboard_filter) NotImplemented_nid_no_patch("sceImeOpen: external keyboard filter"); Session session; + session.id = ++g_nextSessionId; session.param = *param; + session.extKeyboardFilter = extended ? extended->ext_keyboard_filter : nullptr; session.text.assign(param->input_text_buffer, TextLength(param->input_text_buffer, param->max_text_length)); session.caret = static_cast(session.text.size()); SyncBuffers(session); @@ -346,7 +459,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 +482,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; } @@ -380,6 +493,16 @@ int APS5_VABI sceImeKeyboardSetMode(int32_t user_id, uint32_t mode) { return (mode & ~ValidKeyboardModes) == 0 ? 0 : ErrorInvalidMode; } +int APS5_VABI sceImeFilterText(char16_t* dst, int32_t* out_length, uint32_t capacity, + const ime_filter::FilterParam* param, const char16_t* src, uint32_t src_length) { + return static_cast(ime_filter::FilterText(dst, out_length, capacity, param, src, src_length)); +} + +int APS5_VABI sceImeCheckFilterText(const void* param) { + (void)param; + NotImplemented_nid_no_patch(__func__); +} + void APS5_VABI sceImeParamInit(Param* param) { if (!param) return; std::memset(param, 0, sizeof(*param)); @@ -392,6 +515,13 @@ int APS5_VABI sceImeSetCaret(const Caret* caret) { if (!caret) return ErrorInvalidAddress; if (caret->index > g_session->text.size()) return ErrorInvalidParam; g_session->caret = caret->index; + ++g_session->revision; + return 0; +} + +int APS5_VABI sceImeSetConsecutiveClauseConversionEnabled(std::uint8_t enabled) { + (void)enabled; + NotImplemented_nid_no_patch(__func__); return 0; } @@ -403,6 +533,7 @@ int APS5_VABI sceImeSetText(const char16_t* text, uint32_t length) { const std::size_t count = TextLength(text, std::min(length, session.param.max_text_length)); if (!ValidText(text, count, (session.param.option & OptionMultiline) != 0)) return ErrorInvalidText; session.text.assign(text, count); + ++session.revision; if (session.caret > session.text.size()) session.caret = static_cast(session.text.size()); SyncBuffers(session); return 0; @@ -421,15 +552,280 @@ int APS5_VABI sceImeUpdate(EventHandler handler) { for (;;) { ImeEvent event{}; void* arg = nullptr; + bool deliver = false; + bool runFilter = false; + ImeTextFilter filter = nullptr; + std::u16string filterText; + std::u16string filterOldText; + std::uint32_t filterMaxLength = 0; + bool filterMultiline = false; + std::uint64_t sessionId = 0; + std::uint64_t sessionRevision = 0; { std::lock_guard lock(g_imeMutex); - if (!g_session || g_session->param.handler != handler || g_session->events.empty()) return 0; - event = g_session->events.front(); - g_session->events.pop_front(); - arg = g_session->param.arg; + if (!g_session || g_session->param.handler != handler) return 0; + if (!g_session->hostInputs.empty()) { + const ImeHostInput input = g_session->hostInputs.front(); + g_session->hostInputs.pop_front(); + if (const std::optional deferred = Apply(*g_session, input)) { + runFilter = true; + filter = g_session->param.filter; + filterText = g_session->text; + filterOldText = deferred->oldText; + filterMaxLength = g_session->param.max_text_length; + filterMultiline = (g_session->param.option & OptionMultiline) != 0; + sessionId = g_session->id; + sessionRevision = g_session->revision; + } + } else if (!g_session->events.empty()) { + event = g_session->events.front(); + g_session->events.pop_front(); + arg = g_session->param.arg; + deliver = true; + } else { + return 0; + } } + if (runFilter) { + std::u16string filtered; + const bool accepted = RunTextFilter(filter, filterText, filterMaxLength, filterMultiline, &filtered); + std::lock_guard lock(g_imeMutex); + if (g_session && g_session->id == sessionId) { + if (g_session->revision == sessionRevision) { + CommitFilteredEdit(*g_session, accepted ? &filtered : nullptr, filterOldText); + } else if (g_session->text == filterText) { + g_session->text = filterOldText; + NormalizeCaret(*g_session); + } + } + continue; + } + if (!deliver) continue; handler(arg, &event); } } +int APS5_VABI sceImeVshOpen(ImeVshContext** context, const ImeVshParameter* parameter, const char16_t* text) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeKeyboardUpdate(EventHandler handler) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshUpdate(ImeVshContext* context) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshUpdateContext(ImeVshContext* context, const char16_t* text, std::uint32_t caretIndex) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshClearPreedit(ImeVshContext* context) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifySetText(ImeVshContext* context, const char16_t* text, std::uint32_t length) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetCaretGeometry(ImeVshContext* context, const ImeVshCaretGeometry* geometry) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshConfirmPreedit(ImeVshContext* context) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetCaretIndexInPreedit(ImeVshContext* context, std::int32_t index) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetPreeditGeometry(ImeVshContext* context, const ImeVshTextGeometry* geometry) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetSelectGeometry(ImeVshContext* context, const ImeVshTextGeometry* geometry) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshClose(ImeVshContext* context) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshGetPanelPositionAndForm(ImeVshContext* context, PositionAndForm* form) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeSetCandidateIndex(std::int32_t index) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeConfirmCandidate(std::int32_t index) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDisableController(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeSetVoiceDictationEnabled(bool enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeSetEditEventWithRemotePlayEnabled(bool enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeForTestFunction(void) { + return ErrorInternal; +} + +int APS5_VABI sceImeVshInformConfirmdString2(ImeVshContext* context, const char16_t* text, std::int32_t firstParameter, std::int32_t secondParameter, std::int32_t thirdParameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetSelectionText(ImeVshContext* context, const char16_t* text, std::uint32_t length) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeCheckUpdateTextInfo(ImeEditText* info, std::uint8_t repair) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeCheckRemoteEventParam(ImeEvent* event, std::uint8_t repair) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeKeyboardOpenInternal(std::int32_t userId, EventHandler handler, const KeyboardParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetParam(ImeVshContext* context, const ImeVshParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshUpdateContext2(ImeVshContext* context, const char16_t* text, std::uint32_t caretIndex) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSendTextInfo(ImeVshContext* context, const void* info, std::uint32_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshInformConfirmdString(ImeVshContext* context, const char16_t* text, std::int32_t parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyCurrentTextInfo(ImeVshContext* context, const char16_t* text, std::int32_t caretIndex, std::int32_t preeditFirst, std::int32_t preeditSecond, std::int32_t selectionFirst, std::int32_t selectionSecond) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyCurrentTextInfoForReconvertText(ImeVshContext* context, const char16_t* text, std::int32_t caretIndex, std::int32_t preeditFirst, std::int32_t preeditSecond, std::int32_t selectionFirst, std::int32_t selectionSecond, std::int32_t option) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyDeleteText(ImeVshContext* context, const char16_t* text, std::int32_t length, std::int32_t parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyUpdateCaretText(ImeVshContext* context, const char16_t* text, std::int32_t length, std::int32_t parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshCheckAutoReConvertingPreeditText(ImeVshContext* context, const char16_t* text) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshSetPanelPosition(ImeVshContext* context, float x, float y, std::int32_t horizontalAlignment, std::int32_t verticalAlignment) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeVshNotifyPreeditMetaInfo(ImeVshContext* context, const ImePreeditMetaInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeConfigSet(std::uint64_t unusedArgument, std::uint64_t configuration, std::uint32_t key, std::uint32_t value) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeConfigGet(std::uint64_t unusedArgument, std::uint64_t configuration, std::uint32_t key) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicGetWords(std::int32_t userId, std::uint64_t dictionary, ImeDictionaryWord* words) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicAddWord(std::int32_t userId, std::uint64_t dictionary, const ImeDictionaryWord* word) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicDeleteWord(std::int32_t userId, std::uint64_t dictionary, const ImeDictionaryWord* word) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicReplaceWord(std::int32_t userId, std::uint64_t dictionary, const ImeDictionaryWord* oldWord, const ImeDictionaryWord* newWord) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicDeleteLearnDics(std::int32_t userId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceImeDicDeleteUserDics(std::int32_t userId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI FinalizeImeModule_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI InitializeImeModule_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp b/core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp new file mode 100644 index 0000000000..4249c9b1d7 --- /dev/null +++ b/core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp @@ -0,0 +1,299 @@ +#pragma once +#include + +namespace ime_filter { + +inline constexpr std::uint64_t kCodepageABitmap[1024] = { + 0xffffffffffffffffull, 0xffffffffffffffffull, 0xffffffff00000000ull, 0xffffffffffffffffull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0xfffe03fbfffe0000ull, 0x00000000000003fbull, + 0xffffffffffff0002ull, 0x000000000002ffffull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x480d006333710000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000080200400008ull, 0x03ff03ff00000000ull, 0x00000000000f0000ull, 0x0000000000140000ull, + 0x20305fa1e406098dull, 0x00000cc300040000ull, 0x80000020000000ccull, 0x0000000000000000ull, + 0x0000000000040000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x000fffff00000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x999999393999900full, 0x0000000000000804ull, 0x300c000300000000ull, 0x000080000000c8c0ull, + 0x0000000000000060ull, 0x0000a40000000005ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x00000000b03fffefull, 0xfffffffffffffffeull, 0xfffffffe780fffffull, 0x787fffffffffffffull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0206000000000000ull, 0x0000000000000000ull, 0x000001f000000000ull, 0x0000000000000000ull, + 0x084008cc01102008ull, 0x7800000000822600ull, 0x000000027000c000ull, 0x0000000000002010ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x9b46254243f36f8bull, 0x400a0004e3e0e82cull, 0x04497977db365f65ull, 0x18c5603ae3f0ecd7ull, + 0x375180003403e60bull, 0x986982007eebe0c8ull, 0x8060e8032d56ad48ull, 0xc568c03aad93661cull, + 0x02403f7ec656aa60ull, 0x21751020146183cdull, 0x40bc300007122021ull, 0x0a3060a84562a624ull, + 0x9c84040285740217ull, 0x11e27f3414157ffbull, 0x60ff1f7522efb665ull, 0x676336c338403a70ull, + 0x0fc946b020b24dd9ull, 0xa03f86384850bc98ull, 0x5232be4998162388ull, 0xc72c00ddeba422abull, + 0x8f0a841b26e1a1e7ull, 0x89bfc241559e27ebull, 0x084d636185480014ull, 0x05cfff3eaad07f0cull, + 0x7b407a41a803ff1aull, 0x38eb050080024745ull, 0x710c9b341005dc51ull, 0xa404636601000397ull, + 0x430ac000005180d0ull, 0x5800000830c89071ull, 0x00415f80f7000ed9ull, 0x62800018941000b0ull, + 0x0156820009d00240ull, 0x05101d1008015004ull, 0x10504025001084c1ull, 0xa60d40094d8a410full, + 0x098121c0914cab19ull, 0x800006720203c485ull, 0x0009141d00080b04ull, 0x16900009905c49c9ull, + 0x2433841222200c65ull, 0x42250a0447960c03ull, 0x4f0c4900d0880028ull, 0x3e87d830d3aa14a2ull, + 0x41867ea41f618e04ull, 0x211857ad2dbbc390ull, 0x4e0411382a48241eull, 0x88400d60161b0a40ull, + 0x106082219502020aull, 0x8000144404000243ull, 0x700000000c040000ull, 0x0c00024a00c11a06ull, + 0x4045140400401a00ull, 0x052b0a78bdf30029ull, 0x8379407cbfa0bba9ull, 0xc5695bf6e91d12fdull, + 0xff022115444aeff6ull, 0x0242d033402bed63ull, 0x5dca1b4200131000ull, 0x2c61a703020000a0ull, + 0x000002848ff24880ull, 0x0048b200100d5804ull, 0x3780500420011894ull, 0x68be49ea684d3200ull, + 0x21c9a8202e42184cull, 0xff7c001e80b050b9ull, 0x01e028c114e0849aull, 0xdddb130fac49870eull, + 0x51b2a2e289fbbe1aull, 0x928b3ec632ca5522ull, 0x32986703438f1dbfull, 0xa923081173c03028ull, + 0x04028fe33a65c000ull, 0x00a1bf3da6252c4eull, 0x317c06c98cd43e3aull, 0x0edf018bd52a00e0ull, + 0xf09111838c22e34bull, 0x40fbc9aca7287d94ull, 0x44445a9007534484ull, 0xf5d4004800013fc8ull, + 0x891dc442ec5f7701ull, 0xd242410949286b83ull, 0x3a22184059fe061dull, 0xc0eaf0033b9fb7e4ull, + 0xe400898082021386ull, 0x0cc44b8010a1b200ull, 0x48341faf8944d309ull, 0x0470420a0c458259ull, + 0x4450314010c8a040ull, 0x0540828101004004ull, 0x1a056a30642c0108ull, 0x645690cf051460a6ull, + 0xcbf09c1831000021ull, 0x01b5104c63e2e120ull, 0x3281b8b29a83538cull, 0x0c0233e70a84987aull, + 0x9872e1b1d038d6cdull, 0x0459c3f4e2848a1eull, 0xd314484523c2439aull, 0xffbd024136400292ull, + 0xa5d27dc0e8f0eb09ull, 0xd0afa47fd24bc242ull, 0x0bd8824734a11aa0ull, 0xc83ad294651bc453ull, + 0x33140e0640c8001eull, 0xc0d00088b21f615full, 0x166ba1c5a898a02aull, 0x0604c08b85b4af50ull, + 0xa251056e1e04f933ull, 0x73b8ed0776380400ull, 0xc816408119324406ull, 0xaa04298463097c8aull, + 0x27614e0eca9c1c24ull, 0xc10c0846830009d0ull, 0x0908540d10816011ull, 0x0c000514cc0a000eull, + 0x6784008ba0440430ull, 0x8b18865e8a195288ull, 0x9cbe8c1041602e59ull, 0x00089800895c6861ull, + 0xc1900018089a8100ull, 0x640d8505f4a14007ull, 0xff0a48060e4d314eull, 0x000b852e2ea81632ull, + 0x696c0e20ca841810ull, 0x0390d65816000032ull, 0x112490001a6851a0ull, 0x1fae5d52432698e1ull, + 0x5700fafbae280fa0ull, 0xc044c88099406408ull, 0xa4c48424b1419005ull, 0xc1949000603a1a34ull, + 0xc106180d003a8246ull, 0x1511e05099100022ull, 0x022a041a00824157ull, 0x446ad8138930004full, + 0x400511c0ed228aa2ull, 0x3101880801021000ull, 0x0f08f80002044620ull, 0x22020000a2008900ull, + 0x1040004216108210ull, 0x200052f4126052c0ull, 0x4202110082308510ull, 0xda2070e180b5430aull, + 0xfc65350008012040ull, 0x62140286ab0419c1ull, 0x4246908500440087ull, 0x338032070a85405cull, + 0xc0d0ce30b8c00400ull, 0x0da505080080c030ull, 0x280c020000400a90ull, 0x4122642940446705ull, + 0x847c4664000002e8ull, 0x4049861dde200002ull, 0x20010084c0000a08ull, 0x01c742cd10108400ull, + 0x1d8f9968d52a703aull, 0x81d9aef53e12be50ull, 0x732e08282412cec4ull, 0xd41d020c4b3424acull, + 0x0811009780002a02ull, 0x7d451786114411c4ull, 0x879140405e4949ddull, 0x491444bad8c4254cull, + 0x15800271c8001b92ull, 0xc200096a0c0000c1ull, 0xba49302140024800ull, 0x1008e2ac1c802080ull, + 0x841400e300341004ull, 0x1414981020004020ull, 0x5420868804aa70c2ull, 0x2010918004130c62ull, + 0x54011c4002064082ull, 0x84802125e4e90383ull, 0xe60944c02810e433ull, 0x080112da81260a03ull, + 0xf886400197906901ull, 0xa6510a0e0081e24dull, 0x8441c60081ec011aull, 0x8741acefb62eadb8ull, + 0x026811614b028d54ull, 0x043350a02057bb60ull, 0x01122402f7b4a8c0ull, 0x00c8227120009ad3ull, + 0xe1800c8a809e2081ull, 0x402810318151b009ull, 0x620e69b689a52a0eull, 0x4d548085d1444425ull, + 0x862dd8071fb12c75ull, 0x226e414e5841d97cull, 0xedb7f80d9e088200ull, 0x0814931375668c80ull, + 0x6ea6484ec8040e32ull, 0xba0126c066742c4aull, 0x00000000185dd70cull, 0x0000000000000000ull, + 0x0540000000000000ull, 0x03a54f81813370a0ull, 0x2344c31a641055ecull, 0x1a090a4300341462ull, + 0xa848010213a5187bull, 0xe2dd8106c5440440ull, 0x0416b6262d481af0ull, 0x311280326e405058ull, + 0x420a82080c0007e4ull, 0x87134860803b4840ull, 0xe52903193428850dull, 0x5c1825a9870a2345ull, + 0x03e85e00d9c577a6ull, 0x41c6cd54a7000081ull, 0x2b0ab860a2042800ull, 0x0e1a08eada9e0020ull, + 0x0376890811c0427eull, 0x98a8000401058621ull, 0x20220d05c44846a0ull, 0x28d78a01914854a2ull, + 0x3122160500087898ull, 0x06a2fa4e08804340ull, 0x9b14200292110814ull, 0x9010500016432e52ull, + 0x2020304285ba0041ull, 0x40802f0807a84f0bull, 0x0601df501a930591ull, 0x4e8006303021a202ull, + 0x8001a00404c80cc4ull, 0x0a020880d4316000ull, 0x00418e1800281c00ull, 0x4b00f210ca106ad0ull, + 0x889002201506274dull, 0x8150454982a85a00ull, 0x2c08880480002004ull, 0x4ac48001000508d1ull, + 0x0a42008e0062e0a0ull, 0xe0a5090e6a8c3055ull, 0x80b3481442c42906ull, 0x733c0102b330803eull, + 0x09400c20700d1494ull, 0xc094a451c040301aull, 0xa40c96c205c88dcaull, 0x011000c834040001ull, + 0x1cda2428a9cd550dull, 0x120f7a4d48370142ull, 0xd20531fb452a32b4ull, 0x45ca68d7dc44b894ull, + 0x420819432ed15097ull, 0xa09798409d48d202ull, 0x00000000064d5409ull, 0x0000000000000000ull, + 0x8480000000000000ull, 0x17001c0604215542ull, 0xb9ddff8761107624ull, 0x3c11245d5c0a659full, + 0x00000000005dadb0ull, 0x00db28d000000000ull, 0x4408010802000422ull, 0x90288d0aac409804ull, + 0x00310400e0018700ull, 0x1054001982211794ull, 0x40039c02021a2cb2ull, 0x7900080c8804bd60ull, + 0xcb088640ba3c1628ull, 0x0000001e90807274ull, 0x9c87e188d8000000ull, 0x2791ae6404124034ull, + 0x5366408fe6fbe86bull, 0xb5e4e3ab537feea6ull, 0x012285480002869full, 0x20a0211648004402ull, + 0x0005208002240004ull, 0x01ac162c01547e00ull, 0x05308c1410852a84ull, 0x906060fafdc3fbc3ull, + 0x9690120040336440ull, 0x418200d44e834b31ull, 0x028020801d6a0129ull, 0x9f0c269102ad8000ull, + 0x0c24d96f67018044ull, 0x5021500118d02910ull, 0x0201709004d01000ull, 0x0100013261c30148ull, + 0x0562080207190088ull, 0xf0a104054c0e0132ull, 0x0000000000000002ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0080000000000000ull, 0x5a0421bd035e8e8dull, 0x0000002611703488ull, + 0x8804c50210000000ull, 0x25ed147cf801b815ull, 0x1bd785893bb0ed60ull, 0x0ac50d0c1a627af3ull, + 0x6b0d0490524ae5d1ull, 0x16122b575266a35cull, 0x001829491101a872ull, 0x886c600010080948ull, + 0x39903012058f916eull, 0x001b88a049b0f840ull, 0x0042850000000000ull, 0x7014ea0498000058ull, + 0x60005193611d1628ull, 0x0000000000a71a24ull, 0x1018712043c00000ull, 0x89066004a9270172ull, + 0x40810900020cc022ull, 0x00000e348ca0602dull, 0x1101210000000000ull, 0x0892ec4cd31a8011ull, + 0x1806c7ac85000040ull, 0x003480000512e03eull, 0x0a126d0180cec008ull, 0x0027011e08568641ull, + 0x4e05e032083d3751ull, 0x01400081048401c0ull, 0x0000000000000000ull, 0x00591aa000000000ull, + 0xc8001d48882443c8ull, 0x0405981372030152ull, 0x0d148a1004008280ull, 0x2704a04002088056ull, + 0x000000004e000000ull, 0xa320000000000000ull, 0xdf002660a0ae1902ull, 0x3ad081217b17f010ull, + 0x4800100300284180ull, 0x00c414cf8014cc00ull, 0x0000000130202000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000020000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000010000000ull, + 0x00003fffffffc000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0xfffffffffffffffeull, 0xfffffffe7fffffffull, 0x00000000ffffffffull, 0x0000003f00000000ull, +}; + +struct CodepageRange { + std::uint16_t first; + std::uint16_t last; +}; + +inline constexpr CodepageRange kCodepageBRanges[151] = { + {0x0000, 0x00ff}, {0x0101, 0x0101}, {0x0113, 0x0113}, {0x011b, 0x011b}, {0x012b, 0x012b}, {0x0144, 0x0144}, + {0x0148, 0x0148}, {0x014d, 0x014d}, {0x016b, 0x016b}, {0x01ce, 0x01ce}, {0x01d0, 0x01d0}, {0x01d2, 0x01d2}, + {0x01d4, 0x01d4}, {0x01d6, 0x01d6}, {0x01d8, 0x01d8}, {0x01da, 0x01da}, {0x01dc, 0x01dc}, {0x01f9, 0x01f9}, + {0x0251, 0x0251}, {0x0261, 0x0261}, {0x02c7, 0x02c7}, {0x02c9, 0x02cb}, {0x02d9, 0x02d9}, {0x0391, 0x03a1}, + {0x03a3, 0x03a9}, {0x03b1, 0x03c1}, {0x03c3, 0x03c9}, {0x0401, 0x0401}, {0x0410, 0x044f}, {0x0451, 0x0451}, + {0x2010, 0x2010}, {0x2013, 0x2016}, {0x2018, 0x2019}, {0x201c, 0x201d}, {0x2025, 0x2026}, {0x2030, 0x2030}, + {0x2032, 0x2033}, {0x2035, 0x2035}, {0x203b, 0x203b}, {0x20ac, 0x20ac}, {0x2103, 0x2103}, {0x2105, 0x2105}, + {0x2109, 0x2109}, {0x2116, 0x2116}, {0x2121, 0x2121}, {0x2160, 0x216b}, {0x2170, 0x2179}, {0x2190, 0x2193}, + {0x2196, 0x2199}, {0x2208, 0x2208}, {0x220f, 0x220f}, {0x2211, 0x2211}, {0x2215, 0x2215}, {0x221a, 0x221a}, + {0x221d, 0x2220}, {0x2223, 0x2223}, {0x2225, 0x2225}, {0x2227, 0x222b}, {0x222e, 0x222e}, {0x2234, 0x2237}, + {0x223d, 0x223d}, {0x2248, 0x2248}, {0x224c, 0x224c}, {0x2252, 0x2252}, {0x2260, 0x2261}, {0x2264, 0x2267}, + {0x226e, 0x226f}, {0x2295, 0x2295}, {0x2299, 0x2299}, {0x22a5, 0x22a5}, {0x22bf, 0x22bf}, {0x2312, 0x2312}, + {0x2460, 0x2469}, {0x2474, 0x249b}, {0x2500, 0x254b}, {0x2550, 0x2573}, {0x2581, 0x258f}, {0x2593, 0x2595}, + {0x25a0, 0x25a1}, {0x25b2, 0x25b3}, {0x25bc, 0x25bd}, {0x25c6, 0x25c7}, {0x25cb, 0x25cb}, {0x25ce, 0x25cf}, + {0x25e2, 0x25e5}, {0x2605, 0x2606}, {0x2609, 0x2609}, {0x2640, 0x2640}, {0x2642, 0x2642}, {0x2e84, 0x2e84}, + {0x2e88, 0x2e88}, {0x2e8b, 0x2e8c}, {0x2e97, 0x2e97}, {0x2ea7, 0x2ea7}, {0x2eaa, 0x2eaa}, {0x2eb6, 0x2eb7}, + {0x2ebb, 0x2ebb}, {0x2ff0, 0x2ffb}, {0x3000, 0x3003}, {0x3005, 0x3017}, {0x301d, 0x301e}, {0x3021, 0x3029}, + {0x303e, 0x303e}, {0x3041, 0x3093}, {0x309b, 0x309e}, {0x30a1, 0x30f6}, {0x30fc, 0x30fe}, {0x3105, 0x3129}, + {0x3220, 0x3229}, {0x3231, 0x3231}, {0x32a3, 0x32a3}, {0x338e, 0x338f}, {0x339c, 0x339e}, {0x33a1, 0x33a1}, + {0x33c4, 0x33c4}, {0x33ce, 0x33ce}, {0x33d1, 0x33d2}, {0x33d5, 0x33d5}, {0x3400, 0x3917}, {0x3919, 0x39ce}, + {0x39d0, 0x39de}, {0x39e0, 0x415e}, {0x4160, 0x4336}, {0x4338, 0x4728}, {0x472a, 0x477b}, {0x477d, 0x478c}, + {0x478e, 0x4c9e}, {0x4ca1, 0x4ca2}, {0x4ca4, 0x4db5}, {0x4e00, 0x9fa5}, {0xe766, 0xe864}, {0xf92c, 0xf92c}, + {0xf979, 0xf979}, {0xf995, 0xf995}, {0xf9e7, 0xf9e7}, {0xf9f1, 0xf9f1}, {0xfa0c, 0xfa0f}, {0xfa11, 0xfa11}, + {0xfa13, 0xfa14}, {0xfa18, 0xfa18}, {0xfa1f, 0xfa21}, {0xfa23, 0xfa24}, {0xfa27, 0xfa29}, {0xfe30, 0xfe31}, + {0xfe33, 0xfe44}, {0xfe49, 0xfe52}, {0xfe54, 0xfe57}, {0xfe59, 0xfe66}, {0xfe68, 0xfe6b}, {0xff01, 0xff5e}, + {0xffe0, 0xffe5}, +}; + +} diff --git a/core/libs/prx/libSceIme/include/ImeFilterText.hpp b/core/libs/prx/libSceIme/include/ImeFilterText.hpp new file mode 100644 index 0000000000..6c465fa07e --- /dev/null +++ b/core/libs/prx/libSceIme/include/ImeFilterText.hpp @@ -0,0 +1,177 @@ +#pragma once + +#include +#include + +#include "ImeFilterCodepages.hpp" + +namespace ime_filter { + +struct FilterParam { + std::uint64_t mask; + std::uint32_t type; + std::uint32_t version; + std::uint32_t variant; +}; + +static_assert(sizeof(FilterParam) == 24); + +constexpr std::uint32_t kErrorInvalidParam = 0x80bc0030u; +constexpr std::uint32_t kErrorInvalidAddress = 0x80bc0031u; + +constexpr std::uint64_t kClassCyrillic = 0x200000400ull; +constexpr std::uint64_t kClassVietnamese = 0x40000000ull; +constexpr std::uint64_t kClassRomanian = 0x80000000ull; +constexpr std::uint64_t kClassThai = 0x4000000ull; + +constexpr std::uint64_t kAbsentMasks[8] = {0x188000000ull, 0x40000000ull, 0x10000000ull, 0x1000000ull, + 0x2000ull, 0x18000ull, 0x6000ull, 0x4000ull}; + +constexpr char16_t kPunctuationLatin[] = { + 0xa1, 0xbf, 0xb4, 0x2018, 0x2019, 0x201a, 0x201c, 0x201d, 0x201e, 0xb0, 0x2039, 0x203a, + 0xab, 0xbb, 0xaa, 0xba, 0xd7, 0xf7, 0xa4, 0xa2, 0x20ac, 0xa3, 0xa5, 0x20a9, + 0xa7, 0xa6, 0xb5, 0xac, 0xb7, 0xb9, 0xb2, 0xb3, 0xbc, 0xbd, 0xbe, 0x2116}; + +constexpr char16_t kPunctuationFullwidth[] = { + 0x3001, 0x3002, 0x30fc, 0x30fb, 0x300c, 0x300d, 0xffe5, 0xff01, 0x201d, 0xff03, 0xff04, 0xff05, 0xff06, + 0x2019, 0xff08, 0xff09, 0xff0a, 0xff0b, 0xff0c, 0xff0d, 0xff0e, 0xff0f, 0xff1a, 0xff1b, 0xff1c, 0xff1d, + 0xff1e, 0xff1f, 0xff20, 0xff3b, 0xff3c, 0xff3d, 0xff3e, 0xff3f, 0xff40, 0xff5b, 0xff5c, 0xff5d, 0xff5e}; + +constexpr char16_t kPunctuationFullwidthNoProlong[] = { + 0x3001, 0x3002, 0x300c, 0x300d, 0xff01, 0x201d, 0xff03, 0xff04, 0xff05, 0xff06, 0x2019, 0xff08, + 0xff09, 0xff0a, 0xff0b, 0xff0c, 0xff0d, 0xff0e, 0xff0f, 0xff1a, 0xff1b, 0xff1c, 0xff1d, 0xff1e, + 0xff1f, 0xff20, 0xff3b, 0xff3c, 0xff3d, 0xff3e, 0xff3f, 0xff40, 0xff5b, 0xff5c, 0xff5d, 0xff5e}; + +constexpr char16_t kVietnameseLetters[] = {0x1a0, 0x1a1, 0x1af, 0x1b0, 0x20ab}; +constexpr char16_t kModifierLetters[] = {0x2c7, 0x2d8, 0x2d9, 0x2db, 0x2dd}; +constexpr char16_t kNumberSeparators[] = {0x2c, 0x2e, 0x2d, 0x0a}; + +template +constexpr bool In(const char16_t (&set)[N], std::uint32_t c) { + for (char16_t v : set) { + if (v == c) return true; + } + return false; +} + +constexpr bool InCodepageA(std::uint32_t c) { + return ((kCodepageABitmap[c >> 6] >> (c & 63)) & 1u) != 0; +} + +constexpr bool InCodepageB(std::uint32_t c) { + constexpr std::size_t count = sizeof(kCodepageBRanges) / sizeof(kCodepageBRanges[0]); + std::size_t low = 0; + std::size_t high = count; + while (low < high) { + const std::size_t middle = (low + high) / 2; + if (c > kCodepageBRanges[middle].last) { + low = middle + 1; + } else { + high = middle; + } + } + return low < count && c >= kCodepageBRanges[low].first; +} + +constexpr bool IsVietnameseExtension(std::uint32_t c) { + return static_cast(c - 0x300) < 0x24 || (c & 0xff00) == 0x1e00 || In(kVietnameseLetters, c); +} + +constexpr std::uint64_t DerivedMask(std::uint32_t version, std::uint32_t variant) { + const std::uint32_t second = variant == 1 ? 0x10000000u : 0x6000000u; + const std::uint64_t flag = (variant == 0 || version >= 0x2000000u) ? (1u << 24) : 0; + std::uint64_t low = flag + 0x1fffffu; + const std::uint64_t high = flag | 0x21fffffu; + if (version >= 0x2500000u || variant == 0) low = high; + const std::uint64_t wide = 0x200000000ull + low - 0x4000000ull; + if (version >= 0x5000000u || variant == 0) low = wide; + return version < second ? low : (low | 0x200000000ull); +} + +constexpr bool ValidMask(std::uint64_t mask, std::uint32_t version, std::uint32_t variant, std::uint32_t* limit) { + const bool none = variant == 0; + const bool wide = none || version >= 0x5000000u; + const std::uint64_t shifted = (none || version >= 0x2500000u ? 1u : 0u) << 25; + const std::uint64_t first = wide ? 0x200000000ull + shifted - 0x17e1e401ull : (shifted | 0x1e1bffull); + if ((mask & first) != 0) { + *limit = 0x17f; + return true; + } + *limit = 0x7f; + const std::uint32_t second = variant == 1 ? 0x10000000u : 0x6000000u; + const std::uint64_t flag = static_cast(none || version >= 0x2000000u ? 1u : 0u) << 24; + const std::uint64_t base = (wide ? 0x1401e400ull : 0x1e400ull) | flag; + const std::uint64_t allowed = version < second ? base : (base | 0x200000000ull); + return (mask & allowed) != 0; +} + +constexpr bool HighBitSet(std::uint64_t mask) { + return (mask & 0x80000000ull) != 0; +} + +constexpr bool KeepClassified(std::uint32_t c, std::uint64_t mask, std::uint32_t limit, const bool (&absent)[8]) { + if (c < limit) return true; + if (In(kPunctuationLatin, c)) return true; + if (!absent[7] && (static_cast(c - 0x3130) < 0x60 || static_cast(c + 0x5400) < 0x2bb0)) { + return true; + } + if ((kClassCyrillic & mask) != 0 && (c & 0xff00) == 0x400) return true; + if (!absent[6] && (static_cast(c + 0xf0) <= 9 || In(kPunctuationFullwidth, c))) return true; + if (!absent[5] && (static_cast(c + 0xf0) <= 9 || In(kPunctuationFullwidthNoProlong, c))) return true; + if (!absent[4] && InCodepageA(c)) return true; + if (!absent[5] && InCodepageB(c)) return true; + if (!absent[3] && ((c & 0xff00) == 0x600 || (c & 0xfffc) == 0x200c)) return true; + if ((mask & kClassThai) != 0 && (c & 0xff80) == 0xe00) return true; + if (!absent[2] && (static_cast(c - 0x370) < 0x72 || (c & 0xfff0) == 0x3f0)) return true; + if (!absent[1] && IsVietnameseExtension(c)) return true; + if (HighBitSet(mask) && (c & 0xfffc) == 0x218) return true; + if (!absent[0] && In(kModifierLetters, c)) return true; + return false; +} + +inline std::uint32_t FilterText(char16_t* dst, std::int32_t* out_length, std::uint32_t capacity, + const FilterParam* param, const char16_t* src, std::uint32_t src_length) { + if (dst == nullptr || out_length == nullptr || param == nullptr || src == nullptr) return kErrorInvalidAddress; + if (capacity < 2) return kErrorInvalidParam; + const std::uint32_t type = param->type; + if (type >= 5 && type - 0x100 > 3) return kErrorInvalidParam; + + std::uint64_t mask = param->mask; + if (mask == 0) mask = DerivedMask(param->version, param->variant); + std::uint32_t limit = 0; + if (!ValidMask(mask, param->version, param->variant, &limit)) return kErrorInvalidParam; + + bool absent[8]; + for (int i = 0; i < 8; ++i) absent[i] = (mask & kAbsentMasks[i]) == 0; + + std::uint32_t room = capacity - 1; + std::uint32_t count = 0; + for (std::uint64_t i = 0; i < src_length && room != 0; ++i) { + const std::uint32_t c = src[i]; + if (c == 0) break; + bool keep; + if (type == 4) { + keep = static_cast(c - 0x30) <= 9 || In(kNumberSeparators, c); + } else if (type == 1 || type == 3 || type == 0x102) { + keep = c <= 0x7e; + } else if (type == 0x100) { + keep = c < 0x17f; + if (!keep && (mask & kClassVietnamese) != 0) { + const bool listed = static_cast(c - 0x300) >= 0x24 && (c & 0xff00) != 0x1e00; + keep = !listed || In(kVietnameseLetters, c); + } + if (!keep && HighBitSet(mask)) keep = (c & 0xfffc) == 0x218; + } else { + keep = KeepClassified(c, mask, limit, absent); + } + if (keep) { + dst[count++] = static_cast(c); + --room; + } + } + dst[count] = 0; + *out_length = static_cast(count); + return 0; +} + +} diff --git a/core/libs/prx/libSceIme/include/ImeInput.hpp b/core/libs/prx/libSceIme/include/ImeInput.hpp index 4212dd61ea..3d27316f20 100644 --- a/core/libs/prx/libSceIme/include/ImeInput.hpp +++ b/core/libs/prx/libSceIme/include/ImeInput.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEIME_INCLUDE_IMEINPUT_HPP #define CORE_LIBS_PRX_LIBSCEIME_INCLUDE_IMEINPUT_HPP +#include #include enum class ImeHostInputKind : std::uint32_t { @@ -23,4 +24,8 @@ struct ImeHostInput { extern "C" void ImePublishInput_nid_postfix(const ImeHostInput& input); extern "C" bool ImeCapturesKeyboard_nid_postfix(); +using ImeHostTextSink = void (*)(ImeHostInputKind kind, const char16_t* text, std::size_t length); + +extern "C" void ImeSetHostTextSink_nid_no_patch(ImeHostTextSink sink); + #endif diff --git a/core/libs/prx/libSceImeDialog/CMakeLists.txt b/core/libs/prx/libSceImeDialog/CMakeLists.txt new file mode 100644 index 0000000000..69efa4be3f --- /dev/null +++ b/core/libs/prx/libSceImeDialog/CMakeLists.txt @@ -0,0 +1,17 @@ +add_library(libSceImeDialog SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) +target_include_directories(libSceImeDialog PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceImeDialog PRIVATE libSceIme) +configure_windows_unwind(libSceImeDialog) + +if(BUILD_TESTING) + add_test_executable(guest_ime_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeDialog.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libSceVideoOut/src/ImeTextInput.cpp) + target_include_directories(guest_ime_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(guest_ime_dialog_tests PRIVATE libSceImeDialog libSceIme libc) + configure_windows_unwind(guest_ime_dialog_tests) + add_test(NAME guest_ime_dialog COMMAND guest_ime_dialog_tests) + if(WIN32) + set_tests_properties(guest_ime_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceImeDialog/Export.cpp b/core/libs/prx/libSceImeDialog/Export.cpp index 84fe1c7e23..c19acdd36a 100644 --- a/core/libs/prx/libSceImeDialog/Export.cpp +++ b/core/libs/prx/libSceImeDialog/Export.cpp @@ -1,9 +1,11 @@ -#include +#include #include #include -#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" namespace { @@ -11,21 +13,108 @@ constexpr int StatusNone = 0; constexpr int StatusRunning = 1; constexpr int StatusFinished = 2; constexpr std::uint32_t EndStatusOk = 0; +constexpr std::uint32_t EndStatusUserCanceled = 1; constexpr std::uint32_t EndStatusAborted = 2; +constexpr std::uint32_t MaxTextLength = 2048; +constexpr int ErrorBusy = static_cast(0x80bc0001u); +constexpr int ErrorSuspending = static_cast(0x80bc0009u); constexpr int ErrorNotOpened = static_cast(0x80bc0002u); +constexpr int ErrorNotInUse = static_cast(0x80bc0107u); +constexpr int ErrorInvalidMaxTextLength = static_cast(0x80bc0016u); +constexpr int ErrorInvalidInputTextBuffer = static_cast(0x80bc0017u); +constexpr int ErrorInvalidAddress = static_cast(0x80bc0031u); +constexpr int ErrorInternal = static_cast(0x80bc00ffu); -std::atomic status{StatusNone}; -std::atomic polls{0}; -std::atomic endStatus{EndStatusOk}; +struct Dialog { + int status = StatusNone; + std::uint32_t endStatus = EndStatusOk; + char16_t* buffer = nullptr; + std::uint32_t maxLength = 0; + std::u16string text; + std::size_t caret = 0; +}; + +std::mutex g_mutex; +Dialog g_dialog; + +bool IsHighSurrogate(char16_t unit) { return unit >= 0xd800 && unit <= 0xdbff; } +bool IsLowSurrogate(char16_t unit) { return unit >= 0xdc00 && unit <= 0xdfff; } + +std::size_t UnitsBefore(const Dialog& dialog) { + if (dialog.caret == 0) return 0; + return dialog.caret >= 2 && IsLowSurrogate(dialog.text[dialog.caret - 1]) && IsHighSurrogate(dialog.text[dialog.caret - 2]) ? 2 : 1; +} + +std::size_t UnitsAfter(const Dialog& dialog) { + if (dialog.caret >= dialog.text.size()) return 0; + return dialog.caret + 1 < dialog.text.size() && IsHighSurrogate(dialog.text[dialog.caret]) && IsLowSurrogate(dialog.text[dialog.caret + 1]) ? 2 : 1; +} + +void Finish(std::uint32_t endStatus) { + g_dialog.endStatus = endStatus; + g_dialog.status = StatusFinished; + ImeSetHostTextSink_nid_no_patch(nullptr); +} + +void Insert(Dialog& dialog, const char16_t* units, std::size_t length) { + std::u16string accepted; + for (std::size_t i = 0; i < length; ++i) { + if (units[i] != u'\n' && units[i] != u'\r') accepted.push_back(units[i]); + } + std::size_t count = std::min(accepted.size(), dialog.maxLength - dialog.text.size()); + if (count > 0 && count < accepted.size() && IsHighSurrogate(accepted[count - 1])) --count; + dialog.text.insert(dialog.caret, accepted, 0, count); + dialog.caret += count; +} + +void Receive(ImeHostInputKind kind, const char16_t* units, std::size_t length) { + std::lock_guard lock(g_mutex); + Dialog& dialog = g_dialog; + if (dialog.status != StatusRunning) return; + switch (kind) { + case ImeHostInputKind::Text: + Insert(dialog, units, length); + return; + case ImeHostInputKind::Backspace: { + const std::size_t count = UnitsBefore(dialog); + dialog.caret -= count; + dialog.text.erase(dialog.caret, count); + return; + } + case ImeHostInputKind::Delete: + dialog.text.erase(dialog.caret, UnitsAfter(dialog)); + return; + case ImeHostInputKind::Left: + dialog.caret -= UnitsBefore(dialog); + return; + case ImeHostInputKind::Right: + dialog.caret += UnitsAfter(dialog); + return; + case ImeHostInputKind::Home: + dialog.caret = 0; + return; + case ImeHostInputKind::End: + dialog.caret = dialog.text.size(); + return; + case ImeHostInputKind::Enter: + std::copy(dialog.text.begin(), dialog.text.end(), dialog.buffer); + dialog.buffer[dialog.text.size()] = u'\0'; + Finish(EndStatusOk); + return; + case ImeHostInputKind::Escape: + Finish(EndStatusUserCanceled); + return; + } +} } extern "C" { int APS5_VABI sceImeDialogAbort(void) { - if (status.load() != StatusRunning) return ErrorNotOpened; - endStatus = EndStatusAborted; - status = StatusFinished; + std::lock_guard lock(g_mutex); + if (g_dialog.status != StatusRunning) return ErrorNotOpened; + Finish(EndStatusAborted); return 0; } @@ -50,47 +139,77 @@ int APS5_VABI sceImeDialogGetPanelSizeExtended(const Param* param, const Extende } int APS5_VABI sceImeDialogGetResult(Result* result) { - std::fprintf(stderr, "[ime] GetResult (status %d)\n", status.load()); - if (result == nullptr || status.load() != StatusFinished) return ErrorNotOpened; + std::lock_guard lock(g_mutex); + if (result == nullptr || g_dialog.status != StatusFinished) return ErrorNotOpened; *result = Result{}; - result->endstatus = endStatus.load(); + result->endstatus = g_dialog.endStatus; return 0; } int APS5_VABI sceImeDialogGetStatus(void) { - if (status.load() == StatusRunning && ++polls > 1) status = StatusFinished; - static std::atomic traced{0}; - const int count = traced.fetch_add(1); - if (count < 8 || count % 2000 == 0) std::fprintf(stderr, "[ime] GetStatus #%d -> %d\n", count, status.load()); - return status.load(); + std::lock_guard lock(g_mutex); + return g_dialog.status; } int APS5_VABI sceImeDialogInit(const Param* param, const ExtendedParam* extended) { (void)extended; - static std::atomic reported{0}; - bool filled = false; - if (param != nullptr && param->input_text_buffer != nullptr && param->max_text_length != 0 && param->input_text_buffer[0] == u'\0') { - static constexpr char16_t defaultText[] = u"Slayer"; - std::uint32_t length = 0; - while (defaultText[length] != u'\0' && length < param->max_text_length) { - param->input_text_buffer[length] = defaultText[length]; - ++length; - } - param->input_text_buffer[length] = u'\0'; - filled = true; - } - std::fprintf(stderr, "[ime] Init #%d\n", reported.load()); - if (reported.fetch_add(1) == 0) std::fprintf(stderr, "[ime] dialog requested (max text length %u): finishing at once with %s\n", param != nullptr ? param->max_text_length : 0u, filled ? "a default text" : "the text unchanged"); - polls = 0; - endStatus = EndStatusOk; - status = StatusRunning; + std::lock_guard lock(g_mutex); + if (g_dialog.status != StatusNone) return ErrorBusy; + if (param == nullptr) return ErrorInvalidAddress; + if (param->max_text_length == 0 || param->max_text_length > MaxTextLength) return ErrorInvalidMaxTextLength; + if (param->input_text_buffer == nullptr) return ErrorInvalidInputTextBuffer; + g_dialog = Dialog{}; + g_dialog.buffer = param->input_text_buffer; + g_dialog.maxLength = param->max_text_length; + std::size_t length = 0; + while (length < param->max_text_length && param->input_text_buffer[length] != u'\0') ++length; + g_dialog.text.assign(param->input_text_buffer, length); + g_dialog.caret = length; + g_dialog.status = StatusRunning; + ImeSetHostTextSink_nid_no_patch(&Receive); return 0; } int APS5_VABI sceImeDialogTerm(void) { - std::fprintf(stderr, "[ime] Term (status %d)\n", status.load()); - if (status.load() == StatusNone) return ErrorNotOpened; - status = StatusNone; + std::lock_guard lock(g_mutex); + if (g_dialog.status == StatusNone) return ErrorNotOpened; + ImeSetHostTextSink_nid_no_patch(nullptr); + g_dialog = Dialog{}; + return 0; +} + +int APS5_VABI sceImeDialogForTestFunction() { + return ErrorInternal; +} + +int APS5_VABI sceImeDialogForceClose(void) { + std::lock_guard lock(g_mutex); + if (g_dialog.status == StatusNone) return ErrorNotInUse; + ImeSetHostTextSink_nid_no_patch(nullptr); + g_dialog = Dialog{}; + return 0; +} + +int APS5_VABI sceImeDialogGetCurrentStarState(int64_t state) { + std::lock_guard lock(g_mutex); + if (g_dialog.status == StatusNone) return ErrorNotInUse; + if (state == 0) return ErrorInvalidAddress; + *reinterpret_cast(state) = 0; + return 0; +} + +int APS5_VABI sceImeDialogInitInternal(Param* param, ExtendedParam* extended) { + return sceImeDialogInit(param, extended); +} + +int APS5_VABI sceImeDialogSetPanelPosition(int32_t, int32_t) { + std::lock_guard lock(g_mutex); + return g_dialog.status == StatusNone ? ErrorNotInUse : ErrorSuspending; +} + + +int APS5_VABI sceImeDialogInitInternalWithSubInfo() { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceInvitationDialog/Export.cpp b/core/libs/prx/libSceInvitationDialog/Export.cpp new file mode 100644 index 0000000000..8ae801bc26 --- /dev/null +++ b/core/libs/prx/libSceInvitationDialog/Export.cpp @@ -0,0 +1,31 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceInvitationDialogGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceInvitationDialogInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceInvitationDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceInvitationDialogTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceInvitationDialogUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceIpmi/Export.cpp b/core/libs/prx/libSceIpmi/Export.cpp new file mode 100644 index 0000000000..238bb67ecf --- /dev/null +++ b/core/libs/prx/libSceIpmi/Export.cpp @@ -0,0 +1,34 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN +int APS5_VABI sceIpmiMgrGetIpcBlockerPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceIpmiMgrGetIpcBlockerPidByAppId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceIpmiMgrPauseDump(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceIpmiMgrRaiseExceptionToPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceIpmiMgrStartDump(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceJemspace/Export.cpp b/core/libs/prx/libSceJemspace/Export.cpp new file mode 100644 index 0000000000..5dc318d1a1 --- /dev/null +++ b/core/libs/prx/libSceJemspace/Export.cpp @@ -0,0 +1,58 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceJemspaceCalloc() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceCreate() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceDestroy() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceFree() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceGetStats() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceMalloc() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceMallocUsableSize() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceMemalign() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspacePosixMemalign() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspacePrintStats() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspacePurge() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceRealloc() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceJemspaceReallocalign() { + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libSceJitBridge/Export.cpp b/core/libs/prx/libSceJitBridge/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceJitBridge/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/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..155a95220d 100644 --- a/core/libs/prx/libSceJobManager/Export.cpp +++ b/core/libs/prx/libSceJobManager/Export.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -22,6 +23,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 +69,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 +118,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 +129,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 +169,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; @@ -170,8 +199,12 @@ private: std::uint32_t outstanding = 0; std::size_t cursor = 0; 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 { @@ -188,7 +221,6 @@ public: struct Manager { std::mutex lock; std::condition_variable work; - std::condition_variable nodeFreed; bool running = false; std::uint32_t blockedWaiters = 0; std::uint32_t freeNodes = 0; @@ -196,6 +228,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; @@ -213,7 +250,6 @@ bool TakeNode(Manager& manager) { void ReturnNodes(Manager& manager, std::uint32_t count) { manager.freeNodes += count; - manager.nodeFreed.notify_all(); } int Sequence::Latch(int code) { @@ -231,6 +267,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 +288,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; @@ -257,9 +301,11 @@ void Sequence::Advance() { continue; } bool* const waiter = item.passed; - const auto retired = static_cast(cursor) + 1; - items.erase(items.begin(), items.begin() + static_cast(retired)); + const auto retired = static_cast(cursor); + items.erase(items.begin(), items.begin() + static_cast(retired + 1)); cursor = 0; + commands.clear(); + batchStarted = false; if (error == 0) { submitted.invocations -= completed.invocations; submitted.dispatches -= completed.dispatches; @@ -296,6 +342,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(); @@ -316,12 +437,14 @@ int Sequence::Wait() { int Sequence::WaitAndClose(std::uint32_t index) { std::unique_lock guard(manager->lock); const int result = WaitLocked(guard); - ReturnNodes(*manager, static_cast(items.size()) + 1); + const auto remaining = static_cast(std::count_if(items.begin(), items.end(), [](const Item& item) { return item.type != ItemType::Wait; })); + ReturnNodes(*manager, remaining + 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; } @@ -333,13 +456,7 @@ int Sequence::WaitLocked(std::unique_lock& guard) { } else if (submitted.waits++ >= limits.waits) { result = Latch(SequenceLimitExceeded); } - if (manager->freeNodes == 0 && result == 0) result = Latch(SequenceNoNodes); ++manager->blockedWaiters; - manager->nodeFreed.wait(guard, [this] { return manager->freeNodes != 0; }); - --manager->freeNodes; - guard.unlock(); - const std::lock_guard serial(waiters); - guard.lock(); bool done = false; items.push_back({ItemType::Wait, this, nullptr, nullptr, 0, 0, 0, &done}); Advance(); @@ -353,42 +470,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 +559,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 +605,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 +636,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 +646,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 +667,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; @@ -521,10 +693,16 @@ void APS5_VABI _ZN3sce3Job10JobManagerC2Ev(void* self) { Implementation(self) = nullptr; } -void APS5_VABI _ZN3sce3Job10JobManagerD1Ev(void*) { +int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void* self); + +void APS5_VABI _ZN3sce3Job10JobManagerD1Ev(void* self) { + if (Implementation(self) != nullptr) { + static_cast(_ZN3sce3Job10JobManager8shutdownEv(self)); + } } -void APS5_VABI _ZN3sce3Job10JobManagerD2Ev(void*) { +void APS5_VABI _ZN3sce3Job10JobManagerD2Ev(void* self) { + _ZN3sce3Job10JobManagerD1Ev(self); } std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE(const MemorySizeQueryParams* params) { @@ -539,12 +717,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 +748,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/libSceJpegDec/Export.cpp b/core/libs/prx/libSceJpegDec/Export.cpp index d38764bf71..aeddcbeb6c 100644 --- a/core/libs/prx/libSceJpegDec/Export.cpp +++ b/core/libs/prx/libSceJpegDec/Export.cpp @@ -5,33 +5,44 @@ #include #include #include +#include #include #include "Decoder/Jpeg.hpp" #include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" -static_assert(sizeof(JpegDecCreateParam) == 0x8); +static_assert(sizeof(JpegDecCreateParam) == 0xC); static_assert(sizeof(JpegDecParseParam) == 0x10); -static_assert(sizeof(JpegDecDecodeParam) == 0x20); -static_assert(sizeof(JpegDecImageInfo) == 0x10); +static_assert(sizeof(JpegDecDecodeParam) == 0x30); +static_assert(sizeof(JpegDecImageInfo) == 0x24); namespace { -constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_ADDR = static_cast(0x80690001); -constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_SIZE = static_cast(0x80690002); -constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_PARAM = static_cast(0x80690003); -constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_HANDLE = static_cast(0x80690004); -constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_WORK_MEMORY = static_cast(0x80690005); -constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_DATA = static_cast(0x80690010); -constexpr std::int32_t SCE_JPEG_DEC_ERROR_DECODE_ERROR = static_cast(0x80690012); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_ADDR = static_cast(0x80650001); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_SIZE = static_cast(0x80650002); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_PARAM = static_cast(0x80650003); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_HANDLE = static_cast(0x80650004); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_IMAGE_TOO_WIDE = static_cast(0x80650006); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_DATA = static_cast(0x80650010); +constexpr std::int32_t SCE_JPEG_DEC_ERROR_DECODE_ERROR = static_cast(0x80650012); -constexpr std::uint32_t ATTRIBUTE_NONE = 0; -constexpr std::uint32_t MEMORY_SIZE = 0x20; -constexpr std::uintptr_t HANDLE_ALIGNMENT = 8; +constexpr std::uint32_t ATTRIBUTE_CSC_ORDINARY = 1; +constexpr std::uint32_t ATTRIBUTE_CSC_FULL = 2; +constexpr std::uint32_t MEMORY_OVERHEAD = 29536; +constexpr std::uintptr_t HANDLE_ALIGNMENT = 32; + +constexpr std::uint16_t DECODE_MODE_NORMAL = 0; +constexpr std::uint16_t DOWN_SCALE_NONE = 1; +constexpr std::uint16_t DOWN_SCALE_HALF = 2; +constexpr std::uint16_t DOWN_SCALE_QUARTER = 4; +constexpr std::uint16_t DOWN_SCALE_EIGHTH = 8; constexpr std::uint16_t PIXEL_FORMAT_R8G8B8A8 = 0; constexpr std::uint16_t PIXEL_FORMAT_B8G8R8A8 = 1; -constexpr std::uint16_t COLOR_SPACE_RGB = 3; +constexpr std::uint16_t COLOR_SPACE_UNKNOWN = 0; +constexpr std::uint16_t COLOR_SPACE_YCBCR = 1; +constexpr std::uint16_t COLOR_SPACE_GRAY = 2; constexpr std::uint32_t BYTES_PER_PIXEL = 4; constexpr std::uint32_t MAX_PACKED_DIMENSION = 32767; @@ -39,17 +50,33 @@ constexpr std::uint32_t MAX_PACKED_DIMENSION = 32767; struct Context { Context* self; std::uint32_t attribute; + std::uint32_t maxImageWidth; + std::uint32_t rowBufferCapacity; }; -static_assert(sizeof(Context) + HANDLE_ALIGNMENT - 1 <= MEMORY_SIZE); +static_assert(sizeof(Context) + HANDLE_ALIGNMENT - 1 <= MEMORY_OVERHEAD); std::int32_t validateCreateParam(const JpegDecCreateParam* param) { - if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; - if (param->size != sizeof(JpegDecCreateParam)) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; - if (param->attribute != ATTRIBUTE_NONE) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (!param) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + if (param->this_size != sizeof(JpegDecCreateParam)) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + if (param->attribute != ATTRIBUTE_CSC_ORDINARY && param->attribute != ATTRIBUTE_CSC_FULL) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (param->max_image_width == 0 || param->max_image_width > 65535) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; return 0; } +std::uint32_t workMemorySize(const JpegDecCreateParam& param) { + const std::uint32_t alignedWidth = (param.max_image_width + 31) & ~std::uint32_t{31}; + return MEMORY_OVERHEAD + (alignedWidth + 16) * (param.attribute == ATTRIBUTE_CSC_ORDINARY ? 54 : 166); +} + +bool validDecodeOptions(std::uint16_t decodeMode, std::uint16_t downScale) { + return decodeMode <= 1 && (downScale == DOWN_SCALE_NONE || downScale == DOWN_SCALE_HALF || downScale == DOWN_SCALE_QUARTER || downScale == DOWN_SCALE_EIGHTH); +} + +void requireDecodeMode(const char* function, std::uint16_t decodeMode) { + if (decodeMode != DECODE_MODE_NORMAL) throw std::runtime_error(std::string(function) + ": decode mode " + std::to_string(decodeMode) + " is not implemented"); +} + Context* toContext(void* handle) { const auto address = reinterpret_cast(handle); if (address == 0 || address % HANDLE_ALIGNMENT != 0) return nullptr; @@ -57,12 +84,55 @@ Context* toContext(void* handle) { return context->self == context ? context : nullptr; } -void fillImageInfo(const Decoder::Jpeg::Header& header, JpegDecImageInfo* info) { +void fillImageInfo(const Decoder::Jpeg::Header& header, std::uint32_t downScale, JpegDecImageInfo* info) { + *info = {}; info->image_width = header.width; info->image_height = header.height; - info->color_space = COLOR_SPACE_RGB; - info->bit_depth = 8; - info->image_flag = 0; + info->color_space = COLOR_SPACE_UNKNOWN; + if (header.channels == 1) info->color_space = COLOR_SPACE_GRAY; + if (header.channels == 3 && (header.app0BeforeFrame || (header.componentIds[0] == 1 && header.componentIds[1] == 2 && header.componentIds[2] == 3))) info->color_space = COLOR_SPACE_YCBCR; + info->component_number = static_cast(header.channels); + std::uint32_t maxHorizontal = 1; + std::uint32_t maxVertical = 1; + std::uint32_t blocksPerMcu = 0; + for (std::uint32_t i = 0; i < header.channels; ++i) { + info->component_id[i] = header.componentIds[i]; + info->sampling_factor[i] = header.samplingFactors[i]; + const std::uint32_t horizontal = header.channels == 1 ? 1 : header.samplingFactors[i] >> 4; + const std::uint32_t vertical = header.channels == 1 ? 1 : header.samplingFactors[i] & 15; + if (horizontal > maxHorizontal) maxHorizontal = horizontal; + if (vertical > maxVertical) maxVertical = vertical; + blocksPerMcu += horizontal * vertical; + } + if (info->color_space == COLOR_SPACE_GRAY) info->suitable_csc_attribute = ATTRIBUTE_CSC_ORDINARY; + if (info->color_space == COLOR_SPACE_YCBCR) info->suitable_csc_attribute = (12 / maxHorizontal) * blocksPerMcu < 37 ? ATTRIBUTE_CSC_ORDINARY : ATTRIBUTE_CSC_FULL; + if (header.progressive) { + const std::uint32_t mcuColumns = (header.width + 8 * maxHorizontal - 1) / (8 * maxHorizontal); + const std::uint32_t mcuRows = (header.height + 8 * maxVertical - 1) / (8 * maxVertical); + info->coefficient_mem_size = mcuColumns * mcuRows * blocksPerMcu * 128 + 64; + } + info->output_image_width = Decoder::Jpeg::ScaledDimension(header.width, downScale); + info->output_image_height = Decoder::Jpeg::ScaledDimension(header.height, downScale); +} + +std::uint32_t rowBufferSize(const Decoder::Jpeg::Header& header, std::uint32_t downScale) { + std::uint32_t maxHorizontal = 1; + if (header.channels != 1) { + for (std::uint32_t i = 0; i < header.channels; ++i) { + const std::uint32_t horizontal = header.samplingFactors[i] >> 4; + if (horizontal > maxHorizontal) maxHorizontal = horizontal; + } + } + const std::uint32_t mcuColumns = (header.width + 8 * maxHorizontal - 1) / (8 * maxHorizontal); + const std::uint32_t blockSize = 8 / downScale; + std::uint32_t size = 0; + for (std::uint32_t i = 0; i < header.channels; ++i) { + const std::uint32_t horizontal = header.channels == 1 ? 1 : header.samplingFactors[i] >> 4; + const std::uint32_t vertical = header.channels == 1 ? 1 : header.samplingFactors[i] & 15; + const std::uint32_t stride = (mcuColumns * blockSize * horizontal + 7) & ~std::uint32_t{7}; + size += stride * vertical * blockSize; + } + return (size + 31) & ~std::uint32_t{31}; } std::span toBytes(const void* data, std::uint32_t size) { @@ -73,42 +143,61 @@ std::span toBytes(const void* data, std::uint32_t size) { extern "C" { -int32_t APS5_VABI sceJpegDecCreate(const JpegDecCreateParam* param, void* memory_address, uint32_t memory_size, void** handle) { +int32_t APS5_VABI sceJpegDecCreate(const JpegDecCreateParam* param, void* memoryAddress, uint32_t memorySize, void** handle) { const std::int32_t result = validateCreateParam(param); if (result != 0) return result; - if (!memory_address || !handle) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; - if (memory_size < MEMORY_SIZE) return SCE_JPEG_DEC_ERROR_INVALID_WORK_MEMORY; - const auto address = reinterpret_cast(memory_address); + if (!memoryAddress) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + if (memorySize < workMemorySize(*param)) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + if (!handle) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + const auto address = reinterpret_cast(memoryAddress); const std::uintptr_t aligned = (address + HANDLE_ALIGNMENT - 1) & ~(HANDLE_ALIGNMENT - 1); auto* context = new (reinterpret_cast(aligned)) Context{}; context->self = context; context->attribute = param->attribute; + context->maxImageWidth = (param->max_image_width + 31) & ~std::uint32_t{31}; + context->rowBufferCapacity = (context->maxImageWidth + 16) * (param->attribute == ATTRIBUTE_CSC_ORDINARY ? 24 : 80); *handle = context; return 0; } -int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param, JpegDecImageInfo* image_info) { +int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param, JpegDecImageInfo* imageInfo) { const Context* context = toContext(handle); if (!context) return SCE_JPEG_DEC_ERROR_INVALID_HANDLE; - (void)context; - if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (!param) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (!param->jpeg_mem_addr || !param->image_mem_addr) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + if ((reinterpret_cast(param->image_mem_addr) & 3) != 0 && (param->pixel_format == 0 || param->pixel_format == 1 || param->pixel_format == 9 || param->pixel_format == 10)) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->jpeg_mem_size == 0 || param->image_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; - if (param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (!validDecodeOptions(param->decode_mode, param->down_scale)) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (param->pixel_format != 0 && param->pixel_format != 1 && param->pixel_format != 9 && param->pixel_format != 10 && param->pixel_format != 11) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (param->alpha_value > 255) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + requireDecodeMode(__func__, param->decode_mode); + if (param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) throw std::runtime_error("sceJpegDecDecode: pixel format is not implemented"); + const std::uint32_t downScale = param->down_scale; const std::span jpeg = toBytes(param->jpeg_mem_addr, param->jpeg_mem_size); const std::optional header = Decoder::Jpeg::ParseHeader(jpeg); if (!header) return SCE_JPEG_DEC_ERROR_INVALID_DATA; + JpegDecImageInfo parsedInfo{}; + fillImageInfo(*header, downScale, &parsedInfo); + if (parsedInfo.coefficient_mem_size != 0 && (!param->coefficient_mem_addr || param->coefficient_mem_size < parsedInfo.coefficient_mem_size)) return static_cast(0x80650005); + if (parsedInfo.color_space == COLOR_SPACE_UNKNOWN) return static_cast(0x80650011); + const std::uint32_t outputWidth = Decoder::Jpeg::ScaledDimension(header->width, downScale); + const std::uint32_t outputHeight = Decoder::Jpeg::ScaledDimension(header->height, downScale); - const std::uint64_t rowSize = static_cast(header->width) * BYTES_PER_PIXEL; + const std::uint64_t rowSize = static_cast(outputWidth) * BYTES_PER_PIXEL; const std::uint64_t pitch = param->image_pitch == 0 ? rowSize : param->image_pitch; - if (pitch < rowSize) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; - if ((header->height - 1) * pitch + rowSize > param->image_mem_size) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + if (param->image_pitch != 0) { + if (pitch < rowSize || pitch % 4 != 0 || pitch * outputHeight >= 0x80000000 || pitch * outputHeight > param->image_mem_size) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + } else if (pitch * outputHeight > param->image_mem_size) { + return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + } + if (((outputWidth + 31) & ~std::uint32_t{31}) > context->maxImageWidth || rowBufferSize(*header, downScale) > context->rowBufferCapacity) return SCE_JPEG_DEC_ERROR_IMAGE_TOO_WIDE; - const std::optional image = Decoder::Jpeg::Decode(jpeg); - if (!image || image->width != header->width || image->height != header->height) return SCE_JPEG_DEC_ERROR_DECODE_ERROR; + const std::optional image = downScale == DOWN_SCALE_NONE ? Decoder::Jpeg::Decode(jpeg) : Decoder::Jpeg::DecodeScaled(jpeg, downScale); + if (!image || image->width != outputWidth || image->height != outputHeight) return SCE_JPEG_DEC_ERROR_DECODE_ERROR; const bool swapRedBlue = param->pixel_format == PIXEL_FORMAT_B8G8R8A8; + const auto alpha = static_cast(param->alpha_value); auto* output = static_cast(param->image_mem_addr); for (std::uint32_t y = 0; y < image->height; ++y) { std::uint8_t* out = output + y * pitch; @@ -125,13 +214,13 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param pixel[2] = in[2]; } if (swapRedBlue) std::swap(pixel[0], pixel[2]); - pixel[3] = 0xFF; + pixel[3] = alpha; } } - if (image_info) fillImageInfo(*header, image_info); - if (header->width > MAX_PACKED_DIMENSION || header->height > MAX_PACKED_DIMENSION) return 0; - return static_cast(header->width << 16 | header->height); + if (imageInfo) fillImageInfo(*header, downScale, imageInfo); + if (outputWidth > MAX_PACKED_DIMENSION || outputHeight > MAX_PACKED_DIMENSION) return 0; + return static_cast(outputWidth << 16 | outputHeight); } int32_t APS5_VABI sceJpegDecDelete(void* handle) { @@ -141,20 +230,29 @@ int32_t APS5_VABI sceJpegDecDelete(void* handle) { return 0; } -int32_t APS5_VABI sceJpegDecParseHeader(const JpegDecParseParam* param, JpegDecImageInfo* image_info) { - if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; - if (!param->jpeg_mem_addr || !image_info) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; +int32_t APS5_VABI sceJpegDecParseHeader(const JpegDecParseParam* param, JpegDecImageInfo* imageInfo) { + if (!param) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + if (!param->jpeg_mem_addr) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->jpeg_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + if (!validDecodeOptions(param->decode_mode, param->down_scale)) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (!imageInfo) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + requireDecodeMode(__func__, param->decode_mode); + const std::uint32_t downScale = param->down_scale; const std::optional header = Decoder::Jpeg::ParseHeader(toBytes(param->jpeg_mem_addr, param->jpeg_mem_size)); if (!header) return SCE_JPEG_DEC_ERROR_INVALID_DATA; - fillImageInfo(*header, image_info); + fillImageInfo(*header, downScale, imageInfo); return 0; } int32_t APS5_VABI sceJpegDecQueryMemorySize(const JpegDecCreateParam* param) { const std::int32_t result = validateCreateParam(param); if (result != 0) return result; - return static_cast(MEMORY_SIZE); + return static_cast(workMemorySize(*param)); +} + +int APS5_VABI sceJpegDecDecodeWithInputControl() { + NotImplemented_nid_no_patch(__func__); + return 0; } } diff --git a/core/libs/prx/libSceJpegEnc/Export.cpp b/core/libs/prx/libSceJpegEnc/Export.cpp index 6de513f53d..4ebcc09712 100644 --- a/core/libs/prx/libSceJpegEnc/Export.cpp +++ b/core/libs/prx/libSceJpegEnc/Export.cpp @@ -82,7 +82,7 @@ std::int32_t validateEncodeParam(const JpegEncEncodeParam* param) { if (param->sampling_type != SAMPLING_TYPE_FULL && param->sampling_type != SAMPLING_TYPE_422 && param->sampling_type != SAMPLING_TYPE_420) { return SCE_JPEG_ENC_ERROR_INVALID_PARAM; } - if (param->restart_interval > static_cast(MAX_IMAGE_DIMENSION)) return SCE_JPEG_ENC_ERROR_INVALID_PARAM; + if (param->restart_interval < RESTART_INTERVAL_PER_ROW || param->restart_interval > static_cast(MAX_IMAGE_DIMENSION)) return SCE_JPEG_ENC_ERROR_INVALID_PARAM; switch (param->pixel_format) { case PIXEL_FORMAT_R8G8B8A8: @@ -186,25 +186,23 @@ int32_t APS5_VABI sceJpegEncDelete(void* handle) { return 0; } -int32_t APS5_VABI sceJpegEncEncode(void* handle, const JpegEncEncodeParam* param, JpegEncOutputInfo* output_info) { +int32_t APS5_VABI sceJpegEncEncode(void* handle, const JpegEncEncodeParam* param, JpegEncOutputInfo* outputInfo) { if (!toEncoder(handle)) return SCE_JPEG_ENC_ERROR_INVALID_HANDLE; const std::int32_t result = validateEncodeParam(param); if (result != 0) return result; if (param->encode_mode == ENCODE_MODE_MJPEG) throw std::runtime_error("sceJpegEncEncode: MJPEG encode mode is not implemented"); - if (param->restart_interval < RESTART_INTERVAL_PER_ROW) throw std::runtime_error("sceJpegEncEncode: unknown negative restart interval"); const std::uint32_t channels = param->pixel_format == PIXEL_FORMAT_Y8 ? 1 : 3; const std::vector pixels = toPackedPixels(*param, channels); const std::uint32_t restartBlocks = param->restart_interval > 0 ? static_cast(param->restart_interval) : 0; const std::uint32_t restartRows = param->restart_interval == RESTART_INTERVAL_PER_ROW ? 1 : 0; - const std::vector jpeg = Decoder::Jpeg::Encode(pixels, param->image_width, param->image_height, channels, toQuality(*param), toSampling(*param), - restartBlocks, restartRows); + const std::vector jpeg = Decoder::Jpeg::Encode(pixels, param->image_width, param->image_height, channels, toQuality(*param), toSampling(*param), restartBlocks, restartRows); if (jpeg.size() > param->jpeg_size) return SCE_JPEG_ENC_ERROR_INVALID_SIZE; std::memcpy(param->jpeg, jpeg.data(), jpeg.size()); - if (output_info) { - output_info->size = static_cast(jpeg.size()); - output_info->height = param->image_height; + if (outputInfo) { + outputInfo->size = static_cast(jpeg.size()); + outputInfo->height = param->image_height; } return 0; } diff --git a/core/libs/prx/libSceJpegParser/Export.cpp b/core/libs/prx/libSceJpegParser/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceJpegParser/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceJsc/Export.cpp b/core/libs/prx/libSceJsc/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceJsc/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceJscCompiler/Export.cpp b/core/libs/prx/libSceJscCompiler/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceJscCompiler/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceJson/Export.cpp b/core/libs/prx/libSceJson/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceJson/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceJson2/CMakeLists.txt b/core/libs/prx/libSceJson2/CMakeLists.txt index dff923fb2f..dbc704366f 100644 --- a/core/libs/prx/libSceJson2/CMakeLists.txt +++ b/core/libs/prx/libSceJson2/CMakeLists.txt @@ -8,8 +8,34 @@ if(BUILD_TESTING) target_link_libraries(guest_json_tests PRIVATE libSceJson2 libc) configure_windows_unwind(guest_json_tests) add_test(NAME guest_json COMMAND guest_json_tests) + add_test(NAME guest_json_numeric_locale COMMAND guest_json_tests --numeric-locale) + set_tests_properties(guest_json_numeric_locale PROPERTIES SKIP_RETURN_CODE 77) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(numeric_locale_dir ${CMAKE_CURRENT_BINARY_DIR}/numeric-locale) + set(numeric_locale_data ${LIBS_INCLUDE_DIR}/tests/numeric-locale) + add_test(NAME guest_json_numeric_locale_setup COMMAND sh -c + "rm -rf '${numeric_locale_dir}' && mkdir -p '${numeric_locale_dir}' && I18NPATH='${numeric_locale_dir}' localedef -c -f '${numeric_locale_data}/comma.charmap' -i '${numeric_locale_data}/comma.locale' '${numeric_locale_dir}/xx_COMMA'; test -f '${numeric_locale_dir}/xx_COMMA/LC_NUMERIC'") + set_tests_properties(guest_json_numeric_locale_setup PROPERTIES FIXTURES_SETUP json_numeric_locale) + set_tests_properties(guest_json_numeric_locale PROPERTIES FIXTURES_REQUIRED json_numeric_locale + ENVIRONMENT "LOCPATH=${numeric_locale_dir}") + endif() if(WIN32) - set_tests_properties(guest_json PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_json guest_json_numeric_locale PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_json_copy_failure_tests ${LIBS_INCLUDE_DIR}/tests/JsonCopyFailure.cpp Export.cpp) + target_include_directories(guest_json_copy_failure_tests PRIVATE ${LIBS_INCLUDE_DIR}) + if(WIN32) + target_link_libraries(guest_json_copy_failure_tests PRIVATE libc) + configure_windows_unwind(guest_json_copy_failure_tests) + endif() + add_test(NAME guest_json_copy_failure COMMAND guest_json_copy_failure_tests) + set_tests_properties(guest_json_copy_failure PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_json_copy_failure PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index ea9bc796d5..a84b073cd9 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -1,12 +1,17 @@ #include +#include #include +#include #include #include #include #include #include #include +#include #include +#include +#include #include #include #include "SceTypes.hpp" @@ -25,7 +30,7 @@ enum ValueType : std::int32_t { TypeObject = 7, }; -constexpr int JsonErrorParse = static_cast(0x80920101); +constexpr int JsonErrorParse = static_cast(0x80848101); struct Node; struct Value { Node* node; }; @@ -33,8 +38,16 @@ struct String { std::string* text; }; struct Array { std::list* items; }; struct Pair; struct Object { std::list* items; }; -struct ArrayIterator { std::list::iterator it; }; -struct ObjectIterator { std::list::iterator it; }; +struct ArrayIteratorState { + std::list::iterator position; + std::list::iterator end; +}; +struct ArrayIterator { ArrayIteratorState* state; }; +struct ObjectIteratorState { + std::list* owner = nullptr; + std::list::iterator position{}; +}; +struct ObjectIterator { ObjectIteratorState* state; }; struct Pair { String key; std::uint64_t reserved; @@ -44,6 +57,19 @@ static_assert(sizeof(Value) == 8 && sizeof(String) == 8 && sizeof(Array) == 8 && static_assert(sizeof(ArrayIterator) == 8 && sizeof(ObjectIterator) == 8); static_assert(offsetof(Pair, value) == 0x10); +using TypeMismatchHandler = const Value& (APS5_VABI*)(ValueType, const Value*, const Value*, void*); +using SpecialFloatHandler = Value* (APS5_VABI*)(Value*, std::int32_t, double, const Value*, void*, bool*); +using ElementAccessFailureHandler = void (APS5_VABI*)(int, const String*, const std::size_t*, const Value*, const Value*, void*); + +struct Handlers { + TypeMismatchHandler typeMismatch = nullptr; + void* typeMismatchContext = nullptr; + SpecialFloatHandler specialFloat = nullptr; + void* specialFloatContext = nullptr; + ElementAccessFailureHandler elementAccessFailure = nullptr; + void* elementAccessFailureContext = nullptr; +}; + struct Node { ValueType type = TypeNull; bool boolean = false; @@ -53,6 +79,7 @@ struct Node { String string{}; Array array{}; Object object{}; + Handlers handlers{}; }; using NullAccessCallback = const Value& (APS5_VABI*)(ValueType, const Value*, void*); @@ -95,7 +122,9 @@ void Clear(Node& n) { if (n.type == TypeString) Destroy(n.string); if (n.type == TypeArray) Destroy(n.array); if (n.type == TypeObject) Destroy(n.object); + const Handlers handlers = n.handlers; n = Node{}; + n.handlers = handlers; } void Destroy(Value& v) { @@ -112,23 +141,47 @@ Node& NodeOf(const Value& v) { void Construct(Value& v) { v.node = new Node(); } -void CopyArray(Array& destination, const Array& source) { - Construct(destination); - for (const auto& item : *source.items) { - Value copy{}; - CopyInto(copy, item); - destination.items->push_back(copy); +template +void Construct(Value& destination, TInitialize initialize) { + Value pending{}; + Construct(pending); + try { + initialize(*pending.node); + } catch (...) { + Destroy(pending); + throw; } + destination = pending; +} + +void CopyArray(Array& destination, const Array& source) { + Array pending{}; + Construct(pending); + try { + for (const auto& item : *source.items) { + CopyInto(pending.items->emplace_back(), item); + } + } catch (...) { + Destroy(pending); + throw; + } + destination = pending; } void CopyObject(Object& destination, const Object& source) { - Construct(destination); - for (const auto& pair : *source.items) { - Pair copy{}; - Construct(copy.key, *pair.key.text); - CopyInto(copy.value, pair.value); - destination.items->push_back(copy); + Object pending{}; + Construct(pending); + try { + for (const auto& pair : *source.items) { + auto& copy = pending.items->emplace_back(); + Construct(copy.key, *pair.key.text); + CopyInto(copy.value, pair.value); + } + } catch (...) { + Destroy(pending); + throw; } + destination = pending; } void AssignNode(Node& destination, const Node& source) { @@ -138,25 +191,33 @@ void AssignNode(Node& destination, const Node& source) { copy.integer = source.integer; copy.uinteger = source.uinteger; copy.real = source.real; - if (source.type == TypeString) Construct(copy.string, *source.string.text); - if (source.type == TypeArray) CopyArray(copy.array, source.array); - if (source.type == TypeObject) CopyObject(copy.object, source.object); + try { + if (source.type == TypeString) Construct(copy.string, *source.string.text); + if (source.type == TypeArray) CopyArray(copy.array, source.array); + if (source.type == TypeObject) CopyObject(copy.object, source.object); + } catch (...) { + Clear(copy); + throw; + } Clear(destination); + copy.handlers = destination.handlers; destination = copy; } void CopyInto(Value& destination, const Value& source) { - Construct(destination); - AssignNode(*destination.node, NodeOf(source)); + Construct(destination, [&](Node& node) { AssignNode(node, NodeOf(source)); }); } void SetType(Node& n, ValueType type) { - Clear(n); - n.type = type; - if (type == TypeString) Construct(n.string, std::string()); - if (type == TypeArray) Construct(n.array); - if (type == TypeObject) Construct(n.object); if (type < TypeNull || type > TypeObject) throw std::invalid_argument("sce::Json::Value::set: invalid value type"); + Node pending{}; + pending.type = type; + if (type == TypeString) Construct(pending.string, std::string()); + if (type == TypeArray) Construct(pending.array); + if (type == TypeObject) Construct(pending.object); + Clear(n); + pending.handlers = n.handlers; + n = pending; } const Value& Defaults(ValueType type) { @@ -187,12 +248,35 @@ const Value& NullAccess(ValueType requested, const Value* parent) { return Defaults(requested); } +const Value& TypeMismatch(ValueType requested, const Value* self) { + const Handlers& handlers = NodeOf(*self).handlers; + const Value& result = handlers.typeMismatch(requested, self, nullptr, handlers.typeMismatchContext); + if (result.node && result.node->type == requested) return result; + return Defaults(requested); +} + const Node& Typed(const Value* self, ValueType requested) { const Node& n = NodeOf(*self); if (n.type == requested) return n; + if (n.type != TypeNull && n.handlers.typeMismatch) return NodeOf(TypeMismatch(requested, self)); return NodeOf(NullAccess(requested, self)); } +const Value& MissingElement(const Value* self) { + if (NodeOf(*self).handlers.elementAccessFailure) + throw std::runtime_error("sce::Json::Value: element access failure handler: the error code the console passes is unverified"); + return NullAccess(TypeNull, self); +} + +template +void SetHandlers(Node& n, const TSetter& setter) { + setter(n.handlers); + if (n.type == TypeArray) + for (auto& item : *n.array.items) SetHandlers(NodeOf(item), setter); + if (n.type == TypeObject) + for (auto& pair : *n.object.items) SetHandlers(NodeOf(pair.value), setter); +} + void AppendEscaped(std::string& out, const std::string& text) { out += '"'; for (const unsigned char c : text) { @@ -218,8 +302,9 @@ void AppendEscaped(std::string& out, const std::string& text) { std::string RealText(double value) { if (!std::isfinite(value)) throw std::runtime_error("sce::Json: cannot serialize a non-finite number"); char buffer[32]; - std::snprintf(buffer, sizeof(buffer), "%.17g", value); - return buffer; + const auto result = std::to_chars(buffer, buffer + sizeof(buffer), value, std::chars_format::general, 17); + if (result.ec != std::errc{}) throw std::runtime_error("sce::Json: real number formatting failed"); + return std::string(buffer, result.ptr); } void Serialize(std::string& out, const Node& n) { @@ -228,7 +313,11 @@ void Serialize(std::string& out, const Node& n) { case TypeBoolean: out += n.boolean ? "true" : "false"; return; case TypeInteger: out += std::to_string(n.integer); return; case TypeUInteger: out += std::to_string(n.uinteger); return; - case TypeReal: out += RealText(n.real); return; + case TypeReal: + if (!std::isfinite(n.real) && n.handlers.specialFloat) + throw std::runtime_error("sce::Json::Value: special float handler: the FunctionType the console passes is unverified"); + out += RealText(n.real); + return; case TypeString: AppendEscaped(out, *n.string.text); return; case TypeArray: { out += '['; @@ -258,29 +347,84 @@ void Serialize(std::string& out, const Node& n) { throw std::runtime_error("sce::Json: invalid value type"); } +int CompareText(const char* left, const char* right) { + while (*left != '\0' && *left == *right) { + ++left; + ++right; + } + return static_cast(*left) - static_cast(*right); +} + +std::list::iterator ObjectPosition(const Object& object, const std::string& key) { + return std::find_if(object.items->begin(), object.items->end(), [&](const Pair& pair) { return std::strcmp(pair.key.text->c_str(), key.c_str()) >= 0; }); +} + +Value& ObjectEntry(Object& object, const std::string& key) { + auto position = ObjectPosition(object, key); + if (position != object.items->end() && std::strcmp(position->key.text->c_str(), key.c_str()) == 0) return position->value; + Pair pair{}; + Construct(pair.key, key); + try { + Construct(pair.value); + return object.items->insert(position, pair)->value; + } catch (...) { + Destroy(pair.value); + Destroy(pair.key); + throw; + } +} + +using ParserReadCallback = int (APS5_VABI*)(char&, void*); + class Parser { public: - Parser(const char* text, std::size_t size) : _text(text), _end(text + size) {} + Parser(const char* text, std::size_t size, const Handlers& handlers) : text(text), end(text + size), handlers(handlers) {} + Parser(ParserReadCallback read, void* context, const Handlers& handlers) : read(read), context(context), handlers(handlers) {} bool Parse(Node& out) { skip(); - if (!value(out)) return false; - skip(); - return _text == _end || *_text == '\0'; + return value(out); } + int Error() const { return readError < 0 ? readError : JsonErrorParse; } + private: static constexpr std::size_t MaxNestingDepth = 512; - const char* _text; - const char* _end; - std::size_t _depth = 0; + const char* text = nullptr; + const char* end = nullptr; + ParserReadCallback read = nullptr; + void* context = nullptr; + Handlers handlers; + std::size_t depth = 0; + int current = -2; + int readError = 0; - void skip() { while (_text < _end && (*_text == ' ' || *_text == '\t' || *_text == '\n' || *_text == '\r')) ++_text; } + int peek() { + if (current != -2) return current; + if (read != nullptr) { + char character = 0; + readError = read(character, context); + current = readError < 0 ? -1 : static_cast(character); + } else if (text == end) { + readError = JsonErrorParse; + current = -1; + } else { + current = static_cast(*text++); + } + return current; + } + int take() { + const int character = peek(); + if (character >= 0) current = -2; + return character; + } + void skip() { while (peek() == ' ' || peek() == '\t' || peek() == '\n' || peek() == '\r') take(); } bool literal(const char* word) { - const auto length = std::strlen(word); - if (static_cast(_end - _text) < length || std::memcmp(_text, word, length) != 0) return false; - _text += length; + for (; *word != '\0'; ++word) { + if (peek() != static_cast(*word)) return false; + take(); + } return true; } static void appendUtf8(std::string& out, std::uint32_t code) { @@ -290,10 +434,9 @@ private: 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)); } } bool hex4(std::uint32_t& code) { - if (_end - _text < 4) return false; code = 0; for (int i = 0; i < 4; ++i) { - const char c = *_text++; + const int c = take(); code <<= 4; if (c >= '0' && c <= '9') code |= static_cast(c - '0'); else if (c >= 'a' && c <= 'f') code |= static_cast(c - 'a' + 10); @@ -303,14 +446,14 @@ private: return true; } bool string(std::string& out) { - if (_text >= _end || *_text != '"') return false; - ++_text; - while (_text < _end && *_text != '"') { - const char c = *_text++; + if (peek() < 0 || peek() != '"') return false; + take(); + while (peek() >= 0 && peek() != '"') { + const int c = take(); if (static_cast(c) < 0x20) return false; if (c != '\\') { out += c; continue; } - if (_text >= _end) return false; - switch (*_text++) { + if (peek() < 0) return false; + switch (take()) { case '"': out += '"'; break; case '\\': out += '\\'; break; case '/': out += '/'; break; @@ -333,31 +476,30 @@ private: default: return false; } } - if (_text >= _end) return false; - ++_text; + if (peek() < 0) return false; + take(); return true; } bool number(Node& out) { - const char* start = _text; - if (_text < _end && *_text == '-') ++_text; - if (_text >= _end || *_text < '0' || *_text > '9') return false; - if (*_text == '0') ++_text; - else while (_text < _end && *_text >= '0' && *_text <= '9') ++_text; + std::string token; + if (peek() >= 0 && peek() == '-') token.push_back(static_cast(take())); + if (peek() < 0 || peek() < '0' || peek() > '9') return false; + while (peek() >= '0' && peek() <= '9') token.push_back(static_cast(take())); bool integral = true; - if (_text < _end && *_text == '.') { + if (peek() >= 0 && peek() == '.') { integral = false; - ++_text; - if (_text >= _end || *_text < '0' || *_text > '9') return false; - while (_text < _end && *_text >= '0' && *_text <= '9') ++_text; + token.push_back(static_cast(take())); + if (peek() < 0 || peek() < '0' || peek() > '9') return false; + while (peek() >= 0 && peek() >= '0' && peek() <= '9') token.push_back(static_cast(take())); } - if (_text < _end && (*_text == 'e' || *_text == 'E')) { + if (peek() >= 0 && (peek() == 'e' || peek() == 'E')) { integral = false; - ++_text; - if (_text < _end && (*_text == '+' || *_text == '-')) ++_text; - if (_text >= _end || *_text < '0' || *_text > '9') return false; - while (_text < _end && *_text >= '0' && *_text <= '9') ++_text; + token.push_back(static_cast(take())); + if (peek() >= 0 && (peek() == '+' || peek() == '-')) token.push_back(static_cast(take())); + if (peek() < 0 || peek() < '0' || peek() > '9') return false; + while (peek() >= 0 && peek() >= '0' && peek() <= '9') token.push_back(static_cast(take())); } - const std::string text(start, _text); + const std::string& text = token; if (integral) { errno = 0; if (text[0] == '-') { @@ -366,21 +508,29 @@ private: } else { const unsigned long long value = std::strtoull(text.c_str(), nullptr, 10); if (errno == 0) { - if (value <= static_cast(INT64_MAX)) { out.type = TypeInteger; out.integer = static_cast(value); } - else { out.type = TypeUInteger; out.uinteger = value; } + out.type = TypeUInteger; + out.uinteger = value; return true; } } } - const double real = std::strtod(text.c_str(), nullptr); - if (!std::isfinite(real)) throw std::runtime_error("sce::Json::Parser::parse: number " + text + " overflows a double; the console's behaviour is unverified"); + double real = 0; + const auto parsed = std::from_chars(text.data(), text.data() + text.size(), real); + bool converted = parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size(); + if (parsed.ec == std::errc::result_out_of_range) { + std::istringstream stream(text); + stream.imbue(std::locale::classic()); + stream >> real; + converted = !stream.fail() && stream.eof(); + } + if (!converted || !std::isfinite(real)) throw std::runtime_error("sce::Json::Parser::parse: number " + text + " overflows a double; the console's behaviour is unverified"); out.type = TypeReal; out.real = real; return true; } bool value(Node& out) { - if (_text >= _end) return false; - switch (*_text) { + if (peek() < 0) return false; + switch (peek()) { case 'n': if (!literal("null")) return false; SetType(out, TypeNull); return true; case 't': if (!literal("true")) return false; SetType(out, TypeBoolean); out.boolean = true; return true; case 'f': if (!literal("false")) return false; SetType(out, TypeBoolean); return true; @@ -392,47 +542,47 @@ private: return true; } case '[': { - if (_depth == MaxNestingDepth) return false; - ++_text; - ++_depth; + if (depth == MaxNestingDepth) return false; + take(); + ++depth; SetType(out, TypeArray); skip(); - if (_text < _end && *_text == ']') { ++_text; --_depth; return true; } + if (peek() >= 0 && peek() == ']') { take(); --depth; return true; } for (;;) { Value item{}; Construct(item); + item.node->handlers = handlers; out.array.items->push_back(item); skip(); if (!value(*item.node)) return false; skip(); - if (_text < _end && *_text == ',') { ++_text; continue; } - if (_text < _end && *_text == ']') { ++_text; --_depth; return true; } + if (peek() >= 0 && peek() == ',') { take(); continue; } + if (peek() >= 0 && peek() == ']') { take(); --depth; return true; } return false; } } case '{': { - if (_depth == MaxNestingDepth) return false; - ++_text; - ++_depth; + if (depth == MaxNestingDepth) return false; + take(); + ++depth; SetType(out, TypeObject); skip(); - if (_text < _end && *_text == '}') { ++_text; --_depth; return true; } + if (peek() >= 0 && peek() == '}') { take(); --depth; return true; } for (;;) { skip(); std::string key; if (!string(key)) return false; skip(); - if (_text >= _end || *_text != ':') return false; - ++_text; + if (peek() < 0 || peek() != ':') return false; + take(); skip(); - Pair pair{}; - Construct(pair.key, key); - Construct(pair.value); - out.object.items->push_back(pair); - if (!value(*pair.value.node)) return false; + Value& entry = ObjectEntry(out.object, key); + Clear(*entry.node); + entry.node->handlers = handlers; + if (!value(*entry.node)) return false; skip(); - if (_text < _end && *_text == ',') { ++_text; continue; } - if (_text < _end && *_text == '}') { ++_text; --_depth; return true; } + if (peek() >= 0 && peek() == ',') { take(); continue; } + if (peek() >= 0 && peek() == '}') { take(); --depth; return true; } return false; } } @@ -442,6 +592,25 @@ private: } }; +int ParseValue(Value* out, Parser& parser) { + Node parsed{}; + parsed.handlers = NodeOf(*out).handlers; + bool parsedOk = false; + try { + parsedOk = parser.Parse(parsed); + } catch (...) { + Clear(parsed); + throw; + } + if (!parsedOk) { + Clear(parsed); + return parser.Error(); + } + Clear(NodeOf(*out)); + *out->node = parsed; + return 0; +} + [[noreturn]] void APS5_VABI JsonMemAllocatorPureVirtual() { throw std::runtime_error("sce::Json::MemAllocator: pure virtual function called"); } @@ -453,15 +622,6 @@ void* const JsonMemAllocatorVtable[] = { reinterpret_cast(&JsonMemAllocatorPureVirtual), reinterpret_cast(&JsonMemAllocatorPureVirtual), }; -Value& ObjectEntry(Object& object, const std::string& key) { - for (auto& pair : *object.items) - if (*pair.key.text == key) return pair.value; - Pair pair{}; - Construct(pair.key, key); - Construct(pair.value); - object.items->push_back(pair); - return object.items->back().value; -} } @@ -513,6 +673,8 @@ void APS5_VABI _ZN3sce4Json12MemAllocatorD2Ev(void* self) { *static_cast(self) = &JsonMemAllocatorVtable[2]; } +std::size_t _ZN3sce4Json6String4nposE = static_cast(-1); + void APS5_VABI _ZN3sce4Json6StringC1Ev(String* self) { Construct(*self, std::string()); } void APS5_VABI _ZN3sce4Json6StringC1EPKc(String* self, const char* text) { if (text == nullptr) throw std::invalid_argument("sce::Json::String: null text"); @@ -527,10 +689,49 @@ String* APS5_VABI _ZN3sce4Json6StringaSERKS1_(String* self, const String* other) const char* APS5_VABI _ZNK3sce4Json6String5c_strEv(const String* self) { return self->text->c_str(); } bool APS5_VABI _ZNK3sce4Json6String5emptyEv(const String* self) { return self->text->empty(); } std::size_t APS5_VABI _ZNK3sce4Json6String6lengthEv(const String* self) { return self->text->size(); } -bool APS5_VABI _ZNK3sce4Json6StringeqEPKc(const String* self, const char* text) { return text != nullptr && *self->text == text; } +std::size_t APS5_VABI _ZNK3sce4Json6String4sizeEv(const String* self) { return self->text->size(); } +std::size_t APS5_VABI _ZNK3sce4Json6String8capacityEv(const String* self) { return self->text->capacity(); } +bool APS5_VABI _ZNK3sce4Json6StringeqEPKc(const String* self, const char* text) { return text != nullptr && CompareText(self->text->c_str(), text) == 0; } +bool APS5_VABI _ZNK3sce4Json6StringeqERKS1_(const String* self, const String* other) { return other != nullptr && other->text != nullptr && CompareText(self->text->c_str(), other->text->c_str()) == 0; } +void APS5_VABI _ZN3sce4Json6String5clearEv(String* self) { self->text->clear(); } +void APS5_VABI _ZN3sce4Json6String6resizeEm(String* self, std::size_t count) { self->text->resize(count); } +void APS5_VABI _ZN3sce4Json6String7reserveEm(String* self, std::size_t capacity) { self->text->reserve(capacity); } +String* APS5_VABI _ZN3sce4Json6String6appendERKS1_(String* self, const String* other) { + const std::string copy = *other->text; + self->text->append(copy); + return self; +} +String* APS5_VABI _ZN3sce4Json6String6appendEPKc(String* self, const char* text) { + if (text == nullptr) throw std::invalid_argument("sce::Json::String::append: null text"); + self->text->append(text); + return self; +} +String* APS5_VABI _ZN3sce4Json6String6appendEPKcm(String* self, const char* text, std::size_t length) { + if (text == nullptr && length > 0) throw std::invalid_argument("sce::Json::String::append: null text"); + self->text->append(text, length); + return self; +} +String* APS5_VABI _ZN3sce4Json6StringpLEPKc(String* self, const char* text) { + return _ZN3sce4Json6String6appendEPKc(self, text); +} +String* APS5_VABI _ZN3sce4Json6StringpLEh(String* self, unsigned char ch) { + self->text->push_back(static_cast(ch)); + return self; +} +String* APS5_VABI _ZNK3sce4Json6String6substrEmm(String* result, const String* self, std::size_t position, std::size_t length) { + if (position > self->text->size()) throw std::out_of_range("sce::Json::String::substr: position past the end"); + Construct(*result, self->text->substr(position, length)); + return result; +} +std::size_t APS5_VABI _ZNK3sce4Json6String4findEcm(const String* self, char character, std::size_t position) { return self->text->find(character, position); } +std::size_t APS5_VABI _ZNK3sce4Json6String5rfindEcm(const String* self, char character, std::size_t position) { return self->text->rfind(character, position); } int APS5_VABI _ZNK3sce4Json6String7compareEPKc(const String* self, const char* text) { if (text == nullptr) throw std::invalid_argument("sce::Json::String::compare: null text"); - return self->text->compare(text); + return CompareText(self->text->c_str(), text); +} +int APS5_VABI _ZNK3sce4Json6String7compareERKS1_(const String* self, const String* other) { + if (other == nullptr || other->text == nullptr) throw std::invalid_argument("sce::Json::String::compare: null other"); + return CompareText(self->text->c_str(), other->text->c_str()); } void APS5_VABI _ZN3sce4Json5ArrayC1Ev(Array* self) { Construct(*self); } @@ -539,10 +740,15 @@ void APS5_VABI _ZN3sce4Json5ArrayD1Ev(Array* self) { Destroy(*self); } void APS5_VABI _ZN3sce4Json5Array9push_backERKNS0_5ValueE(Array* self, const Value* value) { Value copy{}; CopyInto(copy, *value); - self->items->push_back(copy); + try { + self->items->push_back(copy); + } catch (...) { + Destroy(copy); + throw; + } } -ArrayIterator* APS5_VABI _ZNK3sce4Json5Array5beginEv(ArrayIterator* result, const Array* self) { result->it = self->items->begin(); return result; } -ArrayIterator* APS5_VABI _ZNK3sce4Json5Array3endEv(ArrayIterator* result, const Array* self) { result->it = self->items->end(); return result; } +ArrayIterator* APS5_VABI _ZNK3sce4Json5Array5beginEv(ArrayIterator* result, const Array* self) { result->state = new ArrayIteratorState{self->items->begin(), self->items->end()}; return result; } +ArrayIterator* APS5_VABI _ZNK3sce4Json5Array3endEv(ArrayIterator* result, const Array* self) { result->state = new ArrayIteratorState{self->items->end(), self->items->end()}; return result; } bool APS5_VABI _ZNK3sce4Json5Array5emptyEv(const Array* self) { return self->items->empty(); } std::size_t APS5_VABI _ZNK3sce4Json5Array4sizeEv(const Array* self) { return self->items->size(); } void APS5_VABI _ZN3sce4Json5Array5clearEv(Array* self) { @@ -557,11 +763,39 @@ const Value* APS5_VABI _ZNK3sce4Json5Array4backEv(const Array* self) { if (self->items->empty()) throw std::out_of_range("sce::Json::Array::back: empty array"); return &self->items->back(); } -void APS5_VABI _ZN3sce4Json5Array8iteratorD1Ev(ArrayIterator* self) { (void)self; } -ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iteratorppEv(ArrayIterator* self) { ++self->it; return self; } -Value* APS5_VABI _ZNK3sce4Json5Array8iteratordeEv(const ArrayIterator* self) { return &*self->it; } -Value* APS5_VABI _ZNK3sce4Json5Array8iteratorptEv(const ArrayIterator* self) { return &*self->it; } -bool APS5_VABI _ZNK3sce4Json5Array8iteratorneERKS2_(const ArrayIterator* self, const ArrayIterator* other) { return self->it != other->it; } +void APS5_VABI _ZN3sce4Json5Array9pop_frontEv(Array* self) { + if (self->items->empty()) throw std::out_of_range("sce::Json::Array::pop_front: empty array"); + Destroy(self->items->front()); + self->items->pop_front(); +} +void APS5_VABI _ZN3sce4Json5Array8iteratorD1Ev(ArrayIterator* self) { delete self->state; self->state = nullptr; } +ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iteratorppEv(ArrayIterator* self) { + if (self->state && self->state->position != self->state->end) ++self->state->position; + return self; +} +Value* APS5_VABI _ZNK3sce4Json5Array8iteratordeEv(const ArrayIterator* self) { + if (!self->state || self->state->position == self->state->end) throw std::out_of_range("sce::Json::Array::iterator: no current element"); + return &*self->state->position; +} +Value* APS5_VABI _ZNK3sce4Json5Array8iteratorptEv(const ArrayIterator* self) { return _ZNK3sce4Json5Array8iteratordeEv(self); } +bool APS5_VABI _ZNK3sce4Json5Array8iteratorneERKS2_(const ArrayIterator* self, const ArrayIterator* other) { + if (!self->state || !other->state) return false; + const Value* left = self->state && self->state->position != self->state->end ? &*self->state->position : nullptr; + const Value* right = other->state && other->state->position != other->state->end ? &*other->state->position : nullptr; + return left != right; +} +void APS5_VABI _ZN3sce4Json5Array8iteratorC1ERKS2_(ArrayIterator* self, const ArrayIterator* other) { + self->state = other->state ? new ArrayIteratorState(*other->state) : nullptr; +} +ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iterator7advanceEm(ArrayIterator* self, std::size_t count) { + if (self->state) { + while (count && self->state->position != self->state->end) { + ++self->state->position; + --count; + } + } + return self; +} void APS5_VABI _ZN3sce4Json6ObjectC1Ev(Object* self) { Construct(*self); } void APS5_VABI _ZN3sce4Json6ObjectC1ERKS1_(Object* self, const Object* other) { CopyObject(*self, *other); } @@ -582,19 +816,56 @@ void APS5_VABI _ZN3sce4Json6Object5clearEv(Object* self) { self->items->clear(); } Value* APS5_VABI _ZN3sce4Json6ObjectixERKNS0_6StringE(Object* self, const String* key) { return &ObjectEntry(*self, *key->text); } -ObjectIterator* APS5_VABI _ZNK3sce4Json6Object5beginEv(ObjectIterator* result, const Object* self) { result->it = self->items->begin(); return result; } -ObjectIterator* APS5_VABI _ZNK3sce4Json6Object3endEv(ObjectIterator* result, const Object* self) { result->it = self->items->end(); return result; } -void APS5_VABI _ZN3sce4Json6Object8iteratorD1Ev(ObjectIterator* self) { (void)self; } -ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEv(ObjectIterator* self) { ++self->it; return self; } -Pair* APS5_VABI _ZNK3sce4Json6Object8iteratordeEv(const ObjectIterator* self) { return &*self->it; } -Pair* APS5_VABI _ZNK3sce4Json6Object8iteratorptEv(const ObjectIterator* self) { return &*self->it; } +ObjectIterator* APS5_VABI _ZNK3sce4Json6Object5beginEv(ObjectIterator* result, const Object* self) { result->state = new ObjectIteratorState{self->items, self->items->begin()}; return result; } +ObjectIterator* APS5_VABI _ZNK3sce4Json6Object3endEv(ObjectIterator* result, const Object* self) { result->state = new ObjectIteratorState{self->items, self->items->end()}; return result; } +void APS5_VABI _ZN3sce4Json6Object8iteratorD1Ev(ObjectIterator* self) { delete self->state; self->state = nullptr; } +ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEv(ObjectIterator* self) { + if (self->state && self->state->owner) { + if (self->state->position == self->state->owner->end()) throw std::out_of_range("sce::Json::Object::iterator: advance past the end"); + ++self->state->position; + } + return self; +} +Pair* APS5_VABI _ZNK3sce4Json6Object8iteratordeEv(const ObjectIterator* self) { + if (!self->state || !self->state->owner || self->state->position == self->state->owner->end()) throw std::out_of_range("sce::Json::Object::iterator: no current element"); + return &*self->state->position; +} +Pair* APS5_VABI _ZNK3sce4Json6Object8iteratorptEv(const ObjectIterator* self) { return _ZNK3sce4Json6Object8iteratordeEv(self); } ObjectIterator* APS5_VABI _ZNK3sce4Json6Object4findERKNS0_6StringE(ObjectIterator* result, const Object* self, const String* key) { - result->it = std::find_if(self->items->begin(), self->items->end(), [&](const Pair& pair) { return *pair.key.text == *key->text; }); + const auto position = std::find_if(self->items->begin(), self->items->end(), [&](const Pair& pair) { return std::strcmp(pair.key.text->c_str(), key->text->c_str()) == 0; }); + result->state = new ObjectIteratorState{self->items, position}; return result; } -bool APS5_VABI _ZNK3sce4Json6Object8iteratorneERKS2_(const ObjectIterator* self, const ObjectIterator* other) { return self->it != other->it; } +bool APS5_VABI _ZNK3sce4Json6Object8iteratoreqERKS2_(const ObjectIterator* self, const ObjectIterator* other) { + if (!self->state || !other->state) return false; + return self->state->owner == other->state->owner && self->state->position == other->state->position; +} +bool APS5_VABI _ZNK3sce4Json6Object8iteratorneERKS2_(const ObjectIterator* self, const ObjectIterator* other) { + if (!self->state || !other->state) return false; + return !_ZNK3sce4Json6Object8iteratoreqERKS2_(self, other); +} +void APS5_VABI _ZN3sce4Json6Object8iteratorC1ERKS2_(ObjectIterator* self, const ObjectIterator* other) { + self->state = other->state ? new ObjectIteratorState(*other->state) : nullptr; +} +ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iterator7advanceEm(ObjectIterator* self, std::size_t count) { + if (self->state && self->state->owner) { + while (count) { + _ZN3sce4Json6Object8iteratorppEv(self); + --count; + } + } + return self; +} bool APS5_VABI _ZNK3sce4Json6Object5emptyEv(const Object* self) { return self->items->empty(); } std::size_t APS5_VABI _ZNK3sce4Json6Object4sizeEv(const Object* self) { return self->items->size(); } +void APS5_VABI _ZN3sce4Json6Object5eraseERKNS0_6StringE(Object* self, const String* key) { + if (key == nullptr || key->text == nullptr) throw std::invalid_argument("sce::Json::Object::erase: null key"); + auto it = std::find_if(self->items->begin(), self->items->end(), [&](const Pair& pair) { return std::strcmp(pair.key.text->c_str(), key->text->c_str()) == 0; }); + if (it == self->items->end()) return; + Destroy(it->key); + Destroy(it->value); + self->items->erase(it); +} void APS5_VABI _ZN3sce4Json5ValueC1Ev(Value* self) { Construct(*self); } void APS5_VABI _ZN3sce4Json5ValueC1Eb(Value* self, bool value) { Construct(*self); self->node->type = TypeBoolean; self->node->boolean = value; } @@ -603,24 +874,28 @@ void APS5_VABI _ZN3sce4Json5ValueC1Em(Value* self, std::uint64_t value) { Constr void APS5_VABI _ZN3sce4Json5ValueC1Ed(Value* self, double value) { Construct(*self); self->node->type = TypeReal; self->node->real = value; } void APS5_VABI _ZN3sce4Json5ValueC1EPKc(Value* self, const char* text) { if (text == nullptr) throw std::invalid_argument("sce::Json::Value: null text"); - Construct(*self); - SetType(*self->node, TypeString); - *self->node->string.text = text; + Construct(*self, [text](Node& node) { + SetType(node, TypeString); + *node.string.text = text; + }); } void APS5_VABI _ZN3sce4Json5ValueC1ERKNS0_6StringE(Value* self, const String* text) { - Construct(*self); - SetType(*self->node, TypeString); - *self->node->string.text = *text->text; + Construct(*self, [text](Node& node) { + SetType(node, TypeString); + *node.string.text = *text->text; + }); } void APS5_VABI _ZN3sce4Json5ValueC1ERKNS0_5ArrayE(Value* self, const Array* array) { - Construct(*self); - self->node->type = TypeArray; - CopyArray(self->node->array, *array); + Construct(*self, [array](Node& node) { + node.type = TypeArray; + CopyArray(node.array, *array); + }); } void APS5_VABI _ZN3sce4Json5ValueC1ERKNS0_6ObjectE(Value* self, const Object* object) { - Construct(*self); - self->node->type = TypeObject; - CopyObject(self->node->object, *object); + Construct(*self, [object](Node& node) { + node.type = TypeObject; + CopyObject(node.object, *object); + }); } void APS5_VABI _ZN3sce4Json5ValueC1ERKS1_(Value* self, const Value* other) { CopyInto(*self, *other); } void APS5_VABI _ZN3sce4Json5ValueD1Ev(Value* self) { Destroy(*self); } @@ -628,6 +903,18 @@ Value* APS5_VABI _ZN3sce4Json5ValueaSERKS1_(Value* self, const Value* other) { if (self != other) AssignNode(NodeOf(*self), NodeOf(*other)); return self; } +void APS5_VABI _ZN3sce4Json5Value4swapERS1_(Value* self, Value* other) { + Node temporary{}; + try { + AssignNode(temporary, NodeOf(*self)); + AssignNode(NodeOf(*self), NodeOf(*other)); + AssignNode(NodeOf(*other), temporary); + } catch (...) { + Clear(temporary); + throw; + } + Clear(temporary); +} int APS5_VABI _ZN3sce4Json5Value3setENS0_9ValueTypeE(Value* self, ValueType type) { SetType(NodeOf(*self), type); return 0; } int APS5_VABI _ZN3sce4Json5Value3setEb(Value* self, bool value) { SetType(NodeOf(*self), TypeBoolean); self->node->boolean = value; return 0; } @@ -672,15 +959,15 @@ ValueType APS5_VABI _ZNK3sce4Json5Value7getTypeEv(const Value* self) { return No const bool* APS5_VABI _ZNK3sce4Json5Value10getBooleanEv(const Value* self) { return &Typed(self, TypeBoolean).boolean; } const std::int64_t* APS5_VABI _ZNK3sce4Json5Value10getIntegerEv(const Value* self) { Node& n = const_cast(NodeOf(*self)); - if (n.type == TypeUInteger && n.uinteger <= static_cast(INT64_MAX)) { - n.integer = static_cast(n.uinteger); + if (n.type == TypeUInteger) { + n.integer = std::bit_cast(n.uinteger); return &n.integer; } return &Typed(self, TypeInteger).integer; } const std::uint64_t* APS5_VABI _ZNK3sce4Json5Value11getUIntegerEv(const Value* self) { Node& n = const_cast(NodeOf(*self)); - if (n.type == TypeInteger && n.integer >= 0) { + if (n.type == TypeInteger) { n.uinteger = static_cast(n.integer); return &n.uinteger; } @@ -707,8 +994,8 @@ const Value* APS5_VABI _ZNK3sce4Json5ValueixEPKc(const Value* self, const char* const Node& n = NodeOf(*self); if (key != nullptr && n.type == TypeObject) for (const auto& pair : *n.object.items) - if (*pair.key.text == key) return &pair.value; - return &NullAccess(TypeNull, self); + if (std::strcmp(pair.key.text->c_str(), key) == 0) return &pair.value; + return &MissingElement(self); } const Value* APS5_VABI _ZNK3sce4Json5ValueixEm(const Value* self, std::size_t index) { const Node& n = NodeOf(*self); @@ -717,19 +1004,18 @@ const Value* APS5_VABI _ZNK3sce4Json5ValueixEm(const Value* self, std::size_t in std::advance(it, static_cast(index)); return &*it; } - return &NullAccess(TypeNull, self); + return &MissingElement(self); } void APS5_VABI _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(Value* self, ValueType type) { - Construct(*self); - SetType(*self->node, type); + Construct(*self, [type](Node& node) { SetType(node, type); }); } const Value* APS5_VABI _ZNK3sce4Json5Value8getValueEm(const Value* self, std::size_t index) { return _ZNK3sce4Json5ValueixEm(self, index); } const Value* APS5_VABI _ZNK3sce4Json5Value8getValueERKNS0_6StringE(const Value* self, const String* key) { const Node& n = NodeOf(*self); if (key != nullptr && n.type == TypeObject) for (const auto& pair : *n.object.items) - if (*pair.key.text == *key->text) return &pair.value; - return &NullAccess(TypeNull, self); + if (std::strcmp(pair.key.text->c_str(), key->text->c_str()) == 0) return &pair.value; + return &MissingElement(self); } Value* APS5_VABI _ZN3sce4Json5Value10referValueEm(Value* self, std::size_t index) { Node& n = NodeOf(*self); @@ -742,7 +1028,7 @@ Value* APS5_VABI _ZN3sce4Json5Value10referValueERKNS0_6StringE(Value* self, cons Node& n = NodeOf(*self); if (key == nullptr || n.type != TypeObject) return nullptr; for (auto& pair : *n.object.items) - if (*pair.key.text == *key->text) return &pair.value; + if (std::strcmp(pair.key.text->c_str(), key->text->c_str()) == 0) return &pair.value; return nullptr; } Array* APS5_VABI _ZN3sce4Json5Value10referArrayEv(Value* self) { @@ -775,36 +1061,18 @@ int APS5_VABI _ZNK3sce4Json5Value8toStringERNS0_6StringE(const Value* self, Stri return 0; } -using ParserReadCallback = int (APS5_VABI*)(char&, void*); - -int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value* out, const char* text, std::size_t size); int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(Value* out, ParserReadCallback read, void* context) { if (read == nullptr) throw std::invalid_argument("sce::Json::Parser::parse: null read callback"); - std::string text; - char ch = 0; - while (read(ch, context) == 0 && ch != '\0') text.push_back(ch); - return _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(out, text.data(), text.size()); + Parser parser(read, context, NodeOf(*out).handlers); + return ParseValue(out, parser); } int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value* out, const char* text, std::size_t size) { + if (size == 0) return JsonErrorParse; if (text == nullptr) throw std::invalid_argument("sce::Json::Parser::parse: null text"); - Node parsed{}; - Parser parser(text, size); - bool parsedOk = false; - try { - parsedOk = parser.Parse(parsed); - } catch (...) { - Clear(parsed); - throw; - } - if (!parsedOk) { - Clear(parsed); - return JsonErrorParse; - } - Clear(NodeOf(*out)); - *out->node = parsed; - return 0; + Parser parser(text, size, NodeOf(*out).handlers); + return ParseValue(out, parser); } @@ -831,6 +1099,7 @@ static_assert(sizeof(InitParameter2) == 0x28 && offsetof(InitParameter2, special void APS5_VABI _ZN3sce4Json14InitParameter2C1Ev(InitParameter2* self) { *self = {}; + self->specialFloatFormatType = 2; } void APS5_VABI _ZN3sce4Json14InitParameter2C2Ev(InitParameter2* self) { @@ -866,63 +1135,154 @@ void APS5_VABI _ZN3sce4Json5Value5clearEv(Value* self) { Clear(NodeOf(*self)); } -int APS5_VABI _ZN3sce4Json12MemAllocator11notifyErrorEimPv(void) { - NotImplemented_nid_no_patch(__func__); +void APS5_VABI _ZN3sce4Json12MemAllocator11notifyErrorEimPv(void* self, int error, std::size_t size, void* userData) { + (void)self; + (void)error; + (void)size; + (void)userData; +} + +void APS5_VABI _ZN3sce4Json5Value21setNullAccessCallBackEPFRKS1_NS0_9ValueTypeEPS2_PvES6_(NullAccessCallback callback, void* context) { + auto& state = State(); + std::lock_guard lock(state.mutex); + state.callback = callback; + state.context = context; +} + +int APS5_VABI _ZN3sce4Json5Value30setElementAccessFailureHandlerEPFviPKNS0_6StringEPKmPKS1_S8_PvES9_(Value* self, ElementAccessFailureHandler handler, void* context) { + SetHandlers(NodeOf(*self), [&](Handlers& handlers) { + handlers.elementAccessFailure = handler; + handlers.elementAccessFailureContext = context; + }); return 0; } -int APS5_VABI _ZN3sce4Json5Value21setNullAccessCallBackEPFRKS1_NS0_9ValueTypeEPS2_PvES6_() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI _ZN3sce4Json5Value22setSpecialFloatHandlerEPFKS1_NS0_12FunctionTypeEdPS2_PvPbES5_(Value* self, SpecialFloatHandler handler, void* context) { + SetHandlers(NodeOf(*self), [&](Handlers& handlers) { + handlers.specialFloat = handler; + handlers.specialFloatContext = context; + }); return 0; } -int APS5_VABI _ZN3sce4Json5Value30setElementAccessFailureHandlerEPFviPKNS0_6StringEPKmPKS1_S8_PvES9_(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI _ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEPS2_S5_PvES6_(Value* self, TypeMismatchHandler handler, void* context) { + SetHandlers(NodeOf(*self), [&](Handlers& handlers) { + handlers.typeMismatch = handler; + handlers.typeMismatchContext = context; + }); return 0; } -int APS5_VABI _ZN3sce4Json5Value22setSpecialFloatHandlerEPFKS1_NS0_12FunctionTypeEdPS2_PvPbES5_(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +bool APS5_VABI _ZNK3sce4Json5ValuecvbEv(const Value* self) { + const Node& n = NodeOf(*self); + switch (n.type) { + case TypeNull: return false; + case TypeBoolean: return n.boolean; + case TypeInteger: return n.integer != 0; + case TypeUInteger: return n.uinteger != 0; + case TypeReal: return n.real != 0.0; + case TypeString: return n.string.text && !n.string.text->empty(); + case TypeArray: + case TypeObject: return true; + default: throw std::runtime_error("sce::Json::Value: invalid value type"); + } } -int APS5_VABI _ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEPS2_S5_PvES6_(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(Pair* self, const String* key, const Value* value) { + Pair pair{}; + pair.reserved = 0; + Construct(pair.key, *key->text); + try { + CopyInto(pair.value, *value); + } catch (...) { + Destroy(pair.key); + throw; + } + *self = pair; } -int APS5_VABI _ZNK3sce4Json5ValuecvbEv(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI _ZN3sce4Json6Object4PairD1Ev(Pair* self) { + Destroy(self->key); + Destroy(self->value); } -int APS5_VABI _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE() { - NotImplemented_nid_no_patch(__func__); - return 0; +ObjectIterator* APS5_VABI _ZN3sce4Json6Object6insertERKNS1_4PairE(ObjectIterator* result, Object* self, const Pair* pair) { + auto it = ObjectPosition(*self, *pair->key.text); + auto state = std::make_unique(ObjectIteratorState{self->items, it}); + if (it != self->items->end() && std::strcmp(it->key.text->c_str(), pair->key.text->c_str()) == 0) { + result->state = state.release(); + return result; + } + Pair copy{}; + copy.reserved = 0; + Construct(copy.key, *pair->key.text); + try { + CopyInto(copy.value, pair->value); + state->position = self->items->insert(it, copy); + result->state = state.release(); + return result; + } catch (...) { + Destroy(copy.value); + Destroy(copy.key); + throw; + } } -int APS5_VABI _ZN3sce4Json6Object4PairD1Ev() { - NotImplemented_nid_no_patch(__func__); - return 0; +ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iteratorppEi(ArrayIterator* result, ArrayIterator* self, int) { + _ZN3sce4Json5Array8iteratorC1ERKS2_(result, self); + _ZN3sce4Json5Array8iteratorppEv(self); + return result; } -int APS5_VABI _ZN3sce4Json6Object6insertERKNS1_4PairE() { - NotImplemented_nid_no_patch(__func__); - return 0; +ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iteratoraSERKS2_(ArrayIterator* self, const ArrayIterator* other) { + if (self->state) { + if (other->state && other->state->position != other->state->end) { + *self->state = *other->state; + } else { + delete self->state; + self->state = nullptr; + } + } + return self; } -int APS5_VABI _ZN3sce4Json5Array8iteratorppEi() { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI _ZN3sce4Json5Array8iteratorC1Ev(ArrayIterator* self) { + self->state = new ArrayIteratorState{}; } -int APS5_VABI _ZN3sce4Json5Array8iteratoraSERKS2_() { - NotImplemented_nid_no_patch(__func__); - return 0; +ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEi(ObjectIterator* result, ObjectIterator* self, int) { + auto state = self->state ? std::make_unique(*self->state) : nullptr; + _ZN3sce4Json6Object8iteratorppEv(self); + result->state = state.release(); + return result; } -int APS5_VABI _ZN3sce4Json5Array8iteratorC1Ev() { - NotImplemented_nid_no_patch(__func__); - return 0; +ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratoraSERKS2_(ObjectIterator* self, const ObjectIterator* other) { + if (self->state) { + if (other->state && other->state->owner) { + *self->state = *other->state; + } else { + delete self->state; + self->state = nullptr; + } + } + return self; +} + +void APS5_VABI _ZN3sce4Json6Object8iteratorC1Ev(ObjectIterator* self) { + self->state = new ObjectIteratorState{}; +} + +void APS5_VABI _ZN3sce4Json6Object4PairC1Ev(Pair* self) { + Pair pair{}; + pair.reserved = 0; + Construct(pair.key, std::string()); + try { + Construct(pair.value); + } catch (...) { + Destroy(pair.key); + throw; + } + *self = pair; } } diff --git a/core/libs/prx/libSceJxr/CMakeLists.txt b/core/libs/prx/libSceJxr/CMakeLists.txt new file mode 100644 index 0000000000..251402ba35 --- /dev/null +++ b/core/libs/prx/libSceJxr/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceJxr SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceJxr PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceJxr) + +if(BUILD_TESTING) + add_test_executable(guest_jxr_tests ${LIBS_INCLUDE_DIR}/tests/GuestJxr.cpp) + target_include_directories(guest_jxr_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_jxr_tests PRIVATE libSceJxr libc) + configure_windows_unwind(guest_jxr_tests) + add_test(NAME guest_jxr COMMAND guest_jxr_tests) + if(WIN32) + set_tests_properties(guest_jxr PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceJxr/Export.cpp b/core/libs/prx/libSceJxr/Export.cpp new file mode 100644 index 0000000000..de2c55aa4e --- /dev/null +++ b/core/libs/prx/libSceJxr/Export.cpp @@ -0,0 +1,38 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { +constexpr std::int64_t WmpErrSuccess = 0; +} + +extern "C" { + +void APS5_VABI free_nid_postfix(void* memory); + +APS5_DUMMY_FUN +std::int64_t APS5_VABI PKFree_nid_postfix(void** memory) { + if (memory != nullptr) { + free_nid_postfix(*memory); + *memory = nullptr; + } + return WmpErrSuccess; +} + +int APS5_VABI PixelFormatLookup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI readIS_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI writeIS_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceJxrParser/Export.cpp b/core/libs/prx/libSceJxrParser/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceJxrParser/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceKbEmulate/Export.cpp b/core/libs/prx/libSceKbEmulate/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceKbEmulate/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceKeyboard/Export.cpp b/core/libs/prx/libSceKeyboard/Export.cpp index 9333cf673d..9cbc7cd436 100644 --- a/core/libs/prx/libSceKeyboard/Export.cpp +++ b/core/libs/prx/libSceKeyboard/Export.cpp @@ -1,5 +1,9 @@ +#include "prx/libc/include/General.hpp" #include "prx/libSceKeyboard/include/KeyboardState.hpp" +struct KeyboardDeviceInfo; +struct KeyboardProcessFocus; + extern "C" { int APS5_VABI sceKeyboardInit(void) { @@ -23,8 +27,106 @@ int APS5_VABI sceKeyboardReadState(int32_t handle, KeyboardData* data) { return Keyboard::ReadState(handle, data); } -int APS5_VABI sceKeyboardGetKey2Char(int32_t handle, int32_t arrange, uint32_t led, uint32_t modifier_key, uint16_t key_code, KeyboardCharData* char_data) { - return Keyboard::GetKey2Char(handle, arrange, led, modifier_key, key_code, char_data); +int APS5_VABI sceKeyboardGetConnection(std::int32_t handle, bool* connected) { + return Keyboard::GetConnection(handle, connected); +} + +int APS5_VABI sceKeyboardGetKey2Char(std::int32_t handle, std::int32_t arrange, std::uint8_t led, std::uint8_t modifierKey, std::uint16_t keyCode, KeyboardCharData* charData) { + return Keyboard::GetKey2Char(handle, arrange, led, modifierKey, keyCode, charData); +} + +int APS5_VABI sceKeyboardConnectPort(std::int32_t first, std::int32_t second) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardDebugGetDeviceId(std::int32_t handle, std::int32_t* deviceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardDeviceOpen(std::uint64_t deviceId, std::int32_t index, std::uint32_t options) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardDisconnectAll(void) { + return Keyboard::DisconnectAll(); +} + +int APS5_VABI sceKeyboardDisconnectDevice(std::int32_t deviceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardDisconnectPort(std::int32_t first, std::int32_t second) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardGetDeviceInfo(std::int32_t deviceId, KeyboardDeviceInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardGetHandle(std::int32_t userId, std::int32_t type, std::int32_t index) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardMbusInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardPadEmulateClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardPadEmulateOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardSetProcessFocus(const KeyboardProcessFocus* focus) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKeyboardSetProcessPrivilege(std::uint8_t privilege) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDbgKeyboardClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDbgKeyboardGetKey2Char() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDbgKeyboardInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDbgKeyboardOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDbgKeyboardRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDbgKeyboardReadState() { + NotImplemented_nid_no_patch(__func__); + return 0; } void KeyboardPublishInput_nid_postfix(const KeyboardInputEvent& event) { diff --git a/core/libs/prx/libSceKeyboard/include/KeyboardState.hpp b/core/libs/prx/libSceKeyboard/include/KeyboardState.hpp index 11c32a5447..3552af3153 100644 --- a/core/libs/prx/libSceKeyboard/include/KeyboardState.hpp +++ b/core/libs/prx/libSceKeyboard/include/KeyboardState.hpp @@ -18,6 +18,8 @@ int Open(int userId, std::int32_t type, std::int32_t index); int Close(std::int32_t handle); int Read(std::int32_t handle, KeyboardData* data, std::int32_t num); int ReadState(std::int32_t handle, KeyboardData* data); +int GetConnection(std::int32_t handle, bool* connected); +int DisconnectAll(); int GetKey2Char(std::int32_t handle, std::int32_t arrange, std::uint32_t led, std::uint32_t modifierKey, std::uint16_t keyCode, KeyboardCharData* charData); void Publish(const KeyboardInputEvent& event); } diff --git a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp index 0a16e2d75e..954040b147 100644 --- a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp +++ b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp @@ -19,8 +19,8 @@ std::uint32_t modifiers = 0; std::array keys{}; int keyCount = 0; -constexpr char US_NORMAL[] = "1234567890\n\0\b\t -=[]\\\0;'`,./"; -constexpr char US_SHIFTED[] = "!@#$%^&*()\n\0\b\t _+{}|\0:\"~<>?"; +constexpr char US_NORMAL[] = "1234567890\n\0\b\t -=[]\\\\;'`,./"; +constexpr char US_SHIFTED[] = "!@#$%^&*()\n\0\b\t _+{}||:\"~<>?"; constexpr char JIS_NORMAL[] = "1234567890\n\0\b\t -^@[]];:\0,./"; constexpr char JIS_SHIFTED[] = "!\"#$%&'()\0\n\0\b\t =~`{}}+*\0<>?"; constexpr char KEYPAD_DIGITS[] = "1234567890."; @@ -81,8 +81,9 @@ std::uint16_t character(bool jis, std::uint32_t ledState, std::uint32_t modifier const char* table = jis ? (shift ? JIS_SHIFTED : JIS_NORMAL) : (shift ? US_SHIFTED : US_NORMAL); return static_cast(table[keyCode - 0x1e]); } + if (!jis && keyCode == 0x64) return shift ? '|' : '\\'; if (jis && keyCode == 0x87) return shift ? '_' : '\\'; - if (jis && keyCode == 0x89) return shift ? '|' : '\\'; + if (jis && keyCode == 0x89) return shift ? '|' : 0x00A5; switch (keyCode) { case 0x54: return '/'; case 0x55: return '*'; @@ -153,6 +154,23 @@ int ReadState(std::int32_t handle, KeyboardData* data) { return KEYBOARD_OK; } +int GetConnection(std::int32_t handle, bool* isConnected) { + std::lock_guard lock(keyboardMutex); + if (!initialized) return KEYBOARD_ERROR_NOT_INITIALIZED; + if (handle != KEYBOARD_HANDLE || !opened) return KEYBOARD_ERROR_INVALID_HANDLE; + if (isConnected == nullptr) return KEYBOARD_ERROR_INVALID_ARG; + *isConnected = connected; + return KEYBOARD_OK; +} + +int DisconnectAll() { + KeyboardInputEvent event{}; + event.connectionChange = true; + event.connected = false; + Publish(event); + return KEYBOARD_OK; +} + int GetKey2Char(std::int32_t handle, std::int32_t arrange, std::uint32_t ledState, std::uint32_t modifierKey, std::uint16_t keyCode, KeyboardCharData* charData) { { std::lock_guard lock(keyboardMutex); diff --git a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp index cb05958e16..0307b994df 100644 --- a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp +++ b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp @@ -5,6 +5,7 @@ #include "SDL_keyboard.h" #include +#include extern "C" { int APS5_VABI sceKeyboardInit(void); @@ -12,7 +13,10 @@ int APS5_VABI sceKeyboardOpen(int, std::int32_t, std::int32_t, const void*); int APS5_VABI sceKeyboardClose(std::int32_t); int APS5_VABI sceKeyboardRead(std::int32_t, KeyboardData*, std::int32_t); int APS5_VABI sceKeyboardReadState(std::int32_t, KeyboardData*); -int APS5_VABI sceKeyboardGetKey2Char(std::int32_t, std::int32_t, std::uint32_t, std::uint32_t, std::uint16_t, KeyboardCharData*); +int APS5_VABI sceKeyboardGetConnection(std::int32_t, bool*); +int APS5_VABI sceKeyboardGetKey2Char(std::int32_t, std::int32_t, std::uint8_t, std::uint8_t, std::uint16_t, KeyboardCharData*); +int APS5_VABI sceKeyboardConnectPort(void); +int APS5_VABI sceKeyboardDisconnectAll(void); } static void Require(bool value) { if (!value) std::abort(); } @@ -41,14 +45,20 @@ static std::uint16_t Jis(std::uint32_t modifierKey, std::uint16_t keyCode, std:: int main() { KeyboardData data[16]{}; KeyboardCharData charData{}; + bool connected = false; Require(sceKeyboardOpen(1, 0, 0, nullptr) == KEYBOARD_ERROR_NOT_INITIALIZED); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_ERROR_NOT_INITIALIZED); Require(sceKeyboardInit() == KEYBOARD_OK); Require(sceKeyboardInit() == KEYBOARD_OK); Require(sceKeyboardOpen(1, 0, 1, nullptr) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardOpen(1, 1, 0, nullptr) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, KEYBOARD_ARRANGEMENT_101, 0, 0, 0x04, &charData) == KEYBOARD_ERROR_INVALID_HANDLE); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardOpen(1, 0, 0, nullptr) == KEYBOARD_HANDLE); Require(sceKeyboardOpen(1, 0, 0, nullptr) == KEYBOARD_ERROR_ALREADY_OPENED); + Require(sceKeyboardGetConnection(42, &connected) == KEYBOARD_ERROR_INVALID_HANDLE); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, nullptr) == KEYBOARD_ERROR_INVALID_ARG); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_OK && connected); Require(sceKeyboardRead(42, data, 1) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardRead(KEYBOARD_HANDLE, nullptr, 1) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 17) == KEYBOARD_ERROR_INVALID_ARG); @@ -93,9 +103,11 @@ int main() { window.window.event = SDL_WINDOWEVENT_CLOSE; input.HandleEvent(window, 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && !data[0].connected && data[0].length == 1 && data[0].key_code[0] == 0); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_OK && !connected); window.window.event = SDL_WINDOWEVENT_FOCUS_GAINED; input.HandleEvent(window, 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].connected); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_OK && connected); for (std::uint16_t key = 0x04; key < 0x04 + 20; ++key) { KeyboardInputEvent press{}; @@ -112,13 +124,15 @@ int main() { Require(Char(KEYBOARD_LED_CAPS_LOCK, 0, 0x1e) == '1' && Char(0, KEYBOARD_MOD_LEFT_SHIFT, 0x1e) == '!'); Require(Char(0, 0, 0x27) == '0' && Char(0, KEYBOARD_MOD_LEFT_SHIFT, 0x27) == ')'); Require(Char(0, 0, 0x28) == '\n' && Char(0, 0, 0x2a) == '\b' && Char(0, 0, 0x2b) == '\t' && Char(0, 0, 0x2c) == ' '); - Require(Char(0, 0, 0x29) == 0 && Char(0, 0, 0x32) == 0 && Char(0, 0, 0x3a) == 0 && Char(0, 0, 0xe1) == 0); + Require(Char(0, 0, 0x29) == 0 && Char(0, 0, 0x3a) == 0 && Char(0, 0, 0xe1) == 0); Require(Char(0, 0, 0x31) == '\\' && Char(0, KEYBOARD_MOD_LEFT_SHIFT, 0x31) == '|'); Require(Char(0, 0, 0x34) == '\'' && Char(0, KEYBOARD_MOD_LEFT_SHIFT, 0x34) == '"'); Require(Char(0, 0, 0x38) == '/' && Char(0, KEYBOARD_MOD_LEFT_SHIFT, 0x38) == '?'); Require(Char(0, 0, 0x59) == 0 && Char(KEYBOARD_LED_NUM_LOCK, 0, 0x59) == '1'); Require(Char(KEYBOARD_LED_NUM_LOCK, 0, 0x62) == '0' && Char(KEYBOARD_LED_NUM_LOCK, 0, 0x63) == '.'); Require(Char(0, 0, 0x54) == '/' && Char(0, 0, 0x57) == '+' && Char(0, 0, 0x58) == '\n'); + Require(Char(0, 0, 0x32) == '\\' && Char(0, KEYBOARD_MOD_LEFT_SHIFT, 0x32) == '|'); + Require(Char(0, 0, 0x64) == '\\' && Char(0, KEYBOARD_MOD_LEFT_SHIFT, 0x64) == '|'); Require(Char(0, 0, 0x87) == 0 && Char(0, 0, 0x89) == 0); constexpr std::uint32_t shift = KEYBOARD_MOD_LEFT_SHIFT; @@ -131,17 +145,45 @@ int main() { Require(Jis(0, 0x31) == ']' && Jis(shift, 0x31) == '}' && Jis(0, 0x32) == ']' && Jis(shift, 0x32) == '}'); Require(Jis(0, 0x33) == ';' && Jis(shift, 0x33) == '+' && Jis(0, 0x34) == ':' && Jis(shift, 0x34) == '*'); Require(Jis(0, 0x35) == 0 && Jis(shift, 0x35) == 0 && Jis(0, 0x36) == ',' && Jis(shift, 0x37) == '>' && Jis(shift, 0x38) == '?'); - Require(Jis(0, 0x87) == '\\' && Jis(shift, 0x87) == '_' && Jis(0, 0x89) == '\\' && Jis(shift, 0x89) == '|'); + Require(Jis(0, 0x87) == '\\' && Jis(shift, 0x87) == '_' && Jis(0, 0x89) == 0x00A5 && Jis(shift, 0x89) == '|'); Require(Jis(0, 0x59, KEYBOARD_LED_NUM_LOCK) == '1' && Jis(0, 0x59) == 0 && Jis(0, 0x55) == '*' && Jis(0, 0xe1) == 0); Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, KEYBOARD_ARRANGEMENT_101, 0, 0, 0x04, nullptr) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, KEYBOARD_ARRANGEMENT_106, 0, 0, 0x04, nullptr) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, 2, 0, 0, 0x04, &charData) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_OK); + Require(sceKeyboardGetConnection(KEYBOARD_HANDLE, &connected) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardReadState(KEYBOARD_HANDLE, data) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardOpen(1, 0, 0, nullptr) == KEYBOARD_HANDLE); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].length == 1 && data[0].key_code[0] == 0); + Require(data[0].connected); + KeyboardInputEvent pressed{}; + pressed.keyCode = 0x04; + pressed.pressed = true; + KeyboardPublishInput_nid_postfix(pressed); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].key_code[0] == 0x04); + Require(sceKeyboardDisconnectAll() == KEYBOARD_OK); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && !data[0].connected && data[0].length == 1 && data[0].key_code[0] == 0); + Require(sceKeyboardReadState(KEYBOARD_HANDLE, data) == KEYBOARD_OK && !data[0].connected && data[0].key_code[0] == 0); + Require(sceKeyboardDisconnectAll() == KEYBOARD_OK); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 0); + pressed.keyCode = 0x05; + KeyboardPublishInput_nid_postfix(pressed); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 0); + window.window.event = SDL_WINDOWEVENT_FOCUS_LOST; + input.HandleEvent(window, 7); + window.window.event = SDL_WINDOWEVENT_FOCUS_GAINED; + input.HandleEvent(window, 7); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].connected); Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_OK); + + bool threwConnect = false; + try { + sceKeyboardConnectPort(); + } catch (const std::runtime_error&) { + threwConnect = true; + } + Require(threwConnect); } diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index b5dde2fe3f..1297f6abcf 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -1,9 +1,12 @@ add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp Getopt.cpp SignalList.cpp Spawn.cpp UserContext.cpp) target_include_directories(libSceLibcInternal PRIVATE ${LIBS_INCLUDE_DIR}) +set_source_files_properties(UserContext.cpp PROPERTIES COMPILE_OPTIONS -fno-omit-frame-pointer) target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS}) target_sources(libSceLibcInternal PRIVATE Directory.cpp) +target_sources(libSceLibcInternal PRIVATE Err.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) @@ -85,6 +88,16 @@ if(BUILD_TESTING) set_tests_properties(guest_thread_atexit PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_err_tests ${LIBS_INCLUDE_DIR}/tests/GuestErr.cpp) + target_include_directories(guest_err_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_err_tests PRIVATE libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_err_tests) + add_test(NAME guest_err COMMAND guest_err_tests) + set_tests_properties(guest_err PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_err PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_getopt_tests ${LIBS_INCLUDE_DIR}/tests/GuestGetopt.cpp) target_include_directories(guest_getopt_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_getopt_tests PRIVATE libSceLibcInternal libkernel libc) @@ -95,6 +108,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/Directory.cpp b/core/libs/prx/libSceLibcInternal/Directory.cpp index 937f52fd97..48a1614e85 100644 --- a/core/libs/prx/libSceLibcInternal/Directory.cpp +++ b/core/libs/prx/libSceLibcInternal/Directory.cpp @@ -1,16 +1,21 @@ #include "prx/libc/include/GuestDirectory.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include +#include extern "C" int APS5_VABI close_nid_postfix(int d); +extern "C" int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...); extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" void* APS5_VABI fdopendir_nid_postfix(int fd) { + constexpr int SetDescriptorFlags = 2; + constexpr int CloseOnExec = 1; + if (fcntl_nid_postfix(fd, SetDescriptorFlags, CloseOnExec) == -1) return nullptr; int error = 0; const char* path = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); if (path == nullptr) { *__error_nid_postfix() = error == EBADF ? 9 : 20; return nullptr; } - return OpenDescriptorDirectory_nid_no_patch(path, fd, close_nid_postfix); + return OpenDescriptorDirectory_nid_no_patch(path, fd, close_nid_postfix, GetDirectoryDescriptorPosition_nid_no_patch(fd)); } diff --git a/core/libs/prx/libSceLibcInternal/Err.cpp b/core/libs/prx/libSceLibcInternal/Err.cpp new file mode 100644 index 0000000000..f16dd24ca1 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/Err.cpp @@ -0,0 +1,137 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include + +using ErrExit = void (APS5_VABI*)(int); + +extern "C" { +extern const char* __progname_nid_postfix; +int APS5_VABI vfprintf_nid_postfix(FileStream* stream, const char* format, VaList* args); +int APS5_VABI fprintf_nid_postfix(FileStream* stream, const char* format, ...); +int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream); +char* APS5_VABI strerror_nid_postfix(int error); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +FileStream* g_errFile = nullptr; +ErrExit g_errExit = nullptr; + +FileStream* ErrFile() { + if (g_errFile == nullptr) g_errFile = &_Stderr_nid_postfix; + return g_errFile; +} + +const char* ProgramName() { + return __progname_nid_postfix ? __progname_nid_postfix : "(null)"; +} + +void Report(const char* format, VaList* args, const int* code) { + FileStream* const stream = ErrFile(); + fprintf_nid_postfix(stream, "%s: ", ProgramName()); + if (format != nullptr) { + vfprintf_nid_postfix(stream, format, args); + if (code != nullptr) fputs_nid_postfix(": ", stream); + } + if (code != nullptr) fprintf_nid_postfix(stream, "%s\n", strerror_nid_postfix(*code)); + else fputs_nid_postfix("\n", stream); +} + +[[noreturn]] void Exit(int status) { + if (g_errExit != nullptr) g_errExit(status); + LibcExit_nid_no_patch(status); +} + +} + +#ifdef _WIN32 +#define APS5_ERR_VA_LIST __builtin_sysv_va_list +#define APS5_ERR_VA_START(list, last) __builtin_sysv_va_start(list, last) +#define APS5_ERR_VA_END(list) __builtin_sysv_va_end(list) +#else +#define APS5_ERR_VA_LIST std::va_list +#define APS5_ERR_VA_START(list, last) va_start(list, last) +#define APS5_ERR_VA_END(list) va_end(list) +#endif + +extern "C" { + +void APS5_VABI err_set_file_nid_postfix(FileStream* stream) { + g_errFile = stream != nullptr ? stream : &_Stderr_nid_postfix; +} + +void APS5_VABI err_set_exit_nid_postfix(ErrExit function) { + g_errExit = function; +} + +void APS5_VABI vwarnc_nid_postfix(int code, const char* format, VaList* args) { + Report(format, args, &code); +} + +void APS5_VABI vwarn_nid_postfix(const char* format, VaList* args) { + vwarnc_nid_postfix(*__error_nid_postfix(), format, args); +} + +void APS5_VABI vwarnx_nid_postfix(const char* format, VaList* args) { + Report(format, args, nullptr); +} + +[[noreturn]] void APS5_VABI verrc_nid_postfix(int status, int code, const char* format, VaList* args) { + Report(format, args, &code); + Exit(status); +} + +[[noreturn]] void APS5_VABI verr_nid_postfix(int status, const char* format, VaList* args) { + verrc_nid_postfix(status, *__error_nid_postfix(), format, args); +} + +[[noreturn]] void APS5_VABI verrx_nid_postfix(int status, const char* format, VaList* args) { + Report(format, args, nullptr); + Exit(status); +} + +void APS5_VABI warnc_nid_postfix(int code, const char* format, ...) { + APS5_ERR_VA_LIST args; + APS5_ERR_VA_START(args, format); + vwarnc_nid_postfix(code, format, reinterpret_cast(args)); + APS5_ERR_VA_END(args); +} + +void APS5_VABI warn_nid_postfix(const char* format, ...) { + const int code = *__error_nid_postfix(); + APS5_ERR_VA_LIST args; + APS5_ERR_VA_START(args, format); + vwarnc_nid_postfix(code, format, reinterpret_cast(args)); + APS5_ERR_VA_END(args); +} + +void APS5_VABI warnx_nid_postfix(const char* format, ...) { + APS5_ERR_VA_LIST args; + APS5_ERR_VA_START(args, format); + vwarnx_nid_postfix(format, reinterpret_cast(args)); + APS5_ERR_VA_END(args); +} + +[[noreturn]] void APS5_VABI errc_nid_postfix(int status, int code, const char* format, ...) { + APS5_ERR_VA_LIST args; + APS5_ERR_VA_START(args, format); + verrc_nid_postfix(status, code, format, reinterpret_cast(args)); +} + +[[noreturn]] void APS5_VABI err_nid_postfix(int status, const char* format, ...) { + const int code = *__error_nid_postfix(); + APS5_ERR_VA_LIST args; + APS5_ERR_VA_START(args, format); + verrc_nid_postfix(status, code, format, reinterpret_cast(args)); +} + +[[noreturn]] void APS5_VABI errx_nid_postfix(int status, const char* format, ...) { + APS5_ERR_VA_LIST args; + APS5_ERR_VA_START(args, format); + verrx_nid_postfix(status, format, reinterpret_cast(args)); +} + +} 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/FilesystemStatistics.cpp b/core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp index 083431cef6..8cb50f1eca 100644 --- a/core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp +++ b/core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp @@ -1,26 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" -#include "prx/libkernel/File/include/KernelStatfs.hpp" -#include -#include +#include "prx/libSceLibcInternal/include/Statvfs.hpp" #include -struct LibcStatvfs { - std::uint64_t f_bavail; - std::uint64_t f_bfree; - std::uint64_t f_blocks; - std::uint64_t f_favail; - std::uint64_t f_ffree; - std::uint64_t f_files; - std::uint64_t f_bsize; - std::uint64_t f_flag; - std::uint64_t f_frsize; - std::uint64_t f_fsid; - std::uint64_t f_namemax; -}; -static_assert(sizeof(LibcStatvfs) == 88); -static_assert(offsetof(LibcStatvfs, f_bsize) == 48); -static_assert(offsetof(LibcStatvfs, f_namemax) == 80); - extern "C" int APS5_VABI _fstatfs_nid_postfix(int, KernelStatfs*); extern "C" int* APS5_VABI __error_nid_postfix(); @@ -34,12 +15,7 @@ extern "C" int APS5_VABI fstatvfs_nid_postfix(int descriptor, LibcStatvfs* outpu *error = 14; return -1; } - const LibcStatvfs result{ - static_cast(volume.f_bavail), volume.f_bfree, volume.f_blocks, - static_cast(volume.f_ffree), static_cast(volume.f_ffree), volume.f_files, - volume.f_iosize, (volume.f_flags & 1) | ((volume.f_flags & 8) >> 2), volume.f_bsize, - 0, volume.f_namemax - }; + const LibcStatvfs result = StatvfsFromStatfs(volume); std::memcpy(output, &result, sizeof(result)); *error = saved; return 0; diff --git a/core/libs/prx/libSceLibcInternal/Getopt.cpp b/core/libs/prx/libSceLibcInternal/Getopt.cpp index 17062904ff..80645072b4 100644 --- a/core/libs/prx/libSceLibcInternal/Getopt.cpp +++ b/core/libs/prx/libSceLibcInternal/Getopt.cpp @@ -38,6 +38,7 @@ constexpr int OptionalArgument = 2; constexpr int FlagPermute = 1; constexpr int FlagAllArguments = 2; +constexpr int FlagLongOnly = 4; constexpr int BadCharacter = '?'; constexpr int InOrder = 1; @@ -112,7 +113,7 @@ void PermuteArguments(int nonOptionStart, int nonOptionEnd, int optionEnd, char* } } -int ParseLongOptions(char* const* arguments, const char* options, const GuestOption* longOptions, int* index, const char* dash) { +int ParseLongOptions(char* const* arguments, const char* options, const GuestOption* longOptions, int* index, const char* dash, bool shortToo, int flags) { char* const current = g_longPlace; int match = -1; bool exactMatch = false; @@ -136,8 +137,9 @@ int ParseLongOptions(char* const* arguments, const char* options, const GuestOpt exactMatch = true; break; } + if (shortToo && length == 1) continue; if (match == -1) match = i; - else if (longOptions[i].hasArgument != longOptions[match].hasArgument || + else if ((flags & FlagLongOnly) || longOptions[i].hasArgument != longOptions[match].hasArgument || longOptions[i].flag != longOptions[match].flag || longOptions[i].value != longOptions[match].value) secondPartialMatch = true; @@ -149,6 +151,10 @@ int ParseLongOptions(char* const* arguments, const char* options, const GuestOpt return BadCharacter; } if (match == -1) { + if (shortToo) { + --optind_nid_postfix; + return -1; + } if (PrintError(options)) Warn(std::string("unrecognized option `") + dash + current + "'"); optopt_nid_postfix = 0; return BadCharacter; @@ -237,11 +243,21 @@ int GetoptInternal(int count, char* const* arguments, const char* options, const break; } - if (longOptions && *g_longPlace == '-' && g_longPlace != arguments[optind_nid_postfix]) { - ++g_longPlace; - const int result = ParseLongOptions(arguments, options, longOptions, index, "--"); - g_longPlace = g_emptyMessage; - return result; + if (longOptions && g_longPlace != arguments[optind_nid_postfix] && + (*g_longPlace == '-' || (flags & FlagLongOnly))) { + bool shortToo = false; + const char* dash = "-"; + if (*g_longPlace == '-') { + ++g_longPlace; + dash = "--"; + } else if (*g_longPlace != ':' && std::strchr(options, *g_longPlace)) { + shortToo = true; + } + const int result = ParseLongOptions(arguments, options, longOptions, index, dash, shortToo, flags); + if (result != -1) { + g_longPlace = g_emptyMessage; + return result; + } } const int character = OptionCharacter(g_longPlace++); @@ -265,7 +281,7 @@ int GetoptInternal(int count, char* const* arguments, const char* options, const } g_longPlace = Argument(arguments, optind_nid_postfix, "getopt_long"); } - const int result = ParseLongOptions(arguments, options, longOptions, index, "-W "); + const int result = ParseLongOptions(arguments, options, longOptions, index, "-W ", false, flags); g_longPlace = g_emptyMessage; return result; } @@ -355,4 +371,8 @@ int APS5_VABI getopt_long_nid_postfix(int count, char* const* arguments, const c return GetoptInternal(count, arguments, options, longOptions, index, FlagPermute); } +int APS5_VABI getopt_long_only_nid_postfix(int count, char* const* arguments, const char* options, const GuestOption* longOptions, int* index) { + return GetoptInternal(count, arguments, options, longOptions, index, FlagPermute | FlagLongOnly); +} + } 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/PathStatistics.cpp b/core/libs/prx/libSceLibcInternal/PathStatistics.cpp index a621e90dd2..c66f3dcc3b 100644 --- a/core/libs/prx/libSceLibcInternal/PathStatistics.cpp +++ b/core/libs/prx/libSceLibcInternal/PathStatistics.cpp @@ -10,6 +10,12 @@ #else #include #endif +#ifdef __APPLE__ +#include "prx/libSceLibcInternal/include/Statvfs.hpp" +#include +#include +#include +#endif struct LibcStatvfs; extern "C" int APS5_VABI fstatvfs_nid_postfix(int, LibcStatvfs*); @@ -38,6 +44,32 @@ int HostError(int error) { } } +#ifdef __APPLE__ +int PathVolume(const std::filesystem::path& path, bool directory, LibcStatvfs* output) { + struct statvfs host{}; + if (::statvfs(path.c_str(), &host) != 0) return Fail(HostError(errno)); + if (directory) { + struct stat file{}; + if (::stat(path.c_str(), &file) != 0) return Fail(HostError(errno)); + if (!S_ISDIR(file.st_mode)) return Fail(20); + } + if (output == nullptr) return Fail(14); + KernelStatfs volume{}; + if (host.f_flag & ST_RDONLY) volume.f_flags |= 0x1; + if (host.f_flag & ST_NOSUID) volume.f_flags |= 0x8; + volume.f_bsize = host.f_frsize != 0 ? host.f_frsize : host.f_bsize; + volume.f_iosize = host.f_bsize; + volume.f_blocks = host.f_blocks; + volume.f_bfree = host.f_bfree; + volume.f_bavail = static_cast(host.f_bavail); + volume.f_files = host.f_files; + volume.f_ffree = static_cast(host.f_favail); + volume.f_namemax = static_cast(host.f_namemax); + const LibcStatvfs result = StatvfsFromStatfs(volume); + std::memcpy(output, &result, sizeof(result)); + return 0; +} +#else struct Descriptor { int value; ~Descriptor() { @@ -104,19 +136,26 @@ int OpenMetadata(const std::filesystem::path& path, bool directory) { return descriptor < 0 ? Fail(HostError(errno)) : descriptor; #endif } +#endif } extern "C" int APS5_VABI statvfs_nid_postfix(const char* path, LibcStatvfs* output) { - if (path == nullptr) return Fail(14); + if (path == nullptr || output == nullptr) return Fail(14); if (*path == '\0') return Fail(2); const int saved = errno; const auto last = std::string_view(path).back(); const bool directory = last == '/' || last == '\\' || FinalPathComponentIsDot_nid_no_patch(path); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); +#ifdef __APPLE__ + if (PathVolume(native, directory, output) != 0) return -1; + errno = saved; + return 0; +#else const int descriptor = OpenMetadata(native, directory); if (descriptor < 0) return -1; const Descriptor owner{descriptor}; errno = saved; return fstatvfs_nid_postfix(descriptor, output); +#endif } diff --git a/core/libs/prx/libSceLibcInternal/SignalList.cpp b/core/libs/prx/libSceLibcInternal/SignalList.cpp index c51fa70fe8..d27a5531c4 100644 --- a/core/libs/prx/libSceLibcInternal/SignalList.cpp +++ b/core/libs/prx/libSceLibcInternal/SignalList.cpp @@ -1,3 +1,8 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + extern "C" { extern const char* const sys_siglist_nid_postfix[32] = { @@ -37,4 +42,51 @@ extern const char* const sys_siglist_nid_postfix[32] = { extern const int sys_nsig_nid_postfix = sizeof(sys_siglist_nid_postfix) / sizeof(sys_siglist_nid_postfix[0]); +extern const char* const sys_signame_nid_postfix[32] = { + "Signal 0", + "HUP", + "INT", + "QUIT", + "ILL", + "TRAP", + "ABRT", + "EMT", + "FPE", + "KILL", + "BUS", + "SEGV", + "SYS", + "PIPE", + "ALRM", + "TERM", + "URG", + "STOP", + "TSTP", + "CONT", + "CHLD", + "TTIN", + "TTOU", + "IO", + "XCPU", + "XFSZ", + "VTALRM", + "PROF", + "WINCH", + "INFO", + "USR1", + "USR2", +}; + +std::int64_t APS5_VABI _write_nid_postfix(int descriptor, const void* buffer, std::size_t count); + +void APS5_VABI psignal_nid_postfix(unsigned int signal, const char* prefix) { + const char* const message = signal < 32 ? sys_siglist_nid_postfix[signal] : "Unknown signal"; + if (prefix != nullptr && prefix[0] != 0) { + _write_nid_postfix(2, prefix, std::strlen(prefix)); + _write_nid_postfix(2, ": ", 2); + } + _write_nid_postfix(2, message, std::strlen(message)); + _write_nid_postfix(2, "\n", 1); +} + } diff --git a/core/libs/prx/libSceLibcInternal/Spawn.cpp b/core/libs/prx/libSceLibcInternal/Spawn.cpp index c4571fb64e..b40cbc9d10 100644 --- a/core/libs/prx/libSceLibcInternal/Spawn.cpp +++ b/core/libs/prx/libSceLibcInternal/Spawn.cpp @@ -1,15 +1,23 @@ #include "prx/libSceLibcInternal/include/Spawn.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/General.hpp" #include +#include #include #include #include +#include +#include #include +extern "C" char* APS5_VABI getenv_nid_postfix(const char* name); + namespace { +constexpr int NoSuchFile = 2; constexpr int BadDescriptor = 9; constexpr int OutOfMemory = 12; +constexpr std::string_view DefaultSearchPath = "/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"; PosixSpawnAttributes& Attributes(PosixSpawnAttributes* const* attributes, const char* function) { if (attributes == nullptr || *attributes == nullptr) throw std::invalid_argument(std::string(function) + ": uninitialized attributes"); @@ -21,6 +29,36 @@ PosixSpawnFileActions& FileActions(PosixSpawnFileActions* const* actions, const return **actions; } +bool GuestPathExists(const std::string& path) { + std::filesystem::path native; + if (ResolvePathError_nid_no_patch(path.c_str(), native) != 0) return false; + std::error_code error; + return std::filesystem::exists(native, error); +} + +bool ProgramFound(std::string_view name, bool searchesPath) { + if (name.empty()) return false; + if (!searchesPath || name.find('/') != std::string_view::npos) return GuestPathExists(std::string(name)); + const char* variable = getenv_nid_postfix("PATH"); + std::string_view directories = variable != nullptr ? std::string_view(variable) : DefaultSearchPath; + while (true) { + const auto separator = directories.find(':'); + const auto directory = directories.substr(0, separator); + if (GuestPathExists((directory.empty() ? std::string(".") : std::string(directory)) + '/' + std::string(name))) return true; + if (separator == std::string_view::npos) return false; + directories.remove_prefix(separator + 1); + } +} + +int Spawn(const char* function, const char* file, bool searchesPath, PosixSpawnFileActions* const* actions, PosixSpawnAttributes* const* attributes) { + if (file == nullptr) throw std::invalid_argument(std::string(function) + ": null path"); + if (actions != nullptr) FileActions(actions, function); + if (attributes != nullptr) Attributes(attributes, function); + if (!ProgramFound(file, searchesPath)) return NoSuchFile; + NotImplemented_nid_no_patch((std::string(function) + ": process creation").c_str()); + return -1; +} + int Append(PosixSpawnFileActions& actions, PosixSpawnFileAction action) { try { actions.actions.push_back(std::move(action)); @@ -60,6 +98,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{}; @@ -96,24 +200,16 @@ int APS5_VABI posix_spawn_file_actions_addclose_nid_postfix(PosixSpawnFileAction int APS5_VABI posix_spawn_nid_postfix(std::int32_t* processId, const char* path, PosixSpawnFileActions* const* actions, PosixSpawnAttributes* const* attributes, char* const* arguments, char* const* environment) { (void)processId; - (void)path; - (void)actions; - (void)attributes; (void)arguments; (void)environment; - NotImplemented_nid_no_patch("posix_spawn: process creation"); - return -1; + return Spawn("posix_spawn", path, false, actions, attributes); } int APS5_VABI posix_spawnp_nid_postfix(std::int32_t* processId, const char* file, PosixSpawnFileActions* const* actions, PosixSpawnAttributes* const* attributes, char* const* arguments, char* const* environment) { (void)processId; - (void)file; - (void)actions; - (void)attributes; (void)arguments; (void)environment; - NotImplemented_nid_no_patch("posix_spawnp: process creation"); - return -1; + return Spawn("posix_spawnp", file, true, actions, attributes); } } diff --git a/core/libs/prx/libSceLibcInternal/UserContext.cpp b/core/libs/prx/libSceLibcInternal/UserContext.cpp index f5f6c7eae8..3f743af8f4 100644 --- a/core/libs/prx/libSceLibcInternal/UserContext.cpp +++ b/core/libs/prx/libSceLibcInternal/UserContext.cpp @@ -20,12 +20,51 @@ constexpr std::uint64_t MinimumStackSize = 2048; extern "C" { +#if defined(_WIN32) && defined(__SEH__) +[[noreturn]] void APS5_VABI Aps5MakecontextStart_nid_no_patch(UserUcontext* context, ContextFunction function, const std::uint64_t* arguments); + +[[noreturn]] void APS5_VABI Aps5MakecontextFinish_nid_no_patch(UserUcontext* context) { + if (context->link == nullptr) LibcExit_nid_no_patch(0); + setcontext_nid_postfix(context->link); + std::abort(); +} + +asm(R"( +.text +.globl Aps5MakecontextStart_nid_no_patch +.def Aps5MakecontextStart_nid_no_patch; .scl 2; .type 32; .endef +.seh_proc Aps5MakecontextStart_nid_no_patch +Aps5MakecontextStart_nid_no_patch: + pushq %rbp + .seh_pushreg %rbp + subq $48, %rsp + .seh_stackalloc 48 + leaq 48(%rsp), %rbp + .seh_setframe %rbp, 48 + .seh_endprologue + movq %rdi, -8(%rbp) + movq %rsi, %rax + movq %rdx, %r10 + movq (%r10), %rdi + movq 8(%r10), %rsi + movq 16(%r10), %rdx + movq 24(%r10), %rcx + movq 32(%r10), %r8 + movq 40(%r10), %r9 + call *%rax + movq -8(%rbp), %rdi + call Aps5MakecontextFinish_nid_no_patch + ud2 +.seh_endproc +)"); +#else [[noreturn]] void APS5_VABI Aps5MakecontextStart_nid_no_patch(UserUcontext* context, ContextFunction function, const std::uint64_t* arguments) { function(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]); if (context->link == nullptr) LibcExit_nid_no_patch(0); setcontext_nid_postfix(context->link); std::abort(); } +#endif void APS5_VABI makecontext_nid_postfix(UserUcontext* context, void (APS5_VABI* start)(), int count, ...) { if (context == nullptr || context->mcontext.len != sizeof(UserMcontext)) return; diff --git a/core/libs/prx/libSceLibcInternal/include/Statvfs.hpp b/core/libs/prx/libSceLibcInternal/include/Statvfs.hpp new file mode 100644 index 0000000000..4d6b238770 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/include/Statvfs.hpp @@ -0,0 +1,34 @@ +#ifndef CORE_LIBS_PRX_LIBSCELIBCINTERNAL_INCLUDE_STATVFS_HPP +#define CORE_LIBS_PRX_LIBSCELIBCINTERNAL_INCLUDE_STATVFS_HPP + +#include "prx/libkernel/File/include/KernelStatfs.hpp" +#include +#include + +struct LibcStatvfs { + std::uint64_t f_bavail; + std::uint64_t f_bfree; + std::uint64_t f_blocks; + std::uint64_t f_favail; + std::uint64_t f_ffree; + std::uint64_t f_files; + std::uint64_t f_bsize; + std::uint64_t f_flag; + std::uint64_t f_frsize; + std::uint64_t f_fsid; + std::uint64_t f_namemax; +}; +static_assert(sizeof(LibcStatvfs) == 88); +static_assert(offsetof(LibcStatvfs, f_bsize) == 48); +static_assert(offsetof(LibcStatvfs, f_namemax) == 80); + +inline LibcStatvfs StatvfsFromStatfs(const KernelStatfs& volume) { + return LibcStatvfs{ + static_cast(volume.f_bavail), volume.f_bfree, volume.f_blocks, + static_cast(volume.f_ffree), static_cast(volume.f_ffree), volume.f_files, + volume.f_iosize, (volume.f_flags & 1) | ((volume.f_flags & 8) >> 2), volume.f_bsize, + 0, volume.f_namemax + }; +} + +#endif diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt new file mode 100644 index 0000000000..d9d266270c --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp) +target_include_directories(libSceLibreSsl PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceLibreSsl) + +if(BUILD_TESTING) + add_test_executable(guest_libressl_digest_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslDigest.cpp) + target_include_directories(guest_libressl_digest_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_digest_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_digest_tests) + add_test(NAME guest_libressl_digest COMMAND guest_libressl_digest_tests) + set_tests_properties(guest_libressl_digest PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_digest PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp new file mode 100644 index 0000000000..670bb8911e --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -0,0 +1,1894 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN +int APS5_VABI AES_cbc_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AES_cfb128_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AES_ctr128_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AES_decrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AES_ecb_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AES_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AES_set_decrypt_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AES_set_encrypt_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_INTEGER_get_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_STRING_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_STRING_length_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_STRING_print_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_STRING_type_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_TIME_print_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_ctrl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_f_ssl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_new_fp_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_new_mem_buf_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_new_socket_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_printf_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_puts_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_s_file_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_s_mem_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_write_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_CTX_end_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_CTX_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_CTX_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_CTX_start_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_add_word_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_bin2bn_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_bn2bin_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_hex2bn_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_mod_inverse_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_mul_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_num_bits_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_print_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_set_word_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_sub_word_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CMAC_CTX_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CMAC_CTX_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CMAC_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CMAC_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CMAC_Update_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CONF_modules_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CONF_modules_load_file_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_THREADID_set_callback_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_THREADID_set_numeric_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_cleanup_all_ex_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_malloc_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_num_locks_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_set_add_lock_callback_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_set_locking_callback_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_set_mem_functions_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI DES_ecb_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI DES_set_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI DES_set_odd_parity_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI DTLSv1_client_method_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI DTLSv1_server_method_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ECDH_compute_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ECDSA_sign_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ECDSA_size_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ECDSA_verify_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_GROUP_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_GROUP_get_degree_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_GROUP_new_by_curve_name_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_check_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_generate_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_get0_group_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_get0_private_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_get0_public_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_new_by_curve_name_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_set_group_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_set_private_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_set_public_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_POINT_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_POINT_mul_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_POINT_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_POINT_oct2point_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_POINT_point2oct_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_by_id_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_cleanup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_ctrl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_ctrl_cmd_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_finish_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_get_first_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_get_id_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_get_next_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_load_builtin_engines_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_load_private_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_set_default_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_clear_error_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_error_string_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_error_string_n_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_free_strings_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_get_error_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_peek_error_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_remove_thread_state_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CIPHER_CTX_cleanup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CIPHER_CTX_ctrl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CIPHER_CTX_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CIPHER_CTX_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CIPHER_CTX_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CIPHER_CTX_set_padding_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CIPHER_block_size_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CipherFinal_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CipherInit_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CipherUpdate_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DecryptFinal_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DecryptInit_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DecryptUpdate_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_Digest_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DigestFinal_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DigestInit_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DigestUpdate_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DigestVerifyFinal_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DigestVerifyInit_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_EncryptFinal_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_EncryptInit_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_EncryptUpdate_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_MD_CTX_cleanup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_MD_CTX_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_MD_CTX_destroy_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_MD_CTX_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_MD_size_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_copy_parameters_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_get1_EC_KEY_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_get1_RSA_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_id_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_set1_RSA_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_type_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_128_cbc_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_128_cfb8_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_128_ctr_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_128_gcm_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_192_cbc_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_192_cfb8_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_192_ctr_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_192_gcm_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_256_cbc_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_256_cfb8_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_256_ctr_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_256_gcm_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_cleanup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_md5_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_sha256_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_sha384_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_sha512_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI GENERAL_NAMES_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI GENERAL_NAME_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI HMAC_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI HMAC_CTX_cleanup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI HMAC_CTX_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI HMAC_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI HMAC_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI HMAC_Init_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI HMAC_Update_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OBJ_cleanup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OBJ_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OBJ_nid2sn_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OBJ_obj2nid_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OBJ_sn2nid_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_BASICRESP_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_RESPONSE_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_basic_add1_cert_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_basic_verify_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_cert_status_str_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_check_validity_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_crl_reason_str_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_resp_count_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_resp_get0_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_response_get1_basic_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_response_status_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_response_status_str_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_single_get0_status_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OPENSSL_add_all_algorithms_noconf_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OPENSSL_load_builtin_modules_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PEM_X509_INFO_read_bio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PEM_read_RSAPrivateKey_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PEM_read_X509_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PEM_read_bio_PrivateKey_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PEM_read_bio_X509_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PEM_read_bio_X509_AUX_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PEM_write_bio_X509_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PKCS12_PBE_add_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PKCS12_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PKCS12_parse_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RAND_add_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RAND_bytes_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RAND_file_name_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RAND_load_file_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RAND_status_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_flags_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_generate_key_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_padding_add_PKCS1_PSS_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_private_decrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_private_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_public_decrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_public_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_sign_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_size_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_verify_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_verify_PKCS1_PSS_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CIPHER_get_bits_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CIPHER_get_name_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CIPHER_get_version_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_add_client_CA_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_callback_ctrl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_check_private_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_ctrl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_get_cert_store_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_get_ex_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_get_ex_new_index_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_get_verify_mode_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_load_verify_locations_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_load_verify_mem_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_sess_set_new_cb_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set1_groups_list_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_alpn_protos_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_alpn_select_cb_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_cert_store_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_cert_verify_callback_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_cipher_list_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_client_cert_cb_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_default_passwd_cb_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_default_passwd_cb_userdata_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_ex_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_info_callback_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_msg_callback_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_next_proto_select_cb_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_quiet_shutdown_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_session_id_context_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_verify_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_use_PrivateKey_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_use_PrivateKey_ASN1_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_use_PrivateKey_file_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_use_certificate_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_use_certificate_ASN1_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_use_certificate_chain_file_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_use_certificate_file_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_SESSION_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_accept_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_alert_desc_string_long_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_check_private_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_connect_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_ctrl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get0_alpn_selected_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get0_param_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get1_session_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_SSL_CTX_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_certificate_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_current_cipher_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_error_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_ex_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_ex_data_X509_STORE_CTX_idx_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_ex_new_index_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_fd_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_peer_cert_chain_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_peer_certificate_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_privatekey_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_rbio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_servername_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_session_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_shutdown_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_verify_result_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_wbio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_library_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_load_error_strings_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_peek_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_pending_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_read_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_select_next_proto_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_set_SSL_CTX_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_set_bio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_set_connect_state_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_set_ex_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_set_fd_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_set_session_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_set_verify_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_shutdown_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_use_PrivateKey_ASN1_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_use_certificate_ASN1_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_version_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_want_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_write_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSLeay_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSLv23_client_method_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSLv23_server_method_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI TLS_client_method_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_OpenSSL_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_create_method_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_destroy_method_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_get0_user_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_get_input_flags_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_get_string_type_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_method_get_closer_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_method_get_opener_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_method_get_reader_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_method_get_writer_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_method_set_closer_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_method_set_opener_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_method_set_reader_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_method_set_writer_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_set_result_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509V3_EXT_print_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_EXTENSION_get_critical_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_EXTENSION_get_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_EXTENSION_get_object_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_INFO_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_LOOKUP_file_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_NAME_ENTRY_get_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_NAME_get_entry_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_NAME_get_index_by_NID_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_NAME_get_text_by_NID_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_NAME_oneline_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_NAME_print_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_get1_chain_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_get_chain_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_get_current_cert_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_get_error_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_get_error_depth_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_get_ex_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_set_error_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_add_cert_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_add_crl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_add_lookup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_set_flags_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_VERIFY_PARAM_set1_host_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_VERIFY_PARAM_set1_ip_asc_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_VERIFY_PARAM_set_hostflags_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_check_issued_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get_ext_d2i_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get_issuer_name_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get_pubkey_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get_serialNumber_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get_subject_name_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_load_crl_file_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_print_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_verify_cert_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_verify_cert_error_string_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_OCSP_RESPONSE_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_PKCS12_bio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_PKCS12_fp_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_RSA_PUBKEY_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_X509_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI i2a_ASN1_OBJECT_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI i2d_X509_PUBKEY_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI i2t_ASN1_OBJECT_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLibreSslGetMemoryPoolStats(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLibreSslInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLibreSslInitEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLibreSslInitEx2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLibreSslTerm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sk_num_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sk_pop_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sk_pop_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sk_value_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceLibreSsl/src/Digest.cpp b/core/libs/prx/libSceLibreSsl/src/Digest.cpp new file mode 100644 index 0000000000..e58974597e --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Digest.cpp @@ -0,0 +1,549 @@ +#include +#include +#include +#include +#include +#include +#include "prx/libc/include/General.hpp" + +namespace { +struct Md4Context { + std::uint32_t A, B, C, D; + std::uint32_t Nl, Nh; + std::uint32_t data[16]; + std::uint32_t num; +}; + +struct Md5Context { + std::uint32_t A, B, C, D; + std::uint32_t Nl, Nh; + std::uint32_t data[16]; + std::uint32_t num; +}; + +struct Sha1Context { + std::uint32_t h[5]; + std::uint32_t Nl, Nh; + std::uint32_t data[16]; + std::uint32_t num; +}; + +struct Sha256Context { + std::uint32_t h[8]; + std::uint32_t Nl, Nh; + std::uint32_t data[16]; + std::uint32_t num, mdLength; +}; + +struct Sha512Context { + std::uint64_t h[8]; + std::uint64_t Nl, Nh; + std::uint64_t data[16]; + std::uint32_t num, mdLength; +}; + +static_assert(sizeof(Md4Context) == 92); +static_assert(sizeof(Md5Context) == 92); +static_assert(sizeof(Sha1Context) == 96); +static_assert(sizeof(Sha256Context) == 112); +static_assert(sizeof(Sha512Context) == 216); + +constexpr std::size_t Sha224Length = 28; +constexpr std::size_t Sha256Length = 32; +constexpr std::size_t Sha384Length = 48; +constexpr std::size_t Sha512Length = 64; + +std::uint32_t LoadLittle32(const unsigned char* p) { + return std::uint32_t(p[0]) | std::uint32_t(p[1]) << 8 | std::uint32_t(p[2]) << 16 | std::uint32_t(p[3]) << 24; +} + +std::uint32_t LoadBig32(const unsigned char* p) { + return std::uint32_t(p[0]) << 24 | std::uint32_t(p[1]) << 16 | std::uint32_t(p[2]) << 8 | std::uint32_t(p[3]); +} + +std::uint64_t LoadBig64(const unsigned char* p) { + return std::uint64_t(LoadBig32(p)) << 32 | LoadBig32(p + 4); +} + +void StoreLittle32(unsigned char* p, std::uint32_t value) { + for (int i = 0; i < 4; ++i) p[i] = static_cast(value >> (8 * i)); +} + +void StoreBig32(unsigned char* p, std::uint32_t value) { + for (int i = 0; i < 4; ++i) p[i] = static_cast(value >> (24 - 8 * i)); +} + +void StoreBig64(unsigned char* p, std::uint64_t value) { + StoreBig32(p, static_cast(value >> 32)); + StoreBig32(p + 4, static_cast(value)); +} + +void Md4Block(Md4Context& c, const unsigned char* block) { + std::uint32_t x[16]; + for (int i = 0; i < 16; ++i) x[i] = LoadLittle32(block + 4 * i); + std::uint32_t a = c.A, b = c.B, cc = c.C, d = c.D; + const auto f = [](std::uint32_t x1, std::uint32_t y, std::uint32_t z) { return (x1 & y) | (~x1 & z); }; + const auto g = [](std::uint32_t x1, std::uint32_t y, std::uint32_t z) { return (x1 & y) | (x1 & z) | (y & z); }; + const auto h = [](std::uint32_t x1, std::uint32_t y, std::uint32_t z) { return x1 ^ y ^ z; }; + static constexpr int shifts1[4] {3, 7, 11, 19}; + static constexpr int shifts2[4] {3, 5, 9, 13}; + static constexpr int shifts3[4] {3, 9, 11, 15}; + static constexpr int order2[16] {0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15}; + static constexpr int order3[16] {0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15}; + for (int i = 0; i < 16; ++i) { + const auto t = std::rotl(a + f(b, cc, d) + x[i], shifts1[i % 4]); + a = d; d = cc; cc = b; b = t; + } + for (int i = 0; i < 16; ++i) { + const auto t = std::rotl(a + g(b, cc, d) + x[order2[i]] + 0x5a827999u, shifts2[i % 4]); + a = d; d = cc; cc = b; b = t; + } + for (int i = 0; i < 16; ++i) { + const auto t = std::rotl(a + h(b, cc, d) + x[order3[i]] + 0x6ed9eba1u, shifts3[i % 4]); + a = d; d = cc; cc = b; b = t; + } + c.A += a; c.B += b; c.C += cc; c.D += d; +} + +void Md5Block(Md5Context& c, const unsigned char* block) { + static constexpr std::uint32_t constants[64] { + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, + }; + static constexpr int shifts[16] {7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21}; + std::uint32_t x[16]; + for (int i = 0; i < 16; ++i) x[i] = LoadLittle32(block + 4 * i); + std::uint32_t a = c.A, b = c.B, cc = c.C, d = c.D; + for (int i = 0; i < 64; ++i) { + std::uint32_t mix; + int index; + switch (i / 16) { + case 0: mix = (b & cc) | (~b & d); index = i; break; + case 1: mix = (d & b) | (~d & cc); index = (5 * i + 1) % 16; break; + case 2: mix = b ^ cc ^ d; index = (3 * i + 5) % 16; break; + default: mix = cc ^ (b | ~d); index = (7 * i) % 16; break; + } + const auto t = d; + d = cc; + cc = b; + b += std::rotl(a + mix + constants[i] + x[index], shifts[(i / 16) * 4 + i % 4]); + a = t; + } + c.A += a; c.B += b; c.C += cc; c.D += d; +} + +void Sha1Block(Sha1Context& c, const unsigned char* block) { + std::uint32_t w[80]; + for (int i = 0; i < 16; ++i) w[i] = LoadBig32(block + 4 * i); + for (int i = 16; i < 80; ++i) w[i] = std::rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); + std::uint32_t a = c.h[0], b = c.h[1], cc = c.h[2], d = c.h[3], e = c.h[4]; + for (int i = 0; i < 80; ++i) { + std::uint32_t f, k; + if (i < 20) { f = (b & cc) | (~b & d); k = 0x5a827999; } + else if (i < 40) { f = b ^ cc ^ d; k = 0x6ed9eba1; } + else if (i < 60) { f = (b & cc) | (b & d) | (cc & d); k = 0x8f1bbcdc; } + else { f = b ^ cc ^ d; k = 0xca62c1d6; } + const auto t = std::rotl(a, 5) + f + e + k + w[i]; + e = d; d = cc; cc = std::rotl(b, 30); b = a; a = t; + } + c.h[0] += a; c.h[1] += b; c.h[2] += cc; c.h[3] += d; c.h[4] += e; +} + +void Sha256Block(Sha256Context& c, const unsigned char* block) { + static constexpr std::uint32_t constants[64] { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, + }; + std::uint32_t w[64]; + for (int i = 0; i < 16; ++i) w[i] = LoadBig32(block + 4 * i); + for (int i = 16; i < 64; ++i) { + const auto s0 = std::rotr(w[i - 15], 7) ^ std::rotr(w[i - 15], 18) ^ (w[i - 15] >> 3); + const auto s1 = std::rotr(w[i - 2], 17) ^ std::rotr(w[i - 2], 19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + std::uint32_t v[8]; + std::memcpy(v, c.h, sizeof(v)); + for (int i = 0; i < 64; ++i) { + const auto s1 = std::rotr(v[4], 6) ^ std::rotr(v[4], 11) ^ std::rotr(v[4], 25); + const auto choice = (v[4] & v[5]) ^ (~v[4] & v[6]); + const auto t1 = v[7] + s1 + choice + constants[i] + w[i]; + const auto s0 = std::rotr(v[0], 2) ^ std::rotr(v[0], 13) ^ std::rotr(v[0], 22); + const auto majority = (v[0] & v[1]) ^ (v[0] & v[2]) ^ (v[1] & v[2]); + std::memmove(v + 1, v, 7 * sizeof(v[0])); + v[4] += t1; + v[0] = t1 + s0 + majority; + } + for (int i = 0; i < 8; ++i) c.h[i] += v[i]; +} + +void Sha512Block(Sha512Context& c, const unsigned char* block) { + static constexpr std::uint64_t constants[80] { + 0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, 0x3956c25bf348b538, + 0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, 0xd807aa98a3030242, 0x12835b0145706fbe, + 0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235, + 0xc19bf174cf692694, 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65, + 0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, 0x983e5152ee66dfab, + 0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, 0xc6e00bf33da88fc2, 0xd5a79147930aa725, + 0x06ca6351e003826f, 0x142929670a0e6e70, 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed, + 0x53380d139d95b3df, 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b, + 0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, 0xd192e819d6ef5218, + 0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, 0x19a4c116b8d2d0c8, 0x1e376c085141ab53, + 0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373, + 0x682e6ff3d6b2b8a3, 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec, + 0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, 0xca273eceea26619c, + 0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, 0x06f067aa72176fba, 0x0a637dc5a2c898a6, + 0x113f9804bef90dae, 0x1b710b35131c471b, 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc, + 0x431d67c49c100d4c, 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817, + }; + std::uint64_t w[80]; + for (int i = 0; i < 16; ++i) w[i] = LoadBig64(block + 8 * i); + for (int i = 16; i < 80; ++i) { + const auto s0 = std::rotr(w[i - 15], 1) ^ std::rotr(w[i - 15], 8) ^ (w[i - 15] >> 7); + const auto s1 = std::rotr(w[i - 2], 19) ^ std::rotr(w[i - 2], 61) ^ (w[i - 2] >> 6); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + std::uint64_t v[8]; + std::memcpy(v, c.h, sizeof(v)); + for (int i = 0; i < 80; ++i) { + const auto s1 = std::rotr(v[4], 14) ^ std::rotr(v[4], 18) ^ std::rotr(v[4], 41); + const auto choice = (v[4] & v[5]) ^ (~v[4] & v[6]); + const auto t1 = v[7] + s1 + choice + constants[i] + w[i]; + const auto s0 = std::rotr(v[0], 28) ^ std::rotr(v[0], 34) ^ std::rotr(v[0], 39); + const auto majority = (v[0] & v[1]) ^ (v[0] & v[2]) ^ (v[1] & v[2]); + std::memmove(v + 1, v, 7 * sizeof(v[0])); + v[4] += t1; + v[0] = t1 + s0 + majority; + } + for (int i = 0; i < 8; ++i) c.h[i] += v[i]; +} + +template TContext& RequireContext(TContext* context, const char* function) { + if (!context) throw std::invalid_argument(std::string(function) + ": null context"); + return *context; +} + +void RequireInput(const void* data, std::size_t length, const char* function) { + if (!data && length) throw std::invalid_argument(std::string(function) + ": null input"); +} + +void RequireDigest(const unsigned char* digest, const char* function) { + if (!digest) throw std::invalid_argument(std::string(function) + ": null digest buffer"); +} + +void RequireBuffered(std::uint32_t buffered, std::size_t block, const char* function) { + if (buffered >= block) throw std::invalid_argument(std::string(function) + ": buffered byte count is not below the block size"); +} + +template void Update32(TContext& c, const void* input, std::size_t length, TBlock block) { + constexpr std::size_t blockSize = 64; + auto* buffer = reinterpret_cast(c.data); + auto* data = static_cast(input); + const std::uint64_t bits = (std::uint64_t(c.Nh) << 32 | c.Nl) + (std::uint64_t(length) << 3); + c.Nl = static_cast(bits); + c.Nh = static_cast(bits >> 32); + if (c.num) { + const std::size_t room = blockSize - c.num; + if (length < room) { + std::memcpy(buffer + c.num, data, length); + c.num += static_cast(length); + return; + } + std::memcpy(buffer + c.num, data, room); + block(c, buffer); + data += room; + length -= room; + c.num = 0; + std::memset(buffer, 0, blockSize); + } + for (; length >= blockSize; data += blockSize, length -= blockSize) block(c, data); + if (length) { + std::memcpy(buffer, data, length); + c.num = static_cast(length); + } +} + +template void Final32(TContext& c, bool bigEndian, TBlock block) { + constexpr std::size_t blockSize = 64; + auto* buffer = reinterpret_cast(c.data); + std::size_t n = c.num; + buffer[n++] = 0x80; + if (n > blockSize - 8) { + std::memset(buffer + n, 0, blockSize - n); + n = 0; + block(c, buffer); + } + std::memset(buffer + n, 0, blockSize - 8 - n); + if (bigEndian) { + StoreBig32(buffer + 56, c.Nh); + StoreBig32(buffer + 60, c.Nl); + } else { + StoreLittle32(buffer + 56, c.Nl); + StoreLittle32(buffer + 60, c.Nh); + } + block(c, buffer); + c.num = 0; + std::memset(buffer, 0, blockSize); +} + +void Sha512Update(Sha512Context& c, const void* input, std::size_t length) { + constexpr std::size_t blockSize = 128; + auto* buffer = reinterpret_cast(c.data); + auto* data = static_cast(input); + const std::uint64_t low = c.Nl + (std::uint64_t(length) << 3); + if (low < c.Nl) ++c.Nh; + c.Nh += std::uint64_t(length) >> 61; + c.Nl = low; + if (c.num) { + const std::size_t room = blockSize - c.num; + if (length < room) { + std::memcpy(buffer + c.num, data, length); + c.num += static_cast(length); + return; + } + std::memcpy(buffer + c.num, data, room); + Sha512Block(c, buffer); + data += room; + length -= room; + c.num = 0; + } + for (; length >= blockSize; data += blockSize, length -= blockSize) Sha512Block(c, data); + if (length) { + std::memcpy(buffer, data, length); + c.num = static_cast(length); + } +} + +int Sha512Final(unsigned char* digest, Sha512Context& c) { + constexpr std::size_t blockSize = 128; + auto* buffer = reinterpret_cast(c.data); + std::size_t n = c.num; + buffer[n++] = 0x80; + if (n > blockSize - 16) { + std::memset(buffer + n, 0, blockSize - n); + n = 0; + Sha512Block(c, buffer); + } + std::memset(buffer + n, 0, blockSize - 16 - n); + StoreBig64(buffer + 112, c.Nh); + StoreBig64(buffer + 120, c.Nl); + Sha512Block(c, buffer); + if (!digest) return 0; + if (c.mdLength == Sha224Length || c.mdLength == Sha256Length) + NotImplemented_nid_no_patch("SHA512_Final: SHA-512/224 and SHA-512/256 digest lengths"); + if (c.mdLength != Sha384Length && c.mdLength != Sha512Length) return 0; + for (std::size_t i = 0; i < c.mdLength / 8; ++i) StoreBig64(digest + 8 * i, c.h[i]); + return 1; +} + +int Sha256Final(unsigned char* digest, Sha256Context& c, const char* function) { + RequireDigest(digest, function); + RequireBuffered(c.num, 64, function); + Final32(c, true, Sha256Block); + if (c.mdLength > Sha256Length) return 0; + for (std::size_t i = 0; i < c.mdLength / 4; ++i) StoreBig32(digest + 4 * i, c.h[i]); + return 1; +} +} + +extern "C" { + +int APS5_VABI MD4_Init_nid_postfix(Md4Context* context) { + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + c.A = 0x67452301; + c.B = 0xefcdab89; + c.C = 0x98badcfe; + c.D = 0x10325476; + return 1; +} + +int APS5_VABI MD4_Update_nid_postfix(Md4Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 64, __func__); + Update32(c, data, length, Md4Block); + return 1; +} + +int APS5_VABI MD4_Final_nid_postfix(unsigned char* digest, Md4Context* context) { + auto& c = RequireContext(context, __func__); + RequireDigest(digest, __func__); + RequireBuffered(c.num, 64, __func__); + Final32(c, false, Md4Block); + StoreLittle32(digest, c.A); + StoreLittle32(digest + 4, c.B); + StoreLittle32(digest + 8, c.C); + StoreLittle32(digest + 12, c.D); + return 1; +} + +int APS5_VABI MD5_Init_nid_postfix(Md5Context* context) { + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + c.A = 0x67452301; + c.B = 0xefcdab89; + c.C = 0x98badcfe; + c.D = 0x10325476; + return 1; +} + +int APS5_VABI MD5_Update_nid_postfix(Md5Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 64, __func__); + Update32(c, data, length, Md5Block); + return 1; +} + +int APS5_VABI MD5_Final_nid_postfix(unsigned char* digest, Md5Context* context) { + auto& c = RequireContext(context, __func__); + RequireDigest(digest, __func__); + RequireBuffered(c.num, 64, __func__); + Final32(c, false, Md5Block); + StoreLittle32(digest, c.A); + StoreLittle32(digest + 4, c.B); + StoreLittle32(digest + 8, c.C); + StoreLittle32(digest + 12, c.D); + return 1; +} + +int APS5_VABI SHA1_Init_nid_postfix(Sha1Context* context) { + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + c.h[0] = 0x67452301; + c.h[1] = 0xefcdab89; + c.h[2] = 0x98badcfe; + c.h[3] = 0x10325476; + c.h[4] = 0xc3d2e1f0; + return 1; +} + +int APS5_VABI SHA1_Update_nid_postfix(Sha1Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 64, __func__); + Update32(c, data, length, Sha1Block); + return 1; +} + +int APS5_VABI SHA1_Final_nid_postfix(unsigned char* digest, Sha1Context* context) { + auto& c = RequireContext(context, __func__); + RequireDigest(digest, __func__); + RequireBuffered(c.num, 64, __func__); + Final32(c, true, Sha1Block); + for (int i = 0; i < 5; ++i) StoreBig32(digest + 4 * i, c.h[i]); + return 1; +} + +int APS5_VABI SHA224_Init_nid_postfix(Sha256Context* context) { + static constexpr std::uint32_t initial[8] {0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939, 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4}; + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + std::memcpy(c.h, initial, sizeof(initial)); + c.mdLength = Sha224Length; + return 1; +} + +int APS5_VABI SHA256_Init_nid_postfix(Sha256Context* context) { + static constexpr std::uint32_t initial[8] {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + std::memcpy(c.h, initial, sizeof(initial)); + c.mdLength = Sha256Length; + return 1; +} + +int APS5_VABI SHA256_Update_nid_postfix(Sha256Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 64, __func__); + Update32(c, data, length, Sha256Block); + return 1; +} + +int APS5_VABI SHA224_Update_nid_postfix(Sha256Context* context, const void* data, std::size_t length) { + return SHA256_Update_nid_postfix(context, data, length); +} + +int APS5_VABI SHA256_Final_nid_postfix(unsigned char* digest, Sha256Context* context) { + return Sha256Final(digest, RequireContext(context, __func__), __func__); +} + +int APS5_VABI SHA224_Final_nid_postfix(unsigned char* digest, Sha256Context* context) { + return Sha256Final(digest, RequireContext(context, __func__), __func__); +} + +unsigned char* APS5_VABI SHA256_nid_postfix(const unsigned char* data, std::size_t length, unsigned char* digest) { + RequireInput(data, length, __func__); + if (!digest) NotImplemented_nid_no_patch("SHA256: null digest buffer"); + Sha256Context c; + SHA256_Init_nid_postfix(&c); + if (length) Update32(c, data, length, Sha256Block); + Sha256Final(digest, c, __func__); + return digest; +} + +int APS5_VABI SHA384_Init_nid_postfix(Sha512Context* context) { + static constexpr std::uint64_t initial[8] { + 0xcbbb9d5dc1059ed8, 0x629a292a367cd507, 0x9159015a3070dd17, 0x152fecd8f70e5939, + 0x67332667ffc00b31, 0x8eb44a8768581511, 0xdb0c2e0d64f98fa7, 0x47b5481dbefa4fa4, + }; + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + std::memcpy(c.h, initial, sizeof(initial)); + c.mdLength = Sha384Length; + return 1; +} + +int APS5_VABI SHA512_Init_nid_postfix(Sha512Context* context) { + static constexpr std::uint64_t initial[8] { + 0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179, + }; + auto& c = RequireContext(context, __func__); + std::memset(&c, 0, sizeof(c)); + std::memcpy(c.h, initial, sizeof(initial)); + c.mdLength = Sha512Length; + return 1; +} + +int APS5_VABI SHA512_Update_nid_postfix(Sha512Context* context, const void* data, std::size_t length) { + auto& c = RequireContext(context, __func__); + if (!length) return 1; + RequireInput(data, length, __func__); + RequireBuffered(c.num, 128, __func__); + Sha512Update(c, data, length); + return 1; +} + +int APS5_VABI SHA384_Update_nid_postfix(Sha512Context* context, const void* data, std::size_t length) { + return SHA512_Update_nid_postfix(context, data, length); +} + +int APS5_VABI SHA512_Final_nid_postfix(unsigned char* digest, Sha512Context* context) { + auto& c = RequireContext(context, __func__); + RequireBuffered(c.num, 128, __func__); + return Sha512Final(digest, c); +} + +int APS5_VABI SHA384_Final_nid_postfix(unsigned char* digest, Sha512Context* context) { + return SHA512_Final_nid_postfix(digest, context); +} + +} diff --git a/core/libs/prx/libSceLibreSsl3/CMakeLists.txt b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt new file mode 100644 index 0000000000..1a3d213378 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt @@ -0,0 +1,22 @@ +add_library(libSceLibreSsl3 SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp) +target_include_directories(libSceLibreSsl3 PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceLibreSsl3 PRIVATE libSceLibreSsl) +configure_windows_unwind(libSceLibreSsl3) + +if(BUILD_TESTING) + add_test_executable(guest_crypto_memcmp_tests ${LIBS_INCLUDE_DIR}/tests/GuestCryptoMemcmp.cpp) + target_include_directories(guest_crypto_memcmp_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_crypto_memcmp_tests PRIVATE libSceLibreSsl3) + add_test(NAME guest_crypto_memcmp COMMAND guest_crypto_memcmp_tests) + set_tests_properties(guest_crypto_memcmp PROPERTIES TIMEOUT 20) + add_test_executable(guest_libressl3_digest_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSsl3Digest.cpp) + target_include_directories(guest_libressl3_digest_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl3_digest_tests PRIVATE libSceLibreSsl3 libc) + configure_windows_unwind(guest_libressl3_digest_tests) + add_test(NAME guest_libressl3_digest COMMAND guest_libressl3_digest_tests) + set_tests_properties(guest_libressl3_digest PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_crypto_memcmp guest_libressl3_digest PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl3/Export.cpp b/core/libs/prx/libSceLibreSsl3/Export.cpp new file mode 100644 index 0000000000..623b05d144 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/Export.cpp @@ -0,0 +1,438 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN +int APS5_VABI ASN1_INTEGER_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_OBJECT_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_STRING_get0_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_vfree_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_memcmp_nid_postfix(const void* first, const void* second, std::size_t length) { + const auto* firstBytes = static_cast(first); + const auto* secondBytes = static_cast(second); + unsigned int difference = 0; + for (std::size_t index = 0; index < length; ++index) + difference |= firstBytes[index] ^ secondBytes[index]; + return static_cast(difference); +} + +int APS5_VABI DH_get0_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI DH_get0_pqg_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI DSA_get0_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI DSA_get0_pqg_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_load_crypto_strings_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_peek_last_error_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_print_errors_cb_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CIPHER_CTX_set_flags_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DigestInit_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DigestSignFinal_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DigestSignInit_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_EncryptInit_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_MD_CTX_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_MD_CTX_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKCS82PKEY_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_CTX_ctrl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_CTX_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_decrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_decrypt_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_encrypt_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_get0_DH_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_get0_DSA_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_get0_RSA_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_new_mac_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_set1_EC_KEY_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_size_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_SignFinal_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_VerifyFinal_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_128_wrap_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_192_wrap_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_256_wrap_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_sha1_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_sha224_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OBJ_cmp_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OBJ_txt2obj_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_CERTID_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_cert_to_id_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_resp_find_status_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OpenSSL_version_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PKCS7_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PKCS8_PRIV_KEY_INFO_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_check_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_get0_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_padding_add_PKCS1_PSS_mgf1_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_add0_chain_cert_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_clear_chain_certs_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_max_proto_version_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_min_proto_version_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_SESSION_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_client_random_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_PUBKEY_get0_param_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get0_extensions_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get0_notAfter_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get0_notBefore_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get0_signature_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get_ext_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get_ext_count_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get_version_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_getm_notAfter_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_getm_notBefore_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_ASN1_INTEGER_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_ECParameters_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_PKCS7_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_PKCS8_PRIV_KEY_INFO_bio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_PrivateKey_bio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_RSAPublicKey_bio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_X509_bio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI o2i_ECPublicKey_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceLibreSsl3/src/Digest.cpp b/core/libs/prx/libSceLibreSsl3/src/Digest.cpp new file mode 100644 index 0000000000..373654f3d5 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/src/Digest.cpp @@ -0,0 +1,30 @@ +#include +#include "prx/libc/include/General.hpp" + +extern "C" { +int APS5_VABI MD5_Init_nid_postfix(void* context); +int APS5_VABI MD5_Update_nid_postfix(void* context, const void* data, std::size_t length); +int APS5_VABI MD5_Final_nid_postfix(unsigned char* digest, void* context); +int APS5_VABI SHA1_Init_nid_postfix(void* context); +int APS5_VABI SHA1_Update_nid_postfix(void* context, const void* data, std::size_t length); +int APS5_VABI SHA1_Final_nid_postfix(unsigned char* digest, void* context); + +unsigned char* APS5_VABI MD5_nid_postfix(const unsigned char* data, std::size_t length, unsigned char* digest) { + if (!digest) NotImplemented_nid_no_patch("MD5: null digest buffer"); + alignas(8) unsigned char context[92]; + MD5_Init_nid_postfix(context); + MD5_Update_nid_postfix(context, data, length); + MD5_Final_nid_postfix(digest, context); + return digest; +} + +unsigned char* APS5_VABI SHA1_nid_postfix(const unsigned char* data, std::size_t length, unsigned char* digest) { + if (!digest) NotImplemented_nid_no_patch("SHA1: null digest buffer"); + alignas(8) unsigned char context[96]; + SHA1_Init_nid_postfix(context); + SHA1_Update_nid_postfix(context, data, length); + SHA1_Final_nid_postfix(digest, context); + return digest; +} + +} diff --git a/core/libs/prx/libSceLoginDialog/Export.cpp b/core/libs/prx/libSceLoginDialog/Export.cpp index a986655ab2..8fcd0f4daf 100644 --- a/core/libs/prx/libSceLoginDialog/Export.cpp +++ b/core/libs/prx/libSceLoginDialog/Export.cpp @@ -3,6 +3,7 @@ #include #include #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" namespace { @@ -11,13 +12,14 @@ 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 LOGIN_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x81340001u); +constexpr int LOGIN_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x81340002u); +constexpr int LOGIN_DIALOG_ERROR_BUSY = static_cast(0x81340005u); +constexpr int LOGIN_DIALOG_ERROR_INVALID_ARGUMENT = static_cast(0x81340003u); constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; +constexpr int LOGIN_DIALOG_ERROR_INVALID_USER = static_cast(0x81340007u); + std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; } @@ -26,15 +28,18 @@ extern "C" { int APS5_VABI sceLoginDialogInitialize(void) { int expected = COMMON_DIALOG_STATUS_NONE; - if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return LOGIN_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } -int APS5_VABI sceLoginDialogOpen(const void* param) { +int APS5_VABI sceLoginDialogOpen(const LoginDialogParam* param) { + if (param == nullptr || param->size != sizeof(*param) || param->mode > 1 || !std::all_of(std::begin(param->reserved), std::end(param->reserved), [](std::uint32_t value) { return value == 0; })) return LOGIN_DIALOG_ERROR_INVALID_ARGUMENT; + for (const auto userId : param->userIds) { + if (userId != -1 && (static_cast(userId) < 0x10000000u || static_cast(userId) >= 0x40000000u)) return LOGIN_DIALOG_ERROR_INVALID_USER; + } 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; + if (status == COMMON_DIALOG_STATUS_NONE) return LOGIN_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return LOGIN_DIALOG_ERROR_BUSY; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } @@ -47,23 +52,24 @@ int APS5_VABI sceLoginDialogGetStatus(void) { return g_status.load(); } -int APS5_VABI sceLoginDialogGetResult(void* result) { +int APS5_VABI sceLoginDialogGetResult(LoginDialogResult* result) { + if (result == nullptr) return LOGIN_DIALOG_ERROR_INVALID_ARGUMENT; 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)); + if (status == COMMON_DIALOG_STATUS_NONE) return LOGIN_DIALOG_ERROR_NOT_INITIALIZED; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + result->errorCode = LOGIN_DIALOG_ERROR_INVALID_USER; + if (status != COMMON_DIALOG_STATUS_FINISHED) return LOGIN_DIALOG_ERROR_BUSY; return 0; } int APS5_VABI sceLoginDialogClose(void) { - if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return LOGIN_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } int APS5_VABI sceLoginDialogTerminate(void) { - if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return LOGIN_DIALOG_ERROR_NOT_INITIALIZED; return 0; } diff --git a/core/libs/prx/libSceLoginMgrServer/Export.cpp b/core/libs/prx/libSceLoginMgrServer/Export.cpp new file mode 100644 index 0000000000..c0497cf3de --- /dev/null +++ b/core/libs/prx/libSceLoginMgrServer/Export.cpp @@ -0,0 +1,158 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceLoginMgrServerCheckSafetyNoticeShown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerClearMorpheusRequiredUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerConvertToUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerCreateUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerDestroyUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerDialogGetAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerDialogGetOpenParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerDialogNotifyCloseFinished() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerDialogNotifyOpenFinished() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerDialogSetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerGetLogoutInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerGetMorpheusRequiredUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerGetSharePlayAllowPadOperation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerInitializeSharePlayAllowPadOperation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerIsRequestedCdlgClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerLoginServiceGetRequestParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerLoginServiceNotifyRequestFinished() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerNotifyEasySignInFinished() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerNotifyEasySignInUserCode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerNotifyHmdConnectInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerNotifySafetyNoticeShown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerNotifyShellUIState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerSetLoginFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerSetMorpheusAssignEnabledFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerSetMorpheusRequiredUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerSetSharePlayMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerSetUserStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLoginMgrServerUpdateUserIdRalatedToPadUniqueId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} 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..71262ec59b --- /dev/null +++ b/core/libs/prx/libSceLoginService/Export.cpp @@ -0,0 +1,23 @@ +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +struct LoginServiceDeviceRequest; + +extern "C" { + +int APS5_VABI sceLoginServiceInitialize(void) { + return 0; +} + +int APS5_VABI sceLoginServiceTerminate(void) { + return 0; +} + +int APS5_VABI sceLoginServiceRequestDevices(const LoginServiceDeviceRequest* param) { + (void)param; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceM4aacEnc/CMakeLists.txt b/core/libs/prx/libSceM4aacEnc/CMakeLists.txt new file mode 100644 index 0000000000..85a6f282a7 --- /dev/null +++ b/core/libs/prx/libSceM4aacEnc/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceM4aacEnc SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceM4aacEnc PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceM4aacEnc) + +if(BUILD_TESTING) + add_test_executable(guest_m4aac_enc_tests ${LIBS_INCLUDE_DIR}/tests/GuestM4aacEnc.cpp) + target_include_directories(guest_m4aac_enc_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_m4aac_enc_tests PRIVATE libSceM4aacEnc libc) + configure_windows_unwind(guest_m4aac_enc_tests) + add_test(NAME guest_m4aac_enc COMMAND guest_m4aac_enc_tests) + if(WIN32) + set_tests_properties(guest_m4aac_enc PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceM4aacEnc/Export.cpp b/core/libs/prx/libSceM4aacEnc/Export.cpp new file mode 100644 index 0000000000..e71f829cca --- /dev/null +++ b/core/libs/prx/libSceM4aacEnc/Export.cpp @@ -0,0 +1,94 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + + +extern "C" { + +int32_t APS5_VABI sceM4aacEncClearContext(int32_t m4aacEncHandle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncClearContextEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceM4aacEncCreateEncoder(int32_t channels, int32_t samplingRate, int32_t bitRate, int32_t inputFormat, int32_t outputFormat, int32_t *pInternalError) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncCreateEncoderEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceM4aacEncDeleteEncoder(int32_t m4aacEncHandle, int32_t *pInternalError) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncDeleteEncoderEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceM4aacEncEncode(int32_t m4aacEncHandle, void *pInput, uint32_t inputByte, void *pOutput, uint32_t *pOutputByte, int32_t *pInternalError) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncEncodeEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncEncodeWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncEncodeWithPriorityEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceM4aacEncFlush(int32_t m4aacEncHandle, void *pOutput, uint32_t *pOutputByte, int32_t *pInternalError) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncFlushEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncFlushWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncFlushWithPriorityEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncGetContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceMarlin/Export.cpp b/core/libs/prx/libSceMarlin/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceMarlin/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceMat/Export.cpp b/core/libs/prx/libSceMat/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceMat/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceMbus/CMakeLists.txt b/core/libs/prx/libSceMbus/CMakeLists.txt index 7901f908fa..9de325ed3f 100644 --- a/core/libs/prx/libSceMbus/CMakeLists.txt +++ b/core/libs/prx/libSceMbus/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceMbus SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(libSceMbus SHARED EXCLUDE_FROM_ALL Export.cpp PendingExports.cpp) target_include_directories(libSceMbus PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceMbus) diff --git a/core/libs/prx/libSceMbus/Export.cpp b/core/libs/prx/libSceMbus/Export.cpp index 9bfe48812e..ca49b545be 100644 --- a/core/libs/prx/libSceMbus/Export.cpp +++ b/core/libs/prx/libSceMbus/Export.cpp @@ -5,11 +5,41 @@ #include +struct DeviceServiceThreadParam { + std::uint64_t affinityMask; + std::int32_t priority; + std::uint32_t reserved; +}; + +struct DeviceServiceInitParam { + std::uint32_t size; + std::uint32_t reserved; + const DeviceServiceThreadParam* threadParam; +}; + +struct DeviceServiceUsageFilter { + std::uint16_t usagePage; + std::uint16_t minimumUsage; + std::uint16_t maximumUsage; +}; + +static_assert(sizeof(DeviceServiceThreadParam) == 16); +static_assert(offsetof(DeviceServiceThreadParam, priority) == 8); +static_assert(sizeof(DeviceServiceInitParam) == 16); +static_assert(offsetof(DeviceServiceInitParam, threadParam) == 8); +static_assert(sizeof(DeviceServiceUsageFilter) == 6); + extern "C" { -std::int32_t APS5_VABI sceDeviceServiceInitialize(std::int32_t mode, std::int32_t flags) { +std::int32_t APS5_VABI sceDeviceServiceInitialize(std::int32_t mode, const DeviceServiceInitParam* param) { (void)mode; - (void)flags; + if (param != nullptr) { + if (param->size != sizeof(DeviceServiceInitParam) || param->reserved != 0) return static_cast(0x809B0080u); + if (param->threadParam != nullptr) { + if (param->threadParam->reserved != 0) return static_cast(0x809B0080u); + throw std::runtime_error("sceDeviceServiceInitialize: custom worker scheduling is not implemented"); + } + } return 0; } @@ -17,20 +47,39 @@ std::int32_t APS5_VABI sceDeviceServiceTerminate() { return 0; } -std::int32_t APS5_VABI sceDeviceServiceGetEventState(std::int32_t deviceType) { - (void)deviceType; +std::int32_t APS5_VABI sceDeviceServiceGetEventState(std::int32_t clear) { + (void)clear; return 0; } -std::int32_t APS5_VABI sceDeviceServiceQueryDeviceInfo_(std::int32_t deviceClass, std::int32_t arg1, std::int32_t arg2, void* infos, std::int32_t maxInfos, std::int32_t* count, std::int32_t* reserved, std::uint64_t infoSize) { - (void)deviceClass; - (void)arg1; +std::int32_t APS5_VABI sceDeviceServiceQueryDeviceInfo_(std::int32_t deviceClass, const DeviceServiceUsageFilter* filter, std::int32_t arg2, void* infos, std::int32_t maxInfos, std::int32_t* count, std::int32_t* reserved, std::uint32_t infoSize) { (void)arg2; - (void)infos; (void)reserved; - (void)infoSize; - if (count == nullptr || maxInfos < 0) throw std::invalid_argument(std::string(__func__) + ": invalid argument"); - *count = 0; + constexpr auto invalidArgument = static_cast(0x809B0080u); + switch (deviceClass) { + case 0x1001: + case 0x2001: + case 0x3001: + case 0x4001: + case 0x4002: + case 0x5001: + case 0x6001: + case 0x7001: + case 0x9001: + if (filter != nullptr) return invalidArgument; + break; + case 0x8001: + if (filter == nullptr || filter->minimumUsage > filter->maximumUsage) return invalidArgument; + break; + default: + return invalidArgument; + } + if (infoSize != 112 || maxInfos < 0 || (maxInfos != 0 && infos == nullptr)) return invalidArgument; + if (count != nullptr) *count = 0; + return 0; +} + +std::int32_t APS5_VABI sceDeviceServiceGetGeneration(void) { return 0; } diff --git a/core/libs/prx/libSceMbus/PendingExports.cpp b/core/libs/prx/libSceMbus/PendingExports.cpp new file mode 100644 index 0000000000..76313b1a67 --- /dev/null +++ b/core/libs/prx/libSceMbus/PendingExports.cpp @@ -0,0 +1,612 @@ +#include "prx/libc/include/General.hpp" + +#include + +struct MbusDeviceFunctionState { + std::byte data[180]; +}; + +static_assert(sizeof(MbusDeviceFunctionState) == 180); +struct MbusDeviceDescription; +struct MbusDeviceInfo; +struct MbusSimulatedHandleStatus; +struct MbusSparkState; +struct MbusDriverCallbacks; +struct MbusUsbPowerEntry; +struct MbusEvent; +struct alignas(8) MbusDeviceCondition { + std::byte data[256]; +}; + +static_assert(sizeof(MbusDeviceCondition) == 256); +struct MbusDeviceServiceInfo; +struct MbusEventCallbacks { + void (APS5_VABI *onOverflow)(std::int32_t eventHandle, void* userData); + void (APS5_VABI *onEvent)(std::int32_t eventHandle, void* userData, const MbusEvent* event); +}; + +static_assert(sizeof(MbusEventCallbacks) == 16); +struct MbusProcessInfo; +struct MbusStartupInfo; +struct MbusStartupParam; +struct MbusControlStatus; +struct MbusDebugDeviceInfo; +struct MbusInternalInfo; +struct MbusPriorityInfo; + +extern "C" { + +int APS5_VABI sceMbusAcquireControl(std::uint64_t control, std::int32_t first, std::int32_t second) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAcquireControlList() { + return static_cast(0x809B00FF); +} + +int APS5_VABI sceMbusAcquireControlWithState(std::uint32_t deviceClass, std::int32_t state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAcquireControlWithState2(std::uint32_t deviceClass, std::int32_t moduleId, std::int32_t state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAcquireControlWithStateFlag(std::uint32_t deviceClass, std::uint32_t moduleId, std::uint32_t state, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandle(std::uint32_t deviceClass, std::int32_t handle, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandleByDeviceId(std::uint32_t deviceClass, std::int32_t handle, std::uint64_t deviceId, std::uint32_t first, std::uint32_t second, std::uint32_t third) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandleByUserId(std::uint32_t deviceClass, std::int32_t handle, std::int32_t userId, std::int32_t first, std::int32_t second, std::int32_t third) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandleByVidPid(std::uint32_t deviceClass, std::int32_t handle, std::int32_t first, std::uint16_t vendorId, std::uint16_t productId, std::int32_t second, std::int32_t third) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAddHandleWithModuleId(std::uint32_t deviceClass, std::int32_t handle, std::uint32_t flags, std::uint32_t moduleId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAttachToApplicationByPid(std::int32_t processId, std::int32_t deviceClass, std::int32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusBindDeviceWithUserId(std::uint64_t deviceId, std::int32_t userId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusChangeAppModuleState(std::uint32_t deviceClass, std::int32_t moduleId, std::uint32_t state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusCheckDriverCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusConvertToLocalDeviceId(std::uint64_t deviceId, std::uint32_t* localId, std::uint32_t* deviceClass) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusConvertToLocalDeviceId2(std::uint64_t deviceId, std::uint32_t deviceClass, std::uint64_t* localId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusConvertToMbusDeviceId(std::uint64_t localId, std::uint32_t deviceClass, std::uint64_t* deviceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAcquireControl(std::uint64_t control, std::int32_t first, std::int32_t second, std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAcquireControlWithState(std::uint32_t deviceClass, std::int32_t state, std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAcquireControlWithState2(std::uint32_t deviceClass, std::int32_t moduleId, std::int32_t state, std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAcquireControlWithStateFlag(std::uint32_t deviceClass, std::uint32_t moduleId, std::uint32_t state, std::uint32_t flags, std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugAddProcess(std::int32_t processId, std::int32_t flags, const MbusProcessInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugCheckProcessResume(MbusProcessInfo* info, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugDisableBgmForShellUi(std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetApplicationStartupInfo(std::uint32_t appId, const MbusProcessInfo* info, MbusStartupInfo* startupInfo) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetControlStatus(std::int32_t deviceClass, std::int32_t processId, MbusControlStatus* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetDeviceInfo(std::uint64_t deviceClass, MbusDebugDeviceInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetInternalInfo(MbusInternalInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugGetPriorityInfo(std::uint32_t deviceClass, MbusPriorityInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugReenableBgmForShellUi(std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugReleaseControl(std::uint64_t control, std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugRemoveCameraAppModuleFocus(std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugResumeApplication(std::int32_t appId, std::uint8_t firstFlag, std::uint8_t secondFlag, MbusProcessInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetApplicationFocusByAppId(std::uint32_t appId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetCameraAppModuleFocus(std::int32_t processId, std::int32_t moduleId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetControllerFocusByAppId(std::uint32_t appId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetOtherProcessExcludedAction(std::uint64_t control, std::uint32_t action, std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSetPriority(std::uint64_t control, std::uint32_t priority, std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugStartApplication(std::int32_t appId, std::int32_t first, std::int32_t second, std::uint8_t firstFlag, std::uint8_t secondFlag, const MbusStartupParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugStartApplication2(std::int32_t appId, std::int32_t first, std::int32_t second, std::uint8_t firstFlag, std::uint8_t secondFlag, const MbusStartupParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugStartApplicationNull(std::int32_t appId, std::int32_t first, std::int32_t second, std::uint8_t firstFlag, std::uint8_t secondFlag) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugSuspendApplication(std::uint32_t appId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugTerminateApplication(std::uint32_t appId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDebugTerminateProcess(std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDetachFromApplicationByPid(std::int32_t processId, std::int32_t deviceClass) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDisableBgmForShellUi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDisableChat(std::uint32_t deviceClass) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDisconnectDevice(std::uint64_t deviceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpAppInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpHandleStatusInfo(std::uint64_t control, std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpHandleStatusInfoAll(std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpModuleStateInfo(std::uint64_t control) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpModuleStateInfoAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpPlayerInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpPortHandleInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpProcMediaInfo(std::uint64_t control) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpProcMediaInfoAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusDumpUserMediaInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEnableChat(std::uint32_t deviceClass) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEnterAudioOutAutoAttenuation(std::int32_t first, std::int32_t second) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventBusStatusChangeSubscribe(std::int32_t eventHandle, std::uint32_t first, std::uint32_t second, std::uint64_t firstValue, std::uint64_t secondValue) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventBusStatusChangeUnsubscribe(std::uint64_t unusedSlot, std::int32_t subscription) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventCallbackFuncsInit_(MbusEventCallbacks* callbacks, std::uint32_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventCreate_(std::uint32_t flags, MbusEvent* buffer, std::uint32_t capacity, const MbusEventCallbacks* callbacks, void* userData) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventDestroy(std::int32_t eventHandle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusEventReceive(std::int32_t eventHandle, MbusEvent* events, std::int32_t capacity, std::int32_t wait) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetControlStatus(std::uint64_t control, MbusControlStatus* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceDescription(std::uint64_t deviceId, std::uint32_t selector, MbusDeviceDescription* description) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceInfoByBusId_(std::int32_t busId, MbusDeviceInfo* info, std::uint32_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceInfoByConditionForDeviceService(std::int32_t flags, MbusDeviceServiceInfo* infos, std::int32_t capacity, std::int32_t* first, std::int32_t* second, MbusDeviceCondition condition) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceInfoByCondition_(const MbusDeviceCondition* condition, std::int32_t flags, MbusDeviceInfo* infos, std::int32_t capacity, std::int32_t* first, std::int32_t* second, std::uint32_t infoSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetDeviceInfo_(std::uint64_t deviceId, MbusDeviceInfo* info, std::uint32_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetSimulatedBusUsableStatusByBusType(std::uint32_t busType, std::int32_t first, std::uint64_t deviceId, std::uint32_t second, std::uint32_t third) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetSimulatedHandleStatusInfo(std::int32_t deviceClass, std::int32_t first, std::int32_t second, MbusSimulatedHandleStatus* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetSimulatedHandleStatusInfo2(std::uint32_t deviceClass, std::uint32_t first, std::uint32_t second, std::uint32_t third, MbusSimulatedHandleStatus* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusGetSparkState(std::uint64_t deviceId, MbusSparkState* state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusIsBgmProhibited() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusIsUsingDevice(std::int32_t deviceClass, std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusLeaveAudioOutAutoAttenuation(std::int32_t first, std::int32_t second) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusNotifyBtDisconnection(std::uint64_t deviceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusNotifyDriverEvent(std::int32_t driverId, std::int32_t eventId, std::uint64_t firstValue, std::int32_t flags, std::uint64_t secondValue) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusReenableBgmForShellUi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusRegisterDriver(std::int32_t driverId, const MbusDriverCallbacks* callbacks, void* userData) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusReleaseControl(std::uint64_t control) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusRemoveCameraAppModuleFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusRemoveHandle(std::int32_t deviceClass, std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusRemoveProxy(std::int32_t processId, std::uint32_t deviceClass, std::uint64_t deviceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusResolveByDeviceId(std::int32_t deviceClass, std::uint64_t deviceId, std::uint32_t first, std::uint32_t second) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusResolveByHandle(std::int32_t deviceClass, std::int32_t handle, std::int32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusResolveByPlayerId(std::uint32_t deviceClass, std::uint32_t playerId, std::uint32_t first, std::uint32_t second) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusResolveByUserId(std::uint32_t deviceClass, std::int32_t userId, std::uint32_t first, std::uint32_t second) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetAutoLoginMode(std::uint32_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetCameraAppModuleFocus(std::int32_t moduleId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetDeviceFunctionState(std::uint64_t deviceId, MbusDeviceFunctionState state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetDuckingSetting(std::int32_t setting) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetProxy(std::int32_t processId, std::int32_t first, std::int32_t second, std::int32_t third, std::uint64_t deviceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetRoutingMode(std::int32_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetScratchData(std::uint64_t deviceId, std::int32_t first, std::int32_t second) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetScratchDataUpdatedEventMask(std::uint32_t mask) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetSocialScreenAudioMode(std::int32_t first, std::int32_t second) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetTestFlag(std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetThreadParam(std::int32_t priority, std::uint64_t affinityMask, std::uint64_t reserved) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetUsbPowerBlackList(std::int32_t first, std::uint8_t count, const MbusUsbPowerEntry* entries) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetUserMuteStatus(std::int32_t userId, std::int32_t status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusSetVolumeDown(std::uint32_t deviceClass, std::uint8_t volume) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusStartAudioOutAuxBroadcast() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusStartAudioOutSharePlay(std::int32_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusStopAudioOutAuxBroadcast() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusStopAudioOutSharePlay() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusTerm() { + return 0; +} + +int APS5_VABI sceMbusUnregisterDriver(std::int32_t driverId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusUserLogin(std::int32_t userId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusUserLogout(std::int32_t userId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceMediaAppSharedStorage/Export.cpp b/core/libs/prx/libSceMediaAppSharedStorage/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceMediaAppSharedStorage/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceMediaFrameworkInterface/Export.cpp b/core/libs/prx/libSceMediaFrameworkInterface/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceMediaFrameworkInterface/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceMediaFrameworkUtil/Export.cpp b/core/libs/prx/libSceMediaFrameworkUtil/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceMediaFrameworkUtil/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceMetadataReaderWriter/Export.cpp b/core/libs/prx/libSceMetadataReaderWriter/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceMetadataReaderWriter/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceMouse/Export.cpp b/core/libs/prx/libSceMouse/Export.cpp index f41c9eaf48..121b9d2e6a 100644 --- a/core/libs/prx/libSceMouse/Export.cpp +++ b/core/libs/prx/libSceMouse/Export.cpp @@ -19,17 +19,66 @@ int APS5_VABI sceMouseRead(std::int32_t handle, MouseData* data, std::int32_t nu return Mouse::Read(handle, data, num); } -void MousePublishInput_nid_postfix(const MouseInputEvent& event) { +void MousePublishInput_nid_no_patch(const MouseInputEvent& event) { Mouse::Publish(event); } -int APS5_VABI sceMouseResetAbsolutePosition() { +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() { + return Mouse::IsOpen(); +} + +int APS5_VABI sceMouseConnectPort() { NotImplemented_nid_no_patch(__func__); return 0; } -bool MouseIsOpen_nid_postfix() { - return Mouse::IsOpen(); +int APS5_VABI sceMouseDebugGetDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseDeviceOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseDisconnectDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseDisconnectPort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseMbusInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseSetHandType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseSetPointerSpeed() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMouseSetProcessPrivilege() { + NotImplemented_nid_no_patch(__func__); + return 0; } } diff --git a/core/libs/prx/libSceMouse/include/MouseState.hpp b/core/libs/prx/libSceMouse/include/MouseState.hpp index 15c89831b6..390cf33968 100644 --- a/core/libs/prx/libSceMouse/include/MouseState.hpp +++ b/core/libs/prx/libSceMouse/include/MouseState.hpp @@ -21,10 +21,11 @@ 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); } -extern "C" void MousePublishInput_nid_postfix(const MouseInputEvent& event); -extern "C" bool MouseIsOpen_nid_postfix(); +extern "C" void MousePublishInput_nid_no_patch(const MouseInputEvent& event); +extern "C" bool MouseIsOpen_nid_no_patch(); #endif diff --git a/core/libs/prx/libSceMouse/src/mouse_impl.cpp b/core/libs/prx/libSceMouse/src/mouse_impl.cpp index c139fff6a2..b4df78375a 100644 --- a/core/libs/prx/libSceMouse/src/mouse_impl.cpp +++ b/core/libs/prx/libSceMouse/src/mouse_impl.cpp @@ -82,6 +82,14 @@ int Read(std::int32_t handle, MouseData* data, std::int32_t num) { return available; } +int ResetAbsolutePosition(std::int32_t handle, std::uint16_t x, std::uint16_t y) { + std::lock_guard lock(mouseMutex); + if (!initialized) return MOUSE_ERROR_NOT_INITIALIZED; + if (handle != MOUSE_HANDLE || !opened) return MOUSE_ERROR_INVALID_HANDLE; + if (x >= 1920 || y >= 1080) return MOUSE_ERROR_INVALID_ARG; + 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 e01f3f9b4f..7e7b1fcf13 100644 --- a/core/libs/prx/libSceMouse/tests/Mouse.cpp +++ b/core/libs/prx/libSceMouse/tests/Mouse.cpp @@ -7,25 +7,36 @@ #include extern "C" { +bool MouseIsOpen_nid_no_patch(); int APS5_VABI sceMouseInit(); int APS5_VABI sceMouseOpen(int, std::int32_t, std::int32_t, const void*); int APS5_VABI sceMouseClose(std::int32_t); 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(!MouseIsOpen_nid_no_patch()); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_ERROR_NOT_INITIALIZED); Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_ERROR_NOT_INITIALIZED); Require(sceMouseInit() == MOUSE_OK); 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(MouseIsOpen_nid_no_patch()); Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_ERROR_ALREADY_OPENED); + Require(sceMouseResetAbsolutePosition(42, 959, 539) == MOUSE_ERROR_INVALID_HANDLE); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_OK); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 1919, 1079) == MOUSE_OK); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 1920, 1079) == MOUSE_ERROR_INVALID_ARG); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 1919, 1080) == MOUSE_ERROR_INVALID_ARG); 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); @@ -92,17 +103,23 @@ int main() { for (int i = 0; i < 70; ++i) { MouseInputEvent motion{}; motion.x = i; - MousePublishInput_nid_postfix(motion); + MousePublishInput_nid_no_patch(motion); } Require(sceMouseRead(MOUSE_HANDLE, data, 64) == 64); Require(data[0].x_axis == 6 && data[63].x_axis == 69); Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); + Require(!MouseIsOpen_nid_no_patch()); Require(sceMouseRead(MOUSE_HANDLE, data, 1) == MOUSE_ERROR_INVALID_HANDLE); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_ERROR_INVALID_HANDLE); Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_ERROR_INVALID_HANDLE); MouseOpenParam merged{}; merged.behaviorFlag = MOUSE_OPEN_PARAM_MERGED; Require(sceMouseOpen(1, 0, 0, &merged) == MOUSE_HANDLE); + Require(MouseIsOpen_nid_no_patch()); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_OK); Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 1 && data[0].buttons == 0); Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); + Require(!MouseIsOpen_nid_no_patch()); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_ERROR_INVALID_HANDLE); } diff --git a/core/libs/prx/libSceMove/Export.cpp b/core/libs/prx/libSceMove/Export.cpp new file mode 100644 index 0000000000..effeda1e5c --- /dev/null +++ b/core/libs/prx/libSceMove/Export.cpp @@ -0,0 +1,164 @@ +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libSceUserService/UserService.hpp" + +namespace { + +constexpr int MOVE_OK = 0; +constexpr int MOVE_NO_CONTROLLER_CONNECTED = 1; +constexpr int MOVE_ERROR_NOT_INIT = static_cast(0x80EE0001); +constexpr int MOVE_ERROR_ALREADY_INIT = static_cast(0x80EE0002); +constexpr int MOVE_ERROR_INVALID_ARG = static_cast(0x80EE0003); +constexpr int MOVE_ERROR_INVALID_HANDLE = static_cast(0x80EE0004); +constexpr int MOVE_ERROR_ALREADY_OPENED = static_cast(0x80EE0007); +constexpr int32_t MOVE_FIRST_HANDLE = 0x30B0100; +constexpr int32_t MOVE_HANDLE_STEP = 0x100; + +std::mutex stateMutex; +bool initialized = false; +int32_t nextHandle = MOVE_FIRST_HANDLE; +struct Slot { + int32_t type; + int32_t index; +}; + +std::unordered_map openHandles; + +int checkHandle(int32_t handle, bool validArguments = true) { + std::lock_guard lock(stateMutex); + if (!initialized) return MOVE_ERROR_NOT_INIT; + if (!validArguments) return MOVE_ERROR_INVALID_ARG; + if (!openHandles.contains(handle)) return MOVE_ERROR_INVALID_HANDLE; + return MOVE_OK; +} + +} + +extern "C" { + +int APS5_VABI sceMoveInit(void) { + std::lock_guard lock(stateMutex); + if (initialized) return MOVE_ERROR_ALREADY_INIT; + initialized = true; + return MOVE_OK; +} + +int APS5_VABI sceMoveTerm(void) { + std::lock_guard lock(stateMutex); + if (!initialized) return MOVE_ERROR_NOT_INIT; + initialized = false; + openHandles.clear(); + return MOVE_OK; +} + +int APS5_VABI sceMoveOpen(int32_t userId, int32_t type, int32_t index) { + std::lock_guard lock(stateMutex); + if (!initialized) return MOVE_ERROR_NOT_INIT; + if (userId != USER_SERVICE_INITIAL_USER_ID) NotImplemented_nid_no_patch(__func__); + for (const auto& [openHandle, slot] : openHandles) { + if (slot.type == type && slot.index == index) return MOVE_ERROR_ALREADY_OPENED; + } + const int32_t handle = nextHandle; + nextHandle += MOVE_HANDLE_STEP; + openHandles.emplace(handle, Slot{type, index}); + return handle; +} + +int APS5_VABI sceMoveClose(int32_t handle) { + std::lock_guard lock(stateMutex); + if (!initialized) return MOVE_ERROR_NOT_INIT; + return openHandles.erase(handle) != 0 ? MOVE_OK : MOVE_ERROR_INVALID_HANDLE; +} + +int APS5_VABI sceMoveGetDeviceInfo(int32_t handle, void* info) { + if (const int result = checkHandle(handle, info != nullptr); result != MOVE_OK) return result; + return MOVE_NO_CONTROLLER_CONNECTED; +} + +int APS5_VABI sceMoveGetExtensionPortInfo(int32_t handle, void* data) { + if (const int result = checkHandle(handle, data != nullptr); result != MOVE_OK) return result; + return MOVE_NO_CONTROLLER_CONNECTED; +} + +int APS5_VABI sceMoveReadStateLatest(int32_t handle, void* data) { + if (const int result = checkHandle(handle, data != nullptr); result != MOVE_OK) return result; + return MOVE_NO_CONTROLLER_CONNECTED; +} + +int APS5_VABI sceMoveReadStateRecent(int32_t handle, int64_t timestamp, void* data, int32_t* outCount) { + if (const int result = checkHandle(handle, timestamp >= 0 && data != nullptr && outCount != nullptr); result != MOVE_OK) return result; + return MOVE_NO_CONTROLLER_CONNECTED; +} + +int APS5_VABI sceMoveSetVibration(int32_t handle, uint8_t motor) { + (void)motor; + if (const int result = checkHandle(handle); result != MOVE_OK) return result; + return MOVE_NO_CONTROLLER_CONNECTED; +} + +int APS5_VABI sceMoveSetLightSphere(int32_t handle, uint8_t red, uint8_t green, uint8_t blue) { + (void)red; + (void)green; + (void)blue; + if (const int result = checkHandle(handle); result != MOVE_OK) return result; + return MOVE_NO_CONTROLLER_CONNECTED; +} + +int APS5_VABI sceMoveResetLightSphere(int32_t handle) { + return checkHandle(handle); +} +int APS5_VABI sceMoveGetBluetoothAddress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveGetCalibrationInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveGetDeviceId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveIsReproductionModel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveIsValidHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveReadStateRecentForTracker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveSetCalibrationMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveSetExtensionPortOutput(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveSetLightSphereForTracker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveWriteCalibrationInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceMoveTracker/Export.cpp b/core/libs/prx/libSceMoveTracker/Export.cpp new file mode 100644 index 0000000000..ef780c7000 --- /dev/null +++ b/core/libs/prx/libSceMoveTracker/Export.cpp @@ -0,0 +1,101 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +struct MoveTrackerCameraFrame; +struct MoveTrackerControllerInput; +struct MoveTrackerState; +struct MoveTrackerPlaybackInfo; + +extern "C" { + +int APS5_VABI sceMoveTrackerCalibrateReset() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerCalibrateYaw(std::int32_t handle, const float* direction) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerCameraUpdate(const MoveTrackerCameraFrame* frames) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerCaptureInit(void* imageMemory, std::int32_t capacity, std::int32_t interval, const char* path, std::int32_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerCaptureTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerControllersUpdate(const MoveTrackerControllerInput* controllers) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerGetState(std::int32_t handle, std::uint64_t timestamp, MoveTrackerState* state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerGetVersion() { + return 2; +} + +int APS5_VABI sceMoveTrackerGetWorkingMemorySize(std::uint32_t* totalSize, std::uint32_t* processingSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerInit(void* workMemory, std::uint64_t requiredArgument, std::uint32_t pipeId, std::uint32_t queueId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerPlayGetImages(MoveTrackerCameraFrame* frames) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerPlayGetStates(MoveTrackerPlaybackInfo* info, MoveTrackerState* states) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerPlayStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerPlayStep() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerRecordSave() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerRecordStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveTrackerRecordStop() { + return 0; +} + +int APS5_VABI sceMoveTrackerTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceMsgDialog.native/Export.cpp b/core/libs/prx/libSceMsgDialog.native/Export.cpp index edb76df0f7..339bf0049a 100644 --- a/core/libs/prx/libSceMsgDialog.native/Export.cpp +++ b/core/libs/prx/libSceMsgDialog.native/Export.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -11,6 +10,7 @@ std::atomic g_status{0}; constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_OK = 0; +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast(0x80B8000Bu); @@ -23,41 +23,38 @@ extern "C" { int SceMsgDialogNativeModuleLoaded_nid_no_patch = 1; int APS5_VABI sceMsgDialogClose(void) { - int expected = COMMON_DIALOG_STATUS_RUNNING; - if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_FINISHED)) return COMMON_DIALOG_ERROR_NOT_RUNNING; - return 0; + int expected = COMMON_DIALOG_STATUS_RUNNING; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_FINISHED)) return COMMON_DIALOG_ERROR_NOT_RUNNING; + return 0; } int APS5_VABI sceMsgDialogGetResult(MsgDialogResult* result) { - const int status = g_status.load(); - if (status == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - *result = MsgDialogResult{}; - result->result = COMMON_DIALOG_RESULT_OK; - result->button_id = BUTTON_ID_OK; - return 0; + const int status = g_status.load(); + if (status == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + *result = MsgDialogResult{}; + result->result = COMMON_DIALOG_RESULT_OK; + result->button_id = BUTTON_ID_OK; + return 0; } int APS5_VABI sceMsgDialogInitialize(void) { int expected = 0; - if (!g_status.compare_exchange_strong(expected, 1)) throw std::logic_error("sceMsgDialogInitialize: already initialized"); + if (!g_status.compare_exchange_strong(expected, 1)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } int APS5_VABI sceMsgDialogOpen(const void* param) { - const int status = g_status.load(); - if (status == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - g_status = COMMON_DIALOG_STATUS_FINISHED; - return 0; + const int status = g_status.load(); + if (status == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; } int APS5_VABI sceMsgDialogTerminate(void) { - int expected = 1; - if (g_status.compare_exchange_strong(expected, 0)) return 0; - expected = COMMON_DIALOG_STATUS_FINISHED; - if (!g_status.compare_exchange_strong(expected, 0)) throw std::logic_error("sceMsgDialogTerminate: not initialized or still running"); + if (g_status.exchange(0) == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; return 0; } @@ -66,21 +63,21 @@ int APS5_VABI sceMsgDialogUpdateStatus(void) { } int APS5_VABI sceMsgDialogProgressBarInc(int target, std::uint32_t delta) { - (void)target; - (void)delta; - return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; + (void)target; + (void)delta; + return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; } int APS5_VABI sceMsgDialogProgressBarSetMsg(int target, const char* msg) { - (void)target; - (void)msg; - return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; + (void)target; + (void)msg; + return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; } int APS5_VABI sceMsgDialogProgressBarSetValue(int target, std::uint32_t rate) { - (void)target; - (void)rate; - return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; + (void)target; + (void)rate; + return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; } int APS5_VABI sceMsgDialogGetStatus(void) { diff --git a/core/libs/prx/libSceMusicCoreInterface/Export.cpp b/core/libs/prx/libSceMusicCoreInterface/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceMusicCoreInterface/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceMusicFrameworkInterface/Export.cpp b/core/libs/prx/libSceMusicFrameworkInterface/Export.cpp new file mode 100644 index 0000000000..dc0e9adc97 --- /dev/null +++ b/core/libs/prx/libSceMusicFrameworkInterface/Export.cpp @@ -0,0 +1,93 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceMusicFwGetCurrentTime() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMusicFwGetLoop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMusicFwGetMuted() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMusicFwGetNetworkState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMusicFwGetVolume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMusicFwIsEnded() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMusicFwIsPlaying() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMusicFwIsStopped() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMusicFwRemoveSrc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMusicFwSendMessageAsynchronously() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMusicFwSendMessageSynchronously() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMusicFwSetCurrentTime() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMusicFwSetLoop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMusicFwSetShuffle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMusicFwSetVolume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMusicFwSkipNext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMusicFwSkipPrevious() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNKWeb/Export.cpp b/core/libs/prx/libSceNKWeb/Export.cpp new file mode 100644 index 0000000000..81401ab485 --- /dev/null +++ b/core/libs/prx/libSceNKWeb/Export.cpp @@ -0,0 +1,18 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNKWebInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNKWebTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNKWebCdlgInjectedBundle/Export.cpp b/core/libs/prx/libSceNKWebCdlgInjectedBundle/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceNKWebCdlgInjectedBundle/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceNKWebKit/Export.cpp b/core/libs/prx/libSceNKWebKit/Export.cpp new file mode 100644 index 0000000000..a6caa403e8 --- /dev/null +++ b/core/libs/prx/libSceNKWebKit/Export.cpp @@ -0,0 +1,758 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNKWebKitUnknown____URPJNZ18_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++-URPJNZ18", sceNKWebKitUnknown____URPJNZ18_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___67uYkhqBU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++67uYkhqBU", sceNKWebKitUnknown___67uYkhqBU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___8RYr2gjrw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++8RYr2gjrw", sceNKWebKitUnknown___8RYr2gjrw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___BntMcao1o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++BntMcao1o", sceNKWebKitUnknown___BntMcao1o_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___R8IXi10PI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++R8IXi10PI", sceNKWebKitUnknown___R8IXi10PI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___j2h_QO22w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++j2h+QO22w", sceNKWebKitUnknown___j2h_QO22w_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown____7XwmYLnI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+-+7XwmYLnI", sceNKWebKitUnknown____7XwmYLnI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown___wmVyLhgm0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+-wmVyLhgm0", sceNKWebKitUnknown___wmVyLhgm0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__0QEt8ZQWgg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+0QEt8ZQWgg", sceNKWebKitUnknown__0QEt8ZQWgg_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__0YNxcA_duk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+0YNxcA-duk", sceNKWebKitUnknown__0YNxcA_duk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__0ZZzGYxx_I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+0ZZzGYxx+I", sceNKWebKitUnknown__0ZZzGYxx_I_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__0rdnkSicqk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+0rdnkSicqk", sceNKWebKitUnknown__0rdnkSicqk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__1Ex5GCP8Xs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+1Ex5GCP8Xs", sceNKWebKitUnknown__1Ex5GCP8Xs_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__1FfZ_0sxvQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+1FfZ+0sxvQ", sceNKWebKitUnknown__1FfZ_0sxvQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__1ObAtzCrYE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+1ObAtzCrYE", sceNKWebKitUnknown__1ObAtzCrYE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__1n_h4FUu9E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+1n+h4FUu9E", sceNKWebKitUnknown__1n_h4FUu9E_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__26aZpK535w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+26aZpK535w", sceNKWebKitUnknown__26aZpK535w_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__2XuBaAXb_4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+2XuBaAXb+4", sceNKWebKitUnknown__2XuBaAXb_4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__2rTX2KM7Ms_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+2rTX2KM7Ms", sceNKWebKitUnknown__2rTX2KM7Ms_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__31dbB3S_6Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+31dbB3S-6Y", sceNKWebKitUnknown__31dbB3S_6Y_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__3p6wRzzzlQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+3p6wRzzzlQ", sceNKWebKitUnknown__3p6wRzzzlQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__4_GNOUIBt0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+4-GNOUIBt0", sceNKWebKitUnknown__4_GNOUIBt0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__45BiDnCTxw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+45BiDnCTxw", sceNKWebKitUnknown__45BiDnCTxw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__4lVmXsIwqM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+4lVmXsIwqM", sceNKWebKitUnknown__4lVmXsIwqM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__5E2YZZIstM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+5E2YZZIstM", sceNKWebKitUnknown__5E2YZZIstM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__5V_UOk8zS4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+5V-UOk8zS4", sceNKWebKitUnknown__5V_UOk8zS4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__63IB3UdMOY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+63IB3UdMOY", sceNKWebKitUnknown__63IB3UdMOY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__6boXsT6XFA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+6boXsT6XFA", sceNKWebKitUnknown__6boXsT6XFA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__79w68212vg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+79w68212vg", sceNKWebKitUnknown__79w68212vg_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__7HpqmfJz48_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+7HpqmfJz48", sceNKWebKitUnknown__7HpqmfJz48_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__8T1y8Y_lgQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+8T1y8Y-lgQ", sceNKWebKitUnknown__8T1y8Y_lgQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__8VuTEFp13s_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+8VuTEFp13s", sceNKWebKitUnknown__8VuTEFp13s_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__9TKACifR2g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+9TKACifR2g", sceNKWebKitUnknown__9TKACifR2g_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__9ZBRWNf4og_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+9ZBRWNf4og", sceNKWebKitUnknown__9ZBRWNf4og_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__9us0nrDaCU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+9us0nrDaCU", sceNKWebKitUnknown__9us0nrDaCU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__AIEKk6K05M_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+AIEKk6K05M", sceNKWebKitUnknown__AIEKk6K05M_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Abk7CfTivU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Abk7CfTivU", sceNKWebKitUnknown__Abk7CfTivU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__AnsPWo12m8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+AnsPWo12m8", sceNKWebKitUnknown__AnsPWo12m8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__BDqCDede1o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+BDqCDede1o", sceNKWebKitUnknown__BDqCDede1o_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__BSUP6XSeAw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+BSUP6XSeAw", sceNKWebKitUnknown__BSUP6XSeAw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__BZCJi3OIiQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+BZCJi3OIiQ", sceNKWebKitUnknown__BZCJi3OIiQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ByVxWl1pW8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ByVxWl1pW8", sceNKWebKitUnknown__ByVxWl1pW8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__CgSI_4D534_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+CgSI-4D534", sceNKWebKitUnknown__CgSI_4D534_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__CpPA9_CKUA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+CpPA9+CKUA", sceNKWebKitUnknown__CpPA9_CKUA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__DcUw5YwkSY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+DcUw5YwkSY", sceNKWebKitUnknown__DcUw5YwkSY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__DpDBrSZWUc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+DpDBrSZWUc", sceNKWebKitUnknown__DpDBrSZWUc_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__E3D_W4kpl8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+E3D-W4kpl8", sceNKWebKitUnknown__E3D_W4kpl8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__EN6MgNmVv4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+EN6MgNmVv4", sceNKWebKitUnknown__EN6MgNmVv4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ErgockOJwc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ErgockOJwc", sceNKWebKitUnknown__ErgockOJwc_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__FKudG3uS8A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+FKudG3uS8A", sceNKWebKitUnknown__FKudG3uS8A_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__FWUl_T8yG8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+FWUl-T8yG8", sceNKWebKitUnknown__FWUl_T8yG8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Fca5Eyr4BU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Fca5Eyr4BU", sceNKWebKitUnknown__Fca5Eyr4BU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__FcvpbbdrZM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+FcvpbbdrZM", sceNKWebKitUnknown__FcvpbbdrZM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__FvUhrQaiJQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+FvUhrQaiJQ", sceNKWebKitUnknown__FvUhrQaiJQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__GOwQvxqztE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+GOwQvxqztE", sceNKWebKitUnknown__GOwQvxqztE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Gp0M0sPj9I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Gp0M0sPj9I", sceNKWebKitUnknown__Gp0M0sPj9I_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__H_bnpXdWtk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+H-bnpXdWtk", sceNKWebKitUnknown__H_bnpXdWtk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__H5PTMieXNE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+H5PTMieXNE", sceNKWebKitUnknown__H5PTMieXNE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__HuPyGuX_jY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+HuPyGuX+jY", sceNKWebKitUnknown__HuPyGuX_jY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__IMGO_dXHis_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+IMGO+dXHis", sceNKWebKitUnknown__IMGO_dXHis_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ImJ026_DAw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ImJ026-DAw", sceNKWebKitUnknown__ImJ026_DAw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__IoqHgIw0PM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+IoqHgIw0PM", sceNKWebKitUnknown__IoqHgIw0PM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__JnngM7WyHI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+JnngM7WyHI", sceNKWebKitUnknown__JnngM7WyHI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Kb7j57jDzs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Kb7j57jDzs", sceNKWebKitUnknown__Kb7j57jDzs_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Kx_IV_21hk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Kx-IV+21hk", sceNKWebKitUnknown__Kx_IV_21hk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__LUabdo811U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+LUabdo811U", sceNKWebKitUnknown__LUabdo811U_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__LiOmqon2ZI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+LiOmqon2ZI", sceNKWebKitUnknown__LiOmqon2ZI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__LoAenYdWlM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+LoAenYdWlM", sceNKWebKitUnknown__LoAenYdWlM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__LrmM3KN3N8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+LrmM3KN3N8", sceNKWebKitUnknown__LrmM3KN3N8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__MzGFuNN348_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+MzGFuNN348", sceNKWebKitUnknown__MzGFuNN348_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__N_GwpCV4yk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+N-GwpCV4yk", sceNKWebKitUnknown__N_GwpCV4yk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__N6UhRaLrJE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+N6UhRaLrJE", sceNKWebKitUnknown__N6UhRaLrJE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__NGrXdCa0q8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+NGrXdCa0q8", sceNKWebKitUnknown__NGrXdCa0q8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__NiLTGGKF_E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+NiLTGGKF+E", sceNKWebKitUnknown__NiLTGGKF_E_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Nqfy8HVG84_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Nqfy8HVG84", sceNKWebKitUnknown__Nqfy8HVG84_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__NuhuZH75ak_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+NuhuZH75ak", sceNKWebKitUnknown__NuhuZH75ak_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__OCxbaKiBUo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+OCxbaKiBUo", sceNKWebKitUnknown__OCxbaKiBUo_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__OOjOkuWL6g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+OOjOkuWL6g", sceNKWebKitUnknown__OOjOkuWL6g_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__OkTn3_PBxM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+OkTn3+PBxM", sceNKWebKitUnknown__OkTn3_PBxM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__PDx3sfB_l4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+PDx3sfB-l4", sceNKWebKitUnknown__PDx3sfB_l4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__POsWAWjXo8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+POsWAWjXo8", sceNKWebKitUnknown__POsWAWjXo8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__PeIrQMgYpw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+PeIrQMgYpw", sceNKWebKitUnknown__PeIrQMgYpw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Q7xLvpc0PA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Q7xLvpc0PA", sceNKWebKitUnknown__Q7xLvpc0PA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__QL3zQ6ZNy4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+QL3zQ6ZNy4", sceNKWebKitUnknown__QL3zQ6ZNy4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__QqzEnc5c80_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+QqzEnc5c80", sceNKWebKitUnknown__QqzEnc5c80_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__R0GXLrsPM0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+R0GXLrsPM0", sceNKWebKitUnknown__R0GXLrsPM0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__R2eXISeSvw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+R2eXISeSvw", sceNKWebKitUnknown__R2eXISeSvw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__RkwSDm5kGU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+RkwSDm5kGU", sceNKWebKitUnknown__RkwSDm5kGU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__RtBREW92M0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+RtBREW92M0", sceNKWebKitUnknown__RtBREW92M0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__S72K_fa2_8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+S72K+fa2-8", sceNKWebKitUnknown__S72K_fa2_8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__S7ftAAWhoQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+S7ftAAWhoQ", sceNKWebKitUnknown__S7ftAAWhoQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__SHzgUiuQDE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+SHzgUiuQDE", sceNKWebKitUnknown__SHzgUiuQDE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Sgy63JgEgQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Sgy63JgEgQ", sceNKWebKitUnknown__Sgy63JgEgQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__Sozphy1Wa8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+Sozphy1Wa8", sceNKWebKitUnknown__Sozphy1Wa8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__TPntL1OK80_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+TPntL1OK80", sceNKWebKitUnknown__TPntL1OK80_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__TT_AplhQWA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+TT+AplhQWA", sceNKWebKitUnknown__TT_AplhQWA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__TjNmuhxdBQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+TjNmuhxdBQ", sceNKWebKitUnknown__TjNmuhxdBQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__U_nxwdrnDY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+U-nxwdrnDY", sceNKWebKitUnknown__U_nxwdrnDY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__UKI_MUuiCU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+UKI-MUuiCU", sceNKWebKitUnknown__UKI_MUuiCU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__VvktcfIouQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+VvktcfIouQ", sceNKWebKitUnknown__VvktcfIouQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__WS2yRv5aBI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+WS2yRv5aBI", sceNKWebKitUnknown__WS2yRv5aBI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__XZfnHByVO0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+XZfnHByVO0", sceNKWebKitUnknown__XZfnHByVO0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__YBfWz5udq4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+YBfWz5udq4", sceNKWebKitUnknown__YBfWz5udq4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__YPwmEWTZwE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+YPwmEWTZwE", sceNKWebKitUnknown__YPwmEWTZwE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__YYEOsi3IMk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+YYEOsi3IMk", sceNKWebKitUnknown__YYEOsi3IMk_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__YyqQTUIQJQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+YyqQTUIQJQ", sceNKWebKitUnknown__YyqQTUIQJQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ZgGbFykhls_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ZgGbFykhls", sceNKWebKitUnknown__ZgGbFykhls_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ZwjLJ74Y38_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ZwjLJ74Y38", sceNKWebKitUnknown__ZwjLJ74Y38_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__b1xwU7aUwU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+b1xwU7aUwU", sceNKWebKitUnknown__b1xwU7aUwU_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__b5TfVQ3_4s_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+b5TfVQ3-4s", sceNKWebKitUnknown__b5TfVQ3_4s_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__bDb7iby5NI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+bDb7iby5NI", sceNKWebKitUnknown__bDb7iby5NI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__baqz7ahOng_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+baqz7ahOng", sceNKWebKitUnknown__baqz7ahOng_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__bvtn3pCtek_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+bvtn3pCtek", sceNKWebKitUnknown__bvtn3pCtek_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__byORTSkWQo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+byORTSkWQo", sceNKWebKitUnknown__byORTSkWQo_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__dAwquupevQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+dAwquupevQ", sceNKWebKitUnknown__dAwquupevQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__dTlroLZao8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+dTlroLZao8", sceNKWebKitUnknown__dTlroLZao8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__eVN0UdRT8c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+eVN0UdRT8c", sceNKWebKitUnknown__eVN0UdRT8c_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__fTKZiGqo8E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+fTKZiGqo8E", sceNKWebKitUnknown__fTKZiGqo8E_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__feeFP8Y2_c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+feeFP8Y2+c", sceNKWebKitUnknown__feeFP8Y2_c_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__iLBOBrdx5A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+iLBOBrdx5A", sceNKWebKitUnknown__iLBOBrdx5A_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__iLdIVuhDhY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+iLdIVuhDhY", sceNKWebKitUnknown__iLdIVuhDhY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__jG4wqqA8yg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+jG4wqqA8yg", sceNKWebKitUnknown__jG4wqqA8yg_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__jLvgfDA97c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+jLvgfDA97c", sceNKWebKitUnknown__jLvgfDA97c_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ji7IBGwnCM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ji7IBGwnCM", sceNKWebKitUnknown__ji7IBGwnCM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__k3JGU_UUXo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+k3JGU-UUXo", sceNKWebKitUnknown__k3JGU_UUXo_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__kX_CdbguB0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+kX-CdbguB0", sceNKWebKitUnknown__kX_CdbguB0_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__keSg8ay8cE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+keSg8ay8cE", sceNKWebKitUnknown__keSg8ay8cE_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__lI9zCCS_HQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+lI9zCCS-HQ", sceNKWebKitUnknown__lI9zCCS_HQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__lwLqZJLo3g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+lwLqZJLo3g", sceNKWebKitUnknown__lwLqZJLo3g_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__mS029ncrbo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+mS029ncrbo", sceNKWebKitUnknown__mS029ncrbo_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__mYy22Ucg7Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+mYy22Ucg7Y", sceNKWebKitUnknown__mYy22Ucg7Y_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__nHWcLWOmb4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+nHWcLWOmb4", sceNKWebKitUnknown__nHWcLWOmb4_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__nSg5kHtXkg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+nSg5kHtXkg", sceNKWebKitUnknown__nSg5kHtXkg_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__nc3IouJMEA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+nc3IouJMEA", sceNKWebKitUnknown__nc3IouJMEA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ndmq8LhAjA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ndmq8LhAjA", sceNKWebKitUnknown__ndmq8LhAjA_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__nrEMsyYHiw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+nrEMsyYHiw", sceNKWebKitUnknown__nrEMsyYHiw_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__oJHrfcyJJM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+oJHrfcyJJM", sceNKWebKitUnknown__oJHrfcyJJM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__ovAafdG0O8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+ovAafdG0O8", sceNKWebKitUnknown__ovAafdG0O8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__pWGccNXto8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+pWGccNXto8", sceNKWebKitUnknown__pWGccNXto8_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__q0GELad_mY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+q0GELad-mY", sceNKWebKitUnknown__q0GELad_mY_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__qXuNlEFSRQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+qXuNlEFSRQ", sceNKWebKitUnknown__qXuNlEFSRQ_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__qsTbJFS86A_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+qsTbJFS86A", sceNKWebKitUnknown__qsTbJFS86A_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__r_TJDA2yIM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+r+TJDA2yIM", sceNKWebKitUnknown__r_TJDA2yIM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__r0UH41I1dI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+r0UH41I1dI", sceNKWebKitUnknown__r0UH41I1dI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__r8OGkuL4dc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+r8OGkuL4dc", sceNKWebKitUnknown__r8OGkuL4dc_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__rFA8ZR_qaI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+rFA8ZR+qaI", sceNKWebKitUnknown__rFA8ZR_qaI_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__rPvFDsPbXM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+rPvFDsPbXM", sceNKWebKitUnknown__rPvFDsPbXM_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__sUvE9LF80k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+sUvE9LF80k", sceNKWebKitUnknown__sUvE9LF80k_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__suuwfNT5Oc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+suuwfNT5Oc", sceNKWebKitUnknown__suuwfNT5Oc_nid_no_patch); + +int APS5_VABI sceNKWebKitUnknown__t_MaTLADe4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+t-MaTLADe4", sceNKWebKitUnknown__t_MaTLADe4_nid_no_patch); + +} diff --git a/core/libs/prx/libSceNKWebKitRequirements/Export.cpp b/core/libs/prx/libSceNKWebKitRequirements/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceNKWebKitRequirements/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index 98776d4b5e..9ecb525065 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -1,8 +1,47 @@ add_library(libSceNet SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceNet PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceNet PRIVATE libSceNetCtl) configure_windows_unwind(libSceNet) if(BUILD_TESTING) + add_test_executable(net_dontwait_tests ${LIBS_INCLUDE_DIR}/tests/NetDontWait.cpp) + target_include_directories(net_dontwait_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(net_dontwait_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(net_dontwait_tests) + add_test_executable(guest_net_buffer_validation_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetBufferValidation.cpp) + target_include_directories(guest_net_buffer_validation_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_buffer_validation_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_net_buffer_validation_tests) + add_test(NAME guest_net_buffer_validation COMMAND guest_net_buffer_validation_tests) + set_tests_properties(guest_net_buffer_validation PROPERTIES TIMEOUT 20) + if(WIN32) + target_link_libraries(guest_net_buffer_validation_tests PRIVATE ws2_32) + set_tests_properties(guest_net_buffer_validation PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + foreach(api IN ITEMS send sendto sendmsg recv recvfrom recvmsg duplex blocking) + add_test(NAME net_dontwait_${api} COMMAND net_dontwait_tests ${api}) + set_tests_properties(net_dontwait_${api} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(net_dontwait_${api} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test_executable(net_blocking_send_tests ${LIBS_INCLUDE_DIR}/tests/NetBlockingSend.cpp) + target_include_directories(net_blocking_send_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(net_blocking_send_tests PRIVATE libkernel libc libSceNetCtl) + configure_windows_unwind(net_blocking_send_tests) + if(WIN32) + target_link_libraries(net_blocking_send_tests PRIVATE ws2_32) + endif() + foreach(api IN ITEMS send sendto sendmsg) + add_test(NAME net_dontwait_blocking_${api} COMMAND net_blocking_send_tests ${api}) + set_tests_properties(net_dontwait_blocking_${api} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(net_dontwait_blocking_${api} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(guest_datagram_receive_tests ${LIBS_INCLUDE_DIR}/tests/GuestDatagramReceive.cpp) target_include_directories(guest_datagram_receive_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_datagram_receive_tests PRIVATE libSceNet libkernel libc) @@ -15,12 +54,22 @@ 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}) target_link_libraries(guest_socket_host_write_tests PRIVATE libSceNet libkernel libc) configure_windows_unwind(guest_socket_host_write_tests) - foreach(api IN ITEMS recv recvfrom read sceKernelRead sceNetRecv sceNetRecvfrom) + foreach(api IN ITEMS recv recvfrom read sceKernelRead sceNetRecv sceNetRecvfrom sceNetRecvmsg) add_test(NAME guest_socket_host_write_${api}_udp COMMAND guest_socket_host_write_tests ${api}) add_test(NAME guest_socket_host_write_${api}_tcp COMMAND guest_socket_host_write_tests ${api} tcp) set_tests_properties(guest_socket_host_write_${api}_udp guest_socket_host_write_${api}_tcp @@ -39,4 +88,19 @@ if(BUILD_TESTING) set_tests_properties(guest_sce_net PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(net_epoll_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/NetEpollLifetime.cpp) + target_include_directories(net_epoll_lifetime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(net_epoll_lifetime_tests PRIVATE libkernel libc libSceNetCtl) + configure_windows_unwind(net_epoll_lifetime_tests) + if(WIN32) + target_link_libraries(net_epoll_lifetime_tests PRIVATE ws2_32) + endif() + foreach(operation IN ITEMS reuse control close delete modify replace abort destroy) + add_test(NAME net_epoll_lifetime_${operation} COMMAND net_epoll_lifetime_tests ${operation}) + set_tests_properties(net_epoll_lifetime_${operation} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(net_epoll_lifetime_${operation} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() endif() diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index e03f88e831..450868e186 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -23,10 +24,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -35,10 +38,192 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestHeap.hpp" +#include "prx/libkernel/Socket/include/SocketError.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include "prx/libkernel/Time/include/Time.hpp" +#include "src/ResolverTestHooks.hpp" // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. +struct NetWlanInformationElement; +struct NetWlanWakeParameter; +struct NetWlanScanParameter; +struct NetWlanScanResult; +struct NetWlanAdhocScanParameter; +struct NetWlanInfraScanParameter; +struct NetWlanJoinResult; +struct NetDumpFilterParameter; + +struct NetWlanAdhocParameter; +struct NetWlanWakeInfo; +struct NetWlanPspWakeInfo; +struct NetWlanPspWakeParameter; +struct NetWlanApParameter; +struct NetWlanDeviceInfo; +struct NetWlanAntennaParameter; +struct NetWlanRateParameter; +struct NetWlanDeviceConfiguration; +struct NetWlanRssiInfo; +struct NetDhcpInfo; +struct NetAutoIpInfo; +struct NetAddress6Info; + +struct NetDhcpOptionInfo { + std::uint32_t option; + std::uint32_t capacity; + void* data; + std::uint32_t size; + std::uint32_t reserved; +}; + +static_assert(sizeof(NetDhcpOptionInfo) == 24); +static_assert(offsetof(NetDhcpOptionInfo, capacity) == 4); +static_assert(offsetof(NetDhcpOptionInfo, data) == 8); +static_assert(offsetof(NetDhcpOptionInfo, size) == 16); + +struct NetSockInfo6; +struct NetStatisticsInfo; +struct NetInterfaceStats; +struct NetRouteInfo; +struct NetDebugParameter; +struct NetArpInfo; +struct NetAppMemoryInfo; +struct NetSystemMemoryInfo; +struct NetBandwidthParameter; +struct NetDataTraffic; +struct NetEmulationParameter; + +struct NetResolverConnection; +struct NetThreadParameter; +struct NetIpv4InterfaceAddress; +struct NetStatisticsInfoInternal; +struct NetInitializeParameter; +struct NetDhcpStartParameter; +struct NetAddress6StartParameter; + +using NetAddressCallback = void (APS5_VABI *)(void* userArg, std::int32_t event, std::int32_t error); + +struct NetDhcpdStartParameter { + std::uint32_t addresses[5]; + std::uint32_t maxClients; +}; + +struct NetPppoeStartParameter { + std::uint32_t dnsAddresses[2]; + char userName[128]; + char password[128]; + std::int32_t mtu; + std::uint32_t flags; +}; + +struct NetRoutingAddressParameter { + std::uint32_t address; + std::uint32_t mask; +}; + +struct NetRoutingServerParameter { + std::int32_t count; + std::uint32_t localAddresses[2]; + std::uint32_t remoteAddresses[2]; +}; + +struct NetRoutingRule { + std::int32_t protocol; + std::uint32_t firstAddress; + std::uint16_t firstPort; + std::uint16_t reserved; + std::uint32_t secondAddress; + std::uint16_t secondPort; + std::uint16_t flags; +}; + +struct NetRoutingRuleParameter { + std::int32_t count; + const NetRoutingRule* rules; +}; + +static_assert(sizeof(NetRoutingAddressParameter) == 8); +static_assert(sizeof(NetRoutingServerParameter) == 20); +static_assert(offsetof(NetRoutingServerParameter, remoteAddresses) == 12); +static_assert(sizeof(NetRoutingRule) == 20); +static_assert(offsetof(NetRoutingRule, firstPort) == 8); +static_assert(offsetof(NetRoutingRule, secondAddress) == 12); +static_assert(offsetof(NetRoutingRule, flags) == 18); +static_assert(sizeof(NetRoutingRuleParameter) == 16); +static_assert(offsetof(NetRoutingRuleParameter, rules) == 8); + +static_assert(sizeof(NetDhcpdStartParameter) == 24); +static_assert(offsetof(NetDhcpdStartParameter, maxClients) == 20); +static_assert(sizeof(NetPppoeStartParameter) == 272); +static_assert(offsetof(NetPppoeStartParameter, userName) == 8); +static_assert(offsetof(NetPppoeStartParameter, password) == 136); +static_assert(offsetof(NetPppoeStartParameter, mtu) == 264); +static_assert(offsetof(NetPppoeStartParameter, flags) == 268); + +struct NetInfoDumpParameter { + std::uint32_t flags; + std::uint32_t intervalSeconds; + char path[64]; +}; + +static_assert(sizeof(NetInfoDumpParameter) == 72); +static_assert(offsetof(NetInfoDumpParameter, path) == 8); + +struct NetInterfaceInfo; +struct NetInterfaceStatistics; + +struct NetResolverConnectOption { + std::int32_t connectionTimeout; + std::int32_t resolverTimeout; + std::int32_t resolverRetryCount; + std::uint32_t reserved; +}; + +static_assert(sizeof(NetResolverConnectOption) == 16); +static_assert(offsetof(NetResolverConnectOption, resolverTimeout) == 4); +static_assert(offsetof(NetResolverConnectOption, resolverRetryCount) == 8); +static_assert(offsetof(NetResolverConnectOption, reserved) == 12); + +struct NetSyncObject { + void* mutex; + void* condition; + std::uint32_t signals; + std::uint32_t reserved; +}; + +static_assert(sizeof(NetSyncObject) == 24); + +using NetEventCallback = std::uint64_t (APS5_VABI*)(std::uint32_t interfaceId, std::uint64_t events, void* userData, void* waitData); + +struct NetIpv6Address { + std::uint8_t bytes[16]; +}; + +static_assert(sizeof(NetIpv6Address) == 16); + +struct NetDnsInfo { + std::uint32_t primaryAddress; + std::uint32_t secondaryAddress; +}; + +struct NetDns6Info { + NetIpv6Address primaryAddress; + NetIpv6Address secondaryAddress; +}; + +static_assert(sizeof(NetDnsInfo) == 8); +static_assert(sizeof(NetDns6Info) == 32); + +struct NetIpv6InterfaceAddress { + NetIpv6Address address; + std::uint8_t prefixLength; + std::uint8_t reserved[3]; +}; + +static_assert(sizeof(NetIpv6InterfaceAddress) == 20); + namespace { // FreeBSD errno numbers as reported through sceNetErrnoLoc (SCE_NET_ERROR_* is 0x80410100 + errno). constexpr int NET_ENOENT = 2; @@ -53,6 +238,7 @@ constexpr int NET_EAGAIN = 35; constexpr int NET_EINPROGRESS = 36; constexpr int NET_EALREADY = 37; constexpr int NET_ENOTSOCK = 38; +constexpr int NET_ENOPROTOOPT = 42; constexpr int NET_EOPNOTSUPP = 45; constexpr int NET_EPROTONOSUPPORT = 43; constexpr int NET_EAFNOSUPPORT = 47; @@ -67,6 +253,7 @@ constexpr int NET_EMSGSIZE = 40; constexpr int NET_ETIMEDOUT = 60; constexpr int NET_ECONNREFUSED = 61; constexpr int NET_EHOSTUNREACH = 65; +constexpr int NET_ENOTINIT = 200; constexpr int NET_ERROR_BASE = static_cast(0x80410100u); constexpr int NET_ERROR_RESOLVER_ENODNS = static_cast(0x804101E1u); constexpr int NET_RESOLVER_ABORT_FLAG_NTOA_PRESERVATION = 0x1; @@ -81,12 +268,16 @@ constexpr int NET_EPOLL_CTL_ADD = 1; constexpr int NET_EPOLL_CTL_MOD = 2; constexpr int NET_EPOLL_CTL_DEL = 3; constexpr int NET_SOL_SOCKET = 0xFFFF; +constexpr int NET_IPPROTO_TCP = 6; +constexpr int NET_TCP_NODELAY = 1; +constexpr int NET_TF_NODELAY = 4; constexpr int NET_SO_SNDTIMEO = 0x1005; constexpr int NET_SO_RCVTIMEO = 0x1006; constexpr int NET_SO_ERROR = 0x1007; 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; @@ -109,32 +300,7 @@ bool initialize_sockets() { return result == 0; } void close_socket(NativeSocket socket) { closesocket(socket); } -int net_error(int code) { - switch (code) { - case WSAEWOULDBLOCK: return NET_EAGAIN; - case WSAEINPROGRESS: return NET_EINPROGRESS; - case WSAEALREADY: return NET_EALREADY; - case WSAEADDRINUSE: return NET_EADDRINUSE; - case WSAEADDRNOTAVAIL: return NET_EADDRNOTAVAIL; - case WSAEAFNOSUPPORT: return NET_EAFNOSUPPORT; - case WSAENETUNREACH: return NET_ENETUNREACH; - case WSAEHOSTUNREACH: return NET_EHOSTUNREACH; - case WSAECONNABORTED: return NET_ECONNABORTED; - case WSAENOBUFS: return NET_ENOBUFS; - case WSAEISCONN: return NET_EISCONN; - case WSAENOTCONN: return NET_ENOTCONN; - case WSAEACCES: return NET_EACCES; - case WSAECONNRESET: return 54; - case WSAETIMEDOUT: return NET_ETIMEDOUT; - case WSAECONNREFUSED: return NET_ECONNREFUSED; - case WSAEMSGSIZE: return NET_EMSGSIZE; - case WSAEINTR: return 4; - case WSAEINVAL: return NET_EINVAL; - case WSAENOTSOCK: return NET_ENOTSOCK; - case WSAESHUTDOWN: return 32; - default: return 5; - } -} +int net_error(int code) { return GuestSockets::GuestErrorFromHost(code); } int native_error() { return net_error(WSAGetLastError()); } #else using NativeSocket = int; @@ -143,41 +309,25 @@ constexpr NativeSocket INVALID_NATIVE_SOCKET = -1; constexpr int NATIVE_SEND_FLAGS = MSG_NOSIGNAL; bool initialize_sockets() { return true; } void close_socket(NativeSocket socket) { ::close(socket); } -int net_error(int code) { - switch (code) { - case EAGAIN: return NET_EAGAIN; -#if EWOULDBLOCK != EAGAIN - case EWOULDBLOCK: return NET_EAGAIN; -#endif - case EINPROGRESS: return NET_EINPROGRESS; - case EALREADY: return NET_EALREADY; - case EADDRINUSE: return NET_EADDRINUSE; - case EADDRNOTAVAIL: return NET_EADDRNOTAVAIL; - case EAFNOSUPPORT: return NET_EAFNOSUPPORT; - case ENETUNREACH: return NET_ENETUNREACH; - case EHOSTUNREACH: return NET_EHOSTUNREACH; - case ECONNABORTED: return NET_ECONNABORTED; - case ENOBUFS: return NET_ENOBUFS; - case EISCONN: return NET_EISCONN; - case ENOTCONN: return NET_ENOTCONN; - case EACCES: return NET_EACCES; - case ECONNRESET: return 54; - case ETIMEDOUT: return NET_ETIMEDOUT; - case ECONNREFUSED: return NET_ECONNREFUSED; - case EMSGSIZE: return NET_EMSGSIZE; - case EINTR: return 4; - case EINVAL: return NET_EINVAL; - case ENOTSOCK: return NET_ENOTSOCK; - case EPIPE: return 32; - default: return 5; - } -} +int net_error(int code) { return GuestSockets::GuestErrorFromHost(code); } int native_error() { return net_error(errno); } #endif -struct NativeSocketHandle { +#ifdef _WIN32 +int TransferSocketForKernel(KernelSocketPoll::NativeOwner* owner, bool send, void* buffer, int length); +#endif + +struct NativeSocketHandle : KernelSocketPoll::NativeOwner { NativeSocket value; - explicit NativeSocketHandle(NativeSocket socket) : value(socket) {} + const bool stream; + std::atomic nonblock{false}; + std::atomic receiveTimeout{0}; + std::atomic sendTimeout{0}; + NativeSocketHandle(NativeSocket socket, bool stream) : value(socket), stream(stream) { +#ifdef _WIN32 + transfer = &TransferSocketForKernel; +#endif + } ~NativeSocketHandle() { if (value != INVALID_NATIVE_SOCKET) close_socket(value); } @@ -242,13 +392,14 @@ struct Sock { int family = 0; int type = 0; std::shared_ptr native; - bool nonblock = false; bool bound = false; bool listening = false; bool aborted = false; - int rcv_timeout_us = 0; - int snd_timeout_us = 0; + bool read_shut = false; int reported_error = 0; + int snd_buffer = 0; + int rcv_buffer = 0; + int onesbcast = 0; }; #ifdef _WIN32 @@ -305,7 +456,8 @@ HostMutex g_mutex; HostCondition g_cv; std::map g_socks; std::set g_epolls; -std::map> g_epoll_socks; +using EpollRegistrations = std::map>; +std::map g_epoll_socks; std::set g_epoll_aborted; std::map g_pools; @@ -327,6 +479,7 @@ int g_next_epoll = 0x4000; int g_next_pool = 1; int g_next_resolver = 0x100; bool g_net_inited = false; +std::atomic g_resolverLookupForTests{nullptr}; int* errno_slot() { static thread_local int e = 0; @@ -334,6 +487,7 @@ int* errno_slot() { } extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" std::size_t NetCtlHostDnsServers_nid_no_patch(std::uint32_t* servers, std::size_t capacity); void set_errno(int err) { *errno_slot() = err; @@ -378,8 +532,8 @@ int set_kernel_timeout(int descriptor, int optname, const void* value, std::uint const auto it = g_socks.find(descriptor); if (it != g_socks.end()) { const int stored = static_cast(std::min(microseconds, INT_MAX)); - if (optname == NET_SO_RCVTIMEO) it->second.rcv_timeout_us = stored; - else it->second.snd_timeout_us = stored; + if (optname == NET_SO_RCVTIMEO) it->second.native->receiveTimeout = stored; + else it->second.native->sendTimeout = stored; } return 0; } @@ -465,22 +619,64 @@ std::int64_t message_length(const NetMsghdr* message) { return static_cast(total); } -int receive_error(const Sock& socket) { -#ifdef _WIN32 - if (WSAGetLastError() == WSAETIMEDOUT && socket.type != NET_SOCK_STREAM && socket.rcv_timeout_us > 0) return NET_EAGAIN; -#else - (void)socket; -#endif - return native_error(); -} - bool message_flags_supported(int flags, int accepted) { -#ifdef _WIN32 - accepted &= ~NET_MSG_DONTWAIT; -#endif return (flags & ~accepted) == 0; } +template +auto native_transfer(const NativeSocketHandle& socket, int flags, bool send, TOperation operation, bool timed = true, int length = 0) { +#ifdef _WIN32 + using Result = decltype(operation(0)); + const bool nonblocking = (flags & NET_MSG_DONTWAIT) != 0 || socket.nonblock.load(); + const int completeLength = send && socket.stream && !nonblocking ? length : 0; + const int timeout = timed ? (send ? socket.sendTimeout.load() : socket.receiveTimeout.load()) : 0; + const auto deadline = timeout > 0 ? std::chrono::steady_clock::now() + std::chrono::microseconds(timeout) + : std::chrono::steady_clock::time_point::max(); + int transferred = 0; + for (;;) { + const auto result = operation(transferred); + if (result != static_cast(-1)) { + if (completeLength == 0) return result; + transferred += static_cast(result); + if (transferred == completeLength || result == 0) return static_cast(transferred); + } else if (WSAGetLastError() != WSAEWOULDBLOCK || nonblocking) { + return transferred > 0 ? static_cast(transferred) : result; + } + int milliseconds = 10; + if (timeout > 0) { + const auto remaining = deadline - std::chrono::steady_clock::now(); + if (remaining <= std::chrono::steady_clock::duration::zero()) { + WSASetLastError(send && completeLength == 0 ? WSAETIMEDOUT : WSAEWOULDBLOCK); + return transferred > 0 ? static_cast(transferred) : static_cast(-1); + } + milliseconds = static_cast(std::min(std::chrono::ceil(remaining).count(), milliseconds)); + } + WSAPOLLFD descriptor{}; + descriptor.fd = socket.value; + descriptor.events = send ? POLLWRNORM : POLLRDNORM; + const int ready = WSAPoll(&descriptor, 1, milliseconds); + if (ready < 0) return transferred > 0 ? static_cast(transferred) : static_cast(-1); + } +#else + static_cast(socket); + static_cast(flags); + static_cast(send); + static_cast(timed); + static_cast(length); + return operation(0); +#endif +} + +#ifdef _WIN32 +int TransferSocketForKernel(KernelSocketPoll::NativeOwner* owner, bool send, void* buffer, int length) { + const auto* socket = static_cast(owner); + return native_transfer(*socket, 0, send, [&](int offset) { + return send ? ::send(socket->value, offset ? static_cast(buffer) + offset : static_cast(buffer), length - offset, 0) + : ::recv(socket->value, static_cast(buffer), length, 0); + }, true, length); +} +#endif + int native_message_flags(int flags) { int native = (flags & NET_MSG_PEEK) ? MSG_PEEK : 0; #ifndef _WIN32 @@ -489,8 +685,106 @@ 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, int flags) { + if (socket.type == NET_SOCK_STREAM || socket.bound) return 0; + const bool named = has_local_address(socket.native->value); + if (!named && (socket.read_shut || empty)) return RECEIVE_NOTHING; + if (!named && (socket.native->nonblock || (flags & NET_MSG_DONTWAIT) != 0)) return NET_EAGAIN; + const int timeout = socket.native->receiveTimeout; + const auto deadline = timeout > 0 + ? std::chrono::steady_clock::now() + std::chrono::microseconds(timeout) + : std::chrono::steady_clock::time_point::max(); + std::unique_lock lk(g_mutex); + for (bool waited = false;; waited = true) { + 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 + +bool parse_ipv4_parts(const char* text, std::uint32_t& address) { + std::array parts{}; + std::size_t count = 0; + std::uint32_t value = 0; + for (;;) { + if (*text < '0' || *text > '9') return false; + std::uint32_t base = 10; + if (*text == '0') { + ++text; + if (*text == 'x' || *text == 'X') { + base = 16; + ++text; + } else if (*text != '9') { + base = 8; + } + } + value = 0; + for (;; ++text) { + const char c = *text; + if (c >= '0' && c <= '9') { + if (base == 8 && c >= '8') return false; + value = value * base + static_cast(c - '0'); + } else if (base == 16 && ((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + value = value * 16 + static_cast((c | 0x20) - 'a' + 10); + } else { + break; + } + } + if (*text != '.') break; + if (count == parts.size() || value > 0xff) return false; + parts[count++] = value; + ++text; + } + if (*text != '\0' && std::strchr(" \t\n\v\f\r", *text) == nullptr) return false; + if (value > (0xffffffffu >> (8 * count))) return false; + for (std::size_t i = 0; i < count; ++i) value |= parts[i] << (24 - 8 * i); + address = value; + return true; +} + } // namespace +void GuestSceNetTest::SetResolverLookupForTests_nid_no_patch(ResolverLookup lookup) noexcept { + g_resolverLookupForTests.store(lookup); +} + extern "C" { extern const std::uint32_t sce_net_in6addr_any[4] = {}; @@ -501,8 +795,9 @@ int* APS5_VABI sceNetErrnoLoc(void) { } int APS5_VABI sceNetShowNetstat(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!g_net_inited) return fail(NET_ENOTINIT); + throw std::runtime_error(__func__); } int PollSockets(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds) { @@ -595,6 +890,52 @@ bool SetSocketOptionForKernel(int descriptor, int level, int option, const void* const bool g_socketOptionSetterRegistered = (KernelSetSocketOptionSetter_nid_no_patch(&SetSocketOptionForKernel), true); +constexpr int GuestFcntlGetStatus = 3; +constexpr int GuestFcntlSetStatus = 4; +constexpr int GuestReadWrite = 2; +constexpr int GuestNonblocking = 4; +constexpr int GuestStatusFlagsMask = 7; + +bool ControlSocketStatusForKernel(int descriptor, int command, int flags, int* result, int* error) { + bool nonblock = false; + { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(descriptor); + if (it == g_socks.end()) return false; + nonblock = it->second.native->nonblock; + } + *result = 0; + *error = 0; + if (command == GuestFcntlGetStatus) { + *result = GuestReadWrite | (nonblock ? GuestNonblocking : 0); + return true; + } + if (command != GuestFcntlSetStatus) { + *error = NET_EINVAL; + return true; + } + if ((flags & ~GuestStatusFlagsMask) != 0) { + *error = NET_EOPNOTSUPP; + return true; + } + const int enabled = (flags & GuestNonblocking) != 0 ? 1 : 0; + if (sceNetSetsockopt(descriptor, NET_SOL_SOCKET, NET_SO_NBIO, &enabled, sizeof(enabled)) != 0) *error = *errno_slot(); + return true; +} + +const bool g_socketStatusControlRegistered = (KernelSetSocketStatusControl_nid_no_patch(&ControlSocketStatusForKernel), true); + +bool FindSocketNativeForKernel(int descriptor, std::shared_ptr* owner, std::uintptr_t* native) { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(descriptor); + if (it == g_socks.end() || !it->second.native) return false; + *native = static_cast(it->second.native->value); + *owner = std::static_pointer_cast(it->second.native); + return true; +} + +const bool g_socketNativeLookupRegistered = (KernelSetSocketNativeLookup_nid_no_patch(&FindSocketNativeForKernel), true); + int APS5_VABI sceNetInit_nid_postfix(void) { std::lock_guard lk(g_mutex); g_net_inited = true; @@ -635,6 +976,14 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) if (!initialize_sockets()) return fail(5); const NativeSocket native = ::socket(family == NET_AF_INET ? AF_INET : AF_INET6, type, protocol); if (native == INVALID_NATIVE_SOCKET) return fail(native_error()); +#ifdef _WIN32 + u_long nonblocking = 1; + if (ioctlsocket(native, FIONBIO, &nonblocking) != 0) { + const int error = native_error(); + close_socket(native); + return fail(error); + } +#endif const int ipv6_only = 1; if (family == NET_AF_INET6 && ::setsockopt(native, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&ipv6_only), sizeof(ipv6_only)) != 0) { @@ -644,7 +993,7 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) } std::shared_ptr handle; try { - handle = std::make_shared(native); + handle = std::make_shared(native, type == NET_SOCK_STREAM); } catch (const std::bad_alloc&) { close_socket(native); return fail(12); @@ -660,14 +1009,24 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) return fd; } +extern "C" int APS5_VABI close_nid_postfix(int d); + int APS5_VABI sceNetSocketClose(int s) { std::shared_ptr native; + bool found = false; { std::lock_guard lk(g_mutex); const auto it = g_socks.find(s); - if (it == g_socks.end()) return fail(NET_EBADF); - native = it->second.native; - g_socks.erase(it); + if (it != g_socks.end()) { + native = it->second.native; + g_socks.erase(it); + for (auto& epoll : g_epoll_socks) epoll.second.erase(s); + found = true; + } + } + if (!found) { + if (!GuestSocketIsOpen_nid_no_patch(s)) return fail(NET_EBADF); + return close_nid_postfix(s) == 0 ? 0 : fail(*__error_nid_postfix()); } KernelReleaseSocketNumber_nid_no_patch(s); if (native) { @@ -718,6 +1077,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; } @@ -752,12 +1112,15 @@ int APS5_VABI sceNetAccept(int s, void* addr, uint32_t* addrlen) { if (addr && !addrlen) return fail(NET_EINVAL); sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); - const NativeSocket accepted = ::accept(listener.native->value, - addr ? reinterpret_cast(&peer) : nullptr, addr ? &peer_length : nullptr); + const NativeSocket accepted = native_transfer(*listener.native, 0, false, [&](int) { + peer_length = sizeof(peer); + return ::accept(listener.native->value, + addr ? reinterpret_cast(&peer) : nullptr, addr ? &peer_length : nullptr); + }, false); if (accepted == INVALID_NATIVE_SOCKET) return fail(native_error()); std::shared_ptr handle; try { - handle = std::make_shared(accepted); + handle = std::make_shared(accepted, true); } catch (const std::bad_alloc&) { close_socket(accepted); return fail(12); @@ -766,12 +1129,24 @@ int APS5_VABI sceNetAccept(int s, void* addr, uint32_t* addrlen) { connection.family = listener.family; connection.type = NET_SOCK_STREAM; connection.native = std::move(handle); +#ifdef _WIN32 + connection.native->receiveTimeout = listener.native->receiveTimeout.load(); + connection.native->sendTimeout = listener.native->sendTimeout.load(); +#endif const int descriptor = KernelReserveSocketNumber_nid_no_patch(static_cast(connection.native->value)); if (descriptor < 0) return fail(-descriptor); { std::lock_guard lk(g_mutex); g_socks.emplace(descriptor, connection); } + if (listener.native->nonblock) { + const int enabled = 1; + if (sceNetSetsockopt(descriptor, NET_SOL_SOCKET, NET_SO_NBIO, &enabled, sizeof(enabled)) != 0) { + const int error = *errno_slot(); + sceNetSocketClose(descriptor); + return fail(error); + } + } if (addr && !native_to_guest_address(peer, addr, addrlen)) { sceNetSocketClose(descriptor); return fail(NET_EAFNOSUPPORT); @@ -811,6 +1186,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; { @@ -827,10 +1206,32 @@ 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); - 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; #ifdef _WIN32 - if (WSAGetLastError() == WSAEWOULDBLOCK) return fail(NET_EINPROGRESS); + 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) { +#ifdef _WIN32 + note_local_address(s, socket); +#endif + return 0; + } +#ifdef _WIN32 + if (WSAGetLastError() == WSAEWOULDBLOCK) { + if (socket.native->nonblock) return fail(NET_EINPROGRESS); + fd_set writable; + fd_set failed; + FD_ZERO(&writable); + FD_ZERO(&failed); + FD_SET(socket.native->value, &writable); + FD_SET(socket.native->value, &failed); + if (::select(0, nullptr, &writable, &failed, nullptr) < 0) return fail(native_error()); + int error = 0; + int size = sizeof(error); + if (::getsockopt(socket.native->value, SOL_SOCKET, SO_ERROR, reinterpret_cast(&error), &size) != 0) + return fail(native_error()); + return error == 0 ? 0 : fail(net_error(error)); + } #endif return fail(native_error()); } @@ -846,14 +1247,19 @@ 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, flags)) return wait == RECEIVE_NOTHING ? 0 : fail(wait); +#endif const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); if (!destination.Open()) return fail(NET_EFAULT); const int native_flags = native_message_flags(flags); - const int result = ::recv(socket.native->value, static_cast(buf), static_cast(len), native_flags); + const int result = native_transfer(*socket.native, flags, false, [&](int) { + return ::recv(socket.native->value, static_cast(buf), static_cast(len), native_flags); + }); #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) return static_cast(len); #endif - return result >= 0 ? result : fail(receive_error(socket)); + return result >= 0 ? result : fail(native_error()); } int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* from, uint32_t* fromlen) { @@ -868,17 +1274,33 @@ 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, flags)) { + 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{}; NativeLength peer_length = sizeof(peer); const int native_flags = native_message_flags(flags); - int result = ::recvfrom(socket.native->value, static_cast(buf), static_cast(len), - native_flags, from ? reinterpret_cast(&peer) : nullptr, from ? &peer_length : nullptr); + int result = native_transfer(*socket.native, flags, false, [&](int) { + peer_length = sizeof(peer); + return ::recvfrom(socket.native->value, static_cast(buf), static_cast(len), + native_flags, from ? reinterpret_cast(&peer) : nullptr, from ? &peer_length : nullptr); + }); #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(len); #endif - if (result < 0) return fail(receive_error(socket)); + if (result < 0) return fail(native_error()); if (from && !native_to_guest_address(peer, from, fromlen)) return fail(NET_EAFNOSUPPORT); return result; } @@ -894,7 +1316,9 @@ int64_t APS5_VABI sceNetSend(int s, const void* buf, size_t len, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } - const int result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags)); + const int result = native_transfer(*socket.native, flags, true, [&](int offset) { + return ::send(socket.native->value, offset ? static_cast(buf) + offset : static_cast(buf), static_cast(len) - offset, NATIVE_SEND_FLAGS | native_message_flags(flags)); + }, true, static_cast(len)); return result >= 0 ? result : fail(native_error()); } @@ -912,16 +1336,24 @@ int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, co int result; if (!to) { if (tolen != 0) return fail(NET_EINVAL); - result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags)); + result = native_transfer(*socket.native, flags, true, [&](int offset) { + return ::send(socket.native->value, offset ? static_cast(buf) + offset : static_cast(buf), static_cast(len) - offset, NATIVE_SEND_FLAGS | native_message_flags(flags)); + }, true, static_cast(len)); } else { sockaddr_storage destination{}; NativeLength destination_length = 0; if (!guest_to_native_address(to, tolen, destination, destination_length)) return fail(NET_EINVAL); if (destination.ss_family != (socket.family == NET_AF_INET ? AF_INET : AF_INET6)) return fail(NET_EAFNOSUPPORT); - result = ::sendto(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags), - reinterpret_cast(&destination), destination_length); + result = native_transfer(*socket.native, flags, true, [&](int offset) { + return ::sendto(socket.native->value, offset ? static_cast(buf) + offset : static_cast(buf), static_cast(len) - offset, NATIVE_SEND_FLAGS | native_message_flags(flags), + reinterpret_cast(&destination), destination_length); + }, true, static_cast(len)); } - 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) { @@ -953,7 +1385,24 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } + const GuestArena::HostWrite messageOutput(msg, sizeof(*msg)); + if (!messageOutput.Open()) return fail(NET_EFAULT); + std::deque destinations; + for (int i = 0; i < msg->iov_length; ++i) { + const auto& entry = msg->iov[i]; + if (entry.length == 0) continue; + if (!destinations.emplace_back(entry.base, static_cast(entry.length)).Open()) return fail(NET_EFAULT); + } const bool datagram = socket.type != NET_SOCK_STREAM; +#ifdef _WIN32 + if (const int wait = wait_for_local_address(s, socket, total == 0, flags)) { + if (wait != RECEIVE_NOTHING) return fail(wait); + msg->flags = 0; + if (msg->name) msg->name_length = 0; + msg->control_length = 0; + return 0; + } +#endif std::vector buffer; try { buffer.resize(static_cast(total) + (datagram ? 1u : 0u)); @@ -962,11 +1411,14 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { } sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); - std::int64_t received = ::recvfrom(socket.native->value, buffer.data(), static_cast(buffer.size()), - native_message_flags(flags), reinterpret_cast(&peer), &peer_length); + std::int64_t received = native_transfer(*socket.native, flags, false, [&](int) { + peer_length = sizeof(peer); + return ::recvfrom(socket.native->value, buffer.data(), static_cast(buffer.size()), + native_message_flags(flags), reinterpret_cast(&peer), &peer_length); + }); if (received < 0) { #ifdef _WIN32 - if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(receive_error(socket)); + if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(native_error()); received = static_cast(buffer.size()); #else return fail(native_error()); @@ -997,27 +1449,55 @@ 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); } +extern "C" int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, const void* value, std::uint32_t length); + int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, uint32_t optlen) { Sock socket; + bool found = false; { std::lock_guard lk(g_mutex); auto it = g_socks.find(s); - if (it == g_socks.end()) return fail(NET_EBADF); - socket = it->second; + if (it != g_socks.end()) { + socket = it->second; + found = true; + } } + if (!found) { + if (!GuestSocketIsOpen_nid_no_patch(s)) return fail(NET_EBADF); + return setsockopt_nid_postfix(s, level, optname, optval, optlen) == 0 ? 0 : fail(*__error_nid_postfix()); + } + if (level == NET_IPPROTO_TCP && socket.type != NET_SOCK_STREAM) return fail(NET_EINVAL); + if (level == NET_IPPROTO_TCP && optname == NET_TCP_NODELAY) { + if (!optval || optlen < sizeof(int)) return fail(NET_EINVAL); + int value = 0; + std::memcpy(&value, optval, sizeof(value)); + const int enabled = value != 0; + if (::setsockopt(socket.native->value, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&enabled), sizeof(enabled)) != 0) + return fail(native_error()); + return 0; + } + if (level == NET_IPPROTO_TCP) return fail(NET_ENOPROTOOPT); if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP); + if (optname == NET_SO_RCVTIMEO || optname == NET_SO_SNDTIMEO) { + const int error = set_kernel_timeout(s, optname, optval, optlen); + return error == 0 ? 0 : fail(error); + } optname = timeout_option(optname); if (!optval || optlen < sizeof(int)) return fail(NET_EINVAL); int value = 0; std::memcpy(&value, optval, sizeof(value)); if (optname == NET_SO_NBIO) { -#ifdef _WIN32 - u_long enabled = value != 0; - if (ioctlsocket(socket.native->value, FIONBIO, &enabled) != 0) return fail(native_error()); -#else +#ifndef _WIN32 const int old_flags = fcntl(socket.native->value, F_GETFL, 0); if (old_flags < 0 || fcntl(socket.native->value, F_SETFL, value ? old_flags | O_NONBLOCK : old_flags & ~O_NONBLOCK) != 0) return fail(native_error()); @@ -1026,18 +1506,23 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval if (value < 0) return fail(NET_EINVAL); if (set_native_timeout(socket.native->value, optname, value) != 0) return fail(native_error()); } else if (optname == NET_SO_REUSEADDR || optname == NET_SO_BROADCAST || optname == NET_SO_SNDBUF || optname == NET_SO_RCVBUF) { + if ((optname == NET_SO_SNDBUF || optname == NET_SO_RCVBUF) && value < 1) return fail(NET_EINVAL); 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); } std::lock_guard lk(g_mutex); auto it = g_socks.find(s); if (it != g_socks.end()) { - if (optname == NET_SO_NBIO) it->second.nonblock = value != 0; - if (optname == NET_SO_RCVTIMEO) it->second.rcv_timeout_us = value; - if (optname == NET_SO_SNDTIMEO) it->second.snd_timeout_us = value; + if (optname == NET_SO_NBIO) it->second.native->nonblock = value != 0; + if (optname == NET_SO_RCVTIMEO) it->second.native->receiveTimeout = value; + if (optname == NET_SO_SNDTIMEO) it->second.native->sendTimeout = value; + if (optname == NET_SO_SNDBUF) it->second.snd_buffer = value; + if (optname == NET_SO_RCVBUF) it->second.rcv_buffer = value; + if (optname == NET_SO_ONESBCAST) it->second.onesbcast = value; } return 0; } @@ -1050,15 +1535,34 @@ 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.native->receiveTimeout.load() : socket.native->sendTimeout.load(); + const KernelTimeval timeout{microseconds / 1000000, microseconds % 1000000}; + *optlen = std::min(*optlen, static_cast(sizeof(timeout))); + std::memcpy(optval, &timeout, *optlen); + return 0; } + if (*optlen < sizeof(int)) return fail(NET_EINVAL); + if (level == NET_IPPROTO_TCP && socket.type != NET_SOCK_STREAM) return fail(NET_EINVAL); + if (level == NET_IPPROTO_TCP && optname == NET_TCP_NODELAY) { + int enabled = 0; + NativeLength size = sizeof(enabled); + if (::getsockopt(socket.native->value, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&enabled), &size) != 0) + return fail(native_error()); + const int value = enabled != 0 ? NET_TF_NODELAY : 0; + std::memcpy(optval, &value, sizeof(value)); + *optlen = sizeof(value); + return 0; + } + if (level == NET_IPPROTO_TCP) return fail(NET_ENOPROTOOPT); if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP); int value = 0; switch (timeout_option(optname)) { - case NET_SO_NBIO: value = socket.nonblock ? 1 : 0; break; - case NET_SO_RCVTIMEO: value = socket.rcv_timeout_us; break; - case NET_SO_SNDTIMEO: value = socket.snd_timeout_us; break; + case NET_SO_NBIO: value = socket.native->nonblock ? 1 : 0; break; + case NET_SO_RCVTIMEO: value = socket.native->receiveTimeout; break; + case NET_SO_SNDTIMEO: value = socket.native->sendTimeout; break; + case NET_SO_ONESBCAST: value = socket.onesbcast; break; case NET_SO_ERROR: { int pending = 0; NativeLength size = sizeof(pending); @@ -1074,6 +1578,16 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint value = pending == 0 ? 0 : net_error(pending); break; } + case NET_SO_SNDBUF: + case NET_SO_RCVBUF: { + const bool send = optname == NET_SO_SNDBUF; + value = send ? socket.snd_buffer : socket.rcv_buffer; + if (value != 0) break; + NativeLength size = sizeof(value); + if (::getsockopt(socket.native->value, SOL_SOCKET, send ? SO_SNDBUF : SO_RCVBUF, reinterpret_cast(&value), &size) != 0) + return fail(native_error()); + break; + } default: return fail(NET_EOPNOTSUPP); } std::memcpy(optval, &value, sizeof(value)); @@ -1120,12 +1634,116 @@ int APS5_VABI sceNetGetpeername(int s, void* addr, uint32_t* addrlen) { return native_to_guest_address(native, addr, addrlen) ? 0 : fail(NET_EAFNOSUPPORT); } +constexpr int NET_SOCKINFO_F_SELF = 0x1; +constexpr int NET_SOCKINFO_F_NONBLOCK = 0x200000; +constexpr int NET_SOCKINFO_TIME_WAIT = 0x40; + +struct NetSockInfo { + char name[32]; + std::int32_t pid; + std::int32_t s; + std::int8_t socket_type; + std::int8_t policy; + std::int8_t priority; + std::int8_t reserved8; + std::int32_t recv_queue_length; + std::int32_t send_queue_length; + std::uint32_t local_adr; + std::uint32_t remote_adr; + std::uint16_t local_port; + std::uint16_t remote_port; + std::uint16_t local_vport; + std::uint16_t remote_vport; + std::int32_t state; + std::int32_t flags; + std::int32_t tx_bps; + std::int32_t rx_bps; + std::int32_t max_tx_bps; + std::int32_t max_rx_bps; + std::int32_t tx_vbps; + std::int32_t rx_vbps; + std::int32_t recv_buffer_size; + std::int32_t send_buffer_size; + std::int32_t reserved6[8]; + std::int32_t tx_drops; + std::int32_t rx_drops; + std::int32_t tx_wait; + std::int32_t reserved[2]; +}; +static_assert(sizeof(NetSockInfo) == 0xA0 && offsetof(NetSockInfo, s) == 0x24 && offsetof(NetSockInfo, local_adr) == 0x34 && + offsetof(NetSockInfo, local_port) == 0x3C && offsetof(NetSockInfo, recv_buffer_size) == 0x64); + +static int sock_buffer_size(const Sock& socket, bool send, std::int32_t& value) { + value = send ? socket.snd_buffer : socket.rcv_buffer; + if (value != 0) return 0; + NativeLength size = sizeof(value); + return ::getsockopt(socket.native->value, SOL_SOCKET, send ? SO_SNDBUF : SO_RCVBUF, reinterpret_cast(&value), &size) == 0 ? 0 : native_error(); +} + +static int fill_sock_info(int s, NetSockInfo& record) { + Sock socket; + { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(s); + if (it == g_socks.end()) return NET_EBADF; + socket = it->second; + } + if (socket.family != NET_AF_INET) return NET_EAFNOSUPPORT; + sockaddr_in local{}; + NativeLength length = sizeof(local); + if (::getsockname(socket.native->value, reinterpret_cast(&local), &length) != 0) { +#ifdef _WIN32 + if (WSAGetLastError() != WSAEINVAL) return native_error(); +#else + return native_error(); +#endif + } + sockaddr_in remote{}; + length = sizeof(remote); + if (::getpeername(socket.native->value, reinterpret_cast(&remote), &length) != 0) { + const int error = native_error(); + if (error != NET_ENOTCONN) return error; + } + record = {}; + if (const int error = sock_buffer_size(socket, false, record.recv_buffer_size)) return error; + if (const int error = sock_buffer_size(socket, true, record.send_buffer_size)) return error; + record.s = s; + record.socket_type = static_cast(socket.type); + record.local_adr = local.sin_addr.s_addr; + record.remote_adr = remote.sin_addr.s_addr; + record.local_port = local.sin_port; + record.remote_port = remote.sin_port; + record.flags = NET_SOCKINFO_F_SELF | (socket.native->nonblock ? NET_SOCKINFO_F_NONBLOCK : 0); + return 0; +} + int APS5_VABI sceNetGetSockInfo(int s, void* info, int n, int flags) { - (void)info; - (void)n; - (void)flags; - std::lock_guard lk(g_mutex); - return g_socks.count(s) != 0 ? 0 : fail(NET_EBADF); + if ((flags & ~NET_SOCKINFO_TIME_WAIT) != 0) return fail(NET_EINVAL); + std::vector listed; + if (s < 0) { + std::lock_guard lk(g_mutex); + for (const auto& [descriptor, socket] : g_socks) { + if (socket.family == NET_AF_INET) listed.push_back(descriptor); + } + } + if (!info) return s < 0 ? static_cast(listed.size()) : fail(NET_EINVAL); + if (n < 1) return fail(NET_EINVAL); + NetSockInfo record; + if (s >= 0) { + if (const int error = fill_sock_info(s, record)) return fail(error); + std::memcpy(info, &record, sizeof(record)); + return 1; + } + int written = 0; + for (const int descriptor : listed) { + if (written == n) break; + const int error = fill_sock_info(descriptor, record); + if (error == NET_EBADF || error == NET_EAFNOSUPPORT) continue; + if (error != 0) return fail(error); + std::memcpy(static_cast(info) + static_cast(written) * sizeof(record), &record, sizeof(record)); + ++written; + } + return written; } int APS5_VABI sceNetEpollCreate(const char* name, int flags) { @@ -1134,7 +1752,7 @@ int APS5_VABI sceNetEpollCreate(const char* name, int flags) { std::lock_guard lk(g_mutex); const int id = g_next_epoll++; g_epolls.insert(id); - g_epoll_socks.emplace(id, std::map{}); + g_epoll_socks.emplace(id, EpollRegistrations{}); return id; } @@ -1149,7 +1767,7 @@ int APS5_VABI sceNetShowNetstatWithMemory(int memid) { poolSize = pool->second; sockets.reserve(g_socks.size()); for (const auto& [id, socket] : g_socks) - sockets.push_back({id, socket.family, socket.type, socket.nonblock, socket.bound, socket.listening}); + sockets.push_back({id, socket.family, socket.type, socket.native->nonblock, socket.bound, socket.listening}); } std::fprintf(stdout, "sceNet netstat: pool=%d bytes=%zu sockets=%zu\n", memid, poolSize, sockets.size()); for (const auto& socket : sockets) @@ -1199,16 +1817,17 @@ int APS5_VABI sceNetEpollControl(int eid, int op, int id, const NetEpollEvent* e } if (op != NET_EPOLL_CTL_ADD && op != NET_EPOLL_CTL_MOD && op != NET_EPOLL_CTL_DEL) return fail(NET_EINVAL); if (op != NET_EPOLL_CTL_DEL && !event) return fail(NET_EINVAL); - if (op != NET_EPOLL_CTL_DEL && g_socks.count(id) == 0) return fail(NET_EBADF); + if (g_socks.count(id) == 0) return fail(NET_EBADF); auto& registrations = g_epoll_socks[eid]; if (op == NET_EPOLL_CTL_ADD) { - if (!registrations.emplace(id, *event).second) return fail(NET_EADDRINUSE); + if (registrations.count(id) != 0) return fail(NET_EADDRINUSE); + registrations.emplace(id, std::make_shared(*event)); } else if (op == NET_EPOLL_CTL_DEL) { if (registrations.erase(id) == 0) return fail(NET_ENOENT); } else { auto registration = registrations.find(id); if (registration == registrations.end()) return fail(NET_ENOENT); - registration->second = *event; + registration->second = std::make_shared(*event); } g_cv.NotifyAll(); return 0; @@ -1216,12 +1835,16 @@ int APS5_VABI sceNetEpollControl(int eid, int op, int id, const NetEpollEvent* e int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int timeout) { if (!events || maxevents <= 0) return fail(NET_EINVAL); + struct Watch { + int descriptor; + std::shared_ptr registration; + std::shared_ptr native; + }; const auto deadline = timeout > 0 ? std::chrono::steady_clock::now() + std::chrono::microseconds(timeout) : std::chrono::steady_clock::time_point::max(); for (;;) { - std::vector registered_events; - std::vector> native_sockets; + std::vector watches; { std::lock_guard lk(g_mutex); if (g_epolls.count(eid) == 0) return fail(NET_EBADF); @@ -1231,12 +1854,11 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int for (const auto& entry : registration->second) { const auto socket = g_socks.find(entry.first); if (socket == g_socks.end()) continue; - registered_events.push_back({entry.second.events, 0, static_cast(entry.first), entry.second.data}); - native_sockets.push_back(socket->second.native); + watches.push_back({entry.first, entry.second, socket->second.native}); } } } - if (registered_events.empty()) { + if (watches.empty()) { if (timeout == 0) return 0; std::unique_lock lk(g_mutex); const auto now = std::chrono::steady_clock::now(); @@ -1248,18 +1870,18 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int } #ifdef _WIN32 - std::vector descriptors(registered_events.size()); + std::vector descriptors(watches.size()); for (std::size_t index = 0; index < descriptors.size(); ++index) { - descriptors[index].fd = native_sockets[index]->value; - if (registered_events[index].events & 1) descriptors[index].events |= POLLRDNORM; - if (registered_events[index].events & 2) descriptors[index].events |= POLLWRNORM; + descriptors[index].fd = watches[index].native->value; + if (watches[index].registration->events & 1) descriptors[index].events |= POLLRDNORM; + if (watches[index].registration->events & 2) descriptors[index].events |= POLLWRNORM; } #else - std::vector descriptors(registered_events.size()); + std::vector descriptors(watches.size()); for (std::size_t index = 0; index < descriptors.size(); ++index) { - descriptors[index].fd = native_sockets[index]->value; - if (registered_events[index].events & 1) descriptors[index].events |= POLLIN; - if (registered_events[index].events & 2) descriptors[index].events |= POLLOUT; + descriptors[index].fd = watches[index].native->value; + if (watches[index].registration->events & 1) descriptors[index].events |= POLLIN; + if (watches[index].registration->events & 2) descriptors[index].events |= POLLOUT; } #endif const auto now = std::chrono::steady_clock::now(); @@ -1280,27 +1902,32 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int std::lock_guard lk(g_mutex); if (g_epolls.count(eid) == 0) return fail(NET_EBADF); if (g_epoll_aborted.erase(eid) != 0) return fail(NET_ECONNABORTED); - } - if (ready_count > 0) { - int count = 0; - for (std::size_t index = 0; index < descriptors.size() && count < maxevents; ++index) { - const auto native_events = descriptors[index].revents; - if (!native_events) continue; - std::uint32_t guest_events = 0; + if (ready_count > 0) { + int count = 0; + const auto& registrations = g_epoll_socks.at(eid); + for (std::size_t index = 0; index < descriptors.size() && count < maxevents; ++index) { + const auto registration = registrations.find(watches[index].descriptor); + if (registration == registrations.end() || registration->second != watches[index].registration) continue; + const auto native_events = descriptors[index].revents; + if (!native_events) continue; + std::uint32_t guest_events = 0; #ifdef _WIN32 - if (native_events & (POLLRDNORM | POLLPRI)) guest_events |= 1; - if (native_events & POLLWRNORM) guest_events |= 2; + if (native_events & (POLLRDNORM | POLLPRI)) guest_events |= 1; + if (native_events & POLLWRNORM) guest_events |= 2; #else - if (native_events & (POLLIN | POLLPRI)) guest_events |= 1; - if (native_events & POLLOUT) guest_events |= 2; + if (native_events & (POLLIN | POLLPRI)) guest_events |= 1; + if (native_events & POLLOUT) guest_events |= 2; #endif - if (native_events & POLLERR) guest_events |= 8; - if (native_events & POLLHUP) guest_events |= 16; - events[count] = registered_events[index]; - events[count].events = guest_events; - ++count; + if (native_events & POLLERR) guest_events |= 8; + if (native_events & POLLHUP) guest_events |= 16; + events[count] = *watches[index].registration; + events[count].ident = static_cast(watches[index].descriptor); + events[count].reserved = 0; + events[count].events = guest_events; + ++count; + } + if (count > 0) return count; } - if (count > 0) return count; } if (timeout == 0) return 0; } @@ -1321,11 +1948,17 @@ int APS5_VABI sceNetInetPton(int af, const char* src, void* dst) { if (af != NET_AF_INET && af != NET_AF_INET6) { return fail(NET_EAFNOSUPPORT); } + const std::string text = af == NET_AF_INET ? std::string(src, std::strcspn(src, " \t\n\v\f\r")) : std::string(src); + std::array address{}; #ifdef _WIN32 - return InetPtonA(af == NET_AF_INET ? AF_INET : AF_INET6, src, dst); + const int converted = InetPtonA(af == NET_AF_INET ? AF_INET : AF_INET6, text.c_str(), address.data()); #else - return ::inet_pton(af == NET_AF_INET ? AF_INET : AF_INET6, src, dst); + const int converted = ::inet_pton(af == NET_AF_INET ? AF_INET : AF_INET6, text.c_str(), address.data()); #endif + if (converted == 1) { + std::memcpy(dst, address.data(), af == NET_AF_INET ? 4 : 16); + } + return converted; } const char* APS5_VABI sceNetInetNtop(int af, const void* src, char* dst, uint32_t size) { @@ -1364,6 +1997,28 @@ int APS5_VABI sceNetEtherNtostr(const NetEtherAddr* n, char* str, size_t len) { return 0; } +int APS5_VABI sceNetEtherStrton(const char* text, NetEtherAddr* address) { + if (text == nullptr || address == nullptr) return fail(NET_EINVAL); + for (std::size_t index = 0; index < 17; ++index) { + if (text[index] == '\0') return fail(NET_EINVAL); + } + const auto hexDigit = [](char character) { + if (character >= '0' && character <= '9') return character - '0'; + if (character >= 'a' && character <= 'z') return character - 'a' + 10; + if (character >= 'A' && character <= 'Z') return character - 'A' + 10; + return 0x7f; + }; + NetEtherAddr parsed{}; + for (std::size_t index = 0; index < sizeof(parsed.data); ++index) { + if (static_cast(text[index * 3]) >= 128 || static_cast(text[index * 3 + 1]) >= 128) { + NotImplemented_nid_no_patch(__func__); + } + parsed.data[index] = static_cast(hexDigit(text[index * 3]) * 16 + hexDigit(text[index * 3 + 1])); + } + *address = parsed; + return 0; +} + int APS5_VABI sceNetGetMacAddress(NetEtherAddr* addr, int flags) { (void)flags; if (addr == nullptr) { @@ -1395,6 +2050,14 @@ int lookup_host(int rid, const char* hostname, const char* func, int family, std if (!initialize_sockets()) return fail(5); std::uint64_t generation = 0; if (const int error = begin_lookup(rid, &Resolver::abort_ntoa, generation)) return error; + if (const auto lookup = g_resolverLookupForTests.load()) { + if (!lookup(hostname, family, addresses) || addresses.empty()) { + set_errno(NET_ENOENT); + log_soft(func, "host DNS lookup failed"); + return end_lookup(rid, generation, NET_ERROR_RESOLVER_ENODNS); + } + return end_lookup(rid, generation, 0); + } addrinfo hints{}; hints.ai_family = family; addrinfo* results = nullptr; @@ -1497,23 +2160,15 @@ int APS5_VABI sceNetResolverStartNtoaMultipleRecordsEx(int rid, const char* host return lookup_records(rid, hostname, info, flags, __func__); } -int APS5_VABI sceNetResolverStartAton(int rid, const void* addr, char* hostname, int len, int timeout, int retry, int flags) { - (void)timeout; - (void)retry; - (void)flags; - if (!addr || !hostname || len <= 0) return fail(NET_EINVAL); +int reverse_lookup(int rid, const sockaddr* address, NativeLength addressLength, char* hostname, int len, const char* func) { if (!initialize_sockets()) return fail(5); std::uint64_t generation = 0; if (const int error = begin_lookup(rid, &Resolver::abort_aton, generation)) return error; - sockaddr_in address{}; - address.sin_family = AF_INET; - std::memcpy(&address.sin_addr, addr, sizeof(address.sin_addr)); std::vector name(static_cast(len)); - const int result = ::getnameinfo(reinterpret_cast(&address), sizeof(address), name.data(), - static_cast(len), nullptr, 0, NI_NAMEREQD); + const int result = ::getnameinfo(address, addressLength, name.data(), static_cast(len), nullptr, 0, NI_NAMEREQD); if (result != 0) { set_errno(result == EAI_AGAIN ? NET_ETIMEDOUT : NET_ENOENT); - log_soft(__func__, "host reverse DNS lookup failed"); + log_soft(func, "host reverse DNS lookup failed"); return end_lookup(rid, generation, NET_ERROR_RESOLVER_ENODNS); } if (const int error = end_lookup(rid, generation, 0)) return error; @@ -1521,6 +2176,28 @@ int APS5_VABI sceNetResolverStartAton(int rid, const void* addr, char* hostname, return 0; } +int APS5_VABI sceNetResolverStartAton(int rid, const void* addr, char* hostname, int len, int timeout, int retry, int flags) { + (void)timeout; + (void)retry; + (void)flags; + if (!addr || !hostname || len <= 0) return fail(NET_EINVAL); + sockaddr_in address{}; + address.sin_family = AF_INET; + std::memcpy(&address.sin_addr, addr, sizeof(address.sin_addr)); + return reverse_lookup(rid, reinterpret_cast(&address), sizeof(address), hostname, len, __func__); +} + +int APS5_VABI sceNetResolverStartAton6(int rid, const void* addr, char* hostname, int len, int timeout, int retry, int flags) { + (void)timeout; + (void)retry; + if (flags != 0) throw std::runtime_error("sceNetResolverStartAton6: flags " + std::to_string(flags) + " are not supported"); + if (!addr || !hostname || len <= 0) return fail(NET_EINVAL); + sockaddr_in6 address{}; + address.sin6_family = AF_INET6; + std::memcpy(&address.sin6_addr, addr, sizeof(address.sin6_addr)); + return reverse_lookup(rid, reinterpret_cast(&address), sizeof(address), hostname, len, __func__); +} + int APS5_VABI sceNetResolverGetError(int rid, int* status) { if (!status) return fail(NET_EINVAL); std::lock_guard lk(g_mutex); @@ -1547,11 +2224,895 @@ int APS5_VABI sceNetResolverAbort(int rid, int flags) { } int APS5_VABI sceNetShowIfconfig() { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!g_net_inited) return fail(NET_ENOTINIT); + throw std::runtime_error(__func__); } extern const std::uint8_t in6addr_any_nid_postfix[16] = {}; extern const std::uint8_t in6addr_loopback_nid_postfix[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; +int APS5_VABI sceNetClearDnsCache(int flags) { + std::lock_guard lock(g_mutex); + if (!g_net_inited) return fail(NET_ENOTINIT); + if (flags != 0) return fail(NET_EINVAL); + return 0; +} + +int APS5_VABI sceNetGetStatisticsInfo(NetStatisticsInfo* info, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetInterfaceStats(NetInterfaceStats* stats, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSetDnsInfo(const NetDnsInfo* info, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowIfconfigWithMemory(std::int32_t poolId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowPolicy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowPolicyWithMemory(std::int32_t poolId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowRoute(void) { + return 0; +} + +int APS5_VABI sceNetShowRouteWithMemory(std::int32_t poolId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetResolverConnectAbort(NetResolverConnection* connection, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetResolverConnectCreate(const char* name, std::int32_t socketType, NetResolverConnection* connection, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetResolverConnect(NetResolverConnection* connection, const char* hostname, std::uint16_t port, const NetResolverConnectOption* option) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetResolverConnectDestroy(NetResolverConnection* connection) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetSockInfo6(std::int32_t socket, NetSockInfo6* info, std::uint32_t capacity, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSetDns6Info(const NetDns6Info* info, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowRoute6(void) { + return 0; +} + +int APS5_VABI sceNetShowRoute6WithMemory(std::int32_t poolId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +NetRouteInfo* APS5_VABI sceNetAllocateAllRouteInfo(std::int32_t interfaceId, std::int32_t* count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +NetRouteInfo* APS5_VABI sceNetAllocateAllRouteInfo6(std::int32_t interfaceId, std::int32_t* count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetControl(std::uint32_t interfaceId, std::int32_t interfaceIndex, std::uint32_t command, void* data, std::uint32_t dataSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDbgInit(NetDebugParameter* parameter, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEventCallbackCreate(std::uint32_t interfaceId, NetEventCallback callback, void* userData, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEventCallbackDestroy(std::uint32_t interfaceId, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEventCallbackGetError(std::uint32_t interfaceId, std::int32_t* error, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEventCallbackWaitCB(std::int32_t timeout, void* waitData, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetFreeAllRouteInfo(void* routes) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetArpInfo(NetArpInfo* info, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetAppMemInfo(NetAppMemoryInfo* info, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetDnsInfo(NetDnsInfo* info, std::uint32_t flags) { + if (flags != 0) throw std::runtime_error("sceNetGetDnsInfo: flags " + std::to_string(flags) + " are not supported"); + if (!info) throw std::runtime_error("sceNetGetDnsInfo: a null info is not supported"); + std::array servers{}; + const std::size_t count = std::min(NetCtlHostDnsServers_nid_no_patch(servers.data(), servers.size()), servers.size()); + std::memcpy(info, servers.data(), sizeof(servers)); + return static_cast(count); +} + +int APS5_VABI sceNetGetIfList(std::int32_t interfaceId, NetInterfaceInfo* interfaces, std::uint32_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetIfListEx(std::int32_t interfaceId, NetInterfaceInfo* interfaces, std::uint32_t capacity, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetIfListOnce(std::int32_t interfaceId, NetInterfaceStatistics* statistics, std::uint32_t count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +const char* APS5_VABI sceNetGetIfName(std::uint32_t index) { + static constexpr std::array names = { + "lo0", "eth0", "eth1", "dbg0", "wlan0", "wlan1", "gbe0", "bt0", "phone0", "pppoe0"}; + if (index >= names.size()) { + set_errno(NET_EINVAL); + return ""; + } + return names[index]; +} + +int APS5_VABI sceNetGetIfnameNumList(std::uint32_t* interfaceIds, std::int32_t capacity, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetNameToIndex(std::uint32_t interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetRandom(std::uint32_t* output) { + if (!g_net_inited) return fail(NET_ENOTINIT); + if (output == nullptr) return fail(NET_EINVAL); + static std::mutex mutex; + static std::random_device device; + const std::lock_guard lock(mutex); + *output = static_cast(device()); + return 0; +} + +int APS5_VABI sceNetGetRouteInfo(std::int32_t family, NetRouteInfo* routes, std::int32_t capacity, std::uint8_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetsockoptEx(int socketId, int level, int option, void* value, std::uint32_t* valueSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI sceNetGetSystemTime(std::uint64_t* output) { + if (output != nullptr) *output = sceKernelGetProcessTime(); +} + +int APS5_VABI sceNetGetSystemMemInfo(NetSystemMemoryInfo* info, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetInitParam(const NetInitializeParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetInetPtonEx(int af, const char* src, void* dst, int) { + if (af != NET_AF_INET) { + return sceNetInetPton(af, src, dst); + } + if (src == nullptr || dst == nullptr) { + return fail(NET_EINVAL); + } + std::uint32_t address = 0; + if (!parse_ipv4_parts(src, address)) { + return 0; + } + const std::array bytes{static_cast(address >> 24), static_cast(address >> 16), + static_cast(address >> 8), static_cast(address)}; + std::memcpy(dst, bytes.data(), bytes.size()); + return 1; +} + +int APS5_VABI sceNetIoctl(int socketId, std::uint32_t command, void* argument) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI sceNetMemoryAllocate(std::int32_t size, int flags) { + constexpr int zeroFill = 2; + constexpr std::size_t blockAlignment = 64; + if (size <= 0) return nullptr; + void* memory = nullptr; + try { + memory = GuestHeap::GuestHeapAlign_nid_postfix(blockAlignment, static_cast(size)); + } catch (const std::bad_alloc&) { + return nullptr; + } catch (const std::length_error&) { + return nullptr; + } + if ((flags & zeroFill) != 0) std::memset(memory, 0, static_cast(size)); + return memory; +} + +void APS5_VABI sceNetMemoryFree(void* memory) { + GuestHeap::GuestHeapFree_nid_postfix(memory); +} + +int APS5_VABI sceNetSetDnsInfoToKernel(const NetDnsInfo* info, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSetsockoptEx(int socketId, int level, int option, const void* value, std::uint32_t valueSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSocketInternal(const char* name, int domain, int type, int protocol) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSyncCreate(NetSyncObject* sync, std::uint32_t signals, const char* name) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSyncDestroy(NetSyncObject* sync) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +NetSyncObject* APS5_VABI sceNetSyncGet(std::uint32_t index) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSyncSignal(NetSyncObject* sync, std::uint32_t signals) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSyncWait(NetSyncObject* sync, std::uint32_t* timeout) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSysctl(const int* name, std::uint32_t nameLength, void* oldValue, std::size_t* oldSize, const void* newValue, std::size_t newSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetThreadCreate(Pthread* thread, const NetThreadParameter* parameter, const char* name) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetThreadExit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetThreadJoin(void* thread) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetUsleep(int microseconds) { + if (!g_net_inited) return fail(NET_ENOTINIT); + if (microseconds < 0) return fail(NET_EINVAL); + const int result = sceKernelUsleep_nid_postfix(static_cast(microseconds)); + if (result < 0) set_errno((result & 0xFF00) == 0x100 ? result & 0xFF : 205); + return result; +} + +int APS5_VABI sceNetShowIfconfigForBuffer(char* buffer, std::int32_t bufferSize, std::int32_t poolId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowRouteForBuffer(char* buffer, std::int32_t bufferSize, std::int32_t poolId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowNetstatForBuffer(char* buffer, std::int32_t bufferSize, std::int32_t poolId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowNetstatEx(std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowNetstatExForBuffer(char* buffer, std::int32_t bufferSize, std::int32_t poolId, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +const char* APS5_VABI sceNetInetNtopWithScopeId(std::int32_t family, const void* address, char* text, std::uint32_t size, std::uint32_t scopeId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetInetPtonWithScopeId(std::int32_t family, const char* text, void* address, std::uint32_t* scopeId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetDns6Info(NetDns6Info* info, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetSetDns6InfoToKernel(const NetDns6Info* info, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetShowRoute6ForBuffer(char* buffer, std::int32_t bufferSize, std::int32_t poolId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigGetIfFlags(std::uint32_t interfaceId, std::uint32_t* flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetIfFlags(std::uint32_t interfaceId, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigUnsetIfFlags(std::uint32_t interfaceId, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDelIfaddr(int interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDownInterface(int interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetDefaultRoute(const std::uint32_t* gateway) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetDefaultRouteDev(const std::uint32_t* gateway) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetIfaddr(int interfaceId, const std::uint32_t* address, const std::uint32_t* broadcastAddress, const std::uint32_t* netmask) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetIfLinkLocalAddr6(std::uint32_t interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigUpInterface(int interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigUpInterfaceWithFlags(std::uint32_t interfaceId, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigCleanUpAllInterfaces(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigFlushRoute(std::uint32_t interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigFlushRoute6(std::uint32_t interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigAddIfaddr(int interfaceId, const std::uint32_t* address, const std::uint32_t* broadcastAddress, const std::uint32_t* netmask) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigGetIfaddr(std::int32_t interfaceId, NetIpv4InterfaceAddress* addresses, std::int32_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetIfmtu(std::uint32_t interfaceId, int mtu) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigAddArp(const NetEtherAddr* etherAddress, const std::uint32_t* address) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigAddArpWithInterface(int interfaceId, const NetEtherAddr* etherAddress, const std::uint32_t* address) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigAddMRoute(const std::uint32_t* address) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigAddRoute(const std::uint32_t* destination, const std::uint32_t* gateway, const std::uint32_t* netmask, int hostRoute) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigAddRouteWithInterface(int interfaceId, const std::uint32_t* destination, const std::uint32_t* gateway, const std::uint32_t* netmask, int hostRoute) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDelArp(const NetEtherAddr* etherAddress, const std::uint32_t* address) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDelArpWithInterface(int interfaceId, const NetEtherAddr* etherAddress, const std::uint32_t* address) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDelDefaultRoute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDelDefaultRouteDev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDelMRoute(const std::uint32_t* address) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDelRoute(const std::uint32_t* destination, const std::uint32_t* gateway, const std::uint32_t* netmask) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigGetDefaultRoute(std::uint32_t* gateway) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDhcpGetAutoipInfo(std::int32_t interfaceId, NetAutoIpInfo* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDhcpGetInfo(std::int32_t interfaceId, NetDhcpInfo* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDhcpGetInfoEx(std::int32_t interfaceId, std::int32_t type, NetDhcpOptionInfo* info, std::uint32_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDhcpStart(std::uint32_t interfaceId, NetAddressCallback callback, void* userArg, std::int32_t threadParameter1, std::int32_t threadParameter2, NetDhcpStartParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDhcpStop(int interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigAddIfaddr6(int interfaceId, const NetIpv6Address* address, std::uint8_t prefixLength) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDelIfaddr6(int interfaceId, const NetIpv6Address* address) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDelDefaultRoute6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetIfaddr6(int interfaceId, const NetIpv6Address* address, std::uint8_t prefixLength) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetIfaddr6WithFlags(int interfaceId, const NetIpv6Address* address, std::uint8_t prefixLength, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetDefaultRoute6(int interfaceId, const NetIpv6Address* gateway) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigGetIfaddr6(int interfaceId, NetIpv6InterfaceAddress* addresses, std::uint32_t count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigGetDefaultRoute6(NetIpv6Address* gateway) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigDelRoute6(const NetIpv6Address* destination, std::uint8_t prefixLength) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigAddRoute6(const NetIpv6Address* destination, const NetIpv6Address* gateway, std::uint8_t prefixLength) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetAddrConfig6Start(std::uint32_t interfaceId, NetAddressCallback callback, void* userArg, std::int32_t threadParameter1, std::int32_t threadParameter2, const NetAddress6StartParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetAddrConfig6Stop(int interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetAddrConfig6GetInfo(std::int32_t interfaceId, NetAddress6Info* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetInfoDumpStart(std::int32_t threadParameter1, std::int32_t threadParameter2, const NetInfoDumpParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetInfoDumpStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDhcpdStart(std::uint32_t interfaceId, NetAddressCallback callback, void* userArg, std::int32_t threadParameter1, std::int32_t threadParameter2, const NetDhcpdStartParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDhcpdStop(int interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDuplicateIpStart(std::int32_t interfaceId, NetAddressCallback callback, void* userArg, std::int32_t threadParameter1, std::int32_t threadParameter2) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDuplicateIpStop(int interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocClearWakeOnWlan(std::uint32_t interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocCreate(std::int32_t interfaceId, NetWlanAdhocParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocGetWakeOnWlanInfo(std::int32_t interfaceId, NetWlanWakeInfo* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocJoin(std::int32_t interfaceId, NetWlanAdhocParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocLeave(int interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocPspEmuClearWakeOnWlan(std::uint32_t interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocPspEmuGetWakeOnWlanInfo(std::int32_t interfaceId, NetWlanPspWakeInfo* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocPspEmuSetWakeOnWlan(std::int32_t interfaceId, NetWlanPspWakeParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocScanJoin(std::int32_t interfaceId, const NetWlanAdhocScanParameter* parameter, void* buffer, std::size_t bufferSize, NetWlanJoinResult** result) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocSetExtInfoElement(std::int32_t interfaceId, NetWlanInformationElement* parameter, std::int32_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanAdhocSetWakeOnWlan(std::int32_t interfaceId, NetWlanWakeParameter* parameter, std::size_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanApStart(std::int32_t interfaceId, NetWlanApParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanApStop(int interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanBackgroundScanQuery(std::int32_t interfaceId, void* buffer, std::int32_t size, NetWlanScanResult** results, std::size_t* count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanBackgroundScanStart(std::int32_t interfaceId, NetWlanScanParameter* parameter, std::uint32_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanBackgroundScanStop(int interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanDiagGetDeviceInfo(std::int32_t interfaceId, NetWlanDeviceInfo* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanDiagSetAntenna(std::int32_t interfaceId, NetWlanAntennaParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanDiagSetTxFixedRate(std::int32_t interfaceId, NetWlanRateParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanGetDeviceConfig(std::int32_t interfaceId, NetWlanDeviceConfiguration* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanInfraGetRssiInfo(std::int32_t interfaceId, NetWlanRssiInfo* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanInfraLeave(int interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanInfraScanJoin(std::int32_t interfaceId, const NetWlanInfraScanParameter* parameter, void* buffer, std::size_t bufferSize, NetWlanJoinResult** result) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanScan(std::int32_t interfaceId, const NetWlanScanParameter* parameter, std::size_t parameterSize, void* buffer, std::size_t bufferSize, NetWlanScanResult** results, std::size_t* count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigWlanSetDeviceConfig(std::int32_t interfaceId, NetWlanDeviceConfiguration* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigEtherGetLinkMode(std::uint32_t interfaceId, int* mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigEtherPostPlugInOutEvent(std::uint32_t interfaceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigEtherSetLinkMode(std::uint32_t interfaceId, int mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetPppoeStart(std::uint32_t interfaceId, std::int32_t pppoeInterfaceId, NetAddressCallback callback, void* userArg, std::int32_t threadParameter1, std::int32_t threadParameter2, const NetPppoeStartParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetPppoeStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBandwidthControlSetDefaultParam(const NetBandwidthParameter* parameter, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBandwidthControlGetDefaultParam(NetBandwidthParameter* parameter, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBandwidthControlGetDataTraffic(std::int32_t interfaceId, NetDataTraffic* traffic, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigSetDefaultScope(std::uint32_t interfaceId, const char* interfaceName, std::int32_t scopeId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigRoutingStart(std::uint32_t firstInterfaceId, std::uint32_t secondInterfaceId, const NetRoutingAddressParameter* addressParameter, NetRoutingServerParameter* serverParameter, const NetRoutingRuleParameter* ruleParameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigRoutingStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigRoutingShowRoutingConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetConfigRoutingShowtCtlVar(const char* name) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetGetStatisticsInfoInternal(NetStatisticsInfoInternal* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDumpAbort(std::int32_t dumpId, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDumpCreate(const char* name, std::int32_t bufferSize, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDumpSetFilterParam(std::int32_t socket, const NetDumpFilterParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDumpDestroy(std::int32_t dumpId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetDumpRead(std::int32_t dumpId, void* buffer, std::int32_t bufferSize, std::uint32_t* flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEmulationDebugSettingsSet(const NetEmulationParameter* parameter, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEmulationDebugSettingsGet(NetEmulationParameter* parameter, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEmulationSet(const NetEmulationParameter* parameter, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetEmulationGet(NetEmulationParameter* parameter, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNet/src/ResolverTestHooks.hpp b/core/libs/prx/libSceNet/src/ResolverTestHooks.hpp new file mode 100644 index 0000000000..cde7e83a8a --- /dev/null +++ b/core/libs/prx/libSceNet/src/ResolverTestHooks.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include +#include +#include + +namespace GuestSceNetTest { +using ResolverLookup = bool (*)(const char*, int, std::vector>&); +void SetResolverLookupForTests_nid_no_patch(ResolverLookup lookup) noexcept; +} diff --git a/core/libs/prx/libSceNetCtl/CMakeLists.txt b/core/libs/prx/libSceNetCtl/CMakeLists.txt index f221f21932..0083a712af 100644 --- a/core/libs/prx/libSceNetCtl/CMakeLists.txt +++ b/core/libs/prx/libSceNetCtl/CMakeLists.txt @@ -1,3 +1,17 @@ add_library(libSceNetCtl SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceNetCtl PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceNetCtl) + +if(BUILD_TESTING) + add_test_executable(guest_net_ctl_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetCtl.cpp) + target_include_directories(guest_net_ctl_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_ctl_tests PRIVATE libSceNetCtl libc) + configure_windows_unwind(guest_net_ctl_tests) + add_test(NAME guest_net_ctl COMMAND guest_net_ctl_tests) + set_tests_properties(guest_net_ctl PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) + if(WIN32) + target_link_libraries(guest_net_ctl_tests PRIVATE ws2_32) + set_tests_properties(guest_net_ctl PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index c6645e18b1..54c9d941fe 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -4,44 +4,109 @@ #include #include #else +#include #include #include +#include #include +#include +#include +#include #ifdef __linux__ #include #endif +#ifdef __APPLE__ +#include #endif +#endif +#include #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include +#include #include +#include #include #include -#ifdef _WIN32 #include -#endif // Network-control state reflects host interface addressing, not PSN sign-in status. +union NetCtlInfoV6; +struct NetCtlConnectionConfiguration; +struct NetCtlScanOptions; +struct NetCtlScanBssidList; +struct NetCtlScanInfo; +struct NetCtlNatInfoInternal; +struct NetCtlBandwidthInfo; +struct NetCtlNetEvConfiguration; +struct NetBweInfo; +struct NetCtlWifiInfo; + +static constexpr int SCE_NET_CTL_ERROR_NOT_INITIALIZED = static_cast(0x80412101); +static constexpr int SCE_NET_CTL_ERROR_ID_NOT_FOUND = static_cast(0x80412104); +static constexpr int SCE_NET_CTL_ERROR_INVALID_SIZE = static_cast(0x80412111); static constexpr int SCE_NET_CTL_ERROR_CALLBACK_MAX = static_cast(0x80412103); static constexpr int SCE_NET_CTL_ERROR_INVALID_ID = static_cast(0x80412105); +static constexpr int SCE_NET_CTL_ERROR_INVALID_CODE = static_cast(0x80412106); static constexpr int SCE_NET_CTL_ERROR_INVALID_ADDR = static_cast(0x80412107); static constexpr int SCE_NET_CTL_ERROR_NOT_CONNECTED = static_cast(0x80412108); static constexpr int NET_CTL_STATE_DISCONNECTED = 0; static constexpr int NET_CTL_STATE_IPOBTAINED = 3; +static constexpr int NET_CTL_EVENT_DISCONNECTED = 1; +static constexpr int NET_CTL_EVENT_IPOBTAINED = 3; +static constexpr int NET_CTL_INFO_DEVICE = 1; +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_BSSID = 5; +static constexpr int NET_CTL_INFO_SSID = 6; +static constexpr int NET_CTL_INFO_WIFI_SECURITY = 7; +static constexpr int NET_CTL_INFO_RSSI_DBM = 8; +static constexpr int NET_CTL_INFO_RSSI_PERCENTAGE = 9; +static constexpr int NET_CTL_INFO_CHANNEL = 10; +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 int NET_CTL_INFO_V6_IP_ADDRESS = 1; +static constexpr int NET_CTL_INFO_V6_SECONDARY_DNS = 5; +static constexpr std::uint32_t NET_CTL_DEVICE_WIRED = 0; +static constexpr std::uint32_t NET_CTL_LINK_CONNECTED = 1; +static constexpr std::uint32_t NET_CTL_IP_DHCP = 0; +static constexpr std::uint32_t NET_CTL_HTTP_PROXY_OFF = 0; +static constexpr std::uint8_t NET_CTL_ETHER_ADDR[6] = {0x02, 0x50, 0x53, 0x35, 0x00, 0x01}; +static constexpr std::size_t NET_CTL_IPV4_ADDR_STR_LEN = 16; static constexpr int MAX_CALLBACKS = 8; -static std::mutex g_callbackLock; -static NetCtlCallback g_callbacks[MAX_CALLBACKS] = {}; -static NetCtlCallback g_callbacksV6[MAX_CALLBACKS] = {}; +struct CallbackSlot { + NetCtlCallback func = nullptr; + void* arg = nullptr; + int lastEvent = 0; +}; -static int RegisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], NetCtlCallback func, int* cid) { +static std::atomic_bool g_initialized{true}; +static std::mutex g_callbackLock; +static CallbackSlot g_callbacks[MAX_CALLBACKS] = {}; +static CallbackSlot g_callbacksV6[MAX_CALLBACKS] = {}; + +static int RegisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], NetCtlCallback func, void* arg, int* cid) { + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; if (!func || !cid) return SCE_NET_CTL_ERROR_INVALID_ADDR; std::lock_guard lock(g_callbackLock); for (int index = 0; index < MAX_CALLBACKS; ++index) { - if (!callbacks[index]) { - callbacks[index] = func; + if (!callbacks[index].func) { + callbacks[index] = {func, arg, 0}; *cid = index; return 0; } @@ -49,14 +114,24 @@ static int RegisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], NetCtlCa return SCE_NET_CTL_ERROR_CALLBACK_MAX; } -static int UnregisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], int cid) { +static int UnregisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], int cid) { + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; if (cid < 0 || cid >= MAX_CALLBACKS) return SCE_NET_CTL_ERROR_INVALID_ID; std::lock_guard lock(g_callbackLock); - callbacks[cid] = nullptr; + if (!callbacks[cid].func) return SCE_NET_CTL_ERROR_ID_NOT_FOUND; + callbacks[cid] = {}; return 0; } -static bool HostHasAddress(int family) { +static bool HasIpv4Callback() { + std::lock_guard lock(g_callbackLock); + for (const auto& slot : g_callbacks) { + if (slot.func) return true; + } + return false; +} + +static bool QueryHostHasAddress(int family) { #ifdef _WIN32 static const int socket_status = [] { WSADATA data{}; @@ -105,6 +180,302 @@ static bool HostHasAddress(int family) { #endif } +struct HostIpv4Interface { + in_addr address{}; + in_addr netmask{}; + in_addr gateway{}; + std::uint32_t mtu = 0; + std::vector dns; +}; + +#ifdef _WIN32 +namespace { + +struct HostAddressCache { + std::mutex lock; + bool registered = false; + bool tried = false; + std::atomic generation{1}; + std::uint64_t seen[3]{}; + bool value[3]{}; + std::uint64_t interfaceSeen = 0; + bool interfaceFound = false; + HostIpv4Interface interfaceValue; +}; + +HostAddressCache& AddressCache() { + static HostAddressCache cache; + return cache; +} + +void CALLBACK HostAddressChanged(PVOID, PMIB_UNICASTIPADDRESS_ROW, MIB_NOTIFICATION_TYPE) { + AddressCache().generation.fetch_add(1, std::memory_order_acq_rel); +} + +void CALLBACK HostInterfaceChanged(PVOID, PMIB_IPINTERFACE_ROW, MIB_NOTIFICATION_TYPE) { + AddressCache().generation.fetch_add(1, std::memory_order_acq_rel); +} + +void CALLBACK HostRouteChanged(PVOID, PMIB_IPFORWARD_ROW2, MIB_NOTIFICATION_TYPE) { + AddressCache().generation.fetch_add(1, std::memory_order_acq_rel); +} + +void WatchHostAddresses(HostAddressCache& cache) { + if (cache.tried) return; + cache.tried = true; + HANDLE addresses = nullptr; + HANDLE interfaces = nullptr; + HANDLE routes = nullptr; + cache.registered = NotifyUnicastIpAddressChange(AF_UNSPEC, HostAddressChanged, nullptr, FALSE, &addresses) == NO_ERROR && + NotifyIpInterfaceChange(AF_UNSPEC, HostInterfaceChanged, nullptr, FALSE, &interfaces) == NO_ERROR && + NotifyRouteChange2(AF_UNSPEC, HostRouteChanged, nullptr, FALSE, &routes) == NO_ERROR; + if (!cache.registered) { + if (addresses != nullptr) CancelMibChangeNotify2(addresses); + if (interfaces != nullptr) CancelMibChangeNotify2(interfaces); + if (routes != nullptr) CancelMibChangeNotify2(routes); + } +} + +} +#endif + +static bool HostHasAddress(int family) { +#ifdef _WIN32 + auto& cache = AddressCache(); + const int slot = family == AF_INET ? 0 : family == AF_INET6 ? 1 : 2; + std::lock_guard guard(cache.lock); + WatchHostAddresses(cache); + const auto generation = cache.generation.load(std::memory_order_acquire); + if (cache.registered && cache.seen[slot] == generation) return cache.value[slot]; + const bool value = QueryHostHasAddress(family); + cache.value[slot] = value; + cache.seen[slot] = generation; + return value; +#else + return QueryHostHasAddress(family); +#endif +} + +static bool UsableIpv4(in_addr address) { + const auto host = ntohl(address.s_addr); + return host != 0 && (host >> 24) != 127; +} + +#ifdef _WIN32 +static const sockaddr_in* Ipv4Of(const SOCKET_ADDRESS& address) { + if (!address.lpSockaddr || address.lpSockaddr->sa_family != AF_INET) return nullptr; + return reinterpret_cast(address.lpSockaddr); +} + +static std::uint32_t PrefixMask(unsigned length) { + if (length == 0) return 0; + return htonl(length >= 32 ? 0xFFFFFFFFu : 0xFFFFFFFFu << (32 - length)); +} +#else +struct HostDefaultRoute { + std::string name; + in_addr gateway{}; + unsigned long metric = 0; +}; + +#ifdef __APPLE__ +struct NetCtlDefaultRoute { + char name[IF_NAMESIZE]; + in_addr gateway; + std::uint32_t metric; +}; + +extern "C" std::size_t NetCtlDefaultRoutes_nid_no_patch(const char* table, std::size_t size, NetCtlDefaultRoute* routes, std::size_t capacity) { + std::size_t count = 0; + for (std::size_t offset = 0; offset + sizeof(rt_msghdr) <= size;) { + const auto* header = reinterpret_cast(table + offset); + if (header->rtm_msglen < sizeof(rt_msghdr) || header->rtm_msglen > size - offset) break; + const sockaddr* addresses[RTAX_MAX] = {}; + const std::size_t end = offset + header->rtm_msglen; + std::size_t field = offset + sizeof(rt_msghdr); + for (int index = 0; index < RTAX_MAX && field + sizeof(sockaddr) <= end; ++index) { + if (!(header->rtm_addrs & (1 << index))) continue; + const auto* address = reinterpret_cast(table + field); + addresses[index] = address; + field += address->sa_len ? (address->sa_len + sizeof(std::uint32_t) - 1) & ~(sizeof(std::uint32_t) - 1) : sizeof(std::uint32_t); + } + offset = end; + const auto* destination = addresses[RTAX_DST]; + const auto* gateway = addresses[RTAX_GATEWAY]; + if (!destination || destination->sa_family != AF_INET || !gateway || gateway->sa_family != AF_INET) continue; + if (reinterpret_cast(destination)->sin_addr.s_addr != 0) continue; + if (!(header->rtm_flags & RTF_UP) || (header->rtm_flags & (RTF_REJECT | RTF_HOST))) continue; + NetCtlDefaultRoute route{}; + if (!if_indextoname(header->rtm_index, route.name)) continue; + route.gateway = reinterpret_cast(gateway)->sin_addr; + route.metric = (header->rtm_flags & RTF_IFSCOPE) ? 1 : 0; + if (count < capacity) routes[count] = route; + ++count; + } + return count; +} + +static std::vector HostDefaultRoutes() { + int name[] = {CTL_NET, PF_ROUTE, 0, AF_INET, NET_RT_FLAGS, RTF_GATEWAY}; + std::size_t size = 0; + if (sysctl(name, 6, nullptr, &size, nullptr, 0) != 0) throw std::runtime_error("sceNetCtlGetInfo: the host routing table is unavailable"); + std::vector table(size); + if (sysctl(name, 6, table.data(), &size, nullptr, 0) != 0) throw std::runtime_error("sceNetCtlGetInfo: the host routing table is unavailable"); + std::vector found(NetCtlDefaultRoutes_nid_no_patch(table.data(), size, nullptr, 0)); + found.resize(NetCtlDefaultRoutes_nid_no_patch(table.data(), size, found.data(), found.size())); + std::vector routes; + for (const auto& entry : found) routes.push_back({entry.name, entry.gateway, entry.metric}); + return routes; +} +#else +static std::vector HostDefaultRoutes() { + std::ifstream table("/proc/net/route"); + if (!table) throw std::runtime_error("sceNetCtlGetInfo: the host routing table is unavailable"); + std::vector routes; + std::string line; + std::getline(table, line); + while (std::getline(table, line)) { + std::istringstream fields(line); + HostDefaultRoute route; + unsigned long destination = 0; + unsigned long gateway = 0; + unsigned long flags = 0; + unsigned long references = 0; + unsigned long uses = 0; + unsigned long mask = 0; + if (!(fields >> route.name >> std::hex >> destination >> gateway >> flags >> std::dec >> references >> uses >> route.metric >> std::hex >> mask)) continue; + if (destination != 0 || mask != 0 || !(flags & RTF_UP) || (flags & RTF_REJECT)) continue; + route.gateway.s_addr = static_cast(gateway); + routes.push_back(route); + } + return routes; +} +#endif + +static std::uint32_t HostInterfaceMtu(const char* name) { + const int descriptor = socket(AF_INET, SOCK_DGRAM, 0); + if (descriptor < 0) throw std::runtime_error("sceNetCtlGetInfo: no socket to read the host interface MTU"); + ifreq request{}; + std::strncpy(request.ifr_name, name, IFNAMSIZ - 1); + const int result = ioctl(descriptor, SIOCGIFMTU, &request); + close(descriptor); + if (result != 0) throw std::runtime_error(std::string("sceNetCtlGetInfo: no MTU for host interface ") + name); + return static_cast(request.ifr_mtu); +} + +static std::vector HostDnsServers() { + std::ifstream config("/etc/resolv.conf"); + std::vector servers; + std::string line; + while (std::getline(config, line)) { + std::istringstream fields(line); + std::string keyword; + std::string value; + in_addr server{}; + if (fields >> keyword >> value && keyword == "nameserver" && inet_pton(AF_INET, value.c_str(), &server) == 1) servers.push_back(server); + } + return servers; +} +#endif + +static bool QueryHostIpv4Interface(HostIpv4Interface& found) { +#ifdef _WIN32 + ULONG size = 0; + const ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS; + if (GetAdaptersAddresses(AF_INET, flags, nullptr, nullptr, &size) != ERROR_BUFFER_OVERFLOW) return false; + std::vector storage(size); + auto* adapters = reinterpret_cast(storage.data()); + if (GetAdaptersAddresses(AF_INET, flags, nullptr, adapters, &size) != NO_ERROR) return false; + bool chosen = false; + bool chosenHasGateway = false; + ULONG chosenMetric = 0; + for (auto* adapter = adapters; adapter; adapter = adapter->Next) { + if (adapter->OperStatus != IfOperStatusUp || adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK) continue; + auto* unicast = adapter->FirstUnicastAddress; + while (unicast && !(Ipv4Of(unicast->Address) && UsableIpv4(Ipv4Of(unicast->Address)->sin_addr))) unicast = unicast->Next; + if (!unicast) continue; + auto* gateway = adapter->FirstGatewayAddress; + while (gateway && !Ipv4Of(gateway->Address)) gateway = gateway->Next; + if (chosen && !(gateway && (!chosenHasGateway || adapter->Ipv4Metric < chosenMetric))) continue; + HostIpv4Interface candidate; + candidate.address = Ipv4Of(unicast->Address)->sin_addr; + candidate.netmask.s_addr = PrefixMask(unicast->OnLinkPrefixLength); + if (gateway) candidate.gateway = Ipv4Of(gateway->Address)->sin_addr; + candidate.mtu = adapter->Mtu; + for (auto* server = adapter->FirstDnsServerAddress; server; server = server->Next) { + if (Ipv4Of(server->Address)) candidate.dns.push_back(Ipv4Of(server->Address)->sin_addr); + } + found = std::move(candidate); + chosen = true; + chosenHasGateway = gateway != nullptr; + chosenMetric = adapter->Ipv4Metric; + } + return chosen; +#else + ifaddrs* list = nullptr; + if (getifaddrs(&list) != 0) return false; + const std::unique_ptr interfaces(list, freeifaddrs); + const auto routes = HostDefaultRoutes(); + const ifaddrs* chosen = nullptr; + const HostDefaultRoute* chosenRoute = nullptr; + for (auto* item = list; item; item = item->ifa_next) { + if (!item->ifa_addr || item->ifa_addr->sa_family != AF_INET || !(item->ifa_flags & IFF_UP) || (item->ifa_flags & IFF_LOOPBACK)) continue; + if (!UsableIpv4(reinterpret_cast(item->ifa_addr)->sin_addr)) continue; + const HostDefaultRoute* route = nullptr; + for (const auto& entry : routes) { + if (entry.name == item->ifa_name && (!route || entry.metric < route->metric)) route = &entry; + } + if (chosen && !(route && (!chosenRoute || route->metric < chosenRoute->metric))) continue; + chosen = item; + chosenRoute = route; + } + if (!chosen) return false; + found.address = reinterpret_cast(chosen->ifa_addr)->sin_addr; + if (chosen->ifa_netmask) found.netmask = reinterpret_cast(chosen->ifa_netmask)->sin_addr; + if (chosenRoute) found.gateway = chosenRoute->gateway; + found.mtu = HostInterfaceMtu(chosen->ifa_name); + found.dns = HostDnsServers(); + return true; +#endif +} + +static bool FindHostIpv4Interface(HostIpv4Interface& found) { +#ifdef _WIN32 + auto& cache = AddressCache(); + std::lock_guard guard(cache.lock); + WatchHostAddresses(cache); + const auto generation = cache.generation.load(std::memory_order_acquire); + if (!cache.registered || cache.interfaceSeen != generation) { + HostIpv4Interface value; + cache.interfaceFound = QueryHostIpv4Interface(value); + cache.interfaceValue = std::move(value); + cache.interfaceSeen = generation; + } + if (cache.interfaceFound) found = cache.interfaceValue; + return cache.interfaceFound; +#else + return QueryHostIpv4Interface(found); +#endif +} + +extern "C" std::size_t NetCtlHostDnsServers_nid_no_patch(std::uint32_t* servers, std::size_t capacity) { + HostIpv4Interface host; + if (!FindHostIpv4Interface(host)) return 0; + for (std::size_t index = 0; index < host.dns.size() && index < capacity; ++index) servers[index] = host.dns[index].s_addr; + return host.dns.size(); +} + +static void WriteInfoWord(NetCtlInfo* info, std::uint32_t value) { + std::memcpy(info->opaque, &value, sizeof(value)); +} + +static void WriteInfoAddress(NetCtlInfo* info, in_addr address) { + const auto* bytes = reinterpret_cast(&address.s_addr); + char text[NET_CTL_IPV4_ADDR_STR_LEN] = {}; + std::snprintf(text, sizeof(text), "%u.%u.%u.%u", static_cast(bytes[0]), static_cast(bytes[1]), static_cast(bytes[2]), static_cast(bytes[3])); + std::memcpy(info->opaque, text, sizeof(text)); +} + extern "C" std::uint64_t NetCtlReadByteCounter_nid_no_patch(const char* path) { std::ifstream file(path); std::uint64_t value = 0; @@ -144,10 +515,25 @@ static void HostTraffic(std::uint64_t& sent, std::uint64_t& received) { extern "C" { int APS5_VABI sceNetCtlCheckCallback(void) { + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; + if (!HasIpv4Callback()) return 0; + const int event = HostHasAddress(AF_UNSPEC) ? NET_CTL_EVENT_IPOBTAINED : NET_CTL_EVENT_DISCONNECTED; + CallbackSlot due[MAX_CALLBACKS]; + int count = 0; + { + std::lock_guard lock(g_callbackLock); + for (auto& slot : g_callbacks) { + if (!slot.func || slot.lastEvent == event) continue; + slot.lastEvent = event; + due[count++] = slot; + } + } + for (int index = 0; index < count; ++index) due[index].func(event, due[index].arg); return 0; } int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat* stat) { + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; if (!stat) return SCE_NET_CTL_ERROR_INVALID_ADDR; if (!HostHasAddress(AF_UNSPEC)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; *stat = {}; @@ -156,45 +542,83 @@ int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat* stat) { } int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) { - (void)code; - (void)info; - return SCE_NET_CTL_ERROR_NOT_CONNECTED; -} - -int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* nat_info) { - (void)nat_info; - return SCE_NET_CTL_ERROR_NOT_CONNECTED; -} - -int APS5_VABI sceNetCtlGetResult(int event_type, int* error_code) { - (void)event_type; - if (!error_code) return SCE_NET_CTL_ERROR_INVALID_ADDR; - *error_code = 0; + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; + if (!info) return SCE_NET_CTL_ERROR_INVALID_ADDR; + if (code < NET_CTL_INFO_DEVICE || code > NET_CTL_INFO_HTTP_PROXY_PORT) return SCE_NET_CTL_ERROR_INVALID_CODE; + HostIpv4Interface host; + if (!FindHostIpv4Interface(host)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; + switch (code) { + case NET_CTL_INFO_DEVICE: WriteInfoWord(info, NET_CTL_DEVICE_WIRED); break; + case NET_CTL_INFO_ETHER_ADDR: std::memcpy(info->opaque, NET_CTL_ETHER_ADDR, sizeof(NET_CTL_ETHER_ADDR)); break; + case NET_CTL_INFO_MTU: WriteInfoWord(info, host.mtu); break; + case NET_CTL_INFO_LINK: WriteInfoWord(info, NET_CTL_LINK_CONNECTED); break; + case NET_CTL_INFO_IP_CONFIG: WriteInfoWord(info, NET_CTL_IP_DHCP); break; + case NET_CTL_INFO_IP_ADDRESS: WriteInfoAddress(info, host.address); break; + case NET_CTL_INFO_NETMASK: WriteInfoAddress(info, host.netmask); break; + case NET_CTL_INFO_DEFAULT_ROUTE: WriteInfoAddress(info, host.gateway); break; + 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_BSSID: + case NET_CTL_INFO_SSID: + 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; + case NET_CTL_INFO_WIFI_SECURITY: + case NET_CTL_INFO_RSSI_DBM: + case NET_CTL_INFO_RSSI_PERCENTAGE: + case NET_CTL_INFO_CHANNEL: + case NET_CTL_INFO_HTTP_PROXY_PORT: WriteInfoWord(info, 0); break; + default: throw std::runtime_error("sceNetCtlGetInfo: info code " + std::to_string(code) + " is not implemented"); + } return 0; } +int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* info) { + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; + if (!info) return SCE_NET_CTL_ERROR_INVALID_ADDR; + if (info->size != sizeof(NetCtlNatInfo)) return SCE_NET_CTL_ERROR_INVALID_SIZE; + throw std::logic_error("sceNetCtlGetNatInfo: NAT discovery is not implemented"); +} + +int APS5_VABI sceNetCtlGetResult(int event, int* result) { + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; + if (!result) return SCE_NET_CTL_ERROR_INVALID_ADDR; + throw std::logic_error("sceNetCtlGetResult: service event results are not implemented"); +} + int APS5_VABI sceNetCtlGetState(int* state) { + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; if (!state) return SCE_NET_CTL_ERROR_INVALID_ADDR; *state = HostHasAddress(AF_UNSPEC) ? NET_CTL_STATE_IPOBTAINED : NET_CTL_STATE_DISCONNECTED; return 0; } int APS5_VABI sceNetCtlGetStateV6(int* state) { + if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; if (!state) return SCE_NET_CTL_ERROR_INVALID_ADDR; *state = HostHasAddress(AF_INET6) ? NET_CTL_STATE_IPOBTAINED : NET_CTL_STATE_DISCONNECTED; return 0; } int APS5_VABI sceNetCtlInit(void) { + g_initialized.store(true); return 0; } int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback func, void* arg, int* cid) { - return RegisterCallback(g_callbacks, func, cid); + return RegisterCallback(g_callbacks, func, arg, cid); } int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback func, void* arg, int* cid) { - return RegisterCallback(g_callbacksV6, func, cid); + return RegisterCallback(g_callbacksV6, func, arg, cid); +} + +int APS5_VABI sceNetCtlGetInfoV6(std::uint32_t code, NetCtlInfoV6* info) { + if (!info) return SCE_NET_CTL_ERROR_INVALID_ADDR; + if (code < NET_CTL_INFO_V6_IP_ADDRESS || code > NET_CTL_INFO_V6_SECONDARY_DNS) return SCE_NET_CTL_ERROR_INVALID_CODE; + if (!HostHasAddress(AF_INET6)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; + throw std::runtime_error("sceNetCtlGetInfoV6: info code " + std::to_string(code) + " is not implemented"); } void APS5_VABI sceNetCtlTerm(void) { @@ -208,4 +632,352 @@ int APS5_VABI sceNetCtlUnregisterCallbackV6(int cid) { return UnregisterCallback(g_callbacksV6, cid); } +int APS5_VABI sceNetCtlRegisterCallbackForNpToolkit(NetCtlCallback callback, void* argument, int* callbackId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlRegisterCallbackIpcInt(NetCtlCallback callback, void* argument, int* callbackId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlRegisterCallbackForLibIpcInt(NetCtlCallback callback, void* argument, int* callbackId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnregisterCallbackForNpToolkit(int callbackId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnregisterCallbackIpcInt(int callbackId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnregisterCallbackForLibIpcInt(int callbackId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlCheckCallbackForNpToolkit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlCheckCallbackForLibIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlClearEventIpcInt(int event) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlClearEventForNpToolkit(int event) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlClearEventForLibIpcInt(int event) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetResultIpcInt(int event, int* result) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetStateIpcInt(int* state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetState2IpcInt(int* state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetInfoIpcInt(std::uint32_t code, NetCtlInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetEtherLinkMode(int* mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetWifiType(int* type) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlConnectIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlConnectWithRetryIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlConnectConfIpcInt(const NetCtlConnectionConfiguration* configuration) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlDisconnectIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlScanIpcInt(const char* const* ssids, std::uint32_t count, const NetCtlScanOptions* options) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoBssidIpcInt(NetCtlScanBssidList* bssids, std::uint32_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoByBssidIpcInt(const std::uint8_t* bssid, NetCtlScanInfo* info, void* buffer, std::size_t* size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoForSsidScanIpcInt(NetCtlScanInfo* info, void* buffer, std::size_t* size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoBssidForSsidListScanIpcInt(NetCtlScanBssidList* bssids, std::uint32_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetScanInfoForSsidListScanIpcInt(const std::uint8_t* bssid, NetCtlScanInfo* info, void* buffer, std::size_t* size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlSetErrorNotificationEnabledIpcInt(int enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlRegisterCallbackV6IpcInt(NetCtlCallback callback, void* argument, int* callbackId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnregisterCallbackV6IpcInt(int callbackId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetResultV6(int event_type, int* error_code) { + if (!error_code) return SCE_NET_CTL_ERROR_INVALID_ADDR; + if (event_type < NET_CTL_EVENT_DISCONNECTED || event_type > NET_CTL_EVENT_IPOBTAINED) { + throw std::runtime_error("sceNetCtlGetResultV6: event type " + std::to_string(event_type) + " is not implemented"); + } + *error_code = 0; + return 0; +} + +int APS5_VABI sceNetCtlGetResultV6IpcInt(int event, int* result) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetStateV6IpcInt(int* state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetInfoV6IpcInt(std::uint32_t code, NetCtlInfoV6* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetNatInfoIpcInt(NetCtlNatInfoInternal* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlSetStunWithPaddingFlagIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlUnsetStunWithPaddingFlagIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlIsBandwidthManagementEnabledIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlDisableBandwidthManagementIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlEnableBandwidthManagementIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetBandwidthInfoIpcInt(NetCtlBandwidthInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetNetEvConfigInfoIpcInt(NetCtlNetEvConfiguration* configuration) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetConnectionElapsedTimeIpcInt(std::uint64_t* elapsedTime) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpCheckCallback(void) { + return 0; +} + +int APS5_VABI sceNetCtlApRpClearEvent(void) { + return 0; +} + +int APS5_VABI sceNetCtlApRpRegisterCallback(void) { + return 0; +} + +int APS5_VABI sceNetCtlApRpUnregisterCallback(void) { + return 0; +} + +int APS5_VABI sceNetCtlApRpGetResult(void) { + return 0; +} + +int APS5_VABI sceNetCtlApRpGetState(void) { + return 0; +} + +int APS5_VABI sceNetCtlApRpGetInfo(void) { + return 0; +} + +int APS5_VABI sceNetCtlApRpStart(void) { + return 0; +} + +int APS5_VABI sceNetCtlApCpStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApAppStartWithRetryPid(std::int32_t processId, const char* passcode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApAppStartWithRetry(const char* passcode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRpStartWithRetry(void) { + return 0; +} + +int APS5_VABI sceNetCtlApRpStartConf(void) { + return 0; +} + +int APS5_VABI sceNetCtlApRpStop(void) { + return 0; +} + +int APS5_VABI sceNetCtlApCpStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApAppInitWpaKey(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApAppInitWpaKeyForQa(std::uint8_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlApRestart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweCheckCallbackIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweClearEventIpcInt(int event) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweRegisterCallbackIpcInt(NetCtlCallback callback, void* argument, int* callbackId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweUnregisterCallbackIpcInt(int callbackId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweGetInfoIpcInt(NetBweInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweStartInternetConnectionTestIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweFinishInternetConnectionTestIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweStartInternetConnectionTestBandwidthTestIpcInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetBweSetInternetConnectionTestResultIpcInt(std::int32_t firstResult, std::int32_t secondResult) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetCtlGetWifiInfoIpcInt(NetCtlWifiInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNetCtlAp/Export.cpp b/core/libs/prx/libSceNetCtlAp/Export.cpp new file mode 100644 index 0000000000..9da3953097 --- /dev/null +++ b/core/libs/prx/libSceNetCtlAp/Export.cpp @@ -0,0 +1,135 @@ +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int NET_CTL_AP_ERROR_INVALID_OPT_STR = static_cast(0x80412901); +constexpr int NET_CTL_AP_ERROR_NOT_STARTED = static_cast(0x8041298F); +constexpr int NET_CTL_AP_ERROR_INVALID_TYPE = static_cast(0x80412990); +constexpr int NET_CTL_AP_ERROR_INVALID_ADDR = static_cast(0x80412991); +constexpr int NET_CTL_AP_ERROR_NOT_INITIALIZED = static_cast(0x80412994); +constexpr int NET_CTL_AP_ERROR_CALLBACK_MAX = static_cast(0x80412995); +constexpr int NET_CTL_AP_ERROR_INVALID_ID = static_cast(0x80412996); +constexpr int NET_CTL_AP_ERROR_ID_NOT_FOUND = static_cast(0x80412997); +constexpr int NET_CTL_AP_ERROR_INVALID_SIZE = static_cast(0x80412998); +constexpr int NET_CTL_AP_STATE_STOPPED = 0; +constexpr int NET_CTL_AP_EVENT_TYPE_FIRST = 1; +constexpr int NET_CTL_AP_EVENT_TYPE_LAST = 4; +constexpr std::size_t NET_CTL_AP_INFO_SIZE = 152; +constexpr std::size_t NET_CTL_AP_CONNECT_INFO_SIZE = 184; +constexpr int MAX_CALLBACKS = 8; + +using NetCtlApCallback = void(APS5_VABI*)(int, void*); + +struct CallbackSlot { + NetCtlApCallback func = nullptr; + void* arg = nullptr; +}; + +std::mutex g_lock; +bool g_initialized = false; +CallbackSlot g_callbacks[MAX_CALLBACKS] = {}; + +bool ValidEventType(int eventType) { + return eventType >= NET_CTL_AP_EVENT_TYPE_FIRST && eventType <= NET_CTL_AP_EVENT_TYPE_LAST; +} + +} + +extern "C" { + +int APS5_VABI sceNetCtlApInit() { + std::lock_guard lock(g_lock); + if (g_initialized) throw std::runtime_error("sceNetCtlApInit: a second initialization is not implemented"); + g_initialized = true; + return 0; +} + +void APS5_VABI sceNetCtlApTerm() { + std::lock_guard lock(g_lock); + g_initialized = false; + for (auto& slot : g_callbacks) slot = {}; +} + +int APS5_VABI sceNetCtlApCheckCallback() { + std::lock_guard lock(g_lock); + return g_initialized ? 0 : NET_CTL_AP_ERROR_NOT_INITIALIZED; +} + +int APS5_VABI sceNetCtlApClearEvent(int eventType) { + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + return ValidEventType(eventType) ? 0 : NET_CTL_AP_ERROR_INVALID_TYPE; +} + +int APS5_VABI sceNetCtlApRegisterCallback(NetCtlApCallback func, void* arg, int* cid) { + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (!func || !cid) return NET_CTL_AP_ERROR_INVALID_ADDR; + for (int index = 0; index < MAX_CALLBACKS; ++index) { + if (!g_callbacks[index].func) { + g_callbacks[index] = {func, arg}; + *cid = index; + return 0; + } + } + return NET_CTL_AP_ERROR_CALLBACK_MAX; +} + +int APS5_VABI sceNetCtlApUnregisterCallback(int cid) { + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (cid < 0 || cid >= MAX_CALLBACKS) return NET_CTL_AP_ERROR_INVALID_ID; + if (!g_callbacks[cid].func) return NET_CTL_AP_ERROR_ID_NOT_FOUND; + g_callbacks[cid] = {}; + return 0; +} + +int APS5_VABI sceNetCtlApGetResult(int eventType, int* errorCode) { + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (!errorCode) return NET_CTL_AP_ERROR_INVALID_ADDR; + if (!ValidEventType(eventType)) return NET_CTL_AP_ERROR_INVALID_TYPE; + *errorCode = 0; + return 0; +} + +int APS5_VABI sceNetCtlApGetInfo(void* info) { + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (!info) return NET_CTL_AP_ERROR_INVALID_ADDR; + if (*static_cast(info) != NET_CTL_AP_INFO_SIZE) return NET_CTL_AP_ERROR_INVALID_SIZE; + return NET_CTL_AP_ERROR_NOT_STARTED; +} + +int APS5_VABI sceNetCtlApGetState(int* state) { + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (!state) return NET_CTL_AP_ERROR_INVALID_ADDR; + *state = NET_CTL_AP_STATE_STOPPED; + return 0; +} + +int APS5_VABI sceNetCtlApStop() { + std::lock_guard lock(g_lock); + return g_initialized ? NET_CTL_AP_ERROR_NOT_STARTED : NET_CTL_AP_ERROR_NOT_INITIALIZED; +} + +int APS5_VABI sceNetCtlApGetConnectInfo(const char* optStr, void* connectInfo) { + std::lock_guard lock(g_lock); + if (!g_initialized) return NET_CTL_AP_ERROR_NOT_INITIALIZED; + if (optStr != nullptr) { + std::size_t length = 0; + while (length < 9 && optStr[length] != 0) ++length; + if (length > 8) return NET_CTL_AP_ERROR_INVALID_OPT_STR; + } + if (!connectInfo) return NET_CTL_AP_ERROR_INVALID_ADDR; + if (*static_cast(connectInfo) != NET_CTL_AP_CONNECT_INFO_SIZE) return NET_CTL_AP_ERROR_INVALID_SIZE; + return NET_CTL_AP_ERROR_NOT_STARTED; +} + +} diff --git a/core/libs/prx/libSceNetCtlApDialog/Export.cpp b/core/libs/prx/libSceNetCtlApDialog/Export.cpp index e1d2bc4a27..64a0d3086e 100644 --- a/core/libs/prx/libSceNetCtlApDialog/Export.cpp +++ b/core/libs/prx/libSceNetCtlApDialog/Export.cpp @@ -3,6 +3,7 @@ #include #include #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" namespace { @@ -12,19 +13,13 @@ 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_INVALID_ARGUMENT = static_cast(0x80B8000Au); +constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast(0x80B8000Bu); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; -struct SceNetCtlApDialogResult { - std::int32_t result = COMMON_DIALOG_RESULT_USER_CANCELED; - std::uint8_t reserved[36] = {}; -}; - -static_assert(sizeof(SceNetCtlApDialogResult) == 40); - std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; } @@ -42,17 +37,20 @@ int APS5_VABI sceNetCtlApDialogTerminate(void) { return 0; } -int APS5_VABI sceNetCtlApDialogOpen(const void* param) { +int APS5_VABI sceNetCtlApDialogOpen(const NetCtlApDialogParam* param) { + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (!ValidateCommonDialogBaseParam(param->base)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (param->size != sizeof(*param) || param->reserved60 != 0 || std::find(std::begin(param->option), std::end(param->option), '\0') == std::end(param->option) || !std::all_of(std::begin(param->reserved), std::end(param->reserved), [](std::uint8_t value) { return value == 0; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; const int status = g_status.load(); if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; - if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } int APS5_VABI sceNetCtlApDialogClose(void) { if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_status.load() != COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_NOT_RUNNING; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } @@ -65,14 +63,14 @@ int APS5_VABI sceNetCtlApDialogGetStatus(void) { return g_status.load(); } -int APS5_VABI sceNetCtlApDialogGetResult(void* result) { +int APS5_VABI sceNetCtlApDialogGetResult(NetCtlApDialogResult* result) { + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; const int status = g_status.load(); if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (!std::all_of(std::begin(result->reserved), std::end(result->reserved), [](std::uint8_t value) { return value == 0; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - const SceNetCtlApDialogResult dialogResult{}; - std::memcpy(result, &dialogResult, sizeof(dialogResult)); - return 0; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + return COMMON_DIALOG_RESULT_USER_CANCELED; } } diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index ca10bd5243..93bb6c445e 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 VoiceHandles) 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/Export.cpp b/core/libs/prx/libSceNgs2.native/Export.cpp new file mode 100644 index 0000000000..984f3782fe --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/Export.cpp @@ -0,0 +1,83 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNgs2JobSchedulerResetOption(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2ModuleArrayEnumItems(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2ModuleEnumConfigs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2ModuleQueueEnumItems(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2RackQueryInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2RackRunCommands(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2StreamCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2StreamCreateWithAllocator(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2StreamDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2StreamQueryBufferSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2StreamQueryInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2StreamResetOption(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2StreamRunCommands(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2SystemQueryInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2SystemRunCommands(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake index 877276b0b5..b89226f992 100644 --- a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake +++ b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake @@ -9,7 +9,6 @@ function(add_sce_ngs2_library target) ${ngs2Dir}/src/Render.cpp ${ngs2Dir}/src/Reverb.cpp ${ngs2Dir}/src/System.cpp - ${ngs2Dir}/src/Unimplemented.cpp ${ngs2Dir}/src/Voice.cpp ) target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index 2e7886f412..f2d40adab9 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -9,11 +9,25 @@ 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); +static constexpr int SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE = static_cast(0x804A8450); +static constexpr int SCE_NGS2_ERROR_INVALID_PAN_SPEAKER = static_cast(0x804A8451); +static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_HANDLE = static_cast(0x804A8203); +static constexpr int SCE_NGS2_ERROR_NULL_PARAM_LIST = static_cast(0x804A8360); +static constexpr int SCE_NGS2_ERROR_INVALID_GEOM_LISTENER_ADDRESS = static_cast(0x804A8461); +static constexpr int SCE_NGS2_ERROR_INVALID_GEOM_FLAG = static_cast(0x804A8463); +static constexpr int SCE_NGS2_ERROR_INVALID_OPTION_SIZE = static_cast(0x804A8013); +static constexpr int SCE_NGS2_ERROR_INVALID_NUM_CHANNELS = static_cast(0x804A8052); +static constexpr int SCE_NGS2_ERROR_INVALID_PITCH_SHIFT_OPTION = static_cast(0x804A8098); +static constexpr int SCE_NGS2_ERROR_INVALID_REVERB_OPTION = static_cast(0x804A8099); +static constexpr int SCE_NGS2_ERROR_INVALID_STATE_SIZE = static_cast(0x804A8302); +static constexpr int SCE_NGS2_ERROR_INVALID_BUFFER_INDEX = static_cast(0x804A8306); +static constexpr int SCE_NGS2_ERROR_INVALID_NUM_BUFFERS = static_cast(0x804A830B); static constexpr std::uint32_t SCE_NGS2_RACK_ID_SAMPLER = 0x1000; static constexpr std::uint32_t SCE_NGS2_RACK_ID_SUBMIXER = 0x2000; @@ -64,12 +78,25 @@ static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_EXIT_LOOP = 0 static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH = 0x40010005; static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP = 0x40020000; static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER = 0x40001300; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN = 0x40001500; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX = 0x40001d00; static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 = 0x40001f00; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_MODULES = 24; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_PORTS = 16; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_BUFFERS = 4; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_MAX_CHANNELS = 37; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_MAX_INPUTS = 2; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_REVERB_OPTION_SIZE = 12; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_PITCH_SHIFT_OPTION_SIZE = 8; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_PITCH_SHIFT_STATE_SIZE = 8; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_FILTER = 0x13; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_GAIN = 0x15; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_REVERB = 0x1b; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX = 0x1d; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2 = 0x1f; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT = 0x20; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_CONVOLUTION = 0x23; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE = 1; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND = 2; @@ -502,6 +529,15 @@ struct Ngs2CustomSubmixerVoiceSetupParam { }; static_assert(sizeof(Ngs2CustomSubmixerVoiceSetupParam) == 24); +struct Ngs2CustomVoiceUserFxParam { + Ngs2VoiceParamHeader header; + Ngs2UserFxProcessHandler handler; + std::uintptr_t user_data0; + std::uintptr_t user_data1; + std::uintptr_t user_data2; +}; +static_assert(sizeof(Ngs2CustomVoiceUserFxParam) == 40); + struct Ngs2CustomVoiceUserFx2Param { Ngs2VoiceParamHeader header; const void* data; @@ -509,6 +545,12 @@ struct Ngs2CustomVoiceUserFx2Param { }; static_assert(sizeof(Ngs2CustomVoiceUserFx2Param) == 24); +struct Ngs2CustomVoiceGainParam { + Ngs2VoiceParamHeader header; + float gain[SCE_NGS2_CUSTOM_SUBMIXER_MAX_CHANNELS]; +}; +static_assert(sizeof(Ngs2CustomVoiceGainParam) == 156); + struct Ngs2CustomVoiceFilterParam { Ngs2VoiceParamHeader header; std::uint32_t type; @@ -619,6 +661,15 @@ struct Ngs2SamplerVoiceState { }; static_assert(sizeof(Ngs2SamplerVoiceState) == 56); +struct Ngs2CustomSamplerVoiceState { + Ngs2VoiceState voice_state; + std::uint32_t reserved[2]; + std::uint64_t num_decoded_samples; + std::uint64_t reserved2[3]; +}; +static_assert(sizeof(Ngs2CustomSamplerVoiceState) == 48); +static_assert(offsetof(Ngs2CustomSamplerVoiceState, num_decoded_samples) == 16); + struct Ngs2WaveformInfo { Ngs2WaveformFormat format; std::uint32_t data_offset; @@ -727,4 +778,14 @@ struct Ngs2GeomAttribute { }; static_assert(sizeof(Ngs2GeomAttribute) == 308); +struct Ngs2ReportDataHeader { + std::size_t size; + Ngs2Handle handle; + std::uint32_t type; + std::int32_t result; +}; +static_assert(sizeof(Ngs2ReportDataHeader) == 24); + +using Ngs2ReportHandler = void (APS5_VABI *)(const Ngs2ReportDataHeader* data, std::uintptr_t user_data); + #endif diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index b508ac56a3..9357c6a942 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -23,6 +23,38 @@ static constexpr std::size_t FMT_GUID_OFFSET = 24; static constexpr std::size_t FMT_CONFIG_OFFSET = 44; static constexpr std::size_t FACT_ATRAC9_SIZE = 12; static constexpr std::uint32_t MAX_SAMPLE_RATE = 192000; +static constexpr std::uint32_t WAVEFORM_TYPE_PCM_FIRST = 0x10; +static constexpr std::uint32_t WAVEFORM_TYPE_PCM_LAST = 0x1b; +static constexpr std::uint32_t WAVEFORM_TYPE_VAG = 0x1c; +static constexpr std::uint32_t VAG_FRAME_SAMPLES = 28; +static constexpr std::uint32_t PCM_BYTES_PER_SAMPLE[] = {1, 1, 2, 2, 3, 3, 4, 4, 4, 4, 8, 8}; + +static bool IsPcmType(std::uint32_t type) { + return type >= WAVEFORM_TYPE_PCM_FIRST && type <= WAVEFORM_TYPE_PCM_LAST; +} + +static Ngs2WaveformBlock PcmBlock(std::uint32_t type, std::uint32_t channels, std::uint32_t pos, std::uint32_t count) { + const std::uint64_t stride = static_cast(channels) * PCM_BYTES_PER_SAMPLE[type - WAVEFORM_TYPE_PCM_FIRST]; + const auto offset = static_cast(pos * stride); + const auto end = (pos + count) * static_cast(stride); + Ngs2WaveformBlock block{}; + block.data_offset = offset; + block.data_size = static_cast(end - offset); + block.num_samples = count; + return block; +} + +static Ngs2WaveformBlock VagBlock(std::uint32_t channels, std::uint32_t pos, std::uint32_t count) { + const std::uint32_t frameBytes = 16u * channels; + const std::uint32_t first = pos / VAG_FRAME_SAMPLES; + const std::uint32_t past = (pos + count + VAG_FRAME_SAMPLES - 1u) / VAG_FRAME_SAMPLES; + const auto offset = static_cast(static_cast(first) * frameBytes); + Ngs2WaveformBlock block{}; + block.data_offset = offset; + block.data_size = static_cast(past * frameBytes - offset); + block.num_samples = (past - first) * VAG_FRAME_SAMPLES; + return block; +} void Ngs2Atrac9DecoderDeleter::operator()(void* handle) const { Atrac9ReleaseHandle(handle); @@ -407,10 +439,20 @@ int APS5_VABI sceNgs2ParseWaveformFile(const char* path, uint32_t offset, Ngs2Wa int APS5_VABI sceNgs2CalcWaveformBlock(const Ngs2WaveformFormat* format, uint32_t sample_pos, uint32_t num_samples, Ngs2WaveformBlock* block) { if (block == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; *block = {}; - if (format == nullptr || format->num_channels == 0 || format->num_channels > NGS2_MAX_CHANNELS || format->sample_rate == 0 || format->sample_rate > MAX_SAMPLE_RATE) { + if (format == nullptr) return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; + const auto type = format->waveform_type; + if (IsPcmType(type)) { + *block = PcmBlock(type, format->num_channels, sample_pos, num_samples); + return SCE_NGS2_OK; + } + if (type == WAVEFORM_TYPE_VAG) { + *block = VagBlock(format->num_channels, sample_pos, num_samples); + return SCE_NGS2_OK; + } + if (type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) return SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT; + if (format->num_channels == 0 || format->num_channels > NGS2_MAX_CHANNELS || format->sample_rate == 0 || format->sample_rate > MAX_SAMPLE_RATE) { return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; } - if (format->waveform_type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) throw std::runtime_error("NGS2: waveform blocks of type " + Ngs2Hex(format->waveform_type) + " are not implemented"); Atrac9CodecInfo codec{}; if (!MatchingCodecInfo(*format, codec)) return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; const std::uint64_t superframeSamples = static_cast(codec.frameSamples) * codec.framesInSuperframe; @@ -424,6 +466,28 @@ int APS5_VABI sceNgs2CalcWaveformBlock(const Ngs2WaveformFormat* format, uint32_ return SCE_NGS2_OK; } +int APS5_VABI sceNgs2GetWaveformFrameInfo(const Ngs2WaveformFormat* format, uint32_t* out_frame_size, uint32_t* out_num_frame_samples, uint32_t* out_units_per_frame, uint32_t* out_num_delay_samples) { + if (out_frame_size == nullptr || out_num_frame_samples == nullptr || out_units_per_frame == nullptr || out_num_delay_samples == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (format == nullptr || format->num_channels == 0 || format->num_channels > NGS2_MAX_CHANNELS || format->sample_rate == 0 || format->sample_rate > MAX_SAMPLE_RATE) { + return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; + } + if (format->waveform_type == SCE_NGS2_WAVEFORM_TYPE_PCM_I16L) { + *out_frame_size = format->num_channels * sizeof(std::int16_t); + *out_num_frame_samples = 1; + *out_units_per_frame = 1; + *out_num_delay_samples = 0; + return SCE_NGS2_OK; + } + if (format->waveform_type != SCE_NGS2_WAVEFORM_TYPE_ATRAC9) throw std::runtime_error("NGS2: the frame info of waveform type " + Ngs2Hex(format->waveform_type) + " is not implemented"); + Atrac9CodecInfo codec{}; + if (!MatchingCodecInfo(*format, codec)) return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; + *out_frame_size = static_cast(codec.superframeSize); + *out_num_frame_samples = static_cast(codec.frameSamples * codec.framesInSuperframe); + *out_units_per_frame = static_cast(codec.framesInSuperframe); + *out_num_delay_samples = static_cast(codec.frameSamples); + return SCE_NGS2_OK; +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index af850cd2b8..aaad9fdb18 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -12,25 +12,75 @@ static constexpr std::uint32_t USER_FX2_FLAG_FIRST_PROCESS = 1; static constexpr std::uint32_t USER_FX2_FLAG_PARAM_CHANGED = 2; -void Ngs2CheckCustomRack(const Ngs2CustomRackOption& option) { - if (option.num_buffers != 1) throw std::runtime_error("NGS2: custom racks with " + std::to_string(option.num_buffers) + " buffers are not implemented"); +static const char* BuiltInModuleName(std::uint32_t moduleId) { + switch (moduleId) { + case SCE_NGS2_CUSTOM_MODULE_ID_GAIN: return "Gain"; + case SCE_NGS2_CUSTOM_MODULE_ID_REVERB: return "Reverb"; + case SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT: return "PitchShift"; + case SCE_NGS2_CUSTOM_MODULE_ID_CONVOLUTION: return "Convolution"; + default: return nullptr; + } +} + +static std::uint32_t OptionWord(const Ngs2CustomModuleOption& option) { + std::uint32_t word = 0; + std::memcpy(&word, reinterpret_cast(&option) + sizeof(option), sizeof(word)); + return word; +} + +static int CheckBuiltInModule(const Ngs2CustomRackModuleInfo& info) { + const auto stateSizeIs = [&info](std::uint32_t size) { return info.state_size == 0 || info.state_size == size; }; + if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) { + if (!stateSizeIs(0)) return SCE_NGS2_ERROR_INVALID_STATE_SIZE; + } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_REVERB) { + if (!stateSizeIs(0)) return SCE_NGS2_ERROR_INVALID_STATE_SIZE; + if (info.option != nullptr && info.option->size != SCE_NGS2_CUSTOM_REVERB_OPTION_SIZE) return SCE_NGS2_ERROR_INVALID_OPTION_SIZE; + if (info.option != nullptr && OptionWord(*info.option) > 1) return SCE_NGS2_ERROR_INVALID_REVERB_OPTION; + } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT) { + if (!stateSizeIs(SCE_NGS2_CUSTOM_PITCH_SHIFT_STATE_SIZE)) return SCE_NGS2_ERROR_INVALID_STATE_SIZE; + if (info.option != nullptr && info.option->size == SCE_NGS2_CUSTOM_PITCH_SHIFT_OPTION_SIZE && OptionWord(*info.option) != 0) { + return SCE_NGS2_ERROR_INVALID_PITCH_SHIFT_OPTION; + } + } + return SCE_NGS2_OK; +} + +int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option, std::uint32_t maxInputs) { + if (option.num_buffers == 0 || option.num_buffers > SCE_NGS2_CUSTOM_MAX_BUFFERS || option.num_buffers < maxInputs) { + return SCE_NGS2_ERROR_INVALID_NUM_BUFFERS; + } if (option.num_modules > SCE_NGS2_CUSTOM_MAX_MODULES || option.rack_option.max_ports > SCE_NGS2_CUSTOM_MAX_PORTS) APS5_INVALID_ARG_EX; for (std::uint32_t m = 0; m < option.num_modules; m++) { const auto& info = option.module[m]; if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) { if (info.option == nullptr || info.option->size != sizeof(Ngs2CustomUserFx2ModuleOption)) APS5_INVALID_ARG_EX; - if (info.source_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; if (reinterpret_cast(info.option)->process_handler == nullptr) APS5_INVALID_ARG_EX; } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER) { if (info.option != nullptr || info.state_size != 0 || info.state_offset != 0) APS5_INVALID_ARG_EX; - if (info.source_buffer_id != 0 || info.extra_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; - } else { + } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) { + if (info.option != nullptr && info.option->size < sizeof(Ngs2CustomModuleOption)) APS5_INVALID_ARG_EX; + } else if (BuiltInModuleName(info.module_id) == nullptr) { throw std::runtime_error("NGS2: custom module id " + Ngs2Hex(info.module_id) + " is not implemented"); } + if (info.source_buffer_id >= option.num_buffers || info.extra_buffer_id >= option.num_buffers || info.dest_buffer_id >= option.num_buffers) { + return SCE_NGS2_ERROR_INVALID_BUFFER_INDEX; + } + const int result = CheckBuiltInModule(info); + if (result != SCE_NGS2_OK) return result; + } + if (option.num_buffers != 1) throw std::runtime_error("NGS2: custom racks with " + std::to_string(option.num_buffers) + " buffers are not implemented"); + for (std::uint32_t m = 0; m < option.num_modules; m++) { + const auto moduleId = option.module[m].module_id; + if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN && option.module[m].option != nullptr) throw std::runtime_error("NGS2: an option for the custom Gain module is not implemented"); + if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN && rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) continue; + if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) throw std::runtime_error("NGS2: the custom Gain module on a custom sampler rack is not implemented"); + const char* name = BuiltInModuleName(moduleId); + if (name != nullptr) throw std::runtime_error(std::string("NGS2: the custom ") + name + " module is not implemented"); } for (std::uint32_t p = 0; p < option.rack_option.max_ports; p++) { if (option.port[p].source_buffer_id != 0) APS5_INVALID_ARG_EX; } + return SCE_NGS2_OK; } static Ngs2UserFx2SetupContext UserFxContext(Ngs2Rack& rack, std::size_t module, std::uint32_t voiceIndex) { @@ -43,23 +93,33 @@ void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) { rack.customModuleIds.resize(option.num_modules); rack.userFx.resize(option.num_modules); rack.userFxCommon.resize(option.num_modules); - for (auto& voice : rack.voices) voice.userFx.resize(option.num_modules); + for (auto& voice : rack.voices) { + voice.userFx.resize(option.num_modules); + voice.customUserFx.resize(option.num_modules); + voice.customGain.assign(option.num_modules, {}); + } for (std::uint32_t m = 0; m < option.num_modules; m++) { rack.customModuleIds[m] = option.module[m].module_id; if (option.module[m].module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER) continue; - const auto& module = *reinterpret_cast(option.module[m].option); - rack.userFx[m] = module; - rack.userFxCommon[m].resize(module.common_size); - for (std::uint32_t i = 0; i < rack.voices.size(); i++) { - auto& fx = rack.voices[i].userFx[m]; - fx.param.resize(module.param_size); - fx.work.resize(module.work_size); - fx.state.resize(option.module[m].state_size); - fx.flags = USER_FX2_FLAG_FIRST_PROCESS; - if (module.setup_handler == nullptr) continue; - auto context = UserFxContext(rack, m, i); - const int result = module.setup_handler(&context); - if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx2 setup handler failed with " + std::to_string(result)); + if (option.module[m].module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) { + const auto& module = *reinterpret_cast(option.module[m].option); + rack.userFx[m] = module; + rack.userFxCommon[m].resize(module.common_size); + for (std::uint32_t i = 0; i < rack.voices.size(); i++) { + auto& fx = rack.voices[i].userFx[m]; + fx.param.resize(module.param_size); + fx.work.resize(module.work_size); + fx.state.resize(option.module[m].state_size); + fx.flags = USER_FX2_FLAG_FIRST_PROCESS; + if (module.setup_handler == nullptr) continue; + auto context = UserFxContext(rack, m, i); + const int result = module.setup_handler(&context); + if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx2 setup handler failed with " + std::to_string(result)); + } + } else if (option.module[m].module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) { + for (std::uint32_t i = 0; i < rack.voices.size(); i++) { + rack.voices[i].customUserFx[m].flags = 1; + } } } } @@ -93,6 +153,37 @@ static void SetUserFxParam(Ngs2Voice& voice, std::uint32_t index, const Ngs2Cust fx.flags |= USER_FX2_FLAG_PARAM_CHANGED; } +static void SetCustomUserFxParam(Ngs2Voice& voice, std::uint32_t index, const Ngs2CustomVoiceUserFxParam& param) { + if (index >= voice.customUserFx.size()) APS5_INVALID_ARG_EX; + if (index >= voice.rack->customModuleIds.size() || voice.rack->customModuleIds[index] != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) { + APS5_INVALID_ARG_EX; + } + auto& fx = voice.customUserFx[index]; + fx.handler = param.handler; + fx.data = {param.user_data0, param.user_data1, param.user_data2}; + fx.flags = 1; +} + +static void SetGainParam(Ngs2Voice& voice, std::uint32_t index, const Ngs2CustomVoiceGainParam& param) { + if (index >= voice.customGain.size() || voice.rack->customModuleIds[index] != SCE_NGS2_CUSTOM_MODULE_ID_GAIN) APS5_INVALID_ARG_EX; + for (const float gain : param.gain) { + if (!(gain >= -32.0f && gain <= 32.0f)) throw std::runtime_error("NGS2: a custom Gain level outside -32 to 32 is not implemented"); + } + std::copy(std::begin(param.gain), std::end(param.gain), voice.customGain[index].target.begin()); +} + +static void ProcessGain(Ngs2Voice& voice, Ngs2VoiceCustomGain& gain, const std::vector& channels, std::uint32_t grain) { + if (voice.hasSamples) { + for (std::uint32_t c = 0; c < voice.channels; c++) { + const float from = gain.current[c]; + const float step = (gain.target[c] - from) / static_cast(grain); + for (std::uint32_t i = 0; i < grain; i++) channels[c][i] *= from + step * static_cast(i + 1); + } + voice.hasSamples = std::any_of(voice.samples.begin(), voice.samples.end(), [](float sample) { return sample != 0.0f; }); + } + gain.current = gain.target; +} + void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { const auto rackId = voice.rack->rackId; if (param.id == SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP) { @@ -113,7 +204,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; @@ -123,6 +216,11 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { Ngs2SetFilter(voice, filter); return; } + if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN) { + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); + SetGainParam(voice, param.id & 0x1f, ParamAs(param)); + return; + } if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2) { if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SAMPLER && rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); @@ -130,24 +228,43 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { SetUserFxParam(voice, param.id & 0x1f, ParamAs(param)); return; } + if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX) { + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); + SetCustomUserFxParam(voice, param.id & 0x1f, ParamAs(param)); + return; + } if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); throw std::runtime_error("NGS2: voice param " + Ngs2Hex(param.id) + " is not implemented"); } void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate) { - if (voice.userFx.empty() || std::none_of(voice.rack->customModuleIds.begin(), voice.rack->customModuleIds.end(), - [](std::uint32_t id) { return id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2; })) return; + if (voice.userFx.empty() && voice.customUserFx.empty()) return; std::vector channels(voice.channels); for (std::uint32_t c = 0; c < voice.channels; c++) channels[c] = voice.samples.data() + static_cast(c) * grain; - for (std::size_t m = 0; m < voice.userFx.size(); m++) { - if (voice.rack->customModuleIds[m] != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) continue; - auto& fx = voice.userFx[m]; - const auto& module = voice.rack->userFx[m]; - Ngs2UserFx2ProcessContext context{channels.data(), voice.rack->userFxCommon[m].data(), fx.param.data(), fx.work.data(), fx.state.data(), - module.user_data, fx.flags, voice.channels, voice.channels, grain, sampleRate, 0, {}}; - const int result = module.process_handler(&context); - if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx2 process handler failed with " + std::to_string(result)); - fx.flags = 0; + for (std::size_t m = 0; m < voice.rack->customModuleIds.size(); m++) { + if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) { + ProcessGain(voice, voice.customGain[m], channels, grain); + } else if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) { + if (!voice.hasSamples) continue; + auto& fx = voice.userFx[m]; + const auto& module = voice.rack->userFx[m]; + if (module.process_handler == nullptr) continue; + Ngs2UserFx2ProcessContext context{channels.data(), voice.rack->userFxCommon[m].data(), fx.param.data(), fx.work.data(), fx.state.data(), + module.user_data, fx.flags, voice.channels, voice.channels, grain, sampleRate, 0, {}}; + const int result = module.process_handler(&context); + if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx2 process handler failed with " + std::to_string(result)); + fx.flags = 0; + } else if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) { + if (m < voice.customUserFx.size() && voice.customUserFx[m].handler != nullptr) { + auto& fx = voice.customUserFx[m]; + Ngs2UserFxProcessContext context{channels.data(), fx.data[0], fx.data[1], fx.data[2], + fx.flags, voice.channels, grain, sampleRate}; + const int result = fx.handler(&context); + if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx process handler failed with " + std::to_string(result)); + fx.flags = 0; + voice.hasSamples = std::any_of(voice.samples.begin(), voice.samples.end(), [](float sample) { return sample != 0.0f; }); + } + } } } diff --git a/core/libs/prx/libSceNgs2.native/src/Geom.cpp b/core/libs/prx/libSceNgs2.native/src/Geom.cpp index f8ab93148e..9b68d818ef 100644 --- a/core/libs/prx/libSceNgs2.native/src/Geom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Geom.cpp @@ -1,26 +1,74 @@ +#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 Cross(const Vector& left, const Vector& right) { + return {left[1] * right[2] - left[2] * right[1], left[2] * right[0] - left[0] * right[2], left[0] * right[1] - left[1] * right[0]}; +} + +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; +} + +Vector PerpendicularHint(const Vector& front) { + return std::abs(front[1]) < 0.9 ? Vector{0.0, 1.0, 0.0} : Vector{1.0, 0.0, 0.0}; +} + +} + int Ngs2GeomCalcListener(const Ngs2GeomListenerParam* param, Ngs2GeomListenerWork* outWork, std::uint32_t flags) { - if (param == nullptr) APS5_INVALID_ARG_EX; + if (param == nullptr) return SCE_NGS2_ERROR_INVALID_GEOM_LISTENER_ADDRESS; 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) return SCE_NGS2_ERROR_INVALID_GEOM_FLAG; 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 = Cross(up, front); + if (!(std::hypot(right[0], right[1], right[2]) > 0.0)) { + up = Unit(PerpendicularHint(front)); + right = Cross(up, front); + } + right = Unit(right); + up = Cross(front, right); + 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 +101,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 8dbe429dc9..a0e886d230 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "prx/libSceNgs2.native/include/Ngs2Types.hpp" @@ -101,6 +102,17 @@ struct Ngs2UserFx2 { std::uint32_t flags = 0; }; +struct Ngs2VoiceCustomUserFx { + Ngs2UserFxProcessHandler handler = nullptr; + std::array data{}; + std::uint32_t flags = 0; +}; + +struct Ngs2VoiceCustomGain { + std::array current{}; + std::array target{}; +}; + struct Ngs2Voice { Ngs2Rack* rack = nullptr; Ngs2PlayState state = Ngs2PlayState::Empty; @@ -128,6 +140,8 @@ struct Ngs2Voice { float fbwLevel = 1.0f; float lfeLevel = 1.0f; std::vector userFx; + std::vector customUserFx; + std::vector customGain; Ngs2UserFxProcessHandler userFxHandler = nullptr; std::array userFxData{}; std::uint32_t userFxFlags = 0; @@ -151,12 +165,15 @@ struct Ngs2Rack { std::uint32_t maxGrainSamples = 0; std::uint32_t maxChannelWorks = 0; std::uint32_t maxInputs = 0; + std::uintptr_t userData = 0; Ngs2ContextBufferInfo bufferInfo{}; Ngs2BufferAllocator allocator{}; std::vector voices; std::vector userFx; std::vector> userFxCommon; std::vector customModuleIds; + std::thread::id lockOwner{}; + std::uint32_t lockDepth = 0; }; struct Ngs2System { @@ -165,6 +182,7 @@ struct Ngs2System { Ngs2BufferAllocator allocator{}; std::uint32_t uid = 0; std::int64_t renderCount = 0; + std::uint32_t renderDepth = 0; std::uintptr_t userData = 0; std::vector racks; }; @@ -172,6 +190,32 @@ struct Ngs2System { std::string Ngs2Hex(std::uint32_t value); const std::uint8_t* Ngs2StreamEnd(const Ngs2Voice& voice, const Ngs2Block& block); std::recursive_mutex& Ngs2Mutex(); + +class Ngs2Lock { +public: + Ngs2Lock(); + ~Ngs2Lock(); + Ngs2Lock(const Ngs2Lock&) = delete; + Ngs2Lock& operator=(const Ngs2Lock&) = delete; +}; + +class Ngs2Unlocked { +public: + Ngs2Unlocked(); + ~Ngs2Unlocked(); + Ngs2Unlocked(const Ngs2Unlocked&) = delete; + Ngs2Unlocked& operator=(const Ngs2Unlocked&) = delete; + +private: + std::uint32_t depth; +}; + +bool Ngs2ExclusiveElsewhere(); +bool Ngs2RenderingHere(const Ngs2System& system); +void Ngs2EnterExclusive(); +void Ngs2LeaveExclusive(std::uint32_t depth = 1); +void Ngs2WaitOnce(); +void Ngs2NotifyWaiters(); Ngs2System* Ngs2FindSystem(Ngs2Handle handle); template @@ -191,7 +235,7 @@ std::size_t Ngs2Atrac9BlockBytes(const Ngs2Voice& voice, const Ngs2WaveformBlock void Ngs2RestartAtrac9(Ngs2Voice& voice); void Ngs2ConsumeAtrac9(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t systemRate); bool Ngs2FinishBlock(Ngs2Voice& voice); -void Ngs2CheckCustomRack(const Ngs2CustomRackOption& option); +int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option, std::uint32_t maxInputs); void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option); void Ngs2CleanupUserFx(Ngs2Rack& rack); void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param); @@ -202,11 +246,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/Pan.cpp b/core/libs/prx/libSceNgs2.native/src/Pan.cpp index af88b23204..e1a4d70548 100644 --- a/core/libs/prx/libSceNgs2.native/src/Pan.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Pan.cpp @@ -55,10 +55,13 @@ static void AddBetweenSpeakers(const Ngs2PanWork& work, std::uint32_t speaker, f } int Ngs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers) { - if (work == nullptr) APS5_INVALID_ARG_EX; - if (speaker_angles != nullptr && !IsUnitAngle(unit_angle)) APS5_INVALID_ARG_EX; + if (work == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (speaker_angles != nullptr && !IsUnitAngle(unit_angle)) return SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE; const auto* layout = LayoutFor(num_speakers); - if (layout == nullptr) APS5_INVALID_ARG_EX; + if (layout == nullptr) { + if (speaker_angles == nullptr && num_speakers == 0) APS5_INVALID_ARG_EX; + return SCE_NGS2_ERROR_INVALID_PAN_SPEAKER; + } *work = {}; work->num_speakers = num_speakers; if (speaker_angles == nullptr) { diff --git a/core/libs/prx/libSceNgs2.native/src/Rack.cpp b/core/libs/prx/libSceNgs2.native/src/Rack.cpp index a9df1495ce..f5c85495eb 100644 --- a/core/libs/prx/libSceNgs2.native/src/Rack.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Rack.cpp @@ -87,22 +87,35 @@ static std::uint32_t RackMaxChannels(std::uint32_t rackId, const RackOptions& op return NGS2_MAX_CHANNELS; } -static RackOptions CheckedRackOption(std::uint32_t rackId, const Ngs2RackOption* option) { +static int CheckedRackOption(std::uint32_t rackId, const Ngs2RackOption* option, RackOptions& options) { const auto size = RackOptionSize(rackId); - if (option == nullptr) return DefaultRackOption(rackId); + if (option == nullptr) { + options = DefaultRackOption(rackId); + return SCE_NGS2_OK; + } if (option->size != size) throw std::invalid_argument("NGS2: unexpected rack option size " + std::to_string(option->size)); - RackOptions options{}; + options = {}; std::memcpy(&options, option, size); const auto maxChannels = RackMaxChannels(rackId, options); - if (options.common.max_voices == 0 || maxChannels == 0 || maxChannels > NGS2_MAX_CHANNELS) { + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER && maxChannels > SCE_NGS2_CUSTOM_SUBMIXER_MAX_CHANNELS) return SCE_NGS2_ERROR_INVALID_NUM_CHANNELS; + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER && (options.customSubmixer.max_inputs == 0 || options.customSubmixer.max_inputs > SCE_NGS2_CUSTOM_SUBMIXER_MAX_INPUTS)) { + throw std::runtime_error("NGS2: custom submixer racks with " + std::to_string(options.customSubmixer.max_inputs) + " inputs are not implemented"); + } + if (options.common.max_voices == 0 || maxChannels == 0) throw std::invalid_argument("NGS2: invalid rack voice or channel count"); + int result = SCE_NGS2_OK; + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) result = Ngs2CheckCustomRack(rackId, options.customSampler.custom_rack_option, 0); + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) result = Ngs2CheckCustomRack(rackId, options.customSubmixer.custom_rack_option, options.customSubmixer.max_inputs); + if (result != SCE_NGS2_OK) return result; + if (maxChannels > NGS2_MAX_CHANNELS) { + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { + throw std::runtime_error("NGS2: custom submixer racks with " + std::to_string(maxChannels) + " channels are not implemented"); + } throw std::invalid_argument("NGS2: invalid rack voice or channel count"); } - if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) Ngs2CheckCustomRack(options.customSampler.custom_rack_option); - if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) Ngs2CheckCustomRack(options.customSubmixer.custom_rack_option); if (rackId == SCE_NGS2_RACK_ID_REVERB && options.reverb.reverb_size != 1) { throw std::runtime_error("NGS2: reverb rack size " + std::to_string(options.reverb.reverb_size) + " is not implemented"); } - return options; + return SCE_NGS2_OK; } static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackOptions& options, const Ngs2ContextBufferInfo& bufferInfo, @@ -141,9 +154,11 @@ static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackO } int Ngs2DestroyRack(Ngs2Rack& rack, Ngs2ContextBufferInfo* outBufferInfo) { + if (rack.lockDepth != 0) Ngs2LeaveExclusive(rack.lockDepth); Ngs2CleanupUserFx(rack); auto& racks = rack.system->racks; std::erase(racks, &rack); + Ngs2NotifyWaiters(); for (auto* other : racks) { for (auto& voice : other->voices) { for (auto& port : voice.ports) { @@ -163,7 +178,9 @@ extern "C" { int APS5_VABI sceNgs2RackQueryBufferSize(uint32_t rack_id, const Ngs2RackOption* option, Ngs2ContextBufferInfo* buffer_info) { if (buffer_info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - CheckedRackOption(rack_id, option); + RackOptions options{}; + const int result = CheckedRackOption(rack_id, option, options); + if (result != SCE_NGS2_OK) throw std::invalid_argument("NGS2: invalid rack option, rejected at creation with " + Ngs2Hex(static_cast(result))); *buffer_info = {}; buffer_info->host_buffer_size = sizeof(Ngs2Rack); return SCE_NGS2_OK; @@ -171,17 +188,21 @@ int APS5_VABI sceNgs2RackQueryBufferSize(uint32_t rack_id, const Ngs2RackOption* int APS5_VABI sceNgs2RackCreate(uintptr_t system_handle, uint32_t rack_id, const Ngs2RackOption* option, const Ngs2ContextBufferInfo* buffer_info, uintptr_t* handle) { if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - const auto options = CheckedRackOption(rack_id, option); if (buffer_info == nullptr) APS5_INVALID_ARG_EX; - std::lock_guard lock(Ngs2Mutex()); + RackOptions options{}; + const int result = CheckedRackOption(rack_id, option, options); + if (result != SCE_NGS2_OK) return result; + const Ngs2Lock lock; return CreateRack(system_handle, rack_id, options, *buffer_info, {}, handle); } int APS5_VABI sceNgs2RackCreateWithAllocator(uintptr_t system_handle, uint32_t rack_id, const Ngs2RackOption* option, const Ngs2BufferAllocator* allocator, uintptr_t* handle) { if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - const auto options = CheckedRackOption(rack_id, option); if (allocator == nullptr || allocator->alloc_handler == nullptr || allocator->free_handler == nullptr) APS5_INVALID_ARG_EX; - std::lock_guard lock(Ngs2Mutex()); + RackOptions options{}; + const int checked = CheckedRackOption(rack_id, option, options); + if (checked != SCE_NGS2_OK) return checked; + const Ngs2Lock lock; if (Ngs2FindSystem(system_handle) == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; Ngs2ContextBufferInfo bufferInfo{}; bufferInfo.host_buffer_size = sizeof(Ngs2Rack); @@ -192,19 +213,25 @@ int APS5_VABI sceNgs2RackCreateWithAllocator(uintptr_t system_handle, uint32_t r } int APS5_VABI sceNgs2RackDestroy(uintptr_t rack_handle, Ngs2ContextBufferInfo* buffer_info) { - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; if (buffer_info != nullptr) *buffer_info = {}; auto* rack = Ngs2FindRack(rack_handle); if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + while (rack != nullptr && Ngs2ExclusiveElsewhere()) { + Ngs2WaitOnce(); + rack = Ngs2FindRack(rack_handle); + } + if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + if (Ngs2RenderingHere(*rack->system)) throw std::runtime_error("NGS2: destroying a rack of a system from that system's render (a voice callback) is not implemented"); return Ngs2DestroyRack(*rack, buffer_info); } int APS5_VABI sceNgs2RackGetVoiceHandle(uintptr_t rack_handle, uint32_t voice_id, uintptr_t* handle) { - if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; 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; } @@ -212,7 +239,7 @@ int APS5_VABI sceNgs2RackGetVoiceHandle(uintptr_t rack_handle, uint32_t voice_id int APS5_VABI sceNgs2RackGetInfo(uintptr_t rack_handle, Ngs2RackInfo* info, size_t info_size) { if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; if (info_size != sizeof(Ngs2RackInfo)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; const auto* rack = Ngs2FindRack(rack_handle); if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; *info = {}; @@ -237,17 +264,44 @@ int APS5_VABI sceNgs2RackGetInfo(uintptr_t rack_handle, Ngs2RackInfo* info, size return SCE_NGS2_OK; } +int APS5_VABI sceNgs2RackSetUserData(uintptr_t rack_handle, uintptr_t user_data) { + std::lock_guard lock(Ngs2Mutex()); + auto* rack = Ngs2FindRack(rack_handle); + if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + rack->userData = user_data; + return SCE_NGS2_OK; +} + +int APS5_VABI sceNgs2RackGetUserData(uintptr_t rack_handle, uintptr_t* user_data) { + std::lock_guard lock(Ngs2Mutex()); + const auto* rack = Ngs2FindRack(rack_handle); + if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + if (user_data == nullptr) APS5_INVALID_ARG_EX; + *user_data = rack->userData; + return SCE_NGS2_OK; +} + int APS5_VABI sceNgs2RackLock(uintptr_t rack_handle) { - Ngs2Mutex().lock(); - if (Ngs2FindRack(rack_handle) != nullptr) return SCE_NGS2_OK; - Ngs2Mutex().unlock(); - return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + const Ngs2Lock lock; + const auto self = std::this_thread::get_id(); + auto* rack = Ngs2FindRack(rack_handle); + while (rack != nullptr && Ngs2ExclusiveElsewhere()) { + Ngs2WaitOnce(); + rack = Ngs2FindRack(rack_handle); + } + if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + Ngs2EnterExclusive(); + rack->lockOwner = self; + rack->lockDepth++; + return SCE_NGS2_OK; } int APS5_VABI sceNgs2RackUnlock(uintptr_t rack_handle) { - std::lock_guard lock(Ngs2Mutex()); - if (Ngs2FindRack(rack_handle) == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; - Ngs2Mutex().unlock(); + const Ngs2Lock lock; + auto* rack = Ngs2FindRack(rack_handle); + if (rack == nullptr || rack->lockDepth == 0 || rack->lockOwner != std::this_thread::get_id()) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + if (--rack->lockDepth == 0) rack->lockOwner = {}; + Ngs2LeaveExclusive(); return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index bf14563e82..fa764d375a 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -78,7 +78,10 @@ bool Ngs2FinishBlock(Ngs2Voice& voice) { voice.blocks.pop_front(); } const auto revision = voice.waveformRevision; - if (voice.callback != nullptr && (voice.callbackFlags & info.flag) != 0) voice.callback(&info); + if (voice.callback != nullptr && (voice.callbackFlags & info.flag) != 0) { + const Ngs2Unlocked unlocked; + voice.callback(&info); + } return voice.waveformRevision == revision && voice.state == Ngs2PlayState::Playing; } @@ -95,16 +98,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 +152,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); @@ -249,7 +261,7 @@ static void RenderVoice(Ngs2Voice& voice, const std::vector& voices, } if (voice.rack->rackId == SCE_NGS2_RACK_ID_REVERB && voice.reverb) voice.hasSamples = Ngs2ProcessReverb(voice, grain, systemRate); Ngs2ProcessLegacyUserFx(voice, grain, systemRate); - if (voice.hasSamples) Ngs2ProcessUserFx(voice, grain, systemRate); + Ngs2ProcessUserFx(voice, grain, systemRate); } voice.rendered = true; } @@ -289,8 +301,31 @@ 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++) { + const auto& output = bufferInfo[i]; + if (output.buffer == nullptr) continue; + if (output.num_channels == 0 || output.num_channels > NGS2_MAX_CHANNELS) { + throw std::invalid_argument("NGS2: render channel count is invalid"); + } + 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) { @@ -311,8 +346,8 @@ void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo } } for (std::uint32_t i = 0; i < numBufferInfo; i++) { - if (bufferInfo[i].buffer != nullptr) std::memset(bufferInfo[i].buffer, 0, bufferInfo[i].buffer_size); if (!mixes[i].empty()) WriteOutput(bufferInfo[i], mixes[i]); + else if (outputBytes[i] != 0) std::memset(bufferInfo[i].buffer, 0, outputBytes[i]); } for (auto* voice : voices) { if (voice->state == Ngs2PlayState::Stopped) voice->state = Ngs2PlayState::Empty; diff --git a/core/libs/prx/libSceNgs2.native/src/System.cpp b/core/libs/prx/libSceNgs2.native/src/System.cpp index cf861a6ae8..2fe15a7311 100644 --- a/core/libs/prx/libSceNgs2.native/src/System.cpp +++ b/core/libs/prx/libSceNgs2.native/src/System.cpp @@ -1,13 +1,17 @@ #include +#include #include #include #include #include +#include #include #include #include #include #include +#include +#include #include #include "prx/libc/include/General.hpp" @@ -19,6 +23,17 @@ static std::vector& Systems() { return *systems; } +struct Ngs2ReportRegistration { + std::uint32_t type; + Ngs2ReportHandler handler; + std::uintptr_t userData; +}; + +static std::vector& ReportRegistrations() { + static auto* const registrations = new std::vector(); + return *registrations; +} + std::string Ngs2Hex(std::uint32_t value) { char text[16]; std::snprintf(text, sizeof(text), "0x%x", value); @@ -30,6 +45,116 @@ std::recursive_mutex& Ngs2Mutex() { return *mutex; } +static std::uint32_t& LockDepth() { + thread_local std::uint32_t depth = 0; + return depth; +} + +static std::condition_variable_any& Ngs2Waiters() { + static auto* const waiters = new std::condition_variable_any(); + return *waiters; +} + +Ngs2Lock::Ngs2Lock() { + Ngs2Mutex().lock(); + ++LockDepth(); +} + +Ngs2Lock::~Ngs2Lock() { + --LockDepth(); + Ngs2Mutex().unlock(); +} + +Ngs2Unlocked::Ngs2Unlocked() : depth(std::exchange(LockDepth(), 0u)) { + for (std::uint32_t i = 0; i < depth; ++i) Ngs2Mutex().unlock(); +} + +Ngs2Unlocked::~Ngs2Unlocked() { + for (std::uint32_t i = 0; i < depth; ++i) Ngs2Mutex().lock(); + LockDepth() = depth; +} + +struct HeldNgs2Lock { + std::uint32_t depth = 0; + + void unlock() { + depth = std::exchange(LockDepth(), 0u); + for (std::uint32_t i = 0; i < depth; ++i) Ngs2Mutex().unlock(); + } + + void lock() { + for (std::uint32_t i = 0; i < depth; ++i) Ngs2Mutex().lock(); + LockDepth() = depth; + } +}; + +struct ExclusiveState { + std::thread::id owner; + std::uint32_t depth = 0; +}; + +static ExclusiveState& Exclusive() { + static ExclusiveState state; + return state; +} + +bool Ngs2RenderingHere(const Ngs2System& system) { + return system.renderDepth != 0; +} + +bool Ngs2ExclusiveElsewhere() { + return Exclusive().depth != 0 && Exclusive().owner != std::this_thread::get_id(); +} + +void Ngs2EnterExclusive() { + Exclusive().owner = std::this_thread::get_id(); + ++Exclusive().depth; +} + +void Ngs2LeaveExclusive(std::uint32_t depth) { + Exclusive().depth -= depth; + if (Exclusive().depth != 0) return; + Exclusive().owner = {}; + Ngs2NotifyWaiters(); +} + +void Ngs2WaitOnce() { + HeldNgs2Lock held; + Ngs2Waiters().wait(held); +} + +void Ngs2NotifyWaiters() { + Ngs2Waiters().notify_all(); +} + +struct SystemLockState { + std::thread::id owner; + std::uint32_t depth = 0; +}; + +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)); @@ -87,6 +212,7 @@ static Ngs2SystemOption DefaultSystemOption() { option.max_grain_samples = 512; option.num_grain_samples = 256; option.sample_rate = 48000; + option.max_voice_channels = 8; return option; } @@ -110,6 +236,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; } @@ -129,7 +257,7 @@ int APS5_VABI sceNgs2SystemCreate(const Ngs2SystemOption* option, const Ngs2Cont if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; const auto checked = CheckedSystemOption(option); if (buffer_info == nullptr) APS5_INVALID_ARG_EX; - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; return CreateSystem(checked, *buffer_info, {}, handle); } @@ -137,20 +265,30 @@ int APS5_VABI sceNgs2SystemCreateWithAllocator(const Ngs2SystemOption* option, c if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; const auto checked = CheckedSystemOption(option); const auto bufferInfo = Allocate(allocator, sizeof(Ngs2System)); - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; return CreateSystem(checked, bufferInfo, *allocator, handle); } int APS5_VABI sceNgs2SystemDestroy(uintptr_t system_handle, Ngs2ContextBufferInfo* buffer_info) { - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; if (buffer_info != nullptr) *buffer_info = {}; auto* system = Ngs2FindSystem(system_handle); + while (system != nullptr && Ngs2ExclusiveElsewhere()) { + Ngs2WaitOnce(); + system = Ngs2FindSystem(system_handle); + } if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + if (Ngs2RenderingHere(*system)) throw std::runtime_error("NGS2: destroying a system from its own render (a voice callback) is not implemented"); while (!system->racks.empty()) { const int result = Ngs2DestroyRack(*system->racks.back(), nullptr); if (result != SCE_NGS2_OK) return result; } 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); @@ -160,7 +298,7 @@ int APS5_VABI sceNgs2SystemDestroy(uintptr_t system_handle, Ngs2ContextBufferInf int APS5_VABI sceNgs2SystemGetInfo(uintptr_t system_handle, Ngs2SystemInfo* info, size_t info_size) { if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; if (info_size != sizeof(Ngs2SystemInfo)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; const auto* system = Ngs2FindSystem(system_handle); if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; *info = {}; @@ -179,7 +317,7 @@ int APS5_VABI sceNgs2SystemGetInfo(uintptr_t system_handle, Ngs2SystemInfo* info } int APS5_VABI sceNgs2SystemSetGrainSamples(uintptr_t system_handle, uint32_t num_samples) { - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; auto* system = Ngs2FindSystem(system_handle); if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; if (num_samples == 0 || num_samples > system->option.max_grain_samples) APS5_INVALID_ARG_EX; @@ -188,7 +326,7 @@ int APS5_VABI sceNgs2SystemSetGrainSamples(uintptr_t system_handle, uint32_t num } int APS5_VABI sceNgs2SystemSetSampleRate(uintptr_t system_handle, uint32_t sample_rate) { - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; auto* system = Ngs2FindSystem(system_handle); if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; if (sample_rate == 0) APS5_INVALID_ARG_EX; @@ -206,7 +344,7 @@ int APS5_VABI sceNgs2SystemSetSampleRate(uintptr_t system_handle, uint32_t sampl } int APS5_VABI sceNgs2SystemSetUserData(uintptr_t system_handle, uintptr_t user_data) { - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; auto* system = Ngs2FindSystem(system_handle); if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; system->userData = user_data; @@ -214,7 +352,7 @@ int APS5_VABI sceNgs2SystemSetUserData(uintptr_t system_handle, uintptr_t user_d } int APS5_VABI sceNgs2SystemGetUserData(uintptr_t system_handle, uintptr_t* user_data) { - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; const auto* system = Ngs2FindSystem(system_handle); if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; if (user_data == nullptr) APS5_INVALID_ARG_EX; @@ -223,34 +361,94 @@ 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; } int APS5_VABI sceNgs2SystemRender(uintptr_t system_handle, const Ngs2RenderBufferInfo* buffer_info, uint32_t num_buffer_info) { - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; auto* system = Ngs2FindSystem(system_handle); + while (system != nullptr && Ngs2ExclusiveElsewhere()) { + Ngs2WaitOnce(); + system = Ngs2FindSystem(system_handle); + } if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; if (buffer_info == nullptr || num_buffer_info == 0) APS5_INVALID_ARG_EX; + SystemLockState* state = nullptr; + { + 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; + } + Ngs2EnterExclusive(); + system->renderDepth++; + struct Release { + SystemLockState* state; + Ngs2System* system; + ~Release() { + system->renderDepth--; + Ngs2LeaveExclusive(); + Ngs2NotifyWaiters(); + std::lock_guard locks(SystemLockMutex()); + ReleaseSystemLock(*state); + } + } release{state, system}; Ngs2RenderSystem(*system, buffer_info, num_buffer_info); return SCE_NGS2_OK; } int APS5_VABI sceNgs2SystemResetOption(Ngs2SystemOption* option) { - if (option == nullptr) APS5_INVALID_ARG_EX; + if (option == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; *option = DefaultSystemOption(); return SCE_NGS2_OK; } +int APS5_VABI sceNgs2ReportRegisterHandler(uint32_t report_type, Ngs2ReportHandler handler, uintptr_t user_data, Ngs2Handle* out_handle) { + if (out_handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (handler == nullptr) APS5_INVALID_ARG_EX; + const Ngs2Lock lock; + auto* registration = new Ngs2ReportRegistration{report_type, handler, user_data}; + ReportRegistrations().push_back(registration); + *out_handle = reinterpret_cast(registration); + return SCE_NGS2_OK; +} + +int APS5_VABI sceNgs2ReportUnregisterHandler(Ngs2Handle report_handle) { + const Ngs2Lock lock; + auto& registrations = ReportRegistrations(); + const auto found = std::find(registrations.begin(), registrations.end(), reinterpret_cast(report_handle)); + if (found == registrations.end()) throw std::runtime_error("NGS2: unregistering a report handler that is not registered"); + Ngs2ReportRegistration* registration = *found; + registrations.erase(found); + delete registration; + return SCE_NGS2_OK; +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp deleted file mode 100644 index 1541b64394..0000000000 --- a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include -#include - -#include "prx/libc/include/General.hpp" -#include "prx/libSceNgs2.native/include/Ngs2Types.hpp" - -#pragma GCC visibility push(default) - -extern "C" { - -int APS5_VABI 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; - (void)user_data; - (void)out_handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNgs2ReportUnregisterHandler(Ngs2Handle report_handle) { - (void)report_handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} - -#pragma GCC visibility pop diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index 0b49d01cb3..bc19ab0a0a 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -69,6 +69,9 @@ void Ngs2Voice::ResetSetup() { userFxHandler = nullptr; userFxData = {}; userFxFlags = 0; + callback = nullptr; + callbackData = 0; + callbackFlags = 0; } const std::uint8_t* Ngs2StreamEnd(const Ngs2Voice& voice, const Ngs2Block& block) { @@ -95,6 +98,10 @@ static Ngs2Port& PortAt(Ngs2Voice& voice, std::uint32_t port) { } static void SetMatrixLevels(Ngs2Voice& voice, std::uint32_t matrixId, const float* levels, std::uint32_t numLevels) { + if (numLevels == 0 && voice.channels == 0 && matrixId < voice.matrices.size()) { + voice.matrices[matrixId].clear(); + return; + } if (matrixId >= voice.matrices.size() || numLevels == 0 || numLevels > NGS2_MAX_CHANNELS * NGS2_MAX_CHANNELS || levels == nullptr) { APS5_INVALID_ARG_EX; } @@ -208,7 +215,7 @@ static void AppendStreamData(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBlo static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBlocksParam& param) { constexpr std::uint32_t knownFlags = SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_SILENCE; if ((param.flags & ~knownFlags) != 0) throw std::runtime_error("NGS2: waveform block flags " + Ngs2Hex(param.flags) + " are not implemented"); - if (voice.channels == 0 || (param.num_blocks != 0 && (param.blocks == nullptr || param.data == nullptr))) APS5_INVALID_ARG_EX; + if ((param.num_blocks != 0 && (voice.channels == 0 || param.blocks == nullptr || param.data == nullptr)) || (voice.channels == 0 && (param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND) != 0)) APS5_INVALID_ARG_EX; const bool reset = (param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET) != 0; if (!voice.acceptsBlocks && !reset) throw std::invalid_argument("NGS2: the voice waveform was already closed"); if (reset) { @@ -383,20 +390,16 @@ static void ApplyParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { throw std::runtime_error("NGS2: voice param " + Ngs2Hex(param.id) + " is not implemented"); } -static Ngs2Voice& CheckedVoice(Ngs2Handle handle) { - auto* voice = Ngs2FindVoice(handle); - if (voice == nullptr) throw std::invalid_argument("NGS2: invalid voice handle"); - return *voice; -} - #pragma GCC visibility push(default) extern "C" { int APS5_VABI sceNgs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamHeader* param_list) { - std::lock_guard lock(Ngs2Mutex()); - auto& voice = CheckedVoice(voice_handle); - if (param_list == nullptr) APS5_INVALID_ARG_EX; + if (param_list == nullptr) return SCE_NGS2_ERROR_NULL_PARAM_LIST; + const Ngs2Lock lock; + auto* found = Ngs2FindVoice(voice_handle); + if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; + auto& voice = *found; for (const auto* param = param_list;; param = reinterpret_cast(reinterpret_cast(param) + param->next)) { ApplyParam(voice, *param); if (param->next == 0) break; @@ -405,8 +408,10 @@ int APS5_VABI sceNgs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamHe } int APS5_VABI sceNgs2VoiceRunCommands(uintptr_t voice_handle, const Ngs2VoiceCommand* commands, size_t num_commands) { - std::lock_guard lock(Ngs2Mutex()); - auto& voice = CheckedVoice(voice_handle); + const Ngs2Lock lock; + auto* found = Ngs2FindVoice(voice_handle); + if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; + auto& voice = *found; if (commands == nullptr && num_commands != 0) APS5_INVALID_ARG_EX; for (std::size_t i = 0; i < num_commands; i++) { const auto& command = commands[i]; @@ -427,9 +432,12 @@ int APS5_VABI sceNgs2VoiceRunCommands(uintptr_t voice_handle, const Ngs2VoiceCom } int APS5_VABI sceNgs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state, size_t state_size) { + const Ngs2Lock lock; + auto* found = Ngs2FindVoice(voice_handle); + if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; + if (state_size < sizeof(Ngs2VoiceState)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; if (state == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - std::lock_guard lock(Ngs2Mutex()); - const auto& voice = CheckedVoice(voice_handle); + const auto& voice = *found; switch (voice.rack->rackId) { case SCE_NGS2_RACK_ID_SAMPLER: { if (state_size != sizeof(Ngs2SamplerVoiceState)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; @@ -456,6 +464,14 @@ int APS5_VABI sceNgs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state submixer.voice_state.state_flags = voice.stateFlags; return SCE_NGS2_OK; } + case SCE_NGS2_RACK_ID_CUSTOM_SAMPLER: { + if (state_size != sizeof(Ngs2CustomSamplerVoiceState)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; + auto& sampler = *reinterpret_cast(state); + sampler = {}; + sampler.voice_state.state_flags = voice.stateFlags; + sampler.num_decoded_samples = voice.decodedSamples; + return SCE_NGS2_OK; + } default: throw std::runtime_error("NGS2: voice state of rack " + Ngs2Hex(voice.rack->rackId) + " is not implemented"); } } @@ -463,8 +479,10 @@ int APS5_VABI sceNgs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state int APS5_VABI sceNgs2VoiceGetPortInfo(uintptr_t voice_handle, uint32_t port, Ngs2VoicePortInfo* info, size_t info_size) { if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; if (info_size != sizeof(Ngs2VoicePortInfo)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; - std::lock_guard lock(Ngs2Mutex()); - auto& voice = CheckedVoice(voice_handle); + const Ngs2Lock lock; + auto* found = Ngs2FindVoice(voice_handle); + if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; + auto& voice = *found; const auto& source = PortAt(voice, port); *info = {}; info->matrix_id = source.matrix; @@ -475,8 +493,10 @@ int APS5_VABI sceNgs2VoiceGetPortInfo(uintptr_t voice_handle, uint32_t port, Ngs int APS5_VABI sceNgs2VoiceQueryInfo(uintptr_t voice_handle, uint32_t info_id, void* info, size_t info_size) { if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - std::lock_guard lock(Ngs2Mutex()); - const auto& voice = CheckedVoice(voice_handle); + const Ngs2Lock lock; + auto* found = Ngs2FindVoice(voice_handle); + if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; + const auto& voice = *found; switch (info_id) { case SCE_NGS2_VOICE_INFO_CHANNELS: { if (info_size != sizeof(Ngs2VoiceChannelsInfo)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; @@ -489,8 +509,20 @@ int APS5_VABI sceNgs2VoiceQueryInfo(uintptr_t voice_handle, uint32_t info_id, vo int APS5_VABI sceNgs2VoiceGetStateFlags(uintptr_t voice_handle, uint32_t* state_flags) { if (state_flags == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + const Ngs2Lock lock; + const auto* found = Ngs2FindVoice(voice_handle); + if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; + *state_flags = found->stateFlags; + return SCE_NGS2_OK; +} + +int APS5_VABI sceNgs2VoiceGetOwner(uintptr_t voice_handle, uintptr_t* owner_rack, uint32_t* voice_index) { std::lock_guard lock(Ngs2Mutex()); - *state_flags = CheckedVoice(voice_handle).stateFlags; + const auto* found = Ngs2FindVoice(voice_handle); + if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; + if (owner_rack == nullptr || voice_index == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + *owner_rack = reinterpret_cast(found->rack); + *voice_index = static_cast(found - found->rack->voices.data()); return SCE_NGS2_OK; } @@ -502,6 +534,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/Ngs2Atrac9.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp index b9278520ac..bbbebab2e4 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Atrac9.cpp @@ -195,6 +195,99 @@ static void TestCalcBlock() { Require(sceNgs2CalcWaveformBlock(&silent, 0, 1, &block) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); } +static void TestFrameInfo() { + const Ngs2WaveformFormat supported[] = { + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 48000, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 48000, Config, 7, 9}, + }; + const Ngs2WaveformFormat rejected[] = { + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 2, 48000, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 0, 48000, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 9, 48000, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 44100, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 0, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 192001, Config, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_ATRAC9, 1, 48000, 0, 0, 0}, + }; + for (const auto& format : supported) { + Ngs2WaveformBlock block{}; + Require(sceNgs2CalcWaveformBlock(&format, 0, SuperframeSamples, &block) == SCE_NGS2_OK && block.data_size == SuperframeBytes); + std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; + Require(sceNgs2GetWaveformFrameInfo(&format, &values[0], &values[1], &values[2], &values[3]) == SCE_NGS2_OK); + Require(values[0] == SuperframeBytes && values[1] == SuperframeSamples && values[2] == 4 && values[3] == SuperframeSamples / 4); + } + for (const auto& format : rejected) { + Ngs2WaveformBlock block{}; + const int calc = sceNgs2CalcWaveformBlock(&format, 0, SuperframeSamples, &block); + Require(calc == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; + Require(sceNgs2GetWaveformFrameInfo(&format, &values[0], &values[1], &values[2], &values[3]) == calc); + Require(values[0] == 0xa1 && values[1] == 0xa2 && values[2] == 0xa3 && values[3] == 0xa4); + } + std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; + Require(sceNgs2GetWaveformFrameInfo(nullptr, &values[0], &values[1], &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + Require(sceNgs2GetWaveformFrameInfo(&supported[0], nullptr, &values[1], &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&supported[0], &values[0], nullptr, &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&supported[0], &values[0], &values[1], nullptr, &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&supported[0], &values[0], &values[1], &values[2], nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(nullptr, nullptr, nullptr, nullptr, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(values[0] == 0xa1 && values[1] == 0xa2 && values[2] == 0xa3 && values[3] == 0xa4); +} + +static void RequirePcmBlock(std::uint32_t type, std::uint32_t channels, std::uint32_t pos, std::uint32_t count, std::uint64_t offset, std::uint64_t size) { + const Ngs2WaveformFormat format{type, channels, 48000, 0, 0, 0}; + Ngs2WaveformBlock block; + std::memset(&block, 0xab, sizeof(block)); + Require(sceNgs2CalcWaveformBlock(&format, pos, count, &block) == SCE_NGS2_OK); + Require(block.data_offset == offset && block.data_size == size && block.num_samples == count); + Require(block.num_repeats == 0 && block.num_skip_samples == 0 && block.reserved == 0 && block.user_data == 0); +} + +static void TestCalcPcmBlock() { + const std::uint32_t bytesPerSample[] = {1, 1, 2, 2, 3, 3, 4, 4, 4, 4, 8, 8}; + for (std::uint32_t type = 0x10; type <= 0x1b; ++type) { + const auto stride = 100u * bytesPerSample[type - 0x10]; + RequirePcmBlock(type, 100, 3, 5, 3u * stride, 5u * stride); + } + RequirePcmBlock(SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 100, 50, 400, 200); + RequirePcmBlock(SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1, 10, 0, 40, 0); + RequirePcmBlock(SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 0, 7, 9, 0, 0); + RequirePcmBlock(0x1a, 8, 0x4000000, 1, 0, 64); + RequirePcmBlock(0x1a, 2, 0xffffffffu, 2, 0xfffffff0u, 32); + const Ngs2WaveformFormat noRate{SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 0, 0, 0, 0}; + Ngs2WaveformBlock block{}; + Require(sceNgs2CalcWaveformBlock(&noRate, 1, 1, &block) == SCE_NGS2_OK && block.data_offset == 4 && block.data_size == 4); + for (std::uint32_t type : {0x0u, 0xfu, 0x1du, 0x20u, 0x30u, 0x43u}) { + const Ngs2WaveformFormat unknown{type, 2, 48000, 0, 0, 0}; + std::memset(&block, 0xab, sizeof(block)); + Require(sceNgs2CalcWaveformBlock(&unknown, 0, 1, &block) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); + const auto* bytes = reinterpret_cast(&block); + Require(std::all_of(bytes, bytes + sizeof(block), [](std::uint8_t byte) { return byte == 0; })); + } + std::memset(&block, 0xab, sizeof(block)); + Require(sceNgs2CalcWaveformBlock(nullptr, 0, 1, &block) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT && block.data_size == 0 && block.num_samples == 0); +} + +static void RequireVagBlock(std::uint32_t channels, std::uint32_t pos, std::uint32_t count, std::uint64_t offset, std::uint64_t size, std::uint32_t samples) { + const Ngs2WaveformFormat format{0x1c, channels, 48000, 0, 0, 0}; + Ngs2WaveformBlock block; + std::memset(&block, 0xab, sizeof(block)); + Require(sceNgs2CalcWaveformBlock(&format, pos, count, &block) == SCE_NGS2_OK); + Require(block.data_offset == offset && block.data_size == size && block.num_samples == samples); + Require(block.num_repeats == 0 && block.num_skip_samples == 0 && block.reserved == 0 && block.user_data == 0); +} + +static void TestCalcVagBlock() { + RequireVagBlock(1, 0, 28, 0, 16, 28); + RequireVagBlock(1, 27, 2, 0, 32, 56); + RequireVagBlock(1, 28, 28, 16, 16, 28); + RequireVagBlock(2, 30, 1, 32, 32, 28); + RequireVagBlock(6, 56, 57, 192, 288, 84); + RequireVagBlock(1, 0, 0, 0, 0, 0); + RequireVagBlock(1, 5, 0, 0, 16, 28); + RequireVagBlock(0, 100, 50, 0, 0, 84); +} + static void Set32(std::vector& bytes, std::size_t offset, std::uint32_t value) { for (int i = 0; i < 4; ++i) bytes[offset + i] = static_cast(value >> (i * 8)); } @@ -504,6 +597,9 @@ int main() { TestParseLoops(); TestMalformedLoops(); TestCalcBlock(); + TestFrameInfo(); + TestCalcPcmBlock(); + TestCalcVagBlock(); TestCorruptSuperframeAtPageEnd(); TestStreamPartitions(); TestRefill(reference); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index c810add593..5b6acd7ad2 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -281,6 +281,13 @@ static void TestCustomSampler() { const auto out = Render(system); for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f && out[i * 2 + 1] == 0.75f); + Ngs2CustomSamplerVoiceState state{}; + Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(Ngs2SamplerVoiceState)) == SCE_NGS2_ERROR_INVALID_OUT_SIZE); + Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + Require(state.voice_state.state_flags == (SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING) && state.num_decoded_samples == Grain); + Render(system); + Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK && state.num_decoded_samples == Grain * 2); + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -335,6 +342,323 @@ 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); +} + + +struct ModuleOptionWords { + std::uint32_t size; + std::uint32_t words[15]; +}; + +static int allocCalls = 0; + +static std::int32_t APS5_VABI CountAlloc(Ngs2ContextBufferInfo*) { + allocCalls++; + return SCE_NGS2_OK; +} + +static std::int32_t APS5_VABI CountFree(Ngs2ContextBufferInfo*) { + return SCE_NGS2_OK; +} + +static Ngs2CustomSubmixerRackOption BuiltInRackOption(const Ngs2CustomUserFx2ModuleOption& userFx2, std::uint32_t moduleId, + const ModuleOptionWords* moduleOption, std::uint32_t stateSize) { + auto option = RackOption(&userFx2, 1); + auto& module = option.custom_rack_option.module[0]; + module.module_id = moduleId; + module.option = reinterpret_cast(moduleOption); + module.state_size = stateSize; + return option; +} + +static void TestCustomRackChecks() { + calls = {}; + const auto system = CreateSystem(); + const auto userFx2 = Module(Gain); + const auto good = RackOption(&userFx2, 1); + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &good.custom_rack_option.rack_option, &query) == SCE_NGS2_OK); + const auto info = Buffer(query); + const auto create = [&](const Ngs2CustomSubmixerRackOption& option) { + uintptr_t rack = 0; + const int result = sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, &info, &rack); + Require(rack == 0); + return result; + }; + + auto option = good; + option.custom_rack_option.num_buffers = 0; + Require(create(option) == SCE_NGS2_ERROR_INVALID_NUM_BUFFERS); + Require(CreateThrows(option)); + option.custom_rack_option.num_buffers = SCE_NGS2_CUSTOM_MAX_BUFFERS + 1; + Require(create(option) == SCE_NGS2_ERROR_INVALID_NUM_BUFFERS); + option = good; + option.max_inputs = 2; + Require(create(option) == SCE_NGS2_ERROR_INVALID_NUM_BUFFERS); + option.custom_rack_option.num_buffers = 2; + Require(CreateThrows(option)); + option.custom_rack_option.num_buffers = SCE_NGS2_CUSTOM_MAX_BUFFERS; + option.custom_rack_option.module[0].dest_buffer_id = SCE_NGS2_CUSTOM_MAX_BUFFERS - 1; + Require(CreateThrows(option)); + option.custom_rack_option.module[0].dest_buffer_id = SCE_NGS2_CUSTOM_MAX_BUFFERS; + Require(create(option) == SCE_NGS2_ERROR_INVALID_BUFFER_INDEX); + option = good; + option.max_inputs = 0; + Require(CreateThrows(option)); + option.max_inputs = SCE_NGS2_CUSTOM_SUBMIXER_MAX_INPUTS + 1; + Require(CreateThrows(option)); + + option = good; + option.custom_rack_option.module[0].source_buffer_id = 1; + Require(create(option) == SCE_NGS2_ERROR_INVALID_BUFFER_INDEX); + option = good; + option.custom_rack_option.module[0].extra_buffer_id = 1; + Require(create(option) == SCE_NGS2_ERROR_INVALID_BUFFER_INDEX); + + option = good; + option.max_channels = SCE_NGS2_CUSTOM_SUBMIXER_MAX_CHANNELS + 1; + Require(create(option) == SCE_NGS2_ERROR_INVALID_NUM_CHANNELS); + option.max_channels = 16; + Require(CreateThrows(option)); + option.max_channels = SCE_NGS2_CUSTOM_SUBMIXER_MAX_CHANNELS; + Require(CreateThrows(option)); + + Require(!CreateThrows(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 0))); + Require(create(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 4)) == SCE_NGS2_ERROR_INVALID_STATE_SIZE); + const ModuleOptionWords gainOption{sizeof(Ngs2CustomModuleOption), {}}; + Require(CreateThrows(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, &gainOption, 0))); + + const ModuleOptionWords reverb{SCE_NGS2_CUSTOM_REVERB_OPTION_SIZE, {1}}; + const ModuleOptionWords reverbOff{SCE_NGS2_CUSTOM_REVERB_OPTION_SIZE, {0}}; + const ModuleOptionWords reverbSize{16, {1}}; + const ModuleOptionWords reverbWord{SCE_NGS2_CUSTOM_REVERB_OPTION_SIZE, {2}}; + Require(CreateThrows(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_REVERB, &reverb, 0))); + Require(CreateThrows(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_REVERB, &reverbOff, 0))); + Require(create(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_REVERB, &reverbSize, 0)) == SCE_NGS2_ERROR_INVALID_OPTION_SIZE); + Require(create(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_REVERB, &reverbWord, 0)) == SCE_NGS2_ERROR_INVALID_REVERB_OPTION); + Require(create(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_REVERB, &reverb, 4)) == SCE_NGS2_ERROR_INVALID_STATE_SIZE); + + const ModuleOptionWords pitchShift{SCE_NGS2_CUSTOM_PITCH_SHIFT_OPTION_SIZE, {0}}; + const ModuleOptionWords pitchShiftWord{SCE_NGS2_CUSTOM_PITCH_SHIFT_OPTION_SIZE, {1}}; + const auto pitchShiftState = SCE_NGS2_CUSTOM_PITCH_SHIFT_STATE_SIZE; + Require(CreateThrows(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT, &pitchShift, pitchShiftState))); + Require(CreateThrows(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT, &pitchShift, 0))); + Require(create(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT, &pitchShift, 4)) == SCE_NGS2_ERROR_INVALID_STATE_SIZE); + Require(create(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_PITCH_SHIFT, &pitchShiftWord, pitchShiftState)) == SCE_NGS2_ERROR_INVALID_PITCH_SHIFT_OPTION); + + const ModuleOptionWords convolution{64, {}}; + Require(CreateThrows(BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_CONVOLUTION, &convolution, 64))); + option = BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_CONVOLUTION, &convolution, 64); + option.custom_rack_option.module[0].dest_buffer_id = 1; + Require(create(option) == SCE_NGS2_ERROR_INVALID_BUFFER_INDEX); + + option = BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 0); + option.custom_rack_option.num_modules = 2; + option.custom_rack_option.module[1] = good.custom_rack_option.module[0]; + option.custom_rack_option.module[1].dest_buffer_id = 1; + Require(create(option) == SCE_NGS2_ERROR_INVALID_BUFFER_INDEX); + Require(CreateThrows(BuiltInRackOption(userFx2, 0x19, nullptr, 0))); + + option = good; + option.custom_rack_option.num_buffers = 0; + bool rejected = false; + uintptr_t rack = 0; + try { + sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, nullptr, &rack); + } catch (const std::invalid_argument&) { + rejected = true; + } + Require(rejected); + const Ngs2BufferAllocator allocator{CountAlloc, CountFree, 0}; + Require(sceNgs2RackCreateWithAllocator(system, SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, &allocator, &rack) == SCE_NGS2_ERROR_INVALID_NUM_BUFFERS); + Require(rack == 0 && allocCalls == 0 && calls.setupVoices.empty()); + + Ngs2CustomSamplerRackOption sampler{}; + sampler.custom_rack_option.rack_option.size = sizeof(sampler); + sampler.custom_rack_option.rack_option.max_grain_samples = 512; + sampler.custom_rack_option.rack_option.max_voices = 1; + sampler.custom_rack_option.num_modules = 1; + sampler.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_FILTER; + sampler.max_channel_works = 1; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_ERROR_INVALID_NUM_BUFFERS); + sampler.custom_rack_option.num_buffers = 1; + sampler.custom_rack_option.module[0].extra_buffer_id = 1; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_ERROR_INVALID_BUFFER_INDEX); + Require(rack == 0); + sampler.custom_rack_option.module[0].extra_buffer_id = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); + sampler.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN; + Require(CreateSamplerThrows(sampler)); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +struct LegacyUserFxState { + std::uint32_t calls = 0; + std::uint32_t flags = 0; + std::int32_t result = SCE_NGS2_OK; + float val0 = 0.25f; + float val1 = 0.5f; +}; + +static std::int32_t APS5_VABI ProcessLegacy(Ngs2UserFxProcessContext* context) { + auto& state = *reinterpret_cast(context->user_data0); + state.calls++; + state.flags = context->flags; + if (state.result != SCE_NGS2_OK) return state.result; + for (std::uint32_t i = 0; i < context->num_grain_samples; i++) { + context->channel_data[0][i] = state.val0; + context->channel_data[1][i] = state.val1; + } + return SCE_NGS2_OK; +} + +static Ngs2CustomSubmixerRackOption LegacyRackOption(const Ngs2CustomModuleOption* option) { + Ngs2CustomSubmixerRackOption rack{}; + auto& common = rack.custom_rack_option.rack_option; + common.size = sizeof(rack); + common.max_grain_samples = 512; + common.max_voices = 2; + common.max_matrices = 1; + common.max_ports = 1; + rack.custom_rack_option.num_buffers = 1; + rack.custom_rack_option.num_modules = 1; + rack.custom_rack_option.module[0].option = option; + rack.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_USER_FX; + rack.custom_rack_option.module[0].state_size = 16; + rack.max_channels = 2; + rack.max_inputs = 1; + return rack; +} + +static void TestCustomLegacyUserFx() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + const Ngs2CustomModuleOption header{sizeof(Ngs2CustomModuleOption)}; + const auto rack = CustomRack(system, LegacyRackOption(&header)); + const auto custom = Voice(rack); + Control(custom, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0}); + Patch(custom, master); + Event(custom, SCE_NGS2_VOICE_EVENT_PLAY); + + LegacyUserFxState state{}; + Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 0, + Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, reinterpret_cast(&state), 123, 456}); + + auto out = Render(system); + Require(state.calls == 1 && state.flags == 1); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.25f && out[i * 2 + 1] == 0.5f); + + out = Render(system); + Require(state.calls == 2 && state.flags == 0); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.25f && out[i * 2 + 1] == 0.5f); + + struct BadSizeParam { + Ngs2VoiceParamHeader header; + std::uint8_t dummy[10]; + }; + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 0, BadSizeParam{})); + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 2, Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, 0, 0, 0})); + + Ngs2CustomModuleOption badSize{2}; + Require(CreateThrows(LegacyRackOption(&badSize))); + + struct ExtendedOption { + std::uint32_t size = 20; + std::uint32_t reserved = 0; + std::uint32_t channels = 4; + float param0 = 1000.0f; + float param1 = 0.0f; + } extendedOption; + const auto rackExtended = CustomRack(system, LegacyRackOption(reinterpret_cast(&extendedOption))); + Require(sceNgs2RackDestroy(rackExtended, nullptr) == SCE_NGS2_OK); + + state.result = -1; + bool failed = false; + try { + Render(system); + } catch (const std::runtime_error&) { + failed = true; + } + Require(failed); + state.result = SCE_NGS2_OK; + + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + + const auto rackNullOpt = CustomRack(system, LegacyRackOption(nullptr)); + const auto voiceNullOpt = Voice(rackNullOpt); + Control(voiceNullOpt, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0}); + Patch(voiceNullOpt, master); + Event(voiceNullOpt, SCE_NGS2_VOICE_EVENT_PLAY); + + LegacyUserFxState state2{}; + state2.val0 = 0.125f; + state2.val1 = 0.75f; + Control(voiceNullOpt, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 0, + Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, reinterpret_cast(&state2), 123, 456}); + out = Render(system); + Require(state2.calls == 1 && state2.flags == 1); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.125f && out[i * 2 + 1] == 0.75f); + + Require(sceNgs2RackDestroy(rackNullOpt, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestCustomGain() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + const auto userFx2 = Module(Gain); + const auto rack = CustomRack(system, BuiltInRackOption(userFx2, SCE_NGS2_CUSTOM_MODULE_ID_GAIN, nullptr, 0)); + const auto custom = Voice(rack); + Control(custom, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0}); + Patch(custom, master); + Event(custom, SCE_NGS2_VOICE_EVENT_PLAY); + std::vector pcm(Grain * 2 * 8, 16384); + const auto sampler = Sampler(system, pcm); + Patch(sampler, custom); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + + auto out = Render(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.0f && out[i * 2 + 1] == 0.0f); + + Ngs2CustomVoiceGainParam gain{}; + gain.gain[0] = 2.0f; + gain.gain[1] = -0.5f; + Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, gain); + out = Render(system); + for (std::uint32_t i = 0; i < Grain; i++) { + Require(out[i * 2] == 0.5f * 2.0f * static_cast(i + 1) / Grain); + Require(out[i * 2 + 1] == 0.5f * -0.5f * static_cast(i + 1) / Grain); + } + out = Render(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 1.0f && out[i * 2 + 1] == -0.25f); + + gain.gain[0] = 1.0f; + Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, gain); + out = Render(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f * (2.0f - static_cast(i + 1) / Grain)); + + auto outside = gain; + outside.gain[2] = 33.0f; + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, outside)); + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1, gain)); + Require(ControlThrows(sampler, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, gain)); + struct ShortGainParam { + Ngs2VoiceParamHeader header; + float gain[2]; + }; + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, ShortGainParam{})); + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -342,7 +666,10 @@ static void TestCustomFilter() { int main() { TestUserFxChain(); TestRackOptions(); + TestCustomRackChecks(); TestCustomSampler(); TestCustomFilter(); + TestCustomLegacyUserFx(); + TestCustomGain(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp index 91a377793b..ae95253a90 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,21 @@ 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); + Ngs2GeomListenerWork work; + std::memset(&work, 0x5a, sizeof(work)); + unsigned char before[sizeof(work)]; + std::memcpy(before, &work, sizeof(work)); + Require(sceNgs2GeomCalcListener(nullptr, &work, 0) == static_cast(0x804A8461)); + Require(sceNgs2GeomCalcListener(nullptr, &work, 2) == static_cast(0x804A8461)); + Require(sceNgs2GeomCalcListener(nullptr, nullptr, 0) == static_cast(0x804A8461)); + Require(sceNgs2GeomCalcListener(¶m, &work, 2) == static_cast(0x804A8463)); + Require(sceNgs2GeomCalcListener(¶m, &work, 0x80000000u) == static_cast(0x804A8463)); + Require(sceNgs2GeomCalcListener(¶m, &work, 0xffffffffu) == static_cast(0x804A8463)); + Require(sceNgs2GeomCalcListener(¶m, nullptr, 2) == static_cast(0x804A8010)); + Require(std::memcmp(before, &work, sizeof(work)) == 0); for (const float value : {0.0f, -1.0f, std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()}) { param.sound_speed = value; RejectListener(¶m, 0); @@ -137,29 +135,66 @@ static void TestInvalidListener() { param.orient_front = {}; RejectListener(¶m, 0); param = Listener(); - param.orient_up = param.orient_front; - RejectListener(¶m, 0); - param = Listener(); param.orient_up = {}; RejectListener(¶m, 0); - for (const auto vector : {&Ngs2GeomListenerParam::position, &Ngs2GeomListenerParam::orient_front, - &Ngs2GeomListenerParam::orient_up, &Ngs2GeomListenerParam::velocity}) { - for (const auto component : {&Ngs2GeomVector::x, &Ngs2GeomVector::y, &Ngs2GeomVector::z}) { - for (const float value : {std::numeric_limits::infinity(), -std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()}) { - param = Listener(); - (param.*vector).*component = value; - RejectListener(¶m, 0); - } - } + param = Listener(); + param.orient_up = param.orient_front; + Ngs2GeomListenerWork parallelWork{}; + Require(sceNgs2GeomCalcListener(¶m, ¶llelWork, 0) == SCE_NGS2_OK); + RequireMatrix(parallelWork, {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}}); + 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..c9a0094eda --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp @@ -0,0 +1,73 @@ +#include "Ngs2Test.hpp" + +#include +#include +#include +#include + +static void Status(int actual, int expected, const char* message) { + if (actual != expected) throw std::runtime_error(message); +} + +static void CheckResetOption() { + Status(sceNgs2SystemResetOption(nullptr), static_cast(0x804A8010u), "null system option did not return the invalid out address error"); + constexpr std::uint64_t size = 0x90; + constexpr std::uint32_t maxGrainSamples = 512; + constexpr std::uint32_t numGrainSamples = 256; + constexpr std::uint32_t sampleRate = 48000; + constexpr std::uint32_t maxVoiceChannels = 8; + unsigned char expected[0x90] = {}; + std::memcpy(expected + 0x00, &size, sizeof(size)); + std::memcpy(expected + 0x6c, &maxGrainSamples, sizeof(maxGrainSamples)); + std::memcpy(expected + 0x70, &numGrainSamples, sizeof(numGrainSamples)); + std::memcpy(expected + 0x74, &sampleRate, sizeof(sampleRate)); + std::memcpy(expected + 0x78, &maxVoiceChannels, sizeof(maxVoiceChannels)); + static_assert(sizeof(Ngs2SystemOption) == sizeof(expected)); + Ngs2SystemOption option; + std::memset(&option, 0xAB, sizeof(option)); + Status(sceNgs2SystemResetOption(&option), SCE_NGS2_OK, "system option reset failed"); + Require(std::memcmp(&option, expected, sizeof(expected)) == 0); + Ngs2ContextBufferInfo query{}; + Status(sceNgs2SystemQueryBufferSize(&option, &query), SCE_NGS2_OK, "reset system option was rejected"); +} + +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); + CheckResetOption(); + 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/Ngs2Parse.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp index a2545f52e6..6a0c74f5b3 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp @@ -113,9 +113,59 @@ static void TestParseFileBank() { std::filesystem::remove(path); } +static void TestFrameInfoPcm() { + for (std::uint32_t channels = 1; channels <= 8; channels++) { + for (const std::uint32_t rate : {1u, 22050u, 48000u, 192000u}) { + const Ngs2WaveformFormat format{SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, channels, rate, 0, 0, 0}; + std::uint32_t frameSize = 0xa1, frameSamples = 0xa2, units = 0xa3, delay = 0xa4; + Require(sceNgs2GetWaveformFrameInfo(&format, &frameSize, &frameSamples, &units, &delay) == SCE_NGS2_OK); + Require(frameSize == channels * 2 && frameSamples == 1 && units == 1 && delay == 0); + const auto file = PcmFile(channels, rate, 16, 10); + Ngs2WaveformInfo info{}; + Require(sceNgs2ParseWaveformData(file.data(), file.size(), &info) == SCE_NGS2_OK); + Require(info.audio_frame_size == frameSize && info.num_audio_frame_samples == frameSamples); + Require(info.num_audio_unit_per_frame == units && info.num_delay_samples == delay); + } + } + const Ngs2WaveformFormat ignored{SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 48000, 0x12345678, 3, 5}; + std::uint32_t frameSize = 0xa1, frameSamples = 0xa2, units = 0xa3, delay = 0xa4; + Require(sceNgs2GetWaveformFrameInfo(&ignored, &frameSize, &frameSamples, &units, &delay) == SCE_NGS2_OK); + Require(frameSize == 4 && frameSamples == 1 && units == 1 && delay == 0); +} + +static void TestFrameInfoErrors() { + const Ngs2WaveformFormat pcm{SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 48000, 0, 0, 0}; + std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; + const auto untouched = [&] { return values[0] == 0xa1 && values[1] == 0xa2 && values[2] == 0xa3 && values[3] == 0xa4; }; + Require(sceNgs2GetWaveformFrameInfo(&pcm, nullptr, &values[1], &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&pcm, &values[0], nullptr, &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&pcm, &values[0], &values[1], nullptr, &values[3]) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(&pcm, &values[0], &values[1], &values[2], nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(nullptr, nullptr, nullptr, nullptr, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2GetWaveformFrameInfo(nullptr, &values[0], &values[1], &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + Require(untouched()); + const Ngs2WaveformFormat invalid[] = { + {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 0, 48000, 0, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 9, 48000, 0, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 0, 0, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 192001, 0, 0, 0}, + {0x99, 0, 48000, 0, 0, 0}, + {SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2, 0, 0, 0, 0}, + }; + for (const auto& format : invalid) { + Require(sceNgs2GetWaveformFrameInfo(&format, &values[0], &values[1], &values[2], &values[3]) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); + Require(untouched()); + } + for (const std::uint32_t type : {0u, 0x10u, 0x11u, 0x13u, 0x14u, 0x16u, SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 0x19u, 0x41u, 0xffffffffu}) { + Require(FrameInfoThrows({type, 2, 48000, 0, 0, 0})); + } +} + int main() { TestParsePcm(); TestParseFile(); TestParseFileBank(); + TestFrameInfoPcm(); + TestFrameInfoErrors(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index e9ae7428fa..d76c464a6c 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -6,14 +6,18 @@ #include #include #include +#include #include #include #include +#include #include #include -static uintptr_t Sampler(uintptr_t system, const std::vector& pcm, std::uint32_t repeats) { - const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); +static uintptr_t Sampler(uintptr_t system, const std::vector& pcm, std::uint32_t repeats, uintptr_t* rack = nullptr) { + const auto created = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + if (rack != nullptr) *rack = created; + const auto voice = Voice(created); Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), repeats, 0, static_cast(pcm.size()), 0, 0x55}; Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); @@ -41,8 +45,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); @@ -259,10 +263,32 @@ static void TestPan() { work.speaker_angles[0] = -INFINITY; Require(PanThrows(&work, &front, 1, 2)); Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK); - Require(PanInitThrows(nullptr, nullptr, 360.0f, 2)); - for (std::uint32_t count : {0u, 1u, 3u, 6u, 8u}) Require(PanInitThrows(&work, nullptr, 360.0f, count)); + Ngs2PanWork marked; + std::memset(&marked, 0x5a, sizeof(marked)); + Ngs2PanWork expected; + std::memcpy(&expected, &marked, sizeof(marked)); + const auto unchanged = [&] { return std::memcmp(&marked, &expected, sizeof(marked)) == 0; }; const float pair[] = {-30.0f, 30.0f}; - Require(PanInitThrows(&work, pair, 180.0f, 2) && !PanInitThrows(&work, pair, 2.0f * pi, 2)); + Require(sceNgs2PanInit(nullptr, nullptr, 360.0f, 2) == static_cast(0x804A8010)); + Require(sceNgs2PanInit(nullptr, pair, 180.0f, 3) == static_cast(0x804A8010)); + Require(sceNgs2PanInit(nullptr, nullptr, 100.0f, 3) == static_cast(0x804A8010)); + for (std::uint32_t count : {1u, 3u, 6u, 8u, 255u}) { + Require(sceNgs2PanInit(&marked, nullptr, 360.0f, count) == static_cast(0x804A8451)); + Require(unchanged()); + } + Require(PanInitThrows(&marked, nullptr, 360.0f, 0)); + Require(unchanged()); + for (std::uint32_t count : {0u, 1u, 3u, 6u, 8u, 255u}) { + Require(sceNgs2PanInit(&marked, pair, 360.0f, count) == static_cast(0x804A8451)); + Require(sceNgs2PanInit(&marked, pair, 2.0f * pi, count) == static_cast(0x804A8451)); + Require(unchanged()); + } + for (const float unitAngle : {180.0f, 0.0f, 359.0f, 361.0f, 90.0f, pi}) { + Require(sceNgs2PanInit(&marked, pair, unitAngle, 2) == static_cast(0x804A8450)); + Require(sceNgs2PanInit(&marked, pair, unitAngle, 3) == static_cast(0x804A8450)); + Require(unchanged()); + } + Require(sceNgs2PanInit(&work, pair, 2.0f * pi, 2) == SCE_NGS2_OK); const float invalid[] = {0.0f, NAN}; Require(PanInitThrows(&work, invalid, 360.0f, 2)); const float onUnit[] = {720.0f, 30.0f}; @@ -297,6 +323,163 @@ static void TestPitchAndRepeat() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestSetupClearsCallback() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + const std::vector pcm{1000, 2000, 3000, 4000}; + const auto sampler = Sampler(system, pcm, 0); + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, OnBlock, 7, SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); + const Ngs2WaveformBlock oneShot{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast(pcm.size()), 0, 0}; + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &oneShot}); + Patch(sampler, master); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + + callbackFlags.clear(); + auto out = RenderI16(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < pcm.size() ? pcm[i] : 0)); + Require(callbackFlags.empty() && Flags(sampler) == 0); + + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); + Patch(sampler, master); + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, OnBlock, 7, SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + const Ngs2WaveformBlock stream{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast(pcm.size()), 0, 0x55}; + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &stream}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + out = RenderI16(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < pcm.size() ? pcm[i] : 0)); + Require(callbackFlags.size() == 1 && callbackFlags[0] == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +struct PcmResetCallback { + std::vector pcm; + std::uint32_t flag; + 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); @@ -605,6 +788,33 @@ static void TestUserData() { threw = true; } Require(threw); + + Require(sceNgs2RackSetUserData(0x1234, 5) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); + Require(sceNgs2RackSetUserData(system, 5) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); + value = 1; + Require(sceNgs2RackGetUserData(0x1234, &value) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE && value == 1); + Require(sceNgs2RackGetUserData(system, &value) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE && value == 1); + Require(sceNgs2RackGetUserData(0x1234, nullptr) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + const auto other = CreateRack(system, SCE_NGS2_RACK_ID_MASTERING); + Require(sceNgs2RackGetUserData(rack, &value) == SCE_NGS2_OK && value == 0); + Require(sceNgs2RackSetUserData(rack, 0x0123456789abcdef) == SCE_NGS2_OK); + Require(sceNgs2RackGetUserData(rack, &value) == SCE_NGS2_OK && value == 0x0123456789abcdef); + Require(sceNgs2RackGetUserData(other, &value) == SCE_NGS2_OK && value == 0); + Require(sceNgs2SystemGetUserData(system, &value) == SCE_NGS2_OK && value == 0xfedcba9876543210); + Require(sceNgs2RackSetUserData(other, 0) == SCE_NGS2_OK); + Require(sceNgs2RackGetUserData(rack, &value) == SCE_NGS2_OK && value == 0x0123456789abcdef); + threw = false; + try { + sceNgs2RackGetUserData(rack, nullptr); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + value = 1; + Require(sceNgs2RackGetUserData(rack, &value) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE && value == 1); + Require(sceNgs2RackSetUserData(rack, 5) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -751,25 +961,335 @@ static void TestStereoIntoSurround() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static bool AnsweredWithin(const std::atomic& answered, std::chrono::milliseconds limit = std::chrono::seconds(2)) { + const auto deadline = std::chrono::steady_clock::now() + limit; + while (!answered && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + return answered; +} + +static void TestRackLockLeavesQueries() { + const auto system = CreateSystem(); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + const auto voice = Voice(rack); + Require(sceNgs2RackLock(0x1234) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); + Require(sceNgs2RackLock(rack) == SCE_NGS2_OK); + Require(sceNgs2RackLock(rack) == SCE_NGS2_OK); + std::atomic answered = false; + std::thread query([&] { + std::uint32_t flags = 0; + Require(sceNgs2VoiceGetStateFlags(voice, &flags) == SCE_NGS2_OK); + answered = true; + }); + const bool answeredWhileLocked = AnsweredWithin(answered); + std::atomic rendered = false; + std::thread render([&] { + RenderI16(system); + rendered = true; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + const bool renderedWhileLocked = rendered; + Require(sceNgs2RackUnlock(rack) == SCE_NGS2_OK); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + const bool renderedWhileLockedOnce = rendered; + Require(sceNgs2RackUnlock(rack) == SCE_NGS2_OK); + query.join(); + render.join(); + Require(answeredWhileLocked); + Require(!renderedWhileLocked && !renderedWhileLockedOnce && rendered); + Require(sceNgs2RackUnlock(rack) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +struct QueryInCallback { + uintptr_t voice = 0; + std::atomic answered = false; + bool answeredInCallback = false; + std::thread query; +}; + +static void APS5_VABI QueryFromAnotherThread(const Ngs2VoiceCallbackInfo* info) { + auto& callback = *reinterpret_cast(info->callback_data); + callback.query = std::thread([&callback] { + std::uint32_t flags = 0; + Require(sceNgs2VoiceGetStateFlags(callback.voice, &flags) == SCE_NGS2_OK); + callback.answered = true; + }); + callback.answeredInCallback = AnsweredWithin(callback.answered); +} + +static void TestVoiceCallbackLeavesQueries() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + const std::vector pcm(Grain / 2, 1000); + const auto sampler = Sampler(system, pcm, 0); + Patch(sampler, master); + QueryInCallback callback; + callback.voice = sampler; + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, QueryFromAnotherThread, reinterpret_cast(&callback), SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + RenderI16(system); + Require(callback.query.joinable()); + callback.query.join(); + Require(callback.answered && callback.answeredInCallback); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +struct RackLockInCallback { + uintptr_t rack = 0; + std::atomic locked = false; + std::atomic callbackReturned = false; + std::atomic release = false; + bool lockedDuringCallback = false; + std::thread locker; +}; + +static void APS5_VABI LockFromAnotherThread(const Ngs2VoiceCallbackInfo* info) { + auto& callback = *reinterpret_cast(info->callback_data); + callback.locker = std::thread([&callback] { + Require(sceNgs2RackLock(callback.rack) == SCE_NGS2_OK); + callback.lockedDuringCallback = !callback.callbackReturned; + callback.locked = true; + while (!callback.release) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(sceNgs2RackUnlock(callback.rack) == SCE_NGS2_OK); + }); + static_cast(AnsweredWithin(callback.locked, std::chrono::milliseconds(200))); + callback.callbackReturned = true; +} + +static void TestRackLockWaitsForRender() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + const std::vector pcm(Grain / 2, 1000); + RackLockInCallback callback; + const auto sampler = Sampler(system, pcm, 0, &callback.rack); + Patch(sampler, master); + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, LockFromAnotherThread, reinterpret_cast(&callback), SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + RenderI16(system); + const bool lockedAfterRender = AnsweredWithin(callback.locked); + callback.release = true; + Require(callback.locker.joinable()); + callback.locker.join(); + Require(lockedAfterRender && !callback.lockedDuringCallback); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +struct CrossSystemCallback { + uintptr_t otherRack = 0; + std::atomic* entered = nullptr; + const std::atomic* awaited = nullptr; +}; + +static void APS5_VABI LockAnotherSystem(const Ngs2VoiceCallbackInfo* info) { + auto& callback = *reinterpret_cast(info->callback_data); + *callback.entered = true; + static_cast(AnsweredWithin(*callback.awaited, std::chrono::milliseconds(200))); + Require(sceNgs2RackLock(callback.otherRack) == SCE_NGS2_OK); + Require(sceNgs2RackUnlock(callback.otherRack) == SCE_NGS2_OK); +} + +static uintptr_t CallbackSampler(uintptr_t system, CrossSystemCallback& callback, uintptr_t* rack) { + static const std::vector pcm(Grain / 2, 1000); + const auto sampler = Sampler(system, pcm, 0, rack); + Patch(sampler, Mastering(system, 1)); + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, LockAnotherSystem, reinterpret_cast(&callback), SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + return sampler; +} + +static void TestCallbacksLockEachOthersSystem() { + const auto first = CreateSystem(); + const auto second = CreateSystem(); + std::atomic firstEntered = false; + std::atomic secondEntered = false; + CrossSystemCallback firstCallback{0, &firstEntered, &secondEntered}; + CrossSystemCallback secondCallback{0, &secondEntered, &firstEntered}; + uintptr_t firstRack = 0; + uintptr_t secondRack = 0; + CallbackSampler(first, firstCallback, &firstRack); + CallbackSampler(second, secondCallback, &secondRack); + firstCallback.otherRack = secondRack; + secondCallback.otherRack = firstRack; + std::atomic firstRendered = false; + std::atomic secondRendered = false; + std::thread firstRender([&] { + RenderI16(first); + firstRendered = true; + }); + std::thread secondRender([&] { + RenderI16(second); + secondRendered = true; + }); + Require(AnsweredWithin(firstRendered) && AnsweredWithin(secondRendered)); + firstRender.join(); + secondRender.join(); + Require(firstEntered && secondEntered); + Require(sceNgs2SystemDestroy(first, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(second, nullptr) == SCE_NGS2_OK); +} + +static void TestRackLocksAcrossSystemsAndRender() { + const auto first = CreateSystem(); + const auto second = CreateSystem(); + const auto heldRack = CreateRack(second, SCE_NGS2_RACK_ID_SAMPLER); + std::atomic entered = false; + const std::atomic never = false; + CrossSystemCallback callback{heldRack, &entered, &never}; + uintptr_t renderedRack = 0; + CallbackSampler(first, callback, &renderedRack); + std::atomic holding = false; + std::atomic released = false; + std::thread locker([&] { + Require(sceNgs2RackLock(heldRack) == SCE_NGS2_OK); + holding = true; + static_cast(AnsweredWithin(entered, std::chrono::milliseconds(200))); + Require(sceNgs2RackLock(renderedRack) == SCE_NGS2_OK); + Require(sceNgs2RackUnlock(renderedRack) == SCE_NGS2_OK); + Require(sceNgs2RackUnlock(heldRack) == SCE_NGS2_OK); + released = true; + }); + Require(AnsweredWithin(holding)); + std::atomic rendered = false; + std::thread render([&] { + RenderI16(first); + rendered = true; + }); + Require(AnsweredWithin(released) && AnsweredWithin(rendered)); + locker.join(); + render.join(); + Require(entered); + Require(sceNgs2SystemDestroy(first, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(second, nullptr) == SCE_NGS2_OK); +} + +static void TestRackDestroyWaitsForLock() { + const auto system = CreateSystem(); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + Require(sceNgs2RackLock(rack) == SCE_NGS2_OK); + std::atomic destroyed = false; + std::thread destroyer([&] { + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + destroyed = true; + }); + const bool destroyedWhileLocked = AnsweredWithin(destroyed, std::chrono::milliseconds(200)); + Require(!destroyedWhileLocked && sceNgs2RackUnlock(rack) == SCE_NGS2_OK); + Require(AnsweredWithin(destroyed)); + destroyer.join(); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +struct DestroyInCallback { + uintptr_t handle = 0; + bool system = false; +}; + +static void APS5_VABI DestroyFromCallback(const Ngs2VoiceCallbackInfo* info) { + const auto& destroy = *reinterpret_cast(info->callback_data); + static_cast(destroy.system ? sceNgs2SystemDestroy(destroy.handle, nullptr) : sceNgs2RackDestroy(destroy.handle, nullptr)); +} + +static void APS5_VABI RenderFromCallback(const Ngs2VoiceCallbackInfo* info) { + static_cast(RenderI16(info->callback_data)); +} + +static void TestDestroyDuringOwnRenderThrows() { + for (const bool destroySystem : {false, true}) { + const auto system = CreateSystem(); + uintptr_t rack = 0; + const std::vector pcm(Grain / 2, 1000); + const auto sampler = Sampler(system, pcm, 0, &rack); + Patch(sampler, Mastering(system, 1)); + const DestroyInCallback destroy{destroySystem ? system : rack, destroySystem}; + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, DestroyFromCallback, reinterpret_cast(&destroy), SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + bool refused = false; + try { + RenderI16(system); + } catch (const std::runtime_error& error) { + refused = std::string(error.what()).find("is not implemented") != std::string::npos; + } + Require(refused); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + } + for (const bool destroySystem : {false, true}) { + const auto outer = CreateSystem(); + const auto inner = CreateSystem(); + uintptr_t outerRack = 0; + const std::vector pcm(Grain / 2, 1000); + const auto outerSampler = Sampler(outer, pcm, 0, &outerRack); + Patch(outerSampler, Mastering(outer, 1)); + const auto innerSampler = Sampler(inner, pcm, 0); + Patch(innerSampler, Mastering(inner, 1)); + const DestroyInCallback destroy{destroySystem ? outer : outerRack, destroySystem}; + Control(outerSampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, RenderFromCallback, inner, SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + Control(innerSampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, DestroyFromCallback, reinterpret_cast(&destroy), SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + Event(outerSampler, SCE_NGS2_VOICE_EVENT_PLAY); + Event(innerSampler, SCE_NGS2_VOICE_EVENT_PLAY); + bool refused = false; + try { + RenderI16(outer); + } catch (const std::runtime_error& error) { + refused = std::string(error.what()).find("is not implemented") != std::string::npos; + } + Require(refused); + Require(sceNgs2SystemDestroy(inner, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(outer, nullptr) == SCE_NGS2_OK); + } +} + static void TestLock() { const auto system = CreateSystem(); + const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); 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); } @@ -883,6 +1403,60 @@ static void TestUnsetMatrix() { Require(sceNgs2SystemDestroy(stereoSystem, nullptr) == SCE_NGS2_OK); } +template +static void TestRenderCapacity(std::uint32_t waveformType, std::uint32_t channels) { + const auto system = CreateSystem(); + const auto samples = Grain * channels; + const TSample sentinel = 123; + std::vector out(samples * 3, sentinel); + const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(TSample), waveformType, channels}; + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + Require(std::all_of(out.begin(), out.begin() + samples, [](TSample value) { return value == 0; })); + Require(std::all_of(out.begin() + samples, out.end(), [sentinel](TSample value) { return value == sentinel; })); + + const auto master = Mastering(system, channels); + const std::vector pcm(Grain, 16384); + const auto sampler = Sampler(system, pcm, 0); + Patch(sampler, master); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + std::fill(out.begin(), out.end(), sentinel); + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + Require(std::all_of(out.begin(), out.begin() + samples, [sentinel](TSample value) { return value > 0 && value != sentinel; })); + Require(std::all_of(out.begin() + samples, out.end(), [sentinel](TSample value) { return value == sentinel; })); + + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + Require(std::all_of(out.begin(), out.begin() + samples, [](TSample value) { return value == 0; })); + Require(std::all_of(out.begin() + samples, out.end(), [sentinel](TSample value) { return value == sentinel; })); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestSilentRenderValidation() { + const auto system = CreateSystem(); + std::vector out(Grain, 123.0f); + Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float) - 1, SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1}; + const auto invalid = [&] { + bool rejected = false; + try { sceNgs2SystemRender(system, &info, 1); } catch (const std::invalid_argument&) { rejected = true; } + Require(rejected); + Require(std::all_of(out.begin(), out.end(), [](float value) { return value == 123.0f; })); + }; + invalid(); + info.buffer_size++; + info.num_channels = 0; + invalid(); + info.num_channels = 9; + invalid(); + info.num_channels = 1; + info.waveform_type = UINT32_MAX; + bool unsupported = false; + try { sceNgs2SystemRender(system, &info, 1); } catch (const std::runtime_error&) { unsupported = true; } + Require(unsupported); + Require(std::all_of(out.begin(), out.end(), [](float value) { return value == 123.0f; })); + info = {}; + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static void TestAllocator() { const Ngs2BufferAllocator allocator{Allocate, Release, 9}; uintptr_t system = 0; @@ -916,12 +1490,28 @@ int main() { TestReverbSetup(); TestSampleRate(); TestUserData(); + TestPcmCallbackReset(); + TestPcmCallbackRefill(); + TestPcmCallbackSetup(); + TestSetupClearsCallback(); TestMasteringGain(); TestMatrixLevelClamp(); TestStereoIntoSurround(); TestLock(); + TestRackLockLeavesQueries(); + TestVoiceCallbackLeavesQueries(); + TestRackLockWaitsForRender(); + TestCallbacksLockEachOthersSystem(); + TestRackLocksAcrossSystemsAndRender(); + TestRackDestroyWaitsForLock(); + TestDestroyDuringOwnRenderThrows(); TestDefaultChannelMap(); TestUnsetMatrix(); + TestRenderCapacity(SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1); + TestRenderCapacity(SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2); + TestRenderCapacity(SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1); + TestRenderCapacity(SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2); + TestSilentRenderValidation(); TestAllocator(); exitSystem = CreateSystem(); return 0; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Report.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Report.cpp index f76d61f46e..146b691a34 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Report.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Report.cpp @@ -1,33 +1,43 @@ -#include "prx/libc/include/general/VabiMacros.hpp" +#include "Ngs2Test.hpp" #include -#include #include -extern "C" { -int APS5_VABI sceNgs2ReportRegisterHandler(std::uint32_t reportType, void* handler, std::uintptr_t userData, std::uintptr_t* outHandle); -int APS5_VABI sceNgs2ReportUnregisterHandler(std::uintptr_t reportHandle); -} - namespace { -void Require(bool value) { if (!value) std::abort(); } - template bool Throws(Call call) { try { call(); - } catch (const std::runtime_error&) { + } catch (const std::exception&) { return true; } return false; } +int callbacks = 0; +void APS5_VABI Counted(const Ngs2ReportDataHeader*, std::uintptr_t) { + callbacks++; +} + } int main() { - std::uintptr_t handle = 0x1234; + Ngs2Handle handle = 0x1234; Require(Throws([&] { sceNgs2ReportRegisterHandler(5, nullptr, 0, &handle); })); Require(handle == 0x1234); + Require(sceNgs2ReportRegisterHandler(5, Counted, 0, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + + Require(sceNgs2ReportRegisterHandler(5, Counted, 42, &handle) == SCE_NGS2_OK); + Require(handle != 0); + Ngs2Handle second = 0; + Require(sceNgs2ReportRegisterHandler(7, Counted, 43, &second) == SCE_NGS2_OK); + Require(second != 0 && second != handle); + + Require(sceNgs2ReportUnregisterHandler(handle) == SCE_NGS2_OK); + Require(Throws([&] { sceNgs2ReportUnregisterHandler(handle); })); Require(Throws([] { sceNgs2ReportUnregisterHandler(0x1234); })); + Require(sceNgs2ReportUnregisterHandler(second) == SCE_NGS2_OK); + Require(callbacks == 0); + return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp index 726cf9148b..fa222ce472 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp @@ -158,6 +158,24 @@ static void TestSilenceBlock() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestNoBlocksForClearedVoice() { + const auto system = CreateSystem(); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + const auto voice = RackVoice(rack, 0); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {}}); + const Ngs2WaveformBlock unused{}; + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, &Unused, 0, 0, &unused}); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, &Unused, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET, 0, &unused}); + const float level = 1.0f; + Control(voice, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, 0, &level}); + Require(Throws([&] { Control(voice, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 1, 0, &level}); })); + Require(Flags(voice) == 0); + const std::vector pcm(4, 16384); + Require(Throws([&] { AddBlock(voice, pcm.data(), pcm.size() * 2, 4, 0); })); + Require(Throws([&] { Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, &Unused, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND, 0, &unused}); })); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static void TestDrainedStreamReportsEmpty() { const auto system = CreateSystem(); const auto master = Mastering(system, 1); @@ -190,5 +208,6 @@ int main() { TestStreamRejections(); TestSilenceBlock(); TestDrainedStreamReportsEmpty(); + TestNoBlocksForClearedVoice(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index fdc379e560..4768cc7cb3 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -6,12 +6,14 @@ #include #include #include +#include #include extern "C" { int APS5_VABI sceNgs2ParseWaveformData(const void*, size_t, Ngs2WaveformInfo*); int APS5_VABI sceNgs2ParseWaveformFile(const char*, uint32_t, Ngs2WaveformInfo*); int APS5_VABI sceNgs2CalcWaveformBlock(const Ngs2WaveformFormat*, uint32_t, uint32_t, Ngs2WaveformBlock*); +int APS5_VABI sceNgs2GetWaveformFrameInfo(const Ngs2WaveformFormat*, uint32_t*, uint32_t*, uint32_t*, uint32_t*); int APS5_VABI sceNgs2SystemResetOption(Ngs2SystemOption*); int APS5_VABI sceNgs2SystemQueryBufferSize(const Ngs2SystemOption*, Ngs2ContextBufferInfo*); int APS5_VABI sceNgs2SystemCreate(const Ngs2SystemOption*, const Ngs2ContextBufferInfo*, uintptr_t*); @@ -28,20 +30,28 @@ int APS5_VABI sceNgs2SystemRender(uintptr_t, const Ngs2RenderBufferInfo*, uint32 int APS5_VABI sceNgs2PanInit(Ngs2PanWork*, const float*, float, uint32_t); int APS5_VABI sceNgs2PanGetVolumeMatrix(Ngs2PanWork*, const Ngs2PanParam*, uint32_t, uint32_t, float*); int APS5_VABI sceNgs2RackQueryBufferSize(uint32_t, const Ngs2RackOption*, Ngs2ContextBufferInfo*); +int APS5_VABI sceNgs2RackLock(uintptr_t); +int APS5_VABI sceNgs2RackUnlock(uintptr_t); int APS5_VABI sceNgs2RackCreate(uintptr_t, uint32_t, const Ngs2RackOption*, const Ngs2ContextBufferInfo*, uintptr_t*); int APS5_VABI sceNgs2RackCreateWithAllocator(uintptr_t, uint32_t, const Ngs2RackOption*, const Ngs2BufferAllocator*, uintptr_t*); int APS5_VABI sceNgs2RackDestroy(uintptr_t, Ngs2ContextBufferInfo*); int APS5_VABI sceNgs2RackGetVoiceHandle(uintptr_t, uint32_t, uintptr_t*); int APS5_VABI sceNgs2RackGetInfo(uintptr_t, Ngs2RackInfo*, size_t); +int APS5_VABI sceNgs2RackSetUserData(uintptr_t, uintptr_t); +int APS5_VABI sceNgs2RackGetUserData(uintptr_t, uintptr_t*); int APS5_VABI sceNgs2VoiceControl(uintptr_t, const Ngs2VoiceParamHeader*); int APS5_VABI sceNgs2VoiceRunCommands(uintptr_t, const Ngs2VoiceCommand*, size_t); int APS5_VABI sceNgs2VoiceGetState(uintptr_t, Ngs2VoiceState*, size_t); int APS5_VABI sceNgs2VoiceGetStateFlags(uintptr_t, uint32_t*); +int APS5_VABI sceNgs2VoiceGetOwner(uintptr_t, uintptr_t*, uint32_t*); int APS5_VABI sceNgs2VoiceGetPortInfo(uintptr_t, uint32_t, Ngs2VoicePortInfo*, size_t); int APS5_VABI sceNgs2VoiceQueryInfo(uintptr_t, uint32_t, void*, size_t); int APS5_VABI sceNgs2GeomResetListenerParam(Ngs2GeomListenerParam*); int APS5_VABI sceNgs2GeomCalcListener(const Ngs2GeomListenerParam*, Ngs2GeomListenerWork*, std::uint32_t); int APS5_VABI sceNgs2GeomResetSourceParam(Ngs2GeomSourceParam*); +int APS5_VABI sceNgs2GeomApply(const Ngs2GeomListenerWork*, const Ngs2GeomSourceParam*, Ngs2GeomAttribute*, std::uint32_t); +int APS5_VABI sceNgs2ReportRegisterHandler(std::uint32_t, Ngs2ReportHandler, std::uintptr_t, Ngs2Handle*); +int APS5_VABI sceNgs2ReportUnregisterHandler(Ngs2Handle); } inline void Check(bool value, int line) { @@ -52,6 +62,18 @@ inline void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) +inline bool FrameInfoThrows(const Ngs2WaveformFormat& format) { + std::uint32_t values[4] = {0xa1, 0xa2, 0xa3, 0xa4}; + bool threw = false; + try { + sceNgs2GetWaveformFrameInfo(&format, &values[0], &values[1], &values[2], &values[3]); + } catch (const std::exception&) { + threw = true; + } + Require(values[0] == 0xa1 && values[1] == 0xa2 && values[2] == 0xa3 && values[3] == 0xa4); + return threw; +} + inline constexpr std::uint32_t Grain = 8; inline std::vector> buffers; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceHandles.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceHandles.cpp new file mode 100644 index 0000000000..c5fed18835 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceHandles.cpp @@ -0,0 +1,89 @@ +#include "Ngs2Test.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +static constexpr std::array Names{"control", "commands", "state", "port", "query", "flags", "owner"}; +static constexpr int InvalidVoice = static_cast(0x804A8203u); +static constexpr int NullList = static_cast(0x804A8360u); + +static void Status(int actual, int expected, const std::string& message) { + if (actual != expected) throw std::runtime_error(message); +} + +static void Invalid(std::string_view api, uintptr_t handle) { + alignas(16) std::array output; + output.fill(0xa5); + const Ngs2VoiceEventParam event{{sizeof(Ngs2VoiceEventParam), 0, SCE_NGS2_VOICE_PARAM_EVENT}, SCE_NGS2_VOICE_EVENT_STOP_IMM}; + const Ngs2VoiceCommand command{2, 0, 4, 0, {.u = SCE_NGS2_VOICE_EVENT_STOP_IMM}}; + int result = 0; + if (api == "control") result = sceNgs2VoiceControl(handle, &event.header); + else if (api == "commands") result = sceNgs2VoiceRunCommands(handle, &command, 1); + else if (api == "state") result = sceNgs2VoiceGetState(handle, reinterpret_cast(output.data()), sizeof(Ngs2SamplerVoiceState)); + else if (api == "port") result = sceNgs2VoiceGetPortInfo(handle, 0, reinterpret_cast(output.data()), sizeof(Ngs2VoicePortInfo)); + else if (api == "query") result = sceNgs2VoiceQueryInfo(handle, SCE_NGS2_VOICE_INFO_CHANNELS, output.data(), sizeof(Ngs2VoiceChannelsInfo)); + else if (api == "flags") result = sceNgs2VoiceGetStateFlags(handle, reinterpret_cast(output.data())); + else if (api == "owner") result = sceNgs2VoiceGetOwner(handle, reinterpret_cast(output.data()), reinterpret_cast(output.data() + 16)); + else throw std::runtime_error("unknown voice API test selection"); + Status(result, InvalidVoice, std::string(api) + " returned the wrong invalid-voice error"); + Require(std::all_of(output.begin(), output.end(), [](auto byte) { return byte == 0xa5; })); +} + +int main(int argc, char** argv) { + try { + if (argc > 2 || (argc == 2 && std::find(Names.begin(), Names.end(), std::string_view(argv[1])) == Names.end())) throw std::runtime_error("unknown voice API test selection"); + const auto selected = [&](auto name) { return argc == 1 || std::string_view(argv[1]) == name; }; + for (auto name : Names) if (selected(name)) { Invalid(name, 0); Invalid(name, 0x1234); } + Status(sceNgs2VoiceControl(0x1234, nullptr), NullList, "Control parameter-list precedence differs from PS5"); + Status(sceNgs2VoiceGetState(0x1234, nullptr, 0), InvalidVoice, "GetState checked output before the voice"); + Status(sceNgs2VoiceGetOwner(0x1234, nullptr, nullptr), InvalidVoice, "GetOwner checked output before the voice"); + const auto system = CreateSystem(); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + const auto voice = Voice(rack); + for (auto name : Names) if (selected(name)) { Invalid(name, system); Invalid(name, rack); } + Status(sceNgs2VoiceControl(voice, nullptr), NullList, "live voice lost the null parameter-list error"); + Status(sceNgs2VoiceGetState(voice, nullptr, 0), SCE_NGS2_ERROR_INVALID_OUT_SIZE, "GetState checked null output before its minimum size"); + Status(sceNgs2VoiceGetState(voice, nullptr, sizeof(Ngs2VoiceState) - 1), SCE_NGS2_ERROR_INVALID_OUT_SIZE, "GetState lost its minimum-size boundary"); + Status(sceNgs2VoiceGetState(voice, nullptr, sizeof(Ngs2VoiceState)), SCE_NGS2_ERROR_INVALID_OUT_ADDRESS, "GetState rejected its minimum-size boundary before null output"); + Status(sceNgs2VoiceGetState(voice, nullptr, sizeof(Ngs2SamplerVoiceState)), SCE_NGS2_ERROR_INVALID_OUT_ADDRESS, "GetState lost null-output validation"); + Status(sceNgs2VoiceGetStateFlags(voice, nullptr), SCE_NGS2_ERROR_INVALID_OUT_ADDRESS, "GetStateFlags lost null-output validation"); + uintptr_t owner = 0x5a5a; + std::uint32_t index = 0x5a5a; + Status(sceNgs2VoiceGetOwner(voice, nullptr, &index), SCE_NGS2_ERROR_INVALID_OUT_ADDRESS, "GetOwner lost null-rack validation"); + Status(sceNgs2VoiceGetOwner(voice, &owner, nullptr), SCE_NGS2_ERROR_INVALID_OUT_ADDRESS, "GetOwner lost null-index validation"); + Require(owner == 0x5a5a && index == 0x5a5a); + const auto other = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + for (const auto owned : {rack, other}) { + Ngs2RackInfo info{}; + Status(sceNgs2RackGetInfo(owned, &info, sizeof(info)), SCE_NGS2_OK, "rack info lookup failed"); + Require(info.max_voices > 1); + for (std::uint32_t voiceId = 0; voiceId < info.max_voices; voiceId++) { + uintptr_t handle = 0; + Status(sceNgs2RackGetVoiceHandle(owned, voiceId, &handle), SCE_NGS2_OK, "voice handle lookup failed"); + Status(sceNgs2VoiceGetOwner(handle, &owner, &index), SCE_NGS2_OK, "valid owner lookup failed"); + Require(owner == owned && index == voiceId); + } + } + Require(sceNgs2RackDestroy(other, nullptr) == SCE_NGS2_OK); + std::uint32_t flags = 0; + Status(sceNgs2VoiceGetStateFlags(voice, &flags), SCE_NGS2_OK, "valid state-flags lookup failed"); + Ngs2SamplerVoiceState state{}; + Status(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)), SCE_NGS2_OK, "valid sampler state lookup failed"); + const Ngs2VoiceCommand stop{2, 0, 4, 0, {.u = SCE_NGS2_VOICE_EVENT_STOP_IMM}}; + Status(sceNgs2VoiceRunCommands(voice, &stop, 1), SCE_NGS2_OK, "valid voice command failed"); + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + for (auto name : Names) if (selected(name)) Invalid(name, voice); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + std::cout << "NGS2 invalid voice errors, outputs and 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/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/libSceNgs2/CMakeLists.txt b/core/libs/prx/libSceNgs2/CMakeLists.txt index 6d09bbbe29..41b6cfe96a 100644 --- a/core/libs/prx/libSceNgs2/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2/CMakeLists.txt @@ -1,5 +1,6 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/Ngs2Library.cmake) add_sce_ngs2_library(libSceNgs2) +target_sources(libSceNgs2 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/PendingExports.cpp) if(BUILD_TESTING) add_test_executable(ngs2_core_render_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/tests/Ngs2Render.cpp) diff --git a/core/libs/prx/libSceNgs2/PendingExports.cpp b/core/libs/prx/libSceNgs2/PendingExports.cpp new file mode 100644 index 0000000000..d930fb00c1 --- /dev/null +++ b/core/libs/prx/libSceNgs2/PendingExports.cpp @@ -0,0 +1,41 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNgs2CustomRackGetModuleInfo(std::uintptr_t rackHandle, std::uint32_t moduleIndex, void* outInfo, std::size_t infoSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2FftInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2FftProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2FftQuerySize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2ParseWaveformUser(std::uintptr_t handler, std::uintptr_t userData, void* outInfo) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2SystemEnumHandles(std::uintptr_t* outHandles, std::uint32_t maxHandles) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2SystemEnumRackHandles(std::uintptr_t systemHandle, std::uintptr_t* outHandles, std::uint32_t maxHandles) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNgs2VoiceGetMatrixInfo(std::uintptr_t voiceHandle, std::uint32_t matrixId, void* outInfo, std::size_t infoSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNotification/Export.cpp b/core/libs/prx/libSceNotification/Export.cpp new file mode 100644 index 0000000000..90362f2991 --- /dev/null +++ b/core/libs/prx/libSceNotification/Export.cpp @@ -0,0 +1,34 @@ +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNotificationSend(int type, std::uint8_t flag, std::uint8_t* message) { + (void)type; + (void)flag; + (void)message; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNotificationSendById(int type, std::uint8_t flag, std::uint8_t* id, std::uint8_t* jsonData) { + (void)type; + (void)flag; + (void)id; + (void)jsonData; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNotificationShowPsButtonPersistentBanner(std::uint8_t* jsonParam) { + (void)jsonParam; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI sceNotificationHidePsButtonPersistentBanner() { + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libSceNotificationClient/Export.cpp b/core/libs/prx/libSceNotificationClient/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceNotificationClient/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceNpAuth/CMakeLists.txt b/core/libs/prx/libSceNpAuth/CMakeLists.txt index ce6ce6a2f8..67acfa7500 100644 --- a/core/libs/prx/libSceNpAuth/CMakeLists.txt +++ b/core/libs/prx/libSceNpAuth/CMakeLists.txt @@ -5,7 +5,8 @@ configure_windows_unwind(libSceNpAuth) if(BUILD_TESTING) add_test_executable(guest_np_auth_authorized_app_code_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpAuthAuthorizedAppCode.cpp) target_include_directories(guest_np_auth_authorized_app_code_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_np_auth_authorized_app_code_tests PRIVATE libSceNpAuth) + target_link_libraries(guest_np_auth_authorized_app_code_tests PRIVATE libSceNpAuth libc) + configure_windows_unwind(guest_np_auth_authorized_app_code_tests) add_test(NAME guest_np_auth_authorized_app_code COMMAND guest_np_auth_authorized_app_code_tests) if(WIN32) set_tests_properties(guest_np_auth_authorized_app_code PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceNpAuth/Export.cpp b/core/libs/prx/libSceNpAuth/Export.cpp index 86734f9a5a..3289f920f0 100644 --- a/core/libs/prx/libSceNpAuth/Export.cpp +++ b/core/libs/prx/libSceNpAuth/Export.cpp @@ -1,7 +1,9 @@ #include +#include #include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -22,6 +24,8 @@ struct RequestInfo { bool aborted = false; bool polled = false; int result = SCE_NP_ERROR_SIGNED_OUT; + std::int32_t resolveRetry = 0; + std::array timeouts{}; }; std::mutex g_requestMutex; @@ -62,8 +66,14 @@ int APS5_VABI sceNpAuthDeleteRequest(int req_id) { return 0; } -int APS5_VABI sceNpAuthGetAuthorizedAppCode() { - return SCE_NP_ERROR_SIGNED_OUT; +int APS5_VABI sceNpAuthGetAuthorizedAppCode(std::int32_t requestId, const NpAuthAuthorizedAppCodeParameter* parameter, char* code, std::int32_t* issuerId, bool* authorizationRequired) { + (void)issuerId; + if (parameter == nullptr || code == nullptr || authorizationRequired == nullptr) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + if (parameter->size != sizeof(NpAuthAuthorizedAppCodeParameter)) return static_cast(0x80550302u); + if (parameter->userId == -1 || parameter->clientId == nullptr || parameter->scope == nullptr || parameter->option > 1) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_requestMutex); + if (!g_requests.contains(requestId)) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; + throw std::logic_error("sceNpAuthGetAuthorizedAppCode: authorization backend is not implemented"); } int APS5_VABI sceNpAuthGetAuthorizationCodeV3(int req_id, const void* param, void* auth_code, int* issuer_id) { @@ -74,7 +84,6 @@ int APS5_VABI sceNpAuthGetAuthorizationCodeV3(int req_id, const void* param, voi if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; if (it->second.processed) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; if (it->second.aborted) { - it->second.result = ORBIS_NP_AUTH_ERROR_ABORTED; return ORBIS_NP_AUTH_ERROR_ABORTED; } it->second.processed = true; @@ -92,7 +101,6 @@ int APS5_VABI sceNpAuthGetIdTokenV3(int req_id, const void* param, void* id_toke if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; if (it->second.processed) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; if (it->second.aborted) { - it->second.result = ORBIS_NP_AUTH_ERROR_ABORTED; return ORBIS_NP_AUTH_ERROR_ABORTED; } it->second.processed = true; @@ -103,11 +111,19 @@ int APS5_VABI sceNpAuthGetIdTokenV3(int req_id, const void* param, void* id_toke return SCE_NP_ERROR_SIGNED_OUT; } +int APS5_VABI sceNpAuthGetAuthorizationCode(int req_id, const void* param, void* auth_code) { + return sceNpAuthGetAuthorizationCodeV3(req_id, param, auth_code, nullptr); +} + +int APS5_VABI sceNpAuthGetIdToken(int req_id, const void* param, void* id_token) { + return sceNpAuthGetIdTokenV3(req_id, param, id_token); +} + int APS5_VABI sceNpAuthPollAsync(int req_id, int* result) { std::lock_guard lock(g_requestMutex); auto it = g_requests.find(req_id); if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; - if (!it->second.isAsync || (!it->second.processed && !it->second.aborted)) return ORBIS_NP_AUTH_ERROR_INVALID_ID; + if (!it->second.isAsync || !it->second.processed) return ORBIS_NP_AUTH_ERROR_INVALID_ID; if (!it->second.polled) { it->second.polled = true; return NP_POLL_ASYNC_RUNNING; @@ -116,14 +132,19 @@ int APS5_VABI sceNpAuthPollAsync(int req_id, int* result) { return NP_POLL_ASYNC_FINISHED; } -int APS5_VABI sceNpAuthSetTimeout(int req_id, int resolve_retry, uint32_t resolve_timeout, uint32_t connect_timeout, uint32_t send_timeout, uint32_t receive_timeout) { - (void)resolve_retry; - (void)resolve_timeout; - (void)connect_timeout; - (void)send_timeout; - (void)receive_timeout; +int APS5_VABI sceNpAuthSetTimeout(std::int32_t requestId, std::int32_t resolveRetry, std::uint32_t resolveTimeout, std::uint32_t connectTimeout, std::uint32_t sendTimeout, std::uint32_t receiveTimeout) { + if (resolveRetry < 0) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + if ((static_cast(resolveRetry) | resolveTimeout | connectTimeout | sendTimeout | receiveTimeout) == 0) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + if (resolveTimeout != 0 && resolveTimeout < 1000000) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + if (connectTimeout != 0 && connectTimeout < 10000000) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + if (sendTimeout != 0 && sendTimeout < 10000000) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + if (receiveTimeout != 0 && receiveTimeout < 10000000) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; std::lock_guard lock(g_requestMutex); - return g_requests.contains(req_id) ? 0 : ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; + const auto found = g_requests.find(requestId); + if (found == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; + found->second.resolveRetry = resolveRetry; + found->second.timeouts = {resolveTimeout, connectTimeout, sendTimeout, receiveTimeout}; + return 0; } int APS5_VABI sceNpAuthWaitAsync(int req_id, int* result) { @@ -131,9 +152,17 @@ int APS5_VABI sceNpAuthWaitAsync(int req_id, int* result) { std::lock_guard lock(g_requestMutex); auto it = g_requests.find(req_id); if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; - if (!it->second.isAsync || (!it->second.processed && !it->second.aborted)) return ORBIS_NP_AUTH_ERROR_INVALID_ID; + if (!it->second.isAsync || !it->second.processed) return ORBIS_NP_AUTH_ERROR_INVALID_ID; *result = it->second.result; return NP_POLL_ASYNC_FINISHED; } +int APS5_VABI sceNpAuthGetAuthorizationCodeA(int req_id, const void* param, void* auth_code, int* issuer_id) { + return sceNpAuthGetAuthorizationCodeV3(req_id, param, auth_code, issuer_id); +} + +int APS5_VABI sceNpAuthGetIdTokenA(int req_id, const void* param, void* token) { + return sceNpAuthGetIdTokenV3(req_id, param, token); +} + } diff --git a/core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt index 3d65cfe796..15dcbb4f2e 100644 --- a/core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt +++ b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt @@ -2,10 +2,12 @@ add_library(libSceNpAuthAuthorizedAppDialog SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceNpAuthAuthorizedAppDialog PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceNpAuthAuthorizedAppDialog) +target_link_libraries(libSceNpAuthAuthorizedAppDialog PRIVATE libSceUserService) + if(BUILD_TESTING) add_test_executable(guest_np_auth_authorized_app_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpAuthAuthorizedAppDialog.cpp) target_include_directories(guest_np_auth_authorized_app_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_np_auth_authorized_app_dialog_tests PRIVATE libSceNpAuthAuthorizedAppDialog) + target_link_libraries(guest_np_auth_authorized_app_dialog_tests PRIVATE libSceNpAuthAuthorizedAppDialog libSceUserService) add_test(NAME guest_np_auth_authorized_app_dialog COMMAND guest_np_auth_authorized_app_dialog_tests) if(WIN32) set_tests_properties(guest_np_auth_authorized_app_dialog PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp index 6a722de5a4..9244dedeea 100644 --- a/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp +++ b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp @@ -3,8 +3,11 @@ #include #include #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" +extern "C" int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* users); + namespace { constexpr int COMMON_DIALOG_STATUS_NONE = 0; @@ -12,7 +15,8 @@ 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_INVALID_ARGUMENT = static_cast(0x80B8000Au); +constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast(0x80B8000Bu); 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); @@ -30,11 +34,17 @@ int APS5_VABI sceNpAuthAuthorizedAppDialogInitialize(void) { return 0; } -int APS5_VABI sceNpAuthAuthorizedAppDialogOpen(const void* param) { +int APS5_VABI sceNpAuthAuthorizedAppDialogOpen(const NpAuthAuthorizedAppDialogParam* param) { + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (!ValidateCommonDialogBaseParam(param->base)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (param->size != sizeof(*param) || param->userId == -1) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + UserServiceLoginUserIdList users{}; + if (sceUserServiceGetLoginUserIdList(&users) != 0 || std::find(std::begin(users.user_id), std::end(users.user_id), param->userId) == std::end(users.user_id)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (param->application == nullptr || param->application[0] == 0 || param->text == nullptr || param->text[0] == 0) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (std::strlen(param->text) > 1024 || param->mode > 1 || !std::all_of(std::begin(param->reserved), std::end(param->reserved), [](std::uint8_t value) { return value == 0; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; 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; } @@ -47,18 +57,19 @@ int APS5_VABI sceNpAuthAuthorizedAppDialogGetStatus(void) { return g_status.load(); } -int APS5_VABI sceNpAuthAuthorizedAppDialogGetResult(void* result) { +int APS5_VABI sceNpAuthAuthorizedAppDialogGetResult(std::int32_t* result) { + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; 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; + *result = COMMON_DIALOG_RESULT_USER_CANCELED; + return COMMON_DIALOG_RESULT_USER_CANCELED; } int APS5_VABI sceNpAuthAuthorizedAppDialogClose(void) { if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_status.load() != COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_NOT_RUNNING; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/prx/libSceNpCommerce/CMakeLists.txt b/core/libs/prx/libSceNpCommerce/CMakeLists.txt new file mode 100644 index 0000000000..1c59d83752 --- /dev/null +++ b/core/libs/prx/libSceNpCommerce/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceNpCommerce SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpCommerce PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpCommerce) +target_link_libraries(libSceNpCommerce PRIVATE libc libSceUserService) + +if(BUILD_TESTING) + add_test_executable(guest_np_commerce_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpCommerceDialog.cpp) + target_include_directories(guest_np_commerce_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_commerce_dialog_tests PRIVATE libSceNpCommerce libc libSceUserService) + configure_windows_unwind(guest_np_commerce_dialog_tests) + add_test(NAME guest_np_commerce_dialog COMMAND guest_np_commerce_dialog_tests) + if(WIN32) + set_tests_properties(guest_np_commerce_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpCommerce/Export.cpp b/core/libs/prx/libSceNpCommerce/Export.cpp index 525178f39e..e224383c46 100644 --- a/core/libs/prx/libSceNpCommerce/Export.cpp +++ b/core/libs/prx/libSceNpCommerce/Export.cpp @@ -1,21 +1,95 @@ #include #include #include +#include +#include "prx/libc/include/ApplicationHeap.hpp" #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" +extern "C" int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* users); + 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_RESULT_USER_CANCELED = 1; 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(0x80B80006u); -constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B80009u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); + +constexpr int COMMON_DIALOG_ERROR_INVALID_ARGUMENT = static_cast(0x80B8000Au); +constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast(0x80B8000Bu); int g_status = COMMON_DIALOG_STATUS_NONE; +std::uint64_t g_userData = 0; + +std::uint32_t compiledSdkVersion() { + const auto* process = static_cast(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t version; + std::memcpy(&version, process + 16, sizeof(version)); + return version; +} + +bool isLoggedIn(std::int32_t userId) { + UserServiceLoginUserIdList users{}; + return sceUserServiceGetLoginUserIdList(&users) == 0 && userId != -1 && std::find(std::begin(users.user_id), std::end(users.user_id), userId) != std::end(users.user_id); +} + +bool validateParam(const NpCommerceDialogParam& param) { + const auto sdkVersion = compiledSdkVersion(); + if (sdkVersion >= 0x01700000 && (param.size != sizeof(param) || !std::all_of(std::begin(param.reserved), std::end(param.reserved), [](std::uint8_t value) { return value == 0; }) || !isLoggedIn(param.userId) || param.option > 7)) return false; + switch (param.mode) { + case 0: + return param.productCount <= 1 && (param.products != nullptr) == (param.productCount != 0); + case 1: + return param.productCount == 1 && param.products != nullptr; + case 2: + return param.productCount <= 1 && (param.productCount != 0 ? param.products != nullptr : sdkVersion < 0x01700000 || param.products == nullptr); + case 3: + return param.productCount >= 1 && param.productCount <= 10 && param.products != nullptr; + case 4: + return param.productCount <= 10 && (param.productCount != 0 ? param.products != nullptr : sdkVersion < 0x01700000 || param.products == nullptr); + case 5: + return param.productCount == 0 && (sdkVersion >= 0x03500000 ? param.argument == 1 : (param.argument & 3) != 0 && (sdkVersion < 0x01700000 || param.argument < 4)); + default: + return false; + } +} + +bool validateParam2(const NpCommerceDialogParam2& param) { + if (!isLoggedIn(param.userId) || param.size != sizeof(param)) return false; + if (param.option >= 8 && (param.contentMode == 0 || (param.contentMode == 1 && param.option >= 100))) return false; + if (!std::all_of(std::begin(param.reserved), std::end(param.reserved), [](std::uint8_t value) { return value == 0; })) return false; + bool valid = false; + switch (param.mode) { + case 0: + valid = param.contentMode < 2 && param.productCount <= 1 && (param.products != nullptr) == (param.productCount != 0); + break; + case 1: + valid = param.contentMode < 2 && param.productCount == 1 && param.products != nullptr; + break; + case 2: + valid = param.productCount <= 1 && (param.products != nullptr) == (param.productCount != 0); + break; + case 3: + valid = param.contentMode < 2 && param.productCount >= 1 && param.productCount <= 10 && param.products != nullptr; + break; + case 4: + valid = param.contentMode < 2 && param.productCount <= 10 && (param.products != nullptr) == (param.productCount != 0); + break; + case 5: + valid = param.productCount == 0 && param.argument == 1; + break; + default: + return false; + } + if (!valid) return false; + if (param.contentMode != 0 && (param.contentMode != 1 || param.mode != 1)) throw std::runtime_error("commerce dialog: extended content process version is not implemented"); + return true; +} } @@ -28,9 +102,12 @@ int APS5_VABI sceNpCommerceDialogInitialize() { return 0; } -int APS5_VABI sceNpCommerceDialogOpen(const void* param) { - if (g_status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; +int APS5_VABI sceNpCommerceDialogOpen(const NpCommerceDialogParam* param) { if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (!ValidateCommonDialogBaseParam(param->base)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (g_status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (!validateParam(*param)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + g_userData = param->userData; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } @@ -43,24 +120,27 @@ int APS5_VABI sceNpCommerceDialogGetStatus(void) { return g_status; } -int APS5_VABI sceNpCommerceDialogGetResult(void* result) { +int APS5_VABI sceNpCommerceDialogGetResult(NpCommerceDialogResult* result) { if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (g_status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; if (g_status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - // SceNpCommerceDialogResult: int32 result, bool authorized, then reserved bytes. - std::int32_t value = COMMON_DIALOG_RESULT_USER_CANCELED; - std::memcpy(result, &value, sizeof(value)); - std::uint8_t authorized = 0; - std::memcpy(static_cast(result) + sizeof(value), &authorized, sizeof(authorized)); - return 0; + const auto sdkVersion = compiledSdkVersion(); + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + result->authorized = 0; + result->userData = g_userData; + if (sdkVersion >= 0x01700000) std::fill(std::begin(result->reserved), std::end(result->reserved), 0); + return COMMON_DIALOG_RESULT_USER_CANCELED; } int APS5_VABI sceNpCommerceDialogClose(void) { if (g_status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_status != COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_NOT_RUNNING; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } int APS5_VABI sceNpCommerceDialogTerminate() { + if (g_status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; g_status = COMMON_DIALOG_STATUS_NONE; return 0; } @@ -79,7 +159,17 @@ int APS5_VABI sceNpCommerceSetPsStoreIconLayout(int layout) { return 0; } -int APS5_VABI sceNpCommerceDialogOpen2(void) { +int APS5_VABI sceNpCommerceDialogOpen2(const NpCommerceDialogParam2* param) { + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (!ValidateCommonDialogBaseParam(param->base)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (g_status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (!validateParam2(*param)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + g_userData = param->userData; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; +} + +int APS5_VABI sceNpCommerceDialogInitializeInternal() { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceNpCommon/CMakeLists.txt b/core/libs/prx/libSceNpCommon/CMakeLists.txt new file mode 100644 index 0000000000..f85817c30e --- /dev/null +++ b/core/libs/prx/libSceNpCommon/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(libSceNpCommon SHARED EXCLUDE_FROM_ALL PendingExports.cpp) +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp") + target_sources(libSceNpCommon PRIVATE Export.cpp) +endif() +target_include_directories(libSceNpCommon PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpCommon) diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp new file mode 100644 index 0000000000..d2daed1a92 --- /dev/null +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -0,0 +1,763 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpAllocateKernelMemoryNoAlignment(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAllocateKernelMemoryWithAlignment(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpArchInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpArchTerm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAtomicCas32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAtomicDec32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAtomicInc32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBase64Decoder(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBase64Encoder(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBase64GetDecodeSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBase64UrlDecoder(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBase64UrlEncoder(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBase64UrlGetDecodeSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCalloutInitCtx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCalloutStartOnCtx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCalloutStartOnCtx64(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCalloutStopOnCtx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCalloutTermCtx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCancelEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpClearEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCloseEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCloseSema(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCmpNpId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCmpNpIdInOrder(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCmpOnlineId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCondDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCondInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCondSignal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCondSignalAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCondSignalTo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCondTimedwait(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCondWait(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCreateEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCreateSema(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCreateThread(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpDbgAssignDebugId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpDbgDumpBinary(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpDbgDumpText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpDeleteEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpDeleteSema(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpFreeKernelMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGetNavSdkVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGetPlatformType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGetProcessId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGetRandom(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGetSdkVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGetSdkVersionUInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGetSystemClockUsec(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGlobalHeapGetAllocator(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGlobalHeapGetAllocatorEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGlobalHeapGetAllocatorExPtr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGlobalHeapGetAllocatorPtr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpHeapDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpHeapGetAllocator(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpHeapGetStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpHeapInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpHeapShowStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpHexToInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInt32ToStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInt64ToStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpIntGetPlatformType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpIntIsOnlineIdString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpIntIsValidOnlineId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpIntSetPlatformType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpIntToHex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpIpc2ClientInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpIpc2ClientTerm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpJoinThread(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpJsonParse(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpJsonParseBuf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpJsonParseBufInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpJsonParseEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpJsonParseExInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpJsonParseInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwCondDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwCondInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwCondSignal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwCondSignalAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwCondSignalTo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwCondWait(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwMutexDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwMutexInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwMutexLock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwMutexTryLock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwMutexUnlock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMemoryHeapDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMemoryHeapGetAllocator(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMemoryHeapGetAllocatorEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMemoryHeapInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMutexDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMutexInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMutexLock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMutexTryLock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMutexUnlock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpOpenEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpOpenSema(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPanic(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPollEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPollSema(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpRtcConvertToPosixTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpRtcFormatRFC3339(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpRtcParseRFC3339(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpServerErrorJsonGetErrorCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpServerErrorJsonMultiGetErrorCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpServerErrorJsonParse(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpServerErrorJsonParseInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpServerErrorJsonParseMultiInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSetEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSetPlatformType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalSema(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrBuildHex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrParseHex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrToInt32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrToInt64(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrToUInt32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrToUInt64(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrcpyToBuf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrnParseHex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrncpyToBuf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpThreadGetId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUInt32ToStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUInt64ToStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUserGetUserIdList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilBuildTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilCanonicalizeNpIdForPs4(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilCanonicalizeNpIdForPsp2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilCmpAccountId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetDateSetAuto(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetDbgCommerce(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetEnv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetFakeDisplayNameMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetFakeRateLimit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetIgnoreNpTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetNpDebug(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetNpLanguageCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetNpLanguageCode2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetNpLanguageCode2Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetNpLanguageCodeStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetNpTestPatch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetNthChar(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetShareTitleCheck(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetSystemLanguage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetTrcNotify(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetWebApi2FakeRateLimit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetWebApi2FakeRateLimitTarget(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetWebTraceSetting(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilHttpUrlEncode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilJidToNpId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilJsonEscape(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilJsonGetOneChar(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} + +int APS5_VABI sceNpUtilJsonUnescape(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} diff --git a/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt b/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt new file mode 100644 index 0000000000..2519e8cc81 --- /dev/null +++ b/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt @@ -0,0 +1,21 @@ +add_library(libSceNpCppWebApi SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpCppWebApi PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpCppWebApi) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_np_cpp_web_api_string_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpCppWebApiString.cpp) + target_include_directories(guest_np_cpp_web_api_string_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_cpp_web_api_string_tests PRIVATE libSceNpCppWebApi libc) + configure_windows_unwind(guest_np_cpp_web_api_string_tests) + add_test(NAME guest_np_cpp_web_api_string COMMAND guest_np_cpp_web_api_string_tests) + if(WIN32) + set_tests_properties(guest_np_cpp_web_api_string PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp new file mode 100644 index 0000000000..5c53984a9f --- /dev/null +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -0,0 +1,2360 @@ +#include +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +static constexpr int SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN = static_cast(0x80552907); + +// Offline PSN: request entry points (API calls, transaction start/readData, factories) fail with +// SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN, the constant shadPS4/fpPS4 document for this code; parameter +// bookkeeping succeeds; response accessors stay unimplemented because a failed request never +// produces a response. +extern "C" { + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V111UserFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_4UserEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody18setStartSerialRankERKi() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody8setGroupERKNS3_5GroupE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody8setLimitERKi() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody8setUsersERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_4UserEEEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody9setOffsetERKi() { + return 0; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122GetRankingResponseBody10getEntriesEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody10setCommentEPKc() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody12setSmallDataEPKvm() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody15setNeedsTmpRankERKb() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody19setComparedDateTimeERK10SceRtcTick() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody7setPcIdERKi() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V128GetRankingRequestBodyFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_21GetRankingRequestBodyEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V129RecordScoreRequestBodyFactory6createEPNS1_6Common10LibContextElPNS5_12IntrusivePtrINS3_22RecordScoreRequestBodyEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V14User12setAccountIdERKm() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V14User7setPcIdERKi() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi10getRankingEiRKNS4_21ParameterToGetRankingERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_22GetRankingResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRanking10initializeEPNS1_6Common10LibContextEi() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRanking24setgetRankingRequestBodyENS1_6Common12IntrusivePtrINS3_21GetRankingRequestBodyEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRanking9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRankingC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRankingD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi22getLargeDataByObjectIdEiRKNS4_33ParameterToGetLargeDataByObjectIdERNS1_6Common21DownStreamTransactionINS8_12IntrusivePtrINS4_37GetLargeDataByObjectIdResponseHeadersEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi33ParameterToGetLargeDataByObjectId10initializeEPNS1_6Common10LibContextEPKc() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi33ParameterToGetLargeDataByObjectId9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi33ParameterToGetLargeDataByObjectIdC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi33ParameterToGetLargeDataByObjectIdD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi18getBoardDefinitionEiRKNS4_29ParameterToGetBoardDefinitionERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_30GetBoardDefinitionResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi29ParameterToGetBoardDefinition10initializeEPNS1_6Common10LibContextEi() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi29ParameterToGetBoardDefinition9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi29ParameterToGetBoardDefinitionC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi29ParameterToGetBoardDefinitionD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi11recordScoreEiRKNS4_22ParameterToRecordScoreERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_23RecordScoreResponseBodyEEENSA_INS4_26RecordScoreResponseHeadersEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi15recordLargeDataEiRKNS4_26ParameterToRecordLargeDataERNS1_6Common19UpStreamTransactionINS8_12IntrusivePtrINS3_27RecordLargeDataResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScore10initializeEPNS1_6Common10LibContextEiNS6_12IntrusivePtrINS3_22RecordScoreRequestBodyEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScore22setxPsnAtomicOperationENS5_19XPsnAtomicOperationE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScore9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScoreC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScoreD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeData10initializeEPNS1_6Common10LibContextEiNS5_19XPsnAtomicOperationEPKc() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeData9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeDataC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeDataD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi12downloadDataEiRKNS4_23ParameterToDownloadDataERNS1_6Common21DownStreamTransactionINS8_12IntrusivePtrINS4_27DownloadDataResponseHeadersEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData10initializeEPNS1_6Common10LibContextEPKci() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadDataC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadDataD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common10InitParamsC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common10InitParamsD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common10LibContextC1Ev(uint64_t* self) { + *self = 0; + return 0; +} + +// Initialization succeeds as on an offline console (see libSceJson2); individual requests fail. +int APS5_VABI _ZN3sce2Np9CppWebApi6Common10initializeERKNS2_10InitParamsERNS2_10LibContextE(const void* params, uint64_t* context) { + (void)params; + if (context) *context = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +struct IntrusivePtr { + void* object; + void (APS5_VABI* deleter)(void*); + void* libContext; +}; +static_assert(sizeof(IntrusivePtr) == 0x18); + +static constexpr std::size_t ReferenceCountOffset = 0; +static constexpr std::size_t VectorReferenceCountOffset = 0x30; + +static std::atomic_ref ReferenceCount(void* object, const std::size_t offset) { + return std::atomic_ref(*reinterpret_cast(static_cast(object) + offset)); +} + +static void CopyIntrusivePtr(IntrusivePtr* self, const IntrusivePtr* source, const std::size_t referenceCountOffset) { + *self = *source; + if (self->object) ReferenceCount(self->object, referenceCountOffset).fetch_add(1); +} + +static void ReleaseIntrusivePtr(IntrusivePtr* self, const std::size_t referenceCountOffset, const char* function) { + if (!self->object) return; + if (ReferenceCount(self->object, referenceCountOffset).fetch_sub(1) != 1) return; + if (!self->deleter) NotImplemented_nid_no_patch(function); + self->deleter(self->object); + self->object = nullptr; + self->deleter = nullptr; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, ReferenceCountOffset); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, ReferenceCountOffset); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, VectorReferenceCountOffset, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEED2Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextEm() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE8sendDataEPKvm() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEE8readDataEPcm() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEE8readDataEPcm() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEED1Ev(void* self) { + (void)self; + return 0; +} + +struct CommonString { + std::int32_t referenceCount; + char* buffer; + std::uint64_t bufferSize; + void* libContext; +}; +static_assert(sizeof(CommonString) == 0x20 && offsetof(CommonString, buffer) == 0x08); +static_assert(offsetof(CommonString, bufferSize) == 0x10 && offsetof(CommonString, libContext) == 0x18); + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(CommonString* self, void* libContext) { + self->referenceCount = 0; + self->buffer = nullptr; + self->bufferSize = 0; + self->libContext = libContext; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringD1Ev(CommonString* self) { + std::free(self->buffer); + self->buffer = nullptr; + self->bufferSize = 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V14UserEEEE8pushBackERKS8_() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V14UserEEEEC1EPNS2_10LibContextE(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V14UserEEEED1Ev(void* self) { + (void)self; + return 0; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEE3endEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEE5beginEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEEppEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common9terminateERNS2_10LibContextE(uint64_t* context) { + *context = 0; + return 0; +} + +const void* APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V122GetRankingResponseBody22getLastUpdatedDateTimeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V123RecordScoreResponseBody10getTmpRankEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::uint32_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V123RecordScoreResponseBody16getTmpSerialRankEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody11getSortModeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::uint32_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody13getEntryLimitEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody13getUpdateModeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody13sortModeIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody15entryLimitIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody15updateModeIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody16getMaxScoreLimitEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody16getMinScoreLimitEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody18maxScoreLimitIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody18minScoreLimitIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +std::uint32_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody20getLargeDataNumLimitEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::size_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody21getLargeDataSizeLimitEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody22largeDataNumLimitIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody23largeDataSizeLimitIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry10getCommentEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry11getObjectIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry11getOnlineIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry12commentIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +std::uint64_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry12getAccountIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +const void* APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry12getSmallDataEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +std::uint32_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry13getSerialRankEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry13objectIdIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +std::uint32_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry14getHighestRankEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry14smallDataIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +std::uint32_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry20getHighestSerialRankEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry7getPcIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::uint32_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry7getRankEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry8getScoreEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V19RecordApi26RecordScoreResponseHeaders24getXPsnAtomicOperationIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V15EntryEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6BinaryEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEEdeEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE18getResponseHeadersERSB_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +std::size_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6Binary4sizeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +const void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common6Binary9getBinaryEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String5c_strEv(const CommonString* self) { + return self->buffer != nullptr ? self->buffer : ""; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEEdeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEEneERKS9_(const void* self, const void* other) { + (void)self; + (void)other; + NotImplemented_nid_no_patch(__func__); + return false; +} + +// Shared vector bodies. A Vector handle is an IntrusivePtr whose object is a body; +// bodies adopted from a title through the pointer/deleter constructor keep their +// 32-bit reference count at 0x30, so bodies created here do the same. +static constexpr std::uint64_t VectorBodyMagic = 0x434e507056656342ULL; +struct VectorBody { + std::uint64_t magic; + std::uint8_t reserved[0x28]; + std::int32_t referenceCount; + std::int32_t elementKind; // 1 = IntrusivePtr elements, 2 = String elements + void* elements; + std::uint64_t size; + std::uint64_t capacity; +}; +static_assert(sizeof(VectorBody) == 0x50 && offsetof(VectorBody, referenceCount) == 0x30); + +static void APS5_VABI DestroyVectorBody(void* object) { + auto* body = static_cast(object); + if (body->elementKind == 1) { + auto* elements = static_cast(body->elements); + for (std::uint64_t index = 0; index < body->size; ++index) { + ReleaseIntrusivePtr(&elements[index], 0x00, __func__); + } + } else if (body->elementKind == 2) { + auto* elements = static_cast(body->elements); + for (std::uint64_t index = 0; index < body->size; ++index) { + std::free(elements[index].buffer); + } + } + std::free(body->elements); + std::free(body); +} + +static VectorBody* VectorBodyFor(IntrusivePtr* self, const std::int32_t elementKind, const char* function) { + auto* body = static_cast(self->object); + if (body == nullptr) { + body = static_cast(std::calloc(1, sizeof(VectorBody))); + if (body == nullptr) return nullptr; + body->magic = VectorBodyMagic; + body->referenceCount = 1; + body->elementKind = elementKind; + self->object = body; + self->deleter = &DestroyVectorBody; + return body; + } + if (body->magic != VectorBodyMagic || body->elementKind != elementKind) { + // appending to a title-provided body needs its element layout, which is unknown + NotImplemented_nid_no_patch(function); + return nullptr; + } + return body; +} + +static bool VectorGrow(VectorBody* body, const std::size_t elementSize) { + if (body->size < body->capacity) return true; + const std::uint64_t capacity = body->capacity != 0 ? body->capacity * 2 : 4; + void* elements = std::realloc(body->elements, static_cast(capacity) * elementSize); + if (elements == nullptr) return false; + body->elements = elements; + body->capacity = capacity; + return true; +} + +static int VectorPushBackIntrusivePtr(IntrusivePtr* self, const IntrusivePtr* element) { + VectorBody* body = VectorBodyFor(self, 1, __func__); + if (body == nullptr) return -1; + if (!VectorGrow(body, sizeof(IntrusivePtr))) return -1; + auto* elements = static_cast(body->elements); + elements[body->size] = *element; + if (element->object != nullptr) ReferenceCount(element->object, ReferenceCountOffset).fetch_add(1); + body->size += 1; + return 0; +} + +static int VectorPushBackString(IntrusivePtr* self, const CommonString* element) { + VectorBody* body = VectorBodyFor(self, 2, __func__); + if (body == nullptr) return -1; + if (!VectorGrow(body, sizeof(CommonString))) return -1; + CommonString copy = *element; + copy.referenceCount = 0; + copy.buffer = nullptr; + copy.bufferSize = 0; + if (element->buffer != nullptr) { + const std::size_t length = std::strlen(element->buffer) + 1; + copy.buffer = static_cast(std::malloc(length)); + if (copy.buffer == nullptr) return -1; + std::memcpy(copy.buffer, element->buffer, length); + copy.bufferSize = length; + } + static_cast(body->elements)[body->size] = copy; + body->size += 1; + return 0; +} +// -------------------------------------------------------------------------- +// SessionManager2V1 and extra Common symbols, from a title dump whose modules lack +// libSceNpCppWebApi.prx. +// -------------------------------------------------------------------------- + + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V115LocalizedStringEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x00); +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V120RequestPlayerSessionEEptEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120RequestPlayerSession16setMaxSpectatorsERKi() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V139RequestPlayerSessionMemberPlayerFactory6createEPNS1_6Common10LibContextERKNS5_6VectorINS5_12IntrusivePtrINS3_32RequestCreatePlayerSessionPlayerEEEEEPNS9_INS3_32RequestPlayerSessionMemberPlayerEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEE3endEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayerD1Ev() { + return 0; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V110FromMember11getOnlineIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitations26setinvitationInvalidFilterEPKc() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V120RequestPlayerSessionEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V151PostPlayerSessionsSessionIdMemberPlayersRequestBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSessionD1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V122LocalizedStringFactory6createEPNS1_6Common10LibContextEPKcNS_4Json6ObjectEPNS5_12IntrusivePtrINS3_15LocalizedStringEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V151PostPlayerSessionsSessionIdMemberPlayersRequestBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V132RequestCreatePlayerSessionPlayerEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEED2Ev(void* self) { + (void)self; + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V113PlayerSessionEEptEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120RequestPlayerSession20setInvitableUserTypeERKNS3_17InvitableUserTypeE() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V132RequestCreatePlayerSessionPlayerEEEEC1EPNS2_10LibContextE(IntrusivePtr* self, void* libContext) { + self->object = nullptr; + self->deleter = nullptr; + self->libContext = libContext; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V132RequestCreatePlayerSessionPlayerEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEEC1EPNS2_10LibContextE(IntrusivePtr* self, void* libContext) { + self->object = nullptr; + self->deleter = nullptr; + self->libContext = libContext; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V120RequestPlayerSessionEEEE8pushBackERKS8_(IntrusivePtr* self, const void* element) { + return VectorPushBackIntrusivePtr(self, static_cast(element)); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEE5beginEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V129PostPlayerSessionsRequestBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEEEEdeEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V115LocalizedStringEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V151PostPlayerSessionsSessionIdMemberPlayersRequestBodyEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x00); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi25joinPlayerSessionAsPlayerEiRKNS4_36ParameterToJoinPlayerSessionAsPlayerERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_52PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6String6appendEPKc(CommonString* self, const char* text) { + if (text == nullptr) return 0; + const std::size_t textLength = std::strlen(text); + const std::size_t oldLength = self->buffer != nullptr ? std::strlen(self->buffer) : 0; + const std::size_t needed = oldLength + textLength + 1; + if (needed > self->bufferSize) { + std::size_t capacity = self->bufferSize != 0 ? static_cast(self->bufferSize) : 16; + while (capacity < needed) capacity *= 2; + char* buffer = static_cast(std::malloc(capacity)); + if (buffer == nullptr) return -1; + if (oldLength != 0) std::memcpy(buffer, self->buffer, oldLength); + std::free(self->buffer); + self->buffer = buffer; + self->bufferSize = capacity; + } + std::memcpy(self->buffer + oldLength, text, textLength + 1); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V132RequestCreatePlayerSessionPlayerEEEE8pushBackERKS8_(IntrusivePtr* self, const void* element) { + return VectorPushBackIntrusivePtr(self, static_cast(element)); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15DefaultResponseC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayer9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessions10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_29PostPlayerSessionsRequestBodyEEE() { + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V110FromMemberEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V132RequestPlayerSessionMemberPlayerEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSessionC1Ev() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead12getSessionIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead7getFromEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V129PostPlayerSessionsRequestBodyEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x00); +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V130PostPlayerSessionsResponseBody17getPlayerSessionsEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi18leavePlayerSessionEiRKNS4_29ParameterToLeavePlayerSessionERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V124PlayerSessionPushContextEEEE8pushBackERKS8_(IntrusivePtr* self, const void* element) { + return VectorPushBackIntrusivePtr(self, static_cast(element)); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessionsD1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi27getPlayerSessionInvitationsEiRKNS4_38ParameterToGetPlayerSessionInvitationsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_54GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V132RequestPlayerSessionMemberPlayerEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V120RequestPlayerSessionEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V113PlayerSessionEEEEEEdeEv(const IntrusivePtr* self) { + return self->object; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V124PlayerSessionPushContextEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V110FromMemberEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEptEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitationsC1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEE8pushBackERKS8_(IntrusivePtr* self, const void* element) { + return VectorPushBackIntrusivePtr(self, static_cast(element)); +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEEppEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120RequestPlayerSession19setJoinableUserTypeERKNS3_16JoinableUserTypeE() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitationsD1Ev() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V113PlayerSessionEEEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V132RequestPlayerSessionMemberPlayerEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x00); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead14sessionIdIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead17invitationIdIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessionsC1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayer10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_51PostPlayerSessionsSessionIdMemberPlayersRequestBodyEEE() { + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBody16invitationsIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V113PlayerSessionEEEEixEm(void* self, std::uint64_t index) { + (void)self; + (void)index; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSession10initializeEPNS1_6Common10LibContextEPKcSA_() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V115LocalizedStringEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x30); +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEEdeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitations9setfieldsENS1_6Common12IntrusivePtrINS6_6VectorINS6_6StringEEEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15DefaultResponseD1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEEC1EPS6_PFvS8_EPNS2_10LibContextE(IntrusivePtr* self, void* body, void (APS5_VABI* deleter)(void*), void* libContext) { + self->object = body; + self->deleter = deleter; + self->libContext = libContext; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V120RequestPlayerSessionEEEEC1EPNS2_10LibContextE(IntrusivePtr* self, void* libContext) { + self->object = nullptr; + self->deleter = nullptr; + self->libContext = libContext; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V133RequestPlayerSessionPlayerFactory6createEPNS1_6Common10LibContextEPKcS9_PNS5_12IntrusivePtrINS3_26RequestPlayerSessionPlayerEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi20createPlayerSessionsEiRKNS4_31ParameterToCreatePlayerSessionsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_30PostPlayerSessionsResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayerC1Ev() { + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead9fromIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V126RequestPlayerSessionPlayer15setPushContextsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_24PlayerSessionPushContextEEEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V127RequestPlayerSessionFactory6createEPNS1_6Common10LibContextEiNS5_12IntrusivePtrINS3_32RequestPlayerSessionMemberPlayerEEERKNS5_6VectorINS5_6StringEEENS8_INS3_15LocalizedStringEEEPNS8_INS3_20RequestPlayerSessionEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEEC1EPNS2_10LibContextE(IntrusivePtr* self, void* libContext) { + self->object = nullptr; + self->deleter = nullptr; + self->libContext = libContext; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V124PlayerSessionPushContextEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V131PlayerSessionPushContextFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_24PlayerSessionPushContextEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V129PostPlayerSessionsRequestBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE8pushBackERKS4_(IntrusivePtr* self, const void* element) { + return VectorPushBackString(self, static_cast(element)); +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEEneERKS9_(const void* self, const void* other) { + (void)self; + (void)other; + NotImplemented_nid_no_patch(__func__); + return false; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitations10initializeEPNS1_6Common10LibContextEPKc() { + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBody14getInvitationsEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V132RequestCreatePlayerSessionPlayerEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead22receivedTimestampIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V120RequestPlayerSessionEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE11getResponseERS4_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V139RequestCreatePlayerSessionPlayerFactory6createEPNS1_6Common10LibContextEPKcS9_RKNS5_6VectorINS5_12IntrusivePtrINS3_24PlayerSessionPushContextEEEEEPNSB_INS3_32RequestCreatePlayerSessionPlayerEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V124PlayerSessionPushContextEEEEC1EPNS2_10LibContextE(IntrusivePtr* self, void* libContext) { + self->object = nullptr; + self->deleter = nullptr; + self->libContext = libContext; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V124PlayerSessionPushContextEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V113PlayerSession12getSessionIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V158PostPlayerSessionsSessionIdMemberPlayersRequestBodyFactory6createEPNS1_6Common10LibContextERKNS5_6VectorINS5_12IntrusivePtrINS3_26RequestPlayerSessionPlayerEEEEEPNS9_INS3_51PostPlayerSessionsSessionIdMemberPlayersRequestBodyEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V136PostPlayerSessionsRequestBodyFactory6createEPNS1_6Common10LibContextERKNS5_6VectorINS5_12IntrusivePtrINS3_20RequestPlayerSessionEEEEEPNS9_INS3_29PostPlayerSessionsRequestBodyEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120RequestPlayerSession28setExclusiveLeaderPrivilegesERKNS1_6Common6VectorINS5_6StringEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEED2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEED2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEC1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEdeEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEptEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V57ProductEEEEEEptEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V57ProductEEEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V53SkuEEEEEEptEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V53SkuEEEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_17TitleCloudStorage2V110DataStatusEEEEEEdeEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_17TitleCloudStorage2V110DataStatusEEEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEC1ERS7_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEptEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V59ContainerEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V57ProductEEC1ERS7_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V57ProductEEptEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V57ProductEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEptEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEC1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEptEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V15OwnerEEptEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V15OwnerEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEEneERKS9_(const void* self, const void* other) { + (void)self; + (void)other; + NotImplemented_nid_no_patch(__func__); + return false; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEEdeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEEppEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEEneERKS9_(const void* self, const void* other) { + (void)self; + (void)other; + NotImplemented_nid_no_patch(__func__); + return false; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEEdeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEEppEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERSB_(const void* self, void* response) { + (void)self; + (void)response; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_(const void* self, void* response) { + (void)self; + (void)response; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(void* self, void* context) { + (void)self; + (void)context; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(void* self, void* context) { + (void)self; + (void)context; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEEEixEm(void* self, std::uint64_t index) { + (void)self; + (void)index; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +std::size_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEEE4sizeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEE5beginEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEE3endEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +std::size_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V53SkuEEEE4sizeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEE5beginEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEE3endEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V59Container11getChildrenEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerC1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer10initializeEPNS1_6Common10LibContextE(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer13isInitializedEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer15setserviceLabelEi(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer9terminateEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi12getContainerEiRKNS4_23ParameterToGetContainerERNS1_6Common11TransactionINS8_12IntrusivePtrINS8_6VectorINSA_INS3_9ContainerEEEEEEENSA_INS8_18ResponseHeaderBaseEEEEE(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product8getLabelEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V57Product7getSkusEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product7getTypeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product9skusIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData11setobjectIdEPKc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserC1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser10initializeEPNS1_6Common10LibContextEPKcSA_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9setfieldsEPKc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9terminateEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi26getMultiDataStatusesByUserEiRKNS4_37ParameterToGetMultiDataStatusesByUserERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_32GetMultiDataStatusesResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::size_t APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus11getDataSizeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus11getObjectIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus8getOwnerEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus9getSlotIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V132GetMultiDataStatusesResponseBody17getDataStatusListEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +std::uint32_t APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V132GetMultiDataStatusesResponseBody23getTotalDataStatusCountEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::uint64_t APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V15Owner12getAccountIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi10getFriendsEiRKNS4_21ParameterToGetFriendsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_18GetFriendsResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends10initializeEPNS1_6Common10LibContextEPKc() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi17getBasicPresencesEiRKNS4_28ParameterToGetBasicPresencesERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_25GetBasicPresencesResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences10initializeEPNS1_6Common10LibContextEPKc() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V118GetFriendsResponse10getFriendsEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V124GetBlockingUsersResponse9getBlocksEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V125GetBasicPresencesResponse17getBasicPresencesEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi16getBlockingUsersEiRKNS4_27ParameterToGetBlockingUsersERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24GetBlockingUsersResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers10initializeEPNS1_6Common10LibContextE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi15filterProfanityEiRKNS4_26ParameterToFilterProfanityERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_23FilterProfanityResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity10initializeEPNS1_6Common10LibContextEPKcSA_NS6_12IntrusivePtrINS3_19WebApiFilterRequestEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V226WebApiFilterRequestFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_19WebApiFilterRequestEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi33getCommunicationRestrictionStatusEiRKNS4_44ParameterToGetCommunicationRestrictionStatusERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_38CommunicationRestrictionStatusResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus10initializeEPNS1_6Common10LibContextEm() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, ReferenceCountOffset); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, VectorReferenceCountOffset, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEED2Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorImED2Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common18ResponseHeaderBase14getHeaderValueEPKcRNS2_12IntrusivePtrINS2_6StringEEE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE3endEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE5beginEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorImE3endEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorImE5beginEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEEppEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorImEppEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V113BasicPresence12getAccountIdEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V113BasicPresence12getInContextEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi11UserProfile2V113BasicPresence15getOnlineStatusEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi15ProfanityFilter2V223FilterProfanityResponse10getMessageEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponse13getRestrictedEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEE3getEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEE3getEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEdeEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEdeEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEptEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE4sizeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorImE4sizeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEEdeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEEneERKS9_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorImEdeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorImEneERKS4_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +} diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index c40dbfd360..683dde7c84 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -23,8 +23,14 @@ static constexpr uint32_t SKU_FLAG_FULL = 3; namespace { +#ifdef _WIN32 +const wchar_t* ConfiguredEntitlements() { return _wgetenv(L"ANYPS5_ENTITLEMENTS"); } +#else +const char* ConfiguredEntitlements() { return std::getenv("ANYPS5_ENTITLEMENTS"); } +#endif + std::filesystem::path EntitlementsPath() { - if (const char* configured = std::getenv("ANYPS5_ENTITLEMENTS"); configured != nullptr && configured[0] != '\0') return configured; + if (const auto* configured = ConfiguredEntitlements(); configured != nullptr && configured[0] != 0) return configured; #ifdef _WIN32 wchar_t module[MAX_PATH]; const auto length = GetModuleFileNameW(nullptr, module, MAX_PATH); @@ -50,7 +56,7 @@ const std::vector& OwnedAddons() { const auto path = EntitlementsPath(); std::ifstream file(path); if (!file) { - if (std::getenv("ANYPS5_ENTITLEMENTS") != nullptr || std::filesystem::exists(path)) throw std::runtime_error("NpEntitlementAccess: cannot read " + path.string()); + if (ConfiguredEntitlements() != nullptr || std::filesystem::exists(path)) throw std::runtime_error("NpEntitlementAccess: cannot read " + path.string()); return addons; } std::string line; @@ -118,46 +124,46 @@ int APS5_VABI sceNpEntitlementAccessInitialize(const NpEntitlementAccessInitPara return 0; } -int APS5_VABI sceNpEntitlementAccessAbortRequest(void) { - return 0; +int APS5_VABI sceNpEntitlementAccessAbortRequest(std::int64_t requestId) { + throw std::logic_error("sceNpEntitlementAccessAbortRequest: asynchronous request tracking is unimplemented"); } -int APS5_VABI sceNpEntitlementAccessDeleteRequest(void) { - return 0; +int APS5_VABI sceNpEntitlementAccessDeleteRequest(std::int64_t requestId) { + throw std::logic_error("sceNpEntitlementAccessDeleteRequest: asynchronous request tracking is unimplemented"); } -int APS5_VABI sceNpEntitlementAccessGenerateTransactionId(void* transaction_id) { - if (!transaction_id) return SCE_NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER; +int APS5_VABI sceNpEntitlementAccessGenerateTransactionId(NpEntitlementAccessTransactionId* transactionId) { + if (!transactionId) return SCE_NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER; return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessPollConsumeEntitlement(void) { +int APS5_VABI sceNpEntitlementAccessPollConsumeEntitlement(std::int64_t requestId, std::int32_t* result, std::int32_t* countResult) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessRequestConsumeUnifiedEntitlement(void) { +int APS5_VABI sceNpEntitlementAccessRequestConsumeUnifiedEntitlement(std::int32_t userId, std::uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, const NpEntitlementAccessTransactionId* transactionId, std::int32_t count, std::int64_t* requestId) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessRequestConsumeServiceEntitlement(void) { +int APS5_VABI sceNpEntitlementAccessRequestConsumeServiceEntitlement(std::int32_t userId, std::uint32_t serviceLabel, const NpServiceEntitlementLabel* entitlementLabel, const NpEntitlementAccessTransactionId* transactionId, std::int32_t count, std::int64_t* requestId) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfo(void) { +int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfo(std::int64_t requestId, std::int32_t* result, NpEntitlementAccessServiceInfo* info) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfo(void) { +int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfo(std::int64_t requestId, std::int32_t* result, NpEntitlementAccessUnifiedInfo* info) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfo(void) { +int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfo(std::int32_t userId, std::uint32_t serviceLabel, const NpServiceEntitlementLabel* entitlementLabel, std::int64_t* requestId) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(void) { +int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(std::int32_t userId, std::uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, std::int64_t* requestId) { return SCE_NP_ERROR_SIGNED_OUT; } @@ -175,20 +181,40 @@ int APS5_VABI sceNpEntitlementAccessGetGameTrialsFlag(uint32_t* flag) { return 0; } -int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfoList() { +int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfoList(std::int64_t requestId, std::int32_t* result, NpEntitlementAccessServiceInfo* infos, std::uint32_t capacity, std::int32_t* firstResult, std::int32_t* secondResult, std::int32_t* thirdResult) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfoList() { +int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfoList(std::int64_t requestId, std::int32_t* result, NpEntitlementAccessUnifiedInfo* infos, std::uint32_t capacity, std::int32_t* firstResult, std::int32_t* secondResult, std::int32_t* thirdResult) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfoList() { +int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfoList(std::int32_t userId, std::uint32_t serviceLabel, const NpServiceEntitlementLabel* entitlementLabels, std::uint32_t labelCount, const NpEntitlementAccessListParameter* parameter, std::int64_t* requestId) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfoList() { +int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfoList(std::int32_t userId, std::uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabels, std::uint32_t labelCount, const NpEntitlementAccessListParameter* parameter, std::int64_t* requestId) { return SCE_NP_ERROR_SIGNED_OUT; } +int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfoIndividual() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessPollConsumableEntitlementInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessRequestConsumableEntitlementInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessRequestConsumeEntitlement() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpEulaDialog/Export.cpp b/core/libs/prx/libSceNpEulaDialog/Export.cpp new file mode 100644 index 0000000000..1b666cc199 --- /dev/null +++ b/core/libs/prx/libSceNpEulaDialog/Export.cpp @@ -0,0 +1,53 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpEulaDialogCheckVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEulaDialogCheckVersionAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEulaDialogClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEulaDialogGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEulaDialogGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEulaDialogInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEulaDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEulaDialogTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEulaDialogUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpFriendListDialog/CMakeLists.txt b/core/libs/prx/libSceNpFriendListDialog/CMakeLists.txt new file mode 100644 index 0000000000..219499b30b --- /dev/null +++ b/core/libs/prx/libSceNpFriendListDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceNpFriendListDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpFriendListDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpFriendListDialog) + +if(BUILD_TESTING) + add_test_executable(guest_np_friend_list_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpFriendListDialog.cpp) + target_include_directories(guest_np_friend_list_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_friend_list_dialog_tests PRIVATE libSceNpFriendListDialog) + add_test(NAME guest_np_friend_list_dialog COMMAND guest_np_friend_list_dialog_tests) + if(WIN32) + set_tests_properties(guest_np_friend_list_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpFriendListDialog/Export.cpp b/core/libs/prx/libSceNpFriendListDialog/Export.cpp new file mode 100644 index 0000000000..4ca7288353 --- /dev/null +++ b/core/libs/prx/libSceNpFriendListDialog/Export.cpp @@ -0,0 +1,81 @@ +#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 sceNpFriendListDialogInitialize(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 sceNpFriendListDialogOpenA(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 sceNpFriendListDialogUpdateStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceNpFriendListDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceNpFriendListDialogGetResultA(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 sceNpFriendListDialogClose(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 sceNpFriendListDialogTerminate(void) { + if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + return 0; +} + +int APS5_VABI sceNpFriendListDialogGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpFriendListDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpGameIntent/Export.cpp b/core/libs/prx/libSceNpGameIntent/Export.cpp index 4df4d87fd4..c8be4b9f09 100644 --- a/core/libs/prx/libSceNpGameIntent/Export.cpp +++ b/core/libs/prx/libSceNpGameIntent/Export.cpp @@ -12,6 +12,7 @@ constexpr int NpGameIntentErrorIntentNotFound = -2141898746; constexpr int NpGameIntentErrorValueNotFound = -2141898745; constexpr int NpGameIntentUserIdInvalid = -1; + } extern "C" { @@ -46,4 +47,80 @@ int APS5_VABI sceNpGameIntentTerminate(void) { return 0; } +int APS5_VABI sceNpGameIntentInitParamInit(NpGameIntentInitParam* param) { + if (param == nullptr) { + APS5_INVALID_ARG_EX; + } + std::memset(param, 0, sizeof(NpGameIntentInitParam)); + param->size = sizeof(NpGameIntentInitParam); + return 0; +} + +int APS5_VABI sceNpGameIntentInfoInit(NpGameIntentInfo* info) { + if (info == nullptr) { + APS5_INVALID_ARG_EX; + } + std::memset(info, 0, sizeof(NpGameIntentInfo)); + info->size = sizeof(NpGameIntentInfo); + info->user_id = NpGameIntentUserIdInvalid; + return 0; +} + +int APS5_VABI sceNpGameIntentVshInitialize(void) { + return 0; +} + +int APS5_VABI sceNpGameIntentVshTerminate(void) { + return 0; +} + +int APS5_VABI sceNpAppLauncherInitialize(const void* initParam) { + (void)initParam; + return 0; +} + +int APS5_VABI sceNpAppLauncherLaunchApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAppLauncherTerminate() { + return 0; +} + +int APS5_VABI sceNpGameIntentGetUdsNpComId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGameIntentLaunchApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGameIntentLaunchApp2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGameIntentNotifyAppLaunched() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionServiceGetJoinedPartySession() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionServiceGetRunningGamePlayerSession() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionServiceSendPlayerSessionInvitations() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpManager/CMakeLists.txt b/core/libs/prx/libSceNpManager/CMakeLists.txt index f0f629d89b..cd99c36b34 100644 --- a/core/libs/prx/libSceNpManager/CMakeLists.txt +++ b/core/libs/prx/libSceNpManager/CMakeLists.txt @@ -20,7 +20,8 @@ if(BUILD_TESTING) endif() add_test_executable(guest_np_manager_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpManager.cpp) target_include_directories(guest_np_manager_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_np_manager_tests PRIVATE libSceNpManager) + target_link_libraries(guest_np_manager_tests PRIVATE libSceNpManager libc) + configure_windows_unwind(guest_np_manager_tests) add_test(NAME guest_np_manager COMMAND guest_np_manager_tests) if(WIN32) set_tests_properties(guest_np_manager PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index a384c22023..15fd862b97 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -2,6 +2,8 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include #include #include #include @@ -12,10 +14,13 @@ static constexpr int SCE_NP_ERROR_SIGNED_OUT = static_cast(0x80550006); static constexpr int SCE_NP_ERROR_USER_NOT_FOUND = static_cast(0x80550007); static constexpr int SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED = static_cast(0x80550008); static constexpr int SCE_NP_ERROR_CALLBACK_NOT_REGISTERED = static_cast(0x80550009); +static constexpr int SCE_NP_ERROR_INVALID_SIZE = static_cast(0x80550011); static constexpr int SCE_NP_ERROR_REQUEST_NOT_FOUND = static_cast(0x80550014); +static constexpr int SCE_NP_ERROR_CALLBACK_MAX = static_cast(0x8055001D); static constexpr uint32_t NP_STATE_SIGNED_OUT = 1; static constexpr int NP_POLL_ASYNC_FINISHED = 0; static constexpr uint32_t NP_REACHABILITY_STATE_UNAVAILABLE = 0; +static constexpr uint32_t NP_GAME_PRESENCE_STATUS_OFFLINE = 0; static std::atomic g_nextRequest{1}; @@ -32,22 +37,666 @@ int AddRequest() { liveRequests.insert(reqId); return reqId; } + +int SignedOutRequest(int reqId) { + if (reqId == 0) return SCE_NP_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(requestMutex); + return liveRequests.count(reqId) ? SCE_NP_ERROR_SIGNED_OUT : SCE_NP_ERROR_REQUEST_NOT_FOUND; +} + +using NpStateCallbackA = void (APS5_VABI *)(int userId, uint32_t state, void* userdata); +using NpStateCallback = void (APS5_VABI *)(int userId, uint32_t state, NpId* npId, void* userdata); + +constexpr int NP_STATE_CALLBACK_MAX = 8; + +struct StateCallbackA { + NpStateCallbackA callback = nullptr; + void* userdata = nullptr; + bool pending = false; +}; + +struct StateCallback { + NpStateCallback callback = nullptr; + void* userdata = nullptr; + bool pending = false; +}; + +std::mutex stateMutex; +std::array stateCallbacksA; +StateCallback stateCallback; } extern "C" { +int APS5_VABI sceNpAsmAbort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientAbortRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientClearNpTitleToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientClearNpTitleTokenA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientCreateRequest2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientCreateResourceContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientCreateResourceContext2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientDeleteRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientDeleteResourceContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientDeleteResourceContext2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetAppId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetCacheControlMaxAge(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetGameNpTitleInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetGameNpTitleToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetGameTitleBanInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetNpComInfo2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetNpComInfo2A(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetNpComInfo3(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetNpComInfo4(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetNpTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetNpTitleToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetNpTitleToken2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetNpTitleTokenA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetRelatedGameNpTitleIds(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetRelatedGameNpTitleIdsA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetRelatedGameNpTitleIdsResult(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetServiceBaseUrl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetServiceBaseUrlA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetServiceBaseUrlWithNpTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetServiceBaseUrlWithNpTitleIdA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetServiceIdInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetServiceIdInfoA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientSetNpTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmCreateConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmCreateRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmDeleteConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmDeleteRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmGenerateNpTitleToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmGenerateNpTitleToken2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmGetNpCommInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmGetNpCommInfo2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmGetRelatedGameNpTitleIds(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmGetServiceBaseUrl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmGetServiceIdInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpCheckNpAvailabilityA(int reqId, int userId) { + if (userId == USER_SERVICE_USER_ID_INVALID) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SignedOutRequest(reqId); +} +int APS5_VABI sceNpGetAccountDateOfBirthA(int userId, void* dateOfBirth) { + if (!dateOfBirth || userId == USER_SERVICE_USER_ID_INVALID) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; +} +int APS5_VABI sceNpGetAccountLanguageA(int reqId, int userId, void* language) { + if (!language || userId == USER_SERVICE_USER_ID_INVALID) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SignedOutRequest(reqId); +} +int APS5_VABI sceNpGetGamePresenceStatus(const NpOnlineId* onlineId, uint32_t* status) { + if (!onlineId || !status) return SCE_NP_ERROR_INVALID_ARGUMENT; + *status = NP_GAME_PRESENCE_STATUS_OFFLINE; + return 0; +} +int APS5_VABI sceNpGetGamePresenceStatusA(int userId, uint32_t* status) { + if (!status || userId == USER_SERVICE_USER_ID_INVALID) return SCE_NP_ERROR_INVALID_ARGUMENT; + *status = NP_GAME_PRESENCE_STATUS_OFFLINE; + return 0; +} +int APS5_VABI sceNpGetParentalControlInfoA(int reqId, int userId, int8_t* age, void* info) { + if (!age || !info || userId == USER_SERVICE_USER_ID_INVALID) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SignedOutRequest(reqId); +} +int APS5_VABI sceNpIdMapperAbortRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIdMapperAccountIdToNpId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIdMapperAccountIdToOnlineId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIdMapperCreateRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIdMapperDeleteRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIdMapperNpIdToAccountId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIdMapperOnlineIdToAccountId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpInGameMessageAbortHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpInGameMessageCheckCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpInGameMessageGetMemoryPoolStatistics(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpInGameMessagePrepareA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpInGameMessageSendDataA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIntGetAppType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIntGetGamePresenceStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIntGetNpTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIntGetNpTitleIdSecret(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIntRegisterCheckCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIntRegisterGamePresenceCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIsPlusMember(int, bool* isMember) { + if (!isMember) return SCE_NP_ERROR_INVALID_ARGUMENT; + *isMember = false; + return 0; +} +int APS5_VABI sceNpManagerIntAbortRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntAddActiveSigninStateCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntAddOnlineIdChangeCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntAddPlusMemberTypeCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntAddSigninStateCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntAuthGetAuthorizationCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntAuthGetIdToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntCheckNpAvailability(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntCheckNpAvailabilityByPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntCheckNpState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntCheckNpStateA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntClearGameAccessToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntClearOnlineIdChangeFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntClearTicket(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntClearUsedFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntClearVshAccessToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntCreateAuthenticationTicket(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntCreateLoginContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntCreateLoginRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntCreateRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntDeleteLoginContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntDeleteRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccessTokenViaImplicitFlow(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountAge(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountCountry(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountCountryA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountCountrySdk(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountDateOfBirthSdk(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountIdSdk(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountLanguage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountLanguageA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountNpEnv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetActiveSigninState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAuthServerErrorFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAuthorizationCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAuthorizationCodeWithPsnoUri(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetClientCredentialAccessToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetCommunicationRestrictionStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetGameAccessToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetIssuerId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetNpEnv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetNpId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetNpIdByOnlineId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetNpIdSdk(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetOfflineAccountId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetOnlineId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetOnlineIdChangeFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetOnlineIdSdk(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetParentalControlFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetParentalControlInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetPlusMemberType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetPlusMemberTypeNB(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetServerError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetSigninState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetSigninTelemetryInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetTicket(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetTicketA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetTitleToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetTitleTokenWithCheck(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetUserIdByAccountId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetUserIdByNpId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetUserIdByOnlineId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetUserIdByOnlineIdSdk(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetUserList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetUserNum(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetVshAccessToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetVshAccessTokenWithCheck(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetVshClientId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntIsSubAccount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntIsTemporarySignout(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntIsUnregisteredClientError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntLoginAuthenticate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntLoginBind(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntLoginCreateAuthenticationTicket(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntLoginGetAccessToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceNpAbortRequest(int req_id) { (void)req_id; return 0; } int APS5_VABI sceNpCheckCallback(void) { + std::array callbacksA; + StateCallback legacy; + { + std::lock_guard lock(stateMutex); + callbacksA = stateCallbacksA; + legacy = stateCallback; + for (auto& entry : stateCallbacksA) entry.pending = false; + stateCallback.pending = false; + } + if (legacy.pending) legacy.callback(USER_SERVICE_INITIAL_USER_ID, NP_STATE_SIGNED_OUT, nullptr, legacy.userdata); + for (const auto& entry : callbacksA) { + if (entry.pending) entry.callback(USER_SERVICE_INITIAL_USER_ID, NP_STATE_SIGNED_OUT, entry.userdata); + } return 0; } -int APS5_VABI sceNpCheckNpAvailability(int req_id, const NpOnlineId* online_id) { - (void)req_id; - if (!online_id) return SCE_NP_ERROR_INVALID_ARGUMENT; +int APS5_VABI sceNpCheckNpAvailability(std::int32_t requestId, const NpOnlineId* onlineId, std::uint8_t* available) { + if (requestId == 0 || onlineId == nullptr) return SCE_NP_ERROR_INVALID_ARGUMENT; + if (available != nullptr) *available = 0; return SCE_NP_ERROR_USER_NOT_FOUND; } @@ -86,9 +735,62 @@ int APS5_VABI sceNpGetAccountAge(int req_id, int user_id, uint8_t* age) { return SCE_NP_ERROR_SIGNED_OUT; } +int APS5_VABI sceNpGetAccountCountry(const NpOnlineId* online_id, void* country_code) { + if (!online_id || !country_code) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_USER_NOT_FOUND; +} + +int APS5_VABI sceNpGetAccountDateOfBirth(const NpOnlineId* online_id, void* date_of_birth) { + if (!online_id || !date_of_birth) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_USER_NOT_FOUND; +} + +int APS5_VABI sceNpGetAccountLanguage(int req_id, const NpOnlineId* online_id, void* language) { + (void)req_id; + if (!online_id || !language) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_USER_NOT_FOUND; +} + +int APS5_VABI sceNpGetParentalControlInfo(int req_id, const NpOnlineId* online_id, int8_t* age, void* info) { + (void)req_id; + if (!online_id || !age || !info) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_USER_NOT_FOUND; +} + +int APS5_VABI sceNpGetUserIdByOnlineId(const NpOnlineId* online_id, int* user_id) { + if (!online_id || !user_id) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; +} + +int APS5_VABI sceNpCheckPlus(int req_id, const void* param, void* result) { + if (req_id == 0 || !param || !result) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; +} + +int APS5_VABI sceNpNotifyPlusFeature(const void* param) { + (void)param; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGetAccountId(const NpOnlineId* online_id, uint64_t* account_id) { + if (!online_id || !account_id) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; +} + +int APS5_VABI sceNpSetGamePresenceOnline(int req_id, const NpOnlineId* online_id, uint32_t presence) { + (void)presence; + if (req_id == 0 || !online_id) return SCE_NP_ERROR_INVALID_ARGUMENT; + { + std::lock_guard lock(requestMutex); + if (!liveRequests.count(req_id)) return SCE_NP_ERROR_REQUEST_NOT_FOUND; + } + return SCE_NP_ERROR_SIGNED_OUT; +} + int APS5_VABI sceNpGetAccountCountryA(int user_id, void* country_code) { (void)user_id; - (void)country_code; + if (!country_code) return SCE_NP_ERROR_INVALID_ARGUMENT; return SCE_NP_ERROR_SIGNED_OUT; } @@ -113,7 +815,7 @@ int APS5_VABI sceNpGetNpReachabilityState(int user_id, uint32_t* state) { int APS5_VABI sceNpGetOnlineId(int user_id, NpOnlineId* online_id) { (void)user_id; - (void)online_id; + if (!online_id) return SCE_NP_ERROR_INVALID_ARGUMENT; return SCE_NP_ERROR_SIGNED_OUT; } @@ -138,9 +840,8 @@ int APS5_VABI sceNpPollAsync(int req_id, int* result) { } void APS5_VABI sceNpRegisterGamePresenceCallback(void* callback, void* userdata) { - (void)callback; - (void)userdata; - NotImplemented_nid_no_patch(__func__); + (void)callback; + (void)userdata; } int APS5_VABI sceNpRegisterNpReachabilityStateCallback(void* callback, void* userdata) { @@ -172,16 +873,28 @@ int APS5_VABI sceNpRegisterPremiumEventCallback(void* callback, void* userdata) return 0; } -int APS5_VABI sceNpRegisterStateCallback(void* callback, void* userdata) { - (void)userdata; +int APS5_VABI sceNpRegisterStateCallback(NpStateCallback callback, void* userdata) { if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(stateMutex); + if (stateCallback.callback) return SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED; + stateCallback = {callback, userdata, true}; return 0; } int APS5_VABI sceNpSetContentRestriction(const NpContentRestriction* restriction) { - (void)restriction; - NotImplemented_nid_no_patch(__func__); - return 0; + struct Fields { + std::uint64_t size; + std::int8_t defaultAgeRestriction; + std::uint8_t padding[3]; + std::int32_t ageRestrictionCount; + const void* ageRestrictions; + }; + if (!restriction) return SCE_NP_ERROR_INVALID_ARGUMENT; + const auto* fields = reinterpret_cast(restriction); + if (fields->size != sizeof(Fields)) return SCE_NP_ERROR_INVALID_SIZE; + if (fields->defaultAgeRestriction < 0 || fields->ageRestrictionCount > 0x100) return SCE_NP_ERROR_INVALID_ARGUMENT; + if (fields->ageRestrictionCount > 0 && !fields->ageRestrictions) return SCE_NP_ERROR_INVALID_ARGUMENT; + return 0; } int APS5_VABI sceNpSetNpTitleId(const NpTitleId* title_id, const NpTitleSecret* title_secret) { @@ -208,6 +921,9 @@ int APS5_VABI sceNpSetTimeout(int reqId, int32_t resolveRetry, uint32_t resolveT } int APS5_VABI sceNpUnregisterStateCallback(void) { + std::lock_guard lock(stateMutex); + if (!stateCallback.callback) return SCE_NP_ERROR_CALLBACK_NOT_REGISTERED; + stateCallback = {}; return 0; } @@ -223,14 +939,26 @@ int APS5_VABI sceNpNotifyPremiumFeature(const void* param) { return 0; } -int APS5_VABI sceNpRegisterStateCallbackA(void* callback, void* userdata) { - (void)userdata; +int APS5_VABI sceNpRegisterStateCallbackA(NpStateCallbackA callback, void* userdata) { if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; - return 1; + std::lock_guard lock(stateMutex); + for (const auto& entry : stateCallbacksA) { + if (entry.callback == callback) return SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED; + } + for (int index = 0; index < NP_STATE_CALLBACK_MAX; ++index) { + if (stateCallbacksA[index].callback) continue; + stateCallbacksA[index] = {callback, userdata, true}; + return index + 1; + } + return SCE_NP_ERROR_CALLBACK_MAX; } -int APS5_VABI sceNpUnregisterStateCallbackA(int callback_id) { - (void)callback_id; +int APS5_VABI sceNpUnregisterStateCallbackA(int callbackId) { + if (callbackId < 1 || callbackId > NP_STATE_CALLBACK_MAX) return SCE_NP_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(stateMutex); + auto& entry = stateCallbacksA[callbackId - 1]; + if (!entry.callback) return SCE_NP_ERROR_CALLBACK_NOT_REGISTERED; + entry = {}; return 0; } @@ -238,15 +966,87 @@ int APS5_VABI sceNpUnregisterPremiumEventCallback(void) { return 0; } -int APS5_VABI sceNpGetUserIdByAccountId() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpUnregisterPlusEventCallback(void) { return 0; } +int APS5_VABI sceNpGetUserIdByAccountId(uint64_t account_id, int* user_id) { + if (account_id == 0 || !user_id) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; +} + int APS5_VABI sceNpGetSandboxInfo(void* sandbox_info) { (void)sandbox_info; NotImplemented_nid_no_patch(__func__); return 0; } +APS5_EXPORT("8weiQyIdI3A", sceNpUnknown_8weiQyIdI3A); +int APS5_VABI sceNpUnknown_8weiQyIdI3A(int userId, const NpOnlineId* 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; +} + + +int APS5_VABI sceNpCheckCallbackForLib() { + return sceNpCheckCallback(); +} + +} + +extern "C" { + +int APS5_VABI sceNpInGameMessageCreateHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessageDeleteHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessageInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessagePrepare() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessageSendData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessageTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookGetAccessToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpMatching2/Export.cpp b/core/libs/prx/libSceNpMatching2/Export.cpp new file mode 100644 index 0000000000..2746971c77 --- /dev/null +++ b/core/libs/prx/libSceNpMatching2/Export.cpp @@ -0,0 +1,151 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpMatching2ContextStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2ContextStop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2CreateContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2CreateJoinRoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2DestroyContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetRoomDataExternalList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetRoomDataInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetRoomMemberDataInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetServerId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetUserInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetWorldInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2Initialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2JoinRoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2KickoutRoomMember() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2LeaveRoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2RegisterContextCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2RegisterRoomEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2RegisterRoomMessageCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2RegisterSignalingCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SearchRoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SendRoomChatMessage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SendRoomMessage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetDefaultRequestOptParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetRoomDataExternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetRoomDataInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetRoomMemberDataInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetUserInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SignalingGetConnectionInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2Terminate() { + 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/libSceNpParty/Export.cpp b/core/libs/prx/libSceNpParty/Export.cpp new file mode 100644 index 0000000000..305da3f6cc --- /dev/null +++ b/core/libs/prx/libSceNpParty/Export.cpp @@ -0,0 +1,153 @@ +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int NP_PARTY_ERROR_ALREADY_INITIALIZED = static_cast(0x80552502u); +constexpr int NP_PARTY_ERROR_NOT_INITIALIZED = static_cast(0x80552503u); +constexpr int NP_PARTY_ERROR_INVALID_ARGUMENT = static_cast(0x80552504u); +constexpr int NP_PARTY_ERROR_NOT_IN_PARTY = static_cast(0x80552506u); +constexpr std::uint16_t NP_PARTY_STATE_NOT_IN_PARTY = 2; + +std::atomic g_initialized{false}; + +int notInParty(const void* out) { + if (!out) return NP_PARTY_ERROR_INVALID_ARGUMENT; + return NP_PARTY_ERROR_NOT_IN_PARTY; +} + +} + +extern "C" { + +int APS5_VABI sceNpPartyGetState(std::uint16_t* state) { + if (!state) return NP_PARTY_ERROR_INVALID_ARGUMENT; + *state = NP_PARTY_STATE_NOT_IN_PARTY; + return 0; +} + +int APS5_VABI sceNpPartyCheckCallback(void) { + if (!g_initialized) return NP_PARTY_ERROR_NOT_INITIALIZED; + return 0; +} + +int APS5_VABI sceNpPartyCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyCreateA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetId(void* id) { + return notInParty(id); +} + +int APS5_VABI sceNpPartyGetMemberInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetMemberInfoA(std::uint16_t, void* memberInfo) { + return notInParty(memberInfo); +} + +int APS5_VABI sceNpPartyGetMemberSessionInfo(std::uint16_t, void* memberGameSessionInfo) { + return notInParty(memberGameSessionInfo); +} + +int APS5_VABI sceNpPartyGetMemberVoiceInfo(std::uint16_t, std::uint16_t, std::uint8_t* memberVoiceState) { + return notInParty(memberVoiceState); +} + +int APS5_VABI sceNpPartyGetMembers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetMembersA(void* memberList) { + return notInParty(memberList); +} + +int APS5_VABI sceNpPartyGetStateAsUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetStateAsUserA(std::int32_t, std::uint16_t* state) { + return sceNpPartyGetState(state); +} + +int APS5_VABI sceNpPartyGetVoiceChatPriority(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyInitialize(const void* param) { + if (!param) return NP_PARTY_ERROR_INVALID_ARGUMENT; + bool expected = false; + if (!g_initialized.compare_exchange_strong(expected, true)) return NP_PARTY_ERROR_ALREADY_INITIALIZED; + return 0; +} + +int APS5_VABI sceNpPartyJoin(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyLeave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyRegisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyRegisterHandlerA(const void* handlers, void*) { + if (!g_initialized) return NP_PARTY_ERROR_NOT_INITIALIZED; + if (!handlers) return NP_PARTY_ERROR_INVALID_ARGUMENT; + return 0; +} + +int APS5_VABI sceNpPartyRegisterPrivateHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartySendBinaryMessage(const void* sendParams) { + return notInParty(sendParams); +} + +int APS5_VABI sceNpPartySetVoiceChatPriority(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyShowInvitationList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyShowInvitationListA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyTerminate(void) { + if (!g_initialized.exchange(false)) return NP_PARTY_ERROR_NOT_INITIALIZED; + return 0; +} + +int APS5_VABI sceNpPartyUnregisterPrivateHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpProfileDialog/Export.cpp b/core/libs/prx/libSceNpProfileDialog/Export.cpp new file mode 100644 index 0000000000..b0dd450371 --- /dev/null +++ b/core/libs/prx/libSceNpProfileDialog/Export.cpp @@ -0,0 +1,48 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpProfileDialogClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpProfileDialogGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpProfileDialogGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpProfileDialogInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpProfileDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpProfileDialogOpenA() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpProfileDialogTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpProfileDialogUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpRemotePlaySessionSignaling/Export.cpp b/core/libs/prx/libSceNpRemotePlaySessionSignaling/Export.cpp new file mode 100644 index 0000000000..766da74191 --- /dev/null +++ b/core/libs/prx/libSceNpRemotePlaySessionSignaling/Export.cpp @@ -0,0 +1,58 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpRemotePlaySessionSignalingCreateJoinSession() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpRemotePlaySessionSignalingGetConnectionStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpRemotePlaySessionSignalingGetCustomProperty() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpRemotePlaySessionSignalingGetLocalNetInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpRemotePlaySessionSignalingGetMemoryInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpRemotePlaySessionSignalingInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpRemotePlaySessionSignalingJoinSession() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpRemotePlaySessionSignalingLeaveSession() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpRemotePlaySessionSignalingSetCustomProperty() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpRemotePlaySessionSignalingTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpScoreRanking/Export.cpp b/core/libs/prx/libSceNpScoreRanking/Export.cpp new file mode 100644 index 0000000000..1bb8bc6df6 --- /dev/null +++ b/core/libs/prx/libSceNpScoreRanking/Export.cpp @@ -0,0 +1,81 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpScoreAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreCreateNpTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreDeleteNpTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetBoardInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetFriendsRanking() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetFriendsRankingAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetRankingByNpId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetRankingByNpIdAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetRankingByRange() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetRankingByRangeAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScorePollAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreRecordScore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreRecordScoreAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index d3086ee40a..79969f86ed 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -2,99 +2,327 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -#include +#include +#include // Peer-to-peer signaling needs the network: contexts exist, but sessions never activate. static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT = static_cast(0x80553303); static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_CONN_NOT_FOUND = static_cast(0x8055330C); static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE = static_cast(0x80552D06); -static std::atomic g_nextContext{1}; +static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED = static_cast(0x80553301); +static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_ALREADY_INITIALIZED = static_cast(0x80553302); +static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_LIMIT = static_cast(0x80553307); +static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND = static_cast(0x80553308); +static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_GROUP_NOT_FOUND = static_cast(0x8055330A); + +struct SessionSignalingContext { + std::uint32_t id; + std::int32_t userId; + bool active; +}; + +static std::mutex g_mutex; +static bool g_initialized = false; +static std::array g_contexts{}; + +struct SessionSignalingInitParam { + std::int32_t userId; + std::uint64_t poolSize; + std::uint64_t affinityMask; + std::int32_t threadPriority; + std::uint64_t stackSize; +}; + +struct SessionSignalingMemoryInfo { + std::uint64_t poolSize; + std::uint64_t usedSize; + std::uint64_t peakUsedSize; +}; + +struct SessionSignalingPeerAddress { + std::uint64_t accountId; + std::uint32_t platform; +}; + +struct SessionSignalingSessionParam { + std::uint32_t mode; + std::uint32_t option; +}; + +struct SessionSignalingNetAddress { + std::uint32_t address; + std::uint16_t port; +}; + +union SessionSignalingConnectionInfo { + std::uint32_t value; + SessionSignalingNetAddress netAddress; + SessionSignalingPeerAddress peerAddress; +}; + +struct SessionSignalingLocalNetInfo { + std::uint32_t localAddress; + std::uint32_t mappedAddress; + std::uint32_t mappedPort; + std::uint32_t mappingStatus; +}; + +using SessionSignalingEventCallback = void (APS5_VABI *)(std::uint32_t, std::uint32_t, std::int32_t, const SessionSignalingPeerAddress*, std::int32_t, void*); +using SessionSignalingConnectionCallback = void (APS5_VABI *)(std::uint32_t, std::uint32_t, std::int32_t, std::int32_t, void*); +using SessionSignalingConnectionCallback2 = void (APS5_VABI *)(std::uint32_t, std::uint32_t, std::uint32_t, std::int32_t, std::int32_t, void*); + +template +struct SessionSignalingContextParam { + std::int32_t userId; + std::uint32_t serviceLabel; + SessionSignalingEventCallback prepareCallback; + void* prepareUserData; + SessionSignalingEventCallback sessionCallback; + void* sessionUserData; + TConnectionCallback connectionCallback; + void* connectionUserData; +}; + +using SessionSignalingContextParam1 = SessionSignalingContextParam; +using SessionSignalingContextParam2 = SessionSignalingContextParam; + +struct SessionSignalingConnectionStatistics { + std::uint32_t peakConnectionCount; + std::uint32_t connectionCount; + std::uint32_t connectingCount; + std::uint32_t connectedCount; +}; + +struct SessionSignalingGroupInfo { + std::uint32_t connectionIds[64]; + std::uint64_t connectionCount; +}; + +static_assert(sizeof(SessionSignalingMemoryInfo) == 24); +static_assert(sizeof(SessionSignalingInitParam) == 40); +static_assert(offsetof(SessionSignalingInitParam, poolSize) == 8); +static_assert(offsetof(SessionSignalingInitParam, affinityMask) == 16); +static_assert(offsetof(SessionSignalingInitParam, threadPriority) == 24); +static_assert(offsetof(SessionSignalingInitParam, stackSize) == 32); +static_assert(offsetof(SessionSignalingMemoryInfo, usedSize) == 8); +static_assert(offsetof(SessionSignalingMemoryInfo, peakUsedSize) == 16); +static_assert(sizeof(SessionSignalingPeerAddress) == 16); +static_assert(sizeof(SessionSignalingSessionParam) == 8); +static_assert(offsetof(SessionSignalingSessionParam, option) == 4); +static_assert(sizeof(SessionSignalingNetAddress) == 8); +static_assert(offsetof(SessionSignalingNetAddress, port) == 4); +static_assert(sizeof(SessionSignalingConnectionInfo) == 16); +static_assert(sizeof(SessionSignalingLocalNetInfo) == 16); +static_assert(offsetof(SessionSignalingLocalNetInfo, mappedAddress) == 4); +static_assert(offsetof(SessionSignalingLocalNetInfo, mappedPort) == 8); +static_assert(offsetof(SessionSignalingLocalNetInfo, mappingStatus) == 12); +static_assert(sizeof(SessionSignalingContextParam1) == 56); +static_assert(sizeof(SessionSignalingContextParam2) == 56); +static_assert(offsetof(SessionSignalingContextParam1, prepareCallback) == 8); +static_assert(offsetof(SessionSignalingContextParam1, sessionCallback) == 24); +static_assert(offsetof(SessionSignalingContextParam1, connectionCallback) == 40); +static_assert(offsetof(SessionSignalingContextParam2, connectionCallback) == 40); +static_assert(offsetof(SessionSignalingContextParam2, connectionUserData) == 48); +static_assert(offsetof(SessionSignalingPeerAddress, platform) == 8); +static_assert(sizeof(SessionSignalingConnectionStatistics) == 16); +static_assert(offsetof(SessionSignalingConnectionStatistics, connectionCount) == 4); +static_assert(offsetof(SessionSignalingConnectionStatistics, connectingCount) == 8); +static_assert(offsetof(SessionSignalingConnectionStatistics, connectedCount) == 12); +static_assert(sizeof(SessionSignalingGroupInfo) == 264); +static_assert(offsetof(SessionSignalingGroupInfo, connectionCount) == 256); + +static SessionSignalingContext* FindContext(std::uint32_t contextId) { + auto& context = g_contexts[contextId % g_contexts.size()]; + return context.active && context.id == contextId ? &context : nullptr; +} + +template +static int CreateContext(const SessionSignalingContextParam* param, std::uint32_t* contextId) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; + if (!param || !contextId) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + for (const auto& context : g_contexts) { + if (context.active && context.userId == param->userId) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_LIMIT; + } + for (auto& context : g_contexts) { + if (!context.active) { + context.userId = param->userId; + context.active = true; + *contextId = context.id; + return 0; + } + } + return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_LIMIT; +} extern "C" { -int APS5_VABI sceNpSessionSignalingInitialize(void* param) { - (void)param; +int APS5_VABI sceNpSessionSignalingInitialize(const SessionSignalingInitParam* param) { + std::lock_guard lock(g_mutex); + if (g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_ALREADY_INITIALIZED; + if (!param || !param->poolSize) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + for (std::uint32_t index = 0; index < g_contexts.size(); ++index) { + g_contexts[index] = {1000 + index, 0, false}; + } + g_initialized = true; return 0; } -int APS5_VABI sceNpSessionSignalingActivateSession(void) { +int APS5_VABI sceNpSessionSignalingActivateSession(std::uint32_t contextId, const char* sessionId, std::uint32_t sessionType, const SessionSignalingSessionParam* param, std::uint32_t* groupId) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; + if (!sessionId || !param || !groupId) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + if (!FindContext(contextId)) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND; + if ((param->mode == 1 && param->option != 0) || (param->mode == 2 && param->option == 0)) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; } -int APS5_VABI sceNpSessionSignalingCreateContext2(const void* param, uint32_t* context_id) { - (void)param; - if (!context_id) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; - *context_id = g_nextContext.fetch_add(1, std::memory_order_relaxed); +int APS5_VABI sceNpSessionSignalingCreateContext2(const SessionSignalingContextParam2* param, std::uint32_t* contextId) { + return CreateContext(param, contextId); +} + +int APS5_VABI sceNpSessionSignalingCreateContext(const SessionSignalingContextParam1* param, std::uint32_t* contextId) { + return CreateContext(param, contextId); +} + +int APS5_VABI sceNpSessionSignalingDeactivate(std::uint32_t contextId, std::uint32_t connectionId) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; + if (!FindContext(contextId)) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND; + return SCE_NP_SESSION_SIGNALING_ERROR_GROUP_NOT_FOUND; +} + +int APS5_VABI sceNpSessionSignalingDestroyContext(std::uint32_t contextId) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; + auto* context = FindContext(contextId); + if (!context) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND; + context->active = false; + const auto nextId = context->id + static_cast(g_contexts.size()); + context->id = nextId < 100001 ? nextId : 1000 + contextId % static_cast(g_contexts.size()); return 0; } -int APS5_VABI sceNpSessionSignalingCreateContext(const void* param, uint32_t* context_id) { - return sceNpSessionSignalingCreateContext2(param, context_id); +int APS5_VABI sceNpSessionSignalingGetConnectionInfo(std::uint32_t contextId, std::uint32_t connectionId, std::uint32_t code, SessionSignalingConnectionInfo* info) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; + if (!info) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + if (!FindContext(contextId)) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND; + return SCE_NP_SESSION_SIGNALING_ERROR_CONN_NOT_FOUND; } -int APS5_VABI sceNpSessionSignalingDeactivate(uint32_t context_id) { - (void)context_id; - return 0; -} - -int APS5_VABI sceNpSessionSignalingDestroyContext(uint32_t context_id) { - (void)context_id; - return 0; -} - -int APS5_VABI sceNpSessionSignalingGetConnectionInfo(void) { +int APS5_VABI sceNpSessionSignalingActivateUser(std::uint32_t contextId, const SessionSignalingPeerAddress* peerAddress, std::uint32_t* connectionId) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; + if (!peerAddress || !connectionId) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + if (!FindContext(contextId)) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND; return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; } -int APS5_VABI sceNpSessionSignalingActivateUser(void) { - return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; -} - -int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress2(void) { - return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; +int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress2(std::uint32_t contextId, std::uint32_t groupId, const SessionSignalingPeerAddress* peerAddress, std::uint32_t* connectionId) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; + if (!peerAddress || !connectionId) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + if (!FindContext(contextId)) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND; + return SCE_NP_SESSION_SIGNALING_ERROR_GROUP_NOT_FOUND; } int APS5_VABI sceNpSessionSignalingTerminate(void) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; + g_initialized = false; + for (auto& context : g_contexts) context.active = false; return 0; } -int32_t APS5_VABI sceNpSessionSignalingGetConnectionStatus(int32_t context_id, int32_t connection_id, int32_t* status, void* peer_address, uint16_t* peer_port) { - (void)context_id; - (void)connection_id; - (void)peer_address; - (void)peer_port; +int32_t APS5_VABI sceNpSessionSignalingGetConnectionStatus(std::uint32_t contextId, std::uint32_t connectionId, std::int32_t* status, std::uint32_t* peerAddress, std::uint16_t* peerPort) { + (void)connectionId; + (void)peerAddress; + (void)peerPort; + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; if (!status) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + if (!FindContext(contextId)) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND; return SCE_NP_SESSION_SIGNALING_ERROR_CONN_NOT_FOUND; } -int32_t APS5_VABI sceNpSessionSignalingGetLocalNetInfo(int32_t context_id, void* info) { - (void)context_id; +int32_t APS5_VABI sceNpSessionSignalingGetLocalNetInfo(std::uint32_t contextId, SessionSignalingLocalNetInfo* info) { + (void)contextId; + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; if (!info) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; } int APS5_VABI sceNpSessionSignalingRequestPrepare(uint32_t contextId, uint32_t* requestId) { - (void)contextId; + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED; if (!requestId) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + if (!FindContext(contextId)) return SCE_NP_SESSION_SIGNALING_ERROR_CONTEXT_NOT_FOUND; return SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE; } -int APS5_VABI sceNpSessionSignalingGetMemoryInfo(void) { +int APS5_VABI sceNpSessionSignalingGetMemoryInfo(SessionSignalingMemoryInfo* memoryInfo) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpSessionSignalingGetConnectionStatistics(void) { +int APS5_VABI sceNpSessionSignalingGetConnectionStatistics(SessionSignalingConnectionStatistics* statistics) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress(void) { +int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress(std::uint32_t contextId, const SessionSignalingPeerAddress* peerAddress, std::uint32_t* connectionId) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpSessionSignalingGetGroupFromSessionId(void) { +int APS5_VABI sceNpSessionSignalingGetGroupFromSessionId(std::uint32_t contextId, const char* sessionId, std::uint32_t* groupId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingGetGroupInfo(std::uint32_t contextId, std::uint32_t groupId, SessionSignalingGroupInfo* groupInfo) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress2(std::uint32_t contextId, std::uint32_t groupId, std::uint32_t address, std::uint16_t port, SessionSignalingGroupInfo* groupInfo) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingCreateJoinActivate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingGetCustomProperty() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingGetGroupFromPeerAddress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingInitializeWithPort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingJoinActivate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingSetCustomProperty() { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceNpSignaling/Export.cpp b/core/libs/prx/libSceNpSignaling/Export.cpp new file mode 100644 index 0000000000..8ab2a25170 --- /dev/null +++ b/core/libs/prx/libSceNpSignaling/Export.cpp @@ -0,0 +1,46 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpSignalingActivateConnection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingCreateContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingDeleteContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingGetConnectionInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingSetContextOption() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingTerminateConnection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpSns/Export.cpp b/core/libs/prx/libSceNpSns/Export.cpp new file mode 100644 index 0000000000..c6647c709c --- /dev/null +++ b/core/libs/prx/libSceNpSns/Export.cpp @@ -0,0 +1,113 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpSnsFacebookAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsIntAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsIntCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsIntDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsIntFbGetGameAccessToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsIntFbGetGameAccessTokenAllowed() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsIntFbGetSystemAccessToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsIntFbGetTitleInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsIntLinkedStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsIntTwGetSystemAccessToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsIntUnlink() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsIntYtGetAccessToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsIntYtRefreshMasterToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsTwitchAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsTwitchCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsTwitchDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsTwitchGetAccessToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsYouTubeAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsYouTubeCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsYouTubeDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsYouTubeGetAccessToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp b/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp new file mode 100644 index 0000000000..9a4fe0e8cd --- /dev/null +++ b/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp @@ -0,0 +1,43 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpSnsFacebookDialogClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpSnsYouTubeDialog/Export.cpp b/core/libs/prx/libSceNpSnsYouTubeDialog/Export.cpp new file mode 100644 index 0000000000..e1b07d4bfc --- /dev/null +++ b/core/libs/prx/libSceNpSnsYouTubeDialog/Export.cpp @@ -0,0 +1,43 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpSnsYouTubeDialogClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsYouTubeDialogGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsYouTubeDialogGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsYouTubeDialogInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsYouTubeDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsYouTubeDialogTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsYouTubeDialogUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpSpeechToText/Export.cpp b/core/libs/prx/libSceNpSpeechToText/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceNpSpeechToText/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceNpTcs/Export.cpp b/core/libs/prx/libSceNpTcs/Export.cpp new file mode 100644 index 0000000000..483b8fa844 --- /dev/null +++ b/core/libs/prx/libSceNpTcs/Export.cpp @@ -0,0 +1,23 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpTcsIntDownloadData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTcsIntUploadDataToNewSlotAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTcsIntUploadDataToNewSlotBegin() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpTextToSpeech/Export.cpp b/core/libs/prx/libSceNpTextToSpeech/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceNpTextToSpeech/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceNpTrophy/CMakeLists.txt b/core/libs/prx/libSceNpTrophy/CMakeLists.txt new file mode 100644 index 0000000000..7301fa20ab --- /dev/null +++ b/core/libs/prx/libSceNpTrophy/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceNpTrophy SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpTrophy PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpTrophy) + +if(BUILD_TESTING) + add_test_executable(guest_np_trophy_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpTrophy.cpp) + target_include_directories(guest_np_trophy_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_trophy_tests PRIVATE libSceNpTrophy) + configure_windows_unwind(guest_np_trophy_tests) + add_test(NAME guest_np_trophy COMMAND guest_np_trophy_tests) + set_tests_properties(guest_np_trophy PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_trophy PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpTrophy/Export.cpp b/core/libs/prx/libSceNpTrophy/Export.cpp new file mode 100644 index 0000000000..aad30b5af0 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy/Export.cpp @@ -0,0 +1,268 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int NP_TROPHY_ERROR_INVALID_ARGUMENT = static_cast(0x80551604u); +constexpr int NP_TROPHY_ERROR_INSUFFICIENT_BUFFER = static_cast(0x80551605u); +constexpr int NP_TROPHY_ERROR_INVALID_HANDLE = static_cast(0x80551608u); +constexpr int NP_TROPHY_ERROR_INVALID_CONTEXT = static_cast(0x80551609u); +constexpr int NP_TROPHY_ERROR_INVALID_TROPHY_ID = static_cast(0x8055160au); +constexpr int NP_TROPHY_ERROR_INVALID_GROUP_ID = static_cast(0x8055160bu); +constexpr int NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED = static_cast(0x8055160cu); +constexpr int NP_TROPHY_ERROR_NOT_REGISTERED = static_cast(0x8055160fu); +constexpr int NP_TROPHY_ERROR_ALREADY_REGISTERED = static_cast(0x80551610u); +constexpr int NP_TROPHY_ERROR_CONTEXT_ALREADY_EXISTS = static_cast(0x80551613u); +constexpr int NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND = static_cast(0x80551614u); +constexpr int NP_TROPHY_ERROR_INVALID_TRP_FILE_FORMAT = static_cast(0x80551616u); +constexpr int NP_TROPHY_ERROR_TROPHY_NOT_UNLOCKED = static_cast(0x8055161au); +constexpr int NP_TROPHY_ERROR_TITLE_CONF_NOT_INSTALLED = static_cast(0x80551626u); +constexpr int NP_TROPHY_INVALID_ID = -1; +constexpr int NP_TROPHY_NUM_MAX = 128; +constexpr std::uint32_t TrpMagic = 0xdca24d00u; +constexpr std::size_t TrpHeaderBytes = 0x60; +constexpr std::size_t TrpEntryBytes = 0x40; + +struct TrophyFlagArray { + std::uint32_t bits[NP_TROPHY_NUM_MAX / 32]; +}; + +struct TrpEntry { + std::uint64_t offset; + std::uint64_t size; +}; + +struct Context { + int userId; + std::uint32_t serviceLabel; + bool registered = false; + std::filesystem::path package; + std::map entries; + int trophies = 0; + std::filesystem::path state; + std::set unlocked; +}; + +std::mutex g_mutex; +std::map g_contexts; +std::set g_handles; +int g_nextContext = 1; +int g_nextHandle = 1; + +std::uint32_t BigEndian32(const std::uint8_t* bytes) { + return (std::uint32_t{bytes[0]} << 24) | (std::uint32_t{bytes[1]} << 16) | (std::uint32_t{bytes[2]} << 8) | bytes[3]; +} + +std::uint64_t BigEndian64(const std::uint8_t* bytes) { + return (std::uint64_t{BigEndian32(bytes)} << 32) | BigEndian32(bytes + 4); +} + +std::optional> ReadPackage(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) return std::nullopt; + std::array header{}; + if (!file.read(reinterpret_cast(header.data()), header.size()) || BigEndian32(header.data()) != TrpMagic) return std::map{}; + const auto count = BigEndian32(header.data() + 0x10); + if (BigEndian32(header.data() + 0x14) != TrpEntryBytes) return std::map{}; + std::map entries; + for (std::uint32_t i = 0; i < count; ++i) { + std::array entry{}; + if (!file.read(reinterpret_cast(entry.data()), entry.size())) return std::map{}; + const std::string name(reinterpret_cast(entry.data()), strnlen(reinterpret_cast(entry.data()), 32)); + entries[name] = {BigEndian64(entry.data() + 32), BigEndian64(entry.data() + 40)}; + } + return entries; +} + +void LoadState(Context& context) { + std::ifstream file(context.state); + int id = 0; + while (file >> id) { + if (id >= 0 && id < context.trophies) context.unlocked.insert(id); + } +} + +void SaveState(const Context& context) { + std::filesystem::create_directories(context.state.parent_path()); + const auto temporary = context.state.string() + ".tmp"; + { + std::ofstream file(temporary, std::ios::trunc); + for (const auto id : context.unlocked) file << id << '\n'; + } + std::filesystem::rename(temporary, context.state); +} + +int Lookup(int contextId, int handle, Context*& context) { + if (contextId == NP_TROPHY_INVALID_ID) return NP_TROPHY_ERROR_INVALID_CONTEXT; + if (handle == NP_TROPHY_INVALID_ID) return NP_TROPHY_ERROR_INVALID_HANDLE; + const auto found = g_contexts.find(contextId); + if (found == g_contexts.end()) return NP_TROPHY_ERROR_INVALID_CONTEXT; + if (!g_handles.contains(handle)) return NP_TROPHY_ERROR_INVALID_HANDLE; + context = &found->second; + return 0; +} + +int CopyIcon(const Context& context, const std::string& name, void* buffer, std::uint64_t* size) { + const auto entry = context.entries.find(name); + if (entry == context.entries.end()) return NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND; + if (buffer == nullptr) { + *size = entry->second.size; + return 0; + } + if (*size < entry->second.size) return NP_TROPHY_ERROR_INSUFFICIENT_BUFFER; + std::ifstream file(context.package, std::ios::binary); + file.seekg(static_cast(entry->second.offset)); + if (!file.read(static_cast(buffer), static_cast(entry->second.size))) return NP_TROPHY_ERROR_INVALID_TRP_FILE_FORMAT; + *size = entry->second.size; + return 0; +} + +} + +extern "C" { + +int APS5_VABI sceNpTrophyCreateContext(int* context, int userId, std::uint32_t serviceLabel, std::uint64_t options) { + if (!context || options != 0) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + for (const auto& [id, existing] : g_contexts) { + if (existing.userId == userId && existing.serviceLabel == serviceLabel) return NP_TROPHY_ERROR_CONTEXT_ALREADY_EXISTS; + } + const int id = g_nextContext++; + g_contexts[id] = Context{userId, serviceLabel}; + *context = id; + return 0; +} + +int APS5_VABI sceNpTrophyDestroyContext(int context) { + std::lock_guard lock(g_mutex); + return g_contexts.erase(context) == 1 ? 0 : NP_TROPHY_ERROR_INVALID_CONTEXT; +} + +int APS5_VABI sceNpTrophyCreateHandle(int* handle) { + if (!handle) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + const int id = g_nextHandle++; + g_handles.insert(id); + *handle = id; + return 0; +} + +int APS5_VABI sceNpTrophyDestroyHandle(int handle) { + std::lock_guard lock(g_mutex); + return g_handles.erase(handle) == 1 ? 0 : NP_TROPHY_ERROR_INVALID_HANDLE; +} + +int APS5_VABI sceNpTrophyRegisterContext(int contextId, int handle, std::uint64_t options) { + if (options != 0) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (context->registered) return NP_TROPHY_ERROR_ALREADY_REGISTERED; + char name[32]; + std::snprintf(name, sizeof(name), "trophy%02u.trp", context->serviceLabel); + context->package = ResolvePath_nid_no_patch((std::string("/app0/sce_sys/trophy/") + name).c_str()); + const auto entries = ReadPackage(context->package); + if (!entries) return NP_TROPHY_ERROR_TITLE_CONF_NOT_INSTALLED; + if (entries->empty()) return NP_TROPHY_ERROR_INVALID_TRP_FILE_FORMAT; + context->entries = *entries; + for (int id = 0; id < NP_TROPHY_NUM_MAX; ++id) { + char icon[16]; + std::snprintf(icon, sizeof(icon), "TROP%03d.PNG", id); + if (!context->entries.contains(icon)) break; + context->trophies = id + 1; + } + std::snprintf(name, sizeof(name), "trophy%02u.%d", context->serviceLabel, context->userId); + context->state = std::filesystem::path("_trophy") / name; + LoadState(*context); + context->registered = true; + return 0; +} + +int APS5_VABI sceNpTrophyGetTrophyUnlockState(int contextId, int handle, TrophyFlagArray* flags, std::uint32_t* count) { + if (!flags || !count) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + std::memset(flags, 0, sizeof(*flags)); + for (const auto id : context->unlocked) flags->bits[id >> 5] |= 1u << (id & 31); + *count = static_cast(context->trophies); + return 0; +} + +int APS5_VABI sceNpTrophyUnlockTrophy(int contextId, int handle, int trophyId, int* platinumId) { + if (!platinumId) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (trophyId < 0 || trophyId >= context->trophies) return NP_TROPHY_ERROR_INVALID_TROPHY_ID; + if (context->unlocked.contains(trophyId)) return NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED; + context->unlocked.insert(trophyId); + SaveState(*context); + *platinumId = NP_TROPHY_INVALID_ID; + return 0; +} + +int APS5_VABI sceNpTrophyGetGameIcon(int contextId, int handle, void* buffer, std::uint64_t* size) { + if (!size) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + return CopyIcon(*context, "ICON0.PNG", buffer, size); +} + +int APS5_VABI sceNpTrophyGetGroupIcon(int contextId, int handle, int groupId, void* buffer, std::uint64_t* size) { + if (!size) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (groupId < 0) return NP_TROPHY_ERROR_INVALID_GROUP_ID; + char icon[16]; + std::snprintf(icon, sizeof(icon), "GR%03d.PNG", groupId); + return CopyIcon(*context, icon, buffer, size); +} + +int APS5_VABI sceNpTrophyGetTrophyIcon(int contextId, int handle, int trophyId, void* buffer, std::uint64_t* size) { + if (!size) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (trophyId < 0 || trophyId >= context->trophies) return NP_TROPHY_ERROR_INVALID_TROPHY_ID; + if (!context->unlocked.contains(trophyId)) return NP_TROPHY_ERROR_TROPHY_NOT_UNLOCKED; + char icon[16]; + std::snprintf(icon, sizeof(icon), "TROP%03d.PNG", trophyId); + return CopyIcon(*context, icon, buffer, size); +} + +int APS5_VABI sceNpTrophyGetGameInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophyGetGroupInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophyGetTrophyInfo() { + 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/Export.cpp b/core/libs/prx/libSceNpTrophy2/Export.cpp new file mode 100644 index 0000000000..9befeb73c9 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/Export.cpp @@ -0,0 +1,123 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpTrophy2SystemAbortHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemBuildGroupIconUri(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemBuildRewardIconUri(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemBuildTrophyIconUri(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemCheckCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemCheckNetSyncTitles(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemCreateContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemCreateHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemDebugLockTrophy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemDebugUnlockTrophy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemDestroyContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemGetTrophyDetails(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemGetTrophyGroupConf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemGetTrophySetArray(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemGetTrophyTitleConf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemGetTrophyTitleDetails(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemGetTrpIconByUri(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemIsServerAvailable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemRegisterTitleSyncedCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemRegisterTitleUpdateCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemRemoveAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemRemoveTitleData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemRemoveUserData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp index 8102f3785b..58ec7f5e33 100644 --- a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp @@ -4,6 +4,7 @@ #include static constexpr int SCE_NP_TROPHY2_OK = 0; +static constexpr int SCE_NP_TROPHY2_ERROR_INVALID_TROPHY_ID = -2141898486; static constexpr int SCE_NP_TROPHY2_ERROR_ICON_FILE_NOT_FOUND = -2141898479; static constexpr int NP_TROPHY2_CONTEXT_DEFAULT = 1; @@ -29,7 +30,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..cc32a75483 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp @@ -1,39 +1,90 @@ +#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); - } - 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 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; } + const auto trophy = package->trophies.find(trophy_id); + if (trophy == package->trophies.end()) return SCE_NP_TROPHY2_ERROR_INVALID_TROPHY_ID; + if (details != nullptr) fillDetails(trophy->second, details); + if (data != nullptr) fillData(*owner, trophy->second, data); return SCE_NP_TROPHY2_OK; } @@ -41,28 +92,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/libSceNpTus/CMakeLists.txt b/core/libs/prx/libSceNpTus/CMakeLists.txt new file mode 100644 index 0000000000..6046093997 --- /dev/null +++ b/core/libs/prx/libSceNpTus/CMakeLists.txt @@ -0,0 +1,22 @@ +add_library(libSceNpTus SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpTus PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpTus) + +if(BUILD_TESTING) + add_test_executable(guest_np_tus_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpTus.cpp) + target_include_directories(guest_np_tus_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_tus_tests PRIVATE libSceNpTus) + configure_windows_unwind(guest_np_tus_tests) + add_test(NAME guest_np_tus COMMAND guest_np_tus_tests) + set_tests_properties(guest_np_tus PROPERTIES TIMEOUT 20) + add_test_executable(guest_np_tus_offline_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpTusOffline.cpp) + target_include_directories(guest_np_tus_offline_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_tus_offline_tests PRIVATE libSceNpTus) + configure_windows_unwind(guest_np_tus_offline_tests) + add_test(NAME guest_np_tus_offline COMMAND guest_np_tus_offline_tests) + set_tests_properties(guest_np_tus_offline PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_tus guest_np_tus_offline PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpTus/Export.cpp b/core/libs/prx/libSceNpTus/Export.cpp new file mode 100644 index 0000000000..da02e06801 --- /dev/null +++ b/core/libs/prx/libSceNpTus/Export.cpp @@ -0,0 +1,1049 @@ +#include +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +static constexpr int SCE_NP_ERROR_SIGNED_OUT = static_cast(0x80550006); +static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT = static_cast(0x80550704); +static constexpr int SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS = static_cast(0x80550706); +static constexpr int SCE_NP_COMMUNITY_ERROR_ABORTED = static_cast(0x80550707); +static constexpr int SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT = static_cast(0x8055070C); +static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_ID = static_cast(0x8055070E); +static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_TYPE = static_cast(0x80550711); +static constexpr int SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID = static_cast(0x80550718); +static constexpr int SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID = static_cast(0x80550719); +static constexpr int POLL_FINISHED = 0; +static constexpr int POLL_RUNNING = 1; +static constexpr std::size_t MAX_TITLE_CONTEXTS = 32; +static constexpr std::size_t MAX_REQUESTS = 256; +static constexpr int MAX_SLOTS = 64; +static constexpr int MAX_USERS = 101; +static constexpr int MAX_FRIENDS = 100; +static constexpr std::int32_t TSS_MAX_SLOT_ID = 15; +static constexpr std::uint64_t TSS_MAX_SIZE = 64 * 1024; +static constexpr std::int32_t OPETYPE_EQUAL = 1; +static constexpr std::int32_t OPETYPE_LESS_OR_EQUAL = 6; +static constexpr std::int32_t DATASTATUS_SORTTYPE_DESCENDING_DATE = 1; +static constexpr std::int32_t DATASTATUS_SORTTYPE_ASCENDING_DATE = 2; +static constexpr std::int32_t VARIABLE_SORTTYPE_DESCENDING_DATE = 1; +static constexpr std::int32_t VARIABLE_SORTTYPE_ASCENDING_VALUE = 4; + +namespace { +struct Request { + int titleCtxId = 0; + bool started = false; + bool aborted = false; + int result = 0; +}; + +std::mutex tusMutex; +std::unordered_set titleContexts; +std::unordered_map requests; +int nextTitleContext = 1; +int nextRequest = 1; + +int ValidateSlots(const std::int32_t* slotIds, int arrayLen, const void* option) { + if (!slotIds) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (arrayLen < 1 || option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + for (int index = 0; index < arrayLen; ++index) { + if (slotIds[index] < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return 0; +} + +int ValidateNonEmptySlots(const std::int32_t* slotIds, int arrayLen, const void* option) { + if (!slotIds || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + return ValidateSlots(slotIds, arrayLen, option); +} + +bool IsEmpty(const NpOnlineId* user) { + return !user->data[0]; +} + +int StartOffline(int reqId) { + std::lock_guard lock(tusMutex); + const auto request = requests.find(reqId); + if (request == requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + request->second.started = true; + request->second.result = request->second.aborted ? SCE_NP_COMMUNITY_ERROR_ABORTED : SCE_NP_ERROR_SIGNED_OUT; + return 0; +} + +int WaitOffline(int reqId, int* result) { + std::lock_guard lock(tusMutex); + const auto request = requests.find(reqId); + if (request == requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (!request->second.started) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (result) *result = request->second.result; + return 0; +} + +int Complete(int reqId, int asyncResult) { + if (asyncResult < 0) return asyncResult; + int result = 0; + if (const int error = WaitOffline(reqId, &result)) return error; + return result; +} + +constexpr std::uint32_t INVALID_SERVICE_LABEL = 0xFFFFFFFF; + +int CreateTitleContext() { + std::lock_guard lock(tusMutex); + if (titleContexts.size() >= MAX_TITLE_CONTEXTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS; + const int contextId = nextTitleContext++; + titleContexts.insert(contextId); + return contextId; +} +} + +extern "C" { + +int APS5_VABI sceNpTusCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId) { + (void)serviceLabel; + (void)userId; + return CreateTitleContext(); +} + +int APS5_VABI sceNpTusCreateRequest(int titleCtxId) { + std::lock_guard lock(tusMutex); + if (!titleContexts.contains(titleCtxId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (requests.size() >= MAX_REQUESTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS; + const int reqId = nextRequest++; + requests.emplace(reqId, Request{titleCtxId}); + return reqId; +} + +int APS5_VABI sceNpTusDeleteNpTitleCtx(int titleCtxId) { + std::lock_guard lock(tusMutex); + if (!titleContexts.erase(titleCtxId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + std::erase_if(requests, [titleCtxId](const auto& entry) { return entry.second.titleCtxId == titleCtxId; }); + return 0; +} + +int APS5_VABI sceNpTusDeleteRequest(int reqId) { + std::lock_guard lock(tusMutex); + return requests.erase(reqId) ? 0 : SCE_NP_COMMUNITY_ERROR_INVALID_ID; +} + +int APS5_VABI sceNpTusAbortRequest(int reqId) { + std::lock_guard lock(tusMutex); + const auto request = requests.find(reqId); + if (request == requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + request->second.aborted = true; + return 0; +} + +int APS5_VABI sceNpTusPollAsync(int reqId, int* result) { + std::lock_guard lock(tusMutex); + const auto request = requests.find(reqId); + if (request == requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (!request->second.started) return POLL_RUNNING; + if (result) *result = request->second.result; + return POLL_FINISHED; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)targetAccountId; + (void)variableArraySize; + if (!variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateSlots(slotIds, arrayLen, option)) return error; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusSetMultiSlotVariableAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option) { + (void)targetAccountId; + if (!variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateSlots(slotIds, arrayLen, option)) return error; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariableAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option) { + (void)targetAccountId; + if (const int error = ValidateSlots(slotIds, arrayLen, option)) return error; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusAddAndGetVariableAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + (void)targetAccountId; + (void)inVariable; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)outVariableSize; + if (!outVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0 || option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusSetThreadParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssCreateNpTitleCtx(std::uint32_t serviceLabel, const NpId* npId) { + if (!npId) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (serviceLabel == INVALID_SERVICE_LABEL) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return CreateTitleContext(); +} + +int APS5_VABI sceNpTssCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId) { + return sceNpTusCreateNpTitleCtxA(serviceLabel, userId); +} + +int APS5_VABI sceNpTusCreateTitleCtx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetTimeout(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusChangeModeForOtherSaveDataOwners(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusWaitAsync(int reqId, int* result) { + return WaitOffline(reqId, result); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotVariableAAsync(reqId, targetAccountId, slotIds, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)targetNpId; + (void)variableArraySize; + if (!variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateSlots(slotIds, arrayLen, option)) return error; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetVirtualUserId || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateNonEmptySlots(slotIds, arrayLen, option)) return error; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetVirtualUserId || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateSlots(slotIds, arrayLen, option)) return error; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotVariableAVUserAsync(reqId, targetVirtualUserId, slotIds, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)targetAccountId; + (void)variableArraySize; + if (!variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateSlots(slotIds, arrayLen, option)) return error; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSave(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotVariableForCrossSaveAsync(reqId, targetAccountId, slotIds, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetVirtualUserId || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateSlots(slotIds, arrayLen, option)) return error; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotVariableForCrossSaveVUserAsync(reqId, targetVirtualUserId, slotIds, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusSetMultiSlotVariableA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option) { + return Complete(reqId, sceNpTusSetMultiSlotVariableAAsync(reqId, targetAccountId, slotIds, variableArray, arrayLen, option)); +} + +int APS5_VABI sceNpTusSetMultiSlotVariableAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option) { + (void)targetNpId; + if (!variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateSlots(slotIds, arrayLen, option)) return error; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusSetMultiSlotVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option) { + if (!targetVirtualUserId || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateSlots(slotIds, arrayLen, option)) return error; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariableA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option) { + return Complete(reqId, sceNpTusDeleteMultiSlotVariableAAsync(reqId, targetAccountId, slotIds, arrayLen, option)); +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariableAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option) { + if (!targetNpId) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateNonEmptySlots(slotIds, arrayLen, option)) return error; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariable(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option) { + return Complete(reqId, sceNpTusDeleteMultiSlotVariableAsync(reqId, targetNpId, slotIds, arrayLen, option)); +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, int arrayLen, void* option) { + if (!targetVirtualUserId) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateNonEmptySlots(slotIds, arrayLen, option)) return error; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, int arrayLen, void* option) { + return Complete(reqId, sceNpTusDeleteMultiSlotVariableVUserAsync(reqId, targetVirtualUserId, slotIds, arrayLen, option)); +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetStorage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetStorageAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetSmallStorageAsync(int reqId, void* data, std::uint64_t maxSize, std::uint64_t* contentLength, void* option) { + (void)data; + (void)contentLength; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (maxSize > TSS_MAX_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusAddAndGetVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + (void)inVariable; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)outVariableSize; + if (!targetVirtualUserId || !outVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusAddAndGetVariableAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + (void)targetNpId; + (void)inVariable; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)outVariableSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!outVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + (void)targetAccountId; + (void)inVariable; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)outVariableSize; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!outVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + (void)inVariable; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)outVariableSize; + if (!targetVirtualUserId || !outVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusAddAndGetVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + (void)inVariable; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)outVariableSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!targetVirtualUserId || !outVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option) { + (void)targetAccountId; + if (!slotIds) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option) { + if (!targetNpId || !slotIds || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetDataAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + (void)targetAccountId; + (void)data; + (void)recvSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!dataStatus || dataStatusSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetDataAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + (void)data; + (void)recvSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!targetVirtualUserId || !dataStatus || dataStatusSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetDataAsync(int reqId, const void* targetNpId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + (void)targetNpId; + (void)data; + (void)recvSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!dataStatus || dataStatusSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetDataForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + (void)targetAccountId; + (void)data; + (void)recvSize; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!dataStatus || dataStatusSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetDataVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + (void)data; + (void)recvSize; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!targetVirtualUserId || !dataStatus || dataStatusSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetFriendsDataStatusAsync(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)includeSelf; + (void)statusArraySize; + if (!statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_FRIENDS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + if (sortType < DATASTATUS_SORTTYPE_DESCENDING_DATE || sortType > DATASTATUS_SORTTYPE_ASCENDING_DATE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetFriendsVariableAsync(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)includeSelf; + (void)variableArraySize; + if (!variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_FRIENDS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + if (sortType < VARIABLE_SORTTYPE_DESCENDING_DATE || sortType > VARIABLE_SORTTYPE_ASCENDING_VALUE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)targetAccountId; + (void)statusArraySize; + if (!slotIds || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserId || !slotIds || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetNpId || !slotIds || !statusArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)targetAccountId; + (void)statusArraySize; + if (!slotIds || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserId || !slotIds || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserId || !slotIds || !statusArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_SLOTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + if (std::any_of(slotIds, slotIds + arrayLen, [](std::int32_t id) { return id < 0; })) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetAccountIdArray || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserIdArray || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAsync(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetNpIdArray || !statusArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetAccountIdArray || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserIdArray || !statusArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + (void)statusArraySize; + if (!targetVirtualUserIdArray || !statusArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserVariableAAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetAccountIdArray || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetVirtualUserIdArray || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserVariableAsync(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetNpIdArray || !variableArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetAccountIdArray || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetVirtualUserIdArray || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || arrayLen < 1 || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiUserVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetVirtualUserIdArray || !variableArray || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayLen > MAX_USERS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + for (int index = 0; index < arrayLen; ++index) { + if (IsEmpty(&targetVirtualUserIdArray[index])) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusSetDataAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + (void)targetAccountId; + (void)info; + (void)infoStructSize; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!data || sendSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0 || sendSize > totalSize) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusSetDataAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + (void)info; + (void)infoStructSize; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!targetVirtualUserId || !data || sendSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0 || sendSize > totalSize) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusSetDataAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const void* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + (void)targetNpId; + (void)info; + (void)infoStructSize; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!data || sendSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0 || sendSize > totalSize) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusSetDataVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const void* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + (void)info; + (void)infoStructSize; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!targetVirtualUserId || !data || sendSize == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (slotId < 0 || sendSize > totalSize) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusTryAndSetVariableAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)targetAccountId; + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_TYPE; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusTryAndSetVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!targetVirtualUserId || !resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_TYPE; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusTryAndSetVariableAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!targetNpId || !resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)targetAccountId; + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_TYPE; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!targetVirtualUserId || !resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_TYPE; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusTryAndSetVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + (void)variable; + (void)compareValue; + (void)isLastChangedAuthor; + (void)isLastChangedDate; + (void)resultVariableSize; + if (!targetVirtualUserId || !resultVariable) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (option || slotId < 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (opeType < OPETYPE_EQUAL || opeType > OPETYPE_LESS_OR_EQUAL) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTssGetSmallStorage(int reqId, void* data, std::uint64_t maxSize, std::uint64_t* contentLength, void* option) { + return Complete(reqId, sceNpTssGetSmallStorageAsync(reqId, data, maxSize, contentLength, option)); +} + +int APS5_VABI sceNpTusAddAndGetVariable(int reqId, const void* targetNpId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableAsync(reqId, targetNpId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusAddAndGetVariableA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableAAsync(reqId, targetAccountId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusAddAndGetVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableAVUserAsync(reqId, targetVirtualUserId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableForCrossSaveAsync(reqId, targetAccountId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableForCrossSaveVUserAsync(reqId, targetVirtualUserId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusAddAndGetVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option) { + return Complete(reqId, sceNpTusAddAndGetVariableVUserAsync(reqId, targetVirtualUserId, slotId, inVariable, isLastChangedAuthor, isLastChangedDate, outVariable, outVariableSize, option)); +} + +int APS5_VABI sceNpTusDeleteMultiSlotData(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option) { + return Complete(reqId, sceNpTusDeleteMultiSlotDataAsync(reqId, targetNpId, slotIds, arrayLen, option)); +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option) { + return Complete(reqId, sceNpTusDeleteMultiSlotDataAAsync(reqId, targetAccountId, slotIds, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetDataA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + return Complete(reqId, sceNpTusGetDataAAsync(reqId, targetAccountId, slotId, dataStatus, dataStatusSize, data, recvSize, option)); +} + +int APS5_VABI sceNpTusGetDataAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + return Complete(reqId, sceNpTusGetDataAVUserAsync(reqId, targetVirtualUserId, slotId, dataStatus, dataStatusSize, data, recvSize, option)); +} + +int APS5_VABI sceNpTusGetDataForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option) { + return Complete(reqId, sceNpTusGetDataForCrossSaveAsync(reqId, targetAccountId, slotId, dataStatus, dataStatusSize, data, recvSize, option)); +} + +int APS5_VABI sceNpTusGetFriendsDataStatus(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetFriendsDataStatusAsync(reqId, slotId, includeSelf, sortType, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetFriendsVariable(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetFriendsVariableAsync(reqId, slotId, includeSelf, sortType, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatus(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusAsync(reqId, targetNpId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusAAsync(reqId, targetAccountId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusAVUserAsync(reqId, targetVirtualUserId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSave(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(reqId, targetAccountId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(reqId, targetVirtualUserId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotDataStatusVUserAsync(reqId, targetVirtualUserId, slotIds, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatus(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusAsync(reqId, targetNpIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusA(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusAAsync(reqId, targetAccountIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusAVUserAsync(reqId, targetVirtualUserIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSave(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusForCrossSaveAsync(reqId, targetAccountIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(reqId, targetVirtualUserIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserDataStatusVUserAsync(reqId, targetVirtualUserIdArray, slotId, statusArray, statusArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserVariable(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserVariableAsync(reqId, targetNpIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserVariableA(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserVariableAAsync(reqId, targetAccountIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserVariableAVUserAsync(reqId, targetVirtualUserIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(reqId, targetVirtualUserIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserVariableVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiUserVariableVUserAsync(reqId, targetVirtualUserIdArray, slotId, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusSetDataA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + return Complete(reqId, sceNpTusSetDataAAsync(reqId, targetAccountId, slotId, totalSize, sendSize, data, info, infoStructSize, isLastChangedAuthor, isLastChangedDate, option)); +} + +int APS5_VABI sceNpTusSetDataAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option) { + return Complete(reqId, sceNpTusSetDataAVUserAsync(reqId, targetVirtualUserId, slotId, totalSize, sendSize, data, info, infoStructSize, isLastChangedAuthor, isLastChangedDate, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariable(int reqId, const void* targetNpId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableAsync(reqId, targetNpId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariableA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableAAsync(reqId, targetAccountId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableAVUserAsync(reqId, targetVirtualUserId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableForCrossSaveAsync(reqId, targetAccountId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableForCrossSaveVUserAsync(reqId, targetVirtualUserId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); +} + +int APS5_VABI sceNpTusTryAndSetVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option) { + return Complete(reqId, sceNpTusTryAndSetVariableVUserAsync(reqId, targetVirtualUserId, slotId, opeType, variable, compareValue, isLastChangedAuthor, isLastChangedDate, resultVariable, resultVariableSize, option)); +} + +int APS5_VABI sceNpTssGetData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusCreateNpTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataVUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableVUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataVUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetMultiSlotVariable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetMultiSlotVariableVUser() { + NotImplemented_nid_no_patch(__func__); + 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/Export.cpp b/core/libs/prx/libSceNpUniversalDataSystem/Export.cpp new file mode 100644 index 0000000000..0231b0cf03 --- /dev/null +++ b/core/libs/prx/libSceNpUniversalDataSystem/Export.cpp @@ -0,0 +1,63 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpUniversalDataSystemIntAbortHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntCheckNetSyncTitles(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntCreateContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntCreateHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntDeleteAllData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntDeleteUserDataWithServerData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntDestroyContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntDestroyHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntNetSyncTitles(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpUniversalDataSystem/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..94767a7d35 --- /dev/null +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/UdsEvent.cpp @@ -0,0 +1,195 @@ +#include "prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" + +#include +#include +#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); + +template +void appendFormattedNumber(std::string& out, T value, int precision) { + char number[40]; + const auto result = std::to_chars(number, number + sizeof(number), value, std::chars_format::general, precision); + if (result.ec != std::errc{}) throw std::runtime_error("event property number formatting failed"); + out.append(number, static_cast(result.ptr - number)); +} + +void appendObject(std::string& out, const NpUniversalDataSystemEventPropertyObject& object) { + out += '{'; + bool first = true; + 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) { + appendFormattedNumber(out, held, 9); + } else if constexpr (std::is_same_v) { + appendFormattedNumber(out, held, 17); + } 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..73128d5ff0 --- /dev/null +++ b/core/libs/prx/libSceNpUniversalDataSystem/tests/Events.cpp @@ -0,0 +1,332 @@ +#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 +#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"); + } +} + +void testLocaleIndependentNumberFormatting() { + const char* previous = std::setlocale(LC_NUMERIC, nullptr); + check(previous != nullptr, "the current locale could not be read"); + const std::string saved = previous; + struct RestoreLocale { + std::string saved; + ~RestoreLocale() { std::setlocale(LC_NUMERIC, saved.c_str()); } + } restore{saved}; + bool commaDecimal = false; + for (const auto* name : {"French", "French_France.1252", "fr_FR.UTF-8", "de_DE.UTF-8"}) { + if (std::setlocale(LC_NUMERIC, name) != nullptr && std::string(std::localeconv()->decimal_point) == ",") { + commaDecimal = true; + break; + } + } + if (!commaDecimal) { +#ifdef _WIN32 + throw std::runtime_error("No Windows comma-decimal locale available for regression"); +#else + std::puts("Comma-decimal locale regression unavailable on this host"); + return; +#endif + } + NpUniversalDataSystemEvent event; + event.properties.Set("float", 0.1f); + event.properties.Set("double", 0.1); + check(Uds::Describe(event.properties) == R"({"float":0.100000001,"double":0.10000000000000001})", + "described event properties changed with the numeric locale"); + const auto converted = Uds::StatEvent(event); + check(converted.properties.size() == 2, "float and double event properties were lost"); + check(converted.properties[0].text == "0.100000001", "float property text changed with the numeric locale"); + check(converted.properties[1].text == "0.10000000000000001", "double property text changed with the numeric locale"); +} + +struct Unlocks { + int count = 0; +}; + +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(); + testLocaleIndependentNumberFormatting(); + 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/libSceNpUtility/Export.cpp b/core/libs/prx/libSceNpUtility/Export.cpp index 79f5389752..2247cdf62b 100644 --- a/core/libs/prx/libSceNpUtility/Export.cpp +++ b/core/libs/prx/libSceNpUtility/Export.cpp @@ -38,3 +38,82 @@ int APS5_VABI sceNpBandwidthTestShutdown(int context_id, void* result) { } } + +extern "C" { + +int APS5_VABI sceNpBandwidthTestInitStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupCreateTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupDeleteTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupNpId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterCensorComment() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterCreateAsyncRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterCreateTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterDeleteTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterPollAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterSanitizeComment() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpWebApi/CMakeLists.txt b/core/libs/prx/libSceNpWebApi/CMakeLists.txt new file mode 100644 index 0000000000..0e3e193000 --- /dev/null +++ b/core/libs/prx/libSceNpWebApi/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceNpWebApi SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpWebApi PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpWebApi) + +if(BUILD_TESTING) + add_test_executable(guest_np_web_api_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpWebApi.cpp) + target_include_directories(guest_np_web_api_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_web_api_tests PRIVATE libSceNpWebApi) + configure_windows_unwind(guest_np_web_api_tests) + add_test(NAME guest_np_web_api COMMAND guest_np_web_api_tests) + set_tests_properties(guest_np_web_api PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_web_api PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpWebApi/Export.cpp b/core/libs/prx/libSceNpWebApi/Export.cpp new file mode 100644 index 0000000000..1aece4e185 --- /dev/null +++ b/core/libs/prx/libSceNpWebApi/Export.cpp @@ -0,0 +1,181 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include + +static constexpr int SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT = static_cast(0x80552902); +static constexpr int SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND = static_cast(0x80552904); +static constexpr int SCE_NP_WEBAPI_ERROR_PUSH_EVENT_FILTER_NOT_FOUND = static_cast(0x8055290B); +static constexpr int SCE_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND = static_cast(0x8055290D); +static constexpr int SCE_NP_WEBAPI_ERROR_SERVICE_PUSH_EVENT_FILTER_NOT_FOUND = static_cast(0x8055290E); +static constexpr int SCE_NP_WEBAPI_ERROR_SIGNED_IN_USER_NOT_FOUND = static_cast(0x80552910); +static constexpr int SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_MAX = static_cast(0x8055291A); + +namespace { + +struct LibraryContext { + int libHttpCtxId; + std::size_t poolSize; + std::unordered_set handles; + std::unordered_set pushEventFilters; +}; + +std::mutex contextsMutex; +std::unordered_map contexts; +int lastContext = 0; +int lastObject = 0; + +int nextObject() { + lastObject = lastObject % 0xefffffff + 1; + return lastObject; +} + +} + +extern "C" { + +int APS5_VABI sceNpWebApiAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiCreateContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiCreateHandle(int libCtxId) { + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + const auto handle = nextObject(); + context->second.handles.insert(handle); + return handle; +} + +int APS5_VABI sceNpWebApiCreatePushEventFilter(int libCtxId, const void* filterParam, std::size_t filterParamNum) { + if (!filterParam || filterParamNum == 0) return SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + const auto filter = nextObject(); + context->second.pushEventFilters.insert(filter); + return filter; +} + +int APS5_VABI sceNpWebApiCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiCreateServicePushEventFilter(int libCtxId, int handleId, const char* npServiceName, std::uint32_t npServiceLabel, const void* filterParam, std::size_t filterParamNum) { + (void)npServiceLabel; + if (!npServiceName || !filterParam || filterParamNum == 0) return SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + if (!context->second.handles.contains(handleId)) return SCE_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND; + return SCE_NP_WEBAPI_ERROR_SIGNED_IN_USER_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiDeleteContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiDeleteHandle(int libCtxId, int handleId) { + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + return context->second.handles.erase(handleId) != 0 ? 0 : SCE_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiDeletePushEventFilter(int libCtxId, int filterId) { + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + return context->second.pushEventFilters.erase(filterId) != 0 ? 0 : SCE_NP_WEBAPI_ERROR_PUSH_EVENT_FILTER_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiDeleteServicePushEventFilter(int libCtxId, int filterId) { + (void)filterId; + std::lock_guard lock(contextsMutex); + if (!contexts.contains(libCtxId)) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + return SCE_NP_WEBAPI_ERROR_SERVICE_PUSH_EVENT_FILTER_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiGetHttpResponseHeaderValue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiGetHttpResponseHeaderValueLength() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiGetHttpStatusCode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiInitialize(int libHttpCtxId, std::size_t poolSize) { + std::lock_guard lock(contextsMutex); + for (int attempt = 0; attempt < 0x7fff; ++attempt) { + lastContext = lastContext % 0x7fff + 1; + if (contexts.contains(lastContext)) continue; + contexts[lastContext] = {libHttpCtxId, poolSize, {}, {}}; + return lastContext; + } + return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_MAX; +} + +int APS5_VABI sceNpWebApiReadData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiRegisterPushEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiRegisterServicePushEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiSendRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiTerminate(int libCtxId) { + std::lock_guard lock(contextsMutex); + return contexts.erase(libCtxId) != 0 ? 0 : SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiUnregisterPushEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiUnregisterServicePushEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiUtilityParseNpId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index f36474b481..daef37aa53 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -3,6 +3,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include +#include #include #include #include @@ -10,8 +11,16 @@ // The PSN web API has no service behind it: handles can be created, but requests fail. static constexpr int SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT = static_cast(0x80553402); static constexpr int SCE_NP_WEBAPI2_ERROR_UNAVAILABLE = static_cast(0x80553406); +static constexpr int SCE_NP_WEBAPI2_ERROR_INVALID_CONTEXT = static_cast(0x80553404); +static constexpr int SCE_NP_WEBAPI2_ERROR_INVALID_HANDLE = static_cast(0x8055340D); static std::atomic g_nextHandle{1}; +struct NpWebApi2PushContextId { + char value[37]; +}; + +static_assert(sizeof(NpWebApi2PushContextId) == 37); + struct NpWebApi2MemoryPoolStats { size_t pool_size; size_t max_inuse_size; @@ -19,9 +28,26 @@ struct NpWebApi2MemoryPoolStats { int32_t reserved; }; +static_assert(sizeof(NpWebApi2MemoryPoolStats) == 32); + static std::mutex g_poolsMutex; static std::unordered_map g_pools; +static int CreateLibraryContext(size_t poolSize) { + const int libCtxId = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_poolsMutex); + g_pools[libCtxId] = poolSize; + return libCtxId; +} + +struct PushHandle { + std::int32_t libraryContextId; + std::uint32_t timeout = 0; + bool aborted = false; +}; + +static std::unordered_map pushHandles; + extern "C" { int APS5_VABI sceNpWebApi2AbortRequest(int64_t request_id) { @@ -36,8 +62,9 @@ int APS5_VABI sceNpWebApi2AddHttpRequestHeader(int64_t request_id, const char* f return 0; } -int APS5_VABI sceNpWebApi2AddWebTraceTag() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpWebApi2AddWebTraceTag(std::int64_t requestId, const char* value) { + (void)requestId; + if (!value) return SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT; return 0; } @@ -88,18 +115,16 @@ int APS5_VABI sceNpWebApi2GetHttpResponseHeaderValueLength(int64_t request_id, c int APS5_VABI sceNpWebApi2Initialize(int lib_http_ctx_id, size_t pool_size) { (void)lib_http_ctx_id; - const int lib_ctx_id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); - std::lock_guard lock(g_poolsMutex); - g_pools[lib_ctx_id] = pool_size; - return lib_ctx_id; + return CreateLibraryContext(pool_size); } -int APS5_VABI sceNpWebApi2GetMemoryPoolStats(int lib_ctx_id, NpWebApi2MemoryPoolStats* stats) { - if (stats == nullptr) APS5_INVALID_ARG_EX; +int APS5_VABI sceNpWebApi2GetMemoryPoolStats(int libraryContextId, NpWebApi2MemoryPoolStats* stats) { std::lock_guard lock(g_poolsMutex); - const auto pool = g_pools.find(lib_ctx_id); - if (pool == g_pools.end()) throw std::invalid_argument("sceNpWebApi2GetMemoryPoolStats: unknown context"); - *stats = {pool->second, 0, 0, 0}; + const auto pool = g_pools.find(libraryContextId); + if (pool == g_pools.end()) return SCE_NP_WEBAPI2_ERROR_INVALID_CONTEXT; + if (!stats) return 0; + std::memset(stats, 0, sizeof(*stats)); + stats->pool_size = pool->second; return 0; } @@ -113,21 +138,29 @@ int APS5_VABI sceNpWebApi2PushEventCreateFilter(int lib_ctx_id, int handle_id, c return g_nextHandle.fetch_add(1, std::memory_order_relaxed); } -int APS5_VABI sceNpWebApi2PushEventCreateHandle(int lib_ctx_id) { - (void)lib_ctx_id; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); +int APS5_VABI sceNpWebApi2PushEventCreateHandle(int libraryContextId) { + std::lock_guard lock(g_poolsMutex); + if (!g_pools.contains(libraryContextId)) return SCE_NP_WEBAPI2_ERROR_INVALID_CONTEXT; + const int handleId = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + pushHandles.emplace(handleId, PushHandle{libraryContextId}); + return handleId; } -int APS5_VABI sceNpWebApi2PushEventAbortHandle(int libCtxId, int handleId) { - (void)libCtxId; - (void)handleId; - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpWebApi2PushEventAbortHandle(int libraryContextId, int handleId) { + std::lock_guard lock(g_poolsMutex); + if (!g_pools.contains(libraryContextId)) return SCE_NP_WEBAPI2_ERROR_INVALID_CONTEXT; + const auto handle = pushHandles.find(handleId); + if (handle == pushHandles.end() || handle->second.libraryContextId != libraryContextId) return SCE_NP_WEBAPI2_ERROR_INVALID_HANDLE; + handle->second.aborted = true; return 0; } -int APS5_VABI sceNpWebApi2PushEventDeleteHandle(int lib_ctx_id, int handle_id) { - (void)lib_ctx_id; - (void)handle_id; +int APS5_VABI sceNpWebApi2PushEventDeleteHandle(int libraryContextId, int handleId) { + std::lock_guard lock(g_poolsMutex); + if (!g_pools.contains(libraryContextId)) return SCE_NP_WEBAPI2_ERROR_INVALID_CONTEXT; + const auto handle = pushHandles.find(handleId); + if (handle == pushHandles.end() || handle->second.libraryContextId != libraryContextId) return SCE_NP_WEBAPI2_ERROR_INVALID_HANDLE; + pushHandles.erase(handle); return 0; } @@ -162,23 +195,25 @@ int APS5_VABI sceNpWebApi2SendRequest(int64_t request_id, const void* data, size int APS5_VABI sceNpWebApi2Terminate(int lib_ctx_id) { std::lock_guard lock(g_poolsMutex); + std::erase_if(pushHandles, [lib_ctx_id](const auto& item) { return item.second.libraryContextId == lib_ctx_id; }); g_pools.erase(lib_ctx_id); return 0; } -int APS5_VABI sceNpWebApi2PushEventCreatePushContext() { +int APS5_VABI sceNpWebApi2PushEventCreatePushContext(std::int32_t userContextId, NpWebApi2PushContextId* pushContextId) { return SCE_NP_WEBAPI2_ERROR_UNAVAILABLE; } -int APS5_VABI sceNpWebApi2PushEventDeleteFilter() { +int APS5_VABI sceNpWebApi2PushEventDeleteFilter(std::int32_t libraryContextId, std::int32_t filterId) { return 0; } -int APS5_VABI sceNpWebApi2PushEventSetHandleTimeout(int lib_ctx_id, int handle_id, uint32_t timeout) { - (void)handle_id; - (void)timeout; +int APS5_VABI sceNpWebApi2PushEventSetHandleTimeout(int libraryContextId, int handleId, std::uint32_t timeout) { std::lock_guard lock(g_poolsMutex); - if (!g_pools.contains(lib_ctx_id)) throw std::invalid_argument("sceNpWebApi2PushEventSetHandleTimeout: unknown context"); + if (!g_pools.contains(libraryContextId)) return SCE_NP_WEBAPI2_ERROR_INVALID_CONTEXT; + const auto handle = pushHandles.find(handleId); + if (handle == pushHandles.end() || handle->second.libraryContextId != libraryContextId) return SCE_NP_WEBAPI2_ERROR_INVALID_HANDLE; + handle->second.timeout = timeout; return 0; } @@ -186,15 +221,15 @@ int APS5_VABI sceNpWebApi2PushEventRegisterPushContextCallback() { return g_nextHandle.fetch_add(1, std::memory_order_relaxed); } -int APS5_VABI sceNpWebApi2PushEventStartPushContextCallback() { +int APS5_VABI sceNpWebApi2PushEventStartPushContextCallback(std::int32_t userContextId, const NpWebApi2PushContextId* pushContextId) { return 0; } -int APS5_VABI sceNpWebApi2PushEventUnregisterCallback() { +int APS5_VABI sceNpWebApi2PushEventUnregisterCallback(std::int32_t userContextId, std::int32_t callbackId) { return 0; } -int APS5_VABI sceNpWebApi2PushEventUnregisterPushContextCallback() { +int APS5_VABI sceNpWebApi2PushEventUnregisterPushContextCallback(std::int32_t userContextId, std::int32_t callbackId) { return 0; } @@ -204,4 +239,54 @@ int APS5_VABI sceNpWebApi2SetRequestTimeout(int64_t request_id, uint32_t timeout return 0; } +int APS5_VABI sceNpWebApi2AddMultipartPart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2CreateMultipartRequest(int user_ctx_id, const char* api_group, const char* path, const char* method, int64_t* request_id) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2InitializeForPresence(int lib_http_ctx_id, uint64_t pool_size) { + (void)lib_http_ctx_id; + return CreateLibraryContext(pool_size); +} + +int APS5_VABI sceNpWebApi2IntCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2IntInitialize(const NpWebApi2IntInitializeArgs* args) { + if (args == nullptr || args->struct_size != sizeof(NpWebApi2IntInitializeArgs)) return SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT; + return CreateLibraryContext(args->pool_size); +} + +int APS5_VABI sceNpWebApi2IntInitialize2(const NpWebApi2IntInitialize2Args* args) { + if (args == nullptr || args->struct_size != sizeof(NpWebApi2IntInitialize2Args)) return SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT; + return CreateLibraryContext(args->pool_size); +} + +int APS5_VABI sceNpWebApi2IntPushEventCreateCtxIndFilter() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2IntPushEventCreateDestPs4CtxIndFilter() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2SendMultipartRequest(int64_t request_id, int part_index, void* data, uint64_t data_size, void* resp_info_option) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2SetMultipartContentType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceOpusCeltDec/CMakeLists.txt b/core/libs/prx/libSceOpusCeltDec/CMakeLists.txt index 25870e93b9..32be01d2dd 100644 --- a/core/libs/prx/libSceOpusCeltDec/CMakeLists.txt +++ b/core/libs/prx/libSceOpusCeltDec/CMakeLists.txt @@ -1,6 +1,6 @@ add_library(libSceOpusCeltDec SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) target_include_directories(libSceOpusCeltDec PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceOpusCeltDec PRIVATE libc) +target_link_libraries(libSceOpusCeltDec PRIVATE ffmpeg) configure_windows_unwind(libSceOpusCeltDec) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceOpusCeltDec/Export.cpp b/core/libs/prx/libSceOpusCeltDec/Export.cpp index b70e7fe123..5f40c01b3c 100644 --- a/core/libs/prx/libSceOpusCeltDec/Export.cpp +++ b/core/libs/prx/libSceOpusCeltDec/Export.cpp @@ -1,34 +1,53 @@ -#include "prx/libc/include/General.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceOpusDec/include/OpusDecoder.hpp" #include +#include "prx/libc/include/General.hpp" + +namespace { + +OpusDecoderLibrary library("OpusCeltDec", true); + +} // namespace extern "C" { -int APS5_VABI sceOpusCeltDecInitialize(std::uint32_t*) { +int APS5_VABI sceOpusCeltDecInitialize(std::uint32_t* context) { return library.Initialize(context); } + +int APS5_VABI sceOpusCeltDecTerminate(const std::uint32_t* context) { return library.Terminate(context); } + +int APS5_VABI sceOpusCeltDecGetSize(int channels) { return library.GetSize(channels); } + +int APS5_VABI sceOpusCeltDecCreateEx(const std::uint32_t* context, void* state, int sampleRate, int channels) { + return library.CreateEx(context, state, sampleRate, channels); +} + +int APS5_VABI sceOpusCeltDecDecode(void* state, const std::uint8_t* data, int bytes, std::int16_t* pcm, int capacity) { + return library.Decode(state, data, bytes, pcm, capacity); +} + +int APS5_VABI sceOpusCeltDecDestroy(void* state) { return library.Destroy(state); } + +int APS5_VABI sceOpusCeltDecCreate() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltDecTerminate(std::uint32_t*) { +int APS5_VABI sceOpusCeltDecCtl() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltDecGetSize(int) { +int APS5_VABI sceOpusCeltDecDecodeFloat() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltDecCreateEx(std::uint32_t*, void*, int, int) { +int APS5_VABI sceOpusCeltDecDecodeFloatWithPriority() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltDecDecode(void*, const std::uint8_t*, int, std::int16_t*, int) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusCeltDecDestroy(void*) { +int APS5_VABI sceOpusCeltDecDecodeWithPriority() { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceOpusCeltEnc/Export.cpp b/core/libs/prx/libSceOpusCeltEnc/Export.cpp new file mode 100644 index 0000000000..bcbe6e9029 --- /dev/null +++ b/core/libs/prx/libSceOpusCeltEnc/Export.cpp @@ -0,0 +1,65 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +struct OpusCeltEncoder; + +extern "C" { + +int APS5_VABI sceOpusCeltEncCreate(OpusCeltEncoder* encoder, std::int32_t sampleRate, std::int32_t channels, std::int32_t application) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncCreateEx(const std::uint32_t* contextId, OpusCeltEncoder* encoder, std::int32_t sampleRate, std::int32_t channels, std::int32_t application) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncCtl(OpusCeltEncoder* encoder, std::int32_t request, ...) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncDestroy(OpusCeltEncoder* encoder) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncEncode(OpusCeltEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncEncodeFloat(OpusCeltEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncEncodeFloatWithPriority(OpusCeltEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncEncodeWithPriority(OpusCeltEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncGetSize(std::int32_t channels) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncInitialize(std::uint32_t* contextId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusCeltEncTerminate(const std::uint32_t* contextId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceOpusDec/Export.cpp b/core/libs/prx/libSceOpusDec/Export.cpp index 383c6e3117..ebf78206f4 100644 --- a/core/libs/prx/libSceOpusDec/Export.cpp +++ b/core/libs/prx/libSceOpusDec/Export.cpp @@ -1,144 +1,69 @@ #include "prx/libc/include/general/VabiMacros.hpp" -#include -#include +#include "prx/libSceOpusDec/include/OpusDecoder.hpp" #include -#include -#include -#include -#include -#include -#include -#include - -extern "C" { -#include -} +#include "prx/libc/include/General.hpp" namespace { -struct Decoder { - std::uint32_t context; - int channels; - AVCodecContext* codec = nullptr; - std::vector pending; - ~Decoder() { avcodec_free_context(&codec); } -}; - -std::mutex mutex; -std::unordered_map contexts; -std::unordered_map> decoders; -std::uint32_t nextContext = 1; - -int PacketSamples(const std::uint8_t* packet, int size) { - const unsigned config = packet[0] >> 3; - const int frameSamples = config < 12 ? ((config & 3) == 3 ? 2880 : 480 << (config & 3)) - : config < 16 ? 480 << (config & 1) : 120 << (config & 3); - int frames = 1; - switch (packet[0] & 3) { - case 1: - case 2: frames = 2; break; - case 3: - if (size < 2) throw std::runtime_error("OpusDec: truncated packet frame count"); - frames = packet[1] & 63; - break; - } - if (frames == 0 || frames * frameSamples > 5760) - throw std::runtime_error("OpusDec: invalid packet duration"); - return frames * frameSamples; -} +OpusDecoderLibrary library("OpusDec", false); } // namespace extern "C" { -int APS5_VABI sceOpusDecInitialize(std::uint32_t* context) { - if (!context) throw std::runtime_error("OpusDec: null context output"); - std::lock_guard lock(mutex); - for (const auto& [id, owner] : contexts) - if (owner == context) throw std::runtime_error("OpusDec: context already initialized"); - if (nextContext == 0) throw std::runtime_error("OpusDec: context identifiers exhausted"); - const auto id = nextContext++; - contexts.emplace(id, context); - *context = id; - return 0; -} +int APS5_VABI sceOpusDecInitialize(std::uint32_t* context) { return library.Initialize(context); } -int APS5_VABI sceOpusDecTerminate(std::uint32_t* context) { - if (!context) throw std::runtime_error("OpusDec: null context"); - std::lock_guard lock(mutex); - if (!contexts.contains(*context)) throw std::runtime_error("OpusDec: unknown context"); - for (const auto& [state, decoder] : decoders) - if (decoder->context == *context) throw std::runtime_error("OpusDec: termination with live decoders is not implemented"); - contexts.erase(*context); - return 0; -} +int APS5_VABI sceOpusDecTerminate(const std::uint32_t* context) { return library.Terminate(context); } -int APS5_VABI sceOpusDecGetSize(int channels) { - if (channels != 1 && channels != 2) throw std::runtime_error("OpusDec: only mono and stereo are supported"); - return 640; -} +int APS5_VABI sceOpusDecGetSize(int channels) { return library.GetSize(channels); } -int APS5_VABI sceOpusDecCreateEx(std::uint32_t* context, void* state, int sampleRate, int channels) { - if (!context || !state) throw std::runtime_error("OpusDec: null context or state"); - sceOpusDecGetSize(channels); - if (sampleRate != 48000) throw std::runtime_error("OpusDec: sample rates other than 48000 Hz are not implemented"); - std::lock_guard lock(mutex); - if (!contexts.contains(*context)) throw std::runtime_error("OpusDec: unknown context"); - if (decoders.contains(state)) throw std::runtime_error("OpusDec: state already owns a decoder"); - auto decoder = std::make_unique(); - decoder->context = *context; - decoder->channels = channels; - const auto* codec = avcodec_find_decoder(AV_CODEC_ID_OPUS); - if (!codec) throw std::runtime_error("OpusDec: FFmpeg Opus decoder unavailable"); - decoder->codec = avcodec_alloc_context3(codec); - if (!decoder->codec) throw std::bad_alloc(); - av_channel_layout_default(&decoder->codec->ch_layout, channels); - decoder->codec->sample_rate = 48000; - if (avcodec_open2(decoder->codec, codec, nullptr) < 0) - throw std::runtime_error("OpusDec: cannot open FFmpeg decoder"); - decoders.emplace(state, std::move(decoder)); - return 0; +int APS5_VABI sceOpusDecCreateEx(const std::uint32_t* context, void* state, int sampleRate, int channels) { + return library.CreateEx(context, state, sampleRate, channels); } int APS5_VABI sceOpusDecDecode(void* state, const std::uint8_t* data, int bytes, std::int16_t* pcm, int capacity) { - if (!data || bytes <= 0 || bytes > std::numeric_limits::max() - AV_INPUT_BUFFER_PADDING_SIZE || !pcm || capacity < 0) - throw std::runtime_error("OpusDec: invalid packet or PCM buffer; packet loss concealment is not implemented"); - std::lock_guard lock(mutex); - const auto found = decoders.find(state); - if (found == decoders.end()) throw std::runtime_error("OpusDec: unknown decoder state"); - auto& decoder = *found->second; - const int samples = PacketSamples(data, bytes); - const int outputBytes = samples * decoder.channels * 2; - if (capacity / 2 * 2 < outputBytes) throw std::runtime_error("OpusDec: PCM buffer is too small"); - std::unique_ptr packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); }); - std::unique_ptr frame(av_frame_alloc(), [](AVFrame* f) { av_frame_free(&f); }); - if (!packet || !frame || av_new_packet(packet.get(), bytes) < 0) throw std::bad_alloc(); - std::memcpy(packet->data, data, bytes); - if (avcodec_send_packet(decoder.codec, packet.get()) < 0 || avcodec_receive_frame(decoder.codec, frame.get()) < 0) - throw std::runtime_error("OpusDec: FFmpeg rejected the packet"); - const auto format = static_cast(frame->format); - if (frame->ch_layout.nb_channels != decoder.channels || frame->sample_rate != 48000 || - (format != AV_SAMPLE_FMT_FLTP && format != AV_SAMPLE_FMT_FLT)) - throw std::runtime_error("OpusDec: unexpected FFmpeg PCM format"); - for (int sample = 0; sample < 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]); - } - } - const auto count = static_cast(samples) * decoder.channels; - if (decoder.pending.size() < count) decoder.pending.insert(decoder.pending.begin(), count - decoder.pending.size(), 0.0f); - for (std::size_t index = 0; index < count; ++index) { - const auto converted = static_cast(std::lrint(std::clamp(decoder.pending[index] * 32768.0f, -32768.0f, 32767.0f))); - std::memcpy(reinterpret_cast(pcm) + index * 2, &converted, 2); - } - decoder.pending.erase(decoder.pending.begin(), decoder.pending.begin() + static_cast(count)); - return outputBytes; + return library.Decode(state, data, bytes, pcm, capacity); } -int APS5_VABI sceOpusDecDestroy(void* state) { - std::lock_guard lock(mutex); - if (decoders.erase(state) == 0) throw std::runtime_error("OpusDec: unknown decoder state"); +int APS5_VABI sceOpusDecDestroy(void* state) { return library.Destroy(state); } + +int APS5_VABI sceOpusDecCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecCtl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecDecodeFloat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecDecodeFloatWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecDecodeFloatWithPriorityAndTimeout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecDecodeWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusDecDecodeWithPriorityAndTimeout() { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp b/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp new file mode 100644 index 0000000000..fee4c48957 --- /dev/null +++ b/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp @@ -0,0 +1,197 @@ +#ifndef CORE_LIBS_PRX_LIBSCEOPUSDEC_INCLUDE_OPUSDECODER_HPP +#define CORE_LIBS_PRX_LIBSCEOPUSDEC_INCLUDE_OPUSDECODER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +} + +namespace { + +class OpusDecoderLibrary { +public: + OpusDecoderLibrary(const char* libraryName, bool celtPacketsOnly) : name(libraryName), celtOnly(celtPacketsOnly) {} + + int Initialize(std::uint32_t* context) { + if (!context) return error(13); + std::lock_guard lock(mutex); + for (const auto& [id, owner] : contexts) + if (owner == context) fail("context already initialized"); + if (nextContext == 0) fail("context identifiers exhausted"); + const auto id = nextContext++; + contexts.emplace(id, context); + *context = id; + return 0; + } + + int Terminate(const std::uint32_t* context) { + if (!context) fail("null context"); + std::lock_guard lock(mutex); + if (!contexts.contains(*context)) fail("unknown context"); + for (const auto& [state, decoder] : decoders) + if (decoder->context == *context) fail("termination with live decoders is not implemented"); + contexts.erase(*context); + return 0; + } + + int GetSize(int channels) { + if (channels != 1 && channels != 2) return error(3); + return 640; + } + + int CreateEx(const std::uint32_t* context, void* state, int sampleRate, int channels) { + if (!context || !state) return error(0); + if (celtOnly && channels != 1 && channels != 2) return error(3); + if (sampleRate != 8000 && sampleRate != 12000 && sampleRate != 16000 && sampleRate != 24000 && sampleRate != 48000) return error(2); + if (channels != 1 && channels != 2) return error(3); + 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); + if (!decoder->codec) throw std::bad_alloc(); + 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; + } + + int Decode(void* state, const std::uint8_t* data, int bytes, std::int16_t* pcm, int capacity) { + if (!state) return error(0); + if (bytes != 0 && !data) return error(6); + if (bytes < 0) return error(7); + if (!pcm) return error(8); + if (capacity < 0) return error(9); + std::lock_guard lock(mutex); + const auto found = decoders.find(state); + if (found == decoders.end()) return error(1); + if (bytes == 0 || bytes > std::numeric_limits::max() - AV_INPUT_BUFFER_PADDING_SIZE) fail("packet loss concealment or oversized packet is not implemented"); + auto& decoder = *found->second; + if (celtOnly && (data[0] >> 3) < 16) fail("SILK and hybrid packets are not implemented"); + 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); }); + std::unique_ptr frame(av_frame_alloc(), [](AVFrame* f) { av_frame_free(&f); }); + if (!packet || !frame || av_new_packet(packet.get(), bytes) < 0) throw std::bad_alloc(); + std::memcpy(packet->data, data, bytes); + if (avcodec_send_packet(decoder.codec, packet.get()) < 0 || avcodec_receive_frame(decoder.codec, frame.get()) < 0) + fail("FFmpeg rejected the packet"); + const auto format = static_cast(frame->format); + 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]); + 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) { + const auto converted = static_cast(std::lrint(std::clamp(decoder.pending[index] * 32768.0f, -32768.0f, 32767.0f))); + std::memcpy(reinterpret_cast(pcm) + index * 2, &converted, 2); + } + decoder.pending.erase(decoder.pending.begin(), decoder.pending.begin() + static_cast(count)); + return outputBytes; + } + + int Destroy(void* state) { + if (!state) return error(0); + std::lock_guard lock(mutex); + if (decoders.erase(state) == 0) return error(1); + return 0; + } + +private: + struct Decoder { + std::uint32_t context; + int channels; + int sampleRate; + AVCodecContext* codec = nullptr; + SwrContext* resampler = nullptr; + std::vector pending; + ~Decoder() { + swr_free(&resampler); + avcodec_free_context(&codec); + } + }; + + const char* name; + bool celtOnly; + std::mutex mutex; + std::unordered_map contexts; + std::unordered_map> decoders; + std::uint32_t nextContext = 1; + + int error(std::uint32_t offset) const { + return static_cast((celtOnly ? 0x81400000u : 0x81740000u) + offset); + } + + [[noreturn]] void fail(const char* message) const { + throw std::runtime_error(std::string(name) + ": " + message); + } + + int packetSamples(const std::uint8_t* packet, int size) const { + const unsigned config = packet[0] >> 3; + const int frameSamples = config < 12 ? ((config & 3) == 3 ? 2880 : 480 << (config & 3)) + : config < 16 ? 480 << (config & 1) : 120 << (config & 3); + int frames = 1; + switch (packet[0] & 3) { + case 1: + case 2: frames = 2; break; + case 3: + if (size < 2) fail("truncated packet frame count"); + frames = packet[1] & 63; + break; + } + if (frames == 0 || frames * frameSamples > 5760) fail("invalid packet duration"); + return frames * frameSamples; + } +}; + +} // namespace + +#endif diff --git a/core/libs/prx/libSceOpusSilkEnc/Export.cpp b/core/libs/prx/libSceOpusSilkEnc/Export.cpp new file mode 100644 index 0000000000..4e741a3bf9 --- /dev/null +++ b/core/libs/prx/libSceOpusSilkEnc/Export.cpp @@ -0,0 +1,80 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +struct OpusSilkEncoder; + +extern "C" { + +int APS5_VABI sceOpusSilkEncCancel(OpusSilkEncoder* encoder, std::uint32_t batchId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncCreate(OpusSilkEncoder* encoder, std::int32_t sampleRate, std::int32_t channels, std::int32_t application) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncCreateEx(const std::uint32_t* contextId, OpusSilkEncoder* encoder, std::int32_t sampleRate, std::int32_t channels, std::int32_t application) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncCtl(OpusSilkEncoder* encoder, std::int32_t request, ...) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncDestroy(OpusSilkEncoder* encoder) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncode(OpusSilkEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeFloat(OpusSilkEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeFloatWithPriority(OpusSilkEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeFloatWithPriorityAndTimeout(OpusSilkEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority, std::uint32_t timeout, std::uint32_t* batchId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeWithPriority(OpusSilkEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncEncodeWithPriorityAndTimeout(OpusSilkEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority, std::uint32_t timeout, std::uint32_t* batchId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncGetSize(std::int32_t channels) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncInitialize(std::uint32_t* contextId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceOpusSilkEncTerminate(const std::uint32_t* contextId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceOrbisCompat/Export.cpp b/core/libs/prx/libSceOrbisCompat/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceOrbisCompat/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libScePad/CMakeLists.txt b/core/libs/prx/libScePad/CMakeLists.txt index 16919e233d..b979e5344c 100644 --- a/core/libs/prx/libScePad/CMakeLists.txt +++ b/core/libs/prx/libScePad/CMakeLists.txt @@ -17,3 +17,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_pad_vibration_mode_tests ${LIBS_INCLUDE_DIR}/tests/GuestPadVibrationMode.cpp) + target_include_directories(guest_pad_vibration_mode_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pad_vibration_mode_tests PRIVATE libScePad libc) + configure_windows_unwind(guest_pad_vibration_mode_tests) + add_test(NAME guest_pad_vibration_mode COMMAND guest_pad_vibration_mode_tests) + if(WIN32) + set_tests_properties(guest_pad_vibration_mode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index bc8838e917..057d434611 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 @@ -10,12 +11,104 @@ #include "prx/libScePad/include/Pad.hpp" #include "prx/libScePad/include/PadState.hpp" +struct PadStickTuningParameter; +struct PadTriggerTuningParameter; +struct PadHuntProfile; +struct PadLeonardoProfile; +struct PadProfileInfo; +struct PadActiveProfileInfo; +struct PadSavedSettingInfo; +struct PadUsbDetectionState; +struct PadVrUsbConnectionStatus; +struct PadDeviceInfo; +struct PadLicenseControllerInformation; +struct PadRemoteSetting; +struct PadVirtualTriggerSetting; +struct PadProcessFocusParameter; +struct PadDataExtended; +struct PadVrControllerData { + std::uint32_t buttons; + std::uint8_t stick[2]; + std::uint8_t unused6[2]; + std::uint8_t triggers[2]; + std::uint8_t unused10[2]; + std::uint32_t touchButtons; + std::uint8_t touchValues[4]; + float acceleration[3]; + float angularVelocity[3]; + float orientation[4]; + std::uint8_t unused60[4]; + std::uint8_t connected; + std::uint8_t connectedCount; + std::uint8_t unused66[6]; + std::uint64_t timestamp; + std::uint8_t unused80[24]; +}; + +static_assert(sizeof(PadVrControllerData) == 104); +static_assert(offsetof(PadVrControllerData, triggers) == 8); +static_assert(offsetof(PadVrControllerData, touchButtons) == 12); +static_assert(offsetof(PadVrControllerData, acceleration) == 20); +static_assert(offsetof(PadVrControllerData, angularVelocity) == 32); +static_assert(offsetof(PadVrControllerData, orientation) == 44); +static_assert(offsetof(PadVrControllerData, connected) == 64); +static_assert(offsetof(PadVrControllerData, timestamp) == 72); +struct PadVrControllerDeviceInformation; +struct PadVrTriggerEffect; +struct PadVrTriggerEffects; +struct PadTriggerEffectExtended; +struct PadExtensionReport; +struct PadExtensionUnitInfo; +struct PadTriggerInternalState; +struct PadTrackerData; +struct PadBlasterTrackerData; +struct PadAstonTrackerData; +struct PadUiData; +struct PadUiTuningData; +struct PadLotusButtonData; +struct PadVersionInfo; +struct PadTelemetryInfo; +struct PadPortInfo; +struct PadVrControllerCombinedState; +struct PadLeonardoData; +struct PadLeonardoUniqueData; +struct PadHistoryData; +struct PadUpdateParameter; +struct PadVirtualDeviceParameter; +struct PadVirtualDeviceId; +struct PadVirtualDeviceComponentInformation; + +struct PadVirtualTriggerInternalState { + std::uint32_t states[2]; + std::uint8_t mode; + std::uint8_t flags[2]; +}; + +static_assert(sizeof(PadVirtualTriggerInternalState) == 12); +static_assert(offsetof(PadVirtualTriggerInternalState, mode) == 8); +static_assert(offsetof(PadVirtualTriggerInternalState, flags) == 9); + +struct PadLightBarBlinkingParameter { + std::uint8_t values[3]; +}; + +struct PadButtonRemappingInfo { + std::uint8_t mapping[14]; + std::uint8_t reserved; + std::uint8_t mode; +}; + +static_assert(sizeof(PadLightBarBlinkingParameter) == 3); +static_assert(sizeof(PadButtonRemappingInfo) == 16); +static_assert(offsetof(PadButtonRemappingInfo, mode) == 15); + namespace { constexpr int PAD_ERROR_DEVICE_NOT_CONNECTED = static_cast(0x80920007); constexpr int PAD_ERROR_DEVICE_NO_HANDLE = static_cast(0x80920008); 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; @@ -25,6 +118,22 @@ bool ValidPort(int userId, int type, int index) { } +struct PadUpdateData { + std::size_t size; + std::uint8_t data[32768]; +}; + +static_assert(sizeof(PadUpdateData) == 32776); +static_assert(offsetof(PadUpdateData, data) == 8); + +struct PadMotionSensorPosition { + float x; + float y; + float z; +}; + +static_assert(sizeof(PadMotionSensorPosition) == 12); + extern "C" { int APS5_VABI scePadClose_nid_postfix(int handle) { @@ -35,17 +144,17 @@ int APS5_VABI scePadClose_nid_postfix(int handle) { return PAD_OK; } -int APS5_VABI scePadVrControllerSetVibration() { +int APS5_VABI scePadVrControllerSetVibration(std::int32_t handle, std::uint8_t intensity) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadVrControllerSetVibrationMode() { +int APS5_VABI scePadVrControllerSetVibrationMode(std::int32_t handle, std::uint32_t mode) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadVrControllerReadCombinedState() { +int APS5_VABI scePadVrControllerReadCombinedState(std::int32_t leftHandle, std::int32_t rightHandle, PadVrControllerCombinedState* state) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -76,10 +185,10 @@ int APS5_VABI scePadGetControllerInformation(int handle, PadControllerInformatio } std::memset(info, 0, sizeof(*info)); info->touchPadInfo.pixelDensity = 44.86f; - info->touchPadInfo.resolution.x = 1920; - info->touchPadInfo.resolution.y = 943; - info->stickInfo.deadZoneLeft = 2; - info->stickInfo.deadZoneRight = 2; + info->touchPadInfo.resolution.x = kTouchPadWidth; + info->touchPadInfo.resolution.y = kTouchPadHeight; + info->stickInfo.deadZoneLeft = 13; + info->stickInfo.deadZoneRight = 13; info->connectionType = PAD_CONNECTION_TYPE_LOCAL; info->connectedCount = 1; info->connected = true; @@ -120,10 +229,10 @@ int APS5_VABI scePadGetExtControllerInformation(int handle, void* info) { std::memset(info, 0, 0x2c); PadControllerInformation* base = static_cast(info); base->touchPadInfo.pixelDensity = 44.86f; - base->touchPadInfo.resolution.x = 1920; - base->touchPadInfo.resolution.y = 943; - base->stickInfo.deadZoneLeft = 2; - base->stickInfo.deadZoneRight = 2; + base->touchPadInfo.resolution.x = kTouchPadWidth; + base->touchPadInfo.resolution.y = kTouchPadHeight; + base->stickInfo.deadZoneLeft = 13; + base->stickInfo.deadZoneRight = 13; base->connectionType = PAD_CONNECTION_TYPE_LOCAL; base->connectedCount = 1; base->connected = true; @@ -143,6 +252,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; } @@ -164,23 +274,23 @@ int APS5_VABI scePadOpenExt(int userId, int type, int index, const void* param) return PAD_ERROR_DEVICE_NOT_CONNECTED; } -int APS5_VABI scePadReadExt() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI scePadReadExt(std::int32_t handle, PadDataExtended* data, std::int32_t count) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadGetFeatureReport() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI scePadGetFeatureReport(std::int32_t handle, std::uint8_t reportId, void* data, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadSetFeatureReport() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI scePadSetFeatureReport(std::int32_t handle, std::uint8_t reportId, const void* data, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadOutputReport() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI scePadOutputReport(std::int32_t handle, std::uint8_t reportId, const void* data, std::uint64_t size) { NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI scePadRead_nid_postfix(int handle, PadData* data, int num) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - if (data == nullptr || num <= 0) APS5_INVALID_ARG_EX; + if (data == nullptr || num <= 0) return PAD_ERROR_INVALID_ARG; return Pad::Read(data, num); } int APS5_VABI scePadReadState(int handle, PadData* data) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - if (data == nullptr) APS5_INVALID_ARG_EX; + if (data == nullptr) return PAD_ERROR_INVALID_ARG; *data = Pad::ReadState(); @@ -251,8 +361,10 @@ int APS5_VABI scePadSetVibration(int handle, const PadVibrationParam* param) { } int APS5_VABI scePadSetVibrationMode(int handle, int mode) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (mode <= 0) return PAD_ERROR_INVALID_ARG; if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - if (mode < 0 || mode > 3) return PAD_ERROR_INVALID_ARG; + if (mode > 3 && mode != 5) return PAD_ERROR_INVALID_ARG; Pad::SetVibrationMode(mode); return PAD_OK; } @@ -262,34 +374,34 @@ int APS5_VABI scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool enab return PAD_OK; } -int APS5_VABI scePadVrControllerGetDeviceInformation() { +int APS5_VABI scePadVrControllerGetDeviceInformation(std::int32_t handle, PadVrControllerDeviceInformation* info) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadVrControllerRead() { +int APS5_VABI scePadVrControllerRead(std::int32_t handle, PadVrControllerData* data, std::int32_t count) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadVrControllerGetTriggerEffectState() { +int APS5_VABI scePadVrControllerGetTriggerEffectState(std::int32_t leftHandle, std::int32_t rightHandle, PadTriggerEffectStateInformation* states) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadVrControllerReadState(int handle, void* state) { +int APS5_VABI scePadVrControllerReadState(int handle, PadVrControllerData* state) { (void)handle; (void)state; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadVrControllerSetTriggerEffect() { +int APS5_VABI scePadVrControllerSetTriggerEffect(std::int32_t handle, const PadVrTriggerEffect* effect) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadVrControllerSetTriggerEffects() { +int APS5_VABI scePadVrControllerSetTriggerEffects(std::int32_t leftHandle, std::int32_t rightHandle, const PadVrTriggerEffects* effects) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -310,17 +422,762 @@ int APS5_VABI scePadSetAngularVelocityBiasCorrectionState(int handle, bool enabl APS5_EXPORT("n3kSX62fgNo", scePadUnknown_n3kSX62fgNo); int APS5_VABI scePadUnknown_n3kSX62fgNo() { + return g_initialized.load(std::memory_order_acquire) ? PAD_OK : PAD_ERROR_NOT_INITIALIZED; +} + +int APS5_VABI scePadVrControllerGetTriggerEffectStates(std::int32_t leftHandle, std::int32_t rightHandle, PadTriggerEffectStateInformation* states) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePadVrControllerGetTriggerEffectStates(void) { +int APS5_VABI scePadGetAccessibilityControllerState(int handle, std::uint32_t* state) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (state == nullptr) return PAD_ERROR_INVALID_ARG; + if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + *state = 0; + return PAD_OK; +} + +int APS5_VABI scePadSetTriggerEffectExt(std::int32_t handle, const PadTriggerEffectExtended* effect) { NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("zBsgF0q8DIM", scePadUnknown_zBsgF0q8DIM); -int APS5_VABI scePadUnknown_zBsgF0q8DIM() { +int APS5_VABI scePadLastLotusButtonData(std::int32_t handle, std::uint32_t parameter, PadLotusButtonData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadReadStateExt(std::int32_t handle, PadDataExtended* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetExtensionReport(std::int32_t handle, const PadExtensionReport* report) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetExtensionUnitInfo(std::int32_t handle, PadExtensionUnitInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadEnableExtensionPort(bool enabled) { + return 0; +} + +int APS5_VABI scePadEnableSpecificDeviceClass(std::int32_t deviceClass) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadOpenExt2(std::uint64_t deviceId, std::int32_t type, const void* reserved) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDeviceOpen(std::uint64_t deviceId, std::int32_t type, std::int32_t index) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetDataInternal(std::uint8_t port, PadData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetUserColor(std::int32_t handle, std::uint32_t color) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisableVibration(std::int32_t handle, bool disabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetVibrationStrength(std::int32_t handle, std::uint32_t strength) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLightBarBaseBrightness(std::int32_t handle, std::uint32_t brightness) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLightBarBlinking(std::int32_t handle, const PadLightBarBlinkingParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::uint64_t APS5_VABI scePadGetIdleCount(std::int32_t userId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetControllerIdleCount(std::int32_t handle, std::uint64_t* count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetPowerSavingState(std::int32_t handle, std::uint8_t state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetAutoPowerOffCount(std::int32_t count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadEnableUsbConnection(bool enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadEnableAutoDetect(bool enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadEnablePlayPauseEmulation(bool enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisableTrackingLedBatterySaver(bool disabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLoginUserNumber(std::int32_t number) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetCapability(std::int32_t deviceId, std::uint32_t* capability) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadShareOutputData(std::int32_t source, std::int32_t destination) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadShareOutputDataWithFunctionType(std::int32_t source, std::int32_t destination, std::uint32_t functionType) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetVibrationForce(std::int32_t handle, const PadVibrationParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetVersionInfo(std::int32_t handle, PadVersionInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetControllerType(std::int32_t handle, std::int32_t* type) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisableDataConversion(std::int32_t handle, bool disabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetButtonRemappingInfo(std::int32_t handle, const PadButtonRemappingInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetProcessPrivilegeOfButtonRemapping(bool enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetForceIntercepted(bool enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadIsLightBarBaseBrightnessControllable(std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSwitchConnection(std::int32_t handle, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisableTriggerEffect(std::int32_t handle, bool disabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetTriggerEffectStrength(std::int32_t handle, std::uint32_t strength) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetTriggerEffectInternalState(std::int32_t handle, PadTriggerInternalState* state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetUserNumber(std::int32_t handle, std::int32_t number) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisableTransportButtons(bool disabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLotusIrCodeSetStart(std::int32_t handle, std::uint8_t codeSet, std::uint16_t code, std::size_t dataSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLotusIrCodeSetData(std::int32_t handle, std::uint8_t packetIndex, const void* data, std::size_t dataSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLotusIrCodeSetEnd(std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetLotusIrCodeSetResult(std::int32_t handle, std::int32_t* result, std::uint8_t* deviceResult) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLotusEnableIrKey(std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLotusDisableIrKey(std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetUsbDetectionState(std::int32_t deviceId, PadUsbDetectionState* state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadUiReadState(std::int32_t handle, PadUiData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadUiReadStateForTuning(std::int32_t handle, PadUiTuningData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetStickTriggerTuningMode(std::int32_t handle, std::uint32_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetStickTuningParam(std::int32_t handle, std::uint32_t stick, const PadStickTuningParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetTriggerTuningParam(std::int32_t handle, const PadTriggerTuningParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetProfileHunt(std::int32_t handle, std::int32_t profileId, PadHuntProfile* profile) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetProfileHunt(std::int32_t handle, std::int32_t profileId, PadHuntProfile* profile, std::uint32_t* result) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadResetHunt(std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetProfileInfo(std::int32_t deviceId, std::int32_t profileId, PadProfileInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetActiveProfileInfo(std::int32_t handle, PadActiveProfileInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisableActiveProfileChange(std::int32_t handle, bool disabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisableActiveProfileChangeByDeviceId(std::int32_t deviceId, bool disabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetActiveProfileSlot(std::int32_t deviceId, std::uint32_t slot) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetSavedSettingInfo(std::int32_t handle, PadSavedSettingInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetSavedSettingInfo(std::int32_t handle, const PadSavedSettingInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVrControllerGetPhysicalUsbConnectionStatus(PadVrUsbConnectionStatus* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadParseUniqueDataLeonardo(std::int32_t handle, const PadLeonardoData* data, PadLeonardoUniqueData* uniqueData) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetControllerTypeLeonardo(std::int32_t deviceId, std::int32_t* controllerType) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetControllerTypeLeonardo(std::int32_t deviceId, std::int32_t controllerType) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetProfileLeonardo(std::int32_t handle, std::int32_t profileId, PadLeonardoProfile* profile) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetProfileLeonardo(std::int32_t handle, std::int32_t profileId, PadLeonardoProfile* profile, std::uint32_t* result) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadResetLeonardo(std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetProfileEditMode(std::int32_t handle, bool enabled, std::uint32_t profile) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetProfileEditMode(std::int32_t handle, std::int32_t* mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetProfileEditModeLeonardo(std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetSelectButtonCustomizeMode(std::int32_t handle, bool enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetSelectButtonCustomizeMode(std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetToggleAllReleaseLeonardo(std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadEnableControllerIndicator(std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadEnableProfileIndicator(std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetProfileIndicatorAnimation(std::int32_t handle, std::uint8_t animation) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadIsCoPilot(std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetIsCoPilot(std::int32_t handle, bool enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadRegisterCollaboInputMgrProcess(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetTelemetryInfo(std::int32_t handle, PadTelemetryInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetActiveTime(std::int32_t handle, std::int32_t type, std::uint64_t* activeTime) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetDeviceId(std::int32_t handle, std::int32_t* deviceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetInfoByPortType(std::int32_t type, PadPortInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadResetLightBarAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadResetLightBarAllByPortType(std::uint32_t portType) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetLightBarForTracker(std::int32_t handle, const PadLightBarParam* color) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadReadForTracker(std::int32_t handle, PadTrackerData* data, std::int32_t count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadReadBlasterForTracker(std::int32_t handle, PadBlasterTrackerData* data, std::int32_t count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadResetOrientationForTracker(std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetMotionTimerUnit(std::int32_t handle, float* unit) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetVrTrackingMode(std::uint8_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetMotionSensorPosition(std::int32_t handle, PadMotionSensorPosition* position) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetSphereRadius(std::int32_t handle, float* radius) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI scePadIsValidHandle(std::int32_t handle, std::int32_t portType) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI scePadIsDS4Connected(std::int32_t handle) { + (void)handle; + return false; +} + +bool APS5_VABI scePadIsBlasterConnected(std::int32_t handle) { + (void)handle; + return false; +} + +bool APS5_VABI scePadIsMoveConnected(std::int32_t handle) { + (void)handle; + return false; +} + +int APS5_VABI scePadGetBluetoothAddress(std::int32_t handle, std::uint64_t* address) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadIsMoveReproductionModel(std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadReadAstonForTracker(std::int32_t handle, PadAstonTrackerData* data, std::int32_t count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetAstonModel(std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadMbusInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadMbusTerm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadConnectPort(std::int32_t userId, std::int32_t portType) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisconnectPort(std::int32_t userId, std::int32_t portType) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadDisconnectDevice(std::int32_t deviceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetDeviceInfo(std::int32_t deviceId, PadDeviceInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetProcessFocus(const PadProcessFocusParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVertualDeviceAddDevice(const PadVirtualDeviceParameter* parameter, PadVirtualDeviceId* deviceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVirtualDeviceAddDevice(const PadVirtualDeviceParameter* parameter, PadVirtualDeviceId* deviceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVirtualDeviceDeleteDevice(std::int32_t deviceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVirtualDeviceInsertData(std::int32_t deviceId, const PadData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVirtualDeviceInsertTriggerInternalState(std::int32_t deviceId, const PadVirtualTriggerInternalState* state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVirtualDeviceGetRemoteSetting(std::int32_t deviceId, PadRemoteSetting* setting) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVirtualDeviceSetTriggerEffectSetting(std::int32_t handle, const PadVirtualTriggerSetting* setting) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVirtualDeviceDisableButtonRemapping(bool disabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetShareButtonMaskForRemotePlay(std::uint8_t mask) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetPS4BcVibrationMode(std::uint8_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetVirtualDeviceComponentInformation(std::int32_t deviceId, const PadVirtualDeviceComponentInformation* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadEnablePs4CompatibleMode(std::int32_t handle, bool enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetLicenseControllerInformation(std::int32_t handle, PadLicenseControllerInformation* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadReadHistory(std::int32_t handle, PadHistoryData* data, std::int32_t count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadStartRecording(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadStopRecording(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadRemoteControlInsertData(const std::uint8_t* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadRemoteControlInsertDataForAston(const std::uint8_t* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadGetFeatureReport2(std::int32_t handle, std::uint8_t reportId, void* data, std::size_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetFeatureReport2(std::int32_t handle, std::uint8_t reportId, const void* data, std::size_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadSetMuteLed(std::int32_t handle, std::uint32_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadBondUpdateInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadBondUpdateIsReady(std::int32_t deviceId, std::int32_t* ready) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadBondUpdateStart(std::int32_t handle, const PadUpdateParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadBondUpdateGetStatus(std::int32_t deviceId, std::int32_t* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadBondUpdateSendData(std::int32_t deviceId, const PadUpdateData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadBondUpdateResetBond(std::int32_t deviceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadBondUpdateGetReconnectStatus(std::int32_t* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadBondUpdateTerm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadControllerUpdateInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadControllerUpdateIsUpdateNeeded2(std::int32_t deviceId, const std::uint32_t* version, std::uint16_t* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadControllerUpdateIsReady(std::int32_t deviceId, std::int32_t* ready) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadControllerUpdateStart(std::int32_t handle, const PadUpdateParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadControllerUpdateGetStatus(std::int32_t deviceId, std::int32_t* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadControllerUpdateSendData(std::int32_t deviceId, const PadUpdateData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadControllerUpdateResetController(std::int32_t deviceId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadControllerUpdateGetReconnectStatus(std::int32_t* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadControllerUpdateTerm(void) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libScePad/include/InputCommandFile.hpp b/core/libs/prx/libScePad/include/InputCommandFile.hpp index 7433fd9f49..e372a1f3f7 100644 --- a/core/libs/prx/libScePad/include/InputCommandFile.hpp +++ b/core/libs/prx/libScePad/include/InputCommandFile.hpp @@ -1,12 +1,18 @@ #ifndef CORE_LIBS_PRX_LIBSCEPAD_INPUTCOMMANDFILE_HPP #define CORE_LIBS_PRX_LIBSCEPAD_INPUTCOMMANDFILE_HPP +#include #include #include #include #include +#include +#include #include +#include #include +#include +#include namespace Pad { class InputCommandFile { @@ -15,35 +21,49 @@ 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; } Input Poll(std::uint64_t now) { if (path.empty()) return {}; if (now >= nextPoll) { - nextPoll = now + 100000; - const auto command = read(); + nextPoll = now > std::numeric_limits::max() - 100000 + ? std::numeric_limits::max() : now + 100000; + auto command = read(); if (command.sequence > lastSequence) { lastSequence = command.sequence; - input = command.input; - until = now + command.durationMs * 1000ull; + events = std::move(command.events); + started = now; } } - return now < until ? input : Input{}; + if (events.empty() || now < started) return {}; + const auto elapsed = now - started; + const auto next = std::upper_bound(events.begin(), events.end(), elapsed, + [](std::uint64_t time, const Event& event) { return time < event.start; }); + if (next == events.begin()) return {}; + const auto& event = *std::prev(next); + return elapsed < event.end ? event.input : Input{}; } std::uint32_t Buttons(std::uint64_t now) { return Poll(now).buttons; } private: + struct Event { + std::uint64_t start = 0; + std::uint64_t end = 0; + Input input; + }; + struct Command { std::uint64_t sequence = 0; - Input input; - std::uint32_t durationMs = 0; + std::vector events; }; Command read() const { @@ -53,6 +73,40 @@ private: const std::string text(line); if (text.find('-') != std::string::npos) return {}; std::istringstream fields(text); + std::string first; + if (!(fields >> first)) return {}; + if (first == "APS5_PAD_TIMELINE") { + Command command; + std::string extra; + std::uint64_t version = 0, count = 0; + if (!(fields >> version >> command.sequence >> count) || fields >> extra) return {}; + if (version != 1 || command.sequence == 0 || count == 0 || count > 4096) return {}; + if (command.sequence <= lastSequence) return command; + command.events.reserve(static_cast(count)); + std::uint64_t previousEnd = 0; + for (std::uint64_t index = 0; index < count; ++index) { + if (!file.getline(line, sizeof(line))) return {}; + const std::string eventText(line); + if (eventText.find('-') != std::string::npos) return {}; + fields.clear(); + fields.str(eventText); + std::uint64_t start = 0, duration = 0, buttons = 0; + if (!(fields >> start >> duration >> buttons) || fields >> extra) return {}; + if ((buttons & ~(0x10fffeull | TouchLeftBit | TouchRightBit)) != 0 || duration == 0 || duration > 5000 || + start < previousEnd || start > 3600000 - duration) return {}; + previousEnd = start + duration; + Input input; + input.buttons = static_cast(buttons & 0x10fffeull); + input.touchLeft = (buttons & TouchLeftBit) != 0; + input.touchRight = (buttons & TouchRightBit) != 0; + command.events.push_back({start * 1000, previousEnd * 1000, input}); + } + file >> std::ws; + if (file.peek() != std::char_traits::eof()) return {}; + return command; + } + fields.clear(); + fields.str(text); std::string tokens[8]; int count = 0; while (count < 8 && fields >> tokens[count]) ++count; @@ -64,24 +118,30 @@ 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.durationMs = static_cast(values[2]); + Input input; + input.buttons = static_cast(values[1] & 0x10fffeull); + input.touchLeft = (values[1] & TouchLeftBit) != 0; + input.touchRight = (values[1] & TouchRightBit) != 0; for (int i = 3; i < count; ++i) { if (values[i] > 255) return {}; - command.input.sticks[i - 3] = static_cast(values[i]); + input.sticks[i - 3] = static_cast(values[i]); } - command.input.hasSticks = count == 7; + input.hasSticks = count == 7; + command.events.push_back({0, values[2] * 1000, input}); return command; } - 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; - Input input; + std::uint64_t started = 0; + std::vector events; }; } diff --git a/core/libs/prx/libScePad/include/InputMapping.hpp b/core/libs/prx/libScePad/include/InputMapping.hpp index a8914303d5..ec4da4ab47 100644 --- a/core/libs/prx/libScePad/include/InputMapping.hpp +++ b/core/libs/prx/libScePad/include/InputMapping.hpp @@ -26,6 +26,9 @@ struct InputSettings { double mouseMaxSpeed = 1.0; double mouseSmoothingMs = 10.0; double mouseDecayMs = 30.0; + double stickModifierScale = 0.5; + double keyboardRampMs = 0.0; + double triggerRampMs = 0.0; }; struct InputConfiguration { @@ -35,6 +38,8 @@ struct InputConfiguration { inline constexpr std::array InputMapping{ InputBinding{SDL_SCANCODE_F11, MouseButton::None, InputControl::ToggleFullscreen}, + InputBinding{SDL_SCANCODE_F10, MouseButton::None, InputControl::TogglePerformanceOverlay}, + InputBinding{SDL_SCANCODE_F12, MouseButton::None, InputControl::Screenshot}, InputBinding{SDL_SCANCODE_RETURN, MouseButton::None, InputControl::Button, PadButton::Cross}, InputBinding{SDL_SCANCODE_SPACE, MouseButton::None, InputControl::Button, PadButton::Cross}, InputBinding{SDL_SCANCODE_ESCAPE, MouseButton::None, InputControl::Button, PadButton::Options}, @@ -42,6 +47,7 @@ inline constexpr std::array InputMapping{ InputBinding{SDL_SCANCODE_C, MouseButton::None, InputControl::Button, PadButton::Circle}, InputBinding{SDL_SCANCODE_Q, MouseButton::None, InputControl::Button, PadButton::L1}, InputBinding{SDL_SCANCODE_E, MouseButton::None, InputControl::Button, PadButton::R1}, + InputBinding{SDL_SCANCODE_R, MouseButton::None, InputControl::Button, PadButton::L2}, InputBinding{SDL_SCANCODE_LALT, MouseButton::None, InputControl::Button, PadButton::R1}, InputBinding{SDL_SCANCODE_RALT, MouseButton::None, InputControl::Button, PadButton::R1}, InputBinding{SDL_SCANCODE_LSHIFT, MouseButton::None, InputControl::Button, PadButton::L3}, @@ -62,9 +68,12 @@ inline constexpr std::array InputMapping{ InputBinding{SDL_SCANCODE_G, MouseButton::None, InputControl::RightStickDown}, InputBinding{SDL_SCANCODE_BACKSPACE, MouseButton::None, InputControl::TouchLeft}, InputBinding{SDL_SCANCODE_TAB, MouseButton::None, InputControl::TouchRight}, + InputBinding{SDL_SCANCODE_F7, MouseButton::None, InputControl::ToggleMouse}, + InputBinding{SDL_SCANCODE_X, MouseButton::None, InputControl::Shake}, InputBinding{SDL_SCANCODE_UNKNOWN, MouseButton::Middle, InputControl::ToggleMouse}, InputBinding{SDL_SCANCODE_UNKNOWN, MouseButton::Left, InputControl::Button, PadButton::Square}, InputBinding{SDL_SCANCODE_UNKNOWN, MouseButton::Right, InputControl::Button, PadButton::R2}, + InputBinding{SDL_SCANCODE_UNKNOWN, MouseButton::X1, InputControl::Button, PadButton::L2}, InputBinding{SDL_SCANCODE_UNKNOWN, MouseButton::None, InputControl::Button, PadButton::Up, 1}, InputBinding{SDL_SCANCODE_UNKNOWN, MouseButton::None, InputControl::Button, PadButton::Down, -1} }; diff --git a/core/libs/prx/libScePad/include/KeyboardMouse.hpp b/core/libs/prx/libScePad/include/KeyboardMouse.hpp index 68e501a384..14002488e1 100644 --- a/core/libs/prx/libScePad/include/KeyboardMouse.hpp +++ b/core/libs/prx/libScePad/include/KeyboardMouse.hpp @@ -14,6 +14,13 @@ namespace Pad { inline constexpr double MouseFullTiltCountsPerSecond = 3000.0; inline constexpr double MouseMinimumStepSeconds = 0.0005; +inline constexpr double MouseRateWindowSeconds = 0.25; +inline constexpr double MouseRateGapSeconds = 0.05; +inline constexpr double MouseSmoothingReports = 3.0; +inline constexpr double MouseSmoothingLimitMs = 30.0; +inline constexpr std::chrono::milliseconds ShakeHalfPeriod{50}; +inline constexpr float ShakeAcceleration = 2.0f * 9.80665f; +inline constexpr float ShakeAngularVelocity = 6.0f; class MouseStick { public: @@ -22,20 +29,32 @@ public: void Advance(double seconds); void Reset(); double Speed() const; + double ReportRate() const; + double SmoothingMs() const; std::array Deflection() const; private: + void measureReportRate(double step, bool moved); + InputSettings settings; double pendingX = 0.0; double pendingY = 0.0; double pendingSeconds = 0.0; + double pendingCounts = 0.0; + int pendingReports = 0; double speedX = 0.0; double speedY = 0.0; double idleSeconds = 0.0; + double rateSeconds = 0.0; + double rateCounts = 0.0; + int rateReports = 0; + double reportRate = 0.0; }; struct InputRequests { bool toggleFullscreen = false; + bool togglePerformanceOverlay = false; + bool screenshot = false; bool toggleMouse = false; bool releaseMouse = false; }; @@ -48,19 +67,34 @@ 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; const InputConfiguration& Configuration() const; private: + struct StickInput { + std::array targets{}; + std::array held{}; + }; + + StickInput stickInput() const; + std::array triggerTargets() const; + bool advanceAxes(Clock::time_point now); + InputConfiguration configuration; std::vector pressed; std::vector wheelReleaseTimes; MouseStick mouse; Clock::time_point lastAdvance{}; + Clock::time_point lastAxisAdvance{}; + std::array stickAxes{}; + std::array triggerAxes{}; bool advanced = false; + bool axesAdvanced = false; bool mouseMode = false; + bool titleReadsMouse = false; }; std::uint8_t StickByte(double deflection); diff --git a/core/libs/prx/libScePad/include/Pad.hpp b/core/libs/prx/libScePad/include/Pad.hpp index b24a7a8f3c..8a4a7406a5 100644 --- a/core/libs/prx/libScePad/include/Pad.hpp +++ b/core/libs/prx/libScePad/include/Pad.hpp @@ -6,6 +6,7 @@ constexpr int PAD_OK = 0; constexpr int PAD_ERROR_INVALID_ARG = -2137915391; constexpr int PAD_ERROR_INVALID_HANDLE = -2137915389; +constexpr int PAD_ERROR_NOT_INITIALIZED = static_cast(0x80920005u); constexpr int PAD_PORT_TYPE_STANDARD = 0; constexpr int PAD_PORT_TYPE_SPECIAL = 2; diff --git a/core/libs/prx/libScePad/include/PadAudioHapticQueue.hpp b/core/libs/prx/libScePad/include/PadAudioHapticQueue.hpp new file mode 100644 index 0000000000..2ad526fa00 --- /dev/null +++ b/core/libs/prx/libScePad/include/PadAudioHapticQueue.hpp @@ -0,0 +1,48 @@ +#ifndef CORE_LIBS_PRX_LIBSCEPAD_PADAUDIOHAPTICQUEUE_HPP +#define CORE_LIBS_PRX_LIBSCEPAD_PADAUDIOHAPTICQUEUE_HPP + +#include +#include +#include +#include +#include +#include +#include + +class PadAudioHapticQueue { + struct Block { + std::uint64_t begin; + std::uint64_t end; + std::array rumble; + }; + std::map> sources; + +public: + void Queue(int source, std::uint64_t begin, std::uint64_t end, std::uint8_t large, std::uint8_t small) { + if (source <= 0 || begin >= end) throw std::invalid_argument("Pad: invalid legacy haptic interval"); + auto& blocks = sources[source]; + if (!blocks.empty() && begin < blocks.back().end) throw std::invalid_argument("Pad: overlapping legacy haptic intervals"); + blocks.push_back({begin, end, {large, small}}); + } + + void Clear(int source) { sources.erase(source); } + + std::array Read(std::uint64_t now) { + std::array result{}; + for (auto source = sources.begin(); source != sources.end();) { + auto& blocks = source->second; + while (!blocks.empty() && blocks.front().end <= now) blocks.pop_front(); + if (blocks.empty()) { + source = sources.erase(source); + continue; + } + if (blocks.front().begin <= now) { + for (std::size_t channel = 0; channel < result.size(); ++channel) result[channel] = std::max(result[channel], blocks.front().rumble[channel]); + } + ++source; + } + return result; + } +}; + +#endif diff --git a/core/libs/prx/libScePad/include/PadInputTypes.hpp b/core/libs/prx/libScePad/include/PadInputTypes.hpp index 81842bae72..f8e544e092 100644 --- a/core/libs/prx/libScePad/include/PadInputTypes.hpp +++ b/core/libs/prx/libScePad/include/PadInputTypes.hpp @@ -19,8 +19,12 @@ enum class InputControl { RightStickDown, TouchLeft, TouchRight, + StickModifier, + Shake, ToggleMouse, - ToggleFullscreen + ToggleFullscreen, + TogglePerformanceOverlay, + Screenshot }; enum class PadButton : std::uint32_t { diff --git a/core/libs/prx/libScePad/include/PadState.hpp b/core/libs/prx/libScePad/include/PadState.hpp index d38385a704..fb621171ee 100644 --- a/core/libs/prx/libScePad/include/PadState.hpp +++ b/core/libs/prx/libScePad/include/PadState.hpp @@ -6,6 +6,9 @@ #include #include "SceTypes.hpp" +constexpr std::uint16_t kTouchPadWidth = 1920; +constexpr std::uint16_t kTouchPadHeight = 1080; + struct PadTouchPoint { bool active = false; std::uint16_t x = 0; @@ -40,6 +43,8 @@ struct PadOutputState { std::uint32_t sequence = 0; std::uint8_t vibrationLarge = 0; std::uint8_t vibrationSmall = 0; + std::uint8_t audioVibrationLarge = 0; + std::uint8_t audioVibrationSmall = 0; bool lightBarValid = false; std::uint8_t lightBar[3]{}; PadTriggerRequest trigger[2]; @@ -50,6 +55,7 @@ struct PadOutputState { namespace Pad { void Initialize(); +bool IsInitialized(); PadData ReadState(); int Read(PadData* data, int num); void SetVibration(std::uint8_t large, std::uint8_t small); @@ -65,5 +71,8 @@ void SetAngularVelocityDeadband(bool enabled); extern "C" void PadPublishInput_nid_postfix(const PadInputState& input); extern "C" void PadReportInputFailure_nid_postfix(std::exception_ptr error); extern "C" bool PadFetchOutput_nid_postfix(std::uint32_t* seenSequence, PadOutputState* out); +extern "C" void PadSubmitAudioHaptics_nid_postfix(std::uint8_t large, std::uint8_t small); +extern "C" void PadQueueLegacyAudioHaptics_nid_postfix(int source, std::uint64_t begin, std::uint64_t end, std::uint8_t large, std::uint8_t small); +extern "C" void PadClearLegacyAudioHaptics_nid_postfix(int source); #endif diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index 2ab3f126c3..5a6795f32e 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -1,8 +1,10 @@ #include "prx/libScePad/include/PadState.hpp" +#include "prx/libScePad/include/PadAudioHapticQueue.hpp" #include "prx/libScePad/include/Pad.hpp" #include "prx/libScePad/include/InputCommandFile.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include +#include #include #include #include @@ -13,7 +15,15 @@ namespace { std::mutex stateMutex; PadInputState state; + std::uint32_t pressedSinceRead = 0; + std::uint8_t pressedAnalogL2 = 0; + std::uint8_t pressedAnalogR2 = 0; + std::uint64_t pressedAt = 0; + constexpr std::uint64_t kLatchExpiry = 100000; PadOutputState output; + std::chrono::steady_clock::time_point audioHapticEnd; + std::array audioHapticRumble{}; + PadAudioHapticQueue legacyHaptics; std::uint64_t timestamp = 0; struct QueuedInput { @@ -36,6 +46,17 @@ namespace { bool prevTouchActive[2] = {false, false}; std::uint8_t touchIds[2] = {0, 0}; + bool RefreshAudioHaptics() { + if (std::chrono::steady_clock::now() >= audioHapticEnd) audioHapticRumble = {}; + const auto legacy = legacyHaptics.Read(sceKernelGetProcessTime()); + const auto large = std::max(audioHapticRumble[0], legacy[0]); + const auto small = std::max(audioHapticRumble[1], legacy[1]); + if (output.audioVibrationLarge == large && output.audioVibrationSmall == small) return false; + output.audioVibrationLarge = large; + output.audioVibrationSmall = small; + return true; + } + void normalize(Quat& q) { const float n = std::sqrt(q.w * q.w + q.x * q.x + q.y * q.y + q.z * q.z); if (n < 1e-6f) { q = Quat{}; return; } @@ -106,10 +127,16 @@ void Pad::Initialize() { timestamp = sceKernelGetProcessTime(); lastFuseTime = timestamp; queue.clear(); + pressedSinceRead = 0; } initialized = true; } +bool Pad::IsInitialized() { + std::lock_guard lock(stateMutex); + return initialized; +} + namespace { PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uint64_t now, const Pad::InputCommandFile::Input& injected) { PadData data{}; @@ -172,11 +199,11 @@ 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_y = 471; + data.touch_data_touch0_x = state.touchRight || injected.touchRight ? kTouchPadWidth * 3 / 4 : kTouchPadWidth / 4; + data.touch_data_touch0_y = kTouchPadHeight / 2; data.touch_data_touch0_id = 0; } data.touch_data_touch_num = touchNum; @@ -184,7 +211,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) { @@ -201,7 +235,14 @@ PadData Pad::ReadState() { if (!initialized) throw std::runtime_error("Pad: read before initialization"); const std::uint64_t now = sceKernelGetProcessTime(); const auto injected = InjectedInput(now); - return BuildData(state, timestamp, now, injected); + PadData data = BuildData(state, timestamp, now, injected); + if (pressedSinceRead != 0 && now - pressedAt <= kLatchExpiry) { + data.buttons |= pressedSinceRead; + if (pressedSinceRead & 0x100u) data.analog_buttons_l2 = std::max(data.analog_buttons_l2, pressedAnalogL2); + if (pressedSinceRead & 0x200u) data.analog_buttons_r2 = std::max(data.analog_buttons_r2, pressedAnalogR2); + } + pressedSinceRead = 0; + return data; } int Pad::Read(PadData* data, int num) { @@ -209,6 +250,7 @@ int Pad::Read(PadData* data, int num) { std::lock_guard lock(stateMutex); if (failure) std::rethrow_exception(failure); if (!initialized) throw std::runtime_error("Pad: read before initialization"); + pressedSinceRead = 0; const std::uint64_t now = sceKernelGetProcessTime(); const auto injected = InjectedInput(now); if (queue.empty()) { @@ -343,8 +385,16 @@ extern "C" void PadPublishInput_nid_postfix(const PadInputState& input) { state.touchLeft == input.touchLeft && state.touchRight == input.touchRight && state.hasMotion == input.hasMotion && state.accel == input.accel && state.gyro == input.gyro && state.touch == input.touch && state.deviceKind == input.deviceKind) return; - state = input; + const std::uint32_t pressed = input.buttons & ~state.buttons; timestamp = sceKernelGetProcessTime(); + if (pressed != 0) { + if (pressedSinceRead != 0 && timestamp - pressedAt > kLatchExpiry) pressedSinceRead = 0; + pressedSinceRead |= pressed; + pressedAt = timestamp; + if (pressed & 0x100u) pressedAnalogL2 = input.analogButtonsL2; + if (pressed & 0x200u) pressedAnalogR2 = input.analogButtonsR2; + } + state = input; if (queue.size() == kQueueCapacity) queue.pop_front(); queue.push_back({input, timestamp}); } @@ -357,8 +407,33 @@ extern "C" void PadReportInputFailure_nid_postfix(std::exception_ptr error) { extern "C" bool PadFetchOutput_nid_postfix(std::uint32_t* seenSequence, PadOutputState* out) { std::lock_guard lock(stateMutex); + if (RefreshAudioHaptics()) ++output.sequence; if (seenSequence == nullptr || out == nullptr || *seenSequence == output.sequence) return false; *seenSequence = output.sequence; *out = output; return true; } + +extern "C" void PadSubmitAudioHaptics_nid_postfix(std::uint8_t large, std::uint8_t small) { + std::lock_guard lock(stateMutex); + if (large == 0 && small == 0) return; + const auto now = std::chrono::steady_clock::now(); + if (now >= audioHapticEnd) audioHapticRumble = {}; + audioHapticRumble[0] = std::max(audioHapticRumble[0], large); + audioHapticRumble[1] = std::max(audioHapticRumble[1], small); + audioHapticEnd = now + std::chrono::milliseconds(80); + static_cast(RefreshAudioHaptics()); + ++output.sequence; +} + +extern "C" void PadQueueLegacyAudioHaptics_nid_postfix(int source, std::uint64_t begin, std::uint64_t end, std::uint8_t large, std::uint8_t small) { + std::lock_guard lock(stateMutex); + legacyHaptics.Queue(source, begin, end, large, small); + if (RefreshAudioHaptics()) ++output.sequence; +} + +extern "C" void PadClearLegacyAudioHaptics_nid_postfix(int source) { + std::lock_guard lock(stateMutex); + legacyHaptics.Clear(source); + if (RefreshAudioHaptics()) ++output.sequence; +} diff --git a/core/libs/prx/libScePad/tests/InputCommandFile.cpp b/core/libs/prx/libScePad/tests/InputCommandFile.cpp new file mode 100644 index 0000000000..64309a4892 --- /dev/null +++ b/core/libs/prx/libScePad/tests/InputCommandFile.cpp @@ -0,0 +1,111 @@ +#include "prx/libScePad/include/InputCommandFile.hpp" +#include +#include +#include +#include +#include +#include + +void Check(bool condition) { + if (!condition) throw std::runtime_error("input command assertion failed"); +} + +int main() { + const auto path = std::filesystem::temp_directory_path() / + ("anyps5-pad-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + const auto write = [&](const std::string& text) { std::ofstream(path) << text; }; + try { + write("1 16384 300\n"); + Pad::InputCommandFile input(path); + Check(input.Buttons(0) == 0); + write("APS5_PAD_TIMELINE 1 2 3\n200 100 18432\n500 200 16\n800 100 0\n"); + Check(input.Buttons(100000) == 0); + Check(input.Buttons(299999) == 0); + Check(input.Buttons(300000) == 18432); + Check(input.Buttons(399999) == 18432); + Check(input.Buttons(400000) == 0); + Check(input.Buttons(600000) == 16); + Check(input.Buttons(800000) == 0); + Check(input.Buttons(1100000) == 0); + write("APS5_PAD_TIMELINE 1 3 2\n0 500 32768\n300 100 16\n"); + Check(input.Buttons(1200000) == 0); + write("APS5_PAD_TIMELINE 1 3 2\n0 500 32768\n600 100 16\n"); + Check(input.Buttons(1300000) == 32768); + write("APS5_PAD_TIMELINE 1 4 1\n0 6000 16\n"); + Check(input.Buttons(1400000) == 32768); + write("APS5_PAD_TIMELINE 1 4 1\n0 500 16\nextra\n"); + Check(input.Buttons(1500000) == 32768); + write("APS5_PAD_TIMELINE 1 4 2\n0 100 16\n"); + Check(input.Buttons(1600000) == 32768); + write("APS5_PAD_TIMELINE 2 4 1\n0 100 16\n"); + Check(input.Buttons(1700000) == 32768); + Check(input.Buttons(1800000) == 0); + write("4 8 200\n"); + Check(input.Buttons(1900000) == 8); + Check(input.Buttons(2100000) == 0); + write("3 16 200\n"); + Check(input.Buttons(2200000) == 0); + write("5 0 1\n"); + Check(input.Buttons(2300000) == 0); + const std::string bad[] = { + "APS5_PAD_TIMELINE 1 6 0\n", "APS5_PAD_TIMELINE 1 6 4097\n", + "APS5_PAD_TIMELINE 1 6 1\n0 1 1\n", "APS5_PAD_TIMELINE 1 6 1\n-1 1 16\n", + "APS5_PAD_TIMELINE 1 6 1\n3600000 1 16\n", + "APS5_PAD_TIMELINE 1 18446744073709551616 1\n0 1 16\n", + "APS5_PAD_TIMELINE 1 6 1\n18446744073709551615 1 16\n", + "APS5_PAD_TIMELINE 1 6 1\n0 1 4294967296\n" + }; + std::uint64_t clock = 2400000; + for (const auto& text : bad) { + write(text); + Check(input.Buttons(clock) == 0); + clock += 100000; + } + write("APS5_PAD_TIMELINE 1 6 1\n3599999 1 16384\n"); + Check(input.Buttons(clock) == 0); + Check(input.Buttons(clock + 3599999000ull) == 16384); + Check(input.Buttons(clock + 3600000000ull) == 0); + write("APS5_PAD_TIMELINE 1 7 1\n0 100 16\n"); + const auto maximum = std::numeric_limits::max(); + Check(input.Buttons(maximum - 10) == 16); + Check(input.Buttons(maximum) == 16); + Pad::InputCommandFile stale(path); + Check(stale.Buttons(0) == 0); + Pad::InputCommandFile disabled(std::filesystem::path{}); + Check(disabled.Buttons(0) == 0); + write("10 16384 300 0 255 128 64\n"); + Pad::InputCommandFile sticks(path); + Check(sticks.Poll(0) == Pad::InputCommandFile::Input{}); + write("11 16 200 0 255 128 64\n"); + Check(sticks.Poll(100000) == Pad::InputCommandFile::Input{16, true, {0, 255, 128, 64}}); + write("12 16 200 0 256 128 64\n"); + Check(sticks.Poll(200000) == Pad::InputCommandFile::Input{16, true, {0, 255, 128, 64}}); + Check(sticks.Poll(300000) == Pad::InputCommandFile::Input{}); + write("12 16 200 0 255 128 64 1\n"); + Check(sticks.Poll(400000) == Pad::InputCommandFile::Input{}); + write("APS5_PAD_TIMELINE 1 12 1\n0 500 16384\n"); + Check(sticks.Poll(500000) == Pad::InputCommandFile::Input{16384}); + write("13 8 200 255 0 64 128\n"); + Check(sticks.Poll(600000) == Pad::InputCommandFile::Input{8, true, {255, 0, 64, 128}}); + write("14 16 100\n"); + Check(sticks.Poll(700000) == Pad::InputCommandFile::Input{16}); + Check(sticks.Poll(800000) == Pad::InputCommandFile::Input{}); + write("15 2113536 100 0 255 128 64\n"); + Check(sticks.Poll(900000) == Pad::InputCommandFile::Input{16384, true, {0, 255, 128, 64}, true, false}); + Check(sticks.Poll(1000000) == Pad::InputCommandFile::Input{}); + write("APS5_PAD_TIMELINE 1 16 3\n0 100 2113536\n100 100 4194304\n200 100 6291456\n"); + Check(sticks.Poll(1100000) == Pad::InputCommandFile::Input{16384, false, {}, true, false}); + Check(sticks.Poll(1200000) == Pad::InputCommandFile::Input{0, false, {}, false, true}); + Check(sticks.Poll(1300000) == Pad::InputCommandFile::Input{0, false, {}, true, true}); + Check(sticks.Poll(1400000) == Pad::InputCommandFile::Input{}); + write("APS5_PAD_TIMELINE 1 17 1\n0 100 8388608\n"); + Check(sticks.Poll(1500000) == Pad::InputCommandFile::Input{}); + std::filesystem::remove(path); + std::cout << "Controller timeline runtime cases passed\n"; + return 0; + } catch (const std::exception& error) { + std::filesystem::remove(path); + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libScePadTracker/Export.cpp b/core/libs/prx/libScePadTracker/Export.cpp new file mode 100644 index 0000000000..e43ebed776 --- /dev/null +++ b/core/libs/prx/libScePadTracker/Export.cpp @@ -0,0 +1,38 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI scePadTrackerCalibrate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadTrackerGetWorkingMemorySize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadTrackerInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadTrackerReadState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadTrackerTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadTrackerUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libScePatchCheckerClient/Export.cpp b/core/libs/prx/libScePatchCheckerClient/Export.cpp new file mode 100644 index 0000000000..3cce60d2e7 --- /dev/null +++ b/core/libs/prx/libScePatchCheckerClient/Export.cpp @@ -0,0 +1,68 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI scePatchCheckerCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePatchCheckerCheckPatch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePatchCheckerCreateHandler() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePatchCheckerDestroyHandler() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePatchCheckerDisableAutoDownload() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePatchCheckerEnableAutoDownload() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePatchCheckerGetApplicableTick() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePatchCheckerGetPackageInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePatchCheckerInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePatchCheckerRequestCheckPatch() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePatchCheckerRequestCheckPatchByType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePatchCheckerTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libScePerf/Export.cpp b/core/libs/prx/libScePerf/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libScePerf/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libScePlayGo/Export.cpp b/core/libs/prx/libScePlayGo/Export.cpp index 64420b32d3..9fae1d940e 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 @@ -19,9 +20,12 @@ static constexpr int8_t PLAYGO_LOCUS_NOT_DOWNLOADED = 0; static constexpr int8_t PLAYGO_LOCUS_LOCAL_SLOW = 2; static constexpr int8_t PLAYGO_LOCUS_LOCAL_FAST = 3; static constexpr uint64_t PLAYGO_LANGUAGE_MASK_ALL = ~0ull; +static constexpr uint64_t PLAYGO_OPTIONAL_CHUNK_MASK_ALL = ~0ull; static constexpr int32_t PLAYGO_INSTALL_SPEED_FULL = 2; +static constexpr uint32_t PLAYGO_SCENARIO_CHUNK_COUNT = 1000; static int32_t g_installSpeed = PLAYGO_INSTALL_SPEED_FULL; +static uint64_t g_languageMask = PLAYGO_LANGUAGE_MASK_ALL; static constexpr int SCE_PLAYGO_ERROR_BAD_CHUNK_ID = static_cast(0x80B2000C); static constexpr int SCE_PLAYGO_ERROR_BAD_SIZE = static_cast(0x80B2000B); @@ -38,8 +42,8 @@ static bool ReadPackageChunks(ChunkTable& table) { std::ifstream file(ResolvePath_nid_no_patch("/app0/sce_sys/playgo-chunk.dat"), std::ios::binary); char header[CountOffset + sizeof(uint16_t)]{}; if (!file) return false; - const auto unreadable = [] { return std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat is not a chunk table this library reads (expected a plgx header with a nonzero chunk count)"); }; - if (!file.read(header, sizeof(header)) || std::memcmp(header, "plgx", 4) != 0) throw unreadable(); + const auto unreadable = [] { return std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat is not a chunk table this library reads (expected a plgx or plgo header with a nonzero chunk count)"); }; + if (!file.read(header, sizeof(header)) || (std::memcmp(header, "plgx", 4) != 0 && std::memcmp(header, "plgo", 4) != 0)) throw unreadable(); uint16_t count = 0; std::memcpy(&count, header + CountOffset, sizeof(count)); if (count == 0) throw unreadable(); @@ -66,6 +70,7 @@ static const ChunkTable& Chunks() { if (ReadPackageChunks(table)) return table; table.scenario = std::ifstream(ResolvePath_nid_no_patch("/app0/sce_sys/playgo-scenario.json")).good(); if (!table.scenario) RejectUnreadablePlayGoFiles(); + else for (uint32_t id = 1; id < PLAYGO_SCENARIO_CHUNK_COUNT; ++id) table.ids.insert(static_cast(id)); return table; } table.defined = true; @@ -85,9 +90,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(); - return chunks.defined ? chunks.ids.contains(id) : chunks.scenario || id == 0; + if (chunks.defined) return chunks.ids.contains(id); + if (chunks.scenario || id == 0) return true; + 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) { @@ -105,20 +131,21 @@ int APS5_VABI scePlayGoClose(int handle) { return 0; } -int APS5_VABI scePlayGoGetChunkId(int handle, uint16_t* out_chunk_id_list, uint32_t number_of_entries, uint32_t* out_entries) { +int APS5_VABI scePlayGoGetChunkId(int handle, uint16_t* outChunkIdList, uint32_t numberOfEntries, uint32_t* outEntries) { if (handle != PLAYGO_HANDLE) return SCE_PLAYGO_ERROR_BAD_HANDLE; - if (!out_entries) return SCE_PLAYGO_ERROR_BAD_POINTER; + if (!outEntries) return SCE_PLAYGO_ERROR_BAD_POINTER; const auto& valid = Chunks().ids; - if (!out_chunk_id_list) { - *out_entries = static_cast(valid.size()); + if (!outChunkIdList) { + *outEntries = static_cast(valid.size()); return 0; } + if (numberOfEntries == 0 || numberOfEntries > 1000) return SCE_PLAYGO_ERROR_BAD_SIZE; uint32_t written = 0; for (const auto id : valid) { - if (written == number_of_entries) break; - out_chunk_id_list[written++] = id; + if (written == numberOfEntries) break; + outChunkIdList[written++] = id; } - *out_entries = written; + *outEntries = written; return 0; } @@ -144,23 +171,24 @@ int APS5_VABI scePlayGoGetInstallSpeed(int handle, int32_t* out_speed) { int APS5_VABI scePlayGoGetLanguageMask(int handle, uint64_t* out_language_mask) { if (handle != PLAYGO_HANDLE) return SCE_PLAYGO_ERROR_BAD_HANDLE; if (!out_language_mask) return SCE_PLAYGO_ERROR_BAD_POINTER; - *out_language_mask = PLAYGO_LANGUAGE_MASK_ALL; + *out_language_mask = g_languageMask; return 0; } -int APS5_VABI scePlayGoGetLocus(int handle, const uint16_t* chunk_ids, uint32_t number_of_entries, int8_t* out_loci) { +int APS5_VABI scePlayGoGetLocus(int handle, const uint16_t* chunkIds, uint32_t numberOfEntries, int8_t* outLoci) { if (handle != PLAYGO_HANDLE) return SCE_PLAYGO_ERROR_BAD_HANDLE; - if (!out_loci) return SCE_PLAYGO_ERROR_BAD_POINTER; - if (const int error = ValidateChunks(chunk_ids, number_of_entries)) return error; - for (uint32_t index = 0; index < number_of_entries; ++index) out_loci[index] = PLAYGO_LOCUS_LOCAL_FAST; + if (!outLoci) return SCE_PLAYGO_ERROR_BAD_POINTER; + if (chunkIds && numberOfEntries > 1000) return SCE_PLAYGO_ERROR_BAD_SIZE; + if (const int error = ValidateChunks(chunkIds, numberOfEntries)) return error; + for (uint32_t index = 0; index < numberOfEntries; ++index) outLoci[index] = PLAYGO_LOCUS_LOCAL_FAST; return 0; } -int APS5_VABI scePlayGoGetOptionalChunk(int handle, int32_t type, PlayGoOptionalChunk* option) { +int APS5_VABI scePlayGoGetOptionalChunk(int handle, std::uint32_t type, PlayGoOptionalChunk* option) { if (handle != PLAYGO_HANDLE) return SCE_PLAYGO_ERROR_BAD_HANDLE; - (void)type; if (!option) return SCE_PLAYGO_ERROR_BAD_POINTER; - option->bitmask = 0; + if (type > 2) return static_cast(0x80B20024); + option->bitmask = PLAYGO_OPTIONAL_CHUNK_MASK_ALL; return 0; } @@ -173,11 +201,11 @@ int APS5_VABI scePlayGoGetProgress(int handle, const uint16_t* chunk_ids, uint32 return 0; } -int APS5_VABI scePlayGoGetSupportedOptionalChunk(int handle, int32_t type, PlayGoOptionalChunk* option) { +int APS5_VABI scePlayGoGetSupportedOptionalChunk(int handle, std::uint32_t type, PlayGoOptionalChunk* option) { if (handle != PLAYGO_HANDLE) return SCE_PLAYGO_ERROR_BAD_HANDLE; - (void)type; if (!option) return SCE_PLAYGO_ERROR_BAD_POINTER; - option->bitmask = 0; + if (type > 2) return static_cast(0x80B20024); + option->bitmask = PLAYGO_OPTIONAL_CHUNK_MASK_ALL; return 0; } @@ -237,4 +265,20 @@ int APS5_VABI scePlayGoTerminate(void) { return 0; } +int APS5_VABI scePlayGoRequestNextChunk() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayGoSetLanguageMask(int handle, uint64_t language_mask) { + if (handle != PLAYGO_HANDLE) return SCE_PLAYGO_ERROR_BAD_HANDLE; + g_languageMask = language_mask; + return 0; +} + +int APS5_VABI scePlayGoSnapshot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libScePlayGoDialog/Export.cpp b/core/libs/prx/libScePlayGoDialog/Export.cpp index 16615b02ee..e341d17b84 100644 --- a/core/libs/prx/libScePlayGoDialog/Export.cpp +++ b/core/libs/prx/libScePlayGoDialog/Export.cpp @@ -1,12 +1,15 @@ #include +#include +#include #include #include #include -#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" static_assert(sizeof(PlayGoDialogResult) == 40); +static_assert(offsetof(PlayGoDialogResult, result) == 4); +static_assert(offsetof(PlayGoDialogResult, reserved) == 8); namespace { @@ -16,11 +19,14 @@ constexpr std::int32_t STATUS_RUNNING = 2; constexpr std::int32_t STATUS_FINISHED = 3; constexpr std::int32_t RESULT_OK = 0; +constexpr std::int32_t ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr std::int32_t ERROR_NOT_RUNNING = static_cast(0x80B8000Bu); -constexpr std::int32_t ERROR_NOT_INITIALIZED = static_cast(0x80ED0001); -constexpr std::int32_t ERROR_ALREADY_INITIALIZED = static_cast(0x80ED0002); -constexpr std::int32_t ERROR_PARAM_INVALID = static_cast(0x80ED0003); -constexpr std::int32_t ERROR_INVALID_STATE = static_cast(0x80ED0005); +constexpr std::int32_t ERROR_NOT_INITIALIZED = static_cast(0x80B80003); +constexpr std::int32_t ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004); +constexpr std::int32_t ERROR_PARAM_INVALID = static_cast(0x80B8000A); +constexpr std::int32_t ERROR_INVALID_STATE = static_cast(0x80B80007); +constexpr std::int32_t COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); std::atomic g_status{STATUS_NONE}; @@ -44,7 +50,8 @@ int APS5_VABI scePlayGoDialogTerminate(void) { } int APS5_VABI scePlayGoDialogOpen(const void* param) { - if (!param) return ERROR_PARAM_INVALID; + if (!param) return ERROR_ARG_NULL; + if (g_status.load() == STATUS_NONE) return ERROR_NOT_INITIALIZED; int expected = STATUS_INITIALIZED; if (!g_status.compare_exchange_strong(expected, STATUS_FINISHED)) { expected = STATUS_FINISHED; @@ -55,6 +62,7 @@ int APS5_VABI scePlayGoDialogOpen(const void* param) { int APS5_VABI scePlayGoDialogClose(void) { if (g_status.load() == STATUS_NONE) return ERROR_NOT_INITIALIZED; + if (g_status.load() != STATUS_RUNNING) return ERROR_NOT_RUNNING; g_status.store(STATUS_FINISHED); return 0; } @@ -65,12 +73,13 @@ int APS5_VABI scePlayGoDialogUpdateStatus(void) { return g_status.load(); } -int APS5_VABI scePlayGoDialogGetResult(void* result) { +int APS5_VABI scePlayGoDialogGetResult(PlayGoDialogResult* result) { + if (result == nullptr) return ERROR_ARG_NULL; + if (!std::all_of(std::begin(result->reserved), std::end(result->reserved), [](std::uint8_t value) { return value == 0; })) return ERROR_PARAM_INVALID; if (g_status.load() == STATUS_NONE) return ERROR_NOT_INITIALIZED; - if (!result) return ERROR_PARAM_INVALID; - auto* r = static_cast(result); - std::memset(r, 0, sizeof(*r)); - r->result = RESULT_OK; + if (g_status.load() != STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + *result = {}; + result->result = RESULT_OK; return 0; } diff --git a/core/libs/prx/libScePlayReady/Export.cpp b/core/libs/prx/libScePlayReady/Export.cpp new file mode 100644 index 0000000000..59330d9d35 --- /dev/null +++ b/core/libs/prx/libScePlayReady/Export.cpp @@ -0,0 +1,258 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceBufferAlloc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBufferDup() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBufferFree() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBufferInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBufferRealloc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyApiInitializeInitParams() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyApiIsStarted() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyApiStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyApiStop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyApiVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyApiVersionString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyBufferFree() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyBufferInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyBufferMalloc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyBufferRealloc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyContentGetProperty() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyContentSetProperty() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyDomainCertEnumFini() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyDomainCertEnumInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyDomainCertEnumNext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyDomainCertFind() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyDomainJoinGenerateChallenge() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyDomainJoinProcessResponse() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyDomainLeaveGenerateChallenge() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyDomainLeaveProcessResponse() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyEnvelopeClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyEnvelopeGetSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyEnvelopeInitializeRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyEnvelopeOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyEnvelopeRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyEnvelopeSeek() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyFinalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyFreeDecryptContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyInitializeInitParams() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyLicenseAcqGenerateAck() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyLicenseAcqGenerateChallenge() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyLicenseAcqGetAdditionalResponseData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyLicenseAcqProcessAckResponse() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyLicenseAcqProcessResponse() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyMemAlloc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyMemFree() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyMemGetStats() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyMemRealloc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyNewDecryptContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyReaderBind() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyReaderCommit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyReaderDecrypt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyReset() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyStoreCleanup() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libScePlayReady2/Export.cpp b/core/libs/prx/libScePlayReady2/Export.cpp new file mode 100644 index 0000000000..2b08a30037 --- /dev/null +++ b/core/libs/prx/libScePlayReady2/Export.cpp @@ -0,0 +1,88 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI scePlayReadyBufferInitWithData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCloneDecryptContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyDebugPrintf() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyDebugSetLevel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyLicenseDelete() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyLicenseDeleteInMemory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyLicenseGetExpirationTime() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyLicenseGetProperty() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyReaderClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyReaderDecryptMultiple() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyReaderDecryptMultipleOpaque() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyReaderDecryptOpaque() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyReaderDecrypt_Legacy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyReaderFreeOpaqueContent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyReaderRetrieveOpaqueContent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyReinitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libScePlayReady4/Export.cpp b/core/libs/prx/libScePlayReady4/Export.cpp new file mode 100644 index 0000000000..0454139571 --- /dev/null +++ b/core/libs/prx/libScePlayReady4/Export.cpp @@ -0,0 +1,183 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI scePlayReadyApiPlatform() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiCreateMediaKeySession() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiCreateMediaKeySystemAccess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiCreateMediaKeys() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiDecryptMultipleOpaque() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiDecryptOpaque() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiDecryptOpaque2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiDestroyMediaKeySession() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiDestroyMediaKeySystemAccess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiDestroyMediaKeys() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiFreeOpaqueDecryptedContent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiGenerateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiGetConfiguration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiGetExpiration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiGetKeySystem() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiGetMediaKeyCount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiGetMediaKeyStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiGetSessionAppContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiGetSessionId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiIsLicenseAckRequired() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiLoad() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiRemove() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiRetrieveOpaqueDecryptedContent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiSetServerCertificate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiSetSessionCallbacks() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyCdmiUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyLicenseCount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyProfileClear() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyProfileEnter() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyProfileLeave() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyProfilePause() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyProfileResume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyProfileStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayReadyProfileStop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt b/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt new file mode 100644 index 0000000000..0d85abead0 --- /dev/null +++ b/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libScePlayerSelectionDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libScePlayerSelectionDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libScePlayerSelectionDialog) +target_link_libraries(libScePlayerSelectionDialog PRIVATE libSceUserService) + +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 libSceUserService libc) + configure_windows_unwind(guest_player_selection_dialog_tests) + add_test(NAME guest_player_selection_dialog COMMAND guest_player_selection_dialog_tests) + if(WIN32) + set_tests_properties(guest_player_selection_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp index e32913a7ad..06df01e1c8 100644 --- a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp +++ b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp @@ -1,11 +1,91 @@ +#include #include #include +#include #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" +extern "C" int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* users); + +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_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_INVALID_ARGUMENT = static_cast(0x80B8000Au); +constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast(0x80B8000Bu); +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 PlayerSelectionDialogParam* param) { + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (!ValidateCommonDialogBaseParam(param->base)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (param->size != sizeof(*param) || param->userId == -1) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + UserServiceLoginUserIdList users{}; + if (sceUserServiceGetLoginUserIdList(&users) != 0 || std::find(std::begin(users.user_id), std::end(users.user_id), param->userId) == std::end(users.user_id)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (param->maxPlayers > 256 || param->mode > 3 || param->playerCount > 256) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (param->playerCount != 0 && param->players == nullptr) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + for (std::uint32_t index = 0; index < param->playerCount; ++index) { + const auto& player = param->players[index]; + if (player.accountId == 0 || player.option0 > 1 || player.option1 > 1 || player.mode > 2 || !std::all_of(std::begin(player.reserved), std::end(player.reserved), [](std::uint8_t value) { return value == 0; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (param->maxPlayers == 1 && player.option0 == 1) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + } + if (!std::all_of(std::begin(param->reserved), std::end(param->reserved), [](std::uint8_t value) { return value == 0; })) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + 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; + if (g_status.load() != COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_NOT_RUNNING; + 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(PlayerSelectionDialogResult* result) { + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + return COMMON_DIALOG_RESULT_USER_CANCELED; +} + +int APS5_VABI scePlayerSelectionDialogParamInitialize(void* param) { + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + NotImplemented_nid_no_patch(__func__); +} + int APS5_VABI scePlayerSelectionDialogTerminate(void) { + if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; return 0; } diff --git a/core/libs/prx/libScePngDec/Export.cpp b/core/libs/prx/libScePngDec/Export.cpp index 220f2aaeec..8ff5827de0 100644 --- a/core/libs/prx/libScePngDec/Export.cpp +++ b/core/libs/prx/libScePngDec/Export.cpp @@ -8,6 +8,7 @@ #include #include "Decoder/Png.hpp" #include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" static_assert(sizeof(PngDecCreateParam) == 0xC); static_assert(sizeof(PngDecParseParam) == 0x10); @@ -27,8 +28,8 @@ constexpr std::int32_t SCE_PNG_DEC_ERROR_DECODE_ERROR = static_castthis_size != sizeof(PngDecCreateParam)) return SCE_PNG_DEC_ERROR_INVALID_SIZE; if (param->attribute != ATTRIBUTE_NONE && param->attribute != ATTRIBUTE_BIT_DEPTH_16) return SCE_PNG_DEC_ERROR_INVALID_PARAM; - if (param->max_image_width == 0 || param->max_image_width - 1 > MAX_IMAGE_WIDTH) return SCE_PNG_DEC_ERROR_INVALID_SIZE; + if (param->max_image_width - 1 >= MAX_IMAGE_WIDTH) return SCE_PNG_DEC_ERROR_INVALID_PARAM; return 0; } @@ -101,44 +108,57 @@ std::span toBytes(const void* data, std::uint32_t size) { return {static_cast(data), size}; } -} // namespace +} // namespace extern "C" { -int32_t APS5_VABI scePngDecCreate(const PngDecCreateParam* param, void* memory_address, uint32_t memory_size, void** handle) { +int32_t APS5_VABI scePngDecCreate(const PngDecCreateParam* param, void* memoryAddress, uint32_t memorySize, void** handle) { const std::int32_t result = validateCreateParam(param); if (result != 0) return result; - if (!memory_address || !handle) return SCE_PNG_DEC_ERROR_INVALID_ADDR; - if (memory_size < MEMORY_SIZE) return SCE_PNG_DEC_ERROR_INVALID_WORK_MEMORY; - const auto address = reinterpret_cast(memory_address); + if (!memoryAddress) return SCE_PNG_DEC_ERROR_INVALID_ADDR; + if (memorySize < requiredMemory(*param)) return SCE_PNG_DEC_ERROR_INVALID_SIZE; + if (!handle) return SCE_PNG_DEC_ERROR_INVALID_ADDR; + const auto address = reinterpret_cast(memoryAddress); const std::uintptr_t aligned = (address + HANDLE_ALIGNMENT - 1) & ~(HANDLE_ALIGNMENT - 1); - auto* context = new (reinterpret_cast(aligned)) Context{}; + auto* context = new (reinterpret_cast(aligned)) Context; context->self = context; + context->size = sizeof(PngDecCreateParam); context->attribute = param->attribute; - context->maxImageWidth = param->max_image_width; + context->maxImageWidth = (param->max_image_width + 7u) & ~7u; + context->memorySize = requiredMemory(*param) - 144u; *handle = context; return 0; } -int32_t APS5_VABI scePngDecDecode(void* handle, const PngDecDecodeParam* param, PngDecImageInfo* image_info) { +int32_t APS5_VABI scePngDecDecode(void* handle, const PngDecDecodeParam* param, PngDecImageInfo* imageInfo) { const Context* context = toContext(handle); if (!context) return SCE_PNG_DEC_ERROR_INVALID_HANDLE; - if (!param) return SCE_PNG_DEC_ERROR_INVALID_PARAM; - if (!param->png_mem_addr || !param->image_mem_addr) return SCE_PNG_DEC_ERROR_INVALID_ADDR; + if (!param) return SCE_PNG_DEC_ERROR_INVALID_ADDR; + if (!param->png_mem_addr || !param->image_mem_addr || (reinterpret_cast(param->image_mem_addr) & 3u) != 0) return SCE_PNG_DEC_ERROR_INVALID_ADDR; if (param->png_mem_size == 0 || param->image_mem_size == 0) return SCE_PNG_DEC_ERROR_INVALID_SIZE; + if (param->pixel_format == 132) throw std::runtime_error("scePngDecDecode: pixel format 132 is not implemented"); if (param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) return SCE_PNG_DEC_ERROR_INVALID_PARAM; + if (param->alpha_value > 0xFF) return SCE_PNG_DEC_ERROR_INVALID_PARAM; const std::span png = toBytes(param->png_mem_addr, param->png_mem_size); const std::optional header = Decoder::Png::ParseHeader(png); if (!header) return SCE_PNG_DEC_ERROR_INVALID_DATA; + if (imageInfo) fillImageInfo(*header, imageInfo); if (header->bitDepth == 16 && context->attribute == ATTRIBUTE_BIT_DEPTH_16) { throw std::runtime_error("scePngDecDecode: 16-bit output is not implemented"); } const std::uint64_t rowSize = static_cast(header->width) * BYTES_PER_PIXEL; const std::uint64_t pitch = param->image_pitch == 0 ? rowSize : param->image_pitch; - if (pitch < rowSize) return SCE_PNG_DEC_ERROR_INVALID_PARAM; - if ((header->height - 1) * pitch + rowSize > param->image_mem_size) return SCE_PNG_DEC_ERROR_INVALID_SIZE; + if (param->image_pitch != 0 && (pitch < rowSize || (pitch & 3u) != 0)) return SCE_PNG_DEC_ERROR_INVALID_PARAM; + if (param->image_pitch == 0) { + if (header->height * rowSize > param->image_mem_size) return SCE_PNG_DEC_ERROR_INVALID_SIZE; + } else if (header->height * pitch >= 0x80000000ull || header->height * pitch > param->image_mem_size) { + return SCE_PNG_DEC_ERROR_INVALID_PARAM; + } + const std::uint64_t alignedWidth = (static_cast(header->width) + 7u) & ~std::uint64_t{7u}; + const std::uint64_t alignedMaxWidth = (static_cast(context->maxImageWidth) + 7u) & ~std::uint64_t{7u}; + if (alignedWidth > alignedMaxWidth) return SCE_PNG_DEC_ERROR_INVALID_WORK_MEMORY; const std::optional image = Decoder::Png::Decode(png); if (!image || image->width != header->width || image->height != header->height) return SCE_PNG_DEC_ERROR_DECODE_ERROR; @@ -158,7 +178,6 @@ int32_t APS5_VABI scePngDecDecode(void* handle, const PngDecDecodeParam* param, } } - if (image_info) fillImageInfo(*header, image_info); if (header->width > MAX_PACKED_DIMENSION || header->height > MAX_PACKED_DIMENSION) return 0; return static_cast(header->width << 16 | header->height); } @@ -170,20 +189,27 @@ int32_t APS5_VABI scePngDecDelete(void* handle) { return 0; } -int32_t APS5_VABI scePngDecParseHeader(const PngDecParseParam* param, PngDecImageInfo* image_info) { - if (!param) return SCE_PNG_DEC_ERROR_INVALID_PARAM; - if (!param->png_mem_addr || !image_info) return SCE_PNG_DEC_ERROR_INVALID_ADDR; +int32_t APS5_VABI scePngDecParseHeader(const PngDecParseParam* param, PngDecImageInfo* imageInfo) { + if (!param) return SCE_PNG_DEC_ERROR_INVALID_ADDR; + if (!param->png_mem_addr) return SCE_PNG_DEC_ERROR_INVALID_ADDR; if (param->png_mem_size == 0) return SCE_PNG_DEC_ERROR_INVALID_SIZE; + if (param->reserved0 != 0) return SCE_PNG_DEC_ERROR_INVALID_PARAM; + if (!imageInfo) return SCE_PNG_DEC_ERROR_INVALID_ADDR; const std::optional header = Decoder::Png::ParseHeader(toBytes(param->png_mem_addr, param->png_mem_size)); if (!header) return SCE_PNG_DEC_ERROR_INVALID_DATA; - fillImageInfo(*header, image_info); + fillImageInfo(*header, imageInfo); return 0; } int32_t APS5_VABI scePngDecQueryMemorySize(const PngDecCreateParam* param) { const std::int32_t result = validateCreateParam(param); if (result != 0) return result; - return static_cast(MEMORY_SIZE); + return static_cast(requiredMemory(*param)); +} + +int APS5_VABI scePngDecDecodeWithInputControl() { + NotImplemented_nid_no_patch(__func__); + return 0; } } diff --git a/core/libs/prx/libScePngEnc/Export.cpp b/core/libs/prx/libScePngEnc/Export.cpp index 4ff0af04e3..c6e3c9532a 100644 --- a/core/libs/prx/libScePngEnc/Export.cpp +++ b/core/libs/prx/libScePngEnc/Export.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include #include @@ -18,7 +20,7 @@ constexpr int PNG_ENC_ERROR_FATAL = static_cast(0x80690120); constexpr std::uint32_t MAX_IMAGE_WIDTH = 1000000; constexpr std::uint32_t MAX_IMAGE_HEIGHT = 1000000; -constexpr std::uint32_t MAX_FILTER_NUMBER = 5; +constexpr std::uint32_t MAX_FILTER_NUMBER = 4; constexpr std::uint16_t COLOR_SPACE_RGB = 3; constexpr std::uint16_t COLOR_SPACE_RGBA = 19; constexpr std::uint16_t PIXEL_FORMAT_R8G8B8A8 = 0; @@ -28,12 +30,15 @@ constexpr std::uint16_t FILTER_UP = 2; constexpr std::uint16_t FILTER_AVERAGE = 4; constexpr std::uint16_t FILTER_PAETH = 8; constexpr std::uint16_t FILTER_ALL = FILTER_SUB | FILTER_UP | FILTER_AVERAGE | FILTER_PAETH; -constexpr std::uint64_t CONTEXT_MAGIC = 0x434E45474E505341ull; +constexpr std::uintptr_t HANDLE_ALIGNMENT = 16; struct PngEncContext { - std::uint64_t magic; - std::uint32_t maxImageWidth; + PngEncContext* self; + std::uint32_t size; std::uint32_t reserved; + std::uint32_t maxImageWidth; + std::uint32_t maxFilterNumber; + std::uint32_t scratchSize; }; static_assert(sizeof(PngEncCreateParam) == 16); @@ -42,20 +47,25 @@ static_assert(sizeof(PngEncOutputInfo) == 8); int validateCreateParam(const PngEncCreateParam* param) { if (!param) return PNG_ENC_ERROR_INVALID_ADDR; - if (param->attribute != 0 || param->max_filter_number > MAX_FILTER_NUMBER) return PNG_ENC_ERROR_INVALID_PARAM; - if (param->max_image_width == 0 || param->max_image_width > MAX_IMAGE_WIDTH) return PNG_ENC_ERROR_INVALID_SIZE; + if (param->this_size != sizeof(PngEncCreateParam)) return PNG_ENC_ERROR_INVALID_SIZE; + if (param->attribute != 0 || param->max_image_width == 0 || param->max_image_width > MAX_IMAGE_WIDTH || param->max_filter_number > MAX_FILTER_NUMBER) return PNG_ENC_ERROR_INVALID_PARAM; return 0; } +std::uint32_t workMemorySize(const PngEncCreateParam& param) { + return 285808 + ((4 * param.max_image_width + 28) & ~std::uint32_t{31}) * (param.max_filter_number + 2); +} + PngEncContext* context(void* handle) { + if (reinterpret_cast(handle) % HANDLE_ALIGNMENT != 0) return nullptr; auto* ctx = static_cast(handle); - return ctx && ctx->magic == CONTEXT_MAGIC ? ctx : nullptr; + return ctx && ctx->self == ctx ? ctx : nullptr; } std::uint8_t filterSet(std::uint16_t filterType) { if (filterType == FILTER_ALL) return Decoder::Png::FILTER_ALL; if (filterType == 0) return Decoder::Png::FILTER_NONE; - std::uint8_t filters = 0; + std::uint8_t filters = Decoder::Png::FILTER_NONE; if (filterType & FILTER_SUB) filters |= Decoder::Png::FILTER_SUB; if (filterType & FILTER_UP) filters |= Decoder::Png::FILTER_UP; if (filterType & FILTER_AVERAGE) filters |= Decoder::Png::FILTER_AVERAGE; @@ -69,41 +79,50 @@ extern "C" { int APS5_VABI scePngEncQueryMemorySize(const PngEncCreateParam* param) { if (const int result = validateCreateParam(param); result != 0) return result; - return sizeof(PngEncContext); + return static_cast(workMemorySize(*param)); } int APS5_VABI scePngEncCreate(const PngEncCreateParam* param, void* memoryAddress, uint32_t memorySize, void** handle) { if (const int result = validateCreateParam(param); result != 0) return result; - if (!memoryAddress || !handle) return PNG_ENC_ERROR_INVALID_ADDR; - if (memorySize < sizeof(PngEncContext)) return PNG_ENC_ERROR_INVALID_SIZE; - *static_cast(memoryAddress) = {CONTEXT_MAGIC, param->max_image_width, 0}; - *handle = memoryAddress; + if (!memoryAddress) return PNG_ENC_ERROR_INVALID_ADDR; + const std::uint32_t requiredSize = workMemorySize(*param); + if (memorySize < requiredSize) return PNG_ENC_ERROR_INVALID_SIZE; + if (!handle) return PNG_ENC_ERROR_INVALID_ADDR; + const std::uintptr_t aligned = (reinterpret_cast(memoryAddress) + HANDLE_ALIGNMENT - 1) & ~(HANDLE_ALIGNMENT - 1); + auto* ctx = new (reinterpret_cast(aligned)) PngEncContext{}; + ctx->self = ctx; + ctx->size = sizeof(PngEncCreateParam); + ctx->maxImageWidth = (param->max_image_width + 7) & ~std::uint32_t{7}; + ctx->maxFilterNumber = param->max_filter_number; + ctx->scratchSize = requiredSize - 112; + *handle = ctx; return 0; } int APS5_VABI scePngEncDelete(void* handle) { PngEncContext* ctx = context(handle); if (!ctx) return PNG_ENC_ERROR_INVALID_HANDLE; - ctx->magic = 0; + ctx->self = nullptr; return 0; } int APS5_VABI scePngEncEncode(void* handle, const PngEncEncodeParam* param, PngEncOutputInfo* outputInfo) { const PngEncContext* ctx = context(handle); if (!ctx) return PNG_ENC_ERROR_INVALID_HANDLE; - if (!param) return PNG_ENC_ERROR_INVALID_PARAM; - if (!param->image_mem_addr || !param->png_mem_addr) return PNG_ENC_ERROR_INVALID_ADDR; - if ((param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) - || (param->color_space != COLOR_SPACE_RGB && param->color_space != COLOR_SPACE_RGBA) || param->bit_depth != 8 - || param->clut_number != 0 || (param->filter_type & ~FILTER_ALL) != 0 || param->compression_level > 9) { - return PNG_ENC_ERROR_INVALID_PARAM; - } - + if (!param) return PNG_ENC_ERROR_INVALID_ADDR; + if (!param->image_mem_addr || reinterpret_cast(param->image_mem_addr) % 4 != 0 || !param->png_mem_addr) return PNG_ENC_ERROR_INVALID_ADDR; + if (param->image_mem_size == 0 || param->png_mem_size == 0) return PNG_ENC_ERROR_INVALID_SIZE; const std::uint32_t width = param->image_width; const std::uint32_t height = param->image_height; - if (width == 0 || height == 0 || width > ctx->maxImageWidth || height > MAX_IMAGE_HEIGHT || param->png_mem_size == 0 - || param->image_pitch < width * 4 || static_cast(param->image_pitch) * (height - 1) + width * 4 > param->image_mem_size) { - return PNG_ENC_ERROR_INVALID_SIZE; + const std::uint64_t inputSize = static_cast(param->image_pitch) * height; + if (width == 0 || width > ctx->maxImageWidth || height == 0 || height > MAX_IMAGE_HEIGHT + || param->image_pitch == 0 || param->image_pitch % 4 != 0 || param->image_pitch > 0x0FFFFFFF || param->image_pitch / 4 < width + || inputSize > 0x7FFFFFFF || inputSize > param->image_mem_size) return PNG_ENC_ERROR_INVALID_PARAM; + if ((param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) + || (param->color_space != COLOR_SPACE_RGB && param->color_space != COLOR_SPACE_RGBA) || param->bit_depth != 8 + || param->clut_number != 0 || (param->filter_type & ~FILTER_ALL) != 0 + || static_cast(std::popcount(param->filter_type)) > ctx->maxFilterNumber || param->compression_level > 9) { + return PNG_ENC_ERROR_INVALID_PARAM; } const std::uint32_t channels = param->color_space == COLOR_SPACE_RGBA ? 4 : 3; diff --git a/core/libs/prx/libScePngParser/Export.cpp b/core/libs/prx/libScePngParser/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libScePngParser/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libScePosixForWebKit/Export.cpp b/core/libs/prx/libScePosixForWebKit/Export.cpp index 4ff202b864..c02c94fd7a 100644 --- a/core/libs/prx/libScePosixForWebKit/Export.cpp +++ b/core/libs/prx/libScePosixForWebKit/Export.cpp @@ -1,11 +1,22 @@ +#include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/HostEntropy.hpp" +#include "prx/libc/include/General.hpp" #include #include #include #include #include +extern "C" { +int APS5_VABI access_nid_postfix(const char*, int); +int APS5_VABI clock_gettime_nid_postfix(int, KernelTimespec*); +char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); +char* APS5_VABI getenv_nid_postfix(const char*); +int APS5_VABI lstat_nid_postfix(const char*, FileStat*); +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +} + namespace { constexpr int FnmNomatch = 1; @@ -180,4 +191,95 @@ std::uint32_t APS5_VABI arc4random_nid_postfix() { return value; } +int APS5_VABI __wrap_access(const char* path, int mode) { + return access_nid_postfix(path, mode); +} + +int APS5_VABI __wrap_clock_gettime(int clockId, KernelTimespec* tp) { + return clock_gettime_nid_postfix(clockId, tp); +} + +int APS5_VABI __wrap_fchown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +char* APS5_VABI __wrap_getcwd(char* buffer, std::size_t size) { + return getcwd_nid_postfix(buffer, size); +} + +char* APS5_VABI __wrap_getenv(const char* name) { + return getenv_nid_postfix(name); +} + +int APS5_VABI __wrap_link() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI __wrap_lstat(const char* path, FileStat* sb) { + return lstat_nid_postfix(path, sb); +} + +void* APS5_VABI __wrap_mmap(void* address, std::size_t length, int protection, int flags, int descriptor, std::int64_t offset) { + return mmap_nid_postfix(address, length, protection, flags, descriptor, offset); +} + +int APS5_VABI __wrap_readlink() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI __wrap_statvfs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI __wrap_symlink() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI clock_gettime_np() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI getenv_np() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +APS5_EXPORT("dt8ermYp-Wc", scePosixForWebKit_gethostbyname_r); +int APS5_VABI scePosixForWebKit_gethostbyname_r() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI mmap_np() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI msetname() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_setschedparam_np() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI setenv_np() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +APS5_EXPORT("9Vqxt-qBPB4", scePosixForWebKit_sysinfo); +int APS5_VABI scePosixForWebKit_sysinfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libScePosixForWebKit/Resolver.cpp b/core/libs/prx/libScePosixForWebKit/Resolver.cpp index 0dd6901832..f9b8a2acc2 100644 --- a/core/libs/prx/libScePosixForWebKit/Resolver.cpp +++ b/core/libs/prx/libScePosixForWebKit/Resolver.cpp @@ -182,9 +182,17 @@ GuestResolver::HostEntry* APS5_VABI gethostbyname_nid_postfix(const char* name) int hostError = 0; int error = 0; if (!numeric) { +#ifdef __APPLE__ + native = ::gethostbyname(name); + if (native == nullptr) { + hostError = h_errno; + error = hostError == TRY_AGAIN ? EAGAIN : 0; + } +#else buffer.resize(1024); while ((error = ::gethostbyname_r(name, &nativeEntry, buffer.data(), buffer.size(), &native, &hostError)) == ERANGE) buffer.resize(buffer.size() * 2); +#endif } if (error != 0 || !native) { *sceNetErrnoLoc() = hostError != 0 ? hostError : 3; 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/libScePs2EmuMenuDialog/Export.cpp b/core/libs/prx/libScePs2EmuMenuDialog/Export.cpp new file mode 100644 index 0000000000..99db1f489a --- /dev/null +++ b/core/libs/prx/libScePs2EmuMenuDialog/Export.cpp @@ -0,0 +1,43 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI scePs2EmuMenuDialogClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePs2EmuMenuDialogGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePs2EmuMenuDialogGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePs2EmuMenuDialogInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePs2EmuMenuDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePs2EmuMenuDialogTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePs2EmuMenuDialogUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libScePsm/Export.cpp b/core/libs/prx/libScePsm/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libScePsm/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libScePsmUtil/Export.cpp b/core/libs/prx/libScePsmUtil/Export.cpp new file mode 100644 index 0000000000..0a7d7939be --- /dev/null +++ b/core/libs/prx/libScePsmUtil/Export.cpp @@ -0,0 +1,28 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI scePsmUtilFinalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePsmUtilGetPsmIniFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePsmUtilInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePsmUtilRegisterCallbacks() { + 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 bec0b00b8e..44021762c2 100644 --- a/core/libs/prx/libScePsml_debug/Export.cpp +++ b/core/libs/prx/libScePsml_debug/Export.cpp @@ -1,11 +1,209 @@ #include #include +#include +#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::int32_t SCE_PSML_ERROR_INVALID_ARGUMENT = static_cast(0x8A810005); +constexpr std::int32_t SCE_PSML_ERROR_NULL_OUTPUT = static_cast(0x8A810009); +constexpr std::uint64_t MfsrBlockBytes = 0x200000; +constexpr std::uint64_t MfsrBlockCount = 4; +constexpr std::uint32_t MfsrDispatchPacketDwords = 0x600; +constexpr std::uint64_t MfsrSharedBlockCount = 52; + +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 Mfsr2SharedResourcesParam { + std::uint32_t type; + std::uint32_t reserved0; + const MfsrBlock* blocks; + std::uint64_t blockCount; + std::uint16_t width; + std::uint16_t height; + std::uint32_t reserved1; +}; + +static_assert(sizeof(Mfsr2SharedResourcesParam) == 32); +static_assert(offsetof(Mfsr2SharedResourcesParam, blocks) == 8); +static_assert(offsetof(Mfsr2SharedResourcesParam, blockCount) == 16); +static_assert(offsetof(Mfsr2SharedResourcesParam, width) == 24); +static_assert(offsetof(Mfsr2SharedResourcesParam, reserved1) == 28); + +struct Mfsr2ContextParam { + std::uint32_t profile; + std::uint32_t reserved0; + void* sharedResources; + const MfsrBlock* blocks; + std::uint64_t blockCount; + std::uint64_t reserved1; + std::uint16_t outputWidth; + std::uint16_t outputHeight; + std::uint32_t inputColorSpace; +}; + +struct MfsrCommandBuffer { + std::uint32_t* cursor; + std::uint32_t sizeInDwords; +}; + +struct MfsrTextureDescriptor { + std::uint32_t words[8]; +}; + +struct Mfsr2DispatchParam { + const MfsrTextureDescriptor* outputColor; + const MfsrTextureDescriptor* color; + const MfsrTextureDescriptor* motionVectors; + const MfsrTextureDescriptor* depth; + const MfsrTextureDescriptor* exposure; + std::uint64_t reserved0; + float preExposure; + std::uint32_t renderX; + std::uint32_t renderY; + std::uint32_t renderWidth; + std::uint32_t renderHeight; + float jitter[2]; + float motionVectorScale[2]; + float motionVectorOffset[2]; + float depthRange[2]; + std::uint8_t resetHistory; + std::uint8_t reserved1[3]; + std::uint32_t optionFlags; + float sharpness; +}; + +static_assert(sizeof(Mfsr2ContextParam) == 48); +static_assert(offsetof(Mfsr2ContextParam, sharedResources) == 8); +static_assert(offsetof(Mfsr2ContextParam, blocks) == 16); +static_assert(offsetof(Mfsr2ContextParam, blockCount) == 24); +static_assert(offsetof(Mfsr2ContextParam, outputWidth) == 40); +static_assert(offsetof(Mfsr2ContextParam, inputColorSpace) == 44); +static_assert(sizeof(MfsrCommandBuffer) == 16); +static_assert(offsetof(MfsrCommandBuffer, sizeInDwords) == 8); +static_assert(sizeof(MfsrTextureDescriptor) == 32); +static_assert(sizeof(Mfsr2DispatchParam) == 112); +static_assert(offsetof(Mfsr2DispatchParam, preExposure) == 48); +static_assert(offsetof(Mfsr2DispatchParam, renderX) == 52); +static_assert(offsetof(Mfsr2DispatchParam, jitter) == 68); +static_assert(offsetof(Mfsr2DispatchParam, depthRange) == 92); +static_assert(offsetof(Mfsr2DispatchParam, resetHistory) == 100); +static_assert(offsetof(Mfsr2DispatchParam, optionFlags) == 104); +static_assert(offsetof(Mfsr2DispatchParam, sharpness) == 108); + +struct MfsrContextParam { + std::uint32_t reserved0[2]; + const MfsrBlock* blocks; + std::uint64_t blockCount; + std::uint16_t inputWidth; + std::uint16_t inputHeight; + std::uint32_t reserved1; + const void* descriptor0; + const void* descriptor1; + std::uint32_t reserved2[2]; + 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, inputWidth) == 0x18); +static_assert(offsetof(MfsrContextParam, descriptor0) == 0x20); +static_assert(offsetof(MfsrContextParam, descriptor1) == 0x28); +static_assert(offsetof(MfsrContextParam, sharedResources) == 0x38); + +enum class MfsrObjectKind { SharedResources, Context }; + +struct MfsrObject { + MfsrObjectKind kind; + void* sharedResources; + std::uint32_t inputWidth; + std::uint32_t inputHeight; +}; + +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, std::uint64_t requiredCount, const char* function) { + RequireMfsr(blockCount >= requiredCount, function, "fewer memory blocks than the requirement"); + RequireMfsr(blocks != nullptr, function, "null memory block list"); + for (std::uint64_t i = 0; i < requiredCount; ++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::uint32_t inputWidth = 0, std::uint32_t inputHeight = 0) { + 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, inputWidth, inputHeight}; + g_mfsrObjects.emplace(object, object); + return object; +} + +bool FindMfsrObject(void* handle, MfsrObject* out) { + std::lock_guard lock(g_mfsrMutex); + const auto found = g_mfsrObjects.find(handle); + if (found == g_mfsrObjects.end()) return false; + *out = *found->second; + return true; +} + +std::pair MfsrInputDimensions(const MfsrContextParam* param) { + if (param->inputWidth != 0) return {param->inputWidth, param->inputHeight}; + if (param->descriptor0 == nullptr) return {0, 0}; + std::uint16_t width = 0, height = 0; + std::memcpy(&width, static_cast(param->descriptor0) + 0x20, sizeof(width)); + std::memcpy(&height, static_cast(param->descriptor0) + 0x22, sizeof(height)); + return {width, height}; +} + +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 +212,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,105 +227,147 @@ 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, MfsrSharedBlockCount}; 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, MfsrSharedBlockCount, __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; } -int APS5_VABI scePsmlMfsrGetDispatchMfsrPacketSizeInDwords() { - NotImplemented_nid_no_patch(__func__); +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; } -int APS5_VABI scePsmlMfsrGetContextBufferRequirement800M3_2() { - NotImplemented_nid_no_patch(__func__); +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, MfsrBlockCount, __func__); + const auto [inputWidth, inputHeight] = MfsrInputDimensions(param); + *context = CreateMfsrObject(MfsrObjectKind::Context, param->sharedResources, __func__, inputWidth, inputHeight); return 0; } -int APS5_VABI scePsmlMfsrSelectConfig() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI scePsmlMfsrReleaseContext(void* context) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + ReleaseMfsrObject(context, MfsrObjectKind::Context, __func__); return 0; } -int APS5_VABI scePsmlMfsrGetMipmapBias() { - NotImplemented_nid_no_patch(__func__); +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; +} + +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacketSizeInDwords(void* context, std::uint32_t* packetSizeInDwords) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + MfsrObject object; + if (!FindMfsrObject(context, &object)) return SCE_PSML_ERROR_INVALID_ARGUMENT; + if (packetSizeInDwords == nullptr) return SCE_PSML_ERROR_NULL_OUTPUT; + *packetSizeInDwords = MfsrDispatchPacketDwords; return 0; } -int APS5_VABI scePsmlMfsrRequestCapture() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI scePsmlMfsrSelectConfig(void* config) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + MfsrObject object; + if (!FindMfsrObject(config, &object)) return SCE_PSML_ERROR_INVALID_ARGUMENT; return 0; } -int APS5_VABI scePsmlMfsrReleaseContext() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI scePsmlMfsrGetMipmapBias(void* context, float* bias, std::uint32_t dimension) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + MfsrObject object; + if (!FindMfsrObject(context, &object) || object.kind != MfsrObjectKind::Context) return SCE_PSML_ERROR_INVALID_ARGUMENT; + if (bias == nullptr) return SCE_PSML_ERROR_NULL_OUTPUT; + if (object.inputWidth == 0) return SCE_PSML_ERROR_INVALID_ARGUMENT; + *bias = std::log2f(static_cast(dimension) / static_cast(object.inputWidth)) - 1.0f; return 0; } -int APS5_VABI scePsmlMfsrIsCaptureInProgress() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI scePsmlMfsrRequestCapture(void* context) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + MfsrObject object; + if (!FindMfsrObject(context, &object)) return SCE_PSML_ERROR_INVALID_ARGUMENT; return 0; } -int APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1000() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI scePsmlMfsrIsCaptureInProgress(void* context) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + MfsrObject object; + if (!FindMfsrObject(context, &object)) return SCE_PSML_ERROR_INVALID_ARGUMENT; return 0; } -int APS5_VABI scePsmlMfsrGetDispatchMfsrPacket900() { - 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 scePsmlMfsrCreateSharedResources() { - 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 scePsmlMfsrCreateContext800M3_2() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI scePsmlMfsrReleaseSharedResources() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI scePsmlMfsr2ReleaseSharedResources() { +int APS5_VABI scePsmlMfsr2ReleaseSharedResources(void* sharedResources) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePsmlMfsr2GetSharedResourcesInitRequirement() { +int APS5_VABI scePsmlMfsr2GetSharedResourcesInitRequirement(MfsrRequirement* requirement, const Mfsr2SharedResourcesParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePsmlMfsr2GetContextInitRequirement() { +int APS5_VABI scePsmlMfsr2GetContextInitRequirement(MfsrRequirement* requirement, const Mfsr2ContextParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePsmlMfsr2ReleaseContext() { +int APS5_VABI scePsmlMfsr2ReleaseContext(void* context) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -135,17 +377,17 @@ int APS5_VABI scePsmlMfsr2GetDispatchPacketsSizeInDwords() { return 0; } -int APS5_VABI scePsmlMfsr2CreateSharedResources() { +int APS5_VABI scePsmlMfsr2CreateSharedResources(void** sharedResources, const Mfsr2SharedResourcesParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePsmlMfsr2GetDispatchPackets() { +int APS5_VABI scePsmlMfsr2GetDispatchPackets(void* context, const MfsrCommandBuffer* commandBuffer, const Mfsr2DispatchParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePsmlMfsr2CreateContext() { +int APS5_VABI scePsmlMfsr2CreateContext(void** context, const Mfsr2ContextParam* param) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -155,15 +397,13 @@ int APS5_VABI scePsmlMfsr2Init() { return 0; } -APS5_EXPORT("xNZM9Y5lf-M", scePsmlUnknown01); -int APS5_VABI scePsmlUnknown01(void) { - NotImplemented_nid_no_patch("xNZM9Y5lf-M"); +int APS5_VABI scePsmlMfsrEnableSpecularAA() { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("lcZD9tJraAw", scePsmlUnknown02); -int APS5_VABI scePsmlUnknown02(void) { - NotImplemented_nid_no_patch("lcZD9tJraAw"); +int APS5_VABI scePsmlMfsrEnableQuality8K() { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceRazorCpu/CMakeLists.txt b/core/libs/prx/libSceRazorCpu/CMakeLists.txt index 38dc075ae6..e4d7808ed9 100644 --- a/core/libs/prx/libSceRazorCpu/CMakeLists.txt +++ b/core/libs/prx/libSceRazorCpu/CMakeLists.txt @@ -6,6 +6,7 @@ if(BUILD_TESTING) add_test_executable(guest_razor_cpu_tests ${LIBS_INCLUDE_DIR}/tests/GuestRazorCpu.cpp) target_include_directories(guest_razor_cpu_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_razor_cpu_tests PRIVATE libSceRazorCpu) + configure_windows_unwind(guest_razor_cpu_tests) add_test(NAME guest_razor_cpu COMMAND guest_razor_cpu_tests) if(WIN32) set_tests_properties(guest_razor_cpu PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceRazorCpu/Export.cpp b/core/libs/prx/libSceRazorCpu/Export.cpp index 23369d1158..defd9d798b 100644 --- a/core/libs/prx/libSceRazorCpu/Export.cpp +++ b/core/libs/prx/libSceRazorCpu/Export.cpp @@ -9,18 +9,30 @@ uint32_t APS5_VABI sceRazorCpuIsCapturing(void) { return 0; } -int APS5_VABI sceRazorCpuJobManagerDispatch(const void* args) { - (void)args; +int APS5_VABI sceRazorCpuJobManagerDispatch(const char* label, uint16_t firstId, uint16_t secondId, uint16_t firstCount, uint16_t secondCount, uint64_t firstValue, uint64_t secondValue) { + (void)label; + (void)firstId; + (void)secondId; + (void)firstCount; + (void)secondCount; + (void)firstValue; + (void)secondValue; return 0; } -int APS5_VABI sceRazorCpuJobManagerJob(const void* args) { - (void)args; +int APS5_VABI sceRazorCpuJobManagerJob(uint16_t firstId, uint16_t secondId, uint16_t thirdId, uint16_t event) { + (void)firstId; + (void)secondId; + (void)thirdId; + (void)event; return 0; } -int APS5_VABI sceRazorCpuJobManagerSequence(const void* args) { - (void)args; +int APS5_VABI sceRazorCpuJobManagerSequence(const char* label, uint16_t firstId, uint16_t secondId, uint16_t event) { + (void)label; + (void)firstId; + (void)secondId; + (void)event; return 0; } @@ -31,6 +43,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; @@ -42,9 +59,9 @@ int APS5_VABI sceRazorCpuPopMarker(void) { return 0; } -int APS5_VABI sceRazorCpuFlushOccurred(uint64_t* timeSpentInFlush) { - if (timeSpentInFlush != nullptr) *timeSpentInFlush = 0; - return 0; +bool APS5_VABI sceRazorCpuFlushOccurred(uint64_t* timeSpentInFlush) { + (void)timeSpentInFlush; + return false; } int APS5_VABI sceRazorCpuPlotValue(const char* series, float value) { @@ -63,4 +80,129 @@ int APS5_VABI sceRazorCpuSync(void) { return 0; } +void APS5_VABI sceRazorCpuDisableFiberUserMarkers(void) { +} + +int APS5_VABI sceRazorCpuInitDataTags(void* buffer, size_t size) { + (void)buffer; + (void)size; + return 0; +} + +int APS5_VABI sceRazorCpuShutdownDataTags(void) { + return 0; +} + +size_t APS5_VABI sceRazorCpuGetDataTagStorageSize(uint32_t tagCount) { + (void)tagCount; + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceRazorCpuTagArray(const char* label, uint32_t category, void* array, size_t bufferSize, size_t elementSize) { + (void)label; + (void)category; + (void)array; + (void)bufferSize; + (void)elementSize; + return 0; +} + +int APS5_VABI sceRazorCpuTagBuffer(const char* label, uint32_t category, void* buffer, size_t bufferSize) { + (void)label; + (void)category; + (void)buffer; + (void)bufferSize; + return 0; +} + +int APS5_VABI sceRazorCpuResizeTaggedBuffer(uint32_t category, void* buffer, size_t newBufferSize) { + (void)category; + (void)buffer; + (void)newBufferSize; + return 0; +} + +int APS5_VABI sceRazorCpuUnTagBuffer(uint32_t category, void* buffer) { + (void)category; + (void)buffer; + return 0; +} + +int APS5_VABI sceRazorCpuBeginLogicalFileAccess(const char* filename, uint64_t tag, uint64_t size, uint8_t operation) { + (void)filename; + (void)tag; + (void)size; + (void)operation; + return 0; +} + +int APS5_VABI sceRazorCpuEndLogicalFileAccess(void) { + return 0; +} + +int APS5_VABI sceRazorCpuFiberLogNameChange(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRazorCpuFiberSwitch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRazorCpuGpuMarkerSync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRazorCpuInitializeGpuMarkerContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRazorCpuInitializeInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRazorCpuSetPopMarkerCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRazorCpuSetPushMarkerCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRazorCpuSetPushMarkerStaticCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRazorCpuStartCaptureInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRazorCpuStopCaptureInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRazorCpuWorkloadRunBegin(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRazorCpuWorkloadRunEnd(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRazorCpuWorkloadSubmit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceRazorCpu_debug/Export.cpp b/core/libs/prx/libSceRazorCpu_debug/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceRazorCpu_debug/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceRegMgr/CMakeLists.txt b/core/libs/prx/libSceRegMgr/CMakeLists.txt new file mode 100644 index 0000000000..e10c382e33 --- /dev/null +++ b/core/libs/prx/libSceRegMgr/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceRegMgr SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceRegMgr PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceRegMgr) + +if(BUILD_TESTING) + add_test_executable(guest_regmgr_tests ${LIBS_INCLUDE_DIR}/tests/GuestRegMgr.cpp) + target_include_directories(guest_regmgr_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_regmgr_tests PRIVATE libSceRegMgr libc) + configure_windows_unwind(guest_regmgr_tests) + add_test(NAME guest_regmgr COMMAND guest_regmgr_tests) + if(WIN32) + set_tests_properties(guest_regmgr PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceRegMgr/Export.cpp b/core/libs/prx/libSceRegMgr/Export.cpp new file mode 100644 index 0000000000..9d71463de6 --- /dev/null +++ b/core/libs/prx/libSceRegMgr/Export.cpp @@ -0,0 +1,330 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceRegMgrGetBin(std::uint32_t optionId, void* data, std::size_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceRegMgrGetInt(int32_t optionId, int32_t* outInt) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceRegMgrGetStr(int32_t optionId, char* outString, std::size_t stringLength) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceRegMgrSetBin(std::uint32_t optionId, const void* data, std::size_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceRegMgrSetInt(int32_t optionId, int32_t inInt) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceRegMgrSetStr(int32_t optionId, const char* inStirng, std::size_t stringLength) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +std::int32_t APS5_VABI sceRegMgrNonSysGetInt(std::int64_t key, std::int32_t unk, std::int32_t* data) { + (void)key; + (void)unk; + (void)data; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrCheckError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrCheckIntVal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrGetBinInitVal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrGetIntInitVal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrGetStrInitVal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrGetVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrIsChange(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrEvtGetCnt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrEvtGetRegId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrResetVal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrSetInitLevel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrSrvCnvRegionInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrSrvCnvRegionStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrSrvGetMachineType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrSrvGetQAFforReg(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrSrvGetRealMachineType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrSrvGetRegion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrSrvGetRegionStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrBackupNeedMem(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrBackupPullData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrBackupPushData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrBackupPullDataGkp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrBackupPushDataGkp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrCntlStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrCntlDeleteReg(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrCntlGetFilesCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrCntlGetFileName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrCntSetInitReinst(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrIsInitOK(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrGetInitError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrRecoverRegNvs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrNonSysCheckError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrNonSysGetBin(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrNonSysGetStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrNonSysSetBin(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrNonSysSetInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrNonSysSetStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrGetVersionForPS4(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrBackupNeedMemForPS4(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrBackupPullDataForPS4(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrBackupPushDataForPS4(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrEvtGetCntForPS4(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrEvtGetRegIdForPS4(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrStrncpy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrMemset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrSetbin(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrToolGetUpdateCnt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrToolGetEntryCnt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrToolDataCheckGet(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrDrvGetUpdateCnt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrDrvGetEntCnt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrDrvDataOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrDrvDataCheckGet(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrDrvDataClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrToolGetInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrLogPull(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrCntlStopHdd(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRegMgrPrintInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceRemotePlayClientIpc/Export.cpp b/core/libs/prx/libSceRemotePlayClientIpc/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceRemotePlayClientIpc/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceRemoteplay/Export.cpp b/core/libs/prx/libSceRemoteplay/Export.cpp index 1c3e5bbad0..bc653c840f 100644 --- a/core/libs/prx/libSceRemoteplay/Export.cpp +++ b/core/libs/prx/libSceRemoteplay/Export.cpp @@ -25,4 +25,199 @@ int APS5_VABI sceRemoteplayTerminate(void) { return 0; } +int APS5_VABI sceRemoteplayApprove(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayClearAllRegistData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayClearConnectHistory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayConfirmDeviceRegist(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayDisconnect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGeneratePinCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGetApMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGetConnectHistory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGetConnectUserId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGetMbusDeviceInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGetOperationStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGetPrimaryUserId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGetRemoteplayStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGetRpMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayImeClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayImeFilterResult(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayImeGetEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayImeNotify(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayImeNotifyEventResult(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayImeOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayImeSetCaret(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayImeSetText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayIsRemoteOskReady(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayIsRemotePlaying(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayNotifyMbusDeviceRegistComplete(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayNotifyNpPushWakeup(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayNotifyPinCodeError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayNotifySubmitCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayNotifyUserDelete(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayPrintAllRegistData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayProhibit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayProhibitStreaming(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayServerLock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayServerUnLock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplaySetApMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplaySetLogLevel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplaySetProhibition(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplaySetProhibitionForVsh(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplaySetRpMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceResourceArbitrator/Export.cpp b/core/libs/prx/libSceResourceArbitrator/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceResourceArbitrator/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceRnpsAppMgr/Export.cpp b/core/libs/prx/libSceRnpsAppMgr/Export.cpp new file mode 100644 index 0000000000..a2fdcbc15d --- /dev/null +++ b/core/libs/prx/libSceRnpsAppMgr/Export.cpp @@ -0,0 +1,143 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceRnpsAppMgrBlockAppInstall() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrCreateUpdateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrCreateUrlRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrDestroyBundle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrDestroyUpdateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrFinishUrlRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrGetAppInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrGetAppInfoDebugString() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrGetAppInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrGetEventForShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrGetUpdateProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrHasEventForShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrPushEventForShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrRecoverUfsImage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrRegisterShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrRemoveApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrRemoveAppOnSystemShutdown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrRemoveUfsImageOnSystemShutdown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrRequestUpdateCheck() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrSetCrlUrl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrSetEventFlagForClients() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrSetUpdateUrl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrStartService() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrUnblockAppInstall() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRnpsAppMgrUnregisterShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceRnpsNKInjectedBundle/Export.cpp b/core/libs/prx/libSceRnpsNKInjectedBundle/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceRnpsNKInjectedBundle/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 0744be6f86..3cc3b045de 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -31,6 +32,15 @@ constexpr std::uint64_t UNIX_EPOCH_TICK = UNIX_EPOCH_DAYS * TICKS_PER_DAY; constexpr std::uint64_t WIN32_EPOCH_TICK = WIN32_EPOCH_DAYS * TICKS_PER_DAY; constexpr std::uint64_t MAX_TICK = 3652059ull * TICKS_PER_DAY - 1; +struct ClockState { + std::uint64_t tick = 0; + std::chrono::steady_clock::time_point setAt{}; + bool isSet = false; +}; + +std::mutex clockMutex; +ClockState clocks[5]; + bool isLeapYear(std::int64_t year) { return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; } @@ -90,11 +100,31 @@ RtcDateTime fromTick(std::uint64_t tick) { static_cast(timeOfDay % TICKS_PER_MINUTE / TICKS_PER_SECOND), static_cast(timeOfDay % TICKS_PER_SECOND)}; } -std::uint64_t currentTick() { +std::uint64_t currentTick(std::size_t clock = 0) { + ClockState state; + { + std::lock_guard lock(clockMutex); + state = clocks[clock]; + } + if (state.isSet) { + const auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - state.setAt).count(); + const auto elapsedTicks = static_cast(elapsed); + return elapsedTicks > std::numeric_limits::max() - state.tick + ? std::numeric_limits::max() : state.tick + elapsedTicks; + } + if (clock == 2 || clock == 3) return currentTick(1); const auto now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); return UNIX_EPOCH_TICK + static_cast(now); } +int setCurrentTick(std::size_t clock, const RtcTick* tick) { + if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; + if (tick->tick < UNIX_EPOCH_TICK) return SCE_RTC_ERROR_INVALID_VALUE; + std::lock_guard lock(clockMutex); + clocks[clock] = ClockState{tick->tick, std::chrono::steady_clock::now(), true}; + return 0; +} + std::int64_t localOffsetSeconds(std::uint64_t utcTick) { const auto elapsed = std::chrono::microseconds{static_cast(utcTick) - static_cast(UNIX_EPOCH_TICK)}; const auto seconds = static_cast(std::chrono::floor(elapsed).count()); @@ -126,16 +156,22 @@ int addTicks(RtcTick* dst, const RtcTick* src, std::int64_t count, std::int64_t return 0; } -int addMonths(RtcTick* dst, const RtcTick* src, std::int64_t months) { +int addTicksWrapping(RtcTick* dst, const RtcTick* src, std::int64_t count, std::int64_t unit) { + if (!dst || !src) return SCE_RTC_ERROR_INVALID_POINTER; + dst->tick = src->tick + static_cast(count) * static_cast(unit); + return 0; +} + +int addMonths(RtcTick* dst, const RtcTick* src, std::int32_t months) { if (!dst || !src) return SCE_RTC_ERROR_INVALID_POINTER; - if (src->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; RtcDateTime time = fromTick(src->tick); - const std::int64_t monthIndex = time.year * 12 + (time.month - 1) + months; - const std::int64_t year = monthIndex / 12; - if (monthIndex < 12 || year > 9999) return SCE_RTC_ERROR_INVALID_VALUE; - time.year = static_cast(year); - time.month = static_cast(monthIndex % 12 + 1); - const int lastDay = daysInMonth(time.year, time.month); + const auto index = static_cast(static_cast(time.year) * 12u + time.month + static_cast(months) - 1u); + const auto year = static_cast(index / 12); + const std::int32_t month = index % 12; + if (year < 1 || year > 9999 || month < 0) return 0; + time.year = year; + time.month = static_cast(month + 1); + const int lastDay = daysInMonth(year, time.month); if (time.day > lastDay) time.day = static_cast(lastDay); dst->tick = toTick(time); return 0; @@ -234,14 +270,20 @@ bool parseZoneName(const char* cursor, std::int64_t& offsetMinutes) { const char* name; std::int64_t minutes; } zones[] = { - {"WETDST", 60}, {"METDST", 120}, {"EETDST", 180}, {"SWT", 60}, {"MEZ", 60}, {"AESST", 660}, {"ACSST", 630}, - {"CAST", 570}, {"CADT", 630}, {"SAST", 570}, {"SADT", 630}, {"WST", 480}, {"WDT", 540}, - {"GMT", 0}, {"WET", 0}, {"BST", 60}, {"WAT", -60}, {"AST", -240}, {"ADT", -180}, {"EST", -300}, {"EDT", -240}, - {"CST", -360}, {"CDT", -300}, {"MST", -420}, {"MDT", -360}, {"PST", -480}, {"PDT", -420}, {"YST", -540}, {"YDT", -480}, - {"HDT", -540}, {"CAT", -600}, {"AHST", -600}, {"NST", -150}, {"NDT", -90}, {"IDLW", -720}, {"CET", 60}, {"MET", 60}, - {"MEWT", 60}, {"MEST", 120}, {"CEST", 120}, {"SST", 120}, {"FWT", 120}, {"FST", 60}, {"EET", 120}, {"IST", 120}, - {"WAST", 420}, {"WADT", 480}, {"CCT", 480}, {"AWST", 480}, {"JST", 540}, {"KST", 540}, {"ACST", 570}, {"EAST", 600}, - {"GST", 600}, {"AEST", 600}, {"NZT", 720}, {"NZST", 720}, {"NZDT", 780}, {"IDLE", 720}, {"JT", 450}, + {"GMT", 0}, {"EST", -300}, {"EDT", -240}, {"CST", -360}, {"CDT", -300}, + {"MST", -420}, {"MDT", -360}, {"PST", -480}, {"PDT", -420}, {"NZDT", 780}, + {"NZST", 720}, {"IDLE", 720}, {"NZT", 720}, {"AESST", 660}, {"ACSST", 630}, + {"CADT", 630}, {"SADT", 630}, {"AEST", 600}, {"EAST", 600}, {"GST", 600}, + {"LIGT", 600}, {"ACST", 570}, {"SAST", 570}, {"CAST", 570}, {"AWSST", 540}, + {"JST", 540}, {"KST", 540}, {"WDT", 540}, {"MT", 510}, {"AWST", 480}, + {"CCT", 480}, {"WADT", 480}, {"WST", 480}, {"JT", 450}, {"WAST", 420}, + {"IT", 210}, {"BT", 180}, {"EETDST", 180}, {"EET", 120}, {"CETDST", 120}, {"CEST", 120}, + {"FWT", 120}, {"IST", 120}, {"MEST", 120}, {"METDST", 120}, {"SST", 120}, + {"BST", 60}, {"CET", 60}, {"DNT", 60}, {"FST", 60}, {"MET", 60}, + {"MEWT", 60}, {"MEZ", 60}, {"NOR", 60}, {"SET", 60}, {"SWT", 60}, + {"WETDST", 60}, {"WET", 0}, {"WAT", -60}, {"NDT", -90}, {"ADT", -180}, + {"NFT", -150}, {"NST", -150}, {"AST", -240}, {"YDT", -480}, {"HDT", -540}, + {"YST", -540}, {"AHST", -600}, {"CAT", -600}, {"NT", -660}, {"IDLW", -720}, }; for (const auto& zone : zones) { const char* name = cursor; @@ -359,7 +401,9 @@ int APS5_VABI sceRtcGetCurrentTick(RtcTick* tick) { } int APS5_VABI sceRtcGetCurrentNetworkTick(RtcTick* tick) { - return sceRtcGetCurrentTick(tick); + if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; + tick->tick = currentTick(1); + return 0; } int APS5_VABI sceRtcGetCurrentClock(RtcDateTime* time, int time_zone_minutes) { @@ -372,8 +416,10 @@ int APS5_VABI sceRtcGetCurrentClock(RtcDateTime* time, int time_zone_minutes) { int APS5_VABI sceRtcGetCurrentClockLocalTime(RtcDateTime* time) { if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - const std::uint64_t utc = currentTick(); - *time = fromTick(utc + static_cast(localOffsetSeconds(utc) * TICKS_PER_SECOND)); + RtcTick local{}; + const RtcTick utc{currentTick()}; + if (const int result = addTicks(&local, &utc, localOffsetSeconds(utc.tick), TICKS_PER_SECOND); result != 0) return result; + *time = fromTick(local.tick); return 0; } @@ -449,44 +495,148 @@ int APS5_VABI sceRtcSetDosTime(RtcDateTime* time, uint32_t dos_time) { return 0; } -int APS5_VABI sceRtcFormatRFC3339(char* date_time, const RtcTick* utc, int time_zone_minutes) { - if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; - if (time_zone_minutes < -1439 || time_zone_minutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; +int APS5_VABI sceRtcGetYear(const RtcDateTime* time) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + return static_cast(time->year); +} + +int APS5_VABI sceRtcSetYear(RtcDateTime* time, int year) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + if (year < 1 || year > 9999) return SCE_RTC_ERROR_INVALID_YEAR; + time->year = static_cast(year); + return 0; +} + +int APS5_VABI sceRtcGetMonth(const RtcDateTime* time) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + return static_cast(time->month); +} + +int APS5_VABI sceRtcSetMonth(RtcDateTime* time, int month) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + if (month < 1 || month > 12) return SCE_RTC_ERROR_INVALID_MONTH; + time->month = static_cast(month); + return 0; +} + +int APS5_VABI sceRtcGetDay(const RtcDateTime* time) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + return static_cast(time->day); +} + +int APS5_VABI sceRtcSetDay(RtcDateTime* time, int day) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + if (day < 1 || day > 31) return SCE_RTC_ERROR_INVALID_DAY; + time->day = static_cast(day); + return 0; +} + +int APS5_VABI sceRtcGetHour(const RtcDateTime* time) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + return static_cast(time->hour); +} + +int APS5_VABI sceRtcSetHour(RtcDateTime* time, int hour) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + if (hour < 0 || hour > 23) return SCE_RTC_ERROR_INVALID_HOUR; + time->hour = static_cast(hour); + return 0; +} + +int APS5_VABI sceRtcGetMinute(const RtcDateTime* time) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + return static_cast(time->minute); +} + +int APS5_VABI sceRtcSetMinute(RtcDateTime* time, int minute) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + if (minute < 0 || minute > 59) return SCE_RTC_ERROR_INVALID_MINUTE; + time->minute = static_cast(minute); + return 0; +} + +int APS5_VABI sceRtcGetSecond(const RtcDateTime* time) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + return static_cast(time->second); +} + +int APS5_VABI sceRtcSetSecond(RtcDateTime* time, int second) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + if (second < 0 || second > 59) return SCE_RTC_ERROR_INVALID_SECOND; + time->second = static_cast(second); + return 0; +} + +int APS5_VABI sceRtcGetMicrosecond(const RtcDateTime* time) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + return static_cast(time->microsecond); +} + +int APS5_VABI sceRtcSetMicrosecond(RtcDateTime* time, int microsecond) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + if (microsecond < 0 || static_cast(microsecond) >= TICKS_PER_SECOND) return SCE_RTC_ERROR_INVALID_MICROSECOND; + time->microsecond = static_cast(microsecond); + return 0; +} + +int APS5_VABI sceRtcFormatRFC3339(char* dateTime, const RtcTick* utc, int timeZoneMinutes) { + if (!dateTime) return SCE_RTC_ERROR_INVALID_POINTER; + RtcTick now{}; + if (!utc) { + now.tick = currentTick(); + utc = &now; + } + if (timeZoneMinutes < -1439 || timeZoneMinutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; RtcTick local{}; - if (const int result = addTicks(&local, utc, time_zone_minutes, TICKS_PER_MINUTE); result != 0) return result; + if (const int result = addTicks(&local, utc, timeZoneMinutes, TICKS_PER_MINUTE); result != 0) return result; const RtcDateTime time = fromTick(local.tick); - int written = std::snprintf(date_time, 23, "%04u-%02u-%02uT%02u:%02u:%02u.%02u", time.year, time.month, time.day, time.hour, time.minute, time.second, time.microsecond / 10000); - if (time_zone_minutes == 0) { - std::snprintf(date_time + written, 2, "Z"); + int written = std::snprintf(dateTime, 23, "%04u-%02u-%02uT%02u:%02u:%02u.%02u", time.year, time.month, time.day, time.hour, time.minute, time.second, time.microsecond / 10000); + if (timeZoneMinutes == 0) { + std::snprintf(dateTime + written, 2, "Z"); } else { - const int offset = time_zone_minutes < 0 ? -time_zone_minutes : time_zone_minutes; - std::snprintf(date_time + written, 7, "%c%02d:%02d", time_zone_minutes < 0 ? '-' : '+', offset / 60, offset % 60); + const int offset = timeZoneMinutes < 0 ? -timeZoneMinutes : timeZoneMinutes; + std::snprintf(dateTime + written, 7, "%c%02d:%02d", timeZoneMinutes < 0 ? '-' : '+', offset / 60, offset % 60); } return 0; } -int APS5_VABI sceRtcFormatRFC3339LocalTime(char* date_time, const RtcTick* utc) { - if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; +int APS5_VABI sceRtcFormatRFC3339LocalTime(char* dateTime, const RtcTick* utc) { + if (!dateTime) return SCE_RTC_ERROR_INVALID_POINTER; + RtcTick now{}; + if (!utc) { + now.tick = currentTick(); + utc = &now; + } if (utc->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; - return sceRtcFormatRFC3339(date_time, utc, localOffsetMinutes(*utc)); + return sceRtcFormatRFC3339(dateTime, utc, localOffsetMinutes(*utc)); } -int APS5_VABI sceRtcFormatRFC2822(char* date_time, const RtcTick* utc, int time_zone_minutes) { - if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; - if (time_zone_minutes < -1439 || time_zone_minutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; +int APS5_VABI sceRtcFormatRFC2822(char* dateTime, const RtcTick* utc, int timeZoneMinutes) { + if (!dateTime) return SCE_RTC_ERROR_INVALID_POINTER; + RtcTick now{}; + if (!utc) { + now.tick = currentTick(); + utc = &now; + } + if (timeZoneMinutes < -1439 || timeZoneMinutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; RtcTick local{}; - if (const int result = addTicks(&local, utc, time_zone_minutes, TICKS_PER_MINUTE); result != 0) return result; + if (const int result = addTicks(&local, utc, timeZoneMinutes, TICKS_PER_MINUTE); result != 0) return result; const RtcDateTime time = fromTick(local.tick); - const int offset = time_zone_minutes < 0 ? -time_zone_minutes : time_zone_minutes; - std::snprintf(date_time, 32, "%s, %02u %s %04u %02u:%02u:%02u %c%02d%02d", WEEKDAY_NAMES[(local.tick / TICKS_PER_DAY + 1) % 7], time.day, - MONTH_NAMES[time.month - 1], time.year, time.hour, time.minute, time.second, time_zone_minutes < 0 ? '-' : '+', offset / 60, offset % 60); + const int offset = timeZoneMinutes < 0 ? -timeZoneMinutes : timeZoneMinutes; + std::snprintf(dateTime, 32, "%s, %02u %s %04u %02u:%02u:%02u %c%02d%02d", WEEKDAY_NAMES[(local.tick / TICKS_PER_DAY + 1) % 7], time.day, + MONTH_NAMES[time.month - 1], time.year, time.hour, time.minute, time.second, timeZoneMinutes < 0 ? '-' : '+', offset / 60, offset % 60); return 0; } -int APS5_VABI sceRtcFormatRFC2822LocalTime(char* date_time, const RtcTick* utc) { - if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; +int APS5_VABI sceRtcFormatRFC2822LocalTime(char* dateTime, const RtcTick* utc) { + if (!dateTime) return SCE_RTC_ERROR_INVALID_POINTER; + RtcTick now{}; + if (!utc) { + now.tick = currentTick(); + utc = &now; + } if (utc->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; - return sceRtcFormatRFC2822(date_time, utc, localOffsetMinutes(*utc)); + return sceRtcFormatRFC2822(dateTime, utc, localOffsetMinutes(*utc)); } int APS5_VABI sceRtcParseRFC3339(RtcTick* utc, const char* date_time) { @@ -509,31 +659,31 @@ int APS5_VABI sceRtcParseDateTime(RtcTick* utc, const char* date_time) { } int APS5_VABI sceRtcTickAddTicks(RtcTick* dst, const RtcTick* src, int64_t ticks) { - return addTicks(dst, src, ticks, 1); + return addTicksWrapping(dst, src, ticks, 1); } int APS5_VABI sceRtcTickAddMicroseconds(RtcTick* dst, const RtcTick* src, int64_t usec) { - return addTicks(dst, src, usec, 1); + return addTicksWrapping(dst, src, usec, 1); } int APS5_VABI sceRtcTickAddSeconds(RtcTick* dst, const RtcTick* src, int64_t seconds) { - return addTicks(dst, src, seconds, TICKS_PER_SECOND); + return addTicksWrapping(dst, src, seconds, TICKS_PER_SECOND); } int APS5_VABI sceRtcTickAddMinutes(RtcTick* dst, const RtcTick* src, int64_t minutes) { - return addTicks(dst, src, minutes, TICKS_PER_MINUTE); + return addTicksWrapping(dst, src, minutes, TICKS_PER_MINUTE); } int APS5_VABI sceRtcTickAddHours(RtcTick* dst, const RtcTick* src, int32_t hours) { - return addTicks(dst, src, hours, TICKS_PER_HOUR); + return addTicksWrapping(dst, src, hours, TICKS_PER_HOUR); } int APS5_VABI sceRtcTickAddDays(RtcTick* dst, const RtcTick* src, int32_t days) { - return addTicks(dst, src, days, TICKS_PER_DAY); + return addTicksWrapping(dst, src, days, TICKS_PER_DAY); } int APS5_VABI sceRtcTickAddWeeks(RtcTick* dst, const RtcTick* src, int32_t weeks) { - return addTicks(dst, src, weeks, 7 * TICKS_PER_DAY); + return addTicksWrapping(dst, src, weeks, 7 * TICKS_PER_DAY); } int APS5_VABI sceRtcTickAddMonths(RtcTick* dst, const RtcTick* src, int32_t months) { @@ -541,7 +691,102 @@ int APS5_VABI sceRtcTickAddMonths(RtcTick* dst, const RtcTick* src, int32_t mont } int APS5_VABI sceRtcTickAddYears(RtcTick* dst, const RtcTick* src, int16_t years) { - return addMonths(dst, src, static_cast(years) * 12); + return addMonths(dst, src, static_cast(years) * 12); +} + +int APS5_VABI sceRtcInit(void) { + return 0; +} + +int APS5_VABI sceRtcEnd(void) { + return 0; +} + +int APS5_VABI sceRtcCompareTick(const RtcTick* first, const RtcTick* second) { + if (!first || !second) return SCE_RTC_ERROR_INVALID_POINTER; + if (first->tick < second->tick) return -1; + return first->tick > second->tick ? 1 : 0; +} + +int APS5_VABI sceRtcGetCurrentRawNetworkTick(RtcTick* tick) { + if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; + tick->tick = currentTick(4); + return 0; +} + +int APS5_VABI sceRtcGetCurrentAdNetworkTick(RtcTick* tick) { + if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; + tick->tick = currentTick(2); + return 0; +} + +int APS5_VABI sceRtcGetCurrentDebugNetworkTick(RtcTick* tick) { + if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; + tick->tick = currentTick(3); + return 0; +} + +int APS5_VABI sceRtcSetCurrentTick(const RtcTick* tick) { + return setCurrentTick(0, tick); +} + +int APS5_VABI sceRtcSetCurrentNetworkTick(const RtcTick* tick) { + return setCurrentTick(1, tick); +} + +int APS5_VABI sceRtcSetCurrentAdNetworkTick(const RtcTick* tick) { + return setCurrentTick(2, tick); +} + +int APS5_VABI sceRtcSetCurrentDebugNetworkTick(const RtcTick* tick) { + return setCurrentTick(3, tick); +} + +int APS5_VABI sceRtcSetConf(int first, int second, int third, int fourth) { + (void)first; + (void)second; + (void)third; + (void)fourth; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRtcFormatRFC3339Precise(char* date_time, const RtcTick* utc, int time_zone_minutes, int precision) { + if (!date_time) return SCE_RTC_ERROR_INVALID_POINTER; + RtcTick now{}; + if (!utc) { + now.tick = currentTick(); + utc = &now; + } + if (time_zone_minutes < -1439 || time_zone_minutes > 1439 || precision < 0 || precision > 6) return SCE_RTC_ERROR_INVALID_VALUE; + RtcTick local{}; + if (const int result = addTicks(&local, utc, time_zone_minutes, TICKS_PER_MINUTE); result != 0) return result; + const RtcDateTime time = fromTick(local.tick); + int written = std::snprintf(date_time, 20, "%04u-%02u-%02uT%02u:%02u:%02u", time.year, time.month, time.day, time.hour, time.minute, time.second); + if (precision != 0) { + std::uint32_t divisor = 1; + for (int digits = precision; digits < 6; ++digits) divisor *= 10; + written += std::snprintf(date_time + written, 8, ".%0*u", precision, time.microsecond / divisor); + } + if (time_zone_minutes == 0) { + std::snprintf(date_time + written, 2, "Z"); + } else { + const int offset = time_zone_minutes < 0 ? -time_zone_minutes : time_zone_minutes; + std::snprintf(date_time + written, 7, "%c%02d:%02d", time_zone_minutes < 0 ? '-' : '+', offset / 60, offset % 60); + } + return 0; +} + +int APS5_VABI sceRtcFormatRFC3339PreciseLocalTime(char* date_time, const RtcTick* utc, int precision) { + if (!date_time) return SCE_RTC_ERROR_INVALID_POINTER; + RtcTick now{}; + if (!utc) { + now.tick = currentTick(); + utc = &now; + } + if (precision < 0 || precision > 6) return SCE_RTC_ERROR_INVALID_VALUE; + if (utc->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; + return sceRtcFormatRFC3339Precise(date_time, utc, localOffsetMinutes(*utc), precision); } } diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index 2bf31d06e5..dcfb2c25be 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -1,10 +1,14 @@ #include +#include +#include #include #include #include #include +#include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -16,10 +20,18 @@ constexpr int RUDP_ERROR_NOT_INITIALIZED = static_cast(0x80770001u); constexpr int RUDP_ERROR_ALREADY_INITIALIZED = static_cast(0x80770002u); constexpr int RUDP_ERROR_INVALID_CONTEXT_ID = static_cast(0x80770003u); constexpr int RUDP_ERROR_INVALID_ARGUMENT = static_cast(0x80770004u); +constexpr int RUDP_ERROR_INVALID_OPTION = static_cast(0x80770005u); constexpr int RUDP_ERROR_MEMORY = static_cast(0x80770007u); constexpr int RUDP_ERROR_CONN_RESET = static_cast(0x80770009u); constexpr int RUDP_ERROR_CONN_REFUSED = static_cast(0x8077000Au); constexpr int RUDP_ERROR_NOT_ACCEPTABLE = static_cast(0x80770011u); +constexpr int RUDP_ERROR_INVALID_POLL_ID = static_cast(0x8077001Fu); +constexpr int RUDP_ERROR_TOO_MANY_CONTEXTS = static_cast(0x80770020u); + +constexpr int RUDP_POLL_OP_ADD = 1; +constexpr int RUDP_POLL_OP_MODIFY = 2; +constexpr int RUDP_POLL_OP_REMOVE = 3; +constexpr uint64_t RUDP_POLL_WAIT_MAX_USEC = static_cast(std::numeric_limits::max() / 2000); constexpr int RUDP_POOL_MIN_SIZE = 0xC58; constexpr std::uintptr_t RUDP_POOL_ALIGNMENT = 8; @@ -27,18 +39,74 @@ constexpr std::size_t RUDP_STATUS_SIZE = 0xF8; constexpr std::size_t RUDP_STATUS_CURRENT_CONTEXTS = 0x64; constexpr uint16_t RUDP_MAX_SEGMENT_SIZE = 1410; -using GuestRudpEventHandler = void(APS5_VABI*)(int, int, int, void*); +enum RudpOption { + RUDP_OPTION_MAX_PAYLOAD = 1, + RUDP_OPTION_SNDBUF = 2, + RUDP_OPTION_RCVBUF = 3, + RUDP_OPTION_NODELAY = 4, + RUDP_OPTION_DELIVERY_CRITICAL = 5, + RUDP_OPTION_ORDER_CRITICAL = 6, + RUDP_OPTION_NONBLOCK = 7, + RUDP_OPTION_STREAM = 8, + RUDP_OPTION_CONNECTION_TIMEOUT = 9, + RUDP_OPTION_CLOSE_WAIT_TIMEOUT = 10, + RUDP_OPTION_AGGREGATION_TIMEOUT = 11, + RUDP_OPTION_READ_TIMEOUT = 15, + RUDP_OPTION_WRITE_TIMEOUT = 16, + RUDP_OPTION_FLUSH_TIMEOUT = 17, + RUDP_OPTION_KEEP_ALIVE_INTERVAL = 18, + RUDP_OPTION_KEEP_ALIVE_TIMEOUT = 19, +}; + +using GuestRudpEventHandler = RudpEventHandler; std::mutex g_mutex; bool g_inited = false; int g_next_ctx = 0; -std::map g_ctx; // id -> bound +struct RudpContext { + bool bound = false; + std::map options = { + {RUDP_OPTION_MAX_PAYLOAD, 1346}, + {RUDP_OPTION_SNDBUF, 65536}, + {RUDP_OPTION_RCVBUF, 65536}, + {RUDP_OPTION_NODELAY, 0}, + {RUDP_OPTION_DELIVERY_CRITICAL, 1}, + {RUDP_OPTION_ORDER_CRITICAL, 1}, + {RUDP_OPTION_NONBLOCK, 1}, + {RUDP_OPTION_STREAM, 0}, + {RUDP_OPTION_CONNECTION_TIMEOUT, 60000}, + {RUDP_OPTION_CLOSE_WAIT_TIMEOUT, 0}, + {RUDP_OPTION_AGGREGATION_TIMEOUT, 30}, + {RUDP_OPTION_READ_TIMEOUT, 0}, + {RUDP_OPTION_WRITE_TIMEOUT, 0}, + {RUDP_OPTION_FLUSH_TIMEOUT, 0}, + {RUDP_OPTION_KEEP_ALIVE_INTERVAL, 0}, + {RUDP_OPTION_KEEP_ALIVE_TIMEOUT, 0}, + }; +}; +std::map g_ctx; GuestRudpEventHandler g_handler = nullptr; void* g_handler_arg = nullptr; +struct RudpPoll { + std::size_t capacity; + std::map entries; +}; + + +int g_next_poll = 1; +std::map g_polls; +std::condition_variable g_polls_changed; + bool valid_ctx(int ctx) { return g_ctx.count(ctx) != 0; } + +void remove_ctx_from_polls(int ctx) { + for (auto& [id, poll] : g_polls) { + poll.entries.erase(ctx); + } +} } // namespace extern "C" { @@ -58,8 +126,12 @@ int APS5_VABI sceRudpInit_nid_postfix(void* mem_pool, int mem_pool_size) { return 0; } -int APS5_VABI sceRudpActivate() { - return 0; +int APS5_VABI sceRudpActivate(std::int32_t contextId, const void* address, std::uint32_t addressLength) { + std::lock_guard lock(g_mutex); + if (!g_inited) return RUDP_ERROR_NOT_INITIALIZED; + if (contextId < 0 || !address) return RUDP_ERROR_INVALID_ARGUMENT; + static_cast(addressLength); + throw std::runtime_error(__func__); } int APS5_VABI sceRudpGetStatus(void* status, std::size_t size) { @@ -84,6 +156,8 @@ int APS5_VABI sceRudpEnd(void) { } g_inited = false; g_ctx.clear(); + g_polls.clear(); + g_polls_changed.notify_all(); return 0; } @@ -92,7 +166,11 @@ int APS5_VABI sceRudpTerminate(int ctx_id) { if (!g_inited) { return RUDP_ERROR_NOT_INITIALIZED; } - return g_ctx.erase(ctx_id) != 0 ? 0 : RUDP_ERROR_INVALID_CONTEXT_ID; + if (g_ctx.erase(ctx_id) == 0) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + remove_ctx_from_polls(ctx_id); + return 0; } int APS5_VABI sceRudpEnableInternalIOThread(uint32_t stack_size, uint32_t priority) { @@ -102,9 +180,12 @@ int APS5_VABI sceRudpEnableInternalIOThread(uint32_t stack_size, uint32_t priori return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; } -int APS5_VABI sceRudpEnableInternalIOThread2() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceRudpEnableInternalIOThread2(uint32_t stack_size, uint32_t priority, uint64_t affinity) { + (void)stack_size; + (void)priority; + (void)affinity; + std::lock_guard lk(g_mutex); + return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; } int APS5_VABI sceRudpSetEventHandler(GuestRudpEventHandler handler, void* arg) { @@ -114,10 +195,10 @@ int APS5_VABI sceRudpSetEventHandler(GuestRudpEventHandler handler, void* arg) { return 0; } -int APS5_VABI sceRudpCreateContext(GuestRudpEventHandler handler, void* arg, int* ctx_id) { +int APS5_VABI sceRudpCreateContext(GuestRudpEventHandler handler, void* arg, int* ctxId) { (void)handler; (void)arg; - if (ctx_id == nullptr) { + if (ctxId == nullptr) { return RUDP_ERROR_INVALID_ARGUMENT; } std::lock_guard lk(g_mutex); @@ -125,12 +206,12 @@ int APS5_VABI sceRudpCreateContext(GuestRudpEventHandler handler, void* arg, int return RUDP_ERROR_NOT_INITIALIZED; } const int id = g_next_ctx++; - g_ctx[id] = false; - *ctx_id = id; + g_ctx.emplace(id, RudpContext{}); + *ctxId = id; return 0; } -int APS5_VABI sceRudpBind(int ctx_id, uint16_t vport, int transport, uint16_t port) { +int APS5_VABI sceRudpBind(int ctxId, uint16_t vport, int transport, uint16_t port) { (void)vport; (void)transport; (void)port; @@ -138,26 +219,43 @@ int APS5_VABI sceRudpBind(int ctx_id, uint16_t vport, int transport, uint16_t po if (!g_inited) { return RUDP_ERROR_NOT_INITIALIZED; } - if (!valid_ctx(ctx_id)) { + if (!valid_ctx(ctxId)) { return RUDP_ERROR_INVALID_CONTEXT_ID; } - g_ctx[ctx_id] = true; + g_ctx.at(ctxId).bound = true; return 0; } -int APS5_VABI sceRudpSetOption(int ctx_id, int option, const void* value, uint32_t len) { - (void)option; - (void)value; - (void)len; +int APS5_VABI sceRudpSetOption(int ctxId, int option, const void* value, uint32_t len) { std::lock_guard lk(g_mutex); if (!g_inited) { return RUDP_ERROR_NOT_INITIALIZED; } - return valid_ctx(ctx_id) ? 0 : RUDP_ERROR_INVALID_CONTEXT_ID; + const auto context = g_ctx.find(ctxId); + if (context == g_ctx.end()) return RUDP_ERROR_INVALID_CONTEXT_ID; + if (option < RUDP_OPTION_MAX_PAYLOAD || option > RUDP_OPTION_KEEP_ALIVE_TIMEOUT) return RUDP_ERROR_INVALID_OPTION; + uint32_t optionValue; + if (value == nullptr || len != sizeof(optionValue)) return RUDP_ERROR_INVALID_ARGUMENT; + std::memcpy(&optionValue, value, sizeof(optionValue)); + context->second.options.insert_or_assign(option, optionValue); + return 0; } -int APS5_VABI sceRudpGetOption() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceRudpGetOption(int ctxId, int option, void* value, uint32_t len) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + const auto context = g_ctx.find(ctxId); + if (context == g_ctx.end()) return RUDP_ERROR_INVALID_CONTEXT_ID; + if (option < RUDP_OPTION_MAX_PAYLOAD || option > RUDP_OPTION_KEEP_ALIVE_TIMEOUT) return RUDP_ERROR_INVALID_OPTION; + const auto entry = context->second.options.find(option); + if (entry == context->second.options.end() || + (option > RUDP_OPTION_AGGREGATION_TIMEOUT && option < RUDP_OPTION_READ_TIMEOUT)) { + NotImplemented_nid_no_patch(__func__); + } + if (value == nullptr || len != sizeof(entry->second)) return RUDP_ERROR_INVALID_ARGUMENT; + std::memcpy(value, &entry->second, sizeof(entry->second)); return 0; } @@ -189,7 +287,7 @@ int APS5_VABI sceRudpGetContextStatus(int ctx_id, void* status, uint32_t size) { return 0; } -int APS5_VABI sceRudpGetRemoteInfo() { +int APS5_VABI sceRudpGetRemoteInfo(int contextId, void* address, std::uint32_t* addressSize, std::uint16_t* virtualPort) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -264,14 +362,14 @@ int APS5_VABI sceRudpNetFlush(void) { return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; } -int APS5_VABI sceRudpProcessEvents(void) { +int APS5_VABI sceRudpProcessEvents(std::uint64_t timeout) { std::lock_guard lk(g_mutex); return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; } -int APS5_VABI sceRudpNetReceived(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceRudpNetReceived(int socketId, const void* data, std::size_t dataSize, const void* sourceAddress, std::uint32_t sourceAddressSize) { + std::lock_guard lk(g_mutex); + return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; } int APS5_VABI sceRudpGetMaxSegmentSize(uint16_t* size) { @@ -286,4 +384,123 @@ int APS5_VABI sceRudpGetMaxSegmentSize(uint16_t* size) { return 0; } +int APS5_VABI sceRudpPollCreate(std::size_t size) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (size == 0) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + if (g_next_poll == std::numeric_limits::max()) { + throw std::runtime_error("sceRudpPollCreate: poll ids exhausted"); + } + const int id = g_next_poll++; + g_polls[id] = RudpPoll{size, {}}; + return id; +} + +int APS5_VABI sceRudpPollControl(int pollId, int op, int ctxId, uint16_t events) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (pollId < 0 || ctxId < 0 || (op != RUDP_POLL_OP_ADD && op != RUDP_POLL_OP_MODIFY && op != RUDP_POLL_OP_REMOVE)) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + const auto poll = g_polls.find(pollId); + if (poll == g_polls.end()) { + return RUDP_ERROR_INVALID_POLL_ID; + } + if (!valid_ctx(ctxId)) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + auto& entries = poll->second.entries; + const auto entry = entries.find(ctxId); + if (op == RUDP_POLL_OP_ADD) { + if (entries.size() >= poll->second.capacity) { + return RUDP_ERROR_TOO_MANY_CONTEXTS; + } + if (entry != entries.end()) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + entries.emplace(ctxId, events); + return 0; + } + if (entry == entries.end()) { + return RUDP_ERROR_INVALID_CONTEXT_ID; + } + if (op == RUDP_POLL_OP_MODIFY) { + entry->second = events; + } else { + entries.erase(entry); + } + return 0; +} + +int APS5_VABI sceRudpPollWait(int pollId, RudpPollEvent* events, std::size_t eventLen, uint64_t timeout) { + std::unique_lock lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (pollId < 0 || events == nullptr || eventLen == 0) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + if (g_polls.count(pollId) == 0) { + return RUDP_ERROR_INVALID_POLL_ID; + } + const auto closed = [pollId] { return !g_inited || g_polls.count(pollId) == 0; }; + if (timeout > RUDP_POLL_WAIT_MAX_USEC) { + g_polls_changed.wait(lk, closed); + } else if (!g_polls_changed.wait_for(lk, std::chrono::microseconds(timeout), closed)) { + return 0; + } + return g_inited ? RUDP_ERROR_INVALID_POLL_ID : RUDP_ERROR_NOT_INITIALIZED; +} + +int APS5_VABI sceRudpPollDestroy(int pollId) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (pollId < 0) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + if (g_polls.erase(pollId) == 0) { + return RUDP_ERROR_INVALID_POLL_ID; + } + g_polls_changed.notify_all(); + return 0; +} + +int APS5_VABI sceRudpAccept() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpFlush() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpGetLocalInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpListen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpPollCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpSetMaxSegmentSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceS3DConversion/Export.cpp b/core/libs/prx/libSceS3DConversion/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceS3DConversion/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceS3da/Export.cpp b/core/libs/prx/libSceS3da/Export.cpp new file mode 100644 index 0000000000..79ca9b7758 --- /dev/null +++ b/core/libs/prx/libSceS3da/Export.cpp @@ -0,0 +1,30 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +struct S3daInfo; + +extern "C" { + +int APS5_VABI sceS3daClose(std::uint32_t port) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceS3daGetInfo(std::uint32_t port, S3daInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceS3daOpen(std::uint32_t port) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceS3daSendData(std::uint32_t port, const void* data, std::uint32_t size) { + 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 00529793b7..e3ccc08dee 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -18,6 +18,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/RemoveTree.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "SaveData.hpp" #include "prx/libSceSaveData/SaveDataFile.hpp" @@ -119,6 +120,90 @@ bool read_file_all(const std::string& path, std::vector& out) { return static_cast(f); } +constexpr char MEM_JOURNAL_MAGIC[] = {'A', 'P', 'S', '5', 'M', 'J', 'N', '1'}; + +struct MemoryWrite { + std::uint64_t offset; + std::uint64_t size; + const char* data; +}; + +std::vector encode_memory_journal(const std::vector& writes) { + std::vector bytes(std::begin(MEM_JOURNAL_MAGIC), std::end(MEM_JOURNAL_MAGIC)); + for (const MemoryWrite& write : writes) { + const std::size_t at = bytes.size(); + bytes.resize(at + sizeof(write.offset) + sizeof(write.size) + write.size); + std::memcpy(bytes.data() + at, &write.offset, sizeof(write.offset)); + std::memcpy(bytes.data() + at + sizeof(write.offset), &write.size, sizeof(write.size)); + std::memcpy(bytes.data() + at + sizeof(write.offset) + sizeof(write.size), write.data, write.size); + } + return bytes; +} + +std::vector decode_memory_journal(const std::string& journal, const std::vector& bytes, std::size_t memory_size) { + const auto malformed = [&journal]() { return std::runtime_error("SaveData: memory journal " + journal + " is malformed"); }; + if (bytes.size() < sizeof(MEM_JOURNAL_MAGIC) || std::memcmp(bytes.data(), MEM_JOURNAL_MAGIC, sizeof(MEM_JOURNAL_MAGIC)) != 0) { + throw malformed(); + } + std::vector writes; + std::size_t at = sizeof(MEM_JOURNAL_MAGIC); + while (at != bytes.size()) { + MemoryWrite write{}; + if (bytes.size() - at < sizeof(write.offset) + sizeof(write.size)) { + throw malformed(); + } + std::memcpy(&write.offset, bytes.data() + at, sizeof(write.offset)); + std::memcpy(&write.size, bytes.data() + at + sizeof(write.offset), sizeof(write.size)); + at += sizeof(write.offset) + sizeof(write.size); + if (write.size > bytes.size() - at || write.offset > memory_size || write.size > memory_size - write.offset) { + throw malformed(); + } + write.data = bytes.data() + at; + at += write.size; + writes.push_back(write); + } + return writes; +} + +void apply_memory_journal(std::fstream& memory, const std::string& journal, const std::vector& writes) { + for (const MemoryWrite& write : writes) { + memory.seekp(static_cast(write.offset)); + memory.write(write.data, static_cast(write.size)); + } + memory.flush(); + if (!memory) { + throw std::runtime_error("SaveData: cannot write memory journal " + journal + " into the save-data memory"); + } + std::error_code ec; + if (!std::filesystem::remove(journal, ec)) { + throw std::runtime_error("SaveData: cannot remove memory journal " + journal); + } +} + +void replay_memory_journal(std::int32_t user_id, std::uint32_t slot) { + const std::string journal = mem_path(user_id, slot, "journal"); + std::error_code ec; + const bool pending = std::filesystem::exists(journal, ec); + if (ec) { + throw std::runtime_error("SaveData: cannot check memory journal " + journal); + } + if (!pending) { + return; + } + const std::string path = mem_path(user_id, slot, "bin"); + std::vector bytes; + std::size_t size = 0; + if (!read_file_all(journal, bytes) || !file_size_of(path, &size)) { + throw std::runtime_error("SaveData: cannot replay memory journal " + journal); + } + const std::vector writes = decode_memory_journal(journal, bytes, size); + std::fstream memory(path, std::ios::binary | std::ios::in | std::ios::out); + if (!memory) { + throw std::runtime_error("SaveData: cannot open " + path + " to replay memory journal " + journal); + } + apply_memory_journal(memory, journal, writes); +} + std::string param_path(const std::string& real_path) { return real_path + ".param"; } @@ -145,7 +230,7 @@ SaveDataParam load_param(const std::string& real_path) { std::error_code ec; const auto written = std::filesystem::last_write_time(real_path, ec); if (!ec) { - const auto system = std::filesystem::file_time_type::clock::to_sys(written); + const auto system = std::chrono::file_clock::to_sys(written); param.mtime = std::chrono::duration_cast(system.time_since_epoch()).count(); } } @@ -217,6 +302,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); } @@ -224,6 +310,21 @@ static std::string blocks_path(const std::string& real_path) { return real_path + ".blocks"; } +static bool isMountedDirectory(const std::string& path) { + for (const auto& slot : g_slots) { + if (!slot.used) continue; + if (slot.real_path == path) return true; + if (!std::filesystem::exists(path) || !std::filesystem::exists(slot.real_path)) continue; + std::error_code error; + const bool sameDirectory = std::filesystem::equivalent(slot.real_path, path, error); + if (error && error != std::errc::no_such_file_or_directory) { + throw std::filesystem::filesystem_error("SaveData: failed to compare save paths", slot.real_path, path, error); + } + if (sameDirectory) return true; + } + return false; +} + static int deleteSave(const SaveDataDelete* del) { if (del == nullptr || del->dir_name == nullptr) { throw std::runtime_error("sceSaveDataDelete: null argument"); @@ -238,24 +339,9 @@ static int deleteSave(const SaveDataDelete* del) { } const std::string path = save_root() + "/" + dirName; std::lock_guard lock(g_slots_mutex); - for (const auto& slot : g_slots) { - if (!slot.used) { - continue; - } - if (slot.real_path == path) { - return SAVE_DATA_ERROR_BUSY; - } - std::error_code ec; - const bool same_directory = std::filesystem::equivalent(slot.real_path, path, ec); - if (ec && ec != std::errc::no_such_file_or_directory) { - throw std::filesystem::filesystem_error("sceSaveDataDelete: failed to compare save paths", slot.real_path, path, ec); - } - if (same_directory) { - return SAVE_DATA_ERROR_BUSY; - } - } + if (isMountedDirectory(path)) return SAVE_DATA_ERROR_BUSY; if (std::filesystem::is_directory(path)) { - std::filesystem::remove_all(path); + RemoveTree(path); } std::error_code ec; std::filesystem::remove(param_path(path), ec); @@ -271,10 +357,11 @@ 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; } -static constexpr std::uint64_t SAVE_DATA_BLOCK_BYTES = 32768; +static constexpr std::uint64_t SAVE_DATA_BLOCK_BYTES = 65536; static std::uint64_t save_blocks(const std::string& real_path) { std::ifstream file(blocks_path(real_path)); @@ -403,7 +490,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 +512,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); @@ -458,6 +545,7 @@ static int getSaveDataMemory2(SaveDataMemoryGet2* get_param) { return SAVE_DATA_ERROR_NOT_INITIALIZED; } std::lock_guard lk(g_mem_mutex); + replay_memory_journal(get_param->user_id, get_param->slot_id); const std::string path = mem_path(get_param->user_id, get_param->slot_id, "bin"); std::size_t size = 0; if (!file_size_of(path, &size)) { @@ -503,7 +591,7 @@ int APS5_VABI sceSaveDataInitialize3(const void* init) { int APS5_VABI sceSaveDataLoadIcon(const SaveDataMountPoint* mount_point, SaveDataIcon* icon) { (void)icon; if (mount_point == nullptr) { - throw std::runtime_error("sceSaveDataLoadIcon: null mount_point"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); if (find_slot_by_mount_point(mount_point->data) == -1) { @@ -529,19 +617,15 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result } const auto* nameEnd = static_cast(std::memchr(mount->dir_name->data, '\0', sizeof(mount->dir_name->data))); if (nameEnd == nullptr) { - throw std::runtime_error("sceSaveDataMount3: unterminated directory name"); + return SAVE_DATA_ERROR_PARAMETER; } const std::string dirName(mount->dir_name->data, static_cast(nameEnd - mount->dir_name->data)); if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) { - throw std::runtime_error("sceSaveDataMount3: invalid directory name"); + return SAVE_DATA_ERROR_PARAMETER; } const std::string real_path = save_root() + "/" + dirName; std::lock_guard lock(g_slots_mutex); - for (const auto& used : g_slots) { - if (used.used && used.real_path == real_path) { - return SAVE_DATA_ERROR_BUSY; - } - } + if (isMountedDirectory(real_path)) return SAVE_DATA_ERROR_BUSY; const bool exists = std::filesystem::is_directory(real_path); if (create && exists) { return SAVE_DATA_ERROR_EXISTS; @@ -576,7 +660,7 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result int APS5_VABI sceSaveDataMount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result) { const int rc = mount3(mount, mount_result); - SAVEDATA_TRACE("mount3 user=%d dir=%s mode=0x%x blocks=%llu -> 0x%08x point=%s status=%u", mount != nullptr ? mount->user_id : -1, mount != nullptr && mount->dir_name != nullptr ? mount->dir_name->data : "(null)", mount != nullptr ? mount->mount_mode : 0u, mount != nullptr ? static_cast(mount->blocks) : 0ull, static_cast(rc), mount_result != nullptr ? mount_result->mount_point.data : "", mount_result != nullptr ? mount_result->mount_status : 0u); + SAVEDATA_TRACE("mount3 user=%d dir=%.*s mode=0x%x blocks=%llu -> 0x%08x point=%.*s status=%u", mount != nullptr ? mount->user_id : -1, static_cast(sizeof(SceSaveDataDirName::data)), mount != nullptr && mount->dir_name != nullptr ? mount->dir_name->data : "(null)", mount != nullptr ? mount->mount_mode : 0u, mount != nullptr ? static_cast(mount->blocks) : 0ull, static_cast(rc), static_cast(sizeof(SaveDataMountPoint::data)), mount_result != nullptr ? mount_result->mount_point.data : "", mount_result != nullptr ? mount_result->mount_status : 0u); return rc; } @@ -589,7 +673,7 @@ int APS5_VABI sceSaveDataPrepare(const SaveDataMountPoint* mount_point, const Sa int APS5_VABI sceSaveDataSaveIcon(const SaveDataMountPoint* mount_point, const SaveDataIcon* icon) { (void)icon; if (mount_point == nullptr) { - throw std::runtime_error("sceSaveDataSaveIcon: null mount_point"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); if (find_slot_by_mount_point(mount_point->data) == -1) { @@ -606,7 +690,7 @@ int APS5_VABI sceSaveDataSaveIconByPath(const SaveDataMountPoint* mount_point, c static int setParam(const SaveDataMountPoint* mount_point, uint32_t param_type, const void* param_buf, size_t param_buf_size) { if (mount_point == nullptr || param_buf == nullptr) { - throw std::runtime_error("sceSaveDataSetParam: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); const int slot = find_slot_by_mount_point(mount_point->data); @@ -648,6 +732,7 @@ static int setSaveDataMemory2(const SaveDataMemorySet2* set_param) { return SAVE_DATA_ERROR_NOT_INITIALIZED; } std::lock_guard lk(g_mem_mutex); + replay_memory_journal(set_param->user_id, set_param->slot_id); const std::string path = mem_path(set_param->user_id, set_param->slot_id, "bin"); std::size_t size = 0; if (!file_size_of(path, &size)) { @@ -664,21 +749,27 @@ static int setSaveDataMemory2(const SaveDataMemorySet2* set_param) { return SAVE_DATA_ERROR_PARAMETER; } } - if (n != 0) { - std::vector memory; - if (!read_file_all(path, memory) || memory.size() != size) { + std::vector writes; + for (std::uint32_t i = 0; i < n; i++) { + const SaveDataMemoryData& d = set_param->data[i]; + if (d.buf_size != 0) { + writes.push_back({d.offset, d.buf_size, static_cast(d.buf)}); + } + } + if (!writes.empty()) { + std::fstream memory(path, std::ios::binary | std::ios::in | std::ios::out); + if (!memory) { return SAVE_DATA_ERROR_INTERNAL; } - for (std::uint32_t i = 0; i < n; i++) { - const SaveDataMemoryData& d = set_param->data[i]; - if (d.buf_size == 0) { - continue; - } - std::memcpy(memory.data() + d.offset, d.buf, d.buf_size); - } - if (!write_file_replace(path, memory)) { + const std::string journal = mem_path(set_param->user_id, set_param->slot_id, "journal"); + const std::vector bytes = encode_memory_journal(writes); + const bool staged = savedata::replace_file_with(path, bytes.data(), bytes.size(), [&journal](const auto& temporary, const auto&) { + return savedata::move_replacing(temporary, journal); + }); + if (!staged) { return SAVE_DATA_ERROR_INTERNAL; } + apply_memory_journal(memory, journal, writes); } if (set_param->param != nullptr) { std::vector pd(sizeof(SaveDataParam)); @@ -707,6 +798,7 @@ static int setupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDat return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lk(g_mem_mutex); + replay_memory_journal(setup_param->user_id, setup_param->slot_id); const std::string path = mem_path(setup_param->user_id, setup_param->slot_id, "bin"); std::size_t existed = 0; const bool have = file_size_of(path, &existed); @@ -756,10 +848,14 @@ int APS5_VABI sceSaveDataSyncSaveDataMemory(const void* sync_param) { if (sync_param == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } + if (g_initializations == 0) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; + } // Every Set already writes straight through to disk; sync only has to confirm the memory exists. const std::int32_t user_id = *static_cast(sync_param); const std::uint32_t slot_id = reinterpret_cast(sync_param)[1]; std::lock_guard lk(g_mem_mutex); + replay_memory_journal(user_id, slot_id); std::size_t size = 0; if (!file_size_of(mem_path(user_id, slot_id, "bin"), &size)) { return SAVE_DATA_ERROR_MEMORY_NOT_READY; @@ -800,7 +896,7 @@ int APS5_VABI sceSaveDataTransferringMount(const SaveDataTransferringMount* moun static int umount2(uint32_t mode, const SaveDataMountPoint* mount_point) { (void)mode; if (mount_point == nullptr) { - throw std::runtime_error("sceSaveDataUmount2: null mount_point"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); int slot = find_slot_by_mount_point(mount_point->data); @@ -882,7 +978,7 @@ int APS5_VABI sceSaveDataGetConvertProgress(float* progress) { return rc; } -int APS5_VABI sceSaveDataCancel() { +int APS5_VABI sceSaveDataCancel(std::uint32_t cancelArgument) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -893,10 +989,119 @@ int APS5_VABI sceSaveDataDirNameSearch2(const SaveDataDirNameSearchCond* cond, S return rc; } -APS5_EXPORT("RvID8BVeg+g", sceSaveDataUnknown_RvID8BVeg); -int APS5_VABI sceSaveDataUnknown_RvID8BVeg() { - NotImplemented_nid_no_patch("RvID8BVeg+g"); +int APS5_VABI sceSaveDataPrepareForTransferring(const SaveDataMountPoint* mountPoint, const SaveDataPrepareParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCheckIpmiIfSize() { + return 0; +} + +int APS5_VABI sceSaveDataDebug() { + return 0; +} + +int APS5_VABI sceSaveDataDebugCheckBackupData() { + return 0; +} + +int APS5_VABI sceSaveDataDebugCleanMount() { + return 0; +} + +int APS5_VABI sceSaveDataDebugCompiledSdkVersion() { + return 0; +} + +int APS5_VABI sceSaveDataDebugCreateSaveDataRoot() { + return 0; +} + +int APS5_VABI sceSaveDataDebugProspero() { + return 0; +} + +int APS5_VABI sceSaveDataDebugRemoveSaveDataRoot() { + return 0; +} + +int APS5_VABI sceSaveDataDebugTarget() { return 0; } } + +namespace { + +struct SaveDataMountPs4 { + std::int32_t user_id; + std::int32_t pad; + const void* title_id; + const SceSaveDataDirName* dir_name; + const void* fingerprint; + std::uint64_t blocks; + std::uint32_t mount_mode; + std::uint8_t reserved[32]; +}; + +} + +extern "C" { + +int APS5_VABI sceSaveDataInitialize(const void* init) { + return sceSaveDataInitialize3(init); +} + +int APS5_VABI sceSaveDataMount(const SaveDataMountPs4* mount, SaveDataMountResult* mount_result) { + if (mount == nullptr || mount->dir_name == nullptr) return SAVE_DATA_ERROR_PARAMETER; + if (mount->title_id != nullptr) NotImplemented_nid_no_patch("sceSaveDataMount of another title's save data"); + SaveDataMount3 converted{}; + converted.user_id = mount->user_id; + converted.dir_name = mount->dir_name; + converted.blocks = mount->blocks; + converted.mount_mode = mount->mount_mode; + return sceSaveDataMount3(&converted, mount_result); +} + +int APS5_VABI sceSaveDataUmount(const SaveDataMountPoint* mount_point) { + return sceSaveDataUmount2(0, mount_point); +} + +int APS5_VABI sceSaveDataSetupSaveDataMemory(std::int32_t user_id, std::size_t memory_size, const SaveDataParam* param) { + SaveDataMemorySetup2 setup{}; + setup.user_id = user_id; + setup.memory_size = memory_size; + SaveDataMemorySetupResult result{}; + if (const int rc = sceSaveDataSetupSaveDataMemory2(&setup, &result); rc != SAVE_DATA_OK) return rc; + if (param == nullptr) return SAVE_DATA_OK; + SaveDataMemorySet2 set{}; + set.user_id = user_id; + set.param = param; + return sceSaveDataSetSaveDataMemory2(&set); +} + +int APS5_VABI sceSaveDataGetSaveDataMemory(std::int32_t user_id, void* buf, std::size_t buf_size, std::int64_t offset) { + SaveDataMemoryData data{}; + data.buf = buf; + data.buf_size = buf_size; + data.offset = static_cast(offset); + SaveDataMemoryGet2 get{}; + get.user_id = user_id; + get.data = &data; + return sceSaveDataGetSaveDataMemory2(&get); +} + +int APS5_VABI sceSaveDataSetSaveDataMemory(std::int32_t user_id, void* buf, std::size_t buf_size, std::int64_t offset) { + SaveDataMemoryData data{}; + data.buf = buf; + data.buf_size = buf_size; + data.offset = static_cast(offset); + SaveDataMemorySet2 set{}; + set.user_id = user_id; + set.data = &data; + set.data_num = 1; + return sceSaveDataSetSaveDataMemory2(&set); +} + +} diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index ceffb09c01..1a8681cb38 100644 --- a/core/libs/prx/libSceSaveData.native/SaveData.hpp +++ b/core/libs/prx/libSceSaveData.native/SaveData.hpp @@ -25,7 +25,7 @@ constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_RDWR = 2; constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_CREATE = 4; constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_CREATE2 = 32; -constexpr std::uint64_t SAVE_DATA_BLOCKS_MAX = 32768; +constexpr std::uint64_t SAVE_DATA_BLOCKS_MAX = 16384; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_ALL = 0; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_TITLE = 1; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_SUB_TITLE = 2; diff --git a/core/libs/prx/libSceSaveData/Export.cpp b/core/libs/prx/libSceSaveData/Export.cpp index 0d44da8d1b..f2a68ec989 100644 --- a/core/libs/prx/libSceSaveData/Export.cpp +++ b/core/libs/prx/libSceSaveData/Export.cpp @@ -91,4 +91,335 @@ int APS5_VABI sceSaveDataTransferringMount(const SaveDataTransferringMount* moun SAVEDATA_TRACE("sceSaveDataTransferringMount -> 0x%08x", static_cast(rc)); return rc; } + +int APS5_VABI sceSaveDataAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataBindPsnAccount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataBindPsnAccountForSystemBackup(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataChangeDatabase(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataChangeInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCheckBackupData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCheckBackupDataForCdlg(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCheckBackupDataInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCheckBackupDataLight(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCheckCloudData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCheckSaveDataBroken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCheckSaveDataVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCheckSaveDataVersionLatest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataClearProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCreateUploadData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebugGetThreadId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDeleteAllUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDeleteBackupData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDeleteCloudData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDeleteProspero(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDeleteTitle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDeleteUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDirNameSearchInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDownload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetAllSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetAppLaunchedUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetAutoUploadConditions(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetAutoUploadRequestInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetAutoUploadSetting(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetBoundPsnAccountCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetClientThreadPriority(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetCloudQuotaInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetEventInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetMountedSaveDataCount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetSaveDataCount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetSaveDataRootDir(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetSaveDataRootPath(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetSaveDataRootUsbPath(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetSavePoint(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetUpdatedDataCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataInitialize2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataInitializeForCdlg(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataIsDeletingUsbDb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataIsMounted() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataManualDelete(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataManualDownload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataManualUpload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataMount5() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataMountInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataMountSys(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataRebuildDatabase(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataRegisterEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataRestoreBackupData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataRestoreBackupDataForCdlg(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataRestoreLoadSaveDataMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataSetAutoUploadSetting(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataSetEventInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataSetSaveDataLibraryUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataShutdownStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataSupportedFakeBrokenStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataSyncCloudList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataUmountSys(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataUmountWithBackup(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataUnregisterEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataUpload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceSaveData/SaveDataCore.hpp b/core/libs/prx/libSceSaveData/SaveDataCore.hpp index 200b607716..58afdfa721 100644 --- a/core/libs/prx/libSceSaveData/SaveDataCore.hpp +++ b/core/libs/prx/libSceSaveData/SaveDataCore.hpp @@ -22,6 +22,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/RemoveTree.hpp" #include "prx/libSceSaveData/SaveDataFile.hpp" namespace savedata { @@ -106,14 +107,6 @@ constexpr char SD_MEMORY_PREFIX[] = "sce_sdmemory"; constexpr std::size_t SD_MOUNT_PREFIX_LEN = sizeof(SD_ROOT_GUEST); constexpr std::size_t SD_MOUNT_POINT_CAPACITY = sizeof(SaveDataMountPoint) - SD_MOUNT_PREFIX_LEN - 1; -struct MemoryData { - void* buf; - std::size_t buf_size; - std::int64_t offset; - std::uint8_t reserved[40]; -}; -static_assert(sizeof(MemoryData) == 64, "SceSaveDataMemoryData is 64 bytes"); - struct MemorySync { std::int32_t user_id; std::uint32_t slot_id; @@ -506,14 +499,14 @@ int mount_internal(const SceSaveDataDirName* dir_name, std::uint32_t mount_mode, created = true; if (write_u64(save_blocks_path(directory), blocks) != SD_OK) { std::error_code cleanup; - std::filesystem::remove_all(directory, cleanup); + RemoveTree(directory, cleanup); return SD_ERROR_INTERNAL; } SaveDataParam initial {}; std::strncpy(initial.title, "Saved Data", sizeof(initial.title) - 1); if (!write_blob(save_param_path(directory), &initial, sizeof(initial))) { std::error_code cleanup; - std::filesystem::remove_all(directory, cleanup); + RemoveTree(directory, cleanup); return SD_ERROR_INTERNAL; } } @@ -582,7 +575,7 @@ int delete_internal(std::int32_t user_id, const SceSaveDataDirName* dir_name, if (!std::filesystem::exists(directory, error) || error) { return SD_ERROR_NOT_FOUND; } - std::filesystem::remove_all(directory, error); + RemoveTree(directory, error); RecordWrittenPath_nid_no_patch(directory); if (error) { return SD_ERROR_INTERNAL; @@ -873,10 +866,8 @@ std::filesystem::path memory_directory(std::int32_t user_id, std::uint32_t slot_ return save_root() / name / std::to_string(static_cast(user_id)); } -bool valid_memory_range(const MemoryData& data, std::size_t size) { - return data.buf != nullptr && data.offset >= 0 && - static_cast(data.offset) <= size && - data.buf_size <= size - static_cast(data.offset); +bool valid_memory_range(const SaveDataMemoryData& data, std::size_t size) { + return data.buf != nullptr && data.offset <= size && data.buf_size <= size - data.offset; } int setup_memory_internal(const SaveDataMemorySetup2* setup, SaveDataMemorySetupResult* result, @@ -959,14 +950,14 @@ int get_memory_internal(SaveDataMemoryGet2* get_param) { } const MemorySlot& slot = it->second; if (get_param->data != nullptr && - !valid_memory_range(*reinterpret_cast(get_param->data), slot.data.size())) { + !valid_memory_range(*get_param->data, slot.data.size())) { return SD_ERROR_PARAMETER; } if (get_param->param != nullptr && (slot.option & SD_MEMORY_SET_PARAM) == 0) { return SD_ERROR_PARAMETER; } if (get_param->data != nullptr) { - const MemoryData& data = *reinterpret_cast(get_param->data); + const SaveDataMemoryData& data = *get_param->data; std::memcpy(data.buf, slot.data.data() + data.offset, data.buf_size); } if (get_param->param != nullptr) { @@ -1001,7 +992,7 @@ int set_memory_internal(const SaveDataMemorySet2* set_param) { } const std::uint32_t count = set_param->data != nullptr && set_param->data_num == 0 ? 1u : set_param->data_num; - const MemoryData* descriptors = reinterpret_cast(set_param->data); + const SaveDataMemoryData* descriptors = set_param->data; for (std::uint32_t i = 0; i < count; i++) { if (!valid_memory_range(descriptors[i], it->second.data.size())) { return SD_ERROR_PARAMETER; @@ -1009,7 +1000,7 @@ int set_memory_internal(const SaveDataMemorySet2* set_param) { } MemorySlot updated = it->second; for (std::uint32_t i = 0; i < count; i++) { - const MemoryData& data = descriptors[i]; + const SaveDataMemoryData& data = descriptors[i]; std::memcpy(updated.data.data() + data.offset, data.buf, data.buf_size); } if (set_param->param != nullptr) { diff --git a/core/libs/prx/libSceSaveData/SaveDataFile.hpp b/core/libs/prx/libSceSaveData/SaveDataFile.hpp index 9b31589899..84fd888180 100644 --- a/core/libs/prx/libSceSaveData/SaveDataFile.hpp +++ b/core/libs/prx/libSceSaveData/SaveDataFile.hpp @@ -1,8 +1,11 @@ #pragma once +#include #include +#include #include -#include +#include +#include #include #ifdef _WIN32 @@ -20,6 +23,7 @@ namespace savedata { template inline bool replace_file_with(const std::filesystem::path& path, const void* data, std::size_t size, ReplaceOperation&& replace_operation) { + if (size != 0 && data == nullptr) return false; const auto parent = path.parent_path(); std::error_code error; if (!parent.empty()) { @@ -29,60 +33,57 @@ inline bool replace_file_with(const std::filesystem::path& path, const void* dat } } - auto temporary = path; - temporary += ".tmp"; - const auto remove_temporary = [&temporary]() { - std::error_code ignored; - std::filesystem::remove(temporary, ignored); - }; - - { - std::ofstream file(temporary, std::ios::binary | std::ios::trunc); - if (!file) { - remove_temporary(); - return false; - } - if (size != 0) { - if (data == nullptr) { - file.close(); - remove_temporary(); - return false; - } - file.write(static_cast(data), static_cast(size)); - } - file.flush(); - if (!file) { - file.close(); - remove_temporary(); - return false; - } - file.close(); - if (!file) { - remove_temporary(); - return false; - } + std::filesystem::path temporary; + std::FILE* file = nullptr; + std::random_device random; + for (unsigned attempt = 0; attempt < 128; ++attempt) { + temporary = path; + temporary += ".tmp." + std::to_string(random()) + "." + std::to_string(random()); +#ifdef _WIN32 + file = _wfopen(temporary.c_str(), L"wbx"); +#else + file = std::fopen(temporary.c_str(), "wbx"); +#endif + if (file) break; + if (errno != EEXIST) return false; } + if (!file) return false; + struct Cleanup { + const std::filesystem::path& path; + std::FILE*& file; + bool remove = true; + ~Cleanup() { + if (file) std::fclose(file); + std::error_code ignored; + if (remove) std::filesystem::remove(path, ignored); + } + } cleanup{temporary, file}; + + const bool written = size == 0 || std::fwrite(data, 1, size, file) == size; + const bool closed = std::fclose(file) == 0; + file = nullptr; + if (!written || !closed) return false; const bool replaced = replace_operation(temporary, path); - if (!replaced) { - remove_temporary(); - } + cleanup.remove = !replaced; return replaced; } -inline bool replace_file(const std::filesystem::path& path, const void* data, std::size_t size) { +inline bool move_replacing(const std::filesystem::path& source, const std::filesystem::path& destination) { #ifdef _WIN32 - return replace_file_with(path, data, size, [](const auto& temporary, const auto& destination) { - return ::MoveFileExW(temporary.c_str(), destination.c_str(), - MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0; - }); + return ::MoveFileExW(source.c_str(), destination.c_str(), + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0; #else - return replace_file_with(path, data, size, [](const auto& temporary, const auto& destination) { - std::error_code error; - std::filesystem::rename(temporary, destination, error); - return !error; - }); + std::error_code error; + std::filesystem::rename(source, destination, error); + return !error; #endif } +inline bool replace_file(const std::filesystem::path& path, const void* data, std::size_t size) { + return replace_file_with(path, data, size, [](const auto& temporary, const auto& destination) { + return move_replacing(temporary, destination); + }); +} + } 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/libSceScm/Export.cpp b/core/libs/prx/libSceScm/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceScm/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceScreenShot/CMakeLists.txt b/core/libs/prx/libSceScreenShot/CMakeLists.txt new file mode 100644 index 0000000000..0300d678fa --- /dev/null +++ b/core/libs/prx/libSceScreenShot/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceScreenShot SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceScreenShot PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceScreenShot) + +if(BUILD_TESTING) + add_test_executable(guest_screenshot_tests ${LIBS_INCLUDE_DIR}/tests/GuestScreenShot.cpp) + target_include_directories(guest_screenshot_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_screenshot_tests PRIVATE libSceScreenShot libkernel libc) + configure_windows_unwind(guest_screenshot_tests) + add_test(NAME guest_screenshot COMMAND guest_screenshot_tests small) + add_test(NAME guest_screenshot_large_overlay COMMAND guest_screenshot_tests large) + if(WIN32) + set_tests_properties(guest_screenshot guest_screenshot_large_overlay PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceScreenShot/Export.cpp b/core/libs/prx/libSceScreenShot/Export.cpp new file mode 100644 index 0000000000..dbc4cf09af --- /dev/null +++ b/core/libs/prx/libSceScreenShot/Export.cpp @@ -0,0 +1,111 @@ +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/File.hpp" + +extern "C" int APS5_VABI sceKernelFstat(int d, FileStat* sb); + +namespace { + +constexpr int ErrorArgument = static_cast(0x80BE0001); +constexpr int ErrorOverlayFile = static_cast(0x80BE0003); +constexpr int ErrorOverlaySize = static_cast(0x80BE000E); +constexpr std::uint32_t LargeOverlaySdkVersion = 0x3500000; +constexpr std::size_t MaxOverlayPath = 0x400; +constexpr std::uint16_t FileTypeMask = 0xF000; +constexpr std::uint16_t DirectoryType = 0x4000; + +std::atomic disabled{false}; + +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; +} + +int SetOverlayImage(const char* path, int offsetX, int offsetY) { + const bool large = CompiledSdkVersion() >= LargeOverlaySdkVersion; + const int maxX = large ? 3839 : 1919; + const int maxY = large ? 2159 : 1079; + const std::int64_t maxSize = large ? 8 * 1024 * 1024 : 2 * 1024 * 1024; + if (!path || offsetX < 0 || offsetX > maxX || offsetY < 0 || offsetY > maxY) return ErrorArgument; + if (strnlen(path, MaxOverlayPath) >= MaxOverlayPath) return ErrorArgument; + const int descriptor = sceKernelOpen(path, 0, 0); + if (descriptor < 0) return ErrorOverlayFile; + FileStat stat{}; + int result = ErrorOverlayFile; + if (sceKernelFstat(descriptor, &stat) == 0 && (stat.st_mode & FileTypeMask) != DirectoryType) { + result = stat.st_size > 0 && stat.st_size <= maxSize ? 0 : ErrorOverlaySize; + } + sceKernelClose(descriptor); + return result; +} + +} + +extern "C" { + +int APS5_VABI sceScreenShotSetOverlayImage(const char* path, int offsetX, int offsetY) { + return SetOverlayImage(path, offsetX, offsetY); +} + +int APS5_VABI sceScreenShotSetOverlayImageWithOrigin(const char* path, int offsetX, int offsetY, int origin) { + if (origin < 1 || origin > 9) return ErrorArgument; + return SetOverlayImage(path, offsetX, offsetY); +} + +int APS5_VABI sceScreenShotDisable() { + disabled = true; + return 0; +} + +int APS5_VABI sceScreenShotEnable() { + disabled = false; + return 0; +} + +int APS5_VABI sceScreenShotIsDisabled(int id, int* isDisabled) { + if (id == -1 || !isDisabled) return ErrorArgument; + *isDisabled = disabled ? 1 : 0; + return 0; +} + +int APS5_VABI sceScreenShotDisableNotification() { + return 0; +} + +int APS5_VABI sceScreenShotEnableNotification() { + return 0; +} + +int APS5_VABI sceScreenShotCapture() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotGetAppInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotGetDrcParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotIsVshScreenCaptureDisabled() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotSetDrcParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceScreenShotSetParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSdma/Export.cpp b/core/libs/prx/libSceSdma/Export.cpp new file mode 100644 index 0000000000..97a2ea23cc --- /dev/null +++ b/core/libs/prx/libSceSdma/Export.cpp @@ -0,0 +1,139 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN +int APS5_VABI sceSdmaBeginQueueMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCancelQueue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaConstantFill(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaConstantFillNonBlocking(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyLinear(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyLinearNonBlocking(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyTiled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyTiledNonBlocking(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyWindowL2L(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyWindowL2LNonBlocking(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyWindowT2T(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyWindowT2TNonBlocking(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyWindowTiled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyWindowTiledNonBlocking(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaEndQueueMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaFenceNonBlocking(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaFinalize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaFlushAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaFlushAllWithEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaKickQueue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaQueryWorkingBufferSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaTryFlush(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaWaitForEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceShare.native/Export.cpp b/core/libs/prx/libSceShare.native/Export.cpp index 2d8f2cf361..fb34206c21 100644 --- a/core/libs/prx/libSceShare.native/Export.cpp +++ b/core/libs/prx/libSceShare.native/Export.cpp @@ -3,86 +3,8 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -// Capture and sharing are not emulated; policy and parameter calls are accepted. - extern "C" { -int APS5_VABI sceShareCaptureScreenshot(const void* param, int32_t* req_id) { - (void)param; - (void)req_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShareCaptureVideoClip(const void* param, int32_t* req_id) { - (void)param; - (void)req_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShareFeaturePermit(uint32_t feature_flags) { - (void)feature_flags; - return 0; -} - -int APS5_VABI sceShareFeatureProhibit(uint32_t feature_flags) { - (void)feature_flags; - return 0; -} - -int APS5_VABI sceShareGetCurrentStatus(uint32_t feature_flag, ShareCurrentStatus* status) { - (void)feature_flag; - (void)status; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShareInitialize(size_t heap_size, int thread_priority, uint64_t affinity_mask) { - (void)heap_size; - (void)thread_priority; - (void)affinity_mask; - return 0; -} - -int APS5_VABI sceShareOpenMenuForContent(const void* content_id) { - (void)content_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShareRegisterContentEventCallback(void* callback, void* user_data) { - (void)callback; - (void)user_data; - return 0; -} - -int APS5_VABI sceShareSetCaptureSource(uint32_t feature_flags, const void* tap_point) { - (void)feature_flags; - (void)tap_point; - return 0; -} - -int APS5_VABI sceShareSetContentParam(const char* content_param) { - (void)content_param; - return 0; -} - -int APS5_VABI sceShareSetScreenshotOverlayImage(const char* file_path, int32_t margin_x, int32_t margin_y, int32_t origin) { - (void)file_path; - (void)margin_x; - (void)margin_y; - (void)origin; - return 0; -} - -int APS5_VABI sceShareTerminate(void) { - return 0; -} - -int APS5_VABI sceShareUnregisterContentEventCallback(void* callback) { - (void)callback; - return 0; -} +APS5_DUMMY_FUN } diff --git a/core/libs/prx/libSceShare/Export.cpp b/core/libs/prx/libSceShare/Export.cpp index a313edc364..375682887e 100644 --- a/core/libs/prx/libSceShare/Export.cpp +++ b/core/libs/prx/libSceShare/Export.cpp @@ -6,6 +6,28 @@ // Capture and sharing are not emulated; policy and parameter calls are accepted. +struct ShareScreenshotParamExtended { + std::uint64_t size; + std::uint32_t source; + std::uint32_t mode; + const char* text; +}; + +struct ShareVideoClipParamExtended { + std::uint64_t size; + std::uint64_t value8; + std::uint64_t value16; + std::uint32_t mode; + std::uint32_t reserved; + const char* text; +}; + +static_assert(sizeof(ShareScreenshotParamExtended) == 24); +static_assert(offsetof(ShareScreenshotParamExtended, text) == 16); +static_assert(sizeof(ShareVideoClipParamExtended) == 40); +static_assert(offsetof(ShareVideoClipParamExtended, mode) == 24); +static_assert(offsetof(ShareVideoClipParamExtended, text) == 32); + namespace { constexpr std::int32_t ERROR_INVALID_PARAM = static_cast(0x81960002); @@ -96,20 +118,16 @@ int APS5_VABI sceShareUnregisterContentEventCallback(void* callback) { return 0; } -int APS5_VABI sceShareCaptureScreenshotExtended(const void* extended_param, int32_t* req_id) { - (void)extended_param; - if (req_id != nullptr) { - *req_id = REQUEST_ID_INVALID; - } +int APS5_VABI sceShareCaptureScreenshotExtended(const ShareScreenshotParamExtended* param, std::int32_t* requestId) { + if (param == nullptr || param->size != sizeof(*param)) return ERROR_INVALID_PARAM; + if (requestId != nullptr) *requestId = REQUEST_ID_INVALID; return ERROR_NOT_SUPPORTED; } - -int APS5_VABI sceShareCaptureVideoClipExtended(const void* extended_param, int32_t* req_id) { - (void)extended_param; - if (req_id != nullptr) { - *req_id = REQUEST_ID_INVALID; - } +int APS5_VABI sceShareCaptureVideoClipExtended(const ShareVideoClipParamExtended* param, std::int32_t* requestId) { + if (param == nullptr || param->size != sizeof(*param)) return ERROR_INVALID_PARAM; + if (requestId != nullptr) *requestId = REQUEST_ID_INVALID; + if (param->reserved != 0 || (param->mode == 0 && param->value16 != 0)) return ERROR_INVALID_PARAM; return ERROR_NOT_SUPPORTED; } @@ -121,10 +139,11 @@ int APS5_VABI sceShareGetRunningStatus(uint32_t* status) { return 0; } -int APS5_VABI sceShareSetContentParamForApplicationTitle(const char* application_title) { - if (application_title == nullptr) { - return ERROR_INVALID_PARAM; - } +int APS5_VABI sceShareSetContentParamForApplicationTitle(const char* applicationTitle) { + if (applicationTitle == nullptr) return ERROR_INVALID_PARAM; + std::size_t length = 0; + while (length < 5120 && applicationTitle[length] != 0) ++length; + if (length == 0 || length == 5120) return ERROR_INVALID_PARAM; return 0; } diff --git a/core/libs/prx/libSceShareFactoryUtil/Export.cpp b/core/libs/prx/libSceShareFactoryUtil/Export.cpp new file mode 100644 index 0000000000..ca9e3680ad --- /dev/null +++ b/core/libs/prx/libSceShareFactoryUtil/Export.cpp @@ -0,0 +1,38 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceShareFactoryUtilBackToGameFromShareFactory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareFactoryUtilGetLaunchStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareFactoryUtilGetLaunchStatusOfShareFactory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareFactoryUtilInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareFactoryUtilLaunchShareFactory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareFactoryUtilTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceShareInternal.native/Export.cpp b/core/libs/prx/libSceShareInternal.native/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceShareInternal.native/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceSharePlay/Export.cpp b/core/libs/prx/libSceSharePlay/Export.cpp index 9f8cc23fee..a1dcf5eadf 100644 --- a/core/libs/prx/libSceSharePlay/Export.cpp +++ b/core/libs/prx/libSceSharePlay/Export.cpp @@ -1,24 +1,36 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" static constexpr int SCE_SHARE_PLAY_ERROR_INVALID_ARGS = static_cast(0x810E0001); +static constexpr int SCE_SHARE_PLAY_ERROR_ALREADY_INITIALIZED = static_cast(0x810E0003); +static constexpr int SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED = static_cast(0x810E0004); +static std::mutex g_mutex; +static bool g_initialized = false; static constexpr std::int32_t SCE_USER_SERVICE_USER_ID_INVALID = -1; extern "C" { int APS5_VABI sceSharePlayInitialize(void* heap, size_t heapSize) { - (void)heap; - if (heapSize == 0) APS5_INVALID_ARG_EX; + std::lock_guard lock(g_mutex); + if (g_initialized) return SCE_SHARE_PLAY_ERROR_ALREADY_INITIALIZED; + if (heap && heapSize < 0x1800) return SCE_SHARE_PLAY_ERROR_INVALID_ARGS; + g_initialized = true; return 0; } int APS5_VABI sceSharePlayTerminate(void) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED; + g_initialized = false; return 0; } int APS5_VABI sceSharePlayGetCurrentConnectionInfoA(SharePlayConnectionInfoA* info) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED; if (info == nullptr) return SCE_SHARE_PLAY_ERROR_INVALID_ARGS; *info = {}; info->hostUserId = SCE_USER_SERVICE_USER_ID_INVALID; @@ -26,9 +38,91 @@ int APS5_VABI sceSharePlayGetCurrentConnectionInfoA(SharePlayConnectionInfoA* in return 0; } -int APS5_VABI sceSharePlayProhibitController(int prohibited) { +int APS5_VABI sceSharePlayProhibitController(std::uint8_t prohibited) { + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED; if (prohibited != 0 && prohibited != 1) APS5_INVALID_ARG_EX; return 0; } +int APS5_VABI sceSharePlayCrashDaemon(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSharePlayGetCurrentConnectionInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSharePlayGetCurrentInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSharePlayGetEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSharePlayNotifyDialogOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSharePlayNotifyForceCloseForCdlg(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSharePlayResumeScreenForCdlg(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSharePlayServerLock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSharePlayServerUnLock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSharePlaySetMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSharePlaySetProhibition(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSharePlaySetProhibitionModeWithAppId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSharePlayStartStandby(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSharePlayStartStreaming(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSharePlayStopStandby(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSharePlayStopStreaming(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceShareUtility/Export.cpp b/core/libs/prx/libSceShareUtility/Export.cpp new file mode 100644 index 0000000000..b8d35a3625 --- /dev/null +++ b/core/libs/prx/libSceShareUtility/Export.cpp @@ -0,0 +1,118 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceShareUtilityAdvanceFromFileWithContentIdList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceFromFileWithTitleIdList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceGetServiceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceQueryServiceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceQueryServiceInfoA() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceResetPlayingContent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceSetPlayingContent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceSetUploadContentData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceValidateContents() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceWakeUpShareMenu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceWakeUpShareMenuBroadcast() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceWakeUpShareMenuDefault() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityAdvanceWakeUpShareMenuForShareContent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityInitializeEx2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityOpenShareMenu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityOpenShareMenuBroadcast() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityOpenShareMenuDefault() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityOpenShareMenuForShareContent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareUtilityTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceShellUIUtil/Export.cpp b/core/libs/prx/libSceShellUIUtil/Export.cpp new file mode 100644 index 0000000000..2837093a4f --- /dev/null +++ b/core/libs/prx/libSceShellUIUtil/Export.cpp @@ -0,0 +1,40 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +struct ShellUIUtilLaunchParameter; + +extern "C" { + +int APS5_VABI sceShellUIUtilCreateTaskToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilGetAppUrl(const char* application, const char* parameter, char* output, std::uint32_t capacity, std::uint32_t* outputSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilLaunchByUri(const char* uri, const ShellUIUtilLaunchParameter* parameter) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilLaunchTask(std::int32_t taskToken, const char* uri, char* output, std::uint32_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellUIUtilTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSmart/Export.cpp b/core/libs/prx/libSceSmart/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceSmart/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceSocialScreen/Export.cpp b/core/libs/prx/libSceSocialScreen/Export.cpp new file mode 100644 index 0000000000..ebaf31823a --- /dev/null +++ b/core/libs/prx/libSceSocialScreen/Export.cpp @@ -0,0 +1,63 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceSocialScreenCloseSeparateMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSocialScreenConfigureSeparateMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSocialScreenCrashDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSocialScreenGetIgnoreSeparateMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSocialScreenInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSocialScreenInitializeSeparateModeParameter() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSocialScreenOpenSeparateMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSocialScreenReserveHighFrameRateVideoCaptureRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSocialScreenSetIgnoreSeparateMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSocialScreenSetMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSocialScreenTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSocialScreenDialog/CMakeLists.txt b/core/libs/prx/libSceSocialScreenDialog/CMakeLists.txt new file mode 100644 index 0000000000..83af8dda26 --- /dev/null +++ b/core/libs/prx/libSceSocialScreenDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceSocialScreenDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceSocialScreenDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceSocialScreenDialog) + +if(BUILD_TESTING) + add_test_executable(guest_social_screen_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocialScreenDialog.cpp) + target_include_directories(guest_social_screen_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_social_screen_dialog_tests PRIVATE libSceSocialScreenDialog) + add_test(NAME guest_social_screen_dialog COMMAND guest_social_screen_dialog_tests) + if(WIN32) + set_tests_properties(guest_social_screen_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceSocialScreenDialog/Export.cpp b/core/libs/prx/libSceSocialScreenDialog/Export.cpp new file mode 100644 index 0000000000..c3c5849381 --- /dev/null +++ b/core/libs/prx/libSceSocialScreenDialog/Export.cpp @@ -0,0 +1,71 @@ +#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 sceSocialScreenDialogInitialize(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 sceSocialScreenDialogOpen(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 sceSocialScreenDialogUpdateStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceSocialScreenDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceSocialScreenDialogGetResult(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 sceSocialScreenDialogClose(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 sceSocialScreenDialogTerminate(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/libSceSpSysCallWrapper/Export.cpp b/core/libs/prx/libSceSpSysCallWrapper/Export.cpp new file mode 100644 index 0000000000..e15c0a9054 --- /dev/null +++ b/core/libs/prx/libSceSpSysCallWrapper/Export.cpp @@ -0,0 +1,248 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceSpGetPid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpKernelGettimeofday() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpKernelNanosleep() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetConnect() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetEpollAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetEpollControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetEpollCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetEpollDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetEpollWait() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetErrnoLoc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetPoolCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetPoolDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetRecv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetResolverAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetResolverCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetResolverDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetResolverGetError() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetResolverStartNtoa() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetSend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetSetsockopt() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetSocket() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpNetSocketClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadAttrDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadAttrInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadAttrSetinheritsched() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadAttrSetschedparam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadAttrSetstacksize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadCondDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadCondInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadCondSignal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadCondWait() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadCondattrDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadCondattrInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadDetach() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadExit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadJoin() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadMutexDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadMutexInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadMutexLock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadMutexUnlock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadMutexattrDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadMutexattrInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadMutexattrSetprioceiling() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadMutexattrSetprotocol() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadMutexattrSettype() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpPthreadSetprio() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSpSelect() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSrcUtl/Export.cpp b/core/libs/prx/libSceSrcUtl/Export.cpp new file mode 100644 index 0000000000..fac2eada51 --- /dev/null +++ b/core/libs/prx/libSceSrcUtl/Export.cpp @@ -0,0 +1,53 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceSrcUtilityFinalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSrcUtilityFlush() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSrcUtilityGetDelaySamples() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSrcUtilityGetHandleSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSrcUtilityGetPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSrcUtilityInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSrcUtilityInitializeEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSrcUtilityResample() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSrcUtilitySetPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSsl/CMakeLists.txt b/core/libs/prx/libSceSsl/CMakeLists.txt index 8142808e06..cb39ff0e4c 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,22 @@ 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() + add_test_executable(guest_ssl_ps5_readers_tests ${LIBS_INCLUDE_DIR}/tests/GuestSslPs5Readers.cpp) + target_include_directories(guest_ssl_ps5_readers_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ssl_ps5_readers_tests PRIVATE libSceSsl libc) + configure_windows_unwind(guest_ssl_ps5_readers_tests) + add_test(NAME guest_ssl_ps5_readers COMMAND guest_ssl_ps5_readers_tests) 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_ps5_readers 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..ceb7654ba3 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -1,15 +1,32 @@ +#ifdef _WIN32 +#include +#else +#include +#include +#endif + #include "SslTypes.hpp" +#include "prx/libc/include/ApplicationHeap.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 #include #include @@ -26,10 +43,54 @@ namespace { constexpr int ErrorNotFound = static_cast(0x8095f004); constexpr int ErrorInvalidArg = static_cast(0x8095177a); constexpr int ErrorInvalidId = static_cast(0x8095f006); +constexpr int ErrorInvalidCert = static_cast(0x8095f00a); +constexpr int ErrorCnCheck = static_cast(0x8095f00b); +constexpr int ErrorUnknownCa = static_cast(0x8095f00c); +constexpr int ErrorNotAfterCheck = static_cast(0x8095f00d); +constexpr int ErrorNotBeforeCheck = static_cast(0x8095f00e); +constexpr int ErrorFatalAlert = static_cast(0x8095f010); +constexpr int ErrorWantPollIn = static_cast(0x8095f013); +constexpr int ErrorWantPollOut = static_cast(0x8095f014); +constexpr int ErrorSocketClosed = static_cast(0x8095170d); +constexpr int ErrorCallbackCheck = static_cast(0x8095f0fd); +constexpr int ErrorInvalidValue = static_cast(0x8095f007); +constexpr int ErrorBadLength = static_cast(0x80951773); +constexpr int ErrorBadRecord = static_cast(0x80951d55); +constexpr int ErrorNetBadDescriptor = static_cast(0x80410109); +constexpr int ErrorNetPipe = static_cast(0x80410120); +constexpr int ErrorNetReset = static_cast(0x80410136); +constexpr int ErrorKernelInvalid = static_cast(0x80020016); +constexpr std::uint32_t FlagServerVerify = 0x1; +constexpr std::uint32_t FlagCnCheck = 0x4; +constexpr std::uint32_t FlagNotAfterCheck = 0x8; +constexpr std::uint32_t FlagNotBeforeCheck = 0x10; +constexpr std::uint32_t FlagKnownCaCheck = 0x20; +constexpr std::uint32_t VerifyOptionMask = 0x3f; +constexpr std::uint32_t InitialVerifyOptions = FlagServerVerify | FlagCnCheck | FlagKnownCaCheck; + +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 +99,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 +128,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 +166,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); } @@ -135,9 +206,14 @@ struct Random { } }; +struct EncodedPrivateKey { + Block bytes; + std::size_t size; +}; + struct Identity { std::shared_ptr chain; - PrivateKey key; + std::shared_ptr key; }; struct Snapshot { @@ -146,6 +222,18 @@ struct Snapshot { std::size_t count; }; +struct CertNameEntry { + std::string oid; + std::string value; +}; + +struct CertName { + void* certificate = nullptr; + bool subject = false; + std::weak_ptr owner; + std::vector entries; +}; + struct Context { Pool pool; std::vector> authorities; @@ -153,22 +241,251 @@ 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 +} + +bool HostReset(int error) { +#ifdef _WIN32 + return error == WSAECONNRESET || error == WSAECONNABORTED || error == WSAENETRESET; +#else + return error == ECONNRESET || error == EPIPE; +#endif +} + +bool HostTimedOut(int error) { +#ifdef _WIN32 + return error == WSAETIMEDOUT; +#else + (void)error; + return false; +#endif +} + +enum class Operation { Connect, Read, Write, Close }; + +struct Session { + int contextHandle; + std::shared_ptr context; + std::shared_ptr identity; + std::shared_ptr peer; + std::vector peerHandles; + std::shared_ptr socketOwner; + std::uintptr_t socket; + std::mutex mutex; + int hostError = 0; + std::uint32_t verifyOptions = 0; + std::vector alpnStrings; + std::vector alpnList; + std::string alpnSelected; + Block peekBuffer; + std::size_t peekOffset = 0; + std::size_t peekSize = 0; + bool handshaken = false; + bool verifying = false; + bool verified = false; + bool closed = false; + bool reset = false; + bool gone = false; + int verifyResult = 0; + unsigned int failedOptions = 0; + Random random; + PrivateKey privateKey; + mbedtls_x509_crt authorities; + mbedtls_ssl_config config; + mbedtls_ssl_context ssl; + + Session(int handle, std::shared_ptr owner, std::shared_ptr socketHolder, std::uintptr_t native) + : contextHandle(handle), context(std::move(owner)), identity(context->identity), socketOwner(std::move(socketHolder)), socket(native) { + mbedtls_x509_crt_init(&authorities); + mbedtls_ssl_config_init(&config); + mbedtls_ssl_init(&ssl); + } + + ~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, const std::string& alpn, std::uint32_t options) { + verifyOptions = options; + 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_OPTIONAL); + mbedtls_ssl_conf_min_tls_version(&config, MBEDTLS_SSL_VERSION_TLS1_2); + mbedtls_ssl_conf_ca_chain(&config, &authorities, nullptr); + mbedtls_ssl_conf_rng(&config, mbedtls_ctr_drbg_random, &random.drbg); + if (identity) { + if (!identity->chain || !identity->key) throw std::logic_error("Incomplete SSL client identity is not supported"); + const auto& encoded = *identity->key; + auto bytes = Allocate(Add(encoded.size, 1)); + std::memcpy(bytes.get(), encoded.bytes.get(), encoded.size); + std::size_t first = 0; + while (first < encoded.size && (bytes.get()[first] == ' ' || bytes.get()[first] == '\r' || bytes.get()[first] == '\n' || bytes.get()[first] == '\t')) ++first; + const auto size = first < encoded.size && bytes.get()[first] == '-' ? Add(encoded.size, 1) : encoded.size; + CheckBackend(mbedtls_pk_parse_key(&privateKey.key, bytes.get(), size, nullptr, 0, mbedtls_ctr_drbg_random, &random.drbg), "Invalid SSL private key"); + CheckBackend(mbedtls_pk_check_pair(&identity->chain->root.pk, &privateKey.key, mbedtls_ctr_drbg_random, &random.drbg), "SSL client certificate and private key do not match"); + CheckBackend(mbedtls_ssl_conf_own_cert(&config, &identity->chain->root, &privateKey.key), "SSL client certificate setup failed"); + } + for (std::size_t start = 0; start <= alpn.size();) { + const auto end = alpn.find(',', start); + const auto protocol = alpn.substr(start, end == std::string::npos ? std::string::npos : end - start); + if (!protocol.empty()) alpnStrings.push_back(protocol); + if (end == std::string::npos) break; + start = end + 1; + } + if (!alpnStrings.empty()) { + for (const auto& protocol : alpnStrings) alpnList.push_back(protocol.c_str()); + alpnList.push_back(nullptr); + CheckBackend(mbedtls_ssl_conf_alpn_protocols(&config, alpnList.data()), "SSL ALPN list rejected"); + } + 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 + auto* owner = static_cast(session.socketOwner.get()); + const auto sent = owner && owner->transfer + ? owner->transfer(owner, true, const_cast(buffer), chunk) + : ::send(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); +#else + const auto sent = ::send(static_cast(session.socket), buffer, static_cast(chunk), MSG_NOSIGNAL); +#endif + if (sent >= 0) return static_cast(sent); + const int error = HostError(); + if (HostInterrupted(error)) continue; + if (HostWouldBlock(error) || HostTimedOut(error)) return MBEDTLS_ERR_SSL_WANT_WRITE; + if (HostReset(error)) return MBEDTLS_ERR_NET_CONN_RESET; + session.hostError = error; + return MBEDTLS_ERR_NET_SEND_FAILED; + } + } + + 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 + auto* owner = static_cast(session.socketOwner.get()); + const auto received = owner && owner->transfer + ? owner->transfer(owner, false, buffer, chunk) + : ::recv(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); +#else + const auto received = ::recv(static_cast(session.socket), buffer, static_cast(chunk), 0); +#endif + if (received >= 0) return static_cast(received); + const int error = HostError(); + if (HostInterrupted(error)) continue; + if (HostWouldBlock(error) || HostTimedOut(error)) return MBEDTLS_ERR_SSL_WANT_READ; + if (HostReset(error)) return MBEDTLS_ERR_NET_CONN_RESET; + session.hostError = error; + return MBEDTLS_ERR_NET_RECV_FAILED; + } + } + + void Verify() { + const auto flags = mbedtls_ssl_get_verify_result(&ssl); + failedOptions = 0; + verifyResult = 0; + if (!(verifyOptions & FlagServerVerify)) return; + if ((verifyOptions & FlagKnownCaCheck) && (flags & MBEDTLS_X509_BADCERT_NOT_TRUSTED)) failedOptions |= FlagKnownCaCheck; + if ((verifyOptions & FlagCnCheck) && (flags & MBEDTLS_X509_BADCERT_CN_MISMATCH)) failedOptions |= FlagCnCheck; + if ((verifyOptions & FlagNotAfterCheck) && (flags & MBEDTLS_X509_BADCERT_EXPIRED)) failedOptions |= FlagNotAfterCheck; + if ((verifyOptions & FlagNotBeforeCheck) && (flags & MBEDTLS_X509_BADCERT_FUTURE)) failedOptions |= FlagNotBeforeCheck; + constexpr std::uint32_t optional = + MBEDTLS_X509_BADCERT_NOT_TRUSTED | MBEDTLS_X509_BADCERT_CN_MISMATCH | MBEDTLS_X509_BADCERT_EXPIRED | MBEDTLS_X509_BADCERT_FUTURE; + if (failedOptions & FlagKnownCaCheck) verifyResult = ErrorUnknownCa; + else if (failedOptions & FlagCnCheck) verifyResult = ErrorCnCheck; + else if (failedOptions & FlagNotAfterCheck) verifyResult = ErrorNotAfterCheck; + else if (failedOptions & FlagNotBeforeCheck) verifyResult = ErrorNotBeforeCheck; + else if (flags & ~optional) verifyResult = ErrorInvalidCert; + } + + int Code(int result, Operation operation) { + switch (result) { + case MBEDTLS_ERR_SSL_WANT_READ: return ErrorWantPollIn; + case MBEDTLS_ERR_SSL_WANT_WRITE: return ErrorWantPollOut; + case MBEDTLS_ERR_NET_CONN_RESET: + reset = true; + if (operation == Operation::Write) return ErrorNetPipe; + if (operation == Operation::Connect) return ErrorSocketClosed; + return ErrorNetReset; + case MBEDTLS_ERR_SSL_CONN_EOF: return operation == Operation::Connect ? ErrorSocketClosed : 0; + case MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE: return ErrorFatalAlert; + case MBEDTLS_ERR_SSL_INVALID_MAC: return operation == Operation::Read ? ErrorBadRecord : 0; + default: return 0; + } + } + + [[noreturn]] void Fail(const char* function, int error) const { + if (currentPoolExhausted || error == MBEDTLS_ERR_SSL_ALLOC_FAILED) throw std::bad_alloc(); + 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 SslVersionSsl30 = 2; +constexpr int SslVersionTls12 = 5; + struct SslConnection { int context; int socket; std::string host; SslVerifyCallback verifyCallback; void* verifyArgument; + std::uint32_t verifyOptions = InitialVerifyOptions; + int minimumVersion = SslVersionTls12; + 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() { @@ -188,13 +505,38 @@ Context& GetContext(int handle) { return *found->second; } -SslConnection& RequireConnection(const char* function, int sslConnectionId) { +SslConnection* FindConnection(int sslConnectionId) { const auto connection = State().connections.find(sslConnectionId); - if (connection == State().connections.end()) throw std::invalid_argument(std::string(function) + ": unknown connection"); - if (!State().contexts.contains(connection->second.context)) { - throw std::invalid_argument(std::string(function) + ": the connection's context was terminated"); + if (connection == State().connections.end()) return nullptr; + if (!State().contexts.contains(connection->second.context)) return nullptr; + return &connection->second; +} + +SslConnection& RequireConnection(const char* function, int sslConnectionId) { + auto* connection = FindConnection(sslConnectionId); + if (!connection) throw std::invalid_argument(std::string(function) + ": unknown connection"); + return *connection; +} + +void RequireUsable(const char* function, const Session& session) { + if (session.verifyResult != 0 && session.verifyResult != ErrorCallbackCheck) + throw std::logic_error(std::string(function) + ": the connection failed verification; the console's behaviour is unknown"); +} + +void RegisterPeer(Session& session) { + const auto* certificate = mbedtls_ssl_get_peer_cert(&session.ssl); + if (!certificate || session.peer) return; + auto chain = std::make_shared(); + for (const auto* entry = certificate; entry && entry->raw.p; entry = entry->next) + CheckBackend(mbedtls_x509_crt_parse_der(&chain->root, entry->raw.p, entry->raw.len), "SSL peer certificate copy failed"); + std::lock_guard lock(State().mutex); + for (const auto* entry = &chain->root; entry && entry->raw.p; entry = entry->next) { + auto handle = std::make_unique(CertificateHandle{chain, entry}); + session.peerHandles.push_back(handle.get()); + chain->handles.push_back(handle.get()); + session.context->certificateHandles.push_back(std::move(handle)); } - return connection->second; + session.peer = std::move(chain); } struct Input { @@ -236,16 +578,78 @@ 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"); +} + +const char* LongAttributeName(const char* shortName) { + static constexpr std::pair names[] = { + {"CN", "commonName"}, {"C", "countryName"}, {"L", "localityName"}, {"ST", "stateOrProvinceName"}, + {"O", "organizationName"}, {"OU", "organizationalUnitName"}, {"SN", "surname"}, {"GN", "givenName"}, + {"DC", "domainComponent"}, {"uid", "userId"}, + }; + for (const auto& [name, longName] : names) + if (std::strcmp(shortName, name) == 0) return longName; + return shortName; +} + +void* ExtractCertName(const char* function, int sslCtxId, void* sslCert, bool subject) { + std::lock_guard lock(State().mutex); + if (!sslCert || !State().contexts.contains(sslCtxId)) return nullptr; + auto& context = GetContext(sslCtxId); + const auto* crt = RequireCertificate(function, sslCtxId, sslCert); + for (const auto& [handle, stored] : context.certNames) { + if (stored->certificate == sslCert && stored->subject == subject) return handle; + } + const auto* name = subject ? &crt->subject : &crt->issuer; + auto certName = std::make_unique(); + certName->certificate = sslCert; + certName->subject = subject; + certName->owner = static_cast(sslCert)->owner; + 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 = LongAttributeName(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" { int APS5_VABI sceSslInit_nid_postfix(std::uint64_t poolSize) { + if (poolSize == 0) return ErrorKernelInvalid; auto& state = State(); std::lock_guard lock(state.mutex); if (state.nextHandle > static_cast(std::numeric_limits::max())) throw std::overflow_error("SSL context handles exhausted"); 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; } @@ -253,34 +657,35 @@ int APS5_VABI sceSslInit_nid_postfix(std::uint64_t poolSize) { int APS5_VABI sceSslTerm_nid_postfix(int sslCtxId) { auto& state = State(); std::lock_guard lock(state.mutex); - state.contexts.erase(sslCtxId); + if (!state.contexts.erase(sslCtxId)) return ErrorInvalidId; return 0; } int APS5_VABI sceSslGetMemoryPoolStats(int sslCtxId, SceSsl::MemoryPoolStats* stats) { - if (!stats) return static_cast(0x8095F007); + if (!stats) return ErrorInvalidValue; auto& state = State(); std::lock_guard lock(state.mutex); const auto found = state.contexts.find(sslCtxId); if (found == state.contexts.end()) { std::memset(stats, 0, sizeof(*stats)); - return static_cast(0x8095F006); + return ErrorInvalidId; } - 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; } int APS5_VABI sceSslLoadCert(int sslCtxId, int caCertNum, SceSsl::Data** caList, SceSsl::Data* cert, SceSsl::Data* privateKey) { - if (caCertNum < 0 || (caCertNum && !caList) || (!!cert != !!privateKey)) - throw std::invalid_argument("Invalid SSL certificate load arguments"); auto& state = State(); std::lock_guard lock(state.mutex); - auto& context = GetContext(sslCtxId); + const auto found = state.contexts.find(sslCtxId); + if (found == state.contexts.end()) return ErrorInvalidId; + auto& context = *found->second; PoolScope scope(context.pool); std::vector> authorities; std::vector> handles; - for (int index = 0; index < caCertNum; ++index) { + for (int index = 0; caList && index < caCertNum; ++index) { if (!caList[index]) throw std::invalid_argument("Missing SSL certificate descriptor"); auto chain = ParseCertificate(*caList[index]); for (auto* entry = &chain->root; entry; entry = entry->next) { @@ -290,40 +695,51 @@ int APS5_VABI sceSslLoadCert(int sslCtxId, int caCertNum, SceSsl::Data** caList, } authorities.push_back(std::move(chain)); } - std::shared_ptr identity; - if (cert) { - identity = std::make_shared(); - identity->chain = ParseCertificate(*cert); - const auto key = CopyInput(*privateKey); - Random random; - random.Seed(); - CheckBackend(mbedtls_pk_parse_key(&identity->key.key, key.bytes.get(), key.size, nullptr, 0, - mbedtls_ctr_drbg_random, &random.drbg), "Invalid SSL private key"); - CheckBackend(mbedtls_pk_check_pair(&identity->chain->root.pk, &identity->key.key, mbedtls_ctr_drbg_random, &random.drbg), - "SSL client certificate and private key do not match"); - } context.authorities.reserve(Add(context.authorities.size(), authorities.size())); context.certificateHandles.reserve(Add(context.certificateHandles.size(), handles.size())); for (auto& chain : authorities) context.authorities.push_back(std::move(chain)); for (auto& handle : handles) context.certificateHandles.push_back(std::move(handle)); - if (identity) context.identity = std::move(identity); + if (cert) { + if (!cert->ptr) return static_cast(0x80951771); + context.identity = std::make_shared(); + context.identity->chain = ParseCertificate(*cert); + } + if (privateKey) { + if (!privateKey->ptr) return static_cast(0x80951771); + if (!context.identity) context.identity = std::make_shared(); + auto identity = std::make_shared(*context.identity); + context.identity = identity; + identity->key.reset(); + auto bytes = Allocate(privateKey->size); + std::memcpy(bytes.get(), privateKey->ptr, privateKey->size); + identity->key = std::make_shared(EncodedPrivateKey{std::move(bytes), privateKey->size}); + } return 0; } int APS5_VABI sceSslUnloadCert(int sslCtxId) { auto& state = State(); std::lock_guard lock(state.mutex); - auto& context = GetContext(sslCtxId); + const auto found = state.contexts.find(sslCtxId); + if (found == state.contexts.end()) { + const auto* parameters = static_cast(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t sdkVersion; + std::memcpy(&sdkVersion, parameters + 16, sizeof(sdkVersion)); + return sdkVersion < 0x07500000 ? 0 : ErrorInvalidId; + } + auto& context = *found->second; context.authorities.clear(); context.identity.reset(); return 0; } int APS5_VABI sceSslGetCaList(int sslCtxId, SceSsl::CaList* output) { - if (!output) return ErrorInvalidArg; + if (!output) return ErrorInvalidValue; auto& state = State(); std::lock_guard lock(state.mutex); - auto& context = GetContext(sslCtxId); + const auto found = state.contexts.find(sslCtxId); + if (found == state.contexts.end()) return ErrorInvalidId; + auto& context = *found->second; PoolScope scope(context.pool); const auto count = CertificateCount(context); if (!count) { @@ -355,14 +771,11 @@ int APS5_VABI sceSslFreeCaList(int sslCtxId, SceSsl::CaList* list) { } int APS5_VABI sceSslGetCaCerts(int sslCtxId, SceSsl::CaCerts* output) { - if (!output) return ErrorInvalidArg; auto& state = State(); std::lock_guard lock(state.mutex); const auto found = state.contexts.find(sslCtxId); - if (found == state.contexts.end()) { - *output = {}; - return ErrorNotFound; - } + if (found == state.contexts.end()) return ErrorInvalidId; + if (!output) return ErrorInvalidValue; auto& context = *found->second; PoolScope scope(context.pool); const auto count = CertificateCount(context); @@ -409,7 +822,7 @@ int APS5_VABI sceSslFreeCaCerts(int sslCtxId, SceSsl::CaCerts* certificates) { } int APS5_VABI sceSslCreateConnection(int sslCtxId, int sockId, const char* host) { - if (!host) throw std::invalid_argument("sceSslCreateConnection: null host"); + if (!host) return ErrorInvalidValue; std::lock_guard lock(State().mutex); if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; auto& state = State(); @@ -421,104 +834,522 @@ int APS5_VABI sceSslCreateConnection(int sslCtxId, int sockId, const char* host) } int APS5_VABI sceSslDeleteConnection(int sslConnectionId) { + std::shared_ptr session; std::lock_guard lock(State().mutex); - if (!State().connections.erase(sslConnectionId)) throw std::invalid_argument("sceSslDeleteConnection: unknown connection"); + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + session = std::move(connection->session); + State().connections.erase(sslConnectionId); return 0; } int APS5_VABI sceSslSetVerifyCallback(int sslConnectionId, SslVerifyCallback cbfunc, void* userArg) { std::lock_guard lock(State().mutex); - SslConnection& connection = RequireConnection(__func__, sslConnectionId); - connection.verifyCallback = cbfunc; - connection.verifyArgument = userArg; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + connection->verifyCallback = cbfunc; + connection->verifyArgument = userArg; return 0; } +} + +namespace { + +int RunCallback(Session& session, std::unique_lock& sessionLock, SslVerifyCallback callback, void* argument) { + session.verifying = true; + sessionLock.unlock(); + const int decision = callback(session.contextHandle, session.failedOptions, session.peerHandles.data(), + static_cast(session.peerHandles.size()), argument); + sessionLock.lock(); + session.verifying = false; + return decision < 0 ? ErrorCallbackCheck : 0; +} + +int Establish(const char* function, int sslConnectionId, std::shared_ptr& session) { + SslVerifyCallback callback = nullptr; + void* callbackArgument = nullptr; + { + auto& state = State(); + std::lock_guard lock(state.mutex); + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (!connection->session) { + auto context = state.contexts.at(connection->context); + std::shared_ptr owner; + std::uintptr_t native = 0; + if (!KernelLookupSocketNative_nid_no_patch(connection->socket, &owner, &native)) return ErrorNetBadDescriptor; + PoolScope scope(context->pool); + session = std::make_shared(connection->context, std::move(context), std::move(owner), native); + session->Configure(connection->host, connection->alpn, connection->verifyOptions); + connection->session = session; + } else { + session = connection->session; + } + callback = connection->verifyCallback; + callbackArgument = connection->verifyArgument; + } + std::unique_lock sessionLock(session->mutex); + if (session->verifying) throw std::logic_error(std::string(function) + ": the verify callback is running"); + if (session->verified) { + if (session->verifyResult == ErrorCallbackCheck && callback) session->verifyResult = RunCallback(*session, sessionLock, callback, callbackArgument); + return session->verifyResult; + } + if (session->gone) return 0; + { + PoolScope scope(session->context->pool); + if (!session->handshaken) { + const int result = mbedtls_ssl_handshake(&session->ssl); + if (result == MBEDTLS_ERR_SSL_WANT_READ || result == MBEDTLS_ERR_SSL_WANT_WRITE) return session->Code(result, Operation::Connect); + if (result == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE) { + session->gone = true; + return ErrorFatalAlert; + } + if (result != 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(); + } + if (const int failure = session->Code(result, Operation::Connect)) return failure; + session->Fail(function, result); + } + session->handshaken = true; + const char* selected = mbedtls_ssl_get_alpn_protocol(&session->ssl); + session->alpnSelected = selected ? selected : ""; + } + session->Verify(); + if (callback && session->verifyResult == ErrorInvalidCert) + throw std::runtime_error(std::string(function) + ": the chain failed a check no verify option names; what the console passes to the verify callback is unknown"); + if (callback) RegisterPeer(*session); + } + if (callback) session->verifyResult = RunCallback(*session, sessionLock, callback, callbackArgument); + session->verified = true; + if (session->verifyResult != 0 && session->verifyResult != ErrorCallbackCheck) { + PoolScope scope(session->context->pool); + mbedtls_ssl_close_notify(&session->ssl); + session->closed = true; + } + return session->verifyResult; +} + +} + +extern "C" { + int APS5_VABI sceSslConnect(int sslConnectionId) { - (void)sslConnectionId; - NotImplemented_nid_no_patch(__func__); - return 0; + std::shared_ptr session; + return Establish(__func__, sslConnectionId, session); } 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 (static_cast(flags) > 3) return ErrorInvalidValue; + if (!buf && len) throw std::invalid_argument("sceSslRead: null buffer"); + std::shared_ptr session; + if (const int code = Establish(__func__, sslConnectionId, session)) { + if (session) { + std::lock_guard sessionLock(session->mutex); + RequireUsable(__func__, *session); + } + return code; + } + std::lock_guard sessionLock(session->mutex); + if (session->gone || session->closed) return 0; + if (session->reset) return ErrorNetReset; + PoolScope scope(session->context->pool); + auto* bytes = static_cast(buf); + const auto total = std::min(len, INT_MAX); + std::size_t received = 0; + while (received < total) { + if (session->peekOffset < session->peekSize) { + const auto count = std::min(total - received, session->peekSize - session->peekOffset); + std::memcpy(bytes + received, session->peekBuffer.get() + session->peekOffset, count); + received += count; + if (flags == 3) break; + session->peekOffset += count; + if (session->peekOffset == session->peekSize) { + session->peekOffset = 0; + session->peekSize = 0; + session->peekBuffer.reset(); + } + if (flags != 0) break; + continue; + } + if (flags == 3 && !session->peekBuffer) session->peekBuffer = Allocate(MBEDTLS_SSL_IN_CONTENT_LEN); + const int result = flags == 3 + ? mbedtls_ssl_read(&session->ssl, session->peekBuffer.get(), MBEDTLS_SSL_IN_CONTENT_LEN) + : mbedtls_ssl_read(&session->ssl, bytes + received, total - received); + if (result > 0 && flags == 3) { + session->peekSize = static_cast(result); + continue; + } + if (result > 0) { + received += static_cast(result); + if (flags != 0) break; + continue; + } + if (result == 0 || result == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY || result == MBEDTLS_ERR_SSL_CONN_EOF) break; + const int code = session->Code(result, Operation::Read); + if (code == 0) session->Fail(__func__, result); + if (received) break; + return code; + } + return static_cast(received); } int APS5_VABI sceSslWrite(int sslConnectionId, void* buf, size_t len, int flags) { - (void)sslConnectionId; - (void)buf; - (void)len; - (void)flags; + if (flags != 0) return ErrorInvalidValue; + if (!buf && len) throw std::invalid_argument("sceSslWrite: null buffer"); + std::shared_ptr session; + if (const int code = Establish(__func__, sslConnectionId, session)) { + if (session) { + std::lock_guard sessionLock(session->mutex); + RequireUsable(__func__, *session); + } + return code; + } + std::lock_guard sessionLock(session->mutex); + if (session->gone) return ErrorSocketClosed; + if (session->closed) throw std::logic_error("sceSslWrite: the connection was closed; the console's behaviour is unknown"); + if (session->reset) return ErrorNetPipe; + PoolScope scope(session->context->pool); + const auto* bytes = static_cast(buf); + const auto total = std::min(len, INT_MAX); + std::size_t written = 0; + while (written < total) { + const int result = mbedtls_ssl_write(&session->ssl, bytes + written, total - written); + if (result < 0) { + const int code = session->Code(result, Operation::Write); + if (code == 0) session->Fail(__func__, result); + if (written) break; + return code; + } + written += static_cast(result); + } + return static_cast(written); +} + +int APS5_VABI sceSslClose(int sslConnectionId) { + std::shared_ptr session; + { + std::lock_guard lock(State().mutex); + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + session = connection->session; + } + if (!session) return 0; + std::lock_guard sessionLock(session->mutex); + if (session->gone || session->closed || !session->handshaken) return 0; + if (session->reset) return ErrorNetReset; + PoolScope scope(session->context->pool); + const int result = mbedtls_ssl_close_notify(&session->ssl); + if (result == MBEDTLS_ERR_SSL_WANT_WRITE || result == MBEDTLS_ERR_SSL_WANT_READ) return session->Code(result, Operation::Close); + if (result == MBEDTLS_ERR_NET_CONN_RESET) return session->Code(result, Operation::Close); + if (result != 0) session->Fail(__func__, result); + session->closed = true; + return 0; +} + +int APS5_VABI sceSslGetSerialNumber(int sslCtxId, void* sslCert, unsigned char* sboData, std::size_t* sboLen) { + std::lock_guard lock(State().mutex); + if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; + if (!sslCert) return ErrorInvalidValue; + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + if (!sboData) { + if (sboLen) *sboLen = crt->serial.len; + return 0; + } + if (!sboLen) throw std::invalid_argument("sceSslGetSerialNumber: output buffer requires a length pointer"); + const auto copyLength = std::min(*sboLen, crt->serial.len); + if (copyLength != 0) std::memcpy(sboData, crt->serial.p, copyLength); + return 0; +} + +int APS5_VABI sceSslFreeSslCertName(int sslCtxId, void* certName) { + std::lock_guard lock(State().mutex); + if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; + return certName ? 0 : ErrorInvalidValue; +} + +int APS5_VABI sceSslEnableVerifyOption(int sslConnectionId, std::uint32_t option) { + std::lock_guard lock(State().mutex); + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (option & ~VerifyOptionMask) return ErrorInvalidValue; + connection->verifyOptions |= option; + return 0; +} + +int APS5_VABI sceSslDisableVerifyOption(int sslConnectionId, std::uint32_t option) { + std::lock_guard lock(State().mutex); + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (option & ~VerifyOptionMask) return ErrorInvalidValue; + connection->verifyOptions &= ~option; + return 0; +} + +const char* APS5_VABI sceSslGetAlpnSelected(int sslConnectionId) { + std::shared_ptr session; + { + std::lock_guard lock(State().mutex); + const auto* connection = FindConnection(sslConnectionId); + if (!connection) return nullptr; + session = connection->session; + } + if (!session) return nullptr; + std::lock_guard sessionLock(session->mutex); + if (!session->handshaken || session->alpnSelected.empty()) return nullptr; + return session->alpnSelected.c_str(); +} + +int APS5_VABI sceSslSetAlpn(int sslConnectionId, const char* proto) { + std::lock_guard lock(State().mutex); + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + connection->alpn.clear(); + if (!proto) return 0; + std::size_t length = 0; + while (length < 65535 && proto[length] != 0) ++length; + if (length >= 65535) return ErrorBadLength; + std::size_t tokenLength = 0; + for (std::size_t index = 0; index <= length; ++index) { + if (index == length || proto[index] == ',') { + if (tokenLength == 0 || tokenLength > 255) return ErrorBadLength; + tokenLength = 0; + } else { + ++tokenLength; + } + } + connection->alpn.assign(proto, length); + return 0; +} + +int APS5_VABI sceSslSetMinSslVersion(int sslConnectionId, int version) { + std::lock_guard lock(State().mutex); + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (version < SslVersionSsl30 || version > SslVersionTls12) return ErrorInvalidValue; + connection->minimumVersion = version; + return 0; +} + +void* APS5_VABI sceSslGetIssuerName(int sslCtxId, void* sslCert) { + return ExtractCertName(__func__, sslCtxId, sslCert, false); +} + +int APS5_VABI sceSslGetNameEntryCount(int sslCtxId, void* certName) { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (!state.contexts.contains(sslCtxId)) return ErrorInvalidId; + if (!certName) return ErrorInvalidValue; + auto& context = GetContext(sslCtxId); + const auto found = context.certNames.find(certName); + if (found == context.certNames.end() || found->second->owner.expired()) throw std::invalid_argument("sceSslGetNameEntryCount: unknown or expired 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) { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (!state.contexts.contains(sslCtxId)) return ErrorInvalidId; + if (!certName || !oidname || !value || !valueLen) return ErrorInvalidValue; + auto& context = GetContext(sslCtxId); + const auto found = context.certNames.find(certName); + if (found == context.certNames.end() || found->second->owner.expired()) throw std::invalid_argument("sceSslGetNameEntryInfo: unknown or expired cert name"); + const auto& entries = found->second->entries; + if (entryNum < 0 || static_cast(entryNum) >= entries.size()) return ErrorInvalidValue; + const auto& entry = entries[static_cast(entryNum)]; + const auto copyLength = std::min(entry.value.size(), maxValueLen); + if (copyLength != 0) std::memcpy(value, entry.value.data(), copyLength); + *valueLen = copyLength; + if (maxOidnameLen != 0) { + std::memset(oidname, 0, maxOidnameLen); + std::memcpy(oidname, entry.oid.data(), std::min(entry.oid.size(), maxOidnameLen)); + } + return 0; +} + +int APS5_VABI sceSslGetNotBefore(int sslCtxId, void* sslCert, SceSsl::RtcTick* begin) { + std::lock_guard lock(State().mutex); + if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; + if (!sslCert || !begin) return ErrorInvalidValue; + 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) { + std::lock_guard lock(State().mutex); + if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; + if (!sslCert || !limit) return ErrorInvalidValue; + 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) { + std::lock_guard lock(State().mutex); + if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; + if (!sslCert || !fingerprint) return ErrorInvalidValue; + 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; + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + if (mbedtls_md(info, crt->raw.p, crt->raw.len, fingerprint) != 0) return ErrorInvalidArg; + return 0; +} + +int APS5_VABI sceSslGetPem(int sslCtxId, void* sslCert, char* buf, std::size_t* bufLen) { + if (!sslCert || !bufLen) return ErrorInvalidValue; + std::lock_guard lock(State().mutex); + if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + const std::size_t encodedLength = Add(crt->raw.len, 2) / 3 * 4; + const std::size_t lineCount = Add(encodedLength, 63) / 64; + const std::size_t required = Add(57, Add(encodedLength, lineCount * 2)); + const std::size_t capacity = *bufLen; + if (buf && capacity != 0) std::memset(buf, 0, capacity); + *bufLen = required; + if (!buf) return 0; + if (capacity < required) return static_cast(0x8095f008); + constexpr char header[] = "-----BEGIN CERTIFICATE-----\r\n"; + constexpr char footer[] = "-----END CERTIFICATE-----\r\n"; + std::memcpy(buf, header, sizeof(header) - 1); + std::size_t offset = sizeof(header) - 1; + for (std::size_t position = 0; position < crt->raw.len; position += 48) { + std::size_t written = 0; + CheckBackend(mbedtls_base64_encode(reinterpret_cast(buf + offset), capacity - offset, &written, crt->raw.p + position, std::min(48, crt->raw.len - position)), "SSL PEM encoding failed"); + offset += written; + buf[offset++] = '\r'; + buf[offset++] = '\n'; + } + std::memcpy(buf + offset, footer, sizeof(footer)); + return 0; +} + +void* APS5_VABI sceSslGetSubjectName(int sslCtxId, void* sslCert) { + return ExtractCertName(__func__, sslCtxId, sslCert, true); +} + +int APS5_VABI CA_MGMT_extractKeyBlobEx() { 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) { +int APS5_VABI CA_MGMT_extractPublicKeyInfo() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSslDisableVerifyOption(void) { +int APS5_VABI CA_MGMT_freeKeyBlob() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSslGetAlpnSelected(void) { +int APS5_VABI CRYPTO_initAsymmetricKey() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSslSetAlpn(void) { +int APS5_VABI CRYPTO_uninitAsymmetricKey() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSslSetMinSslVersion(void) { +int APS5_VABI RSA_verifySignature() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSslGetIssuerName(void) { +int APS5_VABI VLONG_freeVlongQueue() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSslGetNameEntryCount(void) { +int APS5_VABI sceSslCheckRecvPending() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSslGetNameEntryInfo(void) { +int APS5_VABI sceSslCreateSslConnection() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSslGetPem(void) { +int APS5_VABI sceSslDeleteSslConnection() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSslGetSubjectName(void) { +int APS5_VABI sceSslDisableOption() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSslEnableVerifyOption(void) { +int APS5_VABI sceSslDisableOptionInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslDisableOptionInternalInsecure() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslEnableOption() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslEnableOptionInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslGetNanoSSLModuleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslGetPeerCert() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslGetSslError() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslLoadRootCACert() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslRecv(int sslConnectionId, void* buf, std::size_t fulllen) { + return sceSslRead(sslConnectionId, buf, fulllen, 0); +} + +int APS5_VABI sceSslReuseConnection(int sslConnectionId, int sockId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslSend(int sslConnectionId, void* msg, std::size_t fulllen) { + return sceSslWrite(sslConnectionId, msg, fulllen, 0); +} + +int APS5_VABI sceSslSetSslVersion() { NotImplemented_nid_no_patch(__func__); return 0; } 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/libSceSulphaDrv/Export.cpp b/core/libs/prx/libSceSulphaDrv/Export.cpp new file mode 100644 index 0000000000..b956051655 --- /dev/null +++ b/core/libs/prx/libSceSulphaDrv/Export.cpp @@ -0,0 +1,49 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN +int APS5_VABI sceSulphaGetClientInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaGetConnectedClientCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaGetDefaultConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaGetNeededMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaMessage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaSetBookmark(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaShutdown(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSysBridge/Export.cpp b/core/libs/prx/libSceSysBridge/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceSysBridge/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceSysCore/Export.cpp b/core/libs/prx/libSceSysCore/Export.cpp new file mode 100644 index 0000000000..f06a188fd8 --- /dev/null +++ b/core/libs/prx/libSceSysCore/Export.cpp @@ -0,0 +1,19 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +struct ApplicationInfo { + std::byte data[152]; +}; + +static_assert(sizeof(ApplicationInfo) == 152); + +extern "C" { + +int APS5_VABI sceApplicationGetAppInfoByAppId(std::uint32_t applicationId, ApplicationInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSysUtil/Export.cpp b/core/libs/prx/libSceSysUtil/Export.cpp new file mode 100644 index 0000000000..6ff461210b --- /dev/null +++ b/core/libs/prx/libSceSysUtil/Export.cpp @@ -0,0 +1,128 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceSysUtilSendSystemNotificationWithText(int32_t type, const char* message) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendAddressingSystemNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendAddressingSystemNotificationWithDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendAddressingSystemNotificationWithUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendAddressingSystemNotificationWithUserName() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendNotificationRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendNpDebugNotificationRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendSystemNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendSystemNotification2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendSystemNotificationParamInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendSystemNotificationWithAppId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendSystemNotificationWithAppInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendSystemNotificationWithAppName() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendSystemNotificationWithAppNameRelatedToUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendSystemNotificationWithDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendSystemNotificationWithDeviceIdRelatedToUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendSystemNotificationWithErrorCode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendSystemNotificationWithParams() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendSystemNotificationWithTextRelatedToUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendSystemNotificationWithUserId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendSystemNotificationWithUserName() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendSystemNotificationWithUserNameInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendTrcCheckNotificationRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysUtilSendWebDebugNotificationRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSysmodule/Export.cpp b/core/libs/prx/libSceSysmodule/Export.cpp index 8ed9239477..78b4774cfa 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); @@ -210,4 +272,71 @@ int APS5_VABI sceSysmoduleUnloadModuleInternal(std::uint32_t id) { return 0; } +int APS5_VABI sceSysmoduleUnloadModuleInternalWithArg(std::uint32_t id, int argc, void* argv, std::uint64_t unk, int* ret) { + (void)argc; + (void)argv; + (void)unk; + if ((id & 0x7fffffffu) == 0) { + return SCE_SYSMODULE_ERROR_INVALID_VALUE; + } + if (!findModuleName(id)) { + throw std::runtime_error(std::string("sceSysmoduleUnloadModuleInternalWithArg: unknown id ") + std::to_string(id)); + } + std::lock_guard lock(gMutex); + auto it = gLoadCount.find(id); + if (it == gLoadCount.end() || it->second < 1) { + return SCE_SYSMODULE_ERROR_UNLOADED; + } + it->second--; + if (ret) { + *ret = 0; + } + return 0; +} + +int APS5_VABI sceSysmoduleIsLoadedInternal(std::uint32_t id) { + if ((id & 0x7fffffffu) == 0) { + return SCE_SYSMODULE_ERROR_INVALID_VALUE; + } + if (!findModuleName(id)) { + throw std::runtime_error(std::string("sceSysmoduleIsLoadedInternal: unknown id ") + std::to_string(id)); + } + std::lock_guard lock(gMutex); + const auto it = gLoadCount.find(id); + if (it == gLoadCount.end() || it->second < 1) { + return SCE_SYSMODULE_ERROR_UNLOADED; + } + return 0; +} + +int APS5_VABI sceSysmoduleIsCalledFromSysModule() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysmoduleIsCameraPreloaded() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysmoduleLoadModuleByNameInternal(std::uint8_t* fname, std::uint64_t a1, std::uint64_t a2, std::uint64_t a3, std::uint64_t a4, std::uint64_t a5) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysmoduleMapLibcForLibkernel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysmodulePreloadModuleForLibkernel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSysmoduleUnloadModuleByNameInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceSysmodule/ModuleTable.hpp b/core/libs/prx/libSceSysmodule/ModuleTable.hpp index 038095889c..eba12e75ac 100644 --- a/core/libs/prx/libSceSysmodule/ModuleTable.hpp +++ b/core/libs/prx/libSceSysmodule/ModuleTable.hpp @@ -7,6 +7,7 @@ inline const std::unordered_map kModuleTable = { {0x00000001, "libSceAudioOut"}, {0x00000002, "libSceAudioIn"}, + {0x00000005, "libScePerf"}, {0x00000006, "libSceFiber"}, {0x00000007, "libSceUlt"}, {0x0000000b, "libSceNgs2"}, @@ -147,9 +148,11 @@ inline const std::unordered_map kModuleTable = { {0x00000107, "libSceGic"}, {0x00000108, "libScePlayReady2"}, {0x00000109, "libSceHdrScopes"}, + {0x0000010a, "libSceDeci5"}, {0x0000010b, "libSceAcm"}, {0x0000010c, "libSceCesCs"}, {0x0000010d, "libScePlayerInvitationDialog"}, + {0x0000010e, "libScePlayerSelectionDialog"}, {0x0000010f, "libSceShare"}, {0x00000110, "libSceNpTrophy2"}, {0x00000112, "libSceNpSessionSignaling"}, @@ -158,8 +161,17 @@ inline const std::unordered_map kModuleTable = { {0x00000115, "libSceNpCppWebApi"}, {0x00000116, "libSceHubAppUtil"}, {0x0000011a, "libSceNpPartner001"}, + {0x0000011b, "libSceFrontPanelDisplay"}, {0x00000120, "libSceAudioPropagation"}, + {0x00000125, "libSceTextToSpeech2"}, + {0x00000126, "libSceCdlgG2P"}, + {0x00000127, "libSceCdlgPlayerReview"}, + {0x00000128, "libSceDepth2"}, + {0x0000012a, "libSceHmd2"}, + {0x0000012b, "libSceVrTracker2"}, {0x0000012f, "libSceFontGs"}, + {0x00000130, "libSceVrSetupDialog"}, + {0x00000132, "libSceProprietaryVoiceChatHelperPrx"}, {0x00000135, "libSceFontGsm"}, {0x00000137, "libScePsml"}, {0x00000138, "libSceNpPartnerSubscription"}, @@ -273,23 +285,74 @@ inline const std::unordered_map kModuleTable = { {0x80000078, "libSceHttpCache"}, {0x80000079, "libSceNKWeb"}, {0x8000007a, "libSceNKWebKit"}, + {0x8000007b, "libSceGpuTrace"}, {0x8000007c, "libSceVnaInternal"}, {0x8000007d, "libSceVnaWebsocket"}, + {0x8000007e, "libSceAjmi"}, + {0x8000007f, "libSceGpuCapture"}, + {0x80000080, "libSceAgcDriver"}, + {0x80000082, "libSceAudiodecCpuTrhd"}, + {0x80000083, "libSceAudioSystem"}, + {0x80000084, "libSceVenc"}, + {0x80000085, "libSceAvcap2"}, + {0x80000086, "libSceAgcVsh"}, + {0x80000087, "libSceAgcVshDebug"}, + {0x80000088, "libSceSystemLogger2NativeQueueClient"}, + {0x80000089, "libSceSystemLogger2Delivery"}, {0x8000008a, "libSceComposite"}, {0x8000008b, "libSceCompositeExt"}, {0x8000008c, "libSceHttp2"}, {0x8000008d, "libSceNpGameIntent"}, + {0x8000008e, "libSceShareInternal"}, {0x8000008f, "libSceNpWebApi2"}, + {0x80000090, "libSceNgs2"}, + {0x80000091, "libSceVcodec"}, + {0x80000092, "libSceResourceArbitrator"}, + {0x80000093, "libSceAgcResourceRegistration"}, {0x80000094, "libSceAgc"}, + {0x80000095, "libSceMediaFrameworkInterface"}, + {0x80000097, "libSceSystemTts"}, {0x80000098, "libScePosixForWebKit"}, + {0x80000099, "libSceVoiceCommand"}, {0x8000009a, "libSceNpRemotePlaySessionSignaling"}, + {0x8000009b, "libSceEmbeddedTtsCoreG3"}, + {0x8000009c, "libSceEmbeddedTts"}, + {0x8000009e, "libSceJemspace"}, + {0x8000009f, "libSceSystemLogger2Game"}, + {0x800000a0, "libSceNpTcs"}, + {0x800000a1, "libSceAbstractTcs"}, + {0x800000a2, "libSceTEEClient"}, + {0x800000a3, "libSceBgsStorage"}, + {0x800000a4, "libSceWebmParserMdrw"}, + {0x800000a6, "libSceIdu"}, + {0x800000a7, "libSceAppDbShellCoreClient"}, + {0x800000a8, "libSceIcu"}, + {0x800000a9, "libSceGLSlimVSH"}, {0x800000aa, "libicu"}, {0x800000ab, "libpng16"}, {0x800000ac, "libcairo"}, + {0x800000ad, "libSceContentListController"}, + {0x800000ae, "libSceSocialScreen"}, + {0x800000af, "libSceVdecSvp9"}, + {0x800000b0, "libSceJsc"}, {0x800000b1, "libcurl"}, + {0x800000b2, "libSceVideoStreamingEngine_sys"}, + {0x800000b3, "libSceSystemLogger2"}, + {0x800000b4, "libSceJxr"}, + {0x800000b5, "libSceJxrParser"}, + {0x800000b6, "libSceMediaFrameworkUtil"}, + {0x800000b7, "libSceAmpr"}, {0x800000b8, "libSceLibreSsl3"}, + {0x800000b9, "libSceRemotePlayClientIpc"}, + {0x800000ba, "libSceEsvmEngine"}, + {0x800000bb, "libSceWorkspace"}, + {0x800000bc, "libSceAvcap2mvr"}, + {0x800000bd, "libSceHdrScopes"}, + {0x800000be, "libSceAppDbShellCoreClientForSystemService"}, + {0x800000bf, "libwvoec"}, {0x800000c0, "libSceGLSlimClientVSH"}, {0x800000c1, "libSceGLSlimServerVSH"}, + {0x800000c3, "libSceRnpsBundle"}, }; #endif diff --git a/core/libs/prx/libSceSystemGesture/Export.cpp b/core/libs/prx/libSceSystemGesture/Export.cpp index 9c71e1031b..5e1fb249e6 100644 --- a/core/libs/prx/libSceSystemGesture/Export.cpp +++ b/core/libs/prx/libSceSystemGesture/Export.cpp @@ -2,156 +2,371 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include +#include -// No touch input is emulated: recognizers exist but never report events. -static constexpr int32_t GESTURE_HANDLE = 1; -static constexpr int SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT = static_cast(0x80D10002); -static constexpr int SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE = static_cast(0x80D10003); -static constexpr int SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY = static_cast(0x80D10005); +static constexpr int SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT = static_cast(0x80890001); +static constexpr int SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE = static_cast(0x80890006); +static constexpr int SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY = static_cast(0x80890004); + +namespace { + +constexpr std::uint64_t RECOGNIZER_STATE_MAGIC = 0x41505335'47455354ULL; + +struct RecognizerState { + std::uint64_t magic; + std::int32_t gestureType; + std::uint32_t hasRectangle; + SystemGestureRectangle rectangle; +}; + +static_assert(sizeof(RecognizerState) <= sizeof(SystemGestureTouchRecognizer)); + +void storeRecognizerState(SystemGestureTouchRecognizer* recognizer, const RecognizerState& state) { + std::memcpy(recognizer->reserve, &state, sizeof(state)); +} + +bool loadRecognizerState(const SystemGestureTouchRecognizer* recognizer, RecognizerState& state) { + std::memcpy(&state, recognizer->reserve, sizeof(state)); + return state.magic == RECOGNIZER_STATE_MAGIC; +} + +constexpr std::int32_t TOUCH_STATE_BEGIN = 1; +constexpr std::int32_t TOUCH_STATE_ACTIVE = 2; +constexpr std::int32_t TOUCH_STATE_END = 3; + +struct TrackedTouch { + std::uint16_t primitiveId; + std::int32_t eventState; + bool isUpdated; + bool sampled; + SystemGestureVector2 pressedPosition; + SystemGestureVector2 currentPosition; + SystemGestureVector2 deltaVector; + std::uint64_t pressedTime; + std::uint64_t updatedTime; + std::uint64_t deltaTime; +}; + +std::mutex primitiveTouchMutex; +struct GestureContext { + std::int32_t handle = 0; + std::vector touches; +}; + +std::array gestureContexts; +bool gestureInitialized = false; +std::uint16_t gestureGeneration = 0; + +GestureContext* findGestureContext(std::int32_t gestureHandle) { + const auto found = std::find_if(gestureContexts.begin(), gestureContexts.end(), [gestureHandle](const GestureContext& context) { return context.handle != 0 && context.handle == gestureHandle; }); + return found == gestureContexts.end() ? nullptr : &*found; +} + +struct TouchSample { + std::uint16_t primitiveId; + SystemGestureVector2 position; +}; + +std::vector reportSamples(const PadData& report) { + std::vector samples; + const std::uint8_t count = report.touch_data_touch_num; + if (count > 0) + samples.push_back(TouchSample{report.touch_data_touch0_id, + SystemGestureVector2{static_cast(report.touch_data_touch0_x), static_cast(report.touch_data_touch0_y)}}); + if (count > 1) + samples.push_back(TouchSample{report.touch_data_touch1_id, + SystemGestureVector2{static_cast(report.touch_data_touch1_x), static_cast(report.touch_data_touch1_y)}}); + return samples; +} + +void sampleReport(std::vector& primitiveTouches, const PadData& report) { + std::erase_if(primitiveTouches, [](const TrackedTouch& touch) { return touch.eventState == TOUCH_STATE_END; }); + for (TrackedTouch& touch : primitiveTouches) { + touch.sampled = false; + touch.isUpdated = false; + } + const std::uint64_t time = report.timestamp; + for (const TouchSample& sample : reportSamples(report)) { + const auto tracked = std::find_if(primitiveTouches.begin(), primitiveTouches.end(), + [&](const TrackedTouch& touch) { return touch.primitiveId == sample.primitiveId; }); + if (tracked == primitiveTouches.end()) { + TrackedTouch touch{}; + touch.primitiveId = sample.primitiveId; + touch.eventState = TOUCH_STATE_BEGIN; + touch.isUpdated = true; + touch.sampled = true; + touch.pressedPosition = sample.position; + touch.currentPosition = sample.position; + touch.pressedTime = time; + touch.updatedTime = time; + primitiveTouches.push_back(touch); + continue; + } + const SystemGestureVector2 previous = tracked->currentPosition; + tracked->deltaVector = SystemGestureVector2{sample.position.x - previous.x, sample.position.y - previous.y}; + tracked->currentPosition = sample.position; + tracked->deltaTime = time >= tracked->updatedTime ? time - tracked->updatedTime : 0; + tracked->isUpdated = tracked->eventState != TOUCH_STATE_ACTIVE || tracked->deltaVector.x != 0.0f || tracked->deltaVector.y != 0.0f; + tracked->eventState = TOUCH_STATE_ACTIVE; + tracked->sampled = true; + tracked->updatedTime = time; + } + for (TrackedTouch& touch : primitiveTouches) { + if (touch.sampled) continue; + touch.deltaVector = SystemGestureVector2{0.0f, 0.0f}; + touch.deltaTime = time >= touch.updatedTime ? time - touch.updatedTime : 0; + touch.eventState = TOUCH_STATE_END; + touch.isUpdated = true; + touch.updatedTime = time; + } +} + +SystemGesturePrimitiveTouchEvent buildPrimitiveTouchEvent(const TrackedTouch& touch) { + SystemGesturePrimitiveTouchEvent event{}; + event.event_state = touch.eventState; + event.primitive_id = touch.primitiveId; + event.is_updated = touch.isUpdated ? 1 : 0; + event.pressed_position = touch.pressedPosition; + event.current_position = touch.currentPosition; + event.delta_vector = touch.deltaVector; + event.delta_time = touch.deltaTime; + event.elapsed_time = touch.updatedTime - touch.pressedTime; + return event; +} + +} extern "C" { -int APS5_VABI sceSystemGestureAppendTouchRecognizer(int32_t gesture_handle, SystemGestureTouchRecognizer* recognizer) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureAppendTouchRecognizer(int32_t gestureHandle, SystemGestureTouchRecognizer* recognizer) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; return 0; } -int APS5_VABI sceSystemGestureClose(int32_t gesture_handle) { - return gesture_handle == GESTURE_HANDLE ? 0 : SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureClose(int32_t gestureHandle) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); + auto* context = findGestureContext(gestureHandle); + if (!context) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + context->touches.clear(); + context->handle = 0; + return 0; } -int APS5_VABI sceSystemGestureCreateTouchRecognizer(int32_t gesture_handle, SystemGestureTouchRecognizer* recognizer, int32_t type, const SystemGestureRectangle* rectangle, const void* param) { - (void)type; - (void)rectangle; +int APS5_VABI sceSystemGestureCreateTouchRecognizer(int32_t gestureHandle, SystemGestureTouchRecognizer* recognizer, int32_t type, const SystemGestureRectangle* rectangle, const void* param) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); (void)param; - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + RecognizerState state{}; + state.magic = RECOGNIZER_STATE_MAGIC; + state.gestureType = type; + state.hasRectangle = rectangle ? 1 : 0; + if (rectangle) state.rectangle = *rectangle; + storeRecognizerState(recognizer, state); return 0; } int APS5_VABI sceSystemGestureFinalizePrimitiveTouchRecognizer(void) { + std::lock_guard lock(primitiveTouchMutex); + gestureInitialized = false; return 0; } -int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByIndex(int32_t gesture_handle, uint32_t index, SystemGesturePrimitiveTouchEvent* event) { - (void)index; +int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByIndex(int32_t gestureHandle, uint32_t index, SystemGesturePrimitiveTouchEvent* event) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); + if (!event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (index >= findGestureContext(gestureHandle)->touches.size()) return SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY; + *event = buildPrimitiveTouchEvent(findGestureContext(gestureHandle)->touches[index]); + return 0; +} + +int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(int32_t gestureHandle, uint16_t primitiveId, SystemGesturePrimitiveTouchEvent* event) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); + if (!event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + const auto tracked = std::find_if(findGestureContext(gestureHandle)->touches.begin(), findGestureContext(gestureHandle)->touches.end(), + [&](const TrackedTouch& touch) { return touch.primitiveId == primitiveId; }); + if (tracked == findGestureContext(gestureHandle)->touches.end()) return static_cast(0x80890005); + *event = buildPrimitiveTouchEvent(*tracked); + return 0; +} + +int APS5_VABI sceSystemGestureGetPrimitiveTouchEvents(int32_t gestureHandle, SystemGesturePrimitiveTouchEvent* eventBuffer, uint32_t capacityOfBuffer, uint32_t* numberOfEvent) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); + if (!eventBuffer || !capacityOfBuffer || !numberOfEvent) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + const std::uint32_t reported = static_cast(std::min(findGestureContext(gestureHandle)->touches.size(), capacityOfBuffer)); + for (std::uint32_t i = 0; i < reported; ++i) eventBuffer[i] = buildPrimitiveTouchEvent(findGestureContext(gestureHandle)->touches[i]); + *numberOfEvent = reported; + return 0; +} + +int APS5_VABI sceSystemGestureGetPrimitiveTouchEventsCount(int32_t gestureHandle) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + return static_cast(findGestureContext(gestureHandle)->touches.size()); +} + +int APS5_VABI sceSystemGestureGetTouchEventByEventID(int32_t gestureHandle, const SystemGestureTouchRecognizer* recognizer, uint32_t eventId, SystemGestureTouchEvent* event) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); + (void)eventId; (void)event; - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; return SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY; } -int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(int32_t gesture_handle, uint16_t primitiveId, SystemGesturePrimitiveTouchEvent* event) { - (void)gesture_handle; - (void)primitiveId; - (void)event; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSystemGestureGetPrimitiveTouchEvents(int32_t gesture_handle, SystemGesturePrimitiveTouchEvent* event_buffer, uint32_t capacity_of_buffer, uint32_t* number_of_event) { - (void)event_buffer; - (void)capacity_of_buffer; - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; - if (!number_of_event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; - *number_of_event = 0; - return 0; -} - -int APS5_VABI sceSystemGestureGetPrimitiveTouchEventsCount(int32_t gesture_handle) { - return gesture_handle == GESTURE_HANDLE ? 0 : SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; -} - -int APS5_VABI sceSystemGestureGetTouchEventByEventID(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer, uint32_t eventId, SystemGestureTouchEvent* event) { - (void)gesture_handle; - (void)recognizer; - (void)eventId; - (void)event; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSystemGestureGetTouchEventByIndex(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer, uint32_t index, SystemGestureTouchEvent* event) { - (void)gesture_handle; - (void)recognizer; +int APS5_VABI sceSystemGestureGetTouchEventByIndex(int32_t gestureHandle, const SystemGestureTouchRecognizer* recognizer, uint32_t index, SystemGestureTouchEvent* event) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); (void)index; (void)event; + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; return SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY; } -int APS5_VABI sceSystemGestureGetTouchEvents(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer, SystemGestureTouchEvent* event_buffer, uint32_t capacity_of_buffer, uint32_t* number_of_event) { +int APS5_VABI sceSystemGestureGetTouchEvents(int32_t gestureHandle, const SystemGestureTouchRecognizer* recognizer, SystemGestureTouchEvent* eventBuffer, uint32_t capacityOfBuffer, uint32_t* numberOfEvent) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); (void)recognizer; - (void)event_buffer; - (void)capacity_of_buffer; - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; - if (!number_of_event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; - *number_of_event = 0; + (void)eventBuffer; + (void)capacityOfBuffer; + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!numberOfEvent) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + *numberOfEvent = 0; return 0; } -int APS5_VABI sceSystemGestureGetTouchEventsCount(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer) { +int APS5_VABI sceSystemGestureGetTouchEventsCount(int32_t gestureHandle, const SystemGestureTouchRecognizer* recognizer) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); (void)recognizer; - return gesture_handle == GESTURE_HANDLE ? 0 : SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + return findGestureContext(gestureHandle) != nullptr ? 0 : SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; } -int APS5_VABI sceSystemGestureGetTouchRecognizerInformation(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer, SystemGestureTouchRecognizerInformation* information) { - (void)gesture_handle; - (void)recognizer; - (void)information; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSystemGestureInitializePrimitiveTouchRecognizer(const void* param) { - (void)param; +int APS5_VABI sceSystemGestureGetTouchRecognizerInformation(int32_t gestureHandle, const SystemGestureTouchRecognizer* recognizer, SystemGestureTouchRecognizerInformation* information) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!recognizer || !information) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + RecognizerState state{}; + if (!loadRecognizerState(recognizer, state)) + throw std::runtime_error(std::string(__func__) + ": recognizer was not created by sceSystemGestureCreateTouchRecognizer"); + if (!state.hasRectangle) + throw std::runtime_error(std::string(__func__) + ": recognizer was created without a rectangle"); + information->gesture_type = state.gestureType; + information->rectangle = state.rectangle; + information->updated_time = 0; return 0; } -int32_t APS5_VABI sceSystemGestureOpen(int32_t input_type, const void* param) { - (void)input_type; - (void)param; - return GESTURE_HANDLE; +int APS5_VABI sceSystemGestureInitializePrimitiveTouchRecognizer() { + std::lock_guard lock(primitiveTouchMutex); + gestureInitialized = true; + return 0; } -int APS5_VABI sceSystemGestureRemoveTouchRecognizer(int32_t gesture_handle, SystemGestureTouchRecognizer* recognizer) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int32_t APS5_VABI sceSystemGestureOpen(int32_t inputType, const void* param) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); + if (inputType != 0 || param) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + for (std::size_t index = 0; index < gestureContexts.size(); ++index) { + auto& context = gestureContexts[index]; + if (context.handle != 0) continue; + ++gestureGeneration; + if (gestureGeneration == 0) gestureGeneration = 1; + context.touches.clear(); + context.handle = static_cast(1191182336u + (static_cast(gestureGeneration) << 8) + index); + return context.handle; + } + return static_cast(0x8089000A); +} + +int APS5_VABI sceSystemGestureRemoveTouchRecognizer(int32_t gestureHandle, SystemGestureTouchRecognizer* recognizer) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; return 0; } -int APS5_VABI sceSystemGestureResetPrimitiveTouchRecognizer(int32_t gesture_handle) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureResetPrimitiveTouchRecognizer(int32_t gestureHandle) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + findGestureContext(gestureHandle)->touches.clear(); return 0; } -int APS5_VABI sceSystemGestureResetTouchRecognizer(int32_t gesture_handle, SystemGestureTouchRecognizer* recognizer) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureResetTouchRecognizer(int32_t gestureHandle, SystemGestureTouchRecognizer* recognizer) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; return 0; } -int APS5_VABI sceSystemGestureUpdateAllTouchRecognizer(int32_t gesture_handle) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureUpdateAllTouchRecognizer(int32_t gestureHandle) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; return 0; } -int APS5_VABI sceSystemGestureUpdatePrimitiveTouchRecognizer(int32_t gesture_handle, const void* param) { - (void)param; - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureUpdatePrimitiveTouchRecognizer(int32_t gestureHandle, const SystemGestureTouchPadData* input) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); + if (!input || !input->pad_data_buffer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + auto* context = findGestureContext(gestureHandle); + if (!context) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (input->report_number < 0 || input->report_number > 1) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + if (input->report_number == 1) sampleReport(context->touches, *input->pad_data_buffer); return 0; } -int APS5_VABI sceSystemGestureUpdateTouchRecognizer(int32_t gesture_handle, SystemGestureTouchRecognizer* recognizer) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureUpdateTouchRecognizer(int32_t gestureHandle, SystemGestureTouchRecognizer* recognizer) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; return 0; } -int APS5_VABI sceSystemGestureUpdateTouchRecognizerRectangle(int32_t gesture_handle, SystemGestureTouchRecognizer* recognizer, const SystemGestureRectangle* rectangle) { - if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; +int APS5_VABI sceSystemGestureUpdateTouchRecognizerRectangle(int32_t gestureHandle, SystemGestureTouchRecognizer* recognizer, const SystemGestureRectangle* rectangle) { + std::lock_guard lock(primitiveTouchMutex); + if (!gestureInitialized) return static_cast(0x80890003); + if (!findGestureContext(gestureHandle)) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; if (!rectangle) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + RecognizerState state{}; + if (loadRecognizerState(recognizer, state)) { + state.hasRectangle = 1; + state.rectangle = *rectangle; + storeRecognizerState(recognizer, state); + } return 0; } +int APS5_VABI sceSystemGestureDebugGetVersion() { + return 0x6a00; +} + } diff --git a/core/libs/prx/libSceSystemLogger/Export.cpp b/core/libs/prx/libSceSystemLogger/Export.cpp new file mode 100644 index 0000000000..0b82cdf1e0 --- /dev/null +++ b/core/libs/prx/libSceSystemLogger/Export.cpp @@ -0,0 +1,23 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceSystemLoggerInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemLoggerSetLogData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemLoggerTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSystemLogger2/Export.cpp b/core/libs/prx/libSceSystemLogger2/Export.cpp new file mode 100644 index 0000000000..82a408b187 --- /dev/null +++ b/core/libs/prx/libSceSystemLogger2/Export.cpp @@ -0,0 +1,33 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceSystemLogger2GetTimestamp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemLogger2Initialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemLogger2SetEventAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemLogger2SetEventAsyncEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemLogger2Terminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSystemLogger2Delivery/Export.cpp b/core/libs/prx/libSceSystemLogger2Delivery/Export.cpp new file mode 100644 index 0000000000..a071ef1bed --- /dev/null +++ b/core/libs/prx/libSceSystemLogger2Delivery/Export.cpp @@ -0,0 +1,28 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceSystemLogger2DeliveryGetBatchResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemLogger2DeliveryGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemLogger2DeliverySetBatchDeliveryEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemLogger2DeliveryTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSystemLogger2Game/Export.cpp b/core/libs/prx/libSceSystemLogger2Game/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceSystemLogger2Game/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceSystemLogger2NativeQueueClient/Export.cpp b/core/libs/prx/libSceSystemLogger2NativeQueueClient/Export.cpp new file mode 100644 index 0000000000..1087fa235d --- /dev/null +++ b/core/libs/prx/libSceSystemLogger2NativeQueueClient/Export.cpp @@ -0,0 +1,33 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceSystemLogger2NativeQueueClientCheckAndMaskData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemLogger2NativeQueueClientGetEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemLogger2NativeQueueClientSetDeliveryEnable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemLogger2NativeQueueClientSetDeliveryResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemLogger2NativeQueueClientSetNativeQueueConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 3674b130bc..20a06f0cd9 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -1,5 +1,6 @@ -add_library(libSceSystemService SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(libSceSystemService SHARED EXCLUDE_FROM_ALL Export.cpp PendingExports.cpp LoadExec.cpp) target_include_directories(libSceSystemService PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceSystemService PRIVATE libSceAgcDriver) configure_windows_unwind(libSceSystemService) if(BUILD_TESTING) @@ -14,27 +15,60 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_library(guest_loadexec_host SHARED ${LIBS_INCLUDE_DIR}/tests/GuestLoadExecHost.cpp) + target_include_directories(guest_loadexec_host PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_loadexec_host PRIVATE libSceSystemService libc libkernel) + set_target_properties(guest_loadexec_host PROPERTIES PREFIX "" SUFFIX ".prx" BUILD_RPATH "$ORIGIN") + configure_windows_unwind(guest_loadexec_host) + find_package(Python3 COMPONENTS Interpreter QUIET) + if(Python3_FOUND) + set(loadexecLibraries $ $ $ $) + if(APS5_ENABLE_TRACY) + list(APPEND loadexecLibraries $) + endif() + add_test(NAME loadexec_arguments COMMAND ${Python3_EXECUTABLE} + ${CMAKE_SOURCE_DIR}/core/relinker/relinker/tests/test_loadexec_arguments.py + $ $ + ${loadexecLibraries} ${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}) target_link_libraries(guest_system_exit_tests PRIVATE libSceSystemService libc) 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_immediate_exit_drain COMMAND guest_system_exit_tests --exit-drain-driver) + set_tests_properties(guest_immediate_exit_drain PROPERTIES TIMEOUT 20) add_test(NAME guest_return_from_main COMMAND guest_system_exit_tests --return-from-main) add_test(NAME guest_system_relaunch COMMAND guest_system_exit_tests --relaunch-driver) add_test(NAME guest_nested_exit COMMAND guest_system_exit_tests --nested-exit-driver) - set_tests_properties(guest_system_exit PROPERTIES - PASS_REGULAR_EXPRESSION "Guest shutdown and atexit completed") + 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" OR APPLE) + 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_immediate_exit_drain guest_return_from_main guest_quick_exit guest_system_relaunch guest_nested_exit guest_exit_finalizers_before_shutdown PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_system_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemService.cpp) @@ -55,6 +89,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) @@ -62,10 +105,14 @@ if(BUILD_TESTING) add_test(NAME guest_system_kill_app_parameters COMMAND guest_system_kill_app_tests) add_test(NAME guest_system_kill_app COMMAND guest_system_kill_app_tests --kill) add_test(NAME guest_system_kill_app_status COMMAND guest_system_kill_app_tests --kill) - set_tests_properties(guest_system_kill_app PROPERTIES + add_test(NAME guest_lnc_util_kill_app COMMAND guest_system_kill_app_tests --lnc-kill) + add_test(NAME guest_lnc_util_kill_app_with_reason COMMAND guest_system_kill_app_tests --lnc-kill-reason) + add_test(NAME guest_lnc_util_kill_app_status COMMAND guest_system_kill_app_tests --lnc-kill) + add_test(NAME guest_lnc_util_kill_app_with_reason_status COMMAND guest_system_kill_app_tests --lnc-kill-reason) + set_tests_properties(guest_system_kill_app guest_lnc_util_kill_app guest_lnc_util_kill_app_with_reason PROPERTIES PASS_REGULAR_EXPRESSION "Guest shutdown and atexit completed") if(WIN32) - set_tests_properties(guest_system_kill_app guest_system_kill_app_status guest_system_kill_app_parameters PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_system_kill_app guest_system_kill_app_status guest_system_kill_app_parameters guest_lnc_util_kill_app guest_lnc_util_kill_app_with_reason guest_lnc_util_kill_app_status guest_lnc_util_kill_app_with_reason_status PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 0db2c17be3..3da3b74835 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 @@ -15,28 +16,46 @@ #include #elif defined(__linux__) #include +#elif defined(__APPLE__) +#include +#include #endif #include "prx/libc/include/Shutdown.hpp" #include "SceTypes.hpp" +#include "LoadExec.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libSceSystemService/SystemService.hpp" -#include "prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp" +#include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp" 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"; @@ -148,16 +167,30 @@ void StartHostExecutable(HostLaunch& launch) { CloseHandle(process.hThread); CloseHandle(process.hProcess); } -#elif defined(__linux__) +#elif defined(__linux__) || defined(__APPLE__) struct HostLaunch { std::string executable; std::vector arguments; }; -HostLaunch PrepareLaunch(const char* path, const char* const* arguments) { +std::filesystem::path OwnExecutable() { std::error_code error; +#ifdef __APPLE__ + std::uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + std::vector executable(size); + std::filesystem::path own; + if (_NSGetExecutablePath(executable.data(), &size) == 0) own = std::filesystem::canonical(executable.data(), error); + if (own.empty()) throw std::runtime_error("sceSystemServiceLoadExec: cannot locate the executable"); +#else const auto own = std::filesystem::read_symlink("/proc/self/exe", error); if (error) throw std::runtime_error("sceSystemServiceLoadExec: cannot read /proc/self/exe: " + error.message()); +#endif + return own; +} + +HostLaunch PrepareLaunch(const char* path, const char* const* arguments) { + const auto own = OwnExecutable(); HostLaunch launch; if (std::strcmp(path, MainExecutable) == 0) { const char* main = std::getenv(MainExecutableVariable); @@ -182,6 +215,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))); } @@ -201,8 +237,12 @@ extern "C" { int APS5_VABI sceSystemServiceLoadExec(const char* path, const char* const* arguments) { if (!path || !*path) return SYSTEM_SERVICE_ERROR_PARAMETER; - if (std::strcmp(path, "exit") == 0) LibcExit_nid_no_patch(0); + if (std::strcmp(path, "exit") == 0) LibcExitWithoutHandlers_nid_no_patch(0); auto launch = PrepareLaunch(path, arguments); +#if defined(_WIN32) || defined(__linux__) || defined(__APPLE__) + LoadConvertedExecutable_nid_no_patch(std::filesystem::path(launch.executable), arguments); +#endif + LibcStartExit_nid_postfix(); LibcRunShutdown_nid_postfix(); std::fflush(nullptr); StartHostExecutable(launch); @@ -222,13 +262,19 @@ int APS5_VABI sceSystemServiceGetDisplaySafeAreaInfo(SystemServiceDisplaySafeAre return SYSTEM_SERVICE_OK; } +int APS5_VABI sceSystemServiceShowDisplaySafeAreaSettings(void) { + return SYSTEM_SERVICE_OK; +} + int APS5_VABI sceSystemServiceGetHdrToneMapLuminance(SystemServiceHdrToneMapLuminance* luminance) { if (luminance == nullptr) { return SYSTEM_SERVICE_ERROR_PARAMETER; } - luminance->max_full_frame_tone_map_luminance = AgcDriver::SdrToneMapLuminanceNits; - luminance->max_tone_map_luminance = AgcDriver::SdrToneMapLuminanceNits; - luminance->min_tone_map_luminance = 0.0f; + const auto hdr = AgcDriver::CurrentHostHdr(); + luminance->max_tone_map_luminance = AgcDriver::ToneMapPeakNits(hdr); + luminance->max_full_frame_tone_map_luminance = hdr.available ? hdr.display.maxFullFrameLuminance : AgcDriver::SdrToneMapLuminanceNits; + luminance->min_tone_map_luminance = hdr.available ? hdr.display.minLuminance : 0.0f; + AgcDriverNoteReportedToneMapPeak_nid_postfix(luminance->max_tone_map_luminance); return SYSTEM_SERVICE_OK; } @@ -258,30 +304,29 @@ 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_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; + 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_100: *value = 0; break; + default: + if ((paramId >= 101 && paramId <= 111) || (paramId >= 200 && paramId <= 213) || paramId == 300) { + NotImplemented_nid_no_patch("sceSystemServiceParamGetInt: registry-backed parameter"); + } + return SYSTEM_SERVICE_ERROR_PARAMETER; } return SYSTEM_SERVICE_OK; } int APS5_VABI sceSystemServiceParamGetString(int paramId, char* buf, size_t bufSize) { - if (buf == nullptr || bufSize == 0) { + if (paramId != SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME || buf == nullptr || bufSize < SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH) { return SYSTEM_SERVICE_ERROR_PARAMETER; } - if (paramId != SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME) { - NotImplemented_nid_no_patch("sceSystemServiceParamGetString: parameter other than the system name"); - } - if (bufSize < SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH) { - NotImplemented_nid_no_patch("sceSystemServiceParamGetString: buffer shorter than 65 bytes"); - } constexpr char SystemName[] = "PS5"; + std::memset(buf, 0, bufSize); std::memcpy(buf, SystemName, sizeof(SystemName)); + buf[SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH - 1] = '\0'; return SYSTEM_SERVICE_OK; } @@ -305,9 +350,11 @@ int APS5_VABI sceSystemServiceSetNoticeScreenSkipFlag(void) { return SYSTEM_SERVICE_OK; } -int APS5_VABI sceSystemServiceInitializePlayerDialogParam(void* param) { - if (param == nullptr) return SYSTEM_SERVICE_ERROR_PARAMETER; - return SYSTEM_SERVICE_OK; +void APS5_VABI sceSystemServiceInitializePlayerDialogParam(SystemServicePlayerDialogParam* param) { + if (param == nullptr) APS5_INVALID_ARG_EX; + *param = SystemServicePlayerDialogParam{}; + param->size = sizeof(SystemServicePlayerDialogParam); + param->user_id = -1; } int APS5_VABI sceSystemServiceDisableMediaPlay() { @@ -323,10 +370,13 @@ int APS5_VABI sceSystemServiceLaunchPlayerDialog(const void* param) { return SYSTEM_SERVICE_OK; } -int APS5_VABI sceSystemServiceLaunchWebBrowser(const char* uri, void* param) { - (void)uri; - (void)param; - return SCE_KERNEL_ERROR_EOPNOTSUPP; +int APS5_VABI sceSystemServiceLaunchWebBrowser(const char* uri) { + if (!uri) return static_cast(0x80a10003); + std::size_t length = 0; + while (length < 6016 && uri[length] != 0) ++length; + if (length > 6015) return static_cast(0x80a10007); + if (std::strncmp(uri, "http:", 5) != 0 && std::strncmp(uri, "https:", 6) != 0 && std::strncmp(uri, "psno:", 5) != 0) return static_cast(0x80a10008); + return SCE_KERNEL_ERROR_EOPNOTSUPP; } int APS5_VABI sceSystemServiceLaunchApp(const char* titleId, const char* const* arguments, const void* param) { @@ -345,7 +395,29 @@ int APS5_VABI sceSystemServiceOpenChallengeActivity(void) { return SYSTEM_SERVICE_OK; } -int APS5_VABI sceSystemServiceOpenTournamentOccurrence(void) { +struct SystemServiceTournamentOccurrenceParam { + std::uint64_t size; + const char* occurrenceId; + std::int32_t userId; + std::uint32_t padding; + const char* context; +}; + +static_assert(sizeof(SystemServiceTournamentOccurrenceParam) == 32); +static_assert(offsetof(SystemServiceTournamentOccurrenceParam, occurrenceId) == 8); +static_assert(offsetof(SystemServiceTournamentOccurrenceParam, userId) == 16); +static_assert(offsetof(SystemServiceTournamentOccurrenceParam, context) == 24); + +int APS5_VABI sceSystemServiceOpenTournamentOccurrence(const SystemServiceTournamentOccurrenceParam* param) { + return SYSTEM_SERVICE_OK; +} + +int APS5_VABI sceSystemServiceAddLocalProcessForPsmKit(const char*, const char* const*, const void*) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceKillLocalProcessForPsmKit(int) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -372,4 +444,419 @@ int APS5_VABI sceSystemServiceKillApp(int appId, int how, int reason, int coreDu LibcExit_nid_no_patch(0); } +int APS5_VABI sceLncUtilKillApp(uint32_t appId) { + if (appId != static_cast(SYSTEM_SERVICE_RUNNING_APP_ID)) { + NotImplemented_nid_no_patch("sceLncUtilKillApp: application other than the running title"); + } + LibcExit_nid_no_patch(0); +} + +int APS5_VABI sceLncUtilKillAppWithReason(uint32_t appId, int reason) { + if (appId != static_cast(SYSTEM_SERVICE_RUNNING_APP_ID)) { + NotImplemented_nid_no_patch("sceLncUtilKillAppWithReason: application other than the running title"); + } + if (reason != 0) { + NotImplemented_nid_no_patch("sceLncUtilKillAppWithReason: a reason other than 0"); + } + LibcExit_nid_no_patch(0); +} + +int APS5_VABI sceSystemServiceLaunchSystemDeeplink(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilLaunchByUriParamInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrTickVideoPlayback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceTelemetrySetData2WithUserId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilStartUmmDictation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetSpotifyTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppStatusListContainsDaemon(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSwitchToVRApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSwitchToNonVRApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetSpmemAllocatorConfigValue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrCreateLocalSandbox(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrDestroyLocalSandbox(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrSwitchLocalSandbox(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrGetCurrentLocalSandboxId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrGetLocalSandboxIdList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrGetLocalSandboxInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrSetLocalSandboxInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrIssueStreamingSandboxInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceReportSequenceHandlerRequestReport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceReportSequenceHandlerCancelReport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceReportSequenceHandlerExistsEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceReportSequenceHandlerUpdateEventCache(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceReportSequenceHandlerGetEventCache(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRegisterCallEntries(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetSpmemConfigJson(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilExportSpmemConfigJson(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilImportSpmemConfigJson(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilExitAppWithValue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNpDebugSpecialNotification(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsDddTemperatureDanger(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsM2StorageTemperatureDanger(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAvailableSizeM2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyConnectAccountStatusChanged(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsInternalCronosUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilChangePbtcMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTelemetryCacheData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMicMuteKeyPressed(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSwitchMicMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyStartEventToUniversalMicMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsLotusMicActivated(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilResetAllNoticeScreenSkipFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilResetNoticeScreenSkipFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetNoticeScreenSkipFlagByAppId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPasscodeInputType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPasscodeDisplayInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifySystemResumeTelemetryInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilUpdateTelemetryToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetVRBaseMatRectangles(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetVRBaseMatRectangles(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsLaunchBlockDialog(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetConsoleToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetDeviceIdByDeviceUniqueId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetDeviceUniqueIdByDeviceId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetStreamingClientInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestCffUpdate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCffConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRemoveCffConfigFile(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDebugGetNumberOfCffItems(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDebugGetCffInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDebugSetCffStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPeripheralConnectionSessionId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCdlgEnabledFreezeRendering(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetNpTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsCronosInGameCommerceEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilLemSettingGet(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilLemSettingSet(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilLemSettingGetList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilLemSettingReset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsEChassisVariant(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrGetRebootCause(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpSequenceHandlerUpdateEventCache(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpSequenceHandlerGetEventCache(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpSequenceHandlerGetWorkingEventSummary(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpSequenceHandlerGetCoredumpStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpSequenceHandlerGetCoredumpStatusByAppId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpSequenceHandlerCancelCoredump(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsBgmPlaying(void) { + return 0; +} + +int APS5_VABI sceShellCoreUtilIsBgmPlaying(void) { + return 0; +} + } diff --git a/core/libs/prx/libSceSystemService/LoadExec.cpp b/core/libs/prx/libSceSystemService/LoadExec.cpp new file mode 100644 index 0000000000..82e0c2b3c4 --- /dev/null +++ b/core/libs/prx/libSceSystemService/LoadExec.cpp @@ -0,0 +1,219 @@ +#include "LoadExec.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#elif defined(__linux__) +#include +#include +extern char** environ; +#elif defined(__APPLE__) +#include +#include +#include +#include +#include +#endif + +namespace { + +#ifdef _WIN32 +using Handle = std::unique_ptr; + +std::wstring Wide(const std::string& text) { + if (text.size() > static_cast(std::numeric_limits::max())) + throw std::runtime_error("LoadExec: argument is too long"); + if (text.empty()) return {}; + const int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), nullptr, 0); + if (!size) throw std::runtime_error("LoadExec: argument is not valid UTF-8"); + std::wstring wide(size, L'\0'); + if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), wide.data(), size) != size) + throw std::runtime_error("LoadExec: cannot convert argument to UTF-16"); + return wide; +} + +std::wstring Quote(const std::wstring& text) { + std::wstring result = L"\""; + std::size_t slashes = 0; + for (const wchar_t character : text) { + if (character == L'\\') { ++slashes; continue; } + result.append(character == L'"' ? slashes * 2 + 1 : slashes, L'\\'); + result += character; + slashes = 0; + } + result.append(slashes * 2, L'\\'); + return result + L'"'; +} + +Handle Duplicate(HANDLE source, DWORD access) { + HANDLE handle = nullptr; + if (!DuplicateHandle(GetCurrentProcess(), source, GetCurrentProcess(), &handle, access, TRUE, + access ? 0 : DUPLICATE_SAME_ACCESS)) + throw std::system_error(GetLastError(), std::system_category(), "LoadExec: duplicating a process handle"); + return Handle(handle, CloseHandle); +} + +[[noreturn]] void Replace(const std::filesystem::path& target, const std::vector& arguments) { + std::wstring command = Quote(target.wstring()); + for (const auto& argument : arguments) { + if (!command.empty()) command += L' '; + command += Quote(Wide(argument)); + } + if (command.size() >= 32767) throw std::runtime_error("LoadExec: command line is too long"); + auto parent = Duplicate(GetCurrentProcess(), SYNCHRONIZE); + std::vector standard; + std::vector handles{parent.get()}; + STARTUPINFOEXW startup{}; + startup.StartupInfo.cb = sizeof(startup); + startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + HANDLE* outputs[] = {&startup.StartupInfo.hStdInput, &startup.StartupInfo.hStdOutput, &startup.StartupInfo.hStdError}; + const DWORD identifiers[] = {STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE}; + for (std::size_t index = 0; index < 3; ++index) { + const HANDLE source = GetStdHandle(identifiers[index]); + if (source && source != INVALID_HANDLE_VALUE) { + standard.push_back(Duplicate(source, 0)); + *outputs[index] = standard.back().get(); + handles.push_back(*outputs[index]); + } + } + SIZE_T bytes = 0; + InitializeProcThreadAttributeList(nullptr, 1, 0, &bytes); + std::vector storage((bytes + sizeof(std::max_align_t) - 1) / sizeof(std::max_align_t)); + auto* attributes = reinterpret_cast(storage.data()); + if (!InitializeProcThreadAttributeList(attributes, 1, 0, &bytes)) + throw std::system_error(GetLastError(), std::system_category(), "LoadExec: creating process attributes"); + const std::unique_ptr<_PROC_THREAD_ATTRIBUTE_LIST, decltype(&DeleteProcThreadAttributeList)> cleanup(attributes, DeleteProcThreadAttributeList); + if (!UpdateProcThreadAttribute(attributes, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handles.data(), handles.size() * sizeof(HANDLE), nullptr, nullptr)) + throw std::system_error(GetLastError(), std::system_category(), "LoadExec: selecting inherited handles"); + startup.lpAttributeList = attributes; + const std::unique_ptr inherited(GetEnvironmentStringsW(), FreeEnvironmentStringsW); + if (!inherited) throw std::runtime_error("LoadExec: cannot read process environment"); + constexpr wchar_t Variable[] = L"ANYPS5_LOADEXEC_PARENT="; + constexpr wchar_t Skip[] = L"ANYPS5_LOADEXEC_ARGV_SKIP="; + std::vector entries; + for (const wchar_t* at = inherited.get(); *at;) { + const std::wstring entry(at); + if (_wcsnicmp(entry.c_str(), Variable, std::size(Variable) - 1) != 0 && + _wcsnicmp(entry.c_str(), Skip, std::size(Skip) - 1) != 0) { + entries.push_back(entry); + } + at += entry.size() + 1; + } + std::wostringstream value; + value << Variable << std::hex << reinterpret_cast(parent.get()); + entries.push_back(value.str()); + entries.push_back(std::wstring(Skip) + L"1"); + std::sort(entries.begin(), entries.end(), [](const auto& left, const auto& right) { return _wcsicmp(left.c_str(), right.c_str()) < 0; }); + std::vector environment; + for (const auto& entry : entries) { + environment.insert(environment.end(), entry.begin(), entry.end()); + environment.push_back(0); + } + environment.push_back(0); + PROCESS_INFORMATION process{}; + if (!CreateProcessW(target.c_str(), command.data(), nullptr, nullptr, TRUE, + CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT, + environment.data(), target.parent_path().c_str(), &startup.StartupInfo, &process)) + 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)) + throw std::system_error(GetLastError(), std::system_category(), "LoadExec: starting the converted executable"); + } catch (...) { + TerminateProcess(child.get(), 1); + throw; + } + thread.reset(); + child.reset(); + parent.reset(); + LibcTerminate_nid_no_patch(0); +} +#elif defined(__linux__) +[[noreturn]] void Replace(const std::filesystem::path& target, const std::vector& arguments) { + const int descriptor = open(target.c_str(), O_RDONLY | O_CLOEXEC); + if (descriptor < 0) throw std::system_error(errno, std::generic_category(), "LoadExec: opening the converted executable"); + const auto close = [](int* fd) { ::close(*fd); delete fd; }; + const std::unique_ptr file(new int(descriptor), close); + std::array header{}; + if (pread(descriptor, header.data(), header.size(), 0) != static_cast(header.size()) || + header[0] != 0x7f || header[1] != 'E' || header[2] != 'L' || header[3] != 'F' || + header[4] != 2 || header[5] != 1 || (header[7] != 0 && header[7] != 3) || header[17] != 0 || (header[16] != 2 && header[16] != 3) || header[18] != 62 || header[19] != 0 || access(target.c_str(), X_OK) != 0) + throw std::runtime_error("LoadExec: target is not an executable x86-64 Linux ELF"); + 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) + throw std::system_error(errno, std::generic_category(), "LoadExec: closing the old executable's descriptors"); + fexecve(descriptor, pointers.data(), environ); + throw std::system_error(errno, std::generic_category(), "LoadExec: replacing the executable"); +} +#elif defined(__APPLE__) +[[noreturn]] void Replace(const std::filesystem::path& target, const std::vector& arguments) { + const int descriptor = open(target.c_str(), O_RDONLY | O_CLOEXEC); + if (descriptor < 0) throw std::system_error(errno, std::generic_category(), "LoadExec: opening the converted executable"); + mach_header_64 header{}; + const auto read = pread(descriptor, &header, sizeof(header), 0); + ::close(descriptor); + if (read != static_cast(sizeof(header)) || header.magic != MH_MAGIC_64 || header.cputype != CPU_TYPE_X86_64 || header.filetype != MH_EXECUTE || access(target.c_str(), X_OK) != 0) + throw std::runtime_error("LoadExec: target is not an executable x86-64 Mach-O"); + std::vector pointers; + for (const auto& argument : arguments) pointers.push_back(const_cast(argument.c_str())); + pointers.push_back(nullptr); + posix_spawnattr_t attributes; + posix_spawn_file_actions_t actions; + if (posix_spawnattr_init(&attributes) != 0 || posix_spawn_file_actions_init(&actions) != 0 || + posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETEXEC | POSIX_SPAWN_CLOEXEC_DEFAULT) != 0) + throw std::runtime_error("LoadExec: preparing the replacement"); + 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"); + for (const int standard : {STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO}) { + if (fcntl(standard, F_GETFD) != -1 && posix_spawn_file_actions_addinherit_np(&actions, standard) != 0) + throw std::runtime_error("LoadExec: keeping the standard streams"); + } + std::filesystem::current_path(target.parent_path()); + const int error = posix_spawn(nullptr, target.c_str(), &actions, &attributes, pointers.data(), *_NSGetEnviron()); + throw std::system_error(error, std::generic_category(), "LoadExec: replacing the executable"); +} +#endif + +} + +extern "C" bool LoadConvertedExecutable_nid_no_patch(const std::filesystem::path& target, const char* const* arguments) { +#if defined(_WIN32) || defined(__linux__) || defined(__APPLE__) + if (!arguments || !arguments[0]) return false; + std::vector copied; + for (std::size_t index = 0; arguments[index]; ++index) copied.emplace_back(arguments[index]); + Replace(target, copied); +#else + (void)target; + (void)arguments; + return false; +#endif +} diff --git a/core/libs/prx/libSceSystemService/LoadExec.hpp b/core/libs/prx/libSceSystemService/LoadExec.hpp new file mode 100644 index 0000000000..7b689c7745 --- /dev/null +++ b/core/libs/prx/libSceSystemService/LoadExec.hpp @@ -0,0 +1,8 @@ +#ifndef CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_LOADEXEC_HPP +#define CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_LOADEXEC_HPP + +#include + +extern "C" bool LoadConvertedExecutable_nid_no_patch(const std::filesystem::path& target, const char* const* arguments); + +#endif diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp new file mode 100644 index 0000000000..9fbb45de38 --- /dev/null +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -0,0 +1,2558 @@ +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int SystemServiceUnavailable = static_cast(0x80A10002); +constexpr int SystemServiceRejected = static_cast(0x80A10005); + +} + +struct ShellCoreErrorDialogParam { + const char* message; + const char* const* formatArgs; + std::uint8_t flag; +}; + +static_assert(sizeof(ShellCoreErrorDialogParam) == 24); +static_assert(offsetof(ShellCoreErrorDialogParam, message) == 0); +static_assert(offsetof(ShellCoreErrorDialogParam, formatArgs) == 8); +static_assert(offsetof(ShellCoreErrorDialogParam, flag) == 16); + +struct SystemServiceDaemonEvent; +struct SystemServiceLocalProcessStatus; +struct SystemServiceEvent; +struct SystemServicePSButtonEvent; +struct SystemServicePrivacyDefinitionData; +struct SystemServicePrivacyDefinitionVersion; +struct SystemServiceTitleWorkaroundInfo; +struct SystemServiceStoreParam { + std::uint32_t size; +}; + +static_assert(sizeof(SystemServiceStoreParam) == 4); + +struct SystemServiceCpuPstateParam; +struct SystemServiceLocalProcessParam; +struct SystemServiceJvmProcessParam; +struct SystemServicePs2ProcessParam; +struct SystemServiceAppInstallStatus; +struct SystemServiceAppStatus; +struct ShellCoreLaunchByUriParam { + std::uint32_t option; + std::int32_t userId; +}; + +static_assert(sizeof(ShellCoreLaunchByUriParam) == 8); +static_assert(offsetof(ShellCoreLaunchByUriParam, option) == 0); +static_assert(offsetof(ShellCoreLaunchByUriParam, userId) == 4); +struct ShellCoreTelemetryToken; +struct ShellCoreUmmStatus { + std::int32_t state; + std::int32_t inDeviceMuteState; +}; + +static_assert(sizeof(ShellCoreUmmStatus) == 8); +static_assert(offsetof(ShellCoreUmmStatus, state) == 0); +static_assert(offsetof(ShellCoreUmmStatus, inDeviceMuteState) == 4); +struct ShellCorePbtcUserInfoList; +struct ShellCorePrivacyEventData; +struct ShellCorePsnAccountInfoFirst; +struct ShellCorePsnAccountInfoSecond; +struct ShellCorePsnAccountInfoThird; +struct ShellCorePsnAccountInfoFourth; +struct ShellCorePsnAccountInfoFifth; +struct ShellCoreAppLaunchTypeInfo; +struct LncGpuCrashAppStatus { + char titleId[16]; + std::int32_t appId; + std::int32_t appLocalPid; + char path[1024]; + std::int32_t appType; + std::int32_t coredumpReason; + char corefileName[256]; + std::uint32_t exceptionCode; + std::uint32_t appCategoryType; +}; + +static_assert(sizeof(LncGpuCrashAppStatus) == 1320); +static_assert(offsetof(LncGpuCrashAppStatus, titleId) == 0); +static_assert(offsetof(LncGpuCrashAppStatus, appId) == 16); +static_assert(offsetof(LncGpuCrashAppStatus, appLocalPid) == 20); +static_assert(offsetof(LncGpuCrashAppStatus, path) == 24); +static_assert(offsetof(LncGpuCrashAppStatus, appType) == 1048); +static_assert(offsetof(LncGpuCrashAppStatus, coredumpReason) == 1052); +static_assert(offsetof(LncGpuCrashAppStatus, corefileName) == 1056); +static_assert(offsetof(LncGpuCrashAppStatus, exceptionCode) == 1312); +static_assert(offsetof(LncGpuCrashAppStatus, appCategoryType) == 1316); +struct LncLaunchParam { + std::uint32_t size; + std::int32_t userId; + std::int32_t appAttr; + std::int32_t enableCrashReport; + std::uint64_t checkFlag; + std::uint64_t contextId; + bool isSpeculativeLaunch; +}; + +static_assert(sizeof(LncLaunchParam) == 40); +static_assert(offsetof(LncLaunchParam, size) == 0); +static_assert(offsetof(LncLaunchParam, userId) == 4); +static_assert(offsetof(LncLaunchParam, appAttr) == 8); +static_assert(offsetof(LncLaunchParam, enableCrashReport) == 12); +static_assert(offsetof(LncLaunchParam, checkFlag) == 16); +static_assert(offsetof(LncLaunchParam, contextId) == 24); +static_assert(offsetof(LncLaunchParam, isSpeculativeLaunch) == 32); +struct LncDaemonEvent; +struct LncShellUiEvent; +struct LncLocalProcessStatus { + std::int32_t appLocalPid; + std::uint8_t reserved[32]; +}; + +static_assert(sizeof(LncLocalProcessStatus) == 36); +static_assert(offsetof(LncLocalProcessStatus, appLocalPid) == 0); +static_assert(offsetof(LncLocalProcessStatus, reserved) == 4); +struct LncCoredumpState { + std::uint32_t progress; + std::uint32_t isFinished; + std::int32_t result; +}; + +static_assert(sizeof(LncCoredumpState) == 12); +static_assert(offsetof(LncCoredumpState, progress) == 0); +static_assert(offsetof(LncCoredumpState, isFinished) == 4); +static_assert(offsetof(LncCoredumpState, result) == 8); +struct LncVideoFinalizeState { + std::uint32_t progress; + std::int32_t result; +}; + +static_assert(sizeof(LncVideoFinalizeState) == 8); +static_assert(offsetof(LncVideoFinalizeState, progress) == 0); +static_assert(offsetof(LncVideoFinalizeState, result) == 4); +struct LncLocalProcessParam; +struct LncAppStatus { + std::int32_t appId; + std::int32_t launchRequestAppId; + std::int32_t appType; + std::int32_t appAttr; + char titleId[16]; + std::uint32_t appCategoryType; + std::int32_t appBootMode; + std::int32_t appBootSubMode; + char appVer[6]; + char contentVersion[11]; + bool isSystemExApp; + bool isCdlgInitialized; +}; + +static_assert(sizeof(LncAppStatus) == 64); +static_assert(offsetof(LncAppStatus, appId) == 0); +static_assert(offsetof(LncAppStatus, launchRequestAppId) == 4); +static_assert(offsetof(LncAppStatus, appType) == 8); +static_assert(offsetof(LncAppStatus, appAttr) == 12); +static_assert(offsetof(LncAppStatus, titleId) == 16); +static_assert(offsetof(LncAppStatus, appCategoryType) == 32); +static_assert(offsetof(LncAppStatus, appBootMode) == 36); +static_assert(offsetof(LncAppStatus, appBootSubMode) == 40); +static_assert(offsetof(LncAppStatus, appVer) == 44); +static_assert(offsetof(LncAppStatus, contentVersion) == 50); +static_assert(offsetof(LncAppStatus, isSystemExApp) == 61); +static_assert(offsetof(LncAppStatus, isCdlgInitialized) == 62); +struct LncShellUiAppStatus { + std::int32_t appId; + std::int32_t appType; + std::int32_t appAttr; + std::int32_t launchRequestAppId; + std::int32_t userId; + std::int32_t isActiveCdlg; + char path[1024]; + char sceSysDir[256]; + std::int32_t isCoredumped; + std::int32_t isPrxModuleLoadFailed; + std::int32_t appLocalPid; + std::int32_t crashReportMode; + char category[4]; + std::uint32_t appCategoryType; + std::int32_t appBootMode; + std::int32_t appBootSubMode; +}; + +static_assert(sizeof(LncShellUiAppStatus) == 1336); +static_assert(offsetof(LncShellUiAppStatus, appId) == 0); +static_assert(offsetof(LncShellUiAppStatus, appType) == 4); +static_assert(offsetof(LncShellUiAppStatus, appAttr) == 8); +static_assert(offsetof(LncShellUiAppStatus, launchRequestAppId) == 12); +static_assert(offsetof(LncShellUiAppStatus, userId) == 16); +static_assert(offsetof(LncShellUiAppStatus, isActiveCdlg) == 20); +static_assert(offsetof(LncShellUiAppStatus, path) == 24); +static_assert(offsetof(LncShellUiAppStatus, sceSysDir) == 1048); +static_assert(offsetof(LncShellUiAppStatus, isCoredumped) == 1304); +static_assert(offsetof(LncShellUiAppStatus, isPrxModuleLoadFailed) == 1308); +static_assert(offsetof(LncShellUiAppStatus, appLocalPid) == 1312); +static_assert(offsetof(LncShellUiAppStatus, crashReportMode) == 1316); +static_assert(offsetof(LncShellUiAppStatus, category) == 1320); +static_assert(offsetof(LncShellUiAppStatus, appCategoryType) == 1324); +static_assert(offsetof(LncShellUiAppStatus, appBootMode) == 1328); +static_assert(offsetof(LncShellUiAppStatus, appBootSubMode) == 1332); +struct AppMessagingMessage { + std::int32_t sender; + std::uint32_t messageType; + std::uint8_t payload[8192]; + std::uint32_t payloadSize; + std::uint64_t timestamp; +}; + +static_assert(sizeof(AppMessagingMessage) == 8216); +static_assert(offsetof(AppMessagingMessage, sender) == 0); +static_assert(offsetof(AppMessagingMessage, messageType) == 4); +static_assert(offsetof(AppMessagingMessage, payload) == 8); +static_assert(offsetof(AppMessagingMessage, payloadSize) == 8200); +static_assert(offsetof(AppMessagingMessage, timestamp) == 8208); + +struct UsbStorageEvent; +struct UsbStorageDeviceInfo; +using UsbStorageCallback = void (APS5_VABI *)(const UsbStorageEvent* event, void* userData); + +extern "C" { + +int APS5_VABI sceAppMessagingClearEventFlag(std::int32_t appId, std::uint64_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingReceiveMsg(AppMessagingMessage* message) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingSendMsg(std::uint32_t appId, std::uint32_t messageId, const void* data, std::size_t size, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingSendMsgToShellCore(std::uint32_t messageId, const void* data, std::size_t size, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingSendMsgToShellUI(std::uint32_t messageId, const void* data, std::size_t size, std::uint32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingSetEventFlag(std::int32_t appId, std::uint64_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingTryGetEventFlag(std::uint64_t mask, std::uint64_t* flags, std::uint8_t clear) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppMessagingTryReceiveMsg(AppMessagingMessage* message) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilAcquireCpuBudgetOfExtraAudioDevices() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilAcquireCpuBudgetOfImeForBigApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilAcquireCpuBudgetOfInGameStore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilActivateCdlg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilAddLocalProcess(std::int32_t appId, const char* path, const char* const* argv, const LncLocalProcessParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilBlockAppSuspend(const char* reason) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilBlockingGetEventForDaemon(LncDaemonEvent* event, std::uint64_t receiveArgument) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilContinueApp(std::int32_t appId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilCrashSyscore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilDeactivateCdlg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilDeclareReadyForSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilDisableSuspendNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilEnableSuspendNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilFinishSpecialResume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilForceKillApp(std::int32_t appId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilForceKillLocalProcess(std::int32_t appId, std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppCategory(std::int32_t appId, char* category) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppCategoryType(std::int32_t appId, std::uint32_t* categoryType) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppFocusedAppStatus(LncAppStatus* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppId(const char* titleId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppIdListOfBigApp(std::int32_t* appIds, std::uint32_t capacity, std::uint32_t* count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppIdOfRunningBigApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppLaunchedUser(std::int32_t appId, std::int32_t* userId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppStatus(LncAppStatus* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppStatusList(LncAppStatus* statuses, std::uint32_t capacity, std::uint32_t* count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppStatusListForShellUIReboot(LncShellUiAppStatus* statuses, std::uint32_t capacity, std::uint32_t* count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppTitleId(std::int32_t appId, char* titleId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetAppType(std::int32_t appId, std::uint32_t* type) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetCdlgExec(std::int32_t appId, bool* executing) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetCdlgType(std::int32_t appId, std::uint32_t* type) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetCoredumpState(std::int32_t appId, std::int32_t processId, LncCoredumpState* state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetDbgExecutablePath(char* path, std::uint64_t selector) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetEventForDaemon(LncDaemonEvent* event) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetEventForShellUI(LncShellUiEvent* event) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetGpuCrashFullDumpAppStatus(LncGpuCrashAppStatus* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetLocalProcessStatusList(LncLocalProcessStatus* statuses, std::uint32_t capacity, std::uint32_t* count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetParentSocket() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetResultKillApp(std::uint32_t requestId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetResultLaunchAppByTitleId(std::uint32_t requestId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetVideoFinalizeState(std::int32_t appId, std::int32_t processId, LncVideoFinalizeState* state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsActiveCdlg(bool* active) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsAppLaunched(const char* titleId, bool* launched) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsAppSuspended(std::int32_t appId, bool* suspended) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsCdlgActivated(std::uint8_t* activated) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceLncUtilIsCpuBudgetOfExtraAudioDevicesAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsHostBoot(std::int32_t appId, bool* hostBoot) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsPs2Emu(bool* emulated) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceLncUtilIsShellUiFgAndGameBgCpuMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilIsSpeculativeLaunch(std::int32_t appId, bool* speculative) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilKickCoredumpOnlyProcMem(std::int32_t appId, std::int32_t processId, const char* first, const char* second) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilKillLocalProcess(std::int32_t appId, std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilLaunchApp(const char* titleId, const char* const* argv, const LncLaunchParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilLoadExec(const char* path, const char* const* argv) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilNotifyCoredumpRequestEnd(std::int32_t appId, std::int32_t processId, std::int32_t result) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilNotifyCoredumpRequestProgress(std::int32_t appId, std::int32_t processId, std::int32_t progress) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilNotifyVshReady(std::uint32_t errorCode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRaiseException(std::int32_t appId, std::uint64_t exception) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRaiseExceptionLocalProcess(std::int32_t appId, std::int32_t processId, std::uint64_t exception) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRegisterCdlgSharedMemoryName(const char* name) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRegisterDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRegisterShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilReleaseCpuBudgetOfExtraAudioDevices() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilReleaseCpuBudgetOfImeForBigApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilReleaseCpuBudgetOfInGameStore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilResumeApp(std::int32_t appId, std::uint64_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilResumeLocalProcess(std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSetAppFocus(std::int32_t appId, std::uint64_t flags, std::uint64_t contextId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSetCdlgExec(std::int32_t appId, std::uint8_t executing) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSetControllerFocus(std::int32_t appId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSetControllerFocusPermission(std::uint8_t permission) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilStartKillApp(std::int32_t appId, std::uint32_t* requestId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilStartLaunchAppByTitleId(const char* titleId, const char* const* argv, const LncLaunchParam* param, std::uint32_t* requestId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSuspendApp(std::int32_t appId, std::uint64_t flags, std::int32_t reason) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSuspendBackgroundApp(std::int32_t first, std::int32_t second) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilSuspendLocalProcess(std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilTryBlockAppSuspend(const char* reason) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilUnblockAppSuspend(const char* reason) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilUnregisterCdlgSharedMemoryName(const char* name) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilUnregisterDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilUnregisterShellUI() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilAccessibilityZoomLock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilAccessibilityZoomUnlock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilAcquireBgmCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilAcquireRemotePlayCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilAcquireSharePlayCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateAbort(std::intptr_t handle) { + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateGetStatus(std::intptr_t handle, std::int32_t* status, std::int32_t* result) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateInit(std::int32_t type, std::intptr_t* handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateIsActivated(std::intptr_t handle, bool* activated) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateRecordActivation() { + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateStart(std::intptr_t handle) { + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateStartAsync(std::intptr_t handle) { + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateTerm(std::intptr_t handle) { + return 0; +} + +int APS5_VABI sceShellCoreUtilChangeRunLevel(std::int32_t runLevel) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilChangeToStaffModeForIDU() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilCheckerAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilCleanupCrashReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilClearAppData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilClearPs4AppCategoryDefaultsListUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilClearPsnAccountInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilCrashReportRequestCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeclareBeginOfExternalStorageAppMove() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeclareEndOfExternalStorageAppMove() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeleteDiscInstalledTitleWorkaroundFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeleteDownloadedHidConfigFile(std::int32_t configIndex) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeleteDownloadedNetEvConfigFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDeleteDownloadedTitleWorkaroundFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDoFsck(const char* path) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDownloadHidConfigFileFromServer(std::int32_t configIndex) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDownloadNetEvConfigFileFromServer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDownloadTitleWorkaroundFileFromServer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilEndUmmDictation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilEnterPowerLockSection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilExecuteCrashReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilExfatFormatExternalHdd(const char* path) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilExitApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilExitMiniApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilExitMiniAppWithValue(const char* value) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilFormatExternalHdd(const char* path) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilFormatHddForRestore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppData(const char* key, void* data, std::int32_t capacity, std::int32_t* size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppEnableTTS(std::int32_t appId, bool* enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppEnterButtonAssign(std::int32_t appId, std::int32_t* assignment) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchTypeInfo(std::int32_t appId, ShellCoreAppLaunchTypeInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamInt(std::int32_t appId, const char* key, std::int32_t* value) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamIntByBudgetType(std::int32_t budgetType, const char* key, std::int32_t* value) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamString(std::int32_t appId, const char* key, char* value, std::int32_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAppLaunchedParamStringByBudgetType(std::int32_t budgetType, const char* key, char* value, std::int32_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAutoPowerDownRemainingSeconds() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetBasicProductShape() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCheckerString(const char* key, char* value, std::size_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCheckerStringEx(const char* firstKey, const char* secondKey, char* value, std::size_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCloudClientStatus(std::uint32_t* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportCoreFileSetSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportFilterInfoStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportInfoForBoot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportInfoForBootStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportInfoStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCrashReportUploadStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetDeviceIndexBehavior(std::uint32_t* behavior, std::int32_t* userId, std::uint32_t* scene) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetDeviceIndexBehaviorWithTimeout(std::uint32_t* behavior, std::int32_t* userId, std::uint32_t* scene, std::uint8_t timeout) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetEffectiveTotalSizeOfUserPartition(std::uint64_t* size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::uint64_t APS5_VABI sceShellCoreUtilGetFreeSizeOfAvContentsTmp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetFreeSizeOfUserPartition(std::uint64_t* freeSize, std::uint64_t* totalSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetFsckProgress(const char* titleId, std::uint32_t* progress) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetGameLiveStreamingStatus(bool* active) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceShellCoreUtilGetGnmCompositorOnScreenProfilerFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetGpuLoadEmulationMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetGpuLoadEmulationModeByAppId(std::int32_t appId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetHidConfigFileInfoString(char* value, std::size_t capacity, std::int32_t configIndex) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetHidConfigFileString(char* value, std::size_t capacity, std::int32_t configIndex) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetHidConfigName(char* value, std::size_t capacity, std::int32_t configIndex) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetHidConfigNum(std::int32_t* configNum) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetIDUMode(std::uint8_t* mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceShellCoreUtilGetImposeMenuFlagForPs2Emu(std::int32_t appId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetManifestFileStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetNeedSizeOfAppContent(std::uint64_t* size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetNetEvConfigFileInfoString(char* value, std::size_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetOutOfVrPlayZoneWarning() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPapcGamePcl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPbtcUserInfoList(ShellCorePbtcUserInfoList* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPlatformPrivacyDefinitionEventData(const char* firstKey, const char* secondKey, ShellCorePrivacyEventData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +float APS5_VABI sceShellCoreUtilGetProgressOfFormatExternalHdd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +float APS5_VABI sceShellCoreUtilGetProgressOfFsck() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPs4AppCategoryDefaultsListId(void* data, std::size_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPs4AppCategoryForTitleId(const char* titleId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPsStoreIconLayout(std::int32_t appId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPsStoreIconState(std::int32_t appId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPsnAccountInfo(ShellCorePsnAccountInfoFirst* first, ShellCorePsnAccountInfoSecond* second, ShellCorePsnAccountInfoThird* third, ShellCorePsnAccountInfoFourth* fourth, ShellCorePsnAccountInfoFifth* fifth) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetRemotePlayStatus(bool* active) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetRunLevel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSharePlayStatus(bool* active) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSocialScreenStatus(bool* active) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSplashScreenState(std::int32_t appId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSupportSiteURL(std::uint32_t first, std::uint32_t second, char* value, std::size_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceShellCoreUtilGetSuspendConfirmationDialogFlag(std::int32_t appId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSystemBGState(std::int32_t appId, std::uint32_t* state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSystemBGWaveColor(std::int32_t appId, std::uint32_t* color) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetSystemBGWaveState(std::int32_t appId, std::uint32_t* state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetTelemetryToken(ShellCoreTelemetryToken* token) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetTitleWorkaroundFileInfoString(char* value, std::size_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetTitleWorkaroundFileString(char* value, std::size_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetUIStatus(std::uint64_t* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetUmmStatus(std::int32_t userId, ShellCoreUmmStatus* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetUserFocus(std::int32_t appId, std::int32_t* userId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetUserIdOfMorpheusUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetVMStatsForTelemetry(char* value, std::size_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetVersionNumberOfCameraCalibrationData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGoBackToKratosCurrentSessionGame() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilHideBlocksFromUser(std::int64_t bytes, std::int32_t flags) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIncrementVersionNumberOfCameraCalibrationData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceShellCoreUtilIsAccessibilityZoomLocked(std::int32_t appId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceShellCoreUtilIsBgmCpuBudgetAcquired() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceShellCoreUtilIsBgmCpuBudgetAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceShellCoreUtilIsExternalStorageAppMoveInProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsEyeToEyeDistanceAdjusted(std::uint32_t userId, std::uint8_t* adjusted) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceShellCoreUtilIsGameLiveStreamingOnAir() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsIDUState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceShellCoreUtilIsImposeScreenOverlaid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceShellCoreUtilIsInSystemSuspendBlackList(std::uint32_t appId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsInternalKratosUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsKilledOrSuspendedByLogout(std::int32_t first, std::int32_t second, std::uint8_t* result) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsNeededCrashReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceShellCoreUtilIsPowerSaveAlertRequested() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceShellCoreUtilIsRemotePlayCpuBudgetAcquired() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceShellCoreUtilIsScreenSaverOn() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceShellCoreUtilIsSharePlayCpuBudgetAcquired() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsShowCrashReport() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsTemperatureDanger() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsTitleWorkaroundEnabled(std::int32_t selector, const char* key) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceShellCoreUtilIsUsbMassStorageMounted(std::uint32_t deviceIndex) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilLaunchByUri(const char* uri, const ShellCoreLaunchByUriParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilLeavePowerLockSection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMakeManifestFile() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMountAppRight(const char* key) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMountDownloadDataForShellUI(const char* key) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMountHddForBackup() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMountHddForRestore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNavigateToAnotherApp(std::uint32_t target) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNavigateToGoHome(std::uint32_t target) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNavigateToLaunchedApp(std::uint32_t target) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotificationCancelForIDU(std::int64_t notificationId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotificationRequestedForIDU(std::int64_t notificationId, const char* text, std::uint32_t type) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyBgmCoreTermination() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyFsReadError(std::int32_t first, std::int32_t second, std::int32_t third, const char* detail) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyImeStatusChanged(std::int32_t imeStatus, std::uint8_t isCalledFromGame) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyPsnAccountInfoReceived() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifySystemSuspendTelemetryInfo(std::uint32_t info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPfAuthClientConsoleTokenClearCache() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPostActivityForPsNow(std::int32_t type, const char* activity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPostErrorLog(const char* titleId, std::int32_t internalErrorCode, const char* shortError, const char* messageBuffer, std::int32_t suggestionType, const char* logReport) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPostLaunchConfirmResult(std::uint32_t first, std::uint32_t second) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPostPsmEventToShellUI(std::int32_t eventId, const char* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPreNotifyOfGameLiveStreaming() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPreNotifyOfRemotePlay() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilPreNotifyOfSharePlay() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilReleaseBgmCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilReleaseRemotePlayCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilReleaseSharePlayCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilReportSessionErrorToGaikaiController(std::int32_t error, const char* detail) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilReportUnexpectedFatalErrorToSystemTelemetry(std::int32_t error, const char* detail) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestCameraCalibration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestEjectDevice(const char* path) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestRebootApp(std::int32_t appId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestShutdown(std::int32_t reason) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilResetAutoPowerDownTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilResetBgdcConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetAppData(const char* key, const char* value) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetBgmProhibition(std::uint32_t value) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetDeviceIndexBehavior(std::uint32_t behavior, std::int32_t userId, std::uint8_t scene) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGameLiveStreamingOnAirFlag(std::int32_t enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGameLiveStreamingStatus(std::int32_t active) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGnmCompositorOnScreenProfilerFlag(std::int32_t enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGpuLoadEmulationMode(std::uint32_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetGpuLoadEmulationModeByAppId(std::int32_t appId, std::uint32_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetIDUMode(std::uint8_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetImposeStatusFlag(std::int32_t enabled) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetPsStoreIconLayout(std::uint32_t layout) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetPsStoreIconState(std::uint32_t state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetRemotePlayStatus(std::int32_t active) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSharePlayStatus(std::int32_t active) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSkipUpdateCheck(std::uint8_t skip) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSocialScreenStatus(std::int32_t active) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSplashScreenState(std::uint32_t state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSystemBGState(std::uint32_t state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSystemBGWaveColor(std::uint32_t color) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetSystemBGWaveState(std::uint32_t state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetUIStatus(std::uint64_t status, std::uint64_t mask) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetUserFocus(std::int32_t userId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowCriticalErrorDialog(std::uint32_t error, const char* message) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowErrorDialog(std::uint32_t error, const char* message) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowErrorDialogWithFormatArgs(std::uint32_t error, const char* message, const char* const* formatArgs) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowErrorDialogWithParam(std::uint32_t error, const ShellCoreErrorDialogParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilShowPsUnderLockIndicator() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSignalUserInput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilStartPsNowGame(const char* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilStopPsNowGame(const char* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTalkKeyEventReceived(std::int64_t deviceId, std::int32_t userId, std::int32_t keyEventType) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTestBusTransferSpeed(const char* path, std::uint8_t* result) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTickHeartBeat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTriggerPapcUpdate(std::uint32_t value) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTurnOffScreenSaver() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilUnmountAppRight(const char* key) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilUnmountDownloadDataForShellUI(const char* key) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilUnmountHddForBackup() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilUnmountHddForRestore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreutilGetCrashReportProcessInformation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemModalDialogClose(std::uint32_t dialogId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemModalDialogOpen(const char* message) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAcquireBgmCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAcquireFb0() { + return SystemServiceUnavailable; +} + +int APS5_VABI sceSystemServiceActivateHevc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcAbort(std::uintptr_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcGetStatus(std::uintptr_t handle, std::uint32_t* status, std::int32_t* result) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcInit(std::uintptr_t* handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcIsActivated(std::uint8_t* activated) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoft() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftAbort(std::uintptr_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftGetStatus(std::uintptr_t handle, std::uint32_t* status, std::int32_t* result) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftInit(std::uintptr_t* handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftIsActivated(std::uint8_t* activated) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftStart(std::uintptr_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcSoftTerm(std::uintptr_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcStart(std::uintptr_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateHevcTerm(std::uintptr_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2Abort(std::uintptr_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2GetStatus(std::uintptr_t handle, std::uint32_t* status, std::int32_t* result) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2Init(std::uintptr_t* handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2IsActivated(std::uint8_t* activated) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2Start(std::uintptr_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceActivateMpeg2Term(std::uintptr_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAddLocalProcess(std::int32_t appId, const char* path, const char* const* argv, const SystemServiceLocalProcessParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAddLocalProcessForJvm(const char* path, const char* const* argv, const SystemServiceJvmProcessParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAddLocalProcessForPs2Emu(const char* path, const char* const* argv, const SystemServicePs2ProcessParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceAppGetAppInstallStatus(const char* titleId, SystemServiceAppInstallStatus* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeAcpClock() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeClock(const char* clock, std::uint32_t value) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeCpuClock() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeCpuPstate(const SystemServiceCpuPstateParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeGpuClock() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemPstate(std::uint32_t state) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemoryClock() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemoryClockToBaseMode() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemoryClockToDefault() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemoryClockToMultiMediaMode() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeNumberOfGpuCu() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeSamuClock() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeUvdClock() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeVceClock() { + return 0; +} + +int APS5_VABI sceSystemServiceDeclareReadyForSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceDisablePartyVoice() { + return SystemServiceUnavailable; +} + +int APS5_VABI sceSystemServiceDisablePersonalEyeToEyeDistanceSetting() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceDisableSuspendConfirmationDialog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceDisableSuspendNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceDisableVoiceRecognition() { + return SystemServiceUnavailable; +} + +int APS5_VABI sceSystemServiceEnablePersonalEyeToEyeDistanceSetting() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceEnableSuspendConfirmationDialog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceEnableSuspendNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppCategoryType(std::int32_t appId, std::uint32_t* categoryType) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppFocusedAppStatus(SystemServiceAppStatus* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppId(const char* titleId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppIdListOfBigApp(std::int32_t* appIds, std::uint32_t capacity, std::uint32_t* count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppLaunchedUser(std::int32_t appId, std::int32_t* userId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppStatus(SystemServiceAppStatus* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppTitleId(std::int32_t appId, char* titleId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppType(std::int32_t appId, std::int32_t* type) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetDbgExecutablePath(char* path, std::uint64_t selector) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetEventForDaemon(SystemServiceDaemonEvent* event) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetGpuLoadEmulationMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetLocalProcessStatusList(SystemServiceLocalProcessStatus* statuses, std::uint32_t capacity, std::uint32_t* count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetMainAppTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetPSButtonEvent(const SystemServiceEvent* event, SystemServicePSButtonEvent* buttonEvent) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetParentSocket() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetParentSocketForJvm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetParentSocketForPs2Emu() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetParentSocketForPsmKit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetPlatformPrivacyDefinitionData(const char* key, SystemServicePrivacyDefinitionData* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetPlatformPrivacyDefinitionVersion(const char* key, SystemServicePrivacyDefinitionVersion* version) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetPlatformPrivacySetting(std::uint32_t* setting) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetRecentLotusPushedButton(std::uint32_t timeout, std::int32_t* deviceId, std::uint32_t* buttons) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetRemoteplayStatus(bool* active) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetRenderingMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetSubAppTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetSubAppTitleIdForSP() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetTitleWorkaroundInfo(SystemServiceTitleWorkaroundInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetUIStatus(std::uint64_t* status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetVersionNumberOfCameraCalibrationData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceInitializeForShellCore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceInvokeAppLaunchLink(const char* uri) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsAppSuspended(std::int32_t appId, bool* suspended) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceSystemServiceIsBgmCpuBudgetAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceIsEyeToEyeDistanceAdjusted(std::uint32_t userId, std::uint8_t* adjusted) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceSystemServiceIsGameLiveStreamingOnAir() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceSystemServiceIsScreenSaverOn() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceSystemServiceIsShellUiFgAndGameBgCpuMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceKillLocalProcess(std::int32_t appId, std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceKillLocalProcessForJvm(std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceKillLocalProcessForPs2Emu(std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchEventDetails(std::uint64_t unusedSlot, const char* eventId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchStore(const char* uri, const SystemServiceStoreParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchTournamentList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchTournamentsTeamProfile(std::uint64_t unusedSlot, const char* profile, const char* tournament, const char* context) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchUdsApp() { + return SystemServiceRejected; +} + +int APS5_VABI sceSystemServiceLaunchWebApp(const char* argument) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLoadExecVideoServiceWebApp() { + return SystemServiceUnavailable; +} + +int APS5_VABI sceSystemServiceNavigateToAnotherApp(std::uint32_t target) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNavigateToGoBack() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNavigateToGoBackWithValue(const char* value) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNavigateToGoHome(std::uint32_t target) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceNotifyBgmCoreTermination() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceRaiseExceptionLocalProcess(std::int32_t appId, std::int32_t processId, std::uint64_t exception) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceReenablePartyVoice() { + return SystemServiceUnavailable; +} + +int APS5_VABI sceSystemServiceReenableVoiceRecognition() { + return SystemServiceUnavailable; +} + +int APS5_VABI sceSystemServiceRegisterDaemon() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceReleaseBgmCpuBudget() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceReleaseFb0() { + return SystemServiceUnavailable; +} + +int APS5_VABI sceSystemServiceRequestCameraCalibration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceRequestPowerOff() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceRequestReboot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceRequestToChangeRenderingMode() { + return SystemServiceRejected; +} + +int APS5_VABI sceSystemServiceResumeLocalProcess(std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSaveVideoToken(std::int32_t userId, const char* token) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetControllerFocusPermission(std::uint8_t permission) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetGpuLoadEmulationMode(std::uint32_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetOutOfVrPlayAreaFlag(std::uint8_t flag) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSetOutOfVrPlayZoneWarning() { + return 0; +} + +int APS5_VABI sceSystemServiceSetPowerSaveLevel(std::uint32_t level) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowClosedCaptionAdvancedSettings() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowClosedCaptionSettings() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowErrorDialog(std::uint32_t error, const char* message) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowEyeToEyeDistanceSetting() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowImposeMenuForPs2Emu(std::uint8_t enabled, std::uint64_t reserved) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSuspendBackgroundApp(std::int32_t first, std::int32_t second) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSuspendLocalProcess(std::int32_t processId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceTelemetrySetData(const char* key, const void* data, std::size_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceTelemetrySetData2(const char* key, const char* data) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceTickVideoPlayback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceTurnOffScreenSaver() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetDeviceInfo(std::uint32_t device, UsbStorageDeviceInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetDeviceList(std::uint32_t* devices, std::int32_t* count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetdentsClose(std::int32_t fd) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetdentsOpen(std::uint32_t device, std::int32_t* fd, std::uint64_t* bufferSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageGetdentsRead(std::int32_t fd, void* buffer, std::uint64_t bufferSize, std::uint64_t* bytesRead) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageInit(void* attr) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceSystemServiceUsbStorageIsExist(std::uint32_t device, const char* directory, std::uint8_t* exists) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRegisterCallback(std::int32_t eventType, UsbStorageCallback callback, void* arg) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRegisterCallbackForMapAvailable(std::uint32_t device, const char* directory, UsbStorageCallback callback, void* arg) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRegisterCallbackForMapUnavailable(std::uint32_t device, const char* directory, UsbStorageCallback callback, void* arg) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRequestMap(std::uint32_t device, const char* directory, std::int32_t flags, std::uint64_t volumeSize, char* mountPoint, std::uint64_t* mountId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRequestMapWSB(std::uint32_t device, const char* directory, std::int32_t flags, std::uint64_t volumeSize, char* mountPoint, std::uint64_t* mountId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageRequestUnmap(std::uint32_t device, const char* directory) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageUnregisterCallback(std::int32_t eventType, UsbStorageCallback callback) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageUnregisterCallbackForMapAvailable(std::uint32_t device, const char* directory, UsbStorageCallback callback) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceUsbStorageUnregisterCallbackForMapUnavailable(std::uint32_t device, const char* directory, UsbStorageCallback callback) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrCancelShutdownTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrEnterMediaPlaybackMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrEnterStandby() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrExtendShutdownTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrExtendShutdownTimerForPostAutoUpdateProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrGetCurrentState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrGetTriggerCode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceSystemStateMgrIsBdDriveReady() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrIsGpuPerformanceNormal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceSystemStateMgrIsShellUIShutdownInProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI sceSystemStateMgrIsStandbyModeEnabled() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrLeaveMediaPlaybackMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrNotifySystemSuspendResumeProgress(const char* progress) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrReboot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrRequestToKeepMainOnStandby(const char* reason) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrSendCecOneTouchPlayCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrStartRebootTimer(std::int32_t mode) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrStartShutdownTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrStartStadbyTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrStartVshAutoUpdateTimer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrTickMusicPlayback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrTickPartyChat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrTurnOff() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrVshAutoUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrWaitVshAutoUpdateVerifyDone() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrWakeUp(std::uint16_t trigger) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSystemService/SystemService.hpp b/core/libs/prx/libSceSystemService/SystemService.hpp index 99b73cc0b5..22f85c1123 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; @@ -17,6 +23,7 @@ constexpr int SYSTEM_SERVICE_PARAM_ID_TIME_ZONE = 4; constexpr int SYSTEM_SERVICE_PARAM_ID_SUMMERTIME = 5; constexpr int SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME = 6; constexpr int SYSTEM_SERVICE_PARAM_ID_GAME_PARENTAL_LEVEL = 7; +constexpr int SYSTEM_SERVICE_PARAM_ID_100 = 100; constexpr int SYSTEM_SERVICE_PARAM_ID_400 = 400; constexpr int SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN = 1000; @@ -28,7 +35,19 @@ constexpr int SYSTEM_SERVICE_PARAM_LANG_ARABIC = 21; constexpr int SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN = 30; constexpr int SYSTEM_SERVICE_PARAM_DATE_FORMAT_DDMMYYYY = 1; 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/libSceSystemTts/Export.cpp b/core/libs/prx/libSceSystemTts/Export.cpp new file mode 100644 index 0000000000..d895b270e8 --- /dev/null +++ b/core/libs/prx/libSceSystemTts/Export.cpp @@ -0,0 +1,33 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceSystemTtsCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemTtsIsAccessibilityAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemTtsRegisterCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemTtsSpeakRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemTtsUnregisterCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceTEEClient/Export.cpp b/core/libs/prx/libSceTEEClient/Export.cpp new file mode 100644 index 0000000000..58c703695b --- /dev/null +++ b/core/libs/prx/libSceTEEClient/Export.cpp @@ -0,0 +1,59 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN +int APS5_VABI AMDTEE_DLM_FetchDebugStrings_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AMDTEE_DLM_GetDebugToken_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AMDTEE_DLM_StopTADebug_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI TEECI_InitASD_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI TEECI_SetTimeout_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI TEEC_CloseSession_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI TEEC_InvokeCommand_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI TEEC_OpenSession_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI TEEC_ReleaseSharedMemory_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI TEEC_RequestCancellation_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceTextToSpeech/Export.cpp b/core/libs/prx/libSceTextToSpeech/Export.cpp new file mode 100644 index 0000000000..05c84e9df9 --- /dev/null +++ b/core/libs/prx/libSceTextToSpeech/Export.cpp @@ -0,0 +1,58 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceTextToSpeechCancelImpl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTextToSpeechCloseImpl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTextToSpeechGetStatusImpl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTextToSpeechInitImpl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTextToSpeechOpenImpl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTextToSpeechPauseImpl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTextToSpeechResumeImpl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTextToSpeechSpeakContinuousImpl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTextToSpeechSpeakImpl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTextToSpeechTermImpl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceTextToSpeech2/Export.cpp b/core/libs/prx/libSceTextToSpeech2/Export.cpp index 09950022bc..375bf561f4 100644 --- a/core/libs/prx/libSceTextToSpeech2/Export.cpp +++ b/core/libs/prx/libSceTextToSpeech2/Export.cpp @@ -9,13 +9,40 @@ constexpr std::int32_t SCE_KERNEL_ERROR_EOPNOTSUPP = static_cast(0 } +struct TextToSpeechInitParameter { + std::uint32_t version; + std::int32_t threadPriority; + std::uint64_t cpuAffinity; + std::uint8_t reserved[48]; +}; + +struct TextToSpeechOpenParameter { + std::uint32_t language; + std::uint32_t voiceType; + std::uint8_t reserved[56]; +}; + +struct TextToSpeechSpeakParameter { + const char16_t* text; + std::uint32_t length; + std::uint32_t mode; + std::uint32_t volume; + std::int32_t speed; + std::uint8_t reserved[40]; +}; + +static_assert(sizeof(TextToSpeechInitParameter) == 64); +static_assert(sizeof(TextToSpeechOpenParameter) == 64); +static_assert(sizeof(TextToSpeechSpeakParameter) == 64); +static_assert(offsetof(TextToSpeechSpeakParameter, reserved) == 24); + extern "C" { -int APS5_VABI sceTextToSpeech2GetSystemStatus() { +int APS5_VABI sceTextToSpeech2GetSystemStatus(std::int32_t* status) { return SCE_KERNEL_ERROR_EOPNOTSUPP; } -std::int32_t APS5_VABI sceTextToSpeech2Initialize(const void* param) { +std::int32_t APS5_VABI sceTextToSpeech2Initialize(const TextToSpeechInitParameter* param) { (void)param; return SCE_KERNEL_ERROR_EOPNOTSUPP; } @@ -28,15 +55,15 @@ int APS5_VABI sceTextToSpeech2Close() { return SCE_KERNEL_ERROR_EOPNOTSUPP; } -int APS5_VABI sceTextToSpeech2GetSpeechStatus() { +int APS5_VABI sceTextToSpeech2GetSpeechStatus(std::int32_t* status) { return SCE_KERNEL_ERROR_EOPNOTSUPP; } -int APS5_VABI sceTextToSpeech2Open() { +int APS5_VABI sceTextToSpeech2Open(const TextToSpeechOpenParameter* parameter) { return SCE_KERNEL_ERROR_EOPNOTSUPP; } -int APS5_VABI sceTextToSpeech2Speak() { +int APS5_VABI sceTextToSpeech2Speak(const TextToSpeechSpeakParameter* parameter) { return SCE_KERNEL_ERROR_EOPNOTSUPP; } diff --git a/core/libs/prx/libSceTtsCoreG3Bc/Export.cpp b/core/libs/prx/libSceTtsCoreG3Bc/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceTtsCoreG3Bc/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceUlt/CMakeLists.txt b/core/libs/prx/libSceUlt/CMakeLists.txt index e8cb567f4d..ea7e2f4a31 100644 --- a/core/libs/prx/libSceUlt/CMakeLists.txt +++ b/core/libs/prx/libSceUlt/CMakeLists.txt @@ -1,8 +1,21 @@ -add_library(libSceUlt SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(libSceUlt SHARED EXCLUDE_FROM_ALL Export.cpp src/ReaderWriterLock.cpp) target_include_directories(libSceUlt PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceUlt) if(BUILD_TESTING) + add_test_executable(guest_ult_condition_tests ${LIBS_INCLUDE_DIR}/tests/GuestUltCondition.cpp) + target_include_directories(guest_ult_condition_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ult_condition_tests PRIVATE libSceUlt libc) + configure_windows_unwind(guest_ult_condition_tests) + foreach(testCase IN ITEMS validation handoff signal-one stress finalize-queued finalize-reacquire) + add_test(NAME guest_ult_condition_${testCase} COMMAND guest_ult_condition_tests ${testCase}) + set_tests_properties(guest_ult_condition_${testCase} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_ult_condition_${testCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test_executable(guest_ult_queue_tests ${LIBS_INCLUDE_DIR}/tests/GuestUltQueue.cpp) target_include_directories(guest_ult_queue_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ult_queue_tests PRIVATE libSceUlt libc) @@ -28,4 +41,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_ult_sync_tests ${LIBS_INCLUDE_DIR}/tests/GuestUltSync.cpp) + target_include_directories(guest_ult_sync_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ult_sync_tests PRIVATE libSceUlt libc) + configure_windows_unwind(guest_ult_sync_tests) + foreach(testCase IN ITEMS mutex-trylock opt-params rwlock-validation rwlock-readers rwlock-writer rwlock-pool rwlock-finalize ulthreads) + add_test(NAME guest_ult_sync_${testCase} COMMAND guest_ult_sync_tests ${testCase}) + set_tests_properties(guest_ult_sync_${testCase} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_ult_sync_${testCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() endif() diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index a9fc644954..f876cfba21 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -1,8 +1,11 @@ +#include #include +#include #include #include #include #include +#include #include #include #include @@ -13,15 +16,19 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "SceUltTypes.hpp" +#include "prx/libSceUlt/src/ReaderWriterLock.hpp" extern "C" int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); extern "C" int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +extern "C" void APS5_VABI scePthreadExit(void* retval); +extern "C" void APS5_VABI scePthreadYield(); namespace { std::mutex gMutex; std::unordered_map> gMutexes; +std::unordered_map> gConditions; std::unordered_map> gSemaphores; std::unordered_map gResourcePools; std::unordered_map> gQueueDataPools; @@ -29,8 +36,15 @@ std::unordered_map> gQueues; std::unordered_map gRuntimes; std::unordered_map> gUlthreads; -std::uint64_t alignUp(std::uint64_t value, std::uint64_t alignment) { - return (value + alignment - 1u) & ~(alignment - 1u); + +int validateCommonOptParam(const void* optParam) { + if (optParam == nullptr) return ULT_OK; + if ((reinterpret_cast(optParam) & 7u) != 0) return ULT_ERROR_ALIGNMENT; + UltCommonOptParam value; + std::memcpy(&value, optParam, sizeof(value)); + if (value.header[0] != 0x6C696275 || value.header[1] != 0x6C746D61 || value.header[2] != 0x67696300) return ULT_ERROR_INVALID; + if (std::any_of(std::begin(value.reserved), std::end(value.reserved), [](std::uint8_t byte) { return byte != 0; })) return ULT_ERROR_INVALID; + return ULT_OK; } int semaphoreGetState(void* semaphore, std::shared_ptr* out) { @@ -106,8 +120,11 @@ void ReleaseQueue(UltQueueState& state) { --pool._queues; } +thread_local UltUlthreadState* gCurrentUlthread = nullptr; + void* APS5_VABI ulthreadRunner(void* arg) { auto* state = static_cast(arg); + gCurrentUlthread = state; const auto result = state->_entry(state->_arg); state->_exited.store(true, std::memory_order_release); return reinterpret_cast(static_cast(result)); @@ -115,6 +132,11 @@ void* APS5_VABI ulthreadRunner(void* arg) { } +bool UltWaitingPoolExists(void* pool) { + std::lock_guard lock(gMutex); + return gResourcePools.contains(pool); +} + extern "C" { int APS5_VABI sceUltInitialize() { @@ -123,6 +145,7 @@ int APS5_VABI sceUltInitialize() { int APS5_VABI sceUltFinalize() { std::vector> semaphores; + std::vector> mutexes; { std::lock_guard lock(gMutex); if (!gUlthreads.empty()) { @@ -136,7 +159,22 @@ int APS5_VABI sceUltFinalize() { semaphores.push_back(state); } gSemaphores.clear(); + mutexes.reserve(gMutexes.size()); + for (auto& entry : gMutexes) { + auto& state = entry.second; + std::lock_guard stateLock(state->_mutex); + state->_alive = false; + mutexes.push_back(state); + } + for (const auto& [object, condition] : gConditions) { + std::lock_guard stateLock(condition->_mutex->_mutex); + condition->_alive = false; + --condition->_mutex->_conditions; + for (auto* waiter : condition->_queue) waiter->_available.notify_one(); + } + gConditions.clear(); gMutexes.clear(); + UltFinalizeReaderWriterLocks(); gResourcePools.clear(); for (auto& entry : gQueues) { auto& state = *entry.second; @@ -152,18 +190,61 @@ int APS5_VABI sceUltFinalize() { for (const auto& state : semaphores) { state->_available.notify_all(); } + for (const auto& state : mutexes) { + state->_available.notify_all(); + } return ULT_OK; } int APS5_VABI sceUltMutexOptParamInitialize(UltMutexOptParam* optParam, std::uint32_t buildVersion) { - (void)buildVersion; if (optParam == nullptr) { return ULT_ERROR_NULL; } + if ((reinterpret_cast(optParam) & 7u) != 0) { + return ULT_ERROR_ALIGNMENT; + } std::memset(optParam, 0, sizeof(*optParam)); + optParam->header[0] = 0x6C696275; + optParam->header[1] = 0x6C746D61; + optParam->header[2] = 0x67696300; + optParam->buildVersion = buildVersion; return ULT_OK; } +int APS5_VABI sceUltSemaphoreOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltQueueOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltQueueDataResourcePoolOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltWaitingQueueResourcePoolOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltUlthreadOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltConditionVariableOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI _sceUltConditionVariableOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + return sceUltConditionVariableOptParamInitialize(optParam, buildVersion); +} + int APS5_VABI sceUltMutexCreate(void* mutex, const char* name, void* waitingQueueResourcePool, const UltMutexOptParam* optParam, std::uint32_t buildVersion) { (void)name; (void)buildVersion; @@ -172,15 +253,38 @@ int APS5_VABI sceUltMutexCreate(void* mutex, const char* name, void* waitingQueu } auto state = std::make_shared(); state->_attribute = optParam != nullptr ? optParam->attribute : 0; + state->_waitingPool = waitingQueueResourcePool; std::lock_guard lock(gMutex); if (waitingQueueResourcePool != nullptr && gResourcePools.find(waitingQueueResourcePool) == gResourcePools.end()) { return ULT_ERROR_INVALID; } + if (gMutexes.find(mutex) != gMutexes.end()) { + return ULT_ERROR_STATE; + } std::memset(mutex, 0, 256); gMutexes[mutex] = std::move(state); return ULT_OK; } +int APS5_VABI sceUltMutexDestroy(void* mutex) { + if (mutex == nullptr) { + return ULT_ERROR_NULL; + } + std::lock_guard lock(gMutex); + auto it = gMutexes.find(mutex); + if (it == gMutexes.end()) { + return ULT_ERROR_STATE; + } + auto state = it->second; + std::lock_guard stateLock(state->_mutex); + if (state->_lockCount != 0 || state->_waiters != 0 || state->_conditions != 0 || state->_conditionWaiters != 0) { + return ULT_ERROR_STATE; + } + state->_alive = false; + gMutexes.erase(it); + return ULT_OK; +} + int APS5_VABI sceUltMutexLock(void* mutex) { std::shared_ptr state; { @@ -191,7 +295,60 @@ int APS5_VABI sceUltMutexLock(void* mutex) { } state = it->second; } - state->_mutex.lock(); + const auto self = std::this_thread::get_id(); + std::unique_lock lock(state->_mutex); + if (!state->_alive) { + return ULT_ERROR_STATE; + } + if (state->_owner == self) { + if (state->_attribute == 0) { + return ULT_ERROR_STATE; + } + ++state->_lockCount; + return ULT_OK; + } + ++state->_waiters; + try { + state->_available.wait(lock, [&] { + return !state->_alive || state->_lockCount == 0; + }); + } catch (...) { + --state->_waiters; + throw; + } + --state->_waiters; + if (!state->_alive) { + return ULT_ERROR_STATE; + } + state->_owner = self; + state->_lockCount = 1; + return ULT_OK; +} + +int APS5_VABI sceUltMutexTryLock(void* mutex) { + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + auto it = gMutexes.find(mutex); + if (it == gMutexes.end()) { + return mutex == nullptr ? ULT_ERROR_NULL : ULT_ERROR_STATE; + } + state = it->second; + } + const auto self = std::this_thread::get_id(); + std::lock_guard lock(state->_mutex); + if (!state->_alive) { + return ULT_ERROR_STATE; + } + if (state->_owner == self) { + ++state->_lockCount; + return ULT_OK; + } + if (state->_lockCount != 0) { + return ULT_ERROR_BUSY; + } + state->_owner = self; + state->_lockCount = 1; return ULT_OK; } @@ -205,12 +362,125 @@ int APS5_VABI sceUltMutexUnlock(void* mutex) { } state = it->second; } - state->_mutex.unlock(); + const auto self = std::this_thread::get_id(); + std::lock_guard lock(state->_mutex); + if (!state->_alive) { + return ULT_ERROR_STATE; + } + if (state->_owner != self) { + return ULT_ERROR_PERMISSION; + } + --state->_lockCount; + if (state->_lockCount == 0) { + state->_owner = {}; + state->_available.notify_one(); + } return ULT_OK; } -int APS5_VABI sceUltWaitingQueueResourcePoolGetWorkAreaSize(std::uint32_t numThreads, std::uint32_t numSyncObjects) { - return static_cast(alignUp(static_cast(numThreads + numSyncObjects) * 256u, 8u)); +int APS5_VABI sceUltConditionVariableCreate(void* condition, const char* name, void* mutex, const void* optParam, std::uint32_t buildVersion) { + (void)name; + (void)buildVersion; + if (condition == nullptr || mutex == nullptr) return ULT_ERROR_NULL; + if (optParam != nullptr) throw std::runtime_error("ULT condition variable options are not implemented"); + std::lock_guard lock(gMutex); + const auto mutexIt = gMutexes.find(mutex); + if (mutexIt == gMutexes.end()) return ULT_ERROR_INVALID; + if (gConditions.contains(condition)) return ULT_ERROR_STATE; + auto state = std::make_shared(); + state->_mutex = mutexIt->second; + std::lock_guard stateLock(state->_mutex->_mutex); + if (!state->_mutex->_alive) return ULT_ERROR_STATE; + gConditions.emplace(condition, state); + ++state->_mutex->_conditions; + return ULT_OK; +} + +int APS5_VABI _sceUltConditionVariableCreate(void* condition, const char* name, void* mutex, const void* optParam, std::uint32_t buildVersion) { + return sceUltConditionVariableCreate(condition, name, mutex, optParam, buildVersion); +} + +int APS5_VABI sceUltConditionVariableDestroy(void* condition) { + std::lock_guard lock(gMutex); + const auto it = gConditions.find(condition); + if (it == gConditions.end()) return condition == nullptr ? ULT_ERROR_NULL : ULT_ERROR_STATE; + auto state = it->second; + std::lock_guard stateLock(state->_mutex->_mutex); + if (state->_waiters != 0) return ULT_ERROR_BUSY; + state->_alive = false; + --state->_mutex->_conditions; + gConditions.erase(it); + return ULT_OK; +} + +int APS5_VABI sceUltConditionVariableSignal(void* condition) { + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + const auto it = gConditions.find(condition); + if (it == gConditions.end()) return condition == nullptr ? ULT_ERROR_NULL : ULT_ERROR_STATE; + state = it->second; + } + std::lock_guard stateLock(state->_mutex->_mutex); + if (!state->_alive || !state->_mutex->_alive) return ULT_ERROR_STATE; + if (!state->_queue.empty()) { + auto* waiter = state->_queue.front(); + state->_queue.pop_front(); + waiter->_signaled = true; + waiter->_available.notify_one(); + } + return ULT_OK; +} + +int APS5_VABI sceUltConditionVariableWait(void* condition) { + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + const auto it = gConditions.find(condition); + if (it == gConditions.end()) return condition == nullptr ? ULT_ERROR_NULL : ULT_ERROR_STATE; + state = it->second; + } + const auto held = state->_mutex; + const auto owner = std::this_thread::get_id(); + std::unique_lock lock(held->_mutex); + if (!state->_alive || !held->_alive || held->_owner != owner || held->_lockCount == 0) return ULT_ERROR_STATE; + if (held->_lockCount != 1) throw std::runtime_error("ULT condition wait with a recursively held mutex is not implemented"); + UltConditionWaiter waiter; + state->_queue.push_back(&waiter); + ++state->_waiters; + ++held->_conditionWaiters; + held->_lockCount = 0; + held->_owner = {}; + held->_available.notify_one(); + try { + waiter._available.wait(lock, [&] { return waiter._signaled || !state->_alive || !held->_alive; }); + if (held->_alive && state->_alive) { + ++held->_waiters; + try { + held->_available.wait(lock, [&] { return !held->_alive || held->_lockCount == 0; }); + } catch (...) { + --held->_waiters; + throw; + } + --held->_waiters; + } + } catch (...) { + state->_queue.remove(&waiter); + --state->_waiters; + --held->_conditionWaiters; + throw; + } + state->_queue.remove(&waiter); + --state->_waiters; + --held->_conditionWaiters; + if (!held->_alive || !state->_alive) return ULT_ERROR_STATE; + held->_owner = owner; + held->_lockCount = 1; + return ULT_OK; +} + +std::uint64_t APS5_VABI sceUltWaitingQueueResourcePoolGetWorkAreaSize(std::uint32_t numThreads, std::uint32_t numSyncObjects) { + return 32ull * (numSyncObjects + 2u * numThreads + 2u); } int APS5_VABI sceUltWaitingQueueResourcePoolCreate(void* pool, const char* name, std::uint32_t numThreads, std::uint32_t numSyncObjects, void* workArea, const void* optParam, std::uint32_t buildVersion) { @@ -226,15 +496,52 @@ int APS5_VABI sceUltWaitingQueueResourcePoolCreate(void* pool, const char* name, return ULT_OK; } +int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void* pool) { + if (pool == nullptr) { + return ULT_ERROR_NULL; + } + std::lock_guard lock(gMutex); + auto it = gResourcePools.find(pool); + if (it == gResourcePools.end()) { + return ULT_ERROR_STATE; + } + for (const auto& entry : gMutexes) { + if (entry.second->_waitingPool == pool) { + std::lock_guard stateLock(entry.second->_mutex); + if (entry.second->_waiters != 0) { + return ULT_ERROR_STATE; + } + } + } + for (const auto& entry : gSemaphores) { + if (entry.second->_waitingPool == pool) { + std::lock_guard stateLock(entry.second->_mutex); + if (entry.second->_waiters != 0) { + return ULT_ERROR_STATE; + } + } + } + for (const auto& entry : gQueues) { + if (entry.second->_pool && entry.second->_pool->_waitingPool == pool) { + std::lock_guard poolLock(entry.second->_pool->_mutex); + if (entry.second->_waiters != 0) { + return ULT_ERROR_STATE; + } + } + } + if (UltReaderWriterLocksWaitOn(pool)) { + return ULT_ERROR_BUSY; + } + gResourcePools.erase(it); + return ULT_OK; +} + std::uint64_t APS5_VABI sceUltQueueDataResourcePoolGetWorkAreaSize(std::uint32_t numData, std::uint64_t dataSize, std::uint32_t numQueueObject) { - if (dataSize > UINT64_MAX - 7u) throw std::out_of_range("ULT queue data size overflow"); - const auto stride = alignUp(dataSize, 8u); - const auto queueArea = static_cast(numQueueObject) * 512u; - if (stride != 0 && numData > (UINT64_MAX - queueArea) / stride) { + const auto nodeArea = 32ull * (numQueueObject + 2u * numData + 3u); + if (dataSize != 0 && numData > (UINT64_MAX - nodeArea) / dataSize) { throw std::out_of_range("ULT queue work area size overflow"); } - const std::uint64_t dataArea = static_cast(numData) * stride; - return alignUp(dataArea + queueArea, 8u); + return static_cast(numData) * dataSize + nodeArea; } int APS5_VABI sceUltQueueDataResourcePoolCreate(void* pool, const char* name, std::uint32_t numData, std::uint64_t dataSize, std::uint32_t numQueueObject, void* waitingQueueResourcePool, void* workArea, const void* optParam, std::uint32_t buildVersion) { @@ -305,7 +612,7 @@ int APS5_VABI sceUltQueueCreate(void* queue, const char* name, std::uint64_t dat auto& pool = *state->_pool; std::lock_guard stateLock(pool._mutex); if (dataSize > pool._dataSize || waitingQueueResourcePool != pool._waitingPool) return ULT_ERROR_INVALID; - if (pool._queues == pool._numQueueObject) return ULT_ERROR_AGAIN; + if (pool._queues == pool._numQueueObject) return ULT_ERROR_BUSY; gQueues.emplace(queue, std::move(state)); ++pool._queues; std::memset(queue, 0, 512); @@ -337,7 +644,7 @@ int APS5_VABI sceUltQueueDestroy(void* queue) { auto& pool = *state->_pool; { std::lock_guard stateLock(pool._mutex); - if (state->_waiters != 0) return ULT_ERROR_BUSY; + if (state->_waiters != 0) return ULT_ERROR_STATE; ReleaseQueue(*state); gQueues.erase(it); } @@ -345,7 +652,7 @@ int APS5_VABI sceUltQueueDestroy(void* queue) { return ULT_OK; } -int APS5_VABI sceUltSemaphoreCreate(void* semaphore, const char* name, std::int32_t numInitialResource, void* waitingQueueResourcePool, const void* optParam, std::uint32_t buildVersion) { +int APS5_VABI sceUltSemaphoreCreate(void* semaphore, const char* name, std::uint32_t numInitialResource, void* waitingQueueResourcePool, const void* optParam, std::uint32_t buildVersion) { (void)name; (void)optParam; (void)buildVersion; @@ -355,11 +662,9 @@ int APS5_VABI sceUltSemaphoreCreate(void* semaphore, const char* name, std::int3 if ((reinterpret_cast(semaphore) & 7u) != 0) { return ULT_ERROR_ALIGNMENT; } - if (numInitialResource < 0) { - return ULT_ERROR_RANGE; - } auto state = std::make_shared(); - state->_resources = numInitialResource; + state->_resources = std::bit_cast(numInitialResource); + state->_waitingPool = waitingQueueResourcePool; std::lock_guard lock(gMutex); if (waitingQueueResourcePool != nullptr && gResourcePools.find(waitingQueueResourcePool) == gResourcePools.end()) { return ULT_ERROR_INVALID; @@ -373,6 +678,8 @@ int APS5_VABI sceUltSemaphoreCreate(void* semaphore, const char* name, std::int3 } int APS5_VABI sceUltSemaphoreAcquire(void* semaphore, std::int32_t numResource) { + if (semaphore == nullptr) return ULT_ERROR_NULL; + if ((reinterpret_cast(semaphore) & 7u) != 0) return ULT_ERROR_ALIGNMENT; if (numResource <= 0) { return ULT_ERROR_RANGE; } @@ -395,6 +702,8 @@ int APS5_VABI sceUltSemaphoreAcquire(void* semaphore, std::int32_t numResource) } int APS5_VABI sceUltSemaphoreTryAcquire(void* semaphore, std::int32_t numResource) { + if (semaphore == nullptr) return ULT_ERROR_NULL; + if ((reinterpret_cast(semaphore) & 7u) != 0) return ULT_ERROR_ALIGNMENT; if (numResource <= 0) { return ULT_ERROR_RANGE; } @@ -414,6 +723,8 @@ int APS5_VABI sceUltSemaphoreTryAcquire(void* semaphore, std::int32_t numResourc } int APS5_VABI sceUltSemaphoreRelease(void* semaphore, std::int32_t numResource) { + if (semaphore == nullptr) return ULT_ERROR_NULL; + if ((reinterpret_cast(semaphore) & 7u) != 0) return ULT_ERROR_ALIGNMENT; if (numResource <= 0) { return ULT_ERROR_RANGE; } @@ -436,9 +747,8 @@ int APS5_VABI sceUltSemaphoreRelease(void* semaphore, std::int32_t numResource) } int APS5_VABI sceUltSemaphoreDestroy(void* semaphore) { - if (semaphore == nullptr) { - return ULT_ERROR_NULL; - } + if (semaphore == nullptr) return ULT_ERROR_NULL; + if ((reinterpret_cast(semaphore) & 7u) != 0) return ULT_ERROR_ALIGNMENT; std::lock_guard globalLock(gMutex); auto it = gSemaphores.find(semaphore); if (it == gSemaphores.end()) { @@ -447,7 +757,7 @@ int APS5_VABI sceUltSemaphoreDestroy(void* semaphore) { auto state = it->second; std::lock_guard stateLock(state->_mutex); if (state->_waiters != 0) { - return ULT_ERROR_BUSY; + return ULT_ERROR_STATE; } state->_alive = false; gSemaphores.erase(it); @@ -464,7 +774,11 @@ int APS5_VABI sceUltUlthreadRuntimeOptParamInitialize(UltUlthreadRuntimeOptParam } std::uint64_t APS5_VABI sceUltUlthreadRuntimeGetWorkAreaSize(std::uint32_t maxNumUlthread, std::uint32_t numWorkerThread) { - return alignUp(static_cast(maxNumUlthread) * 256u + static_cast(numWorkerThread) * 16u * 1024u, 8u); + const std::uint32_t totalThreads = maxNumUlthread + numWorkerThread; + const auto threadNodes = 32ull * (2u * maxNumUlthread + 4u); + const auto threadArea = (threadNodes + 4ull * maxNumUlthread + 7u) & 0x3FFFFFFFF8ull; + const auto schedulerArea = 32ull * (2u * numWorkerThread + 4u) + 32ull * (2u * totalThreads + 4u); + return threadArea + schedulerArea + 512ull * numWorkerThread + 8ull * (static_cast(maxNumUlthread) + totalThreads); } int APS5_VABI sceUltUlthreadRuntimeCreate(void* runtime, const char* name, std::uint32_t maxNumUlthread, std::uint32_t numWorkerThread, void* workArea, const void* optParam, std::uint32_t buildVersion) { @@ -491,6 +805,7 @@ int APS5_VABI sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthread auto state = std::make_shared(); state->_entry = entry; state->_arg = arg; + state->_object = ulthread; state->_runtime = runtime; { std::lock_guard lock(gMutex); @@ -548,12 +863,40 @@ int APS5_VABI sceUltUlthreadTryJoin(void* ulthread, std::int32_t* status) { return ULT_ERROR_STATE; } if (!it->second->_exited.load(std::memory_order_acquire)) { - return ULT_ERROR_BUSY; + return ULT_ERROR_AGAIN; } } return sceUltUlthreadJoin(ulthread, status); } +int APS5_VABI sceUltUlthreadGetSelf(void** ulthread) { + if (ulthread == nullptr) { + return ULT_ERROR_NULL; + } + if (gCurrentUlthread == nullptr) { + return ULT_ERROR_STATE; + } + *ulthread = gCurrentUlthread->_object; + return ULT_OK; +} + +int APS5_VABI sceUltUlthreadYield() { + if (gCurrentUlthread == nullptr) { + return ULT_ERROR_STATE; + } + scePthreadYield(); + return ULT_OK; +} + +int APS5_VABI sceUltUlthreadExit(std::int32_t status) { + if (gCurrentUlthread == nullptr) { + return ULT_ERROR_STATE; + } + gCurrentUlthread->_exited.store(true, std::memory_order_release); + scePthreadExit(reinterpret_cast(static_cast(status))); + return ULT_OK; +} + int APS5_VABI sceUltUlthreadRuntimeDestroy(void* runtime) { if (runtime == nullptr) { return ULT_ERROR_NULL; @@ -565,47 +908,193 @@ int APS5_VABI sceUltUlthreadRuntimeDestroy(void* runtime) { } for (const auto& entry : gUlthreads) { if (entry.second->_runtime == runtime) { - return ULT_ERROR_BUSY; + return ULT_ERROR_STATE; } } gRuntimes.erase(it); return ULT_OK; } -int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void* pool, const char* name, std::uint32_t numThreads, std::uint32_t numSyncObjects, void* workArea, const void* optParam, std::uint32_t buildVersion) { - return sceUltWaitingQueueResourcePoolCreate(pool, name, numThreads, numSyncObjects, workArea, optParam, buildVersion); +int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void* pool, const char* name, std::uint32_t numThreads, std::uint32_t numSyncObjects, void* workArea, const void* optParam) { + if (pool == nullptr) return ULT_ERROR_NULL; + if ((reinterpret_cast(pool) & 7u) != 0) return ULT_ERROR_ALIGNMENT; + if (name == nullptr) return ULT_ERROR_NULL; + if (const int result = validateCommonOptParam(optParam); result != ULT_OK) return result; + if (numThreads == 0 || numSyncObjects == 0) return ULT_ERROR_INVALID; + if ((reinterpret_cast(workArea) & 7u) != 0) return ULT_ERROR_ALIGNMENT; + if (workArea == nullptr) return ULT_ERROR_NULL; + return sceUltWaitingQueueResourcePoolCreate(pool, name, numThreads, numSyncObjects, workArea, optParam, 0); } -int APS5_VABI _sceUltSemaphoreCreate(void* semaphore, const char* name, std::int32_t numInitialResource, void* waitingQueueResourcePool, const void* optParam, std::uint32_t buildVersion) { - return sceUltSemaphoreCreate(semaphore, name, numInitialResource, waitingQueueResourcePool, optParam, buildVersion); +int APS5_VABI _sceUltSemaphoreCreate(void* semaphore, const char* name, std::uint32_t numInitialResource, void* waitingQueueResourcePool, const void* optParam) { + if (semaphore == nullptr || name == nullptr) return ULT_ERROR_NULL; + if (((reinterpret_cast(semaphore) | reinterpret_cast(waitingQueueResourcePool)) & 7u) != 0) return ULT_ERROR_ALIGNMENT; + if (const int result = validateCommonOptParam(optParam); result != ULT_OK) return result; + return sceUltSemaphoreCreate(semaphore, name, numInitialResource, waitingQueueResourcePool, optParam, 0); } int APS5_VABI _sceUltMutexOptParamInitialize(UltMutexOptParam* optParam, std::uint32_t buildVersion) { return sceUltMutexOptParamInitialize(optParam, buildVersion); } -int APS5_VABI _sceUltMutexCreate(void* mutex, const char* name, void* waitingQueueResourcePool, const UltMutexOptParam* optParam, std::uint32_t buildVersion) { - return sceUltMutexCreate(mutex, name, waitingQueueResourcePool, optParam, buildVersion); +int APS5_VABI _sceUltMutexCreate(void* mutex, const char* name, void* waitingQueueResourcePool, const UltMutexOptParam* optParam) { + if (mutex == nullptr || name == nullptr || waitingQueueResourcePool == nullptr) { + return ULT_ERROR_NULL; + } + if (((reinterpret_cast(mutex) | reinterpret_cast(optParam) | reinterpret_cast(waitingQueueResourcePool)) & 7u) != 0) { + return ULT_ERROR_ALIGNMENT; + } + if (optParam != nullptr && (optParam->header[0] != 0x6C696275 || optParam->header[1] != 0x6C746D61 || optParam->header[2] != 0x67696300 || optParam->attribute > 1 || std::any_of(std::begin(optParam->reserved), std::end(optParam->reserved), [](std::uint32_t value) { return value != 0; }))) { + return ULT_ERROR_INVALID; + } + return sceUltMutexCreate(mutex, name, waitingQueueResourcePool, optParam, 0); } -int APS5_VABI _sceUltQueueDataResourcePoolCreate(void* pool, const char* name, std::uint32_t numData, std::uint64_t dataSize, std::uint32_t numQueueObject, void* waitingQueueResourcePool, void* workArea, const void* optParam, std::uint32_t buildVersion) { - return sceUltQueueDataResourcePoolCreate(pool, name, numData, dataSize, numQueueObject, waitingQueueResourcePool, workArea, optParam, buildVersion); +int APS5_VABI _sceUltQueueDataResourcePoolCreate(void* pool, const char* name, std::uint32_t numData, std::uint64_t dataSize, std::uint32_t numQueueObject, void* waitingQueueResourcePool, void* workArea, const void* optParam) { + if (pool == nullptr) return ULT_ERROR_NULL; + if ((reinterpret_cast(pool) & 7u) != 0) return ULT_ERROR_ALIGNMENT; + if (const int result = validateCommonOptParam(optParam); result != ULT_OK) return result; + if (numData == 0 || dataSize == 0 || numQueueObject == 0) return ULT_ERROR_INVALID; + if ((reinterpret_cast(workArea) & 7u) != 0) return ULT_ERROR_ALIGNMENT; + if (workArea == nullptr || name == nullptr) return ULT_ERROR_NULL; + if ((reinterpret_cast(waitingQueueResourcePool) & 7u) != 0) return ULT_ERROR_ALIGNMENT; + return sceUltQueueDataResourcePoolCreate(pool, name, numData, dataSize, numQueueObject, waitingQueueResourcePool, workArea, optParam, 0); } -int APS5_VABI _sceUltQueueCreate(void* queue, const char* name, std::uint64_t dataSize, void* waitingQueueResourcePool, void* queueDataResourcePool, const void* optParam, std::uint32_t buildVersion) { - return sceUltQueueCreate(queue, name, dataSize, waitingQueueResourcePool, queueDataResourcePool, optParam, buildVersion); +int APS5_VABI _sceUltQueueCreate(void* queue, const char* name, std::uint64_t dataSize, void* waitingQueueResourcePool, void* queueDataResourcePool, const void* optParam) { + if (queue == nullptr || name == nullptr || queueDataResourcePool == nullptr) return ULT_ERROR_NULL; + if (((reinterpret_cast(queue) | reinterpret_cast(waitingQueueResourcePool) | reinterpret_cast(queueDataResourcePool)) & 7u) != 0) return ULT_ERROR_ALIGNMENT; + { + std::lock_guard lock(gMutex); + const auto it = gQueueDataPools.find(queueDataResourcePool); + if (it == gQueueDataPools.end()) return ULT_ERROR_INVALID; + if (dataSize == 0 || dataSize > it->second->_dataSize) return ULT_ERROR_INVALID; + } + if (const int result = validateCommonOptParam(optParam); result != ULT_OK) return result; + return sceUltQueueCreate(queue, name, dataSize, waitingQueueResourcePool, queueDataResourcePool, optParam, 0); } int APS5_VABI _sceUltUlthreadRuntimeOptParamInitialize(UltUlthreadRuntimeOptParam* optParam, std::uint32_t buildVersion) { return sceUltUlthreadRuntimeOptParamInitialize(optParam, buildVersion); } -int APS5_VABI _sceUltUlthreadRuntimeCreate(void* runtime, const char* name, std::uint32_t maxNumUlthread, std::uint32_t numWorkerThread, void* workArea, const void* optParam, std::uint32_t buildVersion) { - return sceUltUlthreadRuntimeCreate(runtime, name, maxNumUlthread, numWorkerThread, workArea, optParam, buildVersion); +int APS5_VABI _sceUltUlthreadRuntimeCreate(void* runtime, const char* name, std::uint32_t maxNumUlthread, std::uint32_t numWorkerThread, void* workArea, const void* optParam) { + return sceUltUlthreadRuntimeCreate(runtime, name, maxNumUlthread, numWorkerThread, workArea, optParam, 0); } -int APS5_VABI _sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthreadEntry entry, std::uint64_t arg, void* context, std::uint64_t sizeContext, void* runtime, const void* optParam, std::uint32_t buildVersion) { - return sceUltUlthreadCreate(ulthread, name, entry, arg, context, sizeContext, runtime, optParam, buildVersion); +int APS5_VABI _sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthreadEntry entry, std::uint64_t arg, void* context, std::uint64_t sizeContext, void* runtime, const void* optParam) { + return sceUltUlthreadCreate(ulthread, name, entry, arg, context, sizeContext, runtime, optParam, 0); } +int APS5_VABI sceUltConditionVariableSignalAll(void* conditionVariable) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetConditionVariableInfo(void* object, UltConditionVariableInfo* info, UltWaitingThreadInfo* waitingThreads, std::uint32_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetMutexInfo(void* object, UltMutexInfo* info, UltWaitingThreadInfo* waitingThreads, std::uint32_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetQueueDataResourcePoolInfo(void* object, UltQueueDataResourcePoolInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetQueueInfo(void* object, UltQueueInfo* info, UltWaitingThreadInfo* waitingThreads, std::uint32_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetReaderWriterLockInfo(void* object, UltReaderWriterLockInfo* info, UltWaitingThreadInfo* waitingThreads, std::uint32_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetSemaphoreInfo(void* object, UltSemaphoreInfo* info, UltWaitingThreadInfo* waitingThreads, std::uint32_t capacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetUlthreadInfo(void* object, UltUlthreadInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetUlthreadRuntimeInfo(void* runtime, UltUlthreadRuntimeInfo* info, void** ulthreads, std::uint32_t ulthreadCapacity, Pthread* workerThreads, std::uint32_t workerCapacity) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltGetWaitingQueueResourcePoolInfo(void* object, UltWaitingQueueResourcePoolInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltAbstractThreadFactoryCreateThread() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltAbstractThreadFactoryGetNumMaxThreads() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltAbstractThreadFactoryJoinThread() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltKernelThreadFactoryFinalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltKernelThreadFactoryGetFactoryPointer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltKernelThreadFactoryInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltModuleFinalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltModuleInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadFactoryFinalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadFactoryGetFactoryPointer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadFactoryGetWorkAreaSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUltUlthreadFactoryInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + + } diff --git a/core/libs/prx/libSceUlt/SceUltTypes.hpp b/core/libs/prx/libSceUlt/SceUltTypes.hpp index 0a314a9d56..4c77af1649 100644 --- a/core/libs/prx/libSceUlt/SceUltTypes.hpp +++ b/core/libs/prx/libSceUlt/SceUltTypes.hpp @@ -4,8 +4,13 @@ #include #include #include +#include + +#include #include +#include #include +#include #include #include "SceTypes.hpp" @@ -14,13 +19,275 @@ constexpr int ULT_ERROR_NULL = -2139029503; constexpr int ULT_ERROR_ALIGNMENT = -2139029502; constexpr int ULT_ERROR_RANGE = -2139029501; constexpr int ULT_ERROR_INVALID = -2139029500; +constexpr int ULT_ERROR_PERMISSION = -2139029499; constexpr int ULT_ERROR_STATE = -2139029498; constexpr int ULT_ERROR_BUSY = -2139029497; constexpr int ULT_ERROR_AGAIN = -2139029496; +struct UltOptParam { + std::uint8_t bytes[128]; +}; + +inline int InitializeOptParam(UltOptParam* optParam) { + if (optParam == nullptr) { + return ULT_ERROR_NULL; + } + std::memset(optParam, 0, sizeof(*optParam)); + return ULT_OK; +} + +struct alignas(8) UltCommonOptParam { + std::uint32_t header[3]; + std::uint32_t buildVersion; + std::uint8_t reserved[112]; +}; + +static_assert(sizeof(UltCommonOptParam) == 128); + +struct UltWaitingThreadInfo { + std::uint64_t size; + void* thread; + std::uint32_t threadType; + std::int32_t waitValue; + std::byte reserved[40]; +}; + +static_assert(sizeof(UltWaitingThreadInfo) == 64); +static_assert(offsetof(UltWaitingThreadInfo, size) == 0); +static_assert(offsetof(UltWaitingThreadInfo, thread) == 8); +static_assert(offsetof(UltWaitingThreadInfo, threadType) == 16); +static_assert(offsetof(UltWaitingThreadInfo, waitValue) == 20); +static_assert(offsetof(UltWaitingThreadInfo, reserved) == 24); + +struct UltConditionVariableInfo { + std::uint64_t size; + void* object; + char name[32]; + void* mutex; + std::uint32_t waitingThreads; + std::byte reserved[68]; +}; + +static_assert(sizeof(UltConditionVariableInfo) == 128); +static_assert(offsetof(UltConditionVariableInfo, size) == 0); +static_assert(offsetof(UltConditionVariableInfo, object) == 8); +static_assert(offsetof(UltConditionVariableInfo, name) == 16); +static_assert(offsetof(UltConditionVariableInfo, mutex) == 48); +static_assert(offsetof(UltConditionVariableInfo, waitingThreads) == 56); +static_assert(offsetof(UltConditionVariableInfo, reserved) == 60); + +struct UltMutexInfo { + std::uint64_t size; + void* object; + char name[32]; + void* waitingQueuePool; + void* owner; + std::uint32_t ownerType; + std::uint32_t attributes; + std::uint32_t lockCount; + std::uint32_t waitingThreads; + std::byte reserved[48]; +}; + +static_assert(sizeof(UltMutexInfo) == 128); +static_assert(offsetof(UltMutexInfo, size) == 0); +static_assert(offsetof(UltMutexInfo, object) == 8); +static_assert(offsetof(UltMutexInfo, name) == 16); +static_assert(offsetof(UltMutexInfo, waitingQueuePool) == 48); +static_assert(offsetof(UltMutexInfo, owner) == 56); +static_assert(offsetof(UltMutexInfo, ownerType) == 64); +static_assert(offsetof(UltMutexInfo, attributes) == 68); +static_assert(offsetof(UltMutexInfo, lockCount) == 72); +static_assert(offsetof(UltMutexInfo, waitingThreads) == 76); +static_assert(offsetof(UltMutexInfo, reserved) == 80); + +struct UltSemaphoreInfo { + std::uint64_t size; + void* object; + char name[32]; + void* waitingQueuePool; + std::uint32_t resources; + std::uint32_t waitingThreads; + std::byte reserved[64]; +}; + +static_assert(sizeof(UltSemaphoreInfo) == 128); +static_assert(offsetof(UltSemaphoreInfo, size) == 0); +static_assert(offsetof(UltSemaphoreInfo, object) == 8); +static_assert(offsetof(UltSemaphoreInfo, name) == 16); +static_assert(offsetof(UltSemaphoreInfo, waitingQueuePool) == 48); +static_assert(offsetof(UltSemaphoreInfo, resources) == 56); +static_assert(offsetof(UltSemaphoreInfo, waitingThreads) == 60); +static_assert(offsetof(UltSemaphoreInfo, reserved) == 64); + +struct UltReaderWriterLockInfo { + std::uint64_t size; + void* object; + char name[32]; + void* waitingQueuePool; + std::uint32_t lockType; + std::uint32_t readers; + void* owner; + std::uint32_t ownerType; + std::uint32_t waitingThreads; + std::byte reserved[48]; +}; + +static_assert(sizeof(UltReaderWriterLockInfo) == 128); +static_assert(offsetof(UltReaderWriterLockInfo, size) == 0); +static_assert(offsetof(UltReaderWriterLockInfo, object) == 8); +static_assert(offsetof(UltReaderWriterLockInfo, name) == 16); +static_assert(offsetof(UltReaderWriterLockInfo, waitingQueuePool) == 48); +static_assert(offsetof(UltReaderWriterLockInfo, lockType) == 56); +static_assert(offsetof(UltReaderWriterLockInfo, readers) == 60); +static_assert(offsetof(UltReaderWriterLockInfo, owner) == 64); +static_assert(offsetof(UltReaderWriterLockInfo, ownerType) == 72); +static_assert(offsetof(UltReaderWriterLockInfo, waitingThreads) == 76); +static_assert(offsetof(UltReaderWriterLockInfo, reserved) == 80); + +struct UltQueueInfo { + std::uint64_t size; + void* object; + char name[32]; + void* waitingQueuePool; + void* dataPool; + std::uint64_t dataSize; + std::uint32_t queuedItems; + std::uint32_t waitingPushers; + std::uint32_t waitingPoppers; + std::byte reserved[172]; +}; + +static_assert(sizeof(UltQueueInfo) == 256); +static_assert(offsetof(UltQueueInfo, size) == 0); +static_assert(offsetof(UltQueueInfo, object) == 8); +static_assert(offsetof(UltQueueInfo, name) == 16); +static_assert(offsetof(UltQueueInfo, waitingQueuePool) == 48); +static_assert(offsetof(UltQueueInfo, dataPool) == 56); +static_assert(offsetof(UltQueueInfo, dataSize) == 64); +static_assert(offsetof(UltQueueInfo, queuedItems) == 72); +static_assert(offsetof(UltQueueInfo, waitingPushers) == 76); +static_assert(offsetof(UltQueueInfo, waitingPoppers) == 80); +static_assert(offsetof(UltQueueInfo, reserved) == 84); + +struct UltWaitingQueueResourcePoolInfo { + std::uint64_t size; + void* object; + char name[32]; + std::uint32_t availableThreads; + std::uint32_t maxThreads; + std::uint32_t availableSyncObjects; + std::uint32_t maxSyncObjects; + std::byte reserved[64]; +}; + +static_assert(sizeof(UltWaitingQueueResourcePoolInfo) == 128); +static_assert(offsetof(UltWaitingQueueResourcePoolInfo, size) == 0); +static_assert(offsetof(UltWaitingQueueResourcePoolInfo, object) == 8); +static_assert(offsetof(UltWaitingQueueResourcePoolInfo, name) == 16); +static_assert(offsetof(UltWaitingQueueResourcePoolInfo, availableThreads) == 48); +static_assert(offsetof(UltWaitingQueueResourcePoolInfo, maxThreads) == 52); +static_assert(offsetof(UltWaitingQueueResourcePoolInfo, availableSyncObjects) == 56); +static_assert(offsetof(UltWaitingQueueResourcePoolInfo, maxSyncObjects) == 60); +static_assert(offsetof(UltWaitingQueueResourcePoolInfo, reserved) == 64); + +struct UltQueueDataResourcePoolInfo { + std::uint64_t size; + void* object; + char name[32]; + void* waitingQueuePool; + std::uint64_t dataSize; + std::uint32_t availableItems; + std::uint32_t maxItems; + std::uint32_t availableQueues; + std::uint32_t maxQueues; + std::byte reserved[48]; +}; + +static_assert(sizeof(UltQueueDataResourcePoolInfo) == 128); +static_assert(offsetof(UltQueueDataResourcePoolInfo, size) == 0); +static_assert(offsetof(UltQueueDataResourcePoolInfo, object) == 8); +static_assert(offsetof(UltQueueDataResourcePoolInfo, name) == 16); +static_assert(offsetof(UltQueueDataResourcePoolInfo, waitingQueuePool) == 48); +static_assert(offsetof(UltQueueDataResourcePoolInfo, dataSize) == 56); +static_assert(offsetof(UltQueueDataResourcePoolInfo, availableItems) == 64); +static_assert(offsetof(UltQueueDataResourcePoolInfo, maxItems) == 68); +static_assert(offsetof(UltQueueDataResourcePoolInfo, availableQueues) == 72); +static_assert(offsetof(UltQueueDataResourcePoolInfo, maxQueues) == 76); +static_assert(offsetof(UltQueueDataResourcePoolInfo, reserved) == 80); + +struct UltUlthreadRuntimeInfo { + std::uint64_t size; + void* object; + char name[32]; + std::uint32_t activeUlthreads; + std::uint32_t maxUlthreads; + std::uint32_t workerThreads; + std::uint32_t reserved60; + std::uint64_t workerStackSize; + std::byte reserved[56]; +}; + +static_assert(sizeof(UltUlthreadRuntimeInfo) == 128); +static_assert(offsetof(UltUlthreadRuntimeInfo, size) == 0); +static_assert(offsetof(UltUlthreadRuntimeInfo, object) == 8); +static_assert(offsetof(UltUlthreadRuntimeInfo, name) == 16); +static_assert(offsetof(UltUlthreadRuntimeInfo, maxUlthreads) == 52); +static_assert(offsetof(UltUlthreadRuntimeInfo, activeUlthreads) == 48); +static_assert(offsetof(UltUlthreadRuntimeInfo, workerThreads) == 56); +static_assert(offsetof(UltUlthreadRuntimeInfo, reserved60) == 60); +static_assert(offsetof(UltUlthreadRuntimeInfo, workerStackSize) == 64); +static_assert(offsetof(UltUlthreadRuntimeInfo, reserved) == 72); + +struct UltUlthreadInfo { + std::uint64_t size; + void* object; + char name[32]; + std::uint32_t contextAttribute; + std::uint32_t reserved52; + void* runtime; + FiberInfo fiber; + std::byte reserved[64]; +}; + +static_assert(sizeof(FiberInfo) == 128); +static_assert(offsetof(FiberInfo, entry) == 8); +static_assert(offsetof(FiberInfo, arg_on_initialize) == 16); +static_assert(offsetof(FiberInfo, addr_context) == 24); +static_assert(offsetof(FiberInfo, size_context) == 32); +static_assert(offsetof(FiberInfo, name) == 40); +static_assert(offsetof(FiberInfo, size_context_margin) == 72); +static_assert(sizeof(UltUlthreadInfo) == 256); +static_assert(offsetof(UltUlthreadInfo, object) == 8); +static_assert(offsetof(UltUlthreadInfo, name) == 16); +static_assert(offsetof(UltUlthreadInfo, contextAttribute) == 48); +static_assert(offsetof(UltUlthreadInfo, runtime) == 56); +static_assert(offsetof(UltUlthreadInfo, fiber) == 64); +static_assert(offsetof(UltUlthreadInfo, reserved) == 192); + struct UltMutexState { - std::recursive_mutex _mutex; + std::mutex _mutex; + std::condition_variable _available; + std::thread::id _owner{}; + std::uint32_t _lockCount = 0; + std::uint32_t _waiters = 0; + std::uint32_t _conditions = 0; + std::uint32_t _conditionWaiters = 0; + bool _alive = true; std::uint32_t _attribute = 0; + void* _waitingPool = nullptr; +}; + +struct UltConditionWaiter { + std::condition_variable _available; + bool _signaled = false; +}; + +struct UltConditionState { + std::shared_ptr _mutex; + std::list _queue; + std::uint32_t _waiters = 0; + bool _alive = true; }; struct UltSemaphoreState { @@ -29,6 +296,7 @@ struct UltSemaphoreState { std::int32_t _resources = 0; std::uint32_t _waiters = 0; bool _alive = true; + void* _waitingPool = nullptr; }; struct UltResourcePoolState { @@ -70,6 +338,7 @@ struct UltRuntimeState { struct UltUlthreadState { UltUlthreadEntry _entry = nullptr; std::uint64_t _arg = 0; + void* _object = nullptr; Pthread _thread = nullptr; void* _runtime = nullptr; std::atomic _exited{false}; diff --git a/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp b/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp new file mode 100644 index 0000000000..c947260041 --- /dev/null +++ b/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp @@ -0,0 +1,201 @@ +#include "prx/libSceUlt/src/ReaderWriterLock.hpp" +#include "prx/libSceUlt/SceUltTypes.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct UltReaderWriterLockState { + std::mutex _mutex; + std::condition_variable _released; + std::uint32_t _readers = 0; + bool _writing = false; + std::thread::id _writer{}; + std::uint32_t _waiters = 0; + bool _alive = true; + void* _waitingPool = nullptr; +}; + +std::mutex gLocksMutex; +std::unordered_map> gLocks; + +int FindLock(void* rwlock, std::shared_ptr& state) { + if (rwlock == nullptr) { + return ULT_ERROR_NULL; + } + std::lock_guard lock(gLocksMutex); + const auto it = gLocks.find(rwlock); + if (it == gLocks.end()) { + return ULT_ERROR_STATE; + } + state = it->second; + return ULT_OK; +} + +bool HeldByCaller(const UltReaderWriterLockState& state) { + return state._writing && state._writer == std::this_thread::get_id(); +} + +int Wait(UltReaderWriterLockState& state, std::unique_lock& lock, bool writer) { + ++state._waiters; + state._released.wait(lock, [&] { return !state._alive || (!state._writing && (!writer || state._readers == 0)); }); + --state._waiters; + return state._alive ? ULT_OK : ULT_ERROR_STATE; +} + +} + +bool UltReaderWriterLocksWaitOn(void* pool) { + std::lock_guard lock(gLocksMutex); + for (const auto& entry : gLocks) { + if (entry.second->_waitingPool != pool) continue; + std::lock_guard stateLock(entry.second->_mutex); + if (entry.second->_waiters != 0) return true; + } + return false; +} + +void UltFinalizeReaderWriterLocks() { + std::lock_guard lock(gLocksMutex); + for (const auto& entry : gLocks) { + std::lock_guard stateLock(entry.second->_mutex); + entry.second->_alive = false; + entry.second->_released.notify_all(); + } + gLocks.clear(); +} + +extern "C" { + +int APS5_VABI sceUltReaderWriterLockOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltReaderWriterLockCreate(void* rwlock, const char* name, void* waitingQueueResourcePool, const UltOptParam* optParam, std::uint32_t buildVersion) { + (void)name; + (void)optParam; + (void)buildVersion; + if (rwlock == nullptr) { + return ULT_ERROR_NULL; + } + if (waitingQueueResourcePool != nullptr && !UltWaitingPoolExists(waitingQueueResourcePool)) { + return ULT_ERROR_INVALID; + } + auto state = std::make_shared(); + state->_waitingPool = waitingQueueResourcePool; + std::lock_guard lock(gLocksMutex); + if (gLocks.contains(rwlock)) { + return ULT_ERROR_STATE; + } + std::memset(rwlock, 0, 256); + gLocks[rwlock] = std::move(state); + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockDestroy(void* rwlock) { + if (rwlock == nullptr) { + return ULT_ERROR_NULL; + } + std::lock_guard lock(gLocksMutex); + const auto it = gLocks.find(rwlock); + if (it == gLocks.end()) { + return ULT_ERROR_STATE; + } + const auto owner = it->second; + auto& state = *owner; + std::lock_guard stateLock(state._mutex); + if (state._readers != 0 || state._writing || state._waiters != 0) { + return ULT_ERROR_BUSY; + } + state._alive = false; + gLocks.erase(it); + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockLockRead(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::unique_lock lock(state->_mutex); + if (!state->_alive || HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + if (const int waited = Wait(*state, lock, false); waited != ULT_OK) return waited; + ++state->_readers; + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockTryLockRead(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::lock_guard lock(state->_mutex); + if (!state->_alive || HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + if (state->_writing) { + return ULT_ERROR_BUSY; + } + ++state->_readers; + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockUnlockRead(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::lock_guard lock(state->_mutex); + if (!state->_alive || state->_readers == 0) { + return ULT_ERROR_STATE; + } + if (--state->_readers == 0) { + state->_released.notify_all(); + } + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockLockWrite(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::unique_lock lock(state->_mutex); + if (!state->_alive || HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + if (const int waited = Wait(*state, lock, true); waited != ULT_OK) return waited; + state->_writing = true; + state->_writer = std::this_thread::get_id(); + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockTryLockWrite(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::lock_guard lock(state->_mutex); + if (!state->_alive || HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + if (state->_writing || state->_readers != 0) { + return ULT_ERROR_BUSY; + } + state->_writing = true; + state->_writer = std::this_thread::get_id(); + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockUnlockWrite(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::lock_guard lock(state->_mutex); + if (!state->_alive || !HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + state->_writing = false; + state->_writer = {}; + state->_released.notify_all(); + return ULT_OK; +} + +} diff --git a/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp b/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp new file mode 100644 index 0000000000..df98dcd077 --- /dev/null +++ b/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp @@ -0,0 +1,8 @@ +#ifndef CORE_LIBS_PRX_LIBSCEULT_SRC_READERWRITERLOCK_HPP +#define CORE_LIBS_PRX_LIBSCEULT_SRC_READERWRITERLOCK_HPP + +bool UltWaitingPoolExists(void* pool); +bool UltReaderWriterLocksWaitOn(void* pool); +void UltFinalizeReaderWriterLocks(); + +#endif diff --git a/core/libs/prx/libSceUpdateService/Export.cpp b/core/libs/prx/libSceUpdateService/Export.cpp new file mode 100644 index 0000000000..7fc0488786 --- /dev/null +++ b/core/libs/prx/libSceUpdateService/Export.cpp @@ -0,0 +1,208 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceUpsrvGetCompletedTaskId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvGetRebootFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvGetSystemExUpdateTaskState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvPostErrorLog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvResumePausedTask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvSetFakeVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvStartUpdateSession() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvSwitchSystemExBank() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvSwitchSystemExBankWithoutVerify() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateCancelBDUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateCheckCancel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateCheckCreateHandler() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateCheckDestroyHandler() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateCheckDoCheckSystem() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateCheckDoCheckSystemForDevKit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateCheckDoCheckSystemForSettings() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateCheckDoCheckSystemWithPupInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateCreateUpdateTask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateDestroyUpdateTask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateDoCheckBootUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateDoUpdateSystemEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateDoUpdateWithEntitlement() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateDoUpdateWithPupInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateDoUpdateWithPupPath2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateGetBetaAgreementUrl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateGetEulaDisplay() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateGetEulaStr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateGetEulaStrLen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateGetFeatureListUrl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateGetFeatureListUrlForSystemExUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateGetFeatureListUrlForUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateGetImageWritingProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateGetProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateGetUpdateTaskInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateGetUpdateTaskMgrInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateSetEulaAccept() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvUpdateStartUpdateTask() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUpsrvVerifySystemExBank() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceUrlConfigResolver/Export.cpp b/core/libs/prx/libSceUrlConfigResolver/Export.cpp new file mode 100644 index 0000000000..60d243d931 --- /dev/null +++ b/core/libs/prx/libSceUrlConfigResolver/Export.cpp @@ -0,0 +1,49 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceUrlConfigResolverGetAppConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUrlConfigResolverGetDefaultQueryParameter() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUrlConfigResolverGetDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUrlConfigResolverGetJscHeapSizeSoftLimit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + + +int APS5_VABI sceUrlConfigResolverGetReleaseVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUrlConfigResolverGetSysVer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUrlConfigResolverIsUpdated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUrlConfigResolverUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceUsbStorage/Export.cpp b/core/libs/prx/libSceUsbStorage/Export.cpp index fb34206c21..96c447c943 100644 --- a/core/libs/prx/libSceUsbStorage/Export.cpp +++ b/core/libs/prx/libSceUsbStorage/Export.cpp @@ -3,8 +3,100 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +struct UsbStorageEvent; +struct UsbStorageDeviceInfo; +using UsbStorageCallback = void (APS5_VABI *)(const UsbStorageEvent* event, void* userData); + extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceUsbStorageInit(void* attr) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageTerm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageRegisterCallback(int32_t eventType, UsbStorageCallback callback, void* arg) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageUnregisterCallback(int32_t eventType, UsbStorageCallback callback) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageRegisterCallbackForMapAvailable(uint32_t device, const char* directory, UsbStorageCallback callback, void* arg) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageUnregisterCallbackForMapAvailable(uint32_t device, const char* directory, UsbStorageCallback callback) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageRegisterCallbackForMapUnavailable(uint32_t device, const char* directory, UsbStorageCallback callback, void* arg) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageUnregisterCallbackForMapUnavailable(uint32_t device, const char* directory, UsbStorageCallback callback) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageGetDeviceInfo(uint32_t device, UsbStorageDeviceInfo* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageGetDeviceList(uint32_t* devices, int32_t* count) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageGetMountPointOfShellCore(uint32_t device, char* mountPoint) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +bool APS5_VABI sceUsbStorageIsExist(uint32_t device, const char* directory, std::uint8_t* exists) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageRequestMap(uint32_t device, const char* directory, int32_t flags, uint64_t volumeSize, char* mountPoint, uint64_t* mountId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageRequestUnmap(uint32_t device, const char* directory) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageGetdentsOpen(uint32_t device, int32_t* fd, uint64_t* bufferSize) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageGetdentsRead(int32_t fd, void* buffer, uint64_t bufferSize, uint64_t* bytesRead) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceUsbStorageGetdentsClose(int32_t fd) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUsbStorageClearFakeWriteProtection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUsbStorageRequestMapWSB(uint32_t device_id, const char* subpath, int32_t flags, + int32_t unknown, char* out_mount_path, int32_t* out_status) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUsbStorageSetFakeMapLockForDebug() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUsbStorageSetFakeWriteProtection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceUsbStorageDialog/Export.cpp b/core/libs/prx/libSceUsbStorageDialog/Export.cpp index fb34206c21..612ac95a57 100644 --- a/core/libs/prx/libSceUsbStorageDialog/Export.cpp +++ b/core/libs/prx/libSceUsbStorageDialog/Export.cpp @@ -1,10 +1,100 @@ +#include #include #include +#include #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +extern "C" int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* users); + +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_INVALID_ARGUMENT = static_cast(0x80B8000Au); +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); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; +std::uint32_t g_sdkVersion = 0; +std::atomic_bool g_closed{false}; + +} + extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceUsbStorageDialogInitialize(void) { + if (g_status.load() != COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + const auto* process = static_cast(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t sdkVersion; + std::memcpy(&sdkVersion, process + 16, sizeof(sdkVersion)); + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + g_sdkVersion = sdkVersion; + g_closed = false; + return 0; +} + +int APS5_VABI sceUsbStorageDialogOpen(const UsbStorageDialogParam* param) { + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (!ValidateCommonDialogBaseParam(param->base)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + UserServiceLoginUserIdList users{}; + if (param->userId == -1 || sceUserServiceGetLoginUserIdList(&users) != 0 || std::find(std::begin(users.user_id), std::end(users.user_id), param->userId) == std::end(users.user_id)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (param->path[1023] != 0 || param->path[0] == 0) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + if (g_sdkVersion >= 0x04500000 && (std::strpbrk(param->path, ",*?\"<>|") != nullptr || std::strstr(param->path, "..") != nullptr)) return static_cast(0x812D0003u); + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + g_closed = false; + g_status = COMMON_DIALOG_STATUS_RUNNING; + return 0; +} + +int APS5_VABI sceUsbStorageDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceUsbStorageDialogUpdateStatus(void) { + int expected = COMMON_DIALOG_STATUS_RUNNING; + g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_FINISHED); + return g_status.load(); +} + +int APS5_VABI sceUsbStorageDialogGetResult(UsbStorageDialogResult* result) { + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_closed.load()) { + result->result = COMMON_DIALOG_ERROR_NOT_FINISHED; + result->status = 0; + return 0; + } + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + if (g_sdkVersion >= 0x04500000 && (result->size != sizeof(*result) || result->result != COMMON_DIALOG_ERROR_BUSY)) return COMMON_DIALOG_ERROR_INVALID_ARGUMENT; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + return COMMON_DIALOG_RESULT_USER_CANCELED; +} + +int APS5_VABI sceUsbStorageDialogClose(void) { + 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_NOT_RUNNING; + g_closed = true; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; +} + +int APS5_VABI sceUsbStorageDialogTerminate(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/libSceUsbd/Export.cpp b/core/libs/prx/libSceUsbd/Export.cpp index 9518092966..6d791a3166 100644 --- a/core/libs/prx/libSceUsbd/Export.cpp +++ b/core/libs/prx/libSceUsbd/Export.cpp @@ -534,4 +534,105 @@ void APS5_VABI sceUsbdUnrefDevice(void* device) { libusb_unref_device(host); } +std::int32_t APS5_VABI sceUsbdClearHalt(void* deviceHandle, std::uint8_t endpoint) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int32_t APS5_VABI sceUsbdDetachKernelDriver(void* deviceHandle, int interfaceNumber) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int32_t APS5_VABI sceUsbdEventHandlerActive() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int32_t APS5_VABI sceUsbdGetConfigDescriptorByValue(void* device, std::uint8_t configurationValue, void** config) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int32_t APS5_VABI sceUsbdGetConfiguration(void* deviceHandle, std::int32_t* configuration) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int32_t APS5_VABI sceUsbdGetDescriptor(void* deviceHandle, std::uint8_t descriptorType, std::uint8_t descriptorIndex, std::uint8_t* data, std::int32_t length) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI sceUsbdGetDevice(void* deviceHandle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUsbdGetDeviceSpeed(void* device) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int32_t APS5_VABI sceUsbdGetMaxIsoPacketSize(void* device, std::uint8_t endpoint) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int32_t APS5_VABI sceUsbdGetMaxPacketSize(void* device, std::uint8_t endpoint) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int32_t APS5_VABI sceUsbdGetStringDescriptorAscii(void* deviceHandle, std::uint8_t descriptorIndex, std::uint8_t* data, std::int32_t length) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int32_t APS5_VABI sceUsbdHandleEvents() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int32_t APS5_VABI sceUsbdHandleEventsLocked(UsbdTimeval* timeout) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI sceUsbdLockEventWaiters() { + NotImplemented_nid_no_patch(__func__); +} + +void APS5_VABI sceUsbdLockEvents() { + NotImplemented_nid_no_patch(__func__); +} + +void* APS5_VABI sceUsbdOpenDeviceWithVidPid(std::uint16_t vendorId, std::uint16_t productId) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int32_t APS5_VABI sceUsbdSetInterfaceAltSetting(void* deviceHandle, int interfaceNumber, int alternateSetting) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int32_t APS5_VABI sceUsbdTryLockEvents() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI sceUsbdUnlockEventWaiters() { + NotImplemented_nid_no_patch(__func__); +} + +void APS5_VABI sceUsbdUnlockEvents() { + NotImplemented_nid_no_patch(__func__); +} + +std::int32_t APS5_VABI sceUsbdWaitForEvent(UsbdTimeval* timeout) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceUserService/CMakeLists.txt b/core/libs/prx/libSceUserService/CMakeLists.txt index ac0ab70e9a..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 @@ -23,3 +24,14 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_user_service_lifecycle_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserServiceLifecycle.cpp) + target_include_directories(guest_user_service_lifecycle_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_user_service_lifecycle_tests PRIVATE libSceUserService) + add_test(NAME guest_user_service_lifecycle COMMAND guest_user_service_lifecycle_tests) + if(WIN32) + set_tests_properties(guest_user_service_lifecycle PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 4996849ae0..a12d5c3263 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -6,6 +6,9 @@ #include #include "prx/libSceUserService/UserService.hpp" +static std::atomic initialized{false}; +static std::atomic loginReported{false}; + static int ReportAccessibilitySetting(int user_id, int32_t* setting, int32_t value) { if (user_id != USER_SERVICE_INITIAL_USER_ID) { return USER_SERVICE_ERROR_NOT_LOGGED_IN; @@ -23,6 +26,11 @@ int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, int32 return ReportAccessibilitySetting(user_id, chat_transcription, 0); } +int APS5_VABI sceUserServiceGetAccessibilityKeyremapEnable(std::int32_t userId, std::int32_t* enabled) { + 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); } @@ -35,6 +43,11 @@ int APS5_VABI sceUserServiceGetAccessibilityVibration(int user_id, int32_t* vibr return ReportAccessibilitySetting(user_id, vibration, 1); } +int APS5_VABI sceUserServiceGetAccessibilityZoom(std::int32_t userId, std::int32_t* zoom) { + 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); } @@ -55,12 +68,14 @@ int APS5_VABI sceUserServiceGetAgeLevel(int user_id, uint32_t* age_level) { return USER_SERVICE_OK; } -// The initial user is reported as logging in once; afterwards there are no user events. +// The initial user is reported as logging in once per initialization; afterwards there are no user events. int APS5_VABI sceUserServiceGetEvent(SceUserServiceEvent* event) { + if (!initialized.load()) { + return USER_SERVICE_ERROR_NOT_INITIALIZED; + } if (event == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } - static std::atomic loginReported{false}; if (loginReported.exchange(true)) { return USER_SERVICE_ERROR_NO_EVENT; } @@ -97,6 +112,9 @@ int APS5_VABI sceUserServiceGetInitialUser(int* user_id) { } int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* user_id_list) { + if (!initialized.load()) { + return USER_SERVICE_ERROR_NOT_INITIALIZED; + } if (user_id_list == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } @@ -130,16 +148,38 @@ int APS5_VABI sceUserServiceGetUserNumber(int user_id, int32_t* number) { return USER_SERVICE_OK; } -int APS5_VABI sceUserServiceInitialize(const void* params) { - (void)params; +static bool ValidThreadPriority(int priority) { + return priority >= USER_SERVICE_THREAD_PRIORITY_MIN && priority <= USER_SERVICE_THREAD_PRIORITY_MAX; +} + +static int MarkInitialized() { + if (initialized.exchange(true)) { + return USER_SERVICE_ERROR_ALREADY_INITIALIZED; + } + loginReported.store(false); return USER_SERVICE_OK; } -int APS5_VABI sceUserServiceInitialize2(void) { - return USER_SERVICE_OK; +int APS5_VABI sceUserServiceInitialize(const UserServiceInitParam* params) { + if (params != nullptr) { + if (!ValidThreadPriority(params->priority)) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + } + return MarkInitialized(); +} + +int APS5_VABI sceUserServiceInitialize2(std::int32_t priority, KernelCpumask affinityMask) { + if (!ValidThreadPriority(priority)) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + return MarkInitialized(); } int APS5_VABI sceUserServiceTerminate(void) { + if (!initialized.exchange(false)) { + return USER_SERVICE_ERROR_NOT_INITIALIZED; + } return USER_SERVICE_OK; } diff --git a/core/libs/prx/libSceUserService/UserService.hpp b/core/libs/prx/libSceUserService/UserService.hpp index 342b82052c..eeb192f8aa 100644 --- a/core/libs/prx/libSceUserService/UserService.hpp +++ b/core/libs/prx/libSceUserService/UserService.hpp @@ -4,12 +4,22 @@ #include #include +struct UserServiceInitParam { + std::int32_t priority; +}; + +static_assert(sizeof(UserServiceInitParam) == 4); + constexpr int USER_SERVICE_OK = 0; +constexpr int USER_SERVICE_ERROR_NOT_INITIALIZED = static_cast(0x80960002u); +constexpr int USER_SERVICE_ERROR_ALREADY_INITIALIZED = static_cast(0x80960003u); constexpr int USER_SERVICE_ERROR_INVALID_ARGUMENT = -2137653243; constexpr int USER_SERVICE_ERROR_NO_EVENT = -2137653241; constexpr int USER_SERVICE_ERROR_NOT_LOGGED_IN = -2137653239; constexpr int USER_SERVICE_USER_ID_INVALID = -1; constexpr int USER_SERVICE_INITIAL_USER_ID = 0x10000000; +constexpr int USER_SERVICE_THREAD_PRIORITY_MIN = 0x100; +constexpr int USER_SERVICE_THREAD_PRIORITY_MAX = 0x2FF; constexpr int USER_SERVICE_ERROR_BUFFER_TOO_SHORT = -2137653238; constexpr int USER_SERVICE_ERROR_UNKNOWN_USER = static_cast(0x80960105u); constexpr std::size_t USER_SERVICE_MAX_USER_NAME_LENGTH = 16; diff --git a/core/libs/prx/libSceVcodec/Export.cpp b/core/libs/prx/libSceVcodec/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceVcodec/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceVdecCore.native/Export.cpp b/core/libs/prx/libSceVdecCore.native/Export.cpp new file mode 100644 index 0000000000..f619cb820b --- /dev/null +++ b/core/libs/prx/libSceVdecCore.native/Export.cpp @@ -0,0 +1,28 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVdecCoreCreateDecoderBid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreCreateDecoderSvp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreMapMemory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreSetDecodeInputSvp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceVdecCore/Export.cpp b/core/libs/prx/libSceVdecCore/Export.cpp new file mode 100644 index 0000000000..343d1ec6a6 --- /dev/null +++ b/core/libs/prx/libSceVdecCore/Export.cpp @@ -0,0 +1,108 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVdecCoreAnalyzeStream() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreCreateDecoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreDeleteDecoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreFinalizeComputeResource() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreFlushDecodeOutput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreGetDecodeOutput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreInitializeComputeResource() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreMapMemoryBlock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreQueryComputeResourceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreQueryFrameBufferInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreQueryInstanceSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreResetDecoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreSetDecodeInput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreSetDecodeOutputSw() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreSyncDecode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreSyncDecodeOutputSw() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreSyncDecodeWptr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreTrySyncDecode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreTrySyncDecodeOutputSw() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecCoreTrySyncDecodeWptr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceVdecSavc/Export.cpp b/core/libs/prx/libSceVdecSavc/Export.cpp new file mode 100644 index 0000000000..c30e4f9c37 --- /dev/null +++ b/core/libs/prx/libSceVdecSavc/Export.cpp @@ -0,0 +1,28 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceSdecCreateSw() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdecDestroySw() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdecGetVersionSw() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdecQueryMemorySw() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceVdecSavc2.native/Export.cpp b/core/libs/prx/libSceVdecSavc2.native/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceVdecSavc2.native/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceVdecSavc2/Export.cpp b/core/libs/prx/libSceVdecSavc2/Export.cpp new file mode 100644 index 0000000000..6f217fc959 --- /dev/null +++ b/core/libs/prx/libSceVdecSavc2/Export.cpp @@ -0,0 +1,28 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceSdecCreateSw2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdecDestroySw2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdecGetVersionSw2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdecQueryMemorySw2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceVdecShevc.native/Export.cpp b/core/libs/prx/libSceVdecShevc.native/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceVdecShevc.native/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceVdecShevc/Export.cpp b/core/libs/prx/libSceVdecShevc/Export.cpp new file mode 100644 index 0000000000..8f63aa61a9 --- /dev/null +++ b/core/libs/prx/libSceVdecShevc/Export.cpp @@ -0,0 +1,43 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceSdecCreateSwHevc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdecDestroySwHevc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdecExecuteSwHevc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdecGetFrameSwHevc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdecGetVersionSwHevc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdecQueryMemorySwHevc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdecResetSwHevc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceVdecSvp9.native/Export.cpp b/core/libs/prx/libSceVdecSvp9.native/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceVdecSvp9.native/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceVdecsw/Export.cpp b/core/libs/prx/libSceVdecsw/Export.cpp index d56d38b16b..d6fa384070 100644 --- a/core/libs/prx/libSceVdecsw/Export.cpp +++ b/core/libs/prx/libSceVdecsw/Export.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -16,7 +18,10 @@ namespace { constexpr int VdecswErrorOutputPending = static_cast(0x81510115u); constexpr int VdecswErrorInputQueueEmpty = static_cast(0x81510116u); constexpr int VdecswErrorDecodePending = static_cast(0x81510117u); +constexpr std::uint32_t CodecVp9 = 2382845; constexpr std::size_t MaxQueuedPictures = 4; +constexpr std::uint32_t CodecAvc = 1; +constexpr std::uint32_t CodecHevc = 974921; struct InputData { std::uint64_t thisSize; @@ -57,6 +62,26 @@ struct OutputInfo { std::uint32_t framePitchInBytes; }; +struct Vp9PictureInfo { + std::uint64_t thisSize; + bool isValid; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; + std::uint32_t profile; + std::uint32_t level; + std::uint32_t keyFrameFlag; + std::uint32_t intraOnly; + std::uint8_t bitDepth; + std::uint8_t colorRange; + std::uint32_t colorSpace; + std::uint32_t numFrames; + std::uint32_t frameWidth; + std::uint32_t frameHeight; + std::uint32_t renderWidth; + std::uint32_t renderHeight; +}; + struct AvcPictureInfo { std::uint64_t thisSize; bool isValid; @@ -77,9 +102,55 @@ struct AvcPictureInfo { std::uint8_t vui[44]; }; +struct HevcPictureInfo { + std::uint64_t thisSize; + bool isValid; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; + std::uint32_t picWidthInLumaSamples; + std::uint32_t picHeightInLumaSamples; + std::uint8_t bitDepthLumaMinus8; + std::uint8_t bitDepthChromaMinus8; + std::uint8_t timingInfoPresentFlag; + std::uint32_t numUnitsInTick; + std::uint32_t timeScale; + std::uint32_t aspectRatioInfoPresentFlag; + std::uint8_t aspectRatioIdc; + std::uint16_t sarWidth; + std::uint16_t sarHeight; + std::uint8_t videoSignal[8]; + std::uint32_t picStruct; + std::uint32_t sourceScanType; + std::uint32_t duplicateFlag; + std::uint32_t conformanceWindowFlag; + std::uint32_t confWinLeftOffset; + std::uint32_t confWinRightOffset; + std::uint32_t confWinTopOffset; + std::uint32_t confWinBottomOffset; + std::uint32_t defaultDisplayWindow[5]; + std::uint8_t chromaLocation[4]; + std::uint8_t presentFlags[11]; + std::uint8_t idrPictureFlag; + std::uint8_t irapPictureFlag; + std::uint8_t generalProfileSpace; + std::uint8_t generalTierFlag; + std::uint8_t generalProfileIdc; + std::uint8_t generalSourceFlags[3]; + std::uint8_t generalLevelIdc; + std::uint8_t subLayer[9]; + std::uint8_t frameCroppingFlag; + std::uint32_t frameCropLeftOffset; + std::uint32_t frameCropRightOffset; + std::uint32_t frameCropTopOffset; + std::uint32_t frameCropBottomOffset; +}; + static_assert(sizeof(InputData) == 0x30 && sizeof(InputResult) == 0x18 && sizeof(FrameBuffer) == 0x18); +static_assert(sizeof(Vp9PictureInfo) == 0x58 && offsetof(Vp9PictureInfo, ptsData) == 0x10 && offsetof(Vp9PictureInfo, profile) == 0x28 && offsetof(Vp9PictureInfo, bitDepth) == 0x38 && offsetof(Vp9PictureInfo, colorSpace) == 0x3c && offsetof(Vp9PictureInfo, renderHeight) == 0x50); static_assert(sizeof(OutputInfo) == 0x38 && offsetof(OutputInfo, isLastFrame) == 9 && offsetof(OutputInfo, pictureCount) == 0xb && offsetof(OutputInfo, isDiscardedFrame) == 0x1c); static_assert(sizeof(AvcPictureInfo) == 0x78 && offsetof(AvcPictureInfo, picWidthInMbsMinus1) == 0x2c && offsetof(AvcPictureInfo, frameCropBottomOffset) == 0x44); +static_assert(sizeof(HevcPictureInfo) == 0xb8 && offsetof(HevcPictureInfo, picWidthInLumaSamples) == 0x28 && offsetof(HevcPictureInfo, sarWidth) == 0x42 && offsetof(HevcPictureInfo, picStruct) == 0x50 && offsetof(HevcPictureInfo, conformanceWindowFlag) == 0x5c && offsetof(HevcPictureInfo, idrPictureFlag) == 0x93 && offsetof(HevcPictureInfo, generalProfileIdc) == 0x97 && offsetof(HevcPictureInfo, generalLevelIdc) == 0x9b && offsetof(HevcPictureInfo, frameCroppingFlag) == 0xa5 && offsetof(HevcPictureInfo, frameCropBottomOffset) == 0xb4); struct Videodec2FrameBuffer { std::uint64_t thisSize; @@ -103,17 +174,23 @@ struct Videodec2OutputInfo { std::uint32_t framePitchInBytes; }; -struct Videodec2AvcPictureInfo { - std::uint64_t thisSize; - bool isValid; - std::uint64_t ptsData; - std::uint64_t dtsData; - std::uint64_t attachedData; - std::uint8_t idrPictureFlag; - std::uint8_t profileIdc; - std::uint8_t levelIdc; - std::uint32_t picWidthInLumaSamples; - std::uint32_t picHeightInLumaSamples; +struct Videodec2Crop { + std::uint32_t left; + std::uint32_t right; + std::uint32_t top; + std::uint32_t bottom; +}; + +struct Videodec2PictureHeader { + std::uint64_t pts; + std::uint64_t dts; + std::uint64_t attached; + bool keyFrame; + bool hevcIdr; + std::uint32_t profile; + std::uint32_t level; + Videodec2Crop crop; + std::uint8_t bitDepthMinus8; }; struct DecoderMemoryInfo { @@ -139,8 +216,9 @@ int APS5_VABI sceVideodec2CreateDecoder_nid_postfix(const void* config, const vo int APS5_VABI sceVideodec2DeleteDecoder_nid_postfix(std::uint64_t handle); int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputData* input, Videodec2FrameBuffer* frame, Videodec2OutputInfo* output); int APS5_VABI sceVideodec2Flush_nid_postfix(std::uint64_t handle, Videodec2FrameBuffer* frame, Videodec2OutputInfo* output); +int APS5_VABI sceVideodec2GetVp9PictureInfo_nid_postfix(const Videodec2OutputInfo* output, Vp9PictureInfo* info); int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t handle); -int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const Videodec2OutputInfo* output, Videodec2AvcPictureInfo* first, Videodec2AvcPictureInfo* second); +void Videodec2PictureHeader_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Videodec2PictureHeader* header); } namespace { @@ -155,10 +233,12 @@ struct ConsumedInput { std::uint32_t frames; }; +using PictureInfo = std::variant; + struct Picture { std::vector pixels; Videodec2OutputInfo output; - AvcPictureInfo info; + PictureInfo info; }; struct Decoder { @@ -168,9 +248,10 @@ struct Decoder { std::deque consumed; std::deque outputs; std::deque pictures; - std::map delivered; + std::map delivered; bool finalized = false; bool drained = false; + std::map deliveredVp9; }; std::mutex decodersMutex; @@ -183,30 +264,91 @@ std::shared_ptr findDecoder(std::uint64_t handle) { return found->second; } -AvcPictureInfo pictureInfo(const Videodec2OutputInfo& output) { - Videodec2AvcPictureInfo source{sizeof(Videodec2AvcPictureInfo)}; - sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &source, nullptr); - if (!source.isValid) throw std::runtime_error("Vdecsw: decoded picture has no picture info"); +PictureInfo pictureInfo(std::uint64_t handle, const Videodec2OutputInfo& output) { + Videodec2PictureHeader header{}; + Videodec2PictureHeader_nid_no_patch(handle, output.frameBuffer, &header); + const auto& crop = header.crop; + const bool cropped = crop.left != 0 || crop.right != 0 || crop.top != 0 || crop.bottom != 0; + if (output.codecType == CodecHevc) { + HevcPictureInfo info{}; + info.thisSize = sizeof(HevcPictureInfo); + info.isValid = true; + info.ptsData = header.pts; + info.dtsData = header.dts; + info.attachedData = header.attached; + info.picWidthInLumaSamples = output.frameWidth; + info.picHeightInLumaSamples = output.frameHeight; + info.bitDepthLumaMinus8 = header.bitDepthMinus8; + info.bitDepthChromaMinus8 = header.bitDepthMinus8; + info.conformanceWindowFlag = cropped; + info.confWinLeftOffset = crop.left / 2u; + info.confWinRightOffset = crop.right / 2u; + info.confWinTopOffset = crop.top / 2u; + info.confWinBottomOffset = crop.bottom / 2u; + info.idrPictureFlag = header.hevcIdr; + info.irapPictureFlag = header.keyFrame; + info.generalProfileIdc = static_cast(header.profile); + info.generalLevelIdc = static_cast(header.level); + info.frameCroppingFlag = cropped; + info.frameCropLeftOffset = info.confWinLeftOffset; + info.frameCropRightOffset = info.confWinRightOffset; + info.frameCropTopOffset = info.confWinTopOffset; + info.frameCropBottomOffset = info.confWinBottomOffset; + return info; + } + if (output.codecType != CodecAvc) throw std::runtime_error("Vdecsw: picture info is only implemented for H.264 and HEVC"); const auto widthInMbs = (output.frameWidth + 15u) / 16u; const auto heightInMbs = (output.frameHeight + 15u) / 16u; AvcPictureInfo info{}; info.thisSize = sizeof(AvcPictureInfo); info.isValid = true; - info.ptsData = source.ptsData; - info.dtsData = source.dtsData; - info.attachedData = source.attachedData; - info.idrPictureFlag = source.idrPictureFlag; - info.profileIdc = source.profileIdc; - info.levelIdc = source.levelIdc; + info.ptsData = header.pts; + info.dtsData = header.dts; + info.attachedData = header.attached; + info.idrPictureFlag = header.keyFrame; + info.profileIdc = static_cast(header.profile); + info.levelIdc = static_cast(header.level); info.picWidthInMbsMinus1 = widthInMbs - 1u; info.picHeightInMapUnitsMinus1 = heightInMbs - 1u; info.frameMbsOnlyFlag = 1; - info.frameCroppingFlag = widthInMbs * 16u != output.frameWidth || heightInMbs * 16u != output.frameHeight; - info.frameCropRightOffset = (widthInMbs * 16u - output.frameWidth) / 2u; - info.frameCropBottomOffset = (heightInMbs * 16u - output.frameHeight) / 2u; + info.frameCroppingFlag = cropped; + info.frameCropLeftOffset = crop.left / 2u; + info.frameCropRightOffset = crop.right / 2u; + info.frameCropTopOffset = crop.top / 2u; + info.frameCropBottomOffset = crop.bottom / 2u; return info; } +Vp9PictureInfo vp9PictureInfo(const Videodec2OutputInfo& output) { + Vp9PictureInfo info{sizeof(Vp9PictureInfo)}; + sceVideodec2GetVp9PictureInfo_nid_postfix(&output, &info); + if (!info.isValid) throw std::runtime_error("Vdecsw: decoded picture has no VP9 picture info"); + return info; +} + +template +int deliveredPictureInfo(const OutputInfo* output, TInfo* info) { + if (output == nullptr || info == nullptr || info->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"); + if (output->pictureCount == 0) return 0; + std::vector> all; + { + std::lock_guard lock(decodersMutex); + for (const auto& [handle, decoder] : decoders) all.push_back(decoder); + } + for (const auto& decoder : all) { + std::lock_guard lock(decoder->mutex); + const auto found = decoder->delivered.find(output->frameBuffer); + if (found == decoder->delivered.end()) continue; + const auto* source = std::get_if(&found->second); + if (source == nullptr) throw std::runtime_error("Vdecsw: picture info requested for another codec"); + const auto bytes = std::min(info->thisSize, sizeof(TInfo)) - sizeof(std::uint64_t); + std::memcpy(reinterpret_cast(info) + sizeof(std::uint64_t), reinterpret_cast(source) + sizeof(std::uint64_t), bytes); + return 0; + } + throw std::runtime_error("Vdecsw: picture info requested for a frame buffer no decoder returned"); +} + void advance(std::uint64_t handle, Decoder& decoder) { while (decoder.pictures.size() < MaxQueuedPictures) { std::vector pixels(decoder.frameBytes); @@ -226,7 +368,7 @@ void advance(std::uint64_t handle, Decoder& decoder) { return; } if (output.isValid) { - const auto info = pictureInfo(output); + const auto info = output.codecType == CodecVp9 ? PictureInfo{} : pictureInfo(handle, output); decoder.pictures.push_back({std::move(pixels), output, info}); } } @@ -299,6 +441,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); @@ -340,15 +490,40 @@ int APS5_VABI sceVdecswTrySyncDecodeOutput(std::uint64_t handle, OutputInfo* out output->frameBufferSize = picture.output.frameBufferSize; output->frameFormat = picture.output.frameFormat; output->framePitchInBytes = picture.output.framePitchInBytes; - decoder->delivered[target.frameBuffer] = picture.info; + if (picture.output.codecType == CodecVp9) decoder->deliveredVp9[target.frameBuffer] = vp9PictureInfo(picture.output); + else decoder->delivered[target.frameBuffer] = picture.info; + 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 sceVdecswGetPictureInfo(const OutputInfo* output, void* first, void* second) { + (void)output; + (void)first; + (void)second; + NotImplemented_nid_no_patch(__func__); return 0; } 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"); - if (output->pictureCount == 0) return 0; static_cast(second); + return deliveredPictureInfo(output, first); +} + +int APS5_VABI sceVdecswGetHevcPictureInfo(const OutputInfo* output, HevcPictureInfo* picture) { + return deliveredPictureInfo(output, picture); +} + +int APS5_VABI sceVdecswGetVp9PictureInfo(const OutputInfo* output, Vp9PictureInfo* picture) { + if (output == nullptr || picture == nullptr || picture->thisSize < sizeof(std::uint64_t)) throw std::runtime_error("Vdecsw: invalid argument pointer"); + if (output->pictureCount > 1) throw std::runtime_error("Vdecsw: more than one VP9 picture info is not implemented"); + if (output->pictureCount == 0) return 0; std::vector> all; { std::lock_guard lock(decodersMutex); @@ -356,13 +531,13 @@ int APS5_VABI sceVdecswGetAvcPictureInfo(const OutputInfo* output, AvcPictureInf } for (const auto& decoder : all) { std::lock_guard lock(decoder->mutex); - const auto found = decoder->delivered.find(output->frameBuffer); - if (found == decoder->delivered.end()) continue; - const auto bytes = std::min(first->thisSize, sizeof(AvcPictureInfo)) - sizeof(std::uint64_t); - std::memcpy(reinterpret_cast(first) + sizeof(std::uint64_t), reinterpret_cast(&found->second) + sizeof(std::uint64_t), bytes); + const auto found = decoder->deliveredVp9.find(output->frameBuffer); + if (found == decoder->deliveredVp9.end()) continue; + const auto bytes = std::min(picture->thisSize, sizeof(Vp9PictureInfo)) - sizeof(std::uint64_t); + std::memcpy(reinterpret_cast(picture) + sizeof(std::uint64_t), reinterpret_cast(&found->second) + sizeof(std::uint64_t), bytes); return 0; } - throw std::runtime_error("Vdecsw: picture info requested for a frame buffer no decoder returned"); + throw std::runtime_error("Vdecsw: VP9 picture info requested for a frame buffer no decoder returned"); } int APS5_VABI sceVdecswFinalizeDecodeSequence(std::uint64_t handle) { @@ -381,6 +556,7 @@ int APS5_VABI sceVdecswResetDecoder(std::uint64_t handle) { decoder->outputs.clear(); decoder->pictures.clear(); decoder->delivered.clear(); + decoder->deliveredVp9.clear(); decoder->finalized = false; decoder->drained = false; return sceVideodec2Reset_nid_postfix(handle); diff --git a/core/libs/prx/libSceVdecwrap/Export.cpp b/core/libs/prx/libSceVdecwrap/Export.cpp new file mode 100644 index 0000000000..ae14644219 --- /dev/null +++ b/core/libs/prx/libSceVdecwrap/Export.cpp @@ -0,0 +1,63 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVdecwrapCreateDecoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecwrapDeleteDecoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecwrapFlushDecodeOutput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecwrapGetDecodeOutput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecwrapMapDirectMemory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecwrapMapMemory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecwrapQueryDecoderMemoryInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecwrapQueryFrameBufferInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecwrapResetDecoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecwrapSetDecodeInput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecwrapSyncDecode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceVenc/Export.cpp b/core/libs/prx/libSceVenc/Export.cpp new file mode 100644 index 0000000000..8d8de5aa65 --- /dev/null +++ b/core/libs/prx/libSceVenc/Export.cpp @@ -0,0 +1,78 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVencCreateEncoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencDeleteEncoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencGetConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencMapMemory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencQueryHeader() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencQueryMemorySize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencQueryPreset() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencSetConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencSetDecodedFrame() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencSetPictureConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencSetRateControlConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencSetReferenceFrameInvalidationConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencStartEncode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVencStopEncode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceVideoArbitration/Export.cpp b/core/libs/prx/libSceVideoArbitration/Export.cpp new file mode 100644 index 0000000000..2825147db8 --- /dev/null +++ b/core/libs/prx/libSceVideoArbitration/Export.cpp @@ -0,0 +1,23 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVideoArbitrationAcceptEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoArbitrationEnable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoArbitrationInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceVideoCoreInterface/Export.cpp b/core/libs/prx/libSceVideoCoreInterface/Export.cpp new file mode 100644 index 0000000000..69f5eb3f02 --- /dev/null +++ b/core/libs/prx/libSceVideoCoreInterface/Export.cpp @@ -0,0 +1,29 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVideoCoreIfSetFunctionTableUnlimited() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + + +int APS5_VABI sceVideoCoreIfInitializeInterface() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreIfMainLoop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreIfSetFunctionTable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceVideoCoreServerInterface/Export.cpp b/core/libs/prx/libSceVideoCoreServerInterface/Export.cpp new file mode 100644 index 0000000000..37c16803eb --- /dev/null +++ b/core/libs/prx/libSceVideoCoreServerInterface/Export.cpp @@ -0,0 +1,243 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVideoCoreAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreAcquireDecoderResource() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreAcquireDecoderResourceForBeWrapper() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreAddStream() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreAsyncAddStream() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreAsyncSendRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreCancelThumbnail() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreChangeActiveStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreChangeAudioStream() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreChangeDisplaySize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreChangeHdcpSetting() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreChangeSubtitle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreDeleteStream() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreGenerateThumbnail() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreGenerateThumbnail2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreGetCanvasHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreGetPlayStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreGetPlayStatus2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreGetStreamUid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreGetThumbnail() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreGetThumbnail2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreLaunchVideoCore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreMediaSourceAbortSourceBuffer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreMediaSourceAddSourceBuffer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreMediaSourceAppendData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreMediaSourceEndOfStream() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreMediaSourceGetBufferedTimeRanges() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreMediaSourceGetInputBufferEmptySize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreMediaSourceGetPlaybackQuality() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreMediaSourceGetSourceBufferEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreMediaSourceRemoveSourceBuffer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreMediaSourceSetDuration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreMediaSourceSetTimestampOffset() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCorePlayControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreRegistEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreReleaseDecoderResource() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreReleaseDecoderResourceForBeWrapper() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreSetAudioMute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreSetAudioVolume() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreSetFullScreen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreSetInitialInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreSetInitializeInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreSetPlaybackRange() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreSetThumbnailInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreSyncAddStream() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreSyncSendRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreUnregistEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceVideoDecoderArbitration/Export.cpp b/core/libs/prx/libSceVideoDecoderArbitration/Export.cpp new file mode 100644 index 0000000000..4e2ab88329 --- /dev/null +++ b/core/libs/prx/libSceVideoDecoderArbitration/Export.cpp @@ -0,0 +1,28 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVideoDecoderArbitrationAcceptEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoDecoderArbitrationEnable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoDecoderArbitrationEnableSuspendMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoDecoderArbitrationInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceVideoNativeExtEssential/Export.cpp b/core/libs/prx/libSceVideoNativeExtEssential/Export.cpp new file mode 100644 index 0000000000..2c3047328f --- /dev/null +++ b/core/libs/prx/libSceVideoNativeExtEssential/Export.cpp @@ -0,0 +1,33 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVideoNativeExtEssential_CreateDecInstance() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoNativeExtEssential_DestroyDecInstance() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoNativeExtEssential_Finalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoNativeExtEssential_Initialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoNativeExtEssential_IsInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 0bfa4cdd06..dee987ce6e 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -6,11 +6,13 @@ add_library(libSceVideoOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/VideoOutDriver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/DisplayWindow.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/PadInput.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Telemetry.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/MouseInput.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/KeyboardInput.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ImeTextInput.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/InputMapping.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/KeyboardMouse.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/HostHdrProbe.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libkernel/Time/TimedWait.cpp ) @@ -18,10 +20,12 @@ target_include_directories(libSceVideoOut PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${CMAKE_BINARY_DIR}/core/libs/prx/libSceAgcDriver/generated ) +add_dependencies(libSceVideoOut agc_trace_revision) target_link_libraries(libSceVideoOut PRIVATE libSceAgcDriver libScePad libSceMouse libSceKeyboard libSceIme libc libkernel decoder_png SDL2-static) if(WIN32) - target_link_libraries(libSceVideoOut PRIVATE comctl32) + target_link_libraries(libSceVideoOut PRIVATE comctl32 dxgi) endif() set_target_properties(libSceVideoOut PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(libSceVideoOut) @@ -70,11 +74,35 @@ if(BUILD_TESTING) set_tests_properties(guest_video_out_high_frame_rate PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - set(ANYPS5_VIDEO_OUT_TEST_DRIVER "offscreen" CACHE STRING + add_test_executable(guest_video_out_cursor_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutCursor.cpp) + target_include_directories(guest_video_out_cursor_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_video_out_cursor_tests PRIVATE libSceVideoOut libkernel libc) + configure_windows_unwind(guest_video_out_cursor_tests) + add_test(NAME guest_video_out_cursor COMMAND guest_video_out_cursor_tests) + set_tests_properties(guest_video_out_cursor PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_video_out_cursor PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + if(WIN32) + set(videoOutTestDriver "windows") + else() + set(videoOutTestDriver "offscreen") + endif() + set(ANYPS5_VIDEO_OUT_TEST_DRIVER "${videoOutTestDriver}" CACHE STRING "SDL video driver for VideoOut flip tests") + foreach(flipMode IN ITEMS labels labels_main) + add_test(NAME video_out_flip_${flipMode} COMMAND video_out_flip_tests ${flipMode}) + set_tests_properties(video_out_flip_${flipMode} PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER}") + if(WIN32) + set_tests_properties(video_out_flip_${flipMode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + 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() @@ -86,6 +114,20 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + foreach(flipMode IN ITEMS eop eop_main eop_aftergpu cpuflip) + add_test(NAME video_out_flip_${flipMode} COMMAND video_out_flip_tests ${flipMode}) + set_tests_properties(video_out_flip_${flipMode} PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER}") + if(WIN32) + set_tests_properties(video_out_flip_${flipMode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test(NAME video_out_flip_cpuflip_in_order COMMAND video_out_flip_tests cpuflip) + set_tests_properties(video_out_flip_cpuflip_in_order PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER};APS5_CPU_FLIP_IN_ORDER=1") + if(WIN32) + set_tests_properties(video_out_flip_cpuflip_in_order PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(BUILD_TESTING AND NOT WIN32) @@ -96,6 +138,9 @@ if(BUILD_TESTING AND NOT WIN32) add_test(NAME video_out_frame_dump_queue COMMAND video_out_frame_dump_queue_tests) set_tests_properties(video_out_frame_dump_queue PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER};APS5_DUMP_FRAMES=4;APS5_DUMP_SCALE=4") + add_test(NAME video_out_frame_dump_wait COMMAND video_out_frame_dump_queue_tests wait) + set_tests_properties(video_out_frame_dump_wait PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 + ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER};APS5_DUMP_FRAMES=4;APS5_DUMP_SCALE=1") endif() if(BUILD_TESTING) @@ -112,6 +157,15 @@ if(BUILD_TESTING) set_tests_properties(pad_input_mapping PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(pad_output_pacing_tests tests/PadOutputPacing.cpp) + target_include_directories(pad_output_pacing_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(pad_output_pacing_tests PRIVATE libc) + configure_windows_unwind(pad_output_pacing_tests) + add_test(NAME pad_output_pacing COMMAND pad_output_pacing_tests) + if(WIN32) + set_tests_properties(pad_output_pacing PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(BUILD_TESTING) @@ -166,6 +220,57 @@ 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) + add_test_executable(guest_video_out_hdr_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutHdr.cpp) + target_include_directories(guest_video_out_hdr_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_video_out_hdr_tests PRIVATE libSceVideoOut libSceSystemService libSceAgcDriver libkernel libc) + configure_windows_unwind(guest_video_out_hdr_tests) + add_test(NAME guest_video_out_hdr COMMAND guest_video_out_hdr_tests) + set_tests_properties(guest_video_out_hdr PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0) + if(WIN32) + set_tests_properties(guest_video_out_hdr PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(video_out_hdr_probe_tests tests/HdrProbe.cpp) + target_include_directories(video_out_hdr_probe_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(video_out_hdr_probe_tests PRIVATE libSceVideoOut libSceAgcDriver libc SDL2-static) + if(WIN32) + target_link_libraries(video_out_hdr_probe_tests PRIVATE dxgi) + endif() + set_target_properties(video_out_hdr_probe_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(video_out_hdr_probe_tests) + add_test(NAME video_out_hdr_probe COMMAND video_out_hdr_probe_tests) + set_tests_properties(video_out_hdr_probe PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77 ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER}") + if(WIN32) + set_tests_properties(video_out_hdr_probe PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(video_out_hdr_present_tests tests/HdrPresent.cpp) + target_include_directories(video_out_hdr_present_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) + target_link_libraries(video_out_hdr_present_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc) + set_target_properties(video_out_hdr_present_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(video_out_hdr_present_tests) + add_test(NAME video_out_hdr_present COMMAND video_out_hdr_present_tests) + set_tests_properties(video_out_hdr_present PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0) + if(WIN32) + set_tests_properties(video_out_hdr_present PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(BUILD_TESTING) @@ -179,3 +284,14 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING AND NOT WIN32) + add_test_executable(video_out_telemetry_tests tests/Telemetry.cpp) + target_include_directories(video_out_telemetry_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(video_out_telemetry_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc SDL2-static) + set_target_properties(video_out_telemetry_tests PROPERTIES CXX_EXTENSIONS OFF) + add_test(NAME video_out_telemetry COMMAND video_out_telemetry_tests) + add_test(NAME video_out_telemetry_caption COMMAND video_out_telemetry_tests caption) + set_tests_properties(video_out_telemetry PROPERTIES TIMEOUT 30) + set_tests_properties(video_out_telemetry_caption PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER}") +endif() diff --git a/core/libs/prx/libSceVideoOut/Unimplemented.cpp b/core/libs/prx/libSceVideoOut/Unimplemented.cpp index ef58c55add..91b75fbb31 100644 --- a/core/libs/prx/libSceVideoOut/Unimplemented.cpp +++ b/core/libs/prx/libSceVideoOut/Unimplemented.cpp @@ -7,12 +7,32 @@ extern "C" { -int APS5_VABI sceVideoOutSetFlipMaster() { +int APS5_VABI sceVideoOutSetFlipMaster(std::int32_t handle, std::int32_t masterHandle) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVideoOutClearFlipMaster() { +int APS5_VABI sceVideoOutClearFlipMaster(std::int32_t handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoOutCursorEnable(int32_t, int32_t, const void*) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoOutCursorDisable(int32_t, int32_t) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoOutCursorSetImageAddress(int32_t, int32_t, const void*) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoOutCursorSetPosition(int32_t, int32_t, uint32_t, uint32_t) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -27,4 +47,25 @@ 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); +} + + +int APS5_VABI sceDbgVideoOutAddOutputModeVr60Privilege() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceVideoOut/include/Buffer.hpp b/core/libs/prx/libSceVideoOut/include/Buffer.hpp index 41a36673a7..5ae8013ae6 100644 --- a/core/libs/prx/libSceVideoOut/include/Buffer.hpp +++ b/core/libs/prx/libSceVideoOut/include/Buffer.hpp @@ -8,12 +8,17 @@ 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); +void APS5_VABI sceVideoOutRemoveBufferAttributeOption(VideoOutBufferAttribute2* attribute, uint64_t option); int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int set_index, const VideoOutBufferAttribute2* attribute, void* option); +int APS5_VABI sceVideoOutSubmitEopFlip(int handle, int index, int flip_mode, int64_t flip_arg, std::uint32_t* token); int APS5_VABI sceVideoOutSubmitFlip(int handle, int index, int flip_mode, int64_t flip_arg); int APS5_VABI sceVideoOutUnregisterBuffers(int handle, int set_index); +int APS5_VABI sceVideoOutGetBufferLabelAddress(int handle, uintptr_t* address); #endif diff --git a/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp b/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp index 18cfc77f12..63ba3f0281 100644 --- a/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp +++ b/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp @@ -20,6 +20,7 @@ public: SDL_Window* Handle() const; void DrawableSize(std::uint32_t& width, std::uint32_t& height) const; void UpdateTitle(); + std::uint64_t FrameNumber() const { return frameNum; } void ToggleFullscreen(); private: @@ -34,6 +35,7 @@ private: SDL_Window* window = nullptr; std::uint32_t aspectWidth = 0; std::uint32_t aspectHeight = 0; + std::uint64_t frameNum = 0; }; #endif diff --git a/core/libs/prx/libSceVideoOut/include/HostHdrProbe.hpp b/core/libs/prx/libSceVideoOut/include/HostHdrProbe.hpp new file mode 100644 index 0000000000..489e9d7102 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/include/HostHdrProbe.hpp @@ -0,0 +1,22 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_INCLUDE_HOSTHDRPROBE_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_INCLUDE_HOSTHDRPROBE_HPP + +#include "SDL.h" + +class HostHdrProbe { +public: + HostHdrProbe() = default; + ~HostHdrProbe(); + HostHdrProbe(const HostHdrProbe&) = delete; + HostHdrProbe& operator=(const HostHdrProbe&) = delete; + + void Poll(SDL_Window* window); + void Release() noexcept; + +private: + void* factory = nullptr; + void* monitor = nullptr; + bool published = false; +}; + +#endif 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/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index de1c799e0e..dfa068d16d 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -5,7 +5,9 @@ #include "SDL_gamecontroller.h" #include "prx/libScePad/include/KeyboardMouse.hpp" #include "prx/libScePad/include/PadState.hpp" +#include "prx/libSceVideoOut/include/PadOutputPacing.hpp" #include +#include "prx/libSceVideoOut/include/Telemetry.hpp" class DisplayWindow; @@ -14,12 +16,12 @@ public: PadInput(); ~PadInput(); void HandleEvent(const SDL_Event& event, DisplayWindow& window); - void Update(); + void Update(const DisplayWindow& window); bool MouseModeEnabled() const { return keyboardMouse.MouseMode(); } private: explicit PadInput(Pad::InputConfiguration configuration); - void publish(); + void publish(std::uint64_t frame); void setMouseMode(bool enabled); void openFirstAvailableController(); void openController(int deviceIndex); @@ -34,7 +36,10 @@ private: std::uint32_t outputSequence = 0; PadOutputState outputState{}; bool outputPending = false; + VideoOutTelemetry::PadChangeFilter telemetryFilter; std::chrono::steady_clock::time_point nextRumbleRefresh{}; + LightBarPacer lightBar; + TriggerEffectCache triggerEffect; }; #endif diff --git a/core/libs/prx/libSceVideoOut/include/PadOutputPacing.hpp b/core/libs/prx/libSceVideoOut/include/PadOutputPacing.hpp new file mode 100644 index 0000000000..6061fcf618 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/include/PadOutputPacing.hpp @@ -0,0 +1,63 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_PADOUTPUTPACING_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_PADOUTPUTPACING_HPP + +#include +#include +#include + +class LightBarPacer { +public: + using Clock = std::chrono::steady_clock; + using Color = std::array; + static constexpr std::chrono::milliseconds Interval{30}; + + bool Pending(const Color& color) const { + return !sent || color != last; + } + + bool Due(const Color& color, Clock::time_point now) const { + return Pending(color) && now >= next; + } + + void Result(const Color& color, Clock::time_point now, bool succeeded) { + next = now + Interval; + if (!succeeded) return; + last = color; + sent = true; + } + + void Reset() { + sent = false; + next = {}; + } + +private: + Color last{}; + bool sent = false; + Clock::time_point next{}; +}; + +class TriggerEffectCache { +public: + using Effect = std::array; + + bool NeedsSend(const Effect& effect) const { + return !sent || effect != last; + } + + void Result(const Effect& effect, bool succeeded) { + if (!succeeded) return; + last = effect; + sent = true; + } + + void Reset() { + sent = false; + } + +private: + Effect last{}; + bool sent = false; +}; + +#endif diff --git a/core/libs/prx/libSceVideoOut/include/Telemetry.hpp b/core/libs/prx/libSceVideoOut/include/Telemetry.hpp new file mode 100644 index 0000000000..186cf20310 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/include/Telemetry.hpp @@ -0,0 +1,74 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_INCLUDE_TELEMETRY_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_INCLUDE_TELEMETRY_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "prx/libScePad/include/PadState.hpp" + +namespace VideoOutTelemetry { + +class BoundedLineQueue { +public: + explicit BoundedLineQueue(std::size_t capacity); + bool TryPush(std::string line); + bool Push(std::string line); + std::deque Drain(); + std::uint64_t Dropped() const; + +private: + bool pushLocked(std::string line); + + std::mutex mutex; + std::deque lines; + std::size_t capacity; + std::atomic dropped{0}; +}; + +class Writer { +public: + Writer(const std::string& directory, std::size_t capacity, const std::string& commit); + ~Writer(); + Writer(const Writer&) = delete; + Writer& operator=(const Writer&) = delete; + + bool EmitPad(std::uint64_t frame, const PadInputState& state); + void EmitPresent(std::uint64_t frame, double fps); + std::uint64_t Dropped() const; + +private: + bool emit(std::string line, bool mustKeep); + void run(std::stop_token token); + void writeAll(const std::string& text); + void writeDrops(bool force); + + std::ofstream output; + BoundedLineQueue queue; + std::uint64_t reportedDrops = 0; + std::jthread thread; +}; + +class PadChangeFilter { +public: + bool Changed(const PadInputState& state) const; + void Commit(const PadInputState& state); + +private: + PadInputState last{}; +}; + +bool EmitPadIfChanged(Writer& writer, PadChangeFilter& filter, std::uint64_t frame, const PadInputState& state); + +std::unique_ptr OpenFromEnvironment(const char* directory, const std::string& commit); +Writer* Instance(); + +} + +#endif diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 8cfcc7299c..7c868f3ce2 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -19,6 +19,7 @@ #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" #include "prx/libSceVideoOut/include/DisplayWindow.hpp" #include "prx/libSceVideoOut/include/BufferReuseTracker.hpp" +#include "prx/libSceVideoOut/include/HostHdrProbe.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" @@ -43,6 +44,7 @@ static constexpr int VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE = -2144796642; static constexpr int VIDEO_OUT_BUS_TYPE_MAIN = 0; static constexpr int VIDEO_OUT_BUS_TYPE_OVERLAY = 1; static constexpr int VIDEO_OUT_BUS_TYPE_SUB = 2; +static constexpr int VIDEO_OUT_BUS_TYPE_HMD2_MAIN = 0x20; static constexpr uint32_t VIDEO_OUT_DEFAULT_WIDTH = 1920; static constexpr uint32_t VIDEO_OUT_DEFAULT_HEIGHT = 1080; @@ -87,6 +89,10 @@ static constexpr int VIDEO_OUT_BUFFER_INDEX_BLANK = -1; static constexpr uint64_t VIDEO_OUT_OUTPUT_MODE_DEFAULT = 0x0000000000000001ULL; static constexpr uint64_t VIDEO_OUT_OUTPUT_MODE_119_88HZ = 0x000000000000000FULL; +static constexpr std::uint32_t VIDEO_OUT_DYNAMIC_RANGE_SDR = 1; +static constexpr std::uint32_t VIDEO_OUT_DYNAMIC_RANGE_HDR = 2; +static constexpr std::uint64_t VIDEO_OUT_OUTPUT_STATUS_FLAG_HDR = 1; + static constexpr uint64_t VIDEO_OUT_REFRESH_RATE_59_94HZ = 3; static constexpr uint64_t VIDEO_OUT_REFRESH_RATE_119_88HZ = 13; @@ -102,6 +108,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); @@ -127,6 +134,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; @@ -140,6 +148,7 @@ struct VideoOutConfig { VideoOutVblankStatus preVblankStatus{}; std::array buffers{}; + alignas(std::atomic_ref::required_alignment) std::array bufferLabels{}; std::array bufferPending{}; std::array bufferReuse; std::array groups{}; @@ -220,6 +229,7 @@ public: // 0, or VIDEO_OUT_ERROR_FLIP_QUEUE_FULL when the title has VIDEO_OUT_FLIP_QUEUE_CAPACITY flips pending. int SubmitFlip(int handle, int index, int flipMode, int64_t flipArg); + int SubmitEopFlip(int handle, int index, int flipMode, int64_t flipArg, std::uint32_t* token); private: bool close(int handle); @@ -229,6 +239,7 @@ private: void updateDisplayRefreshRate(); void processFlip(FlipRequest& req); void triggerEvents(VideoOutConfig& cfg, int eventKind, void* triggerData); + void pollHostHdr(); std::mutex mutex; std::mutex shutdownMutex; @@ -238,6 +249,8 @@ private: std::shared_ptr flipQueue = std::make_shared(); DisplayWindow window; + HostHdrProbe hdrProbe; + std::uint64_t hdrGeneration = 0; std::atomic displayRefreshRate{0}; std::jthread presentThread; diff --git a/core/libs/prx/libSceVideoOut/src/Buffer.cpp b/core/libs/prx/libSceVideoOut/src/Buffer.cpp index 82b62b6c24..7197986ca7 100644 --- a/core/libs/prx/libSceVideoOut/src/Buffer.cpp +++ b/core/libs/prx/libSceVideoOut/src/Buffer.cpp @@ -24,13 +24,79 @@ 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 { + if (address == nullptr) return VIDEO_OUT_ERROR_INVALID_ADDRESS; + auto& driver = VideoOutDriver::Get(); + if (!driver.HasConfig(handle)) return VIDEO_OUT_ERROR_INVALID_HANDLE; + auto cfg = driver.GetConfig(handle); + std::lock_guard lock(cfg->mutex); + cfg->CheckAlive(); + if (cfg->Closed()) return VIDEO_OUT_ERROR_INVALID_HANDLE; + const auto labels = reinterpret_cast(cfg->bufferLabels.data()); + std::memcpy(address, &labels, sizeof(labels)); + return VIDEO_OUT_BUFFER_NUM_MAX; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + void APS5_VABI sceVideoOutSetBufferAttribute2(VideoOutBufferAttribute2* attribute, uint64_t pixelFormat, uint32_t tilingMode, uint32_t width, uint32_t height, uint64_t option, uint32_t dccControl, uint64_t dccCbRegisterClearColor) { if (attribute == nullptr) { throw std::runtime_error("sceVideoOutSetBufferAttribute2: null attribute"); @@ -54,6 +120,13 @@ void APS5_VABI sceVideoOutAddBufferAttributeOption(VideoOutBufferAttribute2* att attribute->option |= option; } +void APS5_VABI sceVideoOutRemoveBufferAttributeOption(VideoOutBufferAttribute2* attribute, uint64_t option) { + if (attribute == nullptr) { + throw std::runtime_error("sceVideoOutRemoveBufferAttributeOption: null attribute"); + } + attribute->option &= ~option; +} + int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int setIndex, int bufferIndexStart, const VideoOutBuffers* buffers, int bufferNum, const VideoOutBufferAttribute2* attribute, int category, void* option) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { @@ -76,40 +149,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]; - } + 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) { @@ -135,7 +233,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; @@ -173,6 +271,13 @@ int APS5_VABI sceVideoOutUnregisterBuffers(int handle, int setIndex) try { LibcAwaitExit_nid_postfix(); } +int APS5_VABI sceVideoOutSubmitEopFlip(int handle, int index, int flipMode, int64_t flipArg, std::uint32_t* token) try { + if (token == nullptr) return VIDEO_OUT_ERROR_INVALID_ADDRESS; + return VideoOutDriver::Get().SubmitEopFlip(handle, index, flipMode, flipArg, token); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + int APS5_VABI sceVideoOutSubmitFlip(int handle, int index, int flipMode, int64_t flipArg) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { diff --git a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp index 5cd47dbc81..35c6c86917 100644 --- a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp +++ b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp @@ -1,10 +1,11 @@ #include "prx/libSceVideoOut/include/DisplayWindow.hpp" +#include "prx/libSceVideoOut/include/Telemetry.hpp" #include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" -#include "prx/libkernel/Time/include/Time.hpp" #include "Decoder/Png.hpp" #include "SDL_vulkan.h" +#include #include #include #include @@ -106,22 +107,22 @@ void DisplayWindow::DrawableSize(std::uint32_t& width, std::uint32_t& height) co void DisplayWindow::UpdateTitle() { require(window != nullptr, "window must exist before updating title"); static const AppTitle title = GetAppTitle_nid_postfix(); - static std::uint64_t fpsStart = sceKernelGetProcessTimeCounter(); - static std::uint64_t frameNum = 0; + static auto fpsStart = std::chrono::steady_clock::now(); static std::uint64_t fpsFrames = 0; static double currentFps = 0.0; - const auto now = sceKernelGetProcessTimeCounter(); - const auto frequency = sceKernelGetProcessTimeCounterFrequency(); + const auto now = std::chrono::steady_clock::now(); frameNum++; fpsFrames++; - if (now - fpsStart >= frequency * 2) { - currentFps = static_cast(fpsFrames) * static_cast(frequency) / static_cast(now - fpsStart); + const double elapsed = std::chrono::duration(now - fpsStart).count(); + if (elapsed >= 2.0) { + currentFps = static_cast(fpsFrames) / elapsed; fpsStart = now; fpsFrames = 0; } char text[160]; std::snprintf(text, sizeof(text), "%s | FPS: %.2f (%llu)", title.value, currentFps, static_cast(frameNum)); SDL_SetWindowTitle(window, text); + if (auto* telemetry = VideoOutTelemetry::Instance(); telemetry != nullptr && (frameNum == 1 || frameNum % 30 == 0)) telemetry->EmitPresent(frameNum, currentFps); } void DisplayWindow::installSubclass() { diff --git a/core/libs/prx/libSceVideoOut/src/HostHdrProbe.cpp b/core/libs/prx/libSceVideoOut/src/HostHdrProbe.cpp new file mode 100644 index 0000000000..b1b562c43b --- /dev/null +++ b/core/libs/prx/libSceVideoOut/src/HostHdrProbe.cpp @@ -0,0 +1,88 @@ +#include "prx/libSceVideoOut/include/HostHdrProbe.hpp" +#include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp" +#include + +#ifdef _WIN32 +#include "SDL_syswm.h" +#include +#include +#endif + +namespace { + +#ifdef _WIN32 +template +void releaseCom(TInterface*& object) noexcept { + if (object != nullptr) object->Release(); + object = nullptr; +} + +AgcDriver::HostDisplayHdr describeMonitor(IDXGIFactory1* factory, HMONITOR monitor) { + for (UINT adapterIndex = 0;; ++adapterIndex) { + IDXGIAdapter1* adapter = nullptr; + if (FAILED(factory->EnumAdapters1(adapterIndex, &adapter))) return {}; + for (UINT outputIndex = 0;; ++outputIndex) { + IDXGIOutput* output = nullptr; + if (FAILED(adapter->EnumOutputs(outputIndex, &output))) break; + DXGI_OUTPUT_DESC description{}; + const bool same = SUCCEEDED(output->GetDesc(&description)) && description.Monitor == monitor; + IDXGIOutput6* output6 = nullptr; + DXGI_OUTPUT_DESC1 description1{}; + const bool described = same && SUCCEEDED(output->QueryInterface(__uuidof(IDXGIOutput6), reinterpret_cast(&output6))) && SUCCEEDED(output6->GetDesc1(&description1)); + releaseCom(output6); + releaseCom(output); + if (!same) continue; + releaseCom(adapter); + if (!described) return {}; + return AgcDriver::DisplayHdrFromOutput(description1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020, description1.MaxLuminance, description1.MinLuminance, description1.MaxFullFrameLuminance); + } + releaseCom(adapter); + } +} +#endif + +} + +HostHdrProbe::~HostHdrProbe() { + Release(); +} + +void HostHdrProbe::Release() noexcept { +#ifdef _WIN32 + auto* dxgiFactory = static_cast(factory); + releaseCom(dxgiFactory); +#endif + factory = nullptr; + monitor = nullptr; +} + +void HostHdrProbe::Poll(SDL_Window* window) { + if (window == nullptr) throw std::runtime_error("VideoOut: the HDR probe needs the window"); +#ifdef _WIN32 + SDL_SysWMinfo info; + SDL_VERSION(&info.version); + if (SDL_GetWindowWMInfo(window, &info) != SDL_TRUE || info.subsystem != SDL_SYSWM_WINDOWS) { + if (!published) AgcDriverSetHostDisplayHdr_nid_postfix({}); + published = true; + return; + } + const HMONITOR current = MonitorFromWindow(info.info.win.window, MONITOR_DEFAULTTONEAREST); + auto* dxgiFactory = static_cast(factory); + if (published && current == monitor && (dxgiFactory == nullptr || dxgiFactory->IsCurrent())) return; + releaseCom(dxgiFactory); + factory = nullptr; + monitor = current; + published = true; + if (FAILED(CreateDXGIFactory1(__uuidof(IDXGIFactory1), reinterpret_cast(&dxgiFactory)))) { + AgcDriverSetHostDisplayHdr_nid_postfix({}); + return; + } + factory = dxgiFactory; + AgcDriverSetHostDisplayHdr_nid_postfix(describeMonitor(dxgiFactory, current)); + return; +#else + if (published) return; + AgcDriverSetHostDisplayHdr_nid_postfix({}); +#endif + published = true; +} diff --git a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp index d974db9e75..dc9bb16da1 100644 --- a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp @@ -51,8 +51,12 @@ constexpr auto actions = std::array{ InputAction{"RightStickDown", Pad::InputControl::RightStickDown, Pad::PadButton::None}, InputAction{"TouchLeft", Pad::InputControl::TouchLeft, Pad::PadButton::None}, InputAction{"TouchRight", Pad::InputControl::TouchRight, Pad::PadButton::None}, + InputAction{"StickModifier", Pad::InputControl::StickModifier, Pad::PadButton::None}, + InputAction{"Shake", Pad::InputControl::Shake, Pad::PadButton::None}, InputAction{"ToggleMouse", Pad::InputControl::ToggleMouse, Pad::PadButton::None}, - InputAction{"ToggleFullscreen", Pad::InputControl::ToggleFullscreen, Pad::PadButton::None} + InputAction{"ToggleFullscreen", Pad::InputControl::ToggleFullscreen, Pad::PadButton::None}, + InputAction{"TogglePerformanceOverlay", Pad::InputControl::TogglePerformanceOverlay, Pad::PadButton::None}, + InputAction{"Screenshot", Pad::InputControl::Screenshot, Pad::PadButton::None} }; std::string upper(std::string_view value) { @@ -111,6 +115,12 @@ Pad::InputBinding parseBinding(const InputAction& action, std::string_view sourc if (action.control == Pad::InputControl::ToggleFullscreen) { throw std::runtime_error("ToggleFullscreen can only use a keyboard key"); } + if (action.control == Pad::InputControl::TogglePerformanceOverlay) { + throw std::runtime_error("TogglePerformanceOverlay can only use a keyboard key"); + } + if (action.control == Pad::InputControl::Screenshot) { + throw std::runtime_error("Screenshot can only use a keyboard key"); + } return {SDL_SCANCODE_UNKNOWN, parseMouseButton(value), action.control, action.button, 0}; } if (type == "WHEEL") { @@ -132,7 +142,10 @@ enum class InputSetting { MouseDeadzoneOffset, MouseMaxSpeed, MouseSmoothingMs, - MouseDecayMs + MouseDecayMs, + StickModifierScale, + KeyboardRampMs, + TriggerRampMs }; struct InputSettingName { @@ -149,7 +162,10 @@ constexpr auto settingNames = std::array{ InputSettingName{"MouseDeadzoneOffset", InputSetting::MouseDeadzoneOffset}, InputSettingName{"MouseMaxSpeed", InputSetting::MouseMaxSpeed}, InputSettingName{"MouseSmoothingMs", InputSetting::MouseSmoothingMs}, - InputSettingName{"MouseDecayMs", InputSetting::MouseDecayMs} + InputSettingName{"MouseDecayMs", InputSetting::MouseDecayMs}, + InputSettingName{"StickModifierScale", InputSetting::StickModifierScale}, + InputSettingName{"KeyboardRampMs", InputSetting::KeyboardRampMs}, + InputSettingName{"TriggerRampMs", InputSetting::TriggerRampMs} }; const InputSettingName* findSetting(std::string_view name) { @@ -207,6 +223,9 @@ void applySetting(Pad::InputSettings& settings, InputSetting setting, std::strin case InputSetting::MouseMaxSpeed: settings.mouseMaxSpeed = parseNumber(value, "mouse max speed", 0.1, 10.0); break; case InputSetting::MouseSmoothingMs: settings.mouseSmoothingMs = parseNumber(value, "mouse smoothing", 0.0, 500.0); break; case InputSetting::MouseDecayMs: settings.mouseDecayMs = parseNumber(value, "mouse decay", 1.0, 1000.0); break; + case InputSetting::StickModifierScale: settings.stickModifierScale = parseNumber(value, "stick modifier scale", 0.05, 1.0); break; + case InputSetting::KeyboardRampMs: settings.keyboardRampMs = parseNumber(value, "keyboard ramp", 0.0, 2000.0); break; + case InputSetting::TriggerRampMs: settings.triggerRampMs = parseNumber(value, "trigger ramp", 0.0, 2000.0); break; } } @@ -214,13 +233,32 @@ std::uint8_t stickByte(std::int32_t axis) { return static_cast(((axis + 32768) * 255 + 32767) / 65535); } +std::filesystem::path pathFromUtf8(std::string_view text) { + return std::filesystem::path(std::u8string(reinterpret_cast(text.data()), text.size())); +} + +std::string pathToUtf8(const std::filesystem::path& path) { + const auto text = path.u8string(); + return std::string(reinterpret_cast(text.data()), text.size()); +} + +std::filesystem::path configuredPath() { +#ifdef _WIN32 + const wchar_t* configured = _wgetenv(L"ANYPS5_INPUT_CONFIG"); + return configured != nullptr ? std::filesystem::path(configured) : std::filesystem::path{}; +#else + const char* configured = std::getenv("ANYPS5_INPUT_CONFIG"); + return configured != nullptr ? pathFromUtf8(configured) : std::filesystem::path{}; +#endif +} + [[noreturn]] void invalidLine(const std::filesystem::path& path, std::size_t line, const std::string& reason) { - throw std::runtime_error("Pad: invalid input mapping " + path.string() + ":" + std::to_string(line) + ": " + reason); + throw std::runtime_error("Pad: invalid input mapping " + pathToUtf8(path) + ":" + std::to_string(line) + ": " + reason); } std::filesystem::path defaultConfigPath() { std::unique_ptr basePath(SDL_GetBasePath(), SDL_free); - if (basePath) return std::filesystem::path(basePath.get()) / "anyps5-input.ini"; + if (basePath) return pathFromUtf8(basePath.get()) / "anyps5-input.ini"; return "anyps5-input.ini"; } @@ -244,13 +282,13 @@ std::array Pad::StickWithDeadzone(std::int16_t x, std::int16_t Pad::InputConfiguration Pad::LoadInputMapping() { InputConfiguration configuration{{InputMapping.begin(), InputMapping.end()}, {}}; auto& bindings = configuration.bindings; - const char* configuredPath = std::getenv("ANYPS5_INPUT_CONFIG"); - const bool explicitPath = configuredPath != nullptr && configuredPath[0] != '\0'; - const std::filesystem::path path = explicitPath ? configuredPath : defaultConfigPath(); + const auto configured = configuredPath(); + const bool explicitPath = !configured.empty(); + const std::filesystem::path path = explicitPath ? configured : defaultConfigPath(); std::ifstream file(path); if (!file) { if (explicitPath || std::filesystem::exists(path)) { - throw std::runtime_error("Pad: cannot read input mapping '" + path.string() + "'"); + throw std::runtime_error("Pad: cannot read input mapping '" + pathToUtf8(path) + "'"); } return configuration; } @@ -301,7 +339,7 @@ Pad::InputConfiguration Pad::LoadInputMapping() { invalidLine(path, lineNumber, error.what()); } } - if (file.bad()) throw std::runtime_error("Pad: cannot read input mapping '" + path.string() + "'"); + if (file.bad()) throw std::runtime_error("Pad: cannot read input mapping '" + pathToUtf8(path) + "'"); return configuration; } diff --git a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp index d8cf0e3d7c..1ef93610d0 100644 --- a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp @@ -4,11 +4,51 @@ #include #include +namespace { + +bool slew(double& value, double target, double travel) { + const double before = value; + value = target > before ? std::min(target, before + travel) : std::max(target, before - travel); + return value != before; +} + +std::uint8_t triggerByte(double value) { + return static_cast(std::lround(std::clamp(value, 0.0, 1.0) * 255.0)); +} + +} + Pad::MouseStick::MouseStick(const InputSettings& settings) : settings(settings) {} void Pad::MouseStick::Move(int deltaX, int deltaY) { pendingX += deltaX; pendingY += deltaY; + pendingCounts += std::hypot(deltaX, deltaY); + ++pendingReports; +} + +void Pad::MouseStick::measureReportRate(double step, bool moved) { + if (moved) { + rateSeconds += std::min(idleSeconds + step, MouseRateGapSeconds); + rateCounts += pendingCounts; + rateReports += pendingReports; + } + pendingCounts = 0.0; + pendingReports = 0; + if (rateSeconds < MouseRateWindowSeconds) return; + if (rateReports >= 10 && rateCounts >= 2.0 * rateReports) reportRate = rateReports / rateSeconds; + rateSeconds = 0.0; + rateCounts = 0.0; + rateReports = 0; +} + +double Pad::MouseStick::ReportRate() const { + return reportRate; +} + +double Pad::MouseStick::SmoothingMs() const { + const double reports = reportRate > 0.0 ? std::min(MouseSmoothingReports * 1000.0 / reportRate, MouseSmoothingLimitMs) : 0.0; + return settings.mouseSmoothingMs > 0.0 ? std::max(settings.mouseSmoothingMs, reports) : 0.0; } void Pad::MouseStick::Advance(double seconds) { @@ -23,13 +63,15 @@ void Pad::MouseStick::Advance(double seconds) { const double targetY = pendingY * scale * settings.mouseVerticalScale; pendingX = 0.0; pendingY = 0.0; + measureReportRate(step, moved); idleSeconds = moved ? 0.0 : idleSeconds + step; if (idleSeconds * 1000.0 >= settings.mouseDecayMs) { speedX = 0.0; speedY = 0.0; return; } - const double keep = settings.mouseSmoothingMs > 0.0 ? std::exp(-step * 1000.0 / settings.mouseSmoothingMs) : 0.0; + const double smoothing = SmoothingMs(); + const double keep = smoothing > 0.0 ? std::exp(-step * 1000.0 / smoothing) : 0.0; speedX = targetX + (speedX - targetX) * keep; speedY = targetY + (speedY - targetY) * keep; const double speed = std::hypot(speedX, speedY); @@ -43,6 +85,11 @@ void Pad::MouseStick::Reset() { pendingX = 0.0; pendingY = 0.0; pendingSeconds = 0.0; + pendingCounts = 0.0; + pendingReports = 0; + rateSeconds = 0.0; + rateCounts = 0.0; + rateReports = 0; speedX = 0.0; speedY = 0.0; idleSeconds = 0.0; @@ -79,6 +126,64 @@ bool Pad::KeyboardMouse::MouseMode() const { return mouseMode; } +Pad::KeyboardMouse::StickInput Pad::KeyboardMouse::stickInput() const { + std::array negative{}; + std::array positive{}; + bool modifier = false; + for (std::size_t index = 0; index < configuration.bindings.size(); ++index) { + if (!pressed[index]) continue; + switch (configuration.bindings[index].control) { + case InputControl::LeftStickLeft: negative[0] = true; break; + case InputControl::LeftStickRight: positive[0] = true; break; + case InputControl::LeftStickUp: negative[1] = true; break; + case InputControl::LeftStickDown: positive[1] = true; break; + case InputControl::RightStickLeft: negative[2] = true; break; + case InputControl::RightStickRight: positive[2] = true; break; + case InputControl::RightStickUp: negative[3] = true; break; + case InputControl::RightStickDown: positive[3] = true; break; + case InputControl::StickModifier: modifier = true; break; + default: break; + } + } + const double scale = modifier ? configuration.settings.stickModifierScale : 1.0; + StickInput input; + for (std::size_t axis = 0; axis < input.targets.size(); ++axis) { + input.targets[axis] = (static_cast(positive[axis]) - static_cast(negative[axis])) * scale; + input.held[axis] = negative[axis] || positive[axis]; + } + return input; +} + +std::array Pad::KeyboardMouse::triggerTargets() const { + std::array targets{}; + for (std::size_t index = 0; index < configuration.bindings.size(); ++index) { + const auto& binding = configuration.bindings[index]; + if (!pressed[index] || binding.control != InputControl::Button) continue; + if (binding.button == PadButton::L2) targets[0] = 1.0; + if (binding.button == PadButton::R2) targets[1] = 1.0; + } + return targets; +} + +bool Pad::KeyboardMouse::advanceAxes(Clock::time_point now) { + const double seconds = axesAdvanced && now > lastAxisAdvance ? std::chrono::duration(now - lastAxisAdvance).count() : 0.0; + if (!axesAdvanced || now > lastAxisAdvance) lastAxisAdvance = now; + axesAdvanced = true; + if (seconds <= 0.0) return false; + bool moved = false; + if (configuration.settings.keyboardRampMs > 0.0) { + const auto targets = stickInput().targets; + const double travel = seconds * 1000.0 / configuration.settings.keyboardRampMs; + for (std::size_t axis = 0; axis < stickAxes.size(); ++axis) moved = slew(stickAxes[axis], targets[axis], travel) || moved; + } + if (configuration.settings.triggerRampMs > 0.0) { + const auto targets = triggerTargets(); + const double travel = seconds * 1000.0 / configuration.settings.triggerRampMs; + for (std::size_t trigger = 0; trigger < triggerAxes.size(); ++trigger) moved = slew(triggerAxes[trigger], targets[trigger], travel) || moved; + } + return moved; +} + bool Pad::KeyboardMouse::Advance(Clock::time_point now) { if (!advanced) { advanced = true; @@ -89,14 +194,15 @@ bool Pad::KeyboardMouse::Advance(Clock::time_point now) { lastAdvance = now; if (mouseMode) mouse.Advance(seconds); } - bool released = false; + bool changed = advanceAxes(now); for (std::size_t index = 0; index < configuration.bindings.size(); ++index) { + if (pressed[index] && configuration.bindings[index].control == InputControl::Shake) changed = true; if (configuration.bindings[index].wheelDirection == 0 || !pressed[index] || now < wheelReleaseTimes[index]) continue; pressed[index] = false; wheelReleaseTimes[index] = {}; - released = true; + changed = true; } - return released; + return changed; } void Pad::KeyboardMouse::SetMouseMode(bool enabled) { @@ -104,9 +210,15 @@ 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{}); + stickAxes = {}; + triggerAxes = {}; } Pad::InputRequests Pad::KeyboardMouse::HandleEvent(const SDL_Event& event, Clock::time_point now) { @@ -118,13 +230,15 @@ 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; + advanceAxes(now); const auto releaseTime = now + std::chrono::milliseconds(WheelPressDurationMs); for (std::size_t index = 0; index < configuration.bindings.size(); ++index) { const auto& binding = configuration.bindings[index]; @@ -138,6 +252,8 @@ 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; + advanceAxes(now); 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]; @@ -146,6 +262,8 @@ Pad::InputRequests Pad::KeyboardMouse::HandleEvent(const SDL_Event& event, Clock : binding.mouseButton != MouseButton::None && binding.mouseButton == static_cast(event.button.button); if (!matches) continue; if (binding.control == InputControl::ToggleFullscreen && keyboard && down && !pressed[index]) requests.toggleFullscreen = true; + if (binding.control == InputControl::TogglePerformanceOverlay && keyboard && down && !pressed[index]) requests.togglePerformanceOverlay = true; + if (binding.control == InputControl::Screenshot && keyboard && down && !pressed[index]) requests.screenshot = true; if (binding.control == InputControl::ToggleMouse && down && !pressed[index]) requests.toggleMouse = true; pressed[index] = down; } @@ -154,39 +272,54 @@ Pad::InputRequests Pad::KeyboardMouse::HandleEvent(const SDL_Event& event, Clock PadInputState Pad::KeyboardMouse::Compose(const PadInputState& controller) const { PadInputState state = controller; - std::array negative{}; - std::array positive{}; for (std::size_t index = 0; index < configuration.bindings.size(); ++index) { if (!pressed[index]) continue; const auto& binding = configuration.bindings[index]; switch (binding.control) { case InputControl::Button: + if (configuration.settings.triggerRampMs > 0.0 && (binding.button == PadButton::L2 || binding.button == PadButton::R2)) break; state.buttons |= static_cast(binding.button); if (binding.button == PadButton::L2) state.analogButtonsL2 = 255; if (binding.button == PadButton::R2) state.analogButtonsR2 = 255; break; - case InputControl::LeftStickLeft: negative[0] = true; break; - case InputControl::LeftStickRight: positive[0] = true; break; - case InputControl::LeftStickUp: negative[1] = true; break; - case InputControl::LeftStickDown: positive[1] = true; break; - case InputControl::RightStickLeft: negative[2] = true; break; - case InputControl::RightStickRight: positive[2] = true; break; - case InputControl::RightStickUp: negative[3] = true; break; - case InputControl::RightStickDown: positive[3] = true; break; case InputControl::TouchLeft: state.touchLeft = true; break; case InputControl::TouchRight: state.touchRight = true; break; + case InputControl::LeftStickLeft: + case InputControl::LeftStickRight: + case InputControl::LeftStickUp: + case InputControl::LeftStickDown: + case InputControl::RightStickLeft: + case InputControl::RightStickRight: + case InputControl::RightStickUp: + case InputControl::RightStickDown: + case InputControl::StickModifier: break; + case InputControl::Shake: { + const float sign = (lastAdvance.time_since_epoch() / ShakeHalfPeriod) % 2 == 0 ? 1.0f : -1.0f; + state.hasMotion = true; + state.accel = {sign * ShakeAcceleration, 9.80665f, 0.0f}; + state.gyro = {0.0f, 0.0f, sign * ShakeAngularVelocity}; + break; + } case InputControl::ToggleMouse: break; case InputControl::ToggleFullscreen: break; + case InputControl::TogglePerformanceOverlay: break; + case InputControl::Screenshot: break; } } + const auto input = stickInput(); + const auto& axes = configuration.settings.keyboardRampMs > 0.0 ? stickAxes : input.targets; for (std::size_t axis = 0; axis < state.sticks.size(); ++axis) { - if (negative[axis] || positive[axis]) state.sticks[axis] = negative[axis] == positive[axis] ? 128 : negative[axis] ? 0 : 255; + if (input.held[axis] || axes[axis] != 0.0) state.sticks[axis] = StickByte(axes[axis]); } if (mouseMode) { const auto deflection = mouse.Deflection(); state.sticks[2] = StickByte(deflection[0]); state.sticks[3] = StickByte(deflection[1]); } + if (configuration.settings.triggerRampMs > 0.0) { + state.analogButtonsL2 = std::max(state.analogButtonsL2, triggerByte(triggerAxes[0])); + state.analogButtonsR2 = std::max(state.analogButtonsR2, triggerByte(triggerAxes[1])); + } if (state.analogButtonsL2 != 0) state.buttons |= static_cast(PadButton::L2); if (state.analogButtonsR2 != 0) state.buttons |= static_cast(PadButton::R2); return state; diff --git a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp index c00aef3ff3..f1e79af366 100644 --- a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp @@ -54,7 +54,7 @@ void MouseInput::HandleEvent(const SDL_Event& event, unsigned windowId) { default: return; } - MousePublishInput_nid_postfix(input); + MousePublishInput_nid_no_patch(input); } void MouseInput::SetPresentation(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t presentedWidth, std::uint32_t presentedHeight) { @@ -78,10 +78,14 @@ std::int32_t MouseInput::scaled(std::int32_t delta, double scale, double& remain } bool MouseInput::CaptureWanted(bool allowed) const { - return allowed && focused && MouseIsOpen_nid_postfix(); + return allowed && focused && MouseIsOpen_nid_no_patch(); } 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 35c3228916..24f9b6f15c 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -12,6 +12,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libSceVideoOut/include/VideoOutDriver.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp" static constexpr std::uint32_t VIDEO_OUT_OUTPUT_OPTIONS_FIRST_WORD = 0x00FF0000; static constexpr std::size_t VIDEO_OUT_OUTPUT_OPTIONS_FREE_WORD = 3; @@ -19,8 +20,10 @@ static constexpr std::size_t VIDEO_OUT_OUTPUT_OPTIONS_FREE_WORD = 3; static VideoOutOutputStatus outputStatus(const auto& cfg) { VideoOutOutputStatus status{}; status.resolution = (cfg.width >= 3840 || cfg.height >= 2160) ? 2u : 1u; - status.dynamicRange = 1; + const bool hdr = AgcDriver::CurrentHostHdr().available; + status.dynamicRange = hdr ? VIDEO_OUT_DYNAMIC_RANGE_HDR : VIDEO_OUT_DYNAMIC_RANGE_SDR; status.refreshRate = (cfg.outputMode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) ? VIDEO_OUT_REFRESH_RATE_119_88HZ : VIDEO_OUT_REFRESH_RATE_59_94HZ; + status.flags = hdr ? VIDEO_OUT_OUTPUT_STATUS_FLAG_HDR : 0; return status; } @@ -77,6 +80,7 @@ static void validateOpenParam(const void* param) { extern "C" { int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* param) try { + if (busType == VIDEO_OUT_BUS_TYPE_HMD2_MAIN) return VIDEO_OUT_ERROR_INVALID_VALUE; validateOpenParam(param); if (userId != 255 && userId != 0) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE"); @@ -114,9 +118,7 @@ int APS5_VABI sceVideoOutSetFlipRate(int handle, int rate) try { if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); } - if (rate < 0 || rate > 2) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE"); - } + if (rate < 0 || rate > 2) return VIDEO_OUT_ERROR_INVALID_VALUE; std::lock_guard lock(cfg->mutex); cfg->Check(); cfg->flipRate = rate; @@ -135,7 +137,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(); @@ -151,7 +159,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(); @@ -326,6 +340,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 e5844a4b42..d9abeeb048 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -9,7 +9,10 @@ #include "SDL.h" #include "prx/libSceVideoOut/include/PadInput.hpp" +#include "prx/libSceMouse/include/MouseState.hpp" #include "prx/libSceVideoOut/include/DisplayWindow.hpp" +#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libScePad/include/PadState.hpp" #include "prx/libScePad/include/PadInputTypes.hpp" #include "prx/libc/include/General.hpp" @@ -57,6 +60,8 @@ void PadInput::openController(int deviceIndex) { SDL_GameControllerGetNumTouchpads(controller), SDL_GameControllerHasLED(controller) == SDL_TRUE, SDL_GameControllerHasRumbleTriggers(controller) == SDL_TRUE); enableSensors(); outputPending = true; + lightBar.Reset(); + triggerEffect.Reset(); } void PadInput::enableSensors() { @@ -83,9 +88,17 @@ void PadInput::applyOutput() { } if (controller == nullptr) return; const auto now = std::chrono::steady_clock::now(); - const bool rumbling = outputState.vibrationLarge != 0 || outputState.vibrationSmall != 0; - const bool triggerRumble = outputState.trigger[0].fallback != 0 || outputState.trigger[1].fallback != 0; const bool isPs5 = SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_PS5; + const auto large = std::max(outputState.vibrationLarge, outputState.audioVibrationLarge); + const auto small = std::max(outputState.vibrationSmall, outputState.audioVibrationSmall); + const bool rumbling = large != 0 || small != 0; + const bool triggerRumble = outputState.trigger[0].fallback != 0 || outputState.trigger[1].fallback != 0; + if (SDL_GameControllerHasLED(controller) == SDL_TRUE) { + const LightBarPacer::Color color = outputState.lightBarValid + ? LightBarPacer::Color{outputState.lightBar[0], outputState.lightBar[1], outputState.lightBar[2]} + : LightBarPacer::Color{0, 64, 255}; + if (lightBar.Due(color, now)) lightBar.Result(color, now, SDL_GameControllerSetLED(controller, color[0], color[1], color[2]) == 0); + } if (!outputPending) { if ((rumbling || (triggerRumble && !isPs5)) && now >= nextRumbleRefresh) outputPending = true; else return; @@ -93,18 +106,16 @@ void PadInput::applyOutput() { outputPending = false; nextRumbleRefresh = now + std::chrono::milliseconds(700); constexpr Uint32 rumbleMs = 2000; - SDL_GameControllerRumble(controller, static_cast(outputState.vibrationLarge * 257), static_cast(outputState.vibrationSmall * 257), rumbling ? rumbleMs : 0); - if (SDL_GameControllerHasLED(controller) == SDL_TRUE) { - if (outputState.lightBarValid) SDL_GameControllerSetLED(controller, outputState.lightBar[0], outputState.lightBar[1], outputState.lightBar[2]); - else SDL_GameControllerSetLED(controller, 0, 64, 255); - } + SDL_GameControllerRumble(controller, static_cast(large * 257), static_cast(small * 257), rumbling ? rumbleMs : 0); if (outputState.triggerTouched) { if (isPs5) { - Uint8 effect[47] = {}; + TriggerEffectCache::Effect effect{}; effect[0] = 0x04 | 0x08; - std::memcpy(effect + 10, outputState.trigger[1].effect, 11); - std::memcpy(effect + 21, outputState.trigger[0].effect, 11); - SDL_GameControllerSendEffect(controller, effect, sizeof(effect)); + std::memcpy(effect.data() + 10, outputState.trigger[1].effect, 11); + std::memcpy(effect.data() + 21, outputState.trigger[0].effect, 11); + if (triggerEffect.NeedsSend(effect)) { + triggerEffect.Result(effect, SDL_GameControllerSendEffect(controller, effect.data(), static_cast(effect.size())) == 0); + } } else if (SDL_GameControllerHasRumbleTriggers(controller) == SDL_TRUE) { SDL_GameControllerRumbleTriggers(controller, static_cast(outputState.trigger[0].fallback * 257), static_cast(outputState.trigger[1].fallback * 257), triggerRumble ? rumbleMs : 0); } @@ -181,12 +192,12 @@ PadInputState PadInput::sampleController() const { float pressure = 0.0f; if (SDL_GameControllerGetTouchpadFinger(controller, 0, finger, &down, &x, &y, &pressure) != 0 || down == 0) continue; result.touch[finger].active = true; - result.touch[finger].x = static_cast(std::clamp(x, 0.0f, 1.0f) * 1919.0f); - result.touch[finger].y = static_cast(std::clamp(y, 0.0f, 1.0f) * 942.0f); + result.touch[finger].x = static_cast(std::clamp(x, 0.0f, 1.0f) * (kTouchPadWidth - 1)); + result.touch[finger].y = static_cast(std::clamp(y, 0.0f, 1.0f) * (kTouchPadHeight - 1)); } } if (viewPressed && !result.touch[0].active && !result.touch[1].active) { - result.touch[0] = {true, 960, 471, 0}; + result.touch[0] = {true, kTouchPadWidth / 2, kTouchPadHeight / 2, 0}; } return result; } @@ -201,18 +212,18 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { if (instanceId == event.cdevice.which) { closeController(); openFirstAvailableController(); - publish(); + publish(window.FrameNumber()); } return; } if (event.type == SDL_CONTROLLERBUTTONDOWN || event.type == SDL_CONTROLLERBUTTONUP || event.type == SDL_CONTROLLERAXISMOTION || event.type == SDL_CONTROLLERDEVICEREMAPPED) { - publish(); + publish(window.FrameNumber()); return; } if (event.type == SDL_WINDOWEVENT) { if (keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now()).releaseMouse) setMouseMode(false); - publish(); + publish(window.FrameNumber()); return; } // APS5_NO_PAD_INPUT=1 keeps a measurement run from reacting to keys or mouse buttons that reach @@ -222,26 +233,31 @@ 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.togglePerformanceOverlay && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) AgcDriverTogglePerformanceOverlay_nid_postfix(); + if (requests.screenshot && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) AgcDriverRequestScreenshot_nid_postfix(); if (requests.toggleMouse) setMouseMode(!keyboardMouse.MouseMode()); - if (event.type != SDL_MOUSEMOTION) publish(); + if (event.type != SDL_MOUSEMOTION) publish(window.FrameNumber()); } -void PadInput::Update() { +void PadInput::Update(const DisplayWindow& window) { if (controller != nullptr) SDL_GameControllerUpdate(); applyOutput(); if (controller != nullptr) controllerState = sampleController(); if (keyboardMouse.MouseMode() && SDL_GetKeyboardFocus() == nullptr) { keyboardMouse.Release(); setMouseMode(false); - publish(); + publish(window.FrameNumber()); return; } const bool changed = keyboardMouse.Advance(std::chrono::steady_clock::now()); - if (controller != nullptr || keyboardMouse.MouseMode() || changed) publish(); + if (controller != nullptr || keyboardMouse.MouseMode() || changed) publish(window.FrameNumber()); } -void PadInput::publish() { - PadPublishInput_nid_postfix(keyboardMouse.Compose(controllerState)); +void PadInput::publish(std::uint64_t frame) { + const auto state = keyboardMouse.Compose(controllerState); + PadPublishInput_nid_postfix(state); + if (auto* telemetry = VideoOutTelemetry::Instance(); telemetry != nullptr) VideoOutTelemetry::EmitPadIfChanged(*telemetry, telemetryFilter, frame, state); } diff --git a/core/libs/prx/libSceVideoOut/src/Telemetry.cpp b/core/libs/prx/libSceVideoOut/src/Telemetry.cpp new file mode 100644 index 0000000000..5a1130a7bc --- /dev/null +++ b/core/libs/prx/libSceVideoOut/src/Telemetry.cpp @@ -0,0 +1,195 @@ +#include "prx/libSceVideoOut/include/Telemetry.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +#include "prx/libSceAgcDriver/TraceRevision.hpp" + +namespace VideoOutTelemetry { + +namespace { + +constexpr char ContractLevel[] = "v1-pad"; + +constexpr auto DropReportInterval = std::chrono::seconds(10); +constexpr std::size_t DefaultQueueCapacity = 4096; + +std::int64_t steadyNanos() { + return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); +} + +std::int64_t processId() { +#ifdef _WIN32 + return static_cast(::GetCurrentProcessId()); +#else + return static_cast(::getpid()); +#endif +} + +std::string head(std::int64_t timeNanos, const char* event) { + char text[96]; + std::snprintf(text, sizeof(text), "{\"v\":1,\"t_ns\":%lld,\"ev\":\"%s\"", static_cast(timeNanos), event); + return text; +} + +} + +BoundedLineQueue::BoundedLineQueue(std::size_t capacity) : capacity(capacity) { +} + +bool BoundedLineQueue::TryPush(std::string line) { + std::unique_lock lock(mutex, std::try_to_lock); + if (!lock.owns_lock()) { + dropped.fetch_add(1, std::memory_order_relaxed); + return false; + } + return pushLocked(std::move(line)); +} + +bool BoundedLineQueue::Push(std::string line) { + std::lock_guard lock(mutex); + return pushLocked(std::move(line)); +} + +bool BoundedLineQueue::pushLocked(std::string line) { + if (lines.size() >= capacity) { + dropped.fetch_add(1, std::memory_order_relaxed); + return false; + } + lines.push_back(std::move(line)); + return true; +} + +std::deque BoundedLineQueue::Drain() { + std::lock_guard lock(mutex); + return std::exchange(lines, {}); +} + +std::uint64_t BoundedLineQueue::Dropped() const { + return dropped.load(std::memory_order_relaxed); +} + +Writer::Writer(const std::string& directory, std::size_t capacity, const std::string& commit) : queue(capacity) { + const std::string path = directory + "/videoout.jsonl"; + output.open(path, std::ios::binary | std::ios::app); + if (!output.is_open()) throw std::runtime_error("VideoOut telemetry: cannot open " + path); + output.exceptions(std::ios::badbit | std::ios::failbit); + const auto steady = steadyNanos(); + char text[256]; + std::snprintf(text, sizeof(text), ",\"contract\":\"%s\",\"commit\":\"%s\",\"pid\":%lld,\"prx\":\"libSceVideoOut\"}\n", ContractLevel, commit.c_str(), static_cast(processId())); + queue.Push(head(steady, "contract") + text); + thread = std::jthread([this](std::stop_token token) { run(token); }); +} + +Writer::~Writer() { + thread.request_stop(); + if (thread.joinable()) thread.join(); +} + +bool Writer::emit(std::string line, bool mustKeep) { + return mustKeep ? queue.Push(std::move(line)) : queue.TryPush(std::move(line)); +} + +void Writer::writeAll(const std::string& text) { + output.write(text.data(), static_cast(text.size())); + output.flush(); +} + +void Writer::writeDrops(bool force) { + const auto total = queue.Dropped(); + if (total == 0 || (total == reportedDrops && !force)) return; + reportedDrops = total; + char text[64]; + std::snprintf(text, sizeof(text), ",\"dropped\":%llu}\n", static_cast(total)); + writeAll(head(steadyNanos(), "drops") + text); +} + +void Writer::run(std::stop_token token) { + try { + auto nextDropReport = std::chrono::steady_clock::now() + DropReportInterval; + std::mutex sleepMutex; + std::condition_variable_any sleepWake; + while (true) { + { + std::unique_lock lock(sleepMutex); + sleepWake.wait_for(lock, token, std::chrono::milliseconds(200), [] { return false; }); + } + std::string batch; + for (auto& line : queue.Drain()) batch += line; + if (!batch.empty()) writeAll(batch); + if (std::chrono::steady_clock::now() >= nextDropReport) { + nextDropReport += DropReportInterval; + writeDrops(false); + } + if (token.stop_requested()) break; + } + std::string rest; + for (auto& line : queue.Drain()) rest += line; + if (!rest.empty()) writeAll(rest); + writeDrops(true); + } catch (const std::exception& error) { + std::fprintf(stderr, "[videoout] telemetry writer stopped: %s\n", error.what()); + } catch (...) { + std::fprintf(stderr, "[videoout] telemetry writer stopped\n"); + } +} + +bool Writer::EmitPad(std::uint64_t frame, const PadInputState& state) { + char text[192]; + std::snprintf(text, sizeof(text), ",\"n\":%llu,\"buttons\":%u,\"sticks\":[%u,%u,%u,%u],\"triggers\":[%u,%u]}\n", + static_cast(frame), state.buttons, state.sticks[0], state.sticks[1], state.sticks[2], state.sticks[3], + state.analogButtonsL2, state.analogButtonsR2); + return emit(head(steadyNanos(), "pad") + text, true); +} + +void Writer::EmitPresent(std::uint64_t frame, double fps) { + char text[96]; + std::snprintf(text, sizeof(text), ",\"n\":%llu,\"fps\":%.2f}\n", static_cast(frame), fps); + emit(head(steadyNanos(), "present") + text, false); +} + +std::uint64_t Writer::Dropped() const { + return queue.Dropped(); +} + +bool PadChangeFilter::Changed(const PadInputState& state) const { + return state.buttons != last.buttons || state.sticks != last.sticks || + state.analogButtonsL2 != last.analogButtonsL2 || state.analogButtonsR2 != last.analogButtonsR2; +} + +void PadChangeFilter::Commit(const PadInputState& state) { + last.buttons = state.buttons; + last.sticks = state.sticks; + last.analogButtonsL2 = state.analogButtonsL2; + last.analogButtonsR2 = state.analogButtonsR2; +} + +bool EmitPadIfChanged(Writer& writer, PadChangeFilter& filter, std::uint64_t frame, const PadInputState& state) { + if (!filter.Changed(state)) return false; + if (!writer.EmitPad(frame, state)) return false; + filter.Commit(state); + return true; +} + +std::unique_ptr OpenFromEnvironment(const char* directory, const std::string& commit) { + if (directory == nullptr || directory[0] == '\0') return nullptr; + return std::make_unique(directory, DefaultQueueCapacity, commit); +} + +Writer* Instance() { + static const std::unique_ptr writer = OpenFromEnvironment(std::getenv("APS5_VIDEOOUT_TELEMETRY_DIR"), APS5_TRACE_COMMIT); + return writer.get(); +} + +} diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 0decc71701..f308f42739 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -18,14 +19,16 @@ #include "prx/libScePad/include/PadState.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include "prx/libSceVideoOut/include/VideoOutDriver.hpp" #if APS5_ENABLE_TIMING_LOG #include "prx/libSceVideoOut/include/FrameTimingLog.hpp" #endif #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp" #include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" -#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/PreciseWait.hpp" #include "prx/libc/include/Shutdown.hpp" @@ -132,7 +135,10 @@ public: require(room, "flip queue stayed full for 60 s: the presenter is not completing flips"); } - std::shared_ptr Reserve(const AgcDriver::FlipInfo& info) override { + std::shared_ptr Reserve(const AgcDriver::FlipInfo& info) override { return Reserve(info, false); } + std::shared_ptr TryReserve(const AgcDriver::FlipInfo& info) override { return Reserve(info, true); } + + std::shared_ptr Reserve(const AgcDriver::FlipInfo& info, bool limited) { // All flip modes are presented at the next vsync. require(info.mode >= VIDEO_OUT_FLIP_MODE_VSYNC && info.mode <= 6, "unsupported flip mode"); require(info.index >= VIDEO_OUT_BUFFER_INDEX_BLACK && info.index < VIDEO_OUT_BUFFER_NUM_MAX, "invalid flip index"); @@ -146,11 +152,15 @@ public: std::lock_guard queueLock(queue->mutex); if (queue->failure) std::rethrow_exception(queue->failure); if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; + if (limited && queue->reservations.load() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) return {}; std::lock_guard lock(cfg->mutex); checkConfig(*cfg); request->generation = cfg->generation; request->flipRate = cfg->flipRate; - if (info.index >= 0) request->reuseTicket = cfg->bufferReuse[info.index].Reserve(); + if (info.index >= 0) { + if (limited) require(cfg->buffers[info.index].Occupied(), "EOP flip buffer is not registered"); + request->reuseTicket = cfg->bufferReuse[info.index].Reserve(); + } ++queue->reservations; ++cfg->flipStatus.flipPendingNum; if (info.index >= 0) ++cfg->bufferPending[info.index]; @@ -262,6 +272,7 @@ VideoOutDriver::VideoOutDriver() { SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2"); #ifdef _WIN32 SDL_SetHintWithPriority(SDL_HINT_DIRECTINPUT_ENABLED, "0", SDL_HINT_DEFAULT); + if (std::getenv("APS5_NO_WGI") != nullptr) SDL_SetHintWithPriority(SDL_HINT_JOYSTICK_WGI, "0", SDL_HINT_DEFAULT); #endif #ifdef __linux__ SDL_SetHint(SDL_HINT_VIDEODRIVER, "wayland,x11"); @@ -430,10 +441,25 @@ int VideoOutDriver::SubmitFlip(int handle, int index, int flipMode, int64_t flip // A title that does not pace on flipPendingNum can run ahead of the presenter now that the queue // worker no longer waits per flip; a full queue is the documented error, not a failure. if (flipQueue->reservations.load() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) return VIDEO_OUT_ERROR_FLIP_QUEUE_FULL; - std::array words{AgcDriver::FlipPacketHeader, static_cast(handle), static_cast(index), static_cast(flipMode), static_cast(static_cast(flipArg)), static_cast(static_cast(flipArg) >> 32u)}; - Packet packet{words.data(), static_cast(words.size()), 0, {}}; - const auto result = sceAgcDriverSubmitDcb(&packet); - require(result == 0, "driver rejected flip submission"); + AgcDriverSubmitFlip_nid_postfix(AgcDriver::FlipInfo{static_cast(handle), index, static_cast(flipMode), flipArg}); + return 0; +} + +int VideoOutDriver::SubmitEopFlip(int handle, int index, int flipMode, int64_t flipArg, std::uint32_t* token) { + std::lock_guard lock(mutex); + if (handle <= 0 || handle >= VIDEO_OUT_NUM_MAX || !outputs[handle]) return VIDEO_OUT_ERROR_INVALID_HANDLE; + if (flipMode < VIDEO_OUT_FLIP_MODE_VSYNC || flipMode > 6) return VIDEO_OUT_ERROR_INVALID_VALUE; + if (index < VIDEO_OUT_BUFFER_INDEX_BLACK || index >= VIDEO_OUT_BUFFER_NUM_MAX) return VIDEO_OUT_ERROR_INVALID_INDEX; + const auto cfg = contexts[handle]; + { + std::lock_guard cfgLock(cfg->mutex); + checkConfig(*cfg); + if (index >= 0 && !cfg->buffers[index].Occupied()) return VIDEO_OUT_ERROR_INVALID_INDEX; + } + const AgcDriver::FlipInfo info{static_cast(handle), index, static_cast(flipMode), flipArg}; + const auto reserved = AgcDriverReserveEopFlip_nid_postfix(info); + if (reserved == 0) return VIDEO_OUT_ERROR_FLIP_QUEUE_FULL; + std::memcpy(token, &reserved, sizeof(reserved)); return 0; } @@ -471,6 +497,20 @@ void VideoOutDriver::updateDisplayRefreshRate() { displayRefreshRate.store(mode.refresh_rate); } +void VideoOutDriver::pollHostHdr() { + hdrProbe.Poll(window.Handle()); + const auto generation = AgcDriver::CurrentHostHdr().generation; + if (generation == hdrGeneration) return; + hdrGeneration = generation; + std::lock_guard lock(mutex); + for (const auto& cfg : contexts) { + if (!cfg) continue; + std::lock_guard cfgLock(cfg->mutex); + if (!cfg->opened || cfg->closing || cfg->failure) continue; + triggerEvents(*cfg, VIDEO_OUT_EVENT_SET_MODE, reinterpret_cast(cfg->outputMode)); + } +} + void VideoOutDriver::vblankEnd(std::uint64_t tick) { std::lock_guard lock(mutex); for (const auto& cfg : contexts) { @@ -502,7 +542,11 @@ void VideoOutDriver::processFlip(FlipRequest& req) { req.ReleaseLocked(); return; } - require(req.cfg->outputMode != VIDEO_OUT_OUTPUT_MODE_119_88HZ || SupportsHighFrameRate(), "display no longer supports 119.88 Hz output"); + if (req.cfg->outputMode == VIDEO_OUT_OUTPUT_MODE_119_88HZ && !SupportsHighFrameRate()) { + req.cfg->outputMode = VIDEO_OUT_OUTPUT_MODE_DEFAULT; + triggerEvents(*req.cfg, VIDEO_OUT_EVENT_SET_MODE, reinterpret_cast(req.cfg->outputMode)); + std::fprintf(stderr, "[videoout] display no longer supports 119.88 Hz; fell back to 59.94 Hz\n"); + } require(req.ready && !req.terminal && req.generation == req.cfg->generation, "stale or incomplete flip request"); const auto interval = static_cast(req.flipRate + 1); require(req.cfg->lastFlipVblank <= std::numeric_limits::max() - interval, "flip interval overflow"); @@ -564,6 +608,11 @@ void VideoOutDriver::processFlip(FlipRequest& req) { req.cfg->flipStatus.processTime = sceKernelGetProcessTime(); req.cfg->flipStatus.processTimeCounter = sceKernelGetProcessTimeCounter(); req.cfg->flipStatus.flipArg = req.flipArg; + const auto previous = req.cfg->flipStatus.currentBuffer; + if (previous >= 0) { + auto& label = req.cfg->bufferLabels[previous]; + AgcDriverStoreGuestLabel_nid_postfix(&label, 0); + } req.cfg->flipStatus.currentBuffer = req.index; req.cfg->width = req.width; req.cfg->height = req.height; @@ -575,6 +624,7 @@ void VideoOutDriver::processFlip(FlipRequest& req) { } req.terminal = true; req.cfg->vblankCond.notify_all(); + KernelWakeSleepers_nid_no_patch(); timing.Mark("notify_game"); } @@ -582,9 +632,12 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star try { window.Ensure(VIDEO_OUT_DEFAULT_WIDTH, VIDEO_OUT_DEFAULT_HEIGHT); updateDisplayRefreshRate(); + hdrProbe.Poll(window.Handle()); const auto extensions = windowExtensions(window.Handle()); AgcDriverAttachWindow_nid_postfix({window.Handle(), extensions, &createWindowSurface, &windowDrawableSize, VIDEO_OUT_DEFAULT_WIDTH, VIDEO_OUT_DEFAULT_HEIGHT, nullptr}); + hdrGeneration = AgcDriver::CurrentHostHdr().generation; } catch (...) { + hdrProbe.Release(); window.Destroy(); started.set_exception(std::current_exception()); return; @@ -627,8 +680,9 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star imeTextInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); } } - padInput.Update(); + padInput.Update(window); updateDisplayRefreshRate(); + pollHostHdr(); mouseInput.UpdateCapture(!padInput.MouseModeEnabled()); if (current) { require(current->timing != nullptr, "missing presentation timing"); @@ -692,6 +746,7 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star std::lock_guard lock(flipQueue->mutex); if (!flipQueue->failure) flipQueue->failure = std::current_exception(); } + hdrProbe.Release(); window.Destroy(); } diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index b90bbc768a..365eddf412 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -15,10 +15,12 @@ #include #include #include +#include #include #include #include #include +#include #include #undef main @@ -123,6 +125,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); @@ -155,6 +226,57 @@ void testDeferredBuffer() { LibcRunShutdown_nid_postfix(); } +void testCpuFlipOrder() { + const bool inOrder = std::getenv("APS5_CPU_FLIP_IN_ORDER") != nullptr; + const int handle = sceVideoOutOpen(255, 0, 0, nullptr); + auto cfg = VideoOutDriver::Get().GetConfig(handle); + std::vector allocation(65536 + 65535); + const auto storage = alignedBuffer(allocation); + VideoOutBuffers buffer{storage.data(), nullptr, {nullptr, nullptr}}; + VideoOutBufferAttribute2 attribute{}; + sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 64, 64, 0, 0, 0); + check(sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr) == 0, "buffer registration failed"); + const auto flipsWithin = [&](int count, std::chrono::milliseconds limit) { + std::unique_lock lock(cfg->mutex); + const bool done = cfg->vblankCond.wait_for(lock, limit, [&] { return cfg->failure || cfg->flipStatus.count == count; }); + if (cfg->failure) std::rethrow_exception(cfg->failure); + return done; + }; + const auto shownArgument = [&] { + std::lock_guard lock(cfg->mutex); + return cfg->flipStatus.flipArg; + }; + auto first = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, first); + std::array firstHeld{0xc004105c, 7, 0xfffffffeu, 1, 0, 0}; + Packet firstPacket{firstHeld.data(), 6, 0, {}}; + check(sceAgcDriverSubmitDcb(&firstPacket) == 0, "the first gated submission was not accepted"); + std::thread flipThread([&] { check(sceVideoOutSubmitFlip(handle, 0, 1, 5) == 0, "the flip thread's flip was not accepted"); }); + flipThread.join(); + const bool ahead = flipsWithin(1, std::chrono::milliseconds(inOrder ? 1000 : 8000)); + if (inOrder) check(!ahead, "with APS5_CPU_FLIP_IN_ORDER a flip completed while a queue-0 submission ahead of it was held"); + else check(ahead, "a flip from a thread that never submitted queue-0 work waited for a held queue-0 submission"); + first->Release(); + check(flipsWithin(1, std::chrono::seconds(30)), "the flip thread's flip did not complete"); + check(shownArgument() == 5, "the flip thread's flip presented the wrong request"); + auto second = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(8, second); + std::array secondHeld{0xc004105c, 8, 0xfffffffeu, 1, 0, 0}; + Packet secondPacket{secondHeld.data(), 6, 0, {}}; + check(sceAgcDriverSubmitDcb(&secondPacket) == 0, "the second gated submission was not accepted"); + check(sceVideoOutSubmitFlip(handle, 0, 1, 6) == 0, "the submitting thread's flip was not accepted"); + check(!flipsWithin(2, std::chrono::milliseconds(1000)), "a flip from the thread that submitted queue-0 work completed ahead of that work"); + second->Release(); + check(flipsWithin(2, std::chrono::seconds(30)), "the submitting thread's flip did not complete"); + check(shownArgument() == 6, "the submitting thread's flip presented the wrong request"); + AgcDriverWaitIdle_nid_postfix(); + sceVideoOutUnregisterBuffers(handle, 0); + sceVideoOutClose(handle); + AgcDriverUnregisterVideoOutput_nid_postfix(7, first); + AgcDriverUnregisterVideoOutput_nid_postfix(8, second); + LibcRunShutdown_nid_postfix(); +} + void testUnregisteredBuffer() { const int handle = sceVideoOutOpen(255, 0, 0, nullptr); auto cfg = VideoOutDriver::Get().GetConfig(handle); @@ -251,6 +373,12 @@ void testDecode() { check(group.attribute.option == 0x28, "an added buffer option did not keep the existing bits"); check(expectFailure([&] { DescribeVideoOutBuffer(registered, group); }).find("buffer option 40") != std::string::npos, "an unsupported added buffer option was accepted"); check(expectFailure([&] { sceVideoOutAddBufferAttributeOption(nullptr, 0x20); }).find("null attribute") != std::string::npos, "a null attribute was accepted"); + sceVideoOutRemoveBufferAttributeOption(&group.attribute, 0x21); + check(group.attribute.option == VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY && group.attribute.width == 259 && group.attribute.height == 137, "a removed buffer option was not cleared alone"); + check(DescribeVideoOutBuffer(registered, group).address == plain.address, "a buffer whose unsupported option was removed was rejected"); + sceVideoOutRemoveBufferAttributeOption(&group.attribute, VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY); + check(group.attribute.option == VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_NONE, "removing the last buffer option did not clear it"); + check(expectFailure([&] { sceVideoOutRemoveBufferAttributeOption(nullptr, 0x20); }).find("null attribute") != std::string::npos, "a null attribute was accepted for removal"); VideoOutBuffer dccBuffer{0, reinterpret_cast(tiled.data()), 0x7f0000}; sceVideoOutSetBufferAttribute2(&group.attribute, 0x8000000000000000ull, 0, 259, 137, VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_NONE, 0x208, 0x11223344); group.category = VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED; @@ -315,7 +443,10 @@ void testControls() { for (int rate = 0; rate <= 2; ++rate) { check(sceVideoOutSetFlipRate(handle, rate) == 0 && cfg->flipRate == rate, "flip rate was not applied"); } - expectFailure([&] { sceVideoOutSetFlipRate(handle, 3); }); + for (const int rate : {3, 60, -1}) { + check(sceVideoOutSetFlipRate(handle, rate) == VIDEO_OUT_ERROR_INVALID_VALUE, "flip rate outside 0..2 was accepted"); + check(cfg->flipRate == 2, "a rejected flip rate changed the stored rate"); + } VideoOutColorSettings settings{}; expectFailure([&] { sceVideoOutColorSettingsSetGamma(&settings, std::numeric_limits::quiet_NaN()); }); KernelEqueue queue = 0; @@ -348,10 +479,12 @@ void testControls() { check(owner->GetTriggeredEvents(&event, 1) == 1 && event.udata == &settings && sceVideoOutGetEventId(&event) == VIDEO_OUT_EVENT_VBLANK, "vblank event or updated user data missing"); check(sceVideoOutAddPreVblankStartEvent(queue, handle, &mode) == 0, "pre-vblank subscription failed"); check(sceVideoOutWaitVblank(handle) == 0, "vblank wait failed"); - KernelEvent events[2]{}; - check(owner->GetTriggeredEvents(events, 2) == 2, "pre-vblank or vblank event missing"); - const auto& preVblank = sceVideoOutGetEventId(&events[0]) == VIDEO_OUT_EVENT_PRE_VBLANK_START ? events[0] : events[1]; - check(sceVideoOutGetEventId(&preVblank) == VIDEO_OUT_EVENT_PRE_VBLANK_START && preVblank.udata == &mode, "pre-vblank event missing"); + std::array events{}; + const int collected = owner->GetTriggeredEvents(events.data(), static_cast(events.size())); + check(collected >= 2, "pre-vblank or vblank event missing"); + const auto end = events.begin() + collected; + check(std::any_of(events.begin(), end, [&](KernelEvent& item) { return sceVideoOutGetEventId(&item) == VIDEO_OUT_EVENT_VBLANK && item.udata == &settings; }), "vblank event missing"); + check(std::any_of(events.begin(), end, [&](KernelEvent& item) { return sceVideoOutGetEventId(&item) == VIDEO_OUT_EVENT_PRE_VBLANK_START && item.udata == &mode; }), "pre-vblank event missing"); check(sceVideoOutDeletePreVblankStartEvent(queue, handle) == 0, "pre-vblank unsubscription failed"); std::vector allocation(65536 + 65535); const auto storage = alignedBuffer(allocation); @@ -557,7 +690,12 @@ struct BusyLoop { std::vector commands; }; -BusyLoop prepareBusyLoop(std::uint32_t iterations) { +constexpr std::uint32_t busyLoopTotalIterations = 100000000u; +constexpr std::uint32_t busyLoopDispatchCount = 100u; +constexpr std::uint32_t busyLoopIterationsPerDispatch = busyLoopTotalIterations / busyLoopDispatchCount; +static_assert(busyLoopTotalIterations % busyLoopDispatchCount == 0); + +BusyLoop prepareBusyLoop(std::uint32_t iterations, std::uint32_t dispatchCount = 1u) { alignas(256) static const std::array code{ 0xbe850380, 0x7e040281, 0xd5690002, 0x00020502, 0x4a040481, 0x80058105, 0xbf070405, 0xbf85fffa, 0x7e020280, 0xe0701000, 0x80000201, 0xbf810000}; @@ -579,13 +717,17 @@ BusyLoop prepareBusyLoop(std::uint32_t iterations) { const auto program = reinterpret_cast(code.data()); const auto output = reinterpret_cast(result); const auto target = reinterpret_cast(label); - return {result, label, { + std::vector commands{ 0xc0027600, 0x20c, static_cast(program >> 8u), static_cast(program >> 40u), 0xc0017600, 0x213, 5u << 1u, 0xc0037600, 0x207, 32, 1, 1, 0xc0057600, 0x240, static_cast(output), static_cast(output >> 32u) & 0xffffu, 4, 0x31016fac, iterations, - 0xc0031500, 1, 1, 1, 0x8041, - 0xc0064900, 0x514, 1u << 29u, static_cast(target), static_cast(target >> 32u), 1, 0, 0}}; + }; + for (std::uint32_t dispatch = 0; dispatch < dispatchCount; ++dispatch) { + commands.insert(commands.end(), {0xc0031500, 1, 1, 1, 0x8041}); + } + commands.insert(commands.end(), {0xc0064900, 0x514, 1u << 29u, static_cast(target), static_cast(target >> 32u), 1, 0, 0}); + return {result, label, std::move(commands)}; } void testReleaseVblank() { @@ -599,7 +741,7 @@ void testReleaseVblank() { sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0, 0); sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr); check(sceVideoOutSetFlipRate(handle, 2) == 0, "flip rate was not applied"); - auto busy = prepareBusyLoop(100000000u); + auto busy = prepareBusyLoop(busyLoopIterationsPerDispatch, busyLoopDispatchCount); Packet packet{busy.commands.data(), static_cast(busy.commands.size()), 0, {}}; check(sceAgcDriverSubmitDcb(&packet) == 0, "the GPU work was not accepted"); sceVideoOutSubmitFlip(handle, 0, 1, 1); @@ -608,14 +750,14 @@ void testReleaseVblank() { const bool done = cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return cfg->failure || cfg->flipStatus.count == 1; }); if (cfg->failure) std::rethrow_exception(cfg->failure); check(done, "the flip did not complete"); - check(cfg->lastFlipVblank >= 3 && cfg->vblankStatus.count >= cfg->lastFlipVblank + 3, "the next flip is not paced from the vblank this flip was released at"); + check(cfg->lastFlipVblank >= 3, "the first flip was released before its configured interval"); } sceVideoOutUnregisterBuffers(handle, 0); sceVideoOutClose(handle); LibcRunShutdown_nid_postfix(); } -void testFlipAfterGpuWork() { +void testFlipAfterGpuWork(bool eop = false) { const int handle = sceVideoOutOpen(255, 0, 0, nullptr); auto cfg = VideoOutDriver::Get().GetConfig(handle); std::vector allocation(6 * 65536 + 65535); @@ -625,8 +767,8 @@ void testFlipAfterGpuWork() { VideoOutBufferAttribute2 attribute{}; sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0, 0); sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr); - constexpr std::uint32_t iterations = 100000000u; - auto busy = prepareBusyLoop(iterations); + constexpr std::uint32_t iterations = busyLoopIterationsPerDispatch; + auto busy = prepareBusyLoop(iterations, busyLoopDispatchCount); auto& result = *busy.result; auto& label = *busy.label; Packet packet{busy.commands.data(), static_cast(busy.commands.size()), 0, {}}; @@ -637,6 +779,15 @@ void testFlipAfterGpuWork() { if (cfg->failure) std::rethrow_exception(cfg->failure); check(done, "the first flip did not complete"); } + if (eop) { + std::uint32_t token = 0; + check(sceVideoOutSubmitEopFlip(handle, 0, 1, 5, &token) == 0, "GPU-work EOP reservation failed"); + const auto target = reinterpret_cast(busy.label); + const std::array release{0xc0064900u, 0x06200504u, 0x42010000u, static_cast(target), static_cast(target >> 32u), 1u, 0u, token}; + busy.commands.insert(busy.commands.end(), release.begin(), release.end()); + packet.addr = busy.commands.data(); + packet.dw_num = static_cast(busy.commands.size()); + } std::atomic landed{0}; const auto start = std::chrono::steady_clock::now(); const auto since = [&] { return std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); }; @@ -645,7 +796,7 @@ void testFlipAfterGpuWork() { if (label == 1) landed.store(since()); }); check(sceAgcDriverSubmitDcb(&packet) == 0, "the GPU work was not accepted"); - check(sceVideoOutSubmitFlip(handle, 0, 1, 5) == 0, "the flip was not accepted"); + if (!eop) check(sceVideoOutSubmitFlip(handle, 0, 1, 5) == 0, "the flip was not accepted"); std::int64_t flipped = 0; { std::unique_lock lock(cfg->mutex); @@ -665,6 +816,75 @@ void testFlipAfterGpuWork() { LibcRunShutdown_nid_postfix(); } +void testLabels(bool overlay) { + std::uintptr_t address = 0x1234; + for (const int invalid : {-1, 0, VIDEO_OUT_NUM_MAX, std::numeric_limits::max()}) { + check(sceVideoOutGetBufferLabelAddress(invalid, &address) == VIDEO_OUT_ERROR_INVALID_HANDLE && address == 0x1234, "invalid label query changed its output"); + } + check(sceVideoOutGetBufferLabelAddress(1, nullptr) == VIDEO_OUT_ERROR_INVALID_ADDRESS, "null label output was accepted"); + check(sceVideoOutGetBufferLabelAddress(2, &address) == VIDEO_OUT_ERROR_INVALID_HANDLE && address == 0x1234, "unopened label query changed its output"); + const int bus = overlay ? VIDEO_OUT_BUS_TYPE_OVERLAY : VIDEO_OUT_BUS_TYPE_MAIN; + const int handle = sceVideoOutOpen(255, bus, 0, nullptr); + auto cfg = VideoOutDriver::Get().GetConfig(handle); + check(sceVideoOutGetBufferLabelAddress(handle, &address) == VIDEO_OUT_BUFFER_NUM_MAX, "label query returned the wrong count"); + check(address != 0 && address % std::atomic_ref::required_alignment == 0, "label array is absent or unaligned"); + auto* labels = reinterpret_cast(address); + for (int i = 0; i < VIDEO_OUT_BUFFER_NUM_MAX; ++i) check(std::atomic_ref(labels[i]).load() == 0, "new port has a nonzero label"); + alignas(std::uintptr_t) std::array unaligned; + unaligned.fill(std::byte{0x5a}); + check(sceVideoOutGetBufferLabelAddress(handle, reinterpret_cast(unaligned.data() + 1)) == VIDEO_OUT_BUFFER_NUM_MAX, "unaligned label-address output failed"); + std::uintptr_t repeated = 0; + std::memcpy(&repeated, unaligned.data() + 1, sizeof(repeated)); + check(repeated == address && unaligned.front() == std::byte{0x5a} && unaligned.back() == std::byte{0x5a}, "label query changed its address or output canaries"); + for (int i : {0, 1, 15}) std::atomic_ref(labels[i]).store(55); + std::vector allocation(2 * 65536 + 65535); + const auto storage = alignedBuffer(allocation); + std::array buffers{}; + buffers[0].data = storage.data(); + buffers[1].data = storage.data() + 65536; + VideoOutBufferAttribute2 attribute{}; + attribute.width = 64; + attribute.height = 64; + attribute.pixel_format = 0x8000000000000000ull; + check(sceVideoOutRegisterBuffers2(handle, 0, 0, buffers.data(), 2, &attribute, 0, nullptr) == 0, "label-test buffers failed to register"); + check(std::atomic_ref(labels[0]).load() == 0 && std::atomic_ref(labels[1]).load() == 0 && std::atomic_ref(labels[15]).load() == 55, "registration reset the wrong labels"); + std::atomic_ref(labels[0]).store(1); + std::atomic_ref(labels[1]).store(1); + const auto flip = [&](int index, std::uint64_t count) { + check(sceVideoOutSubmitFlip(handle, index, VIDEO_OUT_FLIP_MODE_VSYNC, index) == 0, "label-test flip failed to submit"); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + VideoOutFlipStatus status{}; + do { + check(sceVideoOutGetFlipStatus(handle, &status) == 0, "label-test flip status failed"); + if (status.count >= count) return; + std::this_thread::yield(); + } while (std::chrono::steady_clock::now() < deadline); + throw std::runtime_error("label-test flip timed out"); + }; + flip(0, 1); + check(std::atomic_ref(labels[0]).load() == 1 && std::atomic_ref(labels[1]).load() == 1, "first flip released a label before replacement"); + flip(1, 2); + check(std::atomic_ref(labels[0]).load() == 0 && std::atomic_ref(labels[1]).load() == 1, "replacement flip released the wrong label"); + flip(VIDEO_OUT_BUFFER_INDEX_BLACK, 3); + check(std::atomic_ref(labels[1]).load() == 0 && std::atomic_ref(labels[15]).load() == 55, "black flip did not release only the previous buffer"); + flip(VIDEO_OUT_BUFFER_INDEX_BLANK, 4); + check(std::atomic_ref(labels[15]).load() == 55, "blank flip accessed a negative label index"); + const int otherHandle = sceVideoOutOpen(255, overlay ? VIDEO_OUT_BUS_TYPE_MAIN : VIDEO_OUT_BUS_TYPE_OVERLAY, 0, nullptr); + std::uintptr_t other = 0; + check(sceVideoOutGetBufferLabelAddress(otherHandle, &other) == VIDEO_OUT_BUFFER_NUM_MAX && other != address, "ports share their labels"); + sceVideoOutClose(otherHandle); + sceVideoOutClose(handle); + repeated = 0x1234; + check(sceVideoOutGetBufferLabelAddress(handle, &repeated) == VIDEO_OUT_ERROR_INVALID_HANDLE && repeated == 0x1234, "closed label query changed its output"); + const int reopened = sceVideoOutOpen(255, bus, 0, nullptr); + check(sceVideoOutGetBufferLabelAddress(reopened, &repeated) == VIDEO_OUT_BUFFER_NUM_MAX && repeated != address, "reopened port shares live old-generation labels"); + auto* fresh = reinterpret_cast(repeated); + for (int i = 0; i < VIDEO_OUT_BUFFER_NUM_MAX; ++i) check(std::atomic_ref(fresh[i]).load() == 0, "reopened port retained an old label"); + check(std::atomic_ref(labels[15]).load() == 55, "reopening changed old-generation labels"); + sceVideoOutClose(reopened); + LibcRunShutdown_nid_postfix(); +} + void testUnopenedEventRegistration() { KernelEqueue eq = 0; check(sceKernelCreateEqueue(&eq, "unopened") == 0, "the event queue was not created"); @@ -678,6 +898,99 @@ void testUnopenedEventRegistration() { LibcRunShutdown_nid_postfix(); } +void testEop(bool overlay) { + std::uint32_t token = 0x5a5a5a5a; + check(sceVideoOutSubmitEopFlip(0, 0, 1, 0, &token) == VIDEO_OUT_ERROR_INVALID_HANDLE && token == 0x5a5a5a5a, "invalid EOP handle changed the token"); + check(sceVideoOutSubmitEopFlip(1, 0, 1, 0, nullptr) == VIDEO_OUT_ERROR_INVALID_ADDRESS, "null EOP token output was accepted"); + const int bus = overlay ? VIDEO_OUT_BUS_TYPE_OVERLAY : VIDEO_OUT_BUS_TYPE_MAIN; + const int handle = sceVideoOutOpen(255, bus, 0, nullptr); + auto cfg = VideoOutDriver::Get().GetConfig(handle); + for (const auto [index, mode, error] : {std::array{16, 1, VIDEO_OUT_ERROR_INVALID_INDEX}, {0, 1, VIDEO_OUT_ERROR_INVALID_INDEX}, {-2, 7, VIDEO_OUT_ERROR_INVALID_VALUE}}) { + check(sceVideoOutSubmitEopFlip(handle, index, mode, 0, &token) == error && token == 0x5a5a5a5a, "invalid EOP parameters changed the token"); + } + std::vector allocation(65536 + 65535); + const auto storage = alignedBuffer(allocation); + VideoOutBuffers buffer{}; + buffer.data = storage.data(); + VideoOutBufferAttribute2 attribute{}; + attribute.width = 64; + attribute.height = 64; + attribute.pixel_format = 0x8000000000000000ull; + check(sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr) == 0, "EOP-test buffer failed to register"); + alignas(std::uint32_t) std::array output; + output.fill(std::byte{0x5a}); + constexpr std::int64_t argument = -0x1234567890abcdefLL; + check(sceVideoOutSubmitEopFlip(handle, 0, 1, argument, reinterpret_cast(output.data() + 1)) == 0, "EOP flip reservation failed"); + std::memcpy(&token, output.data() + 1, sizeof(token)); + check(token != 0 && token <= 0x0fffffff && output.front() == std::byte{0x5a} && std::all_of(output.begin() + 5, output.end(), [](auto byte) { return byte == std::byte{0x5a}; }), "EOP token overwrote its DWORD output"); + VideoOutFlipStatus status{}; + check(sceVideoOutGetFlipStatus(handle, &status) == 0 && status.flipPendingNum == 1 && status.count == 0, "EOP reservation presented before its completion packet"); + alignas(8) std::uint64_t label = 0; + const auto address = reinterpret_cast(&label); + std::array release{0xc0064900u, 0x06200504u, 0x42010000u, static_cast(address), static_cast(address >> 32u), 1u, 0u, token}; + auto rejected = release; + rejected[0] = 0xc0054900u; + Packet malformed{rejected.data(), static_cast(rejected.size()), 0, {}}; + expectFailure([&] { sceAgcDriverSubmitDcb(&malformed); }); + check(sceVideoOutGetFlipStatus(handle, &status) == 0 && status.flipPendingNum == 1 && status.count == 0, "rejected packet consumed an EOP reservation"); + const auto submit = [](auto& words) { + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "EOP completion packet was rejected"); + }; + std::vector duplicate(release.begin(), release.end()); + duplicate.insert(duplicate.end(), release.begin(), release.end()); + Packet twice{duplicate.data(), static_cast(duplicate.size()), 0, {}}; + check(expectFailure([&] { sceAgcDriverSubmitDcb(&twice); }).find("token repeated") != std::string::npos, "a submission consumed the same EOP token twice"); + const std::uint64_t bits = static_cast(argument); + std::vector completion{0xc0027900u, 0x342u, 0xc7010107u, static_cast(bus), 0xc0043700u, 0x06010000u, 0xc343u, 0u, static_cast(bits), static_cast(bits >> 32u)}; + completion.push_back(0x80004900u); + const std::array nop{0xc0061000u, 0x02000000u, 0u, 0u, 0u, 0u, token, 0u}; + completion.insert(completion.end(), nop.begin(), nop.end()); + completion.insert(completion.end(), release.begin(), release.end()); + submit(completion); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + do { + check(sceVideoOutGetFlipStatus(handle, &status) == 0, "EOP flip status failed"); + if (status.count == 1 && status.flipPendingNum == 0) break; + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } while (std::chrono::steady_clock::now() < deadline); + check(status.count == 1 && status.flipPendingNum == 0 && status.currentBuffer == 0 && status.flipArg == argument && std::atomic_ref(label).load() == 1, "EOP packet did not finish its label and reserved flip"); + release[5] = 2; + submit(release); + AgcDriverWaitIdle_nid_postfix(); + check(sceVideoOutGetFlipStatus(handle, &status) == 0 && status.count == 1 && status.flipPendingNum == 0 && std::atomic_ref(label).load() == 2, "ordinary context ID replay changed the flip or lost its label"); + std::array pending; + pending.fill(0x5a5a5a5a); + std::array outcomes{}; + std::vector callers; + for (std::size_t i = 0; i < pending.size(); ++i) callers.emplace_back([&, i] { outcomes[i] = sceVideoOutSubmitEopFlip(handle, VIDEO_OUT_BUFFER_INDEX_BLACK, 1, 0, &pending[i]); }); + for (auto& caller : callers) caller.join(); + std::vector abandoned; + for (std::size_t i = 0; i < pending.size(); ++i) { + if (outcomes[i] == 0) abandoned.push_back(pending[i]); + else check(outcomes[i] == VIDEO_OUT_ERROR_FLIP_QUEUE_FULL && pending[i] == 0x5a5a5a5a, "concurrent EOP overflow changed its token"); + } + std::sort(abandoned.begin(), abandoned.end()); + check(abandoned.size() == VIDEO_OUT_FLIP_QUEUE_CAPACITY && abandoned.front() > token && std::adjacent_find(abandoned.begin(), abandoned.end()) == abandoned.end(), "concurrent EOP reservations exceeded capacity or reused tokens"); + std::uint32_t unchanged = 0x5a5a5a5a; + check(sceVideoOutSubmitEopFlip(handle, VIDEO_OUT_BUFFER_INDEX_BLACK, 1, 0, &unchanged) == VIDEO_OUT_ERROR_FLIP_QUEUE_FULL && unchanged == 0x5a5a5a5a, "full EOP queue accepted a reservation or changed its output"); + sceVideoOutClose(handle); + { + std::lock_guard lock(cfg->mutex); + check(cfg->flipStatus.flipPendingNum == 0, "closing kept unsubmitted EOP reservations"); + } + const int reopened = sceVideoOutOpen(255, bus, 0, nullptr); + release[7] = abandoned.front(); + release[5] = 3; + submit(release); + AgcDriverWaitIdle_nid_postfix(); + check(sceVideoOutGetFlipStatus(reopened, &status) == 0 && status.count == 0 && status.flipPendingNum == 0 && std::atomic_ref(label).load() == 3, "cancelled context ID changed the reopened port or lost its label"); + std::uint32_t fresh = 0; + check(sceVideoOutSubmitEopFlip(reopened, VIDEO_OUT_BUFFER_INDEX_BLACK, 1, 7, &fresh) == 0 && fresh > abandoned.back(), "reopening reused an EOP token"); + sceVideoOutClose(reopened); + LibcRunShutdown_nid_postfix(); +} + void testOneDevice() { const int handle = sceVideoOutOpen(255, 0, 0, nullptr); auto cfg = VideoOutDriver::Get().GetConfig(handle); @@ -722,11 +1035,18 @@ int main(int argc, char** argv) { else if (argc == 2 && std::string(argv[1]) == "pacing") testReleaseVblank(); else if (argc == 2 && std::string(argv[1]) == "aftergpu") testFlipAfterGpuWork(); else if (argc == 2 && std::string(argv[1]) == "unavailable") testUnavailable(); + else if (argc == 2 && std::string(argv[1]) == "labels") testLabels(true); + else if (argc == 2 && std::string(argv[1]) == "labels_main") testLabels(false); else if (argc == 2 && std::string(argv[1]) == "onedevice") testOneDevice(); else if (argc == 2 && std::string(argv[1]) == "unopened") testUnopenedEventRegistration(); + else if (argc == 2 && std::string(argv[1]) == "eop_aftergpu") testFlipAfterGpuWork(true); + else if (argc == 2 && std::string(argv[1]) == "eop") testEop(true); + else if (argc == 2 && std::string(argv[1]) == "eop_main") testEop(false); 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 if (argc == 2 && std::string(argv[1]) == "cpuflip") testCpuFlipOrder(); else testLifetime(argc == 2 && std::string(argv[1]) == "reopen"); std::puts("VideoOut flip tests passed"); return 0; @@ -734,7 +1054,9 @@ int main(int argc, char** argv) { std::fprintf(stderr, "%s\n", error.what()); try { LibcRunShutdown_nid_postfix(); } catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); } - if (std::string(error.what()).find("Vulkan support") != std::string::npos && !std::getenv("ANYPS5_REQUIRE_DISPLAY")) { + const std::string what = error.what(); + const bool noVulkanWindow = what.find("Vulkan support") != std::string::npos || (what.starts_with("DisplayWindow: ") && what.find("Vulkan") != std::string::npos); + if (noVulkanWindow && !std::getenv("ANYPS5_REQUIRE_DISPLAY")) { std::printf("skipped, no display or Vulkan device: %s\n", error.what()); return 77; } diff --git a/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp index a5105da79f..b98c07e1be 100644 --- a/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp +++ b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -28,11 +29,16 @@ namespace { constexpr std::uint32_t Width = 3840; constexpr std::uint32_t Height = 2160; -constexpr std::uint32_t Scale = 4; -constexpr std::uint32_t DumpWidth = Width / Scale; -constexpr std::uint32_t DumpHeight = Height / Scale; -constexpr std::size_t DumpBytes = 54 + static_cast(DumpWidth) * DumpHeight * 4; +std::uint32_t scaleFromEnvironment() { + const char* value = std::getenv("APS5_DUMP_SCALE"); + return value != nullptr ? static_cast(std::strtoul(value, nullptr, 10)) : 4u; +} +const std::uint32_t Scale = scaleFromEnvironment(); +const std::uint32_t DumpWidth = Width / Scale; +const std::uint32_t DumpHeight = Height / Scale; +const std::size_t DumpBytes = 54 + static_cast(DumpWidth) * DumpHeight * 4; constexpr int Frames = 4; +bool WaitMode = false; void check(bool condition, const char* reason) { if (!condition) throw std::runtime_error(reason); @@ -137,6 +143,13 @@ void testDumpQueue() { check(sceVideoOutRegisterBuffers2(handle, 1, 1, &linearBuffer, 1, &linearAttribute, 0, nullptr) == 0, "linear buffer registration failed"); const HeldDump first(0); const HeldDump last(Frames - 1); + std::vector firstDump; + std::thread reader; + const auto started = std::chrono::steady_clock::now(); + if (WaitMode) reader = std::thread([&] { + std::this_thread::sleep_for(std::chrono::seconds(2)); + firstDump = first.Release(); + }); std::exception_ptr failure; try { for (int frame = 0; frame < Frames; ++frame) { @@ -149,8 +162,10 @@ void testDumpQueue() { } catch (...) { failure = std::current_exception(); } - const auto firstDump = first.Release(); + if (WaitMode) reader.join(); + else firstDump = first.Release(); if (failure) std::rethrow_exception(failure); + if (WaitMode) check(std::chrono::steady_clock::now() - started >= std::chrono::milliseconds(1500), "the presenter did not wait for the writer"); checkDump(firstDump, tiledPixels); const auto lastDump = last.Release(); checkDump(lastDump, linearPixels); @@ -164,11 +179,13 @@ void testDumpQueue() { } -int main() { +int main(int argc, char** argv) { + WaitMode = argc > 1 && std::strcmp(argv[1], "wait") == 0; VideoOutTestEnvironment environment; try { + if (WaitMode) check(Scale == 1, "the wait test needs APS5_DUMP_SCALE=1"); testDumpQueue(); - std::puts("VideoOut frame dump queue test passed"); + std::puts(WaitMode ? "VideoOut frame dump wait test passed" : "VideoOut frame dump queue test passed"); return 0; } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); diff --git a/core/libs/prx/libSceVideoOut/tests/HdrPresent.cpp b/core/libs/prx/libSceVideoOut/tests/HdrPresent.cpp new file mode 100644 index 0000000000..4acc208218 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/HdrPresent.cpp @@ -0,0 +1,192 @@ +#include "prx/libSceVideoOut/include/VideoOutDriver.hpp" +#include "prx/libSceVideoOut/include/Buffer.hpp" +#include "prx/libSceVideoOut/include/Output.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "tests/VideoOutTestEnvironment.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef main + +namespace { + +constexpr int SkipReturnCode = 77; +constexpr std::uint64_t Bgr10Pq = 0x8100070400000000ull; +constexpr std::uint64_t Rgb10Pq = 0x8100070422000000ull; +constexpr std::uint64_t Bgra8 = 0x8000000000000000ull; +constexpr std::uint32_t Width = 259; +constexpr std::uint32_t Height = 137; +constexpr std::uint32_t ClearWord = 600u | (500u << 10u) | (300u << 20u) | (3u << 30u); + +void check(bool condition, const std::string& reason) { + if (!condition) throw std::runtime_error(reason); +} + +std::span alignedBuffer(std::vector& allocation, std::size_t bytes) { + const auto address = reinterpret_cast(allocation.data()); + const auto aligned = (address + 65535u) & ~static_cast(65535u); + return {allocation.data() + (aligned - address), bytes}; +} + +void fillPq(std::span storage) { + for (std::size_t i = 0; i + 4 <= storage.size(); i += 4) { + const auto code = static_cast((i / 4) % 1024u); + const std::uint32_t word = code | (((code * 7u) % 1024u) << 10u) | ((1023u - code) << 20u) | (3u << 30u); + std::memcpy(storage.data() + i, &word, 4); + } +} + +void flip(int handle, const std::shared_ptr& cfg, int index) { + std::uint64_t target = 0; + { + std::lock_guard lock(cfg->mutex); + target = cfg->flipStatus.count + 1; + } + check(sceVideoOutSubmitFlip(handle, index, 1, index) == 0, "flip submission failed"); + std::unique_lock lock(cfg->mutex); + check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return cfg->failure != nullptr || cfg->flipStatus.count == target; }), "presentation did not complete"); + if (cfg->failure) std::rethrow_exception(cfg->failure); + check(cfg->flipStatus.currentBuffer == index && cfg->flipStatus.flipPendingNum == 0, "presentation status is wrong"); +} + +std::vector> readDumps(std::size_t bytes) { + std::vector> dumps; + for (int index = 0; index < 64; ++index) { + char name[32]; + std::snprintf(name, sizeof(name), "frame_%03d.bmp", index); + if (!std::filesystem::exists(name)) continue; + std::ifstream file(name, std::ios::binary); + std::vector bmp((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + if (bmp.size() != 54 + bytes) continue; + dumps.emplace_back(bytes); + std::memcpy(dumps.back().data(), bmp.data() + 54, bytes); + } + return dumps; +} + +void findDump(const std::vector>& dumps, const std::vector& expected, const char* what) { + int best = 256; + for (const auto& dump : dumps) { + int worst = 0; + for (std::size_t i = 0; i < expected.size() && worst <= best; ++i) worst = std::max(worst, std::abs(std::to_integer(expected[i]) - std::to_integer(dump[i]))); + best = std::min(best, worst); + } + std::printf("%s: largest difference from the CPU conversion %d\n", what, best); + check(best <= 1, std::string(what) + " has no frame dump within one level of the CPU conversion"); +} + +int run() { +#ifdef _WIN32 + _putenv_s("APS5_DUMP_FRAMES", "64"); + _putenv_s("APS5_DUMP_SCALE", "1"); +#else + setenv("APS5_DUMP_FRAMES", "64", 1); + setenv("APS5_DUMP_SCALE", "1", 1); +#endif + VideoOutTestEnvironment environment; + int handle = 0; + try { + handle = sceVideoOutOpen(255, 0, 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 SkipReturnCode; + } + check(handle > 0, "sceVideoOutOpen failed"); + auto cfg = VideoOutDriver::Get().GetConfig(handle); + const std::size_t tiledBytes = static_cast((Width + 127u) / 128u) * ((Height + 127u) / 128u) * 65536u; + const std::size_t linearBytes = static_cast(Width) * Height * 4u; + std::vector pqAllocation(tiledBytes + 65535u), linearAllocation(linearBytes + 65535u), sdrAllocation(tiledBytes + 65535u), clearAllocation(tiledBytes + 65535u); + const auto pq = alignedBuffer(pqAllocation, tiledBytes); + const auto linear = alignedBuffer(linearAllocation, linearBytes); + const auto sdr = alignedBuffer(sdrAllocation, tiledBytes); + const auto cleared = alignedBuffer(clearAllocation, tiledBytes); + fillPq(pq); + fillPq(linear); + std::memset(sdr.data(), 0x40, sdr.size()); + std::vector keys(4096, 0x20); + + VideoOutBufferAttribute2 attribute{}; + VideoOutBuffers buffer{pq.data(), nullptr, {nullptr, nullptr}}; + sceVideoOutSetBufferAttribute2(&attribute, Bgr10Pq, 0, Width, Height, 0, 0, 0); + check(sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr) == 0, "the tiled PQ buffer was not registered"); + buffer = {linear.data(), nullptr, {nullptr, nullptr}}; + sceVideoOutSetBufferAttribute2(&attribute, Rgb10Pq, 1, Width, Height, 0, 0, 0); + check(sceVideoOutRegisterBuffers2(handle, 1, 1, &buffer, 1, &attribute, 0, nullptr) == 0, "the linear PQ buffer was not registered"); + buffer = {sdr.data(), nullptr, {nullptr, nullptr}}; + sceVideoOutSetBufferAttribute2(&attribute, Bgra8, 0, Width, Height, 0, 0, 0); + check(sceVideoOutRegisterBuffers2(handle, 2, 2, &buffer, 1, &attribute, 0, nullptr) == 0, "the SDR buffer was not registered"); + buffer = {cleared.data(), keys.data(), {nullptr, nullptr}}; + sceVideoOutSetBufferAttribute2(&attribute, Rgb10Pq, 0, Width, Height, 0, 0x208, ClearWord); + check(sceVideoOutRegisterBuffers2(handle, 3, 3, &buffer, 1, &attribute, VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED, nullptr) == 0, "the DCC PQ buffer was not registered"); + + const auto present = [&](int index) { flip(handle, cfg, index); }; + const auto host = AgcDriver::CurrentHostHdr(); + VideoOutOutputStatus status{}; + check(sceVideoOutGetOutputStatus(handle, &status) == 0 && status.dynamicRange == (host.available ? 2u : 1u), "the output status does not follow the host's HDR output"); + if (host.available) { + for (const int index : {0, 1, 3, -1}) { + present(index); + check(AgcDriverPresentingHdr10_nid_postfix(), "PQ display buffer " + std::to_string(index) + " did not reach an HDR10 swapchain"); + } + present(2); + check(!AgcDriverPresentingHdr10_nid_postfix(), "the SDR buffer did not bring the SDR swapchain back"); + present(0); + check(AgcDriverPresentingHdr10_nid_postfix(), "the HDR10 swapchain did not come back for a PQ buffer"); + AgcDriverSetHostDisplayHdr_nid_postfix(AgcDriver::DisplayHdrFromOutput(true, 600.0f, 0.05f, 350.0f)); + present(0); + check(AgcDriverPresentingHdr10_nid_postfix() && AgcDriver::CurrentHostHdr().display.maxLuminance == 600.0f, "a new monitor luminance left the HDR10 swapchain"); + std::printf("HDR10 presentation passed (peak %.1f nits)\n", host.display.maxLuminance); + } else { + std::printf("HDR output is unavailable on this host (display in HDR: %s, surface offers HDR10: %s): HDR10 presentation not exercised\n", host.display.hdr ? "yes" : "no", host.surfaceHdr10 ? "yes" : "no"); + } + AgcDriverSetHostDisplayHdr_nid_postfix({}); + AgcDriverNoteReportedToneMapPeak_nid_postfix(437.0f); + for (const int index : {0, 1, 3}) { + present(index); + present(index); + check(!AgcDriverPresentingHdr10_nid_postfix(), "PQ display buffer " + std::to_string(index) + " reached an HDR10 swapchain without HDR output"); + } + const auto whiteNits = AgcDriver::PqToSdrWhiteNits(AgcDriver::CurrentHostHdr()); + check(whiteNits == 437.0f, "the SDR conversion does not use the peak the title was told"); + const auto tiledExpected = AgcDriver::DecodeDisplayBuffer({reinterpret_cast(pq.data()), Bgr10Pq, Width, Height}, pq); + const auto linearExpected = AgcDriver::DecodeDisplayBuffer({reinterpret_cast(linear.data()), Rgb10Pq, Width, Height, 1, 0}, linear); + const auto clearPixel = AgcDriver::PqToSdrPixel(ClearWord, true, whiteNits); + std::vector clearExpected(linearBytes); + for (std::size_t i = 0; i < clearExpected.size(); ++i) clearExpected[i] = static_cast(clearPixel[i % 4]); + for (int set = 0; set < 4; ++set) sceVideoOutUnregisterBuffers(handle, set); + sceVideoOutClose(handle); + LibcRunShutdown_nid_postfix(); + const auto dumps = readDumps(linearBytes); + findDump(dumps, tiledExpected, "SDR presentation of the tiled PQ buffer at 437 nits (GPU detile conversion)"); + findDump(dumps, linearExpected, "SDR presentation of the linear PQ buffer at 437 nits (CPU conversion)"); + findDump(dumps, clearExpected, "SDR presentation of the DCC-cleared PQ buffer at 437 nits (clear value conversion)"); + return 0; +} + +} + +int main() { + try { + return run(); + } catch (const std::exception& error) { + std::fprintf(stderr, "FAIL: %s\n", error.what()); + try { LibcRunShutdown_nid_postfix(); } + catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); } + return 1; + } +} diff --git a/core/libs/prx/libSceVideoOut/tests/HdrProbe.cpp b/core/libs/prx/libSceVideoOut/tests/HdrProbe.cpp new file mode 100644 index 0000000000..68d5afc3ce --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/HdrProbe.cpp @@ -0,0 +1,84 @@ +#include "prx/libSceVideoOut/include/HostHdrProbe.hpp" +#include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp" +#include +#include +#include + +#ifdef _WIN32 +#include "SDL_syswm.h" +#include +#include +#endif + +#undef main + +namespace { + +#ifdef _WIN32 +bool monitorReportsHdr(SDL_Window* window) { + SDL_SysWMinfo info; + SDL_VERSION(&info.version); + if (SDL_GetWindowWMInfo(window, &info) != SDL_TRUE || info.subsystem != SDL_SYSWM_WINDOWS) return false; + const HMONITOR monitor = MonitorFromWindow(info.info.win.window, MONITOR_DEFAULTTONEAREST); + IDXGIFactory1* factory = nullptr; + if (FAILED(CreateDXGIFactory1(__uuidof(IDXGIFactory1), reinterpret_cast(&factory)))) return false; + bool hdr = false; + IDXGIAdapter1* adapter = nullptr; + for (UINT adapterIndex = 0; !hdr && SUCCEEDED(factory->EnumAdapters1(adapterIndex, &adapter)); ++adapterIndex) { + IDXGIOutput* output = nullptr; + for (UINT outputIndex = 0; !hdr && SUCCEEDED(adapter->EnumOutputs(outputIndex, &output)); ++outputIndex) { + DXGI_OUTPUT_DESC description{}; + IDXGIOutput6* output6 = nullptr; + DXGI_OUTPUT_DESC1 description1{}; + if (SUCCEEDED(output->GetDesc(&description)) && description.Monitor == monitor && + SUCCEEDED(output->QueryInterface(__uuidof(IDXGIOutput6), reinterpret_cast(&output6))) && SUCCEEDED(output6->GetDesc1(&description1))) + hdr = description1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; + if (output6 != nullptr) output6->Release(); + output->Release(); + } + adapter->Release(); + } + factory->Release(); + return hdr; +} +#endif + +} + +int main() { + if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { + std::printf("skipped, no SDL video driver: %s\n", SDL_GetError()); + return 77; + } + SDL_Window* window = SDL_CreateWindow("HDR probe test", 0, 0, 64, 64, SDL_WINDOW_HIDDEN); + if (window == nullptr) { + std::printf("skipped, no SDL window: %s\n", SDL_GetError()); + SDL_QuitSubSystem(SDL_INIT_VIDEO); + return 77; + } + const std::string driver = SDL_GetCurrentVideoDriver() != nullptr ? SDL_GetCurrentVideoDriver() : "unknown"; +#ifdef _WIN32 + if (monitorReportsHdr(window)) { + std::printf("skipped, the window's monitor reports HDR to Windows (driver %s)\n", driver.c_str()); + SDL_DestroyWindow(window); + SDL_QuitSubSystem(SDL_INIT_VIDEO); + return 77; + } +#endif + AgcDriverSetSurfaceHdr10_nid_postfix(true); + int result = 0; + { + HostHdrProbe probe; + probe.Poll(window); + probe.Poll(window); + const auto host = AgcDriver::CurrentHostHdr(); + if (host.display.hdr || host.available) { + std::fprintf(stderr, "FAIL: a window without a Windows HDR monitor report reads as HDR although only the surface offers HDR10 (driver %s)\n", driver.c_str()); + result = 1; + } + } + SDL_DestroyWindow(window); + SDL_QuitSubSystem(SDL_INIT_VIDEO); + if (result == 0) std::printf("HDR probe tests passed (driver %s)\n", driver.c_str()); + return result; +} diff --git a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp index daa2dad4d6..6b0c4348ed 100644 --- a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -18,6 +18,14 @@ void require(bool condition, const char* message) { std::filesystem::path root; +void setConfigPath(const std::filesystem::path& path) { +#ifdef _WIN32 + require(_wputenv_s(L"ANYPS5_INPUT_CONFIG", path.wstring().c_str()) == 0, "set ANYPS5_INPUT_CONFIG"); +#else + require(::setenv("ANYPS5_INPUT_CONFIG", path.string().c_str(), 1) == 0, "set ANYPS5_INPUT_CONFIG"); +#endif +} + void useConfig(const std::string& text) { const auto path = root / "anyps5-input.ini"; { @@ -25,11 +33,7 @@ void useConfig(const std::string& text) { file << text; require(file.good(), "write the input mapping"); } -#ifdef _WIN32 - require(_putenv_s("ANYPS5_INPUT_CONFIG", path.string().c_str()) == 0, "set ANYPS5_INPUT_CONFIG"); -#else - require(::setenv("ANYPS5_INPUT_CONFIG", path.string().c_str(), 1) == 0, "set ANYPS5_INPUT_CONFIG"); -#endif + setConfigPath(path); } bool rejects(const std::string& text) { @@ -75,6 +79,41 @@ void settingsReject() { require(rejects("MouseSensitivity = 1\nMouseSensitivity = 2\n"), "a setting given twice is rejected"); } +void performanceOverlayBinding() { + useConfig("Cross = KEY:F\n"); + bool onF10 = false; + for (const auto& binding : Pad::LoadInputMapping().bindings) onF10 = onF10 || (binding.control == Pad::InputControl::TogglePerformanceOverlay && binding.key == SDL_SCANCODE_F10); + require(onF10, "F10 toggles the performance overlay by default"); + useConfig("togglePerformanceOverlay = KEY:F9\n"); + bool onF9 = false; + bool stillOnF10 = false; + for (const auto& binding : Pad::LoadInputMapping().bindings) { + if (binding.control != Pad::InputControl::TogglePerformanceOverlay) continue; + onF9 = onF9 || binding.key == SDL_SCANCODE_F9; + stillOnF10 = stillOnF10 || binding.key == SDL_SCANCODE_F10; + } + require(onF9 && !stillOnF10, "the performance overlay toggle moves to another key"); + require(rejects("TogglePerformanceOverlay = MOUSE:Left\n"), "the performance overlay toggle refuses a mouse button"); +} + +void screenshotBinding() { + useConfig("Cross = KEY:F\n"); + bool onF12 = false; + for (const auto& binding : Pad::LoadInputMapping().bindings) onF12 = onF12 || (binding.control == Pad::InputControl::Screenshot && binding.key == SDL_SCANCODE_F12); + require(onF12, "F12 takes a screenshot by default"); + useConfig("Screenshot = KEY:F9\n"); + bool onF9 = false; + bool stillOnF12 = false; + for (const auto& binding : Pad::LoadInputMapping().bindings) { + if (binding.control != Pad::InputControl::Screenshot) continue; + onF9 = onF9 || binding.key == SDL_SCANCODE_F9; + stillOnF12 = stillOnF12 || binding.key == SDL_SCANCODE_F12; + } + require(onF9 && !stillOnF12, "the screenshot key moves to another key"); + require(rejects("Screenshot = MOUSE:Left\n"), "the screenshot key refuses a mouse button"); + require(rejects("Screenshot = WHEEL:Up\n"), "the screenshot key refuses the mouse wheel"); +} + void mouseLookDefault() { useConfig("Cross = KEY:F\n"); const auto settings = Pad::LoadInputMapping().settings; @@ -105,6 +144,104 @@ void mouseLookReject() { require(rejects("MouseDecayMs = 20\nmousedecayms = 30\n"), "a mouse setting given twice is rejected"); } +void nonAsciiPaths() { + const std::u8string name = u8"anyps5-input-\u00fc-\u65e5\u672c-" + std::u8string(root.filename().u8string()); + const auto directory = root / std::filesystem::path(name); + std::filesystem::create_directories(directory); + const auto file = directory / "anyps5-input.ini"; + { + std::ofstream stream(file, std::ios::binary | std::ios::trunc); + stream << "MouseSensitivity = 3\n"; + require(stream.good(), "write the input mapping in a non-ASCII directory"); + } + setConfigPath(file); + require(Pad::LoadInputMapping().settings.mouseSensitivity == 3.0, "ANYPS5_INPUT_CONFIG may name a path with non-ASCII characters"); + { + std::ofstream stream(file, std::ios::binary | std::ios::trunc); + stream << "MouseSensitivity = fast\n"; + } + std::string message; + try { + Pad::LoadInputMapping(); + } catch (const std::runtime_error& error) { + message = error.what(); + } + const auto utf8Name = std::string(reinterpret_cast(name.data()), name.size()); + require(message.find(utf8Name) != std::string::npos, "an error names the path in UTF-8"); + setConfigPath(directory / "missing.ini"); + message.clear(); + try { + Pad::LoadInputMapping(); + } catch (const std::runtime_error& error) { + message = error.what(); + } + require(message.find(utf8Name) != std::string::npos, "a missing file names the path in UTF-8"); +} + +std::size_t stickModifierBindings(const Pad::InputConfiguration& configuration) { + std::size_t count = 0; + for (const auto& binding : configuration.bindings) count += binding.control == Pad::InputControl::StickModifier ? 1 : 0; + return count; +} + +void stickModifier() { + useConfig("Cross = KEY:F\n"); + auto configuration = Pad::LoadInputMapping(); + require(stickModifierBindings(configuration) == 0, "no key is a stick modifier by default"); + require(configuration.settings.stickModifierScale == 0.5, "the stick modifier scale defaults to 0.5"); + useConfig("StickModifier = KEY:Z\nstickmodifier = MOUSE:X2\nStickModifierScale = 0.35\n"); + configuration = Pad::LoadInputMapping(); + require(stickModifierBindings(configuration) == 2, "StickModifier takes a key and a mouse button"); + require(configuration.settings.stickModifierScale == 0.35, "the stick modifier scale is read"); + require(rejects("StickModifier = WHEEL:Up\n"), "a wheel direction cannot be a stick modifier"); + require(rejects("StickModifierScale = 0.04\n"), "a stick modifier scale below 0.05 is rejected"); + require(rejects("StickModifierScale = 1.5\n"), "a stick modifier scale above 1 is rejected"); + require(rejects("StickModifierScale = half\n"), "a non-numeric stick modifier scale is rejected"); + require(rejects("StickModifierScale = 0.5\nStickModifierScale = 0.6\n"), "a stick modifier scale given twice is rejected"); +} + +void keyboardRamp() { + useConfig("Cross = KEY:F\n"); + require(Pad::LoadInputMapping().settings.keyboardRampMs == 0.0, "the keyboard ramp defaults to 0"); + useConfig("KeyboardRampMs = 120\n"); + require(Pad::LoadInputMapping().settings.keyboardRampMs == 120.0, "the keyboard ramp is read"); + useConfig("keyboardrampms = 0\n"); + require(Pad::LoadInputMapping().settings.keyboardRampMs == 0.0, "a ramp of 0 is accepted"); + require(rejects("KeyboardRampMs = -1\n"), "a negative keyboard ramp is rejected"); + require(rejects("KeyboardRampMs = 2001\n"), "a keyboard ramp above 2000 is rejected"); + require(rejects("KeyboardRampMs = slow\n"), "a non-numeric keyboard ramp is rejected"); + require(rejects("KeyboardRampMs = 50\nKeyboardRampMs = 60\n"), "a keyboard ramp given twice is rejected"); +} + +void triggerRamp() { + useConfig("Cross = KEY:F\n"); + require(Pad::LoadInputMapping().settings.triggerRampMs == 0.0, "the trigger ramp defaults to 0"); + useConfig("TriggerRampMs = 80\n"); + require(Pad::LoadInputMapping().settings.triggerRampMs == 80.0, "the trigger ramp is read"); + require(rejects("TriggerRampMs = -5\n"), "a negative trigger ramp is rejected"); + require(rejects("TriggerRampMs = 2500\n"), "a trigger ramp above 2000 is rejected"); + require(rejects("TriggerRampMs = 10\ntriggerrampms = 20\n"), "a trigger ramp given twice is rejected"); +} + +std::size_t toggleMouseBindings(const Pad::InputConfiguration& configuration, SDL_Scancode key, Pad::MouseButton button) { + std::size_t count = 0; + for (const auto& binding : configuration.bindings) { + if (binding.control == Pad::InputControl::ToggleMouse && binding.key == key && binding.mouseButton == button) ++count; + } + return count; +} + +void toggleMouseDefaults() { + useConfig("Cross = KEY:F\n"); + auto configuration = Pad::LoadInputMapping(); + require(toggleMouseBindings(configuration, SDL_SCANCODE_F7, Pad::MouseButton::None) == 1, "F7 toggles the mouse by default"); + require(toggleMouseBindings(configuration, SDL_SCANCODE_UNKNOWN, Pad::MouseButton::Middle) == 1, "the middle button toggles the mouse by default"); + useConfig("ToggleMouse = MOUSE:Right\n"); + configuration = Pad::LoadInputMapping(); + require(toggleMouseBindings(configuration, SDL_SCANCODE_F7, Pad::MouseButton::None) == 0, "a ToggleMouse line replaces F7"); + require(toggleMouseBindings(configuration, SDL_SCANCODE_UNKNOWN, Pad::MouseButton::Right) == 1, "a ToggleMouse line adds its own binding"); +} + void deadzoneOffMatchesRawSticks() { for (const std::int16_t axis : {std::int16_t{-32768}, std::int16_t{-20000}, std::int16_t{-1}, std::int16_t{0}, std::int16_t{1}, std::int16_t{1234}, std::int16_t{32767}}) { const auto stick = Pad::StickWithDeadzone(axis, axis, 0); @@ -138,9 +275,16 @@ int main() try { settingsDefault(); settingsParse(); settingsReject(); + performanceOverlayBinding(); + screenshotBinding(); mouseLookDefault(); mouseLookParse(); mouseLookReject(); + stickModifier(); + keyboardRamp(); + triggerRamp(); + toggleMouseDefaults(); + nonAsciiPaths(); deadzoneOffMatchesRawSticks(); deadzoneShapesSticks(); std::filesystem::remove_all(root); diff --git a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp index 93715508cb..e4d9c6892f 100644 --- a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp @@ -4,10 +4,12 @@ #include #include +#include #include #include #include #include +#include #include #include @@ -182,6 +184,62 @@ void testSmoothing() { check(early > 0.5 * 0.55 && early < 0.5 * 0.72, "smoothing reaches about 63 percent after one time constant"); } +void testSlowReportingMouse() { + const double speed = 0.5 * Pad::MouseFullTiltCountsPerSecond; + const double expected = expectedDeflection(defaults().settings, 0.5); + for (const double reportHz : {125.0, 250.0, 1000.0}) { + std::vector events{PadSimulation::MouseModeAt(0.0, true)}; + PadSimulation::AppendMotion(events, 0.0, 2000.0, speed, 0.0, reportHz); + for (const double poll : {16.7, 8.3, 6.9, 1.0}) { + const auto samples = Simulate(defaults(), events, poll, 2000.0); + double low = 1.0; + double high = 0.0; + for (const auto& sample : samples) { + if (sample.ms < 1000.0) continue; + low = std::min(low, axis(sample.state.sticks[2])); + high = std::max(high, axis(sample.state.sticks[2])); + } + check(high - low < 0.125, "a " + std::to_string(reportHz) + " Hz mouse at " + std::to_string(poll) + " ms polls wavers by " + std::to_string(high - low)); + check(std::abs((high + low) / 2.0 - expected) < 0.05, "a " + std::to_string(reportHz) + " Hz mouse at " + std::to_string(poll) + " ms polls stays around the expected tilt"); + } + } +} + +void testReportRateSmoothing() { + for (const double reportHz : {125.0, 250.0, 1000.0}) { + Pad::MouseStick mouse(defaults().settings); + check(mouse.SmoothingMs() == defaults().settings.mouseSmoothingMs, "smoothing starts at the setting"); + const double periodSeconds = 1.0 / reportHz; + for (int report = 0; report < static_cast(reportHz); ++report) { + mouse.Move(6, 0); + mouse.Advance(periodSeconds); + } + check(std::abs(mouse.ReportRate() - reportHz) < 0.1 * reportHz, "measured " + std::to_string(mouse.ReportRate()) + " reports per second for a " + std::to_string(reportHz) + " Hz mouse"); + const double smoothing = mouse.SmoothingMs(); + check(std::abs(smoothing - std::max(10.0, 3000.0 / reportHz)) < 0.15 * smoothing, "smoothing " + std::to_string(smoothing) + " ms for a " + std::to_string(reportHz) + " Hz mouse"); + } + Pad::MouseStick slow(defaults().settings); + for (int step = 0; step < 40; ++step) { + slow.Move(30, 0); + slow.Advance(0.025); + } + check(std::abs(slow.ReportRate() - 40.0) < 2.0 && slow.SmoothingMs() == Pad::MouseSmoothingLimitMs, "smoothing for very slow reports stops at 30 ms"); + auto off = defaults().settings; + off.mouseSmoothingMs = 0.0; + Pad::MouseStick unsmoothed(off); + for (int report = 0; report < 125; ++report) { + unsmoothed.Move(6, 0); + unsmoothed.Advance(0.008); + } + check(unsmoothed.SmoothingMs() == 0.0, "a smoothing of 0 stays off for a slow mouse"); + Pad::MouseStick single(defaults().settings); + for (int report = 0; report < 125; ++report) { + single.Move(1, 0); + single.Advance(0.008); + } + check(single.ReportRate() == 0.0 && single.SmoothingMs() == defaults().settings.mouseSmoothingMs, "single-count reports are not taken for a slow mouse"); +} + std::vector waveMotion(double base, double swing) { constexpr double Pi = 3.14159265358979323846; std::vector events{PadSimulation::MouseModeAt(0.0, true)}; @@ -254,6 +312,108 @@ void testKeyboardSticksUnchanged() { check(samples[44].state.sticks[0] == 128, "released keys center the stick"); } +Pad::InputConfiguration withModifier(double scale) { + auto configuration = defaults(); + configuration.bindings.push_back({SDL_SCANCODE_Z, Pad::MouseButton::None, Pad::InputControl::StickModifier}); + configuration.bindings.push_back({SDL_SCANCODE_UNKNOWN, Pad::MouseButton::X2, Pad::InputControl::StickModifier}); + configuration.settings.stickModifierScale = scale; + return configuration; +} + +void testStickModifier() { + const std::vector events{ + At(0.0, KeyEvent(SDL_SCANCODE_W, true)), + At(10.0, KeyEvent(SDL_SCANCODE_Z, true)), + At(20.0, KeyEvent(SDL_SCANCODE_D, true)), + At(30.0, KeyEvent(SDL_SCANCODE_A, true)), + At(40.0, KeyEvent(SDL_SCANCODE_Z, false)), + At(50.0, KeyEvent(SDL_SCANCODE_A, false)), + At(60.0, KeyEvent(SDL_SCANCODE_D, false)), + At(70.0, PadSimulation::ButtonEvent(SDL_BUTTON_X2, true)), + At(80.0, KeyEvent(SDL_SCANCODE_S, true)), + At(90.0, PadSimulation::ButtonEvent(SDL_BUTTON_X2, false))}; + const auto samples = Simulate(withModifier(0.5), events, 1.0, 100.0); + check(samples[4].state.sticks[1] == 0, "keys tilt fully before the modifier is held"); + check(samples[14].state.sticks[1] == 64 && samples[14].state.sticks[0] == 128, "the modifier halves the tilt of a held key"); + check(samples[24].state.sticks[0] == 192 && samples[24].state.sticks[1] == 64, "the modifier halves a second axis"); + check(samples[34].state.sticks[0] == 128, "opposite keys still cancel under the modifier"); + check(samples[44].state.sticks[1] == 0 && samples[44].state.sticks[0] == 128, "letting go of the modifier restores the full tilt"); + check(samples[74].state.sticks[1] == 64 && samples[74].state.sticks[0] == 128, "a modifier on a mouse button scales the held keys"); + check(samples[84].state.sticks[1] == 128, "opposite keys cancel under a mouse button modifier"); + check(samples[94].state.sticks[1] == 128, "opposite keys still cancel once the mouse button modifier is released"); + const auto alone = Simulate(withModifier(0.5), {At(0.0, KeyEvent(SDL_SCANCODE_Z, true))}, 1.0, 10.0); + check(alone.back().state.sticks[0] == 128 && alone.back().state.sticks[1] == 128, "the modifier alone moves no stick"); + const auto quarter = Simulate(withModifier(0.25), {At(0.0, KeyEvent(SDL_SCANCODE_Z, true)), At(0.0, KeyEvent(SDL_SCANCODE_D, true))}, 1.0, 10.0); + check(quarter.back().state.sticks[0] == 160, "the scale setting sets the tilt"); + const auto full = Simulate(withModifier(1.0), {At(0.0, KeyEvent(SDL_SCANCODE_Z, true)), At(0.0, KeyEvent(SDL_SCANCODE_D, true))}, 1.0, 10.0); + check(full.back().state.sticks[0] == 255, "a scale of 1 tilts fully"); +} + +int byteDistance(std::uint8_t value, int expected) { + return std::abs(static_cast(value) - expected); +} + +void testKeyboardRamp() { + auto configuration = withModifier(0.5); + configuration.settings.keyboardRampMs = 100.0; + const std::vector events{At(0.0, KeyEvent(SDL_SCANCODE_W, true)), At(150.0, KeyEvent(SDL_SCANCODE_W, false))}; + const auto samples = Simulate(configuration, events, 1.0, 400.0); + check(byteDistance(samples[20].state.sticks[1], 102) <= 1, "a ramp of 100 ms is a fifth of the way after 20 ms"); + check(byteDistance(samples[50].state.sticks[1], 64) <= 1, "a ramp of 100 ms is halfway after 50 ms"); + check(samples[119].state.sticks[1] == 0, "a ramp ends at full tilt"); + for (std::size_t index = 2; index < 119; ++index) check(samples[index].state.sticks[1] <= samples[index - 1].state.sticks[1], "the stick only moves on while the key is held"); + check(byteDistance(samples[199].state.sticks[1], 64) <= 1, "letting go ramps back at the same speed"); + for (std::size_t index = 152; index < 259; ++index) check(samples[index].state.sticks[1] >= samples[index - 1].state.sticks[1], "the stick only moves back after the key is let go"); + check(samples[259].state.sticks[1] == 128 && samples[259].state.sticks[0] == 128, "the stick ends centered"); + const std::vector modified{At(0.0, KeyEvent(SDL_SCANCODE_Z, true)), At(0.0, KeyEvent(SDL_SCANCODE_W, true)), PadSimulation::ReleaseAt(80.0)}; + const auto scaled = Simulate(configuration, modified, 1.0, 120.0); + check(byteDistance(scaled[20].state.sticks[1], 102) <= 1, "a ramp toward a modified tilt moves at the ramp speed"); + check(scaled[70].state.sticks[1] == 64, "a ramp toward a modified tilt stops at it"); + check(scaled[99].state.sticks[1] == 128, "releasing every key centers a ramped stick at once"); + const auto held = Simulate(configuration, {At(0.0, KeyEvent(SDL_SCANCODE_Z, true)), At(0.0, KeyEvent(SDL_SCANCODE_W, true))}, 1.0, 120.0); + check(held[99].state.sticks[1] == 64, "a ramp stops at the modified tilt"); +} + +void testRampKeepsAdvanceReporting() { + auto configuration = defaults(); + configuration.settings.keyboardRampMs = 100.0; + Pad::KeyboardMouse keyboardMouse(configuration); + const auto start = Pad::KeyboardMouse::Clock::now(); + keyboardMouse.Advance(start); + check(!keyboardMouse.Advance(start + std::chrono::milliseconds(5)), "nothing moves while no key is held"); + keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_W, true), start + std::chrono::milliseconds(10)); + check(keyboardMouse.Advance(start + std::chrono::milliseconds(20)), "a ramping stick asks for a new state"); + keyboardMouse.Advance(start + std::chrono::milliseconds(200)); + check(!keyboardMouse.Advance(start + std::chrono::milliseconds(210)), "a stick at its tilt asks for nothing"); +} + +void testTriggerRamp() { + const auto r2 = static_cast(Pad::PadButton::R2); + const std::vector events{At(0.0, PadSimulation::ButtonEvent(SDL_BUTTON_RIGHT, true)), At(150.0, PadSimulation::ButtonEvent(SDL_BUTTON_RIGHT, false))}; + const auto instant = Simulate(defaults(), events, 1.0, 10.0); + check(instant[0].state.analogButtonsR2 == 255 && (instant[0].state.buttons & r2) != 0, "without a ramp a press is full at once"); + auto configuration = defaults(); + configuration.settings.triggerRampMs = 100.0; + const auto samples = Simulate(configuration, events, 1.0, 400.0); + check(samples[0].state.analogButtonsR2 == 0 && (samples[0].state.buttons & r2) == 0, "a ramped press starts at 0"); + check(byteDistance(samples[50].state.analogButtonsR2, 128) <= 2 && (samples[50].state.buttons & r2) != 0, "a ramp of 100 ms is halfway after 50 ms"); + for (std::size_t index = 2; index < 119; ++index) check(samples[index].state.analogButtonsR2 >= samples[index - 1].state.analogButtonsR2, "the trigger only presses on while the button is held"); + check(samples[119].state.analogButtonsR2 == 255, "a ramp ends at a full press"); + check(byteDistance(samples[199].state.analogButtonsR2, 128) <= 2, "letting go ramps back at the same speed"); + check(samples[259].state.analogButtonsR2 == 0 && (samples[259].state.buttons & r2) == 0, "the trigger ends released"); + check(samples[119].state.analogButtonsL2 == 0, "the other trigger is left alone"); + Pad::KeyboardMouse keyboardMouse(configuration); + const auto start = Pad::KeyboardMouse::Clock::now(); + keyboardMouse.Advance(start); + keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_RIGHT, true), start); + keyboardMouse.Advance(start + std::chrono::milliseconds(30)); + PadInputState controller; + controller.analogButtonsR2 = 200; + check(keyboardMouse.Compose(controller).analogButtonsR2 == 200, "a controller trigger above the ramp is passed on"); + controller.analogButtonsR2 = 10; + check(byteDistance(keyboardMouse.Compose(controller).analogButtonsR2, 77) <= 1, "a ramp above a controller trigger is passed on"); +} + void testWheelPress() { const auto samples = Simulate(defaults(), {At(0.0, PadSimulation::WheelEvent(1))}, 1.0, 200.0); const auto up = static_cast(Pad::PadButton::Up); @@ -278,12 +438,99 @@ void testFocusLossReleasesMouse() { check(!keyboardMouse.HandleEvent(PadSimulation::FocusLostEvent(), Pad::KeyboardMouse::Clock::now()).releaseMouse, "nothing to release without capture"); } +void testShake() { + const auto samples = Simulate(defaults(), {At(0.0, KeyEvent(SDL_SCANCODE_X, true)), At(200.0, KeyEvent(SDL_SCANCODE_X, false))}, 10.0, 300.0); + const auto& first = samples[1].state; + const auto& second = samples[6].state; + check(first.hasMotion && second.hasMotion, "a held shake key reports motion"); + check(std::fabs(first.accel[0]) == Pad::ShakeAcceleration && second.accel[0] == -first.accel[0], "the shake acceleration flips every half period"); + check(second.gyro[2] == -first.gyro[2] && std::fabs(first.gyro[2]) == Pad::ShakeAngularVelocity, "the shake angular velocity flips with it"); + check(!samples[25].state.hasMotion && samples[25].state.accel == PadInputState{}.accel, "releasing the shake key stops the motion"); + Pad::KeyboardMouse keyboardMouse(defaults()); + const auto now = Pad::KeyboardMouse::Clock::now(); + keyboardMouse.Advance(now); + keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_X, true), now); + check(keyboardMouse.Advance(now + Pad::ShakeHalfPeriod), "a held shake key keeps the state changing"); +} + void testToggleRequests() { Pad::KeyboardMouse keyboardMouse(defaults()); const auto now = Pad::KeyboardMouse::Clock::now(); check(keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, true), now).toggleMouse, "middle click asks to toggle the mouse"); check(!keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, false), now).toggleMouse, "releasing it asks for nothing"); + check(keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F7, true), now).toggleMouse, "F7 asks to toggle the mouse"); + check(!keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F7, false), now).toggleMouse, "releasing F7 asks for nothing"); check(keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F11, true), now).toggleFullscreen, "F11 asks to toggle fullscreen"); + const auto overlay = keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F10, true), now); + check(overlay.togglePerformanceOverlay && !overlay.toggleFullscreen, "F10 asks to toggle the performance overlay"); + check(!keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F10, false), now).togglePerformanceOverlay, "releasing F10 asks for nothing"); + const auto screenshot = keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F12, true), now); + check(screenshot.screenshot && !screenshot.toggleFullscreen && !screenshot.togglePerformanceOverlay, "F12 asks for a screenshot"); + auto repeated = KeyEvent(SDL_SCANCODE_F12, true); + repeated.key.repeat = 1; + check(!keyboardMouse.HandleEvent(repeated, now).screenshot, "a repeated F12 asks for nothing"); + check(!keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F12, false), now).screenshot, "releasing F12 asks for nothing"); +} + +void testDefaultL2() { + const auto l2 = static_cast(Pad::PadButton::L2); + const auto r2 = static_cast(Pad::PadButton::R2); + const auto now = Pad::KeyboardMouse::Clock::now(); + for (const auto& [name, press, release] : { + std::tuple{"R", KeyEvent(SDL_SCANCODE_R, true), KeyEvent(SDL_SCANCODE_R, false)}, + std::tuple{"mouse button 4", PadSimulation::ButtonEvent(SDL_BUTTON_X1, true), PadSimulation::ButtonEvent(SDL_BUTTON_X1, false)}}) { + Pad::KeyboardMouse keyboardMouse(defaults()); + keyboardMouse.HandleEvent(press, now); + const auto held = keyboardMouse.Compose(PadInputState{}); + check((held.buttons & l2) != 0 && held.analogButtonsL2 == 255, std::string(name) + " presses L2 fully"); + check((held.buttons & r2) == 0 && held.analogButtonsR2 == 0, std::string(name) + " leaves R2 alone"); + keyboardMouse.HandleEvent(release, now); + const auto released = keyboardMouse.Compose(PadInputState{}); + check((released.buttons & l2) == 0 && released.analogButtonsL2 == 0, std::string(name) + " released lets go of L2"); + } +} + +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"); } } @@ -297,14 +544,23 @@ int main() { {"decay", testDecay}, {"max speed clamp", testMaxSpeedClamp}, {"smoothing", testSmoothing}, + {"slow reporting mouse", testSlowReportingMouse}, + {"report rate smoothing", testReportRateSmoothing}, {"poll rate independence", testPollRateIndependence}, {"vertical scale", testVerticalScale}, {"mouse mode off", testMouseModeOff}, {"keyboard sticks unchanged", testKeyboardSticksUnchanged}, + {"stick modifier", testStickModifier}, + {"keyboard ramp", testKeyboardRamp}, + {"trigger ramp", testTriggerRamp}, + {"ramp keeps advance reporting", testRampKeepsAdvanceReporting}, {"wheel press", testWheelPress}, {"wheel and keys on focus loss", testWheelFocusLoss}, {"focus loss releases the mouse", testFocusLossReleasesMouse}, + {"shake", testShake}, {"toggle requests", testToggleRequests}, + {"default L2", testDefaultL2}, + {"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/libSceVideoOut/tests/PadOutputPacing.cpp b/core/libs/prx/libSceVideoOut/tests/PadOutputPacing.cpp new file mode 100644 index 0000000000..ab624ef5a4 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/PadOutputPacing.cpp @@ -0,0 +1,89 @@ +#include "prx/libSceVideoOut/include/PadOutputPacing.hpp" + +#include +#include +#include + +namespace { + +using namespace std::chrono_literals; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "pad_output_pacing: %s\n", message); + std::exit(1); +} + +void LightBarSendsChangesOnly() { + LightBarPacer pacer; + const auto start = LightBarPacer::Clock::time_point{} + 1s; + const LightBarPacer::Color blue{0, 64, 255}; + Require(pacer.Due(blue, start), "the first colour is sent at once"); + pacer.Result(blue, start, true); + Require(!pacer.Pending(blue), "a colour that was sent is not sent again"); + Require(!pacer.Due(blue, start + 1s), "an unchanged colour stays quiet"); + const LightBarPacer::Color red{255, 0, 0}; + Require(pacer.Pending(red) && !pacer.Due(red, start + 10ms), "a new colour waits for the interval"); + Require(pacer.Due(red, start + LightBarPacer::Interval), "a new colour goes out once the interval has passed"); +} + +void LightBarRetriesFailures() { + LightBarPacer pacer; + const auto start = LightBarPacer::Clock::time_point{} + 1s; + const LightBarPacer::Color green{0, 255, 0}; + pacer.Result(green, start, false); + Require(pacer.Pending(green), "a colour SDL refused is still pending"); + Require(!pacer.Due(green, start + 1ms), "a refused colour is not retried on the next frame"); + Require(pacer.Due(green, start + LightBarPacer::Interval), "a refused colour is retried after the interval"); + pacer.Result(green, start + LightBarPacer::Interval, true); + Require(!pacer.Pending(green), "a retried colour that was sent is done"); + pacer.Reset(); + Require(pacer.Due(green, start + LightBarPacer::Interval), "a new controller gets the current colour"); +} + +void LightBarAnimationIsPaced() { + LightBarPacer pacer; + const auto start = LightBarPacer::Clock::time_point{} + 1s; + constexpr int frames = 120; + int sent = 0; + LightBarPacer::Color color{}; + for (int frame = 0; frame < frames; ++frame) { + const auto now = start + std::chrono::microseconds(frame * 8333); + color = {static_cast(frame), 32, 32}; + if (!pacer.Due(color, now)) continue; + pacer.Result(color, now, true); + ++sent; + } + Require(sent >= 30 && sent <= 34, "a colour that changes every frame at 120 Hz goes out about 33 times a second"); + const auto end = start + 1s; + Require(pacer.Pending(color), "the last colour of the animation is still owed"); + Require(pacer.Due(color, end + LightBarPacer::Interval), "the last colour of the animation is sent when the interval allows"); +} + +void TriggerEffectRetriesFailures() { + TriggerEffectCache cache; + TriggerEffectCache::Effect effect{}; + effect[0] = 0x0C; + effect[10] = 0x21; + Require(cache.NeedsSend(effect), "the first effect is sent"); + cache.Result(effect, false); + Require(cache.NeedsSend(effect), "an effect SDL refused is sent again"); + cache.Result(effect, true); + Require(!cache.NeedsSend(effect), "an effect that was sent is not repeated"); + auto changed = effect; + changed[21] = 0x26; + Require(cache.NeedsSend(changed), "a changed effect is sent"); + cache.Reset(); + Require(cache.NeedsSend(effect), "a new controller gets the current effect"); +} + +} + +int main() { + LightBarSendsChangesOnly(); + LightBarRetriesFailures(); + LightBarAnimationIsPaced(); + TriggerEffectRetriesFailures(); + std::puts("pad_output_pacing: ok"); + return 0; +} diff --git a/core/libs/prx/libSceVideoOut/tests/Telemetry.cpp b/core/libs/prx/libSceVideoOut/tests/Telemetry.cpp new file mode 100644 index 0000000000..d3fc3ce183 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/Telemetry.cpp @@ -0,0 +1,184 @@ +#include "prx/libSceVideoOut/include/DisplayWindow.hpp" +#include "prx/libSceVideoOut/include/Telemetry.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef main + +namespace { + +void check(bool condition, const std::string& reason) { + if (!condition) throw std::runtime_error(reason); +} + +std::vector readLines(const std::filesystem::path& path) { + std::ifstream input(path); + std::vector lines; + for (std::string line; std::getline(input, line);) lines.push_back(line); + return lines; +} + +long long field(const std::string& line, const std::string& name) { + const auto position = line.find("\"" + name + "\":"); + check(position != std::string::npos, "missing field " + name + " in " + line); + return std::stoll(line.substr(position + name.size() + 3)); +} + +std::filesystem::path freshDirectory(const char* name) { + auto directory = std::filesystem::temp_directory_path() / (std::string("aps5-videoout-telemetry-") + name + "-" + std::to_string(::getpid())); + std::filesystem::remove_all(directory); + std::filesystem::create_directories(directory); + return directory; +} + +void padChangeIgnoresSensors() { + VideoOutTelemetry::PadChangeFilter filter; + PadInputState state; + check(!filter.Changed(state), "idle state must not be a change"); + state.hasMotion = true; + state.accel = {1.0f, 2.0f, 3.0f}; + state.gyro = {0.5f, 0.5f, 0.5f}; + state.touch[0] = {true, 100, 200, 1}; + state.touchLeft = true; + check(!filter.Changed(state), "motion and touch must not be a change"); + state.buttons = 0x4000; + check(filter.Changed(state), "a button must be a change"); + check(filter.Changed(state), "an uncommitted change must stay a change"); + filter.Commit(state); + check(!filter.Changed(state), "a repeated state must not be a change"); + state.sticks[2] = 200; + check(filter.Changed(state), "a stick must be a change"); + filter.Commit(state); + state.analogButtonsR2 = 10; + check(filter.Changed(state), "a trigger must be a change"); +} + +void padChangeSurvivesFullQueue() { + const auto directory = freshDirectory("fullqueue"); + { + VideoOutTelemetry::Writer writer(directory.string(), 1, "test"); + VideoOutTelemetry::PadChangeFilter filter; + PadInputState state; + state.buttons = 0x4000; + check(!VideoOutTelemetry::EmitPadIfChanged(writer, filter, 5, state), "a full queue must refuse the pad line"); + check(filter.Changed(state), "a refused pad change must stay pending"); + check(writer.Dropped() == 1, "the refused pad line must be counted"); + std::this_thread::sleep_for(std::chrono::milliseconds(600)); + check(VideoOutTelemetry::EmitPadIfChanged(writer, filter, 6, state), "the retry must succeed once the queue drains"); + check(!filter.Changed(state), "a written pad change must be committed"); + } + const auto lines = readLines(directory / "videoout.jsonl"); + bool found = false; + for (const auto& line : lines) found = found || (line.find("\"ev\":\"pad\"") != std::string::npos && field(line, "n") == 6 && field(line, "buttons") == 0x4000); + check(found, "the retried pad line must be in the file"); + bool dropsLine = false; + for (const auto& line : lines) dropsLine = dropsLine || (line.find("\"ev\":\"drops\"") != std::string::npos && field(line, "dropped") == 1); + check(dropsLine, "the writer must report the refused line in a drops line"); + std::filesystem::remove_all(directory); +} + +void noEnvironmentOpensNothing() { + const auto directory = freshDirectory("noenv"); + check(VideoOutTelemetry::OpenFromEnvironment(nullptr, "test") == nullptr, "unset variable must not open a writer"); + check(VideoOutTelemetry::OpenFromEnvironment("", "test") == nullptr, "empty variable must not open a writer"); + check(std::filesystem::is_empty(directory), "no file may exist without the variable"); + std::filesystem::remove_all(directory); +} + +void queueOverflowIsCounted() { + VideoOutTelemetry::BoundedLineQueue queue(2); + check(queue.TryPush("a"), "first line must fit"); + check(queue.TryPush("b"), "second line must fit"); + check(!queue.TryPush("c"), "third line must overflow"); + check(!queue.TryPush("d"), "fourth line must overflow"); + check(queue.Dropped() == 2, "two overflows must be counted"); + check(queue.Drain().size() == 2, "the stored lines must survive"); + check(queue.TryPush("e"), "a drained queue must accept lines"); +} + +void contractPadAndPresentLines() { + const auto directory = freshDirectory("contract"); + { + auto writer = VideoOutTelemetry::OpenFromEnvironment(directory.c_str(), "abc123"); + check(writer != nullptr, "variable must open a writer"); + PadInputState state; + state.buttons = 0x4000; + writer->EmitPad(7, state); + writer->EmitPresent(30, 59.94); + } + const auto lines = readLines(directory / "videoout.jsonl"); + check(lines.size() == 3, "expected contract, pad and present, got " + std::to_string(lines.size())); + check(lines[0].find("\"ev\":\"contract\"") != std::string::npos, "contract must be first"); + check(lines[0].find("\"contract\":\"v1-pad\"") != std::string::npos, "contract must name the level"); + check(lines[0].find("\"commit\":\"abc123\"") != std::string::npos, "contract must carry the commit"); + check(lines[0].find("\"prx\":\"libSceVideoOut\"") != std::string::npos, "contract must name the prx"); + check(field(lines[0], "pid") > 0, "contract must carry the pid"); + check(lines[1].find("\"ev\":\"pad\"") != std::string::npos && field(lines[1], "n") == 7 && field(lines[1], "buttons") == 0x4000, "pad line"); + check(lines[1].find("\"sticks\":[128,128,128,128]") != std::string::npos, "pad sticks"); + check(lines[2].find("\"ev\":\"present\"") != std::string::npos && field(lines[2], "n") == 30, "present line"); + std::filesystem::remove_all(directory); +} + +int frameNumberMatchesCaption() { + const auto directory = freshDirectory("caption"); + std::filesystem::create_directories(directory / "app0" / "sce_sys"); + std::ofstream(directory / "app0" / "sce_sys" / "param.json") << R"({"titleId":"PPSA00000","localizedParameters":{"defaultLanguage":"en-US","en-US":{"titleName":"Telemetry Test"}}})"; + std::filesystem::current_path(directory); + if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { + std::printf("skipped, no display or Vulkan device: SDL video driver: %s\n", SDL_GetError()); + return 77; + } + DisplayWindow window; + try { + window.Ensure(640, 360); + } catch (const std::exception& error) { + std::printf("skipped, no display or Vulkan device: %s\n", error.what()); + SDL_QuitSubSystem(SDL_INIT_VIDEO); + return 77; + } + check(window.FrameNumber() == 0, "no frame before the first title update"); + for (std::uint64_t expected = 1; expected <= 65; ++expected) { + window.UpdateTitle(); + check(window.FrameNumber() == expected, "FrameNumber must count title updates"); + const std::string title = SDL_GetWindowTitle(window.Handle()); + const auto open = title.rfind('('); + check(open != std::string::npos && std::stoull(title.substr(open + 1)) == expected, "caption counter differs from FrameNumber: " + title); + } + window.Destroy(); + SDL_QuitSubSystem(SDL_INIT_VIDEO); + std::filesystem::current_path(directory.parent_path()); + std::filesystem::remove_all(directory); + return 0; +} + +} + +int main(int argc, char** argv) { + try { + if (argc > 1 && std::string(argv[1]) == "caption") { + const int result = frameNumberMatchesCaption(); + if (result == 0) std::printf("VideoOut caption counter test passed\n"); + return result; + } + padChangeIgnoresSensors(); + padChangeSurvivesFullQueue(); + noEnvironmentOpensNothing(); + queueOverflowIsCounted(); + contractPadAndPresentLines(); + } catch (const std::exception& error) { + std::fprintf(stderr, "FAIL: %s\n", error.what()); + return 1; + } + std::printf("VideoOut telemetry tests passed\n"); + return 0; +} diff --git a/core/libs/prx/libSceVideoOutSecondary/Export.cpp b/core/libs/prx/libSceVideoOutSecondary/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceVideoOutSecondary/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceVideoRecording.native/Export.cpp b/core/libs/prx/libSceVideoRecording.native/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceVideoRecording.native/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceVideoRecording/Export.cpp b/core/libs/prx/libSceVideoRecording/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceVideoRecording/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceVideoRecordingP/CMakeLists.txt b/core/libs/prx/libSceVideoRecordingP/CMakeLists.txt new file mode 100644 index 0000000000..ba8c08e28a --- /dev/null +++ b/core/libs/prx/libSceVideoRecordingP/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceVideoRecordingP SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceVideoRecordingP PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceVideoRecordingP) + +if(BUILD_TESTING) + add_test_executable(guest_video_recording_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoRecording.cpp) + target_include_directories(guest_video_recording_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_video_recording_tests PRIVATE libSceVideoRecordingP libc) + configure_windows_unwind(guest_video_recording_tests) + add_test(NAME guest_video_recording COMMAND guest_video_recording_tests) + if(WIN32) + set_tests_properties(guest_video_recording PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceVideoRecordingP/Export.cpp b/core/libs/prx/libSceVideoRecordingP/Export.cpp index 7d1cb61a29..c3b67583f6 100644 --- a/core/libs/prx/libSceVideoRecordingP/Export.cpp +++ b/core/libs/prx/libSceVideoRecordingP/Export.cpp @@ -2,42 +2,142 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -#include -#include -#include -#include -#include -#include + +struct VideoRecordingContext; + +struct VideoRecordingQueryParam { + std::uint64_t size; + std::uint32_t reserved8; + std::int32_t mode; + std::uint8_t reserved16[12]; + std::uint32_t option; + std::uint8_t reserved32[32]; +}; + +static_assert(sizeof(VideoRecordingQueryParam) == 64); +static_assert(offsetof(VideoRecordingQueryParam, mode) == 12); +static_assert(offsetof(VideoRecordingQueryParam, option) == 28); + + +struct VideoRecordingSupportInfo { + std::uint8_t data[18]; +}; + +static_assert(sizeof(VideoRecordingSupportInfo) == 18); + +struct VideoRecordingParam { + std::uint64_t size; + std::int32_t threadPriority; + std::uint32_t duration; + std::uint32_t mode; + std::int32_t outputMode; + std::uint32_t option; + std::uint32_t format; +}; + +struct VideoRecordingParam2 { + std::uint64_t size; + std::uint64_t affinityMask; + std::int32_t threadPriority; + std::uint32_t duration; + std::uint32_t mode; + std::int32_t outputMode; + std::uint32_t option; + std::uint32_t format; +}; + +static_assert(sizeof(VideoRecordingParam) == 32); +static_assert(offsetof(VideoRecordingParam, duration) == 12); +static_assert(offsetof(VideoRecordingParam, format) == 28); +static_assert(sizeof(VideoRecordingParam2) == 40); +static_assert(offsetof(VideoRecordingParam2, threadPriority) == 16); +static_assert(offsetof(VideoRecordingParam2, duration) == 20); +static_assert(offsetof(VideoRecordingParam2, format) == 36); namespace { -std::mutex g_infoMutex; -std::map> g_info; +constexpr int SceVideoRecordingStatusNone = 0; } extern "C" { +void APS5_VABI _sceVideoRecordingQueryParam_nid_postfix(VideoRecordingQueryParam* param) { + (void)param; + NotImplemented_nid_no_patch("_sceVideoRecordingQueryParam"); +} + int APS5_VABI sceVideoRecordingClose(int discard) { (void)discard; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVideoRecordingGetInfo(int32_t info, void* data, size_t size) { - if (data == nullptr) APS5_INVALID_ARG_EX; - std::lock_guard lock(g_infoMutex); - const auto found = g_info.find(info); - if (found == g_info.end()) throw std::logic_error(std::string(__func__) + ": info " + std::to_string(info) + " was never set"); - if (size < found->second.size()) APS5_INVALID_ARG_EX; - std::memcpy(data, found->second.data(), found->second.size()); - return 0; -} - -int APS5_VABI sceVideoRecordingGetStatus(void) { +int APS5_VABI sceVideoRecordingCloseByHandle(VideoRecordingContext* handle, int discard) { + (void)handle; + (void)discard; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVideoRecordingOpen(const char* path, const void* param, void* heap, int heapSize) { +int APS5_VABI sceVideoRecordingCopyBGRAtoNV12(void* dst, const void* src, uint32_t width, uint32_t height) { + (void)dst; + (void)src; + (void)width; + (void)height; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingGetInfo(std::int32_t info, void*, std::size_t) { + switch (info) { + case 0xa001: + case 0xa002: + case 0xa003: + case 0xa005: + case 0xa00a: + case 0xa00b: + case 0xa00c: + return static_cast(0x80a80006); + default: + return static_cast(0x80a80003); + } +} + +int APS5_VABI sceVideoRecordingGetInfoByHandle(VideoRecordingContext* handle, int32_t info, void* data, size_t size) { + (void)handle; + (void)info; + (void)data; + (void)size; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingGetStatus(void) { + return SceVideoRecordingStatusNone; +} + +int APS5_VABI sceVideoRecordingGetSupportInfo(VideoRecordingSupportInfo* info) { + (void)info; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingGetVideoOutMode(std::uint32_t* mode) { + (void)mode; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingInit(void) { + return 0; +} + +int APS5_VABI sceVideoRecordingInitialize(void* param) { + (void)param; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingOpen(const char* path, const VideoRecordingParam* param, void* heap, int heapSize) { (void)path; (void)param; (void)heap; @@ -46,18 +146,63 @@ int APS5_VABI sceVideoRecordingOpen(const char* path, const void* param, void* h return 0; } -int APS5_VABI sceVideoRecordingQueryMemSize(const void* param) { +int APS5_VABI sceVideoRecordingOpen2(const char* path, const VideoRecordingParam2* param, void* heap, int heapSize) { + (void)path; + (void)param; + (void)heap; + (void)heapSize; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int64_t APS5_VABI sceVideoRecordingQueryHddSize2(const VideoRecordingParam2* param) { (void)param; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVideoRecordingSetInfo(int32_t info, const void* data, size_t size) { - if (data == nullptr && size != 0) APS5_INVALID_ARG_EX; - const auto* bytes = static_cast(data); - std::lock_guard lock(g_infoMutex); - g_info[info].assign(bytes, bytes + size); - return 0; +int APS5_VABI sceVideoRecordingQueryMemSize(const VideoRecordingParam* param) { + (void)param; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingQueryMemSize2(const VideoRecordingParam2* param) { + (void)param; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingRestModeProcessGetStatus(void* status) { + (void)status; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingRestModeProcessStart(void* param) { + (void)param; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingSendCommand(int cmd, const void* param) { + (void)cmd; + (void)param; + return 0; +} + +int APS5_VABI sceVideoRecordingSetInfo(std::int32_t info, const void* data, std::size_t size) { + switch (info) { + case 0x8002: + case 0x8003: + if (!data || size < sizeof(float)) return static_cast(0x80a80003); + return static_cast(0x80a80002); + case 0xa004: + case 0xa006: + return static_cast(0x80a80006); + default: + return static_cast(0x80a80003); + } } int APS5_VABI sceVideoRecordingStart(void) { @@ -70,6 +215,10 @@ int APS5_VABI sceVideoRecordingStop(void) { return 0; } +int APS5_VABI sceVideoRecordingTerm(void) { + return 0; +} + APS5_EXPORT("iQS6DUtLybE", videoRecordingUnknown_iQS6DUtLybE); int APS5_VABI videoRecordingUnknown_iQS6DUtLybE(void) { NotImplemented_nid_no_patch("iQS6DUtLybE"); diff --git a/core/libs/prx/libSceVideoStreamingEngine/Export.cpp b/core/libs/prx/libSceVideoStreamingEngine/Export.cpp new file mode 100644 index 0000000000..1a09e3b9df --- /dev/null +++ b/core/libs/prx/libSceVideoStreamingEngine/Export.cpp @@ -0,0 +1,68 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVideoStreamingEngineDestroyMediaKeySystemAccess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoStreamingEngineMediaKeySessionGenerateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoStreamingEngineMediaKeySessionGetExpiration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoStreamingEngineMediaKeySessionGetKeyStatuses() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoStreamingEngineMediaKeySessionGetSessionId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoStreamingEngineMediaKeySessionLoad() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoStreamingEngineMediaKeySessionSetEventHandler() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoStreamingEngineMediaKeySystemAccessCreateMediaKeys() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoStreamingEngineMediaKeySystemAccessDestroyMediaKeys() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoStreamingEngineMediaKeySystemAccessGetKeySystem() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoStreamingEngineMediaKeySystemAccessSetCdmStorageConfig() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoStreamingEngineMediaKeysCreateSession() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceVideoStreamingEngine_sys/Export.cpp b/core/libs/prx/libSceVideoStreamingEngine_sys/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceVideoStreamingEngine_sys/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceVideodec/CMakeLists.txt b/core/libs/prx/libSceVideodec/CMakeLists.txt new file mode 100644 index 0000000000..9289cda4e6 --- /dev/null +++ b/core/libs/prx/libSceVideodec/CMakeLists.txt @@ -0,0 +1,20 @@ +add_library(libSceVideodec SHARED EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp +) + +target_include_directories(libSceVideodec PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceVideodec) + +target_link_libraries(libSceVideodec PRIVATE libSceVideodec2) + +if(BUILD_TESTING) + add_test_executable(guest_videodec_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideodec.cpp) + target_include_directories(guest_videodec_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_videodec_tests PRIVATE libSceVideodec libSceVideodec2 libc) + configure_windows_unwind(guest_videodec_tests) + add_test(NAME guest_videodec COMMAND guest_videodec_tests) + if(WIN32) + set_tests_properties(guest_videodec PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceVideodec/Export.cpp b/core/libs/prx/libSceVideodec/Export.cpp new file mode 100644 index 0000000000..56ae352fcf --- /dev/null +++ b/core/libs/prx/libSceVideodec/Export.cpp @@ -0,0 +1,437 @@ +#include +#include +#include +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int VideodecErrorApiFail = static_cast(0x80C10000); +constexpr int VideodecErrorStructSize = static_cast(0x80C10002); +constexpr int VideodecErrorHandle = static_cast(0x80C10003); +constexpr int VideodecErrorAuSize = static_cast(0x80C10009); +constexpr int VideodecErrorAuPointer = static_cast(0x80C1000A); +constexpr int VideodecErrorFrameBufferSize = static_cast(0x80C1000B); +constexpr int VideodecErrorFrameBufferPointer = static_cast(0x80C1000C); +constexpr int VideodecErrorConfigInfo = static_cast(0x80C1000E); +constexpr int VideodecErrorArgumentPointer = static_cast(0x80C1000F); +constexpr int VideodecErrorDecodeAu = static_cast(0x80C10011); + +constexpr std::uint32_t CodecAvc = 1; +constexpr std::uint32_t PitchAlignment = 64; + +struct ConfigInfo { + std::uint64_t thisSize; + std::uint32_t codecType; + std::uint32_t profile; + std::uint32_t maxLevel; + std::int32_t maxFrameWidth; + std::int32_t maxFrameHeight; + std::int32_t maxDpbFrameCount; + std::uint64_t videodecFlags; +}; + +struct ResourceInfo { + std::uint64_t thisSize; + std::uint64_t cpuMemorySize; + void* cpuMemory; + std::uint64_t cpuGpuMemorySize; + void* cpuGpuMemory; + std::uint64_t maxFrameBufferSize; + std::uint32_t frameBufferAlignment; +}; + +struct Ctrl { + std::uint64_t thisSize; + void* handle; + std::uint64_t version; +}; + +struct FrameBuffer { + std::uint64_t thisSize; + void* frameBuffer; + std::uint64_t frameBufferSize; +}; + +struct AvcInfo { + std::uint32_t numUnitsInTick; + std::uint32_t timeScale; + std::uint8_t fixedFrameRateFlag; + std::uint8_t aspectRatioIdc; + std::uint16_t sarWidth; + std::uint16_t sarHeight; + std::uint8_t colourPrimaries; + std::uint8_t transferCharacteristics; + std::uint8_t matrixCoefficients; + std::uint8_t videoFullRangeFlag; + std::uint32_t frameCropLeftOffset; + std::uint32_t frameCropRightOffset; + std::uint32_t frameCropTopOffset; + std::uint32_t frameCropBottomOffset; +}; + +union CodecInfo { + std::uint8_t reserved[64]; + AvcInfo avc; +}; + +struct PictureInfo { + std::uint64_t thisSize; + std::uint32_t isValid; + std::uint32_t codecType; + std::uint32_t frameWidth; + std::uint32_t framePitch; + std::uint32_t frameHeight; + std::uint32_t isErrorPic; + std::uint64_t ptsData; + std::uint64_t attachedData; + CodecInfo codec; +}; + +struct InputData { + std::uint64_t thisSize; + const std::uint8_t* auData; + std::uint64_t auSize; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; +}; + +struct Decoder2ConfigInfo { + std::uint64_t thisSize; + std::uint32_t resourceType; + std::uint32_t codecType; + std::uint32_t profile; + std::uint32_t maxLevel; + std::int32_t maxFrameWidth; + std::int32_t maxFrameHeight; + std::int32_t maxDpbFrameCount; + std::uint32_t decodePipelineDepth; +}; + +struct Decoder2MemoryInfo { + std::uint64_t thisSize; + std::uint64_t cpuMemorySize; + void* cpuMemory; + std::uint64_t gpuMemorySize; + void* gpuMemory; + std::uint64_t cpuGpuMemorySize; + void* cpuGpuMemory; + std::uint64_t maxFrameBufferSize; + std::uint32_t frameBufferAlignment; +}; + +struct Decoder2FrameBuffer { + std::uint64_t thisSize; + void* frameBuffer; + std::uint64_t frameBufferSize; + bool isAccepted; +}; + +struct Decoder2OutputInfo { + std::uint64_t thisSize; + bool isValid; + bool isErrorFrame; + std::uint8_t pictureCount; + std::uint32_t codecType; + std::uint32_t frameWidth; + std::uint32_t framePitch; + std::uint32_t frameHeight; + void* frameBuffer; + std::uint64_t frameBufferSize; + std::uint32_t frameFormat; + std::uint32_t framePitchInBytes; +}; + +struct Decoder2AvcPictureInfo { + std::uint64_t thisSize; + bool isValid; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; + std::uint8_t idrPictureFlag; + std::uint8_t profileIdc; + std::uint8_t levelIdc; + std::uint32_t picWidthInLumaSamples; + std::uint32_t picHeightInLumaSamples; +}; + +struct Decoder2Crop { + std::uint32_t left; + std::uint32_t right; + std::uint32_t top; + std::uint32_t bottom; +}; + +static_assert(sizeof(ConfigInfo) == 40 && sizeof(ResourceInfo) == 56 && sizeof(Ctrl) == 24); +static_assert(offsetof(ConfigInfo, maxFrameWidth) == 20 && offsetof(ConfigInfo, videodecFlags) == 32); +static_assert(offsetof(ResourceInfo, cpuGpuMemorySize) == 24 && offsetof(ResourceInfo, maxFrameBufferSize) == 40); +static_assert(offsetof(ResourceInfo, frameBufferAlignment) == 48); +static_assert(sizeof(Decoder2ConfigInfo) == 40 && sizeof(Decoder2MemoryInfo) == 72); +static_assert(sizeof(FrameBuffer) == 24 && sizeof(InputData) == 48 && sizeof(PictureInfo) == 112); +static_assert(sizeof(CodecInfo) == 64 && offsetof(PictureInfo, codec) == 48); + +struct Decoder { + std::uint64_t handle = 0; + std::vector picture; +}; + +std::mutex decodersMutex; +std::map decoders; + +Decoder* FindDecoder(const void* handle) { + if (handle == nullptr) return nullptr; + std::lock_guard lock(decodersMutex); + const auto found = decoders.find(handle); + return found == decoders.end() ? nullptr : found->second; +} + +std::uint32_t AlignUp(std::uint32_t value, std::uint32_t alignment) { + return (value + alignment - 1) / alignment * alignment; +} + +} + +extern "C" { +int APS5_VABI sceVideodec2QueryDecoderMemoryInfo_nid_postfix(const Decoder2ConfigInfo*, Decoder2MemoryInfo*); +int APS5_VABI sceVideodec2CreateDecoder_nid_postfix(const Decoder2ConfigInfo*, const Decoder2MemoryInfo*, std::uint64_t*); +int APS5_VABI sceVideodec2DeleteDecoder_nid_postfix(std::uint64_t); +int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t, const InputData*, Decoder2FrameBuffer*, Decoder2OutputInfo*); +int APS5_VABI sceVideodec2Flush_nid_postfix(std::uint64_t, Decoder2FrameBuffer*, Decoder2OutputInfo*); +int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t); +int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const Decoder2OutputInfo*, Decoder2AvcPictureInfo*, Decoder2AvcPictureInfo*); +void Videodec2PictureCrop_nid_no_patch(std::uint64_t, const void*, Decoder2Crop*); +} + +namespace { + +struct Geometry { + std::uint32_t width = 0; + std::uint32_t height = 0; + std::uint32_t pitch = 0; + Decoder2Crop crop{}; +}; + +Decoder2ConfigInfo MapConfig(const ConfigInfo& config) { + Decoder2ConfigInfo decoderConfig{}; + decoderConfig.thisSize = sizeof(Decoder2ConfigInfo); + decoderConfig.resourceType = 1; + decoderConfig.codecType = CodecAvc; + decoderConfig.profile = config.profile; + decoderConfig.maxLevel = config.maxLevel; + decoderConfig.maxFrameWidth = config.maxFrameWidth; + decoderConfig.maxFrameHeight = config.maxFrameHeight; + decoderConfig.maxDpbFrameCount = config.maxDpbFrameCount; + decoderConfig.decodePipelineDepth = 1; + return decoderConfig; +} + +Geometry Measure(const Decoder2OutputInfo& output, const Decoder2Crop& crop) { + Geometry geometry; + geometry.width = output.frameWidth; + geometry.height = output.frameHeight; + geometry.crop = crop; + geometry.pitch = AlignUp(geometry.width, PitchAlignment); + return geometry; +} + +void FillPictureInfo(const Geometry& geometry, const Decoder2OutputInfo& output, PictureInfo* picture) { + picture->isValid = 0; + picture->codecType = 0; + picture->frameWidth = 0; + picture->framePitch = 0; + picture->frameHeight = 0; + picture->isErrorPic = 0; + picture->ptsData = 0; + picture->attachedData = 0; + std::memset(picture->codec.reserved, 0, sizeof(picture->codec.reserved)); + if (!output.isValid) return; + picture->isValid = 1; + picture->frameWidth = geometry.width; + picture->framePitch = geometry.pitch; + picture->frameHeight = geometry.height; + picture->isErrorPic = output.isErrorFrame ? 1 : 0; + picture->codec.avc.frameCropLeftOffset = geometry.crop.left / 2; + picture->codec.avc.frameCropRightOffset = geometry.crop.right / 2; + picture->codec.avc.frameCropTopOffset = geometry.crop.top / 2; + picture->codec.avc.frameCropBottomOffset = geometry.crop.bottom / 2; +} + +int CopyPicture(const Decoder2OutputInfo& output, const Geometry& geometry, const FrameBuffer* frameBuffer) { + const auto bytes = static_cast(geometry.pitch) * geometry.height * 3 / 2; + if (frameBuffer->frameBuffer == nullptr) return VideodecErrorFrameBufferPointer; + if (frameBuffer->frameBufferSize < bytes) return VideodecErrorFrameBufferSize; + const auto sourcePitch = static_cast(output.framePitchInBytes); + const auto* source = static_cast(output.frameBuffer); + auto* destination = static_cast(frameBuffer->frameBuffer); + for (std::uint32_t row = 0; row < geometry.height; ++row) + std::memcpy(destination + static_cast(row) * geometry.pitch, source + static_cast(row) * sourcePitch, geometry.width); + const auto* sourceChroma = source + sourcePitch * geometry.height; + auto* destinationChroma = destination + static_cast(geometry.pitch) * geometry.height; + for (std::uint32_t row = 0; row < geometry.height / 2; ++row) + std::memcpy(destinationChroma + static_cast(row) * geometry.pitch, sourceChroma + static_cast(row) * sourcePitch, geometry.width); + return 0; +} + +} + +extern "C" { + +int APS5_VABI sceVideodecCreateDecoder(const ConfigInfo* config, const ResourceInfo* resource, Ctrl* ctrl) { + if (config == nullptr || resource == nullptr || ctrl == nullptr) return VideodecErrorArgumentPointer; + if (config->thisSize < sizeof(ConfigInfo) || resource->thisSize < sizeof(ResourceInfo)) return VideodecErrorStructSize; + const auto decoderConfig = MapConfig(*config); + Decoder2MemoryInfo decoderMemory{}; + decoderMemory.thisSize = sizeof(Decoder2MemoryInfo); + decoderMemory.cpuMemorySize = resource->cpuMemorySize; + decoderMemory.cpuMemory = resource->cpuMemory; + decoderMemory.cpuGpuMemorySize = resource->cpuGpuMemorySize; + decoderMemory.cpuGpuMemory = resource->cpuGpuMemory; + decoderMemory.maxFrameBufferSize = resource->maxFrameBufferSize; + decoderMemory.frameBufferAlignment = resource->frameBufferAlignment; + try { + sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&decoderConfig, &decoderMemory); + } catch (const std::exception&) { + return VideodecErrorConfigInfo; + } + std::uint64_t handle = 0; + if (sceVideodec2CreateDecoder_nid_postfix(&decoderConfig, &decoderMemory, &handle) != 0) return VideodecErrorApiFail; + auto* decoder = new Decoder(); + decoder->handle = handle; + decoder->picture.resize(decoderMemory.maxFrameBufferSize); + { + std::lock_guard lock(decodersMutex); + decoders.emplace(decoder, decoder); + } + ctrl->thisSize = sizeof(Ctrl); + ctrl->handle = decoder; + ctrl->version = 1; + return 0; +} + +int APS5_VABI sceVideodecDecode(Ctrl* ctrl, const InputData* input, FrameBuffer* frameBuffer, PictureInfo* picture) { + if (ctrl == nullptr || input == nullptr || picture == nullptr) return VideodecErrorArgumentPointer; + if (frameBuffer == nullptr) return VideodecErrorFrameBufferPointer; + if (ctrl->thisSize < sizeof(Ctrl)) return VideodecErrorStructSize; + if (input->thisSize < sizeof(InputData)) return VideodecErrorStructSize; + if (picture->thisSize < sizeof(PictureInfo)) return VideodecErrorStructSize; + if (frameBuffer->thisSize < sizeof(FrameBuffer)) return VideodecErrorStructSize; + auto* decoder = FindDecoder(ctrl->handle); + if (decoder == nullptr) return VideodecErrorHandle; + if (input->auData == nullptr) return VideodecErrorAuPointer; + if (input->auSize == 0) return VideodecErrorAuSize; + if (frameBuffer->frameBuffer == nullptr) return VideodecErrorFrameBufferPointer; + Decoder2FrameBuffer target{}; + target.thisSize = sizeof(Decoder2FrameBuffer); + target.frameBuffer = decoder->picture.data(); + target.frameBufferSize = decoder->picture.size(); + Decoder2OutputInfo output{sizeof(Decoder2OutputInfo)}; + try { + sceVideodec2Decode_nid_postfix(decoder->handle, input, &target, &output); + } catch (const std::exception&) { + return VideodecErrorDecodeAu; + } + Decoder2Crop crop{}; + if (output.isValid) Videodec2PictureCrop_nid_no_patch(decoder->handle, output.frameBuffer, &crop); + const auto geometry = Measure(output, crop); + Decoder2AvcPictureInfo avc{sizeof(Decoder2AvcPictureInfo)}; + if (output.isValid) sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr); + FillPictureInfo(geometry, output, picture); + picture->ptsData = avc.ptsData; + picture->attachedData = avc.attachedData; + if (!output.isValid) return 0; + return CopyPicture(output, geometry, frameBuffer); +} + +int APS5_VABI sceVideodecDeleteDecoder(Ctrl* ctrl) { + if (ctrl == nullptr) return VideodecErrorArgumentPointer; + if (ctrl->thisSize < sizeof(Ctrl)) return VideodecErrorStructSize; + Decoder* decoder = nullptr; + { + std::lock_guard lock(decodersMutex); + const auto found = decoders.find(ctrl->handle); + if (found == decoders.end()) return VideodecErrorHandle; + decoder = found->second; + decoders.erase(found); + } + const auto handle = decoder->handle; + delete decoder; + try { + sceVideodec2DeleteDecoder_nid_postfix(handle); + } catch (const std::exception&) { + return VideodecErrorApiFail; + } + return 0; +} + +int APS5_VABI sceVideodecFlush(Ctrl* ctrl, FrameBuffer* frameBuffer, PictureInfo* picture) { + if (ctrl == nullptr || picture == nullptr) return VideodecErrorArgumentPointer; + if (frameBuffer == nullptr) return VideodecErrorFrameBufferPointer; + if (ctrl->thisSize < sizeof(Ctrl)) return VideodecErrorStructSize; + if (picture->thisSize < sizeof(PictureInfo)) return VideodecErrorStructSize; + if (frameBuffer->thisSize < sizeof(FrameBuffer)) return VideodecErrorStructSize; + auto* decoder = FindDecoder(ctrl->handle); + if (decoder == nullptr) return VideodecErrorHandle; + if (frameBuffer->frameBuffer == nullptr) return VideodecErrorFrameBufferPointer; + Decoder2FrameBuffer target{}; + target.thisSize = sizeof(Decoder2FrameBuffer); + target.frameBuffer = decoder->picture.data(); + target.frameBufferSize = decoder->picture.size(); + Decoder2OutputInfo output{sizeof(Decoder2OutputInfo)}; + try { + sceVideodec2Flush_nid_postfix(decoder->handle, &target, &output); + } catch (const std::exception&) { + return VideodecErrorDecodeAu; + } + Decoder2Crop crop{}; + if (output.isValid) Videodec2PictureCrop_nid_no_patch(decoder->handle, output.frameBuffer, &crop); + const auto geometry = Measure(output, crop); + Decoder2AvcPictureInfo avc{sizeof(Decoder2AvcPictureInfo)}; + if (output.isValid) sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr); + FillPictureInfo(geometry, output, picture); + picture->ptsData = avc.ptsData; + picture->attachedData = avc.attachedData; + if (!output.isValid) return 0; + return CopyPicture(output, geometry, frameBuffer); +} + +int APS5_VABI sceVideodecReset(Ctrl* ctrl) { + if (ctrl == nullptr) return VideodecErrorArgumentPointer; + if (ctrl->thisSize < sizeof(Ctrl)) return VideodecErrorStructSize; + auto* decoder = FindDecoder(ctrl->handle); + if (decoder == nullptr) return VideodecErrorHandle; + try { + sceVideodec2Reset_nid_postfix(decoder->handle); + } catch (const std::exception&) { + return VideodecErrorApiFail; + } + return 0; +} +int APS5_VABI sceVideodecMapMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideodecQueryResourceInfo(const ConfigInfo* config, ResourceInfo* resource) { + if (config == nullptr || resource == nullptr) return VideodecErrorArgumentPointer; + if (config->thisSize < sizeof(ConfigInfo) || resource->thisSize < sizeof(ResourceInfo)) return VideodecErrorStructSize; + const auto decoderConfig = MapConfig(*config); + Decoder2MemoryInfo decoderMemory{}; + decoderMemory.thisSize = sizeof(Decoder2MemoryInfo); + try { + sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&decoderConfig, &decoderMemory); + } catch (const std::exception&) { + return VideodecErrorConfigInfo; + } + resource->cpuMemorySize = decoderMemory.cpuMemorySize; + resource->cpuGpuMemorySize = decoderMemory.cpuGpuMemorySize; + resource->maxFrameBufferSize = decoderMemory.maxFrameBufferSize; + resource->frameBufferAlignment = decoderMemory.frameBufferAlignment; + return 0; +} + +} diff --git a/core/libs/prx/libSceVideodec2/CMakeLists.txt b/core/libs/prx/libSceVideodec2/CMakeLists.txt index c2b41cd27b..8bc94af3d1 100644 --- a/core/libs/prx/libSceVideodec2/CMakeLists.txt +++ b/core/libs/prx/libSceVideodec2/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceVideodec2 SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp ) target_include_directories(libSceVideodec2 PRIVATE ${LIBS_INCLUDE_DIR}) @@ -17,4 +18,14 @@ if(BUILD_TESTING) set_tests_properties(guest_videodec2 PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_videodec2_unimplemented_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideodec2Unimplemented.cpp) + target_include_directories(guest_videodec2_unimplemented_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_videodec2_unimplemented_tests PRIVATE libSceVideodec2 libc) + configure_windows_unwind(guest_videodec2_unimplemented_tests) + add_test(NAME guest_videodec2_unimplemented COMMAND guest_videodec2_unimplemented_tests) + if(WIN32) + set_tests_properties(guest_videodec2_unimplemented PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index 6d9f99f87b..583b18c0ff 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -28,6 +30,8 @@ constexpr std::uint32_t HeightAlignment = 32; constexpr std::uint32_t CodecAvc = 1; constexpr std::uint32_t CodecHevc = 974921; constexpr std::uint32_t CodecVp9 = 2382845; +constexpr int Videodec2ErrorAccessUnitSize = static_cast(0x811D010Du); +constexpr int Videodec2ErrorAccessUnitPointer = static_cast(0x811D010Eu); struct ComputeMemoryInfo { std::uint64_t thisSize; @@ -110,11 +114,189 @@ struct AvcPictureInfo { std::uint32_t picHeightInLumaSamples; }; +struct Vp9PictureInfo { + std::uint64_t thisSize; + bool isValid; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; + std::uint32_t profile; + std::uint32_t level; + std::uint32_t keyFrameFlag; + std::uint32_t intraOnly; + std::uint8_t bitDepth; + std::uint8_t colorRange; + std::uint32_t colorSpace; + std::uint32_t numFrames; + std::uint32_t frameWidth; + std::uint32_t frameHeight; + std::uint32_t renderWidth; + std::uint32_t renderHeight; +}; + +static_assert(sizeof(Vp9PictureInfo) == 88); +static_assert(offsetof(Vp9PictureInfo, colorSpace) == 60); + +class Vp9Bits { +public: + explicit Vp9Bits(std::span bytes) : bytes(bytes) {} + std::uint32_t Read(unsigned count) { + std::uint32_t result = 0; + while (count-- != 0) { + if (position / 8 >= bytes.size()) throw std::runtime_error("Videodec2: truncated VP9 header"); + result = (result << 1u) | ((bytes[position / 8] >> (7u - position % 8)) & 1u); + ++position; + } + return result; + } +private: + std::span bytes; + std::size_t position = 0; +}; + +struct Vp9Unit { + std::span bytes; + Vp9PictureInfo info{}; + bool shown = false; +}; + +Vp9Unit ParseVp9Unit(std::span bytes, std::array& references, Vp9PictureInfo& colors, std::uint32_t count) { + Vp9Bits bits(bytes); + if (bits.Read(2) != 2) throw std::runtime_error("Videodec2: invalid VP9 frame marker"); + const auto low = bits.Read(1); + const auto profile = low | (bits.Read(1) << 1u); + if (profile == 3 && bits.Read(1) != 0) throw std::runtime_error("Videodec2: invalid VP9 reserved bit"); + Vp9Unit unit{bytes}; + auto& info = unit.info; + if (bits.Read(1) != 0) { + info = references[bits.Read(3)]; + if (!info.isValid) throw std::runtime_error("Videodec2: missing VP9 display reference"); + info.keyFrameFlag = 0; + info.intraOnly = 0; + info.profile = profile; + info.numFrames = count; + unit.shown = true; + return unit; + } + const bool key = bits.Read(1) == 0; + unit.shown = bits.Read(1) != 0; + const bool resilient = bits.Read(1) != 0; + const bool intra = !key && !unit.shown && bits.Read(1) != 0; + if (!key && !resilient) bits.Read(2); + const auto dimensions = [&] { + info.frameWidth = bits.Read(16) + 1; + info.frameHeight = bits.Read(16) + 1; + }; + const auto render = [&] { + const bool different = bits.Read(1) != 0; + info.renderWidth = different ? bits.Read(16) + 1 : info.frameWidth; + info.renderHeight = different ? bits.Read(16) + 1 : info.frameHeight; + }; + std::uint32_t refresh = 0; + if (key || intra) { + if (bits.Read(24) != 0x498342) throw std::runtime_error("Videodec2: invalid VP9 sync code"); + info.bitDepth = 8; + info.colorSpace = 1; + if (key || profile != 0) { + if (profile >= 2) info.bitDepth = bits.Read(1) != 0 ? 12 : 10; + info.colorSpace = bits.Read(3); + if (info.colorSpace != 7) { + info.colorRange = static_cast(bits.Read(1)); + if (profile == 1 || profile == 3) { + bits.Read(2); + if (bits.Read(1) != 0) throw std::runtime_error("Videodec2: invalid VP9 color reserved bit"); + } + } else { + info.colorRange = 1; + if (profile == 1 || profile == 3) { + if (bits.Read(1) != 0) throw std::runtime_error("Videodec2: invalid VP9 RGB reserved bit"); + } else throw std::runtime_error("Videodec2: invalid VP9 RGB profile"); + } + } + refresh = key ? 255 : bits.Read(8); + dimensions(); + } else { + refresh = bits.Read(8); + std::array indexes{}; + for (auto& index : indexes) { index = bits.Read(3); bits.Read(1); } + info = colors; + if (!info.isValid) throw std::runtime_error("Videodec2: missing VP9 color configuration"); + bool inherited = false; + for (const auto index : indexes) { + if (bits.Read(1) != 0) { + if (!references[index].isValid) throw std::runtime_error("Videodec2: missing VP9 size reference"); + info.frameWidth = references[index].frameWidth; + info.frameHeight = references[index].frameHeight; + inherited = true; + break; + } + } + if (!inherited) dimensions(); + } + render(); + info.isValid = true; + info.profile = profile; + info.keyFrameFlag = key ? 1 : 0; + info.intraOnly = intra ? 1 : 0; + info.numFrames = count; + if (key || intra) colors = info; + for (unsigned index = 0; index < references.size(); ++index) + if ((refresh & (1u << index)) != 0) references[index] = info; + return unit; +} + +std::vector ParseVp9Packet(std::span bytes, std::array& references, Vp9PictureInfo& colors) { + std::vector> frames; + const auto marker = bytes.back(); + const unsigned count = (marker & 7u) + 1; + const unsigned magnitude = ((marker >> 3u) & 3u) + 1; + const std::size_t indexSize = 2 + count * magnitude; + if ((marker & 0xe0u) == 0xc0u && bytes.size() >= indexSize && bytes[bytes.size() - indexSize] == marker) { + const auto end = bytes.size() - indexSize; + std::size_t offset = 0; + for (unsigned frame = 0; frame < count; ++frame) { + std::size_t size = 0; + for (unsigned byte = 0; byte < magnitude; ++byte) + size |= static_cast(bytes[end + 1 + frame * magnitude + byte]) << (8u * byte); + if (size == 0 || size > end - offset) throw std::runtime_error("Videodec2: invalid VP9 superframe size"); + frames.push_back(bytes.subspan(offset, size)); + offset += size; + } + if (offset != end) throw std::runtime_error("Videodec2: VP9 superframe contains unindexed bytes"); + } else frames.push_back(bytes); + std::vector result; + for (const auto frame : frames) result.push_back(ParseVp9Unit(frame, references, colors, static_cast(frames.size()))); + return result; +} + +struct Crop { + std::uint32_t left = 0; + std::uint32_t right = 0; + std::uint32_t top = 0; + std::uint32_t bottom = 0; +}; + struct Picture { std::uint64_t pts = 0; std::uint64_t dts = 0; std::uint64_t attached = 0; bool idr = false; + Vp9PictureInfo vp9{}; + Crop crop{}; + bool hevcIdr = false; + std::uint8_t bitDepthMinus8 = 0; +}; + +struct PictureHeader { + std::uint64_t pts; + std::uint64_t dts; + std::uint64_t attached; + bool keyFrame; + bool hevcIdr; + std::uint32_t profile; + std::uint32_t level; + Crop crop; + std::uint8_t bitDepthMinus8; }; struct FrameDeleter { @@ -128,6 +310,7 @@ struct Decoder { std::uint32_t height = 0; std::uint32_t profile = 0; std::uint32_t level = 0; + std::uint32_t sampleBytes = 1; std::map pictures; std::uint64_t decoded = 0; std::mutex mutex; @@ -137,7 +320,12 @@ struct Decoder { std::int64_t nextKey = 0; std::deque ready; std::vector annexB; + std::vector parameterSets; + bool lengthPrefixed = false; + bool shortStartCodes = false; bool flushing = false; + std::array vp9References{}; + Vp9PictureInfo vp9Colors{}; ~Decoder() { sws_freeContext(scaler); @@ -160,12 +348,20 @@ std::uint32_t AlignUp(std::uint32_t value, std::uint32_t alignment) { return (value + alignment - 1) / alignment * alignment; } -std::uint64_t ChromaOffset(std::uint32_t width, std::uint32_t height) { - return static_cast(AlignUp(width, PitchAlignment)) * height; +std::uint32_t PitchBytes(std::uint32_t width, std::uint32_t sampleBytes) { + return AlignUp(width * sampleBytes, PitchAlignment); } -std::uint64_t LumaBytes(std::uint32_t width, std::uint32_t height) { - return static_cast(AlignUp(width, PitchAlignment)) * AlignUp(height, HeightAlignment); +std::uint64_t ChromaOffset(std::uint32_t width, std::uint32_t height, std::uint32_t sampleBytes) { + return static_cast(PitchBytes(width, sampleBytes)) * height; +} + +std::uint64_t LumaBytes(std::uint32_t width, std::uint32_t height, std::uint32_t sampleBytes) { + return static_cast(PitchBytes(width, sampleBytes)) * AlignUp(height, HeightAlignment); +} + +std::uint32_t ConfiguredSampleBytes(std::uint32_t codec, std::uint32_t profile) { + return (codec == CodecHevc || codec == CodecVp9) && profile == 2 ? 2 : 1; } template @@ -196,30 +392,75 @@ void OpenCodec(Decoder& decoder) { decoder.context->err_recognition = AV_EF_EXPLODE; decoder.context->thread_count = 0; decoder.context->thread_type = FF_THREAD_SLICE; + decoder.context->apply_cropping = 0; if (avcodec_open2(decoder.context, codec, nullptr) < 0) { avcodec_free_context(&decoder.context); throw std::runtime_error("Videodec2: cannot open decoder"); } } -const std::vector* ToAnnexB(Decoder& decoder, const std::uint8_t* data, std::size_t size) { - if (size >= 4 && data[0] == 0 && data[1] == 0 && (data[2] == 1 || (data[2] == 0 && data[3] == 1))) { - decoder.annexB.assign(data, data + size); - return &decoder.annexB; - } - decoder.annexB.clear(); +bool TilesAsAvcc(const std::uint8_t* data, std::size_t size) { + if (size < 4) return false; for (std::size_t index = 0; index < size;) { - if (size - index < 4) throw std::runtime_error("Videodec2: truncated NAL length"); + if (size - index < 4) return false; const std::size_t length = (static_cast(data[index]) << 24u) | (static_cast(data[index + 1]) << 16u) | (static_cast(data[index + 2]) << 8u) | data[index + 3]; - if (length == 0 || length > size - index - 4) throw std::runtime_error("Videodec2: invalid NAL length"); - static constexpr std::uint8_t startCode[4] = {0, 0, 0, 1}; - decoder.annexB.insert(decoder.annexB.end(), startCode, startCode + 4); - decoder.annexB.insert(decoder.annexB.end(), data + index + 4, data + index + 4 + length); + if (length == 0 || length > size - index - 4) return false; index += 4 + length; } + return true; +} + +bool StartsWithStartCode(const std::uint8_t* data, std::size_t size) { + return size >= 3 && data[0] == 0 && data[1] == 0 && (data[2] == 1 || (size >= 4 && data[2] == 0 && data[3] == 1)); +} + +const std::vector* ToAnnexB(Decoder& decoder, const std::uint8_t* data, std::size_t size) { + const bool tiles = TilesAsAvcc(data, size); + const bool opensWithStartCode = StartsWithStartCode(data, size); + if (!tiles && !opensWithStartCode) throw std::runtime_error("Videodec2: access unit is neither Annex-B nor length-prefixed"); + const bool shortStartCode = opensWithStartCode && data[2] == 1; + bool lengthPrefixed = tiles; + if (tiles && opensWithStartCode) { + lengthPrefixed = shortStartCode ? !decoder.shortStartCodes : decoder.lengthPrefixed; + } else { + decoder.lengthPrefixed = tiles; + if (shortStartCode) decoder.shortStartCodes = true; + } + if (lengthPrefixed) { + decoder.annexB.clear(); + for (std::size_t index = 0; index < size;) { + const std::size_t length = (static_cast(data[index]) << 24u) | (static_cast(data[index + 1]) << 16u) | (static_cast(data[index + 2]) << 8u) | data[index + 3]; + static constexpr std::uint8_t startCode[4] = {0, 0, 0, 1}; + decoder.annexB.insert(decoder.annexB.end(), startCode, startCode + 4); + decoder.annexB.insert(decoder.annexB.end(), data + index + 4, data + index + 4 + length); + index += 4 + length; + } + return &decoder.annexB; + } + decoder.annexB.assign(data, data + size); return &decoder.annexB; } +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; +} + +bool StartsHevcIdr(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] >> 1u) & 0x3fu; + if (type < 32) return type == 19 || type == 20; + index += 2; + } + return false; +} + void Receive(Decoder& decoder) { for (;;) { FramePointer frame(av_frame_alloc()); @@ -241,14 +482,30 @@ bool WriteNv12(Decoder& decoder, const AVFrame& frame, std::uint8_t* target, std if (frame.width <= 0 || frame.height <= 0 || frame.width % 2 != 0 || frame.height % 2 != 0) throw std::runtime_error("Videodec2: unsupported frame dimensions"); const auto width = static_cast(frame.width); const auto height = static_cast(frame.height); - const auto pitch = AlignUp(width, PitchAlignment); - const auto luma = ChromaOffset(width, height); + const auto format = static_cast(frame.format); + const AVPixFmtDescriptor* descriptor = av_pix_fmt_desc_get(format); + if (descriptor == nullptr || descriptor->comp[0].depth > 10) throw std::runtime_error("Videodec2: output deeper than 10 bits is not implemented"); + const std::uint32_t sampleBytes = descriptor->comp[0].depth > 8 ? 2 : 1; + if (sampleBytes == 2 && format != AV_PIX_FMT_YUV420P10LE) throw std::runtime_error("Videodec2: 10-bit output from a decoded format other than yuv420p10le is not implemented"); + decoder.sampleBytes = sampleBytes; + const auto pitch = PitchBytes(width, sampleBytes); + const auto luma = ChromaOffset(width, height, sampleBytes); if (luma + static_cast(pitch) * (height / 2) > bytes) return false; std::uint8_t* planes[4] = {target, target + luma, nullptr, nullptr}; const int strides[4] = {static_cast(pitch), static_cast(pitch), 0, 0}; - const auto format = static_cast(frame.format); - const AVPixFmtDescriptor* descriptor = av_pix_fmt_desc_get(format); - if (descriptor == nullptr || descriptor->comp[0].depth > 8) throw std::runtime_error("Videodec2: output deeper than 8 bits is not implemented"); + if (sampleBytes == 2) { + for (std::uint32_t row = 0; row < height; ++row) std::memcpy(planes[0] + static_cast(row) * pitch, frame.data[0] + static_cast(row) * frame.linesize[0], static_cast(width) * 2); + for (std::uint32_t row = 0; row < height / 2; ++row) { + const auto* u = frame.data[1] + static_cast(row) * frame.linesize[1]; + const auto* v = frame.data[2] + static_cast(row) * frame.linesize[2]; + auto* uv = planes[1] + static_cast(row) * pitch; + for (std::uint32_t column = 0; column < width / 2; ++column) { + std::memcpy(uv + column * 4, u + column * 2, 2); + std::memcpy(uv + column * 4 + 2, v + column * 2, 2); + } + } + return true; + } if (format == AV_PIX_FMT_YUV420P || format == AV_PIX_FMT_YUVJ420P) { for (std::uint32_t row = 0; row < height; ++row) std::memcpy(planes[0] + static_cast(row) * pitch, frame.data[0] + static_cast(row) * frame.linesize[0], width); for (std::uint32_t row = 0; row < height / 2; ++row) { @@ -267,12 +524,16 @@ bool WriteNv12(Decoder& decoder, const AVFrame& frame, std::uint8_t* target, std return sws_scale(decoder.scaler, frame.data, frame.linesize, 0, frame.height, planes, strides) == frame.height; } -void EmitDecoded(std::uint64_t handle, Decoder& decoder, FrameBuffer* frame, OutputInfo* output) { +void ClearOutput(const Decoder& decoder, FrameBuffer* frame, OutputInfo* output) { output->isValid = false; output->isErrorFrame = false; output->pictureCount = 0; output->codecType = decoder.codec; if (frame) frame->isAccepted = false; +} + +void EmitDecoded(std::uint64_t handle, Decoder& decoder, FrameBuffer* frame, OutputInfo* output) { + ClearOutput(decoder, frame, output); if (decoder.ready.empty()) return; if (!frame || !frame->frameBuffer) throw std::runtime_error("Videodec2: missing output buffer"); const auto picture = std::move(decoder.ready.front()); @@ -283,18 +544,20 @@ void EmitDecoded(std::uint64_t handle, Decoder& decoder, FrameBuffer* frame, Out auto timestamps = found->second; decoder.inputs.erase(found); timestamps.idr = (picture->flags & AV_FRAME_FLAG_KEY) != 0; + timestamps.crop = {static_cast(picture->crop_left), static_cast(picture->crop_right), static_cast(picture->crop_top), static_cast(picture->crop_bottom)}; decoder.width = static_cast(picture->width); decoder.height = static_cast(picture->height); if (decoder.context->profile > 0) decoder.profile = static_cast(decoder.context->profile) & 0xffu; if (decoder.context->level > 0) decoder.level = static_cast(decoder.context->level); if (!WriteNv12(decoder, *picture, static_cast(frame->frameBuffer), frame->frameBufferSize)) throw std::runtime_error("Videodec2: NV12 conversion failed"); - const auto pitch = AlignUp(decoder.width, PitchAlignment); + timestamps.bitDepthMinus8 = decoder.sampleBytes == 2 ? 2 : 0; + const auto pitch = PitchBytes(decoder.width, decoder.sampleBytes); frame->isAccepted = true; output->isValid = true; output->isErrorFrame = false; output->pictureCount = 1; output->frameWidth = decoder.width; - output->framePitch = pitch; + output->framePitch = pitch / decoder.sampleBytes; output->frameHeight = decoder.height; output->frameBuffer = frame->frameBuffer; output->frameBufferSize = frame->frameBufferSize; @@ -340,7 +603,7 @@ int APS5_VABI sceVideodec2QueryDecoderMemoryInfo_nid_postfix(const DecoderConfig if (config->maxFrameWidth <= 0 || config->maxFrameHeight <= 0) throw std::runtime_error("Videodec2: invalid frame dimensions"); const auto width = static_cast(config->maxFrameWidth); const auto height = static_cast(config->maxFrameHeight); - memory->maxFrameBufferSize = LumaBytes(width, height) * 3 / 2; + memory->maxFrameBufferSize = LumaBytes(width, height, ConfiguredSampleBytes(config->codecType, config->profile)) * 3 / 2; memory->frameBufferAlignment = PitchAlignment; return 0; } @@ -371,21 +634,64 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa if (!found) throw std::runtime_error("Videodec2: invalid decoder handle"); auto& decoder = *found; std::lock_guard guard(decoder.mutex); + if (!input->auData || input->auSize == 0) { + ClearOutput(decoder, frame, output); + return input->auData ? Videodec2ErrorAccessUnitSize : Videodec2ErrorAccessUnitPointer; + } 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 (!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) decoder.annexB.assign(input->auData, input->auData + input->auSize); - const auto* unit = decoder.codec == CodecVp9 ? &decoder.annexB : ToAnnexB(decoder, input->auData, static_cast(input->auSize)); + if (decoder.flushing) { + avcodec_flush_buffers(decoder.context); + decoder.flushing = false; + } + if (input->auSize > static_cast(std::numeric_limits::max())) throw std::runtime_error("Videodec2: invalid access unit"); + if (decoder.codec == CodecVp9) { + auto references = decoder.vp9References; + auto colors = decoder.vp9Colors; + const auto units = ParseVp9Packet({input->auData, static_cast(input->auSize)}, references, colors); + for (const auto& unit : units) { + AVPacket* packet = av_packet_alloc(); + if (!packet) throw std::runtime_error("Videodec2: cannot allocate VP9 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.bytes.size())) < 0) throw std::runtime_error("Videodec2: cannot allocate VP9 packet data"); + std::memcpy(packet->data, unit.bytes.data(), unit.bytes.size()); + packet->pts = decoder.nextKey; + auto tagged = picture; + tagged.vp9 = unit.info; + tagged.vp9.ptsData = picture.pts; + tagged.vp9.dtsData = picture.dts; + tagged.vp9.attachedData = picture.attached; + const auto key = decoder.nextKey++; + decoder.inputs.emplace(key, tagged); + Send(decoder, packet); + if (!unit.shown) decoder.inputs.erase(key); + } + decoder.vp9References = references; + decoder.vp9Colors = colors; + EmitDecoded(handle, decoder, frame, output); + 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"); + if (!decoder.parameterSets.empty()) std::memcpy(packet->data, decoder.parameterSets.data(), decoder.parameterSets.size()); + std::memcpy(packet->data + decoder.parameterSets.size(), unit->data(), unit->size()); + decoder.parameterSets.clear(); packet->pts = decoder.nextKey; - decoder.inputs.emplace(decoder.nextKey++, picture); + auto tagged = picture; + tagged.hevcIdr = decoder.codec == CodecHevc && StartsHevcIdr(*unit); + decoder.inputs.emplace(decoder.nextKey++, tagged); Send(decoder, packet); EmitDecoded(handle, decoder, frame, output); return 0; @@ -397,7 +703,7 @@ int APS5_VABI sceVideodec2Flush_nid_postfix(std::uint64_t handle, FrameBuffer* f if (!found) throw std::runtime_error("Videodec2: invalid decoder handle"); auto& decoder = *found; std::lock_guard guard(decoder.mutex); - if (!decoder.flushing) { + if (!decoder.flushing && decoder.ready.empty() && !decoder.inputs.empty()) { Send(decoder, nullptr); decoder.flushing = true; } @@ -411,6 +717,8 @@ int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t handle) { auto& decoder = *found; std::lock_guard guard(decoder.mutex); decoder.pictures.clear(); + decoder.vp9References = {}; + decoder.vp9Colors = {}; { std::lock_guard ownersGuard(lock); std::erase_if(pictureOwners, [handle](const auto& owner) { return owner.second == handle; }); @@ -418,6 +726,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; } @@ -437,7 +746,15 @@ int APS5_VABI sceVideodec2GetPictureInfo_nid_postfix(const OutputInfo* output, A if (owner() != handle) continue; const auto found = decoder->pictures.find(output->frameBuffer); if (found == decoder->pictures.end()) throw std::runtime_error("Videodec2: owned picture is missing"); - if (decoder->codec != CodecAvc) throw std::runtime_error("Videodec2: picture info is only implemented for H.264"); + if (decoder->codec == CodecVp9) { + auto* info = reinterpret_cast(first); + if (!Fits(info, sizeof(Vp9PictureInfo))) throw std::runtime_error("Videodec2: VP9 picture structure is too small"); + const auto size = info->thisSize; + *info = found->second.vp9; + info->thisSize = size; + return 0; + } + if (decoder->codec != CodecAvc) throw std::runtime_error("Videodec2: picture info is only implemented for H.264 and VP9"); const auto& picture = found->second; first->isValid = true; first->ptsData = picture.pts; @@ -460,4 +777,49 @@ int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const OutputInfo* output return sceVideodec2GetPictureInfo_nid_postfix(output, first, second); } +int APS5_VABI sceVideodec2GetVp9PictureInfo_nid_postfix(const OutputInfo* output, Vp9PictureInfo* info) { + if (!output || !info) throw std::runtime_error("Videodec2: invalid VP9 picture argument"); + if (!Fits(output, offsetof(OutputInfo, frameBuffer) + sizeof(output->frameBuffer)) || !Fits(info, sizeof(Vp9PictureInfo))) + throw std::runtime_error("Videodec2: VP9 picture structure is too small"); + const auto owner = [output]() -> std::uint64_t { + std::lock_guard guard(lock); + const auto found = pictureOwners.find(output->frameBuffer); + return found != pictureOwners.end() ? found->second : 0; + }; + for (auto handle = owner(); handle != 0; handle = owner()) { + const auto decoder = FindDecoder(handle); + if (!decoder) continue; + std::lock_guard guard(decoder->mutex); + if (owner() != handle) continue; + const auto found = decoder->pictures.find(output->frameBuffer); + if (found == decoder->pictures.end()) throw std::runtime_error("Videodec2: owned VP9 picture is missing"); + if (decoder->codec != CodecVp9) throw std::runtime_error("Videodec2: VP9 info requested for another codec"); + const auto size = info->thisSize; + *info = found->second.vp9; + info->thisSize = size; + return 0; + } + info->isValid = false; + return 0; +} + +void Videodec2PictureHeader_nid_no_patch(std::uint64_t handle, const void* frameBuffer, PictureHeader* header) { + if (!header) throw std::runtime_error("Videodec2: invalid argument pointer"); + const auto found = FindDecoder(handle); + if (!found) throw std::runtime_error("Videodec2: invalid decoder handle"); + auto& decoder = *found; + std::lock_guard guard(decoder.mutex); + const auto picture = decoder.pictures.find(frameBuffer); + if (picture == decoder.pictures.end()) throw std::runtime_error("Videodec2: unknown frame buffer"); + const auto& stored = picture->second; + *header = {stored.pts, stored.dts, stored.attached, stored.idr, stored.hevcIdr, decoder.profile, decoder.level, stored.crop, stored.bitDepthMinus8}; +} + +void Videodec2PictureCrop_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Crop* crop) { + if (!crop) throw std::runtime_error("Videodec2: invalid argument pointer"); + PictureHeader header{}; + Videodec2PictureHeader_nid_no_patch(handle, frameBuffer, &header); + *crop = header.crop; +} + } diff --git a/core/libs/prx/libSceVideodec2/Unimplemented.cpp b/core/libs/prx/libSceVideodec2/Unimplemented.cpp new file mode 100644 index 0000000000..b427d652ac --- /dev/null +++ b/core/libs/prx/libSceVideodec2/Unimplemented.cpp @@ -0,0 +1,61 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVideodec2TermLibrary() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2QueryLibraryInfo() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2QueryDecoderInfo() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2MapMemory() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2MapDirectMemory(void*, const void*) { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2PresetDecoderConfigInfo() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2PresetDecoderConfigInfoAvc() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2PresetDecoderConfigInfoHevc() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2PresetDecoderConfigInfoVp9() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2CreateDecoderBid() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2CreateHevcDecoder() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2QueryHevcDecoderMemoryInfo() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2GetHevcPictureInfo() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2InitLibrary() { + NotImplemented_nid_no_patch(__func__); +} +} diff --git a/core/libs/prx/libSceVisionManager/Export.cpp b/core/libs/prx/libSceVisionManager/Export.cpp new file mode 100644 index 0000000000..91dd5c2463 --- /dev/null +++ b/core/libs/prx/libSceVisionManager/Export.cpp @@ -0,0 +1,288 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVisionManagerApplyCalibrateHmdResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerCancelRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerFinalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGenerateRegisterUserData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGenerateRegisterUserDataFinalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGenerateRegisterUserDataInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetCalibrateTrackingLedInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetCalibrateTrackingLedNecessity() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetHitCountOfRegisterUserData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetLibraryVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetNumOfRegisterUserDataSlot() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetRegisterUserDataSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetRegisterUserDataVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetResultOfCalibrateHmd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetResultOfCalibrateStereoCamera() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetResultOfCalibrateTrackingLed() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetResultOfFindFace() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetResultOfFindPad() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetResultOfFindPadCorrespondRegion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetResultOfFindUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetResultOfFindUserInRegion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetResultOfFindUserWithoutIdentify() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetResultOfRegisterUserInRegion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetStoreCameraImgYPtr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerGetWorkingMemorySize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerLoadSettings() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRegistryClear() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRequestCalibrateHmd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRequestCalibrateStereoCamera() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRequestCalibrateTrackingLed() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRequestClose() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRequestFindFace() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRequestFindPad() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRequestFindPadCorrespondRegion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRequestFindUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRequestFindUserInRegion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRequestFindUserWithoutIdentify() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRequestHeadDetection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRequestInitializeRegisterUserData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRequestOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRequestPadTracking() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRequestRecogUtility() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRequestRegisterUserInRegion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerRequestStoreCameraImgY() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerSetCallbackForUpdateCalibrateHmdResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerSetCallbackForUpdateCalibrateStereoCameraResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerSetCallbackForUpdateCalibrateTrackingLedResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerSetCallbackForUpdateCameraFrame() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerSetCallbackForUpdateCtrlCore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerSetCallbackForUpdateRecog() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerSetCallbackForUpdateRunningStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerSetCallbackForUpdateStoreFrame() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVisionManagerWaitResponse() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceVnaInternal/Export.cpp b/core/libs/prx/libSceVnaInternal/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceVnaInternal/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceVnaWebsocket/Export.cpp b/core/libs/prx/libSceVnaWebsocket/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceVnaWebsocket/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceVoice/CMakeLists.txt b/core/libs/prx/libSceVoice/CMakeLists.txt new file mode 100644 index 0000000000..e2cc5765c8 --- /dev/null +++ b/core/libs/prx/libSceVoice/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceVoice SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceVoice PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceVoice) + +if(BUILD_TESTING) + add_test_executable(guest_voice_tests ${LIBS_INCLUDE_DIR}/tests/GuestVoice.cpp) + target_include_directories(guest_voice_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_voice_tests PRIVATE libSceVoice libc) + configure_windows_unwind(guest_voice_tests) + add_test(NAME guest_voice COMMAND guest_voice_tests) + if(WIN32) + set_tests_properties(guest_voice PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index ba55f55dd2..3357ee67ea 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -1,5 +1,8 @@ #include #include +#include +#include +#include #include #include #include @@ -18,20 +21,32 @@ constexpr std::int32_t PortOutPcm = 3; 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 int VoiceErrorNotInitialized = static_cast(0x804E0801u); +constexpr auto NotInitialized = static_cast(0x804e0801u); +constexpr auto InvalidPort = static_cast(0x804e0804u); +constexpr auto InvalidArgument = static_cast(0x804e0805u); +constexpr auto PortNotFound = static_cast(0x804e0807u); +constexpr std::int32_t ThreadPriorityHighest = 256; +constexpr std::int32_t ThreadPriorityLowest = 767; +constexpr std::uint64_t ThreadAffinityAll = 0x1fff; struct Port { std::int32_t type; float volume; std::uint32_t bitrate; + std::uint16_t bufferSize; + std::uint16_t frameSize; bool muted = false; + bool paused = false; }; struct Voice { std::mutex mutex; bool initialized = false; bool started = false; - std::uint32_t nextId = 0; + std::int32_t appType = 0; + bool mutedAll = false; + std::deque freePorts; std::map ports; std::set> connections; }; @@ -45,6 +60,10 @@ Voice& State() { throw std::logic_error(std::string(function) + ": " + why); } +bool IsThreadSetting(std::int32_t priority, std::uint64_t affinity) { + return priority >= ThreadPriorityHighest && priority <= ThreadPriorityLowest && affinity != 0 && (affinity & ~ThreadAffinityAll) == 0; +} + Port& RequirePort(Voice& voice, std::uint32_t id, const char* function) { if (!voice.initialized) Fail(function, "library not initialized"); const auto found = voice.ports.find(id); @@ -54,30 +73,60 @@ Port& RequirePort(Voice& voice, std::uint32_t id, const char* function) { bool IsInput(std::int32_t type) { return type == PortInDevice || type == PortInPcm || type == PortInVoice; } +bool VoiceFrame(std::int32_t bitrate, std::uint16_t& frameSize, std::uint16_t& frameDuration) { + switch (bitrate) { + case 3850: frameSize = 20; frameDuration = 40; return true; + case 4650: frameSize = 24; frameDuration = 40; return true; + case 5700: frameSize = 15; frameDuration = 20; return true; + case 7300: frameSize = 19; frameDuration = 20; return true; + case 11800: frameSize = 15; frameDuration = 10; return true; + case 12200: frameSize = 16; frameDuration = 10; return true; + case 14400: frameSize = 27; frameDuration = 15; return true; + case 16000: frameSize = 20; frameDuration = 10; return true; + case 22533: frameSize = 43; frameDuration = 15; return true; + default: return false; + } +} + } extern "C" { -int APS5_VABI sceVoiceInit(VoiceInitParam* param, int32_t version) { - (void)version; - if (param == nullptr) APS5_INVALID_ARG_EX; +int APS5_VABI sceVoiceInit(const VoiceInitParam* param, std::int32_t version) { auto& voice = State(); std::lock_guard lock(voice.mutex); - if (voice.initialized) Fail(__func__, "already initialized"); + if (voice.initialized) return static_cast(0x804e0802u); + if (!param || version != 100) return InvalidArgument; + if (param->app_type != 4 && param->app_type != 0x08000000 && param->app_type != 0x10000000 && param->app_type != 0x20000000) return InvalidArgument; + voice.appType = param->app_type; + voice.freePorts.clear(); + for (std::uint32_t id = 0; id < 128; ++id) voice.freePorts.push_back(id); voice.initialized = true; return 0; } -int APS5_VABI sceVoiceSetThreadsParams(void* params) { - if (params == nullptr) APS5_INVALID_ARG_EX; +int APS5_VABI sceVoiceSetThreadsParams(const VoiceThreadsParams* params) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + if (!params) return InvalidArgument; + if (voice.started) return static_cast(0x804e080bu); + if (params->mode == 0) return 0; + if (params->mode != 1) return InvalidArgument; + if (!IsThreadSetting(params->firstPriority, params->firstAffinity) || !IsThreadSetting(params->secondPriority, params->secondAffinity) || !IsThreadSetting(params->thirdPriority, params->thirdAffinity)) { + Fail(__func__, "the result for a priority outside 256-767 or an affinity mask outside 0x1fff is unknown"); + } return 0; } int APS5_VABI sceVoiceStart(const VoiceStartParam* param) { - if (param == nullptr) APS5_INVALID_ARG_EX; auto& voice = State(); std::lock_guard lock(voice.mutex); - if (!voice.initialized) Fail(__func__, "library not initialized"); + if (!voice.initialized) return NotInitialized; + if (!param || !param->container) return InvalidArgument; + if (voice.started) return 0; + const std::uint32_t memorySize = voice.appType == 0x10000000 || voice.appType == 0x20000000 ? 0x40000 : 0xc0000; + if (param->mem_size != memorySize) return static_cast(0x804e0806u); voice.started = true; return 0; } @@ -85,7 +134,7 @@ int APS5_VABI sceVoiceStart(const VoiceStartParam* param) { int APS5_VABI sceVoiceStop(void) { auto& voice = State(); std::lock_guard lock(voice.mutex); - if (!voice.initialized) Fail(__func__, "library not initialized"); + if (!voice.initialized) return NotInitialized; voice.started = false; return 0; } @@ -93,33 +142,78 @@ int APS5_VABI sceVoiceStop(void) { int APS5_VABI sceVoiceEnd_nid_postfix(void) { auto& voice = State(); std::lock_guard lock(voice.mutex); - if (!voice.initialized) Fail(__func__, "library not initialized"); + if (!voice.initialized) return NotInitialized; voice.ports.clear(); voice.connections.clear(); voice.started = false; + voice.appType = 0; + voice.mutedAll = false; voice.initialized = false; return 0; } -int APS5_VABI sceVoiceCreatePort(uint32_t* port_id, const VoicePortParam* param) { - if (port_id == nullptr || param == nullptr) APS5_INVALID_ARG_EX; - if (param->port_type < PortInDevice || param->port_type > PortOutDevice) Fail(__func__, "unknown port type " + std::to_string(param->port_type)); +int APS5_VABI sceVoiceCreatePort(std::uint32_t* portId, const VoicePortParam* param) { auto& voice = State(); std::lock_guard lock(voice.mutex); - if (!voice.initialized) Fail(__func__, "library not initialized"); - const bool voicePort = param->port_type == PortInVoice || param->port_type == PortOutVoice; - const auto id = voice.nextId++; - voice.ports.emplace(id, Port{param->port_type, param->volume, voicePort ? static_cast(param->voice.bitrate) : 0u}); - *port_id = id; + if (!voice.initialized) return NotInitialized; + if (!portId || !param) return InvalidArgument; + *portId = 255; + std::uint16_t frameSize = 512; + std::uint16_t frameDuration = 16; + const auto type = param->port_type; + if (type < PortInDevice || type > PortOutDevice) return InvalidArgument; + if (type == PortInPcm || type == PortOutPcm) { + if (param->pcmaudio.sample_rate != 16000 || param->pcmaudio.data_type != 0) return InvalidArgument; + } else if (type == PortInVoice || type == PortOutVoice) { + if (!VoiceFrame(param->voice.bitrate, frameSize, frameDuration)) return InvalidArgument; + } + constexpr auto resourceFull = static_cast(0x804e0808u); + if (voice.freePorts.empty()) return resourceFull; + std::uint32_t count = 0; + std::uint32_t pcmCount = 0; + std::uint32_t pcmBytes = 0; + for (const auto& [id, port] : voice.ports) { + if (port.type == type) ++count; + if (port.type == PortInPcm || port.type == PortOutPcm) { + ++pcmCount; + pcmBytes += 2u * port.bufferSize; + } + } + std::uint16_t bufferSize = 2048; + if (type == PortInDevice || type == PortOutDevice) { + const std::uint32_t limit = type == PortOutDevice && voice.appType == 4 ? 5 : 4; + if (count == limit) return resourceFull; + if (type == PortInDevice ? param->device.type != 0 : param->device.type != 2 && param->device.type != 14) return InvalidArgument; + bufferSize = 4096; + } else if (type == PortInPcm || type == PortOutPcm) { + bufferSize = static_cast(param->pcmaudio.buffer_size - param->pcmaudio.buffer_size % frameSize); + if (bufferSize < 1024 || bufferSize > 64512) return InvalidArgument; + if (type == PortInPcm && bufferSize / frameSize * frameDuration < param->threshold) return resourceFull; + const std::uint32_t limit = voice.appType == 4 ? 128 : voice.appType == 0x08000000 ? 30 : 14; + const std::uint32_t byteLimit = voice.appType == 4 || voice.appType == 0x08000000 ? 0x80000 : voice.appType == 0x10000000 ? 0x5000 : 0x10000; + if (pcmCount == limit || pcmBytes + 2u * bufferSize > byteLimit) return resourceFull; + } else { + if (type == PortInVoice && bufferSize / frameSize * frameDuration < param->threshold) return resourceFull; + const std::uint32_t limit = voice.appType == 4 ? 0 : type == PortOutVoice ? 4 : voice.appType == 0x20000000 ? 7 : 15; + if (count == limit) return resourceFull; + } + const auto id = voice.freePorts.front(); + const bool voicePort = type == PortInVoice || type == PortOutVoice; + voice.ports.emplace(id, Port{type, param->volume, voicePort ? static_cast(param->voice.bitrate) : 0u, bufferSize, frameSize, param->mute != 0}); + voice.freePorts.pop_front(); + *portId = id; return 0; } int APS5_VABI sceVoiceDeletePort(uint32_t port_id) { auto& voice = State(); std::lock_guard lock(voice.mutex); - RequirePort(voice, port_id, __func__); + if (!voice.initialized) return NotInitialized; + if (port_id > 127) return InvalidPort; + if (!voice.ports.contains(port_id)) return PortNotFound; std::erase_if(voice.connections, [port_id](const auto& link) { return link.first == port_id || link.second == port_id; }); voice.ports.erase(port_id); + voice.freePorts.push_back(port_id); return 0; } @@ -141,50 +235,86 @@ int APS5_VABI sceVoiceDisconnectIPortFromOPort(uint32_t input_port_id, uint32_t return 0; } -int APS5_VABI sceVoiceGetBitRate(uint32_t port_id, uint32_t* bitrate) { - if (bitrate == nullptr) APS5_INVALID_ARG_EX; +int APS5_VABI sceVoiceGetBitRate(std::uint32_t portId, std::uint32_t* bitrate) { auto& voice = State(); std::lock_guard lock(voice.mutex); - const auto& port = RequirePort(voice, port_id, __func__); - if (port.type != PortInVoice && port.type != PortOutVoice) Fail(__func__, "port does not carry encoded voice"); + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + if (!bitrate) return InvalidArgument; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + const auto& port = found->second; + if (port.type != PortInVoice && port.type != PortOutVoice) return PortNotFound; *bitrate = port.bitrate; return 0; } -int APS5_VABI sceVoiceSetVolume(uint32_t port_id, float volume) { +int APS5_VABI sceVoiceSetVolume(std::uint32_t portId, float volume) { auto& voice = State(); std::lock_guard lock(voice.mutex); - RequirePort(voice, port_id, __func__).volume = volume; + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + found->second.volume = std::isfinite(volume) ? volume : 0.0f; return 0; } -int APS5_VABI sceVoiceGetVolume(uint32_t port_id, float* volume) { - if (volume == nullptr) APS5_INVALID_ARG_EX; +int APS5_VABI sceVoiceGetVolume(std::uint32_t portId, float* volume) { auto& voice = State(); std::lock_guard lock(voice.mutex); - *volume = RequirePort(voice, port_id, __func__).volume; + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + if (!volume) return InvalidArgument; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + *volume = found->second.volume; return 0; } int APS5_VABI sceVoiceGetPortInfo(uint32_t port_id, VoicePortInfo* info) { - if (info == nullptr) APS5_INVALID_ARG_EX; auto& voice = State(); std::lock_guard lock(voice.mutex); + if (!voice.initialized) return VoiceErrorNotInitialized; + if (info == nullptr) APS5_INVALID_ARG_EX; const auto& port = RequirePort(voice, port_id, __func__); - *info = VoicePortInfo{}; info->port_type = port.type; info->state = PortStateReady; - if (port.bitrate != 0) info->frame_size = (port.bitrate * VoiceFrameMs + 7999u) / 8000u; + info->byte_count = 0; + info->frame_size = port.bitrate != 0 ? port.frameSize : 2; return 0; } -int APS5_VABI sceVoiceGetPortAttr(uint32_t port_id, int32_t attr, void* value, int32_t size) { - (void)value; - (void)size; +int APS5_VABI sceVoiceGetPortAttr(std::uint32_t portId, std::int32_t attr, void* value, std::int32_t size) { auto& voice = State(); std::lock_guard lock(voice.mutex); - RequirePort(voice, port_id, __func__); - Fail(__func__, "attribute " + std::to_string(attr) + " not implemented"); + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + if (value == nullptr) return InvalidArgument; + std::int32_t expectedSize; + switch (attr) { + case 1000: + case 1001: + case 2000: + case 2002: + expectedSize = 1; + break; + case 1002: + case 2001: + expectedSize = 4; + break; + case 1003: + expectedSize = 2; + break; + default: + return InvalidArgument; + } + if (size != expectedSize) return InvalidArgument; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return InvalidPort; + const auto requiredType = attr == 1003 ? PortOutDevice : PortInDevice; + if (found->second.type != requiredType) return InvalidArgument; + Fail(__func__, "device attribute backend is not implemented"); } int APS5_VABI sceVoiceReadFromOPort(uint32_t output_port_id, void* data, uint32_t* size) { @@ -205,39 +335,191 @@ int APS5_VABI sceVoiceWriteToIPort(uint32_t input_port_id, const void* data, uin return 0; } -int APS5_VABI sceVoiceSetMuteFlag(uint32_t port_id, bool muted) { +int APS5_VABI sceVoiceSetMuteFlag(std::uint32_t portId, std::uint8_t muted) { auto& voice = State(); std::lock_guard lock(voice.mutex); - RequirePort(voice, port_id, __func__).muted = muted; + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + found->second.muted = muted != 0; return 0; } -int APS5_VABI sceVoiceGetMuteFlag(uint32_t port_id, bool* muted) { - if (muted == nullptr) APS5_INVALID_ARG_EX; +int APS5_VABI sceVoiceGetMuteFlag(std::uint32_t portId, bool* muted) { auto& voice = State(); std::lock_guard lock(voice.mutex); - *muted = RequirePort(voice, port_id, __func__).muted; + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + if (!muted) return InvalidArgument; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + *muted = found->second.muted || voice.mutedAll; return 0; } -int APS5_VABI sceVoiceGetResourceInfo(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVoiceGetResourceInfo(VoiceResourceInfo* info) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + if (info == nullptr) return InvalidArgument; + switch (voice.appType) { + case 4: + *info = {0, 0, 4, 5}; + break; + case 0x08000000: + case 0x10000000: + *info = {15, 4, 4, 4}; + break; + case 0x20000000: + *info = {7, 4, 4, 4}; + break; + default: + Fail(__func__, "unsupported initialization mode"); + } return 0; } int APS5_VABI sceVoiceEnableChat(void) { - NotImplemented_nid_no_patch(__func__); + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + Fail(__func__, "audio chat backend is not implemented"); +} + +int APS5_VABI sceVoiceResetPort(std::uint32_t portId) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + if (voice.started && (found->second.type == PortInDevice || found->second.type == PortOutDevice)) Fail(__func__, "device reset backend is not implemented"); return 0; } -int APS5_VABI sceVoiceResetPort(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVoicePausePort(std::uint32_t portId) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + found->second.paused = true; + return 0; +} + +int APS5_VABI sceVoiceResumePort(std::uint32_t portId) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + found->second.paused = false; return 0; } int APS5_VABI sceVoiceDisableChat(void) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + Fail(__func__, "audio chat backend is not implemented"); +} + +int APS5_VABI sceVoiceInitHQ(const VoiceInitParam* param, std::int32_t version) { + return sceVoiceInit(param, version); +} + +int APS5_VABI sceVoiceGetMemorySize(void) { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceVoiceDeviceInCustomerData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceQAEnableRecording(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceVADAdjustment(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceVADPVTAdjustment(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceVADSetVersion(std::int32_t version) { + return version == 0 || version == 1 ? 0 : InvalidArgument; +} + +int APS5_VABI sceVoiceSetMuteFlagAll(bool muted) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + voice.mutedAll = muted; + return 0; +} + +int APS5_VABI sceVoiceSetBitRate(std::uint32_t portId, std::uint32_t bitrate) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + auto& port = found->second; + if (port.type != PortInVoice && port.type != PortOutVoice) Fail(__func__, "port does not carry encoded voice"); + std::uint16_t frameSize = 0; + std::uint16_t frameDuration = 0; + if (bitrate > INT32_MAX || !VoiceFrame(static_cast(bitrate), frameSize, frameDuration)) Fail(__func__, "unsupported bitrate " + std::to_string(bitrate)); + port.bitrate = bitrate; + port.frameSize = frameSize; + return 0; +} + +int APS5_VABI sceVoiceUpdatePort(std::uint32_t portId, const VoicePortParam* param) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return NotInitialized; + if (portId > 127) return InvalidPort; + if (!param) Fail(__func__, "null port parameters"); + const auto found = voice.ports.find(portId); + if (found == voice.ports.end()) return PortNotFound; + auto& port = found->second; + if (param->port_type != port.type) Fail(__func__, "port type " + std::to_string(param->port_type) + " differs from the port's " + std::to_string(port.type)); + std::uint16_t frameSize = port.frameSize; + std::uint16_t frameDuration = 0; + const bool voicePort = port.type == PortInVoice || port.type == PortOutVoice; + if (voicePort && !VoiceFrame(param->voice.bitrate, frameSize, frameDuration)) Fail(__func__, "unsupported bitrate " + std::to_string(param->voice.bitrate)); + port.muted = param->mute != 0; + port.volume = std::isfinite(param->volume) ? param->volume : 0.0f; + if (voicePort) { + port.bitrate = static_cast(param->voice.bitrate); + port.frameSize = frameSize; + } + return 0; +} + +int APS5_VABI sceVoicePausePortAll(void) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return VoiceErrorNotInitialized; + return 0; +} + +int APS5_VABI sceVoiceResumePortAll(void) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return VoiceErrorNotInitialized; + return 0; +} + } diff --git a/core/libs/prx/libSceVoiceChat/Export.cpp b/core/libs/prx/libSceVoiceChat/Export.cpp index 03569b4890..49e582df5f 100644 --- a/core/libs/prx/libSceVoiceChat/Export.cpp +++ b/core/libs/prx/libSceVoiceChat/Export.cpp @@ -65,4 +65,101 @@ VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestCreateVoiceChatGroup) VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestJoinVoiceChatGroup) VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestLeaveVoiceChatGroup) VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestDeleteVoiceChatGroup) + +VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequest0) +VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequest1) +VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestAcsTts) +VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestAction) +VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestSetMemberVolume) +VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestUpdateGameSessionVoiceChatChannelName) +VOICECHAT_OFFLINE_REQUEST(sceVoiceChatRequestUpdatePlayerSessionVoiceChatChannelName) + +int APS5_VABI sceVoiceChatAbortRequest(int requestId) { + (void)requestId; + return 0; +} + +int APS5_VABI sceVoiceChatGetActiveChannel(void* channel) { + (void)channel; + logSoft(__func__, "SIGNED_OUT (offline)"); + return VOICECHAT_ERROR_SIGNED_OUT; +} + +int APS5_VABI sceVoiceChatGetChannelMemberActiveChannelMatchingState(void* a, void* b) { + (void)a; + (void)b; + logSoft(__func__, "SIGNED_OUT (offline)"); + return VOICECHAT_ERROR_SIGNED_OUT; +} + +int APS5_VABI sceVoiceChatGetChannelMemberVoiceConnectionState(void* a, void* b) { + (void)a; + (void)b; + logSoft(__func__, "SIGNED_OUT (offline)"); + return VOICECHAT_ERROR_SIGNED_OUT; +} + +int APS5_VABI sceVoiceChatGetChatChannelMemberInfoList(void* a, void* b) { + (void)a; + (void)b; + logSoft(__func__, "SIGNED_OUT (offline)"); + return VOICECHAT_ERROR_SIGNED_OUT; +} + +int APS5_VABI sceVoiceChatGetChatGroupMemberInfoList(void* a, void* b) { + (void)a; + (void)b; + logSoft(__func__, "SIGNED_OUT (offline)"); + return VOICECHAT_ERROR_SIGNED_OUT; +} + +int APS5_VABI sceVoiceChatGetMicState(void* state) { + (void)state; + logSoft(__func__, "SIGNED_OUT (offline)"); + return VOICECHAT_ERROR_SIGNED_OUT; +} + +int APS5_VABI sceVoiceChatGetUserVoiceVolumeGain(void* a, void* gain) { + (void)a; + (void)gain; + logSoft(__func__, "SIGNED_OUT (offline)"); + return VOICECHAT_ERROR_SIGNED_OUT; +} + +int APS5_VABI sceVoiceChatSetUserVoiceVolumeGain(void* a, int32_t gain) { + (void)a; + (void)gain; + logSoft(__func__, "SIGNED_OUT (offline)"); + return VOICECHAT_ERROR_SIGNED_OUT; +} + +int APS5_VABI sceVoiceChatRegisterChannelEventHandler(void* a, void* b) { (void)a; (void)b; return 0; } +int APS5_VABI sceVoiceChatRegisterDaemonEventHandler(void* a, void* b) { (void)a; (void)b; return 0; } +int APS5_VABI sceVoiceChatRegisterMemberEventHandler(void* a, void* b) { (void)a; (void)b; return 0; } +int APS5_VABI sceVoiceChatRegisterPartyUserStateChangedHandler(void* a, void* b) { (void)a; (void)b; return 0; } +int APS5_VABI sceVoiceChatRegisterVoiceEventHandler(void* a, void* b) { (void)a; (void)b; return 0; } + +int APS5_VABI sceVoiceChatStartAccessibilityEvent(void* a) { (void)a; return 0; } +int APS5_VABI sceVoiceChatStartChannelEvent(void* a) { (void)a; return 0; } +int APS5_VABI sceVoiceChatStartDaemonEvent(void* a) { (void)a; return 0; } +int APS5_VABI sceVoiceChatStartMemberEvent(void* a) { (void)a; return 0; } +int APS5_VABI sceVoiceChatStartRangedMemberEvent(void* a) { (void)a; return 0; } +int APS5_VABI sceVoiceChatStartVoiceEvent(void* a) { (void)a; return 0; } + +int APS5_VABI sceVoiceChatStopAccessibilityEvent(void* a) { (void)a; return 0; } +int APS5_VABI sceVoiceChatStopChannelEvent(void* a) { (void)a; return 0; } +int APS5_VABI sceVoiceChatStopDaemonEvent(void* a) { (void)a; return 0; } +int APS5_VABI sceVoiceChatStopMemberEvent(void* a) { (void)a; return 0; } +int APS5_VABI sceVoiceChatStopRangedMemberEvent(void* a) { (void)a; return 0; } +int APS5_VABI sceVoiceChatStopVoiceEvent(void* a) { (void)a; return 0; } + +int APS5_VABI sceVoiceChatUnregisterAccessibilityEventHandler(void* a) { (void)a; return 0; } +int APS5_VABI sceVoiceChatUnregisterChannelEventHandler(void* a) { (void)a; return 0; } +int APS5_VABI sceVoiceChatUnregisterDaemonEventHandler(void* a) { (void)a; return 0; } +int APS5_VABI sceVoiceChatUnregisterMemberEventHandler(void* a) { (void)a; return 0; } +int APS5_VABI sceVoiceChatUnregisterMicEventHandler(void* a) { (void)a; return 0; } +int APS5_VABI sceVoiceChatUnregisterPartyUserStateChangedHandler(void* a) { (void)a; return 0; } +int APS5_VABI sceVoiceChatUnregisterRangedMemberEventHandler(void* a) { (void)a; return 0; } +int APS5_VABI sceVoiceChatUnregisterVoiceEventHandler(void* a) { (void)a; return 0; } + } diff --git a/core/libs/prx/libSceVoiceCommand/Export.cpp b/core/libs/prx/libSceVoiceCommand/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceVoiceCommand/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index e8213885a5..6c52605b5e 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -1,6 +1,10 @@ +#include #include #include +#include +#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -11,86 +15,247 @@ constexpr std::int32_t VOICE_QOS_APP_TYPE_10000000 = 0x10000000; constexpr std::uint32_t VOICE_QOS_MEMORY_SIZE = 0x40000; 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_LIMIT = static_cast(0x804E0907); constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_INITIALIZED = static_cast(0x804E0905); -std::mutex g_mutex; -bool g_initialized = false; +struct Endpoint {}; + +struct Connection { + std::int32_t local; + std::int32_t remote; +}; + +struct QoS { + std::mutex mutex; + bool initialized = false; + std::int32_t endpointLimit = 0; + std::map locals; + std::map remotes; + std::map connections; +}; + +QoS& State() { + static QoS qos; + return qos; +} + +int CreateEndpoint(std::int32_t* id, std::map& endpoints, std::int32_t limit) { + if (!id) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + for (std::int32_t candidate = 0; candidate < limit; ++candidate) { + if (!endpoints.contains(candidate)) { + endpoints.emplace(candidate, Endpoint{}); + *id = candidate; + return 0; + } + } + *id = -1; + return SCE_VOICE_ERROR_LIBVOICEQOS_LIMIT; +} + +int DeleteEndpoint(std::int32_t id, std::map& endpoints) { + return endpoints.erase(id) != 0 ? 0 : SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; +} + +template +int Locked(TBody&& 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.endpointLimit = appType == VOICE_QOS_APP_TYPE_10000000 ? 15 : 7; + 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 Locked([](QoS& qos) { + qos.connections.clear(); + qos.locals.clear(); + qos.remotes.clear(); + qos.initialized = false; + return 0; + }); +} + +int APS5_VABI sceVoiceQoSCreateLocalEndpoint(int32_t* localId, int32_t userId, int32_t deviceInId, int32_t deviceOutId) { + (void)deviceInId; + (void)deviceOutId; + return Locked([&](QoS& qos) { + if (localId && userId == -1) { + *localId = -1; + return SCE_VOICE_ERROR_LIBVOICEQOS_LIMIT; + } + return CreateEndpoint(localId, qos.locals, 1); + }); +} + +int APS5_VABI sceVoiceQoSDeleteLocalEndpoint(int32_t localId) { + return Locked([&](QoS& qos) { return DeleteEndpoint(localId, qos.locals); }); +} + +int APS5_VABI sceVoiceQoSCreateRemoteEndpoint(int32_t* remoteId) { + return Locked([&](QoS& qos) { return CreateEndpoint(remoteId, qos.remotes, qos.endpointLimit); }); +} + +int APS5_VABI sceVoiceQoSDeleteRemoteEndpoint(int32_t remoteId) { + return Locked([&](QoS& qos) { + const int result = DeleteEndpoint(remoteId, qos.remotes); + if (result != 0) return result; + for (auto it = qos.connections.begin(); it != qos.connections.end(); ++it) { + if (it->second.remote == remoteId) { + qos.connections.erase(it); + break; + } + } + return 0; + }); +} + +int APS5_VABI sceVoiceQoSConnect(int32_t* connectionId, int32_t localId, int32_t remoteId) { + return Locked([&](QoS& qos) { + if (!connectionId || localId != 0 || remoteId < 0 || remoteId >= qos.endpointLimit) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + if (!qos.locals.contains(localId) || !qos.remotes.contains(remoteId)) throw std::runtime_error("sceVoiceQoSConnect: missing voice port error behavior is not implemented"); + for (std::int32_t candidate = 0; candidate < qos.endpointLimit; ++candidate) { + if (!qos.connections.contains(candidate)) { + qos.connections.emplace(candidate, Connection{localId, remoteId}); + *connectionId = candidate; + return 0; + } + } + *connectionId = -1; + return SCE_VOICE_ERROR_LIBVOICEQOS_LIMIT; + }); +} + +int APS5_VABI sceVoiceQoSDisconnect(int32_t connectionId) { + return Locked([&](QoS& qos) { + if (connectionId < 0 || connectionId >= qos.endpointLimit) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + qos.connections.erase(connectionId); + return 0; + }); +} + +int APS5_VABI sceVoiceQoSSetConnectionAttribute(std::int32_t connectionId, std::int32_t attributeId, const void* value, std::int32_t size) { + 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&) -> int { + if (localId != 0 || !value || attributeId < 0 || attributeId > 3 || size != (attributeId % 2 == 0 ? 4 : 1)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + throw std::runtime_error("sceVoiceQoSSetLocalEndpointAttribute: voice port attributes are not implemented"); + }); +} + +int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute(int32_t remoteId, int32_t attributeId, const void* value, int32_t size) { + return Locked([&](QoS& qos) -> int { + if (remoteId < 0 || remoteId >= qos.endpointLimit || !value || attributeId != 7 || size != 1) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + throw std::runtime_error("sceVoiceQoSSetRemoteEndpointAttribute: voice port attributes are not implemented"); + }); +} + +int APS5_VABI sceVoiceQoSReadPacket(int32_t connectionId, void* buffer, uint32_t* size) { + return Locked([&](QoS& qos) { + if (!buffer || !size || !qos.connections.contains(connectionId)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + *size = 0; + return 0; + }); +} + +int APS5_VABI sceVoiceQoSWritePacket(int32_t connectionId, const void* buffer, uint32_t* size) { + return Locked([&](QoS& qos) { + if (!buffer || !size || !qos.connections.contains(connectionId)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + return 0; + }); +} + +int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute(int32_t localId, int32_t attributeId, void* value, int32_t size) { + NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVoiceQoSCreateLocalEndpoint() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSGetRemoteEndpointAttribute(int32_t remoteId, int32_t attributeId, void* value, int32_t size) { + (void)remoteId; + (void)attributeId; + (void)value; + (void)size; + NotImplemented_nid_no_patch(__func__); + return 0; } -int APS5_VABI sceVoiceQoSDeleteLocalEndpoint() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSGetConnectionAttribute(int32_t connectionId, int32_t attributeId, void* value, int32_t size) { + (void)connectionId; + (void)attributeId; + (void)value; + (void)size; + NotImplemented_nid_no_patch(__func__); + return 0; } -int APS5_VABI sceVoiceQoSCreateRemoteEndpoint() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSGetLocalEndpoint(int32_t connectionId, int32_t* localId) { + return Locked([&](QoS& qos) { + if (!localId || connectionId < 0 || connectionId >= qos.endpointLimit) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + const auto it = qos.connections.find(connectionId); + *localId = it == qos.connections.end() ? -1 : it->second.local; + return 0; + }); } -int APS5_VABI sceVoiceQoSDeleteRemoteEndpoint() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSGetRemoteEndpoint(int32_t connectionId, int32_t* remoteId) { + return Locked([&](QoS& qos) { + if (!remoteId || connectionId < 0 || connectionId >= qos.endpointLimit) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + const auto it = qos.connections.find(connectionId); + *remoteId = it == qos.connections.end() ? -1 : it->second.remote; + return 0; + }); } -int APS5_VABI sceVoiceQoSConnect() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSEnableChat() { + NotImplemented_nid_no_patch(__func__); + return 0; } -int APS5_VABI sceVoiceQoSDisconnect() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSDisableChat() { + NotImplemented_nid_no_patch(__func__); + return 0; } -int APS5_VABI sceVoiceQoSSetConnectionAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSGetStatus(int32_t connectionId, int32_t statusId, void* value, int32_t size) { + (void)connectionId; + (void)statusId; + (void)value; + (void)size; + NotImplemented_nid_no_patch(__func__); + return 0; } -int APS5_VABI sceVoiceQoSSetLocalEndpointAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSDebugGetStatus(std::int32_t connectionId, std::int32_t statusId, void* value, std::int32_t size) { + NotImplemented_nid_no_patch(__func__); + return 0; } -int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSInitHQ(void* memory, std::uint32_t memorySize, std::int32_t appType) { + NotImplemented_nid_no_patch(__func__); + return 0; } -int APS5_VABI sceVoiceQoSReadPacket() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSSetMode() { + NotImplemented_nid_no_patch(__func__); + return 0; } -int APS5_VABI sceVoiceQoSWritePacket() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute() { +int APS5_VABI sceVoiceQoSTerminate() { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceVorbisDec/CMakeLists.txt b/core/libs/prx/libSceVorbisDec/CMakeLists.txt new file mode 100644 index 0000000000..0da3f0a43e --- /dev/null +++ b/core/libs/prx/libSceVorbisDec/CMakeLists.txt @@ -0,0 +1,77 @@ +enable_language(C) + +set(OGG_DIR ${CMAKE_SOURCE_DIR}/3rdparty/ogg) +set(VORBIS_DIR ${CMAKE_SOURCE_DIR}/3rdparty/vorbis) +set(OGG_GENERATED_DIR ${CMAKE_CURRENT_BINARY_DIR}/ogg/include) + +include(CheckIncludeFiles) +check_include_files(inttypes.h INCLUDE_INTTYPES_H) +check_include_files(stdint.h INCLUDE_STDINT_H) +check_include_files(sys/types.h INCLUDE_SYS_TYPES_H) +set(SIZE16 int16_t) +set(USIZE16 uint16_t) +set(SIZE32 int32_t) +set(USIZE32 uint32_t) +set(SIZE64 int64_t) +set(USIZE64 uint64_t) +configure_file(${OGG_DIR}/include/ogg/config_types.h.in ${OGG_GENERATED_DIR}/ogg/config_types.h @ONLY) + +add_library(ogg STATIC EXCLUDE_FROM_ALL + ${OGG_DIR}/src/bitwise.c + ${OGG_DIR}/src/framing.c +) +target_include_directories(ogg PUBLIC ${OGG_DIR}/include ${OGG_GENERATED_DIR}) + +add_library(vorbis STATIC EXCLUDE_FROM_ALL + ${VORBIS_DIR}/lib/analysis.c + ${VORBIS_DIR}/lib/bitrate.c + ${VORBIS_DIR}/lib/block.c + ${VORBIS_DIR}/lib/codebook.c + ${VORBIS_DIR}/lib/envelope.c + ${VORBIS_DIR}/lib/floor0.c + ${VORBIS_DIR}/lib/floor1.c + ${VORBIS_DIR}/lib/info.c + ${VORBIS_DIR}/lib/lookup.c + ${VORBIS_DIR}/lib/lpc.c + ${VORBIS_DIR}/lib/lsp.c + ${VORBIS_DIR}/lib/mapping0.c + ${VORBIS_DIR}/lib/mdct.c + ${VORBIS_DIR}/lib/psy.c + ${VORBIS_DIR}/lib/registry.c + ${VORBIS_DIR}/lib/res0.c + ${VORBIS_DIR}/lib/sharedbook.c + ${VORBIS_DIR}/lib/smallft.c + ${VORBIS_DIR}/lib/synthesis.c + ${VORBIS_DIR}/lib/window.c +) +target_include_directories(vorbis PUBLIC ${VORBIS_DIR}/include PRIVATE ${VORBIS_DIR}/lib) +target_link_libraries(vorbis PUBLIC ogg) +if(NOT WIN32) + target_link_libraries(vorbis PRIVATE m) +endif() + +add_library(vorbisfile STATIC EXCLUDE_FROM_ALL ${VORBIS_DIR}/lib/vorbisfile.c) +target_include_directories(vorbisfile PRIVATE ${VORBIS_DIR}/lib) +target_link_libraries(vorbisfile PUBLIC vorbis) +set_target_properties(ogg vorbis vorbisfile PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) +if(MINGW) + foreach(library IN ITEMS ogg vorbis vorbisfile) + target_compile_options(${library} PRIVATE -fexceptions) + endforeach() +endif() + +add_library(libSceVorbisDec SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceVorbisDec PRIVATE ${LIBS_INCLUDE_DIR}) +target_compile_definitions(libSceVorbisDec PRIVATE OV_EXCLUDE_STATIC_CALLBACKS) +target_link_libraries(libSceVorbisDec PRIVATE vorbisfile vorbis ogg) +if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(libSceVorbisDec PRIVATE LINKER:--exclude-libs,ALL) +endif() +configure_windows_unwind(libSceVorbisDec) + +if(BUILD_TESTING) + add_library(vorbisenc STATIC EXCLUDE_FROM_ALL ${VORBIS_DIR}/lib/vorbisenc.c) + target_include_directories(vorbisenc PRIVATE ${VORBIS_DIR}/lib) + target_link_libraries(vorbisenc PUBLIC vorbis) + set_target_properties(vorbisenc PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) +endif() diff --git a/core/libs/prx/libSceVorbisDec/Export.cpp b/core/libs/prx/libSceVorbisDec/Export.cpp new file mode 100644 index 0000000000..b6b60cadf5 --- /dev/null +++ b/core/libs/prx/libSceVorbisDec/Export.cpp @@ -0,0 +1,325 @@ +#include "prx/libSceVorbisDec/include/GuestVorbisFile.hpp" +#include "prx/libc/include/General.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace VorbisDec; + +static_assert(sizeof(vorbis_comment) == sizeof(GuestComment)); +static_assert(offsetof(vorbis_comment, user_comments) == offsetof(GuestComment, user_comments)); +static_assert(offsetof(vorbis_comment, comment_lengths) == offsetof(GuestComment, comment_lengths)); +static_assert(offsetof(vorbis_comment, comments) == offsetof(GuestComment, comments)); +static_assert(offsetof(vorbis_comment, vendor) == offsetof(GuestComment, vendor)); +static_assert(SEEK_SET == 0 && SEEK_CUR == 1 && SEEK_END == 2); + +namespace { + +struct Handle { + OggVorbis_File file{}; + void* datasource = nullptr; + GuestCallbacks callbacks{}; + std::vector infos; + bool tellOutOfRange = false; + + ~Handle() { + file.datasource = nullptr; + ov_clear(&file); + } +}; + +struct Table { + std::mutex mutex; + std::unordered_map> handles; +}; + +Table& Handles() { + static auto* table = new Table; + return *table; +} + +Handle* Find(const void* vf) { + auto& table = Handles(); + const std::lock_guard lock(table.mutex); + const auto found = table.handles.find(vf); + return found == table.handles.end() ? nullptr : found->second.get(); +} + +std::unique_ptr Take(const void* vf) { + auto& table = Handles(); + const std::lock_guard lock(table.mutex); + const auto found = table.handles.find(vf); + if (found == table.handles.end()) return nullptr; + auto handle = std::move(found->second); + table.handles.erase(found); + return handle; +} + +void Store(const void* vf, std::unique_ptr handle) { + auto& table = Handles(); + const std::lock_guard lock(table.mutex); + table.handles[vf] = std::move(handle); +} + +void SyncInfos(Handle& handle) { + for (std::size_t link = 0; link < handle.infos.size(); ++link) { + const vorbis_info& info = handle.file.vi[link]; + handle.infos[link] = {info.version, info.channels, info.rate, info.bitrate_upper, info.bitrate_nominal, info.bitrate_lower, info.bitrate_window, info.codec_setup}; + } +} + +void RequireFile(const void* vf, const char* function) { + if (!vf) throw std::invalid_argument(std::string(function) + ": a null OggVorbis_File is not implemented"); +} + +template +auto WithHandle(const void* vf, const char* function, TCall call) { + RequireFile(vf, function); + if (Handle* handle = Find(vf)) { + const auto result = call(*handle); + SyncInfos(*handle); + return result; + } + Handle closed; + return call(closed); +} + +std::size_t ReadSource(void* buffer, std::size_t size, std::size_t count, void* source) { + const auto* handle = static_cast(source); + return handle->callbacks.read_func(buffer, size, count, handle->datasource); +} + +int SeekSource(void* source, ogg_int64_t offset, int whence) { + const auto* handle = static_cast(source); + return handle->callbacks.seek_func(handle->datasource, offset, whence); +} + +int CloseSource(void* source) { + const auto* handle = static_cast(source); + return handle->callbacks.close_func(handle->datasource); +} + +long TellSource(void* source) { + auto* handle = static_cast(source); + const std::int64_t position = handle->callbacks.tell_func(handle->datasource); + if (position < std::numeric_limits::min() || position > std::numeric_limits::max()) { + handle->tellOutOfRange = true; + return -1; + } + return static_cast(position); +} + +struct FilterContext { + GuestFilter filter; + void* param; +}; + +void RunFilter(float** pcm, long channels, long samples, void* context) { + const auto* filter = static_cast(context); + filter->filter(pcm, channels, samples, filter->param); +} + +} + +extern "C" { + +int APS5_VABI sceVorbisDecOpenCallbacks(void* datasource, void* vf, const char* initial, std::int64_t ibytes, GuestCallbacks callbacks) { + RequireFile(vf, __func__); + if (initial && (ibytes < std::numeric_limits::min() || ibytes > std::numeric_limits::max())) { + throw std::runtime_error(std::string(__func__) + ": an initial byte count outside the host long range is not implemented"); + } + Take(vf).reset(); + auto handle = std::make_unique(); + handle->datasource = datasource; + handle->callbacks = callbacks; + const ov_callbacks host{ + callbacks.read_func ? ReadSource : nullptr, + callbacks.seek_func ? SeekSource : nullptr, + callbacks.close_func ? CloseSource : nullptr, + callbacks.tell_func ? TellSource : nullptr, + }; + const int result = ov_open_callbacks(datasource ? handle.get() : nullptr, &handle->file, initial, initial ? static_cast(ibytes) : 0, host); + if (handle->tellOutOfRange) { + throw std::runtime_error(std::string(__func__) + ": a stream length outside the host long range is not implemented"); + } + if (result != 0) return result; + handle->infos.resize(static_cast(handle->file.links)); + SyncInfos(*handle); + Store(vf, std::move(handle)); + return 0; +} + +int APS5_VABI sceVorbisDecClear(void* vf) { + const std::unique_ptr handle = Take(vf); + if (!handle) { + Handle closed; + return ov_clear(&closed.file); + } + void* const source = handle->file.datasource; + handle->file.datasource = nullptr; + const int result = ov_clear(&handle->file); + if (source && handle->callbacks.close_func) handle->callbacks.close_func(handle->datasource); + return result; +} + +std::int64_t APS5_VABI sceVorbisDecBitrate(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { return ov_bitrate(&handle.file, link); }); +} + +std::int64_t APS5_VABI sceVorbisDecBitrateInstant(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) -> std::int64_t { return ov_bitrate_instant(&handle.file); }); +} + +GuestComment* APS5_VABI sceVorbisDecComment(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) { return reinterpret_cast(ov_comment(&handle.file, link)); }); +} + +int APS5_VABI sceVorbisDecCrossLap(void* first, void* second) { + if (first != second) RequireFile(first, __func__); + Handle* from = Find(first); + if (first != second && from) RequireFile(second, __func__); + Handle* to = second == first ? from : Find(second); + Handle closedFrom; + Handle closedTo; + OggVorbis_File* fromFile = from ? &from->file : &closedFrom.file; + OggVorbis_File* toFile = second == first ? fromFile : to ? &to->file : &closedTo.file; + const int result = ov_crosslap(fromFile, toFile); + if (from) SyncInfos(*from); + if (to) SyncInfos(*to); + return result; +} + +int APS5_VABI sceVorbisDecGetDecodePriority(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVorbisDecGetLastDecodeError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVorbisDecHalfrate(void* vf, int flag) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_halfrate(&handle.file, flag); }); +} + +int APS5_VABI sceVorbisDecHalfrateP(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) { return ov_halfrate_p(&handle.file); }); +} + +GuestInfo* APS5_VABI sceVorbisDecInfo(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) -> GuestInfo* { + const vorbis_info* info = ov_info(&handle.file, link); + return info ? &handle.infos.at(static_cast(info - handle.file.vi)) : nullptr; + }); +} + +int APS5_VABI sceVorbisDecPcmSeek(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_pcm_seek(&handle.file, position); }); +} + +int APS5_VABI sceVorbisDecPcmSeekLap(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_pcm_seek_lap(&handle.file, position); }); +} + +int APS5_VABI sceVorbisDecPcmSeekPage(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_pcm_seek_page(&handle.file, position); }); +} + +int APS5_VABI sceVorbisDecPcmSeekPageLap(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_pcm_seek_page_lap(&handle.file, position); }); +} + +std::int64_t APS5_VABI sceVorbisDecPcmTell(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) -> std::int64_t { return ov_pcm_tell(&handle.file); }); +} + +std::int64_t APS5_VABI sceVorbisDecPcmTotal(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { return ov_pcm_total(&handle.file, link); }); +} + +int APS5_VABI sceVorbisDecRawSeek(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_raw_seek(&handle.file, position); }); +} + +int APS5_VABI sceVorbisDecRawSeekLap(void* vf, std::int64_t position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_raw_seek_lap(&handle.file, position); }); +} + +std::int64_t APS5_VABI sceVorbisDecRawTell(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) -> std::int64_t { return ov_raw_tell(&handle.file); }); +} + +std::int64_t APS5_VABI sceVorbisDecRawTotal(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { return ov_raw_total(&handle.file, link); }); +} + +std::int64_t APS5_VABI sceVorbisDecRead(void* vf, char* buffer, int length, int bigEndian, int word, int isSigned, int* bitstream) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { + return ov_read(&handle.file, buffer, length, bigEndian, word, isSigned, bitstream); + }); +} + +std::int64_t APS5_VABI sceVorbisDecReadFilter(void* vf, char* buffer, int length, int bigEndian, int word, int isSigned, int* bitstream, GuestFilter filter, void* filterParam) { + FilterContext context{filter, filterParam}; + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { + return ov_read_filter(&handle.file, buffer, length, bigEndian, word, isSigned, bitstream, filter ? RunFilter : nullptr, &context); + }); +} + +std::int64_t APS5_VABI sceVorbisDecReadFloat(void* vf, float*** pcmChannels, int samples, int* bitstream) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { return ov_read_float(&handle.file, pcmChannels, samples, bitstream); }); +} + +std::int64_t APS5_VABI sceVorbisDecSeekable(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) -> std::int64_t { return ov_seekable(&handle.file); }); +} + +std::int64_t APS5_VABI sceVorbisDecSerialNumber(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) -> std::int64_t { return ov_serialnumber(&handle.file, link); }); +} + +int APS5_VABI sceVorbisDecSetDecodePriority(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int64_t APS5_VABI sceVorbisDecStreams(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) -> std::int64_t { return ov_streams(&handle.file); }); +} + +int APS5_VABI sceVorbisDecTimeSeek(void* vf, double position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_time_seek(&handle.file, position); }); +} + +int APS5_VABI sceVorbisDecTimeSeekLap(void* vf, double position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_time_seek_lap(&handle.file, position); }); +} + +int APS5_VABI sceVorbisDecTimeSeekPage(void* vf, double position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_time_seek_page(&handle.file, position); }); +} + +int APS5_VABI sceVorbisDecTimeSeekPageLap(void* vf, double position) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_time_seek_page_lap(&handle.file, position); }); +} + +double APS5_VABI sceVorbisDecTimeTell(void* vf) { + return WithHandle(vf, __func__, [](Handle& handle) { return ov_time_tell(&handle.file); }); +} + +double APS5_VABI sceVorbisDecTimeTotal(void* vf, int link) { + return WithHandle(vf, __func__, [&](Handle& handle) { return ov_time_total(&handle.file, link); }); +} + +} diff --git a/core/libs/prx/libSceVorbisDec/include/GuestVorbisFile.hpp b/core/libs/prx/libSceVorbisDec/include/GuestVorbisFile.hpp new file mode 100644 index 0000000000..9da5f3e1d5 --- /dev/null +++ b/core/libs/prx/libSceVorbisDec/include/GuestVorbisFile.hpp @@ -0,0 +1,46 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVORBISDEC_INCLUDE_GUESTVORBISFILE_HPP +#define CORE_LIBS_PRX_LIBSCEVORBISDEC_INCLUDE_GUESTVORBISFILE_HPP + +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include + +namespace VorbisDec { + +struct GuestCallbacks { + std::size_t (APS5_VABI *read_func)(void*, std::size_t, std::size_t, void*); + int (APS5_VABI *seek_func)(void*, std::int64_t, int); + int (APS5_VABI *close_func)(void*); + std::int64_t (APS5_VABI *tell_func)(void*); +}; + +struct GuestInfo { + std::int32_t version; + std::int32_t channels; + std::int64_t rate; + std::int64_t bitrate_upper; + std::int64_t bitrate_nominal; + std::int64_t bitrate_lower; + std::int64_t bitrate_window; + void* codec_setup; +}; + +struct GuestComment { + char** user_comments; + std::int32_t* comment_lengths; + std::int32_t comments; + char* vendor; +}; + +using GuestFilter = void (APS5_VABI *)(float**, std::int64_t, std::int64_t, void*); + +static_assert(sizeof(GuestCallbacks) == 32); +static_assert(offsetof(GuestInfo, rate) == 8 && offsetof(GuestInfo, bitrate_window) == 40 && offsetof(GuestInfo, codec_setup) == 48); +static_assert(sizeof(GuestInfo) == 56); +static_assert(offsetof(GuestComment, comment_lengths) == 8 && offsetof(GuestComment, comments) == 16 && offsetof(GuestComment, vendor) == 24); +static_assert(sizeof(GuestComment) == 32); + +} + +#endif diff --git a/core/libs/prx/libSceVrServiceDialog/CMakeLists.txt b/core/libs/prx/libSceVrServiceDialog/CMakeLists.txt new file mode 100644 index 0000000000..56c0611c39 --- /dev/null +++ b/core/libs/prx/libSceVrServiceDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceVrServiceDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceVrServiceDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceVrServiceDialog) + +if(BUILD_TESTING) + add_test_executable(guest_vr_service_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestVrServiceDialog.cpp) + target_include_directories(guest_vr_service_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_vr_service_dialog_tests PRIVATE libSceVrServiceDialog) + add_test(NAME guest_vr_service_dialog COMMAND guest_vr_service_dialog_tests) + if(WIN32) + set_tests_properties(guest_vr_service_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceVrServiceDialog/Export.cpp b/core/libs/prx/libSceVrServiceDialog/Export.cpp new file mode 100644 index 0000000000..a4c1d1f62e --- /dev/null +++ b/core/libs/prx/libSceVrServiceDialog/Export.cpp @@ -0,0 +1,71 @@ +#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 sceVrServiceDialogInitialize(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 sceVrServiceDialogOpen(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 sceVrServiceDialogUpdateStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceVrServiceDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceVrServiceDialogGetResult(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 sceVrServiceDialogClose(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 sceVrServiceDialogTerminate(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/libSceVrSetupDialog/Export.cpp b/core/libs/prx/libSceVrSetupDialog/Export.cpp index 128cbcd16a..2aa6aad360 100644 --- a/core/libs/prx/libSceVrSetupDialog/Export.cpp +++ b/core/libs/prx/libSceVrSetupDialog/Export.cpp @@ -3,6 +3,7 @@ #include #include #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" namespace { @@ -11,11 +12,10 @@ 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 VR_SETUP_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x8A7C0001u); +constexpr int VR_SETUP_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x8A7C0002u); +constexpr int VR_SETUP_DIALOG_ERROR_INVALID_ARGUMENT = static_cast(0x8A7C0003u); +constexpr int VR_SETUP_DIALOG_ERROR_BUSY = static_cast(0x8A7C0005u); constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; @@ -26,15 +26,15 @@ extern "C" { int APS5_VABI sceVrSetupDialogInitialize(void) { int expected = COMMON_DIALOG_STATUS_NONE; - if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return VR_SETUP_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } -int APS5_VABI sceVrSetupDialogOpen(const void* param) { +int APS5_VABI sceVrSetupDialogOpen(const VrSetupDialogParam* param) { + if (param == nullptr || param->size != sizeof(*param) || param->reserved8 != 0 || param->reserved9 != 0) return VR_SETUP_DIALOG_ERROR_INVALID_ARGUMENT; 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; + if (status == COMMON_DIALOG_STATUS_NONE) return VR_SETUP_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return VR_SETUP_DIALOG_ERROR_BUSY; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } @@ -43,23 +43,27 @@ int APS5_VABI sceVrSetupDialogUpdateStatus(void) { return g_status.load(); } -int APS5_VABI sceVrSetupDialogGetResult(void* result) { - const int status = g_status.load(); - if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - 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 sceVrSetupDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceVrSetupDialogGetResult(std::int32_t* result) { + if (result == nullptr) return VR_SETUP_DIALOG_ERROR_INVALID_ARGUMENT; + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return VR_SETUP_DIALOG_ERROR_NOT_INITIALIZED; + *result = COMMON_DIALOG_RESULT_USER_CANCELED; + if (status != COMMON_DIALOG_STATUS_FINISHED) return VR_SETUP_DIALOG_ERROR_BUSY; + return 0; } int APS5_VABI sceVrSetupDialogClose(void) { - if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return VR_SETUP_DIALOG_ERROR_NOT_INITIALIZED; + if (g_status.load() == COMMON_DIALOG_STATUS_RUNNING) g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } int APS5_VABI sceVrSetupDialogTerminate(void) { - if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return VR_SETUP_DIALOG_ERROR_NOT_INITIALIZED; return 0; } diff --git a/core/libs/prx/libSceVrTracker/Export.cpp b/core/libs/prx/libSceVrTracker/Export.cpp new file mode 100644 index 0000000000..14776d3483 --- /dev/null +++ b/core/libs/prx/libSceVrTracker/Export.cpp @@ -0,0 +1,200 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +struct VrTrackerMemoryBlock { + void* address; + std::uint32_t size; + std::uint32_t alignment; +}; + +struct VrTrackerInitConfig { + std::uint32_t type; + std::uint32_t option; + std::uint32_t mode; + std::uint32_t parameter; + std::uint32_t reserved[4]; +}; + +struct VrTrackerInitParam { + std::uint32_t size; + std::int32_t mode; + std::uint32_t reserved[6]; + VrTrackerInitConfig config; + VrTrackerMemoryBlock memory[3]; + std::uint32_t option112; + std::uint32_t option116; + std::uint32_t reservedTail[6]; +}; + +static_assert(sizeof(VrTrackerMemoryBlock) == 16); +static_assert(sizeof(VrTrackerInitConfig) == 32); +static_assert(sizeof(VrTrackerInitParam) == 144); +static_assert(offsetof(VrTrackerInitParam, config) == 32); +static_assert(offsetof(VrTrackerInitParam, memory) == 64); +static_assert(offsetof(VrTrackerInitParam, option112) == 112); +struct VrTrackerGpuSubmitParam; +struct VrTrackerResult; + +struct VrTrackerCpuProcessParam { + std::uint32_t size; + std::uint32_t mode; + std::uint32_t argument; + std::uint32_t reserved[5]; +}; + +struct VrTrackerGpuWaitParam { + std::uint32_t size; + std::byte payload[28]; +}; + +struct VrTrackerGetResultParam { + std::uint32_t size; + std::int32_t handle; + std::uint32_t mode; + std::uint32_t extendedResult; + std::uint64_t time; + std::uint32_t statusMode; + std::uint32_t reserved0; + std::uint32_t option32; + std::byte outputValue[4]; + std::uint32_t option40; + std::uint32_t reserved1[2]; + std::byte unused[4]; +}; + +static_assert(sizeof(VrTrackerCpuProcessParam) == 32); +static_assert(offsetof(VrTrackerCpuProcessParam, mode) == 4); +static_assert(offsetof(VrTrackerCpuProcessParam, argument) == 8); +static_assert(sizeof(VrTrackerGpuWaitParam) == 32); +static_assert(sizeof(VrTrackerGetResultParam) == 56); +static_assert(offsetof(VrTrackerGetResultParam, time) == 16); +static_assert(offsetof(VrTrackerGetResultParam, statusMode) == 24); +static_assert(offsetof(VrTrackerGetResultParam, outputValue) == 36); +static_assert(offsetof(VrTrackerGetResultParam, reserved1) == 44); + +extern "C" { + +int APS5_VABI sceVrTrackerCpuProcess(const VrTrackerCpuProcessParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerGetResult(const VrTrackerGetResultParam* param, VrTrackerResult* result) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerGetTime(std::uint64_t* time) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerGpuSubmit(const VrTrackerGpuSubmitParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerGpuWait(const VrTrackerGpuWaitParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerInit(const VrTrackerInitParam* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerCpuPopMarker() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerCpuPushMarker() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerGetLiveCaptureId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerGetPlayAreaWarningInfo(void* info) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerGpuWaitAndCpuProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerNotifyEndOfCpuProcess(const void* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerQueryMemory(const void* param, void* result) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerRecalibrate(const void* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerRegisterDevice(int device_type, int handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerRegisterDevice2(int device_type, int handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerRegisterDeviceInternal(int device_type, int handle, int unk0, int unk1) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerResetAll() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerResetOrientationRelative(int device_type, int handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerSaveInternalBuffers() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerSetDeviceRejection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerSetDurationUntilStatusNotTracking(int device_type, std::uint32_t duration_camera_frames) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerSetExtendedMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerSetLEDBrightness() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerSetRestingMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerStartLiveCapture() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerStopLiveCapture() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerUnregisterDevice(int handle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceVrTrackerUpdateMotionSensorData(const void* param) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceVrTracker2/CMakeLists.txt b/core/libs/prx/libSceVrTracker2/CMakeLists.txt new file mode 100644 index 0000000000..aaf8e867d1 --- /dev/null +++ b/core/libs/prx/libSceVrTracker2/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceVrTracker2 SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceVrTracker2 PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceVrTracker2 PRIVATE libc) +configure_windows_unwind(libSceVrTracker2) + +if(BUILD_TESTING) + add_test_executable(guest_vr_tracker2_tests ${LIBS_INCLUDE_DIR}/tests/GuestVrTracker2.cpp) + target_include_directories(guest_vr_tracker2_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_vr_tracker2_tests PRIVATE libSceVrTracker2 libc) + configure_windows_unwind(guest_vr_tracker2_tests) + add_test(NAME guest_vr_tracker2 COMMAND guest_vr_tracker2_tests) + if(WIN32) + set_tests_properties(guest_vr_tracker2 PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 42b258980d..2e521f452e 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -1,57 +1,762 @@ -#include #include -#include "SceTypes.hpp" +#include +#include +#include +#include #include "prx/libc/include/General.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" + +struct VrTracker2InitializeParameter { + std::uint32_t size; + std::uint32_t mode; + std::int32_t threadPriorities[2]; + std::uint64_t threadAffinityMasks[2]; + void* workMemory; + std::uint32_t workMemorySize; + std::uint32_t workMemoryAlignment; + std::uint32_t option; + std::uint32_t coordinateSystem; + std::uint8_t disableOption; + std::int8_t optionMode; + std::uint8_t reserved[14]; +}; + +struct VrTracker2MemoryParameter { + std::uint32_t size; + std::uint32_t mode; + std::uint8_t reserved[24]; +}; + +struct VrTracker2MemoryRequirement { + std::uint32_t size; + std::uint32_t memorySize; + std::uint32_t memoryAlignment; + std::uint8_t reserved[20]; +}; + +static_assert(sizeof(VrTracker2InitializeParameter) == 72); +static_assert(offsetof(VrTracker2InitializeParameter, threadAffinityMasks) == 16); +static_assert(offsetof(VrTracker2InitializeParameter, workMemory) == 32); +static_assert(offsetof(VrTracker2InitializeParameter, coordinateSystem) == 52); +static_assert(offsetof(VrTracker2InitializeParameter, reserved) == 58); +static_assert(sizeof(VrTracker2MemoryParameter) == 32); +static_assert(sizeof(VrTracker2MemoryRequirement) == 32); +struct VrTracker2LegacyParameter12 { + std::uint32_t size; + std::uint8_t opaque[8]; +}; + +struct VrTracker2LegacyParameter20 { + std::uint32_t size; + std::uint8_t opaque[16]; +}; + +struct VrTracker2UnsupportedCoordinateParameter; + +static_assert(sizeof(VrTracker2LegacyParameter12) == 12); +static_assert(sizeof(VrTracker2LegacyParameter20) == 20); + +struct VrTracker2CalibrationResult; +struct VrTracker2TelemetryData; +struct VrTracker2PoseParameter; +struct VrTracker2PoseResult; + +struct VrTracker2MeshRequirement { + std::uint32_t vertexCount; + std::uint32_t indexCount; + std::uint32_t vertexDataSize; + std::uint32_t indexDataSize; +}; + +struct VrTracker2Pose { + float position[3]; + std::uint8_t padding[4]; + float orientation[4]; +}; + +static_assert(sizeof(VrTracker2MeshRequirement) == 16); +static_assert(sizeof(VrTracker2Pose) == 32); +static_assert(offsetof(VrTracker2Pose, orientation) == 16); + +struct VrTracker2ResultParameter { + std::uint32_t size; + std::int32_t deviceHandle; + std::uint32_t reserved; + std::uint8_t padding[4]; + std::uint64_t timestamp; + std::uint32_t reserved2; + std::uint8_t padding2[4]; + std::uint64_t userTag; + std::uint32_t reserved3[2]; +}; + +struct VrTracker2LegacyResultParameter { + std::uint32_t size; + std::int32_t deviceHandle; + std::uint32_t reserved; + std::uint8_t padding[4]; + std::uint64_t timestamp; + std::uint32_t userTag; + std::uint8_t padding2[4]; +}; + +static_assert(sizeof(VrTracker2ResultParameter) == 48); +static_assert(offsetof(VrTracker2ResultParameter, timestamp) == 16); +static_assert(offsetof(VrTracker2ResultParameter, userTag) == 32); +static_assert(sizeof(VrTracker2LegacyResultParameter) == 32); +static_assert(offsetof(VrTracker2LegacyResultParameter, userTag) == 24); +struct VrTracker2Result; +struct VrTracker2CoordinateLocation; + +struct VrTracker2BoundaryGeometry { + std::uint32_t size; + std::uint32_t pointCount; + float points[256][3]; +}; + +struct VrTracker2OrientedBoundingBox { + std::uint32_t size; + float transform[16]; + float extents[3]; +}; + +static_assert(sizeof(VrTracker2BoundaryGeometry) == 3080); +static_assert(offsetof(VrTracker2BoundaryGeometry, points) == 8); +static_assert(sizeof(VrTracker2OrientedBoundingBox) == 80); +static_assert(offsetof(VrTracker2OrientedBoundingBox, transform) == 4); +static_assert(offsetof(VrTracker2OrientedBoundingBox, extents) == 68); + +namespace { + +constexpr std::int32_t SCE_OK = 0; +constexpr std::int32_t VR_TRACKER2_ERROR_NOT_INITIALIZED = static_cast(0x8A740001u); +constexpr std::int32_t VR_TRACKER2_ERROR_INVALID_ARGUMENT = static_cast(0x8A740006u); + +std::int32_t ClearThenReportNotInitialized(void* output, std::size_t size) { + if (output == nullptr) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + std::memset(output, 0, size); + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +} + +struct VrTracker2RelativePoseResult { + VrTracker2Pose pose; + std::uint8_t reserved[32]; +}; + +static_assert(sizeof(VrTracker2RelativePoseResult) == 64); + +struct VrTracker2ScalarResult { + std::uint32_t size; + float value; +}; + +struct VrTracker2TrackingOptions { + std::int32_t options[3]; + std::uint8_t reset; + std::uint8_t reserved[3]; + float vector[3]; +}; + +struct VrTracker2TrackingOptionsResult { + VrTracker2TrackingOptions options; + float position[3]; +}; + +struct VrTracker2DiagnosticResult; +struct VrTracker2ExtendedResult; +struct VrTracker2TrackingConfiguration; + +static_assert(sizeof(VrTracker2ScalarResult) == 8); +static_assert(sizeof(VrTracker2TrackingOptions) == 28); +static_assert(offsetof(VrTracker2TrackingOptions, vector) == 16); +static_assert(sizeof(VrTracker2TrackingOptionsResult) == 40); +static_assert(offsetof(VrTracker2TrackingOptionsResult, position) == 28); + +struct VrTracker2ModeParameter { + std::uint32_t size; + std::uint32_t mode; + float value; +}; + +struct VrTracker2DiagnosticText { + char sections[6][512]; +}; + +struct VrTracker2CompositePoseResult; +struct VrTracker2CameraImages; +struct VrTracker2SensorFrame; +struct VrTracker2TrackingStatus; +struct VrTracker2BoundaryParameter; + +static_assert(sizeof(VrTracker2ModeParameter) == 12); +static_assert(sizeof(VrTracker2DiagnosticText) == 3072); + +struct VrTracker2CameraFrame { + std::uint32_t selector; + std::uint32_t identifiers[2]; + std::uint32_t width; + std::uint32_t height; + std::uint32_t pitch; + const void* pixels; + std::int32_t bufferIndex; +}; + +struct VrTracker2CameraFrameMetadata; + +static_assert(offsetof(VrTracker2CameraFrame, pixels) == 24); +static_assert(offsetof(VrTracker2CameraFrame, bufferIndex) == 32); +static_assert(sizeof(VrTracker2CameraFrame) == 40); + +struct VrTracker2SpatialQueryResult { + std::uint32_t size; + std::int32_t status; + float vectors[2][3]; + float value; +}; + +struct VrTracker2DeviceCalibrationStatus; +struct VrTracker2DeviceBoundaryParameter; +struct VrTracker2ResetPoseParameter; + +static_assert(sizeof(VrTracker2SpatialQueryResult) == 36); +static_assert(offsetof(VrTracker2SpatialQueryResult, vectors) == 8); +static_assert(offsetof(VrTracker2SpatialQueryResult, value) == 32); + +struct VrTracker2MapData; +struct VrTracker2MapStatus; + +struct VrTracker2RuntimeStatus; +struct VrTracker2DiagnosticState; +struct VrTracker2ExtendedDiagnosticState; +struct VrTracker2SensorDiagnostics; +struct VrTracker2DeviceDiagnostics; +struct VrTracker2TimingDiagnostics; +struct VrTracker2MapConfiguration; +struct VrTracker2SensorSummary; + +struct VrTracker2CoordinateResultParameter { + std::uint32_t coordinateSystem; + std::uint32_t reserved; + VrTracker2ResultParameter parameter; +}; + +struct VrTracker2BoundaryFitResult { + std::uint8_t flags[4]; + float dimensions[4]; +}; + +struct VrTracker2VectorResult { + std::uint32_t size; + float value[3]; +}; + +struct VrTracker2ExtendedTrackingResult; + +static_assert(sizeof(VrTracker2CoordinateResultParameter) == 56); +static_assert(offsetof(VrTracker2CoordinateResultParameter, parameter) == 8); +static_assert(sizeof(VrTracker2BoundaryFitResult) == 20); +static_assert(sizeof(VrTracker2VectorResult) == 16); + +struct VrTracker2MeshVertex; +struct VrTracker2TimedPoseResult; +struct VrTracker2ImagePoseResult; +struct VrTracker2ImageFrame; extern "C" { -int APS5_VABI sceVrTracker2Finalize() { +std::int32_t APS5_VABI sceVrTracker2Finalize() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2GetCoordinateSystem(std::uint32_t* coordinateSystem) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2GetPlayAreaBoundaryGeometry(VrTracker2BoundaryGeometry* geometry) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2GetPlayAreaOrientedBoundingBox(VrTracker2OrientedBoundingBox* box) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2GetResult(const VrTracker2ResultParameter* parameter, VrTracker2Result* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2LocateCoordinateSystem(std::uint32_t coordinateSystem, VrTracker2CoordinateLocation* location) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2RegisterDevice(std::uint32_t deviceType, std::int32_t deviceHandle) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2ResetLocalCoordinate(std::int32_t deviceHandle) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2SetCoordinateSystem(std::uint32_t coordinateSystem) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2UnregisterDevice(std::int32_t deviceHandle) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2QueryMemory(const VrTracker2MemoryParameter* parameter, VrTracker2MemoryRequirement* requirement) { + if (parameter == nullptr || requirement == nullptr) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + if (parameter->size != sizeof(VrTracker2MemoryParameter)) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + std::uint32_t memorySize = 32u << 20; + if (parameter->mode == 1000 || parameter->mode == 1001) { + const auto* process = static_cast(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t sdkVersion; + std::memcpy(&sdkVersion, process + 0x10, sizeof(sdkVersion)); + if (sdkVersion < 0x1000000) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + memorySize = parameter->mode == 1000 ? 7u << 20 : 3u << 20; + } else if (parameter->mode != 0) { + return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + } + for (const auto value : parameter->reserved) + if (value != 0) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + *requirement = {sizeof(VrTracker2MemoryRequirement), memorySize, 64u << 10, {}}; + return SCE_OK; +} + +std::int32_t APS5_VABI sceVrTracker2Initialize(const VrTracker2InitializeParameter* parameter) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVrTracker2ResetLocalCoordinate() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("1m6DD9BBmSE", VrTracker2GetRelativePose); +std::int32_t APS5_VABI VrTracker2GetRelativePose(std::uint32_t firstCoordinateSystem, std::uint32_t secondCoordinateSystem, VrTracker2RelativePoseResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("SgunslIO-C0", VrTracker2GetDiagnosticResult); +std::int32_t APS5_VABI VrTracker2GetDiagnosticResult(VrTracker2DiagnosticResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("KMIToVnHrhc", VrTracker2SetTrackingOptions); +std::int32_t APS5_VABI VrTracker2SetTrackingOptions(const VrTracker2TrackingOptions* options) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("XfEBVemeJyE", VrTracker2GetTrackingOptions); +std::int32_t APS5_VABI VrTracker2GetTrackingOptions(VrTracker2TrackingOptionsResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("y79S3-zrJKg", VrTracker2GetExtendedResult); +std::int32_t APS5_VABI VrTracker2GetExtendedResult(VrTracker2ExtendedResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("b777Kpfe2J8", VrTracker2GetCompositePose); +std::int32_t APS5_VABI VrTracker2GetCompositePose(const VrTracker2ResultParameter* parameter, VrTracker2CompositePoseResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("UfUYgiT7VK0", VrTracker2GetCounter); +std::int32_t APS5_VABI VrTracker2GetCounter(std::uint64_t* counter, std::uint32_t selector) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("MB7B6396K6U", VrTracker2SetTrackingConfiguration); +std::int32_t APS5_VABI VrTracker2SetTrackingConfiguration(const VrTracker2TrackingConfiguration* configuration) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("H2Mnx7gz4ac", VrTracker2NotInitialized); +APS5_EXPORT("SgNHzN6XaEc", VrTracker2GetTrackingConfiguration); +std::int32_t APS5_VABI VrTracker2GetTrackingConfiguration(VrTracker2TrackingConfiguration* configuration) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("sYBNLrMxjAM", VrTracker2GetCameraImages); +std::int32_t APS5_VABI VrTracker2GetCameraImages(VrTracker2CameraImages* images) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("3gQncidodmw", VrTracker2GetCameraImage); +std::int32_t APS5_VABI VrTracker2GetCameraImage(void* image, std::uint32_t size, std::uint32_t* frameId) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("qnCmEbUGqWY", VrTracker2GetDiagnosticText); +std::int32_t APS5_VABI VrTracker2GetDiagnosticText(VrTracker2DiagnosticText* text) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("oGNn7JIdMHw", VrTracker2GetSensorFrame); +std::int32_t APS5_VABI VrTracker2GetSensorFrame(VrTracker2SensorFrame* frame) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("80IxOn8swIA", VrTracker2NotInitialized); +APS5_EXPORT("NTbJNAZA4ro", VrTracker2NotInitialized); +APS5_EXPORT("914+9oJxNts", VrTracker2GetTrackingStatus); +std::int32_t APS5_VABI VrTracker2GetTrackingStatus(VrTracker2TrackingStatus* status) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("c6SZtdEjH0E", VrTracker2GetBoundaryParameter); +std::int32_t APS5_VABI VrTracker2GetBoundaryParameter(VrTracker2BoundaryParameter* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("74IXIMe+Lto", VrTracker2SetBoundaryParameter); +std::int32_t APS5_VABI VrTracker2SetBoundaryParameter(const VrTracker2BoundaryParameter* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("hmahOYMiYNY", VrTracker2SetMode); +std::int32_t APS5_VABI VrTracker2SetMode(const VrTracker2ModeParameter* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("pNXc6ZlLq-8", VrTracker2GetScalarResult); +std::int32_t APS5_VABI VrTracker2GetScalarResult(VrTracker2ScalarResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("omJ7b8FqAN0", VrTracker2LegacyQuery12); +std::int32_t APS5_VABI VrTracker2LegacyQuery12(const VrTracker2LegacyParameter12* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("G2PWj1HLqn8", VrTracker2LegacyQuery20); +std::int32_t APS5_VABI VrTracker2LegacyQuery20(const VrTracker2LegacyParameter20* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("73zcxAFawYk", VrTracker2NotInitialized); +APS5_EXPORT("vegrWFNiQwc", VrTracker2NotInitialized); +APS5_EXPORT("6r01IawmypA", VrTracker2NotInitialized); +APS5_EXPORT("zeof7sF2fqE", VrTracker2UnsupportedCoordinateOperation); +std::int32_t APS5_VABI VrTracker2UnsupportedCoordinateOperation(const VrTracker2UnsupportedCoordinateParameter* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("Z0rfHOHQV1w", VrTracker2NotInitialized); +APS5_EXPORT("4l7x+uy3dWw", VrTracker2NotInitialized); +APS5_EXPORT("ZXlt-Xb3da4", VrTracker2NotInitialized); +APS5_EXPORT("BSwAdB3GM3o", VrTracker2NotInitialized); +APS5_EXPORT("9ATuhVfn9Dw", VrTracker2NotInitialized); +APS5_EXPORT("wjmVbmf58rE", VrTracker2NotInitialized); +APS5_EXPORT("4+B+TcHEHks", VrTracker2NotInitialized); +APS5_EXPORT("cjDz4kYJKTw", VrTracker2NotInitialized); +APS5_EXPORT("BjVg0fKV9VA", VrTracker2NotInitialized); +APS5_EXPORT("oDV5j-2Luic", VrTracker2NotInitialized); +APS5_EXPORT("t+86o9sCZ-Y", VrTracker2AcquireCameraFrame); +std::int32_t APS5_VABI VrTracker2AcquireCameraFrame(std::uint32_t selector, VrTracker2CameraFrame* frame) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} +; +APS5_EXPORT("TIpYP35JzUs", VrTracker2AcquireCameraFrameWithMetadata); +std::int32_t APS5_VABI VrTracker2AcquireCameraFrameWithMetadata(std::uint32_t selector, VrTracker2CameraFrame* frame, VrTracker2CameraFrameMetadata* metadata) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} +; +APS5_EXPORT("LrSc+KuxIkg", VrTracker2ReleaseCameraFrame); +std::int32_t APS5_VABI VrTracker2ReleaseCameraFrame(const VrTracker2CameraFrame* frame) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} +; +APS5_EXPORT("Ur+M5wy+QBY", VrTracker2SetScalarOption); +std::int32_t APS5_VABI VrTracker2SetScalarOption(float value) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} +; +APS5_EXPORT("OvKC351Udqo", VrTracker2GetScalarOption); +std::int32_t APS5_VABI VrTracker2GetScalarOption(float* value) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} +; +APS5_EXPORT("LQV4nz6wteY", VrTracker2NotInitialized); +APS5_EXPORT("x0pGthEwf88", VrTracker2NotInitialized); +APS5_EXPORT("d5Fjs0rHkBg", VrTracker2SelectCoordinateSystem); +std::int32_t APS5_VABI VrTracker2SelectCoordinateSystem(std::uint32_t coordinateSystem) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} +; +APS5_EXPORT("Yed3ZONcuMk", VrTracker2GetSelectedCoordinateSystem); +std::int32_t APS5_VABI VrTracker2GetSelectedCoordinateSystem(std::uint32_t* coordinateSystem) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} +; +APS5_EXPORT("fkptW6uRdWc", VrTracker2ResetDevicePose); +std::int32_t APS5_VABI VrTracker2ResetDevicePose(std::int32_t deviceHandle) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("HDlD-xE1Xuk", VrTracker2ResetPose); +std::int32_t APS5_VABI VrTracker2ResetPose(const VrTracker2ResetPoseParameter* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("IGdE6nsMTAY", VrTracker2QueryPosition); +std::int32_t APS5_VABI VrTracker2QueryPosition(const float* position, VrTracker2SpatialQueryResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("DSTergmOvvE", VrTracker2QueryDevicePosition); +std::int32_t APS5_VABI VrTracker2QueryDevicePosition(std::int32_t deviceHandle, VrTracker2SpatialQueryResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("B7hPwvvg79E", VrTracker2GetTrackingBounds); +std::int32_t APS5_VABI VrTracker2GetTrackingBounds(VrTracker2OrientedBoundingBox* bounds) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("ZwnIsDe1eNE", VrTracker2GetDeviceState); +std::int32_t APS5_VABI VrTracker2GetDeviceState(std::int32_t deviceHandle, std::int32_t* state) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("B2vw+s-0Dfo", VrTracker2GetDeviceCalibrationStatus); +std::int32_t APS5_VABI VrTracker2GetDeviceCalibrationStatus(std::int32_t deviceHandle, VrTracker2DeviceCalibrationStatus* status) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("4LNTLivfRuA", VrTracker2SetDeviceBoundary); +std::int32_t APS5_VABI VrTracker2SetDeviceBoundary(std::int32_t deviceHandle, std::int32_t selector, const VrTracker2DeviceBoundaryParameter* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("edajhZhz9Uo", VrTracker2GetDeviceBoundary); +std::int32_t APS5_VABI VrTracker2GetDeviceBoundary(std::int32_t deviceHandle, std::int32_t selector, VrTracker2DeviceBoundaryParameter* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("QS+7oqiszuc", VrTracker2ApplyDeviceBoundary); +std::int32_t APS5_VABI VrTracker2ApplyDeviceBoundary(std::int32_t deviceHandle, std::int32_t selector) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("-dDyvzd2jf0", VrTracker2GetDeviceMapState); +std::int32_t APS5_VABI VrTracker2GetDeviceMapState(std::int32_t deviceHandle, std::int32_t* state) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("k8JDLkCrOgs", VrTracker2InitializeMap); +std::int32_t APS5_VABI VrTracker2InitializeMap(std::int32_t deviceHandle, const float* parameter) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("-HFx+dnDSOI", VrTracker2GetCoordinateTrackingResult); +std::int32_t APS5_VABI VrTracker2GetCoordinateTrackingResult(const VrTracker2CoordinateResultParameter* parameter, VrTracker2ExtendedTrackingResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("wgDeDXirl0g", VrTracker2GetExtendedTrackingResult); +std::int32_t APS5_VABI VrTracker2GetExtendedTrackingResult(const VrTracker2ResultParameter* parameter, VrTracker2ExtendedTrackingResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("a1kWSTYGBxg", VrTracker2FitBoundary); +std::int32_t APS5_VABI VrTracker2FitBoundary(const VrTracker2BoundaryGeometry* boundary, VrTracker2BoundaryFitResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("zUwxBEKr3fc", VrTracker2FitBoundaryWithBounds); +std::int32_t APS5_VABI VrTracker2FitBoundaryWithBounds(const VrTracker2BoundaryGeometry* boundary, VrTracker2OrientedBoundingBox* firstBounds, VrTracker2OrientedBoundingBox* secondBounds, VrTracker2BoundaryFitResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("TC7VTVzj0xc", VrTracker2GetDeviceCenter); +std::int32_t APS5_VABI VrTracker2GetDeviceCenter(std::int32_t deviceHandle, VrTracker2VectorResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("1DwROxtuANs", VrTracker2GetRuntimeStatus); +std::int32_t APS5_VABI VrTracker2GetRuntimeStatus(VrTracker2RuntimeStatus* status) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("wwbt4s8cbMQ", VrTracker2GetTrackingFlags); +std::int32_t APS5_VABI VrTracker2GetTrackingFlags(bool* active, std::uint8_t* flags) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("eGxBJ0IolOA", VrTracker2GetDiagnosticState); +std::int32_t APS5_VABI VrTracker2GetDiagnosticState(VrTracker2DiagnosticState* state) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("9BaFhspHF+E", VrTracker2GetExtendedDiagnosticState); +std::int32_t APS5_VABI VrTracker2GetExtendedDiagnosticState(VrTracker2ExtendedDiagnosticState* state) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("lb9q5nJ4vzk", VrTracker2GetSensorDiagnostics); +std::int32_t APS5_VABI VrTracker2GetSensorDiagnostics(VrTracker2SensorDiagnostics* diagnostics) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("ZsD1-thjXS4", VrTracker2GetDeviceDiagnostics); +std::int32_t APS5_VABI VrTracker2GetDeviceDiagnostics(VrTracker2DeviceDiagnostics* diagnostics) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("QdHQnM0XICI", VrTracker2GetTimingDiagnostics); +std::int32_t APS5_VABI VrTracker2GetTimingDiagnostics(VrTracker2TimingDiagnostics* diagnostics) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("Ek4WMOvwv6g", VrTracker2GetMapConfiguration); +std::int32_t APS5_VABI VrTracker2GetMapConfiguration(VrTracker2MapConfiguration* configuration) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("W1YAO6G+hNc", VrTracker2SetMapConfiguration); +std::int32_t APS5_VABI VrTracker2SetMapConfiguration(const VrTracker2MapConfiguration* configuration) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("HAJB7irjl+0", VrTracker2GetMapStatus); +std::int32_t APS5_VABI VrTracker2GetMapStatus(VrTracker2MapStatus* status) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("x2mN50nKScs", VrTracker2NotInitialized); +APS5_EXPORT("cCKExge5d3A", VrTracker2NotInitialized); +APS5_EXPORT("gn6e5I4g9bw", VrTracker2NotInitialized); +APS5_EXPORT("OYTKt9diFOY", VrTracker2NotInitialized); +APS5_EXPORT("loU5lT92Zrw", VrTracker2NotInitialized); +APS5_EXPORT("o6fsgA3R2Yc", VrTracker2NotInitialized); +APS5_EXPORT("AeBtEIE0idk", VrTracker2NotInitialized); +APS5_EXPORT("DBa3wov7dKQ", VrTracker2NotInitialized); +APS5_EXPORT("ojTGClQ52r4", VrTracker2BuildMesh); +std::int32_t APS5_VABI VrTracker2BuildMesh(VrTracker2MeshVertex* vertices, std::uint16_t* indices, std::uint32_t format) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("oOLE62H66z0", VrTracker2GetMapScalar); +std::int32_t APS5_VABI VrTracker2GetMapScalar(float* value, const VrTracker2MapData* data) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("MHekhnRBKAw", VrTracker2GetTimedPose); +std::int32_t APS5_VABI VrTracker2GetTimedPose(VrTracker2TimedPoseResult* result, std::uint64_t timestamp, std::uint64_t userTag) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("JnSmj+5E8WY", VrTracker2GetMapData); +std::int32_t APS5_VABI VrTracker2GetMapData(VrTracker2MapData* data, std::uint32_t size) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("N1P+melAgb8", VrTracker2GetPoseScalar); +std::int32_t APS5_VABI VrTracker2GetPoseScalar(float* value) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("-e7A3rAM3Yk", VrTracker2SubmitImageFrame); +std::int32_t APS5_VABI VrTracker2SubmitImageFrame(const VrTracker2ImageFrame* frame) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("RefT1spBleg", VrTracker2GetImagePose); +std::int32_t APS5_VABI VrTracker2GetImagePose(VrTracker2ImagePoseResult* result) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("DsOfryhbm4U", VrTracker2GetSensorSummary); +std::int32_t APS5_VABI VrTracker2GetSensorSummary(VrTracker2SensorSummary* summary) { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI VrTracker2NotInitialized() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("XknY0IDcOwU", VrTracker2ClearWideResult); +std::int32_t APS5_VABI VrTracker2ClearWideResult(void* result) { + return ClearThenReportNotInitialized(result, 24); +} + +APS5_EXPORT("ark13GkiskA", VrTracker2ClearNarrowResult); +std::int32_t APS5_VABI VrTracker2ClearNarrowResult(void* result) { + return ClearThenReportNotInitialized(result, 16); +} + +APS5_EXPORT("-YvjOUph8B0", VrTracker2CheckSecondThenReportNotInitialized); +std::int32_t APS5_VABI VrTracker2CheckSecondThenReportNotInitialized(const void* first, const void* second) { + (void)first; + return second == nullptr ? VR_TRACKER2_ERROR_INVALID_ARGUMENT : VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("QyZvFyf-QEk", VrTracker2Accept); +APS5_EXPORT("fTYZOZepkqE", VrTracker2Accept); +std::int32_t APS5_VABI VrTracker2Accept() { + return SCE_OK; +} + +APS5_EXPORT("TZIUIk9id3c", VrTracker2FillDefaults); +std::int32_t APS5_VABI VrTracker2FillDefaults(void* output) { + if (output == nullptr) APS5_INVALID_ARG_EX; + auto* bytes = static_cast(output); + const std::uint32_t first = 0xC000; + const std::uint32_t second = 0; + const std::uint16_t third = 0x40; + std::memcpy(bytes + 4, &first, sizeof first); + std::memcpy(bytes + 8, &second, sizeof second); + std::memcpy(bytes + 12, &third, sizeof third); + std::memcpy(bytes + 14, &third, sizeof third); + return SCE_OK; +} + +APS5_EXPORT("IB0qcDVu8vU", VrTracker2ClearWord); +std::int32_t APS5_VABI VrTracker2ClearWord(void* output) { + if (output == nullptr) APS5_INVALID_ARG_EX; + std::memset(output, 0, sizeof(std::uint32_t)); + return SCE_OK; +} + +APS5_EXPORT("jRmuXjsknsI", VrTracker2Unknown01); +std::int32_t APS5_VABI VrTracker2Unknown01(VrTracker2CalibrationResult* result, std::uint32_t capacity) { + NotImplemented_nid_no_patch("jRmuXjsknsI"); return 0; } -int APS5_VABI sceVrTracker2GetPlayAreaOrientedBoundingBox() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("NBhWy+Gl0tY", VrTracker2Unknown02); +std::int32_t APS5_VABI VrTracker2Unknown02(VrTracker2MeshRequirement* requirement, std::uint32_t format) { + NotImplemented_nid_no_patch("NBhWy+Gl0tY"); return 0; } -int APS5_VABI sceVrTracker2GetResult() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("9R6W33Au4B4", VrTracker2Unknown03); +std::int32_t APS5_VABI VrTracker2Unknown03(std::uint32_t mode, void* output, std::size_t capacity, std::size_t* writtenSize) { + NotImplemented_nid_no_patch("9R6W33Au4B4"); return 0; } -int APS5_VABI sceVrTracker2Initialize() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("O8ZkTFtvUL4", VrTracker2Unknown04); +std::int32_t APS5_VABI VrTracker2Unknown04(const VrTracker2ResultParameter* parameter, VrTracker2Result* result) { + NotImplemented_nid_no_patch("O8ZkTFtvUL4"); return 0; } -int APS5_VABI sceVrTracker2LocateCoordinateSystem() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("940WPg+WwIk", VrTracker2Unknown05); +std::int32_t APS5_VABI VrTracker2Unknown05(VrTracker2Pose* pose, std::uint64_t coordinateId) { + NotImplemented_nid_no_patch("940WPg+WwIk"); return 0; } -int APS5_VABI sceVrTracker2QueryMemory() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("oZDm1bynCaA", VrTracker2Unknown06); +std::int32_t APS5_VABI VrTracker2Unknown06(const VrTracker2TelemetryData* data) { + NotImplemented_nid_no_patch("oZDm1bynCaA"); return 0; } -int APS5_VABI sceVrTracker2RegisterDevice() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVrTracker2SetCoordinateSystem() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVrTracker2UnregisterDevice() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("U1VYczwqukY", VrTracker2Unknown07); +std::int32_t APS5_VABI VrTracker2Unknown07(const VrTracker2PoseParameter* parameter, VrTracker2PoseResult* result) { + NotImplemented_nid_no_patch("U1VYczwqukY"); return 0; } diff --git a/core/libs/prx/libSceWeb/Export.cpp b/core/libs/prx/libSceWeb/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceWeb/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt b/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt index 2c498e5df1..641f60463e 100644 --- a/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt +++ b/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt @@ -12,3 +12,14 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_web_browser_dialog_cookies_tests ${LIBS_INCLUDE_DIR}/tests/GuestWebBrowserDialogCookies.cpp) + target_include_directories(guest_web_browser_dialog_cookies_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_web_browser_dialog_cookies_tests PRIVATE libSceWebBrowserDialog) + add_test(NAME guest_web_browser_dialog_cookies COMMAND guest_web_browser_dialog_cookies_tests) + if(WIN32) + set_tests_properties(guest_web_browser_dialog_cookies PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index 45939e99e8..3b8080508c 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -1,12 +1,31 @@ #include #include -#include #include +#include #include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" +struct WebBrowserDialogCookieParam { + std::uint64_t size; + const char* address; + const char* cookie; + std::uint8_t reserved[256]; +}; + +struct WebBrowserDialogResetCookieParam { + std::uint64_t size; + std::uint8_t reserved[256]; +}; + +static_assert(sizeof(WebBrowserDialogCookieParam) == 280); +static_assert(offsetof(WebBrowserDialogCookieParam, address) == 8); +static_assert(offsetof(WebBrowserDialogCookieParam, cookie) == 16); +static_assert(sizeof(WebBrowserDialogResetCookieParam) == 264); + namespace { std::atomic g_status{0}; +std::atomic g_open{false}; constexpr int COMMON_DIALOG_STATUS_NONE = 0; constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; @@ -14,38 +33,71 @@ constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_INVALID_STATE = static_cast(0x80B80006u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast(0x80B8000Bu); +constexpr int COMMON_DIALOG_ERROR_PARAMETER = static_cast(0x80B8000Au); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); + +bool ReservedClear(const std::uint8_t (&reserved)[256]) { + for (const auto byte : reserved) + if (byte != 0) return false; + return true; +} + +bool LengthWithin(const char* text, std::size_t limit) { + if (!text) return false; + const auto length = strnlen(text, limit); + return length != 0 && length < limit; +} } extern "C" { int APS5_VABI sceWebBrowserDialogInitialize(void) { - int expected = 0; - if (!g_status.compare_exchange_strong(expected, 1)) throw std::logic_error("sceWebBrowserDialogInitialize: already initialized"); + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, 1)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } int APS5_VABI sceWebBrowserDialogTerminate(void) { int expected = 1; - if (g_status.compare_exchange_strong(expected, 0)) return 0; + if (g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_NONE)) { + g_open = false; + return 0; + } expected = COMMON_DIALOG_STATUS_FINISHED; - if (!g_status.compare_exchange_strong(expected, 0)) throw std::logic_error("sceWebBrowserDialogTerminate: not initialized or still running"); - return 0; + if (g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_NONE)) { + g_open = false; + return 0; + } + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + return COMMON_DIALOG_ERROR_BUSY; } int APS5_VABI sceWebBrowserDialogClose(void) { - if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - return 0; + g_open = false; + 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_NOT_RUNNING; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; } -int APS5_VABI sceWebBrowserDialogGetResult(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; - *static_cast(result) = COMMON_DIALOG_RESULT_USER_CANCELED; - return 0; +int APS5_VABI sceWebBrowserDialogGetResult(WebBrowserDialogResult* result) { + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (std::any_of(std::begin(result->reserved), std::end(result->reserved), [](std::uint8_t value) { return value != 0; })) return COMMON_DIALOG_ERROR_PARAMETER; + if (result->extended != nullptr) { + const auto& extended = *result->extended; + if (extended.size != sizeof(extended)) return COMMON_DIALOG_ERROR_PARAMETER; + if (std::any_of(std::begin(extended.reserved0), std::end(extended.reserved0), [](std::uint8_t value) { return value != 0; })) return COMMON_DIALOG_ERROR_PARAMETER; + if (std::any_of(std::begin(extended.reserved1), std::end(extended.reserved1), [](std::uint8_t value) { return value != 0; })) return COMMON_DIALOG_ERROR_PARAMETER; + } + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + return 0; } int APS5_VABI sceWebBrowserDialogGetStatus(void) { @@ -58,25 +110,67 @@ int APS5_VABI sceWebBrowserDialogOpen(const void* param) { 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; + g_open = true; return 0; } int APS5_VABI sceWebBrowserDialogUpdateStatus(void) { - return g_status.load(); + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_FINISHED) g_open = false; + return status; } -int APS5_VABI sceWebBrowserDialogSetCookie(void) { +int APS5_VABI sceWebBrowserDialogSetCookie(const WebBrowserDialogCookieParam* param) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (g_open.load()) return COMMON_DIALOG_ERROR_INVALID_STATE; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (param->size != sizeof(WebBrowserDialogCookieParam) || !LengthWithin(param->address, 0x800) || !LengthWithin(param->cookie, 0x1000) || + !ReservedClear(param->reserved)) + return COMMON_DIALOG_ERROR_PARAMETER; + return 0; +} + +int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent(const void* param, const WebBrowserDialogAllowList* allowList) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (allowList == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (allowList->size != sizeof(*allowList)) return COMMON_DIALOG_ERROR_PARAMETER; + bool hasAddress = false; + for (const char* address : allowList->addresses) { + if (address == nullptr) continue; + hasAddress = true; + std::size_t length = 0; + while (length < 256 && address[length] != '\0') ++length; + if (length == 0 || length == 256) return COMMON_DIALOG_ERROR_PARAMETER; + } + if (!hasAddress || std::any_of(std::begin(allowList->reserved), std::end(allowList->reserved), [](std::uint8_t value) { return value != 0; })) return COMMON_DIALOG_ERROR_PARAMETER; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + g_status = COMMON_DIALOG_STATUS_FINISHED; + g_open = true; + return 0; +} + +int APS5_VABI sceWebBrowserDialogResetCookie(const WebBrowserDialogResetCookieParam* param) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (g_open.load()) return COMMON_DIALOG_ERROR_INVALID_STATE; + if (param->size != sizeof(WebBrowserDialogResetCookieParam) || !ReservedClear(param->reserved)) return COMMON_DIALOG_ERROR_PARAMETER; + return 0; +} + +int APS5_VABI sceWebBrowserDialogGetEvent() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent() { +int APS5_VABI sceWebBrowserDialogNavigate() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceWebBrowserDialogResetCookie() { +int APS5_VABI sceWebBrowserDialogSetZoom() { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceWebKit2/Export.cpp b/core/libs/prx/libSceWebKit2/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceWebKit2/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceWebKit2Secure/Export.cpp b/core/libs/prx/libSceWebKit2Secure/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceWebKit2Secure/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceWebmParserMdrw/Export.cpp b/core/libs/prx/libSceWebmParserMdrw/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceWebmParserMdrw/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceWkFontConfig/Export.cpp b/core/libs/prx/libSceWkFontConfig/Export.cpp new file mode 100644 index 0000000000..08ae01f5ec --- /dev/null +++ b/core/libs/prx/libSceWkFontConfig/Export.cpp @@ -0,0 +1,34 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN +int APS5_VABI sceWkFontConfigGetAttr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWkFontConfigGetFont(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWkFontConfigGetFontFromFontName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWkFontConfigGetFontName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWkFontConfigGetFontPath(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceWorkspace/Export.cpp b/core/libs/prx/libSceWorkspace/Export.cpp new file mode 100644 index 0000000000..d81f4a5e32 --- /dev/null +++ b/core/libs/prx/libSceWorkspace/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceZlib/Export.cpp b/core/libs/prx/libSceZlib/Export.cpp index ba1067f8bc..093a5401d2 100644 --- a/core/libs/prx/libSceZlib/Export.cpp +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -13,6 +13,8 @@ static_assert(sizeof(std::uint32_t) == 4 && sizeof(std::uint64_t) == 8 && sizeof constexpr std::int32_t NOT_INITIALIZED = static_cast(0x81120032); constexpr std::int32_t ALREADY_INITIALIZED = static_cast(0x81120033); constexpr std::int32_t NO_SPACE = static_cast(0x8112001C); +constexpr std::int32_t INVALID_ARGUMENT = static_cast(0x81120016); +constexpr std::int32_t NOT_FOUND = static_cast(0x81120002); constexpr std::int32_t FATAL = static_cast(0x811200FF); struct Result { @@ -34,7 +36,8 @@ extern "C" { std::int32_t APS5_VABI sceZlibInitialize(const void* buffer, std::uint64_t length) { std::lock_guard lock(serviceMutex); if (initialized) return ALREADY_INITIALIZED; - if ((buffer == nullptr) != (length == 0)) throw std::runtime_error("sceZlibInitialize: invalid work buffer"); + if ((buffer == nullptr) != (length == 0)) return INVALID_ARGUMENT; + if (buffer) throw std::runtime_error("sceZlibInitialize: work memory registration is not implemented"); initialized = true; return 0; } @@ -42,20 +45,18 @@ std::int32_t APS5_VABI sceZlibInitialize(const void* buffer, std::uint64_t lengt std::int32_t APS5_VABI sceZlibFinalize() { std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; - if (result && !result->retrieved) throw std::runtime_error("sceZlibFinalize: outstanding result semantics unknown"); result.reset(); initialized = false; return 0; } -std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLength, - void* destination, std::uint32_t destinationLength, - std::uint64_t* requestId) { +std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLength, void* destination, std::uint32_t destinationLength, std::uint64_t* requestId) { std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; if (!source || !sourceLength || !destination || !requestId || !destinationLength || - destinationLength > 65536 || reinterpret_cast(destination) % 2048 != 0) - throw std::runtime_error("sceZlibInflate: invalid buffer or size"); + destinationLength > 65536 || destinationLength % 2048 != 0) return INVALID_ARGUMENT; + if (reinterpret_cast(destination) % 2048 != 0) + throw std::runtime_error("sceZlibInflate: unaligned destination driver behavior is not implemented"); if (result && !result->retrieved) throw std::runtime_error("sceZlibInflate: multiple outstanding requests unsupported"); if (nextId == std::numeric_limits::max()) throw std::runtime_error("sceZlibInflate: request IDs exhausted"); z_stream stream{}; @@ -67,7 +68,7 @@ std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLe const int decoded = inflate(&stream, Z_FINISH); const auto produced = static_cast(stream.total_out); const std::int32_t status = decoded == Z_STREAM_END ? 0 : - (decoded == Z_BUF_ERROR && stream.avail_out == 0 ? NO_SPACE : FATAL); + (decoded == Z_BUF_ERROR && stream.avail_out == 0 && stream.avail_in != 0 ? NO_SPACE : FATAL); const int ended = inflateEnd(&stream); if (ended != Z_OK) throw std::runtime_error("sceZlibInflate: decoder cleanup failed"); result = Result{nextId++, produced, status}; @@ -78,18 +79,18 @@ std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLe std::int32_t APS5_VABI sceZlibWaitForDone(std::uint64_t* requestId, std::uint32_t* timeout) { std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; + if (!requestId) return INVALID_ARGUMENT; if (timeout) throw std::runtime_error("sceZlibWaitForDone: timeout semantics unknown"); - if (!requestId || !result || result->retrieved) throw std::runtime_error("sceZlibWaitForDone: no outstanding request"); + if (!result || result->retrieved) throw std::runtime_error("sceZlibWaitForDone: no outstanding request"); *requestId = result->id; return 0; } -std::int32_t APS5_VABI sceZlibGetResult(std::uint64_t requestId, std::uint32_t* destinationLength, - std::int32_t* status) { +std::int32_t APS5_VABI sceZlibGetResult(std::uint64_t requestId, std::uint32_t* destinationLength, std::int32_t* status) { std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; - if (!destinationLength || !status || !result || result->id != requestId) - throw std::runtime_error("sceZlibGetResult: invalid request or output"); + if (!destinationLength || !status) return INVALID_ARGUMENT; + if (!result || result->id != requestId) return NOT_FOUND; if (result->retrieved) throw std::runtime_error("sceZlibGetResult: repeated retrieval semantics unknown"); *destinationLength = result->produced; *status = result->status; diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 62f3b7a43b..b3dc056483 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -1,23 +1,73 @@ +if(BUILD_TESTING) + if(WIN32) + add_test_executable(windows_mappings_release_tests ${LIBS_INCLUDE_DIR}/tests/WindowsMappingsRelease.cpp) + target_include_directories(windows_mappings_release_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(windows_mappings_release_tests PRIVATE libc) + configure_windows_unwind(windows_mappings_release_tests) + add_test(NAME windows_mappings_release COMMAND windows_mappings_release_tests) + set_tests_properties(windows_mappings_release PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + foreach(mode 0 1 2) + add_test(NAME windows_mappings_release_mismatch_${mode} COMMAND windows_mappings_release_tests ${mode}) + set_tests_properties(windows_mappings_release_mismatch_${mode} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endforeach() + endif() + + add_test_executable(guest_binary_word_tests ${LIBS_INCLUDE_DIR}/tests/GuestBinaryWord.cpp) + target_include_directories(guest_binary_word_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_binary_word_tests PRIVATE libc) + 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 + ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/x86_64/CallTrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/linux/DlIteratePhdr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/macos/GuestImages.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Strings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AllocatingStrings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/WideConversion.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/BoundsChecking.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Multibyte.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/RuneLocale.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StringExtensions.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/VectorAlgorithms.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Math.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/MathExact.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Bessel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Random.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/FloatBuiltins.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/HostEntropy.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Backtrace.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/DinkumwareAtomics.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/DinkumwareInternals.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Time.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/TimeExtras.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/AtomicBuiltins.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileAndHeap.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/HostErrno.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileLock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StdioExtras.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StandardStreams.cpp @@ -33,9 +83,11 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationProcessParameters.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/JumpBuffer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Formatting.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/ScanWideField.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FormattingWide.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ScanningWide.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/RuntimeSupport.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/IntegerBuiltins.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/LocaleSupport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Process.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Hostname.cpp @@ -52,15 +104,78 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/exception/Exports.cpp ) +set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/src/MathExact.cpp + TARGET_DIRECTORY libc PROPERTIES COMPILE_OPTIONS "-frounding-math") + +target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/BinaryWordIo.cpp) + target_include_directories(libc PRIVATE ${LIBS_INCLUDE_DIR} ) +target_link_libraries(libc PRIVATE zarchive) + +target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/FloatingPoint.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_floating_point_tests ${LIBS_INCLUDE_DIR}/tests/GuestFloatingPoint.cpp) + target_include_directories(guest_floating_point_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_floating_point_tests PRIVATE libc) + configure_windows_unwind(guest_floating_point_tests) + add_test(NAME guest_floating_point COMMAND guest_floating_point_tests) + set_tests_properties(guest_floating_point PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_floating_point PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + if(WIN32) target_link_libraries(libc PRIVATE advapi32 ws2_32 iphlpapi) endif() 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_index_tests ${LIBS_INCLUDE_DIR}/tests/GuestIndex.cpp) + target_include_directories(guest_index_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_index_tests PRIVATE libc ${CMAKE_DL_LIBS}) + configure_windows_unwind(guest_index_tests) + add_test(NAME guest_index COMMAND guest_index_tests $) + if(WIN32) + set_tests_properties(guest_index PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_allocating_strings_tests ${LIBS_INCLUDE_DIR}/tests/GuestAllocatingStrings.cpp) + target_include_directories(guest_allocating_strings_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_allocating_strings_tests PRIVATE libc) + configure_windows_unwind(guest_allocating_strings_tests) + add_test(NAME guest_allocating_strings COMMAND guest_allocating_strings_tests) + if(WIN32) + set_tests_properties(guest_allocating_strings PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_app_archive_tests ${LIBS_INCLUDE_DIR}/tests/GuestAppArchive.cpp) + target_include_directories(guest_app_archive_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_app_archive_tests PRIVATE libc zarchive) + configure_windows_unwind(guest_app_archive_tests) + foreach(mode present absent damaged invalid) + add_test(NAME guest_app_archive_${mode} COMMAND guest_app_archive_tests ${mode}) + set_tests_properties(guest_app_archive_${mode} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_app_archive_${mode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test_executable(guest_array_callbacks_tests ${LIBS_INCLUDE_DIR}/tests/GuestArrayCallbacks.cpp) target_include_directories(guest_array_callbacks_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_array_callbacks_tests PRIVATE libc) @@ -100,6 +215,16 @@ if(BUILD_TESTING) set_tests_properties(guest_mktime PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_time_extras_tests ${LIBS_INCLUDE_DIR}/tests/GuestTimeExtras.cpp) + target_include_directories(guest_time_extras_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_time_extras_tests PRIVATE libc) + configure_windows_unwind(guest_time_extras_tests) + add_test(NAME guest_time_extras COMMAND guest_time_extras_tests) + set_tests_properties(guest_time_extras PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_time_extras PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(precise_wait_tests ${LIBS_INCLUDE_DIR}/tests/PreciseWait.cpp) target_include_directories(precise_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(precise_wait_tests PRIVATE libc) @@ -138,6 +263,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) @@ -157,6 +291,15 @@ if(BUILD_TESTING) set_tests_properties(guest_wide_strings PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_atomic_builtins_tests ${LIBS_INCLUDE_DIR}/tests/GuestAtomicBuiltins.cpp) + target_include_directories(guest_atomic_builtins_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_atomic_builtins_tests PRIVATE libc) + configure_windows_unwind(guest_atomic_builtins_tests) + add_test(NAME guest_atomic_builtins COMMAND guest_atomic_builtins_tests) + if(WIN32) + set_tests_properties(guest_atomic_builtins PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_wide_conversion_tests ${LIBS_INCLUDE_DIR}/tests/GuestWideConversion.cpp) target_include_directories(guest_wide_conversion_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_wide_conversion_tests PRIVATE libc) @@ -166,6 +309,25 @@ if(BUILD_TESTING) set_tests_properties(guest_wide_conversion PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_bounds_checking_tests ${LIBS_INCLUDE_DIR}/tests/GuestBoundsChecking.cpp) + target_include_directories(guest_bounds_checking_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_bounds_checking_tests PRIVATE libc) + configure_windows_unwind(guest_bounds_checking_tests) + add_test(NAME guest_bounds_checking COMMAND guest_bounds_checking_tests guest_bounds_checking.tmp) + set_tests_properties(guest_bounds_checking PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_bounds_checking PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_float_builtins_tests ${LIBS_INCLUDE_DIR}/tests/GuestFloatBuiltins.cpp) + target_include_directories(guest_float_builtins_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_float_builtins_tests PRIVATE libc) + configure_windows_unwind(guest_float_builtins_tests) + add_test(NAME guest_float_builtins COMMAND guest_float_builtins_tests) + if(WIN32) + set_tests_properties(guest_float_builtins PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_character_tests ${LIBS_INCLUDE_DIR}/tests/GuestCharacter.cpp) target_include_directories(guest_character_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_character_tests PRIVATE libc) @@ -193,6 +355,33 @@ if(BUILD_TESTING) set_tests_properties(guest_random_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_integer_builtins_tests ${LIBS_INCLUDE_DIR}/tests/GuestIntegerBuiltins.cpp) + target_include_directories(guest_integer_builtins_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_integer_builtins_tests PRIVATE libc) + configure_windows_unwind(guest_integer_builtins_tests) + add_test(NAME guest_integer_builtins COMMAND guest_integer_builtins_tests) + if(WIN32) + set_tests_properties(guest_integer_builtins PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_wide_stream_scan_tests ${LIBS_INCLUDE_DIR}/tests/GuestWideStreamScan.cpp) + target_include_directories(guest_wide_stream_scan_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_wide_stream_scan_tests PRIVATE libc) + configure_windows_unwind(guest_wide_stream_scan_tests) + add_test(NAME guest_wide_stream_scan COMMAND guest_wide_stream_scan_tests) + if(WIN32) + set_tests_properties(guest_wide_stream_scan PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_stdio_variadic_tests ${LIBS_INCLUDE_DIR}/tests/GuestStdioVariadic.cpp) + target_include_directories(guest_stdio_variadic_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_stdio_variadic_tests PRIVATE libc) + configure_windows_unwind(guest_stdio_variadic_tests) + add_test(NAME guest_stdio_variadic COMMAND guest_stdio_variadic_tests) + if(WIN32) + set_tests_properties(guest_stdio_variadic PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_execute_once_tests ${LIBS_INCLUDE_DIR}/tests/GuestExecuteOnce.cpp) target_include_directories(guest_execute_once_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_execute_once_tests PRIVATE libc) @@ -213,6 +402,36 @@ if(BUILD_TESTING) set_tests_properties(guest_std_execute_once PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_execute_once_concurrency_tests ${LIBS_INCLUDE_DIR}/tests/GuestExecuteOnceConcurrency.cpp) + target_include_directories(guest_execute_once_concurrency_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_execute_once_concurrency_tests PRIVATE libc) + configure_windows_unwind(guest_execute_once_concurrency_tests) + foreach(onceEntry IN ITEMS mangled plain) + add_test(NAME guest_execute_once_concurrency_${onceEntry} COMMAND guest_execute_once_concurrency_tests ${onceEntry}) + set_tests_properties(guest_execute_once_concurrency_${onceEntry} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_execute_once_concurrency_${onceEntry} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test_executable(guest_math_exact_tests ${LIBS_INCLUDE_DIR}/tests/GuestMathExact.cpp) + target_include_directories(guest_math_exact_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_math_exact_tests PRIVATE libc) + configure_windows_unwind(guest_math_exact_tests) + add_test(NAME guest_math_exact COMMAND guest_math_exact_tests) + if(WIN32) + set_tests_properties(guest_math_exact PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_bessel_tests ${LIBS_INCLUDE_DIR}/tests/GuestBessel.cpp) + target_include_directories(guest_bessel_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_bessel_tests PRIVATE libc) + configure_windows_unwind(guest_bessel_tests) + add_test(NAME guest_bessel COMMAND guest_bessel_tests) + if(WIN32) + set_tests_properties(guest_bessel PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_libc_threads_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibcThreads.cpp) target_include_directories(guest_libc_threads_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_libc_threads_tests PRIVATE libc libkernel) @@ -271,9 +490,19 @@ if(BUILD_TESTING) set_tests_properties(guest_path_case PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_path_drive_component_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathDriveComponent.cpp) + target_include_directories(guest_path_drive_component_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_path_drive_component_tests PRIVATE libc) + configure_windows_unwind(guest_path_drive_component_tests) + add_test(NAME guest_path_drive_component COMMAND guest_path_drive_component_tests) + set_tests_properties(guest_path_drive_component PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_path_drive_component PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_directory_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectory.cpp) target_include_directories(guest_directory_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_directory_tests PRIVATE libc) + target_link_libraries(guest_directory_tests PRIVATE libc libkernel) configure_windows_unwind(guest_directory_tests) add_test(NAME guest_directory COMMAND guest_directory_tests) if(WIN32) @@ -289,6 +518,24 @@ if(BUILD_TESTING) set_tests_properties(guest_environment PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_dinkumware_atomics_tests ${LIBS_INCLUDE_DIR}/tests/GuestDinkumwareAtomics.cpp) + target_include_directories(guest_dinkumware_atomics_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_dinkumware_atomics_tests PRIVATE libc) + configure_windows_unwind(guest_dinkumware_atomics_tests) + add_test(NAME guest_dinkumware_atomics COMMAND guest_dinkumware_atomics_tests) + if(WIN32) + set_tests_properties(guest_dinkumware_atomics PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_dinkumware_internals_tests ${LIBS_INCLUDE_DIR}/tests/GuestDinkumwareInternals.cpp) + target_include_directories(guest_dinkumware_internals_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_dinkumware_internals_tests PRIVATE libc) + configure_windows_unwind(guest_dinkumware_internals_tests) + add_test(NAME guest_dinkumware_internals COMMAND guest_dinkumware_internals_tests) + if(WIN32) + set_tests_properties(guest_dinkumware_internals PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_hostname_tests ${LIBS_INCLUDE_DIR}/tests/GuestHostname.cpp) target_include_directories(guest_hostname_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_hostname_tests PRIVATE libc) @@ -299,6 +546,15 @@ if(BUILD_TESTING) set_tests_properties(guest_hostname PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_vector_algorithms_tests ${LIBS_INCLUDE_DIR}/tests/GuestVectorAlgorithms.cpp) + target_include_directories(guest_vector_algorithms_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_vector_algorithms_tests PRIVATE libc) + configure_windows_unwind(guest_vector_algorithms_tests) + add_test(NAME guest_vector_algorithms COMMAND guest_vector_algorithms_tests) + if(WIN32) + set_tests_properties(guest_vector_algorithms PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_memrchr_tests ${LIBS_INCLUDE_DIR}/tests/GuestMemrchr.cpp) target_include_directories(guest_memrchr_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_memrchr_tests PRIVATE libc) @@ -390,8 +646,10 @@ if(BUILD_TESTING) target_link_libraries(guest_standard_stream_tests PRIVATE libc) configure_windows_unwind(guest_standard_stream_tests) add_test(NAME guest_standard_streams COMMAND guest_standard_stream_tests) + add_test(NAME guest_standard_streams_closed_stdout COMMAND guest_standard_stream_tests --closed-stdout) + add_test(NAME guest_standard_streams_exclusive_stdout COMMAND guest_standard_stream_tests --exclusive-stdout) if(WIN32) - set_tests_properties(guest_standard_streams PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_standard_streams guest_standard_streams_closed_stdout guest_standard_streams_exclusive_stdout PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(file_streams_tests ${LIBS_INCLUDE_DIR}/tests/FileStreams.cpp) @@ -413,17 +671,35 @@ if(BUILD_TESTING) set_tests_properties(guest_errors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - if(WIN32) + if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") add_test_executable(guest_call_trace_tests ${LIBS_INCLUDE_DIR}/tests/GuestCallTrace.cpp) target_include_directories(guest_call_trace_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_call_trace_tests PRIVATE libc) configure_windows_unwind(guest_call_trace_tests) add_test(NAME guest_call_trace COMMAND guest_call_trace_tests) add_test(NAME guest_call_trace_abort COMMAND guest_call_trace_tests abort) - set_tests_properties(guest_call_trace guest_call_trace_abort PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") + if(WIN32) + set_tests_properties(guest_call_trace guest_call_trace_abort PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set_tests_properties(guest_call_trace_abort PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION - "last traced system library calls[^~]*#1 thread [0-9]+ libTraced.prx Integers from [^~]*guest_call_trace_tests.exe[^~]*#2 thread [0-9]+ libOther.prx Blocks from[^~]*most called traced imports \\(2 calls in total\\):[^~]* 1 libTraced.prx Integers[^~]* 1 libOther.prx Blocks") + "last traced system library calls[^~]*#1 thread [0-9]+ libTraced.prx Integers from [^~]*guest_call_trace_tests${CMAKE_EXECUTABLE_SUFFIX}[^~]*#2 thread [0-9]+ libOther.prx Blocks from[^~]*last traced calls of thread [0-9]+:[^~]*#1 libTraced.prx Integers from[^~]*#2 libOther.prx Blocks from[^~]*most called traced imports \\(2 calls in total\\):[^~]* 1 libTraced.prx Integers[^~]* 1 libOther.prx Blocks") + if(NOT WIN32) + add_test(NAME guest_call_trace_abort_race COMMAND guest_call_trace_tests abort-race) + set_tests_properties(guest_call_trace_abort_race PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION + "last traced system library calls[^~]*#102 thread [0-9]+ libTraced.prx Integers from[^~]*most called traced imports \\(102 calls in total\\):[^~]* 101 libTraced.prx Integers[^~]* 1 libOther.prx Blocks") + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_call_trace_fixed_base_tests ${LIBS_INCLUDE_DIR}/tests/GuestCallTrace.cpp) + target_include_directories(guest_call_trace_fixed_base_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_call_trace_fixed_base_tests PRIVATE libc) + set_target_properties(guest_call_trace_fixed_base_tests PROPERTIES POSITION_INDEPENDENT_CODE OFF) + target_compile_options(guest_call_trace_fixed_base_tests PRIVATE -fno-pie) + target_link_options(guest_call_trace_fixed_base_tests PRIVATE -no-pie -fuse-ld=bfd) + add_test(NAME guest_call_trace_fixed_base_abort COMMAND guest_call_trace_fixed_base_tests abort) + set_tests_properties(guest_call_trace_fixed_base_abort PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION + "#1 thread [0-9]+ libTraced.prx Integers from 0x00000000004[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f] guest_call_trace_fixed_base_tests\\+0x[0-9a-f]?[0-9a-f]?[0-9a-f]?[0-9a-f]?[0-9a-f][^0-9a-f]") + endif() endif() add_test_executable(guest_syslog_tests ${LIBS_INCLUDE_DIR}/tests/GuestSyslog.cpp) target_include_directories(guest_syslog_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -521,6 +797,17 @@ if(BUILD_TESTING) set_tests_properties(guest_wcsrtombs_s PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(unwind_callback_abi_tests ${LIBS_INCLUDE_DIR}/tests/UnwindCallbackAbi.cpp) + target_include_directories(unwind_callback_abi_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(unwind_callback_abi_tests PRIVATE libc) + configure_windows_unwind(unwind_callback_abi_tests) + foreach(callback IN ITEMS backtrace forced_unwind delete_exception end_catch end_catch_gnu) + add_test(NAME unwind_callback_abi_${callback} COMMAND unwind_callback_abi_tests ${callback}) + if(WIN32) + set_tests_properties(unwind_callback_abi_${callback} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(guest_overflow_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestOverflowError.cpp) target_include_directories(guest_overflow_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_overflow_error_tests PRIVATE libc) @@ -530,6 +817,15 @@ if(BUILD_TESTING) set_tests_properties(guest_overflow_error PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_bad_optional_access_tests ${LIBS_INCLUDE_DIR}/tests/GuestBadOptionalAccess.cpp) + target_include_directories(guest_bad_optional_access_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_bad_optional_access_tests PRIVATE libc) + configure_windows_unwind(guest_bad_optional_access_tests) + add_test(NAME guest_bad_optional_access COMMAND guest_bad_optional_access_tests) + if(WIN32) + set_tests_properties(guest_bad_optional_access PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(UNIX) add_test_executable(exception_runtime_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionRuntime.cpp) target_include_directories(exception_runtime_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -543,6 +839,11 @@ if(BUILD_TESTING) target_include_directories(guest_exit_order_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_exit_order_tests PRIVATE libc) add_test(NAME guest_exit_order COMMAND guest_exit_order_tests) + add_test_executable(guest_path_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathLifetime.cpp) + target_include_directories(guest_path_lifetime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_path_lifetime_tests PRIVATE libc) + add_test(NAME guest_path_lifetime COMMAND guest_path_lifetime_tests) + set_tests_properties(guest_path_lifetime PROPERTIES TIMEOUT 20) if(NOT APPLE) enable_language(C) add_test_executable(exception_c_cleanup_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionCCleanup.cpp ${LIBS_INCLUDE_DIR}/tests/ExceptionCCleanup.c) @@ -581,8 +882,20 @@ if(BUILD_TESTING) add_test(NAME application_heap_invalid_metadata COMMAND application_heap_tests invalid) add_test(NAME application_heap_default COMMAND application_heap_tests default) add_test(NAME application_heap_exit_order COMMAND application_heap_tests exit-order) + add_test(NAME application_heap_scripting COMMAND application_heap_tests scripting) + add_test(NAME application_heap_lazy COMMAND application_heap_tests lazy) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_library(guest_sdk_heap_fixture MODULE ${LIBS_INCLUDE_DIR}/tests/GuestSdkHeap.cpp) + add_library(guest_sdk_heap_partial_fixture MODULE ${LIBS_INCLUDE_DIR}/tests/GuestSdkHeap.cpp) + target_compile_definitions(guest_sdk_heap_partial_fixture PRIVATE SDK_HEAP_PARTIAL) + set_target_properties(application_heap_tests PROPERTIES ENABLE_EXPORTS ON) + add_dependencies(application_heap_tests guest_sdk_heap_fixture guest_sdk_heap_partial_fixture) + add_test(NAME application_heap_guest_sdk COMMAND application_heap_tests sdk $) + add_test(NAME application_heap_guest_sdk_partial COMMAND application_heap_tests sdk-partial $) + add_test(NAME application_heap_guest_sdk_default COMMAND application_heap_tests sdk-default $) + endif() if(WIN32) - set_tests_properties(application_heap_replacement application_heap_invalid_metadata application_heap_default application_heap_exit_order + set_tests_properties(application_heap_replacement application_heap_invalid_metadata application_heap_default application_heap_exit_order application_heap_scripting application_heap_lazy PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_allocator_failures_tests ${LIBS_INCLUDE_DIR}/tests/GuestAllocatorFailures.cpp) @@ -611,13 +924,33 @@ if(BUILD_TESTING) target_include_directories(cpu_topology_tests PRIVATE ${LIBS_INCLUDE_DIR}) add_test(NAME cpu_topology COMMAND cpu_topology_tests) endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" OR WIN32) + add_test_executable(guest_backtrace_tests ${LIBS_INCLUDE_DIR}/tests/GuestBacktrace.cpp) + target_include_directories(guest_backtrace_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_backtrace_tests PRIVATE libc) + configure_windows_unwind(guest_backtrace_tests) + set_target_properties(guest_backtrace_tests PROPERTIES ENABLE_EXPORTS ON) + add_test(NAME guest_backtrace COMMAND guest_backtrace_tests) + if(WIN32) + set_tests_properties(guest_backtrace PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endif() add_test_executable(log_macros_tests ${LIBS_INCLUDE_DIR}/tests/LogMacros.cpp) target_include_directories(log_macros_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(log_macros_tests PRIVATE libc) configure_windows_unwind(log_macros_tests) add_test(NAME log_macros COMMAND log_macros_tests) + add_test(NAME log_macros_timestamps COMMAND log_macros_tests timestamps) if(WIN32) - set_tests_properties(log_macros PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(log_macros log_macros_timestamps PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + add_test_executable(thread_name_tests ${LIBS_INCLUDE_DIR}/tests/ThreadName.cpp) + target_include_directories(thread_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(thread_name_tests PRIVATE libc) + configure_windows_unwind(thread_name_tests) + add_test(NAME thread_name COMMAND thread_name_tests) + set_tests_properties(thread_name PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() @@ -683,6 +1016,18 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(guest_libc_cnd_init_default_name_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestLibcCndInitDefaultName.cpp) +target_include_directories(guest_libc_cnd_init_default_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(guest_libc_cnd_init_default_name_tests PRIVATE libc) +configure_windows_unwind(guest_libc_cnd_init_default_name_tests) +if(BUILD_TESTING) + set_target_properties(guest_libc_cnd_init_default_name_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_libc_cnd_init_default_name COMMAND guest_libc_cnd_init_default_name_tests) + if(WIN32) + set_tests_properties(guest_libc_cnd_init_default_name PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(guest_libc_cnd_timed_wait_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestLibcCndTimedWait.cpp) target_include_directories(guest_libc_cnd_timed_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_libc_cnd_timed_wait_tests PRIVATE libc) @@ -695,6 +1040,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(guest_heap_trace_info_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestHeapTraceInfo.cpp) +target_include_directories(guest_heap_trace_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(guest_heap_trace_info_tests PRIVATE libc) +configure_windows_unwind(guest_heap_trace_info_tests) +if(BUILD_TESTING) + set_target_properties(guest_heap_trace_info_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_heap_trace_info COMMAND guest_heap_trace_info_tests) + if(WIN32) + set_tests_properties(guest_heap_trace_info PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + if(MINGW) if(BUILD_TESTING) enable_language(ASM) @@ -704,7 +1062,8 @@ if(MINGW) target_include_directories(guest_message_destructor_abi_tests PRIVATE ${LIBS_INCLUDE_DIR}) 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 + foreach(mode IN ITEMS control hybrid 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 @@ -738,5 +1097,24 @@ 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") + add_test(NAME windows_crash_stack_words COMMAND windows_backtrace_report_tests fault) + set_tests_properties(windows_crash_stack_words PROPERTIES TIMEOUT 20 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;APS5_CRASH_STACK_BYTES=set:2048" + PASS_REGULAR_EXPRESSION "FATAL: unhandled exception 0xc0000005[^~]*stack words from rsp[^~]*data at stack words that point outside the stack[^~]*c0ffee0000000000 c0ffee0000000001 c0ffee0000000002 c0ffee0000000003 c0ffee0000000004 c0ffee0000000005 c0ffee0000000006 c0ffee0000000007") + add_test(NAME windows_crash_without_stack_words COMMAND windows_backtrace_report_tests fault) + set_tests_properties(windows_crash_without_stack_words PROPERTIES TIMEOUT 20 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;APS5_CRASH_STACK_BYTES=unset:" + PASS_REGULAR_EXPRESSION "FATAL: unhandled exception 0xc0000005" + FAIL_REGULAR_EXPRESSION "stack words from rsp;data at stack words") 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/ApplicationHeap.hpp b/core/libs/prx/libc/include/ApplicationHeap.hpp index ea9e7ad411..00033a1d59 100644 --- a/core/libs/prx/libc/include/ApplicationHeap.hpp +++ b/core/libs/prx/libc/include/ApplicationHeap.hpp @@ -6,6 +6,7 @@ extern "C" { const void* ApplicationProcessParameters_nid_no_patch(); +bool ApplicationGuestHeapApi_nid_no_patch(void** api); void ApplicationHeapInitialize_nid_no_patch(const void* processParameters); void ApplicationHeapRegister_nid_no_patch(void* const* api); void* ApplicationHeapAllocate_nid_no_patch(std::size_t bytes); diff --git a/core/libs/prx/libc/include/CallTrace.hpp b/core/libs/prx/libc/include/CallTrace.hpp index 2c1f46c600..b53c1ba96e 100644 --- a/core/libs/prx/libc/include/CallTrace.hpp +++ b/core/libs/prx/libc/include/CallTrace.hpp @@ -21,6 +21,7 @@ struct CallTraceCount { extern "C" { void* TraceImport_nid_no_patch(void* target, const char* name, const char* library); std::size_t CopyCallTrace_nid_no_patch(CallTraceEntry* entries, std::size_t capacity); +std::size_t CopyThreadCallTrace_nid_no_patch(std::uint64_t thread, CallTraceEntry* entries, std::size_t capacity); std::size_t CopyCallCounts_nid_no_patch(CallTraceCount* entries, std::size_t capacity, std::uint64_t* total); } diff --git a/core/libs/prx/libc/include/CpuTopology.hpp b/core/libs/prx/libc/include/CpuTopology.hpp index a1061cb3c4..0b51816e23 100644 --- a/core/libs/prx/libc/include/CpuTopology.hpp +++ b/core/libs/prx/libc/include/CpuTopology.hpp @@ -54,6 +54,24 @@ inline std::uint64_t MaskFromCpuList(const char* path) { } #endif +inline Layout LayoutFromClasses(const std::uint64_t* byClass, const bool* seen, std::size_t count) { + Layout result{}; + int lowest = -1; + int highest = -1; + for (std::size_t cls = 0; cls < count; ++cls) { + if (!seen[cls]) continue; + if (lowest < 0) lowest = static_cast(cls); + highest = static_cast(cls); + } + if (lowest < 0 || lowest == highest) return result; + for (int cls = 0; cls < highest; ++cls) { + if (seen[cls]) result.efficient |= byClass[cls]; + } + result.performant = byClass[highest]; + result.hybrid = result.efficient != 0 && result.performant != 0; + return result; +} + inline const Layout& Get() { static const Layout layout = [] { Layout result{}; @@ -78,17 +96,10 @@ inline const Layout& Get() { } offset += entry->Size; } - int lowest = -1; - int highest = -1; - for (int cls = 0; cls < 256; ++cls) { - if (!seen[cls]) continue; - if (lowest < 0) lowest = cls; - highest = cls; - } - if (lowest < 0 || lowest == highest) return result; - result.efficient = byClass[lowest]; - result.performant = byClass[highest]; - result.hybrid = result.efficient != 0 && result.performant != 0; + const auto classified = LayoutFromClasses(byClass, seen, 256); + result.efficient = classified.efficient; + result.performant = classified.performant; + result.hybrid = classified.hybrid; #elif defined(__linux__) cpu_set_t set; CPU_ZERO(&set); diff --git a/core/libs/prx/libc/include/CrashText.hpp b/core/libs/prx/libc/include/CrashText.hpp new file mode 100644 index 0000000000..675cc7c096 --- /dev/null +++ b/core/libs/prx/libc/include/CrashText.hpp @@ -0,0 +1,59 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_CRASHTEXT_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_CRASHTEXT_HPP + +#include +#include + +namespace crash_text { + +constexpr std::size_t MaximumCharacters = 512; +constexpr std::size_t MinimumCharacters = 4; +constexpr std::size_t MaximumBytes = 2 * (MaximumCharacters + 1); + +inline std::uint16_t Unit(const std::uint8_t* bytes, std::size_t index, std::size_t width) { + if (width == 1) return bytes[index]; + return static_cast(bytes[2 * index] | bytes[2 * index + 1] << 8); +} + +inline bool IsText(std::uint16_t unit) { + return (unit >= 0x20 && unit < 0x7f) || unit == '\t' || unit == '\n' || unit == '\r'; +} + +inline bool StartsWithText(const std::uint8_t* bytes, std::size_t size, std::size_t width) { + if (size < MinimumCharacters * width) return false; + for (std::size_t index = 0; index < MinimumCharacters; ++index) { + if (!IsText(Unit(bytes, index, width))) return false; + } + return true; +} + +inline bool Format(const std::uint8_t* bytes, std::size_t size, char* out, std::size_t capacity) { + const std::size_t width = StartsWithText(bytes, size, 2) ? 2 : StartsWithText(bytes, size, 1) ? 1 : 0; + if (width == 0 || capacity < 8) return false; + std::size_t length = 0; + if (width == 2) out[length++] = 'u'; + out[length++] = '"'; + bool terminated = false; + for (std::size_t index = 0; (index + 1) * width <= size; ++index) { + const std::uint16_t unit = Unit(bytes, index, width); + if (unit == 0) { + terminated = true; + break; + } + if (index == MaximumCharacters || !IsText(unit)) break; + const char escape = unit == '\n' ? 'n' : unit == '\r' ? 'r' : unit == '\t' ? 't' : unit == '"' || unit == '\\' ? static_cast(unit) : '\0'; + if (length + (escape != '\0' ? 2 : 1) + 5 > capacity) break; + if (escape != '\0') out[length++] = '\\'; + out[length++] = escape != '\0' ? escape : static_cast(unit); + } + out[length++] = '"'; + if (!terminated) { + for (int dot = 0; dot < 3; ++dot) out[length++] = '.'; + } + out[length] = '\0'; + return true; +} + +} + +#endif diff --git a/core/libs/prx/libc/include/FailureReport.hpp b/core/libs/prx/libc/include/FailureReport.hpp new file mode 100644 index 0000000000..7a05284d6e --- /dev/null +++ b/core/libs/prx/libc/include/FailureReport.hpp @@ -0,0 +1,8 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_FAILUREREPORT_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_FAILUREREPORT_HPP + +extern "C" void LibcRegisterFailureReport_nid_no_patch(void (*report)() noexcept); +extern "C" void LibcUnregisterFailureReport_nid_no_patch(void (*report)() noexcept) noexcept; +extern "C" void LibcRunFailureReports_nid_no_patch() noexcept; + +#endif diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 4ad9fa00b8..3f1d90eb88 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -1,6 +1,8 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_FILESTREAM_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_FILESTREAM_HPP +#include +#include #include #include #include @@ -15,7 +17,22 @@ #include "prx/libc/include/FileLock.hpp" extern "C" void* HostStandardHandle_nid_no_patch(int descriptor); extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle); +struct UcrtStreamState { + char* pointer; + char* base; + int count; + long flags; +}; +extern "C" void LibcRegisterDirectoryStreams_nid_no_patch(int (*open)(const wchar_t* path), void (*forget)(int descriptor)); +extern "C" void ForgetDirectoryStream_nid_no_patch(int descriptor); +#else +#include #endif +#if defined(__GLIBC__) +#include +extern "C" int _IO_switch_to_get_mode(std::FILE* stream); +#endif +extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor); struct GuestFilePrefix { unsigned char* position = nullptr; @@ -50,6 +67,8 @@ class FileStream { std::FILE* _handle; bool _dynamic; bool guestError = false; + bool randomDevice = false; + bool directory = false; public: explicit FileStream(std::FILE* handle, bool dynamic = false) : _handle(handle), _dynamic(dynamic) { @@ -71,26 +90,117 @@ public: return _handle; } + bool IsOpen() const { + return _handle != nullptr; + } + bool IsDynamic() const { return _dynamic; } GuestFilePrefix& GuestState() { return _guest; } - bool Reopen(const std::filesystem::path& filename, const char* mode) { + bool IsHostStandard() const { + return _handle == stdin || _handle == stdout || _handle == stderr; + } + + void Detach() { + _handle = nullptr; + _guest.flags = 0; + _guest.descriptor = -1; + } + + void RestoreHostStandard(std::FILE* stream, int kept) { + const int error = errno; +#ifdef _WIN32 + const bool restored = _handle || (::_wfreopen(L"NUL", stream == stdin ? L"r" : L"w", stream) && ::_dup2(kept, ::_fileno(stream)) == 0); + ::_unlock_file(stream); + ::_close(kept); +#else + const bool restored = _handle || (std::freopen("/dev/null", stream == stdin ? "r" : "w", stream) && ::dup2(kept, ::fileno(stream)) >= 0); + ::funlockfile(stream); + ::close(kept); +#endif + if (!restored) throw std::runtime_error("FileStream: cannot restore the host standard stream"); + errno = error; + } + + void MarkRandomDevice() { + randomDevice = true; + _guest.descriptor = -1; + } + + bool IsRandomDevice() const { + return randomDevice; + } + + void MarkDirectory() { + directory = true; + } + + bool IsDirectory() const { + return directory; + } + + bool Reopen(const std::filesystem::path& filename, const char* mode, + [[maybe_unused]] std::FILE* (*open)(const std::filesystem::path&, const std::string&) = nullptr) { auto* previous = GetHandle(); _guest = {}; guestError = false; + randomDevice = false; #ifdef _WIN32 std::wstring wideMode; for (const char character : std::string_view(mode)) wideMode.push_back(static_cast(character)); const int replaced = previous ? _fileno(previous) : -1; + if (std::exchange(directory, false)) ForgetDirectoryStream_nid_no_patch(replaced); auto* const standard = HostStandardHandle_nid_no_patch(replaced); GuestFileLockForget_nid_no_patch(replaced); - _handle = ::_wfreopen(filename.c_str(), wideMode.c_str(), previous); + if (open && !filename.empty()) { + if (!IsHostStandard()) { + _handle = nullptr; + std::fclose(previous); + _handle = open(filename, mode); + } else { + std::wstring resetMode; + for (const char character : std::string_view(mode)) + if (character != 'x') resetMode.push_back(static_cast(character)); + const int kept = DuplicateHostDescriptor_nid_no_patch(replaced); + if (kept >= 0) ::_lock_file(previous); + _handle = nullptr; + try { + std::FILE* const replacement = open(filename, mode); + if (replacement) { + if (::_wfreopen(L"NUL", resetMode.c_str(), previous) == previous) { + const int moved = ::_dup2(_fileno(replacement), replaced); + const int error = errno; + std::fclose(replacement); + if (moved == 0) _handle = previous; + else errno = error; + } else { + const int error = errno; + std::fclose(replacement); + errno = error; + } + } + } catch (...) { + if (kept >= 0) RestoreHostStandard(previous, kept); + KeepStandardHandle_nid_no_patch(replaced, standard); + throw; + } + if (kept >= 0) RestoreHostStandard(previous, kept); + } + } else { + const int kept = IsHostStandard() ? DuplicateHostDescriptor_nid_no_patch(replaced) : -1; + if (kept >= 0) ::_lock_file(previous); + _handle = ::_wfreopen(filename.c_str(), wideMode.c_str(), previous); + if (kept >= 0) RestoreHostStandard(previous, kept); + } KeepStandardHandle_nid_no_patch(replaced, standard); #else if (::fileno(previous) == 2) KeepHostStandardError_nid_no_patch(); + const int kept = IsHostStandard() ? DuplicateHostDescriptor_nid_no_patch(::fileno(previous)) : -1; + if (kept >= 0) ::flockfile(previous); _handle = std::freopen(filename.c_str(), mode, previous); + if (kept >= 0) RestoreHostStandard(previous, kept); #endif if (!_handle) return false; _guest.flags = 0x10; @@ -102,16 +212,21 @@ public: _guest.descriptor = descriptor >= 0 && descriptor <= 32767 ? static_cast(descriptor) : -1; return true; } + void PublishStatus(bool endOfFile, bool error) { + __atomic_store_n(&_guest.readRemaining, 0, __ATOMIC_RELAXED); + __atomic_store_n(&_guest.writeRemaining, 0, __ATOMIC_RELAXED); + auto old = __atomic_load_n(&_guest.flags, __ATOMIC_RELAXED); + std::int16_t next; + do next = static_cast((old & ~0x60) | (endOfFile ? 0x20 : 0) | (error ? 0x40 : 0)); + while (!__atomic_compare_exchange_n(&_guest.flags, &old, next, false, __ATOMIC_RELAXED, __ATOMIC_RELAXED)); + } void SyncStatus() { - _guest.readRemaining = 0; - _guest.writeRemaining = 0; - _guest.flags = static_cast((_guest.flags & ~0x60) | - (std::feof(GetHandle()) ? 0x20 : 0) | (std::ferror(GetHandle()) || guestError ? 0x40 : 0)); + if (!_handle) return; + PublishStatus(std::feof(GetHandle()) != 0, std::ferror(GetHandle()) != 0 || guestError); } void SyncUnlockedStatus() { - _guest.readRemaining = 0; - _guest.writeRemaining = 0; + if (!_handle) return; #ifdef _WIN32 const bool endOfFile = std::feof(GetHandle()) != 0; const bool error = std::ferror(GetHandle()) != 0; @@ -119,8 +234,48 @@ public: const bool endOfFile = ::feof_unlocked(GetHandle()) != 0; const bool error = ::ferror_unlocked(GetHandle()) != 0; #endif - _guest.flags = static_cast((_guest.flags & ~0x60) | - (endOfFile ? 0x20 : 0) | (error || guestError ? 0x40 : 0)); + PublishStatus(endOfFile, error || guestError); + } + + bool SwitchToReading() { +#ifdef _WIN32 + ::_lock_file(GetHandle()); + const bool switched = SwitchToReadingUnlocked(); + ::_unlock_file(GetHandle()); + return switched; +#else + ::flockfile(GetHandle()); + const bool switched = SwitchToReadingUnlocked(); + ::funlockfile(GetHandle()); + return switched; +#endif + } + + bool SwitchToReadingUnlocked() { +#ifdef _WIN32 + constexpr long UcrtReading = 0x0001; + constexpr long UcrtWriting = 0x0002; + constexpr long UcrtUpdate = 0x0004; + auto* native = GetHandle(); + std::atomic_ref state(reinterpret_cast(native)->flags); + if ((state.load() & (UcrtReading | UcrtUpdate)) != UcrtUpdate) return true; + const bool switched = (state.load() & UcrtWriting) == 0 || ::_fflush_nolock(native) == 0; + if (switched) state.fetch_or(UcrtReading); + SyncUnlockedStatus(); + return switched; +#elif defined(__GLIBC__) + auto* native = GetHandle(); + if (!::__freadable(native) || !::__fwriting(native)) return true; + const bool switched = ::_IO_switch_to_get_mode(native) == 0; + SyncUnlockedStatus(); + return switched; +#else + return true; +#endif + } + + void KeepDirectoryReadError(bool failed) { + if (failed && directory && (_guest.flags & 0x40) != 0) errno = 21; } void SetError() { @@ -129,6 +284,7 @@ public: } void ClearError() { + if (!_handle) return; std::clearerr(GetHandle()); guestError = false; SyncStatus(); @@ -138,6 +294,7 @@ public: GetHandle(); #ifdef _WIN32 GuestFileLockForget_nid_no_patch(::_fileno(_handle)); + if (std::exchange(directory, false)) ForgetDirectoryStream_nid_no_patch(::_fileno(_handle)); #endif const int result = std::fclose(std::exchange(_handle, nullptr)); _guest.flags = 0; @@ -147,11 +304,17 @@ public: }; static_assert(std::is_standard_layout_v); -inline std::FILE* GetNativeStream(FileStream* stream) { +inline std::FILE* GetStreamHandle(FileStream* stream) { if (!stream) throw std::runtime_error("FileStream: null stream"); return stream->GetHandle(); } +inline std::FILE* GetNativeStream(FileStream* stream) { + auto* handle = GetStreamHandle(stream); + if (stream->IsRandomDevice()) throw std::runtime_error("FileStream: this function is not implemented on a /dev/random or /dev/urandom stream"); + return handle; +} + extern "C" { extern FileStream _Stdout_nid_postfix; extern FileStream _Stderr_nid_postfix; diff --git a/core/libs/prx/libc/include/FilesystemError.hpp b/core/libs/prx/libc/include/FilesystemError.hpp index 8962c34b16..bb50c8d70e 100644 --- a/core/libs/prx/libc/include/FilesystemError.hpp +++ b/core/libs/prx/libc/include/FilesystemError.hpp @@ -23,11 +23,25 @@ inline int FilesystemError(const std::error_code& error) { return 5; } -std::error_code RenameNative_nid_no_patch(const std::filesystem::path& source, const std::filesystem::path& destination, bool replacesFile); +inline bool HasNonDirectoryPrefix_nid_no_patch(const std::filesystem::path& native) { + std::error_code error; + const auto trimmed = native.has_filename() ? native : native.parent_path(); + for (auto prefix = trimmed.parent_path(); prefix.has_relative_path(); prefix = prefix.parent_path()) { + const auto status = std::filesystem::status(prefix, error); + if (std::filesystem::exists(status)) return !std::filesystem::is_directory(status); + } + return false; +} -inline int ResolvePathError_nid_no_patch(const char* path, std::filesystem::path& resolved) { +inline int NotFoundError_nid_no_patch(const std::filesystem::path& native) { + return HasNonDirectoryPrefix_nid_no_patch(native) ? 20 : 2; +} + +std::error_code RenameNative_nid_no_patch(const std::filesystem::path& source, const std::filesystem::path& destination, bool replacesExisting); + +inline int ResolvePathError_nid_no_patch(const char* path, std::filesystem::path& resolved, bool module = false, bool archived = false) { try { - resolved = 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 654f5c78c3..f22c2e8556 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -2,22 +2,36 @@ #define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_HPP #include +#include #include #include +#include +#include #include "general/LogMacros.hpp" #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); +extern "C" bool RejectIntegerBase_nid_no_patch(const char* str, char** endptr, int base, bool setError); #ifndef _WIN32 extern "C" int KeepHostStandardError_nid_no_patch(); extern "C" int HostStandardError_nid_no_patch(); +extern "C" int KeepHostStandardOutput_nid_no_patch(); +#else +extern "C" bool AdoptHostStandardStream_nid_no_patch(int standard, int saved); +extern "C" std::FILE* HostStandardOutputFile_nid_no_patch(); +extern "C" std::FILE* HostStandardErrorFile_nid_no_patch(); #endif +extern "C" bool CopyGuestEnvironment_nid_no_patch(const char* name, std::string& value); + extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path); +extern "C" std::filesystem::path ResolvePathOrRelinkedModule_nid_no_patch(const char* path); +extern "C" std::filesystem::path ResolveArchivedPath_nid_no_patch(const char* path, bool module = false); +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); @@ -26,16 +40,37 @@ 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(); extern "C" void BlockPathAlias_nid_no_patch(const char* guestPrefix); +extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor); #ifdef _WIN32 extern "C" int WindowsOpenError_nid_no_patch(unsigned long error); extern "C" void* HostStandardHandle_nid_no_patch(int descriptor); extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle); #endif +using PthreadCreateFn_nid_no_patch = int (APS5_VABI*)(void**, const void*, void* (APS5_VABI*)(void*), void*); +using PthreadJoinFn_nid_no_patch = int (APS5_VABI*)(void*, void**); +using PthreadDetachFn_nid_no_patch = int (APS5_VABI*)(void*); +extern "C" void RegisterPthreadCreate_nid_no_patch(PthreadCreateFn_nid_no_patch fn); +extern "C" void RegisterPthreadJoin_nid_no_patch(PthreadJoinFn_nid_no_patch fn); +extern "C" void RegisterPthreadDetach_nid_no_patch(PthreadDetachFn_nid_no_patch fn); +struct ThreadForwards_nid_no_patch { + int (APS5_VABI* once)(std::int32_t*, void (APS5_VABI*)(void)); + int (APS5_VABI* keyCreate)(int*, void (APS5_VABI*)(void*)); + int (APS5_VABI* keyDelete)(int); + void* (APS5_VABI* getSpecific)(int); + int (APS5_VABI* setSpecific)(int, void*); + int (APS5_VABI* equal)(void*, void*); + void* (APS5_VABI* self)(); + void (APS5_VABI* exit)(void*); +}; +extern "C" void RegisterThreadForwards_nid_no_patch(const ThreadForwards_nid_no_patch* forwards); + #define APS5_INVALID_ARG_EX throw std::invalid_argument(std::string(__func__) + ": invalid argument") #define APS5_DUMMY_FUN \ diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index ce42a0be61..7811725142 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -19,6 +19,7 @@ struct Range { std::size_t allocationBytes; bool releasable = true; bool gpu = false; + std::uint64_t backingSerial = 0; }; class Lease { @@ -53,16 +54,15 @@ 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); void GuestAllocationsRequireAvailable_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); bool GuestAllocationsCovers_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); bool GuestAllocationsOverlaps_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); +bool GuestAllocationsForRegistered_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, const std::function& visit); Range GuestAllocationsFind_nid_postfix(void* mutation, const void* pointer); void GuestAllocationsRemove_nid_postfix(void* mutation, const void* pointer); void GuestAllocationsProtect_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, bool readable, bool writable, bool gpu, const std::function& apply); @@ -94,15 +94,14 @@ 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); } bool Covers(const void* pointer, std::size_t bytes) const { return GuestAllocationsCovers_nid_postfix(handle, pointer, bytes); } bool Overlaps(const void* pointer, std::size_t bytes) const { return GuestAllocationsOverlaps_nid_postfix(handle, pointer, bytes); } + bool ForRegistered(const void* pointer, std::size_t bytes, const std::function& visit) const { return GuestAllocationsForRegistered_nid_postfix(handle, pointer, bytes, visit); } Range Find(const void* pointer) const { return GuestAllocationsFind_nid_postfix(handle, pointer); } void Remove(const void* pointer) { GuestAllocationsRemove_nid_postfix(handle, pointer); } void Unmap(const void* pointer, std::size_t bytes, const std::function& apply) { GuestAllocationsUnmap_nid_postfix(handle, pointer, bytes, apply); } diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index 3c96845e4d..bf0d756d40 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -8,6 +8,12 @@ // absolute address and breaks on host addresses outside that range. namespace GuestArena { +#ifdef ANYPS5_ENABLE_ASAN +inline constexpr std::uintptr_t GuestAddressOffset = 0x0000100000000000ull; +#else +inline constexpr std::uintptr_t GuestAddressOffset = 0; +#endif + #ifndef _WIN32 using SharedBackingResolver = bool (*)(std::uintptr_t address, std::size_t bytes, int* file, std::uint64_t* offset); using SharedBackingWriter = bool (*)(std::uintptr_t address, const void* source, std::size_t bytes); @@ -19,7 +25,9 @@ bool GuestArenaAvailable_nid_postfix(); bool GuestArenaContains_nid_postfix(const void* pointer, std::size_t bytes); void* GuestArenaAllocate_nid_postfix(std::size_t bytes, std::size_t alignment); void* GuestArenaAllocateAtOrAbove_nid_postfix(std::uintptr_t hint, std::size_t bytes, std::size_t alignment); +void* GuestArenaAllocateFromHint_nid_postfix(std::uintptr_t hint, std::size_t bytes, std::size_t alignment); std::uintptr_t GuestArenaHighestFree_nid_no_patch(std::size_t bytes, std::size_t alignment); +bool GuestArenaNextUsed_nid_no_patch(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end); void GuestArenaMarkUsed_nid_postfix(const void* pointer, std::size_t bytes); void GuestArenaRelease_nid_postfix(const void* pointer, std::size_t bytes); // The reserved range, and whether it was reserved with page write watching (Windows MEM_WRITE_WATCH). @@ -28,6 +36,7 @@ bool GuestArenaWriteWatched_nid_postfix(); void GuestArenaSetProtection_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uint32_t protection); void GuestArenaCommit_nid_postfix(void* pointer, std::size_t bytes, std::uint32_t protection, std::size_t granule); void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes); +void GuestArenaSetAccessObserver_nid_postfix(void (*callback)(std::uintptr_t address, std::size_t bytes)); #ifdef _WIN32 bool GuestArenaHandleWrite_nid_postfix(std::uintptr_t address); bool GuestArenaHandleRead_nid_postfix(std::uintptr_t address); @@ -42,10 +51,14 @@ std::uint64_t GuestArenaCommitGeneration_nid_postfix(); void GuestArenaSetPrivateMappingObserver_nid_postfix(void (*callback)(std::uintptr_t address, std::size_t bytes, std::uint64_t generation)); void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection); void GuestArenaMapDirect_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection); +void GuestArenaMapFile_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection, std::uint32_t viewProtection, std::size_t sectionBytes); void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes); void GuestArenaUnmapAlias_nid_postfix(void* alias); bool GuestArenaResidentSpan_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uintptr_t* start, std::uintptr_t* end); void GuestArenaForgetSection_nid_postfix(void* section); +bool GuestArenaBeginHostImport_nid_no_patch(void* pointer, std::size_t bytes); +void GuestArenaEndHostImport_nid_no_patch(void* pointer, std::size_t bytes); +bool GuestArenaForgetReusableSection_nid_no_patch(void* section); #else void GuestArenaSetSharedBacking_nid_postfix(SharedBackingResolver resolver); void GuestArenaSetSharedBackingWriter_nid_no_patch(SharedBackingWriter writer); @@ -54,6 +67,8 @@ bool GuestArenaSharedBacking_nid_postfix(std::uintptr_t address, std::size_t byt #endif bool GuestArenaBeginHostWrite_nid_postfix(void* pointer, std::size_t bytes); void GuestArenaEndHostWrite_nid_postfix(void* pointer, std::size_t bytes); +void GuestArenaSetHostReadHook_nid_no_patch(void (*hook)(std::uintptr_t address, std::size_t bytes)); +void GuestArenaPrepareHostRead_nid_no_patch(const void* pointer, std::size_t bytes); } @@ -73,6 +88,23 @@ private: bool open; }; +#ifdef _WIN32 +class HostImportAccess { +public: + HostImportAccess(void* pointer, std::size_t bytes) : pointer(pointer), bytes(bytes), open(GuestArenaBeginHostImport_nid_no_patch(pointer, bytes)) {} + ~HostImportAccess() { + if (open) GuestArenaEndHostImport_nid_no_patch(pointer, bytes); + } + HostImportAccess(const HostImportAccess&) = delete; + HostImportAccess& operator=(const HostImportAccess&) = delete; + +private: + void* pointer; + std::size_t bytes; + bool open; +}; +#endif + } #endif diff --git a/core/libs/prx/libc/include/GuestDirectory.hpp b/core/libs/prx/libc/include/GuestDirectory.hpp index 092289d03a..3f83ab1a18 100644 --- a/core/libs/prx/libc/include/GuestDirectory.hpp +++ b/core/libs/prx/libc/include/GuestDirectory.hpp @@ -14,4 +14,4 @@ struct GuestDirectoryEntry { static_assert(offsetof(GuestDirectoryEntry, name) == 8); static_assert(sizeof(GuestDirectoryEntry) == 264); -extern "C" void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int)); +extern "C" void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int), std::uint64_t position); diff --git a/core/libs/prx/libc/include/GuestHeap.hpp b/core/libs/prx/libc/include/GuestHeap.hpp index 24051f79c5..2b0f8cebb7 100644 --- a/core/libs/prx/libc/include/GuestHeap.hpp +++ b/core/libs/prx/libc/include/GuestHeap.hpp @@ -14,6 +14,7 @@ void* GuestHeapReallocate_nid_postfix(void* pointer, std::size_t bytes); void* GuestHeapAlign_nid_postfix(std::size_t alignment, std::size_t bytes); void* GuestHeapRealign_nid_postfix(void* pointer, std::size_t bytes, std::size_t alignment); void* GuestHeapAlignForGpu_nid_postfix(std::size_t alignment, std::size_t bytes); +int GuestHeapStatsFast_nid_postfix(void* stats); } 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/HostStandardStreams.hpp b/core/libs/prx/libc/include/HostStandardStreams.hpp new file mode 100644 index 0000000000..5c01f269cf --- /dev/null +++ b/core/libs/prx/libc/include/HostStandardStreams.hpp @@ -0,0 +1,28 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_HOSTSTANDARDSTREAMS_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_HOSTSTANDARDSTREAMS_HPP + +#if defined(__MINGW32__) && !defined(__USE_MINGW_ANSI_STDIO) +#define __USE_MINGW_ANSI_STDIO 1 +#endif +#ifdef __cplusplus +#include +#else +#include +#endif + +#ifdef _WIN32 +#undef stdout +#undef stderr +#ifdef __cplusplus +extern "C" { +#endif +FILE* HostStandardOutputFile_nid_no_patch(void); +FILE* HostStandardErrorFile_nid_no_patch(void); +#ifdef __cplusplus +} +#endif +#define stdout HostStandardOutputFile_nid_no_patch() +#define stderr HostStandardErrorFile_nid_no_patch() +#endif + +#endif diff --git a/core/libs/prx/libc/include/MallocManagedSize.hpp b/core/libs/prx/libc/include/MallocManagedSize.hpp new file mode 100644 index 0000000000..007a3ba2b2 --- /dev/null +++ b/core/libs/prx/libc/include/MallocManagedSize.hpp @@ -0,0 +1,18 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_MALLOCMANAGEDSIZE_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_MALLOCMANAGEDSIZE_HPP + +#include +#include + +struct MallocManagedSize { + std::uint16_t size; + std::uint16_t version; + std::uint32_t reserved; + std::size_t maxSystemSize; + std::size_t currentSystemSize; + std::size_t maxInuseSize; + std::size_t currentInuseSize; +}; +static_assert(sizeof(MallocManagedSize) == 0x28); + +#endif diff --git a/core/libs/prx/libc/include/PreciseWait.hpp b/core/libs/prx/libc/include/PreciseWait.hpp index 73249384bb..b187039495 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,257 @@ 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 std::uint64_t PreciseQpcFrequency() { + static std::atomic cachedFreq{0}; + std::uint64_t freq = cachedFreq.load(std::memory_order_relaxed); + if (freq == 0) [[unlikely]] { + LARGE_INTEGER f; + if (QueryPerformanceFrequency(&f) && f.QuadPart > 0) { + freq = static_cast(f.QuadPart); + cachedFreq.store(freq, std::memory_order_relaxed); + } + } + return freq; +} + +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; + const std::uint64_t freq = PreciseQpcFrequency(); + std::uint64_t deltaTicks = hundredNanos; + std::uint64_t pauseThresholdTicks = 2ULL; + if (freq != 10000000ULL) [[unlikely]] { + 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; +#if defined(__APPLE__) && defined(_LIBCPP_VERSION) && defined(CLOCK_MONOTONIC_RAW) + clock_gettime(CLOCK_MONOTONIC_RAW, &ts); +#else + clock_gettime(CLOCK_MONOTONIC, &ts); +#endif + 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 + if constexpr (std::is_same_v) { + if (PreciseQpcFrequency() == 10000000ULL) [[likely]] { + const auto rawDeadline = deadline.time_since_epoch().count(); + if (rawDeadline <= 0) [[unlikely]] { + return; + } + using HundredNanos = std::chrono::duration>; + std::uint64_t targetHns = 0; + if constexpr (std::is_same_v) { + targetHns = (static_cast(rawDeadline) + 99ULL) / 100ULL; + } else { + targetHns = static_cast( + std::chrono::ceil(deadline.time_since_epoch()).count()); + } + LARGE_INTEGER counter; + QueryPerformanceCounter(&counter); + const std::uint64_t nowTicks = static_cast(counter.QuadPart); + if (nowTicks < targetHns) { + PreciseSleepFromCounter(counter, targetHns - nowTicks); + } + return; + } + } + 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__) +#if (defined(__linux__) && (defined(__GLIBCXX__) || (defined(_LIBCPP_VERSION) && !defined(_LIBCPP_HAS_TIMESPEC_GET)))) || \ + (defined(__APPLE__) && defined(_LIBCPP_VERSION) && defined(CLOCK_MONOTONIC_RAW)) + if constexpr (std::is_same_v) { + const auto rawDeadline = deadline.time_since_epoch().count(); + if (rawDeadline <= 0) [[unlikely]] { + return; + } + std::uint64_t targetNanos = 0; + if constexpr (std::is_same_v) { + targetNanos = static_cast(rawDeadline); + } else { + targetNanos = static_cast( + std::chrono::ceil(deadline.time_since_epoch()).count()); + } + const std::uint64_t startNanos = PreciseMonotonicNanos(); + if (startNanos < targetNanos) { + PreciseSleepFromNanos(startNanos, targetNanos - startNanos); + } + return; + } +#endif + 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/RemoveTree.hpp b/core/libs/prx/libc/include/RemoveTree.hpp new file mode 100644 index 0000000000..ec5429a0aa --- /dev/null +++ b/core/libs/prx/libc/include/RemoveTree.hpp @@ -0,0 +1,62 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_REMOVETREE_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_REMOVETREE_HPP + +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif + +inline void RemoveTree(const std::filesystem::path& target, std::error_code& error) { + error.clear(); +#ifdef _WIN32 + const std::wstring wide = target.wstring(); + const DWORD attributes = GetFileAttributesW(wide.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES) { + const DWORD missing = GetLastError(); + if (missing != ERROR_FILE_NOT_FOUND && missing != ERROR_PATH_NOT_FOUND) + error = std::error_code(static_cast(missing), std::system_category()); + return; + } + const bool directory = (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0; + if (directory && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) { + std::error_code listing; + { + std::filesystem::directory_iterator entries(target, listing); + while (!listing && entries != std::filesystem::directory_iterator()) { + RemoveTree(entries->path(), error); + if (error) return; + entries.increment(listing); + } + } + if (listing) { + error = listing; + return; + } + } + if (directory ? RemoveDirectoryW(wide.c_str()) != 0 : DeleteFileW(wide.c_str()) != 0) return; + const DWORD failed = GetLastError(); + SetFileAttributesW(wide.c_str(), FILE_ATTRIBUTE_NORMAL); + if (directory ? RemoveDirectoryW(wide.c_str()) != 0 : DeleteFileW(wide.c_str()) != 0) return; + error = std::error_code(static_cast(failed), std::system_category()); +#else + std::error_code removal; + std::filesystem::remove_all(target, removal); + if (removal) error = removal; +#endif +} + +inline void RemoveTree(const std::filesystem::path& target) { + std::error_code error; + RemoveTree(target, error); + if (error) throw std::filesystem::filesystem_error("RemoveTree", target, error); +} + +#endif diff --git a/core/libs/prx/libc/include/RenameDot.hpp b/core/libs/prx/libc/include/RenameDot.hpp new file mode 100644 index 0000000000..a792ac7f32 --- /dev/null +++ b/core/libs/prx/libc/include/RenameDot.hpp @@ -0,0 +1,35 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_RENAMEDOT_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_RENAMEDOT_HPP + +#include +#include +#include "FilesystemError.hpp" + +inline std::string TrimTrailingSeparators_nid_no_patch(std::string path) { + const auto separator = [](char character) { return character == '/' || character == '\\'; }; + const std::size_t root = std::filesystem::path(path).root_path().native().size(); + while (path.size() > root && path.size() > 1 && separator(path.back())) path.pop_back(); + return path; +} + +inline int RenameDotError_nid_no_patch(const char* path) { + std::size_t length = std::char_traits::length(path); + const auto separator = [](char character) { return character == '/' || character == '\\'; }; + while (length > 1 && separator(path[length - 1])) --length; + std::size_t start = length; + while (start > 0 && !separator(path[start - 1])) --start; + const std::size_t name = length - start; + const bool dot = name == 1 && path[start] == '.'; + const bool dotDot = name == 2 && path[start] == '.' && path[start + 1] == '.'; + if (!dot && !dotDot) return 0; + const std::string parent = start == 0 ? std::string(".") : TrimTrailingSeparators_nid_no_patch(std::string(path, start)); + std::filesystem::path resolved; + if (const int error = ResolvePathError_nid_no_patch(parent.c_str(), resolved)) return error; + std::error_code status; + const auto type = std::filesystem::status(resolved, status).type(); + if (type == std::filesystem::file_type::not_found) return status == std::errc::not_a_directory ? 20 : 2; + if (status) return FilesystemError(status); + return type == std::filesystem::file_type::directory ? 22 : 20; +} + +#endif diff --git a/core/libs/prx/libc/include/ScanWideField.hpp b/core/libs/prx/libc/include/ScanWideField.hpp new file mode 100644 index 0000000000..f210ec5d5e --- /dev/null +++ b/core/libs/prx/libc/include/ScanWideField.hpp @@ -0,0 +1,15 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_SCANWIDEFIELD_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_SCANWIDEFIELD_HPP + +#include +#include + +namespace LibcDetail { + +std::size_t ReadWideScanCharacter(const char* text, char32_t* point); +bool InWideScanset(const std::string& specifier, char32_t point); +void RequireNoPositionalWideScan(const char* format); + +} + +#endif diff --git a/core/libs/prx/libc/include/Shutdown.hpp b/core/libs/prx/libc/include/Shutdown.hpp index a7618a0321..a59aa05ced 100644 --- a/core/libs/prx/libc/include/Shutdown.hpp +++ b/core/libs/prx/libc/include/Shutdown.hpp @@ -11,7 +11,12 @@ 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(); +extern "C" void LibcSetExitDrain_nid_no_patch(void (*drain)()); #endif diff --git a/core/libs/prx/libc/include/SonyCrt.hpp b/core/libs/prx/libc/include/SonyCrt.hpp index 39a655f51e..db53cd3d54 100644 --- a/core/libs/prx/libc/include/SonyCrt.hpp +++ b/core/libs/prx/libc/include/SonyCrt.hpp @@ -20,11 +20,9 @@ extern "C" GuestTm* APS5_VABI localtime_s_nid_postfix(const std::int64_t* timer, GuestTm* result); -namespace { +using SonyConstraintHandler = void (APS5_VABI *)(const char* message, void* pointer, int error); -using SonyConstraintHandler = void (*)(const char* message, void* pointer, int error); - -inline void SonyDefaultConstraintHandler(const char* message, void* pointer, int error) { +inline void APS5_VABI SonyDefaultConstraintHandler(const char* message, void* pointer, int error) { (void)message; (void)pointer; (void)error; @@ -41,6 +39,22 @@ inline SonyConstraintHandler& SonyConstraintHandlerSlot() { return handler; } +namespace { + +template +inline TInteger ParseInteger(const char* text, char** end, int base, TInteger (*parse)(const char*, char**, int)) { +#ifdef __APPLE__ + const int saved = errno; + char* parsedEnd = nullptr; + const TInteger value = parse(text, &parsedEnd, base); + if (parsedEnd == text && errno == EINVAL && (base == 0 || (base >= 2 && base <= 36))) errno = saved; + if (end != nullptr) *end = parsedEnd; + return value; +#else + return parse(text, end, base); +#endif +} + inline void SonyInvokeConstraintHandler(const char* message, void* pointer, int error) { SonyConstraintHandler handler = nullptr; { @@ -74,21 +88,39 @@ inline int SonyFormatAsctime(char* buffer, std::size_t size, const GuestTm& time static const char* const days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; static const char* const months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; if (!buffer || size < 26) return EINVAL; - if (time.tm_wday < 0 || time.tm_wday > 6 || time.tm_mon < 0 || time.tm_mon > 11) return EINVAL; + const int year = 1900 + time.tm_year; + if (time.tm_wday < 0 || time.tm_wday > 6 || time.tm_mon < 0 || time.tm_mon > 11 || + time.tm_mday < 1 || time.tm_mday > 31 || time.tm_hour < 0 || time.tm_hour > 23 || + time.tm_min < 0 || time.tm_min > 59 || time.tm_sec < 0 || time.tm_sec > 60 || + year < 0 || year > 9999) { + buffer[0] = '\0'; + return EINVAL; + } std::snprintf(buffer, size, "%.3s %.3s %2d %.2d:%.2d:%.2d %4d\n", days[time.tm_wday], months[time.tm_mon], - time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec, 1900 + time.tm_year); + time.tm_mday, time.tm_hour, time.tm_min, time.tm_sec, year); return 0; } inline int SonyCtimeS(char* buffer, std::size_t size, const std::int64_t* time) { - if (!buffer || size < 26 || !time) return EINVAL; + if (!buffer || size < 26) return EINVAL; + if (!time) { + buffer[0] = '\0'; + return EINVAL; + } GuestTm parts{}; - if (!localtime_s_nid_postfix(time, &parts)) return EINVAL; + if (!localtime_s_nid_postfix(time, &parts)) { + buffer[0] = '\0'; + return EINVAL; + } return SonyFormatAsctime(buffer, size, parts); } inline int SonyAsctimeS(char* buffer, std::size_t size, const GuestTm* time) { - if (!buffer || size < 26 || !time) return EINVAL; + if (!buffer || size < 26) return EINVAL; + if (!time) { + buffer[0] = '\0'; + return EINVAL; + } return SonyFormatAsctime(buffer, size, *time); } @@ -106,20 +138,38 @@ inline void* SonySetConstraintHandlerS(void* handler) { } inline int SonyWcscpyS(std::uint16_t* dest, std::size_t size, const std::uint16_t* src) { - if (!dest || !src || size == 0) { - SonyInvokeConstraintHandler("wcscpy_s", dest, EINVAL); - return EINVAL; + constexpr int GuestEinval = 22; + constexpr int GuestErange = 34; + constexpr std::size_t rsizeMax = (SIZE_MAX >> 1) / sizeof(std::uint16_t); + if (!dest || size == 0) { + SonyInvokeConstraintHandler("wcscpy_s", dest, GuestEinval); + return GuestEinval; } - std::size_t i = 0; - while (i + 1 < size && src[i] != 0) { - dest[i] = src[i]; - ++i; + if (size > rsizeMax) { + SonyInvokeConstraintHandler("wcscpy_s", dest, GuestErange); + return GuestErange; } - dest[i] = 0; - if (src[i] != 0) { - SonyInvokeConstraintHandler("wcscpy_s", dest, ERANGE); - return ERANGE; + if (!src) { + dest[0] = 0; + SonyInvokeConstraintHandler("wcscpy_s", dest, GuestEinval); + return GuestEinval; } + std::size_t length = 0; + while (length < size && src[length] != 0) ++length; + if (length >= size) { + dest[0] = 0; + SonyInvokeConstraintHandler("wcscpy_s", dest, GuestErange); + return GuestErange; + } + const auto destination = reinterpret_cast(dest); + const auto source = reinterpret_cast(src); + const auto distance = destination < source ? source - destination : destination - source; + if (length != 0 && distance < (length + 1) * sizeof(std::uint16_t)) { + dest[0] = 0; + SonyInvokeConstraintHandler("wcscpy_s", dest, GuestEinval); + return GuestEinval; + } + std::memcpy(dest, src, (length + 1) * sizeof(std::uint16_t)); return 0; } @@ -226,6 +276,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/WindowsDescriptor.hpp b/core/libs/prx/libc/include/WindowsDescriptor.hpp index 529011b509..66ebc1d9ac 100644 --- a/core/libs/prx/libc/include/WindowsDescriptor.hpp +++ b/core/libs/prx/libc/include/WindowsDescriptor.hpp @@ -18,15 +18,33 @@ static inline HANDLE WindowsDescriptorHandle_nid_no_patch(int descriptor) { return reinterpret_cast(handle); } -static inline int WindowsDescriptorAccess_nid_no_patch(HANDLE handle, int descriptor, const char* function) { +static inline ACCESS_MASK WindowsHandleAccess_nid_no_patch(HANDLE handle, int descriptor, const char* function) { using QueryObject = NTSTATUS(NTAPI*)(HANDLE, OBJECT_INFORMATION_CLASS, PVOID, ULONG, PULONG); static const auto query = reinterpret_cast(reinterpret_cast(::GetProcAddress(::GetModuleHandleW(L"ntdll.dll"), "NtQueryObject"))); PUBLIC_OBJECT_BASIC_INFORMATION information{}; const NTSTATUS status = query == nullptr ? -1 : query(handle, ObjectBasicInformation, &information, sizeof(information), nullptr); if (status < 0) throw std::runtime_error(std::string(function) + ": cannot read the access of descriptor " + std::to_string(descriptor) + ", status=" + std::to_string(status)); - const bool read = (information.GrantedAccess & FILE_READ_DATA) != 0; - const bool write = (information.GrantedAccess & (FILE_WRITE_DATA | FILE_APPEND_DATA)) != 0; + return information.GrantedAccess; +} + +static inline std::uint32_t WindowsFileNumber_nid_no_patch(std::uint64_t fileId) { + const std::uint32_t folded = static_cast(fileId) ^ (static_cast(fileId >> 32) * 0x9E3779B1u); + return folded == 0 ? 1 : folded; +} + +static inline std::uint32_t WindowsFileNumber_nid_no_patch(const BY_HANDLE_FILE_INFORMATION& identity) { + return WindowsFileNumber_nid_no_patch((static_cast(identity.nFileIndexHigh) << 32) | identity.nFileIndexLow); +} + +static inline bool WindowsHandleOnlyAppends_nid_no_patch(ACCESS_MASK access) { + return (access & (FILE_WRITE_DATA | FILE_APPEND_DATA)) == FILE_APPEND_DATA; +} + +static inline int WindowsDescriptorAccess_nid_no_patch(HANDLE handle, int descriptor, const char* function) { + const ACCESS_MASK access = WindowsHandleAccess_nid_no_patch(handle, descriptor, function); + const bool read = (access & FILE_READ_DATA) != 0; + const bool write = (access & (FILE_WRITE_DATA | FILE_APPEND_DATA)) != 0; if (read && write) return 2; if (write) return 1; if (read) return 0; diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index f3deb658df..0c8dd2861f 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -16,11 +16,17 @@ #include extern "C" int APS5_VABI strerror_r_nid_postfix(int error, char* buffer, std::size_t length); +extern "C" std::FILE* GuestStandardOutputFile_nid_no_patch(); namespace LibcDetail { enum class ArgumentClass : unsigned char { Integer, Double, LongDouble }; +class FormatConstraintViolation : public std::invalid_argument { +public: + using std::invalid_argument::invalid_argument; +}; + class FormatArguments { VaList args; std::vector> numbered; @@ -193,11 +199,10 @@ inline std::string FormatPointer(const std::string& spec, const void* pointer) { for (unsigned long long rest = value; rest != 0; rest >>= 4) digits.insert(digits.begin(), "0123456789abcdef"[rest & 15]); if (digits.empty() && !(hasPrecision && precision == 0)) digits = "0"; if (digits.size() < precision) digits.insert(0, precision - digits.size(), '0'); - const std::size_t used = digits.size() + 2; - const std::size_t padding = width > used ? width - used : 0; - if (left) return "0x" + digits + std::string(padding, ' '); - if (zero && !hasPrecision) return "0x" + std::string(padding, '0') + digits; - return std::string(padding, ' ') + "0x" + digits; + const std::size_t padding = width > digits.size() ? width - digits.size() : 0; + if (left) return digits + std::string(padding, ' '); + if (zero && !hasPrecision) return std::string(padding, '0') + digits; + return std::string(padding, ' ') + digits; } constexpr size_t maximumArgumentPosition = 99; @@ -251,7 +256,7 @@ inline std::vector NumberedArgumentClasses(const char* format) { return classes; } -inline int FormatWindows(char* buffer, size_t size, const char* format, const void* source, std::string* complete = nullptr) { +inline int FormatWindows(char* buffer, size_t size, const char* format, const void* source, std::string* complete = nullptr, bool secure = false) { if (!format || !source) throw std::invalid_argument("Null formatting argument"); const int savedError = errno; const char* const formatStart = format; @@ -327,6 +332,10 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo output.Value(spec + "ll" + conversion, value); } else if (std::strchr("ouxX", conversion)) { if (!integerLength) throw std::invalid_argument("Invalid integer length"); + for (size_t flag = 1; flag < spec.size() && std::strchr("-+ #0", spec[flag]);) { + if (spec[flag] == ' ' || spec[flag] == '+') spec.erase(flag, 1); + else ++flag; + } unsigned long long value; if (length.empty() || length == "h" || length == "hh") { value = args.Next(); @@ -359,10 +368,12 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo else output.Value(spec + 's', utf8.c_str()); } else if (conversion == 's' && (length.empty() || length == "h")) { const char* value = args.Next(); + if (!value && secure) throw FormatConstraintViolation("Null string argument"); if (!value) value = "(null)"; output.Value(spec + 's', value); } else if ((conversion == 's' && length == "l") || (conversion == 'S' && length.empty())) { const char16_t* value = args.Next(); + if (!value && secure) throw FormatConstraintViolation("Null string argument"); if (!value) value = u"(null)"; std::string utf8; while (utf8.size() < precisionLimit && *value) { @@ -376,13 +387,15 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo } } output.Value(spec + 's', utf8.c_str()); - } else if (conversion == 'p' && length.empty()) { + } else if (conversion == 'p' && (length.empty() || length == "l")) { const std::string text = FormatPointer(spec, args.Next()); output.Append(text.data(), text.size()); } else if (conversion == 'm') { char message[128]; strerror_r_nid_postfix(savedError, message, sizeof(message)); output.Value(spec + 's', static_cast(message)); + } else if (conversion == 'n' && secure) { + throw FormatConstraintViolation("Format count conversion"); } else if (conversion == 'n' && integerLength && spec == "%") { void* pointer = args.Next(); if (!pointer) throw std::invalid_argument("Null format count pointer"); @@ -401,7 +414,7 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo inline int PrintWindows(const char* format, const void* args) { std::string buffer; const int size = FormatWindows(nullptr, 0, format, args, &buffer); - if (std::fwrite(buffer.data(), 1, static_cast(size), stdout) != static_cast(size)) + if (std::fwrite(buffer.data(), 1, static_cast(size), GuestStandardOutputFile_nid_no_patch()) != static_cast(size)) throw std::runtime_error("Formatted output write failed"); return size; } diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 8df63459a7..624685e8a9 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -24,8 +24,8 @@ public: enum class Collection { All, Armed, Fresh }; static WindowsMappings& Get() { - static WindowsMappings mappings; - return mappings; + static auto* mappings = new WindowsMappings; + return *mappings; } void* Reserve(void* address, std::size_t bytes) { @@ -76,10 +76,12 @@ public: reset(reinterpret_cast(address), bytes); } - void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection, bool watched = false, bool resident = false) { + void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection, bool watched = false, bool resident = false, DWORD viewProtection = PAGE_EXECUTE_READWRITE, std::size_t sectionBytes = SIZE_MAX, std::vector>* createdPrivate = nullptr) { + if (sectionBytes < bytes && (resident || sectionBytes == 0 || bytes - sectionBytes >= pageBytes)) throw std::runtime_error("shared guest mapping extends a whole page past its section"); std::lock_guard lock(mutex); collectableRanges.clear(); auto cursor = reinterpret_cast(address); + if (createdPrivate != nullptr) createdPrivate->reserve(createdPrivate->size() + bytes / pageBytes); reset(cursor, bytes); if (resident) watchResident = watched; HANDLE duplicate = nullptr; @@ -110,6 +112,10 @@ public: while (done + runBytes < bytes && (base + runBytes) % chunkBytes != 0 && single(done + runBytes)) runBytes += pageBytes; split(base, runBytes); commitResident(base, runBytes); + if (createdPrivate != nullptr) { + if (!createdPrivate->empty() && createdPrivate->back().first + createdPrivate->back().second == base) createdPrivate->back().second += runBytes; + else createdPrivate->emplace_back(base, runBytes); + } residentRuns.emplace(base, Run{base + runBytes, {}, static_cast(runBytes >> 12)}); for (std::size_t at = 0; at < runBytes; at += pageBytes) { const auto slot = slotOf(done + at); @@ -134,11 +140,12 @@ public: for (const auto alias : aliases) { if (views.at(alias).resident) demote(alias); } + const auto viewBytes = std::min(pageBytes, sectionBytes - done); split(base, pageBytes); - mapShared(base, section, offset + done, protection); + mapShared(base, section, offset + done, protection, viewProtection, viewBytes); slot->stored = true; shared->aliases.push_back(base); - views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, slot}); + views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, slot, hostPages(viewBytes)}); remember(freshRanges, base, base + pageBytes); invalidate(*shared); done += pageBytes; @@ -163,7 +170,7 @@ public: for (const auto alias : page.aliases) { auto& view = views.at(alias); if (view.armed == 0) continue; - protectSubpages(alias, view.armed, view.protection, "pin shared guest page writable"); + protectSubpages(alias, view.armed, view.protection, "pin shared guest page writable", view.viewBytes); view.armed = 0; } invalidate(page); @@ -185,13 +192,17 @@ public: const auto found = views.find(base); if (found == views.end() || !writable(found->second.protection)) return false; auto& view = found->second; - if (view.resident) { + const auto written = subpagesOf(base, address, address + 1); + if (view.resident && (view.armed & written) == 0) { const auto memory = query(address); return memory.State == MEM_COMMIT && writable(memory.Protect & 0xffu); } - const auto written = subpagesOf(base, address, address + 1); + if (view.resident) { + resumeResidentWrites(base); + return true; + } invalidate(*view.page, written); - protectSubpages(base, written, view.protection, "resume shared memory write"); + protectSubpages(base, written, view.protection, "resume shared memory write", view.viewBytes); view.armed &= static_cast(~written); return true; } @@ -213,13 +224,17 @@ public: return true; } - void ForgetSection(HANDLE section) { + bool ForgetSection(HANDLE section) { std::lock_guard lock(mutex); const auto key = reinterpret_cast(section); - physical.erase(physical.lower_bound(std::make_pair(key, std::uint64_t{0})), physical.lower_bound(std::make_pair(key + 1, std::uint64_t{0}))); + const auto first = physical.lower_bound(std::make_pair(key, std::uint64_t{0})); + const auto last = physical.lower_bound(std::make_pair(key + 1, std::uint64_t{0})); + const bool blank = std::none_of(first, last, [](const auto& entry) { return entry.second->stored || !entry.second->page.expired(); }); + physical.erase(first, last); + return blank; } - bool BeginHostWrite(std::uintptr_t address, std::size_t bytes) { + bool BeginHostWrite(std::uintptr_t address, std::size_t bytes, bool store = true) { std::lock_guard lock(mutex); const auto first = views.lower_bound(address & ~(pageBytes - 1)); const auto end = address + bytes; @@ -228,23 +243,27 @@ public: } for (auto it = first; it != views.end() && it->first < end; ++it) { auto& view = it->second; - ++view.hostWrites; + if (store) ++view.hostWrites; + else ++view.hostImports; const auto written = subpagesOf(it->first, address, end); - invalidate(*view.page, written); - const auto opened = static_cast(view.armed & written); + if (store) invalidate(*view.page, written); + else if (view.resident) forgetClean(it->first, it->first + pageBytes); + const auto opened = static_cast((store || view.resident) ? view.armed & written : 0); if (opened == 0) continue; - protectSubpages(it->first, opened, view.protection, "open shared memory to a host write"); + protectSubpages(it->first, opened, view.protection, "open shared memory to a host write", view.viewBytes); view.armed &= static_cast(~opened); } return true; } - void EndHostWrite(std::uintptr_t address, std::size_t bytes) { + void EndHostWrite(std::uintptr_t address, std::size_t bytes, bool store = true) { std::lock_guard lock(mutex); const auto end = address + bytes; for (auto it = views.lower_bound(address & ~(pageBytes - 1)); it != views.end() && it->first < end; ++it) { - --it->second.hostWrites; - invalidate(*it->second.page, subpagesOf(it->first, address, end)); + if (store) --it->second.hostWrites; + else --it->second.hostImports; + if (store) invalidate(*it->second.page, subpagesOf(it->first, address, end)); + else if (it->second.resident) forgetClean(it->first, it->first + pageBytes); } } @@ -261,6 +280,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); @@ -276,6 +298,7 @@ public: for (std::size_t done = 0; done < bytes; done += pageBytes) { const auto page = views.find(address + done); if (page == views.end()) throw refuse("the range is not a run of shared views"); + if (page->second.viewBytes != pageBytes) throw refuse("a page extends past the end of its file"); if (page->second.offset != offset + done || (page->second.section != section && !sameSection(page->second.section->handle, section->handle))) contiguous = false; } if (!contiguous) return mapAliasRuns(address, bytes); @@ -334,6 +357,12 @@ public: } } else if (clear && !collectable(address, end)) return false; auto position = views.lower_bound(address & ~(pageBytes - 1)); + std::uintptr_t cleanStart = 0; + std::uintptr_t cleanEnd = 0; + const auto flushClean = [&] { + if (cleanEnd > cleanStart) rememberClean(cleanStart, cleanEnd); + cleanStart = cleanEnd = 0; + }; for (auto cursor = address; cursor < end;) { const auto nextClean = cleanRanges.upper_bound(cursor); if (nextClean != cleanRanges.begin()) { @@ -346,11 +375,12 @@ public: const auto base = cursor & ~(pageBytes - 1); const auto found = viewAt(base, position); if (found != views.end() && found->second.resident) { + flushClean(); if (!watchResident) return false; const auto available = capacity - *count; if (available == 0) return true; const auto run = std::prev(residentRuns.upper_bound(cursor)); - const auto stop = std::min({end, run->second.end, (cursor & ~std::uintptr_t{4095}) + available * 4096}); + const auto stop = std::min({end, run->second.end, (cursor & ~std::uintptr_t{4095}) + available * 4096, nextClean == cleanRanges.end() ? end : nextClean->first}); collectResident(run->first, run->second, cursor, stop, pages, count, clear, collection); if (*count == capacity) return true; cursor = stop; @@ -371,7 +401,10 @@ public: } } } - if (view.protection == PAGE_NOACCESS) return false; + if (view.protection == PAGE_NOACCESS) { + flushClean(); + return false; + } const bool pinned = view.page->pins != 0; std::uint8_t settled = 0; auto settledEnd = cursor; @@ -410,28 +443,29 @@ public: continue; } const DWORD protection = other.protection == PAGE_EXECUTE_READWRITE ? PAGE_EXECUTE_READ : PAGE_READONLY; - protectSubpages(alias, arming, protection, "arm shared memory write tracking"); + protectSubpages(alias, arming, protection, "arm shared memory write tracking", other.viewBytes); other.armed |= arming; } const auto tracked = static_cast(settled & ~untracked); - auto cleanFrom = cursor; - auto cleanTo = cursor; for (std::size_t sub = 0; sub < subpages; ++sub) { if ((tracked & (1u << sub)) == 0) continue; const auto from = std::max(cursor, base + sub * subpageBytes); const auto to = std::min(settledEnd, base + (sub + 1) * subpageBytes); if (from == base + sub * subpageBytes && to == base + (sub + 1) * subpageBytes) view.seen[sub] = view.page->generation[sub]; - if (from != cleanTo) { - if (cleanFrom != cleanTo) rememberClean(cleanFrom, cleanTo); - cleanFrom = from; + if (from != cleanEnd) { + flushClean(); + cleanStart = from; } - cleanTo = to; + cleanEnd = to; } - if (cleanFrom != cleanTo) rememberClean(cleanFrom, cleanTo); } - if (full) return true; + if (full) { + flushClean(); + return true; + } cursor = stop; } else { + flushClean(); if (collection == Collection::Fresh) { const auto next = views.lower_bound(cursor); cursor = next == views.end() ? end : std::min(end, next->first); @@ -450,6 +484,7 @@ public: cursor = stop; } } + flushClean(); return true; } @@ -496,11 +531,24 @@ private: ~Section() { CloseHandle(handle); } }; struct Saved { - unsigned char* bytes; - explicit Saved(unsigned char* bytes) : bytes(bytes) {} + unsigned char* bytes = nullptr; + std::size_t length; + std::array committed{}; + explicit Saved(std::size_t length) : length(length) { + bytes = static_cast(VirtualAlloc(nullptr, length, MEM_RESERVE, PAGE_READWRITE)); + if (bytes == nullptr) fail("reserve unmapped direct memory"); + } Saved(const Saved&) = delete; Saved& operator=(const Saved&) = delete; ~Saved() { VirtualFree(bytes, 0, MEM_RELEASE); } + + void Commit(std::size_t offset) { + const auto page = offset / pageBytes; + const auto bit = std::uint64_t{1} << (page & 63); + if ((committed[page >> 6] & bit) != 0) return; + if (!VirtualAlloc(bytes + offset, pageBytes, MEM_COMMIT, PAGE_READWRITE)) fail("keep unmapped direct memory"); + committed[page >> 6] |= bit; + } }; struct Physical { std::weak_ptr page; @@ -519,11 +567,14 @@ private: std::uint8_t collected = 0; bool resident = false; std::shared_ptr slot; + std::size_t viewBytes = pageBytes; + std::uint32_t hostImports = 0; }; struct Run { std::uintptr_t end; std::array collected; std::uint32_t fresh; + bool writeObserved = false; }; struct Window { void* view = nullptr; @@ -537,6 +588,16 @@ private: void collectResident(std::uintptr_t start, Run& run, std::uintptr_t from, std::uintptr_t stop, void** pages, std::size_t* count, bool clear, Collection collection) { const auto first = from & ~std::uintptr_t{4095}; + const bool armed = clear && run.fresh == 0 && !run.writeObserved && collection != Collection::Fresh && run.end - start >= 0x40000 && armResident(first, stop); + if (run.fresh == 0 && collection != Collection::Fresh) { + ULONG_PTR reported = (stop - first + 4095) / 4096; + DWORD granularity = 0; + if (GetWriteWatch(clear ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast(first), stop - first, pages + *count, &reported, &granularity) != 0) fail("collect resident guest writes"); + for (ULONG_PTR index = 0; index < reported; ++index) pages[*count + index] = reinterpret_cast(std::max(from, reinterpret_cast(pages[*count + index]))); + *count += reported; + if (armed) rememberClean(from, stop); + return; + } std::vector written; if (collection != Collection::Fresh) { std::vector found((stop - first + 4095) / 4096); @@ -572,6 +633,44 @@ private: if (collection == Collection::Fresh) flushReset(stop); } + void resumeResidentWrites(std::uintptr_t address) { + const auto run = std::prev(residentRuns.upper_bound(address)); + run->second.writeObserved = true; + forgetClean(run->first, run->second.end); + auto first = views.lower_bound(run->first); + while (first != views.end() && first->first < run->second.end) { + const auto begin = first->first; + auto end = begin; + const auto protection = first->second.protection; + auto next = first; + bool armed = false; + do { + armed = armed || next->second.armed != 0; + end = next->first + pageBytes; + ++next; + } while (next != views.end() && next->first == end && end < run->second.end && next->second.protection == protection); + if (armed) { + DWORD previous; + if (!VirtualProtect(reinterpret_cast(begin), end - begin, protection, &previous)) fail("resume resident guest writes"); + } + for (; first != next; ++first) first->second.armed = 0; + } + } + + bool armResident(std::uintptr_t from, std::uintptr_t stop) { + const auto first = views.lower_bound(from & ~(pageBytes - 1)); + if (first == views.end() || !writable(first->second.protection)) return false; + const auto protection = first->second.protection; + for (auto it = first; it != views.end() && it->first < stop; ++it) { + if (!it->second.resident || it->second.protection != protection || it->second.hostWrites != 0 || it->second.hostImports != 0 || it->second.page->pins != 0) return false; + } + DWORD previous; + const auto readOnly = protection == PAGE_EXECUTE_READWRITE ? PAGE_EXECUTE_READ : PAGE_READONLY; + if (!VirtualProtect(reinterpret_cast(from), stop - from, readOnly, &previous)) fail("arm resident memory write tracking"); + for (auto it = first; it != views.end() && it->first < stop; ++it) it->second.armed |= subpagesOf(it->first, from, stop); + return true; + } + Window open(HANDLE section, std::uint64_t offset, std::size_t bytes) { const auto lead = offset % granularity; Window window; @@ -591,12 +690,22 @@ private: if (!allocate(GetCurrentProcess(), reinterpret_cast(base), bytes, flags, PAGE_READWRITE, nullptr, 0)) fail("replace guest placeholder with resident direct memory"); } - void mapShared(std::uintptr_t base, HANDLE section, std::uint64_t offset, DWORD protection) { + static std::size_t hostPages(std::size_t bytes) { + return (bytes + subpageBytes - 1) & ~(subpageBytes - 1); + } + + void mapShared(std::uintptr_t base, HANDLE section, std::uint64_t offset, DWORD protection, DWORD viewProtection = PAGE_EXECUTE_READWRITE, std::size_t sectionBytes = pageBytes) { void* page = reinterpret_cast(base); - if (!map(section, GetCurrentProcess(), page, offset, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map shared guest page"); - if (query(base).State == MEM_RESERVE && !VirtualAlloc(page, pageBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); + const auto viewBytes = hostPages(sectionBytes); + if (viewBytes != pageBytes) split(base, viewBytes); + if (!map(section, GetCurrentProcess(), page, offset, sectionBytes, MEM_REPLACE_PLACEHOLDER, viewProtection, nullptr, 0)) fail("map shared guest page"); + if (query(base).State == MEM_RESERVE && !VirtualAlloc(page, viewBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); DWORD previous; - if (!VirtualProtect(page, pageBytes, protection, &previous)) fail("protect shared guest page"); + if (!VirtualProtect(page, viewBytes, protection, &previous)) fail("protect shared guest page"); + if (viewBytes == pageBytes) return; + void* zeros = reinterpret_cast(base + viewBytes); + if (!allocate(GetCurrentProcess(), zeros, pageBytes - viewBytes, MEM_RESERVE | MEM_COMMIT | MEM_REPLACE_PLACEHOLDER, PAGE_READWRITE, nullptr, 0)) fail("replace guest placeholder past the end of a file with zero pages"); + if (!VirtualProtect(zeros, pageBytes - viewBytes, protection, &previous)) fail("protect guest pages past the end of a file"); } static bool blank(std::uintptr_t page) { @@ -653,12 +762,8 @@ private: const auto found = views.find(page); auto& view = found->second; if (!blank(page)) { - if (!saved) { - auto* bytes = static_cast(VirtualAlloc(nullptr, stop - start, MEM_RESERVE, PAGE_READWRITE)); - if (bytes == nullptr) fail("reserve unmapped direct memory"); - saved = std::make_shared(bytes); - } - if (!VirtualAlloc(saved->bytes + (page - start), pageBytes, MEM_COMMIT, PAGE_READWRITE)) fail("keep unmapped direct memory"); + if (!saved) saved = savedMemory(stop - start); + saved->Commit(page - start); std::memcpy(saved->bytes + (page - start), reinterpret_cast(page), pageBytes); view.slot->saved = saved; view.slot->savedOffset = page - start; @@ -670,6 +775,28 @@ private: residentRuns.erase(run); } + std::shared_ptr savedMemory(std::size_t bytes) { + for (const auto& saved : savedPool) { + if (saved.use_count() == 1 && saved->length == bytes) return saved; + } + constexpr std::size_t limit = 32u * 1024u * 1024u; + const auto reserved = (bytes + granularity - 1) / granularity * granularity; + for (auto it = savedPool.begin(); reserved <= limit && savedPoolBytes > limit - reserved && it != savedPool.end();) { + if (it->use_count() != 1) { + ++it; + continue; + } + savedPoolBytes -= ((*it)->length + granularity - 1) / granularity * granularity; + it = savedPool.erase(it); + } + auto saved = std::make_shared(bytes); + if (reserved <= limit && savedPoolBytes <= limit - reserved) { + savedPool.push_back(saved); + savedPoolBytes += reserved; + } + return saved; + } + static void forget(Ranges& ranges, std::uintptr_t start, std::uintptr_t end) { auto it = ranges.lower_bound(start); if (it != ranges.begin() && std::prev(it)->second > start) --it; @@ -718,14 +845,14 @@ private: return static_cast(((1u << (last + 1)) - 1) & ~((1u << first) - 1)); } - static void protectSubpages(std::uintptr_t base, std::uint8_t mask, DWORD protection, const char* operation) { + static void protectSubpages(std::uintptr_t base, std::uint8_t mask, DWORD protection, const char* operation, std::size_t viewBytes = pageBytes) { for (std::size_t sub = 0; sub < subpages;) { if ((mask & (1u << sub)) == 0) { ++sub; continue; } - auto stop = sub; - while (stop < subpages && (mask & (1u << stop)) != 0) ++stop; + auto stop = sub + 1; + while (stop < subpages && (mask & (1u << stop)) != 0 && stop * subpageBytes != viewBytes) ++stop; DWORD previous; if (!VirtualProtect(reinterpret_cast(base + sub * subpageBytes), (stop - sub) * subpageBytes, protection, &previous)) fail(operation); sub = stop; @@ -786,6 +913,21 @@ private: forgetClean(address, end); breakRun(address); breakRun(end); + if (const auto run = residentRuns.find(address); run != residentRuns.end() && run->second.end == end) { + const auto memory = query(address); + if (reinterpret_cast(memory.AllocationBase) != address) throw std::runtime_error("cannot release part of a host allocation"); + if (memory.Type != MEM_PRIVATE) throw std::runtime_error("resident guest mapping is not private memory"); + auto allocationEnd = address; + do { + const auto part = query(allocationEnd); + if (part.AllocationBase != memory.AllocationBase) break; + allocationEnd = reinterpret_cast(part.BaseAddress) + part.RegionSize; + } while (allocationEnd < end); + if (allocationEnd != end || query(end).AllocationBase == memory.AllocationBase) throw std::runtime_error("guest release truncates a host allocation"); + evict(run); + if (!VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory"); + return; + } for (auto cursor = address; cursor < end;) { const auto memory = query(cursor); if (memory.State == MEM_RESERVE) { @@ -876,6 +1018,8 @@ private: } std::map, std::shared_ptr> physical; std::map residentRuns; + std::vector> savedPool; + std::size_t savedPoolBytes = 0; std::map splitAliases; std::size_t granularity = 0x10000; bool watchResident = false; diff --git a/core/libs/prx/libc/include/exceptions/Runtime.hpp b/core/libs/prx/libc/include/exceptions/Runtime.hpp index ae0e5ee94d..e5d1b8200c 100644 --- a/core/libs/prx/libc/include/exceptions/Runtime.hpp +++ b/core/libs/prx/libc/include/exceptions/Runtime.hpp @@ -33,6 +33,7 @@ struct Globals { Header* caught {}; unsigned uncaught {}; }; inline thread_local Globals globals; inline constexpr std::uint64_t PrimaryClass = 0x414e5950432b2b00; inline constexpr std::uint64_t DependentClass = PrimaryClass | 1; +inline constexpr std::uint64_t GnuClass = 0x474e5543432b2b00; inline Header* FromUnwind(_Unwind_Exception* e) { return reinterpret_cast(reinterpret_cast(e) - offsetof(Header, unwind)); @@ -42,6 +43,7 @@ inline Allocation* AllocationOf(Header* header) { return reinterpret_cast(reinterpret_cast(header) - offsetof(Allocation, header)); } inline bool Native(std::uint64_t value) { return (value & ~std::uint64_t(1)) == PrimaryClass; } +inline bool Gnu(std::uint64_t value) { return (value & ~std::uint64_t(1)) == GnuClass; } inline Header* Primary(Header* header) { return header->unwind.exception_class == DependentClass ? FromObject(reinterpret_cast(header->type)) : header; } diff --git a/core/libs/prx/libc/include/general/AtomicSharedPtr.hpp b/core/libs/prx/libc/include/general/AtomicSharedPtr.hpp new file mode 100644 index 0000000000..30054c527e --- /dev/null +++ b/core/libs/prx/libc/include/general/AtomicSharedPtr.hpp @@ -0,0 +1,50 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_ATOMICSHAREDPTR_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_ATOMICSHAREDPTR_HPP + +#include +#include +#include +#include + +#if defined(__cpp_lib_atomic_shared_ptr) +template +using AtomicSharedPtr = std::atomic>; +#else +template +class AtomicSharedPtr { +public: + AtomicSharedPtr() noexcept = default; + AtomicSharedPtr(std::shared_ptr desired) noexcept : value(std::move(desired)) {} + AtomicSharedPtr(const AtomicSharedPtr&) = delete; + AtomicSharedPtr& operator=(const AtomicSharedPtr&) = delete; + + std::shared_ptr load(std::memory_order = std::memory_order_seq_cst) const noexcept { + std::lock_guard lock(mutex); + return value; + } + void store(std::shared_ptr desired, std::memory_order order = std::memory_order_seq_cst) noexcept { + exchange(std::move(desired), order); + } + std::shared_ptr exchange(std::shared_ptr desired, std::memory_order = std::memory_order_seq_cst) noexcept { + std::lock_guard lock(mutex); + return std::exchange(value, std::move(desired)); + } + bool compare_exchange_strong(std::shared_ptr& expected, std::shared_ptr desired, std::memory_order = std::memory_order_seq_cst) noexcept { + std::lock_guard lock(mutex); + if (value == expected) { + value = std::move(desired); + return true; + } + expected = value; + return false; + } + operator std::shared_ptr() const noexcept { return load(); } + void operator=(std::shared_ptr desired) noexcept { store(std::move(desired)); } + +private: + mutable std::mutex mutex; + std::shared_ptr value; +}; +#endif + +#endif diff --git a/core/libs/prx/libc/include/general/LogMacros.hpp b/core/libs/prx/libc/include/general/LogMacros.hpp index c10306ecc4..00e511b24b 100644 --- a/core/libs/prx/libc/include/general/LogMacros.hpp +++ b/core/libs/prx/libc/include/general/LogMacros.hpp @@ -1,11 +1,33 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_LOGMACROS_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_LOGMACROS_HPP +#include +#include #include #include +#include #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 @@ -13,20 +35,32 @@ #endif inline int WriteLogLine(std::FILE* stream, const char* format, ...) { char small[1024]; + std::size_t prefixLength = 0; + 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); + } std::va_list arguments; va_start(arguments, format); std::va_list again; va_copy(again, arguments); - const int length = std::vsnprintf(small, sizeof(small), format, arguments); + const int length = std::vsnprintf(small + prefixLength, sizeof(small) - prefixLength, format, arguments); va_end(arguments); std::string large; - if (length >= static_cast(sizeof(small))) { - large.resize(static_cast(length)); - std::vsnprintf(large.data(), large.size() + 1, format, again); + const auto remaining = sizeof(small) - prefixLength; + if (length >= static_cast(remaining)) { + large.resize(prefixLength + static_cast(length)); + if (prefixLength != 0) std::memcpy(large.data(), small, prefixLength); + std::vsnprintf(large.data() + prefixLength, large.size() - prefixLength + 1, format, again); } va_end(again); if (length < 0) return length; - std::fwrite(large.empty() ? small : large.data(), 1, static_cast(length), stream); + const auto totalLength = prefixLength + static_cast(length); + 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/include/general/ThreadName.hpp b/core/libs/prx/libc/include/general/ThreadName.hpp new file mode 100644 index 0000000000..df1ff39530 --- /dev/null +++ b/core/libs/prx/libc/include/general/ThreadName.hpp @@ -0,0 +1,41 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_THREADNAME_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_THREADNAME_HPP + +#ifdef _WIN32 +#include +#include +#include + +namespace ThreadName { + +template +inline TFunction Resolve(const char* name) { + TFunction function = nullptr; + const auto kernel = GetModuleHandleW(L"Kernel32.dll"); + if (kernel == nullptr) return function; + const FARPROC address = GetProcAddress(kernel, name); + static_assert(sizeof(function) == sizeof(address)); + std::memcpy(&function, &address, sizeof(function)); + return function; +} + +inline HRESULT SetCurrent(const char* name) { + if (name == nullptr) return E_INVALIDARG; + wchar_t wideName[64]; + std::size_t length = 0; + while (name[length] != '\0' && length < (sizeof(wideName) / sizeof(wideName[0])) - 1) { + wideName[length] = static_cast(name[length]); + ++length; + } + if (name[length] != '\0') return E_INVALIDARG; + wideName[length] = L'\0'; + using SetThreadDescriptionFunction = HRESULT(WINAPI*)(HANDLE, PCWSTR); + const auto setThreadDescription = Resolve("SetThreadDescription"); + if (setThreadDescription == nullptr) return S_FALSE; + return setThreadDescription(GetCurrentThread(), wideName); +} + +} +#endif + +#endif diff --git a/core/libs/prx/libc/include/specifics/itanium/UnwindAbi.hpp b/core/libs/prx/libc/include/specifics/itanium/UnwindAbi.hpp index 4dc8ad4daa..7bead3b20c 100644 --- a/core/libs/prx/libc/include/specifics/itanium/UnwindAbi.hpp +++ b/core/libs/prx/libc/include/specifics/itanium/UnwindAbi.hpp @@ -3,6 +3,8 @@ #include +#include "prx/libc/include/general/VabiMacros.hpp" + using _Unwind_Word = std::uintptr_t; using _Unwind_Ptr = std::uintptr_t; using _Unwind_Reason_Code = int; @@ -26,14 +28,14 @@ static constexpr _Unwind_Action _UA_END_OF_STACK = 16; struct _Unwind_Exception { std::uint64_t exception_class; - void (*exception_cleanup)(_Unwind_Reason_Code, _Unwind_Exception*); + void (APS5_VABI *exception_cleanup)(_Unwind_Reason_Code, _Unwind_Exception*); std::uintptr_t private_1; std::uintptr_t private_2; }; struct _Unwind_Context; -using _Unwind_Trace_Fn = _Unwind_Reason_Code (*)(_Unwind_Context*, void*); -using _Unwind_Stop_Fn = _Unwind_Reason_Code (*)(int, _Unwind_Action, std::uint64_t, _Unwind_Exception*, _Unwind_Context*, void*); +using _Unwind_Trace_Fn = _Unwind_Reason_Code (APS5_VABI *)(_Unwind_Context*, void*); +using _Unwind_Stop_Fn = _Unwind_Reason_Code (APS5_VABI *)(int, _Unwind_Action, std::uint64_t, _Unwind_Exception*, _Unwind_Context*, void*); #endif diff --git a/core/libs/prx/libc/src/AllocatingStrings.cpp b/core/libs/prx/libc/src/AllocatingStrings.cpp index 2e91cc2159..1605fbd125 100644 --- a/core/libs/prx/libc/src/AllocatingStrings.cpp +++ b/core/libs/prx/libc/src/AllocatingStrings.cpp @@ -11,6 +11,7 @@ extern "C" { int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaList* args); char* APS5_VABI strndup_nid_postfix(const char* source, std::size_t limit) { + if (!source) { errno = 22; return nullptr; } std::size_t length = 0; while (length < limit && source[length] != '\0') ++length; try { diff --git a/core/libs/prx/libc/src/AppArchive.cpp b/core/libs/prx/libc/src/AppArchive.cpp new file mode 100644 index 0000000000..ba32feae07 --- /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 auto* archive = new 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 e619429eee..84a8f46eb7 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -51,10 +51,13 @@ int APS5_VABI defaultPosixAlign(void** pointer, std::size_t alignment, std::size catch (const std::length_error&) { return 12; } } +int APS5_VABI defaultStatsFast(void* stats) { return GuestHeap::GuestHeapStatsFast_nid_postfix(stats); } + std::array defaultApi() { return {reinterpret_cast(defaultAllocate), reinterpret_cast(defaultFree), reinterpret_cast(defaultCalloc), reinterpret_cast(defaultReallocate), - reinterpret_cast(defaultAlign), nullptr, reinterpret_cast(defaultPosixAlign)}; + reinterpret_cast(defaultAlign), nullptr, reinterpret_cast(defaultPosixAlign), nullptr, + reinterpret_cast(defaultStatsFast)}; } class CallbackScope { @@ -76,9 +79,17 @@ TValue read(const void* pointer, std::size_t offset) { return value; } +void ensureInitialized(std::unique_lock& lock) { + if (heapApi[0] != nullptr || heapFailure || heapFinalized) return; + lock.unlock(); + ApplicationHeapInitialize_nid_no_patch(ApplicationProcessParameters_nid_no_patch()); + lock.lock(); +} + template TCallback callback(std::size_t index) { - std::lock_guard lock(heapMutex); + std::unique_lock lock(heapMutex); + ensureInitialized(lock); if (heapFailure) std::rethrow_exception(heapFailure); if (heapFinalized) throw std::runtime_error("application heap: allocator has been finalized"); if (heapApi[index] == nullptr) throw std::runtime_error("application heap: allocator API is not registered"); @@ -146,6 +157,11 @@ void ApplicationHeapInitialize_nid_no_patch(const void* processParameters) { if (read(replacement, 0) != 0x78 || read(replacement, 8) != 2) throw std::runtime_error("application heap: unsupported allocator replacement table"); std::array api; std::memcpy(api.data(), static_cast(replacement) + 0x20, sizeof(api)); + const bool replaced = std::any_of(api.begin(), api.end(), [](const void* entry) { return entry != nullptr; }); + if (replaced && ApplicationGuestHeapApi_nid_no_patch(api.data())) { + ApplicationHeapRegister_nid_no_patch(api.data()); + return; + } ApplicationHeapRegister_nid_no_patch(api.data()); const auto initialize = read(replacement, 0x10); if (initialize != nullptr) initialize(); @@ -194,6 +210,22 @@ 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 GuestHeap::GuestHeapAlign_nid_postfix(alignment, bytes); + } catch (const std::bad_alloc&) { + return nullptr; + } catch (const std::length_error&) { + return nullptr; + } +} + +extern "C" void APS5_VABI scriptingFreeMem_nid_postfix(void* pointer) { + GuestHeap::GuestHeapFree_nid_postfix(pointer); +} + void* ApplicationHeapRealign_nid_no_patch(void* pointer, std::size_t bytes, std::size_t alignment) { if (bytes == 0) { ApplicationHeapFree_nid_no_patch(pointer); @@ -202,7 +234,8 @@ void* ApplicationHeapRealign_nid_no_patch(void* pointer, std::size_t bytes, std: requireAlignment(alignment); Realign realign; { - std::lock_guard lock(heapMutex); + std::unique_lock lock(heapMutex); + ensureInitialized(lock); if (heapFailure) std::rethrow_exception(heapFailure); if (heapFinalized) throw std::runtime_error("application heap: allocator has been finalized"); if (heapApi[0] == nullptr) throw std::runtime_error("application heap: allocator API is not registered"); @@ -240,7 +273,8 @@ int ApplicationHeapPosixAlign_nid_no_patch(void** pointer, std::size_t alignment int ApplicationHeapStatsFast_nid_no_patch(void* stats) { Stats statsFast; { - std::lock_guard lock(heapMutex); + std::unique_lock lock(heapMutex); + ensureInitialized(lock); if (heapFailure) std::rethrow_exception(heapFailure); if (heapFinalized) throw std::runtime_error("application heap: allocator has been finalized"); if (heapApi[0] == nullptr) throw std::runtime_error("application heap: allocator API is not registered"); @@ -254,7 +288,8 @@ int ApplicationHeapStatsFast_nid_no_patch(void* stats) { std::size_t ApplicationHeapUsableSize_nid_no_patch(void* pointer) { UsableSize usableSize = nullptr; { - std::lock_guard lock(heapMutex); + std::unique_lock lock(heapMutex); + ensureInitialized(lock); if (heapFailure) std::rethrow_exception(heapFailure); if (heapFinalized) throw std::runtime_error("application heap: allocator has been finalized"); if (heapApi[0] == nullptr) throw std::runtime_error("application heap: allocator API is not registered"); diff --git a/core/libs/prx/libc/src/ApplicationProcessParameters.cpp b/core/libs/prx/libc/src/ApplicationProcessParameters.cpp index ecf3d973b9..5efa1f00d3 100644 --- a/core/libs/prx/libc/src/ApplicationProcessParameters.cpp +++ b/core/libs/prx/libc/src/ApplicationProcessParameters.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #ifdef _WIN32 #ifndef NOMINMAX @@ -11,8 +12,46 @@ #include #elif defined(__linux__) #include "prx/libc/include/specifics/linux/ElfTypes.hpp" +#include #endif +bool ApplicationGuestHeapApi_nid_no_patch(void** api) { +#ifdef __linux__ + static const auto guest = [] { + constexpr std::array names{ + "gQX+4GDQjpM#guest", "tIhsqj0qsFE#guest", "2X5agFjKxMc#guest", "Y7aJ1uydPMo#guest", + "Ujf3KzMvRmI#guest", "OGybVuPAhAY#guest", "cVSk9y8URbc#guest", "CC-BLMBu9-I#guest", + "KuOuD58hqn4#guest", "NDcSfcYZRC8#guest" + }; + std::array result{}; + bool found = false; + for (std::size_t index = 0; index < names.size(); ++index) { + result[index] = ::dlsym(RTLD_DEFAULT, names[index]); + found |= result[index] != nullptr; + } + if (!found) return result; + if (result[0] == nullptr) throw std::runtime_error("application heap: incomplete guest allocator API"); + Dl_info owner{}; + if (!::dladdr(result[0], &owner) || owner.dli_fbase == nullptr || owner.dli_fname == nullptr) + throw std::runtime_error("application heap: cannot identify guest allocator owner"); + for (std::size_t index = 1; index < names.size(); ++index) { + Dl_info info{}; + if (result[index] == nullptr || !::dladdr(result[index], &info) || info.dli_fbase != owner.dli_fbase) + throw std::runtime_error("application heap: incomplete or mixed guest allocator API"); + } + if (::dlopen(owner.dli_fname, RTLD_LAZY | RTLD_NOLOAD) == nullptr) + throw std::runtime_error("application heap: cannot retain guest allocator owner"); + return result; + }(); + if (guest[0] == nullptr) return false; + std::memcpy(api, guest.data(), sizeof(guest)); + return true; +#else + (void)api; + return false; +#endif +} + const void* ApplicationProcessParameters_nid_no_patch() { #ifdef _WIN32 const auto* image = reinterpret_cast(GetModuleHandleW(nullptr)); diff --git a/core/libs/prx/libc/src/AtomicBuiltins.cpp b/core/libs/prx/libc/src/AtomicBuiltins.cpp new file mode 100644 index 0000000000..93df860262 --- /dev/null +++ b/core/libs/prx/libc/src/AtomicBuiltins.cpp @@ -0,0 +1,95 @@ +#include +#include +#include +#include + +#include "prx/libc/include/General.hpp" + +namespace { + +using Uint128 = unsigned __int128; + +[[noreturn]] void ConsoleTrap(const char* function) { + throw std::runtime_error(std::string(function) + ": the console implementation traps (ud2)"); +} + +bool CompareExchange16(Uint128* target, Uint128& expected, Uint128 desired) { + std::uint64_t low = static_cast(expected); + std::uint64_t high = static_cast(expected >> 64); + bool exchanged; + asm volatile("lock cmpxchg16b %[target]" + : [target] "+m"(*target), "+a"(low), "+d"(high), "=@ccz"(exchanged) + : "b"(static_cast(desired)), "c"(static_cast(desired >> 64)) + : "memory"); + expected = (static_cast(high) << 64) | low; + return exchanged; +} + +Uint128 ReadHalves(const Uint128* target) { + const auto* halves = reinterpret_cast(target); + const std::uint64_t low = halves[0]; + const std::uint64_t high = halves[1]; + return (static_cast(high) << 64) | low; +} + +template +Uint128 FetchAndApply16(Uint128* target, TOperation operation) { + Uint128 expected = ReadHalves(target); + while (!CompareExchange16(target, expected, operation(expected))) { + } + return expected; +} + +} + +extern "C" { + +void APS5_VABI __atomic_load_nid_postfix(std::size_t, void*, void*, int) { ConsoleTrap("__atomic_load"); } +void APS5_VABI __atomic_store_nid_postfix(std::size_t, void*, void*, int) { ConsoleTrap("__atomic_store"); } +void APS5_VABI __atomic_exchange_nid_postfix(std::size_t, void*, void*, void*, int) { ConsoleTrap("__atomic_exchange"); } + +bool APS5_VABI __atomic_compare_exchange_nid_postfix(std::size_t, void*, void*, void*, int, int) { + ConsoleTrap("__atomic_compare_exchange"); +} + +bool APS5_VABI __atomic_is_lock_free_nid_postfix(std::size_t size, const void* pointer) { + const std::uintptr_t address = reinterpret_cast(pointer); + switch (size) { + case 0: + case 1: + return true; + case 2: + case 4: + case 8: + case 16: + return (address & (size - 1)) == 0; + default: + return false; + } +} + +Uint128 APS5_VABI __sync_fetch_and_add_16_nid_postfix(Uint128* target, Uint128 value) { + return FetchAndApply16(target, [value](Uint128 current) { return current + value; }); +} + +Uint128 APS5_VABI __sync_fetch_and_sub_16_nid_postfix(Uint128* target, Uint128 value) { + return FetchAndApply16(target, [value](Uint128 current) { return current - value; }); +} + +Uint128 APS5_VABI __sync_fetch_and_and_16_nid_postfix(Uint128* target, Uint128 value) { + return FetchAndApply16(target, [value](Uint128 current) { return current & value; }); +} + +Uint128 APS5_VABI __sync_fetch_and_or_16_nid_postfix(Uint128* target, Uint128 value) { + return FetchAndApply16(target, [value](Uint128 current) { return current | value; }); +} + +Uint128 APS5_VABI __sync_fetch_and_xor_16_nid_postfix(Uint128* target, Uint128 value) { + return FetchAndApply16(target, [value](Uint128 current) { return current ^ value; }); +} + +Uint128 APS5_VABI __sync_lock_test_and_set_16_nid_postfix(Uint128* target, Uint128 value) { + return FetchAndApply16(target, [value](Uint128) { return value; }); +} + +} diff --git a/core/libs/prx/libc/src/Backtrace.cpp b/core/libs/prx/libc/src/Backtrace.cpp index 5286225707..a07a476b4a 100644 --- a/core/libs/prx/libc/src/Backtrace.cpp +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -1,16 +1,172 @@ #include +#include +#include +#include +#include +#include + #include "prx/libc/include/General.hpp" +#if defined(__linux__) +#include +#include +#elif defined(_WIN32) +#include +#include "prx/libc/include/exceptions/Unwind.hpp" +extern "C" { +_Unwind_Reason_Code APS5_VABI _Unwind_Backtrace_nid_postfix(_Unwind_Trace_Fn, void*); +_Unwind_Ptr APS5_VABI _Unwind_GetIP_nid_postfix(_Unwind_Context*); +std::string BacktraceModulePath_nid_no_patch(HMODULE handle, std::size_t initialCapacity) { + std::string module; + std::size_t capacity = initialCapacity == 0 ? 1 : initialCapacity; + while (true) { + module.resize(capacity); + SetLastError(ERROR_SUCCESS); + const DWORD length = GetModuleFileNameA(handle, module.data(), static_cast(module.size())); + if (length == 0) return {}; + if (length < capacity && GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + module.resize(length); + return module; + } + if (capacity >= 32768) return {}; + capacity *= 2; + } +} +} +#endif + +#if defined(__linux__) || defined(_WIN32) + +namespace { + +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]; +#if defined(__linux__) + const int captured = backtrace(addresses, MaxFrames); +#elif defined(_WIN32) + struct TraceState { + void** addrs; + int max; + int count; + }; + static const auto traceCallback = [](_Unwind_Context* ctx, void* arg) APS5_VABI -> _Unwind_Reason_Code { + auto* s = static_cast(arg); + if (s->count < s->max) { + s->addrs[s->count++] = reinterpret_cast(_Unwind_GetIP_nid_postfix(ctx)); + } + return (s->count >= s->max) ? _URC_NORMAL_STOP : _URC_NO_REASON; + }; + TraceState state{addresses, MaxFrames, 0}; + _Unwind_Backtrace_nid_postfix(traceCallback, &state); + const int captured = state.count; +#endif + std::vector frames; + for (int index = 2; index < captured && (limit < 0 || static_cast(frames.size()) < limit); ++index) { +#if defined(__linux__) + Dl_info info{}; + if (dladdr(addresses[index], &info) == 0 || info.dli_fbase == nullptr || info.dli_fname == nullptr) continue; + const auto offset = reinterpret_cast(addresses[index]) - reinterpret_cast(info.dli_fbase); + if (offset > UINT32_MAX) continue; + std::string module = info.dli_fname; +#elif defined(_WIN32) + HMODULE handle = nullptr; + if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + static_cast(addresses[index]), &handle) || handle == nullptr) { + continue; + } + const auto offset = reinterpret_cast(addresses[index]) - reinterpret_cast(handle); + if (offset > UINT32_MAX) continue; + std::string module = BacktraceModulePath_nid_no_patch(handle, MAX_PATH); + if (module.empty()) continue; +#endif + if (const auto slash = module.find_last_of("/\\"); slash != std::string::npos) module.erase(0, slash + 1); + if (module.size() > sizeof(GuestSuffix) - 1 && module.compare(module.size() - (sizeof(GuestSuffix) - 1), std::string::npos, GuestSuffix) == 0) + module.resize(module.size() - (sizeof(GuestSuffix) - 1)); + frames.push_back({static_cast(offset), std::move(module)}); + } + 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/Bessel.cpp b/core/libs/prx/libc/src/Bessel.cpp new file mode 100644 index 0000000000..7ba3585ad3 --- /dev/null +++ b/core/libs/prx/libc/src/Bessel.cpp @@ -0,0 +1,424 @@ +#include +#include +#include +#include +#include +#include + +#include "prx/libc/include/general/VabiMacros.hpp" + +namespace { + +constexpr double One = 1.0; +constexpr double Huge = 1.0e300; +constexpr double InverseSqrtPi = 5.64189583547756279280e-01; +constexpr double TwoOverPi = 6.36619772367581382433e-01; +constexpr double LogMax = 7.09782712893383973096e+02; + +constexpr double PZeroNumerators[4][6] = { + {0.00000000000000000000e+00, -7.03124999999900357484e-02, -8.08167041275349795626e+00, -2.57063105679704847262e+02, -2.48521641009428822144e+03, -5.25304380490729545272e+03}, + {-1.14125464691894502584e-11, -7.03124940873599280078e-02, -4.15961064470587782438e+00, -6.76747652265167261021e+01, -3.31231299649172967747e+02, -3.46433388365604912451e+02}, + {-2.54704601771951915620e-09, -7.03119616381481654654e-02, -2.40903221549529611423e+00, -2.19659774734883086467e+01, -5.80791704701737572236e+01, -3.14479470594888503854e+01}, + {-8.87534333032526411254e-08, -7.03030995483624743247e-02, -1.45073846780952986357e+00, -7.63569613823527770791e+00, -1.11931668860356747786e+01, -3.23364579351335335033e+00}, +}; +constexpr double PZeroDenominators[4][5] = { + {1.16534364619668181717e+02, 3.83374475364121826715e+03, 4.05978572648472545552e+04, 1.16752972564375915681e+05, 4.76277284146730962675e+04}, + {6.07539382692300335975e+01, 1.05125230595704579173e+03, 5.97897094333855784498e+03, 9.62544514357774460223e+03, 2.40605815922939109441e+03}, + {3.58560338055209726349e+01, 3.61513983050303863820e+02, 1.19360783792111533330e+03, 1.12799679856907414432e+03, 1.73580930813335754692e+02}, + {2.22202997532088808441e+01, 1.36206794218215208048e+02, 2.70470278658083486789e+02, 1.53875394208320329881e+02, 1.46576176948256193810e+01}, +}; +constexpr double QZeroNumerators[4][6] = { + {0.00000000000000000000e+00, 7.32421874999935051953e-02, 1.17682064682252693899e+01, 5.57673380256401856059e+02, 8.85919720756468632317e+03, 3.70146267776887834771e+04}, + {1.84085963594515531381e-11, 7.32421766612684765896e-02, 5.83563508962056953777e+00, 1.35111577286449829671e+02, 1.02724376596164097464e+03, 1.98997785864605384631e+03}, + {4.37741014089738620906e-09, 7.32411180042911447163e-02, 3.34423137516170720929e+00, 4.26218440745412650017e+01, 1.70808091340565596283e+02, 1.66733948696651168575e+02}, + {1.50444444886983272379e-07, 7.32234265963079278272e-02, 1.99819174093815998816e+00, 1.44956029347885735348e+01, 3.16662317504781540833e+01, 1.62527075710929267416e+01}, +}; +constexpr double QZeroDenominators[4][6] = { + {1.63776026895689824414e+02, 8.09834494656449805916e+03, 1.42538291419120476348e+05, 8.03309257119514397345e+05, 8.40501579819060512818e+05, -3.43899293537866615225e+05}, + {8.27766102236537761883e+01, 2.07781416421392987104e+03, 1.88472887785718085070e+04, 5.67511122894947329769e+04, 3.59767538425114471465e+04, -5.35434275601944773371e+03}, + {4.87588729724587182091e+01, 7.09689221056606015736e+02, 3.70414822620111362994e+03, 6.46042516752568917582e+03, 2.51633368920368957333e+03, -1.49247451836156386662e+02}, + {3.03655848355219184498e+01, 2.69348118608049844624e+02, 8.44783757595320139444e+02, 8.82935845112488550512e+02, 2.12666388511798828631e+02, -5.31095493882666946917e+00}, +}; +constexpr double POneNumerators[4][6] = { + {0.00000000000000000000e+00, 1.17187499999988647970e-01, 1.32394806593073575129e+01, 4.12051854307378562225e+02, 3.87474538913960532227e+03, 7.91447954031891731574e+03}, + {1.31990519556243522749e-11, 1.17187493190614097638e-01, 6.80275127868432871736e+00, 1.08308182990189109773e+02, 5.17636139533199752805e+02, 5.28715201363337541807e+02}, + {3.02503916137373618024e-09, 1.17186865567253592491e-01, 3.93297750033315640650e+00, 3.51194035591636932736e+01, 9.10550110750781271918e+01, 4.85590685197364919645e+01}, + {1.07710830106873743082e-07, 1.17176219462683348094e-01, 2.36851496667608785174e+00, 1.22426109148261232917e+01, 1.76939711271687727390e+01, 5.07352312588818499250e+00}, +}; +constexpr double POneDenominators[4][5] = { + {1.14207370375678408436e+02, 3.65093083420853463394e+03, 3.69562060269033463555e+04, 9.76027935934950801311e+04, 3.08042720627888811578e+04}, + {5.92805987221131331921e+01, 9.91401418733614377743e+02, 5.35326695291487976647e+03, 7.84469031749551231769e+03, 1.50404688810361062679e+03}, + {3.47913095001251519989e+01, 3.36762458747825746741e+02, 1.04687139975775130551e+03, 8.90811346398256432622e+02, 1.03787932439639277504e+02}, + {2.14364859363821409488e+01, 1.25290227168402751090e+02, 2.32276469057162813669e+02, 1.17679373287147100768e+02, 8.36463893371618283368e+00}, +}; +constexpr double QOneNumerators[4][6] = { + {0.00000000000000000000e+00, -1.02539062499992714161e-01, -1.62717534544589987888e+01, -7.59601722513950107896e+02, -1.18498066702429587167e+04, -4.84385124285750353010e+04}, + {-2.08979931141764104297e-11, -1.02539050241375426231e-01, -8.05644828123936029840e+00, -1.83669607474888380239e+02, -1.37319376065508163265e+03, -2.61244440453215656817e+03}, + {-5.07831226461766561369e-09, -1.02537829820837089745e-01, -4.61011581139473403113e+00, -5.78472216562783643212e+01, -2.28244540737631695038e+02, -2.19210128478909325622e+02}, + {-1.78381727510958865572e-07, -1.02517042607985553460e-01, -2.75220568278187460720e+00, -1.96636162643703720221e+01, -4.23253133372830490089e+01, -2.13719211703704061733e+01}, +}; +constexpr double QOneDenominators[4][6] = { + {1.61395369700722909556e+02, 7.82538599923348465381e+03, 1.33875336287249578163e+05, 7.19657723683240939863e+05, 6.66601232617776375264e+05, -2.94490264303834643215e+05}, + {8.12765501384335777857e+01, 1.99179873460485964642e+03, 1.74684851924908907677e+04, 4.98514270910352279316e+04, 2.79480751638918118260e+04, -4.71918354795128470869e+03}, + {4.76651550323729509273e+01, 6.73865112676699709482e+02, 3.38015286679526343505e+03, 5.54772909720722782367e+03, 1.90311919338810798763e+03, -1.35201191444307340817e+02}, + {2.95333629060523854548e+01, 2.52981549982190529136e+02, 7.57502834868645436472e+02, 7.39393205320467245656e+02, 1.55949003336666123687e+02, -4.95949898822628210127e+00}, +}; + +constexpr double J0Numerator[] = {1.56249999999999947958e-02, -1.89979294238854721751e-04, 1.82954049532700665670e-06, -4.61832688532103189199e-09}; +constexpr double J0Denominator[] = {1.56191029464890010492e-02, 1.16926784663337450260e-04, 5.13546550207318111446e-07, 1.16614003333790000205e-09}; +constexpr double Y0Numerator[] = { + -7.38042951086872317523e-02, 1.76666452509181115538e-01, -1.38185671945596898896e-02, 3.47453432093683650238e-04, + -3.81407053724364161125e-06, 1.95590137035022920206e-08, -3.98205194132103398453e-11, +}; +constexpr double Y0Denominator[] = {1.27304834834123699328e-02, 7.60068627350353253702e-05, 2.59150851840457805467e-07, 4.41110311332675467403e-10}; +constexpr double J1Numerator[] = {-6.25000000000000000000e-02, 1.40705666955189706048e-03, -1.59955631084035597520e-05, 4.96727999609584448412e-08}; +constexpr double J1Denominator[] = { + 1.91537599538363460805e-02, 1.85946785588630915560e-04, 1.17718464042623683263e-06, 5.04636257076217042715e-09, 1.23542274426137913908e-11, +}; +constexpr double Y1Numerator[] = { + -1.96057090646238940668e-01, 5.04438716639811282616e-02, -1.91256895875763547298e-03, 2.35252600561610495928e-05, -9.19099158039878874504e-08, +}; +constexpr double Y1Denominator[] = { + 1.99167318236649903973e-02, 2.02552581025135171496e-04, 1.35608801097516229404e-06, 6.22741452364621501295e-09, 1.66559246207992079114e-11, +}; + +std::uint32_t HighWord(double x) { + return static_cast(std::bit_cast(x) >> 32); +} + +std::uint32_t LowWord(double x) { + return static_cast(std::bit_cast(x)); +} + +double SecondKindAtZero() { + return -std::numeric_limits::infinity(); +} + +double SecondKindOfNegative() { + return std::bit_cast(0x7ff8000000000000ull); +} + +double Signed(std::uint32_t value) { + return static_cast(static_cast(value)); +} + +double SquareRoot(double x) { + return _mm_cvtsd_f64(_mm_sqrt_sd(_mm_setzero_pd(), _mm_set_sd(x))); +} + +const bool HardwareFma = [] { + __builtin_cpu_init(); + return __builtin_cpu_supports("fma") != 0; +}(); + +[[gnu::target("fma")]] double HardwareFused(double a, double b, double c) { + return _mm_cvtsd_f64(_mm_fmadd_sd(_mm_set_sd(a), _mm_set_sd(b), _mm_set_sd(c))); +} + +double Fused(double a, double b, double c) { + return HardwareFma ? HardwareFused(a, b, c) : std::fma(a, b, c); +} + +template +double Horner(double z, const double (&coefficients)[Count]) { + double result = coefficients[Count - 1]; + for (std::size_t index = Count - 1; index-- > 0;) result = Fused(z, result, coefficients[index]); + return result; +} + +std::size_t AsymptoticRange(std::uint32_t magnitude) { + if (magnitude >= 0x40200000u) return 0; + if (magnitude >= 0x40122e8bu) return 1; + if (magnitude >= 0x4006db6du) return 2; + return 3; +} + +template +double Asymptotic(double x, const double (&numerator)[NumeratorCount], const double (&denominator)[DenominatorCount]) { + const double z = One / (x * x); + return Horner(z, numerator) / Fused(z, Horner(z, denominator), One); +} + +double PZero(double x, std::uint32_t magnitude) { + const std::size_t range = AsymptoticRange(magnitude); + return Asymptotic(x, PZeroNumerators[range], PZeroDenominators[range]) + One; +} + +double QZero(double x, std::uint32_t magnitude) { + const std::size_t range = AsymptoticRange(magnitude); + return (Asymptotic(x, QZeroNumerators[range], QZeroDenominators[range]) + -0.125) / x; +} + +double POne(double x, std::uint32_t magnitude) { + const std::size_t range = AsymptoticRange(magnitude); + return Asymptotic(x, POneNumerators[range], POneDenominators[range]) + One; +} + +double QOne(double x, std::uint32_t magnitude) { + const std::size_t range = AsymptoticRange(magnitude); + return (Asymptotic(x, QOneNumerators[range], QOneDenominators[range]) + 0.375) / x; +} + +} + +extern "C" { + +double APS5_VABI j0_nid_postfix(double x) { + const std::uint32_t magnitude = HighWord(x) & 0x7fffffffu; + if (magnitude >= 0x7ff00000u) return One / (x * x); + const double absolute = std::fabs(x); + if (magnitude >= 0x40000000u) { + const double s = std::sin(absolute); + const double c = std::cos(x); + double ss = s - c; + double cc = c + s; + if (magnitude < 0x7fe00000u) { + const double z = -std::cos(absolute + absolute); + if (s * c < 0.0) cc = z / ss; + else ss = z / cc; + } + if (magnitude > 0x48000000u) return cc * InverseSqrtPi / SquareRoot(absolute); + const double u = PZero(absolute, magnitude); + const double v = QZero(absolute, magnitude); + return Fused(u, cc, -(ss * v)) * InverseSqrtPi / SquareRoot(absolute); + } + if (Huge + absolute > One && magnitude < 0x3f200000u) { + if (magnitude < 0x3e400000u) return One; + return Fused(x * x, -0.25, One); + } + const double z = x * x; + const double r = z * Horner(z, J0Numerator); + const double s = Fused(z, Horner(z, J0Denominator), One); + if (magnitude < 0x3ff00000u) return Fused(z, r / s + -0.25, One); + const double u = absolute * 0.5; + return Fused(One + u, One - u, z * (r / s)); +} + +double APS5_VABI y0_nid_postfix(double x) { + const std::uint32_t high = HighWord(x); + const std::uint32_t magnitude = high & 0x7fffffffu; + if (magnitude >= 0x7ff00000u) return One / Fused(x, x, x); + if ((magnitude | LowWord(x)) == 0) return SecondKindAtZero(); + if ((high & 0x80000000u) != 0) return SecondKindOfNegative(); + if (magnitude >= 0x40000000u) { + const double s = std::sin(x); + const double c = std::cos(x); + double ss = s - c; + double cc = s + c; + if (magnitude < 0x7fe00000u) { + const double z = -std::cos(x + x); + if (s * c < 0.0) cc = z / ss; + else ss = z / cc; + } + if (magnitude > 0x48000000u) return ss * InverseSqrtPi / SquareRoot(x); + const double u = PZero(x, magnitude); + const double v = QZero(x, magnitude); + return Fused(u, ss, cc * v) * InverseSqrtPi / SquareRoot(x); + } + if (magnitude <= 0x3e400000u) return Fused(TwoOverPi, std::log(x), Y0Numerator[0]); + const double z = x * x; + const double u = Horner(z, Y0Numerator); + const double v = Fused(z, Horner(z, Y0Denominator), One); + return Fused(std::log(x) * j0_nid_postfix(x), TwoOverPi, u / v); +} + +double APS5_VABI j1_nid_postfix(double x) { + const std::uint32_t high = HighWord(x); + const std::uint32_t magnitude = high & 0x7fffffffu; + if (magnitude >= 0x7ff00000u) return One / x; + if (magnitude >= 0x40000000u) { + const double y = std::fabs(x); + const double s = std::sin(y); + const double c = std::cos(x); + double ss = -s - c; + double cc = s - c; + if (magnitude < 0x7fe00000u) { + const double z = std::cos(y + y); + if (s * c > 0.0) cc = z / ss; + else ss = z / cc; + } + double result; + if (magnitude > 0x48000000u) { + result = cc * InverseSqrtPi / SquareRoot(y); + } else { + const double u = POne(y, magnitude); + const double v = QOne(y, magnitude); + result = Fused(u, cc, -(ss * v)) * InverseSqrtPi / SquareRoot(y); + } + return (high & 0x80000000u) != 0 ? -result : result; + } + if (Huge + x > One && magnitude < 0x3e400000u) return x * 0.5; + const double z = x * x; + const double r = z * Horner(z, J1Numerator); + const double s = Fused(z, Horner(z, J1Denominator), One); + return Fused(x, 0.5, r * x / s); +} + +double APS5_VABI y1_nid_postfix(double x) { + const std::uint32_t high = HighWord(x); + const std::uint32_t magnitude = high & 0x7fffffffu; + if (magnitude >= 0x7ff00000u) return One / Fused(x, x, x); + if ((magnitude | LowWord(x)) == 0) return SecondKindAtZero(); + if ((high & 0x80000000u) != 0) return SecondKindOfNegative(); + if (magnitude >= 0x40000000u) { + const double s = std::sin(x); + const double c = std::cos(x); + double ss = -s - c; + double cc = s - c; + if (magnitude < 0x7fe00000u) { + const double z = std::cos(x + x); + if (s * c > 0.0) cc = z / ss; + else ss = z / cc; + } + if (magnitude > 0x48000000u) return ss * InverseSqrtPi / SquareRoot(x); + const double u = POne(x, magnitude); + const double v = QOne(x, magnitude); + return Fused(u, ss, cc * v) * InverseSqrtPi / SquareRoot(x); + } + if (magnitude <= 0x3c900000u) return -TwoOverPi / x; + const double z = x * x; + const double u = Horner(z, Y1Numerator); + const double v = Fused(z, Horner(z, Y1Denominator), One); + return Fused(x, u / v, Fused(std::log(x), j1_nid_postfix(x), -One / x) * TwoOverPi); +} + +double APS5_VABI jn_nid_postfix(int order, double x) { + const std::uint32_t high = HighWord(x); + const std::uint32_t low = LowWord(x); + const std::uint32_t magnitude = high & 0x7fffffffu; + if ((magnitude | (low != 0 ? 1u : 0u)) > 0x7ff00000u) return x + x; + const double argument = order < 0 ? -x : x; + const std::uint32_t n = order < 0 ? 0u - static_cast(order) : static_cast(order); + if (n == 1) return j1_nid_postfix(argument); + if (n == 0) return j0_nid_postfix(argument); + const bool negative = ((high ^ static_cast(order)) & 0x80000000u) != 0; + double result = 0.0; + if (magnitude < 0x7ff00000u && (magnitude | low) != 0) { + const double absolute = std::fabs(x); + const double orderValue = Signed(n); + if (absolute >= orderValue) { + if (magnitude >= 0x52d00000u) { + double sum; + switch (n & 3u) { + case 0: sum = std::sin(absolute) + std::cos(argument); break; + case 1: sum = std::sin(absolute) - std::cos(argument); break; + case 2: sum = -std::cos(argument) - std::sin(absolute); break; + default: sum = std::cos(argument) - std::sin(absolute); break; + } + result = sum * InverseSqrtPi / SquareRoot(absolute); + } else { + double previous = j0_nid_postfix(absolute); + result = j1_nid_postfix(absolute); + std::uint32_t twice = 2; + for (std::uint32_t remaining = n - 1; remaining != 0; --remaining) { + const double next = Fused(result, Signed(twice) / absolute, -previous); + previous = result; + result = next; + twice += 2; + } + } + } else if (magnitude < 0x3e100000u) { + if (n <= 33u) { + const double half = absolute * 0.5; + double power = half; + double factorial = One; + for (std::uint32_t index = 2; index <= n; ++index) { + power = half * power; + factorial *= Signed(index); + } + result = power / factorial; + } + } else { + const double h = 2.0 / absolute; + const double w = Signed(2u * n) / absolute; + double q0 = w; + double z = h + w; + double q1 = Fused(w, z, -One); + std::uint32_t k = 1; + while (q1 < 1.0e9) { + z = h + z; + ++k; + const double next = Fused(z, q1, -q0); + q0 = q1; + q1 = next; + } + const std::int32_t lowest = static_cast(2u * n); + std::uint32_t index = 2u * (k + n); + double t = 0.0; + do { + t = One / (Signed(index) / absolute - t); + index -= 2u; + } while (static_cast(index) >= lowest); + const double bound = std::log(std::fabs(h * orderValue)) * orderValue; + double a = t; + double b = One; + double di = Signed(2u * n - 2u); + std::int32_t remaining = static_cast(n); + const bool rescale = !(bound < LogMax); + do { + const double next = b * di / absolute - a; + di += -2.0; + a = b; + b = next; + if (rescale && b > 1.0e100) { + t /= b; + a /= b; + b = One; + } + --remaining; + } while (remaining > 1); + const double z0 = j0_nid_postfix(absolute); + const double z1 = j1_nid_postfix(absolute); + result = std::fabs(z0) >= std::fabs(z1) ? z0 * t / b : t * z1 / a; + } + } + const double flipped = negative ? -result : result; + return (n & 1u) != 0 ? flipped : result; +} + +double APS5_VABI yn_nid_postfix(int order, double x) { + const std::uint32_t high = HighWord(x); + const std::uint32_t low = LowWord(x); + const std::uint32_t magnitude = high & 0x7fffffffu; + if ((magnitude | (low != 0 ? 1u : 0u)) > 0x7ff00000u) return x + x; + if ((magnitude | low) == 0) return SecondKindAtZero(); + if ((high & 0x80000000u) != 0) return SecondKindOfNegative(); + std::uint32_t n = static_cast(order); + std::int32_t sign = 1; + if (order < 0) { + n = 0u - n; + sign = 1 - static_cast((n << 1) & 2u); + } + if (n == 1) return static_cast(sign) * y1_nid_postfix(x); + if (n == 0) return y0_nid_postfix(x); + if (magnitude == 0x7ff00000u) return 0.0; + double result; + if (magnitude >= 0x52d00000u) { + const double s = std::sin(x); + const double c = std::cos(x); + double sum; + switch (n & 3u) { + case 0: sum = s - c; break; + case 1: sum = -s - c; break; + case 2: sum = c - s; break; + default: sum = s + c; break; + } + result = sum * InverseSqrtPi / SquareRoot(x); + } else { + double previous = y0_nid_postfix(x); + result = y1_nid_postfix(x); + std::uint32_t twice = 2; + std::int32_t index = 2; + while (HighWord(result) != 0xfff00000u) { + const double next = Fused(result, Signed(twice) / x, -previous); + previous = result; + result = next; + if (index >= static_cast(n)) break; + ++index; + twice += 2; + } + } + return sign > 0 ? result : -result; +} + +} diff --git a/core/libs/prx/libc/src/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/BoundsChecking.cpp b/core/libs/prx/libc/src/BoundsChecking.cpp new file mode 100644 index 0000000000..a29df4a74f --- /dev/null +++ b/core/libs/prx/libc/src/BoundsChecking.cpp @@ -0,0 +1,298 @@ +#include +#include +#include +#include + +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/SonyCrt.hpp" + +extern "C" { +std::size_t APS5_VABI mbrtowc_nid_postfix(std::uint16_t* destination, const char* source, std::size_t count, void* state); +std::size_t APS5_VABI wcrtomb_nid_postfix(char* destination, std::uint16_t value, void* state); +int APS5_VABI wctomb_nid_postfix(char* destination, std::uint16_t value); +int APS5_VABI ___mb_cur_max_nid_postfix(); +int APS5_VABI getchar_nid_postfix(); +int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream); +int APS5_VABI fputc_nid_postfix(int value, FileStream* stream); +extern FileStream* __stderrp_nid_postfix; +[[noreturn]] void abort_nid_postfix(std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t); +} + +namespace { + +constexpr int guestEinval = 22; +constexpr int guestErange = 34; +constexpr int guestEilseq = 86; +constexpr int guestEof = -1; +constexpr std::size_t failed = static_cast(-1); +constexpr std::size_t rsizeMax = (std::size_t{1} << 61) - 1; +constexpr std::size_t guestStateSize = 128; + +using GuestContextCompare = int (APS5_VABI*)(const void*, const void*, void*); + +bool usableSize(std::size_t size) { + return size != 0 && size <= rsizeMax; +} + +bool convertFailed(std::size_t length) { + return length >= failed - 1; +} + +int violation(const char* message, int error) { + SonyInvokeConstraintHandler(message, nullptr, error); + return error; +} + +} // namespace + +extern "C" { + +void APS5_VABI abort_handler_s_nid_postfix(const char* message, void*, int) { + fputs_nid_postfix(message, __stderrp_nid_postfix); + fputc_nid_postfix('\n', __stderrp_nid_postfix); + abort_nid_postfix(0, 0, 0, 0, 0, 0); +} + +void* APS5_VABI bsearch_s_nid_postfix(const void* key, const void* base, std::size_t count, std::size_t size, GuestContextCompare compare, void* context) { + if (count > rsizeMax || size > rsizeMax) { + violation("bsearch_s: bad argument", guestEinval); + return nullptr; + } + if (count == 0) return nullptr; + if (!key || !base || !compare) { + violation("bsearch_s: bad argument", guestEinval); + return nullptr; + } + const auto* low = static_cast(base); + for (std::size_t remaining = count; remaining != 0; remaining >>= 1) { + const auto* probe = low + (remaining >> 1) * size; + const int order = compare(key, probe, context); + if (order == 0) return const_cast(probe); + if (order > 0) { + low = probe + size; + --remaining; + } + } + return nullptr; +} + +std::uint16_t* APS5_VABI wcstok_s_nid_postfix(std::uint16_t* str, std::size_t* remaining, const std::uint16_t* delimiters, std::uint16_t** context) { + if (!remaining || !context || !delimiters || *remaining > rsizeMax || (!str && !*context)) { + violation("wcstok_s: bad argument", guestEinval); + return nullptr; + } + if (!str) str = *context; + const auto isDelimiter = [delimiters](std::uint16_t value) { + for (const auto* delimiter = delimiters; *delimiter != 0; ++delimiter) + if (*delimiter == value) return true; + return false; + }; + const std::size_t available = *remaining; + std::size_t skipped = 0; + if (available == 0) { + violation("wcstok_s: missing token", guestEinval); + return nullptr; + } + while (str[skipped] != 0 && isDelimiter(str[skipped])) { + if (++skipped == available) { + violation("wcstok_s: missing token", guestEinval); + return nullptr; + } + } + std::uint16_t* const token = str + skipped; + const std::size_t left = available - skipped; + *remaining = left; + if (*token == 0) { + *context = token; + return nullptr; + } + std::size_t length = 0; + while (token[length] != 0 && !isDelimiter(token[length])) { + if (++length == left) { + violation("wcstok_s: missing delimiter", guestEinval); + return nullptr; + } + } + std::uint16_t* next = token + length; + *remaining = left - length; + if (*next != 0) { + *next++ = 0; + if (left != length) *remaining = left - length - 1; + } + *context = next; + return token; +} + +int APS5_VABI wctomb_s_nid_postfix(int* status, char* destination, std::size_t capacity, std::uint16_t wide) { + if (!status || (!destination && capacity) || (destination && capacity > rsizeMax)) + return violation("wctomb_s: bad argument", guestEinval); + if (destination && static_cast(___mb_cur_max_nid_postfix()) > capacity) { + char bytes[MB_LEN_MAX]; + const int length = wctomb_nid_postfix(bytes, wide); + if (length > 0 && static_cast(length) > capacity) return violation("wctomb_s: buffer too small", guestErange); + if (length > 0) std::memcpy(destination, bytes, static_cast(length)); + *status = length; + return length < 0 ? guestEilseq : 0; + } + const int length = wctomb_nid_postfix(destination, wide); + *status = length; + return destination && length < 0 ? guestEilseq : 0; +} + +int APS5_VABI wcrtomb_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, std::uint16_t wide, void* state) { + if (!result || !state) { + if (destination && usableSize(capacity)) destination[0] = '\0'; + if (result) *result = failed; + return violation("wcrtomb_s: bad argument", guestEinval); + } + if ((!destination && capacity) || (destination && !usableSize(capacity))) { + *result = failed; + return violation("wcrtomb_s: bad argument", guestEinval); + } + char bytes[MB_LEN_MAX]; + std::size_t length = 0; + if (!destination) { + length = wcrtomb_nid_postfix(bytes, 0, state); + } else if (static_cast(___mb_cur_max_nid_postfix()) > capacity) { + length = wcrtomb_nid_postfix(bytes, wide, state); + if (length != failed && length > capacity) { + *result = failed; + destination[0] = '\0'; + return violation("wcrtomb_s: buffer too small", guestErange); + } + if (length != failed) std::memcpy(destination, bytes, length); + } else { + length = wcrtomb_nid_postfix(destination, wide, state); + } + *result = length; + return length == failed ? guestEilseq : 0; +} + +int APS5_VABI mbstowcs_s_nid_postfix(std::size_t* result, std::uint16_t* destination, std::size_t capacity, const char* source, std::size_t limit) { + if (!result || !source || (!destination && capacity) || (destination && (!usableSize(capacity) || limit > rsizeMax))) { + if (result) *result = failed; + if (destination && usableSize(capacity)) destination[0] = 0; + return violation("mbstowcs_s: bad argument", guestEinval); + } + alignas(8) unsigned char state[guestStateSize]{}; + if (!destination) { + std::size_t count = 0; + for (;; ++count) { + std::uint16_t wide = 0; + const std::size_t length = mbrtowc_nid_postfix(&wide, source, INT_MAX, state); + if (convertFailed(length)) { + *result = failed; + return guestEilseq; + } + if (length == 0 && wide == 0) break; + source += length; + } + *result = count; + return 0; + } + const std::size_t total = limit < capacity ? limit : capacity; + std::size_t index = 0; + while (index < total) { + const std::size_t length = mbrtowc_nid_postfix(destination + index, source, INT_MAX, state); + if (convertFailed(length)) { + *result = failed; + destination[index] = 0; + return guestEilseq; + } + if (length == 0 && destination[index] == 0) { + *result = index; + return 0; + } + source += length; + ++index; + } + if (index == capacity) { + *result = failed; + destination[0] = 0; + return violation("mbstowcs_s: buffer overflow", guestErange); + } + *result = index; + destination[index] = 0; + return 0; +} + +int APS5_VABI mbsrtowcs_s_nid_postfix(std::size_t* result, std::uint16_t* destination, std::size_t capacity, const char** source, std::size_t limit, void* state) { + if (!result || !source || !*source || !state || (!destination && capacity) || (destination && (!usableSize(capacity) || limit > rsizeMax))) { + if (destination && usableSize(capacity)) destination[0] = 0; + if (result) *result = failed; + return violation("mbsrtowcs_s: bad argument", guestEinval); + } + const char* cursor = *source; + if (!destination) { + std::size_t count = 0; + for (;; ++count) { + std::uint16_t wide = 0; + const std::size_t length = mbrtowc_nid_postfix(&wide, cursor, INT_MAX, state); + if (convertFailed(length)) { + *result = failed; + return guestEilseq; + } + if (length == 0 && wide == 0) break; + cursor += length; + } + *result = count; + return 0; + } + const std::size_t total = limit < capacity ? limit : capacity; + std::size_t index = 0; + while (index < total) { + const std::size_t length = mbrtowc_nid_postfix(destination + index, cursor, INT_MAX, state); + if (convertFailed(length)) { + *source = cursor; + destination[index] = 0; + *result = failed; + return guestEilseq; + } + if (length == 0 && destination[index] == 0) { + *source = nullptr; + *result = index; + return 0; + } + cursor += length; + ++index; + } + if (total != 0) *source = cursor; + if (index == capacity) { + destination[0] = 0; + *result = failed; + return violation("mbsrtowcs_s: buffer too small", guestErange); + } + destination[index] = 0; + *result = index; + return 0; +} + +char* APS5_VABI gets_s_nid_postfix(char* buffer, std::size_t size) { + if (size == 0) { + violation("gets_s: zero buffer size", guestEinval); + return nullptr; + } + if (!buffer || size > rsizeMax) { + violation("gets_s: bad argument", guestEinval); + return nullptr; + } + char* cursor = buffer; + int value = getchar_nid_postfix(); + for (std::size_t left = size - 1; left != 0 && value != guestEof && value != '\n'; --left) { + *cursor++ = static_cast(value); + value = getchar_nid_postfix(); + } + if (value == '\n' || (value == guestEof && cursor != buffer)) { + *cursor = '\0'; + return buffer; + } + if (value != guestEof) { + violation("gets_s: short buffer", guestErange); + while (value != guestEof && value != '\n') value = getchar_nid_postfix(); + } + buffer[0] = '\0'; + return nullptr; +} + +} diff --git a/core/libs/prx/libc/src/CxxAbiSupport.cpp b/core/libs/prx/libc/src/CxxAbiSupport.cpp index 2553a80f7c..1cbe9c38cd 100644 --- a/core/libs/prx/libc/src/CxxAbiSupport.cpp +++ b/core/libs/prx/libc/src/CxxAbiSupport.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include #include "prx/libc/include/General.hpp" @@ -99,12 +101,28 @@ const std::error_category* _ZSt17iostream_categoryv_nid_postfix() { return &std: int APS5_VABI _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix( int* flag, int (APS5_VABI *callback)(void*, void*, void**), void* arg ) { - static std::recursive_mutex mutex; - std::lock_guard lock(mutex); + static std::mutex mutex; + static std::condition_variable changed; + static std::unordered_set running; + std::unique_lock lock(mutex); + changed.wait(lock, [&] { return *flag != 0 || !running.contains(flag); }); if (*flag != 0) return 1; - if (callback(nullptr, arg, nullptr) == 0) return 0; - *flag = 1; - return 1; + running.insert(flag); + lock.unlock(); + bool initialized; + try { + initialized = callback(nullptr, arg, nullptr) != 0; + } catch (...) { + lock.lock(); + running.erase(flag); + changed.notify_all(); + throw; + } + lock.lock(); + if (initialized) *flag = 1; + running.erase(flag); + changed.notify_all(); + return initialized ? 1 : 0; } } diff --git a/core/libs/prx/libc/src/DinkumwareAtomics.cpp b/core/libs/prx/libc/src/DinkumwareAtomics.cpp new file mode 100644 index 0000000000..f8aecb774e --- /dev/null +++ b/core/libs/prx/libc/src/DinkumwareAtomics.cpp @@ -0,0 +1,175 @@ +#include +#include +#include + +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int MemoryOrderSeqCst = 5; + +template +void Store(volatile TValue* target, TValue value) { + __atomic_store_n(target, value, __ATOMIC_SEQ_CST); +} + +template +TValue Load(volatile TValue* target) { + return __atomic_load_n(target, __ATOMIC_SEQ_CST); +} + +bool IsValidOrder(int order) { + return order >= 0 && order <= MemoryOrderSeqCst; +} + +template +TValue Exchange(volatile TValue* target, TValue value) { + return __atomic_exchange_n(target, value, __ATOMIC_SEQ_CST); +} + +template +int CompareExchange(volatile TValue* target, TValue* expected, TValue desired) { + return __atomic_compare_exchange_n(target, expected, desired, false, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST) ? 1 : 0; +} + +void LockFlag(volatile unsigned char* flag) { + while (__atomic_exchange_n(flag, static_cast(1), __ATOMIC_ACQUIRE) != 0) { + } +} + +void UnlockFlag(volatile unsigned char* flag) { + __atomic_store_n(flag, static_cast(0), __ATOMIC_SEQ_CST); +} + +void CopyBytes(volatile void* destination, const volatile void* source, std::size_t size) { + std::memcpy(const_cast(destination), const_cast(source), size); +} + +int LockedCompareExchange(volatile unsigned char* flag, std::size_t size, volatile void* target, void* expected, const void* desired) { + LockFlag(flag); + void* object = const_cast(target); + const bool equal = std::memcmp(object, expected, size) == 0; + if (equal) { + std::memcpy(object, desired, size); + } else { + std::memcpy(expected, object, size); + } + UnlockFlag(flag); + return equal ? 1 : 0; +} + +} + +extern "C" { + +void APS5_VABI _Atomic_store_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t value, int) { Store(target, value); } +void APS5_VABI _Atomic_store_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t value, int) { Store(target, value); } +void APS5_VABI _Atomic_store_4_nid_postfix(volatile std::uint32_t* target, std::uint32_t value, int) { Store(target, value); } +void APS5_VABI _Atomic_store_8_nid_postfix(volatile std::uint64_t* target, std::uint64_t value, int) { Store(target, value); } + +std::uint8_t APS5_VABI _Atomic_load_1_nid_postfix(volatile std::uint8_t* target, int) { return Load(target); } +std::uint16_t APS5_VABI _Atomic_load_2_nid_postfix(volatile std::uint16_t* target, int) { return Load(target); } +std::uint64_t APS5_VABI _Atomic_load_8_nid_postfix(volatile std::uint64_t* target, int order) { + if (order == 3 || order == 4 || !IsValidOrder(order)) return 0; + return Load(target); +} + +std::uint8_t APS5_VABI _Atomic_exchange_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t value, int) { return Exchange(target, value); } +std::uint16_t APS5_VABI _Atomic_exchange_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t value, int) { return Exchange(target, value); } +std::uint32_t APS5_VABI _Atomic_exchange_4_nid_postfix(volatile std::uint32_t* target, std::uint32_t value, int) { return Exchange(target, value); } +std::uint64_t APS5_VABI _Atomic_exchange_8_nid_postfix(volatile std::uint64_t* target, std::uint64_t value, int order) { return IsValidOrder(order) ? Exchange(target, value) : 0; } + +int APS5_VABI _Atomic_compare_exchange_strong_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t* expected, std::uint8_t desired, int, int) { + return CompareExchange(target, expected, desired); +} + +int APS5_VABI _Atomic_compare_exchange_strong_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t* expected, std::uint16_t desired, int, int) { + return CompareExchange(target, expected, desired); +} + +int APS5_VABI _Atomic_compare_exchange_strong_4_nid_postfix(volatile std::uint32_t* target, std::uint32_t* expected, std::uint32_t desired, int, int) { + return CompareExchange(target, expected, desired); +} + +int APS5_VABI _Atomic_compare_exchange_strong_8_nid_postfix(volatile std::uint64_t* target, std::uint64_t* expected, std::uint64_t desired, int, int) { + return CompareExchange(target, expected, desired); +} + +int APS5_VABI _Atomic_compare_exchange_weak_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t* expected, std::uint8_t desired, int, int) { + return CompareExchange(target, expected, desired); +} + +int APS5_VABI _Atomic_compare_exchange_weak_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t* expected, std::uint16_t desired, int, int) { + return CompareExchange(target, expected, desired); +} + +int APS5_VABI _Atomic_compare_exchange_weak_8_nid_postfix(volatile std::uint64_t* target, std::uint64_t* expected, std::uint64_t desired, int, int) { + return CompareExchange(target, expected, desired); +} + +std::uint8_t APS5_VABI _Atomic_fetch_add_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t value, int) { return __atomic_fetch_add(target, value, __ATOMIC_SEQ_CST); } +std::uint16_t APS5_VABI _Atomic_fetch_add_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t value, int) { return __atomic_fetch_add(target, value, __ATOMIC_SEQ_CST); } +std::uint64_t APS5_VABI _Atomic_fetch_add_8_nid_postfix(volatile std::uint64_t* target, std::uint64_t value, int order) { return IsValidOrder(order) ? __atomic_fetch_add(target, value, __ATOMIC_SEQ_CST) : 0; } +std::uint8_t APS5_VABI _Atomic_fetch_sub_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t value, int) { return __atomic_fetch_sub(target, value, __ATOMIC_SEQ_CST); } +std::uint16_t APS5_VABI _Atomic_fetch_sub_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t value, int) { return __atomic_fetch_sub(target, value, __ATOMIC_SEQ_CST); } +std::uint64_t APS5_VABI _Atomic_fetch_sub_8_nid_postfix(volatile std::uint64_t* target, std::uint64_t value, int order) { return IsValidOrder(order) ? __atomic_fetch_sub(target, value, __ATOMIC_SEQ_CST) : 0; } +std::uint8_t APS5_VABI _Atomic_fetch_and_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t value, int) { return __atomic_fetch_and(target, value, __ATOMIC_SEQ_CST); } +std::uint16_t APS5_VABI _Atomic_fetch_and_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t value, int) { return __atomic_fetch_and(target, value, __ATOMIC_SEQ_CST); } +std::uint32_t APS5_VABI _Atomic_fetch_and_4_nid_postfix(volatile std::uint32_t* target, std::uint32_t value, int) { return __atomic_fetch_and(target, value, __ATOMIC_SEQ_CST); } +std::uint64_t APS5_VABI _Atomic_fetch_and_8_nid_postfix(volatile std::uint64_t* target, std::uint64_t value, int order) { return IsValidOrder(order) ? __atomic_fetch_and(target, value, __ATOMIC_SEQ_CST) : 0; } +std::uint8_t APS5_VABI _Atomic_fetch_or_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t value, int) { return __atomic_fetch_or(target, value, __ATOMIC_SEQ_CST); } +std::uint16_t APS5_VABI _Atomic_fetch_or_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t value, int) { return __atomic_fetch_or(target, value, __ATOMIC_SEQ_CST); } +std::uint32_t APS5_VABI _Atomic_fetch_or_4_nid_postfix(volatile std::uint32_t* target, std::uint32_t value, int) { return __atomic_fetch_or(target, value, __ATOMIC_SEQ_CST); } +std::uint64_t APS5_VABI _Atomic_fetch_or_8_nid_postfix(volatile std::uint64_t* target, std::uint64_t value, int order) { return IsValidOrder(order) ? __atomic_fetch_or(target, value, __ATOMIC_SEQ_CST) : 0; } +std::uint8_t APS5_VABI _Atomic_fetch_xor_1_nid_postfix(volatile std::uint8_t* target, std::uint8_t value, int) { return __atomic_fetch_xor(target, value, __ATOMIC_SEQ_CST); } +std::uint16_t APS5_VABI _Atomic_fetch_xor_2_nid_postfix(volatile std::uint16_t* target, std::uint16_t value, int) { return __atomic_fetch_xor(target, value, __ATOMIC_SEQ_CST); } +std::uint32_t APS5_VABI _Atomic_fetch_xor_4_nid_postfix(volatile std::uint32_t* target, std::uint32_t value, int) { return __atomic_fetch_xor(target, value, __ATOMIC_SEQ_CST); } +std::uint64_t APS5_VABI _Atomic_fetch_xor_8_nid_postfix(volatile std::uint64_t* target, std::uint64_t value, int order) { return IsValidOrder(order) ? __atomic_fetch_xor(target, value, __ATOMIC_SEQ_CST) : 0; } + +int APS5_VABI _Atomic_is_lock_free_1_nid_postfix() { return 1; } +int APS5_VABI _Atomic_is_lock_free_2_nid_postfix() { return 1; } +int APS5_VABI _Atomic_is_lock_free_4_nid_postfix() { return 1; } +int APS5_VABI _Atomic_is_lock_free_8_nid_postfix() { return 1; } + +int APS5_VABI _Atomic_flag_test_and_set_nid_postfix(volatile unsigned char* flag, int) { + return __atomic_exchange_n(flag, static_cast(1), __ATOMIC_SEQ_CST); +} + +void APS5_VABI _Atomic_flag_clear_nid_postfix(volatile unsigned char* flag, int) { + __atomic_store_n(flag, static_cast(0), __ATOMIC_SEQ_CST); +} + +void APS5_VABI _Atomic_thread_fence_nid_postfix(int order) { + if (order == MemoryOrderSeqCst) __atomic_thread_fence(__ATOMIC_SEQ_CST); +} + +void APS5_VABI _Atomic_signal_fence_nid_postfix(int) { +} + +void APS5_VABI _Atomic_copy_nid_postfix(volatile unsigned char* flag, std::size_t size, volatile void* target, const volatile void* source, int) { + LockFlag(flag); + CopyBytes(target, source, size); + UnlockFlag(flag); +} + +void APS5_VABI _Atomic_exchange_nid_postfix(volatile unsigned char* flag, std::size_t size, volatile void* target, volatile void* value, int) { + LockFlag(flag); + auto* object = static_cast(const_cast(target)); + auto* other = static_cast(const_cast(value)); + for (std::size_t i = 0; i < size; ++i) { + const unsigned char byte = object[i]; + object[i] = other[i]; + other[i] = byte; + } + UnlockFlag(flag); +} + +int APS5_VABI _Atomic_compare_exchange_strong_nid_postfix(volatile unsigned char* flag, std::size_t size, volatile void* target, void* expected, const void* desired, int, int) { + return LockedCompareExchange(flag, size, target, expected, desired); +} + +int APS5_VABI _Atomic_compare_exchange_weak_nid_postfix(volatile unsigned char* flag, std::size_t size, volatile void* target, void* expected, const void* desired, int, int) { + return LockedCompareExchange(flag, size, target, expected, desired); +} + +} diff --git a/core/libs/prx/libc/src/DinkumwareInternals.cpp b/core/libs/prx/libc/src/DinkumwareInternals.cpp new file mode 100644 index 0000000000..163a7362fa --- /dev/null +++ b/core/libs/prx/libc/src/DinkumwareInternals.cpp @@ -0,0 +1,96 @@ +#include +#include +#include +#include +#include +#include + +#include "prx/libc/include/General.hpp" + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +double APS5_VABI strtod_nid_postfix(const char* str, char** endptr); +long long APS5_VABI strtoll_nid_postfix(const char* str, char** endptr, int base); +double APS5_VABI sin_nid_postfix(double x); +double APS5_VABI cos_nid_postfix(double x); +float APS5_VABI sinf_nid_postfix(float x); +float APS5_VABI cosf_nid_postfix(float x); +long double APS5_VABI sinl_nid_postfix(long double x); +long double APS5_VABI cosl_nid_postfix(long double x); +double APS5_VABI log_nid_postfix(double x); +double APS5_VABI log10_nid_postfix(double x); +double APS5_VABI log2_nid_postfix(double x); +float APS5_VABI logf_nid_postfix(float x); +float APS5_VABI log10f_nid_postfix(float x); +float APS5_VABI log2f_nid_postfix(float x); +} + +namespace { + +constexpr long long ExponentLimit = 1000000000; +constexpr int RangeError = 34; + +bool Hexadecimal(const char* text) { + while (std::isspace(static_cast(*text))) ++text; + if (*text == '+' || *text == '-') ++text; + return text[0] == '0' && (text[1] == 'x' || text[1] == 'X'); +} + +} + +extern "C" { + +double APS5_VABI _Stod_nid_postfix(const char* text, char** end, std::int64_t pten) { + int& guestErrno = *__error_nid_postfix(); + const int saved = guestErrno; + guestErrno = 0; + char* stop = nullptr; + const double value = strtod_nid_postfix(text, &stop); + if (end != nullptr) *end = stop; + const bool overflowed = std::isinf(value) && guestErrno == RangeError; + if (pten == 0 || stop == text || !(std::isfinite(value) || overflowed)) { + if (guestErrno == 0) guestErrno = saved; + return value; + } + if (Hexadecimal(text)) NotImplemented_nid_no_patch("_Stod (hexadecimal text with a power-of-ten scale)"); + std::string number(text, static_cast(stop)); + long long exponent = 0; + const auto marker = number.find_first_of("eE"); + if (marker != std::string::npos) { + exponent = std::strtoll(number.c_str() + marker + 1, nullptr, 10); + number.resize(marker); + } + exponent = std::clamp(exponent, -ExponentLimit, ExponentLimit) + std::clamp(pten, -ExponentLimit, ExponentLimit); + number += 'e'; + number += std::to_string(exponent); + guestErrno = saved; + return strtod_nid_postfix(number.c_str(), nullptr); +} + +long long APS5_VABI _Stoll_nid_postfix(const char* text, char** end, int base) { + return strtoll_nid_postfix(text, end, base); +} + +double APS5_VABI _Sin_nid_postfix(double x, unsigned int quadrant) { + return quadrant != 0 ? cos_nid_postfix(x) : sin_nid_postfix(x); +} + +float APS5_VABI _FSin_nid_postfix(float x, unsigned int quadrant) { + return quadrant != 0 ? cosf_nid_postfix(x) : sinf_nid_postfix(x); +} + +long double APS5_VABI _LSin_nid_postfix(long double x, unsigned int quadrant) { + return quadrant != 0 ? cosl_nid_postfix(x) : sinl_nid_postfix(x); +} + +double APS5_VABI _Log_nid_postfix(double x, int base) { + if (base > 0) return log10_nid_postfix(x); + return base == 0 ? log_nid_postfix(x) : log2_nid_postfix(x); +} + +float APS5_VABI _FLog_nid_postfix(float x, int base) { + if (base > 0) return log10f_nid_postfix(x); + return base == 0 ? logf_nid_postfix(x) : log2f_nid_postfix(x); +} + +} diff --git a/core/libs/prx/libc/src/Directory.cpp b/core/libs/prx/libc/src/Directory.cpp index ff24e15a7e..425669cdcf 100644 --- a/core/libs/prx/libc/src/Directory.cpp +++ b/core/libs/prx/libc/src/Directory.cpp @@ -1,12 +1,20 @@ #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 #include #include #include #include +#include +#ifdef _WIN32 +#include "prx/libc/include/WindowsDescriptor.hpp" +#include +#include +#endif namespace { int DirectoryError(int error) { @@ -26,17 +34,102 @@ int DirectoryError(int error) { } } #ifdef _WIN32 -using NativeDirectory = _WDIR; -NativeDirectory* OpenNative(const std::filesystem::path& path) { return ::_wopendir(path.c_str()); } -int CloseNative(NativeDirectory* native) { return ::_wclosedir(native); } -void RewindNative(NativeDirectory* native) { ::_wrewinddir(native); } +struct NativeDirectory { + HANDLE handle = INVALID_HANDLE_VALUE; + std::uint32_t seed = 0; + bool identifiers = true; + bool restart = true; + bool buffered = false; + std::size_t next = 0; + long position = 0; + alignas(8) std::byte buffer[65536]; + ~NativeDirectory() { if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle); } +}; +int NativeOpenError(DWORD error) { + if (error == ERROR_CANT_RESOLVE_FILENAME) return ELOOP; + if (error == ERROR_DIRECTORY) return ENOTDIR; + return WindowsOpenError_nid_no_patch(error); +} +NativeDirectory* OpenNative(const std::filesystem::path& path) { + const DWORD attributes = GetFileAttributesW(path.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES) { errno = NativeOpenError(GetLastError()); return nullptr; } + if ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { errno = ENOTDIR; return nullptr; } + auto native = std::make_unique_for_overwrite(); + native->handle = CreateFileW(path.c_str(), FILE_LIST_DIRECTORY | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (native->handle == INVALID_HANDLE_VALUE) { errno = NativeOpenError(GetLastError()); return nullptr; } + BY_HANDLE_FILE_INFORMATION identity{}; + if (GetFileInformationByHandle(native->handle, &identity)) { + if ((identity.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { errno = ENOTDIR; return nullptr; } + native->seed = WindowsFileNumber_nid_no_patch(identity); + } + return native.release(); +} +int CloseNative(NativeDirectory* native) { delete native; return 0; } +void RewindNative(NativeDirectory* native) { + native->restart = true; + native->buffered = false; + native->position = 0; +} +bool QueryNative(NativeDirectory* native) { + const auto query = [native](FILE_INFO_BY_HANDLE_CLASS restartClass, FILE_INFO_BY_HANDLE_CLASS nextClass) { + return GetFileInformationByHandleEx(native->handle, native->restart ? restartClass : nextClass, native->buffer, sizeof(native->buffer)) != 0; + }; + bool filled = native->identifiers ? query(FileIdBothDirectoryRestartInfo, FileIdBothDirectoryInfo) : query(FileFullDirectoryRestartInfo, FileFullDirectoryInfo); + DWORD error = filled ? ERROR_SUCCESS : GetLastError(); + if (!filled && native->restart && native->identifiers && (error == ERROR_INVALID_PARAMETER || error == ERROR_INVALID_FUNCTION || + error == ERROR_INVALID_LEVEL || error == ERROR_NOT_SUPPORTED)) { + native->identifiers = false; + filled = query(FileFullDirectoryRestartInfo, FileFullDirectoryInfo); + error = filled ? ERROR_SUCCESS : GetLastError(); + } + if (!filled) { + if (error != ERROR_NO_MORE_FILES && error != ERROR_FILE_NOT_FOUND) errno = EIO; + return false; + } + native->restart = false; + native->buffered = true; + native->next = 0; + return true; +} +const std::byte* NextRecord(NativeDirectory* native) { + if (!native->buffered && !QueryNative(native)) return nullptr; + const std::byte* record = native->buffer + native->next; + const DWORD offset = native->identifiers ? reinterpret_cast(record)->NextEntryOffset + : reinterpret_cast(record)->NextEntryOffset; + if (offset == 0) native->buffered = false; + native->next += offset; + ++native->position; + return record; +} +std::uint32_t StableFileNumber(std::uint32_t seed, std::wstring_view name) { + std::uint32_t hash = 2166136261u ^ seed; + for (const wchar_t character : name) hash = (hash ^ static_cast(character)) * 16777619u; + return hash == 0 ? 1 : hash; +} +long TellNative(NativeDirectory* native) { return native->position; } +void SeekNative(NativeDirectory* native, long location) { + if (location < 0) return; + RewindNative(native); + while (native->position < location && NextRecord(native)) {} +} bool ReadNative(NativeDirectory* native, std::filesystem::path& component, std::string& name, std::uint64_t& inode) { - auto* entry = ::_wreaddir(native); - if (!entry) return false; - component = entry->d_name; + const std::byte* record = NextRecord(native); + if (!record) return false; + std::wstring_view fileName; + std::uint64_t fileId = 0; + if (native->identifiers) { + const auto* information = reinterpret_cast(record); + fileName = std::wstring_view(information->FileName, information->FileNameLength / sizeof(WCHAR)); + fileId = static_cast(information->FileId.QuadPart); + } else { + const auto* information = reinterpret_cast(record); + fileName = std::wstring_view(information->FileName, information->FileNameLength / sizeof(WCHAR)); + } + component = fileName; const auto utf8 = component.u8string(); name.assign(utf8.begin(), utf8.end()); - inode = static_cast(entry->d_ino); + inode = fileId != 0 ? WindowsFileNumber_nid_no_patch(fileId) : StableFileNumber(native->seed, fileName); return true; } #else @@ -44,6 +137,8 @@ using NativeDirectory = DIR; NativeDirectory* OpenNative(const std::filesystem::path& path) { return ::opendir(path.c_str()); } int CloseNative(NativeDirectory* native) { return ::closedir(native); } void RewindNative(NativeDirectory* native) { ::rewinddir(native); } +long TellNative(NativeDirectory* native) { return ::telldir(native); } +void SeekNative(NativeDirectory* native, long location) { ::seekdir(native, location); } bool ReadNative(NativeDirectory* native, std::filesystem::path& component, std::string& name, std::uint64_t& inode) { auto* entry = ::readdir(native); if (!entry) return false; @@ -81,16 +176,17 @@ GuestDirectoryEntry* ReadEntry(Directory& directory) { std::filesystem::path component; std::string name; std::uint64_t inode = 0; - if (!ReadNative(directory.native, component, name, inode)) { - errno = errno ? DirectoryError(errno) : savedError; - return nullptr; - } + try { + if (!ReadNative(directory.native, component, name, inode)) { + errno = errno ? DirectoryError(errno) : savedError; + return nullptr; + } + } catch (const std::bad_alloc&) { errno = 12; return nullptr; } const auto length = name.size(); if (length > 255) { errno = 63; return nullptr; } try { directory.entry = {}; - // MinGW reports no inode identity (zero); do not fabricate one. - directory.entry.fileNumber = inode <= UINT32_MAX ? static_cast(inode) : 0; + directory.entry.fileNumber = static_cast(inode); directory.entry.recordLength = static_cast(8 + ((length + 1 + 3) & ~3)); directory.entry.nameLength = static_cast(length); directory.entry.type = DirectoryType(directory.path / component); @@ -109,8 +205,13 @@ 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; } + if (!directory->native) { + errno = DirectoryError(errno); + if (errno == 2) errno = NotFoundError_nid_no_patch(directory->path); + return nullptr; + } return directory.release(); } catch (const std::bad_alloc&) { errno = 12; return nullptr; } catch (const std::filesystem::filesystem_error&) { errno = 5; return nullptr; } @@ -158,12 +259,40 @@ void APS5_VABI rewinddir_nid_postfix(void* handle) { RewindNative(directory.native); } -void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int)) { +std::int64_t APS5_VABI telldir_nid_postfix(void* handle) { + if (!handle) { errno = 9; return -1; } + auto& directory = *static_cast(handle); + std::lock_guard lock(directory.mutex); + return TellNative(directory.native); +} + +void APS5_VABI seekdir_nid_postfix(void* handle, std::int64_t location) { + if (!handle) { errno = 9; return; } + auto& directory = *static_cast(handle); + std::lock_guard lock(directory.mutex); + SeekNative(directory.native, static_cast(location)); +} + +void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int), std::uint64_t position) { try { auto directory = std::make_unique(); directory->path = std::filesystem::path(reinterpret_cast(hostPath)); +#ifdef _WIN32 directory->native = OpenNative(directory->path); if (!directory->native) { errno = DirectoryError(errno); return nullptr; } + std::filesystem::path component; + std::string name; + std::uint64_t inode = 0; + for (std::uint64_t skipped = 0; skipped < position; ++skipped) { + if (!ReadNative(directory->native, component, name, inode)) break; + } +#else + (void)position; + const int duplicate = ::dup(descriptor); + if (duplicate < 0) { errno = DirectoryError(errno); return nullptr; } + directory->native = ::fdopendir(duplicate); + if (!directory->native) { const int error = errno; ::close(duplicate); errno = DirectoryError(error); return nullptr; } +#endif directory->descriptor = descriptor; directory->closeDescriptor = closeDescriptor; return directory.release(); diff --git a/core/libs/prx/libc/src/Environment.cpp b/core/libs/prx/libc/src/Environment.cpp index 03996644bf..f77b8727f9 100644 --- a/core/libs/prx/libc/src/Environment.cpp +++ b/core/libs/prx/libc/src/Environment.cpp @@ -7,12 +7,9 @@ #include #include #include +#include "prx/libc/include/General.hpp" #include "prx/libc/include/general/VabiMacros.hpp" -#ifndef _WIN32 -extern char** environ; -#endif - namespace { struct EnvironmentEntry { std::unique_ptr owned; @@ -26,20 +23,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) { @@ -49,6 +33,17 @@ int EnvironmentError(int error) { errno = error; return -1; } } extern "C" { +bool CopyGuestEnvironment_nid_no_patch(const char* name, std::string& value) { + if (!ValidEnvironmentName(name)) return false; + std::lock_guard lock(environmentMutex); + for (auto& entry : Environment()) + if (entry.Matches(name)) { + value = entry.Data() + std::strlen(name) + 1; + return true; + } + return false; +} + char* APS5_VABI getenv_nid_postfix(const char* name) { if (!ValidEnvironmentName(name)) { errno = 22; return nullptr; } try { diff --git a/core/libs/prx/libc/src/Errors.cpp b/core/libs/prx/libc/src/Errors.cpp index 56a77382bb..4e2d3f08bb 100644 --- a/core/libs/prx/libc/src/Errors.cpp +++ b/core/libs/prx/libc/src/Errors.cpp @@ -1,3 +1,4 @@ +#include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -76,8 +77,9 @@ void APS5_VABI perror_nid_postfix(const char* prefix) { const int saved = *__error_nid_postfix(); char message[128]; strerror_r_nid_postfix(saved, message, sizeof(message)); - if (prefix && *prefix) std::fprintf(stderr, "%s: %s\n", prefix, message); - else std::fprintf(stderr, "%s\n", message); + auto* stream = GetNativeStream(&_Stderr_nid_postfix); + if (prefix && *prefix) std::fprintf(stream, "%s: %s\n", prefix, message); + else std::fprintf(stream, "%s\n", message); *__error_nid_postfix() = saved; } } diff --git a/core/libs/prx/libc/src/FailureReport.cpp b/core/libs/prx/libc/src/FailureReport.cpp new file mode 100644 index 0000000000..9fcb5473de --- /dev/null +++ b/core/libs/prx/libc/src/FailureReport.cpp @@ -0,0 +1,39 @@ +#include "prx/libc/include/FailureReport.hpp" +#include +#include +#include + +namespace { + +using Report = void (*)() noexcept; + +std::array, 8> reports{}; +std::atomic ran{false}; + +} + +extern "C" void LibcRegisterFailureReport_nid_no_patch(Report report) { + if (report == nullptr) throw std::invalid_argument("libc: null failure report"); + for (auto& slot : reports) { + if (slot.load(std::memory_order_acquire) == report) return; + } + for (auto& slot : reports) { + Report empty = nullptr; + if (slot.compare_exchange_strong(empty, report, std::memory_order_acq_rel)) return; + } + throw std::runtime_error("libc: more than 8 failure reports registered"); +} + +extern "C" void LibcUnregisterFailureReport_nid_no_patch(Report report) noexcept { + for (auto& slot : reports) { + Report expected = report; + slot.compare_exchange_strong(expected, nullptr, std::memory_order_acq_rel); + } +} + +extern "C" void LibcRunFailureReports_nid_no_patch() noexcept { + if (ran.exchange(true, std::memory_order_acq_rel)) return; + for (auto& slot : reports) { + if (const auto report = slot.load(std::memory_order_acquire)) report(); + } +} diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 69e7f8d0fe..6187b36c04 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -22,11 +23,15 @@ #include "prx/libc/include/WindowsDescriptor.hpp" #endif +#include "prx/libc/include/SonyCrt.hpp" #include "prx/libc/include/FileStream.hpp" #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" +#include "prx/libc/include/HostEntropy.hpp" #ifdef __linux__ #include @@ -58,15 +63,35 @@ static std::optional ParseFileMode(const char* mode) { return result; } +#ifdef _WIN32 +static constexpr DWORD AppendOnlyAccess = FILE_GENERIC_WRITE & ~FILE_WRITE_DATA; + +static std::optional HostOpenName(const std::filesystem::path& path) { + 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 std::nullopt; } + 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; + } + return filename; +} +#endif + static std::FILE* OpenNative(const std::filesystem::path& path, const std::string& nativeMode) { #ifdef _WIN32 const char access = nativeMode[0]; const bool update = nativeMode.find('+') != std::string::npos; - const DWORD desired = update ? GENERIC_READ | GENERIC_WRITE : access == 'r' ? GENERIC_READ : GENERIC_WRITE; + const DWORD writes = access == 'a' ? AppendOnlyAccess : GENERIC_WRITE; + const DWORD desired = update ? GENERIC_READ | writes : access == 'r' ? GENERIC_READ : writes; 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, + const auto filename = HostOpenName(path); + if (!filename) return nullptr; + 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()); @@ -89,14 +114,11 @@ static std::FILE* OpenNative(const std::filesystem::path& path, const std::strin } #ifdef _WIN32 -static bool AppendThroughDescriptor(int descriptor) { - const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); - if (handle == INVALID_HANDLE_VALUE) return false; - if (descriptor <= 2 || GetFileType(handle) != FILE_TYPE_DISK) return true; +static bool MoveAppendingHandle(int descriptor, HANDLE handle, DWORD access, DWORD options) { const int translation = ::_setmode(descriptor, _O_BINARY); if (translation == -1 || ::_setmode(descriptor, translation) == -1) return false; HANDLE copy = nullptr; - if (!DuplicateHandle(GetCurrentProcess(), handle, GetCurrentProcess(), ©, 0, TRUE, DUPLICATE_SAME_ACCESS)) { + if (!DuplicateHandle(GetCurrentProcess(), handle, GetCurrentProcess(), ©, access, TRUE, options)) { errno = EBADF; return false; } @@ -105,19 +127,38 @@ static bool AppendThroughDescriptor(int descriptor) { CloseHandle(copy); return false; } + auto* const standard = HostStandardHandle_nid_no_patch(descriptor); const int moved = ::_dup2(appending, descriptor); const int error = errno; ::_close(appending); + KeepStandardHandle_nid_no_patch(descriptor, standard); errno = error; return moved == 0; } +static bool AppendThroughDescriptor(int descriptor) { + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + if (handle == INVALID_HANDLE_VALUE) return false; + if (descriptor <= 2 || GetFileType(handle) != FILE_TYPE_DISK) return true; + return MoveAppendingHandle(descriptor, handle, 0, DUPLICATE_SAME_ACCESS); +} + +static bool AppendOnlyDescriptor(int descriptor) { + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + if (handle == INVALID_HANDLE_VALUE) return false; + if (GetFileType(handle) != FILE_TYPE_DISK) return true; + const ACCESS_MASK access = WindowsHandleAccess_nid_no_patch(handle, descriptor, "freopen"); + if (WindowsHandleOnlyAppends_nid_no_patch(access)) return true; + return MoveAppendingHandle(descriptor, handle, ((access & FILE_READ_DATA) != 0 ? FILE_GENERIC_READ : 0) | AppendOnlyAccess, 0); +} + static bool IsPipeStream(std::FILE* native) { return GetFileType(reinterpret_cast(::_get_osfhandle(_fileno(native)))) == FILE_TYPE_PIPE; } #endif using GuestCompare = int (APS5_VABI *)(const void*, const void*); +using GuestContextCompare = int (APS5_VABI *)(const void*, const void*, void*); static void SwapElements(unsigned char* left, unsigned char* right, size_t size) { for (size_t index = 0; index < size; ++index) std::swap(left[index], right[index]); @@ -127,19 +168,22 @@ static void SwapRanges(unsigned char* left, unsigned char* right, size_t bytes) if (bytes > 0) SwapElements(left, right, bytes); } -static unsigned char* MedianOfThree(unsigned char* a, unsigned char* b, unsigned char* c, GuestCompare compare) { +template +static unsigned char* MedianOfThree(unsigned char* a, unsigned char* b, unsigned char* c, Compare compare) { return compare(a, b) < 0 ? (compare(b, c) < 0 ? b : (compare(a, c) < 0 ? c : a)) : (compare(b, c) > 0 ? b : (compare(a, c) < 0 ? a : c)); } -static void InsertionSort(unsigned char* base, size_t count, size_t size, GuestCompare compare) { +template +static void InsertionSort(unsigned char* base, size_t count, size_t size, Compare compare) { for (unsigned char* next = base + size; next < base + count * size; next += size) for (unsigned char* element = next; element > base && compare(element - size, element) > 0; element -= size) SwapElements(element, element - size, size); } -static void SortElements(unsigned char* base, size_t count, size_t size, GuestCompare compare) { +template +static void SortElements(unsigned char* base, size_t count, size_t size, Compare compare) { for (;;) { if (count < 7) { InsertionSort(base, count, size, compare); @@ -237,6 +281,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 { @@ -262,6 +307,53 @@ FileStream* APS5_VABI fdopen_nid_postfix(int descriptor, const char* mode) { } catch (const std::bad_alloc&) { errno = 12; return nullptr; } } +static int OpenFailure(const std::filesystem::path& path, bool writes, int hostError) { + const int error = GuestErrnoFromHost_nid_no_patch(hostError); + std::error_code status; + return writes && error == 13 && std::filesystem::is_directory(path, status) ? 21 : error; +} + +#ifdef _WIN32 +static int (*g_openDirectoryDescriptor)(const wchar_t*) = nullptr; +static void (*g_forgetDirectoryDescriptor)(int) = nullptr; + +void LibcRegisterDirectoryStreams_nid_no_patch(int (*open)(const wchar_t*), void (*forget)(int)) { + g_openDirectoryDescriptor = open; + g_forgetDirectoryDescriptor = forget; +} + +void ForgetDirectoryStream_nid_no_patch(int descriptor) { + if (!g_forgetDirectoryDescriptor) throw std::runtime_error("ForgetDirectoryStream: libkernel has not registered its directory descriptors"); + g_forgetDirectoryDescriptor(descriptor); +} + +static FileStream* OpenDirectoryStream(const std::filesystem::path& path) { + if (!g_openDirectoryDescriptor) throw std::runtime_error("fopen: libkernel has not registered its directory descriptors"); + const auto filename = HostOpenName(path); + if (!filename) { errno = GuestErrnoFromHost_nid_no_patch(errno); return nullptr; } + const auto release = [](void* pointer) { ::operator delete(pointer); }; + std::unique_ptr storage(::operator new(sizeof(FileStream)), release); + const int descriptor = g_openDirectoryDescriptor(filename->c_str()); + if (descriptor < 0) { + const int error = GuestErrnoFromHost_nid_no_patch(errno); + errno = error == 2 ? NotFoundError_nid_no_patch(path) : error; + return nullptr; + } + auto* native = ::_fdopen(descriptor, "rb"); + if (!native) { + const int error = GuestErrnoFromHost_nid_no_patch(errno); + ForgetDirectoryStream_nid_no_patch(descriptor); + ::_close(descriptor); + errno = error; + return nullptr; + } + auto* stream = new (storage.get()) FileStream(native, true); + storage.release(); + stream->MarkDirectory(); + return stream; +} +#endif + FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode, FileStream* stream) { if (!stream || !mode) { errno = 22; return nullptr; } if (!filename) { errno = 45; return nullptr; } // Mode-only reopening is not supported. @@ -271,18 +363,29 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode std::filesystem::path path; if (*filename) { if (const int error = ResolvePathError_nid_no_patch(filename, path)) { - stream->Close(); + if (stream->IsHostStandard()) stream->Detach(); + else stream->Close(); if (stream->IsDynamic()) delete stream; errno = error; return nullptr; } } - if (stream->Reopen(path, parsed->native.c_str())) { + if (stream->Reopen(path, parsed->native.c_str(), OpenNative)) { +#ifdef _WIN32 + if (parsed->append && !AppendOnlyDescriptor(_fileno(stream->GetHandle()))) { + const int error = errno; + stream->Close(); + if (stream->IsDynamic()) delete stream; + errno = error; + return nullptr; + } +#endif if (parsed->append) static_cast(std::fseek(stream->GetHandle(), 0, SEEK_END)); if (!path.empty() && parsed->writes) RecordWrittenPath_nid_no_patch(path); return stream; } - const int error = errno; + int error = OpenFailure(path, parsed->writes, errno); + if (error == 2) error = NotFoundError_nid_no_patch(path); if (stream->IsDynamic()) delete stream; errno = error; return nullptr; @@ -297,12 +400,31 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) errno = EINVAL; return nullptr; } + if (const std::string_view path(filename); path == "/dev/random" || path == "/dev/urandom") { + if (parsed->writes) throw std::runtime_error(std::string(__func__) + ": writing to " + filename + " is not implemented"); +#ifdef _WIN32 + std::unique_ptr device(std::fopen("NUL", "rb"), std::fclose); +#else + std::unique_ptr device(std::fopen("/dev/null", "rb"), std::fclose); +#endif + if (!device) return nullptr; + auto stream = std::make_unique(device.get(), true); + device.release(); + stream->MarkRandomDevice(); + return stream.release(); + } std::filesystem::path fpath; 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; + const int hostError = errno; +#ifdef _WIN32 + std::error_code status; + if (!parsed->writes && GuestErrnoFromHost_nid_no_patch(hostError) == 13 && std::filesystem::is_directory(fpath, status)) + return OpenDirectoryStream(fpath); +#endif + errno = OpenFailure(fpath, parsed->writes, hostError); + if (errno == 2) errno = NotFoundError_nid_no_patch(fpath); return nullptr; } // APS5_LOG_OUT("success: \"%s\"", abs_path.c_str()); @@ -315,16 +437,20 @@ 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) { - GetNativeStream(stream); + if (stream && !stream->IsOpen()) { + std::unique_ptr owner(stream->IsDynamic() ? stream : nullptr); + errno = 9; + return -1; + } + GetStreamHandle(stream); std::unique_ptr owner(stream->IsDynamic() ? stream : nullptr); return stream->Close(); } @@ -339,17 +465,15 @@ FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const {In, "r"}, {Out, "w"}, {Out | Trunc, "w"}, {Out | App, "a"}, {In | Binary, "rb"}, {Out | Binary, "wb"}, {Out | Trunc | Binary, "wb"}, {Out | App | Binary, "ab"}, {In | Out, "r+"}, {In | Out | Trunc, "w+"}, {In | Out | App, "a+"}, - {In | Out | Binary, "r+b"}, {In | Out | Trunc | Binary, "w+b"}, {In | Out | App | Binary, "a+b"}}; - const int open = (mode & (In | Out | App | Trunc | Binary)) | ((mode & Nocreate) ? In : 0) | ((mode & App) ? Out : 0); + {In | Out | Binary, "r+b"}, {In | Out | Trunc | Binary, "w+b"}, {In | Out | App | Binary, "a+b"}, + {Out | Noreplace, "wx"}, {Out | Trunc | Noreplace, "wx"}, {In | Out | Trunc | Noreplace, "w+x"}, + {Out | Binary | Noreplace, "wbx"}, {Out | Trunc | Binary | Noreplace, "wbx"}, + {In | Out | Trunc | Binary | Noreplace, "w+bx"}}; + const int open = (mode & (In | Out | App | Trunc | Binary)) | ((mode & Nocreate) ? In : 0) | ((mode & App) ? Out : 0) | + (((mode & Noreplace) && (mode & (Out | App))) ? Noreplace : 0); const char* openMode = nullptr; for (const auto& [flags, text] : modes) if (flags == open) openMode = text; if (!openMode) return nullptr; - if ((mode & Noreplace) && (open & Out)) { - if (auto* existing = fopen_nid_postfix(filename, "r")) { - fclose_nid_postfix(existing); - return nullptr; - } - } auto* stream = fopen_nid_postfix(filename, openMode); if (!stream || !(mode & Ate) || fseek_nid_postfix(stream, 0, SEEK_END) == 0) return stream; fclose_nid_postfix(stream); @@ -357,29 +481,43 @@ FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const } size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, FileStream* stream) { - auto* handle = GetNativeStream(stream); + if (size != 0 && count > std::numeric_limits::max() / size) { errno = EINVAL; return 0; } + if (stream && !stream->IsOpen()) { errno = 9; return 0; } + auto* handle = GetStreamHandle(stream); if (size == 0 || count == 0) return 0; if (!buffer) throw std::runtime_error("fread: null buffer"); - if (count > std::numeric_limits::max() / size) throw std::overflow_error("fread: buffer size overflow"); const auto bytes = size * count; if (bytes > std::numeric_limits::max() - reinterpret_cast(buffer)) throw std::overflow_error("fread: buffer address overflow"); const GuestArena::HostWrite destination(buffer, bytes); if (!destination.Open()) throw std::runtime_error("fread: buffer is not writable"); + if (stream->IsRandomDevice()) { + HostEntropy_nid_no_patch(buffer, bytes); + stream->SyncStatus(); + return count; + } + if (!stream->SwitchToReading()) return 0; const auto result = std::fread(buffer, size, count, handle); stream->SyncStatus(); + stream->KeepDirectoryReadError(result < count); return result; } size_t APS5_VABI fwrite_nid_postfix(const void* buffer, size_t size, size_t count, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return 0; } auto* handle = GetNativeStream(stream); if (size == 0 || count == 0) return 0; if (!buffer) throw std::runtime_error("fwrite: null buffer"); + if (count > std::numeric_limits::max() / size) throw std::overflow_error("fwrite: buffer size overflow"); + const auto bytes = size * count; + if (bytes > std::numeric_limits::max() - reinterpret_cast(buffer)) throw std::overflow_error("fwrite: buffer address overflow"); + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buffer, bytes); const auto result = std::fwrite(buffer, size, count, handle); stream->SyncStatus(); return result; } int APS5_VABI fseeko_nid_postfix(FileStream* stream, std::int64_t offset, int origin) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } if (origin != SEEK_SET && origin != SEEK_CUR && origin != SEEK_END) { errno = 22; return -1; } #ifdef _WIN32 auto* native = GetNativeStream(stream); @@ -396,6 +534,7 @@ int APS5_VABI fseeko_nid_postfix(FileStream* stream, std::int64_t offset, int or } std::int64_t APS5_VABI ftello_nid_postfix(FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } #ifdef _WIN32 auto* native = GetNativeStream(stream); if (IsPipeStream(native)) { errno = 29; return -1; } @@ -435,13 +574,16 @@ void APS5_VABI rewind_nid_postfix(FileStream* stream) { int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream) { if (!str) throw std::runtime_error("fputs: null string"); + if (stream && !stream->IsOpen()) { errno = 9; return -1; } const int result = std::fputs(str, GetNativeStream(stream)); stream->SyncStatus(); return result; } int APS5_VABI fflush_nid_postfix(FileStream* stream) { - std::FILE* native = stream ? GetNativeStream(stream) : nullptr; + if (stream && !stream->IsOpen()) { errno = 9; return -1; } + std::FILE* native = stream ? GetStreamHandle(stream) : nullptr; + if (stream && (stream->IsRandomDevice() || stream->IsDirectory())) return 0; #ifdef __linux__ if (native && !__fwriting(native)) return 0; #endif @@ -534,6 +676,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"); @@ -545,4 +720,20 @@ void APS5_VABI qsort_nid_postfix(void* base, size_t count, size_t size, int (APS SortElements(static_cast(base), count, size, compare); } +int APS5_VABI qsort_s_nid_postfix(void* base, size_t count, size_t size, GuestContextCompare compare, void* context) { + constexpr size_t RsizeMax = std::numeric_limits::max() >> 1; + const char* const violation = count > RsizeMax ? "qsort_s : n > RSIZE_MAX" + : size > RsizeMax ? "qsort_s : es > RSIZE_MAX" + : count != 0 && !base ? "qsort_s : a == NULL" + : count != 0 && !compare ? "qsort_s : cmp == NULL" + : nullptr; + if (violation) { + SonyInvokeConstraintHandler(violation, nullptr, 22); + return 22; + } + if (count > 1 && size != 0) + SortElements(static_cast(base), count, size, [compare, context](const void* left, const void* right) { return compare(left, right, context); }); + return 0; +} + } diff --git a/core/libs/prx/libc/src/FileLock.cpp b/core/libs/prx/libc/src/FileLock.cpp index 40efecd962..1c088bb0d1 100644 --- a/core/libs/prx/libc/src/FileLock.cpp +++ b/core/libs/prx/libc/src/FileLock.cpp @@ -9,6 +9,8 @@ #include #include +#include "prx/libc/include/WindowsDescriptor.hpp" + namespace { constexpr int LockShared = 1; @@ -21,6 +23,14 @@ constexpr DWORD LockByteHigh = 0x7FFFFFFF; struct LockState { std::mutex mutex; int mode = 0; + HANDLE locker = nullptr; + + LockState() = default; + LockState(const LockState&) = delete; + LockState& operator=(const LockState&) = delete; + ~LockState() { + if (locker != nullptr) ::CloseHandle(locker); + } }; std::mutex g_mutex; @@ -59,16 +69,21 @@ int GuestFileLock_nid_no_patch(int fd, int operation) { OVERLAPPED overlapped{}; overlapped.Offset = LockByteLow; overlapped.OffsetHigh = LockByteHigh; - if (!::UnlockFileEx(handle, 0, 1, 0, &overlapped)) return -1; + if (!::UnlockFileEx(state->locker != nullptr ? state->locker : handle, 0, 1, 0, &overlapped)) return -1; state->mode = 0; } if (mode == LockUnlock) return 0; + if (state->locker == nullptr && (WindowsHandleAccess_nid_no_patch(handle, fd, "flock") & (FILE_READ_DATA | FILE_WRITE_DATA)) == 0) { + const auto locker = ::ReOpenFile(handle, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0); + if (locker == INVALID_HANDLE_VALUE) return -1; + state->locker = locker; + } DWORD flags = mode == LockExclusive ? LOCKFILE_EXCLUSIVE_LOCK : 0; if (operation & LockNonblocking) flags |= LOCKFILE_FAIL_IMMEDIATELY; OVERLAPPED overlapped{}; overlapped.Offset = LockByteLow; overlapped.OffsetHigh = LockByteHigh; - if (!::LockFileEx(handle, flags, 0, 1, 0, &overlapped)) return -1; + if (!::LockFileEx(state->locker != nullptr ? state->locker : handle, flags, 0, 1, 0, &overlapped)) return -1; state->mode = mode; return 0; } diff --git a/core/libs/prx/libc/src/Filesystem.cpp b/core/libs/prx/libc/src/Filesystem.cpp index cff29e87ec..510477eb7a 100644 --- a/core/libs/prx/libc/src/Filesystem.cpp +++ b/core/libs/prx/libc/src/Filesystem.cpp @@ -12,8 +12,8 @@ #endif extern "C" int APS5_VABI access_nid_postfix(const char* path, int mode) { - if (!path) { errno = 14; return -1; } if (mode < 0 || (mode & ~7)) { errno = 22; return -1; } + if (!path) { errno = 14; return -1; } if (!*path) { errno = 2; return -1; } try { const auto resolved = ResolvePath_nid_no_patch(path); @@ -21,6 +21,7 @@ extern "C" int APS5_VABI access_nid_postfix(const char* path, int mode) { const DWORD attributes = GetFileAttributesW(resolved.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES) { errno = FilesystemError(std::error_code(static_cast(GetLastError()), std::system_category())); + if (errno == 2) errno = NotFoundError_nid_no_patch(resolved); return -1; } if (mode == 0) return 0; @@ -110,6 +111,7 @@ extern "C" int APS5_VABI remove_nid_postfix(const char* path) { #endif if (error || !removed) { errno = error ? FilesystemError(error) : 2; + if (errno == 2) errno = NotFoundError_nid_no_patch(resolved); return -1; } RecordWrittenPath_nid_no_patch(resolved); diff --git a/core/libs/prx/libc/src/FloatBuiltins.cpp b/core/libs/prx/libc/src/FloatBuiltins.cpp new file mode 100644 index 0000000000..b059652259 --- /dev/null +++ b/core/libs/prx/libc/src/FloatBuiltins.cpp @@ -0,0 +1,584 @@ +#include +#include +#include +#include +#include + +#include "prx/libc/include/General.hpp" + +namespace { + +using Int128 = __int128; +using Uint128 = unsigned __int128; + +constexpr std::uint32_t DefaultMxcsr = 0x1f80; + +template +struct FloatFormat; + +template<> +struct FloatFormat { + using Rep = std::uint32_t; + static constexpr int SignificandBits = 23; + static constexpr int Bias = 127; + static constexpr int MaxBiased = 0xff; +}; + +template<> +struct FloatFormat { + using Rep = std::uint64_t; + static constexpr int SignificandBits = 52; + static constexpr int Bias = 1023; + static constexpr int MaxBiased = 0x7ff; +}; + +template +struct Decomposed { + bool negative; + int exponent; + typename FloatFormat::Rep significand; +}; + +template +Decomposed Decompose(TFloat value) { + using Format = FloatFormat; + using Rep = typename Format::Rep; + const Rep bits = std::bit_cast(value); + const Rep implicitBit = static_cast(1) << Format::SignificandBits; + const int biased = static_cast((bits >> Format::SignificandBits) & Format::MaxBiased); + return {(bits >> (sizeof(Rep) * 8 - 1)) != 0, biased - Format::Bias, (bits & (implicitBit - 1)) | implicitBit}; +} + +struct ExtendedBits { + std::uint64_t mantissa; + bool negative; + int exponent; +}; + +ExtendedBits DecomposeExtended(long double value) { + unsigned char bytes[sizeof(long double)]; + std::memcpy(bytes, &value, sizeof(bytes)); + std::uint64_t mantissa; + std::uint16_t signExponent; + std::memcpy(&mantissa, bytes, sizeof(mantissa)); + std::memcpy(&signExponent, bytes + 8, sizeof(signExponent)); + return {mantissa, (signExponent & 0x8000u) != 0, static_cast(signExponent & 0x7fffu) - 16383}; +} + +long double ComposeExtended(bool negative, int biased, std::uint64_t mantissa) { + unsigned char bytes[sizeof(long double)] = {}; + const std::uint16_t signExponent = static_cast((negative ? 0x8000u : 0u) | static_cast(biased)); + std::memcpy(bytes, &mantissa, sizeof(mantissa)); + std::memcpy(bytes + 8, &signExponent, sizeof(signExponent)); + long double value; + std::memcpy(&value, bytes, sizeof(value)); + return value; +} + +template +constexpr int ShiftMask = static_cast(sizeof(TShift) * 8 - 1); + +template +TShift ShiftSignificand(const Decomposed& parts) { + constexpr int significandBits = FloatFormat::SignificandBits; + const TShift significand = parts.significand; + if (parts.exponent < significandBits) return significand >> (significandBits - parts.exponent); + return significand << ((parts.exponent - significandBits) & ShiftMask); +} + +template +TResult FixSigned(TFloat value) { + const Decomposed parts = Decompose(value); + if (parts.exponent < 0) return 0; + const TShift magnitude = ShiftSignificand(parts); + return static_cast(parts.negative ? static_cast(0) - magnitude : magnitude); +} + +template +TResult FixUnsigned(TFloat value) { + const Decomposed parts = Decompose(value); + if (parts.negative || parts.exponent < 0) return 0; + return ShiftSignificand(parts); +} + +template +TResult FixExtendedSigned(long double value) { + const ExtendedBits parts = DecomposeExtended(value); + if (parts.exponent < 0) return 0; + TResult magnitude; + if constexpr (sizeof(TResult) == 16) { + const Uint128 mantissa = parts.mantissa; + magnitude = static_cast(parts.exponent < 64 ? mantissa >> (63 - parts.exponent) + : mantissa << ((parts.exponent - 63) & 127)); + } else { + magnitude = static_cast(parts.mantissa >> ((63 - parts.exponent) & 63)); + } + using TUnsigned = std::conditional_t; + const TUnsigned bits = static_cast(magnitude); + return static_cast(parts.negative ? static_cast(0) - bits : bits); +} + +template +TResult FixExtendedUnsigned(long double value) { + const ExtendedBits parts = DecomposeExtended(value); + if (parts.negative || parts.exponent < 0) return 0; + if constexpr (sizeof(TResult) == 16) { + const Uint128 mantissa = parts.mantissa; + return parts.exponent < 64 ? mantissa >> (63 - parts.exponent) : mantissa << ((parts.exponent - 63) & 127); + } else if constexpr (sizeof(TResult) == 8) { + return parts.mantissa >> ((63 - parts.exponent) & 63); + } else { + return static_cast(parts.mantissa >> 32) >> ((31 - parts.exponent) & 31); + } +} + +int HighestBit(Uint128 value) { + const std::uint64_t high = static_cast(value >> 64); + return high != 0 ? 127 - std::countl_zero(high) : 63 - std::countl_zero(static_cast(value)); +} + +template +TFloat RoundToFloat(bool negative, Uint128 magnitude) { + using Format = FloatFormat; + using Rep = typename Format::Rep; + const Rep sign = negative ? static_cast(1) << (sizeof(Rep) * 8 - 1) : 0; + if (magnitude == 0) return std::bit_cast(sign); + int exponent = HighestBit(magnitude); + Uint128 significand; + if (exponent <= Format::SignificandBits) { + significand = magnitude << (Format::SignificandBits - exponent); + } else { + const int shift = exponent - Format::SignificandBits; + significand = magnitude >> shift; + const Uint128 rest = magnitude & ((static_cast(1) << shift) - 1); + const Uint128 half = static_cast(1) << (shift - 1); + if (rest > half || (rest == half && (significand & 1) != 0)) ++significand; + if (significand >> (Format::SignificandBits + 1) != 0) { + significand >>= 1; + ++exponent; + } + } + const int biased = exponent + Format::Bias; + if (biased >= Format::MaxBiased) return std::bit_cast(static_cast(sign | (static_cast(Format::MaxBiased) << Format::SignificandBits))); + const Rep fraction = static_cast(significand) & ((static_cast(1) << Format::SignificandBits) - 1); + return std::bit_cast(static_cast(sign | (static_cast(biased) << Format::SignificandBits) | fraction)); +} + +long double RoundToExtended(bool negative, Uint128 magnitude) { + if (magnitude == 0) return ComposeExtended(negative, 0, 0); + int exponent = HighestBit(magnitude); + Uint128 significand; + if (exponent <= 63) { + significand = magnitude << (63 - exponent); + } else { + const int shift = exponent - 63; + significand = magnitude >> shift; + const Uint128 rest = magnitude & ((static_cast(1) << shift) - 1); + const Uint128 half = static_cast(1) << (shift - 1); + if (rest > half || (rest == half && (significand & 1) != 0)) ++significand; + if (significand >> 64 != 0) { + significand >>= 1; + ++exponent; + } + } + return ComposeExtended(negative, exponent + 16383, static_cast(significand)); +} + +Uint128 Magnitude(Int128 value) { + return value < 0 ? static_cast(0) - static_cast(value) : static_cast(value); +} + +template +bool IsNan(TFloat value) { + using Format = FloatFormat; + using Rep = typename Format::Rep; + const Rep magnitude = std::bit_cast(value) & (~static_cast(0) >> 1); + return magnitude > (static_cast(Format::MaxBiased) << Format::SignificandBits); +} + +template +TFloat QuietNan() { + using Format = FloatFormat; + using Rep = typename Format::Rep; + return std::bit_cast(static_cast((static_cast(Format::MaxBiased) << Format::SignificandBits) + | (static_cast(1) << (Format::SignificandBits - 1)))); +} + +template +TFloat Negate(TFloat value) { + using Rep = typename FloatFormat::Rep; + return std::bit_cast(static_cast(std::bit_cast(value) ^ (static_cast(1) << (sizeof(Rep) * 8 - 1)))); +} + +template +TFloat GeneratedNan(TFloat result, TFloat a, TFloat b) { + return IsNan(result) && !IsNan(a) && !IsNan(b) ? QuietNan() : result; +} + +double AddDouble(double a, double b) { + std::uint32_t saved = 0; + asm volatile("stmxcsr %[saved]\n\tldmxcsr %[standard]\n\taddsd %[b], %[a]\n\tldmxcsr %[saved]" + : [a] "+x"(a), [saved] "+m"(saved) : [b] "x"(b), [standard] "m"(DefaultMxcsr)); + return a; +} + +double MultiplyDouble(double a, double b) { + std::uint32_t saved = 0; + asm volatile("stmxcsr %[saved]\n\tldmxcsr %[standard]\n\tmulsd %[b], %[a]\n\tldmxcsr %[saved]" + : [a] "+x"(a), [saved] "+m"(saved) : [b] "x"(b), [standard] "m"(DefaultMxcsr)); + return a; +} + +double DivideDouble(double a, double b) { + std::uint32_t saved = 0; + asm volatile("stmxcsr %[saved]\n\tldmxcsr %[standard]\n\tdivsd %[b], %[a]\n\tldmxcsr %[saved]" + : [a] "+x"(a), [saved] "+m"(saved) : [b] "x"(b), [standard] "m"(DefaultMxcsr)); + return a; +} + +float AddFloat(float a, float b) { + std::uint32_t saved = 0; + asm volatile("stmxcsr %[saved]\n\tldmxcsr %[standard]\n\taddss %[b], %[a]\n\tldmxcsr %[saved]" + : [a] "+x"(a), [saved] "+m"(saved) : [b] "x"(b), [standard] "m"(DefaultMxcsr)); + return a; +} + +float MultiplyFloat(float a, float b) { + std::uint32_t saved = 0; + asm volatile("stmxcsr %[saved]\n\tldmxcsr %[standard]\n\tmulss %[b], %[a]\n\tldmxcsr %[saved]" + : [a] "+x"(a), [saved] "+m"(saved) : [b] "x"(b), [standard] "m"(DefaultMxcsr)); + return a; +} + +float DivideFloat(float a, float b) { + std::uint32_t saved = 0; + asm volatile("stmxcsr %[saved]\n\tldmxcsr %[standard]\n\tdivss %[b], %[a]\n\tldmxcsr %[saved]" + : [a] "+x"(a), [saved] "+m"(saved) : [b] "x"(b), [standard] "m"(DefaultMxcsr)); + return a; +} + +constexpr std::uint32_t TowardZeroMxcsr = DefaultMxcsr | 0x6000; + +double DivideDoubleTowardZero(double a, double b) { + std::uint32_t saved = 0; + asm volatile("stmxcsr %[saved]\n\tldmxcsr %[standard]\n\tdivsd %[b], %[a]\n\tldmxcsr %[saved]" + : [a] "+x"(a), [saved] "+m"(saved) : [b] "x"(b), [standard] "m"(TowardZeroMxcsr)); + return a; +} + +float DivideFloatTowardZero(float a, float b) { + std::uint32_t saved = 0; + asm volatile("stmxcsr %[saved]\n\tldmxcsr %[standard]\n\tdivss %[b], %[a]\n\tldmxcsr %[saved]" + : [a] "+x"(a), [saved] "+m"(saved) : [b] "x"(b), [standard] "m"(TowardZeroMxcsr)); + return a; +} + +template +TFloat FlushSubnormalQuotient(TFloat quotient, TFloat truncated) { + using Format = FloatFormat; + using Rep = typename Format::Rep; + const Rep signBit = static_cast(1) << (sizeof(Rep) * 8 - 1); + const Rep magnitude = std::bit_cast(truncated) & ~signBit; + if (magnitude >= (static_cast(1) << Format::SignificandBits)) return quotient; + return std::bit_cast(static_cast(std::bit_cast(truncated) & signBit)); +} + +float TruncateDouble(double value) { + std::uint32_t saved = 0; + float result; + asm volatile("stmxcsr %[saved]\n\tldmxcsr %[standard]\n\tcvtsd2ss %[value], %[result]\n\tldmxcsr %[saved]" + : [result] "=x"(result), [saved] "+m"(saved) : [value] "x"(value), [standard] "m"(DefaultMxcsr)); + return result; +} + +double ExtendFloat(float value) { + const std::uint32_t bits = std::bit_cast(value); + const std::uint64_t sign = static_cast(bits >> 31) << 63; + const std::uint32_t magnitude = bits & 0x7fffffffu; + if (magnitude >= 0x7f800000u) return std::bit_cast(sign | 0x7ff0000000000000ull | (static_cast(magnitude & 0x400000u) << 29) | (magnitude & 0x3fffffu)); + if (magnitude >= 0x00800000u) return std::bit_cast(sign | ((static_cast(magnitude) << 29) + (static_cast(1023 - 127) << 52))); + if (magnitude == 0) return std::bit_cast(sign); + const int shift = std::countl_zero(magnitude) - 8; + const std::uint64_t fraction = static_cast(magnitude << shift) & 0x7fffffu; + const std::uint64_t biased = static_cast(1023 - 126 - shift); + return std::bit_cast(sign | (biased << 52) | (fraction << 29)); +} + +template +int LessEqual(TFloat a, TFloat b, int unordered) { + using Rep = typename FloatFormat::Rep; + using Signed = std::make_signed_t; + if (IsNan(a) || IsNan(b)) return unordered; + const Rep aBits = std::bit_cast(a), bBits = std::bit_cast(b); + const Rep magnitudeMask = ~static_cast(0) >> 1; + if (((aBits | bBits) & magnitudeMask) == 0) return 0; + const Signed aInt = static_cast(aBits), bInt = static_cast(bBits); + if ((aInt & bInt) >= 0) { + if (aInt < bInt) return -1; + return aInt == bInt ? 0 : 1; + } + if (aInt > bInt) return -1; + return aInt == bInt ? 0 : 1; +} + +template +bool IsInfinite(TFloat value) { return std::isinf(value); } + +template +TFloat Box(TFloat value) { return std::copysign(std::isinf(value) ? TFloat(1) : TFloat(0), value); } + +template +TFloat ZeroIfNan(TFloat value) { return std::isnan(value) ? std::copysign(TFloat(0), value) : value; } + +template +TFloat MultiplyAdd(TFloat a, TFloat b, TFloat product) { + if constexpr (Fused) return std::fma(a, b, product); + else return a * b + product; +} + +template +TFloat MultiplySubtract(TFloat a, TFloat b, TFloat product) { + return MultiplyAdd(a, b, std::isnan(product) ? product : -product); +} + +template +void MultiplyComplex(TFloat a, TFloat b, TFloat c, TFloat d, TFloat& real, TFloat& imaginary) { + const TFloat ac = a * c, bd = b * d, ad = a * d, bc = b * c; + real = ac - bd; + imaginary = bc + ad; + if (!std::isnan(real) || !std::isnan(imaginary)) return; + bool recalculate = false; + if (IsInfinite(a) || IsInfinite(b)) { + a = Box(a); + b = Box(b); + c = ZeroIfNan(c); + d = ZeroIfNan(d); + recalculate = true; + } + if (IsInfinite(c) || IsInfinite(d)) { + c = Box(c); + d = Box(d); + a = ZeroIfNan(a); + b = ZeroIfNan(b); + recalculate = true; + } + if (!recalculate && (IsInfinite(ac) || IsInfinite(bd) || IsInfinite(ad) || IsInfinite(bc))) { + a = ZeroIfNan(a); + b = ZeroIfNan(b); + c = ZeroIfNan(c); + d = ZeroIfNan(d); + recalculate = true; + } + if (recalculate) { + real = MultiplySubtract(a, c, b * d) * static_cast(INFINITY); + imaginary = MultiplyAdd(a, d, b * c) * static_cast(INFINITY); + } +} + +template +TFloat Scale(TFloat value, int exponent) { + return static_cast(std::scalbn(static_cast(value), exponent)); +} + +template +TFloat LargerMagnitude(TFloat c, TFloat d) { + const TFloat magnitudeC = std::fabs(c), magnitudeD = std::fabs(d); + if (std::isnan(magnitudeC)) return magnitudeD; + if (std::isnan(magnitudeD)) return magnitudeC; + return magnitudeC < magnitudeD ? magnitudeD : magnitudeC; +} + +template +void DivideComplex(TFloat a, TFloat b, TFloat c, TFloat d, TFloat& real, TFloat& imaginary) { + int scale = 0; + const TFloat logScale = std::logb(LargerMagnitude(c, d)); + if (std::isfinite(logScale)) { + scale = static_cast(logScale); + c = Scale(c, -scale); + d = Scale(d, -scale); + } + const TFloat denominator = MultiplyAdd(c, c, d * d); + real = Scale(MultiplyAdd(c, a, d * b) / denominator, -scale); + imaginary = Scale(MultiplySubtract(c, b, d * a) / denominator, -scale); + if (!std::isnan(real) || !std::isnan(imaginary)) return; + if (denominator == TFloat(0) && (!std::isnan(a) || !std::isnan(b))) { + real = std::copysign(static_cast(INFINITY), c) * a; + imaginary = std::copysign(static_cast(INFINITY), c) * b; + } else if ((IsInfinite(a) || IsInfinite(b)) && std::isfinite(c) && std::isfinite(d)) { + a = Box(a); + b = Box(b); + real = MultiplyAdd(a, c, b * d) * static_cast(INFINITY); + imaginary = MultiplySubtract(b, c, d * a) * static_cast(INFINITY); + } else if (IsInfinite(logScale) && logScale > TFloat(0) && std::isfinite(a) && std::isfinite(b)) { + c = Box(c); + d = Box(d); + real = MultiplyAdd(a, c, d * b) * TFloat(0); + imaginary = MultiplySubtract(b, c, d * a) * TFloat(0); + } +} + +using ComplexFloat = __complex__ float; +using ComplexDouble = __complex__ double; +using ComplexExtended = __complex__ long double; + +ComplexFloat MakeComplex(float real, float imaginary) { + ComplexFloat result; + __real__ result = real; + __imag__ result = imaginary; + return result; +} + +ComplexDouble MakeComplex(double real, double imaginary) { + ComplexDouble result; + __real__ result = real; + __imag__ result = imaginary; + return result; +} + +ComplexExtended MakeComplex(long double real, long double imaginary) { + ComplexExtended result; + __real__ result = real; + __imag__ result = imaginary; + return result; +} + +} + +extern "C" { + +double APS5_VABI __adddf3_nid_postfix(double a, double b) { return GeneratedNan(AddDouble(a, b), a, b); } +float APS5_VABI __addsf3_nid_postfix(float a, float b) { return GeneratedNan(AddFloat(a, b), a, b); } + +double APS5_VABI __subdf3_nid_postfix(double a, double b) { + const double negated = Negate(b); + return GeneratedNan(AddDouble(a, negated), a, negated); +} + +float APS5_VABI __subsf3_nid_postfix(float a, float b) { + const float negated = Negate(b); + return GeneratedNan(AddFloat(a, negated), a, negated); +} + +double APS5_VABI __muldf3_nid_postfix(double a, double b) { return GeneratedNan(MultiplyDouble(a, b), a, b); } +float APS5_VABI __mulsf3_nid_postfix(float a, float b) { return GeneratedNan(MultiplyFloat(a, b), a, b); } +double APS5_VABI __divdf3_nid_postfix(double a, double b) { + if (IsNan(a) || IsNan(b)) return GeneratedNan(DivideDouble(a, b), a, b); + return GeneratedNan(FlushSubnormalQuotient(DivideDouble(a, b), DivideDoubleTowardZero(a, b)), a, b); +} +float APS5_VABI __divsf3_nid_postfix(float a, float b) { + if (IsNan(a) || IsNan(b)) return GeneratedNan(DivideFloat(a, b), a, b); + return GeneratedNan(FlushSubnormalQuotient(DivideFloat(a, b), DivideFloatTowardZero(a, b)), a, b); +} +double APS5_VABI __negdf2_nid_postfix(double a) { return Negate(a); } +float APS5_VABI __negsf2_nid_postfix(float a) { return Negate(a); } + +int APS5_VABI __eqdf2_nid_postfix(double a, double b) { return LessEqual(a, b, 1); } +int APS5_VABI __nedf2_nid_postfix(double a, double b) { return LessEqual(a, b, 1); } +int APS5_VABI __ltdf2_nid_postfix(double a, double b) { return LessEqual(a, b, 1); } +int APS5_VABI __ledf2_nid_postfix(double a, double b) { return LessEqual(a, b, 1); } +int APS5_VABI __gtdf2_nid_postfix(double a, double b) { return LessEqual(a, b, -1); } +int APS5_VABI __gedf2_nid_postfix(double a, double b) { return LessEqual(a, b, -1); } +int APS5_VABI __unorddf2_nid_postfix(double a, double b) { return IsNan(a) || IsNan(b) ? 1 : 0; } +int APS5_VABI __eqsf2_nid_postfix(float a, float b) { return LessEqual(a, b, 1); } +int APS5_VABI __nesf2_nid_postfix(float a, float b) { return LessEqual(a, b, 1); } +int APS5_VABI __ltsf2_nid_postfix(float a, float b) { return LessEqual(a, b, 1); } +int APS5_VABI __lesf2_nid_postfix(float a, float b) { return LessEqual(a, b, 1); } +int APS5_VABI __gtsf2_nid_postfix(float a, float b) { return LessEqual(a, b, -1); } +int APS5_VABI __gesf2_nid_postfix(float a, float b) { return LessEqual(a, b, -1); } +int APS5_VABI __unordsf2_nid_postfix(float a, float b) { return IsNan(a) || IsNan(b) ? 1 : 0; } + +double APS5_VABI __extendsfdf2_nid_postfix(float a) { return ExtendFloat(a); } +float APS5_VABI __truncdfsf2_nid_postfix(double a) { + if (!IsNan(a)) return TruncateDouble(a); + const std::uint64_t bits = std::bit_cast(a); + return std::bit_cast(static_cast(((bits >> 32) & 0x80000000u) | 0x7fc00000u | (bits & 0x3fffffu))); +} + +std::int32_t APS5_VABI __fixsfsi_nid_postfix(float a) { return FixSigned(a); } +std::int64_t APS5_VABI __fixsfdi_nid_postfix(float a) { return FixSigned(a); } +Int128 APS5_VABI __fixsfti_nid_postfix(float a) { return FixSigned(a); } +std::int32_t APS5_VABI __fixdfsi_nid_postfix(double a) { return FixSigned(a); } +std::int64_t APS5_VABI __fixdfdi_nid_postfix(double a) { return FixSigned(a); } +Int128 APS5_VABI __fixdfti_nid_postfix(double a) { return FixSigned(a); } +std::uint32_t APS5_VABI __fixunssfsi_nid_postfix(float a) { return FixUnsigned(a); } +std::uint64_t APS5_VABI __fixunssfdi_nid_postfix(float a) { return FixUnsigned(a); } +Uint128 APS5_VABI __fixunssfti_nid_postfix(float a) { return FixUnsigned(a); } + +std::uint32_t APS5_VABI __fixunsdfsi_nid_postfix(double a) { + const Decomposed parts = Decompose(a); + if (parts.negative || parts.exponent < 0) return 0; + return static_cast(parts.significand >> 21) >> ((31 - parts.exponent) & 31); +} + +std::uint64_t APS5_VABI __fixunsdfdi_nid_postfix(double a) { return FixUnsigned(a); } +Uint128 APS5_VABI __fixunsdfti_nid_postfix(double a) { return FixUnsigned(a); } +std::int64_t APS5_VABI __fixxfdi_nid_postfix(long double a) { return FixExtendedSigned(a); } +Int128 APS5_VABI __fixxfti_nid_postfix(long double a) { return FixExtendedSigned(a); } +std::uint32_t APS5_VABI __fixunsxfsi_nid_postfix(long double a) { return FixExtendedUnsigned(a); } +std::uint64_t APS5_VABI __fixunsxfdi_nid_postfix(long double a) { return FixExtendedUnsigned(a); } +Uint128 APS5_VABI __fixunsxfti_nid_postfix(long double a) { return FixExtendedUnsigned(a); } + +double APS5_VABI __floatsidf_nid_postfix(std::int32_t a) { return static_cast(a); } +double APS5_VABI __floatunsidf_nid_postfix(std::uint32_t a) { return static_cast(a); } +float APS5_VABI __floatsisf_nid_postfix(std::int32_t a) { return RoundToFloat(a < 0, Magnitude(a)); } +float APS5_VABI __floatunsisf_nid_postfix(std::uint32_t a) { return RoundToFloat(false, a); } +double APS5_VABI __floatdidf_nid_postfix(std::int64_t a) { return static_cast(a); } +float APS5_VABI __floatdisf_nid_postfix(std::int64_t a) { return static_cast(a); } +long double APS5_VABI __floatdixf_nid_postfix(std::int64_t a) { return static_cast(a); } +double APS5_VABI __floatundidf_nid_postfix(std::uint64_t a) { return static_cast(a); } +float APS5_VABI __floatundisf_nid_postfix(std::uint64_t a) { return static_cast(a); } +long double APS5_VABI __floatundixf_nid_postfix(std::uint64_t a) { return static_cast(a); } +double APS5_VABI __floattidf_nid_postfix(Int128 a) { return RoundToFloat(a < 0, Magnitude(a)); } +float APS5_VABI __floattisf_nid_postfix(Int128 a) { return RoundToFloat(a < 0, Magnitude(a)); } +long double APS5_VABI __floattixf_nid_postfix(Int128 a) { return RoundToExtended(a < 0, Magnitude(a)); } +double APS5_VABI __floatuntidf_nid_postfix(Uint128 a) { return RoundToFloat(false, a); } +float APS5_VABI __floatuntisf_nid_postfix(Uint128 a) { return RoundToFloat(false, a); } +long double APS5_VABI __floatuntixf_nid_postfix(Uint128 a) { return RoundToExtended(false, a); } + +__complex__ float APS5_VABI __mulsc3_nid_postfix(float a, float b, float c, float d) { + float real, imaginary; + MultiplyComplex(a, b, c, d, real, imaginary); + return MakeComplex(real, imaginary); +} + +__complex__ double APS5_VABI __muldc3_nid_postfix(double a, double b, double c, double d) { + double real, imaginary; + MultiplyComplex(a, b, c, d, real, imaginary); + return MakeComplex(real, imaginary); +} + +__complex__ long double APS5_VABI __mulxc3_nid_postfix(long double a, long double b, long double c, long double d) { + long double real, imaginary; + MultiplyComplex(a, b, c, d, real, imaginary); + return MakeComplex(real, imaginary); +} + +__complex__ float APS5_VABI __divsc3_nid_postfix(float a, float b, float c, float d) { + float real, imaginary; + DivideComplex(a, b, c, d, real, imaginary); + return MakeComplex(real, imaginary); +} + +__complex__ double APS5_VABI __divdc3_nid_postfix(double a, double b, double c, double d) { + double real, imaginary; + DivideComplex(a, b, c, d, real, imaginary); + return MakeComplex(real, imaginary); +} + +__complex__ long double APS5_VABI __divxc3_nid_postfix(long double a, long double b, long double c, long double d) { + long double real, imaginary; + DivideComplex(a, b, c, d, real, imaginary); + return MakeComplex(real, imaginary); +} + +long double APS5_VABI __powixf2_nid_postfix(long double base, int exponent) { + const bool reciprocal = exponent < 0; + long double result = (exponent & 1) != 0 ? base : 1.0L; + for (exponent /= 2; exponent != 0; exponent /= 2) { + base *= base; + if (exponent & 1) result *= base; + } + return reciprocal ? 1 / result : result; +} + +} diff --git a/core/libs/prx/libc/src/FloatingPoint.cpp b/core/libs/prx/libc/src/FloatingPoint.cpp new file mode 100644 index 0000000000..e3717ef61b --- /dev/null +++ b/core/libs/prx/libc/src/FloatingPoint.cpp @@ -0,0 +1,133 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +namespace { + +constexpr unsigned Exceptions = 0x3f; +constexpr unsigned Rounding = 0xc00; + +void ReplaceFlags(unsigned mask, unsigned flags) { + std::array environment; + asm volatile("fnstenv %0" : "=m"(environment)); + environment[1] = (environment[1] & ~mask) | (flags & mask); + asm volatile("fldenv %0" : : "m"(environment)); + _mm_setcsr((_mm_getcsr() & ~mask) | (flags & mask)); +} + +int ChangeTraps(int exceptions, bool enable) { + const auto mask = static_cast(exceptions) & Exceptions; + std::uint16_t control; + asm volatile("fnstcw %0" : "=m"(control)); + const unsigned csr = _mm_getcsr(); + const auto changed = static_cast(enable ? (control & ~mask) : (control | mask)); + asm volatile("fldcw %0" : : "m"(changed)); + _mm_setcsr(enable ? (csr & ~(mask << 7)) : (csr | (mask << 7))); + return static_cast(~((csr >> 7) | control) | ~Exceptions); +} + +} + +extern "C" { + +int APS5_VABI fegetround_nid_postfix() { + std::uint16_t control; + asm volatile("fnstcw %0" : "=m"(control)); + return control & Rounding; +} + +int APS5_VABI fesetround_nid_postfix(int rounding) { + const auto mode = static_cast(rounding); + if ((mode & ~Rounding) != 0) return -1; + std::uint16_t control; + asm volatile("fnstcw %0" : "=m"(control)); + control = static_cast((control & ~Rounding) | mode); + asm volatile("fldcw %0" : : "m"(control)); + _mm_setcsr((_mm_getcsr() & ~(Rounding << 3)) | (mode << 3)); + return 0; +} + +int APS5_VABI fetestexcept_nid_postfix(int exceptions) { + std::uint16_t status; + asm volatile("fnstsw %0" : "=am"(status)); + return (status | _mm_getcsr()) & static_cast(exceptions) & Exceptions; +} + +int APS5_VABI fegetexceptflag_nid_postfix(std::uint16_t* flags, int exceptions) { + if (flags == nullptr) throw std::invalid_argument("fegetexceptflag: null flags"); + *flags = static_cast(fetestexcept_nid_postfix(exceptions)); + return 0; +} + +int* APS5_VABI __error_nid_postfix(); + +void APS5_VABI _Feraise_nid_postfix(int exceptions) { + constexpr unsigned Invalid = 0x01, Ranges = 0x1c, Overflows = 0x18, Inexact = 0x20; + constexpr int GuestEdom = 33, GuestErange = 34; + auto raised = static_cast(exceptions); + if ((raised & Overflows) != 0) raised |= Inexact; + *__error_nid_postfix() = GuestEdom; + std::array environment; + asm volatile("fnstenv %0" : "=m"(environment)); + environment[1] = (environment[1] & ~raised) | (raised & 0xffff); + asm volatile("fldenv %0" : : "m"(environment)); + _mm_setcsr((_mm_getcsr() & ~raised) | (raised & 0xffff)); + asm volatile("fwait"); + if ((raised & Invalid) != 0) *__error_nid_postfix() = GuestEdom; + else if ((raised & Ranges) != 0) *__error_nid_postfix() = GuestErange; +} + +int APS5_VABI fesetexceptflag_nid_postfix(const std::uint16_t* flags, int exceptions) { + if (flags == nullptr) throw std::invalid_argument("fesetexceptflag: null flags"); + ReplaceFlags(static_cast(exceptions) & Exceptions, *flags); + return 0; +} + +int APS5_VABI feclearexcept_nid_postfix(int exceptions) { + const auto mask = static_cast(exceptions) & Exceptions; + if (mask == Exceptions) { + asm volatile("fnclex"); + _mm_setcsr(_mm_getcsr() & ~Exceptions); + } else if (mask != 0) { + ReplaceFlags(mask, 0); + } + return 0; +} + + +int APS5_VABI feenableexcept_nid_postfix(int exceptions) { + return ChangeTraps(exceptions, true); +} + +int APS5_VABI fedisableexcept_nid_postfix(int exceptions) { + return ChangeTraps(exceptions, false); +} + +int APS5_VABI __feenableexcept_nid_postfix(int exceptions) { + return ChangeTraps(exceptions, true); +} + +int APS5_VABI __fedisableexcept_nid_postfix(int exceptions) { + return ChangeTraps(exceptions, false); +} + +int APS5_VABI fegettrapenable_nid_postfix() { + std::uint16_t control; + asm volatile("fnstcw %0" : "=m"(control)); + return ~control & Exceptions; +} + +int APS5_VABI fesettrapenable_nid_postfix(int exceptions) { + const auto mask = static_cast(exceptions); + if (mask > Exceptions) return 1; + std::uint16_t control; + asm volatile("fnstcw %0" : "=m"(control)); + control = static_cast(control & ~mask); + asm volatile("fldcw %0" : : "m"(control)); + _mm_setcsr(_mm_getcsr() & ~(static_cast(mask) << 7)); + return 0; +} + +} diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index bc08460a33..5a35df68eb 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include "prx/libc/include/General.hpp" #include "SceTypes.hpp" @@ -16,6 +18,7 @@ #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/WindowsFormatting.hpp" +#include "prx/libc/include/ScanWideField.hpp" #ifdef _WIN32 #include "prx/libc/include/WindowsScanning.hpp" @@ -31,26 +34,27 @@ int __isoc99_vfscanf(std::FILE*, const char*, std::va_list); namespace { #ifndef _WIN32 -bool HasGuestWideFormat(const char* format) { +bool RequiresGuestFormatting(const char* format) { if (format == nullptr) return false; while ((format = std::strchr(format, '%')) != nullptr) { ++format; if (*format == '%') { ++format; continue; } while (*format && std::strchr("-+ #0'0123456789.*$", *format)) ++format; - if (*format == 'S' || *format == 'C' || *format == 'm' || (*format == 'l' && (format[1] == 's' || format[1] == 'c'))) return true; + if (*format == 'S' || *format == 'C' || *format == 'm' || *format == 'p' || + (*format == 'l' && (format[1] == 's' || format[1] == 'c' || format[1] == 'p'))) return true; if (*format) ++format; } return false; } -void RejectGuestWideScan(const char* format) { - if (format == nullptr) return; +bool HasGuestWideScan(const char* format) { + if (format == nullptr) return false; while ((format = std::strchr(format, '%')) != nullptr) { ++format; if (*format == '%') { ++format; continue; } while (*format && std::strchr("-+ #0'0123456789.*$", *format)) ++format; if (*format == 'S' || *format == 'C' || (*format == 'l' && format[1] != '\0' && std::strchr("sc[", format[1]))) - NotImplemented_nid_no_patch("scanf 16-bit wide conversion on Linux"); + return true; if (*format == '[') { ++format; if (*format == '^') ++format; @@ -59,7 +63,24 @@ void RejectGuestWideScan(const char* format) { } if (*format) ++format; } + return false; } + +bool ScansWideInProcess(const char* format) { + if (!HasGuestWideScan(format)) return false; + LibcDetail::RequireNoPositionalWideScan(format); + return true; +} + +void RejectGuestWideScan(const char* format) { + if (HasGuestWideScan(format)) NotImplemented_nid_no_patch("fscanf 16-bit wide conversion on Linux"); +} + +bool IsWideScan(const std::string& length, char conversion) { + return conversion == 'S' || conversion == 'C' || (length == "l" && (conversion == 's' || conversion == 'c' || conversion == '[')); +} + + #endif std::string HostLength(const std::string& length, char conversion) { @@ -142,6 +163,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); @@ -153,6 +175,58 @@ int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer n } continue; } +#ifndef _WIN32 + if (IsWideScan(length, conversion)) { + const char kind = conversion == 'S' ? 's' : conversion == 'C' ? 'c' : conversion; + const char* field = input; + if (kind == 's') { + while (std::isspace(static_cast(*field))) ++field; + } + if (*field == '\0') return finish(converted ? assigned : EOF); + auto* target = suppress ? nullptr : static_cast(nextPointer()); + unsigned long limit = ULONG_MAX; + if (secure && !suppress) { + const auto capacity = static_cast(nextCapacity()); + limit = kind == 'c' ? capacity : (capacity == 0 ? 0 : capacity - 1); + if (limit == 0 || (kind == 'c' && (width == 0 ? 1 : width) > limit)) { + if (capacity != 0) target[0] = u'\0'; + return finish(assigned); + } + } + const unsigned long wanted = width != 0 ? width : (kind == 'c' ? 1 : ULONG_MAX); + const unsigned long taken = std::min(wanted, limit); + std::u16string text; + const auto accepts = [&](char32_t point) { + if (kind == 's') return point >= 0x80 || !std::isspace(static_cast(point)); + if (kind == '[') return LibcDetail::InWideScanset(specifier, point); + return true; + }; + while (text.size() < taken && *field != '\0') { + char32_t point = 0; + const std::size_t size = LibcDetail::ReadWideScanCharacter(field, &point); + if (!accepts(point)) break; + text.push_back(static_cast(point)); + field += size; + } + if (text.empty()) return finish(assigned); + if (target != nullptr && kind != 'c' && wanted > limit && text.size() == limit && *field != '\0') { + char32_t point = 0; + LibcDetail::ReadWideScanCharacter(field, &point); + if (accepts(point)) { + target[0] = u'\0'; + return finish(assigned); + } + } + if (target != nullptr) { + std::memcpy(target, text.data(), text.size() * sizeof(char16_t)); + if (kind != 'c') target[text.size()] = u'\0'; + ++assigned; + } + input = field; + converted = true; + continue; + } +#endif if (conversion != 'c' && conversion != '[') { const char* probe = input; while (std::isspace(static_cast(*probe))) ++probe; @@ -163,7 +237,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); @@ -218,17 +295,21 @@ int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaLi const char* source = text.c_str(); #else std::string guestText; - const bool guestWide = HasGuestWideFormat(format); + const bool guestFormatting = RequiresGuestFormatting(format); char* text = nullptr; std::va_list copy; va_copy(copy, *reinterpret_cast(args)); - const int count = guestWide ? LibcDetail::FormatWindows(nullptr, 0, format, args, &guestText) : ::vasprintf(&text, format, copy); + const int count = guestFormatting ? LibcDetail::FormatWindows(nullptr, 0, format, args, &guestText) : ::vasprintf(&text, format, copy); va_end(copy); std::unique_ptr owner(text, std::free); if (count < 0) return -1; - const char* source = guestWide ? guestText.c_str() : text; + const char* source = guestFormatting ? guestText.c_str() : text; #endif auto* output = static_cast(ApplicationHeapAllocate_nid_no_patch(static_cast(count) + 1)); + if (!output) { + errno = 12; + return -1; + } std::memcpy(output, source, static_cast(count) + 1); *destination = output; return count; @@ -239,6 +320,7 @@ int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaLi } int APS5_VABI vfprintf_nid_postfix(FileStream* stream, const char* format, VaList* args) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } auto* native = GetNativeStream(stream); #ifdef _WIN32 std::string buffer; @@ -246,7 +328,7 @@ int APS5_VABI vfprintf_nid_postfix(FileStream* stream, const char* format, VaLis const int result = std::fwrite(buffer.data(), 1, static_cast(count), native) == static_cast(count) ? count : -1; #else - if (HasGuestWideFormat(format)) { + if (RequiresGuestFormatting(format)) { std::string buffer; const int count = LibcDetail::FormatWindows(nullptr, 0, format, args, &buffer); const int result = std::fwrite(buffer.data(), 1, static_cast(count), native) == static_cast(count) ? count : -1; @@ -279,8 +361,17 @@ int APS5_VABI fprintf_nid_postfix(FileStream* stream, const char* format, ...) { return result; } +static bool SizeOverflowsInt(char* buffer, size_t size) { + if (size == 0 || size - 1 <= static_cast(INT_MAX)) return false; + errno = 84; + if (buffer != nullptr) *buffer = '\0'; + return true; +} + int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } auto* native = GetNativeStream(stream); + if (!stream->SwitchToReading()) return EOF; #ifdef _WIN32 __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); @@ -294,12 +385,14 @@ int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { va_end(args); #endif stream->SyncStatus(); + stream->KeepDirectoryReadError(result == -1); return result; } #ifdef _WIN32 int APS5_VABI printf_nid_postfix(const char* format, ...) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); const int result = LibcDetail::PrintWindows(format, args); @@ -308,6 +401,7 @@ int APS5_VABI printf_nid_postfix(const char* format, ...) { } int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); const int result = LibcDetail::PrintWindows(format, args); @@ -316,6 +410,7 @@ int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { } int APS5_VABI snprintf_nid_postfix(char* buffer, size_t size, const char* format, ...) { + if (SizeOverflowsInt(buffer, size)) return -1; __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); const int result = LibcDetail::FormatWindows(buffer, size, format, args); @@ -334,25 +429,28 @@ int APS5_VABI sprintf_nid_postfix(char* buffer, const char* format, ...) { #else int APS5_VABI printf_nid_postfix(const char* format, ...) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vfprintf(GuestStandardOutputFile_nid_no_patch(), format, args); va_end(args); return result; } int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vfprintf(GuestStandardOutputFile_nid_no_patch(), format, args); va_end(args); return result; } int APS5_VABI snprintf_nid_postfix(char* buffer, size_t size, const char* format, ...) { + if (SizeOverflowsInt(buffer, size)) return -1; std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); va_end(args); return result; } @@ -360,7 +458,7 @@ int APS5_VABI snprintf_nid_postfix(char* buffer, size_t size, const char* format int APS5_VABI sprintf_nid_postfix(char* buffer, const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, SIZE_MAX, format, args) : std::vsprintf(buffer, format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, SIZE_MAX, format, args) : std::vsprintf(buffer, format, args); va_end(args); return result; } @@ -371,7 +469,10 @@ int APS5_VABI vsscanf_nid_postfix(const char* input, const char* format, VaList* #ifdef _WIN32 return LibcDetail::ScanWindows(input, format, args); #else - RejectGuestWideScan(format); + if (ScansWideInProcess(format)) { + LibcDetail::FormatArguments arguments(args); + return ScanGuest(input, format, false, [&] { return arguments.Next(); }, [] { return 0u; }); + } std::va_list copy; va_copy(copy, *reinterpret_cast(args)); const int result = HostVsscanf(input, format, copy); @@ -390,10 +491,10 @@ int APS5_VABI sscanf_nid_postfix(const char* input, const char* format, ...) { } #else int APS5_VABI sscanf_nid_postfix(const char* input, const char* format, ...) { - RejectGuestWideScan(format); std::va_list args; va_start(args, format); - const int result = HostVsscanf(input, format, args); + const int result = ScansWideInProcess(format) ? ScanGuest(input, format, false, [&] { return va_arg(args, void*); }, [] { return 0u; }) + : HostVsscanf(input, format, args); va_end(args); return result; } @@ -412,7 +513,6 @@ int APS5_VABI sscanf_s_nid_postfix(const char* buffer, const char* format, ...) #else int APS5_VABI sscanf_s_nid_postfix(const char* buffer, const char* format, ...) { - RejectGuestWideScan(format); std::va_list args; va_start(args, format); const int result = ScanGuest(buffer, format, true, [&] { return va_arg(args, void*); }, [&] { return va_arg(args, unsigned int); }); @@ -423,13 +523,14 @@ int APS5_VABI sscanf_s_nid_postfix(const char* buffer, const char* format, ...) #endif int APS5_VABI vprintf_nid_postfix(const char* str, VaList* c) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } #ifdef _WIN32 return LibcDetail::PrintWindows(str, c); #else - if (HasGuestWideFormat(str)) return LibcDetail::PrintWindows(str, c); + if (RequiresGuestFormatting(str)) return LibcDetail::PrintWindows(str, c); std::va_list copy; va_copy(copy, *reinterpret_cast(c)); - const int result = std::vprintf(str, copy); + const int result = std::vfprintf(GuestStandardOutputFile_nid_no_patch(), str, copy); va_end(copy); return result; #endif @@ -439,7 +540,7 @@ int APS5_VABI vsprintf_nid_postfix(char* str, const char* format, VaList* args) #ifdef _WIN32 return LibcDetail::FormatWindows(str, SIZE_MAX, format, args); #else - if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(str, SIZE_MAX, format, args); + if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(str, SIZE_MAX, format, args); std::va_list copy; va_copy(copy, *reinterpret_cast(args)); const int result = std::vsprintf(str, format, copy); @@ -449,10 +550,11 @@ int APS5_VABI vsprintf_nid_postfix(char* str, const char* format, VaList* args) } int APS5_VABI vsnprintf_nid_postfix(char* str, size_t size, const char* format, VaList* c) { + if (SizeOverflowsInt(str, size)) return -1; #ifdef _WIN32 return LibcDetail::FormatWindows(str, size, format, c); #else - if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(str, size, format, c); + if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(str, size, format, c); std::va_list copy; va_copy(copy, *reinterpret_cast(c)); const int result = std::vsnprintf(str, size, format, copy); @@ -461,6 +563,23 @@ int APS5_VABI vsnprintf_nid_postfix(char* str, size_t size, const char* format, #endif } +static int FormatSecure(char* buffer, size_t size, const char* format, const void* args) { + constexpr size_t RsizeMax = SIZE_MAX >> 1; + int result = -1; + if (buffer != nullptr && format != nullptr && args != nullptr && size != 0 && size <= RsizeMax) { + try { + result = LibcDetail::FormatWindows(buffer, size, format, args, nullptr, true); + } catch (const LibcDetail::FormatConstraintViolation&) { + } + } + if (result < 0 && buffer != nullptr && size != 0 && size <= RsizeMax) buffer[0] = 0; + return result; +} + +int APS5_VABI vsnprintf_s_nid_postfix(char* buffer, size_t size, const char* format, VaList* args) { + return FormatSecure(buffer, size, format, args); +} + #ifdef _WIN32 int APS5_VABI vsscanf_s_nid_postfix(const char* buffer, const char* format, VaList* args) { @@ -468,14 +587,10 @@ int APS5_VABI vsscanf_s_nid_postfix(const char* buffer, const char* format, VaLi return ScanGuest(buffer, format, true, [&] { return arguments.Next(); }, [&] { return arguments.Next(); }); } -int APS5_VABI vsnprintf_s_nid_postfix(char* buffer, size_t size, const char* format, VaList* args) { - return LibcDetail::FormatWindows(buffer, size, format, args); -} - int APS5_VABI snprintf_s_nid_postfix(char* buffer, size_t size, const char* format, ...) { __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); - const int result = LibcDetail::FormatWindows(buffer, size, format, args); + const int result = FormatSecure(buffer, size, format, args); __builtin_sysv_va_end(args); return result; } @@ -491,22 +606,12 @@ int APS5_VABI printf_s_nid_postfix(const char* format, ...) { #else int APS5_VABI vsscanf_s_nid_postfix(const char* buffer, const char* format, VaList* args) { - RejectGuestWideScan(format); LibcDetail::FormatArguments arguments(args); return ScanGuest(buffer, format, true, [&] { return arguments.Next(); }, [&] { return arguments.Next(); }); } -int APS5_VABI vsnprintf_s_nid_postfix(char* buffer, size_t size, const char* format, VaList* args) { - if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(buffer, size, format, args); - std::va_list copy; - va_copy(copy, *reinterpret_cast(args)); - const int result = std::vsnprintf(buffer, size, format, copy); - va_end(copy); - return result; -} - int APS5_VABI vsprintf_s_nid_postfix(char* buffer, size_t size, const char* format, VaList* args) { - if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(buffer, size, format, args); + if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(buffer, size, format, args); std::va_list copy; va_copy(copy, *reinterpret_cast(args)); const int result = std::vsnprintf(buffer, size, format, copy); @@ -517,7 +622,7 @@ int APS5_VABI vsprintf_s_nid_postfix(char* buffer, size_t size, const char* form int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); va_end(args); return result; } @@ -525,7 +630,7 @@ int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* forma int APS5_VABI snprintf_s_nid_postfix(char* buffer, size_t size, const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); + const int result = FormatSecure(buffer, size, format, args); va_end(args); return result; } @@ -533,7 +638,7 @@ int APS5_VABI snprintf_s_nid_postfix(char* buffer, size_t size, const char* form int APS5_VABI printf_s_nid_postfix(const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vfprintf(GuestStandardOutputFile_nid_no_patch(), format, args); va_end(args); return result; } @@ -541,7 +646,50 @@ int APS5_VABI printf_s_nid_postfix(const char* format, ...) { #endif int APS5_VABI puts_nid_postfix(const char* s) { - return std::puts(s); + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } + auto* native = GetNativeStream(&_Stdout_nid_postfix); + if (std::fputs(s, native) < 0 || std::fputc('\n', native) == EOF) return EOF; + return 0; +} + +extern FileStream* __stdinp_nid_postfix; + +int APS5_VABI vfscanf_nid_postfix(FileStream* stream, const char* format, VaList* args) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } + auto* native = GetNativeStream(stream); +#ifdef _WIN32 + const int result = LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); +#else + RejectGuestWideScan(format); + std::va_list copy; + va_copy(copy, *reinterpret_cast(args)); + const int result = HostVfscanf(native, format, copy); + va_end(copy); +#endif + stream->SyncStatus(); + stream->KeepDirectoryReadError(result == -1); + return result; +} + +int APS5_VABI vscanf_nid_postfix(const char* format, VaList* args) { + return vfscanf_nid_postfix(__stdinp_nid_postfix, format, args); +} + +int APS5_VABI scanf_nid_postfix(const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vfscanf_nid_postfix(__stdinp_nid_postfix, format, reinterpret_cast(args)); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; } } diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index e9a5661781..a2b3d0fe7d 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -167,6 +167,8 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = if (length == "h") value = static_cast(value); if (length == "hh") value = static_cast(value); } else value = args.Next(); + if (conversion == u'o' && value == 0 && precision == 0 && spec.find('#') != std::string::npos) + spec = spec.substr(0, spec.find('.')) + ".1"; AppendNumber(out, spec + "ll" + static_cast(conversion), value); } else if (In(conversion, "aAeEfFgG")) { if (length == "L") AppendNumber(out, spec + "L" + static_cast(conversion), args.Next()); @@ -215,6 +217,9 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = } // namespace extern "C" { +extern FileStream* __stdoutp_nid_postfix; +std::int32_t APS5_VABI fgetwc_nid_postfix(FileStream* stream); +int APS5_VABI ferror_nid_postfix(FileStream* stream); int APS5_VABI vswprintf_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, VaList* args) { if (buffer == nullptr || size == 0) { errno = 22; return -1; } @@ -322,7 +327,7 @@ int APS5_VABI wprintf_nid_postfix(const char16_t* format, ...) { try { const std::u16string text = FormatWide(format, reinterpret_cast(args)); const std::string bytes = ToUtf8(text); - if (std::fwrite(bytes.data(), 1, bytes.size(), stdout) == bytes.size()) result = static_cast(text.size()); + if (std::fwrite(bytes.data(), 1, bytes.size(), GetNativeStream(__stdoutp_nid_postfix)) == bytes.size()) result = static_cast(text.size()); } catch (const std::exception&) { errno = 22; } @@ -330,6 +335,32 @@ int APS5_VABI wprintf_nid_postfix(const char16_t* format, ...) { return result; } +int APS5_VABI vfwprintf_nid_postfix(FileStream* stream, const char16_t* format, VaList* args) { + if (stream == nullptr || format == nullptr) { errno = 22; return -1; } + if (!stream->IsOpen()) { errno = 9; return -1; } + int result = -1; + try { + const std::u16string text = FormatWide(format, args); + const std::string bytes = ToUtf8(text); + if (std::fwrite(bytes.data(), 1, bytes.size(), GetNativeStream(stream)) == bytes.size()) result = static_cast(text.size()); + } catch (const std::exception&) { + errno = 22; + } + stream->SyncStatus(); + return result; +} + +int APS5_VABI fwprintf_nid_postfix(FileStream* stream, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfwprintf_nid_postfix(stream, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + +int APS5_VABI vwprintf_nid_postfix(const char16_t* format, VaList* args) { + return vfwprintf_nid_postfix(__stdoutp_nid_postfix, format, args); +} + int APS5_VABI fputwc_nid_postfix(char16_t value, FileStream* stream) { if (stream == nullptr) { errno = 22; return -1; } try { @@ -359,4 +390,33 @@ int APS5_VABI fputws_nid_postfix(const char16_t* str, FileStream* stream) { } } +int APS5_VABI putwc_nid_postfix(char16_t value, FileStream* stream) { + return fputwc_nid_postfix(value, stream); +} + +int APS5_VABI putwchar_nid_postfix(char16_t value) { + return fputwc_nid_postfix(value, __stdoutp_nid_postfix); +} + +char16_t* APS5_VABI fgetws_nid_postfix(char16_t* buffer, int size, FileStream* stream) { + if (buffer == nullptr || stream == nullptr || size <= 0) { + if (stream != nullptr) stream->SetError(); + errno = 22; + return nullptr; + } + int count = 0; + while (count < size - 1) { + const std::int32_t character = fgetwc_nid_postfix(stream); + if (character == -1) { + if (count == 0) return nullptr; + break; + } + buffer[count++] = static_cast(character); + if (character == static_cast(u'\n')) break; + } + if (ferror_nid_postfix(stream) != 0) return nullptr; + buffer[count] = 0; + return buffer; +} + } diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 402da585ff..26c1935c5a 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #ifdef _WIN32 #ifndef NOMINMAX @@ -30,6 +31,8 @@ #include #endif #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/AppArchive.hpp" #include "prx/libc/include/ApplicationHeap.hpp" namespace { @@ -39,11 +42,12 @@ struct PathAliases { std::mutex mutex; std::vector> entries; std::vector blocked; + std::vector> providers; }; PathAliases& Aliases() { - static PathAliases aliases; - return aliases; + static auto* aliases = new PathAliases; + return *aliases; } std::string TrimSlashes(const char* path) { @@ -71,31 +75,173 @@ bool HasPrefix(const std::string& path, const std::string& prefix) { (path.size() == prefix.size() || path[prefix.size()] == '/'); } -std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::filesystem::path& relative) { - const auto direct = root / relative; +std::filesystem::path RelinkedModuleName(std::filesystem::path path) { + path += ".guest.prx"; + return path; +} + +struct NameMatch { + std::string name; + std::optional path; + bool ambiguous = false; + bool stem = false; +}; + +std::string_view NameStem(std::string_view name) { + const auto dot = name.rfind('.'); + return dot == std::string_view::npos || dot == 0 ? name : name.substr(0, dot); +} + +bool SameRelinkedStem(std::string_view filename, std::string_view name) { + constexpr std::string_view suffix = ".guest.prx"; + if (filename.size() <= suffix.size() || !SameName(filename.substr(filename.size() - suffix.size()), suffix)) return false; + return SameName(NameStem(filename.substr(0, filename.size() - suffix.size())), NameStem(name)); +} + +#ifdef _WIN32 +bool CaseInsensitiveDirectoryWindows(const std::filesystem::path& directory) { + const auto handle = CreateFileW(directory.c_str(), FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) return false; + const std::unique_ptr owner(handle, &CloseHandle); + struct { ULONG flags; } information{}; + constexpr auto caseSensitiveInfo = static_cast(23); + return GetFileInformationByHandleEx(handle, caseSensitiveInfo, &information, sizeof(information)) && + (information.flags & FILE_CS_FLAG_CASE_SENSITIVE_DIR) == 0; +} + +std::error_code MatchNamesWindows(const std::filesystem::path& directory, std::span names) { + for (auto& name : names) { + const auto pattern = directory / name.name; + WIN32_FIND_DATAW data{}; + const auto handle = FindFirstFileExW(pattern.c_str(), FindExInfoBasic, &data, FindExSearchNameMatch, nullptr, 0); + if (handle == INVALID_HANDLE_VALUE) { + const auto error = GetLastError(); + if (error == ERROR_FILE_NOT_FOUND || error == ERROR_NO_MORE_FILES) continue; + return {static_cast(error), std::system_category()}; + } + const std::unique_ptr owner(handle, &FindClose); + do { + const auto filename = std::filesystem::path(data.cFileName); + if (!SameName(filename.string(), name.name)) continue; + name.ambiguous = name.ambiguous || name.path.has_value(); + name.path = directory / filename; + } while (FindNextFileW(handle, &data)); + const auto error = GetLastError(); + if (error != ERROR_NO_MORE_FILES) return {static_cast(error), std::system_category()}; + } + return {}; +} +#endif + +std::error_code MatchNames(const std::filesystem::path& directory, std::span names) { +#ifdef _WIN32 + bool stem = false; + for (const auto& name : names) stem = stem || name.stem; + if (!stem && CaseInsensitiveDirectoryWindows(directory)) return MatchNamesWindows(directory, names); +#endif std::error_code error; + std::filesystem::directory_iterator entry(directory, error), end; + for (; !error && entry != end; entry.increment(error)) { + const auto filename = entry->path().filename().string(); + for (auto& name : names) { + if (name.stem) { + std::error_code status; + if (!SameRelinkedStem(filename, name.name) || !entry->is_regular_file(status) || status) continue; + if (!name.path || filename.size() < name.path->filename().string().size()) name.path = entry->path(); + continue; + } + if (!SameName(filename, name.name)) continue; + name.ambiguous = name.ambiguous || name.path.has_value(); + name.path = entry->path(); + } + } + return error; +} + +std::filesystem::path MatchedModuleOrPath(const std::filesystem::path& path, const std::filesystem::path& relinked, const NameMatch& match) { + if (match.ambiguous) throw std::runtime_error("Ambiguous case-insensitive guest path: " + relinked.string()); + std::error_code error; + return match.path && std::filesystem::is_regular_file(*match.path, error) ? *match.path : path; +} + +std::filesystem::path PathOrRelinkedModule(const std::filesystem::path& path) { + std::error_code error; + if (std::filesystem::exists(std::filesystem::symlink_status(path, error))) return path; + return RelinkedModulePath_nid_no_patch(path); +} + +std::vector SplitNames(std::string_view text) { + std::vector names; + while (!text.empty()) { + const auto separator = text.find('/'); + names.emplace_back(text.substr(0, separator)); + if (separator == std::string_view::npos) break; + text.remove_prefix(separator + 1); + if (text.empty()) names.emplace_back(); + } + return names; +} + +std::string JoinNames(const std::vector& names) { + std::string joined; + for (std::size_t i = 0; i < names.size(); ++i) { + if (i != 0) joined += '/'; + joined += names[i]; + } + return joined; +} + +std::filesystem::path Child(std::filesystem::path parent, const std::string& name) { + parent /= ""; + parent += name; + return parent; +} + +std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::vector& relative, bool module = false) { + const auto direct = Child(root, JoinNames(relative)); + std::error_code error; + auto first = relative.begin(); +#ifdef _WIN32 + const auto attributes = GetFileAttributesW(direct.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return direct; + const auto failure = GetLastError(); + const bool missing = attributes == INVALID_FILE_ATTRIBUTES && (failure == ERROR_FILE_NOT_FOUND || failure == ERROR_PATH_NOT_FOUND); + if (!missing && std::filesystem::exists(direct, error)) return direct; + for (auto last = relative.end(); last != relative.begin();) { + const auto parent = Child(root, JoinNames(std::vector(relative.begin(), std::prev(last)))); + const auto parentAttributes = GetFileAttributesW(parent.c_str()); + const bool present = parentAttributes != INVALID_FILE_ATTRIBUTES && (parentAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0; + if (!present && !std::filesystem::exists(parent, error)) { --last; continue; } + root = parent; + first = std::prev(last); + break; + } +#else if (std::filesystem::exists(direct, error)) return direct; - for (const auto& part : relative) { - const auto candidate = root / part; +#endif + for (auto part = first; part != relative.end(); ++part) { + const auto candidate = Child(root, *part); const auto status = std::filesystem::symlink_status(candidate, error); if (std::filesystem::exists(status) || (error && error != std::errc::no_such_file_or_directory)) { root = candidate; continue; } - std::optional matched; - const auto name = part.string(); - std::filesystem::directory_iterator entry(root, error), end; - for (; !error && entry != end; entry.increment(error)) { - if (!SameName(entry->path().filename().string(), name)) continue; - if (matched) throw std::runtime_error("Ambiguous case-insensitive guest path: " + candidate.string()); - matched = entry->path(); - } - root = !error && matched ? *matched : candidate; + const bool last = module && std::next(part) == relative.end(); + const auto relinked = RelinkedModuleName(candidate); + NameMatch names[] = {{*part}, {relinked.filename().string()}}; + error = MatchNames(root, std::span(names, last ? 2 : 1)); + if (names[0].ambiguous) throw std::runtime_error("Ambiguous case-insensitive guest path: " + candidate.string()); + const bool matched = !error && names[0].path; + root = matched ? *names[0].path : candidate; + if (!last || matched) continue; + if (std::filesystem::is_regular_file(relinked, error)) return relinked; + return MatchedModuleOrPath(candidate, relinked, names[1]); } - return root; + return module ? PathOrRelinkedModule(root) : root; } -std::optional ResolveAlias(const std::string& guestPath) { +std::optional ResolveAlias(const std::string& guestPath, bool module) { const auto relative = TrimSlashes(guestPath.c_str()); auto& aliases = Aliases(); std::lock_guard lock(aliases.mutex); @@ -105,12 +251,18 @@ std::optional ResolveAlias(const std::string& guestPath) } for (const auto& [prefix, host] : aliases.entries) { if (!HasPrefix(relative, prefix)) continue; - if (relative.size() == prefix.size()) return std::filesystem::path(host).make_preferred(); - std::filesystem::path result = ResolveHostPath(host, relative.substr(prefix.size() + 1)); + if (relative.size() == prefix.size()) { + const auto result = std::filesystem::path(host).make_preferred(); + return module ? PathOrRelinkedModule(result) : result; + } #ifndef _WIN32 - if (guestPath.back() == '/') result /= ""; + if (guestPath.back() == '/') { + auto result = ResolveHostPath(host, SplitNames(std::string_view(relative).substr(prefix.size() + 1))); + result /= ""; + return module ? PathOrRelinkedModule(result) : result; + } #endif - return result.make_preferred(); + return ResolveHostPath(host, SplitNames(std::string_view(relative).substr(prefix.size() + 1)), module).make_preferred(); } return std::nullopt; } @@ -120,13 +272,35 @@ struct WorkingDirectory { const std::filesystem::path root = std::filesystem::canonical(std::filesystem::current_path()); std::filesystem::path current = root; }; -WorkingDirectory& Directories() { static WorkingDirectory state; return state; } -std::filesystem::path HostPath(WorkingDirectory& state, const std::filesystem::path& guest) { - if (auto aliased = ResolveAlias(guest.relative_path().generic_string())) return *aliased; - return ResolveHostPath(state.root, guest.relative_path()).make_preferred(); +WorkingDirectory& Directories() { + static auto* state = new WorkingDirectory; + return *state; } -void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path& guest) { - const auto host = HostPath(state, guest); +std::optional ProvidedPath(const WorkingDirectory& state, const std::vector& names) { + const auto relative = TrimSlashes(JoinNames(names).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, names); + 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::vector& names, bool module = false) { + if (auto aliased = ResolveAlias(JoinNames(names), module)) return *aliased; + if (auto provided = ProvidedPath(state, names)) return *provided; + return ResolveHostPath(state.root, names, module).make_preferred(); +} +void RequireHostDirectory(WorkingDirectory& state, const std::vector& names) { + const auto host = HostPath(state, names); std::error_code error; const auto status = std::filesystem::status(host, error); if (std::filesystem::is_directory(status)) return; @@ -144,25 +318,49 @@ 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) { +std::vector WithTrailingSeparator(std::vector names, bool trailing) { + if (trailing && !names.empty()) names.emplace_back(); + return names; +} +std::filesystem::path Resolve(WorkingDirectory& state, const char* path, bool module = false, bool archived = false) { std::string text(path); + constexpr std::size_t MaxPath = 1024; + constexpr std::size_t MaxName = 255; + if (text.size() >= MaxPath) throw std::filesystem::filesystem_error("Guest path is too long", std::make_error_code(std::errc::filename_too_long)); + std::size_t name = 0; + for (std::size_t index = 0; index < text.size(); ++index) { + name = text[index] == '/' || text[index] == '\\' ? 0 : name + 1; + if (name <= MaxName) continue; + const std::size_t end = index + 1 - name; + std::error_code status; + if (end == 0 || std::filesystem::is_directory(Resolve(state, text.substr(0, end).c_str()), status)) { + throw std::filesystem::filesystem_error("Guest path name is too long", std::make_error_code(std::errc::filename_too_long)); + } + break; + } for (auto& character : text) if (character == '\\') character = '/'; - std::filesystem::path input(text); #ifdef _WIN32 // Preserve the existing ability to pass explicit native drive paths. - if (input.has_root_name()) return input; + if (std::filesystem::path input(text); input.has_root_name()) return module ? PathOrRelinkedModule(input) : input; #endif - const auto current = std::filesystem::path("/") / state.current.lexically_relative(state.root); - const auto relative = (input.is_absolute() ? input : current / input).relative_path(); - std::filesystem::path guest("/"); - 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 (text.empty() || text.front() != '/') text = state.current.lexically_relative(state.root).generic_string() + "/" + text; + std::vector names; + bool trailing = true; + bool endsWithDot = false; + const auto parts = SplitNames(text); + for (std::size_t i = 0; i < parts.size(); ++i) { + const auto& name = parts[i]; + if (name.empty() && i + 1 != parts.size()) continue; + if (!name.empty()) endsWithDot = name == "."; + if (name == ".." && !names.empty()) { + if (!(archived && AppArchiveHasDirectory_nid_no_patch(HostPath(state, names)))) RequireHostDirectory(state, WithTrailingSeparator(names, trailing)); + names.pop_back(); } - guest /= *part; + trailing = name.empty() || name == "." || name == ".."; + if (!trailing) names.push_back(name); } - return HostPath(state, guest.lexically_normal()); + if (endsWithDot && !names.empty() && !(archived && AppArchiveHasDirectory_nid_no_patch(HostPath(state, names)))) RequireHostDirectory(state, names); + return HostPath(state, WithTrailingSeparator(std::move(names), trailing), module); } #ifdef _WIN32 using WriteMark = FILETIME; @@ -250,9 +448,25 @@ int SetWorkingDirectory(WorkingDirectory& state, const std::filesystem::path& re const auto canonical = std::filesystem::canonical(resolved, error); if (error) return DirectoryFailure(error); if (!std::filesystem::is_directory(canonical, error)) return error ? DirectoryFailure(error) : 20; - const auto relative = canonical.lexically_relative(state.root); + auto relative = canonical.lexically_relative(state.root); +#ifdef _WIN32 + if (relative.empty() || *relative.begin() == "..") { + auto ancestor = canonical; + while (!std::filesystem::equivalent(ancestor, state.root, error)) { + if (error) return DirectoryFailure(error); + const auto parent = ancestor.parent_path(); + if (parent == ancestor) return 45; + ancestor = parent; + } + relative = canonical.lexically_relative(ancestor); + } +#endif if (relative.empty() || *relative.begin() == "..") return 45; +#ifdef _WIN32 + state.current = relative == "." ? state.root : state.root / relative; +#else state.current = canonical; +#endif return 0; } } @@ -287,6 +501,62 @@ extern "C" int KeepHostStandardError_nid_no_patch() { extern "C" int HostStandardError_nid_no_patch() { return g_hostStandardError.load(); } + +namespace { +std::mutex g_hostStandardOutputMutex; +std::atomic g_hostStandardOutput{-1}; +} + +extern "C" int KeepHostStandardOutput_nid_no_patch() { + const std::lock_guard lock(g_hostStandardOutputMutex); + if (const int kept = g_hostStandardOutput.load(); kept >= 0) return kept; + const int kept = ::fcntl(1, F_DUPFD_CLOEXEC, 3); + if (kept < 0) { + if (errno == EBADF) return -1; + throw std::system_error(errno, std::generic_category(), "Keeping the host standard output"); + } + std::FILE* const stream = ::fdopen(kept, "w"); + if (stream == nullptr) { + const int error = errno; + ::close(kept); + throw std::system_error(error, std::generic_category(), "Keeping the host standard output"); + } + std::setvbuf(stream, nullptr, _IONBF, 0); + std::fflush(stdout); + stdout = stream; + g_hostStandardOutput.store(kept); + return kept; +} + +extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor) { + return ::fcntl(descriptor, F_DUPFD_CLOEXEC, 3); +} +#endif + +#ifdef _WIN32 +namespace { +std::atomic g_hostStandardOutputFile{nullptr}; +std::atomic g_hostStandardErrorFile{nullptr}; +} + +extern "C" bool AdoptHostStandardStream_nid_no_patch(int standard, int saved) { + std::fflush(__acrt_iob_func(standard)); + std::FILE* stream = ::_fdopen(saved, "w"); + if (stream == nullptr) return false; + std::setvbuf(stream, nullptr, _IONBF, 0); + (standard == 1 ? g_hostStandardOutputFile : g_hostStandardErrorFile).store(stream, std::memory_order_release); + return true; +} + +extern "C" std::FILE* HostStandardOutputFile_nid_no_patch() { + if (std::FILE* kept = g_hostStandardOutputFile.load(std::memory_order_acquire)) return kept; + return __acrt_iob_func(1); +} + +extern "C" std::FILE* HostStandardErrorFile_nid_no_patch() { + if (std::FILE* kept = g_hostStandardErrorFile.load(std::memory_order_acquire)) return kept; + return __acrt_iob_func(2); +} #endif extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* hostPath) { @@ -306,6 +576,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; @@ -376,27 +663,42 @@ 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) { + 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) { - auto relinked = path; - relinked += ".guest.prx"; + const auto relinked = RelinkedModuleName(path); std::error_code error; if (std::filesystem::is_regular_file(relinked, error)) return relinked; - const auto name = relinked.filename().string(); - std::optional matched; - std::filesystem::directory_iterator entry(relinked.parent_path().empty() ? "." : relinked.parent_path(), error), end; - for (; !error && entry != end; entry.increment(error)) { - if (!SameName(entry->path().filename().string(), name)) continue; - if (matched) throw std::runtime_error("Ambiguous case-insensitive guest path: " + relinked.string()); - matched = entry->path(); - } - return matched && std::filesystem::is_regular_file(*matched, error) ? *matched : path; + NameMatch names[] = {{relinked.filename().string()}, {path.filename().string(), std::nullopt, false, true}}; + MatchNames(relinked.parent_path().empty() ? "." : relinked.parent_path(), names); + if (names[0].ambiguous || names[0].path) return MatchedModuleOrPath(path, relinked, names[0]); + return names[1].path ? *names[1].path : path; } extern "C" int SetWorkingDirectory_nid_no_patch(const char* resolved) { @@ -404,7 +706,7 @@ extern "C" int SetWorkingDirectory_nid_no_patch(const char* resolved) { auto& state = Directories(); std::lock_guard lock(state.mutex); try { - return SetWorkingDirectory(state, std::filesystem::u8path(resolved)); + return SetWorkingDirectory(state, std::filesystem::path(reinterpret_cast(resolved))); } catch (const std::bad_alloc&) { return 12; } catch (const std::filesystem::filesystem_error& error) { @@ -424,11 +726,13 @@ 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; } + if (error) { errno = error == 2 ? NotFoundError_nid_no_patch(resolved) : error; return -1; } return 0; } catch (const std::bad_alloc&) { errno = 12; return -1; } catch (const std::filesystem::filesystem_error& error) { errno = DirectoryFailure(error.code()); return -1; } @@ -446,8 +750,8 @@ extern "C" char* APS5_VABI getcwd_nid_postfix(char* buffer, std::size_t size) { const auto relative = state.current.lexically_relative(state.root); const auto path = relative == "." ? std::string("/") : "/" + relative.generic_string(); const auto required = path.size() + 1; - if ((buffer || size) && size < required) { errno = 34; return nullptr; } - if (!buffer) buffer = static_cast(ApplicationHeapAllocate_nid_no_patch(size ? size : required)); + if (buffer && size < required) { errno = 34; return nullptr; } + if (!buffer) buffer = static_cast(ApplicationHeapAllocate_nid_no_patch(required)); if (!buffer) { errno = 12; return nullptr; } std::memcpy(buffer, path.c_str(), required); return buffer; @@ -484,6 +788,8 @@ extern "C" int WindowsOpenError_nid_no_patch(unsigned long error) { return ENOMEM; case ERROR_DISK_FULL: return ENOSPC; + case ERROR_CANT_RESOLVE_FILENAME: + return ELOOP; default: return EINVAL; } @@ -510,8 +816,14 @@ extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle) { if (handle != nullptr && !SetStdHandle(StandardHandleId(descriptor), handle)) throw std::runtime_error("Cannot keep the host standard handle of descriptor " + std::to_string(descriptor)); } +extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor) { + const auto previous = _set_thread_local_invalid_parameter_handler([](const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {}); + const int kept = ::_dup(descriptor); + _set_thread_local_invalid_parameter_handler(previous); + return kept; +} #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 c7b286af87..96ff94d0bf 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -36,13 +36,30 @@ struct Registry { std::map> ranges; bool mainImageRegistered = false; std::map registeredImages; - std::weak_ptr readableLease; - std::weak_ptr allLease; + std::shared_ptr readableLease; + std::shared_ptr allLease; + std::uint64_t backingSerials = 0; }; Registry& registry() { - static Registry value; - return value; + static auto* value = new Registry; + return *value; +} + +std::uint64_t freshBacking() { + return ++registry().backingSerials; +} + +void renewBacking(std::uintptr_t address, std::size_t bytes) { + auto& ranges = registry().ranges; + auto it = ranges.upper_bound(address); + if (it != ranges.begin()) --it; + for (; it != ranges.end() && it->first < address + bytes; ++it) { + if (it->first + it->second->bytes <= address) continue; + auto renewed = *it->second; + renewed.backingSerial = freshBacking(); + it->second = std::make_shared(renewed); + } } std::atomic generation{1}; @@ -71,6 +88,7 @@ struct MutationState { std::unique_lock lock; std::vector> changed; std::vector> gpuMapped; + std::vector> unpinned; }; void recordChange(void* mutation, const void* pointer, std::size_t bytes) { @@ -95,12 +113,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; @@ -120,6 +150,7 @@ void* GuestAllocationsBegin_nid_postfix() { void GuestAllocationsEnd_nid_postfix(void* mutation) noexcept { auto* state = static_cast(mutation); + for (const auto& [address, bytes] : state->unpinned) renewBacking(address, bytes); const auto ended = generation.fetch_add(1, std::memory_order_release) + 1; if (const auto callback = invalidator.load(std::memory_order_acquire)) { for (const auto& [address, bytes] : state->changed) callback(address, bytes); @@ -202,7 +233,7 @@ void registerImage(const void* image) { const auto& previous = *std::prev(next)->second; require(previous.address + previous.bytes <= cursor, "guest image overlaps a registered allocation"); } - replacement.emplace(cursor, std::make_shared(Range{cursor, memory.RegionSize, readable, writable, cursor, memory.RegionSize, false})); + replacement.emplace(cursor, std::make_shared(Range{cursor, memory.RegionSize, readable, writable, cursor, memory.RegionSize, false, false, freshBacking()})); registered = true; } cursor += memory.RegionSize; @@ -227,43 +258,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; @@ -276,14 +306,49 @@ void GuestAllocationsRegisterMainImage_nid_postfix(void*) { const auto& previous = *std::prev(next)->second; require(previous.address + previous.bytes <= address, "guest image overlaps a registered allocation"); } - replacement.emplace(address, std::make_shared(Range{address, bytes, page->second.readable, page->second.writable, address, bytes, false})); + replacement.emplace(address, std::make_shared(Range{address, bytes, page->second.readable, page->second.writable, address, bytes, false, false, freshBacking()})); 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); @@ -296,7 +361,7 @@ void GuestAllocationsAdd_nid_postfix(void* mutation, void* pointer, std::size_t const auto& previous = *std::prev(next)->second; require(previous.address + previous.bytes <= address, "overlapping guest allocation"); } - ranges.emplace(address, std::make_shared(Range{address, bytes, readable, writable, address, bytes, true, gpu})); + ranges.emplace(address, std::make_shared(Range{address, bytes, readable, writable, address, bytes, true, gpu, freshBacking()})); } [[noreturn]] void PinnedFailure(std::uintptr_t address, std::size_t bytes, const char* why) { @@ -333,14 +398,22 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi break; } } - if (!pinned) return; + if (!pinned) { + if (state != nullptr && bytes != 0) state->unpinned.emplace_back(address, bytes); + return; + } if (std::chrono::steady_clock::now() >= deadline) PinnedFailure(address, bytes, ("waited " + std::to_string(pinWait().count()) + " ms for the lease (APS5_PIN_WAIT_MS)").c_str()); const auto waiter = pinWaiter.load(std::memory_order_acquire); 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(); @@ -378,6 +451,33 @@ bool GuestAllocationsOverlaps_nid_postfix(void*, const void* pointer, std::size_ return false; } +bool GuestAllocationsForRegistered_nid_postfix(void*, const void* pointer, std::size_t bytes, const std::function& visit) { + const auto address = reinterpret_cast(pointer); + require(address != 0 && bytes != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest allocation range"); + const auto end = address + bytes; + const auto& ranges = registry().ranges; + auto it = ranges.upper_bound(address); + if (it != ranges.begin()) --it; + std::uintptr_t first = 0; + std::uintptr_t last = 0; + bool any = false; + for (; it != ranges.end() && it->first < end; ++it) { + const auto base = std::max(it->first, address); + const auto finish = std::min(it->first + it->second->bytes, end); + if (finish <= base) continue; + if (any && base == last) { + last = finish; + continue; + } + if (any) visit(reinterpret_cast(first), last - first); + first = base; + last = finish; + any = true; + } + if (any) visit(reinterpret_cast(first), last - first); + return any; +} + bool GuestAllocationsCovers_nid_postfix(void*, const void* pointer, std::size_t bytes) { const auto address = reinterpret_cast(pointer); require(address != 0 && bytes != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest allocation range"); @@ -453,13 +553,13 @@ RangePatch replaceRange(const void* pointer, std::size_t bytes, bool remove, boo if (base >= end) break; require(base <= cursor, "guest protection or unmap range has a hole"); patch.erased.push_back(base); - const auto insert = [&](std::uint64_t first, std::uint64_t last, bool canRead, bool canWrite, bool gpuMapped) { - if (first < last) patch.inserted.emplace_back(first, std::make_shared(Range{first, static_cast(last - first), canRead, canWrite, range.allocationAddress, range.allocationBytes, range.releasable, gpuMapped})); + const auto insert = [&](std::uint64_t first, std::uint64_t last, bool canRead, bool canWrite, bool gpuMapped, std::uint64_t backing) { + if (first < last) patch.inserted.emplace_back(first, std::make_shared(Range{first, static_cast(last - first), canRead, canWrite, range.allocationAddress, range.allocationBytes, range.releasable, gpuMapped, backing})); }; - insert(base, std::max(base, address), range.readable, range.writable, range.gpu); - if (!remove) insert(std::max(base, address), std::min(finish, end), readable, writable, gpu); - insert(std::min(finish, end), finish, range.readable, range.writable, range.gpu); - cursor = std::min(finish, end); + insert(base, std::max(base, address), range.readable, range.writable, range.gpu, range.backingSerial); + if (!remove) insert(std::max(base, address), std::min(finish, end), readable, writable, gpu, freshBacking()); + insert(std::min(finish, end), finish, range.readable, range.writable, range.gpu, range.backingSerial); + cursor = std::min(finish, end); } require(cursor == end, "guest protection or unmap range is not registered"); return patch; @@ -533,7 +633,7 @@ void GuestAllocationsSetUnmapObserver_nid_postfix(void (*callback)(std::uintptr_ Lease GuestAllocationsAcquire_nid_postfix() { std::lock_guard lock(registry().mutex); - if (auto shared = registry().readableLease.lock()) return Lease(std::move(shared)); + if (auto shared = registry().readableLease) return Lease(std::move(shared)); auto ranges = std::make_shared(); for (const auto& [address, range] : registry().ranges) { if (range->readable && range->bytes != 0) ranges->push_back(range); @@ -560,7 +660,7 @@ std::shared_ptr GuestAllocationsLookup_nid_postfix(std::uintptr_t a Lease GuestAllocationsAcquireAll_nid_postfix() { std::lock_guard lock(registry().mutex); - if (auto shared = registry().allLease.lock()) return Lease(std::move(shared)); + if (auto shared = registry().allLease) return Lease(std::move(shared)); auto ranges = std::make_shared(); ranges->reserve(registry().ranges.size()); for (const auto& [address, range] : registry().ranges) { diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 5387ae0390..20cdbc1820 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #ifndef _WIN32 #include #include @@ -11,8 +12,13 @@ #include #include #endif +#ifdef __APPLE__ +#include +#include +#endif #include #include +#include #include #include #include @@ -27,16 +33,19 @@ namespace GuestArena { namespace { +std::atomic hostReadHook{nullptr}; + #ifdef _WIN32 std::atomic commitGeneration{1}; std::atomic privateMappingObserver{nullptr}; #endif +std::atomic accessObserver{nullptr}; -#ifdef ANYPS5_ENABLE_ASAN -constexpr std::uintptr_t GuestArenaOffset = 0x0000100000000000ull; -#else -constexpr std::uintptr_t GuestArenaOffset = 0; -#endif +void noteAccess(const void* pointer, std::size_t bytes) { + if (const auto callback = accessObserver.load(std::memory_order_acquire)) callback(reinterpret_cast(pointer), bytes); +} + +constexpr std::uintptr_t GuestArenaOffset = GuestArena::GuestAddressOffset; constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull + GuestArenaOffset; constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull + GuestArenaOffset; constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull + GuestArenaOffset; @@ -49,8 +58,8 @@ std::uintptr_t alignUp(std::uintptr_t value, std::size_t alignment) { class Arena { public: static Arena& Get() { - static Arena arena; - return arena; + static auto* arena = new Arena; + return *arena; } bool Available() const { @@ -67,21 +76,18 @@ public: } void* AllocateAtOrAbove(std::uintptr_t hint, std::size_t bytes, std::size_t alignment) { - if (_base == 0) throw std::runtime_error("guest address space arena is unavailable"); - if (alignment == 0 || (alignment & (alignment - 1)) != 0) throw std::invalid_argument("invalid guest arena alignment"); + RequireAllocatable(alignment); std::lock_guard lock(_lock); if (hint >= _end) throw std::runtime_error("mapping address hint is above the guest address space arena"); - std::uintptr_t candidate = alignUp(std::max(_base, hint), alignment); - auto it = _used.upper_bound(candidate); - if (it != _used.begin() && std::prev(it)->second > candidate) --it; - for (; it != _used.end(); ++it) { - const auto& [start, end] = *it; - if (candidate <= _end && bytes <= _end - candidate && candidate + bytes <= start) break; - candidate = std::max(candidate, alignUp(end, alignment)); - } - if (candidate > _end || bytes > _end - candidate) throw std::runtime_error("guest address space arena exhausted"); - _used.emplace(candidate, candidate + bytes); - return reinterpret_cast(candidate); + return Claim(FindAtOrAbove(hint, bytes, alignment), bytes); + } + + void* AllocateFromHint(std::uintptr_t hint, std::size_t bytes, std::size_t alignment) { + RequireAllocatable(alignment); + std::lock_guard lock(_lock); + auto candidate = hint < _end ? FindAtOrAbove(hint, bytes, alignment) : 0; + if (candidate == 0 && hint > _base) candidate = FindAtOrAbove(_base, bytes, alignment); + return Claim(candidate, bytes); } std::uintptr_t HighestFree(std::size_t bytes, std::size_t alignment) { @@ -99,6 +105,16 @@ public: return 0; } + bool NextUsed(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end) { + std::lock_guard lock(_lock); + auto it = _used.upper_bound(address); + if (it != _used.begin() && std::prev(it)->second > address) --it; + if (it == _used.end()) return false; + *start = it->first; + *end = it->second; + return true; + } + bool MarkUsedForCommit(const void* pointer, std::size_t bytes) { std::lock_guard lock(_lock); const auto start = reinterpret_cast(pointer); @@ -151,8 +167,8 @@ public: private: Arena() { -#if defined(__linux__) - SeedLinuxUsedRanges(); +#if defined(__linux__) || defined(__APPLE__) + SeedHostUsedRanges(); #elif defined(_WIN32) _writeWatched = std::getenv("APS5_NO_WRITE_WATCH") == nullptr; SYSTEM_INFO system{}; @@ -185,7 +201,8 @@ private: } #if defined(__linux__) - void SeedLinuxUsedRanges() { + static std::vector> HostMappings() { + std::vector> mappings; std::ifstream maps("/proc/self/maps"); if (!maps.is_open()) throw std::runtime_error("guest arena: cannot open /proc/self/maps"); std::string line; @@ -194,6 +211,48 @@ private: std::uintptr_t begin = 0, end = 0; char dash = 0; if (!(fields >> std::hex >> begin >> dash >> end) || dash != '-' || end <= begin) throw std::runtime_error("guest arena: malformed /proc/self/maps line: " + line); + mappings.emplace_back(begin, end); + } + return mappings; + } + + static int ReserveNoReplace(std::uintptr_t address, std::size_t bytes) { + void* const wanted = reinterpret_cast(address); + void* const placed = mmap(wanted, bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, -1, 0); + if (placed == wanted) return 0; + if (placed == MAP_FAILED) return errno; + munmap(placed, bytes); + return EINVAL; + } +#elif defined(__APPLE__) + static std::vector> HostMappings() { + std::vector> mappings; + mach_vm_address_t address = ArenaStart; + while (address < ApplicationAreaEnd) { + mach_vm_size_t size = 0; + vm_region_basic_info_data_64_t info{}; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + const kern_return_t result = mach_vm_region(mach_task_self(), &address, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast(&info), &count, &object); + if (result == KERN_INVALID_ADDRESS) break; + if (result != KERN_SUCCESS) throw std::runtime_error("guest arena: mach_vm_region failed"); + mappings.emplace_back(static_cast(address), static_cast(address + size)); + address += size; + } + return mappings; + } + + static int ReserveNoReplace(std::uintptr_t address, std::size_t bytes) { + mach_vm_address_t wanted = address; + const kern_return_t result = mach_vm_map(mach_task_self(), &wanted, bytes, 0, VM_FLAGS_FIXED, MEMORY_OBJECT_NULL, 0, FALSE, VM_PROT_NONE, VM_PROT_ALL, VM_INHERIT_DEFAULT); + if (result == KERN_SUCCESS) return 0; + return result == KERN_NO_SPACE ? EEXIST : ENOMEM; + } +#endif + +#if defined(__linux__) || defined(__APPLE__) + void SeedHostUsedRanges() { + for (const auto& [begin, end] : HostMappings()) { if (end <= ArenaStart || begin >= ApplicationAreaEnd) continue; const auto first = std::max(begin, ArenaStart); const auto last = std::min(end, ApplicationAreaEnd); @@ -211,12 +270,8 @@ private: constexpr std::uintptr_t kChunk = 1ULL << 30; for (std::uintptr_t cursor = from; cursor < to; cursor += kChunk) { const auto end = std::min(cursor + kChunk, to); - void* const wanted = reinterpret_cast(cursor); - void* const placed = mmap(wanted, end - cursor, PROT_NONE, - MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, -1, 0); - if (placed == wanted) continue; - const int failed = placed == MAP_FAILED ? errno : EINVAL; - if (placed != MAP_FAILED) munmap(placed, end - cursor); + const int failed = ReserveNoReplace(cursor, end - cursor); + if (failed == 0) continue; if (failed != EEXIST) { char message[128]; std::snprintf(message, sizeof(message), "guest arena: cannot reserve 0x%llx+0x%llx", @@ -248,6 +303,30 @@ private: std::map _used; std::vector> _holes; std::vector> _hostRegions; + void RequireAllocatable(std::size_t alignment) const { + if (_base == 0) throw std::runtime_error("guest address space arena is unavailable"); + if (alignment == 0 || (alignment & (alignment - 1)) != 0) throw std::invalid_argument("invalid guest arena alignment"); + } + + std::uintptr_t FindAtOrAbove(std::uintptr_t hint, std::size_t bytes, std::size_t alignment) const { + std::uintptr_t candidate = alignUp(std::max(_base, hint), alignment); + auto it = _used.upper_bound(candidate); + if (it != _used.begin() && std::prev(it)->second > candidate) --it; + for (; it != _used.end(); ++it) { + const auto& [start, end] = *it; + if (candidate <= _end && bytes <= _end - candidate && candidate + bytes <= start) break; + candidate = std::max(candidate, alignUp(end, alignment)); + } + if (candidate > _end || bytes > _end - candidate) return 0; + return candidate; + } + + void* Claim(std::uintptr_t candidate, std::size_t bytes) { + if (candidate == 0) throw std::runtime_error("guest address space arena exhausted"); + _used.emplace(candidate, candidate + bytes); + return reinterpret_cast(candidate); + } + std::uintptr_t _base = 0; std::uintptr_t _end = 0; bool _writeWatched = false; @@ -284,10 +363,18 @@ void* GuestArenaAllocateAtOrAbove_nid_postfix(std::uintptr_t hint, std::size_t b return Arena::Get().AllocateAtOrAbove(hint, bytes, alignment); } +void* GuestArenaAllocateFromHint_nid_postfix(std::uintptr_t hint, std::size_t bytes, std::size_t alignment) { + return Arena::Get().AllocateFromHint(hint, bytes, alignment); +} + std::uintptr_t GuestArenaHighestFree_nid_no_patch(std::size_t bytes, std::size_t alignment) { return Arena::Get().HighestFree(bytes, alignment); } +bool GuestArenaNextUsed_nid_no_patch(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end) { + return Arena::Get().NextUsed(address, start, end); +} + void GuestArenaMarkUsed_nid_postfix(const void* pointer, std::size_t bytes) { Arena::Get().MarkUsed(pointer, bytes); } @@ -314,6 +401,7 @@ std::invalid_argument OutsideArena(const char* operation, const void* pointer, s #ifdef _WIN32 void GuestArenaSetProtection_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uint32_t protection) { WindowsMappings::Get().SetProtection(address, bytes, protection); + noteAccess(reinterpret_cast(address), bytes); } bool GuestArenaHandleWrite_nid_postfix(std::uintptr_t address) { @@ -369,6 +457,7 @@ void GuestArenaCommit_nid_postfix(void* pointer, std::size_t bytes, std::uint32_ if (const auto callback = privateMappingObserver.load(std::memory_order_acquire)) { for (const auto& [address, size] : created) callback(address, size, generation); } + noteAccess(pointer, bytes); } void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes) { @@ -379,11 +468,34 @@ void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes) { void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection) { if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("shared mapping", pointer, bytes); WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched(), false); + noteAccess(pointer, bytes); } void GuestArenaMapDirect_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection) { if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("direct memory mapping", pointer, bytes); - WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched(), true); + const auto generation = commitGeneration.fetch_add(1, std::memory_order_acq_rel) + 1; + std::vector> created; + std::exception_ptr failure; + try { + WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched(), true, PAGE_EXECUTE_READWRITE, SIZE_MAX, &created); + } catch (...) { + failure = std::current_exception(); + } + try { + if (const auto callback = privateMappingObserver.load(std::memory_order_acquire)) { + for (const auto& [address, size] : created) callback(address, size, generation); + } + } catch (...) { + if (failure != nullptr) std::rethrow_exception(failure); + throw; + } + if (failure != nullptr) std::rethrow_exception(failure); + noteAccess(pointer, bytes); +} + +void GuestArenaMapFile_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection, std::uint32_t viewProtection, std::size_t sectionBytes) { + if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("file mapping", pointer, bytes); + WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched(), false, viewProtection, sectionBytes); } void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes) { @@ -401,9 +513,13 @@ bool GuestArenaResidentSpan_nid_postfix(std::uintptr_t address, std::size_t byte void GuestArenaForgetSection_nid_postfix(void* section) { WindowsMappings::Get().ForgetSection(section); } + +bool GuestArenaForgetReusableSection_nid_no_patch(void* section) { + return WindowsMappings::Get().ForgetSection(section); +} #endif -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) namespace { int PosixProtection(std::uint32_t windowsProtection) { switch (windowsProtection) { @@ -424,11 +540,12 @@ void* PlaceAt(void* pointer, std::size_t bytes, std::uint32_t protection) { } #endif -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) void GuestArenaSetProtection_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uint32_t protection) { if (!Arena::Get().Contains(reinterpret_cast(address), bytes)) throw OutsideArena("set protection", reinterpret_cast(address), bytes); if (mprotect(reinterpret_cast(address), bytes, PosixProtection(protection)) != 0) throw std::system_error(errno, std::generic_category(), "guest arena protection failed"); + noteAccess(reinterpret_cast(address), bytes); } void GuestArenaCommit_nid_postfix(void* pointer, std::size_t bytes, std::uint32_t protection, std::size_t granule) { @@ -441,6 +558,7 @@ void GuestArenaCommit_nid_postfix(void* pointer, std::size_t bytes, std::uint32_ if (marked) Arena::Get().Release(pointer, bytes); throw; } + noteAccess(pointer, bytes); } void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes) { @@ -453,6 +571,20 @@ bool GuestArenaWriteWatched_nid_postfix() { return Arena::Get().WriteWatched(); } +void GuestArenaSetAccessObserver_nid_postfix(void (*callback)(std::uintptr_t address, std::size_t bytes)) { + accessObserver.store(callback, std::memory_order_release); +} + +void GuestArenaSetHostReadHook_nid_no_patch(void (*hook)(std::uintptr_t, std::size_t)) { + hostReadHook.store(hook, std::memory_order_release); +} + +void GuestArenaPrepareHostRead_nid_no_patch(const void* pointer, std::size_t bytes) { + const auto address = reinterpret_cast(pointer); + if (address == 0 || bytes == 0 || bytes > std::numeric_limits::max() - address) return; + if (const auto hook = hostReadHook.load(std::memory_order_acquire)) hook(address, bytes); +} + bool GuestArenaBeginHostWrite_nid_postfix(void* pointer, std::size_t bytes) { #ifdef _WIN32 return WindowsMappings::Get().BeginHostWrite(reinterpret_cast(pointer), bytes); @@ -462,6 +594,16 @@ bool GuestArenaBeginHostWrite_nid_postfix(void* pointer, std::size_t bytes) { #endif } +#ifdef _WIN32 +bool GuestArenaBeginHostImport_nid_no_patch(void* pointer, std::size_t bytes) { + return WindowsMappings::Get().BeginHostWrite(reinterpret_cast(pointer), bytes, false); +} + +void GuestArenaEndHostImport_nid_no_patch(void* pointer, std::size_t bytes) { + WindowsMappings::Get().EndHostWrite(reinterpret_cast(pointer), bytes, false); +} +#endif + void GuestArenaEndHostWrite_nid_postfix(void* pointer, std::size_t bytes) { #ifdef _WIN32 WindowsMappings::Get().EndHostWrite(reinterpret_cast(pointer), bytes); diff --git a/core/libs/prx/libc/src/GuestHeap.cpp b/core/libs/prx/libc/src/GuestHeap.cpp index d35ccdb451..96c95d1063 100644 --- a/core/libs/prx/libc/src/GuestHeap.cpp +++ b/core/libs/prx/libc/src/GuestHeap.cpp @@ -1,8 +1,10 @@ #include "prx/libc/include/GuestHeap.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/MallocManagedSize.hpp" #include #include +#include #include #include #include @@ -38,6 +40,28 @@ std::size_t alignUp(std::size_t value, std::size_t alignment) { return (value + alignment - 1) & ~(alignment - 1); } +struct Usage { + std::atomic system{0}; + std::atomic maxSystem{0}; + std::atomic inUse{0}; + std::atomic maxInUse{0}; +}; + +Usage& usage() { + static Usage value; + return value; +} + +void grow(std::atomic& current, std::atomic& peak, std::size_t bytes) { + const auto now = current.fetch_add(bytes, std::memory_order_relaxed) + bytes; + auto seen = peak.load(std::memory_order_relaxed); + while (now > seen && !peak.compare_exchange_weak(seen, now, std::memory_order_relaxed)) {} +} + +void shrink(std::atomic& current, std::size_t bytes) { + current.fetch_sub(bytes, std::memory_order_relaxed); +} + class ArenaHeap { public: static ArenaHeap& Get() { @@ -54,6 +78,7 @@ public: if (void* cached = _large.Take(blockBytes)) return cached; void* raw = arenaAllocate(blockBytes, PageBytes); commit(raw, blockBytes); + grow(usage().system, usage().maxSystem, blockBytes); return raw; } auto& sizeClass = _classes[classIndex(total)]; @@ -67,6 +92,7 @@ public: if (sizeClass.bump == nullptr || sizeClass.bytes > static_cast(sizeClass.bumpEnd - sizeClass.bump)) { auto* span = static_cast(arenaAllocate(SpanBytes, PageBytes)); commit(span, SpanBytes); + grow(usage().system, usage().maxSystem, SpanBytes); sizeClass.bump = span; sizeClass.bumpEnd = span + SpanBytes; } @@ -202,6 +228,7 @@ private: GuestArena::GuestArenaReset_nid_postfix(raw, bytes); GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(raw), bytes); GuestArena::GuestArenaRelease_nid_postfix(raw, bytes); + shrink(usage().system, bytes); } std::mutex _lock; @@ -219,12 +246,17 @@ void* rawAllocate(std::size_t total, std::size_t& blockBytes) { blockBytes = total; void* raw = std::malloc(total); if (raw == nullptr) throw std::bad_alloc(); + grow(usage().system, usage().maxSystem, total); return raw; } void rawFree(void* raw, std::size_t blockBytes) { - if (GuestArena::GuestArenaAvailable_nid_postfix()) ArenaHeap::Get().Free(raw, blockBytes); - else std::free(raw); + if (GuestArena::GuestArenaAvailable_nid_postfix()) { + ArenaHeap::Get().Free(raw, blockBytes); + } else { + std::free(raw); + shrink(usage().system, blockBytes); + } } void* allocate(GuestAllocations::Mutation& mutation, std::size_t alignment, std::size_t bytes, bool gpu) { @@ -244,12 +276,15 @@ void* allocate(GuestAllocations::Mutation& mutation, std::size_t alignment, std: rawFree(raw, blockBytes); throw; } + grow(usage().inUse, usage().maxInUse, blockBytes); return pointer; } void free(GuestAllocations::Mutation& mutation, void* pointer) { mutation.Remove(pointer); - rawFree(reinterpret_cast(pointer)[-2], reinterpret_cast(pointer)[-1]); + const auto blockBytes = reinterpret_cast(pointer)[-1]; + shrink(usage().inUse, blockBytes); + rawFree(reinterpret_cast(pointer)[-2], blockBytes); } } @@ -313,4 +348,15 @@ void* GuestHeapAlignForGpu_nid_postfix(std::size_t alignment, std::size_t bytes) return allocate(mutation, alignment, bytes, true); } +int GuestHeapStatsFast_nid_postfix(void* stats) { + auto* managed = static_cast(stats); + if (managed == nullptr || managed->size < sizeof(MallocManagedSize)) return 22; + auto& value = usage(); + managed->maxSystemSize = value.maxSystem.load(std::memory_order_relaxed); + managed->currentSystemSize = value.system.load(std::memory_order_relaxed); + managed->maxInuseSize = value.maxInUse.load(std::memory_order_relaxed); + managed->currentInuseSize = value.inUse.load(std::memory_order_relaxed); + return 0; +} + } diff --git a/core/libs/prx/libc/src/GuestWriteWatch.cpp b/core/libs/prx/libc/src/GuestWriteWatch.cpp index 55f991f047..e0a4344e67 100644 --- a/core/libs/prx/libc/src/GuestWriteWatch.cpp +++ b/core/libs/prx/libc/src/GuestWriteWatch.cpp @@ -29,7 +29,7 @@ #include #include #include -#include +#include #include #include #endif @@ -47,8 +47,8 @@ constexpr std::uintptr_t PageBytes = 4096; class Watch { public: static Watch& Get() { - static Watch watch; - return watch; + static auto* watch = new Watch; + return *watch; } bool Available() const { @@ -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); @@ -321,8 +328,8 @@ const bool g_opened = (Watch::Get(), true); class Watch { public: static Watch& Get() { - static Watch watch; - return watch; + static auto* watch = new Watch; + return *watch; } bool Available() const { @@ -330,9 +337,10 @@ public: } void Register(std::uintptr_t begin, std::uintptr_t end) { + if (!_available || end <= begin) return; begin = pageStart(begin); end = pageEnd(end); - if (!_available || end <= begin) return; + if (end <= begin) return; std::vector pages((end - begin) / _page, Written | Fresh); std::vector queried(pages.size(), false); const Lock lock(_lock); @@ -364,9 +372,10 @@ public: } bool Unregister(std::uintptr_t begin, std::uintptr_t end) { + if (!_available || end <= begin) return false; begin = pageStart(begin); end = pageEnd(end); - if (!_available || end <= begin) return false; + if (end <= begin) return false; const Lock lock(_lock); return erase(begin, end); } @@ -377,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) { @@ -407,9 +423,10 @@ public: } void ProtectionChanged(std::uintptr_t begin, std::uintptr_t end, int protection) { + if (!_available || end <= begin) return; begin = pageStart(begin); end = pageEnd(end); - if (!_available || end <= begin) return; + if (end <= begin) return; const Lock lock(_lock); forEachPage(begin, end, [&](std::uintptr_t, std::uint8_t& state) { state = static_cast(Written | (state & Fresh) | (protection & ProtectionMask)); @@ -417,9 +434,10 @@ public: } void BeginHostWrite(std::uintptr_t begin, std::uintptr_t end) { + if (!_available || end <= begin) return; begin = pageStart(begin); end = pageEnd(end); - if (!_available || end <= begin) return; + if (end <= begin) return; const Lock lock(_lock); _hostWrites.emplace_back(begin, end); forEachPage(begin, end, [&](std::uintptr_t page, std::uint8_t& state) { @@ -429,9 +447,10 @@ public: } void EndHostWrite(std::uintptr_t begin, std::uintptr_t end) { + if (!_available || end <= begin) return; begin = pageStart(begin); end = pageEnd(end); - if (!_available || end <= begin) return; + if (end <= begin) return; const Lock lock(_lock); const auto found = std::find(_hostWrites.begin(), _hostWrites.end(), std::pair{begin, end}); if (found != _hostWrites.end()) _hostWrites.erase(found); @@ -465,20 +484,41 @@ private: std::vector pages; }; - struct Turns { - std::atomic next{0}; - std::atomic serving{0}; + struct Mutex { + Mutex() { + pthread_mutexattr_t attributes; + pthread_mutexattr_init(&attributes); + pthread_mutexattr_setpolicy_np(&attributes, PTHREAD_MUTEX_POLICY_FIRSTFIT_NP); + pthread_mutex_init(&mutex, &attributes); + pthread_mutexattr_destroy(&attributes); + pthread_cond_init(&idle, nullptr); + } + pthread_mutex_t mutex; + pthread_cond_t idle; + std::atomic faulting{0}; }; struct Lock { - explicit Lock(Turns& turns) : _turns(turns), _turn(turns.next.fetch_add(1, std::memory_order_relaxed)) { - while (_turns.serving.load(std::memory_order_acquire) != _turn) ::sched_yield(); + explicit Lock(Mutex& mutex) : _mutex(mutex) { + pthread_mutex_lock(&_mutex.mutex); + while (_mutex.faulting.load(std::memory_order_acquire) != 0) pthread_cond_wait(&_mutex.idle, &_mutex.mutex); } ~Lock() { - _turns.serving.store(_turn + 1, std::memory_order_release); + pthread_mutex_unlock(&_mutex.mutex); } - Turns& _turns; - std::uint64_t _turn; + Mutex& _mutex; + }; + + struct FaultLock { + explicit FaultLock(Mutex& mutex) : _mutex(mutex) { + _mutex.faulting.fetch_add(1, std::memory_order_acq_rel); + pthread_mutex_lock(&_mutex.mutex); + } + ~FaultLock() { + if (_mutex.faulting.fetch_sub(1, std::memory_order_acq_rel) == 1) pthread_cond_broadcast(&_mutex.idle); + pthread_mutex_unlock(&_mutex.mutex); + } + Mutex& _mutex; }; Watch() { @@ -511,7 +551,7 @@ private: bool record(std::uintptr_t address) { const auto page = pageStart(address); - const Lock lock(_lock); + const FaultLock lock(_lock); auto it = _ranges.upper_bound(page); if (it == _ranges.begin()) return false; --it; @@ -604,7 +644,7 @@ private: const std::uintptr_t _page = static_cast(::getpagesize()); bool _available = false; - Turns _lock; + Mutex _lock; std::map _ranges; std::vector> _hostWrites; struct sigaction _previousBus {}; @@ -655,6 +695,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/HeapDiagnostics.cpp b/core/libs/prx/libc/src/HeapDiagnostics.cpp index 135c46f928..04324b8d39 100644 --- a/core/libs/prx/libc/src/HeapDiagnostics.cpp +++ b/core/libs/prx/libc/src/HeapDiagnostics.cpp @@ -9,7 +9,11 @@ namespace { std::uint64_t mspaceAtomicIdMask = 0; -std::array mstateTable{}; +constexpr std::size_t MspaceTableCapacity = 65; +std::array mstateTable{}; + +static_assert(MspaceTableCapacity == 65); +static_assert(mstateTable.size() == MspaceTableCapacity); static_assert(sizeof(LibcHeapInfo) == 32); static_assert(offsetof(LibcHeapInfo, mspace_atomic_id_mask) == 16); diff --git a/core/libs/prx/libc/src/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/IntegerBuiltins.cpp b/core/libs/prx/libc/src/IntegerBuiltins.cpp new file mode 100644 index 0000000000..2df42c3913 --- /dev/null +++ b/core/libs/prx/libc/src/IntegerBuiltins.cpp @@ -0,0 +1,349 @@ +#include +#include +#include +#include + +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/SonyCrt.hpp" + +namespace { + +using Int128 = __int128; +using Uint128 = unsigned __int128; + +constexpr Uint128 Int128SignBit = static_cast(1) << 127; +constexpr Int128 Int128Max = static_cast(~Int128SignBit); +constexpr Int128 Int128Min = static_cast(Int128SignBit); + +std::uint64_t High(Uint128 value) { return static_cast(value >> 64); } +std::uint64_t Low(Uint128 value) { return static_cast(value); } + +template +struct IntegerTraits; + +template<> +struct IntegerTraits { + using Unsigned = std::uint32_t; + static constexpr std::int32_t Min = INT32_MIN; + static constexpr std::int32_t Max = INT32_MAX; +}; + +template<> +struct IntegerTraits { + using Unsigned = std::uint64_t; + static constexpr std::int64_t Min = INT64_MIN; + static constexpr std::int64_t Max = INT64_MAX; +}; + +template<> +struct IntegerTraits { + using Unsigned = Uint128; + static constexpr Int128 Min = Int128Min; +}; + +[[noreturn]] void Overflow(const char* function) { + throw std::runtime_error(std::string(function) + ": integer overflow"); +} + +[[noreturn]] void DivideByZero(const char* function) { + throw std::runtime_error(std::string(function) + ": integer divide by zero"); +} + +template +TSigned Divide(TSigned dividend, TSigned divisor, const char* function) { + if (divisor == 0) DivideByZero(function); + if (dividend == IntegerTraits::Min && divisor == -1) return dividend; + return dividend / divisor; +} + +template +TSigned Modulo(TSigned dividend, TSigned divisor, const char* function) { + if (divisor == 0) DivideByZero(function); + if (divisor == -1) return 0; + return dividend % divisor; +} + +template +TUnsigned UnsignedDivide(TUnsigned dividend, TUnsigned divisor, const char* function) { + if (divisor == 0) DivideByZero(function); + return dividend / divisor; +} + +template +TUnsigned UnsignedModulo(TUnsigned dividend, TUnsigned divisor, const char* function) { + if (divisor == 0) DivideByZero(function); + return dividend % divisor; +} + +template +TSigned WrappingAdd(TSigned a, TSigned b) { + using TUnsigned = typename IntegerTraits::Unsigned; + return static_cast(static_cast(a) + static_cast(b)); +} + +template +TSigned WrappingSubtract(TSigned a, TSigned b) { + using TUnsigned = typename IntegerTraits::Unsigned; + return static_cast(static_cast(a) - static_cast(b)); +} + +std::uint32_t UnsignedDivide32(std::uint32_t dividend, std::uint32_t divisor) { + return divisor == 0 ? 0 : dividend / divisor; +} + +template +TSigned CheckedNegate(TSigned value, const char* function) { + if (value == IntegerTraits::Min) Overflow(function); + return -value; +} + +template +TSigned CheckedAbsolute(TSigned value, const char* function) { + if (value == IntegerTraits::Min) Overflow(function); + return value < 0 ? -value : value; +} + +template +TSigned MultiplyWithOverflow(TSigned a, TSigned b, int* overflow) { + using TUnsigned = typename IntegerTraits::Unsigned; + const TSigned product = static_cast(static_cast(a) * static_cast(b)); + constexpr TSigned min = IntegerTraits::Min; + constexpr TSigned max = IntegerTraits::Max; + bool overflowed = false; + if (a == min) { + overflowed = b != 0 && b != 1; + } else if (b == min) { + overflowed = a != 0 && a != 1; + } else { + const TSigned absoluteA = a < 0 ? -a : a; + const TSigned absoluteB = b < 0 ? -b : b; + if (absoluteA >= 2 && absoluteB >= 2) { + overflowed = ((a < 0) == (b < 0)) ? absoluteA > max / absoluteB : absoluteA > -(min / absoluteB); + } + } + *overflow = overflowed ? 1 : 0; + return product; +} + +Int128 Multiply128(Int128 a, Int128 b) { + const Uint128 ua = static_cast(a); + const Uint128 ub = static_cast(b); + const std::uint64_t aLow = Low(ua), aHigh = High(ua), bLow = Low(ub), bHigh = High(ub); + const std::uint64_t aLowLow = aLow & 0xffffffffu, aLowHigh = aLow >> 32; + const std::uint64_t bLowLow = bLow & 0xffffffffu, bLowHigh = bLow >> 32; + const std::uint64_t lowLow = aLowLow * bLowLow; + const std::uint64_t middleFirst = aLowHigh * bLowLow + (lowLow >> 32); + const std::uint64_t middleSecond = aLowLow * bLowHigh + (middleFirst & 0xffffffffu); + const std::uint64_t productLow = (middleSecond << 32) | (lowLow & 0xffffffffu); + const std::uint64_t productHigh = aLowHigh * bLowHigh + (middleFirst >> 32) + (middleSecond >> 32) + + aHigh * bLow + aLow * bHigh; + return static_cast((static_cast(productHigh) << 64) | productLow); +} + +Int128 Multiply128WithOverflow(Int128 a, Int128 b, int* overflow) { + const Int128 product = Multiply128(a, b); + bool overflowed = false; + if (a == Int128Min) { + overflowed = b != 0 && b != 1; + } else if (b == Int128Min) { + overflowed = a != 0 && a != 1; + } else { + const Int128 absoluteA = a < 0 ? -a : a; + const Int128 absoluteB = b < 0 ? -b : b; + if (absoluteA >= 2 && absoluteB >= 2) { + const Uint128 limit = ((a < 0) == (b < 0)) ? static_cast(Int128Max) : Int128SignBit; + overflowed = static_cast(absoluteA) > SonyUdivti3(limit, static_cast(absoluteB)); + } + } + *overflow = overflowed ? 1 : 0; + return product; +} + +int CountLeadingZeros128(Uint128 value) { + const std::uint64_t high = High(value); + return high != 0 ? std::countl_zero(high) : 64 + std::countl_zero(Low(value)); +} + +int CountTrailingZeros128(Uint128 value) { + const std::uint64_t low = Low(value); + return low != 0 ? std::countr_zero(low) : 64 + std::countr_zero(High(value)); +} + +template +int Compare(TValue a, TValue b) { + if (a < b) return 0; + return a > b ? 2 : 1; +} + +} + +extern "C" { + +std::int64_t APS5_VABI __ashldi3_nid_postfix(std::int64_t value, int shift) { + return static_cast(static_cast(value) << shift); +} + +std::int64_t APS5_VABI __ashrdi3_nid_postfix(std::int64_t value, int shift) { return value >> shift; } + +std::int64_t APS5_VABI __lshrdi3_nid_postfix(std::int64_t value, int shift) { + return static_cast(static_cast(value) >> shift); +} + +Int128 APS5_VABI __ashlti3_nid_postfix(Int128 value, int shift) { + return static_cast(static_cast(value) << shift); +} + +Int128 APS5_VABI __ashrti3_nid_postfix(Int128 value, int shift) { return value >> shift; } + +Int128 APS5_VABI __lshrti3_nid_postfix(Int128 value, int shift) { + return static_cast(static_cast(value) >> shift); +} + +int APS5_VABI __clzsi2_nid_postfix(std::uint32_t value) { return std::countl_zero(value); } +int APS5_VABI __clzdi2_nid_postfix(std::uint64_t value) { return value == 0 ? 32 : std::countl_zero(value); } +int APS5_VABI __clzti2_nid_postfix(Uint128 value) { return value == 0 ? 64 : CountLeadingZeros128(value); } +int APS5_VABI __ctzsi2_nid_postfix(std::uint32_t value) { return std::countr_zero(value); } +int APS5_VABI __ctzdi2_nid_postfix(std::uint64_t value) { return value == 0 ? 32 : std::countr_zero(value); } +int APS5_VABI __ctzti2_nid_postfix(Uint128 value) { return value == 0 ? 64 : CountTrailingZeros128(value); } + +int APS5_VABI __ffsdi2_nid_postfix(std::uint64_t value) { + return value == 0 ? 0 : std::countr_zero(value) + 1; +} + +int APS5_VABI __ffsti2_nid_postfix(Uint128 value) { + return value == 0 ? 0 : CountTrailingZeros128(value) + 1; +} + +int APS5_VABI __paritysi2_nid_postfix(std::uint32_t value) { return std::popcount(value) & 1; } +int APS5_VABI __paritydi2_nid_postfix(std::uint64_t value) { return std::popcount(value) & 1; } + +int APS5_VABI __parityti2_nid_postfix(Uint128 value) { + return (std::popcount(High(value)) + std::popcount(Low(value))) & 1; +} + +int APS5_VABI __popcountsi2_nid_postfix(std::uint32_t value) { return std::popcount(value); } +int APS5_VABI __popcountdi2_nid_postfix(std::uint64_t value) { return std::popcount(value); } + +int APS5_VABI __popcountti2_nid_postfix(Uint128 value) { + return std::popcount(High(value)) + std::popcount(Low(value)); +} + +int APS5_VABI __cmpdi2_nid_postfix(std::int64_t a, std::int64_t b) { return Compare(a, b); } +int APS5_VABI __cmpti2_nid_postfix(Int128 a, Int128 b) { return Compare(a, b); } +int APS5_VABI __ucmpdi2_nid_postfix(std::uint64_t a, std::uint64_t b) { return Compare(a, b); } +int APS5_VABI __ucmpti2_nid_postfix(Uint128 a, Uint128 b) { return Compare(a, b); } + +std::int64_t APS5_VABI __negdi2_nid_postfix(std::int64_t value) { + return static_cast(0 - static_cast(value)); +} + +Int128 APS5_VABI __negti2_nid_postfix(Int128 value) { + return static_cast(0 - static_cast(value)); +} + +std::int64_t APS5_VABI __muldi3_nid_postfix(std::int64_t a, std::int64_t b) { + return static_cast(static_cast(a) * static_cast(b)); +} + +Int128 APS5_VABI __multi3_nid_postfix(Int128 a, Int128 b) { return Multiply128(a, b); } + +std::int32_t APS5_VABI __divsi3_nid_postfix(std::int32_t a, std::int32_t b) { return Divide(a, b, "__divsi3"); } +std::int64_t APS5_VABI __divdi3_nid_postfix(std::int64_t a, std::int64_t b) { return Divide(a, b, "__divdi3"); } +std::int32_t APS5_VABI __modsi3_nid_postfix(std::int32_t a, std::int32_t b) { return Modulo(a, b, "__modsi3"); } +std::int64_t APS5_VABI __moddi3_nid_postfix(std::int64_t a, std::int64_t b) { return Modulo(a, b, "__moddi3"); } + +std::uint32_t APS5_VABI __udivsi3_nid_postfix(std::uint32_t a, std::uint32_t b) { + return UnsignedDivide32(a, b); +} + +std::uint64_t APS5_VABI __udivdi3_nid_postfix(std::uint64_t a, std::uint64_t b) { + return UnsignedDivide(a, b, "__udivdi3"); +} + +std::uint32_t APS5_VABI __umodsi3_nid_postfix(std::uint32_t a, std::uint32_t b) { + return a - UnsignedDivide32(a, b) * b; +} + +std::uint64_t APS5_VABI __umoddi3_nid_postfix(std::uint64_t a, std::uint64_t b) { + return UnsignedModulo(a, b, "__umoddi3"); +} + +std::int32_t APS5_VABI __divmodsi4_nid_postfix(std::int32_t a, std::int32_t b, std::int32_t* remainder) { + const std::int32_t quotient = Divide(a, b, "__divmodsi4"); + *remainder = static_cast(static_cast(a) - static_cast(quotient) * static_cast(b)); + return quotient; +} + +std::int64_t APS5_VABI __divmoddi4_nid_postfix(std::int64_t a, std::int64_t b, std::int64_t* remainder) { + const std::int64_t quotient = Divide(a, b, "__divmoddi4"); + *remainder = static_cast(static_cast(a) - static_cast(quotient) * static_cast(b)); + return quotient; +} + +std::uint32_t APS5_VABI __udivmodsi4_nid_postfix(std::uint32_t a, std::uint32_t b, std::uint32_t* remainder) { + const std::uint32_t quotient = UnsignedDivide32(a, b); + *remainder = a - quotient * b; + return quotient; +} + +std::uint64_t APS5_VABI __udivmoddi4_nid_postfix(std::uint64_t a, std::uint64_t b, std::uint64_t* remainder) { + const std::uint64_t quotient = UnsignedDivide(a, b, "__udivmoddi4"); + if (remainder != nullptr) *remainder = a - quotient * b; + return quotient; +} + +Uint128 APS5_VABI __udivmodti4_nid_postfix(Uint128 a, Uint128 b, Uint128* remainder) { + if (b == 0) DivideByZero("__udivmodti4"); + Uint128 rest = 0; + const Uint128 quotient = SonyUdivmod128(a, b, &rest); + if (remainder != nullptr) *remainder = rest; + return quotient; +} + +std::int32_t APS5_VABI __mulosi4_nid_postfix(std::int32_t a, std::int32_t b, int* overflow) { + return MultiplyWithOverflow(a, b, overflow); +} + +std::int64_t APS5_VABI __mulodi4_nid_postfix(std::int64_t a, std::int64_t b, int* overflow) { + return MultiplyWithOverflow(a, b, overflow); +} + +Int128 APS5_VABI __muloti4_nid_postfix(Int128 a, Int128 b, int* overflow) { + return Multiply128WithOverflow(a, b, overflow); +} + +std::int32_t APS5_VABI __mulvsi3_nid_postfix(std::int32_t a, std::int32_t b) { + int overflow = 0; + const std::int32_t product = MultiplyWithOverflow(a, b, &overflow); + if (overflow) Overflow("__mulvsi3"); + return product; +} + +std::int64_t APS5_VABI __mulvdi3_nid_postfix(std::int64_t a, std::int64_t b) { + int overflow = 0; + const std::int64_t product = MultiplyWithOverflow(a, b, &overflow); + if (overflow) Overflow("__mulvdi3"); + return product; +} + +Int128 APS5_VABI __mulvti3_nid_postfix(Int128 a, Int128 b) { + int overflow = 0; + const Int128 product = Multiply128WithOverflow(a, b, &overflow); + if (overflow) Overflow("__mulvti3"); + return product; +} + +std::int32_t APS5_VABI __addvsi3_nid_postfix(std::int32_t a, std::int32_t b) { return WrappingAdd(a, b); } +std::int64_t APS5_VABI __addvdi3_nid_postfix(std::int64_t a, std::int64_t b) { return WrappingAdd(a, b); } +Int128 APS5_VABI __addvti3_nid_postfix(Int128 a, Int128 b) { return WrappingAdd(a, b); } +std::int32_t APS5_VABI __subvsi3_nid_postfix(std::int32_t a, std::int32_t b) { return WrappingSubtract(a, b); } +std::int64_t APS5_VABI __subvdi3_nid_postfix(std::int64_t a, std::int64_t b) { return WrappingSubtract(a, b); } +Int128 APS5_VABI __subvti3_nid_postfix(Int128 a, Int128 b) { return WrappingSubtract(a, b); } +std::int32_t APS5_VABI __negvsi2_nid_postfix(std::int32_t value) { return CheckedNegate(value, "__negvsi2"); } +std::int64_t APS5_VABI __negvdi2_nid_postfix(std::int64_t value) { return CheckedNegate(value, "__negvdi2"); } +Int128 APS5_VABI __negvti2_nid_postfix(Int128 value) { return CheckedNegate(value, "__negvti2"); } +std::int32_t APS5_VABI __absvsi2_nid_postfix(std::int32_t value) { return CheckedAbsolute(value, "__absvsi2"); } +std::int64_t APS5_VABI __absvdi2_nid_postfix(std::int64_t value) { return CheckedAbsolute(value, "__absvdi2"); } +Int128 APS5_VABI __absvti2_nid_postfix(Int128 value) { return CheckedAbsolute(value, "__absvti2"); } + +} diff --git a/core/libs/prx/libc/src/Interfaces.cpp b/core/libs/prx/libc/src/Interfaces.cpp index 3391a07053..8e39cfa29b 100644 --- a/core/libs/prx/libc/src/Interfaces.cpp +++ b/core/libs/prx/libc/src/Interfaces.cpp @@ -5,16 +5,25 @@ #include #else #include +#include #include +#include #include +#include +#include +#include +#include +#include #include +#include #endif +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include -#include #include #include +#include #include #include #include @@ -52,6 +61,7 @@ struct Entry { std::vector address; std::vector netmask; std::vector destination; + std::vector data; }; std::vector GuestIpv4(const std::uint8_t* bytes) { @@ -150,6 +160,100 @@ int HostEntries(std::vector& entries) { return 0; } #else +constexpr std::uint8_t guestLinkFamily = 18; +constexpr std::size_t guestLinkNameArea = 8 + 16; +constexpr std::size_t guestLinkMinimum = 54; +constexpr std::uint8_t guestTypeOther = 0x1; +constexpr std::uint8_t guestTypeEthernet = 0x6; +constexpr std::uint8_t guestTypePpp = 0x17; +constexpr std::uint8_t guestTypeLoopback = 0x18; +constexpr std::uint8_t guestLinkStateDown = 1; +constexpr std::uint8_t guestLinkStateUp = 2; +constexpr std::size_t guestInterfaceDataSize = 152; +constexpr unsigned int hostLowerUp = 0x10000; + +struct HostInterface { + std::string name; + bool linked = false; + Entry link; + std::vector addresses; +}; + +template +void Store(std::vector& bytes, std::size_t offset, TValue value) { + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +std::vector GuestLinkAddress(const std::string& name, std::uint16_t index, std::uint8_t type, const std::uint8_t* hardware, std::size_t hardwareLength) { + std::size_t size = std::max(guestLinkMinimum, guestLinkNameArea + hardwareLength); + size = Aligned(size); + if (size > 0xFF) throw std::runtime_error("getifaddrs: link-level address of " + name + " does not fit sockaddr_dl"); + std::vector guest(size, 0); + guest[0] = static_cast(size); + guest[1] = guestLinkFamily; + Store(guest, 2, index); + guest[4] = type; + guest[5] = static_cast(name.size()); + guest[6] = static_cast(hardwareLength); + std::memcpy(guest.data() + 8, name.data(), name.size()); + if (hardwareLength != 0) std::memcpy(guest.data() + 8 + name.size(), hardware, hardwareLength); + return guest; +} + +std::optional ReadSysfsCounter(const std::string& name, const char* file) { + std::ifstream input("/sys/class/net/" + name + "/" + file); + long long value = 0; + if (!(input >> value)) return std::nullopt; + return static_cast(value); +} + +std::uint32_t HostMtu(const std::string& name) { + const int probe = ::socket(AF_INET, SOCK_DGRAM, 0); + if (probe < 0) throw std::runtime_error(std::string("getifaddrs: no socket to read the MTU of ") + name + ": " + std::strerror(errno)); + ifreq request{}; + std::memcpy(request.ifr_name, name.c_str(), name.size() + 1); + const int result = ::ioctl(probe, SIOCGIFMTU, &request); + const int error = errno; + ::close(probe); + if (result != 0) throw std::runtime_error(std::string("getifaddrs: SIOCGIFMTU failed on ") + name + ": " + std::strerror(error)); + return static_cast(request.ifr_mtu); +} + +std::vector GuestInterfaceData(const std::string& name, unsigned int hostFlags, const void* hostStats, std::uint8_t type, std::uint8_t hardwareLength) { + std::vector data(guestInterfaceDataSize, 0); + data[0] = type; + data[2] = hardwareLength; + data[3] = type == guestTypeEthernet ? 14 : 0; + if (type == guestTypeLoopback) data[4] = (hostFlags & IFF_UP) ? guestLinkStateUp : guestLinkStateDown; + else data[4] = (hostFlags & hostLowerUp) ? guestLinkStateUp : guestLinkStateDown; + Store(data, 6, static_cast(guestInterfaceDataSize)); + Store(data, 8, HostMtu(name)); + if (const auto speed = ReadSysfsCounter(name, "speed"); speed && static_cast(*speed) > 0) + Store(data, 16, *speed * 1000000u); + constexpr std::pair counters[] = { + {"statistics/rx_packets", 24}, {"statistics/rx_errors", 32}, {"statistics/tx_packets", 40}, {"statistics/tx_errors", 48}, + {"statistics/collisions", 56}, {"statistics/rx_bytes", 64}, {"statistics/tx_bytes", 72}, {"statistics/multicast", 80}, + {"statistics/rx_dropped", 96}, {"statistics/tx_dropped", 104}, + }; + bool complete = true; + for (const auto& [file, offset] : counters) { + const auto value = ReadSysfsCounter(name, file); + if (!value) { complete = false; break; } + Store(data, offset, *value); + } + if (!complete) { + if (!hostStats) throw std::runtime_error("getifaddrs: no counters for " + name + " in sysfs or the host list"); + rtnl_link_stats stats{}; + std::memcpy(&stats, hostStats, sizeof(stats)); + const std::pair fallback[] = { + {stats.rx_packets, 24}, {stats.rx_errors, 32}, {stats.tx_packets, 40}, {stats.tx_errors, 48}, {stats.collisions, 56}, + {stats.rx_bytes, 64}, {stats.tx_bytes, 72}, {stats.multicast, 80}, {stats.rx_dropped, 96}, {stats.tx_dropped, 104}, + }; + for (const auto& [value, offset] : fallback) Store(data, offset, static_cast(value)); + } + return data; +} + std::uint32_t InterfaceFlags(unsigned int host) { constexpr std::pair flags[] = { {IFF_UP, guestUp}, {IFF_BROADCAST, guestBroadcast}, {IFF_LOOPBACK, guestLoopback}, @@ -174,9 +278,31 @@ int HostEntries(std::vector& entries) { default: throw std::runtime_error(std::string("getifaddrs: host enumeration failed: ") + std::strerror(hostError)); } } + std::vector interfaces; + const auto interfaceFor = [&](const std::string& name) -> HostInterface& { + const auto found = std::find_if(interfaces.begin(), interfaces.end(), [&](const HostInterface& candidate) { return candidate.name == name; }); + if (found != interfaces.end()) return *found; + interfaces.push_back({name, false, {}, {}}); + return interfaces.back(); + }; try { for (auto* item = host; item; item = item->ifa_next) { - if (!item->ifa_addr || (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6)) continue; + if (!item->ifa_addr) continue; + if (item->ifa_addr->sa_family == AF_PACKET) { + const auto& link = *reinterpret_cast(item->ifa_addr); + HostInterface& owner = interfaceFor(CheckedName(item->ifa_name)); + if (owner.linked) throw std::runtime_error("getifaddrs: two link-level entries for " + owner.name); + if (link.sll_ifindex <= 0 || link.sll_ifindex > 0xFFFF) throw std::runtime_error("getifaddrs: interface index of " + owner.name + " does not fit sockaddr_dl"); + const std::uint8_t type = link.sll_hatype == ARPHRD_ETHER ? guestTypeEthernet : link.sll_hatype == ARPHRD_LOOPBACK ? guestTypeLoopback + : link.sll_hatype == ARPHRD_PPP ? guestTypePpp : guestTypeOther; + const std::uint8_t hardwareLength = type == guestTypeLoopback ? 0 : std::min(link.sll_halen, sizeof(link.sll_addr)); + owner.link = Entry{owner.name, InterfaceFlags(item->ifa_flags), + GuestLinkAddress(owner.name, static_cast(link.sll_ifindex), type, link.sll_addr, hardwareLength), {}, {}, + GuestInterfaceData(owner.name, item->ifa_flags, item->ifa_data, type, hardwareLength)}; + owner.linked = true; + continue; + } + if (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6) continue; if (!item->ifa_netmask) throw std::runtime_error(std::string("getifaddrs: host address without a netmask on ") + item->ifa_name); Entry entry{CheckedName(item->ifa_name), InterfaceFlags(item->ifa_flags), GuestAddress(item->ifa_addr), GuestAddress(item->ifa_netmask), {}}; const int family = item->ifa_addr->sa_family; @@ -187,13 +313,17 @@ int HostEntries(std::vector& entries) { entry.destination = item->ifa_broadaddr && item->ifa_broadaddr->sa_family == AF_INET ? GuestAddress(item->ifa_broadaddr) : BroadcastAddress(entry.address, entry.netmask); } - entries.push_back(std::move(entry)); + interfaceFor(entry.name).addresses.push_back(std::move(entry)); } } catch (...) { ::freeifaddrs(host); throw; } ::freeifaddrs(host); + for (auto& owner : interfaces) { + if (owner.linked) entries.push_back(std::move(owner.link)); + for (auto& address : owner.addresses) entries.push_back(std::move(address)); + } return 0; } #endif @@ -212,8 +342,8 @@ 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)); + size += Aligned(entry.name.size() + 1) + Aligned(entry.address.size()) + Aligned(entry.netmask.size()) + Aligned(entry.destination.size()) + Aligned(entry.data.size()); + auto* block = static_cast(ApplicationHeapAllocate_nid_no_patch(size)); if (!block) { *__error_nid_postfix() = 12; return -1; @@ -233,9 +363,9 @@ int APS5_VABI getifaddrs_nid_postfix(GuestInterfaceAddress** list) { item.name = static_cast(place(entry.name.c_str(), entry.name.size() + 1)); item.flags = entry.flags; item.address = place(entry.address.data(), entry.address.size()); - item.netmask = place(entry.netmask.data(), entry.netmask.size()); + item.netmask = entry.netmask.empty() ? nullptr : place(entry.netmask.data(), entry.netmask.size()); item.destination = entry.destination.empty() ? nullptr : place(entry.destination.data(), entry.destination.size()); - item.data = nullptr; + item.data = entry.data.empty() ? nullptr : place(entry.data.data(), entry.data.size()); } *list = items; return 0; @@ -243,7 +373,7 @@ int APS5_VABI getifaddrs_nid_postfix(GuestInterfaceAddress** list) { } void APS5_VABI freeifaddrs_nid_postfix(GuestInterfaceAddress* list) { - std::free(list); + ApplicationHeapFree_nid_no_patch(list); } } diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index 9d70e10e48..fb1f8042ab 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -35,6 +35,11 @@ std::mutex g_localeInitMutex; bool g_localeInitialized = false; std::vector g_registeredFacets; +constexpr std::int64_t StandardStreamSlots = 8; +std::mutex g_standardStreamMutex; +GuestLocale::IosBase* g_standardStreams[StandardStreamSlots + 1] = {}; +int g_standardStreamOpens[StandardStreamSlots + 1] = {}; + void APS5_VABI DestroyClassicLocale(GuestLocale::Facet*) { throw std::runtime_error("Cannot destroy the classic locale"); } @@ -157,6 +162,64 @@ std::int64_t APS5_VABI CollateHash(const GuestLocale::CollateFacet*, const char* const GuestLocale::CollateVtable g_collateVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, CollateCompare, CollateTransform, CollateHash}; +constexpr std::size_t GuestWideCharacterSize = 2; +constexpr std::size_t GuestWideInlineCapacity = sizeof(GuestLocale::String::buffer) / GuestWideCharacterSize - 1; + +void ValidateWideCharacterRange(const std::uint16_t* first, const std::uint16_t* last, const char* function) { + if ((first == nullptr) != (last == nullptr) || last < first) throw std::invalid_argument(std::string(function) + ": invalid character range"); +} + +int APS5_VABI WideCollateCompare(const GuestLocale::CollateFacet*, const char* first1, const char* last1, const char* first2, const char* last2) { + const auto* characters1 = reinterpret_cast(first1); + const auto* end1 = reinterpret_cast(last1); + const auto* characters2 = reinterpret_cast(first2); + const auto* end2 = reinterpret_cast(last2); + ValidateWideCharacterRange(characters1, end1, "collate::do_compare"); + ValidateWideCharacterRange(characters2, end2, "collate::do_compare"); + const auto length1 = static_cast(end1 - characters1); + const auto length2 = static_cast(end2 - characters2); + const auto common = std::min(length1, length2); + for (std::size_t index = 0; index < common; ++index) { + if (characters1[index] != characters2[index]) return characters1[index] < characters2[index] ? -1 : 1; + } + if (length1 == length2) return 0; + return length1 < length2 ? -1 : 1; +} + +GuestLocale::String* APS5_VABI WideCollateTransform(GuestLocale::String* result, const GuestLocale::CollateFacet*, const char* first, const char* last) { + if (result == nullptr) throw std::invalid_argument("collate::do_transform: null result"); + const auto* characters = reinterpret_cast(first); + const auto* end = reinterpret_cast(last); + ValidateWideCharacterRange(characters, end, "collate::do_transform"); + const auto length = static_cast(end - characters); + if (length != 0 && std::find(characters, end, static_cast(0)) != end) throw std::runtime_error("collate::do_transform: embedded null characters are not supported"); + const auto bytes = length * GuestWideCharacterSize; + char* data = result->buffer; + std::size_t capacity = GuestWideInlineCapacity; + if (length > GuestWideInlineCapacity) { + capacity = length | GuestWideInlineCapacity; + data = static_cast(AllocateStringStorage((capacity + 1) * GuestWideCharacterSize)); + } + if (length != 0) std::memcpy(data, characters, bytes); + data[bytes] = 0; + data[bytes + 1] = 0; + if (data != result->buffer) result->pointer = data; + result->size = length; + result->capacity = capacity; + return result; +} + +std::int64_t APS5_VABI WideCollateHash(const GuestLocale::CollateFacet*, const char* first, const char* last) { + const auto* characters = reinterpret_cast(first); + const auto* end = reinterpret_cast(last); + ValidateWideCharacterRange(characters, end, "collate::do_hash"); + std::uint64_t hash = 0xcbf29ce484222325; + for (; characters != end; ++characters) hash = (hash ^ static_cast(*characters)) * 0x100000001b3; + return static_cast(hash); +} + +const GuestLocale::CollateVtable g_wideCollateVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, WideCollateCompare, WideCollateTransform, WideCollateHash}; + constexpr std::array MakeClassificationTable() { std::array table{}; for (unsigned int value = 0; value < 256; ++value) { @@ -235,6 +298,9 @@ std::uint64_t _ZNSt7codecvtIcc9_MbstatetE2idE_nid_postfix = 0; std::uintptr_t _ZTVSt7codecvtIcc9_MbstatetE_nid_postfix[16] {}; std::uint64_t _ZNSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; std::uintptr_t _ZTVSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[12] {}; +std::uint64_t _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; +std::uintptr_t _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[17] {}; +std::uint64_t _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; std::streamoff _ZSt7_BADOFF_nid_postfix = -1; std::fpos_t _ZSt4_Fpz_nid_postfix {}; @@ -244,11 +310,43 @@ GuestLocale::Implementation* _ZSt21_sceLibcClassicLocale_nid_postfix = &g_classi void APS5_VABI _ZNSt8ios_baseD2Ev_nid_postfix(GuestLocale::IosBase* self) { if (self == nullptr) throw std::invalid_argument("ios_base destructor: null object"); - if (self->standardStream != 0 || self->storage != nullptr || self->callbacks != nullptr) throw std::runtime_error("ios_base destructor: unsupported stream storage or callbacks"); + std::lock_guard lock(g_standardStreamMutex); + if (self->standardStream > 0) { + if (self->standardStream > StandardStreamSlots) throw std::runtime_error("ios_base destructor: unknown standard stream slot"); + if (0 < --g_standardStreamOpens[self->standardStream]) return; + } + if (self->storage != nullptr || self->callbacks != nullptr) throw std::runtime_error("ios_base destructor: unsupported stream storage or callbacks"); if (self->locale != &_ZSt21_sceLibcClassicLocale_nid_postfix) throw std::runtime_error("ios_base destructor: unsupported locale ownership"); self->locale = nullptr; } +GuestLocale::IosBase* APS5_VABI SetStreamWidth(GuestLocale::IosBase* stream, std::int32_t width) { + if (stream == nullptr) throw std::invalid_argument("setw: null stream"); + stream->width = width; + return stream; +} + +struct StreamManipulator { + GuestLocale::IosBase* (APS5_VABI* function)(GuestLocale::IosBase*, std::int32_t); + std::int32_t argument; +}; + +static_assert(sizeof(StreamManipulator) == 0x10); + +StreamManipulator APS5_VABI _ZSt4setwi_nid_postfix(std::int32_t width) { + return {&SetStreamWidth, width}; +} + +void APS5_VABI _ZNSt8ios_base7_AddstdEPS__nid_postfix(GuestLocale::IosBase* self) { + if (self == nullptr) throw std::invalid_argument("ios_base _Addstd: null stream"); + std::lock_guard lock(g_standardStreamMutex); + for (self->standardStream = 0; ++self->standardStream < StandardStreamSlots; ) { + if (g_standardStreams[self->standardStream] == nullptr || g_standardStreams[self->standardStream] == self) break; + } + g_standardStreams[self->standardStream] = self; + ++g_standardStreamOpens[self->standardStream]; +} + // Dinkumware's static locale::_Init(bool) returns the global _Locimp, which the caller copies. GuestLocale::Implementation* APS5_VABI _ZNSt6locale5_InitEv_nid_postfix() { std::lock_guard lock(g_localeInitMutex); @@ -271,8 +369,25 @@ GuestLocale::Implementation* APS5_VABI _ZNSt6locale16_GetgloballocaleEv_nid_post return &g_classicLocale; } -void APS5_VABI _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet**, const GuestLocale::Implementation*) { - NotImplemented_nid_no_patch(__func__); +namespace { +struct GuestLocaleFacade { + GuestLocale::Implementation* implementation; +}; +GuestLocaleFacade g_classicLocaleFacade{&g_classicLocale}; +} + +GuestLocaleFacade* APS5_VABI _ZNSt6locale7classicEv_nid_postfix() { + return &g_classicLocaleFacade; +} + +std::size_t APS5_VABI _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale) { + if (facet != nullptr && *facet == nullptr) { + if (locale == nullptr || *locale == nullptr || (*locale)->name == nullptr || std::strcmp((*locale)->name, "C") != 0) throw std::invalid_argument("collate::_Getcat: only the C locale is supported"); + auto* collate = static_cast(_Znwm_nid_postfix(sizeof(GuestLocale::CollateFacet))); + *collate = {{&g_wideCollateVtable.facet, 0, 0}, nullptr, nullptr}; + *facet = &collate->base; + } + return 1; } std::size_t APS5_VABI _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale) { diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index ac5bcac781..37616b7be3 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -1,14 +1,17 @@ #include #include +#include #include #include #include +#include #include #include #include #include #include #include +#include #include "prx/libc/include/General.hpp" #include "prx/libc/include/SonyCrt.hpp" @@ -124,6 +127,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)); @@ -163,6 +183,85 @@ long double AsinDenominator(long double t) { return 1.0L + t * (q[0] + t * (q[1] + t * (q[2] + t * (q[3] + t * q[4])))); } +template +bool NanWithoutException(T value) { + if constexpr (sizeof(T) == sizeof(std::uint32_t)) { + std::uint32_t bits; + std::memcpy(&bits, &value, sizeof(bits)); + return (bits & UINT32_C(0x7fffffff)) > UINT32_C(0x7f800000); + } else if constexpr (sizeof(T) == sizeof(std::uint64_t)) { + std::uint64_t bits; + std::memcpy(&bits, &value, sizeof(bits)); + return (bits & UINT64_C(0x7fffffffffffffff)) > UINT64_C(0x7ff0000000000000); + } else { + static_assert(std::numeric_limits::digits == 64); + std::uint64_t significand; + std::uint16_t signAndExponent; + std::memcpy(&significand, &value, sizeof(significand)); + std::memcpy(&signAndExponent, reinterpret_cast(&value) + sizeof(significand), sizeof(signAndExponent)); + return (signAndExponent & 0x7fff) == 0x7fff && (significand & UINT64_C(0x7fffffffffffffff)) != 0; + } +} + +template +T GuestFmin(T x, T y) { + if (NanWithoutException(x)) + return y; + if (NanWithoutException(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 (NanWithoutException(x)) + return y; + if (NanWithoutException(y)) + return x; + if (std::signbit(x) != std::signbit(y)) + return std::signbit(x) ? y : x; + return x < y ? y : x; +} + +float GuestBesselSecondKindZero() { + std::feraiseexcept(FE_DIVBYZERO); + return -std::numeric_limits::infinity(); +} + +float ScaledHyperbolic(float x, float y, bool sine) { + const auto xBits = std::bit_cast(x); + const auto xMagnitude = xBits & 0x7fffffffu; + const auto yMagnitude = std::bit_cast(y) & 0x7fffffffu; + constexpr std::uint32_t infinity = 0x7f800000u; + if (xMagnitude > infinity) return x; + if (yMagnitude > infinity) return y; + if ((xMagnitude == infinity && yMagnitude == 0) || (sine && xMagnitude == 0 && yMagnitude == infinity)) { + errno = EDOM; + std::feraiseexcept(FE_INVALID); + return std::numeric_limits::quiet_NaN(); + } + if (xMagnitude == infinity) return sine ? x * y : std::copysign(std::numeric_limits::infinity(), y); + if (xMagnitude == 0 || yMagnitude == 0 || yMagnitude == infinity) return sine ? x * y : y; + constexpr double limit = 2.0 * std::numeric_limits::max_exponent; + const float magnitude = std::bit_cast(xMagnitude); + const double argument = magnitude < limit ? static_cast(magnitude) : limit; + const double value = static_cast(y) * (sine ? std::sinh(argument) : std::cosh(argument)); + const bool exponential = magnitude >= (sine ? 7.7f : 8.675f); + float result; + if (exponential && std::fabs(value) >= 0x1p128) { + result = std::copysign(std::numeric_limits::infinity(), y); + } else { + result = static_cast(value); + } + if (exponential && std::isinf(result)) { + errno = ERANGE; + std::feraiseexcept(FE_OVERFLOW | FE_INEXACT); + } + return sine && (xBits & 0x80000000u) != 0 ? -result : result; +} + } extern "C" { @@ -175,6 +274,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; @@ -218,6 +333,9 @@ 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 significand_nid_postfix(double x) { return x == 0.0 || !std::isfinite(x) ? x : std::scalbn(x, -std::ilogb(x)); } double APS5_VABI frexp_nid_postfix(double x, int* exponent) { return std::frexp(x, exponent); } float APS5_VABI frexpf_nid_postfix(float x, int* exponent) { return std::frexp(x, exponent); } // Guest long is 64-bit, including on Windows where native long is 32-bit. @@ -246,6 +364,14 @@ double APS5_VABI acos_nid_postfix(double x) { return std::acos(x); } double APS5_VABI sqrt_nid_postfix(double x) { return std::sqrt(x); } float APS5_VABI sqrtf_nid_postfix(float x) { return std::sqrt(x); } double APS5_VABI fabs_nid_postfix(double x) { return std::fabs(x); } +float APS5_VABI fabsf_nid_postfix(float x) { return std::fabs(x); } +long double APS5_VABI fabsl_nid_postfix(long double x) { return std::fabs(x); } +double APS5_VABI copysign_nid_postfix(double x, double y) { + return std::copysign(x, y); +} +float APS5_VABI copysignf_nid_postfix(float x, float y) { + return std::copysign(x, y); +} int APS5_VABI abs_nid_postfix(int x) { return x < 0 ? static_cast(0u - static_cast(x)) : x; } double APS5_VABI trunc_nid_postfix(double x) { return std::trunc(x); } double APS5_VABI ceil_nid_postfix(double x) { return std::ceil(x); } @@ -254,7 +380,42 @@ 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); } +float APS5_VABI fmaf_nid_postfix(float x, float y, float 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); } +double APS5_VABI fdim_nid_postfix(double x, double y) { return std::fdim(x, y); } +float APS5_VABI fdimf_nid_postfix(float x, float y) { return std::fdim(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); } +float APS5_VABI tgammaf_nid_postfix(float x) { return static_cast(std::tgamma(static_cast(x))); } +long double APS5_VABI hypotl_nid_postfix(long double x, long double y) { + if (std::isinf(x) || std::isinf(y)) return std::numeric_limits::infinity(); + return std::hypot(x, y); +} +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 x == 0.0f ? GuestBesselSecondKindZero() : static_cast(::y0(x)); } +float APS5_VABI y1f_nid_postfix(float x) { return x == 0.0f ? GuestBesselSecondKindZero() : static_cast(::y1(x)); } +float APS5_VABI ynf_nid_postfix(int order, float x) { return x == 0.0f ? GuestBesselSecondKindZero() : static_cast(::yn(order, x)); } long double APS5_VABI asinl_nid_postfix(long double x) { const ExtendedParts parts = SplitExtended(x); @@ -300,7 +461,19 @@ float APS5_VABI log1pf_nid_postfix(float x) { return std::log1p(x); } double APS5_VABI expm1_nid_postfix(double x) { return std::expm1(x); } float APS5_VABI expm1f_nid_postfix(float x) { return std::expm1(x); } float APS5_VABI erff_nid_postfix(float x) { return std::erf(x); } -double APS5_VABI lgamma_nid_postfix(double x) { return std::lgamma(x); } +double APS5_VABI erf_nid_postfix(double x) { return std::erf(x); } +double APS5_VABI erfc_nid_postfix(double x) { return std::erfc(x); } +float APS5_VABI erfcf_nid_postfix(float x) { return std::erfc(x); } +int signgam_nid_postfix = 0; + +double APS5_VABI lgamma_nid_postfix(double x) { + const double whole = std::floor(x); + if (x == 0) signgam_nid_postfix = std::signbit(x) ? -1 : 1; + else signgam_nid_postfix = x < 0 && whole != x && std::fmod(whole, 2.0) != 0 ? -1 : 1; + const std::uint64_t magnitude = std::bit_cast(x) & 0x7fffffffffffffffull; + if (magnitude != 0 && magnitude < 0x3c70000000000000ull) return -std::log(std::fabs(x)); + return std::lgamma(x); +} float APS5_VABI sinf_nid_postfix(float x) { return std::sin(x); } float APS5_VABI cosf_nid_postfix(float x) { return std::cos(x); } @@ -414,6 +587,7 @@ double APS5_VABI round_nid_postfix(double x) { return std::round(x); } float APS5_VABI cbrtf_nid_postfix(float x) { return std::cbrt(x); } float APS5_VABI remainderf_nid_postfix(float x, float y) { return std::remainder(x, y); } float APS5_VABI nextafterf_nid_postfix(float x, float y) { return std::nextafter(x, y); } +double APS5_VABI nextafter_nid_postfix(double x, double y) { return std::nextafter(x, y); } int APS5_VABI __isfinite_nid_postfix(double x) { return std::isfinite(x) ? 1 : 0; } int APS5_VABI __isnan_nid_postfix(double x) { return std::isnan(x) ? 1 : 0; } int APS5_VABI __isinf_nid_postfix(double x) { return std::isinf(x) ? 1 : 0; } @@ -445,10 +619,24 @@ long double APS5_VABI log10l_nid_postfix(long double x) { return std::log10(x); long double APS5_VABI powl_nid_postfix(long double base, long double exp) { return std::pow(base, exp); } long double APS5_VABI ldexpl_nid_postfix(long double x, int exp) { return std::ldexp(x, exp); } long double APS5_VABI truncl_nid_postfix(long double x) { return std::trunc(x); } +long double APS5_VABI ceill_nid_postfix(long double x) { return std::ceil(x); } +long double APS5_VABI floorl_nid_postfix(long double x) { return std::floor(x); } double APS5_VABI logb_nid_postfix(double x) { return std::logb(x); } +int APS5_VABI ilogb_nid_postfix(double x) { + if (x == 0.0) { + return INT_MIN; + } + if (!std::isfinite(x)) { + return INT_MAX; + } + return std::ilogb(x); +} +int APS5_VABI ilogbf_nid_postfix(float x) { return ilogb_nid_postfix(static_cast(x)); } double APS5_VABI remainder_nid_postfix(double x, double y) { return std::remainder(x, y); } -float APS5_VABI _FSinh_nid_postfix(float x, float y) { return y * std::sinh(x); } -float APS5_VABI _FCosh_nid_postfix(float x, float y) { return y * std::cosh(x); } +double APS5_VABI remquo_nid_postfix(double x, double y, int* quo) { return std::remquo(x, y, quo); } +float APS5_VABI remquof_nid_postfix(float x, float y, int* quo) { return std::remquo(x, y, quo); } +float APS5_VABI _FSinh_nid_postfix(float x, float y) { return ScaledHyperbolic(x, y, true); } +float APS5_VABI _FCosh_nid_postfix(float x, float y) { return ScaledHyperbolic(x, y, false); } struct alignas(16) LibcFloatConstant { std::uint32_t bits[4]; }; LibcFloatConstant _FInf_nid_postfix {{0x7f800000u, 0, 0, 0}}; @@ -498,6 +686,33 @@ int APS5_VABI __signbitl_nid_postfix(long double x) { return std::signbit(x) ? 1 : 0; } +int APS5_VABI __isfinitel_nid_postfix(long double x) { + static_assert(sizeof(long double) == 16 && std::numeric_limits::digits == 64 && std::numeric_limits::max_exponent == 16384 && std::endian::native == std::endian::little, "guest long double requires little-endian x87 extended precision storage"); + std::uint16_t exponent; + std::memcpy(&exponent, reinterpret_cast(&x) + 8, sizeof(exponent)); + return (exponent & 0x7fffu) != 0x7fffu; +} + +struct DinkumwareFloatCharacteristics { + int digits; + int mantissaDigits; + int maxExponent10; + int maxExponent; + int minExponent10; + int minExponent; + alignas(16) std::uint16_t epsilon[8]; + alignas(16) std::uint16_t max[8]; + alignas(16) std::uint16_t min[8]; +}; +static_assert(sizeof(DinkumwareFloatCharacteristics) == 80 && offsetof(DinkumwareFloatCharacteristics, epsilon) == 32); + +DinkumwareFloatCharacteristics _Flt_nid_postfix {6, 24, 38, 128, -37, -125, + {0, 0x3400}, {0xffff, 0x7f7f}, {0, 0x0080}}; +DinkumwareFloatCharacteristics _Dbl_nid_postfix {15, 53, 308, 1024, -307, -1021, + {0, 0, 0, 0x3cb0}, {0xffff, 0xffff, 0xffff, 0x7fef}, {0, 0, 0, 0x0010}}; +DinkumwareFloatCharacteristics _Ldbl_nid_postfix {18, 64, 4932, 16384, -4931, -16381, + {0, 0, 0, 0x8000, 0x3fc0}, {0xffff, 0xffff, 0xffff, 0xffff, 0x7ffe}, {0, 0, 0, 0x8000, 0x0001}}; + static std::mutex g_randLock; static std::uint32_t g_randState = 1; @@ -535,10 +750,46 @@ double APS5_VABI _Sinh_nid_postfix(double x, double y) { return SonySinh(x, y); } +double APS5_VABI _Cosh_nid_postfix(double x, double y) { + if (!std::isfinite(x) || !std::isfinite(y)) return y * std::cosh(x); + if (y == 0.0) return std::copysign(0.0, y); + constexpr double Ln2High = 0x1.62e42fefa0000p-1; + constexpr double Ln2Low = 0x1.cf79abc9e3b3ap-40; + constexpr double InverseLn2 = 0x1.71547652b82fep+0; + const double magnitude = std::abs(x); + if (magnitude <= (std::numeric_limits::max_exponent - 1) * (Ln2High + Ln2Low)) + return y * std::cosh(x); + int scaleExponent; + const double scale = std::frexp(y, &scaleExponent); + constexpr int MaximumExponentSpan = std::numeric_limits::max_exponent - + std::numeric_limits::min_exponent + std::numeric_limits::digits + 4; + if (magnitude > MaximumExponentSpan * (Ln2High + Ln2Low)) + return std::scalbn(scale, INT_MAX); + const int exponent = static_cast(magnitude * InverseLn2); + const double reduced = std::fma(-exponent, Ln2Low, std::fma(-exponent, Ln2High, magnitude)); + return std::scalbn(scale * (0.5 * std::exp(reduced)), scaleExponent + exponent); +} + int APS5_VABI _Fltrounds_nid_postfix(void) { return SonyFltrounds(); } +struct GuestFloatingPointEnvironment { + std::uint32_t control; + std::uint32_t status; + std::uint32_t tag; + unsigned char other[16]; + std::uint32_t mxcsr; +}; +static_assert(sizeof(GuestFloatingPointEnvironment) == 32 && offsetof(GuestFloatingPointEnvironment, mxcsr) == 28); + +int APS5_VABI fegetenv_nid_postfix(GuestFloatingPointEnvironment* environment) { + if (environment == nullptr) throw std::invalid_argument("fegetenv: null environment"); + asm volatile("fnstenv %0\n\tfldcw %0" : "+m"(*environment)); + environment->mxcsr = _mm_getcsr(); + return 0; +} + union SonyDconst { unsigned short _Word[8]; float _Float; @@ -548,4 +799,14 @@ union SonyDconst { static_assert(std::endian::native == std::endian::little); SonyDconst _Inf_nid_postfix {{0, 0, 0, 0x7ff0, 0, 0, 0, 0}}; +SonyDconst _Denorm_nid_postfix {{1, 0, 0, 0, 0, 0, 0, 0}}; +SonyDconst _Hugeval_nid_postfix {{0, 0, 0, 0x7ff0, 0, 0, 0, 0}}; +SonyDconst _Nan_nid_postfix {{0, 0, 0, 0x7ff8, 0, 0, 0, 0}}; +SonyDconst _Snan_nid_postfix {{1, 0, 0, 0x7ff0, 0, 0, 0, 0}}; +LibcFloatConstant _FDenorm_nid_postfix {{0x00000001u, 0, 0, 0}}; +LibcFloatConstant _FSnan_nid_postfix {{0x7f800001u, 0, 0, 0}}; +SonyDconst _LDenorm_nid_postfix {{1, 0, 0, 0, 0, 0, 0, 0}}; +SonyDconst _LInf_nid_postfix {{0, 0, 0, 0x8000, 0x7fff, 0, 0, 0}}; +SonyDconst _LNan_nid_postfix {{0, 0, 0, 0xc000, 0x7fff, 0, 0, 0}}; +SonyDconst _LSnan_nid_postfix {{1, 0, 0, 0x8000, 0x7fff, 0, 0, 0}}; } diff --git a/core/libs/prx/libc/src/MathExact.cpp b/core/libs/prx/libc/src/MathExact.cpp new file mode 100644 index 0000000000..1217ca4a4c --- /dev/null +++ b/core/libs/prx/libc/src/MathExact.cpp @@ -0,0 +1,261 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "prx/libc/include/General.hpp" + +namespace { + +class ErrnoGuard { +public: + ErrnoGuard() : saved(errno) {} + ~ErrnoGuard() { errno = saved; } + ErrnoGuard(const ErrnoGuard&) = delete; + ErrnoGuard& operator=(const ErrnoGuard&) = delete; + +private: + int saved; +}; + +struct ExtendedBits { + std::uint64_t mantissa; + std::uint16_t signExponent; +}; + +ExtendedBits Bits(const long double& value) { + ExtendedBits bits; + unsigned char bytes[sizeof(long double)]; + std::memcpy(bytes, &value, sizeof(bytes)); + std::memcpy(&bits.mantissa, bytes, sizeof(bits.mantissa)); + std::memcpy(&bits.signExponent, bytes + 8, sizeof(bits.signExponent)); + return bits; +} + +constexpr short Denormal = -2, Finite = -1, Zero = 0, Infinite = 1, NotANumber = 2; + +short TestExtended(const ExtendedBits& bits) { + const int exponent = bits.signExponent & 0x7fff; + const std::uint64_t fraction = bits.mantissa & 0x7fffffffffffffffull; + if (exponent == 0x7fff) return fraction != 0 ? NotANumber : Infinite; + if (exponent != 0 || (bits.mantissa >> 63) != 0) return Finite; + return bits.mantissa != 0 ? Denormal : Zero; +} + +template +short TestFloat(TFloat value) { + switch (std::fpclassify(value)) { + case FP_NAN: + return NotANumber; + case FP_INFINITE: + return Infinite; + case FP_ZERO: + return Zero; + case FP_SUBNORMAL: + return Denormal; + default: + return Finite; + } +} + +template +T Remainder(T x, T y) { + const bool domain = !std::isnan(x) && !std::isnan(y) && (std::isinf(x) || y == T(0)); + T result; + { + ErrnoGuard guard; + result = std::remainder(x, y); + } + if (domain) errno = EDOM; + return result; +} + +int Exponent(std::int64_t exponent) { + return static_cast(std::clamp(exponent, INT_MIN, INT_MAX)); +} + +} + +extern "C" { + +long double APS5_VABI copysignl_nid_postfix(long double x, long double y) { return std::copysign(x, y); } +long double APS5_VABI fdiml_nid_postfix(long double x, long double y) { + if (std::isnan(x)) return x; + if (std::isnan(y)) return y; + return x > y ? x - y : 0.0L; +} + +long double APS5_VABI fmodl_nid_postfix(long double x, long double y) { + if (!std::isfinite(x) || y == 0.0L || std::isnan(y)) { + errno = EDOM; + return (x * y) / (x * y); + } + ErrnoGuard guard; + return std::fmod(x, y); +} + +long double APS5_VABI remainderl_nid_postfix(long double x, long double y) { + return Remainder(x, y); +} + +long double APS5_VABI remquol_nid_postfix(long double x, long double y, int* quotient) { + const bool domain = !std::isnan(x) && !std::isnan(y) && (std::isinf(x) || y == 0.0L); + int bits = 0; + long double result; + { + ErrnoGuard guard; + result = std::remquo(x, y, &bits); + } + if (domain) errno = EDOM; + if (!std::isnan(result)) *quotient = bits < 0 ? -(-bits & 7) : (bits & 7); + return result; +} + +long double APS5_VABI logbl_nid_postfix(long double x) { + ErrnoGuard guard; + return std::logb(x); +} + +int APS5_VABI ilogbl_nid_postfix(long double x) { + if ((Bits(x).signExponent & 0x7fff) == 0x7fff) return INT_MAX; + ErrnoGuard guard; + return std::ilogb(x); +} + +long double APS5_VABI rintl_nid_postfix(long double x) { return std::rint(x); } +long double APS5_VABI roundl_nid_postfix(long double x) { return std::isfinite(x) ? std::round(x) : x; } + +std::int64_t APS5_VABI lroundl_nid_postfix(long double x) { + ErrnoGuard guard; + return std::llround(x); +} + +std::int64_t APS5_VABI lrintl_nid_postfix(long double x) { + ErrnoGuard guard; + return std::llrint(x); +} + +long long APS5_VABI llrintl_nid_postfix(long double x) { + ErrnoGuard guard; + return std::llrint(x); +} + +long double APS5_VABI sqrtl_nid_postfix(long double x) { + return std::sqrt(x); +} + +long double APS5_VABI modfl_nid_postfix(long double x, long double* integral) { + if (std::isnan(x)) { + *integral = x; + return x; + } + return std::modf(x, integral); +} +long double APS5_VABI nanl_nid_postfix(const char*) { return std::numeric_limits::quiet_NaN(); } + +long double APS5_VABI scalblnl_nid_postfix(long double x, std::int64_t exponent) { + ErrnoGuard guard; + return std::scalbn(x, static_cast(exponent)); +} + +long double APS5_VABI fmal_nid_postfix(long double x, long double y, long double z) { + if (x == 0.0L || y == 0.0L) return x * y + z; + if (z == 0.0L) return x * y; + if (!std::isfinite(x) || !std::isfinite(y)) return x * y + z; + if (!std::isfinite(z)) return z; + int xExponent = 0, yExponent = 0, zExponent = 0; + std::frexp(x, &xExponent); + std::frexp(y, &yExponent); + std::frexp(z, &zExponent); + if (xExponent + yExponent - zExponent > 2 * std::numeric_limits::digits) return x * y; + ErrnoGuard guard; + return std::fma(x, y, z); +} + +double APS5_VABI rint_nid_postfix(double x) { return std::isnan(x) ? x + x : std::rint(x); } +float APS5_VABI rintf_nid_postfix(float x) { return std::isnan(x) ? x + x : std::rint(x); } + +std::int64_t APS5_VABI lrint_nid_postfix(double x) { + ErrnoGuard guard; + return std::llrint(x); +} + +std::int64_t APS5_VABI lrintf_nid_postfix(float x) { + ErrnoGuard guard; + return std::llrint(x); +} + +long long APS5_VABI llrint_nid_postfix(double x) { + ErrnoGuard guard; + return std::llrint(x); +} + +long long APS5_VABI llrintf_nid_postfix(float x) { + ErrnoGuard guard; + return std::llrint(x); +} + +double APS5_VABI drem_nid_postfix(double x, double y) { + return Remainder(x, y); +} + +float APS5_VABI dremf_nid_postfix(float x, float y) { + return Remainder(x, y); +} + +double APS5_VABI scalbln_nid_postfix(double x, std::int64_t exponent) { + ErrnoGuard guard; + return std::scalbn(x, Exponent(exponent)); +} + +float APS5_VABI scalblnf_nid_postfix(float x, std::int64_t exponent) { + ErrnoGuard guard; + return std::scalbn(x, Exponent(exponent)); +} + +double APS5_VABI scalb_nid_postfix(double x, double exponent) { + ErrnoGuard guard; + if (std::isnan(x) || std::isnan(exponent)) return x * exponent; + if (std::isinf(exponent)) return exponent > 0 ? x * exponent : x / -exponent; + if (std::rint(exponent) != exponent) return (exponent - exponent) / (exponent - exponent); + if (exponent > 65000.0) return std::scalbn(x, 65000); + if (exponent < -65000.0) return std::scalbn(x, -65000); + return std::scalbn(x, static_cast(exponent)); +} + +float APS5_VABI scalbf_nid_postfix(float x, float exponent) { + ErrnoGuard guard; + if (std::isnan(x) || std::isnan(exponent)) return x * exponent; + if (std::isinf(exponent)) return exponent > 0 ? x * exponent : x / -exponent; + if (std::rint(exponent) != exponent) return (exponent - exponent) / (exponent - exponent); + if (exponent > 65000.0f) return std::scalbn(x, 65000); + if (exponent < -65000.0f) return std::scalbn(x, -65000); + return std::scalbn(x, static_cast(exponent)); +} + +int APS5_VABI finite_nid_postfix(double x) { return std::isfinite(x) ? 1 : 0; } +int APS5_VABI finitef_nid_postfix(float x) { return std::isfinite(x) ? 1 : 0; } + +int APS5_VABI __fpclassifyl_nid_postfix(long double x) { + const ExtendedBits bits = Bits(x); + if ((bits.signExponent & 0x7fff) != 0 || bits.mantissa == 0) return TestExtended(bits); + return Denormal; +} +int APS5_VABI __isinfl_nid_postfix(long double x) { return TestExtended(Bits(x)) == Infinite ? 1 : 0; } +int APS5_VABI __isnanl_nid_postfix(long double x) { return TestExtended(Bits(x)) == NotANumber ? 1 : 0; } + +int APS5_VABI __isnormall_nid_postfix(long double x) { + const int exponent = Bits(x).signExponent & 0x7fff; + return exponent != 0 && exponent != 0x7fff ? 1 : 0; +} + +short APS5_VABI _LDtest_nid_postfix(const long double* value) { return TestExtended(Bits(*value)); } +int APS5_VABI _Dclass_nid_postfix(double x) { return TestFloat(x); } +int APS5_VABI _FDclass_nid_postfix(float x) { return TestFloat(x); } +int APS5_VABI _LDclass_nid_postfix(long double x) { return TestExtended(Bits(x)); } +short APS5_VABI _LDsign_nid_postfix(long double x) { return static_cast(Bits(x).signExponent & 0x8000); } + +} diff --git a/core/libs/prx/libc/src/Mspace.cpp b/core/libs/prx/libc/src/Mspace.cpp index 143d50eba2..8830e5c75c 100644 --- a/core/libs/prx/libc/src/Mspace.cpp +++ b/core/libs/prx/libc/src/Mspace.cpp @@ -1,5 +1,7 @@ +#include "prx/libc/include/MallocManagedSize.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include @@ -8,6 +10,7 @@ #include #include #include +#include #include extern "C" int* APS5_VABI __error_nid_postfix(); @@ -124,17 +127,6 @@ bool InsideAllocation(const Arena& arena, std::uintptr_t start, std::size_t size return found->second.used && size <= found->second.end - start && start - found->first <= found->second.end - found->first - size; } -struct MallocManagedSize { - std::uint16_t size; - std::uint16_t version; - std::uint32_t reserved; - std::size_t maxSystemSize; - std::size_t currentSystemSize; - std::size_t maxInuseSize; - std::size_t currentInuseSize; -}; -static_assert(sizeof(MallocManagedSize) == 0x28); - int FillStats(void* handle, MallocManagedSize* stats) { if (!stats || stats->size < sizeof(MallocManagedSize)) return 22; std::lock_guard lock(arenaMutex); @@ -265,19 +257,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 +299,56 @@ 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 sceLibcPafMspaceMemalign_nid_postfix(void* handle, std::size_t alignment, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspaceMemalign_nid_postfix(handle, alignment, size); +} + +void APS5_VABI sceLibcPafMspaceFree_nid_postfix(void* handle, void* pointer, const void* owner) { + (void)owner; + sceLibcMspaceFree_nid_postfix(handle, pointer); +} + +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 sceLibcPafMspacePosixMemalign_nid_postfix(void* handle, void** result, std::size_t alignment, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspacePosixMemalign_nid_postfix(handle, result, alignment, size); +} + +int APS5_VABI sceLibcPafMspaceMallocStats_nid_postfix(void* handle, MallocManagedSize* stats) { + return sceLibcMspaceMallocStats_nid_postfix(handle, stats); +} + +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..e7dc400ac3 100644 --- a/core/libs/prx/libc/src/Multibyte.cpp +++ b/core/libs/prx/libc/src/Multibyte.cpp @@ -1,10 +1,59 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/SonyCrt.hpp" +#include #include #include #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 +100,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; @@ -143,6 +304,13 @@ int APS5_VABI wcsrtombs_s_nid_postfix( return 0; } +int APS5_VABI wcstombs_s_nid_postfix( + std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit +) { + std::array state{}; + return wcsrtombs_s_nid_postfix(result, destination, capacity, &source, limit, state.data()); +} + std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const char** source, std::size_t capacity, void*) { if (!destination) return std::strlen(*source); std::size_t converted = 0; diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 76a702a2a6..9d2f6909b0 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 @@ -5,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -52,7 +54,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() { @@ -96,6 +98,58 @@ extern "C" void LibcRunShutdown_nid_postfix() { if (error) std::rethrow_exception(error); } +namespace { + +std::atomic endFlush{nullptr}; +std::atomic exitDrain{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(); +} + +extern "C" void LibcSetExitDrain_nid_no_patch(void (*drain)()) { + if (drain == nullptr) throw std::runtime_error("libc: invalid exit drain registration"); + void (*registered)() = nullptr; + if (!exitDrain.compare_exchange_strong(registered, drain) && registered != drain) throw std::runtime_error("libc: exit drain already registered"); +} + +namespace { + +void StartExit() { + exitRequested.store(true); + if (exitStarted.exchange(true)) { + if (!runningExit) LibcAwaitExit_nid_postfix(); + } else { + runningExit = true; + } +} + +} + extern "C" { [[noreturn]] void APS5_VABI _Exit_nid_postfix(int code) { @@ -108,24 +162,51 @@ extern "C" { } [[noreturn]] void LibcTerminate_nid_no_patch(int code) { + if (const auto drain = exitDrain.exchange(nullptr)) { + try { + auto completion = std::make_shared>(); + auto finished = completion->get_future(); + std::thread([completion, drain] { + try { + drain(); + completion->set_value(); + } catch (...) { + completion->set_exception(std::current_exception()); + } + }).detach(); + if (finished.wait_for(std::chrono::seconds(8)) == std::future_status::ready) finished.get(); + else std::fputs("[libc] host exit drain exceeded 8 seconds\n", stderr); + } catch (const std::exception& error) { + std::fprintf(stderr, "[libc] host exit drain failed: %s\n", error.what()); + } catch (...) { + std::fputs("[libc] host exit drain failed\n", stderr); + } + } + LibcRunEndFlush_nid_no_patch(); #ifdef _WIN32 TerminateProcess(GetCurrentProcess(), static_cast(code)); #endif std::_Exit(code); } +void LibcStartExit_nid_postfix() { + StartExit(); +} + [[noreturn]] void LibcExit_nid_no_patch(int code) { - exitRequested.store(true); - if (exitStarted.exchange(true)) { - if (!runningExit) LibcAwaitExit_nid_postfix(); - } else { - runningExit = true; - LibcRunShutdown_nid_postfix(); - } + 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); +} + void APS5_VABI exit_nid_postfix(int code) { static const bool trace = std::getenv("APS5_TRACE_EXIT") != nullptr; if (trace) { @@ -145,6 +226,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..06cf1d789d 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); @@ -123,7 +126,7 @@ std::int64_t APS5_VABI nrand48_nid_postfix(std::uint16_t* seed) { } std::int64_t APS5_VABI jrand48_nid_postfix(std::uint16_t* seed) { - return static_cast(static_cast(NextRand48(seed, __func__) >> 16)); + return static_cast(NextRand48(seed, __func__) >> 16); } double APS5_VABI drand48_nid_postfix() { @@ -138,7 +141,7 @@ std::int64_t APS5_VABI lrand48_nid_postfix() { std::int64_t APS5_VABI mrand48_nid_postfix() { std::lock_guard lock(g_randomLock); - return static_cast(static_cast(NextRand48(g_rand48Seed, __func__) >> 16)); + return static_cast(NextRand48(g_rand48Seed, __func__) >> 16); } void APS5_VABI srand48_nid_postfix(std::int64_t seedval) { @@ -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/Rename.cpp b/core/libs/prx/libc/src/Rename.cpp index d7f2126c76..973c142ff6 100644 --- a/core/libs/prx/libc/src/Rename.cpp +++ b/core/libs/prx/libc/src/Rename.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/RenameDot.hpp" #include "prx/libc/include/General.hpp" #include #include @@ -10,11 +11,11 @@ #include #endif -std::error_code RenameNative_nid_no_patch(const std::filesystem::path& source, const std::filesystem::path& destination, [[maybe_unused]] bool replacesFile) { +std::error_code RenameNative_nid_no_patch(const std::filesystem::path& source, const std::filesystem::path& destination, [[maybe_unused]] bool replacesExisting) { namespace fs = std::filesystem; std::error_code error; #ifdef _WIN32 - if (replacesFile) { + if (replacesExisting) { const auto target = fs::absolute(destination, error); if (error) return error; struct RenameInfo { DWORD flags; HANDLE root; DWORD nameBytes; WCHAR name[1]; }; @@ -28,7 +29,7 @@ std::error_code RenameNative_nid_no_patch(const std::filesystem::path& source, c info->flags = replaceExisting | posixSemantics | (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_READONLY) ? ignoreReadOnly : 0); info->nameBytes = static_cast(nameBytes); std::memcpy(info->name, target.c_str(), nameBytes); - const auto handle = CreateFileW(source.c_str(), DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, nullptr); + const auto handle = CreateFileW(source.c_str(), DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); if (handle == INVALID_HANDLE_VALUE) return std::error_code(static_cast(GetLastError()), std::system_category()); constexpr auto renameInfoEx = static_cast(22); if (!SetFileInformationByHandle(handle, renameInfoEx, info, static_cast(buffer.size()))) error = std::error_code(static_cast(GetLastError()), std::system_category()); @@ -42,7 +43,6 @@ std::error_code RenameNative_nid_no_patch(const std::filesystem::path& source, c static int RenamePath_nid_no_patch(const std::filesystem::path& source, const std::filesystem::path& destination) { constexpr int GuestEnoent = 2; - constexpr int GuestEio = 5; constexpr int GuestEnotdir = 20; constexpr int GuestEisdir = 21; constexpr int GuestEinval = 22; @@ -90,24 +90,36 @@ static int RenamePath_nid_no_patch(const std::filesystem::path& source, const st return error ? FilesystemError(error) : GuestEnotempty; } } -#ifdef _WIN32 - if (!sameFile && sourceIsDirectory && destinationIsDirectory) { - error.clear(); - if (!fs::remove(destination, error)) return error ? FilesystemError(error) : GuestEio; - } -#endif - - error = RenameNative_nid_no_patch(source, destination, destinationExists && !sourceIsDirectory && !destinationIsDirectory); + error = RenameNative_nid_no_patch(source, destination, destinationExists && (!sourceIsDirectory || !sameFile)); return error ? FilesystemError(error) : 0; } +static int SourceLookupError_nid_no_patch(const std::filesystem::path& source) { + std::error_code status; + const auto sourceStatus = std::filesystem::symlink_status(source, status); + if (HasNonDirectoryPrefix_nid_no_patch(source)) return 20; + if (status && status != std::errc::no_such_file_or_directory) return FilesystemError(status); + return sourceStatus.type() == std::filesystem::file_type::not_found ? 2 : 0; +} + extern "C" int RenameGuestPath_nid_no_patch(const char* from, const char* to) { - if (!from || !to) { errno = 14; return -1; } - if (!*from || !*to) { errno = 2; return -1; } + if (!from) { errno = 14; return -1; } + if (!*from) { errno = 2; return -1; } + if (const int dot = RenameDotError_nid_no_patch(from)) { errno = dot; return -1; } + const int destinationError = !to ? 14 : !*to ? 2 : RenameDotError_nid_no_patch(to); try { const auto source = ResolvePath_nid_no_patch(from); + if (destinationError) { + const int sourceError = SourceLookupError_nid_no_patch(source); + errno = sourceError ? sourceError : destinationError; + return -1; + } const auto destination = ResolvePath_nid_no_patch(to); - const int error = RenamePath_nid_no_patch(source, destination); + int error = RenamePath_nid_no_patch(source, destination); + if (error == 2) { + std::error_code status; + if (HasNonDirectoryPrefix_nid_no_patch(source) || (std::filesystem::exists(source, status) && HasNonDirectoryPrefix_nid_no_patch(destination))) error = 20; + } if (error) { errno = error; return -1; } RecordWrittenPath_nid_no_patch(source); RecordWrittenPath_nid_no_patch(destination); diff --git a/core/libs/prx/libc/src/RuneLocale.cpp b/core/libs/prx/libc/src/RuneLocale.cpp index 787200a281..fa25f0412c 100644 --- a/core/libs/prx/libc/src/RuneLocale.cpp +++ b/core/libs/prx/libc/src/RuneLocale.cpp @@ -154,4 +154,34 @@ int APS5_VABI wctob_nid_postfix(std::uint32_t c) { return c < 256 ? static_cast(c) : -1; } +std::uint32_t APS5_VABI _Towctrans_nid_postfix(std::uint32_t c, std::uint64_t desc); +const short* APS5_VABI _Getptolower_nid_postfix(); +const short* APS5_VABI _Getptoupper_nid_postfix(); + +std::uint64_t APS5_VABI wctrans_nid_postfix(const char* property) { + if (std::strcmp(property, "tolower") == 0) return 1; + if (std::strcmp(property, "toupper") == 0) return 2; + return 0; +} + +std::uint32_t APS5_VABI towctrans_nid_postfix(std::uint32_t c, std::uint64_t desc) { + return desc == 1 || desc == 2 ? _Towctrans_nid_postfix(c, desc) : c; +} + +std::uint32_t APS5_VABI _Towlower_nid_postfix(std::uint32_t c, const void*) { + return _Towctrans_nid_postfix(c, 1); +} + +std::uint32_t APS5_VABI _Towupper_nid_postfix(std::uint32_t c, const void*) { + return _Towctrans_nid_postfix(c, 2); +} + +int APS5_VABI _Tolower_nid_postfix(int c, const void*) { + return static_cast(c) <= 0x7f ? _Getptolower_nid_postfix()[c] : c; +} + +int APS5_VABI _Toupper_nid_postfix(int c, const void*) { + return static_cast(c) <= 0x7f ? _Getptoupper_nid_postfix()[c] : c; +} + } diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index 4b7ae591d7..f7204ea7f1 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -57,6 +57,10 @@ FileStream _Stderr_nid_postfix{stderr}; FileStream _Stdout_nid_postfix{stdout}; FileStream _Stdin_nid_postfix{stdin}; +std::FILE* GuestStandardOutputFile_nid_no_patch() { + return GetNativeStream(&_Stdout_nid_postfix); +} + int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dsoHandle) { std::lock_guard lock(g_exitMutex); g_exitDestructors.push_back({func, arg, dsoHandle}); @@ -113,13 +117,13 @@ unsigned int APS5_VABI _Atomic_load_4_nid_postfix(volatile unsigned int* target, } std::intmax_t APS5_VABI strtoimax_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; - return std::strtoimax(str, endptr, base); + if (RejectIntegerBase_nid_no_patch(str, endptr, base, false) || StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; + return ParseInteger(str, endptr, base, std::strtoimax); } std::uintmax_t APS5_VABI strtoumax_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; - return std::strtoumax(str, endptr, base); + if (RejectIntegerBase_nid_no_patch(str, endptr, base, false) || StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; + return ParseInteger(str, endptr, base, std::strtoumax); } unsigned int APS5_VABI _ZSt14_Random_devicev_nid_postfix() { @@ -135,18 +139,18 @@ unsigned int APS5_VABI _ZSt14_Random_devicev_nid_postfix() { void APS5_VABI _Lockfilelock_nid_postfix(FileStream* stream) { if (stream == nullptr) throw std::invalid_argument("_Lockfilelock: null stream"); #ifdef _WIN32 - _lock_file(GetNativeStream(stream)); + _lock_file(GetStreamHandle(stream)); #else - flockfile(GetNativeStream(stream)); + flockfile(GetStreamHandle(stream)); #endif } void APS5_VABI _Unlockfilelock_nid_postfix(FileStream* stream) { if (stream == nullptr) throw std::invalid_argument("_Unlockfilelock: null stream"); #ifdef _WIN32 - _unlock_file(GetNativeStream(stream)); + _unlock_file(GetStreamHandle(stream)); #else - funlockfile(GetNativeStream(stream)); + funlockfile(GetStreamHandle(stream)); #endif } @@ -161,15 +165,15 @@ void APS5_VABI funlockfile_nid_postfix(FileStream* stream) { int APS5_VABI ftrylockfile_nid_postfix(FileStream* stream) { if (stream == nullptr) throw std::invalid_argument("ftrylockfile: null stream"); #ifdef _WIN32 - return TryEnterCriticalSection(&reinterpret_cast(GetNativeStream(stream))->lock) ? 0 : -1; + return TryEnterCriticalSection(&reinterpret_cast(GetStreamHandle(stream))->lock) ? 0 : -1; #else - return ftrylockfile(GetNativeStream(stream)) == 0 ? 0 : -1; + return ftrylockfile(GetStreamHandle(stream)) == 0 ? 0 : -1; #endif } std::uint64_t APS5_VABI _Stoul_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; - return std::strtoull(str, endptr, base); + if (RejectIntegerBase_nid_no_patch(str, endptr, base, false) || StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; + return ParseInteger(str, endptr, base, std::strtoull); } void APS5_VABI _Locksyslock_nid_postfix() { @@ -192,8 +196,8 @@ unsigned __int128 APS5_VABI __umodti3_nid_postfix(unsigned __int128 dividend, un return SonyUmodti3(dividend, divisor); } -void APS5_VABI _ZNSt8ios_base7_AddstdEPS__nid_postfix(void*) { - NotImplemented_nid_no_patch(__func__); +__int128 APS5_VABI __modti3_nid_postfix(__int128 dividend, __int128 divisor) { + return SonyModti3(dividend, divisor); } } diff --git a/core/libs/prx/libc/src/ScanWideField.cpp b/core/libs/prx/libc/src/ScanWideField.cpp new file mode 100644 index 0000000000..5d5a35559d --- /dev/null +++ b/core/libs/prx/libc/src/ScanWideField.cpp @@ -0,0 +1,68 @@ +#include "prx/libc/include/ScanWideField.hpp" +#include "prx/libc/include/General.hpp" + +#include + +namespace { + +std::size_t DecodeUtf8(const char* text, char32_t* point) { + const auto lead = static_cast(text[0]); + if (lead < 0x80) { + *point = lead; + return 1; + } + std::size_t size = 0; + char32_t value = 0; + if ((lead & 0xE0) == 0xC0) { size = 2; value = lead & 0x1F; } + else if ((lead & 0xF0) == 0xE0) { size = 3; value = lead & 0x0F; } + else if ((lead & 0xF8) == 0xF0) { size = 4; value = lead & 0x07; } + else return 0; + for (std::size_t index = 1; index < size; ++index) { + const auto next = static_cast(text[index]); + if ((next & 0xC0) != 0x80) return 0; + value = (value << 6) | (next & 0x3F); + } + constexpr char32_t Smallest[] = {0, 0, 0x80, 0x800, 0x10000}; + if (value < Smallest[size] || value > 0x10FFFF || (value >= 0xD800 && value <= 0xDFFF)) return 0; + *point = value; + return size; +} + +} + +namespace LibcDetail { + +std::size_t ReadWideScanCharacter(const char* text, char32_t* point) { + const std::size_t size = DecodeUtf8(text, point); + if (size == 0) NotImplemented_nid_no_patch("scanf 16-bit wide conversion of input that is not UTF-8"); + if (*point > 0xFFFF) NotImplemented_nid_no_patch("scanf 16-bit wide conversion of a character outside the BMP"); + return size; +} + +bool InWideScanset(const std::string& specifier, char32_t point) { + std::size_t index = 1; + const bool negated = specifier[index] == '^'; + if (negated) ++index; + const std::size_t end = specifier.size() - 1; + bool member = false; + for (std::size_t first = index; index < end; ++index) { + const auto value = static_cast(specifier[index]); + if (value >= 0x80) NotImplemented_nid_no_patch("scanf 16-bit wide scanset with a character outside ASCII"); + if (specifier[index] == '-' && index != first && index + 1 < end) { + const auto low = static_cast(specifier[index - 1]); + const auto high = static_cast(specifier[index + 1]); + if (high >= 0x80) NotImplemented_nid_no_patch("scanf 16-bit wide scanset with a character outside ASCII"); + if (point >= low && point <= high) member = true; + ++index; + continue; + } + if (point == value) member = true; + } + return member != negated; +} + +void RequireNoPositionalWideScan(const char* format) { + if (std::strchr(format, '$') != nullptr) NotImplemented_nid_no_patch("sscanf positional arguments with a 16-bit wide conversion on Linux"); +} + +} diff --git a/core/libs/prx/libc/src/ScanningWide.cpp b/core/libs/prx/libc/src/ScanningWide.cpp index e03bf6a2d4..759e475c90 100644 --- a/core/libs/prx/libc/src/ScanningWide.cpp +++ b/core/libs/prx/libc/src/ScanningWide.cpp @@ -10,6 +10,7 @@ #include #include "SceTypes.hpp" +#include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/WindowsFormatting.hpp" @@ -23,6 +24,10 @@ extern "C" int APS5_VABI iswspace_nid_postfix(std::uint32_t c); extern "C" std::size_t APS5_VABI wcrtomb_nid_postfix(char* destination, std::uint16_t value, void* state); +extern "C" std::int32_t APS5_VABI fgetwc_nid_postfix(FileStream* stream); +extern "C" int APS5_VABI ungetc_nid_postfix(int value, FileStream* stream); +extern "C" void APS5_VABI flockfile_nid_postfix(FileStream* stream); +extern "C" void APS5_VABI funlockfile_nid_postfix(FileStream* stream); namespace { @@ -122,29 +127,101 @@ Fit FloatFit(std::string_view text) { if (digits == 0) return Fit::Invalid; if (Lower(text[index]) != (hex ? 'p' : 'e')) return Fit::Invalid; ++index; - if (index < text.size() && (text[index] == '+' || text[index] == '-')) ++index; - if (index == text.size()) return Fit::Prefix; + if (index == text.size()) return Fit::Valid; + if (text[index] == '+' || text[index] == '-') ++index; + if (index == text.size()) return Fit::Valid; for (; index < text.size(); ++index) { if (!IsDecimal(text[index])) return Fit::Invalid; } return Fit::Valid; } +class WideInput { + const char16_t* const text; + FileStream* const stream; + std::int32_t next = 0; + bool fetched = false; + std::int64_t consumed = 0; + +public: + explicit WideInput(const char16_t* buffer) : text(buffer), stream(nullptr) {} + explicit WideInput(FileStream* source) : text(nullptr), stream(source) {} + + std::int32_t Peek() { + if (!fetched) { + next = stream != nullptr ? fgetwc_nid_postfix(stream) : text[consumed] != 0 ? text[consumed] : -1; + fetched = true; + } + return next; + } + + void Advance() { + fetched = false; + ++consumed; + } + + std::int64_t Consumed() const { + return consumed; + } + + void Release() { + if (!fetched || stream == nullptr || next < 0) return; + fetched = false; + const std::uint32_t code = static_cast(next); + unsigned char bytes[4]; + std::size_t count = 0; + if (code < 0x80) { + bytes[count++] = static_cast(code); + } else if (code < 0x800) { + bytes[count++] = static_cast(0xc0 | (code >> 6)); + bytes[count++] = static_cast(0x80 | (code & 0x3f)); + } else if (code < 0x10000) { + bytes[count++] = static_cast(0xe0 | (code >> 12)); + bytes[count++] = static_cast(0x80 | ((code >> 6) & 0x3f)); + bytes[count++] = static_cast(0x80 | (code & 0x3f)); + } else { + bytes[count++] = static_cast(0xf0 | (code >> 18)); + bytes[count++] = static_cast(0x80 | ((code >> 12) & 0x3f)); + bytes[count++] = static_cast(0x80 | ((code >> 6) & 0x3f)); + bytes[count++] = static_cast(0x80 | (code & 0x3f)); + } + while (count != 0) ungetc_nid_postfix(bytes[--count], stream); + } +}; + +class LockedStream { + FileStream* const stream; + +public: + explicit LockedStream(FileStream* value) : stream(value->IsOpen() ? value : nullptr) { + if (stream != nullptr) flockfile_nid_postfix(stream); + } + ~LockedStream() { + if (stream != nullptr) funlockfile_nid_postfix(stream); + } +}; + class WideScanner { const char* const name; const bool sized; - const char16_t* const start; - const char16_t* input; + WideInput input; const char16_t* format; LibcDetail::FormatArguments args; int assigned = 0; bool converted = false; public: - WideScanner(const char* function, bool withSizes, const char16_t* buffer, const char16_t* formatText, const void* source) - : name(function), sized(withSizes), start(buffer), input(buffer), format(formatText), args(source) {} + WideScanner(const char* function, bool withSizes, WideInput source, const char16_t* formatText, const void* arguments) + : name(function), sized(withSizes), input(source), format(formatText), args(arguments) {} int Run() { + const int result = Scan(); + input.Release(); + return result; + } + +private: + int Scan() { while (*format != 0) { Outcome outcome = Outcome::Done; if (IsSpace(*format)) { @@ -163,7 +240,6 @@ public: return assigned; } -private: [[noreturn]] void Fail(const std::string& message) const { throw std::invalid_argument(std::string(name) + ": " + message); } @@ -177,13 +253,13 @@ private: } void SkipSpace() { - while (*input != 0 && IsSpace(*input)) ++input; + while (input.Peek() >= 0 && IsSpace(static_cast(input.Peek()))) input.Advance(); } Outcome Literal(char16_t expected) { - if (*input == 0) return Outcome::Input; - if (*input != expected) return Outcome::Matching; - ++input; + if (input.Peek() < 0) return Outcome::Matching; + if (input.Peek() != expected) return Outcome::Matching; + input.Advance(); return Outcome::Done; } @@ -228,6 +304,7 @@ private: if (*format == u'%') { ++format; SkipSpace(); + if (input.Peek() < 0) return Outcome::Input; return Literal(u'%'); } const Directive directive = Parse(); @@ -238,6 +315,7 @@ private: case u'u': return Integer(directive, 10, false); case u'x': case u'X': return Integer(directive, 16, false); + case u'p': return Pointer(directive); case u'a': case u'A': case u'e': case u'E': case u'f': case u'F': case u'g': case u'G': return Float(directive); case u's': @@ -249,30 +327,33 @@ private: } template - std::string Field(std::size_t width, TFit fit) const { + std::string Field(std::size_t width, TFit fit) { std::string field; const std::size_t limit = width == 0 ? std::numeric_limits::max() : width; while (field.size() < limit) { - const char16_t next = input[field.size()]; - if (next == 0 || next >= 0x80) break; + const std::int32_t next = input.Peek(); + if (next < 0 || next >= 0x80) break; field.push_back(static_cast(next)); if (fit(field) == Fit::Invalid) { field.pop_back(); break; } + input.Advance(); } return field; } Outcome Integer(const Directive& directive, int base, bool isSigned) { - const std::size_t size = IntegerSize(directive.length); + return Integer(directive, base, isSigned, IntegerSize(directive.length)); + } + + Outcome Integer(const Directive& directive, int base, bool isSigned, std::size_t size) { void* target = nullptr; if (!directive.suppress && (target = args.Next()) == nullptr) return Outcome::Violation; SkipSpace(); - if (*input == 0) return Outcome::Input; + if (input.Peek() < 0) return Outcome::Input; const std::string field = Field(directive.width, [base](std::string_view text) { return IntegerFit(text, base); }); if (IntegerFit(field, base) != Fit::Valid) return Outcome::Matching; - input += field.size(); if (target != nullptr) { const std::uint64_t value = isSigned ? static_cast(std::strtoll(field.c_str(), nullptr, base)) : std::strtoull(field.c_str(), nullptr, base); std::memcpy(target, &value, size); @@ -282,6 +363,11 @@ private: return Outcome::Done; } + Outcome Pointer(const Directive& directive) { + if (!directive.length.empty()) Fail("invalid pointer length modifier " + directive.length); + return Integer(directive, 16, false, sizeof(void*)); + } + Outcome Float(const Directive& directive) { static_assert(sizeof(long double) == 16 && std::numeric_limits::digits == 64); if (!directive.length.empty() && directive.length != "l" && directive.length != "L") @@ -289,10 +375,9 @@ private: void* target = nullptr; if (!directive.suppress && (target = args.Next()) == nullptr) return Outcome::Violation; SkipSpace(); - if (*input == 0) return Outcome::Input; + if (input.Peek() < 0) return Outcome::Input; const std::string field = Field(directive.width, FloatFit); if (FloatFit(field) != Fit::Valid) return Outcome::Matching; - input += field.size(); if (target != nullptr) { if (directive.length == "L") { const long double value = std::strtold(field.c_str(), nullptr); @@ -335,22 +420,25 @@ private: if (target == nullptr) return Outcome::Violation; } if (directive.conversion == u's') SkipSpace(); - if (*input == 0) return Outcome::Input; + if (input.Peek() < 0) return Outcome::Input; std::u16string item; if (directive.conversion == u'c') { const std::size_t count = directive.width == 0 ? 1 : directive.width; - while (item.size() < count && input[item.size()] != 0) item.push_back(input[item.size()]); + while (item.size() < count && input.Peek() >= 0) { + item.push_back(static_cast(input.Peek())); + input.Advance(); + } if (item.size() < count) return Outcome::Matching; } else { const std::size_t limit = directive.width == 0 ? std::numeric_limits::max() : directive.width; - while (item.size() < limit && input[item.size()] != 0) { - const char16_t next = input[item.size()]; + while (item.size() < limit && input.Peek() >= 0) { + const char16_t next = static_cast(input.Peek()); if (directive.conversion == u's' ? IsSpace(next) : !InSet(directive, next)) break; item.push_back(next); + input.Advance(); } if (item.empty()) return Outcome::Matching; } - input += item.size(); if (target == nullptr) { converted = true; return Outcome::Done; @@ -382,7 +470,7 @@ private: const std::size_t size = IntegerSize(directive.length); void* target = args.Next(); if (target == nullptr) return Outcome::Violation; - const std::int64_t count = input - start; + const std::int64_t count = input.Consumed(); std::memcpy(target, &count, size); return Outcome::Done; } @@ -396,7 +484,7 @@ int APS5_VABI swscanf_nid_postfix(const char16_t* buffer, const char16_t* format if (buffer == nullptr || format == nullptr) return EOF; APS5_VA_BEGIN(format); try { - const int result = WideScanner("swscanf", false, buffer, format, args).Run(); + const int result = WideScanner("swscanf", false, WideInput(buffer), format, args).Run(); APS5_VA_END(); return result; } catch (...) { @@ -409,7 +497,87 @@ int APS5_VABI swscanf_s_nid_postfix(const char16_t* buffer, const char16_t* form if (buffer == nullptr || format == nullptr) return EOF; APS5_VA_BEGIN(format); try { - const int result = WideScanner("swscanf_s", true, buffer, format, args).Run(); + const int result = WideScanner("swscanf_s", true, WideInput(buffer), format, args).Run(); + APS5_VA_END(); + return result; + } catch (...) { + APS5_VA_END(); + throw; + } +} + +int APS5_VABI vswscanf_nid_postfix(const char16_t* buffer, const char16_t* format, VaList* args) { + if (buffer == nullptr || format == nullptr) return EOF; + return WideScanner("vswscanf", false, WideInput(buffer), format, args).Run(); +} + +int APS5_VABI vswscanf_s_nid_postfix(const char16_t* buffer, const char16_t* format, VaList* args) { + if (buffer == nullptr || format == nullptr) return EOF; + return WideScanner("vswscanf_s", true, WideInput(buffer), format, args).Run(); +} + +extern FileStream* __stdinp_nid_postfix; + +int APS5_VABI vfwscanf_nid_postfix(FileStream* stream, const char16_t* format, VaList* args) { + if (stream == nullptr || format == nullptr) return EOF; + const LockedStream lock(stream); + return WideScanner("vfwscanf", false, WideInput(stream), format, args).Run(); +} + +int APS5_VABI vfwscanf_s_nid_postfix(FileStream* stream, const char16_t* format, VaList* args) { + if (stream == nullptr || format == nullptr) return EOF; + const LockedStream lock(stream); + return WideScanner("vfwscanf_s", true, WideInput(stream), format, args).Run(); +} + +int APS5_VABI vwscanf_nid_postfix(const char16_t* format, VaList* args) { + return vfwscanf_nid_postfix(__stdinp_nid_postfix, format, args); +} + +int APS5_VABI vwscanf_s_nid_postfix(const char16_t* format, VaList* args) { + return vfwscanf_s_nid_postfix(__stdinp_nid_postfix, format, args); +} + +int APS5_VABI fwscanf_nid_postfix(FileStream* stream, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + try { + const int result = vfwscanf_nid_postfix(stream, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; + } catch (...) { + APS5_VA_END(); + throw; + } +} + +int APS5_VABI fwscanf_s_nid_postfix(FileStream* stream, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + try { + const int result = vfwscanf_s_nid_postfix(stream, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; + } catch (...) { + APS5_VA_END(); + throw; + } +} + +int APS5_VABI wscanf_nid_postfix(const char16_t* format, ...) { + APS5_VA_BEGIN(format); + try { + const int result = vfwscanf_nid_postfix(__stdinp_nid_postfix, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; + } catch (...) { + APS5_VA_END(); + throw; + } +} + +int APS5_VABI wscanf_s_nid_postfix(const char16_t* format, ...) { + APS5_VA_BEGIN(format); + try { + const int result = vfwscanf_s_nid_postfix(__stdinp_nid_postfix, format, reinterpret_cast(args)); APS5_VA_END(); return result; } catch (...) { diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 69e24de20c..2cd656f01f 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -1,13 +1,32 @@ #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include +#include +#include #ifdef _WIN32 #include #else +#include #include #endif -namespace { FileStream input{stdin}; } +namespace { +#ifndef _WIN32 +const bool standardDescriptorsOpen = [] { + for (int descriptor = 0; descriptor <= 2; ++descriptor) { + if (::fcntl(descriptor, F_GETFD) != -1 || errno != EBADF) continue; + const int opened = ::open("/dev/null", descriptor == 0 ? O_RDONLY : O_WRONLY); + if (opened > descriptor) { + ::dup2(opened, descriptor); + ::close(opened); + } + } + return true; +}(); +#endif +FileStream input{stdin}; +} extern "C" { FileStream* __stdinp_nid_postfix = &input; FileStream* __stdoutp_nid_postfix = &_Stdout_nid_postfix; @@ -15,14 +34,43 @@ FileStream* __stderrp_nid_postfix = &_Stderr_nid_postfix; int __isthreaded_nid_postfix = 1; int APS5_VABI fgetc_nid_postfix(FileStream* stream) { - const int result = std::fgetc(GetNativeStream(stream)); + if (stream && !stream->IsOpen()) { errno = 9; return -1; } + auto* native = GetNativeStream(stream); + if (!stream->SwitchToReading()) return -1; + const int result = std::fgetc(native); stream->SyncStatus(); + stream->KeepDirectoryReadError(result == -1); return result; } std::int32_t APS5_VABI fgetwc_nid_postfix(FileStream* stream) { - return fgetc_nid_postfix(stream); + if (stream == nullptr) { + errno = 22; + return -1; + } + const int first = fgetc_nid_postfix(stream); + if (first == -1) return -1; + const unsigned char lead = static_cast(first); + if (lead < 0x80) return lead; + const std::size_t extra = lead >= 0xf0 ? 3 : lead >= 0xe0 ? 2 : lead >= 0xc0 ? 1 : 0; + if (extra == 0) { + errno = 86; + stream->SetError(); + return -1; + } + std::uint32_t code = extra == 3 ? (lead & 0x07) : extra == 2 ? (lead & 0x0f) : (lead & 0x1f); + for (std::size_t index = 0; index < extra; ++index) { + const int next = fgetc_nid_postfix(stream); + if (next == -1 || (static_cast(next) & 0xc0) != 0x80) { + errno = 86; + stream->SetError(); + return -1; + } + code = (code << 6) | (static_cast(next) & 0x3f); + } + return static_cast(code); } + std::int32_t APS5_VABI ungetwc_nid_postfix(std::int32_t value, FileStream* stream) { if (value == -1) return -1; if (value < 0 || value > 255) { @@ -30,29 +78,40 @@ std::int32_t APS5_VABI ungetwc_nid_postfix(std::int32_t value, FileStream* strea stream->SetError(); return -1; } - const int result = std::ungetc(value, GetNativeStream(stream)); + auto* native = GetNativeStream(stream); + if (!stream->SwitchToReading()) return -1; + const int result = std::ungetc(value, native); stream->SyncStatus(); return result; } int APS5_VABI getc_nid_postfix(FileStream* stream) { return fgetc_nid_postfix(stream); } int APS5_VABI getc_unlocked_nid_postfix(FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } + auto* native = GetNativeStream(stream); + if (!stream->SwitchToReadingUnlocked()) return -1; #ifdef _WIN32 - const int result = _fgetc_nolock(GetNativeStream(stream)); + const int result = _fgetc_nolock(native); #else - const int result = ::getc_unlocked(GetNativeStream(stream)); + const int result = ::getc_unlocked(native); #endif stream->SyncUnlockedStatus(); + stream->KeepDirectoryReadError(result == -1); return result; } int APS5_VABI __srget_nid_postfix(FileStream* stream) { return fgetc_nid_postfix(stream); } int APS5_VABI getchar_nid_postfix() { return fgetc_nid_postfix(__stdinp_nid_postfix); } +std::int32_t APS5_VABI getwc_nid_postfix(FileStream* stream) { return fgetwc_nid_postfix(stream); } +std::int32_t APS5_VABI getwchar_nid_postfix() { return fgetwc_nid_postfix(__stdinp_nid_postfix); } +int APS5_VABI getchar_unlocked_nid_postfix() { return getc_unlocked_nid_postfix(__stdinp_nid_postfix); } int APS5_VABI fputc_nid_postfix(int value, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } const int result = std::fputc(value, GetNativeStream(stream)); stream->SyncStatus(); return result; } int APS5_VABI putc_nid_postfix(int value, FileStream* stream) { return fputc_nid_postfix(value, stream); } int APS5_VABI putc_unlocked_nid_postfix(int value, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } #ifdef _WIN32 const int result = _fputc_nolock(value, GetNativeStream(stream)); #else @@ -63,19 +122,27 @@ int APS5_VABI putc_unlocked_nid_postfix(int value, FileStream* stream) { } int APS5_VABI __swbuf_nid_postfix(int value, FileStream* stream) { return fputc_nid_postfix(value, stream); } int APS5_VABI putchar_nid_postfix(int value) { return fputc_nid_postfix(value, __stdoutp_nid_postfix); } +int APS5_VABI putchar_unlocked_nid_postfix(int value) { return putc_unlocked_nid_postfix(value, __stdoutp_nid_postfix); } int APS5_VABI ungetc_nid_postfix(int value, FileStream* stream) { - const int result = std::ungetc(value, GetNativeStream(stream)); + if (stream && !stream->IsOpen()) { errno = 9; return -1; } + auto* native = GetNativeStream(stream); + if (value != EOF && !stream->SwitchToReading()) return -1; + const int result = std::ungetc(value, native); stream->SyncStatus(); return result; } char* APS5_VABI fgets_nid_postfix(char* buffer, int size, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return nullptr; } if (size <= 0) { stream->SetError(); errno = 22; return nullptr; } - auto* result = std::fgets(buffer, size, GetNativeStream(stream)); + auto* native = GetNativeStream(stream); + if (!stream->SwitchToReading()) return nullptr; + auto* result = std::fgets(buffer, size, native); stream->SyncStatus(); + stream->KeepDirectoryReadError(result == nullptr); return result; } int APS5_VABI feof_nid_postfix(FileStream* stream) { @@ -86,10 +153,21 @@ int APS5_VABI ferror_nid_postfix(FileStream* stream) { stream->SyncStatus(); return (stream->GuestState().flags & 0x40) != 0; } + +int APS5_VABI feof_unlocked_nid_postfix(FileStream* stream) { + stream->SyncUnlockedStatus(); + return (stream->GuestState().flags & 0x20) != 0; +} + +int APS5_VABI ferror_unlocked_nid_postfix(FileStream* stream) { + stream->SyncUnlockedStatus(); + return (stream->GuestState().flags & 0x40) != 0; +} void APS5_VABI clearerr_nid_postfix(FileStream* stream) { stream->ClearError(); } int APS5_VABI fileno_nid_postfix(FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } #ifdef _WIN32 const int descriptor = _fileno(GetNativeStream(stream)); #else @@ -98,14 +176,25 @@ 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; const int native = mode == 0 ? _IOFBF : mode == 1 ? _IOLBF : _IONBF; if (native != _IONBF && size == 0) { buffer = nullptr; size = BUFSIZ; } - return std::setvbuf(GetNativeStream(stream), buffer, native, size); + return std::setvbuf(GetStreamHandle(stream), buffer, native, size); } void APS5_VABI setbuf_nid_postfix(FileStream* stream, char* buffer) { setvbuf_nid_postfix(stream, buffer, buffer ? 0 : 2, buffer ? 1024 : 0); diff --git a/core/libs/prx/libc/src/StdioExtras.cpp b/core/libs/prx/libc/src/StdioExtras.cpp index ab69378356..28b5f7b33b 100644 --- a/core/libs/prx/libc/src/StdioExtras.cpp +++ b/core/libs/prx/libc/src/StdioExtras.cpp @@ -38,6 +38,24 @@ void* Allocate(std::size_t size) { } } +void* AllocateAligned(std::size_t size, std::size_t alignment) { + for (;;) { + GuestNewHandler handler = nullptr; + try { + return ApplicationHeapAlign_nid_no_patch(alignment, size == 0 ? 1 : size); + } catch (const std::bad_alloc&) { + handler = g_newHandler.load(); + if (handler == nullptr) throw; + } catch (const std::length_error&) { + handler = g_newHandler.load(); + if (handler == nullptr) throw std::bad_alloc(); + } catch (const std::invalid_argument&) { + throw std::bad_alloc(); + } + handler(); + } +} + } extern "C" { @@ -89,6 +107,10 @@ void* APS5_VABI _Znam_nid_postfix(std::size_t size) { return Allocate(size); } +void* APS5_VABI _ZnwmSt11align_val_t_nid_postfix(std::size_t size, std::size_t alignment) { + return AllocateAligned(size, alignment); +} + void* APS5_VABI _ZnwmRKSt9nothrow_t_nid_postfix(std::size_t size, const void*) noexcept { try { return Allocate(size); diff --git a/core/libs/prx/libc/src/StringExtensions.cpp b/core/libs/prx/libc/src/StringExtensions.cpp index f1f4eb975d..18b9cb6f18 100644 --- a/core/libs/prx/libc/src/StringExtensions.cpp +++ b/core/libs/prx/libc/src/StringExtensions.cpp @@ -3,24 +3,51 @@ #include #include #include +#include +#include +#include #include extern "C" { -char* APS5_VABI basename_nid_postfix(const char* path) { - thread_local char buffer[1024]; +long long APS5_VABI strtonum_nid_postfix(const char* text, long long minimum, long long maximum, const char** error) { + const int savedError = errno; + const auto fail = [error](const char* message, int number) -> long long { + if (error) *error = message; + errno = number; + return 0; + }; + if (minimum > maximum) return fail("invalid", 22); + errno = 0; + char* end = nullptr; + const long long value = std::strtoll(text, &end, 10); + if (errno == EINVAL || end == text || *end != '\0') return fail("invalid", 22); + if ((value == LLONG_MIN && errno == ERANGE) || value < minimum) return fail("too small", 34); + if ((value == LLONG_MAX && errno == ERANGE) || value > maximum) return fail("too large", 34); + if (error) *error = nullptr; + errno = savedError; + return value; +} + +char* APS5_VABI basename_r_nid_postfix(const char* path, char* buffer) { + if (buffer == nullptr) throw std::invalid_argument("basename_r: null buffer"); std::string_view name = path && *path ? path : "."; while (name.size() > 1 && name.back() == '/') name.remove_suffix(1); if (name != "/") { const auto separator = name.find_last_of('/'); if (separator != std::string_view::npos) name.remove_prefix(separator + 1); } - if (name.size() >= sizeof(buffer)) { errno = 63; return nullptr; } + if (name.size() >= 1024) { errno = 63; return nullptr; } std::memcpy(buffer, name.data(), name.size()); buffer[name.size()] = '\0'; return buffer; } +char* APS5_VABI basename_nid_postfix(const char* path) { + thread_local char buffer[1024]; + return basename_r_nid_postfix(path, buffer); +} + char* APS5_VABI __inet_ntoa_nid_postfix(unsigned int address) { thread_local char buffer[sizeof("255.255.255.255")]; unsigned char bytes[sizeof(address)]; diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index c0f6747057..b3d173267a 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include #include "prx/libc/include/General.hpp" #include "prx/libc/include/SonyCrt.hpp" @@ -37,31 +39,130 @@ std::string AsciiPrefix(const char16_t* text) { return prefix; } -bool IsNanSubject(const char* text) { +struct NanSubject { + const char* tail; + bool negative; +}; + +NanSubject FindNanSubject(const char* text) { while (*text == ' ' || (*text >= '\t' && *text <= '\r')) ++text; - if (*text == '+' || *text == '-') ++text; - return *text == 'n' || *text == 'N'; + bool negative = false; + if (*text == '+' || *text == '-') negative = *text++ == '-'; + if ((text[0] | 0x20) == 'n' && (text[1] | 0x20) == 'a' && (text[2] | 0x20) == 'n') return {text + 3, negative}; + return {nullptr, negative}; } -double QuietNan(double value, const char* text) { - const auto bits = std::bit_cast(value); - if ((bits & 0x7ff0000000000000u) != 0x7ff0000000000000u || !IsNanSubject(text)) return value; - return std::bit_cast((bits & 0x8000000000000000u) | 0x7ff8000000000000u); +unsigned HexNanDigit(unsigned char c) { + if (c >= '0' && c <= '9') return c - '0' + 0x10; + if (c >= 'a' && c <= 'f') return c - 'a' + 0x1a; + if (c >= 'A' && c <= 'F') return c - 'A' + 0x1a; + return 0; } -float QuietNan(float value, const char* text) { - const auto bits = std::bit_cast(value); - if ((bits & 0x7f800000u) != 0x7f800000u || !IsNanSubject(text)) return value; - return std::bit_cast((bits & 0x80000000u) | 0x7fc00000u); +void ShiftNanWords(std::uint32_t* words, int from, int to, int digits) { + const int shift = (8 - digits) * 4; + const int carry = 32 - shift; + int index = from; + do { + words[index] |= words[index + 1] << carry; + words[index + 1] >>= shift; + } while (++index < to); } -long double QuietNan(long double value, const char* text) { - std::uint16_t exponent; - std::memcpy(&exponent, reinterpret_cast(&value) + sizeof(std::uint64_t), sizeof(exponent)); - if ((exponent & 0x7fffu) != 0x7fffu || !IsNanSubject(text)) return value; - const std::uint64_t significand = 0xc000000000000000u; - std::memcpy(&value, &significand, sizeof(significand)); - return value; +std::uint64_t HexNanPayload(const char* open, const char*& end) { + std::uint32_t words[2] = {}; + constexpr int high = 1; + int top = high; + int group = high; + int digits = 0; + int groupStart = 0; + int inWord = 0; + const auto* s = reinterpret_cast(open); + const auto skipBlanksAndPrefix = [&s](bool inside) { + while (s[1] != 0 && s[1] <= ' ') ++s; + if (inside && s[1] == 0) throw std::invalid_argument("strtod: blanks after a NAN payload digit reach the terminating NUL, where the console reads past it"); + if (s[1] == '0' && (s[2] == 'x' || s[2] == 'X') && s[3] > ' ') s += 2; + }; + skipBlanksAndPrefix(false); + for (unsigned char c; (c = *++s) != 0;) { + const unsigned digit = HexNanDigit(c); + if (digit == 0) { + if (c <= ' ') { + if (groupStart < digits) { + if (top < group && inWord < 8) ShiftNanWords(words, top, group, inWord); + if (top <= 0) { + inWord = 8; + continue; + } + groupStart = digits; + words[--top] = 0; + group = top; + inWord = 0; + } + skipBlanksAndPrefix(true); + continue; + } + if (c == ')' && digits != 0) { + end = reinterpret_cast(s + 1); + break; + } + for (; c != 0; c = *++s) { + if (c == ')') { + end = reinterpret_cast(s + 1); + break; + } + } + return 0; + } + ++digits; + if (++inWord > 8) { + if (top <= 0) continue; + inWord = 1; + words[--top] = 0; + } + words[top] = (words[top] << 4) | (digit & 0xf); + } + if (digits == 0) return 0; + if (top < group && inWord < 8) ShiftNanWords(words, top, group, inWord); + if (top > 0) { + int to = 0; + int from = top; + do words[to++] = words[from++]; + while (from <= high); + do words[to++] = 0; + while (to <= high); + } else { + words[high] &= 0xffffffffu >> 12; + } + if (words[0] == 0 && words[1] == 0) words[0] = 1; + return (static_cast(words[1]) << 32) | words[0]; +} + +const char* NanSequenceEnd(const char* tail) { + if (*tail != '(') return tail; + const char* cursor = tail + 1; + while ((*cursor >= '0' && *cursor <= '9') || ((*cursor | 0x20) >= 'a' && (*cursor | 0x20) <= 'z') || *cursor == '_') ++cursor; + return *cursor == ')' ? cursor + 1 : tail; +} + +template +TFloat NanValue(const NanSubject& subject, const char*& end) { + if constexpr (std::is_same_v) { + end = subject.tail; + const std::uint64_t payload = *subject.tail == '(' ? HexNanPayload(subject.tail, end) : 0; + return std::bit_cast((subject.negative ? 0x8000000000000000ull : 0) | 0x7ff8000000000000ull | payload); + } else if constexpr (std::is_same_v) { + end = NanSequenceEnd(subject.tail); + return std::bit_cast(0x7fc00000u); + } else { + end = NanSequenceEnd(subject.tail); + TFloat value{}; + const std::uint64_t significand = 0xc000000000000000u; + const std::uint16_t signExponent = 0x7fff; + std::memcpy(&value, &significand, sizeof(significand)); + std::memcpy(reinterpret_cast(&value) + sizeof(significand), &signExponent, sizeof(signExponent)); + return value; + } } template @@ -75,10 +176,18 @@ auto ParseAsciiPrefix(const char16_t* text, char16_t** end, TParse parse) { template TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, char**)) { + if (const NanSubject subject = FindNanSubject(text); subject.tail != nullptr) { + const char* stop = subject.tail; + const TFloat value = NanValue(subject, stop); + if (end != nullptr) *end = const_cast(stop); + return value; + } char* parsedEnd = nullptr; + const int saved = errno; const TFloat value = parse(text, &parsedEnd); + if (errno == ERANGE && value != 0 && (value < 0 ? -value : value) < std::numeric_limits::min()) errno = saved; if (end != nullptr) *end = parsedEnd; - return parsedEnd == text ? TFloat{0} : QuietNan(value, text); + return parsedEnd == text ? TFloat{0} : value; } } @@ -94,6 +203,10 @@ void APS5_VABI bzero_nid_postfix(void* destination, size_t count) { std::memset(destination, 0, count); } +void APS5_VABI bcopy_nid_postfix(const void* source, void* destination, size_t count) { + if (count != 0) std::memmove(destination, source, count); +} + void* APS5_VABI memcpy_nid_postfix(void* dest, const void* src, size_t n) { return std::memcpy(dest, src, n); } @@ -120,6 +233,32 @@ void* APS5_VABI memrchr_nid_postfix(const void* s, int c, size_t n) { return nullptr; } +void* APS5_VABI memccpy_nid_postfix(void* dest, const void* src, int c, size_t n) { + auto* d = static_cast(dest); + const auto* s = static_cast(src); + const auto target = static_cast(c); + for (size_t i = 0; i < n; ++i) { + d[i] = s[i]; + if (s[i] == target) return d + i + 1; + } + return nullptr; +} + +void* APS5_VABI memmem_nid_postfix(const void* haystack, size_t haystacklen, const void* needle, size_t needlelen) { + if (needlelen == 0) return const_cast(haystack); + if (haystacklen < needlelen) return nullptr; + const auto* h = static_cast(haystack); + const auto* n = static_cast(needle); + const unsigned char first = n[0]; + const size_t limit = haystacklen - needlelen; + for (size_t i = 0; i <= limit; ++i) { + if (h[i] == first && std::memcmp(h + i, n, needlelen) == 0) { + return const_cast(h + i); + } + } + return nullptr; +} + int APS5_VABI strcmp_nid_postfix(const char* s1, const char* s2) { return std::strcmp(s1, s2); } @@ -148,8 +287,20 @@ const char* APS5_VABI strchr_nid_postfix(const char* s, int c) { return std::strchr(s, c); } +char* APS5_VABI index_nid_postfix(const char* s, int c) { + return const_cast(strchr_nid_postfix(s, c)); +} + char* APS5_VABI strrchr_nid_postfix(const char* s, int c) { - return std::strrchr(const_cast(s), c); + return std::strrchr(const_cast(s), static_cast(c)); +} + +char* APS5_VABI rindex_nid_postfix(const char* s, int c) { + 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) { @@ -168,6 +319,7 @@ size_t APS5_VABI strlcpy_nid_postfix(char* dest, const char* src, size_t size) { bool StopAtPrefixLetter_nid_no_patch(const char* str, char** endptr, int base) { if (base != 0 && base != 2 && base != 16) return false; + const char* const start = str; while (*str == ' ' || (*str >= '\t' && *str <= '\r')) ++str; if (*str == '+' || *str == '-') ++str; if (str[0] != '0' || str[1] == '\0') return false; @@ -176,28 +328,39 @@ bool StopAtPrefixLetter_nid_no_patch(const char* str, char** endptr, int base) { const bool hexDigit = (next >= '0' && next <= '9') || (next >= 'a' && next <= 'f') || (next >= 'A' && next <= 'F'); const bool bareHex = base != 2 && (str[1] == 'x' || str[1] == 'X') && !hexDigit; if (!binary && !bareHex) return false; - if (endptr) *endptr = const_cast(str + 1); + if (endptr) *endptr = const_cast(binary ? str + 1 : start); + return true; +} + +bool RejectIntegerBase_nid_no_patch(const char* str, char** endptr, int base, bool setError) { + if (base == 0 || (base >= 2 && base <= 36)) return false; + if (endptr) *endptr = const_cast(str); + if (setError) errno = EINVAL; return true; } std::int64_t APS5_VABI strtol_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; - return std::strtoll(str, endptr, base); + if (RejectIntegerBase_nid_no_patch(str, endptr, base, true) || StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; + return ParseInteger(str, endptr, base, std::strtoll); } std::uint64_t APS5_VABI strtoul_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; - return std::strtoull(str, endptr, base); + if (RejectIntegerBase_nid_no_patch(str, endptr, base, true) || StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; + return ParseInteger(str, endptr, base, std::strtoull); } long long APS5_VABI strtoll_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; - return std::strtoll(str, endptr, base); + if (RejectIntegerBase_nid_no_patch(str, endptr, base, true) || StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; + return ParseInteger(str, endptr, base, std::strtoll); } unsigned long long APS5_VABI strtoull_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; - return std::strtoull(str, endptr, base); + if (RejectIntegerBase_nid_no_patch(str, endptr, base, true) || StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; + return ParseInteger(str, endptr, base, std::strtoull); +} + +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) { @@ -214,15 +377,15 @@ long double APS5_VABI strtold_nid_postfix(const char* str, char** endptr) { } int APS5_VABI atoi_nid_postfix(const char* str) { - return static_cast(strtol_nid_postfix(str, nullptr, 10)); + return static_cast(strtoull_nid_postfix(str, nullptr, 10)); } std::int64_t APS5_VABI atol_nid_postfix(const char* str) { - return std::strtoll(str, nullptr, 10); + return static_cast(strtoull_nid_postfix(str, nullptr, 10)); } long long APS5_VABI atoll_nid_postfix(const char* str) { - return std::strtoll(str, nullptr, 10); + return static_cast(strtoull_nid_postfix(str, nullptr, 10)); } std::div_t APS5_VABI div_nid_postfix(int numerator, int denominator) { @@ -291,7 +454,9 @@ int APS5_VABI strcoll_nid_postfix(const char* s1, const char* s2) { int APS5_VABI strncpy_s_nid_postfix(char* dest, size_t destsz, const char* src, size_t count) { constexpr int GuestEinval = 22; constexpr int GuestErange = 34; + constexpr size_t RsizeMax = SIZE_MAX >> 1; if (!dest || destsz == 0) return GuestEinval; + if (destsz > RsizeMax) return GuestErange; if (!src) { dest[0] = '\0'; return GuestEinval; @@ -302,6 +467,12 @@ int APS5_VABI strncpy_s_nid_postfix(char* dest, size_t destsz, const char* src, dest[0] = '\0'; return GuestErange; } + const auto destination = reinterpret_cast(dest); + const auto source = reinterpret_cast(src); + if (destination < source + (length + 1) && source < destination + length + 1) { + dest[0] = '\0'; + return GuestEinval; + } std::memcpy(dest, src, length); dest[length] = '\0'; return 0; @@ -314,7 +485,9 @@ int APS5_VABI strcpy_s_nid_postfix(char* dest, size_t destsz, const char* src) { int APS5_VABI strncat_s_nid_postfix(char* dest, size_t destsz, const char* src, size_t count) { constexpr int GuestEinval = 22; constexpr int GuestErange = 34; + constexpr size_t RsizeMax = SIZE_MAX >> 1; if (!dest || destsz == 0) return GuestEinval; + if (destsz > RsizeMax) return GuestErange; size_t used = 0; while (used < destsz && dest[used] != '\0') ++used; if (used == destsz || !src) { @@ -327,6 +500,12 @@ int APS5_VABI strncat_s_nid_postfix(char* dest, size_t destsz, const char* src, dest[0] = '\0'; return GuestErange; } + const auto destination = reinterpret_cast(dest); + const auto source = reinterpret_cast(src); + if (destination < source + (length + 1) && source < destination + used + length + 1) { + dest[0] = '\0'; + return GuestEinval; + } std::memcpy(dest + used, src, length); dest[used + length] = '\0'; return 0; @@ -392,14 +571,21 @@ char* APS5_VABI strnstr_nid_postfix(const char* haystack, const char* needle, si } unsigned long long APS5_VABI _Stoull_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; - return std::strtoull(str, endptr, base); + if (RejectIntegerBase_nid_no_patch(str, endptr, base, false) || StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; + return ParseInteger(str, endptr, base, std::strtoull); } size_t APS5_VABI wcslen_nid_postfix(const char16_t* s) { return Length(s); } +size_t APS5_VABI wcsnlen_s_nid_postfix(const char16_t* s, size_t maxlen) { + if (!s) return 0; + size_t length = 0; + while (length < maxlen && s[length] != 0) ++length; + return length; +} + int APS5_VABI wcscmp_nid_postfix(const char16_t* s1, const char16_t* s2) { for (; *s1 == *s2; ++s1, ++s2) { if (*s1 == 0) return 0; @@ -471,6 +657,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; @@ -509,20 +701,48 @@ long double APS5_VABI wcstold_nid_postfix(const char16_t* str, char16_t** endptr return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return ParseFloat(text, end, std::strtold); }); } -long long APS5_VABI wcstol_nid_postfix(const char16_t* str, char16_t** endptr, int base) { - return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0LL : std::strtoll(text, end, base); }); -} - -long long APS5_VABI wcstoll_nid_postfix(const char16_t* str, char16_t** endptr, int base) { - return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0LL : std::strtoll(text, end, base); }); -} - unsigned long long APS5_VABI wcstoul_nid_postfix(const char16_t* str, char16_t** endptr, int base) { - return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0ULL : std::strtoull(text, end, base); }); + return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return RejectIntegerBase_nid_no_patch(text, end, base, false) || StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0ULL : ParseInteger(text, end, base, std::strtoull); }); } unsigned long long APS5_VABI wcstoull_nid_postfix(const char16_t* str, char16_t** endptr, int base) { - return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0ULL : std::strtoull(text, end, base); }); + return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return RejectIntegerBase_nid_no_patch(text, end, base, false) || StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0ULL : ParseInteger(text, end, base, std::strtoull); }); +} + +long long APS5_VABI wcstoll_nid_postfix(const char16_t* str, char16_t** endptr, int base) { + const char16_t* digits = str; + while (*digits == u' ' || (*digits >= u'\t' && *digits <= u'\r')) ++digits; + const bool negative = *digits == u'-'; + if (*digits == u'+' || *digits == u'-') ++digits; + const int saved = errno; + errno = 0; + char16_t* stop = nullptr; + const unsigned long long magnitude = wcstoull_nid_postfix(digits, &stop, base); + const bool overflowed = errno == ERANGE; + errno = saved; + if (stop == digits) { + if (endptr) *endptr = const_cast(str); + return 0; + } + if (endptr) *endptr = stop; + const unsigned long long limit = negative ? 0x8000000000000000ULL : 0x7fffffffffffffffULL; + if (overflowed || magnitude > limit) { + errno = ERANGE; + return negative ? std::numeric_limits::min() : std::numeric_limits::max(); + } + return static_cast(negative ? 0 - magnitude : magnitude); +} + +long long APS5_VABI wcstol_nid_postfix(const char16_t* str, char16_t** endptr, int base) { + return wcstoll_nid_postfix(str, endptr, base); +} + +long long APS5_VABI wcstoimax_nid_postfix(const char16_t* str, char16_t** endptr, int base) { + 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) { @@ -552,4 +772,109 @@ char* APS5_VABI strtok_s_nid_postfix(char* str, const char* delim, char** contex return SonyStrtokS(str, delim, context); } +int APS5_VABI wmemcpy_s_nid_postfix(char16_t* dest, size_t destsz, const char16_t* src, size_t count) { + constexpr int GuestEinval = 22; + constexpr int GuestErange = 34; + constexpr size_t RsizeMax = (SIZE_MAX >> 1) / sizeof(char16_t); + if (!dest) return GuestEinval; + if (destsz > RsizeMax) return GuestErange; + if (!src || count > destsz) { + wmemset_nid_postfix(dest, 0, destsz); + return src ? GuestErange : GuestEinval; + } + const auto destination = reinterpret_cast(dest); + const auto source = reinterpret_cast(src); + const auto distance = destination < source ? source - destination : destination - source; + if (count != 0 && distance < count * sizeof(char16_t)) { + wmemset_nid_postfix(dest, 0, destsz); + return GuestEinval; + } + if (count != 0) std::memcpy(dest, src, count * sizeof(char16_t)); + return 0; +} + +int APS5_VABI wmemmove_s_nid_postfix(char16_t* dest, size_t destsz, const char16_t* src, size_t count) { + constexpr int GuestEinval = 22; + constexpr int GuestErange = 34; + constexpr size_t RsizeMax = (SIZE_MAX >> 1) / sizeof(char16_t); + if (!dest) return GuestEinval; + if (destsz > RsizeMax) return GuestErange; + if (!src || count > destsz) { + wmemset_nid_postfix(dest, 0, destsz); + return src ? GuestErange : GuestEinval; + } + if (count != 0) std::memmove(dest, src, count * sizeof(char16_t)); + return 0; +} + +int APS5_VABI wcsncpy_s_nid_postfix(char16_t* dest, size_t destsz, const char16_t* src, size_t count) { + constexpr int GuestEinval = 22; + constexpr int GuestErange = 34; + constexpr size_t RsizeMax = (SIZE_MAX >> 1) / sizeof(char16_t); + if (!dest || destsz == 0) return GuestEinval; + if (destsz > RsizeMax) return GuestErange; + if (!src) { + dest[0] = 0; + return GuestEinval; + } + size_t length = 0; + while (length < count && src[length] != 0) ++length; + if (length >= destsz) { + dest[0] = 0; + return GuestErange; + } + const auto destination = reinterpret_cast(dest); + const auto source = reinterpret_cast(src); + const auto distance = destination < source ? source - destination : destination - source; + if (length != 0 && distance < (length + 1) * sizeof(char16_t)) { + dest[0] = 0; + return GuestEinval; + } + std::memcpy(dest, src, length * sizeof(char16_t)); + dest[length] = 0; + return 0; +} + +char16_t* APS5_VABI wcsncat_nid_postfix(char16_t* dest, const char16_t* src, size_t n) { + const size_t dest_len = Length(dest); + size_t index = 0; + for (; index < n && src[index] != 0; ++index) dest[dest_len + index] = src[index]; + dest[dest_len + index] = 0; + return dest; +} + +int APS5_VABI wcsncat_s_nid_postfix(char16_t* dest, size_t destsz, const char16_t* src, size_t count) { + constexpr int GuestEinval = 22; + constexpr int GuestErange = 34; + constexpr size_t RsizeMax = (SIZE_MAX >> 1) / sizeof(char16_t); + if (!dest || destsz == 0) return GuestEinval; + if (destsz > RsizeMax) return GuestErange; + size_t used = 0; + while (used < destsz && dest[used] != 0) ++used; + if (used == destsz || !src) { + dest[0] = 0; + return used == destsz ? GuestErange : GuestEinval; + } + size_t length = 0; + while (length < count && src[length] != 0) ++length; + if (length >= destsz - used) { + dest[0] = 0; + return GuestErange; + } + const auto destination = reinterpret_cast(dest + used); + const auto source = reinterpret_cast(src); + const auto distance = destination < source ? source - destination : destination - source; + if (length != 0 && distance < (length + 1) * sizeof(char16_t)) { + dest[0] = 0; + return GuestEinval; + } + std::memcpy(dest + used, src, length * sizeof(char16_t)); + dest[used + length] = 0; + return 0; +} + +int APS5_VABI wcscat_s_nid_postfix(char16_t* dest, size_t destsz, const char16_t* src) { + return wcsncat_s_nid_postfix(dest, destsz, src, static_cast(-1)); +} + } diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index b1d85f9faf..a00ba39463 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -1,5 +1,8 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/General.hpp" +#ifdef _WIN32 +#include "prx/libc/include/PreciseWait.hpp" +#endif #include #include #include @@ -14,6 +17,12 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif namespace { @@ -54,19 +63,68 @@ constexpr int mutexRecursive = 0x100; constexpr std::uint64_t mutexTag = 0x5854'4D5F'4D54'5844ull; constexpr std::uint64_t conditionTag = 0x5854'4E43'5F44'4E43ull; +#ifdef _WIN32 +struct DinkumwareNativeMutex { + CRITICAL_SECTION value; + + DinkumwareNativeMutex() { + if (!InitializeCriticalSectionAndSpinCount(&value, 0)) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "_Mtx_init: initializing critical section failed"); + } + + ~DinkumwareNativeMutex() { DeleteCriticalSection(&value); } + void lock() { EnterCriticalSection(&value); } + void unlock() { LeaveCriticalSection(&value); } + bool try_lock() { return TryEnterCriticalSection(&value) != FALSE; } +}; + +struct DinkumwareNativeCondition { + CONDITION_VARIABLE value = CONDITION_VARIABLE_INIT; +}; +#else +using DinkumwareNativeMutex = std::mutex; +using DinkumwareNativeCondition = std::condition_variable; +#endif + struct DinkumwareMutex { std::uint64_t tag = mutexTag; int type = 0; int count = 0; std::atomic owner{}; - std::mutex native; + DinkumwareNativeMutex native; }; struct DinkumwareCondition { std::uint64_t tag = conditionTag; - std::condition_variable native; + DinkumwareNativeCondition native; }; +#ifdef _WIN32 +DWORD WaitMillisecondsUntil(std::chrono::system_clock::time_point deadline) { + const auto now = std::chrono::system_clock::now(); + if (now >= deadline) + return 0; + const auto remaining = std::chrono::ceil(deadline - now).count(); + constexpr auto largest = static_cast(std::numeric_limits::max() - 1U); + return static_cast(std::min(remaining, largest)); +} + +bool WaitUntil(DinkumwareCondition* condition, DinkumwareMutex* mutex, std::chrono::system_clock::time_point deadline) { + for (;;) { + const DWORD milliseconds = WaitMillisecondsUntil(deadline); + if (milliseconds == 0) + return false; + if (SleepConditionVariableCS(&condition->native.value, &mutex->native.value, milliseconds) != FALSE) + return true; + const DWORD error = GetLastError(); + if (error != ERROR_TIMEOUT) + throw std::system_error(static_cast(error), std::system_category(), "_Cnd_timedwait: condition wait failed"); + if (std::chrono::system_clock::now() >= deadline) + return false; + } +} +#endif + DinkumwareMutex* ResolveMutex(void** handle, const char* function) { if (!handle) throw std::invalid_argument(std::string(function) + ": null mutex handle pointer"); @@ -187,6 +245,10 @@ int APS5_VABI _Cnd_init_nid_postfix(void** handle) { return threadSuccess; } +int APS5_VABI _Cnd_init_with_default_name_override_nid_postfix(void** handle, const char*) { + return _Cnd_init_nid_postfix(handle); +} + void APS5_VABI _Cnd_destroy_nid_postfix(void** handle) { if (!handle) throw std::invalid_argument("_Cnd_destroy: null condition handle pointer"); @@ -209,7 +271,12 @@ int APS5_VABI _Cnd_wait_nid_postfix(void** conditionHandle, void** mutexHandle) mutex->owner.store(std::thread::id{}, std::memory_order_release); std::unique_lock lock(mutex->native, std::adopt_lock); try { +#ifdef _WIN32 + if (SleepConditionVariableCS(&condition->native.value, &mutex->native.value, INFINITE) == FALSE) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "_Cnd_wait: condition wait failed"); +#else condition->native.wait(lock); +#endif } catch (const std::system_error&) { lock.release(); mutex->owner.store(std::this_thread::get_id(), std::memory_order_release); @@ -246,8 +313,13 @@ 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))); +#ifdef _WIN32 + timeout = !WaitUntil(condition, mutex, deadlineTime); +#else timeout = (condition->native.wait_until(lock, deadlineTime) == std::cv_status::timeout); +#endif } catch (const std::system_error&) { lock.release(); mutex->owner.store(std::this_thread::get_id(), std::memory_order_release); @@ -261,12 +333,20 @@ int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHan } int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle) { +#ifdef _WIN32 + WakeAllConditionVariable(&ResolveCondition(handle, "_Cnd_broadcast")->native.value); +#else ResolveCondition(handle, "_Cnd_broadcast")->native.notify_all(); +#endif return threadSuccess; } int SignalCondition_nid_no_patch(void** handle) { +#ifdef _WIN32 + WakeConditionVariable(&ResolveCondition(handle, "_Cnd_signal")->native.value); +#else ResolveCondition(handle, "_Cnd_signal")->native.notify_one(); +#endif return threadSuccess; } @@ -286,11 +366,16 @@ int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime*) { --remaining.tv_sec; } remaining.tv_sec = std::min(remaining.tv_sec, longestSleepSeconds); +#ifdef _WIN32 + PreciseSleepHundredNanos(static_cast(remaining.tv_sec) * 10000000ULL + + (static_cast(remaining.tv_nsec) + 99ULL) / 100ULL); +#else if (nanosleep(&remaining, nullptr) != 0) { if (errno == EINTR) continue; throw std::runtime_error("_Thrd_sleep: nanosleep failed"); } +#endif } } @@ -299,5 +384,167 @@ int APS5_VABI _Cnd_signal_nid_postfix(void** handle) { return SignalCondition_nid_no_patch(handle); } +struct GuestPad { + unsigned (APS5_VABI* const* vtable)(GuestPad*); + void* condition; + void* mutex; + bool started; +}; + +static_assert(offsetof(GuestPad, condition) == 0x8 && offsetof(GuestPad, mutex) == 0x10 && offsetof(GuestPad, started) == 0x18 && sizeof(GuestPad) == 0x20); + +[[noreturn]] void ThrowCppError_nid_no_patch(int code); + +static void RequireThrd(int result, const char* what) { + switch (result) { + case threadSuccess: + return; + case threadNomem: + case threadTimedout: + ThrowCppError_nid_no_patch(6); + case threadBusy: + ThrowCppError_nid_no_patch(0); + case threadError: + ThrowCppError_nid_no_patch(1); + } + throw std::runtime_error(std::string(what) + " failed with " + std::to_string(result)); +} + +static std::atomic registeredPthreadCreate{nullptr}; +static std::atomic registeredPthreadJoin{nullptr}; +static std::atomic registeredPthreadDetach{nullptr}; + +void RegisterPthreadCreate_nid_no_patch(PthreadCreateFn_nid_no_patch fn) { + registeredPthreadCreate.store(fn, std::memory_order_release); +} + +void RegisterPthreadJoin_nid_no_patch(PthreadJoinFn_nid_no_patch fn) { + registeredPthreadJoin.store(fn, std::memory_order_release); +} + +void RegisterPthreadDetach_nid_no_patch(PthreadDetachFn_nid_no_patch fn) { + registeredPthreadDetach.store(fn, std::memory_order_release); +} + +static void* APS5_VABI PadEntry_nid_no_patch(void* argument) { + auto* pad = static_cast(argument); + const auto go = pad->vtable[0]; + return reinterpret_cast(static_cast(go(pad))); +} + +void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Cnd_init_nid_postfix(&pad->condition), "_Pad::_Pad: _Cnd_init"); + const int created = _Mtx_init_nid_postfix(&pad->mutex, 1); + if (created != threadSuccess) { + _Cnd_destroy_nid_postfix(&pad->condition); + RequireThrd(created, "_Pad::_Pad: _Mtx_init"); + } + pad->started = false; + const int locked = _Mtx_lock_nid_postfix(&pad->mutex); + if (locked != threadSuccess) { + _Mtx_destroy_nid_postfix(&pad->mutex); + _Cnd_destroy_nid_postfix(&pad->condition); + RequireThrd(locked, "_Pad::_Pad: _Mtx_lock"); + } +} + +void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::~_Pad: _Mtx_unlock"); + _Mtx_destroy_nid_postfix(&pad->mutex); + _Cnd_destroy_nid_postfix(&pad->condition); +} + +void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Mtx_lock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_lock"); + pad->started = true; + RequireThrd(_Cnd_signal_nid_postfix(&pad->condition), "_Pad::_Release: _Cnd_signal"); + RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_unlock"); +} + +void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, void** thread) { + if (!pad || !thread) APS5_INVALID_ARG_EX; + const auto create = registeredPthreadCreate.load(std::memory_order_acquire); + if (!create) throw std::runtime_error("_Pad::_Launch: pthread_create is not registered"); + const int created = create(thread, nullptr, PadEntry_nid_no_patch, pad); + if (created != 0) ThrowCppError_nid_no_patch(6); + while (!pad->started) RequireThrd(_Cnd_wait_nid_postfix(&pad->condition, &pad->mutex), "_Pad::_Launch: _Cnd_wait"); +} + +int APS5_VABI _Thrd_join_nid_postfix(void* thread, int* code) { + const auto join = registeredPthreadJoin.load(std::memory_order_acquire); + if (!join) throw std::runtime_error("_Thrd_join: pthread_join is not registered"); + void* result = nullptr; + if (join(thread, &result) != 0) return threadError; + if (code) *code = static_cast(reinterpret_cast(result)); + return threadSuccess; +} + +int APS5_VABI _Thrd_detach_nid_postfix(void* thread) { + const auto detach = registeredPthreadDetach.load(std::memory_order_acquire); + if (!detach) throw std::runtime_error("_Thrd_detach: pthread_detach is not registered"); + return detach(thread) == 0 ? threadSuccess : threadError; +} + +int APS5_VABI _Thrd_id_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +[[noreturn]] void APS5_VABI _ZSt16_Throw_Cpp_errori_nid_postfix(int code) { + ThrowCppError_nid_no_patch(code); +} + +static std::atomic registeredThreadForwards{nullptr}; + +void RegisterThreadForwards_nid_no_patch(const ThreadForwards_nid_no_patch* forwards) { + registeredThreadForwards.store(forwards, std::memory_order_release); +} + +static const ThreadForwards_nid_no_patch& ThreadForwards(const char* function) { + const auto* forwards = registeredThreadForwards.load(std::memory_order_acquire); + if (!forwards) throw std::runtime_error(std::string(function) + ": libkernel threads are not registered"); + return *forwards; +} + +void APS5_VABI _Call_once_nid_postfix(std::int32_t* flag, void (APS5_VABI* function)(void)) { + ThreadForwards(__func__).once(flag, function); +} + +int APS5_VABI _Tss_create_nid_postfix(int* key, void (APS5_VABI* destructor)(void*)) { + return ThreadForwards(__func__).keyCreate(key, destructor); +} + +int APS5_VABI _Tss_delete_nid_postfix(int key) { + return ThreadForwards(__func__).keyDelete(key); +} + +void* APS5_VABI _Tss_get_nid_postfix(int key) { + return ThreadForwards(__func__).getSpecific(key); +} + +int APS5_VABI _Tss_set_nid_postfix(int key, void* value) { + return ThreadForwards(__func__).setSpecific(key, value); +} + +int APS5_VABI _Thrd_equal_nid_postfix(void* first, void* second) { + return ThreadForwards(__func__).equal(first, second); +} + +void* APS5_VABI _Thrd_current_nid_postfix(void) { + return ThreadForwards(__func__).self(); +} + +[[noreturn]] void APS5_VABI _Thrd_exit_nid_postfix(int code) { + ThreadForwards(__func__).exit(reinterpret_cast(static_cast(static_cast(code)))); + std::abort(); +} + +int APS5_VABI _Mtx_current_owns_nid_postfix(void**) { + return 1; +} + } diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index b782ef1549..c3499f8047 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -18,6 +19,8 @@ #include "prx/libc/include/GuestTime.hpp" #include "prx/libc/include/SonyCrt.hpp" +extern "C" char* tzname_nid_postfix[2]; + namespace { constexpr const char* WEEKDAY_NAMES[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; @@ -25,15 +28,41 @@ constexpr const char* MONTH_NAMES[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", constexpr std::size_t ASCTIME_SIZE = sizeof("Sun Jan-2147483648 -2147483648:-2147483648:-2147483648 -2147481748\n"); constexpr std::int64_t SECONDS_PER_DAY = 86400; constexpr std::int64_t DAYS_FROM_MARCH_TO_YEAR_END = 306; -#ifdef _WIN32 +#if defined(_WIN32) || defined(__APPLE__) constexpr std::int64_t CALENDAR_CYCLE_YEARS = 400; constexpr std::int64_t CALENDAR_CYCLE_SECONDS = 146097 * SECONDS_PER_DAY; +#ifdef _WIN32 +constexpr int HOST_ORDINARY_FIRST_YEAR = 70; +constexpr int HOST_ORDINARY_LAST_YEAR = 1100; +constexpr std::int64_t HOST_FIRST_SECOND = 0; constexpr std::int64_t HOST_LAST_SECOND = 32535215999; +#else +constexpr int HOST_ORDINARY_FIRST_YEAR = 0; +constexpr int HOST_ORDINARY_LAST_YEAR = std::numeric_limits::max(); +constexpr std::int64_t HOST_FIRST_SECOND = -2208988800; +constexpr std::int64_t HOST_LAST_SECOND = std::numeric_limits::max(); +#endif std::int64_t calendarCycles(std::int64_t value, std::int64_t cycle) { return value >= 0 ? value / cycle : -((-(value + 1)) / cycle + 1); } +#ifdef __APPLE__ +bool timeOfLocalSeconds(std::int64_t local, std::int64_t& time) { + if (local < std::numeric_limits::min() + SECONDS_PER_DAY) return false; + time = local; + for (int step = 0; step < 4; ++step) { + const std::time_t probe = static_cast(time); + std::tm fields{}; + if (localtime_r(&probe, &fields) == nullptr) return false; + const std::int64_t next = local - fields.tm_gmtoff; + if (next == time) return true; + time = next; + } + return true; +} +#endif + std::int64_t utcSeconds(const std::tm& time) { const std::int64_t years = calendarCycles(time.tm_mon, 12); const std::int64_t month = time.tm_mon - years * 12 + 1; @@ -132,6 +161,80 @@ extern "C" int64_t APS5_VABI libc_mktime_nid_postfix(GuestTm* timeptr); namespace { +bool setHostZone(const char* zone) { +#ifdef _WIN32 + return _putenv_s("TZ", zone == nullptr ? "" : zone) == 0; +#else + return zone == nullptr ? ::unsetenv("TZ") == 0 : ::setenv("TZ", zone, 1) == 0; +#endif +} + +const char* hostZoneFor(const char* guest) { +#ifdef _WIN32 + return guest != nullptr && guest[0] == 0 ? "UTC0" : guest; +#else + return guest == nullptr ? ":/etc/localtime" : guest; +#endif +} + +std::recursive_mutex& zoneMutex() { + static std::recursive_mutex mutex; + return mutex; +} + +char** HostZoneNames(); + +void applyGuestZone() { + static bool initialized = false; + static bool present = false; + static std::string current; + if (!initialized) { + const char* host = std::getenv("TZ"); + present = host != nullptr; + if (present) current = host; + initialized = true; + } + std::string guest; + const bool guestPresent = CopyGuestEnvironment_nid_no_patch("TZ", guest); + if (guestPresent == present && (!guestPresent || current == guest)) return; + if (!setHostZone(hostZoneFor(guestPresent ? guest.c_str() : nullptr))) throw std::runtime_error("tzset: the host rejected the guest TZ"); + present = guestPresent; + current = guest; +#ifdef _WIN32 + _tzset(); +#else + ::tzset(); +#endif + char** names = HostZoneNames(); + tzname_nid_postfix[0] = names[0]; + tzname_nid_postfix[1] = names[1]; +} + +int& zoneDepth() { + static thread_local int depth = 0; + return depth; +} + +class ZoneScope { +public: + ZoneScope() : lock(zoneMutex()) { + if (zoneDepth() == 0) applyGuestZone(); + ++zoneDepth(); + } + + ~ZoneScope() { --zoneDepth(); } + + ZoneScope(const ZoneScope&) = delete; + ZoneScope& operator=(const ZoneScope&) = delete; + +private: + std::lock_guard lock; +}; + +} // namespace + +namespace { + constexpr const char* WEEKDAY_FULL_NAMES[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; constexpr const char* MONTH_FULL_NAMES[] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; @@ -417,17 +520,29 @@ char** HostZoneNames() { extern "C" { char* tzname_nid_postfix[2] = {HostZoneNames()[0], HostZoneNames()[1]}; +int64_t timezone_nid_postfix = 0; +int daylight_nid_postfix = 0; GuestTm* APS5_VABI localtime_s_nid_postfix(const int64_t* timer, GuestTm* result); GuestTm* APS5_VABI gmtime_s_nid_postfix(const int64_t* timer, GuestTm* result); +int64_t APS5_VABI libc_time_nid_postfix(int64_t* timer); +double APS5_VABI libc_difftime_nid_postfix(int64_t time1, int64_t time0); std::size_t APS5_VABI wcsrtombs_nid_postfix(char* destination, const std::uint16_t** source, std::size_t capacity, void* state); std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const char** source, std::size_t capacity, void* state); int APS5_VABI ___mb_cur_max_nid_postfix(); void APS5_VABI tzset_nid_postfix(void) { + const ZoneScope zone; char** names = HostZoneNames(); tzname_nid_postfix[0] = names[0]; tzname_nid_postfix[1] = names[1]; + const int64_t now = libc_time_nid_postfix(nullptr); + GuestTm universal{}; + GuestTm local{}; + gmtime_s_nid_postfix(&now, &universal); + localtime_s_nid_postfix(&now, &local); + daylight_nid_postfix = local.tm_isdst >= 0 ? 1 : 0; + timezone_nid_postfix = static_cast(libc_difftime_nid_postfix(libc_mktime_nid_postfix(&universal), libc_mktime_nid_postfix(&local))); } int64_t APS5_VABI libc_time_nid_postfix(int64_t* timer) { @@ -478,11 +593,13 @@ GuestTm* APS5_VABI localtime_nid_postfix(const int64_t* timer) { GuestTm* APS5_VABI localtime_s_nid_postfix(const int64_t* timer, GuestTm* result) { if (timer == nullptr || result == nullptr) return nullptr; + const ZoneScope zone; std::tm host{}; const int callerError = errno; #ifdef _WIN32 const std::int64_t cycles = *timer >= 0 && *timer <= HOST_LAST_SECOND ? 0 : calendarCycles(*timer, CALENDAR_CYCLE_SECONDS); - const std::time_t t = static_cast(*timer - cycles * CALENDAR_CYCLE_SECONDS); + const std::int64_t offset = *timer % CALENDAR_CYCLE_SECONDS; + const std::time_t t = static_cast(cycles == 0 ? *timer : offset < 0 ? offset + CALENDAR_CYCLE_SECONDS : offset); if (localtime_s(&host, &t) != 0) { errno = callerError; return nullptr; @@ -527,15 +644,27 @@ GuestTm* APS5_VABI gmtime_nid_postfix(const int64_t* timer) { } int64_t APS5_VABI libc_mktime_nid_postfix(GuestTm* timeptr) { + const ZoneScope zone; std::tm host = toHostTm(*timeptr); host.tm_wday = -1; int64_t moved = 0; -#ifdef _WIN32 - const bool ordinary = host.tm_year > 70 && host.tm_year < 1100 && host.tm_mon >= 0 && host.tm_mon < 12 && host.tm_mday >= 1 && host.tm_mday <= 31 - && host.tm_hour >= 0 && host.tm_hour < 24 && host.tm_min >= 0 && host.tm_min < 60 && host.tm_sec >= 0 && host.tm_sec <= 60; - const std::int64_t approximate = ordinary ? SECONDS_PER_DAY : utcSeconds(host); - if (approximate < SECONDS_PER_DAY || approximate > HOST_LAST_SECOND - SECONDS_PER_DAY) { - moved = calendarCycles(approximate - SECONDS_PER_DAY, CALENDAR_CYCLE_SECONDS) * CALENDAR_CYCLE_SECONDS; +#if defined(_WIN32) || defined(__APPLE__) + const bool ordinary = host.tm_year > HOST_ORDINARY_FIRST_YEAR && host.tm_year < HOST_ORDINARY_LAST_YEAR && host.tm_mon >= 0 && host.tm_mon < 12 + && host.tm_mday >= 1 && host.tm_mday <= 31 && host.tm_hour >= 0 && host.tm_hour < 24 && host.tm_min >= 0 && host.tm_min < 60 + && host.tm_sec >= 0 && host.tm_sec <= 60; + constexpr std::int64_t firstOrdinary = HOST_FIRST_SECOND + SECONDS_PER_DAY; + const std::int64_t approximate = ordinary ? firstOrdinary : utcSeconds(host); +#ifdef __APPLE__ + if (approximate < firstOrdinary) { + std::int64_t time = 0; + GuestTm normalized{}; + if (!timeOfLocalSeconds(approximate, time) || localtime_s_nid_postfix(&time, &normalized) == nullptr) return -1; + *timeptr = normalized; + return time; + } +#endif + if (approximate < firstOrdinary || approximate > HOST_LAST_SECOND - SECONDS_PER_DAY) { + moved = calendarCycles(approximate - firstOrdinary, CALENDAR_CYCLE_SECONDS) * CALENDAR_CYCLE_SECONDS; GuestTm window{}; if (!utcCalendarFields(approximate - moved, window)) return -1; window.tm_isdst = host.tm_isdst; @@ -557,6 +686,7 @@ int64_t APS5_VABI mktime_nid_postfix(GuestTm* timeptr) { } size_t APS5_VABI libc_strftime_nid_postfix(char* str, size_t count, const char* format, const GuestTm* timeptr) { + const ZoneScope zone; tzset_nid_postfix(); return formatGuestTime(str, count, format, *timeptr); } diff --git a/core/libs/prx/libc/src/TimeExtras.cpp b/core/libs/prx/libc/src/TimeExtras.cpp new file mode 100644 index 0000000000..ee2a225b25 --- /dev/null +++ b/core/libs/prx/libc/src/TimeExtras.cpp @@ -0,0 +1,93 @@ +#include + +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestTime.hpp" + +namespace { + +struct Xtime { + std::int64_t sec; + std::int64_t nsec; +}; + +constexpr int timeUtc = 1; +constexpr std::int64_t nanosPerSecond = 1000000000; +constexpr std::int64_t secondsPerDay = 86400; + +std::int64_t floorDivide(std::int64_t value, std::int64_t divisor) { + return value >= 0 ? value / divisor : -((-(value + 1)) / divisor + 1); +} + +std::int64_t daysFromCivil(std::int64_t year, std::int64_t month, std::int64_t day) { + year -= month <= 2 ? 1 : 0; + const std::int64_t era = floorDivide(year, 400); + const std::int64_t yearOfEra = year - era * 400; + const std::int64_t dayOfYear = (153 * (month > 2 ? month - 3 : month + 9) + 2) / 5 + day - 1; + const std::int64_t dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear; + return era * 146097 + dayOfEra - 719468; +} + +std::int64_t wrappingAdd(std::int64_t left, std::int64_t right) { + return static_cast(static_cast(left) + static_cast(right)); +} + +} // namespace + +extern "C" { + +GuestTm* APS5_VABI gmtime_s_nid_postfix(const std::int64_t* timer, GuestTm* result); +GuestTm* APS5_VABI localtime_s_nid_postfix(const std::int64_t* timer, GuestTm* result); +std::int64_t APS5_VABI _Xtime_get_ticks_nid_postfix(); + +GuestTm* APS5_VABI gmtime_r_nid_postfix(const std::int64_t* timer, GuestTm* result) { + return gmtime_s_nid_postfix(timer, result); +} + +GuestTm* APS5_VABI localtime_r_nid_postfix(const std::int64_t* timer, GuestTm* result) { + return localtime_s_nid_postfix(timer, result); +} + +std::int64_t APS5_VABI timegm_nid_postfix(GuestTm* time) { + const std::int64_t years = floorDivide(time->tm_mon, 12); + const std::int64_t month = time->tm_mon - years * 12 + 1; + const std::int64_t year = std::int64_t{time->tm_year} + 1900 + years; + const std::int64_t days = daysFromCivil(year, month, 1) + time->tm_mday - 1; + const std::int64_t seconds = days * secondsPerDay + std::int64_t{time->tm_hour} * 3600 + std::int64_t{time->tm_min} * 60 + time->tm_sec; + GuestTm normalized{}; + if (gmtime_s_nid_postfix(&seconds, &normalized) == nullptr) return -1; + *time = normalized; + return seconds; +} + +int APS5_VABI xtime_get_nid_postfix(Xtime* time, int base) { + if (time == nullptr || base != timeUtc) return 0; + const std::int64_t micros = _Xtime_get_ticks_nid_postfix(); + time->sec = floorDivide(micros, 1000000); + time->nsec = (micros - time->sec * 1000000) * 1000; + return timeUtc; +} + +Xtime APS5_VABI _Xtime_to_ts_nid_postfix(const Xtime* time) { + return *time; +} + +Xtime APS5_VABI _Xtime_diff_to_ts_nid_postfix(const Xtime* target) { + Xtime now{}; + xtime_get_nid_postfix(&now, timeUtc); + std::int64_t seconds = wrappingAdd(target->sec, target->nsec / nanosPerSecond); + std::int64_t nanos = target->nsec % nanosPerSecond; + if (nanos < 0) { + nanos += nanosPerSecond; + seconds = wrappingAdd(seconds, -1); + } + seconds = wrappingAdd(seconds, -now.sec); + if (nanos < now.nsec) { + nanos += nanosPerSecond; + seconds = wrappingAdd(seconds, -1); + } + nanos -= now.nsec; + if (seconds < 0 || (seconds == 0 && nanos == 0)) return {0, 0}; + return {seconds, nanos}; +} + +} diff --git a/core/libs/prx/libc/src/VectorAlgorithms.cpp b/core/libs/prx/libc/src/VectorAlgorithms.cpp new file mode 100644 index 0000000000..169c0a23d8 --- /dev/null +++ b/core/libs/prx/libc/src/VectorAlgorithms.cpp @@ -0,0 +1,161 @@ +#include +#include +#include +#include + +#include "prx/libc/include/General.hpp" + +namespace { + +struct MinMaxPointers { + const void* minimum; + const void* maximum; +}; + +template +TValue Read(const unsigned char* address) { + TValue value; + std::memcpy(&value, address, sizeof(value)); + return value; +} + +template +const void* Find(const void* first, const void* last, TValue value) { + auto* current = static_cast(first); + auto* end = static_cast(last); + for (; current != end; current += sizeof(TValue)) { + if (Read(current) == value) return current; + } + return end; +} + +constexpr std::uintptr_t UnsizedBlock = 32; + +template +const void* FindUnsized(const void* first, TValue value) { + auto* start = static_cast(first); + auto* block = start - (reinterpret_cast(start) & (UnsizedBlock - 1)); + for (auto* current = start; current != block + UnsizedBlock; current += sizeof(TValue)) { + if (Read(current) == value) return current; + } + auto* current = block; + while (Read(current) != value) current += sizeof(TValue); + return current; +} + +template +std::size_t Count(const void* first, const void* last, TValue value) { + auto* current = static_cast(first); + auto* end = static_cast(last); + std::size_t count = 0; + for (; current != end; current += sizeof(TValue)) { + if (Read(current) == value) ++count; + } + return count; +} + +template +MinMaxPointers MinMax(const void* first, const void* last, bool wantMinimum, bool wantMaximum) { + auto* current = static_cast(first); + auto* end = static_cast(last); + MinMaxPointers result{first, first}; + if (current == end) return result; + TValue minimum = Read(current); + TValue maximum = minimum; + for (current += sizeof(TValue); current != end; current += sizeof(TValue)) { + const TValue value = Read(current); + if (wantMinimum && value < minimum) { + minimum = value; + result.minimum = current; + } + if (wantMaximum && (wantMinimum ? !(value < maximum) : maximum < value)) { + maximum = value; + result.maximum = current; + } + } + return result; +} + +template +MinMaxPointers MinMaxBySign(const void* first, const void* last, bool isSigned, bool wantMinimum, bool wantMaximum) { + using TSigned = std::make_signed_t; + return isSigned ? MinMax(first, last, wantMinimum, wantMaximum) : MinMax(first, last, wantMinimum, wantMaximum); +} + +template +void Reverse(void* first, void* last) { + auto* low = static_cast(first); + auto* high = static_cast(last); + unsigned char buffer[Size]; + while (high - low >= static_cast(2 * Size)) { + high -= Size; + std::memcpy(buffer, low, Size); + std::memcpy(low, high, Size); + std::memcpy(high, buffer, Size); + low += Size; + } +} + +template +void ReverseCopy(const void* first, const void* last, void* destination) { + auto* begin = static_cast(first); + auto* end = static_cast(last); + auto* out = static_cast(destination); + while (end != begin) { + end -= Size; + std::memcpy(out, end, Size); + out += Size; + } +} + +} + +extern "C" { + +const void* APS5_VABI __std_find_trivial_1_nid_postfix(const void* first, const void* last, std::uint8_t value) { return Find(first, last, value); } +const void* APS5_VABI __std_find_trivial_2_nid_postfix(const void* first, const void* last, std::uint16_t value) { return Find(first, last, value); } +const void* APS5_VABI __std_find_trivial_4_nid_postfix(const void* first, const void* last, std::uint32_t value) { return Find(first, last, value); } +const void* APS5_VABI __std_find_trivial_8_nid_postfix(const void* first, const void* last, std::uint64_t value) { return Find(first, last, value); } +const void* APS5_VABI __std_find_trivial_unsized_1_nid_postfix(const void* first, std::uint8_t value) { return FindUnsized(first, value); } +const void* APS5_VABI __std_find_trivial_unsized_2_nid_postfix(const void* first, std::uint16_t value) { return FindUnsized(first, value); } +const void* APS5_VABI __std_find_trivial_unsized_4_nid_postfix(const void* first, std::uint32_t value) { return FindUnsized(first, value); } +const void* APS5_VABI __std_find_trivial_unsized_8_nid_postfix(const void* first, std::uint64_t value) { return FindUnsized(first, value); } +std::size_t APS5_VABI __std_count_trivial_1_nid_postfix(const void* first, const void* last, std::uint8_t value) { return Count(first, last, value); } +std::size_t APS5_VABI __std_count_trivial_2_nid_postfix(const void* first, const void* last, std::uint16_t value) { return Count(first, last, value); } +std::size_t APS5_VABI __std_count_trivial_4_nid_postfix(const void* first, const void* last, std::uint32_t value) { return Count(first, last, value); } +std::size_t APS5_VABI __std_count_trivial_8_nid_postfix(const void* first, const void* last, std::uint64_t value) { return Count(first, last, value); } + +const void* APS5_VABI __std_min_element_1_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign(first, last, isSigned, true, false).minimum; } +const void* APS5_VABI __std_min_element_2_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign(first, last, isSigned, true, false).minimum; } +const void* APS5_VABI __std_min_element_4_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign(first, last, isSigned, true, false).minimum; } +const void* APS5_VABI __std_min_element_8_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign(first, last, isSigned, true, false).minimum; } +const void* APS5_VABI __std_max_element_1_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign(first, last, isSigned, false, true).maximum; } +const void* APS5_VABI __std_max_element_2_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign(first, last, isSigned, false, true).maximum; } +const void* APS5_VABI __std_max_element_4_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign(first, last, isSigned, false, true).maximum; } +const void* APS5_VABI __std_max_element_8_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign(first, last, isSigned, false, true).maximum; } +MinMaxPointers APS5_VABI __std_minmax_element_1_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign(first, last, isSigned, true, true); } +MinMaxPointers APS5_VABI __std_minmax_element_2_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign(first, last, isSigned, true, true); } +MinMaxPointers APS5_VABI __std_minmax_element_4_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign(first, last, isSigned, true, true); } +MinMaxPointers APS5_VABI __std_minmax_element_8_nid_postfix(const void* first, const void* last, bool isSigned) { return MinMaxBySign(first, last, isSigned, true, true); } + +void APS5_VABI __std_reverse_trivially_swappable_1_nid_postfix(void* first, void* last) { Reverse<1>(first, last); } +void APS5_VABI __std_reverse_trivially_swappable_2_nid_postfix(void* first, void* last) { Reverse<2>(first, last); } +void APS5_VABI __std_reverse_trivially_swappable_4_nid_postfix(void* first, void* last) { Reverse<4>(first, last); } +void APS5_VABI __std_reverse_trivially_swappable_8_nid_postfix(void* first, void* last) { Reverse<8>(first, last); } +void APS5_VABI __std_reverse_copy_trivially_copyable_1_nid_postfix(const void* first, const void* last, void* destination) { ReverseCopy<1>(first, last, destination); } +void APS5_VABI __std_reverse_copy_trivially_copyable_2_nid_postfix(const void* first, const void* last, void* destination) { ReverseCopy<2>(first, last, destination); } +void APS5_VABI __std_reverse_copy_trivially_copyable_4_nid_postfix(const void* first, const void* last, void* destination) { ReverseCopy<4>(first, last, destination); } +void APS5_VABI __std_reverse_copy_trivially_copyable_8_nid_postfix(const void* first, const void* last, void* destination) { ReverseCopy<8>(first, last, destination); } + +void APS5_VABI __std_swap_ranges_trivially_swappable_noalias_nid_postfix(void* first1, void* last1, void* first2) { + auto* left = static_cast(first1); + auto* right = static_cast(first2); + const std::size_t length = static_cast(static_cast(last1) - left); + for (std::size_t i = 0; i < length; ++i) { + const unsigned char byte = left[i]; + left[i] = right[i]; + right[i] = byte; + } +} + +} 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..e790d1a2db 100644 --- a/core/libs/prx/libc/src/exception/Runtime.cpp +++ b/core/libs/prx/libc/src/exception/Runtime.cpp @@ -117,7 +117,7 @@ void Release(void* object) { } } -void Cleanup(_Unwind_Reason_Code reason, _Unwind_Exception* exception) { +void APS5_VABI Cleanup(_Unwind_Reason_Code reason, _Unwind_Exception* exception) { if (reason != _URC_FOREIGN_EXCEPTION_CAUGHT) Terminate(); auto* header = FromUnwind(exception); if (exception->exception_class == DependentClass) { @@ -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..31feeb3d8e 100644 --- a/core/libs/prx/libc/src/exception/StandardExceptions.cpp +++ b/core/libs/prx/libc/src/exception/StandardExceptions.cpp @@ -92,6 +92,8 @@ const char* PlainWhat(const ExceptionObject* object) { if (std::strcmp(name, "St20bad_array_new_length") == 0) return "std::bad_array_new_length"; if (std::strcmp(name, "St17bad_function_call") == 0) return "std::bad_function_call"; if (std::strcmp(name, "St12bad_weak_ptr") == 0) return "bad_weak_ptr"; + if (std::strcmp(name, "St18bad_variant_access") == 0) return "bad_variant_access"; + if (std::strcmp(name, "St19bad_optional_access") == 0) return "bad_optional_access"; return name; } const char* MessageWhat(const ExceptionObject* object) { return object->message ? object->message : ""; } @@ -333,6 +335,18 @@ void APS5_VABI _ZNSt12bad_weak_ptrD2Ev_nid_postfix(LibcException::ExceptionObjec void APS5_VABI _ZNSt12bad_weak_ptrD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeletePlain(self); } const char* APS5_VABI _ZNKSt12bad_weak_ptr4whatEv_nid_postfix(const LibcException::ExceptionObject* self) { return LibcException::PlainWhat(self); } +LibcException::TypeRecord _ZTISt18bad_variant_access_nid_postfix {LibcException::SingleTypeVtable + 2, "St18bad_variant_access", &_ZTISt9exception_nid_postfix}; +LibcException::ExceptionVtable _ZTVSt18bad_variant_access_nid_postfix {0, &_ZTISt18bad_variant_access_nid_postfix, LibcException::GuestDestroyPlain_nid_no_patch, LibcException::GuestDeletePlain_nid_no_patch, LibcException::GuestPlainWhat_nid_no_patch}; + +LibcException::TypeRecord _ZTISt19bad_optional_access_nid_postfix {LibcException::SingleTypeVtable + 2, "St19bad_optional_access", &_ZTISt9exception_nid_postfix}; +LibcException::ExceptionVtable _ZTVSt19bad_optional_access_nid_postfix {0, &_ZTISt19bad_optional_access_nid_postfix, LibcException::GuestDestroyPlain_nid_no_patch, LibcException::GuestDeletePlain_nid_no_patch, LibcException::GuestPlainWhat_nid_no_patch}; +void APS5_VABI _ZNSt19bad_optional_accessD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } +void APS5_VABI _ZNSt19bad_optional_accessD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } +void APS5_VABI _ZNSt19bad_optional_accessD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeletePlain(self); } +const char* APS5_VABI _ZNKSt19bad_optional_access4whatEv_nid_postfix(const LibcException::ExceptionObject* self) { return LibcException::PlainWhat(self); } +void APS5_VABI _ZNSt19bad_optional_accessC1Ev_nid_postfix(LibcException::ExceptionObject* self) { self->vtable = &_ZTVSt19bad_optional_access_nid_postfix.destroy; } +void APS5_VABI _ZNSt19bad_optional_accessC2Ev_nid_postfix(LibcException::ExceptionObject* self) { self->vtable = &_ZTVSt19bad_optional_access_nid_postfix.destroy; } + LibcException::TypeRecord _ZTISt11logic_error_nid_postfix {LibcException::SingleTypeVtable + 2, "St11logic_error", &_ZTISt9exception_nid_postfix}; LibcException::ExceptionVtable _ZTVSt11logic_error_nid_postfix {0, &_ZTISt11logic_error_nid_postfix, LibcException::GuestDestroyMessage_nid_no_patch, LibcException::GuestDeleteMessage_nid_no_patch, LibcException::GuestMessageWhat_nid_no_patch}; void APS5_VABI _ZNSt11logic_errorD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } @@ -344,6 +358,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,10 +477,13 @@ 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); } [[noreturn]] void APS5_VABI _ZSt19_Throw_bad_weak_ptrv_nid_postfix() { LibcException::ThrowPlain(_ZTVSt12bad_weak_ptr_nid_postfix); } +[[noreturn]] void APS5_VABI _ZSt26__throw_bad_variant_accessv_nid_postfix() { LibcException::ThrowPlain(_ZTVSt18bad_variant_access_nid_postfix); } +[[noreturn]] void APS5_VABI _ZSt26_Throw_bad_optional_accessv_nid_postfix() { LibcException::ThrowPlain(_ZTVSt19bad_optional_access_nid_postfix); } [[noreturn]] void APS5_VABI _ZSt13_Xregex_errorNSt15regex_constants10error_typeE_nid_postfix(std::regex_constants::error_type code) { struct RegexObject { LibcException::ExceptionObject base; std::regex_constants::error_type code; }; auto* object = static_cast(__cxa_allocate_exception_nid_postfix(sizeof(RegexObject))); diff --git a/core/libs/prx/libc/src/exception/Unwind.cpp b/core/libs/prx/libc/src/exception/Unwind.cpp index 6aac4ddddb..94226226e0 100644 --- a/core/libs/prx/libc/src/exception/Unwind.cpp +++ b/core/libs/prx/libc/src/exception/Unwind.cpp @@ -15,6 +15,14 @@ #include #include #endif +#if defined(__linux__) && defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 35)) +#define LIBC_UNWIND_FIND_OBJECT 1 +#include +#include +#include +#include +#include +#endif #ifdef __APPLE__ #include #include @@ -92,16 +100,26 @@ const std::uintptr_t* UnwindCursorSlots_nid_no_patch() { extern "C" _Unwind_Reason_Code __gxx_personality_v0(int, _Unwind_Action, std::uint64_t, _Unwind_Exception*, _Unwind_Context*); extern "C" _Unwind_Reason_Code __gcc_personality_v0(int, _Unwind_Action, std::uint64_t, _Unwind_Exception*, _Unwind_Context*); +#ifdef _WIN32 +extern "C" void _Unwind_DeleteException(_Unwind_Exception*); +#endif namespace LibcUnwind { -_Unwind_Reason_Code CallPersonality(Word personality, _Unwind_Action actions, _Unwind_Exception* exception, _Unwind_Context* context) { #ifdef _WIN32 - const auto* code = reinterpret_cast(personality); +static Word ResolveImport(Word address) { + const auto* code = reinterpret_cast(address); if (code[0] == 0xff && code[1] == 0x25) { std::int32_t displacement; - std::memcpy(&displacement, code + 2, 4); - std::memcpy(&personality, code + 6 + displacement, sizeof(personality)); + std::memcpy(&displacement, code + 2, sizeof(displacement)); + std::memcpy(&address, code + 6 + displacement, sizeof(address)); } + return address; +} +#endif + +_Unwind_Reason_Code CallPersonality(Word personality, _Unwind_Action actions, _Unwind_Exception* exception, _Unwind_Context* context) { +#ifdef _WIN32 + personality = ResolveImport(personality); #endif if (personality == reinterpret_cast(__gxx_personality_v0_nid_postfix) || personality == reinterpret_cast(__gxx_personality_v0)) return __gxx_personality_v0_nid_postfix(1, actions, exception->exception_class, exception, context); @@ -110,6 +128,8 @@ _Unwind_Reason_Code CallPersonality(Word personality, _Unwind_Action actions, _U return (actions & _UA_SEARCH_PHASE) ? _URC_FATAL_PHASE1_ERROR : _URC_FATAL_PHASE2_ERROR; } #if defined(__linux__) || defined(__APPLE__) +void SearchHeader(const Byte* header, Lookup& query); + int FindFrame(dl_phdr_info* info, std::size_t, void* argument) { auto& query = *static_cast(argument); const Byte* header = nullptr; @@ -123,10 +143,15 @@ int FindFrame(dl_phdr_info* info, std::size_t, void* argument) { if (ph.p_type == PT_LOAD && (ph.p_flags & PF_W)) query.data = start; } if (!contains || !header || header[0] != 1) return 0; + SearchHeader(header, query); + return 1; +} + +void SearchHeader(const Byte* header, Lookup& query) { const Byte* p = header + 4; Encoded(p, header[1], Word(header)); Word count = Encoded(p, header[2]); - if (header[3] == 255) return 1; + if (header[3] == 255) return; unsigned width = EncodingSize(header[3]); const Byte* table = p; Word lo = 0, hi = count; @@ -140,9 +165,40 @@ int FindFrame(dl_phdr_info* info, std::size_t, void* argument) { p = table + (lo - 1) * width * 2 + width; query.fde = reinterpret_cast(Encoded(p, header[3], Word(header))); } - return 1; } +#ifdef LIBC_UNWIND_FIND_OBJECT +bool FindFrameFast(Lookup& query) { + dl_find_object object {}; + if (_dl_find_object(reinterpret_cast(query.pc), &object) != 0 || object.dlfo_eh_frame == nullptr) return false; + const auto* header = static_cast(object.dlfo_eh_frame); + if (header[0] != 1) return false; + static std::mutex mutex; + static std::map, std::pair> bases; + const std::tuple key {header, static_cast(object.dlfo_map_start), static_cast(object.dlfo_map_end)}; + { + std::lock_guard lock(mutex); + auto found = bases.find(key); + if (found != bases.end()) { + query.text = found->second.first; + query.data = found->second.second; + SearchHeader(header, query); + return true; + } + } + Lookup probe {query.pc}; + dl_iterate_phdr(FindFrame, &probe); + { + std::lock_guard lock(mutex); + const auto found = bases.emplace(key, std::pair {probe.text, probe.data}).first; + query.text = found->second.first; + query.data = found->second.second; + } + SearchHeader(header, query); + return true; +} +#endif + #endif bool DecodeCandidate(_Unwind_Context& context, Frame& frame, const Lookup& query) { @@ -208,14 +264,20 @@ const IMAGE_NT_HEADERS64* ImageHeaders(Word pc, const Byte*& base) { return nt->Signature == IMAGE_NT_SIGNATURE ? nt : nullptr; } -bool VirtualUnwind(const _Unwind_Context& context, _Unwind_Context& caller) { +bool VirtualUnwind(_Unwind_Context& context, Frame& frame) { const Word pc = context.registers[16]; const Byte* base = nullptr; const auto* nt = ImageHeaders(pc, base); if (!nt) return false; const auto* sections = IMAGE_FIRST_SECTION(nt); - for (unsigned i = 0; i < nt->FileHeader.NumberOfSections; ++i) - if (std::memcmp(sections[i].Name, ".ehmeta", 8) == 0 || std::memcmp(sections[i].Name, ".ehfram", 8) == 0) return false; + const PEXCEPTION_ROUTINE* personalities = nullptr; + bool dwarf = false; + for (unsigned i = 0; i < nt->FileHeader.NumberOfSections; ++i) { + if (std::memcmp(sections[i].Name, ".ehmeta", 8) == 0 || std::memcmp(sections[i].Name, ".ehfram", 8) == 0) dwarf = true; + if (std::memcmp(sections[i].Name, ".sehgcc", 8) == 0 && sections[i].Misc.VirtualSize >= 2 * sizeof(PEXCEPTION_ROUTINE)) + personalities = reinterpret_cast(base + sections[i].VirtualAddress); + } + if (dwarf && !personalities) return false; DWORD64 imageBase = 0; auto* function = RtlLookupFunctionEntry(pc, &imageBase, nullptr); if (!function) return false; @@ -226,11 +288,27 @@ bool VirtualUnwind(const _Unwind_Context& context, _Unwind_Context& caller) { for (unsigned i = 0; i < 16; ++i) std::memcpy(&state.FltSave.XmmRegisters[i], context.vectorRegisters[i], 16); void* handlerData = nullptr; DWORD64 establisher = 0; - if (RtlVirtualUnwind(UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER, imageBase, pc, function, &state, &handlerData, &establisher, nullptr)) return false; + const auto handler = RtlVirtualUnwind(UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER, imageBase, pc, function, &state, &handlerData, &establisher, nullptr); + frame.start = imageBase + function->BeginAddress; + if (handler) { + if (!personalities) return false; + const Word resolved = ResolveImport(reinterpret_cast(handler)); + if (resolved == ResolveImport(reinterpret_cast(personalities[0]))) frame.personality = reinterpret_cast(__gxx_personality_v0_nid_postfix); + else if (resolved == ResolveImport(reinterpret_cast(personalities[1]))) frame.personality = reinterpret_cast(__gcc_personality_v0_nid_postfix); + else return false; + frame.lsda = reinterpret_cast(handlerData); + } + auto& caller = frame.caller; caller = context; for (unsigned i = 0; i < 17; ++i) caller.registers[i] = *registers[i]; for (unsigned i = 0; i < 16; ++i) std::memcpy(caller.vectorRegisters[i], &state.FltSave.XmmRegisters[i], 16); caller.signalFrame = false; + context.cfa = caller.registers[7]; + context.region = frame.start; + context.lsda = frame.lsda; + context.personality = frame.personality; + context.textBase = imageBase; + context.dataBase = 0; return true; } #endif @@ -238,7 +316,10 @@ bool VirtualUnwind(const _Unwind_Context& context, _Unwind_Context& caller) { bool DecodeFrame(_Unwind_Context& context, Frame& frame) { Lookup query {context.registers[16] - !context.signalFrame}; #if defined(__linux__) || defined(__APPLE__) - dl_iterate_phdr(FindFrame, &query); +#ifdef LIBC_UNWIND_FIND_OBJECT + if (!FindFrameFast(query)) +#endif + dl_iterate_phdr(FindFrame, &query); #ifdef __APPLE__ if (!query.fde) { Dl_info image {}; @@ -362,10 +443,8 @@ bool GetRules(_Unwind_Context& context, Frame& frame, Rules& rules) { if (!DecodeFrame(context, frame)) { #ifdef _WIN32 frame = {}; - if (!VirtualUnwind(context, frame.caller)) return false; + if (!VirtualUnwind(context, frame)) return false; frame.foreign = true; - context.cfa = frame.caller.registers[7]; - context.region = context.lsda = context.personality = context.textBase = context.dataBase = 0; return true; #else return false; @@ -520,6 +599,12 @@ _Unwind_Reason_Code APS5_VABI _Unwind_Resume_or_Rethrow_nid_postfix(_Unwind_Exce } void APS5_VABI _Unwind_DeleteException_nid_postfix(_Unwind_Exception* exception) { +#ifdef _WIN32 + if (exception && LibcException::Gnu(exception->exception_class)) { + _Unwind_DeleteException(exception); + return; + } +#endif if (exception && exception->exception_cleanup) exception->exception_cleanup(_URC_FOREIGN_EXCEPTION_CAUGHT, exception); } diff --git a/core/libs/prx/libc/src/specifics/windows/CallTrace.cpp b/core/libs/prx/libc/src/specifics/windows/CallTrace.cpp deleted file mode 100644 index 7e30971457..0000000000 --- a/core/libs/prx/libc/src/specifics/windows/CallTrace.cpp +++ /dev/null @@ -1,202 +0,0 @@ -#ifdef _WIN32 -#include -#include "prx/libc/include/CallTrace.hpp" -#include "prx/libc/include/general/AsmMacros.hpp" -#include "prx/libc/include/general/VabiMacros.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -extern "C" void CallTraceEnter_nid_no_patch(); - -namespace { - -struct ImportRecord { - void* target; - std::string name; - std::string library; - mutable std::atomic calls{0}; -}; - -struct RingSlot { - std::atomic sequence{0}; - std::atomic thread{0}; - std::atomic returnAddress{0}; - std::atomic record{nullptr}; -}; - -constexpr std::size_t RingCapacity = 65536; -constexpr std::size_t ThunkBytes = 24; -constexpr std::size_t ThunkAreaBytes = 1024 * 1024; - -std::mutex g_mutex; -std::deque g_records; -std::unordered_map g_thunkByTarget; -std::unique_ptr g_ring; -std::atomic g_sequence{0}; -std::uint8_t* g_writable = nullptr; -std::uint8_t* g_executable = nullptr; -std::size_t g_thunks = 0; - -bool Selected(std::string_view library) { - const char* value = std::getenv("APS5_TRACE_CALLS"); - if (value == nullptr || *value == '\0') return false; - std::string_view rest(value); - if (library.ends_with(".prx")) library.remove_suffix(4); - while (!rest.empty()) { - const auto comma = rest.find(','); - auto token = rest.substr(0, comma); - rest = comma == std::string_view::npos ? std::string_view{} : rest.substr(comma + 1); - if (token.ends_with(".prx")) token.remove_suffix(4); - if (token == "all" || token == library) return true; - } - return false; -} - -void MapThunkArea() { - const HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, static_cast(ThunkAreaBytes), nullptr); - if (section == nullptr) throw std::system_error(static_cast(GetLastError()), std::system_category(), "call trace: CreateFileMappingW"); - g_writable = static_cast(MapViewOfFile(section, FILE_MAP_WRITE, 0, 0, ThunkAreaBytes)); - g_executable = static_cast(MapViewOfFile(section, FILE_MAP_READ | FILE_MAP_EXECUTE, 0, 0, ThunkAreaBytes)); - const DWORD code = GetLastError(); - CloseHandle(section); - if (g_writable == nullptr || g_executable == nullptr) throw std::system_error(static_cast(code), std::system_category(), "call trace: MapViewOfFile"); -} - -void* WriteThunk(const ImportRecord& record) { - if ((g_thunks + 1) * ThunkBytes > ThunkAreaBytes) throw std::runtime_error("call trace: thunk area exhausted"); - const std::size_t offset = g_thunks * ThunkBytes; - const std::uint8_t prefix[] = {0x49, 0xbb}; - const std::uint8_t jump[] = {0xff, 0x25, 0, 0, 0, 0}; - const auto recordAddress = reinterpret_cast(&record); - const auto handler = reinterpret_cast(&CallTraceEnter_nid_no_patch); - std::uint8_t* thunk = g_writable + offset; - std::memcpy(thunk, prefix, sizeof(prefix)); - std::memcpy(thunk + 2, &recordAddress, sizeof(recordAddress)); - std::memcpy(thunk + 10, jump, sizeof(jump)); - std::memcpy(thunk + 16, &handler, sizeof(handler)); - FlushInstructionCache(GetCurrentProcess(), g_executable + offset, ThunkBytes); - ++g_thunks; - return g_executable + offset; -} - -} - -static_assert(offsetof(ImportRecord, target) == 0); - -extern "C" { - -void APS5_VABI CallTraceRecord_nid_no_patch(const void* record, std::uint64_t returnAddress) { - static_cast(record)->calls.fetch_add(1, std::memory_order_relaxed); - const std::uint64_t sequence = g_sequence.fetch_add(1, std::memory_order_relaxed) + 1; - RingSlot& slot = g_ring[sequence % RingCapacity]; - slot.sequence.store(0, std::memory_order_release); - slot.thread.store(GetCurrentThreadId(), std::memory_order_relaxed); - slot.returnAddress.store(returnAddress, std::memory_order_relaxed); - slot.record.store(static_cast(record), std::memory_order_relaxed); - slot.sequence.store(sequence, std::memory_order_release); -} - -void* TraceImport_nid_no_patch(void* target, const char* name, const char* library) { - if (target == nullptr || name == nullptr || library == nullptr || !Selected(library)) return target; - std::lock_guard lock(g_mutex); - if (g_ring == nullptr) { - MapThunkArea(); - g_ring = std::make_unique(RingCapacity); - } - if (const auto existing = g_thunkByTarget.find(target); existing != g_thunkByTarget.end()) return existing->second; - g_records.emplace_back(target, name, library); - void* thunk = WriteThunk(g_records.back()); - g_thunkByTarget.emplace(target, thunk); - return thunk; -} - -std::size_t CopyCallTrace_nid_no_patch(CallTraceEntry* entries, std::size_t capacity) { - if (entries == nullptr || capacity == 0 || g_ring == nullptr) return 0; - const std::uint64_t last = g_sequence.load(std::memory_order_acquire); - const std::uint64_t span = std::min({last, capacity, RingCapacity}); - std::size_t count = 0; - for (std::uint64_t sequence = last - span + 1; sequence <= last; ++sequence) { - const RingSlot& slot = g_ring[sequence % RingCapacity]; - if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; - const auto* record = slot.record.load(std::memory_order_relaxed); - const CallTraceEntry entry{sequence, slot.thread.load(std::memory_order_relaxed), slot.returnAddress.load(std::memory_order_relaxed), record->name.c_str(), record->library.c_str()}; - if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; - entries[count++] = entry; - } - return count; -} - -std::size_t CopyCallCounts_nid_no_patch(CallTraceCount* entries, std::size_t capacity, std::uint64_t* total) { - if (total != nullptr) *total = g_sequence.load(std::memory_order_acquire); - if (entries == nullptr || capacity == 0) return 0; - std::unique_lock lock(g_mutex, std::try_to_lock); - if (!lock.owns_lock()) return 0; - std::size_t count = 0; - for (const auto& record : g_records) { - const std::uint64_t calls = record.calls.load(std::memory_order_relaxed); - if (calls == 0 || (count == capacity && calls <= entries[count - 1].calls)) continue; - std::size_t position = count < capacity ? count++ : count - 1; - while (position > 0 && entries[position - 1].calls < calls) { - entries[position] = entries[position - 1]; - --position; - } - entries[position] = {calls, record.name.c_str(), record.library.c_str()}; - } - return count; -} - -} - -asm(".text\n" - APS5_ASM_FUNCTION("CallTraceEnter_nid_no_patch") - " push %rdi\n" - " push %rsi\n" - " push %rdx\n" - " push %rcx\n" - " push %r8\n" - " push %r9\n" - " push %rax\n" - " push %r10\n" - " push %r11\n" - " sub $128, %rsp\n" - " movdqu %xmm0, 0(%rsp)\n" - " movdqu %xmm1, 16(%rsp)\n" - " movdqu %xmm2, 32(%rsp)\n" - " movdqu %xmm3, 48(%rsp)\n" - " movdqu %xmm4, 64(%rsp)\n" - " movdqu %xmm5, 80(%rsp)\n" - " movdqu %xmm6, 96(%rsp)\n" - " movdqu %xmm7, 112(%rsp)\n" - " mov %r11, %rdi\n" - " mov 200(%rsp), %rsi\n" - " call CallTraceRecord_nid_no_patch\n" - " movdqu 0(%rsp), %xmm0\n" - " movdqu 16(%rsp), %xmm1\n" - " movdqu 32(%rsp), %xmm2\n" - " movdqu 48(%rsp), %xmm3\n" - " movdqu 64(%rsp), %xmm4\n" - " movdqu 80(%rsp), %xmm5\n" - " movdqu 96(%rsp), %xmm6\n" - " movdqu 112(%rsp), %xmm7\n" - " add $128, %rsp\n" - " pop %r11\n" - " pop %r10\n" - " pop %rax\n" - " pop %r9\n" - " pop %r8\n" - " pop %rcx\n" - " pop %rdx\n" - " pop %rsi\n" - " pop %rdi\n" - " jmp *(%r11)\n"); -#endif diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 6491da364c..1d031c17a1 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 @@ -232,6 +235,14 @@ constexpr DWORD GccException = 0x20474343u; constexpr std::uint64_t GccExceptionClass = 0x474e5543432b2b00ull; constexpr std::uint64_t CxaExceptionTypeOffset = 80; +PWSTR ThreadDescription(HANDLE thread) { + using GetDescription = HRESULT (WINAPI *)(HANDLE, PWSTR*); + static const auto getDescription = reinterpret_cast(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "GetThreadDescription")); + PWSTR description = nullptr; + if (!getDescription || FAILED(getDescription(thread, &description))) return nullptr; + return description; +} + std::uint64_t FrameWithoutUnwindData(const CONTEXT& start) { const HMODULE runtime[] = {GetModuleHandleA("KERNELBASE.dll"), GetModuleHandleA("libgcc_s_seh-1.dll"), GetModuleHandleA("libstdc++-6.dll")}; CONTEXT context = start; @@ -253,6 +264,58 @@ bool ReadWord(std::uint64_t address, std::uint64_t* value) { return ReadCode(address, reinterpret_cast(value), sizeof(*value)) == sizeof(*value); } +std::uint64_t EnvironmentNumber(const char* name) { + char text[32]; + const DWORD length = GetEnvironmentVariableA(name, text, sizeof(text)); + if (length == 0 || length >= sizeof(text)) return 0; + return std::strtoull(text, nullptr, 0); +} + +void ReportStackWords(std::uint64_t rsp) { + const auto bytes = std::min(EnvironmentNumber("APS5_CRASH_STACK_BYTES"), std::uint64_t{1} << 20); + if (bytes == 0) return; + const auto* tib = reinterpret_cast(NtCurrentTeb()); + const auto stackLow = reinterpret_cast(tib->StackLimit); + const auto stackHigh = reinterpret_cast(tib->StackBase); + const auto first = rsp & ~std::uint64_t{7}; + Report(" stack words from rsp 0x%016llx (APS5_CRASH_STACK_BYTES):\n", static_cast(rsp)); + const auto limit = std::min(first + bytes, stackHigh); + std::uint64_t end = first; + std::uint64_t readablePage = 0; + for (std::uint64_t at = first; at < limit; at += 32) { + std::uint64_t words[4] = {}; + int count = 0; + for (; count < 4; ++count) { + const auto slot = at + static_cast(count) * 8; + const auto page = slot & ~std::uint64_t{0xfff}; + if (page != readablePage) { + if (!IsReadable(slot)) break; + readablePage = page; + } + words[count] = *reinterpret_cast(slot); + } + if (count == 0) break; + static char text[192]; + int used = std::snprintf(text, sizeof(text), " +0x%04llx:", static_cast(at - first)); + for (int k = 0; k < count && used > 0 && used < static_cast(sizeof(text)); ++k) used += std::snprintf(text + used, sizeof(text) - static_cast(used), " %016llx", static_cast(words[k])); + Report("%s\n", text); + end = at + static_cast(count) * 8; + if (count < 4) break; + } + static std::uint64_t seen[256]; + std::size_t seenCount = 0; + Report(" data at stack words that point outside the stack (8 words each):\n"); + for (std::uint64_t slot = first; slot + 8 <= end && seenCount < std::size(seen); slot += 8) { + const auto value = *reinterpret_cast(slot); + if (value < 0x10000 || (value & 7) != 0 || value > 0x7fffffffffffull || (value >= stackLow && value < stackHigh)) continue; + if (std::find(seen, seen + seenCount, value) != seen + seenCount) continue; + if (IsExecutable(value) || !IsReadable(value) || !IsReadable(value + 0x38)) continue; + seen[seenCount++] = value; + const auto* words = reinterpret_cast(value); + Report(" [+0x%04llx] 0x%016llx: %016llx %016llx %016llx %016llx %016llx %016llx %016llx %016llx\n", static_cast(slot - first), static_cast(value), words[0], words[1], words[2], words[3], words[4], words[5], words[6], words[7]); + } +} + void ReportUndeliverableException(const EXCEPTION_POINTERS* info, std::uint64_t frame) { const auto* record = info->ExceptionRecord; char type[256] = "of unknown type"; @@ -304,11 +367,11 @@ 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] = ""; - PWSTR description = nullptr; - if (SUCCEEDED(GetThreadDescription(GetCurrentThread(), &description)) && description) { + if (PWSTR description = ThreadDescription(GetCurrentThread())) { WideCharToMultiByte(CP_UTF8, 0, description, -1, threadName, sizeof(threadName), nullptr, nullptr); LocalFree(description); } @@ -325,6 +388,11 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { if (record->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && record->NumberParameters >= 2) { const char* kind = record->ExceptionInformation[0] == 0 ? "read" : record->ExceptionInformation[0] == 1 ? "write" : "execute"; Report(" %s of 0x%016llx\n", kind, static_cast(record->ExceptionInformation[1])); + std::uint8_t trap[2]; + if (record->ExceptionInformation[0] == 0 && record->ExceptionInformation[1] == ~0ull && ReadCode(context->Rip, trap, sizeof(trap)) == sizeof(trap) && trap[0] == 0xcd) { + if (trap[1] == 0x45) Report(" guest abort() (int 0x45)\n"); + else if (trap[1] == 0x41) Report(" guest assert/break (int 0x41)\n"); + } } Report(" rax %016llx rbx %016llx rcx %016llx rdx %016llx\n", context->Rax, context->Rbx, context->Rcx, context->Rdx); Report(" rsi %016llx rdi %016llx rbp %016llx rsp %016llx\n", context->Rsi, context->Rdi, context->Rbp, context->Rsp); @@ -352,6 +420,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) { @@ -364,7 +441,8 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { frame = next; } ReportStackCandidates(context->Rsp, 16, 0x2000); - if (context->Rip == 0 || !IsExecutable(context->Rip)) { + ReportStackWords(context->Rsp); + if (context->Rip == 0 ||!IsExecutable(context->Rip)) { // A jump into nothing usually follows a return through a clobbered frame; the frames that just // returned are still below rsp. Report(" recently popped return address candidates:\n"); @@ -394,6 +472,7 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { ReportCallTrace(); std::fflush(stderr); ReportAllThreads(); + LibcRunFailureReports_nid_no_patch(); std::fflush(stderr); return EXCEPTION_CONTINUE_SEARCH; } @@ -409,6 +488,16 @@ void ReportCallTrace() { Report(" #%llu thread %llu %s %s from %s\n", static_cast(entries[index].sequence), static_cast(entries[index].thread), entries[index].library, entries[index].name, line); } + static CallTraceEntry own[32]; + const std::uint64_t thread = GetCurrentThreadId(); + const std::size_t owned = CopyThreadCallTrace_nid_no_patch(thread, own, std::size(own)); + if (owned != 0) { + Report(" last traced calls of thread %llu:\n", static_cast(thread)); + for (std::size_t index = 0; index < owned; ++index) { + DescribeAddress(own[index].returnAddress, line, sizeof(line)); + Report(" #%llu %s %s from %s\n", static_cast(own[index].sequence), own[index].library, own[index].name, line); + } + } static CallTraceCount counts[32]; std::uint64_t total = 0; const std::size_t counted = CopyCallCounts_nid_no_patch(counts, std::size(counts), &total); @@ -433,7 +522,17 @@ std::size_t CaptureFrames(const CONTEXT* context, std::uint64_t* frames, std::si return count; } -void ReportThreadContext(const CONTEXT* context, DWORD threadId, const char* name, const std::uint64_t* frames, std::size_t frameCount) { +std::size_t CaptureStackCandidates(std::uint64_t rsp, std::uint64_t* candidates, std::size_t capacity) { + std::size_t count = 0; + for (std::uint64_t slot = rsp; count < capacity && slot < rsp + 0x8000; slot += 8) { + if (!IsReadable(slot)) break; + const auto value = *reinterpret_cast(slot); + if (IsExecutable(value)) candidates[count++] = value; + } + return count; +} + +void ReportThreadContext(const CONTEXT* context, DWORD threadId, const char* name, const std::uint64_t* frames, std::size_t frameCount, const std::uint64_t* candidates, std::size_t candidateCount) { char line[MAX_PATH + 64]; Report(" thread %lu '%s': rip ", static_cast(threadId), name ? name : ""); DescribeAddress(context->Rip, line, sizeof(line)); @@ -442,6 +541,11 @@ void ReportThreadContext(const CONTEXT* context, DWORD threadId, const char* nam DescribeAddress(frames[depth], line, sizeof(line)); Report(" #%llu %s\n", static_cast(depth), line); } + if (candidateCount != 0) Report(" stack return address candidates:\n"); + for (std::size_t index = 0; index < candidateCount; ++index) { + DescribeAddress(candidates[index], line, sizeof(line)); + Report(" %s\n", line); + } } void ReportAllThreads() { @@ -457,6 +561,8 @@ void ReportAllThreads() { CONTEXT context; std::uint64_t frames[12]; std::size_t frameCount; + std::uint64_t candidates[24]; + std::size_t candidateCount; }; static CapturedThread captured[128]; std::size_t capturedCount = 0; @@ -479,10 +585,11 @@ void ReportAllThreads() { const BOOL haveContext = GetThreadContext(thread, &context); std::uint64_t frames[12] = {}; const std::size_t frameCount = haveContext ? CaptureFrames(&context, frames, sizeof(frames) / sizeof(frames[0])) : 0; + std::uint64_t candidates[24] = {}; + const std::size_t candidateCount = haveContext && frameCount < 2 ? CaptureStackCandidates(context.Rsp, candidates, sizeof(candidates) / sizeof(candidates[0])) : 0; ResumeThread(thread); char name[128] = ""; - PWSTR description = nullptr; - if (SUCCEEDED(GetThreadDescription(thread, &description)) && description) { + if (PWSTR description = ThreadDescription(thread)) { WideCharToMultiByte(CP_UTF8, 0, description, -1, name, sizeof(name), nullptr, nullptr); LocalFree(description); } @@ -493,13 +600,15 @@ void ReportAllThreads() { captured[capturedCount].context = context; std::memcpy(captured[capturedCount].frames, frames, sizeof(captured[capturedCount].frames)); captured[capturedCount].frameCount = frameCount; + std::memcpy(captured[capturedCount].candidates, candidates, sizeof(captured[capturedCount].candidates)); + captured[capturedCount].candidateCount = candidateCount; ++capturedCount; } while (Thread32Next(snapshot, &entry)); } CloseHandle(snapshot); Report(" all threads:\n"); for (std::size_t i = 0; i < capturedCount; ++i) { - ReportThreadContext(&captured[i].context, captured[i].id, captured[i].name, captured[i].frames, captured[i].frameCount); + ReportThreadContext(&captured[i].context, captured[i].id, captured[i].name, captured[i].frames, captured[i].frameCount, captured[i].candidates, captured[i].candidateCount); } if (skippedCount != 0) Report(" %llu further thread(s) left out\n", static_cast(skippedCount)); } @@ -546,10 +655,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/libc/src/specifics/windows/SehPersonalities.cpp b/core/libs/prx/libc/src/specifics/windows/SehPersonalities.cpp new file mode 100644 index 0000000000..cd4c856063 --- /dev/null +++ b/core/libs/prx/libc/src/specifics/windows/SehPersonalities.cpp @@ -0,0 +1,11 @@ +#include + +extern "C" { +EXCEPTION_DISPOSITION __gxx_personality_seh0(EXCEPTION_RECORD*, void*, CONTEXT*, void*); +EXCEPTION_DISPOSITION __gcc_personality_seh0(EXCEPTION_RECORD*, void*, CONTEXT*, void*); + +[[gnu::section(".sehgcc"), gnu::used]] extern const PEXCEPTION_ROUTINE LibcSehPersonalities_nid_no_patch[] { + __gxx_personality_seh0, + __gcc_personality_seh0, +}; +} diff --git a/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp new file mode 100644 index 0000000000..c81b91a39a --- /dev/null +++ b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp @@ -0,0 +1,454 @@ +#if defined(_WIN32) || defined(__linux__) +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#include +#include +#endif +#include "prx/libc/include/CallTrace.hpp" +#include "prx/libc/include/general/AsmMacros.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" void CallTraceEnter_nid_no_patch(); + +namespace { + +struct ImportRecord { + void* target = nullptr; + std::string name; + std::string library; + mutable std::atomic calls{0}; +}; + +struct RingSlot { + std::atomic sequence{0}; + std::atomic thread{0}; + std::atomic returnAddress{0}; + std::atomic record{nullptr}; +}; + +constexpr std::size_t RingCapacity = 65536; +constexpr std::size_t ThunkBytes = 24; +constexpr std::size_t ThunkAreaBytes = 1024 * 1024; +constexpr std::size_t RecordCapacity = ThunkAreaBytes / ThunkBytes; + +std::mutex g_mutex; +std::unordered_map g_thunkByTarget; +std::atomic g_records{nullptr}; +std::atomic g_recordCount{0}; +std::atomic g_ring{nullptr}; +std::atomic g_sequence{0}; +std::uint8_t* g_writable = nullptr; +std::uint8_t* g_executable = nullptr; + +bool Selected(std::string_view library) { + const char* value = std::getenv("APS5_TRACE_CALLS"); + if (value == nullptr || *value == '\0') return false; + std::string_view rest(value); + if (library.ends_with(".prx")) library.remove_suffix(4); + while (!rest.empty()) { + const auto comma = rest.find(','); + auto token = rest.substr(0, comma); + rest = comma == std::string_view::npos ? std::string_view{} : rest.substr(comma + 1); + if (token.ends_with(".prx")) token.remove_suffix(4); + if (token == "all" || token == library) return true; + } + return false; +} + +std::uint64_t CurrentThread() { +#ifdef _WIN32 + return GetCurrentThreadId(); +#else + return static_cast(gettid()); +#endif +} + +#ifdef _WIN32 +void MapThunkArea() { + const HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, static_cast(ThunkAreaBytes), nullptr); + if (section == nullptr) throw std::system_error(static_cast(GetLastError()), std::system_category(), "call trace: CreateFileMappingW"); + g_writable = static_cast(MapViewOfFile(section, FILE_MAP_WRITE, 0, 0, ThunkAreaBytes)); + g_executable = static_cast(MapViewOfFile(section, FILE_MAP_READ | FILE_MAP_EXECUTE, 0, 0, ThunkAreaBytes)); + const DWORD code = GetLastError(); + CloseHandle(section); + if (g_writable == nullptr || g_executable == nullptr) throw std::system_error(static_cast(code), std::system_category(), "call trace: MapViewOfFile"); +} +#else +void MapThunkArea() { + const int file = memfd_create("aps5-call-trace", MFD_CLOEXEC); + if (file < 0) throw std::system_error(errno, std::generic_category(), "call trace: memfd_create"); + void* writable = MAP_FAILED; + void* executable = MAP_FAILED; + if (ftruncate(file, ThunkAreaBytes) == 0) { + writable = mmap(nullptr, ThunkAreaBytes, PROT_READ | PROT_WRITE, MAP_SHARED, file, 0); + if (writable != MAP_FAILED) executable = mmap(nullptr, ThunkAreaBytes, PROT_READ | PROT_EXEC, MAP_SHARED, file, 0); + } + const int code = errno; + close(file); + if (writable == MAP_FAILED || executable == MAP_FAILED) { + if (writable != MAP_FAILED) munmap(writable, ThunkAreaBytes); + throw std::system_error(code, std::generic_category(), "call trace: mmap"); + } + g_writable = static_cast(writable); + g_executable = static_cast(executable); +} + +struct ModuleSegment { + std::uint64_t start; + std::uint64_t end; + std::uint64_t base; + char name[64]; +}; + +struct ModuleMap { + std::size_t count; + std::uint64_t pageSize; + ModuleSegment segments[256]; +}; + +struct sigaction g_previousAbort{}; +std::atomic g_modules{nullptr}; +std::atomic g_reporting{false}; +std::atomic g_reported{false}; +unsigned long long g_moduleChanges = ~0ull; + +void SnapshotModules() { + unsigned long long changes = 0; + dl_iterate_phdr([](dl_phdr_info* info, std::size_t, void* data) { + *static_cast(data) = info->dlpi_adds + info->dlpi_subs; + return 1; + }, &changes); + if (changes == g_moduleChanges) return; + g_moduleChanges = changes; + auto* map = new ModuleMap{}; + map->pageSize = static_cast(sysconf(_SC_PAGESIZE)); + dl_iterate_phdr([](dl_phdr_info* info, std::size_t, void* data) { + auto& modules = *static_cast(data); + const char* path = info->dlpi_name != nullptr && *info->dlpi_name != '\0' ? info->dlpi_name : program_invocation_short_name; + const char* slash = std::strrchr(path, '/'); + const char* name = slash == nullptr ? path : slash + 1; + std::uint64_t first = std::numeric_limits::max(); + for (int index = 0; index < info->dlpi_phnum; ++index) + if (info->dlpi_phdr[index].p_type == PT_LOAD) first = std::min(first, info->dlpi_phdr[index].p_vaddr & ~(modules.pageSize - 1)); + for (int index = 0; index < info->dlpi_phnum && modules.count < std::size(modules.segments); ++index) { + const auto& header = info->dlpi_phdr[index]; + if (header.p_type != PT_LOAD || header.p_memsz == 0) continue; + auto& segment = modules.segments[modules.count++]; + segment.base = info->dlpi_addr + first; + segment.start = info->dlpi_addr + header.p_vaddr; + segment.end = segment.start + header.p_memsz; + std::strncpy(segment.name, name, sizeof(segment.name) - 1); + } + return 0; + }, map); + g_modules.store(map, std::memory_order_release); +} + +class ReportLine { +public: + ReportLine& Text(const char* text) { + while (*text != '\0' && _length < sizeof(_buffer)) _buffer[_length++] = *text++; + return *this; + } + + ReportLine& Decimal(std::uint64_t value, std::size_t width = 0) { + char digits[20]; + std::size_t count = 0; + do { + digits[count++] = static_cast('0' + value % 10); + value /= 10; + } while (value != 0); + for (std::size_t pad = count; pad < width; ++pad) Text(" "); + while (count > 0 && _length < sizeof(_buffer)) _buffer[_length++] = digits[--count]; + return *this; + } + + ReportLine& Hex(std::uint64_t value, std::size_t digits) { + while (digits < 16 && (value >> (digits * 4)) != 0) ++digits; + Text("0x"); + for (std::size_t index = digits; index > 0 && _length < sizeof(_buffer); --index) { + _buffer[_length++] = "0123456789abcdef"[(value >> ((index - 1) * 4)) & 0xf]; + } + return *this; + } + + void Write() { + Text("\n"); + std::size_t written = 0; + while (written < _length) { + const ssize_t result = ::write(STDERR_FILENO, _buffer + written, _length - written); + if (result < 0 && errno == EINTR) continue; + if (result <= 0) break; + written += static_cast(result); + } + _length = 0; + } + +private: + char _buffer[512]; + std::size_t _length = 0; +}; + +void DescribeAddress(ReportLine& line, std::uint64_t address) { + line.Hex(address, 16); + const ModuleMap* modules = g_modules.load(std::memory_order_acquire); + if (modules == nullptr) return; + for (std::size_t index = 0; index < modules->count; ++index) { + const auto& segment = modules->segments[index]; + if (address < segment.start || address >= segment.end) continue; + line.Text(" ").Text(segment.name).Text("+"); + line.Hex(address - segment.base, 1); + return; + } +} + +void ReportCallTrace() { + static CallTraceEntry entries[64]; + static CallTraceCount counts[32]; + ReportLine line; + const std::size_t count = CopyCallTrace_nid_no_patch(entries, std::size(entries)); + if (count == 0) return; + line.Text(" last traced system library calls (APS5_TRACE_CALLS):").Write(); + for (std::size_t index = 0; index < count; ++index) { + line.Text(" #").Decimal(entries[index].sequence).Text(" thread ").Decimal(entries[index].thread); + line.Text(" ").Text(entries[index].library).Text(" ").Text(entries[index].name).Text(" from "); + DescribeAddress(line, entries[index].returnAddress); + line.Write(); + } + static CallTraceEntry own[32]; + const std::uint64_t thread = CurrentThread(); + const std::size_t owned = CopyThreadCallTrace_nid_no_patch(thread, own, std::size(own)); + if (owned != 0) { + line.Text(" last traced calls of thread ").Decimal(thread).Text(":").Write(); + for (std::size_t index = 0; index < owned; ++index) { + line.Text(" #").Decimal(own[index].sequence).Text(" ").Text(own[index].library).Text(" ").Text(own[index].name).Text(" from "); + DescribeAddress(line, own[index].returnAddress); + line.Write(); + } + } + std::uint64_t total = 0; + const std::size_t counted = CopyCallCounts_nid_no_patch(counts, std::size(counts), &total); + if (counted == 0) return; + line.Text(" most called traced imports (").Decimal(total).Text(" calls in total):").Write(); + for (std::size_t index = 0; index < counted; ++index) { + line.Text(" ").Decimal(counts[index].calls, 12).Text(" ").Text(counts[index].library).Text(" ").Text(counts[index].name).Write(); + } +} + +void ReportOnAbort(int number, siginfo_t* info, void* context) { + const int savedErrno = errno; + if (!g_reporting.exchange(true, std::memory_order_acq_rel)) { + ReportLine line; + line.Text("[libc] SIGABRT").Write(); + ReportCallTrace(); + g_reported.store(true, std::memory_order_release); + } else { + const timespec pause{0, 1000000}; + for (int waited = 0; waited < 2000 && !g_reported.load(std::memory_order_acquire); ++waited) nanosleep(&pause, nullptr); + } + errno = savedErrno; + if ((g_previousAbort.sa_flags & SA_SIGINFO) != 0) { + if (g_previousAbort.sa_sigaction != nullptr) g_previousAbort.sa_sigaction(number, info, context); + return; + } + if (g_previousAbort.sa_handler == SIG_IGN) return; + if (g_previousAbort.sa_handler != SIG_DFL) { + g_previousAbort.sa_handler(number); + return; + } + struct sigaction fallback{}; + fallback.sa_handler = SIG_DFL; + sigemptyset(&fallback.sa_mask); + sigaction(number, &fallback, nullptr); + raise(number); +} + +void InstallAbortReport() { + struct sigaction action{}; + action.sa_sigaction = ReportOnAbort; + action.sa_flags = SA_SIGINFO; + sigemptyset(&action.sa_mask); + if (sigaction(SIGABRT, &action, &g_previousAbort) != 0) throw std::system_error(errno, std::generic_category(), "call trace: sigaction"); +} +#endif + +void* WriteThunk(const ImportRecord& record, const std::size_t index) { + const std::size_t offset = index * ThunkBytes; + const std::uint8_t prefix[] = {0x49, 0xbb}; + const std::uint8_t jump[] = {0xff, 0x25, 0, 0, 0, 0}; + const auto recordAddress = reinterpret_cast(&record); + const auto handler = reinterpret_cast(&CallTraceEnter_nid_no_patch); + std::uint8_t* thunk = g_writable + offset; + std::memcpy(thunk, prefix, sizeof(prefix)); + std::memcpy(thunk + 2, &recordAddress, sizeof(recordAddress)); + std::memcpy(thunk + 10, jump, sizeof(jump)); + std::memcpy(thunk + 16, &handler, sizeof(handler)); +#ifdef _WIN32 + FlushInstructionCache(GetCurrentProcess(), g_executable + offset, ThunkBytes); +#else + __builtin___clear_cache(reinterpret_cast(g_executable + offset), reinterpret_cast(g_executable + offset + ThunkBytes)); +#endif + return g_executable + offset; +} + +} + +static_assert(offsetof(ImportRecord, target) == 0); + +extern "C" { + +void APS5_VABI CallTraceRecord_nid_no_patch(const void* record, std::uint64_t returnAddress) { + static_cast(record)->calls.fetch_add(1, std::memory_order_relaxed); + const std::uint64_t sequence = g_sequence.fetch_add(1, std::memory_order_relaxed) + 1; + RingSlot& slot = g_ring.load(std::memory_order_relaxed)[sequence % RingCapacity]; + slot.sequence.store(0, std::memory_order_release); + slot.thread.store(CurrentThread(), std::memory_order_relaxed); + slot.returnAddress.store(returnAddress, std::memory_order_relaxed); + slot.record.store(static_cast(record), std::memory_order_relaxed); + slot.sequence.store(sequence, std::memory_order_release); +} + +void* TraceImport_nid_no_patch(void* target, const char* name, const char* library) { + if (target == nullptr || name == nullptr || library == nullptr || !Selected(library)) return target; + std::lock_guard lock(g_mutex); + if (g_ring.load(std::memory_order_relaxed) == nullptr) { + MapThunkArea(); + g_records.store(new ImportRecord[RecordCapacity], std::memory_order_release); + g_ring.store(new RingSlot[RingCapacity], std::memory_order_release); +#ifdef __linux__ + InstallAbortReport(); +#endif + } +#ifdef __linux__ + SnapshotModules(); +#endif + if (const auto existing = g_thunkByTarget.find(target); existing != g_thunkByTarget.end()) return existing->second; + const std::size_t index = g_recordCount.load(std::memory_order_relaxed); + if (index == RecordCapacity) throw std::runtime_error("call trace: thunk area exhausted"); + ImportRecord& record = g_records.load(std::memory_order_relaxed)[index]; + record.target = target; + record.name = name; + record.library = library; + void* thunk = WriteThunk(record, index); + g_thunkByTarget.emplace(target, thunk); + g_recordCount.store(index + 1, std::memory_order_release); + return thunk; +} + +std::size_t CopyCallTrace_nid_no_patch(CallTraceEntry* entries, std::size_t capacity) { + const RingSlot* ring = g_ring.load(std::memory_order_acquire); + if (entries == nullptr || capacity == 0 || ring == nullptr) return 0; + const std::uint64_t last = g_sequence.load(std::memory_order_acquire); + const std::uint64_t span = std::min({last, capacity, RingCapacity}); + std::size_t count = 0; + for (std::uint64_t sequence = last - span + 1; sequence <= last; ++sequence) { + const RingSlot& slot = ring[sequence % RingCapacity]; + if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; + const auto* record = slot.record.load(std::memory_order_relaxed); + const CallTraceEntry entry{sequence, slot.thread.load(std::memory_order_relaxed), slot.returnAddress.load(std::memory_order_relaxed), record->name.c_str(), record->library.c_str()}; + if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; + entries[count++] = entry; + } + return count; +} + +std::size_t CopyThreadCallTrace_nid_no_patch(std::uint64_t thread, CallTraceEntry* entries, std::size_t capacity) { + const RingSlot* ring = g_ring.load(std::memory_order_acquire); + if (entries == nullptr || capacity == 0 || ring == nullptr) return 0; + const std::uint64_t last = g_sequence.load(std::memory_order_acquire); + const std::uint64_t span = std::min(last, RingCapacity); + std::size_t count = 0; + for (std::uint64_t sequence = last; sequence > last - span && count < capacity; --sequence) { + const RingSlot& slot = ring[sequence % RingCapacity]; + if (slot.sequence.load(std::memory_order_acquire) != sequence || slot.thread.load(std::memory_order_relaxed) != thread) continue; + const auto* record = slot.record.load(std::memory_order_relaxed); + const CallTraceEntry entry{sequence, thread, slot.returnAddress.load(std::memory_order_relaxed), record->name.c_str(), record->library.c_str()}; + if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; + entries[count++] = entry; + } + std::reverse(entries, entries + count); + return count; +} + +std::size_t CopyCallCounts_nid_no_patch(CallTraceCount* entries, std::size_t capacity, std::uint64_t* total) { + if (total != nullptr) *total = g_sequence.load(std::memory_order_acquire); + if (entries == nullptr || capacity == 0) return 0; + const std::size_t recorded = g_recordCount.load(std::memory_order_acquire); + const ImportRecord* records = g_records.load(std::memory_order_acquire); + std::size_t count = 0; + for (std::size_t index = 0; index < recorded; ++index) { + const ImportRecord& record = records[index]; + const std::uint64_t calls = record.calls.load(std::memory_order_relaxed); + if (calls == 0 || (count == capacity && calls <= entries[count - 1].calls)) continue; + std::size_t position = count < capacity ? count++ : count - 1; + while (position > 0 && entries[position - 1].calls < calls) { + entries[position] = entries[position - 1]; + --position; + } + entries[position] = {calls, record.name.c_str(), record.library.c_str()}; + } + return count; +} + +} + +asm(".text\n" + APS5_ASM_FUNCTION("CallTraceEnter_nid_no_patch") + " push %rdi\n" + " push %rsi\n" + " push %rdx\n" + " push %rcx\n" + " push %r8\n" + " push %r9\n" + " push %rax\n" + " push %r10\n" + " push %r11\n" + " sub $128, %rsp\n" + " movdqu %xmm0, 0(%rsp)\n" + " movdqu %xmm1, 16(%rsp)\n" + " movdqu %xmm2, 32(%rsp)\n" + " movdqu %xmm3, 48(%rsp)\n" + " movdqu %xmm4, 64(%rsp)\n" + " movdqu %xmm5, 80(%rsp)\n" + " movdqu %xmm6, 96(%rsp)\n" + " movdqu %xmm7, 112(%rsp)\n" + " mov %r11, %rdi\n" + " mov 200(%rsp), %rsi\n" + " call CallTraceRecord_nid_no_patch\n" + " movdqu 0(%rsp), %xmm0\n" + " movdqu 16(%rsp), %xmm1\n" + " movdqu 32(%rsp), %xmm2\n" + " movdqu 48(%rsp), %xmm3\n" + " movdqu 64(%rsp), %xmm4\n" + " movdqu 80(%rsp), %xmm5\n" + " movdqu 96(%rsp), %xmm6\n" + " movdqu 112(%rsp), %xmm7\n" + " add $128, %rsp\n" + " pop %r11\n" + " pop %r10\n" + " pop %rax\n" + " pop %r9\n" + " pop %r8\n" + " pop %rcx\n" + " pop %rdx\n" + " pop %rsi\n" + " pop %rdi\n" + " jmp *(%r11)\n" + APS5_ASM_FUNCTION_END("CallTraceEnter_nid_no_patch")); +#endif diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index c16167088f..9926ac1dd6 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; @@ -74,11 +80,12 @@ std::int64_t NativePread(std::int32_t fd, void* buf, std::size_t nbyte, std::int } std::int64_t NativePwrite(std::int32_t fd, const void* buf, std::size_t nbyte, std::int64_t offset) { -#ifdef _WIN32 if (offset < 0) { errno = EINVAL; return -1; } + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbyte); +#ifdef _WIN32 return NativePositioned_nid_no_patch(fd, const_cast(buf), nbyte, offset, true); #else const auto result = File::PwriteSharedMemory_nid_no_patch(fd, buf, nbyte, offset); @@ -127,9 +134,9 @@ int SubmitCommands(KernelAioRwRequest* req, std::int32_t size, bool write, std:: } int SubmitCommandsMultiple(KernelAioRwRequest* req, std::int32_t size, bool write, std::int32_t* id, const char* name) { + if (req != nullptr && id != nullptr && size > AioRequestNumMax) throw std::runtime_error(std::string(name) + ": more than 128 requests per batch is not modelled"); const int error = ValidateRequests(req, size, id); if (error != 0) return error; - if (size > AioRequestNumMax) throw std::runtime_error(std::string(name) + ": more than 128 requests per batch is not modelled"); for (std::int32_t i = 0; i < size; ++i) { const std::int32_t queue = AllocateId(); SetState(queue, RunRequest(req[i], write) ? AioCompleted : AioAborted); diff --git a/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp b/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp index 0cceb906ff..ef97a2c7cf 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp @@ -11,6 +11,10 @@ struct AppTitleId { char value[12]; }; +struct AppContentVersion { + char value[16]; +}; + struct AppIconData { const std::uint8_t* bytes; std::uint64_t size; @@ -20,6 +24,8 @@ extern "C" { AppTitle GetAppTitle_nid_postfix(); AppTitleId GetAppTitleId_nid_postfix(); +bool HasAppParamJson_nid_no_patch(); +AppContentVersion GetAppContentVersion_nid_no_patch(); bool HasAppIcon_nid_postfix(); AppIconData GetAppIconData_nid_postfix(); std::uint64_t GetAppDownloadDataSizeMiB_nid_postfix(); diff --git a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp index 22f615c522..65e2c6c0a8 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp @@ -9,6 +9,7 @@ struct ParsedParamJson { std::string title; std::string titleId; + std::string contentVersion; std::uint64_t downloadDataSizeMiB = 0; std::uint32_t attribute3 = 0; std::optional flexibleMemorySizeBytes; diff --git a/core/libs/prx/libkernel/AppMetadata/include/ParamSfoParser.hpp b/core/libs/prx/libkernel/AppMetadata/include/ParamSfoParser.hpp new file mode 100644 index 0000000000..4e828a4970 --- /dev/null +++ b/core/libs/prx/libkernel/AppMetadata/include/ParamSfoParser.hpp @@ -0,0 +1,10 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_APPMETADATA_INCLUDE_PARAMSFOPARSER_HPP +#define CORE_LIBS_PRX_LIBKERNEL_APPMETADATA_INCLUDE_PARAMSFOPARSER_HPP + +#include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" + +#include + +ParsedParamJson parseParamSfo(const std::filesystem::path& paramSfoPath); + +#endif diff --git a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp index e5f172fcea..3f46122f14 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp @@ -1,28 +1,33 @@ #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" +#include "prx/libkernel/AppMetadata/include/ParamSfoParser.hpp" #include "prx/libc/include/General.hpp" #include #include #include #include +#include #include #include #include namespace { +std::mutex g_metadataMutex; bool g_titleLoaded = false; bool g_iconAttempted = false; bool g_iconLoaded = false; char g_title[128] = {}; char g_titleId[12] = {}; +char g_contentVersion[16] = {}; std::uint64_t g_downloadDataSizeMiB = 0; std::uint32_t g_attribute3 = 0; std::int32_t g_userDefinedParams[4] = {}; std::vector g_iconBytes; constexpr const char* AppMetadataParamJsonGuestPath = "/app0/sce_sys/param.json"; +constexpr const char* AppMetadataParamSfoGuestPath = "/app0/sce_sys/param.sfo"; constexpr const char* AppMetadataIconGuestPath = "/app0/sce_sys/icon0.png"; void copyToFixedBuffer(char* destination, std::size_t destinationSize, const std::string& source) { @@ -31,13 +36,19 @@ void copyToFixedBuffer(char* destination, std::size_t destinationSize, const std std::memcpy(destination, source.c_str(), source.size() + 1); } +ParsedParamJson parseParams(const std::filesystem::path& paramJson, const std::filesystem::path& paramSfo) { + if (std::filesystem::exists(paramJson)) return parseParamJson(paramJson); + if (std::filesystem::exists(paramSfo)) return parseParamSfo(paramSfo); + throw std::runtime_error("param.json or param.sfo not found"); +} + void ensureTitleLoaded() { + std::lock_guard lock(g_metadataMutex); if (g_titleLoaded) return; - const auto resolvedPath = ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath); - if (!std::filesystem::exists(resolvedPath)) throw std::runtime_error("param.json not found"); - const auto parsed = parseParamJson(resolvedPath); + const auto parsed = parseParams(ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath), ResolvePath_nid_no_patch(AppMetadataParamSfoGuestPath)); copyToFixedBuffer(g_title, sizeof(g_title), parsed.title); copyToFixedBuffer(g_titleId, sizeof(g_titleId), parsed.titleId); + if (!parsed.contentVersion.empty() && parsed.contentVersion.size() < sizeof(g_contentVersion)) copyToFixedBuffer(g_contentVersion, sizeof(g_contentVersion), parsed.contentVersion); g_downloadDataSizeMiB = parsed.downloadDataSizeMiB; g_attribute3 = parsed.attribute3; std::memcpy(g_userDefinedParams, parsed.userDefinedParams, sizeof(g_userDefinedParams)); @@ -47,8 +58,9 @@ void ensureTitleLoaded() { const bool g_downloadDataMounted = [] { try { const auto run = std::filesystem::current_path(); - const auto paramJson = run / "app0" / "sce_sys" / "param.json"; - if (!std::filesystem::exists(paramJson) || parseParamJson(paramJson).downloadDataSizeMiB == 0) return false; + const auto metadata = run / "app0" / "sce_sys"; + if (!std::filesystem::exists(metadata / "param.json") && !std::filesystem::exists(metadata / "param.sfo")) return false; + if (parseParams(metadata / "param.json", metadata / "param.sfo").downloadDataSizeMiB == 0) return false; std::filesystem::create_directories(run / "download0"); return true; } catch (const std::exception&) { @@ -59,7 +71,8 @@ const bool g_downloadDataMounted = [] { const bool g_dataMounted = [] { try { const auto run = std::filesystem::current_path(); - if (!std::filesystem::exists(run / "app0" / "sce_sys" / "param.json")) return false; + const auto metadata = run / "app0" / "sce_sys"; + if (!std::filesystem::exists(metadata / "param.json") && !std::filesystem::exists(metadata / "param.sfo")) return false; std::filesystem::create_directories(run / "data"); return true; } catch (const std::exception&) { @@ -68,6 +81,7 @@ const bool g_dataMounted = [] { }(); void ensureIconLoaded() { + std::lock_guard lock(g_metadataMutex); if (g_iconAttempted) return; g_iconAttempted = true; const auto resolvedPath = ResolvePath_nid_no_patch(AppMetadataIconGuestPath); @@ -76,10 +90,11 @@ void ensureIconLoaded() { if (!file.is_open()) throw std::runtime_error("failed to open icon0.png"); file.seekg(0, std::ios::end); const auto fileSize = file.tellg(); + if (file.fail() || fileSize <= 0) throw std::runtime_error("icon0.png is empty or unreadable"); file.seekg(0, std::ios::beg); std::vector bytes(static_cast(fileSize)); - if (!bytes.empty()) file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); - if (bytes.empty()) throw std::runtime_error("icon0.png is empty"); + file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (!file || file.gcount() != static_cast(bytes.size())) throw std::runtime_error("failed to read icon0.png"); constexpr std::uint8_t PngSignature[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; if (bytes.size() < 8 || std::memcmp(bytes.data(), PngSignature, sizeof(PngSignature)) != 0) throw std::runtime_error("icon0.png is not a valid PNG file"); g_iconBytes = std::move(bytes); @@ -120,6 +135,17 @@ AppTitleId GetAppTitleId_nid_postfix() { return result; } +bool HasAppParamJson_nid_no_patch() { + return std::filesystem::exists(ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath)); +} + +AppContentVersion GetAppContentVersion_nid_no_patch() { + ensureTitleLoaded(); + AppContentVersion result{}; + std::memcpy(result.value, g_contentVersion, sizeof(result.value)); + return result; +} + bool HasAppIcon_nid_postfix() { ensureIconLoaded(); return g_iconLoaded; diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index 8cde56681f..3150e1419a 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -322,6 +322,7 @@ ParsedParamJson parseParamJson(const std::filesystem::path& paramJsonPath) { ParsedParamJson result; result.title = title; result.titleId = titleId; + if (const JsonValue* contentVersion = findObjectMember(root, "contentVersion"); contentVersion != nullptr && contentVersion->type == JsonType::String) result.contentVersion = contentVersion->stringValue; if (const JsonValue* attribute = findObjectMember(root, "attribute3")) { const auto& number = attribute->numberText; if (attribute->type != JsonType::Number || number.find_first_of("-.eE") != std::string::npos) throw std::runtime_error("param.json attribute3 is not an unsigned integer"); @@ -337,9 +338,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/AppMetadata/src/ParamSfoParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp new file mode 100644 index 0000000000..bfaf32d9f8 --- /dev/null +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp @@ -0,0 +1,85 @@ +#include "prx/libkernel/AppMetadata/include/ParamSfoParser.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint16_t SfoUtf8 = 0x0204; +constexpr std::uint16_t SfoInteger = 0x0404; +constexpr std::size_t HeaderSize = 20; +constexpr std::size_t EntrySize = 16; + +std::uint16_t readU16(const std::vector& bytes, std::size_t offset) { + std::uint16_t value; + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +std::uint32_t readU32(const std::vector& bytes, std::size_t offset) { + std::uint32_t value; + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +} + +ParsedParamJson parseParamSfo(const std::filesystem::path& paramSfoPath) { + std::ifstream file(paramSfoPath, std::ios::binary); + if (!file.is_open()) throw std::runtime_error("failed to open param.sfo"); + const std::vector bytes((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + if (bytes.size() < HeaderSize || std::memcmp(bytes.data(), "\0PSF", 4) != 0) throw std::runtime_error("param.sfo has no PSF header"); + const std::uint64_t keyTable = readU32(bytes, 8); + const std::uint64_t dataTable = readU32(bytes, 12); + const std::uint64_t count = readU32(bytes, 16); + if (count > (bytes.size() - HeaderSize) / EntrySize) throw std::runtime_error("param.sfo tables are out of bounds"); + std::set keys; + std::map strings; + std::map integers; + for (std::uint64_t index = 0; index < count; ++index) { + const auto entry = HeaderSize + index * EntrySize; + const auto keyOffset = keyTable + readU16(bytes, entry); + const auto format = readU16(bytes, entry + 2); + const std::uint64_t length = readU32(bytes, entry + 4); + const auto dataOffset = dataTable + readU32(bytes, entry + 12); + if (keyOffset >= bytes.size() || dataOffset > bytes.size() || length > bytes.size() - dataOffset) throw std::runtime_error("param.sfo entry is out of bounds"); + const auto keyEnd = std::find(bytes.begin() + static_cast(keyOffset), bytes.end(), std::uint8_t{0}); + if (keyEnd == bytes.end()) throw std::runtime_error("param.sfo key is not terminated"); + const std::string key(bytes.begin() + static_cast(keyOffset), keyEnd); + if (!keys.insert(key).second) throw std::runtime_error("param.sfo repeats " + key); + if (format == SfoUtf8) { + const auto begin = bytes.begin() + static_cast(dataOffset); + const auto end = std::find(begin, begin + static_cast(length), std::uint8_t{0}); + if (end == begin + static_cast(length)) throw std::runtime_error("param.sfo " + key + " is not a terminated string"); + strings.emplace(key, std::string(begin, end)); + } else if (format == SfoInteger) { + if (length != 4) throw std::runtime_error("param.sfo " + key + " is not a 32-bit integer"); + integers.emplace(key, static_cast(readU32(bytes, dataOffset))); + } + } + const auto text = [&](const char* key) { + const auto found = strings.find(key); + if (found == strings.end() || found->second.empty()) throw std::runtime_error(std::string("param.sfo is missing ") + key); + return found->second; + }; + ParsedParamJson result; + result.title = text("TITLE"); + result.titleId = text("TITLE_ID"); + if (const auto size = integers.find("DOWNLOAD_DATA_SIZE"); size != integers.end()) { + if (size->second < 0) throw std::runtime_error("param.sfo DOWNLOAD_DATA_SIZE is negative"); + result.downloadDataSizeMiB = static_cast(size->second); + } + for (int index = 0; index < 4; ++index) { + const auto param = integers.find("USER_DEFINED_PARAM_" + std::to_string(index + 1)); + if (param != integers.end()) result.userDefinedParams[index] = param->second; + } + return result; +} diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 22e9358fc2..42e4274d9e 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -165,9 +165,23 @@ void _readFile(const Apr::ReadFileCommand& command) { stream.seekg(static_cast(command.offset)); const GuestArena::HostWrite destination(reinterpret_cast(command.destination), command.size); if (!destination.Open()) throw std::runtime_error("APR: the read destination of " + file.path.string() + " is not writable guest memory"); +#ifdef _WIN32 + std::vector staging(static_cast(std::min(command.size, 64u * 1024u))); + std::uint64_t read = 0; + while (read < command.size) { + const auto wanted = static_cast(std::min(command.size - read, staging.size())); + stream.read(staging.data(), wanted); + const auto obtained = static_cast(stream.gcount()); + if (obtained != 0) std::memcpy(reinterpret_cast(command.destination) + read, staging.data(), static_cast(obtained)); + read += obtained; + if (stream.bad()) throw std::runtime_error("APR: read failed for " + file.path.string()); + if (obtained != static_cast(wanted)) break; + } +#else stream.read(reinterpret_cast(command.destination), static_cast(command.size)); if (stream.bad()) throw std::runtime_error("APR: read failed for " + file.path.string()); const auto read = static_cast(stream.gcount()); +#endif if (read != command.size && (!stream.eof() || command.offset + read < std::filesystem::file_size(file.path))) throw std::runtime_error("APR: read of " + file.path.string() + " at offset " + std::to_string(command.offset) + " returned " + std::to_string(read) + " of " + std::to_string(command.size) + " bytes"); if (read != command.size) std::memset(reinterpret_cast(command.destination) + read, 0, static_cast(command.size - read)); } @@ -260,8 +274,10 @@ TCommand _read(const Apr::CommandBufferObject& buffer, std::uint32_t cursor) { return command; } -constexpr std::uint64_t AmmRangeBytes = 32ull << 30; -constexpr std::uint64_t AmmRangeAlignment = 2ull << 20; +constexpr std::uint64_t AmmRangeBytes = 256ull << 30; +constexpr std::uint64_t AmmMultimapRangeBytes = 32ull << 30; +constexpr std::uint64_t AmmReservedBytes = AmmRangeBytes + AmmMultimapRangeBytes; +constexpr std::uint64_t AmmRangeAlignment = 64ull << 30; constexpr int GuestMapFixed = 0x10; constexpr int GuestMapNoOverwrite = 0x80; @@ -292,11 +308,11 @@ std::uintptr_t _ammBase(AmmState& state) { std::uintptr_t arenaBase = 0; std::size_t arenaBytes = 0; GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes); - if (arenaBase == 0 || arenaBytes < 2 * AmmRangeBytes) throw std::runtime_error("AMM: the guest address space arena cannot hold the virtual address range"); - const auto top = GuestArena::GuestArenaHighestFree_nid_no_patch(2 * AmmRangeBytes, AmmRangeAlignment); + if (arenaBase == 0 || arenaBytes < AmmReservedBytes) throw std::runtime_error("AMM: the guest address space arena cannot hold the virtual address range"); + const auto top = GuestArena::GuestArenaHighestFree_nid_no_patch(AmmReservedBytes, AmmRangeAlignment); if (top == 0) throw std::runtime_error("AMM: the guest address space arena has no free virtual address range"); void* address = reinterpret_cast(top); - if (DoReserveVirtual(&address, 2 * AmmRangeBytes, GuestMapFixed | GuestMapNoOverwrite, 0) != 0) throw std::runtime_error("AMM: cannot reserve the virtual address range"); + if (DoReserveVirtual(&address, AmmReservedBytes, GuestMapFixed | GuestMapNoOverwrite, 0) != 0) throw std::runtime_error("AMM: cannot reserve the virtual address range"); state.base = reinterpret_cast(address); } return state.base; @@ -304,7 +320,7 @@ std::uintptr_t _ammBase(AmmState& state) { void _ammCheckRange(AmmState& state, std::uint64_t address, std::uint64_t size) { const auto base = _ammBase(state); - if (address < base || size > 2 * AmmRangeBytes || address - base > 2 * AmmRangeBytes - size) throw std::runtime_error("AMM: range outside the AMM virtual address range"); + if (address < base || size > AmmReservedBytes || address - base > AmmReservedBytes - size) throw std::runtime_error("AMM: range outside the AMM virtual address range"); } int _ammHostProtection(std::int32_t protection) { @@ -704,7 +720,7 @@ void AmmVirtualAddressRanges_nid_no_patch(std::uint64_t* start, std::uint64_t* e if (start) *start = base; if (end) *end = base + AmmRangeBytes; if (multimapStart) *multimapStart = base + AmmRangeBytes; - if (multimapEnd) *multimapEnd = base + 2 * AmmRangeBytes; + if (multimapEnd) *multimapEnd = base + AmmReservedBytes; } struct AprQueue { diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index fbd7e7734e..1b9c5d5348 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -2,7 +2,9 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Apr/src/Apr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/Equeue.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/EdgeWatch.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/EventAccessors.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/ArchivedFile.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/DescriptorControl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/FileEvent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/Kqueue.cpp @@ -14,6 +16,8 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/File/src/OpenFileLimit.cpp ${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 @@ -46,6 +50,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 @@ -63,15 +68,18 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SystemConfiguration.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Login.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SoftwareVersion.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Error.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/ThermalSensors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Signals.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SignalSets.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Notification.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/UserContext.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Time/SleepWake.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/TimedWait.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/AppMetadata.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/ParamJsonParser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/ParamSfoParser.cpp ) target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}) @@ -79,22 +87,62 @@ 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) if(BUILD_TESTING) + add_test_executable(kernel_error_tests ${LIBS_INCLUDE_DIR}/tests/KernelError.cpp) + target_include_directories(kernel_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(kernel_error_tests PRIVATE libkernel libc) + configure_windows_unwind(kernel_error_tests) + add_test(NAME kernel_error COMMAND kernel_error_tests) + if(WIN32) + set_tests_properties(kernel_error PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_apr_read_tests ${LIBS_INCLUDE_DIR}/tests/GuestAprRead.cpp) + target_include_directories(guest_apr_read_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_apr_read_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_apr_read_tests) + set(aprReadCases host direct eof errors streams) + if(WIN32) + list(APPEND aprReadCases fault stack) + endif() + foreach(aprReadCase IN LISTS aprReadCases) + set(aprReadRun ${CMAKE_CURRENT_BINARY_DIR}/apr_read_${aprReadCase}) + file(MAKE_DIRECTORY ${aprReadRun}) + add_test(NAME guest_apr_read_${aprReadCase} COMMAND guest_apr_read_tests ${aprReadCase} + WORKING_DIRECTORY ${aprReadRun}) + set_tests_properties(guest_apr_read_${aprReadCase} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_apr_read_${aprReadCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + if(NOT WIN32) + add_library(guest_memory_lifetime_module MODULE ${LIBS_INCLUDE_DIR}/tests/GuestMemoryLifetimeModule.cpp) + target_include_directories(guest_memory_lifetime_module PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_memory_lifetime_module PRIVATE libkernel libc) + add_test_executable(guest_memory_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/GuestMemoryLifetime.cpp) + target_link_libraries(guest_memory_lifetime_tests PRIVATE ${CMAKE_DL_LIBS}) + add_dependencies(guest_memory_lifetime_tests guest_memory_lifetime_module) + add_test(NAME guest_memory_lifetime COMMAND guest_memory_lifetime_tests $) + set_tests_properties(guest_memory_lifetime PROPERTIES TIMEOUT 25) + endif() add_test_executable(download_data_mount_tests ${LIBS_INCLUDE_DIR}/tests/GuestDownloadDataMount.cpp) target_include_directories(download_data_mount_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(download_data_mount_tests PRIVATE libkernel libc) configure_windows_unwind(download_data_mount_tests) - foreach(downloadDataCase IN ITEMS declared:256 none:0 invalid:256 missing:0) + foreach(downloadDataCase IN ITEMS declared:256 none:0 invalid:256 missing:0 sfo:0) string(REPLACE ":" ";" downloadDataCase ${downloadDataCase}) list(GET downloadDataCase 0 downloadDataName) list(GET downloadDataCase 1 downloadDataSize) set(downloadDataRun ${CMAKE_CURRENT_BINARY_DIR}/download_data_mount_${downloadDataName}) - if(downloadDataName STREQUAL "missing") + if(downloadDataName STREQUAL "missing" OR downloadDataName STREQUAL "sfo") file(MAKE_DIRECTORY ${downloadDataRun}) elseif(downloadDataName STREQUAL "invalid") file(WRITE ${downloadDataRun}/app0/sce_sys/param.json "{\"downloadDataSize\":${downloadDataSize}}") @@ -119,6 +167,15 @@ if(BUILD_TESTING) set_tests_properties(param_json_parser PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(param_sfo_parser_tests ${LIBS_INCLUDE_DIR}/tests/ParamSfoParser.cpp) + target_include_directories(param_sfo_parser_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(param_sfo_parser_tests PRIVATE libkernel libc) + configure_windows_unwind(param_sfo_parser_tests) + add_test(NAME param_sfo_parser COMMAND param_sfo_parser_tests) + if(WIN32) + set_tests_properties(param_sfo_parser PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_posix_sem_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixSem.cpp) target_include_directories(guest_posix_sem_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_posix_sem_tests PRIVATE libkernel libc) @@ -183,7 +240,7 @@ if(BUILD_TESTING) endif() add_test(NAME guest_dynamic_loader COMMAND guest_dynamic_loader_tests $) set_tests_properties(guest_dynamic_loader PROPERTIES - WORKING_DIRECTORY "$") + WORKING_DIRECTORY "$" RESOURCE_LOCK guest_dynamic_loader_files) if(WIN32) set_tests_properties(guest_dynamic_loader PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -213,10 +270,34 @@ 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 "$") + WORKING_DIRECTORY "$" RESOURCE_LOCK guest_dynamic_loader_files) + endif() + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/variant_names") + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/variant_names/guest_module_fixture.sprx.GUEST.PRX" "not a module\n") + add_custom_command(TARGET guest_module_fixture POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy "$" + "${CMAKE_CURRENT_BINARY_DIR}/variant_names/guest_module_fixture.elf.GUEST.PRX" + VERBATIM) + if(NOT WIN32) + add_custom_command(TARGET guest_module_fixture POST_BUILD + COMMAND ${CMAKE_COMMAND} -E create_symlink missing.guest.prx + "${CMAKE_CURRENT_BINARY_DIR}/variant_names/guest_module_fixture.zprx.GUEST.PRX" + VERBATIM) + endif() + add_test_executable(guest_dynamic_loader_variants_tests ${LIBS_INCLUDE_DIR}/tests/GuestDynamicLoaderVariants.cpp) + target_include_directories(guest_dynamic_loader_variants_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_dynamic_loader_variants_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_dynamic_loader_variants_tests) + add_dependencies(guest_dynamic_loader_variants_tests guest_module_fixture) + add_test(NAME guest_dynamic_loader_variant_names COMMAND guest_dynamic_loader_variants_tests) + set_tests_properties(guest_dynamic_loader_variant_names PROPERTIES TIMEOUT 15 + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/variant_names") + if(WIN32) + set_tests_properties(guest_dynamic_loader_variant_names PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() add_test_executable(guest_module_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestModuleInfo.cpp) target_include_directories(guest_module_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -249,11 +330,35 @@ 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_path_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathErrors.cpp) + target_include_directories(guest_path_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_path_errors_tests PRIVATE libkernel libc) + add_test(NAME guest_path_errors COMMAND guest_path_errors_tests) + set_tests_properties(guest_path_errors PROPERTIES TIMEOUT 20) add_test_executable(guest_host_standard_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestHostStandardError.cpp) target_include_directories(guest_host_standard_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_host_standard_error_tests PRIVATE libkernel libc) add_test(NAME guest_host_standard_error COMMAND guest_host_standard_error_tests) set_tests_properties(guest_host_standard_error PROPERTIES TIMEOUT 10) + add_test_executable(guest_closed_standard_descriptors_tests ${LIBS_INCLUDE_DIR}/tests/GuestClosedStandardDescriptors.cpp) + target_include_directories(guest_closed_standard_descriptors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_closed_standard_descriptors_tests PRIVATE libkernel libc) + add_test(NAME guest_closed_standard_descriptors COMMAND guest_closed_standard_descriptors_tests) + set_tests_properties(guest_closed_standard_descriptors PROPERTIES TIMEOUT 10) + endif() + if(WIN32) + add_test_executable(guest_host_standard_error_windows_tests ${LIBS_INCLUDE_DIR}/tests/GuestHostStandardErrorWindows.cpp) + target_include_directories(guest_host_standard_error_windows_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_host_standard_error_windows_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_host_standard_error_windows_tests) + add_test(NAME guest_host_standard_error_windows COMMAND guest_host_standard_error_windows_tests) + set_tests_properties(guest_host_standard_error_windows PROPERTIES TIMEOUT 10 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() add_test_executable(guest_truncate_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncate.cpp) target_include_directories(guest_truncate_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -265,6 +370,26 @@ if(BUILD_TESTING) set_tests_properties(guest_truncate PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_truncate_race_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncateRace.cpp) + target_include_directories(guest_truncate_race_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_truncate_race_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_truncate_race_tests) + add_test(NAME guest_truncate_race COMMAND guest_truncate_race_tests) + set_tests_properties(guest_truncate_race PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_truncate_race PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_termios_speed_tests ${LIBS_INCLUDE_DIR}/tests/GuestTermiosSpeed.cpp) + target_include_directories(guest_termios_speed_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_termios_speed_tests PRIVATE libkernel libc ${CMAKE_DL_LIBS}) + configure_windows_unwind(guest_termios_speed_tests) + add_test(NAME guest_termios_speed COMMAND guest_termios_speed_tests $) + if(WIN32) + set_tests_properties(guest_termios_speed PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_terminal_tests ${LIBS_INCLUDE_DIR}/tests/GuestTerminal.cpp) target_include_directories(guest_terminal_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_terminal_tests PRIVATE libkernel libc) @@ -284,6 +409,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) @@ -341,10 +485,21 @@ if(BUILD_TESTING) configure_windows_unwind(guest_kernel_is_stack_tests) add_test(NAME guest_kernel_is_stack COMMAND guest_kernel_is_stack_tests) set_tests_properties(guest_kernel_is_stack PROPERTIES TIMEOUT 20) + add_asan_test_options(guest_kernel_is_stack detect_stack_use_after_return=0) if(WIN32) set_tests_properties(guest_kernel_is_stack PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_app_icon_tests ${LIBS_INCLUDE_DIR}/tests/GuestAppIcon.cpp) + target_include_directories(guest_app_icon_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_app_icon_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_app_icon_tests) + add_test(NAME guest_app_icon COMMAND guest_app_icon_tests) + set_tests_properties(guest_app_icon PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_app_icon PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_app_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestAppInfo.cpp) target_include_directories(guest_app_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_app_info_tests PRIVATE libkernel libc) @@ -361,6 +516,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:$") @@ -368,9 +524,14 @@ if(BUILD_TESTING) add_test_executable(guest_thread_self_tests ${LIBS_INCLUDE_DIR}/tests/GuestThreadSelf.cpp) target_include_directories(guest_thread_self_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_thread_self_tests PRIVATE libkernel libc) + if(WIN32) + target_compile_definitions(guest_thread_self_tests PRIVATE _WIN32_WINNT=0x0602) + target_link_options(guest_thread_self_tests PRIVATE "SHELL:-Xlinker --stack=0x210000,0x200000") + endif() 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:$") @@ -415,6 +576,27 @@ 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_frame_chain_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadFrameChain.cpp) + target_include_directories(guest_pthread_frame_chain_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_compile_options(guest_pthread_frame_chain_tests PRIVATE -fno-omit-frame-pointer) + target_link_libraries(guest_pthread_frame_chain_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_frame_chain_tests) + add_test(NAME guest_pthread_frame_chain COMMAND guest_pthread_frame_chain_tests) + set_tests_properties(guest_pthread_frame_chain PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_frame_chain 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) @@ -425,6 +607,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_mutexattr PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_destroyed_objects_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadDestroyedObjects.cpp) + target_include_directories(guest_pthread_destroyed_objects_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_destroyed_objects_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_destroyed_objects_tests) + add_test(NAME guest_pthread_destroyed_objects COMMAND guest_pthread_destroyed_objects_tests) + set_tests_properties(guest_pthread_destroyed_objects PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_destroyed_objects PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_mutex_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadMutexErrors.cpp) target_include_directories(guest_pthread_mutex_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_mutex_errors_tests PRIVATE libkernel libc) @@ -445,6 +637,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_start PROPERTIES TIMEOUT 60 RUN_SERIAL TRUE ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_join_detach_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadJoinDetach.cpp) + target_include_directories(guest_pthread_join_detach_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_join_detach_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_join_detach_tests) + add_test(NAME guest_pthread_join_detach COMMAND guest_pthread_join_detach_tests) + set_tests_properties(guest_pthread_join_detach PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_join_detach PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_attr_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadAttrErrors.cpp) target_include_directories(guest_pthread_attr_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_attr_errors_tests PRIVATE libkernel libc) @@ -461,6 +663,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:$") @@ -475,6 +678,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) @@ -535,6 +748,16 @@ if(BUILD_TESTING) set_tests_properties(guest_thread_cpu_clock PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_socket_descriptor_flags_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketDescriptorFlags.cpp) + target_include_directories(guest_socket_descriptor_flags_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_descriptor_flags_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_descriptor_flags_tests) + add_test(NAME guest_socket_descriptor_flags COMMAND guest_socket_descriptor_flags_tests) + set_tests_properties(guest_socket_descriptor_flags PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_socket_descriptor_flags PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_nanosleep_tests ${LIBS_INCLUDE_DIR}/tests/GuestNanosleep.cpp) target_include_directories(guest_nanosleep_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_nanosleep_tests PRIVATE libkernel libc) @@ -544,6 +767,31 @@ 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_sleep_wake_tests ${LIBS_INCLUDE_DIR}/tests/GuestSleepWake.cpp) + target_include_directories(guest_sleep_wake_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sleep_wake_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sleep_wake_tests) + add_test(NAME guest_sleep_wake COMMAND guest_sleep_wake_tests) + set_tests_properties(guest_sleep_wake PROPERTIES TIMEOUT 20) + if(WIN32) + add_test(NAME guest_sleep_wake_coarse COMMAND guest_sleep_wake_tests) + set_tests_properties(guest_sleep_wake_coarse PROPERTIES TIMEOUT 20 ENVIRONMENT "APS5_COARSE_TIMED_WAITS=1") + set_tests_properties(guest_sleep_wake guest_sleep_wake_coarse PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_nanosleep_long_request_tests ${LIBS_INCLUDE_DIR}/tests/GuestNanosleepLongRequest.cpp) + target_include_directories(guest_nanosleep_long_request_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_nanosleep_long_request_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_nanosleep_long_request_tests) + add_test(NAME guest_nanosleep_long_request COMMAND guest_nanosleep_long_request_tests) + set_tests_properties(guest_nanosleep_long_request PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_nanosleep_long_request PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() add_test_executable(guest_pthread_sem_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSem.cpp) target_include_directories(guest_pthread_sem_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -583,10 +831,44 @@ if(BUILD_TESTING) configure_windows_unwind(guest_semaphore_tests) add_test(NAME guest_semaphore COMMAND guest_semaphore_tests) set_tests_properties(guest_semaphore PROPERTIES TIMEOUT 20) + if(Python3_Interpreter_FOUND) + add_test_executable(semaphore_trace_tests + ${LIBS_INCLUDE_DIR}/tests/SemaphoreTrace.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Semaphore/src/Semaphore.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Time/TimedWait.cpp) + target_include_directories(semaphore_trace_tests PRIVATE ${LIBS_INCLUDE_DIR}) + find_package(Threads REQUIRED) + target_link_libraries(semaphore_trace_tests PRIVATE Threads::Threads libc) + add_test(NAME guest_semaphore_trace COMMAND ${Python3_EXECUTABLE} + ${LIBS_INCLUDE_DIR}/tests/test_semaphore_trace.py $) + set_tests_properties(guest_semaphore_trace PROPERTIES TIMEOUT 20) + endif() if(WIN32) - set_tests_properties(guest_semaphore PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_semaphore guest_semaphore_trace 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_sem_canceled_wake_tests ${LIBS_INCLUDE_DIR}/tests/GuestSemCanceledWake.cpp) + target_include_directories(guest_sem_canceled_wake_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sem_canceled_wake_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sem_canceled_wake_tests) + add_test(NAME guest_sem_canceled_wake COMMAND guest_sem_canceled_wake_tests) + add_test(NAME guest_sem_canceled_wake_coarse COMMAND guest_sem_canceled_wake_tests) + set_tests_properties(guest_sem_canceled_wake guest_sem_canceled_wake_coarse PROPERTIES TIMEOUT 20) + set_tests_properties(guest_sem_canceled_wake_coarse PROPERTIES ENVIRONMENT "APS5_COARSE_TIMED_WAITS=1") + if(WIN32) + set_tests_properties(guest_sem_canceled_wake guest_sem_canceled_wake_coarse 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) @@ -597,6 +879,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_rwlock PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_rwlock_retire_tests ${LIBS_INCLUDE_DIR}/tests/GuestRwlockRetire.cpp) + target_include_directories(guest_rwlock_retire_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_rwlock_retire_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_rwlock_retire_tests) + add_test(NAME guest_rwlock_retire COMMAND guest_rwlock_retire_tests) + set_tests_properties(guest_rwlock_retire PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_rwlock_retire PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_join_self_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadJoinSelf.cpp) target_include_directories(guest_pthread_join_self_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_join_self_tests PRIVATE libkernel libc) @@ -607,6 +899,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) @@ -629,6 +931,7 @@ if(BUILD_TESTING) endif() add_test_executable(guest_user_context_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserContext.cpp) target_include_directories(guest_user_context_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_compile_options(guest_user_context_tests PRIVATE -fno-omit-frame-pointer) target_link_libraries(guest_user_context_tests PRIVATE libSceLibcInternal libkernel libc) configure_windows_unwind(guest_user_context_tests) add_test(NAME guest_user_context COMMAND guest_user_context_tests) @@ -662,8 +965,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) @@ -727,7 +1033,8 @@ if(BUILD_TESTING) target_link_libraries(guest_raise_exception_tests PRIVATE libkernel libc) 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) + set_tests_properties(guest_raise_exception PROPERTIES TIMEOUT 60) + 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:$") @@ -918,6 +1225,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) @@ -1047,6 +1364,26 @@ if(BUILD_TESTING) set_tests_properties(guest_socket_options PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_socket_stream_options_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketStreamOptions.cpp) + target_include_directories(guest_socket_stream_options_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_stream_options_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_stream_options_tests) + add_test(NAME guest_socket_stream_options COMMAND guest_socket_stream_options_tests) + set_tests_properties(guest_socket_stream_options PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_socket_stream_options PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_socket_timeouts_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketTimeouts.cpp) + target_include_directories(guest_socket_timeouts_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_timeouts_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_timeouts_tests) + add_test(NAME guest_socket_timeouts COMMAND guest_socket_timeouts_tests) + set_tests_properties(guest_socket_timeouts PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_socket_timeouts PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_udp_tests ${LIBS_INCLUDE_DIR}/tests/GuestUdp.cpp) target_include_directories(guest_udp_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_udp_tests PRIVATE libkernel libc) @@ -1057,6 +1394,16 @@ if(BUILD_TESTING) set_tests_properties(guest_udp PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_socket_address_length_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketAddressLength.cpp) + target_include_directories(guest_socket_address_length_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_address_length_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_address_length_tests) + add_test(NAME guest_socket_address_length COMMAND guest_socket_address_length_tests) + set_tests_properties(guest_socket_address_length PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_socket_address_length PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_socket_read_write_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketReadWrite.cpp) target_include_directories(guest_socket_read_write_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_socket_read_write_tests PRIVATE libkernel libc) @@ -1108,14 +1455,47 @@ if(BUILD_TESTING) set_tests_properties(guest_tcp PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_socket_port_zero_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketPortZero.cpp) + target_include_directories(guest_socket_port_zero_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_port_zero_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_port_zero_tests) + add_test(NAME guest_socket_port_zero COMMAND guest_socket_port_zero_tests) + set_tests_properties(guest_socket_port_zero PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_socket_port_zero PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_socket_pair_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketPair.cpp) target_include_directories(guest_socket_pair_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_socket_pair_tests PRIVATE libkernel libc) configure_windows_unwind(guest_socket_pair_tests) add_test(NAME guest_socket_pair COMMAND guest_socket_pair_tests) - set_tests_properties(guest_socket_pair PROPERTIES TIMEOUT 20) + add_test(NAME guest_recvmsg_memory COMMAND guest_socket_pair_tests recvmsg-memory) + set_tests_properties(guest_socket_pair guest_recvmsg_memory PROPERTIES TIMEOUT 20) if(WIN32) - set_tests_properties(guest_socket_pair PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_socket_pair guest_recvmsg_memory PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_kernel_app_archive_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelAppArchive.cpp) + 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_socket_on_non_socket_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketOnNonSocket.cpp) + target_include_directories(guest_socket_on_non_socket_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_on_non_socket_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_on_non_socket_tests) + add_test(NAME guest_socket_on_non_socket COMMAND guest_socket_on_non_socket_tests) + set_tests_properties(guest_socket_on_non_socket PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_socket_on_non_socket PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_dup_tests ${LIBS_INCLUDE_DIR}/tests/GuestDup.cpp) @@ -1135,6 +1515,7 @@ if(BUILD_TESTING) add_test(NAME guest_kqueue COMMAND guest_kqueue_tests) set_tests_properties(guest_kqueue PROPERTIES TIMEOUT 20) if(WIN32) + target_link_libraries(guest_kqueue_tests PRIVATE advapi32) set_tests_properties(guest_kqueue PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() @@ -1148,6 +1529,16 @@ if(BUILD_TESTING) set_tests_properties(guest_socket_descriptors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_host_stream_tests ${LIBS_INCLUDE_DIR}/tests/GuestHostStreams.cpp) + target_include_directories(guest_host_stream_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_host_stream_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_host_stream_tests) + add_test(NAME guest_host_streams COMMAND guest_host_stream_tests) + set_tests_properties(guest_host_streams PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_host_streams PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_socket_event_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketEvents.cpp) target_include_directories(guest_socket_event_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_socket_event_tests PRIVATE libkernel libc) @@ -1158,6 +1549,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) @@ -1204,8 +1605,29 @@ if(BUILD_TESTING) target_compile_definitions(guest_memory_tests PRIVATE GUEST_MEMORY_ELF0_MODULE="$") endif() add_test(NAME guest_memory COMMAND guest_memory_tests) + add_test(NAME guest_mmap_fixed_anonymous COMMAND guest_memory_tests fixed-anonymous) if(WIN32) - set_tests_properties(guest_memory PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_memory guest_mmap_fixed_anonymous PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_mprotect_unmapped_tests ${LIBS_INCLUDE_DIR}/tests/GuestMprotectUnmapped.cpp) + target_include_directories(guest_mprotect_unmapped_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_mprotect_unmapped_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_mprotect_unmapped_tests) + add_test(NAME guest_mprotect_unmapped COMMAND guest_mprotect_unmapped_tests) + set_tests_properties(guest_mprotect_unmapped PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_mprotect_unmapped PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_protection_report_tests ${LIBS_INCLUDE_DIR}/tests/GuestProtectionReport.cpp) + target_include_directories(guest_protection_report_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_protection_report_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_protection_report_tests) + add_test(NAME guest_protection_report COMMAND guest_protection_report_tests) + set_tests_properties(guest_protection_report PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_protection_report PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_flexible_memory_budget_tests ${LIBS_INCLUDE_DIR}/tests/GuestFlexibleMemoryBudget.cpp) @@ -1284,12 +1706,24 @@ if(BUILD_TESTING) set_tests_properties(guest_directory_rewind PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - if(NOT WIN32) - add_test_executable(guest_directory_entry_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryEntries.cpp) - target_include_directories(guest_directory_entry_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_directory_entry_tests PRIVATE libkernel libc) - add_test(NAME guest_directory_entries COMMAND guest_directory_entry_tests) - else() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_directory_link_type_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryLinkTypes.cpp) + target_include_directories(guest_directory_link_type_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_directory_link_type_tests PRIVATE libkernel libc) + add_test(NAME guest_directory_link_types COMMAND guest_directory_link_type_tests) + set_tests_properties(guest_directory_link_types PROPERTIES TIMEOUT 15) + endif() + add_test_executable(guest_directory_entry_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryEntries.cpp) + target_include_directories(guest_directory_entry_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_directory_entry_tests PRIVATE libkernel libc) + add_test(NAME guest_directory_entries COMMAND guest_directory_entry_tests) + if(WIN32) + configure_windows_unwind(guest_directory_entry_tests) + set_tests_properties(guest_directory_entries PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + if(MINGW) + target_compile_options(guest_directory_entry_tests PRIVATE -fasynchronous-unwind-tables) + endif() add_test_executable(guest_directory_close_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryClose.cpp) target_include_directories(guest_directory_close_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_directory_close_tests PRIVATE libkernel libc) @@ -1300,6 +1734,26 @@ 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_path_name_length_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathNameLength.cpp) + target_include_directories(guest_path_name_length_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_path_name_length_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_path_name_length_tests) + add_test(NAME guest_path_name_length COMMAND guest_path_name_length_tests) + set_tests_properties(guest_path_name_length PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_path_name_length PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() add_test_executable(guest_file_vector_tests ${LIBS_INCLUDE_DIR}/tests/GuestFileVector.cpp) target_include_directories(guest_file_vector_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -1310,6 +1764,46 @@ if(BUILD_TESTING) set_tests_properties(guest_file_vector PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_file_vector_memory_tests ${LIBS_INCLUDE_DIR}/tests/GuestFileVectorMemory.cpp) + target_include_directories(guest_file_vector_memory_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_file_vector_memory_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_file_vector_memory_tests) + add_test(NAME guest_file_vector_memory COMMAND guest_file_vector_memory_tests) + set_tests_properties(guest_file_vector_memory PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_file_vector_memory PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_fstat_fault_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatFault.cpp) + target_include_directories(guest_fstat_fault_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fstat_fault_tests PRIVATE libkernel libc) + 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_rename_dot_tests ${LIBS_INCLUDE_DIR}/tests/GuestRenameDot.cpp) + target_include_directories(guest_rename_dot_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_rename_dot_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_rename_dot_tests) + add_test(NAME guest_rename_dot COMMAND guest_rename_dot_tests) + set_tests_properties(guest_rename_dot PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_rename_dot PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_truncate_extend_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncateExtend.cpp) + target_include_directories(guest_truncate_extend_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_truncate_extend_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_truncate_extend_tests) + add_test(NAME guest_truncate_extend COMMAND guest_truncate_extend_tests) + set_tests_properties(guest_truncate_extend PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_truncate_extend PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_fstatfs_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatfs.cpp) target_include_directories(guest_fstatfs_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_fstatfs_tests PRIVATE libkernel libc) @@ -1320,20 +1814,36 @@ if(BUILD_TESTING) set_tests_properties(guest_fstatfs PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_append_streams_tests ${LIBS_INCLUDE_DIR}/tests/GuestAppendStreams.cpp) + target_include_directories(guest_append_streams_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_append_streams_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_append_streams_tests) + add_test(NAME guest_append_streams COMMAND guest_append_streams_tests) + set_tests_properties(guest_append_streams PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_append_streams PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$;PATH=path_list_prepend:$") + endif() add_test_executable(guest_kernel_fcntl_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelFcntl.cpp) target_include_directories(guest_kernel_fcntl_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_kernel_fcntl_tests PRIVATE libkernel libc) 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}) target_link_libraries(guest_file_lock_tests PRIVATE libkernel libc) configure_windows_unwind(guest_file_lock_tests) - foreach(fileLockCase IN ITEMS duplicate lifetime conversion threads streams process) + foreach(fileLockCase IN ITEMS duplicate lifetime conversion threads streams process directory) add_test(NAME guest_file_lock_${fileLockCase} COMMAND guest_file_lock_tests ${fileLockCase}) set_tests_properties(guest_file_lock_${fileLockCase} PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -1355,6 +1865,20 @@ if(BUILD_TESTING) add_test(NAME guest_mutex_raise_exception COMMAND guest_mutex_raise_exception_tests) set_tests_properties(guest_mutex_raise_exception PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test_executable(guest_raise_in_system_call_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseInSystemCall.cpp) + target_include_directories(guest_raise_in_system_call_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_raise_in_system_call_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_raise_in_system_call_tests) + add_test(NAME guest_raise_in_system_call COMMAND guest_raise_in_system_call_tests) + set_tests_properties(guest_raise_in_system_call PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + add_test_executable(guest_raise_in_host_code_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseInHostCode.cpp) + target_include_directories(guest_raise_in_host_code_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_raise_in_host_code_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_raise_in_host_code_tests) + add_test(NAME guest_raise_in_host_code COMMAND guest_raise_in_host_code_tests) + set_tests_properties(guest_raise_in_host_code PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") add_test_executable(guest_mutex_contention_tests ${LIBS_INCLUDE_DIR}/tests/GuestMutexContention.cpp) target_include_directories(guest_mutex_contention_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_mutex_contention_tests PRIVATE libkernel libc) @@ -1394,3 +1918,20 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_sleep_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestSleepCancel.cpp) + target_include_directories(guest_sleep_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sleep_cancel_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sleep_cancel_tests) + foreach(sleepCase IN ITEMS blocked disabled normal pending) + add_test(NAME guest_sleep_cancel_${sleepCase} COMMAND guest_sleep_cancel_tests ${sleepCase}) + add_test(NAME guest_sleep_cancel_${sleepCase}_coarse COMMAND guest_sleep_cancel_tests ${sleepCase}) + set_tests_properties(guest_sleep_cancel_${sleepCase} guest_sleep_cancel_${sleepCase}_coarse PROPERTIES TIMEOUT 20) + set_tests_properties(guest_sleep_cancel_${sleepCase}_coarse PROPERTIES ENVIRONMENT "APS5_COARSE_TIMED_WAITS=1") + if(WIN32) + set_tests_properties(guest_sleep_cancel_${sleepCase} guest_sleep_cancel_${sleepCase}_coarse PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() +endif() diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index a328556d73..82e570403e 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,7 @@ #if defined(__linux__) #include +#include #include #include #include @@ -31,6 +33,7 @@ #include #else #include +#include "prx/libc/include/WindowsDescriptor.hpp" static constexpr int PROT_NONE = 0; static constexpr int PROT_READ = 1; @@ -57,6 +60,7 @@ struct KernelArena { return arena; } bool Contains(const void* pointer, size_t len) const { return GuestArena::GuestArenaContains_nid_postfix(pointer, len); } + bool Owns(const void* pointer, size_t len) const { return Contains(pointer, len) && !GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(reinterpret_cast(pointer), len); } void* Allocate(size_t len, size_t alignment) { return GuestArena::GuestArenaAllocate_nid_postfix(len, alignment); } void MarkUsed(const void* pointer, size_t len) { GuestArena::GuestArenaMarkUsed_nid_postfix(pointer, len); } void Release(const void* pointer, size_t len) { GuestArena::GuestArenaRelease_nid_postfix(pointer, len); } @@ -68,7 +72,7 @@ static void CommitArenaRange(void* addr, size_t len, DWORD winProt) { static void* mmap_aligned(size_t len, int prot, size_t alignment, std::uintptr_t hint = 0) { auto& arena = KernelArena::Get(); - void* result = GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(hint, len, alignment); + void* result = GuestArena::GuestArenaAllocateFromHint_nid_postfix(hint, len, alignment); if (prot != PROT_NONE) { try { CommitArenaRange(result, len, WinProtFromPosix(prot)); @@ -117,9 +121,42 @@ static int munmap_release(void* addr) { return 0; } +static void CommitReservedPages(void* addr, size_t len) { + auto cursor = reinterpret_cast(addr); + const auto end = cursor + len; + while (cursor < end) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) != sizeof(memory)) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualQuery failed"); + const auto regionEnd = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); + if (memory.State == MEM_RESERVE && !VirtualAlloc(reinterpret_cast(cursor), regionEnd - cursor, MEM_COMMIT, PAGE_READWRITE)) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualAlloc stack commit failed"); + cursor = regionEnd; + } +} + +static std::uintptr_t AboveStackGuard(std::uintptr_t start, std::uintptr_t end) { + auto cursor = start; + while (cursor < end) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) != sizeof(memory)) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualQuery failed"); + if (memory.State != MEM_RESERVE && (memory.State != MEM_COMMIT || (memory.Protect & PAGE_GUARD) == 0)) break; + cursor = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); + } + return cursor; +} + +static bool AtStackReserveBottom(std::uintptr_t start, std::uintptr_t end) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(start), &memory, sizeof(memory)) != sizeof(memory)) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualQuery failed"); + return memory.State == MEM_RESERVE && reinterpret_cast(memory.AllocationBase) == start && reinterpret_cast(memory.BaseAddress) + memory.RegionSize > end; +} + static int mprotect(void* addr, size_t len, int prot) { GuestArena::GuestArenaSetProtection_nid_postfix(reinterpret_cast(addr), len, WinProtFromPosix(prot)); - if (prot != PROT_NONE && KernelArena::Get().Contains(addr, len)) CommitArenaRange(addr, len, WinProtFromPosix(prot)); + if (prot != PROT_NONE && KernelArena::Get().Owns(addr, len)) CommitArenaRange(addr, len, WinProtFromPosix(prot)); auto cursor = reinterpret_cast(addr); const auto end = cursor + len; while (cursor < end) { @@ -140,6 +177,32 @@ static int mprotect(void* addr, size_t len, int prot) { } #endif +#if defined(__linux__) +static void MapUnmappedPages(void* addr, size_t len) { + constexpr std::uintptr_t hostPage = 0x1000; + auto cursor = reinterpret_cast(addr); + const auto end = cursor + len; + const auto half = [&](std::size_t bytes) { return std::max(hostPage, (bytes / 2) & ~(hostPage - 1)); }; + while (cursor < end) { + auto* run = reinterpret_cast(cursor); + auto bytes = static_cast(end - cursor); + if (::msync(run, hostPage, MS_ASYNC) == 0) { + while (bytes > hostPage && ::msync(run, bytes, MS_ASYNC) != 0) bytes = half(bytes); + cursor += bytes; + continue; + } + void* mapped = MAP_FAILED; + while ((mapped = ::mmap(run, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE | MAP_NORESERVE, -1, 0)) == MAP_FAILED && errno == EEXIST && bytes > hostPage) bytes = half(bytes); + if (mapped != run) { + const int error = mapped == MAP_FAILED ? errno : EEXIST; + if (mapped != MAP_FAILED) ::munmap(mapped, bytes); + throw std::system_error(error, std::generic_category(), "cannot map the unmapped pages of a host stack"); + } + cursor += bytes; + } +} +#endif + namespace { constexpr int GuestMapFixedFlag = 0x10; @@ -241,19 +304,19 @@ int PhysicalFile() { return file; } -std::vector g_physicalUsed(DIRECT_MEMORY_SIZE / PS5_PAGE_SIZE); +std::vector& PhysicalUsed(); void ZeroReusedPhysical(std::uint64_t start, std::size_t bytes) { const auto first = start / PS5_PAGE_SIZE; const auto count = bytes / PS5_PAGE_SIZE; for (std::size_t page = 0; page < count;) { - if (!g_physicalUsed[first + page]) { - g_physicalUsed[first + page] = true; + if (!PhysicalUsed()[first + page]) { + PhysicalUsed()[first + page] = true; ++page; continue; } std::size_t run = 0; - while (page + run < count && g_physicalUsed[first + page + run]) ++run; + while (page + run < count && PhysicalUsed()[first + page + run]) ++run; const auto offset = (first + page) * PS5_PAGE_SIZE; void* view = ::mmap(nullptr, run * PS5_PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, PhysicalFile(), static_cast(offset)); if (view == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map reused direct memory"); @@ -264,13 +327,63 @@ void ZeroReusedPhysical(std::uint64_t start, std::size_t bytes) { } #endif +#ifdef _WIN32 +bool SectionPoolEnabled() { + static const bool enabled = std::getenv("APS5_NO_DIRECT_MEMORY_SECTION_POOL") == nullptr; + return enabled; +} + +class SectionPool { +public: + static SectionPool& Get() { + static auto* pool = new SectionPool; + return *pool; + } + + HANDLE Take(std::size_t bytes) { + std::lock_guard lock(mutex); + const auto found = sections.find(bytes); + if (found == sections.end() || found->second.empty()) return nullptr; + const auto section = found->second.back(); + found->second.pop_back(); + --count; + pooledBytes -= bytes; + return section; + } + + bool Keep(HANDLE section, std::size_t bytes) noexcept { + if (bytes > MaxSectionBytes) return false; + std::lock_guard lock(mutex); + if (count == MaxSections || bytes > MaxPooledBytes - pooledBytes) return false; + try { + sections[bytes].push_back(section); + } catch (const std::bad_alloc&) { + return false; + } + ++count; + pooledBytes += bytes; + return true; + } + +private: + static constexpr std::size_t MaxSections = 256; + static constexpr std::size_t MaxSectionBytes = 16u << 20; + static constexpr std::size_t MaxPooledBytes = 256u << 20; + std::mutex mutex; + std::map> sections; + std::size_t count = 0; + std::size_t pooledBytes = 0; +}; +#endif + class PhysicalBacking { public: PhysicalBacking(std::size_t bytes, int memoryType, std::uint64_t start) : memoryType(memoryType), bytes(bytes) { #ifdef _WIN32 static_cast(start); const auto size = static_cast(bytes); - section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, static_cast(size >> 32), static_cast(size), nullptr); + if (SectionPoolEnabled()) section = SectionPool::Get().Take(bytes); + if (!section) section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, static_cast(size >> 32), static_cast(size), nullptr); if (!section) { const auto error = static_cast(GetLastError()); char message[96]; @@ -302,8 +415,12 @@ public: ~PhysicalBacking() { #ifdef _WIN32 - GuestArena::GuestArenaForgetSection_nid_postfix(section); - CloseHandle(section); + if (!SectionPoolEnabled()) { + GuestArena::GuestArenaForgetSection_nid_postfix(section); + CloseHandle(section); + } else if (!GuestArena::GuestArenaForgetReusableSection_nid_no_patch(section) || !SectionPool::Get().Keep(section, bytes)) { + CloseHandle(section); + } #else if (hostWriteView != nullptr) ::munmap(hostWriteView, bytes); #endif @@ -337,9 +454,6 @@ struct PhysicalPage { std::uint64_t offset; }; -std::mutex g_directLock; -std::map g_physPages; - struct DirectMapping { std::uintptr_t end; std::uint64_t phys; @@ -347,13 +461,47 @@ struct DirectMapping { std::shared_ptr backing; }; -std::map g_directMappings; struct PhysicalSpan { std::uint64_t end; std::vector bases; }; -std::map g_physicalSpans; +struct ProtectedRange { + std::uintptr_t end; + int prot; +}; + +struct DirectMemoryState { +#if defined(__linux__) + std::vector physicalUsed = std::vector(DIRECT_MEMORY_SIZE / PS5_PAGE_SIZE); +#endif + std::map physicalSpans; + std::mutex directLock; + std::map physPages; + std::map directMappings; + std::mutex protectionLock; + std::map protections; + std::mutex reservationLock; + std::map reservations; +}; + +DirectMemoryState& MemoryState() { + static auto* state = new DirectMemoryState; + return *state; +} + +#if defined(__linux__) +std::vector& PhysicalUsed() { return MemoryState().physicalUsed; } +#endif + +auto& g_directLock = MemoryState().directLock; +auto& g_physPages = MemoryState().physPages; +auto& g_directMappings = MemoryState().directMappings; +auto& g_physicalSpans = MemoryState().physicalSpans; +auto& g_protectionLock = MemoryState().protectionLock; +auto& g_protections = MemoryState().protections; +auto& g_reservationLock = MemoryState().reservationLock; +auto& g_reservations = MemoryState().reservations; void SplitPhysicalSpan(std::uint64_t at) { auto span = g_physicalSpans.upper_bound(at); @@ -444,7 +592,6 @@ void ErasePhysMappings(std::uint64_t first, std::uint64_t last) { } } - void ValidatePhysicalRange(std::uint64_t phys, std::size_t len) { for (std::size_t offset = 0; offset < len; offset += PS5_PAGE_SIZE) { if (!g_physPages.contains(phys + offset)) { @@ -570,7 +717,8 @@ void RequireAliasesUnpinned(GuestAllocations::Mutation& mutation, std::uint64_t std::vector> aliases; { std::lock_guard lock(g_directLock); - for (const auto& [base, mapping] : g_directMappings) { + for (const auto base : MappingsSharingPhys(phys, phys + len)) { + const auto& mapping = g_directMappings.at(base); const auto first = std::max(phys, mapping.phys); const auto last = std::min(phys + len, mapping.phys + (mapping.end - base)); if (first < last) aliases.emplace_back(base + (first - mapping.phys), last - first); @@ -701,7 +849,7 @@ void* MapPlaced(void* addr, size_t len, int prot, int flags, size_t alignment) { if (addr) { #if defined(__linux__) if (ArenaAvailable()) { - void* placed = GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix( + void* placed = GuestArena::GuestArenaAllocateFromHint_nid_postfix( reinterpret_cast(addr), len, alignment); if (void* committed = ArenaCommit(placed, len, prot); committed != MAP_FAILED) return committed; ArenaForget(placed, len); @@ -749,28 +897,18 @@ void* MapPlaced(void* addr, size_t len, int prot, int flags, size_t alignment) { return aligned; } -struct ProtectedRange { - std::uintptr_t end; - int prot; -}; -std::mutex g_protectionLock; -std::map g_protections; - -void EraseProtections(std::uintptr_t start, std::uintptr_t end) { - auto it = g_protections.lower_bound(start); - if (it != g_protections.begin() && std::prev(it)->second.end > start) --it; - while (it != g_protections.end() && it->first < end) { +void EraseProtections(std::uintptr_t start, std::uintptr_t end, std::map& protections = g_protections) { + auto it = protections.lower_bound(start); + if (it != protections.begin() && std::prev(it)->second.end > start) --it; + while (it != protections.end() && it->first < end) { const auto rangeStart = it->first; const auto range = it->second; - it = g_protections.erase(it); - if (rangeStart < start) g_protections.emplace(rangeStart, ProtectedRange{start, range.prot}); - if (range.end > end) it = g_protections.emplace(end, ProtectedRange{range.end, range.prot}).first; + it = protections.erase(it); + if (rangeStart < start) protections.emplace(rangeStart, ProtectedRange{start, range.prot}); + if (range.end > end) it = protections.emplace(end, ProtectedRange{range.end, range.prot}).first; } } -std::mutex g_reservationLock; -std::map g_reservations; - void EraseReservations(const void* addr, size_t len) { const auto start = reinterpret_cast(addr); const auto end = start + len; @@ -793,11 +931,18 @@ void RecordReservation(const void* addr, size_t len) { g_reservations.emplace(start, start + len); } +int ReportedProtection(int prot) { + if ((prot & 0x2) != 0) prot |= 0x1; + if ((prot & 0x80) != 0) prot |= 0x40; + if ((prot & 0x200) != 0) prot |= 0x100; + return prot; +} + void RecordProtection(const void* addr, size_t len, int prot) { const auto start = reinterpret_cast(addr); std::lock_guard lock(g_protectionLock); EraseProtections(start, start + len); - if (prot >= 0) g_protections.emplace(start, ProtectedRange{start + len, prot}); + if (prot >= 0) g_protections.emplace(start, ProtectedRange{start + len, ReportedProtection(prot)}); } void* MapAligned(void* addr, size_t len, int prot, int flags, size_t alignment) { @@ -902,10 +1047,58 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, return 0; } -int DoMapAnon(void** addr, size_t len, int prot, int flags, size_t alignment) { +int DoMapAnon(void** addr, size_t len, int prot, int flags, size_t alignment, bool exclusive, [[maybe_unused]] bool allowStackReplacement) { ValidateOutput(addr); if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; +#ifdef _WIN32 + if (allowStackReplacement && (flags & GuestMapFixedFlag) != 0 && !KernelArena::Get().Owns(*addr, len)) { + constexpr int GuestMapNoOverwrite = 0x80; + if ((prot & ~7) != 0 || (flags & ~(GuestMapFixedFlag | GuestMapNoOverwrite)) != 0) + return SCE_KERNEL_ERROR_EINVAL; + ValidateRange(*addr, len, ValidateAlignment(alignment)); + const auto start = reinterpret_cast(*addr); + int result = SCE_KERNEL_ERROR_EINVAL; + ReplaceGuestThreadStack(start, len, (prot & 3) == 3, [&] { + if (exclusive || (flags & GuestMapNoOverwrite) != 0) { + result = SCE_KERNEL_ERROR_ENOMEM; + return false; + } + void* allocation = nullptr; + for (auto cursor = start; cursor < start + len;) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) != sizeof(memory) || + (memory.State != MEM_COMMIT && memory.State != MEM_RESERVE) || memory.Type != MEM_PRIVATE || + (memory.Protect & PAGE_GUARD) != 0 || + memory.RegionSize == 0 || reinterpret_cast(memory.BaseAddress) > cursor || + memory.RegionSize > std::numeric_limits::max() - reinterpret_cast(memory.BaseAddress)) + return false; + if (allocation == nullptr) allocation = memory.AllocationBase; + if (memory.AllocationBase != allocation) return false; + const auto next = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; + if (next <= cursor) return false; + cursor = std::min(start + len, next); + } + std::lock_guard lock(g_protectionLock); + auto replacement = g_protections; + EraseProtections(start, start + len, replacement); + const auto recorded = replacement.emplace(start, ProtectedRange{start + len, ReportedProtection(prot)}).first; + result = SCE_KERNEL_ERROR_ENOMEM; + if (!VirtualFree(*addr, len, MEM_DECOMMIT)) return false; + if (VirtualAlloc(*addr, len, MEM_COMMIT, WinProtFromPosix(LinuxProtFromSce(prot))) != *addr) { + const auto error = GetLastError(); + recorded->second.prot = 0; + g_protections.swap(replacement); + throw std::system_error(static_cast(error), std::system_category(), "Guest stack replacement commit failed"); + } + g_protections.swap(replacement); + result = 0; + return true; + }); + return result; + } +#endif GuestAllocations::Mutation mutation; + if (exclusive && mutation.Overlaps(*addr, len)) return SCE_KERNEL_ERROR_ENOMEM; flags = PlaceHintInReservation(*addr, len, flags); if (FixedNoOverwriteConflict(mutation, *addr, len, flags)) return SCE_KERNEL_ERROR_ENOMEM; if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags)) { @@ -932,7 +1125,7 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags, size_t alignment) { int DoMapFileBacked_nid_no_patch(void** addr, size_t len, int prot, int flags, int descriptor, std::uint64_t offset, size_t alignment) { ValidateOutput(addr); if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || (offset & (PS5_PAGE_SIZE - 1)) != 0 || len > static_cast(std::numeric_limits::max()) || offset > static_cast(std::numeric_limits::max()) - len) return SCE_KERNEL_ERROR_EINVAL; -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) const bool shared = (flags & 3) == 1; GuestAllocations::Mutation mutation; const int placementFlags = flags & (GuestMapFixedFlag | 0x80 | 0x400000); @@ -943,11 +1136,21 @@ int DoMapFileBacked_nid_no_patch(void** addr, size_t len, int prot, int flags, i GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(mapped, len); const int nativeProtection = LinuxProtFromSce(prot); const int nativeFlags = (shared ? MAP_SHARED : MAP_PRIVATE) | MAP_FIXED; - if (::mmap(mapped, len, nativeProtection, nativeFlags, descriptor, static_cast(offset)) == MAP_FAILED) { +#ifdef __APPLE__ + const int mappedProtection = nativeProtection & ~PROT_EXEC; +#else + const int mappedProtection = nativeProtection; +#endif + if (::mmap(mapped, len, mappedProtection, nativeFlags, descriptor, static_cast(offset)) == MAP_FAILED) { const int error = errno; Unmap(mapped, len); throw std::system_error(error, std::generic_category(), "map guest shared memory"); } + if (mappedProtection != nativeProtection && ::mprotect(mapped, len, nativeProtection) != 0) { + const int error = errno; + Unmap(mapped, len); + throw std::system_error(error, std::generic_category(), "make a guest file mapping executable"); + } try { mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0, false); } catch (...) { @@ -960,11 +1163,47 @@ int DoMapFileBacked_nid_no_patch(void** addr, size_t len, int prot, int flags, i Trace("map file %p+0x%zx offset=0x%llx prot=0x%x flags=0x%x", mapped, len, static_cast(offset), prot, flags); return 0; #else - static_cast(prot); - static_cast(flags); - static_cast(descriptor); - static_cast(offset); - throw std::system_error(ENOTSUP, std::generic_category(), "file-backed guest mmap is not supported on this host"); + if ((flags & 3) != 1) throw std::system_error(ENOTSUP, std::generic_category(), "map file: a private file mapping is not mapped from the file on Windows"); + if ((prot & PROT_EXEC) != 0) throw std::system_error(ENOTSUP, std::generic_category(), "map file: an executable shared file mapping is not implemented on Windows"); + const HANDLE file = WindowsDescriptorHandle_nid_no_patch(descriptor); + LARGE_INTEGER fileBytes{}; + if (!GetFileSizeEx(file, &fileBytes)) throw std::system_error(EBADF, std::generic_category(), "map file: read the file size"); + const auto fileEnd = static_cast(fileBytes.QuadPart); + if (offset + len >= fileEnd + PS5_PAGE_SIZE) throw std::system_error(ENOTSUP, std::generic_category(), "map file: a shared mapping of a whole page past the end of the file is not implemented on Windows"); + const int access = WindowsDescriptorAccess_nid_no_patch(file, descriptor, "mmap"); + if (access == 1) throw std::system_error(ENOTSUP, std::generic_category(), "map file: a shared mapping of a write-only descriptor is not implemented on Windows"); + const DWORD viewProtection = access == 2 ? PAGE_READWRITE : PAGE_READONLY; + const HANDLE section = CreateFileMappingW(file, nullptr, viewProtection, 0, 0, nullptr); + if (section == nullptr) { + const DWORD error = GetLastError(); + throw std::system_error(error == ERROR_ACCESS_DENIED ? EACCES : EIO, std::generic_category(), "map file: create the file section"); + } + GuestAllocations::Mutation mutation; + const int placementFlags = flags & (GuestMapFixedFlag | 0x80 | 0x400000); + flags = PlaceHintInReservation(*addr, len, placementFlags); + void* mapped = nullptr; + try { + if (FixedNoOverwriteConflict(mutation, *addr, len, flags)) { + CloseHandle(section); + return SCE_KERNEL_ERROR_ENOMEM; + } + ReplaceFixedOverlap(mutation, *addr, len, flags); + mapped = MapPlaced(*addr, len, PROT_NONE, flags, alignment); + GuestArena::GuestArenaMapFile_nid_postfix(mapped, len, section, offset, WinProtFromPosix(LinuxProtFromSce(prot)), viewProtection, static_cast(std::min(len, fileEnd - offset))); + mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0, false); + } catch (...) { + if (mapped != nullptr) Unmap(mapped, len); + GuestArena::GuestArenaForgetSection_nid_postfix(section); + CloseHandle(section); + throw; + } + GuestArena::GuestArenaForgetSection_nid_postfix(section); + CloseHandle(section); + *addr = mapped; + EraseReservations(mapped, len); + RecordProtection(mapped, len, prot); + Trace("map file %p+0x%zx offset=0x%llx prot=0x%x flags=0x%x", mapped, len, static_cast(offset), prot, flags); + return 0; #endif } @@ -992,6 +1231,20 @@ int DoMapShared(void** addr, size_t len, int prot, HostSharedMemory object) { } #ifdef _WIN32 +static int PosixRightsFromWin(DWORD protection) { + switch (protection & 0xff) { + case PAGE_NOACCESS: return PROT_NONE; + case PAGE_READONLY: return PROT_READ; + case PAGE_READWRITE: + case PAGE_WRITECOPY: return PROT_READ | PROT_WRITE; + case PAGE_EXECUTE: return PROT_EXEC; + case PAGE_EXECUTE_READ: return PROT_READ | PROT_EXEC; + case PAGE_EXECUTE_READWRITE: + case PAGE_EXECUTE_WRITECOPY: return PROT_READ | PROT_WRITE | PROT_EXEC; + default: throw std::runtime_error("Unknown Windows page protection of a mapped view"); + } +} + static bool IsGuestModuleImage(const void* base) { const auto address = reinterpret_cast(base); const auto* dos = reinterpret_cast(address); @@ -1005,6 +1258,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) { @@ -1024,13 +1306,38 @@ int DoMprotect(const void* addr, size_t len, int prot) { std::uintptr_t stackEnd = 0; if ((prot & GuestProtGpuReadWrite) == 0 && GuestThreadStack(address, &stackStart, &stackEnd) && address >= stackStart && address + len <= stackEnd) { constexpr std::uintptr_t hostPageMask = 0xfff; - const auto inside = std::max(first, (stackStart + hostPageMask) & ~hostPageMask); + const auto requested = std::max(first, (stackStart + hostPageMask) & ~hostPageMask); const auto insideEnd = std::min(end, stackEnd & ~hostPageMask); - if (inside >= insideEnd) return 0; + if (requested >= insideEnd) return 0; + auto* requestedPointer = reinterpret_cast(requested); + const auto requestedBytes = static_cast(insideEnd - requested); +#ifdef _WIN32 + const bool hostStack = !KernelArena::Get().Owns(requestedPointer, requestedBytes); + const bool reserveBottom = hostStack && nativeProtection != PROT_NONE && AtStackReserveBottom(requested, insideEnd); + const auto inside = hostStack && !reserveBottom ? AboveStackGuard(requested, insideEnd) : requested; + if (inside == insideEnd) { + RecordProtection(requestedPointer, requestedBytes, prot); + return 0; + } +#else + const auto inside = requested; +#endif auto* insidePointer = reinterpret_cast(inside); const auto insideBytes = static_cast(insideEnd - inside); - if (mprotect(insidePointer, insideBytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); +#ifdef _WIN32 + if (nativeProtection != PROT_NONE && hostStack) CommitReservedPages(insidePointer, insideBytes); + const bool protectedPages = mprotect(insidePointer, insideBytes, nativeProtection) == 0; +#else + bool protectedPages = mprotect(insidePointer, insideBytes, nativeProtection) == 0; + if (!protectedPages && errno == ENOMEM) { + MapUnmappedPages(insidePointer, static_cast((stackEnd & ~hostPageMask) - inside)); + protectedPages = mprotect(insidePointer, insideBytes, nativeProtection) == 0; + } +#endif + if (!protectedPages) throw std::system_error(errno, std::generic_category(), "mprotect failed"); + if (inside != requested) RecordProtection(requestedPointer, static_cast(inside - requested), prot); NoteGuestStackProtection(inside, insideBytes, (prot & 3) == 3); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(inside, insideBytes); RecordProtection(insidePointer, insideBytes, prot); return 0; } @@ -1043,18 +1350,38 @@ int DoMprotect(const void* addr, size_t len, int prot) { else if (IsGuestModuleImage(memory.AllocationBase)) mutation.RegisterImage(memory.AllocationBase); else throw std::invalid_argument("Memory protection of a foreign image is not supported"); } + for (auto cursor = first; cursor < end;) { + MEMORY_BASIC_INFORMATION view{}; + if (VirtualQuery(reinterpret_cast(cursor), &view, sizeof(view)) != sizeof(view)) throw std::runtime_error("Cannot query guest memory protection range"); + if (view.Type == MEM_MAPPED && (nativeProtection & ~PosixRightsFromWin(view.AllocationProtect)) != 0) + throw std::system_error(EACCES, std::generic_category(), "mprotect above the maximum protection of a mapped view"); + cursor = reinterpret_cast(view.BaseAddress) + view.RegionSize; + } #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 - mutation.Protect(pointer, bytes, (prot & 3) != 0, (prot & 2) != 0, (prot & GuestProtGpuReadWrite) != 0, [&] { - if (mprotect(const_cast(pointer), bytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); - }); - RecordProtection(pointer, bytes, prot); + std::vector> spans; + mutation.ForRegistered(pointer, bytes, [&](const void* span, std::size_t spanBytes) { spans.emplace_back(span, spanBytes); }); + if (spans.empty() || reinterpret_cast(spans.front().first) != first) return SCE_KERNEL_ERROR_EINVAL; + for (const auto& [span, spanBytes] : spans) mutation.RequireUnpinned(span, spanBytes); + for (const auto& [span, spanBytes] : spans) { + mutation.Protect(span, spanBytes, (prot & 3) != 0, (prot & 2) != 0, (prot & GuestProtGpuReadWrite) != 0, [&] { + if (mprotect(const_cast(span), spanBytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); + }); + RecordProtection(span, spanBytes, prot); + } return 0; } int DoMtypeprotect(const void* addr, size_t len, int type, int prot) { const int result = DoMprotect(addr, len, prot); + if (result != 0) return result; const auto address = reinterpret_cast(addr); constexpr auto pageMask = static_cast(PS5_PAGE_SIZE - 1); const auto first = address & ~pageMask; @@ -1169,14 +1496,46 @@ bool GuestProtection(uintptr_t addr, int* prot) { } void GuestPageTableUsage(std::uint64_t granularity, std::uint64_t* cpuEntries, std::uint64_t* gpuEntries) { + constexpr std::uint64_t GpuDirectorySpan = 1ULL << 30; + struct DirectSpan { + std::uintptr_t start; + std::uintptr_t end; + std::uint64_t phys; + }; + std::vector direct; + { + std::lock_guard lock(g_directLock); + direct.reserve(g_directMappings.size()); + for (const auto& [start, mapping] : g_directMappings) direct.push_back({start, mapping.end, mapping.phys}); + } + const auto largePage = [&](std::uintptr_t granule) { + const auto next = std::upper_bound(direct.begin(), direct.end(), granule, [](std::uintptr_t address, const DirectSpan& span) { return address < span.start; }); + if (next == direct.begin()) return false; + const auto& span = *std::prev(next); + return granule + granularity <= span.end && (span.phys + (granule - span.start)) % granularity == 0; + }; std::uint64_t cpu = 0; std::uint64_t gpu = 0; + std::uint64_t cpuNext = 0; + std::uint64_t gpuNext = 0; + std::uint64_t directoryNext = 0; std::lock_guard lock(g_protectionLock); for (const auto& [start, range] : g_protections) { if (range.end <= start) continue; - const auto entries = (range.end - 1) / granularity - start / granularity + 1; - if (range.prot & 0x7) cpu += entries; - if (range.prot & GuestProtGpuReadWrite) gpu += entries; + const auto first = start / granularity; + const auto last = (range.end - 1) / granularity; + if (last + 1 > std::max(first, cpuNext)) cpu += last + 1 - std::max(first, cpuNext); + cpuNext = std::max(cpuNext, last + 1); + if ((range.prot & GuestProtGpuReadWrite) == 0) continue; + const auto firstDirectory = start / GpuDirectorySpan; + const auto lastDirectory = (range.end - 1) / GpuDirectorySpan; + if (lastDirectory + 1 > std::max(firstDirectory, directoryNext)) gpu += lastDirectory + 1 - std::max(firstDirectory, directoryNext); + directoryNext = std::max(directoryNext, lastDirectory + 1); + for (auto granule = std::max(first, gpuNext); granule <= last; ++granule) { + const auto address = static_cast(granule * granularity); + if (address < start || address + granularity > range.end || !largePage(address)) ++gpu; + } + gpuNext = std::max(gpuNext, last + 1); } *cpuEntries = cpu; *gpuEntries = gpu; diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index 0307c36cab..604e85b34c 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -19,6 +19,9 @@ using HostSharedMemory = int; int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut); void DirectMemoryFree(int64_t start, size_t len); bool DirectMemoryCheckedFree(int64_t start, size_t len); +bool DirectMemoryClaimRelease(int64_t start, size_t len, bool requireAllocated); +void DirectMemoryFinishRelease(int64_t start, size_t len); +void DirectMemoryAbandonRelease(int64_t start, size_t len) noexcept; void CreateDirectMemoryBacking(int64_t start, size_t len, int memoryType); bool QueryDirectMapping(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end, std::uint64_t* offset, int* memoryType); void ForgetDirectMemory(int64_t start, size_t len); @@ -32,11 +35,12 @@ std::vector DirectMemoryViews(int64_t start, size_t len); void UnmapDirectMemoryViews(const std::vector& views); bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType); void DirectMemoryRetype(int64_t start, size_t len, int memoryType); -bool DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut); +int DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut); size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment); -int DoMapAnon(void** addr, size_t len, int prot, int flags, size_t alignment = PS5_PAGE_SIZE); +int DoMapAnon(void** addr, size_t len, int prot, int flags, size_t alignment = PS5_PAGE_SIZE, bool exclusive = false, bool allowStackReplacement = 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 +53,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 dca35ab051..bd93f135d1 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -93,6 +93,16 @@ void ApplyRangeName(uintptr_t address, VirtualQueryInfo* info) { if (next != g_rangeNames.end()) info->end = std::min(info->end, next->first); } +void ReleaseClaimedDirectMemory(int64_t start, size_t len) { + try { + UnmapDirectMemoryViews(DirectMemoryViews(start, len)); + } catch (...) { + DirectMemoryAbandonRelease(start, len); + throw; + } + DirectMemoryFinishRelease(start, len); +} + std::mutex g_prtLock; PrtAperture g_prtApertures[PRT_APERTURE_COUNT] = {}; @@ -105,22 +115,7 @@ size_t _flexibleUsedLocked() { return used; } -int _mapFlexible(void** addr, size_t len, int prot, int flags) { - const size_t configured = _configuredFlexibleSize(); - const size_t images = GuestImageSegmentBytes_nid_no_patch(); - std::lock_guard lock(g_flexibleLock); - const size_t used = images + _flexibleUsedLocked(); - if (len > configured - std::min(configured, used)) { - std::fprintf(stderr, "[memory] flexible memory exhausted: request 0x%zx with 0x%zx of 0x%zx in use\n", len, used, configured); - return SCE_KERNEL_ERROR_ENOMEM; - } - const int result = DoMapAnon(addr, len, prot, flags); - if (result == 0) g_flexibleRanges[reinterpret_cast(*addr)] = len; - return result; -} - -void _releaseFlexible(uintptr_t start, size_t len) { - std::lock_guard lock(g_flexibleLock); +void _releaseFlexibleLocked(uintptr_t start, size_t len) { const uintptr_t end = start + len; auto it = g_flexibleRanges.upper_bound(start); if (it != g_flexibleRanges.begin()) --it; @@ -134,6 +129,78 @@ void _releaseFlexible(uintptr_t start, size_t len) { } } +int _mapFlexible(void** addr, size_t len, int prot, int flags) { + const size_t configured = _configuredFlexibleSize(); + const size_t images = GuestImageSegmentBytes_nid_no_patch(); + std::lock_guard lock(g_flexibleLock); + size_t replaced = 0; + constexpr int MapFixed = 0x10; + constexpr int MapNoOverwrite = 0x80; + if (addr != nullptr && *addr != nullptr && (flags & (MapFixed | MapNoOverwrite)) == MapFixed) { + const auto start = reinterpret_cast(*addr); + if (len <= std::numeric_limits::max() - start) { + const auto end = start + len; + for (const auto& [rangeStart, rangeLength] : g_flexibleRanges) { + const auto rangeEnd = rangeStart + rangeLength; + if (rangeStart < end && rangeEnd > start) replaced += std::min(end, rangeEnd) - std::max(start, rangeStart); + } + } + } + const size_t used = images + _flexibleUsedLocked() - replaced; + if (len > configured - std::min(configured, used)) { + std::fprintf(stderr, "[memory] flexible memory exhausted: request 0x%zx with 0x%zx of 0x%zx in use\n", len, used, configured); + return SCE_KERNEL_ERROR_ENOMEM; + } + const int result = DoMapAnon(addr, len, prot, flags); + if (result == 0) { + const auto start = reinterpret_cast(*addr); + _releaseFlexibleLocked(start, len); + g_flexibleRanges[start] = len; + } + return result; +} + +void _releaseFlexible(uintptr_t start, size_t len) { + std::lock_guard lock(g_flexibleLock); + _releaseFlexibleLocked(start, len); +} + +std::mutex g_lockedRangesLock; +std::map g_lockedRanges; + +void RecordLockedRange(uintptr_t start, uintptr_t end) { + std::lock_guard lock(g_lockedRangesLock); + auto it = g_lockedRanges.begin(); + while (it != g_lockedRanges.end()) { + if (it->first <= end && it->first + it->second >= start) { + start = std::min(start, it->first); + end = std::max(end, it->first + it->second); + it = g_lockedRanges.erase(it); + } else { + ++it; + } + } + g_lockedRanges[start] = end - start; +} + +void ForgetLockedRange(uintptr_t start, uintptr_t end) { + std::lock_guard lock(g_lockedRangesLock); + auto it = g_lockedRanges.upper_bound(start); + if (it != g_lockedRanges.begin()) --it; + while (it != g_lockedRanges.end() && it->first < end) { + const uintptr_t rangeStart = it->first; + const uintptr_t rangeEnd = rangeStart + it->second; + if (rangeEnd <= start) { ++it; continue; } + it = g_lockedRanges.erase(it); + if (rangeStart < start) g_lockedRanges[rangeStart] = start - rangeStart; + if (rangeEnd > end) g_lockedRanges[end] = rangeEnd - end; + } +} + +} + +void ReleaseFlexibleMappingRange(std::uintptr_t address, size_t len) { + _releaseFlexible(address, len); } extern "C" { @@ -154,7 +221,8 @@ int APS5_VABI sceKernelAvailableDirectMemorySize(int64_t search_start, int64_t s if (!phys_addr_out || !size_out) return SCE_KERNEL_ERROR_EINVAL; int64_t start = 0; size_t size = 0; - if (!DirectMemoryLargestFree(search_start, search_end, alignment, &start, &size)) { *phys_addr_out = 0; *size_out = 0; return SCE_KERNEL_ERROR_EAGAIN; } + const int result = DirectMemoryLargestFree(search_start, search_end, alignment, &start, &size); + if (result != 0) { *phys_addr_out = 0; *size_out = 0; return result; } *phys_addr_out = start; *size_out = size; return 0; @@ -192,12 +260,16 @@ int APS5_VABI sceKernelEnableDmemAliasing(void) { int APS5_VABI sceKernelMapDirectMemory(void** addr, size_t len, int prot, int flags, int64_t direct_memory_start, size_t alignment) { (void)alignment; - return DoMapDirect(addr, len, prot, flags, direct_memory_start, alignment); + const int result = DoMapDirect(addr, len, prot, flags, direct_memory_start, alignment); + if (result == 0) _releaseFlexible(reinterpret_cast(*addr), len); + return result; } int APS5_VABI sceKernelMapDirectMemory2(void** addr, size_t len, int type, int prot, int flags, int64_t direct_memory_start, size_t alignment) { (void)type; - return DoMapDirect(addr, len, prot, flags, direct_memory_start, alignment); + const int result = DoMapDirect(addr, len, prot, flags, direct_memory_start, alignment); + if (result == 0) _releaseFlexible(reinterpret_cast(*addr), len); + return result; } int APS5_VABI sceKernelMapFlexibleMemory(void** addr_in_out, size_t len, int prot, int flags) { @@ -209,6 +281,7 @@ int APS5_VABI sceKernelSetVirtualRangeName(const void* addr, uint64_t len, const int APS5_VABI sceKernelMapNamedDirectMemory(void** addr, size_t len, int prot, int flags, int64_t direct_memory_start, size_t alignment, const char* name) { const int result = DoMapDirect(addr, len, prot, flags, direct_memory_start, alignment); + if (result == 0) _releaseFlexible(reinterpret_cast(*addr), len); if (result == 0 && name) sceKernelSetVirtualRangeName(*addr, len, name); return result; } @@ -225,12 +298,17 @@ int32_t APS5_VABI sceKernelMapNamedFlexibleMemoryInternal(void** addr_in_out, si } int APS5_VABI sceKernelMprotect(const void* addr, size_t len, int prot) { - return DoMprotect(addr, len, prot); + constexpr int DefinedProtection = 0x3F7; + if (len == 0) return 0; + return DoMprotect(addr, len, prot & DefinedProtection); } int APS5_VABI sceKernelMunmap(uint64_t vaddr, size_t len) { const int result = DoMunmap(reinterpret_cast(vaddr), len); - if (result == 0) _releaseFlexible(static_cast(vaddr), len); + if (result == 0) { + _releaseFlexible(static_cast(vaddr), len); + ForgetLockedRange(static_cast(vaddr), static_cast(vaddr) + len); + } return result; } @@ -250,14 +328,15 @@ int APS5_VABI sceKernelReleaseDirectMemory(int64_t start, size_t len) { if ((static_cast(start) & (PS5_PAGE_SIZE - 1)) != 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || len > DIRECT_MEMORY_SIZE || static_cast(start) > DIRECT_MEMORY_SIZE - len) return SCE_KERNEL_ERROR_EINVAL; - const auto views = DirectMemoryViews(start, len); - DirectMemoryFree(start, len); - UnmapDirectMemoryViews(views); + if (!DirectMemoryClaimRelease(start, len, false)) return 0; + ReleaseClaimedDirectMemory(start, len); return 0; } int APS5_VABI sceKernelReserveVirtualRange(void** addr, size_t len, int flags, size_t alignment) { - return DoReserveVirtual(addr, len, flags, alignment); + const int result = DoReserveVirtual(addr, len, flags, alignment); + if (result == 0) _releaseFlexible(reinterpret_cast(*addr), len); + return result; } int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInfo* info, uint64_t info_size) { @@ -339,13 +418,13 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf int APS5_VABI sceKernelCheckedReleaseDirectMemory(int64_t start, size_t len) { if (start < 0 || (static_cast(start) & (PS5_PAGE_SIZE - 1)) != 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; if (len == 0) return 0; - const auto views = DirectMemoryViews(start, len); - if (!DirectMemoryCheckedFree(start, len)) return SCE_KERNEL_ERROR_ENOENT; - UnmapDirectMemoryViews(views); + if (!DirectMemoryClaimRelease(start, len, true)) return SCE_KERNEL_ERROR_ENOENT; + ReleaseClaimedDirectMemory(start, len); return 0; } 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); } @@ -386,10 +465,9 @@ int APS5_VABI sceKernelConfiguredFlexibleMemorySize(size_t* size) { return 0; } -int APS5_VABI sceKernelInternalMemoryGetAvailableSize(size_t* size) { - if (!size) return SCE_KERNEL_ERROR_EINVAL; - *size = 0; - return 0; +int APS5_VABI sceKernelInternalMemoryGetAvailableSize(std::size_t* size) { + if (!size) return -1; + throw std::logic_error("sceKernelInternalMemoryGetAvailableSize: internal pool statistics are not implemented"); } int APS5_VABI sceKernelSetVirtualRangeName(const void* addr, uint64_t len, const char* name) { @@ -412,7 +490,7 @@ int APS5_VABI sceKernelClearVirtualRangeName(const void* addr, uint64_t len) { int APS5_VABI sceKernelGetPageTableStats(int* cpu_total, int* cpu_available, int* gpu_total, int* gpu_available) { if (!cpu_total || !cpu_available || !gpu_total || !gpu_available) return SCE_KERNEL_ERROR_EFAULT; constexpr std::uint64_t PageTableGranularity = 2ULL * 1024 * 1024; - constexpr std::uint64_t PageTablePoolEntries = 2048; + constexpr std::uint64_t PageTablePoolEntries = 8192; std::uint64_t cpuUsed = 0; std::uint64_t gpuUsed = 0; GuestPageTableUsage(PageTableGranularity, &cpuUsed, &gpuUsed); @@ -463,6 +541,7 @@ int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry* entries, int num_entries, switch (entry.operation) { case OpMapDirect: result = DoMapDirect(&entry.start, entry.length, static_cast(entry.protection), flags, static_cast(entry.offset), 0); + if (result == 0) _releaseFlexible(reinterpret_cast(entry.start), entry.length); break; case OpUnmap: result = sceKernelMunmap(reinterpret_cast(entry.start), entry.length); @@ -524,7 +603,10 @@ int LockHostPages(std::uintptr_t start, std::uintptr_t end) { auto* const address = reinterpret_cast(start); const SIZE_T bytes = end - start; std::lock_guard lock(g_workingSetLock); - if (VirtualLock(address, bytes)) return 0; + if (VirtualLock(address, bytes)) { + RecordLockedRange(start, end); + return 0; + } if (GetLastError() != ERROR_WORKING_SET_QUOTA) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualLock failed"); SIZE_T minimum = 0; SIZE_T maximum = 0; @@ -532,7 +614,10 @@ int LockHostPages(std::uintptr_t start, std::uintptr_t end) { if (!GetProcessWorkingSetSizeEx(GetCurrentProcess(), &minimum, &maximum, &flags)) throw std::system_error(static_cast(GetLastError()), std::system_category(), "GetProcessWorkingSetSizeEx failed"); if (bytes > std::numeric_limits::max() - maximum) return SCE_KERNEL_ERROR_EAGAIN; if (!SetProcessWorkingSetSizeEx(GetCurrentProcess(), minimum + bytes, maximum + bytes, flags)) return SCE_KERNEL_ERROR_EAGAIN; - if (VirtualLock(address, bytes)) return 0; + if (VirtualLock(address, bytes)) { + RecordLockedRange(start, end); + return 0; + } if (GetLastError() != ERROR_WORKING_SET_QUOTA) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualLock failed"); return SCE_KERNEL_ERROR_EAGAIN; } @@ -581,19 +666,26 @@ int UnlockHostPages(std::uintptr_t start, std::uintptr_t end) { } cursor = next; } + ForgetLockedRange(start, end); return 0; } #else int LockHostPages(std::uintptr_t start, std::uintptr_t end) { auto* const address = reinterpret_cast(start); const std::size_t bytes = end - start; - if (::mlock(address, bytes) == 0) return 0; + if (::mlock(address, bytes) == 0) { + RecordLockedRange(start, end); + return 0; + } int error = errno; rlimit limit{}; if ((error == ENOMEM || error == EPERM || error == EAGAIN) && getrlimit(RLIMIT_MEMLOCK, &limit) == 0 && limit.rlim_cur < limit.rlim_max) { limit.rlim_cur = limit.rlim_max; if (setrlimit(RLIMIT_MEMLOCK, &limit) == 0) { - if (::mlock(address, bytes) == 0) return 0; + if (::mlock(address, bytes) == 0) { + RecordLockedRange(start, end); + return 0; + } error = errno; } } @@ -607,7 +699,10 @@ int LockHostPages(std::uintptr_t start, std::uintptr_t end) { int UnlockHostPages(std::uintptr_t start, std::uintptr_t end) { if (!GuestRangeMapped(start, end)) return SCE_KERNEL_ERROR_ENOMEM; - if (::munlock(reinterpret_cast(start), end - start) == 0) return 0; + if (::munlock(reinterpret_cast(start), end - start) == 0) { + ForgetLockedRange(start, end); + return 0; + } const int error = errno; if (error == ENOMEM) return SCE_KERNEL_ERROR_ENOMEM; throw std::system_error(error, std::generic_category(), "munlock failed"); @@ -634,4 +729,35 @@ int APS5_VABI sceKernelMunlock_nid_postfix(void* address, std::uint64_t length) return UnlockHostPages(start, end); } +int APS5_VABI sceKernelMunlockall_nid_postfix() { +#ifndef _WIN32 + ::munlockall(); +#endif + std::map ranges; + { + std::lock_guard lock(g_lockedRangesLock); + ranges.swap(g_lockedRanges); + } +#ifdef _WIN32 + for (const auto& [start, len] : ranges) { + const auto end = start + len; + for (auto cursor = start; cursor < end;) { + MEMORY_BASIC_INFORMATION region{}; + if (VirtualQuery(reinterpret_cast(cursor), ®ion, sizeof(region)) != sizeof(region)) break; + const auto next = std::min(end, reinterpret_cast(region.BaseAddress) + region.RegionSize); + if (region.State == MEM_COMMIT && (region.Protect & (PAGE_NOACCESS | PAGE_GUARD)) == 0 && !VirtualUnlock(reinterpret_cast(cursor), next - cursor)) { + if (GetLastError() != ERROR_NOT_LOCKED) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualUnlock failed"); + for (auto page = cursor; page < next; page += HostPageSize()) { + if (!VirtualUnlock(reinterpret_cast(page), std::min(HostPageSize(), next - page)) && GetLastError() != ERROR_NOT_LOCKED) { + throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualUnlock failed"); + } + } + } + cursor = next; + } + } +#endif + return 0; +} + } diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index 8fa22b5fbc..54cdbaf12d 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -1,6 +1,7 @@ #include "prx/libkernel/DirectMemory/MemoryPool.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include +#include #include #include #include @@ -52,8 +53,8 @@ bool PageSpan(uint64_t start, size_t len, uint64_t& lo, uint64_t& hi) { struct PhysicalMemoryPool { static PhysicalMemoryPool& Instance() { - static PhysicalMemoryPool inst; - return inst; + static auto* inst = new PhysicalMemoryPool; + return *inst; } int Alloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { @@ -87,6 +88,40 @@ struct PhysicalMemoryPool { bool CheckedFree(uint64_t start, size_t len) { return _release(start, len, true); } + bool ClaimRelease(uint64_t start, size_t len, bool requireAllocated) { + uint64_t lo, hi; + if (!PageSpan(start, len, lo, hi)) return false; + std::unique_lock lock(_mutex); + if (requireAllocated) { + if (!_allUsed(static_cast(lo / kPage), static_cast((hi - lo) / kPage)) || _claimed(lo, hi)) return false; + } else { + _claimFinished.wait(lock, [&] { return !_claimed(lo, hi); }); + } + _claims.emplace(lo, hi); + return true; + } + + void FinishRelease(uint64_t start, size_t len) { + uint64_t lo, hi; + if (!PageSpan(start, len, lo, hi)) return; + { + std::lock_guard lock(_mutex); + _claims.erase(lo); + (void)_releaseLocked(lo, hi, false); + } + _claimFinished.notify_all(); + } + + void AbandonRelease(uint64_t start, size_t len) noexcept { + uint64_t lo, hi; + if (!PageSpan(start, len, lo, hi)) return; + { + std::lock_guard lock(_mutex); + _claims.erase(lo); + } + _claimFinished.notify_all(); + } + bool Find(uint64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { if (!start || !end || !memoryType || offset >= kPoolBytes) return false; std::lock_guard lock(_mutex); @@ -118,14 +153,14 @@ struct PhysicalMemoryPool { return true; } - bool LargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { - if (!startOut || !sizeOut) return false; - if (searchStart < 0 || searchEnd <= searchStart) return false; + int LargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { + if (!startOut || !sizeOut) return SCE_KERNEL_ERROR_EINVAL; + if (searchStart < 0 || searchEnd <= searchStart) return SCE_KERNEL_ERROR_EAGAIN; uint64_t align; - if (!NormalizeAlignment(alignment, align)) return false; + if (!NormalizeAlignment(alignment, align)) return SCE_KERNEL_ERROR_EAGAIN; uint64_t cur; - if (!RoundUp(static_cast(searchStart), kPage, cur)) return false; + if (!RoundUp(static_cast(searchStart), kPage, cur)) return SCE_KERNEL_ERROR_EAGAIN; const uint64_t limit = std::min(static_cast(searchEnd), kPoolBytes) & ~(kPage - 1); std::lock_guard lock(_mutex); @@ -149,10 +184,10 @@ struct PhysicalMemoryPool { cur = runEnd; } - if (bestSize == 0) return false; + if (bestSize == 0) return SCE_KERNEL_ERROR_ENOMEM; *startOut = static_cast(bestStart); *sizeOut = static_cast(bestSize); - return true; + return 0; } size_t FreeRun(uint64_t offset, uint64_t limit) { @@ -211,13 +246,21 @@ private: _mark(static_cast(start / kPage), static_cast(len / kPage), true); } + bool _claimed(uint64_t lo, uint64_t hi) const { + std::map::const_iterator it = _claims.lower_bound(hi); + return it != _claims.begin() && std::prev(it)->second > lo; + } + bool _release(uint64_t start, size_t len, bool requireAllocated) { uint64_t lo, hi; if (!PageSpan(start, len, lo, hi)) return false; + std::lock_guard lock(_mutex); + return _releaseLocked(lo, hi, requireAllocated); + } + + bool _releaseLocked(uint64_t lo, uint64_t hi, bool requireAllocated) { const size_t first = static_cast(lo / kPage); const size_t count = static_cast((hi - lo) / kPage); - - std::lock_guard lock(_mutex); if (requireAllocated && !_allUsed(first, count)) return false; const bool splitLo = _splitAt(lo); @@ -256,8 +299,10 @@ private: } std::mutex _mutex; + std::condition_variable _claimFinished; bool _used[kNumPages] = {}; BlockMap _blocks; + std::map _claims; }; } @@ -278,6 +323,18 @@ bool DirectMemoryCheckedFree(int64_t start, size_t len) { return PhysicalMemoryPool::Instance().CheckedFree(static_cast(start), len); } +bool DirectMemoryClaimRelease(int64_t start, size_t len, bool requireAllocated) { + return PhysicalMemoryPool::Instance().ClaimRelease(static_cast(start), len, requireAllocated); +} + +void DirectMemoryFinishRelease(int64_t start, size_t len) { + PhysicalMemoryPool::Instance().FinishRelease(static_cast(start), len); +} + +void DirectMemoryAbandonRelease(int64_t start, size_t len) noexcept { + PhysicalMemoryPool::Instance().AbandonRelease(static_cast(start), len); +} + bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { return PhysicalMemoryPool::Instance().Find(static_cast(offset), findNext, start, end, memoryType); } @@ -286,7 +343,7 @@ void DirectMemoryRetype(int64_t start, size_t len, int memoryType) { (void)PhysicalMemoryPool::Instance().Retype(static_cast(start), len, memoryType); } -bool DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { +int DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { return PhysicalMemoryPool::Instance().LargestFree(searchStart, searchEnd, alignment, startOut, sizeOut); } diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index 1e314660cf..c540f34c99 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -2,21 +2,34 @@ #include "SceTypes.hpp" #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 #include #include #include #include -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) #include +#include #include +#elif defined(_WIN32) +#include "prx/libc/include/WindowsDescriptor.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/RandomDevice.hpp" #endif extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI sceKernelMunmap(std::uint64_t address, std::size_t length); extern "C" int APS5_VABI sceKernelMlock_nid_postfix(void* address, std::uint64_t length); extern "C" int APS5_VABI sceKernelMunlock_nid_postfix(void* address, std::uint64_t length); +extern "C" int APS5_VABI sceKernelMunlockall_nid_postfix(); extern "C" int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInfo* info, uint64_t info_size); +extern "C" std::int64_t APS5_VABI sceKernelPread(int descriptor, void* buffer, std::size_t length, std::int64_t offset); +extern "C" int APS5_VABI munmap_nid_postfix(void* address, std::size_t length) noexcept; +extern "C" int APS5_VABI mprotect_nid_postfix(void* address, std::size_t length, int protection) noexcept; namespace { // FreeBSD/PS5 ABI values, independent of the host's errno and mmap constants. @@ -24,12 +37,14 @@ constexpr int GuestInvalid = 22; constexpr int GuestNoMemory = 12; constexpr int GuestBadDescriptor = 9; constexpr int GuestAccess = 13; +constexpr int GuestFault = 14; constexpr int GuestIoError = 5; constexpr int GuestNotSupported = 45; constexpr int GuestShared = 0x1; constexpr int GuestPrivate = 0x2; constexpr int GuestAnonymous = 0x1000; constexpr int GuestMapFixed = 0x10; +constexpr int GuestMapExclusive = 0x4000; constexpr int GuestMapNoOverwrite = 0x80; constexpr int GuestMapNoCoalesce = 0x400000; constexpr int GuestSyncAsync = 0x1; @@ -38,11 +53,12 @@ constexpr int GuestAdviceCore = 9; constexpr int GuestAdviceProtect = 10; constexpr int GuestLockCurrent = 0x1; constexpr int GuestLockFuture = 0x2; +constexpr int GuestDefinedProtection = 0x3F7; constexpr std::uintptr_t GuestUserAddressEnd = 0x800000000000; constexpr int GuestAlignedSuper = 0x1000000; constexpr std::size_t SuperpageSize = std::size_t{1} << 21; -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) class ScopedDescriptor { public: explicit ScopedDescriptor(int value) noexcept : descriptor(fcntl(value, F_DUPFD_CLOEXEC, 0)), error(descriptor < 0 ? errno : 0) {} @@ -59,6 +75,11 @@ private: int descriptor = -1; int error = 0; }; + +bool RegularFile(int descriptor) { + struct stat status{}; + return ::fstat(descriptor, &status) == 0 && S_ISREG(status.st_mode); +} #endif bool RoundLength(std::size_t length, std::size_t& rounded) { @@ -77,6 +98,50 @@ bool Mapped(std::uintptr_t address) { VirtualQueryInfo info{}; return sceKernelVirtualQuery(reinterpret_cast(address), 0, &info, sizeof(info)) == 0; } + +#ifdef _WIN32 +int DescriptorAccess(HANDLE handle, int descriptor) noexcept { + try { + return WindowsDescriptorAccess_nid_no_patch(handle, descriptor, "mmap"); + } catch (const std::exception&) { + return -1; + } +} + +template +void* MapPrivateFileCopy(void* address, std::size_t rounded, int protection, int placement, std::size_t alignment, HANDLE handle, + int descriptor, std::int64_t offset, int accessMode, Failed failed) noexcept { + if (accessMode == 1) return failed(GuestNotSupported); + LARGE_INTEGER fileBytes{}; + if (!GetFileSizeEx(handle, &fileBytes)) return failed(GuestBadDescriptor); + const auto size = static_cast(fileBytes.QuadPart); + const auto start = static_cast(offset); + if (start + rounded > ((size + PS5_PAGE_SIZE - 1) & ~static_cast(PS5_PAGE_SIZE - 1))) return failed(GuestNotSupported); + void* mapped = address; + try { + if (DoMapAnon(&mapped, rounded, 3, placement, alignment, false) != 0) return failed(GuestNoMemory); + } catch (const std::invalid_argument&) { + return failed(GuestInvalid); + } catch (const std::exception&) { + return failed(GuestNoMemory); + } + const auto copied = std::min(rounded, size - start); + for (std::uint64_t done = 0; done < copied;) { + const auto read = sceKernelPread(descriptor, static_cast(mapped) + done, static_cast(copied - done), static_cast(start + done)); + if (read <= 0) { + munmap_nid_postfix(mapped, rounded); + return failed(read < 0 ? static_cast(read & 0xffff) : GuestIoError); + } + done += static_cast(read); + } + if (protection != 3 && mprotect_nid_postfix(mapped, rounded, protection) != 0) { + const int error = *__error_nid_postfix(); + munmap_nid_postfix(mapped, rounded); + return failed(error); + } + return mapped; +} +#endif } extern "C" { @@ -90,14 +155,31 @@ void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protecti std::size_t rounded; if (!RoundLength(length, rounded) || (protection & ~7) != 0) return failed(GuestInvalid); - const int mappingFlags = flags & ~GuestAlignedSuper; - if (mappingFlags == (GuestPrivate | GuestAnonymous)) { + if ((flags & (GuestMapExclusive | GuestMapFixed)) == GuestMapExclusive) return failed(GuestInvalid); + if ((flags & (GuestShared | GuestPrivate)) == (GuestShared | GuestPrivate)) return failed(GuestInvalid); + int mappingFlags = flags & ~GuestAlignedSuper; + if ((mappingFlags & (GuestShared | GuestPrivate | GuestAnonymous)) == GuestAnonymous) mappingFlags |= GuestPrivate; + const bool fixedAnonymous = (mappingFlags & ~(GuestMapFixed | GuestMapExclusive)) == (GuestPrivate | GuestAnonymous) + && (mappingFlags & GuestMapFixed) != 0; + if (mappingFlags == (GuestPrivate | GuestAnonymous) || fixedAnonymous) { if (descriptor != -1 || offset != 0) return failed(GuestInvalid); - void* mapped = nullptr; + const auto start = reinterpret_cast(address); + if (fixedAnonymous && ((start & (PS5_PAGE_SIZE - 1)) != 0 || + rounded > std::numeric_limits::max() - start)) return failed(GuestInvalid); +#ifndef _WIN32 + if (fixedAnonymous && !GuestArena::GuestArenaContains_nid_postfix(address, rounded)) return failed(GuestInvalid); +#endif + const bool exclusive = fixedAnonymous && (mappingFlags & GuestMapExclusive) != 0; + const int placement = fixedAnonymous ? GuestMapFixed | (exclusive ? GuestMapNoOverwrite : 0) : 0; + void* mapped = fixedAnonymous ? address : nullptr; + const auto alignment = !fixedAnonymous && (flags & GuestAlignedSuper) != 0 ? SuperpageSize : PS5_PAGE_SIZE; try { - const auto result = DoMapAnon(&mapped, rounded, protection, 0, (flags & GuestAlignedSuper) != 0 ? SuperpageSize : PS5_PAGE_SIZE); - if (result != 0) return failed(GuestNoMemory); + const auto result = DoMapAnon(&mapped, rounded, protection, placement, alignment, exclusive, fixedAnonymous); + if (result != 0) return failed(result == SCE_KERNEL_ERROR_EINVAL ? GuestInvalid : GuestNoMemory); + if (fixedAnonymous) ReleaseFlexibleMappingRange(start, rounded); return mapped; + } catch (const std::invalid_argument&) { + return failed(fixedAnonymous ? GuestInvalid : GuestNoMemory); } catch (const std::bad_alloc&) { return failed(GuestNoMemory); } catch (const std::exception&) { @@ -111,11 +193,12 @@ 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) return failed(GuestInvalid); -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) const ScopedDescriptor pinnedDescriptor(descriptor); if (pinnedDescriptor.Get() < 0) return failed(pinnedDescriptor.Error()); const int mappingDescriptor = pinnedDescriptor.Get(); @@ -124,7 +207,22 @@ void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protecti const int accessMode = descriptorFlags & O_ACCMODE; if ((protection & 1) != 0 && accessMode == O_WRONLY) return failed(GuestAccess); if (kind == GuestShared && (protection & 2) != 0 && accessMode == O_RDONLY) return failed(GuestAccess); - if (!File::IsSharedMemoryDescriptor_nid_no_patch(mappingDescriptor)) return failed(GuestNotSupported); + if (!File::IsSharedMemoryDescriptor_nid_no_patch(mappingDescriptor) && !RegularFile(mappingDescriptor)) return failed(GuestNotSupported); +#elif defined(_WIN32) + const int mappingDescriptor = descriptor; + if (!File::IsSharedMemoryDescriptor_nid_no_patch(mappingDescriptor)) { + const HANDLE handle = WindowsDescriptorHandle_nid_no_patch(mappingDescriptor); + if (handle == INVALID_HANDLE_VALUE) return failed(GuestBadDescriptor); + if (File::DirectoryDescriptorPath(mappingDescriptor) || File::IsRandomDevice(mappingDescriptor) || GetFileType(handle) != FILE_TYPE_DISK) return failed(GuestNotSupported); + const int accessMode = DescriptorAccess(handle, mappingDescriptor); + if (accessMode < 0) return failed(GuestBadDescriptor); + if ((protection & 1) != 0 && accessMode == 1) return failed(GuestAccess); + if (kind == GuestShared && (protection & 2) != 0 && accessMode == 0) return failed(GuestAccess); + if (kind == GuestPrivate) { + const std::size_t privateAlignment = (flags & GuestAlignedSuper) != 0 ? SuperpageSize : PS5_PAGE_SIZE; + return MapPrivateFileCopy(address, rounded, protection, placement, privateAlignment, handle, mappingDescriptor, offset, accessMode, failed); + } + } #else const int mappingDescriptor = descriptor; if (!File::IsSharedMemoryDescriptor_nid_no_patch(mappingDescriptor)) return failed(GuestNotSupported); @@ -135,6 +233,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); @@ -163,7 +262,7 @@ int APS5_VABI munmap_nid_postfix(void* address, std::size_t length) noexcept { rounded > std::numeric_limits::max() - start) return failed(GuestInvalid); try { - if (DoMunmap(address, rounded) != 0) return failed(GuestInvalid); + if (sceKernelMunmap(start, rounded) != 0) return failed(GuestInvalid); return 0; } catch (const std::bad_alloc&) { return failed(GuestNoMemory); @@ -183,7 +282,7 @@ int APS5_VABI mprotect_nid_postfix(void* address, std::size_t length, int protec if (start == 0 || length > std::numeric_limits::max() - start) return failed(GuestInvalid); try { - if (DoMprotect(address, length, protection) != 0) return failed(GuestInvalid); + if (DoMprotect(address, length, protection & GuestDefinedProtection) != 0) return failed(GuestInvalid); return 0; } catch (const std::bad_alloc&) { return failed(GuestNoMemory); @@ -218,6 +317,13 @@ int APS5_VABI mlockall_nid_postfix(int flags) { throw std::runtime_error("mlockall: MCL_CURRENT is not implemented: the process also holds host stacks, heaps, modules and driver mappings"); } +int APS5_VABI munlockall_nid_postfix() { + const int result = sceKernelMunlockall_nid_postfix(); + if (result == 0) return 0; + SetError(result & 0xffff); + return -1; +} + int APS5_VABI msync_nid_postfix(void* address, std::size_t length, int flags) { const auto failed = [](int error) { SetError(error); @@ -245,4 +351,28 @@ int APS5_VABI madvise_nid_postfix(void* address, std::size_t length, int advice) return 0; } +int APS5_VABI mincore_nid_postfix(const void* address, std::size_t length, char* vec) { + const auto failed = [](int error) { + SetError(error); + return -1; + }; + constexpr std::uintptr_t mask = PS5_PAGE_SIZE - 1; + const auto first = reinterpret_cast(address) & ~mask; + if (length > std::numeric_limits::max() - mask) return failed(GuestNoMemory); + const auto size = (length + mask) & ~mask; + if (size > std::numeric_limits::max() - first) return failed(GuestNoMemory); + const auto end = first + size; + if (size == 0 ? !Mapped(first) : !GuestRangeMapped(first, end)) return failed(GuestNoMemory); + if (size == 0) return 0; + if (vec == nullptr) return failed(GuestFault); + for (auto cursor = first; cursor < end;) { + VirtualQueryInfo info{}; + sceKernelVirtualQuery(reinterpret_cast(cursor), 0, &info, sizeof(info)); + const auto extentEnd = std::min(info.end, end); + std::fill_n(vec + (cursor - first) / PS5_PAGE_SIZE, (extentEnd - cursor) / PS5_PAGE_SIZE, static_cast(info.is_committed != 0 ? 1 : 0)); + cursor = extentEnd; + } + return 0; +} + } diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index 641deca1d4..1256cc9391 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -3,6 +3,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/HostStreams.hpp" #include #include @@ -15,6 +16,13 @@ static std::unordered_map g_equeues; static std::mutex g_equeueMutex; static uint64_t g_nextEqueue = 1; +struct KernelEqueuePrivate::WaitSet { + std::vector interests; + std::vector> held; + std::vector> queues; + uint64_t nearestTimerNs = std::numeric_limits::max(); +}; + extern "C" { KernelEqueuePrivate::KernelEqueuePrivate(KernelEqueue handle, bool reportOncePerWait) : m_handle(handle), m_reportOncePerWait(reportOncePerWait) {} @@ -50,7 +58,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 +74,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 +90,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; } @@ -96,17 +104,77 @@ void KernelEqueuePrivate::WakePollers() { } } -void KernelEqueuePrivate::WaitForDescriptors(std::unique_lock& lock, std::uint64_t deadlineNanos) { - std::vector interests; +void KernelEqueuePrivate::GatherInterests(WaitSet& wait, std::uintptr_t waker, bool nested) { for (const auto& e : m_events) { - if (e.filter.pollFunc != nullptr) { - interests.push_back({static_cast(e.event.ident), e.event.filter == EVFILT_WRITE}); + if (!e.enabled) { + continue; + } + if (nested && !e.triggered && e.deadlineNs != 0) { + wait.nearestTimerNs = std::min(wait.nearestTimerNs, e.deadlineNs); + } + if (e.filter.pollFunc == nullptr) { + continue; + } + if (e.filter.watched) { + GatherWatched(wait, e.filter.watched, waker); + } else if (e.filter.waitDescriptor >= 0) { + wait.interests.push_back({e.filter.waitDescriptor, false}); + wait.held.push_back(e.filter.owner); + } else { + wait.interests.push_back({static_cast(e.event.ident), e.event.filter == EVFILT_WRITE}); } } +} + +void KernelEqueuePrivate::GatherWatched(WaitSet& wait, const std::shared_ptr& queue, std::uintptr_t waker) { + std::lock_guard lock(queue->m_mutex); + if (queue->m_closed) { + return; + } + queue->m_pollers.push_back(waker); + wait.queues.push_back(queue); + queue->GatherInterests(wait, waker, true); +} + +void KernelEqueuePrivate::ReleaseWatched(WaitSet& wait, std::uintptr_t waker) { + for (const auto& queue : wait.queues) { + std::lock_guard lock(queue->m_mutex); + const auto found = std::find(queue->m_pollers.begin(), queue->m_pollers.end(), waker); + if (found != queue->m_pollers.end()) { + queue->m_pollers.erase(found); + } + } + wait.queues.clear(); +} + +void KernelEqueuePrivate::WaitForDescriptors(std::unique_lock& lock, std::uint64_t deadlineNanos) { const auto waker = GuestSockets::CurrentWaker(); + WaitSet wait; + GatherInterests(wait, waker, false); m_pollers.push_back(waker); + if (!wait.queues.empty()) { + PollEvents(); + if (std::any_of(m_events.begin(), m_events.end(), [](const KernelEqueueEvent& event) { return event.enabled && event.triggered; })) { + m_pollers.erase(std::find(m_pollers.begin(), m_pollers.end(), waker)); + ReleaseWatched(wait, waker); + return; + } + if (wait.nearestTimerNs != std::numeric_limits::max()) { + const uint64_t nowMonotonic = MonotonicNs(); + const uint64_t remainingNs = wait.nearestTimerNs > nowMonotonic ? wait.nearestTimerNs - nowMonotonic : 0; + const uint64_t timerDeadline = TimedWait::NowNanos() + std::clamp(remainingNs, 1000, static_cast(std::numeric_limits::max()) * 1000ULL); + deadlineNanos = deadlineNanos == 0 ? timerDeadline : std::min(deadlineNanos, timerDeadline); + } + } lock.unlock(); - const bool waited = GuestSockets::WaitAny(interests, deadlineNanos); + bool waited = false; + try { + waited = GuestSockets::WaitAny(wait.interests, deadlineNanos); + } catch (...) { + ReleaseWatched(wait, waker); + throw; + } + ReleaseWatched(wait, waker); lock.lock(); m_pollers.erase(std::find(m_pollers.begin(), m_pollers.end(), waker)); if (!waited) { @@ -121,6 +189,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 +200,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 +231,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 +244,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 +276,31 @@ int KernelEqueuePrivate::GetTriggeredEvents(KernelEvent* ev, int num) { return CollectTriggered(ev, num); } +bool KernelEqueuePrivate::HasTriggeredEvents() { + return CountTriggeredEvents() != 0; +} + +std::size_t KernelEqueuePrivate::CountTriggeredEvents() { + std::unique_lock lock(m_mutex); + if (m_closed) { + return 0; + } + TriggerExpiredTimers(MonotonicNs()); + PollEvents(); + return static_cast(std::count_if(m_events.begin(), m_events.end(), [](const KernelEqueueEvent& event) { return event.enabled && event.triggered; })); +} + +std::vector> KernelEqueuePrivate::WatchedQueues() { + std::unique_lock lock(m_mutex); + std::vector> queues; + for (const auto& event : m_events) { + if (event.filter.watched) { + queues.push_back(event.filter.watched); + } + } + return queues; +} + int KernelEqueuePrivate::WaitForEvents(KernelEvent* ev, int num, uint32_t micros) { return WaitForEventsUntil(ev, num, micros == 0 ? 0 : TimedWait::DeadlineNanos(micros)); } @@ -281,10 +390,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(); @@ -595,13 +706,16 @@ int APS5_VABI sceKernelDeleteAmprSystemEvent(KernelEqueue eq, int id) { static int AddDescriptorEvent(KernelEqueue eq, int fd, std::size_t size, void* udata, int16_t filter) { const bool isSocket = GuestSockets::IsOpen(fd); - if (!isSocket && DescriptorIsOpen_nid_no_patch(fd)) { - NotImplemented_nid_no_patch(filter == EVFILT_READ ? "sceKernelAddReadEvent: non-socket descriptor" : "sceKernelAddWriteEvent: non-socket descriptor"); + std::int64_t available = 0; + bool eof = false; + const bool isPipe = !isSocket && filter == EVFILT_READ && !File::IsHostStream(fd) && GuestSockets::PipeReadState(fd, &available, &eof); + if (!isSocket && !isPipe && DescriptorIsOpen_nid_no_patch(fd)) { + NotImplemented_nid_no_patch(filter == EVFILT_READ ? "sceKernelAddReadEvent: descriptor that is neither a socket nor the read end of a pipe" : "sceKernelAddWriteEvent: non-socket descriptor"); } - if (size > 1) { + if (size > 1 && !isPipe) { NotImplemented_nid_no_patch(filter == EVFILT_READ ? "sceKernelAddReadEvent: low watermark" : "sceKernelAddWriteEvent: low watermark"); } - if (!isSocket) { + if (!isSocket && !isPipe) { return SCE_KERNEL_ERROR_EBADF; } return EqueueAddEvent_nid_postfix(eq, EqueueDescriptorEvent(fd, filter, EV_ADD, udata)); @@ -637,6 +751,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..ef217632d6 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 @@ -41,6 +42,7 @@ static constexpr uint16_t EV_EOF = 0x8000; static constexpr int EQUEUE_OK = 0; struct KernelEqueueEvent; +class KernelEqueuePrivate; using EqueueTriggerFunc = void (*)(KernelEqueueEvent* event, void* triggerData); using EqueueResetFunc = void (*)(KernelEqueueEvent* event); @@ -54,12 +56,16 @@ struct KernelFilter { EqueueResetFunc resetFunc = nullptr; EqueueDeleteFunc deleteEventFunc = nullptr; EqueuePollFunc pollFunc = nullptr; + int waitDescriptor = -1; + std::shared_ptr watched; }; 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 +88,9 @@ 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(); + std::size_t CountTriggeredEvents(); + std::vector> WatchedQueues(); int WaitForEvents(KernelEvent* ev, int num, uint32_t micros); int WaitForEventsUntil(KernelEvent* ev, int num, std::uint64_t deadlineNanos); void RemoveDescriptorEvents(uintptr_t ident); @@ -89,6 +98,7 @@ public: static uint64_t MonotonicNs(); private: + struct WaitSet; void TriggerExpiredTimers(uint64_t nowNs); bool NextTimerWaitMicros(uint64_t nowNs, uint32_t* out) const; int CollectTriggered(KernelEvent* ev, int num); @@ -96,6 +106,9 @@ private: bool PollEvents(); void WakePollers(); void WaitForDescriptors(std::unique_lock& lock, std::uint64_t deadlineNanos); + void GatherInterests(WaitSet& wait, std::uintptr_t waker, bool nested); + static void GatherWatched(WaitSet& wait, const std::shared_ptr& queue, std::uintptr_t waker); + static void ReleaseWatched(WaitSet& wait, std::uintptr_t waker); std::list m_events; std::vector m_pollers; @@ -111,6 +124,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/include/EdgeWatch.hpp b/core/libs/prx/libkernel/Equeue/include/EdgeWatch.hpp new file mode 100644 index 0000000000..230939fd06 --- /dev/null +++ b/core/libs/prx/libkernel/Equeue/include/EdgeWatch.hpp @@ -0,0 +1,29 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_EQUEUE_EDGEWATCH_HPP +#define CORE_LIBS_PRX_LIBKERNEL_EQUEUE_EDGEWATCH_HPP + +#include +#include + +namespace EdgeWatch { + +class Watch { +public: + static std::shared_ptr Open(std::intptr_t hostDescriptor, bool write); + + explicit Watch(int descriptor) : m_descriptor(descriptor) {} + ~Watch(); + Watch(const Watch&) = delete; + Watch& operator=(const Watch&) = delete; + + bool TakeEdge(); + int Descriptor() const { return m_descriptor; } + + bool armed = true; + +private: + int m_descriptor; +}; + +} + +#endif diff --git a/core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp b/core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp new file mode 100644 index 0000000000..60e001c6b3 --- /dev/null +++ b/core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp @@ -0,0 +1,55 @@ +#include "prx/libkernel/Equeue/include/EdgeWatch.hpp" +#include "prx/libc/include/General.hpp" + +#include +#include +#include + +#ifdef __linux__ +#include +#include +#endif + +namespace EdgeWatch { + +#ifdef __linux__ + +std::shared_ptr Watch::Open(std::intptr_t hostDescriptor, bool write) { + const int descriptor = ::epoll_create1(EPOLL_CLOEXEC); + if (descriptor < 0) { + throw std::runtime_error("kevent: EV_CLEAR: epoll_create1 failed, host errno " + std::to_string(errno)); + } + auto watch = std::make_shared(descriptor); + epoll_event interest{}; + interest.events = EPOLLET | (write ? EPOLLOUT : (EPOLLIN | EPOLLRDHUP)); + if (::epoll_ctl(descriptor, EPOLL_CTL_ADD, static_cast(hostDescriptor), &interest) != 0) { + throw std::runtime_error("kevent: EV_CLEAR: watching host descriptor " + std::to_string(hostDescriptor) + " failed, host errno " + std::to_string(errno)); + } + return watch; +} + +Watch::~Watch() { + ::close(m_descriptor); +} + +bool Watch::TakeEdge() { + epoll_event ready{}; + return ::epoll_wait(m_descriptor, &ready, 1, 0) > 0; +} + +#else + +std::shared_ptr Watch::Open(std::intptr_t, bool) { + NotImplemented_nid_no_patch("kevent: EV_CLEAR on a socket or a pipe off Linux"); + return nullptr; +} + +Watch::~Watch() = default; + +bool Watch::TakeEdge() { + return false; +} + +#endif + +} diff --git a/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp b/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp index e72bfe3900..fb42c6e073 100644 --- a/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp +++ b/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp @@ -21,7 +21,13 @@ intptr_t APS5_VABI sceKernelGetEventData(const KernelEvent* ev) { int APS5_VABI sceKernelGetEventError(const KernelEvent* ev) { const KernelEvent& event = requireEvent(ev, __func__); - return (event.flags & EV_ERROR) != 0 ? static_cast(event.data) : 0; + if (event.fflags == 0) { + return 0; + } + if (event.fflags > 0xFFFFu) { + throw std::runtime_error(std::string(__func__) + ": fflags " + std::to_string(event.fflags) + " above 0xFFFF"); + } + return static_cast(0x80020000u | event.fflags); } intptr_t APS5_VABI sceKernelGetEventFflags(const KernelEvent* ev) { diff --git a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp index e3912942b2..cd24ff30d0 100644 --- a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp +++ b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp @@ -17,6 +17,11 @@ #include #include #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#elif defined(__APPLE__) +#include +#include +#include +#include #else #include #include @@ -32,12 +37,17 @@ namespace { constexpr int GUEST_EBADF = 9; constexpr int GUEST_EINVAL = 22; +#ifdef __APPLE__ +static_assert(NOTE_DELETE == 0x0001 && NOTE_WRITE == 0x0002 && NOTE_EXTEND == 0x0004 && NOTE_ATTRIB == 0x0008 && + NOTE_LINK == 0x0010 && NOTE_RENAME == 0x0020); +#else constexpr std::uint32_t NOTE_DELETE = 0x0001; constexpr std::uint32_t NOTE_WRITE = 0x0002; constexpr std::uint32_t NOTE_EXTEND = 0x0004; constexpr std::uint32_t NOTE_ATTRIB = 0x0008; constexpr std::uint32_t NOTE_LINK = 0x0010; constexpr std::uint32_t NOTE_RENAME = 0x0020; +#endif constexpr std::uint32_t MAPPED_NOTES = NOTE_DELETE | NOTE_WRITE | NOTE_EXTEND | NOTE_ATTRIB | NOTE_LINK | NOTE_RENAME; class VnodeWatch { @@ -48,6 +58,15 @@ public: if (stopEvent == nullptr) { throw std::runtime_error("EVFILT_VNODE: CreateEvent failed, error=" + std::to_string(::GetLastError())); } +#elif defined(__APPLE__) + int ends[2]; + if (::pipe(ends) != 0) { + throw std::runtime_error("EVFILT_VNODE: pipe failed, errno=" + std::to_string(errno)); + } + stopEvent = ends[0]; + stopSignal = ends[1]; + ::fcntl(stopEvent, F_SETFD, FD_CLOEXEC); + ::fcntl(stopSignal, F_SETFD, FD_CLOEXEC); #else stopEvent = ::eventfd(0, EFD_CLOEXEC); if (stopEvent < 0) { @@ -59,6 +78,9 @@ public: ~VnodeWatch() { #ifdef _WIN32 ::CloseHandle(stopEvent); +#elif defined(__APPLE__) + ::close(stopEvent); + ::close(stopSignal); #else ::close(stopEvent); #endif @@ -71,6 +93,10 @@ public: stopped.store(true); #ifdef _WIN32 ::SetEvent(stopEvent); +#elif defined(__APPLE__) + const char one = 1; + const auto written = ::write(stopSignal, &one, sizeof(one)); + static_cast(written); #else const std::uint64_t one = 1; const auto written = ::write(stopEvent, &one, sizeof(one)); @@ -96,6 +122,9 @@ private: std::atomic stopped{false}; #ifdef _WIN32 HANDLE stopEvent = nullptr; +#elif defined(__APPLE__) + int stopEvent = -1; + int stopSignal = -1; #else int stopEvent = -1; #endif @@ -136,6 +165,16 @@ struct Snapshot { }; bool TakeSnapshot(HANDLE handle, Snapshot& snapshot) { + std::wstring path(MAX_PATH, L'\0'); + auto length = ::GetFinalPathNameByHandleW(handle, path.data(), static_cast(path.size()), FILE_NAME_NORMALIZED); + if (length >= path.size()) { + path.resize(length); + length = ::GetFinalPathNameByHandleW(handle, path.data(), length, FILE_NAME_NORMALIZED); + } + if (length != 0 && length < path.size()) { + path.resize(length); + snapshot.path = std::move(path); + } FILE_BASIC_INFO basic{}; FILE_STANDARD_INFO standard{}; if (!::GetFileInformationByHandleEx(handle, FileBasicInfo, &basic, sizeof(basic)) || @@ -149,16 +188,6 @@ bool TakeSnapshot(HANDLE handle, Snapshot& snapshot) { snapshot.creation = basic.CreationTime.QuadPart; snapshot.lastWrite = basic.LastWriteTime.QuadPart; snapshot.change = basic.ChangeTime.QuadPart; - std::wstring path(MAX_PATH, L'\0'); - auto length = ::GetFinalPathNameByHandleW(handle, path.data(), static_cast(path.size()), FILE_NAME_NORMALIZED); - if (length >= path.size()) { - path.resize(length); - length = ::GetFinalPathNameByHandleW(handle, path.data(), length, FILE_NAME_NORMALIZED); - } - if (length != 0 && length < path.size()) { - path.resize(length); - snapshot.path = std::move(path); - } return true; } @@ -192,6 +221,24 @@ std::vector SnapshotHints(const Snapshot& before, const Snapshot& return hints; } +std::string hostPathText(const std::wstring& path) { + const int size = ::WideCharToMultiByte(CP_UTF8, 0, path.c_str(), -1, nullptr, 0, nullptr, nullptr); + if (size <= 0) { + return ""; + } + std::string text(static_cast(size), '\0'); + if (::WideCharToMultiByte(CP_UTF8, 0, path.c_str(), -1, text.data(), size, nullptr, nullptr) != size) { + return ""; + } + text.pop_back(); + return text; +} + +std::string changeNotificationFailure(const char* watcher, const std::wstring& path, DWORD error) { + return "EVFILT_VNODE: the " + std::string(watcher) + " watch of \"" + hostPathText(path) + + "\" failed, FindFirstChangeNotification error=" + std::to_string(error); +} + HANDLE WatchParent(const std::wstring& path) { const auto separator = path.find_last_of(L"\\/"); if (separator == std::wstring::npos) { @@ -201,16 +248,16 @@ HANDLE WatchParent(const std::wstring& path) { const HANDLE handle = ::FindFirstChangeNotificationW(parent.c_str(), FALSE, PARENT_CHANGES); const DWORD error = handle == INVALID_HANDLE_VALUE ? ::GetLastError() : ERROR_SUCCESS; if (handle == INVALID_HANDLE_VALUE && error != ERROR_INVALID_FUNCTION) { - throw std::runtime_error("EVFILT_VNODE: FindFirstChangeNotification failed, error=" + std::to_string(error)); + throw std::runtime_error(changeNotificationFailure("parent", parent, error)); } return handle; } -HANDLE WatchContents(const std::wstring& path, DWORD changes) { +HANDLE WatchContents(const std::wstring& path, DWORD changes, const char* watcher) { const HANDLE handle = ::FindFirstChangeNotificationW(path.c_str(), FALSE, changes); if (handle == INVALID_HANDLE_VALUE) { const DWORD error = ::GetLastError(); - throw std::runtime_error("EVFILT_VNODE: FindFirstChangeNotification failed, error=" + std::to_string(error)); + throw std::runtime_error(changeNotificationFailure(watcher, path, error)); } return handle; } @@ -321,10 +368,12 @@ int OpenHostWatch(int descriptor, std::uint32_t notes, HostWatch& host) { throw std::runtime_error("EVFILT_VNODE: reading the watched file failed, error=" + std::to_string(error)); } try { - host.parent = WatchParent(host.last.path); + if (!IsDeleted(host.last)) { + host.parent = WatchParent(host.last.path); + } if (host.directory) { - host.entries = WatchContents(host.last.path, FILE_NOTIFY_CHANGE_FILE_NAME); - host.subdirectories = WatchContents(host.last.path, FILE_NOTIFY_CHANGE_DIR_NAME); + host.entries = WatchContents(host.last.path, FILE_NOTIFY_CHANGE_FILE_NAME, "entries"); + host.subdirectories = WatchContents(host.last.path, FILE_NOTIFY_CHANGE_DIR_NAME, "subdirectories"); } } catch (...) { CloseHostWatch(host); @@ -333,6 +382,80 @@ int OpenHostWatch(int descriptor, std::uint32_t notes, HostWatch& host) { return 0; } +#elif defined(__APPLE__) + +struct HostWatch { + int notify = -1; + int target = -1; +}; + +void CloseHostWatch(HostWatch& host) { + for (int* descriptor : {&host.notify, &host.target}) { + if (*descriptor >= 0) { + ::close(*descriptor); + *descriptor = -1; + } + } +} + +void RunHostWatch(std::shared_ptr watch, HostWatch host) { + struct kevent stop {}; + EV_SET(&stop, watch->StopEvent(), EVFILT_READ, EV_ADD, 0, 0, nullptr); + if (::kevent(host.notify, &stop, 1, nullptr, 0, nullptr) < 0) { + const int error = errno; + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: registering the stop event failed, errno=" + std::to_string(error)); + } + for (;;) { + struct kevent event {}; + if (::kevent(host.notify, nullptr, 0, &event, 1, nullptr) < 0) { + if (errno == EINTR) { + continue; + } + const int error = errno; + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: kevent failed, errno=" + std::to_string(error)); + } + if (event.filter != EVFILT_VNODE) { + break; + } + auto notes = event.fflags & MAPPED_NOTES; + if ((notes & NOTE_DELETE) != 0) { + notes &= ~NOTE_LINK; + } + watch->Deliver(notes); + } + CloseHostWatch(host); +} + +int OpenHostWatch(int descriptor, std::uint32_t, HostWatch& host) { + struct stat status {}; + if (::fstat(descriptor, &status) != 0) { + return GUEST_EBADF; + } + if (S_ISFIFO(status.st_mode) || S_ISSOCK(status.st_mode)) { + return GUEST_EINVAL; + } + if (!S_ISREG(status.st_mode) && !S_ISDIR(status.st_mode)) { + NotImplemented_nid_no_patch("EVFILT_VNODE: a descriptor that is not a file or a directory"); + } + host.target = ::fcntl(descriptor, F_DUPFD_CLOEXEC, 0); + if (host.target < 0) { + const int error = errno; + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: duplicating the watched descriptor failed, errno=" + std::to_string(error)); + } + host.notify = ::kqueue(); + struct kevent change {}; + EV_SET(&change, host.target, EVFILT_VNODE, EV_ADD | EV_CLEAR, MAPPED_NOTES, 0, nullptr); + if (host.notify < 0 || ::kevent(host.notify, &change, 1, nullptr, 0, nullptr) < 0) { + const int error = errno; + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: kqueue failed, errno=" + std::to_string(error)); + } + return 0; +} + #else struct HostWatch { diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index 1696650661..917bc4a8f8 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -1,6 +1,8 @@ #include "prx/libkernel/Equeue/Equeue.hpp" +#include "prx/libkernel/Equeue/include/EdgeWatch.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/HostStreams.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" @@ -34,6 +36,7 @@ constexpr int GUEST_EFAULT = 14; constexpr int GUEST_EINVAL = 22; constexpr int GUEST_ENFILE = 23; constexpr int GUEST_EMFILE = 24; +constexpr int GUEST_EPIPE = 32; constexpr int KQ_NEVENTS = 8; constexpr std::uint32_t NOTE_FFCTRLMASK = 0xc0000000; @@ -52,6 +55,7 @@ constexpr std::uint16_t KNOTE_CLEARED_FLAGS = EV_ADD | EV_DELETE | EV_ENABLE | E std::mutex g_kqueueMutex; std::unordered_map g_kqueues; +std::mutex g_kqueueWatchMutex; int PosixFailure(int error) { *__error_nid_postfix() = error; @@ -98,9 +102,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,24 +128,139 @@ 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.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 AddStandardInputKnote(const KernelEqueueRef& queue, KernelEvent& change, std::uint16_t flags) { + KernelEqueueEvent event{}; + event.event = change; + event.event.flags = flags; + event.event.fflags = 0; + event.event.data = 0; + change.fflags = 0; + change.data = 0; + return GuestErrno(queue->AddEvent(event)); +} + +bool PollEdge(KernelEqueueEvent* e) { + auto& watch = *static_cast(e->filter.data); + if (watch.TakeEdge()) { + watch.armed = true; + } + if (!watch.armed) { + return false; + } + std::int64_t data = 0; + bool eof = false; + if (!GuestSockets::Ready(static_cast(e->event.ident), e->event.filter == EVFILT_WRITE, &data, &eof)) { + watch.armed = false; + return false; + } + if (!eof && data < e->lowWater && !GuestSockets::Listening(static_cast(e->event.ident))) { + watch.armed = false; + return false; + } + e->event.data = static_cast(data); + e->event.flags = static_cast(eof ? (e->event.flags | EV_EOF) : (e->event.flags & ~EV_EOF)); + return true; +} + +void ResetEdge(KernelEqueueEvent* e) { + static_cast(e->filter.data)->armed = false; + e->triggered = false; +} + +void WatchEdges(KernelEqueueEvent& event, bool socket) { + const int descriptor = static_cast(event.event.ident); + auto watch = EdgeWatch::Watch::Open(socket ? GuestSockets::NativeDescriptor(descriptor) : descriptor, event.event.filter == EVFILT_WRITE); + event.filter.data = watch.get(); + event.filter.waitDescriptor = watch->Descriptor(); + event.filter.owner = std::move(watch); + event.filter.pollFunc = PollEdge; + event.filter.resetFunc = ResetEdge; +} + +bool PollKqueue(KernelEqueueEvent* e) { + const auto pending = e->filter.watched->CountTriggeredEvents(); + e->event.data = static_cast(pending); + return pending != 0; +} + +bool KqueueReaches(const KernelEqueueRef& from, const KernelEqueuePrivate* target, std::vector& visited) { + if (from.get() == target) { + return true; + } + if (std::find(visited.begin(), visited.end(), from.get()) != visited.end()) { + return false; + } + visited.push_back(from.get()); + for (const auto& next : from->WatchedQueues()) { + if (KqueueReaches(next, target, visited)) { + return true; + } + } + return false; +} + +int AddKqueueKnote(const KernelEqueueRef& queue, const KernelEqueueRef& watched, KernelEvent& change, std::uint16_t flags) { + if ((flags & EV_CLEAR) != 0) { + NotImplemented_nid_no_patch("kevent: EV_CLEAR on a kqueue descriptor"); + } + std::lock_guard lock(g_kqueueWatchMutex); + std::vector visited; + if (KqueueReaches(watched, queue.get(), visited)) { + NotImplemented_nid_no_patch("kevent: EVFILT_READ on a kqueue that watches the kqueue being registered on"); + } + KernelEqueueEvent event{}; + event.event = change; + event.event.flags = flags; + event.event.fflags = 0; + event.event.data = 0; + event.filter.pollFunc = PollKqueue; + event.filter.watched = watched; + change.fflags = 0; + change.data = 0; + return GuestErrno(queue->AddEvent(event)); +} + int AddDescriptorKnote(const KernelEqueueRef& queue, KernelEvent& change, std::uint16_t flags) { const int descriptor = static_cast(change.ident); - if (IsKqueueDescriptor(descriptor)) { + if (const auto watched = FindKqueue(descriptor)) { if (change.filter != EVFILT_READ) { return GUEST_EINVAL; } - NotImplemented_nid_no_patch("kevent: EVFILT_READ on a kqueue descriptor"); + return AddKqueueKnote(queue, watched, change, flags); } - 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 (descriptor == 0 && change.filter == EVFILT_READ && File::IsHostStream(descriptor)) { + return AddStandardInputKnote(queue, change, flags); } - 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; + bool writable = false; + const bool pipe = !socket && (change.filter == EVFILT_READ ? GuestSockets::PipeReadState(descriptor, &data, &eof) : GuestSockets::PipeWriteState(descriptor, &data, &eof, &writable)); + if (!socket && !pipe) { + NotImplemented_nid_no_patch(change.filter == EVFILT_READ ? "kevent: EVFILT_READ on a descriptor that is neither a socket nor the read end of a pipe" : "kevent: EVFILT_WRITE on a descriptor that is neither a socket nor the write end of a pipe"); } - if ((flags & EV_CLEAR) != 0) { - NotImplemented_nid_no_patch("kevent: EV_CLEAR on a socket"); + if (pipe && change.filter == EVFILT_WRITE && eof) { + return GUEST_EPIPE; } + const auto lowWater = DescriptorLowWater(change); auto event = EqueueDescriptorEvent(descriptor, change.filter, flags, change.udata); + event.lowWater = lowWater; + if ((flags & EV_CLEAR) != 0) { + WatchEdges(event, socket); + } change.fflags = 0; change.data = 0; return GuestErrno(queue->AddEvent(event)); @@ -215,14 +331,29 @@ 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 (event.enabled && event.filter.pollFunc == PollEdge) { + static_cast(event.filter.data)->armed = true; + } if (change.filter == EVFILT_USER) { TouchUserKnote(event, change); } else if (change.filter == EVFILT_VNODE) { @@ -241,15 +372,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 +382,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 +420,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 +470,7 @@ int APS5_VABI kevent_nid_postfix(int descriptor, const KernelEvent* changelist, return PosixFailure(GUEST_EBADF); } if (nevents < 0) { - NotImplemented_nid_no_patch("kevent: negative nevents"); + return PosixFailure(GUEST_EINVAL); } if (nchanges > 0 && changelist == nullptr) { return PosixFailure(GUEST_EFAULT); diff --git a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp index 7d4ebe2ea3..a6403ce990 100644 --- a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp +++ b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp @@ -1,17 +1,24 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include +#include #include +#include +#include #include +#include +#include #include #include static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; +static constexpr int SCE_KERNEL_ERROR_ENOENT = 0x80020002; static constexpr int SCE_KERNEL_ERROR_ESRCH = 0x80020003; static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; static constexpr int SCE_KERNEL_ERROR_EACCES = 0x8002000D; @@ -27,6 +34,15 @@ static constexpr uint32_t EVF_WAITMODE_OR = 0x02; static constexpr uint32_t EVF_WAITMODE_CLEAR_ALL = 0x10; static constexpr uint32_t EVF_WAITMODE_CLEAR_PAT = 0x20; +struct WaiterNode { + uint64_t bitPattern = 0; + uint32_t waitMode = 0; + uint64_t resultPattern = 0; + bool satisfied = false; + WaiterNode* prev = nullptr; + WaiterNode* next = nullptr; +}; + struct KernelEventFlagPrivate { std::mutex _mutex; TimedWait::Condition _changed; @@ -36,8 +52,56 @@ struct KernelEventFlagPrivate { bool _deleted = false; int _waiters = 0; uint64_t _cancelGeneration = 0; + WaiterNode* _waiterHead = nullptr; + WaiterNode* _waiterTail = nullptr; + + void pushWaiter(WaiterNode* node) { + node->prev = _waiterTail; + node->next = nullptr; + if (_waiterTail) { + _waiterTail->next = node; + } else { + _waiterHead = node; + } + _waiterTail = node; + } + + void removeWaiter(WaiterNode* node) { + if (node->prev) { + node->prev->next = node->next; + } else if (_waiterHead == node) { + _waiterHead = node->next; + } + if (node->next) { + node->next->prev = node->prev; + } else if (_waiterTail == node) { + _waiterTail = node->prev; + } + node->prev = nullptr; + node->next = nullptr; + } }; +namespace { + +std::shared_mutex eventFlagRegistryMutex; +std::unordered_map> eventFlagRegistry; + +std::shared_ptr ResolveEventFlag(KernelEventFlag handle) { + if (!handle) return {}; + std::shared_lock lock(eventFlagRegistryMutex); + const auto it = eventFlagRegistry.find(handle); + return it == eventFlagRegistry.end() ? std::shared_ptr{} : it->second; +} + +void RetireEventFlag(KernelEventFlag handle, const std::shared_ptr& owner) { + std::unique_lock lock(eventFlagRegistryMutex); + const auto it = eventFlagRegistry.find(handle); + if (it != eventFlagRegistry.end() && it->second == owner) eventFlagRegistry.erase(it); +} + +} + static bool TraceSync() { static const bool enabled = std::getenv("APS5_TRACE_SYNC") != nullptr; return enabled; @@ -66,43 +130,76 @@ int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, ui (void)param; if (!ef || !name) return SCE_KERNEL_ERROR_EINVAL; if ((attr & EVF_ATTR_SINGLE) && (attr & EVF_ATTR_MULTI)) return SCE_KERNEL_ERROR_EINVAL; - auto* flag = new (std::nothrow) KernelEventFlagPrivate(); - if (!flag) return SCE_KERNEL_ERROR_ENOMEM; - flag->_name = name; - flag->_pattern = init_pattern; - flag->_multipleWaiters = (attr & EVF_ATTR_MULTI) != 0; - *ef = flag; - if (TraceSync()) std::fprintf(stderr, "[evf] create %p '%s' attr=0x%x init=0x%llx\n", static_cast(flag), name, attr, static_cast(init_pattern)); + if ((attr & ~0x33u) != 0) return SCE_KERNEL_ERROR_EINVAL; + std::shared_ptr flag; + try { + flag = std::make_shared(); + flag->_name = name; + flag->_pattern = init_pattern; + flag->_multipleWaiters = (attr & EVF_ATTR_MULTI) != 0; + std::unique_lock lock(eventFlagRegistryMutex); + eventFlagRegistry.emplace(flag.get(), flag); + } catch (const std::bad_alloc&) { + return SCE_KERNEL_ERROR_ENOMEM; + } + *ef = flag.get(); + if (TraceSync()) std::fprintf(stderr, "[evf] create %p '%s' attr=0x%x init=0x%llx\n", static_cast(flag.get()), name, attr, static_cast(init_pattern)); return SCE_OK; } +int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name) { + if (!ef || !name) return SCE_KERNEL_ERROR_EINVAL; + std::shared_lock lock(eventFlagRegistryMutex); + for (const auto& [handle, flag] : eventFlagRegistry) { + std::lock_guard flagLock(flag->_mutex); + if (flag->_deleted || flag->_name != name) continue; + *ef = handle; + return SCE_OK; + } + return SCE_KERNEL_ERROR_ENOENT; +} + int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef) { if (!ef) return SCE_KERNEL_ERROR_ESRCH; - { - std::lock_guard lock(ef->_mutex); - if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; - ef->_deleted = true; - if (ef->_waiters != 0) { - ef->_changed.NotifyAll(); - return SCE_OK; - } - } - delete ef; + const auto keepAlive = ResolveEventFlag(ef); + if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; + std::unique_lock lock(ef->_mutex); + if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; + ef->_deleted = true; + ef->_changed.NotifyAll(); + ef->_changed.Wait(lock, [&] { return ef->_waiters == 0; }); + lock.unlock(); + RetireEventFlag(ef, keepAlive); return SCE_OK; } int APS5_VABI sceKernelSetEventFlag(KernelEventFlag ef, uint64_t bit_pattern) { if (!ef) return SCE_KERNEL_ERROR_ESRCH; + const auto keepAlive = ResolveEventFlag(ef); + if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; std::lock_guard lock(ef->_mutex); if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; ef->_pattern |= bit_pattern; if (TraceSync()) std::fprintf(stderr, "[evf] set %p '%s' |=0x%llx -> 0x%llx\n", static_cast(ef), ef->_name.c_str(), static_cast(bit_pattern), static_cast(ef->_pattern)); + for (auto* waiter = ef->_waiterHead; waiter != nullptr; waiter = waiter->next) { + if (!waiter->satisfied && IsSatisfied(ef->_pattern, waiter->bitPattern, waiter->waitMode)) { + waiter->satisfied = true; + waiter->resultPattern = ef->_pattern; + if (waiter->waitMode & EVF_WAITMODE_CLEAR_ALL) { + ef->_pattern = 0; + } else if (waiter->waitMode & EVF_WAITMODE_CLEAR_PAT) { + ef->_pattern &= ~waiter->bitPattern; + } + } + } ef->_changed.NotifyAll(); return SCE_OK; } int APS5_VABI sceKernelClearEventFlag(KernelEventFlag ef, uint64_t bit_pattern) { if (!ef) return SCE_KERNEL_ERROR_ESRCH; + const auto keepAlive = ResolveEventFlag(ef); + if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; std::lock_guard lock(ef->_mutex); if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; ef->_pattern &= bit_pattern; @@ -111,6 +208,8 @@ int APS5_VABI sceKernelClearEventFlag(KernelEventFlag ef, uint64_t bit_pattern) int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, uint64_t set_pattern, int* num_wait_threads) { if (!ef) return SCE_KERNEL_ERROR_ESRCH; + const auto keepAlive = ResolveEventFlag(ef); + if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; std::lock_guard lock(ef->_mutex); if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; if (num_wait_threads) *num_wait_threads = ef->_waiters; @@ -122,6 +221,8 @@ int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, uint64_t set_pattern, int APS5_VABI sceKernelPollEventFlag(KernelEventFlag ef, uint64_t bit_pattern, uint32_t wait_mode, uint64_t* result_pat) { if (!ef) return SCE_KERNEL_ERROR_ESRCH; + const auto keepAlive = ResolveEventFlag(ef); + if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; if (bit_pattern == 0 || !IsValidWaitMode(wait_mode)) return SCE_KERNEL_ERROR_EINVAL; std::lock_guard lock(ef->_mutex); if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; @@ -136,14 +237,33 @@ int APS5_VABI sceKernelPollEventFlag(KernelEventFlag ef, uint64_t bit_pattern, u int APS5_VABI sceKernelWaitEventFlag(KernelEventFlag ef, uint64_t bit_pattern, uint32_t wait_mode, uint64_t* result_pat, KernelUseconds* timeout) { if (!ef) return SCE_KERNEL_ERROR_ESRCH; + const auto keepAlive = ResolveEventFlag(ef); + if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; if (bit_pattern == 0 || !IsValidWaitMode(wait_mode)) return SCE_KERNEL_ERROR_EINVAL; std::unique_lock lock(ef->_mutex); if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; if (!ef->_multipleWaiters && ef->_waiters != 0) return SCE_KERNEL_ERROR_EPERM; + if (IsSatisfied(ef->_pattern, bit_pattern, wait_mode)) { + Consume(ef, bit_pattern, wait_mode, result_pat); + return SCE_OK; + } const uint64_t generation = ef->_cancelGeneration; if (TraceSync()) std::fprintf(stderr, "[evf] wait %p '%s' want=0x%llx mode=0x%x have=0x%llx timeout=%d caller=%p outer=%p\n", static_cast(ef), ef->_name.c_str(), static_cast(bit_pattern), wait_mode, static_cast(ef->_pattern), timeout ? static_cast(*timeout) : -1, __builtin_return_address(0), static_cast(static_cast(__builtin_frame_address(0))[0])[1]); - const auto released = [&] { return ef->_deleted || ef->_cancelGeneration != generation || IsSatisfied(ef->_pattern, bit_pattern, wait_mode); }; ++ef->_waiters; + WaiterNode waiter; + waiter.bitPattern = bit_pattern; + waiter.waitMode = wait_mode; + ef->pushWaiter(&waiter); + struct WaiterGuard { + KernelEventFlagPrivate* flag; + WaiterNode* node; + ~WaiterGuard() { + flag->removeWaiter(node); + --flag->_waiters; + flag->_changed.NotifyAll(); + } + } waiterGuard{ef, &waiter}; + const auto released = [&] { return ef->_deleted || ef->_cancelGeneration != generation || waiter.satisfied; }; bool timedOut = false; const auto waitStart = std::chrono::steady_clock::now(); if (timeout) { @@ -154,23 +274,26 @@ int APS5_VABI sceKernelWaitEventFlag(KernelEventFlag ef, uint64_t bit_pattern, u ef->_changed.Wait(lock, released); } KernelTraceWait_nid_postfix("evf", __builtin_return_address(0), static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart).count()), timedOut); - --ef->_waiters; if (ef->_deleted) { - const bool last = ef->_waiters == 0; - lock.unlock(); - if (last) delete ef; return SCE_KERNEL_ERROR_EACCES; } if (ef->_cancelGeneration != generation) { if (result_pat) *result_pat = ef->_pattern; return SCE_KERNEL_ERROR_ECANCELED; } - if (timedOut) { + if (timedOut && !waiter.satisfied) { if (result_pat) *result_pat = ef->_pattern; return SCE_KERNEL_ERROR_ETIMEDOUT; } - Consume(ef, bit_pattern, wait_mode, result_pat); + if (result_pat) *result_pat = waiter.resultPattern; return SCE_OK; } } + +extern "C" int APS5_Test_GetEventFlagWaiterCount(KernelEventFlag handle) { + const auto keepAlive = ResolveEventFlag(handle); + if (!keepAlive) return -1; + std::lock_guard lock(keepAlive->_mutex); + return keepAlive->_waiters; +} 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..4358f8e67d 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -9,12 +9,18 @@ enum class FcntlArgument { None, Int, Pointer }; FcntlArgument FcntlArgumentOf(int command); int Fcntl(int fd, int command, std::intptr_t argument); +bool IoctlRequestMalformed(std::uint64_t request); int Ioctl(int fd, std::uint64_t request, void* argument); int DuplicateFrom(int fd, int start); void RememberDescriptor(int fd, int openFlags); void ShareDescriptor(int from, int to); void ForgetDescriptor(int fd); +bool Appends(int fd); +bool IsVnode(int fd); +bool IsCharacterDevice(int fd); } +extern "C" bool DescriptorHasNoVnode_nid_no_patch(int fd); + #endif diff --git a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp index a740c9ac0a..6978a7998a 100644 --- a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -5,17 +5,22 @@ #include #include +struct FileStat; + extern "C" const char* GetDirectoryDescriptorPath_nid_no_patch(int fd, int* error); +extern "C" std::uint64_t GetDirectoryDescriptorPosition_nid_no_patch(int fd); extern "C" int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std::uint16_t mode); +extern "C" int StatInDirectory_nid_no_patch(int directory, const char* path, FileStat* sb, bool follow); namespace File { int OpenDirectoryDescriptor(const std::filesystem::path& path); +bool IsDirectoryDescriptor(int fd); std::optional DirectoryDescriptorPath(int fd); void ForgetDirectoryDescriptor(int fd); void DuplicateDirectoryDescriptor(int from, int to); std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, int whence); -int ReadDirectoryDescriptor(int fd, char* buf, int nbytes); +int ReadDirectoryDescriptor(int fd, char* buf, int nbytes, std::int64_t* base = nullptr); } diff --git a/core/libs/prx/libkernel/File/include/File.hpp b/core/libs/prx/libkernel/File/include/File.hpp index 4a14063e0e..6c8fbfadc2 100644 --- a/core/libs/prx/libkernel/File/include/File.hpp +++ b/core/libs/prx/libkernel/File/include/File.hpp @@ -3,7 +3,9 @@ #include #include +#include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -27,9 +29,15 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ bool DescriptorIsOpen_nid_no_patch(int descriptor); namespace File { +#ifdef _WIN32 +std::optional WindowsDescriptorPath_nid_no_patch(int descriptor); +#endif 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/FileFlags.hpp b/core/libs/prx/libkernel/File/include/FileFlags.hpp index 2423ccdacf..374abfc4c5 100644 --- a/core/libs/prx/libkernel/File/include/FileFlags.hpp +++ b/core/libs/prx/libkernel/File/include/FileFlags.hpp @@ -19,4 +19,13 @@ constexpr std::int32_t SCE_KERNEL_O_NONBLOCK = 0x00000004; constexpr std::int32_t SCE_KERNEL_O_DIRECT = 0x00010000; constexpr std::int32_t SCE_KERNEL_O_DIRECTORY = 0x00020000; +namespace File { + +constexpr bool InvalidOpenFlags(std::int32_t flags) { + constexpr std::int32_t createExclusiveDirectory = SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL | SCE_KERNEL_O_DIRECTORY; + return (flags & SCE_KERNEL_O_ACCMODE) == SCE_KERNEL_O_ACCMODE || (flags & createExclusiveDirectory) == createExclusiveDirectory; +} + +} + #endif diff --git a/core/libs/prx/libkernel/File/include/HostStreams.hpp b/core/libs/prx/libkernel/File/include/HostStreams.hpp new file mode 100644 index 0000000000..d40bcb1600 --- /dev/null +++ b/core/libs/prx/libkernel/File/include/HostStreams.hpp @@ -0,0 +1,10 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_HOSTSTREAMS_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_HOSTSTREAMS_HPP + +namespace File { + +bool IsHostStream(int fd); + +} + +#endif diff --git a/core/libs/prx/libkernel/File/include/NativeStat.hpp b/core/libs/prx/libkernel/File/include/NativeStat.hpp index 0d0e003697..b60abe0b89 100644 --- a/core/libs/prx/libkernel/File/include/NativeStat.hpp +++ b/core/libs/prx/libkernel/File/include/NativeStat.hpp @@ -6,8 +6,8 @@ namespace File { -std::filesystem::path PathOrRelinkedModule(const std::filesystem::path& nativePath); void FillFileStat(const std::filesystem::path& nativePath, FileStat* sb); +bool TryFillFileStat(const std::filesystem::path& nativePath, FileStat* sb); void FillFileStat(int nativeDescriptor, FileStat* sb); bool FillFileStatFromDescriptor(int fd, FileStat* sb); bool FillLinkStat(const std::filesystem::path& nativePath, FileStat* sb); diff --git a/core/libs/prx/libkernel/File/include/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/include/WindowsFileTime.hpp b/core/libs/prx/libkernel/File/include/WindowsFileTime.hpp index 07721951e2..9b3a4a27fd 100644 --- a/core/libs/prx/libkernel/File/include/WindowsFileTime.hpp +++ b/core/libs/prx/libkernel/File/include/WindowsFileTime.hpp @@ -38,6 +38,10 @@ public: static int Failure(DWORD error) { const auto condition = std::system_category().default_error_condition(static_cast(error)); +#ifdef ELOOP + if (error == ERROR_CANT_RESOLVE_FILENAME) errno = ELOOP; + else +#endif if (condition == std::errc::no_such_file_or_directory) errno = ENOENT; else if (condition == std::errc::permission_denied) errno = EACCES; else if (condition == std::errc::bad_file_descriptor) errno = EBADF; diff --git a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp new file mode 100644 index 0000000000..5d3e3bb584 --- /dev/null +++ b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp @@ -0,0 +1,127 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_WINDOWSSYMBOLICLINK_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_WINDOWSSYMBOLICLINK_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace File { + +class WindowsSymbolicLink { + struct ReparseData { + ULONG tag; + USHORT dataLength; + USHORT reserved; + USHORT substituteNameOffset; + USHORT substituteNameLength; + USHORT printNameOffset; + USHORT printNameLength; + ULONG flags; + }; + static constexpr ULONG relativeFlag = 1; + + static std::wstring TextAt(const std::byte* data, DWORD size, std::size_t origin, USHORT offset, USHORT length, const char* operation, const char* malformed) { + const std::size_t begin = origin + offset; + if (length % sizeof(wchar_t) != 0 || begin + length > size) + throw std::runtime_error(std::string(operation) + ": " + malformed); + std::wstring name(length / sizeof(wchar_t), L'\0'); + if (!name.empty()) std::memcpy(name.data(), data + begin, name.size() * sizeof(wchar_t)); + return name; + } + + static std::wstring WithoutNtPrefix(std::wstring name) { + if (name.rfind(L"\\??\\", 0) == 0 || name.rfind(L"\\\\?\\", 0) == 0) name.erase(0, 4); + return name; + } + + static bool IsDriveAbsolute(const std::wstring& name) { + if (name.size() < 3 || name[1] != L':' || (name[2] != L'\\' && name[2] != L'/')) return false; + const wchar_t letter = name[0]; + return (letter >= L'A' && letter <= L'Z') || (letter >= L'a' && letter <= L'z'); + } + +public: + static bool IsNameSurrogate(const std::filesystem::path& native, const char* operation) { + const HANDLE handle = CreateFileW(native.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) + throw std::system_error(static_cast(GetLastError()), std::system_category(), std::string(operation) + ": CreateFileW"); + FILE_ATTRIBUTE_TAG_INFO information{}; + const BOOL read = GetFileInformationByHandleEx(handle, FileAttributeTagInfo, &information, sizeof(information)); + const DWORD code = GetLastError(); + CloseHandle(handle); + if (!read) + throw std::system_error(static_cast(code), std::system_category(), std::string(operation) + ": GetFileInformationByHandleEx"); + return (information.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 && IsReparseTagNameSurrogate(information.ReparseTag); + } + + static bool Read(const std::filesystem::path& native, std::string& target, const char* operation) { + const HANDLE handle = CreateFileW(native.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) + throw std::system_error(static_cast(GetLastError()), std::system_category(), std::string(operation) + ": CreateFileW"); + std::array data{}; + DWORD size = 0; + const BOOL read = DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, nullptr, 0, data.data(), static_cast(data.size()), &size, nullptr); + const DWORD code = GetLastError(); + CloseHandle(handle); + if (!read) + throw std::system_error(static_cast(code), std::system_category(), std::string(operation) + ": FSCTL_GET_REPARSE_POINT"); + ReparseData header{}; + if (size < sizeof(header)) return false; + std::memcpy(&header, data.data(), sizeof(header)); + if (header.tag != IO_REPARSE_TAG_SYMLINK) return false; + const bool relative = (header.flags & relativeFlag) != 0; + std::wstring name = relative + ? TextAt(data.data(), size, sizeof(ReparseData), header.substituteNameOffset, header.substituteNameLength, operation, "malformed symbolic link reparse data") + : TextAt(data.data(), size, sizeof(ReparseData), header.printNameOffset, header.printNameLength, operation, "malformed symbolic link reparse data"); + if (!relative && name.empty()) + name = WithoutNtPrefix(TextAt(data.data(), size, sizeof(ReparseData), header.substituteNameOffset, header.substituteNameLength, operation, "malformed symbolic link reparse data")); + target = std::filesystem::path(name).generic_string(); + return true; + } + + static bool ReadJunction(const std::filesystem::path& native, std::string& target, const char* operation) { + const HANDLE handle = CreateFileW(native.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) + throw std::system_error(static_cast(GetLastError()), std::system_category(), std::string(operation) + ": CreateFileW"); + std::array data{}; + DWORD size = 0; + const BOOL read = DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, nullptr, 0, data.data(), static_cast(data.size()), &size, nullptr); + const DWORD code = GetLastError(); + CloseHandle(handle); + if (!read) + throw std::system_error(static_cast(code), std::system_category(), std::string(operation) + ": FSCTL_GET_REPARSE_POINT"); + struct MountPoint { + ULONG tag; + USHORT dataLength; + USHORT reserved; + USHORT substituteNameOffset; + USHORT substituteNameLength; + USHORT printNameOffset; + USHORT printNameLength; + } header{}; + if (size < sizeof(header)) return false; + std::memcpy(&header, data.data(), sizeof(header)); + if (header.tag != IO_REPARSE_TAG_MOUNT_POINT) return false; + const auto nameAt = [&](USHORT offset, USHORT length) { + return WithoutNtPrefix(TextAt(data.data(), size, sizeof(header), offset, length, operation, "malformed junction reparse data")); + }; + std::wstring name = nameAt(header.printNameOffset, header.printNameLength); + if (!IsDriveAbsolute(name)) name = nameAt(header.substituteNameOffset, header.substituteNameLength); + if (!IsDriveAbsolute(name)) return false; + target = std::filesystem::path(name).generic_string(); + return true; + } +}; + +} + +#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..d9cf12e8bc 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -2,10 +2,13 @@ #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/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/FileLock.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -91,6 +94,8 @@ constexpr std::uint32_t KeepOnExecRequest = 0x20006602; constexpr std::uint32_t NonblockingRequest = 0x8004667e; constexpr std::uint32_t ReadableRequest = 0x4004667f; constexpr std::uint32_t RequestIn = 0x80000000u; +constexpr std::uint32_t RequestOut = 0x40000000u; +constexpr std::uint32_t RequestVoid = 0x20000000u; constexpr std::uint32_t TerminalGroup = 't'; struct GuestLock { @@ -105,7 +110,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, Socket, Device, RandomDevice, ArchivedFile }; struct Entry { bool closeOnExec = false; @@ -125,8 +130,10 @@ const char* KindName(Kind kind) { case Kind::File: return "file"; case Kind::Directory: return "directory"; case Kind::Pipe: return "pipe"; + case Kind::Socket: return "socket"; case Kind::Device: return "character device"; case Kind::RandomDevice: return "random device"; + case Kind::ArchivedFile: return "file read from app0.zar"; } return "descriptor"; } @@ -138,8 +145,9 @@ const char* KindName(Kind kind) { #ifdef _WIN32 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::IsDirectoryDescriptor(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,11 +267,13 @@ 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; if (S_ISCHR(status.st_mode)) return Kind::Device; - throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(fd) + " is neither a file, a directory, a pipe nor a character device"); + if (S_ISSOCK(status.st_mode)) return Kind::Socket; + throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(fd) + " is neither a file, a directory, a pipe, a socket nor a character device"); } int HostStatus(int fd, const char* function) { @@ -340,7 +350,7 @@ int ChangeStatus(int fd, Kind kind, int mask, int value, const char* function) { auto& status = *Materialize(fd).status; const int next = (status & ~mask) | (value & mask); const int changed = status ^ next; - if ((changed & Asynchronous) && (kind == Kind::Pipe || kind == Kind::Device)) Unsupported(function, "O_ASYNC", kind, fd); + if ((changed & Asynchronous) && (kind == Kind::Pipe || kind == Kind::Socket || kind == Kind::Device)) Unsupported(function, "O_ASYNC", kind, fd); if ((changed & Synchronous) && kind == Kind::File) Unsupported(function, "changing O_FSYNC", kind, fd); ApplyHostStatus(fd, kind, changed, next, function); status = next; @@ -366,7 +376,7 @@ int RecordLock(int fd, int command, std::intptr_t argument) { GuestLock lock{}; std::memcpy(&lock, pointer, size); const auto kind = Classify(fd, "fcntl"); - if (!kind || *kind == Kind::Pipe) return Fail(GuestEbadf); + if (!kind || *kind == Kind::Pipe || *kind == Kind::Socket) return Fail(GuestEbadf); if (*kind != Kind::File && *kind != Kind::Directory) Unsupported("fcntl", "a record lock", *kind, fd); #ifdef _WIN32 return ProcessRecordLock(fd, *kind, current, lock, pointer, size); @@ -374,10 +384,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); @@ -408,8 +416,8 @@ 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 == Kind::Pipe || *kind == Kind::Socket) return Fail(GuestEbadf); + if (*kind != Kind::File && *kind != Kind::ArchivedFile) return Fail(GuestEnotty); std::lock_guard lock(g_mutex); auto& status = *Materialize(fd).status; if (command == DarwinReadAhead || argument >= 0) status |= ReadAheadFlag; @@ -505,7 +513,16 @@ int Fcntl(int fd, int command, std::intptr_t argument) { } } +bool IoctlRequestMalformed(std::uint64_t request) { + const auto command = static_cast(request); + const std::uint32_t length = (command >> 16) & 0x1fff; + if ((command & (RequestIn | RequestOut | RequestVoid)) == 0) return true; + if ((command & RequestOut) && length == 0) return true; + return (command & RequestVoid) && length != 0 && length != sizeof(int); +} + int Ioctl(int fd, std::uint64_t request, void* argument) { + if (IoctlRequestMalformed(request)) return Fail(GuestEnotty); const auto command = static_cast(request); const std::uint32_t length = (command >> 16) & 0x1fff; if ((command & RequestIn) && length != 0 && argument == nullptr) return Fail(GuestEfault); @@ -521,8 +538,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::Socket && *kind != Kind::ArchivedFile) Unsupported("ioctl", "FIONREAD", *kind, fd); + const auto archived = File::ArchivedFileRemaining(fd); + const int available = archived ? static_cast(std::min(*archived, std::numeric_limits::max())) : Readable(fd, *kind, "ioctl"); if (argument == nullptr) return Fail(GuestEfault); const GuestArena::HostWrite destination(argument, sizeof(available)); if (!destination.Open()) return Fail(GuestEfault); @@ -544,6 +562,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 +573,41 @@ 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 IsCharacterDevice(int fd) { + if (fd < 0) return false; +#ifdef _WIN32 + return DescriptorIsOpen_nid_no_patch(fd) && !GuestSockets::IsOpen(fd) && !File::DirectoryDescriptorPath(fd) && ::GetFileType(WindowsDescriptorHandle_nid_no_patch(fd)) == FILE_TYPE_CHAR; +#else + struct stat status{}; + return ::fstat(fd, &status) == 0 && S_ISCHR(status.st_mode); +#endif +} + +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 +} + +} + +extern "C" bool DescriptorHasNoVnode_nid_no_patch(int fd) { + return !File::IsVnode(fd) && !File::IsCharacterDevice(fd) && !File::IsRandomDevice(fd); } diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index 05868357b8..e140a6b0f4 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -8,6 +8,7 @@ #ifdef _WIN32 +#include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/General.hpp" #include @@ -17,10 +18,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -32,7 +35,6 @@ constexpr std::uint8_t GuestRegularType = 8; struct DirectoryState { std::mutex mutex; - std::filesystem::path path; std::vector> entries; std::size_t cursor = 0; bool loaded = false; @@ -48,37 +50,69 @@ std::shared_ptr Find(int fd) { return found == g_directories.end() ? nullptr : found->second; } -void Load(DirectoryState& state) { +void Load(DirectoryState& state, int fd) { if (state.loaded) return; + const auto path = File::WindowsDescriptorPath_nid_no_patch(fd); + if (!path) throw std::system_error(errno, std::generic_category(), "directory descriptor path"); state.loaded = true; state.entries.emplace_back(".", GuestDirectoryType); state.entries.emplace_back("..", GuestDirectoryType); std::error_code error; - for (const auto& entry : std::filesystem::directory_iterator(state.path, error)) { + for (const auto& entry : std::filesystem::directory_iterator(*path, error)) { std::error_code typeError; state.entries.emplace_back(entry.path().filename().string(), entry.is_directory(typeError) ? GuestDirectoryType : GuestRegularType); } } +int OpenStreamDirectory(const wchar_t* path) { + return File::OpenDirectoryDescriptor(path); +} + +struct StreamRegistration { + StreamRegistration() { LibcRegisterDirectoryStreams_nid_no_patch(&OpenStreamDirectory, &File::ForgetDirectoryDescriptor); } + ~StreamRegistration() { LibcRegisterDirectoryStreams_nid_no_patch(nullptr, nullptr); } + StreamRegistration(const StreamRegistration&) = delete; + StreamRegistration& operator=(const StreamRegistration&) = delete; +}; + +const StreamRegistration g_streamRegistration; + } namespace File { int OpenDirectoryDescriptor(const std::filesystem::path& path) { - const int fd = ::_open("NUL", _O_RDONLY | _O_BINARY); - if (fd < 0) return -1; - std::lock_guard lock(g_mutex); const auto state = std::make_shared(); - state->path = path; - g_directories[fd] = state; - g_directoryCount = g_directories.size(); + SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; + const auto handle = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + &security, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + errno = WindowsOpenError_nid_no_patch(GetLastError()); + return -1; + } + const int fd = ::_open_osfhandle(reinterpret_cast(handle), _O_RDONLY | _O_BINARY); + if (fd < 0) { + CloseHandle(handle); + return -1; + } + try { + std::lock_guard lock(g_mutex); + g_directories[fd] = state; + g_directoryCount = g_directories.size(); + } catch (...) { + ::_close(fd); + throw; + } return fd; } +bool IsDirectoryDescriptor(int fd) { + return g_directoryCount != 0 && Find(fd) != nullptr; +} + std::optional DirectoryDescriptorPath(int fd) { - const auto state = Find(fd); - if (!state) return std::nullopt; - return state->path; + if (!IsDirectoryDescriptor(fd)) return std::nullopt; + return WindowsDescriptorPath_nid_no_patch(fd); } void ForgetDirectoryDescriptor(int fd) { @@ -100,15 +134,20 @@ void DuplicateDirectoryDescriptor(int from, int to) { std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, int whence) { constexpr int SeekSet = 0; constexpr int SeekCurrent = 1; + constexpr int SeekEnd = 2; if (g_directoryCount == 0) return std::nullopt; const auto found = Find(fd); if (!found) return std::nullopt; auto& state = *found; std::lock_guard lock(state.mutex); - if (whence != SeekSet && whence != SeekCurrent) NotImplemented_nid_no_patch("lseek from the end of a directory descriptor"); - const auto cursor = static_cast(state.cursor); + if (whence != SeekSet && whence != SeekCurrent && whence != SeekEnd) { + errno = EINVAL; + return std::int64_t{-1}; + } + if (whence == SeekEnd) Load(state, fd); + const auto cursor = static_cast(whence == SeekEnd ? state.entries.size() : state.cursor); if (whence == SeekCurrent && offset == 0) return cursor; - if (whence == SeekCurrent && offset > std::numeric_limits::max() - cursor) { + if ((whence == SeekCurrent || whence == SeekEnd) && offset > std::numeric_limits::max() - cursor) { errno = EOVERFLOW; return std::int64_t{-1}; } @@ -123,17 +162,18 @@ std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, state.cursor = 0; return std::int64_t{0}; } - Load(state); + Load(state, fd); state.cursor = static_cast(target); return target; } -int ReadDirectoryDescriptor(int fd, char* buf, int nbytes) { +int ReadDirectoryDescriptor(int fd, char* buf, int nbytes, std::int64_t* base) { const auto found = Find(fd); if (!found) return SCE_KERNEL_ERROR_ENOTDIR; auto& state = *found; std::lock_guard lock(state.mutex); - Load(state); + Load(state, fd); + if (base != nullptr) *base = static_cast(state.cursor); std::size_t used = 0; while (state.cursor < state.entries.size()) { const auto& [name, type] = state.entries[state.cursor]; @@ -188,7 +228,11 @@ extern "C" const char* GetDirectoryDescriptorPath_nid_no_patch(int fd, int* erro } const auto directory = File::DirectoryDescriptorPath(fd); if (!directory) { +#ifdef _WIN32 + *error = File::IsDirectoryDescriptor(fd) ? errno : ENOTDIR; +#else *error = ENOTDIR; +#endif return nullptr; } struct PathTag {}; @@ -198,3 +242,13 @@ extern "C" const char* GetDirectoryDescriptorPath_nid_no_patch(int fd, int* erro *error = 0; return path.c_str(); } + +extern "C" std::uint64_t GetDirectoryDescriptorPosition_nid_no_patch(int fd) { +#ifdef _WIN32 + const auto position = File::SeekDirectoryDescriptor(fd, 0, 1); + return position && *position > 0 ? static_cast(*position) : 0; +#else + (void)fd; + return 0; +#endif +} diff --git a/core/libs/prx/libkernel/File/src/HostStreams.cpp b/core/libs/prx/libkernel/File/src/HostStreams.cpp new file mode 100644 index 0000000000..0dbc0a53c9 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/HostStreams.cpp @@ -0,0 +1,40 @@ +#include "prx/libkernel/File/include/HostStreams.hpp" +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +namespace { + +#ifdef _WIN32 +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} +#endif + +bool OpenAtStart(int fd) { +#ifdef _WIN32 + const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); + const bool open = _get_osfhandle(fd) != -1; + _set_thread_local_invalid_parameter_handler(previous); + return open; +#else + return ::fcntl(fd, F_GETFD) != -1; +#endif +} + +const std::array g_hostStreams = {OpenAtStart(0), OpenAtStart(1), OpenAtStart(2)}; + +} + +namespace File { + +bool IsHostStream(int fd) { + return fd >= 0 && fd < static_cast(g_hostStreams.size()) && g_hostStreams[static_cast(fd)]; +} + +} diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 5e6e304a73..1fda5aabf7 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -1,21 +1,26 @@ #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 +#include #include #include #ifdef _WIN32 #include "prx/libkernel/File/include/WindowsFileTime.hpp" +#include "prx/libkernel/File/include/WindowsSymbolicLink.hpp" +#include "prx/libc/include/WindowsDescriptor.hpp" #include #include #include +constexpr unsigned short SymbolicLinkMode = 0120777; struct NativeStat : __stat64 { FILE_BASIC_INFO times{}; BY_HANDLE_FILE_INFORMATION identity{}; bool hasTimes = false; bool hasIdentity = false; + bool isSymbolicLink = false; }; static int ReadHandleInfo(HANDLE handle, NativeStat* st) { if (GetFileType(handle) != FILE_TYPE_DISK) return 0; @@ -25,12 +30,11 @@ static int ReadHandleInfo(HANDLE handle, NativeStat* st) { st->hasIdentity = GetFileInformationByHandle(handle, &st->identity) != 0; return 0; } -static std::uint32_t FoldFileIndex(const BY_HANDLE_FILE_INFORMATION& identity) { - const std::uint32_t folded = identity.nFileIndexLow ^ (identity.nFileIndexHigh * 0x9E3779B1u); - return folded == 0 ? 1 : folded; -} static int DoStat(const std::filesystem::path& p, NativeStat* st) { - if (_wstat64(p.c_str(), st) != 0) return -1; + if (_wstat64(p.c_str(), st) != 0) { + if (_doserrno == ERROR_CANT_RESOLVE_FILENAME) File::WindowsFileTime::Failure(_doserrno); + return -1; + } const auto handle = CreateFileW(p.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); if (handle == INVALID_HANDLE_VALUE) return File::WindowsFileTime::Failure(GetLastError()); @@ -39,15 +43,34 @@ static int DoStat(const std::filesystem::path& p, NativeStat* st) { return result; } static int DoFstat(int fd, NativeStat* st) { - if (const auto directory = File::DirectoryDescriptorPath(fd)) return DoStat(*directory, st); + if (WindowsDescriptorHandle_nid_no_patch(fd) == INVALID_HANDLE_VALUE) { + errno = EBADF; + return -1; + } if (_fstat64(fd, st) != 0) return -1; return ReadHandleInfo(reinterpret_cast(::_get_osfhandle(fd)), st); } static int DoLstat(const std::filesystem::path& p, NativeStat* st) { const auto attributes = GetFileAttributesW(p.c_str()); - if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) - NotImplemented_nid_no_patch("lstat of a Windows symbolic link"); - return DoStat(p, st); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return DoStat(p, st); + if (!File::WindowsSymbolicLink::IsNameSurrogate(p, "lstat")) return DoStat(p, st); + std::string target; + const bool link = File::WindowsSymbolicLink::Read(p, target, "lstat") || File::WindowsSymbolicLink::ReadJunction(p, target, "lstat"); + const auto handle = CreateFileW(p.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) return File::WindowsFileTime::Failure(GetLastError()); + const int result = ReadHandleInfo(handle, st); + CloseHandle(handle); + if (result != 0) return result; + if (!link) { + st->st_mode = (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0 ? (_S_IFDIR | 0111) : _S_IFREG; + st->st_mode |= (attributes & FILE_ATTRIBUTE_READONLY) != 0 ? 0444 : 0666; + return 0; + } + st->st_mode = SymbolicLinkMode; + st->st_size = static_cast<__int64>(target.size()); + st->isSymbolicLink = true; + return 0; } #else #include @@ -68,7 +91,7 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { sb->st_mode = static_cast(st.st_mode); sb->st_size = static_cast(st.st_size); #ifdef _WIN32 - if (st.hasIdentity) sb->st_size = static_cast((static_cast(st.identity.nFileSizeHigh) << 32u) | st.identity.nFileSizeLow); + if (st.hasIdentity && !st.isSymbolicLink) sb->st_size = static_cast((static_cast(st.identity.nFileSizeHigh) << 32u) | st.identity.nFileSizeLow); sb->st_dev = static_cast(st.st_dev); sb->st_ino = static_cast(st.st_ino); sb->st_nlink = static_cast(st.st_nlink); @@ -93,7 +116,7 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { } if (st.hasIdentity) { sb->st_dev = st.identity.dwVolumeSerialNumber; - sb->st_ino = FoldFileIndex(st.identity); + sb->st_ino = WindowsFileNumber_nid_no_patch(st.identity); sb->st_nlink = static_cast(std::min(st.identity.nNumberOfLinks, 0xffff)); } #else @@ -125,18 +148,17 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { namespace File { -std::filesystem::path PathOrRelinkedModule(const std::filesystem::path& nativePath) { - std::error_code error; - if (std::filesystem::exists(std::filesystem::symlink_status(nativePath, error))) return nativePath; - return RelinkedModulePath_nid_no_patch(nativePath); -} - void FillFileStat(const std::filesystem::path& nativePath, FileStat* sb) { - NativeStat st{}; - if (DoStat(nativePath, &st) != 0) { + if (!TryFillFileStat(nativePath, sb)) { throw std::runtime_error(std::string("FillFileStat: stat failed for ") + nativePath.string()); } +} + +bool TryFillFileStat(const std::filesystem::path& nativePath, FileStat* sb) { + NativeStat st{}; + if (DoStat(nativePath, &st) != 0) return false; CopyNativeStat(st, sb); + return true; } void FillFileStat(int nativeDescriptor, FileStat* sb) { @@ -148,6 +170,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 4989903336..417c79bd96 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -2,6 +2,7 @@ #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/RenameDot.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/File/include/File.hpp" @@ -11,6 +12,8 @@ #include "prx/libkernel/File/include/FileLock.hpp" #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" @@ -26,6 +29,7 @@ #include #include #include +#include "prx/libkernel/File/include/WindowsSymbolicLink.hpp" static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint16_t mode) { const int access = nativeFlags & (_O_RDONLY | _O_WRONLY | _O_RDWR); const DWORD desired = access == _O_RDONLY ? GENERIC_READ : access == _O_WRONLY ? GENERIC_WRITE : GENERIC_READ | GENERIC_WRITE; @@ -56,13 +60,13 @@ static int NativeRead(int fd, void* buf, std::size_t n) { if (n > static_cast(std::numeric_limits::max())) { throw std::runtime_error("sceKernelRead: nbytes exceeds platform limit"); } - const auto positioned = File::LockPositionedTransfer(fd); - const int result = ::_read(fd, buf, static_cast(n)); - if (result < 0 && _doserrno == ERROR_NO_DATA) errno = EAGAIN; - if (result == 0 && File::DirectoryDescriptorPath(fd)) { + if (File::IsDirectoryDescriptor(fd)) { errno = EISDIR; return -1; } + const auto positioned = File::LockPositionedTransfer(fd); + const int result = ::_read(fd, buf, static_cast(n)); + if (result < 0 && _doserrno == ERROR_NO_DATA) errno = EAGAIN; return result; } static int NativeWrite(int fd, const void* buf, std::size_t n) { @@ -72,6 +76,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; @@ -95,6 +103,24 @@ 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 bool NativeSymbolicLinkLoop(const std::filesystem::path& path) { + const auto handle = CreateFileW(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) return GetLastError() == ERROR_CANT_RESOLVE_FILENAME; + CloseHandle(handle); + return false; +} static int MapFlags(int sceFlags) { int f = 0; const int acc = sceFlags & SCE_KERNEL_O_ACCMODE; @@ -149,14 +175,19 @@ static int MapFlags(int sceFlags) { extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...); -static constexpr int GuestSetFlags = 4; static int SceErrorFromErrno(int error) { - constexpr int GuestEio = 5; - const int guest = error > 0 && error <= 34 ? error : GuestEio; + const int guest = error > 0 && error <= 34 ? error : FilesystemError(std::error_code(error, std::generic_category())); return static_cast(0x80020000u | static_cast(guest)); } +static int SceStatErrorFromErrno(int error) { +#ifdef ELOOP + if (error == ELOOP) return SCE_KERNEL_ERROR_ELOOP; +#endif + return SceErrorFromErrno(error); +} + static int SceIoErrorFromErrno(int error) { int guest = SceErrorFromErrno(error) & 0xffff; if (error == EAGAIN) guest = 35; @@ -173,6 +204,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; @@ -184,10 +221,18 @@ static int OpenNative(const std::filesystem::path& native, int flags, std::uint1 constexpr int GuestEmlink = 31; std::error_code error; if (std::filesystem::is_symlink(native, error)) return SceErrorFromErrno(GuestEmlink); +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { + std::string junctionTarget; + if (File::WindowsSymbolicLink::ReadJunction(native, junctionTarget, "open")) return SceErrorFromErrno(GuestEmlink); + } +#endif } int fd = NativeOpen(native, MapFlags(flags), mode); #ifdef _WIN32 if (fd < 0 && (errno != ENOENT || !native.has_filename())) { + const int openError = errno; auto directory = native; while (!directory.has_filename() && directory.has_relative_path()) directory = directory.parent_path(); std::error_code error; @@ -195,11 +240,20 @@ static int OpenNative(const std::filesystem::path& native, int flags, std::uint1 if ((flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) == (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) errno = EEXIST; else if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY || (flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_TRUNC))) errno = EISDIR; else fd = File::OpenDirectoryDescriptor(directory); + } else { + errno = openError; } } +#endif +#ifdef _WIN32 + if (fd < 0 && (errno == ENOENT || errno == ENOTDIR)) { + std::error_code metadataError; + const auto metadataPath = NativeMetadataPath(native, metadataError); + if (!metadataError && NativeSymbolicLinkLoop(metadataPath)) return SCE_KERNEL_ERROR_ELOOP; + } #endif if (fd < 0) { - return SceErrorFromErrno(errno); + return SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno); } if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY || (flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_TRUNC))) RecordWrittenPath_nid_no_patch(native); @@ -210,6 +264,7 @@ static int OpenNative(const std::filesystem::path& native, int flags, std::uint1 extern "C" { int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { + if (File::InvalidOpenFlags(flags)) return SCE_KERNEL_ERROR_EINVAL; if (path != nullptr && *path == '\0') return SCE_KERNEL_ERROR_ENOENT; APS5_LOG_OUT("path=%s flags=0x%X nativeFlags=0x%X mode=0%o", path, flags, MapFlags(flags), mode); if (path != nullptr && File::IsRandomDevicePath(path)) { @@ -220,12 +275,22 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { return fd; } std::filesystem::path native; - if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); - if ((flags & (SCE_KERNEL_O_ACCMODE | SCE_KERNEL_O_CREAT)) == SCE_KERNEL_O_RDONLY) native = File::PathOrRelinkedModule(native); - return OpenNative(native, flags, mode); + 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, true)) { + const int result = SceKernelError(error); + APS5_LOG_ERR("open failed path=%s resolveError=%d result=0x%08X", path != nullptr ? path : "", error, static_cast(result)); + return result; + } + const int result = OpenNative(native, flags, mode); + if (result < 0) { + const auto resolved = native.u8string(); + APS5_LOG_ERR("open failed path=%s resolvedPath=%s result=0x%08X", path != nullptr ? path : "", reinterpret_cast(resolved.c_str()), static_cast(result)); + } + return result; } int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std::uint16_t mode) { + if (File::InvalidOpenFlags(flags)) return SCE_KERNEL_ERROR_EINVAL; int error = 0; const char* base = GetDirectoryDescriptorPath_nid_no_patch(directory, &error); if (base == nullptr) return SceErrorFromErrno(error); @@ -233,7 +298,11 @@ 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); @@ -255,11 +324,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(DescriptorIsOpen_nid_no_patch(d) ? EFAULT : EBADF); + 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); @@ -271,18 +343,34 @@ 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(DescriptorIsOpen_nid_no_patch(d) ? EFAULT : EBADF); + const char empty = 0; + return sceKernelWrite(d, &empty, 0); } + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); auto n = NativeWrite(d, buf, nbytes); if (n < 0) return SceIoErrorFromErrno(errno); return static_cast(n); } +static bool SeekOverflows(int d, std::int64_t offset, int whence) { + const std::int64_t current = NativeLseek(d, 0, 1); + if (current < 0) return false; + std::int64_t base = current; + if (whence == 2) { + base = NativeLseek(d, 0, 2); + NativeLseek(d, current, 0); + if (base < 0) return false; + } + return base > std::numeric_limits::max() - offset; +} + std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { if (GuestSockets::IsOpen(d)) return SCE_KERNEL_ERROR_ESPIPE; if (whence < 0 || whence > 2) { throw std::invalid_argument(std::string(__func__) + ": invalid whence=" + std::to_string(whence)); } + 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); @@ -290,45 +378,76 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { } #endif std::int64_t result = NativeLseek(d, offset, whence); - if (result < 0) return SceIoErrorFromErrno(errno); + if (result < 0) { + const int error = errno; + if (error == EINVAL && offset > 0 && (whence == 1 || whence == 2) && SeekOverflows(d, offset, whence)) return SceIoErrorFromErrno(EOVERFLOW); + return SceIoErrorFromErrno(error); + } return result; } -int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { - if (path == nullptr) { - throw std::invalid_argument(std::string(__func__) + ": path is null"); - } - if (*path == '\0') return SceErrorFromErrno(2); - std::filesystem::path native; - if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); - native = File::PathOrRelinkedModule(native); +constexpr int GuestEfault = 14; + +static int StatNative(const std::filesystem::path& native, bool directory, FileStat* sb) { std::error_code error; constexpr int GuestEnotdir = 20; - constexpr int GuestEfault = 14; - const auto status = std::filesystem::status(native, error); - if (error == std::errc::not_a_directory) return SceErrorFromErrno(GuestEnotdir); - if (!std::filesystem::exists(status)) { - return SceErrorFromErrno(2); + FileStat archived{}; + if (File::FillArchivedStat(native, &archived)) { + if (directory && (archived.st_mode & 0170000) != 0040000) return SceErrorFromErrno(GuestEnotdir); + if (sb == nullptr) return SceErrorFromErrno(GuestEfault); + *sb = archived; + return 0; } - const std::string_view guestPath(path); - if (!guestPath.empty() && guestPath.back() == '/' && !std::filesystem::is_directory(status)) return SceErrorFromErrno(GuestEnotdir); + 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 (error == std::errc::filename_too_long || error == std::errc::too_many_symbolic_link_levels) return SceKernelError(FilesystemError(error)); + if (!std::filesystem::exists(status)) { +#ifdef _WIN32 + if (NativeSymbolicLinkLoop(metadataPath)) return SCE_KERNEL_ERROR_ELOOP; +#endif + return SceErrorFromErrno(NotFoundError_nid_no_patch(native)); + } + if (directory && !std::filesystem::is_directory(status)) return SceErrorFromErrno(GuestEnotdir); if (sb == nullptr) return SceErrorFromErrno(GuestEfault); - File::FillFileStat(native, sb); + if (!File::TryFillFileStat(metadataPath, sb)) return SceStatErrorFromErrno(errno); + return 0; +} + +int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { + if (path == nullptr) return SceErrorFromErrno(GuestEfault); + if (*path == '\0') return SceErrorFromErrno(2); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native, true, true)) return SceKernelError(error); + return StatNative(native, std::string_view(path).back() == '/', sb); +} + +int StatInDirectory_nid_no_patch(int directory, const char* path, FileStat* sb, bool follow) { + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(directory, &error); + if (base == nullptr) return SceErrorFromErrno(error); + const auto native = std::filesystem::path(reinterpret_cast(base)) / std::filesystem::path(reinterpret_cast(path)); + if (follow) return StatNative(native, std::string_view(path).back() == '/', sb); + if (sb == nullptr) return SceErrorFromErrno(GuestEfault); + if (!File::FillLinkStat(native, sb)) return SceErrorFromErrno(errno); return 0; } 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); + if (const int dot = RenameDotError_nid_no_patch(path)) return SceKernelError(dot); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); constexpr int GuestEperm = 1; std::error_code error; if (std::filesystem::is_directory(std::filesystem::symlink_status(native, error))) return SceErrorFromErrno(GuestEperm); if (NativeUnlink(native) != 0) { - return SceErrorFromErrno(errno); + return SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno); } RecordWrittenPath_nid_no_patch(native); return 0; @@ -354,7 +473,8 @@ int APS5_VABI sceKernelFcntl(int d, int command, ...) { } int result; if (GuestSockets::IsOpen(d)) { - if (command == GuestSetFlags) result = fcntl_nid_postfix(d, command, static_cast(argument)); + if (type == File::FcntlArgument::Int) result = fcntl_nid_postfix(d, command, static_cast(argument)); + else if (type == File::FcntlArgument::Pointer) result = fcntl_nid_postfix(d, command, reinterpret_cast(argument)); else result = fcntl_nid_postfix(d, command); } else { result = File::Fcntl(d, command, argument); diff --git a/core/libs/prx/libkernel/File/src/OpenFileLimit.cpp b/core/libs/prx/libkernel/File/src/OpenFileLimit.cpp index 6d698a585f..a5af366a2e 100644 --- a/core/libs/prx/libkernel/File/src/OpenFileLimit.cpp +++ b/core/libs/prx/libkernel/File/src/OpenFileLimit.cpp @@ -1,8 +1,11 @@ -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) #include #include #include +#ifdef __APPLE__ +#include +#endif namespace { @@ -10,12 +13,36 @@ struct OpenFileLimit { OpenFileLimit() { rlimit limit{}; if (getrlimit(RLIMIT_NOFILE, &limit) != 0) throw std::system_error(errno, std::generic_category(), "read open file limit"); - if (limit.rlim_cur >= limit.rlim_max) return; - limit.rlim_cur = limit.rlim_max; + rlim_t usable = limit.rlim_max; +#ifdef __APPLE__ + int perProcess = 0; + std::size_t size = sizeof(perProcess); + if (sysctlbyname("kern.maxfilesperproc", &perProcess, &size, nullptr, 0) != 0) throw std::system_error(errno, std::generic_category(), "read kern.maxfilesperproc"); + if (perProcess > 0 && static_cast(perProcess) < usable) usable = static_cast(perProcess); +#endif + if (limit.rlim_cur == usable && limit.rlim_max == usable) return; + limit.rlim_cur = usable; + limit.rlim_max = usable; if (setrlimit(RLIMIT_NOFILE, &limit) != 0) throw std::system_error(errno, std::generic_category(), "raise open file limit"); } } openFileLimit; } +#elif defined(_WIN32) + +#include +#include +#include + +namespace { + +struct OpenFileLimit { + OpenFileLimit() { + if (_setmaxstdio(8192) != 8192) throw std::system_error(errno, std::generic_category(), "raise open stream limit"); + } +} openFileLimit; + +} + #endif 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..a5af2341a3 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp @@ -0,0 +1,117 @@ +#include "prx/libc/include/General.hpp" + +#include +#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-----"; +constexpr char CertificateEnd[] = "-----END CERTIFICATE-----"; + +std::string CertificateBlocksOnly(const std::string& bundle) { + const std::string_view begin(CertificateBegin); + const std::string_view end(CertificateEnd); + std::string certificates; + std::size_t certificateStart = 0; + bool inCertificate = false; + for (std::size_t lineStart = 0; lineStart < bundle.size();) { + const auto newline = bundle.find('\n', lineStart); + const auto lineEnd = newline == std::string::npos ? bundle.size() : newline; + auto line = std::string_view(bundle).substr(lineStart, lineEnd - lineStart); + if (!line.empty() && line.back() == '\r') line.remove_suffix(1); + + if (line == begin) { + certificateStart = lineStart; + inCertificate = true; + } else if (line == end && inCertificate) { + const auto blockEnd = newline == std::string::npos ? lineEnd : newline + 1; + certificates.append(bundle, certificateStart, blockEnd - certificateStart); + inCertificate = false; + } + + if (newline == std::string::npos) break; + lineStart = newline + 1; + } + return certificates; +} + +std::optional ReadFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + 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 = CertificateBlocksOnly(HostCertificates()); + if (bundle.empty()) 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 58f00fafaa..af3a52401b 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -5,6 +5,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/RenameDot.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/File/include/File.hpp" @@ -16,6 +17,9 @@ #include "prx/libkernel/File/include/DescriptorControl.hpp" #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" @@ -34,12 +38,17 @@ #include #include #include +#ifdef __APPLE__ +#include +#include +#endif struct KernelIovec { void* base; 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; @@ -51,10 +60,12 @@ static constexpr std::uint64_t KERNEL_MNT_NOSUID = 0x8; #ifdef _WIN32 #include #include +#include #include #include #include #include "prx/libkernel/File/include/WindowsFileTime.hpp" +#include "prx/libc/include/WindowsDescriptor.hpp" extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); static void IgnoreDescriptorParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} template static auto WithoutParameterHandler(TCall call) { @@ -83,8 +94,7 @@ static int NativeChmod(const std::filesystem::path& path, int mode) { RecordWrittenPath_nid_no_patch(path); return ::_wchmod(path.wstring().c_str(), mode); } -static std::optional NativeDescriptorPath(int descriptor) { - if (auto directory = File::DirectoryDescriptorPath(descriptor)) return directory; +std::optional File::WindowsDescriptorPath_nid_no_patch(int descriptor) { const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); if (handle == INVALID_HANDLE_VALUE) { errno = EBADF; @@ -96,23 +106,53 @@ static std::optional NativeDescriptorPath(int descriptor) path.resize(length); length = ::GetFinalPathNameByHandleW(handle, path.data(), length, FILE_NAME_NORMALIZED); } - if (length == 0 || length >= path.size()) { + if (length == 0) { + errno = WindowsOpenError_nid_no_patch(GetLastError()); + return std::nullopt; + } + if (length >= path.size()) { errno = EINVAL; return std::nullopt; } path.resize(length); + if (path.starts_with(LR"(\\?\UNC\)")) path.replace(0, 8, LR"(\\)"); + else if (path.starts_with(LR"(\\?\)") && path.size() > 6 && path[5] == L':' && path[6] == L'\\') path.erase(0, 4); return path; } static int NativeFchmod(int descriptor, int mode) { - const auto path = NativeDescriptorPath(descriptor); + const auto path = File::WindowsDescriptorPath_nid_no_patch(descriptor); return path ? NativeChmod(*path, mode) : -1; } +static int TruncateThroughWriter(HANDLE handle, std::int64_t length) { + const auto writer = ReOpenFile(handle, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0); + if (writer == INVALID_HANDLE_VALUE) return WindowsOpenError_nid_no_patch(GetLastError()); + FILE_END_OF_FILE_INFO end{}; + end.EndOfFile.QuadPart = length; + const bool truncated = SetFileInformationByHandle(writer, FileEndOfFileInfo, &end, sizeof(end)); + const DWORD error = GetLastError(); + CloseHandle(writer); + return truncated ? 0 : WindowsOpenError_nid_no_patch(error); +} static int NativeFtruncate(int descriptor, std::int64_t length) { const int result = File::TruncateSharedMemory_nid_no_patch(descriptor, length); if (result != -2) return result; + if (File::IsDirectoryDescriptor(descriptor)) return EINVAL; + const auto positioned = File::LockPositionedTransfer(descriptor); 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; + if (WindowsHandleOnlyAppends_nid_no_patch(WindowsHandleAccess_nid_no_patch(handle, descriptor, "ftruncate"))) return TruncateThroughWriter(handle, length); + FILE_END_OF_FILE_INFO end{}; + end.EndOfFile.QuadPart = length; + if (::SetFileInformationByHandle(handle, FileEndOfFileInfo, &end, sizeof(end))) return 0; + switch (::GetLastError()) { + case ERROR_ACCESS_DENIED: return EACCES; + case ERROR_DISK_FULL: + case ERROR_HANDLE_DISK_FULL: return ENOSPC; + case ERROR_FILE_TOO_LARGE: + case ERROR_INVALID_PARAMETER: return EFBIG; + default: return EINVAL; + } } static int NativeTruncate(const std::filesystem::path& path, std::int64_t length) { const int descriptor = ::_wopen(path.c_str(), _O_WRONLY | _O_BINARY); @@ -127,6 +167,8 @@ static int SetTimes(HANDLE handle, const KernelTimeval* times) { GetSystemTimePreciseAsFileTime(&access); modified = access; } else if (!File::WindowsFileTime::Encode(times[0], access) || !File::WindowsFileTime::Encode(times[1], modified)) { + const FILETIME unchanged{0xFFFFFFFF, 0xFFFFFFFF}; + SetFileTime(handle, nullptr, &unchanged, &unchanged); errno = EINVAL; return -1; } @@ -151,7 +193,7 @@ static int NativeFutimes(int descriptor, const KernelTimeval* times) { const int result = SetTimes(handle, times); CloseHandle(handle); if (result == 0) { - if (const auto path = NativeDescriptorPath(descriptor)) RecordWrittenPath_nid_no_patch(*path); + if (const auto path = File::WindowsDescriptorPath_nid_no_patch(descriptor)) RecordWrittenPath_nid_no_patch(*path); } return result; } @@ -162,6 +204,8 @@ static int NativeSync(int descriptor) { handle = CreateFileW(directory->c_str(), GENERIC_WRITE, share, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); } else { const int previous = errno; + const auto pipe = reinterpret_cast(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); })); + if (pipe != INVALID_HANDLE_VALUE && GetFileType(pipe) == FILE_TYPE_PIPE) { errno = EINVAL; return -1; } if (::_commit(descriptor) == 0) return 0; const auto original = reinterpret_cast(::_get_osfhandle(descriptor)); if (original == INVALID_HANDLE_VALUE || GetFileType(original) != FILE_TYPE_DISK) { errno = EBADF; return -1; } @@ -197,7 +241,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"); } @@ -210,10 +254,13 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ errno = EBADF; return -1; } - const auto positioned = File::LockPositionedTransfer(descriptor); + if (File::IsDirectoryDescriptor(descriptor)) { + errno = write ? EBADF : EISDIR; + return -1; + } LARGE_INTEGER position{}; if (::GetFileType(handle) != FILE_TYPE_DISK || !::SetFilePointerEx(handle, LARGE_INTEGER{}, &position, FILE_CURRENT)) { - errno = !File::DirectoryDescriptorPath(descriptor) ? ESPIPE : write ? EBADF : EISDIR; + errno = ESPIPE; return -1; } OVERLAPPED overlapped{}; @@ -233,17 +280,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; @@ -353,6 +432,28 @@ static bool FailPosixPositionedGuestSocket(int descriptor) { return true; } +static bool IsPipeDescriptor(int descriptor) { +#ifdef _WIN32 + const auto handle = reinterpret_cast(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); })); + return handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) == FILE_TYPE_PIPE; +#else + struct stat status; + return ::fstat(descriptor, &status) == 0 && S_ISFIFO(status.st_mode); +#endif +} + +static int PositionedArgumentError(int descriptor, bool nullBuffer, std::int64_t offset) { + if (!nullBuffer && offset >= 0) return 0; + if (!GuestSockets::IsOpen(descriptor) && !DescriptorIsOpen_nid_no_patch(descriptor)) return GUEST_EBADF; + if (GuestSockets::IsOpen(descriptor) || IsPipeDescriptor(descriptor)) return GUEST_ESPIPE; + return offset < 0 ? GUEST_EINVAL : GUEST_EFAULT; +} + +static int TransferArgumentError(int descriptor, bool nullBuffer) { + if (!nullBuffer) return 0; + return DescriptorIsOpen_nid_no_patch(descriptor) ? GUEST_EFAULT : GUEST_EBADF; +} + static int SceErrorFromErrno(int error) { int guest = error > 0 && error <= 34 ? error : error == GUEST_ENOTEMPTY ? error : GUEST_EIO; if (error == EAGAIN) guest = GUEST_EWOULDBLOCK; @@ -373,6 +474,11 @@ static int SceErrorFromErrno(int error) { return static_cast(0x80020000u | static_cast(guest)); } +static int PathLookupFailure(const std::error_code& error) { + const bool lookup = error == std::errc::filename_too_long || error == std::errc::too_many_symbolic_link_levels; + return lookup ? SceErrorFromErrno(error.default_error_condition().value()) : 0; +} + extern "C" int* APS5_VABI __error_nid_postfix(); static int PosixFailure(int error) { @@ -380,6 +486,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; } @@ -388,17 +498,26 @@ static std::int64_t PosixResult(std::int64_t result) { return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; } +static int DeviceFilesystem(KernelStatfs* fs) { + fs->f_bsize = 512; + fs->f_iosize = 512; + fs->f_blocks = 2; + fs->f_namemax = 255; + return 0; +} + #ifdef _WIN32 static int NativeFstatfs(int descriptor, KernelStatfs* fs) { - if (!File::DirectoryDescriptorPath(descriptor)) { + if (!File::IsDirectoryDescriptor(descriptor)) { const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); if (handle == INVALID_HANDLE_VALUE) return GUEST_EBADF; const DWORD type = ::GetFileType(handle); if (type == FILE_TYPE_PIPE) return GUEST_EINVAL; + if (type == FILE_TYPE_CHAR) return DeviceFilesystem(fs); if (type != FILE_TYPE_DISK) throw std::runtime_error("_fstatfs: descriptor " + std::to_string(descriptor) + " is not a file on a Windows volume"); } - const auto path = NativeDescriptorPath(descriptor); + const auto path = File::WindowsDescriptorPath_nid_no_patch(descriptor); if (!path) throw std::runtime_error("_fstatfs: no path for descriptor " + std::to_string(descriptor) + ", errno=" + std::to_string(errno)); std::wstring root(path->native().size() + 2, L'\0'); if (!::GetVolumePathNameW(path->c_str(), root.data(), static_cast(root.size()))) @@ -421,18 +540,28 @@ static int NativeFstatfs(int descriptor, KernelStatfs* fs) { fs->f_namemax = nameMax; return 0; } + +static bool NativeIsPipe(int descriptor) { + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + return handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) == FILE_TYPE_PIPE; +} #else #include #include -static int NativeFstatfs(int descriptor, KernelStatfs* fs) { +static bool NativeIsPipe(int descriptor) { constexpr long PipeFilesystem = 0x50495045; + struct stat file{}; + struct statfs kind{}; + return ::fstat(descriptor, &file) == 0 && S_ISFIFO(file.st_mode) && ::fstatfs(descriptor, &kind) == 0 + && static_cast(kind.f_type) == PipeFilesystem; +} + +static int NativeFstatfs(int descriptor, KernelStatfs* fs) { struct stat file{}; if (::fstat(descriptor, &file) != 0) return SceErrorFromErrno(errno) & 0xffff; if (S_ISSOCK(file.st_mode) && (::fcntl(descriptor, F_GETFL) & O_PATH) == 0) return GUEST_EINVAL; - if (S_ISFIFO(file.st_mode)) { - struct statfs kind{}; - if (::fstatfs(descriptor, &kind) == 0 && static_cast(kind.f_type) == PipeFilesystem) return GUEST_EINVAL; - } + if (NativeIsPipe(descriptor)) return GUEST_EINVAL; + if (S_ISCHR(file.st_mode)) return DeviceFilesystem(fs); struct statvfs host{}; if (::fstatvfs(descriptor, &host) != 0) return SceErrorFromErrno(errno) & 0xffff; if (host.f_flag & ST_RDONLY) fs->f_flags |= KERNEL_MNT_RDONLY; @@ -456,7 +585,7 @@ extern "C" int APS5_VABI pipe_nid_postfix(int* descriptors) { if (!destination.Open()) return PosixFailure(GUEST_EFAULT); int native[2]; #ifdef _WIN32 - const int result = ::_pipe(native, 4096, _O_BINARY); + const int result = ::_pipe(native, 65536, _O_BINARY); #else const int result = ::pipe(native); #endif @@ -542,29 +671,57 @@ static std::string FileFlagNames(std::uint64_t flags) { return names; } +static int SetFileFlags(const char* function, const std::string& target, std::uint64_t flags) { + std::uint64_t known = 0; + for (const auto& entry : GUEST_FILE_FLAGS) known |= entry.first; + if ((flags & ~known) != 0) return GUEST_EOPNOTSUPP; + if ((flags & GUEST_SF_SNAPSHOT) != 0) return GUEST_EPERM; + if (flags != 0) + throw std::runtime_error(std::string(function) + ": setting " + FileFlagNames(flags) + " on " + target + " is not supported, the host keeps no BSD file flags"); + return 0; +} + extern "C" { int APS5_VABI chmod_nid_postfix(const char* path, int mode) { if (const int error = PathError(path)) return PosixFailure(error); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); - if (NativeChmod(native, mode & 07777) != 0) return PosixResult(SceErrorFromErrno(errno)); + if (NativeChmod(native, mode & 07777) != 0) return PosixResult(SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno)); return 0; } int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { if (const int error = PathError(path)) return PosixFailure(error); - if (const int error = LookupError(ResolvePath_nid_no_patch(path))) return PosixFailure(error); - std::uint64_t known = 0; - for (const auto& entry : GUEST_FILE_FLAGS) known |= entry.first; - if ((flags & ~known) != 0) return PosixFailure(GUEST_EOPNOTSUPP); - if ((flags & GUEST_SF_SNAPSHOT) != 0) return PosixFailure(GUEST_EPERM); - if (flags != 0) - throw std::runtime_error("chflags: setting " + FileFlagNames(flags) + " on " + path + " is not supported, the host keeps no BSD file flags"); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); + if (const int error = LookupError(native)) return PosixFailure(error); + if (const int error = SetFileFlags("chflags", path, flags)) return PosixFailure(error); return 0; } +int APS5_VABI fchflags_nid_postfix(int d, std::uint64_t flags) { + if (GuestSockets::IsOpen(d) || File::IsSharedMemoryDescriptor_nid_no_patch(d)) return PosixFailure(GUEST_EINVAL); + File::RefuseArchivedFile(d, "fchflags"); + if (!File::IsVnode(d)) { + if (!DescriptorIsOpen_nid_no_patch(d)) return PosixFailure(GUEST_EBADF); + if (NativeIsPipe(d)) return PosixFailure(GUEST_EINVAL); + throw std::runtime_error("fchflags: descriptor " + std::to_string(d) + " is neither a file, a directory, a pipe, a socket nor shared memory"); + } + if (const int error = SetFileFlags("fchflags", "descriptor " + std::to_string(d), flags)) return PosixFailure(error); + return 0; +} + +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; if (KernelCloseSocket_nid_no_patch(d, &socketError)) { if (socketError == 0) return 0; @@ -572,6 +729,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); @@ -593,22 +753,34 @@ int APS5_VABI _close_nid_postfix(int descriptor) { namespace { std::atomic g_hostStandardError{-1}; +std::atomic g_hostStandardOutput{-1}; -void PreserveHostStandardError() { - if (g_hostStandardError.load() >= 0) return; +void PreserveHostStream(int standard, std::atomic& slot) { + if (slot.load() >= 0) return; #ifdef _WIN32 - const int saved = WithoutParameterHandler([] { return ::_dup(2); }); + const int saved = WithoutParameterHandler([standard] { return ::_dup(standard); }); #else - const int saved = KeepHostStandardError_nid_no_patch(); + const int saved = standard == 1 ? KeepHostStandardOutput_nid_no_patch() : KeepHostStandardError_nid_no_patch(); #endif - if (saved < 0) throw std::runtime_error("dup2: cannot keep the host standard error"); + if (saved < 0) throw std::runtime_error(standard == 1 ? "dup2: cannot keep the host standard output" : "dup2: cannot keep the host standard error"); int expected = -1; - if (!g_hostStandardError.compare_exchange_strong(expected, saved)) { + if (!slot.compare_exchange_strong(expected, saved)) { #ifdef _WIN32 _close(saved); #endif + return; } +#ifdef _WIN32 + if (!AdoptHostStandardStream_nid_no_patch(standard, saved)) { + slot.store(-1); + _close(saved); + throw std::runtime_error(standard == 1 ? "dup2: cannot keep the host standard output" : "dup2: cannot keep the host standard error"); + } +#endif } + +void PreserveHostStandardError() { PreserveHostStream(2, g_hostStandardError); } +void PreserveHostStandardOutput() { PreserveHostStream(1, g_hostStandardOutput); } } int File::HostStandardError() { @@ -642,6 +814,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); @@ -653,6 +826,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { #ifdef _WIN32 if (WithoutParameterHandler([from] { return ::_get_osfhandle(from) == -1 ? -1 : 0; }) != 0) return PosixFailure(GUEST_EBADF); if (from == to) return to; + if (to == 1) PreserveHostStandardOutput(); if (to == 2) PreserveHostStandardError(); auto* const standard = HostStandardHandle_nid_no_patch(to); const int duplicated = WithoutParameterHandler([from, to] { return ::_dup2(from, to); }); @@ -673,6 +847,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { if (::fcntl(from, F_GETFL) < 0) return PosixFailure(GUEST_EBADF); return to; } + if (to == 1) PreserveHostStandardOutput(); if (to == 2) PreserveHostStandardError(); const int result = ::dup2(from, to); if (result < 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); @@ -688,8 +863,21 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { int APS5_VABI flock_nid_postfix(int d, int operation) { if (GuestSockets::IsOpen(d)) return PosixFailure(GUEST_EOPNOTSUPP); + File::RefuseArchivedFile(d, "flock"); +#ifdef _WIN32 + const auto handle = reinterpret_cast(WithoutParameterHandler([d] { return ::_get_osfhandle(d); })); + if (handle == INVALID_HANDLE_VALUE) return PosixFailure(GUEST_EBADF); + if (::GetFileType(handle) == FILE_TYPE_PIPE) return PosixFailure(GUEST_EOPNOTSUPP); +#else + struct stat status; + if (::fstat(d, &status) != 0) return PosixFailure(GUEST_EBADF); + if (S_ISFIFO(status.st_mode)) return PosixFailure(GUEST_EOPNOTSUPP); +#endif const int type = operation & 8 ? 8 : operation & 2 ? 2 : operation & 1 ? 1 : 0; if (type == 0) return PosixFailure(GUEST_EBADF); +#ifdef _WIN32 + if (File::IsDirectoryDescriptor(d)) return 0; +#endif if (NativeFlock(d, type | (operation & 4)) != 0) { #ifdef _WIN32 const auto error = ::GetLastError(); @@ -705,8 +893,9 @@ 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 (GuestSockets::IsOpen(d) || IsKqueueDescriptor(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); @@ -719,7 +908,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); @@ -729,10 +920,15 @@ int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int d, FileStat* sb) { return 0; } +int64_t APS5_VABI _fstat_nid_postfix(int d, FileStat* sb) { + return fstat_nid_disambig1_nid_postfix(d, sb); +} + 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) { @@ -752,6 +948,7 @@ int APS5_VABI sceKernelFtruncate(int d, int64_t length) { } int64_t APS5_VABI lseek_nid_postfix(int d, int64_t offset, int whence) { + if (!GuestSockets::IsOpen(d) && !DescriptorIsOpen_nid_no_patch(d)) return PosixFailure(GUEST_EBADF); if (whence < 0 || whence > 2) return PosixFailure(GUEST_EINVAL); return PosixResult(sceKernelLseek(d, offset, whence)); } @@ -770,6 +967,7 @@ int APS5_VABI mkdir_nid_postfix(const char* path, uint16_t mode) { } int APS5_VABI open_nid_postfix(const char* path, int flags, int mode) { + if (File::InvalidOpenFlags(flags)) return PosixFailure(GUEST_EINVAL); if (const int error = PathError(path)) return PosixFailure(error); return PosixResult(sceKernelOpen(path, flags, static_cast(mode))); } @@ -797,53 +995,56 @@ int APS5_VABI _open_nid_postfix(const char* path, int flags, ...) { } int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t offset) { - if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); - if (offset < 0) return PosixFailure(GUEST_EINVAL); + if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); + if (const int error = PositionedArgumentError(d, buf == nullptr && nbytes != 0, offset)) return PosixFailure(error); 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 (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); - if (offset < 0) return PosixFailure(GUEST_EINVAL); + if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); + if (const int error = PositionedArgumentError(d, buf == nullptr && nbytes != 0, offset)) return PosixFailure(error); if (FailPosixPositionedGuestSocket(d)) return -1; + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); char empty = 0; const auto result = NativePwrite(d, buf == nullptr ? &empty : buf, nbytes, offset); return result < 0 ? PosixFailure(SceErrorFromErrno(errno) & 0xffff) : result; } 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); + if (const int error = TransferArgumentError(d, buf == nullptr && nbytes != 0)) return PosixFailure(error); char emptyBuffer = 0; return PosixResult(sceKernelRead(d, buf == nullptr ? &emptyBuffer : buf, static_cast(nbytes))); } std::int64_t APS5_VABI _read_nid_postfix(int descriptor, void* buffer, std::size_t count) { + if (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); + if (const int error = TransferArgumentError(descriptor, buffer == nullptr && count != 0)) return PosixFailure(error); char emptyBuffer = 0; return PosixResult(sceKernelRead(descriptor, buffer == nullptr ? &emptyBuffer : buffer, count)); } 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); + if (const int error = TransferArgumentError(d, str == nullptr && size != 0)) return PosixFailure(error); const char emptyBuffer = 0; return PosixResult(sceKernelWrite(d, str == nullptr ? &emptyBuffer : str, static_cast(size))); } std::int64_t APS5_VABI _write_nid_postfix(int descriptor, const void* buffer, std::size_t count) { + if (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); + if (const int error = TransferArgumentError(descriptor, buffer == nullptr && count != 0)) return PosixFailure(error); const char emptyBuffer = 0; return PosixResult(sceKernelWrite(descriptor, buffer == nullptr ? &emptyBuffer : buffer, count)); } @@ -859,7 +1060,7 @@ int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb) { if (const int error = PathError(path)) return PosixFailure(error); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); - if (!File::FillLinkStat(native, sb)) return PosixResult(SceErrorFromErrno(errno)); + if (!File::FillLinkStat(native, sb)) return PosixResult(SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno)); return 0; } @@ -873,14 +1074,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)) return SceKernelError(error); - native = File::PathOrRelinkedModule(native); + 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); @@ -891,6 +1096,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 @@ -901,6 +1107,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 @@ -910,7 +1117,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; @@ -922,10 +1129,17 @@ int APS5_VABI sceKernelWriteThrottlingStatus(std::uint64_t* status) { int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep) { if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); + File::RefuseArchivedFile(fd, "getdirentries"); if (buf == nullptr) return SceErrorFromErrno(GUEST_EFAULT); if (nbytes <= 0) return SceErrorFromErrno(GUEST_EINVAL); - if (basep != nullptr) *basep = 0; - return File::ReadDirectoryDescriptor(fd, buf, nbytes); + if (!File::DirectoryDescriptorPath(fd)) { + if (WithoutParameterHandler([fd] { return ::_get_osfhandle(fd); }) == -1) return SceErrorFromErrno(GUEST_EBADF); + return SceErrorFromErrno(GUEST_EINVAL); + } + const GuestArena::HostWrite destination(buf, static_cast(nbytes)); + const GuestArena::HostWrite baseDestination(basep, basep == nullptr ? 0 : sizeof(*basep)); + if (!destination.Open() || !baseDestination.Open()) return SceErrorFromErrno(GUEST_EFAULT); + return File::ReadDirectoryDescriptor(fd, buf, nbytes, basep); } int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { @@ -934,12 +1148,48 @@ int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { #else +static bool InsideApp0(int fd) { + const int savedErrno = errno; + const bool inside = [fd] { + const auto directory = File::DirectoryDescriptorPath(fd); + if (!directory) return false; + std::error_code error; + const auto app0 = std::filesystem::weakly_canonical(ResolveDiskPath_nid_no_patch("/app0"), error); + if (error || app0.empty()) return false; + const auto relative = directory->lexically_relative(app0); + return !relative.empty() && *relative.begin() != ".."; + }(); + errno = savedErrno; + return inside; +} + +static unsigned char GuestEntryType(int fd, const struct dirent64& entry, bool followLinks) { + if (entry.d_type != DT_LNK && entry.d_type != DT_UNKNOWN) return entry.d_type; + if (entry.d_type == DT_LNK && !followLinks) return DT_LNK; + const int savedErrno = errno; + struct stat target{}; + const bool found = ::fstatat(fd, entry.d_name, &target, followLinks ? 0 : AT_SYMLINK_NOFOLLOW) == 0; + errno = savedErrno; + if (!found) return entry.d_type; + if (S_ISDIR(target.st_mode)) return DT_DIR; + if (S_ISREG(target.st_mode)) return DT_REG; + if (S_ISLNK(target.st_mode)) return DT_LNK; + return entry.d_type; +} + 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 GuestArena::HostWrite destination(buf, static_cast(nbytes)); + const GuestArena::HostWrite baseDestination(basep, basep == nullptr ? 0 : sizeof(*basep)); + if (!destination.Open() || !baseDestination.Open()) return SceErrorFromErrno(GUEST_EFAULT); const off_t base = ::lseek(fd, 0, SEEK_CUR); if (base < 0) return SceErrorFromErrno(errno); if (basep != nullptr) *basep = static_cast(base); @@ -948,6 +1198,7 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas if (read < 0) return SceErrorFromErrno(errno); std::size_t written = 0; off_t resume = base; + const bool followLinks = InsideApp0(fd); for (long offset = 0; offset < read;) { const auto* entry = reinterpret_cast(host.data() + offset); const auto nameLength = std::strlen(entry->d_name); @@ -964,7 +1215,7 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas const auto nameBytes = static_cast(nameLength); std::memcpy(out, &fileNumber, sizeof(fileNumber)); std::memcpy(out + 4, &recordLength, sizeof(recordLength)); - out[6] = static_cast(entry->d_type); + out[6] = static_cast(GuestEntryType(fd, *entry, followLinks)); out[7] = static_cast(nameBytes); std::memcpy(out + GuestHeaderBytes, entry->d_name, nameLength); written += record; @@ -991,13 +1242,14 @@ 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); while (!native.has_filename() && native.has_relative_path()) native = native.parent_path(); std::error_code error; if (std::filesystem::exists(native, error)) return SceErrorFromErrno(GUEST_EEXIST); + if (const int failure = PathLookupFailure(error)) return failure; if (!std::filesystem::is_directory(WithoutTrailingSeparator(native).parent_path(), error)) { return SceErrorFromErrno(HasNonDirectoryPrefix(native) ? GUEST_ENOTDIR : GUEST_ENOENT); } @@ -1006,9 +1258,30 @@ int APS5_VABI sceKernelMkdir(const char* path, uint16_t mode) { return 0; } -static int CheckIovecs(const KernelIovec* iov, int iovcnt) { +static int CopyIovecs(const KernelIovec* iov, int iovcnt, std::vector& vectors) { if (iovcnt < 0 || iovcnt > KERNEL_IOV_MAX) return SceErrorFromErrno(GUEST_EINVAL); - if (iov == nullptr && iovcnt != 0) return SceErrorFromErrno(GUEST_EFAULT); + if (iovcnt == 0) return 0; + const auto bytes = static_cast(iovcnt) * sizeof(KernelIovec); + const auto address = reinterpret_cast(iov); + if (address == 0 || bytes > UINTPTR_MAX - address) return SceErrorFromErrno(GUEST_EFAULT); + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(iov, bytes); + vectors.resize(static_cast(iovcnt)); +#ifdef _WIN32 + SIZE_T copied = 0; + if (!ReadProcessMemory(GetCurrentProcess(), iov, vectors.data(), bytes, &copied) || copied != bytes) return SceErrorFromErrno(GUEST_EFAULT); +#elif defined(__linux__) + iovec local{vectors.data(), bytes}; + iovec remote{const_cast(iov), bytes}; + const auto copied = ::process_vm_readv(::getpid(), &local, 1, &remote, 1, 0); + if (copied < 0 && errno != EFAULT) throw std::runtime_error("iovec: process_vm_readv failed with errno " + std::to_string(errno)); + if (copied < 0 || static_cast(copied) != bytes) return SceErrorFromErrno(GUEST_EFAULT); +#elif defined(__APPLE__) + mach_vm_size_t copied = 0; + if (::mach_vm_read_overwrite(::mach_task_self(), reinterpret_cast(iov), bytes, + reinterpret_cast(vectors.data()), &copied) != KERN_SUCCESS || copied != bytes) return SceErrorFromErrno(GUEST_EFAULT); +#else + throw std::runtime_error("iovec: checked guest copies are not implemented on this host"); +#endif return 0; } @@ -1028,31 +1301,37 @@ static bool OpenIovecs(const KernelIovec* iov, int iovcnt, std::deque(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; } int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t offset) { - if (buf == nullptr && nbytes != 0) return SceErrorFromErrno(GUEST_EFAULT); - if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + if (const int error = PositionedArgumentError(d, buf == nullptr && nbytes != 0, offset)) return SceErrorFromErrno(error); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); char empty = 0; const auto result = NativePwrite(d, nbytes == 0 && buf == nullptr ? &empty : buf, nbytes, offset); return result < 0 ? SceErrorFromErrno(errno) : result; } -static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, const std::int64_t* offset, bool write) { - if (const int error = CheckIovecs(iov, iovcnt)) return error; +static std::int64_t TransferIovecs(int d, const KernelIovec* guestIov, int iovcnt, const std::int64_t* offset, bool write) { + std::vector vectors; + if (const int error = CopyIovecs(guestIov, iovcnt, vectors)) return error; + const auto* iov = vectors.data(); if (offset != nullptr && *offset < 0) return SceErrorFromErrno(GUEST_EINVAL); std::size_t total = 0; for (int i = 0; i < iovcnt; ++i) { @@ -1071,20 +1350,35 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, co : GuestSockets::Readv(d, reinterpret_cast(iov), iovcnt); return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } + if (write) PrepareWriteIovecs(iov, iovcnt); 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 && File::IsDirectoryDescriptor(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); @@ -1113,26 +1407,28 @@ int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* iov, int iovcnt, in #else int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset) { - if (buf == nullptr && nbytes != 0) return SceErrorFromErrno(GUEST_EFAULT); - if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + if (const int error = PositionedArgumentError(d, buf == nullptr && nbytes != 0, offset)) return SceErrorFromErrno(error); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; 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; } int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t offset) { - if (buf == nullptr && nbytes != 0) return SceErrorFromErrno(GUEST_EFAULT); - if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + if (const int error = PositionedArgumentError(d, buf == nullptr && nbytes != 0, offset)) return SceErrorFromErrno(error); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes); const auto result = NativePwrite(d, buf, nbytes, offset); return result < 0 ? SceErrorFromErrno(errno) : result; } -int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* iov, int iovcnt) { - if (const int error = CheckIovecs(iov, iovcnt)) return error; +int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* guestIov, int iovcnt) { + std::vector vectors; + if (const int error = CopyIovecs(guestIov, iovcnt, vectors)) return error; + const auto* iov = vectors.data(); std::deque destinations; if (!OpenIovecs(iov, iovcnt, destinations)) return SceErrorFromErrno(GUEST_EFAULT); if (GuestSockets::IsOpen(d)) { @@ -1140,37 +1436,47 @@ 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; } -int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* iov, int iovcnt) { - if (const int error = CheckIovecs(iov, iovcnt)) return error; +int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* guestIov, int iovcnt) { + std::vector vectors; + if (const int error = CopyIovecs(guestIov, iovcnt, vectors)) return error; + const auto* iov = vectors.data(); if (GuestSockets::IsOpen(d)) { const auto result = GuestSockets::Writev(d, reinterpret_cast(iov), iovcnt); return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } + PrepareWriteIovecs(iov, iovcnt); const auto sharedResult = File::WritevSharedMemory_nid_no_patch(d, NativeIovecs(iov), iovcnt); if (sharedResult != -2) return sharedResult < 0 ? SceErrorFromErrno(errno) : sharedResult; const auto result = static_cast(::writev(d, NativeIovecs(iov), iovcnt)); return result < 0 ? SceErrorFromErrno(errno) : result; } -int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { - if (const int error = CheckIovecs(iov, iovcnt)) return error; +int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* guestIov, int iovcnt, int64_t offset) { + std::vector vectors; + if (const int error = CopyIovecs(guestIov, iovcnt, vectors)) return error; + const auto* iov = vectors.data(); if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; std::deque destinations; 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; } -int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { - if (const int error = CheckIovecs(iov, iovcnt)) return error; +int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* guestIov, int iovcnt, int64_t offset) { + std::vector vectors; + if (const int error = CopyIovecs(guestIov, iovcnt, vectors)) return error; + const auto* iov = vectors.data(); if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; + PrepareWriteIovecs(iov, iovcnt); const auto sharedResult = File::PwritevSharedMemory_nid_no_patch(d, NativeIovecs(iov), iovcnt, offset); if (sharedResult != -2) return sharedResult < 0 ? SceErrorFromErrno(errno) : sharedResult; const auto result = static_cast(::pwritev(d, NativeIovecs(iov), iovcnt, static_cast(offset))); @@ -1190,39 +1496,26 @@ 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 == '\0' || *to == '\0') return SceErrorFromErrno(GUEST_ENOENT); - std::filesystem::path source; - if (const int error = ResolvePathError_nid_no_patch(from, source)) return SceKernelError(error); - std::error_code error; - const auto sourceStatus = std::filesystem::status(source, error); - if (!std::filesystem::exists(sourceStatus)) return SceErrorFromErrno(GUEST_ENOENT); - std::filesystem::path destination; - if (const int result = ResolvePathError_nid_no_patch(to, destination)) return SceKernelError(result); - bool replacesFile = false; -#ifdef _WIN32 - const auto destinationStatus = std::filesystem::symlink_status(destination, error); - replacesFile = !std::filesystem::is_directory(sourceStatus) && std::filesystem::exists(destinationStatus) && !std::filesystem::is_directory(destinationStatus); -#endif - error = RenameNative_nid_no_patch(source, destination, replacesFile); - if (error) return SceErrorFromErrno(GUEST_EIO); - RecordWrittenPath_nid_no_patch(source); - RecordWrittenPath_nid_no_patch(destination); - return 0; + if (!from) return SceErrorFromErrno(GUEST_EFAULT); + const int result = RenameGuestPath_nid_no_patch(from, to); + return result == 0 ? 0 : SceKernelError(errno); + if (const int dot = RenameDotError_nid_no_patch(from)) return SceKernelError(dot); + if (const int dot = RenameDotError_nid_no_patch(to)) return SceKernelError(dot); } int APS5_VABI sceKernelRmdir(const char* path) { - if (path == nullptr) throw std::invalid_argument("sceKernelRmdir: path is null"); + if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); + if (const int dot = RenameDotError_nid_no_patch(path)) return SceKernelError(dot); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; if (std::filesystem::is_symlink(native, error)) return SceErrorFromErrno(GUEST_ENOTDIR); + if (const int failure = PathLookupFailure(error)) return failure; if (!std::filesystem::is_directory(native, error)) { const bool blockedByFile = HasNonDirectoryPrefix(native) || std::filesystem::exists(WithoutTrailingSeparator(native), error); return SceErrorFromErrno(blockedByFile ? GUEST_ENOTDIR : GUEST_ENOENT); } - if (FinalPathComponentIsDot_nid_no_patch(path)) return SceErrorFromErrno(GUEST_EINVAL); if (!std::filesystem::is_empty(native, error)) return SceErrorFromErrno(GUEST_ENOTEMPTY); if (!RemoveEmptyDirectory(native)) return SceErrorFromErrno(GUEST_EIO); RecordWrittenPath_nid_no_patch(native); @@ -1249,6 +1542,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; } @@ -1257,15 +1551,20 @@ int APS5_VABI fchmod_nid_postfix(int d, int mode) { return PosixResult(sceKernelFchmod(d, static_cast(mode))); } +static bool InvalidTimes(const KernelTimeval* times) { + return times != nullptr && (times[0].tv_usec < 0 || times[0].tv_usec >= 1000000 || times[1].tv_usec < 0 || times[1].tv_usec >= 1000000); +} + int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t length) { - if (path == nullptr) throw std::invalid_argument("sceKernelTruncate: path is null"); - if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); if (length < 0) return SceErrorFromErrno(GUEST_EINVAL); + 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); std::error_code error; const auto status = std::filesystem::status(native, error); - if (!std::filesystem::exists(status)) return SceErrorFromErrno(GUEST_ENOENT); + if (const int failure = PathLookupFailure(error)) return failure; + if (!std::filesystem::exists(status)) return SceErrorFromErrno(NotFoundError_nid_no_patch(native)); if (std::filesystem::is_directory(status)) return SceErrorFromErrno(GUEST_EISDIR); if (const int failure = NativeTruncate(native, length)) return SceErrorFromErrno(failure); RecordWrittenPath_nid_no_patch(native); @@ -1273,36 +1572,30 @@ int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t lengt } int APS5_VABI truncate_nid_postfix(const char* path, std::int64_t length) { + if (length < 0) return PosixFailure(GUEST_EINVAL); if (const int error = PathError(path)) return PosixFailure(error); return PosixResult(sceKernelTruncate_nid_postfix(path, length)); } int APS5_VABI sceKernelUtimes_nid_postfix(const char* path, const KernelTimeval* times) { - if (path == nullptr) throw std::invalid_argument("sceKernelUtimes: path is null"); + if (InvalidTimes(times)) return SCE_KERNEL_ERROR_EINVAL; + if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); - if (times != nullptr) { - for (int i = 0; i < 2; ++i) { - if (times[i].tv_usec < 0 || times[i].tv_usec >= 1000000) return SCE_KERNEL_ERROR_EINVAL; - } - } std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); - if (NativeUtimes(native, times) != 0) return SceErrorFromErrno(errno); + if (NativeUtimes(native, times) != 0) return SceErrorFromErrno(errno == ENOENT ? NotFoundError_nid_no_patch(native) : errno); return 0; } int APS5_VABI utimes_nid_postfix(const char* path, const KernelTimeval* times) { + if (InvalidTimes(times)) return PosixFailure(GUEST_EINVAL); if (const int error = PathError(path)) return PosixFailure(error); return PosixResult(sceKernelUtimes_nid_postfix(path, times)); } int APS5_VABI lutimes_nid_postfix(const char* path, const KernelTimeval* times) { + if (InvalidTimes(times)) return PosixFailure(GUEST_EINVAL); if (const int error = PathError(path)) return PosixFailure(error); - if (times != nullptr) { - for (int i = 0; i < 2; ++i) { - if (times[i].tv_usec < 0 || times[i].tv_usec >= 1000000) return PosixFailure(GUEST_EINVAL); - } - } std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (NativeUtimes(native, times, false) != 0) return PosixResult(SceErrorFromErrno(errno)); @@ -1311,6 +1604,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); @@ -1342,4 +1636,12 @@ int64_t APS5_VABI writev_nid_postfix(int d, const KernelIovec* iov, int iovcnt) return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; } +int64_t APS5_VABI _readv_nid_postfix(int d, const KernelIovec* iov, int iovcnt) { + return readv_nid_postfix(d, iov, iovcnt); +} + +int64_t APS5_VABI _writev_nid_postfix(int d, const KernelIovec* iov, int iovcnt) { + return writev_nid_postfix(d, iov, iovcnt); +} + } diff --git a/core/libs/prx/libkernel/File/src/Terminal.cpp b/core/libs/prx/libkernel/File/src/Terminal.cpp index 648c63eb7c..25f8a85526 100644 --- a/core/libs/prx/libkernel/File/src/Terminal.cpp +++ b/core/libs/prx/libkernel/File/src/Terminal.cpp @@ -1,6 +1,8 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include #include +#include #include #ifdef _WIN32 #include @@ -17,6 +19,15 @@ extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handle #endif namespace { +struct GuestTermios { + std::uint32_t flags[4]; + std::uint8_t controlCharacters[20]; + std::uint32_t inputSpeed; + std::uint32_t outputSpeed; +}; +static_assert(sizeof(GuestTermios) == 44 && alignof(GuestTermios) == 4); +static_assert(offsetof(GuestTermios, inputSpeed) == 36 && offsetof(GuestTermios, outputSpeed) == 40); + constexpr int BadDescriptor = 9; constexpr int BadAddress = 14; constexpr int InvalidArgument = 22; @@ -73,6 +84,14 @@ int ProcessGroupError(int descriptor, const char* pipeMessage) { } extern "C" { +int APS5_VABI cfsetspeed_nid_postfix(void* attributes, std::uint32_t speed) { + if (attributes == nullptr) throw std::invalid_argument("cfsetspeed: attributes is null"); + auto* bytes = static_cast(attributes); + std::memcpy(bytes + offsetof(GuestTermios, inputSpeed), &speed, sizeof(speed)); + std::memcpy(bytes + offsetof(GuestTermios, outputSpeed), &speed, sizeof(speed)); + return 0; +} + int APS5_VABI tcgetattr_nid_postfix(int descriptor, void* attributes) { static_cast(attributes); return Fail(TerminalError(descriptor)); diff --git a/core/libs/prx/libkernel/File/src/TrustedRoots.cpp b/core/libs/prx/libkernel/File/src/TrustedRoots.cpp new file mode 100644 index 0000000000..e5f1f4f0c4 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/TrustedRoots.cpp @@ -0,0 +1,78 @@ +#include "prx/libkernel/File/include/TrustedRoots.hpp" + +#include +#include +#include +#include +#include + +namespace { + +void RequireCrypto(bool success, const char* operation) { + if (!success) throw std::system_error(static_cast(GetLastError()), std::system_category(), operation); +} + +bool AllowsServerAuthentication(PCCERT_CONTEXT certificate) { + DWORD size = 0; + RequireCrypto(CertGetEnhancedKeyUsage(certificate, 0, nullptr, &size), "Reading root certificate usage size"); + std::vector storage(size); + auto* usage = reinterpret_cast(storage.data()); + SetLastError(ERROR_SUCCESS); + RequireCrypto(CertGetEnhancedKeyUsage(certificate, 0, usage, &size), "Reading root certificate usage"); + if (usage->cUsageIdentifier == 0) return GetLastError() == static_cast(CRYPT_E_NOT_FOUND); + for (DWORD index = 0; index < usage->cUsageIdentifier; ++index) { + const auto* identifier = usage->rgpszUsageIdentifier[index]; + if (std::strcmp(identifier, szOID_PKIX_KP_SERVER_AUTH) == 0 || std::strcmp(identifier, szOID_ANY_ENHANCED_KEY_USAGE) == 0) + return true; + } + return false; +} + +std::string Pem(PCCERT_CONTEXT certificate) { + DWORD size = 0; + RequireCrypto(CryptBinaryToStringA(certificate->pbCertEncoded, certificate->cbCertEncoded, + CRYPT_STRING_BASE64HEADER, nullptr, &size), "Measuring a PEM root certificate"); + std::string pem(size, '\0'); + RequireCrypto(CryptBinaryToStringA(certificate->pbCertEncoded, certificate->cbCertEncoded, + CRYPT_STRING_BASE64HEADER, pem.data(), &size), "Encoding a PEM root certificate"); + pem.resize(size); + return pem; +} + +} + +extern "C" std::string ExportHostRoots_nid_no_patch(HCERTSTORE roots, HCERTSTORE disallowed) { + if (!roots || !disallowed) throw std::invalid_argument("ExportHostRoots: null certificate store"); + std::string bundle; + using Certificate = std::unique_ptr; + Certificate certificate(nullptr, &CertFreeCertificateContext); + for (;;) { + auto* next = CertEnumCertificatesInStore(roots, certificate.release()); + if (!next) { + const auto error = GetLastError(); + if (error != static_cast(CRYPT_E_NOT_FOUND)) + throw std::system_error(static_cast(error), std::system_category(), "Enumerating trusted roots"); + break; + } + certificate.reset(next); + Certificate rejected(CertFindCertificateInStore(disallowed, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + 0, CERT_FIND_EXISTING, certificate.get(), nullptr), &CertFreeCertificateContext); + if (rejected) continue; + const auto error = GetLastError(); + if (error != static_cast(CRYPT_E_NOT_FOUND)) + throw std::system_error(static_cast(error), std::system_category(), "Checking distrusted roots"); + if (AllowsServerAuthentication(certificate.get())) bundle += Pem(certificate.get()); + } + return bundle; +} + +extern "C" std::string HostTrustedTlsRoots_nid_no_patch() { + const auto closeStore = [](void* store) { CertCloseStore(store, 0); }; + using Store = std::unique_ptr; + const DWORD flags = CERT_SYSTEM_STORE_CURRENT_USER | CERT_STORE_READONLY_FLAG | CERT_STORE_OPEN_EXISTING_FLAG; + Store roots(CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, 0, flags, L"ROOT"), closeStore); + RequireCrypto(roots != nullptr, "Opening the Windows trusted root store"); + Store disallowed(CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, 0, flags, L"Disallowed"), closeStore); + RequireCrypto(disallowed != nullptr, "Opening the Windows disallowed certificate store"); + return ExportHostRoots_nid_no_patch(roots.get(), disallowed.get()); +} diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index 4dac33c1dd..fdead458de 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libkernel/Module/ModuleArgs.hpp" #include +#include #include #include #include @@ -39,6 +40,9 @@ struct Module { void* native = nullptr; bool owned = true; bool global = false; +#ifdef _WIN32 + std::uintptr_t globalOrder = 0; +#endif ~Module() { if (owned && native) { #ifdef _WIN32 @@ -193,7 +197,21 @@ void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { #endif std::lock_guard lock(modulesMutex); const auto handle = nextHandle++; - modules.emplace(handle, std::move(module)); +#ifdef _WIN32 + module->globalOrder = module->global ? handle : 0; + for (const auto& [key, entry] : modules) { + if (entry->native == module->native && entry->globalOrder != 0 && + (module->globalOrder == 0 || entry->globalOrder < module->globalOrder)) module->globalOrder = entry->globalOrder; + } +#endif + modules.emplace(handle, module); +#ifdef _WIN32 + for (const auto& [key, entry] : modules) { + if (entry->native != module->native) continue; + entry->global = module->globalOrder != 0; + entry->globalOrder = module->globalOrder; + } +#endif return reinterpret_cast(handle); } catch (const std::exception& error) { Error(error.what()); return nullptr; } } @@ -205,6 +223,10 @@ void* APS5_VABI dlsym_nid_postfix(void* handle, const char* name) { std::lock_guard lock(modulesMutex); if (handle == nullptr || handle == reinterpret_cast(static_cast(-2))) { for (const auto& [key, module] : modules) if (module->global) search.push_back(module); +#ifdef _WIN32 + std::sort(search.begin(), search.end(), [](const auto& left, const auto& right) { return left->globalOrder < right->globalOrder; }); + search.erase(std::unique(search.begin(), search.end(), [](const auto& left, const auto& right) { return left->native == right->native; }), search.end()); +#endif } else { auto found = modules.find(reinterpret_cast(handle)); if (found == modules.end()) { Error("dlsym: invalid or unsupported module handle"); return nullptr; } @@ -217,6 +239,8 @@ void* APS5_VABI dlsym_nid_postfix(void* handle, const char* name) { if (auto* result = DefaultScopeSymbol(name)) return result; const auto nid = Nid::ComputeNid(name, ""); if (auto* result = DefaultScopeSymbol(nid.c_str())) return result; + if (auto* result = DefaultScopeSymbol((std::string(name) + "#guest").c_str())) return result; + if (auto* result = DefaultScopeSymbol((nid + "#guest").c_str())) return result; } #endif Error("dlsym: symbol not found in supported module scope"); @@ -226,26 +250,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/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index 6e06be8deb..9e71a3183c 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -1,8 +1,10 @@ #include #include #include +#include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -153,10 +155,22 @@ extern "C" { void* APS5_VABI dlopen_nid_postfix(const char* path, int flags); void* APS5_VABI dlsym_nid_postfix(void* handle, const char* name); int APS5_VABI dlclose_nid_postfix(void* handle); +const char* APS5_VABI sceKernelGetFsSandboxRandomWord(); } namespace { constexpr int kRtldNow = 2; + +std::optional SystemLibraryPath(std::string_view path) { + const std::string sandbox = std::string("/") + sceKernelGetFsSandboxRandomWord() + "/common/lib/"; + for (const std::string_view prefix : {std::string_view("/system/common/lib/"), std::string_view(sandbox)}) { + if (!path.starts_with(prefix)) continue; + const auto name = path.substr(prefix.size()); + if (name.size() <= 5 || !name.ends_with(".sprx") || name.find('/') != std::string_view::npos) return std::nullopt; + return "libs/" + std::string(name.substr(0, name.size() - 5)) + ".prx"; + } + return std::nullopt; +} } extern "C" { @@ -281,7 +295,8 @@ KernelModule APS5_VABI sceKernelLoadStartModule(const char* module_file_name, si if (!module_file_name) return static_cast(SCE_KERNEL_ERROR_EFAULT); pendingModuleArgs = {args, argp}; pendingModuleInitResult = 0; - void* handle = dlopen_nid_postfix(module_file_name, kRtldNow); + const auto systemLibrary = SystemLibraryPath(module_file_name); + void* handle = dlopen_nid_postfix(systemLibrary ? systemLibrary->c_str() : module_file_name, kRtldNow); const int started = pendingModuleInitResult; pendingModuleArgs = {}; pendingModuleInitResult = 0; @@ -306,6 +321,17 @@ extern "C" { int APS5_VABI __elf_phdr_match_addr_nid_postfix(dl_phdr_info* phdrInfo, void* addr) { if (phdrInfo == nullptr) throw std::invalid_argument("__elf_phdr_match_addr: phdr_info is null"); const auto address = reinterpret_cast(addr); + + const auto* module = reinterpret_cast(phdrInfo); + if (module->st_size == sizeof(ModuleInfoEx)) { + for (std::uint32_t i = 0; i < module->segment_count && i < std::size(module->segments); ++i) { + const auto& segment = module->segments[i]; + if ((segment.prot & 4) == 0 || segment.size < sizeof(addr)) continue; + if (segment.address <= address && address - segment.address <= segment.size - sizeof(addr)) return 1; + } + return 0; + } + for (std::uint16_t i = 0; i < phdrInfo->dlpi_phnum; ++i) { const Elf64_Phdr& header = phdrInfo->dlpi_phdr[i]; if (header.p_type != PT_LOAD || (header.p_flags & PF_X) == 0) continue; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp index ea6ce0c0b1..86fd01c134 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; } @@ -33,6 +38,7 @@ int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr) p->stackAddress = thread->stackAddress; p->_detachstate = thread->_detached ? 1 : 0; p->_schedpriority = thread->priority.load(std::memory_order_relaxed); + p->_schedpolicy = thread->schedPolicy.load(std::memory_order_relaxed); p->_affinity = thread->affinity.load(std::memory_order_relaxed); p->_affinityExplicit = true; p->_scope = thread->scope; @@ -120,13 +126,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..4f4c7884db 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/General.hpp" +#include #include #include #include @@ -18,7 +19,6 @@ struct PthreadBarrierPrivate { namespace { constexpr int PROCESS_PRIVATE = 0; -constexpr int PROCESS_SHARED = 1; } @@ -54,14 +54,14 @@ 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; } int APS5_VABI pthread_barrier_init_nid_postfix(PthreadBarrierPrivate** barrier, PthreadBarrierattrPrivate* const* attr, unsigned count) { (void)attr; - if (!barrier || count == 0) return 22; + if (!barrier || count == 0 || count > INT_MAX) return 22; try { auto* value = new PthreadBarrierPrivate; value->count = count; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp index 5c5e64d18c..f357cac53a 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; @@ -37,7 +39,7 @@ int APS5_VABI pthread_cond_signal_nid_postfix(PthreadCond* cond) { } int APS5_VABI pthread_cond_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* abstime) { - if (!abstime || abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000) return PosixThread::GUEST_EINVAL; + if (!abstime || abstime->tv_sec < 0 || abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000) return PosixThread::GUEST_EINVAL; return toPosix(CondOperations::AbsoluteTimedwait(cond, mutex, abstime)); } @@ -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 7ab97118fb..346b447bb8 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp @@ -12,6 +12,7 @@ namespace { constexpr int POSIX_EINVAL = 22; +constexpr int POSIX_PROCESS_PRIVATE = 0; constexpr int GUEST_REALTIME_CLOCK = 0; constexpr int POSIX_PRIO_PROTECT = 2; constexpr int POSIX_SCHED_RR = 3; @@ -28,7 +29,7 @@ void _initializeStatic(PthreadMutex* mutex, const char* funcName) { PthreadMutex current = slot.load(std::memory_order_acquire); if (!_isStaticInitializer(current)) return; auto* created = new PthreadMutexPrivate(); - created->_type = reinterpret_cast(current) == POSIX_ADAPTIVE_MUTEX_INITIALIZER ? MutexType::Adaptive : MutexType::ErrorCheck; + created->SetType(reinterpret_cast(current) == POSIX_ADAPTIVE_MUTEX_INITIALIZER ? MutexType::Adaptive : MutexType::ErrorCheck); if (!slot.compare_exchange_strong(current, created, std::memory_order_acq_rel, std::memory_order_acquire)) delete created; } @@ -60,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; @@ -78,6 +79,31 @@ int APS5_VABI pthread_mutex_unlock_nid_postfix(PthreadMutex* mutex) { return PosixThread::ToErrno(scePthreadMutexUnlock(mutex)); } +int APS5_VABI pthread_mutex_reltimedlock_np_nid_postfix(PthreadMutex* mutex, KernelUseconds usec) { + _initializeStatic(mutex, __func__); + return PosixThread::ToErrno(scePthreadMutexTimedlock(mutex, usec)); +} + +int APS5_VABI pthread_mutex_isowned_np_nid_postfix(PthreadMutex* mutex) { + return MutexOperations::IsOwned(mutex) ? 1 : 0; +} + +int APS5_VABI pthread_mutex_getspinloops_np_nid_postfix(PthreadMutex* mutex, int* count) { + return PosixThread::ToErrno(scePthreadMutexGetspinloops(mutex, count)); +} + +int APS5_VABI pthread_mutex_setspinloops_np_nid_postfix(PthreadMutex* mutex, int count) { + return PosixThread::ToErrno(scePthreadMutexSetspinloops(mutex, count)); +} + +int APS5_VABI pthread_mutex_getyieldloops_np_nid_postfix(PthreadMutex* mutex, int* count) { + return PosixThread::ToErrno(scePthreadMutexGetyieldloops(mutex, count)); +} + +int APS5_VABI pthread_mutex_setyieldloops_np_nid_postfix(PthreadMutex* mutex, int count) { + return PosixThread::ToErrno(scePthreadMutexSetyieldloops(mutex, count)); +} + int APS5_VABI pthread_mutexattr_destroy_nid_postfix(PthreadMutexattr* attr) { return PosixThread::ToErrno(scePthreadMutexattrDestroy(attr)); } @@ -88,23 +114,44 @@ int APS5_VABI pthread_mutexattr_init_nid_postfix(PthreadMutexattr* attr) { return PosixThread::ToErrno(result); } +int APS5_VABI pthread_mutexattr_getpshared_nid_postfix(const PthreadMutexattr* attr, int* pshared) { + if (!attr || !*attr) return POSIX_EINVAL; + if (!pshared) throw std::runtime_error("pthread_mutexattr_getpshared: null pshared"); + *pshared = (*attr)->pshared; + return 0; +} + +int APS5_VABI pthread_mutexattr_setpshared_nid_postfix(PthreadMutexattr* attr, int pshared) { + if (!attr || !*attr) return POSIX_EINVAL; + if (pshared != POSIX_PROCESS_PRIVATE) 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); return 0; } +int APS5_VABI pthread_mutexattr_getprotocol_nid_postfix(const PthreadMutexattr* attr, int* protocol) { + if (!attr || !*attr) return PosixThread::GUEST_EINVAL; + if (!protocol) throw std::runtime_error("pthread_mutexattr_getprotocol: null protocol"); + *protocol = (*attr)->protocol; + return 0; +} + int APS5_VABI pthread_mutexattr_setprioceiling_nid_postfix(PthreadMutexattr* attr, int prioceiling) { - if (!attr || !*attr) 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..203af9f0f4 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; @@ -77,6 +93,16 @@ int APS5_VABI pthread_rwlock_timedwrlock_nid_postfix(PthreadRwlock* rwlock, cons return toPosix(scePthreadRwlockTimedwrlock(rwlock, usec)); } +int APS5_VABI pthread_rwlock_reltimedrdlock_np_nid_postfix(PthreadRwlock* rwlock, KernelUseconds usec) { + initializeStatic(rwlock, __func__); + return toPosix(scePthreadRwlockTimedrdlock(rwlock, usec)); +} + +int APS5_VABI pthread_rwlock_reltimedwrlock_np_nid_postfix(PthreadRwlock* rwlock, KernelUseconds usec) { + initializeStatic(rwlock, __func__); + return toPosix(scePthreadRwlockTimedwrlock(rwlock, usec)); +} + int APS5_VABI pthread_rwlock_tryrdlock_nid_postfix(PthreadRwlock* rwlock) { initializeStatic(rwlock, __func__); return toPosix(scePthreadRwlockTryrdlock(rwlock)); 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 90ad038f81..06705a3130 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -8,7 +8,8 @@ #include "../include/Pthread.hpp" #include "Common.hpp" #include "prx/libkernel/KernelErrors.hpp" -#include +#include +#include #include #include @@ -20,17 +21,28 @@ int APS5_VABI scePthreadJoin(Pthread thread, void** retval); int APS5_VABI scePthreadRename(Pthread thread, const char* name); int APS5_VABI scePthreadGetname(Pthread thread, char* name); Pthread APS5_VABI scePthreadSelf(); +int APS5_VABI scePthreadEqual(Pthread thread1, Pthread thread2); +int APS5_VABI scePthreadOnce(int32_t* once, void (APS5_VABI* init)(void)); +int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t destructor); +int APS5_VABI scePthreadKeyDelete(PthreadKey key); +void* APS5_VABI scePthreadGetspecific(PthreadKey key); +int APS5_VABI scePthreadSetspecific(PthreadKey key, void* value); int APS5_VABI scePthreadCancel(Pthread thread); int APS5_VABI scePthreadSetcancelstate(int state, int* old_state); int APS5_VABI scePthreadSetcanceltype(int type, int* old_type); void APS5_VABI scePthreadTestcancel(); int APS5_VABI scePthreadSetprio(Pthread thread, int prio); int APS5_VABI scePthreadGetprio(Pthread thread, int* prio); +void APS5_VABI scePthreadYield(); } static constexpr int GUEST_SCHED_FIFO = 1; static constexpr int GUEST_SCHED_RR = 3; +namespace { +std::atomic guestConcurrency{0}; +} + static bool AcceptsSchedParam(int policy, int priority) { if (policy != GUEST_SCHED_FIFO && policy != GUEST_SCHED_RR) return false; return (priority >= 256 && priority <= 767) || (priority >= 896 && priority <= 959); @@ -56,11 +68,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) { @@ -70,9 +81,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; @@ -94,10 +103,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) { @@ -111,11 +117,55 @@ 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; } +struct GuestRtPrio { + std::uint16_t type; + std::uint16_t prio; +}; + +int APS5_VABI rtprio_thread_nid_postfix(int function, int lwpid, GuestRtPrio* priority) { + constexpr int Lookup = 0, Set = 1; + constexpr int GuestEsrch = 3, GuestEfault = 14, GuestEinval = 22; + constexpr std::uint16_t Normal = 3; + const auto fail = [](int error) { + *__error_nid_postfix() = error; + return -1; + }; + if (function != Lookup && function != Set) return fail(GuestEinval); + if (priority == nullptr) return fail(GuestEfault); + auto* self = scePthreadSelf(); + if (lwpid != 0 && static_cast(lwpid) != static_cast(self->tid)) { + if (!LiveGuestThreadExists(lwpid)) return fail(GuestEsrch); + NotImplemented_nid_no_patch("rtprio_thread: a thread other than the calling one"); + } + const int policy = self->schedPolicy.load(std::memory_order_relaxed); + if (policy == 1 || policy == 3) NotImplemented_nid_no_patch("rtprio_thread: a guest thread's real-time class"); + if (function == Lookup) { + priority->type = Normal; + priority->prio = 0; + return 0; + } + if (priority->type == Normal && priority->prio == 0) return 0; + NotImplemented_nid_no_patch("rtprio_thread: a real-time priority change"); +} + +int APS5_VABI pthread_setconcurrency_nid_postfix(int newLevel) { + constexpr int GuestEinval = 22; + if (newLevel < 0) return GuestEinval; + guestConcurrency.store(newLevel, std::memory_order_relaxed); + return 0; +} + +int APS5_VABI pthread_getconcurrency_nid_postfix() { + return guestConcurrency.load(std::memory_order_relaxed); +} + int APS5_VABI pthread_create_nid_postfix(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg) { return PosixThread::ToErrno(scePthreadCreate(thread, attr, entry, arg, nullptr)); } @@ -151,6 +201,15 @@ int APS5_VABI pthread_getschedparam_nid_postfix(Pthread thread, int* policy, Ker return PosixThread::ToErrno(scePthreadGetschedparam(thread, policy, param)); } +int APS5_VABI pthread_getprio_nid_postfix(Pthread thread) { + const auto lock = LockLiveGuestThread(thread); + if (!lock.owns_lock()) { + *__error_nid_postfix() = PosixThread::GUEST_ESRCH; + return -1; + } + return thread->priority.load(std::memory_order_relaxed); +} + int APS5_VABI pthread_getaffinity_np_nid_postfix(Pthread thread, std::size_t cpusetSize, void* cpuset) { if (!thread) return PosixThread::GUEST_EINVAL; if (cpusetSize != GUEST_CPUSET_BYTES) return GUEST_ERANGE; @@ -182,6 +241,28 @@ int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value) { return PosixThread::ToErrno(scePthreadJoin(thread, value)); } +namespace { +struct RegisterPthreadOnLoad { + RegisterPthreadOnLoad() { + RegisterPthreadCreate_nid_no_patch(reinterpret_cast(pthread_create_nid_postfix)); + RegisterPthreadJoin_nid_no_patch(reinterpret_cast(pthread_join_nid_postfix)); + RegisterPthreadDetach_nid_no_patch(reinterpret_cast(pthread_detach_nid_postfix)); + static const ThreadForwards_nid_no_patch forwards{ + reinterpret_cast(scePthreadOnce), + reinterpret_cast(scePthreadKeyCreate), + scePthreadKeyDelete, + scePthreadGetspecific, + scePthreadSetspecific, + reinterpret_cast(scePthreadEqual), + reinterpret_cast(scePthreadSelf), + scePthreadExit, + }; + RegisterThreadForwards_nid_no_patch(&forwards); + } +}; +const RegisterPthreadOnLoad gRegisterPthread; +} + int APS5_VABI pthread_rename_np_nid_postfix(Pthread thread, const char* name) { if (thread && name && std::strlen(name) >= 32) return PosixThread::ToErrno(SCE_KERNEL_ERROR_ENAMETOOLONG); return PosixThread::ToErrno(scePthreadRename(thread, name)); @@ -190,6 +271,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)); } @@ -220,7 +304,7 @@ int APS5_VABI pthread_getcpuclockid_nid_postfix(Pthread thread, int* clockId) { } int APS5_VABI sched_yield_nid_postfix(void) { - std::this_thread::yield(); + scePthreadYield(); return 0; } @@ -250,12 +334,20 @@ void APS5_VABI pthread_testcancel_nid_postfix(void) { } void APS5_VABI pthread_yield_nid_postfix(void) { - std::this_thread::yield(); + scePthreadYield(); } unsigned int APS5_VABI sleep_nid_postfix(unsigned int seconds) { ThreadCancel::Check(); - std::this_thread::sleep_for(std::chrono::seconds(seconds)); + { + const auto deadline = TimedWait::DeadlineNanos(static_cast(seconds) * 1000000ULL); + std::mutex mutex; + TimedWait::Condition condition; + std::unique_lock lock(mutex); + while (!ThreadCancel::Requested()) { + if (!ThreadCancel::Wait(condition, lock, deadline)) break; + } + } ThreadCancel::Check(); return 0; } diff --git a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp index 6a3dad6611..e45cbaee3f 100644 --- a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp @@ -22,6 +22,8 @@ struct CleanupHandler { extern "C" { 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); +void APS5_VABI pthread_cleanup_push_nid_postfix(void (APS5_VABI* routine)(void*), void* argument); +void APS5_VABI pthread_cleanup_pop_nid_postfix(int execute); } namespace ThreadCancel { @@ -61,6 +63,7 @@ bool WaitUntil(TimedWait::Condition& condition, TLock& lock, std::optional #include #include +#include +#include #include #include #include @@ -32,16 +34,26 @@ struct PthreadMutexattrPrivate { MutexType type; int protocol = 0; int ceiling = 0; + int pshared = 0; }; +constexpr int ADAPTIVE_MUTEX_SPINS = 2000; + struct PthreadMutexPrivate { std::recursive_timed_mutex _rmtx; std::timed_mutex _mtx; MutexType _type; std::atomic _owner; int _count; + int _spinloops = 0; + int _yieldloops = 0; PthreadMutexPrivate() : _type(MutexType::ErrorCheck), _owner(std::thread::id{}), _count(0) {} + + void SetType(MutexType type) { + _type = type; + _spinloops = type == MutexType::Adaptive ? ADAPTIVE_MUTEX_SPINS : 0; + } }; struct PthreadRwlockattrPrivate { @@ -58,10 +70,11 @@ struct PthreadRwlockPrivate { struct PthreadCondattrPrivate { int _clockid; + int _pshared = 0; }; struct PthreadCondPrivate { - TimedWait::Condition _cv; + std::shared_ptr _cv = std::make_shared(); int _clockid = 0; }; @@ -139,6 +152,9 @@ struct PthreadPrivate { void* _retval; bool _detached; bool _adopted; + Pthread* _handleStorage; + std::atomic _joined; + bool _joining = false; std::mutex _join_mtx; TimedWait::Condition _join_cv; std::atomic cancelPending{false}; @@ -154,11 +170,19 @@ struct PthreadPrivate { bool GuestThreadStack(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end); bool GuestThreadStackHostRangeOverlaps(std::uintptr_t start, std::uintptr_t end); void NoteGuestStackProtection(std::uintptr_t start, std::size_t bytes, bool readWrite); +#ifdef _WIN32 +bool ReplaceGuestThreadStack(std::uintptr_t start, std::size_t bytes, bool readWrite, const std::function& replace); +#endif int GuestThreadCpuClockId(const PthreadPrivate* thread); bool GuestThreadCpuNanos(int cpuClockThread, std::uint64_t* nanos); void SetLiveGuestThreadsAffinity(KernelCpumask mask); KernelCpumask LiveGuestThreadsAffinity(); bool GuestThreadIsLive(const PthreadPrivate* thread); +std::unique_lock LockLiveGuestThread(const PthreadPrivate* thread); bool LiveGuestThreadExists(std::int64_t tid); +bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask); +bool SetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask mask); +extern "C" std::size_t GetDefaultStackSize_nid_no_patch(); +extern "C" bool SetDefaultStackSize_nid_no_patch(std::size_t size); #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..0b3b82078c 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -12,6 +12,18 @@ static constexpr int DETACH_JOINABLE = 0; static constexpr int DETACH_DETACHED = 1; static constexpr int SCHED_FIFO_PS5 = 1; +static std::atomic defaultStackSize{DEFAULT_STACK_SIZE}; + +std::size_t GetDefaultStackSize_nid_no_patch() { + return defaultStackSize.load(std::memory_order_relaxed); +} + +bool SetDefaultStackSize_nid_no_patch(std::size_t size) { + if (size <= MIN_STACK_SIZE) return false; + defaultStackSize.store(size, std::memory_order_relaxed); + return true; +} + #ifdef _WIN32 #include #include @@ -23,7 +35,7 @@ int APS5_VABI scePthreadAttrInit(PthreadAttr* attr) { if (!attr) throw std::runtime_error(std::string(__func__) + ": null attr"); auto* p = new (std::nothrow) PthreadAttrPrivate{}; if (!p) return SCE_KERNEL_ERROR_ENOMEM; - p->_stacksize = DEFAULT_STACK_SIZE; + p->_stacksize = GetDefaultStackSize_nid_no_patch(); p->_detachstate = DETACH_JOINABLE; p->_schedpriority = 700; p->_schedpolicy = SCHED_FIFO_PS5; @@ -68,8 +80,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; @@ -78,11 +89,13 @@ int APS5_VABI scePthreadAttrGetstack(const PthreadAttr* attr, void** stackaddr, int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr) { if (!thread || !attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + const auto live = LockLiveGuestThread(thread); + if (!live) return SCE_KERNEL_ERROR_ESRCH; (*attr)->_stacksize = thread->stackSize; (*attr)->stackAddress = thread->stackAddress; (*attr)->_detachstate = thread->_detached ? DETACH_DETACHED : DETACH_JOINABLE; (*attr)->_schedpriority = thread->priority.load(std::memory_order_relaxed); - (*attr)->_schedpolicy = SCHED_FIFO_PS5; + (*attr)->_schedpolicy = thread->schedPolicy.load(std::memory_order_relaxed); (*attr)->_inheritsched = 4; (*attr)->_affinity = thread->affinity.load(std::memory_order_relaxed); (*attr)->_affinityExplicit = true; @@ -104,18 +117,30 @@ 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; } +int APS5_VABI scePthreadAttrGetinheritsched(const PthreadAttr* attr, int* inheritSched) { + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + if (!inheritSched) throw std::runtime_error(std::string(__func__) + ": null output"); + *inheritSched = (*attr)->_inheritsched; + return SCE_OK; +} + int APS5_VABI scePthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedParam* param) { if (!attr || !*attr || !param) return SCE_KERNEL_ERROR_EINVAL; param->sched_priority = (*attr)->_schedpriority; return SCE_OK; } +int APS5_VABI scePthreadAttrGetschedpolicy(const PthreadAttr* attr, int* policy) { + if (!attr || !*attr || !policy) return SCE_KERNEL_ERROR_EINVAL; + *policy = (*attr)->_schedpolicy; + return SCE_OK; +} + int APS5_VABI scePthreadAttrGetsolosched(const PthreadAttr* attr, int* solosched) { if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; if (!solosched) throw std::runtime_error(std::string(__func__) + ": null output"); @@ -124,8 +149,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 +168,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 +200,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..a3a3087bf7 100644 --- a/core/libs/prx/libkernel/Pthread/src/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Cond.cpp @@ -2,6 +2,7 @@ #include "prx/libkernel/Pthread/include/Mutex.hpp" #include "prx/libkernel/Pthread/include/Cond.hpp" #include "prx/libkernel/Pthread/include/Cancel.hpp" +#include "prx/libkernel/Pthread/include/ThreadLifecycle.hpp" #include "prx/libkernel/Pthread/Posix/Common.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Time/include/Time.hpp" @@ -13,11 +14,15 @@ #include #include #include +#include namespace { constexpr int sceTimedOut = static_cast(0x8002003cu); -std::mutex condInitializationMutex; +std::mutex& InitializationMutex() { + static std::mutex mutex; + return mutex; +} PthreadCond destroyedCond() { return reinterpret_cast(std::uintptr_t{2}); @@ -31,21 +36,20 @@ PthreadMutex adaptiveInitializer() { return reinterpret_cast(std::uintptr_t{1}); } -PthreadCond resolveCond(PthreadCond* cond) { +std::shared_ptr resolveCond(PthreadCond* cond, int* clockId = nullptr) { if (!cond) throw std::invalid_argument("Condition variable pointer is null"); + std::lock_guard lock(InitializationMutex()); std::atomic_ref slot(*cond); - if (const auto current = slot.load(std::memory_order_acquire); current && current != destroyedCond()) - return current; - std::lock_guard lock(condInitializationMutex); - const auto current = slot.load(std::memory_order_acquire); + auto current = slot.load(std::memory_order_acquire); if (current == destroyedCond()) - throw std::runtime_error("Condition variable has been destroyed"); - if (current) - return current; - auto* created = new PthreadCondPrivate(); - slot.store(created, std::memory_order_release); - return created; + return nullptr; + if (!current) { + current = new PthreadCondPrivate(); + slot.store(current, std::memory_order_release); + } + if (clockId) *clockId = current->_clockid; + return current->_cv; } int mutexOwnership(PthreadMutex* mutex, PthreadMutex* owned) { @@ -73,8 +77,18 @@ struct WakingLock { } }; -int waitUntil(PthreadCond* cond, PthreadMutex* mutex, std::optional deadlineNanos, const void* caller) { - auto* c = resolveCond(cond); +int finishWait(std::shared_ptr& condition, bool timedOut) { + if (ThreadCancel::Requested()) { + condition->NotifyOne(); + condition.reset(); + scePthreadExit(GUEST_CANCELED); + } + return timedOut ? sceTimedOut : 0; +} + +int waitUntil(std::shared_ptr condition, PthreadMutex* mutex, std::optional deadlineNanos, const void* caller) { + if (!condition) + return SCE_KERNEL_ERROR_EINVAL; PthreadMutex m = nullptr; if (const int error = mutexOwnership(mutex, &m)) return error; @@ -92,32 +106,34 @@ int waitUntil(PthreadCond* cond, PthreadMutex* mutex, std::optional_owner.store(std::thread::id{}, std::memory_order_release); for (int level = 1; level < previousCount; ++level) m->_rmtx.unlock(); - timedOut = !ThreadCancel::Wait(c->_cv, lock, deadlineNanos); + timedOut = !ThreadCancel::Wait(*condition, lock, deadlineNanos); for (int level = 1; level < previousCount; ++level) m->_rmtx.lock(); m->_owner.store(std::this_thread::get_id(), std::memory_order_release); m->_count = previousCount; lock.release(); - ThreadCancel::Check(); - return timedOut ? sceTimedOut : 0; + return finishWait(condition, timedOut); } WakingLock waking{m, m->_mtx}; std::unique_lock lock(waking, std::adopt_lock); m->_owner.store(std::thread::id{}, std::memory_order_release); - timedOut = !ThreadCancel::Wait(c->_cv, lock, deadlineNanos); + timedOut = !ThreadCancel::Wait(*condition, lock, deadlineNanos); m->_owner.store(std::this_thread::get_id(), std::memory_order_release); lock.release(); - ThreadCancel::Check(); - return timedOut ? sceTimedOut : 0; + return finishWait(condition, timedOut); } } int CondOperations::AbsoluteTimedwait(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* abstime) { KernelUseconds usec = 0; - if (!PosixThread::RelativeMicroseconds(resolveCond(cond)->_clockid, abstime, &usec)) + int clockId = 0; + auto condition = resolveCond(cond, &clockId); + if (!condition) + return SCE_KERNEL_ERROR_EINVAL; + if (!PosixThread::RelativeMicroseconds(clockId, abstime, &usec)) throw std::invalid_argument("Invalid absolute condition variable timeout"); - return waitUntil(cond, mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); + return waitUntil(std::move(condition), mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); } extern "C" { @@ -130,16 +146,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,12 +161,10 @@ 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::lock_guard lock(InitializationMutex()); std::atomic_ref(*cond).store(replacement.release(), std::memory_order_release); return 0; } @@ -160,36 +172,45 @@ int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, int APS5_VABI scePthreadCondDestroy(PthreadCond* cond) { if (!cond) throw std::invalid_argument("Condition variable pointer is null"); - std::lock_guard lock(condInitializationMutex); + std::lock_guard lock(InitializationMutex()); if (*cond == destroyedCond()) - throw std::runtime_error("Condition variable has already been destroyed"); + return SCE_KERNEL_ERROR_EINVAL; delete *cond; std::atomic_ref(*cond).store(destroyedCond(), std::memory_order_release); return 0; } int APS5_VABI scePthreadCondSignal(PthreadCond* cond) { - resolveCond(cond)->_cv.NotifyOne(); + const auto resolved = resolveCond(cond); + if (!resolved) + return SCE_KERNEL_ERROR_EINVAL; + resolved->NotifyOne(); return 0; } int APS5_VABI scePthreadCondBroadcast(PthreadCond* cond) { - resolveCond(cond)->_cv.NotifyAll(); + const auto resolved = resolveCond(cond); + if (!resolved) + return SCE_KERNEL_ERROR_EINVAL; + resolved->NotifyAll(); return 0; } int APS5_VABI scePthreadCondSignalto(PthreadCond* cond, Pthread thread) { (void)thread; - resolveCond(cond)->_cv.NotifyAll(); + const auto resolved = resolveCond(cond); + if (!resolved) + return SCE_KERNEL_ERROR_EINVAL; + resolved->NotifyAll(); return 0; } int APS5_VABI scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex) { - return waitUntil(cond, mutex, std::nullopt, __builtin_return_address(0)); + return waitUntil(resolveCond(cond), mutex, std::nullopt, __builtin_return_address(0)); } int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec) { - return waitUntil(cond, mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); + return waitUntil(resolveCond(cond), mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); } } diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index 93e0e693b6..d7bc4013f8 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -16,7 +16,10 @@ constexpr int sceNotPermitted = static_cast(0x80020001u); constexpr int sceDeadlock = static_cast(0x8002000bu); constexpr int sceBusy = static_cast(0x80020010u); constexpr int sceTimedOut = static_cast(0x8002003cu); -std::mutex initializationMutex; +std::mutex& InitializationMutex() { + static std::mutex mutex; + return mutex; +} PthreadMutex destroyedMutex() { return reinterpret_cast(std::uintptr_t{2}); @@ -36,23 +39,25 @@ PthreadMutex resolveMutex(PthreadMutex* mutex, bool initialize) { std::atomic_ref slot(*mutex); if (const auto current = slot.load(std::memory_order_acquire); isInitializedMutex(current)) return current; - std::lock_guard lock(initializationMutex); + std::lock_guard lock(InitializationMutex()); const auto current = slot.load(std::memory_order_acquire); if (current == destroyedMutex()) - throw std::runtime_error("Mutex has been destroyed"); + return nullptr; if (current && current != adaptiveInitializer()) return current; if (!initialize) throw std::runtime_error("Mutex is not initialized"); auto* created = new PthreadMutexPrivate(); if (current == adaptiveInitializer()) - created->_type = MutexType::Adaptive; + created->SetType(MutexType::Adaptive); slot.store(created, std::memory_order_release); return created; } template int acquireMutex(PthreadMutex mutex, TAcquire acquire, int unavailable, bool tryOnly) { + if (!mutex) + return SCE_KERNEL_ERROR_EINVAL; const auto thread = std::this_thread::get_id(); const bool owned = mutex->_owner.load(std::memory_order_acquire) == thread; if (owned && mutex->_type != MutexType::Recursive) { @@ -78,6 +83,15 @@ int acquireMutex(PthreadMutex mutex, TAcquire acquire, int unavailable, bool try } +bool MutexOperations::IsOwned(const PthreadMutex* mutex) { + if (!mutex) + throw std::invalid_argument("Mutex pointer is null"); + const auto current = std::atomic_ref(*const_cast(mutex)).load(std::memory_order_acquire); + if (!isInitializedMutex(current)) + return false; + return current->_owner.load(std::memory_order_acquire) == std::this_thread::get_id(); +} + int MutexOperations::Timedlock(PthreadMutex* mutex, const KernelTimespec* abstime) { if (!abstime || abstime->tv_sec < 0 || abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000) throw std::invalid_argument("Invalid absolute mutex timeout"); @@ -150,8 +164,8 @@ int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* a throw std::invalid_argument("Mutex pointer is null"); auto replacement = std::make_unique(); if (attr && *attr) - replacement->_type = (*attr)->type; - std::lock_guard lock(initializationMutex); + replacement->SetType((*attr)->type); + std::lock_guard lock(InitializationMutex()); std::atomic_ref(*mutex).store(replacement.release(), std::memory_order_release); return 0; } @@ -159,7 +173,7 @@ int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* a int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex) { if (!mutex) throw std::invalid_argument("Mutex pointer is null"); - std::lock_guard lock(initializationMutex); + std::lock_guard lock(InitializationMutex()); if (*mutex == destroyedMutex()) return SCE_KERNEL_ERROR_EINVAL; if (*mutex == adaptiveInitializer()) { @@ -188,6 +202,8 @@ int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex) { int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex) { auto* current = resolveMutex(mutex, false); + if (!current) + return SCE_KERNEL_ERROR_EINVAL; if (current->_owner.load(std::memory_order_acquire) != std::this_thread::get_id()) return sceNotPermitted; bool released = true; @@ -220,4 +236,38 @@ int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex) { return acquireMutex(resolveMutex(mutex, true), [](auto& native) { return native.try_lock(); }, sceBusy, true); } +int APS5_VABI scePthreadMutexGetspinloops(PthreadMutex* mutex, int* count) { + auto* current = resolveMutex(mutex, true); + if (!current) + return SCE_KERNEL_ERROR_EINVAL; + if (!count) + throw std::invalid_argument("Spin loop count pointer is null"); + *count = current->_spinloops; + return 0; +} + +int APS5_VABI scePthreadMutexSetspinloops(PthreadMutex* mutex, int count) { + auto* current = resolveMutex(mutex, true); + if (!current) + return SCE_KERNEL_ERROR_EINVAL; + current->_spinloops = count; + return 0; +} + +int APS5_VABI scePthreadMutexGetyieldloops(PthreadMutex* mutex, int* count) { + auto* current = resolveMutex(mutex, true); + if (!current) + return SCE_KERNEL_ERROR_EINVAL; + if (!count) + throw std::invalid_argument("Yield loop count pointer is null"); + *count = current->_yieldloops; + return 0; +} + +int APS5_VABI scePthreadMutexSetyieldloops(PthreadMutex* mutex, int count) { + if (auto* current = resolveMutex(mutex, true)) + current->_yieldloops = count; + return 0; +} + } diff --git a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp index a77cd365f3..35a043a8e9 100644 --- a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp @@ -138,7 +138,6 @@ int APS5_VABI scePthreadRwlockTimedwrlock(PthreadRwlock* rwlock, KernelUseconds const bool acquired = lock->_changed.WaitUntil(state, deadline, [&] { return WriterMayEnter(lock); }); --lock->_waitingWriters; if (acquired) return EnterWrite(lock); - state.unlock(); lock->_changed.NotifyAll(); return SCE_KERNEL_ERROR_ETIMEDOUT; } @@ -155,7 +154,6 @@ int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock) { if (readLocksHeld != 0) --readLocksHeld; if (lock->_readers.fetch_sub(1, std::memory_order_acq_rel) != 1) return SCE_OK; } - state.unlock(); lock->_changed.NotifyAll(); return SCE_OK; } diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index caeb5c291d..aa76de407d 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -3,8 +3,10 @@ #include "../include/WindowsThreadLocal.hpp" #include "../include/Cancel.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libc/include/CpuTopology.hpp" +#include "prx/libc/include/GuestAllocations.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include #include @@ -33,7 +35,6 @@ static constexpr int SCE_OK = 0; -static constexpr std::size_t DEFAULT_STACK_SIZE = 1u << 20; static constexpr std::size_t HOST_STACK_MARGIN = 1u << 20; static constexpr int DETACH_DETACHED = 1; static constexpr std::size_t THREAD_NAME_CAPACITY = 32; @@ -41,6 +42,7 @@ static constexpr std::size_t THREAD_NAME_CAPACITY = 32; #ifdef _WIN32 #include #include +#include #endif struct ThreadArgs { @@ -72,9 +74,9 @@ static GuestThreadState& CurrentGuestState() { return state; #endif } -static std::mutex stackLock; -static std::map> liveStacks; -static std::map protectedStackPages; +static std::mutex& stackLock = *new std::mutex(); +static auto& liveStacks = *new std::map>(); +static auto& protectedStackPages = *new std::map(); #if defined(__linux__) struct GuestStackMapping { std::uintptr_t mappingEnd; @@ -82,7 +84,7 @@ struct GuestStackMapping { std::uintptr_t stackEnd; }; static std::mutex guestStackMappingsLock; -static std::map guestStackMappings; +static auto& guestStackMappings = *new std::map(); static void RegisterGuestStackMapping(PthreadPrivate* thread) { const auto mappingStart = reinterpret_cast(thread->guestStack); @@ -171,25 +173,51 @@ static void FreeGuestStack(PthreadPrivate*) {} static void RetireGuestStack(PthreadPrivate*) {} #endif +#ifdef _WIN32 +static bool CommittedReadWrite(std::uintptr_t begin, std::uintptr_t end) { + for (auto cursor = begin; cursor < end;) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) != sizeof(memory) || memory.RegionSize == 0) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "Querying the host thread stack"); + if (memory.State != MEM_COMMIT || memory.Protect != PAGE_READWRITE) return false; + cursor = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; + } + return true; +} + +static std::uintptr_t PrecommittedStackStart(std::uintptr_t low, std::uintptr_t high) { + SYSTEM_INFO system{}; + GetSystemInfo(&system); + const std::uintptr_t start = low + system.dwAllocationGranularity; + return start < high && CommittedReadWrite(start, high) ? start : low; +} +#endif + static bool HostStackLimits(std::uintptr_t* low, std::uintptr_t* high) { #ifdef _WIN32 ULONG_PTR lowLimit = 0; ULONG_PTR highLimit = 0; GetCurrentThreadStackLimits(&lowLimit, &highLimit); - *low = lowLimit; + *low = PrecommittedStackStart(lowLimit, highLimit); *high = highLimit; #else - pthread_attr_t attr; - if (pthread_getattr_np(pthread_self(), &attr) != 0) - return false; - void* address = nullptr; - std::size_t size = 0; - const bool queried = pthread_attr_getstack(&attr, &address, &size) == 0; - pthread_attr_destroy(&attr); - if (!queried) - return false; - *low = reinterpret_cast(address); - *high = *low + size; + static thread_local std::uintptr_t stackLow = 0; + static thread_local std::uintptr_t stackHigh = 0; + if (stackHigh == 0) { + pthread_attr_t attr; + if (pthread_getattr_np(pthread_self(), &attr) != 0) + return false; + void* address = nullptr; + std::size_t size = 0; + const bool queried = pthread_attr_getstack(&attr, &address, &size) == 0; + pthread_attr_destroy(&attr); + if (!queried || size == 0) + return false; + stackLow = reinterpret_cast(address); + stackHigh = stackLow + size; + } + *low = stackLow; + *high = stackHigh; #endif return *high > *low; } @@ -223,21 +251,67 @@ static void RegisterStack(PthreadPrivate* self) { static void RestoreStackPages(std::uintptr_t start, std::size_t bytes) noexcept { #ifdef _WIN32 DWORD previous = 0; - const bool restored = VirtualProtect(reinterpret_cast(start), bytes, PAGE_READWRITE, &previous) != 0; + const bool restored = VirtualAlloc(reinterpret_cast(start), bytes, MEM_COMMIT, PAGE_READWRITE) != nullptr && + VirtualProtect(reinterpret_cast(start), bytes, PAGE_READWRITE, &previous) != 0; #else const bool restored = ::mprotect(reinterpret_cast(start), bytes, PROT_READ | PROT_WRITE) == 0; #endif if (!restored) std::fprintf(stderr, "[thread] cannot restore the protection of guest stack pages 0x%llx+0x%zx at thread exit\n", static_cast(start), bytes); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(start, bytes); +} + +static void ReplaceStackProtection(std::map& ranges, std::uintptr_t start, std::size_t bytes, bool readWrite) { + const auto end = start + bytes; + auto it = ranges.upper_bound(start); + if (it != ranges.begin() && std::prev(it)->first + std::prev(it)->second > start) --it; + while (it != ranges.end() && it->first < end) { + const auto previousStart = it->first; + const auto previousEnd = previousStart + it->second; + it = ranges.erase(it); + if (previousStart < start) ranges.emplace(previousStart, start - previousStart); + if (previousEnd > end) { + ranges.emplace(end, previousEnd - end); + break; + } + } + if (!readWrite) ranges[start] = bytes; } void NoteGuestStackProtection(std::uintptr_t start, std::size_t bytes, bool readWrite) { std::lock_guard lock(stackLock); - auto it = protectedStackPages.upper_bound(start); - if (it != protectedStackPages.begin() && std::prev(it)->first + std::prev(it)->second > start) --it; - while (it != protectedStackPages.end() && it->first < start + bytes) it = protectedStackPages.erase(it); - if (!readWrite) protectedStackPages[start] = bytes; + auto replacement = protectedStackPages; + ReplaceStackProtection(replacement, start, bytes, readWrite); + protectedStackPages.swap(replacement); } +#ifdef _WIN32 +bool ReplaceGuestThreadStack(std::uintptr_t start, std::size_t bytes, bool readWrite, const std::function& replace) { + if (bytes == 0 || bytes > std::numeric_limits::max() - start) return false; + std::uintptr_t low = 0; + std::uintptr_t high = 0; + const bool current = CurrentStack(&low, &high) && start >= low && start + bytes <= high; + constexpr std::uintptr_t HostFrameReserve = 64 * 1024; + const auto frame = reinterpret_cast(&low); + if (current && (frame < HostFrameReserve || start + bytes > frame - HostFrameReserve)) return false; + std::lock_guard lock(stackLock); + if (!current && std::none_of(liveStacks.begin(), liveStacks.end(), [&](const auto& entry) { + return start >= entry.second.first && start + bytes <= entry.second.second; + })) return false; + auto replacement = protectedStackPages; + ReplaceStackProtection(replacement, start, bytes, readWrite); + auto failure = protectedStackPages; + ReplaceStackProtection(failure, start, bytes, false); + try { + if (!replace()) return false; + } catch (...) { + protectedStackPages.swap(failure); + throw; + } + protectedStackPages.swap(replacement); + return true; +} +#endif + static void UnregisterStack(PthreadPrivate* self) { std::lock_guard lock(stackLock); if (const auto found = liveStacks.find(self); found != liveStacks.end()) { @@ -303,15 +377,18 @@ 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; const KernelCpumask inherited = parent ? parent->affinity.load(std::memory_order_relaxed) : registry.processAffinity; affinity.store(attr && attr->_affinityExplicit ? attr->_affinity : inherited, std::memory_order_relaxed); constexpr int ExplicitSched = 0; + const bool explicitSched = attr && attr->_inheritsched == ExplicitSched; const int inheritedPolicy = parent ? parent->schedPolicy.load(std::memory_order_relaxed) : schedPolicy.load(std::memory_order_relaxed); - schedPolicy.store(attr && attr->_inheritsched == ExplicitSched ? attr->_schedpolicy : inheritedPolicy, std::memory_order_relaxed); + schedPolicy.store(explicitSched ? attr->_schedpolicy : inheritedPolicy, std::memory_order_relaxed); + const int attrPriority = attr ? attr->_schedpriority : DEFAULT_THREAD_PRIORITY; + priority.store(explicitSched || !parent ? attrPriority : parent->priority.load(std::memory_order_relaxed), std::memory_order_relaxed); if (registry.nextThread > static_cast(CPU_CLOCK_ID_MASK)) throw std::runtime_error("Guest thread CPU clock identifiers are exhausted"); cpuClockThread = registry.nextThread++; @@ -362,12 +439,37 @@ KernelCpumask LiveGuestThreadsAffinity() { return mask; } +std::unique_lock LockLiveGuestThread(const PthreadPrivate* thread) { + auto& registry = CpuClocks(); + std::unique_lock lock(registry.lock); + for (const auto& [id, live] : registry.threads) { + if (live == thread && !live->_finished.load(std::memory_order_acquire)) return lock; + } + return {}; +} + bool GuestThreadIsLive(const PthreadPrivate* thread) { + return LockLiveGuestThread(thread).owns_lock(); +} + +bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask) { auto& registry = CpuClocks(); std::lock_guard lock(registry.lock); - for (const auto& [id, live] : registry.threads) { - if (live != thread) continue; - return !live->_finished.load(std::memory_order_acquire); + 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; } @@ -431,7 +533,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; } @@ -520,6 +622,31 @@ void ThreadCancel::EndWait() { BeginWait(nullptr); } +extern "C" void* APS5_VABI Aps5CallGuestEntry_nid_no_patch(PthreadEntry entry, void* arg); + +asm(".text\n" + APS5_ASM_FUNCTION("Aps5CallGuestEntry_nid_no_patch") R"( + .cfi_startproc + push %rbp + .cfi_adjust_cfa_offset 8 + .cfi_rel_offset %rbp, 0 + push $0 + .cfi_adjust_cfa_offset 8 + push $0 + .cfi_adjust_cfa_offset 8 + mov %rsp, %rbp + mov %rdi, %rax + mov %rsi, %rdi + call *%rax + add $16, %rsp + .cfi_adjust_cfa_offset -16 + pop %rbp + .cfi_adjust_cfa_offset -8 + .cfi_restore %rbp + ret + .cfi_endproc +)" APS5_ASM_FUNCTION_END("Aps5CallGuestEntry_nid_no_patch")); + static void RunThread(std::unique_ptr args) { APS5_LOG_OUT("RunThread entry=0x%llx arg=%p self=%p", static_cast(reinterpret_cast(args->entry)), args->arg, static_cast(args->self)); const auto entry = args->entry; @@ -536,7 +663,7 @@ static void RunThread(std::unique_ptr args) { #ifndef _WIN32 pthread_sigmask(SIG_SETMASK, &signalMask, nullptr); #endif - finishThread(self, entry(arg)); + finishThread(self, Aps5CallGuestEntry_nid_no_patch(entry, arg)); CurrentGuestState().current = nullptr; } @@ -554,6 +681,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 @@ -603,12 +750,8 @@ static unsigned __stdcall StartNativeThread(void* opaque) { if (high <= low || high - low < self->stackSize) throw std::runtime_error("Cannot query guest thread stack"); self->stackAddress = reinterpret_cast(high - self->stackSize); - for (auto cursor = high - self->stackSize; cursor < high;) { - MEMORY_BASIC_INFORMATION memory{}; - if (VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) != sizeof(memory) || memory.State != MEM_COMMIT || memory.Protect != PAGE_READWRITE || memory.RegionSize == 0) - throw std::runtime_error("Guest thread stack is not fully committed"); - cursor = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; - } + if (!CommittedReadWrite(high - self->stackSize, high)) + throw std::runtime_error("Guest thread stack is not fully committed"); self->threadId = std::this_thread::get_id(); CurrentGuestState().current = self; if (!self->name.empty()) { @@ -681,9 +824,8 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread bool detached = false; if (attr && *attr) detached = ((*attr)->_detachstate == DETACH_DETACHED); p->_detached = detached; - p->stackSize = attr ? (*attr)->_stacksize : DEFAULT_STACK_SIZE; + p->stackSize = attr ? (*attr)->_stacksize : GetDefaultStackSize_nid_no_patch(); if (attr) { - p->priority.store((*attr)->_schedpriority, std::memory_order_relaxed); if ((*attr)->_inheritsched != GUEST_INHERIT_SCHED) p->scope = (*attr)->_scope; else if (CurrentGuestState().current) @@ -716,8 +858,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 @@ -772,8 +916,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 @@ -782,32 +928,49 @@ 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->_detached) return SCE_KERNEL_ERROR_EINVAL; + if (thread->_joining) return SCE_KERNEL_ERROR_EOPNOTSUPP; + thread->_joining = true; + while (!thread->_finished.load()) { + ThreadCancel::Wait(thread->_join_cv, lock, std::nullopt); + if (ThreadCancel::Requested()) { + thread->_joining = false; + lock.unlock(); + ThreadCancel::Check(); + lock.lock(); + thread->_joining = true; + } + } + thread->_joining = false; + 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; - thread->_detached = true; + { + std::lock_guard lock(thread->_join_mtx); + if (thread->_detached || thread->_joining || thread->_joined.load(std::memory_order_acquire)) return SCE_KERNEL_ERROR_EINVAL; + thread->_detached = true; + } #ifdef _WIN32 ReleaseThread(thread); #else @@ -869,7 +1032,28 @@ Pthread APS5_VABI scePthreadSelf() { } void APS5_VABI scePthreadYield() { +#ifdef _WIN32 + thread_local unsigned int spins = 0; + thread_local std::uint64_t lastTsc = 0; + const auto now = __rdtsc(); + if (now - lastTsc > 10000) { + spins = 0; + } + lastTsc = now; + if (SwitchToThread()) { + spins = 0; + return; + } + if (++spins < 512) { + _mm_pause(); + return; + } + spins = 0; + Sleep(1); + lastTsc = __rdtsc(); +#else std::this_thread::yield(); +#endif } int APS5_VABI scePthreadCancel(Pthread thread) { @@ -881,7 +1065,7 @@ int APS5_VABI scePthreadCancel(Pthread thread) { if (!thread->cancelWait) return SCE_OK; thread->cancelWait->NotifyAll(); } - std::this_thread::yield(); + scePthreadYield(); } } @@ -899,6 +1083,16 @@ void APS5_VABI __pthread_cleanup_pop_imp_nid_postfix(int execute) { if (!handler) return; CurrentGuestState().cleanupHandlers = handler->previous; if (execute) handler->routine(handler->argument); + if (handler->onHeap) delete handler; +} + +void APS5_VABI pthread_cleanup_push_nid_postfix(void (APS5_VABI* routine)(void*), void* argument) { + auto* handler = new CleanupHandler{CurrentGuestState().cleanupHandlers, routine, argument, 1}; + CurrentGuestState().cleanupHandlers = handler; +} + +void APS5_VABI pthread_cleanup_pop_nid_postfix(int execute) { + __pthread_cleanup_pop_imp_nid_postfix(execute); } int APS5_VABI scePthreadEqual(Pthread thread1, Pthread thread2) { diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index 7375ae7ff2..725f297e67 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp @@ -1,6 +1,8 @@ #include "prx/libkernel/Semaphore/include/Semaphore.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include +#include +#include #include #include @@ -10,6 +12,11 @@ KernelSemaPrivate::KernelSemaPrivate(std::int32_t initCount, std::int32_t maxCou : name(std::move(name)), tokenCount(initCount), initCount(initCount), maxCount(maxCount), isFifo(isFifo) { } +static bool TraceSync() { + static const bool enabled = std::getenv("APS5_TRACE_SYNC") != nullptr; + return enabled; +} + extern "C" { int APS5_VABI sceKernelCreateSema(KernelSema* sem, const char* name, uint32_t attr, int init, int max, void* opt) { @@ -19,6 +26,7 @@ int APS5_VABI sceKernelCreateSema(KernelSema* sem, const char* name, uint32_t at } *sem = new KernelSemaPrivate(init, max, std::string(name), attr == 1); + if (TraceSync()) std::fprintf(stderr, "[sema] create %p '%s' attr=0x%x init=%d max=%d\n", static_cast(*sem), name, attr, init, max); return KERNEL_SEMA_OK; } @@ -44,7 +52,9 @@ int APS5_VABI sceKernelSignalSema(KernelSema sem, int count) { if (count > sem->maxCount - sem->tokenCount) { return SCE_KERNEL_ERROR_EINVAL; } + const auto before = sem->tokenCount; sem->tokenCount += count; + if (TraceSync()) std::fprintf(stderr, "[sema] signal %p '%s' count=%d have=%d->%d\n", static_cast(sem), sem->name.c_str(), count, before, sem->tokenCount); sem->condition.NotifyAll(); return KERNEL_SEMA_OK; } @@ -71,20 +81,28 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) } waiterGuard{sem, generation}; const auto waitStart = std::chrono::steady_clock::now(); + const void* caller = __builtin_return_address(0); + if (TraceSync()) std::fprintf(stderr, "[sema] wait begin %p '%s' need=%d have=%d timeout=%lld caller=%p\n", static_cast(sem), sem->name.c_str(), need, sem->tokenCount, time ? static_cast(*time) : -1LL, caller); + const auto traceSyncWaitEnd = [&](int result) { + if (TraceSync()) std::fprintf(stderr, "[sema] wait end %p '%s' result=0x%x have=%d caller=%p\n", static_cast(sem), sem->name.c_str(), result, sem->tokenCount, caller); + }; const auto traceWait = [&](bool timedOut) { - KernelTraceWait_nid_postfix("sema", __builtin_return_address(0), static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart).count()), timedOut); + KernelTraceWait_nid_postfix("sema", caller, static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart).count()), timedOut); }; const auto released = [&] { return sem->tokenCount >= need || sem->deleted || sem->cancelGeneration != generation; }; if (time == nullptr) { sem->condition.Wait(lock, released); traceWait(false); if (sem->deleted) { + traceSyncWaitEnd(SCE_KERNEL_ERROR_EACCES); return SCE_KERNEL_ERROR_EACCES; } if (sem->cancelGeneration != generation) { + traceSyncWaitEnd(SCE_KERNEL_ERROR_ECANCELED); return SCE_KERNEL_ERROR_ECANCELED; } sem->tokenCount -= need; + traceSyncWaitEnd(KERNEL_SEMA_OK); return KERNEL_SEMA_OK; } @@ -93,15 +111,19 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) *time = static_cast(TimedWait::RemainingMicros(deadline)); traceWait(!acquired); if (sem->deleted) { + traceSyncWaitEnd(SCE_KERNEL_ERROR_EACCES); return SCE_KERNEL_ERROR_EACCES; } if (sem->cancelGeneration != generation) { + traceSyncWaitEnd(SCE_KERNEL_ERROR_ECANCELED); return SCE_KERNEL_ERROR_ECANCELED; } if (!acquired) { + traceSyncWaitEnd(SCE_KERNEL_ERROR_ETIMEDOUT); return SCE_KERNEL_ERROR_ETIMEDOUT; } sem->tokenCount -= need; + traceSyncWaitEnd(KERNEL_SEMA_OK); return KERNEL_SEMA_OK; } diff --git a/core/libs/prx/libkernel/Socket/include/SocketConnect.hpp b/core/libs/prx/libkernel/Socket/include/SocketConnect.hpp new file mode 100644 index 0000000000..51ebf73520 --- /dev/null +++ b/core/libs/prx/libkernel/Socket/include/SocketConnect.hpp @@ -0,0 +1,6 @@ +#pragma once +#ifdef _WIN32 +#include + +extern "C" void RefuseClosedLoopbackAtOnce_nid_no_patch(SOCKET socket, const sockaddr* address); +#endif diff --git a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp index d41d47b63c..56bbe00efe 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp @@ -5,9 +5,18 @@ namespace GuestSockets { struct SocketOptionState { int reportedError = 0; + bool nonblocking = false; + bool noSignal = false; + std::int64_t sendTimeout = 0; + std::int64_t receiveTimeout = 0; }; +bool NativeListening(std::uintptr_t native, bool& listening); int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int option, const void* value, std::uint32_t length); int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int type, int option, void* value, std::uint32_t* length); +int SetTcpLevelOption(std::uintptr_t native, bool tcp, int option, const void* value, std::uint32_t length); +int GetTcpLevelOption(std::uintptr_t native, bool tcp, int option, void* value, std::uint32_t* length); +int SetIpv6LevelOption(std::uintptr_t native, bool ipv6, int option, const void* value, std::uint32_t length); +int GetIpv6LevelOption(std::uintptr_t native, bool ipv6, int option, void* value, std::uint32_t* length); } diff --git a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp index 622a17bd20..97e68db173 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include namespace KernelSocketPoll { constexpr short Readable = 0x1; @@ -18,6 +19,12 @@ struct Entry { using Poller = int (*)(Entry* entries, int count, int timeoutMilliseconds); using Closer = bool (*)(int descriptor, int* error); using OptionSetter = bool (*)(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error); +using StatusControl = bool (*)(int descriptor, int command, int flags, int* result, int* error); +struct NativeOwner { + int (*transfer)(NativeOwner* owner, bool send, void* buffer, int length) = nullptr; +}; + +using NativeLookup = bool (*)(int descriptor, std::shared_ptr* owner, std::uintptr_t* native); } extern "C" void KernelSetSocketPoller_nid_no_patch(KernelSocketPoll::Poller poller); @@ -25,5 +32,10 @@ extern "C" void KernelSetSocketCloser_nid_no_patch(KernelSocketPoll::Closer clos extern "C" bool KernelCloseSocket_nid_no_patch(int descriptor, int* error); extern "C" void KernelSetSocketOptionSetter_nid_no_patch(KernelSocketPoll::OptionSetter setter); extern "C" bool KernelSetSocketOption_nid_no_patch(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error); +extern "C" void KernelSetSocketStatusControl_nid_no_patch(KernelSocketPoll::StatusControl control); +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..d59cfc4304 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -16,6 +16,9 @@ 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::intptr_t NativeDescriptor(int descriptor); std::int64_t Read(int descriptor, void* buffer, std::size_t length); std::int64_t Write(int descriptor, const void* buffer, std::size_t length); int Stat(int descriptor, FileStat* status); @@ -31,6 +34,8 @@ 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); +bool PipeWriteState(int descriptor, std::int64_t* data, bool* eof, bool* writable); std::int64_t Readv(int descriptor, const Iovec* iov, int iovcnt); std::int64_t Writev(int descriptor, const Iovec* iov, int iovcnt); } diff --git a/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp b/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp index 1ed1a2e20a..bbcb17e637 100644 --- a/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp +++ b/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp @@ -2,6 +2,7 @@ #include #include #include +#include extern "C" int* APS5_VABI __error_nid_postfix(); @@ -189,15 +190,17 @@ std::size_t V6ToText(const std::uint8_t* bytes, char* out) { extern "C" { int APS5_VABI __inet_pton_nid_postfix(int family, const char* text, void* destination) { if (family != kGuestInet && family != kGuestInet6) { *__error_nid_postfix() = 47; return -1; } - if (!text || !destination) { *__error_nid_postfix() = 14; return -1; } + if (!text) throw std::runtime_error("inet_pton: null text"); if (family == kGuestInet) { std::uint8_t quad[4]; if (!ParseQuad(text, text + std::strlen(text), quad)) return 0; + if (!destination) throw std::runtime_error("inet_pton: null destination"); std::memcpy(destination, quad, sizeof(quad)); return 1; } std::uint8_t address[16]; if (!TextToV6(text, address)) return 0; + if (!destination) throw std::runtime_error("inet_pton: null destination"); std::memcpy(destination, address, sizeof(address)); return 1; } @@ -205,13 +208,31 @@ int APS5_VABI __inet_pton_nid_postfix(int family, const char* text, void* destin const char* APS5_VABI __inet_ntop_nid_postfix(int family, const void* source, char* destination, std::uint32_t capacity) { if (family != kGuestInet && family != kGuestInet6) { *__error_nid_postfix() = 47; return nullptr; } - if (!source || !destination) { *__error_nid_postfix() = 14; return nullptr; } + if (!source) throw std::runtime_error("inet_ntop: null source"); char text[kMaxText + 1]; const auto* bytes = static_cast(source); const std::size_t length = family == kGuestInet ? FormatQuad(bytes, text) : V6ToText(bytes, text); text[length] = '\0'; if (length + 1 > capacity) { *__error_nid_postfix() = 28; return nullptr; } + if (!destination) throw std::runtime_error("inet_ntop: null destination"); std::memcpy(destination, text, length + 1); + if (family == kGuestInet6) std::memset(destination + length + 1, 0, capacity - length - 1); return destination; } + +std::uint32_t APS5_VABI htonl_nid_postfix(std::uint32_t value) { + return __builtin_bswap32(value); +} + +std::uint32_t APS5_VABI ntohl_nid_postfix(std::uint32_t value) { + return __builtin_bswap32(value); +} + +std::uint16_t APS5_VABI htons_nid_postfix(std::uint16_t value) { + return __builtin_bswap16(value); +} + +std::uint16_t APS5_VABI ntohs_nid_postfix(std::uint16_t value) { + return __builtin_bswap16(value); +} } diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index e01a8b7f34..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 @@ -35,16 +38,13 @@ struct GuestTimeval { std::atomic g_socketPoller{nullptr}; std::atomic g_socketCloser{nullptr}; std::atomic g_socketOptionSetter{nullptr}; +std::atomic g_socketStatusControl{nullptr}; +std::atomic g_socketNativeLookup{nullptr}; 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)); } @@ -59,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) { @@ -102,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))); } } @@ -153,6 +166,24 @@ bool KernelSetSocketOption_nid_no_patch(int descriptor, int level, int option, c return setter != nullptr && setter(descriptor, level, option, value, length, error); } +void KernelSetSocketStatusControl_nid_no_patch(KernelSocketPoll::StatusControl control) { + g_socketStatusControl.store(control); +} + +bool KernelControlSocketStatus_nid_no_patch(int descriptor, int command, int flags, int* result, int* error) { + const auto control = g_socketStatusControl.load(); + return control != nullptr && control(descriptor, command, flags, result, error); +} + +void KernelSetSocketNativeLookup_nid_no_patch(KernelSocketPoll::NativeLookup lookup) { + g_socketNativeLookup.store(lookup); +} + +bool KernelFindSocketNative_nid_no_patch(int descriptor, std::shared_ptr* owner, std::uintptr_t* native) { + const auto lookup = g_socketNativeLookup.load(); + return lookup != nullptr && lookup(descriptor, owner, native); +} + int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout) { const auto* limit = static_cast(timeout); if (nfds < 0 || nfds > SelectDescriptorLimit || (limit != nullptr && (limit->seconds < 0 || limit->microseconds < 0 || limit->microseconds >= 1000000))) { @@ -160,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; @@ -173,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()); @@ -185,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(); @@ -208,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() @@ -241,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/SocketOptions.cpp b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp index 747bf83a97..37fce2af2e 100644 --- a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp +++ b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp @@ -4,6 +4,13 @@ #include #else #include +#include +#include +#include +#ifdef __APPLE__ +#include +#include +#endif #endif #include "prx/libkernel/Socket/include/SocketError.hpp" #include "prx/libkernel/Socket/include/SocketOptions.hpp" @@ -20,21 +27,43 @@ constexpr int GuestDontRoute = 0x10; constexpr int GuestBroadcast = 0x20; constexpr int GuestLinger = 0x80; constexpr int GuestOobInline = 0x100; +constexpr int GuestNoSigpipe = 0x800; constexpr int GuestSendBuffer = 0x1001; constexpr int GuestReceiveBuffer = 0x1002; +constexpr int GuestSendTimeout = 0x1005; +constexpr int GuestReceiveTimeout = 0x1006; constexpr int GuestError = 0x1007; constexpr int GuestType = 0x1008; +constexpr int GuestNonBlocking = 0x1200; +constexpr int GuestTcpNoDelay = 1; +constexpr int GuestTcpNoDelayFlag = 4; +constexpr int GuestIpv6Only = 27; constexpr int ErrorInvalid = 22; constexpr int ErrorDomain = 33; constexpr int ErrorNoProtocolOption = 42; constexpr int MaximumLinger = 65535; +constexpr std::int64_t LongestTimeout = 0x7fffffffffffffffLL; struct GuestLingerValue { std::int32_t enabled; std::int32_t seconds; }; +struct GuestTimeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +std::int64_t TimeoutOf(const GuestTimeval& time) { + if (time.seconds > INT32_MAX) return LongestTimeout; + return (time.seconds << 32) + static_cast((static_cast(time.microseconds) * ((std::uint64_t{1} << 63) / 500000)) >> 32); +} + +GuestTimeval TimevalOf(std::int64_t timeout) { + return {timeout >> 32, (1000000 * static_cast(static_cast(timeout))) >> 32}; +} + #ifdef _WIN32 using NativeHandle = SOCKET; using NativeOptionValue = const char*; @@ -88,6 +117,34 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int op if (::setsockopt(handle, SOL_SOCKET, SO_LINGER, reinterpret_cast(&request), sizeof(request))) return HostError(); return 0; } + if (option == GuestSendTimeout || option == GuestReceiveTimeout) { + if (length < sizeof(GuestTimeval)) return ErrorInvalid; + if (!value) return ErrorInvalid; + GuestTimeval time; + std::memcpy(&time, value, sizeof(time)); + if (time.seconds < 0 || time.microseconds < 0 || time.microseconds >= 1000000) return ErrorDomain; + (option == GuestSendTimeout ? state.sendTimeout : state.receiveTimeout) = TimeoutOf(time); + return 0; + } + if (option == GuestNoSigpipe || option == GuestNonBlocking) { + if (length < sizeof(int)) return ErrorInvalid; + if (!value) return ErrorInvalid; + int number; + std::memcpy(&number, value, sizeof(number)); + if (option == GuestNoSigpipe) { + state.noSignal = number != 0; + return 0; + } +#ifdef _WIN32 + unsigned long enabled = number != 0; + if (ioctlsocket(handle, FIONBIO, &enabled)) return HostError(); +#else + int enabled = number != 0; + if (::ioctl(handle, FIONBIO, &enabled)) return HostError(); +#endif + state.nonblocking = enabled != 0; + return 0; + } const bool native_boolean = NativeBoolean(option) != -1; if (native_boolean || option == GuestSendBuffer || option == GuestReceiveBuffer) { if (length < sizeof(int)) return ErrorInvalid; @@ -107,6 +164,20 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int op return ErrorNoProtocolOption; } +bool NativeListening(std::uintptr_t native, bool& listening) { +#ifdef __APPLE__ + socket_fdinfo info{}; + if (proc_pidfdinfo(getpid(), static_cast(native), PROC_PIDFDSOCKETINFO, &info, sizeof(info)) != static_cast(sizeof(info))) return false; + listening = (info.psi.soi_options & SO_ACCEPTCONN) != 0; +#else + int accepting = 0; + socklen_t size = sizeof(accepting); + if (::getsockopt(static_cast(native), SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&accepting), &size)) return false; + listening = accepting != 0; +#endif + return true; +} + int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int type, int option, void* value, std::uint32_t* length) { const auto handle = static_cast(native); if (option == GuestLinger) { @@ -116,6 +187,10 @@ int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int ty const GuestLingerValue guest{current.l_onoff != 0 ? GuestLinger : 0, static_cast(current.l_linger)}; return Copy(value, length, &guest, sizeof(guest)); } + if (option == GuestSendTimeout || option == GuestReceiveTimeout) { + const GuestTimeval time = TimevalOf(option == GuestSendTimeout ? state.sendTimeout : state.receiveTimeout); + return Copy(value, length, &time, sizeof(time)); + } int number = 0; if (const int nativeOption = NativeBoolean(option); nativeOption != -1) { int enabled = 0; @@ -123,12 +198,15 @@ int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int ty if (::getsockopt(handle, SOL_SOCKET, nativeOption, reinterpret_cast(&enabled), &size)) return HostError(); number = enabled != 0 ? option : 0; } else if (option == GuestAcceptConnection) { - int listening = 0; - socklen_t size = sizeof(listening); - if (::getsockopt(handle, SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&listening), &size)) return HostError(); - number = listening != 0 ? GuestAcceptConnection : 0; + bool listening = false; + if (!NativeListening(native, listening)) return HostError(); + number = listening ? GuestAcceptConnection : 0; } else if (option == GuestType) { number = type; + } else if (option == GuestNoSigpipe) { + number = state.noSignal ? GuestNoSigpipe : 0; + } else if (option == GuestNonBlocking) { + number = state.nonblocking ? 1 : 0; } else if (option == GuestError) { int pending = 0; socklen_t size = sizeof(pending); @@ -147,4 +225,48 @@ int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int ty return Copy(value, length, &number, sizeof(number)); } +int SetTcpLevelOption(std::uintptr_t native, bool tcp, int option, const void* value, std::uint32_t length) { + if (!tcp) return ErrorInvalid; + if (option != GuestTcpNoDelay) return ErrorNoProtocolOption; + if (length < sizeof(int)) return ErrorInvalid; + if (!value) return ErrorInvalid; + int number; + std::memcpy(&number, value, sizeof(number)); + const int enabled = number != 0 ? 1 : 0; + if (::setsockopt(static_cast(native), IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&enabled), sizeof(enabled))) return HostError(); + return 0; +} + +int GetTcpLevelOption(std::uintptr_t native, bool tcp, int option, void* value, std::uint32_t* length) { + if (!tcp) return ErrorInvalid; + if (option != GuestTcpNoDelay) return ErrorNoProtocolOption; + int enabled = 0; + socklen_t size = sizeof(enabled); + if (::getsockopt(static_cast(native), IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&enabled), &size)) return HostError(); + const int number = enabled != 0 ? GuestTcpNoDelayFlag : 0; + return Copy(value, length, &number, sizeof(number)); +} + +int SetIpv6LevelOption(std::uintptr_t native, bool ipv6, int option, const void* value, std::uint32_t length) { + if (!ipv6) return ErrorInvalid; + if (option != GuestIpv6Only) return ErrorNoProtocolOption; + if (length != sizeof(int)) return ErrorInvalid; + if (!value) return ErrorInvalid; + int number; + std::memcpy(&number, value, sizeof(number)); + const int enabled = number != 0 ? 1 : 0; + if (::setsockopt(static_cast(native), IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&enabled), sizeof(enabled))) return HostError(); + return 0; +} + +int GetIpv6LevelOption(std::uintptr_t native, bool ipv6, int option, void* value, std::uint32_t* length) { + if (!ipv6) return ErrorInvalid; + if (option != GuestIpv6Only) return ErrorNoProtocolOption; + int enabled = 0; + socklen_t size = sizeof(enabled); + if (::getsockopt(static_cast(native), IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&enabled), &size)) return HostError(); + const int number = enabled != 0 ? 1 : 0; + return Copy(value, length, &number, sizeof(number)); +} + } diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index e33cc71239..22a664494c 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -7,15 +7,26 @@ #else #include #include +#ifdef __linux__ +#include +#endif #include #include #include #include +#include #include #include #include +#ifdef __linux__ +#include +#elif defined(__APPLE__) +#include +#include +#endif #endif #include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/PreciseWait.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -26,8 +37,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 @@ -35,16 +48,17 @@ #include #include #include +#include #include #include #include #include #include #include -#include #include extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI dup2_nid_postfix(int from, int to); namespace { #ifdef _WIN32 using NativeSocket = SOCKET; @@ -54,13 +68,27 @@ using NativeSocket = int; constexpr auto Invalid = -1; #endif int Fail(int error) { *__error_nid_postfix() = error; return -1; } +constexpr int GuestSocketDescriptorLimit = 1024; +constexpr int GuestPeek = 0x2; +constexpr int GuestWaitAll = 0x40; +constexpr int GuestDontWait = 0x80; constexpr int GuestNoSignal = 0x20000; +constexpr int GuestSendFlags = GuestNoSignal | GuestDontWait; +constexpr int GuestReceiveFlags = GuestPeek | GuestWaitAll | GuestDontWait; int NativeSendFlags(int flags) { #ifdef _WIN32 static_cast(flags); return 0; #else - return flags & GuestNoSignal ? MSG_NOSIGNAL : 0; + return (flags & GuestNoSignal ? MSG_NOSIGNAL : 0) | (flags & GuestDontWait ? MSG_DONTWAIT : 0); +#endif +} +int NativeReceiveFlags(int flags, int type) { + const int waitAll = type == 1 && (flags & GuestWaitAll) != 0 ? MSG_WAITALL : 0; +#ifdef _WIN32 + return (flags & GuestPeek ? MSG_PEEK : 0) | waitAll; +#else + return (flags & GuestPeek ? MSG_PEEK : 0) | (flags & GuestDontWait ? MSG_DONTWAIT : 0) | waitAll; #endif } int NativeError() { @@ -78,18 +106,19 @@ int ConnectError() { } int SendError(int type) { #ifdef _WIN32 - if (type == 2 && WSAGetLastError() == WSAENOTCONN) return 39; + const int error = WSAGetLastError(); + if (type == 2 && error == WSAENOTCONN) return 39; + if (type == 1 && (error == WSAECONNABORTED || error == WSAECONNRESET)) return 32; #else static_cast(type); #endif return NativeError(); } -struct Socket { +struct Socket : KernelSocketPoll::NativeOwner { NativeSocket value; int family; int type; std::mutex modeMutex; - bool nonblocking = false; GuestSockets::SocketOptionState options; bool receiveShut = false; bool sendShut = false; @@ -106,6 +135,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; @@ -142,6 +175,7 @@ Waker& ThreadWaker() { } std::mutex socketsMutex; std::map> sockets; +std::set closeOnExecDescriptors; #ifdef _WIN32 int ReserveNumber(NativeSocket) { const HANDLE placeholder = ::CreateFileW(L"NUL", 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); @@ -152,13 +186,35 @@ int ReserveNumber(NativeSocket) { ::CloseHandle(placeholder); return Fail(24); } + if (number >= GuestSocketDescriptorLimit) { + ::_close(number); + return Fail(24); + } return number; } -int CopyNumber(int number) { - const int copy = ::_dup(number); - return copy < 0 ? Fail(errno == EMFILE ? 24 : 9) : copy; +int CopyNumber(int number, int minimum = 0) { + if (minimum >= GuestSocketDescriptorLimit) return Fail(24); + std::vector below; + int copy = -1; + int error = 0; + try { + while ((copy = ::_dup(number)) >= 0 && copy < minimum) below.push_back(copy); + if (copy < 0) error = errno == EMFILE ? 24 : 9; + } catch (const std::bad_alloc&) { + if (copy >= 0) ::_close(copy); + copy = -1; + error = 12; + } + for (const int temporary : below) ::_close(temporary); + if (copy < 0) return Fail(error); + if (copy >= GuestSocketDescriptorLimit) { + ::_close(copy); + return Fail(24); + } + return copy; } int CopyNumberTo(int number, int target) { + if (target >= GuestSocketDescriptorLimit) return Fail(24); return ::_dup2(number, target) != 0 ? Fail(errno == EMFILE ? 24 : 9) : 0; } void ReleaseNumber(int number) { @@ -167,13 +223,25 @@ void ReleaseNumber(int number) { #else int ReserveNumber(NativeSocket native) { const int number = ::fcntl(native, F_DUPFD_CLOEXEC, 0); - return number < 0 ? Fail(NativeError()) : number; + if (number < 0) return Fail(NativeError()); + if (number >= GuestSocketDescriptorLimit) { + ::close(number); + return Fail(24); + } + return number; } -int CopyNumber(int number) { - const int copy = ::fcntl(number, F_DUPFD_CLOEXEC, 0); - return copy < 0 ? Fail(NativeError()) : copy; +int CopyNumber(int number, int minimum = 0) { + if (minimum >= GuestSocketDescriptorLimit) return Fail(24); + const int copy = ::fcntl(number, F_DUPFD_CLOEXEC, minimum); + if (copy < 0) return Fail(NativeError()); + if (copy >= GuestSocketDescriptorLimit) { + ::close(copy); + return Fail(24); + } + return copy; } int CopyNumberTo(int number, int target) { + if (target >= GuestSocketDescriptorLimit) return Fail(24); return ::dup2(number, target) < 0 ? Fail(NativeError()) : 0; } void ReleaseNumber(int number) { @@ -196,12 +264,18 @@ std::shared_ptr Lookup(int descriptor) { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); if (found != sockets.end()) return found->second; - Fail(9); + Fail(descriptor >= 0 && DescriptorIsOpen_nid_no_patch(descriptor) ? 38 : 9); return {}; } +int AddressLengthError(const void* input, std::uint32_t length) { + if (length > 255) return 63; + if (length < 2) return 22; + return input ? 0 : 14; +} bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, socklen_t& size) { - if (!input || length < 2) { Fail(14); return false; } + if (const int error = AddressLengthError(input, length)) { Fail(error); return false; } const auto* bytes = static_cast(input); + if ((bytes[1] == 2 && length < 16) || (bytes[1] == 28 && length < 28)) { Fail(22); return false; } if (bytes[1] == 2 && length >= 16) { auto& v4 = reinterpret_cast(native); v4.sin_family = AF_INET; @@ -226,6 +300,35 @@ 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) { + bool listening = false; + return GuestSockets::NativeListening(static_cast(socket.value), listening) && listening; +} +bool ZeroPortConnect(const Socket& socket, const void* address, std::uint32_t length) { + if (!address || (socket.type != 1 && socket.type != 2) || (socket.family != 2 && socket.family != 28)) return false; + const auto* bytes = static_cast(address); + if (length < (socket.family == 2 ? 16u : 28u) || bytes[1] != socket.family || (bytes[2] | bytes[3]) != 0) return false; + sockaddr_storage peer{}; + socklen_t peerSize = sizeof(peer); + if (::getpeername(socket.value, reinterpret_cast(&peer), &peerSize) == 0) return false; + Fail(49); + return true; +} +constexpr std::int64_t NotAnswered = -2; +std::int64_t ZeroPortSend(const Socket& socket, const sockaddr_storage& destination, std::uint64_t length) { + if (socket.type != 2 || socket.family != 2 || destination.ss_family != AF_INET) return NotAnswered; + const auto& remote = reinterpret_cast(destination); + if (remote.sin_port != 0) return NotAnswered; + sockaddr_storage peer{}; + socklen_t peerSize = sizeof(peer); + if (::getpeername(socket.value, reinterpret_cast(&peer), &peerSize) == 0) return NotAnswered; + sockaddr_in local{}; + socklen_t localSize = sizeof(local); + const bool bound = ::getsockname(socket.value, reinterpret_cast(&local), &localSize) == 0 + && local.sin_port != 0 && local.sin_addr.s_addr != htonl(INADDR_ANY); + if (bound && remote.sin_addr.s_addr != htonl(INADDR_ANY) && remote.sin_addr.s_addr != htonl(INADDR_BROADCAST)) return static_cast(length); + return Fail(49); +} int Disconnect(const Socket& socket) { sockaddr_storage peer{}; socklen_t peerSize = sizeof(peer); @@ -281,6 +384,7 @@ int GuestSockets::Close(int descriptor) { if (found == sockets.end()) return Fail(9); released = std::move(found->second); sockets.erase(found); + closeOnExecDescriptors.erase(descriptor); } EqueueDescriptorClosed(descriptor); ReleaseNumber(descriptor); @@ -295,6 +399,7 @@ void GuestSockets::Forget(int descriptor) { if (found == sockets.end()) return; released = std::move(found->second); sockets.erase(found); + closeOnExecDescriptors.erase(descriptor); } EqueueDescriptorClosed(descriptor); } @@ -316,6 +421,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; @@ -325,7 +431,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, interest.write ? WriteMask : ReadMask, 0}); +#endif + continue; + } held.push_back(found->second); entries.push_back({found->second->value, interest.write ? WriteMask : ReadMask, 0}); } @@ -343,8 +456,13 @@ 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())); +#elif defined(__APPLE__) + const int timeout = deadlineNanos != 0 ? static_cast(std::min((remaining + 999999) / 1000000, INT_MAX)) : -1; + if (::poll(entries.data(), entries.size(), timeout) < 0 && errno != EINTR) + throw std::runtime_error("equeue wait: poll failed, guest errno " + std::to_string(NativeError())); #else timespec timeout{static_cast(remaining / 1000000000), static_cast(remaining % 1000000000)}; if (::ppoll(entries.data(), entries.size(), deadlineNanos != 0 ? &timeout : nullptr, nullptr) < 0 && errno != EINTR) @@ -355,6 +473,12 @@ bool GuestSockets::WaitAny(const std::vector& interests, std::uint64_t return true; } +std::intptr_t GuestSockets::NativeDescriptor(int descriptor) { + std::lock_guard lock(socketsMutex); + const auto found = sockets.find(descriptor); + return found == sockets.end() ? -1 : static_cast(found->second->value); +} + bool GuestSockets::IsOpen(int descriptor) { std::lock_guard lock(socketsMutex); return sockets.contains(descriptor); @@ -364,6 +488,21 @@ 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; + } + return Listening(*socket); +} extern "C" bool GuestSocketIsOpen_nid_no_patch(int descriptor) { return GuestSockets::IsOpen(descriptor); @@ -413,8 +552,19 @@ 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; +#ifdef _WIN32 +short ClosedStreamEvents(int descriptor, short revents, short requested) { + if ((revents & (POLLHUP | POLLERR)) != POLLHUP) return revents; + const auto socket = Find(descriptor); + if (!socket || socket->type != 1) return revents; + revents = static_cast(revents | (requested & POLLRDNORM)); + std::lock_guard lock(socket->modeMutex); + return socket->sendShut ? revents : static_cast(revents & ~POLLHUP); +} +#endif [[noreturn]] void ThrowReadEndWritable(int descriptor) { throw std::runtime_error("poll: POLLOUT on the read end of pipe descriptor " + std::to_string(descriptor) + " is not supported"); } @@ -440,18 +590,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; @@ -480,20 +634,30 @@ bool GuestSockets::PipeReady(int descriptor, short events, short& revents) { } #endif -int GuestSockets::Duplicate(int descriptor) { +namespace { +int DuplicateSocket_nid_no_patch(int descriptor, int minimum, bool closeOnExec) { + if (minimum < 0) return Fail(22); std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); if (found == sockets.end()) return Fail(9); - const int duplicate = CopyNumber(descriptor); + const int duplicate = CopyNumber(descriptor, minimum); if (duplicate < 0) return -1; try { sockets.insert_or_assign(duplicate, found->second); + if (closeOnExec) closeOnExecDescriptors.insert(duplicate); + else closeOnExecDescriptors.erase(duplicate); } catch (const std::bad_alloc&) { + sockets.erase(duplicate); ReleaseNumber(duplicate); return Fail(12); } return duplicate; } +} + +int GuestSockets::Duplicate(int descriptor) { + return DuplicateSocket_nid_no_patch(descriptor, 0, false); +} int GuestSockets::DuplicateTo(int descriptor, int target) { std::shared_ptr replaced; @@ -517,6 +681,10 @@ int GuestSockets::DuplicateTo(int descriptor, int target) { } } if (replaced) EqueueDescriptorClosed(target); + { + std::lock_guard lock(socketsMutex); + closeOnExecDescriptors.erase(target); + } return target; } @@ -532,9 +700,62 @@ int SetNonBlocking(Socket& socket) { int value = 1; if (::ioctl(socket.value, FIONBIO, &value)) return Fail(NativeError()); #endif - socket.nonblocking = true; + socket.options.nonblocking = true; return 0; } +int ReceiveAnsweredHere(Socket& socket, std::uint64_t length) { +#ifdef _WIN32 + { + std::lock_guard lock(socket.modeMutex); + if (socket.receiveShut) return 0; + } +#endif + if (length != 0 || socket.type != 1) return -1; + sockaddr_storage peer{}; + socklen_t size = sizeof(peer); + return ::getpeername(socket.value, reinterpret_cast(&peer), &size) == 0 ? 0 : NativeError(); +} +int SocketSendFlags(Socket& socket, int flags) { + std::lock_guard lock(socket.modeMutex); + return NativeSendFlags(socket.options.noSignal ? flags | GuestNoSignal : flags); +} +int WaitForTransfer(Socket& socket, bool send, int flags) { + if ((flags & GuestDontWait) != 0) { +#ifdef _WIN32 + NativePollDescriptor entry{}; + entry.fd = socket.value; + entry.events = static_cast(send ? POLLWRNORM : POLLRDNORM); + return WSAPoll(&entry, 1, 0) == 0 ? 35 : 0; +#else + return 0; +#endif + } + std::int64_t timeout; + { + std::lock_guard lock(socket.modeMutex); + if (socket.options.nonblocking) return 0; + timeout = send ? socket.options.sendTimeout : socket.options.receiveTimeout; + } + if (timeout == 0) return 0; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout >> 32) + + std::chrono::nanoseconds((static_cast(static_cast(timeout)) * 1000000000) >> 32); + for (;;) { + const auto left = deadline - std::chrono::steady_clock::now(); + if (left <= std::chrono::steady_clock::duration::zero()) return 35; + const int milliseconds = static_cast(std::min(std::chrono::ceil(left).count(), INT_MAX)); + NativePollDescriptor entry{}; + entry.fd = socket.value; +#ifdef _WIN32 + entry.events = static_cast(send ? POLLWRNORM : POLLRDNORM); + const int ready = WSAPoll(&entry, 1, milliseconds); + if (ready != 0) return 0; +#else + entry.events = static_cast(send ? POLLOUT : POLLIN); + const int ready = ::poll(&entry, 1, milliseconds); + if (ready > 0 || (ready < 0 && errno != EINTR)) return 0; +#endif + } +} #ifdef _WIN32 bool LoopbackName(NativeSocket socket, sockaddr_in& name) { int size = sizeof(name); @@ -589,6 +810,9 @@ int NativePair(int type, NativeSocket (&pair)[2]) { } #else int NativePair(int type, NativeSocket (&pair)[2]) { +#ifdef __APPLE__ + if (type == 5) throw std::runtime_error("socketpair: SOCK_SEQPACKET is not supported on macOS"); +#endif const int nativeType = type == 1 ? SOCK_STREAM : type == 2 ? SOCK_DGRAM : SOCK_SEQPACKET; int native[2]; if (::socketpair(AF_UNIX, nativeType, 0, native)) return NativeError(); @@ -596,6 +820,15 @@ int NativePair(int type, NativeSocket (&pair)[2]) { pair[1] = native[1]; return 0; } +int QueuedBytes(const Socket& socket, int& count) { +#ifdef __APPLE__ + if (socket.type == 2) { + socklen_t size = sizeof(count); + return ::getsockopt(socket.value, SOL_SOCKET, SO_NREAD, &count, &size); + } +#endif + return ::ioctl(socket.value, FIONREAD, &count); +} #endif int UnnamedAddress(void* address, std::uint32_t* length) { const unsigned char bytes[16] = {16, GuestUnix}; @@ -626,7 +859,7 @@ int GuestSockets::Stat(int descriptor, FileStat* status) { if (ioctlsocket(socket->value, FIONREAD, &count) == 0) available = static_cast(count); #else int count = 0; - if (::ioctl(socket->value, FIONREAD, &count) == 0) available = count; + if (QueuedBytes(*socket, count) == 0) available = count; #endif int sendBuffer = 0; socklen_t length = sizeof(sendBuffer); @@ -640,17 +873,42 @@ int GuestSockets::Stat(int descriptor, FileStat* status) { return 0; } +namespace { +std::int64_t ListenQueueLength(NativeSocket socket) { +#ifdef __linux__ + tcp_info info{}; + socklen_t length = sizeof(info); + if (getsockopt(socket, IPPROTO_TCP, TCP_INFO, &info, &length) == 0 && length >= offsetof(tcp_info, tcpi_unacked) + sizeof(info.tcpi_unacked)) + return info.tcpi_unacked; +#else + (void)socket; +#endif + return 1; +} +} + bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* eof) { std::shared_ptr socket; { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); - if (found == sockets.end()) return false; + if (found == sockets.end() && !write) return PipeReadState(descriptor, data, eof) && (*data > 0 || *eof); + if (found == sockets.end()) { + bool writable = false; + return PipeWriteState(descriptor, data, eof, &writable) && (writable || *eof); + } socket = found->second; } #ifdef _WIN32 WSAPOLLFD entry{socket->value, static_cast(write ? POLLWRNORM : POLLRDNORM), 0}; if (WSAPoll(&entry, 1, 0) <= 0) return false; +#elif defined(__APPLE__) + pollfd entry{socket->value, static_cast(write ? POLLOUT : POLLIN), 0}; + if (::poll(&entry, 1, 0) <= 0) return false; + if (!write && socket->type == 1 && (entry.revents & POLLIN)) { + char byte; + if (::recv(socket->value, &byte, 1, MSG_PEEK | MSG_DONTWAIT) == 0) entry.revents |= POLLHUP; + } #else pollfd entry{socket->value, static_cast(write ? POLLOUT : (POLLIN | POLLRDHUP)), 0}; if (::poll(&entry, 1, 0) <= 0) return false; @@ -662,18 +920,68 @@ 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 if (Listening(*socket)) { + *data = ListenQueueLength(socket->value); } else { #ifdef _WIN32 unsigned long available = 0; if (ioctlsocket(socket->value, FIONREAD, &available) == 0) *data = static_cast(available); #else int available = 0; - if (::ioctl(socket->value, FIONREAD, &available) == 0) *data = available; + if (QueuedBytes(*socket, available) == 0) *data = available; #endif } 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; +} + +bool GuestSockets::PipeWriteState(int descriptor, std::int64_t* data, bool* eof, bool* writable) { +#ifdef _WIN32 + ACCESS_MASK access = 0; + PipeLocalInformation pipe{}; + if (!QueryPipe(descriptor, "kevent", access, pipe) || !(access & FILE_WRITE_DATA)) return false; + *eof = pipe.state == PipeClosingState; + *writable = pipe.writeQuotaAvailable >= PipeAtomicBytes; + *data = *eof ? 0 : pipe.writeQuotaAvailable; +#elif defined(__linux__) + if (!IsPipe(descriptor, 0) || (::fcntl(descriptor, F_GETFL) & O_ACCMODE) == O_RDONLY) return false; + pollfd entry{descriptor, POLLOUT, 0}; + int queued = 0; + const int capacity = ::fcntl(descriptor, F_GETPIPE_SZ); + if (::poll(&entry, 1, 0) < 0 || ::ioctl(descriptor, FIONREAD, &queued) != 0 || capacity < 0) + throw std::runtime_error("kevent: querying pipe descriptor " + std::to_string(descriptor) + " failed"); + *eof = (entry.revents & POLLERR) != 0; + *writable = (entry.revents & POLLOUT) != 0; + *data = *eof ? 0 : std::max(0, capacity - queued); +#else + if (!IsPipe(descriptor, 0) || (::fcntl(descriptor, F_GETFL) & O_ACCMODE) == O_RDONLY) return false; + NotImplemented_nid_no_patch("kevent: EVFILT_WRITE on a pipe on macOS"); +#endif + return true; +} + int GuestSockets::Poll(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds) { std::vector> held; std::vector native; @@ -719,44 +1027,65 @@ 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::static_pointer_cast(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)) { - const auto type = File::FcntlArgumentOf(command); - std::intptr_t argument = 0; - if (type != File::FcntlArgument::None) { + const auto type = File::FcntlArgumentOf(command); + std::intptr_t argument = 0; + if (type != File::FcntlArgument::None) { #ifdef _WIN32 - __builtin_sysv_va_list arguments; - __builtin_sysv_va_start(arguments, command); - argument = type == File::FcntlArgument::Int ? __builtin_va_arg(arguments, int) - : reinterpret_cast(__builtin_va_arg(arguments, void*)); - __builtin_sysv_va_end(arguments); + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? __builtin_va_arg(arguments, int) + : reinterpret_cast(__builtin_va_arg(arguments, void*)); + __builtin_sysv_va_end(arguments); #else - std::va_list arguments; - va_start(arguments, command); - argument = type == File::FcntlArgument::Int ? va_arg(arguments, int) - : reinterpret_cast(va_arg(arguments, void*)); - va_end(arguments); + std::va_list arguments; + va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? va_arg(arguments, int) + : reinterpret_cast(va_arg(arguments, void*)); + va_end(arguments); #endif + } + if (!GuestSockets::IsOpen(descriptor)) { + int result = 0; + int netError = 0; + if (KernelControlSocketStatus_nid_no_patch(descriptor, command, static_cast(argument), &result, &netError)) { + return netError == 0 ? result : Fail(netError); } return File::Fcntl(descriptor, command, argument); } + if (command == 0 || command == 17) return DuplicateSocket_nid_no_patch(descriptor, static_cast(argument), command == 17); + if (command == 10 || command == 18) { + const int duplicate = dup2_nid_postfix(descriptor, static_cast(argument)); + if (duplicate >= 0 && command == 18) fcntl_nid_postfix(duplicate, 2, 1); + return duplicate; + } const auto socket = Lookup(descriptor); if (!socket) return -1; std::lock_guard lock(socket->modeMutex); - if (command == 3) return 2 | (socket->nonblocking ? 4 : 0); // F_GETFL, O_RDWR + if (command == 1) { + std::lock_guard registry(socketsMutex); + return closeOnExecDescriptors.count(descriptor) != 0 ? 1 : 0; + } + if (command == 2) { + const int flags = static_cast(argument); + std::lock_guard registry(socketsMutex); + if ((flags & 1) != 0) closeOnExecDescriptors.insert(descriptor); + else closeOnExecDescriptors.erase(descriptor); + return 0; + } + if (command == 3) return 2 | (socket->options.nonblocking ? 4 : 0); // F_GETFL, O_RDWR if (command != 4) return Fail(22); -#ifdef _WIN32 - __builtin_sysv_va_list arguments; - __builtin_sysv_va_start(arguments, command); - const int flags = __builtin_va_arg(arguments, int); - __builtin_sysv_va_end(arguments); -#else - std::va_list arguments; - va_start(arguments, command); - const int flags = va_arg(arguments, int); - va_end(arguments); -#endif + const int flags = static_cast(argument); if ((flags & ~7) != 0) return Fail(45); #ifdef _WIN32 unsigned long enabled = (flags & 4) != 0; @@ -765,9 +1094,23 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { int enabled = (flags & 4) != 0; if (::ioctl(socket->value, FIONBIO, &enabled)) return Fail(NativeError()); #endif - socket->nonblocking = enabled != 0; + socket->options.nonblocking = enabled != 0; return 0; } +int APS5_VABI _fcntl_nid_postfix(int descriptor, int command, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + const auto argument = __builtin_va_arg(arguments, std::uint64_t); + __builtin_sysv_va_end(arguments); +#else + std::va_list arguments; + va_start(arguments, command); + const auto argument = va_arg(arguments, std::uint64_t); + va_end(arguments); +#endif + return fcntl_nid_postfix(descriptor, command, argument); +} int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, const void* value, std::uint32_t length) { int netError = 0; @@ -776,19 +1119,25 @@ int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, } const auto socket = Lookup(descriptor); if (!socket) return -1; - if (level != 0xffff) return Fail(42); + if (level != 0xffff && level != 6 && level != 41) return Fail(42); std::lock_guard lock(socket->modeMutex); - const int error = GuestSockets::SetSocketLevelOption(static_cast(socket->value), socket->options, option, value, length); + const auto native = static_cast(socket->value); + const int error = level == 6 ? GuestSockets::SetTcpLevelOption(native, socket->family != GuestUnix && socket->type == 1, option, value, length) + : level == 41 ? GuestSockets::SetIpv6LevelOption(native, socket->family == 28, option, value, length) + : GuestSockets::SetSocketLevelOption(native, socket->options, option, value, length); return error == 0 ? 0 : Fail(error); } int APS5_VABI getsockopt_nid_postfix(int descriptor, int level, int option, void* value, std::uint32_t* length) { const auto socket = Lookup(descriptor); if (!socket) return -1; - if (level != 0xffff) return Fail(42); + if (level != 0xffff && level != 6 && level != 41) return Fail(42); if (!length) return Fail(22); std::lock_guard lock(socket->modeMutex); - const int error = GuestSockets::GetSocketLevelOption(static_cast(socket->value), socket->options, socket->type, option, value, length); + const auto native = static_cast(socket->value); + const int error = level == 6 ? GuestSockets::GetTcpLevelOption(native, socket->family != GuestUnix && socket->type == 1, option, value, length) + : level == 41 ? GuestSockets::GetIpv6LevelOption(native, socket->family == 28, option, value, length) + : GuestSockets::GetSocketLevelOption(native, socket->options, socket->type, option, value, length); return error == 0 ? 0 : Fail(error); } int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { @@ -797,7 +1146,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); @@ -812,11 +1163,20 @@ int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { auto socket = std::make_shared(native, family, plainType); guard.value = Invalid; if (nonblocking && SetNonBlocking(*socket)) return -1; - return Install(std::move(socket)); + const int number = Install(std::move(socket)); + if (number >= 0 && (type & GuestCloseOnExec) != 0) { + std::lock_guard registry(socketsMutex); + closeOnExecDescriptors.insert(number); + } + return number; } catch (const std::bad_alloc&) { return Fail(12); } } +int APS5_VABI __sys_socketex_nid_postfix(const char* name, int family, int type, int protocol) { + static_cast(name); + return socket_nid_postfix(family, type, protocol); +} int APS5_VABI socketpair_nid_postfix(int family, int type, int protocol, int* descriptors) { const bool nonblocking = (type & GuestNonBlocking) != 0; const int plainType = type & ~(GuestCloseOnExec | GuestNonBlocking); @@ -849,6 +1209,11 @@ int APS5_VABI socketpair_nid_postfix(int family, int type, int protocol, int* de GuestSockets::Close(firstDescriptor); return Fail(error); } + if ((type & GuestCloseOnExec) != 0) { + std::lock_guard registry(socketsMutex); + closeOnExecDescriptors.insert(firstDescriptor); + closeOnExecDescriptors.insert(secondDescriptor); + } descriptors[0] = firstDescriptor; descriptors[1] = secondDescriptor; return 0; @@ -857,6 +1222,7 @@ int APS5_VABI socketpair_nid_postfix(int family, int type, int protocol, int* de } } int APS5_VABI connect_nid_postfix(int descriptor, const void* address, std::uint32_t length) { + if (const int error = AddressLengthError(address, length)) return Fail(error); const auto socket = Lookup(descriptor); if (!socket) return -1; if (socket->family != GuestUnix && socket->type == 2 && address && length >= 2 && static_cast(address)[1] == 0 @@ -865,6 +1231,11 @@ 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 (ZeroPortConnect(*socket, address, length)) return -1; if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); { std::lock_guard lock(socket->modeMutex); @@ -876,7 +1247,9 @@ int APS5_VABI listen_nid_postfix(int descriptor, int backlog) { const auto socket = Lookup(descriptor); if (!socket) return -1; if (socket->type != 1) return Fail(45); - return ::listen(socket->value, backlog) ? Fail(NativeError()) : 0; + if (::listen(socket->value, backlog) == 0) return 0; + const int error = NativeError(); + return Fail(error == 56 ? 22 : error); } int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* length) { const auto listener = Lookup(descriptor); @@ -894,7 +1267,10 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l bool inheritNonblocking; { std::lock_guard modeLock(listener->modeMutex); - inheritNonblocking = listener->nonblocking; + inheritNonblocking = listener->options.nonblocking; + accepted->options.noSignal = listener->options.noSignal; + accepted->options.sendTimeout = listener->options.sendTimeout; + accepted->options.receiveTimeout = listener->options.receiveTimeout; } if (inheritNonblocking && SetNonBlocking(*accepted)) return -1; const int acceptedDescriptor = Install(std::move(accepted)); @@ -908,28 +1284,32 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l std::int64_t APS5_VABI send_nid_postfix(int descriptor, const void* buffer, std::uint64_t length, int flags) { const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~GuestNoSignal) != 0) return Fail(45); + if ((flags & ~GuestSendFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); - const auto result = ::send(socket->value, static_cast(buffer), static_cast(length), NativeSendFlags(flags)); + if (const int waited = WaitForTransfer(*socket, true, flags)) return Fail(waited); + const auto result = ::send(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags)); return result < 0 ? Fail(SendError(socket->type)) : result; } std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint64_t length, int flags) { const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~2) != 0) return Fail(45); + if ((flags & ~GuestReceiveFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); const GuestArena::HostWrite destination(length == 0 ? nullptr : buffer, static_cast(length)); if (!destination.Open()) return Fail(14); + if (const int answered = ReceiveAnsweredHere(*socket, length); answered >= 0) return answered == 0 ? 0 : Fail(answered); + if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); const auto result = ::recv(socket->value, static_cast(buffer), static_cast(length), - flags & 2 ? MSG_PEEK : 0); + NativeReceiveFlags(flags, socket->type)); #ifdef _WIN32 if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) return static_cast(length); #endif return result < 0 ? Fail(NativeError()) : result; } int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_t length) { + if (const int error = AddressLengthError(address, length)) return Fail(error); const auto socket = Lookup(descriptor); if (!socket) return -1; sockaddr_storage native{}; @@ -939,7 +1319,7 @@ int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_ } int APS5_VABI shutdown_nid_postfix(int descriptor, int how) { const auto socket = Lookup(descriptor); - if (!socket) return Fail(9); + if (!socket) return -1; if (how < 0 || how > 2) return Fail(22); if (::shutdown(socket->value, how)) return Fail(NativeError()); std::lock_guard lock(socket->modeMutex); @@ -979,6 +1359,7 @@ int APS5_VABI getpeername_nid_postfix(int descriptor, void* address, std::uint32 } int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* argument) { if (!GuestSockets::IsOpen(descriptor)) return File::Ioctl(descriptor, request, argument); + if (File::IoctlRequestMalformed(request)) return Fail(25); const auto socket = Lookup(descriptor); if (!socket) return -1; if (!argument) return Fail(14); @@ -990,11 +1371,11 @@ int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* arg const auto result = ioctlsocket(socket->value, request == 0x8004667e ? FIONBIO : FIONREAD, &value); #else int nativeValue = static_cast(value); - const auto result = ::ioctl(socket->value, request == 0x8004667e ? FIONBIO : FIONREAD, &nativeValue); + const auto result = request == 0x8004667e ? ::ioctl(socket->value, FIONBIO, &nativeValue) : QueuedBytes(*socket, nativeValue); value = static_cast(nativeValue); #endif if (result) return Fail(NativeError()); - if (request == 0x8004667e) socket->nonblocking = value != 0; + if (request == 0x8004667e) socket->options.nonblocking = value != 0; if (request == 0x4004667f) *static_cast(argument) = static_cast(value); return 0; } @@ -1003,21 +1384,26 @@ int APS5_VABI _ioctl_nid_postfix(int descriptor, std::uint64_t request, void* ar } std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, std::uint64_t length, int flags, const void* address, std::uint32_t addressLength) { + if (address) { + if (const int error = AddressLengthError(address, addressLength)) return Fail(error); + } const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~GuestNoSignal) != 0) return Fail(45); + if ((flags & ~GuestSendFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); + if (const int waited = WaitForTransfer(*socket, true, flags)) return Fail(waited); int result; if (!address) { if (addressLength != 0) return Fail(22); - result = ::send(socket->value, static_cast(buffer), static_cast(length), NativeSendFlags(flags)); + result = ::send(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags)); } else { sockaddr_storage native{}; socklen_t size; if (!Address(address, addressLength, native, size)) return -1; if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); - result = static_cast(::sendto(socket->value, static_cast(buffer), static_cast(length), NativeSendFlags(flags), + if (const auto answered = ZeroPortSend(*socket, native, length); answered != NotAnswered) return answered; + result = static_cast(::sendto(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags), reinterpret_cast(&native), size)); } return result < 0 ? Fail(SendError(socket->type)) : result; @@ -1026,15 +1412,21 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u int flags, void* address, std::uint32_t* addressLength) { const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~2) != 0) return Fail(45); + if ((flags & ~GuestReceiveFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if ((!buffer && length) || (address && !addressLength)) return Fail(14); const GuestArena::HostWrite destination(length == 0 ? nullptr : buffer, static_cast(length)); if (!destination.Open()) return Fail(14); + if (const int answered = ReceiveAnsweredHere(*socket, length); answered >= 0) { + if (answered > 0) return Fail(answered); + if (address) *addressLength = 0; + return 0; + } sockaddr_storage native{}; socklen_t size = sizeof(native); + if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), - flags & 2 ? MSG_PEEK : 0, reinterpret_cast(&native), &size); + NativeReceiveFlags(flags, socket->type), reinterpret_cast(&native), &size); #ifdef _WIN32 if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(length); #endif @@ -1093,12 +1485,84 @@ std::int64_t APS5_VABI sendmsg_nid_postfix(int descriptor, const GuestMsghdr* me } return sendto_nid_postfix(descriptor, buffer.data(), buffer.size(), flags, message->name, message->name ? message->nameLength : 0u); } +#ifdef _WIN32 +static bool GuestPagesAllow(std::uintptr_t start, std::size_t bytes, DWORD allowed) { + const auto end = start + bytes; + for (auto address = start; address < end;) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) != sizeof(memory)) return false; + if (memory.State != MEM_COMMIT || (memory.Protect & PAGE_GUARD) != 0 || (memory.Protect & allowed) == 0) return false; + address = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; + } + return true; +} +#elif defined(__linux__) +static bool Transferred(ssize_t copied, std::size_t bytes, const char* call) { + if (copied >= 0) return static_cast(copied) == bytes; + if (errno == EFAULT) return false; + throw std::runtime_error(std::string("recvmsg: ") + call + " failed with errno " + std::to_string(errno)); +} +#endif +static bool GuestRange(const void* pointer, std::size_t bytes) { + const auto start = reinterpret_cast(pointer); + return start != 0 && bytes <= UINTPTR_MAX - start; +} +static bool ReadGuest(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + if (!GuestRange(source, bytes)) return false; + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(source, bytes); +#ifdef _WIN32 + if (!GuestPagesAllow(reinterpret_cast(source), bytes, + PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY)) return false; + std::memcpy(destination, source, bytes); + return true; +#elif defined(__linux__) + iovec local{destination, bytes}; + iovec remote{const_cast(source), bytes}; + return Transferred(::process_vm_readv(::getpid(), &local, 1, &remote, 1, 0), bytes, "process_vm_readv"); +#elif defined(__APPLE__) + mach_vm_size_t copied = 0; + return ::mach_vm_read_overwrite(::mach_task_self(), reinterpret_cast(source), bytes, + reinterpret_cast(destination), &copied) == KERN_SUCCESS && copied == bytes; +#else + std::memcpy(destination, source, bytes); + return true; +#endif +} +static bool WriteGuest(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + if (!GuestRange(destination, bytes)) return false; + const GuestArena::HostWrite guard(destination, bytes); + if (!guard.Open()) return false; +#ifdef _WIN32 + if (!GuestPagesAllow(reinterpret_cast(destination), bytes, + PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY)) return false; + std::memcpy(destination, source, bytes); + return true; +#elif defined(__linux__) + iovec local{const_cast(source), bytes}; + iovec remote{destination, bytes}; + return Transferred(::process_vm_writev(::getpid(), &local, 1, &remote, 1, 0), bytes, "process_vm_writev"); +#elif defined(__APPLE__) + return ::mach_vm_write(::mach_task_self(), reinterpret_cast(destination), + reinterpret_cast(source), static_cast(bytes)) == KERN_SUCCESS; +#else + std::memcpy(destination, source, bytes); + return true; +#endif +} std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, int flags) { - const auto total = MessageLength(message); + GuestMsghdr header{}; + if (!message || !ReadGuest(&header, message, sizeof(header))) return Fail(14); + if (header.iovLength < 0 || header.iovLength > 1024) return Fail(40); + std::vector vectors(static_cast(header.iovLength)); + if (!ReadGuest(vectors.data(), header.iov, vectors.size() * sizeof(GuestIovec))) return Fail(14); + header.iov = vectors.data(); + const auto total = MessageLength(&header); if (total < 0) return -1; const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~2) != 0) return Fail(45); + if ((flags & ~GuestReceiveFlags) != 0) return Fail(45); const bool records = socket->type == 2 || socket->type == 5; std::vector buffer; try { @@ -1108,8 +1572,9 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, } sockaddr_storage native{}; socklen_t size = sizeof(native); + if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); std::int64_t received = ::recvfrom(socket->value, buffer.data(), static_cast(buffer.size()), - flags & 2 ? MSG_PEEK : 0, reinterpret_cast(&native), &size); + NativeReceiveFlags(flags, socket->type), reinterpret_cast(&native), &size); if (received < 0) { #ifdef _WIN32 if (!records || WSAGetLastError() != WSAEMSGSIZE) return Fail(NativeError()); @@ -1118,27 +1583,33 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, return Fail(NativeError()); #endif } - message->flags = 0; + header.flags = 0; if (received > total) { received = total; - message->flags = 0x10; + header.flags = 0x10; } std::size_t offset = 0; - for (int i = 0; i < message->iovLength && offset < static_cast(received); ++i) { - const auto count = std::min(message->iov[i].length, static_cast(received) - offset); - std::memcpy(message->iov[i].base, buffer.data() + offset, count); + for (const auto& vector : vectors) { + if (offset >= static_cast(received)) break; + const auto count = std::min(vector.length, static_cast(received) - offset); + if (!WriteGuest(vector.base, buffer.data() + offset, count)) return Fail(14); offset += count; } - if (message->name && socket->family == GuestUnix) { - if (socket->type == 1) message->nameLength = 0; else UnnamedAddress(message->name, &message->nameLength); - } else if (message->name) { - if (size > 0 && (native.ss_family == AF_INET || native.ss_family == AF_INET6)) { - GuestAddress(native, message->name, &message->nameLength); + if (header.name) { + unsigned char address[28]{}; + if (socket->family == GuestUnix) { + if (socket->type == 1) header.nameLength = 0; else UnnamedAddress(address, &header.nameLength); + } else if (size > 0 && (native.ss_family == AF_INET || native.ss_family == AF_INET6)) { + GuestAddress(native, address, &header.nameLength); } else { - message->nameLength = 0; + header.nameLength = 0; } + if (!WriteGuest(header.name, address, header.nameLength)) return Fail(14); + if (!WriteGuest(&message->nameLength, &header.nameLength, sizeof(header.nameLength))) return Fail(14); } - message->controlLength = 0; + header.controlLength = 0; + constexpr auto results = offsetof(GuestMsghdr, controlLength); + if (!WriteGuest(reinterpret_cast(message) + results, reinterpret_cast(&header) + results, sizeof(header) - results)) return Fail(14); return received; } struct GuestSendfileHeaders { @@ -1224,7 +1695,7 @@ int APS5_VABI sendfile_nid_postfix(int file, int descriptor, std::int64_t offset int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t count, int timeout) { if (timeout < -1) return Fail(22); if (count && !descriptors) return Fail(14); - std::vector> held; + std::vector> held; std::vector native; std::vector owners; std::vector pipes; @@ -1235,30 +1706,50 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c if (entry.descriptor < 0) continue; if ((entry.events & ~GuestPollAccepted) != 0) throw std::runtime_error("poll: unsupported event flags " + std::to_string(entry.events)); - auto socket = Find(entry.descriptor); - if (!socket) { + std::shared_ptr owner; + std::uintptr_t value = 0; + if (auto socket = Find(entry.descriptor)) { + value = static_cast(socket->value); + owner = std::move(socket); + } else if (!KernelFindSocketNative_nid_no_patch(entry.descriptor, &owner, &value)) { if (!DescriptorIsOpen_nid_no_patch(entry.descriptor)) { entry.revents = GuestPollInvalid; ++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; } NativePollDescriptor request{}; - request.fd = socket->value; + request.fd = static_cast(value); for (const auto& flag : RequestFlags) if (entry.events & flag.guest) request.events = static_cast(request.events | flag.native); native.push_back(request); owners.push_back(i); - held.push_back(std::move(socket)); + held.push_back(std::move(owner)); } #ifndef _WIN32 const std::size_t socketCount = native.size(); @@ -1279,6 +1770,9 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c } const auto collect = [&](std::size_t i, bool pipe) { auto& entry = descriptors[owners[i]]; +#ifdef _WIN32 + if (!pipe) native[i].revents = ClosedStreamEvents(entry.descriptor, native[i].revents, native[i].events); +#endif for (const auto& flag : RequestFlags) if ((entry.events & flag.guest) && (native[i].revents & flag.native)) entry.revents = static_cast(entry.revents | flag.guest); @@ -1310,8 +1804,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 dda99c2edf..98da8ea9e9 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -27,6 +28,8 @@ constexpr int UMTX_OP_WAKE = 3; constexpr int UMTX_OP_WAIT_UINT = 11; constexpr int UMTX_OP_WAIT_UINT_PRIVATE = 15; constexpr int UMTX_OP_WAKE_PRIVATE = 16; +constexpr int UMTX_OP_SEM_WAIT = 19; +constexpr int UMTX_OP_SEM_WAKE = 20; constexpr int UMTX_OP_NWAKE_PRIVATE = 21; constexpr int UMTX_OP_MAX = 27; constexpr std::uint32_t UMTX_ABSTIME = 1; @@ -46,29 +49,60 @@ struct UmtxTime { std::uint32_t clockId; }; +struct UserSemaphore { + std::uint32_t hasWaiters; + std::uint32_t count; + std::uint32_t flags; +}; + struct AddressWaiter { TimedWait::Condition condition; bool woken = false; }; -std::mutex g_waitersLock; -std::unordered_map> g_waiters; +constexpr std::size_t kBucketCount = 64; + +struct alignas(64) WaiterBucket { + std::mutex mutex; + std::unordered_map> waiters; +}; + +std::array g_buckets; + +WaiterBucket& BucketOf(std::uintptr_t address) { + const auto hashed = (address >> 2) * UINT64_C(0x9e3779b97f4a7c15); + return g_buckets[hashed >> 58]; +} template bool IsAlignedAddress(std::uintptr_t address) { return address != 0 && address % alignof(TValue) == 0; } -template -bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller) { - const auto key = reinterpret_cast(address); - std::unique_lock lock(g_waitersLock); - if (std::atomic_ref(*address).load() != expected) { - return true; +template +void DeferringDelivery(TFunction function) { +#ifdef _WIN32 + { + TimedWait::DeliveryDeferral deferral; + function(); + } + TimedWait::DeliverDeferred(); +#else + function(); +#endif +} + +template +std::optional WaitOnKeyLocked(std::uintptr_t key, TReady ready, std::optional deadlineNanos, bool& woken) { + auto& bucket = BucketOf(key); + std::unique_lock lock(bucket.mutex); + if (ready()) { + woken = true; + return std::nullopt; } AddressWaiter waiter; - auto& queue = g_waiters[key]; + auto& queue = bucket.waiters[key]; const auto position = queue.insert(queue.end(), &waiter); const auto isWoken = [&] { return waiter.woken; }; const auto waitStart = std::chrono::steady_clock::now(); @@ -79,28 +113,43 @@ bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional(std::chrono::steady_clock::now() - waitStart); - const bool woken = waiter.woken; + woken = waiter.woken; if (!woken) { queue.erase(position); if (queue.empty()) { - g_waiters.erase(key); + bucket.waiters.erase(key); } } - lock.unlock(); - KernelTraceWait_nid_postfix("addr", caller, static_cast(waited.count()), !woken); + return waited; +} + +template +bool WaitOnKeyUntil(std::uintptr_t key, TReady ready, std::optional deadlineNanos, const void* caller) { + bool woken = false; + std::optional waited; + DeferringDelivery([&] { waited = WaitOnKeyLocked(key, ready, deadlineNanos, woken); }); + if (waited) { + KernelTraceWait_nid_postfix("addr", caller, static_cast(waited->count()), !woken); + } return woken; } +template +bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller) { + return WaitOnKeyUntil(reinterpret_cast(address), [&] { return std::atomic_ref(*address).load() != expected; }, deadlineNanos, caller); +} + template int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeout, const void* caller) { const auto deadline = timeout == nullptr ? std::nullopt : std::optional(TimedWait::DeadlineNanos(*timeout)); return WaitOnAddressUntil(address, expected, deadline, caller) ? SYNC_ON_ADDRESS_OK : SCE_KERNEL_ERROR_ETIMEDOUT; } -void WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { - std::lock_guard lock(g_waitersLock); - const auto entry = g_waiters.find(key); - if (entry == g_waiters.end()) { +void WakeAddressLocked(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { + auto& bucket = BucketOf(key); + std::lock_guard lock(bucket.mutex); + const auto entry = bucket.waiters.find(key); + if (entry == bucket.waiters.end()) { return; } auto& queue = entry->second; @@ -111,10 +160,14 @@ void WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { waiter->condition.NotifyOne(); } if (queue.empty()) { - g_waiters.erase(entry); + bucket.waiters.erase(entry); } } +void WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { + DeferringDelivery([&] { WakeAddressLocked(key, count, wakesAtLeastOne); }); +} + std::uint64_t SaturatingNanos(std::int64_t seconds, std::int64_t nanos) { if (seconds < 0) { return 0; @@ -131,6 +184,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); @@ -138,9 +196,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; } @@ -199,6 +254,57 @@ int UmtxNwakePrivate(void* object, std::uint64_t value) { return 0; } +void WakeSemaphoreLocked(std::uintptr_t key, UserSemaphore* semaphore) { + auto& bucket = BucketOf(key); + std::lock_guard lock(bucket.mutex); + const auto entry = bucket.waiters.find(key); + if (entry == bucket.waiters.end()) { + return; + } + auto& queue = entry->second; + if (queue.size() == 1) { + std::atomic_ref(semaphore->hasWaiters).store(0); + } + AddressWaiter* waiter = queue.front(); + queue.pop_front(); + waiter->woken = true; + waiter->condition.NotifyOne(); + if (queue.empty()) { + bucket.waiters.erase(entry); + } +} + +int SemWait(UserSemaphore* semaphore, const void* sizeArgument, const void* timeArgument, const void* caller) { + std::optional deadline; + if (const int error = UmtxDeadline(sizeArgument, timeArgument, deadline); error != 0) { + return error; + } + if (semaphore == nullptr) { + return GUEST_EFAULT; + } + if (!IsAlignedAddress(reinterpret_cast(semaphore))) { + APS5_INVALID_ARG_EX; + } + const bool woken = WaitOnKeyUntil(reinterpret_cast(semaphore), [&] { + std::uint32_t idle = 0; + std::atomic_ref(semaphore->hasWaiters).compare_exchange_strong(idle, 1); + return std::atomic_ref(semaphore->count).load() != 0; + }, deadline, caller); + return woken ? 0 : GUEST_ETIMEDOUT; +} + +int SemWake(UserSemaphore* semaphore) { + if (semaphore == nullptr) { + return GUEST_EFAULT; + } + if (!IsAlignedAddress(reinterpret_cast(semaphore))) { + APS5_INVALID_ARG_EX; + } + const auto key = reinterpret_cast(semaphore); + DeferringDelivery([&] { WakeSemaphoreLocked(key, semaphore); }); + return 0; +} + } // namespace extern "C" { @@ -271,12 +377,16 @@ int APS5_VABI _umtx_op_nid_postfix(void* object, int operation, std::uint64_t va case UMTX_OP_NWAKE_PRIVATE: error = UmtxNwakePrivate(object, value); break; + case UMTX_OP_SEM_WAIT: + error = SemWait(static_cast(object), sizeArgument, timeArgument, caller); + break; + case UMTX_OP_SEM_WAKE: + error = SemWake(static_cast(object)); + break; default: if (operation < 0 || operation >= UMTX_OP_MAX) { error = GUEST_EINVAL; - } else if (operation == UMTX_OP_RESERVED1) { - NotImplemented_nid_no_patch("_umtx_op operation 1"); - } else if (operation < UMTX_OP_RESERVED1) { + } else if (operation <= UMTX_OP_RESERVED1) { error = GUEST_EOPNOTSUPP; } else { NotImplemented_nid_no_patch(("_umtx_op operation " + std::to_string(operation)).c_str()); diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index 1a42cca261..f6502ea438 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -2,41 +2,41 @@ #include #include #include +#include #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" // The handler is recorded but never invoked: host crashes are not turned into guest core dumps. static std::atomic g_coredumpHandler{0}; static std::atomic g_coredumpContext{0}; +static std::atomic coredumpSdkVersion{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) { - if (mem == nullptr || size == 0 || size > UINTPTR_MAX - reinterpret_cast(mem)) - throw std::runtime_error(std::string(function) + ": null, empty or wrapping region outside the core dump handler"); -} - -static int attachOutsideCoredumpHandler(const char* function, const void* mem, size_t size) { - requireRegionOutsideHandler(function, mem, size); - return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; -} - -static void requireTextOutsideHandler(const char* function, const char* argument, const char* text) { - if (text == nullptr) throw std::runtime_error(std::string(function) + ": null " + argument + " outside the core dump handler"); +static bool validUserFileFlags(std::uint32_t userValue) { + return coredumpSdkVersion.load(std::memory_order_relaxed) < 0x06500000 || (userValue & 0xc0000000u) != 0x40000000u; } extern "C" { int APS5_VABI sceCoredumpRegisterCoredumpHandler(uint64_t handler, size_t stack_size, uint64_t context) { - (void)stack_size; + if (g_coredumpHandler.load(std::memory_order_relaxed)) return static_cast(0x81180002u); + if (!handler || stack_size < 0x4000 || stack_size >= 0x20000000) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; + const auto* parameters = static_cast(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t sdkVersion; + std::memcpy(&sdkVersion, parameters + 16, sizeof(sdkVersion)); + coredumpSdkVersion.store(sdkVersion, std::memory_order_relaxed); g_coredumpHandler.store(handler, std::memory_order_relaxed); g_coredumpContext.store(context, std::memory_order_relaxed); return 0; } int APS5_VABI sceCoredumpUnregisterCoredumpHandler(void) { + if (!g_coredumpHandler.load(std::memory_order_relaxed)) return static_cast(0x81180001u); g_coredumpHandler.store(0, std::memory_order_relaxed); g_coredumpContext.store(0, std::memory_order_relaxed); return 0; @@ -49,26 +49,26 @@ int APS5_VABI sceKernelDebugWriteCppExceptionInfo(const void* exception, uint64_ } -int APS5_VABI sceCoredumpAttachUserFile(uint32_t userValue, const char* path) { - (void)userValue; - requireTextOutsideHandler(__func__, "path", path); +int APS5_VABI sceCoredumpAttachUserFile(std::uint32_t userValue, const char* path) { + if (!path || !validUserFileFlags(userValue)) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; + if (static_cast(std::strlen(path)) < 0) return 0; return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } -int APS5_VABI sceCoredumpGetStopInfoGpu_Agc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const void* mem, size_t size, const char* name) { - (void)userValue; - requireRegionOutsideHandler(__func__, mem, size); - requireTextOutsideHandler(__func__, "name", name); +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 sceCoredumpSetUserDataType(void) { - NotImplemented_nid_no_patch("Uxqkdta7wEg"); +int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(std::uint32_t userValue, const void* mem, std::int32_t size, const char* name) { + (void)mem; + if (!name || !validUserFileFlags(userValue)) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; + return size < 0 ? 0 : SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; +} + +int APS5_VABI sceCoredumpSetUserDataType(std::int32_t type) { + (void)type; return 0; } @@ -81,36 +81,38 @@ void APS5_VABI sceCoredumpDebugTextOut(const char* str, int len) { } int64_t APS5_VABI sceCoredumpWriteUserData(const void* data, size_t size) { - requireRegionOutsideHandler(__func__, data, size); + if (!data || size > INT32_MAX) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } int APS5_VABI sceCoredumpGetStopInfoCpu(void* info, size_t size) { - const auto address = reinterpret_cast(info); - if (info == nullptr || size != 16 || address % alignof(uint64_t) != 0 || size > UINTPTR_MAX - address) - throw std::runtime_error(std::string(__func__) + ": null, unaligned, wrapping or unsupported CPU stop-info buffer outside the core dump handler"); + if (!info || (size != 8 && size != 16)) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } int APS5_VABI sceCoredumpGetThreadContextInfo(void* threadContextInfo, size_t threadContextInfoSize) { - (void)threadContextInfo; - (void)threadContextInfoSize; - NotImplemented_nid_no_patch(__func__); + if (!threadContextInfo || threadContextInfoSize != 144) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; + 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) { +int APS5_VABI sceCoredumpAttachUserMemoryFile(std::uint32_t userValue, const void* mem, std::int32_t size) { (void)userValue; - return attachOutsideCoredumpHandler(__func__, mem, size); + (void)mem; + return size < 0 ? 0 : SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } -int APS5_VABI sceCoredumpAttachMemoryRegion(uint32_t userValue, const void* mem, size_t size) { +int APS5_VABI sceCoredumpAttachMemoryRegion(std::uint32_t userValue, const void* mem, std::int32_t size) { (void)userValue; - return attachOutsideCoredumpHandler(__func__, mem, size); + (void)mem; + return size < 0 ? 0 : SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } } diff --git a/core/libs/prx/libkernel/System/src/Error.cpp b/core/libs/prx/libkernel/System/src/Error.cpp new file mode 100644 index 0000000000..a0eb19f9b6 --- /dev/null +++ b/core/libs/prx/libkernel/System/src/Error.cpp @@ -0,0 +1,8 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" std::int32_t APS5_VABI sceKernelError(std::int32_t posixError) { + if (posixError == 0) return 0; + return std::bit_cast(static_cast(posixError) + 0x80020000u); +} diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index 6dbef36c89..e3a768008f 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -4,6 +4,8 @@ #include "prx/libc/include/General.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" +#include +#include #include #include #include @@ -12,6 +14,7 @@ #include #include #ifdef _WIN32 +#include #include #else #include @@ -123,9 +126,63 @@ void SetFpWord(GuestMcontext& m, std::size_t offset, std::uint32_t value) { std::memcpy(reinterpret_cast(m.fpstate) + offset, &value, sizeof(value)); } +char* AppendText(char* output, const char* text) { + while (*text != '\0') *output++ = *text++; + return output; +} + +char* AppendHex(char* output, std::uint64_t value, std::size_t digits) { + static constexpr char hexDigits[] = "0123456789abcdef"; + while (digits != 0) { + --digits; + *output++ = hexDigits[(value >> (digits * 4)) & 0xf]; + } + return output; +} + +char* AppendHexField(char* output, const char* name, std::uint64_t value, std::size_t digits) { + return AppendHex(AppendText(output, name), value, digits); +} + +void LogGuestExceptionContext(int signum, const GuestMcontext& context) { + char buffer[768]; + char* output = AppendText(buffer, "[exception] guest context"); + output = AppendHexField(output, " signal=0x", static_cast(signum), 2); + output = AppendHexField(output, " rip=0x", context.rip, 16); + output = AppendHexField(output, " rsp=0x", context.rsp, 16); + output = AppendHexField(output, " rflags=0x", context.rflags, 16); + output = AppendHexField(output, " trap=0x", context.trapno, 8); + output = AppendHexField(output, " error=0x", context.err, 16); + output = AppendHexField(output, " fault=0x", context.addr, 16); + output = AppendHexField(output, " rax=0x", context.rax, 16); + output = AppendHexField(output, " rbx=0x", context.rbx, 16); + output = AppendHexField(output, " rcx=0x", context.rcx, 16); + output = AppendHexField(output, " rdx=0x", context.rdx, 16); + output = AppendHexField(output, " rsi=0x", context.rsi, 16); + output = AppendHexField(output, " rdi=0x", context.rdi, 16); + output = AppendHexField(output, " rbp=0x", context.rbp, 16); + output = AppendHexField(output, " r8=0x", context.r8, 16); + output = AppendHexField(output, " r9=0x", context.r9, 16); + output = AppendHexField(output, " r10=0x", context.r10, 16); + output = AppendHexField(output, " r11=0x", context.r11, 16); + output = AppendHexField(output, " r12=0x", context.r12, 16); + output = AppendHexField(output, " r13=0x", context.r13, 16); + output = AppendHexField(output, " r14=0x", context.r14, 16); + output = AppendHexField(output, " r15=0x", context.r15, 16); + *output++ = '\n'; +#ifdef _WIN32 + DWORD written = 0; + WriteFile(GetStdHandle(STD_ERROR_HANDLE), buffer, static_cast(output - buffer), &written, nullptr); +#else + const auto written = write(STDERR_FILENO, buffer, static_cast(output - buffer)); + (void)written; +#endif +} + void CallHandler(GuestExceptionHandler handler, int signum, GuestUcontext& ucontext) { auto& m = ucontext.mcontext; const std::uint32_t mxcsrMask = FpWord(m, MxcsrMaskOffset); + LogGuestExceptionContext(signum, m); handler(signum, &ucontext); SetFpWord(m, MxcsrOffset, FpWord(m, MxcsrOffset) & (mxcsrMask != 0 ? mxcsrMask : DefaultMxcsrMask)); SetFpWord(m, MxcsrMaskOffset, mxcsrMask); @@ -135,6 +192,7 @@ void CallHandler(GuestExceptionHandler handler, int signum, GuestUcontext& ucont constexpr std::size_t RedZone = 128; constexpr std::size_t HomeArea = 32; constexpr auto RetryLimit = std::chrono::seconds(1); +constexpr auto HostCodeLimit = std::chrono::milliseconds(50); constexpr std::size_t VectorBytes = 16 * 32; struct Delivery { @@ -249,6 +307,94 @@ bool InWinpthread(DWORD64 rip) { return winpthread != nullptr && VirtualQuery(reinterpret_cast(rip), &info, sizeof(info)) == sizeof(info) && info.AllocationBase == winpthread; } +enum class CodeKind : std::uint8_t { Unknown, Guest, Host }; + +struct ImageKinds { + static constexpr std::size_t Capacity = 256; + std::array, Capacity> bases{}; + std::array, Capacity> kinds{}; + std::atomic count{0}; +}; + +ImageKinds& Images() { + static ImageKinds images; + return images; +} + +DWORD64 ImageBase(DWORD64 rip) { + MEMORY_BASIC_INFORMATION info{}; + if (VirtualQuery(reinterpret_cast(rip), &info, sizeof(info)) != sizeof(info) || info.Type != MEM_IMAGE) return 0; + return reinterpret_cast(info.AllocationBase); +} + +CodeKind CachedKind(DWORD64 base) { + if (base == 0) return CodeKind::Guest; + auto& images = Images(); + const auto count = std::min(images.count.load(std::memory_order_acquire), ImageKinds::Capacity); + for (std::size_t index = 0; index < count; ++index) { + if (images.bases[index].load(std::memory_order_acquire) == base) return static_cast(images.kinds[index].load(std::memory_order_acquire)); + } + return CodeKind::Unknown; +} + +DWORD64 ExecutableBase() { + static const auto executable = reinterpret_cast(GetModuleHandleW(nullptr)); + return executable; +} + +CodeKind ClassifyImage(DWORD64 base) { + if (const auto known = CachedKind(base); known != CodeKind::Unknown) return known; + auto kind = CodeKind::Host; + if (base == ExecutableBase()) { + kind = CodeKind::Guest; + } else { + wchar_t path[MAX_PATH]{}; + const auto length = GetModuleFileNameW(reinterpret_cast(base), path, MAX_PATH); + const std::wstring_view name(path, length); + constexpr std::wstring_view guestSuffix = L".guest.prx"; + if (name.size() >= guestSuffix.size() && name.substr(name.size() - guestSuffix.size()) == guestSuffix) kind = CodeKind::Guest; + } + auto& images = Images(); + if (const auto slot = images.count.fetch_add(1, std::memory_order_acq_rel); slot < ImageKinds::Capacity) { + images.kinds[slot].store(static_cast(kind), std::memory_order_release); + images.bases[slot].store(base, std::memory_order_release); + } + return kind; +} + +bool BlockedInWait(DWORD64 rip) { + static const std::vector waits = [] { + std::vector stubs; + const HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); + for (const char* name : {"NtWaitForSingleObject", "NtWaitForMultipleObjects", "NtWaitForAlertByThreadId", "NtDelayExecution", "NtRemoveIoCompletion", "NtRemoveIoCompletionEx", "NtWaitForKeyedEvent", "NtSignalAndWaitForSingleObject", "NtWaitForWorkViaWorkerFactory"}) { + if (const auto address = reinterpret_cast(GetProcAddress(ntdll, name)); address != 0) stubs.push_back(address); + } + if (const HMODULE win32u = GetModuleHandleW(L"win32u.dll"); win32u != nullptr) { + if (const auto address = reinterpret_cast(GetProcAddress(win32u, "NtUserMsgWaitForMultipleObjectsEx")); address != 0) stubs.push_back(address); + } + return stubs; + }(); + return std::any_of(waits.begin(), waits.end(), [rip](DWORD64 stub) { return rip - stub < 0x20; }); +} + +DWORD64 NtdllBase() { + static const auto ntdll = reinterpret_cast(GetModuleHandleW(L"ntdll.dll")); + return ntdll; +} + +bool InSystemCall(DWORD64 rip, DWORD64 base) { + if (base == 0 || base != NtdllBase() || rip < base + 2) return false; + const auto* code = reinterpret_cast(rip - 2); + return code[0] == 0x0f && code[1] == 0x05; +} + +CodeKind RunningCode(DWORD64 rip, DWORD64& base) { + base = ImageBase(rip); + auto kind = CachedKind(base); + if (kind == CodeKind::Unknown && Images().count.load(std::memory_order_acquire) >= ImageKinds::Capacity) kind = CodeKind::Host; + return kind == CodeKind::Host && BlockedInWait(rip) ? CodeKind::Guest : kind; +} + bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { if (thread == scePthreadSelf()) { CONTEXT context{}; @@ -259,7 +405,15 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { const auto native = static_cast(thread->nativeHandle); auto queued = std::make_unique(Delivery{handler, signum, {}, {}, 0}); alignas(16) Delivery delivery{handler, signum, {}, {}, SaveVectors ? 1u : 0u}; - const auto retryDeadline = std::chrono::steady_clock::now() + RetryLimit; + const auto start = std::chrono::steady_clock::now(); + const auto retryDeadline = start + RetryLimit; + auto hostDeadline = start + HostCodeLimit; + static_cast(RestoringContext(0)); + static_cast(InWinpthread(0)); + static_cast(BlockedInWait(0)); + static_cast(ExecutableBase()); + static_cast(Images()); + static_cast(NtdllBase()); for (;;) { if (SuspendThread(native) == static_cast(-1)) { if (Exited(native)) return false; @@ -272,7 +426,9 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { delivery.context.ContextFlags = CONTEXT_FULL | CONTEXT_SEGMENTS; if (!GetThreadContext(native, &delivery.context)) { ResumeThread(native); - throw std::runtime_error("sceKernelRaiseException: cannot read the target thread context"); + if (std::chrono::steady_clock::now() >= retryDeadline) throw std::runtime_error("sceKernelRaiseException: cannot read the target thread context"); + SwitchToThread(); + continue; } if (thread->waitCount.load(std::memory_order_seq_cst) > 0) { const bool accepted = QueueUserAPC(WaitingEntry, native, reinterpret_cast(queued.get())) != 0; @@ -281,9 +437,18 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { queued.release(); return true; } - if (!RestoringContext(delivery.context.Rip) && !InWinpthread(delivery.context.Rip)) break; + const bool restoring = RestoringContext(delivery.context.Rip) || InWinpthread(delivery.context.Rip); + DWORD64 image = 0; + const auto running = restoring ? CodeKind::Guest : RunningCode(delivery.context.Rip, image); + const auto now = std::chrono::steady_clock::now(); + if (running == CodeKind::Host && InSystemCall(delivery.context.Rip, image) && now < retryDeadline) hostDeadline = std::max(hostDeadline, std::min(retryDeadline, now + HostCodeLimit)); + if (!restoring && (running == CodeKind::Guest || (running == CodeKind::Host && now >= hostDeadline))) break; ResumeThread(native); - if (std::chrono::steady_clock::now() >= retryDeadline) throw std::runtime_error("sceKernelRaiseException: the target thread stayed inside NtContinue or winpthreads for 1 s"); + if (running == CodeKind::Unknown) { + static_cast(ClassifyImage(image)); + continue; + } + if (restoring && std::chrono::steady_clock::now() >= retryDeadline) throw std::runtime_error("sceKernelRaiseException: the target thread stayed inside NtContinue or winpthreads for 1 s"); SwitchToThread(); } const DWORD64 slot = (delivery.context.Rsp - RedZone - sizeof(Delivery)) & ~static_cast(15); @@ -515,11 +680,11 @@ int APS5_VABI sceKernelRaiseException(Pthread thread, int signum) { } void APS5_VABI sceKernelDebugRaiseException(int c1, int c2) { - APS5_LOG_OUT("sceKernelDebugRaiseException c1=%d c2=%d", c1, c2); + APS5_LOG_OUT("sceKernelDebugRaiseException c1=0x%08x c2=0x%08x caller=%p", static_cast(c1), static_cast(c2), __builtin_return_address(0)); } void APS5_VABI sceKernelDebugRaiseExceptionOnReleaseMode(int c1, int c2) { - APS5_LOG_OUT("sceKernelDebugRaiseExceptionOnReleaseMode c1=%d c2=%d", c1, c2); + APS5_LOG_OUT("sceKernelDebugRaiseExceptionOnReleaseMode c1=0x%08x c2=0x%08x caller=%p", static_cast(c1), static_cast(c2), __builtin_return_address(0)); } } diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 2d13a53816..3cd590761f 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -8,12 +8,23 @@ #else #include #include +#ifdef __linux__ +#include +#elif defined(__APPLE__) +#include +#include +#endif +#endif +#if defined(__linux__) && defined(__x86_64__) +#include +#include #endif #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/HostEntropy.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" @@ -31,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +53,7 @@ extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb); +extern "C" int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInfo* info, std::uint64_t info_size); namespace { @@ -54,6 +67,86 @@ std::mutex supplementaryGroupsMutex; std::vector supplementaryGroups{0}; constexpr unsigned freebsdWaitOptions = 0x8000003Fu; +bool StoreGuestId(std::uint32_t* output) { + if (output == nullptr) return true; + const auto address = reinterpret_cast(output); + if (address > std::numeric_limits::max() - sizeof(*output)) return false; + const auto end = address + sizeof(*output); + for (auto cursor = address; cursor < end;) { + VirtualQueryInfo info{}; + if (sceKernelVirtualQuery(reinterpret_cast(cursor), 0, &info, sizeof(info)) != 0 || + info.end <= cursor || (info.protection & 2) == 0) + return false; + cursor = std::min(end, info.end); + } + const GuestArena::HostWrite guard(output, sizeof(*output)); + if (!guard.Open()) return false; + constexpr std::uint32_t zero = 0; +#ifdef _WIN32 + SIZE_T written = 0; + return WriteProcessMemory(GetCurrentProcess(), output, &zero, sizeof(zero), &written) != 0 && written == sizeof(zero); +#elif defined(__linux__) + iovec local{const_cast(&zero), sizeof(zero)}; + iovec remote{output, sizeof(zero)}; + return ::process_vm_writev(::getpid(), &local, 1, &remote, 1, 0) == static_cast(sizeof(zero)); +#elif defined(__APPLE__) + return ::mach_vm_write(::mach_task_self(), reinterpret_cast(output), + reinterpret_cast(const_cast(&zero)), sizeof(zero)) == KERN_SUCCESS; +#else + *output = zero; + return true; +#endif +} + +bool GuestRange(const void* pointer, std::size_t bytes) { + const auto start = reinterpret_cast(pointer); + return start != 0 && bytes <= std::numeric_limits::max() - start; +} + +bool ReadGuest(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + if (!GuestRange(source, bytes)) return false; + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(source, bytes); +#ifdef _WIN32 + SIZE_T copied = 0; + return ReadProcessMemory(GetCurrentProcess(), source, destination, bytes, &copied) != 0 && copied == bytes; +#elif defined(__linux__) + iovec local{destination, bytes}; + iovec remote{const_cast(source), bytes}; + const auto copied = ::process_vm_readv(::getpid(), &local, 1, &remote, 1, 0); + if (copied < 0 && errno != EFAULT) throw std::runtime_error("credentials: process_vm_readv failed with errno " + std::to_string(errno)); + return copied >= 0 && static_cast(copied) == bytes; +#elif defined(__APPLE__) + mach_vm_size_t copied = 0; + return ::mach_vm_read_overwrite(::mach_task_self(), reinterpret_cast(source), bytes, + reinterpret_cast(destination), &copied) == KERN_SUCCESS && copied == bytes; +#else + throw std::runtime_error("credentials: checked guest reads are not implemented on this host"); +#endif +} + +bool WriteGuest(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + if (!GuestRange(destination, bytes)) return false; + const GuestArena::HostWrite guard(destination, bytes); + if (!guard.Open()) return false; +#ifdef _WIN32 + SIZE_T copied = 0; + return WriteProcessMemory(GetCurrentProcess(), destination, source, bytes, &copied) != 0 && copied == bytes; +#elif defined(__linux__) + iovec local{const_cast(source), bytes}; + iovec remote{destination, bytes}; + const auto copied = ::process_vm_writev(::getpid(), &local, 1, &remote, 1, 0); + if (copied < 0 && errno != EFAULT) throw std::runtime_error("credentials: process_vm_writev failed with errno " + std::to_string(errno)); + return copied >= 0 && static_cast(copied) == bytes; +#elif defined(__APPLE__) + return ::mach_vm_write(::mach_task_self(), reinterpret_cast(destination), + reinterpret_cast(source), static_cast(bytes)) == KERN_SUCCESS; +#else + throw std::runtime_error("credentials: checked guest writes are not implemented on this host"); +#endif +} + std::atomic gpoBits{0}; constexpr std::array openPsId{'A', 'n', 'y', 'P', 'S', '5', 'O', 'p', 'e', 'n', 'P', 's', 'I', 'd', 0, 1}; @@ -81,6 +174,13 @@ public: throw std::system_error(GetLastError(), std::system_category(), "Reading process arguments"); for (int index = 0; index < count; ++index) arguments.push_back(toUtf8(values.get()[index])); + wchar_t skip[2]{}; + if (GetEnvironmentVariableW(L"ANYPS5_LOADEXEC_ARGV_SKIP", skip, 2) == 1 && skip[0] == L'1') { + if (arguments.size() < 2) throw std::runtime_error("LoadExec: missing guest process arguments"); + arguments.erase(arguments.begin()); + if (_wputenv_s(L"ANYPS5_LOADEXEC_ARGV_SKIP", L"") != 0 || !SetEnvironmentVariableW(L"ANYPS5_LOADEXEC_ARGV_SKIP", nullptr)) + throw std::runtime_error("LoadExec: cannot clear the argument marker"); + } #else std::ifstream stream("/proc/self/cmdline", std::ios::binary); if (!stream) @@ -126,9 +226,16 @@ constexpr int guestRlimitFsize = 1; constexpr int guestPriorityProcess = 0; constexpr int guestPriorityGroup = 1; constexpr int guestPriorityUser = 2; -constexpr int guestNice = 0; +std::atomic guestNice{0}; +constexpr int guestNiceMin = -20; +constexpr int guestNiceMax = 20; constexpr int guestRlimitData = 2; +constexpr int guestRlimitStack = 3; constexpr int guestRlimitCore = 4; +constexpr int guestRlimitRss = 5; +constexpr int guestRlimitMemlock = 6; +constexpr int guestRlimitNproc = 7; +constexpr int guestRlimitNofile = 8; constexpr int guestRlimitSbsize = 9; constexpr int guestRlimitVmem = 10; constexpr int guestRlimitNpts = 11; @@ -137,6 +244,8 @@ constexpr int guestRlimitKqueues = 13; constexpr int guestRlimitUmtxp = 14; constexpr int guestRlimitCount = 15; constexpr std::int64_t guestRlimitInfinity = std::numeric_limits::max(); +constexpr std::int64_t guestStackLimit = 0x200000; +constexpr std::int64_t guestProcessLimit = 1604; #ifdef _WIN32 std::int64_t hostMemoryLimit() { @@ -169,6 +278,11 @@ struct GuestResourceLimit { std::int64_t rlim_max; }; +namespace { +std::mutex openFileLimitMutex; +std::optional guestOpenFileLimit; +} + struct GuestResourceUsage { KernelTimeval ru_utime; KernelTimeval ru_stime; @@ -288,6 +402,22 @@ int APS5_VABI issetugid_nid_postfix(void) { return 0; } +int APS5_VABI getresuid_nid_postfix(std::uint32_t* ruid, std::uint32_t* euid, std::uint32_t* suid) { + if (!StoreGuestId(ruid) || !StoreGuestId(euid) || !StoreGuestId(suid)) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } + return 0; +} + +int APS5_VABI getresgid_nid_postfix(std::uint32_t* rgid, std::uint32_t* egid, std::uint32_t* sgid) { + if (!StoreGuestId(rgid) || !StoreGuestId(egid) || !StoreGuestId(sgid)) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } + return 0; +} + void APS5_VABI exit_nid_postfix(int code) { LibcExit_nid_no_patch(code); } @@ -330,6 +460,15 @@ 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 (resource == guestRlimitNproc) + throw std::runtime_error("setrlimit: RLIMIT_NPROC is not stored"); if (requested.rlim_cur != current.rlim_cur || requested.rlim_max != current.rlim_max) throw std::runtime_error("setrlimit: changing the limit of resource " + std::to_string(resource) + " is not supported"); return 0; @@ -418,6 +557,16 @@ int APS5_VABI sceKernelUuidCreate(std::uint32_t* uuid) { return 0; } +int APS5_VABI sceKernelCreateUuidV4(std::uint8_t* uuid) { + if (!uuid) return sceInvalidArgument; + std::array value; + HostEntropy_nid_no_patch(value.data(), value.size()); + value[6] = static_cast((value[6] & 0x0fu) | 0x40u); + value[8] = static_cast((value[8] & 0x3fu) | 0x80u); + std::memcpy(uuid, value.data(), value.size()); + return 0; +} + void APS5_VABI sceKernelSync(void) { SyncWrittenPaths_nid_no_patch(); } @@ -442,15 +591,66 @@ int APS5_VABI sched_get_priority_min_nid_postfix(int policy) { return 256; } +int APS5_VABI sysarch(int operation, void* parameters) { + constexpr int i386GetFsBase = 7; + constexpr int i386GetGsBase = 9; + constexpr int getFsBase = 128; + constexpr int getGsBase = 130; + constexpr int setGsBase = 131; + constexpr std::uint64_t userAddressEnd = 0x0000800000000000ull; + constexpr std::array definedOperations{0, 1, 3, 4, 7, 8, 9, 10, 11, 128, 129, 130, 131, 132}; + if (std::find(definedOperations.begin(), definedOperations.end(), operation) == definedOperations.end()) { + *__error_nid_postfix() = guestInvalid; + return -1; + } + const bool i386 = operation == i386GetFsBase || operation == i386GetGsBase; + if (!i386 && 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; + } + const bool fs = operation == getFsBase || operation == i386GetFsBase; + if (::syscall(SYS_arch_prctl, fs ? ARCH_GET_FS : ARCH_GET_GS, &base) != 0) + throw std::system_error(errno, std::generic_category(), "sysarch: reading a segment base"); + if (i386) { + const auto low = static_cast(base); + std::memcpy(parameters, &low, sizeof(low)); + return 0; + } + 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{}; @@ -521,36 +721,68 @@ int APS5_VABI getrlimit_nid_postfix(int resource, GuestResourceLimit* limit) { *__error_nid_postfix() = guestFault; return -1; } + GuestResourceLimit result{}; switch (resource) { case guestRlimitData: { #ifdef _WIN32 const std::int64_t memory = hostMemoryLimit(); - limit->rlim_cur = memory; - limit->rlim_max = memory; + result.rlim_cur = memory; + result.rlim_max = memory; #else rlimit native{}; if (::getrlimit(RLIMIT_DATA, &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); + result.rlim_cur = toGuestLimit(native.rlim_cur); + result.rlim_max = toGuestLimit(native.rlim_max); #endif - return 0; + break; } + case guestRlimitNofile: { + std::lock_guard lock(openFileLimitMutex); + if (!guestOpenFileLimit) { +#ifdef _WIN32 + 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"); + guestOpenFileLimit = GuestResourceLimit{toGuestLimit(native.rlim_cur), toGuestLimit(native.rlim_max)}; +#endif + } + result = *guestOpenFileLimit; + break; + } + case guestRlimitStack: + result.rlim_cur = guestStackLimit; + result.rlim_max = guestStackLimit; + break; + case guestRlimitNproc: + result.rlim_cur = guestProcessLimit; + result.rlim_max = guestProcessLimit; + break; case guestRlimitCpu: case guestRlimitFsize: case guestRlimitCore: + case guestRlimitRss: + case guestRlimitMemlock: case guestRlimitSbsize: case guestRlimitVmem: case guestRlimitNpts: case guestRlimitSwap: case guestRlimitKqueues: case guestRlimitUmtxp: - limit->rlim_cur = guestRlimitInfinity; - limit->rlim_max = guestRlimitInfinity; - return 0; + result.rlim_cur = guestRlimitInfinity; + result.rlim_max = guestRlimitInfinity; + break; default: throw std::runtime_error("getrlimit: unsupported resource " + std::to_string(resource)); } + if (!WriteGuest(limit, &result, sizeof(result))) { + *__error_nid_postfix() = guestFault; + return -1; + } + return 0; } int APS5_VABI getpriority_nid_postfix(int which, int who) { @@ -564,12 +796,35 @@ int APS5_VABI getpriority_nid_postfix(int which, int who) { *__error_nid_postfix() = guestNoProcess; return -1; } - return guestNice; + return guestNice.load(std::memory_order_relaxed); +} + +int APS5_VABI setpriority_nid_postfix(int which, int who, int niceval) { + if (which != guestPriorityProcess && which != guestPriorityGroup && which != guestPriorityUser) { + *__error_nid_postfix() = guestInvalid; + return -1; + } + const bool found = which == guestPriorityUser ? (who == 0 ? getuid_nid_postfix() : who) == getuid_nid_postfix() + : who == 0 || who == getpid_nid_postfix(); + if (!found) { + *__error_nid_postfix() = guestNoProcess; + return -1; + } + const int clamped = niceval < guestNiceMin ? guestNiceMin : (niceval > guestNiceMax ? guestNiceMax : niceval); + guestNice.store(clamped, std::memory_order_relaxed); + return 0; } int APS5_VABI getdtablesize_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return -1; + GuestResourceLimit limit{}; + getrlimit_nid_postfix(guestRlimitNofile, &limit); + std::int64_t tableSize = limit.rlim_cur; +#ifndef _WIN32 + tableSize = std::min(tableSize, static_cast(::getdtablesize())); +#endif + if (tableSize > std::numeric_limits::max()) + return std::numeric_limits::max(); + return static_cast(tableSize); } int APS5_VABI sceKernelIsTrinityMode(void) { @@ -601,6 +856,12 @@ int APS5_VABI setuid_nid_postfix(std::uint32_t uid) { return 0; } +int APS5_VABI setgid_nid_postfix(std::uint32_t gid) { + if (gid != 0) + throw std::runtime_error("setgid: unsupported group id " + std::to_string(gid)); + return 0; +} + int APS5_VABI setreuid_nid_postfix(std::uint32_t ruid, std::uint32_t euid) { constexpr std::uint32_t unchanged = 0xffffffffu; if (ruid != 0 && ruid != unchanged) @@ -619,6 +880,28 @@ int APS5_VABI setregid_nid_postfix(std::uint32_t rgid, std::uint32_t egid) { return 0; } +int APS5_VABI setresuid_nid_postfix(std::uint32_t ruid, std::uint32_t euid, std::uint32_t suid) { + constexpr std::uint32_t unchanged = 0xffffffffu; + if (ruid != 0 && ruid != unchanged) + throw std::runtime_error("setresuid: unsupported real user id " + std::to_string(ruid)); + if (euid != 0 && euid != unchanged) + throw std::runtime_error("setresuid: unsupported effective user id " + std::to_string(euid)); + if (suid != 0 && suid != unchanged) + throw std::runtime_error("setresuid: unsupported saved user id " + std::to_string(suid)); + return 0; +} + +int APS5_VABI setresgid_nid_postfix(std::uint32_t rgid, std::uint32_t egid, std::uint32_t sgid) { + constexpr std::uint32_t unchanged = 0xffffffffu; + if (rgid != 0 && rgid != unchanged) + throw std::runtime_error("setresgid: unsupported real group id " + std::to_string(rgid)); + if (egid != 0 && egid != unchanged) + throw std::runtime_error("setresgid: unsupported effective group id " + std::to_string(egid)); + if (sgid != 0 && sgid != unchanged) + throw std::runtime_error("setresgid: unsupported saved group id " + std::to_string(sgid)); + return 0; +} + int APS5_VABI setgroups_nid_postfix(int count, const std::uint32_t* groups) { if (count < 0 || count > freebsdGroupsMax) { *__error_nid_postfix() = errnoInvalidArgument; @@ -628,14 +911,22 @@ int APS5_VABI setgroups_nid_postfix(int count, const std::uint32_t* groups) { *__error_nid_postfix() = errnoBadAddress; return -1; } - if (count > 0 && groups[0] != 0) - throw std::runtime_error("setgroups: unsupported effective group id " + std::to_string(groups[0])); + std::vector requested; + if (count > 0) { + requested.resize(static_cast(count)); + if (!ReadGuest(requested.data(), groups, requested.size() * sizeof(requested[0]))) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } + } + if (count > 0 && requested[0] != 0) + throw std::runtime_error("setgroups: unsupported effective group id " + std::to_string(requested[0])); std::lock_guard lock(supplementaryGroupsMutex); if (count == 0) { supplementaryGroups.resize(1); return 0; } - supplementaryGroups.assign(groups, groups + count); + supplementaryGroups.swap(requested); return 0; } @@ -651,7 +942,10 @@ int APS5_VABI getgroups_nid_postfix(int capacity, std::uint32_t* groups) { *__error_nid_postfix() = errnoBadAddress; return -1; } - std::copy(supplementaryGroups.begin(), supplementaryGroups.end(), groups); + if (!WriteGuest(groups, supplementaryGroups.data(), supplementaryGroups.size() * sizeof(supplementaryGroups[0]))) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } return count; } diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 7967f91297..da332603c9 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -32,16 +32,23 @@ struct GuestSigaction { }; static_assert(sizeof(GuestSigaction) == 32 && offsetof(GuestSigaction, flags) == 8 && offsetof(GuestSigaction, mask) == 12); +extern "C" GuestSignalSet _sigintr_nid_postfix{}; + namespace { using GuestHandler = void (APS5_VABI *)(int); constexpr int MaxSignal = 128; constexpr int GuestSigkill = 9; constexpr int GuestSigstop = 17; +constexpr std::uint32_t UnblockableSignals = (1u << (GuestSigkill - 1)) | (1u << (GuestSigstop - 1)); +constexpr int GuestSigalrm = 14; +constexpr int GuestSigcont = 19; constexpr int SaRestart = 0x2; constexpr int SaSiginfo = 0x40; +constexpr int SaDefinedFlags = 0x7f; std::atomic handlers[MaxSignal + 1]{}; static_assert(std::atomic::is_always_lock_free); std::atomic blockedMask{0}; +std::atomic blockedMaskExtra[3]{}; static_assert(std::atomic::is_always_lock_free); std::mutex registration; GuestSigaction dispositions[MaxSignal + 1]{}; @@ -73,11 +80,19 @@ struct GuestTimespec { std::int64_t nanoseconds; }; static_assert(sizeof(GuestTimespec) == 16 && offsetof(GuestTimespec, nanoseconds) == 8); +bool IgnoredByDefault(int guest) { + return guest == 16 || guest == GuestSigcont || guest == 20 || guest == 23 || guest == 28 || guest == 29; +} std::uint32_t SignalBit(int guest) { return 1u << ((guest - 1) % 32); } std::uint32_t SignalWord(int guest) { return static_cast((guest - 1) / 32); } bool SignalInSet(const GuestSignalSet& set, int guest) { return (set.bits[SignalWord(guest)] & SignalBit(guest)) != 0; } bool SetHasSignal(const GuestSignalSet& set) { return (set.bits[0] | set.bits[1] | set.bits[2] | set.bits[3]) != 0; } -bool SignalBlocked(int guest) { return guest <= 32 && (blockedMask.load() & SignalBit(guest)) != 0; } +bool SignalBlocked(int guest) { + if (guest < 1 || guest > MaxSignal) return false; + const auto word = SignalWord(guest); + const auto mask = word == 0 ? blockedMask.load() : blockedMaskExtra[word - 1].load(); + return (mask & SignalBit(guest)) != 0; +} struct PendingSignals { std::mutex lock; @@ -106,8 +121,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) { @@ -181,8 +195,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) { @@ -199,6 +223,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"); @@ -214,15 +239,16 @@ bool DeliverPendingSignal(int guest) { int WaitForPendingInSet(const GuestSignalSet& set, bool waitForever, const GuestTimespec* timeout, int* guest) { auto& pending = Pending(); std::unique_lock lock(pending.lock); + std::optional deadline; for (;;) { if (PendingInSet(set, guest)) return 0; if (waitForever) { pending.changed.wait(lock); continue; } - const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout->seconds) + + if (!deadline) deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout->seconds) + std::chrono::nanoseconds(timeout->nanoseconds); - if (pending.changed.wait_until(lock, deadline) == std::cv_status::no_timeout) continue; + if (pending.changed.wait_until(lock, *deadline) == std::cv_status::no_timeout) continue; return PendingInSet(set, guest) ? 0 : GuestEagain; } } @@ -234,10 +260,27 @@ void FillSiginfo(GuestSiginfo* info, int guest) { info->pid = getpid_nid_postfix(); } +void WaitForDeliverableSignal() { + auto& pending = Pending(); + for (;;) { + int guest = 0; + { + std::unique_lock lock(pending.lock); + if (!TakePendingUnblocked(&guest)) { + pending.changed.wait(lock); + continue; + } + } + if (DeliverPendingSignal(guest)) return; + } +} + +extern "C" int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, GuestSignalSet* previousSet); + } namespace { -constexpr int GuestSigalrm = 14; +constexpr std::int64_t MaxItimerSeconds = 0x7fffffff / 2; struct AlarmState { std::mutex lock; std::condition_variable changed; @@ -265,13 +308,14 @@ void ExpireAlarm(std::chrono::microseconds interval) { handler = dispositions[GuestSigalrm].handler; } if (handler == 1) return; - if ((blockedMask.load() & (1u << (GuestSigalrm - 1))) != 0) - throw std::runtime_error("ITIMER_REAL (it_interval " + DescribeInterval(interval) + ") expired while SIGALRM is blocked: pending signals are not implemented"); - if (handler == 0) { - std::fprintf(stderr, "SIGALRM: the alarm expired with the default disposition; the process terminates\n"); - std::fflush(stderr); - std::_Exit(128 + GuestSigalrm); + 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"); } @@ -313,6 +357,10 @@ void NotifyAlarms(AlarmState& alarms) { } extern "C" unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int seconds) { + if (seconds > MaxItimerSeconds) { + *__error_nid_postfix() = 22; + return static_cast(-1); + } auto& alarms = Alarms(); std::lock_guard lock(alarms.lock); const auto now = std::chrono::steady_clock::now(); @@ -336,11 +384,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(); { @@ -362,7 +413,6 @@ extern "C" int APS5_VABI getitimer_nid_postfix(int which, GuestItimerval* output namespace { constexpr int ItimerReal = 0; constexpr int ItimerProf = 2; -constexpr std::int64_t MaxItimerSeconds = 0x7fffffff / 2; constexpr std::int64_t MinExactIntervalMicros = 1000; bool ValidItimer(const KernelTimeval& time) { @@ -435,7 +485,7 @@ GuestHandler APS5_VABI signal_nid_postfix(int guest, GuestHandler handler) { const auto invalid = reinterpret_cast(static_cast(-1)); if (guest < 1 || guest > MaxSignal || ((guest == GuestSigkill || guest == GuestSigstop) && handler != nullptr)) { *__error_nid_postfix() = 22; return invalid; } std::lock_guard lock(registration); - const GuestSigaction action{reinterpret_cast(handler), SaRestart, {}}; + const GuestSigaction action{reinterpret_cast(handler), SignalInSet(_sigintr_nid_postfix, guest) ? 0 : SaRestart, {}}; if (!Install(guest, action)) { *__error_nid_postfix() = 22; return invalid; } const auto previous = reinterpret_cast(dispositions[guest].handler); dispositions[guest] = action; @@ -443,6 +493,7 @@ GuestHandler APS5_VABI signal_nid_postfix(int guest, GuestHandler handler) { } int APS5_VABI sigaction_nid_postfix(int guest, const GuestSigaction* action, GuestSigaction* previous) { if (guest < 1 || guest > MaxSignal) { *__error_nid_postfix() = 22; return -1; } + if (action && action->handler > 1 && (action->flags & ~SaDefinedFlags) != 0) { *__error_nid_postfix() = 22; return -1; } if (action && (guest == GuestSigkill || guest == GuestSigstop) && action->handler != 0) { *__error_nid_postfix() = 22; return -1; } std::lock_guard lock(registration); const GuestSigaction old = dispositions[guest]; @@ -454,13 +505,16 @@ int APS5_VABI sigaction_nid_postfix(int guest, const GuestSigaction* action, Gue return 0; } int APS5_VABI raise_nid_postfix(int guest) { + if (guest == 0) return 0; if (guest < 1 || guest > MaxSignal) { *__error_nid_postfix() = 22; return -1; } const GuestSigaction action = Disposition(guest); if (action.handler > 1 && !Delivered(action)) throw std::runtime_error("raise: SA_SIGINFO handlers are not delivered"); + if (action.handler == 1 || (action.handler == 0 && guest != GuestSigcont && IgnoredByDefault(guest))) return 0; if (SignalBlocked(guest)) { - if (action.handler != 1) MarkPending(guest); + MarkPending(guest); return 0; } + if (action.handler == 0 && IgnoredByDefault(guest)) return 0; const int native = NativeSignal(guest); if (native != 0) { const int result = std::raise(native); @@ -484,9 +538,9 @@ int APS5_VABI killpg_nid_postfix(int pgrp, int guest) { int APS5_VABI pthread_kill_nid_postfix(Pthread thread, int guest) { if (guest < 0 || guest > MaxSignal) return PosixThread::GUEST_EINVAL; if (thread == nullptr) return PosixThread::GUEST_ESRCH; - if (guest == 0) return 0; - if (thread == scePthreadSelf()) return raise_nid_postfix(guest) == 0 ? 0 : *__error_nid_postfix(); + if (thread == scePthreadSelf()) return guest == 0 || raise_nid_postfix(guest) == 0 ? 0 : *__error_nid_postfix(); if (!GuestThreadIsLive(thread)) return PosixThread::GUEST_ESRCH; + if (guest == 0) return 0; if (Disposition(guest).handler != 1) MarkPending(guest); return 0; } @@ -503,23 +557,31 @@ 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) { previousSet->bits[0] = blockedMask.load(); - previousSet->bits[1] = 0; - previousSet->bits[2] = 0; - previousSet->bits[3] = 0; + for (int word = 1; word < 4; ++word) previousSet->bits[word] = blockedMaskExtra[word - 1].load(); } if (set != nullptr) { switch (how) { - case 1: blockedMask.fetch_or(set->bits[0]); break; - case 2: blockedMask.fetch_and(~set->bits[0]); break; - case 3: blockedMask.store(set->bits[0]); break; - default: throw std::invalid_argument("_sigprocmask: invalid how"); + case 1: + blockedMask.fetch_or(set->bits[0] & ~UnblockableSignals); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_or(set->bits[word]); + break; + case 2: + blockedMask.fetch_and(~set->bits[0]); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_and(~set->bits[word]); + break; + case 3: + blockedMask.store(set->bits[0] & ~UnblockableSignals); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].store(set->bits[word]); + break; } } } + 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); @@ -568,21 +630,23 @@ int APS5_VABI sigwait_nid_postfix(const GuestSignalSet* set, int* received) { } int APS5_VABI pause_nid_postfix(void) { - auto& pending = Pending(); - for (;;) { - int guest = 0; - { - std::unique_lock lock(pending.lock); - if (!TakePendingUnblocked(&guest)) { - pending.changed.wait(lock); - continue; - } - } - if (DeliverPendingSignal(guest)) { - *__error_nid_postfix() = GuestEintr; - return -1; - } + WaitForDeliverableSignal(); + *__error_nid_postfix() = GuestEintr; + return -1; +} + +int APS5_VABI sigsuspend_nid_postfix(const GuestSignalSet* set) { + if (set == nullptr) throw std::invalid_argument("sigsuspend: null mask"); + GuestSignalSet previous{{0, 0, 0, 0}}; + { + std::lock_guard lock(registration); + previous.bits[0] = blockedMask.exchange(set->bits[0] & ~UnblockableSignals); + for (int word = 1; word < 4; ++word) previous.bits[word] = blockedMaskExtra[word - 1].exchange(set->bits[word]); } + WaitForDeliverableSignal(); + _sigprocmask_nid_postfix(GuestSigsetmask, &previous, nullptr); + *__error_nid_postfix() = GuestEintr; + return -1; } int APS5_VABI sigpending_nid_postfix(GuestSignalSet* set) { @@ -590,9 +654,8 @@ int APS5_VABI sigpending_nid_postfix(GuestSignalSet* set) { auto& pending = Pending(); std::lock_guard lock(pending.lock); set->bits[0] = pending.words[0].load(std::memory_order_relaxed) & blockedMask.load(); - set->bits[1] = 0; - set->bits[2] = 0; - set->bits[3] = 0; + for (int word = 1; word < 4; ++word) + set->bits[word] = pending.words[word].load(std::memory_order_relaxed) & blockedMaskExtra[word - 1].load(); return 0; } } diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index ba1525bda9..da87fe7bb2 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -1,14 +1,18 @@ +#include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/System/include/HostCpu.hpp" +#include "SceTypes.hpp" #include +#include #include #include #include #include #include #include +#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include @@ -19,12 +23,18 @@ extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" int APS5_VABI getpid_nid_postfix(); extern "C" std::uint64_t APS5_VABI sceKernelGetTscFrequency(void); +struct GuestRlimit { + std::int64_t rlim_cur; + std::int64_t rlim_max; +}; +extern "C" int APS5_VABI getrlimit_nid_postfix(int resource, GuestRlimit* limit); struct SceKernelSystemSwVersion { std::uint64_t size; char versionString[0x1C]; std::uint32_t version; }; +extern "C" int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInfo* info, uint64_t info_size); namespace { @@ -43,8 +53,128 @@ int CopyOut(const void* value, std::size_t size, void* oldValue, std::size_t* ol return 0; } +struct SystemString { + int mib[2]; + const char* name; + const char* value; +}; + +constexpr SystemString SystemStrings[] = { + {{1, 1}, "kern.ostype", "FreeBSD"}, + {{1, 2}, "kern.osrelease", "11.4-RELEASE"}, + {{1, 4}, "kern.version", "FreeBSD 11.4-RELEASE\n"}, + {{1, 10}, "kern.hostname", "AnyPS5"}, + {{6, 1}, "hw.machine", "amd64"}, +}; + +struct SystemInteger { + int mib[2]; + const char* name; + std::int32_t value; +}; + +constexpr SystemInteger SystemIntegers[] = { + {{1, 24}, "kern.osreldate", 1104001}, +}; + +const SystemString* FindSystemString(const int* name) { + for (const auto& entry : SystemStrings) { + if (name[0] == entry.mib[0] && name[1] == entry.mib[1]) return &entry; + } + return nullptr; +} + +const SystemInteger* FindSystemInteger(const int* name) { + for (const auto& entry : SystemIntegers) { + if (name[0] == entry.mib[0] && name[1] == entry.mib[1]) return &entry; + } + return nullptr; +} + bool IsLeafMib(const int* name) { - return (name[0] == 6 && (name[1] == 2 || name[1] == 3 || name[1] == 5)) || (name[0] == 1 && name[1] == 37); + return FindSystemInteger(name) != nullptr || (name[0] == 6 && (name[1] == 2 || name[1] == 3 || name[1] == 5 || name[1] == 7)) || (name[0] == 1 && (name[1] == 37 || name[1] == 50)); +} + +bool IsMeasuredAbsentMib(const int* name, std::uint32_t nameLength) { + static constexpr struct { + int mib[3]; + std::uint32_t length; + } absent[] = { + {{1, 2147482816, 2147482814}, 3}, + {{1, 2147483151}, 2}, + {{1, 2147483182, 2147483181}, 3}, + {{1, 2147482838, 2147482834}, 3}, + }; + for (const auto& entry : absent) { + if (nameLength == entry.length && std::memcmp(name, entry.mib, entry.length * sizeof(int)) == 0) return true; + } + return false; +} +constexpr std::int32_t VmEntryDefaultType = 1; + +struct GuestVmEntry { + std::int32_t structSize; + std::int32_t type; + std::uint64_t start; + std::uint64_t end; + std::uint64_t offset; + std::uint64_t vnodeFileId; + std::uint32_t vnodeFsid; + std::int32_t flags; + std::int32_t resident; + std::int32_t privateResident; + std::int32_t protection; + std::int32_t referenceCount; + std::int32_t shadowCount; + std::int32_t vnodeType; + std::uint64_t vnodeSize; + std::uint32_t vnodeDevice; + std::uint16_t vnodeMode; + std::uint16_t status; + std::int32_t spare[12]; + char path[8]; +}; +static_assert(offsetof(GuestVmEntry, start) == 8 && offsetof(GuestVmEntry, protection) == 56 && offsetof(GuestVmEntry, path) == 136 && sizeof(GuestVmEntry) == 144); + +std::vector GuestVmEntries() { + std::vector entries; + std::uintptr_t start = 0; + std::uintptr_t end = 0; + for (std::uintptr_t cursor = 0; GuestArena::GuestArenaNextUsed_nid_no_patch(cursor, &start, &end); cursor = end) { + GuestVmEntry entry{}; + entry.structSize = sizeof(entry); + entry.type = VmEntryDefaultType; + entry.start = start; + entry.end = end; + VirtualQueryInfo info{}; + if (sceKernelVirtualQuery(reinterpret_cast(start), 0, &info, sizeof(info)) == 0) entry.protection = info.protection & 7; + entries.push_back(entry); + } + return entries; +} + +bool IsUnansweredMib(const int* name, std::uint32_t nameLength) { + if (nameLength != 2) return false; + return (name[0] == 1 && (name[1] == 6 || name[1] == 7 || name[1] == 12 || name[1] == 27 || name[1] == 32)) || (name[0] == 6 && (name[1] == 4 || name[1] == 11)); +} + +bool IsUnansweredName(const char* name) { + static constexpr const char* unanswered[] = { + "hw.byteorder", + "hw.cacheline_size", + "hw.machine_arch", + "hw.usermem", + "kern.clockrate", + "kern.maxfiles", + "kern.maxfilesperproc", + "kern.maxproc", + "kern.ps_strings", + "kern.sched.name", + "kern.smp.maxcpus", + "kern.stackprot", + "kern.timecounter.hardware", + }; + return std::any_of(std::begin(unanswered), std::end(unanswered), [name](const char* entry) { return std::strcmp(name, entry) == 0; }); } } @@ -55,12 +185,25 @@ std::int64_t APS5_VABI sysconf_nid_postfix(int name) { const int saved = *__error_nid_postfix(); std::int64_t result = -1; switch (name) { + case 1: // _SC_ARG_MAX + result = 262144; + break; case 3: // _SC_CLK_TCK result = 128; break; + case 5: { // _SC_OPEN_MAX + GuestRlimit limit{}; + if (getrlimit_nid_postfix(8, &limit) == 0) { + result = limit.rlim_cur; + } + break; + } case 47: // _SC_PAGESIZE result = PS5_PAGE_SIZE; break; + case 56: // _SC_IOV_MAX + result = 1024; + break; case 57: // _SC_NPROCESSORS_CONF case 58: // _SC_NPROCESSORS_ONLN #ifdef _WIN32 @@ -100,6 +243,24 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *__error_nid_postfix() = 14; return -1; } + if (IsMeasuredAbsentMib(name, nameLength)) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing an unmeasured absent MIB"); + *__error_nid_postfix() = 2; + return -1; + } + if (IsUnansweredMib(name, nameLength)) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing MIB " + std::to_string(name[0]) + "." + std::to_string(name[1])); + *__error_nid_postfix() = 2; + return -1; + } + if (nameLength == 2 && name[0] == 6 && name[1] == 7) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + 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; @@ -129,6 +290,22 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void const std::uint64_t physmem = static_cast(pages) * PS5_PAGE_SIZE; return CopyOut(&physmem, sizeof(physmem), oldValue, oldLength); } + if (nameLength == 2) { + if (const auto* entry = FindSystemString(name)) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + return CopyOut(entry->value, std::strlen(entry->value) + 1, oldValue, oldLength); + } + if (const auto* entry = FindSystemInteger(name)) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + return CopyOut(&entry->value, sizeof(entry->value), oldValue, oldLength); + } + } if (nameLength >= 3 && name[0] == 1 && name[1] == 14 && name[2] == 12) { if (newValue != nullptr) { *__error_nid_postfix() = 1; @@ -146,6 +323,22 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void static constexpr char path[] = "/app0/eboot.bin"; return CopyOut(path, sizeof(path), oldValue, oldLength); } + if (nameLength >= 3 && name[0] == 1 && name[1] == 14 && name[2] == 32) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + if (nameLength != 4) { + *__error_nid_postfix() = 22; + return -1; + } + if (name[3] != getpid_nid_postfix()) { + *__error_nid_postfix() = 3; + return -1; + } + const auto entries = GuestVmEntries(); + return CopyOut(entries.data(), entries.size() * sizeof(GuestVmEntry), oldValue, oldLength); + } if (nameLength == 2 && name[0] == 1 && name[1] == 37) { if (newValue != nullptr) { *__error_nid_postfix() = 1; @@ -161,7 +354,16 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *oldLength = count; return 0; } - if (nameLength > 2 && IsLeafMib(name)) { + if (nameLength == 2 && name[0] == 1 && name[1] == 50) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing kern.rng_pseudo is unverified"); + std::array bytes{}; + if (oldValue != nullptr && oldLength != nullptr) { + static std::random_device device; + for (auto& byte : bytes) byte = static_cast(device()); + } + return CopyOut(bytes.data(), bytes.size(), oldValue, oldLength); + } + if (nameLength > 2 && (IsLeafMib(name) || FindSystemString(name) != nullptr)) { *__error_nid_postfix() = 20; return -1; } @@ -179,6 +381,17 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si *__error_nid_postfix() = 2; return -1; } + for (const auto& entry : SystemStrings) { + if (std::strcmp(name, entry.name) == 0) return sysctl_nid_postfix(entry.mib, 2, oldValue, oldLength, newValue, newLength); + } + for (const auto& entry : SystemIntegers) { + if (std::strcmp(name, entry.name) == 0) return sysctl_nid_postfix(entry.mib, 2, oldValue, oldLength, newValue, newLength); + } + if (IsUnansweredName(name)) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing " + name); + *__error_nid_postfix() = 2; + return -1; + } if (std::strcmp(name, "kern.proc.pathname") == 0) { static constexpr int mib[] = {1, 14, 12}; return sysctl_nid_postfix(mib, 3, oldValue, oldLength, newValue, newLength); @@ -192,11 +405,11 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si *__error_nid_postfix() = 1; return -1; } - if (oldValue == nullptr || oldLength == nullptr) throw std::runtime_error(std::string(__func__) + ": kern.rng_pseudo without an output buffer"); static std::random_device device; - auto* bytes = static_cast(oldValue); - for (std::size_t i = 0; i < *oldLength; ++i) bytes[i] = static_cast(device()); - return 0; + std::uint8_t block[64]{}; + if (oldValue != nullptr && oldLength != nullptr) + for (auto& byte : block) byte = static_cast(device()); + return CopyOut(block, sizeof(block), oldValue, oldLength); } if (std::strcmp(name, "vm.overcommit") == 0) { if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": setting vm.overcommit"); @@ -219,6 +432,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); @@ -247,4 +464,56 @@ int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion* version->version = 0x01000000; return 0; } + +int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_t* size) { + struct KnownName { + const char* name; + int mib[3]; + std::size_t length; + }; + static constexpr KnownName known[] = { + {"hw.byteorder", {6, 4}, 2}, + {"hw.machine_arch", {6, 11}, 2}, + {"hw.model", {6, 2}, 2}, + {"hw.ncpu", {6, 3}, 2}, + {"hw.pagesize", {6, 7}, 2}, + {"hw.physmem", {6, 5}, 2}, + {"hw.realmem", {6, 12}, 2}, + {"kern.arandom", {1, 37}, 2}, + {"kern.clockrate", {1, 12}, 2}, + {"kern.maxfiles", {1, 7}, 2}, + {"kern.maxfilesperproc", {1, 27}, 2}, + {"kern.maxproc", {1, 6}, 2}, + {"kern.proc.pathname", {1, 14, 12}, 3}, + {"kern.ps_strings", {1, 32}, 2}, + {"kern.rng_pseudo", {1, 50}, 2}, + }; + constexpr std::size_t MaximumPath = 1024; + if (name == nullptr || size == nullptr) throw std::invalid_argument(std::string(__func__) + ": null name or size"); + const std::size_t nameLength = strnlen(name, MaximumPath); + if (nameLength == MaximumPath) { + *__error_nid_postfix() = 63; + return -1; + } + if (nameLength == 0 || std::strcmp(name, "hw.cacheline_size") == 0 || std::strcmp(name, "hw.usermem") == 0 || std::strcmp(name, "machdep.cacheline_size") == 0) { + *__error_nid_postfix() = 2; + return -1; + } + const auto copyMib = [&](const int* found, std::size_t length) { + std::size_t bytes = *size * sizeof(int); + const int result = CopyOut(found, length * sizeof(int), mib, &bytes); + *size = bytes / sizeof(int); + return result; + }; + for (const auto& entry : known) { + if (std::strcmp(name, entry.name) == 0) return copyMib(entry.mib, entry.length); + } + for (const auto& entry : SystemStrings) { + if (std::strcmp(name, entry.name) == 0) return copyMib(entry.mib, 2); + } + for (const auto& entry : SystemIntegers) { + if (std::strcmp(name, entry.name) == 0) return copyMib(entry.mib, 2); + } + throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); +} } diff --git a/core/libs/prx/libkernel/System/src/ThermalSensors.cpp b/core/libs/prx/libkernel/System/src/ThermalSensors.cpp index 992e6e468d..bd83c4282c 100644 --- a/core/libs/prx/libkernel/System/src/ThermalSensors.cpp +++ b/core/libs/prx/libkernel/System/src/ThermalSensors.cpp @@ -272,4 +272,9 @@ int APS5_VABI sceKernelGetSocSensorTemperature(int sensorId, int* outCelsius) { return 0; } +int APS5_VABI sceKernelGetSocPowerConsumption(void* consumption) { + if (consumption == nullptr) return SCE_KERNEL_ERROR_EFAULT; + return SCE_KERNEL_ERROR_EINVAL; +} + } diff --git a/core/libs/prx/libkernel/Time/SleepWake.cpp b/core/libs/prx/libkernel/Time/SleepWake.cpp new file mode 100644 index 0000000000..a6a774e8cb --- /dev/null +++ b/core/libs/prx/libkernel/Time/SleepWake.cpp @@ -0,0 +1,57 @@ +#include "prx/libkernel/Time/include/SleepWake.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" + +#include +#include + +namespace SleepWake { + +namespace { + +struct ThreadState { + unsigned earlyWakes = 0; + std::uint64_t lastSleepEndNanos = 0; + bool sleeping = false; +}; + +thread_local ThreadState state; + +std::atomic completions{0}; + +bool Enabled() { + static const bool enabled = std::getenv("APS5_NO_SLEEP_WAKE") == nullptr; + return enabled; +} + +} + +void Sleep(std::uint64_t nanos) { + bool woken = false; + if (nanos != 0 && nanos <= MAX_WAKEABLE_NANOS && !state.sleeping && Enabled()) { + if (TimedWait::NowNanos() - state.lastSleepEndNanos >= nanos) state.earlyWakes = 0; + if (state.earlyWakes < EARLY_WAKES_PER_RUN) { + state.sleeping = true; + const bool early = TimedWait::SleepNanosUnlessChanged(nanos, completions, completions.load(std::memory_order_seq_cst)); + state.sleeping = false; + if (early) ++state.earlyWakes; + woken = true; + } + } + if (!woken) TimedWait::SleepNanos(nanos); + state.lastSleepEndNanos = TimedWait::NowNanos(); +} + +} + +extern "C" { + +void KernelWakeSleepers_nid_no_patch() { + SleepWake::completions.fetch_add(1, std::memory_order_seq_cst); + TimedWait::WakeChangeSleepers(); +} + +void KernelNoteGpuSubmit_nid_no_patch() { + SleepWake::state.earlyWakes = 0; +} + +} diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 1432b9f07a..411f92751d 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/Time/include/Time.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libc/include/General.hpp" @@ -10,6 +11,8 @@ #include #include #include +#include +#include #include #include #include @@ -27,6 +30,10 @@ #include #include #include +#ifdef __linux__ +#include +#include +#endif // __linux__ #endif extern "C" int* APS5_VABI __error_nid_postfix(); @@ -170,12 +177,46 @@ std::uint64_t APS5_VABI sceKernelGetProcessTime() { } std::uint64_t APS5_VABI sceKernelGetProcessTimeCounter() { - const std::uint64_t start = GetStartNanos(); - return GetMonotonicNanos() - start; + const std::uint64_t nanos = GetMonotonicNanos() - GetStartNanos(); + const std::uint64_t frequency = sceKernelGetTscFrequency(); + return nanos / 1000000000ULL * frequency + nanos % 1000000000ULL * frequency / 1000000000ULL; } std::uint64_t APS5_VABI sceKernelGetProcessTimeCounterFrequency() { - return 1000000000ULL; + return sceKernelGetTscFrequency(); +} + +static std::string DescribeCallSite(const void* caller) { + 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 @@ -191,15 +232,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"); } @@ -210,16 +246,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; @@ -243,7 +276,7 @@ void KernelTraceWait_nid_postfix(const char* kind, const void* caller, std::uint static void CancellableSleep(std::uint64_t nanos) { ThreadCancel::Check(); - TimedWait::SleepNanos(nanos); + SleepWake::Sleep(nanos); ThreadCancel::Check(); } @@ -258,8 +291,9 @@ static int SleepForRequest(const KernelTimespec* rqtp) { constexpr int guestInvalid = 22; if (rqtp == nullptr) return guestFault; if (rqtp->tv_nsec < 0 || rqtp->tv_nsec >= 1000000000LL) return guestInvalid; + constexpr std::int64_t longestSleepSeconds = INT32_MAX / 2; if (rqtp->tv_sec >= 0) { - CancellableSleep(static_cast(rqtp->tv_sec) * 1000000000ULL + + CancellableSleep(static_cast(std::min(static_cast(rqtp->tv_sec), longestSleepSeconds)) * 1000000000ULL + static_cast(rqtp->tv_nsec)); } return 0; @@ -287,41 +321,54 @@ int APS5_VABI usleep_nid_postfix(KernelUseconds microseconds) { } #ifdef _WIN32 -static std::uint64_t CpuTimeNanos(bool callingThread, bool countKernel) { +static constexpr int GUEST_EIO = 5; +static constexpr int GUEST_EPERM = 1; + +int APS5_VABI ClockTimeWin32ErrorToGuestErrno_nid_no_patch(std::uint32_t error) { + if (error == ERROR_NOACCESS) return GUEST_EFAULT; + if (error == ERROR_ACCESS_DENIED) return GUEST_EPERM; + return GUEST_EIO; +} + +static bool CpuTimeNanos(bool callingThread, bool countKernel, std::uint64_t* nanos) { FILETIME creation{}; FILETIME exitTime{}; FILETIME kernel{}; FILETIME user{}; if (callingThread) { - if (!GetThreadTimes(GetCurrentThread(), &creation, &exitTime, &kernel, &user)) - throw std::system_error(GetLastError(), std::system_category(), "clock_gettime: GetThreadTimes failed"); + if (!GetThreadTimes(GetCurrentThread(), &creation, &exitTime, &kernel, &user)) { + *__error_nid_postfix() = ClockTimeWin32ErrorToGuestErrno_nid_no_patch(GetLastError()); + return false; + } } else { - if (!GetProcessTimes(GetCurrentProcess(), &creation, &exitTime, &kernel, &user)) - throw std::system_error(GetLastError(), std::system_category(), "clock_gettime: GetProcessTimes failed"); + if (!GetProcessTimes(GetCurrentProcess(), &creation, &exitTime, &kernel, &user)) { + *__error_nid_postfix() = ClockTimeWin32ErrorToGuestErrno_nid_no_patch(GetLastError()); + return false; + } } const auto toTicks = [](const FILETIME& time) { return (static_cast(time.dwHighDateTime) << 32) | time.dwLowDateTime; }; - return (toTicks(user) + (countKernel ? toTicks(kernel) : 0ULL)) * 100ULL; + *nanos = (toTicks(user) + (countKernel ? toTicks(kernel) : 0ULL)) * 100ULL; + return true; } -static std::uint64_t CpuTimeResolutionNanos() { +static bool CpuTimeResolutionNanos(std::uint64_t* nanos) { DWORD adjustment = 0; DWORD increment = 0; BOOL adjustmentDisabled = FALSE; - if (!GetSystemTimeAdjustment(&adjustment, &increment, &adjustmentDisabled)) - throw std::system_error(GetLastError(), std::system_category(), "clock_getres: GetSystemTimeAdjustment failed"); + if (!GetSystemTimeAdjustment(&adjustment, &increment, &adjustmentDisabled)) { + *__error_nid_postfix() = ClockTimeWin32ErrorToGuestErrno_nid_no_patch(GetLastError()); + return false; + } if (increment == 0) throw std::runtime_error("clock_getres: GetSystemTimeAdjustment reported no clock interrupt period"); - return static_cast(increment) * 100ULL; + *nanos = static_cast(increment) * 100ULL; + return true; } -static std::uint64_t ProcessCpuNanos(bool countSystem) { - return CpuTimeNanos(false, countSystem); -} - -static std::uint64_t ProcessCpuResolutionNanos() { - return CpuTimeResolutionNanos(); +static bool ProcessCpuResolutionNanos(std::uint64_t* nanos) { + return CpuTimeResolutionNanos(nanos); } #else static std::uint64_t ClockTicksPerSecond() { @@ -341,16 +388,14 @@ static std::uint64_t ProcessCpuNanos(bool countSystem) { return ticks / rate * 1000000000ULL + ticks % rate * 1000000000ULL / rate; } -static std::uint64_t ProcessCpuResolutionNanos() { +static bool ProcessCpuResolutionNanos(std::uint64_t* nanos) { const std::uint64_t rate = ClockTicksPerSecond(); - return (1000000000ULL + rate - 1ULL) / rate; + *nanos = (1000000000ULL + rate - 1ULL) / rate; + return true; } #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)) { @@ -362,7 +407,12 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { return 0; } if (clockId == 1 || clockId == 2) { +#ifdef _WIN32 + std::uint64_t nanos = 0; + if (!CpuTimeNanos(false, clockId == 2, &nanos)) return -1; +#else const std::uint64_t nanos = ProcessCpuNanos(clockId == 2); +#endif tp->tv_sec = static_cast(nanos / 1000000000ULL); tp->tv_nsec = static_cast(nanos % 1000000000ULL); return 0; @@ -395,12 +445,13 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { return 0; } if (clockId == 14 || clockId == 15) { - const std::uint64_t nanos = CpuTimeNanos(clockId == 14, true); + std::uint64_t nanos = 0; + if (!CpuTimeNanos(clockId == 14, true, &nanos)) return -1; tp->tv_sec = static_cast(nanos / 1000000000ULL); 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) { @@ -433,7 +484,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) { @@ -445,6 +496,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; @@ -475,7 +537,8 @@ int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res) { if (IsCpuClock(clockId)) return clock_getres_nid_postfix(GUEST_CLOCK_THREAD_CPUTIME_ID, res); if (clockId == 1 || clockId == 2) { - const std::uint64_t nanos = ProcessCpuResolutionNanos(); + std::uint64_t nanos = 0; + if (!ProcessCpuResolutionNanos(&nanos)) return -1; res->tv_sec = static_cast(nanos / 1000000000ULL); res->tv_nsec = static_cast(nanos % 1000000000ULL); return 0; @@ -503,7 +566,8 @@ int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res) { return 0; } if (clockId == 14 || clockId == 15) { - const std::uint64_t nanos = CpuTimeResolutionNanos(); + std::uint64_t nanos = 0; + if (!CpuTimeResolutionNanos(&nanos)) return -1; res->tv_sec = static_cast(nanos / 1000000000ULL); res->tv_nsec = static_cast(nanos % 1000000000ULL); return 0; @@ -570,10 +634,51 @@ int APS5_VABI clock_settime_nid_postfix(int clockId, const KernelTimespec* tp) { // Moved as-is (not yet implemented) from the monolithic libkernel/Export.cpp. // --------------------------------------------------------------------------- -// The sce* clock calls share the POSIX implementations above. The console clock is kept in UTC: -// the time zone reported here and by gettimeofday has no offset and no daylight saving. static constexpr int SceKernelErrorEfault = static_cast(0x8002000e); +struct HostZone { + std::int64_t standardEast; + std::int64_t daylight; +}; + +static HostZone HostZoneAt(std::int64_t utcTime) { + std::tm local{}; +#ifdef _WIN32 + constexpr std::int64_t cycleSeconds = 146097LL * 86400LL; + constexpr std::int64_t lastHostSecond = 32535215999LL; + const std::int64_t cycles = utcTime >= 0 && utcTime <= lastHostSecond ? 0 : (utcTime >= 0 ? utcTime / cycleSeconds : -((-(utcTime + 1)) / cycleSeconds + 1)); + const std::time_t t = static_cast(utcTime - cycles * cycleSeconds); + _tzset(); + if (localtime_s(&local, &t) != 0) throw std::runtime_error(std::string(__func__) + ": no host local time for " + std::to_string(utcTime)); + long standardBias = 0; + long daylightBias = 0; + _get_timezone(&standardBias); + _get_dstbias(&daylightBias); + return {-static_cast(standardBias), local.tm_isdst > 0 ? -static_cast(daylightBias) : 0}; +#else + const std::time_t t = static_cast(utcTime); + ::tzset(); + if (localtime_r(&t, &local) == nullptr) throw std::runtime_error(std::string(__func__) + ": no host local time for " + std::to_string(utcTime)); + const std::int64_t daylight = local.tm_isdst > 0 ? static_cast(local.tm_gmtoff) + ::timezone : 0; + return {static_cast(local.tm_gmtoff) - daylight, daylight}; +#endif +} + +static std::int64_t HostDaylightAmount(std::int64_t utcTime) { + constexpr std::int64_t monthSeconds = 2629746; + for (int month = 0; month <= 11; ++month) { + for (const int direction : {1, -1}) { + const HostZone zone = HostZoneAt(utcTime + direction * month * monthSeconds); + if (zone.daylight != 0) return zone.daylight; + } + } + return 0; +} + +static KernelTimesec ZoneState(std::int64_t time, const HostZone& zone) { + return {time, static_cast(zone.standardEast), static_cast(zone.daylight)}; +} + int APS5_VABI sceKernelClockGetres(KernelClockid clock_id, KernelTimespec* tp) { if (tp == nullptr) return SceKernelErrorEfault; if (clock_getres_nid_postfix(static_cast(clock_id), tp) != 0) return SceKernelError(*__error_nid_postfix()); @@ -588,16 +693,19 @@ int APS5_VABI sceKernelClockGettime(KernelClockid clock_id, KernelTimespec* tp) int APS5_VABI sceKernelConvertLocaltimeToUtc(int64_t local_time, int64_t reserved, int64_t* utc_time, KernelTimesec* timezone, int32_t* dst_seconds) { (void)reserved; - if (utc_time != nullptr) *utc_time = local_time; - if (timezone != nullptr) *timezone = {local_time, 0u, 0u}; - if (dst_seconds != nullptr) *dst_seconds = 0; + const HostZone guess = HostZoneAt(local_time); + const HostZone zone = HostZoneAt(local_time - guess.standardEast - guess.daylight); + if (utc_time != nullptr) *utc_time = local_time - zone.standardEast - zone.daylight; + if (timezone != nullptr) *timezone = ZoneState(local_time, zone); + if (dst_seconds != nullptr) *dst_seconds = static_cast(HostDaylightAmount(local_time - zone.standardEast - zone.daylight)); return 0; } int APS5_VABI sceKernelConvertUtcToLocaltime(int64_t utc_time, int64_t* local_time, KernelTimesec* st, int32_t* dst_sec) { - if (local_time != nullptr) *local_time = utc_time; - if (st != nullptr) *st = {utc_time, 0u, 0u}; - if (dst_sec != nullptr) *dst_sec = 0; + const HostZone zone = HostZoneAt(utc_time); + if (local_time != nullptr) *local_time = utc_time + zone.standardEast + zone.daylight; + if (st != nullptr) *st = ZoneState(utc_time, zone); + if (dst_sec != nullptr) *dst_sec = static_cast(zone.daylight); return 0; } diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp index 8535253896..7c171db399 100644 --- a/core/libs/prx/libkernel/Time/TimedWait.cpp +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -1,9 +1,11 @@ #include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libkernel/Pthread/include/WindowsThreadLocal.hpp" +#include #include #include #include +#include #ifdef _WIN32 #include @@ -19,6 +21,28 @@ namespace { constexpr std::uint64_t YIELD_BELOW_NANOS = 50000ULL; constexpr std::uint64_t LEAD_NANOS = 500000ULL; +std::mutex& ChangeSleepersMutex() { + static std::mutex mutex; + return mutex; +} + +std::atomic& ChangeSleeperCount() { + static std::atomic count{0}; + return count; +} + +#ifdef _WIN32 +std::vector& ChangeSleepers() { + static std::vector sleepers; + return sleepers; +} +#else +std::condition_variable& ChangeCondition() { + static std::condition_variable condition; + return condition; +} +#endif + struct ThreadWaitState { std::atomic own{0}; std::atomic* bound = &own; @@ -114,12 +138,14 @@ struct Waiter { Waiter* next = nullptr; Waiter* nested = nullptr; const void* key = nullptr; + HANDLE wake = nullptr; bool queued = false; bool busy = false; ~Waiter() { delete nested; if (event) CloseHandle(event); + if (wake) CloseHandle(wake); if (timer) CloseHandle(timer); for (const auto sleepTimer : sleepTimers) if (sleepTimer) CloseHandle(sleepTimer); } @@ -241,6 +267,18 @@ void Condition::NotifyAll() { } } +DeliveryDeferral::DeliveryDeferral() { + WaitState().bound->fetch_add(1, std::memory_order_seq_cst); +} + +DeliveryDeferral::~DeliveryDeferral() { + WaitState().bound->fetch_sub(1, std::memory_order_seq_cst); +} + +void DeliverDeferred() { + DrainApcs(); +} + namespace { struct alignas(64) ParkBucket { @@ -256,11 +294,6 @@ ParkBucket& ParkBucketFor(const void* key) { return parkBuckets[(reinterpret_cast(key) * 0x9E3779B97F4A7C15ULL) >> 56]; } -struct DeliveryDeferral { - DeliveryDeferral() { WaitState().bound->fetch_add(1, std::memory_order_seq_cst); } - ~DeliveryDeferral() { WaitState().bound->fetch_sub(1, std::memory_order_seq_cst); } -}; - void UnlinkParked(ParkBucket& bucket, Waiter* waiter) { if (waiter->previous) waiter->previous->next = waiter->next; else bucket.head = waiter->next; @@ -419,6 +452,72 @@ void SleepUntil(std::uint64_t deadlineNanos) { #endif } +bool SleepNanosUnlessChanged(std::uint64_t nanos, const std::atomic& counter, std::uint64_t observed) { + const std::uint64_t deadlineNanos = NowNanos() + nanos; +#ifdef _WIN32 + if (Coarse() || nanos < YIELD_BELOW_NANOS) { + SleepNanos(nanos); + return false; + } + Waiter* waiter = ThisThreadWaiter(); + if (!waiter->wake) waiter->wake = CreateEventW(nullptr, FALSE, FALSE, nullptr); + if (!waiter->wake) throw std::runtime_error("TimedWait: CreateEventW failed for a wakeable sleep"); + { + std::lock_guard lock(ChangeSleepersMutex()); + ChangeSleepers().push_back(waiter->wake); + } + ChangeSleeperCount().fetch_add(1, std::memory_order_seq_cst); + struct Unregister { + HANDLE wake; + ~Unregister() { + ChangeSleeperCount().fetch_sub(1, std::memory_order_seq_cst); + std::lock_guard lock(ChangeSleepersMutex()); + auto& sleepers = ChangeSleepers(); + sleepers.erase(std::find(sleepers.begin(), sleepers.end(), wake)); + } + } unregister{waiter->wake}; + ResetEvent(waiter->wake); + std::uint64_t now = NowNanos(); + if (counter.load(std::memory_order_seq_cst) != observed) return true; + if (deadlineNanos > now + LEAD_NANOS) { + const HANDLE handles[2] = {ThreadTimer(1), waiter->wake}; + LARGE_INTEGER due{}; + due.QuadPart = -static_cast((deadlineNanos - now - LEAD_NANOS) / 100ULL); + if (due.QuadPart == 0) due.QuadPart = -1; + if (handles[0] && SetWaitableTimer(handles[0], &due, 0, nullptr, nullptr, FALSE)) { + if (AlertableWait(2, handles, INFINITE) == WAIT_OBJECT_0 + 1) CancelWaitableTimer(handles[0]); + } else { + AlertableWait(1, &handles[1], WholeMilliseconds(deadlineNanos - now - LEAD_NANOS)); + } + if (counter.load(std::memory_order_seq_cst) != observed) return true; + now = NowNanos(); + } + while (now < deadlineNanos) { + if (counter.load(std::memory_order_seq_cst) != observed) return true; + YieldProcessor(); + now = NowNanos(); + } + return false; +#else + std::unique_lock lock(ChangeSleepersMutex()); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::nanoseconds(nanos); + ChangeSleeperCount().fetch_add(1, std::memory_order_seq_cst); + const bool changed = ChangeCondition().wait_until(lock, deadline, [&] { return counter.load(std::memory_order_seq_cst) != observed; }); + ChangeSleeperCount().fetch_sub(1, std::memory_order_seq_cst); + return changed; +#endif +} + +void WakeChangeSleepers() { + if (ChangeSleeperCount().load(std::memory_order_seq_cst) == 0) return; + std::lock_guard lock(ChangeSleepersMutex()); +#ifdef _WIN32 + for (const HANDLE sleeper : ChangeSleepers()) SetEvent(sleeper); +#else + ChangeCondition().notify_all(); +#endif +} + void SleepNanos(std::uint64_t nanos) { if (nanos == 0) return; #ifdef _WIN32 diff --git a/core/libs/prx/libkernel/Time/include/SleepWake.hpp b/core/libs/prx/libkernel/Time/include/SleepWake.hpp new file mode 100644 index 0000000000..47185c0cb1 --- /dev/null +++ b/core/libs/prx/libkernel/Time/include/SleepWake.hpp @@ -0,0 +1,22 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_TIME_INCLUDE_SLEEPWAKE_HPP +#define CORE_LIBS_PRX_LIBKERNEL_TIME_INCLUDE_SLEEPWAKE_HPP + +#include + +namespace SleepWake { + +constexpr std::uint64_t MAX_WAKEABLE_NANOS = 1000000ULL; +constexpr unsigned EARLY_WAKES_PER_RUN = 8; + +void Sleep(std::uint64_t nanos); + +} + +extern "C" { + +void KernelWakeSleepers_nid_no_patch(); +void KernelNoteGpuSubmit_nid_no_patch(); + +} + +#endif diff --git a/core/libs/prx/libkernel/Time/include/Time.hpp b/core/libs/prx/libkernel/Time/include/Time.hpp index dc1f2a0d85..3d292a09a6 100644 --- a/core/libs/prx/libkernel/Time/include/Time.hpp +++ b/core/libs/prx/libkernel/Time/include/Time.hpp @@ -10,11 +10,15 @@ extern "C" { std::uint64_t APS5_VABI sceKernelGetProcessTime(); std::uint64_t APS5_VABI sceKernelGetProcessTimeCounter(); std::uint64_t APS5_VABI sceKernelGetProcessTimeCounterFrequency(); + std::uint64_t APS5_VABI sceKernelGetTscFrequency(); int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); int APS5_VABI sceKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp); int APS5_VABI nanosleep_nid_postfix(const KernelTimespec* rqtp, KernelTimespec* rmtp); int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res); +#ifdef _WIN32 + int APS5_VABI ClockTimeWin32ErrorToGuestErrno_nid_no_patch(std::uint32_t error); +#endif int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz); // Debug aid: APS5_TRACE_WAITS=1 aggregates blocking waits (event flags, semaphores, condition diff --git a/core/libs/prx/libkernel/Time/include/TimedWait.hpp b/core/libs/prx/libkernel/Time/include/TimedWait.hpp index 4f7ccc32b5..2f1725819d 100644 --- a/core/libs/prx/libkernel/Time/include/TimedWait.hpp +++ b/core/libs/prx/libkernel/Time/include/TimedWait.hpp @@ -16,12 +16,23 @@ std::uint64_t DeadlineNanos(std::uint64_t microseconds); std::uint64_t RemainingMicros(std::uint64_t deadlineNanos); void SleepNanos(std::uint64_t nanos); void SleepUntil(std::uint64_t deadlineNanos); +bool SleepNanosUnlessChanged(std::uint64_t nanos, const std::atomic& counter, std::uint64_t observed); +void WakeChangeSleepers(); struct Waiter; #ifdef _WIN32 constexpr unsigned PARK_SPINS = 1000; +class DeliveryDeferral { +public: + DeliveryDeferral(); + ~DeliveryDeferral(); + DeliveryDeferral(const DeliveryDeferral&) = delete; + DeliveryDeferral& operator=(const DeliveryDeferral&) = delete; +}; + +void DeliverDeferred(); void SpinPause(); Waiter* BeginPark(const void* key); bool FinishPark(const void* key, Waiter* waiter, const std::uint64_t* deadlineNanos); diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index fbb3d6935a..617dc506f1 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -10,6 +10,16 @@ int APS5_VABI __tls_get_addr_nid_postfix(void) { return 0; } +int APS5_VABI physhm_open_nid_postfix(void) { + NotImplemented_nid_no_patch("physhm_open"); + return 0; +} + +int APS5_VABI physhm_unlink_nid_postfix(void) { + NotImplemented_nid_no_patch("physhm_unlink"); + return 0; +} + APS5_EXPORT("AC1FtjqMCL0", sceKernelUnknown03); int APS5_VABI sceKernelUnknown03(void) { NotImplemented_nid_no_patch("AC1FtjqMCL0"); @@ -111,4 +121,29 @@ int APS5_VABI sceKernelUnknown19(void) { NotImplemented_nid_no_patch("uDGXwUKqi8s"); return 0; } + +int APS5_VABI sceKernelPrintBacktraceWithModuleInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetProcessAbiType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} + +extern "C" { + +int APS5_VABI __Ux86_64_setcontext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sigreturn() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libkernel_sys/CMakeLists.txt b/core/libs/prx/libkernel_sys/CMakeLists.txt new file mode 100644 index 0000000000..6938d0c456 --- /dev/null +++ b/core/libs/prx/libkernel_sys/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library(${LIB_NAME} SHARED EXCLUDE_FROM_ALL Export.cpp Export.Discovery.cpp) +target_include_directories(${LIB_NAME} PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(${LIB_NAME}) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp new file mode 100644 index 0000000000..e4ef04b948 --- /dev/null +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -0,0 +1,2601 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI aio_cancel_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI aio_error_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI aio_fsync_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI aio_read_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI aio_return_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI aio_suspend_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI aio_waitcomplete_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI aio_write_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI amd64_set_fsbase_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI blockpool_batch_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI blockpool_map_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI blockpool_open_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI blockpool_unmap_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI chown_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI cpuset_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI cpuset_getid_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI cpuset_setid_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI dynlib_get_obj_member_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI fchownat_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI futimesat_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI get_authinfo_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI get_module_info_list_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI get_page_table_stats_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI get_sdk_compiled_version_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI get_self_auth_info_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI get_vm_map_timestamp_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI getfsstat_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI getlogin_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ipmimgr_call_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI is_in_sandbox_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kenv_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kldfind_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kldfirstmod_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kldload_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kldnext_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kldstat_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kldsym_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kldunload_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kmq_notify_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kmq_open_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kmq_setattr_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kmq_timedreceive_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kmq_timedsend_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kmq_unlink_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_close_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_destroy_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_getvalue_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_open_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_post_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_timedwait_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_trywait_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_unlink_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_wait_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ktimer_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ktimer_delete_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ktimer_getoverrun_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ktimer_gettime_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ktimer_settime_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ktrace_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI lchflags_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI lchown_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI linkat_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI mdbg_call_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI mdbg_service_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI mount_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI openintr_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI profil_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pselect_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_attr_getaffinity_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_attr_getsolosched_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_attr_setaffinity_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_attr_setcreatesuspend_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_attr_setstackaddr_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_barrier_setname_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_cond_setname_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_get_specificarray_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_get_user_context_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_getstack_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_main_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_multi_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_mutex_getprioceiling_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_mutex_init_for_mono_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_mutex_setname_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_mutex_setprioceiling_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_mutexattr_getgen_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_mutexattr_setgen_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_resume_all_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_resume_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_resume_user_context_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_rwlock_setname_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_rwlockattr_settype_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_set_user_context_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_single_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_suspend_all_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_suspend_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_suspend_user_context_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_switch_add_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_switch_delete_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_timedjoin_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ptrace_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pwrite_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI reboot_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI revoke_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI rfork_thread_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI rtld_printf_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpDebugForceCoredumpOnAppClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpDebugTriggerCoredump(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpGetStopInfoGpu(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpInternalGetThreadContextInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAddACInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAddCpumodeEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAddGpuExceptionEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAddProcessToCanvasMap(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAddResource(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAllocateDirectMemory2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAllocateDirectMemoryForApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAllocateDirectMemoryForMiniApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAllocateToolMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAllocateTraceDirectMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAprCtrl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAprSubmitCommandBufferAndGetResult_TEST(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAprSubmitCommandBuffer_TEST(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAvailableToolMemorySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelBacktraceSelf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelBeginAppMount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelBeginAppMount2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelCallIndirectBuffer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelCallIndirectBuffer2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelClearBootReqNotifyCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelClearGameDirectMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelCloseEport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelCloseEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelCloseSema(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelCreateBudget(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelCreateEport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugAcquireAndUpdateDebugRegister(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugGetAppStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugGetPauseCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugGetPrivateLogText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugGetSchedLockMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugGetSdkLogText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugGpuPaDebugIsInProgress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugInjectProcessEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugPackageCorrupted(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugRaiseExceptionWithContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugRaiseExceptionWithInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugReleaseDebugContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDeleteACInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDeleteBudget(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDeleteCpumodeEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDeleteEport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDeleteGpuExceptionEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDeleteProcessFromCanvasMap(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDeleteSavedApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDirectMemoryQueryForId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelEnableDmemAliasing2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelEnablePthreadObjectCheck(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelEndAppMount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelEndAppMount2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelEventLogInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelEventLogPread(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelEventLogRead(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelEventLogWrite(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelExpectedHddFormat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelFlock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetACInfoAllForCoredump(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetAppCategoryType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetAppInfoByAppId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetAppState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetAslrStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetAutoUpdateVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetBackupRestoreMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetBackupRestoreModeOfNextBoot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetBasicProductShape(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetBetaUpdateTestForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetBioUsageAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetBootReqNotifyCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetBudget(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetBuildNumber(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCallRecord(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetChildApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCompiledSdkVersionByPath(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCompiledSdkVersionByPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCoredumpAppInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCpuUsage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCpuUsageAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCpuUsageProc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCpuUsageProc2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCpuUsageThread(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCpumodeGame(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCurrentFanDuty(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetDataTransferMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetDebugMenuMiniModeForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetDebugMenuModeForPsmForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetDebugMenuModeForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetDefaultToolMemorySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetEmergencyErrorLog(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetExecutableModuleHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetExtLibcHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetFakeFinalizeMenuForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetFirstImageAddr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetFlagedUpdaterForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetForceUpdateModeForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetGPO(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetHwFeatureInfoForDecid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetHwModelName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetHwSerialNumber(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetIdPs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetIdTableCurrentCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetIoreq(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetIpcPath(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetKnobs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetKqueueFromEqueue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetMemoryPstate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetModuleInfoInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetModuleListInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetOpenPsIdForSystem(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetPagingStatsOfAllObjects(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetPagingStatsOfAllThreads(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetParentApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetPhysPageSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetProcessName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetProcessType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetProcessTypeOfBudget(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetProductCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetProductStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetPs4SystemSwVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetPsmIntdevModeForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetPsnAccessTraceLogForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetQafExpirationTimeNotafterForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetQafExpirationTimeNotbeforeForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetQafGenerationForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetQafNameForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetRenderingMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetResidentCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetResidentFmemCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetResource(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetResourceLimit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetResourceOne(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSafemode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSanitizerMallocReplace(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSanitizerNewReplace(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSocClock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSpecialIForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSubsysId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSubsysSubId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSystemExVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSystemLevelDebuggerModeForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSystemSwBeta(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetThreadName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetTraceMemoryStats(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUniversalMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUpdVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenDataExecutionForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenExpirationTimeNotafterForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenExpirationTimeNotbeforeForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenFakeSharefactoryForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenFlagedUpdaterForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenNpEnvSwitchingForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenSaveDataRepairForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenStoreModeForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenUseSoftwagnerForAcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenUseSoftwagnerForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenWeakenedPortRestrictionForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetVrCaptureSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGiveDirectMemoryToMapper(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelHasNeoMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelHwGetBdDriveInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelHwHasOpticalOut(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelHwHasWlanBt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccControlBDPowerState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccControlUSBPowerState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetBDPowerState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetBootTimestamp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetCPMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetCountTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetCpuInfoBit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetErrLog(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetHwInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetPowerNumberOfBootShutdown(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetPowerOperatingTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetPowerUpCause(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetSysEventLog(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetUSBPowerState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccIndicatorBootDone(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccIndicatorShutdown(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccIndicatorStandby(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccIndicatorStandbyBoot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccIndicatorStandbyShutdown(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccNotifyBootStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccNvsFlush(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccReadPowerBootMessage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccSetBuzzer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccSetCPMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccSetCpuInfoBit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccSetDownloadMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelInternalGetKmemStatistics(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelInternalGetMapStatistics(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelInternalHeapPrintBacktraceWithModuleInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelInternalMapDirectMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelInternalMapNamedDirectMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelInternalResumeDirectMemoryRelease(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelInternalSuspendDirectMemoryRelease(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsAllowedToSelectDvdRegion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsAuthenticNeo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsCEX(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsDebuggerAttached(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsDevKit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsExperimentalBeta(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsGenuineCEX(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsGenuineDevKit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsGenuineKratosCex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsGenuineN(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsGenuineTestKit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsInSandbox(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsKratos(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsM2DeviceAttached(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsMainOnStanbyMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsMainOnStandbyMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsModuleLoaded(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsNeoMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsProspero(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsPs4Process(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsTestKit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelJitGetSharedMemoryInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelKernelHeapUsage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelLoadStartModuleForSysmodule(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelLoadStartModuleInternalForMono(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelLwfsAllocateBlock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelLwfsLseek(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelLwfsSetAttribute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelLwfsTrimBlock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelLwfsWrite(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelMapNamedSystemFlexibleMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelMapSanitizerShadowMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelMapToolMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelMapTraceMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelMapperGetParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelMemoryPoolMove(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelMlockall(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelMsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelNormalizePath(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelNotifyAppEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelNotifyAppStateChanged(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelNotifySystemSuspendResumeProgress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelNotifySystemSuspendStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelOpenEport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelOpenInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelOpenSema(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelPrepareDirectMemorySwap(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelPrepareToResumeProcess(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelPrepareToSuspendProcess(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelProtectDirectMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelProtectDirectMemoryForPID(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelQueryToolMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelQueryTraceMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelRandomizedPath(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelRdup(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelReboot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelReleaseToolMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelReleaseTraceDirectMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelReportUnpatchedFunctionCall(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelReserve2mbPage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelReserveSystemDirectMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelRestoreApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelResumeDirectMemoryRelease(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelResumeProcess(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelRtldControl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSandboxPath(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSaveApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetAppInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetAppState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetBackupRestoreMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetBaseModeClock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetBesteffort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetBootReqNotify(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetBudget(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetCallRecord(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetCompressionAttribute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetCoredumpAppInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetCpumodeGame(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetDataTransferMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetEmergencyErrorLog(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetFsstParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetGPI(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetGameDirectMemoryLimit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetGpuCu(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetIoreq(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetKnobs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetMemoryPstate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetNeoModeClock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetPackageInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetPhysFmemLimit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetProcessName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetProcessProperty(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetProcessPropertyString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetSafemode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetSafemodeDirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetSuspendState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetTimezoneInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetUpdatemode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetVmContainer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSettimeofday(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSflashGetWritePrio(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSflashSetWritePrio(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSlvNotifyError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSpawn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelStreamWriteActivate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelStreamWriteCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelStreamWriteDeactivate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelStreamWriteDelete(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelStreamWriteRead(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelStreamWriteStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelStreamWriteWrite(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSuspendDirectMemoryRelease(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSuspendProcess(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSuspendSystem(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSwitchToBaseMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSwitchToNativeMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSwitchToNeoMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelTerminateProcess(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelTerminateSysCore(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelTitleWorkaroundIsEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelTitleWorkdaroundIsEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelTraceMemoryTypeProtect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelTriggerEport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelVirtualQueryAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWaitCommandBufferCompletion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteEventQueueOnCompletionCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteMapCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteMapCommand2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteMapDirectCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteMapDirectWithGpuMaskIdCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteMapWithGpuMaskIdCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteModifyMtypeProtectWithGpuMaskIdCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteModifyProtectCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteModifyProtectWithGpuMaskIdCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteMultiMapCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteMultiMapWithGpuMaskIdCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteRemapCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteRemapWithGpuMaskIdCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteSdkEventLog(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteSdkEventLogV(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteUnmapCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteWaitOnAddressCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteWaitOnCounterCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteWriteAddressCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteWriteCounterCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelYieldCpumode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLibcMspaceCreateForMonoMutex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadAtfork(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadAttrGetscope(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadAttrSetcreatesuspend(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadAttrSetscope(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadBarrierDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadBarrierInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadBarrierWait(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadBarrierattrDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadBarrierattrGetpshared(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadBarrierattrInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadBarrierattrSetpshared(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadCondattrGetclock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadGetconcurrency(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadGetcpuclockid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMulti(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexGetprioceiling(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexInitForInternalLibc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexIsowned(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexSetprioceiling(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexattrGetgen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexattrGetkind(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexattrInitForInternalLibc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexattrSetgen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexattrSetkind(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadResume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadResumeAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadSetBesteffort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadSetconcurrency(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadSingle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadSuspend(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadSuspendAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadTimedjoin(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sched_getparam_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sched_getscheduler_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sched_rr_get_interval_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sched_setparam_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sched_setscheduler_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sem_close_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sem_open_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sem_setname_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sem_unlink_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI set_phys_fmem_limit_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI setlogin_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI settimeofday_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sigqueue_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sigreturn_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI statfs_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI swapcontext_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI swapon_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI symlinkat_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysKernelGetIntdevModeForInternalLibc_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysKernelGetIntdevModeForRcmgr_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysKernelGetLowerLimitSysexVersion_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysKernelGetLowerLimitUpdVersion_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysKernelGetManufacturingMode_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysKernelGetUpdVersion_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysc_e00_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysc_m00_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysc_s00_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI tcgetsid_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI tcsetsid_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI unmount_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI utrace_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI uuidgen_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI vfork_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI wait3_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI wait4_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 67c4823346..9ece898a95 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -1,9 +1,9 @@ #include -#include #include #include #include #include +#include #include #include #include @@ -11,7 +11,7 @@ #include #include #include -#ifdef __linux__ +#ifndef _WIN32 #include #include #include @@ -19,10 +19,14 @@ #endif #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include "prx/libc/include/FilesystemError.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libc/include/CreationMask.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/KernelErrors.hpp" #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX @@ -30,6 +34,7 @@ #include #include #include +#include "prx/libkernel/File/include/WindowsSymbolicLink.hpp" #ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE #define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2 #endif @@ -38,9 +43,22 @@ extern "C" int* APS5_VABI __error_nid_postfix(); namespace { +#ifdef _WIN32 +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +static void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} +static int GuestFstat(int fd) { + const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); + struct _stat64 status{}; + const int result = ::_fstat64(fd, &status); + _set_thread_local_invalid_parameter_handler(previous); + return result; +} +#endif + constexpr int GuestEnoent = 2; constexpr int GuestEperm = 1; constexpr int GuestEio = 5; +constexpr int GuestEbadf = 9; constexpr int GuestEfault = 14; constexpr int GuestEnotdir = 20; constexpr int GuestEisdir = 21; @@ -125,59 +143,31 @@ std::int64_t CopyLinkTarget(const std::string& target, char* buffer, std::size_t return static_cast(count); } -#ifdef _WIN32 -struct SymbolicLinkReparseData { - ULONG tag; - USHORT dataLength; - USHORT reserved; - USHORT substituteNameOffset; - USHORT substituteNameLength; - USHORT printNameOffset; - USHORT printNameLength; - ULONG flags; -}; -constexpr ULONG SymbolicLinkRelative = 1; - -bool ReadRelativeSymbolicLink(const std::filesystem::path& native, std::string& target) { - const HANDLE handle = CreateFileW(native.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, - OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); - if (handle == INVALID_HANDLE_VALUE) - throw std::system_error(static_cast(GetLastError()), std::system_category(), "readlink: CreateFileW"); - std::array data{}; - DWORD size = 0; - const BOOL read = DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, nullptr, 0, data.data(), static_cast(data.size()), &size, nullptr); - const DWORD code = GetLastError(); - CloseHandle(handle); - if (!read) throw std::system_error(static_cast(code), std::system_category(), "readlink: FSCTL_GET_REPARSE_POINT"); - SymbolicLinkReparseData header{}; - if (size < sizeof(header)) return false; - std::memcpy(&header, data.data(), sizeof(header)); - if (header.tag != IO_REPARSE_TAG_SYMLINK || (header.flags & SymbolicLinkRelative) == 0) return false; - const std::size_t begin = sizeof(header) + header.substituteNameOffset; - if (begin + header.substituteNameLength > size) throw std::runtime_error("readlink: malformed symbolic link reparse data"); - std::wstring name(header.substituteNameLength / sizeof(wchar_t), L'\0'); - std::memcpy(name.data(), data.data() + begin, name.size() * sizeof(wchar_t)); - target = std::filesystem::path(name).generic_string(); - return true; +bool HasNonDirectoryPrefix(const std::filesystem::path& native) { + std::error_code error; + for (auto prefix = native.parent_path(); prefix.has_relative_path(); prefix = prefix.parent_path()) { + const auto status = std::filesystem::status(prefix, error); + if (std::filesystem::exists(status)) return !std::filesystem::is_directory(status); + } + return false; } -#endif std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, std::size_t size) { #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { std::string target; - if (!ReadRelativeSymbolicLink(native, target)) { - NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a relative symbolic link"); - return -1; - } + if (!File::WindowsSymbolicLink::Read(native, target, "readlink") && !File::WindowsSymbolicLink::ReadJunction(native, target, "readlink")) + return Fail(GuestEinval); return CopyLinkTarget(target, buffer, size); } #endif std::error_code error; const auto status = std::filesystem::symlink_status(native, error); - if (error) return Fail(GuestError(error)); - if (status.type() == std::filesystem::file_type::not_found) return Fail(GuestEnoent); + if (error) + return Fail(error == std::errc::no_such_file_or_directory && HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestError(error)); + if (status.type() == std::filesystem::file_type::not_found) + return Fail(HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestEnoent); if (!std::filesystem::is_symlink(status)) return Fail(GuestEinval); const auto target = std::filesystem::read_symlink(native, error).generic_string(); if (error) return Fail(GuestError(error)); @@ -199,15 +189,6 @@ bool HasLongComponent(std::string_view path) { return false; } -bool HasNonDirectoryPrefix(const std::filesystem::path& native) { - std::error_code error; - for (auto prefix = native.parent_path(); prefix.has_relative_path(); prefix = prefix.parent_path()) { - const auto status = std::filesystem::status(prefix, error); - if (std::filesystem::exists(status)) return !std::filesystem::is_directory(status); - } - return false; -} - bool IsDotOrDotDot(std::string_view path) { while (!path.empty() && (path.back() == '/' || path.back() == '\\')) path.remove_suffix(1); const auto separator = path.find_last_of("/\\"); @@ -276,6 +257,13 @@ int RenamePath(const std::filesystem::path& source, const std::filesystem::path& extern "C" { +int APS5_VABI sceKernelGetCompiledSdkVersion(std::int32_t* version) { + if (version == nullptr) return SCE_KERNEL_ERROR_EINVAL; + static const auto* parameters = static_cast(ApplicationProcessParameters_nid_no_patch()); + std::memcpy(version, parameters + 0x10, sizeof(*version)); + return 0; +} + int APS5_VABI fork_nid_postfix(void) { NotImplemented_nid_no_patch("fork: child processes"); return -1; @@ -295,19 +283,25 @@ int APS5_VABI fstatfs_nid_postfix(int d, void* buf) { return _fstatfs_nid_postfix(d, buf); } +std::int64_t APS5_VABI _fstat_nid_postfix(int d, FileStat* sb); + +int APS5_VABI fstat_nid_postfix(int d, FileStat* sb) { + return static_cast(_fstat_nid_postfix(d, sb)); +} + std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std::size_t size) { + if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); if (path == nullptr) return Fail(GuestEfault); if (*path == '\0') return Fail(GuestEnoent); - if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); return ReadNativeLink(native, buffer, size); } std::int64_t APS5_VABI readlinkat_nid_postfix(int fd, const char* path, char* buffer, std::size_t size) { + if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); if (path == nullptr) return Fail(GuestEfault); if (*path == '\0') return Fail(GuestEnoent); - if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); if (fd == GuestAtFdcwd || *path == '/') return readlink_nid_postfix(path, buffer, size); int error = 0; const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); @@ -324,8 +318,8 @@ std::int64_t APS5_VABI pathconf_nid_postfix(const char* path, int name) { if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); std::error_code error; const auto status = std::filesystem::status(native, error); - if (error) return Fail(GuestError(error)); - if (!std::filesystem::exists(status)) return Fail(GuestEnoent); + if (error) return Fail(error == std::errc::no_such_file_or_directory && HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestError(error)); + if (!std::filesystem::exists(status)) return Fail(HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestEnoent); switch (name) { case PcLinkMax: return 32767; case PcNameMax: return static_cast(GuestNameMax); @@ -356,6 +350,35 @@ std::int64_t APS5_VABI pathconf_nid_postfix(const char* path, int name) { } } +#ifdef _WIN32 +static int FollowLinkPath(const std::filesystem::path& source, std::filesystem::path& target) { + target = source; + const DWORD attributes = GetFileAttributesW(source.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return 0; + const HANDLE handle = CreateFileW(source.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + const DWORD code = GetLastError(); + if (code == ERROR_FILE_NOT_FOUND || code == ERROR_PATH_NOT_FOUND) return GuestEnoent; + if (code == ERROR_CANT_RESOLVE_FILENAME) return GuestEloop; + throw std::system_error(static_cast(code), std::system_category(), "link: CreateFileW"); + } + std::wstring name(MAX_PATH, L'\0'); + DWORD length = GetFinalPathNameByHandleW(handle, name.data(), static_cast(name.size()), FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); + if (length >= name.size()) { + name.resize(length); + length = GetFinalPathNameByHandleW(handle, name.data(), static_cast(name.size()), FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); + } + const DWORD code = GetLastError(); + CloseHandle(handle); + if (length == 0 || length >= name.size()) + throw std::system_error(static_cast(code), std::system_category(), "link: GetFinalPathNameByHandleW"); + name.resize(length); + target = std::filesystem::path(name); + return 0; +} +#endif + int APS5_VABI link_nid_postfix(const char* existing, const char* created) { if (existing == nullptr || created == nullptr) return static_cast(Fail(GuestEfault)); if (*existing == '\0' || *created == '\0') return static_cast(Fail(GuestEnoent)); @@ -364,11 +387,17 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { if (const int error = ResolvePathError_nid_no_patch(created, destination)) return static_cast(Fail(error)); std::error_code error; const auto status = std::filesystem::status(source, error); - if (!std::filesystem::exists(status)) return static_cast(Fail(GuestEnoent)); + if (!std::filesystem::exists(status)) return static_cast(Fail(HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent)); if (std::filesystem::is_directory(status)) return static_cast(Fail(GuestEperm)); if (std::filesystem::exists(std::filesystem::symlink_status(destination, error))) return static_cast(Fail(GuestEexist)); - if (!std::filesystem::is_directory(destination.parent_path(), error)) return static_cast(Fail(GuestEnoent)); + if (!std::filesystem::is_directory(destination.parent_path(), error)) return static_cast(Fail(HasNonDirectoryPrefix(destination) ? GuestEnotdir : GuestEnoent)); +#ifdef _WIN32 + std::filesystem::path target; + if (const int followError = FollowLinkPath(source, target)) return static_cast(Fail(followError)); + if (std::filesystem::is_directory(target, error)) return static_cast(Fail(GuestEperm)); +#else const auto target = std::filesystem::is_symlink(source, error) ? std::filesystem::canonical(source, error) : source; +#endif if (!error) std::filesystem::create_hard_link(target, destination, error); if (error == std::errc::cross_device_link) return static_cast(Fail(GuestExdev)); if (error) throw std::system_error(error, "link"); @@ -379,8 +408,9 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { int APS5_VABI fchdir_nid_postfix(int fd) { int error = 0; const char* path = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); + if (path == nullptr && error == GuestEnotdir && DescriptorHasNoVnode_nid_no_patch(fd)) return static_cast(Fail(GuestEinval)); if (path == nullptr) return static_cast(Fail(error)); -#ifdef __linux__ +#ifndef _WIN32 if (::faccessat(fd, ".", X_OK, AT_EACCESS) != 0) return static_cast(Fail(errno)); #endif error = SetWorkingDirectory_nid_no_patch(path); @@ -407,11 +437,19 @@ int APS5_VABI chroot_nid_postfix(const char* path) { int APS5_VABI symlink_nid_postfix(const char* target, const char* created) { if (target == nullptr || created == nullptr) return static_cast(Fail(GuestEfault)); + if (std::strlen(target) >= GuestPathMax) return static_cast(Fail(GuestEnametoolong)); if (*target == '\0' || *created == '\0') return static_cast(Fail(GuestEnoent)); - if (*target == '/') { + const std::filesystem::path contents(target); + if (contents.has_root_name() || contents.has_root_directory()) { NotImplemented_nid_no_patch("symlink: absolute targets"); return -1; } + for (const auto& part : contents) { + if (part == "..") { + NotImplemented_nid_no_patch("symlink: parent-directory targets"); + return -1; + } + } std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(created, native)) return static_cast(Fail(error)); std::error_code error; @@ -420,8 +458,7 @@ int APS5_VABI symlink_nid_postfix(const char* target, const char* created) { #else if (std::filesystem::exists(std::filesystem::symlink_status(native, error))) return static_cast(Fail(GuestEexist)); #endif - if (!std::filesystem::is_directory(native.parent_path(), error)) return static_cast(Fail(GuestEnoent)); - const std::filesystem::path contents(target); + if (!std::filesystem::is_directory(native.parent_path(), error)) return static_cast(Fail(HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestEnoent)); const bool directory = std::filesystem::is_directory(native.parent_path() / contents, error); #ifdef _WIN32 DWORD flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; @@ -461,6 +498,7 @@ int APS5_VABI openat_nid_postfix(int fd, const char* path, int flags, ...) { va_end(arguments); #endif } + if (File::InvalidOpenFlags(flags)) return static_cast(Fail(GuestEinval)); if (path == nullptr) return static_cast(Fail(GuestEfault)); if (*path == '\0') return static_cast(Fail(GuestEnoent)); if (fd == GuestAtFdcwd || *path == '/') return open_nid_postfix(path, flags, mode); @@ -528,19 +566,67 @@ 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))); + if (::_wchmod(native.wstring().c_str(), mode & 07777) != 0) return static_cast(Fail(errno == ENOENT && HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestFileError(errno))); #else - if (::chmod(native.c_str(), static_cast(mode & 07777)) != 0) return static_cast(Fail(GuestFileError(errno))); + if (::chmod(native.c_str(), static_cast(mode & 07777)) != 0) return static_cast(Fail(errno == ENOENT && HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestFileError(errno))); #endif RecordWrittenPath_nid_no_patch(native); return 0; } +int APS5_VABI lchmod_nid_postfix(const char* path, std::uint16_t mode) { +#ifndef __APPLE__ + std::filesystem::path native; + if (path != nullptr && *path != '\0' && ResolvePathError_nid_no_patch(path, native) == 0) { +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); + const bool symbolic = attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; +#else + std::error_code status; + const bool symbolic = std::filesystem::is_symlink(std::filesystem::symlink_status(native, status)); +#endif + if (symbolic) NotImplemented_nid_no_patch("lchmod: the mode of a symbolic link on a host that cannot change it"); + } +#endif + return fchmodat_nid_postfix(GuestAtFdcwd, path, mode, GuestAtSymlinkNofollow); +} + +int APS5_VABI fchown_nid_postfix(int fd, std::uint32_t uid, std::uint32_t gid) { + constexpr std::uint32_t Unchanged = 0xFFFFFFFFu; + if (uid != 0 && uid != Unchanged) NotImplemented_nid_no_patch("fchown: an owner other than the guest user"); + if (gid != 0 && gid != Unchanged) NotImplemented_nid_no_patch("fchown: a group other than the guest group"); +#ifdef _WIN32 + if (GuestFstat(fd) != 0) return static_cast(Fail(GuestEbadf)); +#else + struct stat status{}; + if (::fstat(fd, &status) != 0) return static_cast(Fail(GuestEbadf)); +#endif + return 0; +} + +int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb); +int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb); + +int APS5_VABI fstatat_nid_postfix(int fd, const char* path, FileStat* sb, int flag) { + if (flag & ~GuestAtSymlinkNofollow) return static_cast(Fail(GuestEinval)); + if (path == nullptr) return static_cast(Fail(GuestEfault)); + if (*path == '\0') return static_cast(Fail(GuestEnoent)); + if (fd == GuestAtFdcwd || *path == '/') return flag ? lstat_nid_postfix(path, sb) : stat_nid_postfix(path, sb); + const int result = StatInDirectory_nid_no_patch(fd, path, sb, flag == 0); + if (result < 0) return static_cast(Fail(result & 0xffff)); + return 0; +} + int APS5_VABI unlink_nid_postfix(const char* path); int APS5_VABI rmdir_nid_postfix(const char* path); @@ -559,8 +645,8 @@ int APS5_VABI unlinkat_nid_postfix(int fd, const char* path, int flag) { while (!native.has_filename() && native.has_relative_path()) native = native.parent_path(); std::error_code statusError; const auto symlinkStatus = std::filesystem::symlink_status(native, statusError); + if (IsDotOrDotDot(path)) return static_cast(Fail(GuestEinval)); if (flag & GuestAtRemovedir) { - if (IsDotOrDotDot(path)) return static_cast(Fail(GuestEinval)); if (std::filesystem::is_symlink(symlinkStatus)) return static_cast(Fail(GuestEnotdir)); if (!std::filesystem::is_directory(symlinkStatus)) { if (HasNonDirectoryPrefix(native) || std::filesystem::exists(symlinkStatus)) @@ -673,4 +759,20 @@ int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const return 0; } + +bool SetDefaultStackSize_nid_no_patch(std::size_t size); +void APS5_VABI pthread_set_name_np_nid_postfix(void* thread, const char* name); + +int APS5_VABI pthread_set_defaultstacksize_np_nid_postfix(std::size_t size) { + return SetDefaultStackSize_nid_no_patch(size) ? 0 : GuestEinval; +} + +int APS5_VABI scePthreadSetDefaultstacksize(std::size_t size) { + return SetDefaultStackSize_nid_no_patch(size) ? 0 : SceKernelError(GuestEinval); +} + +void APS5_VABI scePthreadSetName(void* thread, const char* name) { + pthread_set_name_np_nid_postfix(thread, name); +} + } diff --git a/core/libs/prx/libmdbg_syscore/Export.cpp b/core/libs/prx/libmdbg_syscore/Export.cpp new file mode 100644 index 0000000000..195bb3c119 --- /dev/null +++ b/core/libs/prx/libmdbg_syscore/Export.cpp @@ -0,0 +1,723 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceDebugAcquireAndUpdateDebugRegister() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugAttachProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugCancelCoredump() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugClearStepThread() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugCreatePerfScratchDataArea() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugCreatePerfScratchExecutableArea() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugCreateScratchDataArea() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugCreateScratchDataAreaForPrx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugCreateScratchExecutableArea() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugCreateScratchExecutableAreaForPrx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugDestroyPerfScratchDataArea() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugDestroyPerfScratchExecutableArea() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugDestroyScratchDataArea() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugDestroyScratchExecutableArea() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugDetachProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetApplicationIdByTitleId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetApplicationInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetApplicationList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetCrashInfoDetailForCoredump() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetCrashInfoForCoredump() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetDLLoadFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetDataListOfUltQueue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetDebugRegisterStatusMap() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetEventList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetEventListForEQueueFd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetEventSubscriptionList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetEventSubscriptionListForEQueueFd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetFiberInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetFileInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetFileInfoForCoredump() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetFileList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetFileListForCoredump() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetGpuInfoArea() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetJobManagerInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetJobManagerSequenceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetJobManagerSequenceList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetModuleInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetModuleList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetModuleMetaData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetMonoVMInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetMonoVMList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetProcessCoredumpHandlerEventBlob() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetProcessCoredumpHandlerEventInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetProcessCoredumpHandlerResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetProcessEventCntlFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetProcessInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetProcessList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetProcessPropertyForCoredump() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetProcessResourceStatCount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetProcessResourceStatData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetProcessTime() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetProcessTimeCounter() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetSyncExclusiveWaiterList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetSyncObjectData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetSyncObjectList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetSyncWaiterList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetSyncWaiterListForEQueueFd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetSystemStatusBlob() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetSystemStatusCount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetThreadInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetThreadInfoByIdent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetThreadList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetThreadListAsIdent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetUlObjectList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetUltCondvarInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetUltInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetUltListOfUltRuntime() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetUltMutexInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetUltQueueDataResourcePoolInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetUltQueueInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetUltRuntimeInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetUltRwlockInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetUltSemaphoreInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetUltWaitingQueueResourcePoolInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetVirtualMemoryDetailInfoForCoredump() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetVirtualMemoryInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetVirtualMemoryInfoForCoredump() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetWaitingListOfUltCondvar() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetWaitingListOfUltMutex() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetWaitingListOfUltQueue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetWaitingListOfUltRwlock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetWaitingListOfUltSemaphore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugGetWorkerThreadListOfUltRuntime() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugInitForCoredump() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugInitForTest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetBlockTimeInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetBlockedIpcInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetChannelInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetChannelKidList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetChannelWaitingThreadList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetClientInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetClientKidList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetClientKidListByDump() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetClientKidListByServerKid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetConnectRequestInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetConnectionInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetConnectionWaitingThreadListByClientKid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetConnectionWaitingThreadListBySessionKid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetDump() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetEncryptedInfoAllForCoredump() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetKidInfoListForCoredump() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetServerDispatchInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetServerInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetServerKidList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetServerKidListByDump() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetServerWaitingThreadList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetSessionInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetSessionKidList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetSessionKidListByServerKid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugIpmiGetTidListByDump() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugKillApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugKillProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugNoStopChildProcesses() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugNoStopOnDLLoad() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugProcessSpawn() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugReadEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugReadEventForTest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugReadProcessMemory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugReadProcessMemoryForSDBGP() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugReadProcessRegister() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugReadProcessRegisterForSDBGP() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugReadProcessResourceStatData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugReadThreadRegister() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugReadThreadRegisterForSDBGP() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugReleaseDebugRegister() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugResumeApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugResumeProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugResumeThread() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugSetProcessEventCntlFlag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugSetStepThread() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugSpawnApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugStopChildProcesses() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugStopOnDLLoad() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugSuspendApplication() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugSuspendProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugSuspendThread() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugTriggerCoredump() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugTriggerCoredumpForSystem() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugTriggerVrCaptureDump() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugWriteProcessMemory() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugWriteProcessRegister() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDebugWriteThreadRegister() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} 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 d7f755740f..be99345d50 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -8,8 +8,21 @@ #include #include #include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#elif defined(__linux__) +#include "prx/libc/include/specifics/linux/ElfTypes.hpp" +#include +#include +#include +#endif extern "C" { +void APS5_VABI _init_env_nid_postfix(); int APS5_VABI posix_memalign_nid_postfix(void**, std::size_t, std::size_t); void* APS5_VABI reallocf_nid_postfix(void*, std::size_t); void* APS5_VABI _Znwm_nid_postfix(std::size_t); @@ -25,22 +38,83 @@ 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 { alignas(64) std::array storage{}; +alignas(16) std::array process{}; +alignas(16) std::array libc{}; +alignas(16) std::array replacement{}; +#ifdef _WIN32 +[[gnu::section(".proctst"), gnu::used]] alignas(4) std::uint32_t processParameterMetadata[2]{}; +#endif std::size_t lastSize = 0; std::size_t lastAlignment = 0; unsigned posixCalls = 0; unsigned initializes = 0; +unsigned finalizes = 0; unsigned frees = 0; bool fail = false; bool recurse = false; bool nullPosixResult = false; -void require(bool condition) { - if (!condition) throw std::runtime_error("application heap test failed"); +void require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "application heap test failed at line %u\n", location.line()); + throw std::runtime_error("application heap test failed"); + } +} + +void installProcessParameters() { +#ifdef _WIN32 + auto* image = reinterpret_cast(GetModuleHandleW(nullptr)); + require(image != nullptr); + 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; + } + } + require(target != nullptr); + processParameterMetadata[0] = static_cast(process.data() - image); + processParameterMetadata[1] = static_cast(process.size()); + DWORD oldProtect = 0; + require(VirtualProtect(target, sizeof(*target), PAGE_READWRITE, &oldProtect) != 0); + std::memcpy(target->Name, ".procpar", 8); + target->Misc.VirtualSize = sizeof(processParameterMetadata); + DWORD restored = 0; + VirtualProtect(target, sizeof(*target), oldProtect, &restored); +#elif defined(__linux__) + bool installed = false; + dl_iterate_phdr([](dl_phdr_info* image, std::size_t, void* 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); + if (mprotect(reinterpret_cast(pageStart), pageEnd - pageStart, PROT_READ | PROT_WRITE) != 0) return 1; + mutableHeader->p_type = 0x61000001; + mutableHeader->p_vaddr = reinterpret_cast(process.data()) - image->dlpi_addr; + mutableHeader->p_paddr = mutableHeader->p_vaddr; + mutableHeader->p_filesz = process.size(); + mutableHeader->p_memsz = process.size(); + *static_cast(data) = true; + return 1; + } + return 1; + }, &installed); + require(installed); +#endif } template @@ -51,7 +125,7 @@ void reject(TAction action) { } void APS5_VABI initialize() { ++initializes; } -void APS5_VABI finalize() { require(initializes == 1); } +void APS5_VABI finalize() { require(initializes == 1 && ++finalizes == 1); } void APS5_VABI exitCallbackAllocates() { if (ApplicationHeapAllocate_nid_no_patch(16) != storage.data()) throw std::runtime_error("application heap test failed"); } @@ -103,12 +177,20 @@ void write(std::array& data, std::size_t offset, TValue value) } +#ifdef __linux__ +extern "C" void HeapTestSdkInitialize() { initialize(); } +extern "C" void HeapTestSdkFinalize() { finalize(); } +extern "C" void* HeapTestAllocatorEntry(unsigned index) { + require(index < 7); + void* result; + std::memcpy(&result, replacement.data() + 0x20 + index * sizeof(result), sizeof(result)); + return result; +} +#endif + int main(int argc, char** argv) { reject([] { ApplicationHeapAllocate_nid_no_patch(64); }); reject([] { ApplicationHeapRegister_nid_no_patch(nullptr); }); - std::array process{}; - std::array libc{}; - std::array replacement{}; write(process, 0, std::uint64_t{0x40}); write(process, 8, std::uint32_t{0x4942524f}); write(process, 0x38, libc.data()); @@ -125,6 +207,82 @@ int main(int argc, char** argv) { write(replacement, 0x40, &align); write(replacement, 0x48, &realign); write(replacement, 0x50, &posixAlign); +#ifdef __linux__ + if (argc > 1 && (std::strcmp(argv[1], "sdk") == 0 || std::strcmp(argv[1], "sdk-partial") == 0)) { + require(argc == 3); + void* sdk = dlopen(argv[2], RTLD_NOW | RTLD_GLOBAL); + require(sdk != nullptr && initializes == 1 && finalizes == 0); + if (std::strcmp(argv[1], "sdk-partial") == 0) { + reject([] { ApplicationHeapInitialize_nid_no_patch(process.data()); }); + require(initializes == 1); + require(dlclose(sdk) == 0); + return 0; + } + const auto sdkAllocate = reinterpret_cast(dlsym(sdk, "gQX+4GDQjpM#guest")); + require(sdkAllocate != nullptr); + auto* live = static_cast(sdkAllocate(32)); + require(live == reinterpret_cast(storage.data())); + std::memset(live, 0x5a, 32); + ApplicationHeapInitialize_nid_no_patch(process.data()); + require(initializes == 1); + for (unsigned index = 0; index < 32; ++index) require(live[index] == 0x5a); + ApplicationHeapFree_nid_no_patch(live); + require(frees == 1); + require(ApplicationHeapAlign_nid_no_patch(64, 48) == storage.data()); + require(lastSize == 48 && lastAlignment == 64); + ApplicationHeapFree_nid_no_patch(storage.data()); + require(frees == 2); + require(dlclose(sdk) == 0 && finalizes == 0); + require(ApplicationHeapAllocate_nid_no_patch(24) == storage.data() && lastSize == 24); + ApplicationHeapFree_nid_no_patch(storage.data()); + require(frees == 3 && initializes == 1); + return 0; + } + if (argc > 1 && std::strcmp(argv[1], "sdk-default") == 0) { + require(argc == 3); + void* sdk = dlopen(argv[2], RTLD_NOW | RTLD_GLOBAL); + require(sdk != nullptr && initializes == 1); + std::memset(replacement.data() + 0x10, 0, replacement.size() - 0x10); + ApplicationHeapInitialize_nid_no_patch(process.data()); + void* pointer = ApplicationHeapAllocate_nid_no_patch(24); + require(pointer != nullptr && pointer != storage.data() && lastSize == 0); + ApplicationHeapFree_nid_no_patch(pointer); + require(frees == 0 && dlclose(sdk) == 0 && finalizes == 1); + return 0; + } +#endif + if (argc > 1 && std::strcmp(argv[1], "lazy") == 0) { + installProcessParameters(); + require(initializes == 0); + require(_Znwm_nid_postfix(40) == storage.data() && lastSize == 40 && initializes == 1); + _ZdlPv_nid_postfix(storage.data()); + require(frees == 1); + _init_env_nid_postfix(); + require(initializes == 1); + return 0; + } + if (argc > 1 && std::strcmp(argv[1], "scripting") == 0) { + installProcessParameters(); + constexpr std::size_t blockBytes = 0x40000; + auto* first = static_cast(scriptingGetMem_nid_postfix(0x1000, blockBytes)); + require(initializes == 0 && lastSize == 0); + auto* second = static_cast(scriptingGetMem_nid_postfix(0x1000, blockBytes)); + require(initializes == 0 && lastSize == 0); + require(first != nullptr && second != nullptr && reinterpret_cast(first) % 0x1000 == 0 && reinterpret_cast(second) % 0x1000 == 0); + require(first + blockBytes <= second || second + blockBytes <= first); + std::memset(first, 0x5a, blockBytes); + std::memset(second, 0xa5, blockBytes); + require(first[blockBytes - 1] == std::byte{0x5a} && second[0] == std::byte{0xa5}); + scriptingFreeMem_nid_postfix(first); + scriptingFreeMem_nid_postfix(second); + require(initializes == 0 && frees == 0); + _init_env_nid_postfix(); + require(initializes == 1); + require(_Znwm_nid_postfix(40) == storage.data() && lastSize == 40); + _ZdlPv_nid_postfix(storage.data()); + require(frees == 1); + return 0; + } if (argc > 1 && std::strcmp(argv[1], "exit-order") == 0) { require(atexit_nid_postfix(exitCallbackAllocates) == 0); ApplicationHeapInitialize_nid_no_patch(process.data()); @@ -300,7 +458,7 @@ int main(int argc, char** argv) { require(directory == reinterpret_cast(storage.data()) && directory[0] == '/' && lastSize == std::strlen(directory) + 1); ApplicationHeapFree_nid_no_patch(directory); directory = getcwd_nid_postfix(nullptr, 200); - require(directory == reinterpret_cast(storage.data()) && lastSize == 200); + require(directory == reinterpret_cast(storage.data()) && lastSize == std::strlen(directory) + 1); ApplicationHeapFree_nid_no_patch(directory); fail = true; *__error_nid_postfix() = 0; @@ -312,4 +470,24 @@ 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; + const auto sizeBeforeScripting = lastSize; + scriptingFreeMem_nid_postfix(nullptr); + auto* small = static_cast(scriptingGetMem_nid_postfix(0, 31)); + auto* aligned = static_cast(scriptingGetMem_nid_postfix(64, 37)); + require(small != nullptr && small != storage.data() && reinterpret_cast(small) % 16 == 0); + require(aligned != nullptr && aligned != storage.data() && reinterpret_cast(aligned) % 64 == 0); + std::memset(small, 0x5a, 31); + std::memset(aligned, 0xa5, 37); + require(small + 31 <= aligned || aligned + 37 <= small); + scriptingFreeMem_nid_postfix(small); + scriptingFreeMem_nid_postfix(aligned); + require(scriptingGetMem_nid_postfix(24, 80) == nullptr); + require(scriptingGetMem_nid_postfix(16, std::numeric_limits::max()) == nullptr); + fail = true; + auto* unaffected = scriptingGetMem_nid_postfix(16, 64); + require(unaffected != nullptr); + scriptingFreeMem_nid_postfix(unaffected); + fail = false; + require(frees == beforeScripting && lastSize == sizeBeforeScripting); } diff --git a/core/libs/tests/ApplicationHeapStats.cpp b/core/libs/tests/ApplicationHeapStats.cpp index d2694001d3..1345d218d1 100644 --- a/core/libs/tests/ApplicationHeapStats.cpp +++ b/core/libs/tests/ApplicationHeapStats.cpp @@ -62,6 +62,14 @@ void write(std::array& data, std::size_t offset, TValue value) std::memcpy(data.data() + offset, &value, sizeof(value)); } +template +TValue read(const std::array& data, std::size_t offset) { + TValue value{}; + require(offset <= data.size() && sizeof(value) <= data.size() - offset); + std::memcpy(&value, data.data() + offset, sizeof(value)); + return value; +} + } int main(int argc, char** argv) { @@ -90,13 +98,31 @@ int main(int argc, char** argv) { std::memset(replacement.data() + 0x20, 0, 10 * sizeof(void*)); ApplicationHeapInitialize_nid_no_patch(process.data()); require(ApplicationHeapAllocate_nid_no_patch(16) != nullptr); - reject([&] { malloc_stats_fast_nid_postfix(stats.data()); }); + const auto measure = [&] { + stats.fill(std::byte{0x5a}); + write(stats, 0, std::uint16_t{0x28}); + write(stats, 2, std::uint16_t{1}); + require(malloc_stats_fast_nid_postfix(stats.data()) == 0); + const auto maxSystem = read(stats, 0x08), system = read(stats, 0x10); + const auto maxInUse = read(stats, 0x18), inUse = read(stats, 0x20); + require(maxSystem >= system && system >= inUse && maxInUse >= inUse && read(stats, 0x28) == 0x5a5a5a5a5a5a5a5aull); + return std::array{maxSystem, system, maxInUse, inUse}; + }; + const auto initial = measure(); + require(initial[3] >= 16); auto* small = ApplicationHeapAllocate_nid_no_patch(16); auto* large = ApplicationHeapAllocate_nid_no_patch(100000); + const auto holding = measure(); + require(holding[3] >= initial[3] + 100016 && holding[1] >= initial[1]); require(malloc_usable_size_nid_postfix(small) == 16 && malloc_usable_size_nid_postfix(large) == 100000); require(malloc_usable_size_nid_postfix(nullptr) == 0 && usableCalls == 0); ApplicationHeapFree_nid_no_patch(small); ApplicationHeapFree_nid_no_patch(large); + const auto released = measure(); + require(released[3] == initial[3] && released[2] == holding[2]); + write(stats, 0, std::uint16_t{0x27}); + require(malloc_stats_fast_nid_postfix(stats.data()) == 22); + require(malloc_stats_fast_nid_postfix(nullptr) == 22 && statsCalls == 0); return 0; } if (argc > 1 && std::strcmp(argv[1], "missing") == 0) { diff --git a/core/libs/tests/CpuTopology.cpp b/core/libs/tests/CpuTopology.cpp index 09ddd35682..3502b849dd 100644 --- a/core/libs/tests/CpuTopology.cpp +++ b/core/libs/tests/CpuTopology.cpp @@ -34,6 +34,28 @@ int main() { Require(Parse("0;1\n") == 0, "malformed separator"); Require(CpuTopology::MaskFromCpuList("/nonexistent/anyps5/cpus") == 0, "missing file"); + { + std::uint64_t byClass[4] = {}; + bool seen[4] = {}; + const auto empty = CpuTopology::LayoutFromClasses(byClass, seen, 4); + Require(!empty.hybrid && empty.efficient == 0 && empty.performant == 0, "empty classes"); + + byClass[0] = 0xff; + seen[0] = true; + const auto single = CpuTopology::LayoutFromClasses(byClass, seen, 4); + Require(!single.hybrid && single.efficient == 0 && single.performant == 0, "single class"); + + byClass[1] = 0xff00; + seen[1] = true; + const auto twoTier = CpuTopology::LayoutFromClasses(byClass, seen, 4); + Require(twoTier.hybrid && twoTier.efficient == 0xff && twoTier.performant == 0xff00, "two-tier classes"); + + byClass[2] = 0xff0000; + seen[2] = true; + const auto threeTier = CpuTopology::LayoutFromClasses(byClass, seen, 4); + Require(threeTier.hybrid && threeTier.efficient == 0xffff && threeTier.performant == 0xff0000, "three-tier classes aggregate efficient"); + } + const auto& layout = CpuTopology::Get(); Require(layout.process != 0, "process affinity read"); Require(!layout.hybrid || (layout.performant & layout.efficient) == 0, "hybrid classes overlap"); diff --git a/core/libs/tests/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..54111f196c 100644 --- a/core/libs/tests/ExceptionRuntime.cpp +++ b/core/libs/tests/ExceptionRuntime.cpp @@ -10,10 +10,15 @@ #include #include #include +#include +#include #include #include #include #include +#ifdef __linux__ +#include +#endif #include extern "C" unsigned __cxa_uncaught_exceptions_nid_postfix(); @@ -26,6 +31,8 @@ extern "C" [[noreturn]] void _ZSt13_Xrange_errorPKc_nid_postfix(const char*); extern "C" [[noreturn]] void _ZNSt8__sce_v219_Xbad_function_callEv_nid_postfix(); extern "C" [[noreturn]] void __cxa_bad_cast_nid_postfix(); extern "C" [[noreturn]] void _ZSt19_Throw_bad_weak_ptrv_nid_postfix(); +extern "C" [[noreturn]] void _ZSt26__throw_bad_variant_accessv_nid_postfix(); +extern "C" [[noreturn]] void _ZSt26_Throw_bad_optional_accessv_nid_postfix(); extern "C" [[noreturn]] void _ZNKSt9exception6_RaiseEv_nid_postfix(const void*); extern "C" void* __cxa_vec_new3_nid_postfix(std::size_t, std::size_t, std::size_t, void(*)(void*), void(*)(void*), void*(*)(std::size_t), void(*)(void*, std::size_t)); extern "C" void __cxa_vec_delete3_nid_postfix(void*, std::size_t, std::size_t, void(*)(void*), void(*)(void*, std::size_t)); @@ -172,6 +179,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]); @@ -244,6 +254,10 @@ int main() { catch (const std::exception& value) { assert(value.what() != nullptr); } try { _ZSt19_Throw_bad_weak_ptrv_nid_postfix(); std::abort(); } catch (const std::bad_weak_ptr& value) { if (std::strcmp(value.what(), "bad_weak_ptr") != 0) std::abort(); } + try { _ZSt26__throw_bad_variant_accessv_nid_postfix(); std::abort(); } + catch (const std::bad_variant_access& value) { if (std::strcmp(value.what(), "bad_variant_access") != 0) std::abort(); } + try { _ZSt26_Throw_bad_optional_accessv_nid_postfix(); std::abort(); } + catch (const std::bad_optional_access& value) { if (std::strcmp(value.what(), "bad_optional_access") != 0) std::abort(); } try { _ZNKSt9exception6_RaiseEv_nid_postfix(nullptr); assert(false); } catch (const std::invalid_argument&) {} try { _ZNKSt9exception6_RaiseEv_nid_postfix(&object); assert(false); } diff --git a/core/libs/tests/Fiber.cpp b/core/libs/tests/Fiber.cpp index 43ce315163..2267ee2497 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"); @@ -172,6 +172,29 @@ void APS5_VABI HostCallEntry(std::uint64_t, std::uint64_t) { for (;;) sceFiberReturnToThread(0, &received); } +alignas(16) std::array g_chainContext; +FiberStorage g_chainStorage; +bool g_chainEnded = false; + +void APS5_VABI FrameChainEntry(std::uint64_t, std::uint64_t) { + auto* frame = static_cast(__builtin_frame_address(0)); + const auto low = reinterpret_cast(frame); + const auto high = low + (1u << 20); + for (int depth = 0; depth < 100; ++depth) { + const auto address = reinterpret_cast(frame); + Require(address >= low && address + 2 * sizeof(std::uintptr_t) <= high, "frame chain stays on the fiber's stack"); + if (frame[1] == 0) { + g_chainEnded = frame[0] == 0; + break; + } + auto* next = reinterpret_cast(frame[0]); + Require(reinterpret_cast(next) > address, "frame chain climbs the stack to a frame whose return address is 0"); + frame = next; + } + std::uint64_t received = 0; + for (;;) sceFiberReturnToThread(0, &received); +} + void RunHostCallOnSmallContext() { auto* fiber = reinterpret_cast(&g_hostCallStorage); auto* context = g_hostCallArea.data() + g_hostCallArea.size() - HostCallContextSize; @@ -184,6 +207,34 @@ void RunHostCallOnSmallContext() { } } +void RunFrameChain() { + auto* fiber = reinterpret_cast(&g_chainStorage); + Require(_sceFiberInitializeImpl_nid_postfix(fiber, "chain", FrameChainEntry, 0, g_chainContext.data(), g_chainContext.size(), nullptr, 0) == 0, "initialize chain"); + Require(sceFiberRun_nid_postfix(fiber, 0, nullptr) == 0, "run chain"); + Require(g_chainEnded, "frame chain of a fiber ends at a frame whose saved frame pointer and return address are 0"); + 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); @@ -219,6 +270,7 @@ int main() { Require(sceFiberStartContextSizeCheck(1) == FiberErrorInvalid, "start with flags"); Require(_sceFiberInitializeImpl_nid_postfix(checked, "unchecked", CheckedEntry, 0, g_checkedContext.data(), g_checkedContext.size(), nullptr, 0) == 0, "initialize unchecked"); Require(sceFiberGetInfo(checked, &info) == 0 && info.size_context_margin == ~0ull, "no margin without size check"); + Require(info.entry == CheckedEntry, "info preserves the guest entry callback type"); Require(sceFiberFinalize(checked) == 0, "finalize unchecked"); Require(sceFiberStartContextSizeCheck(0) == 0, "start size check"); Require(sceFiberStartContextSizeCheck(0) == FiberErrorState, "start twice"); @@ -231,4 +283,6 @@ int main() { Require(sceFiberStopContextSizeCheck() == 0, "stop size check"); RunHostCallOnSmallContext(); + RunFrameChain(); + RunOnLargeContext(); } diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index ff80543263..2e11f4dfb2 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -1,15 +1,22 @@ #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include +#include #include #include #include #include #include #include +#include +#include +#include #include #ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include #include #else #include @@ -17,6 +24,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); @@ -28,11 +36,17 @@ int APS5_VABI fflush_nid_postfix(FileStream* stream); int APS5_VABI remove_nid_postfix(const char* path); int APS5_VABI rename_nid_postfix(const char* from, const char* to); int APS5_VABI ferror_nid_postfix(FileStream* stream); +int APS5_VABI feof_nid_postfix(FileStream* stream); void APS5_VABI clearerr_nid_postfix(FileStream* stream); +int APS5_VABI fgetc_nid_postfix(FileStream* stream); +int APS5_VABI getc_unlocked_nid_postfix(FileStream* stream); +char* APS5_VABI fgets_nid_postfix(char* buffer, int size, FileStream* stream); +int APS5_VABI ungetc_nid_postfix(int value, FileStream* stream); +int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...); } -static void Require(bool condition) { - 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 +71,136 @@ 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); + } +} + +static std::string ReadWhole(const std::string& path) { + auto* stream = fopen_nid_postfix(path.c_str(), "rb"); + Require(stream != nullptr); + std::array buffer{}; + const auto size = fread_nid_postfix(buffer.data(), 1, buffer.size(), stream); + Require(fclose_nid_postfix(stream) == 0); + return std::string(buffer.data(), size); +} + +template +static void CheckReadAfterWrite(const std::string& path, const char* mode, const char* expected, TRead read) { + auto* stream = fopen_nid_postfix(path.c_str(), "wb"); + Require(stream != nullptr && fputs_nid_postfix("0123456789", stream) >= 0 && fclose_nid_postfix(stream) == 0); + stream = fopen_nid_postfix(path.c_str(), mode); + Require(stream != nullptr && fwrite_nid_postfix("AB", 1, 2, stream) == 2); + read(stream); + Require(ferror_nid_postfix(stream) == 0, "Read after write on an update stream set the error indicator"); + Require(fclose_nid_postfix(stream) == 0); + Require(ReadWhole(path) == expected, "Read after write on an update stream changed the file"); +} + +static void CheckUpdateStreamReadsAfterWrites(const std::string& base) { + const std::string path = base + ".update"; + constexpr char overwritten[] = "AB23456789"; + CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) { + std::array buffer{}; + Require(fread_nid_postfix(buffer.data(), 1, 3, stream) == 3, "fread after fwrite on an r+ stream"); + Require(std::strcmp(buffer.data(), "234") == 0, "fread after fwrite on an r+ stream read other bytes"); + Require(ftell_nid_postfix(stream) == 5); + }); + CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) { + std::vector buffer(65536); + Require(fread_nid_postfix(buffer.data(), 1, buffer.size(), stream) == 8, "fread of more than the stream buffer after fwrite on an r+ stream"); + Require(std::string_view(buffer.data(), 8) == "23456789", "fread of more than the stream buffer after fwrite on an r+ stream read other bytes"); + Require(ftell_nid_postfix(stream) == 10); + }); + CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) { + Require(fgetc_nid_postfix(stream) == '2', "fgetc after fwrite on an r+ stream"); + }); + CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) { + Require(getc_unlocked_nid_postfix(stream) == '2', "getc_unlocked after fwrite on an r+ stream"); + }); + CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) { + std::array buffer{}; + Require(fgets_nid_postfix(buffer.data(), static_cast(buffer.size()), stream) == buffer.data(), "fgets after fwrite on an r+ stream"); + Require(std::strcmp(buffer.data(), "234") == 0, "fgets after fwrite on an r+ stream read other bytes"); + }); + CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) { + Require(ungetc_nid_postfix('Z', stream) == 'Z', "ungetc after fwrite on an r+ stream"); + Require(fgetc_nid_postfix(stream) == 'Z' && fgetc_nid_postfix(stream) == '2', "ungetc after fwrite on an r+ stream pushed back other bytes"); + }); + CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) { + std::array buffer{}; + Require(fscanf_nid_postfix(stream, "%3s", buffer.data()) == 1, "fscanf after fwrite on an r+ stream"); + Require(std::strcmp(buffer.data(), "234") == 0, "fscanf after fwrite on an r+ stream read other bytes"); + }); + CheckReadAfterWrite(path, "w+b", "AB", [](FileStream* stream) { + std::array buffer{}; + Require(fread_nid_postfix(buffer.data(), 1, buffer.size(), stream) == 0 && feof_nid_postfix(stream) != 0, "fread after fwrite on a w+ stream"); + }); + CheckReadAfterWrite(path, "a+b", "0123456789AB", [](FileStream* stream) { + Require(fgetc_nid_postfix(stream) == EOF && feof_nid_postfix(stream) != 0, "fgetc after fwrite on an a+ stream"); + }); + auto* stream = fopen_nid_postfix(path.c_str(), "r+b"); + Require(stream != nullptr); + Require(ungetc_nid_postfix('Z', stream) == 'Z', "ungetc on a new r+ stream"); + Require(fgetc_nid_postfix(stream) == 'Z' && fgetc_nid_postfix(stream) == '0', "ungetc on a new r+ stream pushed back other bytes"); + Require(fseek_nid_postfix(stream, 2, SEEK_SET) == 0 && ungetc_nid_postfix('Y', stream) == 'Y', "ungetc after fseek on an r+ stream"); + Require(fgetc_nid_postfix(stream) == 'Y' && fgetc_nid_postfix(stream) == '2', "ungetc after fseek on an r+ stream pushed back other bytes"); + Require(fclose_nid_postfix(stream) == 0); + Require(std::remove(path.c_str()) == 0); +} + int main(int argc, char** argv) { Require(argc == 2); + CheckExclusiveCppOpen(argv[1]); + CheckUpdateStreamReadsAfterWrites(argv[1]); Require(_Stdout_nid_postfix.GetHandle() == stdout); Require(_Stderr_nid_postfix.GetHandle() == stderr); Require(fputs_nid_postfix("stdout object works\n", &_Stdout_nid_postfix) >= 0); @@ -81,6 +223,11 @@ int main(int argc, char** argv) { ExpectException([&] { fputs_nid_postfix(nullptr, stream); }); ExpectException([&] { fwrite_nid_postfix(nullptr, 1, sizeof(payload), stream); }); errno = 0; + Require(fread_nid_postfix(buffer.data(), 2, SIZE_MAX / 2 + 1, stream) == 0 && errno == EINVAL && ferror_nid_postfix(stream) == 0); + errno = 0; + Require(fread_nid_postfix(buffer.data(), 2, SIZE_MAX / 2 + 1, nullptr) == 0 && errno == EINVAL); + ExpectException([&] { fwrite_nid_postfix(buffer.data(), SIZE_MAX / 2 + 1, 2, stream); }); + errno = 0; Require(fseek_nid_postfix(stream, 0, -1) == -1); Require(errno == EINVAL); Require(fclose_nid_postfix(stream) == 0); @@ -88,23 +235,16 @@ int main(int argc, char** argv) { ExpectException([] { fopen_nid_postfix(nullptr, "r"); }); FileStream closed(std::tmpfile()); closed.Close(); - ExpectException([&] { fflush_nid_postfix(&closed); }); + Require(fflush_nid_postfix(&closed) == -1 && errno == 9); Require(std::remove(argv[1]) == 0); errno = 0; Require(fopen_nid_postfix(argv[1], "rb") == nullptr); Require(errno == ENOENT); -#ifdef _WIN32 - const std::string directoryMode = "rb"; - constexpr int directoryError = EACCES; -#else - const std::string directoryMode = "wb"; - constexpr int directoryError = EISDIR; -#endif const std::string directoryName = std::string(argv[1]) + ".dir"; Require(std::filesystem::create_directory(directoryName)); errno = 0; - Require(fopen_nid_postfix(directoryName.c_str(), directoryMode.c_str()) == nullptr); - Require(errno == directoryError); + Require(fopen_nid_postfix(directoryName.c_str(), "wb") == nullptr); + Require(errno == EISDIR); Require(std::filesystem::remove(directoryName)); const std::u8string unicodeName = u8"セーブé.tmp"; const std::string guestName(unicodeName.begin(), unicodeName.end()); @@ -147,6 +287,17 @@ int main(int argc, char** argv) { std::array appended{}; Require(stream != nullptr && fread_nid_postfix(appended.data(), 1, 4, stream) == 4 && std::strcmp(appended.data(), "abcd") == 0); Require(fclose_nid_postfix(stream) == 0 && remove_nid_postfix(heldName.c_str()) == 0); + stream = fopen_nid_postfix(heldName.c_str(), "w+b"); + Require(stream != nullptr); + stream = freopen_nid_postfix(movedName.c_str(), "w+b", stream); + Require(stream != nullptr && fwrite_nid_postfix(payload, 1, sizeof(payload), stream) == sizeof(payload)); + Require(fflush_nid_postfix(stream) == 0); + Require(rename_nid_postfix(movedName.c_str(), heldName.c_str()) == 0 && !std::filesystem::exists(movedName)); + Require(remove_nid_postfix(heldName.c_str()) == 0 && !std::filesystem::exists(heldName)); + Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0); + std::array reopenedBuffer{}; + Require(fread_nid_postfix(reopenedBuffer.data(), 1, reopenedBuffer.size(), stream) == reopenedBuffer.size()); + Require(std::strcmp(reopenedBuffer.data(), payload) == 0 && fclose_nid_postfix(stream) == 0); constexpr char marker[] = "marker"; stream = fopen_nid_postfix(argv[1], "wb"); Require(stream != nullptr); diff --git a/core/libs/tests/FontFaceLifetime.cpp b/core/libs/tests/FontFaceLifetime.cpp new file mode 100644 index 0000000000..f14802f7fd --- /dev/null +++ b/core/libs/tests/FontFaceLifetime.cpp @@ -0,0 +1,77 @@ +#include "tests/FontFixture.hpp" +#include "prx/libSceFont/include/FontInternal.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool value, const char* step) { + if (!value) { + std::fprintf(stderr, "Font face lifetime: %s\n", step); + std::abort(); + } +} + +static void FinalizeFace(void* object) { + auto* face = static_cast(object); + auto* counter = static_cast*>(face->generic.data); + counter->fetch_add(1, std::memory_order_relaxed); +} + +static void ObserveFace(Font::FontState& state, std::atomic& destroyed) { + Require(state.face != nullptr, "font loaded"); + Require(state.face->generic.data == nullptr && state.face->generic.finalizer == nullptr, "unused finalizer observer"); + state.face->generic.data = &destroyed; + state.face->generic.finalizer = FinalizeFace; + Require(FT_Get_Char_Index(state.face, 'A') == 1, "character mapping preserved"); + Require(FT_Set_Pixel_Sizes(state.face, 32, 32) == 0, "set pixel scale"); + Require(FT_Load_Char(state.face, 'A', FT_LOAD_DEFAULT) == 0, "load glyph"); + Require(state.face->glyph->advance.x == 16 * 64, "glyph advance preserved"); +} + +static bool Run(unsigned workers, unsigned iterations, bool replace, bool churn, + const std::shared_ptr>& bytes, std::atomic& destroyed) { + std::array nativeFonts{}; + std::barrier phase(static_cast(workers)); + std::vector threads; + for (unsigned worker = 0; worker < workers; ++worker) { + threads.emplace_back([&, worker] { + const auto handle = reinterpret_cast(&nativeFonts[worker]); + for (unsigned iteration = 0; iteration < iterations; ++iteration) { + phase.arrive_and_wait(); + auto& state = replace && iteration != 0 ? *Font::TryGetState(handle) : Font::ResetState(handle); + Font::LoadStateFace(state, bytes, 0); + ObserveFace(state, destroyed); + if (!churn) phase.arrive_and_wait(); + if (!replace) Font::RemoveState(handle); + } + if (replace) Font::RemoveState(handle); + }); + } + for (auto& thread : threads) thread.join(); + const unsigned expected = workers * iterations; + std::printf("workers=%u iterations=%u replace=%d churn=%d actual_destructors=%u expected_destructors=%u\n", + workers, iterations, replace, churn, destroyed.load(), expected); + std::fflush(stdout); + return destroyed.load() == expected; +} + +int main(int argc, char** argv) { + Require(argc == 1 || (argc == 2 && (std::string_view(argv[1]) == "replace" || std::string_view(argv[1]) == "churn")), "test arguments"); + const bool replace = argc == 2 && std::string_view(argv[1]) == "replace"; + const bool churn = argc == 2 && std::string_view(argv[1]) == "churn"; + using FontFixture::BuildFont; + using FontFixture::Words; + const auto bytes = std::make_shared>(BuildFont(3, { + {"cmap", Words({0, 1, 3, 1, 0, 12, 4, 40, 0, 6, 4, 1, 2, 'A', 'V', 0xFFFF, 0, 'A', 'V', 0xFFFF, 1 - 'A', 2 - 'V', 1, 0, 0, 0})}, + })); + std::array, 2> destroyed{}; + bool passed = Run(1, 128, replace, churn, bytes, destroyed[0]); + passed = Run(8, 512, replace, churn, bytes, destroyed[1]) && passed; + return passed ? 0 : 1; +} diff --git a/core/libs/tests/FontFixture.hpp b/core/libs/tests/FontFixture.hpp new file mode 100644 index 0000000000..601512cd98 --- /dev/null +++ b/core/libs/tests/FontFixture.hpp @@ -0,0 +1,50 @@ +#ifndef CORE_LIBS_TESTS_FONTFIXTURE_HPP +#define CORE_LIBS_TESTS_FONTFIXTURE_HPP + +#include +#include +#include +#include +#include + +namespace FontFixture { + +inline void Put16(std::vector& out, int value) { + out.push_back(static_cast((value >> 8) & 0xFF)); + out.push_back(static_cast(value & 0xFF)); +} + +inline std::vector Words(std::initializer_list values) { + std::vector out; + for (const int value : values) Put16(out, value); + return out; +} + +inline std::vector BuildFont(int glyphCount, std::map> tables) { + tables["glyf"] = Words({0, 0}); + tables["head"] = Words({1, 0, 1, 0, 0, 0, 0x5F0F, 0x3CF5, 0, 1000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 2, 0, 0}); + tables["hhea"] = Words({1, 0, 800, -200, 0, 500, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1}); + tables["hmtx"] = Words({500}); + tables["hmtx"].resize(2 + 2 * static_cast(glyphCount)); + tables["loca"].assign(2 * (static_cast(glyphCount) + 1), 0); + tables["maxp"] = Words({1, 0, glyphCount, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}); + const int tableCount = static_cast(tables.size()); + int entrySelector = 0; + while ((2 << entrySelector) <= tableCount) ++entrySelector; + std::vector font = Words({1, 0, tableCount, 16 << entrySelector, entrySelector, 16 * tableCount - (16 << entrySelector)}); + int offset = 12 + 16 * tableCount; + for (const auto& table : tables) { + font.insert(font.end(), table.first.begin(), table.first.end()); + for (const int value : {0, 0, offset >> 16, offset & 0xFFFF, 0, static_cast(table.second.size())}) Put16(font, value); + offset += static_cast((table.second.size() + 3) & ~std::size_t{3}); + } + for (const auto& table : tables) { + font.insert(font.end(), table.second.begin(), table.second.end()); + font.resize((font.size() + 3) & ~std::size_t{3}); + } + return font; +} + +} + +#endif diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 8b2260536d..937f348f17 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -3,9 +3,11 @@ #include "prx/libc/include/GuestHeap.hpp" #include #include +#include #include #include #include +#include #include #include #include @@ -18,6 +20,7 @@ int APS5_VABI libc_printf_nid_postfix(const char*, ...); int APS5_VABI sscanf_nid_postfix(const char*, const char*, ...); int APS5_VABI vsnprintf_nid_postfix(char*, size_t, const char*, VaList*); int APS5_VABI vsnprintf_s_nid_postfix(char*, size_t, const char*, VaList*); +int APS5_VABI snprintf_s_nid_postfix(char*, size_t, const char*, ...); int APS5_VABI vsscanf_s_nid_postfix(const char*, const char*, VaList*); int APS5_VABI vsscanf_nid_postfix(const char*, const char*, VaList*); int APS5_VABI vprintf_nid_postfix(const char*, VaList*); @@ -82,6 +85,34 @@ static int APS5_VABI PrintList(const char* format, ...) { return result; } +static void CheckSizeLimits() { + using Formatter = int (APS5_VABI*)(char*, size_t, const char*, ...); + for (const Formatter format : {&snprintf_nid_postfix, &FormatList}) { + char output[8]; + for (const std::size_t oversized : {static_cast(INT_MAX) + 2, static_cast(SIZE_MAX)}) { + for (const bool wide : {false, true}) { + std::memset(output, 'Z', sizeof(output)); + int consumed = -1; + *__error_nid_postfix() = 0; + const int result = wide ? format(output, oversized, "%ls%n", u"x", &consumed) + : format(output, oversized, "x%n", &consumed); + Require(result == -1 && *__error_nid_postfix() == 84 && output[0] == 0 && consumed == -1); + Require(std::all_of(output + 1, output + sizeof(output), [](char value) { return value == 'Z'; })); + } + } + for (const std::size_t capacity : {std::size_t{0}, std::size_t{1}, static_cast(INT_MAX), static_cast(INT_MAX) + 1}) { + std::memset(output, 'Z', sizeof(output)); + *__error_nid_postfix() = 35; + Require(format(output, capacity, "x") == 1 && *__error_nid_postfix() == 35); + Require(output[0] == (capacity == 0 ? 'Z' : capacity == 1 ? '\0' : 'x')); + const std::size_t written = capacity < 2 ? capacity : 2; + if (capacity > 1) Require(output[1] == 0); + Require(std::all_of(output + written, output + sizeof(output), [](char value) { return value == 'Z'; })); + } + Require(format(nullptr, 0, "x") == 1); + } +} + static bool CheckWidePrecision() { const char16_t input[] = u"A\u00e9\u20ac\U0001f600Z"; const char* expected[] = {"", "A", "A", "A\xc3\xa9", "A\xc3\xa9", "A\xc3\xa9", "A\xc3\xa9\xe2\x82\xac", @@ -102,6 +133,8 @@ static bool CheckWidePrecision() { Require(snprintf_nid_postfix(output, sizeof(output), "[%5.1ls]", u"\u00e9") == 7 && std::strcmp(output, "[ ]") == 0); Require(snprintf_nid_postfix(output, sizeof(output), "[%-5.2ls]", u"\u00e9") == 7 && std::strcmp(output, "[\xc3\xa9 ]") == 0); Require(FormatList(output, sizeof(output), "%.3ls", u"\U0001f600") == 0 && output[0] == 0); + Require(FormatList(output, sizeof(output), "%ls[% u][% 4u][%+ lu][%- 3hhu][% x][%+o]", u"", 5u, 5u, 5ULL, 5u, 255u, 8u) == 25 + && std::strcmp(output, "[5][ 5][5][5 ][ff][10]") == 0); Require(FormatList(output, sizeof(output), "%.*ls:%d", -1, u"\u00e9", 7) == 4 && std::strcmp(output, "\xc3\xa9:7") == 0); const char16_t bounded[] = {u'A', u'B'}; Require(snprintf_nid_postfix(output, sizeof(output), "%.2ls", bounded) == 2 && std::strcmp(output, "AB") == 0); @@ -127,6 +160,40 @@ static bool CheckWidePrecision() { return correct; } +static void CheckSecureFormat() { + constexpr size_t rsizeMax = SIZE_MAX >> 1; + char buffer[16]; + std::memset(buffer, '!', sizeof(buffer)); + Require(FormatListS(buffer, 5, "%s", "abcdef") == 6 && std::strcmp(buffer, "abcd") == 0 && buffer[5] == '!'); + Require(snprintf_s_nid_postfix(buffer, 5, "%d-%ls", 42, u"abcd") == 7 && std::strcmp(buffer, "42-a") == 0); + Require(FormatListS(buffer, 1, "%x", 0xabcu) == 3 && buffer[0] == 0); + Require(snprintf_s_nid_postfix(buffer, sizeof(buffer), "") == 0 && buffer[0] == 0); + buffer[0] = 'x'; + Require(FormatListS(buffer, sizeof(buffer), nullptr) < 0 && buffer[0] == 0); + Require(FormatListS(nullptr, sizeof(buffer), "a") < 0 && snprintf_s_nid_postfix(nullptr, sizeof(buffer), "a") < 0); + buffer[0] = 'x'; + Require(FormatListS(buffer, 0, "a") < 0 && snprintf_s_nid_postfix(buffer, 0, "a") < 0 && buffer[0] == 'x'); + Require(FormatListS(buffer, rsizeMax + 1, "a") < 0 && snprintf_s_nid_postfix(buffer, rsizeMax + 1, "a") < 0 && buffer[0] == 'x'); + int written = -7; + Require(FormatListS(buffer, sizeof(buffer), "ab%n", &written) < 0 && buffer[0] == 0 && written == -7); + buffer[0] = 'x'; + Require(snprintf_s_nid_postfix(buffer, sizeof(buffer), "ab%hhn", &written) < 0 && buffer[0] == 0 && written == -7); + buffer[0] = 'x'; + Require(FormatListS(buffer, sizeof(buffer), "a%s", static_cast(nullptr)) < 0 && buffer[0] == 0); + buffer[0] = 'x'; + Require(snprintf_s_nid_postfix(buffer, sizeof(buffer), "a%ls", static_cast(nullptr)) < 0 && buffer[0] == 0); + buffer[0] = 'x'; + Require(FormatListS(buffer, sizeof(buffer), "%2$s%1$S", static_cast(nullptr), "a") < 0 && buffer[0] == 0); + Require(snprintf_s_nid_postfix(buffer, sizeof(buffer), "%.0s|%c", "", 'z') == 2 && std::strcmp(buffer, "|z") == 0); + bool threw = false; + try { + FormatListS(buffer, sizeof(buffer), "%y", 1); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); +} + __attribute__((noinline)) static void APS5_VABI RunChecks() { char buffer[1024]; Require(FormatList(buffer, sizeof(buffer), "Mount requested: %d", 0) == 18); @@ -175,23 +242,28 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(threw); } char expected[1024]; - std::snprintf(expected, sizeof(expected), "%#08x %.3e %a %g 0x%llx", 42u, 1.25, 1.25, 1.25, + std::snprintf(expected, sizeof(expected), "%#08x %.3e %a %g %llx", 42u, 1.25, 1.25, 1.25, static_cast(reinterpret_cast(buffer))); snprintf_nid_postfix(buffer, sizeof(buffer), "%#08x %.3e %a %g %p", 42u, 1.25, 1.25, 1.25, static_cast(buffer)); Require(std::strcmp(buffer, expected) == 0); -#ifdef _WIN32 char pointers[128]; const auto address = [](std::uintptr_t value) { return reinterpret_cast(value); }; - Require(snprintf_nid_postfix(pointers, sizeof(pointers), "%p|%p|%p", address(0), address(0x1234), address(0xdeadbeef12)) == 23 && - std::strcmp(pointers, "0x0|0x1234|0xdeadbeef12") == 0); - Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%8p][%-8p][%08p][%.6p][%10.4p][%.0p]", - address(0x1a), address(0x1a), address(0x1a), address(0x1a), address(0x1a), address(0)) == 56 && - std::strcmp(pointers, "[ 0x1a][0x1a ][0x00001a][0x00001a][ 0x001a][0x]") == 0); - Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%+p][% p][%#p][%*p][%-*p]", address(0x1a), address(0x1a), - address(0x1a), 6, address(0xf), -6, address(0xf)) == 34 && - std::strcmp(pointers, "[0x1a][0x1a][0x1a][ 0xf][0xf ]") == 0); - Require(snprintf_nid_postfix(pointers, 6, "%p", address(0xabcdef)) == 8 && std::strcmp(pointers, "0xabc") == 0); -#endif + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "%p|%p|%p", address(0), address(0x1234), address(0xdeadbeef12)) == 17 && + std::strcmp(pointers, "0|1234|deadbeef12") == 0); + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%p][%5p][%-5p][%05p][%.8p][%10.6p][%.0p]", + address(0x1234), address(0x1234), address(0x1234), address(0x1234), address(0x1234), address(0x1234), address(0)) == 51 && + std::strcmp(pointers, "[1234][ 1234][1234 ][01234][00001234][ 001234][]") == 0); + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%#p][%+p][% p][%lp][%20p][%-20p]", + address(0x1234), address(0x1234), address(0x1234), address(0x1234), address(0x1234), address(0x1234)) == 68 && + std::strcmp(pointers, "[1234][1234][1234][1234][ 1234][1234 ]") == 0); + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%08p][%.6p][%8p]", address(0x1a), address(0x1a), address(0xf)) == 28 && + std::strcmp(pointers, "[0000001a][00001a][ f]") == 0); + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%*p][%-*p]", 6, address(0x1a), -6, address(0x1a)) == 16 && + std::strcmp(pointers, "[ 1a][1a ]") == 0); + Require(FormatList(pointers, sizeof(pointers), "%2$p:%1$d", 7, address(0)) == 3 && std::strcmp(pointers, "0:7") == 0); + Require(snprintf_nid_postfix(pointers, 6, "%p", address(0xabcdef)) == 6 && std::strcmp(pointers, "abcde") == 0); + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "%p", address(0xffffffffffffffffull)) == 16 && + std::strcmp(pointers, "ffffffffffffffff") == 0); snprintf_nid_postfix(buffer, sizeof(buffer), "%.*s", -1, "unlimited"); Require(std::strcmp(buffer, "unlimited") == 0); buffer[5] = '!'; @@ -239,17 +311,50 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(sscanf_nid_postfix(" ", "%*d%d", &scanned) == EOF); Require(sscanf_nid_postfix("", "%d", &scanned) == EOF); #else - char16_t wide[4] = {u'!', u'!', u'!', u'!'}; - const auto rejects = [&](const char* format) { - try { sscanf_nid_postfix("ab cd", format, wide); } catch (const std::runtime_error&) { return wide[0] == u'!'; } - return false; - }; - Require(rejects("%ls") && rejects("%*s %3lc") && rejects("%l[a-z]") && rejects("%S") && rejects("%2$C") && rejects("%'ls")); - const auto secureRejects = [&](const char* format) { - try { ScanListS("ab cd", format, wide, 4u); } catch (const std::runtime_error&) { return wide[0] == u'!'; } - return false; - }; - Require(secureRejects("%ls") && secureRejects("%lc")); + { + char16_t first[8] = {}; + char16_t letter = u'!'; + Require(sscanf_nid_postfix("ab cd", "%ls %lc", first, &letter) == 2); + Require(std::u16string(first) == u"ab" && letter == u'c'); + Require(sscanf_nid_postfix(" xy z", "%S %C", first, &letter) == 2 && std::u16string(first) == u"xy" && letter == u'z'); + char16_t three[5] = {u'!', u'!', u'!', u'!', u'!'}; + Require(sscanf_nid_postfix("abcdef", "%3lc", three) == 1); + Require(three[0] == u'a' && three[1] == u'b' && three[2] == u'c' && three[3] == u'!'); + Require(sscanf_nid_postfix("abcz", "%l[a-c]", first) == 1 && std::u16string(first) == u"abc"); + Require(sscanf_nid_postfix("ab cd", "%l[^ ]", first) == 1 && std::u16string(first) == u"ab"); + Require(sscanf_nid_postfix("]x", "%l[]x]", first) == 1 && std::u16string(first) == u"]x"); + char16_t second[8] = {}; + Require(sscanf_nid_postfix("\xc5\xbelu\xc5\xa5 k\xc5\xaf\xc5\x88", "%ls %ls", first, second) == 2); + Require(std::u16string(first) == u"\u017elu\u0165" && std::u16string(second) == u"k\u016f\u0148"); + int number = 0; + Require(sscanf_nid_postfix("\xc5\xbelu\xc5\xa5 7", "%2ls%*ls %d", first, &number) == 2); + Require(std::u16string(first) == u"\u017el" && number == 7); + char narrowWord[8] = {}; + Require(sscanf_nid_postfix("12 ab cd", "%d %ls %7s", &number, first, narrowWord) == 3); + Require(number == 12 && std::u16string(first) == u"ab" && std::strcmp(narrowWord, "cd") == 0); + int consumed = -1; + Require(sscanf_nid_postfix("ab cd", "%ls%n", first, &consumed) == 1 && consumed == 2); + Require(sscanf_nid_postfix("", "%ls", first) == EOF && sscanf_nid_postfix(" ", "%ls", first) == EOF); + Require(sscanf_nid_postfix("xyz", "%l[a-c]", first) == 0); + Require(sscanf_nid_postfix("1 xyz", "%d %l[a-c]", &number, first) == 1); + Require(ScanList("hi there", "%ls %lc", first, &letter) == 2 && std::u16string(first) == u"hi" && letter == u't'); + char16_t fits[6] = {}; + Require(ScanListS("hello world", "%ls", fits, 6u) == 1 && std::u16string(fits) == u"hello"); + char16_t small[5] = {u'!', u'!', u'!', u'!', u'!'}; + Require(ScanListS("hello", "%ls", small, 5u) == 0 && small[0] == 0); + Require(ScanListS("hello", "%4ls", small, 5u) == 1 && std::u16string(small) == u"hell"); + Require(ScanListS("xy", "%lc", &letter, 1u) == 1 && letter == u'x'); + small[0] = u'!'; + Require(ScanListS("xyz", "%3lc", small, 2u) == 0 && small[0] == 0); + Require(ScanListS("ab cd", "%*ls %l[a-z]", fits, 6u) == 1 && std::u16string(fits) == u"cd"); + char16_t untouched[4] = {u'!', u'!', u'!', u'!'}; + const auto rejects = [&](const char* input, const char* format) { + try { sscanf_nid_postfix(input, format, untouched); } catch (const std::runtime_error&) { return untouched[0] == u'!'; } + return false; + }; + Require(rejects("\xff", "%ls") && rejects("a b", "%2$ls") && rejects("\xc5\xbe", "%l[\xc5\xbe]")); + Require(rejects("\xf0\x9f\x98\x80", "%ls")); + } char narrow[8] = {}; Require(sscanf_nid_postfix("ab%", "%7[^%ls]", narrow) == 1 && std::strcmp(narrow, "ab") == 0); #endif @@ -270,6 +375,7 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(libc_printf_nid_postfix("%d %d %d %d %d %d %d\n", 1, 22, 333, 4444, 55555, 666666, 7777777) == 35); Require(PrintList("vprintf: %d\n", 42) == 12); Require(FormatListS(buffer, sizeof(buffer), "%d-%s", 42, "x") == 4 && std::strcmp(buffer, "42-x") == 0); + CheckSecureFormat(); int scanNum = 0; char scanWord[8] = {}; Require(ScanListS("123 test", "%d %s", &scanNum, scanWord, 8u) == 2 && scanNum == 123 && std::strcmp(scanWord, "test") == 0); @@ -291,4 +397,4 @@ static void CheckAllocatingFormat() { free_nid_postfix(allocated); } -int main() { RunChecks(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } +int main() { CheckSizeLimits(); RunChecks(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } diff --git a/core/libs/tests/FreeTypeFontFixture.hpp b/core/libs/tests/FreeTypeFontFixture.hpp new file mode 100644 index 0000000000..9458d09a81 --- /dev/null +++ b/core/libs/tests/FreeTypeFontFixture.hpp @@ -0,0 +1,76 @@ +#ifndef CORE_LIBS_TESTS_FREETYPEFONTFIXTURE_HPP +#define CORE_LIBS_TESTS_FREETYPEFONTFIXTURE_HPP + +#include +#include +#include +#include + +static void Put16(std::vector& out, std::uint32_t value) { + out.push_back(static_cast(value >> 8)); + out.push_back(static_cast(value)); +} + +static void Put32(std::vector& out, std::uint32_t value) { + Put16(out, value >> 16); + Put16(out, value & 0xFFFFu); +} + +static void PutAll16(std::vector& out, std::initializer_list values) { + for (const int value : values) Put16(out, static_cast(static_cast(value))); +} + +static std::vector SquareGlyphFont(std::uint32_t codepoint, int width, bool symbolMap = false) { + std::vector head; + Put32(head, 0x00010000u); + Put32(head, 0x00010000u); + Put32(head, 0); + Put32(head, 0x5F0F3CF5u); + PutAll16(head, {0x000B, 1000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, width, 800, 0, 8, 2, 0, 0}); + std::vector hhea; + Put32(hhea, 0x00010000u); + PutAll16(hhea, {800, -200, 0, width, 0, 0, width, 1, 0, 0, 0, 0, 0, 0, 0, 2}); + std::vector maxp; + Put32(maxp, 0x00010000u); + PutAll16(maxp, {2, 4, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}); + std::vector hmtx; + PutAll16(hmtx, {width, 0, width, 100}); + std::vector glyf; + PutAll16(glyf, {1, 100, 0, width - 100, 700, 3, 0}); + glyf.insert(glyf.end(), {1, 1, 1, 1}); + PutAll16(glyf, {100, 0, width - 200, 0, 0, 700, 0, -700}); + std::vector loca; + PutAll16(loca, {0, 0, static_cast(glyf.size() / 2)}); + std::vector cmap; + PutAll16(cmap, {0, symbolMap ? 2 : 1, 3, 1}); + Put32(cmap, symbolMap ? 20 : 12); + if (symbolMap) { + PutAll16(cmap, {3, 0}); + Put32(cmap, 52); + } + PutAll16(cmap, {4, 32, 0, 4, 4, 1, 0, static_cast(codepoint), 0xFFFF, 0, static_cast(codepoint), 0xFFFF, 1 - static_cast(codepoint), 1, 0, 0}); + if (symbolMap) { + const int symbol = static_cast(codepoint + 0xF000); + PutAll16(cmap, {4, 32, 0, 4, 4, 1, 0, symbol, 0xFFFF, 0, symbol, 0xFFFF, 1 - symbol, 1, 0, 0}); + } + const std::vector*>> tables = { + {"cmap", &cmap}, {"glyf", &glyf}, {"head", &head}, {"hhea", &hhea}, {"hmtx", &hmtx}, {"loca", &loca}, {"maxp", &maxp}}; + std::vector font; + Put32(font, 0x00010000u); + PutAll16(font, {static_cast(tables.size()), 64, 2, static_cast(tables.size()) * 16 - 64}); + std::uint32_t offset = 12 + static_cast(tables.size()) * 16; + for (const auto& [tag, data] : tables) { + font.insert(font.end(), tag, tag + 4); + Put32(font, 0); + Put32(font, offset); + Put32(font, static_cast(data->size())); + offset += (static_cast(data->size()) + 3u) & ~3u; + } + for (const auto& [tag, data] : tables) { + font.insert(font.end(), data->begin(), data->end()); + font.resize((font.size() + 3u) & ~std::size_t{3}); + } + return font; +} + +#endif diff --git a/core/libs/tests/GuestAccess.cpp b/core/libs/tests/GuestAccess.cpp index 0cce786ad5..f4fa4fb8e8 100644 --- a/core/libs/tests/GuestAccess.cpp +++ b/core/libs/tests/GuestAccess.cpp @@ -38,6 +38,10 @@ int main() { Require(access_nid_postfix(nullptr, 0) == -1 && *__error_nid_postfix() == 14); Require(access_nid_postfix("sample.txt", 8) == -1 && *__error_nid_postfix() == 22); Require(access_nid_postfix("sample.txt", -1) == -1 && *__error_nid_postfix() == 22); + Require(access_nid_postfix(nullptr, 8) == -1 && *__error_nid_postfix() == 22); + Require(access_nid_postfix(nullptr, -1) == -1 && *__error_nid_postfix() == 22); + Require(access_nid_postfix("", 8) == -1 && *__error_nid_postfix() == 22); + Require(access_nid_postfix("", -1) == -1 && *__error_nid_postfix() == 22); #ifdef _WIN32 Require(SetFileAttributesW(file.c_str(), FILE_ATTRIBUTE_READONLY)); Require(access_nid_postfix("sample.txt", 4) == 0); diff --git a/core/libs/tests/GuestAcm.cpp b/core/libs/tests/GuestAcm.cpp index e360f84445..9ba39f03ee 100644 --- a/core/libs/tests/GuestAcm.cpp +++ b/core/libs/tests/GuestAcm.cpp @@ -2,6 +2,9 @@ #include #include #include +#include +#include +#include "SceTypes.hpp" extern "C" { int APS5_VABI sceAcmContextCreate(std::uint32_t*); @@ -9,33 +12,119 @@ int APS5_VABI sceAcmContextDestroy(std::uint32_t); int APS5_VABI sceAcmBatchStartBuffer(std::uint32_t, const void*, std::size_t, void*, std::uint32_t*); int APS5_VABI sceAcmBatchStartBuffers(std::uint32_t, std::uint32_t, const void* const[], void*, std::uint32_t*); int APS5_VABI sceAcmBatchWait(std::uint32_t, std::uint32_t, std::uint32_t); -int APS5_VABI sceAcm_ConvReverb_SharedInput(void); -int APS5_VABI sceAcm_FFT(void); -int APS5_VABI sceAcm_Panner(void); +int APS5_VABI sceAcm_ConvReverb_SharedInput(AcmBatchInfo*, std::uint32_t, AcmReverbInput*, std::uint32_t, const AcmReverbFilter* const*, const float*, const AcmReverbOutput* const*); +int APS5_VABI sceAcm_FFT(AcmBatchInfo*, std::int32_t, std::uint32_t, std::int32_t, const void* const*, std::int32_t, void* const*, std::int32_t); +int APS5_VABI sceAcm_Panner(AcmBatchInfo*, std::uint32_t, const void* const*, std::uint32_t, std::uint32_t, std::uint32_t, const void* const*, const void* const*, const void* const*, void* const*); } static void Require(bool value) { if (!value) std::abort(); } -int main() { - std::uint32_t context = 0; - Require(sceAcmContextCreate(nullptr) != 0); - Require(sceAcmContextCreate(&context) == 0 && context != 0); +constexpr int BadContext = static_cast(0x81940002); +constexpr int BadPointer = static_cast(0x81940006); - std::uint32_t batch = 0; - Require(sceAcmBatchStartBuffer(context, nullptr, 0, nullptr, nullptr) != 0); - Require(sceAcmBatchStartBuffer(context, nullptr, 0, nullptr, &batch) == 0 && batch != 0); - Require(sceAcmBatchWait(context, batch, 0) == 0); +int main() { + Require(sceAcmContextCreate(nullptr) == BadPointer); + Require(sceAcmContextDestroy(0) == BadContext); + Require(sceAcmContextDestroy(999999) == BadContext); + + std::uint32_t context1 = 0; + Require(sceAcmContextCreate(&context1) == 0 && context1 != 0); + + std::uint32_t context2 = 0; + Require(sceAcmContextCreate(&context2) == 0 && context2 != 0 && context2 != context1); + + std::uint32_t batch1 = 0; + Require(sceAcmBatchStartBuffer(0, nullptr, 0, nullptr, &batch1) == BadContext); + Require(sceAcmBatchStartBuffer(999999, nullptr, 0, nullptr, &batch1) == BadContext); + Require(sceAcmBatchStartBuffer(context1, nullptr, 0, nullptr, nullptr) == BadPointer); + Require(sceAcmBatchStartBuffer(context1, nullptr, 0, nullptr, &batch1) == 0 && batch1 != 0); + + Require(sceAcmBatchWait(0, batch1, 0) == BadContext); + Require(sceAcmBatchWait(999999, batch1, 0) == BadContext); + Require(sceAcmBatchWait(context1, batch1, 0) == 0); + Require(sceAcmBatchWait(context1, batch1, 0) == 0); std::uint32_t batch2 = 0; - Require(sceAcmBatchStartBuffers(context, 0, nullptr, nullptr, nullptr) != 0); - Require(sceAcmBatchStartBuffers(context, 0, nullptr, nullptr, &batch2) == 0 && batch2 != 0); - Require(sceAcmBatchWait(context, batch2, 0) == 0); + Require(sceAcmBatchStartBuffers(0, 0, nullptr, nullptr, &batch2) == BadContext); + Require(sceAcmBatchStartBuffers(999999, 0, nullptr, nullptr, &batch2) == BadContext); + Require(sceAcmBatchStartBuffers(context2, 0, nullptr, nullptr, nullptr) == BadPointer); + Require(sceAcmBatchStartBuffers(context2, 0, nullptr, nullptr, &batch2) == 0 && batch2 != 0); + Require(sceAcmBatchWait(context2, batch2, 0) == 0); - Require(sceAcm_ConvReverb_SharedInput() == 0); - Require(sceAcm_FFT() == 0); - Require(sceAcm_Panner() == 0); + Require(sceAcm_ConvReverb_SharedInput(nullptr, 0, nullptr, 0, nullptr, nullptr, nullptr) == 0); + Require(sceAcm_FFT(nullptr, 0, 0, 0, nullptr, 0, nullptr, 0) == BadPointer); + alignas(128) std::byte commands[256]{}; + alignas(128) std::uint32_t sourceData[32]{}; + alignas(128) const void* sources[16]{sourceData}; + alignas(128) void* outputs[16]{sourceData}; + AcmBatchInfo commandBatch{commands, 0, sizeof(commands)}; + Require(sceAcm_Panner(&commandBatch, 0, sources, 0, 0, 1, sources, sources, nullptr, outputs) == BadPointer); + sourceData[0] = 2; + Require(sceAcm_Panner(&commandBatch, 1, sources, 0, 0, 1, sources, sources, nullptr, outputs) == BadPointer); + sourceData[0] = 0; + outputs[0] = reinterpret_cast(sourceData) + 4; + Require(sceAcm_Panner(&commandBatch, 1, sources, 0, 0, 1, sources, sources, nullptr, outputs) == static_cast(0x8194000A)); + outputs[0] = sourceData; + Require(sceAcm_Panner(&commandBatch, 1, sources, 2, 1, 1, sources, sources, nullptr, outputs) == 0); + Require(commandBatch.offset == 32 && commandBatch.buffer_size == 128); + std::uint64_t commandWords[4]; + std::memcpy(commandWords, commands, sizeof(commandWords)); + Require(commandWords[0] == ((std::uint64_t{1} << 34) | (std::uint64_t{1} << 36) | 0x01010300)); + Require(commandWords[1] == 0x8000000000000D && commandWords[2] == reinterpret_cast(commands + 128) && commandWords[3] == 10); + std::uint64_t descriptor[16]; + std::memcpy(descriptor, commands + 128, sizeof(descriptor)); + Require(descriptor[0] == 1 && descriptor[1] == 0 && descriptor[2] == reinterpret_cast(outputs)); + Require(descriptor[3] == reinterpret_cast(sources) && descriptor[4] == 0x100000002 && descriptor[5] == 1); + Require(descriptor[6] == reinterpret_cast(sources) && descriptor[7] == reinterpret_cast(sources)); + for (std::size_t index = 8; index < 16; ++index) Require(descriptor[index] == 0); + commandBatch = {commands, 0, sizeof(commands)}; + Require(sceAcm_FFT(&commandBatch, 2048, 1, 0, sources, 0, outputs, 0) == BadPointer); + Require(sceAcm_FFT(&commandBatch, 512, 1, 0, sources, 0, outputs, 3) == BadPointer); + Require(sceAcm_FFT(&commandBatch, 512, 1, 0, sources, 0, outputs, 0) == 0); + Require(commandBatch.offset == 72 && commandBatch.buffer_size == sizeof(commands)); + std::uint64_t fftWords[9]; + std::memcpy(fftWords, commands, sizeof(fftWords)); + Require(fftWords[0] == 0x1C030400 && fftWords[1] == 0xC0A); + Require(fftWords[2] == 0x400000006 && fftWords[3] == reinterpret_cast(sourceData)); + Require(fftWords[4] == 0x90A && fftWords[5] == 0xB0A && fftWords[6] == 0x40A); + Require(fftWords[7] == 0x400000008 && fftWords[8] == reinterpret_cast(sourceData)); + commandBatch = {commands, 0, 71}; + Require(sceAcm_FFT(&commandBatch, 512, 1, 0, sources, 0, outputs, 0) == static_cast(0x81940009)); + Require(commandBatch.offset == 0); + commandBatch = {commands, 0, sizeof(commands)}; + Require(sceAcm_FFT(&commandBatch, 2048, 1, 1, sources, 1, outputs, 9) == 0); + std::memcpy(fftWords, commands, sizeof(fftWords)); + Require(fftWords[0] == 0x600 && fftWords[1] == 0x80A && fftWords[4] == 0x20A && fftWords[5] == 0x60A && fftWords[6] == 0x50A); + sources[1] = sourceData; + outputs[1] = sourceData; + for (const auto size : {512, 1024, 2048}) { + for (std::int32_t inputFormat = 0; inputFormat < 2; ++inputFormat) { + for (std::int32_t outputFormat = 0; outputFormat < 2; ++outputFormat) { + for (std::int32_t flags = 0; flags < 16; ++flags) { + commandBatch = {commands, 0, sizeof(commands)}; + const auto result = sceAcm_FFT(&commandBatch, size, 1, inputFormat, sources, outputFormat, outputs, flags); + if ((flags & 3) == 3 || (size == 2048 && !(flags & 1))) { + Require(result == BadPointer && commandBatch.offset == 0); + } else { + Require(result == 0 && commandBatch.offset == 72 + ((flags & 2) ? 8 : 0) + ((flags & 4) ? 8 : 0)); + } + } + } + } + } + commandBatch = {commands, 0, 152}; + Require(sceAcm_Panner(&commandBatch, 1, sources, 0, 0, 1, sources, sources, nullptr, outputs) == static_cast(0x81940009)); + Require(commandBatch.offset == 0 && commandBatch.buffer_size == 152); - Require(sceAcmContextDestroy(context) == 0); + Require(sceAcmContextDestroy(context1) == 0); + Require(sceAcmContextDestroy(context1) == BadContext); + Require(sceAcmBatchStartBuffer(context1, nullptr, 0, nullptr, &batch1) == BadContext); + Require(sceAcmBatchWait(context1, batch1, 0) == BadContext); + + Require(sceAcmContextDestroy(context2) == 0); + Require(sceAcmContextDestroy(context2) == BadContext); + + return 0; } diff --git a/core/libs/tests/GuestAddressConversion.cpp b/core/libs/tests/GuestAddressConversion.cpp index 675da4ed60..4d44b0a41b 100644 --- a/core/libs/tests/GuestAddressConversion.cpp +++ b/core/libs/tests/GuestAddressConversion.cpp @@ -3,14 +3,579 @@ #include #include #include +#include extern "C" { int APS5_VABI __inet_pton_nid_postfix(int, const char*, void*); const char* APS5_VABI __inet_ntop_nid_postfix(int, const void*, char*, std::uint32_t); +int APS5_VABI inet_pton_nid_postfix(int, const char*, void*); +const char* APS5_VABI inet_ntop_nid_postfix(int, const void*, char*, std::uint32_t); int* APS5_VABI __error_nid_postfix(); +std::uint32_t APS5_VABI htonl_nid_postfix(std::uint32_t); +std::uint32_t APS5_VABI ntohl_nid_postfix(std::uint32_t); +std::uint16_t APS5_VABI htons_nid_postfix(std::uint16_t); +std::uint16_t APS5_VABI ntohs_nid_postfix(std::uint16_t); } static void Require(bool value) { if (!value) std::abort(); } + +using Ntop = const char* (APS5_VABI *)(int, const void*, char*, std::uint32_t); + +struct PtonCase { bool plain; int family; const char* text; int result; int error; const char* bytes; }; +struct NtopCase { bool plain; int family; const char* source; std::uint32_t size; bool succeeds; int error; const char* bytes; std::size_t lastTouched; }; +struct PadCase { int family; const char* source; std::uint32_t size; std::size_t lastTouched; }; + +static const PtonCase ConsolePton[] = { + {false, 2, "1.2.3.4", 1, 0, "01020304aaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "0.0.0.0", 1, 0, "00000000aaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "255.255.255.255", 1, 0, "ffffffffaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "256.1.1.1", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "01.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.04", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "001.002.003.004", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.0", 1, 0, "01020300aaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "127.1", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.4.5", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1..2.3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, " 1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.4 ", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "0x1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.-1", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.4/24", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "::1", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::", 1, 0, "00000000000000000000000000000000"}, + {false, 28, "::1", 1, 0, "00000000000000000000000000000001"}, + {false, 28, "1::", 1, 0, "00010000000000000000000000000000"}, + {false, 28, "1:2:3:4:5:6:7:8", 1, 0, "00010002000300040005000600070008"}, + {false, 28, "1:2:3:4:5:6:7::", 1, 0, "00010002000300040005000600070000"}, + {false, 28, "::2:3:4:5:6:7:8", 1, 0, "00000002000300040005000600070008"}, + {false, 28, "1::2:3:4:5:6:7", 1, 0, "00010000000200030004000500060007"}, + {false, 28, "1:2:3:4:5:6:7:8:9", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1::2::3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::ffff:1.2.3.4", 1, 0, "00000000000000000000ffff01020304"}, + {false, 28, "::1.2.3.4", 1, 0, "00000000000000000000000001020304"}, + {false, 28, "1:2:3:4:5:6:1.2.3.4", 1, 0, "00010002000300040005000601020304"}, + {false, 28, "1:2:3:4:5:6:7:1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::1.2.3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::01.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "fe80::1%eth0", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "FE80::ABCD", 1, 0, "fe80000000000000000000000000abcd"}, + {false, 28, "12345::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "00001::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1:2:3:4:5:6:7:8:", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, ":1:2:3:4:5:6:7", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "0:0:0:0:0:0:0:0", 1, 0, "00000000000000000000000000000000"}, + {false, 28, "1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, ":", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, ":::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 9999, "1.2.3.4", -1, 47, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 0, "1.2.3.4", -1, 47, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "192.0.2.17", 1, 0, "c0000211aaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "not-an-address", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "256.1.2.3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "01.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.4.5", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.4.", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, " 1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "0x1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1..3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.-4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "255.0.7.100", 1, 0, "ff000764aaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "2001:db8::17", 1, 0, "20010db8000000000000000000000017"}, + {false, 28, "not-an-address", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::ffff:192.0.2.17", 1, 0, "00000000000000000000ffffc0000211"}, + {false, 28, "::ffff:01.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::ffff:1.2.3.04", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1:2:3:4:5:6:7:8::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::1:2:3:4:5:6:7:8", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1::2::3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, ":::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, ":1::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1::2:", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, ":1:2:3:4:5:6:7", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1:2:3:4:5:6:7", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1:2:3:4:5:6:7:8:9", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "12345::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "fe80::1%eth0", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1.2.3.4::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::1.2.3.4:1", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::g", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1:2:3:4:5:6:1.2.3.4:7", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1:2:3:4:5:6:7:1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1:2:3:4:5:6:7::", 1, 0, "00010002000300040005000600070000"}, + {false, 28, "ABCD:ef01::1", 1, 0, "abcdef01000000000000000000000001"}, + {false, 28, "::ffff:0.0.0.1", 1, 0, "00000000000000000000ffff00000001"}, + {false, 28, "::ffff:0:1:0", 1, 0, "0000000000000000ffff000000010000"}, + {false, 28, "::1", 1, 0, "00000000000000000000000000000001"}, + {false, 28, "::", 1, 0, "00000000000000000000000000000000"}, + {false, 28, "1::", 1, 0, "00010000000000000000000000000000"}, + {false, 28, "::2", 1, 0, "00000000000000000000000000000002"}, + {false, 28, "::1.2.3.4", 1, 0, "00000000000000000000000001020304"}, + {false, 28, "1:0:0:2:0:0:0:3", 1, 0, "00010000000000020000000000000003"}, + {false, 28, "1:0:2:0:3:0:4:0", 1, 0, "00010000000200000003000000040000"}, + {false, 28, "0:0:1:0:0:2:0:0", 1, 0, "00000000000100000000000200000000"}, + {false, 28, "::ffff:1.2.3.4", 1, 0, "00000000000000000000ffff01020304"}, + {false, 28, "2001:DB8::17", 1, 0, "20010db8000000000000000000000017"}, + {false, 28, "0:0:0:0:0:0:1:0", 1, 0, "00000000000000000000000000010000"}, + {false, 28, "::1:0", 1, 0, "00000000000000000000000000010000"}, + {false, 28, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 1, 0, "ffffffffffffffffffffffffffffffff"}, + {false, 10, "::1", -1, 47, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 9999, "1.2.3.4", -1, 47, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "192.0.2.17", 1, 0, "c0000211aaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "not-an-address", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "256.1.2.3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "01.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "1.2.3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "1.2.3.4.5", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "1.2.3.4.", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, " 1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "0x1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "1..3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "1.2.3.-4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "255.0.7.100", 1, 0, "ff000764aaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "2001:db8::17", 1, 0, "20010db8000000000000000000000017"}, + {true, 28, "not-an-address", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "::ffff:192.0.2.17", 1, 0, "00000000000000000000ffffc0000211"}, + {true, 28, "::ffff:01.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "::ffff:1.2.3.04", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1:2:3:4:5:6:7:8::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "::1:2:3:4:5:6:7:8", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1::2::3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, ":::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, ":1::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1::2:", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, ":1:2:3:4:5:6:7", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1:2:3:4:5:6:7", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1:2:3:4:5:6:7:8:9", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "12345::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "fe80::1%eth0", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1.2.3.4::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "::1.2.3.4:1", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "::g", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1:2:3:4:5:6:1.2.3.4:7", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1:2:3:4:5:6:7:1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1:2:3:4:5:6:7::", 1, 0, "00010002000300040005000600070000"}, + {true, 28, "ABCD:ef01::1", 1, 0, "abcdef01000000000000000000000001"}, + {true, 28, "::ffff:0.0.0.1", 1, 0, "00000000000000000000ffff00000001"}, + {true, 28, "::ffff:0:1:0", 1, 0, "0000000000000000ffff000000010000"}, + {true, 28, "::1", 1, 0, "00000000000000000000000000000001"}, + {true, 28, "::", 1, 0, "00000000000000000000000000000000"}, + {true, 28, "1::", 1, 0, "00010000000000000000000000000000"}, + {true, 28, "::2", 1, 0, "00000000000000000000000000000002"}, + {true, 28, "::1.2.3.4", 1, 0, "00000000000000000000000001020304"}, + {true, 28, "1:0:0:2:0:0:0:3", 1, 0, "00010000000000020000000000000003"}, + {true, 28, "1:0:2:0:3:0:4:0", 1, 0, "00010000000200000003000000040000"}, + {true, 28, "0:0:1:0:0:2:0:0", 1, 0, "00000000000100000000000200000000"}, + {true, 28, "::ffff:1.2.3.4", 1, 0, "00000000000000000000ffff01020304"}, + {true, 28, "2001:DB8::17", 1, 0, "20010db8000000000000000000000017"}, + {true, 28, "0:0:0:0:0:0:1:0", 1, 0, "00000000000000000000000000010000"}, + {true, 28, "::1:0", 1, 0, "00000000000000000000000000010000"}, + {true, 28, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 1, 0, "ffffffffffffffffffffffffffffffff"}, + {true, 10, "::1", -1, 47, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 9999, "1.2.3.4", -1, 47, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, +}; +static const NtopCase ConsoleNtop[] = { + {false, 2, "01020304", 16, true, 0, "312e322e332e34005a5a5a5a5a5a5a5a", 0}, + {false, 2, "00000000", 16, true, 0, "302e302e302e30005a5a5a5a5a5a5a5a", 0}, + {false, 2, "ffffffff", 16, true, 0, "3235352e3235352e3235352e32353500", 0}, + {false, 2, "01020304", 8, true, 0, "312e322e332e3400", 0}, + {false, 2, "01020304", 7, false, 28, "5a5a5a5a5a5a5a", 0}, + {false, 2, "01020304", 0, false, 28, "", 0}, + {false, 28, "00000000000000000000000000000000", 46, true, 0, "3a3a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00000000000000000000000000000001", 46, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00010000000000000000000000000000", 46, true, 0, "313a3a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00010000000000020000000000000003", 46, true, 0, "313a303a303a323a3a33000000000000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00010000000000020000000000030004", 46, true, 0, "313a3a323a303a303a333a3400000000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00010000000200030004000500060007", 46, true, 0, "313a303a323a333a343a353a363a3700000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00000000000000000000ffff01020304", 46, true, 0, "3a3a666666663a312e322e332e340000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00000000000000000000000001020304", 46, true, 0, "3a3a312e322e332e3400000000000000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00000000000000000000000000000002", 46, true, 0, "3a3a3200000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00000000000000000000ffff00000000", 46, true, 0, "3a3a666666663a302e302e302e300000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "0000000000000000ffff000001020304", 46, true, 0, "3a3a666666663a303a3130323a333034000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "abcd0000000000000000000000000000", 46, true, 0, "616263643a3a00000000000000000000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00000000000000000000000000000001", 4, true, 0, "3a3a3100", 0}, + {false, 28, "00000000000000000000000000000001", 3, false, 28, "5a5a5a", 0}, + {false, 28, "ffffffffffffffffffffffffffffffff", 46, true, 0, "666666663a666666663a666666663a666666663a666666663a666666663a666666663a6666666600000000000000", 0}, + {false, 28, "ffffffffffffffffffffffffffffffff", 40, true, 0, "666666663a666666663a666666663a666666663a666666663a666666663a666666663a6666666600", 0}, + {false, 28, "ffffffffffffffffffffffffffffffff", 39, false, 28, "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a", 0}, + {false, 28, "00000000000000000000ffffffffffff", 46, true, 0, "3a3a666666663a3235352e3235352e3235352e323535000000000000000000000000000000000000000000000000", 0}, + {false, 9999, "01020304000000000000000000000000", 16, false, 47, "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a", 0}, + {false, 2, "c0000211", 64, true, 0, "3139322e302e322e313700", 11}, + {false, 2, "c0000211", 11, true, 0, "3139322e302e322e313700", 11}, + {false, 2, "c0000211", 10, false, 28, "", 0}, + {false, 2, "c0000211", 1, false, 28, "", 0}, + {false, 2, "ff000764", 64, true, 0, "3235352e302e372e31303000", 12}, + {false, 2, "ff000764", 12, true, 0, "3235352e302e372e31303000", 12}, + {false, 2, "ff000764", 11, false, 28, "", 0}, + {false, 2, "ff000764", 1, false, 28, "", 0}, + {false, 28, "20010db8000000000000000000000017", 64, true, 0, "323030313a6462383a3a313700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "20010db8000000000000000000000017", 13, true, 0, "323030313a6462383a3a313700", 13}, + {false, 28, "20010db8000000000000000000000017", 12, false, 28, "", 0}, + {false, 28, "20010db8000000000000000000000017", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000ffffc0000211", 64, true, 0, "3a3a666666663a3139322e302e322e31370000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000ffffc0000211", 18, true, 0, "3a3a666666663a3139322e302e322e313700", 18}, + {false, 28, "00000000000000000000ffffc0000211", 17, false, 28, "", 0}, + {false, 28, "00000000000000000000ffffc0000211", 1, false, 28, "", 0}, + {false, 28, "00010002000300040005000600070000", 64, true, 0, "313a323a333a343a353a363a373a3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00010002000300040005000600070000", 16, true, 0, "313a323a333a343a353a363a373a3000", 16}, + {false, 28, "00010002000300040005000600070000", 15, false, 28, "", 0}, + {false, 28, "00010002000300040005000600070000", 1, false, 28, "", 0}, + {false, 28, "abcdef01000000000000000000000001", 64, true, 0, "616263643a656630313a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "abcdef01000000000000000000000001", 13, true, 0, "616263643a656630313a3a3100", 13}, + {false, 28, "abcdef01000000000000000000000001", 12, false, 28, "", 0}, + {false, 28, "abcdef01000000000000000000000001", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000ffff00000001", 64, true, 0, "3a3a666666663a302e302e302e310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000ffff00000001", 15, true, 0, "3a3a666666663a302e302e302e3100", 15}, + {false, 28, "00000000000000000000ffff00000001", 14, false, 28, "", 0}, + {false, 28, "00000000000000000000ffff00000001", 1, false, 28, "", 0}, + {false, 28, "0000000000000000ffff000000010000", 64, true, 0, "3a3a666666663a303a313a3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "0000000000000000ffff000000010000", 13, true, 0, "3a3a666666663a303a313a3000", 13}, + {false, 28, "0000000000000000ffff000000010000", 12, false, 28, "", 0}, + {false, 28, "0000000000000000ffff000000010000", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000000000000001", 64, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000000000000001", 4, true, 0, "3a3a3100", 4}, + {false, 28, "00000000000000000000000000000001", 3, false, 28, "", 0}, + {false, 28, "00000000000000000000000000000001", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000000000000000", 64, true, 0, "3a3a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000000000000000", 3, true, 0, "3a3a00", 3}, + {false, 28, "00000000000000000000000000000000", 2, false, 28, "", 0}, + {false, 28, "00000000000000000000000000000000", 1, false, 28, "", 0}, + {false, 28, "00010000000000000000000000000000", 64, true, 0, "313a3a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00010000000000000000000000000000", 4, true, 0, "313a3a00", 4}, + {false, 28, "00010000000000000000000000000000", 3, false, 28, "", 0}, + {false, 28, "00010000000000000000000000000000", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000000000000002", 64, true, 0, "3a3a3200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000000000000002", 4, true, 0, "3a3a3200", 4}, + {false, 28, "00000000000000000000000000000002", 3, false, 28, "", 0}, + {false, 28, "00000000000000000000000000000002", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000000001020304", 64, true, 0, "3a3a312e322e332e3400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000000001020304", 10, true, 0, "3a3a312e322e332e3400", 10}, + {false, 28, "00000000000000000000000001020304", 9, false, 28, "", 0}, + {false, 28, "00000000000000000000000001020304", 1, false, 28, "", 0}, + {false, 28, "00010000000000020000000000000003", 64, true, 0, "313a303a303a323a3a33000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00010000000000020000000000000003", 11, true, 0, "313a303a303a323a3a3300", 11}, + {false, 28, "00010000000000020000000000000003", 10, false, 28, "", 0}, + {false, 28, "00010000000000020000000000000003", 1, false, 28, "", 0}, + {false, 28, "00010000000200000003000000040000", 64, true, 0, "313a303a323a303a333a303a343a3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00010000000200000003000000040000", 16, true, 0, "313a303a323a303a333a303a343a3000", 16}, + {false, 28, "00010000000200000003000000040000", 15, false, 28, "", 0}, + {false, 28, "00010000000200000003000000040000", 1, false, 28, "", 0}, + {false, 28, "00000000000100000000000200000000", 64, true, 0, "3a3a313a303a303a323a303a30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000100000000000200000000", 14, true, 0, "3a3a313a303a303a323a303a3000", 14}, + {false, 28, "00000000000100000000000200000000", 13, false, 28, "", 0}, + {false, 28, "00000000000100000000000200000000", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000ffff01020304", 64, true, 0, "3a3a666666663a312e322e332e340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000ffff01020304", 15, true, 0, "3a3a666666663a312e322e332e3400", 15}, + {false, 28, "00000000000000000000ffff01020304", 14, false, 28, "", 0}, + {false, 28, "00000000000000000000ffff01020304", 1, false, 28, "", 0}, + {false, 28, "20010db8000000000000000000000017", 64, true, 0, "323030313a6462383a3a313700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "20010db8000000000000000000000017", 13, true, 0, "323030313a6462383a3a313700", 13}, + {false, 28, "20010db8000000000000000000000017", 12, false, 28, "", 0}, + {false, 28, "20010db8000000000000000000000017", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000000000010000", 64, true, 0, "3a3a302e312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000000000010000", 10, true, 0, "3a3a302e312e302e3000", 10}, + {false, 28, "00000000000000000000000000010000", 9, false, 28, "", 0}, + {false, 28, "00000000000000000000000000010000", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000000000010000", 64, true, 0, "3a3a302e312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000000000010000", 10, true, 0, "3a3a302e312e302e3000", 10}, + {false, 28, "00000000000000000000000000010000", 9, false, 28, "", 0}, + {false, 28, "00000000000000000000000000010000", 1, false, 28, "", 0}, + {false, 28, "ffffffffffffffffffffffffffffffff", 64, true, 0, "666666663a666666663a666666663a666666663a666666663a666666663a666666663a6666666600000000000000000000000000000000000000000000000000", 64}, + {false, 28, "ffffffffffffffffffffffffffffffff", 40, true, 0, "666666663a666666663a666666663a666666663a666666663a666666663a666666663a6666666600", 40}, + {false, 28, "ffffffffffffffffffffffffffffffff", 39, false, 28, "", 0}, + {false, 28, "ffffffffffffffffffffffffffffffff", 1, false, 28, "", 0}, + {false, 10, "01020304", 64, false, 47, "", 0}, + {false, 9999, "01020304", 64, false, 47, "", 0}, + {true, 2, "c0000211", 64, true, 0, "3139322e302e322e313700", 11}, + {true, 2, "c0000211", 11, true, 0, "3139322e302e322e313700", 11}, + {true, 2, "c0000211", 10, false, 28, "", 0}, + {true, 2, "c0000211", 1, false, 28, "", 0}, + {true, 2, "ff000764", 64, true, 0, "3235352e302e372e31303000", 12}, + {true, 2, "ff000764", 12, true, 0, "3235352e302e372e31303000", 12}, + {true, 2, "ff000764", 11, false, 28, "", 0}, + {true, 2, "ff000764", 1, false, 28, "", 0}, + {true, 28, "20010db8000000000000000000000017", 64, true, 0, "323030313a6462383a3a313700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "20010db8000000000000000000000017", 13, true, 0, "323030313a6462383a3a313700", 13}, + {true, 28, "20010db8000000000000000000000017", 12, false, 28, "", 0}, + {true, 28, "20010db8000000000000000000000017", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000ffffc0000211", 64, true, 0, "3a3a666666663a3139322e302e322e31370000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000ffffc0000211", 18, true, 0, "3a3a666666663a3139322e302e322e313700", 18}, + {true, 28, "00000000000000000000ffffc0000211", 17, false, 28, "", 0}, + {true, 28, "00000000000000000000ffffc0000211", 1, false, 28, "", 0}, + {true, 28, "00010002000300040005000600070000", 64, true, 0, "313a323a333a343a353a363a373a3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00010002000300040005000600070000", 16, true, 0, "313a323a333a343a353a363a373a3000", 16}, + {true, 28, "00010002000300040005000600070000", 15, false, 28, "", 0}, + {true, 28, "00010002000300040005000600070000", 1, false, 28, "", 0}, + {true, 28, "abcdef01000000000000000000000001", 64, true, 0, "616263643a656630313a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "abcdef01000000000000000000000001", 13, true, 0, "616263643a656630313a3a3100", 13}, + {true, 28, "abcdef01000000000000000000000001", 12, false, 28, "", 0}, + {true, 28, "abcdef01000000000000000000000001", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000ffff00000001", 64, true, 0, "3a3a666666663a302e302e302e310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000ffff00000001", 15, true, 0, "3a3a666666663a302e302e302e3100", 15}, + {true, 28, "00000000000000000000ffff00000001", 14, false, 28, "", 0}, + {true, 28, "00000000000000000000ffff00000001", 1, false, 28, "", 0}, + {true, 28, "0000000000000000ffff000000010000", 64, true, 0, "3a3a666666663a303a313a3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "0000000000000000ffff000000010000", 13, true, 0, "3a3a666666663a303a313a3000", 13}, + {true, 28, "0000000000000000ffff000000010000", 12, false, 28, "", 0}, + {true, 28, "0000000000000000ffff000000010000", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000000000000001", 64, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000000000000001", 4, true, 0, "3a3a3100", 4}, + {true, 28, "00000000000000000000000000000001", 3, false, 28, "", 0}, + {true, 28, "00000000000000000000000000000001", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000000000000000", 64, true, 0, "3a3a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000000000000000", 3, true, 0, "3a3a00", 3}, + {true, 28, "00000000000000000000000000000000", 2, false, 28, "", 0}, + {true, 28, "00000000000000000000000000000000", 1, false, 28, "", 0}, + {true, 28, "00010000000000000000000000000000", 64, true, 0, "313a3a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00010000000000000000000000000000", 4, true, 0, "313a3a00", 4}, + {true, 28, "00010000000000000000000000000000", 3, false, 28, "", 0}, + {true, 28, "00010000000000000000000000000000", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000000000000002", 64, true, 0, "3a3a3200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000000000000002", 4, true, 0, "3a3a3200", 4}, + {true, 28, "00000000000000000000000000000002", 3, false, 28, "", 0}, + {true, 28, "00000000000000000000000000000002", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000000001020304", 64, true, 0, "3a3a312e322e332e3400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000000001020304", 10, true, 0, "3a3a312e322e332e3400", 10}, + {true, 28, "00000000000000000000000001020304", 9, false, 28, "", 0}, + {true, 28, "00000000000000000000000001020304", 1, false, 28, "", 0}, + {true, 28, "00010000000000020000000000000003", 64, true, 0, "313a303a303a323a3a33000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00010000000000020000000000000003", 11, true, 0, "313a303a303a323a3a3300", 11}, + {true, 28, "00010000000000020000000000000003", 10, false, 28, "", 0}, + {true, 28, "00010000000000020000000000000003", 1, false, 28, "", 0}, + {true, 28, "00010000000200000003000000040000", 64, true, 0, "313a303a323a303a333a303a343a3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00010000000200000003000000040000", 16, true, 0, "313a303a323a303a333a303a343a3000", 16}, + {true, 28, "00010000000200000003000000040000", 15, false, 28, "", 0}, + {true, 28, "00010000000200000003000000040000", 1, false, 28, "", 0}, + {true, 28, "00000000000100000000000200000000", 64, true, 0, "3a3a313a303a303a323a303a30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000100000000000200000000", 14, true, 0, "3a3a313a303a303a323a303a3000", 14}, + {true, 28, "00000000000100000000000200000000", 13, false, 28, "", 0}, + {true, 28, "00000000000100000000000200000000", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000ffff01020304", 64, true, 0, "3a3a666666663a312e322e332e340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000ffff01020304", 15, true, 0, "3a3a666666663a312e322e332e3400", 15}, + {true, 28, "00000000000000000000ffff01020304", 14, false, 28, "", 0}, + {true, 28, "00000000000000000000ffff01020304", 1, false, 28, "", 0}, + {true, 28, "20010db8000000000000000000000017", 64, true, 0, "323030313a6462383a3a313700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "20010db8000000000000000000000017", 13, true, 0, "323030313a6462383a3a313700", 13}, + {true, 28, "20010db8000000000000000000000017", 12, false, 28, "", 0}, + {true, 28, "20010db8000000000000000000000017", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000000000010000", 64, true, 0, "3a3a302e312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000000000010000", 10, true, 0, "3a3a302e312e302e3000", 10}, + {true, 28, "00000000000000000000000000010000", 9, false, 28, "", 0}, + {true, 28, "00000000000000000000000000010000", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000000000010000", 64, true, 0, "3a3a302e312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000000000010000", 10, true, 0, "3a3a302e312e302e3000", 10}, + {true, 28, "00000000000000000000000000010000", 9, false, 28, "", 0}, + {true, 28, "00000000000000000000000000010000", 1, false, 28, "", 0}, + {true, 28, "ffffffffffffffffffffffffffffffff", 64, true, 0, "666666663a666666663a666666663a666666663a666666663a666666663a666666663a6666666600000000000000000000000000000000000000000000000000", 64}, + {true, 28, "ffffffffffffffffffffffffffffffff", 40, true, 0, "666666663a666666663a666666663a666666663a666666663a666666663a666666663a6666666600", 40}, + {true, 28, "ffffffffffffffffffffffffffffffff", 39, false, 28, "", 0}, + {true, 28, "ffffffffffffffffffffffffffffffff", 1, false, 28, "", 0}, + {true, 10, "01020304", 64, false, 47, "", 0}, + {true, 9999, "01020304", 64, false, 47, "", 0}, + {false, 28, "00000000000000000000000000000001", 65, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 65}, + {false, 2, "01020304", 65, true, 0, "312e322e332e3400", 8}, + {false, 28, "00000000000000000000000000000001", 128, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 128}, + {false, 2, "01020304", 128, true, 0, "312e322e332e3400", 8}, + {false, 28, "00000000000000000000000000000001", 1024, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 1024}, + {false, 2, "01020304", 1024, true, 0, "312e322e332e3400", 8}, + {false, 28, "00000000000000000000000000000001", 4096, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 4096}, + {false, 2, "01020304", 4096, true, 0, "312e322e332e3400", 8}, + {false, 28, "00000000000000000000000000000001", 65536, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 65536}, + {false, 2, "01020304", 65536, true, 0, "312e322e332e3400", 8}, + {true, 28, "00000000000000000000000000000001", 65, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 65}, + {true, 2, "01020304", 65, true, 0, "312e322e332e3400", 8}, + {true, 28, "00000000000000000000000000000001", 128, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 128}, + {true, 2, "01020304", 128, true, 0, "312e322e332e3400", 8}, + {true, 28, "00000000000000000000000000000001", 1024, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 1024}, + {true, 2, "01020304", 1024, true, 0, "312e322e332e3400", 8}, + {true, 28, "00000000000000000000000000000001", 4096, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 4096}, + {true, 2, "01020304", 4096, true, 0, "312e322e332e3400", 8}, + {true, 28, "00000000000000000000000000000001", 65536, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 65536}, + {true, 2, "01020304", 65536, true, 0, "312e322e332e3400", 8}, +}; +static const PadCase ConsolePaddingBoth[] = { + {28, "00000000000000000000000000000001", 4, 4}, + {28, "00000000000000000000000000000001", 5, 5}, + {28, "00000000000000000000000000000001", 16, 16}, + {28, "00000000000000000000000000000001", 45, 45}, + {28, "00000000000000000000000000000001", 46, 46}, + {28, "00000000000000000000000000000001", 47, 47}, + {28, "00000000000000000000000000000001", 64, 64}, + {28, "ffffffffffffffffffffffffffffffff", 64, 64}, + {2, "01020304", 8, 8}, + {2, "01020304", 16, 8}, + {2, "01020304", 64, 8}, +}; +static const PadCase ConsolePaddingLarge[] = { + {28, "00000000000000000000000000000001", 65, 65}, + {28, "00000000000000000000000000000001", 128, 128}, + {28, "00000000000000000000000000000001", 1024, 1024}, + {28, "00000000000000000000000000000001", 4096, 4096}, + {28, "00000000000000000000000000000001", 65536, 65536}, + {2, "01020304", 65, 8}, + {2, "01020304", 128, 8}, + {2, "01020304", 1024, 8}, + {2, "01020304", 4096, 8}, + {2, "01020304", 65536, 8}, +}; +static const PadCase ConsolePaddingUnderscored[] = { + {28, "00000000000000000000000000000001", 64, 64}, + {28, "00000000000000000000000000000001", 65, 65}, + {28, "00000000000000000000000000000001", 128, 128}, + {28, "00000000000000000000000000000001", 256, 256}, + {28, "00000000000000000000000000000001", 1024, 1024}, + {2, "01020304", 64, 8}, + {2, "01020304", 65, 8}, + {2, "01020304", 128, 8}, + {2, "01020304", 256, 8}, + {2, "01020304", 1024, 8}, +}; + +static unsigned HexValue(char digit) { return digit <= '9' ? static_cast(digit - '0') : static_cast(digit - 'a' + 10); } + +static std::size_t FromHex(const char* hex, unsigned char* out) { + std::size_t index = 0; + for (; hex[2 * index]; ++index) out[index] = static_cast(HexValue(hex[2 * index]) << 4 | HexValue(hex[2 * index + 1])); + return index; +} + +static bool SameBytes(const void* data, const char* hex) { + const auto* bytes = static_cast(data); + for (std::size_t index = 0; hex[2 * index]; ++index) + if (bytes[index] != (HexValue(hex[2 * index]) << 4 | HexValue(hex[2 * index + 1]))) return false; + return true; +} + +template +static bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +static char Wide[70000]; + +static void CheckConsolePton() { + for (const PtonCase& sample : ConsolePton) { + unsigned char destination[16]; + std::memset(destination, 0xAA, sizeof(destination)); + *__error_nid_postfix() = 0; + const int result = sample.plain ? inet_pton_nid_postfix(sample.family, sample.text, destination) + : __inet_pton_nid_postfix(sample.family, sample.text, destination); + Require(result == sample.result); + Require(*__error_nid_postfix() == sample.error); + Require(SameBytes(destination, sample.bytes)); + } +} + +static void CheckConsoleNtop() { + for (const NtopCase& sample : ConsoleNtop) { + unsigned char source[16]{}; + FromHex(sample.source, source); + std::memset(Wide, 'Z', sizeof(Wide)); + *__error_nid_postfix() = 0; + const char* result = sample.plain ? inet_ntop_nid_postfix(sample.family, source, Wide, sample.size) + : __inet_ntop_nid_postfix(sample.family, source, Wide, sample.size); + Require(result == (sample.succeeds ? Wide : nullptr)); + Require(*__error_nid_postfix() == sample.error); + Require(SameBytes(Wide, sample.bytes)); + if (!sample.lastTouched && sample.succeeds) continue; + const std::size_t shown = std::strlen(sample.bytes) / 2; + for (std::size_t index = shown; index < sample.lastTouched; ++index) Require(Wide[index] != 'Z'); + for (std::size_t index = sample.lastTouched; index < sizeof(Wide); ++index) Require(Wide[index] == 'Z'); + } +} + +static void CheckConsolePadding(const PadCase* samples, std::size_t count, Ntop ntop) { + for (std::size_t sample = 0; sample < count; ++sample) { + unsigned char source[16]{}; + FromHex(samples[sample].source, source); + std::memset(Wide, 'Z', sizeof(Wide)); + Require(ntop(samples[sample].family, source, Wide, samples[sample].size) == Wide); + for (std::size_t index = std::strlen(Wide); index < samples[sample].lastTouched; ++index) Require(Wide[index] == '\0'); + for (std::size_t index = samples[sample].lastTouched; index < sizeof(Wide); ++index) Require(Wide[index] == 'Z'); + } +} + +static void CheckNullPointers() { + const unsigned char loopback[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; + const unsigned char quad[16] = {1, 2, 3, 4}; + unsigned char bytes[16]{}; + char text[64]; + Require(Throws([&] { __inet_ntop_nid_postfix(28, nullptr, text, sizeof(text)); })); + Require(Throws([&] { __inet_ntop_nid_postfix(2, nullptr, text, sizeof(text)); })); + Require(Throws([&] { __inet_ntop_nid_postfix(28, loopback, nullptr, 46); })); + Require(Throws([&] { __inet_ntop_nid_postfix(2, quad, nullptr, 16); })); + Require(Throws([&] { inet_ntop_nid_postfix(28, nullptr, text, sizeof(text)); })); + Require(Throws([&] { inet_ntop_nid_postfix(28, loopback, nullptr, 46); })); + Require(Throws([&] { __inet_ntop_nid_postfix(2, nullptr, text, 0); })); + Require(Throws([&] { __inet_ntop_nid_postfix(28, nullptr, text, 1); })); + Require(Throws([&] { inet_ntop_nid_postfix(2, nullptr, text, 0); })); + Require(Throws([&] { inet_ntop_nid_postfix(28, nullptr, text, 1); })); + for (Ntop ntop : {__inet_ntop_nid_postfix, inet_ntop_nid_postfix}) { + *__error_nid_postfix() = 0; + Require(ntop(28, loopback, nullptr, 3) == nullptr && *__error_nid_postfix() == 28); + *__error_nid_postfix() = 0; + Require(ntop(2, quad, nullptr, 7) == nullptr && *__error_nid_postfix() == 28); + *__error_nid_postfix() = 0; + Require(ntop(2, quad, nullptr, 0) == nullptr && *__error_nid_postfix() == 28); + } + Require(Throws([&] { __inet_pton_nid_postfix(2, nullptr, bytes); })); + Require(Throws([&] { __inet_pton_nid_postfix(28, nullptr, bytes); })); + Require(Throws([&] { __inet_pton_nid_postfix(2, "1.2.3.4", nullptr); })); + Require(Throws([&] { __inet_pton_nid_postfix(28, "::1", nullptr); })); + Require(Throws([&] { inet_pton_nid_postfix(2, nullptr, bytes); })); + Require(Throws([&] { inet_pton_nid_postfix(2, "1.2.3.4", nullptr); })); + *__error_nid_postfix() = 0; + Require(__inet_pton_nid_postfix(2, "1.2.3", nullptr) == 0 && *__error_nid_postfix() == 0); + Require(__inet_pton_nid_postfix(28, "zzz", nullptr) == 0 && *__error_nid_postfix() == 0); + Require(__inet_pton_nid_postfix(2, "", nullptr) == 0 && *__error_nid_postfix() == 0); + Require(inet_pton_nid_postfix(2, "1.2.3", nullptr) == 0 && *__error_nid_postfix() == 0); + Require(inet_pton_nid_postfix(28, "zzz", nullptr) == 0 && *__error_nid_postfix() == 0); + Require(inet_pton_nid_postfix(2, "", nullptr) == 0 && *__error_nid_postfix() == 0); + *__error_nid_postfix() = 0; + Require(__inet_ntop_nid_postfix(9999, nullptr, text, sizeof(text)) == nullptr && *__error_nid_postfix() == 47); + *__error_nid_postfix() = 0; + Require(__inet_ntop_nid_postfix(9999, bytes, nullptr, 46) == nullptr && *__error_nid_postfix() == 47); + *__error_nid_postfix() = 0; + Require(__inet_pton_nid_postfix(9999, nullptr, bytes) == -1 && *__error_nid_postfix() == 47); + *__error_nid_postfix() = 0; + Require(__inet_pton_nid_postfix(9999, "1.2.3.4", nullptr) == -1 && *__error_nid_postfix() == 47); + *__error_nid_postfix() = 0; + Require(inet_ntop_nid_postfix(9999, nullptr, text, sizeof(text)) == nullptr && *__error_nid_postfix() == 47); + *__error_nid_postfix() = 0; + Require(inet_ntop_nid_postfix(9999, quad, nullptr, 46) == nullptr && *__error_nid_postfix() == 47); + *__error_nid_postfix() = 0; + Require(inet_pton_nid_postfix(9999, nullptr, bytes) == -1 && *__error_nid_postfix() == 47); + *__error_nid_postfix() = 0; + Require(inet_pton_nid_postfix(9999, "1.2.3.4", nullptr) == -1 && *__error_nid_postfix() == 47); +} + +static void CheckErrnoKept() { + unsigned char bytes[16]{}; + char text[64]; + *__error_nid_postfix() = 13; + Require(__inet_pton_nid_postfix(2, "1.2.3.4", bytes) == 1 && *__error_nid_postfix() == 13); + Require(__inet_pton_nid_postfix(2, "1.2.3", bytes) == 0 && *__error_nid_postfix() == 13); + Require(__inet_pton_nid_postfix(28, "::1", bytes) == 1 && *__error_nid_postfix() == 13); + Require(__inet_ntop_nid_postfix(2, bytes, text, sizeof(text)) == text && *__error_nid_postfix() == 13); +} + int main() { + Require(htonl_nid_postfix(0x11223344u) == 0x44332211u && ntohl_nid_postfix(0x44332211u) == 0x11223344u); + Require(htons_nid_postfix(0x1234) == 0x3412 && ntohs_nid_postfix(0x3412) == 0x1234); + const std::uint32_t loopback = htonl_nid_postfix(0x7f000001u); + unsigned char loopbackBytes[4]; + std::memcpy(loopbackBytes, &loopback, sizeof(loopback)); + Require(loopbackBytes[0] == 127 && loopbackBytes[3] == 1); for (int family : {2, 28}) { const char* input = family == 2 ? "192.0.2.17" : "2001:db8::17"; std::array address{}; @@ -76,4 +641,14 @@ int main() { Require(__inet_ntop_nid_postfix(2, fourth, widest, 12) == widest && std::strcmp(widest, "255.0.7.100") == 0); Require(__inet_ntop_nid_postfix(2, fourth, widest, 11) == nullptr && *__error_nid_postfix() == 28); Require(__inet_ntop_nid_postfix(10, fourth, widest, sizeof(widest)) == nullptr && *__error_nid_postfix() == 47); + + CheckConsolePton(); + CheckConsoleNtop(); + CheckConsolePadding(ConsolePaddingBoth, sizeof(ConsolePaddingBoth) / sizeof(ConsolePaddingBoth[0]), __inet_ntop_nid_postfix); + CheckConsolePadding(ConsolePaddingBoth, sizeof(ConsolePaddingBoth) / sizeof(ConsolePaddingBoth[0]), inet_ntop_nid_postfix); + CheckConsolePadding(ConsolePaddingUnderscored, sizeof(ConsolePaddingUnderscored) / sizeof(ConsolePaddingUnderscored[0]), __inet_ntop_nid_postfix); + CheckConsolePadding(ConsolePaddingLarge, sizeof(ConsolePaddingLarge) / sizeof(ConsolePaddingLarge[0]), __inet_ntop_nid_postfix); + CheckConsolePadding(ConsolePaddingLarge, sizeof(ConsolePaddingLarge) / sizeof(ConsolePaddingLarge[0]), inet_ntop_nid_postfix); + CheckNullPointers(); + CheckErrnoKept(); } diff --git a/core/libs/tests/GuestAgcDriverDebugUnavailable.cpp b/core/libs/tests/GuestAgcDriverDebugUnavailable.cpp index 89ca36d6f2..72b97bd56f 100644 --- a/core/libs/tests/GuestAgcDriverDebugUnavailable.cpp +++ b/core/libs/tests/GuestAgcDriverDebugUnavailable.cpp @@ -10,9 +10,31 @@ int APS5_VABI sceAgcDriverSetSubmitValidationMode(std::uint32_t); int APS5_VABI sceAgcDriverGetSubmitValidationMode(std::uint32_t*); int APS5_VABI sceAgcDriverSetSubmitValidationConfig(const void*); int APS5_VABI sceAgcDriverGetSubmitValidationConfig(void*); +int APS5_VABI sceAgcDriverSetValidationDisabled(); +int APS5_VABI sceAgcDriverGetCaptureConfig(); +int APS5_VABI sceAgcDriverSetCaptureConfig(); +int APS5_VABI sceAgcDriverGetGpuPrintfWorkArea(); +int APS5_VABI sceAgcDriverSetGpuPrintfEnabled(); +int APS5_VABI sceAgcDriverDbgReadRegister(); +int APS5_VABI sceAgcDriverDbgWriteRegister(); +int APS5_VABI sceAgcDriverGetGpuRefClks(); +int APS5_VABI sceAgcDriverIsSuspendPointInFlight(); +int APS5_VABI sceAgcDriverIsPaDebug(); +int APS5_VABI sceAgcDriverGetExceptionStatus(); +int APS5_VABI sceAgcDriverGetTraceInitiator(); +int APS5_VABI sceAgcDriverRegisterGpuResetCallbacks(); +int APS5_VABI sceAgcDriverUnregisterGpuResetCallbacks(); +int APS5_VABI sceAgdResetGpu(); +int APS5_VABI sceAgcDriverSysSubmitFlipHandleProxy(); +int APS5_VABI sceAgcDriverUserDataImmediateWrite(); +int APS5_VABI sceAgcDriverUserDataImmediateWritePacket(); +int APS5_VABI sceAgcDriverGetCoredumpMode(std::uint32_t*); +int APS5_VABI sceAgcDriverAllocateToolMemoryForGpuReset(std::uint64_t, std::uint64_t, std::uint64_t, void*); +int APS5_VABI sceAgcDriverGetAllocatedToolMemoryForGpuReset(std::uint64_t, void*); } static constexpr int DebugUnavailable = static_cast(0x8A6C1000); +static constexpr int FeatureUnavailable = static_cast(0x8A6D0001); static void Require(bool value) { if (!value) std::abort(); } int main() { @@ -35,4 +57,29 @@ int main() { Require(sceAgcDriverGetSubmitValidationConfig(config.data()) == DebugUnavailable); Require(sceAgcDriverGetSubmitValidationConfig(nullptr) == DebugUnavailable); for (const std::uint8_t byte : config) Require(byte == 0x5A); + + Require(sceAgcDriverSetValidationDisabled() == DebugUnavailable); + Require(sceAgcDriverGetCaptureConfig() == DebugUnavailable && sceAgcDriverSetCaptureConfig() == DebugUnavailable); + Require(sceAgcDriverGetGpuPrintfWorkArea() == FeatureUnavailable && sceAgcDriverSetGpuPrintfEnabled() == FeatureUnavailable); + Require(sceAgcDriverDbgReadRegister() == 0 && sceAgcDriverDbgWriteRegister() == FeatureUnavailable); + Require(sceAgcDriverGetGpuRefClks() == FeatureUnavailable && sceAgcDriverIsSuspendPointInFlight() == FeatureUnavailable); + Require(sceAgcDriverIsPaDebug() == 0 && sceAgcDriverGetExceptionStatus() == 0 && sceAgcDriverGetTraceInitiator() == 0); + Require(sceAgcDriverRegisterGpuResetCallbacks() == 0 && sceAgcDriverUnregisterGpuResetCallbacks() == 0); + Require(sceAgdResetGpu() == static_cast(0x8A6C0045)); + Require(sceAgcDriverSysSubmitFlipHandleProxy() == 0); + Require(sceAgcDriverUserDataImmediateWrite() == 0 && sceAgcDriverUserDataImmediateWritePacket() == 0); + + std::uint32_t coredumpMode = 0xA5A5A5A5u; + Require(sceAgcDriverGetCoredumpMode(nullptr) == static_cast(0x8A6D0003)); + Require(sceAgcDriverGetCoredumpMode(&coredumpMode) == 0 && coredumpMode == 0); + + std::array range{}; + range.fill(0x5A); + Require(sceAgcDriverAllocateToolMemoryForGpuReset(1, 2, 3, range.data()) == FeatureUnavailable); + for (std::size_t i = 0; i < range.size(); ++i) Require(range[i] == (i < 12 ? 0 : 0x5A)); + Require(sceAgcDriverAllocateToolMemoryForGpuReset(1, 2, 3, nullptr) == FeatureUnavailable); + range.fill(0x5A); + Require(sceAgcDriverGetAllocatedToolMemoryForGpuReset(1, range.data()) == FeatureUnavailable); + for (std::size_t i = 0; i < range.size(); ++i) Require(range[i] == (i < 12 ? 0 : 0x5A)); + Require(sceAgcDriverGetAllocatedToolMemoryForGpuReset(1, nullptr) == FeatureUnavailable); } diff --git a/core/libs/tests/GuestAgcDriverResources.cpp b/core/libs/tests/GuestAgcDriverResources.cpp index df3981cfb8..fc55b15a8a 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); @@ -27,6 +28,9 @@ int APS5_VABI sceAgcDriverFindResourcesPublic(const void*, void*); bool APS5_VABI sceAgcDriverIsCaptureInProgress(void); bool APS5_VABI sceAgcDriverIsTraceInProgress(void); bool APS5_VABI sceAgcDriverIsSubmitValidationEnabled(void); +int APS5_VABI sceAgcDriverRegisterOwnerForSystem(); +int APS5_VABI sceAgcDriverInitResourceRegistrationInterface(); +int APS5_VABI sceAgcDriverSetResourceRegistrationInfo(); } static constexpr int Unavailable = static_cast(0x8A6C9018); @@ -47,6 +51,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); @@ -91,4 +97,7 @@ int main() { Require(!sceAgcDriverIsCaptureInProgress()); Require(!sceAgcDriverIsTraceInProgress()); Require(!sceAgcDriverIsSubmitValidationEnabled()); + Require(sceAgcDriverRegisterOwnerForSystem() == Unavailable); + Require(sceAgcDriverInitResourceRegistrationInterface() == Unavailable); + Require(sceAgcDriverSetResourceRegistrationInfo() == 0); } diff --git a/core/libs/tests/GuestAio.cpp b/core/libs/tests/GuestAio.cpp index 96d8030630..cad5b3c0b7 100644 --- a/core/libs/tests/GuestAio.cpp +++ b/core/libs/tests/GuestAio.cpp @@ -328,6 +328,16 @@ int main(int argc, char** argv) { Check(sceKernelAioSubmitWriteCommandsMultiple(writeBatch, 0, 0, writeIds) == SCE_KERNEL_ERROR_EINVAL); KernelAioRwRequest noResult{0, 1, readBack.data(), nullptr, batchFd}; Check(sceKernelAioSubmitWriteCommandsMultiple(&noResult, 1, 0, writeIds) == SCE_KERNEL_ERROR_EFAULT); + std::array unread{}; + std::array unreadIds{}; + for (const auto submit : {sceKernelAioSubmitReadCommandsMultiple, sceKernelAioSubmitWriteCommandsMultiple}) { + bool limited = false; + try { submit(unread.data(), 129, 0, unreadIds.data()); } catch (const std::runtime_error&) { limited = true; } + Check(limited); + } + std::int32_t unreadId = 0; + Check(sceKernelAioSubmitReadCommands(unread.data(), 129, 0, &unreadId) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioSubmitWriteCommands(unread.data(), 129, 0, &unreadId) == SCE_KERNEL_ERROR_EFAULT); Check(sceKernelAioWaitRequests(readIds, 2, nullptr, 1, nullptr) == SCE_KERNEL_ERROR_EFAULT); Check(sceKernelAioWaitRequests(nullptr, 2, readStates, 1, nullptr) == SCE_KERNEL_ERROR_EFAULT); Check(sceKernelAioWaitRequests(readIds, -1, readStates, 1, nullptr) == SCE_KERNEL_ERROR_EINVAL); diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 5d0dc75362..4fca98867a 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #ifdef _WIN32 @@ -89,6 +90,47 @@ const std::uint8_t OPUS_STEREO[] = { 0xA7, 0xFA, 0x99, 0xF9, 0x47, 0x32, 0x07, 0xAF, 0x24, 0xBF, 0x14, 0xC5, }; +const std::uint8_t AAC_STEREO[] = { + 0xFF, 0xF1, 0x4C, 0x80, 0x15, 0x1F, 0xFC, 0x21, 0x28, 0x08, 0x03, 0x00, 0x00, 0x05, 0x25, 0x24, 0x9D, 0xA6, 0x20, 0xE8, 0x87, 0x32, 0xAE, 0x49, + 0xA7, 0x13, 0x8A, 0x5A, 0x44, 0x74, 0x44, 0x93, 0xF1, 0x70, 0x36, 0x6B, 0x1D, 0x1B, 0x5A, 0xB4, 0xD5, 0xF2, 0x8E, 0xD7, 0xBF, 0x7D, 0x45, 0x62, + 0x0E, 0xC7, 0x8B, 0x8F, 0x6E, 0xAF, 0xE6, 0x65, 0xCD, 0x36, 0x5E, 0x35, 0x55, 0xF2, 0xF9, 0x65, 0xCB, 0xF2, 0xE3, 0xC7, 0x8F, 0xCB, 0x2C, 0xD9, + 0x62, 0xE3, 0x57, 0x1F, 0x96, 0x59, 0xA2, 0x8A, 0x2A, 0xAA, 0x88, 0xB8, 0xF1, 0x8B, 0x2E, 0x58, 0xA2, 0x8A, 0x75, 0xAF, 0xA6, 0x40, 0xE8, 0x87, + 0x80, 0x35, 0xBA, 0xA9, 0xCF, 0x75, 0x7E, 0xDA, 0xD5, 0x3F, 0xAE, 0x49, 0x3F, 0x7B, 0xB1, 0x4C, 0x8C, 0xED, 0x5B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, + 0x3C, 0x23, 0xB3, 0xB3, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3A, 0x56, 0x84, 0x84, 0x85, 0x1B, 0xFC, 0xBF, 0xFB, 0x47, 0xF7, 0xDF, 0x4D, 0x6C, + 0xEC, 0xE9, 0x23, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3B, 0x3A, 0x3A, 0x6F, 0x40, 0xC6, 0xCD, 0x9B, 0x96, 0x59, 0x78, + 0xFF, 0xF1, 0x4C, 0x80, 0x22, 0x3F, 0xFC, 0x21, 0x4A, 0x6C, 0x80, 0x00, 0x00, 0x16, 0x00, 0xD0, 0xD9, 0x2B, 0xB2, 0x48, 0xF5, 0x64, 0x7A, 0xB2, + 0x5D, 0x3B, 0xFD, 0x7C, 0xF1, 0xA7, 0x16, 0xAF, 0x5F, 0xF1, 0xFE, 0xBF, 0xF7, 0xF8, 0xEB, 0x5C, 0x6B, 0x5A, 0xFD, 0x3F, 0xFE, 0xA7, 0xFD, 0x7E, + 0xBA, 0xE3, 0xAB, 0xD6, 0x9F, 0xFF, 0x87, 0xF9, 0xFF, 0x4F, 0x6E, 0x3A, 0xE3, 0x5C, 0x04, 0x5D, 0xF4, 0xC6, 0xFE, 0x94, 0x51, 0x43, 0xF1, 0xEC, + 0x7C, 0xF9, 0x28, 0xCD, 0x03, 0x51, 0xE7, 0x1F, 0xCF, 0x50, 0x7F, 0x3D, 0x41, 0xFC, 0xFC, 0xE0, 0xFE, 0x7F, 0xCF, 0xF9, 0x81, 0xE7, 0xE7, 0x51, + 0x98, 0xB2, 0xA9, 0x5E, 0xAC, 0x39, 0xB7, 0xBB, 0xAF, 0xA5, 0x15, 0xFD, 0x4D, 0xFF, 0x93, 0xF0, 0xFE, 0x5E, 0xA3, 0x7D, 0xF5, 0xCC, 0xDF, 0x7D, + 0x9B, 0x0D, 0x5A, 0xB5, 0xEB, 0xD6, 0x37, 0xEF, 0xDF, 0xAF, 0x5E, 0xB1, 0xBF, 0x7E, 0xFF, 0x0D, 0xAC, 0x96, 0xB2, 0x2E, 0x77, 0x99, 0x14, 0x51, + 0x45, 0x14, 0x44, 0xF3, 0x1E, 0x8A, 0x59, 0xEB, 0x65, 0x91, 0xE7, 0xCD, 0x91, 0x7E, 0x45, 0x73, 0xB2, 0x4F, 0xD2, 0x47, 0xF5, 0xFF, 0xC6, 0x7C, + 0x76, 0xF9, 0xE3, 0x5A, 0xFF, 0xFF, 0x9F, 0xFD, 0x7F, 0xF1, 0xF1, 0xFF, 0x3E, 0x7D, 0xB8, 0xD5, 0x79, 0xFF, 0xFB, 0x1F, 0xDB, 0xFD, 0xEF, 0xFD, + 0xBD, 0xBD, 0x55, 0x79, 0xFF, 0xFB, 0xBF, 0xA7, 0xFB, 0xFD, 0xDF, 0xF5, 0xE3, 0x5E, 0xC3, 0xB3, 0xDA, 0x95, 0xC3, 0xCE, 0xBD, 0xB5, 0xDB, 0x12, + 0xED, 0x5F, 0xB7, 0x49, 0x4A, 0x56, 0xD1, 0x26, 0xBE, 0xB1, 0x76, 0xBD, 0x67, 0xB9, 0xAF, 0x59, 0xFC, 0xEF, 0xF0, 0xD7, 0xAC, 0x7F, 0x3D, 0xE8, + 0xA7, 0xD4, 0xCE, 0xAB, 0x51, 0xF7, 0x40, 0x4A, 0x27, 0x3C, 0xC8, 0xF8, 0x07, 0xB1, 0xB3, 0x61, 0xA9, 0xEB, 0x5F, 0xC1, 0x4C, 0x66, 0x09, 0x09, + 0x09, 0x09, 0x09, 0x09, 0x09, 0x06, 0x06, 0x06, 0xE0, 0xFF, 0xF1, 0x4C, 0x80, 0x0E, 0xDF, 0xFC, 0x21, 0x78, 0xCF, 0xA2, 0x63, 0xF0, 0x00, 0x8D, + 0x2B, 0xEB, 0x1C, 0xE3, 0x9D, 0x7C, 0xE2, 0x7E, 0xDF, 0x3D, 0x9D, 0x6B, 0xFE, 0xBF, 0x1A, 0xD4, 0xB7, 0xB5, 0xD6, 0x97, 0x2E, 0x45, 0x87, 0xFE, + 0x3E, 0x3E, 0x1B, 0x75, 0xDF, 0x7F, 0x70, 0x3E, 0x07, 0xC3, 0x1D, 0x64, 0x3D, 0xDE, 0x1D, 0xDB, 0xFC, 0x05, 0xE0, 0x7C, 0x66, 0x1E, 0xFE, 0xFA, + 0x0F, 0xF1, 0xF1, 0x98, 0x7B, 0xFB, 0xE8, 0x3F, 0xC7, 0xC4, 0x68, 0xDF, 0x58, 0xE7, 0x17, 0xBD, 0xD5, 0x77, 0xFF, 0x6E, 0xCF, 0xC7, 0xE2, 0xFA, + 0xF8, 0xD6, 0xA5, 0xBE, 0x6D, 0xA5, 0xCB, 0x92, 0x40, 0x18, 0x5E, 0x6F, 0xE1, 0xD6, 0x8C, 0xFF, 0xE3, 0xE3, 0x58, 0x42, 0xB0, 0x61, 0x97, 0xF0, + 0xFE, 0x1E, 0xA3, 0xC4, 0x00, 0x01, 0xC0, 0xFF, 0xF1, 0x4C, 0x80, 0x0F, 0xFF, 0xFC, 0x21, 0x18, 0xC8, 0x00, 0x00, 0x10, 0x00, 0x7C, 0x2B, 0xEB, + 0x1E, 0xDD, 0xCC, 0x93, 0x6C, 0xCE, 0xFF, 0x4F, 0xEB, 0xFD, 0x3D, 0xD5, 0x7F, 0x1F, 0xF9, 0xFF, 0xBF, 0xDD, 0xEF, 0x09, 0x82, 0xD4, 0x13, 0x04, + 0x6B, 0x35, 0x7D, 0x2B, 0xAF, 0xD6, 0x79, 0xCF, 0x38, 0x38, 0x58, 0x99, 0xFF, 0x86, 0xF9, 0xA8, 0xE9, 0xEC, 0xC3, 0xEC, 0x23, 0x00, 0xC0, 0x50, + 0x60, 0x1E, 0x7A, 0xFF, 0x38, 0x92, 0xBE, 0xB1, 0xDF, 0xAF, 0x9F, 0xB7, 0xCC, 0xDF, 0x83, 0xE2, 0x7F, 0xE5, 0xFE, 0x7C, 0xDD, 0xDA, 0xD1, 0xD3, + 0x28, 0x4C, 0xB6, 0x41, 0xCE, 0x94, 0xA7, 0x09, 0x2B, 0xD3, 0xBF, 0xF7, 0xCB, 0x46, 0x47, 0x48, 0x36, 0x1E, 0x1F, 0x4F, 0xA7, 0xD2, 0x8D, 0x74, + 0x6B, 0xFA, 0x6B, 0xE7, 0xDD, 0xCE, 0x6D, 0xCF, 0x5E, 0xB5, 0x87, 0x82, 0x4C, 0xCE, 0xFF, 0xF1, 0x4C, 0x80, 0x1A, 0x5F, 0xFC, 0x21, 0x18, 0xC8, + 0x00, 0x3C, 0x1E, 0x00, 0x96, 0x47, 0x09, 0x91, 0xEA, 0xCA, 0x55, 0x29, 0x4A, 0xAA, 0xCA, 0xFE, 0x3C, 0x57, 0xE3, 0xCF, 0xB4, 0x4C, 0x92, 0x44, + 0x01, 0xB9, 0x65, 0x96, 0x59, 0x65, 0x96, 0x59, 0x65, 0x96, 0x59, 0x65, 0x46, 0x19, 0x78, 0x02, 0x49, 0x7F, 0x0C, 0x78, 0x01, 0x87, 0x9C, 0x30, + 0x30, 0x32, 0x60, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, + 0xC0, 0xC0, 0xC9, 0xA6, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x98, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x32, 0x9D, 0x2C, 0x24, 0xF4, 0xB7, 0xBE, + 0x7C, 0x6E, 0x4E, 0x3E, 0x35, 0x27, 0xB5, 0x65, 0x44, 0x44, 0x4C, 0x80, 0x81, 0x6D, 0xD1, 0x72, 0xA2, 0xCC, 0x28, 0x29, 0x77, 0x83, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x00, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x36, 0x44, 0x00, 0x00, 0xE0, +}; + struct DecodeSideband { std::int32_t result; std::int32_t internalResult; @@ -349,6 +391,21 @@ void TestMp3ParseFrame() { Require(sceAjmDecMp3ParseFrame(reservedSampleRate, 4, 0, &frame) == invalidParameter); const std::uint8_t mpeg25Above64Kbps[4] = {0xFF, 0xE3, 0x98, 0xC0}; Require(sceAjmDecMp3ParseFrame(mpeg25Above64Kbps, 4, 0, &frame) == invalidParameter); + for (const std::uint8_t layer : {0u, 2u, 3u}) { + const std::uint8_t header[4] = {0xFF, static_cast(0xF9u | (layer << 1u)), 0x90, 0x04}; + if (layer == 0) { + std::memset(&frame, 0xA5, sizeof(frame)); + const auto previous = frame; + Require(sceAjmDecMp3ParseFrame(header, sizeof(header), 0, &frame) == invalidParameter); + Require(std::memcmp(&frame, &previous, sizeof(frame)) == 0); + } else { + Require(ParsesTo(header, sizeof(header), 417, 2, 1152, 128000, 44100)); + } + std::memset(&frame, 0xA5, sizeof(frame)); + const auto previous = frame; + Require(sceAjmDecMp3ParseFrame(header, sizeof(header), 1, &frame) == invalidParameter); + Require(std::memcmp(&frame, &previous, sizeof(frame)) == 0); + } } const std::uint8_t LAME_MPEG1_STEREO_VBR[] = { @@ -415,14 +472,17 @@ bool OflParsesTo(const std::vector& stream, std::uint32_t streamSi return frame.num_frames == frames && frame.encoder_delay == delay && frame.total_samples == total && frame.ofl_type == type; } -bool OflThrows(const std::vector& stream) { - AjmDecMp3ParseFrame frame{}; - try { - sceAjmDecMp3ParseFrame(stream.data(), static_cast(stream.size()), 1, &frame); - } catch (const std::runtime_error&) { - return true; +std::vector ProtectedMp3Frame(std::vector frame, bool tagAfterCrc) { + frame[1] &= 0xFE; + if (tagAfterCrc) { + const auto size = frame.size(); + frame.insert(frame.begin() + 4, {0xA5, 0x5A}); + frame.resize(size); + } else { + frame[4] = 0xA5; + frame[5] = 0x5A; } - return false; + return frame; } void TestMp3ParseOfl() { @@ -439,21 +499,75 @@ void TestMp3ParseOfl() { const auto fgh = Mp3Frame(FGH_FRAME, sizeof(FGH_FRAME), 417); Require(OflParsesTo(fgh, 417, 0, 1105, 441000, 3)); Require(OflParsesTo(Mp3Frame(FGH_BAD_CRC_FRAME, sizeof(FGH_BAD_CRC_FRAME), 417), 417, 0, 0, 0, 0)); + const auto marker = std::find(fgh.begin() + 36, fgh.end(), 0xB4u); + Require(marker != fgh.end() && static_cast(fgh.end() - marker) >= 10); + std::vector misleadingFgh(417, 0); + std::copy_n(fgh.begin(), 4, misleadingFgh.begin()); + const auto markerOffset = static_cast(marker - fgh.begin()); + std::copy_n(fgh.begin() + static_cast(markerOffset), 10, misleadingFgh.begin() + 36); + misleadingFgh[45] ^= 0x01u; + std::copy_n(fgh.begin() + static_cast(markerOffset), 10, misleadingFgh.begin() + 48); + Require(OflParsesTo(misleadingFgh, 417, 0, 0, 0, 0)); auto vbri = Mp3Frame(VBRI_FRAME, sizeof(VBRI_FRAME), 417); Require(OflParsesTo(vbri, 417, 0, 576, 0, 2)); vbri.insert(vbri.end(), fgh.begin(), fgh.end()); Require(OflParsesTo(vbri, 834, 0, 576 + 1105, 441000, 4)); Require(OflParsesTo(vbri, 480, 0, 576, 0, 2)); - auto protectedFrame = stereo; - protectedFrame[1] = 0xFA; - Require(OflThrows(protectedFrame)); - auto layer2 = stereo; - layer2[1] = 0xFD; - Require(OflThrows(layer2)); + for (const bool tagAfterCrc : {false, true}) { + const auto protectedStereo = ProtectedMp3Frame(stereo, tagAfterCrc); + Require(OflParsesTo(protectedStereo, 417, 10, 1152 + 576 + 529, 10000, 1)); + const auto protectedMono = ProtectedMp3Frame(mono, tagAfterCrc); + Require(OflParsesTo(protectedMono, 576, 10, 1152 + 576 + 529, 10000, 1)); + const auto protectedMpeg2 = ProtectedMp3Frame(mpeg2, tagAfterCrc); + Require(OflParsesTo(protectedMpeg2, 208, 20, 576 + 576 + 529, 10000, 1)); + } + auto protectedNonzeroSideInfo = ProtectedMp3Frame(stereo, true); + protectedNonzeroSideInfo[8] = 0x00; + protectedNonzeroSideInfo[9] = 0x10; + std::move_backward(protectedNonzeroSideInfo.begin() + 38, protectedNonzeroSideInfo.end() - 2, protectedNonzeroSideInfo.end()); + protectedNonzeroSideInfo[38] = 0; + protectedNonzeroSideInfo[39] = 0; + Require(OflParsesTo(protectedNonzeroSideInfo, 417, 10, 1152 + 576 + 529, 10000, 1)); + const auto protectedFgh = ProtectedMp3Frame(fgh, true); + Require(OflParsesTo(protectedFgh, 417, 0, 1105, 441000, 3)); auto monoVbri = Mp3Frame(VBRI_FRAME, sizeof(VBRI_FRAME), 417); monoVbri[3] = 0xC4; - Require(OflThrows(monoVbri)); + Require(OflParsesTo(monoVbri, 417, 0, 576, 0, 2)); + for (const std::uint8_t version : {0xF3u, 0xE3u}) { + for (const std::uint8_t mode : {0x04u, 0xC4u}) { + auto otherVbri = Mp3Frame(VBRI_FRAME, sizeof(VBRI_FRAME), version == 0xF3 ? 208 : 417); + otherVbri[1] = version; + otherVbri[2] = 0x80; + otherVbri[3] = mode; + Require(OflParsesTo(otherVbri, static_cast(otherVbri.size()), 0, 576, 0, 2)); + } + } + const auto protectedVbri = ProtectedMp3Frame(monoVbri, false); + Require(OflParsesTo(protectedVbri, 417, 0, 576, 0, 2)); + auto monoVbriAndFgh = monoVbri; + monoVbriAndFgh.insert(monoVbriAndFgh.end(), protectedFgh.begin(), protectedFgh.end()); + Require(OflParsesTo(monoVbriAndFgh, 834, 0, 576 + 1105, 441000, 4)); +} + +void TestMp3MetadataBounds() { + const auto stereo = Mp3Frame(LAME_MPEG1_STEREO_VBR, sizeof(LAME_MPEG1_STEREO_VBR), 417); + const auto protectedStereo = ProtectedMp3Frame(stereo, true); + const auto protectedLame = ProtectedMp3Frame(stereo, false); + auto vbri = Mp3Frame(VBRI_FRAME, sizeof(VBRI_FRAME), 417); + vbri[3] = 0xC4; + for (const auto& complete : {stereo, protectedStereo, protectedLame, vbri}) { + for (std::size_t length = 0; length <= complete.size(); ++length) { + const std::vector prefix(complete.begin(), complete.begin() + length); + AjmDecMp3ParseFrame frame{}; + const int result = sceAjmDecMp3ParseFrame(prefix.data(), static_cast(prefix.size()), 1, &frame); + Require(result == (length < 4 ? static_cast(0x80930005) : 0)); + if (length >= 4) Require(frame.frame_size == 417); + } + } + std::vector chain; + for (std::size_t i = 0; i < 4096; ++i) chain.insert(chain.end(), vbri.begin(), vbri.end()); + Require(OflParsesTo(chain, static_cast(chain.size()), 0, 576, 0, 2)); } struct GaplessDecode { @@ -496,6 +610,69 @@ bool Refused(std::uint32_t context, const AjmBatchInfo& info) { return false; } +void AppendOpusLength(std::vector& out, std::size_t length) { + if (length < 252) { + out.push_back(static_cast(length)); + return; + } + out.push_back(static_cast(252 + (length - 252) % 4)); + out.push_back(static_cast((length - 252) / 4)); +} + +std::vector SelfDelimited(const std::uint8_t* packet, std::size_t bytes) { + const std::uint32_t code = packet[0] & 3u; + Require(code <= 1); + std::vector out{packet[0]}; + AppendOpusLength(out, code == 0 ? bytes - 1 : (bytes - 1) / 2); + out.insert(out.end(), packet + 1, packet + bytes); + return out; +} + +void TestOpusMultistreamFamily255(std::uint32_t context) { + const bool stereo = (OPUS_STEREO[2] & 0x04u) != 0; + const std::uint32_t channels = stereo ? 4 : 2; + std::uint32_t mono = 0; + std::uint32_t multi = 0; + const std::uint32_t parameters[2][3] = {{stereo ? 2u : 1u, 48000, 0}, {channels, 48000, 255}}; + std::uint32_t* instances[2] = {&mono, &multi}; + for (std::size_t index = 0; index < 2; ++index) { + Require(sceAjmInstanceCreate(context, 24, 0, instances[index]) == 0); + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t initResult[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, *instances[index], parameters[index], sizeof(parameters[index]), initResult) == 0); + Submit(context, info); + Require(initResult[0] == 0); + } + const std::uint32_t streamChannels = channels / 2; + std::size_t offset = 0; + for (int packet = 0; packet < 3; ++packet) { + const std::size_t bytes = OPUS_STEREO[offset] | (std::size_t{OPUS_STEREO[offset + 1]} << 8u); + const std::uint8_t* data = OPUS_STEREO + offset + 2; + std::vector joined = SelfDelimited(data, bytes); + joined.insert(joined.end(), data, data + bytes); + std::vector framed{static_cast(joined.size()), static_cast(joined.size() >> 8u)}; + framed.insert(framed.end(), joined.begin(), joined.end()); + std::vector single(960 * streamChannels, -12345); + std::vector both(960 * channels, -12345); + DecodeSideband sideband{}; + RunDecode(context, mono, OPUS_STEREO + offset, 2 + bytes, single.data(), single.size() * sizeof(std::int16_t), sideband); + Require(sideband.result == 0); + RunDecode(context, multi, framed.data(), framed.size(), both.data(), both.size() * sizeof(std::int16_t), sideband); + Require(sideband.result == 0 && static_cast(sideband.inputConsumed) == framed.size() && sideband.outputWritten == 960 * channels * 2); + for (std::size_t sample = 0; sample < 960; ++sample) { + for (std::uint32_t channel = 0; channel < streamChannels; ++channel) { + Require(both[sample * channels + channel] == single[sample * streamChannels + channel]); + Require(both[sample * channels + streamChannels + channel] == single[sample * streamChannels + channel]); + } + } + offset += 2 + bytes; + } + Require(sceAjmInstanceDestroy(context, mono) == 0); + Require(sceAjmInstanceDestroy(context, multi) == 0); +} + void TestOpusMonoFamily255(std::uint32_t context) { std::uint32_t instances[2] = {}; for (std::size_t index = 0; index < 2; ++index) { @@ -528,7 +705,7 @@ void TestOpusMonoFamily255(std::uint32_t context) { } Require(peak > 900 && peak < 1200); - const std::uint32_t refusedParameters[2][3] = {{2, 48000, 255}, {1, 48000, 2}}; + const std::uint32_t refusedParameters[1][3] = {{1, 48000, 2}}; for (const auto& parameters : refusedParameters) { std::uint32_t instance = 0; Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); @@ -728,6 +905,37 @@ void TestBatchCancel(std::uint32_t context) { Require(sceAjmBatchCancel(context, id + 1) == invalidBatch); } +void TestBatchContextOwnership(std::uint32_t context) { + constexpr int invalidContext = static_cast(0x80930002); + constexpr int invalidBatch = static_cast(0x80930004); + std::uint32_t other = 0; + Require(sceAjmInitialize(0, &other) == 0 && other != context); + std::vector storage(256); + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(storage.data(), storage.size(), &info) == 0); + AjmBatchError error{}; + std::uint32_t first = 0; + std::uint32_t second = 0; + Require(sceAjmBatchStart(context, &info, 0, &error, &first) == 0); + Require(sceAjmBatchStart(other, &info, 0, &error, &second) == 0); + Require(sceAjmBatchCancel(other, first) == invalidBatch); + Require(sceAjmBatchWait(other, first, 0, &error) == invalidBatch); + Require(sceAjmBatchCancel(context, second) == invalidBatch); + Require(sceAjmBatchWait(context, second, 0, &error) == invalidBatch); + Require(sceAjmBatchWait(0, first, 0, &error) == invalidContext); + Require(sceAjmBatchWait(other + 1, first, 0, &error) == invalidContext); + Require(sceAjmBatchCancel(context, first) == 0); + Require(sceAjmBatchWait(context, first, 0, &error) == 0); + Require(sceAjmBatchWait(other, second, 0, &error) == 0); + Require(sceAjmBatchWait(context, first, 0, &error) == invalidBatch); + Require(sceAjmBatchWait(other, second, 0, &error) == invalidBatch); + std::uint32_t untouched = 0xfeedu; + Require(sceAjmBatchStart(0, &info, 0, &error, &untouched) == invalidContext); + Require(sceAjmBatchStart(other + 1, &info, 0, &error, &untouched) == invalidContext); + Require(untouched == 0xfeedu); + Require(sceAjmFinalize(other) == 0); +} + constexpr std::uint64_t CONTROL_RESET = 1ull << 13; constexpr std::uint64_t CONTROL_INITIALIZE = 1ull << 14; constexpr std::uint64_t SIDEBAND_GAPLESS_DECODE = 1ull << 45; @@ -900,11 +1108,6 @@ void TestControlAt9(std::uint32_t context) { result[0] = -1; RunControl(context, instance, CONTROL_START, &start, sizeof(start), result); Require(result[0] == 4); - - const std::uint32_t aac[4] = {0, 0, 1, 0}; - Require(sceAjmInstanceCreate(context, 2, 0, &instance) == 0); - Require(ControlRefused(context, instance, CONTROL_START, aac, sizeof(aac))); - Require(sceAjmInstanceDestroy(context, instance) == 0); } void TestControlMp3(std::uint32_t context) { @@ -958,6 +1161,271 @@ void TestControlOpus(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +constexpr std::uint32_t CODEC_M4AAC = 2; +constexpr std::uint64_t M4AAC_STEREO = 2ull << 3; +constexpr std::uint64_t M4AAC_FLOAT = 2ull << 7; +constexpr std::uint64_t M4AAC_NONDELAY = 1ull << 33; +constexpr std::uint64_t RUN_MULTIPLE_FRAMES = 1ull << 12; +constexpr std::uint64_t SIDEBAND_STREAM = 1ull << 47; +constexpr std::size_t AAC_FRAMES = 5; +constexpr std::size_t AAC_FRAME_SAMPLES = 1024; +constexpr std::size_t AAC_PRIMING = 1024; +constexpr std::size_t AAC_INPUT_SAMPLES = 4096; + +struct AacInitialize { + std::uint32_t configType; + std::uint32_t frequencyIndex; +}; + +struct AacControl { + GaplessDecode gapless; + AacInitialize initialize; +}; + +struct AacCodecInfoSideband { + std::int32_t result; + std::int32_t internalResult; + std::uint32_t heaac; + std::uint32_t reserved; +}; + +struct MultipleFramesSideband { + std::int32_t result; + std::int32_t internalResult; + std::int32_t inputConsumed; + std::int32_t outputWritten; + std::uint64_t totalDecodedSamples; + std::uint32_t numFrames; + std::uint32_t reserved; +}; + +std::vector AdtsFrameOffsets() { + std::vector offsets; + for (std::size_t offset = 0; offset < sizeof(AAC_STEREO);) { + offsets.push_back(offset); + offset += ((std::size_t{AAC_STEREO[offset + 3]} & 3u) << 11u) | (std::size_t{AAC_STEREO[offset + 4]} << 3u) | (AAC_STEREO[offset + 5] >> 5u); + } + Require(offsets.size() == AAC_FRAMES); + offsets.push_back(sizeof(AAC_STEREO)); + return offsets; +} + +double AacReference(std::size_t index, std::size_t channel) { + const double duration = static_cast(AAC_INPUT_SAMPLES) / 48000.0; + const double t = static_cast(index) / 48000.0; + const double start = channel == 0 ? 300.0 : 2500.0; + const double end = channel == 0 ? 3000.0 : 400.0; + return std::sin(2.0 * 3.14159265358979323846 * (start * t + (end - start) * t * t / (2.0 * duration))); +} + +double AacCorrelation(const std::vector& pcm, std::size_t channel, std::size_t referenceChannel, std::size_t lag) { + double cross = 0; + double decodedEnergy = 0; + double referenceEnergy = 0; + for (std::size_t sample = 0; sample < pcm.size() / 2; ++sample) { + const double decoded = pcm[sample * 2 + channel]; + const double reference = AacReference(sample + lag, referenceChannel); + cross += decoded * reference; + decodedEnergy += decoded * decoded; + referenceEnergy += reference * reference; + } + return cross / std::sqrt(decodedEnergy * referenceEnergy); +} + +std::uint32_t CreateAac(std::uint32_t context, std::uint64_t flags, std::uint32_t configType, std::uint32_t frequencyIndex) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, CODEC_M4AAC, flags, &instance) == 0); + const AacInitialize parameters{configType, frequencyIndex}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int32_t result[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, ¶meters, sizeof(parameters), result) == 0); + Submit(context, info); + Require(result[0] == 0 && result[1] == 0); + return instance; +} + +MultipleFramesSideband RunAacFrames(std::uint32_t context, std::uint32_t instance, std::uint64_t flags, const std::uint8_t* input, std::size_t inputSize, void* output, std::size_t outputSize) { + std::vector batch(4096); + AjmBatchInfo info{}; + MultipleFramesSideband sideband{-1, -1, -1, -1, 0, 0, 0}; + const std::size_t sidebandSize = (flags & RUN_MULTIPLE_FRAMES) ? sizeof(sideband) : sizeof(DecodeSideband); + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobRun(&info, instance, flags | SIDEBAND_STREAM, input, inputSize, output, outputSize, &sideband, sidebandSize) == 0); + Submit(context, info); + return sideband; +} + +std::int32_t InitializeAacResult(std::uint32_t context, std::uint32_t instance, const void* parameters, std::size_t size) { + std::vector batch(4096); + AjmBatchInfo info{}; + std::int32_t result[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, parameters, size, result) == 0); + Submit(context, info); + return result[0]; +} + +void TestM4aacAdts(std::uint32_t context, std::vector& delayed) { + const auto offsets = AdtsFrameOffsets(); + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, CODEC_M4AAC, M4AAC_STEREO, &instance) == 0); + const AacControl start{{0, 0, 0}, {1, 3}}; + std::int32_t result[2] = {-1, -1}; + RunControl(context, instance, CONTROL_START, &start, sizeof(start), result); + Require(result[0] == 0 && result[1] == 0); + + std::vector pcm(AAC_FRAME_SAMPLES * 2); + for (std::size_t frame = 0; frame < AAC_FRAMES; ++frame) { + const auto decoded = RunAacFrames(context, instance, 0, AAC_STEREO + offsets[frame], sizeof(AAC_STEREO) - offsets[frame], pcm.data(), pcm.size() * sizeof(std::int16_t)); + const std::size_t produced = frame < 2 ? 0 : AAC_FRAME_SAMPLES; + Require(decoded.result == 0 && static_cast(decoded.inputConsumed) == offsets[frame + 1] - offsets[frame]); + Require(static_cast(decoded.outputWritten) == produced * 2 * sizeof(std::int16_t) && decoded.totalDecodedSamples == (frame < 2 ? 0 : (frame - 1) * AAC_FRAME_SAMPLES)); + delayed.insert(delayed.end(), pcm.begin(), pcm.begin() + static_cast(produced * 2)); + } + Require(delayed.size() == (AAC_FRAMES - 2) * AAC_FRAME_SAMPLES * 2); + const auto empty = RunAacFrames(context, instance, 0, AAC_STEREO + sizeof(AAC_STEREO), 0, pcm.data(), pcm.size() * sizeof(std::int16_t)); + Require(empty.result == 8 && empty.inputConsumed == 0 && empty.outputWritten == 0); + + std::vector batch(4096); + AjmBatchInfo info{}; + FormatSideband format{-1, -1, 0, 0, 0, 0xaaaaaaaau, 0, 0xaaaaaaaau}; + AacCodecInfoSideband codec{-1, -1, 0xaaaaaaaau, 0xaaaaaaaau}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobGetInfo(&info, instance, &format) == 0); + Require(sceAjmBatchJobGetCodecInfo(&info, instance, &codec, sizeof(codec)) == 0); + Submit(context, info); + const std::size_t lastFrameBytes = offsets[AAC_FRAMES] - offsets[AAC_FRAMES - 1]; + Require(format.result == 0 && format.numChannels == 2 && format.channelMask == 0x3 && format.sampleRate == 48000 && format.sampleEncoding == 0 && format.bitrate == lastFrameBytes * 8 * 48000 / AAC_FRAME_SAMPLES && format.reserved == 0); + Require(codec.result == 0 && codec.internalResult == 0 && codec.heaac == 0 && codec.reserved == 0); + + const std::size_t lag = AAC_FRAME_SAMPLES * 2 - AAC_PRIMING; + for (std::size_t channel = 0; channel < 2; ++channel) { + const double aligned = AacCorrelation(delayed, channel, channel, lag); + Require(aligned > 0.85 && AacCorrelation(delayed, channel, 1 - channel, lag) < 0.2); + for (std::size_t shifted = lag - 64; shifted < lag; shifted += 8) Require(AacCorrelation(delayed, channel, channel, shifted) < aligned - 0.3); + } + + result[0] = -1; + RunControl(context, instance, CONTROL_RESET, nullptr, 0, result); + const auto reset = RunAacFrames(context, instance, 0, AAC_STEREO, sizeof(AAC_STEREO), pcm.data(), pcm.size() * sizeof(std::int16_t)); + Require(result[0] == 0 && reset.result == 0 && static_cast(reset.inputConsumed) == offsets[1] && reset.outputWritten == 0 && reset.totalDecodedSamples == 0); + + const AacControl gapless{{2000, 100, 0}, {1, 3}}; + result[0] = -1; + RunControl(context, instance, CONTROL_START, &gapless, sizeof(gapless), result); + std::vector all(AAC_FRAMES * AAC_FRAME_SAMPLES * 2); + const auto trimmed = RunAacFrames(context, instance, RUN_MULTIPLE_FRAMES, AAC_STEREO, sizeof(AAC_STEREO), all.data(), all.size() * sizeof(std::int16_t)); + Require(result[0] == 0 && trimmed.result == 0 && trimmed.inputConsumed == static_cast(sizeof(AAC_STEREO)) && trimmed.numFrames == AAC_FRAMES); + Require(trimmed.outputWritten == 2000 * 2 * static_cast(sizeof(std::int16_t)) && trimmed.totalDecodedSamples == 2000); + Require(std::equal(all.begin(), all.begin() + 2000 * 2, delayed.begin() + 100 * 2)); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestM4aacNondelay(std::uint32_t context, const std::vector& delayed) { + const auto offsets = AdtsFrameOffsets(); + const std::uint32_t adts = CreateAac(context, M4AAC_STEREO | M4AAC_NONDELAY, 1, 0); + std::vector whole(AAC_FRAMES * AAC_FRAME_SAMPLES * 2); + const auto decoded = RunAacFrames(context, adts, RUN_MULTIPLE_FRAMES, AAC_STEREO, sizeof(AAC_STEREO), whole.data(), whole.size() * sizeof(std::int16_t)); + Require(decoded.result == 0 && decoded.inputConsumed == static_cast(sizeof(AAC_STEREO)) && decoded.numFrames == AAC_FRAMES); + Require(static_cast(decoded.outputWritten) == whole.size() * sizeof(std::int16_t) && decoded.totalDecodedSamples == AAC_FRAMES * AAC_FRAME_SAMPLES); + Require(std::equal(delayed.begin(), delayed.end(), whole.begin() + 2 * AAC_FRAME_SAMPLES * 2)); + + std::vector small(1000 * 2); + const auto noRoom = RunAacFrames(context, adts, 0, AAC_STEREO, sizeof(AAC_STEREO), small.data(), small.size() * sizeof(std::int16_t)); + Require(noRoom.result == 0x10 && noRoom.inputConsumed == 0 && noRoom.outputWritten == 0); + const auto partial = RunAacFrames(context, adts, 0, AAC_STEREO, offsets[1] - 1, whole.data(), whole.size() * sizeof(std::int16_t)); + Require(partial.result == 8 && partial.inputConsumed == 0 && partial.outputWritten == 0); + const auto header = RunAacFrames(context, adts, 0, AAC_STEREO, 6, whole.data(), whole.size() * sizeof(std::int16_t)); + Require(header.result == 8 && header.inputConsumed == 0 && header.outputWritten == 0); + Require(sceAjmInstanceDestroy(context, adts) == 0); + + const std::uint32_t floating = CreateAac(context, M4AAC_STEREO | M4AAC_NONDELAY | M4AAC_FLOAT, 1, 0); + std::vector samples(AAC_FRAMES * AAC_FRAME_SAMPLES * 2); + const auto decodedFloat = RunAacFrames(context, floating, RUN_MULTIPLE_FRAMES, AAC_STEREO, sizeof(AAC_STEREO), samples.data(), samples.size() * sizeof(float)); + Require(decodedFloat.result == 0 && static_cast(decodedFloat.outputWritten) == samples.size() * sizeof(float)); + for (std::size_t index = 0; index < samples.size(); ++index) Require(std::fabs(samples[index] * 32768.0f - static_cast(whole[index])) <= 1.0f); + Require(sceAjmInstanceDestroy(context, floating) == 0); + + const std::uint32_t raw = CreateAac(context, M4AAC_STEREO | M4AAC_NONDELAY, 2, 3); + std::vector pcm(AAC_FRAME_SAMPLES * 2); + for (std::size_t frame = 0; frame < AAC_FRAMES; ++frame) { + const std::size_t payload = offsets[frame] + 7; + const auto access = RunAacFrames(context, raw, 0, AAC_STEREO + payload, offsets[frame + 1] - payload, pcm.data(), pcm.size() * sizeof(std::int16_t)); + Require(access.result == 0 && static_cast(access.inputConsumed) == offsets[frame + 1] - payload && static_cast(access.outputWritten) == pcm.size() * sizeof(std::int16_t)); + Require(std::equal(pcm.begin(), pcm.end(), whole.begin() + static_cast(frame * pcm.size()))); + } + std::vector batch(4096); + AjmBatchInfo info{}; + MultipleFramesSideband sideband{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobRun(&info, raw, RUN_MULTIPLE_FRAMES | SIDEBAND_STREAM, AAC_STEREO + 7, offsets[1] - 7, pcm.data(), pcm.size() * sizeof(std::int16_t), &sideband, sizeof(sideband)) == 0); + Require(Refused(context, info)); + Require(sceAjmInstanceDestroy(context, raw) == 0); +} + +void TestM4aacInitialize(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, CODEC_M4AAC, M4AAC_STEREO, &instance) == 0); + std::vector pcm(AAC_FRAME_SAMPLES * 2); + const auto early = RunAacFrames(context, instance, 0, AAC_STEREO, sizeof(AAC_STEREO), pcm.data(), pcm.size() * sizeof(std::int16_t)); + Require(early.result == 1); + const AacInitialize noType{0, 3}; + const AacInitialize badType{3, 3}; + const AacInitialize badIndex{2, 12}; + Require(InitializeAacResult(context, instance, &noType, sizeof(noType)) == 4); + Require(InitializeAacResult(context, instance, &badType, sizeof(badType)) == 4); + Require(InitializeAacResult(context, instance, &badIndex, sizeof(badIndex)) == 4); + Require(InitializeAacResult(context, instance, &badIndex, 4) == 4); + const auto stillEarly = RunAacFrames(context, instance, 0, AAC_STEREO, sizeof(AAC_STEREO), pcm.data(), pcm.size() * sizeof(std::int16_t)); + Require(stillEarly.result == 1); + + const AacControl invalid{{0, 0, 0}, {0, 3}}; + std::int32_t result[2] = {-1, -1}; + RunControl(context, instance, CONTROL_START, &invalid, sizeof(invalid), result); + Require(result[0] == 4); + Require(ControlRefused(context, instance, CONTROL_START, &invalid, sizeof(invalid) - 4)); + Require(ControlRefused(context, instance, CONTROL_INITIALIZE, &invalid, sizeof(invalid))); + const AacInitialize adts{1, 15}; + result[0] = -1; + RunControl(context, instance, CONTROL_INITIALIZE, &adts, sizeof(adts), result); + Require(result[0] == 0); + + std::vector broken(AAC_STEREO, AAC_STEREO + sizeof(AAC_STEREO)); + broken[1] = 0x00; + std::vector batch(4096); + AjmBatchInfo info{}; + DecodeSideband sideband{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobDecode(&info, instance, broken.data(), broken.size(), pcm.data(), pcm.size() * sizeof(std::int16_t), &sideband) == 0); + Require(Refused(context, info)); + Require(sceAjmInstanceDestroy(context, instance) == 0); + + Require(sceAjmInstanceCreate(context, CODEC_M4AAC, 1ull << 3, &instance) == 0); + Require(InitializeAacResult(context, instance, &adts, sizeof(adts)) == 0); + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobDecode(&info, instance, AAC_STEREO, sizeof(AAC_STEREO), pcm.data(), pcm.size() * sizeof(std::int16_t), &sideband) == 0); + Require(Refused(context, info)); + Require(sceAjmInstanceDestroy(context, instance) == 0); + + Require(sceAjmInstanceCreate(context, CODEC_M4AAC, 3ull << 3, &instance) == 0); + const AacInitialize raw{2, 3}; + std::int32_t rawResult[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, &raw, sizeof(raw), rawResult) == 0); + Require(Refused(context, info)); + Require(sceAjmInstanceDestroy(context, instance) == 0); + + bool unknownFlagThrows = false; + try { + static_cast(sceAjmInstanceCreate(context, CODEC_M4AAC, M4AAC_STEREO | (1ull << 36), &instance)); + } catch (const std::runtime_error&) { + unknownFlagThrows = true; + } + Require(unknownFlagThrows); +} + struct ResampleInfo { std::int32_t result; std::int32_t internalResult; @@ -981,6 +1449,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)); @@ -1161,14 +1689,25 @@ void TestResampleParametersEx(std::uint32_t context) { std::int64_t result[2] = {-1, -1}; Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); for (const float bad : {0.0f, -1.0f, std::nanf(""), INFINITY}) Require(sceAjmBatchJobSetResampleParametersEx(&info, extended, bad, 0.0f, 1, result) == static_cast(0x80930005)); - bool rampThrows = false; - try { - static_cast(sceAjmBatchJobSetResampleParametersEx(&info, extended, 1.0f, 0.001f, 1, result)); - } catch (const std::runtime_error&) { - rampThrows = true; + for (const float bad : {std::nanf(""), INFINITY, -INFINITY}) Require(sceAjmBatchJobSetResampleParametersEx(&info, extended, 1.0f, bad, 1, result) == static_cast(0x80930005)); + + std::uint32_t ramped = 0; + Require(sceAjmInstanceCreate(context, 0, 0, &ramped) == 0); + std::vector rampedPcm(512); + { + std::vector rampBatch(4096); + AjmBatchInfo rampInfo{}; + std::int64_t setResult[2] = {-1, -1}; + DecodeSideband sideband{}; + Require(sceAjmBatchInitialize(rampBatch.data(), rampBatch.size(), &rampInfo) == 0); + Require(sceAjmBatchJobSetResampleParametersEx(&rampInfo, ramped, 1.0f, 0.001f, 1, setResult) == 0); + Require(sceAjmBatchJobDecode(&rampInfo, ramped, MP3_MONO, sizeof(MP3_MONO), rampedPcm.data(), rampedPcm.size() * sizeof(std::int16_t), &sideband) == 0); + Submit(context, rampInfo); + Require(setResult[0] == 0 && sideband.result == 0 && sideband.outputWritten == 512 * 2); } - Require(rampThrows && info.offset == 0); - Require(sceAjmInstanceDestroy(context, plain) == 0 && sceAjmInstanceDestroy(context, extended) == 0); + const auto resampleInfo = GetResampleInfo(context, ramped); + Require(std::abs(resampleInfo.ratio - 1.512f) < 0.001f && rampedPcm != plainPcm); + Require(sceAjmInstanceDestroy(context, plain) == 0 && sceAjmInstanceDestroy(context, extended) == 0 && sceAjmInstanceDestroy(context, ramped) == 0); } const std::uint8_t AT9_MONO_SILENT_SUPERFRAME[] = { @@ -1261,6 +1800,183 @@ 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); +} + +struct PcmResults { + std::int64_t initialize[2] = {-1, -1}; + std::int64_t resample[2] = {-1, -1}; + DecodeSideband decode{}; +}; + +AjmBatchInfo PcmJobs(std::vector& batch, std::uint32_t instance, const std::uint32_t* parameters, float ratio, const std::vector& input, std::vector& output, PcmResults& results) { + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + if (parameters) Require(sceAjmBatchJobInitialize(&info, instance, parameters, 3 * sizeof(std::uint32_t), results.initialize) == 0); + if (ratio > 0) Require(sceAjmBatchJobSetResampleParameters(&info, instance, ratio, 1, results.resample) == 0); + Require(sceAjmBatchJobDecode(&info, instance, input.data(), input.size() * sizeof(std::int16_t), output.data(), output.size() * sizeof(std::int16_t), &results.decode) == 0); + return info; +} + +void TestPcmResample(std::uint32_t context) { + constexpr std::uint64_t flags = 0x1002; + constexpr std::int16_t untouched = 0x7FFF; + const std::uint32_t parameters[3] = {0, 2, 44100}; + constexpr std::size_t inputFrames = 235; + constexpr float ratio = 0.91875f; + std::vector input(inputFrames * 2); + for (std::size_t frame = 0; frame < inputFrames; ++frame) { + input[frame * 2] = static_cast(frame * 100); + input[frame * 2 + 1] = static_cast((static_cast(frame * 7919 % 2001) - 1000) * 20); + } + const auto at = [&](std::ptrdiff_t frame, std::size_t channel) { + const auto clamped = std::clamp(frame, 0, static_cast(inputFrames) - 1); + return static_cast(input[static_cast(clamped) * 2 + channel]); + }; + std::vector batch(4096); + + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 23, flags, &instance) == 0); + for (int block = 0; block < 2; ++block) { + std::vector output(300 * 2, untouched); + PcmResults results; + Submit(context, PcmJobs(batch, instance, parameters, ratio, input, output, results)); + Require(results.initialize[0] == 0 && results.resample[0] == 0 && results.decode.result == 0); + Require(results.decode.inputConsumed == 940 && results.decode.outputWritten == 1024 && results.decode.totalDecodedSamples == 235); + for (std::size_t frame = 0; frame < 256; ++frame) { + const double position = static_cast(frame) * static_cast(ratio); + const auto index = static_cast(position); + const double t = position - static_cast(index); + for (std::size_t channel = 0; channel < 2; ++channel) { + const double p0 = at(index - 1, channel), p1 = at(index, channel), p2 = at(index + 1, channel), p3 = at(index + 2, channel); + const double expected = std::clamp(p1 + 0.5 * t * (p2 - p0 + t * (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3 + t * (3.0 * (p1 - p2) + p3 - p0))), -32768.0, 32767.0); + Require(std::fabs(output[frame * 2 + channel] - expected) <= 1.0); + } + } + Require(std::all_of(output.begin() + 256 * 2, output.end(), [&](std::int16_t value) { return value == untouched; })); + } + { + std::vector output(256 * 2); + PcmResults results; + Require(Refused(context, PcmJobs(batch, instance, nullptr, ratio, input, output, results))); + } + { + std::vector output(255 * 2); + PcmResults results; + Require(Refused(context, PcmJobs(batch, instance, parameters, ratio, input, output, results))); + } + { + const GaplessDecode gapless{100, 0, 0}; + std::int32_t gaplessResult[2] = {-1, -1}; + std::vector output(256 * 2); + PcmResults results; + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, parameters, sizeof(parameters), results.initialize) == 0); + Require(sceAjmBatchJobSetGaplessDecode(&info, instance, &gapless, 1, gaplessResult) == 0); + Require(sceAjmBatchJobDecode(&info, instance, input.data(), input.size() * sizeof(std::int16_t), output.data(), output.size() * sizeof(std::int16_t), &results.decode) == 0); + Require(Refused(context, info)); + } + Require(sceAjmInstanceDestroy(context, instance) == 0); + + std::uint32_t plain = 0; + Require(sceAjmInstanceCreate(context, 23, flags, &plain) == 0); + for (int run = 0; run < 2; ++run) { + std::vector output(input.size() + 4, untouched); + PcmResults results; + Submit(context, PcmJobs(batch, plain, run == 0 ? parameters : nullptr, 0, input, output, results)); + Require(results.decode.result == 0 && results.decode.inputConsumed == 940 && results.decode.outputWritten == 940); + Require(results.decode.totalDecodedSamples == 235u * static_cast(run + 1)); + Require(std::equal(input.begin(), input.end(), output.begin()) && output[input.size()] == untouched); + } + Require(sceAjmInstanceDestroy(context, plain) == 0); + + std::uint32_t invalid = 0; + Require(sceAjmInstanceCreate(context, 23, flags, &invalid) == 0); + { + const std::uint32_t noChannels[3] = {0, 0, 44100}; + std::vector output(256 * 2); + PcmResults results; + Submit(context, PcmJobs(batch, invalid, noChannels, 0, input, output, results)); + Require(results.initialize[0] == 4 && results.decode.result == 1); + } + { + const std::uint32_t otherFormat[3] = {1, 2, 44100}; + std::vector output(256 * 2); + PcmResults results; + Require(Refused(context, PcmJobs(batch, invalid, otherFormat, 0, input, output, results))); + } + Require(sceAjmInstanceDestroy(context, invalid) == 0); +} + } int main() { @@ -1289,10 +2005,12 @@ int main() { Require(nullBuffersThrow && batchInfo.offset == 0); TestMp3ParseFrame(); TestMp3ParseOfl(); + TestMp3MetadataBounds(); TestMp3(context); TestOpus(context); TestOpus(context, 1); TestOpusMonoFamily255(context); + TestOpusMultistreamFamily255(context); TestOpusFamilyZeroChannels(context); TestOpusSurround(context, 3, 0); for (std::size_t stream = 0; stream < 2; ++stream) { @@ -1312,16 +2030,26 @@ int main() { TestCodecInfo(context); TestGetInfo(context); TestBatchWaitRelease(context); + TestInstanceHandleLifetime(context); TestBatchCancel(context); + TestBatchContextOwnership(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); + TestM4aacInitialize(context); TestResampleOpus(context); TestResampleMp3(context); TestResampleParametersEx(context); TestResampleAt9(context); TestAt9GaplessSegments(context); TestAt9RunDecodesWholeInput(context); + TestPcmResample(context); Require(sceAjmFinalize(context) == 0); } diff --git a/core/libs/tests/GuestAjmVibration.cpp b/core/libs/tests/GuestAjmVibration.cpp new file mode 100644 index 0000000000..11351dcb6f --- /dev/null +++ b/core/libs/tests/GuestAjmVibration.cpp @@ -0,0 +1,171 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAjmInitialize(std::int64_t, std::uint32_t*); +int APS5_VABI sceAjmFinalize(std::uint32_t); +int APS5_VABI sceAjmDecAt9ParseConfigData(const void*, AjmDecAt9ConfigDataInfo*); +int APS5_VABI sceAjmInstanceCreate(std::uint32_t, std::uint32_t, std::uint64_t, std::uint32_t*); +int APS5_VABI sceAjmInstanceDestroy(std::uint32_t, std::uint32_t); +int APS5_VABI sceAjmBatchInitialize(void*, std::size_t, AjmBatchInfo*); +int APS5_VABI sceAjmBatchJobInitialize(AjmBatchInfo*, std::uint32_t, const void*, std::size_t, void*); +int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo*, std::uint32_t, std::uint64_t, const void*, std::size_t, void*, std::size_t, void*, std::size_t); +int APS5_VABI sceAjmBatchStart(std::uint32_t, const AjmBatchInfo*, int, AjmBatchError*, std::uint32_t*); +int APS5_VABI sceAjmBatchWait(std::uint32_t, std::uint32_t, std::uint32_t, AjmBatchError*); +} + +namespace { +void Require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +void Submit(std::uint32_t context, const AjmBatchInfo& info) { + AjmBatchError error{}; + std::uint32_t id = 0; + Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0, "batch submit"); +} + +std::array Config(unsigned channels) { + return {0xFE, static_cast(channels == 1 ? 0x7C : 0x7E), 0x03, 0xF0}; +} + +std::vector Packet(const std::vector& amplitudes) { + std::vector bytes; + std::size_t position = 0; + const auto put = [&](unsigned value, unsigned bits) { + for (unsigned bit = bits; bit > 0; --bit) { + if (position / 8 == bytes.size()) bytes.push_back(0); + bytes[position / 8] |= ((value >> (bit - 1)) & 1u) << (7 - position % 8); + ++position; + } + }; + for (unsigned frame = 0; frame < 4; ++frame) { + for (const auto amplitude : amplitudes) { + put(frame != 0, 1); + put(0, 1); + put(0, 1); + put(0, 4); + put(25, 5); + put(6, 3); + put(static_cast(amplitude) & 255u, 8); + put(static_cast(-amplitude) & 255u, 8); + while (position % 8 != 0) put(0, 1); + } + } + bytes.resize(128, 1); + return bytes; +} + +struct Stream { + std::int32_t result = -1; + std::int32_t internalResult = -1; + std::int32_t consumed = -1; + std::int32_t produced = -1; + std::uint64_t samples = 0; + std::uint32_t frames = 0; + std::uint32_t reserved = 0; +}; + +template +std::vector Decode(std::uint32_t context, unsigned channels, unsigned encoding, const std::vector& amplitudes, unsigned profile = 7, bool partial = false, bool malformed = false) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, static_cast(encoding) << 7u, &instance) == 0, "create vibration instance"); + auto config = Config(channels); + config[1] = static_cast((profile << 4) | (config[1] & 15u)); + std::vector batch(4096); + AjmBatchInfo info{}; + std::int32_t initialized[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0, "initialize batch"); + Require(sceAjmBatchJobInitialize(&info, instance, config.data(), config.size(), initialized) == 0, "append initialize"); + Submit(context, info); + Require(initialized[0] == 0 && initialized[1] == 0, "initialize vibration decoder"); + auto packet = Packet(amplitudes); + const auto samples = profile == 1 ? 256 : profile == 4 ? 512 : 1024; + std::vector pcm(samples * channels); + constexpr std::uint64_t flags = (1ull << 47) | (1ull << 12); + const auto run = [&](const std::vector& input, std::size_t size) { + Stream stream; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0, "reset batch"); + Require(sceAjmBatchJobRun(&info, instance, flags, input.data(), size, pcm.data(), pcm.size() * sizeof(T), &stream, sizeof(stream)) == 0, "append run"); + Submit(context, info); + return stream; + }; + if (partial) { + std::fill(pcm.begin(), pcm.end(), T{123}); + const auto stream = run(packet, packet.size() - 1); + Require(stream.result == 8 && stream.consumed == 0 && stream.produced == 0, "partial superframe accounting"); + Require(std::all_of(pcm.begin(), pcm.end(), [](T value) { return value == T{123}; }), "partial input changed PCM"); + } + if (malformed) { + auto broken = packet; + broken[0] |= 0x80; + const auto stream = run(broken, broken.size()); + Require((stream.result & 2) != 0 && stream.consumed == 0 && stream.produced == 0, "invalid first-frame flag"); + } + const auto stream = run(packet, packet.size()); + Require(stream.result == 0 && stream.internalResult == 0, "decode vibration superframe"); + Require(stream.consumed == 128 && stream.produced == static_cast(pcm.size() * sizeof(T)), "consumed/produced accounting"); + Require(stream.samples == static_cast(samples) && stream.frames == 4, "decoded sample/frame accounting"); + Require(sceAjmInstanceDestroy(context, instance) == 0, "destroy instance"); + return pcm; +} + +void Parse() { + for (const auto index : {6u, 7u}) { + for (const auto profile : {1u, 4u, 7u}) { + auto config = Config(index == 6 ? 1 : 2); + config[1] = static_cast((profile << 4) | (index << 1)); + AjmDecAt9ConfigDataInfo info{}; + Require(sceAjmDecAt9ParseConfigData(config.data(), &info) == 0, "parse vibration config"); + const unsigned samples = profile == 1 ? 64 : profile == 4 ? 128 : 256; + Require(info.channels == (index == 6 ? 1u : 2u) && info.sample_rate == (profile == 1 ? 12000u : profile == 4 ? 24000u : 48000u), "vibration layout/rate"); + Require(info.superframe_size == 128 && info.frame_samples_per_channel == samples && info.superframe_samples_per_channel == samples * 4, "vibration frame geometry"); + } + } + auto invalid = Config(2); + invalid[1] |= 1; + AjmDecAt9ConfigDataInfo info; + std::memset(&info, 0xAA, sizeof(info)); + Require(sceAjmDecAt9ParseConfigData(invalid.data(), &info) == static_cast(0x80930005), "invalid version accepted"); + Require(info.superframe_size == 0xAAAAAAAAu, "invalid parse changed output"); +} +} + +int main() { + try { + Parse(); + std::uint32_t context = 0; + Require(sceAjmInitialize(0, &context) == 0, "initialize AJM"); + const auto left = Decode(context, 1, 2, {1}); + const auto right = Decode(context, 1, 2, {2}); + const auto stereo = Decode(context, 2, 2, {1, 2}, 7, true, true); + Require(std::any_of(left.begin(), left.end(), [](float value) { return std::abs(value) > 0.000001f; }), "vibration decoded to silence"); + for (std::size_t sample = 0; sample < left.size(); ++sample) { + Require(std::isfinite(stereo[2 * sample]) && std::isfinite(stereo[2 * sample + 1]), "non-finite PCM"); + Require(stereo[2 * sample] == left[sample] && stereo[2 * sample + 1] == right[sample], "dual-mono interleaving or reset history"); + } + const auto pcm16 = Decode(context, 2, 0, {1, 2}); + const auto pcm32 = Decode(context, 2, 1, {1, 2}); + for (std::size_t sample = 0; sample < stereo.size(); ++sample) { + Require(std::abs(static_cast(pcm16[sample]) / 32768.0 - stereo[sample]) <= 1.0 / 32768.0, "PCM16 conversion"); + Require(std::abs(static_cast(pcm32[sample]) / 2147483648.0 - stereo[sample]) <= 1.0 / 2147483648.0, "PCM32 conversion"); + } + for (const auto profile : {1u, 4u}) { + const auto mono = Decode(context, 1, 2, {1}, profile); + const auto dual = Decode(context, 2, 2, {1, 1}, profile); + Require(std::any_of(mono.begin(), mono.end(), [](float value) { return std::abs(value) > 0.000001f; }), "lower-rate vibration decoded to silence"); + for (std::size_t sample = 0; sample < mono.size(); ++sample) Require(dual[2 * sample] == mono[sample] && dual[2 * sample + 1] == mono[sample], "lower-rate interleaving"); + } + Require(sceAjmFinalize(context) == 0, "finalize AJM"); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestAjmWVorbis.cpp b/core/libs/tests/GuestAjmWVorbis.cpp new file mode 100644 index 0000000000..4344e98e00 --- /dev/null +++ b/core/libs/tests/GuestAjmWVorbis.cpp @@ -0,0 +1,107 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include + +struct HeaderPacketParam { + std::uint64_t size; + std::uint32_t channels; + std::uint32_t sampleRate; + std::int32_t bytesPerSecond; + std::int32_t blockSize0Log2; + std::int32_t blockSize1Log2; + std::uint32_t reserved; + const std::uint8_t* setupPacket; + std::uint64_t setupPacketSize; + std::uint8_t* output; + std::uint64_t outputSize; + std::uint64_t writtenSize; +}; +static_assert(sizeof(HeaderPacketParam) == 0x48); + +extern "C" int APS5_VABI sceAjmDecWVorbisCreateHeaderPacket(HeaderPacketParam* param); + +namespace { + +constexpr int InvalidParameter = static_cast(0x8A6E0001); +constexpr int InvalidSize = static_cast(0x8A6E0002); +constexpr int OutputTooSmall = static_cast(0x8A6E0003); +constexpr int InvalidSetup = static_cast(0x8A6E0004); + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +constexpr std::array Setup {5, 'v', 'o', 'r', 'b', 'i', 's', 9, 0, 0xAA, 0xBB, 0xCC}; + +HeaderPacketParam Valid(std::uint8_t* output, std::size_t outputSize) { + return {sizeof(HeaderPacketParam), 2, 48000, 16000, 8, 11, 0, Setup.data(), Setup.size(), output, outputSize, 0}; +} + +int Create(HeaderPacketParam param) { return sceAjmDecWVorbisCreateHeaderPacket(¶m); } + +void TestValidation() { + std::array output {}; + Require(sceAjmDecWVorbisCreateHeaderPacket(nullptr) == InvalidParameter, "a null parameter is refused"); + auto param = Valid(output.data(), output.size()); + param.size = 0x40; + Require(Create(param) == InvalidSize, "a wrong structure size is refused"); + param = Valid(output.data(), output.size()); + param.bytesPerSecond = -1; + Require(Create(param) == InvalidParameter, "a negative byte rate is refused"); + param = Valid(output.data(), output.size()); + param.blockSize0Log2 = 5; + Require(Create(param) == InvalidParameter, "a block size below 64 is refused"); + param.blockSize0Log2 = 38; + param.blockSize1Log2 = 38; + Require(Create(param) == 0, "block size exponents are taken modulo 32 as the console's shift does"); + param = Valid(output.data(), output.size()); + param.blockSize1Log2 = 7; + Require(Create(param) == InvalidParameter, "block size 1 below block size 0 is refused"); + param.blockSize1Log2 = 14; + Require(Create(param) == InvalidParameter, "a block size above 8192 is refused"); + param = Valid(output.data(), output.size()); + param.setupPacket = nullptr; + param.setupPacketSize = 0; + Require(Create(param) == InvalidParameter, "a null setup packet is refused before its size"); + param = Valid(output.data(), output.size()); + param.setupPacketSize = 8; + Require(Create(param) == InvalidSetup, "a setup packet below 9 bytes is refused"); + param = Valid(nullptr, 0); + Require(Create(param) == InvalidParameter, "a null output is refused before its size"); + param = Valid(output.data(), 9); + Require(Create(param) == OutputTooSmall, "an output below 10 bytes is refused"); + param = Valid(output.data(), 56 + Setup.size() - 1); + Require(Create(param) == OutputTooSmall, "an output that cannot hold the setup packet is refused"); +} + +void TestPacket() { + std::array output {}; + output.fill(0xEE); + auto param = Valid(output.data(), output.size()); + Require(sceAjmDecWVorbisCreateHeaderPacket(¶m) == 0, "a valid header packet is built"); + Require(param.writtenSize == 56 + Setup.size(), "the written size covers the table, both headers and the setup packet"); + constexpr std::uint8_t Expected[] { + 0, 0, 30, 0, 16, 0, 9, 0, 3, 0, + 1, 'v', 'o', 'r', 'b', 'i', 's', 0, 0, 0, 0, 2, 0x80, 0xBB, 0, 0, 0, 0, 0, 0, 0x00, 0xF4, 0x01, 0x00, 0, 0, 0, 0, 0xB8, 1, + 3, 'v', 'o', 'r', 'b', 'i', 's', 0, 0, 0, 0, 0, 0, 0, 0, 1, + }; + static_assert(sizeof(Expected) == 56); + Require(std::memcmp(output.data(), Expected, sizeof(Expected)) == 0, "the table, identification and comment headers match"); + Require(std::memcmp(output.data() + 56, Setup.data(), Setup.size()) == 0, "the setup packet follows unchanged"); + Require(output[56 + Setup.size()] == 0xEE, "nothing is written past the packet"); +} + +} + +int main() { + TestValidation(); + TestPacket(); + std::puts("guest ajm wvorbis header packet: PASS"); +} diff --git a/core/libs/tests/GuestAllocatingStrings.cpp b/core/libs/tests/GuestAllocatingStrings.cpp new file mode 100644 index 0000000000..8bf053914b --- /dev/null +++ b/core/libs/tests/GuestAllocatingStrings.cpp @@ -0,0 +1,138 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +char* APS5_VABI strndup_nid_postfix(const char*, std::size_t); +char* APS5_VABI strdup_nid_postfix(const char*); +int APS5_VABI asprintf_nid_postfix(char**, const char*, ...); +int APS5_VABI vasprintf_nid_postfix(char**, const char*, VaList*); +void APS5_VABI free_nid_postfix(void*); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +void Require(bool value) { + if (!value) std::abort(); +} + +bool failAllocation = false; + +void* APS5_VABI Allocate(std::size_t size) { return failAllocation ? nullptr : std::malloc(size); } +void APS5_VABI Free(void* ptr) { std::free(ptr); } +void* APS5_VABI Reallocate(void* ptr, std::size_t size) { return std::realloc(ptr, size); } +void* APS5_VABI Calloc(std::size_t count, std::size_t size) { return std::calloc(count, size); } +void* APS5_VABI Align(std::size_t, std::size_t size) { return std::malloc(size); } +void* APS5_VABI Realign(void* ptr, std::size_t size, std::size_t) { return std::realloc(ptr, size); } +int APS5_VABI PosixAlign(void** ptr, std::size_t, std::size_t size) { + *ptr = std::malloc(size); + return *ptr ? 0 : 12; +} + +void InitializeHeap() { + alignas(16) static std::array process{}; + alignas(16) static std::array libc{}; + alignas(16) static std::array replacement{}; + const auto write = [](auto& data, std::size_t offset, auto value) { + std::memcpy(data.data() + offset, &value, sizeof(value)); + }; + write(process, 0, std::uint64_t{0x40}); + write(process, 8, std::uint32_t{0x4942524f}); + write(process, 0x38, libc.data()); + write(libc, 0, std::uint64_t{0x38}); + write(libc, 0x30, replacement.data()); + write(replacement, 0, std::uint64_t{0x78}); + write(replacement, 8, std::uint64_t{2}); + write(replacement, 0x20, reinterpret_cast(&Allocate)); + write(replacement, 0x28, reinterpret_cast(&Free)); + write(replacement, 0x30, reinterpret_cast(&Calloc)); + write(replacement, 0x38, reinterpret_cast(&Reallocate)); + write(replacement, 0x40, reinterpret_cast(&Align)); + write(replacement, 0x48, reinterpret_cast(&Realign)); + write(replacement, 0x50, reinterpret_cast(&PosixAlign)); + ApplicationHeapInitialize_nid_no_patch(process.data()); +} + +int CallVasprintf(char** destination, const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vasprintf_nid_postfix(destination, format, reinterpret_cast(args)); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} + +} + +int main() { + *__error_nid_postfix() = 0; + Require(strndup_nid_postfix(nullptr, 0) == nullptr); + Require(*__error_nid_postfix() == 22); + + *__error_nid_postfix() = 0; + Require(strndup_nid_postfix(nullptr, 32) == nullptr); + Require(*__error_nid_postfix() == 22); + + *__error_nid_postfix() = 0; + Require(strdup_nid_postfix(nullptr) == nullptr); + Require(*__error_nid_postfix() == 22); + + *__error_nid_postfix() = 0; + Require(vasprintf_nid_postfix(nullptr, "format", nullptr) == -1); + Require(*__error_nid_postfix() == 22); + + char* allocated = nullptr; + *__error_nid_postfix() = 0; + Require(CallVasprintf(&allocated, nullptr) == -1); + Require(*__error_nid_postfix() == 22); + Require(allocated == nullptr); + + *__error_nid_postfix() = 0; + Require(vasprintf_nid_postfix(&allocated, "format", nullptr) == -1); + Require(*__error_nid_postfix() == 22); + Require(allocated == nullptr); + + InitializeHeap(); + + char* dup1 = strndup_nid_postfix("hello world", 5); + Require(dup1 != nullptr); + Require(std::strcmp(dup1, "hello") == 0); + free_nid_postfix(dup1); + + char* dup2 = strdup_nid_postfix("testing 1 2 3"); + Require(dup2 != nullptr); + Require(std::strcmp(dup2, "testing 1 2 3") == 0); + free_nid_postfix(dup2); + + allocated = nullptr; + const int count = asprintf_nid_postfix(&allocated, "value=%d %s", 42, "ok"); + Require(count == 11); + Require(allocated != nullptr); + Require(std::strcmp(allocated, "value=42 ok") == 0); + free_nid_postfix(allocated); + + failAllocation = true; + allocated = nullptr; + *__error_nid_postfix() = 0; + Require(asprintf_nid_postfix(&allocated, "value=%d", 1) == -1); + Require(*__error_nid_postfix() == 12); + Require(allocated == nullptr); + failAllocation = false; + + return 0; +} diff --git a/core/libs/tests/GuestAllocatorFailures.cpp b/core/libs/tests/GuestAllocatorFailures.cpp index 22629e0166..5312249a50 100644 --- a/core/libs/tests/GuestAllocatorFailures.cpp +++ b/core/libs/tests/GuestAllocatorFailures.cpp @@ -32,6 +32,17 @@ constexpr std::size_t HalfPlusOne = static_cast(std::numeric_limits int failures = 0; +#ifdef __linux__ +int overcommitMode() { + int mode = -1; + if (std::FILE* file = std::fopen("/proc/sys/vm/overcommit_memory", "r")) { + if (std::fscanf(file, "%d", &mode) != 1) mode = -1; + std::fclose(file); + } + return mode; +} +#endif + void report(const char* what, const char* detail) { std::fprintf(stderr, "%s: %s\n", what, detail); ++failures; @@ -107,6 +118,9 @@ int main() { free_nid_postfix(block); } +#ifdef ANYPS5_ENABLE_ASAN + std::fputs("skipped: AddressSanitizer cannot map the shadow of a 300 GiB commit under a 128 MiB RLIMIT_DATA, so the failed commit and arena release checks did not run\n", stderr); +#else if (GuestArena::GuestArenaAvailable_nid_postfix()) { #ifdef __linux__ rlimit originalLimit{}; @@ -151,7 +165,14 @@ int main() { #endif if (committed) { #ifdef __linux__ - report("malloc(300 GiB)", "succeeded despite RLIMIT_DATA"); + int mode = overcommitMode(); + if (mode == 1) { + std::fprintf(stderr, "skipped: the host can commit 300 GiB (vm.overcommit_memory=%d), so the failed commit and arena release checks did not run\n", mode); + } else { + char detail[64]; + std::snprintf(detail, sizeof(detail), "committed 300 GiB with vm.overcommit_memory=%d", mode); + report("malloc(300 GiB)", detail); + } #else std::fputs("skipped: the host can commit 300 GiB, so the failed commit and arena release checks did not run\n", stderr); #endif @@ -175,6 +196,7 @@ int main() { if (large == nullptr) report("malloc(1 MiB) after a failed huge malloc", "returned NULL"); free_nid_postfix(large); } +#endif void* untouched = &failures; *__error_nid_postfix() = 77; diff --git a/core/libs/tests/GuestAmmPlacement.cpp b/core/libs/tests/GuestAmmPlacement.cpp index e52fe22b59..db65b897bf 100644 --- a/core/libs/tests/GuestAmmPlacement.cpp +++ b/core/libs/tests/GuestAmmPlacement.cpp @@ -28,9 +28,10 @@ int main() { std::uint64_t multimapStart = 0; std::uint64_t multimapEnd = 0; Require(sceAmprAmmGetVirtualAddressRanges(&start, &end, &multimapStart, &multimapEnd) == 0, "AMM ranges were not reported"); - Require(end == multimapStart && multimapEnd - start == 64ull << 30, "AMM ranges are not two adjacent 32 GiB ranges"); + Require(end - start == 256ull << 30 && end == multimapStart && multimapEnd - multimapStart == 32ull << 30, "AMM ranges are not a 256 GiB range and an adjacent 32 GiB multimap range"); Require(start >= arenaBase && multimapEnd <= arenaEnd - occupied, "AMM ranges overlap the used top of the guest arena"); - Require(GuestArena::GuestArenaHighestFree_nid_no_patch(64ull << 30, 2ull << 20) < start, "AMM ranges are not at the top of the free guest arena"); + Require(start % (64ull << 30) == 0, "the AMM range does not start on a 64 GiB boundary"); + Require(GuestArena::GuestArenaHighestFree_nid_no_patch(multimapEnd - start, 64ull << 30) < start, "AMM ranges are not at the top of the free guest arena"); std::puts("AMM ranges placed below the used top of the guest arena"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index e7e80e9e27..b17776bcd9 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -37,6 +37,10 @@ int APS5_VABI sceAmprCommandBufferSetMarkerWithColor(Apr::CommandBufferObject*, std::uint64_t APS5_VABI sceAmprMeasureCommandSizePushMarker(const char*); std::uint64_t APS5_VABI sceAmprMeasureCommandSizePushMarkerWithColor(const char*, std::uint32_t); std::uint64_t APS5_VABI sceAmprMeasureCommandSizePopMarker(); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressOnCompletion(); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteCounterOnCompletion(); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteKernelEventQueueOnCompletion(); +void* APS5_VABI sceAmprCommandBufferGetBufferBaseAddress(const Apr::CommandBufferObject*); std::uint64_t APS5_VABI sceAmprMeasureCommandSizeSetMarker(const char*); std::uint64_t APS5_VABI sceAmprMeasureCommandSizeSetMarkerWithColor(const char*, std::uint32_t); int APS5_VABI sceKernelAprSubmitCommandBuffer(const Apr::CommandBufferObject*, std::uint32_t); @@ -73,6 +77,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 +161,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 { @@ -241,6 +247,16 @@ void TestRejectedArguments() { Require(sceAmprMeasureCommandSizePushMarkerWithColor(nullptr, color) == rejected); Require(sceAmprMeasureCommandSizeSetMarker(nullptr) == rejected); Require(sceAmprMeasureCommandSizeSetMarkerWithColor(nullptr, color) == rejected); + + std::uint64_t gatherState = 0; + std::uint64_t scatterState = 0; + std::array memory{}; + Require(sceAmprCommandBufferConstructor(nullptr) == invalidArgument); + Require(sceAmprAprCommandBufferConstructor(nullptr, &gatherState, &scatterState) == invalidArgument); + Require(sceAmprAprCommandBufferConstructor(&recorder.buffer, nullptr, &scatterState) == invalidArgument); + Require(sceAmprAprCommandBufferConstructor(&recorder.buffer, &gatherState, nullptr) == invalidArgument); + Require(sceAmprCommandBufferReset(nullptr) == invalidArgument); + Require(sceAmprCommandBufferSetBuffer(nullptr, memory.data(), memory.size()) == invalidArgument); } void TestFullBuffer() { @@ -249,19 +265,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() { @@ -441,6 +474,10 @@ void TestNops() { RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNopWithData(4)); Require(std::memcmp(recorder.memory.data() + offset + sizeof(Apr::CommandHeader), data, sizeof(data)) == 0); Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 0, nullptr) == 0); + const auto emptyPayloadOffset = recorder.Offset(); + std::memset(recorder.memory.data() + emptyPayloadOffset, 0x5A, sceAmprMeasureCommandSizeNopWithData(4)); + Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 3, nullptr) == 0); + for (std::size_t index = 0; index < 12; ++index) Require(recorder.memory[emptyPayloadOffset + sizeof(Apr::CommandHeader) + index] == 0x5A); Require(sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0) == 0); const auto rejected = static_cast(static_cast(invalidArgument)); @@ -549,6 +586,10 @@ void TestVersionedCounters() { void TestClearBuffer() { Recorder recorder; + Require(sceAmprMeasureCommandSizeWriteAddressOnCompletion() == sizeof(Apr::WriteAddressCommand)); + Require(sceAmprMeasureCommandSizeWriteCounterOnCompletion() == sizeof(Apr::WriteCounterCommand)); + Require(sceAmprMeasureCommandSizeWriteKernelEventQueueOnCompletion() == sizeof(Apr::WriteKernelEventQueueCommand)); + Require(sceAmprCommandBufferGetBufferBaseAddress(&recorder.buffer) == recorder.buffer.base); Require(sceAmprCommandBufferNop(&recorder.buffer, 1) == 0); Require(recorder.Offset() != 0 && recorder.Commands() == 1); Require(sceAmprCommandBufferClearBuffer(&recorder.buffer) == recorder.memory.data()); @@ -878,8 +919,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..403c445304 100644 --- a/core/libs/tests/GuestAmprThreads.cpp +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -1,5 +1,7 @@ #include "SceTypes.hpp" +#include "prx/libc/include/exceptions/Runtime.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -59,7 +61,7 @@ int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* code); int APS5_VABI _Thrd_sleep_nid_postfix(const GuestXtime* target, GuestXtime* remaining); void APS5_VABI _Lock_shared_ptr_spin_lock_nid_postfix(void); void APS5_VABI _Unlock_shared_ptr_spin_lock_nid_postfix(void); -int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit); +int APS5_VABI sceAmprWcstombsS(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit); int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI pthread_detach_nid_postfix(Pthread thread); int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); @@ -76,6 +78,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; @@ -184,6 +189,23 @@ static void TestThreadSleep() { Require(_Thrd_sleep_nid_postfix(&negative, nullptr) == 0); Require(RealtimeNanos() >= borrowed.sec * NanosPerSecond + borrowed.nsec); +#ifdef _WIN32 + for (const auto duration : {100000LL, 2000000LL}) { + std::array samples{}; + for (auto& sample : samples) { + const auto start = std::chrono::steady_clock::now(); + const auto shortTarget = XtimeAfter(duration); + Require(_Thrd_sleep_nid_postfix(&shortTarget, nullptr) == ThrdSuccess); + sample = std::chrono::steady_clock::now() - start; + Require(RealtimeNanos() >= shortTarget.sec * NanosPerSecond + shortTarget.nsec); + } + std::sort(samples.begin(), samples.end()); + const auto median = samples[samples.size() / 2]; + std::fprintf(stderr, "_Thrd_sleep(%lld ns) median: %lld ns\n", duration, static_cast(median.count())); + Require(median < std::chrono::milliseconds(12)); + } +#endif + bool rejected = false; try { _Thrd_sleep_nid_postfix(nullptr, nullptr); @@ -211,42 +233,42 @@ static void TestWcstombs() { char buffer[8]; std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, sizeof(buffer), text, sizeof(buffer)) == 0); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), text, sizeof(buffer)) == 0); Require(result == 3 && std::strcmp(buffer, "abc") == 0); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, sizeof(buffer), text, 2) == 0); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), text, 2) == 0); Require(result == 2 && std::strcmp(buffer, "ab") == 0); result = 0; - Require(wcstombs_s_nid_postfix(&result, nullptr, 0, text, 0) == 0); + Require(sceAmprWcstombsS(&result, nullptr, 0, text, 0) == 0); Require(result == 3); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, 3, text, 3) == GuestErange); + Require(sceAmprWcstombsS(&result, buffer, 3, text, 3) == GuestErange); Require(result == Failed && buffer[0] == '\0'); result = 0; - Require(wcstombs_s_nid_postfix(&result, nullptr, 4, text, 4) == GuestEinval); + Require(sceAmprWcstombsS(&result, nullptr, 4, text, 4) == GuestEinval); Require(result == Failed); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, sizeof(buffer), nullptr, sizeof(buffer)) == GuestEinval); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), nullptr, sizeof(buffer)) == GuestEinval); Require(result == Failed && buffer[0] == '\0'); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(nullptr, buffer, sizeof(buffer), text, sizeof(buffer)) == GuestEinval); + Require(sceAmprWcstombsS(nullptr, buffer, sizeof(buffer), text, sizeof(buffer)) == GuestEinval); Require(buffer[0] == '\0'); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, 0, text, 4) == GuestEinval); + Require(sceAmprWcstombsS(&result, buffer, 0, text, 4) == GuestEinval); Require(result == Failed && buffer[0] == 'x'); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, sizeof(buffer), wide, sizeof(buffer)) == GuestEilseq); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), wide, sizeof(buffer)) == GuestEilseq); Require(result == Failed && buffer[0] == 'a' && buffer[1] == '\0'); - Require(wcstombs_s_nid_postfix(&result, nullptr, 0, wide, 0) == GuestEilseq); + Require(sceAmprWcstombsS(&result, nullptr, 0, wide, 0) == GuestEilseq); Require(result == Failed); } @@ -487,11 +509,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 +521,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 +554,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 +577,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..42be4b89b2 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include "ProcessParametersFixture.hpp" extern "C" { int APS5_VABI sceAppContentInitialize(const AppContentInitParam*, AppContentBootParam*); @@ -12,6 +14,8 @@ 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 sceAppContentTemporaryDataMount(AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t, AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint*); @@ -33,6 +37,25 @@ static constexpr int ErrorNotMounted = static_cast(0x80D90004); static void Require(bool value) { if (!value) std::abort(); } int main() { + static std::array process{}; + Require(ProcessParametersFixture::Install(process.data(), process.size())); + std::uint32_t sdkVersion = 0x01500000; + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + constexpr int notInitialized = static_cast(0x80D90001u); + Require(sceAppContentAddcontMount(0, nullptr, nullptr) == notInitialized); + Require(sceAppContentAddcontUnmount(nullptr) == notInitialized); + Require(sceAppContentGetAddcontInfo(0, nullptr, nullptr) == notInitialized); + Require(sceAppContentGetAddcontInfoList(0, nullptr, 0, nullptr) == notInitialized); + Require(sceAppContentAppParamGetInt(0, nullptr) == notInitialized); + Require(sceAppContentTemporaryDataMount2(0, nullptr) == notInitialized); + Require(sceAppContentTemporaryDataUnmount(nullptr) == notInitialized); + Require(sceAppContentTemporaryDataFormat(nullptr) == notInitialized); + Require(sceAppContentTemporaryDataGetAvailableSpaceKb(nullptr, nullptr) == notInitialized); + Require(sceAppContentDownloadDataFormat(nullptr) == notInitialized); + Require(sceAppContentDownloadDataGetAvailableSpaceKb(nullptr, nullptr) == notInitialized); + Require(sceAppContentAddcontDelete(0, nullptr) == notInitialized); + Require(sceAppContentAddcontEnqueueDownload(0, nullptr) == notInitialized); + Require(sceAppContentGetAddcontDownloadProgress(0, nullptr, nullptr) == notInitialized); const auto original = std::filesystem::current_path(); const auto directory = original / ("appcontent-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(directory)); @@ -44,12 +67,38 @@ int main() { R"("userDefinedParam1":23566,"userDefinedParam2":-7,"userDefinedParam3":0,"userDefinedParam4":2147483647})"; Require(static_cast(param)); } + AppContentInitParam init{}; + AppContentBootParam boot{}; + Require(sceAppContentInitialize(nullptr, &boot) == ErrorParameter); + Require(sceAppContentInitialize(&init, nullptr) == ErrorParameter); + init.reserved[31] = 1; + Require(sceAppContentInitialize(&init, &boot) == ErrorParameter); + init.reserved[31] = 0; + boot.reserved1[3] = 1; + Require(sceAppContentInitialize(&init, &boot) == ErrorParameter); + boot.reserved1[3] = 0; + boot.reserved2[31] = 1; + Require(sceAppContentInitialize(&init, &boot) == ErrorParameter); + boot.reserved2[31] = 0; + boot.attr = 0xffffffffu; + Require(sceAppContentInitialize(&init, &boot) == 0); + Require(boot.attr == 0); + Require(sceAppContentInitialize(nullptr, nullptr) == ErrorBusy); + sdkVersion = 0x01400000; + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + Require(sceAppContentInitialize(nullptr, &boot) == 0); AppContentMountPoint download{}; std::memcpy(download.data, "/download0", 11); size_t availableKb = 12345; Require(sceAppContentDownloadDataGetAvailableSpaceKb(&download, &availableKb) == 0); Require(availableKb == 0); Require(sceAppContentDownloadDataGetAvailableSpaceKb(&download, nullptr) == ErrorParameter); + availableKb = 12345; + Require(sceAppContentDownloadDataGetAvailableSpaceKb(nullptr, &availableKb) == ErrorParameter); + Require(availableKb == 12345); + Require(sceAppContentDownloadDataGetAvailableSpaceKb(nullptr, nullptr) == ErrorParameter); + AppContentMountPoint invalidMount{}; + Require(sceAppContentDownloadDataGetAvailableSpaceKb(&invalidMount, &availableKb) == ErrorNotMounted); const auto downloadData = ResolvePath_nid_no_patch("/download0"); Require(sceAppContentDownloadDataFormat(&download) == 0); Require(std::filesystem::is_directory(downloadData) && std::filesystem::is_empty(downloadData)); @@ -57,6 +106,7 @@ int main() { { std::ofstream file(downloadData / "cache" / "old.bin"); file << "downloaded"; } { std::ofstream file(downloadData / "index.dat"); file << "downloaded"; } Require(sceAppContentDownloadDataFormat(nullptr) == ErrorParameter); + Require(sceAppContentDownloadDataFormat(&invalidMount) == ErrorNotMounted); Require(std::filesystem::exists(downloadData / "index.dat")); Require(sceAppContentDownloadDataFormat(&download) == 0); Require(std::filesystem::is_directory(downloadData) && std::filesystem::is_empty(downloadData)); @@ -88,12 +138,20 @@ 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); std::filesystem::create_directory(directory / "temp0"); { std::ofstream file(directory / "temp0" / "old.txt"); file << "previous run"; } - AppContentBootParam boot{}; Require(sceAppContentInitialize(nullptr, &boot) == 0); Require(access_nid_postfix("/temp0/old.txt", 0) == -1); Require(sceAppContentTemporaryDataUnmount(nullptr) == ErrorParameter); @@ -134,6 +192,17 @@ int main() { Require(sceAppContentTemporaryDataMount2(1, &mountPoint) == 0); Require(std::filesystem::is_empty(backing)); Require(sceAppContentTemporaryDataUnmount(&mountPoint) == 0); + Require(sceAppContentTemporaryDataMount(nullptr) == ErrorParameter); + { std::ofstream file(backing / "remove.txt"); file << "format"; } + std::memset(&mountPoint, 0x5a, sizeof(mountPoint)); + Require(sceAppContentTemporaryDataMount(&mountPoint) == 0); + Require(std::strcmp(mountPoint.data, "/temp0") == 0); + Require(std::filesystem::is_empty(backing)); + Require(sceAppContentTemporaryDataMount(&second) == ErrorBusy); + Require(sceAppContentTemporaryDataMount2(0, &second) == ErrorBusy); + Require(sceAppContentTemporaryDataUnmount(&mountPoint) == 0); + Require(sceAppContentTemporaryDataMount(&mountPoint) == 0); + Require(sceAppContentTemporaryDataUnmount(&mountPoint) == 0); unsigned char info[24]; std::memset(info, 0x5a, sizeof(info)); unsigned char untouchedInfo[24]; @@ -143,6 +212,24 @@ int main() { Require(sceAppContentGetAddcontInfo(0, nullptr, info) == ErrorParameter); Require(sceAppContentGetAddcontInfo(0, &label, nullptr) == ErrorParameter); + for (const std::uint32_t version : {0x014fffffu, 0x01500000u, 0x034fffffu, 0x03500000u}) { + std::memcpy(process.data() + 16, &version, sizeof(version)); + for (char& padding : label.padding) { + padding = 1; + const int earlyError = version >= 0x01500000 ? ErrorParameter : ErrorNotFound; + const int infoError = version >= 0x01500000 ? ErrorParameter : ErrorDrmNoEntitlement; + const int lateError = version >= 0x03500000 ? ErrorParameter : ErrorNotFound; + Require(sceAppContentAddcontMount(0, &label, &mountPoint) == earlyError); + Require(sceAppContentGetAddcontInfo(0, &label, info) == infoError); + Require(sceAppContentAddcontEnqueueDownload(0, &label) == earlyError); + Require(sceAppContentAddcontDelete(0, &label) == lateError); + Require(sceAppContentGetAddcontDownloadProgress(0, &label, progress) == lateError); + Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); + Require(std::memcmp(progress, progressBefore, sizeof(progress)) == 0); + padding = 0; + } + } + uint32_t hitNum = 0x5a5a5a5a; Require(sceAppContentGetAddcontInfoList(0, nullptr, 0, &hitNum) == 0); Require(hitNum == 0); @@ -156,7 +243,7 @@ int main() { Require(sceAppContentGetAddcontInfoList(0, info, 1, &hitNum) == 0); Require(hitNum == 0); Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); - Require(sceAppContentGetAddcontInfoList(0, info, 1, nullptr) == 0); + Require(sceAppContentGetAddcontInfoList(0, info, 1, nullptr) == ErrorParameter); Require(sceAppContentGetAddcontInfoList(0, nullptr, 0, nullptr) == ErrorParameter); Require(sceAppContentGetAddcontInfoList(0, info, 0, nullptr) == ErrorParameter); Require(sceAppContentGetAddcontInfoList(0, nullptr, 1, nullptr) == ErrorParameter); diff --git a/core/libs/tests/GuestAppIcon.cpp b/core/libs/tests/GuestAppIcon.cpp new file mode 100644 index 0000000000..0657ac60f2 --- /dev/null +++ b/core/libs/tests/GuestAppIcon.cpp @@ -0,0 +1,126 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool value) { + if (!value) std::abort(); +} + +int CheckNoIcon(const char* root) { + std::filesystem::current_path(root); + Require(!HasAppIcon_nid_postfix()); + bool threw = false; + try { + GetAppIconData_nid_postfix(); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find("icon not available") != std::string_view::npos; + } + Require(threw); + return 0; +} + +int CheckValidIcon(const char* root) { + std::filesystem::current_path(root); + Require(HasAppIcon_nid_postfix()); + const auto icon = GetAppIconData_nid_postfix(); + Require(icon.bytes != nullptr); + Require(icon.size == 16); + Require(icon.bytes[0] == 0x89 && icon.bytes[1] == 'P' && icon.bytes[2] == 'N' && icon.bytes[3] == 'G'); + return 0; +} + +int CheckEmptyIcon(const char* root) { + std::filesystem::current_path(root); + bool threw = false; + try { + HasAppIcon_nid_postfix(); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find("icon0.png is empty or unreadable") != std::string_view::npos; + } + Require(threw); + return 0; +} + +int CheckCorruptPng(const char* root) { + std::filesystem::current_path(root); + bool threw = false; + try { + HasAppIcon_nid_postfix(); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find("icon0.png is not a valid PNG file") != std::string_view::npos; + } + Require(threw); + return 0; +} + +int CheckConcurrentAccess(const char* root) { + std::filesystem::current_path(root); + std::vector threads; + for (int i = 0; i < 8; ++i) { + threads.emplace_back([] { + Require(HasAppIcon_nid_postfix()); + const auto icon = GetAppIconData_nid_postfix(); + Require(icon.size == 16); + }); + } + for (auto& t : threads) t.join(); + return 0; +} + +void RunCase(const char* self, const char* mode, std::initializer_list> files) { + const auto root = std::filesystem::temp_directory_path() / ("guest_app_icon_" + std::to_string(std::random_device{}())); + std::filesystem::remove_all(root); + std::filesystem::create_directories(root / "app0" / "sce_sys"); + for (const auto& [name, content] : files) { + std::ofstream stream(root / "app0" / "sce_sys" / name, std::ios::binary); + stream.write(content.data(), static_cast(content.size())); + } + const std::string arguments = "\"" + std::string(self) + "\" " + mode + " \"" + root.string() + "\""; +#ifdef _WIN32 + const std::string command = "\"" + arguments + "\""; +#else + const std::string command = arguments; +#endif + const int status = std::system(command.c_str()); + std::filesystem::remove_all(root); + Require(status == 0); +} + +} + +int main(int argc, char** argv) { + if (argc == 3) { + const std::string_view mode = argv[1]; + if (mode == "--no-icon") return CheckNoIcon(argv[2]); + if (mode == "--valid-icon") return CheckValidIcon(argv[2]); + if (mode == "--empty-icon") return CheckEmptyIcon(argv[2]); + if (mode == "--corrupt-png") return CheckCorruptPng(argv[2]); + if (mode == "--concurrent") return CheckConcurrentAccess(argv[2]); + return 1; + } + + const std::string validPng("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00", 16); + const std::string corruptPng("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", 16); + const std::string emptyPng(""); + + RunCase(argv[0], "--no-icon", {}); + RunCase(argv[0], "--valid-icon", {{"icon0.png", validPng}}); + RunCase(argv[0], "--empty-icon", {{"icon0.png", emptyPng}}); + RunCase(argv[0], "--corrupt-png", {{"icon0.png", corruptPng}}); + RunCase(argv[0], "--concurrent", {{"icon0.png", validPng}}); + + return 0; +} diff --git a/core/libs/tests/GuestAppInfo.cpp b/core/libs/tests/GuestAppInfo.cpp index e65d891a06..860ff13718 100644 --- a/core/libs/tests/GuestAppInfo.cpp +++ b/core/libs/tests/GuestAppInfo.cpp @@ -12,6 +12,7 @@ extern "C" { int APS5_VABI sceKernelGetAppInfo(int pid, void* info); int APS5_VABI getpid_nid_postfix(void); int APS5_VABI sceKernelUuidCreate(std::uint32_t* uuid); +int APS5_VABI sceKernelCreateUuidV4(std::uint8_t* uuid); } static constexpr int SCE_KERNEL_ERROR_EPERM = static_cast(0x80020001); @@ -26,6 +27,29 @@ struct AppInfo { char unknown2[0x3c]; }; +static void WriteParamSfo(const std::filesystem::path& path) { + const char title[] = "Example"; + const char titleId[] = "CUSA00000"; + const char keys[16] = "TITLE\0TITLE_ID"; + unsigned char bytes[86] = {0, 'P', 'S', 'F', 1, 1, 0, 0, 52, 0, 0, 0, 68, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 4, 2, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, + 6, 0, 4, 2, 10, 0, 0, 0, 10, 0, 0, 0, 8, 0, 0, 0}; + std::memcpy(bytes + 52, keys, sizeof(keys)); + std::memcpy(bytes + 68, title, sizeof(title)); + std::memcpy(bytes + 76, titleId, sizeof(titleId)); + std::ofstream(path, std::ios::binary).write(reinterpret_cast(bytes), sizeof(bytes)); +} + +static int RunSelf(const char* program, std::string_view mode, const std::filesystem::path& directory) { + const std::string arguments = "\"" + std::string(program) + "\" " + std::string(mode) + " \"" + directory.string() + "\""; +#ifdef _WIN32 + const std::string command = "\"" + arguments + "\""; +#else + const std::string command = arguments; +#endif + return std::system(command.c_str()); +} + static std::filesystem::path EnterTemporaryRoot(std::string_view name) { const auto root = std::filesystem::temp_directory_path() / (std::string(name) + std::to_string(getpid_nid_postfix())); std::filesystem::remove_all(root); @@ -40,13 +64,21 @@ int main(int argc, char** argv) { AppInfo info{}; try { sceKernelGetAppInfo(getpid_nid_postfix(), &info); } catch (const std::runtime_error& error) { - return std::string_view(error.what()) == "param.json not found" ? 0 : 1; + return std::string_view(error.what()) == "param.json or param.sfo not found" ? 0 : 1; } return 1; } + if (argc == 3 && std::string_view(argv[1]) == "--with-param-sfo") { + std::filesystem::current_path(argv[2]); + AppInfo info{}; + Require(sceKernelGetAppInfo(getpid_nid_postfix(), &info) == 0); + Require(std::strcmp(info.titleId, "CUSA00000") == 0); + return 0; + } const auto previous = std::filesystem::current_path(); const auto root = EnterTemporaryRoot("guest_app_info_"); std::filesystem::create_directories(root / "app0" / "sce_sys"); + WriteParamSfo(root / "app0" / "sce_sys" / "param.sfo"); std::ofstream(root / "app0" / "sce_sys" / "param.json") << "{\n \"contentId\": \"IV0000-PPSA97358_00-XPSEMU0000000000\",\n \"localizedParameters\": {\"defaultLanguage\": \"en-US\", \"en-US\": {\"titleName\": \"XPSemu\"}},\n \"titleId\": \"PPSA97358\"\n}\n"; AppInfo info; std::memset(&info, 0x5a, sizeof(info)); @@ -63,14 +95,14 @@ int main(int argc, char** argv) { const auto empty = std::filesystem::temp_directory_path() / ("guest_app_info_empty_" + std::to_string(self)); std::filesystem::remove_all(empty); std::filesystem::create_directories(empty); - const std::string arguments = "\"" + std::string(argv[0]) + "\" --without-param-json \"" + empty.string() + "\""; -#ifdef _WIN32 - const std::string command = "\"" + arguments + "\""; -#else - const std::string command = arguments; -#endif - Require(std::system(command.c_str()) == 0); + Require(RunSelf(argv[0], "--without-param-json", empty) == 0); std::filesystem::remove_all(empty); + const auto sfo = std::filesystem::temp_directory_path() / ("guest_app_info_sfo_" + std::to_string(self)); + std::filesystem::remove_all(sfo); + std::filesystem::create_directories(sfo / "app0" / "sce_sys"); + WriteParamSfo(sfo / "app0" / "sce_sys" / "param.sfo"); + Require(RunSelf(argv[0], "--with-param-sfo", sfo) == 0); + std::filesystem::remove_all(sfo); Require(sceKernelUuidCreate(nullptr) == SCE_KERNEL_ERROR_EINVAL); std::uint32_t first[4]{}; @@ -78,4 +110,11 @@ int main(int argc, char** argv) { Require(sceKernelUuidCreate(first) == 0 && sceKernelUuidCreate(second) == 0); for (const std::uint32_t* uuid : {first, second}) Require((uuid[1] >> 28) == 4u && (uuid[2] & 0xc0u) == 0x80u); Require(std::memcmp(first, second, sizeof(first)) != 0); + + Require(sceKernelCreateUuidV4(nullptr) == SCE_KERNEL_ERROR_EINVAL); + std::uint8_t third[16]{}; + std::uint8_t fourth[16]{}; + Require(sceKernelCreateUuidV4(third) == 0 && sceKernelCreateUuidV4(fourth) == 0); + for (const std::uint8_t* uuid : {third, fourth}) Require((uuid[6] >> 4) == 4u && (uuid[8] & 0xc0u) == 0x80u); + Require(std::memcmp(third, fourth, sizeof(third)) != 0); } diff --git a/core/libs/tests/GuestAppendStreams.cpp b/core/libs/tests/GuestAppendStreams.cpp new file mode 100644 index 0000000000..2fb3939330 --- /dev/null +++ b/core/libs/tests/GuestAppendStreams.cpp @@ -0,0 +1,137 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +FileStream* APS5_VABI freopen_nid_postfix(const char*, const char*, FileStream*); +std::size_t APS5_VABI fwrite_nid_postfix(const void*, std::size_t, std::size_t, FileStream*); +std::size_t APS5_VABI fread_nid_postfix(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 fflush_nid_postfix(FileStream*); +int APS5_VABI fclose_nid_postfix(FileStream*); +int APS5_VABI fileno_nid_postfix(FileStream*); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +int APS5_VABI flock_nid_postfix(int, int); +int* APS5_VABI __error_nid_postfix(); +} + +static void Check(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "append stream check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr const char* Path = "guest_append_streams.txt"; +static constexpr int Writers = 4; +static constexpr int LinesPerWriter = 1500; +static constexpr std::size_t LineLength = 32; +static constexpr int LockExclusive = 2; +static constexpr int LockNonblocking = 4; +static constexpr int LockUnlock = 8; +static constexpr int GuestEwouldblock = 35; + +static std::string Line(int writer, int index) { + std::string line = "s" + std::to_string(writer) + " " + std::to_string(index) + " "; + line.resize(LineLength - 1, '.'); + return line + "\n"; +} + +static std::string Contents() { + std::ifstream input(Path, std::ios::binary); + return {std::istreambuf_iterator(input), std::istreambuf_iterator()}; +} + +static FileStream* OpenWriter(int writer) { + if (writer == 0) return fopen_nid_postfix(Path, "a"); + if (writer == 1) return fopen_nid_postfix(Path, "a+"); + const std::string other = std::string(Path) + "." + std::to_string(writer); + auto* stream = fopen_nid_postfix(other.c_str(), "w"); + Require(stream != nullptr); + return freopen_nid_postfix(Path, writer == 2 ? "a" : "ab", stream); +} + +static void WriteLines(int writer) { + auto* stream = OpenWriter(writer); + Require(stream != nullptr); + for (int index = 0; index < LinesPerWriter; ++index) { + const std::string line = Line(writer, index); + Require(fwrite_nid_postfix(line.data(), 1, line.size(), stream) == line.size()); + Require(fflush_nid_postfix(stream) == 0); + } + Require(fclose_nid_postfix(stream) == 0); +} + +static void CheckConcurrentStreams() { + std::filesystem::remove(Path); + std::vector writers; + for (int writer = 0; writer < Writers; ++writer) writers.emplace_back(WriteLines, writer); + for (auto& writer : writers) writer.join(); + const std::string contents = Contents(); + Require(contents.size() == static_cast(Writers) * LinesPerWriter * LineLength); + std::set lines; + for (std::size_t offset = 0; offset < contents.size(); offset += LineLength) lines.insert(contents.substr(offset, LineLength)); + for (int writer = 0; writer < Writers; ++writer) + for (int index = 0; index < LinesPerWriter; ++index) Require(lines.count(Line(writer, index)) == 1); +} + +static void CheckReadAndAppend() { + const std::string before = Contents(); + auto* stream = fopen_nid_postfix(Path, "a+"); + Require(stream != nullptr && fseek_nid_postfix(stream, 0, SEEK_SET) == 0); + char start[LineLength]{}; + Require(fread_nid_postfix(start, 1, LineLength, stream) == LineLength && before.compare(0, LineLength, start, LineLength) == 0); + Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0 && fwrite_nid_postfix("x", 1, 1, stream) == 1 && fflush_nid_postfix(stream) == 0); + Require(ftell_nid_postfix(stream) == static_cast(before.size() + 1)); + Require(fclose_nid_postfix(stream) == 0); + Require(Contents() == before + "x"); +} + +static void CheckTruncate() { + const std::string before = Contents(); + auto* stream = fopen_nid_postfix(Path, "a"); + Require(stream != nullptr); + Require(ftruncate_nid_postfix(fileno_nid_postfix(stream), 5) == 0); + Require(fwrite_nid_postfix("yz", 1, 2, stream) == 2 && fflush_nid_postfix(stream) == 0); + Require(fclose_nid_postfix(stream) == 0); + Require(Contents() == before.substr(0, 5) + "yz"); +} + +static void CheckLock() { + auto* holder = fopen_nid_postfix(Path, "a"); + auto* other = fopen_nid_postfix(Path, "r"); + Require(holder != nullptr && other != nullptr); + Require(flock_nid_postfix(fileno_nid_postfix(holder), LockExclusive | LockNonblocking) == 0); + Require(flock_nid_postfix(fileno_nid_postfix(other), LockExclusive | LockNonblocking) == -1 && *__error_nid_postfix() == GuestEwouldblock); + Require(flock_nid_postfix(fileno_nid_postfix(holder), LockUnlock) == 0); + Require(flock_nid_postfix(fileno_nid_postfix(other), LockExclusive | LockNonblocking) == 0); + Require(flock_nid_postfix(fileno_nid_postfix(other), LockUnlock) == 0); + Require(flock_nid_postfix(fileno_nid_postfix(holder), LockExclusive | LockNonblocking) == 0); + Require(fclose_nid_postfix(holder) == 0); + Require(flock_nid_postfix(fileno_nid_postfix(other), LockExclusive | LockNonblocking) == 0); + Require(fclose_nid_postfix(other) == 0); +} + +int main() { + CheckConcurrentStreams(); + CheckReadAndAppend(); + CheckTruncate(); + CheckLock(); + std::filesystem::remove(Path); + for (int writer = 2; writer < Writers; ++writer) std::filesystem::remove(std::string(Path) + "." + std::to_string(writer)); + return 0; +} diff --git a/core/libs/tests/GuestAppendWrites.cpp b/core/libs/tests/GuestAppendWrites.cpp new file mode 100644 index 0000000000..0780e303eb --- /dev/null +++ b/core/libs/tests/GuestAppendWrites.cpp @@ -0,0 +1,110 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GuestIovec { + const void* base; + std::size_t length; +}; + +extern "C" { +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelWrite(int, const void*, std::size_t); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +std::int64_t APS5_VABI writev_nid_postfix(int, const GuestIovec*, int); +int APS5_VABI dup_nid_postfix(int); +} + +static void Check(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "append check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr const char* Path = "guest_append_writes.txt"; +static constexpr int CreateWriteAppend = 0x0209; +static constexpr int Truncate = 0x0400; +static constexpr int FromStart = 0; +static constexpr int FromCurrent = 1; +static constexpr int Writers = 4; +static constexpr int LinesPerWriter = 1500; +static constexpr std::size_t LineLength = 32; + +static std::string Line(int writer, int index) { + std::string line = "w" + std::to_string(writer) + " " + std::to_string(index) + " "; + line.resize(LineLength - 1, '.'); + return line + "\n"; +} + +static void WriteLines(int fd, int writer) { + for (int index = 0; index < LinesPerWriter; ++index) { + const std::string line = Line(writer, index); + std::int64_t written = -1; + if (writer == 0 || writer == 2) { + written = sceKernelWrite(fd, line.data(), line.size()); + } else if (writer == 1) { + written = write_nid_postfix(fd, line.data(), static_cast(line.size())); + } else { + const GuestIovec parts[2]{{line.data(), 7}, {line.data() + 7, line.size() - 7}}; + written = writev_nid_postfix(fd, parts, 2); + } + Require(written == static_cast(LineLength)); + } +} + +static std::string Contents() { + std::ifstream file(Path, std::ios::binary); + return std::string(std::istreambuf_iterator(file), std::istreambuf_iterator()); +} + +int main() { + const int first = sceKernelOpen(Path, CreateWriteAppend | Truncate, 0644); + Require(first >= 0); + const int second = sceKernelOpen(Path, CreateWriteAppend, 0644); + const int duplicate = dup_nid_postfix(first); + const int vectored = sceKernelOpen(Path, CreateWriteAppend, 0644); + Require(second >= 0 && duplicate >= 0 && vectored >= 0); + + const int descriptors[Writers]{first, second, duplicate, vectored}; + std::vector threads; + for (int writer = 0; writer < Writers; ++writer) threads.emplace_back(WriteLines, descriptors[writer], writer); + for (auto& thread : threads) thread.join(); + + const std::string contents = Contents(); + Require(contents.size() == static_cast(Writers) * LinesPerWriter * LineLength); + std::set> seen; + for (std::size_t offset = 0; offset < contents.size(); offset += LineLength) { + const std::string line = contents.substr(offset, LineLength); + int writer = -1; + int index = -1; + Require(std::sscanf(line.c_str(), "w%d %d ", &writer, &index) == 2); + Require(writer >= 0 && writer < Writers && index >= 0 && index < LinesPerWriter); + Require(line == Line(writer, index)); + Require(seen.emplace(writer, index).second); + } + Require(seen.size() == static_cast(Writers) * LinesPerWriter); + + const auto size = static_cast(contents.size()); + Require(sceKernelLseek(first, 0, FromStart) == 0); + const std::string tail = Line(9, 0); + Require(sceKernelWrite(first, tail.data(), tail.size()) == static_cast(LineLength)); + Require(sceKernelLseek(first, 0, FromCurrent) == size + static_cast(LineLength)); + Require(Contents().substr(static_cast(size)) == tail); + + for (const int fd : descriptors) Require(sceKernelClose(fd) == 0); + std::remove(Path); +} diff --git a/core/libs/tests/GuestAprRead.cpp b/core/libs/tests/GuestAprRead.cpp new file mode 100644 index 0000000000..a8ba636dc3 --- /dev/null +++ b/core/libs/tests/GuestAprRead.cpp @@ -0,0 +1,295 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif + +extern "C" { +int APS5_VABI sceKernelAprResolveFilepathsToIds(const char**, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceKernelAprSubmitCommandBuffer(const Apr::CommandBufferObject*, std::uint32_t); +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 sceKernelMunmap(void*, std::size_t); +int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); +int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); +void* APS5_VABI fopen_nid_postfix(const char*, const char*); +int APS5_VABI fclose_nid_postfix(void*); +} + +namespace { + +constexpr std::size_t FileBytes = (16u << 20u) + 257u; +constexpr std::size_t MappingBytes = 17u << 20u; +constexpr const char* FilePath = "apr_read_generated.bin"; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::uint8_t FileByte(std::uint64_t offset) { + return static_cast((offset * 37u) ^ (offset >> 8u) ^ (offset >> 16u)); +} + +class Fixture { +public: + Fixture() { + std::ofstream stream(FilePath, std::ios::binary | std::ios::trunc); + std::array chunk; + for (std::size_t offset = 0; offset < FileBytes; offset += chunk.size()) { + const auto bytes = std::min(chunk.size(), FileBytes - offset); + for (std::size_t index = 0; index < bytes; ++index) chunk[index] = static_cast(FileByte(offset + index)); + stream.write(chunk.data(), static_cast(bytes)); + } + stream.close(); + Require(!stream.fail(), "cannot create the synthetic APR file"); + const char* path = FilePath; + std::uint32_t failed = 0; + Require(sceKernelAprResolveFilepathsToIds(&path, 1, &id, &failed) == 0 && failed == 0, "cannot resolve the synthetic APR file"); + } + + ~Fixture() { + std::error_code ignored; + std::filesystem::remove(FilePath, ignored); + } + + void Read(void* destination, std::uint64_t bytes, std::uint64_t offset) const { + const Apr::ReadFileCommand command{{Apr::Opcode::ReadFile, sizeof(Apr::ReadFileCommand)}, id, 0, + reinterpret_cast(destination), bytes, offset}; + alignas(8) std::array memory{}; + std::memcpy(memory.data(), &command, sizeof(command)); + Apr::CommandBufferObject buffer{Apr::BufferType::Generic, 0, sizeof(memory), 1, sizeof(memory), memory.data()}; + Require(sceKernelAprSubmitCommandBuffer(&buffer, 0) == 0, "APR read submission failed"); + } + +private: + std::uint32_t id = 0; +}; + +class DirectBuffer { +public: + DirectBuffer() { + Require(sceKernelAllocateDirectMemory(0, 13824ll << 20, MappingBytes, 0x4000, 0, &physical) == 0, "cannot allocate direct memory"); + if (sceKernelMapDirectMemory(&pointer, MappingBytes, 0x33, 0, physical, 0x4000) != 0) { + sceKernelReleaseDirectMemory(physical, MappingBytes); + throw std::runtime_error("cannot map direct memory"); + } + } + + ~DirectBuffer() { + sceKernelMunmap(pointer, MappingBytes); + sceKernelReleaseDirectMemory(physical, MappingBytes); + } + + std::uint8_t* Data() const { return static_cast(pointer); } + +private: + void* pointer = nullptr; + std::int64_t physical = 0; +}; + +void CheckRead(const Fixture& fixture, std::uint8_t* data, std::size_t bytes, std::uint64_t offset) { + data[-1] = 0xA5; + data[bytes] = 0x5A; + fixture.Read(data, bytes, offset); + Require(data[-1] == 0xA5 && data[bytes] == 0x5A, "APR wrote outside the requested range"); + for (std::size_t index = 0; index < bytes; ++index) { + const auto expected = offset + index < FileBytes ? FileByte(offset + index) : std::uint8_t{0}; + Require(data[index] == expected, "APR returned incorrect bytes or did not zero-fill EOF"); + } +} + +void TestRead(const Fixture& fixture, std::uint8_t* data) { + CheckRead(fixture, data + 17, (16u << 20u) + 13u, 7); + for (const auto bytes : {std::size_t{0}, std::size_t{1}, std::size_t{65535}, std::size_t{65536}, std::size_t{65537}}) { + CheckRead(fixture, data + 17, bytes, 31); + } +} + +void TestEof(const Fixture& fixture) { + std::vector storage(131200, 0xCC); + auto* data = storage.data() + 17; + CheckRead(fixture, data, 131073, FileBytes - 65537); + CheckRead(fixture, data, 65537, FileBytes); + CheckRead(fixture, data, 65537, FileBytes + 19); +} + +void TestErrors(const Fixture& fixture) { + std::array data; + data.fill(0xA5); + bool rejected = false; + try { + fixture.Read(data.data(), data.size(), std::uint64_t{1} << 63u); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected, "APR accepted an unseekable offset"); + for (const auto byte : data) Require(byte == 0xA5, "failed APR read modified the destination"); +#ifdef _WIN32 + { + const DirectBuffer storage; + storage.Data()[17] = 0xA5; + Require(sceKernelMprotect(storage.Data(), MappingBytes, 0x11) == 0, "cannot protect the direct read buffer"); + rejected = false; + try { + fixture.Read(storage.Data() + 17, 65537, 0); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).find("not writable guest memory") != std::string::npos; + } + Require(rejected && storage.Data()[17] == 0xA5, "APR did not reject a read-only guest destination"); + } +#endif + Require(std::filesystem::remove(FilePath), "cannot remove the synthetic APR file"); + rejected = false; + try { + fixture.Read(data.data(), data.size(), 0); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected, "APR accepted a missing file"); + for (const auto byte : data) Require(byte == 0xA5, "missing-file APR read modified the destination"); +} + +void TestStreams(const Fixture& fixture) { + constexpr int StreamCount = 600; + std::vector streams; + for (int index = 0; index < StreamCount; ++index) { + void* stream = fopen_nid_postfix(("apr_stream_" + std::to_string(index) + ".tmp").c_str(), "w"); + if (stream == nullptr) break; + streams.push_back(stream); + } + const bool held = streams.size() == StreamCount; + std::vector storage(131200, 0xCC); + std::string error; + try { + CheckRead(fixture, storage.data() + 17, 65537, 0); + } catch (const std::runtime_error& failure) { + error = failure.what(); + } + for (void* stream : streams) fclose_nid_postfix(stream); + for (int index = 0; index < StreamCount; ++index) std::filesystem::remove("apr_stream_" + std::to_string(index) + ".tmp"); + Require(error.empty(), ("APR read failed while " + std::to_string(streams.size()) + " guest streams were open: " + error).c_str()); + Require(held, "cannot hold 600 guest fopen streams"); +} + +#ifdef _WIN32 +std::uintptr_t faultBegin = 0; +std::size_t faultBytes = 0; +std::size_t faultPage = 0; +std::size_t committedPages = 0; + +LONG CALLBACK CommitOnWrite(EXCEPTION_POINTERS* exception) { + const auto& record = *exception->ExceptionRecord; + if (record.ExceptionCode != EXCEPTION_ACCESS_VIOLATION || record.NumberParameters < 2 || record.ExceptionInformation[0] != 1) return EXCEPTION_CONTINUE_SEARCH; + const auto address = static_cast(record.ExceptionInformation[1]); + if (address < faultBegin || address - faultBegin >= faultBytes) return EXCEPTION_CONTINUE_SEARCH; + const auto page = address & ~(static_cast(faultPage) - 1); + if (VirtualAlloc(reinterpret_cast(page), faultPage, MEM_COMMIT, PAGE_READWRITE) == nullptr) return EXCEPTION_CONTINUE_SEARCH; + ++committedPages; + return EXCEPTION_CONTINUE_EXECUTION; +} + +class FaultBuffer { +public: + FaultBuffer() { + SYSTEM_INFO system{}; + GetSystemInfo(&system); + pointer = VirtualAlloc(nullptr, MappingBytes, MEM_RESERVE, PAGE_NOACCESS); + Require(pointer != nullptr, "cannot reserve the fault-backed read buffer"); + handler = AddVectoredExceptionHandler(1, CommitOnWrite); + if (handler == nullptr) { + VirtualFree(pointer, 0, MEM_RELEASE); + throw std::runtime_error("cannot install the synthetic page fault handler"); + } + faultPage = system.dwPageSize; + faultBytes = MappingBytes; + faultBegin = reinterpret_cast(pointer); + committedPages = 0; + } + + ~FaultBuffer() { + faultBytes = 0; + RemoveVectoredExceptionHandler(handler); + VirtualFree(pointer, 0, MEM_RELEASE); + } + + std::uint8_t* Data() const { return static_cast(pointer); } + +private: + void* pointer = nullptr; + void* handler = nullptr; +}; + +struct SmallStackRead { + const Fixture* fixture = nullptr; + std::vector* storage = nullptr; + std::string error; +}; + +DWORD WINAPI ReadOnSmallStack(void* parameter) { + auto& read = *static_cast(parameter); + try { + CheckRead(*read.fixture, read.storage->data() + 17, 65537, 0); + } catch (const std::exception& failure) { + read.error = failure.what(); + } + return 0; +} + +void TestSmallStack(const Fixture& fixture) { + std::vector storage(131200, 0xCC); + SmallStackRead read{&fixture, &storage, {}}; + const HANDLE thread = CreateThread(nullptr, 64u * 1024u, ReadOnSmallStack, &read, STACK_SIZE_PARAM_IS_A_RESERVATION, nullptr); + Require(thread != nullptr, "cannot start the small-stack APR reader"); + WaitForSingleObject(thread, INFINITE); + CloseHandle(thread); + Require(read.error.empty(), read.error.c_str()); +} +#endif + +} + +int main(int argc, char** argv) { + try { + Require(argc == 2, "expected host, direct, eof, errors, streams, fault or stack"); + const Fixture fixture; + const std::string mode = argv[1]; + if (mode == "host") { + std::vector storage(MappingBytes, 0xCC); + TestRead(fixture, storage.data()); + } else if (mode == "direct") { + const DirectBuffer storage; + TestRead(fixture, storage.Data()); + } else if (mode == "eof") { + TestEof(fixture); + } else if (mode == "errors") { + TestErrors(fixture); + } else if (mode == "streams") { + TestStreams(fixture); +#ifdef _WIN32 + } else if (mode == "fault") { + const FaultBuffer storage; + TestRead(fixture, storage.Data()); + Require(committedPages > 2, "APR did not exercise demand-committed destination pages"); + } else if (mode == "stack") { + TestSmallStack(fixture); +#endif + } else { + throw std::runtime_error("unknown APR read case"); + } + std::puts("APR synthetic read checks passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestArenaHost.cpp b/core/libs/tests/GuestArenaHost.cpp new file mode 100644 index 0000000000..e7863563a6 --- /dev/null +++ b/core/libs/tests/GuestArenaHost.cpp @@ -0,0 +1,155 @@ +#include "prx/libc/include/GuestArena.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#if !defined(__APPLE__) +#error These tests cover the macOS guest arena reservation +#endif + +using namespace GuestArena; + +namespace { + +constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull; +constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull; +constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull; +constexpr std::uintptr_t ApplicationAreaEnd = 0x000000FC00000000ull; +constexpr std::uintptr_t ReservationChunk = 1ull << 30; +constexpr std::size_t GapBytes = 0x10000; +constexpr std::uint32_t ProtectionNoAccess = 0x01; +constexpr std::uint32_t ProtectionReadOnly = 0x02; +constexpr std::uint32_t ProtectionReadWrite = 0x04; + +struct HostRegion { + std::uintptr_t start; + std::uintptr_t end; + vm_prot_t maxProtection; +}; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::vector RegionsInArenaRange() { + std::vector regions; + mach_vm_address_t address = ArenaStart; + while (address < ApplicationAreaEnd) { + mach_vm_size_t size = 0; + vm_region_basic_info_data_64_t info{}; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + if (mach_vm_region(mach_task_self(), &address, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast(&info), &count, &object) != KERN_SUCCESS) break; + if (address >= ApplicationAreaEnd) break; + regions.push_back({static_cast(address), static_cast(address + size), info.max_protection}); + address += size; + } + return regions; +} + +const std::vector RegionsBeforeArena = RegionsInArenaRange(); + +bool Overlaps(const std::vector& regions, std::uintptr_t start, std::uintptr_t end) { + for (const auto& region : regions) { + if (region.start < end && start < region.end) return true; + } + return false; +} + +void CheckAvailable() { + Require(GuestArenaAvailable_nid_postfix(), "the guest arena is unavailable on macOS"); + std::uintptr_t base = 0; + std::size_t bytes = 0; + GuestArenaRange_nid_postfix(&base, &bytes); + Require(base == ArenaStart, "the guest arena does not start at the PS5 application map base"); + Require(base + bytes == ApplicationAreaEnd, "the guest arena does not end at the PS5 application map limit"); +} + +void CheckSkipsUnmappable() { + for (const auto& region : RegionsInArenaRange()) { + if (region.maxProtection != VM_PROT_NONE || region.end - region.start < 0x10000) continue; + void* placed = GuestArenaAllocateAtOrAbove_nid_postfix(region.start, 0x10000, 0x10000); + const auto address = reinterpret_cast(placed); + Require(address >= region.end, "an allocation landed inside a range the kernel never lets the process map"); + GuestArenaRelease_nid_postfix(placed, 0x10000); + return; + } + std::puts("no unmappable range inside the arena on this host; nothing to skip"); +} + +void CheckHostRegionsKept() { + std::size_t hostRegions = 0; + for (const auto& region : RegionsInArenaRange()) { + if (region.maxProtection == VM_PROT_ALL) continue; + ++hostRegions; + bool rejected = false; + try { + GuestArenaMarkUsed_nid_postfix(reinterpret_cast(region.start), region.end - region.start); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected, "a fixed guest mapping over a host region was accepted"); + } + std::printf("%zu host regions inside the arena range are protected\n", hostRegions); +} + +void CheckGapBesideHostRegion() { + Require(GuestArenaAvailable_nid_postfix(), "the guest arena is unavailable on macOS"); + Require(RegionsBeforeArena.size() < RegionsInArenaRange().size(), "the host map was read after the guest arena reserved its range"); + for (const auto& host : RegionsBeforeArena) { + const auto chunk = host.start & ~(ReservationChunk - 1); + for (auto gap = std::max(chunk, ArenaStart); gap + GapBytes <= chunk + ReservationChunk && gap + GapBytes <= ApplicationAreaEnd; gap += GapBytes) { + if (Overlaps(RegionsBeforeArena, gap, gap + GapBytes) || (gap < SystemReservedEnd && SystemReservedStart < gap + GapBytes)) continue; + auto* pointer = reinterpret_cast(gap); + GuestArenaMarkUsed_nid_postfix(pointer, GapBytes); + GuestArenaRelease_nid_postfix(pointer, GapBytes); + std::printf("free range 0x%llx beside host region 0x%llx-0x%llx accepted\n", static_cast(gap), + static_cast(host.start), static_cast(host.end)); + return; + } + } + std::puts("no free range shares a 1 GiB reservation chunk with a host region on this host; nothing to check"); +} + +void CheckCommitAndProtect() { + constexpr std::size_t bytes = 0x10000; + auto* pointer = static_cast(GuestArenaAllocate_nid_postfix(bytes, bytes)); + GuestArenaCommit_nid_postfix(pointer, bytes, ProtectionReadWrite, bytes); + pointer[0] = 0x5a; + pointer[bytes - 1] = 0xa5; + Require(pointer[0] == 0x5a && pointer[bytes - 1] == 0xa5, "committed guest memory does not keep writes"); + GuestArenaSetProtection_nid_postfix(reinterpret_cast(pointer), bytes, ProtectionReadOnly); + Require(pointer[0] == 0x5a, "read-only guest memory lost its contents"); + GuestArenaReset_nid_postfix(pointer, bytes); + GuestArenaSetProtection_nid_postfix(reinterpret_cast(pointer), bytes, ProtectionNoAccess); + GuestArenaRelease_nid_postfix(pointer, bytes); +} + +} + +int main(int argc, char** argv) { + try { + Require(argc == 2, "expected available, unmappable, host, gap or commit"); + const std::string_view mode(argv[1]); + if (mode == "available") CheckAvailable(); + else if (mode == "unmappable") CheckSkipsUnmappable(); + else if (mode == "host") CheckHostRegionsKept(); + else if (mode == "gap") CheckGapBesideHostRegion(); + else if (mode == "commit") CheckCommitAndProtect(); + else throw std::runtime_error("unknown guest arena test"); + std::printf("guest arena %s passed\n", argv[1]); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestAt9Enc.cpp b/core/libs/tests/GuestAt9Enc.cpp new file mode 100644 index 0000000000..2da60d41b3 --- /dev/null +++ b/core/libs/tests/GuestAt9Enc.cpp @@ -0,0 +1,42 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int32_t APS5_VABI sceAt9EncClearContext(void* pAt9EncWorkArea, int32_t* pInternalResult); +int32_t APS5_VABI sceAt9EncCreateEncoder(void* pAt9EncWorkArea, void* pAt9EncParam, int32_t memSize, int32_t* pInternalResult); +int32_t APS5_VABI sceAt9EncEncode(void* pAt9EncWorkArea, const void* pInputBuf, void* pOutputBuf, uint32_t inputBufSize, uint32_t outputBufSize, uint32_t* pOutputDataByte, uint32_t* pUsedBufSize, int32_t* pInternalResult); +int32_t APS5_VABI sceAt9EncFlush(void* pAt9EncWorkArea, void* pOutputBuf, uint32_t outputBufSize, uint32_t* pOutputDataByte, int32_t* pInternalResult); +int32_t APS5_VABI sceAt9EncQueryMemSize(void* pAt9EncParam, int32_t* pMemSize, int32_t* pInternalResult); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +template +bool Throws(Call call) { + try { + static_cast(call()); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +} + +int main() { + int32_t memSize = 0; + int32_t internalResult = 0; + + Require(Throws([&] { return sceAt9EncClearContext(nullptr, &internalResult); })); + Require(Throws([&] { return sceAt9EncCreateEncoder(nullptr, nullptr, 0, &internalResult); })); + Require(Throws([&] { return sceAt9EncEncode(nullptr, nullptr, nullptr, 0, 0, nullptr, nullptr, &internalResult); })); + Require(Throws([&] { return sceAt9EncFlush(nullptr, nullptr, 0, nullptr, &internalResult); })); + Require(Throws([&] { return sceAt9EncQueryMemSize(nullptr, &memSize, &internalResult); })); + + Require(memSize == 0); + Require(internalResult == 0); +} diff --git a/core/libs/tests/GuestAtoi.cpp b/core/libs/tests/GuestAtoi.cpp index 0c4d77ddfa..31d63e6ccd 100644 --- a/core/libs/tests/GuestAtoi.cpp +++ b/core/libs/tests/GuestAtoi.cpp @@ -35,10 +35,10 @@ int main() { {" \t+4294967297tail", 1, SavedError}, {"9223372036854775807", -1, SavedError}, {"-9223372036854775808", 0, SavedError}, - {"9223372036854775808", -1, 34}, - {"-9223372036854775809", 0, 34}, + {"9223372036854775808", 0, SavedError}, + {"-9223372036854775809", -1, SavedError}, {"999999999999999999999999999999999999", -1, 34}, - {"-999999999999999999999999999999999999", 0, 34}, + {"-999999999999999999999999999999999999", -1, 34}, }; bool correct = true; for (const auto& test : cases) { diff --git a/core/libs/tests/GuestAtomicBuiltins.cpp b/core/libs/tests/GuestAtomicBuiltins.cpp new file mode 100644 index 0000000000..43d9a0f010 --- /dev/null +++ b/core/libs/tests/GuestAtomicBuiltins.cpp @@ -0,0 +1,86 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +using Uint128 = unsigned __int128; + +extern "C" { +void APS5_VABI __atomic_load_nid_postfix(std::size_t, void*, void*, int); +void APS5_VABI __atomic_store_nid_postfix(std::size_t, void*, void*, int); +void APS5_VABI __atomic_exchange_nid_postfix(std::size_t, void*, void*, void*, int); +bool APS5_VABI __atomic_compare_exchange_nid_postfix(std::size_t, void*, void*, void*, int, int); +bool APS5_VABI __atomic_is_lock_free_nid_postfix(std::size_t, const void*); +Uint128 APS5_VABI __sync_fetch_and_add_16_nid_postfix(Uint128*, Uint128); +Uint128 APS5_VABI __sync_fetch_and_sub_16_nid_postfix(Uint128*, Uint128); +Uint128 APS5_VABI __sync_fetch_and_and_16_nid_postfix(Uint128*, Uint128); +Uint128 APS5_VABI __sync_fetch_and_or_16_nid_postfix(Uint128*, Uint128); +Uint128 APS5_VABI __sync_fetch_and_xor_16_nid_postfix(Uint128*, Uint128); +Uint128 APS5_VABI __sync_lock_test_and_set_16_nid_postfix(Uint128*, Uint128); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static void RequireThrows(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return; + } + std::abort(); +} + +static Uint128 Make(std::uint64_t high, std::uint64_t low) { return (static_cast(high) << 64) | low; } + +static void CheckLockFree() { + alignas(16) unsigned char buffer[32] = {}; + Require(__atomic_is_lock_free_nid_postfix(0, buffer + 1) && __atomic_is_lock_free_nid_postfix(1, buffer + 3)); + Require(__atomic_is_lock_free_nid_postfix(2, buffer + 2) && !__atomic_is_lock_free_nid_postfix(2, buffer + 1)); + Require(__atomic_is_lock_free_nid_postfix(4, buffer + 4) && !__atomic_is_lock_free_nid_postfix(4, buffer + 2)); + Require(__atomic_is_lock_free_nid_postfix(8, buffer + 8) && !__atomic_is_lock_free_nid_postfix(8, buffer + 4)); + Require(__atomic_is_lock_free_nid_postfix(16, buffer + 16) && !__atomic_is_lock_free_nid_postfix(16, buffer + 8)); + Require(!__atomic_is_lock_free_nid_postfix(3, buffer) && !__atomic_is_lock_free_nid_postfix(32, buffer)); +} + +static void CheckGenericTraps() { + alignas(16) unsigned char object[24] = {}; + unsigned char other[24] = {}; + RequireThrows([&] { __atomic_load_nid_postfix(sizeof(object), object, other, 5); }); + RequireThrows([&] { __atomic_store_nid_postfix(sizeof(object), object, other, 5); }); + RequireThrows([&] { __atomic_exchange_nid_postfix(sizeof(object), object, other, other, 5); }); + RequireThrows([&] { __atomic_compare_exchange_nid_postfix(sizeof(object), object, other, other, 5, 5); }); +} + +static void CheckSixteenByte() { + alignas(16) Uint128 value = Make(1, ~0ull); + Require(__sync_fetch_and_add_16_nid_postfix(&value, 1) == Make(1, ~0ull) && value == Make(2, 0)); + Require(__sync_fetch_and_sub_16_nid_postfix(&value, 1) == Make(2, 0) && value == Make(1, ~0ull)); + Require(__sync_fetch_and_and_16_nid_postfix(&value, Make(1, 0xff00)) == Make(1, ~0ull) && value == Make(1, 0xff00)); + Require(__sync_fetch_and_or_16_nid_postfix(&value, Make(4, 0xff)) == Make(1, 0xff00) && value == Make(5, 0xffff)); + Require(__sync_fetch_and_xor_16_nid_postfix(&value, Make(5, 0xf0f0)) == Make(5, 0xffff) && value == Make(0, 0x0f0f)); + Require(__sync_lock_test_and_set_16_nid_postfix(&value, Make(9, 9)) == Make(0, 0x0f0f) && value == Make(9, 9)); +} + +static void CheckConcurrentAdds() { + alignas(16) Uint128 counter = Make(0, ~0ull - 1000); + constexpr int Iterations = 20000; + auto worker = [&counter] { + for (int i = 0; i < Iterations; ++i) __sync_fetch_and_add_16_nid_postfix(&counter, 1); + }; + std::thread first(worker); + std::thread second(worker); + first.join(); + second.join(); + Require(counter == Make(0, ~0ull - 1000) + 2 * Iterations); +} + +int main() { + CheckLockFree(); + CheckGenericTraps(); + CheckSixteenByte(); + CheckConcurrentAdds(); + return 0; +} diff --git a/core/libs/tests/GuestAudio3dMixer.cpp b/core/libs/tests/GuestAudio3dMixer.cpp new file mode 100644 index 0000000000..c7b09af09c --- /dev/null +++ b/core/libs/tests/GuestAudio3dMixer.cpp @@ -0,0 +1,132 @@ +#include "Mixer.hpp" + +#include +#include +#include +#include + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +int main() { + std::int16_t pcm[4] = {16384, -16384, 8192, -8192}; + Audio3dInternal::Source s{pcm, 2, 2, false, 1.0f, 0.0f}; + std::vector out; + Audio3dInternal::MixFrame(&s, 1, 2, out); + Require(out.size() == 4, "frame is granularity*2 samples"); + Require(out[0] == 16384 && out[1] == -16384, "centered stereo passes through"); + Require(out[2] == 8192 && out[3] == -8192, "centered stereo passes through the second frame"); + + std::int16_t panned[2] = {10000, 20000}; + Audio3dInternal::Source hard_right{panned, 1, 2, false, 1.0f, 1.0f}; + std::vector panned_out; + Audio3dInternal::MixFrame(&hard_right, 1, 1, panned_out); + Require(panned_out.size() == 2 && panned_out[0] == 0 && panned_out[1] == 19999, "hard right keeps only the right channel"); + + float floats[2] = {0.5f, -0.5f}; + Audio3dInternal::Source float_source{floats, 1, 2, true, 1.0f, 0.0f}; + std::vector float_out; + Audio3dInternal::MixFrame(&float_source, 1, 1, float_out); + Require(float_out.size() == 2 && float_out[0] == 16384 && float_out[1] == -16384, "float samples are converted"); + + std::int16_t mono[1] = {10000}; + Audio3dInternal::Source mono_source{mono, 1, 1, false, 1.0f, 0.0f}; + std::vector mono_out; + Audio3dInternal::MixFrame(&mono_source, 1, 1, mono_out); + Require(mono_out.size() == 2 && mono_out[0] == 10000 && mono_out[1] == 10000, "centered mono is duplicated"); + + std::vector short_out; + Audio3dInternal::MixFrame(&mono_source, 1, 4, short_out); + Require(short_out.size() == 8, "short frame keeps the full frame size"); + Require(short_out[0] == 10000 && short_out[1] == 10000, "short frame mixes the first sample"); + Require(short_out[2] == 0 && short_out[3] == 0 && short_out[4] == 0 && short_out[5] == 0 && short_out[6] == 0 && short_out[7] == 0, "short frame zero pads the tail"); + + std::int16_t loud[1] = {32767}; + Audio3dInternal::Source loud_source{loud, 1, 1, false, 1.0f, 0.0f}; + Audio3dInternal::Source loud_sources[2] = {loud_source, loud_source}; + std::vector loud_out; + Audio3dInternal::MixFrame(loud_sources, 2, 1, loud_out); + Require(loud_out.size() == 2 && loud_out[0] == 32767 && loud_out[1] == 32767, "sums above full scale clamp"); + + Audio3dInternal::Source out_of_range{panned, 1, 2, false, 1.0f, 2.0f}; + std::vector out_of_range_out; + Audio3dInternal::MixFrame(&out_of_range, 1, 1, out_of_range_out); + Require(out_of_range_out.size() == 2 && out_of_range_out[0] == 0 && out_of_range_out[1] == 19999, "out of range pan clamps"); + + std::vector empty_out; + Audio3dInternal::MixFrame(nullptr, 0, 2, empty_out); + Require(empty_out.size() == 4 && empty_out[0] == 0 && empty_out[1] == 0 && empty_out[2] == 0 && empty_out[3] == 0, "no sources produce silence"); + + std::int16_t skipped[2] = {10000, 10000}; + Audio3dInternal::Source null_samples{nullptr, 1, 2, false, 1.0f, 0.0f}; + Audio3dInternal::Source empty_samples{skipped, 0, 2, false, 1.0f, 0.0f}; + Audio3dInternal::Source skipped_sources[2] = {null_samples, empty_samples}; + std::vector skipped_out; + Audio3dInternal::MixFrame(skipped_sources, 2, 1, skipped_out); + Require(skipped_out.size() == 2 && skipped_out[0] == 0 && skipped_out[1] == 0, "null and empty sources are skipped"); + + std::int16_t surround6[6] = {1000, 2000, 3000, 0, 4000, 5000}; + Audio3dInternal::Source surround6_source{surround6, 1, 6, false, 1.0f, 0.0f}; + std::vector surround6_out; + Audio3dInternal::MixFrame(&surround6_source, 1, 1, surround6_out); + Require(surround6_out.size() == 2 && surround6_out[0] == 5950 && surround6_out[1] == 7657, + "6 channel downmix folds center and surrounds, dropping LFE"); + + std::int16_t surround8[8] = {1000, 2000, 3000, 0, 4000, 5000, 6000, 7000}; + Audio3dInternal::Source surround8_source{surround8, 1, 8, false, 1.0f, 0.0f}; + std::vector surround8_out; + Audio3dInternal::MixFrame(&surround8_source, 1, 1, surround8_out); + Require(surround8_out.size() == 2 && surround8_out[0] == 10192 && surround8_out[1] == 12606, + "8 channel downmix folds rear surrounds into stereo"); + + std::int16_t surround5[5] = {1000, 2000, 3000, 0, 4000}; + Audio3dInternal::Source surround5_source{surround5, 1, 5, false, 1.0f, 0.0f}; + std::vector surround5_out; + Audio3dInternal::MixFrame(&surround5_source, 1, 1, surround5_out); + Require(surround5_out.size() == 2 && surround5_out[0] == 5950 && surround5_out[1] == 4121, + "5 channel downmix reads only the channels present"); + + std::int16_t surround3[3] = {1000, 2000, 3000}; + Audio3dInternal::Source surround3_source{surround3, 1, 3, false, 1.0f, 0.0f}; + std::vector surround3_out; + Audio3dInternal::MixFrame(&surround3_source, 1, 1, surround3_out); + Require(surround3_out.size() == 2 && surround3_out[0] == 3121 && surround3_out[1] == 4121, + "3 channel downmix folds center into both sides"); + + std::int16_t surround4[4] = {1000, 2000, 3000, 0}; + Audio3dInternal::Source surround4_source{surround4, 1, 4, false, 1.0f, 0.0f}; + std::vector surround4_out; + Audio3dInternal::MixFrame(&surround4_source, 1, 1, surround4_out); + Require(surround4_out.size() == 2 && surround4_out[0] == 3121 && surround4_out[1] == 4121, + "4 channel downmix drops LFE"); + + std::int16_t surround6_panned[6] = {1000, 2000, 3000, 0, 4000, 5000}; + Audio3dInternal::Source surround6_panned_source{surround6_panned, 1, 6, false, 1.0f, 1.0f}; + std::vector surround6_panned_out; + Audio3dInternal::MixFrame(&surround6_panned_source, 1, 1, surround6_panned_out); + Require(surround6_panned_out.size() == 2 && surround6_panned_out[0] == 0 && surround6_panned_out[1] == 7657, + "6 channel downmix is panned after folding"); + + Audio3dInternal::Source hard_left{panned, 1, 2, false, 1.0f, -1.0f}; + std::vector left_out; + Audio3dInternal::MixFrame(&hard_left, 1, 1, left_out); + Require(left_out.size() == 2 && left_out[0] == 10000 && left_out[1] == 0, + "hard left pan keeps only the left channel"); + + Audio3dInternal::Source zero_gain{panned, 1, 2, false, 0.0f, 0.0f}; + std::vector zero_gain_out; + Audio3dInternal::MixFrame(&zero_gain, 1, 1, zero_gain_out); + Require(zero_gain_out.size() == 2 && zero_gain_out[0] == 0 && zero_gain_out[1] == 0, + "zero gain produces silence"); + + Audio3dInternal::Source half_gain{panned, 1, 2, false, 0.5f, 0.0f}; + std::vector half_gain_out; + Audio3dInternal::MixFrame(&half_gain, 1, 1, half_gain_out); + Require(half_gain_out.size() == 2 && half_gain_out[0] == 5000 && half_gain_out[1] == 10000, + "half gain scales stereo samples by half"); + + return 0; +} diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index fa5d8f2431..7adbb92ede 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -1,10 +1,13 @@ #include "SceTypes.hpp" +#include #include #include #include #include #include +#include #include +#include extern "C" { void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* parameters); @@ -13,6 +16,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); @@ -20,6 +24,12 @@ int APS5_VABI sceAudio3dPortFlush(std::uint32_t port_id); int APS5_VABI sceAudio3dObjectReserve(std::uint32_t port_id, std::uint32_t* object_id); int APS5_VABI sceAudio3dObjectUnreserve(std::uint32_t port_id, std::uint32_t object_id); int APS5_VABI sceAudio3dObjectSetAttributes(std::uint32_t port_id, std::uint32_t object_id, std::uint64_t num_attributes, const Audio3dAttribute* attribute_array); +int APS5_VABI sceAudio3dBedWrite(std::uint32_t port_id, std::uint32_t num_channels, std::uint32_t format, const void* buffer, std::uint32_t num_samples); +int APS5_VABI sceAudio3dBedWrite2(std::uint32_t port_id, std::uint32_t num_channels, std::uint32_t format, const void* buffer, std::uint32_t num_samples, std::uint32_t output_route, bool restricted); +int APS5_VABI sceAudio3dAudioOutOpen(std::uint32_t port_id, int user_id, std::int32_t type, std::int32_t index, std::uint32_t len, std::uint32_t freq, std::uint32_t param); +int APS5_VABI sceAudio3dAudioOutClose(std::int32_t handle); +int APS5_VABI sceAudio3dAudioOutOutput(std::int32_t handle, const void* ptr); +int APS5_VABI sceAudio3dAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num); } namespace { @@ -76,6 +86,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 +150,20 @@ 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"); + Require(sceAudio3dPortGetAttributesSupported(1, nullptr, nullptr) == INVALID_PORT, "port validation precedes count validation"); + num_attributes = 0; + std::uint32_t output[4]{0, 0, 0, 99}; + Require(sceAudio3dPortGetAttributesSupported(0, output, &num_attributes) == 0 && num_attributes == 3, "the count is output only"); + Require(output[0] == supported[0] && output[1] == supported[1] && output[2] == supported[2] && output[3] == 99, "the complete list is written without touching the following element"); + 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"); @@ -176,11 +203,8 @@ void CheckObjects() { } void CheckQueue() { - using namespace std::chrono; - constexpr std::uint32_t granularity = 0x1800; - constexpr auto frame = microseconds(1000000ull * granularity / 48000); Audio3dOpenParameters parameters = Defaults(); - parameters.granularity = granularity; + parameters.granularity = 0x1800; std::uint32_t id = 7; Require(Open(parameters, &id) == 0 && id == 0, "open"); CheckAttributes(); @@ -194,23 +218,53 @@ void CheckQueue() { RequireLevel(1, 1, "one frame queued"); Require(sceAudio3dPortAdvance(0) == 0, "advance 2"); RequireLevel(2, 0, "queue full"); - RequireThrows([] { sceAudio3dPortAdvance(0); }, "advance into a full queue must throw"); + Require(sceAudio3dPortAdvance(0) == NOT_READY, "advance into a full queue must be NOT_READY"); RequireThrows([] { sceAudio3dPortPush(0, 2); }, "unknown blocking mode must throw"); - - auto start = steady_clock::now(); - Require(sceAudio3dPortPush(0, 1) == 0, "blocking push"); - auto waited = steady_clock::now() - start; - Require(waited >= frame - milliseconds(2), "blocking push waits until one frame has played"); - RequireLevel(1, 1, "blocking push returns with one free slot"); - + Require(sceAudio3dPortPush(0, 1) == 0, "sync push"); + RequireLevel(0, 2, "sync push drains every queued frame"); Require(sceAudio3dPortAdvance(0) == 0, "advance 3"); - start = steady_clock::now(); + Require(sceAudio3dPortAdvance(0) == 0, "advance 4"); Require(sceAudio3dPortPush(0, 0) == 0, "async push"); - Require(steady_clock::now() - start < frame, "async push does not wait for a frame"); - std::uint32_t level = 0; - Require(sceAudio3dPortGetQueueLevel(0, &level, nullptr) == 0 && level >= 1, "async push keeps frames queued"); - std::this_thread::sleep_for(frame * 3); - RequireLevel(0, 2, "frames drain at the port's rate"); + RequireLevel(1, 1, "async push keeps the queue one below the depth"); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + +void CheckMixAndPush() { + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + Require(sceAudio3dPortAdvance(0) == 0, "advance mixes an empty bed into silence"); + RequireLevel(1, 1, "the silent frame is queued"); + const std::int16_t samples[8] = {1000, -1000, 2000, -2000, 3000, -3000, 4000, -4000}; + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "bed write"); + Require(sceAudio3dPortAdvance(0) == 0, "advance mixes the bed"); + RequireLevel(2, 0, "the mixed frame is queued"); + Require(sceAudio3dPortAdvance(0) == NOT_READY, "a full queue rejects the mix"); + Require(sceAudio3dPortPush(0, 1) == 0, "sync push"); + RequireLevel(0, 2, "sync push drains every queued frame"); + Require(sceAudio3dPortAdvance(0) == 0, "advance after the sync drain"); + Require(sceAudio3dPortAdvance(0) == 0, "advance fills the queue again"); + Require(sceAudio3dPortPush(0, 0) == 0, "async push"); + RequireLevel(1, 1, "async push leaves the queue one below the depth"); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + +void CheckPushKeepsRealTime() { + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + const std::int16_t samples[8] = {1000, -1000, 2000, -2000, 3000, -3000, 4000, -4000}; + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "bed write"); + constexpr std::uint32_t frames = 200; + const std::uint64_t blockUs = 1000000ull * parameters.granularity / 48000ull; + const auto start = std::chrono::steady_clock::now(); + for (std::uint32_t i = 0; i < frames; i++) { + Require(sceAudio3dPortAdvance(0) == 0, "advance"); + Require(sceAudio3dPortPush(0, 1) == 0, "sync push"); + } + const auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); + const std::uint64_t expected = frames * blockUs; + Require(elapsed + 150000 >= static_cast(expected), "a blocking push must run at real time"); Require(sceAudio3dPortClose(0) == 0, "close"); } @@ -227,6 +281,38 @@ void CheckFlush() { Require(sceAudio3dPortClose(0) == 0, "close"); } +void CheckAudioOutPorts() { + const std::uint32_t granularity = Defaults().granularity; + Require(sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity, 48000, 1) == INVALID_PORT, "audio out open needs an open port"); + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + Require(sceAudio3dAudioOutOpen(1, SYSTEM_USER, 126, 0, granularity, 48000, 1) == INVALID_PORT, "audio out open on port 1"); + Require(sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity + 0x100, 48000, 1) == INVALID_PARAMETER, "len must equal the granularity"); + Require(sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 1, granularity, 48000, 1) == INVALID_PARAMETER, "index must be 0"); + Require(sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity, 48000, 8) == INVALID_PARAMETER, "unknown format"); + const int handle = sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity, 48000, 1); + Require(handle > 0, "audio out open returns a handle"); + const std::vector block(static_cast(granularity) * 2, 256); + for (std::uint32_t i = 0; i < parameters.queue_depth; ++i) { + Require(sceAudio3dAudioOutOutput(handle, block.data()) == static_cast(granularity), "audio out output returns the sample count"); + } + Require(sceAudio3dAudioOutOutput(handle, block.data()) == NOT_READY, "a full pending queue must be NOT_READY"); + Require(sceAudio3dPortPush(0, 1) == 0, "sync push drains the associated ports"); + Require(sceAudio3dAudioOutOutput(handle, block.data()) == static_cast(granularity), "push drains the pending queue"); + Require(sceAudio3dPortFlush(0) == 0, "flush drains the associated ports"); + Require(sceAudio3dAudioOutOutput(handle, block.data()) == static_cast(granularity), "flush resets the pending queue"); + Require(sceAudio3dAudioOutOutputs(nullptr, 0) == INVALID_PARAMETER, "outputs rejects an empty batch"); + Require(sceAudio3dAudioOutClose(handle) == 0, "audio out close"); + Require(sceAudio3dAudioOutClose(handle) == INVALID_PORT, "audio out double close"); + Require(sceAudio3dAudioOutOutput(handle, block.data()) == INVALID_PORT, "output on a closed handle"); + const int open_handle = sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity, 48000, 1); + Require(open_handle > 0, "audio out reopen"); + Require(sceAudio3dPortClose(0) == 0, "close"); + Require(sceAudio3dAudioOutOutput(open_handle, block.data()) == INVALID_PORT, "port close clears the associated ports"); + Require(sceAudio3dAudioOutClose(open_handle) == INVALID_PORT, "port close closes the associated ports"); +} + void CheckObjectAttributes() { float value = 0.5f; Audio3dAttribute attribute{0x10001, 0, &value, sizeof(value)}; @@ -249,6 +335,62 @@ void CheckObjectAttributes() { Require(sceAudio3dPortClose(0) == 0, "close"); } +void CheckBed() { + const std::int16_t samples[8] = {}; + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == INVALID_PORT, "bed write needs an open port"); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 0, false) == INVALID_PORT, "bed write2 needs an open port"); + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + const auto consume = [] { + Require(sceAudio3dPortAdvance(0) == 0, "advance after a bed write"); + Require(sceAudio3dPortFlush(0) == 0, "flush the mixed frame after a bed write"); + }; + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "2 channel s16 bed write"); + consume(); + Require(sceAudio3dBedWrite(0, 1, 0, samples, 4) == INVALID_PARAMETER, "1 channel bed write"); + Require(sceAudio3dBedWrite(0, 2, 0, nullptr, 4) == INVALID_PARAMETER, "null bed buffer"); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 0, false) == 0, "bed write2 route both"); + consume(); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 1, false) == 0, "bed write2 route hmu only"); + consume(); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 2, false) == 0, "bed write2 route tv only"); + consume(); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 3, false) == INVALID_PARAMETER, "bed write2 unknown route"); + Require(sceAudio3dBedWrite(0, 2, 2, samples, 4) == INVALID_PARAMETER, "unknown bed format"); + Require(sceAudio3dBedWrite(0, 2, 0, samples, 0) == INVALID_PARAMETER, "zero bed samples"); + Require(sceAudio3dBedWrite(0, 2, 0, reinterpret_cast(samples) + 1, 4) == INVALID_PARAMETER, "misaligned s16 bed buffer"); + const float floats[8] = {}; + Require(sceAudio3dBedWrite(0, 2, 1, floats, 4) == 0, "float bed write"); + consume(); + const std::uint32_t granularity = parameters.granularity; + std::vector wide(static_cast(granularity) * 8, 0); + Require(sceAudio3dBedWrite(0, 6, 0, wide.data(), granularity) == 0, "6 channel bed write"); + consume(); + Require(sceAudio3dBedWrite(0, 8, 0, wide.data(), granularity) == 0, "8 channel bed write"); + consume(); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + +void CheckBedQueue() { + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + const std::int16_t samples[8] = {1000, -1000, 2000, -2000, 3000, -3000, 4000, -4000}; + for (std::uint32_t i = 0; i < parameters.queue_depth; ++i) { + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "each bed write fills the queue"); + } + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == NOT_READY, "a full bed queue must be NOT_READY"); + Require(sceAudio3dPortAdvance(0) == 0, "advance consumes one bed"); + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "the consumed bed makes room"); + Require(sceAudio3dPortAdvance(0) == 0, "advance consumes the next bed"); + RequireLevel(2, 0, "two frames are queued from the queued beds"); + Require(sceAudio3dPortFlush(0) == 0, "flush"); + Require(sceAudio3dPortAdvance(0) == 0, "advance with no bed left mixes silence"); + RequireLevel(1, 1, "silence is queued behind the flushed frames"); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + void CheckTerminate() { std::uint32_t id = 7; const Audio3dOpenParameters parameters = Defaults(); @@ -273,7 +415,12 @@ int main() { CheckOpenClose(); CheckObjects(); CheckQueue(); + CheckMixAndPush(); + CheckPushKeepsRealTime(); CheckFlush(); + CheckAudioOutPorts(); CheckObjectAttributes(); + CheckBed(); + CheckBedQueue(); CheckTerminate(); } diff --git a/core/libs/tests/GuestAudio3dSpeakerArray.cpp b/core/libs/tests/GuestAudio3dSpeakerArray.cpp new file mode 100644 index 0000000000..7ef0c9942e --- /dev/null +++ b/core/libs/tests/GuestAudio3dSpeakerArray.cpp @@ -0,0 +1,75 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +struct Audio3dSpeakerArrayParameters { + const AudioOut2Position* positions; + std::uint32_t numSpeakers; + std::uint8_t is3d; + void* memory; + std::size_t memorySize; +}; + +extern "C" { +size_t APS5_VABI sceAudio3dGetSpeakerArrayMemorySize(std::uint32_t num_speakers, std::uint8_t is_3d); +int APS5_VABI sceAudio3dCreateSpeakerArray(AudioOut2SpeakerArrayHandle* handle, const Audio3dSpeakerArrayParameters* parameters); +int APS5_VABI sceAudio3dDeleteSpeakerArray(AudioOut2SpeakerArrayHandle handle); +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, std::uint32_t num_coefficients); +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients2(AudioOut2SpeakerArrayHandle handle, AudioOut2Position pos, float spread, float* coefficients, std::uint32_t num_coefficients, std::uint8_t height_aware, float downmix_spread_radius); +size_t APS5_VABI sceAudioOut2GetSpeakerArrayMemorySize(std::uint32_t num_speakers, std::uint8_t is_3d, std::uint8_t is_ambisonics); +} + +namespace { + +constexpr int INVALID_PARAMETER = static_cast(0x80EA0004); + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +template +void RequireThrows(F f, const char* message) { + bool threw = false; + try { + f(); + } catch (const std::exception&) { + threw = true; + } + Require(threw, message); +} + +} + +int main() { + Require(sceAudio3dGetSpeakerArrayMemorySize(4, 1) == sceAudioOut2GetSpeakerArrayMemorySize(4, 1, 0), "memory size forwards to AudioOut2 without ambisonics"); + + const AudioOut2Position positions[4] = {{1, 1, 0}, {1, -1, 0}, {-1, 1, 0}, {-1, -1, 0}}; + alignas(16) unsigned char memory[1024] = {}; + const std::size_t size = sceAudio3dGetSpeakerArrayMemorySize(4, 0); + Require(size <= sizeof(memory), "speaker array fits the test buffer"); + const Audio3dSpeakerArrayParameters parameters{positions, 4, 0, memory, size}; + + AudioOut2SpeakerArrayHandle handle = nullptr; + Require(sceAudio3dCreateSpeakerArray(nullptr, ¶meters) == INVALID_PARAMETER, "null handle is an invalid parameter"); + Require(sceAudio3dCreateSpeakerArray(&handle, nullptr) == INVALID_PARAMETER, "null parameters are an invalid parameter"); + Require(handle == nullptr, "a rejected create leaves the handle"); + Require(sceAudio3dCreateSpeakerArray(&handle, ¶meters) == 0, "create succeeds without ambisonics parameters"); + Require(handle != nullptr, "create stores the handle"); + + float coefficients[4] = {-1, -1, -1, -1}; + Require(sceAudio3dGetSpeakerArrayMixCoefficients(handle, AudioOut2Position{1, 0, 0}, 0.0f, coefficients, 4) == 0, "mix coefficients succeed"); + Require(coefficients[2] == 0.0f && coefficients[3] == 0.0f, "mix coefficients are written"); + float coefficients2[4] = {-1, -1, -1, -1}; + Require(sceAudio3dGetSpeakerArrayMixCoefficients2(handle, AudioOut2Position{1, 0, 0}, 0.0f, coefficients2, 4, 1, 0.5f) == 0, "mix coefficients 2 succeed"); + Require(coefficients2[3] == 0.0f, "mix coefficients 2 are written"); + RequireThrows([&] { sceAudio3dGetSpeakerArrayMixCoefficients(handle, AudioOut2Position{1, 0, 0}, 0.0f, coefficients, 3); }, "a coefficient count that differs from the array is rejected"); + + Require(sceAudio3dDeleteSpeakerArray(handle) == 0, "delete succeeds"); + RequireThrows([&] { sceAudio3dDeleteSpeakerArray(handle); }, "a deleted array is no longer valid"); + return 0; +} diff --git a/core/libs/tests/GuestAudioIn.cpp b/core/libs/tests/GuestAudioIn.cpp index d9e9579114..15caefd1ec 100644 --- a/core/libs/tests/GuestAudioIn.cpp +++ b/core/libs/tests/GuestAudioIn.cpp @@ -16,6 +16,13 @@ int APS5_VABI sceAudioInInit(); int APS5_VABI sceAudioInOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI sceAudioInHqOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI sceAudioInAsyncOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioInOpenEx(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioInHqOpenEx(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioInSetCompressorPreGain(); +int APS5_VABI sceAudioInSetNoiseGateThreshold(); +int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t); +int32_t APS5_VABI sceAudioInGetGain(int32_t, float*); +int32_t APS5_VABI sceAudioInGetRerouteCount(int32_t); int APS5_VABI sceAudioInInput(int, void*); int APS5_VABI sceAudioInGetSilentState(int); int APS5_VABI sceAudioInClose(int); @@ -45,10 +52,27 @@ namespace { constexpr int user = 0x10000000; -void TestValidation() { +void TestOpenEx() { constexpr int invalidSize = static_cast(0x80260102); constexpr int invalidFreq = static_cast(0x80260103); constexpr int invalidType = static_cast(0x80260104); + Require(sceAudioInOpenEx(user, 2, 0, 7, 256, 48000, 2) == invalidType); + Require(sceAudioInOpenEx(user, 0, 0, 7, 512, 48000, 2) == invalidSize); + Require(sceAudioInOpenEx(user, 0, 0, 7, 256, 44100, 2) == invalidFreq); + Require(sceAudioInHqOpenEx(user, 0, 0, 7, 128, 44100, 2) == invalidFreq); + int port = sceAudioInOpenEx(user, 0, 0, 7, 256, 48000, 2); + Require(port > 0 && sceAudioInClose(port) == 0); + port = sceAudioInHqOpenEx(user, 0, 0, 7, 256, 16000, 2); + Require(port > 0 && sceAudioInClose(port) == 0); + Require(sceAudioInSetCompressorPreGain() == 0 && sceAudioInSetNoiseGateThreshold() == 0); +} + +void TestValidation() { + constexpr int invalidHandle = static_cast(0x80260101); + constexpr int invalidSize = static_cast(0x80260102); + constexpr int invalidFreq = static_cast(0x80260103); + constexpr int invalidType = static_cast(0x80260104); + constexpr int invalidPointer = static_cast(0x80260105); constexpr int invalidParam = static_cast(0x80260106); Require(sceAudioInOpen(user, 2, 0, 256, 48000, 2) == invalidType); Require(sceAudioInOpen(user, 0, 1, 256, 48000, 2) == invalidParam); @@ -64,14 +88,48 @@ void TestValidation() { Require(sceAudioInAsyncOpen(user, 2, 0, 128, 48000, 2) == invalidType); Require(sceAudioInAsyncOpen(user, 0, 1, 128, 48000, 2) == invalidParam); Require(sceAudioInAsyncOpen(user, 0, 0, 385, 48000, 2) == invalidSize); + Require(sceAudioInAsyncOpen(user, 0, 0, 200, 48000, 2) == invalidSize); Require(sceAudioInAsyncOpen(user, 0, 0, 256, 44100, 2) == invalidFreq); - Require(sceAudioInAsyncOpen(user, 0, 0, 256, 48000, 0) == invalidParam); - Require(ReportsNotImplemented("sceAudioInHqOpen", [] { sceAudioInHqOpen(user, 0, 0, 256, 48000, 2); })); - Require(ReportsNotImplemented("sceAudioInHqOpen", [] { sceAudioInHqOpen(user, 0, 0, 128, 16000, 2); })); - Require(ReportsNotImplemented("sceAudioInAsyncOpen", [] { sceAudioInAsyncOpen(user, 0, 0, 384, 48000, 2); })); - Require(ReportsNotImplemented("sceAudioInOpen", [] { sceAudioInOpen(user, 0, 0, 256, 48000, 0x10); })); - Require(ReportsNotImplemented("sceAudioInHqOpen", [] { sceAudioInHqOpen(user, 0, 0, 128, 48000, 0x10); })); - Require(ReportsNotImplemented("sceAudioInAsyncOpen", [] { sceAudioInAsyncOpen(user, 0, 0, 128, 48000, 0x10); })); + Require(sceAudioInAsyncOpen(user, 0, 0, 256, 48000, 0) == invalidSize); + Require(sceAudioInHqOpen(user, 0, 0, 256, 48000, 2) == invalidSize); + Require(sceAudioInHqOpen(user, 0, 0, 128, 16000, 2) == invalidFreq); + for (const int badHandle : {-1, 0, 1, 9}) { + float gain = -2.5f; + Require(sceAudioInGetGain(badHandle, &gain) == invalidHandle && gain == 1.0f); + Require(sceAudioInGetRerouteCount(badHandle) == invalidHandle); + Require(sceAudioInGetGain(badHandle, nullptr) == invalidPointer); + } + int port = sceAudioInAsyncOpen(user, 0, 0, 128, 48000, 2); + float gain = 0.0f; + Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0); + Require(sceAudioInGetGain(port, nullptr) == invalidPointer); + Require(sceAudioInClose(port) == 0); + gain = 0.25f; + Require(sceAudioInGetGain(port, &gain) == invalidHandle && gain == 1.0f && sceAudioInGetRerouteCount(port) == invalidHandle); + port = sceAudioInOpen(user, 0, 0, 256, 48000, 0x10); + gain = -1.0f; + Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0 && sceAudioInClose(port) == 0); + for (const std::uint32_t format : {0u, 1u, 3u, 0x10u, 0x11u, 0x12u}) Require(sceAudioInHqOpen(user, 0, 0, 128, 48000, format) == invalidParam); + for (const std::uint32_t size : {0u, 127u, 129u, 256u, 384u}) Require(sceAudioInAsyncOpen(user, 0, 0, size, 48000, 2) == invalidSize); + Require(sceAudioInHqOpen(user, 2, 1, 256, 16000, 0) == invalidFreq); + Require(sceAudioInAsyncOpen(user, 2, 1, 256, 16000, 0) == invalidFreq); + Require(sceAudioInHqOpen(user, 2, 1, 256, 48000, 0) == invalidSize); + Require(sceAudioInAsyncOpen(user, 2, 1, 256, 48000, 0) == invalidSize); + port = sceAudioInAsyncOpen(user, 0, 0, 128, 48000, 0x10); + gain = 0.5f; + Require(port > 0 && sceAudioInGetGain(port, &gain) == 0 && gain == 1.0f && sceAudioInGetRerouteCount(port) == 0 && sceAudioInClose(port) == 0); + Require(sceAudioInIsPadEmbeddedMic(1) == static_cast(0x80260101)); + const int mic = sceAudioInOpen(user, 0, 0, 256, 48000, 0x10); + Require(mic > 0 && sceAudioInIsPadEmbeddedMic(mic) == 0 && sceAudioInClose(mic) == 0); + Require(sceAudioInIsPadEmbeddedMic(mic) == static_cast(0x80260101)); +} + +void InputSound(int handle, std::vector& block, int samples = 256) { + for (int attempt = 0; attempt < 200; ++attempt) { + Require(sceAudioInInput(handle, block.data()) == samples); + if (std::any_of(block.begin(), block.end() - 1, [](std::int16_t sample) { return sample != 0; })) return; + } + Require(false); } void TestCapture() { @@ -83,12 +141,13 @@ void TestCapture() { SetEnvironment("SDL_DISKAUDIOFILEIN", path.string()); TestValidation(); + TestOpenEx(); const int handle = sceAudioInOpen(user, 0, 0, 256, 48000, 2); Require(handle > 0 && sceAudioInGetSilentState(handle) == 0); std::vector block(256 * 2 + 1, 0x5555); std::vector captured; for (int i = 0; i < 4; ++i) { - Require(sceAudioInInput(handle, block.data()) == 256); + InputSound(handle, block); captured.insert(captured.end(), block.begin(), block.end() - 1); Require(block.back() == 0x5555); } @@ -96,9 +155,10 @@ void TestCapture() { Require(sceAudioInInput(handle, block.data()) == 256); for (std::size_t i = 0; i < 256 * 2; ++i) Require(block[i] == 0); Require(sceAudioInClose(handle) == 0); - const int async = sceAudioInAsyncOpen(user, 0, 0, 256, 48000, 2); + const int async = sceAudioInAsyncOpen(user, 0, 0, 128, 48000, 2); Require(async == handle && sceAudioInGetSilentState(async) == 0); - Require(sceAudioInInput(async, block.data()) == 256); + block.assign(128 * 2 + 1, 0x5555); + InputSound(async, block, 128); Require(std::equal(block.begin(), block.end() - 1, recorded.begin()) && block.back() == 0x5555); Require(sceAudioInClose(async) == 0); std::filesystem::remove(path); @@ -110,6 +170,7 @@ void TestNoDevice() { SetEnvironment("SDL_AUDIODRIVER", "none"); TestValidation(); + TestOpenEx(); const int handle = sceAudioInOpen(user, 0, 0, 256, 16000, 0x12); Require(handle > 0 && sceAudioInGetSilentState(handle) == 1); Require(sceAudioInGetSilentState(handle + 1) == invalidHandle); @@ -123,7 +184,7 @@ void TestNoDevice() { Require(buffer[256 * 8] == 0xCC); Require(sceAudioInInput(handle, nullptr) == 0); const int hq = sceAudioInHqOpen(user, 0, 0, 128, 48000, 2); - const int async = sceAudioInAsyncOpen(user, 1, 0, 256, 16000, 0x11); + const int async = sceAudioInAsyncOpen(user, 1, 0, 128, 48000, 0x11); Require(hq > 0 && async > 0 && hq != handle && async != handle && hq != async); Require(sceAudioInGetSilentState(hq) == 1 && sceAudioInGetSilentState(async) == 1); std::fill(buffer.begin(), buffer.end(), 0xCC); @@ -131,13 +192,13 @@ void TestNoDevice() { for (std::size_t i = 0; i < 128 * 4; ++i) Require(buffer[i] == 0); Require(buffer[128 * 4] == 0xCC); std::fill(buffer.begin(), buffer.end(), 0xCC); - Require(sceAudioInInput(async, buffer.data()) == 256); - for (std::size_t i = 0; i < 256 * 4; ++i) Require(buffer[i] == 0); - Require(buffer[256 * 4] == 0xCC); + Require(sceAudioInInput(async, buffer.data()) == 128); + for (std::size_t i = 0; i < 128 * 4; ++i) Require(buffer[i] == 0); + Require(buffer[128 * 4] == 0xCC); Require(sceAudioInInput(async, nullptr) == 0); for (int port = 3; port < 8; ++port) Require(sceAudioInOpen(user, 1, 0, 128, 48000, 1) > 0); Require(sceAudioInOpen(user, 1, 0, 128, 48000, 1) == portFull); - Require(sceAudioInHqOpen(user, 1, 0, 128, 48000, 1) == portFull); + Require(sceAudioInHqOpen(user, 1, 0, 128, 48000, 2) == portFull); Require(sceAudioInAsyncOpen(user, 1, 0, 128, 48000, 1) == portFull); Require(sceAudioInClose(hq) == 0); Require(sceAudioInClose(hq) == invalidHandle); @@ -145,7 +206,7 @@ void TestNoDevice() { Require(sceAudioInAsyncOpen(user, 0, 0, 128, 48000, 2) == hq); Require(sceAudioInClose(async) == 0); Require(sceAudioInInput(async, buffer.data()) == invalidHandle); - Require(sceAudioInHqOpen(user, 1, 0, 128, 48000, 0x12) == async); + Require(sceAudioInHqOpen(user, 1, 0, 128, 48000, 2) == async); Require(sceAudioInClose(handle) == 0); Require(sceAudioInClose(handle) == invalidHandle); Require(sceAudioInInput(handle, buffer.data()) == invalidHandle); diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index 8930cf679d..bc5db1f6a6 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -1,11 +1,23 @@ #include "prx/libSceAudioPropagation/AudioPropagation.hpp" #include +#include #include #include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif + +extern "C" { +int APS5_VABI sceAudioPropagationPathGetNumPoints(); +int APS5_VABI sceAudioPropagationSystemLock(); +} + namespace { void Require(bool condition, const char* message) { @@ -27,6 +39,16 @@ void RequireThrows(TCall call, const char* message) { Require(false, message); } +void* Resolve(const char* name) { +#ifdef _WIN32 + const HMODULE module = GetModuleHandleA("libSceAudioPropagation.prx"); + Require(module != nullptr, "library module"); + return reinterpret_cast(GetProcAddress(module, name)); +#else + return dlsym(RTLD_DEFAULT, name); +#endif +} + AudioPropagationSystemMemory Memory() { AudioPropagationSystemMemory memory{}; memory.desc = {AudioPropagation::SystemMemoryId, AudioPropagation::SystemMemorySize}; @@ -44,38 +66,69 @@ AudioPropagation::Attribute Attribute(const void* value, std::size_t size) { } int main() { - const std::uint64_t options[7]{}; - alignas(16) std::uint8_t arena[64]{}; + const AudioPropagation::SystemOptions options{{AudioPropagation::SystemOptionsId, AudioPropagation::SystemOptionsSize}, 2, 2, 6, 1, 256, 343.0f, 343.0f, 0, {}}; auto memory = Memory(); - Require(sceAudioPropagationSystemQueryMemory(options, &memory) == 0, "query memory failed"); - Require(memory.size_cpu_mem != 0 && memory.size_cpu_mem <= sizeof(arena), "cpu size outside the arena"); + Require(sceAudioPropagationSystemQueryMemory(&options, &memory) == 0, "query memory failed"); + Require(memory.size_cpu_mem == 557264, "native CPU memory size mismatch"); + std::vector arenaStorage((memory.size_cpu_mem + 7) / 8); + auto* arena = reinterpret_cast(arenaStorage.data()); Require(memory.size_gpu_mem == 0 && memory.p_cpu_mem == nullptr && memory.p_gpu_mem == nullptr, "query wrote more than the sizes"); - RequireThrows([&] { sceAudioPropagationSystemQueryMemory(nullptr, &memory); }, "null options"); - RequireThrows([&] { sceAudioPropagationSystemQueryMemory(options, nullptr); }, "null memory"); + Require(sceAudioPropagationSystemQueryMemory(nullptr, &memory) == AudioPropagation::ErrorNullPointer, "null options"); + Require(sceAudioPropagationSystemQueryMemory(&options, nullptr) == AudioPropagation::ErrorNullPointer, "null memory"); auto wrong = Memory(); wrong.desc.size = 0x28; - RequireThrows([&] { sceAudioPropagationSystemQueryMemory(options, &wrong); }, "wrong descriptor size"); + Require(sceAudioPropagationSystemQueryMemory(&options, &wrong) == static_cast(0x8A700007), "wrong descriptor size"); wrong = Memory(); wrong.desc.id = AudioPropagation::RayId; - RequireThrows([&] { sceAudioPropagationSystemQueryMemory(options, &wrong); }, "wrong descriptor id"); + Require(sceAudioPropagationSystemQueryMemory(&options, &wrong) == static_cast(0x8A700007), "wrong descriptor id"); + + auto probeOptions = options; + auto probeMemory = Memory(); + probeMemory.p_cpu_mem = arena; + probeMemory.p_gpu_mem = arena; + probeOptions.maxSources = 0; + Require(sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory) == AudioPropagation::ErrorInvalidArgument, "zero source capacity accepted"); + Require(probeMemory.p_cpu_mem == nullptr && probeMemory.p_gpu_mem == nullptr && probeMemory.size_cpu_mem == 0 && probeMemory.size_gpu_mem == 0, "invalid options did not clear memory output"); + probeOptions = options; + probeOptions.parameter24 = 7; + Require(sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory) == AudioPropagation::ErrorInvalidArgument, "unsupported parameter accepted"); + probeOptions = options; + probeOptions.frameSamples = 768; + Require(sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory) == AudioPropagation::ErrorInvalidArgument, "unsupported frame size accepted"); + probeOptions = options; + probeOptions.propagationSpeed = 0x1p-23f; + Require(sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory) == AudioPropagation::ErrorInvalidArgument, "invalid propagation speed accepted"); + probeOptions = options; + probeOptions.frameSamples = 512; + Require(sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory) == 0 && probeMemory.size_cpu_mem == 524432, "512 sample memory size mismatch"); + probeOptions = options; + probeOptions.reflectionOrder = 2; + Require(sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory) == 0 && probeMemory.size_cpu_mem == 1110608, "second order memory size mismatch"); + probeOptions = options; + probeOptions.flags = 30; + Require(sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory) == 0 && probeMemory.size_cpu_mem == 2604064, "option memory sizes mismatch"); + probeOptions.flags = 1; + Require(sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory) == AudioPropagation::ErrorInvalidArgument, "GPU frame size constraint ignored"); + probeOptions.frameSamples = 512; + RequireThrows([&] { sceAudioPropagationSystemQueryMemory(&probeOptions, &probeMemory); }, "unknown GPU memory query returned a size"); AudioPropagationHandle system = 0; - RequireThrows([&] { sceAudioPropagationSystemCreate(options, &memory, &system); }, "create without cpu memory"); + RequireThrows([&] { sceAudioPropagationSystemCreate(&options, &memory, &system); }, "create without cpu memory"); memory.p_cpu_mem = arena + 1; - RequireThrows([&] { sceAudioPropagationSystemCreate(options, &memory, &system); }, "misaligned cpu memory"); + RequireThrows([&] { sceAudioPropagationSystemCreate(&options, &memory, &system); }, "misaligned cpu memory"); memory.p_cpu_mem = arena; memory.size_gpu_mem = 16; - RequireThrows([&] { sceAudioPropagationSystemCreate(options, &memory, &system); }, "unrequested gpu memory"); + RequireThrows([&] { sceAudioPropagationSystemCreate(&options, &memory, &system); }, "unrequested gpu memory"); memory.size_gpu_mem = 0; const auto required = memory.size_cpu_mem; memory.size_cpu_mem = required - 1; - RequireThrows([&] { sceAudioPropagationSystemCreate(options, &memory, &system); }, "cpu memory too small"); + RequireThrows([&] { sceAudioPropagationSystemCreate(&options, &memory, &system); }, "cpu memory too small"); memory.size_cpu_mem = required; - Require(sceAudioPropagationSystemCreate(options, &memory, &system) == 0, "create failed"); + Require(sceAudioPropagationSystemCreate(&options, &memory, &system) == 0, "create failed"); Require(system == reinterpret_cast(arena), "system handle is not its memory"); AudioPropagationHandle twice = 0; - RequireThrows([&] { sceAudioPropagationSystemCreate(options, &memory, &twice); }, "second system in the same memory"); + RequireThrows([&] { sceAudioPropagationSystemCreate(&options, &memory, &twice); }, "second system in the same memory"); const std::uint64_t value = 1; const auto attribute = Attribute(&value, sizeof(value)); @@ -116,7 +169,11 @@ int main() { std::uint32_t count = 7; Require(sceAudioPropagationSourceGetAudioPathCount(source, &count) == 0 && count == 0, "source reports audio paths"); AudioPropagationHandle path = 0; - RequireThrows([&] { sceAudioPropagationSourceGetAudioPath(source, 0, &path); }, "audio path without paths"); + path = 0x1234; + Require(sceAudioPropagationSourceGetAudioPath(source, 0, &path) == AudioPropagation::ErrorInvalidArgument && path == 0x1234, "audio path without paths"); + Require(sceAudioPropagationSourceGetAudioPath(source, UINT32_MAX, &path) == AudioPropagation::ErrorInvalidArgument && path == 0x1234, "audio path index range"); + Require(sceAudioPropagationSourceGetAudioPath(source, 0, nullptr) == AudioPropagation::ErrorNullPointer, "audio path null output"); + Require(sceAudioPropagationSourceGetAudioPath(0, 0, nullptr) == AudioPropagation::ErrorInvalidHandle, "audio path handle precedence"); std::uint8_t rays[AudioPropagation::RaySize * 2]{}; count = 7; @@ -126,37 +183,39 @@ int main() { RequireThrows([&] { sceAudioPropagationSourceGetRays(source, rays, nullptr); }, "rays without count"); Require(sceAudioPropagationSystemSetRays(system, rays, 0) == 0, "empty ray results failed"); RequireThrows([&] { sceAudioPropagationSystemSetRays(system, rays, 1); }, "unrequested ray results"); - Require(sceAudioPropagationSourceCalculateAudioPaths(source, rays, 0, 1, nullptr, 0) == 0, "empty path calculation failed"); - RequireThrows([&] { sceAudioPropagationSourceCalculateAudioPaths(source, rays, 1, 1, nullptr, 0); }, "path calculation with rays"); + Require(sceAudioPropagationSourceCalculateAudioPaths(source, rays, 0, 1, nullptr, 0) == AudioPropagation::ErrorNullPointer, "path calculation null output"); + Require(sceAudioPropagationSourceCalculateAudioPaths(0, nullptr, 0, 0, nullptr, 0) == AudioPropagation::ErrorInvalidHandle, "path calculation handle precedence"); + Require(sceAudioPropagationSourceCalculateAudioPaths(source, rays, 0, 1, rays, 6) == AudioPropagation::ErrorInvalidArgument, "path calculation empty rays"); + Require(sceAudioPropagationSourceCalculateAudioPaths(source, rays, 6, 0, rays, 6) == AudioPropagation::ErrorInvalidArgument, "path calculation zero reflection order"); + Require(sceAudioPropagationSourceCalculateAudioPaths(source, rays, 6, 1, rays, 0) == AudioPropagation::ErrorInvalidArgument, "path calculation zero output capacity"); + RequireThrows([&] { sceAudioPropagationSourceCalculateAudioPaths(source, rays, 6, 1, rays, 6); }, "unsupported path calculation"); Require(sceAudioPropagationSourceSetAudioPaths(source, nullptr, 0) == 0, "empty audio paths failed"); RequireThrows([&] { sceAudioPropagationSourceSetAudioPaths(source, rays, 1); }, "set audio paths"); - alignas(16) std::uint8_t otherArena[64]{}; + std::vector otherArenaStorage(arenaStorage.size()); + auto* otherArena = reinterpret_cast(otherArenaStorage.data()); auto otherMemory = Memory(); - Require(sceAudioPropagationSystemQueryMemory(options, &otherMemory) == 0, "second query memory failed"); + Require(sceAudioPropagationSystemQueryMemory(&options, &otherMemory) == 0, "second query memory failed"); otherMemory.p_cpu_mem = otherArena; AudioPropagationHandle otherSystem = 0; - Require(sceAudioPropagationSystemCreate(options, &otherMemory, &otherSystem) == 0, "second system create failed"); + Require(sceAudioPropagationSystemCreate(&options, &otherMemory, &otherSystem) == 0, "second system create failed"); AudioPropagationHandle otherSource = 0; Require(sceAudioPropagationSourceCreate(otherSystem, &otherSource) == 0, "second system source create failed"); AudioPropagationHandle secondSource = 0; Require(sceAudioPropagationSourceCreate(system, &secondSource) == 0, "second source create failed"); - float outputs[2][64]; - const auto fill = [&] { std::memset(outputs, 0xab, sizeof(outputs)); }; - const auto zeroed = [&](int index) { - for (const float sample : outputs[index]) { - if (sample != 0.0f) return false; + constexpr std::size_t frameValues = 256 * 16; + float outputs[2][frameValues + 16]; + float expected[2][frameValues + 16]; + const auto fill = [&] { + for (std::size_t channel = 0; channel < 2; ++channel) { + for (std::size_t sample = 0; sample < frameValues + 16; ++sample) { + outputs[channel][sample] = static_cast(sample + 1) / (channel == 0 ? 128.0f : -128.0f); + } } - return true; - }; - const auto untouched = [&] { - const auto* bytes = reinterpret_cast(outputs); - for (std::size_t index = 0; index < sizeof(outputs); ++index) { - if (bytes[index] != 0xab) return false; - } - return true; + std::memcpy(expected, outputs, sizeof(outputs)); }; + const auto untouched = [&] { return std::memcmp(outputs, expected, sizeof(outputs)) == 0; }; AudioPropagation::RenderInfo infos[2]{}; const auto reset = [&] { infos[0] = {{AudioPropagation::RenderInfoId, AudioPropagation::RenderInfoSize}, source, outputs[0], sizeof(outputs[0]), AudioPropagation::RenderFormat, 0}; @@ -165,16 +224,37 @@ int main() { }; reset(); Require(sceAudioPropagationSourceRender(system, infos, 1) == 0, "render failed"); - Require(zeroed(0), "render left the output unwritten"); - const auto* tail = reinterpret_cast(outputs[1]); - Require(tail[0] == 0xab, "render wrote past the output size"); + Require(untouched(), "empty source erased an existing mix"); + infos[0].channelCount = 99; + Require(sceAudioPropagationSourceRender(system, infos, 1) == 0 && infos[0].channelCount == 0, "fixed render format clears the channel count"); + Require(untouched(), "render wrote past the output size"); reset(); - infos[0].outputSize = sizeof(float) * 16; + infos[0].outputSize = sizeof(float) * (frameValues - 1); + RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 2); }, "short frame output accepted"); + Require(untouched(), "short output modified a render buffer"); + reset(); + infos[0].outputSize = sizeof(float) * frameValues; Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render of two sources failed"); - Require(outputs[0][15] == 0.0f && outputs[0][16] != 0.0f && zeroed(1), "render did not write each output for its size"); + Require(untouched(), "empty sources modified separate destination mixes or their tails"); + reset(); + infos[1].output = outputs[0]; + Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render into a shared mix failed"); + Require(untouched(), "empty sources modified a shared destination mix"); + reset(); + infos[0].outputSize = sizeof(float) * frameValues; + infos[1].output = outputs[0] + 16; + infos[1].outputSize = sizeof(float) * frameValues; + Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render into overlapping mixes failed"); + Require(untouched(), "empty sources modified overlapping destination mixes"); + reset(); + std::memset(outputs, 0, sizeof(outputs)); + std::memcpy(expected, outputs, sizeof(outputs)); + Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render into silence failed"); + Require(untouched(), "empty sources modified a silent destination mix"); reset(); RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 0); }, "render without infos"); + RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 3); }, "render exceeds source capacity"); RequireThrows([&] { sceAudioPropagationSourceRender(system, nullptr, 1); }, "render with null infos"); RequireThrows([&] { sceAudioPropagationSourceRender(system + 16, infos, 1); }, "render on an unknown system"); infos[1].desc.id = AudioPropagation::RayId; @@ -224,9 +304,17 @@ int main() { Require(sceAudioPropagationSystemUnregisterMaterial(lateMaterial) == 0, "orphaned material unregister is not a no-op"); RequireThrows([&] { sceAudioPropagationSourceDestroy(system, lateSource); }, "orphan destroyed twice"); RequireThrows([&] { sceAudioPropagationSystemDestroy(system); }, "destroy twice"); - Require(sceAudioPropagationSystemCreate(options, &memory, &system) == 0, "memory reuse after destroy failed"); + Require(sceAudioPropagationSystemCreate(&options, &memory, &system) == 0, "memory reuse after destroy failed"); Require(sceAudioPropagationSystemDestroy(system) == 0, "second destroy failed"); + RequireThrows([&] { sceAudioPropagationPathGetNumPoints(); }, "path get num points throw"); + RequireThrows([&] { sceAudioPropagationSystemLock(); }, "system lock throw"); + + Require(Resolve("sceAudioPropagationSystemCreate") != nullptr && Resolve("sceAudioPropagationSourceSetAudioPath") != nullptr, "export lookup"); + for (const char* name : {"sceAudioPropagationAudioPathInit", "sceAudioPropagationAudioPathPointInit", "sceAudioPropagationMaterialInit", "sceAudioPropagationPortalSettingsInit", "sceAudioPropagationRayInit", "sceAudioPropagationSourceRenderInfoInit", "sceAudioPropagationSourceSetAudioPathsParamInit", "sceAudioPropagationSystemMemoryInit", "sceAudioPropagationSystemOptionInit", "sceAudioPropagationSystem"}) { + Require(Resolve(name) == nullptr, name); + } + std::puts("AudioPropagation tests passed"); return 0; } diff --git a/core/libs/tests/GuestAudiodec.cpp b/core/libs/tests/GuestAudiodec.cpp index 2e6af3008a..3eb7a370db 100644 --- a/core/libs/tests/GuestAudiodec.cpp +++ b/core/libs/tests/GuestAudiodec.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include extern "C" { @@ -143,6 +145,60 @@ const std::uint8_t MP3_MONO[] = { 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, }; +const std::uint8_t HEAAC_V2_MONO[] = { + 0xFF, 0xF1, 0x58, 0x40, 0x10, 0x02, 0x80, 0x01, 0x40, 0x22, 0x80, 0xA3, 0x6E, 0xEF, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4A, 0x80, + 0x00, 0x06, 0xF5, 0x61, 0x0A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5E, 0xFF, 0xF1, 0x58, 0x40, 0x10, 0x02, 0x80, 0x01, 0x40, 0x42, 0x80, 0xA3, 0x78, 0x46, 0x81, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x38, 0x76, 0xE4, 0x89, 0xB5, 0x65, 0xE5, 0xDF, 0xFE, 0xB7, 0x32, 0x7C, 0x7F, 0xF0, 0x00, 0x06, 0xF4, 0xC1, 0x0A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5E, 0xFF, 0xF1, 0x58, 0x40, 0x18, 0xA2, 0x38, 0x01, + 0x0A, 0x97, 0x98, 0xD8, 0x73, 0x75, 0x18, 0xBB, 0x6D, 0xCD, 0xA6, 0xE8, 0x7F, 0x09, 0xD5, 0xA0, 0x7E, 0x99, 0xC7, 0x42, 0xF2, 0x57, 0xBB, 0xC8, + 0xAB, 0x51, 0x57, 0x0A, 0xBF, 0x56, 0x8A, 0x40, 0x48, 0xFB, 0xCC, 0xF1, 0xFF, 0x4E, 0x4C, 0x21, 0xEE, 0x7F, 0x10, 0xFD, 0xA6, 0xB4, 0xE2, 0xCC, + 0x52, 0xA9, 0x38, 0x00, 0x00, 0xC4, 0x1A, 0x9A, 0xD2, 0x05, 0xE8, 0x94, 0xDB, 0xA5, 0x47, 0xFE, 0x92, 0xFC, 0x12, 0x41, 0x11, 0x08, 0x01, 0x22, + 0x65, 0x54, 0x28, 0x24, 0x16, 0x72, 0xE9, 0xAA, 0xC7, 0x45, 0x33, 0xEC, 0x74, 0x13, 0xAC, 0x84, 0xD5, 0xD4, 0x95, 0xCB, 0x41, 0x9B, 0x55, 0x2D, + 0xD5, 0xAC, 0x5C, 0xEF, 0xD4, 0x75, 0xF8, 0xD5, 0xCE, 0xC9, 0x64, 0xD9, 0xF5, 0x80, 0xD3, 0xD9, 0xAA, 0x5F, 0x1E, 0x45, 0x4F, 0xEA, 0x5F, 0xF8, + 0xBF, 0x48, 0x1F, 0xAD, 0xDF, 0x0C, 0xE5, 0xF1, 0x7F, 0x65, 0xC0, 0x12, 0xFA, 0xBF, 0xE6, 0x40, 0x0D, 0xAA, 0xFD, 0x40, 0x02, 0x58, 0x64, 0xA4, + 0x03, 0x2A, 0xC1, 0xAA, 0x79, 0x01, 0x6D, 0x37, 0x60, 0x78, 0x7B, 0x2B, 0x1B, 0x62, 0x6C, 0x87, 0xA7, 0xA2, 0xDB, 0x65, 0x00, 0xDE, 0x0F, 0xA0, + 0x50, 0x00, 0x00, 0x00, 0x00, 0x01, 0xDB, 0x92, 0x26, 0xD5, 0x97, 0x97, 0x7F, 0xFA, 0xDC, 0xC9, 0xF1, 0xFF, 0xC0, 0x01, 0xC0, 0xFF, 0xF1, 0x58, + 0x40, 0x15, 0x02, 0x10, 0x01, 0x3C, 0xF3, 0x08, 0xB6, 0xA1, 0x99, 0x0C, 0x42, 0x0A, 0xA1, 0x40, 0xF6, 0x05, 0x0A, 0x6C, 0x0B, 0xC0, 0x1F, 0x1F, + 0x19, 0x9D, 0x15, 0x2A, 0x56, 0x97, 0x6C, 0x63, 0x88, 0xF4, 0x89, 0x13, 0x53, 0xA1, 0x9D, 0x8E, 0x9A, 0x93, 0x0C, 0x6A, 0x18, 0x0F, 0x2C, 0xCB, + 0x53, 0xB3, 0x93, 0xA9, 0xA9, 0x32, 0x63, 0xC3, 0x0C, 0x07, 0xF0, 0xF1, 0x3C, 0x98, 0xB9, 0x6B, 0x75, 0xB0, 0x95, 0xAD, 0x62, 0xDC, 0xA4, 0xB4, + 0x75, 0xE1, 0xAD, 0xE9, 0x64, 0x31, 0x51, 0x24, 0xAE, 0x6E, 0x20, 0xC0, 0x9D, 0x70, 0x25, 0x47, 0x1D, 0x67, 0x4D, 0x5C, 0xE1, 0xDA, 0x4E, 0x8A, + 0x9E, 0xF9, 0xFA, 0x62, 0x16, 0xDA, 0x99, 0x9F, 0xD0, 0xDC, 0x7D, 0xBD, 0x5F, 0x31, 0x8B, 0xC2, 0xE8, 0xD0, 0xB8, 0xE5, 0xC6, 0xC0, 0x37, 0x8F, + 0x68, 0xBC, 0x92, 0x4C, 0xD0, 0x00, 0x01, 0xFC, 0xFD, 0x43, 0xF2, 0xF5, 0xFF, 0x9F, 0xBF, 0xAD, 0x6D, 0x56, 0x0D, 0x3F, 0xF4, 0xA5, 0xD9, 0x17, + 0xFF, 0x20, 0x02, 0x7C, 0x02, 0xCC, 0x8B, 0x26, 0x49, 0xFF, 0xFB, 0x9E, 0xA2, 0x00, 0x0F, 0xE4, 0xDF, 0xF0, 0x00, 0x00, 0x70, +}; + +const std::uint8_t HEAAC_STEREO[] = { + 0xFF, 0xF1, 0x58, 0x80, 0x10, 0x02, 0xC0, 0x21, 0x21, 0x45, 0x00, 0x14, 0x50, 0x01, 0x46, 0xAD, 0xD5, 0x81, 0x75, 0x00, 0x00, 0x00, 0x1C, 0x60, + 0x18, 0x0D, 0xEA, 0xC2, 0x14, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, + 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, + 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, + 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, + 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xBC, 0xFF, 0xF1, 0x58, 0x80, 0x1B, 0x62, 0x90, 0x21, 0x4A, 0xCC, 0x80, 0x1F, 0xEA, 0xFE, 0xBF, 0xC8, + 0x87, 0x77, 0x07, 0x5B, 0x6B, 0x0A, 0xDA, 0x70, 0x6A, 0xC6, 0x8F, 0xE1, 0xE6, 0xC4, 0x36, 0x01, 0x48, 0xB1, 0x94, 0xF0, 0x67, 0x71, 0x13, 0x21, + 0x28, 0xB0, 0x74, 0xC1, 0x02, 0x0B, 0xED, 0x1B, 0x75, 0xF2, 0x1D, 0x89, 0x8A, 0xFA, 0xCB, 0x17, 0x0D, 0x51, 0xAE, 0x81, 0x2D, 0xB3, 0x27, 0x07, + 0x00, 0x69, 0x11, 0x1E, 0x51, 0x2E, 0x56, 0x4C, 0xD1, 0x9C, 0x39, 0x65, 0xED, 0x43, 0x53, 0x4E, 0x4E, 0x22, 0x8D, 0x5D, 0x48, 0x56, 0x50, 0x66, + 0x75, 0x4B, 0x3D, 0x6B, 0x10, 0xE5, 0x38, 0x97, 0x8D, 0x5C, 0xEC, 0xCC, 0x6F, 0xDA, 0x1D, 0xD5, 0x99, 0x0C, 0xBD, 0x50, 0xDB, 0x92, 0xDE, 0x06, + 0xD1, 0x0A, 0xE2, 0xD5, 0x32, 0x9C, 0xEA, 0x82, 0x1A, 0x30, 0x72, 0x92, 0xC1, 0x30, 0x88, 0xAC, 0x38, 0x15, 0xD9, 0xE7, 0x22, 0x5A, 0x97, 0x94, + 0x0D, 0xE4, 0xFF, 0xFF, 0xFC, 0xC8, 0xD0, 0xEE, 0xDB, 0x60, 0x26, 0x6D, 0x11, 0x0D, 0x69, 0x10, 0x61, 0x63, 0xA7, 0xE7, 0xB7, 0x58, 0x11, 0x63, + 0x29, 0xE0, 0xFB, 0xBD, 0x8E, 0x3C, 0x98, 0xBC, 0x01, 0x7E, 0x8F, 0x7B, 0xF7, 0xCB, 0x3A, 0xC2, 0xC0, 0xC2, 0xC2, 0xCB, 0x01, 0x1F, 0xA3, 0x94, + 0xE8, 0x28, 0x44, 0x40, 0x52, 0x10, 0x42, 0x48, 0x0A, 0x94, 0x53, 0x9D, 0x57, 0xB0, 0x4B, 0x3E, 0x50, 0x80, 0xDF, 0xD2, 0x70, 0x30, 0x95, 0x33, + 0x92, 0x2C, 0xAD, 0x9D, 0x59, 0x74, 0x83, 0xC0, 0x00, 0x00, 0x38, 0xFF, 0xF1, 0x58, 0x80, 0x16, 0xC2, 0x74, 0x21, 0x79, 0x08, 0x00, 0x87, 0xFF, + 0xFF, 0xCF, 0x11, 0x6C, 0xCC, 0x21, 0x92, 0x14, 0x02, 0x80, 0xE8, 0x05, 0x50, 0x10, 0x50, 0x3F, 0xC7, 0xC6, 0x67, 0x50, 0xB0, 0xBB, 0x63, 0x1A, + 0x77, 0x48, 0xF3, 0x0E, 0x0A, 0x02, 0x2F, 0xC6, 0xDA, 0xD2, 0xFA, 0xDA, 0x2D, 0x32, 0x15, 0x6E, 0xEE, 0xF0, 0x11, 0x7F, 0x0F, 0x13, 0xC9, 0x48, + 0xEB, 0x84, 0x2D, 0x63, 0xAD, 0xAD, 0x61, 0xCA, 0x43, 0x5E, 0x1A, 0xDC, 0x31, 0x76, 0x4B, 0x9C, 0xBB, 0xD1, 0xF7, 0xD4, 0x84, 0xF0, 0x35, 0x83, + 0x78, 0xEE, 0x95, 0x05, 0x2E, 0x61, 0x76, 0xDB, 0xE1, 0x68, 0xC9, 0x23, 0xA4, 0x1E, 0xAA, 0x71, 0xCA, 0x9A, 0xA5, 0xEE, 0x88, 0x0D, 0x0F, 0xE7, + 0xFE, 0x55, 0x6D, 0x0F, 0x61, 0xA3, 0xAE, 0xEE, 0xF3, 0x08, 0xCB, 0x13, 0x08, 0x28, 0x0A, 0x1E, 0x28, 0x40, 0x70, 0xA0, 0xA7, 0x84, 0x14, 0x15, + 0xF5, 0xD0, 0x50, 0x53, 0x51, 0x5F, 0x32, 0x89, 0xA8, 0xAC, 0x1F, 0x73, 0x62, 0xD1, 0x67, 0x53, 0x57, 0x86, 0x2C, 0x80, 0x44, 0xFB, 0xC2, 0xF4, + 0xAF, 0x24, 0x93, 0x1D, 0x40, 0x00, 0xFF, 0x89, 0x36, 0xFF, 0xC6, 0x45, 0x23, 0x7F, 0xF0, 0x5B, 0xCC, 0x00, 0x00, 0x00, 0x03, 0x20, 0x0C, 0x01, + 0x38, +}; constexpr std::int32_t errorApiFail = static_cast(0x807F0000); constexpr std::int32_t errorInvalidType = static_cast(0x807F0001); constexpr std::int32_t errorArg = static_cast(0x807F0002); @@ -225,8 +281,48 @@ void TestAacRaw51(std::uint32_t maxChannels) { Require(sceAudiodecDeleteDecoder(handle) == 0); } +double UpperHalfEnergyShare(const std::int16_t* samples, std::size_t count, std::size_t stride) { + double upper = 0; + double total = 0; + for (std::size_t bin = 1; bin < count / 2; ++bin) { + double real = 0; + double imaginary = 0; + for (std::size_t i = 0; i < count; ++i) { + const double phase = 2 * std::numbers::pi * static_cast(bin * i % count) / static_cast(count); + real += samples[i * stride] * std::cos(phase); + imaginary += samples[i * stride] * std::sin(phase); + } + total += real * real + imaginary * imaginary; + if (bin >= count / 4) upper += real * real + imaginary * imaginary; + } + return upper / total; +} + +void TestHeAac(const std::uint8_t* stream, std::size_t streamSize, std::uint32_t channels, bool adts, bool highEfficiency) { + AudiodecParamM4aac param{sizeof(param), 1, adts ? 1u : 2u, 6, channels, highEfficiency ? 1u : 0u}; + AudiodecM4aacInfo info{sizeof(info), 0, 0, 0, 0}; + AudiodecCtrl ctrl{¶m, &info, nullptr, nullptr}; + const std::int32_t handle = sceAudiodecCreateDecoder(&ctrl, 3); + Require(handle > 0); + Streams streams; + const std::uint32_t samples = highEfficiency ? 2048 : 1024; + const std::uint32_t room = samples * channels * 2; + for (std::size_t offset = 0; offset < streamSize; offset += AdtsLength(stream + offset)) { + const std::size_t length = AdtsLength(stream + offset); + Require(Decode(handle, ctrl, streams, adts ? stream + offset : stream + offset + 7, adts ? length : length - 7, room) == 0); + Require(streams.au.ui_au_size == (adts ? length : length - 7)); + Require(streams.pcm.ui_pcm_size == room && info.i_result == 0); + Require(info.ui_sampling_freq == (highEfficiency ? 48000u : 24000u) && info.ui_number_of_channels == channels); + } + if (highEfficiency) { + Require(info.ui_heaac == 1); + Require(UpperHalfEnergyShare(reinterpret_cast(streams.output.data()), samples, channels) > 0.02); + } + Require(sceAudiodecDeleteDecoder(handle) == 0); +} + void TestMp3() { - AudiodecParamMp3 param{sizeof(param), 2}; + AudiodecParamMp3 param{sizeof(param), 1}; AudiodecMp3Info info{}; info.ui_size = sizeof(info); AudiodecCtrl ctrl{¶m, &info, nullptr, nullptr}; @@ -239,9 +335,9 @@ void TestMp3() { while (offset < sizeof(MP3_MONO)) { Require(Decode(handle, ctrl, streams, MP3_MONO + offset, sizeof(MP3_MONO) - offset) == 0); Require(streams.au.ui_au_size == 144 * 32000 / 48000 + (MP3_MONO[offset + 2] >> 1 & 1)); - Require(streams.pcm.ui_pcm_size == 1152 * 4); + Require(streams.pcm.ui_pcm_size == 1152 * 2); Require(info.ui_header >> 21 == 0x7FF && info.uc_mode == 3 && info.i_result == 0); - for (std::size_t i = 0; i < 1152; ++i) peak = std::fmax(peak, std::fabs(reinterpret_cast(streams.output.data())[i])); + for (std::size_t i = 0; i < 1152; ++i) peak = std::fmax(peak, std::fabs(reinterpret_cast(streams.output.data())[i] / 32768.0f)); offset += streams.au.ui_au_size; ++frames; } @@ -266,6 +362,14 @@ void TestAt9() { Require(Decode(handle, ctrl, streams, silence.data(), silence.size(), 4095) == errorInvalidPcmSize); Require(Decode(handle, ctrl, streams, silence.data(), silence.size()) == errorApiFail); Require(sceAudiodecDeleteDecoder(handle) == 0); + for (const std::int32_t wordSize : {0, 3, -1}) { + param.i_bw_pcm = wordSize; + Require(sceAudiodecCreateDecoder(&ctrl, 1) == errorInvalidWordLength); + } + param.i_bw_pcm = 2; + const std::int32_t floatHandle = sceAudiodecCreateDecoder(&ctrl, 1); + Require(floatHandle > 0 && sceAudiodecDeleteDecoder(floatHandle) == 0); + param.i_bw_pcm = 1; param.ui_config_data[0] = 0xFD; Require(sceAudiodecCreateDecoder(&ctrl, 1) == errorInvalidAt9Config); } @@ -290,13 +394,56 @@ int main() { mp3Info.ui_size = 16; Require(sceAudiodecCreateDecoder(&ctrl, 2) == errorInvalidBsiInfoSize); mp3Info.ui_size = sizeof(mp3Info); - mp3.i_bw_pcm = 3; - Require(sceAudiodecCreateDecoder(&ctrl, 2) == errorInvalidWordLength); + for (const std::int32_t wordSize : {0, 2, 3, -1}) { + mp3.i_bw_pcm = wordSize; + Require(sceAudiodecCreateDecoder(&ctrl, 2) == errorInvalidWordLength); + } mp3.i_bw_pcm = 1; AudiodecParamM4aac aac{sizeof(aac), 1, 3, 3, 2, 0}; AudiodecM4aacInfo aacInfo{sizeof(aacInfo), 0, 0, 0, 0}; AudiodecCtrl aacCtrl{&aac, &aacInfo, nullptr, nullptr}; Require(sceAudiodecCreateDecoder(&aacCtrl, 3) == errorInvalidConfigNumber); + aac.ui_config_number = 1; + for (const std::int32_t wordSize : {0, 3, -1}) { + aac.i_bw_pcm = wordSize; + Require(sceAudiodecCreateDecoder(&aacCtrl, 3) == errorInvalidWordLength); + } + aac.i_bw_pcm = 2; + const std::int32_t floatAac = sceAudiodecCreateDecoder(&aacCtrl, 3); + Require(floatAac > 0 && sceAudiodecDeleteDecoder(floatAac) == 0); + for (const std::uint32_t channels : {0u, 3u, 4u, 5u, 7u, 9u}) { + aac.ui_max_channels = channels; + Require(sceAudiodecCreateDecoder(&aacCtrl, 3) == static_cast(0x807F0304)); + } + aac.ui_max_channels = 6; + aac.ui_enable_heaac = 1; + Require(sceAudiodecCreateDecoder(&aacCtrl, 3) == static_cast(0x807F0302)); + struct ExtendedParam { + AudiodecParamM4aac base; + std::uint32_t extensionMode; + std::uint32_t outputMode; + } extended{{32, 1, 1, 3, 2, 0}, 0, 0}; + aacCtrl.pParam = &extended; + for (const std::uint32_t size : {25u, 27u, 29u, 31u, 33u}) { + extended.base.ui_size = size; + Require(sceAudiodecCreateDecoder(&aacCtrl, 3) == errorInvalidParamSize); + } + for (const std::uint32_t size : {28u, 32u}) { + extended.base.ui_size = size; + extended.extensionMode = 2; + Require(sceAudiodecCreateDecoder(&aacCtrl, 3) == static_cast(0x807F0305)); + extended.extensionMode = 1; + bool threw = false; + try { sceAudiodecCreateDecoder(&aacCtrl, 3); } + catch (const std::runtime_error&) { threw = true; } + Require(threw); + extended.extensionMode = 0; + const auto extendedHandle = sceAudiodecCreateDecoder(&aacCtrl, 3); + Require(extendedHandle > 0); + Require(sceAudiodecDeleteDecoder(extendedHandle) == 0); + } + extended.outputMode = 3; + Require(sceAudiodecCreateDecoder(&aacCtrl, 3) == static_cast(0x807F0306)); const std::int32_t handle = sceAudiodecCreateDecoder(&ctrl, 2); Require(handle > 0); @@ -315,6 +462,12 @@ int main() { TestAac(false, 8); TestAacRaw51(6); TestAacRaw51(8); + for (const bool adts : {true, false}) { + for (const bool highEfficiency : {true, false}) { + TestHeAac(HEAAC_V2_MONO, sizeof(HEAAC_V2_MONO), 1, adts, highEfficiency); + TestHeAac(HEAAC_STEREO, sizeof(HEAAC_STEREO), 2, adts, highEfficiency); + } + } TestMp3(); TestAt9(); for (std::uint32_t type = 1; type <= 3; ++type) Require(sceAudiodecTermLibrary(type) == 0); diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index 0410bbab96..54220d0b27 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -17,10 +17,13 @@ #include #include #include +#include #include #include #include "SceTypes.hpp" +#include "prx/libSceAvPlayer/include/AvPlayer.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceAvPlayer/include/AvPlayer.hpp" extern "C" { AvPlayerInternal* APS5_VABI sceAvPlayerInit(AvPlayerInitData*); @@ -35,7 +38,7 @@ int APS5_VABI sceAvPlayerEnableStream(AvPlayerInternal*, std::uint32_t); int APS5_VABI sceAvPlayerChangeStream(AvPlayerInternal*, std::uint32_t, std::uint32_t); int APS5_VABI sceAvPlayerSetAvailableBandwidth(AvPlayerInternal*, std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI sceAvPlayerStart(AvPlayerInternal*); -int APS5_VABI sceAvPlayerStartEx(AvPlayerInternal*, const void*); +int APS5_VABI sceAvPlayerStartEx(AvPlayerInternal*, const AvPlayer::StartInfo*); int APS5_VABI sceAvPlayerStop(AvPlayerInternal*); int APS5_VABI sceAvPlayerPause(AvPlayerInternal*); int APS5_VABI sceAvPlayerResume(AvPlayerInternal*); @@ -72,6 +75,8 @@ 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 int WebmVideoFrames = 6; constexpr std::array Movie{ 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d, 0x00, 0x00, 0x02, 0x00, @@ -451,6 +456,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 +651,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(); @@ -754,7 +867,7 @@ void TestPlayback() { last = frame.timestamp; ++frames; } - return frames >= 8 && sounds >= 8; + return frames >= 2 && sounds >= 2; }), "playback stalled"); Check(sceAvPlayerCurrentTime(player) > 0, "clock not running"); Check(sceAvPlayerSetTrickSpeed(player, 0) == InvalidParams, "zero trick speed accepted"); @@ -829,7 +942,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 +955,56 @@ 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"); + } + const auto play = [](bool withAudio) { + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "WebM init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.webm") == 0, "WebM add source failed"); + Check(WaitFor([&] { return events.Seen(EventReady); }), "WebM ready event missing"); + Check(sceAvPlayerStreamCount(player) == 2, "unexpected WebM stream count"); + AvPlayerStreamInfo video{}; + Check(sceAvPlayerGetStreamInfo(player, 0, &video) == 0 && video.type == 1 && video.details.video.width == 112 && video.details.video.height == 64, "unexpected VP9 stream"); + AvPlayerStreamInfo audio{}; + Check(sceAvPlayerGetStreamInfo(player, 1, &audio) == 0 && audio.type == 0 && audio.details.audio.channel_count == 2 && audio.details.audio.sample_rate == SampleRate, "unexpected Opus stream"); + Check(sceAvPlayerEnableStream(player, 0) == 0 && (!withAudio || sceAvPlayerEnableStream(player, 1) == 0), "enable WebM streams failed"); + if (!withAudio) Check(sceAvPlayerSetAvSyncMode(player, 1) == 0, "WebM no-sync mode rejected"); + Check(sceAvPlayerStart(player) == 0, "WebM start failed"); + AvPlayerFrameInfoEx frame{}; + AvPlayerFrameInfo sound{}; + int frames = 0; + int sounds = 0; + std::uint64_t last = 0; + Check(WaitFor([&] { + if (withAudio && sceAvPlayerGetAudioData(player, &sound)) { + CheckEnglishAudio(sound); + ++sounds; + } + if (sceAvPlayerGetVideoDataEx(player, &frame)) { + CheckVideoFrame(frame); + Check(frames == 0 || frame.timestamp > last, "WebM video timestamps do not increase"); + last = frame.timestamp; + ++frames; + } + return !sceAvPlayerIsActive(player); + }), "WebM playback never ended"); + Check(sceAvPlayerClose(player) == 0, "WebM close failed"); + std::lock_guard lock(allocations.mutex); + Check(allocations.blocks.empty(), "WebM allocations leaked"); + return std::pair{frames, sounds}; + }; + const auto [decoded, silent] = play(false); + Check(decoded == WebmVideoFrames && silent == 0, "WebM video-only playback delivered " + std::to_string(decoded) + " of " + std::to_string(WebmVideoFrames) + " frames"); + const auto [frames, sounds] = play(true); + Check(frames >= 1 && sounds >= 2, "WebM delivered " + std::to_string(frames) + " frames and " + std::to_string(sounds) + " audio chunks"); +} + void TestChangeStream() { Events events; AvPlayerInitData init = InitData(&events); @@ -910,11 +1073,22 @@ void TestExtendedExports() { CheckStreamInfoEx(player); Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "enable video failed"); Check(sceAvPlayerStartEx(nullptr, nullptr) == InvalidParams, "start accepted a null player"); - Check(sceAvPlayerStartEx(player, nullptr) == 0, "extended start failed"); + Check(sceAvPlayerStartEx(player, nullptr) == InvalidParams, "start accepted null parameters"); + AvPlayer::StartInfo startInfo{0, 700}; + Check(sceAvPlayerStartEx(player, &startInfo) == InvalidParams, "start accepted an invalid parameter size"); + startInfo.size = sizeof(startInfo); + Check(sceAvPlayerStartEx(player, &startInfo) == 0, "extended start failed"); Check(WaitFor([&] { return events.Seen(EventPlay); }), "play event missing"); AvPlayerFrameInfoEx frame{}; Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "no frame after the extended start"); CheckVideoFrame(frame); + Check(frame.timestamp >= 700 && frame.timestamp <= 767, "extended start ignored the requested position"); + Check(!events.Seen(EventWarning, JumpComplete), "extended start reported a jump"); + Check(sceAvPlayerStop(player) == 0, "extended stop failed"); + startInfo.startTime = 0; + Check(sceAvPlayerStartEx(player, &startInfo) == 0, "extended restart failed"); + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "no frame after extended restart"); + Check(frame.timestamp <= 67, "zero-position start did not restart playback"); Check(sceAvPlayerClose(player) == 0, "close failed"); } @@ -1028,8 +1202,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 +1226,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); @@ -1094,6 +1286,26 @@ void TestRestartAfterPausedJumpKeepsClockRunning() { Check(sceAvPlayerClose(player) == 0, "close failed"); } +void TestPausedPreparationAfterAudioStarts(std::uint32_t syncMode) { + Events events; + auto init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source failed"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "enable video failed"); + Check(sceAvPlayerEnableStream(player, EnglishAudioStream) == 0, "enable audio failed"); + Check(sceAvPlayerSetAvSyncMode(player, syncMode) == 0, "sync mode failed"); + Check(sceAvPlayerStart(player) == 0, "start failed"); + AvPlayerFrameInfo audio{}; + Check(WaitFor([&] { return sceAvPlayerGetAudioData(player, &audio) != 0; }), "no initial audio frame"); + Check(sceAvPlayerPause(player) == 0, "pause failed"); + AvPlayerFrameInfoEx frame{}; + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "first video frame blocked during paused preparation after audio started"); + CheckVideoFrame(frame); + Check(!sceAvPlayerGetVideoDataEx(player, &frame), "paused preparation delivered more than the initial video frame"); + Check(sceAvPlayerClose(player) == 0, "close failed"); +} + void TestUnsyncedVideoKeepsUpWithAudio() { Events events; AvPlayerInitData init = InitData(&events); @@ -1123,6 +1335,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 +1543,105 @@ 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"); +} + +void TestVideoSyncTolerance() { + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "sync tolerance init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "sync tolerance source failed"); + Check(WaitFor([&] { return events.Seen(EventReady); }), "sync tolerance ready missing"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "sync tolerance video failed"); + Check(sceAvPlayerStart(player) == 0, "sync tolerance start failed"); + Check(WaitFor([&] { return events.Seen(EventPlay); }), "sync tolerance play missing"); + AvPlayerFrameInfoEx frame{}; + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "sync tolerance first frame missing"); + Check(FrameIndex(frame.timestamp) == 0, "first frame not index 0"); + AvPlayer::SetAvPlayerClockForTest_nid_no_patch(player, 5.0); + Check(!WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }, 200), "frame beyond sync tolerance delivered prematurely"); + AvPlayer::SetAvPlayerClockForTest_nid_no_patch(player, 10.0); + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "frame within sync tolerance was not delivered"); + Check(FrameIndex(frame.timestamp) == 1, "unexpected second frame index"); + Check(sceAvPlayerClose(player) == 0, "sync tolerance close failed"); + std::lock_guard lock(allocations.mutex); + Check(allocations.blocks.empty(), "sync tolerance allocations leaked"); +} + +} + +int main(int argc, char** argv) { 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; + } + if (argc == 2 && std::strcmp(argv[1], "--paused-first-video") == 0) { + WriteMovie(); + TestPausedPreparationAfterAudioStarts(0); + TestPausedPreparationAfterAudioStarts(1); + std::puts("AvPlayer paused first video tests passed"); + return 0; + } + Check(argc == 1, "unknown AvPlayer test arguments"); WriteMovie(); + TestLoopWarningAtPlayback(); + TestAudioLoopWarningAtPlayback(); + TestLoopWarningSharedByStreams(); TestPlayback(); + TestVp9OpusPlayback(); TestChangeStream(); TestExtendedExports(); TestWithoutAllocators(); @@ -1194,7 +1652,10 @@ int main() { TestAutoStartKeepsStartedPlayback(); TestRestartAfterPausedJumpKeepsClockRunning(); TestUnsyncedVideoKeepsUpWithAudio(); + TestNoSyncVideoCanLeadAudioClock(); + TestLoopWarningWaitsForPlayback(); TestEofStopLatency(); + TestVideoSyncTolerance(); std::puts("AvPlayer tests passed"); return 0; } catch (const std::exception& error) { 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..7ae55383e3 --- /dev/null +++ b/core/libs/tests/GuestBacktrace.cpp @@ -0,0 +1,149 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#if defined(_WIN32) +#include +#else +#include +#endif +#include +#include + +extern "C" { +int APS5_VABI sceLibcBacktraceGetBufferSize_nid_postfix(int, int*, std::size_t*); +int APS5_VABI sceLibcBacktraceSelf_nid_postfix(int, void*, std::size_t, int*); +#if defined(_WIN32) +std::string BacktraceModulePath_nid_no_patch(HMODULE, std::size_t); +#endif +} + +namespace { + +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]] std::uintptr_t CurrentIp() { + asm volatile("" ::: "memory"); + return reinterpret_cast(__builtin_return_address(0)); +} + +} + +[[gnu::noinline]] void CheckBacktrace() { +#if defined(_WIN32) + HMODULE handle = nullptr; + Require(GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&CheckBacktrace), &handle) != 0 && handle != nullptr, + "GetModuleHandleExA of the test failed"); + std::string executable = BacktraceModulePath_nid_no_patch(handle, 1); + const auto slash = executable.find_last_of("/\\"); + Require(!executable.empty() && slash != std::string::npos, "BacktraceModulePath of the test failed"); + executable.erase(0, slash + 1); +#else + Dl_info self{}; + Require(dladdr(reinterpret_cast(&CheckBacktrace), &self) != 0 && self.dli_fname != nullptr && self.dli_saddr != nullptr, "dladdr of the test failed"); + std::string executable = self.dli_fname; + executable.erase(0, executable.find_last_of('/') + 1); +#endif + + int count = -1; + std::size_t size = 0; + 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; + const auto before = CurrentIp(); + const int selfStatus = sceLibcBacktraceSelf_nid_postfix(-1, buffer.data(), size, &written); + const auto after = CurrentIp(); + Require(selfStatus == 0, "Self failed"); + Require(written == count, "Self wrote a different count than GetBufferSize reported"); + const auto records = Parse(buffer, size, written); + Require(Used(records) == size, "Self used a different size than GetBufferSize reported"); + for (std::size_t index = size; index < buffer.size(); ++index) Require(buffer[index] == Fill, "Self wrote past the buffer size"); + for (const auto& record : records) Require(record.reserved == 0 && !record.module.empty(), "record has a nonzero reserved word or no module"); + Require(records.front().module == executable, "first frame is not in the calling module"); +#if defined(_WIN32) + const auto address = reinterpret_cast(handle) + records.front().offset; + const auto callerReturn = reinterpret_cast(handle) + records[1].offset; + Require(address > before && address < after, "first frame is not the caller"); + Require(callerReturn == reinterpret_cast(__builtin_return_address(0)), "second frame is not the caller of CheckBacktrace"); +#else + Dl_info caller{}; + const auto address = reinterpret_cast(self.dli_fbase) + records.front().offset; + Require(dladdr(reinterpret_cast(address), &caller) != 0 && caller.dli_saddr == self.dli_saddr, "first frame is not the caller"); +#endif + + for (const int limit : {0, 1, 2}) { + Require(sceLibcBacktraceGetBufferSize_nid_postfix(limit, &count, &size) == 0 && count == limit, "GetBufferSize ignored the frame limit"); + 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/GuestBadOptionalAccess.cpp b/core/libs/tests/GuestBadOptionalAccess.cpp new file mode 100644 index 0000000000..a4280e3ef5 --- /dev/null +++ b/core/libs/tests/GuestBadOptionalAccess.cpp @@ -0,0 +1,40 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +struct TypeRecord { const void* const* vtable; const char* name; const TypeRecord* base; }; +struct Object { const void* vtable; const char* message; }; +struct Vtable { std::ptrdiff_t offset; const TypeRecord* type; const void* destroy; const void* deleteObject; const char* (APS5_VABI *what)(const Object*); }; + +extern "C" { +extern const TypeRecord _ZTISt9exception_nid_postfix; +extern const TypeRecord _ZTISt19bad_optional_access_nid_postfix; +extern const Vtable _ZTVSt19bad_optional_access_nid_postfix; +void APS5_VABI _ZNSt19bad_optional_accessC1Ev_nid_postfix(Object*); +void APS5_VABI _ZNSt19bad_optional_accessD1Ev_nid_postfix(Object*); +[[noreturn]] void APS5_VABI _ZSt26_Throw_bad_optional_accessv_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + const auto& type = _ZTISt19bad_optional_access_nid_postfix; + Require(std::strcmp(type.name, "St19bad_optional_access") == 0); + Require(type.base == &_ZTISt9exception_nid_postfix); + const auto& table = _ZTVSt19bad_optional_access_nid_postfix; + Require(table.offset == 0 && table.type == &type && table.what != nullptr); + Object object{}; + _ZNSt19bad_optional_accessC1Ev_nid_postfix(&object); + Require(object.vtable == &table.destroy); + Require(std::strcmp(table.what(&object), "bad_optional_access") == 0); + _ZNSt19bad_optional_accessD1Ev_nid_postfix(&object); + bool thrown = false; + try { + _ZSt26_Throw_bad_optional_accessv_nid_postfix(); + } catch (...) { + thrown = true; + } + Require(thrown); + return 0; +} diff --git a/core/libs/tests/GuestBessel.cpp b/core/libs/tests/GuestBessel.cpp new file mode 100644 index 0000000000..75c94ec4ac --- /dev/null +++ b/core/libs/tests/GuestBessel.cpp @@ -0,0 +1,187 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +double APS5_VABI j0_nid_postfix(double); +double APS5_VABI j1_nid_postfix(double); +double APS5_VABI jn_nid_postfix(int, double); +double APS5_VABI y0_nid_postfix(double); +double APS5_VABI y1_nid_postfix(double); +double APS5_VABI yn_nid_postfix(int, double); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static double Value(std::uint64_t bits) { return std::bit_cast(bits); } + +static std::uint64_t Bits(double value) { return std::bit_cast(value); } + +struct UnaryCase { std::uint64_t input; std::uint64_t output; }; +struct OrderCase { int order; std::uint64_t input; std::uint64_t output; }; + +static void CheckUnary(const char* name, double (APS5_VABI *function)(double), const UnaryCase* cases, std::size_t count) { + for (std::size_t index = 0; index < count; ++index) { + *__error_nid_postfix() = 12345; + const double result = function(Value(cases[index].input)); + if (Bits(result) != cases[index].output || *__error_nid_postfix() != 12345) { + std::fprintf(stderr, "Guest %s failed for %016llx: %016llx\n", name, static_cast(cases[index].input), + static_cast(Bits(result))); + std::abort(); + } + } +} + +static void CheckOrder(const char* name, double (APS5_VABI *function)(int, double), const OrderCase* cases, std::size_t count) { + for (std::size_t index = 0; index < count; ++index) { + *__error_nid_postfix() = 12345; + const double result = function(cases[index].order, Value(cases[index].input)); + if (Bits(result) != cases[index].output || *__error_nid_postfix() != 12345) { + std::fprintf(stderr, "Guest %s failed for %d, %016llx: %016llx\n", name, cases[index].order, + static_cast(cases[index].input), static_cast(Bits(result))); + std::abort(); + } + } +} + +static void CheckFirstKind() { + const UnaryCase zero[] = { + {0x0000000000000000ull, 0x3ff0000000000000ull}, {0x8000000000000000ull, 0x3ff0000000000000ull}, {0x3ddb7cdfd9d7bdbbull, 0x3ff0000000000000ull}, + {0x3e40000000000000ull, 0x3ff0000000000000ull}, {0x3eb0000000000000ull, 0x3feffffffffff800ull}, {0x3f1fffffffffffffull, 0x3feffffffe000000ull}, + {0x3f20000000000000ull, 0x3feffffffe000000ull}, {0x3fd0000000000000ull, 0x3fef807fc72aa864ull}, {0xbfe0000000000000ull, 0x3fee07f1d54c3f34ull}, + {0x3fe8000000000000ull, 0x3feba7df6a752a19ull}, {0x3fefffffffffffffull, 0x3fe87c7fdbd7b8f0ull}, {0x3ff0000000000000ull, 0x3fe87c7fdbd7b8f0ull}, + {0xbff8000000000000ull, 0x3fe060e46ce9651bull}, {0x3fffffffffffffffull, 0x3fcca873fb24cefcull}, {0x7ff0000000000000ull, 0x0000000000000000ull}, + {0xfff0000000000000ull, 0x0000000000000000ull}, {0x7ff8000000000123ull, 0x7ff8000000000123ull}, {0x3fee4317b35275e1ull, 0x3fe93c4854d71c27ull}, + {0x3fe7084d7429a0caull, 0x3febfce08c913631ull}, {0x3ff896d4ffdc2a13ull, 0x3fdf6faac7e7d3ceull}, {0x3ff28d079588dbb8ull, 0x3fe61dc352e1dc33ull}, + {0x3ffe7d3678a4d9a8ull, 0x3fd1d44a54769967ull}, + }; + CheckUnary("j0", j0_nid_postfix, zero, std::size(zero)); + const UnaryCase one[] = { + {0x0000000000000000ull, 0x0000000000000000ull}, {0x8000000000000000ull, 0x8000000000000000ull}, {0x3ddb7cdfd9d7bdbbull, 0x3dcb7cdfd9d7bdbbull}, + {0x3e40000000000000ull, 0x3e30000000000000ull}, {0x3eb0000000000000ull, 0x3e9ffffffffffc00ull}, {0x3f1fffffffffffffull, 0x3f0ffffffeffffffull}, + {0x3f20000000000000ull, 0x3f0fffffff000000ull}, {0x3fd0000000000000ull, 0x3fbfc02a9c749ee9ull}, {0xbfe0000000000000ull, 0xbfcf02a71f4870d7ull}, + {0x3fe8000000000000ull, 0x3fd65a01d66b68bdull}, {0x3fefffffffffffffull, 0x3fdc29c9ee970c6cull}, {0x3ff0000000000000ull, 0x3fdc29c9ee970c6cull}, + {0xbff8000000000000ull, 0xbfe1da9da9d6fc81ull}, {0x3fffffffffffffffull, 0x3fe27487958371f0ull}, {0x7ff0000000000000ull, 0x0000000000000000ull}, + {0xfff0000000000000ull, 0x8000000000000000ull}, {0x7ff8000000000123ull, 0x7ff8000000000123ull}, {0x3fecd8b073b795c0ull, 0x3fda038c68b70948ull}, + {0x3fffe99bfa2fcbc2ull, 0x3fe2775e2433b1d8ull}, {0x3ffa917f997cc231ull, 0x3fe267a06350cc21ull}, + }; + CheckUnary("j1", j1_nid_postfix, one, std::size(one)); +} + +static void CheckSecondKind() { + const UnaryCase zero[] = { + {0x0000000000000000ull, 0xfff0000000000000ull}, {0x8000000000000000ull, 0xfff0000000000000ull}, {0xbff0000000000000ull, 0x7ff8000000000000ull}, + {0x8000000000000001ull, 0x7ff8000000000000ull}, {0x7ff0000000000000ull, 0x0000000000000000ull}, + }; + CheckUnary("y0", y0_nid_postfix, zero, std::size(zero)); + const UnaryCase one[] = { + {0x0000000000000000ull, 0xfff0000000000000ull}, {0x8000000000000000ull, 0xfff0000000000000ull}, {0xbff0000000000000ull, 0x7ff8000000000000ull}, + {0x8000000000000001ull, 0x7ff8000000000000ull}, {0x7ff0000000000000ull, 0x0000000000000000ull}, {0x3bc79ca10c924223ull, 0xc40b9be638027fc9ull}, + {0x3c90000000000000ull, 0xc3445f306dc9c883ull}, {0x0000000000000001ull, 0xfff0000000000000ull}, + }; + CheckUnary("y1", y1_nid_postfix, one, std::size(one)); + Require(std::isnan(y0_nid_postfix(-std::numeric_limits::infinity())) && std::isnan(y1_nid_postfix(-std::numeric_limits::infinity()))); + Require(std::isnan(y0_nid_postfix(std::numeric_limits::quiet_NaN())) && std::isnan(y1_nid_postfix(std::numeric_limits::quiet_NaN()))); + for (const double input : {0.0, -0.0, -1.0, -std::numeric_limits::denorm_min()}) { + std::feclearexcept(FE_ALL_EXCEPT); + y0_nid_postfix(input); + y1_nid_postfix(input); + yn_nid_postfix(3, input); + Require(std::fetestexcept(FE_DIVBYZERO | FE_INVALID) == 0); + } +} + +static void CheckIntegerOrders() { + const OrderCase first[] = { + {2, 0x3ddb7cdfd9d7bdbbull, 0x3b979ca10c924224ull}, {5, 0xbddb7cdfd9d7bdbbull, 0xb4cfed4f3ec0c466ull}, {33, 0x3e00000000000000ull, 0x0000000000000000ull}, + {34, 0x3e00000000000000ull, 0x0000000000000000ull}, {-3, 0x3e00000000000000ull, 0xb9c5555555555555ull}, {2, 0x3ff8000000000000ull, 0x3fcdb50c80d5039aull}, + {5, 0x3ff0000000000000ull, 0x3f305e3d487e8377ull}, {10, 0x3fe0000000000000ull, 0x3d52637b5990470aull}, {-3, 0x3ff4000000000000ull, 0xbfa2e068dde7f417ull}, + {3, 0xbff4000000000000ull, 0xbfa2e068dde7f417ull}, {4, 0xbff4000000000000ull, 0x3f781279da92ee26ull}, {40, 0x3ff8000000000000ull, 0x34f2a1a3311d8462ull}, + {300, 0x3ff0000000000000ull, 0x0000000000000000ull}, {3, 0x8000000000000000ull, 0x8000000000000000ull}, {3, 0xfff0000000000000ull, 0x8000000000000000ull}, + {2, 0xfff0000000000000ull, 0x0000000000000000ull}, {7, 0x7ff8000000000123ull, 0x7ff8000000000123ull}, {-1, 0x3fe0000000000000ull, 0xbfcf02a71f4870d7ull}, + {0, 0xbfe0000000000000ull, 0x3fee07f1d54c3f34ull}, {3, 0x3fe91c9d97f75a72ull, 0x3f83d672272043afull}, {3, 0x3ff33bf06c96fafdull, 0x3fa0ea842d47fdfdull}, + {3, 0x3ffa0a4f9419df40ull, 0x3fb36db8f4e44fabull}, {3, 0x3ffb2f9912a3a9a6ull, 0x3fb5c4cf9b0be24full}, + }; + CheckOrder("jn", jn_nid_postfix, first, std::size(first)); + const OrderCase second[] = { + {2, 0x7ff0000000000000ull, 0x0000000000000000ull}, {-3, 0x7ff0000000000000ull, 0x0000000000000000ull}, {3, 0x0000000000000000ull, 0xfff0000000000000ull}, + {-4, 0x8000000000000000ull, 0xfff0000000000000ull}, {3, 0xbff0000000000000ull, 0x7ff8000000000000ull}, {5, 0x7ff8000000000123ull, 0x7ff8000000000123ull}, + {INT_MIN, 0x7ff0000000000000ull, 0x0000000000000000ull}, + }; + CheckOrder("yn", yn_nid_postfix, second, std::size(second)); + for (const double x : {0.75, 2.5, 3.0, 17.25, 1.0e5, 0x1p303}) { + Require(Bits(jn_nid_postfix(0, x)) == Bits(j0_nid_postfix(x)) && Bits(jn_nid_postfix(1, x)) == Bits(j1_nid_postfix(x))); + Require(Bits(jn_nid_postfix(-1, x)) == Bits(j1_nid_postfix(-x)) && Bits(jn_nid_postfix(-3, x)) == Bits(-jn_nid_postfix(3, x))); + Require(Bits(jn_nid_postfix(4, -x)) == Bits(jn_nid_postfix(4, x)) && Bits(jn_nid_postfix(5, -x)) == Bits(-jn_nid_postfix(5, x))); + Require(Bits(yn_nid_postfix(0, x)) == Bits(y0_nid_postfix(x)) && Bits(yn_nid_postfix(1, x)) == Bits(y1_nid_postfix(x))); + Require(Bits(yn_nid_postfix(-1, x)) == Bits(-y1_nid_postfix(x)) && Bits(yn_nid_postfix(-4, x)) == Bits(yn_nid_postfix(4, x))); + if (x < 0x1p302) Require(Bits(yn_nid_postfix(INT_MIN, x)) == Bits(yn_nid_postfix(2, x))); + } +} + +static bool Near(double actual, double expected, double tolerance) { + return std::fabs(actual - expected) <= std::fabs(expected) * tolerance; +} + +struct ReferenceCase { int order; double x; double value; }; + +static void CheckValues() { + const ReferenceCase first[] = { + {0, 0.3, 0.97762624653829609}, {0, 1.7, 0.39798485944610949}, {0, 2.5, -0.048383776468197996}, {0, 7.25, 0.29199692419177900}, + {0, 25.0, 0.096266783275958116}, {0, 123.5, -0.070484621663252289}, {1, 0.3, 0.14831881627310401}, {1, 1.7, 0.57776523152902322}, + {1, 2.5, 0.49709410246427404}, {1, 7.25, 0.068581700653131745}, {1, 25.0, -0.12535024958028990}, {1, 123.5, -0.013948992295178035}, + {2, 0.3, 0.011165861949063964}, {2, 1.7, 0.28173894235274136}, {2, 2.5, 0.44605905843961723}, {2, 7.25, -0.27307783435643231}, + {2, 25.0, -0.10629480324238131}, {2, 123.5, 0.070258727051184628}, {3, 0.3, 0.00055934304774884612}, {3, 1.7, 0.085149926948015264}, + {3, 2.5, 0.21660039103911352}, {3, 7.25, -0.21924533340150819}, {3, 25.0, 0.10834308106150890}, {3, 123.5, 0.016224578596430978}, + {7, 0.3, 3.3805443102187481e-10}, {7, 1.7, 5.8087210824311728e-5}, {7, 2.5, 0.00077655318753348495}, {7, 7.25, 0.25934915542688296}, + {7, 25.0, -0.010168168212703074}, {7, 123.5, 0.027311804641119322}, + }; + const ReferenceCase second[] = { + {0, 0.3, -0.80727357780451947}, {0, 1.7, 0.45202700018163463}, {0, 2.5, 0.49807035961523189}, {0, 7.25, 0.048422529865202829}, + {0, 25.0, -0.12724943226800614}, {0, 123.5, -0.013663522162735081}, {1, 0.3, -2.2931051383885290}, {1, 1.7, -0.28472624506406838}, + {1, 2.5, 0.14591813796678580}, {1, 7.25, -0.28934799419758705}, {1, 25.0, -0.098829964783237410}, {1, 123.5, 0.070429882264699084}, + {2, 0.3, -14.480094011452341}, {2, 1.7, -0.78699905319818566}, {2, 2.5, -0.38133584924180325}, {2, 7.25, -0.12824266619557167}, + {2, 25.0, 0.11934303508534715}, {2, 123.5, 0.014804087057709964}, {3, 0.3, -190.77481501430935}, {3, 1.7, -1.5670362330493096}, + {3, 2.5, -0.75605549675367100}, {3, 7.25, 0.21859341974485785}, {3, 25.0, 0.11792485039689295}, {3, 123.5, -0.069950397663639652}, + {7, 0.3, -1.3463966616414596e+8}, {7, 1.7, -807.61347285004828}, {7, 2.5, -62.829855967240050}, {7, 7.25, -0.35448064613382143}, + {7, 25.0, 0.16252257251113247}, {7, 123.5, -0.066461555200665692}, + }; + for (const auto& test : first) { + const double result = test.order == 0 ? j0_nid_postfix(test.x) : test.order == 1 ? j1_nid_postfix(test.x) : jn_nid_postfix(test.order, test.x); + if (!Near(result, test.value, 1e-12) || !Near(jn_nid_postfix(test.order, -test.x), test.order % 2 != 0 ? -test.value : test.value, 1e-12)) { + std::fprintf(stderr, "Guest J%d(%g) = %.17g, expected %.17g\n", test.order, test.x, result, test.value); + std::abort(); + } + } + for (const auto& test : second) { + const double result = test.order == 0 ? y0_nid_postfix(test.x) : test.order == 1 ? y1_nid_postfix(test.x) : yn_nid_postfix(test.order, test.x); + if (!Near(result, test.value, 1e-12) || !Near(yn_nid_postfix(test.order, test.x), test.value, 1e-12)) { + std::fprintf(stderr, "Guest Y%d(%g) = %.17g, expected %.17g\n", test.order, test.x, result, test.value); + std::abort(); + } + } + *__error_nid_postfix() = 12345; + for (const double x : {0x1p-30, 0.5, 3.0, 1.0e10, 0x1p310, std::numeric_limits::max()}) { + Require(std::isfinite(j0_nid_postfix(x)) && std::isfinite(j1_nid_postfix(x)) && std::isfinite(y0_nid_postfix(x)) && std::isfinite(y1_nid_postfix(x))); + Require(std::isfinite(jn_nid_postfix(6, x)) && !std::isnan(yn_nid_postfix(6, x))); + } + Require(*__error_nid_postfix() == 12345); + *__error_nid_postfix() = 0; +} + +int main() { + CheckFirstKind(); + CheckSecondKind(); + CheckIntegerOrders(); + CheckValues(); + return 0; +} diff --git a/core/libs/tests/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/GuestBluetoothHid.cpp b/core/libs/tests/GuestBluetoothHid.cpp index f85b1d9ae1..fb37aa3c23 100644 --- a/core/libs/tests/GuestBluetoothHid.cpp +++ b/core/libs/tests/GuestBluetoothHid.cpp @@ -1,24 +1,46 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include +struct BluetoothHidEvent; +struct BluetoothHidDeviceInfo; +struct BluetoothHidThreadParam { + std::int32_t priority; + std::uint32_t padding; + std::uint64_t affinityMask; + std::uint32_t reserved; + std::uint32_t tailPadding; +}; +struct BluetoothHidParam { + const BluetoothHidThreadParam* threadParam; + std::uint32_t reserved; + std::uint32_t padding; +}; +using BluetoothHidCallback = void (APS5_VABI *)(const BluetoothHidEvent*, std::int32_t, void*); + extern "C" { std::int32_t APS5_VABI sceBluetoothHidInit(); -std::int32_t APS5_VABI sceBluetoothHidRegisterCallback(void* callback, std::int32_t flags, void* param); +std::int32_t APS5_VABI sceBluetoothHidRegisterCallback(BluetoothHidCallback callback, void* userData, const BluetoothHidParam* param); std::int32_t APS5_VABI sceBluetoothHidUnregisterCallback(); std::int32_t APS5_VABI sceBluetoothHidRegisterDevice(std::uint16_t vendorId, std::uint16_t productId); std::int32_t APS5_VABI sceBluetoothHidUnregisterDevice(std::uint16_t vendorId, std::uint16_t productId); -int APS5_VABI sceBluetoothHidGetInputReport(); +int APS5_VABI sceBluetoothHidGetInputReport(std::uint64_t, std::uint8_t); +int APS5_VABI sceBluetoothHidDebugGetVersion(); +int APS5_VABI sceBluetoothHidGetDeviceInfo(std::int32_t, BluetoothHidDeviceInfo*); +int APS5_VABI sceBluetoothHidDisconnectDevice(std::int32_t); +int APS5_VABI sceBluetoothHidGetFeatureReport(std::uint64_t, std::uint8_t); +int APS5_VABI sceBluetoothHidSetFeatureReport(std::uint64_t, std::uint8_t, const void*, std::size_t); } namespace { int callbackCalls = 0; -void APS5_VABI Callback() { +void APS5_VABI Callback(const BluetoothHidEvent*, std::int32_t, void*) { ++callbackCalls; } @@ -32,25 +54,67 @@ void Require(bool condition, const char* message) { } int main() { + constexpr auto notInitialized = static_cast(0x81540004u); + constexpr auto invalidArgument = static_cast(0x81540001u); + constexpr auto noCallback = static_cast(0x81540006u); + Require(sceBluetoothHidRegisterCallback(nullptr, nullptr, nullptr) == notInitialized, "initialization check order"); + Require(sceBluetoothHidRegisterDevice(1, 2) == notInitialized, "registration before initialization"); + Require(sceBluetoothHidUnregisterDevice(1, 2) == notInitialized, "unregistration before initialization"); + Require(sceBluetoothHidUnregisterCallback() == notInitialized, "callback removal before initialization"); Require(sceBluetoothHidInit() == 0, "initialization failed"); - std::uint8_t param[16]{}; - Require(sceBluetoothHidRegisterCallback(reinterpret_cast(&Callback), 0, param) == 0, "callback registration failed"); + Require(sceBluetoothHidInit() == 0, "repeated initialization failed"); + Require(sceBluetoothHidRegisterDevice(1, 2) == noCallback, "registration without callback"); + Require(sceBluetoothHidUnregisterCallback() == noCallback, "removing absent callback"); + BluetoothHidParam param{}; + Require(sceBluetoothHidRegisterCallback(&Callback, &callbackCalls, ¶m) == 0, "callback registration failed"); Require(sceBluetoothHidRegisterDevice(0x44f, 0x1000) == 0, "device registration failed"); Require(callbackCalls == 0, "a device connection was reported"); + Require(sceBluetoothHidRegisterCallback(nullptr, nullptr, ¶m) == invalidArgument, "null callback validation"); + param.reserved = 1; + Require(sceBluetoothHidRegisterCallback(&Callback, nullptr, ¶m) == invalidArgument, "reserved field validation"); + param.reserved = 0; + BluetoothHidThreadParam threadParam{}; + threadParam.reserved = 1; + param.threadParam = &threadParam; + Require(sceBluetoothHidRegisterCallback(&Callback, nullptr, ¶m) == invalidArgument, "thread reserved field validation"); + Require(sceBluetoothHidRegisterCallback(&Callback, nullptr, nullptr) == 0, "optional parameters rejected"); bool threw = false; - try { - sceBluetoothHidRegisterCallback(nullptr, 0, param); - } catch (const std::invalid_argument&) { - threw = true; - } - Require(threw, "a null callback was accepted"); threw = false; try { - sceBluetoothHidGetInputReport(); + sceBluetoothHidGetInputReport(0, 0); } catch (const std::runtime_error&) { threw = true; } Require(threw, "an input report without a device did not throw"); + Require(sceBluetoothHidDebugGetVersion() == 0x1e, "debug get version did not return the firmware version"); + threw = false; + try { + sceBluetoothHidGetDeviceInfo(0, nullptr); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "get device info did not throw"); + threw = false; + try { + sceBluetoothHidDisconnectDevice(0); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "disconnect device did not throw"); + threw = false; + try { + sceBluetoothHidGetFeatureReport(0, 0); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "get feature report did not throw"); + threw = false; + try { + sceBluetoothHidSetFeatureReport(0, 0, nullptr, 0); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "set feature report did not throw"); Require(sceBluetoothHidUnregisterDevice(0x44f, 0x1000) == 0, "device unregistration failed"); Require(sceBluetoothHidUnregisterCallback() == 0, "callback unregistration failed"); std::puts("BluetoothHid tests passed"); diff --git a/core/libs/tests/GuestBoundsChecking.cpp b/core/libs/tests/GuestBoundsChecking.cpp new file mode 100644 index 0000000000..4021e99e48 --- /dev/null +++ b/core/libs/tests/GuestBoundsChecking.cpp @@ -0,0 +1,242 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +using GuestContextCompare = int (APS5_VABI*)(const void*, const void*, void*); + +extern "C" { +void* APS5_VABI set_constraint_handler_s_nid_postfix(void*); +void* APS5_VABI bsearch_s_nid_postfix(const void*, const void*, std::size_t, std::size_t, GuestContextCompare, void*); +std::uint16_t* APS5_VABI wcstok_s_nid_postfix(std::uint16_t*, std::size_t*, const std::uint16_t*, std::uint16_t**); +int APS5_VABI wctomb_s_nid_postfix(int*, char*, std::size_t, std::uint16_t); +int APS5_VABI wcrtomb_s_nid_postfix(std::size_t*, char*, std::size_t, std::uint16_t, void*); +int APS5_VABI mbstowcs_s_nid_postfix(std::size_t*, std::uint16_t*, std::size_t, const char*, std::size_t); +int APS5_VABI mbsrtowcs_s_nid_postfix(std::size_t*, std::uint16_t*, std::size_t, const char**, std::size_t, void*); +char* APS5_VABI gets_s_nid_postfix(char*, std::size_t); +FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +int APS5_VABI fclose_nid_postfix(FileStream*); +extern FileStream* __stdinp_nid_postfix; +} + +namespace { + +constexpr std::size_t failed = static_cast(-1); +constexpr std::size_t tooLarge = std::size_t{1} << 61; + +int constraintCalls = 0; +int constraintError = 0; +std::string constraintMessage; + +void APS5_VABI RecordConstraint(const char* message, void* pointer, int error) { + ++constraintCalls; + constraintMessage = message; + constraintError = error; + if (pointer != nullptr) { + std::fprintf(stderr, "constraint handler received a non-null pointer\n"); + std::abort(); + } +} + +void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +void RequireViolation(int before, const char* message, int error) { + if (constraintCalls != before + 1 || constraintMessage != message || constraintError != error) { + std::fprintf(stderr, "expected one \"%s\" violation, got %d calls (last \"%s\")\n", message, constraintCalls - before, constraintMessage.c_str()); + std::abort(); + } +} + +int APS5_VABI CompareInts(const void* left, const void* right, void* context) { + ++*static_cast(context); + const int a = *static_cast(left); + const int b = *static_cast(right); + return (a > b) - (a < b); +} + +std::basic_string Wide(const char* text) { + std::basic_string wide; + while (*text) wide.push_back(static_cast(*text++)); + return wide; +} + +void CheckBsearch() { + const int values[] = {1, 3, 3, 3, 5, 7, 9}; + int calls = 0; + const int key = 3; + Require(bsearch_s_nid_postfix(&key, values, 7, sizeof(int), CompareInts, &calls) == &values[3] && calls == 1, + "bsearch_s did not probe the middle element first"); + const int missing = 4; + calls = 0; + Require(bsearch_s_nid_postfix(&missing, values, 7, sizeof(int), CompareInts, &calls) == nullptr && calls == 3, + "bsearch_s found a missing key"); + const int last = 9; + Require(bsearch_s_nid_postfix(&last, values, 7, sizeof(int), CompareInts, &calls) == &values[6], "bsearch_s missed the last element"); + int before = constraintCalls; + Require(bsearch_s_nid_postfix(nullptr, nullptr, 0, sizeof(int), nullptr, nullptr) == nullptr && constraintCalls == before, + "bsearch_s reported an empty array"); + Require(bsearch_s_nid_postfix(nullptr, values, 7, sizeof(int), CompareInts, &calls) == nullptr, "bsearch_s accepted a null key"); + RequireViolation(before, "bsearch_s: bad argument", 22); + before = constraintCalls; + Require(bsearch_s_nid_postfix(&key, values, 0, tooLarge, CompareInts, &calls) == nullptr, "bsearch_s accepted an oversized element"); + RequireViolation(before, "bsearch_s: bad argument", 22); +} + +void CheckWcstok() { + auto text = Wide(",,ab;c,,;"); + const auto delimiters = Wide(",;"); + std::size_t remaining = text.size() + 1; + std::uint16_t* context = nullptr; + std::uint16_t* token = wcstok_s_nid_postfix(text.data(), &remaining, delimiters.c_str(), &context); + Require(token == text.data() + 2 && token[0] == 'a' && token[1] == 'b' && token[2] == 0 && remaining == 5 && context == text.data() + 5, + "wcstok_s did not return the first token"); + token = wcstok_s_nid_postfix(nullptr, &remaining, delimiters.c_str(), &context); + Require(token == text.data() + 5 && token[1] == 0 && remaining == 3 && context == text.data() + 7, "wcstok_s did not return the second token"); + token = wcstok_s_nid_postfix(nullptr, &remaining, delimiters.c_str(), &context); + Require(token == nullptr && remaining == 1 && context == text.data() + 9, "wcstok_s did not stop at the end of the string"); + + int before = constraintCalls; + auto unterminated = Wide("abc"); + remaining = 2; + Require(wcstok_s_nid_postfix(unterminated.data(), &remaining, delimiters.c_str(), &context) == nullptr, "wcstok_s read past s1max"); + RequireViolation(before, "wcstok_s: missing delimiter", 22); + before = constraintCalls; + auto separators = Wide(",,,x"); + remaining = 3; + Require(wcstok_s_nid_postfix(separators.data(), &remaining, delimiters.c_str(), &context) == nullptr, "wcstok_s skipped past s1max"); + RequireViolation(before, "wcstok_s: missing token", 22); + before = constraintCalls; + remaining = 0; + Require(wcstok_s_nid_postfix(separators.data(), &remaining, delimiters.c_str(), &context) == nullptr, "wcstok_s accepted a zero s1max"); + RequireViolation(before, "wcstok_s: missing token", 22); + before = constraintCalls; + context = nullptr; + remaining = 4; + Require(wcstok_s_nid_postfix(nullptr, &remaining, delimiters.c_str(), &context) == nullptr, "wcstok_s accepted a null context string"); + RequireViolation(before, "wcstok_s: bad argument", 22); +} + +void CheckWideToMultibyte() { + int status = 7; + char bytes[4] = {'x', 'x', 'x', 'x'}; + Require(wctomb_s_nid_postfix(&status, bytes, sizeof(bytes), 'A') == 0 && status == 1 && bytes[0] == 'A', "wctomb_s did not convert"); + Require(wctomb_s_nid_postfix(&status, bytes, sizeof(bytes), 0x263a) == 86 && status == -1, "wctomb_s converted an unrepresentable character"); + Require(wctomb_s_nid_postfix(&status, nullptr, 0, 'A') == 0 && status == 0, "wctomb_s reported state dependence"); + status = 7; + int before = constraintCalls; + Require(wctomb_s_nid_postfix(&status, bytes, 0, 'A') == 34 && status == 7, "wctomb_s wrote into a zero-size buffer"); + RequireViolation(before, "wctomb_s: buffer too small", 34); + before = constraintCalls; + Require(wctomb_s_nid_postfix(nullptr, bytes, sizeof(bytes), 'A') == 22, "wctomb_s accepted a null status"); + RequireViolation(before, "wctomb_s: bad argument", 22); + + unsigned char state[128]{}; + std::size_t result = 7; + Require(wcrtomb_s_nid_postfix(&result, bytes, sizeof(bytes), 'B', state) == 0 && result == 1 && bytes[0] == 'B', "wcrtomb_s did not convert"); + Require(wcrtomb_s_nid_postfix(&result, nullptr, 0, 'B', state) == 0 && result == 1, "wcrtomb_s did not measure the reset sequence"); + Require(wcrtomb_s_nid_postfix(&result, bytes, sizeof(bytes), 0x263a, state) == 86 && result == failed, "wcrtomb_s converted an unrepresentable character"); + before = constraintCalls; + bytes[0] = 'x'; + Require(wcrtomb_s_nid_postfix(&result, bytes, sizeof(bytes), 'B', nullptr) == 22 && result == failed && bytes[0] == 0, "wcrtomb_s accepted a null state"); + RequireViolation(before, "wcrtomb_s: bad argument", 22); + before = constraintCalls; + bytes[0] = 'x'; + Require(wcrtomb_s_nid_postfix(&result, bytes, 0, 'B', state) == 22 && result == failed && bytes[0] == 'x', "wcrtomb_s accepted a zero-size buffer"); + RequireViolation(before, "wcrtomb_s: bad argument", 22); +} + +void CheckMultibyteToWide() { + std::uint16_t wide[8]; + std::size_t result = 0; + Require(mbstowcs_s_nid_postfix(&result, nullptr, 0, "hello", 0) == 0 && result == 5, "mbstowcs_s did not count the characters"); + Require(mbstowcs_s_nid_postfix(&result, wide, 8, "hello", 8) == 0 && result == 5 && wide[0] == 'h' && wide[4] == 'o' && wide[5] == 0, + "mbstowcs_s did not convert the string"); + Require(mbstowcs_s_nid_postfix(&result, wide, 8, "hello", 3) == 0 && result == 3 && wide[2] == 'l' && wide[3] == 0, + "mbstowcs_s did not stop at len"); + wide[0] = 'x'; + Require(mbstowcs_s_nid_postfix(&result, wide, 8, "hello", 0) == 0 && result == 0 && wide[0] == 0, "mbstowcs_s did not handle a zero len"); + int before = constraintCalls; + wide[0] = 'x'; + Require(mbstowcs_s_nid_postfix(&result, wide, 5, "hello", 8) == 34 && result == failed && wide[0] == 0, "mbstowcs_s overflowed the buffer"); + RequireViolation(before, "mbstowcs_s: buffer overflow", 34); + before = constraintCalls; + wide[0] = 'x'; + Require(mbstowcs_s_nid_postfix(&result, wide, 8, nullptr, 8) == 22 && result == failed && wide[0] == 0, "mbstowcs_s accepted a null source"); + RequireViolation(before, "mbstowcs_s: bad argument", 22); + + unsigned char state[128]{}; + const char* source = "world"; + Require(mbsrtowcs_s_nid_postfix(&result, nullptr, 0, &source, 0, state) == 0 && result == 5 && std::strcmp(source, "world") == 0, + "mbsrtowcs_s did not count the characters"); + Require(mbsrtowcs_s_nid_postfix(&result, wide, 8, &source, 8, state) == 0 && result == 5 && wide[4] == 'd' && wide[5] == 0 && source == nullptr, + "mbsrtowcs_s did not convert the string"); + const char* text = "world"; + source = text; + Require(mbsrtowcs_s_nid_postfix(&result, wide, 8, &source, 2, state) == 0 && result == 2 && wide[2] == 0 && source == text + 2, + "mbsrtowcs_s did not stop at len"); + before = constraintCalls; + source = text; + Require(mbsrtowcs_s_nid_postfix(&result, wide, 3, &source, 8, state) == 34 && result == failed && wide[0] == 0 && source == text + 3, + "mbsrtowcs_s overflowed the buffer"); + RequireViolation(before, "mbsrtowcs_s: buffer too small", 34); + before = constraintCalls; + source = nullptr; + Require(mbsrtowcs_s_nid_postfix(&result, wide, 8, &source, 8, state) == 22 && result == failed, "mbsrtowcs_s accepted a null source string"); + RequireViolation(before, "mbsrtowcs_s: bad argument", 22); + before = constraintCalls; + source = text; + Require(mbsrtowcs_s_nid_postfix(&result, wide, tooLarge, &source, 8, state) == 22 && result == failed, "mbsrtowcs_s accepted an oversized buffer"); + RequireViolation(before, "mbsrtowcs_s: bad argument", 22); +} + +void CheckGets(const char* path) { + { + std::ofstream input(path, std::ios::binary | std::ios::trunc); + input << "short\nexactly8\nrest"; + Require(input.good(), "could not write the gets_s input"); + } + FileStream* const saved = __stdinp_nid_postfix; + FileStream* const stream = fopen_nid_postfix(path, "r"); + Require(stream != nullptr, "could not open the gets_s input"); + __stdinp_nid_postfix = stream; + char buffer[8]; + Require(gets_s_nid_postfix(buffer, sizeof(buffer)) == buffer && std::strcmp(buffer, "short") == 0, "gets_s did not read a line"); + int before = constraintCalls; + Require(gets_s_nid_postfix(buffer, sizeof(buffer)) == nullptr && buffer[0] == 0, "gets_s accepted a line longer than the buffer"); + RequireViolation(before, "gets_s: short buffer", 34); + Require(gets_s_nid_postfix(buffer, sizeof(buffer)) == buffer && std::strcmp(buffer, "rest") == 0, "gets_s did not discard the rest of a long line"); + buffer[0] = 'x'; + Require(gets_s_nid_postfix(buffer, sizeof(buffer)) == nullptr && buffer[0] == 0, "gets_s did not report end of file"); + before = constraintCalls; + Require(gets_s_nid_postfix(buffer, 0) == nullptr, "gets_s accepted a zero size"); + RequireViolation(before, "gets_s: zero buffer size", 22); + before = constraintCalls; + Require(gets_s_nid_postfix(nullptr, sizeof(buffer)) == nullptr, "gets_s accepted a null buffer"); + RequireViolation(before, "gets_s: bad argument", 22); + __stdinp_nid_postfix = saved; + fclose_nid_postfix(stream); + std::remove(path); +} + +} // namespace + +int main(int argc, char** argv) { + Require(argc > 1, "usage: guest_bounds_checking_tests "); + set_constraint_handler_s_nid_postfix(reinterpret_cast(RecordConstraint)); + CheckBsearch(); + CheckWcstok(); + CheckWideToMultibyte(); + CheckMultibyteToWide(); + CheckGets(argv[1]); + return 0; +} diff --git a/core/libs/tests/GuestCallTrace.cpp b/core/libs/tests/GuestCallTrace.cpp index 4be35302dc..6fc77ae658 100644 --- a/core/libs/tests/GuestCallTrace.cpp +++ b/core/libs/tests/GuestCallTrace.cpp @@ -1,6 +1,13 @@ #include "prx/libc/include/CallTrace.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#ifdef _WIN32 #include +#else +#include +#include +#endif +#include +#include #include #include #include @@ -49,18 +56,65 @@ static Block APS5_VABI Blocks(std::uint64_t seed) { return {{seed, seed + 1, seed + 2, seed + 3}}; } +static std::uint64_t CurrentThread() { +#ifdef _WIN32 + return GetCurrentThreadId(); +#else + return static_cast(gettid()); +#endif +} + +static const void* ImageOf(const void* address) { +#ifdef _WIN32 + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(address, &memory, sizeof(memory)) != sizeof(memory)) return nullptr; + return memory.AllocationBase; +#else + Dl_info info{}; + if (dladdr(address, &info) == 0) return nullptr; + return info.dli_fbase; +#endif +} + template static TFunction* Traced(TFunction* function, const char* name, const char* library) { return reinterpret_cast(TraceImport_nid_no_patch(reinterpret_cast(function), name, library)); } int main(int argc, char** argv) { +#ifdef _WIN32 Require(_putenv_s("APS5_TRACE_CALLS", "libTraced,libOther.prx") == 0); - if (argc > 1 && std::strcmp(argv[1], "abort") == 0) { +#else + Require(setenv("APS5_TRACE_CALLS", "libTraced,libOther.prx", 1) == 0); +#endif + if (argc > 1 && (std::strcmp(argv[1], "abort") == 0 || std::strcmp(argv[1], "abort-race") == 0)) { +#ifndef _WIN32 + Require(std::signal(SIGABRT, [](int) { _exit(3); }) != SIG_ERR); +#endif const auto first = Traced(&Integers, "Integers", "libTraced.prx"); const auto second = Traced(&Blocks, "Blocks", "libOther.prx"); Require(first(1, 2, 3, 4, 5, 6, 7, 8) == 204 && second(7).words[3] == 10); +#ifdef _WIN32 _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); +#else + std::atomic locked{false}; + std::thread([&locked] { + flockfile(stderr); + locked.store(true); + for (;;) pause(); + }).detach(); + while (!locked.load()) std::this_thread::yield(); + if (std::strcmp(argv[1], "abort-race") == 0) { + for (int call = 0; call < 100; ++call) Require(first(1, 2, 3, 4, 5, 6, 7, 8) == 204); + static std::atomic go{false}; + std::thread([] { + while (!go.load()) { + } + std::abort(); + }).detach(); + go.store(true); + } +#endif std::abort(); } CallTraceEntry entries[16]; @@ -90,15 +144,13 @@ int main(int argc, char** argv) { const char* const names[] = {"Integers", "Doubles", "Mixed", "Variadic", "Blocks"}; const char* const libraries[] = {"libTraced.prx", "libTraced", "libOther.prx", "libOther", "libTraced.prx"}; - MEMORY_BASIC_INFORMATION image{}; - Require(VirtualQuery(reinterpret_cast(&main), &image, sizeof(image)) == sizeof(image)); + const void* image = ImageOf(reinterpret_cast(&main)); + Require(image != nullptr); Require(CopyCallTrace_nid_no_patch(entries, 16) == 5); for (std::uint64_t index = 0; index < 5; ++index) { - Require(entries[index].sequence == index + 1 && entries[index].thread == GetCurrentThreadId()); + Require(entries[index].sequence == index + 1 && entries[index].thread == CurrentThread()); Require(std::strcmp(entries[index].name, names[index]) == 0 && std::strcmp(entries[index].library, libraries[index]) == 0); - MEMORY_BASIC_INFORMATION caller{}; - Require(VirtualQuery(reinterpret_cast(entries[index].returnAddress), &caller, sizeof(caller)) == sizeof(caller)); - Require(caller.AllocationBase == image.AllocationBase); + Require(ImageOf(reinterpret_cast(entries[index].returnAddress)) == image); } Require(CopyCallTrace_nid_no_patch(entries, 2) == 2 && entries[0].sequence == 4 && entries[1].sequence == 5); Require(CopyCallCounts_nid_no_patch(counts, 8, &total) == 5 && total == 5); @@ -109,12 +161,20 @@ int main(int argc, char** argv) { Require(CopyCallCounts_nid_no_patch(counts, 2, nullptr) == 2); Require(counts[0].calls == 3 && std::strcmp(counts[0].name, "Blocks") == 0 && counts[1].calls == 2 && std::strcmp(counts[1].name, "Doubles") == 0); Require(CopyCallCounts_nid_no_patch(nullptr, 8, &total) == 0 && total == 8); + Require(CopyThreadCallTrace_nid_no_patch(CurrentThread(), entries, 16) == 8); + for (std::uint64_t index = 0; index < 8; ++index) Require(entries[index].sequence == index + 1 && entries[index].thread == CurrentThread()); + Require(CopyThreadCallTrace_nid_no_patch(CurrentThread(), entries, 2) == 2 && entries[0].sequence == 7 && entries[1].sequence == 8); + Require(std::strcmp(entries[1].name, "Blocks") == 0); + Require(CopyThreadCallTrace_nid_no_patch(CurrentThread() + 1, entries, 16) == 0); + Require(CopyThreadCallTrace_nid_no_patch(CurrentThread(), nullptr, 16) == 0 && CopyThreadCallTrace_nid_no_patch(CurrentThread(), entries, 0) == 0); constexpr std::uint64_t Workers = 4; constexpr std::uint64_t Calls = 20000; std::vector workers; + std::uint64_t workerThreads[Workers]{}; for (std::uint64_t worker = 0; worker < Workers; ++worker) { - workers.emplace_back([integers, worker] { + workers.emplace_back([integers, worker, &workerThreads] { + workerThreads[worker] = CurrentThread(); for (std::uint64_t call = 0; call < Calls; ++call) Require(integers(worker, call, 1, 2, 3, 4, 5, 6) == Integers(worker, call, 1, 2, 3, 4, 5, 6)); }); } @@ -124,6 +184,19 @@ int main(int argc, char** argv) { Require(CopyCallTrace_nid_no_patch(recent.get(), Window) == Window); Require(recent[Window - 1].sequence == 8 + Workers * Calls && recent[0].sequence == 8 + Workers * Calls - Window + 1); for (std::size_t index = 0; index < Window; ++index) Require(std::strcmp(recent[index].name, "Integers") == 0); + Require(CopyThreadCallTrace_nid_no_patch(CurrentThread(), entries, 16) == 0); + const auto own = std::make_unique(Window); + std::size_t owned = 0; + for (const std::uint64_t thread : workerThreads) { + std::size_t expected = 0; + for (std::size_t index = 0; index < Window; ++index) expected += recent[index].thread == thread; + const std::size_t count = CopyThreadCallTrace_nid_no_patch(thread, own.get(), Window); + Require(count == expected); + for (std::size_t index = 0; index < count; ++index) Require(own[index].thread == thread && (index == 0 || own[index - 1].sequence < own[index].sequence)); + if (count != 0) Require(CopyThreadCallTrace_nid_no_patch(thread, entries, 1) == 1 && entries[0].sequence == own[count - 1].sequence); + owned += count; + } + Require(owned == Window); Require(CopyCallCounts_nid_no_patch(counts, 1, &total) == 1 && total == 8 + Workers * Calls); Require(counts[0].calls == 1 + Workers * Calls && std::strcmp(counts[0].name, "Integers") == 0); } diff --git a/core/libs/tests/GuestCes.cpp b/core/libs/tests/GuestCes.cpp index b37607ad73..de0903e090 100644 --- a/core/libs/tests/GuestCes.cpp +++ b/core/libs/tests/GuestCes.cpp @@ -1,12 +1,52 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include +#include #include #include extern "C" { const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1252(void); const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1251(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1250(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_1(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_2(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_4(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_5(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_9(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_10(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_13(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_14(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_15(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileIso8859_16(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp437(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp737(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp775(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp850(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp852(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp855(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp860(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp861(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp862(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp863(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp865(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp866(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileKoi8R(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileKoi8U(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1254(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1256(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1258(void); int APS5_VABI sceCesMbcsUcsContextInit(void*, const void*); +void* APS5_VABI sceCesUcsProfileInitEucJpCp51932(void*); +int APS5_VABI sceCesUtf8StrToMbcsStr(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf8StrGetMbcsLen(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesMbcsStrToUtf8Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcsStrGetUtf8Len(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesUcs2StrToMbcsStr(void*, const std::uint16_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUcs2StrGetMbcsLen(void*, const std::uint16_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesMbcsStrToUcs2Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcsStrGetUcs2Len(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesEucJpToUcs2(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); int APS5_VABI sceCesSbcToUtf8(const std::uint8_t*, std::uint8_t, std::uint8_t*, std::uint32_t, std::uint32_t*); int APS5_VABI sceCesUtf8ToSbc(const std::uint8_t*, std::uint32_t, std::uint32_t*, const std::uint8_t*, std::uint8_t*); void* APS5_VABI sceCesUcsProfileInitSJis1997Cp932(void*); @@ -35,6 +75,175 @@ static int Utf8ToSbc(const std::uint8_t* profile, const char* utf8, std::uint32_ return sceCesUtf8ToSbc(reinterpret_cast(utf8), utf8max, &length, profile, &sbc); } +static void RequireSbcCode(const std::uint8_t* profile, std::uint8_t sbc, std::uint32_t code) { + char expected[4] = {}; + if (code < 0x80) { + expected[0] = static_cast(code); + } else if (code < 0x800) { + expected[0] = static_cast(0xC0 | (code >> 6)); + expected[1] = static_cast(0x80 | (code & 0x3F)); + } else { + expected[0] = static_cast(0xE0 | (code >> 12)); + expected[1] = static_cast(0x80 | ((code >> 6) & 0x3F)); + expected[2] = static_cast(0x80 | (code & 0x3F)); + } + RequireSbcToUtf8(profile, sbc, expected); +} + +static void CheckSingleByteProfiles() { + constexpr int unassignedCode = static_cast(0x805C0020); + const auto getters = {sceCesRefersUcsProfileIso8859_1, sceCesRefersUcsProfileIso8859_2, sceCesRefersUcsProfileIso8859_4, sceCesRefersUcsProfileIso8859_5, sceCesRefersUcsProfileIso8859_9, sceCesRefersUcsProfileIso8859_10, sceCesRefersUcsProfileIso8859_13, sceCesRefersUcsProfileIso8859_14, sceCesRefersUcsProfileIso8859_15, sceCesRefersUcsProfileIso8859_16, sceCesRefersUcsProfileCp437, sceCesRefersUcsProfileCp737, sceCesRefersUcsProfileCp775, sceCesRefersUcsProfileCp850, sceCesRefersUcsProfileCp852, sceCesRefersUcsProfileCp855, sceCesRefersUcsProfileCp860, sceCesRefersUcsProfileCp861, sceCesRefersUcsProfileCp862, sceCesRefersUcsProfileCp863, sceCesRefersUcsProfileCp865, sceCesRefersUcsProfileCp866, sceCesRefersUcsProfileKoi8R, sceCesRefersUcsProfileKoi8U, sceCesRefersUcsProfileCp1254, sceCesRefersUcsProfileCp1256, sceCesRefersUcsProfileCp1258}; + const std::uint8_t* existing[] = {sceCesRefersUcsProfileCp1252(), sceCesRefersUcsProfileCp1251(), sceCesRefersUcsProfileCp1250()}; + std::size_t index = 0; + for (const auto getter : getters) { + const std::uint8_t* profile = getter(); + Require(profile != nullptr && profile == getter()); + for (const std::uint8_t* other : existing) Require(profile != other); + std::size_t inner = 0; + for (const auto earlier : getters) { + if (inner++ == index) break; + Require(profile != earlier()); + } + ++index; + for (int sbc = 1; sbc < 256; ++sbc) { + std::uint8_t cp[4] = {}; + std::uint32_t cpLength = 0; + std::uint8_t back = 0; + Require(sceCesSbcToUtf8(profile, static_cast(sbc), cp, sizeof(cp), &cpLength) == 0); + if (sbc < 0x80) Require(cpLength == 1 && cp[0] == sbc); + Require(sceCesUtf8ToSbc(cp, cpLength, &cpLength, profile, &back) == 0 && back == sbc); + } + Require(Utf8ToSbc(profile, "\xE3\x81\x82", 3) == unassignedCode); + } + struct Check { + decltype(&sceCesRefersUcsProfileIso8859_1) profile; + std::uint8_t sbc; + std::uint32_t code; + }; + const Check checks[] = { + {sceCesRefersUcsProfileIso8859_1, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_1, 0xA4, 0x00A4}, + {sceCesRefersUcsProfileIso8859_1, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileIso8859_1, 0xFF, 0x00FF}, + {sceCesRefersUcsProfileIso8859_2, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_2, 0xA4, 0x00A4}, + {sceCesRefersUcsProfileIso8859_2, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileIso8859_2, 0xFF, 0x02D9}, + {sceCesRefersUcsProfileIso8859_4, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_4, 0xA4, 0x00A4}, + {sceCesRefersUcsProfileIso8859_4, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileIso8859_4, 0xFF, 0x02D9}, + {sceCesRefersUcsProfileIso8859_5, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_5, 0xA4, 0x0404}, + {sceCesRefersUcsProfileIso8859_5, 0xC1, 0x0421}, + {sceCesRefersUcsProfileIso8859_5, 0xFF, 0x045F}, + {sceCesRefersUcsProfileIso8859_9, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_9, 0xA4, 0x00A4}, + {sceCesRefersUcsProfileIso8859_9, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileIso8859_9, 0xFF, 0x00FF}, + {sceCesRefersUcsProfileIso8859_10, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_10, 0xA4, 0x012A}, + {sceCesRefersUcsProfileIso8859_10, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileIso8859_10, 0xFF, 0x0138}, + {sceCesRefersUcsProfileIso8859_13, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_13, 0xA4, 0x00A4}, + {sceCesRefersUcsProfileIso8859_13, 0xC1, 0x012E}, + {sceCesRefersUcsProfileIso8859_13, 0xFF, 0x2019}, + {sceCesRefersUcsProfileIso8859_14, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_14, 0xA4, 0x010A}, + {sceCesRefersUcsProfileIso8859_14, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileIso8859_14, 0xFF, 0x00FF}, + {sceCesRefersUcsProfileIso8859_15, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_15, 0xA4, 0x20AC}, + {sceCesRefersUcsProfileIso8859_15, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileIso8859_15, 0xFF, 0x00FF}, + {sceCesRefersUcsProfileIso8859_16, 0x80, 0x0080}, + {sceCesRefersUcsProfileIso8859_16, 0xA4, 0x20AC}, + {sceCesRefersUcsProfileIso8859_16, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileIso8859_16, 0xFF, 0x00FF}, + {sceCesRefersUcsProfileCp437, 0x80, 0x00C7}, + {sceCesRefersUcsProfileCp437, 0xA4, 0x00F1}, + {sceCesRefersUcsProfileCp437, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp437, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp737, 0x80, 0x0391}, + {sceCesRefersUcsProfileCp737, 0xA4, 0x03BD}, + {sceCesRefersUcsProfileCp737, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp737, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp775, 0x80, 0x0106}, + {sceCesRefersUcsProfileCp775, 0xA4, 0x017C}, + {sceCesRefersUcsProfileCp775, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp775, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp850, 0x80, 0x00C7}, + {sceCesRefersUcsProfileCp850, 0xA4, 0x00F1}, + {sceCesRefersUcsProfileCp850, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp850, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp852, 0x80, 0x00C7}, + {sceCesRefersUcsProfileCp852, 0xA4, 0x0104}, + {sceCesRefersUcsProfileCp852, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp852, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp855, 0x80, 0x0452}, + {sceCesRefersUcsProfileCp855, 0xA4, 0x0446}, + {sceCesRefersUcsProfileCp855, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp855, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp860, 0x80, 0x00C7}, + {sceCesRefersUcsProfileCp860, 0xA4, 0x00F1}, + {sceCesRefersUcsProfileCp860, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp860, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp861, 0x80, 0x00C7}, + {sceCesRefersUcsProfileCp861, 0xA4, 0x00C1}, + {sceCesRefersUcsProfileCp861, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp861, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp862, 0x80, 0x05D0}, + {sceCesRefersUcsProfileCp862, 0xA4, 0x00F1}, + {sceCesRefersUcsProfileCp862, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp862, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp863, 0x80, 0x00C7}, + {sceCesRefersUcsProfileCp863, 0xA4, 0x00A8}, + {sceCesRefersUcsProfileCp863, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp863, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp865, 0x80, 0x00C7}, + {sceCesRefersUcsProfileCp865, 0xA4, 0x00F1}, + {sceCesRefersUcsProfileCp865, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp865, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileCp866, 0x80, 0x0410}, + {sceCesRefersUcsProfileCp866, 0xA4, 0x0434}, + {sceCesRefersUcsProfileCp866, 0xC1, 0x2534}, + {sceCesRefersUcsProfileCp866, 0xFF, 0x00A0}, + {sceCesRefersUcsProfileKoi8R, 0x80, 0x2500}, + {sceCesRefersUcsProfileKoi8R, 0xA4, 0x2553}, + {sceCesRefersUcsProfileKoi8R, 0xC1, 0x0430}, + {sceCesRefersUcsProfileKoi8R, 0xFF, 0x042A}, + {sceCesRefersUcsProfileKoi8U, 0x80, 0x2500}, + {sceCesRefersUcsProfileKoi8U, 0xA4, 0x0454}, + {sceCesRefersUcsProfileKoi8U, 0xC1, 0x0430}, + {sceCesRefersUcsProfileKoi8U, 0xFF, 0x042A}, + {sceCesRefersUcsProfileCp1254, 0x80, 0x20AC}, + {sceCesRefersUcsProfileCp1254, 0xA4, 0x00A4}, + {sceCesRefersUcsProfileCp1254, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileCp1254, 0xFF, 0x00FF}, + {sceCesRefersUcsProfileCp1256, 0x80, 0x20AC}, + {sceCesRefersUcsProfileCp1256, 0xA4, 0x00A4}, + {sceCesRefersUcsProfileCp1256, 0xC1, 0x0621}, + {sceCesRefersUcsProfileCp1256, 0xFF, 0x06D2}, + {sceCesRefersUcsProfileCp1258, 0x80, 0x20AC}, + {sceCesRefersUcsProfileCp1258, 0xA4, 0x00A4}, + {sceCesRefersUcsProfileCp1258, 0xC1, 0x00C1}, + {sceCesRefersUcsProfileCp1258, 0xFF, 0x00FF}, + }; + for (const Check& check : checks) RequireSbcCode(check.profile(), check.sbc, check.code); + + char context[256] = {}; + Require(sceCesMbcsUcsContextInit(context, sceCesRefersUcsProfileKoi8R()) == 0); + std::uint8_t koi8[16] = {}; + std::uint32_t used = 0; + std::uint32_t produced = 0; + Require(sceCesUtf8StrToMbcsStr(context, reinterpret_cast("\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"), 12, &used, koi8, sizeof(koi8), &produced) == 0); + Require(used == 12 && produced == 6 && std::memcmp(koi8, "\xF0\xD2\xC9\xD7\xC5\xD4", 6) == 0); + std::uint8_t utf8[32] = {}; + Require(sceCesMbcsStrToUtf8Str(context, koi8, 6, &used, utf8, sizeof(utf8), &produced) == 0); + Require(used == 6 && produced == 12 && std::memcmp(utf8, "\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82", 12) == 0); +} + int main() { constexpr int invalidParameter = static_cast(0x805C0001); constexpr int invalidSrcBuffer = static_cast(0x805C0010); @@ -46,6 +255,7 @@ int main() { const std::uint8_t* profile = sceCesRefersUcsProfileCp1252(); Require(profile != nullptr && profile == sceCesRefersUcsProfileCp1252()); + CheckSingleByteProfiles(); RequireSbcToUtf8(profile, 'A', "A"); RequireSbcToUtf8(profile, 0x80, "\xE2\x82\xAC"); @@ -104,12 +314,212 @@ int main() { Require(Utf8ToSbc(cyrillic, "\xC3\xA9", 2) == unassignedCode); Require(Utf8ToSbc(profile, "\xD0\x90", 2) == unassignedCode); + const std::uint8_t* latin2 = sceCesRefersUcsProfileCp1250(); + Require(latin2 != nullptr && latin2 == sceCesRefersUcsProfileCp1250() && latin2 != profile && latin2 != cyrillic); + RequireSbcToUtf8(latin2, 'A', "A"); + RequireSbcToUtf8(latin2, 0x80, "\xE2\x82\xAC"); + RequireSbcToUtf8(latin2, 0x81, "\xC2\x81"); + RequireSbcToUtf8(latin2, 0x8A, "\xC5\xA0"); + RequireSbcToUtf8(latin2, 0xA3, "\xC5\x81"); + RequireSbcToUtf8(latin2, 0xA5, "\xC4\x84"); + RequireSbcToUtf8(latin2, 0xAF, "\xC5\xBB"); + RequireSbcToUtf8(latin2, 0xE9, "\xC3\xA9"); + RequireSbcToUtf8(latin2, 0xFF, "\xCB\x99"); + RequireUtf8ToSbc(latin2, "\xC5\xA0", 0x8A); + RequireUtf8ToSbc(latin2, "\xC5\x81", 0xA3); + RequireUtf8ToSbc(latin2, "\xC3\xA9", 0xE9); + for (int sbc = 1; sbc < 256; ++sbc) { + std::uint8_t cp[4] = {}; + std::uint32_t cpLength = 0; + std::uint8_t back = 0; + Require(sceCesSbcToUtf8(latin2, static_cast(sbc), cp, sizeof(cp), &cpLength) == 0); + Require(sceCesUtf8ToSbc(cp, cpLength, &cpLength, latin2, &back) == 0 && back == sbc); + } + Require(Utf8ToSbc(latin2, "\xD0\x90", 2) == unassignedCode); + char latin2Context[256] = {}; + Require(sceCesMbcsUcsContextInit(latin2Context, latin2) == 0); + std::uint8_t latin2Back[16] = {}; + std::uint32_t latin2Used = 0; + std::uint32_t latin2Produced = 0; + Require(sceCesMbcsStrToUtf8Str(latin2Context, reinterpret_cast("\x80\xA3\xFF"), 3, &latin2Used, latin2Back, sizeof(latin2Back), &latin2Produced) == 0); + Require(latin2Used == 3 && latin2Produced == 7 && std::memcmp(latin2Back, "\xE2\x82\xAC\xC5\x81\xCB\x99", 7) == 0); + Require(sceCesMbcsStrGetUtf8Len(latin2Context, reinterpret_cast("\x80\xA3\xFF"), 3, &latin2Used, &latin2Produced) == 0 && latin2Produced == 7); + char context[256] = {}; Require(sceCesMbcsUcsContextInit(context, cyrillic) == 0); Require(sceCesMbcsUcsContextInit(nullptr, cyrillic) == invalidParameter); - char sheet[256] = {}; - Require(sceCesUcsProfileInitSJis1997Cp932(sheet) == sheet); + char sjisSheet[256] = {}; + Require(sceCesUcsProfileInitSJis1997Cp932(sjisSheet) == sjisSheet); Require(sceCesUcsProfileInitSJis1997Cp932(nullptr) == nullptr); - Require(sceCesMbcsUcsContextInit(context, sheet) == 0); + char sjisContext[256] = {}; + Require(sceCesMbcsUcsContextInit(sjisContext, sjisSheet) == 0); + const auto* eucSource = reinterpret_cast("Go \xEF\xBD\xB1!"); + std::uint8_t mbcs[16] = {}; + std::uint32_t used = 0; + std::uint32_t produced = 0; + char sheet[1024] = {}; + Require(sceCesUcsProfileInitEucJpCp51932(nullptr) == nullptr); + void* eucJp = sceCesUcsProfileInitEucJpCp51932(sheet); + Require(eucJp != nullptr); + char eucContext[256] = {}; + Require(sceCesMbcsUcsContextInit(eucContext, eucJp) == 0); + Require(sceCesUtf8StrToMbcsStr(eucContext, eucSource, 16, &used, mbcs, sizeof(mbcs), &produced) == 0); + Require(used == 7 && produced == 6 && std::memcmp(mbcs, "Go \x8E\xB1!", 6) == 0); + Require(sceCesUtf8StrGetMbcsLen(eucContext, eucSource, 16, &used, &produced) == 0 && used == 7 && produced == 6); + Require(sceCesUtf8StrToMbcsStr(eucContext, eucSource, 16, &used, mbcs, 4, &produced) == dstBufferEnd); + Require(sceCesUtf8StrToMbcsStr(nullptr, eucSource, 16, &used, mbcs, sizeof(mbcs), &produced) == invalidParameter); + Require(sceCesUtf8StrToMbcsStr(eucContext, reinterpret_cast("\xF0\x9F\x98\x80"), 4, &used, mbcs, sizeof(mbcs), &produced) == unassignedCode); + + std::uint8_t back[16] = {}; + Require(sceCesMbcsStrToUtf8Str(nullptr, mbcs, 6, &used, back, sizeof(back), &produced) == invalidParameter); + Require(sceCesMbcsStrToUtf8Str(eucContext, mbcs, 6, &used, nullptr, sizeof(back), &produced) == invalidDstBuffer); + Require(sceCesMbcsStrToUtf8Str(eucContext, mbcs, 6, &used, back, sizeof(back), &produced) == 0); + Require(used == 6 && produced == 7 && std::memcmp(back, eucSource, 7) == 0); + Require(sceCesMbcsStrGetUtf8Len(eucContext, mbcs, 6, &used, &produced) == 0 && produced == 7); + Require(sceCesMbcsStrToUtf8Str(eucContext, reinterpret_cast("\x8E"), 1, &used, back, sizeof(back), &produced) == srcBufferEnd); + Require(sceCesMbcsStrToUtf8Str(eucContext, reinterpret_cast("\x8E\x41"), 2, &used, back, sizeof(back), &produced) == invalidEncode); + + const std::uint16_t ucs2[] = {u'O', u'K', 0xFF9F, 0}; + Require(sceCesUcs2StrToMbcsStr(eucContext, ucs2, 4, &used, mbcs, sizeof(mbcs), &produced) == 0); + Require(used == 3 && produced == 4 && std::memcmp(mbcs, "OK\x8E\xDF", 4) == 0); + Require(sceCesUcs2StrGetMbcsLen(eucContext, ucs2, 4, &used, &produced) == 0 && produced == 4); + std::uint16_t wide[8] = {}; + Require(sceCesMbcsStrToUcs2Str(eucContext, mbcs, 4, &used, wide, 8, &produced) == 0); + Require(used == 4 && produced == 3 && wide[0] == u'O' && wide[1] == u'K' && wide[2] == 0xFF9F); + Require(sceCesMbcsStrGetUcs2Len(eucContext, mbcs, 4, &used, &produced) == 0 && produced == 3); + std::uint16_t single = 0; + Require(sceCesEucJpToUcs2(eucJp, reinterpret_cast("\x8E\xB1"), 2, &used, &single) == 0 && used == 2 && single == 0xFF71); + Require(sceCesEucJpToUcs2(cyrillic, reinterpret_cast("A"), 1, &used, &single) == invalidParameter); + + const auto* japanese = reinterpret_cast("\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1\xE3\x81\xAF\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E"); + Require(sceCesUtf8StrToMbcsStr(eucContext, japanese, 24, &used, mbcs, sizeof(mbcs), &produced) == 0); + Require(used == 24 && produced == 16 && std::memcmp(mbcs, "\xA4\xB3\xA4\xF3\xA4\xCB\xA4\xC1\xA4\xCF\xC6\xFC\xCB\xDC\xB8\xEC", 16) == 0); + std::uint8_t japaneseBack[32] = {}; + Require(sceCesMbcsStrToUtf8Str(eucContext, mbcs, 16, &used, japaneseBack, sizeof(japaneseBack), &produced) == 0 && produced == 24 && std::memcmp(japaneseBack, japanese, 24) == 0); + Require(sceCesMbcsStrToUcs2Str(eucContext, mbcs, 16, &used, wide, 8, &produced) == 0 && used == 16 && produced == 8 && wide[0] == 0x3053 && wide[5] == 0x65E5 && wide[7] == 0x8A9E); + Require(sceCesMbcsStrToUcs2Str(eucContext, mbcs, 15, &used, wide, 8, &produced) == srcBufferEnd); + const std::uint16_t extensions[] = {0x2460, 0xFF5E, 0x2252, 0}; + Require(sceCesUcs2StrToMbcsStr(eucContext, extensions, 4, &used, mbcs, sizeof(mbcs), &produced) == 0 && produced == 6 && std::memcmp(mbcs, "\xAD\xA1\xA1\xC1\xA2\xE2", 6) == 0); + Require(sceCesMbcsStrToUcs2Str(eucContext, reinterpret_cast("\xA9\xA1"), 2, &used, wide, 8, &produced) == unassignedCode); + Require(sceCesMbcsStrToUcs2Str(eucContext, reinterpret_cast("\xA4\x41"), 2, &used, wide, 8, &produced) == invalidEncode); + Require(sceCesMbcsStrToUcs2Str(eucContext, reinterpret_cast("\xA4"), 1, &used, wide, 8, &produced) == srcBufferEnd); + const std::uint16_t yen[] = {0x00A5, 0}; + Require(sceCesUcs2StrToMbcsStr(eucContext, yen, 2, &used, mbcs, sizeof(mbcs), &produced) == unassignedCode); + std::uint32_t assigned = 0; + for (std::uint32_t lead = 0xA1; lead <= 0xFE; ++lead) { + for (std::uint32_t trail = 0xA1; trail <= 0xFE; ++trail) { + const std::uint8_t cell[2] = {static_cast(lead), static_cast(trail)}; + std::uint16_t decoded[2] = {}; + if (sceCesEucJpToUcs2(eucJp, cell, 2, &used, decoded) != 0) continue; + ++assigned; + decoded[1] = 0; + std::uint8_t again[4] = {}; + std::uint16_t second[2] = {}; + Require(sceCesUcs2StrToMbcsStr(eucContext, decoded, 2, &used, again, sizeof(again), &produced) == 0 && produced == 2); + Require(sceCesEucJpToUcs2(eucJp, again, 2, &used, second) == 0 && second[0] == decoded[0]); + if (again[0] != cell[0] || again[1] != cell[1]) Require(again[0] < cell[0] || (again[0] == cell[0] && again[1] < cell[1])); + } + } + Require(assigned == 7336); + + std::uint16_t supplementary = 0; + Require(sceCesEucJpToUcs2(eucJp, reinterpret_cast("\x8F\xA2\xAF"), 3, &used, &supplementary) == 0 && used == 3 && supplementary == 0x02D8); + Require(sceCesEucJpToUcs2(eucJp, reinterpret_cast("\x8F\xA2"), 2, &used, &supplementary) == srcBufferEnd); + Require(sceCesEucJpToUcs2(eucJp, reinterpret_cast("\x8F\x41\xA1"), 3, &used, &supplementary) == invalidEncode); + Require(sceCesEucJpToUcs2(eucJp, reinterpret_cast("\x8F\xA2\x41"), 3, &used, &supplementary) == invalidEncode); + Require(sceCesEucJpToUcs2(eucJp, reinterpret_cast("\x8F\xA1\xA1"), 3, &used, &supplementary) == unassignedCode); + Require(sceCesEucJpToUcs2(eucJp, reinterpret_cast("\x8F\xF5\xA1"), 3, &used, &supplementary) == unassignedCode); + const std::uint16_t supplementaryText[] = {0x02D8, 0x0386, 0x0402, 0x6600, 0}; + Require(sceCesUcs2StrToMbcsStr(eucContext, supplementaryText, 5, &used, mbcs, sizeof(mbcs), &produced) == 0); + Require(used == 4 && produced == 11 && std::memcmp(mbcs, "\x8F\xA2\xAF\x8F\xA6\xE1\x8F\xA7\xC2\xFA\xB4", 11) == 0); + Require(sceCesUcs2StrGetMbcsLen(eucContext, supplementaryText, 5, &used, &produced) == 0 && produced == 11); + std::uint16_t supplementaryBack[8] = {}; + Require(sceCesMbcsStrToUcs2Str(eucContext, mbcs, 11, &used, supplementaryBack, 8, &produced) == 0); + Require(used == 11 && produced == 4 && supplementaryBack[0] == 0x02D8 && supplementaryBack[1] == 0x0386 && supplementaryBack[2] == 0x0402 && supplementaryBack[3] == 0x6600); + Require(sceCesMbcsStrGetUcs2Len(eucContext, mbcs, 11, &used, &produced) == 0 && produced == 4); + Require(sceCesMbcsStrToUtf8Str(eucContext, mbcs, 11, &used, japaneseBack, sizeof(japaneseBack), &produced) == 0); + Require(produced == 9 && std::memcmp(japaneseBack, "\xCB\x98\xCE\x86\xD0\x82\xE6\x98\x80", 9) == 0); + Require(sceCesMbcsStrGetUtf8Len(eucContext, mbcs, 11, &used, &produced) == 0 && produced == 9); + Require(sceCesMbcsStrToUcs2Str(eucContext, mbcs, 10, &used, supplementaryBack, 8, &produced) == srcBufferEnd); + std::uint32_t supplementaryAssigned = 0; + for (std::uint32_t lead = 0xA2; lead <= 0xF4; ++lead) { + for (std::uint32_t trail = 0xA1; trail <= 0xFE; ++trail) { + const std::uint8_t cell[3] = {0x8F, static_cast(lead), static_cast(trail)}; + std::uint16_t decoded[2] = {}; + if (sceCesEucJpToUcs2(eucJp, cell, 3, &used, decoded) != 0) continue; + ++supplementaryAssigned; + decoded[1] = 0; + std::uint8_t again[4] = {}; + std::uint16_t second[2] = {}; + Require(sceCesUcs2StrToMbcsStr(eucContext, decoded, 2, &used, again, sizeof(again), &produced) == 0); + if (produced == 3) Require(again[1] == cell[1] && again[2] == cell[2]); + Require(sceCesEucJpToUcs2(eucJp, again, produced, &used, second) == 0 && second[0] == decoded[0]); + } + } + Require(supplementaryAssigned == 6067); + + const auto* sjisSource = reinterpret_cast("Go \xEF\xBD\xB1!"); + Require(sceCesUtf8StrToMbcsStr(sjisContext, sjisSource, 16, &used, mbcs, sizeof(mbcs), &produced) == 0); + Require(used == 7 && produced == 5 && std::memcmp(mbcs, "Go \xB1!", 5) == 0); + Require(sceCesUtf8StrGetMbcsLen(sjisContext, sjisSource, 16, &used, &produced) == 0 && used == 7 && produced == 5); + Require(sceCesUtf8StrToMbcsStr(sjisContext, sjisSource, 16, &used, mbcs, 4, &produced) == dstBufferEnd); + Require(sceCesUtf8StrToMbcsStr(sjisContext, reinterpret_cast("\xF0\x9F\x98\x80"), 4, &used, mbcs, sizeof(mbcs), &produced) == unassignedCode); + + std::uint8_t sjisBack[16] = {}; + Require(sceCesMbcsStrToUtf8Str(sjisContext, mbcs, 5, &used, sjisBack, sizeof(sjisBack), &produced) == 0); + Require(used == 5 && produced == 7 && std::memcmp(sjisBack, sjisSource, 7) == 0); + Require(sceCesMbcsStrGetUtf8Len(sjisContext, mbcs, 5, &used, &produced) == 0 && produced == 7); + Require(sceCesMbcsStrToUtf8Str(sjisContext, reinterpret_cast("\x82"), 1, &used, sjisBack, sizeof(sjisBack), &produced) == srcBufferEnd); + Require(sceCesMbcsStrToUtf8Str(sjisContext, reinterpret_cast("\x82\x20"), 2, &used, sjisBack, sizeof(sjisBack), &produced) == invalidEncode); + Require(sceCesMbcsStrToUtf8Str(sjisContext, reinterpret_cast("\x82\x7F"), 2, &used, sjisBack, sizeof(sjisBack), &produced) == invalidEncode); + + const std::uint16_t sjisUcs2[] = {u'O', u'K', 0xFF9F, 0}; + Require(sceCesUcs2StrToMbcsStr(sjisContext, sjisUcs2, 4, &used, mbcs, sizeof(mbcs), &produced) == 0); + Require(used == 3 && produced == 3 && std::memcmp(mbcs, "OK\xDF", 3) == 0); + Require(sceCesUcs2StrGetMbcsLen(sjisContext, sjisUcs2, 4, &used, &produced) == 0 && produced == 3); + std::uint16_t sjisWide[8] = {}; + Require(sceCesMbcsStrToUcs2Str(sjisContext, mbcs, 3, &used, sjisWide, 8, &produced) == 0); + Require(used == 3 && produced == 3 && sjisWide[0] == u'O' && sjisWide[1] == u'K' && sjisWide[2] == 0xFF9F); + Require(sceCesMbcsStrGetUcs2Len(sjisContext, mbcs, 3, &used, &produced) == 0 && produced == 3); + + Require(sceCesUtf8StrToMbcsStr(sjisContext, japanese, 24, &used, mbcs, sizeof(mbcs), &produced) == 0); + Require(used == 24 && produced == 16 && std::memcmp(mbcs, "\x82\xB1\x82\xF1\x82\xC9\x82\xBF\x82\xCD\x93\xFA\x96\x7B\x8C\xEA", 16) == 0); + std::uint8_t sjisJapaneseBack[32] = {}; + Require(sceCesMbcsStrToUtf8Str(sjisContext, mbcs, 16, &used, sjisJapaneseBack, sizeof(sjisJapaneseBack), &produced) == 0 && produced == 24 && std::memcmp(sjisJapaneseBack, japanese, 24) == 0); + Require(sceCesMbcsStrToUcs2Str(sjisContext, mbcs, 16, &used, sjisWide, 8, &produced) == 0 && used == 16 && produced == 8 && sjisWide[0] == 0x3053 && sjisWide[5] == 0x65E5 && sjisWide[7] == 0x8A9E); + Require(sceCesMbcsStrToUcs2Str(sjisContext, mbcs, 15, &used, sjisWide, 8, &produced) == srcBufferEnd); + const std::uint16_t sjisExtensions[] = {0x2460, 0xFF5E, 0x2252, 0}; + Require(sceCesUcs2StrToMbcsStr(sjisContext, sjisExtensions, 4, &used, mbcs, sizeof(mbcs), &produced) == 0 && produced == 6 && std::memcmp(mbcs, "\x87\x40\x81\x60\x81\xE0", 6) == 0); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\x85\x40"), 2, &used, sjisWide, 8, &produced) == unassignedCode); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\x80"), 1, &used, sjisWide, 8, &produced) == invalidEncode); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xA0"), 1, &used, sjisWide, 8, &produced) == invalidEncode); + Require(sceCesUcs2StrToMbcsStr(sjisContext, yen, 2, &used, mbcs, sizeof(mbcs), &produced) == unassignedCode); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xFB\xFC"), 2, &used, sjisWide, 8, &produced) == 0 && used == 2 && produced == 1 && sjisWide[0] == 0x9AD9); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xFA\x40"), 2, &used, sjisWide, 8, &produced) == 0 && used == 2 && produced == 1 && sjisWide[0] == 0x2170); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xFC\x4B"), 2, &used, sjisWide, 8, &produced) == 0 && used == 2 && produced == 1 && sjisWide[0] == 0x9ED1); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xFC\x4C"), 2, &used, sjisWide, 8, &produced) == unassignedCode); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xF0\x40"), 2, &used, sjisWide, 8, &produced) == unassignedCode); + std::uint32_t sjisAssigned = 0; + for (std::uint32_t lead = 0x81; lead <= 0xFC; ++lead) { + if (lead > 0x9F && lead < 0xE0) continue; + for (std::uint32_t trail = 0x40; trail <= 0xFC; ++trail) { + if (trail == 0x7F) continue; + const std::uint8_t cell[2] = {static_cast(lead), static_cast(trail)}; + std::uint16_t decoded[2] = {}; + if (sceCesMbcsStrToUcs2Str(sjisContext, cell, 2, &used, decoded, 2, &produced) != 0) continue; + ++sjisAssigned; + decoded[1] = 0; + std::uint8_t again[4] = {}; + std::uint16_t second[2] = {}; + Require(sceCesUcs2StrToMbcsStr(sjisContext, decoded, 2, &used, again, sizeof(again), &produced) == 0 && produced == 2); + Require(sceCesMbcsStrToUcs2Str(sjisContext, again, 2, &used, second, 2, &produced) == 0 && second[0] == decoded[0]); + if (again[0] != cell[0] || again[1] != cell[1]) Require(again[0] < cell[0] || (again[0] == cell[0] && again[1] < cell[1])); + } + } + Require(sjisAssigned == 7724); + + Require(sceCesMbcsStrToUtf8Str(context, reinterpret_cast("\xC0\xFF"), 2, &used, back, sizeof(back), &produced) == 0); + Require(produced == 4 && std::memcmp(back, "\xD0\x90\xD1\x8F", 4) == 0); + Require(sceCesUtf8StrToMbcsStr(context, back, 4, &used, mbcs, sizeof(mbcs), &produced) == 0 && produced == 2 && mbcs[0] == 0xC0 && mbcs[1] == 0xFF); } diff --git a/core/libs/tests/GuestCesDbcs.cpp b/core/libs/tests/GuestCesDbcs.cpp new file mode 100644 index 0000000000..552f5193e9 --- /dev/null +++ b/core/libs/tests/GuestCesDbcs.cpp @@ -0,0 +1,179 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +void* APS5_VABI sceCesUcsProfileInitEucKr(void*); +void* APS5_VABI sceCesUcsProfileInitUhc(void*); +void* APS5_VABI sceCesUcsProfileInitBig5Cp950(void*); +void* APS5_VABI sceCesUcsProfileInitEucJpCp51932(void*); +int APS5_VABI sceCesEucKrToUtf8(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUhcToUtf8(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesBig5ToUtf8(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcsUcsContextInit(void*, const void*); +int APS5_VABI sceCesUtf8StrToMbcsStr(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcsStrToUtf8Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUcs2StrToMbcsStr(void*, const std::uint16_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcsStrToUcs2Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +} + +using ToUtf8 = int (APS5_VABI*)(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); + +constexpr int invalidParameter = static_cast(0x805C0001); +constexpr int invalidSrcBuffer = static_cast(0x805C0010); +constexpr int srcBufferEnd = static_cast(0x805C0011); +constexpr int invalidEncode = static_cast(0x805C0014); +constexpr int unassignedCode = static_cast(0x805C0020); +constexpr int invalidDstBuffer = static_cast(0x805C0030); +constexpr int dstBufferEnd = static_cast(0x805C0031); + +static void Require(bool value) { if (!value) std::abort(); } + +static const std::uint8_t* Bytes(const char* text) { return reinterpret_cast(text); } + +static std::uint32_t Size(const char* text) { return static_cast(std::strlen(text)); } + +static std::uint64_t Fnv(std::uint64_t hash, std::uint8_t byte) { return (hash ^ byte) * 0x100000001B3ull; } + +static void RequireTable(ToUtf8 convert, const void* profile, std::uint64_t expectedHash, std::uint32_t expectedCells) { + std::uint64_t hash = 0xCBF29CE484222325ull; + std::uint32_t cells = 0; + for (std::uint32_t lead = 0; lead < 256; ++lead) { + for (std::uint32_t trail = 0; trail < 256; ++trail) { + const std::uint8_t source[2] = {static_cast(lead), static_cast(trail)}; + std::uint8_t utf8[4] = {}; + std::uint32_t consumed = 0; + std::uint32_t produced = 0; + const int result = convert(profile, source, sizeof(source), &consumed, utf8, sizeof(utf8), &produced); + hash = Fnv(hash, static_cast(result & 0xFF)); + if (result != 0) continue; + hash = Fnv(hash, static_cast(consumed)); + for (std::uint32_t i = 0; i < produced; ++i) hash = Fnv(hash, utf8[i]); + if (consumed == 2) ++cells; + } + } + Require(hash == expectedHash && cells == expectedCells); +} + +static void RequireScalar(ToUtf8 convert, const void* profile, const char* source, const char* expected) { + std::uint8_t utf8[4] = {}; + std::uint32_t consumed = 0; + std::uint32_t produced = 0; + Require(convert(profile, Bytes(source), Size(source), &consumed, utf8, sizeof(utf8), &produced) == 0); + Require(consumed == Size(source) && produced == Size(expected) && std::memcmp(utf8, expected, produced) == 0); +} + +static int Scalar(ToUtf8 convert, const void* profile, const char* source, std::uint32_t sourceMax, std::uint32_t utf8Max) { + std::uint8_t utf8[4] = {0x55, 0x55, 0x55, 0x55}; + std::uint32_t consumed = 77; + std::uint32_t produced = 77; + const int result = convert(profile, Bytes(source), sourceMax, &consumed, utf8, utf8Max, &produced); + if (result != 0) Require(consumed == 77 && produced == 77 && std::memcmp(utf8, "\x55\x55\x55\x55", 4) == 0); + return result; +} + +static void RequireString(void* context, const char* mbcs, const char* utf8) { + std::uint8_t converted[32] = {}; + std::uint32_t read = 0; + std::uint32_t written = 0; + Require(sceCesMbcsStrToUtf8Str(context, Bytes(mbcs), Size(mbcs), &read, converted, sizeof(converted), &written) == 0); + Require(read == Size(mbcs) && written == Size(utf8) && std::memcmp(converted, utf8, written) == 0); + std::uint8_t back[32] = {}; + Require(sceCesUtf8StrToMbcsStr(context, Bytes(utf8), Size(utf8), &read, back, sizeof(back), &written) == 0); + Require(read == Size(utf8) && written == Size(mbcs) && std::memcmp(back, mbcs, written) == 0); +} + +static int Utf8ToMbcs(void* context, const char* utf8) { + std::uint8_t mbcs[8] = {}; + std::uint32_t read = 0; + std::uint32_t written = 0; + return sceCesUtf8StrToMbcsStr(context, Bytes(utf8), Size(utf8), &read, mbcs, sizeof(mbcs), &written); +} + +static std::uint32_t RoundTripMismatches(void* context) { + std::uint32_t mismatches = 0; + for (std::uint32_t lead = 0x81; lead < 0xFF; ++lead) { + for (std::uint32_t trail = 0x40; trail < 0xFF; ++trail) { + const std::uint8_t source[2] = {static_cast(lead), static_cast(trail)}; + std::uint16_t ucs2 = 0; + std::uint32_t read = 0; + std::uint32_t written = 0; + if (sceCesMbcsStrToUcs2Str(context, source, sizeof(source), &read, &ucs2, 1, &written) != 0) continue; + Require(read == 2 && written == 1); + std::uint8_t back[2] = {}; + Require(sceCesUcs2StrToMbcsStr(context, &ucs2, 1, &read, back, sizeof(back), &written) == 0); + Require(read == 1 && written == 2); + if (std::memcmp(source, back, sizeof(back)) != 0) ++mismatches; + } + } + return mismatches; +} + +int main() { + alignas(8) std::uint8_t eucKrSheet[64] = {}; + alignas(8) std::uint8_t uhcSheet[64] = {}; + alignas(8) std::uint8_t big5Sheet[64] = {}; + alignas(8) std::uint8_t eucJpSheet[64] = {}; + Require(sceCesUcsProfileInitEucKr(nullptr) == nullptr); + Require(sceCesUcsProfileInitUhc(nullptr) == nullptr); + Require(sceCesUcsProfileInitBig5Cp950(nullptr) == nullptr); + void* eucKr = sceCesUcsProfileInitEucKr(eucKrSheet); + void* uhc = sceCesUcsProfileInitUhc(uhcSheet); + void* big5 = sceCesUcsProfileInitBig5Cp950(big5Sheet); + void* eucJp = sceCesUcsProfileInitEucJpCp51932(eucJpSheet); + Require(eucKr == eucKrSheet && uhc == uhcSheet && big5 == big5Sheet && eucJp == eucJpSheet); + + RequireTable(sceCesEucKrToUtf8, eucKr, 0x01FF720E36F4AD67ull, 8226); + RequireTable(sceCesUhcToUtf8, uhc, 0x3B861A7E911985ABull, 17048); + RequireTable(sceCesBig5ToUtf8, big5, 0xE4D0D6A8019F35D9ull, 13503); + + RequireScalar(sceCesEucKrToUtf8, eucKr, "\xB0\xA1", "\xEA\xB0\x80"); + RequireScalar(sceCesEucKrToUtf8, eucKr, "\xA2\xE6", "\xE2\x82\xAC"); + RequireScalar(sceCesUhcToUtf8, uhc, "\xB0\xA1", "\xEA\xB0\x80"); + RequireScalar(sceCesUhcToUtf8, uhc, "\x8C\x63", "\xEB\x98\xA0"); + RequireScalar(sceCesBig5ToUtf8, big5, "\xA4\xA4", "\xE4\xB8\xAD"); + RequireScalar(sceCesBig5ToUtf8, big5, "A", "A"); + std::uint8_t zero[4] = {0x55, 0x55, 0x55, 0x55}; + Require(sceCesUhcToUtf8(uhc, Bytes("\0"), 1, nullptr, zero, sizeof(zero), nullptr) == 0 && zero[0] == 0 && zero[1] == 0x55); + + Require(Scalar(sceCesEucKrToUtf8, nullptr, "\xB0\xA1", 2, 4) == invalidParameter); + Require(Scalar(sceCesEucKrToUtf8, uhc, "\xB0\xA1", 2, 4) == invalidParameter); + Require(Scalar(sceCesUhcToUtf8, eucKr, "\xB0\xA1", 2, 4) == invalidParameter); + Require(Scalar(sceCesBig5ToUtf8, eucJp, "\xA4\xA4", 2, 4) == invalidParameter); + Require(Scalar(sceCesEucKrToUtf8, eucKr, nullptr, 2, 4) == invalidSrcBuffer); + Require(Scalar(sceCesEucKrToUtf8, eucKr, "\xB0\xA1", 0, 4) == srcBufferEnd); + Require(Scalar(sceCesUhcToUtf8, uhc, "\x8C\x63", 1, 4) == srcBufferEnd); + Require(Scalar(sceCesBig5ToUtf8, big5, "\xA4\xA4", 1, 4) == srcBufferEnd); + Require(Scalar(sceCesEucKrToUtf8, eucKr, "\xB0\xA1", 2, 2) == dstBufferEnd); + Require(Scalar(sceCesEucKrToUtf8, eucKr, "\x8C\x63", 2, 4) == invalidEncode); + Require(Scalar(sceCesUhcToUtf8, uhc, "\x80\xA1", 2, 4) == invalidEncode); + Require(Scalar(sceCesUhcToUtf8, uhc, "\xB0\x5B", 2, 4) == invalidEncode); + Require(Scalar(sceCesUhcToUtf8, uhc, "\xC9\xA1", 2, 4) == unassignedCode); + Require(Scalar(sceCesUhcToUtf8, uhc, "\xC7\x41", 2, 4) == unassignedCode); + Require(Scalar(sceCesBig5ToUtf8, big5, "\xA4\x80", 2, 4) == invalidEncode); + Require(Scalar(sceCesBig5ToUtf8, big5, "\xFF\xA1", 2, 4) == invalidEncode); + Require(Scalar(sceCesBig5ToUtf8, big5, "\x81\x40", 2, 4) == unassignedCode); + Require(Scalar(sceCesBig5ToUtf8, big5, "\xFA\x40", 2, 4) == unassignedCode); + Require(sceCesEucKrToUtf8(eucKr, Bytes("\xB0\xA1"), 2, nullptr, nullptr, 4, nullptr) == invalidDstBuffer); + + alignas(8) std::uint8_t context[64] = {}; + Require(sceCesMbcsUcsContextInit(context, eucKr) == 0); + RequireString(context, "\xC7\xD1\xB1\xB9\xBE\xEE A", "\xED\x95\x9C\xEA\xB5\xAD\xEC\x96\xB4 A"); + Require(Utf8ToMbcs(context, "\xEB\x98\xA0") == unassignedCode); + Require(RoundTripMismatches(context) == 0); + + Require(sceCesMbcsUcsContextInit(context, uhc) == 0); + RequireString(context, "\x8C\x63\xC7\xD1", "\xEB\x98\xA0\xED\x95\x9C"); + Require(Utf8ToMbcs(context, "\xE4\xB8\xAD") == 0); + Require(Utf8ToMbcs(context, "\xE2\x82\xAC\xEF\xBF\xBF") == unassignedCode); + Require(RoundTripMismatches(context) == 0); + + Require(sceCesMbcsUcsContextInit(context, big5) == 0); + RequireString(context, "\xA4\xA4\xA4\xE5", "\xE4\xB8\xAD\xE6\x96\x87"); + RequireString(context, "\xF9\xF9\xA2\x7E\xA4\x51\xA4\xCA", "\xE2\x95\x90\xE2\x95\xAD\xE5\x8D\x81\xE5\x8D\x85"); + RequireString(context, "\xF9\xE9\xF9\xEB\xF9\xEA\xA2\xA1\xA2\xA3\xA2\xA2", "\xE2\x95\x9E\xE2\x95\xA1\xE2\x95\xAA\xE2\x95\xAE\xE2\x95\xAF\xE2\x95\xB0"); + Require(Utf8ToMbcs(context, "\xEA\xB0\x80") == unassignedCode); + Require(RoundTripMismatches(context) == 10); + return 0; +} diff --git a/core/libs/tests/GuestCesUnicode.cpp b/core/libs/tests/GuestCesUnicode.cpp new file mode 100644 index 0000000000..1603f09810 --- /dev/null +++ b/core/libs/tests/GuestCesUnicode.cpp @@ -0,0 +1,252 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceCesCs/JisX0208.hpp" +#include "prx/libSceCesCs/JisX0212.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceCesUcs2ToUtf8(uint16_t, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf8ToUcs2(const uint8_t*, uint32_t, uint32_t*, uint16_t*); +int APS5_VABI sceCesUtf32ToUtf8(uint32_t, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf32beToUtf8(const uint32_t*, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf32leToUtf8(const uint32_t*, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf32(const uint8_t*, uint32_t, uint32_t*, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf32be(const uint8_t*, uint32_t, uint32_t*, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf32le(const uint8_t*, uint32_t, uint32_t*, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf16(const uint8_t*, uint32_t, uint32_t*, uint16_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf16be(const uint8_t*, uint32_t, uint32_t*, uint16_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf8ToUtf16le(const uint8_t*, uint32_t, uint32_t*, uint16_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf16ToUtf8(const uint16_t*, uint32_t, uint32_t*, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf16beToUtf8(const uint16_t*, uint32_t, uint32_t*, uint8_t*, uint32_t, uint32_t*); +int APS5_VABI sceCesUtf16leToUtf8(const uint16_t*, uint32_t, uint32_t*, uint8_t*, uint32_t, uint32_t*); +void* APS5_VABI sceCesUcsProfileInitEucJpCp51932(void*); +int APS5_VABI sceCesEucJpToUtf8(const void*, const uint8_t*, uint32_t, uint32_t*, uint8_t*, uint32_t, uint32_t*); +} + +namespace { + +constexpr int INVALID_PARAMETER = static_cast(0x805C0001); +constexpr int INVALID_SRC_BUFFER = static_cast(0x805C0010); +constexpr int SRC_BUFFER_END = static_cast(0x805C0011); +constexpr int INVALID_ENCODE = static_cast(0x805C0014); +constexpr int UNASSIGNED_CODE = static_cast(0x805C0020); +constexpr int INVALID_DST_BUFFER = static_cast(0x805C0030); +constexpr int DST_BUFFER_END = static_cast(0x805C0031); +constexpr uint32_t SENTINEL = 0xA5A5A5A5; + +void Require(bool value) { + if (!value) std::abort(); +} + +void Hash(uint64_t& hash, const void* data, uint32_t size) { + const auto* bytes = static_cast(data); + for (uint32_t i = 0; i < size; ++i) hash = (hash ^ bytes[i]) * 1099511628211ull; +} + +void TestByteOrder() { + const uint8_t source[] = {0xF0, 0x9F, 0x98, 0x80}; + const uint8_t expected16be[] = {0xD8, 0x3D, 0xDE, 0x00}; + const uint8_t expected16le[] = {0x3D, 0xD8, 0x00, 0xDE}; + const uint8_t expected32be[] = {0x00, 0x01, 0xF6, 0x00}; + const uint8_t expected32le[] = {0x00, 0xF6, 0x01, 0x00}; + uint16_t units[2] = {}; + uint32_t scalar = 0; + Require(sceCesUtf8ToUtf16be(source, 4, nullptr, units, 2, nullptr) == 0 && std::memcmp(units, expected16be, 4) == 0); + Require(sceCesUtf8ToUtf16le(source, 4, nullptr, units, 2, nullptr) == 0 && std::memcmp(units, expected16le, 4) == 0); + Require(sceCesUtf8ToUtf32be(source, 4, nullptr, &scalar) == 0 && std::memcmp(&scalar, expected32be, 4) == 0); + Require(sceCesUtf8ToUtf32le(source, 4, nullptr, &scalar) == 0 && std::memcmp(&scalar, expected32le, 4) == 0); + const uint8_t bom[] = {0xEF, 0xBB, 0xBF}; + Require(sceCesUtf8ToUtf32(bom, 3, nullptr, &scalar) == 0 && scalar == 0xFEFF); + Require(sceCesUtf8ToUtf16(bom, 3, nullptr, units, 1, nullptr) == 0 && units[0] == 0xFEFF); +} + +void TestScalars() { + uint64_t utf8Hash = 14695981039346656037ull; + uint64_t utf16Hash = utf8Hash; + for (uint32_t code = 0; code <= 0x10FFFF; ++code) { + if (code >= 0xD800 && code <= 0xDFFF) continue; + std::array bytes; + bytes.fill(0xA5); + uint32_t produced = SENTINEL; + Require(sceCesUtf32ToUtf8(code, bytes.data(), 4, &produced) == 0); + Require(produced >= 1 && produced <= 4 && bytes[produced] == 0xA5); + Hash(utf8Hash, bytes.data(), produced); + uint32_t decoded = SENTINEL; + uint32_t consumed = SENTINEL; + Require(sceCesUtf8ToUtf32(bytes.data(), produced, &consumed, &decoded) == 0); + Require(decoded == code && consumed == produced); + + uint16_t ucs2 = 0xA5A5; + consumed = SENTINEL; + const int ucsResult = sceCesUtf8ToUcs2(bytes.data(), produced, &consumed, &ucs2); + std::array back; + back.fill(0xA5); + uint32_t size = SENTINEL; + if (code <= 0xFFFF) { + Require(ucsResult == 0 && ucs2 == code && consumed == produced); + Require(sceCesUcs2ToUtf8(ucs2, back.data(), 4, &size) == 0); + Require(size == produced && std::memcmp(back.data(), bytes.data(), size) == 0); + } else Require(ucsResult == UNASSIGNED_CODE && ucs2 == 0xA5A5 && consumed == SENTINEL); + + using To16 = int (APS5_VABI *)(const uint8_t*, uint32_t, uint32_t*, uint16_t*, uint32_t, uint32_t*); + using From16 = int (APS5_VABI *)(const uint16_t*, uint32_t, uint32_t*, uint8_t*, uint32_t, uint32_t*); + const To16 to16[] = {sceCesUtf8ToUtf16, sceCesUtf8ToUtf16be, sceCesUtf8ToUtf16le}; + const From16 from16[] = {sceCesUtf16ToUtf8, sceCesUtf16beToUtf8, sceCesUtf16leToUtf8}; + for (uint32_t variant = 0; variant < 3; ++variant) { + uint16_t units[3] = {0xA5A5, 0xA5A5, 0xA5A5}; + uint32_t unitCount = SENTINEL; + consumed = SENTINEL; + Require(to16[variant](bytes.data(), produced, &consumed, units, 2, &unitCount) == 0); + Require(consumed == produced && unitCount == (code <= 0xFFFF ? 1u : 2u) && units[unitCount] == 0xA5A5); + if (variant == 2) Hash(utf16Hash, units, 2 * unitCount); + consumed = size = SENTINEL; + Require(from16[variant](units, unitCount, &consumed, back.data(), 4, &size) == 0); + Require(consumed == unitCount && size == produced && std::memcmp(back.data(), bytes.data(), size) == 0); + } + + using To32 = int (APS5_VABI *)(const uint8_t*, uint32_t, uint32_t*, uint32_t*); + using From32 = int (APS5_VABI *)(const uint32_t*, uint8_t*, uint32_t, uint32_t*); + const To32 to32[] = {sceCesUtf8ToUtf32be, sceCesUtf8ToUtf32le}; + const From32 from32[] = {sceCesUtf32beToUtf8, sceCesUtf32leToUtf8}; + for (uint32_t variant = 0; variant < 2; ++variant) { + uint32_t stored[2] = {SENTINEL, SENTINEL}; + Require(to32[variant](bytes.data(), produced, &consumed, stored) == 0); + Require(consumed == produced && stored[1] == SENTINEL); + const auto* raw = reinterpret_cast(stored); + Require(raw[variant == 0 ? 0 : 3] == 0 && raw[variant == 0 ? 3 : 0] == (code & 0xFF)); + Require(from32[variant](stored, back.data(), 4, &size) == 0); + Require(size == produced && std::memcmp(back.data(), bytes.data(), size) == 0); + } + } + Require(utf8Hash == 0x957CC0987E6013E5ull); + Require(utf16Hash == 0x6A25A430597EC325ull); + std::puts("1112064 Unicode scalars match Python UTF-8 and UTF-16LE reference hashes"); +} + +void TestErrors() { + const uint8_t emoji[] = {0xF0, 0x9F, 0x98, 0x80, 'X'}; + uint32_t consumed = SENTINEL; + uint32_t produced = SENTINEL; + uint32_t scalar = SENTINEL; + uint16_t units[3] = {0xA5A5, 0xA5A5, 0xA5A5}; + uint8_t bytes[5] = {0xA5, 0xA5, 0xA5, 0xA5, 0xA5}; + for (uint32_t cap = 0; cap < 4; ++cap) { + Require(sceCesUtf32ToUtf8(0x1F600, bytes, cap, &produced) == DST_BUFFER_END); + Require(produced == SENTINEL && bytes[0] == 0xA5 && bytes[3] == 0xA5); + Require(sceCesUtf8ToUtf32(emoji, cap, &consumed, &scalar) == SRC_BUFFER_END); + Require(consumed == SENTINEL && scalar == SENTINEL); + } + for (uint32_t cap = 0; cap < 2; ++cap) { + Require(sceCesUtf8ToUtf16(emoji, 4, &consumed, units, cap, &produced) == DST_BUFFER_END); + Require(consumed == SENTINEL && produced == SENTINEL && units[0] == 0xA5A5 && units[1] == 0xA5A5); + } + const uint8_t invalid[][4] = {{0x80}, {0xC0, 0x80}, {0xC1, 0xBF}, {0xC2, 0x7F}, {0xE0, 0x9F}, + {0xED, 0xA0}, {0xF0, 0x8F}, {0xF4, 0x90}, {0xF5}, {0xFF}, {0xE2, 0x82, 0x41}, {0xF0, 0x9F, 0x98, 0x41}}; + const uint32_t lengths[] = {1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 3, 4}; + for (uint32_t i = 0; i < std::size(invalid); ++i) { + Require(sceCesUtf8ToUtf32(invalid[i], lengths[i], &consumed, &scalar) == INVALID_ENCODE); + Require(sceCesUtf8ToUtf16be(invalid[i], lengths[i], &consumed, units, 2, &produced) == INVALID_ENCODE); + Require(consumed == SENTINEL && produced == SENTINEL && scalar == SENTINEL && units[0] == 0xA5A5); + } + for (uint32_t code : {0xD800u, 0xDBFFu, 0xDC00u, 0xDFFFu, 0x110000u, 0xFFFFFFFFu}) { + Require(sceCesUtf32ToUtf8(code, bytes, 4, &produced) == INVALID_ENCODE); + if (code <= 0xFFFF) Require(sceCesUcs2ToUtf8(static_cast(code), bytes, 4, &produced) == INVALID_ENCODE); + Require(produced == SENTINEL && bytes[0] == 0xA5); + } + const uint16_t high[] = {0xD800, 0x0041}; + const uint16_t low[] = {0xDC00}; + Require(sceCesUtf16ToUtf8(high, 1, &consumed, bytes, 4, &produced) == SRC_BUFFER_END); + Require(sceCesUtf16ToUtf8(high, 2, &consumed, bytes, 4, &produced) == INVALID_ENCODE); + Require(sceCesUtf16ToUtf8(low, 1, &consumed, bytes, 4, &produced) == INVALID_ENCODE); + Require(consumed == SENTINEL && produced == SENTINEL && bytes[0] == 0xA5); + Require(sceCesUtf8ToUtf32(nullptr, 4, &consumed, &scalar) == INVALID_SRC_BUFFER); + Require(sceCesUtf8ToUtf32(emoji, 4, &consumed, nullptr) == INVALID_DST_BUFFER); + Require(sceCesUtf8ToUcs2(emoji, 4, &consumed, nullptr) == INVALID_DST_BUFFER); + Require(sceCesUtf8ToUtf16(emoji, 4, &consumed, nullptr, 2, &produced) == INVALID_DST_BUFFER); + Require(sceCesUtf16ToUtf8(nullptr, 1, &consumed, bytes, 4, &produced) == INVALID_SRC_BUFFER); + Require(sceCesUtf16ToUtf8(high, 0, &consumed, bytes, 4, &produced) == SRC_BUFFER_END); + Require(sceCesUtf32beToUtf8(nullptr, bytes, 4, &produced) == INVALID_SRC_BUFFER); + Require(sceCesUtf32ToUtf8(0, nullptr, 4, &produced) == INVALID_DST_BUFFER); + Require(sceCesUtf8ToUtf32(emoji, 5, nullptr, &scalar) == 0 && scalar == 0x1F600); + Require(sceCesUtf8ToUtf16(emoji, 5, nullptr, units, 2, nullptr) == 0 && units[0] == 0xD83D && units[1] == 0xDE00); + Require(sceCesUtf16ToUtf8(units, 2, nullptr, bytes, 4, nullptr) == 0 && std::memcmp(bytes, emoji, 4) == 0); + Require(sceCesUcs2ToUtf8(0, bytes, 1, nullptr) == 0 && bytes[0] == 0); + Require(sceCesUtf8ToUcs2(bytes, 1, nullptr, units) == 0 && units[0] == 0); +} + +void TestEucJp() { + std::array sheet{}; + const void* profile = sceCesUcsProfileInitEucJpCp51932(sheet.data()); + auto check = [&](const uint8_t* source, uint32_t length, uint32_t code) { + uint8_t expected[4] = {}; + uint32_t expectedLength = 0; + Require(sceCesUtf32ToUtf8(code, expected, 4, &expectedLength) == 0); + std::array output; + output.fill(0xA5); + uint32_t consumed = SENTINEL; + uint32_t produced = SENTINEL; + Require(sceCesEucJpToUtf8(profile, source, length, &consumed, output.data(), expectedLength, &produced) == 0); + Require(consumed == length && produced == expectedLength && output[produced] == 0xA5); + Require(std::memcmp(output.data(), expected, produced) == 0); + for (uint32_t cap = 0; cap < expectedLength; ++cap) { + output.fill(0xA5); + consumed = produced = SENTINEL; + Require(sceCesEucJpToUtf8(profile, source, length, &consumed, output.data(), cap, &produced) == DST_BUFFER_END); + Require(consumed == SENTINEL && produced == SENTINEL && output[0] == 0xA5); + } + }; + for (uint32_t code = 0; code < 128; ++code) { + const uint8_t source[] = {static_cast(code)}; + check(source, 1, code); + } + for (uint32_t cell = 0; cell < 63; ++cell) { + const uint8_t source[] = {0x8E, static_cast(0xA1 + cell)}; + check(source, 2, 0xFF61 + cell); + } + uint32_t assigned = 0; + for (uint32_t supplementary = 0; supplementary < 2; ++supplementary) { + for (uint32_t cell = 0; cell < 94 * 94; ++cell) { + const uint32_t code = supplementary ? JisX0212::TO_UCS2[cell] : JisX0208::TO_UCS2[cell]; + const uint8_t source[] = {0x8F, static_cast(0xA1 + cell / 94), static_cast(0xA1 + cell % 94)}; + const uint8_t* start = source + (supplementary ? 0 : 1); + const uint32_t length = supplementary ? 3 : 2; + if (code) { check(start, length, code); ++assigned; } + else { + uint8_t output[4] = {0xA5, 0xA5, 0xA5, 0xA5}; + uint32_t consumed = SENTINEL; + uint32_t produced = SENTINEL; + Require(sceCesEucJpToUtf8(profile, start, length, &consumed, output, 4, &produced) == UNASSIGNED_CODE); + Require(consumed == SENTINEL && produced == SENTINEL && output[0] == 0xA5); + } + } + } + Require(assigned == 13403); + const uint8_t source[] = {0x8F, 0xA2, 0xAF, 'X'}; + uint8_t output[4] = {0xA5, 0xA5, 0xA5, 0xA5}; + uint32_t consumed = SENTINEL; + uint32_t produced = SENTINEL; + Require(sceCesEucJpToUtf8(nullptr, source, 3, &consumed, output, 4, &produced) == INVALID_PARAMETER); + Require(sceCesEucJpToUtf8(profile, nullptr, 3, &consumed, output, 4, &produced) == INVALID_SRC_BUFFER); + Require(sceCesEucJpToUtf8(profile, source, 0, &consumed, output, 4, &produced) == SRC_BUFFER_END); + Require(sceCesEucJpToUtf8(profile, source, 3, &consumed, nullptr, 4, &produced) == INVALID_DST_BUFFER); + for (uint32_t size : {1u, 2u}) Require(sceCesEucJpToUtf8(profile, source, size, &consumed, output, 4, &produced) == SRC_BUFFER_END); + const uint8_t malformed[] = {0x8E, 0x41}; + Require(sceCesEucJpToUtf8(profile, malformed, 2, &consumed, output, 4, &produced) == INVALID_ENCODE); + Require(consumed == SENTINEL && produced == SENTINEL && output[0] == 0xA5); + Require(sceCesEucJpToUtf8(profile, source, 4, &consumed, output, 4, &produced) == 0); + Require(consumed == 3 && produced == 2 && output[0] == 0xCB && output[1] == 0x98 && output[2] == 0xA5); + Require(sceCesEucJpToUtf8(profile, source, 4, nullptr, output, 4, nullptr) == 0); + std::puts("13403 assigned JIS cells plus ASCII, half-width katakana and unassigned cells checked"); +} + +} + +int main() { + TestByteOrder(); + TestErrors(); + TestScalars(); + TestEucJp(); +} diff --git a/core/libs/tests/GuestClosedStandardDescriptors.cpp b/core/libs/tests/GuestClosedStandardDescriptors.cpp new file mode 100644 index 0000000000..4cb0deece1 --- /dev/null +++ b/core/libs/tests/GuestClosedStandardDescriptors.cpp @@ -0,0 +1,66 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI fcntl_nid_postfix(int, int, ...); +} + +namespace { + +constexpr int GuestGetFlags = 3; + +int Child(const char* closed) { + struct stat null{}; + if (::stat("/dev/null", &null) != 0) return 70; + for (int descriptor = 0; descriptor <= 2; ++descriptor) { + struct stat status{}; + if (::fstat(descriptor, &status) != 0) return 71 + descriptor; + if (closed[descriptor] != '1') continue; + if (!S_ISCHR(status.st_mode) || status.st_rdev != null.st_rdev) return 74 + descriptor; + if ((::fcntl(descriptor, F_GETFL) & O_ACCMODE) != (descriptor == 0 ? O_RDONLY : O_WRONLY)) return 77 + descriptor; + } + if (closed[0] == '1' && fcntl_nid_postfix(0, GuestGetFlags) < 0) return 80; + return 0; +} + +int RunClosed(const char* self, bool closeInput, bool closeOutput, bool closeError) { + const pid_t child = ::fork(); + if (child < 0) return -1; + if (child == 0) { + if (closeInput) ::close(0); + if (closeOutput) ::close(1); + if (closeError) ::close(2); + char closed[] = {closeInput ? '1' : '0', closeOutput ? '1' : '0', closeError ? '1' : '0', '\0'}; + char* const arguments[] = {const_cast(self), const_cast("--closed-child"), closed, nullptr}; + ::execv(self, arguments); + ::_exit(69); + } + int status = 0; + if (::waitpid(child, &status, 0) != child) return -1; + return WIFEXITED(status) ? WEXITSTATUS(status) : 128 + WTERMSIG(status); +} + +} + +int main(int argc, char** argv) { + if (argc > 2 && std::strcmp(argv[1], "--closed-child") == 0 && std::strlen(argv[2]) == 3) return Child(argv[2]); + const struct { bool input, output, error; const char* name; } cases[] = { + {true, false, false, "stdin"}, + {false, true, false, "stdout"}, + {true, true, true, "all three"}, + }; + for (const auto& test : cases) { + const int result = RunClosed(argv[0], test.input, test.output, test.error); + if (result != 0) { + std::fprintf(stderr, "closed standard descriptors, %s: child exited with %d\n", test.name, result); + return 1; + } + } + std::puts("closed standard descriptors are reopened on /dev/null"); + return 0; +} diff --git a/core/libs/tests/GuestCollate.cpp b/core/libs/tests/GuestCollate.cpp index ddaf978819..7dec7aabab 100644 --- a/core/libs/tests/GuestCollate.cpp +++ b/core/libs/tests/GuestCollate.cpp @@ -11,6 +11,7 @@ extern "C" { extern GuestLocale::Implementation* _ZSt21_sceLibcClassicLocale_nid_postfix; std::size_t APS5_VABI _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale); +std::size_t APS5_VABI _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale); } namespace { @@ -63,6 +64,142 @@ std::uint64_t Hash(const GuestLocale::CollateFacet* facet, const std::string& te return static_cast(Vtable(&facet->base).hash(facet, text.data(), text.data() + text.size())); } +// collate hands the vtable pointers to its 16-bit characters, so the tests below pass the +// characters of a wide string where the vtable declares a range of char and count it in characters. +const char* Characters(const char16_t* text) { + return reinterpret_cast(text); +} + +int CompareWide(const GuestLocale::CollateFacet* facet, const std::u16string& left, const std::u16string& right) { + return Vtable(&facet->base).compare(facet, Characters(left.data()), Characters(left.data() + left.size()), Characters(right.data()), Characters(right.data() + right.size())); +} + +std::uint64_t HashWide(const GuestLocale::CollateFacet* facet, const std::u16string& text) { + return static_cast(Vtable(&facet->base).hash(facet, Characters(text.data()), Characters(text.data() + text.size()))); +} + +GuestLocale::String* TransformWide(const GuestLocale::CollateFacet* facet, GuestLocale::String* result, const std::u16string& text) { + return Vtable(&facet->base).transform(result, facet, Characters(text.data()), Characters(text.data() + text.size())); +} + +// The wide facet mirrors the narrow one in the facet it allocates and the C-locale validation, but it +// carries its own vtable, whose entries work on the guest's 16-bit characters. +void CheckWideGetcat(const GuestLocale::Implementation* const* classic, const GuestLocale::CollateFacet* narrow) { + require(_ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(nullptr, classic) == 1); + + const std::size_t before = allocations; + GuestLocale::Facet existing{}; + GuestLocale::Facet* facet = &existing; + require(_ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&facet, classic) == 1); + require(facet == &existing && allocations == before); + + GuestLocale::Implementation french = *_ZSt21_sceLibcClassicLocale_nid_postfix; + french.name = "fr_FR"; + const GuestLocale::Implementation* frenchPointer = &french; + GuestLocale::Implementation unnamed = *_ZSt21_sceLibcClassicLocale_nid_postfix; + unnamed.name = nullptr; + const GuestLocale::Implementation* unnamedPointer = &unnamed; + const GuestLocale::Implementation* missingPointer = nullptr; + for (const GuestLocale::Implementation* const* rejected : {&frenchPointer, &unnamedPointer, &missingPointer}) { + GuestLocale::Facet* absent = nullptr; + reject([&] { _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&absent, rejected); }); + require(absent == nullptr && allocations == before); + } + GuestLocale::Facet* absent = nullptr; + reject([&] { _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&absent, nullptr); }); + require(absent == nullptr && allocations == before); + + facet = nullptr; + require(_ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&facet, classic) == 1); + require(facet != nullptr && allocations == before + 1 && lastSize == sizeof(GuestLocale::CollateFacet) && lastAllocation == facet); + const auto* wide = reinterpret_cast(facet); + require(facet->references == 0 && facet->vtable != narrow->base.vtable); + require(wide->collation == nullptr && wide->wideCollation == nullptr); + + const std::u16string text = u"abc"; + require(CompareWide(wide, text, u"abd") == -1); + require(CompareWide(wide, u"abd", text) == 1); + require(CompareWide(wide, text, text) == 0); + require(CompareWide(wide, u"ab", text) == -1); + require(CompareWide(wide, u"", text) == -1); + require(CompareWide(wide, u"", u"") == 0); + // Characters above 0xff order by their whole value: 0x0100 and 0x0001 compare the other way round + // when the same characters are read as the little-endian bytes 0x00 0x01 and 0x01 0x00. + require(CompareWide(wide, u"\u0100", u"\u0001") == 1); + require(CompareWide(wide, u"\u0001", u"\u0100") == -1); + require(CompareWide(wide, u"\u00ff", u"\u0100") == -1); + require(CompareWide(wide, u"\u00e9", u"z") == 1); + require(CompareWide(wide, u"a\u0101b", u"a\u0100b") == 1); + require(CompareWide(wide, u"a\u0101b", u"a\u0102b") == -1); + const std::u16string embeddedWide(u"a\0b", 3); + require(CompareWide(wide, embeddedWide, u"a") == 1); + require(CompareWide(wide, text, u"abd") == Compare(narrow, "abc", "abd")); + + require(HashWide(wide, u"") == 0xcbf29ce484222325ull); + require(HashWide(wide, text) == 0xe71fa2190541574bull); + require(HashWide(wide, u"a") == 0xaf63dc4c8601ec8cull); + // One round per character, so a wide string of characters below 0x100 hashes like its narrow + // spelling and a character above 0xff hashes like one value instead of the two bytes it holds. + require(HashWide(wide, u"foobar") == 0x85944171f73967e8ull); + require(HashWide(wide, u"foobar") == Hash(narrow, "foobar")); + require(HashWide(wide, u"\u0100") == 0x0af62bd4c860004dfull); + + GuestLocale::String result; + std::memset(&result, 0xcd, sizeof(result)); + require(TransformWide(wide, &result, text) == &result); + require(result.reserved == 0xcdcdcdcdcdcdcdcdull && result.size == 3 && result.capacity == 7 && allocations == before + 1); + const auto* inlineUnits = reinterpret_cast(result.buffer); + require(inlineUnits[0] == u'a' && inlineUnits[1] == u'b' && inlineUnits[2] == u'c' && inlineUnits[3] == 0); + + std::memset(&result, 0xcd, sizeof(result)); + require(Vtable(facet).transform(&result, wide, nullptr, nullptr) == &result); + require(result.size == 0 && result.capacity == 7 && reinterpret_cast(result.buffer)[0] == 0); + + const std::u16string seven(7, u'q'); + require(TransformWide(wide, &result, seven) == &result); + require(result.size == 7 && result.capacity == 7 && allocations == before + 1); + const auto* inlineSeven = reinterpret_cast(result.buffer); + require(inlineSeven[6] == u'q' && inlineSeven[7] == 0); + + const std::u16string eight(8, u'y'); + require(TransformWide(wide, &result, eight) == &result); + require(allocations == before + 2 && lastSize == 32 && result.size == 8 && result.capacity == 15 && result.pointer == lastAllocation); + require(reinterpret_cast(result.pointer)[0] == u'y' && reinterpret_cast(result.pointer)[8] == 0); + std::free(result.pointer); + + for (const std::size_t length : {2039, 2040}) { + const std::u16string large(length, u'\u0100'); + require(TransformWide(wide, &result, large) == &result); + require(result.size == length && result.capacity == (length | 7)); + const auto* units = reinterpret_cast(result.pointer); + require(std::u16string(units, result.size) == large && units[result.size] == 0); + require(lastSize == (length == 2039 ? 4080 : 4096 + 0x27)); + if (length == 2040) { + require(reinterpret_cast(units) % 0x20 == 0); + require(reinterpret_cast(units)[-1] == lastAllocation); + } else require(result.pointer == lastAllocation); + std::free(lastAllocation); + } + + const std::u16string embeddedText(u"a\0b", 3); + reject([&] { TransformWide(wide, &result, embeddedText); }); + reject([&] { Vtable(facet).transform(nullptr, wide, Characters(seven.data()), Characters(seven.data() + seven.size())); }); + require(allocations == before + 4); + reject([&] { Vtable(facet).compare(wide, Characters(text.data() + 2), Characters(text.data()), Characters(text.data()), Characters(text.data() + 1)); }); + reject([&] { Vtable(facet).compare(wide, nullptr, Characters(text.data() + 1), Characters(text.data()), Characters(text.data() + 1)); }); + reject([&] { Vtable(facet).hash(wide, Characters(text.data() + 1), Characters(text.data())); }); + + Vtable(facet).facet.retain(facet); + require(facet->references == 1); + require(Vtable(facet).facet.release(facet) == facet && facet->references == 0); + const std::size_t freed = frees; + Vtable(facet).facet.destroy(facet); + require(frees == freed); + Vtable(facet).facet.deleteObject(facet); + require(frees == freed + 1 && lastFree == wide); + require(allocations == before + 4); +} + } int main() { @@ -155,9 +292,12 @@ int main() { require(facet->references == 2); require(Vtable(facet).facet.release(facet) == nullptr && facet->references == 1); require(Vtable(facet).facet.release(facet) == facet && facet->references == 0); + CheckWideGetcat(classic, collate); + Vtable(facet).facet.destroy(facet); - require(frees == 0); + require(frees == 1); Vtable(facet).facet.deleteObject(facet); - require(frees == 1 && lastFree == collate); + require(frees == 2 && lastFree == collate); + require(allocations == 8); return 0; } diff --git a/core/libs/tests/GuestCommonDialog.cpp b/core/libs/tests/GuestCommonDialog.cpp new file mode 100644 index 0000000000..e5b9cd993b --- /dev/null +++ b/core/libs/tests/GuestCommonDialog.cpp @@ -0,0 +1,27 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" +#include + +extern "C" { +int APS5_VABI sceCommonDialogInitialize(void); +bool APS5_VABI sceCommonDialogIsUsed(void); +} + +namespace { + +void Require(bool value) { + if (!value) std::abort(); +} + +constexpr int kErrAlreadySystemInitialized = static_cast(0x80B80002); + +} + +int main() { + Require(!sceCommonDialogIsUsed()); + Require(sceCommonDialogInitialize() == COMMON_DIALOG_OK); + Require(!sceCommonDialogIsUsed()); + Require(sceCommonDialogInitialize() == kErrAlreadySystemInitialized); + Require(sceCommonDialogInitialize() == kErrAlreadySystemInitialized); + Require(!sceCommonDialogIsUsed()); +} diff --git a/core/libs/tests/GuestCondTimedwait.cpp b/core/libs/tests/GuestCondTimedwait.cpp index 5a9190e1ec..434d257d2d 100644 --- a/core/libs/tests/GuestCondTimedwait.cpp +++ b/core/libs/tests/GuestCondTimedwait.cpp @@ -16,6 +16,9 @@ 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_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* abstime); 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*); @@ -29,6 +32,7 @@ static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); static constexpr int POSIX_EPERM = 1; +static constexpr int POSIX_EINVAL = 22; static constexpr int POSIX_ETIMEDOUT = 60; static void Require(bool value) { if (!value) std::abort(); } @@ -121,6 +125,23 @@ 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); + { + const KernelTimespec negativeSeconds{-1, 0}; + const KernelTimespec negativeNanoseconds{1, -1}; + Require(scePthreadMutexLock(&context.mutex) == SCE_OK); + Require(pthread_cond_timedwait_nid_postfix(&cond, &context.mutex, &negativeSeconds) == POSIX_EINVAL); + Require(pthread_cond_timedwait_nid_postfix(&cond, &context.mutex, &negativeNanoseconds) == POSIX_EINVAL); + Require(scePthreadMutexUnlock(&context.mutex) == SCE_OK); + } + 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..06cebf5b38 --- /dev/null +++ b/core/libs/tests/GuestContentDelete.cpp @@ -0,0 +1,57 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include + +extern "C" int APS5_VABI sceContentDeleteInitialize(const ContentDeleteInitParam* initParam); +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(sceContentDeleteTerminate() == static_cast(0x809D5002u)); + Require(sceContentDeleteInitialize(nullptr) == static_cast(0x809D5001u)); + Require(sceContentDeleteById(1) == static_cast(0x809D5002u)); + Require(sceContentDeleteByPath("/user/av_contents/photo/a.png") == static_cast(0x809D5002u)); + Require(sceContentDeleteByPath(nullptr) == static_cast(0x809D5002u)); + + ContentDeleteInitParam param{}; + param.heap_size = 0x10000; + Require(sceContentDeleteInitialize(¶m) == static_cast(0x809D5001u)); + param.heap_size = 0x4000; + Require(sceContentDeleteInitialize(¶m) == 0); + Require(sceContentDeleteInitialize(¶m) == static_cast(0x809D5003u)); + Require(sceContentDeleteInitialize(nullptr) == static_cast(0x809D5001u)); + Require(sceContentDeleteByPath("") == static_cast(0x809D5001u)); + + 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(sceContentDeleteTerminate() == static_cast(0x809D5002u)); + Require(sceContentDeleteById(1) == static_cast(0x809D5002u)); + return 0; +} diff --git a/core/libs/tests/GuestContentExport.cpp b/core/libs/tests/GuestContentExport.cpp index ef295d6ec9..4f72b3a7c7 100644 --- a/core/libs/tests/GuestContentExport.cpp +++ b/core/libs/tests/GuestContentExport.cpp @@ -4,14 +4,26 @@ #include #include +extern "C" int APS5_VABI sceContentExportAddToFavorites(std::int64_t contentId); +extern "C" int APS5_VABI sceContentExportDataProvideFunction(void); +extern "C" int APS5_VABI sceContentExportFree(void); +extern "C" int APS5_VABI sceContentExportFrom(void); +extern "C" int APS5_VABI sceContentExportFromFileForSf(std::int32_t expId, const ContentExportParam* param, const ContentExportSfParam* sfParam, const char* sourcePath, const char* thumbnailPath, char* path, std::size_t pathBufferLength); +extern "C" int APS5_VABI sceContentExportFromFileWithContentIdList(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, const char* thumbnailPath, const std::int64_t* contentIds, std::uint32_t contentCount, char* path, std::size_t pathBufferLength); +extern "C" int APS5_VABI sceContentExportFromFileWithTitleIdList(std::int32_t expId, const ContentExportParam* param, const char* sourcePath, const char* thumbnailPath, const char* titleIdList, char* path, std::size_t pathBufferLength); +extern "C" int APS5_VABI sceContentExportInit(const ContentExportInitParam* param); +extern "C" int APS5_VABI sceContentExportMalloc(void); +extern "C" int APS5_VABI sceContentExportRemoveFromFavorites(std::int64_t contentId); +extern "C" int APS5_VABI sceContentExportRenameContent(std::int64_t contentId, const char* name); +extern "C" int APS5_VABI sceContentExportValidateContents(const std::int64_t* contentIds, std::uint32_t count); + extern "C" int APS5_VABI sceContentExportInit2(const ContentExportInitParam2* init_param); extern "C" int APS5_VABI sceContentExportTerm(void); extern "C" int APS5_VABI sceContentExportStart(void); extern "C" int APS5_VABI sceContentExportFinish(std::int32_t expId); extern "C" int APS5_VABI sceContentExportGetProgress(std::int32_t expId); extern "C" int APS5_VABI sceContentExportCancel(std::int32_t expId); -extern "C" int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, - const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength); +extern "C" int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength); namespace { @@ -42,8 +54,8 @@ void Export(std::int32_t expId) { int main() { ContentExportInitParam2 param{&allocator, &allocator, nullptr, 0x10000, 0, 0}; Require(Throws([] { sceContentExportInit2(nullptr); })); - Require(Throws([] { sceContentExportTerm(); })); - Require(Throws([] { sceContentExportStart(); })); + Require(sceContentExportTerm() == static_cast(0x809D3004u)); + Require(sceContentExportStart() == static_cast(0x809D3004u)); Require(Throws([] { sceContentExportFinish(1); })); ContentExportInitParam2 missing = param; missing.free_func = nullptr; @@ -54,20 +66,20 @@ int main() { Require(sceContentExportInit2(¶m) == 0); Require(Throws([&] { sceContentExportInit2(¶m); })); Require(sceContentExportTerm() == 0); - Require(Throws([] { sceContentExportTerm(); })); + Require(sceContentExportTerm() == static_cast(0x809D3004u)); Require(sceContentExportInit2(¶m) == 0); std::set sessions; for (int i = 0; i < 10; ++i) { int session = sceContentExportStart(); - Require(session > 0); + Require(session == i); Require(sessions.insert(session).second); } - Require(Throws([] { sceContentExportStart(); })); - Require(Throws([] { sceContentExportTerm(); })); + Require(sceContentExportStart() == static_cast(0x809D3018u)); + Require(sceContentExportTerm() == static_cast(0x809D3003u)); int first = *sessions.begin(); Require(Throws([&] { sceContentExportGetProgress(first); })); - Require(Throws([&] { sceContentExportCancel(first); })); + Require(sceContentExportCancel(first) == static_cast(0x809D3002u)); Require(Throws([&] { Export(first); })); Require(path[0] == 0); Require(sceContentExportFinish(first) == 0); @@ -79,10 +91,28 @@ int main() { Require(Throws([] { sceContentExportFinish(-1); })); sessions.erase(first); int reopened = sceContentExportStart(); - Require(reopened > 0 && reopened != first); + Require(reopened == first); Require(sessions.insert(reopened).second); + Require(sceContentExportStart() == static_cast(0x809D3018u)); + Require(sceContentExportFinish(7) == 0); + Require(sceContentExportFinish(3) == 0); + Require(sceContentExportStart() == 3); + Require(sceContentExportStart() == 7); for (int session : sessions) Require(sceContentExportFinish(session) == 0); Require(sceContentExportTerm() == 0); - Require(Throws([] { sceContentExportStart(); })); + Require(sceContentExportStart() == static_cast(0x809D3004u)); Require(Throws([&] { sceContentExportFinish(reopened); })); + Require(Throws([] { sceContentExportAddToFavorites(0); })); + Require(Throws([] { sceContentExportDataProvideFunction(); })); + Require(Throws([] { sceContentExportFree(); })); + Require(Throws([] { sceContentExportFrom(); })); + Require(Throws([] { sceContentExportFromFileForSf(0, nullptr, nullptr, nullptr, nullptr, nullptr, 0); })); + Require(Throws([] { sceContentExportFromFileWithContentIdList(0, nullptr, nullptr, nullptr, nullptr, 0, nullptr, 0); })); + Require(Throws([] { sceContentExportFromFileWithTitleIdList(0, nullptr, nullptr, nullptr, nullptr, nullptr, 0); })); + Require(Throws([] { sceContentExportInit(nullptr); })); + Require(Throws([] { sceContentExportMalloc(); })); + Require(Throws([] { sceContentExportRemoveFromFavorites(0); })); + Require(Throws([] { sceContentExportRenameContent(0, nullptr); })); + Require(Throws([] { sceContentExportValidateContents(nullptr, 0); })); } + diff --git a/core/libs/tests/GuestContentSearch.cpp b/core/libs/tests/GuestContentSearch.cpp index 2447383112..5676436c77 100644 --- a/core/libs/tests/GuestContentSearch.cpp +++ b/core/libs/tests/GuestContentSearch.cpp @@ -15,12 +15,34 @@ int APS5_VABI sceContentSearchOpenMetadataByContentId(std::int64_t contentId, st int APS5_VABI sceContentSearchCloseMetadata(std::int32_t metadataId); int APS5_VABI sceContentSearchGetMetadataFieldInfo(std::int32_t metadataId, const char* field, std::int32_t* metadataType, std::int32_t* size); int APS5_VABI sceContentSearchGetMetadataValue(std::int32_t metadataId, const char* field, void* value); +int APS5_VABI sceContentBinderEnd(void); +int APS5_VABI sceContentBinderGetNumOfDifference(void); +int APS5_VABI sceContentBinderInitialize(void); +int APS5_VABI sceContentBinderIsUpdated(void); +int APS5_VABI sceContentBinderReset(void); +int APS5_VABI sceContentBinderSearchDifference(void); +int APS5_VABI sceContentBinderStart(void); +int APS5_VABI sceContentBinderTerminate(void); +int APS5_VABI sceContentSearchGetApplicationLastUpdateId(void); +int APS5_VABI sceContentSearchGetMyApplicationIndex(void); +int APS5_VABI sceContentSearchGetNumOfApplication(void); +int APS5_VABI sceContentSearchSearchApplication(void); } namespace { void Require(bool value) { if (!value) std::abort(); } +template +bool ThrowsRuntimeError(TAction action) { + try { + action(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + template bool Throws(TAction action) { try { @@ -32,6 +54,8 @@ bool Throws(TAction action) { } constexpr int ErrorNotInitialized = static_cast(0x809D1001u); +constexpr int ErrorInvalidArgument = static_cast(0x809D1003u); +constexpr int ErrorAlreadyInitialized = static_cast(0x809D1002u); constexpr int ErrorLimitTooBig = static_cast(0x809D100Au); struct MetadataValue { @@ -66,7 +90,16 @@ int main() { Require(sceContentSearchCloseMetadata(1) == ErrorNotInitialized); Require(count == -1 && size == -1 && updateId == -1 && metadataId == -1); + Require(sceContentSearchTerm() == ErrorNotInitialized); + Require(sceContentSearchInit(nullptr) == ErrorInvalidArgument); + for (std::size_t memorySize : {std::size_t{0}, std::size_t{1}, std::size_t{16383}, param.memory_size - 1}) { + ContentSearchInitParam invalidParam{memorySize}; + Require(sceContentSearchInit(&invalidParam) == ErrorInvalidArgument); + Require(sceContentSearchTerm() == ErrorNotInitialized); + } Require(sceContentSearchInit(¶m) == 0); + Require(sceContentSearchInit(nullptr) == ErrorAlreadyInitialized); + Require(sceContentSearchInit(¶m) == ErrorAlreadyInitialized); Require(sceContentSearchGetContentLastUpdateId(&updateId) == 0); Require(updateId == 0); @@ -89,11 +122,11 @@ int main() { Require(sceContentSearchSearchContent(&columnSet, 1, &orderBy, 1, 0, 93, &count, infos, &updateId) == ErrorLimitTooBig); Require(count == -1 && updateId == -1); - Require(Throws([&] { sceContentSearchGetContentLastUpdateId(nullptr); })); - Require(Throws([&] { sceContentSearchGetNumOfContent(&columnSet, 1, nullptr, &updateId); })); - Require(Throws([&] { sceContentSearchGetNumOfContent(&columnSet, 1, &count, nullptr); })); - Require(Throws([&] { sceContentSearchGetTotalContentSize(nullptr, 1, &size, &updateId); })); - Require(Throws([&] { sceContentSearchSearchContent(&columnSet, 1, &orderBy, 1, 0, 2, &count, nullptr, &updateId); })); + Require(sceContentSearchGetContentLastUpdateId(nullptr) == ErrorInvalidArgument); + Require(sceContentSearchGetNumOfContent(&columnSet, 1, nullptr, &updateId) == ErrorInvalidArgument); + Require(sceContentSearchGetNumOfContent(&columnSet, 1, &count, nullptr) == ErrorInvalidArgument); + Require(sceContentSearchGetTotalContentSize(nullptr, 1, &size, &updateId) == ErrorInvalidArgument); + Require(sceContentSearchSearchContent(&columnSet, 1, &orderBy, 1, 0, 2, &count, nullptr, &updateId) == ErrorInvalidArgument); ContentSearchContentColumn unknownColumn{8, 0, &photo, 2}; ContentSearchContentColumnSet unknownColumnSet{&unknownColumn, 1, 2}; Require(Throws([&] { sceContentSearchGetNumOfContent(&unknownColumnSet, 1, &count, &updateId); })); @@ -106,11 +139,18 @@ int main() { ContentSearchContentOrderByCondition unknownSort{3, 0}; Require(Throws([&] { sceContentSearchSearchContent(&columnSet, 1, &unknownSort, 1, 0, 2, &count, infos, &updateId); })); Require(count == -1 && updateId == -1); + Require(sceContentSearchSearchContent(nullptr, 0, nullptr, 0, 0, 0, nullptr, nullptr, nullptr) == ErrorInvalidArgument); + Require(sceContentSearchSearchContent(nullptr, 0, nullptr, 0, 0, 93, nullptr, nullptr, nullptr) == ErrorLimitTooBig); + Require(sceContentSearchSearchContent(&columnSet, 0, nullptr, 0, 0, 93, nullptr, nullptr, nullptr) == ErrorInvalidArgument); + Require(sceContentSearchSearchContent(nullptr, 0, &orderBy, 0, 0, 92, &count, infos, &updateId) == ErrorInvalidArgument); + Require(sceContentSearchGetNumOfContent(&columnSet, 0, &count, &updateId) == ErrorInvalidArgument); + Require(sceContentSearchGetTotalContentSize(&columnSet, 0, &size, &updateId) == ErrorInvalidArgument); + Require(count == -1 && updateId == -1); Require(Throws([&] { sceContentSearchOpenMetadata("/data/photo.jpg", &metadataId); })); Require(Throws([&] { sceContentSearchOpenMetadataByContentId(1, &metadataId); })); Require(metadataId == -1); - Require(Throws([] { sceContentSearchCloseMetadata(1); })); + Require(sceContentSearchCloseMetadata(1) == ErrorInvalidArgument); Require(Throws([&] { sceContentSearchGetMetadataFieldInfo(1, "title", &metadataType, &fieldSize); })); Require(Throws([&] { sceContentSearchGetMetadataValue(1, "title", &value); })); Require(metadataType == -1 && fieldSize == -1 && value.size == -1); @@ -118,4 +158,18 @@ int main() { Require(sceContentSearchTerm() == 0); Require(sceContentSearchGetContentLastUpdateId(&updateId) == ErrorNotInitialized); Require(updateId == -1); + constexpr int binderResult = static_cast(0x809D000F); + Require(sceContentBinderInitialize() == binderResult); + Require(sceContentBinderStart() == binderResult); + Require(sceContentBinderReset() == binderResult); + Require(sceContentBinderIsUpdated() == binderResult); + Require(sceContentBinderGetNumOfDifference() == binderResult); + Require(sceContentBinderSearchDifference() == binderResult); + Require(sceContentBinderEnd() == binderResult); + Require(sceContentBinderTerminate() == binderResult); + + Require(ThrowsRuntimeError([] { sceContentSearchGetApplicationLastUpdateId(); })); + Require(ThrowsRuntimeError([] { sceContentSearchGetMyApplicationIndex(); })); + Require(ThrowsRuntimeError([] { sceContentSearchGetNumOfApplication(); })); + Require(ThrowsRuntimeError([] { sceContentSearchSearchApplication(); })); } 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..cce08ee498 100644 --- a/core/libs/tests/GuestConvertKeycode.cpp +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -2,9 +2,13 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include +#include extern "C" { +int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint16_t, std::uint32_t, std::uint16_t*); 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(); } @@ -16,15 +20,73 @@ int main() { Require(sceConvertKeycodeGetImeKeyboardType(0, nullptr) == invalidAddress); Require(sceConvertKeycodeGetImeKeyboardType(-1, nullptr) == invalidAddress); + std::uint16_t virtualKey = 0xABCD; + Require(sceConvertKeycodeGetVirtualKeycode(4, 0, nullptr) == invalidAddress); + Require(sceConvertKeycodeGetVirtualKeycode(4, 0, &virtualKey) == static_cast(0x80BC001Du)); + Require(virtualKey == 0xABCD); + Require(sceConvertKeycodeGetVirtualKeycode(4, 40, &virtualKey) == static_cast(0x80BC001Du)); + Require(sceConvertKeycodeGetVirtualKeycode(4, 4, &virtualKey) == 0 && virtualKey == 65); + Require(sceConvertKeycodeGetVirtualKeycode(4, 9, &virtualKey) == 0 && virtualKey == 81); + Require(sceConvertKeycodeGetVirtualKeycode(100, 4, &virtualKey) == 0 && virtualKey == 255); + Require(sceConvertKeycodeGetVirtualKeycode(136, 24, &virtualKey) == 0 && virtualKey == 21); + Require(sceConvertKeycodeGetVirtualKeycode(65535, 4, &virtualKey) == 0 && virtualKey == 255); + for (std::uint32_t layout : {2u, 3u, 12u, 31u, 39u}) { + virtualKey = 0xABCD; + bool rejected = false; + try { + sceConvertKeycodeGetVirtualKeycode(28, layout, &virtualKey); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected && virtualKey == 0xABCD); + } + std::uint32_t type = 0xFFFFFFFFu; Require(sceConvertKeycodeGetImeKeyboardType(-1, &type) == invalidUserId); Require(type == 0xFFFFFFFFu); - Require(sceConvertKeycodeGetImeKeyboardType(0, &type) == 0); - Require(type == 0); + bool rejected = false; + try { + sceConvertKeycodeGetImeKeyboardType(0, &type); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected && type == 0xFFFFFFFFu); - type = 0xFFFFFFFFu; - Require(sceConvertKeycodeGetImeKeyboardType(1, &type) == 0); - Require(type == 0); + Require(sceConvertKeycodeGetCharacter(4, 0, 0, nullptr) == invalidAddress); + char16_t character = u'?'; + Require(sceConvertKeycodeGetCharacter(4, 0, 0, &character) == static_cast(0x80BC001Du)); + Require(character == u'?'); + Require(sceConvertKeycodeGetCharacter(4, 0, 40, &character) == static_cast(0x80BC001Du)); + for (std::uint32_t state : {0u, 0x4000000u}) { + Require(sceConvertKeycodeGetCharacter(4, state, 4, &character) == 0 && character == u'a'); + Require(sceConvertKeycodeGetCharacter(30, state, 4, &character) == 0 && character == u'1'); + Require(sceConvertKeycodeGetCharacter(58, state, 4, &character) == 0 && character == 0); + Require(sceConvertKeycodeGetCharacter(65535, state, 4, &character) == 0 && character == 0); + } + Require(sceConvertKeycodeGetCharacter(4, 0x200, 4, &character) == 0 && character == u'A'); + Require(sceConvertKeycodeGetCharacter(4, 0x20000, 4, &character) == 0 && character == u'A'); + Require(sceConvertKeycodeGetCharacter(4, 0x20200, 4, &character) == 0 && character == u'a'); + Require(sceConvertKeycodeGetCharacter(30, 0x200, 4, &character) == 0 && character == u'!'); + Require(sceConvertKeycodeGetCharacter(89, 0, 4, &character) == 0 && character == 0); + Require(sceConvertKeycodeGetCharacter(89, 0x10000, 4, &character) == 0 && character == u'1'); + Require(sceConvertKeycodeGetCharacter(89, 0x10200, 4, &character) == 0 && character == u'1'); + Require(sceConvertKeycodeGetCharacter(4, 0, 9, &character) == 0 && character == u'q'); + Require(sceConvertKeycodeGetCharacter(28, 0, 2, &character) == 0 && character == u'z'); + Require(sceConvertKeycodeGetCharacter(20, 0x4000, 2, &character) == 0 && character == u'@'); + Require(sceConvertKeycodeGetCharacter(47, 0x20000, 3, &character) == 0 && character == 220); + Require(sceConvertKeycodeGetCharacter(47, 0x20200, 3, &character) == 0 && character == 200); + Require(sceConvertKeycodeGetCharacter(4, 0x4000000, 24, &character) == 0 && character == 0x3141); + Require(sceConvertKeycodeGetCharacter(5, 0x4000000, 30, &character) == 0 && character == 1575); + Require(sceConvertKeycodeGetCharacter(5, 0x4000200, 30, &character) == 0 && character == 1570); + Require(sceConvertKeycodeGetCharacter(30, 0x4000300, 30, &character) == 0 && character == 0x200D); + Require(sceConvertKeycodeGetCharacter(31, 0x4000300, 30, &character) == 0 && character == 0x200C); + Require(sceConvertKeycodeGetCharacter(32, 0x4000300, 30, &character) == 0 && character == 0x200E); + Require(sceConvertKeycodeGetCharacter(33, 0x4000300, 30, &character) == 0 && character == 0x200F); + for (std::uint32_t layout = 1; layout <= 30; ++layout) { + for (std::uint16_t key : {83, 224, 225, 226, 227, 228, 229, 230, 231}) { + Require(sceConvertKeycodeGetCharacter(key, 0xFFFFFFFFu, layout, &character) == 0 && character == 0); + } + } return 0; } diff --git a/core/libs/tests/GuestCoredumpAttach.cpp b/core/libs/tests/GuestCoredumpAttach.cpp index 5c12d2cddc..5d3b7ebb5e 100644 --- a/core/libs/tests/GuestCoredumpAttach.cpp +++ b/core/libs/tests/GuestCoredumpAttach.cpp @@ -1,3 +1,4 @@ +#include "ProcessParametersFixture.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -10,12 +11,18 @@ extern "C" { int APS5_VABI sceCoredumpRegisterCoredumpHandler(uint64_t handler, size_t stackSize, uint64_t context); int APS5_VABI sceCoredumpUnregisterCoredumpHandler(void); -int APS5_VABI sceCoredumpAttachMemoryRegion(uint32_t userValue, const void* mem, size_t size); -int APS5_VABI sceCoredumpAttachUserMemoryFile(uint32_t userValue, const void* mem, size_t size); +int APS5_VABI sceCoredumpAttachMemoryRegion(uint32_t userValue, const void* mem, std::int32_t size); +int APS5_VABI sceCoredumpAttachUserMemoryFile(uint32_t userValue, const void* mem, std::int32_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 { +alignas(16) std::array process{}; +constexpr int InvalidArgument = static_cast(0x81180000u); constexpr int NotInCoredumpHandler = static_cast(0x81180003u); constexpr uint32_t UserFileTruncateIfExceedLimit = 0x80000000u; bool handlerRan = false; @@ -37,14 +44,26 @@ bool Throws(TAttach attach, const char* function) { } int main() { + const std::uint64_t processSize = process.size(); + const std::uint32_t processMagic = 0x4942524f; + const std::uint32_t sdkVersion = 0x06500000; + std::memcpy(process.data(), &processSize, sizeof(processSize)); + std::memcpy(process.data() + 8, &processMagic, sizeof(processMagic)); + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + Require(ProcessParametersFixture::Install(process.data(), process.size())); 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(sceCoredumpGetStopInfoCpu(region.data() + 16, 8) == NotInCoredumpHandler); + 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,26 +74,55 @@ 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")); - Require(Throws([&] { sceCoredumpAttachMemoryRegion(1, region.data(), 0); }, "sceCoredumpAttachMemoryRegion")); - Require(Throws([&] { sceCoredumpAttachMemoryRegion(1, region.data(), SIZE_MAX); }, "sceCoredumpAttachMemoryRegion")); - Require(Throws([&] { sceCoredumpAttachUserMemoryFile(1, nullptr, region.size()); }, "sceCoredumpAttachUserMemoryFile")); - Require(Throws([&] { sceCoredumpAttachUserMemoryFile(1, region.data(), 0); }, "sceCoredumpAttachUserMemoryFile")); - Require(Throws([&] { sceCoredumpAttachUserMemoryFile(1, region.data(), SIZE_MAX); }, "sceCoredumpAttachUserMemoryFile")); - Require(Throws([&] { sceCoredumpGetStopInfoCpu(nullptr, 16); }, "sceCoredumpGetStopInfoCpu")); - Require(Throws([&] { sceCoredumpGetStopInfoCpu(region.data() + 1, 16); }, "sceCoredumpGetStopInfoCpu")); - Require(Throws([&] { sceCoredumpGetStopInfoCpu(reinterpret_cast(UINTPTR_MAX - 7u), 16); }, "sceCoredumpGetStopInfoCpu")); + Require(sceCoredumpAttachMemoryRegion(1, nullptr, region.size()) == NotInCoredumpHandler); + Require(sceCoredumpAttachMemoryRegion(1, region.data(), 0) == NotInCoredumpHandler); + Require(sceCoredumpAttachMemoryRegion(1, region.data(), -1) == 0); + Require(sceCoredumpAttachUserMemoryFile(1, nullptr, region.size()) == NotInCoredumpHandler); + Require(sceCoredumpAttachUserMemoryFile(1, region.data(), 0) == NotInCoredumpHandler); + Require(sceCoredumpAttachUserMemoryFile(1, region.data(), -1) == 0); + Require(sceCoredumpGetStopInfoCpu(nullptr, 16) == InvalidArgument); + Require(sceCoredumpGetStopInfoCpu(region.data() + 1, 16) == NotInCoredumpHandler); + Require(sceCoredumpGetStopInfoCpu(reinterpret_cast(UINTPTR_MAX - 7u), 16) == NotInCoredumpHandler); 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(sceCoredumpGetStopInfoCpu(region.data() + 16, size) == InvalidArgument); + Require(sceCoredumpGetThreadContextInfo(nullptr, 144) == InvalidArgument); + Require(sceCoredumpGetThreadContextInfo(context.data() + 1, 144) == NotInCoredumpHandler); + Require(sceCoredumpGetThreadContextInfo(reinterpret_cast(UINTPTR_MAX - 7u), 144) == NotInCoredumpHandler); + for (const size_t size : {size_t{0}, size_t{1}, size_t{143}, size_t{145}, SIZE_MAX}) + Require(sceCoredumpGetThreadContextInfo(context.data(), size) == InvalidArgument); + for (const auto length : {INT32_MIN, -1, 0, 1, INT32_MAX}) { + const int expected = length < 0 ? 0 : NotInCoredumpHandler; + Require(sceCoredumpAttachMemoryRegion(0, nullptr, length) == expected); + Require(sceCoredumpAttachUserMemoryFile(0, nullptr, length) == expected); + Require(sceCoredumpAttachMemoryRegion(0xffffffffu, region.data(), length) == expected); + Require(sceCoredumpAttachUserMemoryFile(0xffffffffu, region.data(), length) == expected); + } 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/GuestCoredumpUserData.cpp b/core/libs/tests/GuestCoredumpUserData.cpp index 30776ebbcc..d0baa9fd12 100644 --- a/core/libs/tests/GuestCoredumpUserData.cpp +++ b/core/libs/tests/GuestCoredumpUserData.cpp @@ -1,3 +1,4 @@ +#include "ProcessParametersFixture.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -13,10 +14,13 @@ int APS5_VABI sceCoredumpUnregisterCoredumpHandler(void); int64_t APS5_VABI sceCoredumpWriteUserData(const void* data, size_t size); void APS5_VABI sceCoredumpDebugTextOut(const char* str, int len); int APS5_VABI sceCoredumpAttachUserFile(uint32_t userValue, const char* path); -int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const void* mem, size_t size, const char* name); +int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const void* mem, std::int32_t size, const char* name); +int APS5_VABI sceCoredumpSetUserDataType(std::int32_t type); } namespace { +alignas(16) std::array process{}; +constexpr int64_t InvalidArgument = static_cast(0x81180000u); constexpr int64_t NotInCoredumpHandler = static_cast(0x81180003u); bool handlerRan = false; int handlerContext = 0; @@ -55,12 +59,26 @@ void RequireAttachesRefused(const std::array& region) { } int main() { + const std::uint64_t processSize = process.size(); + const std::uint32_t processMagic = 0x4942524f; + const std::uint32_t sdkVersion = 0x06500000; + std::memcpy(process.data(), &processSize, sizeof(processSize)); + std::memcpy(process.data() + 8, &processMagic, sizeof(processMagic)); + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + Require(ProcessParametersFixture::Install(process.data(), process.size())); std::array region{}; for (size_t index = 0; index < region.size(); ++index) region[index] = static_cast(index * 5 + 1); const auto original = region; const char text[] = "coredump text\n"; + Require(sceCoredumpUnregisterCoredumpHandler() == static_cast(0x81180001u)); + Require(sceCoredumpRegisterCoredumpHandler(0, 0x4000, 0) == InvalidArgument); + for (const std::size_t stackSize : {std::size_t{0}, std::size_t{0x3fff}, std::size_t{0x20000000}, SIZE_MAX}) { + Require(sceCoredumpRegisterCoredumpHandler(reinterpret_cast(&Handler), stackSize, 0) == InvalidArgument); + } + Require(sceCoredumpAttachUserFile(0x40000000u, "") == NotInCoredumpHandler); Require(NotInCoredumpHandler < 0); + Require(sceCoredumpSetUserDataType(0) == 0); RequireWritesRefused(region); RequireAttachesRefused(region); sceCoredumpDebugTextOut(text, static_cast(sizeof(text) - 1)); @@ -69,26 +87,46 @@ int main() { sceCoredumpDebugTextOut(nullptr, 0); Require(sceCoredumpRegisterCoredumpHandler(reinterpret_cast(&Handler), 16 * 1024, reinterpret_cast(&handlerContext)) == 0); + Require(sceCoredumpRegisterCoredumpHandler(0, 0, 0) == static_cast(0x81180002u)); + Require(sceCoredumpAttachUserFile(0x40000000u, "") == InvalidArgument); + Require(sceCoredumpAttachMemoryRegionAsUserFile(0x40000000u, nullptr, -1, "") == InvalidArgument); RequireWritesRefused(region); RequireAttachesRefused(region); sceCoredumpDebugTextOut(text, static_cast(sizeof(text) - 1)); Require(!handlerRan); - Require(Throws([&] { sceCoredumpWriteUserData(nullptr, region.size()); }, "sceCoredumpWriteUserData")); - Require(Throws([&] { sceCoredumpWriteUserData(region.data(), 0); }, "sceCoredumpWriteUserData")); - Require(Throws([&] { sceCoredumpWriteUserData(region.data(), SIZE_MAX); }, "sceCoredumpWriteUserData")); + Require(sceCoredumpWriteUserData(nullptr, region.size()) == InvalidArgument); + Require(sceCoredumpWriteUserData(nullptr, 0) == InvalidArgument); + Require(sceCoredumpWriteUserData(region.data(), 0) == NotInCoredumpHandler); + Require(sceCoredumpWriteUserData(region.data(), INT32_MAX) == NotInCoredumpHandler); + Require(sceCoredumpWriteUserData(region.data(), std::size_t{INT32_MAX} + 1) == InvalidArgument); + Require(sceCoredumpWriteUserData(region.data(), SIZE_MAX) == InvalidArgument); Require(Throws([&] { sceCoredumpDebugTextOut(nullptr, 4); }, "sceCoredumpDebugTextOut")); Require(Throws([&] { sceCoredumpDebugTextOut(text, -1); }, "sceCoredumpDebugTextOut")); - Require(Throws([&] { sceCoredumpAttachUserFile(1, nullptr); }, "sceCoredumpAttachUserFile")); - Require(Throws([&] { sceCoredumpAttachMemoryRegionAsUserFile(1, nullptr, region.size(), "a.txt"); }, "sceCoredumpAttachMemoryRegionAsUserFile")); - Require(Throws([&] { sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), 0, "a.txt"); }, "sceCoredumpAttachMemoryRegionAsUserFile")); - Require(Throws([&] { sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), SIZE_MAX, "a.txt"); }, "sceCoredumpAttachMemoryRegionAsUserFile")); - Require(Throws([&] { sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), region.size(), nullptr); }, "sceCoredumpAttachMemoryRegionAsUserFile")); + Require(sceCoredumpAttachUserFile(1, nullptr) == InvalidArgument); + Require(sceCoredumpAttachMemoryRegionAsUserFile(1, nullptr, region.size(), "a.txt") == NotInCoredumpHandler); + Require(sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), 0, "a.txt") == NotInCoredumpHandler); + Require(sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), -1, "a.txt") == 0); + Require(sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), region.size(), nullptr) == InvalidArgument); Require(sceCoredumpUnregisterCoredumpHandler() == 0); RequireWritesRefused(region); RequireAttachesRefused(region); Require(region == original); + Require(sceCoredumpUnregisterCoredumpHandler() == static_cast(0x81180001u)); + Require(sceCoredumpAttachUserFile(0x40000000u, "") == InvalidArgument); + for (const std::uint32_t version : {0x064fffffu, 0x06500000u, 0x12000000u}) { + std::memcpy(process.data() + 16, &version, sizeof(version)); + Require(sceCoredumpRegisterCoredumpHandler(reinterpret_cast(&Handler), 0x4000, 0) == 0); + for (const std::uint32_t flags : {0u, 0xffffu, 0x10000u, 0x40000000u, 0x40000001u, 0x80000000u, 0xc0000000u, UINT32_MAX}) { + const bool invalid = version >= 0x06500000 && (flags & 0xc0000000u) == 0x40000000u; + Require(sceCoredumpAttachUserFile(flags, "") == (invalid ? InvalidArgument : NotInCoredumpHandler)); + Require(sceCoredumpAttachMemoryRegionAsUserFile(flags, nullptr, 0, "") == (invalid ? InvalidArgument : NotInCoredumpHandler)); + Require(sceCoredumpAttachMemoryRegionAsUserFile(flags, nullptr, -1, "") == (invalid ? InvalidArgument : 0)); + Require(sceCoredumpAttachMemoryRegionAsUserFile(flags, nullptr, -1, nullptr) == InvalidArgument); + } + Require(sceCoredumpUnregisterCoredumpHandler() == 0); + } Require(!handlerRan); return 0; } diff --git a/core/libs/tests/GuestCpuTimeClocks.cpp b/core/libs/tests/GuestCpuTimeClocks.cpp index 439a643457..cb7405736e 100644 --- a/core/libs/tests/GuestCpuTimeClocks.cpp +++ b/core/libs/tests/GuestCpuTimeClocks.cpp @@ -8,11 +8,20 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res); int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); int APS5_VABI sceKernelClockGetres(KernelClockid clockId, KernelTimespec* tp); +#ifdef _WIN32 +int APS5_VABI ClockTimeWin32ErrorToGuestErrno_nid_no_patch(std::uint32_t error); +#endif int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); } +#ifdef _WIN32 +static constexpr std::uint32_t WINDOWS_ERROR_NOACCESS = 998; +static constexpr std::uint32_t WINDOWS_ERROR_ACCESS_DENIED = 5; +static constexpr std::uint32_t WINDOWS_ERROR_INVALID_HANDLE = 6; +#endif + static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EFAULT = static_cast(0x8002000E); @@ -98,6 +107,11 @@ static void ProcessClockCountsOtherThreads() { } int main() { +#ifdef _WIN32 + Require(ClockTimeWin32ErrorToGuestErrno_nid_no_patch(WINDOWS_ERROR_NOACCESS) == 14); + Require(ClockTimeWin32ErrorToGuestErrno_nid_no_patch(WINDOWS_ERROR_ACCESS_DENIED) == 1); + Require(ClockTimeWin32ErrorToGuestErrno_nid_no_patch(WINDOWS_ERROR_INVALID_HANDLE) == 5); +#endif KernelTimespec time{-1, -1}; Require(sceKernelClockGettime(GUEST_CLOCK_THREAD_CPUTIME_ID, &time) == SCE_OK); Require(time.tv_sec >= 0 && time.tv_nsec >= 0 && time.tv_nsec < NANOS_PER_SECOND); diff --git a/core/libs/tests/GuestCredentials.cpp b/core/libs/tests/GuestCredentials.cpp index 01cc2d3003..ff69af902b 100644 --- a/core/libs/tests/GuestCredentials.cpp +++ b/core/libs/tests/GuestCredentials.cpp @@ -8,13 +8,20 @@ extern "C" { int APS5_VABI seteuid_nid_postfix(std::uint32_t); int APS5_VABI setegid_nid_postfix(std::uint32_t); int APS5_VABI setuid_nid_postfix(std::uint32_t); +int APS5_VABI setgid_nid_postfix(std::uint32_t); int APS5_VABI setreuid_nid_postfix(std::uint32_t, std::uint32_t); int APS5_VABI setregid_nid_postfix(std::uint32_t, std::uint32_t); +int APS5_VABI setresuid_nid_postfix(std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI setresgid_nid_postfix(std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI setgroups_nid_postfix(int, const std::uint32_t*); int APS5_VABI getgroups_nid_postfix(int, std::uint32_t*); int APS5_VABI getuid_nid_postfix(void); int APS5_VABI geteuid_nid_postfix(void); +int APS5_VABI getgid_nid_postfix(void); int APS5_VABI getegid_nid_postfix(void); +int APS5_VABI issetugid_nid_postfix(void); +int APS5_VABI getresuid_nid_postfix(std::uint32_t*, std::uint32_t*, std::uint32_t*); +int APS5_VABI getresgid_nid_postfix(std::uint32_t*, std::uint32_t*, std::uint32_t*); int* APS5_VABI __error_nid_postfix(); } @@ -40,6 +47,17 @@ static bool ThrowsRe(SetReId function, std::uint32_t r, std::uint32_t e) { return false; } +using SetResId = int (APS5_VABI *)(std::uint32_t, std::uint32_t, std::uint32_t); + +static bool ThrowsRes(SetResId function, std::uint32_t r, std::uint32_t e, std::uint32_t s) { + try { + function(r, e, s); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + constexpr int GuestEfault = 14; constexpr int GuestEinval = 22; constexpr std::uint32_t Unchanged = 0xffffffffu; @@ -60,7 +78,37 @@ int main() { Require(Throws(setegid_nid_postfix, Unchanged)); Require(setuid_nid_postfix(0) == 0); + Require(setgid_nid_postfix(0) == 0); + Require(Throws(setgid_nid_postfix, 1000)); + Require(Throws(setgid_nid_postfix, Unchanged)); Require(getuid_nid_postfix() == 0 && geteuid_nid_postfix() == 0); + Require(getgid_nid_postfix() == 0 && getegid_nid_postfix() == 0 && issetugid_nid_postfix() == 0); + + std::uint32_t ruid = 99, euid = 99, suid = 99; + *__error_nid_postfix() = 13; + Require(getresuid_nid_postfix(&ruid, &euid, &suid) == 0); + Require(ruid == 0 && euid == 0 && suid == 0); + Require(*__error_nid_postfix() == 13); + ruid = 99; + euid = 99; + suid = 99; + Require(getresuid_nid_postfix(&ruid, nullptr, nullptr) == 0 && ruid == 0); + Require(getresuid_nid_postfix(nullptr, &euid, nullptr) == 0 && euid == 0); + Require(getresuid_nid_postfix(nullptr, nullptr, &suid) == 0 && suid == 0); + Require(getresuid_nid_postfix(nullptr, nullptr, nullptr) == 0); + + std::uint32_t rgid = 99, egid = 99, sgid = 99; + *__error_nid_postfix() = 13; + Require(getresgid_nid_postfix(&rgid, &egid, &sgid) == 0); + Require(rgid == 0 && egid == 0 && sgid == 0); + Require(*__error_nid_postfix() == 13); + rgid = 99; + egid = 99; + sgid = 99; + Require(getresgid_nid_postfix(&rgid, nullptr, nullptr) == 0 && rgid == 0); + Require(getresgid_nid_postfix(nullptr, &egid, nullptr) == 0 && egid == 0); + Require(getresgid_nid_postfix(nullptr, nullptr, &sgid) == 0 && sgid == 0); + Require(getresgid_nid_postfix(nullptr, nullptr, nullptr) == 0); Require(setreuid_nid_postfix(Unchanged, Unchanged) == 0); Require(setreuid_nid_postfix(0, Unchanged) == 0); Require(setreuid_nid_postfix(Unchanged, 0) == 0); @@ -75,10 +123,44 @@ int main() { Require(ThrowsRe(setregid_nid_postfix, 1000, 0)); Require(ThrowsRe(setregid_nid_postfix, 0, 1000)); + Require(setresuid_nid_postfix(Unchanged, Unchanged, Unchanged) == 0); + Require(setresuid_nid_postfix(0, Unchanged, Unchanged) == 0); + Require(setresuid_nid_postfix(Unchanged, 0, Unchanged) == 0); + Require(setresuid_nid_postfix(Unchanged, Unchanged, 0) == 0); + Require(setresuid_nid_postfix(0, 0, 0) == 0); + Require(ThrowsRes(setresuid_nid_postfix, 1000, 0, 0)); + Require(ThrowsRes(setresuid_nid_postfix, 0, 1000, 0)); + Require(ThrowsRes(setresuid_nid_postfix, 0, 0, 1000)); + Require(ThrowsRes(setresuid_nid_postfix, 1000, Unchanged, Unchanged)); + Require(ThrowsRes(setresuid_nid_postfix, Unchanged, 1000, Unchanged)); + Require(ThrowsRes(setresuid_nid_postfix, Unchanged, Unchanged, 1000)); + + Require(setresgid_nid_postfix(Unchanged, Unchanged, Unchanged) == 0); + Require(setresgid_nid_postfix(0, Unchanged, Unchanged) == 0); + Require(setresgid_nid_postfix(Unchanged, 0, Unchanged) == 0); + Require(setresgid_nid_postfix(Unchanged, Unchanged, 0) == 0); + Require(setresgid_nid_postfix(0, 0, 0) == 0); + Require(ThrowsRes(setresgid_nid_postfix, 1000, 0, 0)); + Require(ThrowsRes(setresgid_nid_postfix, 0, 1000, 0)); + Require(ThrowsRes(setresgid_nid_postfix, 0, 0, 1000)); + Require(ThrowsRes(setresgid_nid_postfix, 1000, Unchanged, Unchanged)); + Require(ThrowsRes(setresgid_nid_postfix, Unchanged, 1000, Unchanged)); + Require(ThrowsRes(setresgid_nid_postfix, Unchanged, Unchanged, 1000)); + std::array groups{}; Require(getgroups_nid_postfix(0, nullptr) == 1); Require(getgroups_nid_postfix(static_cast(groups.size()), groups.data()) == 1 && groups[0] == 0); Require(groups[0] == static_cast(getegid_nid_postfix())); + *__error_nid_postfix() = 13; + RequireFailure(setgroups_nid_postfix(1, reinterpret_cast(1)), GuestEfault); + *__error_nid_postfix() = 13; + RequireFailure(getgroups_nid_postfix(1, reinterpret_cast(1)), GuestEfault); + Require(getgroups_nid_postfix(0, nullptr) == 1); + + *__error_nid_postfix() = 13; + RequireFailure(getresuid_nid_postfix(reinterpret_cast(1), nullptr, nullptr), GuestEfault); + *__error_nid_postfix() = 13; + RequireFailure(getresgid_nid_postfix(nullptr, reinterpret_cast(1), nullptr), GuestEfault); const std::array wanted{0, 5, 7}; Require(setgroups_nid_postfix(static_cast(wanted.size()), wanted.data()) == 0); diff --git a/core/libs/tests/GuestCrt.cpp b/core/libs/tests/GuestCrt.cpp index f6600788de..d2fe45d744 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); @@ -163,10 +189,20 @@ int main() { Require(asctime_s_nid_postfix(text, sizeof(text), &known) == 0); Require(std::strcmp(text, "Thu Jan 1 00:00:00 1970\n") == 0); known.tm_wday = 7; - Require(sceAmprAsctimeS(text, sizeof(text), &known) == EINVAL); + text[0] = 'x'; + Require(sceAmprAsctimeS(text, sizeof(text), &known) == EINVAL && text[0] == '\0'); known.tm_wday = 4; + known.tm_year = 9000; + text[0] = 'x'; + Require(sceAmprAsctimeS(text, sizeof(text), &known) == EINVAL && text[0] == '\0'); + known.tm_year = 70; + known.tm_mday = 32; + text[0] = 'x'; + Require(sceAmprAsctimeS(text, sizeof(text), &known) == EINVAL && text[0] == '\0'); + known.tm_mday = 1; Require(sceAmprAsctimeS(nullptr, sizeof(text), &known) == EINVAL); - Require(sceAmprAsctimeS(text, sizeof(text), nullptr) == EINVAL); + text[0] = 'x'; + Require(sceAmprAsctimeS(text, sizeof(text), nullptr) == EINVAL && text[0] == '\0'); Require(sceAmprAsctimeS(text, 25, &known) == EINVAL); const std::int64_t epoch = 0; Require(sceAmprCtimeS(text, sizeof(text), &epoch) == 0); @@ -175,7 +211,8 @@ int main() { Require(ctime_s_nid_postfix(libcText, sizeof(libcText), &epoch) == 0); Require(std::strcmp(libcText, text) == 0); Require(sceAmprCtimeS(text, 25, &epoch) == EINVAL); - Require(sceAmprCtimeS(text, sizeof(text), nullptr) == EINVAL); + text[0] = 'x'; + Require(sceAmprCtimeS(text, sizeof(text), nullptr) == EINVAL && text[0] == '\0'); char line[] = "a,b,,c"; char* context = nullptr; diff --git a/core/libs/tests/GuestCryptoMemcmp.cpp b/core/libs/tests/GuestCryptoMemcmp.cpp new file mode 100644 index 0000000000..578b4a4645 --- /dev/null +++ b/core/libs/tests/GuestCryptoMemcmp.cpp @@ -0,0 +1,125 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#endif + +extern "C" int APS5_VABI CRYPTO_memcmp_nid_postfix(const void*, const void*, std::size_t); + +namespace { + +void Require(bool value) { + if (!value) { + std::fprintf(stderr, "CRYPTO_memcmp check failed\n"); + std::abort(); + } +} + +class GuardedPage { +public: + explicit GuardedPage(bool different) { +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + pageSize = info.dwPageSize; + memory = static_cast(VirtualAlloc(nullptr, pageSize * 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(memory != nullptr); + DWORD previous; + Require(VirtualProtect(memory + pageSize, pageSize, PAGE_NOACCESS, &previous) != 0); +#else + const long nativePageSize = sysconf(_SC_PAGESIZE); + Require(nativePageSize > 0); + pageSize = static_cast(nativePageSize); + void* allocation = mmap(nullptr, pageSize * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(allocation != MAP_FAILED); + memory = static_cast(allocation); + Require(mprotect(memory + pageSize, pageSize, PROT_NONE) == 0); +#endif + std::memset(memory, 0xA5, pageSize); + if (different) memory[pageSize - 1] ^= 0xFF; +#ifdef _WIN32 + Require(VirtualProtect(memory, pageSize, PAGE_READONLY, &previous) != 0); +#else + Require(mprotect(memory, pageSize, PROT_READ) == 0); +#endif + } + + GuardedPage(const GuardedPage&) = delete; + GuardedPage& operator=(const GuardedPage&) = delete; + + ~GuardedPage() { +#ifdef _WIN32 + VirtualFree(memory, 0, MEM_RELEASE); +#else + munmap(memory, pageSize * 2); +#endif + } + + const unsigned char* Tail(std::size_t length) const { + Require(length <= pageSize); + return memory + pageSize - length; + } + +private: + unsigned char* memory = nullptr; + std::size_t pageSize = 0; +}; + +} + +int main() { + Require(CRYPTO_memcmp_nid_postfix(nullptr, nullptr, 0) == 0); + unsigned char byte = 0xA5; + Require(CRYPTO_memcmp_nid_postfix(nullptr, &byte, 0) == 0); + Require(CRYPTO_memcmp_nid_postfix(&byte, nullptr, 0) == 0); + for (unsigned int left = 0; left < 256; ++left) { + for (unsigned int right = 0; right < 256; ++right) { + const unsigned char a = static_cast(left); + const unsigned char b = static_cast(right); + Require((CRYPTO_memcmp_nid_postfix(&a, &b, 1) == 0) == (left == right)); + } + } + std::array first; + for (std::size_t index = 0; index < first.size(); ++index) first[index] = static_cast(index * 37); + auto second = first; + const auto original = first; + Require(CRYPTO_memcmp_nid_postfix(first.data(), first.data(), first.size()) == 0); + Require(CRYPTO_memcmp_nid_postfix(first.data() + 1, second.data() + 1, 65) == 0); + for (std::size_t index = 1; index <= 65; ++index) { + second[index] ^= 0x80; + const auto changed = second; + Require(CRYPTO_memcmp_nid_postfix(first.data() + 1, second.data() + 1, 65) != 0); + Require(CRYPTO_memcmp_nid_postfix(second.data() + 1, first.data() + 1, 65) != 0); + Require(first == original && second == changed); + second[index] ^= 0x80; + } + second[32] ^= 0x80; + Require(CRYPTO_memcmp_nid_postfix(first.data(), second.data(), 32) == 0); + Require(CRYPTO_memcmp_nid_postfix(first.data(), second.data(), 33) != 0); + second = first; + second[1] ^= 0x80; + second[65] ^= 0x80; + Require(CRYPTO_memcmp_nid_postfix(first.data(), second.data(), first.size()) != 0); + first.fill(0xCC); + Require(CRYPTO_memcmp_nid_postfix(first.data(), first.data() + 1, first.size() - 1) == 0); + first[32] = 0xCD; + const auto overlap = first; + Require(CRYPTO_memcmp_nid_postfix(first.data(), first.data() + 1, first.size() - 1) != 0); + Require(first == overlap); + const GuardedPage equal(false); + const GuardedPage other(false); + const GuardedPage different(true); + for (const std::size_t length : {0, 1, 2, 7, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129}) { + Require(CRYPTO_memcmp_nid_postfix(equal.Tail(length), other.Tail(length), length) == 0); + Require((CRYPTO_memcmp_nid_postfix(equal.Tail(length), different.Tail(length), length) == 0) == (length == 0)); + } +} diff --git a/core/libs/tests/GuestDatagramReceive.cpp b/core/libs/tests/GuestDatagramReceive.cpp index 22cf4caaf8..2c7bc9e0c5 100644 --- a/core/libs/tests/GuestDatagramReceive.cpp +++ b/core/libs/tests/GuestDatagramReceive.cpp @@ -40,8 +40,12 @@ static void Require(bool condition, const char* message) { } static void Check(bool net, bool from, int family) { + int* error = net ? sceNetErrnoLoc() : __error_nid_postfix(); const auto create = [&] { return net ? sceNetSocket(nullptr, family, 2, 17) : socket_nid_postfix(family, 2, 17); }; - const int receiver = create(), sender = create(); + const auto closeSocket = [&](int descriptor) { return net ? sceNetSocketClose(descriptor) : close_nid_postfix(descriptor); }; + const int receiver = create(); + if (receiver < 0 && family == 28 && *error == 47) return; + const int sender = create(); Require(receiver >= 0 && sender >= 0, "create UDP sockets"); std::array destination{}; const std::uint32_t addressSize = family == 2 ? 16 : 28; @@ -53,7 +57,12 @@ static void Check(bool net, bool from, int family) { return net ? sceNetBind_nid_postfix(descriptor, destination.data(), addressSize) : bind_nid_postfix(descriptor, destination.data(), addressSize); }; - Require(bind(receiver) == 0 && bind(sender) == 0, "bind loopback sockets"); + const int bound = bind(receiver); + if (bound < 0 && family == 28 && *error == 49) { + Require(closeSocket(sender) == 0 && closeSocket(receiver) == 0, "close sockets without IPv6 loopback"); + return; + } + Require(bound == 0 && bind(sender) == 0, "bind loopback sockets"); auto senderAddress = destination; std::uint32_t size = addressSize; Require((net ? sceNetGetsockname(sender, senderAddress.data(), &size) : @@ -66,7 +75,6 @@ static void Check(bool net, bool from, int family) { const int enabled = 1; Require((net ? sceNetSetsockopt(receiver, 0xffff, 0x1200, &enabled, sizeof(enabled)) : fcntl_nid_postfix(receiver, 4, 6)) == 0, "make receiver nonblocking"); - int* error = net ? sceNetErrnoLoc() : __error_nid_postfix(); const auto receive = [&](void* buffer, std::size_t length, int flags, void* address, std::uint32_t* addressLength) { if (net) return from ? sceNetRecvfrom(receiver, buffer, length, flags, address, addressLength) : sceNetRecv(receiver, buffer, length, flags); @@ -132,8 +140,8 @@ static void Check(bool net, bool from, int family) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } while (std::chrono::steady_clock::now() < deadline); Require(result == 0 && byte == '!', "empty datagram"); - Require((net ? sceNetSocketClose(receiver) : close_nid_postfix(receiver)) == 0, "close receiver"); - Require((net ? sceNetSocketClose(sender) : close_nid_postfix(sender)) == 0, "close sender"); + Require(closeSocket(receiver) == 0, "close receiver"); + Require(closeSocket(sender) == 0, "close sender"); } int main(int argc, char** argv) { diff --git a/core/libs/tests/GuestDinkumwareAtomics.cpp b/core/libs/tests/GuestDinkumwareAtomics.cpp new file mode 100644 index 0000000000..8e45d76902 --- /dev/null +++ b/core/libs/tests/GuestDinkumwareAtomics.cpp @@ -0,0 +1,120 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +void APS5_VABI _Atomic_store_1_nid_postfix(volatile std::uint8_t*, std::uint8_t, int); +void APS5_VABI _Atomic_store_8_nid_postfix(volatile std::uint64_t*, std::uint64_t, int); +std::uint16_t APS5_VABI _Atomic_load_2_nid_postfix(volatile std::uint16_t*, int); +std::uint64_t APS5_VABI _Atomic_load_8_nid_postfix(volatile std::uint64_t*, int); +std::uint32_t APS5_VABI _Atomic_exchange_4_nid_postfix(volatile std::uint32_t*, std::uint32_t, int); +std::uint64_t APS5_VABI _Atomic_exchange_8_nid_postfix(volatile std::uint64_t*, std::uint64_t, int); +int APS5_VABI _Atomic_compare_exchange_strong_2_nid_postfix(volatile std::uint16_t*, std::uint16_t*, std::uint16_t, int, int); +int APS5_VABI _Atomic_compare_exchange_weak_8_nid_postfix(volatile std::uint64_t*, std::uint64_t*, std::uint64_t, int, int); +std::uint8_t APS5_VABI _Atomic_fetch_add_1_nid_postfix(volatile std::uint8_t*, std::uint8_t, int); +std::uint64_t APS5_VABI _Atomic_fetch_add_8_nid_postfix(volatile std::uint64_t*, std::uint64_t, int); +std::uint16_t APS5_VABI _Atomic_fetch_sub_2_nid_postfix(volatile std::uint16_t*, std::uint16_t, int); +std::uint32_t APS5_VABI _Atomic_fetch_and_4_nid_postfix(volatile std::uint32_t*, std::uint32_t, int); +std::uint8_t APS5_VABI _Atomic_fetch_or_1_nid_postfix(volatile std::uint8_t*, std::uint8_t, int); +std::uint64_t APS5_VABI _Atomic_fetch_xor_8_nid_postfix(volatile std::uint64_t*, std::uint64_t, int); +int APS5_VABI _Atomic_is_lock_free_4_nid_postfix(); +int APS5_VABI _Atomic_flag_test_and_set_nid_postfix(volatile unsigned char*, int); +void APS5_VABI _Atomic_flag_clear_nid_postfix(volatile unsigned char*, int); +void APS5_VABI _Atomic_thread_fence_nid_postfix(int); +void APS5_VABI _Atomic_signal_fence_nid_postfix(int); +void APS5_VABI _Atomic_copy_nid_postfix(volatile unsigned char*, std::size_t, volatile void*, const volatile void*, int); +void APS5_VABI _Atomic_exchange_nid_postfix(volatile unsigned char*, std::size_t, volatile void*, volatile void*, int); +int APS5_VABI _Atomic_compare_exchange_strong_nid_postfix(volatile unsigned char*, std::size_t, volatile void*, void*, const void*, int, int); +int APS5_VABI _Atomic_compare_exchange_weak_nid_postfix(volatile unsigned char*, std::size_t, volatile void*, void*, const void*, int, int); +} + +static void Require(bool value) { if (!value) std::abort(); } + +constexpr int Relaxed = 0, Acquire = 2, Release = 3, AcqRel = 4, SeqCst = 5; + +static void CheckTyped() { + std::uint8_t byte = 0; + _Atomic_store_1_nid_postfix(&byte, 7, SeqCst); + Require(byte == 7 && _Atomic_fetch_add_1_nid_postfix(&byte, 250, Relaxed) == 7 && byte == 1); + Require(_Atomic_fetch_or_1_nid_postfix(&byte, 0x80, AcqRel) == 1 && byte == 0x81); + std::uint16_t half = 5; + Require(_Atomic_load_2_nid_postfix(&half, Release) == 5 && _Atomic_fetch_sub_2_nid_postfix(&half, 6, SeqCst) == 5 && half == 0xffff); + std::uint16_t expectedHalf = 1; + Require(_Atomic_compare_exchange_strong_2_nid_postfix(&half, &expectedHalf, 2, SeqCst, Relaxed) == 0 && expectedHalf == 0xffff); + Require(_Atomic_compare_exchange_strong_2_nid_postfix(&half, &expectedHalf, 2, SeqCst, Relaxed) == 1 && half == 2); + std::uint32_t word = 0xff0f; + Require(_Atomic_fetch_and_4_nid_postfix(&word, 0xf0ff, Acquire) == 0xff0f && word == 0xf00f); + Require(_Atomic_exchange_4_nid_postfix(&word, 9, 6) == 0xf00f && word == 9); + std::uint64_t wide = 0; + _Atomic_store_8_nid_postfix(&wide, 0x123456789ull, Release); + Require(_Atomic_load_8_nid_postfix(&wide, Acquire) == 0x123456789ull); + Require(_Atomic_load_8_nid_postfix(&wide, Release) == 0 && _Atomic_load_8_nid_postfix(&wide, AcqRel) == 0); + Require(_Atomic_fetch_add_8_nid_postfix(&wide, 1, SeqCst) == 0x123456789ull && wide == 0x12345678aull); + Require(_Atomic_fetch_add_8_nid_postfix(&wide, 1, 6) == 0 && wide == 0x12345678aull); + Require(_Atomic_exchange_8_nid_postfix(&wide, 1, 7) == 0 && wide == 0x12345678aull); + Require(_Atomic_fetch_xor_8_nid_postfix(&wide, 0xf, Relaxed) == 0x12345678aull && wide == 0x123456785ull); + std::uint64_t expectedWide = 0x123456785ull; + Require(_Atomic_compare_exchange_weak_8_nid_postfix(&wide, &expectedWide, 3, SeqCst, SeqCst) == 1 && wide == 3); + Require(_Atomic_is_lock_free_4_nid_postfix() == 1); +} + +static void CheckFlagsAndFences() { + unsigned char flag = 0; + Require(_Atomic_flag_test_and_set_nid_postfix(&flag, SeqCst) == 0 && flag == 1); + Require(_Atomic_flag_test_and_set_nid_postfix(&flag, Relaxed) == 1); + _Atomic_flag_clear_nid_postfix(&flag, Release); + Require(flag == 0); + _Atomic_thread_fence_nid_postfix(SeqCst); + _Atomic_signal_fence_nid_postfix(SeqCst); +} + +struct Wide { + std::uint64_t parts[3]; +}; + +static void CheckGeneric() { + unsigned char lock = 0; + Wide target{{1, 2, 3}}, source{{4, 5, 6}}, expected{{1, 2, 3}}; + _Atomic_copy_nid_postfix(&lock, sizeof(Wide), &target, &source, SeqCst); + Require(std::memcmp(&target, &source, sizeof(Wide)) == 0 && lock == 0); + Wide other{{7, 8, 9}}; + _Atomic_exchange_nid_postfix(&lock, sizeof(Wide), &target, &other, SeqCst); + Require(target.parts[0] == 7 && other.parts[0] == 4 && lock == 0); + const Wide desired{{10, 11, 12}}; + Require(_Atomic_compare_exchange_strong_nid_postfix(&lock, sizeof(Wide), &target, &expected, &desired, SeqCst, SeqCst) == 0); + Require(std::memcmp(&expected, &target, sizeof(Wide)) == 0 && lock == 0); + Require(_Atomic_compare_exchange_weak_nid_postfix(&lock, sizeof(Wide), &target, &expected, &desired, SeqCst, SeqCst) == 1); + Require(std::memcmp(&target, &desired, sizeof(Wide)) == 0 && lock == 0); +} + +static void CheckConcurrentGeneric() { + unsigned char lock = 0; + Wide counter{{0, 0, 0}}; + constexpr int Iterations = 20000; + auto worker = [&] { + for (int i = 0; i < Iterations; ++i) { + Wide expected; + _Atomic_copy_nid_postfix(&lock, sizeof(Wide), &expected, &counter, SeqCst); + Wide next; + do { + next = Wide{{expected.parts[0] + 1, expected.parts[1] + 2, expected.parts[2] + 3}}; + } while (!_Atomic_compare_exchange_strong_nid_postfix(&lock, sizeof(Wide), &counter, &expected, &next, SeqCst, SeqCst)); + } + }; + std::thread first(worker); + std::thread second(worker); + first.join(); + second.join(); + Require(counter.parts[0] == 2 * Iterations && counter.parts[1] == 4 * Iterations && counter.parts[2] == 6 * Iterations); +} + +int main() { + CheckTyped(); + CheckFlagsAndFences(); + CheckGeneric(); + CheckConcurrentGeneric(); + return 0; +} diff --git a/core/libs/tests/GuestDinkumwareInternals.cpp b/core/libs/tests/GuestDinkumwareInternals.cpp new file mode 100644 index 0000000000..aa660dabdf --- /dev/null +++ b/core/libs/tests/GuestDinkumwareInternals.cpp @@ -0,0 +1,88 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +double APS5_VABI _Stod_nid_postfix(const char* text, char** end, std::int64_t pten); +long long APS5_VABI _Stoll_nid_postfix(const char* text, char** end, int base); +double APS5_VABI _Sin_nid_postfix(double x, unsigned int quadrant); +float APS5_VABI _FSin_nid_postfix(float x, unsigned int quadrant); +long double APS5_VABI _LSin_nid_postfix(long double x, unsigned int quadrant); +double APS5_VABI _Log_nid_postfix(double x, int base); +float APS5_VABI _FLog_nid_postfix(float x, int base); +} + +namespace { + +constexpr int RangeError = 34; + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "Dinkumware internals: %s\n", message); + std::exit(1); + } +} + +void CheckStod(const char* text, std::int64_t pten, double expected, long consumed, int error, const char* message) { + *__error_nid_postfix() = 0; + char* end = nullptr; + const double value = _Stod_nid_postfix(text, &end, pten); + const bool same = std::isnan(expected) ? std::isnan(value) : value == expected && std::signbit(value) == std::signbit(expected); + Require(same && end - text == consumed && *__error_nid_postfix() == error, message); +} + +void TestStod() { + CheckStod("1.5", 0, 1.5, 3, 0, "unscaled decimal"); + CheckStod("1.5", 1, 15.0, 3, 0, "decimal scaled up"); + CheckStod("12", -2, 0.12, 2, 0, "decimal scaled down"); + CheckStod(" -2.25e3xyz", 300, -0x1.a3f7f806a8135p+1007, 9, 0, "the scale is applied before rounding"); + CheckStod("1e-400", 300, 1e-100, 6, 0, "an underflowing text scaled back into range"); + CheckStod("1e-400", 0, 0.0, 6, RangeError, "an unscaled underflow"); + CheckStod("1e300", 100, HUGE_VAL, 5, RangeError, "a scale that overflows"); + CheckStod("1e300", 8, 1e308, 5, 0, "a scale up to the largest decades"); + CheckStod("5e-324", -1, 0.0, 6, RangeError, "a scale that underflows to zero"); + CheckStod("1", 2147483647, HUGE_VAL, 1, RangeError, "the largest scale"); + CheckStod("5", std::int64_t{1} << 40, HUGE_VAL, 1, RangeError, "a scale above 32 bits"); + CheckStod("1e400", -100, 1e300, 5, 0, "an overflowing text scaled back into range"); + CheckStod("0.01e41", -2, 1e37, 7, 0, "a negative scale from leading fraction zeros"); + CheckStod("1e-2147483648", -1, 0.0, 13, RangeError, "the smallest exponent and scale"); + CheckStod("-0", 5, -0.0, 2, 0, "negative zero keeps its sign"); + CheckStod("infinity", 3, HUGE_VAL, 8, 0, "infinity is not scaled"); + CheckStod("nan", 3, NAN, 3, 0, "NaN is not scaled"); + CheckStod("1e", 2, 100.0, 1, 0, "an exponent marker without digits"); + CheckStod(".5", 2, 50.0, 2, 0, "a fraction without integer digits"); + CheckStod("0.3", -1, 0.03, 3, 0, "0.3 scaled down rounds as 0.03"); + CheckStod("123456789012345678901234567890", -20, 1234567890.1234567, 30, 0, "a long digit string"); + CheckStod("abc", 4, 0.0, 0, 0, "no number"); + CheckStod("0x1.8p0", 0, 1.5, 7, 0, "unscaled hexadecimal"); +} + +void TestStoll() { + char* end = nullptr; + *__error_nid_postfix() = 0; + Require(_Stoll_nid_postfix("-0x1F", &end, 0) == -31 && std::strcmp(end, "") == 0, "_Stoll base detection"); + Require(_Stoll_nid_postfix("+42abc", &end, 10) == 42 && std::strcmp(end, "abc") == 0, "_Stoll stops at the first non-digit"); + Require(_Stoll_nid_postfix(" z", &end, 36) == 35, "_Stoll base 36"); + Require(*__error_nid_postfix() == 0, "_Stoll set errno without an overflow"); + Require(_Stoll_nid_postfix("9223372036854775808", &end, 10) == 9223372036854775807ll && *__error_nid_postfix() == RangeError, "_Stoll saturates"); +} + +void TestDispatch() { + Require(_Sin_nid_postfix(0.5, 0) == std::sin(0.5) && _Sin_nid_postfix(0.5, 1) == std::cos(0.5) && _Sin_nid_postfix(0.5, 7) == std::cos(0.5), "_Sin"); + Require(_FSin_nid_postfix(0.5f, 0) == std::sin(0.5f) && _FSin_nid_postfix(0.5f, 2) == std::cos(0.5f), "_FSin"); + Require(_LSin_nid_postfix(0.5L, 0) == std::sin(0.5L) && _LSin_nid_postfix(0.5L, 3) == std::cos(0.5L), "_LSin"); + Require(_Log_nid_postfix(8.0, 0) == std::log(8.0) && _Log_nid_postfix(1000.0, 1) == 3.0 && _Log_nid_postfix(1000.0, 5) == 3.0 && _Log_nid_postfix(8.0, -1) == 3.0, "_Log"); + Require(_FLog_nid_postfix(8.0f, 0) == std::log(8.0f) && _FLog_nid_postfix(100.0f, 1) == 2.0f && _FLog_nid_postfix(8.0f, -3) == 3.0f, "_FLog"); +} + +} + +int main() { + TestStod(); + TestStoll(); + TestDispatch(); +} diff --git a/core/libs/tests/GuestDirectory.cpp b/core/libs/tests/GuestDirectory.cpp index 8e358e7c86..4df4394622 100644 --- a/core/libs/tests/GuestDirectory.cpp +++ b/core/libs/tests/GuestDirectory.cpp @@ -1,6 +1,9 @@ #include "prx/libc/include/GuestDirectory.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" #include +#include +#include #include #include #include @@ -8,32 +11,96 @@ #include #include #include +#include +#ifdef _WIN32 +#include +#endif extern "C" { void* APS5_VABI opendir_nid_postfix(const char*); GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void*); int APS5_VABI readdir_r_nid_postfix(void*, GuestDirectoryEntry*, GuestDirectoryEntry**); int APS5_VABI closedir_nid_postfix(void*); void APS5_VABI rewinddir_nid_postfix(void*); +std::int64_t APS5_VABI telldir_nid_postfix(void*); +void APS5_VABI seekdir_nid_postfix(void*, std::int64_t); int* APS5_VABI __error_nid_postfix(); +int APS5_VABI lstat_nid_postfix(const char*, FileStat*); } static void Require(bool value) { if (!value) std::abort(); } +static std::map ReadFileNumbers(void* directory) { + std::map numbers; + rewinddir_nid_postfix(directory); + while (auto* entry = readdir_nid_postfix(directory)) { + Require(entry->fileNumber != 0); + numbers[entry->name] = entry->fileNumber; + } + return numbers; +} +static std::uint32_t FileNumber(const std::filesystem::path& path) { + FileStat status{}; + Require(lstat_nid_postfix(path.string().c_str(), &status) == 0); + return static_cast(status.st_ino); +} +static bool CreateLinks(const std::filesystem::path& root) { +#ifdef _WIN32 + if (CreateSymbolicLinkW((root / "file-link").c_str(), L"sample.txt", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { + Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); + std::fprintf(stderr, "Windows readdir link checks skipped: creating symbolic links needs Developer Mode or administrator rights\n"); + return false; + } + Require(CreateSymbolicLinkW((root / "directory-link").c_str(), L"subdirectory", + SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); +#else + std::filesystem::create_symlink("sample.txt", root / "file-link"); + std::filesystem::create_directory_symlink("subdirectory", root / "directory-link"); +#endif + return true; +} int main() { const auto root = std::filesystem::path("anyps5-directory-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); Require(std::filesystem::create_directory(root / "subdirectory")); { std::ofstream file(root / "sample.txt"); file << "test"; } + const bool links = CreateLinks(root); + std::vector numbered{"sample.txt", "subdirectory"}; + if (links) numbered.insert(numbered.end(), {"file-link", "directory-link"}); void* directory = opendir_nid_postfix(root.string().c_str()); Require(directory != nullptr); std::map entries; + std::map fileNumbers; *__error_nid_postfix() = 13; while (auto* entry = readdir_nid_postfix(directory)) { Require(entry->nameLength == std::strlen(entry->name)); Require(entry->recordLength == 8 + ((entry->nameLength + 4) & ~3)); Require(entry->recordLength <= sizeof(*entry)); entries[entry->name] = entry->type; + Require(entry->fileNumber != 0); + fileNumbers[entry->name] = entry->fileNumber; } Require(*__error_nid_postfix() == 13); // EOF preserves errno. + for (const char* name : numbered) Require(fileNumbers.at(name) == FileNumber(root / name)); + if (links) Require(fileNumbers.at("file-link") != fileNumbers.at("sample.txt")); + { std::ofstream file(root / "created.txt"); file << "new"; } + auto changed = ReadFileNumbers(directory); + Require(changed.at("created.txt") == FileNumber(root / "created.txt")); + Require(changed.at("sample.txt") == fileNumbers.at("sample.txt")); + Require(std::filesystem::remove(root / "created.txt")); + changed = ReadFileNumbers(directory); + Require(changed.count("created.txt") == 0 && changed == fileNumbers); +#ifdef _WIN32 + const HANDLE pending = CreateFileW((root / "pending.txt").c_str(), GENERIC_WRITE | DELETE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, CREATE_NEW, 0, nullptr); + Require(pending != INVALID_HANDLE_VALUE); + FILE_DISPOSITION_INFO disposition{TRUE}; + Require(SetFileInformationByHandle(pending, FileDispositionInfo, &disposition, sizeof(disposition)) != 0); + FileStat pendingStatus{}; + Require(lstat_nid_postfix((root / "pending.txt").string().c_str(), &pendingStatus) == -1); + changed = ReadFileNumbers(directory); + Require(changed.count("pending.txt") == 1 && changed.at("sample.txt") == fileNumbers.at("sample.txt")); + Require(CloseHandle(pending) != 0); + Require(ReadFileNumbers(directory) == fileNumbers); +#endif Require(entries.at("sample.txt") == 8); Require(entries.at("subdirectory") == 4); Require(entries.at(".") == 4 && entries.at("..") == 4); @@ -85,6 +152,23 @@ int main() { nullResultThrows = true; } Require(nullResultThrows); + rewinddir_nid_postfix(directory); + Require(readdir_nid_postfix(directory) != nullptr); + const std::int64_t second = telldir_nid_postfix(directory); + Require(second != -1); + auto* entryAfterTell = readdir_nid_postfix(directory); + Require(entryAfterTell != nullptr); + const std::string secondName = entryAfterTell->name; + while (readdir_nid_postfix(directory)) {} + seekdir_nid_postfix(directory, second); + auto* entryAfterSeek = readdir_nid_postfix(directory); + Require(entryAfterSeek != nullptr && secondName == entryAfterSeek->name); + Require(entryAfterSeek->fileNumber == fileNumbers.at(secondName)); + *__error_nid_postfix() = 0; + Require(telldir_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + seekdir_nid_postfix(nullptr, second); + Require(*__error_nid_postfix() == 9); Require(closedir_nid_postfix(directory) == 0); Require(opendir_nid_postfix((root / "missing").string().c_str()) == nullptr); Require(*__error_nid_postfix() == 2); @@ -114,6 +198,10 @@ int main() { Require(names.at(std::string(file.begin(), file.end())) == 8 && names.size() == 3); Require(closedir_nid_postfix(unicode) == 0); std::filesystem::remove_all(root / std::filesystem::path(folder)); + if (links) { + std::filesystem::remove(root / "file-link"); + std::filesystem::remove(root / "directory-link"); + } std::filesystem::remove(root / "sample.txt"); std::filesystem::remove(root / "subdirectory"); std::filesystem::remove(root); diff --git a/core/libs/tests/GuestDirectoryBuffer.cpp b/core/libs/tests/GuestDirectoryBuffer.cpp index bfd1beb0dd..e5d0c6c122 100644 --- a/core/libs/tests/GuestDirectoryBuffer.cpp +++ b/core/libs/tests/GuestDirectoryBuffer.cpp @@ -18,6 +18,11 @@ int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); int APS5_VABI sceKernelGetdents(int, char*, int); int APS5_VABI sceKernelGetdirentries(int, char*, int, std::int64_t*); +int APS5_VABI 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 sceKernelMprotect(void*, std::size_t, int); +int APS5_VABI sceKernelMunmap(void*, std::size_t); +int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); } static void Check(bool value, int line) { @@ -66,7 +71,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); } @@ -91,6 +96,37 @@ static void CheckOversizedName() { std::filesystem::remove_all(root); } +static void CheckReadOnlyDestination() { + constexpr std::size_t page = 0x4000; + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-directory-readonly-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + { std::ofstream file(root / "entry.bin"); Require(static_cast(file)); } + int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &physical) == 0); + void* mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, page, 3, 0, physical, 0) == 0 && mapping != nullptr); + std::int64_t base = -1; + Require(sceKernelGetdirentries(directory, static_cast(mapping), 4096, &base) > 0); + Require(sceKernelClose(directory) == 0); + directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + Require(sceKernelMprotect(mapping, page, 1) == 0); + Require(sceKernelGetdirentries(directory, static_cast(mapping), 4096, &base) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetdirentries(12345, static_cast(mapping), 4096, &base) == SCE_KERNEL_ERROR_EBADF); + std::array buffer{}; + Require(sceKernelGetdirentries(directory, buffer.data(), static_cast(buffer.size()), + static_cast(mapping)) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelMprotect(mapping, page, 3) == 0); + Require(sceKernelGetdirentries(directory, buffer.data(), static_cast(buffer.size()), &base) > 0); + Require(sceKernelMunmap(mapping, page) == 0); + Require(sceKernelReleaseDirectMemory(physical, page) == 0); + Require(sceKernelClose(directory) == 0); + std::filesystem::remove_all(root); +} + int main() { CheckName(u8"a-long-directory-entry-name.bin"); CheckName(u8"caf\u00e9-music.bin"); @@ -100,4 +136,5 @@ int main() { for (int i = 0; i < 85; ++i) longest += u8"\u30bb"; CheckName(longest); CheckOversizedName(); + CheckReadOnlyDestination(); } 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/GuestDirectoryLinkTypes.cpp b/core/libs/tests/GuestDirectoryLinkTypes.cpp new file mode 100644 index 0000000000..344b0c3a1c --- /dev/null +++ b/core/libs/tests/GuestDirectoryLinkTypes.cpp @@ -0,0 +1,80 @@ +#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*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +int APS5_VABI sceKernelGetdents(int, char*, int); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Directory link type check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr std::uint8_t TypeDirectory = 4; +static constexpr std::uint8_t TypeRegular = 8; +static constexpr std::uint8_t TypeLink = 10; + +static std::map Types(const char* path) { + const int directory = sceKernelOpen(path, SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + std::array buffer{}; + std::map types; + for (;;) { + const int read = sceKernelGetdents(directory, buffer.data(), static_cast(buffer.size())); + Require(read >= 0); + if (read == 0) break; + for (int offset = 0; offset < read;) { + std::uint16_t recordLength = 0; + std::memcpy(&recordLength, buffer.data() + offset + 4, sizeof(recordLength)); + Require(recordLength != 0 && offset + recordLength <= read); + types[std::string(buffer.data() + offset + 8)] = static_cast(buffer[offset + 6]); + offset += recordLength; + } + } + Require(sceKernelClose(directory) == 0); + return types; +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-directory-link-types-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directories(root / "app0" / "real")); + Require(std::filesystem::create_directories(root / "dump" / "packed")); + Require(std::filesystem::create_directories(root / "data")); + { std::ofstream stream(root / "dump" / "eboot.bin", std::ios::binary); stream << "x"; } + std::filesystem::create_directory_symlink(root / "dump" / "packed", root / "app0" / "packed"); + std::filesystem::create_symlink(root / "dump" / "eboot.bin", root / "app0" / "eboot.bin"); + std::filesystem::create_symlink(root / "dump" / "missing", root / "app0" / "dangling"); + std::filesystem::create_directory_symlink(root / "dump" / "packed", root / "data" / "saved"); + std::filesystem::current_path(root); + + auto app0 = Types("/app0"); + Require(app0.contains("real") && app0["real"] == TypeDirectory); + Require(app0.contains("packed") && app0["packed"] == TypeDirectory); + Require(app0.contains("eboot.bin") && app0["eboot.bin"] == TypeRegular); + Require(app0.contains("dangling") && app0["dangling"] == TypeLink); + + auto data = Types("/data"); + Require(data.contains("saved") && data["saved"] == TypeLink); + + std::error_code error; + std::filesystem::current_path(std::filesystem::temp_directory_path(), error); + std::filesystem::remove_all(root, error); + std::puts("directory link type tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestDirectoryRewind.cpp b/core/libs/tests/GuestDirectoryRewind.cpp index a7cb911bef..00217b646e 100644 --- a/core/libs/tests/GuestDirectoryRewind.cpp +++ b/core/libs/tests/GuestDirectoryRewind.cpp @@ -1,7 +1,9 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include +#include #include #include #include @@ -81,10 +83,29 @@ int main() { Require(sceKernelLseek(directory, -98, 1) == 2); Require(ReadAll(directory) == rest); + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelLseek(directory, 0, 2) == 5); + Require(sceKernelLseek(directory, 0, 1) == 5); + Require(ReadAll(directory).empty()); + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelLseek(directory, -3, 2) == 2); + Require(sceKernelLseek(directory, 0, 1) == 2); + Require(ReadAll(directory) == rest); + Require(sceKernelLseek(directory, -5, 2) == 0); + Require(ReadAll(directory) == (std::set{".", "..", "a", "b", "c"})); + Require(sceKernelLseek(directory, 3, 0) == 3); Require(sceKernelLseek(directory, -1, 0) == static_cast(0x80020016u)); Require(sceKernelLseek(directory, -4, 1) == static_cast(0x80020016u)); + Require(sceKernelLseek(directory, -6, 2) == static_cast(0x80020016u)); + errno = 0; + Require(File::SeekDirectoryDescriptor(directory, 0, -1) == -1 && errno == EINVAL); + errno = 0; + Require(File::SeekDirectoryDescriptor(directory, 0, 3) == -1 && errno == EINVAL); Require(sceKernelLseek(directory, 0, 1) == 3); + Require(sceKernelLseek(directory, 1, 2) == 6); + Require(sceKernelLseek(directory, std::numeric_limits::max(), 2) == static_cast(0x80020054u)); + Require(sceKernelLseek(directory, 0, 1) == 6); Require(sceKernelLseek(directory, std::numeric_limits::max(), 0) == std::numeric_limits::max()); Require(sceKernelLseek(directory, 1, 1) == static_cast(0x80020054u)); Require(sceKernelLseek(directory, 0, 1) == std::numeric_limits::max()); diff --git a/core/libs/tests/GuestDiscMap.cpp b/core/libs/tests/GuestDiscMap.cpp new file mode 100644 index 0000000000..498dc7c443 --- /dev/null +++ b/core/libs/tests/GuestDiscMap.cpp @@ -0,0 +1,36 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include + +extern "C" { +std::int32_t APS5_VABI sceDiscMapIsRequestOnHDD(const char* path, std::int64_t offset, std::int64_t size, std::int32_t* result); +std::int32_t APS5_VABI sceDiscMapUnknown_ioKMruft1ek(const char* path, std::int64_t offset, std::int64_t size, std::uint64_t* flags, std::uint64_t* out1, std::uint64_t* out2); +} + +namespace { + +constexpr std::int32_t noBitmapInfo = static_cast(0x81100004); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "DiscMap: %s\n", message); + std::abort(); + } +} + +} + +int main() { + std::int32_t result = 0x5a5a5a5a; + Require(sceDiscMapIsRequestOnHDD("/app0/data.bin", 0, 0x10000, &result) == noBitmapInfo, "sceDiscMapIsRequestOnHDD did not report the missing bitmap"); + Require(result == 0x5a5a5a5a, "sceDiscMapIsRequestOnHDD wrote its result"); + std::uint64_t flags = ~0ull; + std::uint64_t out1 = ~0ull; + std::uint64_t out2 = ~0ull; + Require(sceDiscMapUnknown_ioKMruft1ek("/app0/data.bin", 0x8000, 0x10000, &flags, &out1, &out2) == noBitmapInfo, "ioKMruft1ek did not report the missing bitmap"); + Require(flags == ~0ull && out1 == ~0ull && out2 == ~0ull, "ioKMruft1ek wrote an output"); + std::puts("DiscMap tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestDownloadDataMount.cpp b/core/libs/tests/GuestDownloadDataMount.cpp index a3ebfccef8..1c3abe8c03 100644 --- a/core/libs/tests/GuestDownloadDataMount.cpp +++ b/core/libs/tests/GuestDownloadDataMount.cpp @@ -1,20 +1,46 @@ #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include #include #include #include +#include #include +#include #include static void Require(bool value) { if (!value) std::abort(); } +static void WriteParamSfo(const std::filesystem::path& path) { + struct Entry { std::uint16_t key, format; std::uint32_t length, maximum, offset; }; + const std::uint32_t header[5] = {0x46535000, 0x0101, 68, 104, 3}; + const Entry entries[3] = {{0, 0x0404, 4, 4, 0}, {19, 0x0204, 8, 8, 4}, {25, 0x0204, 10, 10, 12}}; + std::ofstream file(path, std::ios::binary); + file.write(reinterpret_cast(header), sizeof(header)); + file.write(reinterpret_cast(entries), sizeof(entries)); + file << std::string("DOWNLOAD_DATA_SIZE\0TITLE\0TITLE_ID\0\0\0", 36) << std::string("\0\1\0\0Example\0CUSA00000\0", 22); +} + int main(int argc, char** argv) { Require(argc == 2); const std::string_view mode = argv[1]; - Require(std::filesystem::is_directory("download0") == (mode == "declared")); + Require(std::filesystem::is_directory("download0") == (mode == "declared" || mode == "sfo-child")); const bool data = std::filesystem::is_directory("data"); std::filesystem::remove_all("data"); - Require(data == (mode != "missing")); + Require(data == (mode != "missing" && mode != "sfo")); if (mode == "missing") return 0; + if (mode == "sfo") { + std::filesystem::create_directories("app0/sce_sys"); + WriteParamSfo("app0/sce_sys/param.sfo"); +#ifdef _WIN32 + const std::string command = "\"\"" + std::string(argv[0]) + "\" sfo-child\""; +#else + const std::string command = "\"" + std::string(argv[0]) + "\" sfo-child"; +#endif + const int status = std::system(command.c_str()); + std::filesystem::remove_all("app0"); + Require(status == 0); + return 0; + } if (mode == "invalid") { bool rejected = false; try { @@ -25,6 +51,6 @@ int main(int argc, char** argv) { Require(rejected); return 0; } - Require(std::strcmp(GetAppTitleId_nid_postfix().value, "PPSA00000") == 0); + Require(std::strcmp(GetAppTitleId_nid_postfix().value, mode == "sfo-child" ? "CUSA00000" : "PPSA00000") == 0); std::filesystem::remove_all("download0"); } diff --git a/core/libs/tests/GuestDup.cpp b/core/libs/tests/GuestDup.cpp index 04614f8062..4e0451ae85 100644 --- a/core/libs/tests/GuestDup.cpp +++ b/core/libs/tests/GuestDup.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include diff --git a/core/libs/tests/GuestDynamicLoader.cpp b/core/libs/tests/GuestDynamicLoader.cpp index 1eb0a5fb45..8ea9fe7c93 100644 --- a/core/libs/tests/GuestDynamicLoader.cpp +++ b/core/libs/tests/GuestDynamicLoader.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -18,6 +19,8 @@ int APS5_VABI dlclose_nid_postfix(void*); char* APS5_VABI dlerror_nid_postfix(); int APS5_VABI _sceKernelRtldThreadAtexitIncrement_nid_postfix(const void*); int APS5_VABI _sceKernelRtldThreadAtexitDecrement_nid_postfix(const void*); +int APS5_VABI sceKernelLoadStartModule(const char*, std::size_t, const void*, unsigned, const void*, int*); +int APS5_VABI sceKernelStopUnloadModule(int, std::size_t, const void*, unsigned, const void*, int*); } static void Require(bool value, const std::source_location& location = std::source_location::current()) { if (value) return; @@ -34,6 +37,70 @@ static bool ThrowsRuntimeError(TFunction function) { } return false; } +#ifdef _WIN32 +static void CheckGlobalLookupOrder(const char* path) { + const std::filesystem::path other = "anyps5-global-order-for-test.guest.prx"; + std::filesystem::copy_file(path, other, std::filesystem::copy_options::overwrite_existing); + void* local = dlopen_nid_postfix(path, 2); + void* earlierGlobal = dlopen_nid_postfix(other.string().c_str(), 2 | 0x100); + void* promoted = dlopen_nid_postfix(path, 2 | 0x100); + Require(local && earlierGlobal && promoted); + void* firstSymbol = dlsym_nid_postfix(local, "GuestModuleAdd"); + void* earlierSymbol = dlsym_nid_postfix(earlierGlobal, "GuestModuleAdd"); + Require(firstSymbol && earlierSymbol && firstSymbol != earlierSymbol); + const std::array scopes{nullptr, reinterpret_cast(-2)}; + for (void* scope : scopes) Require(dlsym_nid_postfix(scope, "GuestModuleAdd") == earlierSymbol); + Require(dlclose_nid_postfix(promoted) == 0); + for (void* scope : scopes) Require(dlsym_nid_postfix(scope, "GuestModuleAdd") == earlierSymbol); + Require(dlclose_nid_postfix(local) == 0); + Require(dlclose_nid_postfix(earlierGlobal) == 0); + std::filesystem::remove(other); +} +static bool CheckGlobalVisibility(const char* path, bool globalFirst) { + using Add = int (APS5_VABI *)(int, int); + const std::array scopes{nullptr, reinterpret_cast(-2)}; + const auto requireHidden = [&] { + for (void* scope : scopes) { + Require(dlsym_nid_postfix(scope, "GuestModuleAdd") == nullptr); + Require(dlerror_nid_postfix() != nullptr); + } + }; + requireHidden(); + void* first = dlopen_nid_postfix(path, 2 | (globalFirst ? 0x100 : 0)); + Require(first != nullptr); + if (!globalFirst) requireHidden(); + void* second = dlopen_nid_postfix(path, 2 | (globalFirst ? 0 : 0x100)); + Require(second != nullptr && second != first); + auto add = reinterpret_cast(dlsym_nid_postfix(first, "GuestModuleAdd")); + Require(add && add(19, 23) == 42); + Require(dlsym_nid_postfix(second, "GuestModuleAdd") == reinterpret_cast(add)); + for (void* scope : scopes) Require(dlsym_nid_postfix(scope, "GuestModuleAdd") == reinterpret_cast(add)); + void* global = globalFirst ? first : second; + void* local = globalFirst ? second : first; + Require(dlclose_nid_postfix(global) == 0); + Require(dlsym_nid_postfix(local, "GuestModuleAdd") == reinterpret_cast(add)); + Require(add(20, 22) == 42); + bool visible = true; + for (void* scope : scopes) { + const bool found = dlsym_nid_postfix(scope, "GuestModuleAdd") == reinterpret_cast(add); + if (!found) { + std::fprintf(stderr, "RTLD_GLOBAL lost after closing its handle: global_first=%d scope=%s; surviving handle returns %d\n", + globalFirst, scope == nullptr ? "NULL" : "-2", add(20, 22)); + dlerror_nid_postfix(); + visible = false; + } + } + Require(dlclose_nid_postfix(local) == 0); + requireHidden(); + void* reloaded = dlopen_nid_postfix(path, 2); + Require(reloaded != nullptr); + auto reloadedAdd = reinterpret_cast(dlsym_nid_postfix(reloaded, "GuestModuleAdd")); + Require(reloadedAdd && reloadedAdd(21, 21) == 42); + requireHidden(); + Require(dlclose_nid_postfix(reloaded) == 0); + return visible; +} +#endif int main(int argc, char** argv) { Require(argc == 2); Require(dlerror_nid_postfix() == nullptr); @@ -52,6 +119,12 @@ int main(int argc, char** argv) { dlerror_nid_postfix(); void* executable = dlopen_nid_postfix(nullptr, 2); Require(executable != nullptr && dlclose_nid_postfix(executable) == 0); +#ifdef _WIN32 + CheckGlobalLookupOrder(argv[1]); + const bool localFirstVisible = CheckGlobalVisibility(argv[1], false); + const bool globalFirstVisible = CheckGlobalVisibility(argv[1], true); + Require(localFirstVisible && globalFirstVisible); +#endif void* module = dlopen_nid_postfix(argv[1], 2 | 0x100); Require(module != nullptr); using Add = int (APS5_VABI *)(int, int); @@ -95,6 +168,25 @@ int main(int argc, char** argv) { std::filesystem::remove(guest); std::filesystem::remove(relinked); + const bool createdLibs = std::filesystem::create_directories("libs"); + std::filesystem::copy_file(argv[1], "libs/anyps5-system-module-for-test.prx", std::filesystem::copy_options::overwrite_existing); + for (const char* path : {"/gkbHtXc3Wq/common/lib/anyps5-system-module-for-test.sprx", "/system/common/lib/anyps5-system-module-for-test.sprx"}) { + int started = -1; + const int system = sceKernelLoadStartModule(path, 0, nullptr, 0, nullptr, &started); + Require(system > 0 && started == 0); + add = reinterpret_cast(dlsym_nid_postfix(reinterpret_cast(static_cast(system)), "GuestModuleAdd")); + Require(add && add(30, 12) == 42); + Require(sceKernelStopUnloadModule(system, 0, nullptr, 0, nullptr, nullptr) == 0); + } + Require(sceKernelLoadStartModule("/gkbHtXc3Wq/common/lib/anyps5-missing-system-module.sprx", 0, nullptr, 0, nullptr, nullptr) == static_cast(0x80020002u)); + Require(sceKernelLoadStartModule("/gkbHtXc3Wq/common/lib/anyps5-system-module-for-test.prx", 0, nullptr, 0, nullptr, nullptr) == static_cast(0x80020002u)); + std::filesystem::copy_file(argv[1], "anyps5-escape-module-for-test.prx", std::filesystem::copy_options::overwrite_existing); + Require(sceKernelLoadStartModule("/gkbHtXc3Wq/common/lib/../anyps5-escape-module-for-test.sprx", 0, nullptr, 0, nullptr, nullptr) == static_cast(0x80020002u)); + std::filesystem::remove("anyps5-escape-module-for-test.prx"); + std::filesystem::remove("libs/anyps5-system-module-for-test.prx"); + std::error_code notEmpty; + if (createdLibs) std::filesystem::remove("libs", notEmpty); + void* pinned = dlopen_nid_postfix(argv[1], 2); add = reinterpret_cast(dlsym_nid_postfix(pinned, "GuestModuleAdd")); Require(add && _sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast(add)) == 0); @@ -122,6 +214,16 @@ int main(int argc, char** argv) { void* hostAdd = ::dlsym(host, "3qcG+yBRS+s"); Require(hostAdd != nullptr); Require(dlsym_nid_postfix(nullptr, "GuestModuleAdd") == hostAdd); + void* hostMul = ::dlsym(host, "GuestModuleMul#guest"); + void* hostSub = ::dlsym(host, "BOyBJaKwOa8#guest"); + Require(hostMul != nullptr && hostSub != nullptr); + for (void* scope : std::array{nullptr, reinterpret_cast(-2)}) { + auto defaultMul = reinterpret_cast(dlsym_nid_postfix(scope, "GuestModuleMul")); + auto defaultSub = reinterpret_cast(dlsym_nid_postfix(scope, "GuestModuleSub")); + Require(reinterpret_cast(defaultMul) == hostMul); + Require(reinterpret_cast(defaultSub) == hostSub); + Require(defaultMul(6, 7) == 42 && defaultSub(50, 8) == 42); + } Require(dlsym_nid_postfix(nullptr, "malloc") == nullptr); Require(::dlclose(host) == 0); #endif diff --git a/core/libs/tests/GuestDynamicLoaderVariants.cpp b/core/libs/tests/GuestDynamicLoaderVariants.cpp new file mode 100644 index 0000000000..5dd0f764b8 --- /dev/null +++ b/core/libs/tests/GuestDynamicLoaderVariants.cpp @@ -0,0 +1,24 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +extern "C" { +void* APS5_VABI dlopen_nid_postfix(const char*, int); +void* APS5_VABI dlsym_nid_postfix(void*, const char*); +int APS5_VABI dlclose_nid_postfix(void*); +} +static void Require(bool value, const std::source_location& location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "guest_dynamic_loader_variants: failed at %s:%u\n", location.file_name(), location.line()); + std::abort(); +} +int main() { + using Add = int (APS5_VABI *)(int, int); + void* extension = dlopen_nid_postfix("guest_module_fixture.prx", 2); + Require(extension != nullptr); + auto add = reinterpret_cast(dlsym_nid_postfix(extension, "GuestModuleAdd")); + Require(add && add(19, 23) == 42); + Require(dlclose_nid_postfix(extension) == 0); + Require(dlopen_nid_postfix("guest_module_fixture.other.prx", 2) == nullptr); + return 0; +} diff --git a/core/libs/tests/GuestElfPhdrMatch.cpp b/core/libs/tests/GuestElfPhdrMatch.cpp index 5e021769a4..3e8d585a70 100644 --- a/core/libs/tests/GuestElfPhdrMatch.cpp +++ b/core/libs/tests/GuestElfPhdrMatch.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/specifics/linux/ElfTypes.hpp" +#include "SceTypes.hpp" #include #include #include @@ -41,4 +42,17 @@ int main() { info.dlpi_phnum = 0; Require(__elf_phdr_match_addr_nid_postfix(&info, Address(0x403800)) == 0); + + ModuleInfoEx module{}; + module.st_size = sizeof(module); + module.segment_count = 3; + module.segments[0] = {0x401000, 0x1000, 1 | 2}; + module.segments[1] = {0x403000, 0x1000, 1 | 4}; + module.segments[2] = {0x408000, 0x100, 1 | 2 | 4}; + + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x403000)) == 1); + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x403ff8)) == 1); + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x404000)) == 0); + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x401800)) == 0); + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x408010)) == 1); } diff --git a/core/libs/tests/GuestEntitlements.cpp b/core/libs/tests/GuestEntitlements.cpp new file mode 100644 index 0000000000..868c1f6e7f --- /dev/null +++ b/core/libs/tests/GuestEntitlements.cpp @@ -0,0 +1,67 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfo(std::uint32_t, const NpUnifiedEntitlementLabel*, NpEntitlementAccessAddcontEntitlementInfo*); +int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfoList(std::uint32_t, NpEntitlementAccessAddcontEntitlementInfo*, std::uint32_t, std::uint32_t*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static void SetEntitlements(const std::filesystem::path& file) { +#ifdef _WIN32 + Require(_wputenv_s(L"ANYPS5_ENTITLEMENTS", file.wstring().c_str()) == 0); +#else + Require(::setenv("ANYPS5_ENTITLEMENTS", file.c_str(), 1) == 0); +#endif +} + +int main(int argc, char** argv) { + const auto root = std::filesystem::temp_directory_path() / ("anyps5_guest_entitlements-" + std::to_string(std::random_device{}())); + const auto directory = root / std::filesystem::path(u8"dlc-a\u00e7\u00e3o-\u65e5\u672c"); + const auto file = directory / "anyps5-entitlements.ini"; + if (argc == 2 && std::string_view(argv[1]) == "missing") { + SetEntitlements(file); + std::uint32_t hits = 0; + bool rejected = false; + try { + sceNpEntitlementAccessGetAddcontEntitlementInfoList(0, nullptr, 0, &hits); + } catch (const std::runtime_error& error) { + rejected = std::string_view(error.what()).starts_with("NpEntitlementAccess: cannot read "); + } + Require(rejected); + std::puts("Guest entitlements rejected a missing configured file"); + return 0; + } + std::filesystem::create_directories(directory); + { + std::ofstream ini(file, std::ios::binary); + ini << "# owned add-ons\nFIRSTADDON0000\n SECONDADDON ; second\n"; + Require(static_cast(ini)); + } + SetEntitlements(file); + NpEntitlementAccessAddcontEntitlementInfo list[3]{}; + std::uint32_t hits = 0; + Require(sceNpEntitlementAccessGetAddcontEntitlementInfoList(0, list, 3, &hits) == 0); + Require(hits == 2); + Require(std::strcmp(list[0].entitlement_label.data, "FIRSTADDON0000") == 0); + Require(std::strcmp(list[1].entitlement_label.data, "SECONDADDON") == 0); + NpUnifiedEntitlementLabel label{}; + std::memcpy(label.data, "SECONDADDON", 11); + NpEntitlementAccessAddcontEntitlementInfo info{}; + Require(sceNpEntitlementAccessGetAddcontEntitlementInfo(0, &label, &info) == 0); + Require(std::strcmp(info.entitlement_label.data, "SECONDADDON") == 0 && info.package_type == 3 && info.download_status == 4); + std::filesystem::remove_all(root); + std::puts("Guest entitlements passed"); + return 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/GuestEqueueEvents.cpp b/core/libs/tests/GuestEqueueEvents.cpp index 8e97329913..c0858dc39a 100644 --- a/core/libs/tests/GuestEqueueEvents.cpp +++ b/core/libs/tests/GuestEqueueEvents.cpp @@ -36,6 +36,7 @@ static constexpr int EVFILT_USER = -11; static constexpr int EVFILT_HRTIMER = -15; static constexpr int EVFILT_AMPR = -25; static constexpr std::uint16_t EV_ERROR = 0x4000; +static constexpr std::uint16_t EV_EOF = 0x8000; static void Require(bool value) { if (!value) std::abort(); } @@ -49,6 +50,15 @@ static bool RejectsNull(TResult (APS5_VABI *accessor)(const KernelEvent*)) { return false; } +static bool RejectsWideFflags(const KernelEvent* ev) { + try { + sceKernelGetEventError(ev); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + static void SleepAtLeast(std::chrono::steady_clock::duration duration) { const auto start = std::chrono::steady_clock::now(); while (std::chrono::steady_clock::now() - start < duration) { @@ -291,12 +301,30 @@ int main() { Require(sceKernelGetEventFflags(&rawEvent) == static_cast(0x80000001LL)); Require(sceKernelGetEventData(&rawEvent) == -5); Require(sceKernelGetEventUserData(&rawEvent) == nullptr); - Require(sceKernelGetEventError(&rawEvent) == 0); - rawEvent.flags = static_cast(~EV_ERROR); - Require(sceKernelGetEventError(&rawEvent) == 0); + Require(RejectsWideFflags(&rawEvent)); + rawEvent.fflags = 0; rawEvent.flags = EV_ERROR; rawEvent.data = 22; - Require(sceKernelGetEventError(&rawEvent) == 22); + Require(sceKernelGetEventError(&rawEvent) == 0); + rawEvent.data = -1; + Require(sceKernelGetEventError(&rawEvent) == 0); + rawEvent.data = 0x80020016; + Require(sceKernelGetEventError(&rawEvent) == 0); + rawEvent.fflags = 5; + rawEvent.data = 0; + Require(sceKernelGetEventError(&rawEvent) == static_cast(0x80020005)); + rawEvent.data = 22; + Require(sceKernelGetEventError(&rawEvent) == static_cast(0x80020005)); + rawEvent.flags = 0; + rawEvent.fflags = 7; + Require(sceKernelGetEventError(&rawEvent) == static_cast(0x80020007)); + rawEvent.flags = EV_EOF; + rawEvent.fflags = 3; + rawEvent.data = 9; + Require(sceKernelGetEventError(&rawEvent) == static_cast(0x80020003)); + rawEvent.flags = static_cast(~EV_ERROR); + rawEvent.fflags = 0xFFFF; + Require(sceKernelGetEventError(&rawEvent) == static_cast(0x8002FFFF)); Require(RejectsNull(sceKernelGetEventData)); Require(RejectsNull(sceKernelGetEventError)); diff --git a/core/libs/tests/GuestErr.cpp b/core/libs/tests/GuestErr.cpp new file mode 100644 index 0000000000..cf7f656594 --- /dev/null +++ b/core/libs/tests/GuestErr.cpp @@ -0,0 +1,197 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +using ErrExit = void (APS5_VABI*)(int); + +extern "C" { +void APS5_VABI err_set_file_nid_postfix(FileStream* stream); +void APS5_VABI err_set_exit_nid_postfix(ErrExit function); +void APS5_VABI warn_nid_postfix(const char* format, ...); +void APS5_VABI warnc_nid_postfix(int code, const char* format, ...); +void APS5_VABI warnx_nid_postfix(const char* format, ...); +[[noreturn]] void APS5_VABI err_nid_postfix(int status, const char* format, ...); +[[noreturn]] void APS5_VABI errc_nid_postfix(int status, int code, const char* format, ...); +[[noreturn]] void APS5_VABI errx_nid_postfix(int status, const char* format, ...); +void APS5_VABI vwarn_nid_postfix(const char* format, VaList* args); +void APS5_VABI vwarnc_nid_postfix(int code, const char* format, VaList* args); +void APS5_VABI vwarnx_nid_postfix(const char* format, VaList* args); +[[noreturn]] void APS5_VABI verr_nid_postfix(int status, const char* format, VaList* args); +[[noreturn]] void APS5_VABI verrc_nid_postfix(int status, int code, const char* format, VaList* args); +[[noreturn]] void APS5_VABI verrx_nid_postfix(int status, const char* format, VaList* args); +char* APS5_VABI strerror_nid_postfix(int error); +int* APS5_VABI __error_nid_postfix(); +extern const char* __progname_nid_postfix; +void APS5_VABI psignal_nid_postfix(unsigned int signal, const char* prefix); +extern const char* const sys_signame_nid_postfix[32]; +extern const char* const sys_siglist_nid_postfix[32]; +} + +namespace { + +struct ExitRequested { + int status; +}; + +int g_hookStatus = -1; + +void APS5_VABI Hook(int status) { + g_hookStatus = status; + throw ExitRequested{status}; +} + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", what); + std::abort(); +} + +class Capture { + FileStream stream; + long consumed = 0; + +public: + Capture() : stream(std::tmpfile()) { err_set_file_nid_postfix(&stream); } + ~Capture() { + err_set_file_nid_postfix(nullptr); + stream.Close(); + } + std::string Take() { + std::FILE* handle = stream.GetHandle(); + std::fflush(handle); + const long end = std::ftell(handle); + std::fseek(handle, consumed, SEEK_SET); + std::string text; + for (int character; (character = std::fgetc(handle)) != EOF;) text.push_back(static_cast(character)); + consumed = end; + std::fseek(handle, end, SEEK_SET); + return text; + } +}; + +template +std::string DescriptorTwoOutput(TCall call) { + std::fflush(stderr); + std::FILE* capture = std::tmpfile(); + const int saved = dup(2); + dup2(fileno(capture), 2); + call(); + dup2(saved, 2); + close(saved); + std::rewind(capture); + std::string text; + for (int character; (character = std::fgetc(capture)) != EOF;) text.push_back(static_cast(character)); + std::fclose(capture); + return text; +} + +template +int ExitStatus(TCall call) { + try { + call(); + } catch (const ExitRequested& request) { + return request.status; + } + return -1; +} + +enum class Variant { Warn, Warnc, Warnx, Err, Errc, Errx, WarnxSkipping }; + +void APS5_VABI CallVariant(Variant variant, int status, int code, const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); + if (variant == Variant::WarnxSkipping) (void)__builtin_va_arg(args, int); +#else + std::va_list args; + va_start(args, format); + if (variant == Variant::WarnxSkipping) (void)va_arg(args, int); +#endif + auto* list = reinterpret_cast(args); + switch (variant) { + case Variant::Warn: vwarn_nid_postfix(format, list); break; + case Variant::Warnc: vwarnc_nid_postfix(code, format, list); break; + case Variant::Warnx: + case Variant::WarnxSkipping: vwarnx_nid_postfix(format, list); break; + case Variant::Err: verr_nid_postfix(status, format, list); + case Variant::Errc: verrc_nid_postfix(status, code, format, list); + case Variant::Errx: verrx_nid_postfix(status, format, list); + } +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif +} + +} + +int main() { + const char* const savedName = __progname_nid_postfix; + __progname_nid_postfix = "prog"; + const std::string noEntry = strerror_nid_postfix(2); + const std::string denied = strerror_nid_postfix(13); + { + Capture capture; + *__error_nid_postfix() = 2; + warn_nid_postfix("open %s", "a.txt"); + Require(capture.Take() == "prog: open a.txt: " + noEntry + "\n", "warn"); + *__error_nid_postfix() = 2; + warn_nid_postfix(nullptr); + Require(capture.Take() == "prog: " + noEntry + "\n", "warn without format"); + warnc_nid_postfix(13, "%d-%s", 7, "x"); + Require(capture.Take() == "prog: 7-x: " + denied + "\n", "warnc"); + warnx_nid_postfix("plain %c", 'q'); + Require(capture.Take() == "prog: plain q\n", "warnx"); + warnx_nid_postfix(nullptr); + Require(capture.Take() == "prog: \n", "warnx without format"); + + err_set_exit_nid_postfix(Hook); + Require(ExitStatus([] { errx_nid_postfix(3, "bad %d", 1); }) == 3 && g_hookStatus == 3, "errx status"); + Require(capture.Take() == "prog: bad 1\n", "errx text"); + Require(ExitStatus([] { errc_nid_postfix(4, 13, "c"); }) == 4, "errc status"); + Require(capture.Take() == "prog: c: " + denied + "\n", "errc text"); + *__error_nid_postfix() = 2; + Require(ExitStatus([] { err_nid_postfix(5, "e"); }) == 5, "err status"); + Require(capture.Take() == "prog: e: " + noEntry + "\n", "err text"); + + *__error_nid_postfix() = 2; + CallVariant(Variant::Warn, 0, 0, "open %s", "a.txt"); + Require(capture.Take() == "prog: open a.txt: " + noEntry + "\n", "vwarn"); + *__error_nid_postfix() = 2; + CallVariant(Variant::Warnc, 0, 13, "%d-%s", 7, "x"); + Require(capture.Take() == "prog: 7-x: " + denied + "\n", "vwarnc uses its code"); + CallVariant(Variant::Warnc, 0, 13, nullptr); + Require(capture.Take() == "prog: " + denied + "\n", "vwarnc without format"); + CallVariant(Variant::Warnx, 0, 0, "%d %d %d %d %d %d %d %d %.2f %s %ld", 1, 2, 3, 4, 5, 6, 7, 8, 2.5, "s", 123456789LL); + Require(capture.Take() == "prog: 1 2 3 4 5 6 7 8 2.50 s 123456789\n", "vwarnx stack and floating arguments"); + CallVariant(Variant::WarnxSkipping, 0, 0, "after %d", 1, 2); + Require(capture.Take() == "prog: after 2\n", "vwarnx continues an advanced list"); + *__error_nid_postfix() = 16; + Require(ExitStatus([] { CallVariant(Variant::Err, 6, 0, nullptr); }) == 6, "verr status"); + Require(capture.Take() == "prog: " + std::string(strerror_nid_postfix(16)) + "\n", "verr without format"); + *__error_nid_postfix() = 2; + Require(ExitStatus([] { CallVariant(Variant::Errc, 4, 13, "c%s", "!"); }) == 4, "verrc status"); + Require(capture.Take() == "prog: c!: " + denied + "\n", "verrc text"); + Require(ExitStatus([] { CallVariant(Variant::Errx, 0, 0, nullptr); }) == 0, "verrx status 0"); + Require(capture.Take() == "prog: \n", "verrx without format"); + err_set_exit_nid_postfix(nullptr); + } + __progname_nid_postfix = savedName; + + Require(std::string(sys_signame_nid_postfix[0]) == "Signal 0" && std::string(sys_signame_nid_postfix[15]) == "TERM", "sys_signame"); + Require(std::string(sys_signame_nid_postfix[26]) == "VTALRM" && std::string(sys_signame_nid_postfix[31]) == "USR2", "sys_signame tail"); + Require(DescriptorTwoOutput([] { psignal_nid_postfix(2, "tag"); }) == std::string("tag: ") + sys_siglist_nid_postfix[2] + "\n", "psignal with prefix"); + Require(DescriptorTwoOutput([] { psignal_nid_postfix(11, ""); }) == std::string(sys_siglist_nid_postfix[11]) + "\n", "psignal empty prefix"); + Require(DescriptorTwoOutput([] { psignal_nid_postfix(32, nullptr); }) == "Unknown signal\n", "psignal unknown signal"); + return 0; +} diff --git a/core/libs/tests/GuestErrorDialog.cpp b/core/libs/tests/GuestErrorDialog.cpp index cb91dc4fbf..51779896a1 100644 --- a/core/libs/tests/GuestErrorDialog.cpp +++ b/core/libs/tests/GuestErrorDialog.cpp @@ -9,6 +9,8 @@ int APS5_VABI sceErrorDialogUpdateStatus(void); int APS5_VABI sceErrorDialogGetStatus(void); int APS5_VABI sceErrorDialogClose(void); int APS5_VABI sceErrorDialogTerminate(void); +int APS5_VABI sceErrorDialogOpenDetail(void); +int APS5_VABI sceErrorDialogOpenWithReport(void); } namespace { @@ -20,6 +22,7 @@ constexpr int kStatusFinished = 3; constexpr int kErrNotInitialized = static_cast(0x80ED0001); constexpr int kErrAlreadyInitialized = static_cast(0x80ED0002); constexpr int kErrParam = static_cast(0x80ED0003); +constexpr int kErrUnexpectedFatal = static_cast(0x80ED0004); constexpr int kErrInvalidState = static_cast(0x80ED0005); struct ErrorDialogParam { @@ -34,11 +37,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 +68,9 @@ int main() { Require(sceErrorDialogTerminate() == 0); Require(sceErrorDialogTerminate() == kErrNotInitialized); + Require(sceErrorDialogGetStatus() == 0); + Require(sceErrorDialogUpdateStatus() == 0); + Require(sceErrorDialogClose() == kErrInvalidState); + Require(sceErrorDialogOpenDetail() == kErrUnexpectedFatal); + Require(sceErrorDialogOpenWithReport() == kErrUnexpectedFatal); } diff --git a/core/libs/tests/GuestExecuteOnceConcurrency.cpp b/core/libs/tests/GuestExecuteOnceConcurrency.cpp new file mode 100644 index 0000000000..e5fc8f802e --- /dev/null +++ b/core/libs/tests/GuestExecuteOnceConcurrency.cpp @@ -0,0 +1,124 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +using Callback = int (APS5_VABI *)(void*, void*, void**); +using ExecuteFunction = int (APS5_VABI *)(int*, Callback, void*); +extern "C" { +int APS5_VABI _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix(int*, Callback, void*); +int APS5_VABI std_execute_once_nid_postfix(int*, Callback, void*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Execute-once concurrency check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static ExecuteFunction execute = nullptr; + +struct IndependentState { + int outer = 0; + int inner = 0; + int published = 0; + int calls = 0; +}; + +static int APS5_VABI InitializeInner(void*, void* argument, void**) { + auto& state = *static_cast(argument); + ++state.calls; + state.published = 42; + return 1; +} + +static int APS5_VABI InitializeOuter(void*, void* argument, void**) { + auto& state = *static_cast(argument); + std::thread worker([&] { + Require(execute(&state.inner, InitializeInner, &state) == 1); + }); + worker.join(); + Require(state.published == 42); + return 1; +} + +static void IndependentFlags() { + IndependentState state; + std::promise completed; + auto ready = completed.get_future(); + std::thread worker([&] { + Require(execute(&state.outer, InitializeOuter, &state) == 1); + completed.set_value(); + }); + Require(ready.wait_for(std::chrono::seconds(5)) == std::future_status::ready); + worker.join(); + Require(state.outer == 1 && state.inner == 1 && state.calls == 1 && state.published == 42); +} + +struct RetryState { + int flag = 0; + int published = 0; + bool throwing = false; + std::atomic arrived = 0; + std::atomic attempts = 0; + std::atomic failures = 0; + std::atomic exceptions = 0; +}; + +static constexpr unsigned Workers = 16; + +static int APS5_VABI Retry(void*, void* argument, void**) { + auto& state = *static_cast(argument); + const auto attempt = ++state.attempts; + Require(attempt <= 2); + if (attempt == 1) { + while (state.arrived.load() != Workers) std::this_thread::yield(); + if (state.throwing) throw 17; + return 0; + } + state.published = 73; + return 5; +} + +static void ConcurrentRetry(bool throwing) { + RetryState state; + state.throwing = throwing; + std::array workers; + for (auto& worker : workers) { + worker = std::thread([&] { + ++state.arrived; + try { + const auto result = execute(&state.flag, Retry, &state); + if (result == 0) ++state.failures; + else Require(result == 1 && state.published == 73); + } catch (int value) { + Require(value == 17); + ++state.exceptions; + } + }); + } + for (auto& worker : workers) worker.join(); + Require(state.flag == 1 && state.published == 73 && state.attempts == 2); + Require(state.failures == (throwing ? 0u : 1u)); + Require(state.exceptions == (throwing ? 1u : 0u)); +} + +int main(int argc, char** argv) { + Require(argc == 2); + if (std::strcmp(argv[1], "mangled") == 0) execute = _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix; + else { + Require(std::strcmp(argv[1], "plain") == 0); + execute = std_execute_once_nid_postfix; + } + IndependentFlags(); + ConcurrentRetry(false); + ConcurrentRetry(true); + IndependentFlags(); +} diff --git a/core/libs/tests/GuestFdopendir.cpp b/core/libs/tests/GuestFdopendir.cpp index 4e6cfffa82..551ad063f0 100644 --- a/core/libs/tests/GuestFdopendir.cpp +++ b/core/libs/tests/GuestFdopendir.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/File/include/File.hpp" #include +#include #include #include #include @@ -14,6 +15,8 @@ GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void*); int APS5_VABI closedir_nid_postfix(void*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); int* APS5_VABI __error_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } @@ -25,7 +28,9 @@ int main() { { std::ofstream file(root / "sample.txt"); file << "test"; } const int fd = open_nid_postfix(root.string().c_str(), 0x20000, 0); Require(fd >= 0); + Require(fcntl_nid_postfix(fd, 1) == 0); void* directory = fdopendir_nid_postfix(fd); + Require(fcntl_nid_postfix(fd, 1) == 1); Require(directory != nullptr); std::map entries; while (auto* entry = readdir_nid_postfix(directory)) { @@ -41,8 +46,20 @@ int main() { const int file = open_nid_postfix((root / "sample.txt").string().c_str(), 0, 0); Require(file >= 0); Require(fdopendir_nid_postfix(file) == nullptr && *__error_nid_postfix() == 20); + Require(fcntl_nid_postfix(file, 1) == 1); Require(DescriptorIsOpen_nid_no_patch(file)); Require(close_nid_postfix(file) == 0); +#ifdef _WIN32 + const int advanced = open_nid_postfix(root.string().c_str(), 0x20000, 0); + Require(advanced >= 0); + Require(lseek_nid_postfix(advanced, 2, 0) == 2); + void* rest = fdopendir_nid_postfix(advanced); + Require(rest != nullptr); + std::map remaining; + while (auto* entry = readdir_nid_postfix(rest)) remaining[entry->name] = entry->type; + Require(remaining.size() == 2 && remaining.count("sample.txt") == 1 && remaining.count("subdirectory") == 1); + Require(closedir_nid_postfix(rest) == 0); +#endif std::filesystem::remove(root / "sample.txt"); std::filesystem::remove(root / "subdirectory"); std::filesystem::remove(root); diff --git a/core/libs/tests/GuestFileLock.cpp b/core/libs/tests/GuestFileLock.cpp index 5cd80d4496..4bb5835820 100644 --- a/core/libs/tests/GuestFileLock.cpp +++ b/core/libs/tests/GuestFileLock.cpp @@ -35,10 +35,10 @@ static void Require(bool condition) { if (!condition) std::abort(); } -static int Duplicate(int source, int kind) { +static int Duplicate(int source, int kind, int target = 20) { if (kind == 0) return dup_nid_postfix(source); - if (kind == 1) return dup2_nid_postfix(source, 20); - return fcntl_nid_postfix(source, 0, 20); + if (kind == 1) return dup2_nid_postfix(source, target); + return fcntl_nid_postfix(source, 0, target); } static bool Acquires(int descriptor, int mode) { @@ -64,8 +64,8 @@ struct Fixture { descriptors.insert(descriptor); return descriptor; } - int Copy(int kind, int from = -1) { - const int descriptor = Duplicate(from < 0 ? source : from, kind); + int Copy(int kind, int from = -1, int target = 20) { + const int descriptor = Duplicate(from < 0 ? source : from, kind, target); Require(descriptor >= 0); descriptors.insert(descriptor); return descriptor; @@ -258,16 +258,17 @@ static void Streams() { Require(fclose_nid_postfix(stream) == 0); } -static int ChildTry(const std::string& path, int mode) { +static int ChildTry(const Fixture (&fixtures)[6], int mode) { wchar_t executable[32768]; Require(::GetModuleFileNameW(nullptr, executable, 32768) != 0); - std::wstring command = L"\"" + std::wstring(executable) + L"\" child \"" + std::filesystem::path(path).wstring() + L"\" " + std::to_wstring(mode | 4); + std::wstring command = L"\"" + std::wstring(executable) + L"\" child " + std::to_wstring(mode | 4); + for (const Fixture& fixture : fixtures) command += L" \"" + std::filesystem::path(fixture.path).wstring() + L"\""; STARTUPINFOW startup{}; startup.cb = sizeof(startup); PROCESS_INFORMATION process{}; Require(::CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &startup, &process) != 0); ::CloseHandle(process.hThread); - if (::WaitForSingleObject(process.hProcess, 3000) != WAIT_OBJECT_0) { + if (::WaitForSingleObject(process.hProcess, 30000) != WAIT_OBJECT_0) { ::TerminateProcess(process.hProcess, 99); std::abort(); } @@ -278,30 +279,55 @@ static int ChildTry(const std::string& path, int mode) { } static void Processes() { - for (int kind = 0; kind != 3; ++kind) { - for (int mode : {1, 2}) { - Fixture fixture; - Require(flock_nid_postfix(fixture.source, mode | 4) == 0); - const int copy = fixture.Copy(kind); - Require(ChildTry(fixture.path, 2) == 35); - Require(ChildTry(fixture.path, 1) == (mode == 1 ? 0 : 35)); - fixture.Close(fixture.source); - Require(ChildTry(fixture.path, 2) == 35); - Require(flock_nid_postfix(copy, 8) == 0); - Require(ChildTry(fixture.path, 2) == 0); - } + Fixture fixtures[6]; + int copies[6]; + int shared = 0; + for (int index = 0; index != 6; ++index) { + const int mode = index % 2 == 0 ? 1 : 2; + if (mode == 1) shared |= 1 << index; + Require(flock_nid_postfix(fixtures[index].source, mode | 4) == 0); + copies[index] = fixtures[index].Copy(index / 2, -1, 100 + index); } + Require(ChildTry(fixtures, 2) == 0x3f); + Require(ChildTry(fixtures, 1) == (0x3f & ~shared)); + for (Fixture& fixture : fixtures) fixture.Close(fixture.source); + Require(ChildTry(fixtures, 2) == 0x3f); + for (const int copy : copies) Require(flock_nid_postfix(copy, 8) == 0); + Require(ChildTry(fixtures, 2) == 0); +} + +static void Directories() { + const std::filesystem::path directory = std::filesystem::temp_directory_path() / ("anyps5-flock-dir-" + std::to_string(::GetCurrentProcessId())); + std::filesystem::create_directories(directory); + const std::string path = directory.string(); + const int descriptor = open_nid_postfix(path.c_str(), 0x20000, 0); + Require(descriptor >= 0); + Require(flock_nid_postfix(descriptor, 1 | 4) == 0); + Require(flock_nid_postfix(descriptor, 2 | 4) == 0); + Require(flock_nid_postfix(descriptor, 2) == 0); + Require(flock_nid_postfix(descriptor, 8) == 0); + Require(flock_nid_postfix(descriptor, 0) == -1 && *__error_nid_postfix() == 9); + Require(flock_nid_postfix(descriptor, 4) == -1 && *__error_nid_postfix() == 9); + Require(close_nid_postfix(descriptor) == 0); + std::error_code ignored; + std::filesystem::remove_all(directory, ignored); } int main(int argc, char** argv) { - if (argc == 4 && std::string(argv[1]) == "child") { - const int descriptor = open_nid_postfix(argv[2], 2, 0); - Require(descriptor >= 0); - const int result = flock_nid_postfix(descriptor, std::atoi(argv[3])); - const int error = result == 0 ? 0 : *__error_nid_postfix(); - if (result == 0) Require(flock_nid_postfix(descriptor, 8) == 0); - Require(close_nid_postfix(descriptor) == 0); - return error; + if (argc >= 4 && std::string(argv[1]) == "child") { + int busy = 0; + for (int index = 3; index < argc; ++index) { + const int descriptor = open_nid_postfix(argv[index], 2, 0); + Require(descriptor >= 0); + if (flock_nid_postfix(descriptor, std::atoi(argv[2])) == 0) { + Require(flock_nid_postfix(descriptor, 8) == 0); + } else { + Require(*__error_nid_postfix() == 35); + busy |= 1 << (index - 3); + } + Require(close_nid_postfix(descriptor) == 0); + } + return busy; } Require(argc == 2); const std::string mode = argv[1]; @@ -311,5 +337,6 @@ int main(int argc, char** argv) { else if (mode == "threads") Threads(); else if (mode == "streams") Streams(); else if (mode == "process") Processes(); + else if (mode == "directory") Directories(); else std::abort(); } diff --git a/core/libs/tests/GuestFileTimestamps.cpp b/core/libs/tests/GuestFileTimestamps.cpp index 13c71a958d..62750ec5f3 100644 --- a/core/libs/tests/GuestFileTimestamps.cpp +++ b/core/libs/tests/GuestFileTimestamps.cpp @@ -19,6 +19,7 @@ int APS5_VABI stat_nid_postfix(const char*, FileStat*); int APS5_VABI lstat_nid_postfix(const char*, FileStat*); int APS5_VABI sceKernelFstat(int, FileStat*); std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); +std::int64_t APS5_VABI _fstat_nid_postfix(int, FileStat*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); int APS5_VABI pipe_nid_postfix(int*); @@ -37,11 +38,12 @@ static bool Same(const KernelTimespec& actual, const KernelTimeval& expected) { } static void CheckTimes(const std::string& path, int fd, const KernelTimeval* expected) { - FileStat byPath{}, byFd{}, byKernel{}; + FileStat byPath{}, byFd{}, byKernel{}, byAlias{}; Require(stat_nid_postfix(path.c_str(), &byPath) == 0, "stat"); Require(fstat_nid_disambig1_nid_postfix(fd, &byFd) == 0, "fstat"); Require(sceKernelFstat(fd, &byKernel) == 0, "sceKernelFstat"); - for (const auto* status : {&byPath, &byFd, &byKernel}) { + Require(_fstat_nid_postfix(fd, &byAlias) == 0, "_fstat"); + for (const auto* status : {&byPath, &byFd, &byKernel, &byAlias}) { Require(Same(status->st_atim, expected[0]), "access time preserves microseconds"); Require(Same(status->st_mtim, expected[1]), "modification time preserves microseconds"); } @@ -89,6 +91,15 @@ int main() { } } + const KernelTimeval invalidBeforePath[2]{{1000000000, -1}, {1000000001, 0}}; + for (const char* pathless : {static_cast(nullptr), ""}) { + Require(utimes_nid_postfix(pathless, invalidBeforePath) == -1 && *__error_nid_postfix() == 22, "utimes checks microseconds before the path"); + Require(lutimes_nid_postfix(pathless, invalidBeforePath) == -1 && *__error_nid_postfix() == 22, "lutimes checks microseconds before the path"); + Require(sceKernelUtimes_nid_postfix(pathless, invalidBeforePath) == static_cast(0x80020016u), "sceKernelUtimes checks microseconds before the path"); + } + Require(utimes_nid_postfix(nullptr, nullptr) == -1 && *__error_nid_postfix() == 14, "utimes still reports EFAULT for a null path"); + Require(utimes_nid_postfix("", nullptr) == -1 && *__error_nid_postfix() == 2, "utimes still reports ENOENT for an empty path"); + #ifdef _WIN32 const auto handle = CreateFileW(std::filesystem::path(path).c_str(), FILE_WRITE_ATTRIBUTES | FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, 0, nullptr); diff --git a/core/libs/tests/GuestFileVector.cpp b/core/libs/tests/GuestFileVector.cpp index 76f392261f..e9e7b46880 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,12 +34,15 @@ 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); std::int64_t APS5_VABI sceKernelReadv(int, const GuestIovec*, int); std::int64_t APS5_VABI readv_nid_postfix(int, const GuestIovec*, int); std::int64_t APS5_VABI writev_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI _readv_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI _writev_nid_postfix(int, const GuestIovec*, int); int* APS5_VABI __error_nid_postfix(); std::int64_t APS5_VABI sceKernelWritev(int, const GuestIovec*, int); std::int64_t APS5_VABI sceKernelPreadv(int, const GuestIovec*, int, std::int64_t); @@ -69,10 +76,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"; } @@ -94,6 +202,8 @@ int main() { Require(sceKernelLseek(file, 0, 1) == 7); Require(readv_nid_postfix(file, reads, 2) == 3); Require(readv_nid_postfix(file, reads, 2) == 0); + Require(sceKernelLseek(file, 3, 0) == 3 && _readv_nid_postfix(file, reads, 2) == 7); + Require(std::memcmp(first, "345", 3) == 0 && std::memcmp(second, "6789", 4) == 0); Require(readv_nid_postfix(file, reads, -1) == -1 && *__error_nid_postfix() == 22); Require(writev_nid_postfix(file, nullptr, 1) == -1 && *__error_nid_postfix() == 14); @@ -118,6 +228,9 @@ int main() { Require(sceKernelLseek(file, 0, 1) == 13 && Contents(path) == "01ABCDE7CDEAB"); Require(sceKernelLseek(file, 8, 0) == 8 && writev_nid_postfix(file, writes, 2) == 5); Require(Contents(path) == "01ABCDE7ABCDE"); + Require(sceKernelLseek(file, 8, 0) == 8 && _writev_nid_postfix(file, swapped, 2) == 5); + Require(sceKernelLseek(file, 0, 1) == 13 && Contents(path) == "01ABCDE7CDEAB"); + Require(sceKernelLseek(file, 8, 0) == 8 && writev_nid_postfix(file, writes, 2) == 5); GuestIovec gapped[3] = {{first, 2}, {nullptr, 0}, {second, 3}}; Require(sceKernelPreadv(file, gapped, 3, 0) == 5); diff --git a/core/libs/tests/GuestFileVectorMemory.cpp b/core/libs/tests/GuestFileVectorMemory.cpp new file mode 100644 index 0000000000..58285bbdd7 --- /dev/null +++ b/core/libs/tests/GuestFileVectorMemory.cpp @@ -0,0 +1,175 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +struct GuestIovec { + void* base; + std::size_t length; +}; + +extern "C" { +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +std::int64_t APS5_VABI sceKernelReadv(int, const GuestIovec*, int); +std::int64_t APS5_VABI sceKernelWritev(int, const GuestIovec*, int); +std::int64_t APS5_VABI sceKernelPreadv(int, const GuestIovec*, int, std::int64_t); +std::int64_t APS5_VABI sceKernelPwritev(int, const GuestIovec*, int, std::int64_t); +std::int64_t APS5_VABI readv_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI writev_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI _readv_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI _writev_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI preadv_nid_postfix(int, const GuestIovec*, int, std::int64_t); +std::int64_t APS5_VABI pwritev_nid_postfix(int, const GuestIovec*, int, std::int64_t); +int* APS5_VABI __error_nid_postfix(); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "File vector memory check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +struct Transfer { + const char* name; + bool posix; + bool positioned; + bool write; + std::function call; +}; + +static std::uintptr_t observedTable = 0; +static std::size_t observedBytes = 0; + +static void ObserveRead(std::uintptr_t address, std::size_t bytes) { + if (address == observedTable) observedBytes = bytes; +} + +static void Protect(void* address, std::size_t size, bool readable) { +#ifdef _WIN32 + DWORD previous = 0; + Require(VirtualProtect(address, size, readable ? PAGE_READONLY : PAGE_NOACCESS, &previous) != 0); +#else + Require(::mprotect(address, size, readable ? PROT_READ : PROT_NONE) == 0); +#endif +} + +int main(int argc, char** argv) { + const std::array transfers{{ + {"readv", true, false, false, readv_nid_postfix}, + {"writev", true, false, true, writev_nid_postfix}, + {"preadv", true, true, false, [](int fd, const GuestIovec* iov, int count) { return preadv_nid_postfix(fd, iov, count, 1); }}, + {"pwritev", true, true, true, [](int fd, const GuestIovec* iov, int count) { return pwritev_nid_postfix(fd, iov, count, 1); }}, + {"_readv", true, false, false, _readv_nid_postfix}, + {"_writev", true, false, true, _writev_nid_postfix}, + {"sceKernelReadv", false, false, false, sceKernelReadv}, + {"sceKernelWritev", false, false, true, sceKernelWritev}, + {"sceKernelPreadv", false, true, false, [](int fd, const GuestIovec* iov, int count) { return sceKernelPreadv(fd, iov, count, 1); }}, + {"sceKernelPwritev", false, true, true, [](int fd, const GuestIovec* iov, int count) { return sceKernelPwritev(fd, iov, count, 1); }}, + }}; +#ifdef _WIN32 + SYSTEM_INFO system{}; + GetSystemInfo(&system); + const std::size_t page = system.dwPageSize; + auto* memory = static_cast(VirtualAlloc(nullptr, page * 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(memory != nullptr); +#else + const auto page = static_cast(::sysconf(_SC_PAGESIZE)); + auto* memory = static_cast(::mmap(nullptr, page * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(memory != MAP_FAILED); +#endif + std::array bytes{'A', 'B'}; + const std::array vectors{{{bytes.data(), 1}, {bytes.data() + 1, 1}}}; + std::memcpy(memory, vectors.data(), sizeof(vectors)); + std::memcpy(memory + page - sizeof(GuestIovec), vectors.data(), sizeof(GuestIovec)); + Protect(memory, page, true); + Protect(memory + page, page, false); + const auto path = std::filesystem::temp_directory_path() / + ("anyps5-iovec-memory-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + AddPathAlias_nid_no_patch("/anyps5-iovec-memory", path.parent_path().string().c_str()); + const auto guestPath = "/anyps5-iovec-memory/" + path.filename().string(); + for (const auto& transfer : transfers) { + if (argc > 1 && transfer.name != std::string(argv[1])) continue; + std::fprintf(stderr, "%s\n", transfer.name); + { std::ofstream file(path, std::ios::binary); file << "ABCD"; Require(static_cast(file)); } + const int fd = sceKernelOpen(guestPath.c_str(), SCE_KERNEL_O_RDWR, 0); + Require(fd >= 0); + const std::array invalid{{ + nullptr, + reinterpret_cast(std::uintptr_t{1}), + reinterpret_cast(memory + page), + reinterpret_cast(memory + page - sizeof(GuestIovec)), + reinterpret_cast(memory + page - sizeof(GuestIovec) / 2), + reinterpret_cast(UINTPTR_MAX - sizeof(GuestIovec) + 1), + }}; + for (const auto* table : invalid) { + *__error_nid_postfix() = 0; + const auto expected = transfer.posix ? -1 : static_cast(0x8002000eu); + Require(transfer.call(fd, table, 2) == expected); + if (transfer.posix) Require(*__error_nid_postfix() == 14); + Require(sceKernelLseek(fd, 0, SEEK_CUR) == 0); + Require((bytes == std::array{'A', 'B'})); + std::ifstream file(path, std::ios::binary); + std::string contents; + file >> contents; + Require(contents == "ABCD"); + Require(transfer.call(fd, table, 0) == 0); + const auto invalidCount = transfer.posix ? -1 : static_cast(0x80020016u); + Require(transfer.call(fd, table, -1) == invalidCount); + if (transfer.posix) Require(*__error_nid_postfix() == 22); + Require(transfer.call(fd, table, 1025) == invalidCount); + if (transfer.posix) Require(*__error_nid_postfix() == 22); + } + observedTable = reinterpret_cast(memory); + observedBytes = 0; + GuestArena::GuestArenaSetHostReadHook_nid_no_patch(ObserveRead); + Require(transfer.call(fd, reinterpret_cast(memory), 2) == 2); + GuestArena::GuestArenaSetHostReadHook_nid_no_patch(nullptr); + Require(observedBytes == sizeof(vectors)); + Require(sceKernelLseek(fd, 0, SEEK_CUR) == (transfer.positioned ? 0 : 2)); + Require(std::memcmp(memory, vectors.data(), sizeof(vectors)) == 0); + if (transfer.write) { + std::ifstream file(path, std::ios::binary); + std::string contents; + file >> contents; + Require(contents == (transfer.positioned ? "AABD" : "ABCD")); + } else { + Require((bytes == (transfer.positioned ? std::array{'B', 'C'} : std::array{'A', 'B'}))); + } + Require(sceKernelLseek(fd, 0, SEEK_SET) == 0); + Require(transfer.call(fd, reinterpret_cast(memory + page - sizeof(GuestIovec)), 1) == 1); + Require(sceKernelLseek(fd, 0, SEEK_CUR) == (transfer.positioned ? 0 : 1)); + Require(sceKernelLseek(fd, 0, SEEK_SET) == 0); + std::array maximum{}; + maximum.back() = {bytes.data(), 1}; + Require(transfer.call(fd, maximum.data(), static_cast(maximum.size())) == 1); + Require(sceKernelLseek(fd, 0, SEEK_CUR) == (transfer.positioned ? 0 : 1)); + Require(sceKernelClose(fd) == 0); + bytes = {'A', 'B'}; + } + Require(std::filesystem::remove(path)); +#ifdef _WIN32 + Require(VirtualFree(memory, 0, MEM_RELEASE) != 0); +#else + Require(::munmap(memory, page * 2) == 0); +#endif +} diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 854cf0fe33..d759f1051d 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -2,8 +2,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/RenameDot.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/NativeStat.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include +#include #include #include #include @@ -11,15 +15,21 @@ #include #include #include +#include #include #include +#include #include #ifdef _WIN32 #include #include +#include #else #include #endif +#ifdef __linux__ +#include +#endif extern "C" { int APS5_VABI remove_nid_postfix(const char*); int APS5_VABI rename_nid_postfix(const char*, const char*); @@ -28,6 +38,7 @@ int APS5_VABI sceKernelFchmod(int, unsigned short); int APS5_VABI fchmod_nid_postfix(int, int); int APS5_VABI chmod_nid_postfix(const char*, int); int APS5_VABI chflags_nid_postfix(const char*, std::uint64_t); +int APS5_VABI fchflags_nid_postfix(int, std::uint64_t); int APS5_VABI futimes_nid_postfix(int, const KernelTimeval*); int APS5_VABI sceKernelFutimes(int, const KernelTimeval*); int APS5_VABI socket_nid_postfix(int, int, int); @@ -38,6 +49,7 @@ int APS5_VABI sceKernelFdatasync(int); int APS5_VABI sceKernelWriteThrottlingStatus(std::uint64_t*); int APS5_VABI sceKernelFtruncate(int, long long); int APS5_VABI sceKernelTruncate_nid_postfix(const char*, long long); +int APS5_VABI truncate_nid_postfix(const char*, long long); int APS5_VABI sceKernelUtimes_nid_postfix(const char*, const void*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI creat_nid_postfix(const char*, int); @@ -52,6 +64,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); @@ -71,14 +85,37 @@ int APS5_VABI chdir_nid_postfix(const char*); void* APS5_VABI opendir_nid_postfix(const char*); FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); FileStream* APS5_VABI freopen_nid_postfix(const char*, const char*, FileStream*); +std::size_t APS5_VABI fread_nid_postfix(void*, std::size_t, std::size_t, FileStream*); +int APS5_VABI fclose_nid_postfix(FileStream*); +int APS5_VABI fgetc_nid_postfix(FileStream*); +int APS5_VABI fileno_nid_postfix(FileStream*); +char* APS5_VABI fgets_nid_postfix(char*, int, FileStream*); +int APS5_VABI fscanf_nid_postfix(FileStream*, const char*, ...); +int APS5_VABI fputc_nid_postfix(int, FileStream*); +int APS5_VABI fseek_nid_postfix(FileStream*, std::int64_t, int); +std::int64_t APS5_VABI ftell_nid_postfix(FileStream*); +void APS5_VABI clearerr_nid_postfix(FileStream*); +int APS5_VABI feof_nid_postfix(FileStream*); +int APS5_VABI ferror_nid_postfix(FileStream*); +int APS5_VABI setvbuf_nid_postfix(FileStream*, char*, int, std::size_t); +int APS5_VABI fflush_nid_postfix(FileStream*); +void APS5_VABI flockfile_nid_postfix(FileStream*); +void APS5_VABI funlockfile_nid_postfix(FileStream*); +int APS5_VABI ftrylockfile_nid_postfix(FileStream*); int* APS5_VABI __error_nid_postfix(); int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI dup2_nid_postfix(int, int); std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::size_t); std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::size_t); +std::int64_t APS5_VABI pread_nid_postfix(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI sceKernelWrite(int, const void*, std::size_t); int APS5_VABI sceKernelDebugOutText(int, const char*); } +#ifdef _WIN32 +static bool g_invalidParameter = false; +static void RecordInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned, std::uintptr_t) { g_invalidParameter = true; } +#endif static void Check(bool value, int line) { if (!value) { #ifdef _WIN32 @@ -108,6 +145,49 @@ static bool ChflagsThrows(const std::filesystem::path& path, std::uint64_t flags return false; } +static bool FchflagsThrows(int descriptor, std::uint64_t flags, const char* names) { + try { + fchflags_nid_postfix(descriptor, flags); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), names) != nullptr; + } + return false; +} + +static std::string CaptureStderr(const std::function& action) { + std::FILE* capture = std::tmpfile(); + Require(capture != nullptr); + std::fflush(stderr); +#ifdef _WIN32 + const int stream = ::_fileno(stderr); + const int saved = ::_dup(stream); + Require(saved >= 0 && ::_dup2(::_fileno(capture), stream) == 0); +#else + const int stream = ::fileno(stderr); + const int saved = ::dup(stream); + Require(saved >= 0 && ::dup2(::fileno(capture), stream) >= 0); +#endif + action(); + std::fflush(stderr); +#ifdef _WIN32 + Require(::_dup2(saved, stream) == 0); + ::_close(saved); +#else + Require(::dup2(saved, stream) >= 0); + ::close(saved); +#endif + Require(std::fseek(capture, 0, SEEK_SET) == 0); + std::string output; + char buffer[512]; + for (;;) { + const auto bytes = std::fread(buffer, 1, sizeof(buffer), capture); + output.append(buffer, bytes); + if (bytes != sizeof(buffer)) break; + } + std::fclose(capture); + return output; +} + static void TestParentTraversal() { const auto root = std::filesystem::path("anyps5-parent-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); @@ -170,6 +250,39 @@ static void TestParentTraversal() { checkInvalid("/parent-test-mount/present.txt/..", 20); FileStat status{}; Require(sceKernelStat("/parent-test-mount/existing/../present.txt", &status) == 0 && status.st_size == 9); + for (const char* name : {"/parent-test-mount/present.txt/.", "/parent-test-mount/present.txt/./"}) { + Require(stat_nid_postfix(name, &status) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelStat(name, &status) == static_cast(0x80020014u)); + Require(access_nid_postfix(name, 0) == -1 && *__error_nid_postfix() == 20); + Require(open_nid_postfix(name, 0, 0) == -1 && *__error_nid_postfix() == 20); + Require(mkdir_nid_postfix(name, 0755) == -1 && *__error_nid_postfix() == 20); + } + Require(stat_nid_postfix("/parent-test-mount/missing/.", &status) == -1 && *__error_nid_postfix() == 2); + Require(stat_nid_postfix("/parent-test-mount/existing/.", &status) == 0 && (status.st_mode & 0170000) == 0040000); + Require(access_nid_postfix("/parent-test-mount/existing/./", 0) == 0); + Require(TrimTrailingSeparators_nid_no_patch("/") == "/"); + Require(TrimTrailingSeparators_nid_no_patch("/mount/dir//") == "/mount/dir"); + Require(TrimTrailingSeparators_nid_no_patch("dir/") == "dir"); +#ifdef _WIN32 + Require(TrimTrailingSeparators_nid_no_patch("C:\\") == "C:\\"); + Require(TrimTrailingSeparators_nid_no_patch("C:/") == "C:/"); + Require(TrimTrailingSeparators_nid_no_patch("C:\\dir\\") == "C:\\dir"); + Require(TrimTrailingSeparators_nid_no_patch("C:\\\\") == "C:\\"); + Require(TrimTrailingSeparators_nid_no_patch("C:\\folder\\\\") == "C:\\folder"); +#endif + std::filesystem::create_directory(root / "dot-victim"); + for (const char* name : {"/parent-test-mount/dot-victim/.", "/parent-test-mount/dot-victim/..", "/parent-test-mount/dot-victim/./", "/parent-test-mount/dot-victim/../"}) { + Require(unlink_nid_postfix(name) == -1 && *__error_nid_postfix() == 22); + Require(rmdir_nid_postfix(name) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelUnlink(name) == static_cast(0x80020016u)); + Require(sceKernelRmdir(name) == static_cast(0x80020016u)); + } + Require(unlink_nid_postfix("/parent-test-mount/present.txt/.") == -1 && *__error_nid_postfix() == 20); + Require(rename_nid_postfix("/parent-test-mount/present.txt/.", "/parent-test-mount/dot-renamed") == -1 && *__error_nid_postfix() == 20); + Require(rename_nid_postfix("/parent-test-mount/present.txt/..", "/parent-test-mount/dot-renamed") == -1 && *__error_nid_postfix() == 20); + Require(rmdir_nid_postfix("/parent-test-mount/missing/..") == -1 && *__error_nid_postfix() == 2); + Require(std::filesystem::is_directory(root / "dot-victim") && std::filesystem::is_regular_file(root / "present.txt")); + std::filesystem::remove(root / "dot-victim"); #ifndef _WIN32 for (const char* name : {"slash-unlink", "slash-remove", "slash-rename", "slash-open", "slash-kernel"}) std::ofstream(root / name) << "kept"; Require(unlink_nid_postfix("/parent-test-mount/slash-unlink/") == -1 && *__error_nid_postfix() == 20); @@ -213,6 +326,13 @@ static void TestRelinkedModuleNames() { Require(sceKernelStat(original.c_str(), &status) == static_cast(0x80020002u)); Require(sceKernelCheckReachability(original.c_str()) == static_cast(0x80020002u)); Require(sceKernelOpen(original.c_str(), 0, 0) == static_cast(0x80020002u)); + const auto missing = (root / "missing-content.pak").string(); + int openResult = 0; + const auto openLog = CaptureStderr([&] { openResult = sceKernelOpen(missing.c_str(), 0, 0); }); + Require(openResult == static_cast(0x80020002u)); + Require(openLog.find("path=" + missing) != std::string::npos); + Require(openLog.find("resolvedPath=") != std::string::npos && openLog.find("missing-content.pak") != std::string::npos); + Require(openLog.find("result=0x80020002") != std::string::npos); { std::ofstream file(relinked); file << "relinked"; } Require(sceKernelStat(original.c_str(), &status) == 0 && status.st_size == 8); Require(stat_nid_postfix(original.c_str(), &status) == 0 && status.st_size == 8); @@ -246,10 +366,139 @@ static void TestRelinkedModuleNameCase() { std::filesystem::remove_all(root); } +#ifdef _WIN32 +static void TestCaseSensitiveHostDirectory() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5-sensitive-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto handle = CreateFileW(root.c_str(), FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(handle != INVALID_HANDLE_VALUE); + struct { ULONG flags; } information{FILE_CS_FLAG_CASE_SENSITIVE_DIR}; + constexpr auto caseSensitiveInfo = static_cast(23); + const bool enabled = SetFileInformationByHandle(handle, caseSensitiveInfo, &information, sizeof(information)); + const auto error = GetLastError(); + CloseHandle(handle); + if (!enabled && (error == ERROR_NOT_SUPPORTED || error == ERROR_INVALID_PARAMETER || error == ERROR_ACCESS_DENIED)) { + std::printf("case-sensitive host directory unavailable: %lu\n", error); + std::filesystem::remove(root); + return; + } + Require(enabled); + { std::ofstream file(root / "File.txt"); file << "payload"; } + Require(std::filesystem::create_directory(root / "Sub")); + Require(std::filesystem::create_directory(root / "Both")); + Require(std::filesystem::create_directory(root / "both")); + { std::ofstream file(root / "libCase.prx.guest.prx"); file << "module"; } + const auto host = root.string(); + AddPathAlias_nid_no_patch("/sensitive-host", host.c_str()); + Require(ResolvePath_nid_no_patch("/sensitive-host/file.txt") == root / "File.txt"); + Require(ResolvePath_nid_no_patch("/sensitive-host/sub") == root / "Sub"); + Require(ResolvePathOrRelinkedModule_nid_no_patch("/sensitive-host/libcase.prx") == root / "libCase.prx.guest.prx"); + bool ambiguous = false; + try { ResolvePath_nid_no_patch("/sensitive-host/BOTH/file.txt"); } + catch (const std::runtime_error& failure) { ambiguous = std::strstr(failure.what(), "Ambiguous case-insensitive guest path") != nullptr; } + Require(ambiguous); + RemovePathAlias_nid_no_patch("/sensitive-host"); + std::filesystem::remove_all(root); +} + +static std::filesystem::path ExtendedPath(const std::filesystem::path& path) { + const auto& native = path.native(); + if (native.starts_with(L"\\\\?\\") || native.starts_with(L"\\\\.\\")) return path; + 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]; + int opens = 0; + for (ssize_t size; (size = read(watch, buffer, sizeof buffer)) > 0;) { + for (char* event = buffer; event < buffer + size; event += sizeof(inotify_event) + reinterpret_cast(event)->len) + if (reinterpret_cast(event)->mask & IN_OPEN) ++opens; + } + return opens; +} + +static void TestMissingPathListsDirectoryOnce() { + const auto root = std::filesystem::path("anyps5-missing-listing-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directories(root)); + { std::ofstream file(root / "present.txt"); file << "present"; } + const int watch = inotify_init1(IN_NONBLOCK); + Require(watch >= 0 && inotify_add_watch(watch, root.c_str(), IN_OPEN | IN_CLOSE_NOWRITE) >= 0); + const auto missing = (root / "Missing.txt").generic_string(); + FileStat status{}; + Require(sceKernelStat(missing.c_str(), &status) == static_cast(0x80020002u) && DirectoryOpens(watch) == 1); + Require(sceKernelCheckReachability(missing.c_str()) == static_cast(0x80020002u) && DirectoryOpens(watch) == 1); + Require(sceKernelOpen(missing.c_str(), 0, 0) == static_cast(0x80020002u) && DirectoryOpens(watch) == 1); + close(watch); + { std::ofstream file(root / "plugin.prx.guest.prx"); file << "lower"; } + { std::ofstream file(root / "Plugin.prx.guest.prx"); file << "upper"; } + bool ambiguous = false; + try { + sceKernelStat((root / "PLUGIN.prx").generic_string().c_str(), &status); + } catch (const std::runtime_error&) { + ambiguous = true; + } + Require(ambiguous); + std::filesystem::remove_all(root); +} +#endif + int main() { TestParentTraversal(); TestRelinkedModuleNames(); TestRelinkedModuleNameCase(); +#ifdef _WIN32 + TestCaseSensitiveHostDirectory(); + TestLongHostPathStat(); +#endif +#ifdef __linux__ + TestMissingPathListsDirectoryOnce(); +#endif Require(chmod_nid_postfix(nullptr, 0600) == -1 && *__error_nid_postfix() == 14); Require(sceKernelChmod_nid_postfix(nullptr, 0600) == static_cast(0x8002000eu)); Require(chmod_nid_postfix("", 0600) == -1 && *__error_nid_postfix() == 2); @@ -261,6 +510,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); @@ -386,6 +636,48 @@ int main() { std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(mkdir_nid_postfix(root.string().c_str(), 0700) == 0); Require(mkdir_nid_postfix(root.string().c_str(), 0700) == -1 && *__error_nid_postfix() == 17); + for (const char* openMode : {"w", "wb", "r+", "a"}) { + Require(fopen_nid_postfix(root.string().c_str(), openMode) == nullptr && *__error_nid_postfix() == 21); + } + { + const auto listed = root / "listed.txt"; + std::ofstream(listed, std::ios::binary) << "listed"; + for (const char* openMode : {"r", "rb", "re"}) { + auto* directoryStream = fopen_nid_postfix(root.string().c_str(), openMode); + Require(directoryStream != nullptr); + const int directoryDescriptor = fileno_nid_postfix(directoryStream); + FileStat directoryStatus{}; + Require(directoryDescriptor >= 0 && sceKernelFstat(directoryDescriptor, &directoryStatus) == 0 && (directoryStatus.st_mode & 0170000) == 0040000); + Require(feof_nid_postfix(directoryStream) == 0 && ferror_nid_postfix(directoryStream) == 0 && fflush_nid_postfix(directoryStream) == 0); + char unread[8] = {}; + int scanned = 0; + *__error_nid_postfix() = 0; + Require(fread_nid_postfix(unread, 1, sizeof(unread), directoryStream) == 0 && *__error_nid_postfix() == 21); + Require(ferror_nid_postfix(directoryStream) != 0 && feof_nid_postfix(directoryStream) == 0); + clearerr_nid_postfix(directoryStream); + Require(ferror_nid_postfix(directoryStream) == 0); + *__error_nid_postfix() = 0; + Require(fgetc_nid_postfix(directoryStream) == -1 && *__error_nid_postfix() == 21 && ferror_nid_postfix(directoryStream) != 0); + clearerr_nid_postfix(directoryStream); + *__error_nid_postfix() = 0; + Require(fgets_nid_postfix(unread, sizeof(unread), directoryStream) == nullptr && *__error_nid_postfix() == 21 && ferror_nid_postfix(directoryStream) != 0); + clearerr_nid_postfix(directoryStream); + *__error_nid_postfix() = 0; + Require(fscanf_nid_postfix(directoryStream, "%d", &scanned) == -1 && *__error_nid_postfix() == 21 && ferror_nid_postfix(directoryStream) != 0); + clearerr_nid_postfix(directoryStream); + Require(fseek_nid_postfix(directoryStream, 0, 0) == 0 && ftell_nid_postfix(directoryStream) == 0); + *__error_nid_postfix() = 0; + Require(fputc_nid_postfix('x', directoryStream) == -1 && *__error_nid_postfix() == 9 && ferror_nid_postfix(directoryStream) != 0); + Require(fclose_nid_postfix(directoryStream) == 0); +#ifdef _WIN32 + const int reused = open_nid_postfix(listed.string().c_str(), 0, 0); + char text[6]{}; + Require(reused == directoryDescriptor && read_nid_postfix(reused, text, sizeof(text)) == 6 && std::memcmp(text, "listed", 6) == 0); + Require(close_nid_postfix(reused) == 0); +#endif + } + Require(std::filesystem::remove(listed)); + } Require(mkdir_nid_postfix((root / "missing" / "child").string().c_str(), 0700) == -1 && *__error_nid_postfix() == 2); Require(mkdir_nid_postfix(nullptr, 0700) == -1 && *__error_nid_postfix() == 14); const auto file = root / "file.txt"; @@ -404,6 +696,28 @@ int main() { Require(chflags_nid_postfix(file.string().c_str(), 0x200000) == -1 && *__error_nid_postfix() == 1); Require(ChflagsThrows(file, 0x2, "UF_IMMUTABLE")); Require(ChflagsThrows(file, 0x1000 | 0x40000, "UF_READONLY|SF_APPEND")); + const int flagged = open_nid_postfix(file.string().c_str(), 0, 0); + Require(flagged >= 0 && fchflags_nid_postfix(flagged, 0) == 0); + Require(fchflags_nid_postfix(flagged, 0x20) == -1 && *__error_nid_postfix() == 45); + Require(fchflags_nid_postfix(flagged, 1ULL << 32) == -1 && *__error_nid_postfix() == 45); + Require(fchflags_nid_postfix(flagged, ~0ULL) == -1 && *__error_nid_postfix() == 45); + Require(fchflags_nid_postfix(flagged, 0x200000) == -1 && *__error_nid_postfix() == 1); + Require(FchflagsThrows(flagged, 0x2, "UF_IMMUTABLE")); + Require(FchflagsThrows(flagged, 0x1000 | 0x40000, "UF_READONLY|SF_APPEND")); + Require(close_nid_postfix(flagged) == 0); + Require(fchflags_nid_postfix(flagged, 0) == -1 && *__error_nid_postfix() == 9); + Require(fchflags_nid_postfix(flagged, ~0ULL) == -1 && *__error_nid_postfix() == 9); + Require(fchflags_nid_postfix(-1, 0) == -1 && *__error_nid_postfix() == 9); + const int flaggedRoot = sceKernelOpen(root.string().c_str(), 0x20000, 0); + Require(flaggedRoot >= 0 && fchflags_nid_postfix(flaggedRoot, 0) == 0 && sceKernelClose(flaggedRoot) == 0); + int flaggedPipe[2]; + Require(pipe_nid_postfix(flaggedPipe) == 0); + Require(fchflags_nid_postfix(flaggedPipe[0], 0) == -1 && *__error_nid_postfix() == 22); + Require(fchflags_nid_postfix(flaggedPipe[1], ~0ULL) == -1 && *__error_nid_postfix() == 22); + Require(close_nid_postfix(flaggedPipe[0]) == 0 && close_nid_postfix(flaggedPipe[1]) == 0); + const int flaggedSocket = socket_nid_postfix(2, 2, 0); + Require(flaggedSocket >= 0 && fchflags_nid_postfix(flaggedSocket, 0) == -1 && *__error_nid_postfix() == 22); + Require(close_nid_postfix(flaggedSocket) == 0); const auto held = root / "held.txt"; const auto heldName = held.string(); { std::ofstream stream(held); stream << "held"; } @@ -474,6 +788,63 @@ int main() { Require(rename_nid_postfix(name(targetFile).c_str(), name(targetFile).c_str()) == 0); Require(rename_nid_postfix(name(sourceDirectory).c_str(), name(emptyDirectory).c_str()) == 0); Require(!std::filesystem::exists(sourceDirectory) && std::filesystem::is_directory(emptyDirectory / "child")); + const auto kernelSource = area / "kernel_source"; + const auto kernelTarget = area / "kernel_target"; + Require(std::filesystem::create_directories(kernelSource / "child")); + Require(std::filesystem::create_directory(kernelTarget)); + { std::ofstream stream(kernelSource / "marker"); stream << "kernel directory contents"; } + const int directoryResult = sceKernelRename(name(kernelSource).c_str(), name(kernelTarget).c_str()); + if (directoryResult != 0) + std::fprintf(stderr, "kernel empty-directory replacement result=0x%08x, expected=0\n", static_cast(directoryResult)); + Require(directoryResult == 0); + Require(!std::filesystem::exists(kernelSource) && std::filesystem::is_directory(kernelTarget / "child")); + { std::ifstream stream(kernelTarget / "marker"); std::string contents; std::getline(stream, contents); + Require(contents == "kernel directory contents"); } + const int parentResult = sceKernelRename(name(targetFile).c_str(), absentParent.c_str()); + Require(parentResult == static_cast(0x80020002u)); + Require(std::filesystem::is_regular_file(targetFile) && !std::filesystem::exists(area / "absent")); + const int prefixResult = sceKernelRename(name(targetFile).c_str(), name(targetFile / "target").c_str()); + Require(prefixResult == static_cast(0x80020014u)); + Require(std::filesystem::is_regular_file(targetFile)); + const int fileDirectoryResult = sceKernelRename(name(targetFile).c_str(), name(fullDirectory).c_str()); + Require(fileDirectoryResult == static_cast(0x80020015u)); + Require(std::filesystem::is_regular_file(targetFile) && std::filesystem::is_regular_file(fullDirectory / "entry")); + const int directoryFileResult = sceKernelRename(name(kernelTarget).c_str(), name(targetFile).c_str()); + Require(directoryFileResult == static_cast(0x80020014u)); + Require(std::filesystem::is_regular_file(kernelTarget / "marker") && std::filesystem::is_regular_file(targetFile)); + const int nonemptyResult = sceKernelRename(name(kernelTarget).c_str(), name(fullDirectory).c_str()); + Require(nonemptyResult == static_cast(0x80020042u)); + Require(std::filesystem::is_regular_file(kernelTarget / "marker") && std::filesystem::is_regular_file(fullDirectory / "entry")); + const auto descendant = kernelTarget / "child" / "moved"; + const int descendantResult = sceKernelRename(name(kernelTarget).c_str(), name(descendant).c_str()); + Require(descendantResult == static_cast(0x80020016u)); + Require(std::filesystem::is_directory(kernelTarget / "child") && !std::filesystem::exists(descendant)); + { std::ifstream stream(targetFile); std::string contents; std::getline(stream, contents); Require(contents == "source"); } + { std::ifstream stream(fullDirectory / "entry"); std::string contents; std::getline(stream, contents); Require(contents == "entry"); } + { std::ifstream stream(kernelTarget / "marker"); std::string contents; std::getline(stream, contents); + Require(contents == "kernel directory contents"); } +#ifdef _WIN32 + for (const bool kernel : {true, false}) { + const auto lockedSource = area / (kernel ? "locked_kernel_source" : "locked_libc_source"); + const auto preservedTarget = area / (kernel ? "preserved_kernel_target" : "preserved_libc_target"); + Require(std::filesystem::create_directory(lockedSource)); + Require(std::filesystem::create_directory(preservedTarget)); + const auto marker = lockedSource / "marker"; + { std::ofstream stream(marker); stream << "preserved source contents"; } + const HANDLE held = CreateFileW(marker.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + Require(held != INVALID_HANDLE_VALUE); + const int result = kernel ? sceKernelRename(name(lockedSource).c_str(), name(preservedTarget).c_str()) : + rename_nid_postfix(name(lockedSource).c_str(), name(preservedTarget).c_str()); + Require(CloseHandle(held) != 0); + Require(result != 0 && std::filesystem::is_regular_file(marker)); + if (!std::filesystem::is_directory(preservedTarget)) + std::fprintf(stderr, "failed %s directory rename removed its destination\n", kernel ? "kernel" : "libc"); + Require(std::filesystem::is_directory(preservedTarget) && std::filesystem::is_empty(preservedTarget)); + { std::ifstream stream(marker); std::string contents; std::getline(stream, contents); + Require(contents == "preserved source contents"); } + } +#endif for (const bool kernel : {false, true}) { const auto incoming = name(area / "incoming.txt"); const auto occupied = name(area / "occupied.txt"); @@ -500,7 +871,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)); @@ -512,7 +889,7 @@ int main() { Require(std::filesystem::file_size(sized) == 6); { std::ifstream stream(sized); std::string contents; std::getline(stream, contents); Require(contents == "012345"); } - Require(sceKernelTruncate_nid_postfix((sized / "missing").string().c_str(), 6) == static_cast(0x80020002u)); + Require(sceKernelTruncate_nid_postfix((sized / "missing").string().c_str(), 6) == static_cast(0x80020014u)); Require(sceKernelUtimes_nid_postfix(sized.string().c_str(), nullptr) == 0); std::FILE* native = std::fopen(sized.string().c_str(), "r+b"); Require(native != nullptr); @@ -560,6 +937,14 @@ int main() { Require(sceKernelFdatasync(socket) == static_cast(0x80020016u)); Require(fsync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 22); Require(sceKernelFsync(socket) == static_cast(0x80020016u)); + int syncPipe[2] = {-1, -1}; + Require(pipe_nid_postfix(syncPipe) == 0); + for (const int end : syncPipe) { + Require(fsync_nid_postfix(end) == -1 && *__error_nid_postfix() == 22); + Require(fdatasync_nid_postfix(end) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelFsync(end) == static_cast(0x80020016u) && sceKernelFdatasync(end) == static_cast(0x80020016u)); + } + Require(close_nid_postfix(syncPipe[0]) == 0 && close_nid_postfix(syncPipe[1]) == 0); Require(close_nid_postfix(socket) == 0); Require(fchmod_nid_postfix(socket, 0600) == -1 && *__error_nid_postfix() == 9); Require(futimes_nid_postfix(socket, nullptr) == -1 && *__error_nid_postfix() == 9); @@ -569,6 +954,17 @@ int main() { Require(sceKernelFsync(socket) == static_cast(0x80020009u)); Require(sceKernelFdatasync(0x7fffffff) == static_cast(0x80020009u)); Require(sceKernelFsync(0x7fffffff) == static_cast(0x80020009u)); +#ifdef _WIN32 + const auto previousParameterHandler = _set_invalid_parameter_handler(RecordInvalidParameter); +#endif + FileStat closedStatus{}; + Require(sceKernelFstat(descriptor, &closedStatus) == static_cast(0x80020009u)); + Require(sceKernelFstat(socket, &closedStatus) == static_cast(0x80020009u)); + Require(sceKernelFstat(0x7fffffff, &closedStatus) == static_cast(0x80020009u)); +#ifdef _WIN32 + _set_invalid_parameter_handler(previousParameterHandler); + Require(!g_invalidParameter); +#endif Require(remove_nid_postfix(sized.string().c_str()) == 0); const auto present = root / "present.txt"; const auto presentName = present.string(); @@ -581,11 +977,24 @@ int main() { Require(sceKernelCheckReachability("") == static_cast(0x80020002u)); Require(sceKernelRename("", presentName.c_str()) == static_cast(0x80020002u)); Require(sceKernelRename(presentName.c_str(), "") == static_cast(0x80020002u)); + Require(sceKernelRename(missingName.c_str(), nullptr) == static_cast(0x80020002u)); + Require(sceKernelRename("", nullptr) == static_cast(0x80020002u)); + Require(sceKernelRename(".", nullptr) == static_cast(0x80020016u)); + Require(sceKernelRename(missingName.c_str(), ".") == static_cast(0x80020002u)); + Require(sceKernelRename(presentName.c_str(), nullptr) == static_cast(0x8002000Eu)); + Require(sceKernelRename(nullptr, presentName.c_str()) == static_cast(0x8002000Eu)); + Require(rename_nid_postfix(missingName.c_str(), nullptr) == -1 && *__error_nid_postfix() == 2); + Require(rename_nid_postfix(presentName.c_str(), nullptr) == -1 && *__error_nid_postfix() == 14); Require(sceKernelRmdir("") == static_cast(0x80020002u)); Require(sceKernelMkdir("", 0700) == static_cast(0x80020002u)); const auto cwdPermissions = std::filesystem::status(".").permissions(); Require(sceKernelChmod_nid_postfix("", static_cast(cwdPermissions) & 07777) == static_cast(0x80020002u)); Require(sceKernelTruncate_nid_postfix("", 0) == static_cast(0x80020002u)); + Require(sceKernelTruncate_nid_postfix(nullptr, -1) == static_cast(0x80020016u)); + Require(sceKernelTruncate_nid_postfix("", -1) == static_cast(0x80020016u)); + Require(truncate_nid_postfix(nullptr, -1) == -1 && *__error_nid_postfix() == 22); + Require(truncate_nid_postfix("", -1) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelTruncate_nid_postfix(nullptr, 0) == static_cast(0x8002000Eu)); const auto cwdWriteTime = std::filesystem::last_write_time("."); const int emptyUtimes = sceKernelUtimes_nid_postfix("", nullptr); std::error_code restoreTimeError; @@ -621,6 +1030,11 @@ int main() { Require(stat_nid_postfix(link.string().c_str(), &status) == 0 && (status.st_mode & 0170000) == 0100000 && status.st_size == 5); const auto dangling = root / "dangling"; std::filesystem::create_symlink("missing.txt", dangling); + const auto statLoop = root / "stat-loop"; + std::filesystem::create_symlink("stat-loop", statLoop); + errno = 0; + Require(!File::TryFillFileStat(statLoop, &status) && errno == ELOOP); + Require(sceKernelStat(statLoop.generic_string().c_str(), &status) == SCE_KERNEL_ERROR_ELOOP); Require(lstat_nid_postfix(dangling.string().c_str(), &linkStatus) == 0 && (linkStatus.st_mode & 0170000) == 0120000); constexpr int noFollow = 0x100; Require(sceKernelOpen(link.string().c_str(), noFollow, 0) == static_cast(0x8002001Fu)); @@ -640,7 +1054,8 @@ int main() { Require(unlink_nid_postfix(viaMissing.c_str()) == -1 && *__error_nid_postfix() == 2 && std::filesystem::exists(present)); Require(stat_nid_postfix((presentName + "/..").c_str(), &status) == -1 && *__error_nid_postfix() == 20); Require(sceKernelStat((presentName + "/../present.txt").c_str(), &status) == static_cast(0x80020014u)); - Require(unlink_nid_postfix(link.string().c_str()) == 0 && unlink_nid_postfix(dangling.string().c_str()) == 0); + Require(unlink_nid_postfix(link.string().c_str()) == 0 && unlink_nid_postfix(dangling.string().c_str()) == 0 && + unlink_nid_postfix(statLoop.string().c_str()) == 0); #else const auto fileLink = root / "file-link"; if (CreateSymbolicLinkW(fileLink.c_str(), L"present.txt", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { @@ -651,31 +1066,93 @@ int main() { Require(std::filesystem::create_directory(targetDirectory)); const auto directoryLink = root / "directory-link"; const auto danglingLink = root / "dangling-link"; + const auto absoluteLink = root / "absolute-link"; Require(CreateSymbolicLinkW(directoryLink.c_str(), L"target-directory", SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); - Require(CreateSymbolicLinkW(danglingLink.c_str(), missing.c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); - const auto checkUnsupportedLink = [](const std::filesystem::path& link) { - FileStat untouched; - std::memset(&untouched, 0x5a, sizeof(untouched)); - constexpr int sentinelError = 123; - *__error_nid_postfix() = sentinelError; - bool rejected = false; - try { - lstat_nid_postfix(link.string().c_str(), &untouched); - } catch (const std::runtime_error& error) { - rejected = std::strcmp(error.what(), "lstat of a Windows symbolic link not implemented") == 0; - } - const bool errorUnchanged = *__error_nid_postfix() == sentinelError; - const auto* bytes = reinterpret_cast(&untouched); - const bool statusUnchanged = std::all_of(bytes, bytes + sizeof(untouched), [](unsigned char value) { return value == 0x5a; }); - return rejected && errorUnchanged && statusUnchanged; + Require(CreateSymbolicLinkW(danglingLink.c_str(), L"missing.txt", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + Require(CreateSymbolicLinkW(absoluteLink.c_str(), std::filesystem::absolute(present).c_str(), + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + const auto statLoop = root / "stat-loop"; + Require(CreateSymbolicLinkW(statLoop.c_str(), L"stat-loop", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + Require(sceKernelStat(statLoop.generic_string().c_str(), &status) == static_cast(0x8002003eu)); + Require(sceKernelOpen(statLoop.generic_string().c_str(), 0, 0) == static_cast(0x8002003eu)); + Require(open_nid_postfix(statLoop.string().c_str(), 0, 0) == -1 && *__error_nid_postfix() == 62); + Require(fopen_nid_postfix(statLoop.string().c_str(), "r") == nullptr && *__error_nid_postfix() == 62); + const auto checkRelativeLink = [](const std::filesystem::path& link, std::int64_t targetLength) { + FileStat own{}; + return lstat_nid_postfix(link.string().c_str(), &own) == 0 && (own.st_mode & 0170000) == 0120000 && + own.st_size == targetLength && own.st_nlink == 1; }; + Require(checkRelativeLink(fileLink, 11)); + Require(checkRelativeLink(directoryLink, 16)); + Require(checkRelativeLink(danglingLink, 11)); + FileStat followed{}; + Require(lstat_nid_postfix(fileLink.string().c_str(), &linkStatus) == 0); + Require(stat_nid_postfix(fileLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0100000); + Require(followed.st_ino == status.st_ino && linkStatus.st_ino != status.st_ino && linkStatus.st_dev == status.st_dev); + Require(stat_nid_postfix(directoryLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0040000); Require(stat_nid_postfix(fileLink.string().c_str(), &status) == 0 && status.st_size == 5); - Require(checkUnsupportedLink(fileLink)); - Require(checkUnsupportedLink(directoryLink)); - Require(checkUnsupportedLink(danglingLink)); - Require(std::filesystem::remove(fileLink) && std::filesystem::remove(directoryLink) && - std::filesystem::remove(danglingLink) && std::filesystem::remove(targetDirectory)); + FileStat absoluteStatus{}; + Require(lstat_nid_postfix(absoluteLink.string().c_str(), &absoluteStatus) == 0); + Require((absoluteStatus.st_mode & 0170000) == 0120000 && absoluteStatus.st_size > 0 && absoluteStatus.st_nlink == 1); + Require(stat_nid_postfix(absoluteLink.string().c_str(), &followed) == 0 && followed.st_ino == status.st_ino); + Require(sceKernelOpen((statLoop.generic_string() + "/inside").c_str(), 0, 0) == static_cast(0x8002003eu)); + Require(open_nid_postfix((statLoop.string() + "\\inside").c_str(), 0, 0) == -1 && *__error_nid_postfix() == 62); + Require(std::filesystem::remove(fileLink) && std::filesystem::remove(directoryLink) && DeleteFileW(danglingLink.c_str()) != 0 && + DeleteFileW(statLoop.c_str()) != 0 && + std::filesystem::remove(absoluteLink) && std::filesystem::remove(targetDirectory)); + } + const auto junctionTarget = root / "junction-target"; + const auto junction = root / "junction"; + Require(std::filesystem::create_directory(junctionTarget)); + const auto junctionCommand = L"cmd /c mklink /J \"" + junction.wstring() + L"\" \"" + junctionTarget.wstring() + L"\""; + Require(_wsystem(junctionCommand.c_str()) == 0); + FileStat junctionStatus{}; + Require(lstat_nid_postfix(junction.string().c_str(), &junctionStatus) == 0); + Require((junctionStatus.st_mode & 0170000) == 0120000 && junctionStatus.st_size > 0); + FileStat junctionFollowed{}; + Require(stat_nid_postfix(junction.string().c_str(), &junctionFollowed) == 0 && (junctionFollowed.st_mode & 0170000) == 0040000); + constexpr int noFollow = 0x100; + Require(sceKernelOpen(junction.string().c_str(), noFollow, 0) == static_cast(0x8002001Fu)); + Require(open_nid_postfix(junction.string().c_str(), noFollow, 0) == -1 && *__error_nid_postfix() == 31); + const int followedJunction = sceKernelOpen(junction.string().c_str(), 0, 0); + Require(followedJunction >= 0 && sceKernelClose(followedJunction) == 0); + Require(std::filesystem::remove(junction) && std::filesystem::remove(junctionTarget)); + const auto tagged = root / "tagged.bin"; + { std::ofstream stream(tagged, std::ios::binary); stream << std::string(1u << 18, 'a'); } + const HANDLE taggedHandle = CreateFileW(tagged.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(taggedHandle != INVALID_HANDLE_VALUE); + std::array reparseData{}; + REPARSE_GUID_DATA_BUFFER reparseHeader{}; + reparseHeader.ReparseTag = 0x00000ABC; + reparseHeader.ReparseDataLength = 4; + reparseHeader.ReparseGuid = {0x6a1e3d0b, 0x1f6c, 0x4c1e, {0x9b, 0x52, 0x3d, 0x8e, 0x41, 0x07, 0x5c, 0x2a}}; + std::memcpy(reparseData.data(), &reparseHeader, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE); + DWORD returned = 0; + const bool taggedSet = DeviceIoControl(taggedHandle, FSCTL_SET_REPARSE_POINT, reparseData.data(), + static_cast(reparseData.size()), nullptr, 0, &returned, nullptr) != 0; + if (!taggedSet) Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD || GetLastError() == ERROR_ACCESS_DENIED); + Require(CloseHandle(taggedHandle) != 0); + if (!taggedSet) { + Require(std::filesystem::remove(tagged)); + std::fprintf(stderr, "Windows reparse point checks skipped: setting a reparse point on a file needs rights this user lacks\n"); + } else { + Require((GetFileAttributesW(tagged.c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + FileStat taggedStatus{}; + FileStat taggedFollowed{}; + const int taggedResult = lstat_nid_postfix(tagged.string().c_str(), &taggedStatus); + const int taggedError = *__error_nid_postfix(); + Require(stat_nid_postfix(tagged.string().c_str(), &taggedFollowed) == taggedResult); + Require(taggedResult == 0 ? (taggedStatus.st_ino == taggedFollowed.st_ino && taggedStatus.st_mode == taggedFollowed.st_mode) + : *__error_nid_postfix() == taggedError); + const HANDLE taggedCleanup = CreateFileW(tagged.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(taggedCleanup != INVALID_HANDLE_VALUE); + reparseHeader.ReparseDataLength = 0; + Require(DeviceIoControl(taggedCleanup, FSCTL_DELETE_REPARSE_POINT, &reparseHeader, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE, + nullptr, 0, &returned, nullptr) != 0); + Require(CloseHandle(taggedCleanup) != 0 && std::filesystem::remove(tagged)); } #endif FileStat identity{}; @@ -719,6 +1196,42 @@ int main() { Require(flock_nid_postfix(other, 2 | 4) == 0); Require(flock_nid_postfix(other, 8) == 0); Require(flock_nid_postfix(locked, 2 | 4) == 0); + Require(lseek_nid_postfix(-1, 0, 7) == -1 && *__error_nid_postfix() == 9); + Require(lseek_nid_postfix(0x7fffffff, 0, -1) == -1 && *__error_nid_postfix() == 9); + Require(lseek_nid_postfix(locked, 0, 7) == -1 && *__error_nid_postfix() == 22); + { + char transfer[4]{}; + int transferPipe[2] = {-1, -1}; + Require(pipe_nid_postfix(transferPipe) == 0); + Require(read_nid_postfix(-1, nullptr, 1) == -1 && *__error_nid_postfix() == 9); + Require(write_nid_postfix(-1, nullptr, 1) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelRead(-1, nullptr, 1) == static_cast(0x80020009u)); + Require(sceKernelWrite(-1, nullptr, 1) == static_cast(0x80020009u)); + Require(read_nid_postfix(locked, nullptr, 1) == -1 && *__error_nid_postfix() == 14); + Require(pread_nid_postfix(-1, transfer, 1, -1) == -1 && *__error_nid_postfix() == 9); + Require(pread_nid_postfix(-1, nullptr, 1, 0) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelPread(-1, transfer, 1, -1) == static_cast(0x80020009u)); + Require(pread_nid_postfix(transferPipe[0], transfer, 1, -1) == -1 && *__error_nid_postfix() == 29); + Require(pread_nid_postfix(locked, transfer, 1, -1) == -1 && *__error_nid_postfix() == 22); + Require(pread_nid_postfix(locked, nullptr, 1, -1) == -1 && *__error_nid_postfix() == 22); + Require(pread_nid_postfix(locked, nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14); + Require(close_nid_postfix(transferPipe[0]) == 0 && close_nid_postfix(transferPipe[1]) == 0); + } + Require(lseek_nid_postfix(locked, 4, 0) == 4); + Require(lseek_nid_postfix(locked, INT64_MAX, 1) == -1 && *__error_nid_postfix() == 84); + Require(lseek_nid_postfix(locked, 0, 1) == 4); + Require(lseek_nid_postfix(locked, -1, 0) == -1 && *__error_nid_postfix() == 22); + const std::int64_t lockedSize = lseek_nid_postfix(locked, 0, 2); + Require(lockedSize >= 0); + Require(lseek_nid_postfix(locked, INT64_MAX - lockedSize + 1, 2) == -1 && *__error_nid_postfix() == 84); + Require(lseek_nid_postfix(locked, 0, 1) == lockedSize); + Require(flock_nid_postfix(-1, 2 | 4) == -1 && *__error_nid_postfix() == 9); + Require(flock_nid_postfix(0x7fffffff, 1) == -1 && *__error_nid_postfix() == 9); + int flockPipe[2] = {-1, -1}; + Require(pipe_nid_postfix(flockPipe) == 0); + Require(flock_nid_postfix(flockPipe[0], 2 | 4) == -1 && *__error_nid_postfix() == 45); + Require(flock_nid_postfix(flockPipe[1], 8) == -1 && *__error_nid_postfix() == 45); + Require(close_nid_postfix(flockPipe[0]) == 0 && close_nid_postfix(flockPipe[1]) == 0); Require(close_nid_postfix(locked) == 0); const int recycled = open_nid_postfix(presentName.c_str(), 0, 0); Require(recycled >= 0 && flock_nid_postfix(recycled, 8) == 0); @@ -776,6 +1289,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); @@ -793,6 +1313,19 @@ int main() { Require(sceKernelOpen(presentName.c_str(), 0x20402, 0) == static_cast(0x80020014u)); Require(std::filesystem::file_size(present) == 5); Require(sceKernelOpen(missingName.c_str(), 0x20000, 0) == static_cast(0x80020002u)); + const auto invalidFlagsName = (root / "invalid-flags.txt").string(); + Require(sceKernelOpen(presentName.c_str(), 3, 0) == static_cast(0x80020016u)); + Require(sceKernelOpen(missingName.c_str(), 0x203, 0600) == static_cast(0x80020016u)); + Require(open_nid_postfix(presentName.c_str(), 3, 0) == -1 && *__error_nid_postfix() == 22); + Require(_open_nid_postfix(presentName.c_str(), 0x403) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelOpen(invalidFlagsName.c_str(), 0x20a02, 0600) == static_cast(0x80020016u)); + Require(open_nid_postfix(invalidFlagsName.c_str(), 0x20a00, 0600) == -1 && *__error_nid_postfix() == 22); + Require(!std::filesystem::exists(invalidFlagsName)); + Require(open_nid_postfix(nullptr, 3, 0) == -1 && *__error_nid_postfix() == 22); + Require(open_nid_postfix("", 0x20a00, 0600) == -1 && *__error_nid_postfix() == 22); + Require(_open_nid_postfix(nullptr, 0x403) == -1 && *__error_nid_postfix() == 22); + Require(_open_nid_postfix("", 3) == -1 && *__error_nid_postfix() == 22); + Require(std::filesystem::file_size(present) == 5); Require(open_nid_postfix("", 0, 0) == -1 && *__error_nid_postfix() == 2); Require(_open_nid_postfix("", 0) == -1 && *__error_nid_postfix() == 2); Require(sceKernelOpen("", 0, 0) == static_cast(0x80020002u)); @@ -848,6 +1381,28 @@ int main() { Require(rmdir_nid_postfix((missingName + "/").c_str()) == -1 && *__error_nid_postfix() == 2); Require(sceKernelRmdir((presentName + "/").c_str()) == static_cast(0x80020014u)); Require(sceKernelRmdir((missingName + "/").c_str()) == static_cast(0x80020002u)); + const auto missingChild = missingName + "/child"; + for (const auto* blocked : {&underFile, &deepUnderFile, &missingChild}) { + const int expected = blocked == &missingChild ? 2 : 20; + FileStat blockedStatus{}; + Require(open_nid_postfix(blocked->c_str(), 0, 0) == -1 && *__error_nid_postfix() == expected); + Require(open_nid_postfix(blocked->c_str(), 0x202, 0644) == -1 && *__error_nid_postfix() == expected); + Require(stat_nid_postfix(blocked->c_str(), &blockedStatus) == -1 && *__error_nid_postfix() == expected); + Require(lstat_nid_postfix(blocked->c_str(), &blockedStatus) == -1 && *__error_nid_postfix() == expected); + Require(access_nid_postfix(blocked->c_str(), 0) == -1 && *__error_nid_postfix() == expected); + Require(unlink_nid_postfix(blocked->c_str()) == -1 && *__error_nid_postfix() == expected); + Require(chmod_nid_postfix(blocked->c_str(), 0644) == -1 && *__error_nid_postfix() == expected); + Require(sceKernelTruncate_nid_postfix(blocked->c_str(), 0) == static_cast(0x80020000u | static_cast(expected))); + Require(sceKernelUtimes_nid_postfix(blocked->c_str(), nullptr) == static_cast(0x80020000u | static_cast(expected))); + Require(rename_nid_postfix(blocked->c_str(), (root / "renamed-target").string().c_str()) == -1 && *__error_nid_postfix() == expected); + if (blocked != &missingChild) Require(rename_nid_postfix(presentName.c_str(), blocked->c_str()) == -1 && *__error_nid_postfix() == expected); + Require(remove_nid_postfix(blocked->c_str()) == -1 && *__error_nid_postfix() == expected); + Require(opendir_nid_postfix(blocked->c_str()) == nullptr && *__error_nid_postfix() == expected); + Require(chdir_nid_postfix(blocked->c_str()) == -1 && *__error_nid_postfix() == expected); + for (const char* openMode : {"r", "w"}) { + Require(fopen_nid_postfix(blocked->c_str(), openMode) == nullptr && *__error_nid_postfix() == expected); + } + } #ifndef _WIN32 const auto linkedDirectory = root / "linked-directory"; const auto directoryLink = root / "directory-link"; @@ -887,6 +1442,25 @@ int main() { const auto longLeaf = rootName + "/" + std::string(300, 'n'); Require(sceKernelMkdir(longLeaf.c_str(), 0700) == static_cast(0x8002003fu)); Require(mkdir_nid_postfix(longLeaf.c_str(), 0700) == -1 && *__error_nid_postfix() == 63); + FileStat unreachable{}; + Require(sceKernelOpen(longLeaf.c_str(), 0, 0) == static_cast(0x8002003fu)); + Require(sceKernelOpen(longLeaf.c_str(), 0x201, 0600) == static_cast(0x8002003fu)); + Require(open_nid_postfix(longLeaf.c_str(), 0, 0) == -1 && *__error_nid_postfix() == 63); + Require(sceKernelUnlink(longLeaf.c_str()) == static_cast(0x8002003fu)); + Require(unlink_nid_postfix(longLeaf.c_str()) == -1 && *__error_nid_postfix() == 63); + Require(sceKernelStat(longLeaf.c_str(), &unreachable) == static_cast(0x8002003fu)); + Require(stat_nid_postfix(longLeaf.c_str(), &unreachable) == -1 && *__error_nid_postfix() == 63); + const auto loop = root / "loop"; + std::filesystem::create_symlink("loop", loop); + const auto loopName = loop.string(); + const auto underLoop = loopName + "/leaf"; + Require(sceKernelOpen(loopName.c_str(), 0, 0) == static_cast(0x8002003eu)); + Require(open_nid_postfix(loopName.c_str(), 0, 0) == -1 && *__error_nid_postfix() == 62); + Require(sceKernelStat(loopName.c_str(), &unreachable) == static_cast(0x8002003eu)); + Require(stat_nid_postfix(loopName.c_str(), &unreachable) == -1 && *__error_nid_postfix() == 62); + Require(sceKernelUnlink(underLoop.c_str()) == static_cast(0x8002003eu)); + Require(unlink_nid_postfix(underLoop.c_str()) == -1 && *__error_nid_postfix() == 62); + Require(unlink_nid_postfix(loopName.c_str()) == 0 && !std::filesystem::is_symlink(loop)); #endif const auto invalidNameUnderFile = presentName + "/a:b/x"; Require(mkdir_nid_postfix(invalidNameUnderFile.c_str(), 0700) == -1 && *__error_nid_postfix() == 20); @@ -922,6 +1496,38 @@ int main() { Require(read_nid_postfix(random, second, 3) == 3); Require(std::memcmp(first, second, sizeof(first)) != 0 && second[3] == 0); Require(close_nid_postfix(random) == 0); + auto* stream = fopen_nid_postfix(device, "rb"); + Require(stream != nullptr); + Require(setvbuf_nid_postfix(stream, nullptr, 2, 0) == 0); + std::memset(first, 0, sizeof(first)); + std::memset(second, 0, sizeof(second)); + Require(fread_nid_postfix(first, 1, sizeof(first), stream) == sizeof(first)); + Require(fread_nid_postfix(second, 8, 8, stream) == 8); + Require(std::memcmp(first, second, sizeof(first)) != 0); + Require(feof_nid_postfix(stream) == 0 && ferror_nid_postfix(stream) == 0); + flockfile_nid_postfix(stream); + Require(fread_nid_postfix(first, 1, 4, stream) == 4); + funlockfile_nid_postfix(stream); + Require(ftrylockfile_nid_postfix(stream) == 0); + funlockfile_nid_postfix(stream); + Require(fflush_nid_postfix(stream) == 0); + bool unsupported = false; + try { + fgetc_nid_postfix(stream); + } catch (const std::runtime_error&) { + unsupported = true; + } + Require(unsupported); + Require(fclose_nid_postfix(stream) == 0); + for (const char* mode : {"wb", "ab", "r+b"}) { + bool rejected = false; + try { + fopen_nid_postfix(device, mode); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected); + } } Require(unlink_nid_postfix(presentName.c_str()) == 0 && !std::filesystem::exists(present)); const auto empty = root / "empty"; 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/GuestFlexibleMemoryBudget.cpp b/core/libs/tests/GuestFlexibleMemoryBudget.cpp index 7938cd2a8c..cba381e734 100644 --- a/core/libs/tests/GuestFlexibleMemoryBudget.cpp +++ b/core/libs/tests/GuestFlexibleMemoryBudget.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include "SceTypes.hpp" #include #include #include @@ -15,10 +16,20 @@ int APS5_VABI sceKernelConfiguredFlexibleMemorySize(std::size_t*); int APS5_VABI sceKernelAvailableFlexibleMemorySize(std::size_t*); int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); int APS5_VABI sceKernelReleaseFlexibleMemory(void*, std::size_t); +int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; +int APS5_VABI sceKernelVirtualQuery(const void*, int, VirtualQueryInfo*, std::uint64_t); std::uint64_t GuestImageSegmentBytes_nid_no_patch(); void* APS5_VABI dlopen_nid_postfix(const char*, int); char* APS5_VABI dlerror_nid_postfix(); int APS5_VABI dlclose_nid_postfix(void*); +std::size_t APS5_VABI sceKernelGetDirectMemorySize(); +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 sceKernelMapDirectMemory2(void**, std::size_t, int, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelMapNamedDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t, const char*); +int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry*, int, int*, int); +int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); +int APS5_VABI sceKernelVirtualQuery(const void*, int, VirtualQueryInfo*, std::uint64_t); } static void Require(bool condition, std::source_location location = std::source_location::current()) { @@ -28,6 +39,45 @@ static void Require(bool condition, std::source_location location = std::source_ } } +static void CheckPosixRelease(std::size_t before) { + constexpr std::size_t page = PS5_PAGE_SIZE; + const auto checkBudget = [&](const char* stage, std::size_t expected) { + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0); + std::printf("POSIX flexible release %s: available=%zu expected=%zu\n", stage, available, expected); + std::fflush(stdout); + Require(available == expected); + }; + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, page * 4, 3, 0) == 0); + auto* bytes = static_cast(mapped); + bytes[0] = 17; + bytes[page * 4 - 1] = 29; + checkBudget("allocated", before - page * 4); + auto* middle = static_cast(mapped) + page; + Require(munmap_nid_postfix(middle, page + 1) == 0); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(middle, 0, &info, sizeof(info)) != 0); + Require(sceKernelVirtualQuery(middle + page, 0, &info, sizeof(info)) != 0); + Require(bytes[0] == 17 && bytes[page * 4 - 1] == 29); + checkBudget("rounded partial unmap", before - page * 2); + Require(sceKernelReleaseFlexibleMemory(mapped, page) == 0); + checkBudget("kernel releases first page", before - page); + auto* last = static_cast(mapped) + page * 3; + Require(munmap_nid_postfix(last, 1) == 0); + Require(sceKernelVirtualQuery(last, 0, &info, sizeof(info)) != 0); + checkBudget("POSIX releases last page", before); + Require(munmap_nid_postfix(mapped, page * 4) == 0); + checkBudget("already unmapped", before); + mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, page * 3, 3, 0) == 0); + Require(munmap_nid_postfix(mapped, page * 3 - 1) == 0); + for (std::size_t offset = 0; offset < page * 3; offset += page) { + Require(sceKernelVirtualQuery(static_cast(mapped) + offset, 0, &info, sizeof(info)) != 0); + } + checkBudget("rounded full unmap", before); +} + int main(int argc, char** argv) { Require(argc == 2); const std::string_view mode = argv[1]; @@ -68,17 +118,129 @@ int main(int argc, char** argv) { Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - length / 2); Require(sceKernelReleaseFlexibleMemory(static_cast(mapped) + length / 2, length / 2) == 0); Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + CheckPosixRelease(before); void* oversized = nullptr; Require(sceKernelMapFlexibleMemory(&oversized, before + PS5_PAGE_SIZE, 3, 0) == SCE_KERNEL_ERROR_ENOMEM && oversized == nullptr); Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); if (mode == "limited") { + const auto checkBudget = [&](std::size_t expected) { + std::size_t current = 0; + Require(sceKernelAvailableFlexibleMemorySize(¤t) == 0); + std::printf("Fixed flexible mapping: available=%zu expected=%zu\n", current, expected); + std::fflush(stdout); + Require(current == expected); + }; + constexpr auto page = PS5_PAGE_SIZE; + void* original = nullptr; + Require(sceKernelMapFlexibleMemory(&original, page * 4, 3, 0) == 0); + auto* bytes = static_cast(original); + for (std::size_t i = 0; i < 4; ++i) bytes[i * page] = static_cast(17 + i); + auto* middle = static_cast(original) + page; + void* replacement = middle; + Require(sceKernelMapFlexibleMemory(&replacement, page, 3, 0x10) == 0 && replacement == middle); + Require(bytes[0] == 17 && bytes[page] == 0 && bytes[page * 2] == 19 && bytes[page * 3] == 20); + checkBudget(before - page * 4); + bytes[page] = 23; + Require(sceKernelMapFlexibleMemory(&replacement, page, 3, 0x90) == SCE_KERNEL_ERROR_ENOMEM); + Require(bytes[page] == 23); + checkBudget(before - page * 4); + replacement = original; + Require(sceKernelMapFlexibleMemory(&replacement, page, 3, 0x10) == 0 && replacement == original); + Require(bytes[0] == 0 && bytes[page] == 23 && bytes[page * 3] == 20); + checkBudget(before - page * 4); + Require(sceKernelMapFlexibleMemory(&replacement, page * 3, 3, 0x10) == 0 && replacement == original); + Require(bytes[0] == 0 && bytes[page] == 0 && bytes[page * 2] == 0 && bytes[page * 3] == 20); + checkBudget(before - page * 4); + Require(sceKernelReleaseFlexibleMemory(middle, page * 2) == 0); + checkBudget(before - page * 2); + replacement = middle; + Require(sceKernelMapFlexibleMemory(&replacement, page * 3, 3, 0x10) == 0 && replacement == middle); + Require(bytes[page] == 0 && bytes[page * 3] == 0); + checkBudget(before - page * 4); + Require(sceKernelReleaseFlexibleMemory(original, page * 4) == 0); + checkBudget(before); const auto length = before & ~(PS5_PAGE_SIZE - 1); void* full = nullptr; Require(sceKernelMapFlexibleMemory(&full, length, 3, 0) == 0); void* extra = nullptr; Require(sceKernelMapFlexibleMemory(&extra, PS5_PAGE_SIZE, 3, 0) == SCE_KERNEL_ERROR_ENOMEM && extra == nullptr); + static_cast(full)[0] = 31; + replacement = full; + Require(sceKernelMapFlexibleMemory(&replacement, length, 3, 0x90) == SCE_KERNEL_ERROR_ENOMEM); + Require(static_cast(full)[0] == 31); + checkBudget(before - length); + auto* fullBytes = static_cast(full); + fullBytes[page] = 37; + fullBytes[length - page] = 41; + auto* fullMiddle = static_cast(full) + page; + replacement = fullMiddle; + Require(sceKernelMapFlexibleMemory(&replacement, page, 3, 0x10) == 0 && replacement == fullMiddle); + Require(fullBytes[0] == 31 && fullBytes[page] == 0 && fullBytes[length - page] == 41); + checkBudget(before - length); + replacement = static_cast(full) + length - page; + Require(sceKernelMapFlexibleMemory(&replacement, page * 2, 3, 0x10) == SCE_KERNEL_ERROR_ENOMEM); + Require(fullBytes[0] == 31 && fullBytes[length - page] == 41); + checkBudget(before - length); + replacement = full; + Require(sceKernelMapFlexibleMemory(&replacement, length + page, 3, 0x10) == SCE_KERNEL_ERROR_ENOMEM); + Require(static_cast(full)[0] == 31); + checkBudget(before - length); + Require(sceKernelMapFlexibleMemory(&replacement, length, 3, 0x10) == 0 && replacement == full); + Require(static_cast(full)[0] == 0); + checkBudget(before - length); Require(sceKernelReleaseFlexibleMemory(full, length) == 0); Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + for (int api = 0; api < 4; ++api) { + constexpr auto directPage = PS5_PAGE_SIZE; + const auto checkDirectBudget = [&](std::size_t expected) { + std::size_t current = 0; + Require(sceKernelAvailableFlexibleMemorySize(¤t) == 0); + std::printf("Direct replacement API=%d available=%zu expected=%zu\n", api, current, expected); + std::fflush(stdout); + Require(current == expected); + }; + void* original = nullptr; + Require(sceKernelMapFlexibleMemory(&original, directPage * 4, 3, 0) == 0); + auto* bytes = static_cast(original); + bytes[0] = 17; + bytes[directPage * 4 - 1] = 29; + bytes[directPage] = 37; + bytes[directPage * 3 - 1] = 41; + std::int64_t physical = -1; + Require(sceKernelAllocateDirectMemory(0, sceKernelGetDirectMemorySize(), directPage * 2, directPage, 0, &physical) == 0); + const auto mapDirect = [&](void*& address, int flags) { + if (api == 0) return sceKernelMapDirectMemory(&address, directPage * 2, 3, flags, physical, directPage); + if (api == 1) return sceKernelMapDirectMemory2(&address, directPage * 2, 0, 3, flags, physical, directPage); + if (api == 2) return sceKernelMapNamedDirectMemory(&address, directPage * 2, 3, flags, physical, directPage, "budget direct"); + KernelBatchMapEntry entry{address, static_cast(physical), directPage * 2, 3, 0, 0, 0}; + int processed = -1; + const int result = sceKernelBatchMap2(&entry, 1, &processed, flags); + Require(processed == (result == 0 ? 1 : 0)); + address = entry.start; + return result; + }; + void* middle = static_cast(original) + directPage; + Require(mapDirect(middle, 0x90) == SCE_KERNEL_ERROR_ENOMEM); + Require(middle == static_cast(original) + directPage); + checkDirectBudget(before - directPage * 4); + Require(bytes[0] == 17 && bytes[directPage * 4 - 1] == 29); + Require(bytes[directPage] == 37 && bytes[directPage * 3 - 1] == 41); + Require(mapDirect(middle, 0x10) == 0 && middle == static_cast(original) + directPage); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(middle, 0, &info, sizeof(info)) == 0 && info.is_direct && !info.is_flexible); + Require(bytes[0] == 17 && bytes[directPage * 4 - 1] == 29); + checkDirectBudget(before - directPage * 2); + const auto recovered = before - directPage * 2; + void* reused = nullptr; + Require(sceKernelMapFlexibleMemory(&reused, recovered, 3, 0) == 0); + static_cast(reused)[recovered - 1] = 31; + Require(sceKernelReleaseFlexibleMemory(reused, recovered) == 0); + checkDirectBudget(before - directPage * 2); + Require(sceKernelReleaseDirectMemory(physical, directPage * 2) == 0); + checkDirectBudget(before - directPage * 2); + Require(sceKernelReleaseFlexibleMemory(original, directPage * 4) == 0); + checkDirectBudget(before); + } } if (module) { Require(dlclose_nid_postfix(module) == 0); diff --git a/core/libs/tests/GuestFloatBuiltins.cpp b/core/libs/tests/GuestFloatBuiltins.cpp new file mode 100644 index 0000000000..a64f17eb85 --- /dev/null +++ b/core/libs/tests/GuestFloatBuiltins.cpp @@ -0,0 +1,228 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +using Int128 = __int128; +using Uint128 = unsigned __int128; + +extern "C" { +double APS5_VABI __adddf3_nid_postfix(double, double); +float APS5_VABI __addsf3_nid_postfix(float, float); +double APS5_VABI __subdf3_nid_postfix(double, double); +float APS5_VABI __subsf3_nid_postfix(float, float); +double APS5_VABI __muldf3_nid_postfix(double, double); +float APS5_VABI __mulsf3_nid_postfix(float, float); +double APS5_VABI __divdf3_nid_postfix(double, double); +float APS5_VABI __divsf3_nid_postfix(float, float); +double APS5_VABI __negdf2_nid_postfix(double); +float APS5_VABI __negsf2_nid_postfix(float); +int APS5_VABI __eqdf2_nid_postfix(double, double); +int APS5_VABI __nedf2_nid_postfix(double, double); +int APS5_VABI __ltdf2_nid_postfix(double, double); +int APS5_VABI __ledf2_nid_postfix(double, double); +int APS5_VABI __gtdf2_nid_postfix(double, double); +int APS5_VABI __gedf2_nid_postfix(double, double); +int APS5_VABI __unorddf2_nid_postfix(double, double); +int APS5_VABI __eqsf2_nid_postfix(float, float); +int APS5_VABI __ltsf2_nid_postfix(float, float); +int APS5_VABI __gesf2_nid_postfix(float, float); +int APS5_VABI __unordsf2_nid_postfix(float, float); +double APS5_VABI __extendsfdf2_nid_postfix(float); +float APS5_VABI __truncdfsf2_nid_postfix(double); +std::int32_t APS5_VABI __fixsfsi_nid_postfix(float); +std::int64_t APS5_VABI __fixsfdi_nid_postfix(float); +Int128 APS5_VABI __fixsfti_nid_postfix(float); +std::int32_t APS5_VABI __fixdfsi_nid_postfix(double); +std::int64_t APS5_VABI __fixdfdi_nid_postfix(double); +Int128 APS5_VABI __fixdfti_nid_postfix(double); +std::uint32_t APS5_VABI __fixunssfsi_nid_postfix(float); +std::uint64_t APS5_VABI __fixunssfdi_nid_postfix(float); +Uint128 APS5_VABI __fixunssfti_nid_postfix(float); +std::uint32_t APS5_VABI __fixunsdfsi_nid_postfix(double); +std::uint64_t APS5_VABI __fixunsdfdi_nid_postfix(double); +Uint128 APS5_VABI __fixunsdfti_nid_postfix(double); +std::int64_t APS5_VABI __fixxfdi_nid_postfix(long double); +Int128 APS5_VABI __fixxfti_nid_postfix(long double); +std::uint32_t APS5_VABI __fixunsxfsi_nid_postfix(long double); +std::uint64_t APS5_VABI __fixunsxfdi_nid_postfix(long double); +Uint128 APS5_VABI __fixunsxfti_nid_postfix(long double); +double APS5_VABI __floatsidf_nid_postfix(std::int32_t); +double APS5_VABI __floatunsidf_nid_postfix(std::uint32_t); +float APS5_VABI __floatsisf_nid_postfix(std::int32_t); +float APS5_VABI __floatunsisf_nid_postfix(std::uint32_t); +double APS5_VABI __floatdidf_nid_postfix(std::int64_t); +float APS5_VABI __floatdisf_nid_postfix(std::int64_t); +long double APS5_VABI __floatdixf_nid_postfix(std::int64_t); +double APS5_VABI __floatundidf_nid_postfix(std::uint64_t); +float APS5_VABI __floatundisf_nid_postfix(std::uint64_t); +long double APS5_VABI __floatundixf_nid_postfix(std::uint64_t); +double APS5_VABI __floattidf_nid_postfix(Int128); +float APS5_VABI __floattisf_nid_postfix(Int128); +long double APS5_VABI __floattixf_nid_postfix(Int128); +double APS5_VABI __floatuntidf_nid_postfix(Uint128); +float APS5_VABI __floatuntisf_nid_postfix(Uint128); +long double APS5_VABI __floatuntixf_nid_postfix(Uint128); +__complex__ float APS5_VABI __mulsc3_nid_postfix(float, float, float, float); +__complex__ double APS5_VABI __muldc3_nid_postfix(double, double, double, double); +__complex__ long double APS5_VABI __mulxc3_nid_postfix(long double, long double, long double, long double); +__complex__ float APS5_VABI __divsc3_nid_postfix(float, float, float, float); +__complex__ double APS5_VABI __divdc3_nid_postfix(double, double, double, double); +__complex__ long double APS5_VABI __divxc3_nid_postfix(long double, long double, long double, long double); +long double APS5_VABI __powixf2_nid_postfix(long double, int); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static std::uint64_t Bits(double value) { return std::bit_cast(value); } +static std::uint32_t Bits(float value) { return std::bit_cast(value); } +static double Double(std::uint64_t bits) { return std::bit_cast(bits); } +static float Float(std::uint32_t bits) { return std::bit_cast(bits); } +static Uint128 Make(std::uint64_t high, std::uint64_t low) { return (static_cast(high) << 64) | low; } + +static constexpr unsigned GuestMxcsr = 0x1f80 | 0x6000 | 0x8040; + +static void CheckArithmetic() { + Require(__adddf3_nid_postfix(1., 2.) == 3. && __subsf3_nid_postfix(1.f, 2.f) == -1.f); + Require(__muldf3_nid_postfix(3., -4.) == -12. && __divsf3_nid_postfix(1.f, 4.f) == .25f); + Require(Bits(__adddf3_nid_postfix(INFINITY, -INFINITY)) == 0x7ff8000000000000ull); + Require(Bits(__muldf3_nid_postfix(0., INFINITY)) == 0x7ff8000000000000ull); + Require(Bits(__divdf3_nid_postfix(0., 0.)) == 0x7ff8000000000000ull); + Require(Bits(__subsf3_nid_postfix(INFINITY, INFINITY)) == 0x7fc00000u); + Require(Bits(__mulsf3_nid_postfix(0.f, -INFINITY)) == 0x7fc00000u); + Require(Bits(__adddf3_nid_postfix(Double(0xfff0000000000001ull), 1.)) == 0xfff8000000000001ull); + Require(Bits(__adddf3_nid_postfix(Double(0x7ff8000000000002ull), Double(0x7ff0000000000003ull))) == 0x7ff8000000000002ull); + Require(Bits(__muldf3_nid_postfix(2., Double(0x7ff0000000000003ull))) == 0x7ff8000000000003ull); + Require(Bits(__subdf3_nid_postfix(1., Double(0x7ff8000000000004ull))) == 0xfff8000000000004ull); + Require(Bits(__negdf2_nid_postfix(Double(0x7ff8000000000004ull))) == 0xfff8000000000004ull); + Require(Bits(__negsf2_nid_postfix(0.f)) == 0x80000000u); + Require(Bits(__divdf3_nid_postfix(-0x1p-1000, 0x1p30)) == 0x8000000000000000ull); + Require(Bits(__divsf3_nid_postfix(0x1p-120f, 0x1p10f)) == 0u); + Require(Bits(__divdf3_nid_postfix(0x1p-1022, 0x1.0000000000001p0)) == 0u); + Require(__divdf3_nid_postfix(0x1p-1000, 0x1p-22) == 0x1p-978 && __divsf3_nid_postfix(1.f, 3.f) == 1.f / 3.f); + const unsigned saved = _mm_getcsr(); + _mm_setcsr(GuestMxcsr); + const double sum = __adddf3_nid_postfix(1., 0x1.8p-53); + const float denormal = __mulsf3_nid_postfix(0x1p-126f, .5f); + const double quotient = __divdf3_nid_postfix(1., 3.); + const unsigned after = _mm_getcsr(); + _mm_setcsr(saved); + Require(sum == 1. + 0x1p-52); + Require(Bits(denormal) == 0x00400000u); + Require(Bits(quotient) == 0x3fd5555555555555ull); + Require(after == GuestMxcsr); +} + +static void CheckComparisons() { + Require(__ledf2_nid_postfix(1., 2.) == -1 && __ledf2_nid_postfix(2., 1.) == 1 && __ledf2_nid_postfix(2., 2.) == 0); + Require(__ltdf2_nid_postfix(-2., -1.) == -1 && __gtdf2_nid_postfix(-1., -2.) == 1); + Require(__eqdf2_nid_postfix(0., -0.) == 0 && __nedf2_nid_postfix(1., -1.) != 0); + Require(__ledf2_nid_postfix(NAN, 1.) == 1 && __gedf2_nid_postfix(1., NAN) == -1 && __gtdf2_nid_postfix(NAN, NAN) == -1); + Require(__unorddf2_nid_postfix(1., NAN) == 1 && __unorddf2_nid_postfix(1., INFINITY) == 0); + Require(__eqsf2_nid_postfix(-0.f, 0.f) == 0 && __ltsf2_nid_postfix(-INFINITY, -1e30f) == -1); + Require(__gesf2_nid_postfix(NAN, 0.f) == -1 && __unordsf2_nid_postfix(NAN, 0.f) == 1); + Require(__ledf2_nid_postfix(Double(0x0000000000000001ull), 0.) == 1); +} + +static void CheckFloatConversions() { + Require(Bits(__extendsfdf2_nid_postfix(Float(0x7f800001u))) == 0x7ff0000000000001ull); + Require(Bits(__extendsfdf2_nid_postfix(Float(0xffc00001u))) == 0xfff8000000000001ull); + Require(__extendsfdf2_nid_postfix(Float(1u)) == std::ldexp(1., -149)); + Require(__extendsfdf2_nid_postfix(Float(0x80400000u)) == -std::ldexp(1., -127)); + Require(__extendsfdf2_nid_postfix(1.5f) == 1.5 && __extendsfdf2_nid_postfix(-INFINITY) == -INFINITY); + Require(Bits(__truncdfsf2_nid_postfix(Double(0x7ff0000020000001ull))) == 0x7fc00001u); + Require(Bits(__truncdfsf2_nid_postfix(Double(0xfff4000000123456ull))) == 0xffd23456u); + const unsigned saved = _mm_getcsr(); + _mm_setcsr(GuestMxcsr); + const float third = __truncdfsf2_nid_postfix(1. / 3.); + const float tiny = __truncdfsf2_nid_postfix(0x1p-140); + _mm_setcsr(saved); + Require(Bits(third) == 0x3eaaaaabu && Bits(tiny) == 0x00000200u); +} + +static void CheckFloatToInteger() { + Require(__fixdfsi_nid_postfix(-3.7) == -3 && __fixsfsi_nid_postfix(.99f) == 0); + Require(__fixdfsi_nid_postfix(0x1p31) == INT32_MIN && __fixdfsi_nid_postfix(1e10) == 1410065408); + Require(__fixsfsi_nid_postfix(Float(0x7fc00000u)) == INT32_MIN); + Require(__fixdfdi_nid_postfix(0x1p63) == INT64_MIN && __fixdfdi_nid_postfix(-0x1p62) == -0x4000000000000000); + Require(__fixsfdi_nid_postfix(-0x1p40f) == -(1ll << 40)); + Require(__fixdfti_nid_postfix(0x1p100) == static_cast(Make(1ull << 36, 0))); + Require(__fixsfti_nid_postfix(-0x1p70f) == -static_cast(Make(1ull << 6, 0))); + Require(__fixdfdi_nid_postfix(0x1p64) == 0 && __fixdfdi_nid_postfix(0x1p120) == 1ll << 56); + Require(__fixdfti_nid_postfix(0x1p200) == static_cast(1) << 72); + Require(__fixunsdfsi_nid_postfix(-1.) == 0 && __fixunsdfsi_nid_postfix(4294967295.) == 0xffffffffu); + Require(__fixunsdfsi_nid_postfix(0x1p32) == 1); + Require(__fixunssfsi_nid_postfix(0x1p32f) == 0 && __fixunssfsi_nid_postfix(0x1p55f) == 1u << 23 && __fixunssfdi_nid_postfix(0x1p63f) == 1ull << 63); + Require(__fixunsdfdi_nid_postfix(18446744073709549568.) == 18446744073709549568ull); + Require(__fixunssfti_nid_postfix(0x1p127f) == Make(1ull << 63, 0) && __fixunsdfti_nid_postfix(-2.) == 0); + Require(__fixxfdi_nid_postfix(-5.5L) == -5 && __fixxfdi_nid_postfix(0x1p63L) == INT64_MIN); + Require(__fixunsxfdi_nid_postfix(0x1p64L) == 1 && __fixunsxfdi_nid_postfix(18446744073709551615.L) == ~0ull); + Require(__fixxfti_nid_postfix(-0x1p70L) == -static_cast(Make(1ull << 6, 0))); + Require(__fixunsxfti_nid_postfix(0x1.8p100L) == Make(3ull << 35, 0) && __fixunsxfti_nid_postfix(-1.L) == 0); + Require(__fixunsxfsi_nid_postfix(3e9L) == 3000000000u && __fixunsxfsi_nid_postfix(0x1p32L) == 1); +} + +static void CheckIntegerToFloat() { + Require(__floatsidf_nid_postfix(INT32_MIN) == -0x1p31 && __floatunsidf_nid_postfix(0xffffffffu) == 4294967295.); + Require(__floatdidf_nid_postfix(-3) == -3. && __floatdisf_nid_postfix(1ll << 40) == 0x1p40f); + Require(__floatundidf_nid_postfix(~0ull) == 0x1p64 && __floatundisf_nid_postfix(~0ull) == 0x1p64f); + Require(__floatdixf_nid_postfix(INT64_MIN) == -0x1p63L && __floatundixf_nid_postfix(~0ull) == 18446744073709551615.L); + const unsigned saved = _mm_getcsr(); + _mm_setcsr(GuestMxcsr); + const float tie = __floatsisf_nid_postfix(16777217); + const float up = __floatunsisf_nid_postfix(16777219u); + const float negative = __floatsisf_nid_postfix(-16777219); + const double wide = __floattidf_nid_postfix(static_cast(Make(0, (1ull << 63) | 0x601))); + _mm_setcsr(saved); + Require(tie == 16777216.f && up == 16777220.f && negative == -16777220.f); + Require(wide == 0x1p63 + 0x800); + Require(__floattidf_nid_postfix(static_cast(Make(1, 1))) == 0x1p64); + Require(__floattisf_nid_postfix(-static_cast(Make(1ull << 40, 0))) == -0x1p104f); + Require(__floatuntisf_nid_postfix(Make(~0ull, ~0ull)) == INFINITY); + Require(__floatuntidf_nid_postfix(Make(~0ull, ~0ull)) == 0x1p128); + Require(__floatuntidf_nid_postfix(0) == 0. && !std::signbit(__floattidf_nid_postfix(0))); + Require(__floattixf_nid_postfix(-static_cast(Make(1, 1))) == -0x1p64L); + Require(__floattixf_nid_postfix(-static_cast(Make(1, 3))) == -(0x1p64L + 4)); + Require(__floatuntixf_nid_postfix(Make(~0ull, ~0ull)) == 0x1p128L); + Require(__floatuntixf_nid_postfix(Make(0, ~0ull)) == 18446744073709551615.L); +} + +static void CheckComplex() { + const __complex__ float product = __mulsc3_nid_postfix(1.f, 2.f, 3.f, 4.f); + Require(__real__ product == -5.f && __imag__ product == 10.f); + const __complex__ double infinite = __muldc3_nid_postfix(INFINITY, NAN, 1., 0.); + Require(__real__ infinite == INFINITY); + const __complex__ long double extended = __mulxc3_nid_postfix(1.L, 1.L, 1.L, -1.L); + Require(__real__ extended == 2.L && __imag__ extended == 0.L); + const __complex__ double quotient = __divdc3_nid_postfix(6., 8., 3., 4.); + Require(__real__ quotient == 2. && __imag__ quotient == 0.); + const __complex__ float single = __divsc3_nid_postfix(1.f, 0.f, 0.f, 1.f); + Require(__real__ single == 0.f && __imag__ single == -1.f); + const __complex__ double byZero = __divdc3_nid_postfix(1., 0., 0., 0.); + Require(__real__ byZero == INFINITY); + const __complex__ long double large = __divxc3_nid_postfix(1e4000L, 1e4000L, 1e4000L, 1e4000L); + Require(__real__ large == 1.L && __imag__ large == 0.L); + const __complex__ double byInfinity = __divdc3_nid_postfix(1., 1., INFINITY, INFINITY); + Require(__real__ byInfinity == 0. && __imag__ byInfinity == 0.); +} + +int main() { + CheckArithmetic(); + CheckComparisons(); + CheckFloatConversions(); + CheckFloatToInteger(); + CheckIntegerToFloat(); + CheckComplex(); + Require(__powixf2_nid_postfix(2.L, 10) == 1024.L && __powixf2_nid_postfix(2.L, -2) == .25L); + Require(__powixf2_nid_postfix(-3.L, 3) == -27.L && __powixf2_nid_postfix(5.L, 0) == 1.L); + long double signaling; + const unsigned char signalingBytes[10] = {1, 0, 0, 0, 0, 0, 0, 0x80, 0xff, 0x7f}; + std::memset(&signaling, 0, sizeof(signaling)); + std::memcpy(&signaling, signalingBytes, sizeof(signalingBytes)); + const long double same = __powixf2_nid_postfix(signaling, 1); + Require(std::memcmp(&same, signalingBytes, sizeof(signalingBytes)) == 0); + return 0; +} diff --git a/core/libs/tests/GuestFloatingPoint.cpp b/core/libs/tests/GuestFloatingPoint.cpp new file mode 100644 index 0000000000..3ea9884bb0 --- /dev/null +++ b/core/libs/tests/GuestFloatingPoint.cpp @@ -0,0 +1,221 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +void APS5_VABI _Feraise_nid_postfix(int); +int APS5_VABI fegetround_nid_postfix(); +int APS5_VABI fesetround_nid_postfix(int); +int APS5_VABI fetestexcept_nid_postfix(int); +int APS5_VABI feclearexcept_nid_postfix(int); +int APS5_VABI fegetexceptflag_nid_postfix(std::uint16_t*, int); +int APS5_VABI fesetexceptflag_nid_postfix(const std::uint16_t*, int); +int APS5_VABI feenableexcept_nid_postfix(int); +int APS5_VABI fedisableexcept_nid_postfix(int); +int APS5_VABI __feenableexcept_nid_postfix(int); +int APS5_VABI __fedisableexcept_nid_postfix(int); +int APS5_VABI fegettrapenable_nid_postfix(); +int APS5_VABI fesettrapenable_nid_postfix(int); +} + +namespace { + +struct Environment { + std::array x87; + std::uint32_t sse; +}; + +Environment ReadEnvironment() { + Environment state; + asm volatile("fnstenv %0\n\tfldenv %0" : "=m"(state.x87)); + state.sse = _mm_getcsr(); + return state; +} + +void WriteEnvironment(const Environment& state) { + asm volatile("fldenv %0" : : "m"(state.x87)); + _mm_setcsr(state.sse); +} + +void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "floating-point environment: %s\n", message); + std::abort(); + } +} + +Environment SeedFlags(unsigned x87, unsigned sse) { + auto state = ReadEnvironment(); + state.x87[0] |= 0x3f; + state.x87[1] = (state.x87[1] & ~0xffu) | x87; + state.sse = (state.sse & ~0x3fu) | (0x3fu << 7) | sse; + WriteEnvironment(state); + return ReadEnvironment(); +} + +void CheckRounding() { + const auto seeded = SeedFlags(0x02, 0x10); + constexpr int modes[] = {0, 0x400, 0x800, 0xc00}; + constexpr int positive[] = {2, 1, 2, 1}; + constexpr int negative[] = {-2, -2, -1, -1}; + for (unsigned index = 0; index < 4; ++index) { + const auto before = ReadEnvironment(); + Require(fesetround_nid_postfix(modes[index]) == 0, "set rounding mode"); + Require(fegetround_nid_postfix() == modes[index], "query rounding mode"); + const auto state = ReadEnvironment(); + Require((state.x87[0] & 0xc00) == static_cast(modes[index]), "x87 rounding bits"); + Require((state.sse & 0x6000) == static_cast(modes[index] << 3), "SSE rounding bits"); + Require((state.x87[0] & ~0xc00u) == (seeded.x87[0] & ~0xc00u), "preserve x87 control bits"); + Require((state.sse & ~0x603fu) == (seeded.sse & ~0x603fu), "preserve SSE control bits"); + Require(state.x87[1] == before.x87[1] && (state.sse & 0x3f) == (before.sse & 0x3f), "rounding preserves exceptions"); + for (const double input : {1.75, -1.75}) { + const int expected = input > 0 ? positive[index] : negative[index]; + int x87; + asm volatile("fldl %1\n\tfistpl %0" : "=m"(x87) : "m"(input) : "st"); + Require(x87 == expected, "x87 arithmetic observes rounding"); + int sse; + asm volatile("cvtsd2si %1, %0" : "=r"(sse) : "x"(input)); + Require(sse == expected, "SSE arithmetic observes rounding"); + } + } + for (const int invalid : {-1, 1, 0x200, 0x1000, 0x7fffffff}) { + const auto before = ReadEnvironment(); + Require(fesetround_nid_postfix(invalid) == -1, "reject invalid rounding mode"); + const auto after = ReadEnvironment(); + Require(before.x87[0] == after.x87[0] && before.sse == after.sse, "invalid mode leaves state unchanged"); + } + auto distinct = ReadEnvironment(); + distinct.x87[0] = (distinct.x87[0] & ~0xc00u) | 0x400; + distinct.sse = (distinct.sse & ~0x6000u) | 0x4000; + WriteEnvironment(distinct); + Require(fegetround_nid_postfix() == 0x400, "query uses the x87 rounding mode"); +} + +void CheckFeraise() { + for (unsigned requested = 0; requested <= 0x3f; ++requested) { + const auto seeded = SeedFlags(0, 0); + *__error_nid_postfix() = 0; + _Feraise_nid_postfix(static_cast(requested)); + const unsigned raised = (requested & 0x18) != 0 ? requested | 0x20 : requested; + const auto state = ReadEnvironment(); + Require((state.x87[1] & 0x3f) == raised && (state.sse & 0x3f) == raised, "_Feraise raises the flags on both units"); + Require(state.x87[0] == seeded.x87[0] && (state.sse & ~0x3fu) == (seeded.sse & ~0x3fu), "_Feraise preserves control bits"); + const int error = (raised & 0x01) != 0 || (raised & 0x1c) == 0 ? 33 : 34; + Require(*__error_nid_postfix() == error, "_Feraise sets EDOM or ERANGE"); + } + SeedFlags(0, 0); +} + +void CheckFlags() { + const auto initial = SeedFlags(0x03, 0x24); + Require(fetestexcept_nid_postfix(0) == 0, "empty exception query"); + Require(fetestexcept_nid_postfix(0x3f) == 0x27, "combine x87 and SSE flags, including denormals"); + Require(fetestexcept_nid_postfix(0x0c) == 0x04, "query only requested flags"); + struct { std::uint16_t before, value, after; } saved{0x1234, 0xffff, 0x5678}; + Require(fegetexceptflag_nid_postfix(&saved.value, 0x07) == 0 && saved.value == 0x07, "save selected flags"); + Require(saved.before == 0x1234 && saved.after == 0x5678, "fexcept_t writes exactly two bytes"); + auto state = ReadEnvironment(); + Require(state.x87[1] == initial.x87[1] && state.sse == initial.sse, "queries do not change flags"); + + const std::uint16_t flags = 0x22; + Require(fesetexceptflag_nid_postfix(&flags, 0x0a) == 0, "restore selected flags"); + state = ReadEnvironment(); + Require((state.x87[1] & 0x3f) == 0x03 && (state.sse & 0x3f) == 0x26, "restore only selected flags on both units"); + Require(fesetexceptflag_nid_postfix(&flags, 0) == 0, "empty restore"); + Require(feclearexcept_nid_postfix(0x03) == 0, "clear selected flags"); + state = ReadEnvironment(); + Require((state.x87[1] & 0x3f) == 0 && (state.sse & 0x3f) == 0x24, "clear flags on both units"); + Require(state.x87[0] == initial.x87[0], "flag operations preserve x87 control"); + Require((state.sse & ~0x3fu) == (initial.sse & ~0x3fu), "flag operations preserve SSE control"); + Require(feclearexcept_nid_postfix(0) == 0 && fetestexcept_nid_postfix(0x3f) == 0x24, "empty clear"); + SeedFlags(0x3f, 0x3f); + Require(feclearexcept_nid_postfix(0x3f) == 0 && fetestexcept_nid_postfix(0x3f) == 0, "clear every exception"); + Require(fegetexceptflag_nid_postfix(&saved.value, 0) == 0 && saved.value == 0, "save empty mask"); + auto traps = ReadEnvironment(); + traps.x87[0] &= ~0x08u; + traps.sse &= ~(0x08u << 7); + WriteEnvironment(traps); + const std::uint16_t none = 0; + Require(fesetexceptflag_nid_postfix(&none, 0x01) == 0, "restore with an unmasked exception"); + Require(feclearexcept_nid_postfix(0x02) == 0, "clear with an unmasked exception"); + state = ReadEnvironment(); + Require(state.x87[0] == traps.x87[0] && state.sse == traps.sse, "flag updates preserve exception masks"); + SeedFlags(0, 0); + for (const bool get : {false, true}) { + bool threw = false; + try { + if (get) fegetexceptflag_nid_postfix(nullptr, 0x3f); + else fesetexceptflag_nid_postfix(nullptr, 0x3f); + } catch (const std::invalid_argument&) { threw = true; } + Require(threw, "reject null exception storage"); + } + CheckFeraise(); +} + +void CheckThreads() { + Require(fesetround_nid_postfix(0) == 0, "reset main rounding mode"); + Require(feclearexcept_nid_postfix(0x3f) == 0, "reset main exceptions"); + std::barrier ready(2); + const auto worker = [&](int rounding, std::uint16_t flags) { + Require(fesetround_nid_postfix(rounding) == 0, "thread rounding mode"); + Require(fesetexceptflag_nid_postfix(&flags, 0x3f) == 0, "thread exceptions"); + ready.arrive_and_wait(); + Require(fegetround_nid_postfix() == rounding, "thread-local rounding mode"); + Require(fetestexcept_nid_postfix(0x3f) == flags, "thread-local exception flags"); + }; + std::thread first(worker, 0x400, 0x01); + std::thread second(worker, 0x800, 0x04); + first.join(); + second.join(); + Require(fegetround_nid_postfix() == 0 && fetestexcept_nid_postfix(0x3f) == 0, "worker state does not change main"); +} + +} + +unsigned X87Masks() { + return ReadEnvironment().x87[0] & 0x3fu; +} + +unsigned SseMasks() { + return (ReadEnvironment().sse >> 7) & 0x3fu; +} + +void CheckTraps() { + const auto seeded = SeedFlags(0, 0); + Require(feenableexcept_nid_postfix(0x49) == static_cast(0xffffffc0u), "enable reports no previous trap as the console does"); + Require(X87Masks() == 0x36 && fegettrapenable_nid_postfix() == 0x09, "enable unmasks the x87 exceptions"); + Require(SseMasks() == 0x36, "enable unmasks the SSE exceptions"); + Require(fedisableexcept_nid_postfix(0x41) == static_cast(0xffffffc9u), "disable returns the traps enabled on both units"); + Require(X87Masks() == 0x37 && fegettrapenable_nid_postfix() == 0x08, "disable masks the x87 exception"); + Require(SseMasks() == 0x37, "disable masks the SSE exception"); + Require(__feenableexcept_nid_postfix(0x02) == static_cast(0xffffffc8u) && X87Masks() == 0x35, "the internal enable differs"); + Require(__fedisableexcept_nid_postfix(0x3f) == static_cast(0xffffffcau) && X87Masks() == 0x3f && fegettrapenable_nid_postfix() == 0, "the internal disable differs"); + Require(fesettrapenable_nid_postfix(0x40) == 1 && X87Masks() == 0x3f, "an out-of-range trap set was accepted"); + Require(fesettrapenable_nid_postfix(0x10004) == 0 && fegettrapenable_nid_postfix() == 0x04 && SseMasks() == 0x3b, "a trap set did not use the low 16 bits"); + Require(fesettrapenable_nid_postfix(0x01) == 0 && fegettrapenable_nid_postfix() == 0x05 && SseMasks() == 0x3a, "a trap set masked enabled traps"); + Require(fesettrapenable_nid_postfix(0) == 0 && fegettrapenable_nid_postfix() == 0x05 && SseMasks() == 0x3a, "an empty trap set masked enabled traps"); + auto mixed = ReadEnvironment(); + mixed.x87[0] = (mixed.x87[0] & ~0x3fu) | 0x36; + mixed.sse = (mixed.sse & ~(0x3fu << 7)) | (0x3au << 7); + WriteEnvironment(mixed); + Require(feenableexcept_nid_postfix(0) == static_cast(0xffffffc1u), "enable reports only the traps enabled on both units"); + Require(fegettrapenable_nid_postfix() == 0x09, "the trap query reads only the x87 control word"); + WriteEnvironment(seeded); +} + +int main() { + const auto original = ReadEnvironment(); + CheckRounding(); + CheckFlags(); + CheckTraps(); + CheckThreads(); + WriteEnvironment(original); + std::puts("floating-point environment checks passed"); +} diff --git a/core/libs/tests/GuestFminf.cpp b/core/libs/tests/GuestFminf.cpp index 75dd90152b..fcf48171a6 100644 --- a/core/libs/tests/GuestFminf.cpp +++ b/core/libs/tests/GuestFminf.cpp @@ -1,46 +1,79 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include #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))); + + const auto signaling = std::numeric_limits::signaling_NaN(); + std::feclearexcept(FE_ALL_EXCEPT); + Require(minimum(signaling, T(4.0)) == T(4.0) && minimum(T(4.0), signaling) == T(4.0)); + Require(maximum(signaling, T(4.0)) == T(4.0) && maximum(T(4.0), signaling) == T(4.0)); + Require(std::fetestexcept(FE_INVALID) == 0); + + Require(std::signbit(minimum(T(-0.0), T(+0.0)))); + Require(std::signbit(minimum(T(+0.0), T(-0.0)))); + Require(std::signbit(minimum(T(-0.0), T(-0.0)))); + 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..5fc1104e79 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -1,12 +1,18 @@ #include "prx/libSceFont/include/FontDriver.hpp" +#include "prx/libc/include/General.hpp" +#include "tests/FontFixture.hpp" +#include #include #include #include #include +#include #include +#include #include #include #include +#include #include #include @@ -31,6 +37,8 @@ int APS5_VABI sceFontSetScriptLanguage(FontHandle, int, int); int APS5_VABI sceFontGetScriptLanguage(FontHandle, int, int*); int APS5_VABI sceFontSetTypographicDesign(FontHandle, int, int); int APS5_VABI sceFontGetTypographicDesign(FontHandle, int, int*); +int APS5_VABI sceFontDefineAttribute(FontHandle, int, int*); +int APS5_VABI sceFontGetAttribute(FontHandle, int, int*); int APS5_VABI sceFontSetResolutionDpi(FontHandle, std::uint32_t, std::uint32_t); int APS5_VABI sceFontGetResolutionDpi(FontHandle, std::uint32_t*, std::uint32_t*); int APS5_VABI sceFontSetScalePixel(FontHandle, float, float); @@ -44,7 +52,8 @@ int APS5_VABI sceFontGetCharGlyphCode(FontHandle, std::uint32_t, std::uint32_t*) int APS5_VABI sceFontGetFontResolution(FontHandle, std::uint32_t*, float*); int APS5_VABI sceFontGetRenderScaledKerning(FontHandle, std::uint32_t, std::uint32_t, FontKerning*); int APS5_VABI sceFontGenerateCharGlyph(FontHandle, std::uint32_t, const FontGenerateGlyphDetail*, FontGlyph*); -int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph, std::uint32_t, std::uint64_t); +int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph, std::int32_t, std::int32_t*); +int APS5_VABI sceFontGlyphGetAttribute(FontGlyph, int, int*); int APS5_VABI sceFontDeleteGlyph(const FontMemory*, FontGlyph*); int APS5_VABI sceFontTextSourceInit(FontTextSource*, const void*, std::uint32_t, FontTextParseFunction, void*); int APS5_VABI sceFontCreateString(const FontMemory*, FontTextSource*, const FontCreateStringDetail*, FontString*); @@ -55,7 +64,22 @@ const FontWritingStep* APS5_VABI sceFontWritingRefersRenderStep(FontWriting*); FontTextCharacter* APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting*, const FontWritingLetterStep**); const void* APS5_VABI sceFontSelectLibraryFt(int); const void* APS5_VABI sceFontSelectRendererFt(int); +int APS5_VABI sceFontFtSupportBdf(FontLibrary library); +int APS5_VABI sceFontFtSupportCid(FontLibrary library); +int APS5_VABI sceFontFtSupportOpenType(FontLibrary library); +int APS5_VABI sceFontFtSupportOpenTypeOtf(FontLibrary library); +int APS5_VABI sceFontFtSupportOpenTypeTtf(FontLibrary library); +int APS5_VABI sceFontFtSupportPcf(FontLibrary library); +int APS5_VABI sceFontFtSupportPfr(FontLibrary library); +int APS5_VABI sceFontFtSupportTrueType(FontLibrary library); +int APS5_VABI sceFontFtSupportTrueTypeGx(FontLibrary library); +int APS5_VABI sceFontFtSupportType1(FontLibrary library); +int APS5_VABI sceFontFtSupportType42(FontLibrary library); +int APS5_VABI sceFontFtSupportWinFonts(FontLibrary library); 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) { @@ -68,9 +92,9 @@ static void Check(bool value, int line) { static void SetFontDirectory(const std::filesystem::path& directory) { #ifdef _WIN32 - Require(_putenv_s("ANYPS5_SYSTEM_FONTS", directory.string().c_str()) == 0); + Require(_wputenv_s(L"ANYPS5_SYSTEM_FONTS", directory.wstring().c_str()) == 0); #else - Require(::setenv("ANYPS5_SYSTEM_FONTS", directory.string().c_str(), 1) == 0); + Require(::setenv("ANYPS5_SYSTEM_FONTS", directory.c_str(), 1) == 0); #endif } @@ -88,41 +112,14 @@ static std::uint32_t APS5_VABI CoarsePixelResolution() { return 16; } -static void Put16(std::vector& out, int value) { - out.push_back(static_cast((value >> 8) & 0xFF)); - out.push_back(static_cast(value & 0xFF)); +static int APS5_VABI WideFontResolution(Font::FontObj*, std::uint32_t* resolution, float* scale) { + *resolution = 0x12345678; + *scale = 12.5f; + return SCE_FONT_OK; } -static std::vector Words(std::initializer_list values) { - std::vector out; - for (const int value : values) Put16(out, value); - return out; -} - -static std::vector BuildFont(int glyphCount, std::map> tables) { - tables["glyf"] = Words({0, 0}); - tables["head"] = Words({1, 0, 1, 0, 0, 0, 0x5F0F, 0x3CF5, 0, 1000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 2, 0, 0}); - tables["hhea"] = Words({1, 0, 800, -200, 0, 500, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1}); - tables["hmtx"] = Words({500}); - tables["hmtx"].resize(2 + 2 * static_cast(glyphCount)); - tables["loca"].assign(2 * (static_cast(glyphCount) + 1), 0); - tables["maxp"] = Words({1, 0, glyphCount, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}); - const int tableCount = static_cast(tables.size()); - int entrySelector = 0; - while ((2 << entrySelector) <= tableCount) ++entrySelector; - std::vector font = Words({1, 0, tableCount, 16 << entrySelector, entrySelector, 16 * tableCount - (16 << entrySelector)}); - int offset = 12 + 16 * tableCount; - for (const auto& table : tables) { - font.insert(font.end(), table.first.begin(), table.first.end()); - for (const int value : {0, 0, offset >> 16, offset & 0xFFFF, 0, static_cast(table.second.size())}) Put16(font, value); - offset += static_cast((table.second.size() + 3) & ~std::size_t{3}); - } - for (const auto& table : tables) { - font.insert(font.end(), table.second.begin(), table.second.end()); - font.resize((font.size() + 3) & ~std::size_t{3}); - } - return font; -} +using FontFixture::BuildFont; +using FontFixture::Words; static std::vector EmptyGlyphFont() { return BuildFont(1, {}); @@ -153,6 +150,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; } @@ -172,11 +223,32 @@ int main() { FontLibrary library = nullptr; Require(sceFontCreateLibrary(&memory, nullptr, &library) == SCE_FONT_ERROR_INVALID_PARAMETER && library == nullptr); Require(sceFontCreateLibrary(&memory, sceFontSelectLibraryFt(0), &library) == SCE_FONT_OK && library != nullptr); + Require(sceFontFtSupportBdf(library) == 0 && sceFontFtSupportCid(library) == 0 && sceFontFtSupportOpenType(library) == 0 && sceFontFtSupportOpenTypeOtf(library) == 0 && sceFontFtSupportOpenTypeTtf(library) == 0 && sceFontFtSupportPcf(library) == 0 && sceFontFtSupportPfr(library) == 0 && sceFontFtSupportTrueType(library) == 0 && sceFontFtSupportTrueTypeGx(library) == 0 && sceFontFtSupportType1(library) == 0 && sceFontFtSupportType42(library) == 0 && sceFontFtSupportWinFonts(library) == 0); + const auto* nativeLibrary = static_cast(library); + const auto savedLock = nativeLibrary->lock_word; + for (auto support : {sceFontFtSupportBdf, sceFontFtSupportCid, sceFontFtSupportOpenType, sceFontFtSupportOpenTypeOtf, sceFontFtSupportOpenTypeTtf, sceFontFtSupportPcf, sceFontFtSupportPfr, sceFontFtSupportTrueType, sceFontFtSupportTrueTypeGx, sceFontFtSupportType1, sceFontFtSupportType42, sceFontFtSupportWinFonts}) { + Font::FontLibNative invalidLibrary{}; + Require(support(nullptr) == SCE_FONT_ERROR_INVALID_LIBRARY); + Require(support(&invalidLibrary) == SCE_FONT_ERROR_INVALID_LIBRARY); + Require(support(library) == SCE_FONT_OK); + Require(nativeLibrary->lock_word == savedLock); + } + + Font::FontLibNative foreignLibrary{}; + foreignLibrary.magic = Font::LIBRARY_MAGIC; + foreignLibrary.lock_word = 5; + bool foreignDriverRejected = false; + try { + static_cast(sceFontFtSupportTrueType(&foreignLibrary)); + } catch (const std::runtime_error&) { + foreignDriverRejected = true; + } + Require(foreignDriverRejected && foreignLibrary.lock_word == 5); std::uint32_t subPixelCount = 1; Require(sceFontGetPixelResolution(library, &subPixelCount) == SCE_FONT_OK && subPixelCount == 64); Require(sceFontGetPixelResolution(library, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); - Require(sceFontGetPixelResolution(nullptr, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontGetPixelResolution(nullptr, nullptr) == SCE_FONT_ERROR_INVALID_LIBRARY); subPixelCount = 1; Require(sceFontGetPixelResolution(nullptr, &subPixelCount) == SCE_FONT_ERROR_INVALID_LIBRARY && subPixelCount == 0); FontHandleOpaque notALibrary{}; @@ -188,13 +260,18 @@ int main() { Require(sceFontCreateLibrary(&memory, &coarseDriver, &coarseLibrary) == SCE_FONT_OK && coarseLibrary != nullptr); Require(sceFontGetPixelResolution(coarseLibrary, &subPixelCount) == SCE_FONT_OK && subPixelCount == 16); Require(sceFontGetPixelResolution(library, &subPixelCount) == SCE_FONT_OK && subPixelCount == 64); + Require(static_cast(coarseLibrary)->lock_word == 0); coarseDriver.pixel_resolution = nullptr; Require(sceFontGetPixelResolution(coarseLibrary, &subPixelCount) == SCE_FONT_ERROR_INVALID_LIBRARY && subPixelCount == 0); + Require(sceFontGetPixelResolution(coarseLibrary, nullptr) == SCE_FONT_ERROR_INVALID_LIBRARY); + Require(static_cast(coarseLibrary)->lock_word == 0); Require(sceFontDestroyLibrary(&coarseLibrary) == SCE_FONT_OK && coarseLibrary == nullptr); - void* cacheBuffer = nullptr; + void* cacheBuffer = ¬ALibrary; std::uint32_t cacheSize = 0; + cacheSize = 123; Require(sceFontDettachDeviceCacheBuffer(nullptr, &cacheBuffer, &cacheSize) == SCE_FONT_ERROR_INVALID_LIBRARY); + Require(cacheBuffer == nullptr && cacheSize == 0); Require(sceFontDettachDeviceCacheBuffer(¬ALibrary, &cacheBuffer, &cacheSize) == SCE_FONT_ERROR_INVALID_LIBRARY); Require(sceFontClearDeviceCache(library) == SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER); Require(sceFontDettachDeviceCacheBuffer(library, &cacheBuffer, &cacheSize) == SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER); @@ -205,13 +282,15 @@ int main() { Require(sceFontAttachDeviceCacheBuffer(library, callerCache.data(), static_cast(callerCache.size())) == SCE_FONT_ERROR_ALREADY_ATTACHED); Require(sceFontClearDeviceCache(library) == SCE_FONT_OK); Require(sceFontDettachDeviceCacheBuffer(library, &cacheBuffer, &cacheSize) == SCE_FONT_OK && cacheBuffer == callerCache.data() && cacheSize == callerCache.size()); + for (std::size_t offset = 4; offset < 24; ++offset) Require(callerCache[offset] == 0); Require(sceFontDettachDeviceCacheBuffer(library, &cacheBuffer, &cacheSize) == SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER); Require(sceFontAttachDeviceCacheBuffer(library, nullptr, 0x2000) == SCE_FONT_OK); cacheBuffer = nullptr; cacheSize = 0; - Require(sceFontDettachDeviceCacheBuffer(library, &cacheBuffer, &cacheSize) == SCE_FONT_OK && cacheBuffer != nullptr && cacheSize == 0x2000); - Release(nullptr, cacheBuffer); + const int allocationsBeforeDetach = allocations; + Require(sceFontDettachDeviceCacheBuffer(library, &cacheBuffer, &cacheSize) == SCE_FONT_OK && cacheBuffer == nullptr && cacheSize == 0); + Require(allocations == allocationsBeforeDetach - 1); Require(sceFontDettachDeviceCacheBuffer(library, &cacheBuffer, &cacheSize) == SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER); Require(sceFontAttachDeviceCacheBuffer(library, nullptr, 0x2000) == SCE_FONT_OK); @@ -235,18 +314,131 @@ int main() { Require(sceFontOpenFontMemory(library, notAFont, sizeof(notAFont), nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FORMAT && font == nullptr); const std::vector fontData = EmptyGlyphFont(); + const auto fileFont = fontRoot / std::filesystem::path(u8"external-a\u00e7\u00e3o-\u65e5\u672c") / "font.ttf"; + std::filesystem::create_directories(fileFont.parent_path()); + { + std::ofstream file(fileFont, std::ios::binary); + file.write(reinterpret_cast(fontData.data()), static_cast(fontData.size())); + Require(static_cast(file)); + } + const std::u8string fontRootText = fontRoot.u8string(); + AddPathAlias_nid_no_patch("/font-test", reinterpret_cast(fontRootText.c_str())); + const std::u8string fileFontText = u8"/font-test/external-a\u00e7\u00e3o-\u65e5\u672c/font.ttf"; + FontHandle fileHandle = nullptr; + Require(sceFontOpenFontFile(library, reinterpret_cast(fileFontText.c_str()), 1, nullptr, &fileHandle) == SCE_FONT_OK && fileHandle != nullptr); + Require(sceFontCloseFont(fileHandle) == SCE_FONT_OK); + RemovePathAlias_nid_no_patch("/font-test"); Require(sceFontOpenFontMemory(library, fontData.data(), static_cast(fontData.size()), nullptr, &font) == SCE_FONT_OK && font != nullptr); int setting = -1; - Require(sceFontGetScriptLanguage(font, 3, &setting) == SCE_FONT_OK && setting == 0); - Require(sceFontSetScriptLanguage(font, 3, 7) == SCE_FONT_OK && sceFontSetScriptLanguage(font, 4, 9) == SCE_FONT_OK); - Require(sceFontGetScriptLanguage(font, 3, &setting) == SCE_FONT_OK && setting == 7); - Require(sceFontGetScriptLanguage(font, 4, &setting) == SCE_FONT_OK && setting == 9); - Require(sceFontGetScriptLanguage(font, 3, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontGetScriptLanguage(font, 3, &setting) == SCE_FONT_ERROR_INVALID_PARAMETER && setting == 0); + Require(sceFontSetScriptLanguage(font, 3, 7) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontSetScriptLanguage(font, 0x100, 0x106) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontSetScriptLanguage(font, 0x600, 0x603) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontSetScriptLanguage(font, 0x3000, 0x3015) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontGetScriptLanguage(font, 0x100, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); Require(sceFontSetScriptLanguage(nullptr, 3, 7) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); - Require(sceFontSetTypographicDesign(font, 1, 2) == SCE_FONT_OK); - Require(sceFontGetTypographicDesign(font, 1, &setting) == SCE_FONT_OK && setting == 2); - Require(sceFontGetTypographicDesign(font, 5, &setting) == SCE_FONT_OK && setting == 0); - Require(sceFontGetTypographicDesign(nullptr, 1, &setting) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontGetScriptLanguage(nullptr, 3, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + for (const auto script : {0x100, 0x600, 0x3000}) { + bool setThrew = false; + bool getThrew = false; + try { + sceFontSetScriptLanguage(font, script, script); + } catch (const std::logic_error&) { + setThrew = true; + } + try { + sceFontGetScriptLanguage(font, script, &setting); + } catch (const std::logic_error&) { + getThrew = true; + } + Require(setThrew && getThrew && setting == 0); + } + Require(sceFontSetTypographicDesign(font, 1, 2) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontSetTypographicDesign(font, 17, 1) == SCE_FONT_ERROR_INVALID_PARAMETER); + for (const auto selector : {-1, 0, 2, 5, 16, 18, 65536, 65540}) { + Require(sceFontSetTypographicDesign(font, selector, 0) == SCE_FONT_ERROR_INVALID_PARAMETER); + setting = 123; + Require(sceFontGetTypographicDesign(font, selector, &setting) == SCE_FONT_ERROR_INVALID_PARAMETER && setting == 0); + } + Require(sceFontGetTypographicDesign(nullptr, 1, &setting) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && setting == 0); + Require(sceFontGetTypographicDesign(nullptr, 1, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontSetTypographicDesign(nullptr, 1, 2) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontSetTypographicDesign(font, 65537, 0) == SCE_FONT_ERROR_INVALID_PARAMETER); + for (const auto selector : {1, 17, 65537, 65538, 65539}) { + Require(sceFontGetTypographicDesign(font, selector, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + bool getThrew = false; + try { + sceFontGetTypographicDesign(font, selector, &setting); + } catch (const std::logic_error&) { + getThrew = true; + } + Require(getThrew && setting == 0); + if (selector == 65537) continue; + for (const auto feature : {0, selector == 1 ? 1 : 32}) { + bool setThrew = false; + try { + sceFontSetTypographicDesign(font, selector, feature); + } catch (const std::logic_error&) { + setThrew = true; + } + Require(setThrew); + } + } + std::array typographyRecord{}; + auto* nativeFont = reinterpret_cast(font); + nativeFont->open_info.fontset_record = typographyRecord.data(); + for (const auto selector : {65537, 65538, 65539}) { + Require(sceFontSetTypographicDesign(font, selector, 0) == SCE_FONT_ERROR_NO_SUPPORT_TYPOGRAPHY); + setting = 123; + Require(sceFontGetTypographicDesign(font, selector, &setting) == SCE_FONT_ERROR_NO_SUPPORT_TYPOGRAPHY && setting == 0); + Require(sceFontGetTypographicDesign(font, selector, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + } + const std::uint32_t typographyCount = 1; + const std::int32_t typographyFontId = 10; + std::memcpy(typographyRecord.data() + 32, &typographyCount, sizeof(typographyCount)); + std::memcpy(typographyRecord.data() + 36, &typographyFontId, sizeof(typographyFontId)); + Require(sceFontSetTypographicDesign(font, 65537, 0) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontGetTypographicDesign(font, 65538, &setting) == SCE_FONT_ERROR_NO_SUPPORT_TYPOGRAPHY); + bool typographyQueryThrew = false; + try { + sceFontGetTypographicDesign(font, 65537, &setting); + } catch (const std::logic_error&) { + typographyQueryThrew = true; + } + Require(typographyQueryThrew && setting == 0); + nativeFont->open_info.fontset_record = nullptr; + int attribute = -1; + Require(sceFontGetAttribute(font, 0x11, &attribute) == SCE_FONT_OK && attribute == 0); + Require(sceFontDefineAttribute(font, 0x11, &attribute) == SCE_FONT_OK && attribute == 0); + attribute = -1; + Require(sceFontGetAttribute(font, 0x99, &attribute) == SCE_FONT_OK && attribute == 0x11); + Require(sceFontDefineAttribute(font, 0, &attribute) == SCE_FONT_OK && attribute == 0x11); + Require(sceFontDefineAttribute(font, 5, nullptr) == SCE_FONT_OK); + attribute = -1; + Require(sceFontGetAttribute(font, 0, &attribute) == SCE_FONT_OK && attribute == 5); + Require(sceFontGetAttribute(font, 0x11, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + attribute = -1; + Require(sceFontDefineAttribute(nullptr, 0x11, &attribute) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && attribute == -1); + Require(sceFontGetAttribute(nullptr, 0x11, &attribute) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && attribute == -1); + FontHandle otherFont = nullptr; + Require(sceFontOpenFontMemory(library, fontData.data(), static_cast(fontData.size()), nullptr, &otherFont) == SCE_FONT_OK && otherFont != nullptr); + attribute = -1; + Require(sceFontGetAttribute(otherFont, 0, &attribute) == SCE_FONT_OK && attribute == 0); + Require(sceFontDefineAttribute(otherFont, 7, &attribute) == SCE_FONT_OK && attribute == 0); + attribute = -1; + Require(sceFontGetAttribute(otherFont, 0, &attribute) == SCE_FONT_OK && attribute == 7); + Require(sceFontGetAttribute(font, 0, &attribute) == SCE_FONT_OK && attribute == 5); + Require(sceFontCloseFont(otherFont) == SCE_FONT_OK); + auto* resolutionLibrary = static_cast(library); + const Font::SysDriver* originalDriver = resolutionLibrary->sys_driver; + Font::SysDriver wideResolutionDriver = *originalDriver; + wideResolutionDriver.scale = WideFontResolution; + resolutionLibrary->sys_driver = &wideResolutionDriver; + std::uint32_t wideResolution = 0; + float wideScale = 0; + Require(sceFontGetFontResolution(font, &wideResolution, &wideScale) == SCE_FONT_OK); + Require(wideResolution == 0x12345678 && wideScale == 12.5f); + resolutionLibrary->sys_driver = originalDriver; std::uint32_t hDpi = 1; std::uint32_t vDpi = 1; Require(sceFontGetResolutionDpi(font, &hDpi, &vDpi) == SCE_FONT_OK && hDpi == 72 && vDpi == 72); @@ -299,8 +491,16 @@ int main() { font = nullptr; Require(sceFontOpenFontMemory(library, kerningData.data(), static_cast(kerningData.size()), nullptr, &font) == SCE_FONT_OK && font != nullptr); Require(sceFontGetFontGlyphsCount(font, &glyphsCount) == SCE_FONT_OK && glyphsCount == 3); - Require(sceFontGetCharGlyphCode(font, 'A', &glyphCode) == SCE_FONT_OK && glyphCode == 1); - Require(sceFontGetCharGlyphCode(font, 'V', &glyphCode) == SCE_FONT_OK && glyphCode == 2); + Require(sceFontGetCharGlyphCode(font, 'A', &glyphCode) == SCE_FONT_OK && glyphCode == 0x80000001u); + Require(sceFontGetCharGlyphCode(font, 'V', &glyphCode) == SCE_FONT_OK && glyphCode == 0x80000002u); + Require(sceFontGetCharGlyphCode(nullptr, 0, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontGetFontGlyphsCount(nullptr, nullptr) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + Require(sceFontGetCharGlyphCode(font, 0, nullptr) == SCE_FONT_ERROR_NO_SUPPORT_CODE); + Require(sceFontGetCharGlyphCode(font, 0x80000000u, &glyphCode) == SCE_FONT_OK && glyphCode == 0x80000000u); + Require(sceFontGetCharGlyphCode(font, 0x80000002u, &glyphCode) == SCE_FONT_OK && glyphCode == 0x80000002u); + Require(sceFontGetCharGlyphCode(font, 0xc0000001u, &glyphCode) == SCE_FONT_OK && glyphCode == 0x80000001u); + Require(sceFontGetCharGlyphCode(font, 0x80000003u, &glyphCode) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH && glyphCode == 0); + Require(sceFontGetCharGlyphCode(font, 0x80010001u, &glyphCode) == SCE_FONT_ERROR_NO_SUPPORT_CODE && glyphCode == 0); glyphCode = 1; Require(sceFontGetCharGlyphCode(font, 'B', &glyphCode) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH && glyphCode == 0); glyphCode = 1; @@ -309,10 +509,31 @@ int main() { Require(sceFontSetScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); FontGlyph glyph = nullptr; Require(sceFontGenerateCharGlyph(font, 'A', nullptr, &glyph) == SCE_FONT_OK && glyph != nullptr); - Require(sceFontGlyphDefineAttribute(glyph, 0x11, 0) == SCE_FONT_OK); - Require(sceFontGlyphDefineAttribute(nullptr, 0x11, 0) == SCE_FONT_ERROR_INVALID_GLYPH); + const auto originalFlags = glyph->flags; + std::int32_t previousAttribute = -1; + Require(sceFontGlyphDefineAttribute(glyph, 0x11, &previousAttribute) == SCE_FONT_OK); + Require(previousAttribute == ((originalFlags & 2) ? 0x11 : 0x10)); + Require(glyph->flags == (originalFlags | 2)); + Require(sceFontGlyphDefineAttribute(glyph, 0x10, &previousAttribute) == SCE_FONT_OK && previousAttribute == 0x11); + Require(glyph->flags == (originalFlags & ~2u)); + Require(sceFontGlyphDefineAttribute(glyph, 0x10, &previousAttribute) == SCE_FONT_OK && previousAttribute == 0x10); + Require(sceFontGlyphDefineAttribute(glyph, 0x11, nullptr) == SCE_FONT_OK); + for (std::int32_t attribute = -1; attribute <= 256; ++attribute) { + if (attribute == 0x10 || attribute == 0x11) continue; + previousAttribute = -1; + Require(sceFontGlyphDefineAttribute(glyph, attribute, &previousAttribute) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(previousAttribute == 0 && glyph->flags == (originalFlags | 2)); + } + previousAttribute = -1; + Require(sceFontGlyphDefineAttribute(nullptr, 0x11, &previousAttribute) == SCE_FONT_ERROR_INVALID_GLYPH && previousAttribute == 0); + Require(sceFontGlyphDefineAttribute(nullptr, 0x11, nullptr) == SCE_FONT_ERROR_INVALID_GLYPH); FontGlyphOpaque notAGlyph{}; - Require(sceFontGlyphDefineAttribute(¬AGlyph, 0x11, 0) == SCE_FONT_ERROR_INVALID_GLYPH); + previousAttribute = -1; + Require(sceFontGlyphDefineAttribute(¬AGlyph, 0x11, &previousAttribute) == SCE_FONT_ERROR_INVALID_GLYPH && previousAttribute == 0); + int glyphAttribute = -1; + Require(sceFontGlyphGetAttribute(glyph, 0x11, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0x11); + Require(sceFontGlyphGetAttribute(glyph, 0x11, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontGlyphGetAttribute(nullptr, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH); Require(sceFontDeleteGlyph(&memory, &glyph) == SCE_FONT_OK && glyph == nullptr); FontKerning kerning{1.0f, 1.0f, 1.0f, 1.0f}; Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -20.0f)); @@ -363,6 +584,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/GuestFontWritingLine.cpp b/core/libs/tests/GuestFontWritingLine.cpp index 7b40b889ca..5c311aede3 100644 --- a/core/libs/tests/GuestFontWritingLine.cpp +++ b/core/libs/tests/GuestFontWritingLine.cpp @@ -3,6 +3,7 @@ #include #include #include +#include extern "C" { int APS5_VABI sceFontCreateWritingLine(const FontMemory*, std::int32_t, const void*, void**); @@ -32,12 +33,28 @@ static bool Throws(F f) { return false; } -struct WritingLineDetail { - std::uint16_t detailId; - std::uint16_t reserved[3]; - void* reservedPointers[3]; +struct AllocationState { + std::size_t live = 0; + std::uint32_t lastSize = 0; + bool fail = false; }; +static void* APS5_VABI Allocate(void* object, std::uint32_t size) { + auto& state = *static_cast(object); + state.lastSize = size; + if (state.fail) return nullptr; + void* allocation = std::malloc(size); + if (allocation) ++state.live; + return allocation; +} + +static void APS5_VABI Deallocate(void* object, void* allocation) { + auto& state = *static_cast(object); + Require(allocation && state.live > 0); + --state.live; + std::free(allocation); +} + constexpr std::uint64_t SPACE_BEFORE = std::uint64_t{1} << 30; constexpr std::uint64_t SPACE_AFTER = std::uint64_t{1} << 31; @@ -80,15 +97,27 @@ static bool OrderIs(void* line, const std::uint64_t* levels, const std::size_t* } int main() { + AllocationState allocations; + FontMemoryInterface memoryInterface{}; + memoryInterface.alloc = Allocate; + memoryInterface.dealloc = Deallocate; FontMemory memory{}; void* line = nullptr; - Require(Throws([&] { sceFontCreateWritingLine(&memory, 0x10, nullptr, nullptr); })); - Require(Throws([&] { sceFontCreateWritingLine(&memory, 0x13, nullptr, &line); }) && line == nullptr); - Require(Throws([&] { sceFontCreateWritingLine(&memory, 0, nullptr, &line); }) && line == nullptr); - WritingLineDetail detail{0x0FD4, {}, {}}; - Require(Throws([&] { sceFontCreateWritingLine(&memory, 0x10, &detail, &line); }) && line == nullptr); - detail.detailId = 0x0FD5; - Require(sceFontCreateWritingLine(&memory, 0x10, &detail, &line) == SCE_FONT_OK && line != nullptr); + Require(sceFontCreateWritingLine(nullptr, 0x12, nullptr, &line) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontCreateWritingLine(&memory, 0x12, nullptr, &line) == SCE_FONT_ERROR_INVALID_MEMORY); + memory.mem_kind = 0xF00; + memory.mspace_handle = &allocations; + Require(sceFontCreateWritingLine(&memory, 0x12, nullptr, &line) == SCE_FONT_ERROR_INVALID_MEMORY); + memory.iface = &memoryInterface; + Require(sceFontCreateWritingLine(&memory, 0x12, nullptr, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + for (int form : {0, 0x10, 0x13}) Require(sceFontCreateWritingLine(&memory, form, nullptr, &line) == SCE_FONT_ERROR_INVALID_PARAMETER && !line); + std::uint16_t detail = 0x0FD5; + Require(sceFontCreateWritingLine(&memory, 0x12, &detail, &line) == SCE_FONT_ERROR_INVALID_PARAMETER && !line); + allocations.fail = true; + Require(sceFontCreateWritingLine(&memory, 0x12, nullptr, &line) == SCE_FONT_ERROR_ALLOCATION_FAILED && !line); + allocations.fail = false; + Require(sceFontCreateWritingLine(&memory, 0x12, nullptr, &line) == SCE_FONT_OK && line); + Require(allocations.lastSize == 2208 && allocations.live == 1); void* ltr = nullptr; Require(sceFontCreateWritingLine(&memory, 0x12, nullptr, <r) == SCE_FONT_OK && ltr != nullptr && ltr != line); void* rtl = nullptr; @@ -108,14 +137,19 @@ int main() { const FontWritingMetrics runC{8.0f, 0.0f, {-4.0f, 1.0f, 2.0f, 5.0f}}; const FontWritingMetrics runD{4.0f, 0.0f, {-4.0f, 1.0f, 0.0f, 20.0f}}; - Require(Throws([&] { sceFontWritingLineWritesOrder(nullptr, Attribute(0), &runA, &a); })); - Require(Throws([&] { sceFontWritingLineWritesOrder(line, 1, &runA, &a); })); - Require(Throws([&] { sceFontWritingLineWritesOrder(line, 0x1FB, &runA, &a); })); - Require(Throws([&] { sceFontWritingLineWritesOrder(line, Attribute(0), &runA, nullptr); })); - Require(Throws([&] { sceFontWritingLineWritesOrder(line, Attribute(0x7F), &runA, &a); })); + Require(sceFontWritingLineWritesOrder(nullptr, Attribute(0), &runA, &a) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontWritingLineWritesOrder(line, 1, &runA, &a) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontWritingLineWritesOrder(line, 0x1FB, &runA, &a) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontWritingLineWritesOrder(line, Attribute(0), &runA, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontWritingLineWritesOrder(line, Attribute(0x7F), &runA, &a) == SCE_FONT_ERROR_INVALID_PARAMETER); Require(sceFontWritingLineRefersRenderStep(line) == nullptr); - Require(sceFontWritingLineWritesOrder(line, 0, &runA, &a) == SCE_FONT_OK); + Require(sceFontWritingLineWritesOrder(line, 0, &runA, &a) == SCE_FONT_ERROR_INVALID_PARAMETER); + allocations.fail = true; + Require(sceFontWritingLineWritesOrder(line, Attribute(0), &runA, &a) == SCE_FONT_ERROR_ALLOCATION_FAILED); + allocations.fail = false; + Require(sceFontWritingLineWritesOrder(line, Attribute(0), &runA, &a) == SCE_FONT_OK); + Require(allocations.lastSize == 64); Require(sceFontWritingLineWritesOrder(line, Attribute(0), &runB, &b) == SCE_FONT_OK); Require(SpaceIs(line, 16.0f, 1.0f, 0.0f, 1.0f)); Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK && Same(metrics, {})); @@ -221,26 +255,37 @@ int main() { float space = 7.0f; Require(sceFontWritingLineGetOrderingSpace(rtl, nullptr, nullptr, &space, nullptr) == SCE_FONT_OK && space == 15.0f); Require(Throws([&] { sceFontWritingLineGetOrderingSpace(rtl, &space, &space, &space, nullptr); })); - Require(Throws([&] { sceFontWritingLineGetOrderingSpace(nullptr, &space, nullptr, nullptr, nullptr); })); - Require(Throws([&] { sceFontWritingLineGetRenderMetrics(nullptr, &metrics); })); - Require(Throws([&] { sceFontWritingLineGetRenderMetrics(rtl, nullptr); })); - Require(Throws([&] { sceFontWritingLineClear(nullptr); })); + Require(sceFontWritingLineGetOrderingSpace(nullptr, &space, nullptr, nullptr, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER && space == 0.0f); + Require(sceFontWritingLineGetRenderMetrics(nullptr, &metrics) == SCE_FONT_ERROR_INVALID_PARAMETER && Same(metrics, {})); + Require(sceFontWritingLineGetRenderMetrics(rtl, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontWritingLineClear(nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); int notALine = 0; Require(sceFontWritingLineRefersRenderStep(nullptr) == nullptr); - Require(Throws([&] { sceFontWritingLineClear(¬ALine); })); - Require(Throws([&] { sceFontWritingLineRefersRenderStep(¬ALine); })); - Require(Throws([&] { sceFontWritingLineGetOrderingSpace(¬ALine, &space, &space, &space, &space); })); + Require(sceFontWritingLineClear(¬ALine) == SCE_FONT_ERROR_INVALID_WRITING); + Require(sceFontWritingLineRefersRenderStep(¬ALine) == nullptr); + space = 7.0f; + Require(sceFontWritingLineGetOrderingSpace(¬ALine, &space, &space, &space, &space) == SCE_FONT_ERROR_INVALID_WRITING && space == 0.0f); + metrics = runA; + Require(sceFontWritingLineGetRenderMetrics(¬ALine, &metrics) == SCE_FONT_ERROR_INVALID_WRITING && Same(metrics, {})); + Require(sceFontWritingLineWritesOrder(¬ALine, 0, nullptr, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontWritingLineWritesOrder(¬ALine, Attribute(0), nullptr, &a) == SCE_FONT_ERROR_INVALID_WRITING); void* bogus = ¬ALine; - Require(Throws([&] { sceFontDestroyWritingLine(&bogus); }) && bogus == ¬ALine); + Require(sceFontDestroyWritingLine(&bogus) == SCE_FONT_ERROR_INVALID_WRITING && bogus == ¬ALine); + memory.mem_kind = 0; + Require(sceFontWritingLineClear(line) == SCE_FONT_ERROR_INVALID_WRITING); + Require(sceFontWritingLineWritesOrder(line, Attribute(0), &runA, &a) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontDestroyWritingLine(&line) == SCE_FONT_ERROR_INVALID_WRITING && line); + memory.mem_kind = 0xF00; void* destroyed = line; Require(sceFontDestroyWritingLine(&line) == SCE_FONT_OK && line == nullptr); - Require(sceFontDestroyWritingLine(&line) == SCE_FONT_OK && line == nullptr); - Require(sceFontDestroyWritingLine(nullptr) == SCE_FONT_OK); - Require(Throws([&] { sceFontWritingLineClear(destroyed); })); - Require(Throws([&] { sceFontWritingLineWritesOrder(destroyed, Attribute(0), &runA, &a); })); + Require(sceFontDestroyWritingLine(&line) == SCE_FONT_ERROR_INVALID_PARAMETER && line == nullptr); + Require(sceFontDestroyWritingLine(nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontWritingLineClear(destroyed) == SCE_FONT_ERROR_INVALID_WRITING); + Require(sceFontWritingLineWritesOrder(destroyed, Attribute(0), &runA, &a) == SCE_FONT_ERROR_INVALID_WRITING); Require(sceFontDestroyWritingLine(<r) == SCE_FONT_OK && ltr == nullptr); Require(sceFontDestroyWritingLine(&rtl) == SCE_FONT_OK && rtl == nullptr); + Require(allocations.live == 0); std::puts("font writing line: ok"); } 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/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp new file mode 100644 index 0000000000..330a0a9d46 --- /dev/null +++ b/core/libs/tests/GuestFreeType.cpp @@ -0,0 +1,505 @@ +#include "FreeTypeFontFixture.hpp" +#include "prx/libSceFreeType/include/GuestFreeType.hpp" +#include "prx/libc/include/General.hpp" + +#include +#include FT_FREETYPE_H +#include FT_OUTLINE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +void APS5_VABI FT_Set_Transform_nid_postfix(GuestFaceRec*, const GuestMatrix*, const GuestVector*); +int APS5_VABI FT_Init_FreeType_nid_postfix(void**); +int APS5_VABI FT_Done_FreeType_nid_postfix(void*); +int APS5_VABI FT_New_Face_nid_postfix(void*, const char*, std::int64_t, GuestFaceRec**); +int APS5_VABI FT_New_Memory_Face_nid_postfix(void*, const unsigned char*, std::int64_t, std::int64_t, GuestFaceRec**); +int APS5_VABI FT_Done_Face_nid_postfix(GuestFaceRec*); +int APS5_VABI FT_Set_Pixel_Sizes_nid_postfix(GuestFaceRec*, std::uint32_t, std::uint32_t); +std::uint32_t APS5_VABI FT_Get_Char_Index_nid_postfix(GuestFaceRec*, std::uint64_t); +int APS5_VABI FT_Load_Glyph_nid_postfix(GuestFaceRec*, std::uint32_t, std::int32_t); +int APS5_VABI FT_Load_Char_nid_postfix(GuestFaceRec*, std::uint64_t, std::int32_t); +int APS5_VABI FT_Render_Glyph_nid_postfix(GuestGlyphSlotRec*, std::int32_t); +int APS5_VABI FT_Set_Char_Size_nid_postfix(GuestFaceRec*, std::int64_t, std::int64_t, std::uint32_t, std::uint32_t); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +template static bool ThrowsWith(TCall call, const std::string& prefix) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string(error.what()).rfind(prefix, 0) == 0; + } + return false; +} + +static void FaceMatches(const GuestFaceRec* face, FT_Face host, const void* library) { + Require(face->num_faces == host->num_faces && face->face_index == host->face_index && face->face_flags == host->face_flags); + Require(face->style_flags == host->style_flags && face->num_glyphs == host->num_glyphs && face->num_glyphs == 2); + Require((face->family_name == nullptr) == (host->family_name == nullptr) && (face->style_name == nullptr) == (host->style_name == nullptr)); + Require(face->num_fixed_sizes == host->num_fixed_sizes && face->available_sizes == nullptr); + Require(face->num_charmaps == 1 && host->num_charmaps == 1 && face->charmap == face->charmaps[0]); + const GuestCharMapRec* charmap = face->charmaps[0]; + Require(charmap->face == face && charmap->encoding == static_cast(host->charmaps[0]->encoding) && charmap->platform_id == 3 && charmap->encoding_id == 1); + Require(face->bbox.xMin == host->bbox.xMin && face->bbox.yMin == host->bbox.yMin && face->bbox.xMax == host->bbox.xMax && face->bbox.yMax == host->bbox.yMax); + Require(face->units_per_EM == 1000 && face->ascender == host->ascender && face->descender == host->descender && face->height == host->height); + Require(face->max_advance_width == host->max_advance_width && face->max_advance_height == host->max_advance_height); + Require(face->underline_position == host->underline_position && face->underline_thickness == host->underline_thickness); + Require(face->glyph != nullptr && face->glyph->face == face && face->glyph->library == library && face->size != nullptr && face->size->face == face); +} + +static void SlotMatches(const GuestGlyphSlotRec* slot, FT_GlyphSlot host) { + Require(slot->glyph_index == host->glyph_index && slot->format == static_cast(host->format) && slot->next == nullptr); + const auto& metrics = host->metrics; + Require(slot->metrics.width == metrics.width && slot->metrics.height == metrics.height && slot->metrics.horiBearingX == metrics.horiBearingX); + Require(slot->metrics.horiBearingY == metrics.horiBearingY && slot->metrics.horiAdvance == metrics.horiAdvance && slot->metrics.vertBearingX == metrics.vertBearingX); + Require(slot->metrics.vertBearingY == metrics.vertBearingY && slot->metrics.vertAdvance == metrics.vertAdvance); + Require(slot->linearHoriAdvance == host->linearHoriAdvance && slot->linearVertAdvance == host->linearVertAdvance); + Require(slot->advance.x == host->advance.x && slot->advance.y == host->advance.y); + Require(slot->bitmap.rows == host->bitmap.rows && slot->bitmap.width == host->bitmap.width && slot->bitmap.pitch == host->bitmap.pitch); + Require(slot->bitmap.num_grays == host->bitmap.num_grays && slot->bitmap.pixel_mode == host->bitmap.pixel_mode && slot->bitmap_left == host->bitmap_left && slot->bitmap_top == host->bitmap_top); + const std::size_t bytes = static_cast(host->bitmap.rows) * static_cast(host->bitmap.pitch < 0 ? -host->bitmap.pitch : host->bitmap.pitch); + Require((slot->bitmap.buffer == nullptr) == (host->bitmap.buffer == nullptr)); + Require(!host->bitmap.buffer || std::memcmp(slot->bitmap.buffer, host->bitmap.buffer, bytes) == 0); + Require(slot->outline.n_points == host->outline.n_points && slot->outline.n_contours == host->outline.n_contours && slot->outline.flags == host->outline.flags); + for (unsigned index = 0; index < host->outline.n_points; ++index) { + Require(slot->outline.points[index].x == host->outline.points[index].x && slot->outline.points[index].y == host->outline.points[index].y); + Require(slot->outline.tags[index] == host->outline.tags[index]); + } + for (unsigned index = 0; index < host->outline.n_contours; ++index) Require(slot->outline.contours[index] == host->outline.contours[index]); + Require(slot->lsb_delta == host->lsb_delta && slot->rsb_delta == host->rsb_delta && slot->control_len == host->control_len); +} + +static void SizeMatches(const GuestFaceRec* face, FT_Face host) { + const auto& actual = face->size->metrics; + const auto& expected = host->size->metrics; + Require(actual.x_ppem == expected.x_ppem && actual.y_ppem == expected.y_ppem); + Require(actual.x_scale == expected.x_scale && actual.y_scale == expected.y_scale); + Require(actual.ascender == expected.ascender && actual.descender == expected.descender); + Require(actual.height == expected.height && actual.max_advance == expected.max_advance); +} + +struct Finalizers { + std::vector calls; +}; + +static void NativeSlotFinalizer(void* object) { + auto* slot = static_cast(object); + Require(slot->face->glyph == nullptr); + static_cast(slot->generic.data)->calls.push_back(1); +} + +static void NativeSizeFinalizer(void* object) { + auto* size = static_cast(object); + Require(size->face->size == size); + static_cast(size->generic.data)->calls.push_back(2); +} + +static void NativeFaceFinalizer(void* object) { + auto* face = static_cast(object); + Require(face->glyph == nullptr && face->size == nullptr); + static_cast(face->generic.data)->calls.push_back(3); +} + +static void APS5_VABI GuestSlotFinalizer(void* object) { + auto* slot = static_cast(object); + Require(static_cast(slot->face)->glyph == nullptr); + static_cast(slot->generic.data)->calls.push_back(1); +} + +static void APS5_VABI GuestSizeFinalizer(void* object) { + auto* size = static_cast(object); + Require(static_cast(size->face)->size == size); + static_cast(size->generic.data)->calls.push_back(2); +} + +static void APS5_VABI GuestFaceFinalizer(void* object) { + auto* face = static_cast(object); + Require(face->glyph == nullptr && face->size == nullptr); + static_cast(face->generic.data)->calls.push_back(3); +} + +static void SetFinalizers(FT_Face face, Finalizers& events) { + face->generic = {&events, NativeFaceFinalizer}; + face->glyph->generic = {&events, NativeSlotFinalizer}; + face->size->generic = {&events, NativeSizeFinalizer}; +} + +static void SetFinalizers(GuestFaceRec* face, Finalizers& events) { + face->generic = {&events, GuestFaceFinalizer}; + face->glyph->generic = {&events, GuestSlotFinalizer}; + face->size->generic = {&events, GuestSizeFinalizer}; +} + +static void CheckTransforms(const std::vector& font) { + FT_Library native = nullptr; + FT_Face host = nullptr; + FT_Face nativeOther = nullptr; + void* library = nullptr; + GuestFaceRec* face = nullptr; + GuestFaceRec* other = nullptr; + Require(FT_Init_FreeType(&native) == 0 && FT_Init_FreeType_nid_postfix(&library) == 0); + Require(FT_New_Memory_Face(native, font.data(), static_cast(font.size()), 0, &host) == 0); + Require(FT_New_Memory_Face(native, font.data(), static_cast(font.size()), 0, &nativeOther) == 0); + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), font.size(), 0, &face) == 0); + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), font.size(), 0, &other) == 0); + Require(FT_Set_Pixel_Sizes(host, 0, 100) == 0 && FT_Set_Pixel_Sizes_nid_postfix(face, 0, 100) == 0); + Require(FT_Set_Pixel_Sizes(nativeOther, 0, 100) == 0 && FT_Set_Pixel_Sizes_nid_postfix(other, 0, 100) == 0); + const std::array matrices{ + GuestMatrix{65536, 0, 0, 65536}, GuestMatrix{0, -65536, 65536, 0}, GuestMatrix{0, 65536, -65536, 0}, + GuestMatrix{-65536, 0, 0, 65536}, GuestMatrix{131072, 0, 0, 32768}, GuestMatrix{65536, 16384, -8192, 65536}, + GuestMatrix{0, 0, 0, 0} + }; + const std::array deltas{GuestVector{0, 0}, GuestVector{64, -128}, GuestVector{31, -63}, GuestVector{-96, 192}}; + std::size_t comparisons = 0; + for (std::size_t m = 0; m <= matrices.size(); ++m) { + for (std::size_t d = 0; d <= deltas.size(); ++d) { + GuestMatrix input = m < matrices.size() ? matrices[m] : matrices[0]; + GuestVector delta = d < deltas.size() ? deltas[d] : deltas[0]; + FT_Matrix matrix{static_cast(input.xx), static_cast(input.xy), static_cast(input.yx), static_cast(input.yy)}; + FT_Vector translation{static_cast(delta.x), static_cast(delta.y)}; + std::array savedSlot{}; + std::array savedSize{}; + std::memcpy(savedSlot.data(), face->glyph, savedSlot.size()); + std::memcpy(savedSize.data(), face->size, savedSize.size()); + FT_Set_Transform_nid_postfix(face, m < matrices.size() ? &input : nullptr, d < deltas.size() ? &delta : nullptr); + FT_Set_Transform(host, m < matrices.size() ? &matrix : nullptr, d < deltas.size() ? &translation : nullptr); + Require(std::memcmp(savedSlot.data(), face->glyph, savedSlot.size()) == 0); + Require(std::memcmp(savedSize.data(), face->size, savedSize.size()) == 0); + Require(input.xx == matrix.xx && input.xy == matrix.xy && input.yx == matrix.yx && input.yy == matrix.yy); + Require(delta.x == translation.x && delta.y == translation.y); + input = matrices[0]; + delta = deltas[0]; + for (const auto flags : std::array{FT_LOAD_DEFAULT, FT_LOAD_IGNORE_TRANSFORM, FT_LOAD_RENDER}) { + for (const std::uint32_t glyph : {0u, 1u}) { + Require(FT_Load_Glyph_nid_postfix(face, glyph, flags) == FT_Load_Glyph(host, glyph, flags)); + SlotMatches(face->glyph, host->glyph); + if (face->glyph->format == FT_GLYPH_FORMAT_OUTLINE) { + Require(FT_Render_Glyph_nid_postfix(face->glyph, FT_RENDER_MODE_NORMAL) == FT_Render_Glyph(host->glyph, FT_RENDER_MODE_NORMAL)); + SlotMatches(face->glyph, host->glyph); + } + ++comparisons; + } + } + Require(FT_Load_Glyph_nid_postfix(other, 1, FT_LOAD_DEFAULT) == FT_Load_Glyph(nativeOther, 1, FT_LOAD_DEFAULT)); + SlotMatches(other->glyph, nativeOther->glyph); + } + } + GuestMatrix rejected{65536, 0, 0, 65536}; + GuestVector rejectedDelta{}; + const auto loadUnchanged = [&] { + Require(FT_Load_Glyph_nid_postfix(face, 1, FT_LOAD_DEFAULT) == FT_Load_Glyph(host, 1, FT_LOAD_DEFAULT)); + SlotMatches(face->glyph, host->glyph); + }; + if constexpr (sizeof(FT_Long) < sizeof(std::int64_t)) { + for (const auto value : std::array{-0x80000001ll, 0x80000000ll, 0x100000000ll, + std::numeric_limits::min(), std::numeric_limits::max()}) { + for (auto member : {&GuestMatrix::xx, &GuestMatrix::xy, &GuestMatrix::yx, &GuestMatrix::yy}) { + rejected = matrices[0]; + rejected.*member = value; + Require(ThrowsWith([&] { FT_Set_Transform_nid_postfix(face, &rejected, nullptr); }, + "FT_Set_Transform: a component outside the host FT_Long range is not implemented")); + loadUnchanged(); + } + for (auto member : {&GuestVector::x, &GuestVector::y}) { + rejectedDelta = {}; + rejectedDelta.*member = value; + Require(ThrowsWith([&] { FT_Set_Transform_nid_postfix(face, &matrices[1], &rejectedDelta); }, + "FT_Set_Transform: a component outside the host FT_Long range is not implemented")); + loadUnchanged(); + } + } + } + rejected.xx = std::numeric_limits::max(); + rejectedDelta.x = std::numeric_limits::min(); + FT_Set_Transform_nid_postfix(nullptr, &rejected, &rejectedDelta); + FT_Set_Transform_nid_postfix(nullptr, nullptr, nullptr); + Require(FT_Done_FreeType_nid_postfix(library) == 0 && FT_Done_FreeType(native) == 0); + std::printf("FT_Set_Transform: %zu native glyph and bitmap comparisons\n", comparisons); +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5_guest_freetype-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root / "app0"); + const std::vector font = SquareGlyphFont('A', 700); + { + std::ofstream file(root / "app0" / "font.ttf", std::ios::binary); + file.write(reinterpret_cast(font.data()), static_cast(font.size())); + } + const auto previous = std::filesystem::current_path(); + std::filesystem::current_path(root); + + FT_Library hostLibrary = nullptr; + Require(FT_Init_FreeType(&hostLibrary) == 0); + FT_Face host = nullptr; + Require(FT_New_Memory_Face(hostLibrary, font.data(), static_cast(font.size()), 0, &host) == 0); + FT_Face hostSecond = nullptr; + const FT_Error secondFace = FT_New_Memory_Face(hostLibrary, font.data(), static_cast(font.size()), 1, &hostSecond); + Require(secondFace != 0); + + Require(FT_Init_FreeType_nid_postfix(nullptr) == FT_Err_Invalid_Argument); + void* library = nullptr; + Require(FT_Init_FreeType_nid_postfix(&library) == 0 && library != nullptr); + Require(FT_Done_FreeType_nid_postfix(nullptr) == FT_Err_Invalid_Library_Handle); + Require(FT_Done_Face_nid_postfix(nullptr) == FT_Err_Invalid_Face_Handle); + + GuestFaceRec* face = nullptr; + Require(FT_New_Face_nid_postfix(library, nullptr, 0, &face) == FT_Err_Invalid_Argument); + Require(FT_New_Face_nid_postfix(nullptr, "/app0/font.ttf", 0, &face) == FT_Err_Invalid_Library_Handle); + Require(FT_New_Face_nid_postfix(library, "/app0/missing.ttf", 0, &face) == FT_Err_Cannot_Open_Resource && face == nullptr); + Require(FT_New_Face_nid_postfix(library, "/app0/font.ttf", 0, nullptr) == FT_Err_Invalid_Argument); + Require(FT_New_Face_nid_postfix(library, "/app0/font.ttf", -1, nullptr) == 0); + Require(FT_New_Face_nid_postfix(library, "/app0/font.ttf", 1, &face) == secondFace && face == nullptr); + Require(FT_New_Face_nid_postfix(library, "/app0/font.ttf", 0x80000001ll, &face) == secondFace && face == nullptr); + GuestFaceRec* masked = nullptr; + Require(FT_New_Face_nid_postfix(library, "/app0/font.ttf", 0x180000000ll, &masked) == 0 && masked != nullptr && masked->face_index == 0); + Require(FT_New_Face_nid_postfix(library, "/app0/font.ttf", 0, &face) == 0 && face != nullptr && face != masked); + FaceMatches(face, host, library); + + const auto fontSize = static_cast(font.size()); + GuestFaceRec* memoryFace = nullptr; + Require(FT_New_Memory_Face_nid_postfix(library, nullptr, fontSize, 0, &memoryFace) == FT_Err_Invalid_Argument); + Require(FT_New_Memory_Face_nid_postfix(nullptr, font.data(), fontSize, 0, &memoryFace) == FT_Err_Invalid_Library_Handle); + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), fontSize, 0, nullptr) == FT_Err_Invalid_Argument); + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), fontSize, -1, nullptr) == 0); + FT_Face nativeProbe = nullptr; + GuestFaceRec* probe = nullptr; + Require(FT_New_Memory_Face(hostLibrary, font.data(), static_cast(font.size()), -1, &nativeProbe) == 0); + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), fontSize, -1, &probe) == 0 && probe != nullptr); + Require(probe->num_faces == nativeProbe->num_faces && probe->face_index == nativeProbe->face_index); + Require((probe->glyph == nullptr) == (nativeProbe->glyph == nullptr) && (probe->size == nullptr) == (nativeProbe->size == nullptr)); + Require(FT_Set_Char_Size_nid_postfix(probe, std::numeric_limits::max(), 0, 0, 0) == + FT_Set_Char_Size(nativeProbe, 64, 64, 72, 72)); + Finalizers nativeProbeEvents; + Finalizers guestProbeEvents; + nativeProbe->generic = {&nativeProbeEvents, NativeFaceFinalizer}; + probe->generic = {&guestProbeEvents, GuestFaceFinalizer}; + Require(FT_Done_Face(nativeProbe) == 0 && FT_Done_Face_nid_postfix(probe) == 0); + Require(nativeProbeEvents.calls == std::vector({3}) && guestProbeEvents.calls == nativeProbeEvents.calls); + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), fontSize, 1, &memoryFace) == secondFace && memoryFace == nullptr); + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), fontSize, 0x80000001ll, &memoryFace) == secondFace && memoryFace == nullptr); + FT_Face invalidHost = nullptr; + for (const FT_Long length : {0L, 4L}) { + const FT_Error expected = FT_New_Memory_Face(hostLibrary, font.data(), length, 0, &invalidHost); + Require(expected != 0 && invalidHost == nullptr); + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), length, 0, &memoryFace) == expected && memoryFace == nullptr); + } + const unsigned char invalidFont[32]{}; + const FT_Error invalidFormat = FT_New_Memory_Face(hostLibrary, invalidFont, sizeof(invalidFont), 0, &invalidHost); + Require(invalidFormat != 0 && invalidHost == nullptr); + Require(FT_New_Memory_Face_nid_postfix(library, invalidFont, sizeof(invalidFont), 0, &memoryFace) == invalidFormat && memoryFace == nullptr); + if constexpr (sizeof(FT_Long) < sizeof(std::int64_t)) { + for (const std::int64_t length : {0x80000000ll, -0x80000001ll}) { + Require(ThrowsWith([&] { FT_New_Memory_Face_nid_postfix(library, font.data(), length, 0, &memoryFace); }, + "FT_New_Memory_Face: a font buffer size outside the host FT_Long range is not implemented")); + Require(memoryFace == nullptr); + } + } + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), fontSize, 0x180000000ll, &memoryFace) == 0 && memoryFace != nullptr); + FaceMatches(memoryFace, host, library); + Require(memoryFace != face && memoryFace->glyph != face->glyph); + Require(FT_Set_Pixel_Sizes_nid_postfix(memoryFace, 0, 100) == 0 && FT_Set_Pixel_Sizes(host, 0, 100) == 0); + Require(FT_Get_Char_Index_nid_postfix(memoryFace, 'A') == 1); + Require(FT_Load_Glyph_nid_postfix(memoryFace, 1, FT_LOAD_DEFAULT) == 0 && FT_Load_Glyph(host, 1, FT_LOAD_DEFAULT) == 0); + SlotMatches(memoryFace->glyph, host->glyph); + Require(FT_Render_Glyph_nid_postfix(memoryFace->glyph, FT_RENDER_MODE_NORMAL) == 0 && FT_Render_Glyph(host->glyph, FT_RENDER_MODE_NORMAL) == 0); + SlotMatches(memoryFace->glyph, host->glyph); + Require(face->size->metrics.y_ppem == 0); + + Require(FT_Set_Pixel_Sizes_nid_postfix(nullptr, 0, 100) == FT_Err_Invalid_Face_Handle); + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 100) == 0 && FT_Set_Pixel_Sizes(host, 0, 100) == 0); + const auto& metrics = host->size->metrics; + const auto& guestMetrics = face->size->metrics; + Require(guestMetrics.x_ppem == 100 && guestMetrics.y_ppem == 100 && guestMetrics.x_scale == metrics.x_scale && guestMetrics.y_scale == metrics.y_scale); + Require(guestMetrics.ascender == metrics.ascender && guestMetrics.descender == metrics.descender && guestMetrics.height == metrics.height && guestMetrics.max_advance == metrics.max_advance); + + Require(FT_Get_Char_Index_nid_postfix(face, 'A') == 1 && FT_Get_Char_Index_nid_postfix(face, 'B') == 0); + Require(FT_Get_Char_Index_nid_postfix(face, 0x100000041ull) == 1); + Require(FT_Get_Char_Index_nid_postfix(nullptr, 'A') == 0); + + Require(FT_Load_Char_nid_postfix(nullptr, 'A', FT_LOAD_DEFAULT) == FT_Err_Invalid_Face_Handle); + for (const std::uint64_t code : std::array{'A', 'B', 0x100000041ull, 0xFFFFFFFFFFFFFFFFull}) { + for (const std::int32_t flags : std::array{FT_LOAD_DEFAULT, FT_LOAD_NO_SCALE, FT_LOAD_NO_HINTING, FT_LOAD_RENDER}) { + Require(FT_Load_Char_nid_postfix(face, code, flags) == FT_Load_Char(host, static_cast(code), flags)); + SlotMatches(face->glyph, host->glyph); + Require(face->glyph->glyph_index == (static_cast(code) == 'A' ? 1u : 0u)); + } + } + { + auto noMap = font; + const std::size_t tableCount = (static_cast(noMap[4]) << 8) | noMap[5]; + bool renamed = false; + for (std::size_t offset = 12; offset < 12 + tableCount * 16; offset += 16) { + if (std::memcmp(noMap.data() + offset, "cmap", 4) != 0) continue; + std::memcpy(noMap.data() + offset, "none", 4); + renamed = true; + break; + } + Require(renamed); + { + std::ofstream file(root / "app0" / "no-map.ttf", std::ios::binary); + file.write(reinterpret_cast(noMap.data()), static_cast(noMap.size())); + Require(file.good()); + } + GuestFaceRec* guestNoMap = nullptr; + FT_Face nativeNoMap = nullptr; + Require(FT_New_Face_nid_postfix(library, "/app0/no-map.ttf", 0, &guestNoMap) == 0); + Require(FT_New_Memory_Face(hostLibrary, noMap.data(), static_cast(noMap.size()), 0, &nativeNoMap) == 0); + Require(guestNoMap->charmap == nullptr && nativeNoMap->charmap == nullptr); + Require(FT_Set_Pixel_Sizes_nid_postfix(guestNoMap, 0, 100) == 0 && FT_Set_Pixel_Sizes(nativeNoMap, 0, 100) == 0); + for (const std::uint64_t code : {1ull, 2ull}) { + for (const std::int32_t flags : std::array{FT_LOAD_DEFAULT, FT_LOAD_RENDER}) { + const FT_Error expected = FT_Load_Char(nativeNoMap, static_cast(code), flags); + Require((code == 1) == (expected == 0)); + Require(FT_Load_Char_nid_postfix(guestNoMap, code, flags) == expected); + SlotMatches(guestNoMap->glyph, nativeNoMap->glyph); + } + } + Require(FT_Done_Face(nativeNoMap) == 0); + } + + Require(FT_Load_Glyph_nid_postfix(nullptr, 1, FT_LOAD_DEFAULT) == FT_Err_Invalid_Face_Handle); + Require(FT_Load_Glyph_nid_postfix(face, 1, FT_LOAD_DEFAULT) == 0 && FT_Load_Glyph(host, 1, FT_LOAD_DEFAULT) == 0); + GuestGlyphSlotRec* slot = face->glyph; + Require(slot->format == FT_GLYPH_FORMAT_OUTLINE && slot->glyph_index == 1 && slot->advance.x == 70 * 64 && slot->outline.n_points == 4); + SlotMatches(slot, host->glyph); + + Require(FT_Render_Glyph_nid_postfix(nullptr, FT_RENDER_MODE_NORMAL) == FT_Err_Invalid_Argument); + for (unsigned index = 0; index < slot->outline.n_points; ++index) slot->outline.points[index].x += 64; + FT_Outline_Translate(&host->glyph->outline, 64, 0); + Require(FT_Render_Glyph_nid_postfix(slot, FT_RENDER_MODE_NORMAL) == 0 && FT_Render_Glyph(host->glyph, FT_RENDER_MODE_NORMAL) == 0); + Require(slot->format == FT_GLYPH_FORMAT_BITMAP && slot->bitmap.pixel_mode == FT_PIXEL_MODE_GRAY && slot->bitmap.num_grays == 256); + Require(slot->bitmap.rows == 70 && slot->bitmap.width == 50 && slot->bitmap_left == 11 && slot->bitmap_top == 70); + Require(slot->bitmap.buffer[35 * slot->bitmap.pitch + 25] == 255); + SlotMatches(slot, host->glyph); + + Require(FT_Load_Glyph_nid_postfix(face, 1, FT_LOAD_DEFAULT) == 0); + GuestVector replaced[4]{}; + GuestVector* original = slot->outline.points; + slot->outline.points = replaced; + Require(ThrowsWith([slot] { FT_Render_Glyph_nid_postfix(slot, FT_RENDER_MODE_NORMAL); }, "FT_Render_Glyph: an outline whose arrays the guest replaced is not implemented")); + slot->outline.points = original; + Require(FT_Render_Glyph_nid_postfix(slot, FT_RENDER_MODE_NORMAL) == 0 && slot->bitmap_left == 10); + + { + struct Request { + std::int64_t width; + std::int64_t height; + std::uint32_t horizontal; + std::uint32_t vertical; + }; + const std::array requests{ + Request{0, 0, 0, 0}, Request{0, 12 * 64, 0, 0}, Request{12 * 64, 0, 0, 0}, + Request{12 * 64, 18 * 64, 96, 144}, Request{12 * 64 + 32, 12 * 64 + 32, 72, 72}, + Request{1, 63, 72, 72}, Request{-1, -100, 0, 0}, Request{0, -100, 96, 0}, + Request{12 * 64, 18 * 64, 0, 144}, Request{12 * 64, 18 * 64, 96, 0}, + Request{65535ll * 64, 64, 72, 72}, Request{65536ll * 64, 64, 72, 72}, + Request{64, 65536ll * 64, 72, 72}, + Request{18 * 64, 12 * 64, 72, 72} + }; + std::size_t successes = 0; + std::size_t failures = 0; + for (const auto& request : requests) { + std::array previousSlot{}; + std::memcpy(previousSlot.data(), face->glyph, previousSlot.size()); + const auto expected = FT_Set_Char_Size(host, static_cast(request.width), static_cast(request.height), + request.horizontal, request.vertical); + Require(FT_Set_Char_Size_nid_postfix(face, request.width, request.height, request.horizontal, request.vertical) == expected); + Require(std::memcmp(previousSlot.data(), face->glyph, previousSlot.size()) == 0); + expected == 0 ? ++successes : ++failures; + SizeMatches(face, host); + Require(FT_Load_Char_nid_postfix(face, 'A', FT_LOAD_DEFAULT) == FT_Load_Char(host, 'A', FT_LOAD_DEFAULT)); + SlotMatches(face->glyph, host->glyph); + } + Require(successes != 0 && failures != 0); + for (const auto value : std::array{std::numeric_limits::min(), -0x80000001ll}) { + Require(FT_Set_Char_Size_nid_postfix(face, value, 0, 0, 0) == FT_Set_Char_Size(host, 64, 64, 0, 0)); + SizeMatches(face, host); + Require(FT_Set_Char_Size_nid_postfix(face, 0, value, 0, 0) == 0); + SizeMatches(face, host); + } + for (const auto value : {std::numeric_limits::min(), std::numeric_limits::max()}) { + Require(FT_Set_Char_Size_nid_postfix(nullptr, value, value, 0, 0) == FT_Err_Invalid_Face_Handle); + } + if constexpr (sizeof(FT_F26Dot6) < sizeof(std::int64_t)) { + for (const auto value : std::array{0x7FFFFFFFll, 0x80000000ll, 0x100000040ll, std::numeric_limits::max()}) { + for (const bool width : {false, true}) { + Require(ThrowsWith([&] { FT_Set_Char_Size_nid_postfix(face, width ? value : 64, width ? 64 : value, 72, 72); }, + "FT_Set_Char_Size: a character size or resolution outside the host arithmetic range is not implemented")); + SizeMatches(face, host); + } + } + Require(ThrowsWith([&] { FT_Set_Char_Size_nid_postfix(face, 64, 64, std::numeric_limits::max(), 72); }, + "FT_Set_Char_Size: a character size or resolution outside the host arithmetic range is not implemented")); + SizeMatches(face, host); + } + Require(FT_Set_Char_Size_nid_postfix(memoryFace, 0, 48 * 64, 0, 144) == 0); + Require(memoryFace->size->metrics.y_ppem == 96 && face->size->metrics.y_ppem == 1); + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, 100) == 0 && FT_Set_Pixel_Sizes(host, 0, 100) == 0); + std::printf("FT_Set_Char_Size: %zu native request comparisons, %zu successes, %zu errors\n", requests.size(), successes, failures); + } + + Finalizers nativeDone; + Finalizers guestDone; + SetFinalizers(host, nativeDone); + SetFinalizers(face, guestDone); + Require(FT_Done_Face(host) == 0); + Require(FT_Done_Face_nid_postfix(face) == 0); + Require(nativeDone.calls == std::vector({1, 2, 3}) && guestDone.calls == nativeDone.calls); + Require(FT_Get_Char_Index_nid_postfix(masked, 'A') == 1); + + FT_Face nativeFirst = nullptr; + FT_Face nativeSecond = nullptr; + Require(FT_New_Memory_Face(hostLibrary, font.data(), static_cast(font.size()), 0, &nativeFirst) == 0); + Require(FT_New_Memory_Face(hostLibrary, font.data(), static_cast(font.size()), 0, &nativeSecond) == 0); + Finalizers nativeFirstEvents; + Finalizers nativeSecondEvents; + Finalizers guestFirstEvents; + Finalizers guestSecondEvents; + SetFinalizers(nativeFirst, nativeFirstEvents); + SetFinalizers(nativeSecond, nativeSecondEvents); + SetFinalizers(masked, guestFirstEvents); + SetFinalizers(memoryFace, guestSecondEvents); + Require(FT_Done_FreeType(hostLibrary) == 0); + + void* independent = nullptr; + GuestFaceRec* independentFace = nullptr; + Require(FT_Init_FreeType_nid_postfix(&independent) == 0); + Require(FT_New_Memory_Face_nid_postfix(independent, font.data(), fontSize, 0, &independentFace) == 0); + Finalizers independentEvents; + SetFinalizers(independentFace, independentEvents); + Require(FT_Done_FreeType_nid_postfix(library) == 0); + Require(guestFirstEvents.calls == nativeFirstEvents.calls && guestSecondEvents.calls == nativeSecondEvents.calls); + Require(guestFirstEvents.calls == nativeDone.calls && independentEvents.calls.empty()); + Require(FT_Get_Char_Index_nid_postfix(independentFace, 'A') == 1); + Require(FT_Done_FreeType_nid_postfix(independent) == 0 && independentEvents.calls == nativeDone.calls); + + void* empty = nullptr; + Require(FT_Init_FreeType_nid_postfix(&empty) == 0 && FT_Done_FreeType_nid_postfix(empty) == 0); + CheckTransforms(font); + std::filesystem::current_path(previous); + std::error_code ignored; + std::filesystem::remove_all(root, ignored); +} diff --git a/core/libs/tests/GuestFreeTypeCharmap.cpp b/core/libs/tests/GuestFreeTypeCharmap.cpp new file mode 100644 index 0000000000..27372b1526 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeCharmap.cpp @@ -0,0 +1,143 @@ +#include "FreeTypeFontFixture.hpp" +#include "prx/libSceFreeType/include/GuestFreeType.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include FT_FREETYPE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +int APS5_VABI FT_Init_FreeType_nid_postfix(void**); +int APS5_VABI FT_New_Face_nid_postfix(void*, const char*, std::int64_t, GuestFaceRec**); +std::uint32_t APS5_VABI FT_Get_Char_Index_nid_postfix(GuestFaceRec*, std::uint64_t); +int APS5_VABI FT_Load_Glyph_nid_postfix(GuestFaceRec*, std::uint32_t, std::int32_t); +int APS5_VABI FT_Select_Charmap_nid_postfix(GuestFaceRec*, std::int32_t); +int APS5_VABI FT_Set_Charmap_nid_postfix(GuestFaceRec*, GuestCharMapRec*); +std::uint64_t APS5_VABI FT_Get_First_Char_nid_postfix(GuestFaceRec*, std::uint32_t*); +std::uint64_t APS5_VABI FT_Get_Next_Char_nid_postfix(GuestFaceRec*, std::uint64_t, std::uint32_t*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType charmap check failed at line %d\n", line); + std::abort(); + } +} + +#define Require(value) Check((value), __LINE__) + +static std::vector> Walk(GuestFaceRec* guest, FT_Face host) { + std::vector> result; + std::uint32_t guestIndex = 999; + FT_UInt hostIndex = 999; + std::uint64_t guestCode = FT_Get_First_Char_nid_postfix(guest, &guestIndex); + FT_ULong hostCode = FT_Get_First_Char(host, &hostIndex); + for (int iteration = 0; iteration < 4; ++iteration) { + Require(guestCode == hostCode && guestIndex == hostIndex); + if (guestIndex == 0) return result; + result.emplace_back(guestCode, guestIndex); + guestCode = FT_Get_Next_Char_nid_postfix(guest, guestCode, &guestIndex); + hostCode = FT_Get_Next_Char(host, hostCode, &hostIndex); + } + std::abort(); +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5-freetype-charmap-" + std::to_string(std::random_device{}())); + Require(std::filesystem::create_directory(root)); + std::filesystem::create_directory(root / "app0"); + const auto font = SquareGlyphFont('A', 700, true); + const auto zeroFont = SquareGlyphFont(0, 700); + for (const auto& [name, bytes] : {std::pair{"maps.ttf", &font}, std::pair{"zero.ttf", &zeroFont}}) { + std::ofstream file(root / "app0" / name, std::ios::binary); + file.write(reinterpret_cast(bytes->data()), static_cast(bytes->size())); + Require(static_cast(file)); + } + const auto previous = std::filesystem::current_path(); + std::filesystem::current_path(root); + FT_Library native = nullptr; + FT_Face host = nullptr; + FT_Face foreignHost = nullptr; + FT_Face zeroHost = nullptr; + Require(FT_Init_FreeType(&native) == 0); + Require(FT_New_Memory_Face(native, font.data(), static_cast(font.size()), 0, &host) == 0); + Require(FT_New_Memory_Face(native, font.data(), static_cast(font.size()), 0, &foreignHost) == 0); + Require(FT_New_Memory_Face(native, zeroFont.data(), static_cast(zeroFont.size()), 0, &zeroHost) == 0); + void* library = nullptr; + GuestFaceRec* face = nullptr; + GuestFaceRec* foreign = nullptr; + GuestFaceRec* zero = nullptr; + Require(FT_Init_FreeType_nid_postfix(&library) == 0); + Require(FT_New_Face_nid_postfix(library, "/app0/maps.ttf", 0, &face) == 0); + Require(FT_New_Face_nid_postfix(library, "/app0/maps.ttf", 0, &foreign) == 0); + Require(FT_New_Face_nid_postfix(library, "/app0/zero.ttf", 0, &zero) == 0); + Require(face->num_charmaps == 2 && host->num_charmaps == 2); + const auto unicode = Walk(face, host); + Require(unicode.size() == 1 && unicode[0].first == 'A' && unicode[0].second == 1); + const auto zeroChars = Walk(zero, zeroHost); + Require(zeroChars.size() == 1 && zeroChars[0].first == 0 && zeroChars[0].second == 1); + + std::uint32_t index = 999; + Require(FT_Get_First_Char_nid_postfix(nullptr, &index) == 0 && index == 0); + index = 999; + Require(FT_Get_Next_Char_nid_postfix(nullptr, 0, &index) == 0 && index == 0); + Require(FT_Get_First_Char_nid_postfix(face, nullptr) == FT_Get_First_Char(host, nullptr)); + Require(FT_Get_Next_Char_nid_postfix(face, 0, nullptr) == FT_Get_Next_Char(host, 0, nullptr)); + Require(FT_Select_Charmap_nid_postfix(nullptr, FT_ENCODING_UNICODE) == FT_Err_Invalid_Face_Handle); + Require(FT_Set_Charmap_nid_postfix(nullptr, face->charmaps[0]) == FT_Err_Invalid_Face_Handle); + Require(FT_Load_Glyph_nid_postfix(face, 1, FT_LOAD_NO_SCALE) == 0); + Require(face->glyph->outline.n_points == 4); + auto* points = face->glyph->outline.points; + const auto changedX = ++points[0].x; + + for (const FT_Encoding encoding : {FT_ENCODING_MS_SYMBOL, FT_ENCODING_UNICODE}) { + Require(FT_Select_Charmap_nid_postfix(face, encoding) == FT_Select_Charmap(host, encoding)); + Require(face->charmap->encoding == static_cast(host->charmap->encoding)); + Require(face->glyph->outline.points == points && points[0].x == changedX); + const auto characters = Walk(face, host); + Require(characters.size() == 1 && characters[0].first == (encoding == FT_ENCODING_UNICODE ? 'A' : 0xF041)); + Require(FT_Get_Char_Index_nid_postfix(face, characters[0].first) == characters[0].second); + for (const std::uint64_t code : std::array{0, 64, 65, 0xF041, 0xFFFFFFFF, 0x100000040, std::numeric_limits::max()}) { + std::uint32_t guestIndex = 999; + FT_UInt hostIndex = 999; + const auto actual = FT_Get_Next_Char_nid_postfix(face, code, &guestIndex); + const auto expected = FT_Get_Next_Char(host, static_cast(code), &hostIndex); + Require(actual == expected && guestIndex == hostIndex); + } + const auto selected = face->charmap; + Require(FT_Select_Charmap_nid_postfix(face, FT_ENCODING_NONE) == FT_Select_Charmap(host, FT_ENCODING_NONE)); + Require(face->charmap == selected); + Require(FT_Set_Charmap_nid_postfix(face, nullptr) == FT_Set_Charmap(host, nullptr)); + Require(face->charmap == selected); + Require(FT_Set_Charmap_nid_postfix(face, foreign->charmaps[0]) == FT_Set_Charmap(host, foreignHost->charmaps[0])); + Require(face->charmap == selected); + FT_CharMapRec fakeHost{}; + GuestCharMapRec fakeGuest{}; + Require(FT_Set_Charmap_nid_postfix(face, &fakeGuest) == FT_Set_Charmap(host, &fakeHost)); + Require(face->charmap == selected); + } + for (int map = 1; map >= 0; --map) { + Require(FT_Set_Charmap_nid_postfix(face, face->charmaps[map]) == 0 && FT_Set_Charmap(host, host->charmaps[map]) == 0); + Require(face->charmap == face->charmaps[map]); + Require(face->glyph->outline.points == points && points[0].x == changedX); + Require(Walk(face, host).size() == 1); + } + + Require(FT_Done_FreeType(native) == 0); + std::filesystem::current_path(previous); + std::error_code ignored; + std::filesystem::remove_all(root, ignored); +} diff --git a/core/libs/tests/GuestFreeTypeNames.cpp b/core/libs/tests/GuestFreeTypeNames.cpp new file mode 100644 index 0000000000..a6aa3b7673 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeNames.cpp @@ -0,0 +1,128 @@ +#include "FontFixture.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +int APS5_VABI FT_Get_Glyph_Name_nid_postfix(GuestFaceRec*, std::uint32_t, void*, std::uint32_t); +std::uint32_t APS5_VABI FT_Get_Name_Index_nid_postfix(GuestFaceRec*, const char*); +const char* APS5_VABI FT_Get_Postscript_Name_nid_postfix(GuestFaceRec*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType names check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static std::vector NamedFont(const std::string& postscript, const std::string& first, const std::string& second) { + std::vector post(32, 0); + post[1] = 2; + for (const int value : {3, 0, 258, 259}) FontFixture::Put16(post, value); + for (const auto* name : {&first, &second}) { + post.push_back(static_cast(name->size())); + post.insert(post.end(), name->begin(), name->end()); + } + auto names = FontFixture::Words({0, 1, 18, 3, 1, 0x0409, 6, static_cast(postscript.size() * 2), 0}); + for (const unsigned char ch : postscript) FontFixture::Put16(names, ch); + return FontFixture::BuildFont(3, {{"post", post}, {"name", names}}); +} + +struct FacePair { + std::vector bytes; + GuestFace guest; + FT_Face oracle = nullptr; + + FacePair(GuestLibrary& library, FT_Library nativeLibrary, std::vector font) : bytes(std::move(font)) { + guest.library = &library; + Require(FT_New_Memory_Face(library.host, bytes.data(), static_cast(bytes.size()), 0, &guest.host) == 0); + Require(FT_New_Memory_Face(nativeLibrary, bytes.data(), static_cast(bytes.size()), 0, &oracle) == 0); + SyncFace(guest); + } + + ~FacePair() { + Require(FT_Done_Face(guest.host) == 0); + Require(FT_Done_Face(oracle) == 0); + } +}; + +static void CompareGlyph(GuestFaceRec* guest, FT_Face native, std::uint32_t index, std::uint32_t size) { + std::array expected; + expected.fill('?'); + auto actual = expected; + const FT_Error error = FT_Get_Glyph_Name(native, index, expected.data() + 3, size); + Require(FT_Get_Glyph_Name_nid_postfix(guest, index, actual.data() + 3, size) == error); + Require(actual == expected); +} + +static void CheckFace(FacePair& face, bool names) { + Require((FT_HAS_GLYPH_NAMES(face.oracle) != 0) == names); + for (const std::uint32_t index : {0u, 1u, 2u, 3u, 0x7FFFFFFFu}) { + for (const std::uint32_t size : {0u, 1u, 2u, 5u, 24u}) CompareGlyph(&face.guest.rec, face.oracle, index, size); + Require(FT_Get_Glyph_Name_nid_postfix(&face.guest.rec, index, nullptr, 24) == FT_Get_Glyph_Name(face.oracle, index, nullptr, 24)); + } + for (const std::uint32_t index : {0x80000000u, 0xFFFFFFFFu}) { + for (const std::uint32_t size : {0u, 1u, 24u}) { + std::array expected; + expected.fill('?'); + auto actual = expected; + if (size) expected[3] = '\0'; + Require(FT_Get_Glyph_Name_nid_postfix(&face.guest.rec, index, actual.data() + 3, size) == + (size ? FT_Err_Invalid_Glyph_Index : FT_Err_Invalid_Argument)); + Require(actual == expected); + } + } + for (const char* name : {".notdef", "alpha.custom", "beta", "gamma.custom", "delta", "ALPHA.CUSTOM", "missing", ""}) { + Require(FT_Get_Name_Index_nid_postfix(&face.guest.rec, name) == FT_Get_Name_Index(face.oracle, name)); + } + Require(FT_Get_Name_Index_nid_postfix(&face.guest.rec, nullptr) == 0); + const char* actual = FT_Get_Postscript_Name_nid_postfix(&face.guest.rec); + const char* expected = FT_Get_Postscript_Name(face.oracle); + Require((actual == nullptr) == (expected == nullptr)); + Require(!actual || std::strcmp(actual, expected) == 0); + Require(actual == FT_Get_Postscript_Name(face.guest.host)); +} + +int main() { + GuestLibrary library{}; + FT_Library nativeLibrary = nullptr; + Require(FT_Init_FreeType(&library.host) == 0); + Require(FT_Init_FreeType(&nativeLibrary) == 0); + { + FacePair first(library, nativeLibrary, NamedFont("NameFixture-A", "alpha.custom", "beta")); + FacePair second(library, nativeLibrary, NamedFont("NameFixture-B", "gamma.custom", "delta")); + std::vector post(32, 0); + post[1] = 3; + FacePair unnamed(library, nativeLibrary, FontFixture::BuildFont(3, {{"post", post}})); + CheckFace(first, true); + Require(FT_Get_Name_Index_nid_postfix(&first.guest.rec, "alpha.custom") == 1); + Require(FT_Get_Name_Index_nid_postfix(&first.guest.rec, "beta") == 2); + const char* retained = FT_Get_Postscript_Name_nid_postfix(&first.guest.rec); + Require(retained && std::strcmp(retained, "NameFixture-A") == 0); + CheckFace(second, true); + Require(FT_Get_Name_Index_nid_postfix(&second.guest.rec, "gamma.custom") == 1); + Require(FT_Get_Name_Index_nid_postfix(&second.guest.rec, "delta") == 2); + CheckFace(unnamed, false); + Require(FT_Get_Postscript_Name_nid_postfix(&unnamed.guest.rec) == nullptr); + Require(FT_Get_Postscript_Name_nid_postfix(&first.guest.rec) == retained && std::strcmp(retained, "NameFixture-A") == 0); + for (const std::uint32_t size : {0u, 1u, 24u}) CompareGlyph(nullptr, nullptr, 0, size); + Require(FT_Get_Glyph_Name_nid_postfix(nullptr, 0, nullptr, 0) == FT_Err_Invalid_Face_Handle); + Require(FT_Get_Name_Index_nid_postfix(nullptr, "alpha.custom") == 0); + Require(FT_Get_Name_Index_nid_postfix(nullptr, nullptr) == 0); + Require(FT_Get_Postscript_Name_nid_postfix(nullptr) == nullptr); + } + Require(FT_Done_FreeType(library.host) == 0); + Require(FT_Done_FreeType(nativeLibrary) == 0); +} diff --git a/core/libs/tests/GuestFreeTypeRequestSize.cpp b/core/libs/tests/GuestFreeTypeRequestSize.cpp new file mode 100644 index 0000000000..536d8493b1 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeRequestSize.cpp @@ -0,0 +1,188 @@ +#include "FreeTypeFontFixture.hpp" +#include "prx/libSceFreeType/include/GuestFreeType.hpp" + +#include +#include FT_FREETYPE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +int APS5_VABI FT_Init_FreeType_nid_postfix(void**); +int APS5_VABI FT_Done_FreeType_nid_postfix(void*); +int APS5_VABI FT_New_Memory_Face_nid_postfix(void*, const unsigned char*, std::int64_t, std::int64_t, GuestFaceRec**); +int APS5_VABI FT_Request_Size_nid_postfix(GuestFaceRec*, const GuestSizeRequestRec*); +int APS5_VABI FT_Load_Glyph_nid_postfix(GuestFaceRec*, std::uint32_t, std::int32_t); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType size request check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +struct Fixture { + std::vector bytes; + FT_Library library = nullptr; + FT_Face native = nullptr; + void* guestLibrary = nullptr; + GuestFaceRec* guest = nullptr; + + explicit Fixture(std::vector font, int faceIndex = 0) : bytes(std::move(font)) { + Require(FT_Init_FreeType(&library) == 0); + Require(FT_New_Memory_Face(library, bytes.data(), static_cast(bytes.size()), faceIndex, &native) == 0); + Require(FT_Init_FreeType_nid_postfix(&guestLibrary) == 0); + Require(FT_New_Memory_Face_nid_postfix(guestLibrary, bytes.data(), static_cast(bytes.size()), faceIndex, &guest) == 0); + } + + ~Fixture() { + Require(FT_Done_FreeType_nid_postfix(guestLibrary) == 0); + Require(FT_Done_FreeType(library) == 0); + } + + void CheckMetrics() const { + const auto& actual = guest->size->metrics; + const auto& expected = native->size->metrics; + Require(actual.x_ppem == expected.x_ppem && actual.y_ppem == expected.y_ppem); + Require(actual.x_scale == expected.x_scale && actual.y_scale == expected.y_scale); + Require(actual.ascender == expected.ascender && actual.descender == expected.descender); + Require(actual.height == expected.height && actual.max_advance == expected.max_advance); + } + + int Request(GuestSizeRequestRec request) { + FT_Size_RequestRec hostRequest{static_cast(request.type), static_cast(request.width), + static_cast(request.height), request.horiResolution, request.vertResolution}; + const auto before = request; + const auto* size = guest->size; + const auto generic = size->generic; + const int error = FT_Request_Size(native, &hostRequest); + Require(FT_Request_Size_nid_postfix(guest, &request) == error); + Require(request.type == before.type && request.width == before.width && request.height == before.height); + Require(request.horiResolution == before.horiResolution && request.vertResolution == before.vertResolution); + Require(guest->size == size && size->face == guest && size->generic.data == generic.data && size->generic.finalizer == generic.finalizer); + CheckMetrics(); + return error; + } + + void Load() const { + Require(FT_Load_Glyph(native, 1, FT_LOAD_DEFAULT) == 0); + Require(FT_Load_Glyph_nid_postfix(guest, 1, FT_LOAD_DEFAULT) == 0); + const auto& actual = guest->glyph->metrics; + const auto& expected = native->glyph->metrics; + Require(actual.width == expected.width && actual.height == expected.height); + Require(actual.horiBearingX == expected.horiBearingX && actual.horiBearingY == expected.horiBearingY); + Require(actual.horiAdvance == expected.horiAdvance && actual.vertAdvance == expected.vertAdvance); + Require(guest->glyph->advance.x == native->glyph->advance.x && guest->glyph->advance.y == native->glyph->advance.y); + } +}; + +static void CheckScalable() { + Fixture fixture(SquareGlyphFont('A', 913)); + fixture.guest->size->generic.data = &fixture; + for (std::uint32_t type = FT_SIZE_REQUEST_TYPE_NOMINAL; type < FT_SIZE_REQUEST_TYPE_MAX; ++type) { + Require(fixture.Request({type, 0, 0, 0, 0}) == FT_Err_Invalid_PPem); + for (const auto& dimensions : std::array, 4>{{{0, 23 * 64 + 17}, + {31 * 64 + 49, 0}, {17 * 64 + 31, 29 * 64 + 33}, {48 * 64, 12 * 64}}}) { + for (const auto& dpi : std::array, 4>{{{0, 0}, {72, 144}, {96, 0}, {0, 120}}}) { + const auto factor = type == FT_SIZE_REQUEST_TYPE_SCALES ? 64 : 1; + Require(fixture.Request({type, dimensions[0] * factor, dimensions[1] * factor, dpi[0], dpi[1]}) == 0); + fixture.Load(); + } + } + } + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_SCALES, 0x18000, 0x28000, 0, 0}) == 0); + const auto scales = fixture.guest->size->metrics; + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_SCALES, 0x18000, 0x28000, + std::numeric_limits::max(), std::numeric_limits::max()}) == 0); + Require(fixture.guest->size->metrics.x_scale == scales.x_scale && fixture.guest->size->metrics.y_scale == scales.y_scale); + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_NOMINAL, 65536LL * 64, 17 * 64, 0, 0}) == FT_Err_Invalid_Pixel_Size); + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_NOMINAL, 19 * 64, 27 * 64, 0, 0}) == 0); + fixture.Load(); + auto* slot = fixture.guest->glyph; + Require(slot->outline.n_points > 0); + slot->outline.points[0].x += 137; + slot->outline.flags ^= 2; + const auto point = slot->outline.points[0]; + const auto savedSlot = *slot; + for (const auto width : {21LL * 64, 65536LL * 64}) { + fixture.Request({FT_SIZE_REQUEST_TYPE_NOMINAL, width, 15 * 64, 0, 0}); + Require(std::memcmp(slot, &savedSlot, sizeof(savedSlot)) == 0); + Require(slot->outline.points[0].x == point.x && slot->outline.points[0].y == point.y); + } +} + +static void CheckInvalid() { + Fixture fixture(SquareGlyphFont('A', 913)); + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_NOMINAL, 17 * 64, 23 * 64, 0, 0}) == 0); + for (const auto& request : std::array{{ + {FT_SIZE_REQUEST_TYPE_NOMINAL, -1, 1024, 0, 0}, + {FT_SIZE_REQUEST_TYPE_NOMINAL, 1024, std::numeric_limits::min(), 0, 0}, + {FT_SIZE_REQUEST_TYPE_MAX, 1024, 1024, 0, 0}, + {std::numeric_limits::max(), 1024, 1024, 0, 0}, + {FT_SIZE_REQUEST_TYPE_MAX, std::numeric_limits::max(), 1024, 0, 0}, + {FT_SIZE_REQUEST_TYPE_NOMINAL, std::numeric_limits::max(), -1, 0, 0}}}) { + Require(FT_Request_Size_nid_postfix(fixture.guest, &request) == FT_Err_Invalid_Argument); + Require(FT_Request_Size_nid_postfix(nullptr, &request) == FT_Err_Invalid_Face_Handle); + fixture.CheckMetrics(); + } + Require(FT_Request_Size_nid_postfix(nullptr, nullptr) == FT_Err_Invalid_Face_Handle); + Require(FT_Request_Size_nid_postfix(fixture.guest, nullptr) == FT_Request_Size(fixture.native, nullptr)); + fixture.CheckMetrics(); + + Fixture probe(SquareGlyphFont('A', 913), -1); + Require(probe.guest->size == nullptr && probe.native->size == nullptr); + GuestSizeRequestRec request{FT_SIZE_REQUEST_TYPE_NOMINAL, -1, -1, 0, 0}; + Require(FT_Request_Size_nid_postfix(probe.guest, &request) == FT_Err_Invalid_Size_Handle); + Require(FT_Request_Size_nid_postfix(probe.guest, nullptr) == FT_Err_Invalid_Size_Handle); + + for (const auto& dimensions : std::array, 2>{{{0x100000000LL + 1024, 1024}, {1024, 0x100000000LL + 1024}}}) { + request = {FT_SIZE_REQUEST_TYPE_NOMINAL, dimensions[0], dimensions[1], 0, 0}; + if constexpr (sizeof(FT_Long) == sizeof(std::int64_t)) { + Require(fixture.Request(request) == FT_Err_Invalid_Pixel_Size); + } else { + bool threw = false; + try { + FT_Request_Size_nid_postfix(fixture.guest, &request); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + fixture.CheckMetrics(); + } + } +} + +static void CheckBitmap() { + const std::string bdf = "STARTFONT 2.1\nFONT -AnyPS5-Fixed-Medium-R-Normal--8-80-72-72-C-80-ISO10646-1\n" + "SIZE 8 72 72\nFONTBOUNDINGBOX 8 8 0 0\nSTARTPROPERTIES 4\nFONT_ASCENT 8\nFONT_DESCENT 0\n" + "CHARSET_REGISTRY \"ISO10646\"\nCHARSET_ENCODING \"1\"\nENDPROPERTIES\nCHARS 1\nSTARTCHAR A\nENCODING 65\n" + "SWIDTH 1000 0\nDWIDTH 8 0\nBBX 8 8 0 0\nBITMAP\n18\n24\n42\n7E\n42\n42\n42\n00\nENDCHAR\nENDFONT\n"; + Fixture fixture({bdf.begin(), bdf.end()}); + Require(!FT_IS_SCALABLE(fixture.native) && fixture.guest->num_fixed_sizes == 1); + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_NOMINAL, 8 * 64, 8 * 64, 0, 0}) == 0); + fixture.Load(); + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_NOMINAL, 0, 8 * 64, 72, 72}) == 0); + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_REAL_DIM, 8 * 64, 8 * 64, 0, 0}) == 0); + Require(fixture.Request({FT_SIZE_REQUEST_TYPE_NOMINAL, 0, 17 * 64, 0, 0}) == FT_Err_Invalid_Pixel_Size); + for (const auto type : {FT_SIZE_REQUEST_TYPE_BBOX, FT_SIZE_REQUEST_TYPE_CELL, FT_SIZE_REQUEST_TYPE_SCALES}) { + Require(fixture.Request({static_cast(type), 8 * 64, 8 * 64, 0, 0}) == FT_Err_Unimplemented_Feature); + } +} + +int main() { + CheckScalable(); + CheckInvalid(); + CheckBitmap(); +} diff --git a/core/libs/tests/GuestFreeTypeSfntNames.cpp b/core/libs/tests/GuestFreeTypeSfntNames.cpp new file mode 100644 index 0000000000..12affd6cbd --- /dev/null +++ b/core/libs/tests/GuestFreeTypeSfntNames.cpp @@ -0,0 +1,144 @@ +#include "FontFixture.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include FT_SFNT_NAMES_H + +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +std::uint32_t APS5_VABI FT_Get_Sfnt_Name_Count_nid_postfix(GuestFaceRec*); +int APS5_VABI FT_Get_Sfnt_Name_nid_postfix(GuestFaceRec*, std::uint32_t, GuestSfntName*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType SFNT names check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +struct NameRecord { + std::uint16_t platform; + std::uint16_t encoding; + std::uint16_t language; + std::uint16_t name; + std::vector bytes; +}; + +static std::vector NamedFont(const std::vector& records) { + auto table = FontFixture::Words({0, static_cast(records.size()), 6 + 12 * static_cast(records.size())}); + int offset = 0; + for (const auto& record : records) { + for (const int value : {static_cast(record.platform), static_cast(record.encoding), static_cast(record.language), + static_cast(record.name), static_cast(record.bytes.size()), offset}) FontFixture::Put16(table, value); + offset += static_cast(record.bytes.size()); + } + for (const auto& record : records) table.insert(table.end(), record.bytes.begin(), record.bytes.end()); + return FontFixture::BuildFont(3, {{"name", table}}); +} + +struct FacePair { + std::vector bytes; + GuestFace guest; + FT_Face oracle = nullptr; + + FacePair(GuestLibrary& library, FT_Library nativeLibrary, std::vector font) : bytes(std::move(font)) { + guest.library = &library; + Require(FT_New_Memory_Face(library.host, bytes.data(), static_cast(bytes.size()), 0, &guest.host) == 0); + Require(FT_New_Memory_Face(nativeLibrary, bytes.data(), static_cast(bytes.size()), 0, &oracle) == 0); + SyncFace(guest); + } + + ~FacePair() { + Require(FT_Done_Face(guest.host) == 0); + Require(FT_Done_Face(oracle) == 0); + } +}; + +static GuestSfntName Query(FacePair& face, std::uint32_t index, const NameRecord& record) { + struct GuardedName { + std::array before; + GuestSfntName value; + std::array after; + } actual; + std::memset(&actual, 0xA5, sizeof(actual)); + FT_SfntName expected{}; + Require(FT_Get_Sfnt_Name(face.oracle, index, &expected) == 0); + Require(FT_Get_Sfnt_Name_nid_postfix(&face.guest.rec, index, &actual.value) == 0); + for (const auto byte : actual.before) Require(byte == 0xA5); + for (const auto byte : actual.after) Require(byte == 0xA5); + const auto& name = actual.value; + Require(name.platform_id == expected.platform_id && name.platform_id == record.platform); + Require(name.encoding_id == expected.encoding_id && name.encoding_id == record.encoding); + Require(name.language_id == expected.language_id && name.language_id == record.language); + Require(name.name_id == expected.name_id && name.name_id == record.name); + Require(name.string_len == expected.string_len && name.string_len == record.bytes.size()); + Require(std::memcmp(name.string, expected.string, name.string_len) == 0); + Require(std::memcmp(name.string, record.bytes.data(), name.string_len) == 0); + FT_SfntName native{}; + Require(FT_Get_Sfnt_Name(face.guest.host, index, &native) == 0 && name.string == native.string); + return name; +} + +static void InvalidQuery(GuestFaceRec* guest, FT_Face native, std::uint32_t index) { + GuestSfntName actual; + std::memset(&actual, 0xA5, sizeof(actual)); + std::array before; + std::memcpy(before.data(), &actual, sizeof(actual)); + FT_SfntName expected{}; + const FT_Error error = FT_Get_Sfnt_Name(native, index, &expected); + Require(error != 0); + Require(FT_Get_Sfnt_Name_nid_postfix(guest, index, &actual) == error); + Require(std::memcmp(&actual, before.data(), sizeof(actual)) == 0); + Require(FT_Get_Sfnt_Name_nid_postfix(guest, index, nullptr) == FT_Get_Sfnt_Name(native, index, nullptr)); +} + +int main() { + GuestLibrary library{}; + FT_Library nativeLibrary = nullptr; + Require(FT_Init_FreeType(&library.host) == 0); + Require(FT_Init_FreeType(&nativeLibrary) == 0); + { + const std::vector records = { + {1, 0, 0, 1, {'F', 'a', 'm', 'i', 'l', 'y'}}, + {3, 1, 0x0409, 6, {0, 'S', 0, 'f', 0, 'n', 0, 't'}}, + {3, 10, 0x0411, 256, {0, 'A', 0, 0, 0, 'B'}} + }; + FacePair first(library, nativeLibrary, NamedFont(records)); + FacePair empty(library, nativeLibrary, FontFixture::BuildFont(3, {})); + Require(FT_Get_Sfnt_Name_Count_nid_postfix(&first.guest.rec) == records.size()); + Require(FT_Get_Sfnt_Name_Count_nid_postfix(&first.guest.rec) == FT_Get_Sfnt_Name_Count(first.oracle)); + std::vector retained; + for (std::uint32_t index = 0; index < records.size(); ++index) retained.push_back(Query(first, index, records[index])); + { + const std::vector other = {{3, 1, 0x0409, 6, {0, 'O', 0, 't', 0, 'h', 0, 'e', 0, 'r'}}}; + FacePair second(library, nativeLibrary, NamedFont(other)); + Require(FT_Get_Sfnt_Name_Count_nid_postfix(&second.guest.rec) == other.size()); + Query(second, 0, other[0]); + } + for (std::uint32_t index = 0; index < records.size(); ++index) { + Require(Query(first, index, records[index]).string == retained[index].string); + Require(std::memcmp(retained[index].string, records[index].bytes.data(), retained[index].string_len) == 0); + Require(FT_Get_Sfnt_Name_nid_postfix(&first.guest.rec, index, nullptr) == FT_Get_Sfnt_Name(first.oracle, index, nullptr)); + } + Require(FT_Get_Sfnt_Name_Count_nid_postfix(&empty.guest.rec) == 0); + Require(FT_Get_Sfnt_Name_Count_nid_postfix(nullptr) == 0); + for (const std::uint32_t index : {0u, 3u, 0x80000000u, 0xFFFFFFFFu}) { + if (index >= records.size()) InvalidQuery(&first.guest.rec, first.oracle, index); + InvalidQuery(&empty.guest.rec, empty.oracle, index); + InvalidQuery(nullptr, nullptr, index); + } + } + Require(FT_Done_FreeType(library.host) == 0); + Require(FT_Done_FreeType(nativeLibrary) == 0); +} diff --git a/core/libs/tests/GuestFreebsdClocks.cpp b/core/libs/tests/GuestFreebsdClocks.cpp index e1f1b5256a..40972ab464 100644 --- a/core/libs/tests/GuestFreebsdClocks.cpp +++ b/core/libs/tests/GuestFreebsdClocks.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include extern "C" { @@ -40,6 +41,16 @@ static constexpr std::int64_t GIVE_UP_NANOS = 10 * NANOS_PER_SECOND; static void Require(bool value) { if (!value) std::abort(); } +static void SetTimeZone(const char* zone) { +#ifdef _WIN32 + Require(_putenv_s("TZ", zone) == 0); + _tzset(); +#else + Require(setenv("TZ", zone, 1) == 0); + tzset(); +#endif +} + static bool OmittedResolutionSucceeds() { constexpr int clocks[] = {0, 1, 2, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 3, 6, 9999, -1, INT_MIN, INT_MAX}; for (const int clock : clocks) { @@ -68,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); @@ -177,6 +203,7 @@ static void XtimeTicksAreGettimeofdayMicroseconds() { } static void CalendarConversionsInitializeTheirState() { + SetTimeZone("UTC0"); constexpr std::uint64_t sentinel = 0xa5a5a5a5a5a5a5a5ull; struct GuardedState { std::uint64_t before; @@ -242,11 +269,53 @@ static void SettimeValidatesLikeFreebsd() { Require(SettimeThrows(KernelTimespec{1, NANOS_PER_SECOND - 1}, "1.999999999")); } +static std::int32_t RuntimeDaylight(std::int32_t daylight) { +#ifdef _WIN32 + long bias = 0; + Require(_get_dstbias(&bias) == 0); + return daylight == 0 ? 0 : static_cast(-bias); +#else + return daylight; +#endif +} + +static void RequireZone(const char* zone, std::int64_t utc, std::int64_t standardEast, std::int32_t zoneDaylight, std::int32_t daylightInEffect) { + SetTimeZone(zone); + zoneDaylight = RuntimeDaylight(zoneDaylight); + const std::int32_t daylight = daylightInEffect == 0 ? 0 : zoneDaylight; + const std::int64_t east = standardEast + daylight; + std::int64_t local = -1; + KernelTimesec universal{-1, 0xa5a5a5a5u, 0xa5a5a5a5u}; + std::int32_t universalDst = -1; + Require(sceKernelConvertUtcToLocaltime(utc, &local, &universal, &universalDst) == SCE_OK); + Require(local == utc + east && universalDst == daylight); + Require(universal.t == utc && universal.west_sec == static_cast(east - daylight) && universal.dst_sec == static_cast(daylight)); + std::int64_t converted = -1; + KernelTimesec state{-1, 0xa5a5a5a5u, 0xa5a5a5a5u}; + std::int32_t dst = -1; + Require(sceKernelConvertLocaltimeToUtc(local, 0, &converted, &state, &dst) == SCE_OK); + Require(converted == utc && dst == zoneDaylight); + Require(state.t == local && state.west_sec == universal.west_sec && state.dst_sec == universal.dst_sec); +} + +static void CalendarConversionsFollowTheHostZone() { + constexpr std::int64_t winter = 1768478400; + constexpr std::int64_t summer = 1782907200; + RequireZone("UTC0", summer, 0, 0, 0); + RequireZone("IST-5:30", winter, 19800, 0, 0); + RequireZone("BRT+3", summer, -10800, 0, 0); + RequireZone("EST5EDT", winter, -18000, 3600, 0); + RequireZone("EST5EDT", summer, -18000, 3600, 3600); + SetTimeZone("UTC0"); +} + int main() { if (!OmittedResolutionSucceeds()) return 1; UnsupportedResolutionStillThrows(); + OmittedTimeIsFaultAfterClockValidation(); SettimeValidatesLikeFreebsd(); CalendarConversionsInitializeTheirState(); + CalendarConversionsFollowTheHostZone(); SecondClockReportsWholeSeconds(); KernelTimezone zone{-1, -1}; Require(gettimeofday_nid_postfix(nullptr, nullptr) == SCE_OK); diff --git a/core/libs/tests/GuestFstatFault.cpp b/core/libs/tests/GuestFstatFault.cpp new file mode 100644 index 0000000000..06524f7df8 --- /dev/null +++ b/core/libs/tests/GuestFstatFault.cpp @@ -0,0 +1,67 @@ +#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); + RequireFailure(file, GuestEbadf); + RequireFailure(-1, GuestEbadf); + + std::filesystem::remove_all(root); +} diff --git a/core/libs/tests/GuestFstatfs.cpp b/core/libs/tests/GuestFstatfs.cpp index 0c86546ccb..1a647f6094 100644 --- a/core/libs/tests/GuestFstatfs.cpp +++ b/core/libs/tests/GuestFstatfs.cpp @@ -42,6 +42,7 @@ int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI close_nid_postfix(int); int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI socketpair_nid_postfix(int, int, int, int*); +int APS5_VABI kqueue_nid_postfix(); int* APS5_VABI __error_nid_postfix(); } @@ -83,6 +84,19 @@ static void RequireVolume(int descriptor, const std::filesystem::path& path) { Require(fs.f_fstypename[0] == '\0' && fs.f_mntfromname[0] == '\0' && fs.f_mntonname[0] == '\0'); } +static void RequireDeviceFilesystem(int descriptor) { + GuestStatfs fs; + std::memset(&fs, 0x5a, sizeof(fs)); + Require(_fstatfs_nid_postfix(descriptor, &fs) == 0); + GuestStatfs expected{}; + expected.f_version = StatfsVersion; + expected.f_bsize = 512; + expected.f_iosize = 512; + expected.f_blocks = 2; + expected.f_namemax = 255; + Require(std::memcmp(&fs, &expected, sizeof(fs)) == 0); +} + int main() { const auto root = std::filesystem::temp_directory_path() / ("anyps5-fstatfs-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); @@ -122,11 +136,19 @@ int main() { Require(close_nid_postfix(sockets[1]) == 0); RequireFailure(sockets[0], GuestEbadf); + const int random = sceKernelOpen("/dev/urandom", SCE_KERNEL_O_RDONLY, 0); + Require(random >= 0); + RequireDeviceFilesystem(random); + Require(close_nid_postfix(random) == 0); + + const int kqueue = kqueue_nid_postfix(); + Require(kqueue >= 0); + RequireFailure(kqueue, GuestEinval); + Require(close_nid_postfix(kqueue) == 0); + RequireFailure(-1, GuestEbadf); Require(close_nid_postfix(file) == 0); -#ifndef _WIN32 RequireFailure(file, GuestEbadf); -#endif std::filesystem::remove_all(root); } diff --git a/core/libs/tests/GuestG2PDialog.cpp b/core/libs/tests/GuestG2PDialog.cpp index d1666fd28d..a318e77f68 100644 --- a/core/libs/tests/GuestG2PDialog.cpp +++ b/core/libs/tests/GuestG2PDialog.cpp @@ -1,8 +1,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include "prx/libSceUserService/UserService.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include extern "C" { +int APS5_VABI sceUserServiceInitialize(const UserServiceInitParam* param); +int APS5_VABI sceUserServiceTerminate(); int APS5_VABI sceG2PDialogInitialize(void); int APS5_VABI sceG2PDialogOpen(const void* param); int APS5_VABI sceG2PDialogUpdateStatus(void); @@ -16,6 +20,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); @@ -26,29 +32,74 @@ constexpr int kResultUserCanceled = 1; } int main() { - std::int64_t param = 0; - std::int32_t result[2] = {-1, -1}; + Require(sceUserServiceInitialize(nullptr) == 0); + G2PDialogParam param{}; + param.base.size = sizeof(param.base); + param.base.magic = static_cast(reinterpret_cast(¶m)) - 1060003575u; + param.size = sizeof(param); + param.userId = USER_SERVICE_INITIAL_USER_ID; + std::int32_t result[4] = {-1, -1, -1, -1}; Require(sceG2PDialogTerminate() == kErrNotInitialized); Require(sceG2PDialogOpen(¶m) == kErrNotInitialized); + Require(sceG2PDialogClose() == kErrNotInitialized); Require(sceG2PDialogGetResult(result) == kErrNotInitialized); + Require(sceG2PDialogOpen(nullptr) == static_cast(0x80B8000Du)); + Require(sceG2PDialogGetResult(nullptr) == static_cast(0x80B8000Du)); + 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); + param.base.size = 0; + Require(sceG2PDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.base.size = sizeof(param.base); + param.base.magic = 0; + Require(sceG2PDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.base.magic = static_cast(reinterpret_cast(¶m)) - 1060003575u; + param.size = 0; + Require(sceG2PDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.size = sizeof(param); + param.reserved[63] = 1; + Require(sceG2PDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.reserved[63] = 0; + param.userId = -1; + Require(sceG2PDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.userId = 42; + Require(sceG2PDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.userId = USER_SERVICE_INITIAL_USER_ID; + Require(sceUserServiceTerminate() == 0); + Require(sceG2PDialogOpen(¶m) == static_cast(0x80B8000Au)); + Require(sceUserServiceInitialize(nullptr) == 0); Require(sceG2PDialogOpen(¶m) == 0); Require(sceG2PDialogGetStatus() == kStatusFinished); Require(sceG2PDialogUpdateStatus() == kStatusFinished); Require(sceG2PDialogGetResult(nullptr) == kErrArgNull); Require(sceG2PDialogGetResult(result) == 0); - Require(result[0] == kResultUserCanceled); + Require(result[0] == -1 && result[1] == -1 && result[3] == -1); + Require(result[2] == kResultUserCanceled); Require(result[1] == -1); - Require(sceG2PDialogClose() == 0); + Require(sceG2PDialogClose() == static_cast(0x80B8000Bu)); Require(sceG2PDialogTerminate() == 0); Require(sceG2PDialogTerminate() == kErrNotInitialized); + Require(sceG2PDialogGetStatus() == kStatusNone); + + Require(sceG2PDialogInitialize() == 0); + Require(sceG2PDialogGetStatus() == kStatusInitialized); + Require(sceG2PDialogClose() == static_cast(0x80B8000Bu)); + Require(sceG2PDialogGetStatus() == kStatusInitialized); + result[2] = -1; + Require(sceG2PDialogGetResult(result) == kErrNotFinished); + Require(result[0] == -1 && result[1] == -1 && result[3] == -1); + Require(result[2] == -1); + Require(sceG2PDialogTerminate() == 0); + Require(sceUserServiceTerminate() == 0); } diff --git a/core/libs/tests/GuestGameLiveStreaming.cpp b/core/libs/tests/GuestGameLiveStreaming.cpp new file mode 100644 index 0000000000..22950d430e --- /dev/null +++ b/core/libs/tests/GuestGameLiveStreaming.cpp @@ -0,0 +1,96 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +struct GameLiveStreamingStatus2 { + std::int32_t userId; + bool isOnAir; + std::uint8_t align[3]; + std::uint32_t spectatorCounts; + std::uint32_t textMessageCounts; + std::uint32_t commandMessageCounts; + std::uint32_t broadcastVideoResolution; + std::uint8_t reserved[48]; +}; +static_assert(sizeof(GameLiveStreamingStatus2) == 72); + +extern "C" { +int APS5_VABI sceGameLiveStreamingInitialize(std::size_t heap_size); +int APS5_VABI sceGameLiveStreamingTerminate(void); +int APS5_VABI sceGameLiveStreamingGetCurrentStatus2(GameLiveStreamingStatus2* status); +struct GameLiveStreamingProgramInfo; +int APS5_VABI sceGameLiveStreamingGetProgramInfo(GameLiveStreamingProgramInfo*); +int APS5_VABI sceGameLiveStreamingEnableLiveStreaming(bool isEnableLiveStreaming); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr std::int32_t kInvalidUserId = -1; + +bool EnableThrowsNotInitialized(bool isEnableLiveStreaming) { + try { + sceGameLiveStreamingEnableLiveStreaming(isEnableLiveStreaming); + } catch (const std::logic_error&) { + return true; + } + return false; +} + +} + +int main() { + constexpr int invalidArgument = static_cast(0x80A00002); + constexpr int alreadyInitialized = static_cast(0x80A00003); + constexpr int notInitialized = static_cast(0x80A00004); + Require(sceGameLiveStreamingTerminate() == notInitialized); + Require(sceGameLiveStreamingGetCurrentStatus2(nullptr) == notInitialized); + Require(EnableThrowsNotInitialized(true)); + for (const std::size_t size : {0u, 1u, 0x3fffu, 0x4001u, 0x100000u}) Require(sceGameLiveStreamingInitialize(size) == invalidArgument); + Require(sceGameLiveStreamingInitialize(0x4000) == 0); + Require(sceGameLiveStreamingEnableLiveStreaming(true) == 0); + Require(sceGameLiveStreamingInitialize(0) == invalidArgument); + Require(sceGameLiveStreamingInitialize(0x4000) == alreadyInitialized); + Require(sceGameLiveStreamingGetCurrentStatus2(nullptr) == invalidArgument); + + GameLiveStreamingStatus2 status{}; + std::memset(&status, 0x7f, sizeof(status)); + Require(sceGameLiveStreamingGetCurrentStatus2(&status) == 0); + Require(status.userId == kInvalidUserId); + Require(!status.isOnAir); + Require(status.spectatorCounts == 0); + Require(status.textMessageCounts == 0); + Require(status.commandMessageCounts == 0); + Require(status.broadcastVideoResolution == 0); + for (std::uint8_t byte : status.align) { + Require(byte == 0); + } + for (std::uint8_t byte : status.reserved) { + Require(byte == 0); + } + + Require(sceGameLiveStreamingGetProgramInfo(nullptr) == static_cast(0x80A00002)); + std::array programInfo{}; + programInfo.fill(0x7f); + Require(sceGameLiveStreamingGetProgramInfo(reinterpret_cast(programInfo.data())) == 0); + for (std::size_t i = 0; i < programInfo.size(); ++i) { + Require(programInfo[i] == (i < 0x134C ? 0 : 0x7f)); + } + + Require(sceGameLiveStreamingEnableLiveStreaming(false) == 0); + Require(sceGameLiveStreamingTerminate() == 0); + Require(EnableThrowsNotInitialized(false)); + std::memset(&status, 0x7f, sizeof(status)); + Require(sceGameLiveStreamingGetCurrentStatus2(&status) == notInitialized); + for (const auto value : std::as_bytes(std::span(&status, 1))) Require(value == std::byte{0x7f}); + programInfo.fill(0x7f); + Require(sceGameLiveStreamingGetProgramInfo(reinterpret_cast(programInfo.data())) == notInitialized); + for (const std::uint8_t byte : programInfo) Require(byte == 0x7f); + Require(sceGameLiveStreamingTerminate() == notInitialized); +} diff --git a/core/libs/tests/GuestGameUpdate.cpp b/core/libs/tests/GuestGameUpdate.cpp index 12aa20e597..c46d451c83 100644 --- a/core/libs/tests/GuestGameUpdate.cpp +++ b/core/libs/tests/GuestGameUpdate.cpp @@ -3,46 +3,107 @@ #include #include #include +#include #include extern "C" { int APS5_VABI sceGameUpdateInitialize(void); int APS5_VABI sceGameUpdateTerminate(void); -int APS5_VABI sceGameUpdateGetAddcontLatestVersion(std::uint32_t, const void*, GameUpdateAddcontVersionInfo*); +int APS5_VABI sceGameUpdateGetAddcontLatestVersion(std::uint32_t, const NpUnifiedEntitlementLabel*, GameUpdateAddcontVersionInfo*); +int APS5_VABI sceGameUpdateCreateRequest(void); +int APS5_VABI sceGameUpdateCheck(int, const GameUpdateCheckParam*, GameUpdateCheckResult*); +int APS5_VABI sceGameUpdateAbortRequest(int); +int APS5_VABI sceGameUpdateDeleteRequest(int); } static void Require(bool value) { if (!value) std::abort(); } +template +static void RequireThrows(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return; + } + std::abort(); +} + +static void TestRequestArguments(int notInitialized, int invalidArgument, int invalidSize, int notFound) { + GameUpdateCheckParam param{}; + param.size = sizeof(param); + GameUpdateCheckResult result{}; + result.size = sizeof(result); + constexpr int requestId = 0x20000000; + + for (int id : {0, -1}) { + Require(sceGameUpdateCheck(id, nullptr, nullptr) == notFound); + Require(sceGameUpdateAbortRequest(id) == notFound); + Require(sceGameUpdateDeleteRequest(id) == notFound); + } + Require(sceGameUpdateCheck(requestId, nullptr, &result) == invalidArgument); + Require(sceGameUpdateCheck(requestId, ¶m, nullptr) == invalidArgument); + param.size = sizeof(param) - 1; + Require(sceGameUpdateCheck(requestId, ¶m, &result) == invalidSize); + param.size = sizeof(param); + result.size = sizeof(result) + 8; + Require(sceGameUpdateCheck(requestId, ¶m, &result) == invalidSize); + result.size = sizeof(result); + param.option = 1; + Require(sceGameUpdateCheck(requestId, ¶m, &result) == invalidArgument); + param.option = 0; + param.reserved[8] = 1; + Require(sceGameUpdateCheck(requestId, ¶m, &result) == invalidArgument); + param.reserved[8] = 0; + + Require(sceGameUpdateCreateRequest() == notInitialized); + Require(sceGameUpdateCheck(requestId, ¶m, &result) == notInitialized); + Require(sceGameUpdateAbortRequest(requestId) == notInitialized); + Require(sceGameUpdateDeleteRequest(requestId) == notInitialized); + + Require(sceGameUpdateInitialize() == 0); + Require(sceGameUpdateCheck(requestId, ¶m, &result) == notFound); + Require(sceGameUpdateAbortRequest(requestId) == notFound); + Require(sceGameUpdateDeleteRequest(requestId) == notFound); + Require(sceGameUpdateTerminate() == 0); +} + int main() { constexpr int notInitialized = static_cast(0x80412801); constexpr int invalidArgument = static_cast(0x80412803); constexpr int invalidSize = static_cast(0x80412804); static_assert(sizeof(GameUpdateAddcontVersionInfo) == 0x30); - const std::uint8_t label[16] = {}; + const NpUnifiedEntitlementLabel label{}; GameUpdateAddcontVersionInfo info{}; info.size = sizeof(info); - Require(sceGameUpdateGetAddcontLatestVersion(0, label, &info) == notInitialized); - Require(sceGameUpdateInitialize() == 0); - Require(sceGameUpdateGetAddcontLatestVersion(0, label, nullptr) == invalidArgument); + constexpr int alreadyInitialized = static_cast(0x80412802); + constexpr int notFound = static_cast(0x80412805); - bool threw = false; - try { sceGameUpdateGetAddcontLatestVersion(0, nullptr, &info); } - catch (const std::runtime_error&) { threw = true; } - Require(threw); + Require(sceGameUpdateTerminate() == notInitialized); + TestRequestArguments(notInitialized, invalidArgument, invalidSize, notFound); + + Require(sceGameUpdateGetAddcontLatestVersion(0, &label, &info) == notInitialized); + Require(sceGameUpdateGetAddcontLatestVersion(0, &label, nullptr) == invalidArgument); + Require(sceGameUpdateGetAddcontLatestVersion(0, nullptr, &info) == invalidArgument); + Require(sceGameUpdateInitialize() == 0); + Require(sceGameUpdateInitialize() == alreadyInitialized); + Require(sceGameUpdateGetAddcontLatestVersion(0, &label, nullptr) == invalidArgument); + + Require(sceGameUpdateGetAddcontLatestVersion(0, nullptr, &info) == invalidArgument); info.size = sizeof(info) - 1; - Require(sceGameUpdateGetAddcontLatestVersion(0, label, &info) == invalidSize); + RequireThrows([&] { sceGameUpdateGetAddcontLatestVersion(0, &label, &info); }); std::memset(&info, 0xAB, sizeof(info)); info.size = sizeof(info) + 8; - Require(sceGameUpdateGetAddcontLatestVersion(0, label, &info) == 0); - Require(info.size == sizeof(info) + 8 && !info.found); - const auto* bytes = reinterpret_cast(&info); - for (std::size_t i = sizeof(info.size); i < sizeof(info); ++i) Require(bytes[i] == 0); + const auto original = info; + RequireThrows([&] { sceGameUpdateGetAddcontLatestVersion(0, &label, &info); }); + Require(std::memcmp(&info, &original, sizeof(info)) == 0); + RequireThrows([] { sceGameUpdateCreateRequest(); }); Require(sceGameUpdateTerminate() == 0); + Require(sceGameUpdateTerminate() == notInitialized); info.size = sizeof(info); - Require(sceGameUpdateGetAddcontLatestVersion(0, label, &info) == notInitialized); + Require(sceGameUpdateGetAddcontLatestVersion(0, &label, &info) == notInitialized); } diff --git a/core/libs/tests/GuestGenericCategory.cpp b/core/libs/tests/GuestGenericCategory.cpp index e42958827f..b4f3f98687 100644 --- a/core/libs/tests/GuestGenericCategory.cpp +++ b/core/libs/tests/GuestGenericCategory.cpp @@ -8,7 +8,7 @@ #include #include -struct Category { const void* const* vtable; }; +struct Category { void* const* vtable; }; struct ErrorCode { int value; const Category* category; }; using Message = GuestLocale::String* (APS5_VABI *)(GuestLocale::String*, const Category*, int); using Name = const char* (APS5_VABI *)(const Category*); diff --git a/core/libs/tests/GuestGetitimer.cpp b/core/libs/tests/GuestGetitimer.cpp index 57e484e580..1fa5f82516 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}); @@ -89,9 +86,18 @@ int main() { Require(GuestAlarm_nid_no_patch(0) > 0, "getitimer cancelled the alarm"); Require(ReadRemaining() == 0, "cancelled alarm is not zero"); - GuestAlarm_nid_no_patch(std::numeric_limits::max()); - Require(ReadRemaining() > static_cast(std::numeric_limits::max()) * 1000000, - "long alarm was truncated to signed 32 bits"); + constexpr unsigned int longestAlarm = std::numeric_limits::max() / 2; + Require(GuestAlarm_nid_no_patch(longestAlarm) == 0, "alarm was already active"); + Require(ReadRemaining() > static_cast(longestAlarm - 1) * 1000000, + "long alarm was truncated"); + for (unsigned int invalid : {longestAlarm + 1, static_cast(std::numeric_limits::max()), std::numeric_limits::max()}) { + *__error_nid_postfix() = 0; + Require(GuestAlarm_nid_no_patch(invalid) == std::numeric_limits::max() && *__error_nid_postfix() == 22, + "an alarm beyond the itimer range did not fail with EINVAL"); + Require(ReadRemaining() > static_cast(longestAlarm - 1) * 1000000, + "a rejected alarm replaced the pending one"); + } + Require(GuestAlarm_nid_no_patch(0) == longestAlarm, "a rejected alarm changed the remaining time"); GuestAlarm_nid_no_patch(1); std::this_thread::sleep_for(std::chrono::milliseconds(1100)); Require(ReadRemaining() == 0, "expired alarm is not zero"); diff --git a/core/libs/tests/GuestGetopt.cpp b/core/libs/tests/GuestGetopt.cpp index fa3fbebf1b..26e0ca0803 100644 --- a/core/libs/tests/GuestGetopt.cpp +++ b/core/libs/tests/GuestGetopt.cpp @@ -17,6 +17,7 @@ struct GuestOption { extern "C" { int APS5_VABI getopt_nid_postfix(int, char* const*, const char*); int APS5_VABI getopt_long_nid_postfix(int, char* const*, const char*, const GuestOption*, int*); +int APS5_VABI getopt_long_only_nid_postfix(int, char* const*, const char*, const GuestOption*, int*); int APS5_VABI setenv_nid_postfix(const char*, const char*, int); int APS5_VABI unsetenv_nid_postfix(const char*); extern int opterr_nid_postfix; @@ -244,6 +245,71 @@ void ShortOptions() { ExpectText(RunGetopt("bc", permuted), "|3", "getopt does not permute"); } +void LongOnlyOptions() { + int flag = 0; + const GuestOption options[] = { + {"alpha", 0, nullptr, 100}, + {"alpine", 0, nullptr, 100}, + {"b", 0, nullptr, 101}, + {"required", 1, nullptr, 102}, + {"optional", 2, nullptr, 103}, + {"flag", 0, &flag, 7}, + {nullptr, 0, nullptr, 0}, + }; + struct Case { + const char* arguments; + const char* shortOptions; + std::vector results; + std::vector values; + int finalIndex; + }; + const Case cases[] = { + {"foo -alpha --alpha -b", "ab", {100, 100, 101}, {"", "", ""}, 4}, + {"foo -a -acvalue", "ac:", {'a', 'a', 'c'}, {"", "", "value"}, 3}, + {"foo -required=one -req two -optional=three -optional four", "", {102, 102, 103, 103}, {"one", "two", "three", ""}, 6}, + {"foo operand -alpha tail -b", "ab", {100, 101}, {"", ""}, 3}, + {"foo -al --al", "a", {'?', '?'}, {"", ""}, 3}, + {"foo -unknown", "", {'?'}, {""}, 2}, + {"foo -required", ":", {':'}, {""}, 2}, + {"foo -alpha=yes", "", {'?'}, {""}, 2}, + {"foo -flag", "", {0}, {""}, 2}, + {"foo -- -alpha", "", {}, {}, 2}, + {"foo operand -alpha", "+", {}, {}, 1}, + {"foo operand -alpha", "-", {1, 100}, {"operand", ""}, 3}, + {"foo -W alpha", "W;", {100}, {""}, 3}, + }; + for (const auto& test : cases) { + ArgumentVector arguments(test.arguments); + Reset(); + opterr_nid_postfix = 0; + for (std::size_t i = 0; i < test.results.size(); ++i) { + const int result = getopt_long_only_nid_postfix(arguments.Count(), arguments.Values(), test.shortOptions, options, nullptr); + Expect(result == test.results[i], std::string("getopt_long_only result: ") + test.arguments); + ExpectText(optarg_nid_postfix ? optarg_nid_postfix : "", test.values[i], "getopt_long_only argument"); + } + Expect(getopt_long_only_nid_postfix(arguments.Count(), arguments.Values(), test.shortOptions, options, nullptr) == -1, + std::string("getopt_long_only termination: ") + test.arguments); + Expect(optind_nid_postfix == test.finalIndex, std::string("getopt_long_only optind: ") + test.arguments); + } + Expect(flag == 7, "getopt_long_only flag pointer"); + { + ArgumentVector arguments("foo -al"); + StderrCapture capture; + Reset(); + int index = -1; + Expect(getopt_long_only_nid_postfix(arguments.Count(), arguments.Values(), "a", options, &index) == '?' && + optopt_nid_postfix == 0 && index == -1, "ambiguous aliases keep index unchanged"); + ExpectText(capture.Text(), "eboot.bin: option `-al' is ambiguous\n", "single dash diagnostic"); + } + ArgumentVector arguments("foo -alpha"); + Reset(); + int index = -1; + Expect(getopt_long_only_nid_postfix(arguments.Count(), arguments.Values(), "a", options, &index) == 100 && index == 0, + "long-only index"); + optind_nid_postfix = 0; + Expect(getopt_long_only_nid_postfix(arguments.Count(), arguments.Values(), "a", options, nullptr) == 100, "long-only restart"); +} + void LongOptions() { int flag = 0; const GuestOption options[] = { @@ -364,4 +430,5 @@ int main() { FreeBsdGetoptLongCases(); ShortOptions(); LongOptions(); + LongOnlyOptions(); } diff --git a/core/libs/tests/GuestGetrlimit.cpp b/core/libs/tests/GuestGetrlimit.cpp index 5312d114f1..14b00ab71a 100644 --- a/core/libs/tests/GuestGetrlimit.cpp +++ b/core/libs/tests/GuestGetrlimit.cpp @@ -1,11 +1,21 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include +#include +#include #include #include #include -#ifndef _WIN32 +#include +#ifdef _WIN32 +#include +#else #include +#include +#endif +#ifdef __APPLE__ +#include #endif struct ResourceLimit { @@ -16,6 +26,11 @@ struct ResourceLimit { extern "C" int APS5_VABI getrlimit_nid_postfix(int resource, ResourceLimit* limit); extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" int APS5_VABI getdtablesize_nid_postfix(void); +extern "C" int APS5_VABI setrlimit_nid_postfix(int resource, const ResourceLimit* limit); +extern "C" int APS5_VABI pipe_nid_postfix(int* descriptors); +extern "C" int APS5_VABI close_nid_postfix(int descriptor); +extern "C" void* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode); +extern "C" int APS5_VABI fclose_nid_postfix(void* stream); static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EINVAL = 22; @@ -46,12 +61,91 @@ static ResourceLimit GuestLimit(int resource) { return limit; } +#if defined(_WIN32) || defined(__APPLE__) +static void RequireDescriptorsEndAt(const ResourceLimit& limit) { + constexpr int GUEST_EMFILE = 24; + std::vector descriptors; + int pair[2]{-1, -1}; + *__error_nid_postfix() = 0; + while (pipe_nid_postfix(pair) == 0) { + Require(pair[0] >= 0 && pair[0] < limit.rlim_cur && pair[1] >= 0 && pair[1] < limit.rlim_cur); + descriptors.push_back(pair[0]); + descriptors.push_back(pair[1]); + Require(descriptors.size() <= static_cast(limit.rlim_cur)); + } + Require(*__error_nid_postfix() == GUEST_EMFILE); + Require(descriptors.size() + 64 >= static_cast(limit.rlim_cur)); + for (const int descriptor : descriptors) Require(close_nid_postfix(descriptor) == 0); +} +#endif + +#ifdef _WIN32 +static void RequireStreamsOpen() { + const auto path = (std::filesystem::temp_directory_path() / ("aps5-open-streams-" + std::to_string(GetCurrentProcessId()))).string(); + std::vector streams; + for (int index = 0; index < 1024; ++index) { + void* stream = fopen_nid_postfix(path.c_str(), "ab"); + Require(stream != nullptr); + streams.push_back(stream); + } + for (void* stream : streams) Require(fclose_nid_postfix(stream) == 0); + std::filesystem::remove(path); +} +#endif + +static void CheckOpenFileLimit() { + const ResourceLimit limit = GuestLimit(GUEST_RLIMIT_NOFILE); + Require(limit.rlim_cur == limit.rlim_max); +#ifdef _WIN32 + Require(limit.rlim_cur == 8192); + RequireDescriptorsEndAt(limit); + RequireStreamsOpen(); +#else + rlimit host{}; + Require(getrlimit(RLIMIT_NOFILE, &host) == 0); + Require(host.rlim_cur == host.rlim_max); + Require(host.rlim_cur == RLIM_INFINITY ? limit.rlim_cur == GUEST_RLIM_INFINITY : limit.rlim_cur == static_cast(host.rlim_cur)); +#endif +#ifdef __APPLE__ + int perProcess = 0; + std::size_t size = sizeof(perProcess); + Require(sysctlbyname("kern.maxfilesperproc", &perProcess, &size, nullptr, 0) == 0); + Require(perProcess > 0 && limit.rlim_cur <= perProcess); + RequireDescriptorsEndAt(limit); +#endif + Require(setrlimit_nid_postfix(GUEST_RLIMIT_NOFILE, &limit) == 0); + const ResourceLimit lowered{limit.rlim_max > 1 ? limit.rlim_max - 1 : 1, limit.rlim_max}; + 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) { *__error_nid_postfix() = 0; Require(getrlimit_nid_postfix(resource, limit) == -1); Require(*__error_nid_postfix() == error); } +static void RequireInvalidOutputPointer(int resource) { + auto* const invalid = reinterpret_cast(static_cast(1)); + RequireError(resource, invalid, GUEST_EFAULT); +} + #ifndef _WIN32 static std::int64_t Guest(rlim_t value) { return value == RLIM_INFINITY ? GUEST_RLIM_INFINITY : static_cast(value); @@ -76,17 +170,6 @@ static void LoweredHostLimitIsReported() { } #endif -static void RequireUnsupported(int resource) { - ResourceLimit limit{}; - bool rejected = false; - try { - getrlimit_nid_postfix(resource, &limit); - } catch (const std::runtime_error&) { - rejected = true; - } - Require(rejected); -} - int main() { #ifdef _WIN32 GuestLimit(GUEST_RLIMIT_DATA); @@ -98,6 +181,8 @@ int main() { GUEST_RLIMIT_CPU, GUEST_RLIMIT_FSIZE, GUEST_RLIMIT_CORE, + GUEST_RLIMIT_RSS, + GUEST_RLIMIT_MEMLOCK, GUEST_RLIMIT_SBSIZE, GUEST_RLIMIT_AS, GUEST_RLIMIT_NPTS, @@ -116,20 +201,22 @@ int main() { RequireError(GUEST_RLIM_NLIMITS, nullptr, GUEST_EINVAL); RequireError(-1, nullptr, GUEST_EINVAL); RequireError(GUEST_RLIMIT_DATA, nullptr, GUEST_EFAULT); - for (int resource : { - GUEST_RLIMIT_STACK, - GUEST_RLIMIT_RSS, - GUEST_RLIMIT_MEMLOCK, - GUEST_RLIMIT_NPROC, - GUEST_RLIMIT_NOFILE, - }) { - RequireUnsupported(resource); - } - bool unsupported = false; - try { - getdtablesize_nid_postfix(); - } catch (const std::runtime_error&) { - unsupported = true; - } - Require(unsupported); + RequireInvalidOutputPointer(GUEST_RLIMIT_DATA); + RequireInvalidOutputPointer(GUEST_RLIMIT_NOFILE); + const ResourceLimit stack = GuestLimit(GUEST_RLIMIT_STACK); + Require(stack.rlim_cur == 0x200000 && stack.rlim_max == 0x200000); + RequireError(GUEST_RLIMIT_STACK, nullptr, GUEST_EFAULT); + const ResourceLimit processes = GuestLimit(GUEST_RLIMIT_NPROC); + Require(processes.rlim_cur == 1604 && processes.rlim_max == 1604); + RequireError(GUEST_RLIMIT_NPROC, nullptr, GUEST_EFAULT); + const ResourceLimit nofile = GuestLimit(GUEST_RLIMIT_NOFILE); + const int tableSize = getdtablesize_nid_postfix(); + std::int64_t expectedTableSize = nofile.rlim_cur; +#ifndef _WIN32 + expectedTableSize = std::min(expectedTableSize, static_cast(::getdtablesize())); +#endif + if (expectedTableSize > std::numeric_limits::max()) + expectedTableSize = std::numeric_limits::max(); + Require(tableSize == static_cast(expectedTableSize)); + CheckOpenFileLimit(); } 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/GuestGnmNeoRetail.cpp b/core/libs/tests/GuestGnmNeoRetail.cpp new file mode 100644 index 0000000000..29148976fa --- /dev/null +++ b/core/libs/tests/GuestGnmNeoRetail.cpp @@ -0,0 +1,131 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceGnmDebugModuleReset(void); +int APS5_VABI sceGnmDebugReset(void); +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterface(void); +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForGpuDebugger(void); +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForReplay(void); +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForResourceRegistration(void); +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForValidation(void); +int APS5_VABI sceGnmDriverTraceInProgress(void); +int APS5_VABI sceGnmFindResources(void); +int APS5_VABI sceGnmFindResourcesPublic(void); +int APS5_VABI sceGnmGetDbgGcHandle(void); +int APS5_VABI sceGnmGetOwnerName(void); +int APS5_VABI sceGnmGetPhysicalCounterFromVirtualized(void); +int APS5_VABI sceGnmGetResourceBaseAddressAndSizeInBytes(void); +int APS5_VABI sceGnmGetResourceName(void); +int APS5_VABI sceGnmGetResourceRegistrationBuffers(void); +int APS5_VABI sceGnmGetResourceShaderGuid(void); +int APS5_VABI sceGnmGetResourceType(void); +int APS5_VABI sceGnmGetResourceUserData(void); +int APS5_VABI sceGnmQueryResourceRegistrationUserMemoryRequirements(void); +int APS5_VABI sceGnmRegisterGdsResource(void); +int APS5_VABI sceGnmRegisterOwner(void); +int APS5_VABI sceGnmRegisterOwnerForSystem(void); +int APS5_VABI sceGnmRegisterResource(void); +int APS5_VABI sceGnmSdmaClose(void); +int APS5_VABI sceGnmSdmaConstFill(void); +int APS5_VABI sceGnmSdmaCopyLinear(void); +int APS5_VABI sceGnmSdmaCopyTiled(void); +int APS5_VABI sceGnmSdmaCopyWindow(void); +int APS5_VABI sceGnmSdmaFlush(void); +int APS5_VABI sceGnmSdmaGetMinCmdSize(void); +int APS5_VABI sceGnmSdmaOpen(void); +int APS5_VABI sceGnmSetResourceRegistrationUserMemory(void); +int APS5_VABI sceGnmSetResourceUserData(void); +int APS5_VABI sceGnmSetSpiEnableSqCounters(void); +int APS5_VABI sceGnmSetSpiEnableSqCountersForUnitInstance(void); +int APS5_VABI sceGnmUnregisterAllResourcesForOwner(void); +int APS5_VABI sceGnmUnregisterOwnerAndResources(void); +int APS5_VABI sceGnmUnregisterResource(void); +int APS5_VABI sceGnmValidateDisableDiagnostics(void); +int APS5_VABI sceGnmValidateDisableDiagnostics2(void); +int APS5_VABI sceGnmValidateDispatchCommandBuffers(void); +int APS5_VABI sceGnmValidateDrawCommandBuffers(void); +int APS5_VABI sceGnmValidateGetDiagnosticInfo(void); +int APS5_VABI sceGnmValidateGetDiagnostics(void); +int APS5_VABI sceGnmValidateOnSubmitEnabled(void); +int APS5_VABI sceGnmValidateResetState(void); +int APS5_VABI sceGnmValidationRegisterMemoryCheckCallback(void); +} + +namespace { + +constexpr int Failure = static_cast(0x8EEE00FFu); +constexpr int NoInterface = static_cast(0x80000000u); +constexpr int ValidationNotEnabled = static_cast(0x80D13FFFu); + +using Query = int (APS5_VABI *)(void); + +void RequireAll(int expected, std::initializer_list queries) { + for (const auto query : queries) { + if (query() != expected) std::abort(); + } +} + +} // namespace + +int main() { + RequireAll(Failure, { + sceGnmDebugModuleReset, + sceGnmDebugReset, + sceGnmFindResources, + sceGnmFindResourcesPublic, + sceGnmGetOwnerName, + sceGnmGetResourceBaseAddressAndSizeInBytes, + sceGnmGetResourceName, + sceGnmGetResourceRegistrationBuffers, + sceGnmGetResourceShaderGuid, + sceGnmGetResourceType, + sceGnmGetResourceUserData, + sceGnmQueryResourceRegistrationUserMemoryRequirements, + sceGnmRegisterGdsResource, + sceGnmRegisterOwner, + sceGnmRegisterOwnerForSystem, + sceGnmRegisterResource, + sceGnmSetResourceRegistrationUserMemory, + sceGnmSetResourceUserData, + sceGnmUnregisterAllResourcesForOwner, + sceGnmUnregisterOwnerAndResources, + sceGnmUnregisterResource, + sceGnmSdmaClose, + sceGnmSdmaConstFill, + sceGnmSdmaCopyLinear, + sceGnmSdmaCopyTiled, + sceGnmSdmaCopyWindow, + sceGnmSdmaFlush, + sceGnmSdmaGetMinCmdSize, + sceGnmSdmaOpen, + sceGnmGetPhysicalCounterFromVirtualized, + sceGnmSetSpiEnableSqCounters, + sceGnmSetSpiEnableSqCountersForUnitInstance, + }); + RequireAll(NoInterface, { + sceGnmDriverInternalRetrieveGnmInterface, + sceGnmDriverInternalRetrieveGnmInterfaceForGpuDebugger, + sceGnmDriverInternalRetrieveGnmInterfaceForReplay, + sceGnmDriverInternalRetrieveGnmInterfaceForResourceRegistration, + sceGnmDriverInternalRetrieveGnmInterfaceForValidation, + }); + RequireAll(0, { + sceGnmDriverTraceInProgress, + sceGnmValidateOnSubmitEnabled, + }); + RequireAll(-1, { + sceGnmGetDbgGcHandle, + }); + RequireAll(ValidationNotEnabled, { + sceGnmValidateDisableDiagnostics, + sceGnmValidateDisableDiagnostics2, + sceGnmValidateDispatchCommandBuffers, + sceGnmValidateDrawCommandBuffers, + sceGnmValidateGetDiagnosticInfo, + sceGnmValidateGetDiagnostics, + sceGnmValidateResetState, + sceGnmValidationRegisterMemoryCheckCallback, + }); +} diff --git a/core/libs/tests/GuestHeapTraceInfo.cpp b/core/libs/tests/GuestHeapTraceInfo.cpp new file mode 100644 index 0000000000..56c18baf06 --- /dev/null +++ b/core/libs/tests/GuestHeapTraceInfo.cpp @@ -0,0 +1,29 @@ +#include "prx/libc/include/HeapDiagnostics.hpp" +#include +#include +#include + +namespace { + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +} + +int main() { + LibcHeapInfo info{sizeof(LibcHeapInfo), 0, 1, nullptr, nullptr}; + LibcHeapTraceInfo_nid_no_patch(&info); + Require(info.unknown2 == 0, "heap trace info did not clear unknown2"); + Require(info.mspace_atomic_id_mask != nullptr, "heap trace info returned no mspace mask"); + Require(info.mstate_table != nullptr, "heap trace info returned no mstate table"); + + constexpr std::uint64_t first = 0x0123456789abcdefull; + constexpr std::uint64_t last = 0xfedcba9876543210ull; + info.mstate_table[0] = first; + info.mstate_table[64] = last; + Require(info.mstate_table[0] == first, "heap trace info base slot is not writable"); + Require(info.mstate_table[64] == last, "heap trace info final 64-bit mask slot is not writable"); +} diff --git a/core/libs/tests/GuestHmd.cpp b/core/libs/tests/GuestHmd.cpp index 6b97bea9cc..257e9f381c 100644 --- a/core/libs/tests/GuestHmd.cpp +++ b/core/libs/tests/GuestHmd.cpp @@ -1,12 +1,22 @@ +#include "ProcessParametersFixture.hpp" #include "prx/libSceHmd/Hmd.hpp" +#include +#include +#include #include #include #include #include +#include namespace { +constexpr std::uint32_t LastSdkBeforeStartupState = 0x044fffff; +constexpr std::uint32_t FirstSdkWithStartupState = 0x04500000; + +alignas(16) std::array process{}; + void Require(bool condition, const char* message) { if (!condition) { std::fprintf(stderr, "HMD: %s\n", message); @@ -14,12 +24,138 @@ void Require(bool condition, const char* message) { } } +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"); +} + +void SetCompiledSdkVersion(std::uint32_t version) { + std::memcpy(process.data() + 0x10, &version, sizeof(version)); +} + void CheckDisconnected() { Hmd::DeviceStatus status = Hmd::DeviceStatus::Ready; Require(sceHmdInternalGetDeviceStatus(&status) == 0, "status query failed"); Require(status == Hmd::DeviceStatus::NotDetected, "headset reported connected"); Require(sceHmdInternalGetDeviceStatus(nullptr) == Hmd::ParameterNull, "null status"); Require(sceHmdInternalMmapIsConnect() == 0, "connection reported present"); + Require(!sceHmdInternalIsGameVr2d(), "game reported in VR2D without a headset"); + Require(!sceHmdInternalIsMiniAppVr2d(), "mini app reported in VR2D without a headset"); + Require(!sceHmdInternalIsCommonDlgVr2d(), "common dialog reported in VR2D without a headset"); + Require(!sceHmdInternalIsCommonDlgMiniAppVr2d(), "common dialog mini app reported in VR2D without a headset"); +} + +void CheckBufferQueries() { + Require(sceHmdDistortionGetWorkMemoryAlign() == 0x400, "distortion work memory alignment"); + Require(sceHmdDistortionGetWorkMemorySize() == 0x20000, "distortion work memory size"); + Require(sceHmdReprojectionQueryGarlicBuffAlign() == 0x100, "reprojection garlic buffer alignment"); + Require(sceHmdReprojectionQueryGarlicBuffSize() == 0x100000, "reprojection garlic buffer size"); + Require(sceHmdReprojectionQueryOnionBuffAlign() == 0x100, "reprojection onion buffer alignment"); + Require(sceHmdReprojectionQueryOnionBuffSize() == 0x810, "reprojection onion buffer size"); +} + +void CheckHandleQueries(std::int32_t expected) { + unsigned char left[16]; + unsigned char right[16]; + unsigned char fieldOfView[16]; + unsigned char sensor[64]; + std::memset(left, 0x5a, sizeof(left)); + std::memset(right, 0x5a, sizeof(right)); + std::memset(fieldOfView, 0x5a, sizeof(fieldOfView)); + std::memset(sensor, 0x5a, sizeof(sensor)); + for (const auto handle : {0, -1, 1, 0x0f000000}) { + Require(sceHmdGetFieldOfView(handle, nullptr) == Hmd::ParameterNull, "null field of view accepted"); + Require(sceHmdGetFieldOfView(handle, reinterpret_cast(fieldOfView)) == expected, "field of view without a headset"); + Require(sceHmdGet2DEyeOffset(handle, reinterpret_cast(left), reinterpret_cast(right)) == expected, "eye offset without a headset"); + Require(sceHmdGet2DEyeOffset(handle, nullptr, nullptr) == expected, "eye offset with null outputs"); + Require(sceHmdGetInertialSensorData(handle, reinterpret_cast(sensor), 1) == expected, "sensor data without a headset"); + } + for (const auto* bytes : {left, right, fieldOfView}) { + for (std::size_t index = 0; index < 16; ++index) Require(bytes[index] == 0x5a, "a refused query wrote its output"); + } + for (const auto byte : sensor) Require(byte == 0x5a, "a refused sensor query wrote its output"); +} + +template +bool Unimplemented(Call call) { + try { + static_cast(call()); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +void CheckCommandQueries(bool handleZeroMeasured) { + unsigned char zeroed[64] = {}; + unsigned char vr2dBytes[64] = {}; + unsigned char commands[16]; + std::memset(commands, 0x5a, sizeof(commands)); + for (const std::size_t offset : {0x00u, 0x08u, 0x20u}) std::memset(vr2dBytes + offset, 0x11, 8); + const auto* distortion = reinterpret_cast(zeroed); + const auto* wideNear = reinterpret_cast(zeroed); + const auto* zeroedVr2d = reinterpret_cast(zeroed); + const auto* vr2d = reinterpret_cast(vr2dBytes); + auto* commandBuffer = reinterpret_cast(commands); + const auto vr2dWith = [&](std::size_t offset, unsigned char value, std::size_t bytes, auto check) { + unsigned char changed[64]; + std::memcpy(changed, vr2dBytes, sizeof(changed)); + std::memset(changed + offset, value, bytes); + check(reinterpret_cast(changed)); + }; + for (const auto handle : {-1, 1, 0x0f000000}) { + Require(sceHmdGetDistortionCorrectionCommand(handle, distortion, commandBuffer) == Hmd::InvalidHandle, "distortion command accepted a handle sceHmdOpen never issued"); + Require(sceHmdDistortionGetCorrectionCommand(handle, distortion, commandBuffer) == Hmd::InvalidHandle, "aliased distortion command accepted a handle sceHmdOpen never issued"); + Require(sceHmdGetWideNearDistortionCorrectionCommand(handle, wideNear, commandBuffer) == Hmd::InvalidHandle, "wide-near distortion command accepted a handle sceHmdOpen never issued"); + Require(sceHmdDistortionGetWideNearCorrectionCommand(handle, wideNear, commandBuffer) == Hmd::InvalidHandle, "aliased wide-near distortion command accepted a handle sceHmdOpen never issued"); + Require(sceHmdGet2dVrCommand(handle, vr2d, commandBuffer) == Hmd::InvalidHandle, "2D VR command accepted a handle sceHmdOpen never issued"); + Require(sceHmdDistortionGet2dVrCommand(handle, vr2d, commandBuffer) == Hmd::InvalidHandle, "aliased 2D VR command accepted a handle sceHmdOpen never issued"); + } + for (const auto handle : {0, -1, 1, 0x0f000000}) { + for (const auto query : {sceHmdGet2dVrCommand, sceHmdDistortionGet2dVrCommand}) { + Require(query(handle, zeroedVr2d, commandBuffer) == Hmd::ParameterNull, "2D VR command took a parameter with null pointers"); + Require(query(handle, nullptr, commandBuffer) == Hmd::ParameterNull, "2D VR command took a null parameter"); + Require(query(handle, vr2d, nullptr) == Hmd::ParameterNull, "2D VR command took a null command buffer"); + for (const std::size_t offset : {0x00u, 0x08u, 0x20u}) { + vr2dWith(offset, 0, 8, [&](const Hmd2dVrCommandParameter* changed) { + Require(query(handle, changed, commandBuffer) == Hmd::ParameterNull, "2D VR command took a null pointer in its parameter"); + }); + } + vr2dWith(0x34, 0x01, 4, [&](const Hmd2dVrCommandParameter* changed) { + Require(query(handle, changed, commandBuffer) == Hmd::ParameterInvalid, "2D VR command took a non-zero field at 0x34"); + }); + vr2dWith(0x38, 0x01, 8, [&](const Hmd2dVrCommandParameter* changed) { + Require(query(handle, changed, commandBuffer) == Hmd::ParameterInvalid, "2D VR command took a non-zero field at 0x38"); + }); + } + } + if (!handleZeroMeasured) { + Require(Unimplemented([&] { return sceHmdGetDistortionCorrectionCommand(0, distortion, commandBuffer); }), "distortion command answered handle 0 where its answer is not measured"); + Require(Unimplemented([&] { return sceHmdDistortionGetCorrectionCommand(0, distortion, commandBuffer); }), "aliased distortion command answered handle 0 where its answer is not measured"); + Require(Unimplemented([&] { return sceHmdGetWideNearDistortionCorrectionCommand(0, wideNear, commandBuffer); }), "wide-near distortion command answered handle 0 where its answer is not measured"); + Require(Unimplemented([&] { return sceHmdDistortionGetWideNearCorrectionCommand(0, wideNear, commandBuffer); }), "aliased wide-near distortion command answered handle 0 where its answer is not measured"); + Require(Unimplemented([&] { return sceHmdGet2dVrCommand(0, vr2d, commandBuffer); }), "2D VR command answered handle 0 where its answer is not measured"); + Require(Unimplemented([&] { return sceHmdDistortionGet2dVrCommand(0, vr2d, commandBuffer); }), "aliased 2D VR command answered handle 0 where its answer is not measured"); + } else { + Require(sceHmdGetDistortionCorrectionCommand(0, distortion, commandBuffer) == Hmd::NotInitialized, "distortion command before initialize"); + Require(sceHmdDistortionGetCorrectionCommand(0, distortion, commandBuffer) == Hmd::NotInitialized, "aliased distortion command before initialize"); + Require(sceHmdGetWideNearDistortionCorrectionCommand(0, wideNear, commandBuffer) == Hmd::NotInitialized, "wide-near distortion command before initialize"); + Require(sceHmdDistortionGetWideNearCorrectionCommand(0, wideNear, commandBuffer) == Hmd::NotInitialized, "aliased wide-near distortion command before initialize"); + Require(sceHmdGet2dVrCommand(0, vr2d, commandBuffer) == Hmd::NotInitialized, "2D VR command before initialize"); + Require(sceHmdDistortionGet2dVrCommand(0, vr2d, commandBuffer) == Hmd::NotInitialized, "aliased 2D VR command before initialize"); + } + for (const auto byte : commands) Require(byte == 0x5a, "a refused command query wrote its output"); +} + +void CheckAssyError(std::int32_t expected) { + unsigned char data[32]; + std::memset(data, 0x5a, sizeof(data)); + Require(sceHmdGetAssyError(nullptr) == Hmd::ParameterNull, "null assembly error output accepted"); + Require(sceHmdGetAssyError(reinterpret_cast(data)) == expected, "assembly error query without a headset"); + for (const auto byte : data) Require(byte == 0x5a, "a refused assembly error query wrote its output"); } } @@ -30,13 +166,21 @@ int main() { std::memset(&info, 0xa5, sizeof(info)); const auto untouched = info; + InstallProcessParameters(); CheckDisconnected(); + CheckBufferQueries(); Require(sceHmdTerminate() == Hmd::NotInitialized, "terminate before initialize"); Require(sceHmdOpen(1, 0, 0, nullptr) == Hmd::NotInitialized, "open before initialize"); Require(sceHmdClose(0) == Hmd::NotInitialized, "close before initialize"); Require(sceHmdGetDeviceInformation(nullptr) == Hmd::ParameterNull, "null information"); Require(sceHmdGetDeviceInformation(&info) == Hmd::NotInitialized, "query before initialize"); Require(std::memcmp(&info, &untouched, sizeof(info)) == 0, "failed query modified output"); + CheckHandleQueries(Hmd::NotInitialized); + SetCompiledSdkVersion(LastSdkBeforeStartupState); + CheckCommandQueries(true); + SetCompiledSdkVersion(FirstSdkWithStartupState); + CheckCommandQueries(false); + CheckAssyError(Hmd::NotInitialized); Require(sceHmdGetDeviceInformationByHandle(0, &info) == Hmd::NotInitialized, "handle query before initialize"); Require(sceHmdInitialize(nullptr) == Hmd::ParameterNull, "null initialize"); @@ -73,6 +217,9 @@ int main() { "query accepted invalid handle"); Require(std::memcmp(&info, &expected, sizeof(info)) == 0, "invalid handle modified output"); } + CheckHandleQueries(Hmd::InvalidHandle); + CheckCommandQueries(false); + CheckAssyError(Hmd::DeviceDisconnected); Require(sceHmdGetDeviceInformationByHandle(0, nullptr) == Hmd::InvalidHandle, "null query accepted invalid handle"); Require(sceHmdTerminate() == 0, "terminate failed"); diff --git a/core/libs/tests/GuestHmd2.cpp b/core/libs/tests/GuestHmd2.cpp index 85af8b0350..e1bd7b8c52 100644 --- a/core/libs/tests/GuestHmd2.cpp +++ b/core/libs/tests/GuestHmd2.cpp @@ -1,17 +1,73 @@ -#include "prx/libc/include/general/VabiMacros.hpp" +#include "ProcessParametersFixture.hpp" +#include "prx/libSceHmd/Hmd.hpp" +#include "prx/libSceHmd2/Hmd2.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include #include #include #include +#include #include +namespace Hmd2 { +struct ReprojectionOutput; +} + extern "C" { -std::int32_t APS5_VABI sceHmd2Initialize(const void* param); -int APS5_VABI sceHmd2Open(); +std::int32_t APS5_VABI Hmd2WriteStateTwo(void* a0); +std::int32_t APS5_VABI Hmd2WriteSize0x2000Align0x100(std::uint64_t* a0, std::uint64_t* a1); +std::int32_t APS5_VABI Hmd2WriteFourConstants(void* a0); +std::int32_t APS5_VABI Hmd2WriteSixHalvesThenInvalidHandle(std::uint64_t a0, void* a1); +std::int32_t APS5_VABI Hmd2WriteTwelveHalvesThenInvalidHandle(std::uint64_t a0, void* a1); +std::int32_t APS5_VABI Hmd2SlotUpTo3CheckSecondThirdThenZero(std::uint64_t a0, const void* a1, const void* a2); +std::int32_t APS5_VABI Hmd2SlotUpTo1ThenError1002(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo1CheckSecondThenError1002(std::uint64_t a0, const void* a1); +std::int32_t APS5_VABI Hmd2SlotUpTo1ThenMinusOne(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenMinusOne(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenError1002(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenError1003(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo2CheckSecondThenError002C(std::uint64_t a0, const void* a1); +std::int32_t APS5_VABI Hmd2IndexZeroCheckSecondThenZero(std::uint64_t a0, const void* a1); +std::int32_t APS5_VABI Hmd2CheckFirstThenError0004(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenZero(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenError1002(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenMinusOne(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenParameterInvalid(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckSecondThenError002C(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenParameterInvalid(const void*); +std::int32_t APS5_VABI Hmd2CheckAllSixThenZero(const void*, const void*, const void*, const void*, const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckSecondThenParameterInvalid(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenError800d0203(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenError1003(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenError1010(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenError1002(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenError1003(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstAndThirdThenZero(const void*, const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstAndThirdThenError1003(const void*, const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckSecondThenInvalidHandle(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckSecondThenError1002(const void*, const void*); +std::int32_t APS5_VABI Hmd2BeforeInitializeZero(); +std::int32_t APS5_VABI Hmd2BeforeInitializeNotInitialized(); +std::int32_t APS5_VABI Hmd2BeforeInitializeError0004(); +std::int32_t APS5_VABI Hmd2BeforeInitializeParameterNull(); +std::int32_t APS5_VABI Hmd2BeforeInitializeParameterInvalid(); +std::int32_t APS5_VABI Hmd2BeforeInitializeReprojectionNotInitialized(); +std::int32_t APS5_VABI Hmd2BeforeInitializeError002C(); +std::int32_t APS5_VABI Hmd2BeforeInitializeError1002(); +std::int32_t APS5_VABI Hmd2BeforeInitializeError1003(); +std::int32_t APS5_VABI Hmd2BeforeInitializeMinusOne(); +std::int32_t APS5_VABI Hmd2BeforeInitializeValue0400(); +std::int32_t APS5_VABI Hmd2BeforeInitializeValue20000(); +int APS5_VABI sceHmd2GazeActivateGazeTracking(std::int32_t mode); +int APS5_VABI sceHmd2ReprojectionGetCurrentOutput(Hmd2::ReprojectionOutput* output); +int APS5_VABI sceHmd2SetFieldOfView(const Hmd2::FieldOfView* fieldOfView); } namespace { +alignas(16) std::array process{}; + void Require(bool condition, const char* message) { if (!condition) { std::fprintf(stderr, "HMD2: %s\n", message); @@ -19,19 +75,247 @@ void Require(bool condition, const char* message) { } } +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"); +} + +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"); + { + std::uint8_t out[64]; + std::uint64_t words[2]; + Require(Hmd2WriteStateTwo(nullptr) == Hmd2::ParameterNull, "Hmd2WriteStateTwo null before initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteStateTwo(out) == 0 && out[0] == 2 && out[1] == 0 && out[4] == 0 && out[5] == 0xaa, "Hmd2WriteStateTwo bytes before initialization"); + Require(Hmd2WriteSize0x2000Align0x100(nullptr, words) == Hmd2::ParameterNull && Hmd2WriteSize0x2000Align0x100(words, nullptr) == Hmd2::ParameterNull, "Hmd2WriteSize0x2000Align0x100 null before initialization"); + words[0] = words[1] = ~0ull; + Require(Hmd2WriteSize0x2000Align0x100(words, words + 1) == 0 && words[0] == 0x2000 && words[1] == 0x100, "Hmd2WriteSize0x2000Align0x100 values before initialization"); + Require(Hmd2WriteFourConstants(nullptr) == Hmd2::ParameterNull, "Hmd2WriteFourConstants null before initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteFourConstants(out) == 0 && out[0] == 0xe1 && out[15] == 0x3c && out[16] == 0xaa, "Hmd2WriteFourConstants bytes before initialization"); + Require(Hmd2WriteSixHalvesThenInvalidHandle(0, nullptr) == Hmd2::ParameterNull, "Hmd2WriteSixHalvesThenInvalidHandle null before initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteSixHalvesThenInvalidHandle(0, out) == Hmd2::InvalidHandle && out[7] == 0x3f && out[20] == 0x00 && out[23] == 0x3f && out[24] == 0xaa, "Hmd2WriteSixHalvesThenInvalidHandle bytes before initialization"); + Require(Hmd2WriteTwelveHalvesThenInvalidHandle(0, nullptr) == Hmd2::ParameterNull, "Hmd2WriteTwelveHalvesThenInvalidHandle null before initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteTwelveHalvesThenInvalidHandle(0, out) == Hmd2::InvalidHandle && out[7] == 0x3f && out[31] == 0x3f && out[47] == 0x00 && out[48] == 0xaa, "Hmd2WriteTwelveHalvesThenInvalidHandle bytes before initialization"); + Require(Hmd2SlotUpTo3CheckSecondThirdThenZero(9, nullptr, out) == Hmd2::ParameterNull && Hmd2SlotUpTo3CheckSecondThirdThenZero(0, out, nullptr) == Hmd2::ParameterNull, "Hmd2SlotUpTo3CheckSecondThirdThenZero null before initialization"); + Require(Hmd2SlotUpTo3CheckSecondThirdThenZero(3, out, out) == 0 && Hmd2SlotUpTo3CheckSecondThirdThenZero(4, out, out) == Hmd2::ParameterInvalid && Hmd2SlotUpTo3CheckSecondThirdThenZero(~0ull, out, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo3CheckSecondThirdThenZero slots before initialization"); + Require(Hmd2SlotUpTo1ThenError1002(1) == Hmd2::Error1002 && Hmd2SlotUpTo1ThenError1002(2) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1ThenError1002 before initialization"); + Require(Hmd2SlotUpTo1CheckSecondThenError1002(5, nullptr) == Hmd2::ParameterNull && Hmd2SlotUpTo1CheckSecondThenError1002(0, out) == Hmd2::Error1002 && Hmd2SlotUpTo1CheckSecondThenError1002(2, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1CheckSecondThenError1002 before initialization"); + Require(Hmd2SlotUpTo1ThenMinusOne(1) == -1 && Hmd2SlotUpTo1ThenMinusOne(2) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1ThenMinusOne before initialization"); + Require(Hmd2SlotUpTo2ThenMinusOne(2) == -1 && Hmd2SlotUpTo2ThenMinusOne(3) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenMinusOne before initialization"); + Require(Hmd2SlotUpTo2ThenError1002(2) == Hmd2::Error1002 && Hmd2SlotUpTo2ThenError1002(3) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenError1002 before initialization"); + Require(Hmd2SlotUpTo2ThenError1003(0) == Hmd2::Error1003 && Hmd2SlotUpTo2ThenError1003(16) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenError1003 before initialization"); + Require(Hmd2SlotUpTo2CheckSecondThenError002C(8, nullptr) == Hmd2::ParameterNull && Hmd2SlotUpTo2CheckSecondThenError002C(2, out) == Hmd2::Error002C && Hmd2SlotUpTo2CheckSecondThenError002C(3, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2CheckSecondThenError002C before initialization"); + Require(Hmd2IndexZeroCheckSecondThenZero(1, nullptr) == Hmd2::ParameterNull && Hmd2IndexZeroCheckSecondThenZero(0, out) == 0 && Hmd2IndexZeroCheckSecondThenZero(1, out) == Hmd2::Error100F, "Hmd2IndexZeroCheckSecondThenZero before initialization"); + } + const std::uint8_t dummyBytes[16]{}; + const void* dummy = dummyBytes; + Require(Hmd2CheckFirstThenError0004(dummy) == Hmd2::Error0004, "Hmd2CheckFirstThenError0004 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError0004(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError0004 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenZero(dummy) == 0, "Hmd2CheckFirstThenZero with every pointer present before initialization"); + Require(Hmd2CheckFirstThenZero(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenZero with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenError1002(dummy) == Hmd2::Error1002, "Hmd2CheckFirstThenError1002 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError1002(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1002 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenMinusOne(dummy) == -1, "Hmd2CheckFirstThenMinusOne with every pointer present before initialization"); + Require(Hmd2CheckFirstThenMinusOne(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenMinusOne with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(dummy, dummy) == Hmd2::ParameterInvalid, "Hmd2CheckFirstTwoThenParameterInvalid with every pointer present before initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenParameterInvalid with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenParameterInvalid with pointer 1 null before initialization"); + Require(Hmd2CheckSecondThenError002C(dummy, dummy) == Hmd2::Error002C, "Hmd2CheckSecondThenError002C with every pointer present before initialization"); + Require(Hmd2CheckSecondThenError002C(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenError002C with pointer 1 null before initialization"); + Require(Hmd2CheckFirstThenParameterInvalid(dummy) == Hmd2::ParameterInvalid, "Hmd2CheckFirstThenParameterInvalid with every pointer present before initialization"); + Require(Hmd2CheckFirstThenParameterInvalid(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenParameterInvalid with pointer 0 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, dummy, dummy) == 0, "Hmd2CheckAllSixThenZero with every pointer present before initialization"); + Require(Hmd2CheckAllSixThenZero(nullptr, dummy, dummy, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 0 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, nullptr, dummy, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 1 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, nullptr, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 2 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 3 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 4 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 5 null before initialization"); + Require(Hmd2CheckSecondThenParameterInvalid(dummy, dummy) == Hmd2::ParameterInvalid, "Hmd2CheckSecondThenParameterInvalid with every pointer present before initialization"); + Require(Hmd2CheckSecondThenParameterInvalid(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenParameterInvalid with pointer 1 null before initialization"); + Require(Hmd2CheckFirstThenError800d0203(dummy) == Hmd2::Error800d0203, "Hmd2CheckFirstThenError800d0203 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError800d0203(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError800d0203 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenError1003(dummy) == Hmd2::Error1003, "Hmd2CheckFirstThenError1003 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError1003(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1003 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenError1010(dummy) == Hmd2::Error1010, "Hmd2CheckFirstThenError1010 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError1010(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1010 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenError1002(dummy, dummy) == Hmd2::Error1002, "Hmd2CheckFirstTwoThenError1002 with every pointer present before initialization"); + Require(Hmd2CheckFirstTwoThenError1002(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1002 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenError1002(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1002 with pointer 1 null before initialization"); + Require(Hmd2CheckFirstTwoThenError1003(dummy, dummy) == Hmd2::Error1003, "Hmd2CheckFirstTwoThenError1003 with every pointer present before initialization"); + Require(Hmd2CheckFirstTwoThenError1003(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1003 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenError1003(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1003 with pointer 1 null before initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(dummy, dummy, dummy) == 0, "Hmd2CheckFirstAndThirdThenZero with every pointer present before initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenZero with pointer 0 null before initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenZero with pointer 2 null before initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(dummy, dummy, dummy) == Hmd2::Error1003, "Hmd2CheckFirstAndThirdThenError1003 with every pointer present before initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenError1003 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenError1003 with pointer 2 null before initialization"); + Require(Hmd2CheckSecondThenInvalidHandle(dummy, dummy) == Hmd2::InvalidHandle, "Hmd2CheckSecondThenInvalidHandle with every pointer present before initialization"); + Require(Hmd2CheckSecondThenInvalidHandle(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenInvalidHandle with pointer 1 null before initialization"); + Require(Hmd2CheckSecondThenError1002(dummy, dummy) == Hmd2::Error1002, "Hmd2CheckSecondThenError1002 with every pointer present before initialization"); + Require(Hmd2CheckSecondThenError1002(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenError1002 with pointer 1 null before initialization"); + Require(Hmd2BeforeInitializeZero() == 0, "Hmd2BeforeInitializeZero before initialization"); + Require(Hmd2BeforeInitializeNotInitialized() == Hmd2::NotInitialized, "Hmd2BeforeInitializeNotInitialized before initialization"); + Require(Hmd2BeforeInitializeError0004() == Hmd2::Error0004, "Hmd2BeforeInitializeError0004 before initialization"); + Require(Hmd2BeforeInitializeParameterNull() == Hmd2::ParameterNull, "Hmd2BeforeInitializeParameterNull before initialization"); + Require(Hmd2BeforeInitializeParameterInvalid() == Hmd2::ParameterInvalid, "Hmd2BeforeInitializeParameterInvalid before initialization"); + Require(Hmd2BeforeInitializeReprojectionNotInitialized() == Hmd2::ReprojectionNotInitialized, "Hmd2BeforeInitializeReprojectionNotInitialized before initialization"); + Require(Hmd2BeforeInitializeError002C() == Hmd2::Error002C, "Hmd2BeforeInitializeError002C before initialization"); + Require(Hmd2BeforeInitializeError1002() == Hmd2::Error1002, "Hmd2BeforeInitializeError1002 before initialization"); + Require(Hmd2BeforeInitializeError1003() == Hmd2::Error1003, "Hmd2BeforeInitializeError1003 before initialization"); + Require(Hmd2BeforeInitializeMinusOne() == -1, "Hmd2BeforeInitializeMinusOne before initialization"); + Require(Hmd2BeforeInitializeValue0400() == 0x400, "Hmd2BeforeInitializeValue0400 before initialization"); + Require(Hmd2BeforeInitializeValue20000() == 0x20000, "Hmd2BeforeInitializeValue20000 before initialization"); + Require(sceHmd2SetVibration(0, 0) == 0, "vibration of the unopened handle 0 failed"); +} + +void TestWithoutHeadset() { + const std::uint8_t param[16]{}; + Require(sceHmd2Initialize(param) == Hmd::UnsupportedFeature, "initialization without a headset did not fail"); + Require(sceHmd2Initialize(nullptr) == Hmd::UnsupportedFeature, "initialization without a param did not fail as unsupported"); + Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr) == Hmd2::NotInitialized, "open without a headset did not fail"); + Hmd2::DeviceInformation info; + Require(sceHmd2GetDeviceInformation(&info) == Hmd2::NotInitialized, "device information without a headset"); + Require(sceHmd2Terminate() == Hmd2::NotInitialized, "terminate after the failed initialization"); + { + std::uint8_t out[64]; + std::uint64_t words[2]; + Require(Hmd2WriteStateTwo(nullptr) == Hmd2::ParameterNull, "Hmd2WriteStateTwo null after the failed initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteStateTwo(out) == 0 && out[0] == 2 && out[1] == 0 && out[4] == 0 && out[5] == 0xaa, "Hmd2WriteStateTwo bytes after the failed initialization"); + Require(Hmd2WriteSize0x2000Align0x100(nullptr, words) == Hmd2::ParameterNull && Hmd2WriteSize0x2000Align0x100(words, nullptr) == Hmd2::ParameterNull, "Hmd2WriteSize0x2000Align0x100 null after the failed initialization"); + words[0] = words[1] = ~0ull; + Require(Hmd2WriteSize0x2000Align0x100(words, words + 1) == 0 && words[0] == 0x2000 && words[1] == 0x100, "Hmd2WriteSize0x2000Align0x100 values after the failed initialization"); + Require(Hmd2WriteFourConstants(nullptr) == Hmd2::ParameterNull, "Hmd2WriteFourConstants null after the failed initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteFourConstants(out) == 0 && out[0] == 0xe1 && out[15] == 0x3c && out[16] == 0xaa, "Hmd2WriteFourConstants bytes after the failed initialization"); + Require(Hmd2WriteSixHalvesThenInvalidHandle(0, nullptr) == Hmd2::ParameterNull, "Hmd2WriteSixHalvesThenInvalidHandle null after the failed initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteSixHalvesThenInvalidHandle(0, out) == Hmd2::InvalidHandle && out[7] == 0x3f && out[20] == 0x00 && out[23] == 0x3f && out[24] == 0xaa, "Hmd2WriteSixHalvesThenInvalidHandle bytes after the failed initialization"); + Require(Hmd2WriteTwelveHalvesThenInvalidHandle(0, nullptr) == Hmd2::ParameterNull, "Hmd2WriteTwelveHalvesThenInvalidHandle null after the failed initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteTwelveHalvesThenInvalidHandle(0, out) == Hmd2::InvalidHandle && out[7] == 0x3f && out[31] == 0x3f && out[47] == 0x00 && out[48] == 0xaa, "Hmd2WriteTwelveHalvesThenInvalidHandle bytes after the failed initialization"); + Require(Hmd2SlotUpTo3CheckSecondThirdThenZero(9, nullptr, out) == Hmd2::ParameterNull && Hmd2SlotUpTo3CheckSecondThirdThenZero(0, out, nullptr) == Hmd2::ParameterNull, "Hmd2SlotUpTo3CheckSecondThirdThenZero null after the failed initialization"); + Require(Hmd2SlotUpTo3CheckSecondThirdThenZero(3, out, out) == 0 && Hmd2SlotUpTo3CheckSecondThirdThenZero(4, out, out) == Hmd2::ParameterInvalid && Hmd2SlotUpTo3CheckSecondThirdThenZero(~0ull, out, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo3CheckSecondThirdThenZero slots after the failed initialization"); + Require(Hmd2SlotUpTo1ThenError1002(1) == Hmd2::Error1002 && Hmd2SlotUpTo1ThenError1002(2) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1ThenError1002 after the failed initialization"); + Require(Hmd2SlotUpTo1CheckSecondThenError1002(5, nullptr) == Hmd2::ParameterNull && Hmd2SlotUpTo1CheckSecondThenError1002(0, out) == Hmd2::Error1002 && Hmd2SlotUpTo1CheckSecondThenError1002(2, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1CheckSecondThenError1002 after the failed initialization"); + Require(Hmd2SlotUpTo1ThenMinusOne(1) == -1 && Hmd2SlotUpTo1ThenMinusOne(2) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1ThenMinusOne after the failed initialization"); + Require(Hmd2SlotUpTo2ThenMinusOne(2) == -1 && Hmd2SlotUpTo2ThenMinusOne(3) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenMinusOne after the failed initialization"); + Require(Hmd2SlotUpTo2ThenError1002(2) == Hmd2::Error1002 && Hmd2SlotUpTo2ThenError1002(3) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenError1002 after the failed initialization"); + Require(Hmd2SlotUpTo2ThenError1003(0) == Hmd2::Error1003 && Hmd2SlotUpTo2ThenError1003(16) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenError1003 after the failed initialization"); + Require(Hmd2SlotUpTo2CheckSecondThenError002C(8, nullptr) == Hmd2::ParameterNull && Hmd2SlotUpTo2CheckSecondThenError002C(2, out) == Hmd2::Error002C && Hmd2SlotUpTo2CheckSecondThenError002C(3, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2CheckSecondThenError002C after the failed initialization"); + Require(Hmd2IndexZeroCheckSecondThenZero(1, nullptr) == Hmd2::ParameterNull && Hmd2IndexZeroCheckSecondThenZero(0, out) == 0 && Hmd2IndexZeroCheckSecondThenZero(1, out) == Hmd2::Error100F, "Hmd2IndexZeroCheckSecondThenZero after the failed initialization"); + } + const std::uint8_t dummyBytes[16]{}; + const void* dummy = dummyBytes; + Require(Hmd2CheckFirstThenError0004(dummy) == Hmd2::Error0004, "Hmd2CheckFirstThenError0004 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError0004(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError0004 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenZero(dummy) == 0, "Hmd2CheckFirstThenZero with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenZero(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenZero with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenError1002(dummy) == Hmd2::Error1002, "Hmd2CheckFirstThenError1002 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError1002(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1002 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenMinusOne(dummy) == -1, "Hmd2CheckFirstThenMinusOne with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenMinusOne(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenMinusOne with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(dummy, dummy) == Hmd2::ParameterInvalid, "Hmd2CheckFirstTwoThenParameterInvalid with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenParameterInvalid with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenParameterInvalid with pointer 1 null after the failed initialization"); + Require(Hmd2CheckSecondThenError002C(dummy, dummy) == Hmd2::Error002C, "Hmd2CheckSecondThenError002C with every pointer present after the failed initialization"); + Require(Hmd2CheckSecondThenError002C(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenError002C with pointer 1 null after the failed initialization"); + Require(Hmd2CheckFirstThenParameterInvalid(dummy) == Hmd2::ParameterInvalid, "Hmd2CheckFirstThenParameterInvalid with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenParameterInvalid(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenParameterInvalid with pointer 0 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, dummy, dummy) == 0, "Hmd2CheckAllSixThenZero with every pointer present after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(nullptr, dummy, dummy, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 0 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, nullptr, dummy, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 1 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, nullptr, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 2 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 3 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 4 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 5 null after the failed initialization"); + Require(Hmd2CheckSecondThenParameterInvalid(dummy, dummy) == Hmd2::ParameterInvalid, "Hmd2CheckSecondThenParameterInvalid with every pointer present after the failed initialization"); + Require(Hmd2CheckSecondThenParameterInvalid(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenParameterInvalid with pointer 1 null after the failed initialization"); + Require(Hmd2CheckFirstThenError800d0203(dummy) == Hmd2::Error800d0203, "Hmd2CheckFirstThenError800d0203 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError800d0203(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError800d0203 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenError1003(dummy) == Hmd2::Error1003, "Hmd2CheckFirstThenError1003 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError1003(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1003 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenError1010(dummy) == Hmd2::Error1010, "Hmd2CheckFirstThenError1010 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError1010(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1010 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1002(dummy, dummy) == Hmd2::Error1002, "Hmd2CheckFirstTwoThenError1002 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1002(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1002 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1002(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1002 with pointer 1 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1003(dummy, dummy) == Hmd2::Error1003, "Hmd2CheckFirstTwoThenError1003 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1003(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1003 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1003(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1003 with pointer 1 null after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(dummy, dummy, dummy) == 0, "Hmd2CheckFirstAndThirdThenZero with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenZero with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenZero with pointer 2 null after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(dummy, dummy, dummy) == Hmd2::Error1003, "Hmd2CheckFirstAndThirdThenError1003 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenError1003 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenError1003 with pointer 2 null after the failed initialization"); + Require(Hmd2CheckSecondThenInvalidHandle(dummy, dummy) == Hmd2::InvalidHandle, "Hmd2CheckSecondThenInvalidHandle with every pointer present after the failed initialization"); + Require(Hmd2CheckSecondThenInvalidHandle(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenInvalidHandle with pointer 1 null after the failed initialization"); + Require(Hmd2CheckSecondThenError1002(dummy, dummy) == Hmd2::Error1002, "Hmd2CheckSecondThenError1002 with every pointer present after the failed initialization"); + Require(Hmd2CheckSecondThenError1002(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenError1002 with pointer 1 null after the failed initialization"); + Require(Hmd2BeforeInitializeZero() == 0, "Hmd2BeforeInitializeZero after the failed initialization"); + Require(Hmd2BeforeInitializeNotInitialized() == Hmd2::NotInitialized, "Hmd2BeforeInitializeNotInitialized after the failed initialization"); + Require(Hmd2BeforeInitializeError0004() == Hmd2::Error0004, "Hmd2BeforeInitializeError0004 after the failed initialization"); + Require(Hmd2BeforeInitializeParameterNull() == Hmd2::ParameterNull, "Hmd2BeforeInitializeParameterNull after the failed initialization"); + Require(Hmd2BeforeInitializeParameterInvalid() == Hmd2::ParameterInvalid, "Hmd2BeforeInitializeParameterInvalid after the failed initialization"); + Require(Hmd2BeforeInitializeReprojectionNotInitialized() == Hmd2::ReprojectionNotInitialized, "Hmd2BeforeInitializeReprojectionNotInitialized after the failed initialization"); + Require(Hmd2BeforeInitializeError002C() == Hmd2::Error002C, "Hmd2BeforeInitializeError002C after the failed initialization"); + Require(Hmd2BeforeInitializeError1002() == Hmd2::Error1002, "Hmd2BeforeInitializeError1002 after the failed initialization"); + Require(Hmd2BeforeInitializeError1003() == Hmd2::Error1003, "Hmd2BeforeInitializeError1003 after the failed initialization"); + Require(Hmd2BeforeInitializeMinusOne() == -1, "Hmd2BeforeInitializeMinusOne after the failed initialization"); + Require(Hmd2BeforeInitializeValue0400() == 0x400, "Hmd2BeforeInitializeValue0400 after the failed initialization"); + Require(Hmd2BeforeInitializeValue20000() == 0x20000, "Hmd2BeforeInitializeValue20000 after the failed initialization"); +} + +} + +void SizeQueriesReturnTheConsoleSizes() { + const auto display = sceHmd2ReprojectionQueryDisplayBufferSizeAlign(); + Require(display.size == 0x6000000 && display.align == 0x10000, "display buffer size"); + const auto seeThrough = sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign(); + Require(seeThrough.size == 0x1106000 && seeThrough.align == 0x100, "see-through buffer size"); + const auto mirroring = sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign(); + Require(mirroring.size == 0x5FA0000 && mirroring.align == 0x10000, "mirroring display buffer size"); + const auto work = sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign(); + Require(work.size == 0x4000 && work.align == 0x100, "mirroring work memory size"); } int main() { - 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"); + SizeQueriesReturnTheConsoleSizes(); + InstallProcessParameters(); + TestBeforeInitialize(); + TestWithoutHeadset(); bool threw = false; try { - sceHmd2Open(); + static_cast(sceHmd2ReprojectionQueryBufferSizeAlign()); } catch (const std::runtime_error&) { threw = true; } - Require(threw, "open after failed initialization did not throw"); + Require(threw, "buffer size query did not throw"); + Require(sceHmd2GazeActivateGazeTracking(0) == 0, "gaze activation before initialization"); + Require(sceHmd2ReprojectionGetCurrentOutput(nullptr) == Hmd2::ReprojectionNotInitialized, "reprojection before initialization"); + Require(sceHmd2SetFieldOfView(nullptr) == Hmd2::ParameterNull, "null field of view"); std::puts("HMD2 tests passed"); return 0; } diff --git a/core/libs/tests/GuestHmd2Gaze.cpp b/core/libs/tests/GuestHmd2Gaze.cpp new file mode 100644 index 0000000000..e3cce92206 --- /dev/null +++ b/core/libs/tests/GuestHmd2Gaze.cpp @@ -0,0 +1,106 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +struct GazeParam { + std::uint32_t size; + std::int32_t handle; + std::uint64_t reserved; +}; + +extern "C" { +std::int32_t APS5_VABI sceHmd2Initialize(const void* param); +std::int32_t APS5_VABI sceHmd2GazeGetResult(const GazeParam* param, void* result); +std::int32_t APS5_VABI sceHmd2GazeGetResultForFoveatedRendering(const GazeParam* param, void* result); +} + +namespace { + +constexpr std::int32_t InvalidHandle = static_cast(0x8a720003); +constexpr std::int32_t ParameterNull = static_cast(0x8a720008); +constexpr std::int32_t ParameterInvalid = static_cast(0x8a720009); + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "FAIL: %s\n", what); + std::abort(); +} + +template +T Read(const unsigned char* bytes, std::size_t offset) { + T value; + std::memcpy(&value, bytes + offset, sizeof(value)); + return value; +} + +bool NegativeZero(float value) { + return value == 0.0f && std::signbit(value); +} + +void CheckGazeResult() { + GazeParam param {sizeof(GazeParam), 0x0e010000, 0}; + unsigned char result[0x38]; + std::memset(result, 0x5a, sizeof(result)); + const std::uint32_t size = 0x38; + std::memcpy(result, &size, sizeof(size)); + + Require(sceHmd2GazeGetResult(nullptr, result) == ParameterNull, "null param"); + Require(sceHmd2GazeGetResult(¶m, nullptr) == ParameterNull, "null result"); + GazeParam wrongParam = param; + wrongParam.size = 0x18; + Require(sceHmd2GazeGetResult(&wrongParam, result) == ParameterInvalid, "param size"); + const std::uint32_t wrongSize = 0x18; + std::memcpy(result, &wrongSize, sizeof(wrongSize)); + Require(sceHmd2GazeGetResult(¶m, result) == ParameterInvalid, "result size"); + Require(Read(result, 4) == 0x5a5a5a5a, "result untouched after a size error"); + + std::memcpy(result, &size, sizeof(size)); + Require(sceHmd2GazeGetResult(¶m, result) == InvalidHandle, "unopened handle"); + Require(Read(result, 0) == 0x38, "size kept"); + Require(Read(result, 4) == -1, "+4 is -1"); + for (const std::size_t offset : {0x8u, 0xcu, 0x10u, 0x20u, 0x24u, 0x28u, 0x2cu, 0x30u, 0x34u}) + Require(Read(result, offset) == 0, "cleared word"); + Require(NegativeZero(Read(result, 0x14)), "+0x14 is -0.0"); + Require(Read(result, 0x18) == 0, "+0x18 is 0.0"); + Require(Read(result, 0x1c) == -1.0f, "+0x1c is -1.0"); + + GazeParam zero {sizeof(GazeParam), 0, 0}; + Require(sceHmd2GazeGetResult(&zero, result) == InvalidHandle, "handle 0"); +} + +void CheckFoveatedResult() { + GazeParam param {sizeof(GazeParam), 0x0e010000, 0}; + unsigned char result[0x18]; + std::memset(result, 0x5a, sizeof(result)); + const std::uint32_t size = 0x18; + std::memcpy(result, &size, sizeof(size)); + + Require(sceHmd2GazeGetResultForFoveatedRendering(nullptr, result) == ParameterNull, "foveated null param"); + Require(sceHmd2GazeGetResultForFoveatedRendering(¶m, nullptr) == ParameterNull, "foveated null result"); + unsigned char large[0x38] = {}; + const std::uint32_t largeSize = 0x38; + std::memcpy(large, &largeSize, sizeof(largeSize)); + Require(sceHmd2GazeGetResultForFoveatedRendering(¶m, large) == ParameterInvalid, "foveated result size"); + + Require(sceHmd2GazeGetResultForFoveatedRendering(¶m, result) == InvalidHandle, "foveated unopened handle"); + Require(Read(result, 0) == 0x18, "foveated size kept"); + Require(Read(result, 4) == -1, "foveated +4 is -1"); + Require(NegativeZero(Read(result, 8)), "foveated +8 is -0.0"); + Require(Read(result, 0xc) == 0, "foveated +0xc is 0.0"); + Require(Read(result, 0x10) == -1.0f, "foveated +0x10 is -1.0"); + Require(Read(result, 0x14) == 0x5a5a5a5a, "foveated +0x14 untouched"); +} + +} + +int main() { + Require(sceHmd2Initialize(nullptr) != 0, "initialize reports no headset"); + CheckGazeResult(); + CheckFoveatedResult(); + std::puts("guest Hmd2 gaze results without a headset: PASS"); + return 0; +} diff --git a/core/libs/tests/GuestHmdSetupDialog.cpp b/core/libs/tests/GuestHmdSetupDialog.cpp new file mode 100644 index 0000000000..7939b5eb8f --- /dev/null +++ b/core/libs/tests/GuestHmdSetupDialog.cpp @@ -0,0 +1,72 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceHmdSetupDialogInitialize(void); +int APS5_VABI sceHmdSetupDialogOpen(const void* param); +int APS5_VABI sceHmdSetupDialogUpdateStatus(void); +int APS5_VABI sceHmdSetupDialogGetStatus(void); +int APS5_VABI sceHmdSetupDialogGetResult(void* result); +int APS5_VABI sceHmdSetupDialogClose(void); +int APS5_VABI sceHmdSetupDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + std::int64_t param = 0; + std::int32_t result[2] = {-1, -1}; + + Require(sceHmdSetupDialogTerminate() == kErrNotInitialized); + Require(sceHmdSetupDialogOpen(¶m) == kErrNotInitialized); + Require(sceHmdSetupDialogClose() == kErrNotInitialized); + Require(sceHmdSetupDialogGetResult(result) == kErrNotInitialized); + Require(sceHmdSetupDialogGetStatus() == kStatusNone); + Require(sceHmdSetupDialogUpdateStatus() == kStatusNone); + + Require(sceHmdSetupDialogInitialize() == 0); + Require(sceHmdSetupDialogInitialize() == kErrAlreadyInitialized); + Require(sceHmdSetupDialogGetStatus() == kStatusInitialized); + Require(sceHmdSetupDialogUpdateStatus() == kStatusInitialized); + + Require(sceHmdSetupDialogOpen(nullptr) == kErrArgNull); + Require(sceHmdSetupDialogGetResult(result) == kErrNotFinished); + + Require(sceHmdSetupDialogOpen(¶m) == 0); + Require(sceHmdSetupDialogGetStatus() == kStatusFinished); + Require(sceHmdSetupDialogUpdateStatus() == kStatusFinished); + + Require(sceHmdSetupDialogGetResult(nullptr) == kErrArgNull); + Require(sceHmdSetupDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(sceHmdSetupDialogClose() == 0); + Require(sceHmdSetupDialogTerminate() == 0); + Require(sceHmdSetupDialogTerminate() == kErrNotInitialized); + Require(sceHmdSetupDialogGetStatus() == kStatusNone); + Require(sceHmdSetupDialogUpdateStatus() == kStatusNone); + + Require(sceHmdSetupDialogInitialize() == 0); + Require(sceHmdSetupDialogUpdateStatus() == kStatusInitialized); + Require(sceHmdSetupDialogClose() == 0); + Require(sceHmdSetupDialogUpdateStatus() == kStatusFinished); + result[0] = -1; + Require(sceHmdSetupDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(sceHmdSetupDialogTerminate() == 0); +} diff --git a/core/libs/tests/GuestHostStandardError.cpp b/core/libs/tests/GuestHostStandardError.cpp index 72e2ea7025..4c3fdc2309 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*); @@ -20,11 +23,23 @@ int APS5_VABI fflush_nid_postfix(FileStream*); int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI dup2_nid_postfix(int, int); std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::size_t); +int APS5_VABI printf_nid_postfix(const char*, ...); +int APS5_VABI puts_nid_postfix(const char*); +int APS5_VABI fprintf_nid_postfix(FileStream*, const char*, ...); +void APS5_VABI perror_nid_postfix(const char*); +int* APS5_VABI __error_nid_postfix(); extern FileStream* __stderrp_nid_postfix; +extern FileStream* __stdoutp_nid_postfix; } static void Require(bool value) { if (!value) std::abort(); } +static void SkipCrashHandler() { +#ifdef __linux__ + Require(::prctl(PR_SET_DUMPABLE, 0) == 0); +#endif +} + enum class Redirect { Duplicate, Reopen }; struct Outcome { @@ -56,7 +71,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); } @@ -89,5 +107,68 @@ int main() { if (redirect == Redirect::Reopen) Require(Contents(probe).find("report kept") == std::string::npos); } std::filesystem::remove(probe); + + int host[2] = {-1, -1}; + Require(::pipe(host) == 0); + const pid_t child = ::fork(); + Require(child >= 0); + if (child == 0) { + ::close(host[0]); + if (::dup2(host[1], 1) != 1) ::_exit(70); + ::close(host[1]); + int guest[2] = {-1, -1}; + if (pipe_nid_postfix(guest) != 0 || dup2_nid_postfix(guest[1], 1) != 1) ::_exit(72); + std::fputs("host\n", stdout); + std::fflush(stdout); + ::_exit(0); + } + ::close(host[1]); + std::string output; + char buffer[64]; + for (ssize_t count; (count = ::read(host[0], buffer, sizeof(buffer))) > 0;) output.append(buffer, static_cast(count)); + ::close(host[0]); + int status = 0; + Require(::waitpid(child, &status, 0) == child); + Require(WIFEXITED(status) && WEXITSTATUS(status) == 0); + Require(output == "host\n"); + + for (const int standard : {1, 2}) { + int host[2] = {-1, -1}; + Require(::pipe(host) == 0); + const pid_t child = ::fork(); + Require(child >= 0); + if (child == 0) { + ::close(host[0]); + int guest[2] = {-1, -1}; + if (pipe_nid_postfix(guest) != 0 || dup2_nid_postfix(guest[1], standard) != standard) ::_exit(72); + const char* expected = "guest-fprintf\n"; + if (standard == 1) { + if (printf_nid_postfix("guest-printf\n") < 0 || puts_nid_postfix("guest-puts") < 0 || fflush_nid_postfix(__stdoutp_nid_postfix) != 0) ::_exit(73); + expected = "guest-printf\nguest-puts\n"; + } else { + if (fprintf_nid_postfix(__stderrp_nid_postfix, "guest-fprintf\n") < 0) ::_exit(73); + *__error_nid_postfix() = 2; + perror_nid_postfix("guest-perror"); + if (fflush_nid_postfix(__stderrp_nid_postfix) != 0) ::_exit(73); + expected = "guest-fprintf\nguest-perror: No such file or directory\n"; + } + std::string got; + char buffer[64]; + const auto length = std::char_traits::length(expected); + for (ssize_t count; got.size() < length && (count = ::read(guest[0], buffer, sizeof(buffer))) > 0;) got.append(buffer, static_cast(count)); + if (got != expected) ::_exit(75); + if (::write(host[1], "ok", 2) != 2) ::_exit(76); + ::_exit(0); + } + ::close(host[1]); + std::string marker; + char buffer[8]; + for (ssize_t count; (count = ::read(host[0], buffer, sizeof(buffer))) > 0;) marker.append(buffer, static_cast(count)); + ::close(host[0]); + int status = 0; + Require(::waitpid(child, &status, 0) == child); + Require(WIFEXITED(status) && WEXITSTATUS(status) == 0); + Require(marker == "ok"); + } return 0; } diff --git a/core/libs/tests/GuestHostStandardErrorWindows.cpp b/core/libs/tests/GuestHostStandardErrorWindows.cpp new file mode 100644 index 0000000000..f91c92d09e --- /dev/null +++ b/core/libs/tests/GuestHostStandardErrorWindows.cpp @@ -0,0 +1,105 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/HostStandardStreams.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#include +#endif + +extern "C" { +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI dup2_nid_postfix(int, int); +std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::size_t); +int APS5_VABI printf_nid_postfix(const char*, ...); +int APS5_VABI puts_nid_postfix(const char*); +int APS5_VABI fprintf_nid_postfix(FileStream*, const char*, ...); +int APS5_VABI fflush_nid_postfix(FileStream*); +void APS5_VABI perror_nid_postfix(const char*); +int* APS5_VABI __error_nid_postfix(); +extern FileStream _Stdout_nid_postfix; +extern FileStream _Stderr_nid_postfix; +} + +static void Require(bool value) { if (!value) std::abort(); } + +static DWORD Available(int descriptor, void* buffer, DWORD capacity, DWORD& read) { + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + DWORD available = 0; + Require(::PeekNamedPipe(handle, buffer, capacity, &read, &available, nullptr) != 0); + return available; +} + +static void Split(int standard) { + int capture[2]{-1, -1}; + Require(::_pipe(capture, 65536, _O_BINARY) == 0); + const int original = ::_dup(standard); + Require(original >= 0 && ::_dup2(capture[1], standard) == 0); + Require(std::fputs("host-before\n", standard == 1 ? stdout : stderr) >= 0); + + int guest[2]{-1, -1}; + Require(pipe_nid_postfix(guest) == 0); + Require(dup2_nid_postfix(guest[1], standard) == standard); + std::FILE* const stream = standard == 1 ? stdout : stderr; + + std::string host(8192, 'h'); + host.back() = '\n'; + Require(std::fputs(host.c_str(), stream) >= 0); + Require(std::fflush(stream) == 0); + host.insert(0, "host-before\n"); + Require(write_nid_postfix(standard, "guest", 5) == 5); + + char guestBytes[8]{}; + DWORD guestRead = 0; + Require(Available(guest[0], guestBytes, sizeof(guestBytes), guestRead) == 5); + Require(guestRead == 5 && std::string(guestBytes, 5) == "guest"); + char hostBytes[8]{}; + DWORD hostRead = 0; + Require(Available(capture[0], hostBytes, sizeof(hostBytes), hostRead) == host.size()); + Require(hostRead == sizeof(hostBytes) && std::string(hostBytes, hostRead) == host.substr(0, hostRead)); + + Require(::_dup2(original, standard) == 0); + ::_close(original); + ::_close(capture[0]); + ::_close(capture[1]); + ::_close(guest[0]); + ::_close(guest[1]); +} + +static void GuestText(int standard, const std::string& text) { + int guest[2]{-1, -1}; + Require(pipe_nid_postfix(guest) == 0); + const int original = ::_dup(standard); + Require(original >= 0); + Require(dup2_nid_postfix(guest[1], standard) == standard); + if (standard == 1) { + Require(printf_nid_postfix("guest-printf\n") >= 0); + Require(puts_nid_postfix("guest-puts") >= 0); + Require(fflush_nid_postfix(&_Stdout_nid_postfix) == 0); + } else { + Require(fprintf_nid_postfix(&_Stderr_nid_postfix, "guest-fprintf\n") >= 0); + *__error_nid_postfix() = 2; + perror_nid_postfix("guest-perror"); + Require(fflush_nid_postfix(&_Stderr_nid_postfix) == 0); + } + std::string got(text.size(), '\0'); + DWORD read = 0; + Require(Available(guest[0], got.data(), static_cast(got.size()), read) == text.size()); + Require(read == text.size() && got == text); + Require(::_dup2(original, standard) == 0); + ::_close(original); + ::_close(guest[0]); + ::_close(guest[1]); +} + +int main() { + Split(2); + Split(1); + GuestText(1, "guest-printf\nguest-puts\n"); + GuestText(2, "guest-fprintf\nguest-perror: No such file or directory\n"); + return 0; +} diff --git a/core/libs/tests/GuestHostStreams.cpp b/core/libs/tests/GuestHostStreams.cpp new file mode 100644 index 0000000000..e31c1dad51 --- /dev/null +++ b/core/libs/tests/GuestHostStreams.cpp @@ -0,0 +1,38 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +extern "C" { +int APS5_VABI dup_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI sceKernelClose(int); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +constexpr int GuestEbadf = 9; +constexpr int SceKernelErrorEbadf = static_cast(0x80020009); + +static bool Open(int fd) { + const int duplicate = dup_nid_postfix(fd); + if (duplicate < 0) return false; + Require(close_nid_postfix(duplicate) == 0); + return true; +} + +int main() { + bool open[3]; + for (int fd = 0; fd < 3; ++fd) open[fd] = Open(fd); + for (int fd = 0; fd < 3; ++fd) { + if (!open[fd]) continue; + Require(close_nid_postfix(fd) == -1 && *__error_nid_postfix() == GuestEbadf); + Require(sceKernelClose(fd) == SceKernelErrorEbadf); + Require(Open(fd)); + } + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + for (const int fd : pipe) Require(fd >= 3 || !open[fd]); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); + return 0; +} diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index aa184acce1..9ecb47dd94 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -1,15 +1,20 @@ #include "SceTypes.hpp" +#include "ProcessParametersFixture.hpp" +#include #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include #include +#include #include extern "C" { int APS5_VABI sceHttpUriParse(SceHttpUriElement*, const char*, void*, std::size_t*, std::size_t); +int APS5_VABI sceHttpUriCopy(SceHttpUriElement*, const SceHttpUriElement*, void*, std::size_t, std::size_t*); int APS5_VABI sceHttpUriMerge(char*, const char*, const char*, std::size_t*, std::size_t, std::uint32_t); int APS5_VABI sceHttpSetInflateGZIPEnabled(int, int); int APS5_VABI sceHttpSetRequestStatusCallback(int, void (APS5_VABI *)(int, int, void*), void*); @@ -24,6 +29,8 @@ int APS5_VABI sceHttpWaitRequest(HttpEpollHandle, HttpNBEvent*, int, int); int APS5_VABI sceHttpAbortWaitRequest(HttpEpollHandle); int APS5_VABI sceHttpInit_nid_postfix(int, int, std::uint64_t); int APS5_VABI sceHttpTerm_nid_postfix(int); +int APS5_VABI sceHttpAuthCacheFlush(int); +int APS5_VABI sceHttpCookieFlush(int); int APS5_VABI sceHttpReadData(int, void*, std::size_t); int APS5_VABI sceHttpCreateRequest2(int, const char*, const char*, std::uint64_t); int APS5_VABI sceHttpsEnableOption(int, std::uint32_t); @@ -41,16 +48,398 @@ int APS5_VABI sceHttpSetAuthInfoCallback(int, HttpAuthInfoCallback, void*); int APS5_VABI sceHttpParseStatusLine(const char*, std::size_t, std::int32_t*, std::int32_t*, std::int32_t*, const char**, std::size_t*); } +alignas(16) static std::array processParameters{}; + 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 int AuthInfo(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*) { std::abort(); } +static void CheckUriParseCharacters() { + char pool[256]; + SceHttpUriElement element{}; + Require(sceHttpUriParse(&element, "HtTpS://AZaz09-._/path", pool, nullptr, sizeof(pool)) == 0); + Require(Equal(element.scheme, "HtTpS") && Equal(element.hostname, "AZaz09-._") && element.port == 443); + Require(sceHttpUriParse(&element, "aZ09+.-://host/", pool, nullptr, sizeof(pool)) == 0); + Require(Equal(element.scheme, "aZ09+.-") && element.port == 0); + for (unsigned value = 128; value <= 255; ++value) { + const std::string byte(1, static_cast(value)); + for (const auto& url : {"http://[" + byte + "]/", "http://host:" + byte + "/"}) { + std::size_t required = 0; + Require(sceHttpUriParse(nullptr, url.c_str(), nullptr, &required, 0) == static_cast(0x80433060)); + Require(sceHttpUriParse(&element, url.c_str(), pool, nullptr, sizeof(pool)) == static_cast(0x80433060)); + } + Require(sceHttpUriParse(&element, ("http://" + byte + "/").c_str(), pool, nullptr, sizeof(pool)) == 0); + Require(Equal(element.hostname, "") && element.path == byte + "/"); + Require(sceHttpUriParse(&element, (byte + "://host/").c_str(), pool, nullptr, sizeof(pool)) == 0); + Require(element.opaque && Equal(element.scheme, "") && Equal(element.hostname, "") && element.path == byte + "://host/"); + Require(sceHttpUriParse(&element, ("a" + byte + "://host/").c_str(), pool, nullptr, sizeof(pool)) == 0); + Require(Equal(element.scheme, "") && Equal(element.hostname, "a") && element.path == byte + "://host/"); + } +} + +struct UriParseCase { + const char* url; + int opaque; + std::uint16_t port; + const char* scheme; + const char* username; + const char* password; + const char* hostname; + const char* path; + const char* query; + const char* fragment; + std::size_t pathSpace = 0; +}; + +static void CheckUriParseAsMeasured() { + constexpr UriParseCase cases[] = { + {"http://bad host/", 0, 80, "http", "", "", "bad", " host/", "", ""}, + {"http://ex%41mple.com/", 0, 80, "http", "", "", "ex", "%41mple.com/", "", ""}, + {"http://h\t/", 0, 80, "http", "", "", "h", "\t/", "", ""}, + {"not a url", 1, 0, "", "", "", "not", " a url", "", ""}, + {"example.com/a", 1, 0, "", "", "", "example.com", "/a", "", ""}, + {"mailto:x", 1, 0, "mailto", "", "", "x", "", "", ""}, + {"abc:def", 1, 0, "abc", "", "", "def", "", "", ""}, + {"http:", 1, 80, "http", "", "", "", "", "", ""}, + {"https:h", 1, 443, "https", "", "", "h", "", "", ""}, + {"//host/x", 0, 0, "", "", "", "host", "/x", "", ""}, + {"", 1, 0, "", "", "", "", "", "", ""}, + {"?q", 1, 0, "", "", "", "", "", "?q", ""}, + {"http://host:65536/", 0, 0, "http", "", "", "host", "/", "", ""}, + {"http://h:65535", 0, 65535, "http", "", "", "h", "", "", ""}, + {"http://h:0080/", 0, 80, "http", "", "", "h", "/", "", ""}, + {"http://h:00000/", 0, 0, "http", "", "", "h", "/", "", ""}, + {"http://host:/", 0, 80, "http", "", "", "host", "/", "", ""}, + {"http://[::1]x/", 0, 80, "http", "", "", "::1", "x/", "", ""}, + {"http://[::1]]/", 0, 80, "http", "", "", "::1", "]/", "", ""}, + {"http://[::1]:/", 0, 80, "http", "", "", "::1", "/", "", ""}, + {"http://[]/", 0, 80, "http", "", "", "", "/", "", ""}, + {"http://[.]/", 0, 80, "http", "", "", ".", "/", "", ""}, + {"http://u@h@i/", 0, 80, "http", "u", "", "h", "@i/", "", ""}, + {"http://u s@h/", 0, 80, "http", "", "", "u", " s@h/", "", ""}, + {"http://u:p:q@h/", 0, 80, "http", "u", "p:q", "h", "/", "", ""}, + {"http://u%41@h/", 0, 80, "http", "u%41", "", "h", "/", "", ""}, + {"http://h@/", 0, 80, "http", "h", "", "", "/", "", ""}, + {"http://[::1]@h/", 0, 80, "http", "", "", "::1", "@h/", "", ""}, + {"http://u@[::1]:8/", 0, 8, "http", "u", "", "::1", "/", "", ""}, + {".@h/", 1, 0, "", ".", "", "h", "/", "", ""}, + {"http://.a/", 0, 80, "http", "", "", "", ".a/", "", ""}, + {"http://-a/", 0, 80, "http", "", "", "-a", "/", "", ""}, + {"http://a..b/", 0, 80, "http", "", "", "a..b", "/", "", ""}, + {"a.", 1, 0, "", "", "", "a.", "", "", ""}, + {"http://h/a/..", 0, 80, "http", "", "", "h", "/a/..", "", ""}, + {"http://h/a/.", 0, 80, "http", "", "", "h", "/a/.", "", ""}, + {"http://h/a/b/../../..", 0, 80, "http", "", "", "h", "/..", "", "", 13}, + {"http://h/../../a", 0, 80, "http", "", "", "h", "/a", "", "", 8}, + {"http://h/a//b/../c", 0, 80, "http", "", "", "h", "/a//c", "", "", 10}, + {"http://h/a/.../../b", 0, 80, "http", "", "", "h", "/a/b", "", "", 11}, + {"http://h/./a/../b/", 0, 80, "http", "", "", "h", "/b/", "", "", 10}, + {"./a", 1, 0, "", "", "", "", "./a", "", ""}, + {"a/../b", 1, 0, "", "", "", "a", "/b", "", "", 5}, + {"1x://h/", 1, 0, "", "", "", "1x", "//h/", "", ""}, + {"-x://h/", 0, 0, "-x", "", "", "h", "/", "", ""}, + {"s_x://h/p", 1, 0, "", "", "", "s_x", "//h/p", "", ""}, + {"s:x://h/p", 1, 0, "s", "", "", "x", "//h/p", "", ""}, + {"s@x://h/p", 1, 0, "", "s", "", "x", "//h/p", "", ""}, + {"httpx://h/", 0, 80, "httpx", "", "", "h", "/", "", ""}, + {"httpsx://h/", 0, 443, "httpsx", "", "", "h", "/", "", ""}, + {"HTTPx://h/", 0, 80, "HTTPx", "", "", "h", "/", "", ""}, + {"htt://h/", 0, 0, "htt", "", "", "h", "/", "", ""}, + {"ws://h/", 0, 0, "ws", "", "", "h", "/", "", ""}, + {"https://h:80/", 0, 80, "https", "", "", "h", "/", "", ""}, + {"http://h/p?q#f#g", 0, 80, "http", "", "", "h", "/p", "?q", "#f#g"}, + {"http://h/p#f?q", 0, 80, "http", "", "", "h", "/p", "", "#f?q"}, + {"http://h?", 0, 80, "http", "", "", "h", "", "?", ""}, + }; + char pool[256]; + for (const auto& row : cases) { + SceHttpUriElement element{}; + std::size_t required = 0; + Require(sceHttpUriParse(nullptr, row.url, nullptr, &required, 0) == 0); + const std::size_t size = std::strlen(row.scheme) + std::strlen(row.username) + std::strlen(row.password) + std::strlen(row.hostname) + + (row.pathSpace != 0 ? row.pathSpace : std::strlen(row.path)) + std::strlen(row.query) + std::strlen(row.fragment) + 7; + Require(required == size); + required = 77; + Require(sceHttpUriParse(&element, row.url, pool, &required, size - 1) == static_cast(0x80431022) && required == 77); + Require(sceHttpUriParse(&element, row.url, pool, &required, sizeof(pool)) == 0 && required == size); + Require(element.opaque == row.opaque && element.port == row.port); + Require(Equal(element.scheme, row.scheme) && Equal(element.username, row.username) && Equal(element.password, row.password)); + Require(Equal(element.hostname, row.hostname) && Equal(element.path, row.path)); + Require(Equal(element.query, row.query) && Equal(element.fragment, row.fragment)); + } + for (const char* url : {"http://[::1/", "http://[a b]/", "http://[a%b]/", "http://[a@b]/", "http://host:80a/", "http://h:65537/", "http://h:99999/", + "http://h:000080/", "http://h:80?q", "http://h:80#f", "http://h:80 /", "http://h: 80/", "http://h:+80/", "http://h:-1/", + "http://h:80:81/", "http://[::1]:8a/", "http://a:b/p"}) { + std::size_t required = 77; + Require(sceHttpUriParse(nullptr, url, nullptr, &required, 0) == static_cast(0x80433060) && required == 77); + } +} + +struct UriMergeCase { + const char* base; + const char* relative; + std::size_t sizeOnly; + const char* merged; +}; + +static void CheckUriMergeAsMeasured() { + constexpr UriMergeCase cases[] = { + {"http://user:pw@example.com:8080/a/b/c?q=1#f", "?x", 0x84, "http://user:pw@example.com:8080/a/b/?x"}, + {"http://user:pw@example.com:8080/a/b/c?q=1#f", "#y", 0x84, "http://user:pw@example.com:8080/a/b/#y"}, + {"http://user:pw@example.com:8080/a/b/c?q=1#f", "../../../../d", 0x9a, "http://user:pw@example.com:8080/a/b/../../../../d"}, + {"http://user:pw@example.com:8080/a/b/c?q=1#f", "http:d", 0x8c, "http://user:pw@example.com:8080/a/b/http:d"}, + {"http://user:pw@example.com:8080/a/b/c?q=1#f", "//other.com/x", 0x9a, "//other.com/x"}, + {"http://example.com", "../d", 0x44, "http://example.com/../d"}, + {"http://example.com/a/./b/../c", "d", 0x5f, "http://example.com/a/d"}, + {"not a url", "d", 0x26, "not a url/d"}, + {"example.com/a", "b", 0x32, "example.com/b"}, + {"http://[::1]:80/a/b", "c", 0x3c, "http://::1/a/c"}, + {"http://ex ample.com/a", "c", 0x47, "http://ex ample.com/c"}, + {"http://example.com/a", "//bad host/x", 0x5a, "//bad host/x"}, + {"//host/a/b", "c", 0x27, "//host/a/c"}, + {"/a/b", "c", 0x17, "/a/c"}, + {"a/b", "c", 0x14, "a/c"}, + {"", "c", 0xb, "/c"}, + {"", "", 0x9, "/"}, + {"mailto:x@y", "c", 0x27, "mailto:x@y/c"}, + {"http://h", "?q", 0x22, "http://h/?q"}, + {"https://h:443/a", "c", 0x31, "https://h/c"}, + {"http://h:0/a", "c", 0x2a, "http://h/c"}, + {"http://h:65536/a", "c", 0x32, "http://h/c"}, + {"http://:p@h/a", "c", 0x2d, "http://:p@h/c"}, + {"http://bad host/", "./x", 0x3c, "http://bad host/./x"}, + {"http://h/a", "http://bad host/", 0x44, "http://bad host/"}, + {"http://h/a", "HTTPS://Other.com/", 0x48, "HTTPS://Other.com/"}, + {"http://h/a", "//o", 0x2a, "//o"}, + {"http://h/a", "x:y", 0x2a, "http://h/x:y"}, + {"http://h/a", "d e", 0x2a, "http://h/d e"}, + {"http://h/a", "..", 0x28, "http://h/.."}, + {"http://h/a/./b/../c", "../d", 0x47, "http://h/a/../d"}, + {"http://h/a?q#f", "", 0x30, "http://h/"}, + {"not a url", "//x/y", 0x2e, "//x/y"}, + }; + char merged[512]; + for (const auto& row : cases) { + std::size_t required = 0; + Require(sceHttpUriMerge(nullptr, row.base, row.relative, &required, 0, 0) == 0 && required == row.sizeOnly); + std::memset(merged, 'Z', sizeof(merged)); + required = 77; + Require(sceHttpUriMerge(merged, row.base, row.relative, &required, row.sizeOnly - 1, 0) == static_cast(0x80431022)); + Require(merged[0] == 'Z'); + Require(sceHttpUriMerge(merged, row.base, row.relative, &required, sizeof(merged), 0) == 0 && Equal(merged, row.merged)); + } + for (const char* base : {"http://[::1/a", "http://h:80a/a"}) { + std::size_t required = 77; + Require(sceHttpUriMerge(merged, base, "c", &required, sizeof(merged), 0) == static_cast(0x80433060) && required == 77); + } +} + +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'); + } +} + +struct StatusLineCase { + bool tildeFill; + const char* line; + std::size_t length; + int result; + std::int32_t major; + std::int32_t minor; + std::int32_t code; + long reasonOffset; + std::size_t phraseLength; +}; + +static void CheckStatusLineAsMeasured() { + constexpr int parseInvalidResponse = static_cast(0x80432060); + constexpr StatusLineCase cases[] = { + {false, "HTTP/1.1 200 OK\r\n", 17, 17, 1, 1, 200, 12, 0x3}, + {false, "HTTP/1.0 404 Not Found\n", 23, 23, 1, 0, 404, 12, 0xa}, + {false, "HTTP/1.1 200 OK", 15, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {false, "HTTP/1.1 200\r\n", 14, 14, 1, 1, 200, 12, 0x0}, + {false, "HTTP/1.1 200 \r\n", 15, 15, 1, 1, 200, 12, 0x1}, + {false, "HTTP/2 200 OK\r\n", 15, parseInvalidResponse, 2, 0, -7, -1, 0xabcd}, + {false, "HTTP/10.20 999 X\r\n", 18, 18, 10, 20, 999, 14, 0x2}, + {false, "HTTP/1.1 20 OK\r\n", 16, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {false, "HTTP/1.1 2000 OK\r\n", 18, 18, 1, 1, 200, 12, 0x4}, + {false, "HTTP/1.1 abc OK\r\n", 17, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {false, "http/1.1 200 OK\r\n", 17, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {false, "HTTP/1.1 200 OK\r\n", 18, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {false, "HTTP/1.1\t200 OK\r\n", 17, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {false, "HTTP/1.1 200OK\r\n", 16, 16, 1, 1, 200, 12, 0x2}, + {false, "ICY 200 OK\r\n", 12, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {false, "HTTP/1.1 200 OK\r", 16, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {false, "HTTP/1.1 200 OK\r\nX: y", 21, 17, 1, 1, 200, 12, 0x3}, + {false, "HTTP/1.1 200 OK\r\r\n", 18, 18, 1, 1, 200, 12, 0x4}, + {false, "HTTP/.1 200 OK\r\n", 16, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {false, "HTTP/1. 200 OK\r\n", 16, parseInvalidResponse, 1, 0, -7, -1, 0xabcd}, + {false, "HTTP/4294967297.1 200 OK\n", 25, 25, 1, 1, 200, 21, 0x3}, + {false, "HTTP/1.1 -12 OK\n", 16, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {false, " HTTP/1.1 200 OK\n", 17, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {false, "HTTP/1.1 200 A\nB\n", 17, 15, 1, 1, 200, 12, 0x2}, + {false, "HTTP/1.1 200\n", 13, 13, 1, 1, 200, 12, 0x0}, + {false, "HTTP/1.1 200 OK\n", 15, 16, 1, 1, 200, 12, 0x3}, + {false, "HTTP/1.1 200 OK\n", 12, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {false, "HTTP/1.1 200 OK\n", 11, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {false, "HTTP/1.1 200 OK\n", 8, 16, 1, 1, 200, 12, 0x3}, + {false, "HTTP/1.1 200 OK\n", 7, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {false, "HTTP/1.1 200 OK\n", 0, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 0, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 1, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 2, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 3, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 4, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 5, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 6, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 7, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 8, 16, 1, 1, 200, 12, 0x3}, + {true, "HTTP/1.1 200 OK\n", 9, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 10, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 11, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 12, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 13, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 14, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK\n", 15, 16, 1, 1, 200, 12, 0x3}, + {true, "HTTP/1.1 200 OK\n", 16, 16, 1, 1, 200, 12, 0x3}, + {true, "HTTP/10.20 999 X\n", 0, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 1, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 2, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 3, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 4, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 5, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 6, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 7, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 8, 17, 10, 20, 999, 14, 0x2}, + {true, "HTTP/10.20 999 X\n", 9, 17, 10, 20, 999, 14, 0x2}, + {true, "HTTP/10.20 999 X\n", 10, 17, 10, 20, 999, 14, 0x2}, + {true, "HTTP/10.20 999 X\n", 11, parseInvalidResponse, 10, 20, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 12, parseInvalidResponse, 10, 20, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 13, parseInvalidResponse, 10, 20, -7, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 14, parseInvalidResponse, 10, 20, 999, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 15, parseInvalidResponse, 10, 20, 999, -1, 0xabcd}, + {true, "HTTP/10.20 999 X\n", 16, 17, 10, 20, 999, 14, 0x2}, + {true, "HTTP/10.20 999 X\n", 17, 17, 10, 20, 999, 14, 0x2}, + {true, "HTTP/1.12 200 OK\n", 0, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 1, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 2, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 3, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 4, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 5, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 6, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 7, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 8, 17, 1, 12, 200, 13, 0x3}, + {true, "HTTP/1.12 200 OK\n", 9, 17, 1, 12, 200, 13, 0x3}, + {true, "HTTP/1.12 200 OK\n", 10, parseInvalidResponse, 1, 12, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 11, parseInvalidResponse, 1, 12, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 12, parseInvalidResponse, 1, 12, -7, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 13, parseInvalidResponse, 1, 12, 200, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 14, parseInvalidResponse, 1, 12, 200, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 15, parseInvalidResponse, 1, 12, 200, -1, 0xabcd}, + {true, "HTTP/1.12 200 OK\n", 16, 17, 1, 12, 200, 13, 0x3}, + {true, "HTTP/1.12 200 OK\n", 17, 17, 1, 12, 200, 13, 0x3}, + {true, "HTTP/12.1 200 OK\n", 0, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 1, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 2, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 3, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 4, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 5, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 6, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 7, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 8, 17, 12, 1, 200, 13, 0x3}, + {true, "HTTP/12.1 200 OK\n", 9, 17, 12, 1, 200, 13, 0x3}, + {true, "HTTP/12.1 200 OK\n", 10, parseInvalidResponse, 12, 1, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 11, parseInvalidResponse, 12, 1, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 12, parseInvalidResponse, 12, 1, -7, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 13, parseInvalidResponse, 12, 1, 200, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 14, parseInvalidResponse, 12, 1, 200, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 15, parseInvalidResponse, 12, 1, 200, -1, 0xabcd}, + {true, "HTTP/12.1 200 OK\n", 16, 17, 12, 1, 200, 13, 0x3}, + {true, "HTTP/12.1 200 OK\n", 17, 17, 12, 1, 200, 13, 0x3}, + {true, "HTTP/1.1 200\n", 0, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 1, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 2, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 3, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 4, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 5, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 6, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 7, parseInvalidResponse, 0, 0, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 8, 13, 1, 1, 200, 12, 0x0}, + {true, "HTTP/1.1 200\n", 9, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 10, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 11, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200\n", 12, 13, 1, 1, 200, 12, 0x0}, + {true, "HTTP/1.1 200\n", 13, 13, 1, 1, 200, 12, 0x0}, + {true, "HTTP/1.1 200 OK", 15, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK", 8, 255, 1, 1, 200, 12, 0xf2}, + {true, "HTTP/1.1 200 OK", 9, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK", 12, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {true, "HTTP/1.1 200 OK", 13, parseInvalidResponse, 1, 1, 200, -1, 0xabcd}, + {true, "HTTP/1.1 2", 8, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1 20", 8, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + {true, "HTTP/1.1", 8, parseInvalidResponse, 1, 1, -7, -1, 0xabcd}, + }; + static char buffer[256]; + for (const auto& row : cases) { + std::memset(buffer, row.tildeFill ? '~' : '\0', sizeof(buffer)); + std::memcpy(buffer, row.line, std::strlen(row.line)); + if (row.tildeFill) buffer[sizeof(buffer) - 2] = '\n'; + buffer[sizeof(buffer) - 1] = '\0'; + std::int32_t major = -7; + std::int32_t minor = -7; + std::int32_t code = -7; + const char* reason = nullptr; + std::size_t phraseLength = 0xabcd; + Require(sceHttpParseStatusLine(buffer, row.length, &major, &minor, &code, &reason, &phraseLength) == row.result); + Require(major == row.major && minor == row.minor && code == row.code && phraseLength == row.phraseLength); + Require(row.reasonOffset < 0 ? reason == nullptr : reason == buffer + row.reasonOffset); + } +} + +static int APS5_VABI 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); } int main() { + const std::uint64_t processSize = processParameters.size(); + const std::uint32_t processMagic = 0x4942524f; + std::uint32_t sdkVersion = 0x1000000; + std::memcpy(processParameters.data(), &processSize, sizeof(processSize)); + std::memcpy(processParameters.data() + 8, &processMagic, sizeof(processMagic)); + std::memcpy(processParameters.data() + 16, &sdkVersion, sizeof(sdkVersion)); + Require(ProcessParametersFixture::Install(processParameters.data(), processParameters.size())); + for (const std::uint32_t version : {0u, 0xffffffu, 0x1000000u}) { + std::memcpy(processParameters.data() + 16, &version, sizeof(version)); + for (unsigned value = 1; value < 256; ++value) { + if (value == '%') continue; + const char input[]{static_cast(value), 0}; + char output[]{'x', 'x'}; + std::size_t required = 123; + const bool allowed = version >= 0x1000000 || (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z') || (value >= '0' && value <= '9') || value == '*' || value == '-' || value == '.' || value == '_' || value == '+'; + Require(sceHttpUriUnescape(output, &required, sizeof(output), input) == (allowed ? 0 : static_cast(0x80433060))); + Require(allowed ? output[0] == input[0] && output[1] == 0 && required == 2 : output[0] == 'x' && output[1] == 'x' && required == 123); + } + char output[2]{}; + Require(sceHttpUriUnescape(output, nullptr, sizeof(output), "%7E") == 0 && output[0] == '~'); + } + std::memcpy(processParameters.data() + 16, &sdkVersion, sizeof(sdkVersion)); int callbackCount = 0; const auto request = sceHttpCreateRequest2(1, "GET", "/", 0); Require(sceHttpSetRequestStatusCallback(request, RequestStatusCallback, &callbackCount) == 0); @@ -67,7 +456,7 @@ int main() { const char* url = "https://user:secret@example.com:8443/a/./b/../c?x=1&y=2#top"; std::size_t required = 0; Require(sceHttpUriParse(nullptr, url, nullptr, &required, 0) == 0); - Require(required == 6 + 5 + 7 + 12 + 5 + 9 + 5); + Require(required == 6 + 5 + 7 + 12 + 12 + 9 + 5); char pool[256]; SceHttpUriElement element; @@ -77,6 +466,7 @@ int main() { Require(Equal(element.scheme, "https") && Equal(element.username, "user") && Equal(element.password, "secret")); Require(Equal(element.hostname, "example.com") && element.port == 8443); Require(Equal(element.path, "/a/c") && Equal(element.query, "?x=1&y=2") && Equal(element.fragment, "#top")); + Require(element.path == pool + 30 && element.query == pool + 42 && element.fragment == pool + 51); char built[256]; Require(sceHttpUriBuild(nullptr, &required, 0, &element, buildAll) == 0); @@ -106,7 +496,7 @@ int main() { Require(sceHttpUriParse(&element, "http://[::1]:8080/index.html", pool, nullptr, sizeof(pool)) == 0); Require(Equal(element.hostname, "::1") && element.port == 8080 && Equal(element.path, "/index.html")); Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, buildAll) == 0); - Require(Equal(built, "http://[::1]:8080/index.html")); + Require(Equal(built, "http://::1:8080/index.html")); Require(sceHttpUriParse(&element, "mailto:someone@example.com", pool, nullptr, sizeof(pool)) == 0); Require(element.opaque != 0 && element.port == 0); @@ -123,13 +513,38 @@ int main() { Require(sceHttpUriParse(&element, nullptr, pool, nullptr, sizeof(pool)) == invalidUrl); Require(sceHttpUriParse(nullptr, url, nullptr, nullptr, 0) == invalidValue); - Require(sceHttpUriParse(nullptr, "http://host:65536/", nullptr, &required, 0) == invalidUrl); + Require(sceHttpUriParse(nullptr, "http://host:65537/", nullptr, &required, 0) == invalidUrl); Require(sceHttpUriParse(nullptr, "http://host:80a/", nullptr, &required, 0) == invalidUrl); Require(sceHttpUriParse(nullptr, "http://[::1/", nullptr, &required, 0) == invalidUrl); - Require(sceHttpUriParse(nullptr, "http://bad host/", nullptr, &required, 0) == invalidUrl); + Require(sceHttpUriParse(nullptr, "http://host:80?q", nullptr, &required, 0) == invalidUrl); + CheckUriParseAsMeasured(); Require(sceHttpUriBuild(built, nullptr, sizeof(built), nullptr, buildAll) == invalidUrl); Require(sceHttpUriBuild(nullptr, nullptr, 0, &element, buildAll) == invalidValue); + { + SceHttpUriElement bounded{}; + bounded.opaque = 2; + char scheme[33]; + char hostname[256]; + std::memset(scheme, 's', sizeof(scheme)); + std::memset(hostname, 'h', sizeof(hostname)); + bounded.scheme = scheme; + bounded.hostname = hostname; + char output[512]; + Require(sceHttpUriBuild(output, &required, sizeof(output), &bounded, 3) == 0); + Require(required == 32 + 1 + 2 + 255 + 1); + Require(output[32] == ':' && output[33] == '/' && output[34] == '/'); + Require(output[required - 1] == '\0'); + scheme[0] = '\0'; + Require(sceHttpUriBuild(output, &required, sizeof(output), &bounded, 1) == 0); + Require(required == 4 && Equal(output, "//")); + char prefix[] = "HTTPS-extra"; + bounded.scheme = prefix; + bounded.port = 443; + Require(sceHttpUriBuild(output, &required, sizeof(output), &bounded, 4) == 0); + Require(required == 3 && Equal(output, "//")); + } + char escaped[64]; Require(sceHttpUriEscape(nullptr, &required, 0, "a b/~\xC3\xA9") == 0); Require(required == std::strlen("a%20b%2F~%C3%A9") + 1); @@ -137,31 +552,64 @@ 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); + const std::string previousCtype = std::setlocale(LC_CTYPE, nullptr); + Require(std::setlocale(LC_CTYPE, "C") != nullptr); + CheckUriEscapeBytes(); + CheckUriParseCharacters(); +#ifdef _WIN32 + Require(std::setlocale(LC_CTYPE, ".1252") != nullptr); + CheckUriEscapeBytes(); + CheckUriParseCharacters(); +#else + for (const char* locale : {"en_US.ISO8859-1", "en_US.iso88591", "en_US.UTF-8"}) { + if (!std::setlocale(LC_CTYPE, locale)) continue; + CheckUriEscapeBytes(); + CheckUriParseCharacters(); + } +#endif + Require(std::setlocale(LC_CTYPE, previousCtype.c_str()) != nullptr); constexpr const char* unescapeCases[][2] = { {"", ""}, {"plain+text", "plain+text"}, {"a%20b%2F~%c3%a9", "a b/~\xC3\xA9"}, - {"%41%4a%4F%ff", "AJO\xFF"}, {"%2520", "%20"}, - {"%", "%"}, {"%1", "%1"}, {"%1g%gg%+1%-1", "%1g%gg%+1%-1"} + {"%41%4a%4F%ff", "AJO\xFF"}, {"%2520", "%20"} }; for (const auto& row : unescapeCases) { const std::size_t size = std::strlen(row[1]) + 1; Require(sceHttpUriUnescape(nullptr, &required, 0, row[0]) == 0 && required == size); std::memset(escaped, 'Z', sizeof(escaped)); - Require(sceHttpUriUnescape(escaped, &required, size - 1, row[0]) == outOfMemory && required == size); - for (char c : escaped) Require(c == 'Z'); + required = 123; + Require(sceHttpUriUnescape(escaped, &required, size - 1, row[0]) == outOfMemory && required == 123); + Require(std::memcmp(escaped, row[1], size - 1) == 0 && escaped[size - 1] == 'Z'); Require(sceHttpUriUnescape(escaped, nullptr, size, row[0]) == 0); Require(Equal(escaped, row[1]) && escaped[size] == 'Z'); } + for (const char* malformed : {"%", "%1", "%1g", "%gg", "%+1", "%-1", "% 1", "abc%", "%%41", "%4%41"}) { + std::memset(escaped, 'Z', sizeof(escaped)); + required = 77; + Require(sceHttpUriUnescape(escaped, &required, sizeof(escaped), malformed) == invalidUrl && required == 77 && escaped[0] == 'Z'); + Require(sceHttpUriUnescape(nullptr, &required, 0, malformed) == invalidUrl && required == 77); + } Require(sceHttpUriUnescape(escaped, &required, sizeof(escaped), "a%00b") == 0); Require(required == 4 && std::memcmp(escaped, "a\0b", 4) == 0); std::strcpy(escaped, "%41%2f%2520"); Require(sceHttpUriUnescape(escaped, &required, sizeof(escaped), escaped) == 0); Require(Equal(escaped, "A/%20") && required == 6); - Require(sceHttpUriUnescape(nullptr, nullptr, 0, "valid") == 0); + Require(sceHttpUriUnescape(nullptr, nullptr, 0, "valid") == invalidValue); required = 123; Require(sceHttpUriUnescape(escaped, &required, sizeof(escaped), nullptr) == invalidValue); Require(required == 123 && Equal(escaped, "A/%20")); + for (const char* malformed : {"%", "%1", "%1g", "%gg", "%+1", "%-1", "prefix%4", "prefix%xy"}) { + std::memset(escaped, 'Z', sizeof(escaped)); + required = 123; + Require(sceHttpUriUnescape(escaped, &required, sizeof(escaped), malformed) == static_cast(0x80433060)); + Require(required == 123); + for (char value : escaped) Require(value == 'Z'); + } + std::memset(escaped, 'Z', sizeof(escaped)); + Require(sceHttpUriUnescape(escaped, &required, 2, "a%20b") == outOfMemory); + Require(required == 123 && escaped[0] == 'a' && escaped[1] == ' ' && escaped[2] == 'Z'); + char bytes[256], encodedBytes[766], decodedBytes[256]; for (std::size_t i = 1; i < 256; ++i) bytes[i - 1] = static_cast(i); bytes[255] = '\0'; @@ -216,13 +664,21 @@ int main() { Require(sceHttpUriMerge(merged, base, nullptr, &required, sizeof(merged), 0) == invalidValue); Require(sceHttpUriMerge(merged, base, "./x", &required, sizeof(merged), 1) == invalidValue); Require(sceHttpUriMerge(nullptr, nullptr, nullptr, &required, 0, 1) == invalidValue); - Require(sceHttpUriMerge(merged, "http://bad host/", "./x", &required, sizeof(merged), 0) == invalidUrl); - Require(sceHttpUriMerge(merged, base, "http://bad host/", &required, sizeof(merged), 0) == invalidUrl); + Require(sceHttpUriMerge(merged, "http://[::1/", "./x", &required, sizeof(merged), 0) == invalidUrl); + Require(sceHttpUriMerge(merged, "http://h:80a/a", "c", &required, sizeof(merged), 0) == invalidUrl); Require(required == 123); + CheckUriMergeAsMeasured(); constexpr int beforeInit = static_cast(0x80431001); constexpr int aborted = static_cast(0x80431080); constexpr int invalidId = static_cast(0x80431100); + const auto flushCaches = [](int id, int expected) { + Require(sceHttpAuthCacheFlush(id) == expected); + Require(sceHttpCookieFlush(id) == expected); + }; + flushCaches(0, beforeInit); + flushCaches(-1, beforeInit); + flushCaches(request, beforeInit); HttpEpollHandle epoll = nullptr; HttpNBEvent events[2]{}; Require(sceHttpCreateEpoll(1, &epoll) == beforeInit); @@ -230,6 +686,12 @@ int main() { Require(sceHttpTerm_nid_postfix(1) == beforeInit); const int context = sceHttpInit_nid_postfix(0, 0, 0x10000); Require(context > 0); + flushCaches(context, 0); + flushCaches(context, 0); + flushCaches(0, invalidId); + flushCaches(-1, invalidId); + flushCaches(INT32_MAX, invalidId); + flushCaches(request, invalidId); Require(sceHttpCreateEpoll(-1, &epoll) == invalidId); Require(sceHttpCreateEpoll(context, nullptr) == invalidValue); Require(sceHttpCreateEpoll(context, &epoll) == 0 && epoll != nullptr); @@ -273,12 +735,23 @@ int main() { Require(sceHttpCreateEpoll(context, &epoll) == 0); const int otherContext = sceHttpInit_nid_postfix(0, 0, 0x10000); Require(otherContext > 0 && otherContext != context); + flushCaches(otherContext, 0); Require(sceHttpTerm_nid_postfix(otherContext) == 0); + flushCaches(otherContext, invalidId); + flushCaches(context, 0); Require(sceHttpTerm_nid_postfix(otherContext) == invalidId); Require(sceHttpWaitRequest(epoll, events, 2, 0) == 0); Require(waitWokenBy([&] { Require(sceHttpTerm_nid_postfix(context) == 0); }) == 0); Require(sceHttpWaitRequest(epoll, events, 2, 0) == beforeInit); Require(sceHttpTerm_nid_postfix(context) == beforeInit); + flushCaches(context, beforeInit); + const int newContext = sceHttpInit_nid_postfix(0, 0, 0x10000); + Require(newContext > 0 && newContext != context && newContext != otherContext); + flushCaches(context, invalidId); + flushCaches(otherContext, invalidId); + flushCaches(newContext, 0); + Require(sceHttpTerm_nid_postfix(newContext) == 0); + flushCaches(newContext, beforeInit); char data[16]; Require(sceHttpReadData(1, data, sizeof(data)) == network); @@ -355,8 +828,9 @@ int main() { Require(parse("HTTP/1.1 2x0 OK\n", 16) == parseInvalidResponse); Require(parse("HTTP/1.1 20", 11) == parseInvalidResponse); Require(parse(response, 15) == parseInvalidResponse); - Require(parse(response, 16) == parseInvalidResponse); Require(phrase == nullptr && phraseLength == 0); + Require(parse(response, 16) == 17 && phrase == response + 12 && phraseLength == 3); + CheckStatusLineAsMeasured(); auto sweep = [](const char* src, const char* expected) { char swept[128]; std::memset(swept, 'x', sizeof(swept)); @@ -380,7 +854,117 @@ int main() { Require(sceHttpUriSweepPath(sweptPath, "/a/b/../c", 6) == 0 && Equal(sweptPath, "/a/b/")); Require(sceHttpUriSweepPath(sweptPath, "/ab/c", 3) == 0 && Equal(sweptPath, "/a")); Require(sceHttpUriSweepPath(sweptPath, "/a/./b", 5) == 0 && Equal(sweptPath, "/a/.")); - Require(sceHttpUriSweepPath(sweptPath, "/a/b/../c", 8) == 0 && Equal(sweptPath, "/a/b/..")); - char sweptByte[2] = {'x', 'x'}; - Require(sceHttpUriSweepPath(sweptByte, "/", 1) == 0 && sweptByte[0] == '\0' && sweptByte[1] == 'x'); + Require(sceHttpUriSweepPath(sweptPath, "/a/b/../c", 8) == 0 && Equal(sweptPath, "/a/")); + Require(sceHttpUriSweepPath(sweptPath, "/a/b/../c", 5) == 0 && Equal(sweptPath, "/a/b")); + Require(sweep("/a/b/..", "/a/b/..")); + Require(sweep("/a/b/./.", "/a/b/.")); + char sweptByte[3] = {'x', 'x', 'x'}; + Require(sceHttpUriSweepPath(sweptByte, "/a", 1) == 0 && sweptByte[0] == '/' && sweptByte[1] == '\0' && sweptByte[2] == 'x'); + sweptByte[0] = 'x'; + Require(sceHttpUriSweepPath(sweptByte, "", 1) == 0 && sweptByte[0] == '\0'); + + { + SceHttpUriElement source{}; + char parsedPool[256]; + Require(sceHttpUriParse(&source, "http://user:pw@example.com:8080/a/b?q=1#frag", parsedPool, nullptr, sizeof(parsedPool)) == 0); + SceHttpUriElement copy{}; + char pool[256]; + std::size_t needed = 0; + Require(sceHttpUriCopy(nullptr, &source, nullptr, 0, &needed) == 0 && needed == 5 + 12 + 5 + 5 + 3); + Require(sceHttpUriCopy(©, &source, pool, sizeof(pool), nullptr) == 0); + Require(std::strcmp(copy.scheme, "http") == 0 && std::strcmp(copy.hostname, "example.com") == 0 && std::strcmp(copy.path, "/a/b") == 0); + Require(std::strcmp(copy.username, "user") == 0 && std::strcmp(copy.password, "pw") == 0 && copy.port == 8080); + Require(copy.query == nullptr && copy.fragment == nullptr && copy.opaque == 0); + Require(sceHttpUriCopy(©, &source, pool, needed - 1, nullptr) == static_cast(0x80431022)); + Require(copy.scheme == nullptr && copy.hostname == nullptr && pool[0] == 0); + Require(sceHttpUriCopy(©, &source, pool, needed, &needed) == 0 && needed == 30); + Require(sceHttpUriCopy(©, nullptr, pool, sizeof(pool), nullptr) == static_cast(0x80433060)); + Require(sceHttpUriCopy(nullptr, &source, nullptr, 0, nullptr) == static_cast(0x804311FE)); + SceHttpUriElement empty{}; + Require(sceHttpUriCopy(©, &empty, pool, sizeof(pool), &needed) == 0 && needed == 5 && copy.scheme != nullptr && copy.scheme[0] == 0); + Require(copy.scheme == pool && copy.hostname == pool + 1 && copy.path == pool + 2 && copy.username == pool + 3 && copy.password == pool + 4); + + auto allBytes = [](const void* data, std::size_t size, unsigned char value) { + const auto* bytes = static_cast(data); + for (std::size_t i = 0; i < size; ++i) { + if (bytes[i] != value) return false; + } + return true; + }; + std::memset(pool, '~', sizeof(pool)); + std::memset(©, 0x5a, sizeof(copy)); + Require(sceHttpUriCopy(©, &source, pool, 64, &needed) == 0 && needed == 30); + Require(copy.scheme == pool && copy.hostname == pool + 5 && copy.path == pool + 17 && copy.username == pool + 22 && copy.password == pool + 27); + Require(allBytes(pool + 30, 34, 0) && pool[64] == '~' && copy.query == nullptr && copy.fragment == nullptr); + Require(allBytes(reinterpret_cast(©) + 4, 4, 0) && allBytes(copy.reserved, sizeof(copy.reserved), 0)); + + std::memset(pool, '~', sizeof(pool)); + std::memset(©, 0x5a, sizeof(copy)); + needed = 0xabcd; + Require(sceHttpUriCopy(©, &source, pool, 29, &needed) == static_cast(0x80431022) && needed == 0xabcd); + Require(allBytes(©, sizeof(copy), 0) && allBytes(pool, 29, 0) && pool[29] == '~'); + std::memset(pool, '~', sizeof(pool)); + Require(sceHttpUriCopy(©, &empty, pool, 0, &needed) == static_cast(0x80431022) && pool[0] == '~' && needed == 0xabcd); + Require(sceHttpUriCopy(©, &source, pool, 4, nullptr) == static_cast(0x80431022)); + + std::memset(pool, '~', sizeof(pool)); + Require(sceHttpUriCopy(nullptr, &source, pool, sizeof(pool), &needed) == 0 && needed == 30 && pool[0] == '~'); + Require(sceHttpUriCopy(nullptr, &source, pool, sizeof(pool), nullptr) == static_cast(0x804311FE)); + std::memset(©, 0x5a, sizeof(copy)); + Require(sceHttpUriCopy(©, &source, nullptr, 64, &needed) == 0 && needed == 30 && allBytes(©, sizeof(copy), 0x5a)); + Require(sceHttpUriCopy(©, &source, nullptr, 64, nullptr) == static_cast(0x804311FE) && allBytes(©, sizeof(copy), 0x5a)); + needed = 0xabcd; + Require(sceHttpUriCopy(©, nullptr, pool, sizeof(pool), &needed) == static_cast(0x80433060) && needed == 0xabcd && allBytes(©, sizeof(copy), 0x5a)); + Require(sceHttpUriCopy(nullptr, nullptr, nullptr, 0, nullptr) == static_cast(0x80433060)); + + char scheme[] = "SCH"; + char user[] = "usr"; + char pass[] = "pwd"; + char host[] = "hostname"; + char path[] = "/p/q"; + char query[] = "?query"; + char fragment[] = "#frag"; + char blank[] = ""; + SceHttpUriElement manual{}; + manual.opaque = 7; + manual.scheme = scheme; + manual.username = user; + manual.password = pass; + manual.hostname = host; + manual.path = path; + manual.query = query; + manual.fragment = fragment; + manual.port = 1234; + std::memset(manual.reserved, 0x11, sizeof(manual.reserved)); + Require(sceHttpUriCopy(©, &manual, pool, sizeof(pool), &needed) == 0 && needed == 26 && copy.opaque == 0 && copy.port == 1234); + Require(Equal(copy.scheme, "SCH") && copy.hostname == pool + 4 && Equal(copy.hostname, "hostname") && copy.path == pool + 13 && Equal(copy.path, "/p/q")); + Require(copy.username == pool + 18 && Equal(copy.username, "usr") && copy.password == pool + 22 && Equal(copy.password, "pwd")); + Require(copy.query == nullptr && copy.fragment == nullptr && allBytes(copy.reserved, sizeof(copy.reserved), 0)); + + SceHttpUriElement mixed{}; + mixed.hostname = host; + mixed.username = blank; + mixed.query = query; + mixed.port = 80; + Require(sceHttpUriCopy(©, &mixed, pool, sizeof(pool), &needed) == 0 && needed == 13 && copy.port == 80); + Require(copy.scheme == pool && copy.hostname == pool + 1 && copy.path == pool + 10 && copy.username == pool + 11 && copy.password == pool + 12); + + SceHttpUriElement inPlace = manual; + Require(sceHttpUriCopy(&inPlace, &inPlace, pool, sizeof(pool), &needed) == 0 && needed == 5 && inPlace.port == 0); + Require(inPlace.scheme == pool && inPlace.hostname == pool + 1 && inPlace.password == pool + 4 && inPlace.password[0] == 0); + inPlace = manual; + Require(sceHttpUriCopy(&inPlace, &inPlace, nullptr, 0, &needed) == 0 && needed == 26 && inPlace.scheme == scheme); + + alignas(SceHttpUriElement) char hostPool[256]; + auto* inside = reinterpret_cast(hostPool + 160); + std::memcpy(static_cast(inside), &manual, sizeof(manual)); + Require(sceHttpUriCopy(©, inside, hostPool, sizeof(hostPool), &needed) == 0 && needed == 5 && copy.port == 0 && copy.hostname[0] == 0); + + std::memcpy(hostPool + 200, "HOSTINPOOL", 11); + SceHttpUriElement pooled = manual; + pooled.hostname = hostPool + 200; + Require(sceHttpUriCopy(nullptr, &pooled, hostPool, sizeof(hostPool), &needed) == 0 && needed == 28); + Require(sceHttpUriCopy(©, &pooled, hostPool, sizeof(hostPool), &needed) == 0 && needed == 18 && copy.hostname == hostPool + 4 && copy.hostname[0] == 0); + Require(Equal(copy.path, "/p/q") && copy.path == hostPool + 5 && copy.port == 1234); + } } diff --git a/core/libs/tests/GuestHttp2.cpp b/core/libs/tests/GuestHttp2.cpp index d29c0021e4..102369b0c4 100644 --- a/core/libs/tests/GuestHttp2.cpp +++ b/core/libs/tests/GuestHttp2.cpp @@ -7,7 +7,7 @@ extern "C" { int APS5_VABI sceHttp2Init(int, int, std::size_t, int); int APS5_VABI sceHttp2CreateTemplate(int, const char*, int, int); int APS5_VABI sceHttp2CreateRequestWithURL(int, const char*, const char*, std::uint64_t); -int APS5_VABI sceHttp2CreateCookieBox(int); +int APS5_VABI sceHttp2CreateCookieBox(std::size_t); int APS5_VABI sceHttp2DeleteCookieBox(int); int APS5_VABI sceHttp2SetCookieBox(int, int); int APS5_VABI sceHttp2CookieFlush(int); @@ -40,7 +40,7 @@ struct Http2MemoryPoolStats { extern "C" { int APS5_VABI sceHttp2GetMemoryPoolStats(int, Http2MemoryPoolStats*); int APS5_VABI sceHttp2SetResolveRetry(int, std::int32_t); -int APS5_VABI sceHttp2WebSocketSetPingInterval(); +int APS5_VABI sceHttp2WebSocketSetPingInterval(std::int32_t, std::uint32_t); } static void Require(bool value) { if (!value) std::abort(); } @@ -64,15 +64,20 @@ static bool Throws(TCall call) { } int main() { + Require(sceHttp2CreateCookieBox(0) == static_cast(0x817b121du)); + Require(sceHttp2CreateCookieBox(0xffff) == static_cast(0x817b121du)); + const int independentBox = sceHttp2CreateCookieBox(0x10000); + Require(independentBox > 0); + Require(sceHttp2DeleteCookieBox(independentBox) == 0); const int context = sceHttp2Init(1, 1, 0x10000, 4); Require(context > 0); Http2MemoryPoolStats stats{1, 1, 1, 1}; Require(sceHttp2GetMemoryPoolStats(context, &stats) == 0); Require(stats.pool_size == 0x10000 && stats.max_inuse_size == 0 && stats.current_inuse_size == 0 && stats.reserved == 0); - const int box = sceHttp2CreateCookieBox(context); - const int other = sceHttp2CreateCookieBox(context); + const int box = sceHttp2CreateCookieBox(0x10000); + const int other = sceHttp2CreateCookieBox(0x10000); Require(box > 0 && other > 0 && box != other && box != context); - const int discarded = sceHttp2CreateCookieBox(context); + const int discarded = sceHttp2CreateCookieBox(0x10000); Require(discarded > 0 && discarded != box && discarded != other); Require(sceHttp2DeleteCookieBox(discarded) == 0); Require(sceHttp2DeleteCookieBox(discarded) == InvalidId); @@ -86,7 +91,7 @@ int main() { Require(request > 0); Require(sceHttp2SetCookieBox(request, other) == 0); Require(sceHttp2SetRequestNoContentLength(request) == 0); - Require(Throws([] { sceHttp2WebSocketSetPingInterval(); })); + Require(Throws([] { sceHttp2WebSocketSetPingInterval(0, 1000000); })); Require(sceHttp2CookieFlush(context) == 0); Require(sceHttp2SendRequest(request, nullptr, 0) == static_cast(0x80436063)); @@ -116,7 +121,7 @@ int main() { const int doomedContext = sceHttp2Init(1, 1, 0x2000, 4); Require(doomedContext > 0); - const int doomedContextBox = sceHttp2CreateCookieBox(doomedContext); + const int doomedContextBox = sceHttp2CreateCookieBox(0x10001); const int doomedContextTemplate = sceHttp2CreateTemplate(doomedContext, "agent", 2, 0); Require(doomedContextBox > 0 && doomedContextTemplate > 0); const int doomedContextRequest = sceHttp2CreateRequestWithURL(doomedContextTemplate, "GET", "https://example.com/descendant", 0); @@ -124,7 +129,8 @@ int main() { RequireSslSetters(doomedContext, 0); Require(sceHttp2Term(doomedContext) == 0); RequireSslSetters(doomedContext, InvalidId); - RequireSslSetters(doomedContextBox, InvalidId); + RequireSslSetters(doomedContextBox, 0); + Require(sceHttp2DeleteCookieBox(doomedContextBox) == 0); Require(sceHttp2DeleteCookieBox(doomedContextBox) == InvalidId); RequireSslSetters(doomedContextTemplate, InvalidId); RequireSslSetters(doomedContextRequest, InvalidId); diff --git a/core/libs/tests/GuestHttpOptions.cpp b/core/libs/tests/GuestHttpOptions.cpp new file mode 100644 index 0000000000..5944662205 --- /dev/null +++ b/core/libs/tests/GuestHttpOptions.cpp @@ -0,0 +1,225 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceHttpInit_nid_postfix(int, int, std::uint64_t); +int APS5_VABI sceHttpTerm_nid_postfix(int); +int APS5_VABI sceHttpCreateTemplate(int, const char*, int, int); +int APS5_VABI sceHttpDeleteTemplate(int); +int APS5_VABI sceHttpCreateConnectionWithURL(int, const char*, int); +int APS5_VABI sceHttpDeleteConnection(int); +int APS5_VABI sceHttpCreateRequestWithURL(int, int, const char*, std::uint64_t); +int APS5_VABI sceHttpDeleteRequest(int); +int APS5_VABI sceHttpSetAutoRedirect(int, int); +int APS5_VABI sceHttpGetAutoRedirect(int, int*); +int APS5_VABI sceHttpSetAuthEnabled(int, int); +int APS5_VABI sceHttpGetAuthEnabled(int, int*); +int APS5_VABI sceHttpSetNonblock(int, int); +int APS5_VABI sceHttpGetNonblock(int, int*); +int APS5_VABI sceHttpTrySetNonblock(int, int); +int APS5_VABI sceHttpTryGetNonblock(int, int*); +int APS5_VABI sceHttpSetAcceptEncodingGZIPEnabled(int, int); +int APS5_VABI sceHttpGetAcceptEncodingGZIPEnabled(int, int*); +int APS5_VABI sceHttpSetDefaultAcceptEncodingGZIPEnabled(int, int); +int APS5_VABI sceHttpCreateEpoll(int, HttpEpollHandle*); +int APS5_VABI sceHttpDestroyEpoll(int, HttpEpollHandle); +int APS5_VABI sceHttpSetEpoll(int, HttpEpollHandle, void*); +int APS5_VABI sceHttpUnsetEpoll(int); +int APS5_VABI sceHttpGetEpoll(int, HttpEpollHandle*, void**); +int APS5_VABI sceHttpCacheRedirectedConnectionEnabled(int, int); +int APS5_VABI sceHttpsEnableOptionPrivate(int, std::uint32_t); +int APS5_VABI sceHttpsDisableOptionPrivate(int, std::uint32_t); +int APS5_VABI sceHttpAbortRequestForce(int); +int APS5_VABI sceHttpSetProxy(int, int, int, const char*, std::uint16_t); +int APS5_VABI sceHttpAddRequestHeader(int, const char*, const char*, std::uint32_t); +int APS5_VABI sceHttpRemoveRequestHeader(int, const char*); +} + +namespace { + +constexpr int BeforeInit = static_cast(0x80431001u); +constexpr int InvalidId = static_cast(0x80431100u); +constexpr int InvalidValue = static_cast(0x804311FEu); +constexpr int UnknownId = 0x7fff0000; + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "%s\n", what); + std::abort(); +} + +int Get(int (APS5_VABI *getter)(int, int*), int id) { + int value = -1; + Require(getter(id, &value) == 0, "an option getter failed on a live object"); + return value; +} + +void CheckAutoRedirect(int context) { + const int pattern = sceHttpCreateTemplate(context, "anyps5-test", 2, 0); + Require(Get(sceHttpGetAutoRedirect, pattern) == 1, "a template does not follow redirects by default"); + Require(sceHttpSetAutoRedirect(pattern, 0) == 0, "sceHttpSetAutoRedirect failed on a template"); + Require(Get(sceHttpGetAutoRedirect, pattern) == 0, "sceHttpGetAutoRedirect does not return what was set"); + const int connection = sceHttpCreateConnectionWithURL(pattern, "http://example.com/", 1); + const int request = sceHttpCreateRequestWithURL(connection, 0, "http://example.com/a", 0); + Require(Get(sceHttpGetAutoRedirect, connection) == 0 && Get(sceHttpGetAutoRedirect, request) == 0, "a connection or request did not take its parent's redirect option"); + Require(sceHttpSetAutoRedirect(request, 7) == 0 && Get(sceHttpGetAutoRedirect, request) == 1, "a nonzero value did not enable redirects"); + Require(Get(sceHttpGetAutoRedirect, connection) == 0, "setting a request changed its connection"); + Require(sceHttpSetAutoRedirect(pattern, 1) == 0 && Get(sceHttpGetAutoRedirect, connection) == 0, "an existing connection followed a later template change"); + Require(sceHttpGetAutoRedirect(request, nullptr) == InvalidValue, "a null output was not refused"); + Require(sceHttpGetAutoRedirect(UnknownId, nullptr) == InvalidValue, "the output pointer is not checked before the id"); + int value = 0; + Require(sceHttpGetAutoRedirect(UnknownId, &value) == InvalidId && sceHttpSetAutoRedirect(UnknownId, 1) == InvalidId, "an unknown id was not refused"); + Require(sceHttpDeleteRequest(request) == 0 && sceHttpGetAutoRedirect(request, &value) == InvalidId, "a deleted request still has options"); + Require(sceHttpDeleteConnection(connection) == 0 && sceHttpDeleteTemplate(pattern) == 0, "delete failed"); +} + +void CheckAuthEnabled(int context) { + const int pattern = sceHttpCreateTemplate(context, "anyps5-test", 2, 0); + Require(Get(sceHttpGetAuthEnabled, pattern) == 0, "authentication is enabled by default"); + Require(sceHttpSetAuthEnabled(pattern, 1) == 0 && Get(sceHttpGetAuthEnabled, pattern) == 1, "sceHttpGetAuthEnabled did not return the set value"); + const int connection = sceHttpCreateConnectionWithURL(pattern, "http://example.com/", 1); + const int request = sceHttpCreateRequestWithURL(connection, 0, "http://example.com/a", 0); + Require(Get(sceHttpGetAuthEnabled, connection) == 1 && Get(sceHttpGetAuthEnabled, request) == 1, "a child did not inherit its parent's authentication option"); + Require(sceHttpSetAuthEnabled(request, 0) == 0 && Get(sceHttpGetAuthEnabled, request) == 0, "authentication could not be disabled on a request"); + Require(Get(sceHttpGetAuthEnabled, connection) == 1 && Get(sceHttpGetAuthEnabled, pattern) == 1, "setting a request changed its parent options"); + int value = -1; + Require(sceHttpGetAuthEnabled(request, nullptr) == InvalidValue, "a null authentication output was not refused"); + Require(sceHttpGetAuthEnabled(UnknownId, nullptr) == InvalidValue, "the authentication output pointer is not checked before the id"); + Require(sceHttpGetAuthEnabled(UnknownId, &value) == InvalidId && sceHttpSetAuthEnabled(UnknownId, 1) == InvalidId, "an unknown id was not refused for authentication options"); + Require(sceHttpDeleteRequest(request) == 0 && sceHttpGetAuthEnabled(request, &value) == InvalidId, "a deleted request still has authentication options"); + Require(sceHttpDeleteConnection(connection) == 0 && sceHttpDeleteTemplate(pattern) == 0, "delete failed"); +} + +void CheckNonblock(int context) { + const int pattern = sceHttpCreateTemplate(context, "anyps5-test", 2, 0); + Require(Get(sceHttpGetNonblock, pattern) == 0 && Get(sceHttpTryGetNonblock, pattern) == 0, "a template is nonblocking by default"); + Require(sceHttpSetNonblock(pattern, 1) == 0, "sceHttpSetNonblock failed on a template"); + const int connection = sceHttpCreateConnectionWithURL(pattern, "http://example.com/", 1); + Require(Get(sceHttpGetNonblock, connection) == 1, "a connection did not take its template's nonblocking option"); + Require(sceHttpTrySetNonblock(connection, 0) == 0 && Get(sceHttpTryGetNonblock, connection) == 0 && Get(sceHttpGetNonblock, connection) == 0, "sceHttpTrySetNonblock did not change the option sceHttpGetNonblock reads"); + Require(Get(sceHttpGetNonblock, pattern) == 1, "setting a connection changed its template"); + int value = 0; + Require(sceHttpGetNonblock(connection, nullptr) == InvalidValue && sceHttpTryGetNonblock(connection, nullptr) == InvalidValue, "a null output was not refused"); + Require(sceHttpGetNonblock(UnknownId, &value) == InvalidId && sceHttpTryGetNonblock(UnknownId, &value) == InvalidId, "an unknown id was not refused by the getters"); + Require(sceHttpSetNonblock(UnknownId, 1) == InvalidId && sceHttpTrySetNonblock(UnknownId, 1) == InvalidId, "an unknown id was not refused by the setters"); + Require(sceHttpDeleteConnection(connection) == 0 && sceHttpDeleteTemplate(pattern) == 0, "delete failed"); +} + +void CheckAcceptEncodingGzip(int context) { + const int plain = sceHttpCreateTemplate(context, "anyps5-test", 2, 0); + Require(Get(sceHttpGetAcceptEncodingGZIPEnabled, plain) == 0, "a template asks for gzip by default"); + Require(sceHttpSetDefaultAcceptEncodingGZIPEnabled(context, 1) == 0, "sceHttpSetDefaultAcceptEncodingGZIPEnabled failed"); + const int compressed = sceHttpCreateTemplate(context, "anyps5-test", 2, 0); + Require(Get(sceHttpGetAcceptEncodingGZIPEnabled, compressed) == 1, "a new template did not take its context's gzip default"); + Require(Get(sceHttpGetAcceptEncodingGZIPEnabled, plain) == 0, "the context default changed an existing template"); + const int connection = sceHttpCreateConnectionWithURL(compressed, "http://example.com/", 1); + Require(sceHttpSetAcceptEncodingGZIPEnabled(connection, 0) == 0 && Get(sceHttpGetAcceptEncodingGZIPEnabled, connection) == 0, "sceHttpGetAcceptEncodingGZIPEnabled does not return what was set"); + Require(Get(sceHttpGetAcceptEncodingGZIPEnabled, compressed) == 1, "setting a connection changed its template"); + const int other = sceHttpInit_nid_postfix(1, 1, 0x10000); + const int otherTemplate = sceHttpCreateTemplate(other, "anyps5-test", 2, 0); + Require(Get(sceHttpGetAcceptEncodingGZIPEnabled, otherTemplate) == 0, "one context's gzip default reached another context"); + int value = 0; + Require(sceHttpGetAcceptEncodingGZIPEnabled(connection, nullptr) == InvalidValue, "a null output was not refused"); + Require(sceHttpGetAcceptEncodingGZIPEnabled(UnknownId, &value) == InvalidId && sceHttpSetAcceptEncodingGZIPEnabled(UnknownId, 1) == InvalidId, "an unknown id was not refused"); + Require(sceHttpSetDefaultAcceptEncodingGZIPEnabled(UnknownId, 1) == InvalidId, "an unknown context was not refused"); + Require(sceHttpDeleteTemplate(otherTemplate) == 0 && sceHttpTerm_nid_postfix(other) == 0, "the second context did not close"); + Require(sceHttpSetDefaultAcceptEncodingGZIPEnabled(other, 1) == InvalidId, "a closed context still takes a gzip default"); + Require(sceHttpSetDefaultAcceptEncodingGZIPEnabled(context, 0) == 0, "the gzip default could not be reset"); + Require(sceHttpDeleteConnection(connection) == 0 && sceHttpDeleteTemplate(compressed) == 0 && sceHttpDeleteTemplate(plain) == 0, "delete failed"); +} + +void CheckEpoll(int context) { + HttpEpollHandle epoll = nullptr; + Require(sceHttpCreateEpoll(context, &epoll) == 0 && epoll != nullptr, "sceHttpCreateEpoll failed"); + const int pattern = sceHttpCreateTemplate(context, "anyps5-test", 2, 0); + const int connection = sceHttpCreateConnectionWithURL(pattern, "http://example.com/", 1); + const int request = sceHttpCreateRequestWithURL(connection, 0, "http://example.com/a", 0); + int marker = 0; + HttpEpollHandle bound = epoll; + void* argument = ▮ + Require(sceHttpGetEpoll(request, &bound, &argument) == 0 && bound == nullptr && argument == nullptr, "a new request is bound to an epoll"); + Require(sceHttpSetEpoll(request, epoll, &marker) == 0, "sceHttpSetEpoll failed"); + Require(sceHttpGetEpoll(request, &bound, &argument) == 0 && bound == epoll && argument == &marker, "sceHttpGetEpoll does not return the binding that was set"); + bound = nullptr; + Require(sceHttpGetEpoll(request, &bound, nullptr) == 0 && bound == epoll, "sceHttpGetEpoll needs the argument output"); + Require(sceHttpGetEpoll(connection, &bound, &argument) == 0 && bound == nullptr, "binding a request bound its connection"); + Require(sceHttpUnsetEpoll(request) == 0 && sceHttpGetEpoll(request, &bound, &argument) == 0 && bound == nullptr && argument == nullptr, "sceHttpUnsetEpoll left the binding"); + Require(sceHttpGetEpoll(request, nullptr, &argument) == InvalidValue, "a null epoll output was not refused"); + Require(sceHttpGetEpoll(UnknownId, &bound, &argument) == InvalidId && sceHttpSetEpoll(UnknownId, epoll, nullptr) == InvalidId && sceHttpUnsetEpoll(UnknownId) == InvalidId, "an unknown id was not refused"); + const auto unknownEpoll = reinterpret_cast(std::uintptr_t{0x1234}); + Require(sceHttpSetEpoll(request, nullptr, &marker) == InvalidId && sceHttpSetEpoll(request, unknownEpoll, &marker) == InvalidId, "sceHttpSetEpoll took a handle that is not an epoll"); + Require(sceHttpDestroyEpoll(context, epoll) == 0, "sceHttpDestroyEpoll failed"); + Require(sceHttpSetEpoll(request, epoll, &marker) == InvalidId, "sceHttpSetEpoll took a destroyed epoll"); + Require(sceHttpGetEpoll(request, &bound, &argument) == 0 && bound == nullptr, "a refused sceHttpSetEpoll changed the binding"); + Require(sceHttpDeleteRequest(request) == 0 && sceHttpDeleteConnection(connection) == 0 && sceHttpDeleteTemplate(pattern) == 0, "delete failed"); +} + +void CheckTermEndsOptions() { + const int context = sceHttpInit_nid_postfix(1, 1, 0x10000); + const int pattern = sceHttpCreateTemplate(context, "anyps5-test", 2, 0); + const int connection = sceHttpCreateConnectionWithURL(pattern, "http://example.com/", 1); + Require(sceHttpTerm_nid_postfix(context) == 0, "sceHttpTerm failed"); + int value = -1; + HttpEpollHandle bound = nullptr; + Require(sceHttpGetAutoRedirect(pattern, &value) == BeforeInit && sceHttpSetAutoRedirect(pattern, 0) == BeforeInit, "the redirect option outlived the last sceHttpTerm"); + Require(sceHttpGetAuthEnabled(pattern, &value) == BeforeInit && sceHttpSetAuthEnabled(pattern, 0) == BeforeInit, "the authentication option outlived the last sceHttpTerm"); + Require(sceHttpGetNonblock(connection, &value) == BeforeInit && sceHttpTryGetNonblock(connection, &value) == BeforeInit, "the nonblocking getters work after the last sceHttpTerm"); + Require(sceHttpSetNonblock(connection, 1) == BeforeInit && sceHttpTrySetNonblock(connection, 1) == BeforeInit, "the nonblocking setters work after the last sceHttpTerm"); + Require(sceHttpGetAcceptEncodingGZIPEnabled(pattern, &value) == BeforeInit && sceHttpSetAcceptEncodingGZIPEnabled(pattern, 1) == BeforeInit, "the gzip option outlived the last sceHttpTerm"); + Require(sceHttpGetEpoll(connection, &bound, nullptr) == BeforeInit && sceHttpUnsetEpoll(connection) == BeforeInit, "the epoll binding outlived the last sceHttpTerm"); + Require(sceHttpCacheRedirectedConnectionEnabled(connection, 1) == BeforeInit, "sceHttpCacheRedirectedConnectionEnabled works after the last sceHttpTerm"); + Require(sceHttpSetProxy(connection, 1, 0, "proxy.example", 3128) == BeforeInit, "sceHttpSetProxy works after the last sceHttpTerm"); + Require(sceHttpsEnableOptionPrivate(connection, 0x4000u) == BeforeInit && sceHttpsDisableOptionPrivate(connection, 0x4000u) == BeforeInit, "the private SSL options work after the last sceHttpTerm"); + Require(sceHttpGetNonblock(connection, nullptr) == BeforeInit, "a null output is checked before initialisation"); + Require(value == -1 && bound == nullptr, "a refused getter wrote its output"); + const int next = sceHttpInit_nid_postfix(1, 1, 0x10000); + Require(sceHttpSetAutoRedirect(pattern, 0) == InvalidId && sceHttpGetNonblock(connection, &value) == InvalidId, "an object outlived the last sceHttpTerm into a new sceHttpInit"); + Require(sceHttpTerm_nid_postfix(next) == 0, "sceHttpTerm failed"); +} + +void CheckUnobservedOptions(int context) { + const int pattern = sceHttpCreateTemplate(context, "anyps5-test", 2, 0); + const int connection = sceHttpCreateConnectionWithURL(pattern, "http://example.com/", 1); + const int request = sceHttpCreateRequestWithURL(connection, 0, "http://example.com/a", 0); + Require(sceHttpAddRequestHeader(request, "X-Test", "value", 0) == 0 && sceHttpRemoveRequestHeader(request, "X-Test") == 0, "offline request header calls failed"); + Require(sceHttpRemoveRequestHeader(UnknownId, nullptr) == 0, "offline header removal started validating arguments"); + Require(sceHttpCacheRedirectedConnectionEnabled(pattern, 0) == 0 && sceHttpCacheRedirectedConnectionEnabled(request, 1) == 0, "sceHttpCacheRedirectedConnectionEnabled refused a live object"); + Require(sceHttpCacheRedirectedConnectionEnabled(UnknownId, 1) == InvalidId, "sceHttpCacheRedirectedConnectionEnabled took an unknown id"); + for (const auto option : {sceHttpsEnableOptionPrivate, sceHttpsDisableOptionPrivate}) { + Require(option(connection, 0x2dffu) == 0 && option(connection, 0) == 0, "a private SSL option within the mask was refused"); + Require(option(connection, 0x0100u) == 0 && option(connection, 0x2000u) == 0, "a private-only SSL flag was refused"); + Require(option(connection, 0x0200u) == InvalidValue && option(connection, 0x4000u) == InvalidValue, "an SSL flag outside the private mask was taken"); + Require(option(UnknownId, 0x0200u) == InvalidValue, "the SSL flags are not checked before the id"); + Require(option(UnknownId, 0x01u) == InvalidId, "a private SSL option took an unknown id"); + } + for (const int id : {pattern, connection, request}) { + Require(sceHttpSetProxy(id, 1, 0, "proxy.example", 3128) == 0, "sceHttpSetProxy refused a live object"); + } + Require(sceHttpSetProxy(connection, 0, 0, nullptr, 0) == InvalidValue, "sceHttpSetProxy took a null host"); + Require(sceHttpSetProxy(UnknownId, 0, 0, nullptr, 0) == InvalidValue, "the proxy host is not checked before the id"); + Require(sceHttpSetProxy(UnknownId, 1, 1, "proxy.example", 8080) == InvalidId, "sceHttpSetProxy took an unknown id"); + Require(sceHttpAbortRequestForce(request) == 0, "sceHttpAbortRequestForce failed on a live request"); + Require(sceHttpDeleteRequest(request) == 0 && sceHttpDeleteConnection(connection) == 0 && sceHttpDeleteTemplate(pattern) == 0, "delete failed"); +} + +} + +int main() { + const int context = sceHttpInit_nid_postfix(1, 1, 0x10000); + Require(context > 0, "sceHttpInit failed"); + CheckAutoRedirect(context); + CheckAuthEnabled(context); + CheckNonblock(context); + CheckAcceptEncodingGzip(context); + CheckEpoll(context); + CheckUnobservedOptions(context); + Require(sceHttpTerm_nid_postfix(context) == 0, "sceHttpTerm failed"); + Require(sceHttpSetDefaultAcceptEncodingGZIPEnabled(context, 1) == BeforeInit, "a gzip default was taken before sceHttpInit"); + CheckTermEndsOptions(); + std::puts("HTTP option tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestHttpResponseHeader.cpp b/core/libs/tests/GuestHttpResponseHeader.cpp index e02738dbc7..76bbc60b5d 100644 --- a/core/libs/tests/GuestHttpResponseHeader.cpp +++ b/core/libs/tests/GuestHttpResponseHeader.cpp @@ -55,7 +55,7 @@ static void CheckGuardPage() { constexpr std::string_view text = "X: abc\r\n\tdef\r\n"; char* header = pages + pageSize - text.size(); std::memcpy(header, text.data(), text.size()); - Check({header, text.size()}, "x", 3, "abc\r\n\tdef", static_cast(text.size())); + Check({header, text.size()}, "x", 3, "abc\r\n\tdef\r\n", static_cast(text.size())); Check({header, 6}, "x", 3, "abc", 6); const char* value = header; std::size_t length = 123; @@ -75,17 +75,25 @@ int main() { constexpr int invalidResponse = static_cast(0x80432060); constexpr int invalidValue = static_cast(0x804321FE); constexpr int notFound = static_cast(0x80432025); - Check("HTTP/1.1 200 OK\r\nX: \tvalue \t\r\nNext: no\r\n", "x", 21, "value \t", 30); - Check("X: first\r\nX: second\r\n", "X", 3, "first", 10); - Check("X: abc\n def\n\tghi\nY: no\n", "x", 3, "abc\n def\n\tghi", 17); + Check("HTTP/1.1 200 OK\r\nX: \tvalue \t\r\nNext: no\r\n", "x", 21, "value \t", 28); + Check("X: first\r\nX: second\r\n", "X", 3, "first", 8); + Check("X: abc\n def\n\tghi\nY: no\n", "x", 3, "abc\n def\n\tghi", 16); Check("X: no-newline", "X", 3, "no-newline", 13); Check("X: value\r", "X", 3, "value\r", 9); Check("X:", "X", 2, "", 0); Check("X: \t", "X", 4, "", 0); Check("X:\r\n\r\n", "X", 5, "", 0); - Check("X:\r\nnext\r\n", "X", 4, "next", 10); - Check("Prefix: wrong\n X: wrong\nXy: wrong\nX: right\n", "X", 37, "right", 43); - Check(std::string_view("X: a\0b\n", 7), "X", 3, std::string_view("a\0b", 3), 7); + Check("X:\r\nnext\r\n", "X", 0, "", 0); + Check("X:\n b\r\n", "X", 4, "b\r\n", 7); + Check("X:\r\n\tb\r\nY: c", "X", 5, "b", 6); + Check("Prefix: wrong\n X: wrong\nXy: wrong\nX: right\n", "X", 37, "right\n", 43); + Check("HTTP/1.1 200 OK\r\nServer: x\r\n\r\nBody: no\r\n", "Body", 36, "no\r\n", 40); + Check(" A: b\r\nB: c \r\n", "B", 11, "c \r\n", 16); + Check("Empty:\r\nSpaces: \r\nNoSpace:v\r\nZ: z", "Empty", 0, "", 0); + Check("Empty:\r\nSpaces: \r\nNoSpace:v\r\nZ: z", "Spaces", 0, "", 0); + Check("Empty:\r\nSpaces: \r\nNoSpace:v\r\nZ: z", "NoSpace", 29, "v", 30); + Check(std::string_view("X: a\0b\n", 7), "X", 3, std::string_view("a\0b\n", 4), 7); + Check(std::string_view("X: a\0b\r\nY: z", 13), "X", 3, std::string_view("a\0b", 3), 6); const char* marker = "unchanged"; const char* value = marker; @@ -104,6 +112,6 @@ int main() { std::string field(0xfff, 'A'); std::string header = field + ": value\n"; field += "ignored"; - Check(header, field.c_str(), 0x1000 + 1, "value", static_cast(header.size())); + Check(header, field.c_str(), 0x1000 + 1, "value\n", static_cast(header.size())); CheckGuardPage(); } diff --git a/core/libs/tests/GuestImeDialog.cpp b/core/libs/tests/GuestImeDialog.cpp new file mode 100644 index 0000000000..756a809fd7 --- /dev/null +++ b/core/libs/tests/GuestImeDialog.cpp @@ -0,0 +1,210 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" +#include "prx/libSceVideoOut/include/ImeTextInput.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceImeDialogInit(const Param*, const ExtendedParam*); +int APS5_VABI sceImeDialogGetStatus(void); +int APS5_VABI sceImeDialogGetResult(Result*); +int APS5_VABI sceImeDialogAbort(void); +int APS5_VABI sceImeDialogTerm(void); +int APS5_VABI sceImeDialogForceClose(void); +int APS5_VABI sceImeDialogGetCurrentStarState(std::int64_t); +int APS5_VABI sceImeDialogInitInternal(const Param*, const ExtendedParam*); +int APS5_VABI sceImeDialogSetPanelPosition(std::int32_t, std::int32_t); +} + +namespace { + +constexpr int StatusNone = 0; +constexpr int StatusRunning = 1; +constexpr int StatusFinished = 2; +constexpr std::uint32_t EndStatusOk = 0; +constexpr std::uint32_t EndStatusUserCanceled = 1; +constexpr std::uint32_t EndStatusAborted = 2; +constexpr int ErrorBusy = static_cast(0x80bc0001u); +constexpr int ErrorSuspending = static_cast(0x80bc0009u); +constexpr int ErrorNotOpened = static_cast(0x80bc0002u); +constexpr int ErrorNotInUse = static_cast(0x80bc0107u); +constexpr int ErrorInvalidMaxTextLength = static_cast(0x80bc0016u); +constexpr int ErrorInvalidInputTextBuffer = static_cast(0x80bc0017u); +constexpr int ErrorInvalidAddress = static_cast(0x80bc0031u); + +void Require(bool value) { if (!value) std::abort(); } + +constexpr unsigned WindowId = 7; +ImeTextInput g_window; + +void Type(const char* utf8) { + SDL_Event event{}; + event.type = SDL_TEXTINPUT; + event.text.windowID = WindowId; + std::strncpy(event.text.text, utf8, sizeof(event.text.text) - 1); + g_window.HandleEvent(event, WindowId); +} + +void Press(SDL_Scancode scancode) { + SDL_Event event{}; + event.type = SDL_KEYDOWN; + event.key.windowID = WindowId; + event.key.keysym.scancode = scancode; + g_window.HandleEvent(event, WindowId); +} + +Param DialogParam(char16_t* buffer, std::uint32_t maxLength) { + Param param{}; + param.max_text_length = maxLength; + param.input_text_buffer = buffer; + return param; +} + +std::uint32_t EndStatus() { + Result result{}; + Require(sceImeDialogGetResult(&result) == 0); + return result.endstatus; +} + +void Open(const Param& param) { + Require(sceImeDialogInit(¶m, nullptr) == 0); + Require(sceImeDialogGetStatus() == StatusRunning); + Require(ImeCapturesKeyboard_nid_postfix()); +} + +void Close() { + Require(sceImeDialogTerm() == 0); + Require(sceImeDialogGetStatus() == StatusNone); +} + +} + +int main() { + char16_t buffer[16] = u"Hi"; + const Param param = DialogParam(buffer, 8); + Require(!ImeCapturesKeyboard_nid_postfix()); + Require(sceImeDialogGetStatus() == StatusNone); + Require(sceImeDialogTerm() == ErrorNotOpened); + + Open(param); + for (int poll = 0; poll < 8; ++poll) Require(sceImeDialogGetStatus() == StatusRunning); + Require(sceImeDialogInit(¶m, nullptr) == ErrorBusy); + Result early{}; + Require(sceImeDialogGetResult(&early) == ErrorNotOpened); + Type("\xc3\xa9"); + Type("\xf0\x9f\x98\x80"); + Require(std::u16string(buffer) == u"Hi"); + Press(SDL_SCANCODE_RETURN); + Require(sceImeDialogGetStatus() == StatusFinished); + Require(!ImeCapturesKeyboard_nid_postfix()); + Require(EndStatus() == EndStatusOk); + Require(std::u16string(buffer) == u"Hi\u00e9\U0001F600"); + Require(sceImeDialogAbort() == ErrorNotOpened); + Close(); + Require(sceImeDialogTerm() == ErrorNotOpened); + + buffer[0] = u'\0'; + Open(param); + Type("abc"); + Press(SDL_SCANCODE_HOME); + Press(SDL_SCANCODE_DELETE); + Press(SDL_SCANCODE_END); + Type("\xf0\x9f\x98\x80"); + Press(SDL_SCANCODE_BACKSPACE); + Press(SDL_SCANCODE_LEFT); + Type("X"); + Type("1234"); + Press(SDL_SCANCODE_END); + Type("\xf0\x9f\x98\x80"); + Type("98"); + Type("Z"); + Press(SDL_SCANCODE_RETURN); + Require(EndStatus() == EndStatusOk); + Require(std::u16string(buffer) == u"bX1234c9"); + Close(); + + buffer[0] = u'\0'; + Open(param); + Press(SDL_SCANCODE_RETURN); + Require(EndStatus() == EndStatusOk); + Require(buffer[0] == u'\0'); + Close(); + + std::memcpy(buffer, u"keep", sizeof(u"keep")); + Open(param); + Type("zz"); + Press(SDL_SCANCODE_ESCAPE); + Require(sceImeDialogGetStatus() == StatusFinished); + Require(EndStatus() == EndStatusUserCanceled); + Require(std::u16string(buffer) == u"keep"); + Close(); + + Open(param); + Require(sceImeDialogAbort() == 0); + Require(sceImeDialogGetStatus() == StatusFinished); + Require(!ImeCapturesKeyboard_nid_postfix()); + Require(EndStatus() == EndStatusAborted); + Type("ignored"); + Press(SDL_SCANCODE_RETURN); + Require(std::u16string(buffer) == u"keep"); + Close(); + + Require(sceImeDialogForceClose() == ErrorNotInUse); + Open(param); + Type("zz"); + Require(sceImeDialogForceClose() == 0); + Require(sceImeDialogGetStatus() == StatusNone); + Require(!ImeCapturesKeyboard_nid_postfix()); + Type("ignored"); + Press(SDL_SCANCODE_RETURN); + Require(std::u16string(buffer) == u"keep"); + Result closed{}; + Require(sceImeDialogGetResult(&closed) == ErrorNotOpened); + Require(sceImeDialogTerm() == ErrorNotOpened); + Require(sceImeDialogForceClose() == ErrorNotInUse); + + Open(param); + Type("!"); + Press(SDL_SCANCODE_RETURN); + Require(sceImeDialogGetStatus() == StatusFinished); + Require(sceImeDialogForceClose() == 0); + Require(sceImeDialogGetStatus() == StatusNone); + Require(std::u16string(buffer) == u"keep!"); + + Open(param); + Require(sceImeDialogAbort() == 0); + Require(sceImeDialogForceClose() == 0); + Require(sceImeDialogGetStatus() == StatusNone); + Open(param); + Close(); + + Require(sceImeDialogInit(nullptr, nullptr) == ErrorInvalidAddress); + const Param empty = DialogParam(buffer, 0); + Require(sceImeDialogInit(&empty, nullptr) == ErrorInvalidMaxTextLength); + const Param tooLong = DialogParam(buffer, 2049); + Require(sceImeDialogInit(&tooLong, nullptr) == ErrorInvalidMaxTextLength); + const Param noBuffer = DialogParam(nullptr, 8); + Require(sceImeDialogInit(&noBuffer, nullptr) == ErrorInvalidInputTextBuffer); + Require(sceImeDialogGetStatus() == StatusNone); + Require(!ImeCapturesKeyboard_nid_postfix()); + + std::uint32_t star = 0xa5a5a5a5u; + const auto starAddress = static_cast(reinterpret_cast(&star)); + Require(sceImeDialogGetCurrentStarState(starAddress) == ErrorNotInUse && star == 0xa5a5a5a5u); + Require(sceImeDialogSetPanelPosition(640, 360) == ErrorNotInUse); + Require(sceImeDialogInitInternal(nullptr, nullptr) == ErrorInvalidAddress); + Require(sceImeDialogInitInternal(&empty, nullptr) == ErrorInvalidMaxTextLength); + Require(sceImeDialogGetStatus() == StatusNone); + Require(sceImeDialogInitInternal(¶m, nullptr) == 0); + Require(sceImeDialogGetStatus() == StatusRunning && ImeCapturesKeyboard_nid_postfix()); + Require(sceImeDialogInitInternal(¶m, nullptr) == ErrorBusy); + Require(sceImeDialogGetCurrentStarState(0) == ErrorInvalidAddress); + Require(sceImeDialogGetCurrentStarState(starAddress) == 0 && star == 0); + Require(sceImeDialogSetPanelPosition(640, 360) == ErrorSuspending); + Require(sceImeDialogAbort() == 0); + Require(sceImeDialogSetPanelPosition(0, 0) == ErrorSuspending); + Close(); +} diff --git a/core/libs/tests/GuestImeFilterText.cpp b/core/libs/tests/GuestImeFilterText.cpp new file mode 100644 index 0000000000..bac9e2daa2 --- /dev/null +++ b/core/libs/tests/GuestImeFilterText.cpp @@ -0,0 +1,108 @@ +#include "SceTypes.hpp" +#include "prx/libSceIme/include/ImeFilterText.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceImeFilterText(char16_t* dst, int32_t* out_length, uint32_t capacity, + const ime_filter::FilterParam* param, const char16_t* src, uint32_t src_length); +} + +namespace { + +constexpr int kInvalidParam = static_cast(0x80bc0030u); +constexpr int kInvalidAddress = static_cast(0x80bc0031u); + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +struct Result { + int code; + std::vector text; +}; + +Result Run(uint64_t mask, uint32_t type, uint32_t capacity, std::initializer_list input, + uint32_t length = UINT32_MAX) { + std::vector src(input); + src.push_back(0); + if (length == UINT32_MAX) length = static_cast(input.size()); + std::vector dst(capacity + 4, 0xcccc); + int32_t count = -1; + const ime_filter::FilterParam param{mask, type, 0, 0}; + const int code = sceImeFilterText(dst.data(), &count, capacity, ¶m, src.data(), length); + Result result{code, {}}; + if (code == 0) { + Require(count >= 0 && static_cast(count) < capacity, "length must leave room for the terminator"); + Require(dst[static_cast(count)] == 0, "output must be terminated"); + result.text.assign(dst.begin(), dst.begin() + count); + } + return result; +} + +void Expect(const Result& result, std::initializer_list expected, const char* message) { + Require(result.code == 0, message); + Require(result.text == std::vector(expected), message); +} + +void CheckArguments() { + char16_t dst[4]; + int32_t count = 0; + const char16_t src[2] = {u'a', 0}; + const ime_filter::FilterParam param{0, 1, 0, 0}; + Require(sceImeFilterText(nullptr, &count, 4, ¶m, src, 1) == kInvalidAddress, "null destination"); + Require(sceImeFilterText(dst, nullptr, 4, ¶m, src, 1) == kInvalidAddress, "null length"); + Require(sceImeFilterText(dst, &count, 4, nullptr, src, 1) == kInvalidAddress, "null parameter"); + Require(sceImeFilterText(dst, &count, 4, ¶m, nullptr, 1) == kInvalidAddress, "null source"); + Require(Run(0, 1, 0, {u'a'}).code == kInvalidParam, "capacity 0"); + Require(Run(0, 1, 1, {u'a'}).code == kInvalidParam, "capacity below 2"); + Require(Run(0, 5, 16, {u'a'}).code == kInvalidParam, "type 5"); + Require(Run(0, 0xff, 16, {u'a'}).code == kInvalidParam, "type 0xff"); + Require(Run(0, 0x104, 16, {u'a'}).code == kInvalidParam, "type 0x104"); +} + +void CheckSimpleTypes() { + Expect(Run(0, 1, 16, {0x41, 0x7f, 0x42, 0xe9, 0x20ac, 0x7e}), {0x41, 0x42, 0x7e}, "ascii type"); + Expect(Run(0, 3, 16, {0x41, 0x7f, 0x42}), {0x41, 0x42}, "type 3"); + Expect(Run(0, 0x102, 16, {0x41, 0xe9, 0x7e}), {0x41, 0x7e}, "type 0x102"); + Expect(Run(0, 4, 16, {0x31, 0x2c, 0x41, 0x2e, 0x2d, 0x0a, 0xff11}), {0x31, 0x2c, 0x2e, 0x2d, 0x0a}, "number type"); +} + +void CheckLatinType() { + Expect(Run(0, 0x100, 16, {0x41, 0x17e, 0x17f, 0x1a0, 0x218}), {0x41, 0x17e, 0x1a0, 0x218}, "latin type"); + Expect(Run(0x4000001f, 0x100, 16, {0x41, 0x17f, 0x1a0, 0x1af, 0x20ab, 0x300, 0x323, 0x1ea0, 0x218}), + {0x41, 0x1a0, 0x1af, 0x20ab, 0x300, 0x323, 0x1ea0}, "vietnamese latin"); + Expect(Run(0x8000001f, 0x100, 16, {0x41, 0x218, 0x219, 0x21a, 0x21b, 0x21c}), + {0x41, 0x218, 0x219, 0x21a, 0x21b}, "romanian latin"); +} + +void CheckClassifiedType() { + Expect(Run(0, 0, 32, {0x31, 0x2c, 0x42, 0x39, 0xe9, 0x3b1, 0x430, 0x4e2d, 0x3042, 0xe01, 0x627, 0xac00}), + {0x31, 0x2c, 0x42, 0x39, 0xe9, 0x3b1, 0x430, 0x4e2d, 0x3042, 0xe01, 0x627, 0xac00}, "default classes"); + Expect(Run(0x400001f, 0, 16, {0x41, 0xe01, 0xe5b, 0xe80, 0x627}), {0x41, 0xe01, 0xe5b}, "thai class"); + Expect(Run(0x20000041full, 0, 16, {0x41, 0x410, 0x411}), {0x41, 0x410, 0x411}, "cyrillic class"); +} + +void CheckBounds() { + Expect(Run(0, 1, 4, {0x41, 0x42, 0x43, 0x44, 0x45}), {0x41, 0x42, 0x43}, "capacity limit"); + Expect(Run(0, 1, 16, {0x41, 0, 0x42}), {0x41}, "stops at terminator"); + Expect(Run(0, 1, 16, {0x41, 0x42, 0x43}, 2), {0x41, 0x42}, "source length limit"); + Expect(Run(0, 1, 16, {0x41, 0x42, 0x43}, 0), {}, "empty source"); +} + +} + +int main() { + CheckArguments(); + CheckSimpleTypes(); + CheckLatinType(); + CheckClassifiedType(); + CheckBounds(); + return 0; +} diff --git a/core/libs/tests/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/GuestImeOpen.cpp b/core/libs/tests/GuestImeOpen.cpp index 15029e736b..39c7278c1e 100644 --- a/core/libs/tests/GuestImeOpen.cpp +++ b/core/libs/tests/GuestImeOpen.cpp @@ -69,10 +69,103 @@ void APS5_VABI Record(void* arg, const ImeEvent* event) { if (g_closeOnPressClose && event->id == EventPressClose) g_reentrantClose = sceImeClose_nid_postfix(); } +std::int32_t APS5_VABI TextFilter(char16_t*, std::uint32_t*, const char16_t*, std::uint32_t) { + std::abort(); +} + void APS5_VABI Other(void*, const ImeEvent*) { std::abort(); } +int32_t APS5_VABI UpperFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + for (uint32_t i = 0; i < sourceTextLength; ++i) { + const char16_t unit = sourceText[i]; + outText[i] = unit >= u'a' && unit <= u'z' ? static_cast(unit - u'a' + u'A') : unit; + } + *outTextLength = sourceTextLength; + return 0; +} + +int32_t APS5_VABI FailingFilter(char16_t*, uint32_t*, const char16_t*, uint32_t) { + return 1; +} + +int32_t APS5_VABI StripXFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + uint32_t length = 0; + for (uint32_t i = 0; i < sourceTextLength; ++i) { + if (sourceText[i] != u'x') outText[length++] = sourceText[i]; + } + *outTextLength = length; + return 0; +} + +int32_t APS5_VABI InvalidOutputFilter(char16_t* outText, uint32_t* outTextLength, const char16_t*, uint32_t) { + outText[0] = 0xdc00; + *outTextLength = 1; + return 0; +} + +int32_t APS5_VABI TooLongFilter(char16_t*, uint32_t* outTextLength, const char16_t*, uint32_t) { + *outTextLength = 9; + return 0; +} + +int32_t APS5_VABI ShortenFilter(char16_t* outText, uint32_t* outTextLength, const char16_t*, uint32_t) { + outText[0] = u'a'; + *outTextLength = 1; + return 0; +} + +int32_t APS5_VABI PairAtCaretFilter(char16_t* outText, uint32_t* outTextLength, const char16_t*, uint32_t) { + outText[0] = u'a'; + outText[1] = u'b'; + outText[2] = 0xd83d; + outText[3] = 0xde00; + *outTextLength = 4; + return 0; +} + +int APS5_VABI ExtNoop(const ImeKeycode*, std::uint16_t*, std::uint32_t*, void*) { + return 0; +} + +bool g_sawCaptureInsideFilter = false; +int g_setInsideRc = -1; + +int32_t APS5_VABI LockingFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + g_sawCaptureInsideFilter = ImeCapturesKeyboard_nid_postfix(); + for (uint32_t i = 0; i < sourceTextLength; ++i) { + outText[i] = sourceText[i]; + } + *outTextLength = sourceTextLength; + return 0; +} + +int32_t APS5_VABI SetInsideFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + g_setInsideRc = sceImeSetText(u"zz", 2); + for (uint32_t i = 0; i < sourceTextLength; ++i) { + outText[i] = sourceText[i]; + } + *outTextLength = sourceTextLength; + return 0; +} + +bool g_caretInsideFirstCall = true; +int g_caretInsideRc = -1; + +int32_t APS5_VABI SetCaretInsideFilter(char16_t* outText, uint32_t* outTextLength, const char16_t* sourceText, uint32_t sourceTextLength) { + if (g_caretInsideFirstCall) { + g_caretInsideFirstCall = false; + const Caret caret{0.0f, 0.0f, 0, 0}; + g_caretInsideRc = sceImeSetCaret(&caret); + } + for (uint32_t i = 0; i < sourceTextLength; ++i) { + outText[i] = sourceText[i]; + } + *outTextLength = sourceTextLength; + return 0; +} + void Require(bool value, const char* message) { if (value) return; std::fprintf(stderr, "%s\n", message); @@ -174,15 +267,17 @@ void CheckValidation() { Require(sceImeClose_nid_postfix() == 0, "close after over-2K open"); param = MakeParam(buffers, 16); - param.filter = reinterpret_cast(&Record); - bool threw = false; - try { - sceImeOpen_nid_postfix(¶m, nullptr); - } catch (const std::exception&) { - threw = true; - } - Require(threw, "text filter must throw"); - Require(!ImeCapturesKeyboard_nid_postfix(), "failed open left a session"); + param.filter = reinterpret_cast(&UpperFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a text filter"); + Require(ImeCapturesKeyboard_nid_postfix(), "text filter session not open"); + Require(sceImeClose_nid_postfix() == 0, "close the text filter session"); + + param = MakeParam(buffers, 16); + ExtendedParam extFilters{}; + extFilters.ext_keyboard_filter = reinterpret_cast(&ExtNoop); + Require(sceImeOpen_nid_postfix(¶m, &extFilters) == 0, "open with an external keyboard filter"); + Require(ImeCapturesKeyboard_nid_postfix(), "external keyboard filter session not open"); + Require(sceImeClose_nid_postfix() == 0, "close the external keyboard filter session"); } void CheckEditing() { @@ -304,6 +399,141 @@ void CheckMultiline() { Require(sceImeClose_nid_postfix() == 0, "multiline close"); } +void CheckTextFilter() { + Buffers buffers; + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + Param param = MakeParam(buffers, 8); + param.filter = reinterpret_cast(&UpperFilter); + ExtendedParam extFilters{}; + extFilters.ext_keyboard_filter = reinterpret_cast(&ExtNoop); + Require(sceImeOpen_nid_postfix(¶m, &extFilters) == 0, "open with both filters"); + Drain(); + + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"ABC", 3, 0, 1); + Require(std::u16string(buffers.text) == u"ABC", "filtered input buffer"); + Require(std::u16string(reinterpret_cast(buffers.work)) == u"ABC", "filtered work copy"); + + Publish(ImeHostInputKind::Backspace); + RequireText(Drain().at(0), EventUpdateText, u"AB", 2, 2, -1); + + Publish(ImeHostInputKind::Left); + RequireCaretSteps(Drain(), 1, 1); + Publish(ImeHostInputKind::Delete); + RequireText(Drain().at(0), EventUpdateText, u"A", 1, 1, -1); + + Require(sceImeSetText(u"xy", 2) == 0, "set text with a filter"); + Require(std::u16string(buffers.text) == u"xy", "set text is not filtered"); + Require(Drain().empty(), "set text with a filter queued an event"); + Require(sceImeClose_nid_postfix() == 0, "close the filtered session"); + + buffers = Buffers{}; + param = MakeParam(buffers, 8); + param.filter = reinterpret_cast(&StripXFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a stripping filter"); + Drain(); + Publish(ImeHostInputKind::Text, "x"); + Require(Drain().empty(), "a stripped edit sends no event"); + Require(std::u16string(buffers.text).empty(), "a stripped edit leaves the buffers"); + Publish(ImeHostInputKind::Text, "b"); + RequireText(Drain().at(0), EventUpdateText, u"b", 1, 0, 1); + Publish(ImeHostInputKind::Text, "x"); + Require(Drain().empty(), "a stripped edit sends no event again"); + Require(std::u16string(buffers.text) == u"b", "buffers after a stripped edit"); + Require(sceImeClose_nid_postfix() == 0, "close the stripping session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast(&FailingFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a failing filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"abc", 3, 2, 1); + Require(std::u16string(buffers.text) == u"abc", "a failed filter keeps the edit"); + Require(sceImeClose_nid_postfix() == 0, "close the failing session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast(&InvalidOutputFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with an invalid-output filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"abc", 3, 2, 1); + Require(std::u16string(buffers.text) == u"abc", "an invalid filter output keeps the edit"); + Require(sceImeClose_nid_postfix() == 0, "close the invalid-output session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast(&TooLongFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a too-long filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"abc", 3, 2, 1); + Require(std::u16string(buffers.text) == u"abc", "an overlong filter output keeps the edit"); + Require(sceImeClose_nid_postfix() == 0, "close the too-long session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast(&ShortenFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a shortening filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"a", 1, 1, -1); + Require(sceImeClose_nid_postfix() == 0, "close the shortening session"); + + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast(&PairAtCaretFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a pair filter"); + Drain(); + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"ab\U0001F600", 4, 2, 2); + Require(sceImeClose_nid_postfix() == 0, "close the pair session"); + + buffers = Buffers{}; + param = MakeParam(buffers, 8); + param.filter = reinterpret_cast(&LockingFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a filter that calls sceIme*"); + Drain(); + g_sawCaptureInsideFilter = false; + Publish(ImeHostInputKind::Text, "c"); + RequireText(Drain().at(0), EventUpdateText, u"c", 1, 0, 1); + Require(g_sawCaptureInsideFilter, "the filter can call back into sceIme*"); + Require(sceImeClose_nid_postfix() == 0, "close the locking session"); + + buffers = Buffers{}; + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast(&SetInsideFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a filter that sets the text"); + Drain(); + g_setInsideRc = -1; + Publish(ImeHostInputKind::Text, "c"); + Require(Drain().empty(), "an edit whose filter set the text sends no event"); + Require(g_setInsideRc == 0, "sceImeSetText inside the filter returns 0"); + Require(std::u16string(buffers.text) == u"zz", "the setText inside the filter wins"); + Require(std::u16string(reinterpret_cast(buffers.work)) == u"zz", "the setText inside the filter wins in the work copy"); + Require(sceImeClose_nid_postfix() == 0, "close the set-inside session"); + + buffers = Buffers{}; + param = MakeParam(buffers, 8); + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + param.filter = reinterpret_cast(&SetCaretInsideFilter); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open with a filter that moves the caret"); + Drain(); + g_caretInsideFirstCall = true; + g_caretInsideRc = -1; + Publish(ImeHostInputKind::Text, "c"); + Require(Drain().empty(), "an edit whose filter moved the caret sends no event"); + Require(g_caretInsideRc == 0, "sceImeSetCaret inside the filter returns 0"); + Require(std::u16string(buffers.text) == u"ab", "the discarded edit is not in the buffer"); + Publish(ImeHostInputKind::Text, "d"); + RequireText(Drain().at(0), EventUpdateText, u"dab", 1, 0, 1); + Require(std::u16string(buffers.text) == u"dab", "the edit after the discard builds on the old text"); + Require(std::u16string(reinterpret_cast(buffers.work)) == u"dab", "the edit after the discard reaches the work copy"); + Require(sceImeClose_nid_postfix() == 0, "close the caret-inside session"); +} + void CheckHostEvents() { Buffers buffers; Param param = MakeParam(buffers, 16); @@ -346,5 +576,6 @@ int main() { CheckValidation(); CheckEditing(); CheckMultiline(); + CheckTextFilter(); CheckHostEvents(); } diff --git a/core/libs/tests/GuestIndex.cpp b/core/libs/tests/GuestIndex.cpp new file mode 100644 index 0000000000..78fa2b8672 --- /dev/null +++ b/core/libs/tests/GuestIndex.cpp @@ -0,0 +1,97 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +namespace { + +using Search = char* (APS5_VABI*)(const char*, int); + +void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +char* Expected(const char* text, int character) { + const char wanted = static_cast(character); + for (;;) { + if (*text == wanted) return const_cast(text); + if (*text == '\0') return nullptr; + ++text; + } +} + +void Check(Search search, const char* text) { + for (int character = -512; character <= 512; ++character) { + Require(search(text, character) == Expected(text, character), "index returned the wrong position"); + } + for (int character : {INT_MIN, INT_MAX}) { + Require(search(text, character) == Expected(text, character), "index truncated an int incorrectly"); + } +} + +} + +int main(int argc, char** argv) { + Require(argc == 2, "expected the library path"); +#ifdef _WIN32 + const auto library = LoadLibraryA(argv[1]); + Require(library != nullptr, "could not load libc"); + const auto search = reinterpret_cast(GetProcAddress(library, "index_nid_postfix")); + SYSTEM_INFO info{}; + GetSystemInfo(&info); + const std::size_t page = info.dwPageSize; + auto* memory = static_cast(VirtualAlloc(nullptr, 2 * page, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(memory != nullptr, "could not allocate the boundary fixture"); + DWORD previous = 0; + Require(VirtualProtect(memory + page, page, PAGE_NOACCESS, &previous) != 0, "could not protect the boundary fixture"); +#else + const auto library = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); + Require(library != nullptr, "could not load libc"); + const auto search = reinterpret_cast(dlsym(library, "index_nid_postfix")); + const long pageSize = sysconf(_SC_PAGESIZE); + Require(pageSize > 0, "could not query page size"); + const auto page = static_cast(pageSize); + auto* memory = static_cast(mmap(nullptr, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(memory != MAP_FAILED, "could not allocate the boundary fixture"); + Require(mprotect(memory + page, page, PROT_NONE) == 0, "could not protect the boundary fixture"); +#endif + Require(search != nullptr, "index is missing from libc"); + Check(search, ""); + Check(search, "banana"); + const std::array bytes{'a', static_cast(0x80), 'a', static_cast(0xff), '\0', 'z', 'a', '\0', '\0'}; + const auto before = bytes; + Check(search, bytes.data()); + Require(bytes == before, "index modified its input"); + for (std::size_t length = 0; length <= 256; ++length) { + char* text = memory + page - length - 1; + for (std::size_t i = 0; i < length; ++i) text[i] = static_cast(1 + i % 255); + text[length] = '\0'; + Check(search, text); + } + char writable[] = "xyx"; + char* found = search(writable, 'y'); + Require(found == writable + 1, "index did not return the caller's storage"); + *found = 'z'; + Require(std::strcmp(writable, "xzx") == 0, "index returned an unwritable copy"); +#ifdef _WIN32 + Require(VirtualFree(memory, 0, MEM_RELEASE) != 0, "could not free the boundary fixture"); + FreeLibrary(library); +#else + Require(munmap(memory, 2 * page) == 0, "could not free the boundary fixture"); + dlclose(library); +#endif + return 0; +} diff --git a/core/libs/tests/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/GuestIntegerBuiltins.cpp b/core/libs/tests/GuestIntegerBuiltins.cpp new file mode 100644 index 0000000000..64f5cbb0e1 --- /dev/null +++ b/core/libs/tests/GuestIntegerBuiltins.cpp @@ -0,0 +1,266 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +using Int128 = __int128; +using Uint128 = unsigned __int128; + +extern "C" { +std::int64_t APS5_VABI __ashldi3_nid_postfix(std::int64_t, int); +std::int64_t APS5_VABI __ashrdi3_nid_postfix(std::int64_t, int); +std::int64_t APS5_VABI __lshrdi3_nid_postfix(std::int64_t, int); +Int128 APS5_VABI __ashlti3_nid_postfix(Int128, int); +Int128 APS5_VABI __ashrti3_nid_postfix(Int128, int); +Int128 APS5_VABI __lshrti3_nid_postfix(Int128, int); +int APS5_VABI __clzsi2_nid_postfix(std::uint32_t); +int APS5_VABI __clzdi2_nid_postfix(std::uint64_t); +int APS5_VABI __clzti2_nid_postfix(Uint128); +int APS5_VABI __ctzsi2_nid_postfix(std::uint32_t); +int APS5_VABI __ctzdi2_nid_postfix(std::uint64_t); +int APS5_VABI __ctzti2_nid_postfix(Uint128); +int APS5_VABI __ffsdi2_nid_postfix(std::uint64_t); +int APS5_VABI __ffsti2_nid_postfix(Uint128); +int APS5_VABI __paritysi2_nid_postfix(std::uint32_t); +int APS5_VABI __paritydi2_nid_postfix(std::uint64_t); +int APS5_VABI __parityti2_nid_postfix(Uint128); +int APS5_VABI __popcountsi2_nid_postfix(std::uint32_t); +int APS5_VABI __popcountdi2_nid_postfix(std::uint64_t); +int APS5_VABI __popcountti2_nid_postfix(Uint128); +int APS5_VABI __cmpdi2_nid_postfix(std::int64_t, std::int64_t); +int APS5_VABI __cmpti2_nid_postfix(Int128, Int128); +int APS5_VABI __ucmpdi2_nid_postfix(std::uint64_t, std::uint64_t); +int APS5_VABI __ucmpti2_nid_postfix(Uint128, Uint128); +std::int64_t APS5_VABI __negdi2_nid_postfix(std::int64_t); +Int128 APS5_VABI __negti2_nid_postfix(Int128); +std::int64_t APS5_VABI __muldi3_nid_postfix(std::int64_t, std::int64_t); +Int128 APS5_VABI __multi3_nid_postfix(Int128, Int128); +std::int32_t APS5_VABI __divsi3_nid_postfix(std::int32_t, std::int32_t); +std::int64_t APS5_VABI __divdi3_nid_postfix(std::int64_t, std::int64_t); +std::int32_t APS5_VABI __modsi3_nid_postfix(std::int32_t, std::int32_t); +std::int64_t APS5_VABI __moddi3_nid_postfix(std::int64_t, std::int64_t); +std::uint32_t APS5_VABI __udivsi3_nid_postfix(std::uint32_t, std::uint32_t); +std::uint64_t APS5_VABI __udivdi3_nid_postfix(std::uint64_t, std::uint64_t); +std::uint32_t APS5_VABI __umodsi3_nid_postfix(std::uint32_t, std::uint32_t); +std::uint64_t APS5_VABI __umoddi3_nid_postfix(std::uint64_t, std::uint64_t); +std::int32_t APS5_VABI __divmodsi4_nid_postfix(std::int32_t, std::int32_t, std::int32_t*); +std::int64_t APS5_VABI __divmoddi4_nid_postfix(std::int64_t, std::int64_t, std::int64_t*); +std::uint32_t APS5_VABI __udivmodsi4_nid_postfix(std::uint32_t, std::uint32_t, std::uint32_t*); +std::uint64_t APS5_VABI __udivmoddi4_nid_postfix(std::uint64_t, std::uint64_t, std::uint64_t*); +Uint128 APS5_VABI __udivmodti4_nid_postfix(Uint128, Uint128, Uint128*); +std::int32_t APS5_VABI __mulosi4_nid_postfix(std::int32_t, std::int32_t, int*); +std::int64_t APS5_VABI __mulodi4_nid_postfix(std::int64_t, std::int64_t, int*); +Int128 APS5_VABI __muloti4_nid_postfix(Int128, Int128, int*); +std::int32_t APS5_VABI __mulvsi3_nid_postfix(std::int32_t, std::int32_t); +std::int64_t APS5_VABI __mulvdi3_nid_postfix(std::int64_t, std::int64_t); +Int128 APS5_VABI __mulvti3_nid_postfix(Int128, Int128); +std::int32_t APS5_VABI __addvsi3_nid_postfix(std::int32_t, std::int32_t); +std::int64_t APS5_VABI __addvdi3_nid_postfix(std::int64_t, std::int64_t); +Int128 APS5_VABI __addvti3_nid_postfix(Int128, Int128); +std::int32_t APS5_VABI __subvsi3_nid_postfix(std::int32_t, std::int32_t); +std::int64_t APS5_VABI __subvdi3_nid_postfix(std::int64_t, std::int64_t); +Int128 APS5_VABI __subvti3_nid_postfix(Int128, Int128); +std::int32_t APS5_VABI __negvsi2_nid_postfix(std::int32_t); +std::int64_t APS5_VABI __negvdi2_nid_postfix(std::int64_t); +Int128 APS5_VABI __negvti2_nid_postfix(Int128); +std::int32_t APS5_VABI __absvsi2_nid_postfix(std::int32_t); +std::int64_t APS5_VABI __absvdi2_nid_postfix(std::int64_t); +Int128 APS5_VABI __absvti2_nid_postfix(Int128); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static void RequireThrows(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return; + } + std::abort(); +} + +static Uint128 Make(std::uint64_t high, std::uint64_t low) { + return (static_cast(high) << 64) | low; +} + +static constexpr Int128 Int128Max = static_cast(~(static_cast(1) << 127)); +static constexpr Int128 Int128Min = static_cast(static_cast(1) << 127); + +static void CheckShifts() { + Require(__ashldi3_nid_postfix(1, 63) == INT64_MIN); + Require(__ashrdi3_nid_postfix(INT64_MIN, 63) == -1); + Require(__lshrdi3_nid_postfix(INT64_MIN, 63) == 1); + Require(static_cast(__ashlti3_nid_postfix(1, 100)) == Make(1ull << 36, 0)); + Require(__ashrti3_nid_postfix(Int128Min, 127) == -1); + Require(__lshrti3_nid_postfix(Int128Min, 127) == 1); + Require(static_cast(__lshrti3_nid_postfix(static_cast(Make(0x8000000000000001ull, 0)), 64)) == Make(0, 0x8000000000000001ull)); + Require(__ashldi3_nid_postfix(12345, 0) == 12345); + Require(__ashrdi3_nid_postfix(-12345, 0) == -12345); + Require(__lshrdi3_nid_postfix(INT64_MIN, 0) == INT64_MIN); + Require(__ashlti3_nid_postfix(Int128Max, 0) == Int128Max); + Require(__ashrti3_nid_postfix(Int128Min, 0) == Int128Min); + Require(__lshrti3_nid_postfix(Int128Max, 0) == Int128Max); + Require(__ashldi3_nid_postfix(0, 10) == 0); + Require(__ashrdi3_nid_postfix(0, 10) == 0); + Require(__lshrdi3_nid_postfix(0, 10) == 0); + Require(__ashlti3_nid_postfix(0, 70) == 0); + Require(__ashrti3_nid_postfix(0, 70) == 0); + Require(__lshrti3_nid_postfix(0, 70) == 0); + Require(static_cast(__ashlti3_nid_postfix(1, 64)) == Make(1, 0)); + Require(static_cast(__ashlti3_nid_postfix(1, 127)) == Make(1ull << 63, 0)); + Require(__lshrti3_nid_postfix(static_cast(Make(1, 0)), 64) == 1); + Require(__lshrti3_nid_postfix(Int128Min, 127) == 1); + Require(__ashrti3_nid_postfix(Int128Min, 64) == static_cast(Make(~0ull, 1ull << 63))); +} + +static void CheckBitCounts() { + Require(__clzsi2_nid_postfix(1) == 31 && __clzdi2_nid_postfix(1) == 63); + Require(__clzti2_nid_postfix(1) == 127 && __clzti2_nid_postfix(Make(1, 0)) == 63); + Require(__ctzsi2_nid_postfix(0x80000000u) == 31 && __ctzdi2_nid_postfix(1ull << 40) == 40); + Require(__ctzti2_nid_postfix(Make(1, 0)) == 64 && __ctzti2_nid_postfix(4) == 2); + Require(__clzdi2_nid_postfix(0) == 32 && __ctzdi2_nid_postfix(0) == 32); + Require(__clzti2_nid_postfix(0) == 64 && __ctzti2_nid_postfix(0) == 64); + Require(__ffsdi2_nid_postfix(0) == 0 && __ffsdi2_nid_postfix(8) == 4); + Require(__ffsti2_nid_postfix(0) == 0 && __ffsti2_nid_postfix(Make(2, 0)) == 66); + Require(__paritysi2_nid_postfix(7) == 1 && __paritydi2_nid_postfix(3) == 0); + Require(__parityti2_nid_postfix(Make(1, 1)) == 0 && __parityti2_nid_postfix(Make(1, 0)) == 1); + Require(__popcountsi2_nid_postfix(0xffffffffu) == 32 && __popcountdi2_nid_postfix(~0ull) == 64); + Require(__popcountti2_nid_postfix(Make(~0ull, ~0ull)) == 128); + Require(__clzsi2_nid_postfix(0) == 32); + Require(__ctzsi2_nid_postfix(0) == 32); + Require(__popcountsi2_nid_postfix(0) == 0); + Require(__popcountdi2_nid_postfix(0) == 0); + Require(__popcountti2_nid_postfix(0) == 0); + Require(__paritysi2_nid_postfix(0) == 0); + Require(__paritydi2_nid_postfix(0) == 0); + Require(__parityti2_nid_postfix(0) == 0); + Require(__clzsi2_nid_postfix(0x80000000u) == 0); + Require(__clzdi2_nid_postfix(1ull << 63) == 0); + Require(__clzti2_nid_postfix(Make(1ull << 63, 0)) == 0); + Require(__ctzsi2_nid_postfix(1) == 0); + Require(__ctzdi2_nid_postfix(1) == 0); + Require(__ctzti2_nid_postfix(1) == 0); + Require(__ffsdi2_nid_postfix(1) == 1 && __ffsdi2_nid_postfix(1ull << 63) == 64); + Require(__ffsti2_nid_postfix(1) == 1 && __ffsti2_nid_postfix(Make(1ull << 63, 0)) == 128); +} + +static void CheckComparisons() { + Require(__cmpdi2_nid_postfix(-1, 0) == 0 && __cmpdi2_nid_postfix(5, 5) == 1 && __cmpdi2_nid_postfix(6, 5) == 2); + Require(__cmpti2_nid_postfix(Int128Min, 0) == 0 && __cmpti2_nid_postfix(Int128Max, 0) == 2); + Require(__ucmpdi2_nid_postfix(~0ull, 0) == 2 && __ucmpdi2_nid_postfix(0, 1) == 0); + Require(__ucmpti2_nid_postfix(Make(1, 0), Make(0, ~0ull)) == 2 && __ucmpti2_nid_postfix(3, 3) == 1); + Require(__cmpdi2_nid_postfix(INT64_MIN, INT64_MIN) == 1 && __cmpdi2_nid_postfix(INT64_MAX, INT64_MAX) == 1); + Require(__cmpdi2_nid_postfix(INT64_MIN, INT64_MAX) == 0 && __cmpdi2_nid_postfix(INT64_MAX, INT64_MIN) == 2); + Require(__cmpti2_nid_postfix(Int128Min, Int128Min) == 1 && __cmpti2_nid_postfix(Int128Max, Int128Max) == 1); + Require(__cmpti2_nid_postfix(Int128Min, Int128Max) == 0 && __cmpti2_nid_postfix(-1, 1) == 0 && __cmpti2_nid_postfix(1, -1) == 2); + Require(__ucmpdi2_nid_postfix(~0ull, ~0ull) == 1 && __ucmpdi2_nid_postfix(0, 0) == 1); + Require(__ucmpti2_nid_postfix(0, 0) == 1 && __ucmpti2_nid_postfix(Make(~0ull, ~0ull), Make(~0ull, ~0ull)) == 1); + Require(__ucmpti2_nid_postfix(0, Make(~0ull, ~0ull)) == 0 && __ucmpti2_nid_postfix(Make(0, ~0ull), Make(1, 0)) == 0); +} + +static void CheckArithmetic() { + Require(__negdi2_nid_postfix(INT64_MIN) == INT64_MIN && __negdi2_nid_postfix(5) == -5); + Require(__negti2_nid_postfix(Int128Min) == Int128Min && __negti2_nid_postfix(-7) == 7); + Require(__muldi3_nid_postfix(-3, 7) == -21); + Require(static_cast(__multi3_nid_postfix(static_cast(Make(0, ~0ull)), static_cast(Make(0, ~0ull)))) + == Make(0xfffffffffffffffeull, 1)); + Require(__multi3_nid_postfix(-3, static_cast(Make(1, 0))) == -static_cast(Make(3, 0))); + Require(__negdi2_nid_postfix(0) == 0 && __negdi2_nid_postfix(1) == -1 && __negdi2_nid_postfix(-1) == 1); + Require(__negti2_nid_postfix(0) == 0 && __negti2_nid_postfix(1) == -1 && __negti2_nid_postfix(-1) == 1); + Require(__negti2_nid_postfix(Int128Max) == -Int128Max); + Require(__muldi3_nid_postfix(0, 100) == 0 && __muldi3_nid_postfix(100, 0) == 0); + Require(__muldi3_nid_postfix(1, INT64_MIN) == INT64_MIN && __muldi3_nid_postfix(1, INT64_MAX) == INT64_MAX); + Require(__muldi3_nid_postfix(-1, INT64_MAX) == -INT64_MAX && __muldi3_nid_postfix(-1, INT64_MIN) == INT64_MIN); + Require(__muldi3_nid_postfix(INT64_MAX, 2) == -2); + Require(__multi3_nid_postfix(0, Int128Max) == 0 && __multi3_nid_postfix(Int128Min, 0) == 0); + Require(__multi3_nid_postfix(1, Int128Min) == Int128Min && __multi3_nid_postfix(1, Int128Max) == Int128Max); + Require(__multi3_nid_postfix(-1, Int128Max) == -Int128Max && __multi3_nid_postfix(-1, Int128Min) == Int128Min); + Require(__multi3_nid_postfix(Int128Max, 2) == -2 && __multi3_nid_postfix(Int128Min, 2) == 0); +} + +static void CheckDivision() { + Require(__divsi3_nid_postfix(-7, 2) == -3 && __modsi3_nid_postfix(-7, 2) == -1); + Require(__divdi3_nid_postfix(INT64_MIN, -1) == INT64_MIN && __moddi3_nid_postfix(INT64_MIN, -1) == 0); + Require(__udivsi3_nid_postfix(0xffffffffu, 16) == 0x0fffffffu && __umodsi3_nid_postfix(0xffffffffu, 16) == 15); + Require(__udivdi3_nid_postfix(~0ull, 3) == 0x5555555555555555ull && __umoddi3_nid_postfix(10, 4) == 2); + std::int32_t remainder32 = 0; + Require(__divmodsi4_nid_postfix(-9, 4, &remainder32) == -2 && remainder32 == -1); + std::int64_t remainder64 = 0; + Require(__divmoddi4_nid_postfix(9, -4, &remainder64) == -2 && remainder64 == 1); + std::uint32_t uremainder32 = 0; + Require(__udivmodsi4_nid_postfix(17, 5, &uremainder32) == 3 && uremainder32 == 2); + std::uint64_t uremainder64 = 0; + Require(__udivmoddi4_nid_postfix(17, 5, &uremainder64) == 3 && uremainder64 == 2); + Require(__udivmoddi4_nid_postfix(17, 5, nullptr) == 3); + Uint128 remainder128 = 0; + Require(__udivmodti4_nid_postfix(Make(1, 5), 2, &remainder128) == Make(0, 0x8000000000000002ull) && remainder128 == 1); + Require(__udivsi3_nid_postfix(7, 0) == 0 && __umodsi3_nid_postfix(7, 0) == 7); + Require(__udivmodsi4_nid_postfix(7, 0, &uremainder32) == 0 && uremainder32 == 7); + RequireThrows([] { __divsi3_nid_postfix(1, 0); }); + RequireThrows([] { __modsi3_nid_postfix(1, 0); }); + RequireThrows([] { __udivdi3_nid_postfix(1, 0); }); + RequireThrows([] { __umoddi3_nid_postfix(1, 0); }); + RequireThrows([] { __udivmodti4_nid_postfix(1, 0, nullptr); }); + Require(__divsi3_nid_postfix(INT32_MIN, -1) == INT32_MIN && __modsi3_nid_postfix(INT32_MIN, -1) == 0); + Require(__divsi3_nid_postfix(0, 5) == 0 && __modsi3_nid_postfix(0, 5) == 0); + Require(__divdi3_nid_postfix(0, 5) == 0 && __moddi3_nid_postfix(0, 5) == 0); + Require(__divmodsi4_nid_postfix(INT32_MIN, -1, &remainder32) == INT32_MIN && remainder32 == 0); + Require(__divmoddi4_nid_postfix(INT64_MIN, -1, &remainder64) == INT64_MIN && remainder64 == 0); + Require(__udivsi3_nid_postfix(0, 5) == 0 && __umodsi3_nid_postfix(0, 5) == 0); + Require(__udivdi3_nid_postfix(0, 5) == 0 && __umoddi3_nid_postfix(0, 5) == 0); + Require(__udivmodsi4_nid_postfix(0, 5, &uremainder32) == 0 && uremainder32 == 0); + Require(__udivmoddi4_nid_postfix(0, 5, &uremainder64) == 0 && uremainder64 == 0); + Require(__udivmodti4_nid_postfix(0, 5, &remainder128) == 0 && remainder128 == 0); + Require(__udivmodti4_nid_postfix(Make(~0ull, ~0ull), Make(~0ull, ~0ull), &remainder128) == 1 && remainder128 == 0); + Require(__udivmodti4_nid_postfix(Make(0, 10), Make(1, 0), &remainder128) == 0 && remainder128 == 10); +} + +static void CheckOverflow() { + int overflow = -1; + Require(__mulosi4_nid_postfix(0x10000, 0x8000, &overflow) == INT32_MIN && overflow == 1); + Require(__mulosi4_nid_postfix(-0x10000, 0x8000, &overflow) == INT32_MIN && overflow == 0); + Require(__mulodi4_nid_postfix(INT64_MIN, 1, &overflow) == INT64_MIN && overflow == 0); + Require(__mulodi4_nid_postfix(INT64_MIN, -1, &overflow) == INT64_MIN && overflow == 1); + Require(__mulodi4_nid_postfix(3037000500, 3037000500, &overflow) && overflow == 1); + Require(__mulodi4_nid_postfix(3037000499, 3037000499, &overflow) == 9223372030926249001 && overflow == 0); + Require(__muloti4_nid_postfix(Int128Max, -1, &overflow) == -Int128Max && overflow == 0); + Require(__muloti4_nid_postfix(Int128Max, 2, &overflow) == -2 && overflow == 1); + Require(__muloti4_nid_postfix(static_cast(Make(0, 1ull << 63)), -static_cast(Make(1, 0)), &overflow) == Int128Min + && overflow == 0); + Require(__mulvsi3_nid_postfix(-46341, 46340) == -2147441940); + Require(__mulvdi3_nid_postfix(-4, 5) == -20 && __mulvti3_nid_postfix(-4, 5) == -20); + RequireThrows([] { __mulvsi3_nid_postfix(46341, 46341); }); + RequireThrows([] { __mulvdi3_nid_postfix(INT64_MIN, -1); }); + RequireThrows([] { __mulvti3_nid_postfix(Int128Min, 2); }); + Require(__addvsi3_nid_postfix(INT32_MAX, INT32_MIN) == -1 && __addvdi3_nid_postfix(-1, 1) == 0); + Require(__addvti3_nid_postfix(Int128Max, -1) == Int128Max - 1); + Require(__subvsi3_nid_postfix(INT32_MIN, -1) == INT32_MIN + 1 && __subvdi3_nid_postfix(0, INT64_MAX) == -INT64_MAX); + Require(__subvti3_nid_postfix(0, Int128Max) == Int128Min + 1); + Require(__addvsi3_nid_postfix(INT32_MAX, 1) == INT32_MIN && __addvti3_nid_postfix(Int128Min, -1) == Int128Max); + Require(__subvdi3_nid_postfix(INT64_MIN, 1) == INT64_MAX && __subvti3_nid_postfix(Int128Max, -1) == Int128Min); + Require(__negvsi2_nid_postfix(5) == -5 && __negvdi2_nid_postfix(-5) == 5 && __negvti2_nid_postfix(Int128Max) == Int128Min + 1); + Require(__absvsi2_nid_postfix(-5) == 5 && __absvdi2_nid_postfix(5) == 5 && __absvti2_nid_postfix(-Int128Max) == Int128Max); + RequireThrows([] { __negvsi2_nid_postfix(INT32_MIN); }); + RequireThrows([] { __absvdi2_nid_postfix(INT64_MIN); }); + RequireThrows([] { __absvti2_nid_postfix(Int128Min); }); + Require(__mulosi4_nid_postfix(INT32_MIN, 1, &overflow) == INT32_MIN && overflow == 0); + Require(__mulosi4_nid_postfix(INT32_MIN, -1, &overflow) == INT32_MIN && overflow == 1); + Require(__mulosi4_nid_postfix(0, INT32_MIN, &overflow) == 0 && overflow == 0); + Require(__mulodi4_nid_postfix(0, INT64_MIN, &overflow) == 0 && overflow == 0); + Require(__muloti4_nid_postfix(Int128Min, 1, &overflow) == Int128Min && overflow == 0); + Require(__muloti4_nid_postfix(Int128Min, -1, &overflow) == Int128Min && overflow == 1); + Require(__muloti4_nid_postfix(0, Int128Min, &overflow) == 0 && overflow == 0); + Require(__negvsi2_nid_postfix(0) == 0 && __negvdi2_nid_postfix(0) == 0 && __negvti2_nid_postfix(0) == 0); + Require(__absvsi2_nid_postfix(0) == 0 && __absvdi2_nid_postfix(0) == 0 && __absvti2_nid_postfix(0) == 0); +} + +int main() { + CheckShifts(); + CheckBitCounts(); + CheckComparisons(); + CheckArithmetic(); + CheckDivision(); + CheckOverflow(); + return 0; +} diff --git a/core/libs/tests/GuestInterfaces.cpp b/core/libs/tests/GuestInterfaces.cpp index eeb24342a3..5b69f12016 100644 --- a/core/libs/tests/GuestInterfaces.cpp +++ b/core/libs/tests/GuestInterfaces.cpp @@ -4,10 +4,21 @@ #include #include #else +#include #include +#include +#include #include +#include +#include +#include +#include +#include +#include #endif +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -33,6 +44,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(); } @@ -43,6 +55,12 @@ struct HostInterfaceAddress { std::uint8_t address[16]; std::uint8_t netmask[16]; std::uint32_t scope; + char name[16]; + unsigned int index; + std::uint8_t type; + std::uint8_t hardwareLength; + std::uint8_t hardware[8]; + bool loopbackUp; }; static HostInterfaceAddress HostAddress(const sockaddr* address) { @@ -58,6 +76,43 @@ static HostInterfaceAddress HostAddress(const sockaddr* address) { return entry; } +#ifndef _WIN32 +static std::uint64_t SysfsCounter(const char* name, const char* file) { + std::ifstream input(std::string("/sys/class/net/") + name + "/statistics/" + file); + unsigned long long value = 0; + Require(static_cast(input >> value)); + return value; +} + +static std::uint32_t HostMtu(const char* name) { + const int probe = ::socket(AF_INET, SOCK_DGRAM, 0); + Require(probe >= 0); + ifreq request{}; + std::strncpy(request.ifr_name, name, sizeof(request.ifr_name) - 1); + Require(::ioctl(probe, SIOCGIFMTU, &request) == 0); + ::close(probe); + return static_cast(request.ifr_mtu); +} + +static void SendLoopbackDatagram() { + const int receiver = ::socket(AF_INET, SOCK_DGRAM, 0); + const int sender = ::socket(AF_INET, SOCK_DGRAM, 0); + Require(receiver >= 0 && sender >= 0); + sockaddr_in address{}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + Require(::bind(receiver, reinterpret_cast(&address), sizeof(address)) == 0); + socklen_t length = sizeof(address); + Require(::getsockname(receiver, reinterpret_cast(&address), &length) == 0); + const char payload = 'x'; + Require(::sendto(sender, &payload, 1, 0, reinterpret_cast(&address), sizeof(address)) == 1); + char received = 0; + Require(::recv(receiver, &received, 1, 0) == 1); + ::close(sender); + ::close(receiver); +} +#endif + static std::vector HostSnapshot() { std::vector entries; #ifdef _WIN32 @@ -85,31 +140,62 @@ static std::vector HostSnapshot() { #else ifaddrs* host = nullptr; Require(::getifaddrs(&host) == 0); + std::vector names; + std::vector> groups; + const auto group = [&](const char* name) -> std::vector& { + for (std::size_t index = 0; index < names.size(); ++index) + if (names[index] == name) return groups[index]; + names.emplace_back(name); + return groups.emplace_back(); + }; try { for (auto* item = host; item; item = item->ifa_next) { - if (!item->ifa_addr || (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6)) continue; + if (!item->ifa_addr) continue; + if (item->ifa_addr->sa_family == AF_PACKET) { + const auto& link = *reinterpret_cast(item->ifa_addr); + HostInterfaceAddress entry{}; + entry.family = AF_PACKET; + std::strncpy(entry.name, item->ifa_name, sizeof(entry.name) - 1); + entry.index = static_cast(link.sll_ifindex); + entry.type = link.sll_hatype == ARPHRD_ETHER ? 0x6 : link.sll_hatype == ARPHRD_LOOPBACK ? 0x18 : link.sll_hatype == ARPHRD_PPP ? 0x17 : 0x1; + entry.hardwareLength = entry.type == 0x18 ? 0 : link.sll_halen; + std::memcpy(entry.hardware, link.sll_addr, entry.hardwareLength); + entry.loopbackUp = (item->ifa_flags & IFF_UP) != 0; + auto& members = group(item->ifa_name); + members.insert(members.begin(), entry); + continue; + } + if (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6) continue; Require(item->ifa_netmask != nullptr); HostInterfaceAddress entry = HostAddress(item->ifa_addr); + std::strncpy(entry.name, item->ifa_name, sizeof(entry.name) - 1); if (entry.family == AF_INET) std::memcpy(entry.netmask, &reinterpret_cast(item->ifa_netmask)->sin_addr, 4); else std::memcpy(entry.netmask, &reinterpret_cast(item->ifa_netmask)->sin6_addr, 16); - entries.push_back(entry); + group(item->ifa_name).push_back(entry); } } catch (...) { ::freeifaddrs(host); throw; } ::freeifaddrs(host); + for (const auto& members : groups) entries.insert(entries.end(), members.begin(), members.end()); #endif return entries; } 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}; bool matched = false; +#ifndef _WIN32 + bool linkChecked = false; + SendLoopbackDatagram(); +#endif for (unsigned attempt = 0; attempt < 3 && !matched; ++attempt) { const std::vector host = HostSnapshot(); GuestInterfaceAddress* list = nullptr; @@ -123,8 +209,47 @@ int main() { const std::size_t nameLength = std::strlen(item->name); Require(nameLength >= 1 && nameLength <= 15); Require((item->flags & ~knownFlags) == 0); - Require(item->data == nullptr); const auto* address = static_cast(item->address); + Require(address != nullptr && reinterpret_cast(address) % 8 == 0); +#ifndef _WIN32 + if (address[1] == 18) { + Require(item->netmask == nullptr && item->destination == nullptr); + const auto* data = static_cast(item->data); + Require(data != nullptr && reinterpret_cast(data) % 8 == 0); + std::uint16_t index = 0; + std::uint16_t dataLength = 0; + std::uint32_t mtu = 0; + std::uint64_t counters[11] = {}; + std::memcpy(&index, address + 2, 2); + std::memcpy(&dataLength, data + 6, 2); + std::memcpy(&mtu, data + 8, 4); + std::memcpy(counters, data + 24, sizeof(counters)); + Require(address[0] >= 54 && address[0] % 8 == 0 && address[0] >= 24 + address[6]); + Require(address[5] == nameLength && std::memcmp(address + 8, item->name, nameLength) == 0 && address[7] == 0); + Require(dataLength == 152 && data[0] == address[4] && data[2] == address[6] && data[1] == 0 && data[5] == 0); + Require(data[4] == 1 || data[4] == 2); + Require(index == if_nametoindex(item->name)); + Require(mtu == HostMtu(item->name)); + Require(counters[0] <= SysfsCounter(item->name, "rx_packets") && counters[2] <= SysfsCounter(item->name, "tx_packets")); + Require(counters[5] <= SysfsCounter(item->name, "rx_bytes") && counters[6] <= SysfsCounter(item->name, "tx_bytes")); + if (entries <= host.size()) { + const HostInterfaceAddress& expected = host[entries - 1]; + equal = equal && expected.family == AF_PACKET && std::strcmp(expected.name, item->name) == 0 && expected.index == index && + expected.type == address[4] && expected.hardwareLength == address[6] && + std::memcmp(expected.hardware, address + 8 + nameLength, expected.hardwareLength) == 0; + if (expected.type == 0x18) { + Require(data[3] == 0 && data[4] == (expected.loopbackUp ? 2 : 1)); + if (expected.loopbackUp) Require(counters[0] > 0 && counters[2] > 0 && counters[5] > 0 && counters[6] > 0); + linkChecked = linkChecked || expected.loopbackUp; + } + if (expected.type == 0x6) Require(data[3] == 14 && address[6] == 6); + } else { + equal = false; + } + continue; + } +#endif + Require(item->data == nullptr); const auto* netmask = static_cast(item->netmask); Require(address != nullptr && netmask != nullptr); Require(reinterpret_cast(address) % 8 == 0); @@ -146,6 +271,9 @@ int main() { if (item->flags & 0x10) Require(item->destination != nullptr); if (entries <= host.size()) { const HostInterfaceAddress& expected = host[entries - 1]; +#ifndef _WIN32 + equal = equal && std::strcmp(expected.name, item->name) == 0; +#endif const bool ipv4 = address[1] == 2; const std::size_t offset = ipv4 ? 4 : 8; const std::size_t length = ipv4 ? 4 : 16; @@ -167,6 +295,12 @@ int main() { freeifaddrs_nid_postfix(list); } Require(matched); +#ifndef _WIN32 + Require(linkChecked); +#endif + GuestInterfaceAddress* released = nullptr; + Require(getifaddrs_nid_postfix(&released) == 0 && released != nullptr); + free_nid_postfix(released); 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/GuestJitSharedMemory.cpp b/core/libs/tests/GuestJitSharedMemory.cpp index c1c966ee0c..66ed8a4838 100644 --- a/core/libs/tests/GuestJitSharedMemory.cpp +++ b/core/libs/tests/GuestJitSharedMemory.cpp @@ -16,6 +16,8 @@ int APS5_VABI close_nid_postfix(int); int APS5_VABI dup_nid_postfix(int); int APS5_VABI dup2_nid_postfix(int, int); int APS5_VABI shm_open_nid_postfix(const char*, int, int); +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; } namespace { @@ -101,6 +103,21 @@ void OneViewWritesAndRuns() { Require(close_nid_postfix(duplicate) == 0); } +void MmapOfTheDescriptorSharesTheObject() { + const int fd = Create(nullptr, Read | Write); + auto* jit = static_cast(Map(fd, Read | Write)); + void* mapped = mmap_nid_postfix(nullptr, Size, Read | Write, 0x1, fd, 0); + Require(mapped != reinterpret_cast(static_cast(-1))); + auto* view = static_cast(mapped); + jit[0x200] = 0x5a; + Require(view[0x200] == 0x5a); + view[0x300] = 0x6b; + Require(jit[0x300] == 0x6b); + Require(munmap_nid_postfix(mapped, Size) == 0); + Require(sceKernelMunmap(const_cast(jit), Size) == 0); + Require(close_nid_postfix(fd) == 0); +} + void Dup2KeepsTheDescriptorTableInStep() { const int fd = Create(nullptr, Read | Write); const int target = shm_open_nid_postfix(reinterpret_cast(1), 0x2, 0600); @@ -170,5 +187,6 @@ int main() { WriteThroughAliasRunsThroughExecutableView(); OneViewWritesAndRuns(); Dup2KeepsTheDescriptorTableInStep(); + MmapOfTheDescriptorSharesTheObject(); InvalidArgumentsAreRejected(); } diff --git a/core/libs/tests/GuestJobManager.cpp b/core/libs/tests/GuestJobManager.cpp index e82703be18..7258ae6694 100644 --- a/core/libs/tests/GuestJobManager.cpp +++ b/core/libs/tests/GuestJobManager.cpp @@ -354,6 +354,34 @@ static void LimitsCase() { while (accepted < 0x3ff && (result = sequence->Dispatch(Nothing, nullptr, 1, nullptr)) == 0) ++accepted; Require(result == NoNodes && accepted == 33); Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == Invalid); + Require(sequence->Wait() == Invalid); + Require(factory->Destroy(sequence) == 0); + Require(factory->Create(&sequence, 0x7fff, 0x3ff, 0xff, 10, "recycle") == 0); + for (int cycle = 0; cycle < 5; ++cycle) { + for (int index = 0; index < 33; ++index) { + Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == 0); + } + Require(sequence->Wait() == 0); + } + int finalAccepted = 0; + int finalResult = 0; + while (finalAccepted < 0x3ff && (finalResult = sequence->Dispatch(Nothing, nullptr, 1, nullptr)) == 0) ++finalAccepted; + Require(finalResult == NoNodes && finalAccepted == 33); + Require(sequence->Wait() == Invalid); + Require(factory->Destroy(sequence) == 0); + for (int round = 0; round < 5; ++round) { + Require(factory->Create(&sequence, 0x7fff, 0x3ff, 0xff, 2, "recycle_round") == 0); + for (int index = 0; index < 33; ++index) { + Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == 0); + } + Require(sequence->Wait() == 0); + int acceptedInRound = 0; + int resultInRound = 0; + while (acceptedInRound < 0x3ff && (resultInRound = sequence->Dispatch(Nothing, nullptr, 1, nullptr)) == 0) ++acceptedInRound; + Require(resultInRound == NoNodes && acceptedInRound == 33); + Require(sequence->Wait() == Invalid); + Require(factory->Destroy(sequence) == 0); + } Require(Shutdown(manager.storage) == 0); } @@ -418,6 +446,11 @@ static void ShutdownCase() { Require(Shutdown(manager.storage) == NotInitialized); Require(Factories(manager.storage, 0) == nullptr); } + { + Block scopedBlock(SizeFor(2, 2, 8, 2, 2)); + ManagerObject scoped; + Require(Initialize(scoped.storage, scopedBlock.memory, scopedBlock.bytes, 2, 2, nullptr) == 0); + } } int main(int argc, char** argv) { diff --git a/core/libs/tests/GuestJpegDec.cpp b/core/libs/tests/GuestJpegDec.cpp index 1268bd57d2..6a8273a54e 100644 --- a/core/libs/tests/GuestJpegDec.cpp +++ b/core/libs/tests/GuestJpegDec.cpp @@ -1,12 +1,30 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "Decoder/Jpeg.hpp" +#include +#include #include #include #include +#include #include +#include #include +static_assert(sizeof(JpegDecCreateParam) == 0xC); +static_assert(sizeof(JpegDecParseParam) == 0x10); +static_assert(offsetof(JpegDecParseParam, jpeg_mem_size) == 0x8 && offsetof(JpegDecParseParam, decode_mode) == 0xC && offsetof(JpegDecParseParam, down_scale) == 0xE); +static_assert(sizeof(JpegDecDecodeParam) == 0x30); +static_assert(offsetof(JpegDecDecodeParam, coefficient_mem_addr) == 0x10 && offsetof(JpegDecDecodeParam, jpeg_mem_size) == 0x18); +static_assert(offsetof(JpegDecDecodeParam, image_mem_size) == 0x1C && offsetof(JpegDecDecodeParam, coefficient_mem_size) == 0x20); +static_assert(offsetof(JpegDecDecodeParam, decode_mode) == 0x24 && offsetof(JpegDecDecodeParam, down_scale) == 0x26); +static_assert(offsetof(JpegDecDecodeParam, pixel_format) == 0x28 && offsetof(JpegDecDecodeParam, alpha_value) == 0x2A && offsetof(JpegDecDecodeParam, image_pitch) == 0x2C); +static_assert(sizeof(JpegDecImageInfo) == 0x24); +static_assert(offsetof(JpegDecImageInfo, color_space) == 0x8 && offsetof(JpegDecImageInfo, component_number) == 0xA); +static_assert(offsetof(JpegDecImageInfo, component_id) == 0xC && offsetof(JpegDecImageInfo, sampling_factor) == 0x10); +static_assert(offsetof(JpegDecImageInfo, coefficient_mem_size) == 0x14 && offsetof(JpegDecImageInfo, suitable_csc_attribute) == 0x18); +static_assert(offsetof(JpegDecImageInfo, output_image_width) == 0x1C && offsetof(JpegDecImageInfo, output_image_height) == 0x20); + extern "C" { std::int32_t APS5_VABI sceJpegDecQueryMemorySize(const JpegDecCreateParam*); std::int32_t APS5_VABI sceJpegDecCreate(const JpegDecCreateParam*, void*, std::uint32_t, void**); @@ -17,89 +35,271 @@ std::int32_t APS5_VABI sceJpegDecDecode(void*, const JpegDecDecodeParam*, JpegDe static void Require(bool value) { if (!value) std::abort(); } -static std::vector TestJpeg(std::uint32_t width, std::uint32_t height) { - std::vector rgb(static_cast(width) * height * 3); - for (std::size_t i = 0; i < rgb.size(); ++i) rgb[i] = static_cast(10 + i * 13); - return Decoder::Jpeg::Encode(rgb, width, height, 3, 90); +template +static bool ThrowsRuntimeError(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return true; + } + return false; } -static JpegDecDecodeParam DecodeParam(const std::vector& jpeg, std::vector& image) { +static std::vector TestJpeg(std::uint32_t width, std::uint32_t height, std::uint32_t channels) { + std::vector pixels(static_cast(width) * height * channels); + for (std::size_t i = 0; i < pixels.size(); ++i) pixels[i] = static_cast(10 + i * 13); + return Decoder::Jpeg::Encode(pixels, width, height, channels, 90); +} + +static JpegDecDecodeParam DecodeParam(const std::vector& jpeg, std::vector& image, std::uint32_t pitch) { JpegDecDecodeParam param{}; param.jpeg_mem_addr = jpeg.data(); param.image_mem_addr = image.data(); + param.coefficient_mem_addr = nullptr; param.jpeg_mem_size = static_cast(jpeg.size()); param.image_mem_size = static_cast(image.size()); + param.coefficient_mem_size = 0; + param.decode_mode = 0; + param.down_scale = 1; param.pixel_format = 0; - param.reserved0 = 0; - param.image_pitch = 0; + param.alpha_value = 0xFF; + param.image_pitch = pitch; return param; } int main() { - constexpr std::int32_t invalidAddr = static_cast(0x80690001); - constexpr std::int32_t invalidSize = static_cast(0x80690002); - constexpr std::int32_t invalidParam = static_cast(0x80690003); - constexpr std::int32_t invalidHandle = static_cast(0x80690004); - constexpr std::int32_t invalidWorkMemory = static_cast(0x80690005); - constexpr std::int32_t invalidData = static_cast(0x80690010); + constexpr std::int32_t invalidAddr = static_cast(0x80650001); + constexpr std::int32_t invalidSize = static_cast(0x80650002); + constexpr std::int32_t invalidParam = static_cast(0x80650003); + constexpr std::int32_t invalidHandle = static_cast(0x80650004); + constexpr std::int32_t invalidData = static_cast(0x80650010); - const JpegDecCreateParam param{sizeof(JpegDecCreateParam), 0}; + const std::vector jpeg = TestJpeg(7, 5, 3); + JpegDecImageInfo info{}; + const JpegDecParseParam parseParam{jpeg.data(), static_cast(jpeg.size()), 0, 1}; + Require(sceJpegDecParseHeader(nullptr, &info) == invalidAddr); + Require(sceJpegDecParseHeader(&parseParam, nullptr) == invalidAddr); + const JpegDecParseParam emptyParam{nullptr, 0, 0, 1}; + Require(sceJpegDecParseHeader(&emptyParam, &info) == invalidAddr); + for (const std::uint16_t downScale : {std::uint16_t{0}, std::uint16_t{3}, std::uint16_t{16}}) { + const JpegDecParseParam unknownScaleParse{jpeg.data(), static_cast(jpeg.size()), 0, downScale}; + Require(sceJpegDecParseHeader(&unknownScaleParse, &info) == invalidParam); + } + const JpegDecParseParam otherModeParse{jpeg.data(), static_cast(jpeg.size()), 1, 1}; + Require(ThrowsRuntimeError([&] { sceJpegDecParseHeader(&otherModeParse, &info); })); + for (const auto [downScale, outputWidth, outputHeight] : {std::array{2, 4, 3}, std::array{4, 2, 2}, std::array{8, 1, 1}}) { + const JpegDecParseParam downScaledParse{jpeg.data(), static_cast(jpeg.size()), 0, static_cast(downScale)}; + info = {}; + Require(sceJpegDecParseHeader(&downScaledParse, &info) == 0); + Require(info.image_width == 7 && info.image_height == 5 && info.component_number == 3); + Require(info.output_image_width == outputWidth && info.output_image_height == outputHeight); + } + const std::vector grayJpeg = TestJpeg(3, 2, 1); + const JpegDecParseParam grayParse{grayJpeg.data(), static_cast(grayJpeg.size()), 0, 1}; + Require(sceJpegDecParseHeader(&grayParse, &info) == 0); + Require(info.image_width == 3 && info.image_height == 2 && info.component_number == 1); + Require(info.color_space == 2 && info.component_id[0] == 1 && info.sampling_factor[0] == 0x11); + Require(info.component_id[1] == 0 && info.sampling_factor[1] == 0); + Require(sceJpegDecParseHeader(&parseParam, &info) == 0); + Require(info.image_width == 7 && info.image_height == 5 && info.component_number == 3); + Require(info.output_image_width == 7 && info.output_image_height == 5); + Require(info.coefficient_mem_size == 0 && info.suitable_csc_attribute == 1); + Require(info.color_space == 1); + for (std::uint32_t i = 0; i < 3; ++i) Require(info.component_id[i] == i + 1 && info.sampling_factor[i] == 0x11); + std::vector progressiveHeader = jpeg; + bool changedFrame = false; + for (std::size_t offset = 2; offset + 3 < progressiveHeader.size();) { + const std::uint8_t marker = progressiveHeader[offset + 1]; + const std::size_t length = (static_cast(progressiveHeader[offset + 2]) << 8) | progressiveHeader[offset + 3]; + if (marker == 0xC0) { + progressiveHeader[offset + 1] = 0xC2; + changedFrame = true; + break; + } + offset += 2 + length; + } + Require(changedFrame); + const JpegDecParseParam progressiveParse{progressiveHeader.data(), static_cast(progressiveHeader.size()), 0, 1}; + Require(sceJpegDecParseHeader(&progressiveParse, &info) == 0); + Require(info.coefficient_mem_size == 448); + Require(sceJpegDecParseHeader(&parseParam, &info) == 0); + + const JpegDecCreateParam param{sizeof(JpegDecCreateParam), info.suitable_csc_attribute, info.image_width}; const std::int32_t memorySize = sceJpegDecQueryMemorySize(¶m); - Require(memorySize > 0); - Require(sceJpegDecQueryMemorySize(nullptr) == invalidParam); - const JpegDecCreateParam badSize{sizeof(JpegDecCreateParam) - 1, 0}; + Require(memorySize == 32128); + const JpegDecCreateParam fullParam{sizeof(JpegDecCreateParam), 2, info.image_width}; + const std::int32_t fullMemorySize = sceJpegDecQueryMemorySize(&fullParam); + Require(fullMemorySize == 37504); + for (const std::uint32_t width : {0u, 65536u, UINT32_MAX}) { + const JpegDecCreateParam badWidth{sizeof(JpegDecCreateParam), 1, width}; + Require(sceJpegDecQueryMemorySize(&badWidth) == invalidParam); + } + const JpegDecCreateParam largestWidth{sizeof(JpegDecCreateParam), 1, 65535}; + Require(sceJpegDecQueryMemorySize(&largestWidth) == 3569344); + Require(sceJpegDecQueryMemorySize(nullptr) == invalidAddr); + const JpegDecCreateParam badSize{sizeof(JpegDecCreateParam) - 4, 1, 7}; Require(sceJpegDecQueryMemorySize(&badSize) == invalidSize); - const JpegDecCreateParam badAttribute{sizeof(JpegDecCreateParam), 1}; - Require(sceJpegDecQueryMemorySize(&badAttribute) == invalidParam); + for (const std::uint32_t attribute : {0u, 3u}) { + const JpegDecCreateParam badAttribute{sizeof(JpegDecCreateParam), attribute, 7}; + Require(sceJpegDecQueryMemorySize(&badAttribute) == invalidParam); + } - std::vector memory(static_cast(memorySize) + 8); + std::vector memory(static_cast(fullMemorySize) + 32); void* handle = nullptr; - Require(sceJpegDecCreate(¶m, memory.data(), 0, &handle) == invalidWorkMemory); + Require(sceJpegDecCreate(¶m, memory.data(), 0, &handle) == invalidSize); Require(sceJpegDecCreate(¶m, memory.data(), static_cast(memorySize), nullptr) == invalidAddr); - Require(sceJpegDecCreate(¶m, memory.data(), static_cast(memorySize), &handle) == 0); - Require(handle != nullptr); + Require(sceJpegDecCreate(&fullParam, memory.data(), static_cast(fullMemorySize), &handle) == 0); + Require(handle != nullptr && reinterpret_cast(handle) % 32 == 0); + Require(sceJpegDecCreate(¶m, memory.data(), 0, nullptr) == invalidSize); Require(sceJpegDecDelete(nullptr) == invalidHandle); Require(sceJpegDecDelete(handle) == 0); Require(sceJpegDecCreate(¶m, memory.data(), static_cast(memorySize), &handle) == 0); - const std::vector jpeg = TestJpeg(7, 5); - JpegDecImageInfo info{}; - const JpegDecParseParam parseParam{jpeg.data(), static_cast(jpeg.size()), 0}; - Require(sceJpegDecParseHeader(nullptr, &info) == invalidParam); - Require(sceJpegDecParseHeader(&parseParam, nullptr) == invalidAddr); - const JpegDecParseParam emptyParam{nullptr, 0, 0}; - Require(sceJpegDecParseHeader(&emptyParam, &info) == invalidAddr); - Require(sceJpegDecParseHeader(&parseParam, &info) == 0); - Require(info.image_width == 7); - Require(info.image_height == 5); + const std::optional reference = Decoder::Jpeg::Decode(jpeg); + Require(reference && reference->width == 7 && reference->height == 5 && reference->channels == 3); std::vector image(7 * 5 * 4, 0xCC); - JpegDecDecodeParam decodeParam = DecodeParam(jpeg, image); + JpegDecDecodeParam decodeParam = DecodeParam(jpeg, image, 7 * 4); + std::vector tallSampling = TestJpeg(31, 5, 3); + for (std::size_t offset = 2; offset + 3 < tallSampling.size();) { + const std::uint8_t marker = tallSampling[offset + 1]; + const std::size_t length = (static_cast(tallSampling[offset + 2]) << 8) | tallSampling[offset + 3]; + if (marker == 0xC0) { + tallSampling[offset + 11] = 0x14; + break; + } + offset += 2 + length; + } + std::vector tallSamplingOutput(31 * 5 * 4); + JpegDecDecodeParam tallSamplingDecode = DecodeParam(tallSampling, tallSamplingOutput, 0); + Require(sceJpegDecDecode(handle, &tallSamplingDecode, &info) == static_cast(0x80650006)); + JpegDecDecodeParam progressiveDecode = DecodeParam(progressiveHeader, image, 7 * 4); + Require(sceJpegDecDecode(handle, &progressiveDecode, &info) == static_cast(0x80650005)); + std::array insufficientCoefficients{}; + progressiveDecode.coefficient_mem_addr = insufficientCoefficients.data(); + progressiveDecode.coefficient_mem_size = static_cast(insufficientCoefficients.size()); + Require(sceJpegDecDecode(handle, &progressiveDecode, &info) == static_cast(0x80650005)); Require(sceJpegDecDecode(nullptr, &decodeParam, &info) == invalidHandle); - Require(sceJpegDecDecode(handle, nullptr, &info) == invalidParam); + Require(sceJpegDecDecode(handle, nullptr, &info) == invalidAddr); JpegDecDecodeParam badFormat = decodeParam; badFormat.pixel_format = 2; Require(sceJpegDecDecode(handle, &badFormat, &info) == invalidParam); - const std::int32_t packed = sceJpegDecDecode(handle, &decodeParam, &info); - Require(packed == static_cast(7 << 16 | 5)); - Require(info.image_width == 7); - Require(info.image_height == 5); - Require(image[3] == 0xFF); + JpegDecDecodeParam unknownScale = decodeParam; + unknownScale.down_scale = 3; + Require(sceJpegDecDecode(handle, &unknownScale, &info) == invalidParam); + JpegDecDecodeParam invalidOptions = decodeParam; + invalidOptions.alpha_value = 256; + Require(sceJpegDecDecode(handle, &invalidOptions, &info) == invalidParam); + invalidOptions = decodeParam; + invalidOptions.decode_mode = 2; + Require(sceJpegDecDecode(handle, &invalidOptions, &info) == invalidParam); + invalidOptions = decodeParam; + invalidOptions.image_mem_addr = image.data() + 1; + Require(sceJpegDecDecode(handle, &invalidOptions, &info) == invalidAddr); + std::vector invalidAlphaImage(7 * 5 * 4, 0xCC); + JpegDecDecodeParam invalidAlpha = DecodeParam(jpeg, invalidAlphaImage, 7 * 4); + invalidAlpha.alpha_value = 0x100; + Require(sceJpegDecDecode(handle, &invalidAlpha, &info) == invalidParam); + Require(std::all_of(invalidAlphaImage.begin(), invalidAlphaImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + invalidAlpha.alpha_value = 0xFFFF; + Require(sceJpegDecDecode(handle, &invalidAlpha, &info) == invalidParam); + Require(std::all_of(invalidAlphaImage.begin(), invalidAlphaImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + std::vector misalignedImage(7 * 5 * 4 + 1, 0xCC); + JpegDecDecodeParam misaligned = DecodeParam(jpeg, misalignedImage, 7 * 4); + misaligned.image_mem_addr = misalignedImage.data() + 1; + misaligned.image_mem_size = static_cast(misalignedImage.size() - 1); + Require(sceJpegDecDecode(handle, &misaligned, &info) == invalidAddr); + Require(std::all_of(misalignedImage.begin(), misalignedImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + JpegDecDecodeParam otherMode = decodeParam; + otherMode.decode_mode = 1; + Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &otherMode, &info); })); + info = {}; + Require(sceJpegDecDecode(handle, &decodeParam, &info) == static_cast(7 << 16 | 5)); + Require(info.image_width == 7 && info.image_height == 5); + Require(info.output_image_width == 7 && info.output_image_height == 5); + for (std::size_t pixel = 0; pixel < 7 * 5; ++pixel) { + for (std::size_t c = 0; c < 3; ++c) Require(image[pixel * 4 + c] == reference->pixels[pixel * 3 + c]); + Require(image[pixel * 4 + 3] == 0xFF); + } - std::vector bgra(7 * 5 * 4, 0); - JpegDecDecodeParam bgraParam = DecodeParam(jpeg, bgra); + constexpr std::uint32_t pitch = 7 * 4 + 8; + std::vector bgra(5 * pitch, 0xEE); + JpegDecDecodeParam bgraParam = DecodeParam(jpeg, bgra, pitch); bgraParam.pixel_format = 1; + bgraParam.alpha_value = 0x77; Require(sceJpegDecDecode(handle, &bgraParam, nullptr) == static_cast(7 << 16 | 5)); - Require(bgra[0] == image[2]); - Require(bgra[2] == image[0]); + for (std::size_t y = 0; y < 5; ++y) { + for (std::size_t x = 0; x < 7; ++x) { + const std::uint8_t* pixel = &bgra[y * pitch + x * 4]; + const std::uint8_t* rgba = &image[(y * 7 + x) * 4]; + Require(pixel[0] == rgba[2] && pixel[1] == rgba[1] && pixel[2] == rgba[0] && pixel[3] == 0x77); + } + } + for (std::size_t y = 0; y < 4; ++y) { + for (std::size_t x = 7 * 4; x < pitch; ++x) Require(bgra[y * pitch + x] == 0xEE); + } + + const std::vector large = TestJpeg(37, 21, 3); + std::vector wideOutput(37 * 21 * 4); + JpegDecDecodeParam wideParam = DecodeParam(large, wideOutput, 0); + Require(sceJpegDecDecode(handle, &wideParam, nullptr) == static_cast(0x80650006)); + const std::vector largeGray = TestJpeg(19, 11, 1); + for (const auto* source : {&large, &largeGray}) { + const std::optional sourceHeader = Decoder::Jpeg::ParseHeader(*source); + Require(sourceHeader.has_value()); + for (const std::uint16_t downScale : {std::uint16_t{2}, std::uint16_t{4}, std::uint16_t{8}}) { + const std::uint32_t width = (sourceHeader->width + downScale - 1) / downScale; + const std::uint32_t height = (sourceHeader->height + downScale - 1) / downScale; + const std::optional scaled = Decoder::Jpeg::DecodeScaled(*source, downScale); + Require(scaled && scaled->width == width && scaled->height == height); + + std::vector exact(static_cast(width) * height * 4, 0xCC); + JpegDecDecodeParam scaledParam = DecodeParam(*source, exact, 0); + scaledParam.down_scale = downScale; + info = {}; + Require(sceJpegDecDecode(handle, &scaledParam, &info) == static_cast(width << 16 | height)); + Require(info.image_width == sourceHeader->width && info.image_height == sourceHeader->height); + Require(info.output_image_width == width && info.output_image_height == height); + for (std::size_t pixel = 0; pixel < static_cast(width) * height; ++pixel) { + for (std::size_t c = 0; c < 3; ++c) Require(exact[pixel * 4 + c] == scaled->pixels[pixel * scaled->channels + (scaled->channels == 1 ? 0 : c)]); + Require(exact[pixel * 4 + 3] == 0xFF); + } + + const std::uint32_t paddedPitch = width * 4 + 4; + std::vector padded(height * paddedPitch, 0xEE); + JpegDecDecodeParam paddedParam = DecodeParam(*source, padded, paddedPitch); + paddedParam.down_scale = downScale; + paddedParam.pixel_format = 1; + Require(sceJpegDecDecode(handle, &paddedParam, nullptr) == static_cast(width << 16 | height)); + for (std::size_t y = 0; y < height; ++y) { + for (std::size_t x = 0; x < width; ++x) { + const std::uint8_t* pixel = &padded[y * paddedPitch + x * 4]; + const std::uint8_t* rgba = &exact[(y * width + x) * 4]; + Require(pixel[0] == rgba[2] && pixel[1] == rgba[1] && pixel[2] == rgba[0] && pixel[3] == 0xFF); + } + if (y + 1 < height) for (std::size_t x = width * 4; x < paddedPitch; ++x) Require(padded[y * paddedPitch + x] == 0xEE); + } + + std::vector small(exact.size() - 1, 0); + JpegDecDecodeParam smallParam = DecodeParam(*source, small, 0); + smallParam.down_scale = downScale; + Require(sceJpegDecDecode(handle, &smallParam, &info) == invalidSize); + JpegDecDecodeParam narrowParam = DecodeParam(*source, exact, width * 4 - 1); + narrowParam.down_scale = downScale; + Require(sceJpegDecDecode(handle, &narrowParam, &info) == invalidParam); + } + } const std::vector garbage{0x00, 0x01, 0x02, 0x03}; - JpegDecParseParam garbageParam{garbage.data(), static_cast(garbage.size()), 0}; + const JpegDecParseParam garbageParam{garbage.data(), static_cast(garbage.size()), 0, 1}; Require(sceJpegDecParseHeader(&garbageParam, &info) == invalidData); std::vector garbageOut(64, 0); - JpegDecDecodeParam garbageDecode = DecodeParam(garbage, garbageOut); + const JpegDecDecodeParam garbageDecode = DecodeParam(garbage, garbageOut, 4 * 4); Require(sceJpegDecDecode(handle, &garbageDecode, &info) == invalidData); + JpegDecDecodeParam garbageScaled = garbageDecode; + garbageScaled.down_scale = 2; + Require(sceJpegDecDecode(handle, &garbageScaled, &info) == invalidData); Require(sceJpegDecDelete(handle) == 0); return 0; diff --git a/core/libs/tests/GuestJpegEnc.cpp b/core/libs/tests/GuestJpegEnc.cpp index 52437df7a5..8206423e5f 100644 --- a/core/libs/tests/GuestJpegEnc.cpp +++ b/core/libs/tests/GuestJpegEnc.cpp @@ -278,8 +278,9 @@ int main() { Require(encodeWith([](JpegEncEncodeParam& p) { p.image_width = 0; }) == invalidParam); Require(ThrowsRuntimeError([&] { encodeWith([](JpegEncEncodeParam& p) { p.encode_mode = 1; }); })); - Require(ThrowsRuntimeError([&] { encodeWith([](JpegEncEncodeParam& p) { p.restart_interval = -2; }); })); - Require(ThrowsRuntimeError([&] { encodeWith([](JpegEncEncodeParam& p) { p.restart_interval = INT32_MIN; }); })); + Require(encodeWith([](JpegEncEncodeParam& p) { p.restart_interval = -2; }) == invalidParam); + Require(encodeWith([](JpegEncEncodeParam& p) { p.restart_interval = INT32_MIN; }) == invalidParam); + Require(encodeWith([](JpegEncEncodeParam& p) { p.encode_mode = 1; p.restart_interval = -2; }) == invalidParam); constexpr std::uint32_t wideWidth = 48; constexpr std::uint32_t wideHeight = 32; diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index eb29e781ef..b2951558b6 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include +#include #include #include #include @@ -7,6 +9,7 @@ #include #include #include +#include #include struct Value { void* node; }; @@ -21,6 +24,9 @@ struct Pair { }; using NullAccessCallback = const Value& (APS5_VABI*)(std::int32_t, const Value*, void*); using SetNullAccessCallback = int (APS5_VABI*)(void*, NullAccessCallback, void*); +using TypeMismatchHandler = const Value& (APS5_VABI*)(std::int32_t, const Value*, const Value*, void*); +using SpecialFloatHandler = Value* (APS5_VABI*)(Value*, std::int32_t, double, const Value*, void*, bool*); +using ElementAccessFailureHandler = void (APS5_VABI*)(std::int32_t, const String*, const std::size_t*, const Value*, const Value*, void*); extern "C" { int APS5_VABI _ZN3sce4Json11InitializerC1Ev(void*); @@ -29,11 +35,20 @@ int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_13InitParameterE(void* int APS5_VABI _ZN3sce4Json11Initializer9terminateEv(void*); int APS5_VABI _ZN3sce4Json11Initializer27setGlobalNullAccessCallBackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_(void*, NullAccessCallback, void*); int APS5_VABI _ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_(void*, NullAccessCallback, void*); +void APS5_VABI _ZN3sce4Json5Value21setNullAccessCallBackEPFRKS1_NS0_9ValueTypeEPS2_PvES6_(NullAccessCallback, void*); +int APS5_VABI _ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEPS2_S5_PvES6_(Value*, TypeMismatchHandler, void*); +int APS5_VABI _ZN3sce4Json5Value22setSpecialFloatHandlerEPFKS1_NS0_12FunctionTypeEdPS2_PvPbES5_(Value*, SpecialFloatHandler, void*); +int APS5_VABI _ZN3sce4Json5Value30setElementAccessFailureHandlerEPFviPKNS0_6StringEPKmPKS1_S8_PvES9_(Value*, ElementAccessFailureHandler, void*); +int APS5_VABI _ZN3sce4Json5Value3setEd(Value*, double); +int APS5_VABI _ZN3sce4Json5Value3setERKS1_(Value*, const Value*); +Value* APS5_VABI _ZN3sce4Json5ValueaSERKS1_(Value*, const Value*); +void APS5_VABI _ZN3sce4Json5ValueC1ERKS1_(Value*, const Value*); void APS5_VABI _ZN3sce4Json6StringC1Ev(String*); void APS5_VABI _ZN3sce4Json6StringC1EPKc(String*, const char*); void APS5_VABI _ZN3sce4Json6StringD1Ev(String*); const char* APS5_VABI _ZNK3sce4Json6String5c_strEv(const String*); std::size_t APS5_VABI _ZNK3sce4Json6String6lengthEv(const String*); +bool APS5_VABI _ZNK3sce4Json6String5emptyEv(const String*); void APS5_VABI _ZN3sce4Json5ArrayC1Ev(Array*); void APS5_VABI _ZN3sce4Json5ArrayC1ERKS1_(Array*, const Array*); void APS5_VABI _ZN3sce4Json5Array5clearEv(Array*); @@ -45,6 +60,8 @@ Iterator* APS5_VABI _ZNK3sce4Json5Array5beginEv(Iterator*, const Array*); Iterator* APS5_VABI _ZNK3sce4Json5Array3endEv(Iterator*, const Array*); std::size_t APS5_VABI _ZNK3sce4Json5Array4sizeEv(const Array*); const Value* APS5_VABI _ZNK3sce4Json5Array4backEv(const Array*); +void APS5_VABI _ZN3sce4Json5Array9pop_frontEv(Array*); +bool APS5_VABI _ZNK3sce4Json5Array5emptyEv(const Array*); void APS5_VABI _ZN3sce4Json5Array8iteratorD1Ev(Iterator*); Iterator* APS5_VABI _ZN3sce4Json5Array8iteratorppEv(Iterator*); Value* APS5_VABI _ZNK3sce4Json5Array8iteratordeEv(const Iterator*); @@ -60,7 +77,25 @@ Pair* APS5_VABI _ZNK3sce4Json6Object8iteratordeEv(const Iterator*); Pair* APS5_VABI _ZNK3sce4Json6Object8iteratorptEv(const Iterator*); Iterator* APS5_VABI _ZNK3sce4Json6Object4findERKNS0_6StringE(Iterator*, const Object*, const String*); int APS5_VABI _ZNK3sce4Json6String7compareEPKc(const String*, const char*); +int APS5_VABI _ZNK3sce4Json6String7compareERKS1_(const String*, const String*); +std::size_t APS5_VABI _ZNK3sce4Json6String4sizeEv(const String*); +std::size_t APS5_VABI _ZNK3sce4Json6String8capacityEv(const String*); +bool APS5_VABI _ZNK3sce4Json6StringeqEPKc(const String*, const char*); +bool APS5_VABI _ZNK3sce4Json6StringeqERKS1_(const String*, const String*); +extern std::size_t _ZN3sce4Json6String4nposE; +void APS5_VABI _ZN3sce4Json6String5clearEv(String*); +void APS5_VABI _ZN3sce4Json6String6resizeEm(String*, std::size_t); +void APS5_VABI _ZN3sce4Json6String7reserveEm(String*, std::size_t); +String* APS5_VABI _ZN3sce4Json6String6appendERKS1_(String*, const String*); +String* APS5_VABI _ZN3sce4Json6String6appendEPKc(String*, const char*); +String* APS5_VABI _ZN3sce4Json6String6appendEPKcm(String*, const char*, std::size_t); +String* APS5_VABI _ZN3sce4Json6StringpLEPKc(String*, const char*); +String* APS5_VABI _ZN3sce4Json6StringpLEh(String*, unsigned char); +String* APS5_VABI _ZNK3sce4Json6String6substrEmm(String*, const String*, std::size_t, std::size_t); +std::size_t APS5_VABI _ZNK3sce4Json6String4findEcm(const String*, char, std::size_t); +std::size_t APS5_VABI _ZNK3sce4Json6String5rfindEcm(const String*, char, std::size_t); bool APS5_VABI _ZNK3sce4Json6Object8iteratorneERKS2_(const Iterator*, const Iterator*); +bool APS5_VABI _ZNK3sce4Json6Object8iteratoreqERKS2_(const Iterator*, const Iterator*); void APS5_VABI _ZN3sce4Json5ValueC1Ev(Value*); void APS5_VABI _ZN3sce4Json5ValueC1Eb(Value*, bool); void APS5_VABI _ZN3sce4Json5ValueC1Ed(Value*, double); @@ -68,6 +103,7 @@ void APS5_VABI _ZN3sce4Json5ValueC1ERKNS0_6ObjectE(Value*, const Object*); void APS5_VABI _ZN3sce4Json5ValueD1Ev(Value*); void APS5_VABI _ZN3sce4Json5Value5clearEv(Value*); int APS5_VABI _ZN3sce4Json5Value3setEl(Value*, std::int64_t); +int APS5_VABI _ZN3sce4Json5Value3setEm(Value*, std::uint64_t); int APS5_VABI _ZN3sce4Json5Value3setEPKc(Value*, const char*); std::int32_t APS5_VABI _ZNK3sce4Json5Value7getTypeEv(const Value*); const bool* APS5_VABI _ZNK3sce4Json5Value10getBooleanEv(const Value*); @@ -98,6 +134,24 @@ int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(void void APS5_VABI _ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv(void*, void*, void*); void APS5_VABI _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE(void*, std::int32_t); void APS5_VABI _ZN3sce4Json17InitParameterRttiC1EPNS0_14AllocParamRttiEPvm(void*, void*, void*, std::size_t); +void APS5_VABI _ZN3sce4Json6Object4PairC1Ev(Pair*); +void APS5_VABI _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(Pair*, const String*, const Value*); +void APS5_VABI _ZN3sce4Json6Object4PairD1Ev(Pair*); +Iterator* APS5_VABI _ZN3sce4Json6Object6insertERKNS1_4PairE(Iterator*, Object*, const Pair*); +void APS5_VABI _ZN3sce4Json6Object8iteratorC1Ev(Iterator*); +Iterator* APS5_VABI _ZN3sce4Json6Object8iteratoraSERKS2_(Iterator*, const Iterator*); +Iterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEi(Iterator*, Iterator*, int); +void APS5_VABI _ZN3sce4Json5Array8iteratorC1Ev(Iterator*); +Iterator* APS5_VABI _ZN3sce4Json5Array8iteratoraSERKS2_(Iterator*, const Iterator*); +Iterator* APS5_VABI _ZN3sce4Json5Array8iteratorppEi(Iterator*, Iterator*, int); +void APS5_VABI _ZN3sce4Json12MemAllocator11notifyErrorEimPv(void*, int, std::size_t, void*); +bool APS5_VABI _ZNK3sce4Json5ValuecvbEv(const Value*); +void APS5_VABI _ZN3sce4Json5Value4swapERS1_(Value*, Value*); +void APS5_VABI _ZN3sce4Json5Array8iteratorC1ERKS2_(Iterator*, const Iterator*); +Iterator* APS5_VABI _ZN3sce4Json5Array8iterator7advanceEm(Iterator*, std::size_t); +void APS5_VABI _ZN3sce4Json6Object8iteratorC1ERKS2_(Iterator*, const Iterator*); +Iterator* APS5_VABI _ZN3sce4Json6Object8iterator7advanceEm(Iterator*, std::size_t); +void APS5_VABI _ZN3sce4Json6Object5eraseERKNS0_6StringE(Object*, const String*); } static void Check(bool value, int line) { @@ -109,6 +163,7 @@ static void Check(bool value, int line) { #define Require(value) Check((value), __LINE__) enum : std::int32_t { TypeNull, TypeBoolean, TypeInteger, TypeUInteger, TypeReal, TypeString, TypeArray, TypeObject }; +constexpr int JsonErrorParse = static_cast(0x80848101); static Value fallback{}; static int callbackCalls = 0; @@ -147,7 +202,7 @@ static void ParseAndRoundTrip() { Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject); Require(_ZNK3sce4Json5Value5countEv(&root) == 8); Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(_ZNK3sce4Json5Value9getStringEv(&Member(root, "name"))), "x\xc3\xa9\n") == 0); - Require(_ZNK3sce4Json5Value7getTypeEv(&Member(root, "count")) == TypeInteger); + Require(_ZNK3sce4Json5Value7getTypeEv(&Member(root, "count")) == TypeUInteger); Require(*_ZNK3sce4Json5Value10getIntegerEv(&Member(root, "count")) == 3); Require(*_ZNK3sce4Json5Value11getUIntegerEv(&Member(root, "count")) == 3); Require(*_ZNK3sce4Json5Value7getRealEv(&Member(root, "count")) == 3.0); @@ -178,22 +233,32 @@ static void ParseAndRoundTrip() { types.push_back(_ZNK3sce4Json5Value7getTypeEv(_ZNK3sce4Json5Array8iteratordeEv(&it))); _ZN3sce4Json5Array8iteratorD1Ev(&it); _ZN3sce4Json5Array8iteratorD1Ev(&end); - Require((types == std::vector{TypeInteger, TypeString, TypeArray})); + Require((types == std::vector{TypeUInteger, TypeString, TypeArray})); Require(*_ZNK3sce4Json5Value10getIntegerEv(_ZNK3sce4Json5ValueixEm(_ZNK3sce4Json5ValueixEm(&list, 2), 0)) == 3); const std::string serialized = Serialize(root); - Require(serialized == "{\"name\":\"x\xc3\xa9\\n\",\"count\":3,\"big\":18446744073709551615,\"neg\":-5," - "\"pi\":1.5,\"ok\":true,\"none\":null,\"list\":[1,\"two\",[3]]}"); + Require(serialized == "{\"big\":18446744073709551615,\"count\":3,\"list\":[1,\"two\",[3]]," + "\"name\":\"x\xc3\xa9\\n\",\"neg\":-5,\"none\":null,\"ok\":true,\"pi\":1.5}"); Value reparsed{}; _ZN3sce4Json5ValueC1Ev(&reparsed); Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&reparsed, serialized.c_str(), serialized.size()) == 0); Require(Serialize(reparsed) == serialized); _ZN3sce4Json5ValueD1Ev(&reparsed); - for (const char* invalid : {"{\"a\":}", "[1,]", "\"open", "tru", "{} x", "01"}) { - Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, invalid, std::strlen(invalid)) < 0); + for (const char* invalid : {"{\"a\":}", "[1,]", "\"open", "tru"}) { + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, invalid, std::strlen(invalid)) == JsonErrorParse); Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject && _ZNK3sce4Json5Value5countEv(&root) == 8); } + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, nullptr, 0) == JsonErrorParse); + bool nullTextRejected = false; + try { + _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, nullptr, 10); + } catch (const std::invalid_argument&) { + nullTextRejected = true; + } + Require(nullTextRejected); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "", 0) == JsonErrorParse); + Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject && _ZNK3sce4Json5Value5countEv(&root) == 8); for (const char* overflow : {"1e309", "[-1e309]", "{\"a\":[1,1e400]}"}) { bool unverified = false; try { @@ -297,10 +362,100 @@ static void ObjectsAndArrays() { Require(_ZNK3sce4Json5Array4sizeEv(©) == 2); Require(*_ZNK3sce4Json5Value10getBooleanEv(_ZNK3sce4Json5Array5frontEv(©))); Require(*_ZNK3sce4Json5Value7getRealEv(_ZNK3sce4Json5Array4backEv(©)) == 2.5); + _ZN3sce4Json5Array9pop_frontEv(©); + Require(_ZNK3sce4Json5Array4sizeEv(©) == 1); + Require(*_ZNK3sce4Json5Value7getRealEv(_ZNK3sce4Json5Array5frontEv(©)) == 2.5); + _ZN3sce4Json5Array9pop_frontEv(©); + Require(_ZNK3sce4Json5Array5emptyEv(©)); + threw = false; + try { + _ZN3sce4Json5Array9pop_frontEv(©); + } catch (const std::out_of_range&) { + threw = true; + } + Require(threw && _ZNK3sce4Json5Array5emptyEv(©)); _ZN3sce4Json5ArrayD1Ev(©); _ZN3sce4Json5ArrayD1Ev(&array); } +static void ObjectPair() { + String key{}; + _ZN3sce4Json6StringC1EPKc(&key, "answer"); + Value value{}; + _ZN3sce4Json5ValueC1Ev(&value); + Require(_ZN3sce4Json5Value3setEPKc(&value, "forty-two") == 0); + + Pair pair{}; + std::memset(&pair, 0x5a, sizeof(pair)); + _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&pair, &key, &value); + Require(pair.reserved == 0); + Require(pair.key.text != key.text && pair.value.node != value.node); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&pair.key), "answer") == 0); + Require(_ZNK3sce4Json5Value7getTypeEv(&pair.value) == TypeString); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(_ZNK3sce4Json5Value9getStringEv(&pair.value)), "forty-two") == 0); + + Require(_ZN3sce4Json5Value3setEl(&value, 7) == 0); + _ZN3sce4Json6StringD1Ev(&key); + _ZN3sce4Json5ValueD1Ev(&value); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&pair.key), "answer") == 0); + Require(_ZNK3sce4Json5Value7getTypeEv(&pair.value) == TypeString); + _ZN3sce4Json6Object4PairD1Ev(&pair); + Require(pair.key.text == nullptr && pair.value.node == nullptr); +} + +static void ArrayIteratorCopies() { + Array array{}; + _ZN3sce4Json5ArrayC1Ev(&array); + for (std::int64_t i = 1; i <= 3; ++i) { + Value item{}; + _ZN3sce4Json5ValueC1Ev(&item); + Require(_ZN3sce4Json5Value3setEl(&item, i) == 0); + _ZN3sce4Json5Array9push_backERKNS0_5ValueE(&array, &item); + _ZN3sce4Json5ValueD1Ev(&item); + } + + Iterator it{}; + std::memset(&it, 0x5a, sizeof(it)); + _ZN3sce4Json5Array8iteratorC1Ev(&it); + Require(it.position != nullptr); + + Iterator begin{}, end{}; + _ZNK3sce4Json5Array5beginEv(&begin, &array); + _ZNK3sce4Json5Array3endEv(&end, &array); + Require(_ZN3sce4Json5Array8iteratoraSERKS2_(&it, &begin) == &it); + Require(!_ZNK3sce4Json5Array8iteratorneERKS2_(&it, &begin)); + + std::vector seen; + Iterator previous{}; + while (_ZNK3sce4Json5Array8iteratorneERKS2_(&it, &end)) { + _ZN3sce4Json5Array8iteratorD1Ev(&previous); + Require(_ZN3sce4Json5Array8iteratorppEi(&previous, &it, 0) == &previous); + seen.push_back(*_ZNK3sce4Json5Value10getIntegerEv(_ZNK3sce4Json5Array8iteratordeEv(&previous))); + Require(_ZNK3sce4Json5Array8iteratorneERKS2_(&previous, &it)); + } + Require((seen == std::vector{1, 2, 3})); + Require(!_ZNK3sce4Json5Array8iteratorneERKS2_(&it, &end)); + Require(*_ZNK3sce4Json5Value10getIntegerEv(_ZNK3sce4Json5Array8iteratordeEv(&previous)) == 3); + Require(_ZN3sce4Json5Array8iteratoraSERKS2_(&it, &end) == &it); + Require(it.position == nullptr); + Require(_ZN3sce4Json5Array8iteratoraSERKS2_(&it, &begin) == &it); + Require(it.position == nullptr); + Require(!_ZNK3sce4Json5Array8iteratorneERKS2_(&it, &begin)); + + _ZN3sce4Json5Array8iteratorD1Ev(&previous); + _ZN3sce4Json5Array8iteratorD1Ev(&it); + _ZN3sce4Json5Array8iteratorD1Ev(&begin); + _ZN3sce4Json5Array8iteratorD1Ev(&end); + _ZN3sce4Json5ArrayD1Ev(&array); +} + +static void AllocatorNotifyError() { + int allocator = 0x1234; + int userData = 0x5678; + _ZN3sce4Json12MemAllocator11notifyErrorEimPv(&allocator, -1, 64, &userData); + Require(allocator == 0x1234 && userData == 0x5678); +} + static void ObjectFind() { Object object{}; _ZN3sce4Json6ObjectC1Ev(&object); @@ -319,6 +474,7 @@ static void ObjectFind() { Value* second = _ZN3sce4Json6ObjectixERKNS0_6StringE(&object, &beta); Require(_ZN3sce4Json5Value3setEPKc(second, "b") == 0); _ZNK3sce4Json6Object3endEv(&end, &object); + _ZN3sce4Json6Object8iteratorD1Ev(&found); Iterator* returned = _ZNK3sce4Json6Object4findERKNS0_6StringE(&found, &object, &beta); Require(returned == &found); Require(_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); @@ -328,6 +484,7 @@ static void ObjectFind() { Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&pair->key), "beta") == 0); _ZN3sce4Json6Object8iteratorppEv(&found); Require(!_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); + _ZN3sce4Json6Object8iteratorD1Ev(&found); _ZNK3sce4Json6Object4findERKNS0_6StringE(&found, &object, &gamma); Require(!_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); Require(_ZNK3sce4Json6Object4sizeEv(&object) == 2); @@ -351,6 +508,348 @@ static void ObjectFind() { _ZN3sce4Json6ObjectD1Ev(&object); } +static void ObjectIteratorCopies() { + Object object{}; + _ZN3sce4Json6ObjectC1Ev(&object); + String key1{}, key2{}; + _ZN3sce4Json6StringC1EPKc(&key1, "one"); + _ZN3sce4Json6StringC1EPKc(&key2, "two"); + Value val1{}, val2{}; + _ZN3sce4Json5ValueC1Ev(&val1); + _ZN3sce4Json5ValueC1Ev(&val2); + Require(_ZN3sce4Json5Value3setEl(&val1, 10) == 0); + Require(_ZN3sce4Json5Value3setEl(&val2, 20) == 0); + Pair p1{}, p2{}; + _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&p1, &key1, &val1); + _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&p2, &key2, &val2); + Iterator ins1{}, ins2{}; + Require(_ZN3sce4Json6Object6insertERKNS1_4PairE(&ins1, &object, &p1) == &ins1); + Require(_ZN3sce4Json6Object6insertERKNS1_4PairE(&ins2, &object, &p2) == &ins2); + _ZN3sce4Json6Object8iteratorD1Ev(&ins1); + _ZN3sce4Json6Object8iteratorD1Ev(&ins2); + + Iterator it{}; + std::memset(&it, 0x5a, sizeof(it)); + _ZN3sce4Json6Object8iteratorC1Ev(&it); + Require(it.position != nullptr); + _ZN3sce4Json6Object8iteratorD1Ev(&it); + + Iterator begin{}, end{}; + _ZNK3sce4Json6Object5beginEv(&begin, &object); + _ZNK3sce4Json6Object3endEv(&end, &object); + + Require(!_ZNK3sce4Json6Object8iteratoreqERKS2_(&begin, &end)); + Require(_ZNK3sce4Json6Object8iteratorneERKS2_(&begin, &end)); + + Iterator post{}; + Require(_ZN3sce4Json6Object8iteratorppEi(&post, &begin, 0) == &post); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&_ZNK3sce4Json6Object8iteratordeEv(&post)->key), "one") == 0); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&_ZNK3sce4Json6Object8iteratordeEv(&begin)->key), "two") == 0); + _ZN3sce4Json6Object8iteratorD1Ev(&post); + + Iterator assigned{}; + _ZN3sce4Json6Object8iteratorC1Ev(&assigned); + Require(_ZN3sce4Json6Object8iteratoraSERKS2_(&assigned, &begin) == &assigned); + Require(_ZNK3sce4Json6Object8iteratoreqERKS2_(&assigned, &begin)); + Require(!_ZNK3sce4Json6Object8iteratorneERKS2_(&assigned, &begin)); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&_ZNK3sce4Json6Object8iteratordeEv(&assigned)->key), "two") == 0); + _ZN3sce4Json6Object8iteratorD1Ev(&assigned); + + _ZN3sce4Json6Object8iteratorD1Ev(&begin); + _ZN3sce4Json6Object8iteratorD1Ev(&end); + _ZN3sce4Json6Object4PairD1Ev(&p1); + _ZN3sce4Json6Object4PairD1Ev(&p2); + _ZN3sce4Json5ValueD1Ev(&val1); + _ZN3sce4Json5ValueD1Ev(&val2); + _ZN3sce4Json6StringD1Ev(&key1); + _ZN3sce4Json6StringD1Ev(&key2); + _ZN3sce4Json6ObjectD1Ev(&object); +} + +static void ObjectInsert() { + Object object{}; + _ZN3sce4Json6ObjectC1Ev(&object); + String key{}; + _ZN3sce4Json6StringC1EPKc(&key, "score"); + Value value{}; + _ZN3sce4Json5ValueC1Ev(&value); + Require(_ZN3sce4Json5Value3setEl(&value, 100) == 0); + Pair pair{}; + _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&pair, &key, &value); + + Iterator inserted{}, end{}; + _ZNK3sce4Json6Object3endEv(&end, &object); + Require(_ZN3sce4Json6Object6insertERKNS1_4PairE(&inserted, &object, &pair) == &inserted); + Require(_ZNK3sce4Json6Object8iteratorneERKS2_(&inserted, &end)); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 1); + const Pair* p = _ZNK3sce4Json6Object8iteratordeEv(&inserted); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&p->key), "score") == 0); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&p->value) == 100); + + Require(_ZN3sce4Json5Value3setEl(&pair.value, 200) == 0); + Iterator duplicate{}; + Require(_ZN3sce4Json6Object6insertERKNS1_4PairE(&duplicate, &object, &pair) == &duplicate); + Require(_ZNK3sce4Json6Object8iteratoreqERKS2_(&duplicate, &inserted)); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 1); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&_ZNK3sce4Json6Object8iteratordeEv(&duplicate)->value) == 100); + _ZN3sce4Json6Object8iteratorD1Ev(&duplicate); + + String otherKey{}; + _ZN3sce4Json6StringC1EPKc(&otherKey, "other"); + Value otherValue{}; + _ZN3sce4Json5ValueC1Ev(&otherValue); + Require(_ZN3sce4Json5Value3setEPKc(&otherValue, "str") == 0); + Pair otherPair{}; + _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&otherPair, &otherKey, &otherValue); + Iterator otherInserted{}; + Require(_ZN3sce4Json6Object6insertERKNS1_4PairE(&otherInserted, &object, &otherPair) == &otherInserted); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 2); + Iterator first{}; + _ZNK3sce4Json6Object5beginEv(&first, &object); + Require(_ZNK3sce4Json6Object8iteratoreqERKS2_(&first, &otherInserted)); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&_ZNK3sce4Json6Object8iteratordeEv(&inserted)->value) == 100); + _ZN3sce4Json6Object8iteratorD1Ev(&first); + + _ZN3sce4Json6Object8iteratorD1Ev(&otherInserted); + _ZN3sce4Json6Object4PairD1Ev(&otherPair); + _ZN3sce4Json5ValueD1Ev(&otherValue); + _ZN3sce4Json6StringD1Ev(&otherKey); + _ZN3sce4Json6Object8iteratorD1Ev(&inserted); + _ZN3sce4Json6Object8iteratorD1Ev(&end); + _ZN3sce4Json6Object4PairD1Ev(&pair); + _ZN3sce4Json5ValueD1Ev(&value); + _ZN3sce4Json6StringD1Ev(&key); + _ZN3sce4Json6ObjectD1Ev(&object); +} + +static void ObjectKeyOrder() { + Object object{}; + _ZN3sce4Json6ObjectC1Ev(&object); + for (const char* name : {"z", "\xc3\xa9", "a", "A", ""}) { + String key{}; + _ZN3sce4Json6StringC1EPKc(&key, name); + _ZN3sce4Json6ObjectixERKNS0_6StringE(&object, &key); + _ZN3sce4Json6StringD1Ev(&key); + } + Iterator member{}; + _ZNK3sce4Json6Object5beginEv(&member, &object); + for (const char* expected : {"", "A", "a", "z", "\xc3\xa9"}) { + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&_ZNK3sce4Json6Object8iteratordeEv(&member)->key), expected) == 0); + _ZN3sce4Json6Object8iteratorppEv(&member); + } + _ZN3sce4Json6Object8iteratorD1Ev(&member); + String key{}; + _ZN3sce4Json6StringC1EPKc(&key, "a"); + Value* existing = _ZN3sce4Json6ObjectixERKNS0_6StringE(&object, &key); + _ZN3sce4Json6String6appendEPKcm(&key, "\0suffix", 7); + Require(_ZN3sce4Json6ObjectixERKNS0_6StringE(&object, &key) == existing); + _ZNK3sce4Json6Object4findERKNS0_6StringE(&member, &object, &key); + Require(&_ZNK3sce4Json6Object8iteratordeEv(&member)->value == existing); + _ZN3sce4Json6Object8iteratorD1Ev(&member); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 5); + _ZN3sce4Json6Object5eraseERKNS0_6StringE(&object, &key); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 4); + _ZN3sce4Json6StringD1Ev(&key); + _ZN3sce4Json6ObjectD1Ev(&object); +} + +static void ParseDuplicateKeys() { + Value root{}; + _ZN3sce4Json5ValueC1Ev(&root); + for (const char* text : {R"({"a":1,"a":2})", R"({"a":[1,2],"a":2})", R"({"a":{"old":true},"a":2})", R"({"a":1,"a\u0000suffix":2})"}) { + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, text, std::strlen(text)) == 0); + Require(_ZNK3sce4Json5Value5countEv(&root) == 1); + Require(Serialize(root) == R"({"a":2})"); + } + _ZN3sce4Json5ValueD1Ev(&root); +} + +static void StringComparison() { + String left{}, right{}; + _ZN3sce4Json6StringC1EPKc(&left, "a"); + _ZN3sce4Json6StringC1EPKc(&right, "a"); + _ZN3sce4Json6String6appendEPKcm(&left, "\0suffix", 7); + Require(_ZNK3sce4Json6StringeqERKS1_(&left, &right)); + Require(_ZNK3sce4Json6StringeqEPKc(&left, "a")); + Require(_ZNK3sce4Json6String7compareERKS1_(&left, &right) == 0); + Require(_ZNK3sce4Json6String7compareEPKc(&left, "a") == 0); + Require(_ZNK3sce4Json6String7compareEPKc(&left, "z") == -25); + Require(_ZNK3sce4Json6String7compareEPKc(&left, "") == 97); + Require(_ZNK3sce4Json6String7compareEPKc(&left, "\xff") == -158); + _ZN3sce4Json6StringD1Ev(&right); + _ZN3sce4Json6StringD1Ev(&left); +} + +static void StringMethods() { + Require(_ZN3sce4Json6String4nposE == static_cast(-1)); + const auto text = [](const String& string) { return std::string(_ZNK3sce4Json6String5c_strEv(&string), _ZNK3sce4Json6String6lengthEv(&string)); }; + String base{}; + String suffix{}; + _ZN3sce4Json6StringC1EPKc(&base, "a/b/c"); + _ZN3sce4Json6StringC1EPKc(&suffix, "-tail"); + Require(_ZNK3sce4Json6String4findEcm(&base, '/', 0) == 1); + Require(_ZNK3sce4Json6String4findEcm(&base, '/', 2) == 3); + Require(_ZNK3sce4Json6String4findEcm(&base, 'z', 0) == _ZN3sce4Json6String4nposE); + Require(_ZNK3sce4Json6String5rfindEcm(&base, '/', _ZN3sce4Json6String4nposE) == 3); + Require(_ZNK3sce4Json6String5rfindEcm(&base, '/', 2) == 1); + String part{}; + Require(_ZNK3sce4Json6String6substrEmm(&part, &base, 2, 3) == &part); + Require(text(part) == "b/c"); + _ZN3sce4Json6StringD1Ev(&part); + Require(_ZNK3sce4Json6String6substrEmm(&part, &base, 5, _ZN3sce4Json6String4nposE) == &part); + Require(text(part).empty()); + _ZN3sce4Json6StringD1Ev(&part); + bool refused = false; + try { + _ZNK3sce4Json6String6substrEmm(&part, &base, 6, 1); + } catch (const std::out_of_range&) { + refused = true; + } + Require(refused); + Require(_ZNK3sce4Json6String4sizeEv(&base) == 5); + Require(_ZNK3sce4Json6String6lengthEv(&base) == 5); + String match{}; + _ZN3sce4Json6StringC1EPKc(&match, "a/b/c"); + Require(_ZNK3sce4Json6StringeqERKS1_(&base, &match)); + Require(_ZNK3sce4Json6String7compareERKS1_(&base, &match) == 0); + _ZN3sce4Json6StringD1Ev(&match); + _ZN3sce4Json6String7reserveEm(&base, 64); + Require(_ZNK3sce4Json6String8capacityEv(&base) >= 64); + Require(_ZN3sce4Json6String6appendERKS1_(&base, &suffix) == &base); + Require(_ZN3sce4Json6String6appendEPKc(&base, "!") == &base); + Require(_ZN3sce4Json6String6appendEPKcm(&base, "12345", 2) == &base); + Require(_ZN3sce4Json6StringpLEPKc(&base, "+") == &base); + Require(_ZN3sce4Json6StringpLEh(&base, '?') == &base); + Require(text(base) == "a/b/c-tail!12+?"); + _ZN3sce4Json6String6resizeEm(&base, 5); + Require(_ZNK3sce4Json6String4sizeEv(&base) == 5); + Require(text(base) == "a/b/c"); + _ZN3sce4Json6String6appendERKS1_(&base, &base); + Require(text(base) == "a/b/ca/b/c"); + _ZN3sce4Json6String5clearEv(&base); + Require(text(base).empty()); + _ZN3sce4Json6StringD1Ev(&suffix); + _ZN3sce4Json6StringD1Ev(&base); +} + +static void ValueSwap() { + Value v1{}, v2{}; + _ZN3sce4Json5ValueC1Ev(&v1); + _ZN3sce4Json5ValueC1Ev(&v2); + Require(_ZN3sce4Json5Value3setEl(&v1, 123) == 0); + Require(_ZN3sce4Json5Value3setEPKc(&v2, "hello") == 0); + _ZN3sce4Json5Value4swapERS1_(&v1, &v2); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&v2) == 123); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(_ZNK3sce4Json5Value9getStringEv(&v1)), "hello") == 0); + Require(_ZN3sce4Json5Value3setEl(&v1, -456) == 0); + const auto* firstStorage = _ZNK3sce4Json5Value10getIntegerEv(&v1); + const auto* secondStorage = _ZNK3sce4Json5Value10getIntegerEv(&v2); + _ZN3sce4Json5Value4swapERS1_(&v1, &v2); + Require(_ZNK3sce4Json5Value10getIntegerEv(&v1) == firstStorage && *firstStorage == 123); + Require(_ZNK3sce4Json5Value10getIntegerEv(&v2) == secondStorage && *secondStorage == -456); + _ZN3sce4Json5Value4swapERS1_(&v1, &v1); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&v1) == 123); + _ZN3sce4Json5ValueD1Ev(&v1); + _ZN3sce4Json5ValueD1Ev(&v2); +} + +static void ArrayIteratorAdvance() { + Array array{}; + _ZN3sce4Json5ArrayC1Ev(&array); + for (int i = 0; i < 5; ++i) { + Value val{}; + _ZN3sce4Json5ValueC1Ev(&val); + _ZN3sce4Json5Value3setEl(&val, i * 10); + _ZN3sce4Json5Array9push_backERKNS0_5ValueE(&array, &val); + _ZN3sce4Json5ValueD1Ev(&val); + } + Iterator begin{}; + _ZNK3sce4Json5Array5beginEv(&begin, &array); + Iterator copy{}; + _ZN3sce4Json5Array8iteratorC1ERKS2_(©, &begin); + Require(_ZN3sce4Json5Array8iterator7advanceEm(©, 3) == ©); + Require(*_ZNK3sce4Json5Value10getIntegerEv(_ZNK3sce4Json5Array8iteratordeEv(©)) == 30); + Iterator end{}; + _ZNK3sce4Json5Array3endEv(&end, &array); + _ZN3sce4Json5Array8iterator7advanceEm(©, std::numeric_limits::max()); + Require(!_ZNK3sce4Json5Array8iteratorneERKS2_(©, &end)); + _ZN3sce4Json5Array8iteratorppEv(©); + Require(!_ZNK3sce4Json5Array8iteratorneERKS2_(©, &end)); + Require(*_ZNK3sce4Json5Value10getIntegerEv(_ZNK3sce4Json5Array8iteratordeEv(&begin)) == 0); + _ZN3sce4Json5Array8iteratorD1Ev(&end); + _ZN3sce4Json5Array8iteratorD1Ev(&begin); + _ZN3sce4Json5Array8iteratorD1Ev(©); + _ZN3sce4Json5ArrayD1Ev(&array); +} + +static void ObjectOperations() { + Object object{}; + _ZN3sce4Json6ObjectC1Ev(&object); + String k1{}, k2{}, k3{}; + _ZN3sce4Json6StringC1EPKc(&k1, "alpha"); + _ZN3sce4Json6StringC1EPKc(&k2, "beta"); + _ZN3sce4Json6StringC1EPKc(&k3, "gamma"); + Value v1{}, v2{}, v3{}; + _ZN3sce4Json5ValueC1Ev(&v1); + _ZN3sce4Json5ValueC1Ev(&v2); + _ZN3sce4Json5ValueC1Ev(&v3); + _ZN3sce4Json5Value3setEl(&v1, 1); + _ZN3sce4Json5Value3setEl(&v2, 2); + _ZN3sce4Json5Value3setEl(&v3, 3); + Pair p1{}, p2{}, p3{}; + _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&p1, &k1, &v1); + _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&p2, &k2, &v2); + _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&p3, &k3, &v3); + Iterator ins{}; + _ZN3sce4Json6Object6insertERKNS1_4PairE(&ins, &object, &p3); + _ZN3sce4Json6Object8iteratorD1Ev(&ins); + _ZN3sce4Json6Object6insertERKNS1_4PairE(&ins, &object, &p2); + _ZN3sce4Json6Object8iteratorD1Ev(&ins); + _ZN3sce4Json6Object6insertERKNS1_4PairE(&ins, &object, &p1); + _ZN3sce4Json6Object8iteratorD1Ev(&ins); + + Iterator begin{}; + _ZNK3sce4Json6Object5beginEv(&begin, &object); + Iterator copy{}; + _ZN3sce4Json6Object8iteratorC1ERKS2_(©, &begin); + Require(_ZN3sce4Json6Object8iterator7advanceEm(©, 2) == ©); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&_ZNK3sce4Json6Object8iteratordeEv(©)->key), "gamma") == 0); + _ZN3sce4Json6Object8iterator7advanceEm(©, 1); + bool endRejected = false; + try { + _ZN3sce4Json6Object8iterator7advanceEm(©, 1); + } catch (const std::out_of_range&) { + endRejected = true; + } + Require(endRejected); + _ZN3sce4Json6Object8iteratorD1Ev(&begin); + _ZN3sce4Json6Object8iteratorD1Ev(©); + + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 3); + _ZN3sce4Json6Object5eraseERKNS0_6StringE(&object, &k2); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 2); + _ZN3sce4Json6Object5eraseERKNS0_6StringE(&object, &k2); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 2); + + Iterator findResult{}, end{}; + _ZNK3sce4Json6Object4findERKNS0_6StringE(&findResult, &object, &k2); + _ZNK3sce4Json6Object3endEv(&end, &object); + Require(_ZNK3sce4Json6Object8iteratoreqERKS2_(&findResult, &end)); + _ZN3sce4Json6Object8iteratorD1Ev(&findResult); + _ZN3sce4Json6Object8iteratorD1Ev(&end); + + _ZN3sce4Json6Object4PairD1Ev(&p1); + _ZN3sce4Json6Object4PairD1Ev(&p2); + _ZN3sce4Json6Object4PairD1Ev(&p3); + _ZN3sce4Json5ValueD1Ev(&v1); + _ZN3sce4Json5ValueD1Ev(&v2); + _ZN3sce4Json5ValueD1Ev(&v3); + _ZN3sce4Json6StringD1Ev(&k1); + _ZN3sce4Json6StringD1Ev(&k2); + _ZN3sce4Json6StringD1Ev(&k3); + _ZN3sce4Json6ObjectD1Ev(&object); +} + static void NullAccess(SetNullAccessCallback setCallback) { callbackCalls = 0; lastRequested = -1; @@ -400,6 +899,13 @@ static void ValueAccess() { const char text[] = "{\"list\":[10,20,30],\"name\":\"value\",\"flag\":true}"; Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, text, sizeof(text) - 1) == 0); Require(_ZNK3sce4Json6Object4sizeEv(_ZNK3sce4Json5Value9getObjectEv(&root)) == 3); + Iterator member{}; + _ZNK3sce4Json6Object5beginEv(&member, _ZNK3sce4Json5Value9getObjectEv(&root)); + for (const char* expected : {"flag", "list", "name"}) { + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&_ZNK3sce4Json6Object8iteratordeEv(&member)->key), expected) == 0); + _ZN3sce4Json6Object8iteratorppEv(&member); + } + _ZN3sce4Json6Object8iteratorD1Ev(&member); String key{}; _ZN3sce4Json6StringC1EPKc(&key, "list"); @@ -507,7 +1013,7 @@ static void InitParameters() { construct(parameter); StoredInitParameter2 stored = Stored(parameter); Require(stored.allocator == nullptr && stored.userData == nullptr && stored.fileBufferSize == 0); - Require(stored.specialFloatFormatType == 0); + Require(stored.specialFloatFormatType == 2); Require(stored.reserved[0] == 0 && stored.reserved[1] == 0 && stored.reserved[2] == 0); int allocParam = 0; @@ -583,6 +1089,8 @@ struct ReadState { const char* text; std::size_t position; int calls; + int successStatus = 0; + int failureStatus = -1; }; static int APS5_VABI ReadNext(char& ch, void* context) { @@ -590,10 +1098,32 @@ static int APS5_VABI ReadNext(char& ch, void* context) { ++state->calls; if (state->text[state->position] == '\0') { ch = '\0'; - return -1; + return state->failureStatus; } ch = state->text[state->position++]; - return 0; + return state->successStatus; +} + +static void IntegerRepresentation() { + Value value{}; + _ZN3sce4Json5ValueC1Ev(&value); + for (const char* text : {"0", "1", "9223372036854775807", "9223372036854775808", "18446744073709551615"}) { + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&value, text, std::strlen(text)) == 0); + Require(_ZNK3sce4Json5Value7getTypeEv(&value) == TypeUInteger); + Require(Serialize(value) == text); + } + Require(*_ZNK3sce4Json5Value10getIntegerEv(&value) == -1); + Require(_ZNK3sce4Json5Value7getTypeEv(&value) == TypeUInteger); + Require(_ZN3sce4Json5Value3setEl(&value, -1) == 0); + Require(*_ZNK3sce4Json5Value11getUIntegerEv(&value) == std::numeric_limits::max()); + Require(_ZNK3sce4Json5Value7getTypeEv(&value) == TypeInteger); + for (const char* text : {"-0", "-1", "-9223372036854775808"}) { + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&value, text, std::strlen(text)) == 0); + Require(_ZNK3sce4Json5Value7getTypeEv(&value) == TypeInteger); + } + Require(*_ZNK3sce4Json5Value11getUIntegerEv(&value) == (std::uint64_t{1} << 63)); + Require(_ZNK3sce4Json5Value7getTypeEv(&value) == TypeInteger); + _ZN3sce4Json5ValueD1Ev(&value); } static void StreamParse() { @@ -601,25 +1131,365 @@ static void StreamParse() { _ZN3sce4Json5ValueC1Ev(&root); ReadState valid{"{\"list\": [1, 2.5, true]}", 0, 0}; Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &valid) == 0); - Require(valid.calls == static_cast(std::strlen(valid.text)) + 1); + Require(valid.calls == static_cast(std::strlen(valid.text))); Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject); ReadState invalid{"{\"list\": [1, 2.5", 0, 0}; Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &invalid) < 0); ReadState trailing{"[1] 2", 0, 0}; - Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &trailing) < 0); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &trailing) == 0); + Require(trailing.position == 3); + Require(Serialize(root) == "[1]"); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &trailing) == 0); + Require(Serialize(root) == "2"); + ReadState positive{"true false", 0, 0, 7}; + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &positive) == 0); + Require(positive.position == 4 && positive.calls == 4); + Require(Serialize(root) == "true"); + ReadState failed{"tr", 0, 0, 0, -123456}; + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &failed) == -123456); + Require(failed.calls == 3); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "{} trailing", 11) == 0); + Require(Serialize(root) == "{}"); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "01", 2) == 0); + Require(Serialize(root) == "1"); _ZN3sce4Json5ValueD1Ev(&root); } -int main() { +static void StaticNullAccess() { + callbackCalls = 0; + lastRequested = -1; + lastParent = nullptr; + lastContext = nullptr; + int context = 0; + _ZN3sce4Json5Value21setNullAccessCallBackEPFRKS1_NS0_9ValueTypeEPS2_PvES6_(OnNullAccess, &context); + _ZN3sce4Json5ValueC1Ev(&fallback); + Require(_ZN3sce4Json5Value3setEl(&fallback, 99) == 0); + + Value root{}; + _ZN3sce4Json5ValueC1Ev(&root); + const char text[] = "{\"present\":\"text\"}"; + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, text, sizeof(text) - 1) == 0); + Require(&Member(root, "missing") == &fallback); + Require(callbackCalls == 1 && lastRequested == TypeNull && lastParent == &root && lastContext == &context); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&Member(root, "present")) == 99); + Require(callbackCalls == 2 && lastRequested == TypeInteger); + + _ZN3sce4Json5Value21setNullAccessCallBackEPFRKS1_NS0_9ValueTypeEPS2_PvES6_(nullptr, nullptr); + const Value& unhandled = Member(root, "missing"); + Require(&unhandled != &fallback && _ZNK3sce4Json5Value7getTypeEv(&unhandled) == TypeNull); + Require(callbackCalls == 2); + _ZN3sce4Json5ValueD1Ev(&root); + _ZN3sce4Json5ValueD1Ev(&fallback); +} + +static Value replacement{}; +static int mismatchCalls = 0; +static std::int32_t mismatchRequested = -1; +static const Value* mismatchValue = nullptr; +static const Value* mismatchSecond = nullptr; +static void* mismatchContext = nullptr; + +static const Value& APS5_VABI OnTypeMismatch(std::int32_t requested, const Value* value, const Value* second, void* context) { + ++mismatchCalls; + mismatchRequested = requested; + mismatchValue = value; + mismatchSecond = second; + mismatchContext = context; + return replacement; +} + +static void RequireMismatch(int calls, std::int32_t requested, const Value* value, void* context) { + Require(mismatchCalls == calls && mismatchRequested == requested && mismatchValue == value); + Require(mismatchSecond == nullptr && mismatchContext == context); +} + +static void ValueTypeMismatchHandler() { + _ZN3sce4Json5ValueC1Ev(&replacement); + Require(_ZN3sce4Json5Value3setEl(&replacement, 42) == 0); + int context = 0; + Value root{}; + _ZN3sce4Json5ValueC1Ev(&root); + Require(_ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEPS2_S5_PvES6_(&root, OnTypeMismatch, &context) == 0); + const char text[] = "{\"name\":\"text\",\"none\":null,\"list\":[true]}"; + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, text, sizeof(text) - 1) == 0); + + const Value& name = Member(root, "name"); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&name) == 42); + RequireMismatch(1, TypeInteger, &name, &context); + Require(!*_ZNK3sce4Json5Value10getBooleanEv(&name)); + RequireMismatch(2, TypeBoolean, &name, &context); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(_ZNK3sce4Json5Value9getStringEv(&name)), "text") == 0); + Require(mismatchCalls == 2); + Require(_ZNK3sce4Json5Array4sizeEv(_ZNK3sce4Json5Value8getArrayEv(&root)) == 0); + RequireMismatch(3, TypeArray, &root, &context); + const Value* element = _ZNK3sce4Json5ValueixEm(&Member(root, "list"), 0); + Require(*_ZNK3sce4Json5Value7getRealEv(element) == 0.0); + RequireMismatch(4, TypeReal, element, &context); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&Member(root, "none")) == 0); + Require(mismatchCalls == 4); + + Value later{}; + _ZN3sce4Json5ValueC1Ev(&later); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&later, text, sizeof(text) - 1) == 0); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&Member(later, "name")) == 0); + Require(mismatchCalls == 4); + int laterContext = 0; + Require(_ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEPS2_S5_PvES6_(&later, OnTypeMismatch, &laterContext) == 0); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&Member(later, "name")) == 42); + RequireMismatch(5, TypeInteger, &Member(later, "name"), &laterContext); + + Value other{}; + _ZN3sce4Json5ValueC1Ev(&other); + Require(_ZN3sce4Json5Value3setEPKc(&other, "other") == 0); + Require(_ZN3sce4Json5ValueaSERKS1_(&root, &other) == &root); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&root) == 42); + RequireMismatch(6, TypeInteger, &root, &context); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&other) == 0); + Require(mismatchCalls == 6); + Require(_ZN3sce4Json5Value3setEd(&other, 1.5) == 0); + Require(_ZN3sce4Json5Value3setERKS1_(&root, &other) == 0); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&root) == 42); + RequireMismatch(7, TypeInteger, &root, &context); + _ZN3sce4Json5Value5clearEv(&root); + Require(_ZN3sce4Json5Value3setEPKc(&root, "cleared") == 0); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&root) == 42); + RequireMismatch(8, TypeInteger, &root, &context); + Value copy{}; + _ZN3sce4Json5ValueC1ERKS1_(©, &root); + Require(*_ZNK3sce4Json5Value10getIntegerEv(©) == 0); + Require(mismatchCalls == 8); + + Require(_ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEPS2_S5_PvES6_(&root, nullptr, nullptr) == 0); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&root) == 0); + Require(mismatchCalls == 8); + _ZN3sce4Json5ValueD1Ev(©); + _ZN3sce4Json5ValueD1Ev(&other); + _ZN3sce4Json5ValueD1Ev(&later); + _ZN3sce4Json5ValueD1Ev(&root); + _ZN3sce4Json5ValueD1Ev(&replacement); +} + +static int failureCalls = 0; + +static void APS5_VABI OnElementAccessFailure(std::int32_t, const String*, const std::size_t*, const Value*, const Value*, void*) { + ++failureCalls; +} + +template +static bool ThrowsUnverified(const TAccess& access) { + try { + access(); + } catch (const std::runtime_error& error) { + return std::string(error.what()).find("unverified") != std::string::npos; + } + return false; +} + +static void ValueElementAccessFailureHandler() { + Value root{}; + _ZN3sce4Json5ValueC1Ev(&root); + const char text[] = "{\"list\":[1],\"key\":2}"; + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, text, sizeof(text) - 1) == 0); + int context = 0; + Require(_ZN3sce4Json5Value30setElementAccessFailureHandlerEPFviPKNS0_6StringEPKmPKS1_S8_PvES9_(&root, OnElementAccessFailure, &context) == 0); + + const Value& list = Member(root, "list"); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&Member(root, "key")) == 2); + Require(*_ZNK3sce4Json5Value10getIntegerEv(_ZNK3sce4Json5Value8getValueEm(&list, 0)) == 1); + String missing{}; + _ZN3sce4Json6StringC1EPKc(&missing, "missing"); + Require(ThrowsUnverified([&] { _ZNK3sce4Json5ValueixEPKc(&root, "missing"); })); + Require(ThrowsUnverified([&] { _ZNK3sce4Json5Value8getValueERKNS0_6StringE(&root, &missing); })); + Require(ThrowsUnverified([&] { _ZNK3sce4Json5ValueixEm(&root, 0); })); + Require(ThrowsUnverified([&] { _ZNK3sce4Json5Value8getValueEm(&list, 1); })); + Require(failureCalls == 0); + + Require(_ZN3sce4Json5Value30setElementAccessFailureHandlerEPFviPKNS0_6StringEPKmPKS1_S8_PvES9_(&root, nullptr, nullptr) == 0); + Require(_ZNK3sce4Json5Value7getTypeEv(&Member(root, "missing")) == TypeNull); + Require(_ZNK3sce4Json5Value7getTypeEv(_ZNK3sce4Json5Value8getValueEm(&list, 1)) == TypeNull); + _ZN3sce4Json6StringD1Ev(&missing); + _ZN3sce4Json5ValueD1Ev(&root); +} + +static int specialFloatCalls = 0; + +static Value* APS5_VABI OnSpecialFloat(Value* result, std::int32_t, double, const Value*, void*, bool*) { + ++specialFloatCalls; + _ZN3sce4Json5ValueC1Ev(result); + return result; +} + +static void ValueSpecialFloatHandler() { + Value real{}; + _ZN3sce4Json5ValueC1Ed(&real, std::numeric_limits::quiet_NaN()); + int context = 0; + Require(_ZN3sce4Json5Value22setSpecialFloatHandlerEPFKS1_NS0_12FunctionTypeEdPS2_PvPbES5_(&real, OnSpecialFloat, &context) == 0); + String out{}; + _ZN3sce4Json6StringC1Ev(&out); + Require(ThrowsUnverified([&] { _ZN3sce4Json5Value9serializeERNS0_6StringE(&real, &out); })); + Require(_ZN3sce4Json5Value3setEd(&real, std::numeric_limits::infinity()) == 0); + Require(ThrowsUnverified([&] { _ZN3sce4Json5Value9serializeERNS0_6StringE(&real, &out); })); + _ZN3sce4Json6StringD1Ev(&out); + Require(_ZN3sce4Json5Value3setEd(&real, 2.5) == 0); + Require(Serialize(real) == "2.5"); + Require(specialFloatCalls == 0); + _ZN3sce4Json5ValueD1Ev(&real); +} + +static void ValueOperatorBool() { + Value nullVal{}; + _ZN3sce4Json5ValueC1Ev(&nullVal); + Require(!_ZNK3sce4Json5ValuecvbEv(&nullVal)); + _ZN3sce4Json5ValueD1Ev(&nullVal); + + Value typedNull{}; + _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(&typedNull, TypeNull); + Require(!_ZNK3sce4Json5ValuecvbEv(&typedNull)); + _ZN3sce4Json5ValueD1Ev(&typedNull); + + Value boolFalse{}; + _ZN3sce4Json5ValueC1Eb(&boolFalse, false); + Require(!_ZNK3sce4Json5ValuecvbEv(&boolFalse)); + _ZN3sce4Json5ValueD1Ev(&boolFalse); + + Value boolTrue{}; + _ZN3sce4Json5ValueC1Eb(&boolTrue, true); + Require(_ZNK3sce4Json5ValuecvbEv(&boolTrue)); + _ZN3sce4Json5ValueD1Ev(&boolTrue); + + Value scalar{}; + _ZN3sce4Json5ValueC1Ev(&scalar); + for (std::int64_t value : {std::int64_t{0}, std::int64_t{1}, std::int64_t{-1}}) { + Require(_ZN3sce4Json5Value3setEl(&scalar, value) == 0); + Require(_ZNK3sce4Json5ValuecvbEv(&scalar) == (value != 0)); + } + for (std::uint64_t value : {std::uint64_t{0}, std::uint64_t{1}, std::uint64_t{0xffffffffffffffff}}) { + Require(_ZN3sce4Json5Value3setEm(&scalar, value) == 0); + Require(_ZNK3sce4Json5ValuecvbEv(&scalar) == (value != 0)); + } + for (double value : {0.0, -0.0, 1.0, -1.0, std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()}) { + Require(_ZN3sce4Json5Value3setEd(&scalar, value) == 0); + Require(_ZNK3sce4Json5ValuecvbEv(&scalar) == (value != 0.0)); + } + for (const char* text : {"", "0", "false", " "}) { + Require(_ZN3sce4Json5Value3setEPKc(&scalar, text) == 0); + Require(_ZNK3sce4Json5ValuecvbEv(&scalar) == (*text != 0)); + } + _ZN3sce4Json5ValueD1Ev(&scalar); + for (std::int32_t type : {TypeArray, TypeObject}) { + Value container{}; + _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(&container, type); + Require(_ZNK3sce4Json5ValuecvbEv(&container)); + _ZN3sce4Json5ValueD1Ev(&container); + } + + Value cleared{}; + _ZN3sce4Json5ValueC1Eb(&cleared, true); + Require(_ZNK3sce4Json5ValuecvbEv(&cleared)); + _ZN3sce4Json5Value5clearEv(&cleared); + Require(!_ZNK3sce4Json5ValuecvbEv(&cleared)); + _ZN3sce4Json5ValueD1Ev(&cleared); +} + +static int NumericLocale() { + const std::string previous = std::setlocale(LC_NUMERIC, nullptr); + const char* selected = nullptr; + for (const char* name : {"xx_COMMA", "de_DE.UTF-8", "de_DE.utf8", "de-DE", "German_Germany.1252", "fr_FR.UTF-8"}) { + if (std::setlocale(LC_NUMERIC, name) && std::strcmp(std::localeconv()->decimal_point, ",") == 0) { + selected = name; + break; + } + } + if (!selected) { + std::setlocale(LC_NUMERIC, previous.c_str()); + std::fprintf(stderr, "No decimal-comma numeric locale is installed\n"); + return 77; + } + std::printf("JSON numeric locale: %s\n", selected); + int failures = 0; + const auto check = [&](bool success, const char* operation, const char* actual) { + if (!success) { + std::fprintf(stderr, "%s failed: %s\n", operation, actual); + ++failures; + } + }; + for (const auto number : {1.5, -0.125, 1.25e30, std::numeric_limits::max(), + std::numeric_limits::min(), std::numeric_limits::denorm_min()}) { + Value value{}; + _ZN3sce4Json5ValueC1Ed(&value, number); + const auto text = Serialize(value); + check(text.find(',') == std::string::npos, "Decimal-point serialization", text.c_str()); + Value parsed{}; + _ZN3sce4Json5ValueC1Ev(&parsed); + const int result = _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&parsed, text.c_str(), text.size()); + check(result == 0 && *_ZNK3sce4Json5Value7getRealEv(&parsed) == number, "Numeric round trip", text.c_str()); + _ZN3sce4Json5ValueD1Ev(&parsed); + _ZN3sce4Json5ValueD1Ev(&value); + } + for (const auto [text, number] : {std::pair{"1.5", 1.5}, {"-1.25e2", -125.0}, + {"4.9406564584124654e-324", std::numeric_limits::denorm_min()}, + {"2.4703282292062327e-324", 0.0}, + {"2.4703282292062328e-324", std::numeric_limits::denorm_min()}, + {"18446744073709551616", 18446744073709551616.0}, + {"1e-999", 0.0}, {"-1e-999", -0.0}}) { + Value value{}; + _ZN3sce4Json5ValueC1Ev(&value); + const int result = _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&value, text, std::strlen(text)); + const auto parsed = *_ZNK3sce4Json5Value7getRealEv(&value); + check(result == 0 && parsed == number && std::signbit(parsed) == std::signbit(number), "Decimal-point parsing", text); + _ZN3sce4Json5ValueD1Ev(&value); + } + Value root{}; + _ZN3sce4Json5ValueC1Ev(&root); + constexpr char document[] = "{\"list\":[2.5,-0.125],\"value\":1.5}"; + check(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, document, sizeof(document) - 1) == 0, + "Nested numeric parsing", document); + const auto serialized = Serialize(root); + check(serialized == document, "Nested numeric serialization", serialized.c_str()); + check(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "1,5", 3) == 0 && _ZNK3sce4Json5Value7getTypeEv(&root) == TypeUInteger && + *_ZNK3sce4Json5Value11getUIntegerEv(&root) == 1, "Stop at a decimal comma", "1,5"); + bool overflow = false; + try { + _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "1e309", 5); + } catch (const std::runtime_error&) { + overflow = true; + } + check(overflow, "Retain overflow rejection", "1e309"); + _ZN3sce4Json5ValueD1Ev(&root); + check(std::strcmp(std::localeconv()->decimal_point, ",") == 0, "Preserve host locale", selected); + std::setlocale(LC_NUMERIC, previous.c_str()); + return failures ? 1 : 0; +} + +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "--numeric-locale") == 0) return NumericLocale(); ParseAndRoundTrip(); NestingDepth(); ObjectsAndArrays(); ObjectFind(); + ObjectPair(); + ObjectInsert(); + ObjectKeyOrder(); + ArrayIteratorCopies(); + ObjectIteratorCopies(); + AllocatorNotifyError(); + StringMethods(); + StringComparison(); + ParseDuplicateKeys(); + ValueSwap(); + ArrayIteratorAdvance(); + ObjectOperations(); NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallBackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); + StaticNullAccess(); + ValueTypeMismatchHandler(); + ValueElementAccessFailureHandler(); + ValueSpecialFloatHandler(); ValueAccess(); InitParameter2BaseConstructor(); InitParameters(); ValueClear(); + ValueOperatorBool(); + IntegerRepresentation(); StreamParse(); } diff --git a/core/libs/tests/GuestJxr.cpp b/core/libs/tests/GuestJxr.cpp new file mode 100644 index 0000000000..1bcda397b1 --- /dev/null +++ b/core/libs/tests/GuestJxr.cpp @@ -0,0 +1,78 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +void* APS5_VABI malloc_nid_postfix(std::size_t size); +std::int64_t APS5_VABI PKFree_nid_postfix(void** memory); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +void* lastFreed = nullptr; +int frees = 0; + +void* APS5_VABI Allocate(std::size_t size) { return std::malloc(size); } +void APS5_VABI Free(void* pointer) { + if (pointer != nullptr) { + lastFreed = pointer; + ++frees; + } + std::free(pointer); +} +void* APS5_VABI Reallocate(void* pointer, std::size_t size) { return std::realloc(pointer, 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* pointer, std::size_t size, std::size_t) { return std::realloc(pointer, size); } +int APS5_VABI PosixAlign(void** pointer, std::size_t, std::size_t size) { + *pointer = std::malloc(size); + return *pointer ? 0 : 12; +} + +void InitializeHeap() { + alignas(16) static std::array process{}; + alignas(16) static std::array libc{}; + alignas(16) static std::array replacement{}; + const auto write = [](auto& data, std::size_t offset, auto value) { + std::memcpy(data.data() + offset, &value, sizeof(value)); + }; + write(process, 0, std::uint64_t{0x40}); + write(process, 8, std::uint32_t{0x4942524f}); + write(process, 0x38, libc.data()); + write(libc, 0, std::uint64_t{0x38}); + write(libc, 0x30, replacement.data()); + write(replacement, 0, std::uint64_t{0x78}); + write(replacement, 8, std::uint64_t{2}); + write(replacement, 0x20, reinterpret_cast(&Allocate)); + write(replacement, 0x28, reinterpret_cast(&Free)); + write(replacement, 0x30, reinterpret_cast(&Calloc)); + write(replacement, 0x38, reinterpret_cast(&Reallocate)); + write(replacement, 0x40, reinterpret_cast(&Align)); + write(replacement, 0x48, reinterpret_cast(&Realign)); + write(replacement, 0x50, reinterpret_cast(&PosixAlign)); + ApplicationHeapInitialize_nid_no_patch(process.data()); +} + +} + +int main() { + InitializeHeap(); + void* memory = malloc_nid_postfix(64); + Require(memory != nullptr); + void* const allocated = memory; + Require(PKFree_nid_postfix(&memory) == 0); + Require(memory == nullptr); + Require(frees == 1 && lastFreed == allocated); + Require(PKFree_nid_postfix(&memory) == 0); + Require(memory == nullptr); + Require(frees == 1); + Require(PKFree_nid_postfix(nullptr) == 0); + Require(frees == 1); + return 0; +} diff --git a/core/libs/tests/GuestKernelAppArchive.cpp b/core/libs/tests/GuestKernelAppArchive.cpp new file mode 100644 index 0000000000..2eb8aae01f --- /dev/null +++ b/core/libs/tests/GuestKernelAppArchive.cpp @@ -0,0 +1,257 @@ +#include "AppArchiveFixture.hpp" +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/KernelErrors.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct KernelIovec { + void* base; + std::size_t length; +}; + +extern "C" { +int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode); +int APS5_VABI sceKernelClose(int d); +std::int64_t APS5_VABI sceKernelRead(int d, void* buf, std::size_t nbytes); +std::int64_t APS5_VABI sceKernelWrite(int d, const void* buf, std::size_t nbytes); +std::int64_t APS5_VABI sceKernelPread(int d, void* buf, std::size_t nbytes, std::int64_t offset); +std::int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* iov, int iovcnt); +std::int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* iov, int iovcnt, std::int64_t offset); +std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence); +int APS5_VABI sceKernelStat(const char* path, FileStat* sb); +int APS5_VABI sceKernelFstat(int d, FileStat* sb); +int APS5_VABI sceKernelCheckReachability(const char* path); +int APS5_VABI dup_nid_postfix(int d); +int APS5_VABI dup2_nid_postfix(int from, int to); +int APS5_VABI close_nid_postfix(int d); +std::int64_t APS5_VABI pread_nid_postfix(int d, void* buf, std::size_t nbytes, std::int64_t offset); +std::int64_t APS5_VABI lseek_nid_postfix(int d, std::int64_t offset, int whence); +int APS5_VABI fcntl_nid_postfix(int d, int command, ...); +int APS5_VABI ioctl_nid_postfix(int d, std::uint64_t request, void* argument); +void* APS5_VABI opendir_nid_postfix(const char* path); +void* APS5_VABI fdopen_nid_postfix(int d, const char* mode); +int APS5_VABI sceKernelAioSubmitReadCommands(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, std::int32_t* id); +int APS5_VABI sceKernelAioWaitRequest(std::int32_t id, std::int32_t* state, std::uint32_t* usec); +int APS5_VABI sceKernelAioDeleteRequest(std::int32_t id, std::int32_t* ret); +int APS5_VABI flock_nid_postfix(int d, int operation); +int APS5_VABI _fstatfs_nid_postfix(int d, void* buf); +int APS5_VABI ftruncate_nid_postfix(int d, std::int64_t length); +int APS5_VABI sceKernelFsync(int d); +int APS5_VABI sceKernelFdatasync(int d); +int APS5_VABI sceKernelGetdents(int d, char* buf, int nbytes); +int APS5_VABI sceKernelFchmod(int d, std::uint16_t mode); +int APS5_VABI sceKernelFutimes(int d, const void* times); +void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protection, int flags, int descriptor, std::int64_t offset) noexcept; +void* APS5_VABI __cxa_current_primary_exception_nid_postfix(); +std::type_info* APS5_VABI __cxa_current_exception_type_nid_postfix(); +using TerminateHandler = void (*)(); +TerminateHandler APS5_VABI _ZSt13set_terminatePFvvE_nid_postfix(TerminateHandler); +} + +namespace { + +void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "app0.zar kernel check failed at line %d\n", line); + std::exit(1); + } +} +#define Check(value) Require((value), __LINE__) + +void OnMmapRefused() { + const auto* type = __cxa_current_exception_type_nid_postfix(); + const auto* error = static_cast(__cxa_current_primary_exception_nid_postfix()); + const bool refused = type != nullptr && error != nullptr && std::strcmp(type->name(), typeid(std::runtime_error).name()) == 0 && + std::strstr(error->what(), "mmap on a file read from app0.zar") != nullptr && std::strstr(error->what(), "not implemented") != nullptr; + std::_Exit(refused ? 0 : 1); +} + +template bool NotImplemented(TCall call) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), "not implemented") != nullptr; + } + return false; +} + +constexpr std::size_t BigSize = 3 * 65536 + 77; +constexpr std::uint16_t TypeMask = 0170000; +constexpr std::uint16_t RegularType = 0100000; +constexpr std::uint16_t DirectoryType = 0040000; +constexpr int DuplicateFrom = 0; +constexpr std::uint64_t ReadableRequest = 0x4004667f; + +void Archive(const std::filesystem::path& root) { + AppArchiveFixture::WriteArchive(root / "app0.zar", { + {"Data/Big.bin", AppArchiveFixture::Pattern(BigSize)}, + {"Data/Small.txt", {'z', 'a', 'r'}}, + {"sce_sys/Packed.json", {'{', '}'}}, + {"eboot.bin", {'o', 'l', 'd'}}, + }); + std::ofstream eboot(root / "app0" / "eboot.bin", std::ios::binary); + eboot << "disk"; +} + +void Reads() { + const auto expected = AppArchiveFixture::Pattern(BigSize); + const int fd = sceKernelOpen("/app0/data/big.BIN", SCE_KERNEL_O_RDONLY, 0); + Check(fd >= 0); + std::vector buffer(BigSize + 16); + Check(sceKernelRead(fd, buffer.data(), 70000) == 70000); + Check(std::equal(buffer.begin(), buffer.begin() + 70000, expected.begin())); + Check(sceKernelLseek(fd, 0, 1) == 70000); + Check(lseek_nid_postfix(fd, 0, 1) == 70000); + + std::vector left(10), right(65536); + const KernelIovec vector[] = {{left.data(), left.size()}, {right.data(), right.size()}}; + Check(sceKernelReadv(fd, vector, 2) == 65546); + Check(std::equal(left.begin(), left.end(), expected.begin() + 70000) && std::equal(right.begin(), right.end(), expected.begin() + 70010)); + Check(sceKernelPreadv(fd, vector, 2, 5) == 65546); + Check(std::equal(left.begin(), left.end(), expected.begin() + 5) && std::equal(right.begin(), right.end(), expected.begin() + 15)); + Check(sceKernelLseek(fd, 0, 1) == 135546); + + Check(sceKernelPread(fd, buffer.data(), 100, 65500) == 100 && std::equal(buffer.begin(), buffer.begin() + 100, expected.begin() + 65500)); + Check(pread_nid_postfix(fd, buffer.data(), 100, BigSize - 40) == 40 && std::equal(buffer.begin(), buffer.begin() + 40, expected.end() - 40)); + Check(sceKernelPread(fd, buffer.data(), 1, -1) == SCE_KERNEL_ERROR_EINVAL); + Check(sceKernelLseek(fd, 0, 1) == 135546); + + int available = 0; + Check(ioctl_nid_postfix(fd, ReadableRequest, &available) == 0 && available == static_cast(BigSize - 135546)); + + Check(sceKernelLseek(fd, -7, 2) == static_cast(BigSize - 7)); + Check(sceKernelRead(fd, buffer.data(), 100) == 7 && std::equal(buffer.begin(), buffer.begin() + 7, expected.end() - 7)); + Check(sceKernelRead(fd, buffer.data(), 100) == 0); + Check(sceKernelLseek(fd, -1, 0) == SCE_KERNEL_ERROR_EINVAL); + Check(sceKernelLseek(fd, 0, 1) == static_cast(BigSize)); + Check(sceKernelWrite(fd, "x", 1) == SCE_KERNEL_ERROR_EBADF); + + FileStat status{}; + Check(sceKernelFstat(fd, &status) == 0); + Check((status.st_mode & TypeMask) == RegularType && status.st_size == static_cast(BigSize) && status.st_nlink == 1); + + KernelAioResult result{-1, 0}; + KernelAioRwRequest request{65530, 20, buffer.data(), &result, fd}; + std::int32_t id = 0; + std::int32_t state = 0; + Check(sceKernelAioSubmitReadCommands(&request, 1, 0, &id) == 0); + Check(sceKernelAioWaitRequest(id, &state, nullptr) == 0); + Check(result.return_value == 20 && std::equal(buffer.begin(), buffer.begin() + 20, expected.begin() + 65530)); + std::int32_t removed = 0; + Check(sceKernelAioDeleteRequest(id, &removed) == 0); + + Check(sceKernelLseek(fd, 1000, 0) == 1000); + const int duplicate = dup_nid_postfix(fd); + Check(duplicate >= 0 && sceKernelLseek(duplicate, 0, 1) == 1000); + Check(sceKernelRead(duplicate, buffer.data(), 24) == 24 && sceKernelLseek(fd, 0, 1) == 1024); + const int above = fcntl_nid_postfix(fd, DuplicateFrom, 200); + Check(above >= 200 && sceKernelLseek(above, 0, 1) == 1024); + Check(sceKernelClose(fd) == 0); + Check(sceKernelRead(duplicate, buffer.data(), 4) == 4 && std::equal(buffer.begin(), buffer.begin() + 4, expected.begin() + 1024)); + Check(sceKernelLseek(above, 0, 1) == 1028); + Check(close_nid_postfix(above) == 0); + + const int plain = sceKernelOpen("/app0/eboot.bin", SCE_KERNEL_O_RDONLY, 0); + Check(plain >= 0); + Check(dup2_nid_postfix(plain, duplicate) == duplicate); + Check(sceKernelPread(duplicate, buffer.data(), 8, 0) == 4 && std::memcmp(buffer.data(), "disk", 4) == 0); + Check(sceKernelClose(plain) == 0); + Check(sceKernelClose(duplicate) == 0); +} + +void Status() { + FileStat status{}; + Check(sceKernelStat("/app0/Data/Small.txt", &status) == 0); + Check((status.st_mode & TypeMask) == RegularType && (status.st_mode & 0222) == 0 && status.st_size == 3 && status.st_blocks == 1); + Check(sceKernelStat("/app0/data", &status) == 0 && (status.st_mode & TypeMask) == DirectoryType); + Check(sceKernelStat("/app0/Data/", &status) == 0); + Check(sceKernelStat("/app0/Data/Small.txt/", &status) == SCE_KERNEL_ERROR_ENOTDIR); + Check(sceKernelStat("/app0/Data/Missing.txt", &status) == SCE_KERNEL_ERROR_ENOENT); + Check(sceKernelStat("/app0/eboot.bin", &status) == 0 && status.st_size == 4); + Check(sceKernelCheckReachability("/app0/Data/Small.txt") == 0); + Check(sceKernelCheckReachability("/app0/Data/Missing.txt") == SCE_KERNEL_ERROR_ENOENT); +} + +void Refusals() { + Check(sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDWR, 0) == SCE_KERNEL_ERROR_EROFS); + Check(sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_TRUNC, 0) == SCE_KERNEL_ERROR_EROFS); + Check(sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL, 0666) == SCE_KERNEL_ERROR_EEXIST); + Check(sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0) == SCE_KERNEL_ERROR_ENOTDIR); + Check(sceKernelOpen("/app0/Data/Missing.txt", SCE_KERNEL_O_RDONLY, 0) == SCE_KERNEL_ERROR_ENOENT); + Check(NotImplemented([] { (void)sceKernelOpen("/app0/Data", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); })); + Check(NotImplemented([] { (void)sceKernelOpen("/app0/sce_sys", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); })); + Check(NotImplemented([] { (void)opendir_nid_postfix("/app0/sce_sys"); })); + Check(NotImplemented([] { (void)opendir_nid_postfix("/app0/Data"); })); + const int fd = sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY, 0); + Check(fd >= 0); + Check(NotImplemented([fd] { (void)fdopen_nid_postfix(fd, "r"); })); + Check(NotImplemented([fd] { (void)flock_nid_postfix(fd, 1); })); + Check(NotImplemented([fd] { alignas(8) char statfs[512]; (void)_fstatfs_nid_postfix(fd, statfs); })); + Check(NotImplemented([fd] { (void)ftruncate_nid_postfix(fd, 0); })); + Check(NotImplemented([fd] { (void)sceKernelFsync(fd); })); + Check(NotImplemented([fd] { (void)sceKernelFdatasync(fd); })); + Check(NotImplemented([fd] { char entries[512]; (void)sceKernelGetdents(fd, entries, sizeof(entries)); })); + Check(NotImplemented([fd] { (void)sceKernelFchmod(fd, 0444); })); + Check(NotImplemented([fd] { (void)sceKernelFutimes(fd, nullptr); })); + FileStat status{}; + Check(sceKernelFstat(fd, &status) == 0 && status.st_size == 3); + Check(sceKernelClose(fd) == 0); +} + +void WithoutArchive() { + { std::ofstream file(std::filesystem::current_path() / "app0" / "Plain.txt", std::ios::binary); file << "plain"; } + const int fd = sceKernelOpen("/app0/Plain.txt", SCE_KERNEL_O_RDONLY, 0); + Check(fd >= 0); + char buffer[8] = {}; + Check(sceKernelRead(fd, buffer, sizeof(buffer)) == 5 && std::memcmp(buffer, "plain", 5) == 0); + Check(sceKernelClose(fd) == 0); + Check(sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY, 0) == SCE_KERNEL_ERROR_ENOENT); + FileStat status{}; + Check(sceKernelStat("/app0/Data/Small.txt", &status) == SCE_KERNEL_ERROR_ENOENT); + const int directory = sceKernelOpen("/app0/sce_sys", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Check(directory >= 0 && sceKernelClose(directory) == 0); +} + +} + +int main(int argc, char** argv) { + if (argc != 2) { + std::fprintf(stderr, "usage: guest_kernel_app_archive_tests archive|plain\n"); + return 2; + } + const std::string mode = argv[1]; + const auto root = AppArchiveFixture::EnterFreshRoot("anyps5-kernel-app-archive"); + if (mode == "archive") { + Archive(root); + Reads(); + Status(); + Refusals(); + } else if (mode == "mmap") { + Archive(root); + const int fd = sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY, 0); + Check(fd >= 0); + _ZSt13set_terminatePFvvE_nid_postfix(OnMmapRefused); + (void)mmap_nid_postfix(nullptr, 16384, 1, 2, fd, 0); + return 1; + } else if (mode == "plain") { + WithoutArchive(); + } else { + return 2; + } + std::error_code error; + std::filesystem::current_path(std::filesystem::temp_directory_path(), error); + std::filesystem::remove_all(root, error); + return 0; +} diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index 6a76e356e3..1932625356 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -1,8 +1,11 @@ #include "SceTypes.hpp" +#include +#include #include #include #include #include +#include extern "C" { int APS5_VABI sceKernelCreateSema(KernelSema*, const char*, std::uint32_t, int, int, void*); @@ -29,16 +32,35 @@ int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); +int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, std::uint32_t attr, std::uint64_t init_pattern, const void* param); +int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef); +int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name); +int APS5_VABI sceKernelSetEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); +int APS5_VABI sceKernelClearEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); +int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, std::uint64_t set_pattern, int* num_wait_threads); +int APS5_VABI sceKernelPollEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern, std::uint32_t wait_mode, std::uint64_t* result_pat); +int APS5_VABI sceKernelWaitEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern, std::uint32_t wait_mode, std::uint64_t* result_pat, KernelUseconds* timeout); +int APS5_Test_GetEventFlagWaiterCount(KernelEventFlag handle); } static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EPERM = static_cast(0x80020001); static constexpr int SCE_KERNEL_ERROR_ENOENT = static_cast(0x80020002); +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003); static constexpr int SCE_KERNEL_ERROR_EBADF = static_cast(0x80020009); static constexpr int SCE_KERNEL_ERROR_EDEADLK = static_cast(0x8002000B); static constexpr int SCE_KERNEL_ERROR_EFAULT = static_cast(0x8002000E); +static constexpr int SCE_KERNEL_ERROR_EACCES = static_cast(0x8002000D); static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); +static constexpr int SCE_KERNEL_ERROR_ECANCELED = static_cast(0x80020055); +static constexpr std::uint32_t EVF_ATTR_SINGLE = 0x10; +static constexpr std::uint32_t EVF_ATTR_MULTI = 0x20; +static constexpr std::uint32_t EVF_WAITMODE_AND = 0x01; +static constexpr std::uint32_t EVF_WAITMODE_OR = 0x02; +static constexpr std::uint32_t EVF_WAITMODE_CLEAR_ALL = 0x10; +static constexpr std::uint32_t EVF_WAITMODE_CLEAR_PAT = 0x20; static constexpr int MUTEX_TYPE_ERRORCHECK = 1; static constexpr int PRIO_NONE = 0; static constexpr int PRIO_INHERIT = 1; @@ -48,6 +70,14 @@ static constexpr int POSIX_EDEADLK = 11; static void Require(bool value) { if (!value) std::abort(); } +static void WaitForEventFlagWaiters(KernelEventFlag handle, int expected) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (APS5_Test_GetEventFlagWaiterCount(handle) != expected) { + if (std::chrono::steady_clock::now() >= deadline) Require(false); + std::this_thread::yield(); + } +} + int main() { constexpr int maximum = std::numeric_limits::max(); KernelSema semaphore = nullptr; @@ -156,4 +186,178 @@ int main() { Require(pthread_mutex_lock_nid_postfix(&staticAdaptive) == POSIX_EDEADLK); Require(pthread_mutex_unlock_nid_postfix(&staticAdaptive) == 0); Require(pthread_mutex_destroy_nid_postfix(&staticAdaptive) == 0); + + KernelEventFlag ef = nullptr; + Require(sceKernelCreateEventFlag(nullptr, "test", EVF_ATTR_MULTI, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCreateEventFlag(&ef, nullptr, EVF_ATTR_MULTI, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCreateEventFlag(&ef, "test_zero_attr", 0, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + Require(sceKernelCreateEventFlag(&ef, "test_fifo", 0x01, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + Require(sceKernelCreateEventFlag(&ef, "test_single_fifo", EVF_ATTR_SINGLE | 0x01u, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + Require(sceKernelCreateEventFlag(&ef, "test", EVF_ATTR_SINGLE | EVF_ATTR_MULTI, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCreateEventFlag(&ef, "test", EVF_ATTR_MULTI | 0x1000u, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelDeleteEventFlag(nullptr) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelSetEventFlag(nullptr, 1) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelClearEventFlag(nullptr, 1) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelPollEventFlag(nullptr, 1, EVF_WAITMODE_OR, nullptr) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelWaitEventFlag(nullptr, 1, EVF_WAITMODE_OR, nullptr, nullptr) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelCancelEventFlag(nullptr, 0, nullptr) == SCE_KERNEL_ERROR_ESRCH); + + KernelEventFlag opened = nullptr; + Require(sceKernelOpenEventFlag(nullptr, "test") == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelOpenEventFlag(&opened, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelOpenEventFlag(&opened, "SceShellCoreUtilAppFocus") == SCE_KERNEL_ERROR_ENOENT && opened == nullptr); + Require(sceKernelCreateEventFlag(&ef, "test_open", EVF_ATTR_MULTI, 0x05, nullptr) == SCE_OK); + Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_OK && opened == ef); + Require(sceKernelSetEventFlag(opened, 0x08) == SCE_OK); + std::uint64_t openedPattern = 0; + Require(sceKernelPollEventFlag(ef, 0x08, EVF_WAITMODE_OR, &openedPattern) == SCE_OK && openedPattern == 0x0d); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + opened = nullptr; + Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_KERNEL_ERROR_ENOENT && opened == nullptr); + + Require(sceKernelCreateEventFlag(&ef, "flag", EVF_ATTR_MULTI, 0x15, nullptr) == SCE_OK); + std::uint64_t pattern = 0; + Require(sceKernelPollEventFlag(ef, 0, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelPollEventFlag(ef, 1, 0, &pattern) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelPollEventFlag(ef, 1, EVF_WAITMODE_AND | EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelPollEventFlag(ef, 1, EVF_WAITMODE_AND | EVF_WAITMODE_CLEAR_ALL | EVF_WAITMODE_CLEAR_PAT, &pattern) == SCE_KERNEL_ERROR_EINVAL); + + Require(sceKernelPollEventFlag(ef, 0x20, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EBUSY); + Require(pattern == 0x15); + Require(sceKernelPollEventFlag(ef, 0x11, EVF_WAITMODE_AND, &pattern) == SCE_OK); + Require(pattern == 0x15); + + Require(sceKernelClearEventFlag(ef, ~std::uint64_t{0x01}) == SCE_OK); + Require(sceKernelPollEventFlag(ef, 0x01, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EBUSY); + Require(pattern == 0x14); + + Require(sceKernelSetEventFlag(ef, 0x02) == SCE_OK); + Require(sceKernelPollEventFlag(ef, 0x06, EVF_WAITMODE_OR | EVF_WAITMODE_CLEAR_PAT, &pattern) == SCE_OK); + Require(pattern == 0x16); + Require(sceKernelPollEventFlag(ef, 0x02, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EBUSY); + Require(pattern == 0x10); + + Require(sceKernelPollEventFlag(ef, 0x10, EVF_WAITMODE_OR | EVF_WAITMODE_CLEAR_ALL, &pattern) == SCE_OK); + Require(pattern == 0x10); + Require(sceKernelPollEventFlag(ef, 0x10, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EBUSY); + Require(pattern == 0); + + KernelUseconds efTimeout = 1000; + Require(sceKernelWaitEventFlag(ef, 0x01, EVF_WAITMODE_OR, &pattern, &efTimeout) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(pattern == 0); + + int waitThreads = -1; + Require(sceKernelCancelEventFlag(ef, 0x42, &waitThreads) == SCE_OK); + Require(waitThreads == 0); + Require(sceKernelPollEventFlag(ef, 0x42, EVF_WAITMODE_AND, &pattern) == SCE_OK); + Require(pattern == 0x42); + + Require(sceKernelClearEventFlag(ef, 0) == SCE_OK); + Require(sceKernelPollEventFlag(ef, 0x42, EVF_WAITMODE_AND, &pattern) == SCE_KERNEL_ERROR_EBUSY); + Require(pattern == 0); + + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + + Require(sceKernelCreateEventFlag(&ef, "single", EVF_ATTR_SINGLE | 0x02u, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + + Require(sceKernelCreateEventFlag(&ef, "imm_sat", EVF_ATTR_MULTI, 0x05, nullptr) == SCE_OK); + pattern = 0; + Require(sceKernelWaitEventFlag(ef, 0x01, EVF_WAITMODE_AND | EVF_WAITMODE_CLEAR_PAT, &pattern, nullptr) == SCE_OK); + Require(pattern == 0x05); + Require(sceKernelPollEventFlag(ef, 0x01, EVF_WAITMODE_AND, &pattern) == SCE_KERNEL_ERROR_EBUSY); + Require(sceKernelPollEventFlag(ef, 0x04, EVF_WAITMODE_AND, &pattern) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + + Require(sceKernelCreateEventFlag(&ef, "multi_order_nonclear_first", EVF_ATTR_MULTI, 0, nullptr) == SCE_OK); + std::atomic a1Res{-1}; + std::atomic a2Res{-1}; + std::uint64_t a1Pat = 0; + std::uint64_t a2Pat = 0; + std::thread a1([&] { + a1Res = sceKernelWaitEventFlag(ef, 0x01, EVF_WAITMODE_AND, &a1Pat, nullptr); + }); + WaitForEventFlagWaiters(ef, 1); + std::thread a2([&] { + a2Res = sceKernelWaitEventFlag(ef, 0x01, EVF_WAITMODE_AND | EVF_WAITMODE_CLEAR_PAT, &a2Pat, nullptr); + }); + WaitForEventFlagWaiters(ef, 2); + Require(sceKernelSetEventFlag(ef, 0x01) == SCE_OK); + a1.join(); + a2.join(); + Require(a1Res == SCE_OK); + Require(a2Res == SCE_OK); + Require(a1Pat == 0x01); + Require(a2Pat == 0x01); + Require(sceKernelPollEventFlag(ef, 0x01, EVF_WAITMODE_AND, &pattern) == SCE_KERNEL_ERROR_EBUSY); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + + Require(sceKernelCreateEventFlag(&ef, "multi_order_clear_first", EVF_ATTR_MULTI, 0, nullptr) == SCE_OK); + std::atomic b1Res{-1}; + std::atomic b2Res{-1}; + std::uint64_t b1Pat = 0; + std::uint64_t b2Pat = 0; + std::thread b1([&] { + b1Res = sceKernelWaitEventFlag(ef, 0x01, EVF_WAITMODE_AND | EVF_WAITMODE_CLEAR_PAT, &b1Pat, nullptr); + }); + WaitForEventFlagWaiters(ef, 1); + std::thread b2([&] { + b2Res = sceKernelWaitEventFlag(ef, 0x01, EVF_WAITMODE_AND, &b2Pat, nullptr); + }); + WaitForEventFlagWaiters(ef, 2); + Require(sceKernelSetEventFlag(ef, 0x01) == SCE_OK); + b1.join(); + Require(b1Res == SCE_OK); + Require(b1Pat == 0x01); + Require(APS5_Test_GetEventFlagWaiterCount(ef) == 1); + Require(b2Res == -1); + Require(sceKernelSetEventFlag(ef, 0x01) == SCE_OK); + b2.join(); + Require(b2Res == SCE_OK); + Require(b2Pat == 0x01); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + + Require(sceKernelCreateEventFlag(&ef, "del_waiters", EVF_ATTR_MULTI, 0, nullptr) == SCE_OK); + std::atomic delRes1{-1}; + std::atomic delRes2{-1}; + std::thread delT1([&] { + delRes1 = sceKernelWaitEventFlag(ef, 0x01, EVF_WAITMODE_AND, nullptr, nullptr); + }); + std::thread delT2([&] { + delRes2 = sceKernelWaitEventFlag(ef, 0x02, EVF_WAITMODE_AND, nullptr, nullptr); + }); + WaitForEventFlagWaiters(ef, 2); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + delT1.join(); + delT2.join(); + Require(delRes1 == SCE_KERNEL_ERROR_EACCES); + Require(delRes2 == SCE_KERNEL_ERROR_EACCES); + + for (int iter = 0; iter < 50; ++iter) { + KernelEventFlag raceEf = nullptr; + Require(sceKernelCreateEventFlag(&raceEf, "set_del_race", EVF_ATTR_MULTI, 0, nullptr) == SCE_OK); + std::atomic setResult{-1}; + std::thread concurrentSet([&] { + setResult = sceKernelSetEventFlag(raceEf, 0x01); + }); + Require(sceKernelDeleteEventFlag(raceEf) == SCE_OK); + concurrentSet.join(); + Require(setResult == SCE_OK || setResult == SCE_KERNEL_ERROR_ESRCH); + } + + for (int iter = 0; iter < 50; ++iter) { + KernelEventFlag pollEf = nullptr; + Require(sceKernelCreateEventFlag(&pollEf, "poll_del_race", EVF_ATTR_MULTI, 0x01, nullptr) == SCE_OK); + std::atomic pollResult{-1}; + std::thread concurrentPoll([&] { + std::uint64_t observed = 0; + pollResult = sceKernelPollEventFlag(pollEf, 0x01, EVF_WAITMODE_AND, &observed); + }); + Require(sceKernelDeleteEventFlag(pollEf) == SCE_OK); + concurrentPoll.join(); + Require(pollResult == SCE_OK || pollResult == SCE_KERNEL_ERROR_ESRCH); + } } diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 679a739b64..b75d3c22a9 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -9,11 +9,18 @@ #include #include #include +#ifndef _WIN32 +#include +#include +#include +#endif extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI __sys_socketex_nid_postfix(const char*, int, int, int); int APS5_VABI close_nid_postfix(int); int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI fcntl_nid_postfix(int, int, ...); +int APS5_VABI _fcntl_nid_postfix(int, int, ...); int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); 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); @@ -21,6 +28,7 @@ int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceKernelFcntl(int, int, ...); int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); } static void Require(bool value) { if (!value) std::abort(); } @@ -32,12 +40,14 @@ static constexpr int SetFlags = 4; static constexpr int OldGetLock = 7; static constexpr int OldSetLock = 8; static constexpr int OldSetLockWait = 9; +static constexpr int Duplicate2Command = 10; static constexpr int GetLock = 11; static constexpr int SetLock = 12; static constexpr int SetLockWait = 13; static constexpr int ReadAhead = 15; static constexpr int DarwinReadAhead = 16; static constexpr int DuplicateCloseOnExec = 17; +static constexpr int Duplicate2CloseOnExec = 18; static constexpr int ReadOnly = 0; static constexpr int WriteOnly = 1; static constexpr int ReadWrite = 2; @@ -65,6 +75,9 @@ static constexpr std::uint64_t WindowSizeRequest = 0x40087468; static constexpr std::uint64_t AsyncRequest = 0x8004667d; static constexpr std::uint64_t CloseOnExecRequest = 0x20006601; static constexpr std::uint64_t KeepOnExecRequest = 0x20006602; +static constexpr std::uint64_t MalformedRequests[] = {0, 123, 0x5401, 0x5413, 0x40007468, 0x20087468, 0x00087468}; +static constexpr std::uint64_t IntegerArgumentRequest = 0x20046601; +static constexpr std::uint64_t EmptyInputRequest = 0x80006601; static constexpr int SceEbadf = static_cast(0x80020009u); static constexpr int SceEinval = static_cast(0x80020016u); static constexpr int SceEnotsup = static_cast(0x8002002du); @@ -92,6 +105,14 @@ template static bool ThrowsWith(TCall call, const std::string& return false; } +static void RequireMalformedRequestsFail(int descriptor) { + char buffer[16] = {}; + for (const std::uint64_t request : MalformedRequests) { + RequireFailure(ioctl_nid_postfix(descriptor, request, buffer), GuestEnotty); + RequireFailure(ioctl_nid_postfix(descriptor, request, nullptr), GuestEnotty); + } +} + static void SocketsKeepTheirPath() { const int socket = socket_nid_postfix(2, 2, 0); Require(socket >= 0); @@ -100,16 +121,81 @@ static void SocketsKeepTheirPath() { Require(sceKernelFcntl(socket, GetFlags) == (ReadWrite | NonBlocking)); Require(sceKernelFcntl(socket, SetFlags, 0) == 0); Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); + GuestLock lock{}; + Require(sceKernelFcntl(socket, GetLock, &lock) == SceEinval); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, SetDescriptorFlags, 1) == 0); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); + const int duplicate = sceKernelFcntl(socket, DuplicateCommand, 20); + Require(duplicate >= 20); + Require(sceKernelFcntl(duplicate, GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, SetFlags, ReadWrite | NonBlocking) == 0); + Require(sceKernelFcntl(duplicate, GetFlags) == (ReadWrite | NonBlocking)); + Require(sceKernelFcntl(duplicate, SetFlags, 0) == 0); + Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); + const int closing = sceKernelFcntl(socket, DuplicateCloseOnExec, duplicate + 1); + Require(closing >= duplicate + 1); + Require(sceKernelFcntl(closing, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, SetDescriptorFlags, 0) == 0); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(socket, Duplicate2Command, closing) == closing); + Require(sceKernelFcntl(closing, GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, closing) == closing); + Require(sceKernelFcntl(closing, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, socket) == socket); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, Duplicate2Command, socket) == socket); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, SetDescriptorFlags, 0) == 0); + int pipe[2] = {-1, -1}; + Require(pipe_nid_postfix(pipe) == 0); + Require(fcntl_nid_postfix(pipe[0], SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(socket, Duplicate2Command, pipe[0]) == pipe[0]); + Require(sceKernelFcntl(pipe[0], GetFlags) == ReadWrite); + Require(sceKernelFcntl(pipe[0], GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, pipe[1]) == pipe[1]); + Require(sceKernelFcntl(pipe[1], GetFlags) == ReadWrite); + Require(sceKernelFcntl(pipe[1], GetDescriptorFlags) == 1); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); + const int random = sceKernelOpen("/dev/urandom", ReadOnly, 0); + Require(random >= 0); + Require(fcntl_nid_postfix(socket, Duplicate2Command, random) == random); + Require(sceKernelFcntl(random, GetFlags) == ReadWrite); + Require(close_nid_postfix(random) == 0); + const int file = sceKernelOpen("kernel_fcntl_socket_dup2.bin", CreateReadWrite, 0644); + Require(file == random); + char byte = 0; + Require(sceKernelRead(file, &byte, 1) == 0); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, file) == file); + Require(sceKernelFcntl(file, GetFlags) == ReadWrite); + Require(sceKernelFcntl(file, GetDescriptorFlags) == 1); + Require(close_nid_postfix(file) == 0); + std::remove("kernel_fcntl_socket_dup2.bin"); + RequireFailure(fcntl_nid_postfix(socket, DuplicateCommand, -1), GuestEinval); + RequireFailure(fcntl_nid_postfix(socket, DuplicateCloseOnExec, -1), GuestEinval); + RequireFailure(fcntl_nid_postfix(socket, Duplicate2Command, -1), GuestEbadf); + RequireFailure(fcntl_nid_postfix(socket, Duplicate2CloseOnExec, -1), GuestEbadf); + Require(sceKernelFcntl(socket, DuplicateCommand, -1) == SceEinval); + Require(sceKernelFcntl(socket, DuplicateCloseOnExec, -1) == SceEinval); + Require(sceKernelFcntl(socket, Duplicate2Command, -1) == SceEbadf); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, -1) == SceEbadf); Require(sceKernelFcntl(socket, SetFlags, 0x8000) == SceEnotsup); Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); Require(sceKernelFcntl(socket, 5) == SceEinval); Require(sceKernelFcntl(socket + 4096, GetFlags) == SceEbadf); int queued = -1; Require(ioctl_nid_postfix(socket, ReadableRequest, &queued) == 0 && queued == 0); - Require(close_nid_postfix(socket) == 0); + RequireMalformedRequestsFail(socket); + RequireFailure(ioctl_nid_postfix(socket, IntegerArgumentRequest, nullptr), GuestEfault); + RequireFailure(ioctl_nid_postfix(socket, EmptyInputRequest, nullptr), GuestEfault); + for (const int descriptor : {closing, duplicate, socket}) Require(close_nid_postfix(descriptor) == 0); Require(sceKernelFcntl(socket, GetFlags) == SceEbadf); Require(sceKernelFcntl(-1, GetFlags) == SceEbadf); Require(sceKernelFcntl(12345, GetFlags) == SceEbadf); + const int named = __sys_socketex_nid_postfix("probe", 2, 1, 0); + Require(named >= 0 && sceKernelFcntl(named, GetFlags) == ReadWrite && close_nid_postfix(named) == 0); } static void FileFlagsRoundTrip() { @@ -155,6 +241,7 @@ static void FileFlagsRoundTrip() { Require(ioctl_nid_postfix(file, ReadableRequest, &readable) == 0 && readable == 3); char size[8] = {}; RequireFailure(ioctl_nid_postfix(file, WindowSizeRequest, size), GuestEnotty); + RequireMalformedRequestsFail(file); RequireFailure(ioctl_nid_postfix(file, NonblockingRequest, nullptr), GuestEfault); Require(ThrowsWith([file] { int on = 1; ioctl_nid_postfix(file, AsyncRequest, &on); }, "ioctl: request 0x8004667D on a file is not implemented")); @@ -166,6 +253,10 @@ static void FileFlagsRoundTrip() { Require(fcntl_nid_postfix(file, SetLock, &lock) == 0); GuestLock probe{0, 0, 0, 1, 0, 0}; Require(fcntl_nid_postfix(file, GetLock, &probe) == 0 && probe.type == 2); + static GuestLock imageProbe{0, 0, 0, 1, 0, 0}; + Require(_fcntl_nid_postfix(file, GetLock, &imageProbe) == 0 && imageProbe.type == 2); + Require(_fcntl_nid_postfix(file, SetDescriptorFlags, 1) == 0 && _fcntl_nid_postfix(file, GetDescriptorFlags) == 1); + Require(_fcntl_nid_postfix(file, SetDescriptorFlags, 0) == 0); lock.type = 2; Require(fcntl_nid_postfix(file, SetLock, &lock) == 0); lock.type = 7; @@ -175,6 +266,8 @@ static void FileFlagsRoundTrip() { RequireFailure(fcntl_nid_postfix(file, GetDescriptorFlags), GuestEbadf); RequireFailure(fcntl_nid_postfix(file, DuplicateCommand, 0), GuestEbadf); RequireFailure(ioctl_nid_postfix(file, ReadableRequest, &readable), GuestEbadf); + RequireMalformedRequestsFail(file); + RequireMalformedRequestsFail(-1); Require(sceKernelFcntl(file, GetFlags) == SceEbadf); const int appending = sceKernelOpen("kernel_fcntl_probe.bin", CreateWriteAppend, 0644); @@ -287,8 +380,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); @@ -368,10 +466,32 @@ static void PipeFlagsRoundTrip() { RequireFailure(fcntl_nid_postfix(pipe[1], SetLock, &lock), GuestEbadf); char size[8] = {}; RequireFailure(ioctl_nid_postfix(pipe[0], WindowSizeRequest, size), GuestEnotty); + RequireMalformedRequestsFail(pipe[0]); Require(ThrowsWith([&pipe] { fcntl_nid_postfix(pipe[0], SetFlags, NonBlocking | 0x40); }, "fcntl: O_ASYNC on a pipe is not implemented")); for (const int descriptor : pipe) Require(close_nid_postfix(descriptor) == 0); } +#ifndef _WIN32 +static void HostSocketFlags() { + int sockets[2] = {-1, -1}; + Require(::socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == 0); + Require(fcntl_nid_postfix(sockets[0], GetFlags) == ReadWrite); + Require(fcntl_nid_postfix(sockets[0], SetFlags, NonBlocking) == 0); + Require(fcntl_nid_postfix(sockets[0], GetFlags) == (ReadWrite | NonBlocking)); + Require((::fcntl(sockets[0], F_GETFL) & O_NONBLOCK) != 0); + Require(fcntl_nid_postfix(sockets[0], SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(sockets[0], GetDescriptorFlags) == 1); + Require(::write(sockets[1], "abc", 3) == 3); + int available = 0; + Require(ioctl_nid_postfix(sockets[0], ReadableRequest, &available) == 0 && available == 3); + GuestLock lock{0, 0, 0, WriteLock, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(sockets[0], SetLock, &lock), GuestEbadf); + RequireFailure(fcntl_nid_postfix(sockets[0], DarwinReadAhead, 1), GuestEbadf); + Require(ThrowsWith([&sockets] { fcntl_nid_postfix(sockets[0], SetFlags, NonBlocking | 0x40); }, "fcntl: O_ASYNC on a socket is not implemented")); + for (const int socket : sockets) Require(close_nid_postfix(socket) == 0); +} +#endif + static void StandardInputFlags() { const int flags = fcntl_nid_postfix(0, GetFlags); Require(flags >= 0); @@ -389,6 +509,9 @@ int main() { FileFlagsRoundTrip(); RecordLocksBelongToTheProcess(); PipeFlagsRoundTrip(); +#ifndef _WIN32 + HostSocketFlags(); +#endif StandardInputFlags(); return 0; } diff --git a/core/libs/tests/GuestKernelIsStack.cpp b/core/libs/tests/GuestKernelIsStack.cpp index 941ffdc975..968cdc981f 100644 --- a/core/libs/tests/GuestKernelIsStack.cpp +++ b/core/libs/tests/GuestKernelIsStack.cpp @@ -13,7 +13,11 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf } static constexpr int SCE_OK = 0; +#ifdef ANYPS5_ENABLE_ASAN +static constexpr std::uintptr_t APPLICATION_AREA_END = 0x000010FC00000000ull; +#else static constexpr std::uintptr_t APPLICATION_AREA_END = 0x000000FC00000000ull; +#endif static constexpr int SCE_KERNEL_ERROR_EACCES = static_cast(0x8002000d); static int global = 0; diff --git a/core/libs/tests/GuestKernelStackProtect.cpp b/core/libs/tests/GuestKernelStackProtect.cpp index f946e321ae..a980ea9d09 100644 --- a/core/libs/tests/GuestKernelStackProtect.cpp +++ b/core/libs/tests/GuestKernelStackProtect.cpp @@ -1,22 +1,33 @@ #include "SceTypes.hpp" +#include #include +#include #include #include +#include +#include #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX #endif #include #else -#include #include +#include +#include +#include #endif 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 scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t destructor); +int APS5_VABI scePthreadKeyDelete(PthreadKey key); +int APS5_VABI scePthreadSetspecific(PthreadKey key, void* value); int APS5_VABI sceKernelIsStack(void* addr, void** start, void** end); +void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protection, int flags, int descriptor, std::int64_t offset) noexcept; int APS5_VABI mprotect_nid_postfix(void* address, std::size_t length, int protection) noexcept; +int* APS5_VABI __error_nid_postfix(); } static constexpr int SCE_OK = 0; @@ -73,6 +84,134 @@ static void UnmapHostPages(void* pages) { #endif } +#ifdef _WIN32 +static void CheckFixedStackRemap(std::uintptr_t low, void* start) { + constexpr int FixedPrivateAnonymous = 0x1012; + auto* const before = reinterpret_cast(low); + auto* const page = reinterpret_cast(low + Page); + auto* const after = reinterpret_cast(low + 2 * Page); + auto* const address = const_cast(page); + for (std::size_t index = 0; index < Page; ++index) { + before[index] = 0x35; + page[index] = 0xa7; + after[index] = 0x59; + } + const auto checkBytes = [&](unsigned char expected) { + for (std::size_t index = 0; index < Page; ++index) { + Require(before[index] == 0x35); + Require(page[index] == expected); + Require(after[index] == 0x59); + } + }; + const auto reject = [&](void* candidate, std::size_t bytes, int flags, int descriptor, std::int64_t offset, int error) { + *__error_nid_postfix() = 0; + Require(mmap_nid_postfix(candidate, bytes, 0, flags, descriptor, offset) == reinterpret_cast(static_cast(-1))); + if (error != 0) Require(*__error_nid_postfix() == error); + checkBytes(0xa7); + }; + reject(address + 1, Page, FixedPrivateAnonymous, -1, 0, 22); + reject(address, Page, FixedPrivateAnonymous, 0, 0, 22); + reject(address, Page, FixedPrivateAnonymous, -1, Page, 22); + reject(address, Page, FixedPrivateAnonymous | 0x4000, -1, 0, 0); + int active = 0; + const auto activePage = reinterpret_cast(&active) & ~(Page - 1); + void* activeStart = nullptr; + void* activeEnd = nullptr; + Require(sceKernelIsStack(&active, &activeStart, &activeEnd) == SCE_OK); + Require(activePage >= reinterpret_cast(activeStart)); + Require(activePage + Page <= reinterpret_cast(activeEnd)); + const auto activeBefore = QueryPage(activePage).raw; + const auto activeEndBefore = QueryPage(activePage + Page - 1).raw; + reject(reinterpret_cast(activePage), Page, FixedPrivateAnonymous, -1, 0, 22); + Require(QueryPage(activePage).raw == activeBefore); + Require(QueryPage(activePage + Page - 1).raw == activeEndBefore); + const auto boundary = reinterpret_cast(start) & ~(Page - 1); + Require(boundary >= Page); + const auto outside = boundary - Page; + const auto outsideBefore = QueryPage(outside).raw; + reject(reinterpret_cast(outside), 2 * Page, FixedPrivateAnonymous, -1, 0, 22); + Require(QueryPage(outside).raw == outsideBefore); + + void* const host = MapHostPages(); + Require(host != nullptr); + auto* const hostBytes = static_cast(host); + hostBytes[0] = 0x63; + hostBytes[HostBytes - 1] = 0x94; + const auto hostBefore = QueryPage(reinterpret_cast(host)).raw; + reject(host, Page, FixedPrivateAnonymous, -1, 0, 22); + Require(QueryPage(reinterpret_cast(host)).raw == hostBefore); + Require(hostBytes[0] == 0x63 && hostBytes[HostBytes - 1] == 0x94); + UnmapHostPages(host); + + SYSTEM_INFO system{}; + GetSystemInfo(&system); + const auto hostPage = static_cast(system.dwPageSize); + Require(hostPage != 0 && 2 * hostPage <= Page); + auto* const guarded = address + hostPage; + DWORD originalProtection = 0; + Require(VirtualProtect(guarded, hostPage, PAGE_READWRITE | PAGE_GUARD, &originalProtection) != 0); + Require(originalProtection == PAGE_READWRITE); + *__error_nid_postfix() = 0; + Require(mmap_nid_postfix(address, Page, 0, FixedPrivateAnonymous, -1, 0) == reinterpret_cast(static_cast(-1))); + Require(*__error_nid_postfix() == 22); + MEMORY_BASIC_INFORMATION guardedMemory{}; + Require(VirtualQuery(guarded, &guardedMemory, sizeof(guardedMemory)) == sizeof(guardedMemory)); + Require(guardedMemory.State == MEM_COMMIT && guardedMemory.Protect == (PAGE_READWRITE | PAGE_GUARD)); + DWORD replacedProtection = 0; + Require(VirtualProtect(guarded, hostPage, originalProtection, &replacedProtection) != 0); + Require(replacedProtection == (PAGE_READWRITE | PAGE_GUARD)); + checkBytes(0xa7); + + Require(mmap_nid_postfix(address, Page, 0, FixedPrivateAnonymous, -1, 0) == address); + MEMORY_BASIC_INFORMATION memory{}; + Require(VirtualQuery(address, &memory, sizeof(memory)) == sizeof(memory)); + Require(memory.State == MEM_COMMIT && memory.Protect == PAGE_NOACCESS); + Require(reinterpret_cast(memory.BaseAddress) + memory.RegionSize >= low + 2 * Page); + Require(mprotect_nid_postfix(address, Page, 3) == 0); + checkBytes(0); + for (std::size_t index = 0; index < Page; ++index) page[index] = 0xc8; + Require(mmap_nid_postfix(address, Page, 3, FixedPrivateAnonymous, -1, 0) == address); + Require(QueryPage(low + Page).writable); + checkBytes(0); + page[0] = 0x76; + page[Page - 1] = 0x82; + Require(page[0] == 0x76 && page[Page - 1] == 0x82); +} + +struct PeerStack { + std::atomic phase{0}; + void* start = nullptr; + std::uintptr_t low = 0; +}; + +static void* APS5_VABI WaitForPeerRemap(void* arg) { + auto& context = *static_cast(arg); + int local = 0; + void* end = nullptr; + Require(sceKernelIsStack(&local, &context.start, &end) == SCE_OK); + context.low = (reinterpret_cast(context.start) + Page - 1) & ~(Page - 1); + Require(context.low + GuardBytes + Page < reinterpret_cast(&local)); + Require(mprotect_nid_postfix(reinterpret_cast(context.low), GuardBytes, 3) == 0); + context.phase.store(1); + context.phase.notify_one(); + context.phase.wait(1); + Require(context.phase.load() == 2); + return nullptr; +} + +static void CheckPeerStackRemap() { + PeerStack context; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, WaitForPeerRemap, &context, nullptr) == SCE_OK); + context.phase.wait(0); + Require(context.phase.load() == 1); + CheckFixedStackRemap(context.low, context.start); + context.phase.store(2); + context.phase.notify_one(); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); +} +#endif + static void* APS5_VABI Worker(void* arg) { auto& passed = *static_cast*>(arg); int local = 0; @@ -119,6 +258,9 @@ static void* APS5_VABI Worker(void* arg) { Require(QueryPage(low).writable); static_cast(start)[0] = 4; Require(mprotect_nid_postfix(start, stackBytes, 3) == 0); +#ifdef _WIN32 + CheckFixedStackRemap(low, start); +#endif void* host = MapHostPages(); Require(host != nullptr); Require(mprotect_nid_postfix(host, HostBytes, 1) == -1); @@ -129,47 +271,288 @@ static void* APS5_VABI Worker(void* arg) { return nullptr; } -static std::atomic leftGuard{0}; +enum class GuardMode { Protect, Remap, Split }; -static void* APS5_VABI LeaveGuard(void*) { +struct GuardContext { + GuardMode mode; + PthreadKey key = -1; + std::uintptr_t low = 0; + std::atomic calls{0}; +}; + +static void APS5_VABI CheckRestoredGuard(void* arg) { + auto& context = *static_cast(arg); + Require(context.low != 0); + auto* const bytes = reinterpret_cast(context.low); + for (std::uintptr_t offset = 0; offset < GuardBytes; offset += Page) { + Require(QueryPage(context.low + offset).writable); + bytes[offset] = 4; + bytes[offset + Page - 1] = 5; + Require(bytes[offset] == 4 && bytes[offset + Page - 1] == 5); + } + context.calls.fetch_add(1); +} + +static void* APS5_VABI LeaveGuard(void* arg) { + auto& context = *static_cast(arg); int local = 0; void* start = nullptr; void* end = nullptr; Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); const auto low = (reinterpret_cast(start) + Page - 1) & ~(Page - 1); - Require(mprotect_nid_postfix(reinterpret_cast(low), GuardBytes, 0) == 0); - leftGuard.store(low); - return nullptr; -} - -static void* APS5_VABI WriteLeftGuard(void*) { - int local = 0; - void* start = nullptr; - void* end = nullptr; - Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); - const auto guard = leftGuard.load(); - if (guard >= reinterpret_cast(start) && guard + GuardBytes <= reinterpret_cast(&local)) { - reinterpret_cast(guard)[0] = 4; + Require(low + GuardBytes + Page < reinterpret_cast(&local)); + context.low = low; + Require(scePthreadSetspecific(context.key, &context) == SCE_OK); + const auto mode = context.mode; + if (mode == GuardMode::Remap) { + Require(mmap_nid_postfix(reinterpret_cast(low), Page, 0, 0x1012, -1, 0) == reinterpret_cast(low)); + } else { + Require(mprotect_nid_postfix(reinterpret_cast(low), GuardBytes, 0) == 0); + if (mode == GuardMode::Split) { + const auto leftBefore = QueryPage(low).raw; + const auto rightBefore = QueryPage(low + 2 * Page).raw; + const auto rightEndBefore = QueryPage(low + 3 * Page).raw; + auto* const middle = reinterpret_cast(low + Page); + Require(mmap_nid_postfix(middle, Page, 3, 0x1012, -1, 0) == middle); + Require(QueryPage(low + Page).writable); + Require(QueryPage(low).raw == leftBefore); + Require(QueryPage(low + 2 * Page).raw == rightBefore); + Require(QueryPage(low + 3 * Page).raw == rightEndBefore); + } } return nullptr; } -int main() { +static void CheckGuardCleanup(GuardMode mode) { + GuardContext context{mode}; + Require(scePthreadKeyCreate(&context.key, reinterpret_cast(CheckRestoredGuard)) == SCE_OK); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, LeaveGuard, &context, nullptr) == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(context.calls.load() == 1); + Require(scePthreadKeyDelete(context.key) == SCE_OK); +} + +#ifdef _WIN32 +struct HostStackLayout { + std::uintptr_t guard; + std::uintptr_t committed; +}; + +static HostStackLayout QueryHostStack(std::uintptr_t start, std::uintptr_t end) { + HostStackLayout layout{0, end}; + for (auto cursor = start; cursor < end;) { + MEMORY_BASIC_INFORMATION memory{}; + Require(VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) == sizeof(memory)); + if (memory.State == MEM_COMMIT && (memory.Protect & PAGE_GUARD) != 0) layout.guard = cursor; + else if (memory.State == MEM_COMMIT && layout.guard != 0 && layout.committed == end) layout.committed = cursor; + cursor = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); + } + return layout; +} + +static void GrowStack(std::uintptr_t below) { + static void (*volatile const next)(std::uintptr_t) = GrowStack; + volatile unsigned char frame[1024]; + frame[0] = 1; + if (reinterpret_cast(&frame[0]) > below) next(below); + frame[sizeof(frame) - 1] = 2; +} + +static std::uintptr_t StackLimit() { + return reinterpret_cast(reinterpret_cast(NtCurrentTeb())->StackLimit); +} + +static bool Readable(std::uintptr_t address) { + unsigned char byte = 0; + SIZE_T read = 0; + return ReadProcessMemory(GetCurrentProcess(), reinterpret_cast(address), &byte, 1, &read) != 0 && read == 1; +} + +static void RequireGrowableStack(std::uintptr_t start, std::uintptr_t end) { + const auto before = QueryHostStack(start, end); + Require(before.guard > start && before.guard < before.committed); + Require(QueryPage(before.committed).writable); + Require(StackLimit() == before.committed); + GrowStack(before.guard - 0x2000); + const auto after = QueryHostStack(start, end); + Require(after.guard != 0 && after.guard < before.guard); + Require(QueryPage(before.guard).writable); + Require(StackLimit() == after.committed); +} +#endif + +static void ProtectStack(std::uintptr_t skippedBytes) { + int local = 0; + void* start = nullptr; + void* end = nullptr; + Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); + const auto low = reinterpret_cast(start) + skippedBytes; + const auto high = reinterpret_cast(end); + Require(mprotect_nid_postfix(reinterpret_cast(low), high - low, 0xc2) == 0); +#ifdef _WIN32 + RequireGrowableStack(reinterpret_cast(start), high); + Require(mprotect_nid_postfix(start, Page, 0) == 0); + RequireGrowableStack(reinterpret_cast(start), high); +#else + Require(QueryPage(low).writable); + reinterpret_cast(low)[0] = 6; +#endif +} + +#ifdef _WIN32 +static DWORD WINAPI ProtectReservedStack(void* partial) { + int local = 0; + void* start = nullptr; + void* end = nullptr; + Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); + const auto low = reinterpret_cast(start); + const auto guard = QueryHostStack(low, reinterpret_cast(end)).guard; + Require(guard - low >= 2 * Page); + ProtectStack(partial != nullptr ? ((guard - low) / 2) & ~(Page - 1) : 0); + return 0; +} + +static DWORD WINAPI ProtectStackBottom(void*) { + int local = 0; + void* start = nullptr; + void* end = nullptr; + Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); + const auto low = reinterpret_cast(start); + const auto high = reinterpret_cast(end); + const auto island = low + 3 * Page; + Require(QueryHostStack(low, high).guard - low >= 4 * Page); + Require(mprotect_nid_postfix(start, 3 * Page, 3) == 0); + Require(QueryPage(low).writable && QueryPage(island - 1).writable); + reinterpret_cast(low)[0] = 7; + Require(mprotect_nid_postfix(start, 3 * Page, 0) == 0); + Require(QueryPage(low).mapped && !QueryPage(island - 1).writable); + Require(!Readable(low) && !Readable(island - 1)); + RequireGrowableStack(low, high); + const auto layout = QueryHostStack(low, high); + const auto guardBytes = layout.committed - layout.guard; + GrowStack(island + guardBytes + 0xf00); + const auto aboveIsland = QueryHostStack(low, high); + Require(aboveIsland.guard == island); + Require(StackLimit() == aboveIsland.committed); + Require(QueryPage(island - 1).mapped && !QueryPage(island - 1).writable && !Readable(island - 1)); + GrowStack(island + guardBytes - 0x100); + const auto intoIsland = QueryHostStack(low, high); + Require(intoIsland.guard == island - 0x1000); + Require(StackLimit() == intoIsland.committed); + Require(QueryPage(island - 1).raw == ((static_cast(MEM_COMMIT) << 32) | (PAGE_READWRITE | PAGE_GUARD))); + Require(QueryPage(island - 0x1001).mapped && !QueryPage(island - 0x1001).writable && !Readable(island - 0x1001) && !Readable(low)); + return 0; +} + +static void RunOnReservedStack(LPTHREAD_START_ROUTINE routine, void* arg) { + HANDLE thread = CreateThread(nullptr, HostBytes * 4, routine, arg, STACK_SIZE_PARAM_IS_A_RESERVATION, nullptr); + Require(thread != nullptr); + Require(WaitForSingleObject(thread, INFINITE) == WAIT_OBJECT_0); + DWORD exitCode = 1; + Require(GetExitCodeThread(thread, &exitCode) != 0 && exitCode == 0); + Require(CloseHandle(thread) != 0); +} + +static void ProtectHostStacks() { + ProtectStack(0); + for (void* partial : {static_cast(nullptr), reinterpret_cast(1)}) RunOnReservedStack(ProtectReservedStack, partial); + RunOnReservedStack(ProtectStackBottom, nullptr); +} +#else +static int ProtectUnlimitedStack() { + int local = 0; + void* start = nullptr; + void* end = nullptr; + Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); + const auto low = (reinterpret_cast(start) + Page - 1) & ~(Page - 1); + const auto high = reinterpret_cast(end); + if (high - low < (std::uintptr_t{1} << 40)) { + std::puts("skipped the unlimited main stack check, the reported stack is smaller than 1 TiB"); + return 0; + } + alarm(5); + Require(mprotect_nid_postfix(reinterpret_cast(low), high - low, 3) == 0); + reinterpret_cast(low)[0] = 9; + return 0; +} + +static int ProtectMainStackBottom(std::initializer_list protections, std::uint64_t raw) { + int local = 0; + void* start = nullptr; + void* end = nullptr; + Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); + const auto low = (reinterpret_cast(start) + Page - 1) & ~(Page - 1); + if (QueryPage(low + GuardBytes + 0x100000).mapped) { + std::puts("skipped the main stack bottom checks, the stack is mapped within 1 MiB of its bottom"); + return 0; + } + for (const int protection : protections) { + Require(mprotect_nid_postfix(reinterpret_cast(low), GuardBytes, protection) == 0); + void* startAfter = nullptr; + void* endAfter = nullptr; + Require(sceKernelIsStack(&local, &startAfter, &endAfter) == SCE_OK && startAfter == start && endAfter == end); + } + for (const auto offset : {GuardBytes, GuardBytes + 0x40000}) reinterpret_cast(low + offset)[0] = 8; + Require(QueryPage(low).raw == raw); + return 0; +} + +static bool OvercommitStrict() { + std::FILE* setting = std::fopen("/proc/sys/vm/overcommit_memory", "r"); + if (setting == nullptr) return false; + const bool strict = std::fgetc(setting) == '2'; + std::fclose(setting); + return strict; +} + +static bool ChildPasses(const char* mode, bool unlimitedStack) { + const pid_t child = fork(); + Require(child >= 0); + if (child == 0) { + rlimit limit{}; + if (unlimitedStack && getrlimit(RLIMIT_STACK, &limit) == 0) limit.rlim_cur = RLIM_INFINITY; + if (!unlimitedStack || setrlimit(RLIMIT_STACK, &limit) == 0) execl("/proc/self/exe", "/proc/self/exe", mode, static_cast(nullptr)); + std::_Exit(127); + } + int status = 0; + Require(waitpid(child, &status, 0) == child); + return WIFEXITED(status) && WEXITSTATUS(status) == 0; +} + +static void ProtectMainStacks() { + for (const char* mode : {"--read-only-bottom", "--reprotect-bottom"}) Require(ChildPasses(mode, false)); + rlimit limit{}; + Require(getrlimit(RLIMIT_STACK, &limit) == 0); + if (limit.rlim_max != RLIM_INFINITY || OvercommitStrict()) { + std::puts("skipped the unlimited main stack check, the stack limit cannot be raised or overcommit is strict"); + return; + } + Require(ChildPasses("--unlimited-stack", true)); +} +#endif + +int main(int argc, char** argv) { +#ifdef _WIN32 + (void)argc; + (void)argv; + ProtectHostStacks(); +#else + if (argc > 1 && std::strcmp(argv[1], "--unlimited-stack") == 0) return ProtectUnlimitedStack(); + if (argc > 1 && std::strcmp(argv[1], "--read-only-bottom") == 0) return ProtectMainStackBottom({1}, 3); + if (argc > 1 && std::strcmp(argv[1], "--reprotect-bottom") == 0) return ProtectMainStackBottom({0, 3}, 7); + ProtectMainStacks(); + ProtectStack(0); +#endif std::atomic passed{false}; Pthread thread = nullptr; Require(scePthreadCreate(&thread, nullptr, Worker, &passed, nullptr) == SCE_OK); Require(scePthreadJoin(thread, nullptr) == SCE_OK); Require(passed.load()); - Require(scePthreadCreate(&thread, nullptr, LeaveGuard, nullptr, nullptr) == SCE_OK); - Require(scePthreadJoin(thread, nullptr) == SCE_OK); - for (std::uintptr_t offset = 0; offset < GuardBytes; offset += Page) { - const auto left = QueryPage(leftGuard.load() + offset); -#ifdef __linux__ - Require(!left.mapped || left.writable || left.raw == 1); -#else - Require(!left.mapped || left.writable); + CheckGuardCleanup(GuardMode::Protect); +#ifdef _WIN32 + CheckPeerStackRemap(); + CheckGuardCleanup(GuardMode::Remap); + CheckGuardCleanup(GuardMode::Split); #endif - } - Require(scePthreadCreate(&thread, nullptr, WriteLeftGuard, nullptr, nullptr) == SCE_OK); - Require(scePthreadJoin(thread, nullptr) == SCE_OK); } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index d16ff6789a..b481f508fe 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -1,5 +1,9 @@ +#include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/RemoveTree.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" +#include "SceTypes.hpp" +#include #include #include #include @@ -9,14 +13,16 @@ #include #include #include +#include #include #ifdef _WIN32 #include +#include #ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE #define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2 #endif #endif -#ifdef __linux__ +#ifndef _WIN32 #include #include #endif @@ -24,9 +30,14 @@ extern "C" { int APS5_VABI fstatfs_nid_postfix(int, void*); int APS5_VABI _fstatfs_nid_postfix(int, void*); +int APS5_VABI fstat_nid_postfix(int, FileStat*); +std::int64_t APS5_VABI _fstat_nid_postfix(int, FileStat*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI socket_nid_postfix(int, int, int); std::int64_t APS5_VABI readlink_nid_postfix(const char*, char*, std::size_t); +int APS5_VABI lstat_nid_postfix(const char*, FileStat*); std::int64_t APS5_VABI pathconf_nid_postfix(const char*, int); int APS5_VABI fchdir_nid_postfix(int); int APS5_VABI chdir_nid_postfix(const char*); @@ -34,6 +45,13 @@ int APS5_VABI chroot_nid_postfix(const char*); char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); +int APS5_VABI sceKernelRename(const char*, const char*); +int APS5_VABI sceKernelFstat(int, FileStat*); +int APS5_VABI sceKernelGetdents(int, char*, int); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); +int APS5_VABI dup_nid_postfix(int); +int APS5_VABI dup2_nid_postfix(int, int); int APS5_VABI setsid_nid_postfix(void); int APS5_VABI link_nid_postfix(const char*, const char*); int APS5_VABI symlink_nid_postfix(const char*, const char*); @@ -43,6 +61,9 @@ std::int64_t APS5_VABI readlinkat_nid_postfix(int, const char*, char*, std::size int APS5_VABI mkdirat_nid_postfix(int, const char*, std::uint16_t); int APS5_VABI renameat_nid_postfix(int, const char*, int, const char*); int APS5_VABI fchmodat_nid_postfix(int, const char*, int, int); +int APS5_VABI lchmod_nid_postfix(const char*, std::uint16_t); +int APS5_VABI fchown_nid_postfix(int, std::uint32_t, std::uint32_t); +int APS5_VABI fstatat_nid_postfix(int, const char*, FileStat*, int); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t); int* APS5_VABI __error_nid_postfix(); @@ -77,6 +98,57 @@ bool ChrootThrows(const std::string& path) { return false; } +bool SymlinkTargetThrows(const char* target, const std::string& created) { + try { + symlink_nid_postfix(target, created.c_str()); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +bool RemoveSymlink(const std::string& link) { +#ifdef _WIN32 + // std::filesystem::remove follows the link, reports a dangling one as + // missing and clears the error, so remove the reparse point itself. A + // directory reparse point, such as the one the "..." target creates, is + // refused by DeleteFileW and needs RemoveDirectoryW instead. + const auto wide = std::filesystem::path(link).wstring(); + return DeleteFileW(wide.c_str()) != FALSE || RemoveDirectoryW(wide.c_str()) != FALSE; +#else + return std::filesystem::remove(link); +#endif +} + +void CheckSymlinkTargetRestrictions(const std::filesystem::path& root) { + const auto created = (root / "rejected").string(); + for (const auto& target : {std::string(".."), std::string("../x"), std::string("a/../b"), std::string("a/../../x"), std::string("/abs")}) + Require(SymlinkTargetThrows(target.c_str(), created)); +#ifdef _WIN32 + for (const auto& target : {std::string("C:/"), std::string("\\\\server\\share")}) + Require(SymlinkTargetThrows(target.c_str(), created)); +#endif + const auto links = [](const std::string& created, const char* target) { + char buffer[16]; + const auto length = std::strlen(target); + return readlink_nid_postfix(created.c_str(), buffer, sizeof(buffer)) == static_cast(length) && + std::memcmp(buffer, target, length) == 0; + }; + const auto nested = (root / "accepted-nested").string(); + const int link = symlink_nid_postfix("sub/dir", nested.c_str()); + if (link == 0) { + Require(links(nested, "sub/dir")); + const auto dots = (root / "accepted-dots").string(); + Require(symlink_nid_postfix("a..b", dots.c_str()) == 0 && links(dots, "a..b")); + const auto ellipsis = (root / "accepted-ellipsis").string(); + Require(symlink_nid_postfix("...", ellipsis.c_str()) == 0 && links(ellipsis, "...")); + Require(RemoveSymlink(nested) && RemoveSymlink(dots) && RemoveSymlink(ellipsis)); + } else { + Require(link == -1 && Error() == 1); + std::fprintf(stderr, "symbolic links unavailable: the symlink target success cases not tested\n"); + } +} + void CheckPathconf(const std::filesystem::path& root, const std::string& file) { const auto directory = root.generic_string(); for (const auto& path : {file, directory}) { @@ -110,7 +182,93 @@ void CheckPathconf(const std::filesystem::path& root, const std::string& file) { Require(pathconf_nid_postfix(longPath.c_str(), 4) == -1 && Error() == 63); } +void DirectoryRenameTests(const std::filesystem::path& parent) { + const auto fixture = parent / "directory-identity"; + const auto original = fixture / "original"; + const auto moved = fixture / "moved"; + const auto twice = fixture / "twice"; + Require(std::filesystem::create_directory(fixture)); + Require(std::filesystem::create_directory(original)); + { std::ofstream(original / "marker").put('o'); } + { std::ofstream(original / "old-only").put('a'); } + const auto readMarker = [](int directory) { + const int file = openat_nid_postfix(directory, "marker", SCE_KERNEL_O_RDONLY); + Require(file >= 0); + char marker = 0; + Require(sceKernelRead(file, &marker, 1) == 1); + Require(sceKernelClose(file) == 0); + return marker; + }; + const auto names = [](int directory) { + std::set result; + std::array buffer{}; + for (;;) { + const int bytes = sceKernelGetdents(directory, buffer.data(), buffer.size()); + Require(bytes >= 0); + if (bytes == 0) return result; + for (int offset = 0; offset < bytes;) { + std::uint16_t length = 0; + std::memcpy(&length, buffer.data() + offset + 4, sizeof(length)); + Require(length >= 12 && offset + length <= bytes); + result.emplace(buffer.data() + offset + 8); + offset += length; + } + } + }; + const int first = sceKernelOpen(original.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(first >= 0 && readMarker(first) == 'o'); + FileStat before{}; + Require(sceKernelFstat(first, &before) == 0 && (before.st_mode & 0170000) == 0040000); + const int duplicate = dup_nid_postfix(first); + const int target = sceKernelOpen((original / "marker").string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(duplicate >= 0 && target >= 0); + Require(dup2_nid_postfix(first, target) == target); + Require(sceKernelClose(first) == 0); + Require(sceKernelRename(original.string().c_str(), moved.string().c_str()) == 0); + Require(std::filesystem::create_directory(original)); + { std::ofstream(original / "marker").put('r'); } + { std::ofstream(original / "new-only").put('b'); } + const char marker = readMarker(duplicate); + std::fprintf(stderr, "directory marker after rename/replacement: %c, expected o\n", marker); + Require(marker == 'o' && readMarker(target) == 'o'); + const std::set expected{".", "..", "marker", "old-only"}; + Require(names(duplicate) == expected); + Require(names(target).empty()); + FileStat after{}; + Require(sceKernelFstat(target, &after) == 0 && after.st_dev == before.st_dev && after.st_ino == before.st_ino); + Require(sceKernelLseek(target, 0, 0) == 0); + char directoryByte = 0; + Require(sceKernelRead(target, &directoryByte, 1) == static_cast(0x80020015u)); + Require(sceKernelPread(target, &directoryByte, 1, 0) == static_cast(0x80020015u)); + char previous[4096]; + Require(getcwd_nid_postfix(previous, sizeof(previous)) == previous); + Require(fchdir_nid_postfix(target) == 0 && readMarker(-100) == 'o'); + Require(chdir_nid_postfix(previous) == 0); + Require(mkdirat_nid_postfix(target, "created", 0755) == 0); + Require(std::filesystem::is_directory(moved / "created") && !std::filesystem::exists(original / "created")); + Require(unlinkat_nid_postfix(duplicate, "created", 0x800) == 0); + Require(sceKernelRename(moved.string().c_str(), twice.string().c_str()) == 0); + Require(std::filesystem::create_directory(moved)); + { std::ofstream(moved / "different").put('c'); } + Require(sceKernelLseek(target, 0, 0) == 0 && names(target) == expected); + Require(sceKernelClose(duplicate) == 0); + Require(readMarker(target) == 'o'); + Require(unlinkat_nid_postfix(target, "marker", 0) == 0); + Require(unlinkat_nid_postfix(target, "old-only", 0) == 0); + Require(std::filesystem::remove(twice)); + Require(sceKernelFstat(target, &after) == 0 && after.st_dev == before.st_dev && after.st_ino == before.st_ino && (after.st_mode & 0170000) == 0040000); + Require(sceKernelClose(target) == 0); + Require(std::filesystem::remove(original / "marker") && std::filesystem::remove(original / "new-only")); + Require(std::filesystem::remove(moved / "different")); + Require(std::filesystem::remove(original) && std::filesystem::remove(moved) && std::filesystem::remove(fixture)); +} + int main() { +#ifdef _WIN32 + auto hostDirectorySpelling = std::filesystem::current_path().native(); + CharUpperBuffW(hostDirectorySpelling.data(), static_cast(hostDirectorySpelling.size())); + Require(SetCurrentDirectoryW(hostDirectorySpelling.c_str()) != 0); +#endif Require(setsid_nid_postfix() == -1 && Error() == 1); Require(umask_nid_postfix(0) == 022); Require(umask_nid_postfix(07777) == 0); @@ -118,6 +276,7 @@ int main() { const auto root = std::filesystem::path("anyps5-kernel-sys-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); + DirectoryRenameTests(root); const auto file = (root / "file.txt").string(); std::ofstream(file).put('x'); CheckPathconf(root, file); @@ -129,8 +288,15 @@ int main() { Require(std::memcmp(viaPublic, viaSyscall, 4) == 0 && std::memcmp(viaPublic + 16, viaSyscall + 16, 8) == 0); Require(std::memcmp(viaPublic + 280, viaSyscall + 280, 16) == 0); Require(fstatfs_nid_postfix(descriptor, nullptr) == -1 && Error() == 14); + FileStat viaFstat{}; + FileStat viaUnderscore{}; + Require(fstat_nid_postfix(descriptor, &viaFstat) == 0 && _fstat_nid_postfix(descriptor, &viaUnderscore) == 0); + Require(std::memcmp(&viaFstat, &viaUnderscore, sizeof(FileStat)) == 0 && viaFstat.st_size == 1); + Require(fstat_nid_postfix(descriptor, nullptr) == -1 && Error() == 14); Require(close_nid_postfix(descriptor) == 0); Require(fstatfs_nid_postfix(-1, viaPublic) == -1 && Error() == 9); + Require(fstat_nid_postfix(descriptor, &viaFstat) == -1 && Error() == 9); + Require(fstat_nid_postfix(-1, &viaFstat) == -1 && Error() == 9); char buffer[64]; for (const auto& component : {std::string("missing"), std::string("file.txt")}) { const auto prefix = root.generic_string() + "/" + component + "/../"; @@ -143,13 +309,85 @@ int main() { Require(symlink_nid_postfix("file.txt", created.c_str()) == -1 && Error() == expected); Require(std::filesystem::hard_link_count(file) == 1 && !std::filesystem::exists(root / "new.txt")); } + for (const std::size_t length : {std::size_t{1024}, std::size_t{1100}}) { + const auto tooLong = std::string(length, 'a'); + const auto created = (root / "target-too-long").string(); + Require(symlink_nid_postfix(tooLong.c_str(), created.c_str()) == -1 && Error() == 63); + Require(!std::filesystem::exists(std::filesystem::symlink_status(root / "target-too-long"))); + } + { + const auto longest = std::string(1023, 'a'); + const auto created = (root / "target-longest").string(); + const int made = symlink_nid_postfix(longest.c_str(), created.c_str()); + Require(made == 0 || Error() != 63); + if (made == 0) Require(RemoveSymlink(created)); + } Require(readlink_nid_postfix(file.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix(root.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix((root / "missing").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(readlink_nid_postfix((root / "file.txt" / "child").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 20); Require(readlink_nid_postfix("", buffer, sizeof(buffer)) == -1 && Error() == 2); Require(readlink_nid_postfix(nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); + constexpr auto hugeSize = static_cast(-1); + Require(readlink_nid_postfix(nullptr, buffer, hugeSize) == -1 && Error() == 22); + Require(readlink_nid_postfix("", buffer, hugeSize) == -1 && Error() == 22); + Require(readlink_nid_postfix((root / "missing").string().c_str(), buffer, hugeSize) == -1 && Error() == 22); + Require(readlinkat_nid_postfix(-100, nullptr, buffer, hugeSize) == -1 && Error() == 22); + Require(readlinkat_nid_postfix(-100, "", buffer, hugeSize) == -1 && Error() == 22); + Require(readlinkat_nid_postfix(-100, nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); #ifndef _WIN32 Require(readlink_nid_postfix((root / std::string(300, 'n')).string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 63); +#else + const auto setReparseTag = [](const std::filesystem::path& path, DWORD tag) { + const HANDLE handle = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(handle != INVALID_HANDLE_VALUE); + std::array reparseData{}; + REPARSE_GUID_DATA_BUFFER reparseHeader{}; + reparseHeader.ReparseTag = tag; + reparseHeader.ReparseDataLength = 4; + reparseHeader.ReparseGuid = {0x6a1e3d0b, 0x1f6c, 0x4c1e, {0x9b, 0x52, 0x3d, 0x8e, 0x41, 0x07, 0x5c, 0x2a}}; + std::memcpy(reparseData.data(), &reparseHeader, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE); + DWORD returned = 0; + const bool set = DeviceIoControl(handle, FSCTL_SET_REPARSE_POINT, reparseData.data(), static_cast(reparseData.size()), + nullptr, 0, &returned, nullptr) != 0; + if (!set) Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD || GetLastError() == ERROR_ACCESS_DENIED); + Require(CloseHandle(handle) != 0); + if (!set) { + Require(std::filesystem::remove(path)); + std::fprintf(stderr, "Windows reparse point checks skipped: setting a reparse point on a file needs rights this user lacks\n"); + return false; + } + Require((GetFileAttributesW(path.c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + return true; + }; + const auto clearReparseTag = [](const std::filesystem::path& path, DWORD tag) { + const HANDLE handle = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(handle != INVALID_HANDLE_VALUE); + REPARSE_GUID_DATA_BUFFER reparseHeader{}; + reparseHeader.ReparseTag = tag; + reparseHeader.ReparseGuid = {0x6a1e3d0b, 0x1f6c, 0x4c1e, {0x9b, 0x52, 0x3d, 0x8e, 0x41, 0x07, 0x5c, 0x2a}}; + DWORD returned = 0; + Require(DeviceIoControl(handle, FSCTL_DELETE_REPARSE_POINT, &reparseHeader, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE, + nullptr, 0, &returned, nullptr) != 0); + Require(CloseHandle(handle) != 0 && std::filesystem::remove(path)); + }; + const auto tagged = root / "tagged.bin"; + std::ofstream(tagged, std::ios::binary) << "abc"; + if (setReparseTag(tagged, 0x00000ABC)) { + Require(readlink_nid_postfix(tagged.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); + clearReparseTag(tagged, 0x00000ABC); + } + const auto surrogate = root / "surrogate.bin"; + std::ofstream(surrogate, std::ios::binary) << "abc"; + if (setReparseTag(surrogate, 0x20000ABC)) { + Require(readlink_nid_postfix(surrogate.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); + FileStat surrogateStatus{}; + Require(lstat_nid_postfix(surrogate.string().c_str(), &surrogateStatus) == 0); + Require(surrogateStatus.st_mode == 0100666 && surrogateStatus.st_nlink == 1); + clearReparseTag(surrogate, 0x20000ABC); + } #endif std::error_code error; const auto link = (root / "link").string(); @@ -204,11 +442,24 @@ int main() { Require(fileDescriptor >= 0); Require(fchdir_nid_postfix(-1) == -1 && Error() == 9); Require(fchdir_nid_postfix(fileDescriptor) == -1 && Error() == 20); + int pipeEnds[2] = {-1, -1}; + Require(pipe_nid_postfix(pipeEnds) == 0); + Require(fchdir_nid_postfix(pipeEnds[0]) == -1 && Error() == 22); + Require(fchdir_nid_postfix(pipeEnds[1]) == -1 && Error() == 22); + Require(close_nid_postfix(pipeEnds[0]) == 0 && close_nid_postfix(pipeEnds[1]) == 0); + const int characterDevice = open_nid_postfix("/dev/random", 0, 0); + Require(characterDevice >= 0); + Require(fchdir_nid_postfix(characterDevice) == -1 && Error() == 20); + Require(close_nid_postfix(characterDevice) == 0); + const int socketDescriptor = socket_nid_postfix(2, 2, 0); + Require(socketDescriptor >= 0); + Require(fchdir_nid_postfix(socketDescriptor) == -1 && Error() == 22); + Require(close_nid_postfix(socketDescriptor) == 0); Require(fchdir_nid_postfix(directory) == 0); char directoryPath[1024]; Require(getcwd_nid_postfix(directoryPath, sizeof(directoryPath)) == directoryPath); Require(std::strcmp(directoryPath, ("/" + root.filename().string()).c_str()) == 0); -#ifdef __linux__ +#ifndef _WIN32 Require(chdir_nid_postfix("/") == 0); const auto originalPermissions = std::filesystem::status(root).permissions(); std::filesystem::permissions(root, std::filesystem::perms::owner_read | std::filesystem::perms::owner_write); @@ -235,6 +486,12 @@ int main() { Require(chroot_nid_postfix((root / "file.txt" / "..").string().c_str()) == -1 && Error() == 20); Require(chroot_nid_postfix((root / std::string(256, 'n')).string().c_str()) == -1 && Error() == 63); Require(chroot_nid_postfix(("/" + std::string(1024, 'p')).c_str()) == -1 && Error() == 63); + const std::string throughFile = (root / "file.txt" / "child").string(); + Require(pathconf_nid_postfix(throughFile.c_str(), 4) == -1 && Error() == 20); + Require(fchmodat_nid_postfix(-100, throughFile.c_str(), 0600, 0) == -1 && Error() == 20); + Require(symlink_nid_postfix("target", throughFile.c_str()) == -1 && Error() == 20); + Require(link_nid_postfix(file.c_str(), throughFile.c_str()) == -1 && Error() == 20); + Require(link_nid_postfix(throughFile.c_str(), (root / "linked").string().c_str()) == -1 && Error() == 20); Require(ChrootThrows(root.string())); Require(ChrootThrows("/" + root.filename().string() + "/.")); const auto guestRoot = "/" + root.filename().string(); @@ -289,13 +546,18 @@ int main() { const auto absolute = std::filesystem::absolute(root / "absolute"); Require(CreateSymbolicLinkW(absolute.wstring().c_str(), std::filesystem::absolute(original).wstring().c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); - bool refused = false; - try { - readlink_nid_postfix((root / "absolute").string().c_str(), buffer, sizeof(buffer)); - } catch (const std::runtime_error& failure) { - refused = std::string(failure.what()) == "readlink of a Windows reparse point other than a relative symbolic link not implemented"; - } - Require(refused); + char absoluteBuffer[512]; + std::memset(absoluteBuffer, '#', sizeof(absoluteBuffer)); + const auto absoluteLength = readlink_nid_postfix((root / "absolute").string().c_str(), absoluteBuffer, sizeof(absoluteBuffer)); + const bool absoluteFits = absoluteLength > 0 && static_cast(absoluteLength) < sizeof(absoluteBuffer) && + absoluteBuffer[absoluteLength] == '#'; + const std::string absoluteTarget = absoluteFits ? std::string(absoluteBuffer, static_cast(absoluteLength)) : std::string(); + std::error_code absoluteError; + const bool absoluteMatches = absoluteFits && absoluteTarget.find('?') == std::string::npos && + std::filesystem::equivalent(absoluteTarget, original, absoluteError) && !absoluteError; + if (!absoluteMatches) + std::fprintf(stderr, "absolute link returned %lld bytes\n", static_cast(absoluteLength)); + Require(absoluteMatches); #else Require(std::filesystem::is_symlink(std::filesystem::symlink_status(symbolic))); Require(readlink_nid_postfix(symbolic.c_str(), buffer, sizeof(buffer)) == 12 && std::memcmp(buffer, "original.bin", 12) == 0); @@ -312,6 +574,7 @@ int main() { Require(symlink_nid_postfix("original.bin", (root / "missing" / "symbolic").string().c_str()) == -1 && Error() == 2); Require(symlink_nid_postfix(nullptr, symbolic.c_str()) == -1 && Error() == 14); Require(symlink_nid_postfix("original.bin", "") == -1 && Error() == 2); + CheckSymlinkTargetRestrictions(root); const int base = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); Require(base >= 0); const int relative = openat_nid_postfix(base, "file.txt", SCE_KERNEL_O_RDONLY); @@ -323,6 +586,12 @@ int main() { Require(sceKernelClose(made) == 0); Require(openat_nid_postfix(base, "made.bin", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL, 0644) == -1 && Error() == 17); Require(openat_nid_postfix(base, "missing.bin", SCE_KERNEL_O_RDONLY) == -1 && Error() == 2); + Require(openat_nid_postfix(base, "file.txt", SCE_KERNEL_O_ACCMODE) == -1 && Error() == 22); + Require(openat_nid_postfix(base, "absent.bin", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL | SCE_KERNEL_O_DIRECTORY, 0644) == -1 && Error() == 22); + Require(openat_nid_postfix(base, nullptr, SCE_KERNEL_O_ACCMODE) == -1 && Error() == 22); + Require(openat_nid_postfix(base, "", SCE_KERNEL_O_ACCMODE) == -1 && Error() == 22); + Require(openat_nid_postfix(9999, "", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL | SCE_KERNEL_O_DIRECTORY, 0644) == -1 && Error() == 22); + Require(openat_nid_postfix(-100, nullptr, SCE_KERNEL_O_ACCMODE) == -1 && Error() == 22); const auto guestFile = "/" + root.filename().string() + "/file.txt"; const int absolute = openat_nid_postfix(-1, guestFile.c_str(), SCE_KERNEL_O_RDONLY); Require(absolute >= 0 && sceKernelClose(absolute) == 0); @@ -333,6 +602,7 @@ int main() { Require(openat_nid_postfix(plain, "file.txt", SCE_KERNEL_O_RDONLY) == -1 && Error() == 20); Require(readlinkat_nid_postfix(base, "file.txt", buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlinkat_nid_postfix(base, "missing.bin", buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(readlinkat_nid_postfix(base, "file.txt/child", buffer, sizeof(buffer)) == -1 && Error() == 20); Require(readlinkat_nid_postfix(plain, "file.txt", buffer, sizeof(buffer)) == -1 && Error() == 20); Require(readlinkat_nid_postfix(-1, "file.txt", buffer, sizeof(buffer)) == -1 && Error() == 9); Require(readlinkat_nid_postfix(base, nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); @@ -373,7 +643,8 @@ int main() { Require(unlinkat_nid_postfix(base, "file.txt", 0x800) == -1 && Error() == 20); Require(unlinkat_nid_postfix(base, ".", 0x800) == -1 && Error() == 22); Require(unlinkat_nid_postfix(base, "..", 0x800) == -1 && Error() == 22); - Require(unlinkat_nid_postfix(base, ".", 0) == -1 && Error() == 1); + Require(unlinkat_nid_postfix(base, ".", 0) == -1 && Error() == 22); + Require(unlinkat_nid_postfix(base, "..", 0) == -1 && Error() == 22); std::filesystem::create_directory(root / "usub"); Require(unlinkat_nid_postfix(base, "usub", 0) == -1 && Error() == 1); std::filesystem::create_directory(root / "nonempty"); @@ -464,21 +735,107 @@ int main() { Require(fchmodat_nid_postfix(base, "loop/x", 0600, 0) == -1 && Error() == 62); Require(fchmodat_nid_postfix(base, longName.c_str(), 0600, 0) == -1 && Error() == 63); #endif - Require(sceKernelClose(base) == 0); + const auto modeFile = (root / "file.txt").string(); #ifndef _WIN32 + const auto ownerOnly = std::filesystem::perms::owner_read | std::filesystem::perms::owner_write; + const auto readable = ownerOnly | std::filesystem::perms::group_read | std::filesystem::perms::others_read; +#endif + Require(lchmod_nid_postfix(modeFile.c_str(), 0600) == 0); +#ifndef _WIN32 + Require((std::filesystem::status(modeFile).permissions() & std::filesystem::perms::all) == ownerOnly); +#endif + Require(lchmod_nid_postfix(guestFile.c_str(), 0100644) == 0); +#ifndef _WIN32 + Require((std::filesystem::status(modeFile).permissions() & std::filesystem::perms::all) == readable); +#endif + Require(lchmod_nid_postfix((root / "missing.bin").string().c_str(), 0600) == -1 && Error() == 2); + Require(lchmod_nid_postfix(nullptr, 0600) == -1 && Error() == 14); + Require(lchmod_nid_postfix("", 0600) == -1 && Error() == 2); +#ifndef _WIN32 + Require(lchmod_nid_postfix((root / "loop" / "x").string().c_str(), 0600) == -1 && Error() == 62); + Require(lchmod_nid_postfix((root / longName).string().c_str(), 0600) == -1 && Error() == 63); + const auto modeLink = (root / "mode-link.txt").string(); + Require(symlink_nid_postfix("file.txt", modeLink.c_str()) == 0); +#ifdef __APPLE__ + Require(lchmod_nid_postfix(modeLink.c_str(), 0600) == 0); + Require((std::filesystem::symlink_status(modeLink).permissions() & std::filesystem::perms::all) == ownerOnly); +#else + bool lchmodThrew = false; + try { lchmod_nid_postfix(modeLink.c_str(), 0600); } catch (const std::exception&) { lchmodThrew = true; } + Require(lchmodThrew); +#endif + Require((std::filesystem::status(modeFile).permissions() & std::filesystem::perms::all) == readable); + Require(RemoveSymlink(modeLink)); +#endif + Require(fchown_nid_postfix(base, 0, 0) == 0); + Require(fchown_nid_postfix(base, 0xFFFFFFFFu, 0xFFFFFFFFu) == 0); + Require(fchown_nid_postfix(-1, 0, 0) == -1 && Error() == 9); + bool fchownThrew = false; + try { fchown_nid_postfix(base, 1, 0); } catch (const std::exception&) { fchownThrew = true; } + Require(fchownThrew); + fchownThrew = false; + try { fchown_nid_postfix(base, 0, 1); } catch (const std::exception&) { fchownThrew = true; } + Require(fchownThrew); + FileStat stat{}; + Require(fstatat_nid_postfix(base, "file.txt", &stat, 0) == 0 && (stat.st_mode & 0170000) == 0100000 && stat.st_size == 1); + Require(fstatat_nid_postfix(base, "other_dir", &stat, 0) == 0 && (stat.st_mode & 0170000) == 0040000); + Require(fstatat_nid_postfix(base, "other_dir/", &stat, 0) == 0 && (stat.st_mode & 0170000) == 0040000); + Require(fstatat_nid_postfix(base, "file.txt/", &stat, 0) == -1 && Error() == 20); + Require(fstatat_nid_postfix(base, "file.txt", &stat, 0x0200) == 0 && (stat.st_mode & 0170000) == 0100000); + Require(fstatat_nid_postfix(-100, file.c_str(), &stat, 0) == 0 && stat.st_size == 1); + Require(fstatat_nid_postfix(-1, guestFile.c_str(), &stat, 0) == 0 && stat.st_size == 1); + Require(fstatat_nid_postfix(base, "missing.bin", &stat, 0) == -1 && Error() == 2); + Require(fstatat_nid_postfix(base, "missing.bin", &stat, 0x0200) == -1 && Error() == 2); + Require(fstatat_nid_postfix(base, nullptr, &stat, 0) == -1 && Error() == 14); + Require(fstatat_nid_postfix(base, "", &stat, 0) == -1 && Error() == 2); + Require(fstatat_nid_postfix(base, "file.txt", &stat, 0x4000) == -1 && Error() == 22); + Require(fstatat_nid_postfix(-1, "file.txt", &stat, 0) == -1 && Error() == 9); + const int notDirectory = sceKernelOpen(file.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(notDirectory >= 0); + Require(fstatat_nid_postfix(notDirectory, "file.txt", &stat, 0) == -1 && Error() == 20); + Require(sceKernelClose(notDirectory) == 0); +#ifndef _WIN32 + Require(symlink_nid_postfix("file.txt", (root / "link.txt").string().c_str()) == 0); + Require(fstatat_nid_postfix(base, "link.txt", &stat, 0) == 0 && (stat.st_mode & 0170000) == 0100000 && stat.st_size == 1); + Require(fstatat_nid_postfix(base, "link.txt", &stat, 0x0200) == 0 && (stat.st_mode & 0170000) == 0120000); +#endif + Require(sceKernelClose(base) == 0); +#ifdef _WIN32 + const auto junctionTarget = root / "junction-target"; + const auto junction = root / "junction"; + Require(std::filesystem::create_directory(junctionTarget)); + const auto junctionCommand = L"cmd /c mklink /J \"" + junction.wstring() + L"\" \"" + std::filesystem::absolute(junctionTarget).wstring() + L"\""; + Require(_wsystem(junctionCommand.c_str()) == 0); + char junctionBuffer[512]; + std::memset(junctionBuffer, '#', sizeof(junctionBuffer)); + const auto junctionLength = readlink_nid_postfix(junction.string().c_str(), junctionBuffer, sizeof(junctionBuffer)); + Require(junctionLength > 0 && static_cast(junctionLength) < sizeof(junctionBuffer) && junctionBuffer[junctionLength] == '#'); + const std::string junctionText(junctionBuffer, static_cast(junctionLength)); + std::error_code junctionError; + Require(junctionText.find('?') == std::string::npos && std::filesystem::equivalent(junctionText, junctionTarget, junctionError) && !junctionError); + Require(std::filesystem::remove(junction) && std::filesystem::remove(junctionTarget)); +#endif if (created == 0) { const auto throughLink = (root / "through-link.bin").string(); Require(link_nid_postfix(symbolic.c_str(), throughLink.c_str()) == 0); +#ifdef _WIN32 + Require((GetFileAttributesW(std::filesystem::path(throughLink).c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) == 0); +#else Require(!std::filesystem::is_symlink(std::filesystem::symlink_status(throughLink))); +#endif Require(std::filesystem::equivalent(throughLink, original) && std::filesystem::hard_link_count(original) == 2); Require(std::filesystem::remove(throughLink) && std::filesystem::hard_link_count(original) == 1); Require(link_nid_postfix((root / "dangling").string().c_str(), throughLink.c_str()) == -1 && Error() == 2); - const auto directoryLink = (root / "directory-link").string(); - Require(symlink_nid_postfix(".", directoryLink.c_str()) == 0); - Require(link_nid_postfix(directoryLink.c_str(), throughLink.c_str()) == -1 && Error() == 1); + const auto dotLink = (root / "dot-link").string(); + Require(symlink_nid_postfix(".", dotLink.c_str()) == 0); + Require(link_nid_postfix(dotLink.c_str(), throughLink.c_str()) == -1 && Error() == 1); +#ifdef _WIN32 + Require(symlink_nid_postfix("loop-b", (root / "loop-a").string().c_str()) == 0); + Require(symlink_nid_postfix("loop-a", (root / "loop-b").string().c_str()) == 0); + Require(link_nid_postfix((root / "loop-a").string().c_str(), throughLink.c_str()) == -1 && Error() == 62); +#endif Require(!std::filesystem::exists(std::filesystem::symlink_status(throughLink))); } -#endif - std::filesystem::remove_all(root); + RemoveTree(root); return 0; } diff --git a/core/libs/tests/GuestKernelSysSdkVersion.cpp b/core/libs/tests/GuestKernelSysSdkVersion.cpp new file mode 100644 index 0000000000..5579fe7e5e --- /dev/null +++ b/core/libs/tests/GuestKernelSysSdkVersion.cpp @@ -0,0 +1,45 @@ +#include "ProcessParametersFixture.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceKernelGetCompiledSdkVersion(std::int32_t* version); + +namespace { + +alignas(16) std::array process{}; + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "KERNEL SYS SDK VERSION: %s\n", message); + std::abort(); + } +} + +} + +int main() { + const std::uint64_t size = process.size(); + const std::uint32_t magic = 0x4942524f; + const std::uint32_t ps4SdkVersion = 0x07008001; + const std::uint32_t ps5SdkVersion = 0x0a008001; + std::memcpy(process.data(), &size, sizeof(size)); + std::memcpy(process.data() + 8, &magic, sizeof(magic)); + std::memcpy(process.data() + 0x10, &ps4SdkVersion, sizeof(ps4SdkVersion)); + std::memcpy(process.data() + 0x14, &ps5SdkVersion, sizeof(ps5SdkVersion)); + Require(ProcessParametersFixture::Install(process.data(), process.size()), "process parameters were not installed"); + + Require(sceKernelGetCompiledSdkVersion(nullptr) == static_cast(0x80020016), "null output is not EINVAL"); + std::array output{-1, 0x5a5a5a5a}; + Require(sceKernelGetCompiledSdkVersion(output.data()) == 0, "query failed"); + Require(output[0] == 0x07008001, "version is not the 32-bit field at offset 0x10 of the process parameters"); + Require(output[1] == 0x5a5a5a5a, "the query wrote past the version"); + + std::puts("KERNEL SYS SDK VERSION tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestKernelSysThreadDefaults.cpp b/core/libs/tests/GuestKernelSysThreadDefaults.cpp new file mode 100644 index 0000000000..18a48b8f15 --- /dev/null +++ b/core/libs/tests/GuestKernelSysThreadDefaults.cpp @@ -0,0 +1,84 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI pthread_set_defaultstacksize_np_nid_postfix(std::size_t size); +int APS5_VABI scePthreadSetDefaultstacksize(std::size_t size); +void APS5_VABI scePthreadSetName(Pthread thread, const 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); +Pthread APS5_VABI scePthreadSelf(void); +int APS5_VABI scePthreadGetname(Pthread thread, char* name); +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr); +int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, std::size_t* stackSize); +} + +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int POSIX_EINVAL = 22; +static constexpr std::size_t INITIAL_DEFAULT = 1u << 20; + +static void Require(bool value) { if (!value) std::abort(); } + +static std::size_t NewAttrStackSize() { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == 0); + std::size_t size = 0; + Require(scePthreadAttrGetstacksize(&attr, &size) == 0); + Require(scePthreadAttrDestroy(&attr) == 0); + return size; +} + +static void* APS5_VABI ReportStackSize(void* out) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == 0); + Require(scePthreadAttrGet(scePthreadSelf(), &attr) == 0); + Require(scePthreadAttrGetstacksize(&attr, static_cast(out)) == 0); + Require(scePthreadAttrDestroy(&attr) == 0); + return nullptr; +} + +static std::size_t DefaultThreadStackSize() { + std::size_t size = 0; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, ReportStackSize, &size, "stack") == 0); + Require(scePthreadJoin(thread, nullptr) == 0); + return size; +} + +static void CheckDefaultStackSize() { + Require(NewAttrStackSize() == INITIAL_DEFAULT); + Require(pthread_set_defaultstacksize_np_nid_postfix(0) == POSIX_EINVAL); + Require(pthread_set_defaultstacksize_np_nid_postfix(16384) == POSIX_EINVAL); + Require(scePthreadSetDefaultstacksize(16384) == SCE_KERNEL_ERROR_EINVAL); + Require(NewAttrStackSize() == INITIAL_DEFAULT); + + Require(pthread_set_defaultstacksize_np_nid_postfix(16385) == 0); + Require(NewAttrStackSize() == 16385); + + Require(scePthreadSetDefaultstacksize(0x40000) == 0); + Require(NewAttrStackSize() == 0x40000); + Require(DefaultThreadStackSize() == 0x40000); + + Require(scePthreadSetDefaultstacksize(INITIAL_DEFAULT) == 0); + Require(DefaultThreadStackSize() == INITIAL_DEFAULT); +} + +static void CheckSetName() { + char name[32] = {}; + scePthreadSetName(scePthreadSelf(), "renamed main"); + Require(scePthreadGetname(scePthreadSelf(), name) == 0); + Require(std::strcmp(name, "renamed main") == 0); + scePthreadSetName(scePthreadSelf(), nullptr); + Require(scePthreadGetname(scePthreadSelf(), name) == 0); + Require(name[0] == '\0'); +} + +int main() { + CheckDefaultStackSize(); + CheckSetName(); + return 0; +} diff --git a/core/libs/tests/GuestKill.cpp b/core/libs/tests/GuestKill.cpp index 7a1f380d3c..7b75694ea5 100644 --- a/core/libs/tests/GuestKill.cpp +++ b/core/libs/tests/GuestKill.cpp @@ -32,7 +32,7 @@ int main() { Fails(target, 0, 3); Fails(target, 15, 3); } - for (int target : {self + 1, 1000}) { + for (int target : {self + 1, self + 1000}) { KillpgFails(target, 0, 3); KillpgFails(target, 15, 3); } diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index ccbe1d87bf..62085d4470 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -1,11 +1,20 @@ #include "SceTypes.hpp" +#include +#include #include #include +#include +#include #include #include #include #include #include +#include +#ifdef _WIN32 +#include +#include +#endif extern "C" { int APS5_VABI kqueue_nid_postfix(); @@ -13,9 +22,11 @@ int APS5_VABI kevent_nid_postfix(int, const KernelEvent*, int, KernelEvent*, int int APS5_VABI close_nid_postfix(int); int APS5_VABI dup_nid_postfix(int); int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); int APS5_VABI socketpair_nid_postfix(int, int, int, int*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +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*); @@ -34,6 +45,11 @@ int* APS5_VABI __error_nid_postfix(); static constexpr int GuestEnoent = 2; static constexpr int GuestEbadf = 9; static constexpr int GuestEinval = 22; +static constexpr int GuestEpipe = 32; +static constexpr int GuestEagain = 35; +static constexpr int SetStatusFlags = 4; +static constexpr int NonBlocking = 0x0004; +static constexpr std::int64_t PipeAtomic = 512; static constexpr int Unix = 1; static constexpr int Stream = 1; static constexpr int FirstSocket = 0x10000000; @@ -50,11 +66,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; @@ -111,6 +132,15 @@ template static bool Throws(TCall call) { return false; } +template static std::string ThrownMessage(TCall call) { + try { + call(); + } catch (const std::runtime_error& error) { + return error.what(); + } + return {}; +} + static void CreateAndClose() { const int kq = kqueue_nid_postfix(); Require(kq >= 0 && kq < FirstSocket); @@ -146,6 +176,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); @@ -190,6 +226,102 @@ 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); + const KernelTimespec limit{5, 0}; + const auto start = std::chrono::steady_clock::now(); + std::thread writer([&pipe] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(write_nid_postfix(pipe[1], "d", 1) == 1); + }); + Require(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); +} + +#ifndef __APPLE__ +static void PipeWrite() { + const int kq = kqueue_nid_postfix(); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + const auto writeEnd = static_cast(pipe[1]); + int marker = 0; + KernelEvent events[4]{}; + Require(Apply(kq, Change(writeEnd, EvfiltWrite, EvAdd, 0, 0, &marker)) == 0); + Require(Poll(kq, events, 4) == 1); + Require(events[0].ident == writeEnd && events[0].filter == EvfiltWrite && events[0].udata == &marker && (events[0].flags & EvEof) == 0); + const std::int64_t empty = events[0].data; + Require(empty >= PipeAtomic); + Require(write_nid_postfix(pipe[1], "abc", 3) == 3); + Require(Poll(kq, events, 4) == 1 && events[0].data == empty - 3); +#ifndef _WIN32 + Require(fcntl_nid_postfix(pipe[1], SetStatusFlags, NonBlocking) == 0); + const std::string chunk(4096, 'x'); + std::int64_t queued = 3; + for (;;) { + const std::int64_t written = write_nid_postfix(pipe[1], chunk.data(), static_cast(chunk.size())); + if (written < 0) { + Require(*__error_nid_postfix() == GuestEagain); + break; + } + queued += written; + } + Require(queued > PipeAtomic); + Require(Poll(kq, events, 4) == 0); + const KernelTimespec limit{5, 0}; + const auto start = std::chrono::steady_clock::now(); + std::thread reader([&pipe, queued] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + std::string drained(static_cast(queued), '\0'); + std::int64_t done = 0; + while (done < queued) { + const std::int64_t got = read_nid_postfix(pipe[0], drained.data() + done, static_cast(queued - done)); + Require(got > 0); + done += got; + } + }); + Require(kevent_nid_postfix(kq, nullptr, 0, events, 4, &limit) == 1); + const auto waited = std::chrono::steady_clock::now() - start; + reader.join(); + Require(waited >= std::chrono::milliseconds(40) && waited < std::chrono::seconds(4)); + Require(events[0].ident == writeEnd && events[0].data >= PipeAtomic && (events[0].flags & EvEof) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == empty); +#else + char back[3]; + Require(read_nid_postfix(pipe[0], back, 3) == 3); + Require(Poll(kq, events, 4) == 1 && events[0].data == empty); +#endif + Require(close_nid_postfix(pipe[0]) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 0 && (events[0].flags & EvEof) != 0); + const int late = kqueue_nid_postfix(); + RequireFailure(Apply(late, Change(writeEnd, EvfiltWrite, EvAdd)), GuestEpipe); + Require(close_nid_postfix(late) == 0); + Require(close_nid_postfix(pipe[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} +#endif + static void SocketPair() { const int kq = kqueue_nid_postfix(); int pair[2]; @@ -208,13 +340,451 @@ static void SocketPair() { Require(Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvAdd | EvOneshot)) == 0); Require(Wait(kq, events, 4) == 1 && events[0].filter == EvfiltWrite); RequireFailure(Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvDelete)), GuestEnoent); +#ifndef __linux__ Require(Throws([kq, &pair] { Apply(kq, Change(static_cast(pair[1]), EvfiltRead, EvAdd | EvClear)); })); +#endif + const int space = kqueue_nid_postfix(); + Require(Apply(space, Change(static_cast(pair[1]), EvfiltWrite, EvAdd)) == 0); + Require(Poll(space, events, 4) == 1 && events[0].filter == EvfiltWrite); + 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 WatchKqueue() { + const int inner = kqueue_nid_postfix(); + const int outer = kqueue_nid_postfix(); + const auto watched = static_cast(inner); + int marker = 0; + KernelEvent events[4]{}; + Require(Apply(inner, Change(5, EvfiltUser, EvAdd | EvClear)) == 0); + Require(Apply(inner, Change(6, EvfiltUser, EvAdd | EvClear)) == 0); + Require(Apply(outer, Change(watched, EvfiltRead, EvAdd, 0, 0, &marker)) == 0); + Require(Poll(outer, events, 4) == 0); + Require(Poll(inner, events, 4) == 0); + + Require(Apply(inner, Change(5, EvfiltUser, 0, NoteTrigger)) == 0); + Require(Poll(outer, events, 4) == 1); + Require(events[0].ident == watched && events[0].filter == EvfiltRead && events[0].flags == 0 && events[0].fflags == 0); + Require(events[0].data == 1 && events[0].udata == &marker); + Require(Apply(inner, Change(6, EvfiltUser, 0, NoteTrigger)) == 0); + Require(Poll(outer, events, 4) == 1 && events[0].data == 2); + Require(Poll(outer, events, 4) == 1 && events[0].data == 2); + Require(Poll(inner, events, 1) == 1); + Require(Poll(outer, events, 4) == 1 && events[0].data == 1); + Require(Poll(inner, events, 4) == 1); + Require(Poll(outer, events, 4) == 0); + + Require(Apply(outer, Change(watched, EvfiltRead, EvDisable, 0, 0, &marker)) == 0); + Require(Apply(inner, Change(5, EvfiltUser, 0, NoteTrigger)) == 0); + Require(Poll(outer, events, 4) == 0); + Require(Apply(outer, Change(watched, EvfiltRead, EvEnable, 0, 0, &marker)) == 0); + Require(Poll(outer, events, 4) == 1 && events[0].data == 1); + Require(Poll(inner, events, 4) == 1); + + const KernelTimespec limit{5, 0}; + auto start = std::chrono::steady_clock::now(); + std::thread trigger([inner] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(Apply(inner, Change(5, EvfiltUser, 0, NoteTrigger)) == 0); + }); + Require(kevent_nid_postfix(outer, nullptr, 0, events, 4, &limit) == 1); + auto waited = std::chrono::steady_clock::now() - start; + trigger.join(); + Require(waited >= std::chrono::milliseconds(40) && waited < std::chrono::seconds(4)); + Require(events[0].ident == watched && events[0].data == 1 && events[0].udata == &marker); + Require(Poll(inner, events, 4) == 1); + + start = std::chrono::steady_clock::now(); + std::thread blocked([outer] { + KernelEvent event{}; + Require(kevent_nid_postfix(outer, nullptr, 0, &event, 1, nullptr) == 1 && event.data == 1); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(Apply(inner, Change(6, EvfiltUser, 0, NoteTrigger)) == 0); + blocked.join(); + waited = std::chrono::steady_clock::now() - start; + Require(waited < std::chrono::seconds(4)); + Require(Poll(inner, events, 4) == 1); + + Require(Apply(inner, Change(7, EvfiltTimer, EvAdd, 0, 60)) == 0); + start = std::chrono::steady_clock::now(); + Require(kevent_nid_postfix(outer, nullptr, 0, events, 4, &limit) == 1); + waited = std::chrono::steady_clock::now() - start; + Require(waited >= std::chrono::milliseconds(40) && waited < std::chrono::seconds(4)); + Require(events[0].ident == watched && events[0].data == 1); + Require(Poll(inner, events, 4) == 1 && events[0].ident == 7); + Require(Apply(inner, Change(7, EvfiltTimer, EvDelete)) == 0); + Require(Poll(outer, events, 4) == 0); + + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + Require(Apply(inner, Change(static_cast(pair[0]), EvfiltRead, EvAdd)) == 0); + Require(Poll(outer, events, 4) == 0); + std::thread sender([&pair] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(send_nid_postfix(pair[1], "abc", 3, 0) == 3); + }); + Require(kevent_nid_postfix(outer, nullptr, 0, events, 4, &limit) == 1); + sender.join(); + Require(events[0].ident == watched && events[0].data == 1); + char buffer[4]{}; + Require(recv_nid_postfix(pair[0], buffer, sizeof(buffer), 0) == 3); + Require(Poll(outer, events, 4) == 0); + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + + const int top = kqueue_nid_postfix(); + const auto middle = static_cast(outer); + Require(Apply(top, Change(middle, EvfiltRead, EvAdd)) == 0); + Require(Poll(top, events, 4) == 0); + std::thread deep([inner] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(Apply(inner, Change(5, EvfiltUser, 0, NoteTrigger)) == 0); + }); + Require(kevent_nid_postfix(top, nullptr, 0, events, 4, &limit) == 1); + deep.join(); + Require(events[0].ident == middle && events[0].data == 1); + Require(Poll(outer, events, 4) == 1 && events[0].ident == watched); + Require(Poll(inner, events, 4) == 1); + Require(Poll(top, events, 4) == 0); + Require(Apply(top, Change(middle, EvfiltRead, EvDelete)) == 0); + Require(close_nid_postfix(top) == 0); + + Require(Apply(inner, Change(5, EvfiltUser, 0, NoteTrigger)) == 0); + Require(Apply(outer, Change(watched, EvfiltRead, EvDelete)) == 0); + RequireFailure(Apply(outer, Change(watched, EvfiltRead, EvDelete)), GuestEnoent); + Require(Poll(outer, events, 4) == 0); + Require(Apply(outer, Change(watched, EvfiltRead, EvAdd | EvDispatch, 0, 0, &marker)) == 0); + Require(Poll(outer, events, 4) == 1 && events[0].data == 1 && events[0].flags == EvDispatch); + Require(Poll(outer, events, 4) == 0); + Require(Apply(outer, Change(watched, EvfiltRead, EvEnable, 0, 0, &marker)) == 0); + Require(Poll(outer, events, 4) == 1); + Require(Apply(outer, Change(watched, EvfiltRead, EvDelete)) == 0); + Require(Apply(outer, Change(watched, EvfiltRead, EvAdd | EvOneshot)) == 0); + Require(Poll(outer, events, 4) == 1 && events[0].data == 1 && events[0].flags == EvOneshot); + Require(Poll(outer, events, 4) == 0); + RequireFailure(Apply(outer, Change(watched, EvfiltRead, EvDelete)), GuestEnoent); + Require(Poll(inner, events, 4) == 1); + + RequireFailure(Apply(outer, Change(watched, EvfiltWrite, EvAdd)), GuestEinval); + RequireFailure(Apply(outer, Change(watched, EvfiltVnode, EvAdd, NoteWrite)), GuestEinval); + Require(ThrownMessage([outer, watched] { Apply(outer, Change(watched, EvfiltRead, EvAdd | EvClear)); }).find("EV_CLEAR on a kqueue descriptor") != std::string::npos); + RequireFailure(Apply(outer, Change(watched, EvfiltRead, EvDelete)), GuestEnoent); + Require(Apply(outer, Change(watched, EvfiltRead, EvAdd)) == 0); + Require(ThrownMessage([inner, outer] { Apply(inner, Change(static_cast(outer), EvfiltRead, EvAdd)); }).find("watches the kqueue") != std::string::npos); + Require(Apply(inner, Change(5, EvfiltUser, 0, NoteTrigger)) == 0); + Require(Poll(outer, events, 4) == 1 && events[0].data == 1); + + Require(close_nid_postfix(inner) == 0); + Require(Poll(outer, events, 4) == 0); + RequireFailure(Apply(outer, Change(watched, EvfiltRead, EvDelete)), GuestEbadf); + Require(close_nid_postfix(outer) == 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); + const auto writer = static_cast(pair[1]); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, 2)) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].ident == writer && events[0].filter == EvfiltWrite && events[0].data >= 2); + const auto freeSpace = events[0].data; + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, freeSpace)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].ident == writer && events[0].data == freeSpace); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, freeSpace + 1)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Throws([kq, reader] { Apply(kq, Change(reader, EvfiltRead, EvAdd, NoteLowat, 0)); })); + int datagrams[2]; + Require(socketpair_nid_postfix(Unix, 2, 0, datagrams) == 0); + 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); +} + +#ifdef __linux__ +static void EdgeTriggeredSocketRead() { + const int kq = kqueue_nid_postfix(); + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + const auto ident = static_cast(pair[0]); + int marker = 0; + KernelEvent events[4]{}; + char buffer[8]{}; + + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvClear, 0, 0, &marker)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(send_nid_postfix(pair[1], "abc", 3, 0) == 3); + Require(Poll(kq, events, 4) == 1); + Require(events[0].ident == ident && events[0].filter == EvfiltRead && events[0].data == 3 && events[0].udata == &marker); + Require((events[0].flags & EvClear) != 0 && (events[0].flags & EvEof) == 0); + Require(Poll(kq, events, 4) == 0); + + const KernelTimespec quiet{0, 150000000}; + const auto idleStart = std::chrono::steady_clock::now(); + const std::clock_t idleCpu = std::clock(); + Require(kevent_nid_postfix(kq, nullptr, 0, events, 4, &quiet) == 0); + Require(std::clock() - idleCpu < CLOCKS_PER_SEC / 20); + Require(std::chrono::steady_clock::now() - idleStart >= std::chrono::milliseconds(140)); + + Require(send_nid_postfix(pair[1], "d", 1, 0) == 1); + Require(Poll(kq, events, 4) == 1 && events[0].data == 4); + Require(Poll(kq, events, 4) == 0); + + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvClear, 0, 0, &marker)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 4); + Require(Poll(kq, events, 4) == 0); + + Require(recv_nid_postfix(pair[0], buffer, 2, 0) == 2); + Require(Poll(kq, events, 4) == 0); + Require(recv_nid_postfix(pair[0], buffer, sizeof(buffer), 0) == 2); + Require(send_nid_postfix(pair[1], "e", 1, 0) == 1); + Require(recv_nid_postfix(pair[0], buffer, sizeof(buffer), 0) == 1); + Require(Poll(kq, events, 4) == 0); + + const KernelTimespec limit{5, 0}; + const auto start = std::chrono::steady_clock::now(); + std::thread sender([&pair] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(send_nid_postfix(pair[1], "fg", 2, 0) == 2); + }); + Require(kevent_nid_postfix(kq, nullptr, 0, events, 4, &limit) == 1); + const auto waited = std::chrono::steady_clock::now() - start; + sender.join(); + Require(waited >= std::chrono::milliseconds(40) && waited < std::chrono::seconds(4)); + Require(events[0].ident == ident && events[0].data == 2); + + Require(close_nid_postfix(pair[1]) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 2 && (events[0].flags & EvEof) != 0); + Require(Poll(kq, events, 4) == 0); + Require(close_nid_postfix(pair[0]) == 0); + + int marked[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, marked) == 0); + const auto markedReader = static_cast(marked[0]); + Require(Apply(kq, Change(markedReader, EvfiltRead, EvAdd | EvClear, NoteLowat, 4)) == 0); + Require(send_nid_postfix(marked[1], "ab", 2, 0) == 2); + Require(Poll(kq, events, 4) == 0); + Require(send_nid_postfix(marked[1], "cd", 2, 0) == 2); + Require(Poll(kq, events, 4) == 1 && events[0].ident == markedReader && events[0].data == 4); + Require(Poll(kq, events, 4) == 0); + Require(close_nid_postfix(marked[0]) == 0 && close_nid_postfix(marked[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} + +static void EdgeTriggeredSocketWrite() { + const int kq = kqueue_nid_postfix(); + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + const auto ident = static_cast(pair[1]); + KernelEvent events[4]{}; + Require(Apply(kq, Change(ident, EvfiltWrite, EvAdd | EvClear)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].ident == ident && events[0].filter == EvfiltWrite && events[0].data > 0); + Require(Poll(kq, events, 4) == 0); + const std::string payload(1000, 'w'); + Require(send_nid_postfix(pair[1], payload.data(), payload.size(), 0) == static_cast(payload.size())); + Require(Poll(kq, events, 4) == 0); + std::string drained(payload.size(), '\0'); + Require(recv_nid_postfix(pair[0], drained.data(), drained.size(), 0) == static_cast(drained.size())); + Require(Poll(kq, events, 4) == 1 && events[0].ident == ident && events[0].filter == EvfiltWrite && events[0].data > 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltWrite, EvAdd | EvClear)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].filter == EvfiltWrite); + Require(close_nid_postfix(pair[0]) == 0); + Require(Poll(kq, events, 4) == 1 && (events[0].flags & EvEof) != 0); + Require(close_nid_postfix(pair[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} + +static void SocketWriteLowWaterWhileFilling() { + const int kq = kqueue_nid_postfix(); + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + const auto writer = static_cast(pair[1]); + KernelEvent events[4]{}; + Require(fcntl_nid_postfix(pair[1], SetStatusFlags, NonBlocking) == 0); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, 2)) == 0); + Require(Poll(kq, events, 4) == 1); + const auto empty = events[0].data; + const std::string chunk(4096, 'x'); + std::int64_t queued = 0; + Require(send_nid_postfix(pair[1], chunk.data(), chunk.size(), 0) == static_cast(chunk.size())); + queued += static_cast(chunk.size()); + Require(Poll(kq, events, 4) == 1 && events[0].data > 0 && events[0].data < empty); + const auto partial = events[0].data; + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, partial)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == partial); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, partial + 1)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, empty)) == 0); + Require(Poll(kq, events, 4) == 0); + for (;;) { + const std::int64_t written = send_nid_postfix(pair[1], chunk.data(), chunk.size(), 0); + if (written < 0) { + Require(*__error_nid_postfix() == GuestEagain); + break; + } + queued += written; + } + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, 2)) == 0); + Require(Poll(kq, events, 4) == 0); + std::string drained(chunk.size(), '\0'); + while (queued > 0) { + const std::int64_t received = recv_nid_postfix(pair[0], drained.data(), drained.size(), 0); + Require(received > 0); + queued -= received; + } + Require(Poll(kq, events, 4) == 1 && events[0].data == empty); + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} + +static void EdgeTriggeredPipeRead() { + const int kq = kqueue_nid_postfix(); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + const auto ident = static_cast(pipe[0]); + KernelEvent events[4]{}; + char buffer[8]{}; + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvClear)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(write_nid_postfix(pipe[1], "ab", 2) == 2); + Require(Poll(kq, events, 4) == 1 && events[0].ident == ident && events[0].data == 2 && (events[0].flags & EvEof) == 0); + Require(Poll(kq, events, 4) == 0); + Require(write_nid_postfix(pipe[1], "c", 1) == 1); + Require(Poll(kq, events, 4) == 1 && events[0].data == 3); + Require(Poll(kq, events, 4) == 0); + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 3); + Require(close_nid_postfix(pipe[1]) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 0 && (events[0].flags & EvEof) != 0); + Require(Poll(kq, events, 4) == 0); + Require(close_nid_postfix(pipe[0]) == 0); + Require(close_nid_postfix(kq) == 0); +} + +static void EdgeTriggeredDispatch() { + const int kq = kqueue_nid_postfix(); + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + const auto ident = static_cast(pair[0]); + KernelEvent events[4]{}; + char buffer[8]{}; + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvClear | EvDispatch)) == 0); + Require(send_nid_postfix(pair[1], "ab", 2, 0) == 2); + Require(Poll(kq, events, 4) == 1 && events[0].data == 2); + Require(send_nid_postfix(pair[1], "c", 1, 0) == 1); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltRead, EvEnable)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 3); + Require(Poll(kq, events, 4) == 0); + Require(recv_nid_postfix(pair[0], buffer, sizeof(buffer), 0) == 3); + Require(Apply(kq, Change(ident, EvfiltRead, EvEnable)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltRead, EvDisable)) == 0); + Require(send_nid_postfix(pair[1], "d", 1, 0) == 1); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltRead, EvEnable)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 1); + Require(Apply(kq, Change(ident, EvfiltRead, EvDelete)) == 0); + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} +#endif + +static void DisableAndDispatch() { + const int kq = kqueue_nid_postfix(); + int pair[2]; + 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]{}; @@ -233,10 +803,19 @@ 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[0]), EvfiltWrite, EvAdd)); })); +#ifdef __APPLE__ + Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[1]), EvfiltWrite, EvAdd)); })); +#endif +#ifndef __linux__ + Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[0]), EvfiltRead, EvAdd | EvClear)); })); +#endif Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); const KernelEvent skipped = Change(1, 0, EvAdd); const KernelTimespec zero{0, 0}; @@ -330,7 +909,9 @@ static void RenameAndDeleteWatchedFile() { Require(close_nid_postfix(file) == 0); Require(close_nid_postfix(kq) == 0); } +#endif +#if !defined(_WIN32) && !defined(__APPLE__) static std::uint32_t NotesThrough(int kq, std::uintptr_t ident, std::uint32_t last) { std::uint32_t notes = 0; KernelEvent events[4]{}; @@ -418,6 +999,108 @@ static void DeleteWatchedFile() { Require(!std::filesystem::exists(path)); } +#ifdef _WIN32 +static std::string Utf8(const std::wstring& text) { + const int size = ::WideCharToMultiByte(CP_UTF8, 0, text.c_str(), -1, nullptr, 0, nullptr, nullptr); + Require(size > 0); + std::string narrow(static_cast(size), '\0'); + Require(::WideCharToMultiByte(CP_UTF8, 0, text.c_str(), -1, narrow.data(), size, nullptr, nullptr) == size); + narrow.pop_back(); + return narrow; +} + +static PACL DenyListing(const std::wstring& directory, PSECURITY_DESCRIPTOR& owner) { + 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); + _EXPLICIT_ACCESS_W denied{}; + denied.grfAccessPermissions = FILE_LIST_DIRECTORY; + denied.grfAccessMode = DENY_ACCESS; + denied.grfInheritance = NO_INHERITANCE; + denied.Trustee.TrusteeForm = TRUSTEE_IS_SID; + denied.Trustee.TrusteeType = TRUSTEE_IS_USER; + 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, + DACL_SECURITY_INFORMATION, nullptr, nullptr, blocked, nullptr); + ::LocalFree(blocked); + Require(applied == ERROR_SUCCESS); + return original; +} + +static void RequireContext(const std::string& message, const std::string& context) { + if (message.find(context) == std::string::npos) { + std::fprintf(stderr, "the vnode watch error lacks \"%s\": %s\n", context.c_str(), message.c_str()); + std::abort(); + } +} + +static void RequireContextInsensitive(std::string message, std::string context) { + const auto lower = [](char character) { return static_cast(std::tolower(static_cast(character))); }; + std::transform(message.begin(), message.end(), message.begin(), lower); + std::transform(context.begin(), context.end(), context.begin(), lower); + if (message.find(context) == std::string::npos) { + std::fprintf(stderr, "the vnode watch error lacks \"%s\"\n", context.c_str()); + std::abort(); + } +} + +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); + Require(file >= 0); + PSECURITY_DESCRIPTOR owner = nullptr; + const auto directory = std::filesystem::absolute(path).wstring(); + const PACL original = DenyListing(directory, owner); + const int kq = kqueue_nid_postfix(); + KernelEvent events[4]{}; + const auto ident = static_cast(file); + const std::string message = ThrownMessage([kq, ident] { + Apply(kq, Change(ident, EvfiltVnode, EvAdd, NoteWrite)); + }); + const DWORD restored = ::SetNamedSecurityInfoW(const_cast(directory.c_str()), SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, nullptr, nullptr, original, nullptr); + Require(restored == ERROR_SUCCESS); + ::LocalFree(owner); + RequireContext(message, "parent"); + RequireContext(message, "error=5"); + RequireContextInsensitive(message, Utf8(directory)); + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvClear, NoteWrite)) == 0); + Require(write_nid_postfix(file, "abc", 3) == 3); + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident); + Require((events[0].fflags & NoteWrite) != 0); + Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(kq) == 0); + Require(std::filesystem::remove_all(path) == 2); +} +#endif static void Timeout() { const int kq = kqueue_nid_postfix(); KernelEvent event{}; @@ -429,19 +1112,60 @@ static void Timeout() { Require(close_nid_postfix(kq) == 0); } +static void StandardInput() { + const int copy = dup_nid_postfix(0); + if (copy < 0) { + Require(*__error_nid_postfix() == GuestEbadf); + return; + } + Require(close_nid_postfix(copy) == 0); + const int kq = kqueue_nid_postfix(); + int marker = 0; + KernelEvent events[4]{}; + Require(Apply(kq, Change(0, EvfiltRead, EvAdd, 0, 0, &marker)) == 0); + Require(Poll(kq, events, 4) == 0); + const KernelTimespec brief{0, 20000000}; + Require(kevent_nid_postfix(kq, nullptr, 0, events, 4, &brief) == 0); + Require(Apply(kq, Change(0, EvfiltRead, EvAdd, 0, 0, &marker)) == 0); + Require(Apply(kq, Change(0, EvfiltRead, EvDelete)) == 0); + RequireFailure(Apply(kq, Change(0, EvfiltRead, EvDelete)), GuestEnoent); + Require(ThrownMessage([kq] { Apply(kq, Change(0, EvfiltWrite, EvAdd)); }).find("EVFILT_WRITE on a descriptor that is neither a socket nor the write end of a pipe") != std::string::npos); + Require(close_nid_postfix(kq) == 0); +} + int main() { + StandardInput(); CreateAndClose(); Timers(); + PipeRead(); +#ifndef __APPLE__ + PipeWrite(); +#endif UserEvents(); + WatchKqueue(); SocketPair(); +#ifdef __linux__ + EdgeTriggeredSocketRead(); + EdgeTriggeredSocketWrite(); + SocketWriteLowWaterWhileFilling(); + EdgeTriggeredPipeRead(); + EdgeTriggeredDispatch(); +#endif + DisableAndDispatch(); Errors(); + LowWaterMarks(); FileWatch(); #ifndef _WIN32 RenameAndDeleteWatchedFile(); +#endif +#if !defined(_WIN32) && !defined(__APPLE__) MoveDirectoryEntries(); #endif DirectoryWatch(); DeleteWatchedFile(); +#ifdef _WIN32 + DeniedParentWatch(); +#endif Timeout(); return 0; } diff --git a/core/libs/tests/GuestLibcCndInitDefaultName.cpp b/core/libs/tests/GuestLibcCndInitDefaultName.cpp new file mode 100644 index 0000000000..6d5d046398 --- /dev/null +++ b/core/libs/tests/GuestLibcCndInitDefaultName.cpp @@ -0,0 +1,42 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI _Cnd_init_with_default_name_override_nid_postfix(void** handle, const char* name); +void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); +int APS5_VABI _Cnd_signal_nid_postfix(void** handle); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static void RequireThrows(TCall call) { + try { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +constexpr int success = 0; + +int main() { + void* condition = nullptr; + Require(_Cnd_init_with_default_name_override_nid_postfix(&condition, "GameCond") == success); + Require(condition != nullptr); + Require(_Cnd_signal_nid_postfix(&condition) == success); + _Cnd_destroy_nid_postfix(&condition); + Require(condition == nullptr); + + void* unnamed = nullptr; + Require(_Cnd_init_with_default_name_override_nid_postfix(&unnamed, nullptr) == success); + Require(unnamed != nullptr); + _Cnd_destroy_nid_postfix(&unnamed); + + RequireThrows([] { + _Cnd_init_with_default_name_override_nid_postfix(nullptr, "NullHandle"); + }); + return 0; +} diff --git a/core/libs/tests/GuestLibcCndTimedWait.cpp b/core/libs/tests/GuestLibcCndTimedWait.cpp index 07bb8719c3..e85144f158 100644 --- a/core/libs/tests/GuestLibcCndTimedWait.cpp +++ b/core/libs/tests/GuestLibcCndTimedWait.cpp @@ -1,10 +1,18 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include #include #include #include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif struct Xtime { std::int64_t sec; @@ -18,12 +26,30 @@ int APS5_VABI _Mtx_lock_nid_postfix(void** handle); int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); int APS5_VABI _Cnd_init_nid_postfix(void** handle); void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); +int APS5_VABI _Cnd_wait_nid_postfix(void** condition, void** mutex); int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle); int APS5_VABI _Cnd_timedwait_nid_postfix(void** condition, void** mutex, const Xtime* target); } static void Require(bool value) { if (!value) std::abort(); } +#ifdef _WIN32 +struct TimerResolution { + using SetTimerResolution = LONG(WINAPI*)(ULONG, BOOLEAN, PULONG); + SetTimerResolution set = nullptr; + ULONG current = 0; + + TimerResolution() { + const HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); + if (ntdll) + set = reinterpret_cast(GetProcAddress(ntdll, "NtSetTimerResolution")); + Require(set != nullptr && set(5000, TRUE, ¤t) >= 0); + } + + ~TimerResolution() { set(5000, FALSE, ¤t); } +}; +#endif + template static void RequireThrows(TCall call) { try { @@ -72,6 +98,22 @@ int main() { Require(result == success); } +#ifdef _WIN32 + TimerResolution timerResolution; + std::vector shortWaits; + for (int i = 0; i < 31; ++i) { + const auto start = std::chrono::steady_clock::now(); + const auto deadline = std::chrono::system_clock::now() + std::chrono::milliseconds(1); + const auto seconds = std::chrono::duration_cast(deadline.time_since_epoch()).count(); + const auto nanoseconds = std::chrono::duration_cast(deadline.time_since_epoch()).count() % 1000000000; + const Xtime shortTimeout{seconds, nanoseconds}; + Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &shortTimeout) == timedout); + shortWaits.push_back(std::chrono::steady_clock::now() - start); + } + std::sort(shortWaits.begin(), shortWaits.end()); + Require(shortWaits[shortWaits.size() / 2] < std::chrono::milliseconds(12)); +#endif + const auto unnormalizedDeadline = std::chrono::system_clock::now() + std::chrono::milliseconds(20); const auto unnormalizedSec = std::chrono::duration_cast(unnormalizedDeadline.time_since_epoch()).count(); const auto unnormalizedNsec = std::chrono::duration_cast(unnormalizedDeadline.time_since_epoch()).count() % 1000000000; @@ -99,6 +141,38 @@ int main() { } signaler.join(); + ready = false; + std::thread untimedSignaler([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(_Mtx_lock_nid_postfix(&mutex) == success); + ready = true; + Require(_Cnd_broadcast_nid_postfix(&condition) == success); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + }); + Require(_Cnd_wait_nid_postfix(&condition, &mutex) == success); + Require(ready); + untimedSignaler.join(); + + constexpr std::int64_t farSeconds = 10000000000; + const Xtime farTime{farSeconds, 999999999}; + constexpr auto clockSeconds = std::chrono::duration_cast(std::chrono::system_clock::duration::max()).count(); + 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/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index 0aabfe21ce..001558be39 100644 --- a/core/libs/tests/GuestLibcThreads.cpp +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -17,6 +17,44 @@ void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); int APS5_VABI _Cnd_wait_nid_postfix(void** condition, void** mutex); int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle); int APS5_VABI _Cnd_signal_nid_postfix(void** handle); + +struct GuestPad { + unsigned (APS5_VABI* const* vtable)(GuestPad*); + void* condition; + void* mutex; + bool started; +}; + +void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad); +void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad); +void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad); +void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, void** thread); +int APS5_VABI _Thrd_join_nid_postfix(void* thread, int* code); +int APS5_VABI _Thrd_detach_nid_postfix(void* thread); +void RegisterPthreadCreate_nid_no_patch(int (APS5_VABI* fn)(void**, const void*, void* (APS5_VABI*)(void*), void*)); +void RegisterPthreadJoin_nid_no_patch(int (APS5_VABI* fn)(void*, void**)); +void RegisterPthreadDetach_nid_no_patch(int (APS5_VABI* fn)(void*)); + +struct ThreadForwards { + int (APS5_VABI* once)(std::int32_t*, void (APS5_VABI*)(void)); + int (APS5_VABI* keyCreate)(int*, void (APS5_VABI*)(void*)); + int (APS5_VABI* keyDelete)(int); + void* (APS5_VABI* getSpecific)(int); + int (APS5_VABI* setSpecific)(int, void*); + int (APS5_VABI* equal)(void*, void*); + void* (APS5_VABI* self)(); + void (APS5_VABI* exit)(void*); +}; +void RegisterThreadForwards_nid_no_patch(const ThreadForwards* forwards); +void APS5_VABI _Call_once_nid_postfix(std::int32_t* flag, void (APS5_VABI* function)(void)); +int APS5_VABI _Tss_create_nid_postfix(int* key, void (APS5_VABI* destructor)(void*)); +int APS5_VABI _Tss_delete_nid_postfix(int key); +void* APS5_VABI _Tss_get_nid_postfix(int key); +int APS5_VABI _Tss_set_nid_postfix(int key, void* value); +int APS5_VABI _Thrd_equal_nid_postfix(void* first, void* second); +void* APS5_VABI _Thrd_current_nid_postfix(void); +int APS5_VABI _Mtx_current_owns_nid_postfix(void** handle); +void* APS5_VABI scePthreadSelf(); } static void Require(bool value) { if (!value) std::abort(); } @@ -183,10 +221,167 @@ static void ConditionSignal() { _Cnd_destroy_nid_postfix(&condition); } +struct TestPad { + unsigned (APS5_VABI* const* vtable)(GuestPad*); + void* condition; + void* mutex; + bool started; + std::atomic ran{false}; +}; + +struct HostPadThread { + std::thread worker; + void* result = nullptr; +}; + +static int APS5_VABI HostPthreadCreate(void** thread, const void*, void* (APS5_VABI* entry)(void*), void* arg) { + auto* handle = new HostPadThread{}; + handle->worker = std::thread([handle, entry, arg] { handle->result = entry(arg); }); + *thread = handle; + return 0; +} + +static int APS5_VABI HostPthreadJoin(void* thread, void** value) { + if (!thread) return 22; + auto* handle = static_cast(thread); + handle->worker.join(); + if (value) *value = handle->result; + delete handle; + return 0; +} + +static int APS5_VABI HostPthreadDetach(void* thread) { + if (!thread) return 22; + static_cast(thread)->worker.detach(); + return 0; +} + +static unsigned APS5_VABI TestPadGo(GuestPad* base) { + auto* pad = reinterpret_cast(base); + pad->ran.store(true); + _ZNSt4_Pad8_ReleaseEv_nid_postfix(base); + return 42; +} + +static void PadLaunchAndJoin() { + unsigned (APS5_VABI* vtable[1])(GuestPad*) = {TestPadGo}; + TestPad unregPad{}; + unregPad.vtable = vtable; + _ZNSt4_PadC2Ev_nid_postfix(reinterpret_cast(&unregPad)); + RegisterPthreadCreate_nid_no_patch(nullptr); + RegisterPthreadJoin_nid_no_patch(nullptr); + void* unregThread = nullptr; + RequireThrows([&] { + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(reinterpret_cast(&unregPad), &unregThread); + }); + RequireThrows([&] { + int code = 0; + _Thrd_join_nid_postfix(&unregPad, &code); + }); + _ZNSt4_PadD2Ev_nid_postfix(reinterpret_cast(&unregPad)); + + RegisterPthreadCreate_nid_no_patch(HostPthreadCreate); + RegisterPthreadJoin_nid_no_patch(HostPthreadJoin); + + TestPad pad{}; + pad.vtable = vtable; + _ZNSt4_PadC2Ev_nid_postfix(reinterpret_cast(&pad)); + void* thread = nullptr; + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(reinterpret_cast(&pad), &thread); + Require(thread != nullptr && pad.started && pad.ran.load()); + _ZNSt4_PadD2Ev_nid_postfix(reinterpret_cast(&pad)); + int code = 0; + Require(_Thrd_join_nid_postfix(thread, &code) == success && code == 42); + Require(_Thrd_join_nid_postfix(nullptr, &code) == 4); + + RegisterPthreadCreate_nid_no_patch(nullptr); + RegisterPthreadJoin_nid_no_patch(nullptr); +} + +static void PadDetach() { + unsigned (APS5_VABI* vtable[1])(GuestPad*) = {TestPadGo}; + RegisterPthreadDetach_nid_no_patch(nullptr); + RequireThrows([] { _Thrd_detach_nid_postfix(nullptr); }); + + RegisterPthreadCreate_nid_no_patch(HostPthreadCreate); + RegisterPthreadDetach_nid_no_patch(HostPthreadDetach); + + TestPad pad{}; + pad.vtable = vtable; + _ZNSt4_PadC2Ev_nid_postfix(reinterpret_cast(&pad)); + void* thread = nullptr; + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(reinterpret_cast(&pad), &thread); + Require(thread != nullptr && pad.ran.load()); + _ZNSt4_PadD2Ev_nid_postfix(reinterpret_cast(&pad)); + Require(_Thrd_detach_nid_postfix(thread) == success); + Require(_Thrd_detach_nid_postfix(nullptr) == 4); + + RegisterPthreadCreate_nid_no_patch(nullptr); + RegisterPthreadDetach_nid_no_patch(nullptr); +} + +static void KernelKeys() { + Require(_Thrd_current_nid_postfix() == scePthreadSelf()); + int key = -1; + Require(_Tss_create_nid_postfix(&key, nullptr) == success && key >= 0); + int value = 0; + Require(_Tss_get_nid_postfix(key) == nullptr); + Require(_Tss_set_nid_postfix(key, &value) == success && _Tss_get_nid_postfix(key) == &value); + Require(_Tss_delete_nid_postfix(key) == success); +} + +struct ForwardLog { + std::int32_t* flag; + void (APS5_VABI* function)(void); + int key; + void* value; + void* first; + void* second; +}; +static ForwardLog forwardLog; + +static int APS5_VABI FakeOnce(std::int32_t* flag, void (APS5_VABI* function)(void)) { + forwardLog.flag = flag; + forwardLog.function = function; + return 7; +} +static int APS5_VABI FakeKeyCreate(int* key, void (APS5_VABI*)(void*)) { *key = 41; return 0x80020016; } +static int APS5_VABI FakeKeyDelete(int key) { forwardLog.key = key; return 0x80020002; } +static void* APS5_VABI FakeGetSpecific(int key) { forwardLog.key = key; return &forwardLog; } +static int APS5_VABI FakeSetSpecific(int key, void* value) { forwardLog.key = key; forwardLog.value = value; return 0; } +static int APS5_VABI FakeEqual(void* first, void* second) { forwardLog.first = first; forwardLog.second = second; return 1; } +static void* APS5_VABI FakeSelf() { return &forwardLog.key; } +static void APS5_VABI FakeExit(void*) { std::abort(); } +static void APS5_VABI OnceTarget() {} + +static void ForwardedCalls() { + static const ThreadForwards fakes{FakeOnce, FakeKeyCreate, FakeKeyDelete, FakeGetSpecific, FakeSetSpecific, FakeEqual, FakeSelf, FakeExit}; + RegisterThreadForwards_nid_no_patch(&fakes); + std::int32_t flag = 0; + _Call_once_nid_postfix(&flag, OnceTarget); + Require(forwardLog.flag == &flag && forwardLog.function == OnceTarget); + int key = 0; + Require(_Tss_create_nid_postfix(&key, nullptr) == static_cast(0x80020016) && key == 41); + Require(_Tss_delete_nid_postfix(5) == static_cast(0x80020002) && forwardLog.key == 5); + Require(_Tss_get_nid_postfix(6) == &forwardLog && forwardLog.key == 6); + Require(_Tss_set_nid_postfix(8, &flag) == success && forwardLog.key == 8 && forwardLog.value == &flag); + Require(_Thrd_equal_nid_postfix(&flag, &key) == 1 && forwardLog.first == &flag && forwardLog.second == &key); + Require(_Thrd_current_nid_postfix() == &forwardLog.key); + Require(_Mtx_current_owns_nid_postfix(nullptr) == 1); + + RegisterThreadForwards_nid_no_patch(nullptr); + RequireThrows([] { _Thrd_current_nid_postfix(); }); + RequireThrows([] { _Tss_get_nid_postfix(0); }); +} + int main() { + KernelKeys(); + ForwardedCalls(); MutexBasics(); RecursiveMutex(); MutualExclusion(); ConditionBroadcast(); ConditionSignal(); + PadLaunchAndJoin(); + PadDetach(); } diff --git a/core/libs/tests/GuestLibreSsl3Digest.cpp b/core/libs/tests/GuestLibreSsl3Digest.cpp new file mode 100644 index 0000000000..19f6f8cb2b --- /dev/null +++ b/core/libs/tests/GuestLibreSsl3Digest.cpp @@ -0,0 +1,54 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +unsigned char* APS5_VABI MD5_nid_postfix(const unsigned char*, std::size_t, unsigned char*); +unsigned char* APS5_VABI SHA1_nid_postfix(const unsigned char*, std::size_t, unsigned char*); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "LibreSSL3 digest check failed at line %u\n", location.line()); + std::abort(); +} + +template static void RequireThrows(TAction action, std::source_location location = std::source_location::current()) { + try { action(); } catch (const std::exception&) { return; } + Require(false, location); +} + +static std::string Hex(const unsigned char* data, std::size_t size) { + static constexpr char digits[] = "0123456789abcdef"; + std::string text; + for (std::size_t i = 0; i < size; ++i) { + text += digits[data[i] >> 4]; + text += digits[data[i] & 15]; + } + return text; +} + +int main() { + const auto* abc = reinterpret_cast("abc"); + const std::string million(1000000, 'a'); + const auto* many = reinterpret_cast(million.data()); + unsigned char digest[24]; + std::memset(digest, 0xee, sizeof(digest)); + Require(MD5_nid_postfix(abc, 3, digest) == digest && Hex(digest, 16) == "900150983cd24fb0d6963f7d28e17f72" && digest[16] == 0xee); + Require(MD5_nid_postfix(nullptr, 0, digest) == digest && Hex(digest, 16) == "d41d8cd98f00b204e9800998ecf8427e"); + Require(MD5_nid_postfix(many, million.size(), digest) == digest && Hex(digest, 16) == "7707d6ae4e027c70eea2a935c2296f21"); + std::memset(digest, 0xee, sizeof(digest)); + Require(SHA1_nid_postfix(abc, 3, digest) == digest && Hex(digest, 20) == "a9993e364706816aba3e25717850c26c9cd0d89d" && digest[20] == 0xee); + Require(SHA1_nid_postfix(nullptr, 0, digest) == digest && Hex(digest, 20) == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); + Require(SHA1_nid_postfix(many, million.size(), digest) == digest && Hex(digest, 20) == "34aa973cd4c4daa4f61eeb2bdbad27316534016f"); + RequireThrows([&] { MD5_nid_postfix(abc, 3, nullptr); }); + RequireThrows([&] { SHA1_nid_postfix(abc, 3, nullptr); }); + RequireThrows([&] { SHA1_nid_postfix(nullptr, 1, digest); }); + + std::puts("libSceLibreSsl3 digest tests passed"); +} diff --git a/core/libs/tests/GuestLibreSslDigest.cpp b/core/libs/tests/GuestLibreSslDigest.cpp new file mode 100644 index 0000000000..8b9ba6183b --- /dev/null +++ b/core/libs/tests/GuestLibreSslDigest.cpp @@ -0,0 +1,195 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI MD4_Init_nid_postfix(void*); +int APS5_VABI MD4_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI MD4_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI MD5_Init_nid_postfix(void*); +int APS5_VABI MD5_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI MD5_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI SHA1_Init_nid_postfix(void*); +int APS5_VABI SHA1_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA1_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI SHA224_Init_nid_postfix(void*); +int APS5_VABI SHA224_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA224_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI SHA256_Init_nid_postfix(void*); +int APS5_VABI SHA256_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA256_Final_nid_postfix(unsigned char*, void*); +unsigned char* APS5_VABI SHA256_nid_postfix(const unsigned char*, std::size_t, unsigned char*); +int APS5_VABI SHA384_Init_nid_postfix(void*); +int APS5_VABI SHA384_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA384_Final_nid_postfix(unsigned char*, void*); +int APS5_VABI SHA512_Init_nid_postfix(void*); +int APS5_VABI SHA512_Update_nid_postfix(void*, const void*, std::size_t); +int APS5_VABI SHA512_Final_nid_postfix(unsigned char*, void*); +} + +using InitFunction = int (APS5_VABI*)(void*); +using UpdateFunction = int (APS5_VABI*)(void*, const void*, std::size_t); +using FinalFunction = int (APS5_VABI*)(unsigned char*, void*); + +struct Algorithm { + const char* name; + InitFunction init; + UpdateFunction update; + FinalFunction final; + std::size_t contextSize; + std::size_t blockSize; + std::size_t digestSize; + const char* expected[6]; +}; + +static void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", what); + std::abort(); +} + +template static void RequireThrows(TAction action, const char* what) { + try { action(); } catch (const std::exception&) { return; } + Require(false, what); +} + +static std::string Hex(const unsigned char* data, std::size_t size) { + static constexpr char digits[] = "0123456789abcdef"; + std::string text; + for (std::size_t i = 0; i < size; ++i) { + text += digits[data[i] >> 4]; + text += digits[data[i] & 15]; + } + return text; +} + +static std::string Digest(const Algorithm& algorithm, const std::string& message, std::size_t chunk) { + alignas(16) unsigned char context[256] {}; + unsigned char digest[64] {}; + Require(algorithm.init(context) == 1, "init returns 1"); + for (std::size_t offset = 0; offset < message.size(); offset += chunk) { + const auto length = std::min(chunk, message.size() - offset); + Require(algorithm.update(context, message.data() + offset, length) == 1, "update returns 1"); + } + Require(algorithm.final(digest, context) == 1, "final returns 1"); + return Hex(digest, algorithm.digestSize); +} + +int main() { + const std::string messages[6] { + "", + "abc", + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu", + "12345678901234567890123456789012345678901234567890123456789012345678901234567890", + std::string(1000000, 'a'), + }; + const Algorithm algorithms[] { + {"MD4", MD4_Init_nid_postfix, MD4_Update_nid_postfix, MD4_Final_nid_postfix, 92, 64, 16, { + "31d6cfe0d16ae931b73c59d7e0c089c0", "a448017aaf21d8525fc10ae87aa6729d", "4691a9ec81b1a6bd1ab8557240b245c5", + nullptr, "e33b4ddc9c38f2199c3e7b164fcc0536", nullptr}}, + {"MD5", MD5_Init_nid_postfix, MD5_Update_nid_postfix, MD5_Final_nid_postfix, 92, 64, 16, { + "d41d8cd98f00b204e9800998ecf8427e", "900150983cd24fb0d6963f7d28e17f72", "8215ef0796a20bcaaae116d3876c664a", + "03dd8807a93175fb062dfb55dc7d359c", "57edf4a22be3c955ac49da2e2107b67a", "7707d6ae4e027c70eea2a935c2296f21"}}, + {"SHA1", SHA1_Init_nid_postfix, SHA1_Update_nid_postfix, SHA1_Final_nid_postfix, 96, 64, 20, { + "da39a3ee5e6b4b0d3255bfef95601890afd80709", "a9993e364706816aba3e25717850c26c9cd0d89d", "84983e441c3bd26ebaae4aa1f95129e5e54670f1", + "a49b2446a02c645bf419f995b67091253a04a259", "50abf5706a150990a08b2c5ea40fa0e585554732", "34aa973cd4c4daa4f61eeb2bdbad27316534016f"}}, + {"SHA224", SHA224_Init_nid_postfix, SHA224_Update_nid_postfix, SHA224_Final_nid_postfix, 112, 64, 28, { + "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f", "23097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7", + "75388b16512776cc5dba5da1fd890150b0c6455cb4f58b1952522525", "c97ca9a559850ce97a04a96def6d99a9e0e0e2ab14e6b8df265fc0b3", + "b50aecbe4e9bb0b57bc5f3ae760a8e01db24f203fb3cdcd13148046e", "20794655980c91d8bbb4c1ea97618a4bf03f42581948b2ee4ee7ad67"}}, + {"SHA256", SHA256_Init_nid_postfix, SHA256_Update_nid_postfix, SHA256_Final_nid_postfix, 112, 64, 32, { + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1", "cf5b16a778af8380036ce59e7b0492370b249b11e8f07a51afac45037afee9d1", + "f371bc4a311f2b009eef952dd83ca80e2b60026c8e935592d0f9c308453c813e", "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0"}}, + {"SHA384", SHA384_Init_nid_postfix, SHA384_Update_nid_postfix, SHA384_Final_nid_postfix, 216, 128, 48, { + "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b", + "cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7", + "3391fdddfc8dc7393707a65b1b4709397cf8b1d162af05abfe8f450de5f36bc6b0455a8520bc4e6f5fe95b1fe3c8452b", + "09330c33f71147e83d192fc782cd1b4753111b173b3b05d22fa08086e3b0f712fcc7c71a557e2db966c3e9fa91746039", + "b12932b0627d1c060942f5447764155655bd4da0c9afa6dd9b9ef53129af1b8fb0195996d2de9ca0df9d821ffee67026", + "9d0e1809716474cb086e834e310a4a1ced149e9c00f248527972cec5704c2a5b07b8b3dc38ecc4ebae97ddd87f3d8985"}}, + {"SHA512", SHA512_Init_nid_postfix, SHA512_Update_nid_postfix, SHA512_Final_nid_postfix, 216, 128, 64, { + "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", + "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f", + "204a8fc6dda82f0a0ced7beb8e08a41657c16ef468b228a8279be331a703c33596fd15c13b1b07f9aa1d3bea57789ca031ad85c7a71dd70354ec631238ca3445", + "8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909", + "72ec1ef1124a45b047e8b7c75a932195135bb61de24ec0d1914042246e0aec3a2354e093d76f3048b456764346900cb130d2a4fd5dd16abb5e30bcb850dee843", + "e718483d0ce769644e2e42c7bc15b4638e1f98b13b2044285632a803afa973ebde0ff244877ea60a4cb0432ce577c31beb009c5c2c49aa2e4eadb217ad8cc09b"}}, + }; + + for (const auto& algorithm : algorithms) { + for (int i = 0; i < 6; ++i) { + if (!algorithm.expected[i]) continue; + const auto& message = messages[i]; + const std::size_t chunks[] {message.size() ? message.size() : 1, 1, algorithm.blockSize - 1, algorithm.blockSize, algorithm.blockSize + 1, 1000}; + for (auto chunk : chunks) { + if (i == 5 && chunk < 64) continue; + Require(Digest(algorithm, message, chunk) == algorithm.expected[i], algorithm.name); + } + } + + alignas(16) unsigned char context[256] {}; + alignas(16) unsigned char copy[256] {}; + unsigned char digest[64] {}; + unsigned char copyDigest[64] {}; + const bool wide = algorithm.blockSize == 128; + const auto num = [&](const unsigned char* c) { + std::uint32_t value; + std::memcpy(&value, c + (wide ? 208 : (algorithm.contextSize == 112 ? 104 : algorithm.contextSize - 4)), 4); + return value; + }; + const auto bitsLow = [&](const unsigned char* c) { + const std::size_t offset = wide ? 64 : (algorithm.contextSize == 96 ? 20 : algorithm.contextSize == 112 ? 32 : 16); + std::uint64_t value = 0; + std::memcpy(&value, c + offset, wide ? 8 : 4); + return value; + }; + Require(algorithm.init(context) == 1, "init"); + Require(algorithm.update(context, nullptr, 0) == 1, "empty update with null data"); + Require(algorithm.update(context, "ab", 2) == 1, "partial update"); + Require(num(context) == 2 && bitsLow(context) == 16, "context records the buffered bytes and the bit count"); + std::memcpy(copy, context, sizeof(copy)); + Require(algorithm.update(context, "c", 1) == 1 && algorithm.update(copy, "c", 1) == 1, "update of a copied context"); + Require(algorithm.final(digest, context) == 1 && algorithm.final(copyDigest, copy) == 1, "final of a copied context"); + Require(Hex(digest, algorithm.digestSize) == algorithm.expected[1] && !std::memcmp(digest, copyDigest, sizeof(digest)), "copied context digest"); + if (!wide) Require(num(context) == 0, "final clears the buffered count"); + RequireThrows([&] { algorithm.init(nullptr); }, "null context"); + RequireThrows([&] { algorithm.update(context, nullptr, 1); }, "null data"); + if (!wide) RequireThrows([&] { algorithm.final(nullptr, context); }, "null digest"); + } + + alignas(16) unsigned char context[256] {}; + unsigned char digest[64] {}; + Require(SHA512_Init_nid_postfix(context) == 1 && SHA512_Final_nid_postfix(nullptr, context) == 0, "SHA512_Final without a digest buffer"); + SHA256_Init_nid_postfix(context); + std::uint32_t length = 33; + std::memcpy(context + 108, &length, 4); + Require(SHA256_Final_nid_postfix(digest, context) == 0, "SHA256_Final with an oversized digest length"); + SHA256_Init_nid_postfix(context); + length = 16; + std::memcpy(context + 108, &length, 4); + std::memset(digest, 0xee, sizeof(digest)); + Require(SHA256_Final_nid_postfix(digest, context) == 1 && Hex(digest, 16) == "e3b0c44298fc1c149afbf4c8996fb924" && digest[16] == 0xee, "SHA256_Final truncates to the digest length"); + SHA512_Init_nid_postfix(context); + length = 32; + std::memcpy(context + 212, &length, 4); + RequireThrows([&] { SHA512_Final_nid_postfix(digest, context); }, "SHA-512/256 digest length"); + SHA256_Init_nid_postfix(context); + length = 64; + std::memcpy(context + 104, &length, 4); + RequireThrows([&] { SHA256_Update_nid_postfix(context, "a", 1); }, "corrupt buffered count"); + + const auto* abc = reinterpret_cast("abc"); + Require(SHA256_nid_postfix(abc, 3, digest) == digest && Hex(digest, 32) == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", "SHA256 one-shot"); + Require(SHA256_nid_postfix(nullptr, 0, digest) == digest && Hex(digest, 32) == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "SHA256 one-shot of nothing"); + RequireThrows([&] { SHA256_nid_postfix(abc, 3, nullptr); }, "SHA256 one-shot without a digest buffer"); + std::puts("libSceLibreSsl digest tests passed"); +} diff --git a/core/libs/tests/GuestLoadExecHost.cpp b/core/libs/tests/GuestLoadExecHost.cpp new file mode 100644 index 0000000000..bba40568d0 --- /dev/null +++ b/core/libs/tests/GuestLoadExecHost.cpp @@ -0,0 +1,160 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +extern "C" int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*); +extern "C" int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI*)(void*), void*, void*); +extern "C" int APS5_VABI getargc_nid_postfix(); +extern "C" const char** APS5_VABI getargv_nid_postfix(); + +namespace { + +bool cleaned = false; + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "LoadExec fixture: %s\n", message); + std::fflush(stderr); + std::_Exit(98); + } +} + +std::filesystem::path Root() { +#ifdef _WIN32 + std::wstring value(32768, L'\0'); + const auto length = GetEnvironmentVariableW(L"ANYPS5_LOADEXEC_TEST_ROOT", value.data(), static_cast(value.size())); + Require(length > 0 && length < value.size(), "missing fixture directory"); + value.resize(length); + return value; +#else + const auto* value = std::getenv("ANYPS5_LOADEXEC_TEST_ROOT"); + Require(value != nullptr, "missing fixture directory"); + return std::filesystem::path(reinterpret_cast(value)); +#endif +} + +void RecordPid() { + std::ofstream file(Root() / "pids.txt", std::ios::app); +#ifdef _WIN32 + file << GetCurrentProcessId() << '\n'; +#else + file << getpid() << '\n'; +#endif + file.close(); + Require(!file.fail(), "writing the process ID file"); +} + +void Record(const char* value) { + std::ofstream file(Root() / "events.txt", std::ios::app); + file << value << '\n'; + file.close(); + Require(!file.fail(), "writing the event file"); +} + +std::string Events() { + std::ifstream file(Root() / "events.txt"); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +void Shutdown() { cleaned = true; Record("shutdown"); } +void APS5_VABI Finalize(void*) { Record("guest-exit"); } +void HostFinalize() { Record("host-exit"); } + +void Register() { + LibcRegisterShutdown_nid_postfix(Shutdown); + Require(__cxa_atexit_nid_postfix(Finalize, nullptr, nullptr) == 0, "guest exit registration"); + Require(std::atexit(HostFinalize) == 0, "host exit registration"); +} + +void Arguments(const std::uintptr_t* block, const std::vector& expected) { + Require(block[0] == expected.size(), "entry argument count"); + const auto* argv = reinterpret_cast(block + 1); + Require(getargc_nid_postfix() == static_cast(expected.size()), "getargc argument count"); + const auto* kernel = getargv_nid_postfix(); + for (std::size_t index = 0; index < expected.size(); ++index) { + Require(argv[index] && expected[index] == argv[index], "entry argument value"); + Require(kernel[index] && expected[index] == kernel[index], "getargv argument value"); + } + Require(argv[expected.size()] == nullptr && kernel[expected.size()] == nullptr, "argument terminator"); +} + +const char* FirstArgument() { + return std::getenv("ANYPS5_TEST_QUOTED_ARGV0") ? "first\"and\\slash\\ Кириллица 東京" : ""; +} + +void Reject(const char* path, const char* const* argv) { + bool rejected = false; + try { sceSystemServiceLoadExec(path, argv); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected && !cleaned, "rejected target ran shutdown"); +} + +} + +extern "C" void APS5_VABI LoadExecFirst_nid_no_patch(const std::uintptr_t* block) { + if (!Events().empty()) { + Arguments(block, {"back", "argument with spaces"}); + Require(std::filesystem::equivalent(std::filesystem::current_path(), Root()), "return working directory"); + RecordPid(); + Record("returned"); + LibcTerminate_nid_no_patch(37); + } + Require(std::filesystem::equivalent(std::filesystem::current_path(), Root()), "initial working directory"); + RecordPid(); + Record("first"); + Register(); + const char* const argv[] = {FirstArgument(), "argument with spaces", "quote\"and\\slash\\", "Кириллица 東京", nullptr}; + Reject("/app0/absent.bin", argv); + Reject("/other/second.bin", argv); + Reject("/app0/../second.bin", argv); +#ifdef _WIN32 + const auto second = Root() / L"picker.second.bin.exe"; +#else + const auto second = Root() / "picker.second.bin.elf"; +#endif + auto saved = second; + saved += ".saved"; + std::filesystem::rename(second, saved); + Reject("/app0/picker/second.bin", argv); + std::filesystem::rename(saved, second); + std::filesystem::current_path(Root() / "app0" / "working"); + sceSystemServiceLoadExec("/app0/picker/second.bin", argv); + Require(false, "LoadExec returned to the first executable"); +} + +extern "C" void APS5_VABI LoadExecSecond_nid_no_patch(const std::uintptr_t* block) { + Arguments(block, {FirstArgument(), "argument with spaces", "quote\"and\\slash\\", "Кириллица 東京"}); + Require(std::filesystem::equivalent(std::filesystem::current_path(), Root()), "secondary working directory"); + std::ifstream resource(ResolvePath_nid_no_patch("/app0/resource.txt")); + std::string text; + std::getline(resource, text); + Require(text == "resource survives", "secondary resource access"); + Require(Events() == "first\nshutdown\n", "old executable did not end after its shutdown alone"); +#ifdef _WIN32 + wchar_t value[32]{}; + Require(GetEnvironmentVariableW(L"ANYPS5_LOADEXEC_PARENT", value, 32) == 0, "parent wait variable was retained"); + Require(GetEnvironmentVariableW(L"ANYPS5_LOADEXEC_ARGV_SKIP", value, 32) == 0, "argument marker was retained"); +#endif + Require(!cleaned, "secondary inherited old runtime state"); + RecordPid(); + Record("second"); + Register(); + const char* const argv[] = {"back", "argument with spaces", nullptr}; + sceSystemServiceLoadExec("/app0/eboot.bin", argv); + Require(false, "LoadExec returned to the second executable"); +} diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index 93cf43ff11..78aaee27be 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -13,10 +13,18 @@ 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); void APS5_VABI _ZNSt8ios_baseD2Ev_nid_postfix(GuestLocale::IosBase* self); +struct StreamManipulator { + GuestLocale::IosBase* (APS5_VABI* function)(GuestLocale::IosBase*, std::int32_t); + std::int32_t argument; +}; +StreamManipulator APS5_VABI _ZSt4setwi_nid_postfix(std::int32_t width); +void APS5_VABI _ZNSt8ios_base7_AddstdEPS__nid_postfix(GuestLocale::IosBase* self); const short* APS5_VABI _Getpctype_nid_postfix(); const short* APS5_VABI _Getptolower_nid_postfix(); const short* APS5_VABI _Getptoupper_nid_postfix(); @@ -26,6 +34,9 @@ void APS5_VABI _Locksyslock_nid_postfix(); void APS5_VABI _Unlocksyslock_nid_postfix(); std::uint32_t APS5_VABI _Towctrans_nid_postfix(std::uint32_t wc, std::uint64_t desc); int* APS5_VABI __error_nid_postfix(); +extern std::uint64_t _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix; +extern std::uint64_t _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix; +extern std::uintptr_t _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[17]; } @@ -44,11 +55,24 @@ static void RequireException(TAction action) { Require(caught); } +static void CheckInputFacetObjects() { + Require(_ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 0 && _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 0); + _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 7; + _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 9; + Require(_ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 7 && _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 9); + _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; + _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; + for (const auto slot : _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix) Require(slot == 0); +} + static void CheckGuestCalls() { auto* locale = _ZSt21_sceLibcClassicLocale_nid_postfix; 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; @@ -147,6 +171,66 @@ static void CheckStreamDestruction() { Require(object.stream.locale == &_ZSt21_sceLibcClassicLocale_nid_postfix); } +static void CheckStreamWidth() { + struct GuardedStream { + std::uint64_t before = 0x12345678; + GuestLocale::IosBase stream{}; + std::uint64_t after = 0x87654321; + } object; + object.stream.flags = 0x4000; + object.stream.precision = 6; + const auto manipulator = _ZSt4setwi_nid_postfix(12); + Require(manipulator.argument == 12 && manipulator.function != nullptr); + Require(manipulator.function(&object.stream, manipulator.argument) == &object.stream); + Require(object.stream.width == 12 && object.stream.flags == 0x4000 && object.stream.precision == 6); + Require(object.before == 0x12345678 && object.after == 0x87654321); + const auto reset = _ZSt4setwi_nid_postfix(0); + reset.function(&object.stream, reset.argument); + Require(object.stream.width == 0); + RequireException([&] { manipulator.function(nullptr, manipulator.argument); }); +} + +static void CheckStandardStreams() { + struct GuardedStream { + std::uint64_t before = 0x12345678; + GuestLocale::IosBase stream{}; + std::uint64_t after = 0x87654321; + } streams[9]; + auto* classic = &_ZSt21_sceLibcClassicLocale_nid_postfix; + for (auto& object : streams) object.stream.locale = classic; + RequireException([&] { _ZNSt8ios_base7_AddstdEPS__nid_postfix(nullptr); }); + _ZNSt8ios_base7_AddstdEPS__nid_postfix(&streams[0].stream); + _ZNSt8ios_base7_AddstdEPS__nid_postfix(&streams[1].stream); + Require(streams[0].stream.standardStream == 1 && streams[1].stream.standardStream == 2); + _ZNSt8ios_base7_AddstdEPS__nid_postfix(&streams[0].stream); + Require(streams[0].stream.standardStream == 1); + _ZNSt8ios_baseD2Ev_nid_postfix(&streams[0].stream); + Require(streams[0].stream.locale == classic && streams[0].stream.standardStream == 1); + _ZNSt8ios_baseD2Ev_nid_postfix(&streams[0].stream); + Require(streams[0].stream.locale == nullptr && streams[0].stream.standardStream == 1); + streams[0].stream.locale = classic; + _ZNSt8ios_base7_AddstdEPS__nid_postfix(&streams[0].stream); + Require(streams[0].stream.standardStream == 1); + _ZNSt8ios_baseD2Ev_nid_postfix(&streams[1].stream); + Require(streams[1].stream.locale == nullptr && streams[1].stream.standardStream == 2); + for (int index = 2; index < 7; ++index) { + _ZNSt8ios_base7_AddstdEPS__nid_postfix(&streams[index].stream); + Require(streams[index].stream.standardStream == index + 1); + } + _ZNSt8ios_base7_AddstdEPS__nid_postfix(&streams[7].stream); + _ZNSt8ios_base7_AddstdEPS__nid_postfix(&streams[8].stream); + Require(streams[7].stream.standardStream == 8 && streams[8].stream.standardStream == 8); + _ZNSt8ios_baseD2Ev_nid_postfix(&streams[7].stream); + Require(streams[7].stream.locale == classic); + _ZNSt8ios_baseD2Ev_nid_postfix(&streams[8].stream); + Require(streams[8].stream.locale == nullptr); + for (int index = 2; index < 7; ++index) { + _ZNSt8ios_baseD2Ev_nid_postfix(&streams[index].stream); + Require(streams[index].stream.locale == nullptr && streams[index].stream.standardStream == index + 1); + } + for (auto& object : streams) Require(object.before == 0x12345678 && object.after == 0x87654321); +} + static void CheckWideCaseTranslation() { const auto* lower = _Getptolower_nid_postfix(); const auto* upper = _Getptoupper_nid_postfix(); @@ -175,7 +259,10 @@ int main() { CheckCharacterTables(); CheckCharacterConversions(); CheckStreamDestruction(); + CheckStreamWidth(); + CheckStandardStreams(); CheckWideCaseTranslation(); + CheckInputFacetObjects(); _Locksyslock_nid_postfix(); _Locksyslock_nid_postfix(); _Unlocksyslock_nid_postfix(); diff --git a/core/libs/tests/GuestLoginDialog.cpp b/core/libs/tests/GuestLoginDialog.cpp index 7eaeb5619f..09e5d52663 100644 --- a/core/libs/tests/GuestLoginDialog.cpp +++ b/core/libs/tests/GuestLoginDialog.cpp @@ -1,13 +1,14 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include "prx/libSceCommonDialog/CommonDialog.hpp" extern "C" { int APS5_VABI sceLoginDialogInitialize(void); -int APS5_VABI sceLoginDialogOpen(const void* param); +int APS5_VABI sceLoginDialogOpen(const LoginDialogParam* param); int APS5_VABI sceLoginDialogUpdateStatus(void); int APS5_VABI sceLoginDialogGetStatus(void); -int APS5_VABI sceLoginDialogGetResult(void* result); +int APS5_VABI sceLoginDialogGetResult(LoginDialogResult* result); int APS5_VABI sceLoginDialogClose(void); int APS5_VABI sceLoginDialogTerminate(void); } @@ -16,39 +17,78 @@ namespace { void Require(bool value) { if (!value) std::abort(); } +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; constexpr int kStatusFinished = 3; -constexpr int kErrNotInitialized = static_cast(0x80B80003); -constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); -constexpr int kErrNotFinished = static_cast(0x80B80005); -constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kErrNotInitialized = static_cast(0x81340001); +constexpr int kErrAlreadyInitialized = static_cast(0x81340002); +constexpr int kErrNotFinished = static_cast(0x81340005); +constexpr int kErrArgNull = static_cast(0x81340003); constexpr int kResultUserCanceled = 1; } int main() { - std::int64_t param = 0; - std::int32_t result[2] = {-1, -1}; + LoginDialogParam param{}; + param.size = sizeof(param); + std::fill(std::begin(param.userIds), std::end(param.userIds), -1); + LoginDialogResult result{-1, -1, {11, 22, 33, 44, 55, 66, 77, 88}}; Require(sceLoginDialogTerminate() == kErrNotInitialized); Require(sceLoginDialogOpen(¶m) == kErrNotInitialized); - Require(sceLoginDialogGetResult(result) == 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); + Require(sceLoginDialogGetResult(&result) == kErrNotFinished); + Require(result.result == 1 && result.errorCode == static_cast(0x81340007u)); + param.size = 63; + Require(sceLoginDialogOpen(¶m) == kErrArgNull); + param.size = sizeof(param); + param.mode = 2; + Require(sceLoginDialogOpen(¶m) == kErrArgNull); + param.mode = 1; + param.reserved[4] = 1; + Require(sceLoginDialogOpen(¶m) == kErrArgNull); + param.reserved[4] = 0; + for (auto& userId : param.userIds) { + for (const auto invalid : {0, 0x0fffffff, 0x40000000, -2}) { + userId = invalid; + Require(sceLoginDialogOpen(¶m) == static_cast(0x81340007u)); + } + userId = -1; + } + param.userIds[0] = 0x10000000; + param.userIds[8] = 0x3fffffff; Require(sceLoginDialogOpen(¶m) == 0); Require(sceLoginDialogGetStatus() == kStatusFinished); Require(sceLoginDialogUpdateStatus() == kStatusFinished); Require(sceLoginDialogGetResult(nullptr) == kErrArgNull); - Require(sceLoginDialogGetResult(result) == 0); - Require(result[0] == kResultUserCanceled); - Require(result[1] == -1); + Require(sceLoginDialogGetResult(&result) == 0); + Require(result.result == kResultUserCanceled); + Require(result.errorCode == static_cast(0x81340007u)); + Require(result.data[0] == 11 && result.data[7] == 88); Require(sceLoginDialogClose() == 0); Require(sceLoginDialogTerminate() == 0); Require(sceLoginDialogTerminate() == kErrNotInitialized); + Require(sceLoginDialogGetStatus() == kStatusNone); + + Require(sceLoginDialogInitialize() == 0); + Require(sceLoginDialogGetStatus() == kStatusInitialized); + Require(sceLoginDialogClose() == 0); + Require(sceLoginDialogGetStatus() == kStatusFinished); + result.result = -1; + Require(sceLoginDialogGetResult(&result) == 0); + Require(result.result == kResultUserCanceled); + Require(sceLoginDialogTerminate() == 0); } 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/GuestM4aacEnc.cpp b/core/libs/tests/GuestM4aacEnc.cpp new file mode 100644 index 0000000000..d9f6f00888 --- /dev/null +++ b/core/libs/tests/GuestM4aacEnc.cpp @@ -0,0 +1,66 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int32_t APS5_VABI sceM4aacEncClearContext(int32_t m4aacEncHandle); +int APS5_VABI sceM4aacEncClearContextEx(); +int32_t APS5_VABI sceM4aacEncCreateEncoder(int32_t channels, int32_t samplingRate, int32_t bitRate, int32_t inputFormat, int32_t outputFormat, int32_t* pInternalError); +int APS5_VABI sceM4aacEncCreateEncoderEx(); +int32_t APS5_VABI sceM4aacEncDeleteEncoder(int32_t m4aacEncHandle, int32_t* pInternalError); +int APS5_VABI sceM4aacEncDeleteEncoderEx(); +int32_t APS5_VABI sceM4aacEncEncode(int32_t m4aacEncHandle, void* pInput, uint32_t inputByte, void* pOutput, uint32_t* pOutputByte, int32_t* pInternalError); +int APS5_VABI sceM4aacEncEncodeEx(); +int APS5_VABI sceM4aacEncEncodeWithPriority(); +int APS5_VABI sceM4aacEncEncodeWithPriorityEx(); +int32_t APS5_VABI sceM4aacEncFlush(int32_t m4aacEncHandle, void* pOutput, uint32_t* pOutputByte, int32_t* pInternalError); +int APS5_VABI sceM4aacEncFlushEx(); +int APS5_VABI sceM4aacEncFlushWithPriority(); +int APS5_VABI sceM4aacEncFlushWithPriorityEx(); +int APS5_VABI sceM4aacEncGetContext(); +int APS5_VABI sceM4aacEncInitialize(); +int APS5_VABI sceM4aacEncTerminate(); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +template +bool Throws(Call call) { + try { + static_cast(call()); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +} + +int main() { + uint32_t outputByte = 0; + int32_t internalError = 0; + + Require(Throws([&] { return sceM4aacEncClearContext(1); })); + Require(Throws([&] { return sceM4aacEncClearContextEx(); })); + Require(Throws([&] { return sceM4aacEncCreateEncoder(2, 48000, 128000, 0, 0, &internalError); })); + Require(Throws([&] { return sceM4aacEncCreateEncoderEx(); })); + Require(Throws([&] { return sceM4aacEncDeleteEncoder(1, &internalError); })); + Require(Throws([&] { return sceM4aacEncDeleteEncoderEx(); })); + Require(Throws([&] { return sceM4aacEncEncode(1, nullptr, 0, nullptr, &outputByte, &internalError); })); + Require(Throws([&] { return sceM4aacEncEncodeEx(); })); + Require(Throws([&] { return sceM4aacEncEncodeWithPriority(); })); + Require(Throws([&] { return sceM4aacEncEncodeWithPriorityEx(); })); + Require(Throws([&] { return sceM4aacEncFlush(1, nullptr, &outputByte, &internalError); })); + Require(Throws([&] { return sceM4aacEncFlushEx(); })); + Require(Throws([&] { return sceM4aacEncFlushWithPriority(); })); + Require(Throws([&] { return sceM4aacEncFlushWithPriorityEx(); })); + Require(Throws([&] { return sceM4aacEncGetContext(); })); + Require(Throws([&] { return sceM4aacEncInitialize(); })); + Require(Throws([&] { return sceM4aacEncTerminate(); })); + + Require(outputByte == 0); + Require(internalError == 0); +} diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 23c9d20d5d..04541e7c79 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -2,14 +2,27 @@ #include #include #include +#include +#include #include +#include #include #include #include #include #include #include +#include +struct GuestFloatingPointEnvironment { + std::uint32_t control; + std::uint32_t status; + std::uint32_t tag; + unsigned char other[16]; + std::uint32_t mxcsr; +}; +static_assert(sizeof(GuestFloatingPointEnvironment) == 32); extern "C" { +int APS5_VABI fegetenv_nid_postfix(GuestFloatingPointEnvironment*); double APS5_VABI atof_nid_postfix(const char*); double APS5_VABI strtod_nid_postfix(const char*, char**); double APS5_VABI wcstod_nid_postfix(const char16_t*, char16_t**); @@ -25,6 +38,7 @@ unsigned long long APS5_VABI strtoull_nid_postfix(const char*, char**, int); std::uintmax_t APS5_VABI strtoumax_nid_postfix(const char*, char**, int); unsigned long long APS5_VABI _Stoull_nid_postfix(const char*, char**, int); std::uint64_t APS5_VABI _Stoul_nid_postfix(const char*, char**, int); +int APS5_VABI atoi_nid_postfix(const char*); std::int64_t APS5_VABI atol_nid_postfix(const char*); long long APS5_VABI atoll_nid_postfix(const char*); int* APS5_VABI __error_nid_postfix(); @@ -46,6 +60,18 @@ long double APS5_VABI frexpl_nid_postfix(long double, int*); struct LibcFloatConstant { std::uint32_t bits[4]; }; extern LibcFloatConstant _FInf_nid_postfix; extern LibcFloatConstant _FNan_nid_postfix; +extern LibcFloatConstant _FDenorm_nid_postfix; +extern LibcFloatConstant _FSnan_nid_postfix; +union SonyDconst { unsigned short _Word[8]; float _Float; double _Double; long double _Long_double; }; +extern SonyDconst _Inf_nid_postfix; +extern SonyDconst _Denorm_nid_postfix; +extern SonyDconst _Hugeval_nid_postfix; +extern SonyDconst _Nan_nid_postfix; +extern SonyDconst _Snan_nid_postfix; +extern SonyDconst _LDenorm_nid_postfix; +extern SonyDconst _LInf_nid_postfix; +extern SonyDconst _LNan_nid_postfix; +extern SonyDconst _LSnan_nid_postfix; short APS5_VABI _FDtest_nid_postfix(const float*); short APS5_VABI _Dtest_nid_postfix(const double*); int APS5_VABI __fpclassifyf_nid_postfix(float); @@ -59,6 +85,17 @@ double APS5_VABI asin_nid_postfix(double); float APS5_VABI atan2f_nid_postfix(float, float); float APS5_VABI hypotf_nid_postfix(float, float); double APS5_VABI hypot_nid_postfix(double, double); +double APS5_VABI fdim_nid_postfix(double, double); +float APS5_VABI fdimf_nid_postfix(float, float); +float APS5_VABI fabsf_nid_postfix(float); +long double APS5_VABI fabsl_nid_postfix(long double); +double APS5_VABI copysign_nid_postfix(double, double); +float APS5_VABI copysignf_nid_postfix(float, float); +double APS5_VABI remquo_nid_postfix(double, double, int*); +float APS5_VABI remquof_nid_postfix(float, float, int*); +float APS5_VABI fmaf_nid_postfix(float, float, float); +long double APS5_VABI ceill_nid_postfix(long double); +long double APS5_VABI floorl_nid_postfix(long double); float APS5_VABI tanf_nid_postfix(float); int APS5_VABI rand_nid_postfix(); void APS5_VABI srand_nid_postfix(unsigned int); @@ -70,8 +107,14 @@ float APS5_VABI log1pf_nid_postfix(float); double APS5_VABI expm1_nid_postfix(double); float APS5_VABI expm1f_nid_postfix(float); float APS5_VABI erff_nid_postfix(float); +double APS5_VABI erf_nid_postfix(double); +double APS5_VABI erfc_nid_postfix(double); +float APS5_VABI erfcf_nid_postfix(float); double APS5_VABI lgamma_nid_postfix(double); +extern int signgam_nid_postfix; float APS5_VABI logbf_nid_postfix(float); +int APS5_VABI ilogb_nid_postfix(double); +int APS5_VABI ilogbf_nid_postfix(float); double APS5_VABI exp2_nid_postfix(double); float APS5_VABI __powisf2_nid_postfix(float, int); double APS5_VABI __powidf2_nid_postfix(double, int); @@ -79,14 +122,18 @@ double APS5_VABI ldexp_nid_postfix(double, int); double APS5_VABI scalbn_nid_postfix(double, int); float APS5_VABI scalbnf_nid_postfix(float, int); float APS5_VABI nextafterf_nid_postfix(float, float); +double APS5_VABI nextafter_nid_postfix(double, double); double APS5_VABI frexp_nid_postfix(double, int*); float APS5_VABI frexpf_nid_postfix(float, int*); std::int64_t APS5_VABI lround_nid_postfix(double); std::div_t APS5_VABI div_nid_postfix(int, int); double APS5_VABI sinh_nid_postfix(double); double APS5_VABI cosh_nid_postfix(double); +double APS5_VABI _Cosh_nid_postfix(double, double); float APS5_VABI coshf_nid_postfix(float); float APS5_VABI sinhf_nid_postfix(float); +float APS5_VABI _FSinh_nid_postfix(float, float); +float APS5_VABI _FCosh_nid_postfix(float, float); double APS5_VABI asinh_nid_postfix(double); float APS5_VABI asinhf_nid_postfix(float); double APS5_VABI acosh_nid_postfix(double); @@ -98,6 +145,7 @@ std::int64_t APS5_VABI llround_nid_postfix(double); std::int64_t APS5_VABI llroundf_nid_postfix(float); std::int64_t APS5_VABI llroundl_nid_postfix(long double); int APS5_VABI __isfinitef_nid_postfix(float); +int APS5_VABI __isfinitel_nid_postfix(long double); int APS5_VABI __isnormal_nid_postfix(double); int APS5_VABI __isnormalf_nid_postfix(float); int APS5_VABI __isinff_nid_postfix(float); @@ -105,6 +153,36 @@ 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); +double APS5_VABI significand_nid_postfix(double); +long double APS5_VABI acoshl_nid_postfix(long double); +long double APS5_VABI asinhl_nid_postfix(long double); +long double APS5_VABI atanhl_nid_postfix(long double); +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); +float APS5_VABI tgammaf_nid_postfix(float); +long double APS5_VABI hypotl_nid_postfix(long double, long double); +float APS5_VABI hypot3f_nid_postfix(float, float, float); +double APS5_VABI hypot3_nid_postfix(double, double, double); +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*); @@ -125,6 +203,20 @@ long double APS5_VABI truncl_nid_postfix(long double); long double APS5_VABI nearbyintl_nid_postfix(long double); double APS5_VABI logb_nid_postfix(double); double APS5_VABI remainder_nid_postfix(double, double); +struct DinkumwareFloatCharacteristics { + int digits; + int mantissaDigits; + int maxExponent10; + int maxExponent; + int minExponent10; + int minExponent; + alignas(16) unsigned char epsilon[16]; + alignas(16) unsigned char max[16]; + alignas(16) unsigned char min[16]; +}; +extern DinkumwareFloatCharacteristics _Flt_nid_postfix; +extern DinkumwareFloatCharacteristics _Dbl_nid_postfix; +extern DinkumwareFloatCharacteristics _Ldbl_nid_postfix; } static void Require(bool value) { if (!value) std::abort(); } @@ -144,6 +236,11 @@ static void CheckExactOperations() { Require(fabs_nid_postfix(-3.5) == 3.5 && fabs_nid_postfix(-INFINITY) == INFINITY); Require(std::bit_cast(fabs_nid_postfix(-0.)) == 0); Require(std::isnan(fabs_nid_postfix(-nan)) && !std::signbit(fabs_nid_postfix(-nan))); + Require(fabsf_nid_postfix(-3.5f) == 3.5f && fabsf_nid_postfix(-INFINITY) == INFINITY); + Require(std::bit_cast(fabsf_nid_postfix(-0.f)) == 0); + Require(std::isnan(fabsf_nid_postfix(-static_cast(nan))) && !std::signbit(fabsf_nid_postfix(-static_cast(nan)))); + Require(fabsl_nid_postfix(-3.5L) == 3.5L && fabsl_nid_postfix(-INFINITY) == INFINITY); + Require(std::isnan(fabsl_nid_postfix(-static_cast(nan))) && !std::signbit(fabsl_nid_postfix(-static_cast(nan)))); Require(trunc_nid_postfix(2.7) == 2. && trunc_nid_postfix(-2.7) == -2.); Require(std::bit_cast(trunc_nid_postfix(-0.5)) == 0x8000000000000000ull); Require(trunc_nid_postfix(4503599627370497.) == 4503599627370497. && trunc_nid_postfix(-1e300) == -1e300); @@ -170,6 +267,17 @@ static void CheckExactOperations() { Require(fma_nid_postfix(tenth, 10., -1.) == 0x1p-54); Require(fma_nid_postfix(0x1p1023, 2., -0x1p1023) == 0x1p1023); Require(std::isnan(fma_nid_postfix(INFINITY, 0., 1.))); + Require(fdim_nid_postfix(4.0, 1.0) == 3.0 && fdim_nid_postfix(1.0, 4.0) == 0.0); + Require(fdim_nid_postfix(4.0, 4.0) == 0.0 && !std::signbit(fdim_nid_postfix(1.0, 4.0))); + Require(std::isnan(fdim_nid_postfix(std::numeric_limits::quiet_NaN(), 1.0))); + Require(std::isnan(fdim_nid_postfix(1.0, std::numeric_limits::quiet_NaN()))); + Require(fdimf_nid_postfix(4.0f, 1.0f) == 3.0f && fdimf_nid_postfix(1.0f, 4.0f) == 0.0f); + Require(fdimf_nid_postfix(4.0f, 4.0f) == 0.0f && !std::signbit(fdimf_nid_postfix(1.0f, 4.0f))); + Require(std::isnan(fdimf_nid_postfix(std::numeric_limits::quiet_NaN(), 1.0f))); + Require(fmaf_nid_postfix(2.f, 3.f, 4.f) == 10.f); + volatile float tenthFloat = 0.1f; + Require(fmaf_nid_postfix(tenthFloat, 10.f, -1.f) != 0.f); + Require(std::isnan(fmaf_nid_postfix(INFINITY, 0.f, 1.f))); } static void CheckNearbyint() { @@ -185,6 +293,52 @@ static void CheckNearbyint() { Require(std::fetestexcept(FE_INEXACT) == 0); } +static std::uint16_t X87ControlWord() { + std::uint16_t control; + asm volatile("fnstcw %0" : "=m"(control)); + return control; +} + +static std::uint16_t X87StatusWord() { + std::uint16_t status; + asm volatile("fnstsw %0" : "=m"(status)); + return status; +} + +static void CheckFegetenv() { + const std::uint16_t savedControl = X87ControlWord(); + const unsigned savedCsr = _mm_getcsr(); + const std::uint16_t control = 0x0f7e; + const unsigned csr = 0x7fa0; + asm volatile("fnclex"); + asm volatile("fldcw %0" : : "m"(control)); + _mm_setcsr(csr); + volatile long double zero = 0.0L; + volatile long double quotient = 1.0L / zero; + (void)quotient; + const std::uint16_t status = X87StatusWord(); + Require((status & 0x04) != 0 && (status & 0x01) == 0); + + GuestFloatingPointEnvironment environment; + std::memset(&environment, 0xcc, sizeof(environment)); + Require(fegetenv_nid_postfix(&environment) == 0); + const std::uint16_t controlAfter = X87ControlWord(); + const unsigned csrAfter = _mm_getcsr(); + + asm volatile("fnclex"); + asm volatile("fldcw %0" : : "m"(savedControl)); + _mm_setcsr(savedCsr); + + Require((environment.control & 0xffff) == control); + Require((environment.status & 0xffff) == status); + Require(environment.mxcsr == csr); + Require(controlAfter == control); + Require(csrAfter == csr); + bool threw = false; + try { fegetenv_nid_postfix(nullptr); } catch (...) { threw = true; } + Require(threw); +} + static bool ExtendedBits(long double value, std::uint64_t mantissa, std::uint16_t signExponent) { unsigned char bytes[sizeof(long double)]{}; std::memcpy(bytes, &value, sizeof(value)); @@ -196,42 +350,152 @@ static bool ExtendedBits(long double value, std::uint64_t mantissa, std::uint16_ } static void CheckNanConversions() { - const struct { const char* text; std::size_t consumed; bool negative; } cases[] = { - {"nan(0x123)", 10, false}, {" -NaN(1)!", 8, true}, {"nan(0x800000)", 13, false}, - {"nan(0x10000000000000)", 21, false}, {"nan(0x8000000000000000)", 23, false}, + struct MeasuredNan { + const char* text; + std::uint64_t bits; + std::size_t used; + std::size_t sequenceUsed; + bool extendedMeasured; + bool wideDoubleMeasured; + bool wideOtherMeasured; + }; + static constexpr MeasuredNan cases[] = { + {"nan", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"NAN", 0x7ff8000000000000ull, 3, 3, false, false, false}, + {"nan()", 0x7ff8000000000000ull, 5, 5, false, false, false}, + {"nan(1)", 0x7ff8000000000001ull, 6, 6, true, true, true}, + {"nan(0x10)", 0x7ff8000000000010ull, 9, 9, false, false, false}, + {"nan(abc)", 0x7ff8000000000abcull, 8, 8, false, false, false}, + {"nan(", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"-nan", 0xfff8000000000000ull, 4, 4, true, true, true}, + {"+nan(5)", 0x7ff8000000000005ull, 7, 7, false, false, false}, + {"nan(10)", 0x7ff8000000000010ull, 7, 7, false, false, false}, + {"nan(xyz)", 0x7ff8000000000000ull, 8, 8, false, false, false}, + {"nan(_)", 0x7ff8000000000000ull, 6, 6, false, false, false}, + {"nan(12", 0x7ff8000000000012ull, 3, 3, false, false, false}, + {"-nan(5)", 0xfff8000000000005ull, 7, 7, false, false, false}, + {"nan(0x8000000000000)", 0x7ff8000000000000ull, 20, 20, false, false, false}, + {"nan(0x10000000000000)", 0x7ff8000000000001ull, 21, 21, true, true, false}, + {"nan(0x400000)", 0x7ff8000000400000ull, 13, 13, false, false, false}, + {"NaN(0X1F)", 0x7ff800000000001full, 9, 9, false, false, false}, + {"nan(0x)", 0x7ff8000000000000ull, 7, 7, false, false, false}, + {"nan(123abc)", 0x7ff8000000123abcull, 11, 11, true, true, false}, + {"nan(1x2)", 0x7ff8000000000000ull, 8, 8, true, true, false}, + {"nan(0x1g)", 0x7ff8000000000000ull, 9, 9, true, true, false}, + {"nan(g1)", 0x7ff8000000000000ull, 7, 7, true, true, false}, + {"nan(0x0x1)", 0x7ff8000000000000ull, 10, 10, true, true, false}, + {"nan(fffffffffffff)", 0x7fffffffffffffffull, 18, 18, true, true, false}, + {"nan(1fffffffffffff)", 0x7fffffffffffffffull, 19, 19, true, true, false}, + {"nan(10000000000000)", 0x7ff8000000000001ull, 19, 19, true, true, false}, + {"nan(20000000000000)", 0x7ff8000000000001ull, 19, 19, true, true, false}, + {"nan(0x20000000000000)", 0x7ff8000000000001ull, 21, 21, true, true, false}, + {"nan(ffffffffffffffff)", 0x7fffffffffffffffull, 21, 21, true, true, false}, + {"nan(10000000000000000)", 0x7ff8000000000001ull, 22, 22, true, true, false}, + {"nan(0X)", 0x7ff8000000000000ull, 7, 7, true, true, false}, + {"nan(-1)", 0x7ff8000000000000ull, 7, 3, true, true, false}, + {"nan(+1)", 0x7ff8000000000000ull, 7, 3, true, true, false}, + {"nan( 1)", 0x7ff8000000000001ull, 7, 3, true, true, false}, + {"nan(1 )", 0x7ff8000100000000ull, 7, 3, true, true, false}, + {"nan(0)", 0x7ff8000000000001ull, 6, 6, true, true, false}, + {"nan(00001)", 0x7ff8000000000001ull, 10, 10, true, true, false}, + {"nan(0x0001)", 0x7ff8000000000001ull, 11, 11, true, true, false}, + {"nan(8000000000000)", 0x7ff8000000000000ull, 18, 18, true, true, false}, + {"nan(7ffffffffffff)", 0x7fffffffffffffffull, 18, 18, true, true, false}, + {"nan(123", 0x7ff8000000000123ull, 3, 3, true, true, false}, + {"nan(1)x", 0x7ff8000000000001ull, 6, 6, true, true, false}, + {"-nan(1)", 0xfff8000000000001ull, 7, 7, true, true, false}, + {"nan(abcdefABCDEF)", 0x7ff8abcdefabcdefull, 17, 17, true, true, false}, + {"nan(0xABCDEF)", 0x7ff8000000abcdefull, 13, 13, true, true, false}, + {"nan(9)", 0x7ff8000000000009ull, 6, 6, true, true, false}, + {"nan(1_2)", 0x7ff8000000000000ull, 8, 8, true, true, false}, + {"nan(_1)", 0x7ff8000000000000ull, 7, 7, true, true, false}, + {"nan(1)(2)", 0x7ff8000000000001ull, 6, 6, true, true, false}, + {"+nan", 0x7ff8000000000000ull, 4, 4, true, true, true}, + {"NaN", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"-NAN(5)", 0xfff8000000000005ull, 7, 7, true, true, true}, + {"nan(5)", 0x7ff8000000000005ull, 6, 6, true, true, true}, + {"-nan(abc)", 0xfff8000000000abcull, 9, 9, true, true, true}, + {" -nan", 0xfff8000000000000ull, 6, 6, true, true, true}, + {"nan(12 x", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"nan(12x", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"nan(1", 0x7ff8000000000001ull, 3, 3, true, true, true}, + {"nan(a b)", 0x7ff8000a0000000bull, 8, 3, true, true, true}, + {"nan(ab)", 0x7ff80000000000abull, 7, 7, true, true, true}, + {"nanx", 0x7ff8000000000000ull, 3, 3, true, true, true}, + {"-nan()", 0xfff8000000000000ull, 6, 6, true, true, true}, + {"nan(0x8000000000001)", 0x7ff8000000000001ull, 20, 20, true, true, true}, }; bool correct = true; for (const auto& test : cases) { - const char* expectedEnd = test.text + test.consumed; - const std::uint64_t expectedDouble = test.negative ? 0xfff8000000000000ull : 0x7ff8000000000000ull; char* end = nullptr; const auto value = std::bit_cast(strtod_nid_postfix(test.text, &end)); - if (value != expectedDouble || end != expectedEnd) { + if (value != test.bits || end != test.text + test.used) { std::fprintf(stderr, "strtod(\"%s\") = 0x%016llx\n", test.text, static_cast(value)); correct = false; } - const auto ascii = std::bit_cast(atof_nid_postfix(test.text)); - if (ascii != expectedDouble) { - std::fprintf(stderr, "atof(\"%s\") = 0x%016llx\n", test.text, static_cast(ascii)); + if (std::bit_cast(atof_nid_postfix(test.text)) != test.bits) { + std::fprintf(stderr, "atof(\"%s\")\n", test.text); correct = false; } const auto single = std::bit_cast(strtof_nid_postfix(test.text, &end)); - if (single != (test.negative ? 0xffc00000u : 0x7fc00000u) || end != expectedEnd) { + if (single != 0x7fc00000u || end != test.text + test.sequenceUsed) { std::fprintf(stderr, "strtof(\"%s\") = 0x%08x\n", test.text, static_cast(single)); correct = false; } - const long double extended = strtold_nid_postfix(test.text, &end); - if (!ExtendedBits(extended, 0xc000000000000000ull, test.negative ? 0xffff : 0x7fff) || end != expectedEnd) { - std::uint64_t significand = 0; - std::uint16_t signExponent = 0; - std::memcpy(&significand, &extended, sizeof(significand)); - std::memcpy(&signExponent, reinterpret_cast(&extended) + sizeof(significand), sizeof(signExponent)); - std::fprintf(stderr, "strtold(\"%s\") = 0x%04x'%016llx\n", test.text, static_cast(signExponent), - static_cast(significand)); - correct = false; + if (test.extendedMeasured) { + const long double extended = strtold_nid_postfix(test.text, &end); + if (!ExtendedBits(extended, 0xc000000000000000ull, 0x7fff) || end != test.text + test.sequenceUsed) { + std::fprintf(stderr, "strtold(\"%s\")\n", test.text); + correct = false; + } + } + char16_t wide[64] = {}; + for (std::size_t i = 0; test.text[i] != 0; ++i) wide[i] = static_cast(test.text[i]); + char16_t* wideEnd = nullptr; + if (test.wideDoubleMeasured) { + const auto wideValue = std::bit_cast(wcstod_nid_postfix(wide, &wideEnd)); + if (wideValue != test.bits || wideEnd != wide + test.used) { + std::fprintf(stderr, "wcstod(\"%s\") = 0x%016llx\n", test.text, static_cast(wideValue)); + correct = false; + } + } + if (test.wideOtherMeasured) { + const auto wideSingle = std::bit_cast(wcstof_nid_postfix(wide, &wideEnd)); + const bool singleOk = wideSingle == 0x7fc00000u && wideEnd == wide + test.sequenceUsed; + const long double wideExtended = wcstold_nid_postfix(wide, &wideEnd); + if (!singleOk || !ExtendedBits(wideExtended, 0xc000000000000000ull, 0x7fff) || wideEnd != wide + test.sequenceUsed) { + std::fprintf(stderr, "wcstof/wcstold(\"%s\")\n", test.text); + correct = false; + } } } Require(correct); + for (const char* text : {"nan(1 ", "nan(12 \t"}) { + bool threw = false; + try { strtod_nid_postfix(text, nullptr); } catch (...) { threw = true; } + Require(threw); + } + struct PlainNan { const char* text; std::uint64_t bits; std::size_t used; }; + for (const PlainNan& plain : {PlainNan{"nan(1 2)", 0x7ff8000100000002ull, 8}, PlainNan{"nan( \0)", 0x7ff8000000000000ull, 3}}) { + char* end = nullptr; + Require(std::bit_cast(strtod_nid_postfix(plain.text, &end)) == plain.bits && end == plain.text + plain.used); + } +} + +static void CheckFloatUnderflowErrno() { + char* end = nullptr; + errno = 7; + Require(strtod_nid_postfix("1e-310", &end) == 1e-310 && errno == 7); + Require(strtod_nid_postfix("-4.9406564584124654e-324", &end) == -0x1p-1074 && errno == 7); + Require(strtof_nid_postfix("1e-40", &end) == 1e-40f && errno == 7); + Require(strtold_nid_postfix("1e-4940", &end) == 1e-4940L && errno == 7); + Require(strtod_nid_postfix("1e-330", &end) == 0.0 && errno == ERANGE); + errno = 0; + Require(strtof_nid_postfix("1e-46", &end) == 0.0f && errno == ERANGE); + errno = 0; + Require(strtod_nid_postfix("1e400", &end) == HUGE_VAL && errno == ERANGE); + errno = 0; + Require(strtod_nid_postfix("2.2250738585072014e-308", &end) == 0x1p-1022 && errno == 0); } static void CheckHypotSpecialValue() { @@ -290,6 +554,10 @@ static void CheckLongDouble() { Require(frexpl_nid_postfix(INFINITY, &exponent) == INFINITY && exponent == 77); Require(frexpl_nid_postfix(-INFINITY, &exponent) == -INFINITY && exponent == 77); Require(std::isnan(frexpl_nid_postfix(NAN, &exponent)) && exponent == 77); + Require(ceill_nid_postfix(2.2L) == 3.0L && ceill_nid_postfix(-2.7L) == -2.0L); + Require(floorl_nid_postfix(2.7L) == 2.0L && floorl_nid_postfix(-2.2L) == -3.0L); + Require(ceill_nid_postfix(2.0L) == 2.0L && floorl_nid_postfix(-2.0L) == -2.0L); + Require(std::isnan(ceill_nid_postfix(NAN)) && std::isnan(floorl_nid_postfix(NAN))); } static void CheckNearbyintl() { @@ -394,9 +662,9 @@ static void CheckIntegerConversions() { {" -0B1!", 0, 0, 3, 0}, {"0b2", 0, 0, 1, 0}, {"0b101", 16, 0xb101, 5, 0}, - {" -0xz!", 0, 0, 3, 0}, - {"0xg", 16, 0, 1, 0}, - {"0x", 16, 0, 1, 0}, + {" -0xz!", 0, 0, 0, 0}, + {"0xg", 16, 0, 0, 0}, + {"0x", 16, 0, 0, 0}, {"z!", 36, 35, 1, 0}, {"", 10, 0, 0, 0}, {" \t+!", 10, 0, 0, 0}, @@ -442,8 +710,8 @@ static void CheckIntegerConversions() { {"0B11", 2, 0, 1, 0}, {" +0b1!", 2, 0, 3, 0}, {"0b101", 16, 0xb101, 5, 0}, - {" +0Xg!", 0, 0, 3, 0}, - {"0x!", 16, 0, 1, 0}, + {" +0Xg!", 0, 0, 0, 0}, + {"0x!", 16, 0, 0, 0}, {"z!", 36, 35, 1, 0}, {"", 10, 0, 0, 0}, {" \t-!", 10, 0, 0, 0}, @@ -481,6 +749,26 @@ static void CheckIntegerConversions() { Require(*__error_nid_postfix() == 13); Require(_Stoul_nid_postfix("18446744073709551615", nullptr, 10) == UINT64_MAX); Require(_Stoul_nid_postfix("-1", nullptr, 10) == UINT64_MAX && *__error_nid_postfix() == 13); + for (const int base : {-1, 1, 37}) { + const char text[] = " 12"; + const auto rejects = [&](auto parse, int error) { + char* end = nullptr; + *__error_nid_postfix() = 0; + Require(parse(text, &end, base) == 0 && end == text && *__error_nid_postfix() == error); + }; + rejects(strtol_nid_postfix, EINVAL); + rejects(strtoul_nid_postfix, EINVAL); + rejects(strtoll_nid_postfix, EINVAL); + rejects(strtoull_nid_postfix, EINVAL); + rejects(strtoimax_nid_postfix, 0); + rejects(strtoumax_nid_postfix, 0); + rejects(_Stoull_nid_postfix, 0); + rejects(_Stoul_nid_postfix, 0); + } + *__error_nid_postfix() = 0; + Require(atoi_nid_postfix("9223372036854775808") == 0 && atoi_nid_postfix("4294967297") == 1 && atoi_nid_postfix("-9223372036854775809") == -1); + Require(atol_nid_postfix("9223372036854775808") == INT64_MIN && atoll_nid_postfix("-9223372036854775809") == INT64_MAX && *__error_nid_postfix() == 0); + Require(atol_nid_postfix("99999999999999999999") == -1 && *__error_nid_postfix() == ERANGE); *__error_nid_postfix() = 0; } @@ -552,6 +840,14 @@ static void CheckExtendedMath() { static void CheckHyperbolic() { const double infinity = std::numeric_limits::infinity(); + Require(_Cosh_nid_postfix(0.0, 2.0) == 2.0); + Require(Near(_Cosh_nid_postfix(1.0, 2.0), 3.0861612696304874, 1e-15)); + Require(Near(_Cosh_nid_postfix(-1.0, -2.0), -3.0861612696304874, 1e-15)); + Require(std::isinf(_Cosh_nid_postfix(infinity, -1.0)) && std::signbit(_Cosh_nid_postfix(infinity, -1.0))); + Require(std::isnan(_Cosh_nid_postfix(infinity, 0.0))); + Require(Near(_Cosh_nid_postfix(711.0, 1e-300), 0x1.2190c08e2f1a4p+28, 1e-15)); + Require(_Cosh_nid_postfix(720.0, 0.0) == 0.0 && !std::signbit(_Cosh_nid_postfix(720.0, 0.0))); + Require(_Cosh_nid_postfix(720.0, -0.0) == 0.0 && std::signbit(_Cosh_nid_postfix(720.0, -0.0))); Require(sinh_nid_postfix(0.) == 0. && std::signbit(sinh_nid_postfix(-0.))); Require(cosh_nid_postfix(0.) == 1. && coshf_nid_postfix(0.f) == 1.f); Require(asinh_nid_postfix(0.) == 0. && std::signbit(asinh_nid_postfix(-0.)) && std::signbit(asinhf_nid_postfix(-0.f))); @@ -572,6 +868,98 @@ static void CheckHyperbolic() { Require(Near(acosh_nid_postfix(1e300), 691.4686750787736, 1e-15)); } +static void CheckScaledHyperbolic() { + const int rounding = std::fegetround(); + Require(std::fesetround(FE_TONEAREST) == 0); + unsigned checks = 0; + for (bool sine : {true, false}) { + const auto function = sine ? _FSinh_nid_postfix : _FCosh_nid_postfix; + const char* name = sine ? "_FSinh" : "_FCosh"; + const auto checkValue = [&](float x, float y) { + const long double value = sine ? std::sinh(static_cast(x)) : std::cosh(static_cast(x)); + const float expected = static_cast(value * static_cast(y)); + std::feclearexcept(FE_ALL_EXCEPT); + errno = 12345; + const float result = function(x, y); + const auto actualBits = std::bit_cast(result); + const auto expectedBits = std::bit_cast(expected); + const auto distance = actualBits > expectedBits ? actualBits - expectedBits : expectedBits - actualBits; + const bool exact = expected == 0.0f || std::isinf(expected); + if ((exact ? actualBits != expectedBits : !std::isfinite(result) || distance > 2) || + (std::isfinite(expected) && (std::fetestexcept(FE_OVERFLOW | FE_INVALID) != 0 || errno != 12345))) { + std::fprintf(stderr, "%s(%a, %a): got %08x, expected %08x, errno %d, flags %x\n", + name, static_cast(x), static_cast(y), actualBits, expectedBits, errno, std::fetestexcept(FE_ALL_EXCEPT)); + std::abort(); + } + ++checks; + }; + checkValue(90.0f, 1e-20f); + checkValue(100.0f, 1e-40f); + const float magnitudes[] = {0.0f, 0x1p-126f, 0.001f, 0.5f, 1.0f, 4.0f, 7.0f, 8.0f, 10.0f, 20.0f, 50.0f, 88.0f, 90.0f, 100.0f, 150.0f, 180.0f, 190.0f, 193.0f}; + const float scales[] = {0.0f, 0x1p-149f, 0x1p-140f, 0x1p-126f, 1e-20f, 0.5f, 1.0f, 1e20f, std::numeric_limits::max()}; + for (float magnitude : magnitudes) { + for (float scale : scales) { + for (float xSign : {1.0f, -1.0f}) { + for (float ySign : {1.0f, -1.0f}) checkValue(magnitude * xSign, scale * ySign); + } + } + } + for (float scale : scales) { + if (scale == 0.0f) continue; + for (long double target : {static_cast(std::numeric_limits::max()), 0x1p-150L}) { + if (!sine && target < scale) continue; + const long double ratio = target / static_cast(scale); + const float boundary = static_cast(sine ? std::asinh(ratio) : std::acosh(ratio)); + float magnitude = std::nextafter(std::nextafter(boundary, -INFINITY), -INFINITY); + for (unsigned neighbor = 0; neighbor != 5; ++neighbor) { + for (float xSign : {1.0f, -1.0f}) { + for (float ySign : {1.0f, -1.0f}) checkValue(magnitude * xSign, scale * ySign); + } + magnitude = std::nextafter(magnitude, INFINITY); + } + } + } + const auto checkSpecial = [&](float x, float y, std::uint32_t expectedBits, int error, int flags) { + std::feclearexcept(FE_ALL_EXCEPT); + std::feraiseexcept(FE_DIVBYZERO); + errno = 12345; + const float result = function(x, y); + const int raised = std::fetestexcept(FE_ALL_EXCEPT); + if (std::bit_cast(result) != expectedBits || errno != error || raised != (FE_DIVBYZERO | flags)) { + std::fprintf(stderr, "%s special (%a, %a): got %08x, expected %08x, errno %d, flags %x\n", + name, static_cast(x), static_cast(y), std::bit_cast(result), expectedBits, errno, raised); + std::abort(); + } + ++checks; + }; + const float infinity = std::numeric_limits::infinity(); + for (float sign : {1.0f, -1.0f}) { + checkSpecial(sign * infinity, 0.0f, 0x7fc00000u, EDOM, FE_INVALID); + checkSpecial(sign * 100.0f, 0.0f, sine && sign < 0 ? 0x80000000u : 0u, 12345, 0); + checkSpecial(sign * 100.0f, -0.0f, sine && sign < 0 ? 0u : 0x80000000u, 12345, 0); + checkSpecial(sign * 1.0f, infinity, sine && sign < 0 ? 0xff800000u : 0x7f800000u, 12345, 0); + } + for (std::uint32_t bits : {0x7fc12345u, 0xffc54321u, 0x7fa12345u, 0xffa54321u}) { + const float nan = std::bit_cast(bits); + checkSpecial(nan, 0.0f, bits, 12345, 0); + checkSpecial(infinity, nan, bits, 12345, 0); + checkSpecial(nan, std::bit_cast(0x7fc99999u), bits, 12345, 0); + } + checkSpecial(0.0f, infinity, sine ? 0x7fc00000u : 0x7f800000u, sine ? EDOM : 12345, sine ? FE_INVALID : 0); + checkSpecial(1.0f, std::numeric_limits::max(), 0x7f800000u, 12345, FE_OVERFLOW | FE_INEXACT); + checkSpecial(100.0f, 1.0f, 0x7f800000u, ERANGE, FE_OVERFLOW | FE_INEXACT); + for (int mode : {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}) { + Require(std::fesetround(mode) == 0); + checkSpecial(std::numeric_limits::max(), 0x1p-149f, 0x7f800000u, ERANGE, FE_OVERFLOW | FE_INEXACT); + } + Require(std::fesetround(FE_TONEAREST) == 0); + if (sine) checkSpecial(0x1p-126f, 0x1p-149f, 0u, 12345, FE_UNDERFLOW | FE_INEXACT); + } + std::feclearexcept(FE_ALL_EXCEPT); + Require(std::fesetround(rounding) == 0); + std::printf("Scaled hyperbolic checks: %u\n", checks); +} + static void CheckSinhf() { const float infinity = std::numeric_limits::infinity(); const struct { std::uint32_t input; std::uint32_t output; } cases[] = { @@ -662,6 +1050,36 @@ static void CheckFloatClassification() { Require(_FDtest_nid_postfix(reinterpret_cast(&_FNan_nid_postfix)) == 2); } +static void RequireBytes(const void* constant, std::initializer_list expected) { + unsigned char bytes[16]; + std::memcpy(bytes, constant, sizeof(bytes)); + int index = 0; + for (const unsigned char byte : expected) Require(bytes[index++] == byte); + while (index < 16) Require(bytes[index++] == 0); +} + +static void CheckDinkumwareConstants() { + RequireBytes(&_Inf_nid_postfix, {0, 0, 0, 0, 0, 0, 0xf0, 0x7f}); + RequireBytes(&_Denorm_nid_postfix, {1}); + RequireBytes(&_Hugeval_nid_postfix, {0, 0, 0, 0, 0, 0, 0xf0, 0x7f}); + RequireBytes(&_Nan_nid_postfix, {0, 0, 0, 0, 0, 0, 0xf8, 0x7f}); + RequireBytes(&_Snan_nid_postfix, {1, 0, 0, 0, 0, 0, 0xf0, 0x7f}); + RequireBytes(&_FDenorm_nid_postfix, {1}); + RequireBytes(&_FSnan_nid_postfix, {1, 0, 0x80, 0x7f}); + RequireBytes(&_LDenorm_nid_postfix, {1}); + RequireBytes(&_LInf_nid_postfix, {0, 0, 0, 0, 0, 0, 0, 0x80, 0xff, 0x7f}); + RequireBytes(&_LNan_nid_postfix, {0, 0, 0, 0, 0, 0, 0, 0xc0, 0xff, 0x7f}); + RequireBytes(&_LSnan_nid_postfix, {1, 0, 0, 0, 0, 0, 0, 0x80, 0xff, 0x7f}); + Require(_Dtest_nid_postfix(&_Denorm_nid_postfix._Double) == -2 && _Denorm_nid_postfix._Double == std::numeric_limits::denorm_min()); + Require(_Dtest_nid_postfix(&_Hugeval_nid_postfix._Double) == 1 && _Hugeval_nid_postfix._Double == HUGE_VAL); + Require(_Dtest_nid_postfix(&_Nan_nid_postfix._Double) == 2 && _Dtest_nid_postfix(&_Snan_nid_postfix._Double) == 2); + Require(_FDtest_nid_postfix(reinterpret_cast(&_FDenorm_nid_postfix)) == -2); + Require(_FDtest_nid_postfix(reinterpret_cast(&_FSnan_nid_postfix)) == 2); + Require(_LDenorm_nid_postfix._Long_double == std::numeric_limits::denorm_min()); + Require(_LInf_nid_postfix._Long_double == std::numeric_limits::infinity()); + Require(std::isnan(_LNan_nid_postfix._Long_double) && std::isnan(_LSnan_nid_postfix._Long_double)); +} + static void CheckMissingClassificationExports() { const float infinity = std::numeric_limits::infinity(); const float nan = std::numeric_limits::quiet_NaN(); @@ -697,6 +1115,76 @@ 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()))); + Require(significand_nid_postfix(12.0) == 1.5 && significand_nid_postfix(-0.375) == -1.5); + Require(significand_nid_postfix(0x1p-1074) == 1.0); + Require(std::signbit(significand_nid_postfix(-0.0)) && significand_nid_postfix(-0.0) == 0.0); + Require(significand_nid_postfix(std::numeric_limits::infinity()) == std::numeric_limits::infinity()); + Require(std::isnan(significand_nid_postfix(std::numeric_limits::quiet_NaN()))); +} + +static bool Near(long double actual, long double expected) { + 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(tgammaf_nid_postfix(5.0f) == 24.0f && std::abs(tgammaf_nid_postfix(0.5f) - 1.77245385f) < 1e-6f); + Require(tgammaf_nid_postfix(1.0f) == 1.0f && tgammaf_nid_postfix(2.0f) == 1.0f && tgammaf_nid_postfix(3.0f) == 2.0f); + Require(std::isinf(tgammaf_nid_postfix(0.0f)) && std::isnan(tgammaf_nid_postfix(-1.0f))); + Require(hypotl_nid_postfix(3.0L, -4.0L) == 5.0L && hypotl_nid_postfix(nan, -infinity) == infinity); + + Require(hypot3f_nid_postfix(3.0f, -4.0f, 12.0f) == 13.0f && hypot3f_nid_postfix(0.0f, 3.0f, -4.0f) == 5.0f); + 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(-0.048383776468198)) < 1e-6f); + Require(std::fabs(jnf_nid_postfix(3, 4.0f) - static_cast(0.430171473875622)) < 1e-6f); + Require(y0f_nid_postfix(0.0f) == -std::numeric_limits::infinity() && y1f_nid_postfix(0.0f) == -std::numeric_limits::infinity()); + Require(ynf_nid_postfix(2, 0.0f) == -std::numeric_limits::infinity()); + Require(std::fabs(ynf_nid_postfix(2, 3.0f) - static_cast(-0.160400393484924)) < 1e-6f); + Require(std::isnan(y0f_nid_postfix(-1.0f))); +} + static void CheckDoubleClassification() { const struct { std::uint64_t bits; short code; } cases[] = { {0x0000000000000000ull, 0}, {0x8000000000000000ull, 0}, @@ -788,8 +1276,35 @@ static void CheckNoConversionIsPositiveZero() { Require(parsedWideLong == 0.L && !std::signbit(parsedWideLong) && wideEnd == wide); } +template +static void RequireCharacteristic(const unsigned char (&constant)[16], TValue expected) { + TValue value; + std::memcpy(&value, constant, sizeof(value)); + Require(value == expected); + for (std::size_t index = TBytes; index < 16; ++index) Require(constant[index] == 0); +} + +template +static void RequireCharacteristics(const DinkumwareFloatCharacteristics& table, const int (&integers)[6]) { + const int fields[] = {table.digits, table.mantissaDigits, table.maxExponent10, table.maxExponent, table.minExponent10, table.minExponent}; + for (int index = 0; index < 6; ++index) Require(fields[index] == integers[index]); + RequireCharacteristic(table.epsilon, std::numeric_limits::epsilon()); + RequireCharacteristic(table.max, std::numeric_limits::max()); + RequireCharacteristic(table.min, std::numeric_limits::min()); +} + +static void CheckDinkumwareFloatCharacteristics() { + static_assert(sizeof(DinkumwareFloatCharacteristics) == 80 && offsetof(DinkumwareFloatCharacteristics, epsilon) == 32); + RequireCharacteristics(_Flt_nid_postfix, {FLT_DIG, FLT_MANT_DIG, FLT_MAX_10_EXP, FLT_MAX_EXP, FLT_MIN_10_EXP, FLT_MIN_EXP}); + RequireCharacteristics(_Dbl_nid_postfix, {DBL_DIG, DBL_MANT_DIG, DBL_MAX_10_EXP, DBL_MAX_EXP, DBL_MIN_10_EXP, DBL_MIN_EXP}); + RequireCharacteristics(_Ldbl_nid_postfix, {18, 64, 4932, 16384, -4931, -16381}); + for (const auto* table : {&_Flt_nid_postfix, &_Dbl_nid_postfix, &_Ldbl_nid_postfix}) + for (std::size_t index = 24; index < 32; ++index) Require(reinterpret_cast(table)[index] == 0); +} + int main() { CheckFloatClassification(); + CheckDinkumwareConstants(); CheckNoConversionIsPositiveZero(); CheckMissingClassificationExports(); CheckFloatMathExports(); @@ -810,6 +1325,12 @@ int main() { Require(fmodf_nid_postfix(-5.5f, 2.f) == -1.5f); Require(std::signbit(fmodf_nid_postfix(-4.f, 2.f))); Require(std::isnan(fmodf_nid_postfix(1.f, 0.f))); + int quo = 0; + Require(remquo_nid_postfix(7.0, 3.0, &quo) == 1.0 && (quo & 7) == 2); + Require(remquo_nid_postfix(-7.0, 3.0, &quo) == -1.0 && (quo & 7) == ((-2) & 7)); + int quof = 0; + Require(remquof_nid_postfix(7.0f, 3.0f, &quof) == 1.0f && (quof & 7) == 2); + Require(remquof_nid_postfix(-7.0f, 3.0f, &quof) == -1.0f && (quof & 7) == ((-2) & 7)); Require(std::bit_cast(nanf_nid_postfix(nullptr)) == 0x7fc00000u); Require(std::bit_cast(nanf_nid_postfix("")) == 0x7fc00000u); Require(std::bit_cast(nanf_nid_postfix("123")) == 0x7fc00000u); @@ -835,6 +1356,16 @@ int main() { try { rand_r_nid_postfix(nullptr); } catch (...) { threw = true; } Require(threw); Require(hypot_nid_postfix(3.0, 4.0) == 5.0 && hypot_nid_postfix(-3.0, -4.0) == 5.0 && hypotf_nid_postfix(3.f, -4.f) == 5.f); + Require(copysign_nid_postfix(1.0, 2.0) == 1.0 && copysign_nid_postfix(1.0, -2.0) == -1.0); + Require(copysign_nid_postfix(-1.0, 2.0) == 1.0 && copysign_nid_postfix(-1.0, -2.0) == -1.0); + Require(std::bit_cast(copysign_nid_postfix(0.0, -1.0)) == 0x8000000000000000ull); + Require(std::bit_cast(copysign_nid_postfix(-0.0, 1.0)) == 0); + Require(copysign_nid_postfix(INFINITY, -1.0) == -INFINITY && copysign_nid_postfix(-INFINITY, 1.0) == INFINITY); + Require(std::isnan(copysign_nid_postfix(std::numeric_limits::quiet_NaN(), -1.0)) && std::signbit(copysign_nid_postfix(std::numeric_limits::quiet_NaN(), -1.0))); + Require(std::isnan(copysign_nid_postfix(std::numeric_limits::quiet_NaN(), 1.0)) && !std::signbit(copysign_nid_postfix(std::numeric_limits::quiet_NaN(), 1.0))); + Require(copysignf_nid_postfix(1.0f, 2.0f) == 1.0f && copysignf_nid_postfix(1.0f, -2.0f) == -1.0f); + Require(std::bit_cast(copysignf_nid_postfix(0.0f, -1.0f)) == 0x80000000u); + Require(std::bit_cast(copysignf_nid_postfix(-0.0f, 1.0f)) == 0); Require(std::abs(hypot_nid_postfix(1e308, 1e308) / 1.4142135623730951e308 - 1.0) < 1e-15); Require(std::abs(hypotf_nid_postfix(2e38f, 2e38f) / 2.8284271e38f - 1.f) < 1e-6f); Require(std::isinf(hypot_nid_postfix(std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()))); @@ -864,9 +1395,32 @@ int main() { Require(std::abs(expm1f_nid_postfix(1.f) - 1.7182818f) < 1e-6f && std::isinf(expm1f_nid_postfix(std::numeric_limits::infinity()))); Require(erff_nid_postfix(0.f) == 0.f && std::signbit(erff_nid_postfix(-0.f)) && erff_nid_postfix(10.f) == 1.f && erff_nid_postfix(-10.f) == -1.f); Require(std::abs(erff_nid_postfix(0.5f) - 0.5204999f) < 1e-6f && std::isnan(erff_nid_postfix(std::numeric_limits::quiet_NaN()))); + Require(erf_nid_postfix(0.) == 0. && std::signbit(erf_nid_postfix(-0.)) && erf_nid_postfix(10.) == 1. && erf_nid_postfix(-10.) == -1.); + Require(std::abs(erf_nid_postfix(0.5) - 0.5204998778130465) < 1e-15 && std::isnan(erf_nid_postfix(std::numeric_limits::quiet_NaN()))); + Require(erfc_nid_postfix(0.) == 1. && erfc_nid_postfix(100.) == 0. && erfc_nid_postfix(-100.) == 2.); + Require(std::abs(erfc_nid_postfix(0.5) - 0.47950012218695346) < 1e-15 && std::isnan(erfc_nid_postfix(std::numeric_limits::quiet_NaN()))); + Require(erfcf_nid_postfix(0.f) == 1.f && erfcf_nid_postfix(100.f) == 0.f && erfcf_nid_postfix(-100.f) == 2.f); + Require(std::abs(erfcf_nid_postfix(0.5f) - 0.47950012f) < 1e-6f && std::isnan(erfcf_nid_postfix(std::numeric_limits::quiet_NaN()))); Require(lgamma_nid_postfix(1.) == 0. && lgamma_nid_postfix(2.) == 0. && std::isinf(lgamma_nid_postfix(0.))); Require(std::abs(lgamma_nid_postfix(0.5) - 0.5723649429247001) < 1e-15 && std::abs(lgamma_nid_postfix(10.) - 12.801827480081469) < 1e-13); Require(std::abs(lgamma_nid_postfix(-0.5) - 1.2655121234846454) < 1e-15); + for (const double sign : {1.0, -1.0}) { + std::feclearexcept(FE_ALL_EXCEPT); + Require(std::abs(lgamma_nid_postfix(sign * std::numeric_limits::denorm_min()) - 744.44007192138122) < 1e-12); + Require(std::abs(lgamma_nid_postfix(sign * 0x1p-1025) - 710.47586007394398) < 1e-12); + Require(std::abs(lgamma_nid_postfix(sign * 0x1p-1022) - 708.39641853226408) < 1e-12); + Require(std::abs(lgamma_nid_postfix(sign * 0x1p-57) - 39.509389291916882) < 1e-13); + Require(std::fetestexcept(FE_OVERFLOW | FE_DIVBYZERO | FE_INVALID) == 0); + } + const auto gammaSign = [](double x) { + signgam_nid_postfix = 7; + lgamma_nid_postfix(x); + return signgam_nid_postfix; + }; + Require(gammaSign(0.5) == 1 && gammaSign(10.) == 1 && gammaSign(0.) == 1 && gammaSign(-0.) == -1); + Require(gammaSign(-0.5) == -1 && gammaSign(-1.5) == 1 && gammaSign(-2.5) == -1 && gammaSign(-0x1p-80) == -1); + Require(gammaSign(-1.) == 1 && gammaSign(-2.) == 1 && gammaSign(-0x1p60) == 1 && gammaSign(-4503599627370495.5) == 1); + Require(gammaSign(INFINITY) == 1 && gammaSign(-INFINITY) == 1 && gammaSign(NAN) == 1); Require(llroundl_nid_postfix(2.5L) == 3 && llroundl_nid_postfix(-2.5L) == -3 && llroundl_nid_postfix(0.49999999999999999L) == 0); Require(llroundl_nid_postfix(9007199254740993.0L) == 9007199254740993LL && llroundl_nid_postfix(-1e30L) == INT64_MIN); for (const int mode : {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}) { @@ -889,12 +1443,25 @@ int main() { Require(logbf_nid_postfix(0.f) == -std::numeric_limits::infinity()); Require(logbf_nid_postfix(-std::numeric_limits::infinity()) == std::numeric_limits::infinity()); Require(std::isnan(logbf_nid_postfix(std::numeric_limits::quiet_NaN()))); + Require(ilogb_nid_postfix(8.0) == 3 && ilogb_nid_postfix(-0.75) == -1 && ilogb_nid_postfix(1.0) == 0); + Require(ilogb_nid_postfix(1024.0) == 10 && ilogb_nid_postfix(0.125) == -3); + Require(ilogbf_nid_postfix(8.f) == 3 && ilogbf_nid_postfix(-0.75f) == -1 && ilogbf_nid_postfix(1.f) == 0); + Require(ilogbf_nid_postfix(1024.f) == 10 && ilogbf_nid_postfix(0.125f) == -3); + Require(ilogb_nid_postfix(0.0) == INT_MIN && ilogb_nid_postfix(-0.0) == INT_MIN); + Require(ilogb_nid_postfix(std::numeric_limits::infinity()) == INT_MAX); + Require(ilogb_nid_postfix(-std::numeric_limits::infinity()) == INT_MAX); + Require(ilogb_nid_postfix(std::numeric_limits::quiet_NaN()) == INT_MAX); + Require(ilogbf_nid_postfix(0.f) == INT_MIN && ilogbf_nid_postfix(-0.f) == INT_MIN); + Require(ilogbf_nid_postfix(std::numeric_limits::infinity()) == INT_MAX); + Require(ilogbf_nid_postfix(std::numeric_limits::quiet_NaN()) == INT_MAX); + Require(ilogbf_nid_postfix(std::numeric_limits::denorm_min()) == -149); Require(exp2_nid_postfix(-3.) == 0.125); Require(ldexp_nid_postfix(0.75, 4) == 12.); CheckSquareRoot(); CheckExp2Errno(); CheckExactOperations(); CheckNearbyint(); + CheckFegetenv(); CheckLongDouble(); CheckNearbyintl(); CheckAsinl(); @@ -907,6 +1474,13 @@ int main() { Require(nextafterf_nid_postfix(1.f, 1.f) == 1.f && !std::signbit(nextafterf_nid_postfix(-0.f, 0.f))); Require(nextafterf_nid_postfix(std::numeric_limits::max(), std::numeric_limits::infinity()) == std::numeric_limits::infinity()); Require(std::isnan(nextafterf_nid_postfix(std::numeric_limits::quiet_NaN(), 1.f)) && std::isnan(nextafterf_nid_postfix(1.f, std::numeric_limits::quiet_NaN()))); + Require(nextafter_nid_postfix(1.0, 2.0) == 0x1.0000000000001p0 && nextafter_nid_postfix(1.0, 0.0) == 0x1.fffffffffffffp-1); + Require(nextafter_nid_postfix(0.0, 1.0) == std::numeric_limits::denorm_min()); + Require(nextafter_nid_postfix(0.0, -1.0) == -std::numeric_limits::denorm_min()); + Require(nextafter_nid_postfix(std::numeric_limits::denorm_min(), -1.0) == 0.0); + Require(nextafter_nid_postfix(1.0, 1.0) == 1.0 && !std::signbit(nextafter_nid_postfix(-0.0, 0.0))); + Require(nextafter_nid_postfix(std::numeric_limits::max(), std::numeric_limits::infinity()) == std::numeric_limits::infinity()); + Require(std::isnan(nextafter_nid_postfix(std::numeric_limits::quiet_NaN(), 1.0)) && std::isnan(nextafter_nid_postfix(1.0, std::numeric_limits::quiet_NaN()))); int exponent = 0; Require(frexp_nid_postfix(12., &exponent) == 0.75 && exponent == 4); Require(frexpf_nid_postfix(-12.f, &exponent) == -0.75f && exponent == 4); @@ -945,6 +1519,21 @@ int main() { Require(__isinf_nid_postfix(0.) == 0 && __isinf_nid_postfix(1e308 * 10) == 1); Require(__isfinitef_nid_postfix(0.f) == 1 && __isfinitef_nid_postfix(infinity) == 0); Require(__isfinitef_nid_postfix(nan) == 0); + std::feclearexcept(FE_ALL_EXCEPT); + std::feraiseexcept(FE_DIVBYZERO); + for (const long double value : {0.0L, -0.0L, 1.0L, -1.0L, std::numeric_limits::denorm_min(), + -std::numeric_limits::denorm_min(), std::numeric_limits::min(), + std::numeric_limits::max(), std::numeric_limits::lowest(), + 0x1p1024L, -0x1p1024L, 0x1p-16382L}) { + Require(__isfinitel_nid_postfix(value) == 1); + } + for (const long double value : {std::numeric_limits::infinity(), -std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN(), -std::numeric_limits::quiet_NaN(), + std::numeric_limits::signaling_NaN(), -std::numeric_limits::signaling_NaN()}) { + Require(__isfinitel_nid_postfix(value) == 0); + } + Require(std::fetestexcept(FE_ALL_EXCEPT) == FE_DIVBYZERO); + std::feclearexcept(FE_ALL_EXCEPT); Require(__isnormalf_nid_postfix(1.f) == 1 && __isnormalf_nid_postfix(0.f) == 0); Require(__isnormalf_nid_postfix(std::numeric_limits::denorm_min()) == 0); Require(__isnormal_nid_postfix(1.) == 1 && __isnormal_nid_postfix(0.) == 0); @@ -997,9 +1586,15 @@ int main() { Require(std::isinf(__powidf2_nid_postfix(-0.0, -3)) && std::signbit(__powidf2_nid_postfix(-0.0, -3))); Require(__powidf2_nid_postfix(2.0, -127) == 0x1p-127); CheckHyperbolic(); + CheckScaledHyperbolic(); CheckSinhf(); CheckAtanhf(); CheckHypotSpecialValue(); + CheckFloatUnderflowErrno(); CheckFmodErrno(); CheckDoubleClassification(); + CheckIntegerAbsAndDivision(); + CheckScaling(); + CheckLongDoubleFunctions(); + CheckDinkumwareFloatCharacteristics(); } diff --git a/core/libs/tests/GuestMathExact.cpp b/core/libs/tests/GuestMathExact.cpp new file mode 100644 index 0000000000..e601e88973 --- /dev/null +++ b/core/libs/tests/GuestMathExact.cpp @@ -0,0 +1,142 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +long double APS5_VABI fdiml_nid_postfix(long double, long double); +long double APS5_VABI fmodl_nid_postfix(long double, long double); +long double APS5_VABI remquol_nid_postfix(long double, long double, int*); +int APS5_VABI ilogbl_nid_postfix(long double); +long double APS5_VABI roundl_nid_postfix(long double); +long double APS5_VABI modfl_nid_postfix(long double, long double*); +long double APS5_VABI nanl_nid_postfix(const char*); +long double APS5_VABI scalblnl_nid_postfix(long double, std::int64_t); +long double APS5_VABI fmal_nid_postfix(long double, long double, long double); +double APS5_VABI scalb_nid_postfix(double, double); +double APS5_VABI scalbln_nid_postfix(double, std::int64_t); +long double APS5_VABI remainderl_nid_postfix(long double, long double); +double APS5_VABI drem_nid_postfix(double, double); +float APS5_VABI dremf_nid_postfix(float, float); +std::int64_t APS5_VABI lrint_nid_postfix(double); +double APS5_VABI rint_nid_postfix(double); +float APS5_VABI rintf_nid_postfix(float); +long double APS5_VABI rintl_nid_postfix(long double); +long long APS5_VABI llrint_nid_postfix(double); +int APS5_VABI finite_nid_postfix(double); +int APS5_VABI __fpclassifyl_nid_postfix(long double); +int APS5_VABI __isnanl_nid_postfix(long double); +int APS5_VABI __isnormall_nid_postfix(long double); +short APS5_VABI _LDtest_nid_postfix(const long double*); +int APS5_VABI _Dclass_nid_postfix(double); +short APS5_VABI _LDsign_nid_postfix(long double); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static long double Extended(std::uint16_t signExponent, std::uint64_t mantissa) { + long double value = 0; + unsigned char bytes[sizeof(long double)] = {}; + std::memcpy(bytes, &mantissa, sizeof(mantissa)); + std::memcpy(bytes + 8, &signExponent, sizeof(signExponent)); + std::memcpy(&value, bytes, sizeof(bytes)); + return value; +} + +static bool SameBits(long double left, long double right) { return std::memcmp(&left, &right, 10) == 0; } + +static void CheckSignalingNanPassThrough() { + const long double signaling = Extended(0x7fff, 0x8000000000001234ull); + Require(SameBits(fdiml_nid_postfix(signaling, 1.0L), signaling)); + Require(SameBits(fdiml_nid_postfix(1.0L, signaling), signaling)); + Require(SameBits(roundl_nid_postfix(signaling), signaling)); + long double integral = 0; + Require(SameBits(modfl_nid_postfix(signaling, &integral), signaling) && SameBits(integral, signaling)); + Require(SameBits(fmal_nid_postfix(1.0L, 1.0L, signaling), signaling)); + Require(SameBits(nanl_nid_postfix("123"), Extended(0x7fff, 0xc000000000000000ull))); +} + +static void CheckErrno() { + errno = 0; + Require(std::isnan(fmodl_nid_postfix(NAN, 1.0L)) && errno == EDOM); + errno = 0; + Require(fmodl_nid_postfix(7.0L, 2.0L) == 1.0L && errno == 0); + int quotient = 0; + errno = 0; + Require(std::isnan(remquol_nid_postfix(1.0L, 0.0L, "ient)) && errno == EDOM); + errno = 0; + remquol_nid_postfix(NAN, 0.0L, "ient); + Require(errno == 0); + Require(remquol_nid_postfix(-7.0L, 2.0L, "ient) == 1.0L && quotient == -4); + errno = 0; + Require(std::isnan(scalb_nid_postfix(1.0, 2.5)) && errno == 0); + Require(scalb_nid_postfix(1.0, 3.0) == 8.0 && scalb_nid_postfix(4.0, -INFINITY) == 0.0); + errno = 0; + Require(std::isnan(drem_nid_postfix(1.0, 0.0)) && errno == EDOM); + errno = 0; + Require(std::isnan(drem_nid_postfix(INFINITY, 1.0)) && errno == EDOM); + errno = 0; + Require(std::isnan(dremf_nid_postfix(1.0f, 0.0f)) && errno == EDOM); + errno = 0; + Require(std::isnan(remainderl_nid_postfix(1.0L, 0.0L)) && errno == EDOM); + errno = 0; + Require(std::isnan(remainderl_nid_postfix(INFINITY, 1.0L)) && errno == EDOM); + errno = 0; + Require(std::isnan(remainderl_nid_postfix(NAN, 0.0L)) && errno == 0); + Require(remainderl_nid_postfix(7.0L, 2.0L) == -1.0L && errno == 0); + Require(drem_nid_postfix(7.0, 2.0) == -1.0 && lrint_nid_postfix(2.5) == 2); +} + +static void CheckConsoleQuirks() { + Require(ilogbl_nid_postfix(NAN) == INT_MAX && ilogbl_nid_postfix(0.0L) == INT_MIN && ilogbl_nid_postfix(8.0L) == 3); + Require(scalblnl_nid_postfix(1.0L, INT64_C(0x100000001)) == 2.0L && scalblnl_nid_postfix(1.0L, INT64_MAX) == 0.5L); + Require(scalbln_nid_postfix(1.0, INT64_C(0x100000001)) == INFINITY && scalbln_nid_postfix(1.0, INT64_MIN) == 0.0); + Require(lrint_nid_postfix(0x1p40) == INT64_C(0x10000000000)); + const long double product = Extended(0x4000, 0xe000000000000000ull) * Extended(0x4001, 0xda76b022816f6a94ull); + Require(SameBits(fmal_nid_postfix(Extended(0x4000, 0xe000000000000000ull), Extended(0x4001, 0xda76b022816f6a94ull), Extended(0x9b45, 0xd3b49eac84283c95ull)), product)); + Require(finite_nid_postfix(1.0) == 1 && finite_nid_postfix(INFINITY) == 0); +} + +static void CheckRoundingModes() { + struct Case { + int mode; + double negative; + double positive; + }; + const Case cases[] = {{FE_TONEAREST, -2.0, 2.0}, {FE_DOWNWARD, -2.0, 1.0}, {FE_UPWARD, -1.0, 2.0}, {FE_TOWARDZERO, -1.0, 1.0}}; + const int saved = std::fegetround(); + for (const auto& test : cases) { + Require(std::fesetround(test.mode) == 0); + Require(rint_nid_postfix(-1.5) == test.negative && rint_nid_postfix(1.5) == test.positive); + Require(rintf_nid_postfix(-1.5f) == static_cast(test.negative) && rintf_nid_postfix(1.5f) == static_cast(test.positive)); + Require(rintl_nid_postfix(-1.5L) == test.negative && rintl_nid_postfix(1.5L) == test.positive); + Require(lrint_nid_postfix(-1.5) == static_cast(test.negative) && llrint_nid_postfix(1.5) == static_cast(test.positive)); + Require(std::fegetround() == test.mode); + } + Require(std::fesetround(saved) == 0); +} + +static void CheckClassification() { + const long double pseudoDenormal = Extended(0x0000, 0x8000000000000000ull); + Require(_LDtest_nid_postfix(&pseudoDenormal) == -1 && __fpclassifyl_nid_postfix(pseudoDenormal) == -2); + const long double zero = 0.0L, infinity = INFINITY, nan = NAN, denormal = Extended(0, 1); + Require(_LDtest_nid_postfix(&zero) == 0 && _LDtest_nid_postfix(&infinity) == 1 && _LDtest_nid_postfix(&nan) == 2 && _LDtest_nid_postfix(&denormal) == -2); + Require(__fpclassifyl_nid_postfix(1.0L) == -1 && __fpclassifyl_nid_postfix(0.0L) == 0 && __fpclassifyl_nid_postfix(INFINITY) == 1); + Require(__isnanl_nid_postfix(NAN) == 1 && __isnanl_nid_postfix(INFINITY) == 0); + Require(__isnormall_nid_postfix(1.0L) == 1 && __isnormall_nid_postfix(denormal) == 0); + Require(_Dclass_nid_postfix(5e-324) == -2 && _Dclass_nid_postfix(-0.0) == 0); + Require(_LDsign_nid_postfix(-2.0L) == -32768 && _LDsign_nid_postfix(2.0L) == 0); +} + +int main() { + CheckSignalingNanPassThrough(); + CheckErrno(); + CheckConsoleQuirks(); + CheckRoundingModes(); + CheckClassification(); + return 0; +} diff --git a/core/libs/tests/GuestMbus.cpp b/core/libs/tests/GuestMbus.cpp index bf57e10121..e867068157 100644 --- a/core/libs/tests/GuestMbus.cpp +++ b/core/libs/tests/GuestMbus.cpp @@ -6,11 +6,24 @@ #include #include +struct DeviceServiceThreadParam; +struct DeviceServiceInitParam { + std::uint32_t size; + std::uint32_t reserved; + const DeviceServiceThreadParam* threadParam; +}; +struct DeviceServiceUsageFilter { + std::uint16_t usagePage; + std::uint16_t minimumUsage; + std::uint16_t maximumUsage; +}; + extern "C" { -std::int32_t APS5_VABI sceDeviceServiceInitialize(std::int32_t mode, std::int32_t flags); +std::int32_t APS5_VABI sceDeviceServiceInitialize(std::int32_t mode, const DeviceServiceInitParam* param); std::int32_t APS5_VABI sceDeviceServiceTerminate(); -std::int32_t APS5_VABI sceDeviceServiceGetEventState(std::int32_t deviceType); -std::int32_t APS5_VABI sceDeviceServiceQueryDeviceInfo_(std::int32_t deviceClass, std::int32_t arg1, std::int32_t arg2, void* infos, std::int32_t maxInfos, std::int32_t* count, std::int32_t* reserved, std::uint64_t infoSize); +std::int32_t APS5_VABI sceDeviceServiceGetEventState(std::int32_t clear); +std::int32_t APS5_VABI sceDeviceServiceQueryDeviceInfo_(std::int32_t deviceClass, const DeviceServiceUsageFilter* filter, std::int32_t arg2, void* infos, std::int32_t maxInfos, std::int32_t* count, std::int32_t* reserved, std::uint32_t infoSize); +std::int32_t APS5_VABI sceDeviceServiceGetGeneration(); } namespace { @@ -25,19 +38,33 @@ void Require(bool condition, const char* message) { } int main() { + DeviceServiceInitParam param{}; + constexpr auto invalidArgument = static_cast(0x809B0080u); + Require(sceDeviceServiceInitialize(3, ¶m) == invalidArgument, "invalid parameter size accepted"); + param.size = 16; + param.reserved = 1; + Require(sceDeviceServiceInitialize(3, ¶m) == invalidArgument, "reserved field accepted"); + param.reserved = 0; + Require(sceDeviceServiceInitialize(3, ¶m) == 0, "default parameter initialization failed"); Require(sceDeviceServiceInitialize(3, 0) == 0, "initialization failed"); + Require(sceDeviceServiceGetGeneration() == 0, "the device set started at another generation"); Require(sceDeviceServiceGetEventState(1) == 0, "a device event was reported"); std::array info{}; std::int32_t count = 7; std::int32_t reserved = 0; Require(sceDeviceServiceQueryDeviceInfo_(0x7001, 0, 0, info.data(), 1, &count, &reserved, info.size()) == 0 && count == 0, "a device was listed"); - bool threw = false; - try { - sceDeviceServiceQueryDeviceInfo_(0x7001, 0, 0, info.data(), 1, nullptr, &reserved, info.size()); - } catch (const std::invalid_argument&) { - threw = true; - } - Require(threw, "a null count was accepted"); + Require(sceDeviceServiceQueryDeviceInfo_(0x7001, nullptr, 0, info.data(), 1, nullptr, &reserved, info.size()) == 0, "optional count output rejected"); + Require(sceDeviceServiceQueryDeviceInfo_(0x7001, nullptr, 0, nullptr, 0, nullptr, nullptr, info.size()) == 0, "empty query rejected"); + Require(sceDeviceServiceQueryDeviceInfo_(0x7001, nullptr, 0, nullptr, 1, &count, nullptr, info.size()) == invalidArgument, "null record buffer accepted"); + Require(sceDeviceServiceQueryDeviceInfo_(0x7001, nullptr, 0, info.data(), -1, &count, nullptr, info.size()) == invalidArgument, "negative capacity accepted"); + Require(sceDeviceServiceQueryDeviceInfo_(0x7001, nullptr, 0, info.data(), 1, &count, nullptr, info.size() - 1) == invalidArgument, "incorrect record size accepted"); + DeviceServiceUsageFilter filter{1, 2, 3}; + Require(sceDeviceServiceQueryDeviceInfo_(0x8001, &filter, 0, info.data(), 1, &count, nullptr, info.size()) == 0, "valid HID filter rejected"); + Require(sceDeviceServiceQueryDeviceInfo_(0x8001, nullptr, 0, info.data(), 1, &count, nullptr, info.size()) == invalidArgument, "missing HID filter accepted"); + Require(sceDeviceServiceQueryDeviceInfo_(0x7001, &filter, 0, info.data(), 1, &count, nullptr, info.size()) == invalidArgument, "unexpected HID filter accepted"); + filter.minimumUsage = 4; + Require(sceDeviceServiceQueryDeviceInfo_(0x8001, &filter, 0, info.data(), 1, &count, nullptr, info.size()) == invalidArgument, "reversed HID usage range accepted"); + Require(sceDeviceServiceGetGeneration() == 0, "the generation advanced without a device change"); Require(sceDeviceServiceTerminate() == 0, "termination failed"); std::puts("DeviceService tests passed"); return 0; diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index d73d23368c..d03a35de70 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -38,6 +38,8 @@ #define WIN32_LEAN_AND_MEAN #include #include +#include +#include #endif extern "C" { @@ -46,6 +48,7 @@ int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; int APS5_VABI sceKernelMmap(void*, std::size_t, int, int, int, std::int64_t, void**); int APS5_VABI mprotect_nid_postfix(void*, std::size_t, int) noexcept; int APS5_VABI madvise_nid_postfix(void*, std::size_t, int); +int APS5_VABI mincore_nid_postfix(const void*, std::size_t, char*); int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceKernelMapNamedFlexibleMemory(void**, std::size_t, int, int, const char*); int APS5_VABI sceKernelMapNamedFlexibleMemoryInternal(void**, std::size_t, int, int, const char*); @@ -75,6 +78,7 @@ int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI sceKernelPwrite(int, const void*, std::size_t, std::int64_t); int APS5_VABI sceKernelAioInitializeImpl(void*, std::int32_t); int APS5_VABI sceKernelAioSubmitReadCommands(KernelAioRwRequest*, std::int32_t, std::int32_t, std::int32_t*); int APS5_VABI sceKernelAioWaitRequest(std::int32_t, std::int32_t*, std::uint32_t*); @@ -153,8 +157,14 @@ static void CheckPageTableStats() { static void CheckInternalMemoryAvailableSize() { std::size_t available = 1; - Require(sceKernelInternalMemoryGetAvailableSize(&available) == 0 && available == 0); - Require(sceKernelInternalMemoryGetAvailableSize(nullptr) == static_cast(0x80020016u)); + bool threw = false; + try { + sceKernelInternalMemoryGetAvailableSize(&available); + } catch (const std::logic_error&) { + threw = true; + } + Require(threw && available == 1); + Require(sceKernelInternalMemoryGetAvailableSize(nullptr) == -1); } static void CheckNamedAndHintedMappings() { @@ -202,6 +212,98 @@ static void CheckAudioCoprocessorProtection() { Require(rejected && undefined == nullptr); } +static void CheckMprotectIgnoresUndefinedBits() { + constexpr std::size_t length = 0x4000; + const auto check = [&](void* mapping, int protection) { + Require(sceKernelMprotect(mapping, length, protection) == 0); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(mapping, 0, &info, sizeof(info)) == 0); + int expected = protection & 0x3f7; + if ((expected & 0x2) != 0) expected |= 0x1; + if ((expected & 0x80) != 0) expected |= 0x40; + if ((expected & 0x200) != 0) expected |= 0x100; + if (info.protection != expected) + std::fprintf(stderr, "Memory protection 0x%x reported 0x%x, expected 0x%x\n", static_cast(protection), static_cast(info.protection), static_cast(expected)); + Require(info.protection == expected); + auto* bytes = static_cast(mapping); + if ((protection & 0x222) != 0) { + bytes[0] = 5; + bytes[length - 1] = 7; + Require(bytes[0] == 5 && bytes[length - 1] == 7); + } else if ((protection & 0x117) != 0) { + Require(bytes[0] == 5 && bytes[length - 1] == 7); + } + }; + void* mapping = nullptr; + Require(sceKernelMapFlexibleMemory(&mapping, length, 3, 0) == 0); + for (int byte = 0x80; byte <= 0xff; ++byte) check(mapping, byte - 0x100); + for (const int protection : {0, 1, 2, 3, 4, 0x10, 0x20, 0x40, 0x80, 0x100, 0x200, 0x3f7}) { + for (const auto undefined : {0u, 8u, 0x400u, 0x80000000u, 0xfffffc08u}) + check(mapping, static_cast(static_cast(protection) | undefined)); + } + Require(sceKernelMunmap(mapping, length) == 0); + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, length, 0, 0, &physical) == 0); + mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, length, 0xf2, 0, physical, 0) == 0); + check(mapping, -14); + check(mapping, 0x400); + check(mapping, 0x100); + check(mapping, 0x200); + check(mapping, 0x3f7); + Require(sceKernelMunmap(mapping, length) == 0); + Require(sceKernelReleaseDirectMemory(physical, length) == 0); +} + +static void CheckStrictMemoryProtectionBits() { + constexpr std::size_t length = 0x4000; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, length, 0, 0, &physical) == 0); + void* mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, length, 3, 0, physical, 0) == 0); + auto* bytes = static_cast(mapping); + bytes[0] = 5; + bytes[length - 1] = 7; + const auto rejected = [&](auto operation) { + bool threw = false; + try { operation(); } catch (const std::invalid_argument&) { threw = true; } + Require(threw); + }; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0); + for (const int protection : {-256, -129, -128, -14, -1, 8, 0x400, std::numeric_limits::min()}) { + void* invalid = nullptr; + rejected([&] { sceKernelMapFlexibleMemory(&invalid, length, protection, 0); }); + Require(invalid == nullptr); + std::size_t after = 0; + Require(sceKernelAvailableFlexibleMemorySize(&after) == 0 && after == available); + rejected([&] { sceKernelMapDirectMemory(&invalid, length, protection, 0, physical, 0); }); + Require(invalid == nullptr); + rejected([&] { sceKernelMtypeprotect(mapping, length, 2, protection); }); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(mapping, 0, &info, sizeof(info)) == 0); + Require(info.protection == 3 && info.memory_type == 0); + int type = -1; + std::int64_t start = -1; + std::int64_t end = -1; + Require(sceKernelGetDirectMemoryType(physical, &type, &start, &end) == 0 && type == 0); + Require(bytes[0] == 5 && bytes[length - 1] == 7); + } + Require(sceKernelMunmap(mapping, length) == 0); + Require(sceKernelReleaseDirectMemory(physical, length) == 0); +} + +static void CheckPosixMprotectIgnoresUndefinedBits() { + constexpr std::size_t length = 0x4000; + auto* mapping = static_cast(mmap_nid_postfix(nullptr, length, 3, 0x1002, -1, 0)); + Require(mapping != reinterpret_cast(static_cast(-1))); + Require(mprotect_nid_postfix(const_cast(mapping), length, static_cast(0xfffffff2u)) == 0); + mapping[length - 1] = 5; + Require(mapping[length - 1] == 5); + Require(mprotect_nid_postfix(const_cast(mapping), length, 0x8 | 0x3) == 0); + Require(munmap_nid_postfix(const_cast(mapping), length) == 0); +} + static void CheckInternalNamedFlexibleMapping() { constexpr std::size_t length = 0x10000; std::size_t before = 0; @@ -226,6 +328,59 @@ static void CheckInternalNamedFlexibleMapping() { Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); } +static void CheckFlexibleFixedReplacement() { + constexpr std::size_t page = 0x4000; + std::size_t before = 0; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + void* base = nullptr; + Require(sceKernelMapFlexibleMemory(&base, page * 4, 3, 0) == 0 && base != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 4); + auto* bytes = static_cast(base); + for (std::size_t index = 0; index < 4; ++index) bytes[index * page] = static_cast(17 + index); + void* middle = bytes + page; + void* replacement = middle; + Require(sceKernelMapFlexibleMemory(&replacement, page, 3, 0x10) == 0 && replacement == middle); + Require(bytes[0] == 17 && bytes[page] == 0 && bytes[page * 2] == 19 && bytes[page * 3] == 20); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 4); + Require(sceKernelMunmap(base, page * 4) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); +} + +static void CheckMunmapScopedFlexibleRelease() { + constexpr std::size_t page = 0x4000; + std::size_t before = 0; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + void* base = nullptr; + Require(sceKernelMapFlexibleMemory(&base, page * 2, 3, 0) == 0 && base != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 2); + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &phys) == 0); + void* direct = static_cast(base) + page; + Require(sceKernelMapDirectMemory(&direct, page, 3, 0x10, phys, 0) == 0 && direct == static_cast(base) + page); + std::size_t mid = 0; + Require(sceKernelAvailableFlexibleMemorySize(&mid) == 0); + Require(sceKernelMunmap(direct, page) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == mid); + void* reserved = nullptr; + Require(sceKernelReserveVirtualRange(&reserved, page * 2, 0, 0) == 0 && reserved != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == mid); + Require(sceKernelMunmap(reserved, page * 2) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == mid); + Require(sceKernelMunmap(base, page) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == mid + page); + Require(sceKernelReleaseDirectMemory(phys, page) == 0); + void* fresh = nullptr; + Require(sceKernelMapFlexibleMemory(&fresh, page * 4, 3, 0) == 0 && fresh != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 4); + Require(sceKernelMunmap(static_cast(fresh) + page, page * 2) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 2); + Require(sceKernelMunmap(fresh, page) == 0); + Require(sceKernelMunmap(static_cast(fresh) + page * 3, page) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); +} + static void CheckGuestModuleUsesFlexibleMemory() { constexpr std::size_t moduleData = 0x10000; const auto path = std::filesystem::relative(GUEST_MEMORY_MODULE).generic_string(); @@ -417,6 +572,119 @@ static void CheckSparseDirectMemoryBacking() { Require(sceKernelReleaseDirectMemory(physical, size) == 0); } +static void CheckSavedDirectMemorySurvivesCachePressure() { +#ifdef _WIN32 + constexpr std::size_t bytes = 0x200000; + std::array retained{}; + for (std::size_t index = 0; index < retained.size(); ++index) { + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &retained[index]) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, retained[index], bytes) == 0); + std::memset(mapped, static_cast(index + 1), bytes); + Require(sceKernelMunmap(mapped, bytes) == 0); + } + const auto temporary = [] { + constexpr std::size_t small = 0x10000; + for (unsigned char round = 1; round <= 24; ++round) { + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, small, 0, 0, &physical) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, small, 3, 0, physical, 0) == 0); + auto* data = static_cast(mapped); + Require(std::all_of(data, data + small, [](unsigned char value) { return value == 0; })); + data[7] = round; + data[small - 1] = static_cast(round + 0x40); + Require(sceKernelMunmap(mapped, small) == 0); + mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, small, 3, 0, physical, 0) == 0); + data = static_cast(mapped); + for (std::size_t at = 0; at < small; ++at) Require(data[at] == (at == 7 ? round : at == small - 1 ? static_cast(round + 0x40) : 0)); + Require(sceKernelMunmap(mapped, small) == 0); + Require(sceKernelReleaseDirectMemory(physical, small) == 0); + } + }; + temporary(); + for (std::size_t index = 0; index < retained.size(); ++index) { + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, retained[index], bytes) == 0); + const auto* data = static_cast(mapped); + Require(std::all_of(data, data + bytes, [&](unsigned char value) { return value == index + 1; })); + Require(sceKernelMunmap(mapped, bytes) == 0); + Require(sceKernelReleaseDirectMemory(retained[index], bytes) == 0); + } + temporary(); +#endif +} + +static void CheckReusedDirectMemorySections() { +#ifdef _WIN32 + constexpr std::size_t page = 0x4000; + constexpr std::size_t bytes = page * 4; + const bool pooled = std::getenv("APS5_NO_DIRECT_MEMORY_SECTION_POOL") == nullptr; + const auto allocate = [] { + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &physical) == 0); + return physical; + }; + const auto map = [](std::int64_t physical) { + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, physical, 0) == 0); + return static_cast(mapped); + }; + const auto blank = [](const unsigned char* data) { return std::all_of(data, data + bytes, [](unsigned char value) { return value == 0; }); }; + const auto handles = [] { + DWORD count = 0; + Require(GetProcessHandleCount(GetCurrentProcess(), &count) != 0); + return count; + }; + auto physical = allocate(); + auto* data = map(physical); + std::memset(data, 0x5a, bytes); + Require(sceKernelMunmap(data, bytes) == 0); + auto before = handles(); + Require(sceKernelReleaseDirectMemory(physical, bytes) == 0); + const auto released = handles(); + Require(released == (pooled ? before : before - 1)); + physical = allocate(); + Require(handles() == (pooled ? released : released + 1)); + data = map(physical); + Require(blank(data)); + data[5] = 17; + data[bytes - 1] = 29; + void* second = nullptr; + Require(sceKernelMapDirectMemory(&second, bytes, 3, 0, physical, 0) == 0); + auto* alias = static_cast(second); + Require(alias != data && alias[5] == 17 && alias[6] == 0 && alias[bytes - 1] == 29); + alias[page + 3] = 41; + Require(data[page + 3] == 41); + data[page * 2] = 53; + Require(alias[page * 2] == 53); + Require(sceKernelMunmap(alias, bytes) == 0); + Require(data[5] == 17 && data[page + 3] == 41 && data[page * 2] == 53); + Require(sceKernelMunmap(data, bytes) == 0); + data = map(physical); + Require(data[5] == 17 && data[page + 3] == 41 && data[page * 2] == 53 && data[bytes - 1] == 29); + Require(sceKernelMunmap(data, bytes) == 0); + before = handles(); + Require(sceKernelReleaseDirectMemory(physical, bytes) == 0); + Require(handles() == before - 1); + for (unsigned char round = 1; round <= 8; ++round) { + physical = allocate(); + data = map(physical); + Require(blank(data)); + std::memset(data, round, bytes); + if (round % 2 == 0) { + second = nullptr; + Require(sceKernelMapDirectMemory(&second, page, 3, 0, physical + static_cast(page), 0) == 0); + Require(static_cast(second)[page - 1] == round); + Require(sceKernelMunmap(second, page) == 0); + } + Require(sceKernelMunmap(data, bytes) == 0); + Require(sceKernelReleaseDirectMemory(physical, bytes) == 0); + } +#endif +} + static void CheckReleaseDirectMemoryClearsMappings() { constexpr std::size_t page = 0x4000; std::int64_t phys = 0; @@ -736,6 +1004,23 @@ static void CheckHintedVirtualReservation() { Require(sceKernelMunmap(placed, page * 2) == 0); } +static void CheckReservationHintWithoutRoomAbove() { + constexpr std::size_t page = 0x4000; + std::uintptr_t base = 0; + std::size_t size = 0; + GuestArena::GuestArenaRange_nid_postfix(&base, &size); + const auto end = base + size; + void* aboveTheArena = reinterpret_cast(end + 0x40000000); + Require(sceKernelReserveVirtualRange(&aboveTheArena, page * 2, 0, 0) == 0); + Require(GuestArena::GuestArenaContains_nid_postfix(aboveTheArena, page * 2)); + void* const lastPage = reinterpret_cast(end - page); + void* belowTheHint = lastPage; + Require(sceKernelReserveVirtualRange(&belowTheHint, page * 2, 0, 0) == 0); + Require(belowTheHint < lastPage && GuestArena::GuestArenaContains_nid_postfix(belowTheHint, page * 2)); + Require(sceKernelMunmap(belowTheHint, page * 2) == 0); + Require(sceKernelMunmap(aboveTheArena, page * 2) == 0); +} + static void CheckFixedVirtualReservation() { constexpr std::size_t page = 0x4000; void* probe = nullptr; @@ -894,6 +1179,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")); @@ -908,6 +1194,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(); @@ -1014,6 +1301,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() { @@ -1189,6 +1524,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; @@ -1414,6 +1771,264 @@ static void CheckSingleDirectMappingTracksWritesWithoutFaults() { #endif } +static void CheckLargeResidentWritesStopFaulting() { +#ifdef _WIN32 + constexpr std::size_t bytes = 0x200000; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &physical) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, physical, bytes) == 0); + const auto collect = [&] { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(mapped), bytes, pages.data(), &count, true)); + return count; + }; + Require(collect() == bytes / 4096); + Require(collect() == 0 && collect() == 0); + MEMORY_BASIC_INFORMATION memory{}; + Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READONLY); + auto* data = static_cast(mapped); + const auto path = std::filesystem::path("anyps5-resident-read-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".bin"); + const std::vector input(65536, 89); + { + std::ofstream stream(path, std::ios::binary); + stream.write(input.data(), static_cast(input.size())); + } + const int fd = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(fd >= 0); + Require(sceKernelPread(fd, mapped, input.size(), 0) == static_cast(input.size())); + Require(data[0] == 89 && data[input.size() - 1] == 89); + Require(collect() == input.size() / 4096 && collect() == 0); + Require(sceKernelRead(fd, data + input.size(), input.size()) == static_cast(input.size())); + Require(data[input.size()] == 89 && data[input.size() * 2 - 1] == 89); + Require(collect() == input.size() / 4096 && collect() == 0); + Require(sceKernelClose(fd) == 0); + std::filesystem::remove(path); + { + GuestArena::HostImportAccess access(mapped, bytes); + Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READWRITE); + Require(collect() == 0 && collect() == 0); + Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READWRITE); + data[17] = 3; + Require(collect() == 1 && collect() == 0); + } + for (unsigned int round = 0; round < 16; ++round) Require(collect() == 0); + Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READONLY); + const auto before = g_accessViolations; + const auto handler = AddVectoredExceptionHandler(1, CountAccessViolations); + Require(handler != nullptr); + for (unsigned int round = 0; round < 16; ++round) { + for (std::size_t at = 0; at < bytes; at += 4096) static_cast(mapped)[at] = static_cast(round); + Require(collect() == bytes / 4096 && collect() == 0); + } + Require(RemoveVectoredExceptionHandler(handler)); + Require(g_accessViolations == before + 1); + Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READWRITE); + Require(mprotect_nid_postfix(mapped, bytes, 1) == 0); + Require(collect() == 0 && collect() == 0); + Require(mprotect_nid_postfix(mapped, bytes, 3) == 0); + data[0x10000] = 97; + Require(collect() == 1 && collect() == 0); + Require(sceKernelMunmap(mapped, bytes) == 0); + Require(sceKernelReleaseDirectMemory(physical, bytes) == 0); +#endif +} + +static void CheckImportAccessDoesNotInventSharedWrites() { +#ifdef _WIN32 + constexpr std::size_t bytes = 0x100000; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &physical) == 0); + void* first = nullptr; + void* second = nullptr; + Require(sceKernelMapDirectMemory(&first, bytes, 3, 0, physical, 0) == 0); + Require(sceKernelMapDirectMemory(&second, bytes, 3, 0, physical, 0) == 0); + const auto collect = [&](void* pointer) { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(pointer), bytes, pages.data(), &count, true)); + return count; + }; + collect(first); + collect(second); + for (unsigned int round = 0; round < 16; ++round) { + { + GuestArena::HostImportAccess access(first, bytes); + Require(collect(first) == 0 && collect(second) == 0); + } + Require(collect(first) == 0 && collect(second) == 0); + } + { + GuestArena::HostImportAccess access(first, bytes); + static_cast(first)[4097] = 73; + Require(collect(first) == 1 && collect(second) == 1); + } + Require(static_cast(second)[4097] == 73); + Require(collect(first) == 0 && collect(second) == 0); + Require(sceKernelMunmap(first, bytes) == 0); + Require(sceKernelMunmap(second, bytes) == 0); + Require(sceKernelReleaseDirectMemory(physical, bytes) == 0); +#endif +} + +static void CheckResidentCollectPreservesPartialWrites() { +#ifdef _WIN32 + constexpr std::size_t bytes = 0x210000; + constexpr std::size_t small = 0x1000; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &physical) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, physical, 0x200000) == 0); + const auto address = reinterpret_cast(mapped); + const auto collect = [&](std::size_t offset, std::size_t length, std::size_t capacity, bool clear, bool armed) { + unsigned char sentinel = 0; + std::vector pages(capacity + 2, &sentinel); + std::size_t count = capacity; + const auto function = armed ? &GuestArena::GuestArenaCollectArmedWrites_nid_postfix : &GuestArena::GuestArenaCollectWrites_nid_postfix; + Require(function(address + offset, length, pages.data() + 1, &count, clear)); + Require(count <= capacity && pages.front() == &sentinel && pages.back() == &sentinel); + std::vector found; + for (std::size_t at = 0; at < count; ++at) found.push_back(reinterpret_cast(pages[at + 1]) - address); + return found; + }; + Require(collect(0, bytes, 0, true, false).empty()); + Require(collect(0, bytes, bytes / small, true, false).size() == bytes / small); + Require(collect(0, bytes, bytes / small, true, true).empty()); + auto* data = static_cast(mapped); + data[3] = 1; + data[small + 8] = 2; + data[0x200000 + 9] = 3; + Require(collect(0, bytes, 0, true, false).empty()); + Require(collect(1, small - 1, 1, false, true) == std::vector{1}); + Require(collect(0, small, 1, false, false) == std::vector{0}); + Require(collect(0, small, 1, true, true) == std::vector{0}); + Require(collect(0, small, 1, true, false).empty()); + Require(collect(small, bytes - small, 1, true, false) == std::vector{small}); + Require(collect(small * 2, bytes - small * 2, bytes / small, true, true) == std::vector{0x200000}); + Require(collect(0, bytes, bytes / small, true, false).empty()); + Require(sceKernelMunmap(mapped, bytes) == 0); + Require(sceKernelReleaseDirectMemory(physical, bytes) == 0); +#endif +} + +static void CheckResidentCollectionDuringHostWrites() { +#ifdef _WIN32 + constexpr std::size_t bytes = 0x100000; + constexpr std::size_t small = 0x1000; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &physical) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, physical, 0x200000) == 0); + auto* data = static_cast(mapped); + const auto address = reinterpret_cast(mapped); + const auto collect = [&] { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(address, bytes, pages.data(), &count, true)); + return std::vector(pages.begin(), pages.begin() + count); + }; + collect(); + Require(collect().empty()); + Require(GuestArena::GuestArenaBeginHostWrite_nid_postfix(data + small, small)); + data[small + 7] = 1; + Require(collect() == std::vector{data + small}); + data[small + 8] = 2; + GuestArena::GuestArenaEndHostWrite_nid_postfix(data + small, small); + Require(collect() == std::vector{data + small}); + Require(collect().empty()); + GuestArena::GuestArenaPinWritable_nid_postfix(data + small * 2, small); + collect(); + static_cast(mapped)[small * 2 + 9] = 3; + Require(collect() == std::vector{data + small * 2}); + GuestArena::GuestArenaUnpinWritable_nid_postfix(data + small * 2, small); + collect(); + std::atomic written{0}; + std::atomic consumed{0}; + std::thread writer([&] { + for (unsigned int round = 1; round <= 128; ++round) { + while (consumed.load(std::memory_order_acquire) != round - 1) std::this_thread::yield(); + static_cast(mapped)[small * 3 + 7] = static_cast(round); + written.store(round, std::memory_order_release); + } + }); + for (unsigned int round = 1; round <= 128; ++round) { + const auto racingPages = collect(); + while (written.load(std::memory_order_acquire) != round) std::this_thread::yield(); + const auto pages = collect(); + Require(std::find(pages.begin(), pages.end(), data + small * 3) != pages.end() || std::find(racingPages.begin(), racingPages.end(), data + small * 3) != racingPages.end()); + Require(data[small * 3 + 7] == static_cast(round)); + Require(collect().empty()); + consumed.store(round, std::memory_order_release); + } + writer.join(); + void* alias = nullptr; + Require(sceKernelMapDirectMemory(&alias, 0x4000, 3, 0, physical, 0) == 0); + collect(); + static_cast(alias)[small + 11] = 4; + Require(data[small + 11] == 4); + const auto pages = collect(); + Require(std::find(pages.begin(), pages.end(), data + small) != pages.end()); + Require(sceKernelMunmap(alias, 0x4000) == 0); + Require(sceKernelMunmap(mapped, bytes) == 0); + Require(sceKernelReleaseDirectMemory(physical, bytes) == 0); +#endif +} + +static void CheckReleaseKeepsPhysicalUntilViewsUnmapped(decltype(&sceKernelReleaseDirectMemory) release) { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 4, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 4, 3, 0, phys, 0) == 0); + auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + Require(std::count_if(lease.begin(), lease.end(), [&](const auto& range) { return range->address == reinterpret_cast(mapped); }) == 1); + std::atomic started{false}; + std::atomic finished{false}; + std::thread releaser([&] { + started = true; + Require(release(phys, page * 4) == 0); + finished = true; + }); + while (!started) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Require(!finished); + std::int64_t again = 0; + Require(sceKernelAllocateDirectMemory(phys, phys + static_cast(page * 4), page * 4, 0, 0, &again) != 0); + lease.clear(); + releaser.join(); + Require(finished); + Require(sceKernelAllocateDirectMemory(phys, phys + static_cast(page * 4), page * 4, 0, 0, &again) == 0 && again == phys); + Require(sceKernelReleaseDirectMemory(again, page * 4) == 0); +} + +static void CheckConcurrentCheckedReleasesUnmapViewsOnce() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 4, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 4, 3, 0, phys, 0) == 0); + auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + std::atomic started{0}; + int results[2] = {-1, -1}; + std::thread releasers[2]; + for (int index = 0; index < 2; index++) { + releasers[index] = std::thread([&, index] { + started++; + results[index] = sceKernelCheckedReleaseDirectMemory(phys, page * 4); + }); + } + while (started != 2) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + lease.clear(); + for (auto& releaser : releasers) releaser.join(); + Require((results[0] == 0 && results[1] == SCE_KERNEL_ERROR_ENOENT) || (results[0] == SCE_KERNEL_ERROR_ENOENT && results[1] == 0)); + std::int64_t again = 0; + Require(sceKernelAllocateDirectMemory(phys, phys + static_cast(page * 4), page * 4, 0, 0, &again) == 0 && again == phys); + Require(sceKernelReleaseDirectMemory(again, page * 4) == 0); +} + static void CheckSecondDirectMappingWaitsForLeases() { #ifdef _WIN32 constexpr std::size_t page = 0x4000; @@ -2263,6 +2878,107 @@ static void CheckMappingCostFollowsTheMappingCount() { } #endif +#if defined(__linux__) || defined(_WIN32) +static void CheckRegularFileMapping() { + constexpr std::size_t page = 0x4000; + const auto path = std::filesystem::path("anyps5-file-mapping-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".bin"); + { + std::ofstream stream(path, std::ios::binary); + std::vector seed(page * 2, 0x11); + std::fill(seed.begin() + page, seed.end(), 0x22); + stream.write(seed.data(), static_cast(seed.size())); + } + const auto byteAt = [&](int fd, std::int64_t offset) { + unsigned char byte = 0; + Require(sceKernelPread(fd, &byte, 1, offset) == 1); + return byte; + }; + const int fd = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDWR, 0); + Require(fd >= 0); + auto* shared = static_cast(mmap_nid_postfix(nullptr, page * 2, 3, 0x1, fd, 0)); + Require(shared != reinterpret_cast(static_cast(-1))); + Require(shared[0] == 0x11 && shared[page] == 0x22); + const unsigned char written = 0x33; + Require(sceKernelPwrite(fd, &written, 1, page + 7) == 1); + Require(shared[page + 7] == 0x33); + shared[5] = 0x77; + Require(munmap_nid_postfix(const_cast(shared), page * 2) == 0); + Require(byteAt(fd, 5) == 0x77); + auto* privateCopy = static_cast(mmap_nid_postfix(nullptr, page, 3, 0x2, fd, 0)); + Require(privateCopy != reinterpret_cast(static_cast(-1))); + Require(privateCopy[5] == 0x77); + privateCopy[6] = 0x66; + Require(munmap_nid_postfix(const_cast(privateCopy), page) == 0); + Require(byteAt(fd, 6) == 0x11); + auto* second = static_cast(mmap_nid_postfix(nullptr, page, 1, 0x1, fd, page)); + Require(second != reinterpret_cast(static_cast(-1)) && second[0] == 0x22); + Require(munmap_nid_postfix(const_cast(second), page) == 0); + Require(sceKernelClose(fd) == 0); + const int readOnly = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(readOnly >= 0); + *__error_nid_postfix() = 0; + Require(mmap_nid_postfix(nullptr, page, 3, 0x1, readOnly, 0) == reinterpret_cast(static_cast(-1)) && *__error_nid_postfix() == 13); + auto* readable = static_cast(mmap_nid_postfix(nullptr, page, 1, 0x1, readOnly, 0)); + Require(readable != reinterpret_cast(static_cast(-1)) && readable[5] == 0x77); + *__error_nid_postfix() = 0; + Require(mprotect_nid_postfix(const_cast(readable), page, 3) == -1 && *__error_nid_postfix() == 13); + Require(mprotect_nid_postfix(const_cast(readable), page, 1) == 0); + Require(readable[5] == 0x77); + Require(munmap_nid_postfix(const_cast(readable), page) == 0); + Require(sceKernelClose(readOnly) == 0); + constexpr std::size_t tail = 0x1100; + constexpr std::size_t hostPage = 0x1000; + std::filesystem::resize_file(path, page + tail); + const int partial = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(partial >= 0); + auto* rounded = static_cast(mmap_nid_postfix(nullptr, page * 2, 1, 0x1, partial, 0)); + Require(rounded != reinterpret_cast(static_cast(-1))); + Require(rounded[5] == 0x77 && rounded[page] == 0x22 && rounded[page + 7] == 0x33 && rounded[page + tail - 1] == 0x22); + for (std::size_t at = page + tail; at < page + 2 * hostPage; ++at) Require(rounded[at] == 0); +#ifdef _WIN32 + for (std::size_t at = page + 2 * hostPage; at < page * 2; ++at) Require(rounded[at] == 0); + *__error_nid_postfix() = 0; + Require(mmap_nid_postfix(nullptr, page * 3, 1, 0x1, partial, 0) == reinterpret_cast(static_cast(-1)) && *__error_nid_postfix() == 45); +#endif + Require(munmap_nid_postfix(const_cast(rounded), page * 2) == 0); + Require(sceKernelClose(partial) == 0); + Require(std::filesystem::file_size(path) == page + tail); + const int partialWritable = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDWR, 0); + Require(partialWritable >= 0); + auto* roundedWritable = static_cast(mmap_nid_postfix(nullptr, page * 2, 3, 0x1, partialWritable, 0)); + Require(roundedWritable != reinterpret_cast(static_cast(-1))); + const unsigned char nearEnd = 0x44; + Require(sceKernelPwrite(partialWritable, &nearEnd, 1, page + tail - 2) == 1); + Require(roundedWritable[page + tail - 2] == 0x44); + roundedWritable[page + tail - 1] = 0x55; + roundedWritable[page + tail] = 0x66; +#ifdef _WIN32 + roundedWritable[page * 2 - 1] = 0x66; + Require(roundedWritable[page * 2 - 1] == 0x66); +#endif + Require(munmap_nid_postfix(const_cast(roundedWritable), page * 2) == 0); + Require(byteAt(partialWritable, page + tail - 1) == 0x55); + Require(sceKernelClose(partialWritable) == 0); + Require(std::filesystem::file_size(path) == page + tail); + int ends[2] = {-1, -1}; +#ifdef _WIN32 + Require(_pipe(ends, 4096, _O_BINARY) == 0); +#else + Require(pipe(ends) == 0); +#endif + *__error_nid_postfix() = 0; + Require(mmap_nid_postfix(nullptr, page, 1, 0x1, ends[0], 0) == reinterpret_cast(static_cast(-1)) && *__error_nid_postfix() == 45); +#ifdef _WIN32 + _close(ends[0]); + _close(ends[1]); +#else + close(ends[0]); + close(ends[1]); +#endif + Require(std::filesystem::remove(path)); +} +#endif + static void CheckVirtualQuerySplitFlexibleRanges() { constexpr std::size_t page = 0x4000; void* mapped = nullptr; @@ -2295,6 +3011,36 @@ static void CheckVirtualQuerySplitFlexibleRanges() { Require(sceKernelMunmap(bytes + page * 2, page) == 0); } +static void CheckVirtualQueryWireLayout() { + constexpr std::size_t page = 0x4000; + constexpr char name[] = "query wire layout"; + void* mapped = nullptr; + Require(sceKernelMapNamedFlexibleMemory(&mapped, page, 3, 0, name) == 0); + struct { + std::array before; + VirtualQueryInfo info; + std::array after; + } result{}; + result.before.fill(0xa5); + result.after.fill(0x5a); + Require(sceKernelVirtualQuery(mapped, 0, &result.info, 72) == 0); + const auto* bytes = reinterpret_cast(&result.info); + std::uintptr_t start = 0, end = 0; + std::int32_t protection = 0; + std::memcpy(&start, bytes, sizeof(start)); + std::memcpy(&end, bytes + 8, sizeof(end)); + std::memcpy(&protection, bytes + 24, sizeof(protection)); + Require(start == reinterpret_cast(mapped) && end == start + page); + Require(protection == 3 && bytes[32] == 0x11); // flexible and committed + Require(std::memcmp(bytes + 33, name, sizeof(name)) == 0); + Require(bytes[65] == 0 && bytes[66] == 0); + Require(std::all_of(bytes + 67, bytes + 72, [](auto byte) { return byte == 0; })); + Require(std::all_of(result.before.begin(), result.before.end(), [](auto byte) { return byte == 0xa5; })); + Require(std::all_of(result.after.begin(), result.after.end(), [](auto byte) { return byte == 0x5a; })); + Require(sceKernelClearVirtualRangeName(mapped, page) == 0); + Require(sceKernelMunmap(mapped, page) == 0); +} + static void CheckVirtualQueryForNonReadableGuestRange() { constexpr std::size_t page = 0x4000; auto* mapped = static_cast(mmap_nid_postfix(nullptr, page, 3, 0x1002, -1, 0)); @@ -2308,6 +3054,128 @@ static void CheckVirtualQueryForNonReadableGuestRange() { Require(sceKernelMunmap(mapped, page) == 0); } +static void CheckPosixFixedAnonymousMapping() { + constexpr std::size_t page = 0x4000; + constexpr int fixed = 0x1012; + constexpr int exclusive = 0x5012; + const auto failed = reinterpret_cast(static_cast(-1)); + void* reservation = nullptr; + Require(sceKernelReserveVirtualRange(&reservation, page * 4, 0, 0) == 0); + auto* memory = static_cast(reservation); + Require(mmap_nid_postfix(memory, page, 3, fixed, -1, 0) == memory); + Require(std::all_of(memory, memory + page, [](unsigned char value) { return value == 0; })); + memory[0] = 0x31; + memory[page - 1] = 0x32; + Require(memory[0] == 0x31 && memory[page - 1] == 0x32); + Require(mmap_nid_postfix(memory, page, 3, fixed, -1, 0) == memory); + Require(std::all_of(memory, memory + page, [](unsigned char value) { return value == 0; })); + memory[0] = 0x41; + memory[page - 1] = 0x42; + *__error_nid_postfix() = 0; + Require(mmap_nid_postfix(memory, page, 3, exclusive, -1, 0) == failed); + Require(*__error_nid_postfix() == 12); + Require(memory[0] == 0x41 && memory[page - 1] == 0x42); + VirtualQueryInfo before{}; + VirtualQueryInfo after{}; + Require(sceKernelVirtualQuery(memory + page, 0, &before, sizeof(before)) == 0); + *__error_nid_postfix() = 0; + Require(mmap_nid_postfix(memory + page, page, 3, exclusive, -1, 0) == failed); + Require(*__error_nid_postfix() == 12); + Require(sceKernelVirtualQuery(memory + page, 0, &after, sizeof(after)) == 0); + Require(before.start == after.start && before.end == after.end && before.is_committed == after.is_committed); + Require(mmap_nid_postfix(memory + page, page * 2, 3, fixed, -1, 0) == memory + page); + memory[page] = 0x51; + memory[page * 2 - 1] = 0x52; + memory[page * 2] = 0x61; + Require(sceKernelMunmap(memory + page * 3, page) == 0); + Require(mmap_nid_postfix(memory + page * 3, page, 3, exclusive, -1, 0) == memory + page * 3); + Require(std::all_of(memory + page * 3, memory + page * 4, [](unsigned char value) { return value == 0; })); + memory[page * 4 - 1] = 0x62; + Require(memory[page * 4 - 1] == 0x62); + Require(mmap_nid_postfix(memory + page * 2, page + 1, 3, fixed, -1, 0) == memory + page * 2); + Require(std::all_of(memory + page * 2, memory + page * 4, [](unsigned char value) { return value == 0; })); + Require(memory[0] == 0x41 && memory[page - 1] == 0x42); + Require(memory[page] == 0x51 && memory[page * 2 - 1] == 0x52); + memory[page * 4 - 1] = 0x71; + Require(memory[page * 4 - 1] == 0x71); + const auto invalid = [&](void* address, std::size_t length, int protection, int flags, int descriptor, std::int64_t offset) { + *__error_nid_postfix() = 0; + Require(mmap_nid_postfix(address, length, protection, flags, descriptor, offset) == failed); + Require(*__error_nid_postfix() == 22); + Require(memory[0] == 0x41 && memory[page * 4 - 1] == 0x71); + }; + invalid(memory, 0, 3, fixed, -1, 0); + invalid(memory, std::numeric_limits::max(), 3, fixed, -1, 0); + invalid(memory + 1, page, 3, fixed, -1, 0); + invalid(nullptr, page, 3, fixed, -1, 0); + invalid(memory, page, 8, fixed, -1, 0); + invalid(memory, page, 3, fixed, 0, 0); + invalid(memory, page, 3, fixed, -1, 1); + invalid(memory, page, 3, fixed, -1, -1); + invalid(nullptr, page, 3, 0x5002, -1, 0); + std::uintptr_t arenaBase = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes); + Require(arenaBase >= page && arenaBytes >= page); + invalid(reinterpret_cast(arenaBase - page), page, 3, fixed, -1, 0); + invalid(reinterpret_cast(arenaBase + arenaBytes), page, 3, fixed, -1, 0); + invalid(reinterpret_cast(arenaBase + arenaBytes - page), page * 2, 3, fixed, -1, 0); + invalid(reinterpret_cast(std::numeric_limits::max() - page + 1), page * 2, 3, fixed, -1, 0); + 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; @@ -2369,8 +3237,11 @@ static void CheckAvailableDirectMemoryIsLargestFreeBlock() { Require(start == top + 0x200000 && size == 0x400000); Require(sceKernelAvailableDirectMemorySize(top, top + window, 0x1000000, &start, &size) == 0); Require(start == top + 0x1000000 && size == static_cast(window - 0x1000000)); - Require(sceKernelAvailableDirectMemorySize(top, top + 0x200000, 0, &start, &size) == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelAvailableDirectMemorySize(top, top + 0x200000, 0, &start, &size) == SCE_KERNEL_ERROR_ENOMEM); Require(start == 0 && size == 0); + Require(sceKernelAvailableDirectMemorySize(top + window, top, 0, &start, &size) == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelAvailableDirectMemorySize(-1, top + window, 0, &start, &size) == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelAvailableDirectMemorySize(top, top + window, std::numeric_limits::max(), &start, &size) == SCE_KERNEL_ERROR_EAGAIN); Require(sceKernelAvailableDirectMemorySize(top, top + window, 0, nullptr, &size) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelReleaseDirectMemory(first, 0x200000) == 0); @@ -2401,10 +3272,19 @@ static void CheckMunmapOverUnmappedPages() { Require(sceKernelMunmap(memory, 0) == SCE_KERNEL_ERROR_EINVAL); } -int main() { +int main(int argc, char** argv) { #ifdef _WIN32 _putenv_s("APS5_PIN_WAIT_MS", "1000"); #endif + if (argc == 2 && std::strcmp(argv[1], "fixed-anonymous") == 0) { + CheckPosixFixedAnonymousMapping(); + CheckPosixFixedAnonymousReleasesFlexibleAccounting(); +#if defined(__linux__) + CheckPosixFixedFileReleasesFlexibleAccounting(); +#endif + return 0; + } + Require(argc == 1); CheckAvailableDirectMemoryIsLargestFreeBlock(); CheckVirtualQuerySplitFlexibleRanges(); CheckVirtualQueryForNonReadableGuestRange(); @@ -2415,14 +3295,22 @@ int main() { CheckVirtualQueryPartialMunmap(); CheckMunmapOverUnmappedPages(); CheckPageTableStats(); + CheckVirtualQueryWireLayout(); CheckNamedAndHintedMappings(); CheckInternalNamedFlexibleMapping(); + CheckFlexibleFixedReplacement(); + CheckMunmapScopedFlexibleRelease(); CheckGuestModuleUsesFlexibleMemory(); CheckBatchMapStopsAtInvalidEntry(); CheckCheckedReleaseDirectMemory(); CheckAudioCoprocessorProtection(); + CheckMprotectIgnoresUndefinedBits(); + CheckStrictMemoryProtectionBits(); + CheckPosixMprotectIgnoresUndefinedBits(); CheckDirectMemoryFollowsPhysicalPages(); CheckSparseDirectMemoryBacking(); + CheckSavedDirectMemorySurvivesCachePressure(); + CheckReusedDirectMemorySections(); CheckReleaseDirectMemoryClearsMappings(); CheckReleaseDirectMemoryRejectsInvalidRanges(); CheckFixedMappingReplacesPartialOverlap(); @@ -2433,6 +3321,7 @@ int main() { CheckGpuMapsAreObserved(); CheckClearingTheObserverWaitsForItsCalls(); CheckHintedVirtualReservation(); + CheckReservationHintWithoutRoomAbove(); CheckFixedVirtualReservation(); CheckReservedRangeIsNotCommitted(); CheckHintInsideReservation(); @@ -2445,11 +3334,14 @@ int main() { CheckHeapAfterMappingReuse(); CheckNoOverwriteRejectsHostOccupiedMapping(); CheckFixedMappingsReachTheApplicationAreaEnd(); -#ifdef _WIN32 +#if defined(_WIN32) || defined(__linux__) CheckGuestModuleImageProtection(); +#endif +#ifdef _WIN32 CheckAliasAcrossDirectAllocations(); CheckAliasOfReorderedSharedPages(); CheckAliasRefusesPagesThatAreNotSharedViews(); + CheckAliasLeavesResidentRunsUnderAHostWrite(); #endif CheckUnhintedKernelMappingsLandInTheWindow(); #if defined(__linux__) @@ -2465,7 +3357,14 @@ int main() { CheckPinnedSharedPages(); CheckFailedCollectKeepsWrites(); CheckSingleDirectMappingTracksWritesWithoutFaults(); + CheckLargeResidentWritesStopFaulting(); + CheckImportAccessDoesNotInventSharedWrites(); + CheckResidentCollectPreservesPartialWrites(); + CheckResidentCollectionDuringHostWrites(); CheckSecondDirectMappingWaitsForLeases(); + CheckReleaseKeepsPhysicalUntilViewsUnmapped(sceKernelReleaseDirectMemory); + CheckReleaseKeepsPhysicalUntilViewsUnmapped(sceKernelCheckedReleaseDirectMemory); + CheckConcurrentCheckedReleasesUnmapViewsOnce(); CheckBreakingAResidentRunKeepsConcurrentAccesses(); CheckRemappedDirectMemoryIsChargedOnce(); #if defined(__linux__) @@ -2479,6 +3378,9 @@ int main() { CheckDirectMemorySharedBacking(); CheckVirtualQueryFindsNeighbours(); CheckReleaseFindsEveryDirectMapping(); +#endif +#if defined(__linux__) || defined(_WIN32) + CheckRegularFileMapping(); #endif constexpr std::size_t page = 0x4000; const auto failed = reinterpret_cast(static_cast(-1)); @@ -2494,9 +3396,17 @@ int main() { reject(page, 3, 0x1002, 0, 0, 22); reject(page, 3, 0x1002, -1, 1, 22); reject(page, 3, 0x1001, -1, 0, 45); // shared - reject(page, 3, 0x1012, -1, 0, 45); // fixed + reject(page, 3, 0x1012, -1, 0, 22); reject(page, 3, 0x2, 0, 0, 45); // file-backed reject(page, 3, 0x22, -1, 0, 45); // Linux MAP_ANON is not guest MAP_ANON + reject(page, 3, 0x1003, -1, 0, 22); + reject(page, 3, 0x3, 0, 0, 22); + auto* anonymousOnly = static_cast(mmap_nid_postfix(nullptr, page, 3, 0x1000, -1, 0)); + Require(anonymousOnly != failed && (reinterpret_cast(anonymousOnly) & (page - 1)) == 0); + anonymousOnly[page - 1] = 7; + Require(anonymousOnly[page - 1] == 7 && anonymousOnly[0] == 0); + Require(munmap_nid_postfix(const_cast(anonymousOnly), page) == 0); + reject(page, 3, 0x1000, 0, 0, 22); auto* memory = static_cast(mmap_nid_postfix(nullptr, page * 3 - 1, 3, 0x1002, -1, 0)); Require(memory != failed && (reinterpret_cast(memory) & (page - 1)) == 0); @@ -2537,16 +3447,41 @@ int main() { *__error_nid_postfix() = 0; Require(madvise_nid_postfix(reinterpret_cast(0x800000000000), 1, 0) == -1 && *__error_nid_postfix() == 22); Require(madvise_nid_postfix(reinterpret_cast(0x7fffffffc000), 0x4000, 4) == 0); + char mincoreVec[4] = {}; + Require(mincore_nid_postfix(memory, page * 3, mincoreVec) == 0); + Require(mincoreVec[0] == 1 && mincoreVec[1] == 1 && mincoreVec[2] == 1); + mincoreVec[0] = mincoreVec[1] = 0x55; + Require(mincore_nid_postfix(memory + 1, page, mincoreVec) == 0 && mincoreVec[0] == 1 && mincoreVec[1] == 0x55); + Require(mincore_nid_postfix(memory, 0, nullptr) == 0); + Require(mincore_nid_postfix(reinterpret_cast(0x800000000000), 0, nullptr) == -1 && *__error_nid_postfix() == 12); + *__error_nid_postfix() = 0; + Require(mincore_nid_postfix(memory, page, nullptr) == -1 && *__error_nid_postfix() == 14); + *__error_nid_postfix() = 0; + Require(mincore_nid_postfix(reinterpret_cast(0x800000000000), page, nullptr) == -1 && *__error_nid_postfix() == 12); + *__error_nid_postfix() = 0; + Require(mincore_nid_postfix(reinterpret_cast(0x800000000000), page, mincoreVec) == -1 && *__error_nid_postfix() == 12); + *__error_nid_postfix() = 0; + Require(mincore_nid_postfix(memory, std::numeric_limits::max(), mincoreVec) == -1 && *__error_nid_postfix() == 12); + *__error_nid_postfix() = 0; + void* mincoreReserved = nullptr; + Require(sceKernelReserveVirtualRange(&mincoreReserved, page, 0, 0) == 0); + mincoreVec[0] = 0x55; + Require(mincore_nid_postfix(mincoreReserved, page, mincoreVec) == 0 && mincoreVec[0] == 0); + Require(sceKernelMunmap(mincoreReserved, page) == 0); Require(munmap_nid_postfix(memory + 1, page) == -1 && *__error_nid_postfix() == 22); Require(munmap_nid_postfix(memory, 0) == -1 && *__error_nid_postfix() == 22); Require(memory[0] == 42); Require(munmap_nid_postfix(memory + page, 1) == 0); // round to one guest page Require(memory[0] == 42 && memory[page * 2] == 73); + Require(mincore_nid_postfix(memory, page, mincoreVec) == 0 && mincoreVec[0] == 1); + Require(mincore_nid_postfix(memory, page * 3, mincoreVec) == -1 && *__error_nid_postfix() == 12); + *__error_nid_postfix() = 0; Require(munmap_nid_postfix(memory, page) == 0); Require(memory[page * 2] == 73); Require(munmap_nid_postfix(memory + page * 2, page) == 0); Require(munmap_nid_postfix(memory, page) == 0); Require(madvise_nid_postfix(memory, page, 4) == 0); + Require(mincore_nid_postfix(memory, page, mincoreVec) == -1 && *__error_nid_postfix() == 12); *__error_nid_postfix() = 0; Require(mprotect_nid_postfix(memory, page, 1) == -1 && *__error_nid_postfix() == 22); *__error_nid_postfix() = 0; diff --git a/core/libs/tests/GuestMemoryLifetime.cpp b/core/libs/tests/GuestMemoryLifetime.cpp new file mode 100644 index 0000000000..80a0dec272 --- /dev/null +++ b/core/libs/tests/GuestMemoryLifetime.cpp @@ -0,0 +1,75 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +int fixtureDestroyed = 0; +using Exercise = int (*)(); +Exercise exercise = nullptr; + +void ProbeAfterStaticDestruction() { + if (fixtureDestroyed != 1 || exercise == nullptr) { + std::fputs("memory lifetime: fixture finalizer did not run before the exit probe\n", stderr); + std::_Exit(1); + } + if (exercise() != 0) std::_Exit(1); + std::fputs("memory lifetime: allocation, backing, mapping and explicit free survived native exit\n", stderr); +} + +int RunChild(const char* module) { + if (std::atexit(ProbeAfterStaticDestruction) != 0) return 2; + auto* handle = dlopen(module, RTLD_NOW | RTLD_LOCAL); + if (handle == nullptr) { + std::fprintf(stderr, "memory lifetime: load failed: %s\n", dlerror()); + std::_Exit(2); + } + const auto prepare = reinterpret_cast(dlsym(handle, "GuestMemoryLifetimePrepare")); + exercise = reinterpret_cast(dlsym(handle, "GuestMemoryLifetimeExercise")); + if (prepare == nullptr || exercise == nullptr || prepare(&fixtureDestroyed) != 0) std::_Exit(2); + std::exit(0); +} + +} + +int main(int argc, char** argv) { + if (argc == 3 && std::strcmp(argv[1], "--child") == 0) return RunChild(argv[2]); + if (argc != 2) return 2; + const auto child = fork(); + if (child == -1) return 2; + if (child == 0) { + execl(argv[0], argv[0], "--child", argv[1], static_cast(nullptr)); + std::_Exit(2); + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(20); + int status = 0; + for (;;) { + const auto result = waitpid(child, &status, WNOHANG); + if (result == child) break; + if (result < 0) { + if (errno == EINTR) continue; + kill(child, SIGKILL); + while (waitpid(child, &status, 0) < 0 && errno == EINTR) {} + return 2; + } + if (std::chrono::steady_clock::now() >= deadline) { + kill(child, SIGKILL); + while (waitpid(child, &status, 0) < 0 && errno == EINTR) {} + std::fputs("memory lifetime: exit probe timed out\n", stderr); + return 1; + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + std::fprintf(stderr, "memory lifetime: child failed during native exit (status=%d)\n", status); + return 1; + } + return 0; +} diff --git a/core/libs/tests/GuestMemoryLifetimeModule.cpp b/core/libs/tests/GuestMemoryLifetimeModule.cpp new file mode 100644 index 0000000000..173ac84e32 --- /dev/null +++ b/core/libs/tests/GuestMemoryLifetimeModule.cpp @@ -0,0 +1,126 @@ +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include +#include +#include +#include + +namespace { + +int* fixtureDestroyed = nullptr; +std::int64_t physical = -1; +void* mapping = nullptr; +bool watched = false; + +struct StaticExitMarker { + ~StaticExitMarker() { if (fixtureDestroyed != nullptr) *fixtureDestroyed = 1; } +} marker; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void CheckPhysical(std::int64_t expected) { + std::int64_t start = -1, end = -1; + int type = -1; + Require(DirectMemoryFind(expected, false, &start, &end, &type), "physical pool lost a live allocation during exit"); + Require(start == expected && end == expected + static_cast(PS5_PAGE_SIZE) && type == 0, "physical pool range changed during exit"); +} + +void CheckMapping(void* address, std::int64_t expected) { + std::uintptr_t start = 0, end = 0; + std::uint64_t offset = 0; + int type = -1; + Require(QueryDirectMapping(reinterpret_cast(address), &start, &end, &offset, &type), "direct backing map lost a live allocation during exit"); + Require(start == reinterpret_cast(address) && end == start + PS5_PAGE_SIZE && offset == static_cast(expected) && type == 0, "direct backing range changed during exit"); + GuestAllocations::Mutation mutation; + Require(mutation.Covers(address, PS5_PAGE_SIZE), "guest allocation registry lost a live mapping during exit"); + Require(mutation.Find(address).bytes == PS5_PAGE_SIZE, "guest allocation size changed during exit"); +} + +void CheckUnmapped(void* address) { + std::uintptr_t begin = 0, end = 0; + std::uint64_t offset = 0; + int type = -1; + Require(!QueryDirectMapping(reinterpret_cast(address), &begin, &end, &offset, &type), "explicit unmap did not remove the direct mapping"); + int protection = -1; + Require(!GuestProtection(reinterpret_cast(address), &protection), "explicit unmap did not remove protection metadata"); + GuestAllocations::Mutation mutation; + Require(!mutation.Overlaps(address, PS5_PAGE_SIZE), "explicit unmap did not remove the guest allocation"); +} + +void IgnoreWritten(void*, std::uintptr_t, std::uintptr_t) {} + +} + +extern "C" int GuestMemoryLifetimePrepare(int* destroyed) { + try { + fixtureDestroyed = destroyed; + Require(DirectMemoryAlloc(0, DIRECT_MEMORY_SIZE, PS5_PAGE_SIZE, PS5_PAGE_SIZE, 0, &physical) == 0, "prepare physical allocation failed"); + Require(DoMapDirect(&mapping, PS5_PAGE_SIZE, 3, 0, physical, PS5_PAGE_SIZE) == 0, "prepare direct mapping failed"); + CheckPhysical(physical); + CheckMapping(mapping, physical); + std::memset(mapping, 0x6d, PS5_PAGE_SIZE); + watched = GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix(); + if (watched) Require(GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast(mapping), PS5_PAGE_SIZE), "prepare write watch missing"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "memory lifetime prepare: %s\n", error.what()); + return 1; + } +} + +extern "C" int GuestMemoryLifetimeExercise() { + const char* stage = "physical allocation query"; + try { + CheckPhysical(physical); + stage = "backing mapping query"; + CheckMapping(mapping, physical); + Require(static_cast(mapping)[0] == 0x6d, "physical data changed during exit"); + stage = "write watch"; + if (watched) { + Require(GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast(mapping), PS5_PAGE_SIZE), "write watch lost a live mapping during exit"); + Require(GuestWriteWatch::GuestWriteWatchCollect_nid_postfix(reinterpret_cast(mapping), PS5_PAGE_SIZE, IgnoreWritten, nullptr), "write watch collection failed during exit"); + static_cast(mapping)[0] = 0x2a; + } + stage = "mapping protection"; + Require(DoMprotect(mapping, PS5_PAGE_SIZE, 1) == 0, "late read-only protection failed"); + int protection = -1; + Require(GuestProtection(reinterpret_cast(mapping), &protection) && protection == 1, "late protection registry failed"); + Require(DoMprotect(mapping, PS5_PAGE_SIZE, 3) == 0, "late read/write protection failed"); + stage = "explicit unmap and free"; + Require(DoMunmap(mapping, PS5_PAGE_SIZE) == 0, "late direct unmap failed"); + CheckUnmapped(mapping); + DirectMemoryFree(physical, PS5_PAGE_SIZE); + std::int64_t start = -1, end = -1; + int type = -1; + Require(!DirectMemoryFind(physical, false, &start, &end, &type), "explicit physical free did not remove the allocation"); + + stage = "physical offset reuse"; + std::int64_t reused = -1; + Require(DirectMemoryAlloc(physical, physical + PS5_PAGE_SIZE, PS5_PAGE_SIZE, PS5_PAGE_SIZE, 0, &reused) == 0 && reused == physical, "late physical allocation failed"); + mapping = nullptr; + Require(DoMapDirect(&mapping, PS5_PAGE_SIZE, 3, 0, reused, PS5_PAGE_SIZE) == 0, "late direct mapping failed"); + CheckPhysical(reused); + CheckMapping(mapping, reused); + const auto* bytes = static_cast(mapping); + for (std::size_t i = 0; i < PS5_PAGE_SIZE; ++i) Require(bytes[i] == 0, "reused physical backing was not zeroed"); + std::memset(mapping, 0x17, PS5_PAGE_SIZE); + Require(DoMunmap(mapping, PS5_PAGE_SIZE) == 0, "late reused mapping unmap failed"); + CheckUnmapped(mapping); + DirectMemoryFree(reused, PS5_PAGE_SIZE); + + stage = "late arena reservation"; + void* reserved = nullptr; + Require(DoReserveVirtual(&reserved, PS5_PAGE_SIZE, 0, PS5_PAGE_SIZE) == 0, "late arena reservation failed"); + std::uintptr_t begin = 0, finish = 0; + Require(GuestReservation(reinterpret_cast(reserved), &begin, &finish) && begin == reinterpret_cast(reserved) && finish == begin + PS5_PAGE_SIZE, "late reservation registry failed"); + Require(DoMunmap(reserved, PS5_PAGE_SIZE) == 0, "late reserved mapping unmap failed"); + Require(!GuestReservation(reinterpret_cast(reserved), &begin, &finish), "explicit unmap did not remove the reservation"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "memory lifetime exit probe (%s): %s\n", stage, error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestMessageDestructorAbi.S b/core/libs/tests/GuestMessageDestructorAbi.S index 57ac11e827..7330b86885 100644 --- a/core/libs/tests/GuestMessageDestructorAbi.S +++ b/core/libs/tests/GuestMessageDestructorAbi.S @@ -141,7 +141,33 @@ ProbeDestroy: .cfi_def_cfa %rsp, 8 ret .cfi_endproc +.globl GuestCleanup +GuestCleanup: + .cfi_startproc + .cfi_personality 0x00, __gcc_personality_v0_nid_postfix + .cfi_lsda 0x00, .Lcleanup_lsda + PROLOGUE +.Lcleanup_call: + call FixtureHostThrow +.Lcleanup_call_end: + ud2 +.Lcleanup_landing: + incl FixtureGuardDestroyed(%rip) + movq %rax, %rdi + call _Unwind_Resume_nid_postfix + ud2 + .cfi_endproc + .section .rdata,"dr" +.Lcleanup_lsda: + .byte 0xff, 0xff, 0x01 + .uleb128 .Lcleanup_calls_end-.Lcleanup_calls +.Lcleanup_calls: + .uleb128 .Lcleanup_call-GuestCleanup + .uleb128 .Lcleanup_call_end-.Lcleanup_call + .uleb128 .Lcleanup_landing-GuestCleanup + .uleb128 0 +.Lcleanup_calls_end: .Linner_lsda: .byte 0xff, 0x00 .uleb128 .Linner_types_end-.Linner_types_offset diff --git a/core/libs/tests/GuestMessageDestructorAbi.cpp b/core/libs/tests/GuestMessageDestructorAbi.cpp index edf1e8cd41..8c55dbc490 100644 --- a/core/libs/tests/GuestMessageDestructorAbi.cpp +++ b/core/libs/tests/GuestMessageDestructorAbi.cpp @@ -20,12 +20,19 @@ 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 GuestCleanup(); void APS5_VABI GuestStdCapture(); void APS5_VABI ProbeDestroy(GuestDestroy, void*, void*); const char* APS5_VABI ProbeWhat(GuestWhat, const void*, const void*); @@ -196,9 +203,46 @@ extern "C" void APS5_VABI FixtureInspect(void* pointer) { if (!regularRelease) header->destructor = reinterpret_cast(ObserveDestroy); } +static unsigned hybridDestroyed; +struct HybridGuard { ~HybridGuard() { ++hybridDestroyed; } }; + +extern "C" [[gnu::noinline, noreturn]] void APS5_VABI FixtureHostThrow() { + HybridGuard guard; + __asm__ volatile("pxor %%xmm12, %%xmm12" ::: "xmm12"); + throw 73; +} + +[[gnu::noinline]] static void HybridMiddle() { + HybridGuard guard; + GuestCleanup(); +} + +static int TestHybridUnwind() { + const std::uint64_t expected[2] {0x123456789abcdef0, 0xfedcba9876543210}; + std::uint64_t restored[2] {}; + __asm__ volatile("movdqu %0, %%xmm12" :: "m"(expected) : "xmm12"); + unsigned caught = 0; + try { + HybridGuard guard; + try { HybridMiddle(); } + catch (int value) { + if (value != 73 || hybridDestroyed != 2 || FixtureGuardDestroyed != 1) return 63; + ++caught; + throw; + } + } catch (int value) { + if (value != 73) return 64; + ++caught; + } + __asm__ volatile("movdqu %%xmm12, %0" : "=m"(restored)); + return caught == 2 && hybridDestroyed == 3 && FixtureGuardDestroyed == 1 && + std::memcmp(restored, expected, sizeof(expected)) == 0 ? 0 : 65; +} + int main(int argc, char** argv) { SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); if (argc != 2) return 2; + if (std::strcmp(argv[1], "hybrid") == 0) return TestHybridUnwind(); if (std::strncmp(argv[1], "vtable-", 7) == 0) return TestVtable(argv[1]); if (std::strcmp(argv[1], "control") == 0) { if (GuestOuter() != 0 || FixtureDestroyed != 1 || FixtureGuardDestroyed != 1 || @@ -206,25 +250,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/GuestMktime.cpp b/core/libs/tests/GuestMktime.cpp index beaf273370..97d72aeaa9 100644 --- a/core/libs/tests/GuestMktime.cpp +++ b/core/libs/tests/GuestMktime.cpp @@ -13,6 +13,7 @@ std::int64_t APS5_VABI libc_mktime_nid_postfix(GuestTm*); std::int64_t APS5_VABI mktime_nid_postfix(GuestTm*); GuestTm* APS5_VABI localtime_s_nid_postfix(const std::int64_t*, GuestTm*); void APS5_VABI tzset_nid_postfix(); +int APS5_VABI setenv_nid_postfix(const char*, const char*, int); } using Converter = std::int64_t (APS5_VABI *)(GuestTm*); @@ -64,11 +65,7 @@ static void CheckFailure(Converter convert) { } static void CheckZone(const char* zone) { -#ifdef _WIN32 - Require(_putenv_s("TZ", zone) == 0, "Could not set the time zone"); -#else - Require(setenv("TZ", zone, 1) == 0, "Could not set the time zone"); -#endif + Require(setenv_nid_postfix("TZ", zone, 1) == 0, "Could not set the time zone"); tzset_nid_postfix(); constexpr std::array converters{libc_mktime_nid_postfix, mktime_nid_postfix}; constexpr std::array timers{-86401, -2, -1, 0, 1, 59, 60, 86400}; diff --git a/core/libs/tests/GuestMlock.cpp b/core/libs/tests/GuestMlock.cpp index 71ed127626..79b571e9b2 100644 --- a/core/libs/tests/GuestMlock.cpp +++ b/core/libs/tests/GuestMlock.cpp @@ -22,6 +22,7 @@ int APS5_VABI sceKernelMunmap(void*, std::size_t); int APS5_VABI mlock_nid_postfix(const void*, std::size_t); int APS5_VABI munlock_nid_postfix(const void*, std::size_t); int APS5_VABI mlockall_nid_postfix(int); +int APS5_VABI munlockall_nid_postfix(); int* APS5_VABI __error_nid_postfix(); } @@ -42,7 +43,8 @@ static void RequireLockAllUnsupported(int flags) { Require(rejected); } -#if defined(__linux__) +#if defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) +#define HAS_LOCKED_KILOBYTES static std::size_t LockedKilobytes() { std::ifstream status("/proc/self/status"); std::string line; @@ -68,11 +70,11 @@ int main() { *__error_nid_postfix() = untouched; Require(mlock_nid_postfix(mapped, 0) == 0); Require(*__error_nid_postfix() == untouched); -#if defined(__linux__) +#ifdef HAS_LOCKED_KILOBYTES const auto lockedBefore = LockedKilobytes(); #endif Require(mlock_nid_postfix(bytes + 1, length - page) == 0); -#if defined(__linux__) +#ifdef HAS_LOCKED_KILOBYTES Require(LockedKilobytes() - lockedBefore == length / 1024); #endif Require(mlock_nid_postfix(mapped, length) == 0); @@ -83,7 +85,7 @@ int main() { Require(munlock_nid_postfix(mapped, 0) == 0); Require(*__error_nid_postfix() == untouched); Require(munlock_nid_postfix(bytes + 1, length - page) == 0); -#if defined(__linux__) +#ifdef HAS_LOCKED_KILOBYTES Require(LockedKilobytes() == lockedBefore); #elif defined(_WIN32) Require(!VirtualUnlock(mapped, length) && GetLastError() == ERROR_NOT_LOCKED); @@ -93,18 +95,30 @@ int main() { Require(*__error_nid_postfix() == untouched); bytes[0] = 7; Require(mlock_nid_postfix(mapped, page) == 0); -#if defined(__linux__) +#ifdef HAS_LOCKED_KILOBYTES Require(LockedKilobytes() - lockedBefore == page / 1024); #endif Require(munlock_nid_postfix(mapped, length) == 0); -#if defined(__linux__) +#ifdef HAS_LOCKED_KILOBYTES Require(LockedKilobytes() == lockedBefore); #elif defined(_WIN32) Require(!VirtualUnlock(mapped, page) && GetLastError() == ERROR_NOT_LOCKED); #endif Require(mlock_nid_postfix(mapped, page) == 0); Require(munlock_nid_postfix(mapped, page) == 0); -#if defined(__linux__) +#ifdef HAS_LOCKED_KILOBYTES + Require(LockedKilobytes() == lockedBefore); +#elif defined(_WIN32) + Require(!VirtualUnlock(mapped, page) && GetLastError() == ERROR_NOT_LOCKED); +#endif + Require(mlock_nid_postfix(mapped, page) == 0); +#ifdef HAS_LOCKED_KILOBYTES + Require(LockedKilobytes() - lockedBefore == page / 1024); +#endif + *__error_nid_postfix() = untouched; + Require(munlockall_nid_postfix() == 0); + Require(*__error_nid_postfix() == untouched); +#ifdef HAS_LOCKED_KILOBYTES Require(LockedKilobytes() == lockedBefore); #elif defined(_WIN32) Require(!VirtualUnlock(mapped, page) && GetLastError() == ERROR_NOT_LOCKED); @@ -119,18 +133,22 @@ int main() { void* reserved = nullptr; Require(sceKernelReserveVirtualRange(&reserved, page, 0, 0) == 0); +#ifndef ANYPS5_ENABLE_ASAN *__error_nid_postfix() = 0; Require(mlock_nid_postfix(reserved, page) == -1 && *__error_nid_postfix() == 12); +#endif *__error_nid_postfix() = untouched; Require(munlock_nid_postfix(reserved, page) == 0); Require(*__error_nid_postfix() == untouched); Require(sceKernelMunmap(reserved, page) == 0); Require(sceKernelMunmap(mapped, length) == 0); +#ifndef ANYPS5_ENABLE_ASAN *__error_nid_postfix() = 0; Require(mlock_nid_postfix(mapped, page) == -1 && *__error_nid_postfix() == 12); *__error_nid_postfix() = 0; Require(munlock_nid_postfix(mapped, page) == -1 && *__error_nid_postfix() == 12); +#endif RequireLockAllInvalid(0); RequireLockAllInvalid(4); @@ -139,5 +157,9 @@ int main() { RequireLockAllUnsupported(1); RequireLockAllUnsupported(2); RequireLockAllUnsupported(1 | 2); + + *__error_nid_postfix() = untouched; + Require(munlockall_nid_postfix() == 0); + Require(*__error_nid_postfix() == untouched); return 0; } diff --git a/core/libs/tests/GuestModuleInfo.cpp b/core/libs/tests/GuestModuleInfo.cpp index 6c70763d2a..d596bf0280 100644 --- a/core/libs/tests/GuestModuleInfo.cpp +++ b/core/libs/tests/GuestModuleInfo.cpp @@ -3,10 +3,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include extern "C" { @@ -21,7 +23,12 @@ int APS5_VABI sceKernelGetModuleInfo2(KernelModule, ModuleInfo*); std::int32_t APS5_VABI sceKernelGetLibkernelTextLocation_nid_postfix(void**, std::size_t*); std::int32_t APS5_VABI sceKernelInternalMemoryGetModuleSegmentInfo_nid_postfix(InternalSegmentInfo*); } -static void Require(bool value) { if (!value) std::abort(); } +static void Require(bool value, const std::source_location& location = std::source_location::current()) { + if (!value) { + std::fprintf(stderr, "module info check failed at line %u\n", location.line()); + std::abort(); + } +} template static bool ThrowsInvalidArgument(TFunction function) { try { @@ -55,7 +62,11 @@ int main(int argc, char** argv) { executable = (segment.prot & 5) == 5; } Require(executable); +#if defined(_WIN32) && defined(__SEH__) + Require(info.eh_frame_addr == 0 && info.eh_frame_size == 0); +#else Require(info.eh_frame_addr != 0 && info.eh_frame_size != 0); +#endif #ifndef _WIN32 Require(info.eh_frame_hdr_addr != 0 && info.eh_frame_hdr_size != 0); #endif @@ -172,6 +183,8 @@ int main(int argc, char** argv) { Require(reloadedAdd != nullptr); const auto fresh = Query(reloadedAdd, 0); Require(fresh.eh_frame_size == info.eh_frame_size && fresh.eh_frame_hdr_size == info.eh_frame_hdr_size); - Require(fresh.eh_frame_addr - fresh.segments[0].address == info.eh_frame_addr - info.segments[0].address); + if (info.eh_frame_addr) + Require(fresh.eh_frame_addr - fresh.segments[0].address == info.eh_frame_addr - info.segments[0].address); + else Require(fresh.eh_frame_addr == 0); Require(dlclose_nid_postfix(reloaded) == 0); } diff --git a/core/libs/tests/GuestMove.cpp b/core/libs/tests/GuestMove.cpp new file mode 100644 index 0000000000..bbb55c541d --- /dev/null +++ b/core/libs/tests/GuestMove.cpp @@ -0,0 +1,152 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceMoveInit(void); +int APS5_VABI sceMoveTerm(void); +int APS5_VABI sceMoveOpen(std::int32_t userId, std::int32_t type, std::int32_t index); +int APS5_VABI sceMoveClose(std::int32_t handle); +int APS5_VABI sceMoveGetDeviceInfo(std::int32_t handle, void* info); +int APS5_VABI sceMoveGetExtensionPortInfo(std::int32_t handle, void* data); +int APS5_VABI sceMoveReadStateLatest(std::int32_t handle, void* data); +int APS5_VABI sceMoveReadStateRecent(std::int32_t handle, std::int64_t timestamp, void* data, std::int32_t* outCount); +int APS5_VABI sceMoveSetVibration(std::int32_t handle, std::uint8_t motor); +int APS5_VABI sceMoveSetLightSphere(std::int32_t handle, std::uint8_t red, std::uint8_t green, std::uint8_t blue); +int APS5_VABI sceMoveResetLightSphere(std::int32_t handle); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kOk = 0; +constexpr int kNoControllerConnected = 1; +constexpr int kNotInit = static_cast(0x80EE0001); +constexpr int kAlreadyInit = static_cast(0x80EE0002); +constexpr int kInvalidArg = static_cast(0x80EE0003); +constexpr int kInvalidHandle = static_cast(0x80EE0004); +constexpr int kAlreadyOpened = static_cast(0x80EE0007); +constexpr std::int32_t kUserId = 0x10000000; +constexpr std::int32_t kFirstHandle = 0x30B0100; +constexpr std::int32_t kHandleStep = 0x100; +constexpr std::uint8_t kFill = 0x5A; + +struct Buffer { + std::uint8_t bytes[128]; + + Buffer() { std::memset(bytes, kFill, sizeof(bytes)); } + + bool Untouched() const { + for (std::uint8_t byte : bytes) { + if (byte != kFill) return false; + } + return true; + } +}; + +template +bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +void RequireNotInit(std::int32_t handle) { + Buffer buffer; + std::int32_t count = -7; + Require(sceMoveOpen(kUserId, 0, 0) == kNotInit); + Require(sceMoveClose(handle) == kNotInit); + Require(sceMoveGetDeviceInfo(handle, buffer.bytes) == kNotInit); + Require(sceMoveGetExtensionPortInfo(handle, buffer.bytes) == kNotInit); + Require(sceMoveReadStateLatest(handle, buffer.bytes) == kNotInit); + Require(sceMoveReadStateRecent(handle, 0, buffer.bytes, &count) == kNotInit); + Require(sceMoveSetVibration(handle, 0xFF) == kNotInit); + Require(sceMoveSetLightSphere(handle, 0xFF, 0, 0) == kNotInit); + Require(sceMoveResetLightSphere(handle) == kNotInit); + Require(sceMoveTerm() == kNotInit); + Require(buffer.Untouched()); + Require(count == -7); +} + +void RequireUnknownHandle(std::int32_t handle) { + Buffer buffer; + std::int32_t count = -7; + Require(sceMoveClose(handle) == kInvalidHandle); + Require(sceMoveGetDeviceInfo(handle, buffer.bytes) == kInvalidHandle); + Require(sceMoveGetExtensionPortInfo(handle, buffer.bytes) == kInvalidHandle); + Require(sceMoveReadStateLatest(handle, buffer.bytes) == kInvalidHandle); + Require(sceMoveReadStateRecent(handle, 0, buffer.bytes, &count) == kInvalidHandle); + Require(sceMoveSetVibration(handle, 0xFF) == kInvalidHandle); + Require(sceMoveSetLightSphere(handle, 0xFF, 0, 0) == kInvalidHandle); + Require(sceMoveResetLightSphere(handle) == kInvalidHandle); + Require(sceMoveGetDeviceInfo(handle, nullptr) == kInvalidArg); + Require(sceMoveGetExtensionPortInfo(handle, nullptr) == kInvalidArg); + Require(sceMoveReadStateLatest(handle, nullptr) == kInvalidArg); + Require(sceMoveReadStateRecent(handle, -1, buffer.bytes, &count) == kInvalidArg); + Require(sceMoveReadStateRecent(handle, 0, buffer.bytes, nullptr) == kInvalidArg); + Require(buffer.Untouched()); + Require(count == -7); +} + +} + +int main() { + RequireNotInit(kFirstHandle); + + Require(sceMoveInit() == kOk); + Require(sceMoveInit() == kAlreadyInit); + + RequireUnknownHandle(kFirstHandle); + + const std::int32_t first = sceMoveOpen(kUserId, 0, 0); + const std::int32_t second = sceMoveOpen(kUserId, 0, 1); + Require(first == kFirstHandle); + Require(second == kFirstHandle + kHandleStep); + Require(sceMoveOpen(kUserId, 0, 0) == kAlreadyOpened); + Require(sceMoveOpen(kUserId, 0, 1) == kAlreadyOpened); + Require(Throws([] { sceMoveOpen(kUserId + 1, 0, 2); })); + + Require(sceMoveGetDeviceInfo(first, nullptr) == kInvalidArg); + Require(sceMoveGetExtensionPortInfo(first, nullptr) == kInvalidArg); + Require(sceMoveReadStateLatest(first, nullptr) == kInvalidArg); + + Buffer buffer; + std::int32_t count = -7; + Require(sceMoveReadStateRecent(first, -1, buffer.bytes, &count) == kInvalidArg); + Require(sceMoveReadStateRecent(first, 0, nullptr, &count) == kInvalidArg); + Require(sceMoveReadStateRecent(first, 0, buffer.bytes, nullptr) == kInvalidArg); + + Require(sceMoveGetDeviceInfo(first, buffer.bytes) == kNoControllerConnected); + Require(sceMoveGetExtensionPortInfo(first, buffer.bytes) == kNoControllerConnected); + Require(sceMoveReadStateLatest(first, buffer.bytes) == kNoControllerConnected); + Require(sceMoveReadStateRecent(first, 0, buffer.bytes, &count) == kNoControllerConnected); + Require(buffer.Untouched()); + Require(count == -7); + + Require(sceMoveSetVibration(first, 0xFF) == kNoControllerConnected); + Require(sceMoveSetLightSphere(first, 0, 0xFF, 0) == kNoControllerConnected); + Require(sceMoveResetLightSphere(first) == kOk); + + Require(sceMoveClose(first) == kOk); + RequireUnknownHandle(first); + const std::int32_t reopened = sceMoveOpen(kUserId, 0, 0); + Require(reopened == kFirstHandle + 2 * kHandleStep); + Require(sceMoveClose(reopened) == kOk); + Require(sceMoveSetVibration(second, 0) == kNoControllerConnected); + Require(sceMoveTerm() == kOk); + + RequireNotInit(second); + + Require(sceMoveInit() == kOk); + RequireUnknownHandle(second); + const std::int32_t third = sceMoveOpen(kUserId, 0, 0); + Require(third == kFirstHandle + 3 * kHandleStep); + Require(sceMoveClose(third) == kOk); + Require(sceMoveTerm() == kOk); +} diff --git a/core/libs/tests/GuestMprotectUnmapped.cpp b/core/libs/tests/GuestMprotectUnmapped.cpp new file mode 100644 index 0000000000..8a4ed05199 --- /dev/null +++ b/core/libs/tests/GuestMprotectUnmapped.cpp @@ -0,0 +1,88 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include "SceTypes.hpp" +#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; +int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); +int APS5_VABI sceKernelMtypeprotect(const void*, std::size_t, int, int); +int APS5_VABI sceKernelVirtualQuery(const void*, int, VirtualQueryInfo*, std::uint64_t); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr std::size_t Page = 0x4000; +constexpr int ErrorInvalid = 22; +constexpr int ProtectRead = 1; +constexpr int ProtectReadWrite = 3; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s\n", what); + std::abort(); +} + +int Protect(const void* address, std::size_t bytes, int protection) { + try { + return sceKernelMprotect(address, bytes, protection); + } catch (const std::exception& error) { + std::fprintf(stderr, "sceKernelMprotect threw: %s\n", error.what()); + std::abort(); + } +} + +int ProtectionAt(const unsigned char* address) { + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(address, 0, &info, sizeof(info)) == 0, "virtual query of a mapped page failed"); + return info.protection; +} + +unsigned char* Map(std::size_t pages) { + void* raw = mmap_nid_postfix(nullptr, Page * pages, ProtectReadWrite, 0x1002, -1, 0); + Require(raw != reinterpret_cast(static_cast(-1)), "mmap failed"); + return static_cast(raw); +} + +} + +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"); + Require(ProtectionAt(memory) == ProtectReadWrite && ProtectionAt(memory + Page * 2) == ProtectReadWrite, "mprotect must change the mapped parts on both sides of the gap"); + Require(sceKernelMtypeprotect(memory, Page * 3, 0, ProtectRead) == 0, "sceKernelMtypeprotect over a range with a gap must succeed for its mapped parts"); + + Require(Protect(memory + Page - 8, 16, ProtectReadWrite) == 0, "an unaligned range that reaches into the unmapped page protects the mapped page"); + Require(Protect(memory + 100, Page - 200, ProtectReadWrite) == 0, "an unaligned range inside the first page must work"); + + Require(Protect(memory + Page, Page, ProtectRead) == SCE_KERNEL_ERROR_EINVAL, "a range that is completely unmapped must return EINVAL"); + Require(Protect(memory + Page * 2, Page, ProtectReadWrite) == 0, "protecting the last page alone must work"); + Require(Protect(memory + Page, Page * 2, ProtectRead) == SCE_KERNEL_ERROR_EINVAL, "a range whose start is unmapped must return EINVAL even when later pages are mapped"); + Require(ProtectionAt(memory + Page * 2) == ProtectReadWrite, "a refused range must not change the mapped pages behind its start"); + Require(mprotect_nid_postfix(memory + Page, Page, ProtectRead) == -1 && *__error_nid_postfix() == ErrorInvalid, "mprotect over a range that is completely unmapped must fail with EINVAL (22)"); + + Require(munmap_nid_postfix(memory, Page) == 0 && munmap_nid_postfix(memory + Page * 2, Page) == 0, "munmap of the remaining pages failed"); + + unsigned char* tail = Map(2); + Require(munmap_nid_postfix(tail + Page, Page) == 0, "munmap of the last page failed"); + Require(Protect(tail, Page * 2, ProtectRead) == 0, "a range that ends in an unmapped page must succeed for its mapped part"); + Require(ProtectionAt(tail) == ProtectRead, "the mapped part before a trailing gap must be protected"); + Require(munmap_nid_postfix(tail, Page) == 0, "munmap of the remaining page failed"); + return 0; +} diff --git a/core/libs/tests/GuestMsgDialogNative.cpp b/core/libs/tests/GuestMsgDialogNative.cpp index dd43d10b4c..8842c84299 100644 --- a/core/libs/tests/GuestMsgDialogNative.cpp +++ b/core/libs/tests/GuestMsgDialogNative.cpp @@ -4,6 +4,7 @@ #include extern "C" { +extern int SceMsgDialogNativeModuleLoaded_nid_no_patch; int APS5_VABI sceMsgDialogInitialize(void); int APS5_VABI sceMsgDialogOpen(const void* param); int APS5_VABI sceMsgDialogGetStatus(void); @@ -22,20 +23,33 @@ void Require(bool value) { if (!value) std::abort(); } constexpr int kStatusInitialized = 1; constexpr int kStatusFinished = 3; +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004u); +constexpr int kErrNotInitialized = static_cast(0x80B80003u); +constexpr int kErrNotFinished = static_cast(0x80B80005u); constexpr int kErrNotRunning = static_cast(0x80B8000Bu); +constexpr int kErrArgNull = static_cast(0x80B8000Du); constexpr int kButtonIdOk = 1; } int main() { + Require(SceMsgDialogNativeModuleLoaded_nid_no_patch == 1); std::uint8_t param[0x60]{}; + MsgDialogResult result{}; + Require(sceMsgDialogTerminate() == kErrNotInitialized); Require(sceMsgDialogClose() == kErrNotRunning); + Require(sceMsgDialogOpen(param) == kErrNotInitialized); + Require(sceMsgDialogGetResult(&result) == kErrNotInitialized); Require(sceMsgDialogUpdateStatus() == 0); Require(sceMsgDialogGetStatus() == 0); Require(sceMsgDialogInitialize() == 0); + Require(sceMsgDialogInitialize() == kErrAlreadyInitialized); Require(sceMsgDialogClose() == kErrNotRunning); + Require(sceMsgDialogGetResult(&result) == kErrNotFinished); + Require(sceMsgDialogGetResult(nullptr) == kErrArgNull); + Require(sceMsgDialogOpen(nullptr) == kErrArgNull); Require(sceMsgDialogUpdateStatus() == kStatusInitialized); Require(sceMsgDialogGetStatus() == kStatusInitialized); @@ -50,13 +64,16 @@ int main() { Require(sceMsgDialogUpdateStatus() == kStatusFinished); Require(sceMsgDialogGetStatus() == kStatusFinished); - MsgDialogResult result{}; + result = MsgDialogResult{}; result.button_id = -1; Require(sceMsgDialogGetResult(&result) == 0); Require(result.result == 0); Require(result.button_id == kButtonIdOk); Require(sceMsgDialogTerminate() == 0); + Require(sceMsgDialogTerminate() == kErrNotInitialized); + Require(sceMsgDialogOpen(param) == kErrNotInitialized); + Require(sceMsgDialogGetResult(&result) == kErrNotInitialized); Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogGetStatus() == 0); } diff --git a/core/libs/tests/GuestNanosleepLongRequest.cpp b/core/libs/tests/GuestNanosleepLongRequest.cpp new file mode 100644 index 0000000000..b2b6ec9cd6 --- /dev/null +++ b/core/libs/tests/GuestNanosleepLongRequest.cpp @@ -0,0 +1,58 @@ +#include "SceTypes.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI nanosleep_nid_postfix(const KernelTimespec* rqtp, KernelTimespec* rmtp); +int APS5_VABI sceKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp); +} + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "%s:%u: requirement failed\n", location.file_name(), location.line()); + std::abort(); +} + +static constexpr auto ObservationTime = std::chrono::milliseconds(1500); +static std::atomic started{false}; +static std::atomic returned{false}; +static std::int64_t requestedSeconds = 0; +static bool posix = true; + +static void* APS5_VABI Sleeper(void*) { + const KernelTimespec request{requestedSeconds, 0}; + started.store(true); + if (posix) nanosleep_nid_postfix(&request, nullptr); + else sceKernelNanosleep(&request, nullptr); + returned.store(true); + return nullptr; +} + +static void SleepsPastTheObservation(std::int64_t seconds, bool usePosix) { + started.store(false); + returned.store(false); + requestedSeconds = seconds; + posix = usePosix; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Sleeper, nullptr, nullptr) == 0); + while (!started.load()) std::this_thread::yield(); + std::this_thread::sleep_for(ObservationTime); + Require(!returned.load()); +} + +int main() { + for (const bool usePosix : {true, false}) { + SleepsPastTheObservation(INT64_MAX, usePosix); + SleepsPastTheObservation(18446744074LL, usePosix); + SleepsPastTheObservation(std::int64_t{1} << 40, usePosix); + } + std::fflush(nullptr); + std::_Exit(0); +} diff --git a/core/libs/tests/GuestNetBufferValidation.cpp b/core/libs/tests/GuestNetBufferValidation.cpp new file mode 100644 index 0000000000..4b2c66402d --- /dev/null +++ b/core/libs/tests/GuestNetBufferValidation.cpp @@ -0,0 +1,81 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" + +extern "C" { +int APS5_VABI sceNetInit_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetSocketClose(int); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); +int* APS5_VABI sceNetErrnoLoc(); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "check failed at line %u\n", location.line()); + std::abort(); + } +} + +int main() { + Require(sceNetInit_nid_postfix() == 0); + for (const int family : {2, 28}) { + for (const int type : {1, 2}) { + const int socket = sceNetSocket("buffer-validation", family, type, type == 1 ? 6 : 17); + Require(socket >= 0); + std::shared_ptr owner; + std::uintptr_t native = 0; + Require(KernelFindSocketNative_nid_no_patch(socket, &owner, &native)); + for (const int option : {0x1001, 0x1002}) { + const auto nativeBuffer = [&] { + int value = 0; +#ifdef _WIN32 + int size = sizeof(value); +#else + socklen_t size = sizeof(value); +#endif + Require(::getsockopt(native, SOL_SOCKET, option == 0x1001 ? SO_SNDBUF : SO_RCVBUF, + reinterpret_cast(&value), &size) == 0); + return value; + }; + const int initial = 32768; + Require(sceNetSetsockopt(socket, 0xffff, option, &initial, sizeof(initial)) == 0); + const int originalNative = nativeBuffer(); + for (const int invalid : {0, -1, -4096, INT_MIN}) { + Require(sceNetSetsockopt(socket, 0xffff, option, &invalid, sizeof(invalid)) == static_cast(0x80410116u)); + Require(*sceNetErrnoLoc() == 22); + Require(setsockopt_nid_postfix(socket, 0xffff, option, &invalid, sizeof(invalid)) == -1); + Require(*__error_nid_postfix() == 22); + int value = 0; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, option, &value, &size) == 0 && value == initial); + Require(nativeBuffer() == originalNative); + } + Require(sceNetSetsockopt(socket, 0xffff, option, nullptr, sizeof(initial)) == static_cast(0x80410116u)); + Require(sceNetSetsockopt(socket, 0xffff, option, &initial, 3) == static_cast(0x80410116u)); + const int valid = 8192; + Require(sceNetSetsockopt(socket, 0xffff, option, &valid, sizeof(valid)) == 0); + int value = 0; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, option, &value, &size) == 0 && value == valid); + } + Require(sceNetSocketClose(socket) == 0); + } + } + const int zero = 0; + Require(sceNetSetsockopt(-1, 0xffff, 0x1001, &zero, sizeof(zero)) == static_cast(0x80410109u)); + Require(*sceNetErrnoLoc() == 9); +} diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp new file mode 100644 index 0000000000..f45a66dc77 --- /dev/null +++ b/core/libs/tests/GuestNetCtl.cpp @@ -0,0 +1,379 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#else +#include +#include +#include +#include +#endif +#ifdef __APPLE__ +#include +#include +#include +#endif +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include + +#ifdef __APPLE__ +struct NetCtlDefaultRoute { + char name[IF_NAMESIZE]; + in_addr gateway; + std::uint32_t metric; +}; +#endif + +union NetCtlInfoV6; + +extern "C" { +int APS5_VABI sceNetCtlInit(void); +int APS5_VABI sceNetCtlCheckCallback(void); +int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); +int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo*); +int APS5_VABI sceNetCtlGetState(int*); +int APS5_VABI sceNetCtlGetStateV6(int*); +int APS5_VABI sceNetCtlGetInfoV6(std::uint32_t, NetCtlInfoV6*); +int APS5_VABI sceNetCtlGetResult(int, int*); +int APS5_VABI sceNetCtlGetResultV6(int, int*); +#ifdef __APPLE__ +std::size_t NetCtlDefaultRoutes_nid_no_patch(const char*, std::size_t, NetCtlDefaultRoute*, std::size_t); +#endif +int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback, void*, int*); +int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback, void*, int*); +void APS5_VABI sceNetCtlTerm(void); +int APS5_VABI sceNetCtlUnregisterCallback(int); +int APS5_VABI sceNetCtlUnregisterCallbackV6(int); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kErrInvalidCode = static_cast(0x80412106); +constexpr int kErrInvalidAddr = static_cast(0x80412107); +constexpr int kErrNotConnected = static_cast(0x80412108); +constexpr int kStateIpObtained = 3; +constexpr int kEventDisconnected = 1; +constexpr int kEventIpObtained = 3; +constexpr std::uint8_t kFill = 0xa5; +constexpr std::size_t kAddressLength = 16; + +struct Delivery { + int calls = 0; + int event = 0; + void* arg = nullptr; +}; + +Delivery g_first; +Delivery g_second; +Delivery g_v6; +Delivery g_self; +int g_selfUnregister = -1; + +void APS5_VABI RecordFirst(int event, void* arg) { + ++g_first.calls; + g_first.event = event; + g_first.arg = arg; +} + +void APS5_VABI RecordSecond(int event, void* arg) { + ++g_second.calls; + g_second.event = event; + g_second.arg = arg; +} + +void APS5_VABI RecordV6(int event, void* arg) { + ++g_v6.calls; + g_v6.event = event; + g_v6.arg = arg; +} + +void APS5_VABI UnregisterSelf(int event, void* arg) { + ++g_self.calls; + g_self.event = event; + g_self.arg = arg; + g_selfUnregister = sceNetCtlUnregisterCallback(*static_cast(arg)); +} + +void CheckCallbacks() { + int state = -1; + Require(sceNetCtlGetState(&state) == 0); + const int event = state == kStateIpObtained ? kEventIpObtained : kEventDisconnected; + int firstProbe = 0; + int secondProbe = 0; + int v6Probe = 0; + + int v6 = -1; + Require(sceNetCtlRegisterCallbackV6(RecordV6, &v6Probe, &v6) == 0 && v6 == 0); + + int first = -1; + Require(sceNetCtlRegisterCallback(RecordFirst, &firstProbe, &first) == 0 && first == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1 && g_first.event == event && g_first.arg == &firstProbe); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1); + + int second = -1; + Require(sceNetCtlRegisterCallback(RecordSecond, &secondProbe, &second) == 0 && second == 1); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1); + Require(g_second.calls == 1 && g_second.event == event && g_second.arg == &secondProbe); + + Require(sceNetCtlUnregisterCallback(second) == 0); + Require(sceNetCtlRegisterCallback(RecordSecond, &secondProbe, &second) == 0 && second == 1); + Require(sceNetCtlUnregisterCallback(second) == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1 && g_second.calls == 1); + + int self = -1; + Require(sceNetCtlRegisterCallback(UnregisterSelf, &self, &self) == 0 && self == 1); + Require(sceNetCtlCheckCallback() == 0); + Require(g_self.calls == 1 && g_self.event == event && g_self.arg == &self && g_selfUnregister == 0); + Require(g_first.calls == 1 && g_second.calls == 1); + Require(sceNetCtlCheckCallback() == 0); + Require(g_self.calls == 1); + + Require(sceNetCtlRegisterCallback(RecordSecond, &secondProbe, &second) == 0 && second == 1); + sceNetCtlTerm(); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1 && g_second.calls == 2); + Require(sceNetCtlInit() == 0); + Require(sceNetCtlUnregisterCallback(first) == 0); + Require(sceNetCtlUnregisterCallback(second) == 0); + Require(sceNetCtlUnregisterCallbackV6(v6) == 0); + Require(sceNetCtlUnregisterCallback(first) == static_cast(0x80412104)); + Require(sceNetCtlUnregisterCallbackV6(v6) == static_cast(0x80412104)); + + Require(sceNetCtlRegisterCallbackV6(RecordV6, &v6Probe, &v6) == 0 && v6 == 0); + Require(sceNetCtlRegisterCallback(RecordFirst, &secondProbe, &first) == 0 && first == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 2 && g_first.event == event && g_first.arg == &secondProbe); + Require(g_second.calls == 2 && g_self.calls == 1 && g_v6.calls == 0); + Require(sceNetCtlUnregisterCallback(first) == 0); + Require(sceNetCtlUnregisterCallbackV6(v6) == 0); +} + +NetCtlInfo Query(int code) { + NetCtlInfo info; + std::memset(&info, kFill, sizeof(info)); + Require(sceNetCtlGetInfo(code, &info) == 0); + return info; +} + +void RequireUntouchedFrom(const NetCtlInfo& info, std::size_t offset) { + for (std::size_t i = offset; i < sizeof(info.opaque); ++i) Require(info.opaque[i] == kFill); +} + +std::uint32_t QueryWord(int code) { + const auto info = Query(code); + std::uint32_t value = 0; + std::memcpy(&value, info.opaque, sizeof(value)); + RequireUntouchedFrom(info, sizeof(value)); + return value; +} + +in_addr QueryAddress(int code) { + const auto info = Query(code); + char text[kAddressLength]; + std::memcpy(text, info.opaque, sizeof(text)); + Require(std::memchr(text, '\0', sizeof(text)) != nullptr); + RequireUntouchedFrom(info, kAddressLength); + in_addr address{}; + Require(inet_pton(AF_INET, text, &address) == 1); + return address; +} + +bool Binds(in_addr address) { + sockaddr_in local{}; + local.sin_family = AF_INET; + local.sin_addr = address; +#ifdef _WIN32 + const SOCKET descriptor = socket(AF_INET, SOCK_DGRAM, 0); + Require(descriptor != INVALID_SOCKET); + const bool bound = bind(descriptor, reinterpret_cast(&local), sizeof(local)) == 0; + closesocket(descriptor); +#else + const int descriptor = socket(AF_INET, SOCK_DGRAM, 0); + Require(descriptor >= 0); + const bool bound = bind(descriptor, reinterpret_cast(&local), sizeof(local)) == 0; + close(descriptor); +#endif + return bound; +} + +} + +#ifdef __APPLE__ +void AppendRoute(std::vector& table, std::uint32_t destination, std::uint32_t gateway, int flags, unsigned short index) { + rt_msghdr header{}; + header.rtm_msglen = static_cast(sizeof(header) + 2 * sizeof(sockaddr_in) + sizeof(std::uint32_t)); + header.rtm_version = RTM_VERSION; + header.rtm_type = RTM_GET; + header.rtm_index = index; + header.rtm_flags = flags; + header.rtm_addrs = RTA_DST | RTA_GATEWAY | RTA_NETMASK; + sockaddr_in addresses[2]{}; + for (auto& address : addresses) { + address.sin_len = sizeof(address); + address.sin_family = AF_INET; + } + addresses[0].sin_addr.s_addr = htonl(destination); + addresses[1].sin_addr.s_addr = htonl(gateway); + const std::uint32_t emptyMask = 0; + const auto* bytes = reinterpret_cast(&header); + table.insert(table.end(), bytes, bytes + sizeof(header)); + bytes = reinterpret_cast(addresses); + table.insert(table.end(), bytes, bytes + sizeof(addresses)); + bytes = reinterpret_cast(&emptyMask); + table.insert(table.end(), bytes, bytes + sizeof(emptyMask)); +} + +void CheckDefaultRoutes() { + const auto loopback = static_cast(if_nametoindex("lo0")); + Require(loopback != 0); + constexpr int up = RTF_UP | RTF_GATEWAY | RTF_STATIC; + std::vector table; + AppendRoute(table, 0, 0x0A000001, up, loopback); + AppendRoute(table, 0x01020304, 0x0A000009, up | RTF_HOST, loopback); + AppendRoute(table, 0, 0x0A000009, up | RTF_HOST, loopback); + AppendRoute(table, 0, 0x0A000009, up | RTF_REJECT, loopback); + AppendRoute(table, 0, 0x0A000002, up | RTF_IFSCOPE, loopback); + AppendRoute(table, 0, 0x0A000009, RTF_GATEWAY | RTF_STATIC, loopback); + AppendRoute(table, 0, 0x0A000009, up, 0xFFFF); + const std::size_t complete = table.size(); + AppendRoute(table, 0, 0x0A000009, up, loopback); + table.resize(table.size() - sizeof(std::uint32_t)); + + NetCtlDefaultRoute routes[4]{}; + Require(NetCtlDefaultRoutes_nid_no_patch(table.data(), table.size(), routes, 4) == 2); + Require(std::strcmp(routes[0].name, "lo0") == 0 && ntohl(routes[0].gateway.s_addr) == 0x0A000001 && routes[0].metric == 0); + Require(std::strcmp(routes[1].name, "lo0") == 0 && ntohl(routes[1].gateway.s_addr) == 0x0A000002 && routes[1].metric == 1); + + NetCtlDefaultRoute first[1]{}; + Require(NetCtlDefaultRoutes_nid_no_patch(table.data(), complete, first, 1) == 2); + Require(ntohl(first[0].gateway.s_addr) == 0x0A000001); + Require(NetCtlDefaultRoutes_nid_no_patch(table.data(), 0, nullptr, 0) == 0); +} +#endif + +int main() { +#ifdef _WIN32 + WSADATA data{}; + Require(WSAStartup(MAKEWORD(2, 2), &data) == 0); +#endif +#ifdef __APPLE__ + CheckDefaultRoutes(); +#endif + constexpr int invalidId = static_cast(0x80412105); + Require(sceNetCtlCheckCallback() == 0); + Require(sceNetCtlGetState(nullptr) == kErrInvalidAddr); + Require(sceNetCtlGetStateV6(nullptr) == kErrInvalidAddr); + Require(sceNetCtlGetInfo(0, nullptr) == kErrInvalidAddr); + Require(sceNetCtlGetResult(0, nullptr) == kErrInvalidAddr); + Require(sceNetCtlGetNatInfo(nullptr) == kErrInvalidAddr); + Require(sceNetCtlRegisterCallback(nullptr, nullptr, nullptr) == kErrInvalidAddr); + Require(sceNetCtlRegisterCallbackV6(nullptr, nullptr, nullptr) == kErrInvalidAddr); + Require(sceNetCtlUnregisterCallback(-1) == invalidId); + Require(sceNetCtlUnregisterCallbackV6(-1) == invalidId); + CheckCallbacks(); + + Require(sceNetCtlGetStateV6(nullptr) == kErrInvalidAddr); + int stateV6 = -1; + Require(sceNetCtlGetStateV6(&stateV6) == 0); + Require(stateV6 == 0 || stateV6 == kStateIpObtained); + Require(sceNetCtlGetResult(0, nullptr) == kErrInvalidAddr); + int errorCode = -1; + bool resultUnsupported = false; + try { + sceNetCtlGetResult(0, &errorCode); + } catch (const std::logic_error&) { + resultUnsupported = true; + } + Require(resultUnsupported && errorCode == -1); + Require(sceNetCtlGetResultV6(kEventIpObtained, nullptr) == kErrInvalidAddr); + for (const int eventType : {kEventDisconnected, 2, kEventIpObtained}) { + int errorCodeV6 = -1; + Require(sceNetCtlGetResultV6(eventType, &errorCodeV6) == 0 && errorCodeV6 == 0); + } + for (const int eventType : {0, 4, -1}) { + int errorCodeV6 = -1; + bool threw = false; + try { sceNetCtlGetResultV6(eventType, &errorCodeV6); } catch (const std::runtime_error&) { threw = true; } + Require(threw && errorCodeV6 == -1); + } + NetCtlInfo info{}; + Require(sceNetCtlGetInfo(0, &info) == kErrInvalidCode); + Require(sceNetCtlGetInfo(22, &info) == kErrInvalidCode); + Require(sceNetCtlGetInfo(14, nullptr) == kErrInvalidAddr); + + std::uint8_t infoV6[256] = {}; + Require(sceNetCtlGetInfoV6(0, reinterpret_cast(infoV6)) == kErrInvalidCode); + Require(sceNetCtlGetInfoV6(6, reinterpret_cast(infoV6)) == kErrInvalidCode); + Require(sceNetCtlGetInfoV6(1, nullptr) == kErrInvalidAddr); + if (stateV6 != kStateIpObtained) Require(sceNetCtlGetInfoV6(1, reinterpret_cast(infoV6)) == kErrNotConnected); + + const int result = sceNetCtlGetInfo(14, &info); + if (result == kErrNotConnected) { + std::puts("skipped, the host has no IPv4 address"); + return 77; + } + Require(result == 0); + int state = -1; + Require(sceNetCtlGetState(&state) == 0 && state == kStateIpObtained); + + Require(QueryWord(1) == 0); + const auto ether = Query(2); + const std::uint8_t mac[6] = {0x02, 0x50, 0x53, 0x35, 0x00, 0x01}; + Require(std::memcmp(ether.opaque, mac, sizeof(mac)) == 0); + RequireUntouchedFrom(ether, sizeof(mac)); + const auto mtu = QueryWord(3); + Require(mtu >= 68 && mtu <= 65535); + 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); + Require(host != 0 && (host >> 24) != 127); + Require(Binds(address)); + const auto mask = ntohl(QueryAddress(15).s_addr); + Require(mask != 0 && (~mask & (~mask + 1)) == 0); + QueryAddress(16); + QueryAddress(17); + QueryAddress(18); + + for (const int code : {5, 6, 12, 13, 20}) { + std::memset(&info, 0x5a, sizeof(info)); + Require(sceNetCtlGetInfo(code, &info) == 0 && info.opaque[0] == 0); + } + + for (const int code : {7, 8, 9, 10}) { + Require(QueryWord(code) == 0); + } + + Require(sceNetCtlGetNatInfo(nullptr) == kErrInvalidAddr); + NetCtlNatInfo natInfo{}; + for (const auto size : {0u, 15u, 17u, 128u, 0xffffffffu}) { + natInfo.size = size; + Require(sceNetCtlGetNatInfo(&natInfo) == static_cast(0x80412111)); + } + natInfo.size = sizeof(natInfo); + std::memset(natInfo.opaque, kFill, sizeof(natInfo.opaque)); + bool natUnsupported = false; + try { + sceNetCtlGetNatInfo(&natInfo); + } catch (const std::logic_error&) { + natUnsupported = true; + } + Require(natUnsupported && natInfo.size == sizeof(natInfo)); + for (const auto value : natInfo.opaque) Require(value == kFill); +} diff --git a/core/libs/tests/GuestNetCtlAp.cpp b/core/libs/tests/GuestNetCtlAp.cpp new file mode 100644 index 0000000000..3689260ee4 --- /dev/null +++ b/core/libs/tests/GuestNetCtlAp.cpp @@ -0,0 +1,139 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNetCtlApInit(void); +void APS5_VABI sceNetCtlApTerm(void); +int APS5_VABI sceNetCtlApCheckCallback(void); +int APS5_VABI sceNetCtlApClearEvent(int eventType); +int APS5_VABI sceNetCtlApRegisterCallback(void (APS5_VABI *func)(int, void*), void* arg, int* cid); +int APS5_VABI sceNetCtlApUnregisterCallback(int cid); +int APS5_VABI sceNetCtlApGetResult(int eventType, int* errorCode); +int APS5_VABI sceNetCtlApGetInfo(void* info); +int APS5_VABI sceNetCtlApGetState(int* state); +int APS5_VABI sceNetCtlApStop(void); +int APS5_VABI sceNetCtlApGetConnectInfo(const char* optStr, void* connectInfo); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kErrNotStarted = static_cast(0x8041298F); +constexpr int kErrInvalidType = static_cast(0x80412990); +constexpr int kErrInvalidAddr = static_cast(0x80412991); +constexpr int kErrNotInitialized = static_cast(0x80412994); +constexpr int kErrCallbackMax = static_cast(0x80412995); +constexpr int kErrInvalidId = static_cast(0x80412996); +constexpr int kErrIdNotFound = static_cast(0x80412997); +constexpr int kErrInvalidSize = static_cast(0x80412998); + +int g_calls = 0; + +void APS5_VABI Callback(int, void*) { ++g_calls; } + +void RequireUninitialized() { + int value = 0x5a5a; + std::uint8_t block[184] = {}; + Require(sceNetCtlApCheckCallback() == kErrNotInitialized); + Require(sceNetCtlApClearEvent(1) == kErrNotInitialized); + Require(sceNetCtlApRegisterCallback(Callback, nullptr, &value) == kErrNotInitialized && value == 0x5a5a); + Require(sceNetCtlApUnregisterCallback(0) == kErrNotInitialized); + Require(sceNetCtlApGetResult(1, &value) == kErrNotInitialized && value == 0x5a5a); + Require(sceNetCtlApGetInfo(block) == kErrNotInitialized); + Require(sceNetCtlApGetState(&value) == kErrNotInitialized && value == 0x5a5a); + Require(sceNetCtlApStop() == kErrNotInitialized); + Require(sceNetCtlApGetConnectInfo(nullptr, block) == kErrNotInitialized); +} + +} + +int main() { + RequireUninitialized(); + Require(sceNetCtlApInit() == 0); + + int state = -1; + Require(sceNetCtlApGetState(&state) == 0 && state == 0); + Require(sceNetCtlApGetState(nullptr) == kErrInvalidAddr); + + std::uint8_t info[152]; + std::memset(info, 0x5a, sizeof(info)); + const std::uint64_t infoSize = sizeof(info); + std::memcpy(info, &infoSize, sizeof(infoSize)); + Require(sceNetCtlApGetInfo(info) == kErrNotStarted); + for (std::size_t i = sizeof(infoSize); i < sizeof(info); ++i) Require(info[i] == 0x5a); + Require(sceNetCtlApGetInfo(nullptr) == kErrInvalidAddr); + const std::uint64_t wrongInfoSize = sizeof(info) - 8; + std::memcpy(info, &wrongInfoSize, sizeof(wrongInfoSize)); + Require(sceNetCtlApGetInfo(info) == kErrInvalidSize); + + std::uint8_t connect[184]; + std::memset(connect, 0x5a, sizeof(connect)); + const std::uint64_t connectSize = sizeof(connect); + std::memcpy(connect, &connectSize, sizeof(connectSize)); + const char option[] = "opt"; + Require(sceNetCtlApGetConnectInfo(option, connect) == kErrNotStarted); + Require(sceNetCtlApGetConnectInfo(nullptr, connect) == kErrNotStarted); + Require(sceNetCtlApGetConnectInfo("", connect) == kErrNotStarted); + Require(sceNetCtlApGetConnectInfo("12345678", connect) == kErrNotStarted); + Require(sceNetCtlApGetConnectInfo("123456789", nullptr) == static_cast(0x80412901u)); + Require(sceNetCtlApGetConnectInfo("123456789", connect) == static_cast(0x80412901u)); + for (std::size_t i = sizeof(connectSize); i < sizeof(connect); ++i) Require(connect[i] == 0x5a); + Require(sceNetCtlApGetConnectInfo(option, nullptr) == kErrInvalidAddr); + const std::uint64_t wrongConnectSize = sizeof(info); + std::memcpy(connect, &wrongConnectSize, sizeof(wrongConnectSize)); + Require(sceNetCtlApGetConnectInfo(option, connect) == kErrInvalidSize); + + Require(sceNetCtlApStop() == kErrNotStarted); + + for (int type = 1; type <= 4; ++type) { + int code = -1; + Require(sceNetCtlApClearEvent(type) == 0); + Require(sceNetCtlApGetResult(type, &code) == 0 && code == 0); + Require(sceNetCtlApGetResult(type, nullptr) == kErrInvalidAddr); + } + for (const int type : {0, 5, -1}) { + int code = 0x5a5a; + Require(sceNetCtlApClearEvent(type) == kErrInvalidType); + Require(sceNetCtlApGetResult(type, &code) == kErrInvalidType && code == 0x5a5a); + Require(sceNetCtlApGetResult(type, nullptr) == kErrInvalidAddr); + } + + int cids[8]; + for (int i = 0; i < 8; ++i) { + cids[i] = -1; + Require(sceNetCtlApRegisterCallback(Callback, &cids[i], &cids[i]) == 0 && cids[i] == i); + } + int extra = -1; + Require(sceNetCtlApRegisterCallback(Callback, nullptr, &extra) == kErrCallbackMax && extra == -1); + Require(sceNetCtlApRegisterCallback(nullptr, nullptr, &extra) == kErrInvalidAddr); + Require(sceNetCtlApRegisterCallback(Callback, nullptr, nullptr) == kErrInvalidAddr); + Require(sceNetCtlApCheckCallback() == 0 && sceNetCtlApCheckCallback() == 0); + Require(g_calls == 0); + Require(sceNetCtlApUnregisterCallback(3) == 0); + Require(sceNetCtlApUnregisterCallback(3) == kErrIdNotFound); + Require(sceNetCtlApUnregisterCallback(-1) == kErrInvalidId); + Require(sceNetCtlApUnregisterCallback(8) == kErrInvalidId); + Require(sceNetCtlApRegisterCallback(Callback, nullptr, &extra) == 0 && extra == 3); + + bool threw = false; + try { + sceNetCtlApInit(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + + sceNetCtlApTerm(); + RequireUninitialized(); + Require(sceNetCtlApInit() == 0); + int cid = -1; + Require(sceNetCtlApRegisterCallback(Callback, nullptr, &cid) == 0 && cid == 0); + Require(sceNetCtlApGetState(&state) == 0 && state == 0); + sceNetCtlApTerm(); + sceNetCtlApTerm(); + RequireUninitialized(); +} diff --git a/core/libs/tests/GuestNetCtlApDialog.cpp b/core/libs/tests/GuestNetCtlApDialog.cpp index bdce6ca102..f17c171e56 100644 --- a/core/libs/tests/GuestNetCtlApDialog.cpp +++ b/core/libs/tests/GuestNetCtlApDialog.cpp @@ -2,11 +2,12 @@ #include #include #include +#include "prx/libSceCommonDialog/CommonDialog.hpp" extern "C" { int APS5_VABI sceNetCtlApDialogInitialize(void); int APS5_VABI sceNetCtlApDialogTerminate(void); -int APS5_VABI sceNetCtlApDialogOpen(const void* param); +int APS5_VABI sceNetCtlApDialogOpen(const NetCtlApDialogParam* param); int APS5_VABI sceNetCtlApDialogClose(void); int APS5_VABI sceNetCtlApDialogUpdateStatus(void); int APS5_VABI sceNetCtlApDialogGetStatus(void); @@ -37,12 +38,15 @@ void RequireCanceled(const std::uint8_t (&result)[44]) { } int main() { - std::uint8_t param[104] = {}; - std::uint8_t result[44]; + NetCtlApDialogParam param{}; + param.base.size = sizeof(param.base); + param.base.magic = static_cast(reinterpret_cast(¶m)) - 1060003575u; + param.size = sizeof(param); + alignas(NetCtlApDialogResult) std::uint8_t result[44]; std::memset(result, 0x5a, sizeof(result)); Require(sceNetCtlApDialogTerminate() == kErrNotInitialized); - Require(sceNetCtlApDialogOpen(param) == kErrNotInitialized); + Require(sceNetCtlApDialogOpen(¶m) == kErrNotInitialized); Require(sceNetCtlApDialogClose() == kErrNotInitialized); Require(sceNetCtlApDialogGetResult(result) == kErrNotInitialized); Require(sceNetCtlApDialogGetStatus() == kStatusNone); @@ -55,27 +59,39 @@ int main() { Require(sceNetCtlApDialogOpen(nullptr) == kErrArgNull); Require(sceNetCtlApDialogGetStatus() == kStatusInitialized); + Require(sceNetCtlApDialogGetResult(result) == static_cast(0x80B8000Au)); + std::memset(result + 4, 0, 36); Require(sceNetCtlApDialogGetResult(result) == kErrNotFinished); - Require(sceNetCtlApDialogOpen(param) == 0); + param.size = 103; + Require(sceNetCtlApDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.size = sizeof(param); + param.reserved60 = 1; + Require(sceNetCtlApDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.reserved60 = 0; + std::memset(param.option, 'a', sizeof(param.option)); + Require(sceNetCtlApDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.option[8] = 0; + param.reserved[30] = 1; + Require(sceNetCtlApDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.reserved[30] = 0; + Require(sceNetCtlApDialogOpen(¶m) == 0); Require(sceNetCtlApDialogGetStatus() == kStatusFinished); Require(sceNetCtlApDialogUpdateStatus() == kStatusFinished); Require(sceNetCtlApDialogGetResult(nullptr) == kErrArgNull); - Require(sceNetCtlApDialogGetResult(result) == 0); + Require(sceNetCtlApDialogGetResult(result) == kResultUserCanceled); RequireCanceled(result); - Require(sceNetCtlApDialogOpen(param) == 0); - Require(sceNetCtlApDialogClose() == 0); + Require(sceNetCtlApDialogOpen(¶m) == 0); + Require(sceNetCtlApDialogClose() == static_cast(0x80B8000Bu)); Require(sceNetCtlApDialogTerminate() == 0); Require(sceNetCtlApDialogTerminate() == kErrNotInitialized); Require(sceNetCtlApDialogGetStatus() == kStatusNone); Require(sceNetCtlApDialogInitialize() == 0); - Require(sceNetCtlApDialogClose() == 0); - Require(sceNetCtlApDialogGetStatus() == kStatusFinished); - std::memset(result, 0x5a, sizeof(result)); - Require(sceNetCtlApDialogGetResult(result) == 0); - RequireCanceled(result); + Require(sceNetCtlApDialogClose() == static_cast(0x80B8000Bu)); + Require(sceNetCtlApDialogGetStatus() == kStatusInitialized); + Require(sceNetCtlApDialogGetResult(result) == kErrNotFinished); Require(sceNetCtlApDialogTerminate() == 0); } 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/GuestNpAuth.cpp b/core/libs/tests/GuestNpAuth.cpp index 92cdf6c335..695d37a67d 100644 --- a/core/libs/tests/GuestNpAuth.cpp +++ b/core/libs/tests/GuestNpAuth.cpp @@ -2,13 +2,19 @@ #include #include +#include extern "C" { int APS5_VABI sceNpAuthAbortRequest(int req_id); int APS5_VABI sceNpAuthCreateAsyncRequest(const void* param); int APS5_VABI sceNpAuthCreateRequest(void); int APS5_VABI sceNpAuthDeleteRequest(int req_id); +int APS5_VABI sceNpAuthGetAuthorizationCode(int req_id, const void* param, void* auth_code); +int APS5_VABI sceNpAuthGetAuthorizationCodeA(int req_id, const void* param, void* auth_code, int* issuer_id); int APS5_VABI sceNpAuthGetAuthorizationCodeV3(int req_id, const void* param, void* auth_code, int* issuer_id); +int APS5_VABI sceNpAuthGetAuthorizedAppCode(std::int32_t, const NpAuthAuthorizedAppCodeParameter*, char*, std::int32_t*, bool*); +int APS5_VABI sceNpAuthGetIdToken(int req_id, const void* param, void* id_token); +int APS5_VABI sceNpAuthGetIdTokenA(int req_id, const void* param, void* id_token); int APS5_VABI sceNpAuthGetIdTokenV3(int req_id, const void* param, void* id_token); int APS5_VABI sceNpAuthPollAsync(int req_id, int* result); int APS5_VABI sceNpAuthSetTimeout(int req_id, int resolve_retry, uint32_t resolve_timeout, uint32_t connect_timeout, uint32_t send_timeout, uint32_t receive_timeout); @@ -21,7 +27,43 @@ static void Require(bool condition) { } } +static void RequireUnstarted(int request) { + constexpr int invalidId = static_cast(0x80550307); + constexpr int sentinel = 0x13579bdf; + for (int attempt = 0; attempt != 3; ++attempt) { + int result = sentinel; + Require(sceNpAuthPollAsync(request, &result) == invalidId); + Require(result == sentinel); + Require(sceNpAuthWaitAsync(request, &result) == invalidId); + Require(result == sentinel); + } +} + +static void CheckUnstartedAborts() { + constexpr int aborted = static_cast(0x80550304); + alignas(8) char parameter[16]{}; + alignas(8) char output[16]{}; + for (const bool async : {false, true}) { + const int request = async ? sceNpAuthCreateAsyncRequest(nullptr) : sceNpAuthCreateRequest(); + Require(request > 0); + RequireUnstarted(request); + Require(sceNpAuthAbortRequest(request) == 0); + Require(sceNpAuthAbortRequest(request) == 0); + RequireUnstarted(request); + for (auto getter : {sceNpAuthGetIdToken, sceNpAuthGetIdTokenV3, sceNpAuthGetAuthorizationCode}) { + Require(getter(request, parameter, output) == aborted); + RequireUnstarted(request); + } + int issuer = 0x2468ace; + Require(sceNpAuthGetAuthorizationCodeV3(request, parameter, output, &issuer) == aborted); + Require(issuer == 0x2468ace); + RequireUnstarted(request); + Require(sceNpAuthDeleteRequest(request) == 0); + } +} + int main() { + CheckUnstartedAborts(); constexpr int invalidArgument = static_cast(0x80550301); constexpr int signedOut = static_cast(0x80550006); constexpr int aborted = static_cast(0x80550304); @@ -45,21 +87,41 @@ int main() { int issuerId = 0; Require(sceNpAuthSetTimeout(syncReq, 2, 5000000, 10000000, 10000000, 10000000) == 0); - Require(sceNpAuthSetTimeout(asyncReq, 0, 0, 0, 0, 0) == 0); + Require(sceNpAuthSetTimeout(asyncReq, 0, 0, 0, 0, 0) == invalidArgument); + + Require(sceNpAuthSetTimeout(syncReq, -1, 1000000, 10000000, 10000000, 10000000) == invalidArgument); + Require(sceNpAuthSetTimeout(syncReq, 1, 999999, 10000000, 10000000, 10000000) == invalidArgument); + Require(sceNpAuthSetTimeout(syncReq, 1, 1000000, 9999999, 10000000, 10000000) == invalidArgument); + Require(sceNpAuthSetTimeout(syncReq, 1, 1000000, 10000000, 9999999, 10000000) == invalidArgument); + Require(sceNpAuthSetTimeout(syncReq, 1, 1000000, 10000000, 10000000, 9999999) == invalidArgument); + Require(sceNpAuthSetTimeout(syncReq, 1, 1000000, 10000000, 10000000, 10000000) == 0); + Require(sceNpAuthSetTimeout(syncReq, 1, 0, 0, 0, 0) == 0); + Require(sceNpAuthSetTimeout(syncReq, 0, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu) == 0); + Require(sceNpAuthSetTimeout(-1, 0, 0, 0, 0, 0) == invalidArgument); int pollUnprocessed = 0; Require(sceNpAuthPollAsync(asyncReq, &pollUnprocessed) == invalidId); int waitUnprocessed = 0; Require(sceNpAuthWaitAsync(asyncReq, &waitUnprocessed) == invalidId); + Require(sceNpAuthGetAuthorizedAppCode(syncReq, nullptr, nullptr, nullptr, nullptr) == invalidArgument); + + Require(sceNpAuthGetIdToken(syncReq, nullptr, token) == invalidArgument); + Require(sceNpAuthGetIdToken(syncReq, param, nullptr) == invalidArgument); + Require(sceNpAuthGetIdToken(syncReq, nullptr, nullptr) == invalidArgument); Require(sceNpAuthGetIdTokenV3(syncReq, nullptr, token) == invalidArgument); Require(sceNpAuthGetIdTokenV3(syncReq, param, nullptr) == invalidArgument); Require(sceNpAuthGetIdTokenV3(syncReq, nullptr, nullptr) == invalidArgument); - Require(sceNpAuthGetIdTokenV3(syncReq, param, token) == signedOut); + Require(sceNpAuthGetIdToken(syncReq, param, token) == signedOut); + Require(sceNpAuthGetIdToken(syncReq, param, token) == invalidArgument); Require(sceNpAuthGetIdTokenV3(syncReq, param, token) == invalidArgument); + + Require(sceNpAuthGetAuthorizationCode(syncReq2, nullptr, authCode) == invalidArgument); + Require(sceNpAuthGetAuthorizationCode(syncReq2, param, nullptr) == invalidArgument); Require(sceNpAuthGetAuthorizationCodeV3(syncReq, nullptr, authCode, &issuerId) == invalidArgument); Require(sceNpAuthGetAuthorizationCodeV3(syncReq, param, nullptr, &issuerId) == invalidArgument); - Require(sceNpAuthGetAuthorizationCodeV3(syncReq2, param, authCode, &issuerId) == signedOut); + Require(sceNpAuthGetAuthorizationCode(syncReq2, param, authCode) == signedOut); + Require(sceNpAuthGetAuthorizationCode(syncReq2, param, authCode) == invalidArgument); Require(sceNpAuthGetAuthorizationCodeV3(syncReq2, param, authCode, &issuerId) == invalidArgument); Require(sceNpAuthGetIdTokenV3(asyncReq, param, token) == 0); @@ -82,7 +144,9 @@ int main() { Require(asyncWait2 == signedOut); constexpr int unknownReq = 999999; + Require(sceNpAuthGetIdToken(unknownReq, param, token) == requestNotFound); Require(sceNpAuthGetIdTokenV3(unknownReq, param, token) == requestNotFound); + Require(sceNpAuthGetAuthorizationCode(unknownReq, param, authCode) == requestNotFound); Require(sceNpAuthGetAuthorizationCodeV3(unknownReq, param, authCode, &issuerId) == requestNotFound); int pollResult = 0; Require(sceNpAuthPollAsync(unknownReq, &pollResult) == requestNotFound); @@ -103,8 +167,40 @@ int main() { Require(sceNpAuthGetIdTokenV3(abortedReq, param, token) == aborted); Require(sceNpAuthGetAuthorizationCodeV3(abortedReq, param, authCode, &issuerId) == aborted); int abortedWait = 0; - Require(sceNpAuthWaitAsync(abortedReq, &abortedWait) == 0); - Require(abortedWait == aborted); + Require(sceNpAuthWaitAsync(abortedReq, &abortedWait) == invalidId); + Require(abortedWait == 0); + + const int syncReqA = sceNpAuthCreateRequest(); + Require(sceNpAuthGetIdTokenA(syncReqA, nullptr, token) == invalidArgument); + Require(sceNpAuthGetIdTokenA(syncReqA, param, nullptr) == invalidArgument); + Require(sceNpAuthGetIdTokenA(syncReqA, param, token) == signedOut); + Require(sceNpAuthGetIdTokenA(syncReqA, param, token) == invalidArgument); + Require(sceNpAuthGetIdTokenA(unknownReq, param, token) == requestNotFound); + + const int syncReqAuthA = sceNpAuthCreateRequest(); + Require(sceNpAuthGetAuthorizationCodeA(syncReqAuthA, nullptr, authCode, &issuerId) == invalidArgument); + Require(sceNpAuthGetAuthorizationCodeA(syncReqAuthA, param, nullptr, &issuerId) == invalidArgument); + Require(sceNpAuthGetAuthorizationCodeA(syncReqAuthA, param, authCode, nullptr) == signedOut); + Require(sceNpAuthGetAuthorizationCodeA(syncReqAuthA, param, authCode, &issuerId) == invalidArgument); + Require(sceNpAuthGetAuthorizationCodeA(unknownReq, param, authCode, &issuerId) == requestNotFound); + + const int asyncReqA = sceNpAuthCreateAsyncRequest(nullptr); + Require(sceNpAuthGetIdTokenA(asyncReqA, param, token) == 0); + int asyncPollA = 0; + Require(sceNpAuthPollAsync(asyncReqA, &asyncPollA) == pollRunning); + Require(sceNpAuthPollAsync(asyncReqA, &asyncPollA) == 0); + Require(asyncPollA == signedOut); + + const int asyncReqAuthA = sceNpAuthCreateAsyncRequest(nullptr); + Require(sceNpAuthGetAuthorizationCodeA(asyncReqAuthA, param, authCode, &issuerId) == 0); + int asyncWaitA = 0; + Require(sceNpAuthWaitAsync(asyncReqAuthA, &asyncWaitA) == 0); + Require(asyncWaitA == signedOut); + + const int abortedReqA = sceNpAuthCreateAsyncRequest(nullptr); + Require(sceNpAuthAbortRequest(abortedReqA) == 0); + Require(sceNpAuthGetIdTokenA(abortedReqA, param, token) == aborted); + Require(sceNpAuthGetAuthorizationCodeA(abortedReqA, param, authCode, &issuerId) == aborted); Require(sceNpAuthDeleteRequest(syncReq) == 0); Require(sceNpAuthGetIdTokenV3(syncReq, param, token) == requestNotFound); diff --git a/core/libs/tests/GuestNpAuthAuthorizedAppCode.cpp b/core/libs/tests/GuestNpAuthAuthorizedAppCode.cpp index f6dfce0de8..a5d7f6fe17 100644 --- a/core/libs/tests/GuestNpAuthAuthorizedAppCode.cpp +++ b/core/libs/tests/GuestNpAuthAuthorizedAppCode.cpp @@ -1,13 +1,49 @@ -#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include #include +#include extern "C" { -int APS5_VABI sceNpAuthGetAuthorizedAppCode(); +int APS5_VABI sceNpAuthCreateRequest(); +int APS5_VABI sceNpAuthDeleteRequest(int); +int APS5_VABI sceNpAuthGetAuthorizedAppCode(std::int32_t, const NpAuthAuthorizedAppCodeParameter*, char*, std::int32_t*, bool*); +} + +static void Require(bool condition) { + if (!condition) std::abort(); } int main() { - constexpr int kSignedOut = static_cast(0x80550006); - if (sceNpAuthGetAuthorizedAppCode() != kSignedOut) std::abort(); - if (sceNpAuthGetAuthorizedAppCode() != kSignedOut) std::abort(); - return 0; + constexpr auto invalidArgument = static_cast(0x80550301u); + constexpr auto invalidSize = static_cast(0x80550302u); + constexpr auto requestNotFound = static_cast(0x80550306u); + std::array client{}; + std::array code{}; + code.fill('x'); + const auto original = code; + bool authorizationRequired = true; + NpAuthAuthorizedAppCodeParameter parameter{40, 1, reinterpret_cast(client.data()), "", 0}; + Require(sceNpAuthGetAuthorizedAppCode(0, nullptr, code.data(), nullptr, &authorizationRequired) == invalidArgument); + parameter.size = 39; + Require(sceNpAuthGetAuthorizedAppCode(0, ¶meter, code.data(), nullptr, &authorizationRequired) == invalidSize); + parameter.size = 40; + Require(sceNpAuthGetAuthorizedAppCode(0, ¶meter, nullptr, nullptr, &authorizationRequired) == invalidArgument); + Require(sceNpAuthGetAuthorizedAppCode(0, ¶meter, code.data(), nullptr, nullptr) == invalidArgument); + parameter.option = 2; + Require(sceNpAuthGetAuthorizedAppCode(0, ¶meter, code.data(), nullptr, &authorizationRequired) == invalidArgument); + parameter.option = 0; + parameter.userId = -1; + Require(sceNpAuthGetAuthorizedAppCode(0, ¶meter, code.data(), nullptr, &authorizationRequired) == invalidArgument); + parameter.userId = 1; + Require(sceNpAuthGetAuthorizedAppCode(0, ¶meter, code.data(), nullptr, &authorizationRequired) == requestNotFound); + const auto requestId = sceNpAuthCreateRequest(); + Require(requestId > 0); + bool threw = false; + try { + sceNpAuthGetAuthorizedAppCode(requestId, ¶meter, code.data(), nullptr, &authorizationRequired); + } catch (const std::logic_error&) { + threw = true; + } + Require(threw && code == original && authorizationRequired); + Require(sceNpAuthDeleteRequest(requestId) == 0); } diff --git a/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp b/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp index e8a0a3afc8..aedef7375a 100644 --- a/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp +++ b/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp @@ -1,13 +1,18 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include +#include "prx/libSceCommonDialog/CommonDialog.hpp" +#include "prx/libSceUserService/UserService.hpp" extern "C" { +int APS5_VABI sceUserServiceInitialize(const UserServiceInitParam* param); +int APS5_VABI sceUserServiceTerminate(); int APS5_VABI sceNpAuthAuthorizedAppDialogInitialize(void); -int APS5_VABI sceNpAuthAuthorizedAppDialogOpen(const void* param); +int APS5_VABI sceNpAuthAuthorizedAppDialogOpen(const NpAuthAuthorizedAppDialogParam* param); int APS5_VABI sceNpAuthAuthorizedAppDialogUpdateStatus(void); int APS5_VABI sceNpAuthAuthorizedAppDialogGetStatus(void); -int APS5_VABI sceNpAuthAuthorizedAppDialogGetResult(void* result); +int APS5_VABI sceNpAuthAuthorizedAppDialogGetResult(std::int32_t* result); int APS5_VABI sceNpAuthAuthorizedAppDialogClose(void); int APS5_VABI sceNpAuthAuthorizedAppDialogTerminate(void); } @@ -28,7 +33,16 @@ constexpr int kResultUserCanceled = 1; } int main() { - std::int64_t param = 0; + Require(sceUserServiceInitialize(nullptr) == 0); + NpAuthAuthorizedAppDialogParam param{}; + param.base.size = sizeof(param.base); + param.base.magic = static_cast(reinterpret_cast(¶m)) - 1060003575u; + param.size = sizeof(param); + param.userId = USER_SERVICE_INITIAL_USER_ID; + char application[136] = "test"; + std::string text(1024, 'a'); + param.application = application; + param.text = text.c_str(); std::int32_t result[2] = {-1, -1}; Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusNone); @@ -45,18 +59,51 @@ int main() { Require(sceNpAuthAuthorizedAppDialogOpen(nullptr) == kErrArgNull); Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotFinished); + constexpr int invalidArgument = static_cast(0x80B8000Au); + param.size = 119; + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == invalidArgument); + param.size = sizeof(param); + param.userId = 42; + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == invalidArgument); + param.userId = USER_SERVICE_INITIAL_USER_ID; + param.application = nullptr; + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == invalidArgument); + param.application = application; + param.text = ""; + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == invalidArgument); + text.push_back('a'); + param.text = text.c_str(); + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == invalidArgument); + text.pop_back(); + param.text = text.c_str(); + param.mode = 2; + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == invalidArgument); + param.mode = 1; + param.reserved[31] = 1; + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == invalidArgument); + param.reserved[31] = 0; Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == 0); Require(sceNpAuthAuthorizedAppDialogUpdateStatus() == kStatusFinished); Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusFinished); Require(sceNpAuthAuthorizedAppDialogGetResult(nullptr) == kErrArgNull); - Require(sceNpAuthAuthorizedAppDialogGetResult(result) == 0); + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kResultUserCanceled); Require(result[0] == kResultUserCanceled); Require(result[1] == -1); - Require(sceNpAuthAuthorizedAppDialogClose() == 0); + Require(sceNpAuthAuthorizedAppDialogClose() == static_cast(0x80B8000Bu)); Require(sceNpAuthAuthorizedAppDialogTerminate() == 0); Require(sceNpAuthAuthorizedAppDialogTerminate() == kErrNotInitialized); Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusNone); + + Require(sceNpAuthAuthorizedAppDialogInitialize() == 0); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusInitialized); + Require(sceNpAuthAuthorizedAppDialogClose() == static_cast(0x80B8000Bu)); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusInitialized); + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotFinished); + Require(sceNpAuthAuthorizedAppDialogTerminate() == 0); + Require(sceNpAuthAuthorizedAppDialogOpen(nullptr) == kErrArgNull); + Require(sceNpAuthAuthorizedAppDialogGetResult(nullptr) == kErrArgNull); + Require(sceUserServiceTerminate() == 0); return 0; } diff --git a/core/libs/tests/GuestNpCommerceDialog.cpp b/core/libs/tests/GuestNpCommerceDialog.cpp new file mode 100644 index 0000000000..9cb602b2b0 --- /dev/null +++ b/core/libs/tests/GuestNpCommerceDialog.cpp @@ -0,0 +1,198 @@ +#include "ProcessParametersFixture.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include +#include +#include "SceTypes.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" + +#include +#include +#include + +extern "C" { +int APS5_VABI sceUserServiceInitialize(const UserServiceInitParam* param); +int APS5_VABI sceUserServiceTerminate(); +int APS5_VABI sceNpCommerceDialogInitialize(); +int APS5_VABI sceNpCommerceDialogOpen(const NpCommerceDialogParam* param); +int APS5_VABI sceNpCommerceDialogOpen2(const NpCommerceDialogParam2* param); +int APS5_VABI sceNpCommerceDialogUpdateStatus(void); +int APS5_VABI sceNpCommerceDialogGetStatus(void); +int APS5_VABI sceNpCommerceDialogGetResult(NpCommerceDialogResult* result); +int APS5_VABI sceNpCommerceDialogClose(void); +int APS5_VABI sceNpCommerceDialogTerminate(); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +alignas(16) std::array process{}; + +constexpr int statusNone = 0; +constexpr int statusInitialized = 1; +constexpr int statusFinished = 3; +constexpr std::int32_t resultUserCanceled = 1; + +void RequireCanceledResult(std::uint64_t userData, bool clearsReserved) { + NpCommerceDialogResult result; + std::memset(&result, 0x33, sizeof(result)); + Require(sceNpCommerceDialogGetResult(&result) == resultUserCanceled); + Require(result.result == resultUserCanceled); + Require(result.authorized == 0); + Require(result.userData == userData); + for (const auto value : result.padding) Require(value == 0x33); + for (const auto value : result.reserved) Require(value == (clearsReserved ? 0 : 0x33)); +} + +} + +int main() { + Require(ProcessParametersFixture::Install(process.data(), process.size())); + std::uint32_t sdkVersion = 0x03500000; + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + Require(sceUserServiceInitialize(nullptr) == 0); + NpCommerceDialogParam2 param2{}; + param2.base.size = sizeof(param2.base); + param2.base.magic = static_cast(reinterpret_cast(¶m2)) - 1060003575u; + param2.size = sizeof(param2); + param2.userId = USER_SERVICE_INITIAL_USER_ID; + param2.userData = 0x123456789abcdef0; + Require(sceNpCommerceDialogGetStatus() == statusNone); + Require(sceNpCommerceDialogOpen(nullptr) == static_cast(0x80B8000Du)); + Require(sceNpCommerceDialogOpen2(nullptr) == static_cast(0x80B8000Du)); + Require(sceNpCommerceDialogOpen2(¶m2) == static_cast(0x80B80003u)); + param2.base.magic = 0; + Require(sceNpCommerceDialogOpen2(¶m2) == static_cast(0x80B8000Au)); + param2.base.magic = static_cast(reinterpret_cast(¶m2)) - 1060003575u; + param2.base.reserved[35] = 1; + Require(sceNpCommerceDialogOpen2(¶m2) == static_cast(0x80B8000Au)); + param2.base.reserved[35] = 0; + Require(sceNpCommerceDialogTerminate() == static_cast(0x80B80003u)); + Require(sceNpCommerceDialogClose() == static_cast(0x80B80003u)); + Require(sceNpCommerceDialogGetResult(nullptr) == static_cast(0x80B8000Du)); + NpCommerceDialogResult result{}; + Require(sceNpCommerceDialogGetResult(&result) == static_cast(0x80B80003u)); + Require(sceNpCommerceDialogInitialize() == 0); + Require(sceNpCommerceDialogClose() == static_cast(0x80B8000Bu)); + Require(sceNpCommerceDialogGetResult(&result) == static_cast(0x80B80005u)); + Require(sceNpCommerceDialogGetResult(nullptr) == static_cast(0x80B8000Du)); + Require(sceNpCommerceDialogGetStatus() == statusInitialized); + + Require(sceNpCommerceDialogOpen2(¶m2) == 0); + Require(sceNpCommerceDialogUpdateStatus() == statusFinished); + Require(sceNpCommerceDialogGetStatus() == statusFinished); + RequireCanceledResult(param2.userData, true); + + Require(sceNpCommerceDialogClose() == static_cast(0x80B8000Bu)); + Require(sceNpCommerceDialogGetStatus() == statusFinished); + Require(sceNpCommerceDialogTerminate() == 0); + Require(sceNpCommerceDialogGetStatus() == statusNone); + + NpCommerceDialogParam param{}; + param.base.size = sizeof(param.base); + param.base.magic = static_cast(reinterpret_cast(¶m)) - 1060003575u; + param.size = sizeof(param); + param.userId = USER_SERVICE_INITIAL_USER_ID; + Require(sceNpCommerceDialogInitialize() == 0); + param.base.size = 0; + Require(sceNpCommerceDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.base.size = sizeof(param.base); + param.size = 0; + Require(sceNpCommerceDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.size = sizeof(param); + param.option = 8; + Require(sceNpCommerceDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.option = 0; + param.reserved[31] = 1; + Require(sceNpCommerceDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.reserved[31] = 0; + param.userId = -1; + Require(sceNpCommerceDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.userId = USER_SERVICE_INITIAL_USER_ID; + const char* products[11]{}; + for (std::uint32_t mode = 0; mode <= 6; ++mode) { + param.mode = mode; + for (std::uint32_t count = 0; count <= 11; ++count) { + param.productCount = count; + for (bool hasProducts : {false, true}) { + param.products = hasProducts ? products : nullptr; + bool valid = false; + if (mode == 0 || mode == 2) valid = count <= 1 && hasProducts == (count != 0); + if (mode == 1) valid = count == 1 && hasProducts; + if (mode == 3) valid = count >= 1 && count <= 10 && hasProducts; + if (mode == 4) valid = count <= 10 && hasProducts == (count != 0); + Require(sceNpCommerceDialogOpen(¶m) == (valid ? 0 : static_cast(0x80B8000Au))); + param2.mode = mode; + param2.productCount = count; + param2.products = param.products; + Require(sceNpCommerceDialogOpen2(¶m2) == (valid ? 0 : static_cast(0x80B8000Au))); + } + } + } + param2.mode = 1; + param2.contentMode = 1; + param2.productCount = 1; + param2.products = products; + param2.option = 99; + Require(sceNpCommerceDialogOpen2(¶m2) == 0); + param2.option = 100; + Require(sceNpCommerceDialogOpen2(¶m2) == static_cast(0x80B8000Au)); + param2.option = 0; + param2.mode = 2; + bool threw = false; + try { + sceNpCommerceDialogOpen2(¶m2); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + param2.contentMode = 0; + param2.mode = 0; + param2.productCount = 0; + param2.products = nullptr; + param2.reserved[31] = 1; + Require(sceNpCommerceDialogOpen2(¶m2) == static_cast(0x80B8000Au)); + param2.reserved[31] = 0; + param2.size = 0; + Require(sceNpCommerceDialogOpen2(¶m2) == static_cast(0x80B8000Au)); + param2.size = sizeof(param2); + param2.userId = -1; + Require(sceNpCommerceDialogOpen2(¶m2) == static_cast(0x80B8000Au)); + param2.userId = USER_SERVICE_INITIAL_USER_ID; + param2.userData = 0x123456789abcdef0; + for (std::uint32_t mode : {1000u, 1001u}) { + param.mode = mode; + param.productCount = 0; + param.argument = 1; + Require(sceNpCommerceDialogOpen(¶m) == static_cast(0x80B8000Au)); + } + param.mode = 5; + param.productCount = 0; + param.products = nullptr; + for (std::uint64_t argument = 0; argument <= 4; ++argument) { + param.argument = argument; + Require(sceNpCommerceDialogOpen(¶m) == (argument == 1 ? 0 : static_cast(0x80B8000Au))); + } + sdkVersion = 0x01700000; + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + param.argument = 3; + param.userData = 0xfedcba9876543210; + Require(sceNpCommerceDialogOpen(¶m) == 0); + RequireCanceledResult(param.userData, true); + sdkVersion = 0x01600000; + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + param.size = 0; + param.option = 100; + param.userId = -1; + param.reserved[31] = 1; + param.argument = 7; + Require(sceNpCommerceDialogOpen(¶m) == 0); + RequireCanceledResult(param.userData, false); + Require(sceNpCommerceDialogOpen2(¶m2) == 0); + Require(sceNpCommerceDialogUpdateStatus() == statusFinished); + RequireCanceledResult(param2.userData, false); + Require(sceNpCommerceDialogTerminate() == 0); + Require(sceNpCommerceDialogGetStatus() == statusNone); + Require(sceUserServiceTerminate() == 0); + return 0; +} diff --git a/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp new file mode 100644 index 0000000000..f9a03f0d52 --- /dev/null +++ b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp @@ -0,0 +1,366 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +using Deleter = void (APS5_VABI*)(void*); + +struct IntrusivePtr { + void* object; + Deleter deleter; + void* libContext; +}; + +using Construct = void (APS5_VABI*)(IntrusivePtr*); +using Copy = void (APS5_VABI*)(IntrusivePtr*, const IntrusivePtr*); +using Destroy = void (APS5_VABI*)(IntrusivePtr*); +using Access = void* (APS5_VABI*)(const IntrusivePtr*); + +extern "C" { +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEC1Ev(IntrusivePtr*); + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1ERS7_(IntrusivePtr*, const IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_(IntrusivePtr*, const IntrusivePtr*); + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEED1Ev(IntrusivePtr*); + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V15EntryEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6BinaryEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEEdeEv(const IntrusivePtr*); + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEEC1EPNS2_10LibContextE(IntrusivePtr*, void*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEED1Ev(IntrusivePtr*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEE8pushBackERKS8_(IntrusivePtr*, const IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEEC1EPS6_PFvS8_EPNS2_10LibContextE(IntrusivePtr*, void*, Deleter, void*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEED1Ev(IntrusivePtr*); + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1ERS7_(IntrusivePtr*, const IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEED1Ev(IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEE3getEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEE3getEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEdeEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEdeEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEptEv(const IntrusivePtr*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static constexpr std::size_t ObjectSize = 0x40; +static constexpr std::size_t VectorReferenceCountOffset = 0x30; + +struct DestroyCase { + Destroy destroy; + std::size_t referenceCountOffset; +}; + +static void* deletedObject = nullptr; +static int deleterCalls = 0; + +static void APS5_VABI RecordDelete(void* object) { + deletedObject = object; + ++deleterCalls; +} + +static std::int32_t ReadCount(const unsigned char* object, const std::size_t offset) { + std::int32_t value; + std::memcpy(&value, object + offset, sizeof(value)); + return value; +} + +static void WriteCount(unsigned char* object, const std::size_t offset, const std::int32_t value) { + std::memcpy(object + offset, &value, sizeof(value)); +} + +static bool OnlyCountChanged(const unsigned char* object, const std::size_t offset) { + for (std::size_t index = 0; index < ObjectSize; ++index) { + if (index >= offset && index < offset + sizeof(std::int32_t)) continue; + if (object[index] != 0x5a) return false; + } + return true; +} + +static void TestConstruct(const Construct construct) { + IntrusivePtr pointer; + std::memset(&pointer, 0xa5, sizeof(pointer)); + construct(&pointer); + Require(pointer.object == nullptr); + Require(pointer.deleter == nullptr); + Require(pointer.libContext == nullptr); +} + +static void TestAccess(const Access access) { + int object = 0; + int context = 0; + const IntrusivePtr empty{nullptr, RecordDelete, &context}; + Require(access(&empty) == nullptr); + const IntrusivePtr filled{&object, nullptr, nullptr}; + Require(access(&filled) == &object); +} + +static void TestCopy(const Copy copy) { + alignas(8) unsigned char object[ObjectSize]; + std::memset(object, 0x5a, sizeof(object)); + WriteCount(object, 0, 1); + int context = 0; + + const IntrusivePtr source{object, RecordDelete, &context}; + IntrusivePtr target; + std::memset(&target, 0xa5, sizeof(target)); + copy(&target, &source); + Require(target.object == object); + Require(target.deleter == RecordDelete); + Require(target.libContext == &context); + Require(source.object == object); + Require(ReadCount(object, 0) == 2); + Require(OnlyCountChanged(object, 0)); + + const IntrusivePtr empty{nullptr, RecordDelete, &context}; + std::memset(&target, 0xa5, sizeof(target)); + copy(&target, &empty); + Require(target.object == nullptr); + Require(target.deleter == RecordDelete); + Require(target.libContext == &context); +} + +static void TestDestroy(const DestroyCase& testCase) { + alignas(8) unsigned char object[ObjectSize]; + std::memset(object, 0x5a, sizeof(object)); + int context = 0; + const std::size_t offset = testCase.referenceCountOffset; + + deleterCalls = 0; + deletedObject = nullptr; + + IntrusivePtr empty{nullptr, RecordDelete, &context}; + testCase.destroy(&empty); + Require(empty.object == nullptr); + Require(empty.deleter == RecordDelete); + Require(empty.libContext == &context); + Require(deleterCalls == 0); + + WriteCount(object, offset, 2); + IntrusivePtr shared{object, RecordDelete, &context}; + testCase.destroy(&shared); + Require(ReadCount(object, offset) == 1); + Require(shared.object == object); + Require(shared.deleter == RecordDelete); + Require(shared.libContext == &context); + Require(deleterCalls == 0); + + IntrusivePtr last{object, RecordDelete, &context}; + testCase.destroy(&last); + Require(ReadCount(object, offset) == 0); + Require(OnlyCountChanged(object, offset)); + Require(deleterCalls == 1); + Require(deletedObject == object); + Require(last.object == nullptr); + Require(last.deleter == nullptr); + Require(last.libContext == &context); + + WriteCount(object, offset, 1); + IntrusivePtr pooled{object, nullptr, &context}; + bool thrown = false; + try { + testCase.destroy(&pooled); + } catch (const std::runtime_error&) { + thrown = true; + } + Require(thrown); + Require(deleterCalls == 1); +} + +static void TestSharedOwnership() { + alignas(8) unsigned char object[ObjectSize]; + std::memset(object, 0x5a, sizeof(object)); + WriteCount(object, 0, 1); + deleterCalls = 0; + deletedObject = nullptr; + + IntrusivePtr owner; + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1Ev(&owner); + owner.object = object; + owner.deleter = RecordDelete; + + IntrusivePtr argument; + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_(&argument, &owner); + Require(_ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv(&argument) == object); + Require(ReadCount(object, 0) == 2); + + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev(&argument); + Require(ReadCount(object, 0) == 1); + Require(deleterCalls == 0); + + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev(&owner); + Require(ReadCount(object, 0) == 0); + Require(deleterCalls == 1); + Require(deletedObject == object); + Require(_ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv(&owner) == nullptr); +} + +static void TestVectorPushBackReleasesElements() { + int context = 0; + alignas(8) unsigned char object[ObjectSize]; + std::memset(object, 0x5a, sizeof(object)); + WriteCount(object, 0, 1); + deleterCalls = 0; + deletedObject = nullptr; + + IntrusivePtr vector; + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEEC1EPNS2_10LibContextE(&vector, &context); + Require(vector.object == nullptr); + Require(vector.deleter == nullptr); + Require(vector.libContext == &context); + + const IntrusivePtr empty{nullptr, nullptr, &context}; + Require(_ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEE8pushBackERKS8_(&vector, &empty) == 0); + Require(vector.object != nullptr); + Require(vector.deleter != nullptr); + + IntrusivePtr filled{object, RecordDelete, &context}; + Require(_ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEE8pushBackERKS8_(&vector, &filled) == 0); + Require(ReadCount(object, 0) == 2); + + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEED1Ev(&vector); + Require(vector.object == nullptr); + Require(vector.deleter == nullptr); + Require(ReadCount(object, 0) == 1); + Require(deleterCalls == 0); + + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEED1Ev(&filled); + Require(ReadCount(object, 0) == 0); + Require(deleterCalls == 1); + Require(deletedObject == object); +} + +static void TestAdoptedVectorBodyDestroyedThroughDeleter() { + int context = 0; + alignas(8) unsigned char body[ObjectSize]; + std::memset(body, 0x5a, sizeof(body)); + WriteCount(body, VectorReferenceCountOffset, 1); + deleterCalls = 0; + deletedObject = nullptr; + + IntrusivePtr vector; + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEEC1EPS6_PFvS8_EPNS2_10LibContextE(&vector, body, RecordDelete, &context); + Require(vector.object == body); + Require(vector.deleter == RecordDelete); + Require(vector.libContext == &context); + + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEED1Ev(&vector); + Require(ReadCount(body, VectorReferenceCountOffset) == 0); + Require(deleterCalls == 1); + Require(deletedObject == body); + Require(vector.object == nullptr); +} + +int main() { + Require(sizeof(IntrusivePtr) == 0x18); + + const Construct constructors[] = { + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEC1Ev, + }; + for (const auto construct : constructors) TestConstruct(construct); + + const Access accessors[] = { + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V15EntryEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6BinaryEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEEdeEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEE3getEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEE3getEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEdeEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEdeEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEptEv, + }; + for (const auto access : accessors) TestAccess(access); + + const Copy copies[] = { + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1ERS7_, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEEC1ERS7_, + }; + for (const auto copy : copies) TestCopy(copy); + + const DestroyCase destructors[] = { + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEED1Ev, VectorReferenceCountOffset}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_15ProfanityFilter2V219WebApiFilterRequestEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_18ResponseHeaderBaseEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6StringEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_11UserProfile2V113BasicPresenceEEEEEED1Ev, VectorReferenceCountOffset}, + }; + for (const auto& testCase : destructors) TestDestroy(testCase); + + TestSharedOwnership(); + TestVectorPushBackReleasesElements(); + TestAdoptedVectorBodyDestroyedThroughDeleter(); + return 0; +} diff --git a/core/libs/tests/GuestNpCppWebApiSignedOut.cpp b/core/libs/tests/GuestNpCppWebApiSignedOut.cpp new file mode 100644 index 0000000000..455bc254b0 --- /dev/null +++ b/core/libs/tests/GuestNpCppWebApiSignedOut.cpp @@ -0,0 +1,189 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsD1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends10initializeEPNS1_6Common10LibContextEPKc(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi10getFriendsEiRKNS4_21ParameterToGetFriendsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_18GetFriendsResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersD1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers10initializeEPNS1_6Common10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi16getBlockingUsersEiRKNS4_27ParameterToGetBlockingUsersERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24GetBlockingUsersResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesD1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences10initializeEPNS1_6Common10LibContextEPKc(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi17getBasicPresencesEiRKNS4_28ParameterToGetBasicPresencesERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_25GetBasicPresencesResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_(); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityD1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity10initializeEPNS1_6Common10LibContextEPKcSA_NS6_12IntrusivePtrINS3_19WebApiFilterRequestEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi15filterProfanityEiRKNS4_26ParameterToFilterProfanityERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_23FilterProfanityResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(); +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_(); +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusC1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusD1Ev(void*); +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus10initializeEPNS1_6Common10LibContextEm(); +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus9terminateEv(); +int APS5_VABI _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi33getCommunicationRestrictionStatusEiRKNS4_44ParameterToGetCommunicationRestrictionStatusERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_38CommunicationRestrictionStatusResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE(); +int APS5_VABI _ZN3sce2Np9CppWebApi15ProfanityFilter2V226WebApiFilterRequestFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_19WebApiFilterRequestEEE(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static constexpr int NotSignedIn = static_cast(0x80552907); + +using Object = int (APS5_VABI*)(void*); +using Call = int (APS5_VABI*)(); + +struct RequestFlow { + Object transactionConstruct; + Object transactionDestroy; + Call setOnFinishedCallback; + Call setResponseInformationOption; + Call start; + Call finish; + Call getResponseHeaders; + Object parameterConstruct; + Object parameterDestroy; + Call initialize; + Call terminate; + Call call; +}; + +static const RequestFlow flows[] = { + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_, + _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsC1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriendsD1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends10initializeEPNS1_6Common10LibContextEPKc, + _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi21ParameterToGetFriends9terminateEv, + _ZN3sce2Np9CppWebApi11UserProfile2V110FriendsApi10getFriendsEiRKNS4_21ParameterToGetFriendsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_18GetFriendsResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE, + }, + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_, + _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersC1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsersD1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers10initializeEPNS1_6Common10LibContextE, + _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi27ParameterToGetBlockingUsers9terminateEv, + _ZN3sce2Np9CppWebApi11UserProfile2V19BlocksApi16getBlockingUsersEiRKNS4_27ParameterToGetBlockingUsersERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_24GetBlockingUsersResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE, + }, + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_, + _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesC1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresencesD1Ev, + _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences10initializeEPNS1_6Common10LibContextEPKc, + _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi28ParameterToGetBasicPresences9terminateEv, + _ZN3sce2Np9CppWebApi11UserProfile2V111PresenceApi17getBasicPresencesEiRKNS4_28ParameterToGetBasicPresencesERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_25GetBasicPresencesResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE, + }, + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_, + _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityC1Ev, + _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanityD1Ev, + _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity10initializeEPNS1_6Common10LibContextEPKcSA_NS6_12IntrusivePtrINS3_19WebApiFilterRequestEEE, + _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi26ParameterToFilterProfanity9terminateEv, + _ZN3sce2Np9CppWebApi15ProfanityFilter2V212ProfanityApi15filterProfanityEiRKNS4_26ParameterToFilterProfanityERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_23FilterProfanityResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE, + }, + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE21setOnFinishedCallbackEPFvliS8_PvEPFvliSC_ESC_, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE18getResponseHeadersERSA_, + _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusC1Ev, + _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatusD1Ev, + _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus10initializeEPNS1_6Common10LibContextEm, + _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi44ParameterToGetCommunicationRestrictionStatus9terminateEv, + _ZN3sce2Np9CppWebApi30CommunicationRestrictionStatus2V333CommunicationRestrictionStatusApi33getCommunicationRestrictionStatusEiRKNS4_44ParameterToGetCommunicationRestrictionStatusERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_38CommunicationRestrictionStatusResponseEEENSA_INS8_18ResponseHeaderBaseEEEEE, + }, +}; + +static void TestRequestFlow(const RequestFlow& flow) { + alignas(16) unsigned char transaction[0x100]; + alignas(16) unsigned char parameter[0x100]; + std::memset(transaction, 0x5a, sizeof(transaction)); + std::memset(parameter, 0x5a, sizeof(parameter)); + + Require(flow.transactionConstruct(transaction) == 0); + Require(flow.setOnFinishedCallback() == 0); + Require(flow.setResponseInformationOption() == 0); + Require(flow.parameterConstruct(parameter) == 0); + Require(flow.initialize() == 0); + Require(flow.call() == NotSignedIn); + Require(flow.start() == NotSignedIn); + Require(flow.getResponseHeaders() == NotSignedIn); + Require(flow.finish() == 0); + Require(flow.terminate() == 0); + Require(flow.parameterDestroy(parameter) == 0); + Require(flow.transactionDestroy(transaction) == 0); +} + +int main() { + for (const auto& flow : flows) TestRequestFlow(flow); + Require(_ZN3sce2Np9CppWebApi15ProfanityFilter2V226WebApiFilterRequestFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_19WebApiFilterRequestEEE() == NotSignedIn); + return 0; +} diff --git a/core/libs/tests/GuestNpCppWebApiString.cpp b/core/libs/tests/GuestNpCppWebApiString.cpp new file mode 100644 index 0000000000..18a4dd25a6 --- /dev/null +++ b/core/libs/tests/GuestNpCppWebApiString.cpp @@ -0,0 +1,191 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(void* self, void* libContext); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringD1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6String6appendEPKc(void* self, const char* text); +const char* APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String5c_strEv(const void* self); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEEC1EPNS2_10LibContextE(void* self, void* libContext); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEED1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE8pushBackERKS4_(void* self, const void* element); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr std::size_t kStringSize = 0x20; +constexpr std::size_t kGuardSize = 0x10; +constexpr std::size_t kReferenceCountOffset = 0x00; +constexpr std::size_t kBufferOffset = 0x08; +constexpr std::size_t kBufferSizeOffset = 0x10; +constexpr std::size_t kLibContextOffset = 0x18; +constexpr unsigned char kFill = 0xA5; + +constexpr std::size_t kVectorSize = 0x18; +constexpr std::size_t kObjectOffset = 0x00; +constexpr std::size_t kBodyMagicOffset = 0x00; +constexpr std::size_t kBodyReferenceCountOffset = 0x30; +constexpr std::size_t kBodyElementKindOffset = 0x34; +constexpr std::size_t kBodyElementsOffset = 0x38; +constexpr std::size_t kBodySizeOffset = 0x40; + +struct Storage { + alignas(8) unsigned char bytes[kStringSize + kGuardSize]; +}; + +struct VectorStorage { + alignas(8) unsigned char bytes[kVectorSize + kGuardSize]; +}; + +template +TValue Load(const Storage& storage, std::size_t offset) { + TValue value; + std::memcpy(&value, storage.bytes + offset, sizeof(value)); + return value; +} + +template +void Store(Storage& storage, std::size_t offset, TValue value) { + std::memcpy(storage.bytes + offset, &value, sizeof(value)); +} + +template +TValue LoadBody(const void* body, std::size_t offset) { + TValue value; + std::memcpy(&value, static_cast(body) + offset, sizeof(value)); + return value; +} + +bool GuardIntact(const Storage& storage) { + for (std::size_t i = kStringSize; i < sizeof(storage.bytes); i++) { + if (storage.bytes[i] != kFill) return false; + } + return true; +} + +bool VectorGuardIntact(const VectorStorage& storage) { + for (std::size_t i = kVectorSize; i < sizeof(storage.bytes); i++) { + if (storage.bytes[i] != kFill) return false; + } + return true; +} + +void TestConstructedStringIsEmpty() { + std::uint64_t libContext = 1; + Storage storage; + std::memset(storage.bytes, kFill, sizeof(storage.bytes)); + + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(storage.bytes, &libContext); + + Require(Load(storage, kReferenceCountOffset) == 0); + Require(Load(storage, kBufferOffset) == nullptr); + Require(Load(storage, kBufferSizeOffset) == 0); + Require(Load(storage, kLibContextOffset) == &libContext); + Require(GuardIntact(storage)); + + const char* text = _ZNK3sce2Np9CppWebApi6Common6String5c_strEv(storage.bytes); + Require(text != nullptr && text[0] == '\0'); + + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(storage.bytes); + Require(Load(storage, kBufferOffset) == nullptr); + Require(Load(storage, kBufferSizeOffset) == 0); + Require(GuardIntact(storage)); +} + +void TestNullLibContextIsStored() { + Storage storage; + std::memset(storage.bytes, kFill, sizeof(storage.bytes)); + + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(storage.bytes, nullptr); + + Require(Load(storage, kLibContextOffset) == nullptr); + Require(Load(storage, kBufferOffset) == nullptr); + Require(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(storage.bytes)[0] == '\0'); + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(storage.bytes); +} + +void TestAppendBuildsAndClearsBuffer() { + std::uint64_t libContext = 1; + Storage storage; + std::memset(storage.bytes, kFill, sizeof(storage.bytes)); + + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(storage.bytes, &libContext); + + Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(storage.bytes, "rank") == 0); + Require(std::strcmp(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(storage.bytes), "rank") == 0); + Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(storage.bytes, "ing") == 0); + Require(std::strcmp(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(storage.bytes), "ranking") == 0); + Require(Load(storage, kBufferOffset) != nullptr); + Require(GuardIntact(storage)); + + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(storage.bytes); + Require(Load(storage, kBufferOffset) == nullptr); + Require(Load(storage, kBufferSizeOffset) == 0); + Require(GuardIntact(storage)); +} + +void TestAppendNullKeepsEmpty() { + std::uint64_t libContext = 1; + Storage storage; + std::memset(storage.bytes, kFill, sizeof(storage.bytes)); + + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(storage.bytes, &libContext); + Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(storage.bytes, nullptr) == 0); + Require(Load(storage, kBufferOffset) == nullptr); + Require(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(storage.bytes)[0] == '\0'); + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(storage.bytes); +} + +void TestVectorPushBackCopiesStringContent() { + std::uint64_t libContext = 1; + VectorStorage vector; + Storage element; + std::memset(vector.bytes, kFill, sizeof(vector.bytes)); + std::memset(element.bytes, kFill, sizeof(element.bytes)); + + _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEEC1EPNS2_10LibContextE(vector.bytes, &libContext); + Require(VectorGuardIntact(vector)); + + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(element.bytes, &libContext); + Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(element.bytes, "privilege") == 0); + char* elementBuffer = Load(element, kBufferOffset); + Require(elementBuffer != nullptr); + + Require(_ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE8pushBackERKS4_(vector.bytes, element.bytes) == 0); + + void* body; + std::memcpy(&body, vector.bytes + kObjectOffset, sizeof(body)); + Require(body != nullptr); + Require(LoadBody(body, kBodyReferenceCountOffset) == 1); + Require(LoadBody(body, kBodyElementKindOffset) == 2); + Require(LoadBody(body, kBodySizeOffset) == 1); + + void* elements = LoadBody(body, kBodyElementsOffset); + char* storedBuffer; + std::memcpy(&storedBuffer, static_cast(elements) + kBufferOffset, sizeof(storedBuffer)); + Require(storedBuffer != nullptr); + Require(storedBuffer != elementBuffer); + Require(std::strcmp(storedBuffer, "privilege") == 0); + + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(element.bytes); + Require(std::strcmp(storedBuffer, "privilege") == 0); + + _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEED1Ev(vector.bytes); + Require(VectorGuardIntact(vector)); +} + +} + +int main() { + TestConstructedStringIsEmpty(); + TestNullLibContextIsStored(); + TestAppendBuildsAndClearsBuffer(); + TestAppendNullKeepsEmpty(); + TestVectorPushBackCopiesStringContent(); +} diff --git a/core/libs/tests/GuestNpFriendListDialog.cpp b/core/libs/tests/GuestNpFriendListDialog.cpp new file mode 100644 index 0000000000..70385c595c --- /dev/null +++ b/core/libs/tests/GuestNpFriendListDialog.cpp @@ -0,0 +1,72 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceNpFriendListDialogInitialize(void); +int APS5_VABI sceNpFriendListDialogOpenA(const void* param); +int APS5_VABI sceNpFriendListDialogUpdateStatus(void); +int APS5_VABI sceNpFriendListDialogGetStatus(void); +int APS5_VABI sceNpFriendListDialogGetResultA(void* result); +int APS5_VABI sceNpFriendListDialogClose(void); +int APS5_VABI sceNpFriendListDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + std::int64_t param = 0; + std::int32_t result[2] = {-1, -1}; + + Require(sceNpFriendListDialogTerminate() == kErrNotInitialized); + Require(sceNpFriendListDialogOpenA(¶m) == kErrNotInitialized); + Require(sceNpFriendListDialogClose() == kErrNotInitialized); + Require(sceNpFriendListDialogGetResultA(result) == kErrNotInitialized); + Require(sceNpFriendListDialogGetStatus() == kStatusNone); + Require(sceNpFriendListDialogUpdateStatus() == kStatusNone); + + Require(sceNpFriendListDialogInitialize() == 0); + Require(sceNpFriendListDialogInitialize() == kErrAlreadyInitialized); + Require(sceNpFriendListDialogGetStatus() == kStatusInitialized); + Require(sceNpFriendListDialogUpdateStatus() == kStatusInitialized); + + Require(sceNpFriendListDialogOpenA(nullptr) == kErrArgNull); + Require(sceNpFriendListDialogGetResultA(result) == kErrNotFinished); + + Require(sceNpFriendListDialogOpenA(¶m) == 0); + Require(sceNpFriendListDialogGetStatus() == kStatusFinished); + Require(sceNpFriendListDialogUpdateStatus() == kStatusFinished); + + Require(sceNpFriendListDialogGetResultA(nullptr) == kErrArgNull); + Require(sceNpFriendListDialogGetResultA(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(sceNpFriendListDialogClose() == 0); + Require(sceNpFriendListDialogTerminate() == 0); + Require(sceNpFriendListDialogTerminate() == kErrNotInitialized); + Require(sceNpFriendListDialogGetStatus() == kStatusNone); + Require(sceNpFriendListDialogUpdateStatus() == kStatusNone); + + Require(sceNpFriendListDialogInitialize() == 0); + Require(sceNpFriendListDialogUpdateStatus() == kStatusInitialized); + Require(sceNpFriendListDialogClose() == 0); + Require(sceNpFriendListDialogUpdateStatus() == kStatusFinished); + result[0] = -1; + Require(sceNpFriendListDialogGetResultA(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(sceNpFriendListDialogTerminate() == 0); +} diff --git a/core/libs/tests/GuestNpGameIntent.cpp b/core/libs/tests/GuestNpGameIntent.cpp new file mode 100644 index 0000000000..dbf030f79e --- /dev/null +++ b/core/libs/tests/GuestNpGameIntent.cpp @@ -0,0 +1,80 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpGameIntentInitParamInit(NpGameIntentInitParam* param); +int APS5_VABI sceNpGameIntentInfoInit(NpGameIntentInfo* info); +int APS5_VABI sceNpGameIntentInitialize(const void* initParam); +int APS5_VABI sceNpGameIntentReceiveIntent(NpGameIntentInfo* intentInfo); +int APS5_VABI sceNpGameIntentGetPropertyValueString(const NpGameIntentData* intentData, const char* key, char* valueBuf, size_t bufSize); +int APS5_VABI sceNpGameIntentVshInitialize(void); +int APS5_VABI sceNpGameIntentVshTerminate(void); +int APS5_VABI sceNpGameIntentTerminate(void); +int APS5_VABI sceNpAppLauncherInitialize(const void* initParam); +int APS5_VABI sceNpAppLauncherLaunchApp(void); +int APS5_VABI sceNpAppLauncherTerminate(void); +} + +static void Require(bool value, const char* what) { + if (value) return; + std::printf("NpGameIntent: %s\n", what); + std::abort(); +} + +template +static bool Throws(TCall call) { + try { + call(); + } catch (const TError&) { + return true; + } + return false; +} + +int main() { + constexpr int kIntentNotFound = -2141898746; + constexpr int kValueNotFound = -2141898745; + + Require(Throws([] { sceNpGameIntentInitParamInit(nullptr); }), + "null param init did not throw"); + + NpGameIntentInitParam initParam{}; + Require(sceNpGameIntentInitParamInit(&initParam) == 0, "param init failed"); + Require(initParam.size == sizeof(NpGameIntentInitParam), "param size mismatch"); + + Require(Throws([] { sceNpGameIntentInfoInit(nullptr); }), + "null info init did not throw"); + + NpGameIntentInfo info{}; + Require(sceNpGameIntentInfoInit(&info) == 0, "info init failed"); + Require(info.size == sizeof(NpGameIntentInfo), "info size mismatch"); + Require(info.user_id == -1, "info user_id mismatch"); + + Require(sceNpGameIntentInitialize(&initParam) == 0, "initialization failed"); + Require(sceNpGameIntentReceiveIntent(&info) == kIntentNotFound, "receive intent mismatch"); + + char buf[64]{}; + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", buf, sizeof(buf)) == kValueNotFound, + "get property value mismatch"); + + Require(sceNpGameIntentVshInitialize() == 0, "vsh init failed"); + Require(sceNpGameIntentVshTerminate() == 0, "vsh terminate failed"); + Require(sceNpGameIntentTerminate() == 0, "terminate failed"); + + unsigned char appLauncherParam[0x28]{}; + *reinterpret_cast(appLauncherParam) = 0x28; + Require(sceNpAppLauncherInitialize(appLauncherParam) == 0, "app launcher init failed"); + Require(Throws([] { sceNpAppLauncherLaunchApp(); }), + "app launcher launch must throw until implemented"); + Require(sceNpAppLauncherTerminate() == 0, "app launcher terminate failed"); + *reinterpret_cast(appLauncherParam) = 0x10; + Require(sceNpAppLauncherInitialize(appLauncherParam) == 0, + "app launcher init must accept any init parameter"); + + std::puts("NpGameIntent tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index eaa8a77d45..67e63ff3b0 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -4,9 +4,29 @@ #include #include #include +#include +#include extern "C" { +int APS5_VABI sceNpGetAccountCountryA(int user_id, void* country_code); int APS5_VABI sceNpGetNpId(int user_id, NpId* np_id); +int APS5_VABI sceNpGetOnlineId(int user_id, NpOnlineId* online_id); +int APS5_VABI sceNpGetUserIdByAccountId(std::uint64_t account_id, int* user_id); +int APS5_VABI sceNpUnknown_8weiQyIdI3A(int userId, const NpOnlineId* buffer); +int APS5_VABI sceNpUnknown_WxLcN76q19o(void); +int APS5_VABI sceNpGetAccountCountry(const NpOnlineId* online_id, void* country_code); +int APS5_VABI sceNpGetAccountDateOfBirth(const NpOnlineId* online_id, void* date_of_birth); +int APS5_VABI sceNpGetAccountLanguage(int req_id, const NpOnlineId* online_id, void* language); +int APS5_VABI sceNpGetParentalControlInfo(int req_id, const NpOnlineId* online_id, std::int8_t* age, void* info); +int APS5_VABI sceNpGetUserIdByOnlineId(const NpOnlineId* online_id, int* user_id); +int APS5_VABI sceNpCheckPlus(int req_id, const void* param, void* result); +int APS5_VABI sceNpNotifyPlusFeature(const void* param); +int APS5_VABI sceNpGetAccountId(const NpOnlineId* online_id, std::uint64_t* account_id); +int APS5_VABI sceNpSetGamePresenceOnline(int req_id, const NpOnlineId* online_id, std::uint32_t presence); +int APS5_VABI sceNpRegisterPlusEventCallback(void* callback, void* userdata); +int APS5_VABI sceNpUnregisterPlusEventCallback(void); +int APS5_VABI sceNpIsPlusMember(int user_id, bool* is_member); +int APS5_VABI sceNpSetContentRestriction(const NpContentRestriction* restriction); } extern "C" { @@ -17,10 +37,35 @@ int APS5_VABI sceNpSetTimeout(int reqId, std::int32_t resolveRetry, std::uint32_ std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout); } +extern "C" { +int APS5_VABI sceNpCheckCallback(void); +int APS5_VABI sceNpCheckCallbackForLib(void); +int APS5_VABI sceNpRegisterStateCallback(void* callback, void* userdata); +int APS5_VABI sceNpUnregisterStateCallback(void); +int APS5_VABI sceNpRegisterStateCallbackA(void* callback, void* userdata); +int APS5_VABI sceNpUnregisterStateCallbackA(int callbackId); +int APS5_VABI sceNpRegisterNpReachabilityStateCallback(void* callback, void* userdata); +void APS5_VABI sceNpRegisterGamePresenceCallback(void* callback, void* userdata); +int APS5_VABI sceNpUnregisterNpReachabilityStateCallback(void); +} + +extern "C" { +int APS5_VABI sceNpCheckNpAvailabilityA(int reqId, int userId); +int APS5_VABI sceNpGetAccountDateOfBirthA(int userId, void* dateOfBirth); +int APS5_VABI sceNpGetAccountLanguageA(int reqId, int userId, void* language); +int APS5_VABI sceNpGetGamePresenceStatus(const NpOnlineId* onlineId, std::uint32_t* status); +int APS5_VABI sceNpGetGamePresenceStatusA(int userId, std::uint32_t* status); +int APS5_VABI sceNpGetParentalControlInfoA(int reqId, int userId, std::int8_t* age, void* info); +} + namespace { constexpr int InvalidArgument = static_cast(0x80550003u); constexpr int SignedOut = static_cast(0x80550006u); +constexpr int InvalidSize = static_cast(0x80550011u); +constexpr int UserNotFound = static_cast(0x80550007u); +constexpr int AlreadyRegistered = static_cast(0x80550008u); +constexpr int NotRegistered = static_cast(0x80550009u); constexpr int RequestNotFound = static_cast(0x80550014u); void Require(bool condition, const char* message) { @@ -30,6 +75,143 @@ void Require(bool condition, const char* message) { } } +struct StateCall { + int calls; + int userId; + std::uint32_t state; + NpId* npId; + void* userdata; +}; + +constexpr int InitialUserId = 0x10000000; +constexpr std::uint32_t StateSignedOut = 1; +StateCall stateCallsA[9]; +StateCall legacyStateCall; + +template +void APS5_VABI StateCallbackA(int userId, std::uint32_t state, void* userdata) { + stateCallsA[Index] = {stateCallsA[Index].calls + 1, userId, state, nullptr, userdata}; +} + +void APS5_VABI LegacyStateCallback(int userId, std::uint32_t state, NpId* npId, void* userdata) { + legacyStateCall = {legacyStateCall.calls + 1, userId, state, npId, userdata}; +} + +bool SignedOutOnce(const StateCall& call, void* userdata) { + return call.calls == 1 && call.userId == InitialUserId && call.state == StateSignedOut && + call.npId == nullptr && call.userdata == userdata; +} + +void APS5_VABI AbortingGamePresenceCallback(const void*, const void*, void*) { + std::abort(); +} + +void APS5_VABI AbortingReachabilityCallback(int, std::uint32_t, void*) { + std::fprintf(stderr, "NpManager: the reachability callback was called\n"); + std::abort(); +} + +struct SelfUnregister { + int id; + int calls; + int result; +}; + +void APS5_VABI SelfUnregisteringCallback(int, std::uint32_t, void* userdata) { + auto* context = static_cast(userdata); + ++context->calls; + context->result = sceNpUnregisterStateCallbackA(context->id); +} + +void CheckStateCallbacksA() { + constexpr int CallbackMax = static_cast(0x8055001Du); + void* const callbacks[] = { + reinterpret_cast(&StateCallbackA<0>), reinterpret_cast(&StateCallbackA<1>), + reinterpret_cast(&StateCallbackA<2>), reinterpret_cast(&StateCallbackA<3>), + reinterpret_cast(&StateCallbackA<4>), reinterpret_cast(&StateCallbackA<5>), + reinterpret_cast(&StateCallbackA<6>), reinterpret_cast(&StateCallbackA<7>), + reinterpret_cast(&StateCallbackA<8>), + }; + int userdata[9]{}; + Require(sceNpRegisterStateCallbackA(nullptr, &userdata[0]) == InvalidArgument, "null state callback"); + int ids[8]{}; + ids[0] = sceNpRegisterStateCallbackA(callbacks[0], &userdata[0]); + Require(ids[0] >= 1 && ids[0] <= 8, "state callback id outside 1..8"); + Require(sceNpRegisterStateCallbackA(callbacks[0], &userdata[1]) == AlreadyRegistered, "duplicate state callback"); + Require(stateCallsA[0].calls == 0, "registering calls the state callback"); + Require(sceNpRegisterNpReachabilityStateCallback(reinterpret_cast(&AbortingReachabilityCallback), nullptr) == 0, + "registering the reachability callback"); + sceNpRegisterGamePresenceCallback(nullptr, nullptr); + sceNpRegisterGamePresenceCallback(reinterpret_cast(&AbortingGamePresenceCallback), &userdata[0]); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed"); + Require(SignedOutOnce(stateCallsA[0], &userdata[0]), "sceNpCheckCallback must deliver SIGNED_OUT once"); + Require(sceNpUnregisterNpReachabilityStateCallback() == 0, "unregistering the reachability callback"); + Require(sceNpCheckCallback() == 0, "second sceNpCheckCallback failed"); + Require(stateCallsA[0].calls == 1, "the second sceNpCheckCallback delivered the state again"); + for (int index = 1; index < 8; ++index) { + ids[index] = sceNpRegisterStateCallbackA(callbacks[index], &userdata[index]); + Require(ids[index] >= 1 && ids[index] <= 8, "state callback id outside 1..8"); + for (int other = 0; other < index; ++other) { + Require(ids[index] != ids[other], "two state callbacks got the same id"); + } + } + Require(sceNpRegisterStateCallbackA(callbacks[8], &userdata[8]) == CallbackMax, "ninth state callback"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with eight callbacks"); + for (int index = 0; index < 8; ++index) { + Require(SignedOutOnce(stateCallsA[index], &userdata[index]), "each callback gets SIGNED_OUT once"); + } + Require(stateCallsA[8].calls == 0, "a rejected callback was called"); + Require(sceNpUnregisterStateCallbackA(0) == InvalidArgument, "callback id 0"); + Require(sceNpUnregisterStateCallbackA(9) == InvalidArgument, "callback id 9"); + Require(sceNpUnregisterStateCallbackA(ids[3]) == 0, "unregistering one state callback"); + for (int index = 0; index < 8; ++index) { + if (index == 3) continue; + Require(sceNpRegisterStateCallbackA(callbacks[index], &userdata[index]) == AlreadyRegistered, + "unregistering one id removed another callback"); + } + Require(sceNpRegisterStateCallbackA(callbacks[3], &userdata[3]) == ids[3], "registering again takes the freed slot"); + Require(sceNpCheckCallbackForLib() == 0, "sceNpCheckCallbackForLib failed after registering again"); + for (int index = 0; index < 8; ++index) { + Require(stateCallsA[index].calls == (index == 3 ? 2 : 1), "only the registration made again gets SIGNED_OUT"); + } + Require(sceNpCheckCallbackForLib() == 0 && sceNpCheckCallback() == 0, "checking again failed"); + for (int index = 0; index < 8; ++index) { + Require(stateCallsA[index].calls == (index == 3 ? 2 : 1), "sceNpCheckCallbackForLib left the state pending"); + } + for (int index = 0; index < 8; ++index) { + Require(sceNpUnregisterStateCallbackA(ids[index]) == 0, "unregistering a state callback"); + } + Require(sceNpUnregisterStateCallbackA(ids[0]) == NotRegistered, "unregistering a state callback twice"); + const int removed = sceNpRegisterStateCallbackA(callbacks[8], &userdata[8]); + Require(removed >= 1 && removed <= 8, "a freed slot is reused"); + Require(sceNpUnregisterStateCallbackA(removed) == 0, "unregistering before sceNpCheckCallback"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with no callbacks"); + Require(stateCallsA[8].calls == 0, "an unregistered callback was called"); + SelfUnregister self{}; + self.id = sceNpRegisterStateCallbackA(reinterpret_cast(&SelfUnregisteringCallback), &self); + Require(self.id >= 1 && self.id <= 8, "state callback id outside 1..8"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with a callback that unregisters itself"); + Require(self.calls == 1 && self.result == 0, "a state callback must be able to unregister itself"); + Require(sceNpUnregisterStateCallbackA(self.id) == NotRegistered, "the callback did not unregister itself"); +} + +void CheckLegacyStateCallback() { + int userdata = 0; + NpId sentinel{}; + auto* callback = reinterpret_cast(&LegacyStateCallback); + Require(sceNpUnregisterStateCallback() == NotRegistered, "unregistering with no legacy callback"); + Require(sceNpRegisterStateCallback(nullptr, &userdata) == InvalidArgument, "null legacy callback"); + Require(sceNpRegisterStateCallback(callback, &userdata) == 0, "registering the legacy callback"); + Require(sceNpRegisterStateCallback(callback, nullptr) == AlreadyRegistered, "duplicate legacy callback"); + legacyStateCall.npId = &sentinel; + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with the legacy callback"); + Require(SignedOutOnce(legacyStateCall, &userdata), "the legacy callback must get SIGNED_OUT with no NpId"); + Require(sceNpCheckCallback() == 0, "second sceNpCheckCallback failed with the legacy callback"); + Require(legacyStateCall.calls == 1, "the legacy callback got the state again"); + Require(sceNpUnregisterStateCallback() == 0, "unregistering the legacy callback"); + Require(sceNpUnregisterStateCallback() == NotRegistered, "unregistering the legacy callback twice"); +} + void CheckRequestTimeouts() { Require(sceNpSetTimeout(0x7fffffff, 2, 0, 0, 0, 0) == RequestNotFound, "never created request"); Require(sceNpSetTimeout(0x7fffffff, 0, 0, 0, 0, 0) == InvalidArgument, "arguments checked before the request"); @@ -56,10 +238,163 @@ void CheckRequestTimeouts() { Require(sceNpSetTimeout(asyncId, 2, 0, 0, 0, 0) == RequestNotFound, "deleted async request"); } +void CheckOnlineIdQueries() { + NpOnlineId onlineId{}; + std::memcpy(onlineId.data, "player", 6); + unsigned char output[64]; + std::memset(output, 0x5a, sizeof(output)); + unsigned char untouched[64]; + std::memset(untouched, 0x5a, sizeof(untouched)); + std::int8_t age = 0x5a; + int userId = 0x5a5a; + Require(sceNpGetAccountCountry(&onlineId, output) == UserNotFound, "sceNpGetAccountCountry must find no user"); + Require(sceNpGetAccountCountry(nullptr, output) == InvalidArgument, "sceNpGetAccountCountry must reject a null online id"); + Require(sceNpGetAccountCountry(&onlineId, nullptr) == InvalidArgument, "sceNpGetAccountCountry must reject a null output"); + Require(sceNpGetAccountDateOfBirth(&onlineId, output) == UserNotFound, "sceNpGetAccountDateOfBirth must find no user"); + Require(sceNpGetAccountDateOfBirth(nullptr, output) == InvalidArgument, "sceNpGetAccountDateOfBirth must reject a null online id"); + Require(sceNpGetAccountDateOfBirth(&onlineId, nullptr) == InvalidArgument, "sceNpGetAccountDateOfBirth must reject a null output"); + Require(sceNpGetAccountLanguage(1, &onlineId, output) == UserNotFound, "sceNpGetAccountLanguage must find no user"); + Require(sceNpGetAccountLanguage(1, nullptr, output) == InvalidArgument, "sceNpGetAccountLanguage must reject a null online id"); + Require(sceNpGetAccountLanguage(1, &onlineId, nullptr) == InvalidArgument, "sceNpGetAccountLanguage must reject a null output"); + Require(sceNpGetParentalControlInfo(1, &onlineId, &age, output) == UserNotFound, "sceNpGetParentalControlInfo must find no user"); + Require(sceNpGetParentalControlInfo(1, nullptr, &age, output) == InvalidArgument, "sceNpGetParentalControlInfo must reject a null online id"); + Require(sceNpGetParentalControlInfo(1, &onlineId, nullptr, output) == InvalidArgument, "sceNpGetParentalControlInfo must reject a null age"); + Require(sceNpGetParentalControlInfo(1, &onlineId, &age, nullptr) == InvalidArgument, "sceNpGetParentalControlInfo must reject a null info"); + Require(sceNpGetUserIdByOnlineId(&onlineId, &userId) == SignedOut, "sceNpGetUserIdByOnlineId must report NP as signed out"); + Require(sceNpGetUserIdByOnlineId(nullptr, &userId) == InvalidArgument, "sceNpGetUserIdByOnlineId must reject a null online id"); + Require(sceNpGetUserIdByOnlineId(&onlineId, nullptr) == InvalidArgument, "sceNpGetUserIdByOnlineId must reject a null output"); + Require(sceNpCheckPlus(1, output, output + 32) == SignedOut, "sceNpCheckPlus must report NP as signed out"); + Require(sceNpCheckPlus(0, output, output + 32) == InvalidArgument, "sceNpCheckPlus must reject request id 0"); + Require(sceNpCheckPlus(1, nullptr, output + 32) == InvalidArgument, "sceNpCheckPlus must reject a null parameter"); + Require(sceNpCheckPlus(1, output, nullptr) == InvalidArgument, "sceNpCheckPlus must reject a null result"); + std::uint64_t accountId = 0x5a5a; + Require(sceNpGetAccountId(&onlineId, &accountId) == SignedOut, "sceNpGetAccountId must report NP as signed out"); + Require(sceNpGetAccountId(nullptr, &accountId) == InvalidArgument, "sceNpGetAccountId must reject a null online id"); + Require(sceNpGetAccountId(&onlineId, nullptr) == InvalidArgument, "sceNpGetAccountId must reject a null output"); + const int presenceRequest = sceNpCreateRequest(); + Require(presenceRequest > 0, "sceNpCreateRequest must return a positive request id"); + Require(sceNpSetGamePresenceOnline(presenceRequest, &onlineId, 0) == SignedOut, "sceNpSetGamePresenceOnline must report NP as signed out"); + Require(sceNpSetGamePresenceOnline(0, &onlineId, 0) == InvalidArgument, "sceNpSetGamePresenceOnline must reject request id 0"); + Require(sceNpSetGamePresenceOnline(presenceRequest, nullptr, 0) == InvalidArgument, "sceNpSetGamePresenceOnline must reject a null online id"); + Require(sceNpSetGamePresenceOnline(0x7fffffff, &onlineId, 0) == RequestNotFound, "sceNpSetGamePresenceOnline must reject a request that was never created"); + Require(sceNpDeleteRequest(presenceRequest) == 0, "sceNpDeleteRequest failed"); + Require(sceNpSetGamePresenceOnline(presenceRequest, &onlineId, 0) == RequestNotFound, "sceNpSetGamePresenceOnline must reject a deleted request"); + Require(sceNpRegisterPlusEventCallback(output, nullptr) == 0 && sceNpUnregisterPlusEventCallback() == 0, "the plus event callback must register and unregister"); + Require(sceNpUnregisterPlusEventCallback() == 0, "unregistering the plus event callback again must succeed like the premium one"); + Require(std::memcmp(output, untouched, sizeof(output)) == 0 && age == 0x5a && userId == 0x5a5a && accountId == 0x5a5a, "a failed query wrote its output"); + try { + sceNpNotifyPlusFeature(output); + Require(false, "sceNpNotifyPlusFeature must report that it is not implemented"); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()) == "sceNpNotifyPlusFeature not implemented", "sceNpNotifyPlusFeature threw"); + } +} + +void CheckUserIdQueries() { + constexpr int InvalidUserId = -1; + constexpr int OtherUserId = 0x10000; + constexpr int NeverCreated = 0x7fffffff; + unsigned char output[64]; + std::memset(output, 0x5a, sizeof(output)); + unsigned char untouched[64]; + std::memset(untouched, 0x5a, sizeof(untouched)); + std::int8_t age = 0x5a; + const int request = sceNpCreateRequest(); + Require(request > 0, "sceNpCreateRequest must return a positive request id"); + for (const int userId : {InitialUserId, OtherUserId}) { + Require(sceNpCheckNpAvailabilityA(request, userId) == SignedOut, "sceNpCheckNpAvailabilityA must report the user as signed out"); + Require(sceNpGetAccountLanguageA(request, userId, output) == SignedOut, "sceNpGetAccountLanguageA must report the user as signed out"); + Require(sceNpGetParentalControlInfoA(request, userId, &age, output) == SignedOut, "sceNpGetParentalControlInfoA must report the user as signed out"); + Require(sceNpGetAccountDateOfBirthA(userId, output) == SignedOut, "sceNpGetAccountDateOfBirthA must report the user as signed out"); + } + Require(sceNpCheckNpAvailabilityA(request, InvalidUserId) == InvalidArgument, "sceNpCheckNpAvailabilityA must reject the invalid user id"); + Require(sceNpCheckNpAvailabilityA(0, InitialUserId) == InvalidArgument, "sceNpCheckNpAvailabilityA must reject request id 0"); + Require(sceNpCheckNpAvailabilityA(NeverCreated, InitialUserId) == RequestNotFound, "sceNpCheckNpAvailabilityA must reject a request that was never created"); + Require(sceNpCheckNpAvailabilityA(NeverCreated, InvalidUserId) == InvalidArgument, "sceNpCheckNpAvailabilityA must check the user before the request"); + Require(sceNpGetAccountLanguageA(request, InitialUserId, nullptr) == InvalidArgument, "sceNpGetAccountLanguageA must reject a null output"); + Require(sceNpGetAccountLanguageA(request, InvalidUserId, output) == InvalidArgument, "sceNpGetAccountLanguageA must reject the invalid user id"); + Require(sceNpGetAccountLanguageA(0, InitialUserId, output) == InvalidArgument, "sceNpGetAccountLanguageA must reject request id 0"); + Require(sceNpGetAccountLanguageA(NeverCreated, InitialUserId, output) == RequestNotFound, "sceNpGetAccountLanguageA must reject a request that was never created"); + Require(sceNpGetAccountLanguageA(NeverCreated, InitialUserId, nullptr) == InvalidArgument, "sceNpGetAccountLanguageA must check the output before the request"); + Require(sceNpGetParentalControlInfoA(request, InitialUserId, nullptr, output) == InvalidArgument, "sceNpGetParentalControlInfoA must reject a null age"); + Require(sceNpGetParentalControlInfoA(request, InitialUserId, &age, nullptr) == InvalidArgument, "sceNpGetParentalControlInfoA must reject a null info"); + Require(sceNpGetParentalControlInfoA(request, InvalidUserId, &age, output) == InvalidArgument, "sceNpGetParentalControlInfoA must reject the invalid user id"); + Require(sceNpGetParentalControlInfoA(0, InitialUserId, &age, output) == InvalidArgument, "sceNpGetParentalControlInfoA must reject request id 0"); + Require(sceNpGetParentalControlInfoA(NeverCreated, InitialUserId, &age, output) == RequestNotFound, "sceNpGetParentalControlInfoA must reject a request that was never created"); + Require(sceNpGetParentalControlInfoA(NeverCreated, InvalidUserId, &age, output) == InvalidArgument, "sceNpGetParentalControlInfoA must check the user before the request"); + Require(sceNpGetAccountDateOfBirthA(InitialUserId, nullptr) == InvalidArgument, "sceNpGetAccountDateOfBirthA must reject a null output"); + Require(sceNpGetAccountDateOfBirthA(InvalidUserId, output) == InvalidArgument, "sceNpGetAccountDateOfBirthA must reject the invalid user id"); + Require(sceNpDeleteRequest(request) == 0, "sceNpDeleteRequest failed"); + Require(sceNpCheckNpAvailabilityA(request, InitialUserId) == RequestNotFound, "sceNpCheckNpAvailabilityA must reject a deleted request"); + Require(sceNpGetAccountLanguageA(request, InitialUserId, output) == RequestNotFound, "sceNpGetAccountLanguageA must reject a deleted request"); + Require(sceNpGetParentalControlInfoA(request, InitialUserId, &age, output) == RequestNotFound, "sceNpGetParentalControlInfoA must reject a deleted request"); + Require(std::memcmp(output, untouched, sizeof(output)) == 0 && age == 0x5a, "a signed-out account query wrote its output"); + constexpr std::uint32_t Sentinel = 0x5a5a5a5au; + constexpr std::uint32_t Offline = 0; + NpOnlineId onlineId{}; + std::memcpy(onlineId.data, "player", 6); + for (const int userId : {InitialUserId, OtherUserId}) { + std::uint32_t status = Sentinel; + Require(sceNpGetGamePresenceStatusA(userId, &status) == 0 && status == Offline, "sceNpGetGamePresenceStatusA must report the user as offline"); + } + std::uint32_t status = Sentinel; + Require(sceNpGetGamePresenceStatusA(InvalidUserId, &status) == InvalidArgument, "sceNpGetGamePresenceStatusA must reject the invalid user id"); + Require(sceNpGetGamePresenceStatusA(InitialUserId, nullptr) == InvalidArgument, "sceNpGetGamePresenceStatusA must reject a null status"); + Require(sceNpGetGamePresenceStatus(nullptr, &status) == InvalidArgument, "sceNpGetGamePresenceStatus must reject a null online id"); + Require(sceNpGetGamePresenceStatus(&onlineId, nullptr) == InvalidArgument, "sceNpGetGamePresenceStatus must reject a null status"); + Require(status == Sentinel, "a rejected presence query wrote its status"); + Require(sceNpGetGamePresenceStatus(&onlineId, &status) == 0 && status == Offline, "sceNpGetGamePresenceStatus must report the online id as offline"); +} + +} + +struct ContentRestrictionFields { + std::uint64_t size; + std::int8_t defaultAgeRestriction; + std::uint8_t padding[3]; + std::int32_t ageRestrictionCount; + const void* ageRestrictions; +}; + +int SetContentRestriction(const ContentRestrictionFields& fields) { + return sceNpSetContentRestriction(reinterpret_cast(&fields)); +} + +void CheckPlusAndContentRestriction() { + bool isMember = true; + Require(sceNpIsPlusMember(0x10000, &isMember) == 0 && !isMember, "sceNpIsPlusMember must report no Plus membership"); + Require(sceNpIsPlusMember(0x10000, nullptr) == InvalidArgument, "sceNpIsPlusMember must reject a null output"); + + const std::uint8_t ages[8]{}; + ContentRestrictionFields fields{sizeof(ContentRestrictionFields), 12, {}, 2, ages}; + Require(SetContentRestriction(fields) == 0, "sceNpSetContentRestriction must accept a valid restriction"); + fields.ageRestrictionCount = 0; + fields.ageRestrictions = nullptr; + Require(SetContentRestriction(fields) == 0, "sceNpSetContentRestriction must accept no age restrictions"); + Require(sceNpSetContentRestriction(nullptr) == InvalidArgument, "sceNpSetContentRestriction must reject null"); + auto bad = fields; + bad.size = sizeof(ContentRestrictionFields) + 8; + Require(SetContentRestriction(bad) == InvalidSize, "sceNpSetContentRestriction must check the size"); + bad = fields; + bad.defaultAgeRestriction = -1; + Require(SetContentRestriction(bad) == InvalidArgument, "sceNpSetContentRestriction must reject a negative default age"); + bad = fields; + bad.ageRestrictionCount = 0x101; + bad.ageRestrictions = ages; + Require(SetContentRestriction(bad) == InvalidArgument, "sceNpSetContentRestriction must reject more than 256 restrictions"); + bad.ageRestrictionCount = 0x100; + Require(SetContentRestriction(bad) == 0, "sceNpSetContentRestriction must accept 256 restrictions"); + bad.ageRestrictions = nullptr; + Require(SetContentRestriction(bad) == InvalidArgument, "sceNpSetContentRestriction must reject restrictions without an array"); } int main() { + CheckOnlineIdQueries(); + CheckUserIdQueries(); + CheckStateCallbacksA(); + CheckLegacyStateCallback(); CheckRequestTimeouts(); + CheckPlusAndContentRestriction(); NpId npId{}; std::memset(&npId, 0x5a, sizeof(npId)); NpId untouched{}; @@ -67,5 +402,39 @@ int main() { Require(sceNpGetNpId(0x10000, &npId) == SignedOut, "sceNpGetNpId must report the user as signed out"); Require(std::memcmp(&npId, &untouched, sizeof(npId)) == 0, "sceNpGetNpId must leave the NpId untouched"); Require(sceNpGetNpId(0x10000, nullptr) == InvalidArgument, "sceNpGetNpId must reject a null NpId"); + char countryCode[4]{}; + std::memset(countryCode, 0x5a, sizeof(countryCode)); + char untouchedCountryCode[4]{}; + std::memset(untouchedCountryCode, 0x5a, sizeof(untouchedCountryCode)); + Require(sceNpGetAccountCountryA(0x10000, countryCode) == SignedOut, "sceNpGetAccountCountryA must report the user as signed out"); + Require(std::memcmp(countryCode, untouchedCountryCode, sizeof(countryCode)) == 0, "sceNpGetAccountCountryA must leave the country code untouched"); + Require(sceNpGetAccountCountryA(0x10000, nullptr) == InvalidArgument, "sceNpGetAccountCountryA must reject a null country code"); + NpOnlineId onlineId{}; + std::memset(&onlineId, 0x5a, sizeof(onlineId)); + NpOnlineId untouchedOnlineId{}; + std::memset(&untouchedOnlineId, 0x5a, sizeof(untouchedOnlineId)); + Require(sceNpGetOnlineId(0x10000, &onlineId) == SignedOut, "sceNpGetOnlineId must report the user as signed out"); + Require(std::memcmp(&onlineId, &untouchedOnlineId, sizeof(onlineId)) == 0, "sceNpGetOnlineId must leave the online id untouched"); + Require(sceNpGetOnlineId(0x10000, nullptr) == InvalidArgument, "sceNpGetOnlineId must reject a null online id"); + int userId = 0x5a5a; + Require(sceNpGetUserIdByAccountId(0, &userId) == InvalidArgument, "sceNpGetUserIdByAccountId must reject account id 0"); + Require(sceNpGetUserIdByAccountId(0x1234, nullptr) == InvalidArgument, "sceNpGetUserIdByAccountId must reject a null output"); + Require(sceNpGetUserIdByAccountId(0x1234, &userId) == SignedOut, "sceNpGetUserIdByAccountId must report NP as signed out"); + Require(userId == 0x5a5a, "sceNpGetUserIdByAccountId must leave the user id untouched"); + NpOnlineId buffer{}; + 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/GuestNpParty.cpp b/core/libs/tests/GuestNpParty.cpp new file mode 100644 index 0000000000..e60834546c --- /dev/null +++ b/core/libs/tests/GuestNpParty.cpp @@ -0,0 +1,88 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpPartyGetState(std::uint16_t* state); +int APS5_VABI sceNpPartyInitialize(const void* param); +int APS5_VABI sceNpPartyTerminate(void); +int APS5_VABI sceNpPartyCheckCallback(void); +int APS5_VABI sceNpPartyRegisterHandlerA(const void* handlers, void* userdata); +int APS5_VABI sceNpPartyGetStateAsUserA(std::int32_t asUserId, std::uint16_t* state); +int APS5_VABI sceNpPartyGetId(void* id); +int APS5_VABI sceNpPartyGetMembersA(void* memberList); +int APS5_VABI sceNpPartyGetMemberInfoA(std::uint16_t memberId, void* memberInfo); +int APS5_VABI sceNpPartyGetMemberSessionInfo(std::uint16_t memberId, void* memberGameSessionInfo); +int APS5_VABI sceNpPartyGetMemberVoiceInfo(std::uint16_t asMemberId, std::uint16_t memberId, std::uint8_t* memberVoiceState); +int APS5_VABI sceNpPartySendBinaryMessage(const void* sendParams); +} + +namespace { + +constexpr int kAlreadyInitialized = static_cast(0x80552502u); +constexpr int kNotInitialized = static_cast(0x80552503u); +constexpr int kInvalidArgument = static_cast(0x80552504u); +constexpr int kNotInParty = static_cast(0x80552506u); + +bool check(bool value, const char* message) { + if (!value) std::fprintf(stderr, "%s\n", message); + return value; +} + +bool stateTests() { + std::uint16_t states[3]; + std::memset(states, 0xa5, sizeof(states)); + return check(sceNpPartyGetState(nullptr) == kInvalidArgument, "a null state was not refused with INVALID_ARGUMENT") + && check(sceNpPartyGetState(&states[1]) == 0 && states[1] == 2, "the state is not NOT_IN_PARTY") + && check(states[0] == 0xa5a5 && states[2] == 0xa5a5, "sceNpPartyGetState wrote past its 16-bit state") + && check(sceNpPartyGetStateAsUserA(1, nullptr) == kInvalidArgument, "a null user state was not refused with INVALID_ARGUMENT") + && check(sceNpPartyGetStateAsUserA(1, &states[0]) == 0 && states[0] == 2, "the user state is not NOT_IN_PARTY") + && check(states[2] == 0xa5a5, "sceNpPartyGetStateAsUserA wrote past its 16-bit state"); +} + +bool lifecycleTests() { + std::uint64_t param[4] = {}; + std::uint64_t handlers[8] = {}; + return check(sceNpPartyTerminate() == kNotInitialized, "terminate before initialize did not fail") + && check(sceNpPartyCheckCallback() == kNotInitialized, "check callback before initialize did not fail") + && check(sceNpPartyRegisterHandlerA(handlers, nullptr) == kNotInitialized, "register handler before initialize did not fail") + && check(sceNpPartyInitialize(nullptr) == kInvalidArgument, "a null initialize param was not refused") + && check(sceNpPartyInitialize(param) == 0, "initialize failed") + && check(sceNpPartyInitialize(param) == kAlreadyInitialized, "a second initialize did not fail") + && check(sceNpPartyRegisterHandlerA(nullptr, nullptr) == kInvalidArgument, "null handlers were not refused") + && check(sceNpPartyRegisterHandlerA(handlers, param) == 0, "register handler failed") + && check(sceNpPartyCheckCallback() == 0, "check callback failed") + && check(sceNpPartyTerminate() == 0, "terminate failed") + && check(sceNpPartyTerminate() == kNotInitialized, "a second terminate did not fail") + && check(sceNpPartyInitialize(param) == 0, "initialize after terminate failed") + && check(sceNpPartyTerminate() == 0, "terminate after reinitialize failed"); +} + +bool notInPartyTests() { + std::uint8_t out[64]; + std::memset(out, 0xa5, sizeof(out)); + bool ok = check(sceNpPartyGetId(nullptr) == kInvalidArgument, "a null party id was not refused") + && check(sceNpPartyGetId(out) == kNotInParty, "get id did not report NOT_IN_PARTY") + && check(sceNpPartyGetMembersA(nullptr) == kInvalidArgument, "a null member list was not refused") + && check(sceNpPartyGetMembersA(out) == kNotInParty, "get members did not report NOT_IN_PARTY") + && check(sceNpPartyGetMemberInfoA(1, nullptr) == kInvalidArgument, "a null member info was not refused") + && check(sceNpPartyGetMemberInfoA(1, out) == kNotInParty, "get member info did not report NOT_IN_PARTY") + && check(sceNpPartyGetMemberSessionInfo(1, nullptr) == kInvalidArgument, "a null session info was not refused") + && check(sceNpPartyGetMemberSessionInfo(1, out) == kNotInParty, "get session info did not report NOT_IN_PARTY") + && check(sceNpPartyGetMemberVoiceInfo(1, 2, nullptr) == kInvalidArgument, "a null voice state was not refused") + && check(sceNpPartyGetMemberVoiceInfo(1, 2, out) == kNotInParty, "get voice info did not report NOT_IN_PARTY") + && check(sceNpPartySendBinaryMessage(nullptr) == kInvalidArgument, "null send params were not refused") + && check(sceNpPartySendBinaryMessage(out) == kNotInParty, "send did not report NOT_IN_PARTY"); + for (std::uint8_t byte : out) ok = ok && check(byte == 0xa5, "a NOT_IN_PARTY call wrote its output"); + return ok; +} + +} + +int main() { + if (!stateTests() || !lifecycleTests() || !notInPartyTests()) return 1; + std::puts("NpParty tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestNpReachability.cpp b/core/libs/tests/GuestNpReachability.cpp index 4e0c1f2262..e6995ac80d 100644 --- a/core/libs/tests/GuestNpReachability.cpp +++ b/core/libs/tests/GuestNpReachability.cpp @@ -6,7 +6,7 @@ int APS5_VABI sceNpRegisterNpReachabilityStateCallback(void*, void*); int APS5_VABI sceNpUnregisterNpReachabilityStateCallback(); int APS5_VABI sceNpGetNpReachabilityState(int, uint32_t*); int APS5_VABI sceNpCheckCallback(); -int APS5_VABI sceNpCheckNpAvailability(int, const NpOnlineId*); +int APS5_VABI sceNpCheckNpAvailability(int, const NpOnlineId*, std::uint8_t*); } static constexpr int ErrorArgument = static_cast(0x80550003); @@ -33,6 +33,14 @@ int main() { Require(sceNpUnregisterNpReachabilityStateCallback() == 0); NpOnlineId onlineId{}; onlineId.data[0] = 'p'; - Require(sceNpCheckNpAvailability(1, &onlineId) == ErrorUserNotFound); - Require(sceNpCheckNpAvailability(1, nullptr) == ErrorArgument); + Require(sceNpCheckNpAvailability(1, &onlineId, nullptr) == ErrorUserNotFound); + Require(sceNpCheckNpAvailability(1, nullptr, nullptr) == ErrorArgument); + std::uint8_t available[3]{0x55, 0x55, 0x55}; + Require(sceNpCheckNpAvailability(0, &onlineId, &available[1]) == ErrorArgument); + Require(available[1] == 0x55); + Require(sceNpCheckNpAvailability(1, nullptr, &available[1]) == ErrorArgument); + Require(available[1] == 0x55); + Require(sceNpCheckNpAvailability(1, &onlineId, &available[1]) == ErrorUserNotFound); + Require(available[0] == 0x55 && available[1] == 0 && available[2] == 0x55); + } diff --git a/core/libs/tests/GuestNpSessionSignaling.cpp b/core/libs/tests/GuestNpSessionSignaling.cpp index 27bc077144..982664e90c 100644 --- a/core/libs/tests/GuestNpSessionSignaling.cpp +++ b/core/libs/tests/GuestNpSessionSignaling.cpp @@ -3,10 +3,14 @@ #include #include #include +#include extern "C" { int APS5_VABI sceNpSessionSignalingInitialize(void* param); +int APS5_VABI sceNpSessionSignalingCreateContext(const void* param, std::uint32_t* contextId); int APS5_VABI sceNpSessionSignalingCreateContext2(const void* param, std::uint32_t* contextId); +int APS5_VABI sceNpSessionSignalingDestroyContext(std::uint32_t contextId); +int APS5_VABI sceNpSessionSignalingDeactivate(std::uint32_t contextId, std::uint32_t connectionId); std::int32_t APS5_VABI sceNpSessionSignalingGetLocalNetInfo(std::int32_t contextId, void* info); int APS5_VABI sceNpSessionSignalingRequestPrepare(std::uint32_t contextId, std::uint32_t* requestId); std::int32_t APS5_VABI sceNpSessionSignalingGetConnectionStatus(std::int32_t contextId, std::int32_t connectionId, std::int32_t* status, void* peerAddress, std::uint16_t* peerPort); @@ -18,6 +22,11 @@ namespace { constexpr int InvalidArgument = static_cast(0x80553303u); constexpr int Unavailable = static_cast(0x80552D06u); constexpr int ConnectionNotFound = static_cast(0x8055330Cu); +constexpr int NotInitialized = static_cast(0x80553301u); +constexpr int AlreadyInitialized = static_cast(0x80553302u); +constexpr int ContextLimit = static_cast(0x80553307u); +constexpr int ContextNotFound = static_cast(0x80553308u); +constexpr int GroupNotFound = static_cast(0x8055330Au); void Require(bool condition, const char* message) { if (!condition) { @@ -29,13 +38,45 @@ void Require(bool condition, const char* message) { } int main() { - unsigned char initializeParam[32] = {}; + alignas(8) unsigned char initializeParam[40] = {}; + Require(sceNpSessionSignalingTerminate() == NotInitialized, "terminate before initialize"); + Require(sceNpSessionSignalingCreateContext2(nullptr, nullptr) == NotInitialized, "initialization check precedes context arguments"); + Require(sceNpSessionSignalingGetConnectionStatus(0, 0, nullptr, nullptr, nullptr) == NotInitialized, "status before initialize"); + Require(sceNpSessionSignalingInitialize(nullptr) == InvalidArgument, "null initialize parameter"); + Require(sceNpSessionSignalingInitialize(initializeParam) == InvalidArgument, "zero pool size"); + const std::uint64_t poolSize = 4 * 1024 * 1024; + std::memcpy(initializeParam + 8, &poolSize, sizeof(poolSize)); Require(sceNpSessionSignalingInitialize(initializeParam) == 0, "initialize failed"); - unsigned char contextParam[64] = {}; + Require(sceNpSessionSignalingInitialize(nullptr) == AlreadyInitialized, "initialization check precedes initialize arguments"); + alignas(8) unsigned char contextParam[56] = {}; std::uint32_t contextId = 0; + Require(sceNpSessionSignalingCreateContext(nullptr, &contextId) == InvalidArgument, "null context parameter"); + Require(contextId == 0, "invalid context parameter wrote an id"); + Require(sceNpSessionSignalingCreateContext2(nullptr, &contextId) == InvalidArgument, "null context2 parameter"); + Require(contextId == 0, "invalid context2 parameter wrote an id"); + Require(sceNpSessionSignalingCreateContext(contextParam, nullptr) == InvalidArgument, "null context id"); + Require(sceNpSessionSignalingCreateContext2(contextParam, nullptr) == InvalidArgument, "null context2 id"); Require(sceNpSessionSignalingCreateContext2(contextParam, &contextId) == 0, "context creation failed"); + const auto firstId = contextId; + Require(sceNpSessionSignalingCreateContext2(contextParam, &contextId) == ContextLimit, "duplicate user context"); + Require(contextId == firstId, "duplicate context modified output"); + std::array otherContexts{}; + for (std::int32_t userId = 1; userId <= 9; ++userId) { + std::memcpy(contextParam, &userId, sizeof(userId)); + Require(sceNpSessionSignalingCreateContext2(contextParam, &otherContexts[userId - 1]) == 0, "create remaining contexts"); + } + const std::int32_t overflowUser = 10; + std::memcpy(contextParam, &overflowUser, sizeof(overflowUser)); + Require(sceNpSessionSignalingCreateContext2(contextParam, &contextId) == ContextLimit, "context limit"); + Require(contextId == firstId, "context limit modified output"); + Require(sceNpSessionSignalingDestroyContext(firstId) == 0, "destroy first context"); + Require(sceNpSessionSignalingDestroyContext(firstId) == ContextNotFound, "destroy stale context"); + Require(sceNpSessionSignalingCreateContext2(contextParam, &contextId) == 0, "reuse context slot"); + Require(contextId != firstId, "reused context kept stale id"); std::uint32_t requestId = 0xa5a5a5a5u; + Require(sceNpSessionSignalingRequestPrepare(firstId, &requestId) == ContextNotFound, "prepare stale context"); + Require(sceNpSessionSignalingRequestPrepare(firstId, nullptr) == InvalidArgument, "prepare argument precedence"); Require(sceNpSessionSignalingRequestPrepare(contextId, nullptr) == InvalidArgument, "null request id"); Require(sceNpSessionSignalingRequestPrepare(contextId, &requestId) == Unavailable, "prepare without network"); Require(requestId == 0xa5a5a5a5u, "failed prepare wrote a request id"); @@ -52,10 +93,18 @@ int main() { std::int32_t status = 0x5a5a5a5a; std::uint32_t peerAddress = 0xa5a5a5a5u; std::uint16_t peerPort = 0xa5a5; + Require(sceNpSessionSignalingGetConnectionStatus(firstId, 1, &status, &peerAddress, &peerPort) == ContextNotFound, "status stale context"); Require(sceNpSessionSignalingGetConnectionStatus(netContextId, 1, nullptr, &peerAddress, &peerPort) == InvalidArgument, "null connection status"); Require(sceNpSessionSignalingGetConnectionStatus(netContextId, 1, &status, &peerAddress, &peerPort) == ConnectionNotFound, "connection without network"); Require(sceNpSessionSignalingGetConnectionStatus(netContextId, 1, &status, nullptr, nullptr) == ConnectionNotFound, "connection without peer outputs"); Require(status == 0x5a5a5a5a && peerAddress == 0xa5a5a5a5u && peerPort == 0xa5a5, "failed status query wrote an output"); + Require(sceNpSessionSignalingDeactivate(firstId, 1) == ContextNotFound, "deactivate stale context"); + Require(sceNpSessionSignalingDeactivate(contextId, 1) == GroupNotFound, "deactivate nonexistent group"); Require(sceNpSessionSignalingTerminate() == 0, "terminate failed"); + Require(sceNpSessionSignalingDestroyContext(contextId) == NotInitialized, "destroy after terminate"); + Require(sceNpSessionSignalingTerminate() == NotInitialized, "repeated terminate"); + Require(sceNpSessionSignalingInitialize(initializeParam) == 0, "reinitialize"); + Require(sceNpSessionSignalingRequestPrepare(contextId, &requestId) == ContextNotFound, "reinitialize retained a context"); + Require(sceNpSessionSignalingTerminate() == 0, "final terminate"); } diff --git a/core/libs/tests/GuestNpTrophy.cpp b/core/libs/tests/GuestNpTrophy.cpp new file mode 100644 index 0000000000..e8a9b8749f --- /dev/null +++ b/core/libs/tests/GuestNpTrophy.cpp @@ -0,0 +1,119 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpTrophyCreateContext(int* context, int userId, std::uint32_t serviceLabel, std::uint64_t options); +int APS5_VABI sceNpTrophyDestroyContext(int context); +int APS5_VABI sceNpTrophyCreateHandle(int* handle); +int APS5_VABI sceNpTrophyDestroyHandle(int handle); +int APS5_VABI sceNpTrophyRegisterContext(int context, int handle, std::uint64_t options); +int APS5_VABI sceNpTrophyGetTrophyUnlockState(int context, int handle, std::uint32_t* flags, std::uint32_t* count); +int APS5_VABI sceNpTrophyUnlockTrophy(int context, int handle, int trophyId, int* platinumId); +int APS5_VABI sceNpTrophyGetGameIcon(int context, int handle, void* buffer, std::uint64_t* size); +int APS5_VABI sceNpTrophyGetTrophyIcon(int context, int handle, int trophyId, void* buffer, std::uint64_t* size); +} + +namespace { + +constexpr int InvalidArgument = static_cast(0x80551604u); +constexpr int InsufficientBuffer = static_cast(0x80551605u); +constexpr int InvalidContext = static_cast(0x80551609u); +constexpr int InvalidTrophyId = static_cast(0x8055160au); +constexpr int AlreadyUnlocked = static_cast(0x8055160cu); +constexpr int NotRegistered = static_cast(0x8055160fu); +constexpr int AlreadyRegistered = static_cast(0x80551610u); +constexpr int ContextAlreadyExists = static_cast(0x80551613u); +constexpr int TitleConfNotInstalled = static_cast(0x80551626u); +constexpr int TrophyNotUnlocked = static_cast(0x8055161au); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "NpTrophy: %s\n", message); + std::abort(); + } +} + +void PutBigEndian(std::vector& bytes, std::size_t at, std::uint64_t value, int width) { + for (int i = 0; i < width; ++i) bytes[at + i] = static_cast(value >> (8 * (width - 1 - i))); +} + +void WritePackage(const std::filesystem::path& path, const std::vector>& files) { + const std::size_t header = 0x60, entry = 0x40; + std::vector bytes(header + entry * files.size()); + PutBigEndian(bytes, 0, 0xdca24d00u, 4); + PutBigEndian(bytes, 0x10, files.size(), 4); + PutBigEndian(bytes, 0x14, entry, 4); + for (std::size_t i = 0; i < files.size(); ++i) { + const auto at = header + entry * i; + std::memcpy(bytes.data() + at, files[i].first.data(), files[i].first.size()); + PutBigEndian(bytes, at + 32, bytes.size(), 8); + PutBigEndian(bytes, at + 40, files[i].second.size(), 8); + bytes.insert(bytes.end(), files[i].second.begin(), files[i].second.end()); + } + std::filesystem::create_directories(path.parent_path()); + std::ofstream(path, std::ios::binary).write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); +} + +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5-np-trophy-" + std::to_string(std::random_device{}())); + std::filesystem::remove_all(root); + WritePackage(root / "app0/sce_sys/trophy/trophy00.trp", {{"TROPCONF.ESFM", "conf"}, {"ICON0.PNG", "game-icon"}, {"TROP000.PNG", "first"}, {"TROP001.PNG", "second"}}); + std::filesystem::current_path(root); + + int context = 0, handle = 0, missing = 0, platinum = 0; + Require(sceNpTrophyCreateContext(nullptr, 1, 0, 0) == InvalidArgument, "null context"); + Require(sceNpTrophyCreateContext(&context, 1, 0, 1) == InvalidArgument, "context options"); + Require(sceNpTrophyCreateContext(&context, 1, 0, 0) == 0, "create the context"); + Require(sceNpTrophyCreateContext(&missing, 1, 0, 0) == ContextAlreadyExists, "second context for the same user and label"); + Require(sceNpTrophyCreateHandle(&handle) == 0, "create the handle"); + + std::uint32_t flags[4] = {}, count = 0; + Require(sceNpTrophyGetTrophyUnlockState(context, handle, flags, &count) == NotRegistered, "state before registration"); + Require(sceNpTrophyRegisterContext(context, handle, 0) == 0, "register the context"); + Require(sceNpTrophyRegisterContext(context, handle, 0) == AlreadyRegistered, "register twice"); + Require(sceNpTrophyGetTrophyUnlockState(context, handle, flags, &count) == 0 && count == 2 && flags[0] == 0, "two trophies, none unlocked"); + + std::uint64_t size = 0; + Require(sceNpTrophyGetGameIcon(context, handle, nullptr, &size) == 0 && size == 9, "game icon size"); + char icon[16] = {}; + size = 4; + Require(sceNpTrophyGetGameIcon(context, handle, icon, &size) == InsufficientBuffer, "game icon into a short buffer"); + size = sizeof(icon); + Require(sceNpTrophyGetGameIcon(context, handle, icon, &size) == 0 && size == 9 && std::memcmp(icon, "game-icon", 9) == 0, "game icon bytes"); + + size = sizeof(icon); + Require(sceNpTrophyGetTrophyIcon(context, handle, 1, icon, &size) == TrophyNotUnlocked, "icon of a locked trophy"); + Require(sceNpTrophyUnlockTrophy(context, handle, 2, &platinum) == InvalidTrophyId, "unlock a trophy past the count"); + Require(sceNpTrophyUnlockTrophy(context, handle, 1, &platinum) == 0 && platinum == -1, "unlock trophy 1"); + Require(sceNpTrophyUnlockTrophy(context, handle, 1, &platinum) == AlreadyUnlocked, "unlock trophy 1 twice"); + Require(sceNpTrophyGetTrophyUnlockState(context, handle, flags, &count) == 0 && flags[0] == 2u, "trophy 1 unlocked"); + size = sizeof(icon); + Require(sceNpTrophyGetTrophyIcon(context, handle, 1, icon, &size) == 0 && size == 6 && std::memcmp(icon, "second", 6) == 0, "icon of the unlocked trophy"); + int saved = -1; + { + std::ifstream state(root / "_trophy/trophy00.1"); + state >> saved; + } + Require(saved == 1, "unlock state saved for the user"); + + Require(sceNpTrophyCreateContext(&missing, 1, 1, 0) == 0, "context for a label without a package"); + Require(sceNpTrophyRegisterContext(missing, handle, 0) == TitleConfNotInstalled, "register without a trophy package"); + Require(sceNpTrophyDestroyContext(missing) == 0, "destroy the second context"); + Require(sceNpTrophyDestroyHandle(handle) == 0 && sceNpTrophyDestroyContext(context) == 0, "destroy the handle and context"); + Require(sceNpTrophyDestroyContext(context) == InvalidContext, "destroy the context twice"); + + std::filesystem::current_path(root.parent_path()); + std::error_code error; + std::filesystem::remove_all(root, error); + return 0; +} diff --git a/core/libs/tests/GuestNpTrophyData.cpp b/core/libs/tests/GuestNpTrophyData.cpp new file mode 100644 index 0000000000..8bf4e7ad00 --- /dev/null +++ b/core/libs/tests/GuestNpTrophyData.cpp @@ -0,0 +1,260 @@ +#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 +#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 constexpr int InvalidTrophyId = static_cast(0x8055390Au); + +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"); + const auto untouched = [](const void* bytes, std::size_t size) { + const auto* first = static_cast(bytes); + return std::all_of(first, first + size, [](unsigned char byte) { return byte == 0x5a; }); + }; + for (const int unknownId : {8, 99, -1}) { + NpTrophy2Details unknownDetails{}; + NpTrophy2Data unknown{}; + std::memset(&unknownDetails, 0x5a, sizeof(unknownDetails)); + std::memset(&unknown, 0x5a, sizeof(unknown)); + Require(sceNpTrophy2GetTrophyInfo(context, 1, unknownId, &unknownDetails, &unknown) == InvalidTrophyId, "a trophy that is not in the package did not return the invalid trophy id error"); + Require(untouched(&unknownDetails, sizeof(unknownDetails)) && untouched(&unknown, sizeof(unknown)), "a trophy that is not in the package wrote its details or data"); + } + + 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/GuestNpTus.cpp b/core/libs/tests/GuestNpTus.cpp new file mode 100644 index 0000000000..d252efcfaa --- /dev/null +++ b/core/libs/tests/GuestNpTus.cpp @@ -0,0 +1,305 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpTusCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId); +int APS5_VABI sceNpTusDeleteNpTitleCtx(int titleCtxId); +int APS5_VABI sceNpTssCreateNpTitleCtx(std::uint32_t serviceLabel, const NpId* npId); +int APS5_VABI sceNpTssCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId); +int APS5_VABI sceNpTusCreateRequest(int titleCtxId); +int APS5_VABI sceNpTusDeleteRequest(int reqId); +int APS5_VABI sceNpTusAbortRequest(int reqId); +int APS5_VABI sceNpTusPollAsync(int reqId, int* result); +int APS5_VABI sceNpTusGetMultiSlotVariableAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusSetMultiSlotVariableAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotVariableAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusAddAndGetVariableAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusWaitAsync(int reqId, int* result); +int APS5_VABI sceNpTusGetMultiSlotVariableA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariableAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSave(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusSetMultiSlotVariableA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option); +int APS5_VABI sceNpTusSetMultiSlotVariableAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option); +int APS5_VABI sceNpTusSetMultiSlotVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotVariableA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotVariableAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotVariable(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, int arrayLen, void* option); +} + +static constexpr int SIGNED_OUT = static_cast(0x80550006); +static constexpr int INVALID_ARGUMENT = static_cast(0x80550704); +static constexpr int TOO_MANY_OBJECTS = static_cast(0x80550706); +static constexpr int ABORTED = static_cast(0x80550707); +static constexpr int INSUFFICIENT_ARGUMENT = static_cast(0x8055070C); +static constexpr int INVALID_ID = static_cast(0x8055070E); +static constexpr int TOO_MANY_SLOTID = static_cast(0x80550718); + +static void Require(bool value) { if (!value) std::abort(); } + +static void RequireFinished(int reqId, int expected) { + int result = 0; + Require(sceNpTusPollAsync(reqId, &result) == 0 && result == expected); +} + +static void CheckTssTitleContexts() { + const int shared = sceNpTssCreateNpTitleCtxA(0, 1); + Require(shared > 0); + const int request = sceNpTusCreateRequest(shared); + Require(request > 0); + Require(sceNpTusDeleteNpTitleCtx(shared) == 0); + Require(sceNpTusDeleteNpTitleCtx(shared) == INVALID_ID); + Require(sceNpTusDeleteRequest(request) == INVALID_ID); + + const NpId npId{}; + Require(sceNpTssCreateNpTitleCtx(0, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTssCreateNpTitleCtx(0xFFFFFFFFu, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTssCreateNpTitleCtx(0xFFFFFFFFu, &npId) == INVALID_ARGUMENT); + + std::array contexts{}; + for (std::size_t index = 0; index < contexts.size(); ++index) { + const std::size_t form = index % 3; + contexts[index] = form == 0 ? sceNpTusCreateNpTitleCtxA(0, 1) : form == 1 ? sceNpTssCreateNpTitleCtxA(0, 1) : sceNpTssCreateNpTitleCtx(0, &npId); + Require(contexts[index] > 0); + } + Require(sceNpTssCreateNpTitleCtxA(0, 1) == TOO_MANY_OBJECTS); + Require(sceNpTssCreateNpTitleCtx(0, &npId) == TOO_MANY_OBJECTS); + Require(sceNpTusCreateNpTitleCtxA(0, 1) == TOO_MANY_OBJECTS); + Require(sceNpTssCreateNpTitleCtx(0, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTssCreateNpTitleCtx(0xFFFFFFFFu, &npId) == INVALID_ARGUMENT); + for (const int context : contexts) Require(sceNpTusDeleteNpTitleCtx(context) == 0); +} + +static void CheckMultiSlotVariables() { + const int context = sceNpTusCreateNpTitleCtxA(0, 1); + Require(context > 0); + const int request = sceNpTusCreateRequest(context); + Require(request > 0); + + int result = 0; + Require(sceNpTusWaitAsync(request + 1000, &result) == INVALID_ID); + Require(sceNpTusWaitAsync(request, &result) == INVALID_ARGUMENT); + + std::int32_t slots[2] = {0, 3}; + std::int32_t negative[1] = {-1}; + std::int64_t values[2] = {5, 7}; + std::uint8_t variables[512]; + for (std::uint8_t& byte : variables) byte = 0x5a; + const NpId npId{}; + const NpOnlineId user{{'u', 's', 'e', 'r'}, {}}; + const NpOnlineId empty{}; + + Require(sceNpTusGetMultiSlotVariableAsync(request, nullptr, nullptr, variables, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableAsync(request, nullptr, slots, nullptr, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableAsync(request, nullptr, slots, variables, sizeof(variables), 0, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableAsync(request, nullptr, slots, variables, sizeof(variables), 65, nullptr) == TOO_MANY_SLOTID); + Require(sceNpTusGetMultiSlotVariableAsync(request, nullptr, negative, variables, sizeof(variables), 1, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableAsync(request, nullptr, slots, variables, sizeof(variables), 2, variables) == INVALID_ARGUMENT); + Require(sceNpTusWaitAsync(request, &result) == INVALID_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableAsync(request, nullptr, slots, variables, sizeof(variables), 2, nullptr) == 0); + result = 0; + Require(sceNpTusWaitAsync(request, &result) == 0 && result == SIGNED_OUT); + Require(sceNpTusWaitAsync(request, nullptr) == 0); + Require(sceNpTusGetMultiSlotVariableAsync(request, &npId, slots, variables, sizeof(variables), 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, nullptr, slots, variables, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, &user, slots, nullptr, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, &user, nullptr, variables, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, &user, slots, variables, sizeof(variables), 0, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, &user, slots, variables, sizeof(variables), 2, variables) == INVALID_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, &user, slots, variables, sizeof(variables), 65, nullptr) == TOO_MANY_SLOTID); + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, &user, negative, variables, sizeof(variables), 1, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, &empty, slots, variables, sizeof(variables), 2, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, &user, slots, variables, sizeof(variables), 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + + for (const auto call : {sceNpTusGetMultiSlotVariableAVUserAsync, sceNpTusGetMultiSlotVariableForCrossSaveVUserAsync}) { + Require(call(request, nullptr, slots, variables, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(call(request, &user, nullptr, variables, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(call(request, &user, slots, nullptr, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(call(request, &user, slots, variables, sizeof(variables), 0, nullptr) == INVALID_ARGUMENT); + Require(call(request, &user, slots, variables, sizeof(variables), 65, nullptr) == TOO_MANY_SLOTID); + Require(call(request, &user, negative, variables, sizeof(variables), 1, nullptr) == INVALID_ARGUMENT); + Require(call(request, &empty, slots, variables, sizeof(variables), 65, nullptr) == TOO_MANY_SLOTID); + Require(call(request, &empty, slots, variables, sizeof(variables), 2, nullptr) == INVALID_ARGUMENT); + Require(call(request, &user, slots, variables, sizeof(variables), 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + } + for (const auto call : {sceNpTusGetMultiSlotVariableAVUser, sceNpTusGetMultiSlotVariableForCrossSaveVUser}) { + Require(call(request, &empty, slots, variables, sizeof(variables), 2, nullptr) == INVALID_ARGUMENT); + Require(call(request, &user, slots, variables, sizeof(variables), 2, nullptr) == SIGNED_OUT); + Require(call(request + 1000, &user, slots, variables, sizeof(variables), 2, nullptr) == INVALID_ID); + } + + Require(sceNpTusGetMultiSlotVariableForCrossSaveAsync(request, 0, slots, nullptr, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableForCrossSaveAsync(request, 0, slots, variables, sizeof(variables), 0, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableForCrossSaveAsync(request, 0, slots, variables, sizeof(variables), 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + for (const auto call : {sceNpTusGetMultiSlotVariableA, sceNpTusGetMultiSlotVariableForCrossSave}) { + Require(call(request, 0, slots, variables, sizeof(variables), 65, nullptr) == TOO_MANY_SLOTID); + Require(call(request, 0, slots, variables, sizeof(variables), 2, nullptr) == SIGNED_OUT); + Require(call(request + 1000, 0, slots, variables, sizeof(variables), 2, nullptr) == INVALID_ID); + } + + Require(sceNpTusSetMultiSlotVariableAsync(request, nullptr, slots, nullptr, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableAsync(request, nullptr, nullptr, values, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableAsync(request, nullptr, slots, values, 0, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableAsync(request, nullptr, negative, values, 1, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableAsync(request, &npId, slots, values, 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, nullptr, slots, values, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, &user, slots, nullptr, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, &user, nullptr, values, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, &user, slots, values, 0, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, &user, slots, values, 2, values) == INVALID_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, &user, negative, values, 1, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, &user, slots, values, 65, nullptr) == TOO_MANY_SLOTID); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, &empty, slots, values, 2, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, &user, slots, values, 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + Require(sceNpTusSetMultiSlotVariableA(request, 0, slots, nullptr, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableA(request, 0, slots, values, 2, nullptr) == SIGNED_OUT); + + Require(sceNpTusDeleteMultiSlotVariableAsync(request, nullptr, slots, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableAsync(request, &npId, nullptr, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableAsync(request, &npId, slots, 0, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableAsync(request, &npId, slots, 2, values) == INVALID_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableAsync(request, &npId, slots, 65, nullptr) == TOO_MANY_SLOTID); + Require(sceNpTusDeleteMultiSlotVariableAsync(request, &npId, negative, 1, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableAsync(request, &npId, slots, 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + Require(sceNpTusDeleteMultiSlotVariableVUserAsync(request, nullptr, slots, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableVUserAsync(request, &user, slots, 0, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableVUserAsync(request, &user, nullptr, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableVUserAsync(request, &user, slots, 2, values) == INVALID_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableVUserAsync(request, &user, slots, 65, nullptr) == TOO_MANY_SLOTID); + Require(sceNpTusDeleteMultiSlotVariableVUserAsync(request, &user, negative, 1, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableVUserAsync(request, &empty, slots, 2, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableVUserAsync(request, &user, slots, 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + Require(sceNpTusDeleteMultiSlotVariable(request, &npId, slots, 2, nullptr) == SIGNED_OUT); + Require(sceNpTusDeleteMultiSlotVariableVUser(request, &user, slots, 2, nullptr) == SIGNED_OUT); + Require(sceNpTusDeleteMultiSlotVariableA(request, 0, slots, 0, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableA(request, 0, slots, 2, nullptr) == SIGNED_OUT); + + Require(sceNpTusAbortRequest(request) == 0); + Require(sceNpTusDeleteMultiSlotVariable(request, &npId, slots, 2, nullptr) == ABORTED); + Require(sceNpTusGetMultiSlotVariableA(request, 0, slots, variables, sizeof(variables), 2, nullptr) == ABORTED); + + const int fresh = sceNpTusCreateRequest(context); + Require(fresh > 0); + Require(sceNpTusAbortRequest(fresh) == 0); + Require(sceNpTusWaitAsync(fresh, &result) == INVALID_ARGUMENT); + Require(sceNpTusDeleteNpTitleCtx(context) == 0); + Require(sceNpTusWaitAsync(request, &result) == INVALID_ID); + Require(sceNpTusSetMultiSlotVariableA(fresh, 0, slots, values, 2, nullptr) == INVALID_ID); + for (const std::uint8_t byte : variables) Require(byte == 0x5a); +} + +int main() { + CheckMultiSlotVariables(); + + const int context = sceNpTusCreateNpTitleCtxA(0, 1); + Require(context > 0); + Require(sceNpTusCreateRequest(context + 1000) == INVALID_ID); + + const int request = sceNpTusCreateRequest(context); + Require(request > 0); + int result = 0; + Require(sceNpTusPollAsync(request, &result) == 1); + + std::int32_t slots[2] = {0, 3}; + std::int64_t values[2] = {5, 7}; + std::uint8_t variables[256] = {}; + Require(sceNpTusGetMultiSlotVariableAAsync(request, 0, nullptr, variables, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableAAsync(request, 0, slots, nullptr, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableAAsync(request, 0, slots, variables, sizeof(variables), 0, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableAAsync(request, 0, slots, variables, sizeof(variables), 65, nullptr) == TOO_MANY_SLOTID); + Require(sceNpTusGetMultiSlotVariableAAsync(request, 0, slots, variables, sizeof(variables), 2, variables) == INVALID_ARGUMENT); + std::int32_t negative[1] = {-1}; + Require(sceNpTusDeleteMultiSlotVariableAAsync(request, 0, negative, 1, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableAAsync(request, 0, slots, nullptr, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusAddAndGetVariableAAsync(request, 0, 0, 1, nullptr, nullptr, nullptr, 0, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusAddAndGetVariableAAsync(request, 0, -1, 1, nullptr, nullptr, variables, sizeof(variables), nullptr) == INVALID_ARGUMENT); + + Require(sceNpTusGetMultiSlotVariableAAsync(request, 0, slots, variables, sizeof(variables), 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + Require(sceNpTusSetMultiSlotVariableAAsync(request, 0, slots, values, 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + Require(sceNpTusDeleteMultiSlotVariableAAsync(request, 0, slots, 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + Require(sceNpTusAddAndGetVariableAAsync(request, 0, 0, 1, nullptr, nullptr, variables, sizeof(variables), nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + + Require(sceNpTusAbortRequest(request) == 0); + Require(sceNpTusSetMultiSlotVariableAAsync(request, 0, slots, values, 2, nullptr) == 0); + RequireFinished(request, ABORTED); + + Require(sceNpTusDeleteRequest(request) == 0); + Require(sceNpTusDeleteRequest(request) == INVALID_ID); + Require(sceNpTusAbortRequest(request) == INVALID_ID); + Require(sceNpTusPollAsync(request, &result) == INVALID_ID); + Require(sceNpTusSetMultiSlotVariableAAsync(request, 0, slots, values, 2, nullptr) == INVALID_ID); + + Require(sceNpTusDeleteNpTitleCtx(0) == INVALID_ID); + Require(sceNpTusDeleteNpTitleCtx(-1) == INVALID_ID); + Require(sceNpTusDeleteNpTitleCtx(context + 1000) == INVALID_ID); + const int siblingContext = sceNpTusCreateNpTitleCtxA(0, 1); + const int siblingRequest = sceNpTusCreateRequest(siblingContext); + Require(siblingContext > 0 && siblingRequest > 0); + Require(sceNpTusSetMultiSlotVariableAAsync(siblingRequest, 0, slots, values, 2, nullptr) == 0); + const int idle = sceNpTusCreateRequest(context); + const int finished = sceNpTusCreateRequest(context); + const int aborted = sceNpTusCreateRequest(context); + Require(idle > 0 && finished > 0 && aborted > 0); + Require(sceNpTusSetMultiSlotVariableAAsync(finished, 0, slots, values, 2, nullptr) == 0); + Require(sceNpTusAbortRequest(aborted) == 0); + Require(sceNpTusDeleteNpTitleCtx(context) == 0); + Require(sceNpTusDeleteNpTitleCtx(context) == INVALID_ID); + Require(sceNpTusCreateRequest(context) == INVALID_ID); + for (const int child : {idle, finished, aborted}) { + Require(sceNpTusPollAsync(child, &result) == INVALID_ID); + Require(sceNpTusDeleteRequest(child) == INVALID_ID); + Require(sceNpTusAbortRequest(child) == INVALID_ID); + Require(sceNpTusSetMultiSlotVariableAAsync(child, 0, slots, values, 2, nullptr) == INVALID_ID); + } + RequireFinished(siblingRequest, SIGNED_OUT); + Require(sceNpTusDeleteNpTitleCtx(siblingContext) == 0); + + std::array contexts{}; + for (int& titleContext : contexts) { + titleContext = sceNpTusCreateNpTitleCtxA(0, 1); + Require(titleContext > 0); + } + Require(sceNpTusCreateNpTitleCtxA(0, 1) == TOO_MANY_OBJECTS); + for (int& titleContext : contexts) { + const int previous = titleContext; + Require(sceNpTusDeleteNpTitleCtx(previous) == 0); + titleContext = sceNpTusCreateNpTitleCtxA(0, 1); + Require(titleContext > 0 && titleContext != previous); + Require(sceNpTusCreateRequest(previous) == INVALID_ID); + } + std::array children{}; + for (int& child : children) { + child = sceNpTusCreateRequest(contexts[0]); + Require(child > 0); + } + Require(sceNpTusCreateRequest(contexts[1]) == TOO_MANY_OBJECTS); + Require(sceNpTusDeleteNpTitleCtx(contexts[0]) == 0); + Require(sceNpTusCreateRequest(contexts[1]) > 0); + for (const int child : children) Require(sceNpTusPollAsync(child, &result) == INVALID_ID); + for (std::size_t index = 1; index < contexts.size(); ++index) Require(sceNpTusDeleteNpTitleCtx(contexts[index]) == 0); + CheckTssTitleContexts(); +} diff --git a/core/libs/tests/GuestNpTusOffline.cpp b/core/libs/tests/GuestNpTusOffline.cpp new file mode 100644 index 0000000000..af24d244c8 --- /dev/null +++ b/core/libs/tests/GuestNpTusOffline.cpp @@ -0,0 +1,268 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpTusCreateNpTitleCtxA(std::uint32_t, int); +int APS5_VABI sceNpTusCreateRequest(int); +int APS5_VABI sceNpTusDeleteRequest(int); +int APS5_VABI sceNpTusWaitAsync(int, int*); +int APS5_VABI sceNpTssGetSmallStorage(int reqId, void* data, std::uint64_t maxSize, std::uint64_t* contentLength, void* option); +int APS5_VABI sceNpTssGetSmallStorageAsync(int reqId, void* data, std::uint64_t maxSize, std::uint64_t* contentLength, void* option); +int APS5_VABI sceNpTusAddAndGetVariable(int reqId, const void* targetNpId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusAddAndGetVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int64_t inVariable, const void* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotData(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotDataA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotDataAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotDataAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusGetDataA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataAsync(int reqId, const void* targetNpId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetDataVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, void* dataStatus, std::uint64_t dataStatusSize, void* data, std::uint64_t recvSize, void* option); +int APS5_VABI sceNpTusGetFriendsDataStatus(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetFriendsDataStatusAsync(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetFriendsVariable(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetFriendsVariableAsync(int reqId, std::int32_t slotId, std::int32_t includeSelf, std::int32_t sortType, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatus(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSave(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatus(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusA(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusAAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusAsync(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSave(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserDataStatusVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* statusArray, std::uint64_t statusArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariable(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableA(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableAAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableAsync(int reqId, const void* targetNpIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveAsync(int reqId, const std::uint64_t* targetAccountIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableVUser(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiUserVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserIdArray, std::int32_t slotId, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusSetDataA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusSetDataAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusSetDataAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusSetDataAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusSetDataAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const void* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusSetDataVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::uint64_t totalSize, std::uint64_t sendSize, const void* data, const void* info, std::uint64_t infoStructSize, const void* isLastChangedAuthor, const void* isLastChangedDate, void* option); +int APS5_VABI sceNpTusTryAndSetVariable(int reqId, const void* targetNpId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableA(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableAsync(int reqId, const void* targetNpId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableForCrossSave(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +int APS5_VABI sceNpTusTryAndSetVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, std::int32_t slotId, std::int32_t opeType, std::int64_t variable, const std::int64_t* compareValue, const void* isLastChangedAuthor, const void* isLastChangedDate, void* resultVariable, std::uint64_t resultVariableSize, void* option); +} + +namespace { + +constexpr int SignedOut = static_cast(0x80550006u); +constexpr int InvalidArgument = static_cast(0x80550704u); +constexpr int InvalidId = static_cast(0x8055070Eu); +constexpr int InvalidType = static_cast(0x80550711u); +constexpr int TooManySlotId = static_cast(0x80550718u); +constexpr int TooManyNpId = static_cast(0x80550719u); + +int context = 0; +alignas(8) unsigned char buffer[1024]; +unsigned char npId[64]; +NpOnlineId virtualUser{}; +NpOnlineId virtualUsers[2]{}; +NpOnlineId emptyUser{}; +unsigned char npIds[2][64]; +std::uint64_t accounts[2] = {1, 2}; +std::int32_t slots[2] = {1, 2}; +std::int32_t manySlots[65] = {}; +std::uint64_t length = 0; +int failures = 0; + +void Require(bool value, const char* name, const char* what) { + if (value) return; + std::fprintf(stderr, "%s: %s\n", name, what); + ++failures; +} + +int NewRequest() { + const int req = sceNpTusCreateRequest(context); + if (req <= 0) { + std::fprintf(stderr, "sceNpTusCreateRequest failed\n"); + std::abort(); + } + return req; +} + +void CheckAsync(const char* name, const std::function& valid, const std::function& unknownRequest, const std::function& withOption) { + int req = NewRequest(); + Require(valid(req) == 0, name, "valid arguments did not start the request"); + int result = 0; + Require(sceNpTusWaitAsync(req, &result) == 0 && result == SignedOut, name, "the request did not finish signed out"); + sceNpTusDeleteRequest(req); + Require(unknownRequest() == InvalidId, name, "an unknown request was not refused with INVALID_ID"); + if (withOption) { + req = NewRequest(); + Require(withOption(req) == InvalidArgument, name, "a non-null option was not refused with INVALID_ARGUMENT"); + sceNpTusDeleteRequest(req); + } +} + +void CheckSync(const char* name, const std::function& call) { + const int req = NewRequest(); + Require(call(req) == SignedOut, name, "the synchronous call did not return SIGNED_OUT"); + sceNpTusDeleteRequest(req); +} + +} + +int main() { + std::memset(npId, 'n', sizeof(npId)); + std::memset(npIds, 'n', sizeof(npIds)); + std::strcpy(virtualUser.data, "virtual"); + std::strcpy(virtualUsers[0].data, "first"); + std::strcpy(virtualUsers[1].data, "second"); + context = sceNpTusCreateNpTitleCtxA(1, 1); + if (context <= 0) return 1; + CheckAsync("sceNpTssGetSmallStorageAsync", [&](int req) { return sceNpTssGetSmallStorageAsync(req, buffer, 16, &length, nullptr); }, [&]() { int req = 9999; return sceNpTssGetSmallStorageAsync(9999, buffer, 16, &length, nullptr); }, [&](int req) { return sceNpTssGetSmallStorageAsync(req, buffer, 16, &length, &buffer); }); + CheckAsync("sceNpTusAddAndGetVariableAVUserAsync", [&](int req) { return sceNpTusAddAndGetVariableAVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusAddAndGetVariableAVUserAsync(9999, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusAddAndGetVariableAVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusAddAndGetVariableAsync", [&](int req) { return sceNpTusAddAndGetVariableAsync(req, &npId, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusAddAndGetVariableAsync(9999, &npId, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusAddAndGetVariableAsync(req, &npId, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusAddAndGetVariableForCrossSaveAsync", [&](int req) { return sceNpTusAddAndGetVariableForCrossSaveAsync(req, 1, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusAddAndGetVariableForCrossSaveAsync(9999, 1, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusAddAndGetVariableForCrossSaveAsync(req, 1, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusAddAndGetVariableForCrossSaveVUserAsync", [&](int req) { return sceNpTusAddAndGetVariableForCrossSaveVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusAddAndGetVariableForCrossSaveVUserAsync(9999, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusAddAndGetVariableForCrossSaveVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusAddAndGetVariableVUserAsync", [&](int req) { return sceNpTusAddAndGetVariableVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusAddAndGetVariableVUserAsync(9999, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusAddAndGetVariableVUserAsync(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusDeleteMultiSlotDataAAsync", [&](int req) { return sceNpTusDeleteMultiSlotDataAAsync(req, 1, slots, 2, nullptr); }, [&]() { int req = 9999; return sceNpTusDeleteMultiSlotDataAAsync(9999, 1, slots, 2, nullptr); }, [&](int req) { return sceNpTusDeleteMultiSlotDataAAsync(req, 1, slots, 2, &buffer); }); + CheckAsync("sceNpTusDeleteMultiSlotDataAsync", [&](int req) { return sceNpTusDeleteMultiSlotDataAsync(req, &npId, slots, 2, nullptr); }, [&]() { int req = 9999; return sceNpTusDeleteMultiSlotDataAsync(9999, &npId, slots, 2, nullptr); }, [&](int req) { return sceNpTusDeleteMultiSlotDataAsync(req, &npId, slots, 2, &buffer); }); + CheckAsync("sceNpTusGetDataAAsync", [&](int req) { return sceNpTusGetDataAAsync(req, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&]() { int req = 9999; return sceNpTusGetDataAAsync(9999, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&](int req) { return sceNpTusGetDataAAsync(req, 1, 1, buffer, sizeof(buffer), buffer, 16, &buffer); }); + CheckAsync("sceNpTusGetDataAVUserAsync", [&](int req) { return sceNpTusGetDataAVUserAsync(req, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&]() { int req = 9999; return sceNpTusGetDataAVUserAsync(9999, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&](int req) { return sceNpTusGetDataAVUserAsync(req, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, &buffer); }); + CheckAsync("sceNpTusGetDataAsync", [&](int req) { return sceNpTusGetDataAsync(req, &npId, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&]() { int req = 9999; return sceNpTusGetDataAsync(9999, &npId, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&](int req) { return sceNpTusGetDataAsync(req, &npId, 1, buffer, sizeof(buffer), buffer, 16, &buffer); }); + CheckAsync("sceNpTusGetDataForCrossSaveAsync", [&](int req) { return sceNpTusGetDataForCrossSaveAsync(req, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&]() { int req = 9999; return sceNpTusGetDataForCrossSaveAsync(9999, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&](int req) { return sceNpTusGetDataForCrossSaveAsync(req, 1, 1, buffer, sizeof(buffer), buffer, 16, &buffer); }); + CheckAsync("sceNpTusGetDataVUserAsync", [&](int req) { return sceNpTusGetDataVUserAsync(req, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&]() { int req = 9999; return sceNpTusGetDataVUserAsync(9999, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }, [&](int req) { return sceNpTusGetDataVUserAsync(req, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, &buffer); }); + CheckAsync("sceNpTusGetFriendsDataStatusAsync", [&](int req) { return sceNpTusGetFriendsDataStatusAsync(req, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetFriendsDataStatusAsync(9999, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetFriendsDataStatusAsync(req, 1, 0, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetFriendsVariableAsync", [&](int req) { return sceNpTusGetFriendsVariableAsync(req, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetFriendsVariableAsync(9999, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetFriendsVariableAsync(req, 1, 0, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusAAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusAAsync(req, 1, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusAAsync(9999, 1, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusAAsync(req, 1, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusAVUserAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusAVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusAVUserAsync(9999, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusAVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusAsync(req, &npId, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusAsync(9999, &npId, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusAsync(req, &npId, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusForCrossSaveAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(req, 1, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(9999, 1, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(req, 1, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(9999, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiSlotDataStatusVUserAsync", [&](int req) { return sceNpTusGetMultiSlotDataStatusVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiSlotDataStatusVUserAsync(9999, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiSlotDataStatusVUserAsync(req, &virtualUser, slots, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusAAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusAAsync(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusAAsync(9999, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusAAsync(req, accounts, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusAVUserAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusAVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusAVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusAVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusAsync(req, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusAsync(9999, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusAsync(req, npIds, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusForCrossSaveAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSaveAsync(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusForCrossSaveAsync(9999, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSaveAsync(req, accounts, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserDataStatusVUserAsync", [&](int req) { return sceNpTusGetMultiUserDataStatusVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserDataStatusVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserDataStatusVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableAAsync", [&](int req) { return sceNpTusGetMultiUserVariableAAsync(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableAAsync(9999, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableAAsync(req, accounts, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableAVUserAsync", [&](int req) { return sceNpTusGetMultiUserVariableAVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableAVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableAVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableAsync", [&](int req) { return sceNpTusGetMultiUserVariableAsync(req, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableAsync(9999, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableAsync(req, npIds, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableForCrossSaveAsync", [&](int req) { return sceNpTusGetMultiUserVariableForCrossSaveAsync(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableForCrossSaveAsync(9999, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableForCrossSaveAsync(req, accounts, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableForCrossSaveVUserAsync", [&](int req) { return sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusGetMultiUserVariableVUserAsync", [&](int req) { return sceNpTusGetMultiUserVariableVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&]() { int req = 9999; return sceNpTusGetMultiUserVariableVUserAsync(9999, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }, [&](int req) { return sceNpTusGetMultiUserVariableVUserAsync(req, virtualUsers, 1, buffer, sizeof(buffer), 2, &buffer); }); + CheckAsync("sceNpTusSetDataAAsync", [&](int req) { return sceNpTusSetDataAAsync(req, 1, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&]() { int req = 9999; return sceNpTusSetDataAAsync(9999, 1, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&](int req) { return sceNpTusSetDataAAsync(req, 1, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, &buffer); }); + CheckAsync("sceNpTusSetDataAVUserAsync", [&](int req) { return sceNpTusSetDataAVUserAsync(req, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&]() { int req = 9999; return sceNpTusSetDataAVUserAsync(9999, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&](int req) { return sceNpTusSetDataAVUserAsync(req, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, &buffer); }); + CheckAsync("sceNpTusSetDataAsync", [&](int req) { return sceNpTusSetDataAsync(req, &npId, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&]() { int req = 9999; return sceNpTusSetDataAsync(9999, &npId, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&](int req) { return sceNpTusSetDataAsync(req, &npId, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, &buffer); }); + CheckAsync("sceNpTusSetDataVUserAsync", [&](int req) { return sceNpTusSetDataVUserAsync(req, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&]() { int req = 9999; return sceNpTusSetDataVUserAsync(9999, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }, [&](int req) { return sceNpTusSetDataVUserAsync(req, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableAAsync", [&](int req) { return sceNpTusTryAndSetVariableAAsync(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableAAsync(9999, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableAAsync(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableAVUserAsync", [&](int req) { return sceNpTusTryAndSetVariableAVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableAVUserAsync(9999, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableAVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableAsync", [&](int req) { return sceNpTusTryAndSetVariableAsync(req, &npId, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableAsync(9999, &npId, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableAsync(req, &npId, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableForCrossSaveAsync", [&](int req) { return sceNpTusTryAndSetVariableForCrossSaveAsync(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableForCrossSaveAsync(9999, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableForCrossSaveAsync(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableForCrossSaveVUserAsync", [&](int req) { return sceNpTusTryAndSetVariableForCrossSaveVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableForCrossSaveVUserAsync(9999, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableForCrossSaveVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckAsync("sceNpTusTryAndSetVariableVUserAsync", [&](int req) { return sceNpTusTryAndSetVariableVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&]() { int req = 9999; return sceNpTusTryAndSetVariableVUserAsync(9999, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }, [&](int req) { return sceNpTusTryAndSetVariableVUserAsync(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), &buffer); }); + CheckSync("sceNpTssGetSmallStorage", [&](int req) { return sceNpTssGetSmallStorage(req, buffer, 16, &length, nullptr); }); + CheckSync("sceNpTusAddAndGetVariable", [&](int req) { return sceNpTusAddAndGetVariable(req, &npId, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusAddAndGetVariableA", [&](int req) { return sceNpTusAddAndGetVariableA(req, 1, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusAddAndGetVariableAVUser", [&](int req) { return sceNpTusAddAndGetVariableAVUser(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusAddAndGetVariableForCrossSave", [&](int req) { return sceNpTusAddAndGetVariableForCrossSave(req, 1, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusAddAndGetVariableForCrossSaveVUser", [&](int req) { return sceNpTusAddAndGetVariableForCrossSaveVUser(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusAddAndGetVariableVUser", [&](int req) { return sceNpTusAddAndGetVariableVUser(req, &virtualUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusDeleteMultiSlotData", [&](int req) { return sceNpTusDeleteMultiSlotData(req, &npId, slots, 2, nullptr); }); + CheckSync("sceNpTusDeleteMultiSlotDataA", [&](int req) { return sceNpTusDeleteMultiSlotDataA(req, 1, slots, 2, nullptr); }); + CheckSync("sceNpTusGetDataA", [&](int req) { return sceNpTusGetDataA(req, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }); + CheckSync("sceNpTusGetDataAVUser", [&](int req) { return sceNpTusGetDataAVUser(req, &virtualUser, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }); + CheckSync("sceNpTusGetDataForCrossSave", [&](int req) { return sceNpTusGetDataForCrossSave(req, 1, 1, buffer, sizeof(buffer), buffer, 16, nullptr); }); + CheckSync("sceNpTusGetFriendsDataStatus", [&](int req) { return sceNpTusGetFriendsDataStatus(req, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetFriendsVariable", [&](int req) { return sceNpTusGetFriendsVariable(req, 1, 0, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatus", [&](int req) { return sceNpTusGetMultiSlotDataStatus(req, &npId, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatusA", [&](int req) { return sceNpTusGetMultiSlotDataStatusA(req, 1, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatusAVUser", [&](int req) { return sceNpTusGetMultiSlotDataStatusAVUser(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatusForCrossSave", [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSave(req, 1, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatusForCrossSaveVUser", [&](int req) { return sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiSlotDataStatusVUser", [&](int req) { return sceNpTusGetMultiSlotDataStatusVUser(req, &virtualUser, slots, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatus", [&](int req) { return sceNpTusGetMultiUserDataStatus(req, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatusA", [&](int req) { return sceNpTusGetMultiUserDataStatusA(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatusAVUser", [&](int req) { return sceNpTusGetMultiUserDataStatusAVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatusForCrossSave", [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSave(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatusForCrossSaveVUser", [&](int req) { return sceNpTusGetMultiUserDataStatusForCrossSaveVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserDataStatusVUser", [&](int req) { return sceNpTusGetMultiUserDataStatusVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserVariable", [&](int req) { return sceNpTusGetMultiUserVariable(req, npIds, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserVariableA", [&](int req) { return sceNpTusGetMultiUserVariableA(req, accounts, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserVariableAVUser", [&](int req) { return sceNpTusGetMultiUserVariableAVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserVariableForCrossSaveVUser", [&](int req) { return sceNpTusGetMultiUserVariableForCrossSaveVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusGetMultiUserVariableVUser", [&](int req) { return sceNpTusGetMultiUserVariableVUser(req, virtualUsers, 1, buffer, sizeof(buffer), 2, nullptr); }); + CheckSync("sceNpTusSetDataA", [&](int req) { return sceNpTusSetDataA(req, 1, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }); + CheckSync("sceNpTusSetDataAVUser", [&](int req) { return sceNpTusSetDataAVUser(req, &virtualUser, 1, 16, 8, buffer, buffer, sizeof(buffer), nullptr, nullptr, nullptr); }); + CheckSync("sceNpTusTryAndSetVariable", [&](int req) { return sceNpTusTryAndSetVariable(req, &npId, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusTryAndSetVariableA", [&](int req) { return sceNpTusTryAndSetVariableA(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusTryAndSetVariableAVUser", [&](int req) { return sceNpTusTryAndSetVariableAVUser(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusTryAndSetVariableForCrossSave", [&](int req) { return sceNpTusTryAndSetVariableForCrossSave(req, 1, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusTryAndSetVariableForCrossSaveVUser", [&](int req) { return sceNpTusTryAndSetVariableForCrossSaveVUser(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + CheckSync("sceNpTusTryAndSetVariableVUser", [&](int req) { return sceNpTusTryAndSetVariableVUser(req, &virtualUser, 1, 1, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr); }); + { + int req = NewRequest(); + Require(sceNpTusAddAndGetVariableVUserAsync(req, &emptyUser, 1, 5, nullptr, nullptr, buffer, sizeof(buffer), nullptr) == InvalidArgument, "sceNpTusAddAndGetVariableVUserAsync", "an empty virtual user was taken"); + NpOnlineId partlyEmpty[2]{}; + std::strcpy(partlyEmpty[0].data, "first"); + Require(sceNpTusGetMultiUserVariableVUserAsync(req, partlyEmpty, 1, buffer, sizeof(buffer), 2, nullptr) == InvalidArgument, "sceNpTusGetMultiUserVariableVUserAsync", "an empty virtual user in the array was taken"); + Require(sceNpTusTryAndSetVariableAAsync(req, 1, 1, 0, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr) == InvalidType, "sceNpTusTryAndSetVariableAAsync", "operation type 0 was taken"); + Require(sceNpTusTryAndSetVariableAAsync(req, 1, 1, 7, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr) == InvalidType, "sceNpTusTryAndSetVariableAAsync", "operation type 7 was taken"); + Require(sceNpTusTryAndSetVariableAsync(req, npId, 1, 7, 5, nullptr, nullptr, nullptr, buffer, sizeof(buffer), nullptr) == InvalidArgument, "sceNpTusTryAndSetVariableAsync", "operation type 7 was not refused with INVALID_ARGUMENT"); + Require(sceNpTssGetSmallStorageAsync(req, buffer, 64 * 1024 + 1, &length, nullptr) == InvalidArgument, "sceNpTssGetSmallStorageAsync", "a size above 64 KiB was taken"); + Require(sceNpTusDeleteMultiSlotDataAAsync(req, 1, manySlots, 65, nullptr) == TooManySlotId, "sceNpTusDeleteMultiSlotDataAAsync", "65 slots were taken"); + Require(sceNpTusGetMultiSlotDataStatusAsync(req, npId, manySlots, buffer, sizeof(buffer), 65, nullptr) == TooManyNpId, "sceNpTusGetMultiSlotDataStatusAsync", "65 slots did not return TOO_MANY_NPID"); + const std::int32_t negative[2] = {1, -1}; + Require(sceNpTusDeleteMultiSlotDataAAsync(req, 1, negative, 2, nullptr) == InvalidArgument, "sceNpTusDeleteMultiSlotDataAAsync", "a negative slot id was taken"); + Require(sceNpTusGetMultiUserVariableAAsync(req, accounts, 1, buffer, sizeof(buffer), 102, nullptr) == TooManyNpId, "sceNpTusGetMultiUserVariableAAsync", "102 users were taken"); + Require(sceNpTusGetFriendsVariableAsync(req, 1, 0, 5, buffer, sizeof(buffer), 2, nullptr) == InvalidArgument, "sceNpTusGetFriendsVariableAsync", "sort type 5 was taken"); + Require(sceNpTusGetFriendsVariableAsync(req, 1, 0, 1, buffer, sizeof(buffer), 101, nullptr) == TooManyNpId, "sceNpTusGetFriendsVariableAsync", "101 friends were taken"); + sceNpTusDeleteRequest(req); + } + if (failures != 0) return 1; + std::puts("NpTus offline request tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestNpWebApi.cpp b/core/libs/tests/GuestNpWebApi.cpp new file mode 100644 index 0000000000..9281e58632 --- /dev/null +++ b/core/libs/tests/GuestNpWebApi.cpp @@ -0,0 +1,67 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpWebApiInitialize(int libHttpCtxId, std::size_t poolSize); +int APS5_VABI sceNpWebApiTerminate(int libCtxId); +int APS5_VABI sceNpWebApiCreateHandle(int libCtxId); +int APS5_VABI sceNpWebApiDeleteHandle(int libCtxId, int handleId); +int APS5_VABI sceNpWebApiCreatePushEventFilter(int libCtxId, const void* filterParam, std::size_t filterParamNum); +int APS5_VABI sceNpWebApiDeletePushEventFilter(int libCtxId, int filterId); +int APS5_VABI sceNpWebApiCreateServicePushEventFilter(int libCtxId, int handleId, const char* npServiceName, unsigned npServiceLabel, const void* filterParam, std::size_t filterParamNum); +int APS5_VABI sceNpWebApiDeleteServicePushEventFilter(int libCtxId, int filterId); +} + +namespace { + +constexpr int InvalidArgument = static_cast(0x80552902u); +constexpr int LibContextNotFound = static_cast(0x80552904u); +constexpr int PushEventFilterNotFound = static_cast(0x8055290Bu); +constexpr int HandleNotFound = static_cast(0x8055290Du); +constexpr int ServicePushEventFilterNotFound = static_cast(0x8055290Eu); +constexpr int SignedInUserNotFound = static_cast(0x80552910u); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "NpWebApi: %s\n", message); + std::abort(); + } +} + +} + +int main() { + const int context = sceNpWebApiInitialize(1, 0x10000); + Require(context > 0 && context <= 0x7fff, "library context id out of range"); + const int other = sceNpWebApiInitialize(1, 0x10000); + Require(other > 0 && other != context, "a second library context must get its own id"); + Require(sceNpWebApiCreateHandle(0x7fff + 1) == LibContextNotFound, "handle of an unknown context"); + + const int handle = sceNpWebApiCreateHandle(context); + Require(handle > 0 && static_cast(handle) < 0xf0000000u, "handle id out of range"); + const char filterParam[16] = {}; + Require(sceNpWebApiCreatePushEventFilter(context, nullptr, 1) == InvalidArgument, "push event filter without its parameter"); + Require(sceNpWebApiCreatePushEventFilter(context, filterParam, 0) == InvalidArgument, "push event filter with no parameters"); + const int filter = sceNpWebApiCreatePushEventFilter(context, filterParam, 1); + Require(filter > 0 && filter != handle, "push event filter id"); + + Require(sceNpWebApiCreateServicePushEventFilter(context, handle, nullptr, 0, filterParam, 1) == InvalidArgument, "service filter without a service name"); + Require(sceNpWebApiCreateServicePushEventFilter(0x7fff + 1, handle, "service", 0, filterParam, 1) == LibContextNotFound, "service filter of an unknown context"); + Require(sceNpWebApiCreateServicePushEventFilter(other, handle, "service", 0, filterParam, 1) == HandleNotFound, "service filter with another context's handle"); + Require(sceNpWebApiCreateServicePushEventFilter(context, handle, "service", 0, filterParam, 1) == SignedInUserNotFound, "service filter without a signed-in user"); + Require(sceNpWebApiDeleteServicePushEventFilter(context, 1) == ServicePushEventFilterNotFound, "no service filter exists"); + + Require(sceNpWebApiDeletePushEventFilter(other, filter) == PushEventFilterNotFound, "filter deleted through another context"); + Require(sceNpWebApiDeletePushEventFilter(context, filter) == 0, "delete the push event filter"); + Require(sceNpWebApiDeletePushEventFilter(context, filter) == PushEventFilterNotFound, "push event filter deleted twice"); + Require(sceNpWebApiDeleteHandle(context, handle) == 0, "delete the handle"); + Require(sceNpWebApiDeleteHandle(context, handle) == HandleNotFound, "handle deleted twice"); + + Require(sceNpWebApiTerminate(context) == 0, "terminate the library context"); + Require(sceNpWebApiTerminate(context) == LibContextNotFound, "library context terminated twice"); + Require(sceNpWebApiCreateHandle(context) == LibContextNotFound, "handle of a terminated context"); + Require(sceNpWebApiTerminate(other) == 0, "terminate the second library context"); + return 0; +} diff --git a/core/libs/tests/GuestNpWebApi2.cpp b/core/libs/tests/GuestNpWebApi2.cpp index b01b7e1902..1adccd989d 100644 --- a/core/libs/tests/GuestNpWebApi2.cpp +++ b/core/libs/tests/GuestNpWebApi2.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include struct NpWebApi2MemoryPoolStats { @@ -13,48 +14,85 @@ struct NpWebApi2MemoryPoolStats { extern "C" { int APS5_VABI sceNpWebApi2Initialize(int, std::size_t); +int APS5_VABI sceNpWebApi2InitializeForPresence(int, std::uint64_t); +int APS5_VABI sceNpWebApi2IntInitialize(const NpWebApi2IntInitializeArgs*); +int APS5_VABI sceNpWebApi2IntInitialize2(const NpWebApi2IntInitialize2Args*); int APS5_VABI sceNpWebApi2Terminate(int); int APS5_VABI sceNpWebApi2GetMemoryPoolStats(int, NpWebApi2MemoryPoolStats*); int APS5_VABI sceNpWebApi2PushEventCreateHandle(int); +int APS5_VABI sceNpWebApi2PushEventAbortHandle(int, int); +int APS5_VABI sceNpWebApi2PushEventDeleteHandle(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*); int APS5_VABI sceNpWebApi2SetRequestTimeout(std::int64_t, std::uint32_t); +int APS5_VABI sceNpWebApi2AddWebTraceTag(std::int64_t, const char*); int APS5_VABI sceNpWebApi2DeleteRequest(std::int64_t); } static void Require(bool value) { if (!value) std::abort(); } -template -static bool Throws(TCall call) { - try { - call(); - } catch (const std::exception&) { - return true; - } - return false; -} - int main() { + constexpr int invalidContext = static_cast(0x80553404); + constexpr int invalidHandle = static_cast(0x8055340D); const int context = sceNpWebApi2Initialize(1, 0x20000); const int other = sceNpWebApi2Initialize(1, 0x8000); Require(context > 0 && other > 0 && context != other); - NpWebApi2MemoryPoolStats stats{1, 1, 1, 1}; + NpWebApi2MemoryPoolStats stats; + std::memset(&stats, 0xa5, sizeof(stats)); Require(sceNpWebApi2GetMemoryPoolStats(context, &stats) == 0); + const auto* statsBytes = reinterpret_cast(&stats); + for (std::size_t index = sizeof(std::size_t); index < sizeof(stats); ++index) Require(statsBytes[index] == 0); Require(stats.pool_size == 0x20000 && stats.max_inuse_size == 0 && stats.current_inuse_size == 0 && stats.reserved == 0); Require(sceNpWebApi2GetMemoryPoolStats(other, &stats) == 0 && stats.pool_size == 0x8000); - Require(Throws([&] { sceNpWebApi2GetMemoryPoolStats(context, nullptr); })); + Require(sceNpWebApi2GetMemoryPoolStats(context, nullptr) == 0); + Require(sceNpWebApi2GetMemoryPoolStats(-1, nullptr) == invalidContext); + Require(sceNpWebApi2PushEventCreateHandle(-1) == invalidContext); const int handle = sceNpWebApi2PushEventCreateHandle(context); Require(handle > 0); + Require(sceNpWebApi2PushEventAbortHandle(context, handle) == 0); + Require(sceNpWebApi2PushEventAbortHandle(context, handle + 1000) == invalidHandle); Require(sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000) == 0); + Require(sceNpWebApi2PushEventAbortHandle(other, handle) == invalidHandle); + Require(sceNpWebApi2PushEventDeleteHandle(other, handle) == invalidHandle); + Require(sceNpWebApi2PushEventSetHandleTimeout(other, handle, 0) == invalidHandle); + Require(sceNpWebApi2PushEventSetHandleTimeout(context, handle, 0) == 0); + Require(sceNpWebApi2PushEventSetHandleTimeout(context, handle, UINT32_MAX) == 0); + Require(sceNpWebApi2PushEventDeleteHandle(context, handle) == 0); + Require(sceNpWebApi2PushEventDeleteHandle(context, handle) == invalidHandle); + Require(sceNpWebApi2PushEventAbortHandle(context, handle) == invalidHandle); + Require(sceNpWebApi2PushEventSetHandleTimeout(context, handle, 1) == invalidHandle); + const int survivingHandle = sceNpWebApi2PushEventCreateHandle(other); const int user = sceNpWebApi2CreateUserContext(context, 1); std::int64_t request = 0; Require(user > 0 && sceNpWebApi2CreateRequest(user, "sessionManager", "/v1/sessions", "GET", nullptr, &request) == 0); Require(sceNpWebApi2SetRequestTimeout(request, 30000000) == 0); + Require(sceNpWebApi2AddWebTraceTag(request, "trace-tag-1") == 0); + Require(sceNpWebApi2AddWebTraceTag(request, nullptr) == static_cast(0x80553402)); Require(sceNpWebApi2DeleteRequest(request) == 0); Require(sceNpWebApi2Terminate(context) == 0); - Require(Throws([&] { sceNpWebApi2GetMemoryPoolStats(context, &stats); })); - Require(Throws([&] { sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000); })); + Require(sceNpWebApi2GetMemoryPoolStats(context, &stats) == invalidContext); + Require(sceNpWebApi2PushEventAbortHandle(context, handle) == invalidContext); + Require(sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000) == invalidContext); + Require(sceNpWebApi2PushEventAbortHandle(other, survivingHandle) == 0); Require(sceNpWebApi2Terminate(other) == 0); + + constexpr int invalidArgument = static_cast(0x80553402); + NpWebApi2IntInitializeArgs args{1, 0, 0x4000, "anyps5", sizeof(NpWebApi2IntInitializeArgs)}; + const int internal = sceNpWebApi2IntInitialize(&args); + Require(internal > 0 && sceNpWebApi2GetMemoryPoolStats(internal, &stats) == 0 && stats.pool_size == 0x4000); + args.struct_size = sizeof(NpWebApi2IntInitializeArgs) - 8; + Require(sceNpWebApi2IntInitialize(&args) == invalidArgument && sceNpWebApi2IntInitialize(nullptr) == invalidArgument); + NpWebApi2IntInitialize2Args args2{1, 0, 0x6000, nullptr, 1, 0, sizeof(NpWebApi2IntInitialize2Args)}; + const int internal2 = sceNpWebApi2IntInitialize2(&args2); + Require(internal2 > 0 && internal2 != internal && sceNpWebApi2GetMemoryPoolStats(internal2, &stats) == 0 && stats.pool_size == 0x6000); + args2.struct_size = sizeof(NpWebApi2IntInitializeArgs); + Require(sceNpWebApi2IntInitialize2(&args2) == invalidArgument && sceNpWebApi2IntInitialize2(nullptr) == invalidArgument); + const int presence = sceNpWebApi2InitializeForPresence(1, 0x2000); + Require(presence > 0 && sceNpWebApi2GetMemoryPoolStats(presence, &stats) == 0 && stats.pool_size == 0x2000); + for (const int created : {internal, internal2, presence}) { + Require(sceNpWebApi2Terminate(created) == 0); + Require(sceNpWebApi2GetMemoryPoolStats(created, &stats) == invalidContext); + } return 0; } diff --git a/core/libs/tests/GuestOfflineImports.cpp b/core/libs/tests/GuestOfflineImports.cpp index 32dcdd9acf..54acdb9e0c 100644 --- a/core/libs/tests/GuestOfflineImports.cpp +++ b/core/libs/tests/GuestOfflineImports.cpp @@ -9,22 +9,26 @@ #include extern "C" { +int APS5_VABI sceVideoRecordingGetInfo(std::int32_t info, void* data, std::size_t size); +int APS5_VABI sceVideoRecordingSetInfo(std::int32_t info, const void* data, std::size_t size); +int APS5_VABI sceNpEntitlementAccessAbortRequest(std::int64_t); +int APS5_VABI sceNpEntitlementAccessDeleteRequest(std::int64_t); int APS5_VABI sceHttpSetCookieEnabled(int, int); int APS5_VABI sceHttpSendRequest(int, const void*, std::size_t); int APS5_VABI sceNpEntitlementAccessGetEntitlementKey( std::uint32_t, const NpUnifiedEntitlementLabel*, NpEntitlementAccessEntitlementKey*); int APS5_VABI sceNpEntitlementAccessGetGameTrialsFlag(std::uint32_t*); int APS5_VABI sceNpEntitlementAccessGetPftFlag(std::uint32_t*); -int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfoList(); -int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfoList(); -int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfoList(); -int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfoList(); -int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(); -int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfo(); -int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfo(); -int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfo(); +int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfoList(std::int32_t userId, std::uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabels, std::uint32_t labelCount, const NpEntitlementAccessListParameter* parameter, std::int64_t* requestId); +int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfoList(std::int64_t requestId, std::int32_t* result, NpEntitlementAccessUnifiedInfo* infos, std::uint32_t capacity, std::int32_t* firstResult, std::int32_t* secondResult, std::int32_t* thirdResult); +int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfoList(std::int32_t userId, std::uint32_t serviceLabel, const NpServiceEntitlementLabel* entitlementLabels, std::uint32_t labelCount, const NpEntitlementAccessListParameter* parameter, std::int64_t* requestId); +int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfoList(std::int64_t requestId, std::int32_t* result, NpEntitlementAccessServiceInfo* infos, std::uint32_t capacity, std::int32_t* firstResult, std::int32_t* secondResult, std::int32_t* thirdResult); +int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(std::int32_t userId, std::uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, std::int64_t* requestId); +int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfo(std::int64_t requestId, std::int32_t* result, NpEntitlementAccessUnifiedInfo* info); +int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfo(std::int32_t userId, std::uint32_t serviceLabel, const NpServiceEntitlementLabel* entitlementLabel, std::int64_t* requestId); +int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfo(std::int64_t requestId, std::int32_t* result, NpEntitlementAccessServiceInfo* info); int APS5_VABI sceRudpInit_nid_postfix(void*, int); -int APS5_VABI sceRudpActivate(); +int APS5_VABI sceRudpActivate(std::int32_t, const void*, std::uint32_t); int APS5_VABI sceRudpGetStatus(void*, std::size_t); int APS5_VABI sceRudpEnd(); } @@ -32,6 +36,46 @@ int APS5_VABI sceRudpEnd(); static void Require(bool value) { if (!value) std::abort(); } int main() { + constexpr int recordingInvalid = static_cast(0x80a80003); + constexpr int recordingNotOpen = static_cast(0x80a80006); + constexpr int recordingUnavailable = static_cast(0x80a80002); + std::uint32_t recordingOutput = 0x12345678; + for (std::int32_t selector = 0; selector <= 0x10000; ++selector) { + const bool query = selector == 0xa001 || selector == 0xa002 || selector == 0xa003 || selector == 0xa005 || selector == 0xa00a || selector == 0xa00b || selector == 0xa00c; + const bool control = selector == 0xa004 || selector == 0xa006; + Require(sceVideoRecordingGetInfo(selector, &recordingOutput, sizeof(recordingOutput)) == (query ? recordingNotOpen : recordingInvalid)); + Require(recordingOutput == 0x12345678); + Require(sceVideoRecordingGetInfo(selector, nullptr, 0) == (query ? recordingNotOpen : recordingInvalid)); + Require(sceVideoRecordingSetInfo(selector, nullptr, 0) == (control ? recordingNotOpen : recordingInvalid)); + } + for (std::int32_t selector : {-1, INT32_MIN, INT32_MAX}) { + Require(sceVideoRecordingGetInfo(selector, nullptr, 0) == recordingInvalid); + Require(sceVideoRecordingSetInfo(selector, nullptr, 0) == recordingInvalid); + } + const float recordingVolume = 0.5f; + for (std::int32_t selector : {0x8002, 0x8003}) { + for (std::size_t size : {0u, 1u, 3u, 4u, 8u}) { + Require(sceVideoRecordingSetInfo(selector, nullptr, size) == recordingInvalid); + Require(sceVideoRecordingSetInfo(selector, &recordingVolume, size) == (size < sizeof(float) ? recordingInvalid : recordingUnavailable)); + } + } + + for (std::int64_t requestId : {std::int64_t{0}, std::int64_t{1}, INT64_MAX}) { + bool abortFailed = false; + bool deleteFailed = false; + try { + sceNpEntitlementAccessAbortRequest(requestId); + } catch (const std::logic_error&) { + abortFailed = true; + } + try { + sceNpEntitlementAccessDeleteRequest(requestId); + } catch (const std::logic_error&) { + deleteFailed = true; + } + Require(abortFailed && deleteFailed); + } + constexpr int invalidValue = static_cast(0x804311FE); constexpr int network = static_cast(0x80431063); Require(sceHttpSetCookieEnabled(1, 0) == 0); @@ -79,16 +123,16 @@ int main() { Require(sceNpEntitlementAccessGetPftFlag(nullptr) == parameter); constexpr int signedOut = static_cast(0x80550006); - Require(sceNpEntitlementAccessRequestUnifiedEntitlementInfoList() == signedOut); - Require(sceNpEntitlementAccessPollUnifiedEntitlementInfoList() == signedOut); - Require(sceNpEntitlementAccessRequestServiceEntitlementInfoList() == signedOut); - Require(sceNpEntitlementAccessPollServiceEntitlementInfoList() == signedOut); - Require(sceNpEntitlementAccessRequestUnifiedEntitlementInfo() == signedOut); - Require(sceNpEntitlementAccessPollUnifiedEntitlementInfo() == signedOut); - Require(sceNpEntitlementAccessRequestServiceEntitlementInfo() == signedOut); - Require(sceNpEntitlementAccessPollServiceEntitlementInfo() == signedOut); + Require(sceNpEntitlementAccessRequestUnifiedEntitlementInfoList(0, 0, nullptr, 0, nullptr, nullptr) == signedOut); + Require(sceNpEntitlementAccessPollUnifiedEntitlementInfoList(0, nullptr, nullptr, 0, nullptr, nullptr, nullptr) == signedOut); + Require(sceNpEntitlementAccessRequestServiceEntitlementInfoList(0, 0, nullptr, 0, nullptr, nullptr) == signedOut); + Require(sceNpEntitlementAccessPollServiceEntitlementInfoList(0, nullptr, nullptr, 0, nullptr, nullptr, nullptr) == signedOut); + Require(sceNpEntitlementAccessRequestUnifiedEntitlementInfo(0, 0, nullptr, nullptr) == signedOut); + Require(sceNpEntitlementAccessPollUnifiedEntitlementInfo(0, nullptr, nullptr) == signedOut); + Require(sceNpEntitlementAccessRequestServiceEntitlementInfo(0, 0, nullptr, nullptr) == signedOut); + Require(sceNpEntitlementAccessPollServiceEntitlementInfo(0, nullptr, nullptr) == signedOut); - Require(sceRudpActivate() == 0); + Require(sceRudpActivate(-1, nullptr, 0) == static_cast(0x80770001)); std::array status; status.fill(0x5a); const auto originalStatus = status; @@ -97,7 +141,7 @@ int main() { Require(status == originalStatus); alignas(8) static unsigned char rudpPool[0xC58]; Require(sceRudpInit_nid_postfix(rudpPool, sizeof(rudpPool)) == 0); - Require(sceRudpActivate() == 0); + Require(sceRudpActivate(-1, nullptr, 0) == static_cast(0x80770004)); Require(sceRudpGetStatus(status.data(), status.size()) == 0); for (unsigned char byte : status) Require(byte == 0); Require(sceRudpGetStatus(nullptr, 0) == static_cast(0x80770004)); 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 c95c1542cd..3245cc824e 100644 --- a/core/libs/tests/GuestOpusCeltDec.cpp +++ b/core/libs/tests/GuestOpusCeltDec.cpp @@ -1,44 +1,152 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include -#include -#include +#include #include -#include extern "C" { int APS5_VABI sceOpusCeltDecInitialize(std::uint32_t*); -int APS5_VABI sceOpusCeltDecTerminate(std::uint32_t*); +int APS5_VABI sceOpusCeltDecTerminate(const std::uint32_t*); int APS5_VABI sceOpusCeltDecGetSize(int); -int APS5_VABI sceOpusCeltDecCreateEx(std::uint32_t*, void*, int, int); +int APS5_VABI sceOpusCeltDecCreateEx(const std::uint32_t*, void*, int, int); int APS5_VABI sceOpusCeltDecDecode(void*, const std::uint8_t*, int, std::int16_t*, int); int APS5_VABI sceOpusCeltDecDestroy(void*); } -static int failures = 0; +namespace { -static void RequireNotImplemented(const char* name, const std::function& call) { - try { - call(); - } catch (const std::runtime_error& error) { - if (std::string(error.what()) == std::string(name) + " not implemented") return; - std::fprintf(stderr, "%s threw: %s\n", name, error.what()); - ++failures; - return; - } - std::fprintf(stderr, "%s returned instead of reporting that it is not implemented\n", name); - ++failures; +void Require(bool value) { if (!value) std::abort(); } + +template +void RequireThrows(TFunction function) { + try { function(); } catch (const std::runtime_error&) { return; } + std::abort(); } +const std::uint8_t CELT_STEREO[] = { + 0x75, 0x00, 0xFC, 0xB5, 0x1F, 0xBC, 0xC8, 0xCA, 0x0B, 0xC2, 0x18, 0x1A, 0xFB, 0xDB, 0x3E, 0x2B, 0x6B, 0x0A, 0x67, 0x71, 0x25, 0x2F, 0x51, 0x57, + 0x4F, 0x78, 0xB3, 0x1F, 0xE9, 0xF3, 0x99, 0x51, 0xE9, 0x70, 0x72, 0x23, 0x99, 0x3E, 0x26, 0xA3, 0xF1, 0x8F, 0x02, 0xB3, 0xAA, 0xBA, 0xAB, 0x7E, + 0xA5, 0x6A, 0xBA, 0x33, 0xCA, 0xE9, 0x43, 0x4D, 0x41, 0xC9, 0x90, 0x3C, 0xD0, 0x5F, 0x55, 0xC1, 0x54, 0xA8, 0xD0, 0x6F, 0x71, 0x4F, 0xCB, 0x95, + 0xDF, 0x09, 0xC9, 0xB6, 0xAA, 0xA1, 0x8D, 0xD4, 0x2B, 0xC0, 0xF7, 0x23, 0xDD, 0xF5, 0x54, 0xEF, 0x90, 0xC4, 0xD3, 0x47, 0xC1, 0x91, 0x2D, 0xA8, + 0x3D, 0x53, 0xF4, 0x7D, 0xFF, 0xBD, 0x47, 0x0E, 0xD2, 0x95, 0x25, 0x42, 0x66, 0x02, 0xD7, 0x80, 0x00, 0x67, 0x59, 0x13, 0x60, 0x2F, 0x6C, 0x60, + 0x00, 0xFC, 0xB3, 0xF1, 0x8F, 0xA9, 0xA8, 0x43, 0x9D, 0xC0, 0x23, 0xBE, 0x5B, 0x38, 0xE8, 0xF8, 0xB8, 0x85, 0xBE, 0xD2, 0x2F, 0x40, 0x53, 0x40, + 0xEF, 0x24, 0xE5, 0xA6, 0x91, 0xF4, 0x9B, 0x73, 0xC5, 0x61, 0x61, 0x08, 0x3F, 0x56, 0x7D, 0x7A, 0x2A, 0xBC, 0xAF, 0x3A, 0x70, 0xDD, 0x5E, 0x0B, + 0x12, 0xBD, 0xC5, 0xEA, 0x69, 0x63, 0x65, 0x36, 0x5E, 0x27, 0x15, 0xBA, 0xB0, 0xE3, 0x62, 0xE7, 0x4F, 0x54, 0xFA, 0xD2, 0x49, 0x7E, 0xBD, 0xE4, + 0xDA, 0x9E, 0x2F, 0x90, 0x9E, 0x91, 0x84, 0x6F, 0x95, 0xCE, 0x2D, 0x25, 0x63, 0x23, 0x57, 0x1C, 0x00, 0x20, 0x79, 0xE7, 0xEE, 0xBB, 0x91, 0x7F, + 0xEE, 0x63, 0x00, 0xFC, 0xB2, 0x91, 0x0B, 0x44, 0x10, 0xE2, 0xD2, 0x82, 0x28, 0x1C, 0x96, 0x98, 0x61, 0xB7, 0x87, 0x46, 0x4A, 0x96, 0xA1, 0x41, + 0xE7, 0x5F, 0x84, 0xA1, 0x73, 0x0E, 0x28, 0x29, 0xD0, 0x65, 0x9C, 0x99, 0x33, 0xA0, 0x7E, 0x63, 0xB9, 0x51, 0xED, 0x67, 0xCB, 0x5D, 0xD2, 0x22, + 0xC8, 0x0C, 0x59, 0x72, 0xB5, 0x37, 0xA9, 0xB5, 0x24, 0x25, 0xB3, 0xA1, 0x09, 0x8B, 0xBA, 0x9E, 0x6C, 0x5C, 0x23, 0x7C, 0x7B, 0x19, 0xDB, 0x45, + 0xCF, 0x19, 0xC5, 0x1F, 0x79, 0xCC, 0x16, 0x07, 0xA5, 0x4C, 0xD8, 0xA0, 0x4B, 0x48, 0x8F, 0xAE, 0x17, 0x7C, 0x28, 0xF0, 0x03, 0xFF, 0xF9, 0xE1, + 0x80, 0x55, 0x44, 0x44, 0x7F, 0xEE, +}; + +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() { + constexpr std::uint32_t errorBase = 0x81400000u; + Require(sceOpusCeltDecInitialize(nullptr) == static_cast(errorBase + 13)); + Require(sceOpusCeltDecDestroy(nullptr) == static_cast(errorBase)); std::uint32_t context = 0; - std::uint8_t state[64] = {}; - std::int16_t pcm[16] = {}; - const std::uint8_t packet[] = {0xFC, 0x00}; - RequireNotImplemented("sceOpusCeltDecInitialize", [&] { sceOpusCeltDecInitialize(&context); }); - RequireNotImplemented("sceOpusCeltDecTerminate", [&] { sceOpusCeltDecTerminate(&context); }); - RequireNotImplemented("sceOpusCeltDecGetSize", [] { sceOpusCeltDecGetSize(2); }); - RequireNotImplemented("sceOpusCeltDecCreateEx", [&] { sceOpusCeltDecCreateEx(&context, state, 48000, 2); }); - RequireNotImplemented("sceOpusCeltDecDecode", [&] { sceOpusCeltDecDecode(state, packet, sizeof(packet), pcm, sizeof(pcm)); }); - RequireNotImplemented("sceOpusCeltDecDestroy", [&] { sceOpusCeltDecDestroy(state); }); - return failures == 0 ? 0 : 1; + Require(sceOpusCeltDecInitialize(&context) == 0); + std::array invalidState{}; + std::int16_t output = 12345; + const std::uint8_t packet = 0; + Require(sceOpusCeltDecCreateEx(nullptr, invalidState.data(), 0, 0) == static_cast(errorBase)); + Require(sceOpusCeltDecCreateEx(&context, invalidState.data(), 0, 0) == static_cast(errorBase + 3)); + Require(sceOpusCeltDecCreateEx(&context, invalidState.data(), 48000, 0) == static_cast(errorBase + 3)); + Require(sceOpusCeltDecDecode(nullptr, nullptr, -1, nullptr, -1) == static_cast(errorBase)); + Require(sceOpusCeltDecDecode(invalidState.data(), nullptr, -1, nullptr, -1) == static_cast(errorBase + 6)); + Require(sceOpusCeltDecDecode(invalidState.data(), &packet, -1, nullptr, -1) == static_cast(errorBase + 7)); + Require(sceOpusCeltDecDecode(invalidState.data(), &packet, 1, nullptr, -1) == static_cast(errorBase + 8)); + Require(sceOpusCeltDecDecode(invalidState.data(), &packet, 1, &output, -1) == static_cast(errorBase + 9)); + Require(sceOpusCeltDecDecode(invalidState.data(), &packet, 1, &output, 2) == static_cast(errorBase + 1)); + Require(output == 12345); + + RequireThrows([&] { sceOpusCeltDecInitialize(&context); }); + Require(sceOpusCeltDecGetSize(1) == 640 && sceOpusCeltDecGetSize(2) == 640); + Require(sceOpusCeltDecGetSize(3) == static_cast(errorBase + 3)); + std::array state{}, monoState{}; + Require(sceOpusCeltDecCreateEx(&context, state.data(), 48000, 2) == 0); + Require(sceOpusCeltDecCreateEx(&context, monoState.data(), 48000, 1) == 0); + RequireThrows([&] { sceOpusCeltDecCreateEx(&context, state.data(), 48000, 2); }); + RequireThrows([&] { sceOpusCeltDecTerminate(&context); }); + std::array pcm{}; + pcm.fill(12345); + RequireThrows([&] { sceOpusCeltDecDecode(state.data(), HYBRID_PACKET, sizeof(HYBRID_PACKET), pcm.data() + 1, 3840); }); + Require(std::all_of(pcm.begin(), pcm.end(), [](auto value) { return value == 12345; })); + std::size_t offset = 0; + bool audible = false; + for (int frame = 0; frame < 3; ++frame) { + const int bytes = CELT_STEREO[offset] | (CELT_STEREO[offset + 1] << 8); + const auto* packet = CELT_STEREO + offset + 2; + pcm.fill(12345); + RequireThrows([&] { sceOpusCeltDecDecode(state.data(), packet, bytes, pcm.data() + 1, 3839); }); + Require(sceOpusCeltDecDecode(state.data(), packet, bytes, pcm.data() + 1, 3840) == 3840); + Require(pcm.front() == 12345 && pcm.back() == 12345); + audible = audible || std::any_of(pcm.begin() + 1, pcm.end() - 1, [](auto value) { return value != 0; }); + std::array mono{}; + mono.front() = mono.back() = 12345; + Require(sceOpusCeltDecDecode(monoState.data(), packet, bytes, mono.data() + 1, 1920) == 1920); + Require(mono.front() == 12345 && mono.back() == 12345); + offset += bytes + 2; + } + Require(audible); + + std::array unsupported{}; + for (int rate : {0, 22050, 44100, 96000}) + Require(sceOpusCeltDecCreateEx(&context, unsupported.data(), rate, 2) == static_cast(errorBase + 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); + Require(sceOpusCeltDecDestroy(state.data()) == static_cast(errorBase + 1)); + Require(sceOpusCeltDecDecode(state.data(), CELT_STEREO + 2, CELT_STEREO[0], pcm.data(), 3840) == static_cast(errorBase + 1)); + Require(sceOpusCeltDecDestroy(monoState.data()) == 0); + Require(sceOpusCeltDecTerminate(&context) == 0); + RequireThrows([&] { sceOpusCeltDecTerminate(&context); }); + RequireThrows([&] { sceOpusCeltDecCreateEx(&context, state.data(), 48000, 2); }); } diff --git a/core/libs/tests/GuestOpusDec.cpp b/core/libs/tests/GuestOpusDec.cpp index 0b462ff31c..e19fb78141 100644 --- a/core/libs/tests/GuestOpusDec.cpp +++ b/core/libs/tests/GuestOpusDec.cpp @@ -8,9 +8,9 @@ extern "C" { int APS5_VABI sceOpusDecInitialize(std::uint32_t*); -int APS5_VABI sceOpusDecTerminate(std::uint32_t*); +int APS5_VABI sceOpusDecTerminate(const std::uint32_t*); int APS5_VABI sceOpusDecGetSize(int); -int APS5_VABI sceOpusDecCreateEx(std::uint32_t*, void*, int, int); +int APS5_VABI sceOpusDecCreateEx(const std::uint32_t*, void*, int, int); int APS5_VABI sceOpusDecDecode(void*, const std::uint8_t*, int, std::int16_t*, int); int APS5_VABI sceOpusDecDestroy(void*); } @@ -49,8 +49,25 @@ const std::uint8_t OPUS_NARROWBAND[] = { } // namespace int main() { + constexpr std::uint32_t errorBase = 0x81740000u; + Require(sceOpusDecInitialize(nullptr) == static_cast(errorBase + 13)); + Require(sceOpusDecDestroy(nullptr) == static_cast(errorBase)); std::uint32_t context = 0, otherContext = 0; Require(sceOpusDecInitialize(&context) == 0); + std::array invalidState{}; + std::int16_t output = 12345; + const std::uint8_t packet = 0; + Require(sceOpusDecCreateEx(nullptr, invalidState.data(), 0, 0) == static_cast(errorBase)); + Require(sceOpusDecCreateEx(&context, invalidState.data(), 0, 0) == static_cast(errorBase + 2)); + Require(sceOpusDecCreateEx(&context, invalidState.data(), 48000, 0) == static_cast(errorBase + 3)); + Require(sceOpusDecDecode(nullptr, nullptr, -1, nullptr, -1) == static_cast(errorBase)); + Require(sceOpusDecDecode(invalidState.data(), nullptr, -1, nullptr, -1) == static_cast(errorBase + 6)); + Require(sceOpusDecDecode(invalidState.data(), &packet, -1, nullptr, -1) == static_cast(errorBase + 7)); + Require(sceOpusDecDecode(invalidState.data(), &packet, 1, nullptr, -1) == static_cast(errorBase + 8)); + Require(sceOpusDecDecode(invalidState.data(), &packet, 1, &output, -1) == static_cast(errorBase + 9)); + Require(sceOpusDecDecode(invalidState.data(), &packet, 1, &output, 2) == static_cast(errorBase + 1)); + Require(output == 12345); + Require(sceOpusDecInitialize(&otherContext) == 0 && context != otherContext); RequireThrows([&] { sceOpusDecInitialize(&context); }); Require(sceOpusDecGetSize(1) == 640 && sceOpusDecGetSize(2) == 640); @@ -60,10 +77,10 @@ int main() { Require(sceOpusDecCreateEx(&context, monoState.data(), 48000, 1) == 0); RequireThrows([&] { sceOpusDecTerminate(&context); }); RequireThrows([&] { sceOpusDecCreateEx(&context, state.data(), 48000, 2); }); - RequireThrows([&] { sceOpusDecGetSize(3); }); + Require(sceOpusDecGetSize(3) == static_cast(errorBase + 3)); std::array unsupported{}; - for (int rate : {8000, 12000, 16000, 24000}) - RequireThrows([&] { sceOpusDecCreateEx(&context, unsupported.data(), rate, 2); }); + for (int rate : {0, 22050, 44100, 96000}) + Require(sceOpusDecCreateEx(&context, unsupported.data(), rate, 2) == static_cast(errorBase + 2)); std::array pcm{}, reference{}; pcm.fill(12345); RequireThrows([&] { sceOpusDecDecode(state.data(), OPUS_STEREO + 2, 70, pcm.data() + 1, 3839); }); @@ -99,9 +116,28 @@ 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); }); + Require(sceOpusDecDestroy(state.data()) == static_cast(errorBase + 1)); + Require(sceOpusDecDecode(state.data(), OPUS_STEREO + 2, 70, pcm.data(), 3840) == static_cast(errorBase + 1)); Require(sceOpusDecCreateEx(&context, state.data(), 48000, 2) == 0); Require(sceOpusDecDecode(state.data(), OPUS_STEREO + 2, 70, pcm.data(), 3840) == 3840); std::array rejected; diff --git a/core/libs/tests/GuestPPSA01530Exports.cpp b/core/libs/tests/GuestPPSA01530Exports.cpp deleted file mode 100644 index aeafa78f9c..0000000000 --- a/core/libs/tests/GuestPPSA01530Exports.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "prx/libc/include/general/VabiMacros.hpp" - -#include -#include -#include - -extern "C" { -int APS5_VABI sceNetShowIfconfig(); -int APS5_VABI scePadUnknown_n3kSX62fgNo(); -int APS5_VABI _ZN3sce4Json5Value21setNullAccessCallBackEPFRKS1_NS0_9ValueTypeEPS2_PvES6_(); -} - -namespace { - -struct Export { - const char* name; - int (APS5_VABI *function)(); -}; - -const Export exports[] = { - {"sceNetShowIfconfig", sceNetShowIfconfig}, - {"scePadUnknown_n3kSX62fgNo", scePadUnknown_n3kSX62fgNo}, - {"_ZN3sce4Json5Value21setNullAccessCallBackEPFRKS1_NS0_9ValueTypeEPS2_PvES6_", _ZN3sce4Json5Value21setNullAccessCallBackEPFRKS1_NS0_9ValueTypeEPS2_PvES6_}, -}; - -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 9fa2e91b41..6dfa8c9a9c 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -3,11 +3,15 @@ #include "prx/libScePad/include/PadState.hpp" #include "prx/libScePad/include/InputCommandFile.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include +#include #include #include +#include #include +#include extern "C" { int APS5_VABI scePadOpen_nid_postfix(int, int, int, const void*); @@ -17,6 +21,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); @@ -24,6 +29,15 @@ int APS5_VABI scePadResetOrientation(int); int APS5_VABI scePadSetAngularVelocityDeadbandState(int, bool); int APS5_VABI scePadSetAngularVelocityBiasCorrectionState(int, bool); int APS5_VABI scePadIsRemoteController(int, bool*); +int APS5_VABI scePadGetAccessibilityControllerState(int, std::uint32_t*); +bool APS5_VABI scePadIsDS4Connected(std::int32_t); +bool APS5_VABI scePadIsBlasterConnected(std::int32_t); +bool APS5_VABI scePadIsMoveConnected(std::int32_t); +int APS5_VABI scePadGetControllerInformation(int, PadControllerInformation*); +int APS5_VABI scePadGetExtControllerInformation(int, void*); +int APS5_VABI scePadResetLightBar(int); +int APS5_VABI scePadSetLightBar(int, const PadLightBarParam*); +int APS5_VABI scePadSetMotionSensorState(int, bool); } static void Require(bool value) { if (!value) std::abort(); } @@ -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,6 @@ static float SettleOrientationW() { } static void CheckTiltCorrection(int handle) { - Require(scePadInit_nid_postfix() == 0); PadInputState tilted; tilted.hasMotion = true; tilted.accel = {9.80665f, 0.0f, 0.0f}; @@ -131,6 +152,17 @@ static void CheckAngularVelocityDeadband(int handle) { PadPublishInput_nid_postfix(PadInputState{}); } +static void CheckStickDeadZones(int handle) { + PadControllerInformation info{}; + Require(scePadGetControllerInformation(handle, &info) == PAD_OK); + Require(info.stickInfo.deadZoneLeft == 13 && info.stickInfo.deadZoneRight == 13); + std::array extended{}; + Require(scePadGetExtControllerInformation(handle, extended.data()) == PAD_OK); + PadControllerInformation base{}; + std::memcpy(&base, extended.data(), sizeof(base)); + Require(base.stickInfo.deadZoneLeft == 13 && base.stickInfo.deadZoneRight == 13); +} + static void CheckTouchContact() { PadInputState touch; touch.buttons = static_cast(Pad::PadButton::TouchPad); @@ -145,6 +177,46 @@ static void CheckTouchContact() { Pad::ReadState(); } +static void CheckTouchPadInformation(int handle) { + PadControllerInformation info; + std::memset(&info, 0x5a, sizeof(info)); + Require(scePadGetControllerInformation(handle, &info) == PAD_OK); + Require(info.touchPadInfo.resolution.x == kTouchPadWidth); + Require(info.touchPadInfo.resolution.y == kTouchPadHeight); + Require(info.touchPadInfo.pixelDensity == 44.86f); + Require(scePadGetControllerInformation(handle + 1, &info) == PAD_ERROR_INVALID_HANDLE); + + std::array extended{}; + extended.fill(0x5a); + Require(scePadGetExtControllerInformation(handle, extended.data()) == PAD_OK); + PadControllerInformation base{}; + std::memcpy(&base, extended.data(), sizeof(base)); + Require(base.touchPadInfo.resolution.x == kTouchPadWidth); + Require(base.touchPadInfo.resolution.y == kTouchPadHeight); + Require(base.touchPadInfo.pixelDensity == 44.86f); +} + +static void CheckVirtualTouchContact() { + PadInputState left; + left.touchLeft = true; + PadPublishInput_nid_postfix(left); + const auto leftData = Pad::ReadState(); + Require(leftData.touch_data_touch_num == 1); + Require(leftData.touch_data_touch0_x == kTouchPadWidth / 4); + Require(leftData.touch_data_touch0_y == kTouchPadHeight / 2); + + PadInputState right; + right.touchRight = true; + PadPublishInput_nid_postfix(right); + const auto rightData = Pad::ReadState(); + Require(rightData.touch_data_touch_num == 1); + Require(rightData.touch_data_touch0_x == kTouchPadWidth * 3 / 4); + Require(rightData.touch_data_touch0_y == kTouchPadHeight / 2); + + PadPublishInput_nid_postfix(PadInputState{}); + Pad::ReadState(); +} + static void CheckPartialTrigger() { PadInputState partial; partial.buttons = static_cast(Pad::PadButton::L2) | static_cast(Pad::PadButton::R2); @@ -178,12 +250,64 @@ static void CheckReadQueue(int handle) { Require(data[0].buttons == 0); } +static void CheckTapBetweenReads() { + const auto cross = static_cast(Pad::PadButton::Cross); + PadPublishInput_nid_postfix(PadInputState{}); + Pad::ReadState(); + PadInputState pressed; + pressed.buttons = cross; + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + Require((Pad::ReadState().buttons & cross) != 0); + Require((Pad::ReadState().buttons & cross) == 0); + PadPublishInput_nid_postfix(pressed); + Require((Pad::ReadState().buttons & cross) != 0); + Require((Pad::ReadState().buttons & cross) != 0); + PadPublishInput_nid_postfix(PadInputState{}); + Require((Pad::ReadState().buttons & cross) == 0); +} + +static void CheckLatchedPressLifetime(int handle) { + const auto cross = static_cast(Pad::PadButton::Cross); + const auto circle = static_cast(Pad::PadButton::Circle); + PadInputState pressed; + PadData data{}; + PadData drained[64]{}; + scePadRead_nid_postfix(handle, drained, 64); + pressed.buttons = cross; + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + Require(scePadRead_nid_postfix(handle, drained, 64) >= 1); + Require(scePadReadState(handle, &data) == PAD_OK); + Require((data.buttons & cross) == 0); + pressed.buttons = cross; + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + Require(scePadReadState(handle, &data) == PAD_OK); + Require((data.buttons & cross) == 0); + pressed.buttons = cross; + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + pressed.buttons = circle; + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + Require(scePadReadState(handle, &data) == PAD_OK); + Require((data.buttons & cross) == 0); + Require((data.buttons & circle) != 0); +} + static void CheckReadStateHandle(int handle) { PadData data{}; Require(scePadReadState(0, &data) == PAD_ERROR_INVALID_HANDLE); Require(scePadReadState(handle + 1, &data) == PAD_ERROR_INVALID_HANDLE); Require(scePadRead_nid_postfix(0, &data, 1) == PAD_ERROR_INVALID_HANDLE); Require(scePadReadState(handle, &data) == PAD_OK); + Require(scePadRead_nid_postfix(handle, nullptr, 1) == PAD_ERROR_INVALID_ARG); + Require(scePadRead_nid_postfix(handle, &data, 0) == PAD_ERROR_INVALID_ARG); + Require(scePadRead_nid_postfix(handle, &data, -1) == PAD_ERROR_INVALID_ARG); + Require(scePadReadState(handle, nullptr) == PAD_ERROR_INVALID_ARG); } static void CheckRemoteController(int handle) { @@ -195,6 +319,23 @@ static void CheckRemoteController(int handle) { Require(!remote); } +static void CheckTrackerDevices(int handle) { + for (const int candidate : {handle, handle + 1}) { + Require(!scePadIsDS4Connected(candidate)); + Require(!scePadIsBlasterConnected(candidate)); + Require(!scePadIsMoveConnected(candidate)); + } +} + +static void CheckAccessibilityControllerState(int handle) { + std::uint32_t state = 7; + Require(scePadGetAccessibilityControllerState(handle + 1, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadGetAccessibilityControllerState(handle + 1, &state) == PAD_ERROR_INVALID_HANDLE); + Require(state == 7); + Require(scePadGetAccessibilityControllerState(handle, &state) == PAD_OK); + Require(state == 0); +} + static void CheckInjectedState(const std::filesystem::path& path) { PadInputState physical; physical.buttons = 0x2000; @@ -235,21 +376,66 @@ 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", kTouchPadWidth / 4}, std::pair{"4 4194304 300\n", kTouchPadWidth * 3 / 4}}) { + std::ofstream(path) << command; + PadData touched{}; + const auto touchDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + 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 == kTouchPadHeight / 2); + 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(); + std::uint32_t accessibilityState = 7; + Require(scePadGetAccessibilityControllerState(PAD_HANDLE, nullptr) == PAD_ERROR_NOT_INITIALIZED); + Require(scePadGetAccessibilityControllerState(PAD_HANDLE, &accessibilityState) == PAD_ERROR_NOT_INITIALIZED); + Require(accessibilityState == 7); + Require(scePadUnknown_n3kSX62fgNo() == static_cast(0x80920005)); const auto path = std::filesystem::temp_directory_path() / ("anyps5-pad-state-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); struct Cleanup { 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; + PadOutputState snapshot; + Pad::SetVibration(12, 34); + PadSubmitAudioHaptics_nid_postfix(100, 150); + Require(PadFetchOutput_nid_postfix(&sequence, &snapshot)); + Require(snapshot.audioVibrationLarge == 100 && snapshot.audioVibrationSmall == 150); + Require(snapshot.vibrationLarge == 12 && snapshot.vibrationSmall == 34); + PadQueueLegacyAudioHaptics_nid_postfix(1, 0, ~std::uint64_t{0}, 200, 20); + Require(PadFetchOutput_nid_postfix(&sequence, &snapshot)); + Require(snapshot.audioVibrationLarge == 200 && snapshot.audioVibrationSmall == 150); + PadClearLegacyAudioHaptics_nid_postfix(1); + Require(PadFetchOutput_nid_postfix(&sequence, &snapshot)); + Require(snapshot.audioVibrationLarge == 100 && snapshot.audioVibrationSmall == 150); + Require(snapshot.vibrationLarge == 12 && snapshot.vibrationSmall == 34); + PadSubmitAudioHaptics_nid_postfix(0, 0); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Require(PadFetchOutput_nid_postfix(&sequence, &snapshot)); + Require(snapshot.audioVibrationLarge == 0 && snapshot.audioVibrationSmall == 0); + Require(snapshot.vibrationLarge == 12 && snapshot.vibrationSmall == 34); + Pad::SetVibration(0, 0); + } constexpr int noHandle = static_cast(0x80920008); constexpr int user = 0x10000000; @@ -264,8 +450,11 @@ 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() == PAD_OK); const int handle = scePadOpen_nid_postfix(user, 0, 0, nullptr); Require(handle > 0); + Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); Require(scePadGetHandle(user, 0, 0) == handle); Require(scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == static_cast(0x80920007)); Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == noHandle); @@ -273,21 +462,40 @@ int main() { CheckTiltCorrection(handle); CheckAngularVelocityDeadband(handle); CheckTouchContact(); + CheckVirtualTouchContact(); + CheckTouchPadInformation(handle); + CheckStickDeadZones(handle); CheckPartialTrigger(); CheckReadQueue(handle); + CheckTapBetweenReads(); + CheckLatchedPressLifetime(handle); CheckReadStateHandle(handle); CheckRemoteController(handle); - CheckInjectedState(path); + CheckTrackerDevices(handle); + CheckAccessibilityControllerState(handle); + CheckInjectedState(commandPath); + PadLightBarParam lightBar{255, 128, 64}; + Require(scePadSetLightBar(handle, &lightBar) == PAD_OK); + Require(scePadSetLightBar(handle, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadSetLightBar(handle + 1, &lightBar) == PAD_ERROR_INVALID_HANDLE); + Require(scePadResetLightBar(handle) == PAD_OK); + Require(scePadResetLightBar(handle + 1) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetMotionSensorState(handle, true) == PAD_OK); + Require(scePadSetMotionSensorState(handle, false) == PAD_OK); + Require(scePadSetMotionSensorState(handle + 1, true) == PAD_ERROR_INVALID_HANDLE); Require(scePadSetVibrationMode(handle, 1) == 0); Require(scePadSetVibrationMode(handle, 2) == 0); Require(scePadSetVibrationMode(handle, 3) == 0); Require(scePadSetVibrationMode(handle, 4) == PAD_ERROR_INVALID_ARG); + Require(scePadSetVibrationMode(handle, 5) == 0); + Require(scePadSetVibrationMode(handle, 6) == PAD_ERROR_INVALID_ARG); Require(scePadSetVibrationMode(handle, -1) == PAD_ERROR_INVALID_ARG); Require(scePadGetHandle(0xff, 16, 0) == handle); Require(scePadGetHandle(user, 16, 0) == noHandle); Require(scePadGetHandle(user, 0, 1) == noHandle); Require(scePadClose_nid_postfix(handle) == 0); Require(scePadGetHandle(user, 0, 0) == noHandle); + Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); Require(scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(true) == 0); Require(scePadSetAngularVelocityDeadbandState(handle, false) == 0); Require(scePadSetAngularVelocityDeadbandState(handle + 1, false) == PAD_ERROR_INVALID_HANDLE); diff --git a/core/libs/tests/GuestPadVibrationMode.cpp b/core/libs/tests/GuestPadVibrationMode.cpp new file mode 100644 index 0000000000..1d826fedb1 --- /dev/null +++ b/core/libs/tests/GuestPadVibrationMode.cpp @@ -0,0 +1,33 @@ +#include "SceTypes.hpp" +#include "prx/libScePad/include/Pad.hpp" +#include +#include + +extern "C" { +int APS5_VABI scePadInit_nid_postfix(void); +int APS5_VABI scePadOpen_nid_postfix(int userId, int type, int index, const void* param); +int APS5_VABI scePadClose_nid_postfix(int handle); +int APS5_VABI scePadSetVibrationMode(int handle, int mode); +} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + for (int handle : {-1, 0, 1, 12345}) + for (int mode : {-1, 0, 1, 4}) + Require(scePadSetVibrationMode(handle, mode) == PAD_ERROR_NOT_INITIALIZED); + + Require(scePadInit_nid_postfix() == PAD_OK); + for (int handle : {-1, 0, 12345}) { + Require(scePadSetVibrationMode(handle, -1) == PAD_ERROR_INVALID_ARG); + Require(scePadSetVibrationMode(handle, 0) == PAD_ERROR_INVALID_ARG); + for (int mode : {1, 2, 3, 4, 5}) + Require(scePadSetVibrationMode(handle, mode) == PAD_ERROR_INVALID_HANDLE); + } + + const int handle = scePadOpen_nid_postfix(0x10000000, 0, 0, nullptr); + Require(handle > 0); + Require(scePadSetVibrationMode(handle, 0) == PAD_ERROR_INVALID_ARG); + Require(scePadSetVibrationMode(handle, 1) == PAD_OK); + Require(scePadClose_nid_postfix(handle) == PAD_OK); +} diff --git a/core/libs/tests/GuestPageTableStats.cpp b/core/libs/tests/GuestPageTableStats.cpp index 6a47951c8a..eba38dd12c 100644 --- a/core/libs/tests/GuestPageTableStats.cpp +++ b/core/libs/tests/GuestPageTableStats.cpp @@ -26,24 +26,37 @@ static void Require(bool condition, std::source_location location = std::source_ } constexpr std::size_t PageTableGranule = 0x200000; -constexpr int PageTableTotal = 2048; +constexpr std::size_t GpuDirectorySpan = 0x40000000; +constexpr int PageTableTotal = 8192; -struct Stats { - int cpuTotal; - int cpuAvailable; - int gpuTotal; - int gpuAvailable; +struct Usage { + int cpu; + int gpu; }; -static Stats Query() { - Stats stats{}; - Require(sceKernelGetPageTableStats(&stats.cpuTotal, &stats.cpuAvailable, &stats.gpuTotal, &stats.gpuAvailable) == 0); - return stats; +static Usage Used() { + int cpuTotal = 0, cpuAvailable = 0, gpuTotal = 0, gpuAvailable = 0; + Require(sceKernelGetPageTableStats(&cpuTotal, &cpuAvailable, &gpuTotal, &gpuAvailable) == 0); + Require(cpuTotal == PageTableTotal && gpuTotal == PageTableTotal); + Require(cpuAvailable >= 0 && cpuAvailable <= cpuTotal && gpuAvailable >= 0 && gpuAvailable <= gpuTotal); + return {cpuTotal - cpuAvailable, gpuTotal - gpuAvailable}; } -static int Granules(const void* addr, std::size_t len) { - const auto start = reinterpret_cast(addr); - return static_cast((start + len - 1) / PageTableGranule - start / PageTableGranule + 1); +static void RequireTaken(const Usage& before, int cpu, int gpu, std::source_location location = std::source_location::current()) { + const auto now = Used(); + Require(now.cpu - before.cpu == cpu && now.gpu - before.gpu == gpu, location); +} + +static unsigned char* Reserve() { + void* reserved = nullptr; + Require(sceKernelReserveVirtualRange(&reserved, GpuDirectorySpan * 2, 0, GpuDirectorySpan) == 0); + Require(reinterpret_cast(reserved) % GpuDirectorySpan == 0); + return static_cast(reserved); +} + +static void Map(unsigned char* at, std::size_t len, int prot) { + void* address = at; + Require(sceKernelMapFlexibleMemory(&address, len, prot, 0x10) == 0 && address == at); } static void CheckRejectedArguments() { @@ -54,85 +67,54 @@ static void CheckRejectedArguments() { Require(sceKernelGetPageTableStats(&value, &value, &value, nullptr) == SCE_KERNEL_ERROR_EFAULT); } -static void CheckFlexibleMapping() { - const auto before = Query(); - Require(before.cpuTotal == PageTableTotal && before.gpuTotal == PageTableTotal); - Require(before.cpuAvailable <= before.cpuTotal && before.gpuAvailable <= before.gpuTotal); - - constexpr std::size_t len = 3 * PageTableGranule; - void* mapped = nullptr; - Require(sceKernelMapFlexibleMemory(&mapped, len, 3, 0) == 0 && mapped != nullptr); - const auto entries = Granules(mapped, len); - auto during = Query(); - Require(during.cpuAvailable == before.cpuAvailable - entries); - Require(during.gpuAvailable == before.gpuAvailable); - - Require(sceKernelMprotect(mapped, len, 0x30) == 0); - during = Query(); - Require(during.cpuAvailable == before.cpuAvailable); - Require(during.gpuAvailable == before.gpuAvailable - entries); - - Require(sceKernelMunmap(mapped, len) == 0); - const auto after = Query(); - Require(after.cpuAvailable == before.cpuAvailable && after.gpuAvailable == before.gpuAvailable); +static void CheckCpuGranules() { + auto* base = Reserve(); + const auto before = Used(); + Map(base, 0x4000, 3); + Map(base + 0x4000, 0x4000, 3); + Map(base + 0x8000, 0x4000, 1); + RequireTaken(before, 1, 0); + Map(base + PageTableGranule, 0x4000, 0); + RequireTaken(before, 2, 0); + Require(sceKernelMunmap(base, GpuDirectorySpan * 2) == 0); + RequireTaken(before, 0, 0); } -static void CheckDualAccessMapping() { - const auto before = Query(); - constexpr std::size_t len = PageTableGranule; - void* mapped = nullptr; - Require(sceKernelMapFlexibleMemory(&mapped, len, 0x33, 0) == 0 && mapped != nullptr); - const auto entries = Granules(mapped, len); - const auto during = Query(); - Require(during.cpuAvailable == before.cpuAvailable - entries); - Require(during.gpuAvailable == before.gpuAvailable - entries); - Require(sceKernelMunmap(mapped, len) == 0); - const auto after = Query(); - Require(after.cpuAvailable == before.cpuAvailable && after.gpuAvailable == before.gpuAvailable); -} - -static void CheckSharedGranule() { - const auto before = Query(); - constexpr std::size_t page = 0x4000; - void* span = nullptr; - Require(sceKernelReserveVirtualRange(&span, 2 * PageTableGranule, 0, PageTableGranule) == 0 && span != nullptr); - const auto base = reinterpret_cast(span); - const auto aligned = base % PageTableGranule == 0 ? base : (base / PageTableGranule + 1) * PageTableGranule; - void* first = reinterpret_cast(aligned); - void* second = reinterpret_cast(aligned + page); - Require(sceKernelMapFlexibleMemory(&first, page, 3, 0) == 0 && first == reinterpret_cast(aligned)); - Require(sceKernelMapFlexibleMemory(&second, page, 3, 0) == 0 && second == reinterpret_cast(aligned + page)); - const auto during = Query(); - Require(during.cpuAvailable == before.cpuAvailable - 2); - Require(during.gpuAvailable == before.gpuAvailable); - Require(sceKernelMunmap(first, page) == 0); - Require(sceKernelMunmap(second, page) == 0); - const auto after = Query(); - Require(after.cpuAvailable == before.cpuAvailable && after.gpuAvailable == before.gpuAvailable); +static void CheckGpuEntries() { + auto* base = Reserve(); + const auto before = Used(); + Map(base, 3 * PageTableGranule, 3); + RequireTaken(before, 3, 0); + Require(sceKernelMprotect(base, 3 * PageTableGranule, 0x30) == 0); + RequireTaken(before, 3, 4); + Map(base + GpuDirectorySpan, 0x4000, 0x33); + RequireTaken(before, 4, 6); + Require(sceKernelMunmap(base, GpuDirectorySpan * 2) == 0); + RequireTaken(before, 0, 0); } static void CheckGpuDirectMapping() { - const auto before = Query(); - constexpr std::size_t len = 2 * PageTableGranule; + auto* base = Reserve(); + const auto before = Used(); std::int64_t phys = 0; - Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, len, 0, 0, &phys) == 0); - void* mapped = nullptr; - Require(sceKernelMapDirectMemory(&mapped, len, 0x33, 0, phys, 0) == 0 && mapped != nullptr); - const auto entries = Granules(mapped, len); - const auto during = Query(); - Require(during.gpuAvailable == before.gpuAvailable - entries); - Require(during.cpuAvailable == before.cpuAvailable - entries); - Require(sceKernelMunmap(mapped, len) == 0); - Require(sceKernelReleaseDirectMemory(phys, len) == 0); - const auto after = Query(); - Require(after.cpuAvailable == before.cpuAvailable && after.gpuAvailable == before.gpuAvailable); + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, 3 * PageTableGranule, PageTableGranule, 0, &phys) == 0); + Require(phys % static_cast(PageTableGranule) == 0); + void* large = base; + Require(sceKernelMapDirectMemory(&large, 2 * PageTableGranule, 0x33, 0x10, phys, PageTableGranule) == 0 && large == base); + RequireTaken(before, 2, 1); + void* small = base + 2 * PageTableGranule + 0x10000; + Require(sceKernelMapDirectMemory(&small, 0x10000, 0x33, 0x10, phys + static_cast(2 * PageTableGranule), 0x10000) == 0); + Require(small == base + 2 * PageTableGranule + 0x10000); + RequireTaken(before, 3, 2); + Require(sceKernelMunmap(base, GpuDirectorySpan * 2) == 0); + Require(sceKernelReleaseDirectMemory(phys, 3 * PageTableGranule) == 0); + RequireTaken(before, 0, 0); } int main() { CheckRejectedArguments(); - CheckFlexibleMapping(); - CheckDualAccessMapping(); - CheckSharedGranule(); + CheckCpuGranules(); + CheckGpuEntries(); CheckGpuDirectMapping(); return 0; } diff --git a/core/libs/tests/GuestPathDriveComponent.cpp b/core/libs/tests/GuestPathDriveComponent.cpp new file mode 100644 index 0000000000..c17bafcbfc --- /dev/null +++ b/core/libs/tests/GuestPathDriveComponent.cpp @@ -0,0 +1,51 @@ +#include "prx/libc/include/General.hpp" + +#include +#include +#include +#include +#include + +static void Require(bool value) { if (!value) std::abort(); } + +static std::string Under(const std::filesystem::path& directory, const std::string& names) { + auto text = directory.string(); + for (std::size_t start = 0; start <= names.size();) { + const auto end = std::min(names.find('/', start), names.size()); + text += static_cast(std::filesystem::path::preferred_separator); + text += names.substr(start, end - start); + start = end + 1; + } + return text; +} + +int main() { + const auto host = std::filesystem::canonical(std::filesystem::current_path()); + const auto name = "AnyPS5-Drive-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const auto directory = host / name; + const auto mounted = directory / "Mounted"; + Require(std::filesystem::create_directories(mounted / "Nested")); + { std::ofstream file(directory / "Secret.txt"); file << 'x'; } + auto drive = host.root_name().string(); + if (drive.empty()) drive = "C:"; + std::string outside; + for (const auto& part : directory.relative_path()) outside += "/" + part.string(); + AddPathAlias_nid_no_patch("/drive-mount", mounted.string().c_str()); + + Require(ResolvePath_nid_no_patch(("/drive-mount/" + drive + outside + "/Secret.txt").c_str()).string() == + Under(mounted, drive + outside + "/Secret.txt")); + Require(ResolvePath_nid_no_patch(("/drive-mount/" + drive + "Nested").c_str()).string() == Under(mounted, drive + "Nested")); + Require(ResolvePath_nid_no_patch(("/drive-mount/" + drive).c_str()).string() == Under(mounted, drive)); + Require(ResolvePath_nid_no_patch(("/drive-mount/Nested/" + drive + "/x").c_str()).string() == Under(mounted, "Nested/" + drive + "/x")); + Require(ResolvePath_nid_no_patch(("/" + name + "/../" + drive + outside + "/Secret.txt").c_str()).string() == + Under(host, drive + outside + "/Secret.txt")); + Require(ResolvePath_nid_no_patch(("/" + drive + "/" + name + "/Secret.txt").c_str()).string() == + Under(host, drive + "/" + name + "/Secret.txt")); + Require(ResolvePath_nid_no_patch(("/" + drive).c_str()).string() == Under(host, drive)); + Require(ResolvePath_nid_no_patch(("/" + name + "/" + drive + "/x").c_str()).string() == Under(directory, drive + "/x")); + Require(ResolvePath_nid_no_patch(("/" + name + "/Secret.txt").c_str()) == directory / "Secret.txt"); + Require(ResolvePath_nid_no_patch("/drive-mount/Nested").string() == Under(mounted, "Nested")); + + RemovePathAlias_nid_no_patch("/drive-mount"); + std::filesystem::remove_all(directory); +} diff --git a/core/libs/tests/GuestPathErrors.cpp b/core/libs/tests/GuestPathErrors.cpp new file mode 100644 index 0000000000..3da76d9739 --- /dev/null +++ b/core/libs/tests/GuestPathErrors.cpp @@ -0,0 +1,145 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/KernelErrors.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode); +int APS5_VABI sceKernelUnlink(const char* path); +int APS5_VABI sceKernelClose(int d); +int APS5_VABI open_nid_postfix(const char* path, int flags, int mode); +int APS5_VABI unlink_nid_postfix(const char* path); +int APS5_VABI sceKernelStat(const char* path, FileStat* sb); +int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb); +int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb); +int APS5_VABI sceKernelMkdir(const char* path, std::uint16_t mode); +int APS5_VABI sceKernelRmdir(const char* path); +int APS5_VABI mkdir_nid_postfix(const char* path, std::uint16_t mode); +int APS5_VABI rmdir_nid_postfix(const char* path); +int APS5_VABI sceKernelRename(const char* from, const char* to); +int APS5_VABI rename_nid_postfix(const char* from, const char* to); +int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t length); +int APS5_VABI truncate_nid_postfix(const char* path, std::int64_t length); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "path error check failed at line %d\n", line); + std::exit(1); + } +} +#define Check(value) Require((value), __LINE__) + +constexpr int GuestEloop = 62; +constexpr int GuestEnametoolong = 63; +constexpr int SceEloop = SceKernelError(GuestEloop); +constexpr int SceEnametoolong = SceKernelError(GuestEnametoolong); + +bool PosixFails(int result, int error) { + const bool failed = result == -1 && *__error_nid_postfix() == error; + *__error_nid_postfix() = 0; + return failed; +} + +std::filesystem::path EnterFreshRoot() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-path-errors-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + Check(::symlink("loop", "loop") == 0); + Check(::symlink("second", "first") == 0 && ::symlink("first", "second") == 0); + { std::ofstream file("plain.txt"); file << "plain"; } + return root; +} + +const std::string LongName(300, 'n'); + +void OpenAndUnlink() { + Check(sceKernelOpen("loop", SCE_KERNEL_O_RDONLY, 0) == SceEloop); + Check(sceKernelOpen("first", SCE_KERNEL_O_RDONLY, 0) == SceEloop); + Check(sceKernelOpen("loop/inside", SCE_KERNEL_O_RDONLY, 0) == SceEloop); + Check(sceKernelOpen(LongName.c_str(), SCE_KERNEL_O_RDONLY, 0) == SceEnametoolong); + Check(sceKernelOpen((LongName + "/inside").c_str(), SCE_KERNEL_O_RDONLY, 0) == SceEnametoolong); + Check(sceKernelOpen(LongName.c_str(), SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT, 0644) == SceEnametoolong); + Check(PosixFails(open_nid_postfix("loop", SCE_KERNEL_O_RDONLY, 0), GuestEloop)); + Check(PosixFails(open_nid_postfix(LongName.c_str(), SCE_KERNEL_O_RDONLY, 0), GuestEnametoolong)); + Check(sceKernelUnlink(LongName.c_str()) == SceEnametoolong); + Check(sceKernelUnlink("loop/inside") == SceEloop); + Check(PosixFails(unlink_nid_postfix(LongName.c_str()), GuestEnametoolong)); + const int plain = sceKernelOpen("plain.txt", SCE_KERNEL_O_RDONLY, 0); + Check(plain >= 0 && sceKernelClose(plain) == 0); +} + +void Status() { + FileStat status{}; + Check(sceKernelStat("loop", &status) == SceEloop); + Check(sceKernelStat("first", &status) == SceEloop); + Check(sceKernelStat("loop/inside", &status) == SceEloop); + Check(sceKernelStat(LongName.c_str(), &status) == SceEnametoolong); + Check(sceKernelStat((LongName + "/inside").c_str(), &status) == SceEnametoolong); + Check(PosixFails(stat_nid_postfix("loop", &status), GuestEloop)); + Check(PosixFails(stat_nid_postfix(LongName.c_str(), &status), GuestEnametoolong)); + Check(PosixFails(lstat_nid_postfix(LongName.c_str(), &status), GuestEnametoolong)); + Check(lstat_nid_postfix("loop", &status) == 0 && (status.st_mode & 0170000) == 0120000); + Check(sceKernelStat("plain.txt", &status) == 0 && status.st_size == 5); + Check(sceKernelStat("missing.txt", &status) == SceKernelError(2)); +} + +void Directories() { + Check(sceKernelMkdir("loop/inside", 0755) == SceEloop); + Check(sceKernelMkdir(LongName.c_str(), 0755) == SceEnametoolong); + Check(PosixFails(mkdir_nid_postfix("loop/inside", 0755), GuestEloop)); + Check(sceKernelRmdir("loop/inside") == SceEloop); + Check(sceKernelRmdir(LongName.c_str()) == SceEnametoolong); + Check(sceKernelRmdir((LongName + "/inside").c_str()) == SceEnametoolong); + Check(PosixFails(rmdir_nid_postfix(LongName.c_str()), GuestEnametoolong)); + Check(sceKernelRmdir("loop") == SceKernelError(20)); + Check(sceKernelMkdir("made", 0755) == 0 && sceKernelRmdir("made") == 0); + Check(sceKernelRmdir("made") == SceKernelError(2)); +} + +void Rename() { + Check(sceKernelRename(LongName.c_str(), "target.txt") == SceEnametoolong); + Check(sceKernelRename("loop/inside", "target.txt") == SceEloop); + Check(sceKernelRename("plain.txt", LongName.c_str()) == SceEnametoolong); + Check(sceKernelRename("plain.txt", "loop/inside") == SceEloop); + Check(PosixFails(rename_nid_postfix(LongName.c_str(), "target.txt"), GuestEnametoolong)); + Check(sceKernelRename("missing.txt", "target.txt") == SceKernelError(2)); + Check(sceKernelRename("plain.txt", "moved.txt") == 0 && sceKernelRename("moved.txt", "plain.txt") == 0); +} + +void Truncate() { + Check(sceKernelTruncate_nid_postfix("loop", 0) == SceEloop); + Check(sceKernelTruncate_nid_postfix(LongName.c_str(), 0) == SceEnametoolong); + Check(PosixFails(truncate_nid_postfix("loop", 0), GuestEloop)); + Check(PosixFails(truncate_nid_postfix(LongName.c_str(), 0), GuestEnametoolong)); + Check(sceKernelTruncate_nid_postfix("missing.txt", 0) == SceKernelError(2)); + Check(sceKernelTruncate_nid_postfix("plain.txt", 2) == 0 && std::filesystem::file_size("plain.txt") == 2); +} + +} + +int main() { + const auto root = EnterFreshRoot(); + OpenAndUnlink(); + Status(); + Directories(); + Rename(); + Truncate(); + std::error_code error; + std::filesystem::current_path(std::filesystem::temp_directory_path(), error); + std::filesystem::remove_all(root, error); + return 0; +} diff --git a/core/libs/tests/GuestPathLifetime.cpp b/core/libs/tests/GuestPathLifetime.cpp new file mode 100644 index 0000000000..44e8f59072 --- /dev/null +++ b/core/libs/tests/GuestPathLifetime.cpp @@ -0,0 +1,80 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); + +namespace { + +int events[2]; +char expectedRoot[4096]; + +void CheckPathsDuringExit() { + bool passed = false; + try { + std::thread worker([&] { + try { + const std::filesystem::path root(expectedRoot); + char cwd[32] = {}; + passed = getcwd_nid_postfix(cwd, sizeof(cwd)) == cwd && + std::strcmp(cwd, "/") == 0 && + ResolvePath_nid_no_patch(".").lexically_relative(root) == "." && + ResolvePath_nid_no_patch("/").lexically_relative(root) == "." && + ResolvePath_nid_no_patch("/lifetime_alias") == root; + RemovePathAlias_nid_no_patch("/lifetime_alias"); + AddPathAlias_nid_no_patch("/lifetime_alias", expectedRoot); + passed = passed && ResolvePath_nid_no_patch("/lifetime_alias") == root; + } catch (const std::exception& error) { + std::fprintf(stderr, "path services during exit: %s\n", error.what()); + passed = false; + } catch (...) { passed = false; } + }); + worker.join(); + } catch (...) { passed = false; } + const char result = passed ? 'P' : 'F'; + static_cast(!write(events[1], &result, 1)); +} + +bool PathsSurviveExit() { + if (pipe(events) != 0) return false; + const pid_t child = fork(); + if (child < 0) { + close(events[0]); + close(events[1]); + return false; + } + if (child == 0) { + close(events[0]); + const auto root = std::filesystem::canonical(std::filesystem::current_path()).string(); + if (root.size() >= sizeof(expectedRoot)) std::_Exit(2); + std::memcpy(expectedRoot, root.c_str(), root.size() + 1); + if (std::atexit(CheckPathsDuringExit) != 0) std::_Exit(2); + ResolvePath_nid_no_patch("/"); + AddPathAlias_nid_no_patch("/lifetime_alias", expectedRoot); + LibcExit_nid_no_patch(0); + } + close(events[1]); + char result = 0; + const ssize_t count = read(events[0], &result, 1); + close(events[0]); + int status = 0; + return waitpid(child, &status, 0) == child && WIFEXITED(status) && + WEXITSTATUS(status) == 0 && count == 1 && result == 'P'; +} + +} + +int main() { + if (!PathsSurviveExit()) { + std::fputs("guest path services must remain available during process exit\n", stderr); + return 1; + } + return 0; +} diff --git a/core/libs/tests/GuestPathNameLength.cpp b/core/libs/tests/GuestPathNameLength.cpp new file mode 100644 index 0000000000..e07722661b --- /dev/null +++ b/core/libs/tests/GuestPathNameLength.cpp @@ -0,0 +1,74 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI mkdir_nid_postfix(const char*, int); +int APS5_VABI rmdir_nid_postfix(const char*); +int APS5_VABI unlink_nid_postfix(const char*); +int APS5_VABI rename_nid_postfix(const char*, const char*); +int APS5_VABI stat_nid_postfix(const char*, void*); +int APS5_VABI access_nid_postfix(const char*, int); +int APS5_VABI chflags_nid_postfix(const char*, std::uint64_t); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int ErrorNoEntry = 2; +constexpr int ErrorNameTooLong = 63; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + std::abort(); +} + +bool Fails(long result, int error) { return result == -1 && *__error_nid_postfix() == error; } + +bool TooLong(const std::string& path) { + unsigned char status[256]; + return Fails(stat_nid_postfix(path.c_str(), status), ErrorNameTooLong); +} + +} + +int main() { + unsigned char status[256]; + const std::string longName(256, 'n'); + const std::string longPath(1100, 'p'); + const std::string maxName(255, 'm'); + Require(Fails(stat_nid_postfix(longName.c_str(), status), ErrorNameTooLong), "stat of a name above NAME_MAX (255) must fail with ENAMETOOLONG (63)"); + rmdir_nid_postfix("anyps5_name_length"); + Require(mkdir_nid_postfix("anyps5_name_length", 0755) == 0, "mkdir of the test directory failed"); + Require(Fails(stat_nid_postfix(("anyps5_name_length/" + longName).c_str(), status), ErrorNameTooLong), "stat of a long final component below an existing directory must fail with ENAMETOOLONG (63)"); + Require(Fails(stat_nid_postfix(("anyps5_name_length/missing/" + longName).c_str(), status), ErrorNoEntry), "stat of a long component below a missing directory must fail with ENOENT (2), the lookup stops there first"); + const std::string below = "anyps5_name_length/"; + const std::string name255(255, 'n'); + Require(!TooLong(below + name255), "a final component of exactly 255 bytes must not be too long"); + Require(!TooLong(below + name255 + "/x"), "a middle component of exactly 255 bytes must not be too long"); + Require(TooLong(below + longName + "/x"), "a middle component of 256 bytes must fail with ENAMETOOLONG (63)"); + Require(TooLong(below + longName + "/"), "a long component followed by a trailing slash must fail with ENAMETOOLONG (63)"); + Require(!TooLong(below + std::string(200, 'n') + "\\" + std::string(100, 'n')), "a backslash separates components here, so 200 + 100 bytes around it are accepted (FreeBSD would refuse the 301-byte name)"); + const std::string block = name255 + "/"; + Require(!TooLong(block + block + block + name255), "a path of exactly 1023 bytes must not be too long"); + Require(TooLong(block + block + block + block), "a path of exactly 1024 bytes must fail with ENAMETOOLONG (63)"); + Require(rmdir_nid_postfix("anyps5_name_length") == 0, "rmdir of the test directory failed"); + Require(Fails(stat_nid_postfix((longName + "/x").c_str(), status), ErrorNameTooLong), "stat of a long first component must fail with ENAMETOOLONG (63)"); + Require(Fails(stat_nid_postfix(longPath.c_str(), status), ErrorNameTooLong), "stat of a path of 1100 bytes must fail with ENAMETOOLONG (63)"); + Require(Fails(access_nid_postfix(longName.c_str(), 0), ErrorNameTooLong), "access of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(chflags_nid_postfix(longName.c_str(), 0), ErrorNameTooLong), "chflags of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(unlink_nid_postfix(longName.c_str()), ErrorNameTooLong), "unlink of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(mkdir_nid_postfix(longName.c_str(), 0755), ErrorNameTooLong), "mkdir of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(rmdir_nid_postfix(longName.c_str()), ErrorNameTooLong), "rmdir of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(open_nid_postfix(longName.c_str(), 0, 0), ErrorNameTooLong), "open of a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(rename_nid_postfix(longName.c_str(), "anyps5_name_length_target"), ErrorNameTooLong), "rename from a long name must fail with ENAMETOOLONG (63)"); + Require(Fails(rename_nid_postfix("anyps5_name_length_source", longName.c_str()), ErrorNameTooLong), "rename to a long name must fail with ENAMETOOLONG (63)"); + + Require(Fails(stat_nid_postfix(maxName.c_str(), status), ErrorNoEntry), "a name of exactly 255 bytes is not too long"); + return 0; +} diff --git a/core/libs/tests/GuestPlayGo.cpp b/core/libs/tests/GuestPlayGo.cpp index cf93ef9eb2..c03f4761ca 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -2,6 +2,8 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include +#include #include #include #include @@ -10,19 +12,42 @@ #include #include #include +#include extern "C" { int APS5_VABI scePlayGoInitialize(const PlayGoInitParams*); int APS5_VABI scePlayGoOpen(int*, const void*); int APS5_VABI scePlayGoClose(int); int APS5_VABI scePlayGoGetLanguageMask(int, std::uint64_t*); +int APS5_VABI scePlayGoSetLanguageMask(int, std::uint64_t); +int APS5_VABI scePlayGoGetSupportedOptionalChunk(int, std::uint32_t, PlayGoOptionalChunk*); +int APS5_VABI scePlayGoGetOptionalChunk(int, std::uint32_t, PlayGoOptionalChunk*); int APS5_VABI scePlayGoSetToDoList(int, const PlayGoToDo*, std::uint32_t); int APS5_VABI scePlayGoGetToDoList(int, PlayGoToDo*, std::uint32_t, std::uint32_t*); int APS5_VABI scePlayGoGetLocus(int, const std::uint16_t*, std::uint32_t, std::int8_t*); int APS5_VABI scePlayGoGetChunkId(int, std::uint16_t*, std::uint32_t, std::uint32_t*); } -static void Require(bool value) { if (!value) std::abort(); } +static void Require(bool value, const std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "requirement failed at line %u\n", location.line()); + std::abort(); +} + +template +static void RequireMissingChunkMetadata(Operation&& operation) { + bool threw = false; + try { + operation(); + } catch (const std::runtime_error& error) { + 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("APS5_PLAYGO_UNLISTED_CHUNKS") != std::string_view::npos; + } + Require(threw); +} static int CheckPackageChunkTable(const char* root) { constexpr int badChunkId = static_cast(0x80B2000C); @@ -54,7 +79,16 @@ static int CheckScenarioWithoutChunkTable(const char* root) { const PlayGoToDo todo[2] = {{0, 3, 0}, {0xFFFF, 3, 0}}; Require(scePlayGoSetToDoList(handle, todo, 2) == 0); std::uint32_t chunkCount = 0; - Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1); + Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1000); + std::vector ids(chunkCount + 1, 0xFFFF); + Require(scePlayGoGetChunkId(handle, ids.data(), chunkCount, &chunkCount) == 0 && chunkCount == 1000); + for (std::uint32_t index = 0; index < chunkCount; ++index) Require(ids[index] == index); + Require(ids.back() == 0xFFFF); + std::vector listedLoci(chunkCount, 0); + Require(scePlayGoGetLocus(handle, ids.data(), chunkCount, listedLoci.data()) == 0); + for (const auto locus : listedLoci) Require(locus == 3); + std::uint16_t firstTwo[2] = {0xFFFF, 0xFFFF}; + Require(scePlayGoGetChunkId(handle, firstTwo, 2, &chunkCount) == 0 && chunkCount == 2 && firstTwo[0] == 0 && firstTwo[1] == 1); Require(scePlayGoClose(handle) == 0); return 0; } @@ -65,7 +99,6 @@ static int CheckNoPlayGoFile(const char* root) { constexpr int badPointer = static_cast(0x80B2000A); constexpr int badSize = static_cast(0x80B2000B); constexpr int badLocus = static_cast(0x80B20010); - constexpr int badChunkId = static_cast(0x80B2000C); PlayGoInitParams init{}; int handle = 0; @@ -78,6 +111,22 @@ static int CheckNoPlayGoFile(const char* root) { Require(scePlayGoGetLanguageMask(handle + 1, &mask) == badHandle); Require(scePlayGoGetLanguageMask(handle, nullptr) == badPointer); Require(scePlayGoGetLanguageMask(handle, &mask) == 0 && mask == ~0ull); + Require(scePlayGoSetLanguageMask(handle + 1, 0x5) == badHandle); + Require(scePlayGoGetLanguageMask(handle, &mask) == 0 && mask == ~0ull); + Require(scePlayGoSetLanguageMask(handle, 0x5) == 0); + Require(scePlayGoGetLanguageMask(handle, &mask) == 0 && mask == 0x5); + Require(scePlayGoSetLanguageMask(handle, ~0ull) == 0); + Require(scePlayGoGetLanguageMask(handle, &mask) == 0 && mask == ~0ull); + + for (const auto getOptionalChunk : {scePlayGoGetSupportedOptionalChunk, scePlayGoGetOptionalChunk}) { + PlayGoOptionalChunk option{}; + Require(getOptionalChunk(handle + 1, 0, &option) == badHandle); + Require(getOptionalChunk(handle, 0, nullptr) == badPointer); + Require(getOptionalChunk(handle, 0, &option) == 0 && option.bitmask == ~0ull); + Require(getOptionalChunk(handle, 3, &option) == static_cast(0x80B20024)); + Require(getOptionalChunk(handle, 0xFFFFFFFF, &option) == static_cast(0x80B20024)); + Require(getOptionalChunk(handle, 3, nullptr) == badPointer); + } PlayGoToDo todo[2] = {{0, 3, 0}, {0, 0, 0}}; Require(scePlayGoSetToDoList(handle + 1, todo, 2) == badHandle); @@ -87,19 +136,25 @@ static int CheckNoPlayGoFile(const char* root) { todo[1].locus = 1; Require(scePlayGoSetToDoList(handle, todo, 2) == badLocus); todo[1] = {1, 3, 0}; - Require(scePlayGoSetToDoList(handle, todo, 2) == badChunkId); + RequireMissingChunkMetadata([&] { scePlayGoSetToDoList(handle, todo, 2); }); todo[1] = {0xFFFF, 3, 0}; - Require(scePlayGoSetToDoList(handle, todo, 2) == badChunkId); + RequireMissingChunkMetadata([&] { scePlayGoSetToDoList(handle, todo, 2); }); std::uint32_t entries = 1; Require(scePlayGoGetToDoList(handle, todo, 2, &entries) == 0 && entries == 0); const std::uint16_t chunk = 0; std::int8_t locus = 0; Require(scePlayGoGetLocus(handle, &chunk, 0, &locus) == badSize); + Require(scePlayGoGetLocus(handle, &chunk, 1001, &locus) == badSize); + std::uint32_t invalidListCount = 42; + std::uint16_t invalidListOutput = 0; + Require(scePlayGoGetChunkId(handle, &invalidListOutput, 0, &invalidListCount) == badSize); + Require(scePlayGoGetChunkId(handle, &invalidListOutput, 1001, &invalidListCount) == badSize); + Require(invalidListCount == 42); Require(scePlayGoGetLocus(handle, &chunk, 1, &locus) == 0 && locus == 3); const std::uint16_t undeclared[2] = {1, 10}; std::int8_t loci[2] = {0, 0}; - Require(scePlayGoGetLocus(handle, undeclared, 2, loci) == badChunkId); + RequireMissingChunkMetadata([&] { scePlayGoGetLocus(handle, undeclared, 2, loci); }); const std::uint16_t onlyDeclared[2] = {0, 0}; Require(scePlayGoGetLocus(handle, onlyDeclared, 2, loci) == 0 && loci[0] == 3 && loci[1] == 3); std::uint32_t chunkCount = 0; @@ -127,6 +182,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); @@ -143,10 +244,10 @@ static void RunLayout(const char* self, const char* mode, std::initializer_list< Require(status == 0); } -static std::string ChunkTableFile() { +static std::string ChunkTableFile(const char* magic = "plgx", char chunks = 3) { std::string header(0x100, '\0'); - header.replace(0, 4, "plgx"); - header[0x0A] = 3; + header.replace(0, 4, magic); + header[0x0A] = chunks; return header; } @@ -154,6 +255,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"); @@ -161,11 +265,16 @@ 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], "--package-chunk-table", {{"playgo-chunk.dat", ChunkTableFile("plgo")}}); RunLayout(argv[0], "--scenario-with-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}, {"playgo-scenario.json", "{}"}}); RunLayout(argv[0], "--scenario-only", {{"playgo-scenario.json", "{}"}}); RunLayout(argv[0], "--ficm-only", {{"playgo-ficm.dat", "x"}}); RunLayout(argv[0], "--hash-table-only", {{"playgo-hash-table.dat", "x"}}); RunLayout(argv[0], "--invalid-chunk-table", {{"playgo-chunk.dat", "not a chunk table"}}); + RunLayout(argv[0], "--invalid-chunk-table", {{"playgo-chunk.dat", ChunkTableFile("plgo", 0)}}); return 0; } diff --git a/core/libs/tests/GuestPlayGoDialog.cpp b/core/libs/tests/GuestPlayGoDialog.cpp index 40a6a6710a..0a8291f96c 100644 --- a/core/libs/tests/GuestPlayGoDialog.cpp +++ b/core/libs/tests/GuestPlayGoDialog.cpp @@ -11,39 +11,52 @@ int APS5_VABI scePlayGoDialogOpen(const void* param); int APS5_VABI scePlayGoDialogClose(void); int APS5_VABI scePlayGoDialogUpdateStatus(void); int APS5_VABI scePlayGoDialogGetStatus(void); -int APS5_VABI scePlayGoDialogGetResult(void* result); +int APS5_VABI scePlayGoDialogGetResult(PlayGoDialogResult* result); } static void Require(bool value) { if (!value) std::abort(); } int main() { - constexpr std::int32_t notInitialized = static_cast(0x80ED0001); - constexpr std::int32_t alreadyInitialized = static_cast(0x80ED0002); - constexpr std::int32_t paramInvalid = static_cast(0x80ED0003); - constexpr std::int32_t invalidState = static_cast(0x80ED0005); + constexpr std::int32_t notInitialized = static_cast(0x80B80003); + constexpr std::int32_t alreadyInitialized = static_cast(0x80B80004); + constexpr std::int32_t paramInvalid = static_cast(0x80B8000D); + constexpr std::int32_t notFinished = static_cast(0x80B80005u); Require(scePlayGoDialogGetStatus() == 0); Require(scePlayGoDialogTerminate() == notInitialized); + Require(scePlayGoDialogClose() == notInitialized); Require(scePlayGoDialogOpen(nullptr) == paramInvalid); - Require(scePlayGoDialogGetResult(nullptr) == notInitialized); + Require(scePlayGoDialogGetResult(nullptr) == paramInvalid); const int openParam = 1; - Require(scePlayGoDialogOpen(&openParam) == invalidState); + Require(scePlayGoDialogOpen(&openParam) == notInitialized); + PlayGoDialogResult early{}; + Require(scePlayGoDialogGetResult(&early) == notInitialized); + early.reserved[31] = 1; + Require(scePlayGoDialogGetResult(&early) == static_cast(0x80B8000Au)); Require(scePlayGoDialogInitialize() == 0); Require(scePlayGoDialogGetStatus() == 1); Require(scePlayGoDialogInitialize() == alreadyInitialized); Require(scePlayGoDialogOpen(nullptr) == paramInvalid); Require(scePlayGoDialogUpdateStatus() == 1); + PlayGoDialogResult unfinishedResult{}; + Require(scePlayGoDialogGetResult(&unfinishedResult) == notFinished); + Require(scePlayGoDialogOpen(&openParam) == 0); Require(scePlayGoDialogGetStatus() == 3); Require(scePlayGoDialogUpdateStatus() == 3); PlayGoDialogResult result{}; Require(scePlayGoDialogGetResult(&result) == 0); - Require(result.result == 0); - Require(scePlayGoDialogClose() == 0); + Require(result.result == 0 && result.errorCode == 0); + result.errorCode = 7; + result.result = 9; + result.reserved[0] = 1; + Require(scePlayGoDialogGetResult(&result) == static_cast(0x80B8000Au)); + Require(result.errorCode == 7 && result.result == 9 && result.reserved[0] == 1); + Require(scePlayGoDialogClose() == static_cast(0x80B8000Bu)); Require(scePlayGoDialogTerminate() == 0); Require(scePlayGoDialogGetStatus() == 0); Require(scePlayGoDialogTerminate() == notInitialized); diff --git a/core/libs/tests/GuestPlayerReviewDialog.cpp b/core/libs/tests/GuestPlayerReviewDialog.cpp index 7093323415..45621694d4 100644 --- a/core/libs/tests/GuestPlayerReviewDialog.cpp +++ b/core/libs/tests/GuestPlayerReviewDialog.cpp @@ -1,8 +1,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include "prx/libSceUserService/UserService.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include extern "C" { +int APS5_VABI sceUserServiceInitialize(const UserServiceInitParam* param); +int APS5_VABI sceUserServiceTerminate(); int APS5_VABI scePlayerReviewDialogInitialize(void); int APS5_VABI scePlayerReviewDialogTerminate(void); int APS5_VABI scePlayerReviewDialogOpen(const void* param); @@ -27,31 +31,79 @@ void Require(bool value) { if (!value) std::abort(); } } int main() { - std::uint8_t param[64] = {}; - std::int32_t result[8] = {}; + Require(sceUserServiceInitialize(nullptr) == 0); + PlayerReviewDialogParam param{}; + param.base.size = sizeof(param.base); + param.base.magic = static_cast(reinterpret_cast(¶m)) - 1060003575u; + param.size = sizeof(param); + param.userId = USER_SERVICE_INITIAL_USER_ID; + std::int32_t result[4] = {-1, -1, -1, -1}; + Require(scePlayerReviewDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogOpen(nullptr) == static_cast(0x80B8000Du)); + Require(scePlayerReviewDialogGetResult(nullptr) == static_cast(0x80B8000Du)); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); - Require(scePlayerReviewDialogOpen(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogOpen(¶m) == 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); + Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(scePlayerReviewDialogOpen(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); - Require(scePlayerReviewDialogOpen(param) == 0); + param.base.size = 0; + Require(scePlayerReviewDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.base.size = sizeof(param.base); + param.base.magic = 0; + Require(scePlayerReviewDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.base.magic = static_cast(reinterpret_cast(¶m)) - 1060003575u; + param.size = 0; + Require(scePlayerReviewDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.size = sizeof(param); + param.reserved[63] = 1; + Require(scePlayerReviewDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.reserved[63] = 0; + param.userId = -1; + Require(scePlayerReviewDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.userId = USER_SERVICE_INITIAL_USER_ID; + param.mode = 2; + Require(scePlayerReviewDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.mode = 0; + param.userId = 42; + Require(scePlayerReviewDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.userId = USER_SERVICE_INITIAL_USER_ID; + Require(sceUserServiceTerminate() == 0); + Require(scePlayerReviewDialogOpen(¶m) == static_cast(0x80B8000Au)); + Require(sceUserServiceInitialize(nullptr) == 0); + Require(scePlayerReviewDialogOpen(¶m) == 0); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(scePlayerReviewDialogGetResult(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); - Require(scePlayerReviewDialogGetResult(result) == 0); - Require(result[0] == COMMON_DIALOG_RESULT_USER_CANCELED); - Require(scePlayerReviewDialogClose() == 0); + Require(scePlayerReviewDialogGetResult(result) == COMMON_DIALOG_RESULT_USER_CANCELED); + Require(result[0] == -1 && result[1] == -1 && result[3] == -1); + Require(result[2] == COMMON_DIALOG_RESULT_USER_CANCELED); + Require(scePlayerReviewDialogClose() == static_cast(0x80B8000Bu)); + Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); + Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(scePlayerReviewDialogTerminate() == 0); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(scePlayerReviewDialogInitialize() == 0); + Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); + Require(scePlayerReviewDialogClose() == static_cast(0x80B8000Bu)); + Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); + Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_INITIALIZED); + result[2] = -1; + Require(scePlayerReviewDialogGetResult(result) == COMMON_DIALOG_ERROR_NOT_FINISHED); + Require(result[0] == -1 && result[1] == -1 && result[3] == -1); + Require(result[2] == -1); Require(scePlayerReviewDialogTerminate() == 0); + Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_NONE); + Require(sceUserServiceTerminate() == 0); } diff --git a/core/libs/tests/GuestPlayerSelectionDialog.cpp b/core/libs/tests/GuestPlayerSelectionDialog.cpp new file mode 100644 index 0000000000..4d3e163d12 --- /dev/null +++ b/core/libs/tests/GuestPlayerSelectionDialog.cpp @@ -0,0 +1,112 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include "prx/libSceCommonDialog/CommonDialog.hpp" +#include "prx/libSceUserService/UserService.hpp" + +extern "C" { +int APS5_VABI sceUserServiceInitialize(const UserServiceInitParam* param); +int APS5_VABI sceUserServiceTerminate(); +int APS5_VABI scePlayerSelectionDialogInitialize(void); +int APS5_VABI scePlayerSelectionDialogOpen(const PlayerSelectionDialogParam* param); +int APS5_VABI scePlayerSelectionDialogUpdateStatus(void); +int APS5_VABI scePlayerSelectionDialogGetStatus(void); +int APS5_VABI scePlayerSelectionDialogGetResult(PlayerSelectionDialogResult* result); +int APS5_VABI scePlayerSelectionDialogParamInitialize(void* param); +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() { + Require(sceUserServiceInitialize(nullptr) == 0); + PlayerSelectionDialogParam param{}; + param.base.size = sizeof(param.base); + param.base.magic = static_cast(reinterpret_cast(¶m)) - 1060003575u; + param.size = sizeof(param); + param.userId = USER_SERVICE_INITIAL_USER_ID; + PlayerSelectionDialogResult result{-1, 99, nullptr, 77, 55}; + + Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized); + Require(scePlayerSelectionDialogOpen(¶m) == kErrNotInitialized); + Require(scePlayerSelectionDialogClose() == kErrNotInitialized); + Require(scePlayerSelectionDialogParamInitialize(nullptr) == kErrArgNull); + bool threw = false; + try { + scePlayerSelectionDialogParamInitialize(¶m); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + Require(param.size == sizeof(param)); + 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); + + param.size = 0; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.size = sizeof(param); + param.maxPlayers = 257; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.maxPlayers = 1; + param.mode = 4; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.mode = 0; + param.playerCount = 1; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); + PlayerSelectionDialogPlayer player{}; + param.players = &player; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); + player.accountId = 1; + player.option0 = 1; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.maxPlayers = 2; + player.option1 = 2; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); + player.option1 = 1; + player.mode = 3; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); + player.mode = 2; + player.reserved[63] = 1; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); + player.reserved[63] = 0; + param.reserved[63] = 1; + Require(scePlayerSelectionDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.reserved[63] = 0; + Require(scePlayerSelectionDialogOpen(¶m) == 0); + Require(scePlayerSelectionDialogGetStatus() == kStatusFinished); + Require(scePlayerSelectionDialogUpdateStatus() == kStatusFinished); + + Require(scePlayerSelectionDialogGetResult(nullptr) == kErrArgNull); + Require(scePlayerSelectionDialogGetResult(&result) == kResultUserCanceled); + Require(result.result == kResultUserCanceled); + Require(result.option == 99 && result.accountIds == nullptr && result.count == 77 && result.padding == 55); + + Require(scePlayerSelectionDialogClose() == static_cast(0x80B8000Bu)); + Require(scePlayerSelectionDialogTerminate() == 0); + Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized); + Require(scePlayerSelectionDialogGetStatus() == 0); + Require(scePlayerSelectionDialogClose() == kErrNotInitialized); + Require(sceUserServiceTerminate() == 0); +} diff --git a/core/libs/tests/GuestPngDec.cpp b/core/libs/tests/GuestPngDec.cpp index f9536a50a2..4ee4031ad2 100644 --- a/core/libs/tests/GuestPngDec.cpp +++ b/core/libs/tests/GuestPngDec.cpp @@ -1,6 +1,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "Decoder/Png.hpp" +#include #include #include #include @@ -51,6 +52,12 @@ static std::vector RgbPng() { return Decoder::Png::Encode(rgb, 3, 2, 3); } +static std::vector RgbPngWidth(std::uint32_t width) { + std::vector rgb(static_cast(width) * 3); + for (std::size_t i = 0; i < rgb.size(); ++i) rgb[i] = static_cast(i * 17 + 3); + return Decoder::Png::Encode(rgb, width, 1, 3); +} + static PngDecDecodeParam DecodeParam(const std::vector& png, std::vector& image) { PngDecDecodeParam param{}; param.png_mem_addr = png.data(); @@ -74,31 +81,34 @@ int main() { const PngDecCreateParam param{sizeof(PngDecCreateParam), 0, 1920}; const std::int32_t memorySize = scePngDecQueryMemorySize(¶m); - Require(memorySize > 0); - Require(scePngDecQueryMemorySize(nullptr) == invalidParam); + Require(memorySize == 69264); + Require(scePngDecQueryMemorySize(nullptr) == invalidAddr); const PngDecCreateParam badSize{sizeof(PngDecCreateParam) - 1, 0, 1920}; Require(scePngDecQueryMemorySize(&badSize) == invalidSize); const PngDecCreateParam badAttribute{sizeof(PngDecCreateParam), 2, 1920}; Require(scePngDecQueryMemorySize(&badAttribute) == invalidParam); const PngDecCreateParam zeroWidth{sizeof(PngDecCreateParam), 0, 0}; - Require(scePngDecQueryMemorySize(&zeroWidth) == invalidSize); + Require(scePngDecQueryMemorySize(&zeroWidth) == invalidParam); const PngDecCreateParam hugeWidth{sizeof(PngDecCreateParam), 0, 1000002}; - Require(scePngDecQueryMemorySize(&hugeWidth) == invalidSize); - const PngDecCreateParam maxWidth{sizeof(PngDecCreateParam), 1, 1000001}; - Require(scePngDecQueryMemorySize(&maxWidth) == memorySize); + Require(scePngDecQueryMemorySize(&hugeWidth) == invalidParam); + const PngDecCreateParam overWidth{sizeof(PngDecCreateParam), 1, 1000001}; + Require(scePngDecQueryMemorySize(&overWidth) == invalidParam); + const PngDecCreateParam maxWidth{sizeof(PngDecCreateParam), 1, 1000000}; + Require(scePngDecQueryMemorySize(&maxWidth) == 16054416); std::vector memory(static_cast(memorySize) + 1); unsigned char* unaligned = memory.data() + 1; void* handle = nullptr; - Require(scePngDecCreate(nullptr, unaligned, static_cast(memorySize), &handle) == invalidParam); + Require(scePngDecCreate(nullptr, unaligned, static_cast(memorySize), &handle) == invalidAddr); Require(scePngDecCreate(&badSize, unaligned, static_cast(memorySize), &handle) == invalidSize); + Require(scePngDecCreate(&overWidth, unaligned, static_cast(memorySize), &handle) == invalidParam); Require(scePngDecCreate(¶m, nullptr, static_cast(memorySize), &handle) == invalidAddr); Require(scePngDecCreate(¶m, unaligned, static_cast(memorySize), nullptr) == invalidAddr); - Require(scePngDecCreate(¶m, unaligned, static_cast(memorySize) - 1, &handle) == invalidWorkMemory); + Require(scePngDecCreate(¶m, unaligned, static_cast(memorySize) - 1, &handle) == invalidSize); Require(handle == nullptr); Require(scePngDecCreate(¶m, unaligned, static_cast(memorySize), &handle) == 0); - Require(reinterpret_cast(handle) % 8 == 0); - Require(static_cast(handle) >= unaligned && static_cast(handle) < unaligned + 8); + Require(reinterpret_cast(handle) % 16 == 0); + Require(static_cast(handle) >= unaligned && static_cast(handle) < unaligned + 16); const std::vector rgbPng = RgbPng(); PngDecImageInfo info{}; @@ -115,7 +125,7 @@ int main() { } const PngDecParseParam completeTransparency{PALETTE_TRNS, 73, 0}; Require(scePngDecParseHeader(&completeTransparency, &info) == 0 && info.image_flag == 2); - Require(scePngDecParseHeader(nullptr, &info) == invalidParam); + Require(scePngDecParseHeader(nullptr, &info) == invalidAddr); Require(scePngDecParseHeader(&parse, nullptr) == invalidAddr); const PngDecParseParam parseNull{nullptr, 16, 0}; Require(scePngDecParseHeader(&parseNull, &info) == invalidAddr); @@ -135,8 +145,27 @@ int main() { Require(image[pixel * 4 + 3] == 0x77); } + const PngDecCreateParam narrowParam{sizeof(PngDecCreateParam), 0, 9}; + std::vector narrowMemory(static_cast(memorySize)); + void* narrowHandle = nullptr; + Require(scePngDecCreate(&narrowParam, narrowMemory.data(), static_cast(memorySize), &narrowHandle) == 0); + const std::vector alignedWidthPng = RgbPngWidth(16); + std::vector alignedWidthImage(16 * 4, 0xCC); + PngDecDecodeParam narrowDecode = DecodeParam(alignedWidthPng, alignedWidthImage); + Require(scePngDecDecode(narrowHandle, &narrowDecode, &info) == (16 << 16 | 1)); + Require(info.image_width == 16 && info.image_height == 1); + + const std::vector oversizedWidthPng = RgbPngWidth(17); + std::vector oversizedWidthImage(17 * 4, 0xCC); + narrowDecode = DecodeParam(oversizedWidthPng, oversizedWidthImage); + info = {}; + Require(scePngDecDecode(narrowHandle, &narrowDecode, &info) == invalidWorkMemory); + Require(info.image_width == 17 && info.image_height == 1); + Require(std::all_of(oversizedWidthImage.begin(), oversizedWidthImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + Require(scePngDecDelete(narrowHandle) == 0); + constexpr std::uint32_t pitch = 3 * 4 + 8; - std::vector padded(pitch + 3 * 4, 0xEE); + std::vector padded(pitch * 2, 0xEE); decode = DecodeParam(rgbPng, padded); decode.pixel_format = 1; decode.image_pitch = pitch; @@ -151,6 +180,23 @@ int main() { } for (std::size_t x = 3 * 4; x < pitch; ++x) Require(padded[x] == 0xEE); + std::vector invalidPitchImage(3 * 2 * 4 + 8, 0xCC); + decode = DecodeParam(rgbPng, invalidPitchImage); + decode.image_pitch = 3 * 4 + 2; + Require(scePngDecDecode(handle, &decode, &info) == invalidParam); + Require(std::all_of(invalidPitchImage.begin(), invalidPitchImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + + decode = DecodeParam(rgbPng, invalidPitchImage); + decode.image_pitch = pitch; + decode.image_mem_size = pitch + 3 * 4; + Require(scePngDecDecode(handle, &decode, &info) == invalidParam); + Require(std::all_of(invalidPitchImage.begin(), invalidPitchImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + + std::vector fullPitchImage(pitch * 2, 0xCC); + decode = DecodeParam(rgbPng, fullPitchImage); + decode.image_pitch = pitch; + Require(scePngDecDecode(handle, &decode, &info) == (3 << 16 | 2)); + const std::vector palettePng(PALETTE_TRNS, PALETTE_TRNS + sizeof(PALETTE_TRNS)); std::vector paletteImage(2 * 2 * 4); decode = DecodeParam(palettePng, paletteImage); @@ -167,18 +213,32 @@ int main() { decode = DecodeParam(rgbPng, image); Require(scePngDecDecode(nullptr, &decode, &info) == invalidHandle); - Require(scePngDecDecode(handle, nullptr, &info) == invalidParam); + Require(scePngDecDecode(handle, nullptr, &info) == invalidAddr); decode.png_mem_addr = nullptr; Require(scePngDecDecode(handle, &decode, &info) == invalidAddr); decode = DecodeParam(rgbPng, image); decode.image_mem_addr = nullptr; Require(scePngDecDecode(handle, &decode, &info) == invalidAddr); + std::vector misalignedImage(3 * 2 * 4 + 1, 0xCC); + decode = DecodeParam(rgbPng, misalignedImage); + decode.image_mem_addr = misalignedImage.data() + 1; + decode.image_mem_size = static_cast(misalignedImage.size() - 1); + Require(scePngDecDecode(handle, &decode, &info) == invalidAddr); + Require(std::all_of(misalignedImage.begin(), misalignedImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); decode = DecodeParam(rgbPng, image); decode.png_mem_size = 0; Require(scePngDecDecode(handle, &decode, &info) == invalidSize); decode = DecodeParam(rgbPng, image); decode.pixel_format = 2; Require(scePngDecDecode(handle, &decode, &info) == invalidParam); + std::vector invalidAlphaImage(3 * 2 * 4, 0xCC); + decode = DecodeParam(rgbPng, invalidAlphaImage); + decode.alpha_value = 0x100; + Require(scePngDecDecode(handle, &decode, &info) == invalidParam); + Require(std::all_of(invalidAlphaImage.begin(), invalidAlphaImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + decode.alpha_value = 0xFFFF; + Require(scePngDecDecode(handle, &decode, &info) == invalidParam); + Require(std::all_of(invalidAlphaImage.begin(), invalidAlphaImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); decode = DecodeParam(rgbPng, image); decode.image_pitch = 3 * 4 - 1; Require(scePngDecDecode(handle, &decode, &info) == invalidParam); @@ -193,9 +253,11 @@ int main() { Require(scePngDecDecode(handle, &decode, &info) == decodeError); void* deepHandle = nullptr; - std::vector deepMemory(static_cast(memorySize)); const PngDecCreateParam deepParam{sizeof(PngDecCreateParam), 1, 1920}; - Require(scePngDecCreate(&deepParam, deepMemory.data(), static_cast(memorySize), &deepHandle) == 0); + const auto deepSize = scePngDecQueryMemorySize(&deepParam); + Require(deepSize == 85136); + std::vector deepMemory(static_cast(deepSize)); + Require(scePngDecCreate(&deepParam, deepMemory.data(), static_cast(deepSize), &deepHandle) == 0); decode = DecodeParam(gray16Png, gray16Image); Require(ThrowsRuntimeError([&] { scePngDecDecode(deepHandle, &decode, &info); })); decode = DecodeParam(rgbPng, image); diff --git a/core/libs/tests/GuestPngEnc.cpp b/core/libs/tests/GuestPngEnc.cpp index 7535dae853..d8227b3bab 100644 --- a/core/libs/tests/GuestPngEnc.cpp +++ b/core/libs/tests/GuestPngEnc.cpp @@ -26,28 +26,32 @@ int main() { constexpr std::uint32_t height = 3; constexpr std::uint32_t pitch = 24; - PngEncCreateParam create{sizeof(PngEncCreateParam), 0, 16, 5}; + PngEncCreateParam create{sizeof(PngEncCreateParam), 0, 16, 4}; const int memorySize = scePngEncQueryMemorySize(&create); - Require(memorySize > 0); + Require(memorySize == 286192); Require(scePngEncQueryMemorySize(nullptr) == invalidAddr); PngEncCreateParam bad = create; bad.attribute = 1; Require(scePngEncQueryMemorySize(&bad) == invalidParam); bad = create; - bad.max_filter_number = 6; + bad.max_filter_number = 5; Require(scePngEncQueryMemorySize(&bad) == invalidParam); bad = create; bad.max_image_width = 0; - Require(scePngEncQueryMemorySize(&bad) == invalidSize); + Require(scePngEncQueryMemorySize(&bad) == invalidParam); bad.max_image_width = 1000001; - Require(scePngEncQueryMemorySize(&bad) == invalidSize); + Require(scePngEncQueryMemorySize(&bad) == invalidParam); + bad = create; + bad.this_size = 0; + Require(scePngEncQueryMemorySize(&bad) == invalidSize); std::vector memory((memorySize + 7) / 8); void* handle = nullptr; Require(scePngEncCreate(&create, nullptr, memorySize, &handle) == invalidAddr); Require(scePngEncCreate(&create, memory.data(), memorySize - 1, &handle) == invalidSize); - Require(scePngEncCreate(&create, memory.data(), memorySize, &handle) == 0 && handle == memory.data()); + Require(scePngEncCreate(&create, memory.data(), memorySize, &handle) == 0 && reinterpret_cast(handle) % 16 == 0); + Require(scePngEncCreate(&create, memory.data(), 0, nullptr) == invalidSize); std::vector image(pitch * height, 0xEE); for (std::uint32_t y = 0; y < height; ++y) { for (std::uint32_t x = 0; x < width; ++x) { @@ -68,7 +72,8 @@ int main() { encode.color_space = colorSpace; PngEncOutputInfo info{}; const int size = scePngEncEncode(handle, &encode, &info); - Require(size > 0 && info.data_size == static_cast(size) && info.processed_height == height); + Require(size > 1 && static_cast(size) < png.size() + && info.data_size == static_cast(size) && info.processed_height == height); const auto header = Decoder::Png::ParseHeader(std::span(png.data(), size)); Require(header.has_value()); Require(header->colorType == (colorSpace == 19 ? Decoder::Png::ColorType::Rgba : Decoder::Png::ColorType::Rgb)); @@ -83,6 +88,22 @@ int main() { Require(result[3] == (colorSpace == 19 ? source[3] : 0xFF)); } } + const std::vector expected(png.begin(), png.begin() + size); + for (const int capacity : {size - 1, size}) { + std::fill(png.begin(), png.end(), 0xA5); + encode.png_mem_size = static_cast(capacity); + info = {1, 1}; + const int result = scePngEncEncode(handle, &encode, &info); + if (capacity < size) { + Require(result == dataOverflow && info.data_size == 0 && info.processed_height == 0); + Require(std::all_of(png.begin(), png.end(), [](auto byte) { return byte == 0xA5; })); + } else { + Require(result == size && info.data_size == static_cast(size) && info.processed_height == height); + Require(std::equal(expected.begin(), expected.end(), png.begin())); + Require(std::all_of(png.begin() + size, png.end(), [](auto byte) { return byte == 0xA5; })); + } + } + encode.png_mem_size = static_cast(png.size()); } } @@ -109,8 +130,9 @@ int main() { {0, FILTER_NONE}, {1, FILTER_SUB}, {2, FILTER_UP}, {4, FILTER_AVERAGE}, {8, FILTER_PAETH}, {3, FILTER_SUB | FILTER_UP}, {6, FILTER_UP | FILTER_AVERAGE}, {9, FILTER_SUB | FILTER_PAETH}, {14, FILTER_UP | FILTER_AVERAGE | FILTER_PAETH}, {15, FILTER_ALL}}; - for (const auto& [filterType, filters] : filterSets) Require(encodeWith(filterType) == Encode(rows, 16, 6, 4, {6, filters})); - Require(encodeWith(3) != encodeWith(1) && encodeWith(3) != encodeWith(2) && encodeWith(15) != encodeWith(14)); + for (const auto& [filterType, filters] : filterSets) Require(encodeWith(filterType) == Encode(rows, 16, 6, 4, {6, static_cast(filters | FILTER_NONE)})); + const std::vector unfiltered = Encode(rows, 16, 6, 4, {6, FILTER_NONE}); + Require(encodeWith(0) == unfiltered); encode.png_mem_size = 20; PngEncOutputInfo info{1, 1}; @@ -118,7 +140,7 @@ int main() { encode.png_mem_size = static_cast(png.size()); PngEncEncodeParam invalid = encode; - Require(scePngEncEncode(handle, nullptr, nullptr) == invalidParam); + Require(scePngEncEncode(handle, nullptr, nullptr) == invalidAddr); invalid.image_mem_addr = nullptr; Require(scePngEncEncode(handle, &invalid, nullptr) == invalidAddr); for (auto change : {+[](PngEncEncodeParam& p) { p.pixel_format = 2; }, +[](PngEncEncodeParam& p) { p.color_space = 4; }, @@ -129,15 +151,31 @@ int main() { Require(scePngEncEncode(handle, &invalid, nullptr) == invalidParam); } for (auto change : {+[](PngEncEncodeParam& p) { p.image_width = 0; }, +[](PngEncEncodeParam& p) { p.image_width = 17; }, - +[](PngEncEncodeParam& p) { p.image_pitch = 19; }, +[](PngEncEncodeParam& p) { p.image_mem_size = 67; }, - +[](PngEncEncodeParam& p) { p.png_mem_size = 0; }}) { + +[](PngEncEncodeParam& p) { p.image_pitch = 19; }, +[](PngEncEncodeParam& p) { p.image_mem_size = 67; }}) { invalid = encode; change(invalid); - Require(scePngEncEncode(handle, &invalid, nullptr) == invalidSize); + Require(scePngEncEncode(handle, &invalid, nullptr) == invalidParam); } invalid = encode; invalid.image_mem_size = pitch * (height - 1) + width * 4; - Require(scePngEncEncode(handle, &invalid, nullptr) > 0); + Require(scePngEncEncode(handle, &invalid, nullptr) == invalidParam); + invalid = encode; + invalid.png_mem_size = 0; + Require(scePngEncEncode(handle, &invalid, nullptr) == invalidSize); + invalid = encode; + invalid.image_mem_addr = image.data() + 1; + Require(scePngEncEncode(handle, &invalid, nullptr) == invalidAddr); + PngEncCreateParam oneFilter = create; + oneFilter.max_filter_number = 1; + void* oneFilterHandle = nullptr; + std::vector oneFilterMemory((scePngEncQueryMemorySize(&oneFilter) + 7) / 8); + Require(scePngEncCreate(&oneFilter, oneFilterMemory.data(), static_cast(oneFilterMemory.size() * 8), &oneFilterHandle) == 0); + invalid = encode; + invalid.filter_type = 3; + Require(scePngEncEncode(oneFilterHandle, &invalid, nullptr) == invalidParam); + invalid.filter_type = 1; + Require(scePngEncEncode(oneFilterHandle, &invalid, nullptr) > 0); + Require(scePngEncDelete(oneFilterHandle) == 0); Require(scePngEncDelete(handle) == 0); Require(scePngEncDelete(handle) == invalidHandle); diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index 1738f11dd8..d81f0760f6 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; @@ -30,9 +31,14 @@ int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI shutdown_nid_postfix(int, 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 +61,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 +97,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}; @@ -176,8 +213,41 @@ static void PollsUrgentData() { Require(close_nid_postfix(client) == 0); Require(close_nid_postfix(listener) == 0); } +static void PollsAStreamWhosePeerClosed() { + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0 && listen_nid_postfix(listener, 4) == 0); + const int client = socket_nid_postfix(2, 1, 0); + Require(client >= 0 && connect_nid_postfix(client, address.data(), address.size()) == 0); + const int server = accept_nid_postfix(listener, nullptr, nullptr); + Require(server >= 0); + + Require(shutdown_nid_postfix(client, 1) == 0); + PollDescriptor wait{server, In, 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == In); + wait = {server, static_cast(In | Out), 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == (In | Out)); + wait = {server, ReadNormal, 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == ReadNormal); + wait = {server, Out, 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == Out); + char byte; + Require(recv_nid_postfix(server, &byte, 1, 0) == 0); + + Require(shutdown_nid_postfix(server, 1) == 0); + wait = {server, In, 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == (In | Hup)); + wait = {client, In, 0}; + Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == (In | Hup)); + for (const int descriptor : {client, server, listener}) Require(close_nid_postfix(descriptor) == 0); +} + int main() { PollsUrgentData(); + PollsAStreamWhosePeerClosed(); const int listener = socket_nid_postfix(2, 1, 0); Require(listener >= 0 && listener < 1024); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; @@ -217,17 +287,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..3d44da1f08 --- /dev/null +++ b/core/libs/tests/GuestPositionedClose.cpp @@ -0,0 +1,73 @@ +#include "prx/libc/include/PreciseWait.hpp" +#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(); + PreciseSleepUs(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/GuestPosixFilesystem.cpp b/core/libs/tests/GuestPosixFilesystem.cpp index 0e3326d3d0..fbe8e7566e 100644 --- a/core/libs/tests/GuestPosixFilesystem.cpp +++ b/core/libs/tests/GuestPosixFilesystem.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/General.hpp" +#include "SceTypes.hpp" #include #include #include @@ -27,6 +28,20 @@ int APS5_VABI socket_nid_postfix(int, int, int); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::size_t); std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::size_t); std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +int APS5_VABI access_nid_postfix(const char*, int); +int APS5_VABI __wrap_access(const char*, int); +int APS5_VABI clock_gettime_nid_postfix(int, KernelTimespec*); +int APS5_VABI __wrap_clock_gettime(int, KernelTimespec*); +char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); +char* APS5_VABI __wrap_getcwd(char*, std::size_t); +char* APS5_VABI getenv_nid_postfix(const char*); +char* APS5_VABI __wrap_getenv(const char*); +int APS5_VABI setenv_nid_postfix(const char*, const char*, int); +int APS5_VABI lstat_nid_postfix(const char*, FileStat*); +int APS5_VABI __wrap_lstat(const char*, FileStat*); +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +void* APS5_VABI __wrap_mmap(void*, std::size_t, int, int, int, std::int64_t); +int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; } static void Require(bool condition, std::source_location location = std::source_location::current()) { @@ -134,6 +149,52 @@ int main() { Require(isatty_nid_postfix(slave) == 1 && *__error_nid_postfix() == 123); Require(::close(slave) == 0 && ::close(terminal) == 0); #endif + Require(setenv_nid_postfix("WEBKIT_WRAP_SET", "set", 1) == 0); + char* const variable = getenv_nid_postfix("WEBKIT_WRAP_SET"); + Require(variable != nullptr && std::strcmp(variable, "set") == 0 && __wrap_getenv("WEBKIT_WRAP_SET") == variable); + Require(getenv_nid_postfix("WEBKIT_WRAP_UNSET") == nullptr && __wrap_getenv("WEBKIT_WRAP_UNSET") == nullptr); + Require(access_nid_postfix(pattern.c_str(), 0) == 0 && __wrap_access(pattern.c_str(), 0) == 0); + Require(access_nid_postfix("/anyps5-posix-test/absent", 0) == -1); + const int accessError = *__error_nid_postfix(); + *__error_nid_postfix() = 0; + Require(__wrap_access("/anyps5-posix-test/absent", 0) == -1 && *__error_nid_postfix() == accessError && accessError == 2); + std::array expectedDirectory{}; + std::array wrappedDirectory{}; + Require(getcwd_nid_postfix(expectedDirectory.data(), expectedDirectory.size()) == expectedDirectory.data()); + Require(__wrap_getcwd(wrappedDirectory.data(), wrappedDirectory.size()) == wrappedDirectory.data()); + Require(std::strcmp(expectedDirectory.data(), wrappedDirectory.data()) == 0); + Require(getcwd_nid_postfix(wrappedDirectory.data(), 1) == nullptr); + const int directoryError = *__error_nid_postfix(); + *__error_nid_postfix() = 0; + Require(__wrap_getcwd(wrappedDirectory.data(), 1) == nullptr && *__error_nid_postfix() == directoryError && directoryError == 34); + FileStat expectedStatus{}; + FileStat wrappedStatus{}; + Require(lstat_nid_postfix(pattern.c_str(), &expectedStatus) == 0 && __wrap_lstat(pattern.c_str(), &wrappedStatus) == 0); + Require((wrappedStatus.st_mode & 0170000) == 0100000 && wrappedStatus.st_size == static_cast(sizeof(payload))); + Require(std::memcmp(&expectedStatus, &wrappedStatus, sizeof(FileStat)) == 0); + const auto notAfter = [](const KernelTimespec& earlier, const KernelTimespec& later) { + return earlier.tv_sec < later.tv_sec || (earlier.tv_sec == later.tv_sec && earlier.tv_nsec <= later.tv_nsec); + }; + KernelTimespec before{}; + KernelTimespec during{}; + KernelTimespec after{}; + Require(clock_gettime_nid_postfix(4, &before) == 0 && __wrap_clock_gettime(4, &during) == 0 && clock_gettime_nid_postfix(4, &after) == 0); + Require(notAfter(before, during) && notAfter(during, after)); + Require(clock_gettime_nid_postfix(4, nullptr) == -1 && *__error_nid_postfix() == 14); + *__error_nid_postfix() = 0; + Require(__wrap_clock_gettime(4, nullptr) == -1 && *__error_nid_postfix() == 14); + constexpr std::size_t page = 0x4000; + void* const failed = reinterpret_cast(static_cast(-1)); + void* const mapped = __wrap_mmap(nullptr, page, 3, 0x1002, -1, 0); + Require(mapped != failed); + auto* const bytes = static_cast(mapped); + bytes[0] = 0x5a; + bytes[page - 1] = 0xa5; + Require(bytes[0] == 0x5a && bytes[page - 1] == 0xa5); + Require(munmap_nid_postfix(mapped, page) == 0); + Require(mmap_nid_postfix(nullptr, page, 8, 0x1002, -1, 0) == failed && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(__wrap_mmap(nullptr, page, 8, 0x1002, -1, 0) == failed && *__error_nid_postfix() == 22); RemovePathAlias_nid_no_patch("/anyps5-posix-test"); Require(std::filesystem::remove_all(root) > 0); } diff --git a/core/libs/tests/GuestPosixScalarIo.cpp b/core/libs/tests/GuestPosixScalarIo.cpp index 177845674a..ec96da483a 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(0x80020002u)); + Require(sceKernelRename(nullptr, nullptr) == static_cast(0x8002000eu)); Require(close_nid_postfix(descriptor) == 0); const int readOnly = open_nid_postfix(fileName.c_str(), SCE_KERNEL_O_RDONLY, 0); Require(readOnly >= 0); @@ -155,6 +191,15 @@ int main() { Require(_read_nid_postfix(descriptors[0], pipeBuffer, sizeof(pipeBuffer)) == sizeof(pipeBuffer)); Require(std::memcmp(pipeBuffer, pipePayload, sizeof(pipePayload)) == 0); Require(close_nid_postfix(descriptors[0]) == 0); + int capacityDescriptors[2] = {-1, -1}; + Require(pipe_nid_postfix(capacityDescriptors) == 0); + std::string largePayload(8192, 'x'); + Require(write_nid_postfix(capacityDescriptors[1], largePayload.data(), largePayload.size()) == static_cast(largePayload.size())); + Require(close_nid_postfix(capacityDescriptors[1]) == 0); + std::string largeBuffer(8192, '\0'); + Require(_read_nid_postfix(capacityDescriptors[0], largeBuffer.data(), largeBuffer.size()) == static_cast(largeBuffer.size())); + Require(largeBuffer == largePayload); + Require(close_nid_postfix(capacityDescriptors[0]) == 0); #ifndef _WIN32 int nonBlockingPipe[2] = {-1, -1}; Require(pipe_nid_postfix(nonBlockingPipe) == 0); diff --git a/core/libs/tests/GuestPosixSpawn.cpp b/core/libs/tests/GuestPosixSpawn.cpp index 8fe49f711d..5b714f2fb0 100644 --- a/core/libs/tests/GuestPosixSpawn.cpp +++ b/core/libs/tests/GuestPosixSpawn.cpp @@ -1,9 +1,12 @@ #include "prx/libSceLibcInternal/include/Spawn.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include #include +#include +#include #include #include @@ -12,6 +15,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); @@ -19,6 +32,9 @@ int APS5_VABI posix_spawn_file_actions_adddup2_nid_postfix(PosixSpawnFileActions int APS5_VABI posix_spawn_file_actions_addclose_nid_postfix(PosixSpawnFileActions**, int); int APS5_VABI posix_spawn_nid_postfix(std::int32_t*, const char*, PosixSpawnFileActions* const*, PosixSpawnAttributes* const*, char* const*, char* const*); int APS5_VABI posix_spawnp_nid_postfix(std::int32_t*, const char*, PosixSpawnFileActions* const*, PosixSpawnAttributes* const*, char* const*, char* const*); +int APS5_VABI setenv_nid_postfix(const char*, const char*, int); +int APS5_VABI unsetenv_nid_postfix(const char*); +int APS5_VABI chdir_nid_postfix(const char*); } static void Require(bool condition, int line) { @@ -29,6 +45,7 @@ static void Require(bool condition, int line) { } #define Check(value) Require((value), __LINE__) +constexpr int NoSuchFile = 2; constexpr int BadDescriptor = 9; template @@ -67,7 +84,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); })); @@ -115,19 +155,64 @@ static void CheckFileActions() { } static void CheckSpawn() { + const auto name = "anyps5-spawn-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const auto directory = std::filesystem::current_path() / name; + Check(std::filesystem::create_directories(directory / "empty")); + { std::ofstream file(directory / "tool.elf"); file << "tool"; } + const std::string guest = "/" + name; + const std::string existing = guest + "/tool.elf"; + const std::string missing = guest + "/missing.elf"; + const std::string bothDirectories = guest + "/empty:" + guest; + const std::string emptyOnly = guest + "/empty"; + PosixSpawnAttributes* attributes = nullptr; PosixSpawnFileActions* actions = nullptr; Check(posix_spawnattr_init_nid_postfix(&attributes) == 0 && posix_spawn_file_actions_init_nid_postfix(&actions) == 0); - char program[] = "/app0/tool.elf"; + Check(posix_spawn_file_actions_addclose_nid_postfix(&actions, 3) == 0); + char program[] = "tool.elf"; char* arguments[] = {program, nullptr}; char* environment[] = {nullptr}; std::int32_t processId = 77; - Check(ReportsNotImplemented([&] { posix_spawn_nid_postfix(&processId, program, &actions, &attributes, arguments, environment); }, + + Check(ReportsNotImplemented([&] { posix_spawn_nid_postfix(&processId, existing.c_str(), &actions, &attributes, arguments, environment); }, "posix_spawn: process creation not implemented")); + Check(posix_spawn_nid_postfix(&processId, missing.c_str(), &actions, &attributes, arguments, environment) == NoSuchFile); + Check(posix_spawn_nid_postfix(&processId, "", nullptr, nullptr, arguments, nullptr) == NoSuchFile); + Check(posix_spawn_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr) == NoSuchFile); + + Check(setenv_nid_postfix("PATH", bothDirectories.c_str(), 1) == 0); + Check(ReportsNotImplemented([&] { posix_spawnp_nid_postfix(&processId, "tool.elf", &actions, &attributes, arguments, nullptr); }, + "posix_spawnp: process creation not implemented")); + Check(posix_spawnp_nid_postfix(&processId, "missing.elf", &actions, &attributes, arguments, nullptr) == NoSuchFile); + Check(posix_spawnp_nid_postfix(&processId, "", nullptr, nullptr, arguments, nullptr) == NoSuchFile); + Check(setenv_nid_postfix("PATH", emptyOnly.c_str(), 1) == 0); + Check(posix_spawnp_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr) == NoSuchFile); + Check(ReportsNotImplemented([&] { posix_spawnp_nid_postfix(&processId, existing.c_str(), nullptr, nullptr, arguments, nullptr); }, + "posix_spawnp: process creation not implemented")); + Check(posix_spawnp_nid_postfix(&processId, missing.c_str(), nullptr, nullptr, arguments, nullptr) == NoSuchFile); + + Check(setenv_nid_postfix("PATH", (emptyOnly + ":").c_str(), 1) == 0); + Check(posix_spawnp_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr) == NoSuchFile); + Check(chdir_nid_postfix(guest.c_str()) == 0); Check(ReportsNotImplemented([&] { posix_spawnp_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr); }, "posix_spawnp: process creation not implemented")); + Check(ReportsNotImplemented([&] { posix_spawn_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr); }, + "posix_spawn: process creation not implemented")); + Check(chdir_nid_postfix("/") == 0); + + Check(unsetenv_nid_postfix("PATH") == 0); + Check(posix_spawnp_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr) == NoSuchFile); Check(processId == 77); + + PosixSpawnAttributes* uninitializedAttributes = nullptr; + PosixSpawnFileActions* uninitializedActions = nullptr; + Check(ThrowsInvalidArgument([&] { posix_spawn_nid_postfix(&processId, missing.c_str(), &uninitializedActions, nullptr, arguments, nullptr); })); + Check(ThrowsInvalidArgument([&] { posix_spawnp_nid_postfix(&processId, "missing.elf", nullptr, &uninitializedAttributes, arguments, nullptr); })); + Check(ThrowsInvalidArgument([&] { posix_spawn_nid_postfix(&processId, nullptr, nullptr, nullptr, arguments, nullptr); })); + Check(ThrowsInvalidArgument([&] { posix_spawnp_nid_postfix(&processId, nullptr, nullptr, nullptr, arguments, nullptr); })); + Check(posix_spawn_file_actions_destroy_nid_postfix(&actions) == 0 && posix_spawnattr_destroy_nid_postfix(&attributes) == 0); + std::filesystem::remove_all(directory); } int main() { diff --git a/core/libs/tests/GuestProcessTime.cpp b/core/libs/tests/GuestProcessTime.cpp index 145afbc29a..cfff04c3bd 100644 --- a/core/libs/tests/GuestProcessTime.cpp +++ b/core/libs/tests/GuestProcessTime.cpp @@ -24,8 +24,12 @@ int main(int argc, char** argv) { while (std::chrono::steady_clock::now() < slept) std::this_thread::sleep_until(slept); if (argc > 1 && std::strcmp(argv[1], "counter") == 0) { const std::uint64_t counter = sceKernelGetProcessTimeCounter(); - Require(counter < minuteMicros * 1000, "first process time counter wrapped"); - Require(counter >= minimumElapsedMicros * 1000, "first process time counter does not include the time before the first call"); + const std::uint64_t frequency = sceKernelGetProcessTimeCounterFrequency(); + Require(frequency == sceKernelGetTscFrequency(), "process time counter frequency differs from the TSC frequency"); + const std::uint64_t counterMicros = counter / frequency * 1000000 + counter % frequency * 1000000 / frequency; + Require(counterMicros < minuteMicros, "first process time counter wrapped"); + Require(counterMicros >= minimumElapsedMicros, "first process time counter does not include the time before the first call"); + Require(sceKernelGetProcessTimeCounter() >= counter, "process time counter went backwards"); return 0; } const std::uint64_t first = sceKernelGetProcessTime(); diff --git a/core/libs/tests/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/GuestProtectionReport.cpp b/core/libs/tests/GuestProtectionReport.cpp new file mode 100644 index 0000000000..87a3d29b52 --- /dev/null +++ b/core/libs/tests/GuestProtectionReport.cpp @@ -0,0 +1,84 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI mprotect_nid_postfix(void*, std::size_t, int) noexcept; +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 sceKernelMprotect(const void*, std::size_t, int); +int APS5_VABI sceKernelMunmap(std::uint64_t, std::size_t); +int APS5_VABI sceKernelVirtualQuery(const void*, int, VirtualQueryInfo*, std::uint64_t); +int APS5_VABI sceKernelQueryMemoryProtection(void*, void**, void**, int*); +} + +namespace { + +constexpr std::size_t Page = 0x4000; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s\n", what); + std::abort(); +} + +void RequireReported(void* address, int expected, const char* what) { + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(address, 0, &info, sizeof(info)) == 0, "virtual query of a mapped page failed"); + void* start = nullptr; + void* end = nullptr; + int protection = -1; + Require(sceKernelQueryMemoryProtection(address, &start, &end, &protection) == 0, "protection query of a mapped page failed"); + if (info.protection == expected && protection == expected) return; + std::fprintf(stderr, "%s: reported 0x%x (virtual query) and 0x%x (protection query), expected 0x%x\n", what, info.protection, protection, expected); + std::abort(); +} + +void ProtectedReports() { + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, Page, 0x2, 0) == 0 && mapped != nullptr, "write-only flexible map failed"); + RequireReported(mapped, 0x3, "a write-only flexible mapping"); + struct Case { + int requested; + int reported; + }; + constexpr Case Cases[] = { + {0x1, 0x1}, {0x2, 0x3}, {0x3, 0x3}, {0x10, 0x10}, {0x12, 0x13}, {0x20, 0x20}, {0x22, 0x23}, {0x30, 0x30}, {0x40, 0x40}, {0x80, 0xc0}, {0x82, 0xc3}, {0x100, 0x100}, {0x200, 0x300}, + {0x202, 0x303}, {0x220, 0x320}, {0x2a2, 0x3e3}, {0xf2, 0xf3}, {0x3f2, 0x3f3}, {static_cast(0xfffffff2u), 0x3f3}, + }; + for (const auto& item : Cases) { + Require(sceKernelMprotect(mapped, Page, item.requested) == 0, "sceKernelMprotect failed"); + char what[64]; + std::snprintf(what, sizeof(what), "sceKernelMprotect 0x%x", static_cast(item.requested)); + RequireReported(mapped, item.reported, what); + } + Require(mprotect_nid_postfix(mapped, Page, 0x2) == 0, "write-only mprotect failed"); + RequireReported(mapped, 0x3, "mprotect with PROT_WRITE"); + Require(sceKernelMunmap(reinterpret_cast(mapped), Page) == 0, "unmap of the flexible mapping failed"); + + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, Page, 0, 0, &physical) == 0, "direct memory allocation failed"); + void* direct = nullptr; + Require(sceKernelMapDirectMemory(&direct, Page, 0xf2, 0, physical, 0) == 0 && direct != nullptr, "direct map with 0xf2 failed"); + RequireReported(direct, 0xf3, "a direct mapping with 0xf2"); + Require(sceKernelMprotect(direct, Page, static_cast(0xfffffff2u)) == 0, "sceKernelMprotect of the direct mapping failed"); + RequireReported(direct, 0x3f3, "a direct mapping protected with 0xfffffff2"); + Require(sceKernelMunmap(reinterpret_cast(direct), Page) == 0, "unmap of the direct mapping failed"); +} + +} + +int main() { + try { + ProtectedReports(); + } catch (const std::exception& error) { + std::fprintf(stderr, "unexpected exception: %s\n", error.what()); + return 1; + } + std::puts("reported protections include the read bit of every write bit"); + return 0; +} diff --git a/core/libs/tests/GuestPsml.cpp b/core/libs/tests/GuestPsml.cpp index d0aac1d174..882a4b5d04 100644 --- a/core/libs/tests/GuestPsml.cpp +++ b/core/libs/tests/GuestPsml.cpp @@ -2,11 +2,31 @@ #include #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); +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacketSizeInDwords(void* context, std::uint32_t* packetSizeInDwords); +std::int32_t APS5_VABI scePsmlMfsrSelectConfig(void* config); +std::int32_t APS5_VABI scePsmlMfsrGetMipmapBias(void* context, float* bias, std::uint32_t dimension); +std::int32_t APS5_VABI scePsmlMfsrRequestCapture(void* context); +std::int32_t APS5_VABI scePsmlMfsrIsCaptureInProgress(void* context); } namespace { @@ -14,12 +34,239 @@ namespace { void Require(bool value) { if (!value) std::abort(); } constexpr std::int32_t kErrNotInitialized = static_cast(0x8A810001); +constexpr std::int32_t kErrInvalidArgument = static_cast(0x8A810005); +constexpr std::int32_t kErrNullOutput = static_cast(0x8A810009); struct CommandBuffer { std::uint32_t* cursor; 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)); + } + + void SetInputDimensions(std::uint16_t width, std::uint16_t height) { + std::memcpy(bytes.data() + 0x18, &width, sizeof(width)); + std::memcpy(bytes.data() + 0x1a, &height, sizeof(height)); + } + + void SetDescriptor0(const void* descriptor) { + std::memcpy(bytes.data() + 0x20, &descriptor, sizeof(descriptor)); + } +}; + +std::vector Blocks(const Requirement& requirement, std::uint64_t first) { + std::vector blocks(requirement.blockCount); + 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); + + std::uint32_t packetDwords = 0xa5a5a5a5; + Require(scePsmlMfsrGetDispatchMfsrPacketSizeInDwords(sentinel, &packetDwords) == kErrNotInitialized); + Require(packetDwords == 0xa5a5a5a5); + float bias = 0.5f; + Require(scePsmlMfsrGetMipmapBias(sentinel, &bias, 3840) == kErrNotInitialized); + Require(bias == 0.5f); + Require(scePsmlMfsrSelectConfig(sentinel) == kErrNotInitialized); + Require(scePsmlMfsrRequestCapture(sentinel) == kErrNotInitialized); + Require(scePsmlMfsrIsCaptureInProgress(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 == 52); + + 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); + + std::uint32_t packetDwords = 0; + Require(scePsmlMfsrGetDispatchMfsrPacketSizeInDwords(mfsrContext, &packetDwords) == 0); + Require(packetDwords == 0x600); + Require(scePsmlMfsrGetDispatchMfsrPacketSizeInDwords(mfsrContext, nullptr) == kErrNullOutput); + Require(scePsmlMfsrGetDispatchMfsrPacketSizeInDwords(sharedResources, &packetDwords) == 0); + + std::uint64_t foreign = 0; + Require(scePsmlMfsrGetDispatchMfsrPacketSizeInDwords(&foreign, &packetDwords) == kErrInvalidArgument); + Require(scePsmlMfsrGetDispatchMfsrPacketSizeInDwords(nullptr, &packetDwords) == kErrInvalidArgument); + + float bias = 42.0f; + Require(scePsmlMfsrGetMipmapBias(mfsrContext, &bias, 3840) == kErrInvalidArgument); + Require(bias == 42.0f); + Require(scePsmlMfsrGetMipmapBias(&foreign, &bias, 3840) == kErrInvalidArgument); + Require(scePsmlMfsrGetMipmapBias(sharedResources, &bias, 3840) == kErrInvalidArgument); + + Require(scePsmlMfsrSelectConfig(mfsrContext) == 0); + Require(scePsmlMfsrSelectConfig(&foreign) == kErrInvalidArgument); + Require(scePsmlMfsrRequestCapture(mfsrContext) == 0); + Require(scePsmlMfsrRequestCapture(nullptr) == kErrInvalidArgument); + Require(scePsmlMfsrIsCaptureInProgress(mfsrContext) == 0); + Require(scePsmlMfsrIsCaptureInProgress(&foreign) == kErrInvalidArgument); + + ContextParam dimensionsParam; + dimensionsParam.Set(contextBlocks.data(), contextBlocks.size(), sharedResources); + dimensionsParam.SetInputDimensions(1920, 1080); + void* dimensionContext = nullptr; + Require(scePsmlMfsrCreateContext800M3_2(&dimensionContext, dimensionsParam.bytes.data()) == 0); + Require(scePsmlMfsrGetMipmapBias(dimensionContext, &bias, 3840) == 0); + Require(bias == 0.0f); + Require(scePsmlMfsrGetMipmapBias(dimensionContext, &bias, 1920) == 0); + Require(bias == -1.0f); + Require(scePsmlMfsrGetMipmapBias(dimensionContext, nullptr, 1920) == kErrNullOutput); + Require(scePsmlMfsrReleaseContext(dimensionContext) == 0); + + std::array descriptor{}; + const std::uint16_t descriptorWidth = 1280, descriptorHeight = 720; + std::memcpy(descriptor.data() + 0x20, &descriptorWidth, sizeof(descriptorWidth)); + std::memcpy(descriptor.data() + 0x22, &descriptorHeight, sizeof(descriptorHeight)); + ContextParam descriptorParam; + descriptorParam.Set(contextBlocks.data(), contextBlocks.size(), sharedResources); + descriptorParam.SetDescriptor0(descriptor.data()); + void* descriptorContext = nullptr; + Require(scePsmlMfsrCreateContext800M3_2(&descriptorContext, descriptorParam.bytes.data()) == 0); + Require(scePsmlMfsrGetMipmapBias(descriptorContext, &bias, 2560) == 0); + Require(bias == 0.0f); + Require(scePsmlMfsrReleaseContext(descriptorContext) == 0); + + 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 = 51; + 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 +297,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..5afa252aa8 100644 --- a/core/libs/tests/GuestPthreadAttr.cpp +++ b/core/libs/tests/GuestPthreadAttr.cpp @@ -1,7 +1,9 @@ #include "SceTypes.hpp" +#include #include #include #include +#include extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); @@ -27,6 +29,14 @@ 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); +int APS5_VABI scePthreadAttrSetdetachstate(PthreadAttr* attr, int state); +int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t destructor); +int APS5_VABI scePthreadKeyDelete(PthreadKey key); +int APS5_VABI scePthreadSetspecific(PthreadKey key, void* value); +Pthread APS5_VABI scePthreadSelf(); } static constexpr int SCE_OK = 0; @@ -44,6 +54,9 @@ static constexpr std::size_t STACK_SIZE = 2u << 20; static constexpr std::size_t MIN_STACK_SIZE = 16384; static constexpr int GUEST_EINVAL = 22; static constexpr int GUEST_ENOTSUP = 45; +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003); +static constexpr int DETACH_DETACHED = 1; +static constexpr int DETACHED_THREADS = 32; static void Require(bool value) { if (!value) std::abort(); } @@ -116,6 +129,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; @@ -217,11 +266,86 @@ static void CheckThreadScope() { Require(scePthreadJoin(inheritedThread, nullptr) == SCE_OK); } +static int QueryResult(Pthread thread) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(scePthreadAttrSetstacksize(&attr, STACK_SIZE) == SCE_OK); + const int result = scePthreadAttrGet(thread, &attr); + std::size_t stackSize = 0; + Require(scePthreadAttrGetstacksize(&attr, &stackSize) == SCE_OK); + Require(result == SCE_OK || stackSize == STACK_SIZE); + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + return result; +} + +static bool ReportedEnded(Pthread thread) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (QueryResult(thread) == SCE_OK) { + if (std::chrono::steady_clock::now() > deadline) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return QueryResult(thread) == SCE_KERNEL_ERROR_ESRCH; +} + +struct ExitQuery { + PthreadKey key = -1; + int result = -1; +}; + +static void APS5_VABI QuerySelfAtExit(void* value) { + auto* query = static_cast(value); + query->result = QueryResult(scePthreadSelf()); +} + +static void* APS5_VABI SetExitQuery(void* arg) { + auto* query = static_cast(arg); + Require(scePthreadSetspecific(query->key, query) == SCE_OK); + return nullptr; +} + +static void* APS5_VABI Finish(void*) { + return nullptr; +} + +static void CheckEndedThreads() { + Require(QueryResult(scePthreadSelf()) == SCE_OK); + + Pthread joined = nullptr; + Require(scePthreadCreate(&joined, nullptr, Finish, nullptr, nullptr) == SCE_OK); + Require(scePthreadJoin(joined, nullptr) == SCE_OK); + Require(QueryResult(joined) == SCE_KERNEL_ERROR_ESRCH); + PthreadAttr unset = nullptr; + Require(scePthreadAttrGet(joined, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGet(joined, &unset) == SCE_KERNEL_ERROR_EINVAL); + + ExitQuery exitQuery; + Require(scePthreadKeyCreate(&exitQuery.key, reinterpret_cast(QuerySelfAtExit)) == SCE_OK); + Pthread finished = nullptr; + Require(scePthreadCreate(&finished, nullptr, SetExitQuery, &exitQuery, nullptr) == SCE_OK); + Require(ReportedEnded(finished)); + Require(scePthreadJoin(finished, nullptr) == SCE_OK); + Require(exitQuery.result == SCE_OK); + Require(QueryResult(finished) == SCE_KERNEL_ERROR_ESRCH); + Require(scePthreadKeyDelete(exitQuery.key) == SCE_OK); + + PthreadAttr detachedAttr = nullptr; + Require(scePthreadAttrInit(&detachedAttr) == SCE_OK); + Require(scePthreadAttrSetdetachstate(&detachedAttr, DETACH_DETACHED) == SCE_OK); + for (int index = 0; index < DETACHED_THREADS; ++index) { + Pthread detached = nullptr; + Require(scePthreadCreate(&detached, &detachedAttr, Finish, nullptr, nullptr) == SCE_OK); + Require(ReportedEnded(detached)); + } + Require(scePthreadAttrDestroy(&detachedAttr) == SCE_OK); +} + int main() { CheckStackSizeLimit(); CheckSoloSched(); + CheckSchedArguments(); CheckSchedulingAttributes(); CheckThreadScope(); + CheckEndedThreads(); PthreadAttr attr = nullptr; Require(scePthreadAttrInit(&attr) == SCE_OK); Require(scePthreadAttrSetinheritsched(&attr, EXPLICIT_SCHED) == SCE_OK); diff --git a/core/libs/tests/GuestPthreadAttrErrors.cpp b/core/libs/tests/GuestPthreadAttrErrors.cpp index 259ee7789c..06c2e15832 100644 --- a/core/libs/tests/GuestPthreadAttrErrors.cpp +++ b/core/libs/tests/GuestPthreadAttrErrors.cpp @@ -2,6 +2,7 @@ #include #include #include +#include extern "C" { int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); @@ -11,7 +12,9 @@ int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, std::size_t* s int APS5_VABI scePthreadAttrSetdetachstate(PthreadAttr* attr, int state); int APS5_VABI scePthreadAttrGetdetachstate(const PthreadAttr* attr, int* state); int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy); +int APS5_VABI scePthreadAttrGetschedpolicy(const PthreadAttr* attr, int* policy); int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); +int APS5_VABI scePthreadAttrGetinheritsched(const PthreadAttr* attr, int* inheritSched); int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); int APS5_VABI scePthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedParam* param); int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask); @@ -27,6 +30,9 @@ static void Require(bool value) { if (!value) std::abort(); } int main() { PthreadAttr attr = nullptr; Require(scePthreadAttrInit(&attr) == SCE_OK); + int value = -1; + Require(scePthreadAttrGetschedpolicy(&attr, &value) == SCE_OK && value == 1); + Require(scePthreadAttrGetinheritsched(&attr, &value) == SCE_OK && value == 4); for (std::size_t size : {std::size_t{0}, std::size_t{1}, std::size_t{16383}}) Require(scePthreadAttrSetstacksize(&attr, size) == SCE_KERNEL_ERROR_EINVAL); @@ -35,11 +41,23 @@ int main() { for (int state : {-1, 2, 3, 100}) Require(scePthreadAttrSetdetachstate(&attr, state) == SCE_KERNEL_ERROR_EINVAL); for (int state : {0, 1}) Require(scePthreadAttrSetdetachstate(&attr, state) == SCE_OK); - for (int policy : {-1, 0, 2, 4}) Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_KERNEL_ERROR_EOPNOTSUPP); - for (int policy : {1, 3}) Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_OK); + for (int policy : {-1, 0, 2, 4}) { + Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_KERNEL_ERROR_EOPNOTSUPP); + Require(scePthreadAttrGetschedpolicy(&attr, &value) == SCE_OK && value == 1); + } + for (int policy : {3, 1}) { + Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_OK); + Require(scePthreadAttrGetschedpolicy(&attr, &value) == SCE_OK && value == policy); + } - for (int inherit : {-1, 1, 2, 8}) Require(scePthreadAttrSetinheritsched(&attr, inherit) == SCE_KERNEL_ERROR_EOPNOTSUPP); - for (int inherit : {0, 4}) Require(scePthreadAttrSetinheritsched(&attr, inherit) == SCE_OK); + for (int inherit : {-1, 1, 2, 8}) { + Require(scePthreadAttrSetinheritsched(&attr, inherit) == SCE_KERNEL_ERROR_EOPNOTSUPP); + Require(scePthreadAttrGetinheritsched(&attr, &value) == SCE_OK && value == 4); + } + for (int inherit : {0, 4}) { + Require(scePthreadAttrSetinheritsched(&attr, inherit) == SCE_OK); + Require(scePthreadAttrGetinheritsched(&attr, &value) == SCE_OK && value == inherit); + } KernelSchedParam param{}; for (int priority : {-1, 0, 255, 768, 900, 960}) { @@ -62,6 +80,10 @@ int main() { Require(scePthreadAttrGetstacksize(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrGetdetachstate(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrGetschedparam(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetschedpolicy(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + bool threw = false; + try { scePthreadAttrGetinheritsched(&attr, nullptr); } catch (const std::runtime_error&) { threw = true; } + Require(threw); for (PthreadAttr* missing : {static_cast(nullptr), static_cast(&attr)}) { PthreadAttr unset = nullptr; @@ -76,6 +98,8 @@ int main() { Require(scePthreadAttrGetstacksize(target, &size) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrGetdetachstate(target, &state) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrGetschedparam(target, ¶m) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetschedpolicy(target, &state) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetinheritsched(target, &state) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrGetaffinity(target, &mask) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrDestroy(target) == SCE_KERNEL_ERROR_EINVAL); } diff --git a/core/libs/tests/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..163c97b14c 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); @@ -90,6 +91,8 @@ int main() { GuestBarrier* invalid = nullptr; Require(pthread_barrier_init_nid_postfix(nullptr, &attr, 1) == GUEST_EINVAL); Require(pthread_barrier_init_nid_postfix(&invalid, &attr, 0) == GUEST_EINVAL && invalid == nullptr); + Require(pthread_barrier_init_nid_postfix(&invalid, &attr, 0x80000000u) == GUEST_EINVAL && invalid == nullptr); + Require(pthread_barrier_init_nid_postfix(&invalid, &attr, 0xffffffffu) == GUEST_EINVAL && invalid == nullptr); Context withAttr; Require(pthread_barrier_init_nid_postfix(&withAttr.barrier, &attr, WAITERS) == 0 && withAttr.barrier != nullptr); diff --git a/core/libs/tests/GuestPthreadCancel.cpp b/core/libs/tests/GuestPthreadCancel.cpp index 311fb8a5a4..81cc47ebde 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,8 +30,12 @@ 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); +void APS5_VABI pthread_cleanup_push_nid_postfix(void (APS5_VABI* routine)(void*), void* argument); +void APS5_VABI pthread_cleanup_pop_nid_postfix(int execute); } static constexpr int SCE_OK = 0; @@ -57,7 +62,7 @@ struct Shared { std::atomic returned{false}; std::atomic survived{false}; std::atomic cleanups{0}; - int order[2] = {0, 0}; + int order[3] = {0, 0, 0}; }; static Shared shared; @@ -93,6 +98,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; @@ -148,6 +191,18 @@ static void* APS5_VABI Handlers(void*) { return nullptr; } +static void* APS5_VABI HeapHandlers(void*) { + std::uintptr_t first[8]; + __pthread_cleanup_push_imp_nid_postfix(RecordOrder, reinterpret_cast(std::intptr_t{1}), first); + pthread_cleanup_push_nid_postfix(RecordOrder, reinterpret_cast(std::intptr_t{2})); + pthread_cleanup_pop_nid_postfix(0); + pthread_cleanup_push_nid_postfix(RecordOrder, reinterpret_cast(std::intptr_t{3})); + __pthread_cleanup_pop_imp_nid_postfix(1); + pthread_cleanup_push_nid_postfix(RecordOrder, reinterpret_cast(std::intptr_t{4})); + scePthreadExit(reinterpret_cast(std::intptr_t{8})); + return nullptr; +} + static Pthread Start(PthreadEntry entry) { shared.ready.store(false); shared.release.store(false); @@ -197,6 +252,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); @@ -229,6 +289,12 @@ int main() { Require(reinterpret_cast(result) == 7); Require(shared.cleanups.load() == 2 && shared.order[0] == 3 && shared.order[1] == 1); + shared.cleanups.store(0); + Require(scePthreadCreate(&thread, nullptr, HeapHandlers, nullptr, nullptr) == SCE_OK); + Require(scePthreadJoin(thread, &result) == SCE_OK); + Require(reinterpret_cast(result) == 8); + Require(shared.cleanups.load() == 3 && shared.order[0] == 3 && shared.order[1] == 4 && shared.order[2] == 1); + Require(sem_destroy_nid_postfix(&shared.posixSem) == 0); Require(scePthreadSemDestroy(&shared.sem) == SCE_OK); Require(scePthreadCondDestroy(&shared.cond) == SCE_OK); diff --git a/core/libs/tests/GuestPthreadCancelRecovery.cpp b/core/libs/tests/GuestPthreadCancelRecovery.cpp index 59e6056c85..297f761a92 100644 --- a/core/libs/tests/GuestPthreadCancelRecovery.cpp +++ b/core/libs/tests/GuestPthreadCancelRecovery.cpp @@ -1,10 +1,21 @@ #include "SceTypes.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" #include #include +#include #include #include #include #include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#endif extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); @@ -17,6 +28,9 @@ int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t int APS5_VABI scePthreadKeyDelete(PthreadKey key); int APS5_VABI scePthreadSetspecific(PthreadKey key, void* value); int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); @@ -24,6 +38,10 @@ int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); int APS5_VABI scePthreadCondSignal(PthreadCond* cond); int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec); +int APS5_VABI scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex); +int APS5_VABI pthread_cond_wait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex); +int APS5_VABI pthread_cond_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* abstime); +int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* time); int APS5_VABI scePthreadSemInit(PthreadSem* sem, int flag, unsigned int value, const char* name); int APS5_VABI scePthreadSemDestroy(PthreadSem* sem); int APS5_VABI scePthreadSemWait(PthreadSem* sem); @@ -226,6 +244,166 @@ static void ConditionRecovery() { Require(scePthreadMutexDestroy(&mutex) == 0); } +enum class ConditionWaitMode { SceTimed, Sce, PosixTimed, Posix }; + +struct SignaledCondition { + PthreadMutex mutex = nullptr; + PthreadCond condition = nullptr; + PthreadCond* slot = &condition; + ConditionWaitMode mode = ConditionWaitMode::SceTimed; + int lockCount = 1; + bool cancellationDisabled = false; + bool released = false; + std::atomic ready{0}; + std::array results{-1, -1}; + std::array cleaned{}; +}; + +struct SignaledConditionWorker { + SignaledCondition* context; + int index; +}; + +static void APS5_VABI UnlockSignaledCondition(void* value) { + auto& worker = *static_cast(value); + auto& context = *worker.context; + for (int level = 0; level < context.lockCount; ++level) Require(scePthreadMutexUnlock(&context.mutex) == 0); + Require(scePthreadMutexUnlock(&context.mutex) == static_cast(0x80020001)); + context.cleaned[worker.index] = true; +} + +static void* APS5_VABI SignaledConditionWaiter(void* value) { + auto& worker = *static_cast(value); + auto& context = *worker.context; + if (context.cancellationDisabled) Require(scePthreadSetcancelstate(CANCEL_DISABLE, nullptr) == 0); + for (int level = 0; level < context.lockCount; ++level) Require(scePthreadMutexLock(&context.mutex) == 0); + std::uintptr_t cleanup[8]; + __pthread_cleanup_push_imp_nid_postfix(UnlockSignaledCondition, &worker, cleanup); + context.ready.fetch_add(1); + int result = 0; + do { + switch (context.mode) { + case ConditionWaitMode::SceTimed: + result = scePthreadCondTimedwait(context.slot, &context.mutex, 2000000); + break; + case ConditionWaitMode::Sce: + result = scePthreadCondWait(context.slot, &context.mutex); + break; + case ConditionWaitMode::PosixTimed: { + KernelTimespec deadline{}; + Require(clock_gettime_nid_postfix(0, &deadline) == 0); + deadline.tv_sec += 2; + result = pthread_cond_timedwait_nid_postfix(context.slot, &context.mutex, &deadline); + break; + } + case ConditionWaitMode::Posix: + result = pthread_cond_wait_nid_postfix(context.slot, &context.mutex); + break; + } + } while (result == 0 && !context.released); + context.results[worker.index] = result; + __pthread_cleanup_pop_imp_nid_postfix(1); + return nullptr; +} + +static bool ConditionRegistered(Pthread thread) { + const std::lock_guard lock(thread->cancelLock); + return thread->cancelWait != nullptr; +} + +static void InitializeSignaledCondition(SignaledCondition& context, bool recursive) { + PthreadMutexattr attr = nullptr; + Require(scePthreadMutexattrInit(&attr) == 0); + Require(scePthreadMutexattrSettype(&attr, recursive ? 2 : 1) == 0); + Require(scePthreadMutexInit(&context.mutex, &attr, nullptr) == 0); + Require(scePthreadMutexattrDestroy(&attr) == 0); + context.lockCount = recursive ? 3 : 1; + Require(scePthreadCondInit(context.slot, nullptr, nullptr) == 0); +} + +static void CanceledConditionSignal(ConditionWaitMode mode, bool recursive) { + SignaledCondition context; + context.mode = mode; + InitializeSignaledCondition(context, recursive); + std::array workers{{{&context, 0}, {&context, 1}}}; + std::array threads{}; + for (int index = 0; index < 2; ++index) { + Require(scePthreadCreate(&threads[index], nullptr, SignaledConditionWaiter, &workers[index], nullptr) == 0); + while (context.ready.load() <= index) std::this_thread::yield(); + Require(scePthreadMutexLock(&context.mutex) == 0); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (!ConditionRegistered(threads[index]) && std::chrono::steady_clock::now() < deadline) { + Require(scePthreadMutexUnlock(&context.mutex) == 0); + std::this_thread::yield(); + Require(scePthreadMutexLock(&context.mutex) == 0); + } + Require(ConditionRegistered(threads[index])); + Require(scePthreadMutexUnlock(&context.mutex) == 0); + } + Require(scePthreadMutexLock(&context.mutex) == 0); + context.released = true; + Require(scePthreadCondSignal(&context.condition) == 0); + int selected = -1; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (selected < 0 && std::chrono::steady_clock::now() < deadline) { + for (int index = 0; index < 2; ++index) if (!ConditionRegistered(threads[index])) selected = index; + std::this_thread::yield(); + } + Require(selected >= 0); + Require(scePthreadCancel(threads[selected]) == 0); + Require(scePthreadMutexUnlock(&context.mutex) == 0); + Join(threads[selected], CANCELED); + Join(threads[1 - selected], nullptr); + Require(context.results[selected] == -1 && context.results[1 - selected] == 0); + Require(context.cleaned[0] && context.cleaned[1]); + Require(scePthreadCondDestroy(&context.condition) == 0); + Require(scePthreadMutexDestroy(&context.mutex) == 0); +} + +static void DestroyedSignaledCondition(ConditionWaitMode mode, bool recursive, bool cancel, bool cancellationDisabled) { + SignaledCondition context; + context.mode = mode; + context.cancellationDisabled = cancellationDisabled; +#ifdef _WIN32 + SYSTEM_INFO system{}; + GetSystemInfo(&system); + const auto bytes = static_cast(system.dwPageSize); + context.slot = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(context.slot != nullptr); +#else + const auto bytes = static_cast(sysconf(_SC_PAGESIZE)); + context.slot = static_cast(mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(context.slot != MAP_FAILED); +#endif + *context.slot = nullptr; + InitializeSignaledCondition(context, recursive); + const std::weak_ptr retained = (*context.slot)->_cv; + SignaledConditionWorker worker{&context, 0}; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, SignaledConditionWaiter, &worker, nullptr) == 0); + while (context.ready.load() == 0) std::this_thread::yield(); + Require(scePthreadMutexLock(&context.mutex) == 0); + context.released = true; + Require(scePthreadCondSignal(context.slot) == 0); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (ConditionRegistered(thread) && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + Require(!ConditionRegistered(thread)); + Require(scePthreadCondDestroy(context.slot) == 0); + Require(!retained.expired()); +#ifdef _WIN32 + Require(VirtualFree(context.slot, 0, MEM_RELEASE) != 0); +#else + Require(munmap(context.slot, bytes) == 0); +#endif + if (cancel) Require(scePthreadCancel(thread) == 0); + Require(scePthreadMutexUnlock(&context.mutex) == 0); + Join(thread, cancel && !cancellationDisabled ? CANCELED : nullptr); + Require(context.cleaned[0]); + Require(context.results[0] == (cancel && !cancellationDisabled ? -1 : 0)); + Require(retained.expired()); + Require(scePthreadMutexDestroy(&context.mutex) == 0); +} + static void* APS5_VABI HeldTarget(void*) { CheckDefaults(); targetRelease.wait(false); @@ -274,6 +452,14 @@ int main() { } Require(scePthreadKeyDelete(key) == 0); ConditionRecovery(); + for (const auto mode : {ConditionWaitMode::SceTimed, ConditionWaitMode::Sce, ConditionWaitMode::PosixTimed, ConditionWaitMode::Posix}) { + for (bool recursive : {false, true}) { + CanceledConditionSignal(mode, recursive); + DestroyedSignaledCondition(mode, recursive, true, false); + DestroyedSignaledCondition(mode, recursive, false, false); + DestroyedSignaledCondition(mode, recursive, true, true); + } + } disabled = true; ConditionRecovery(); JoinRecovery(); 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/GuestPthreadDestroyedObjects.cpp b/core/libs/tests/GuestPthreadDestroyedObjects.cpp new file mode 100644 index 0000000000..a9a041cfc0 --- /dev/null +++ b/core/libs/tests/GuestPthreadDestroyedObjects.cpp @@ -0,0 +1,46 @@ +#include "SceTypes.hpp" +#include +#include + +extern "C" { +int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); +int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); +int APS5_VABI scePthreadCondSignal(PthreadCond* cond); +int APS5_VABI scePthreadCondBroadcast(PthreadCond* cond); +int APS5_VABI scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex); +int APS5_VABI pthread_mutex_lock_nid_postfix(PthreadMutex* mutex); +int APS5_VABI pthread_cond_signal_nid_postfix(PthreadCond* cond); +int APS5_VABI pthread_cond_destroy_nid_postfix(PthreadCond* cond); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016u); +static constexpr int POSIX_EINVAL = 22; + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + PthreadMutex mutex = nullptr; + Require(scePthreadMutexInit(&mutex, nullptr, "destroyed") == SCE_OK); + Require(scePthreadMutexDestroy(&mutex) == SCE_OK); + Require(scePthreadMutexLock(&mutex) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexTrylock(&mutex) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexUnlock(&mutex) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_mutex_lock_nid_postfix(&mutex) == POSIX_EINVAL); + Require(mutex == reinterpret_cast(std::uintptr_t{2})); + + PthreadCond cond = nullptr; + Require(scePthreadCondInit(&cond, nullptr, "destroyed") == SCE_OK); + Require(scePthreadCondDestroy(&cond) == SCE_OK); + Require(scePthreadCondSignal(&cond) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadCondBroadcast(&cond) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadCondWait(&cond, &mutex) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_cond_signal_nid_postfix(&cond) == POSIX_EINVAL); + Require(scePthreadCondDestroy(&cond) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_cond_destroy_nid_postfix(&cond) == POSIX_EINVAL); +} diff --git a/core/libs/tests/GuestPthreadFrameChain.cpp b/core/libs/tests/GuestPthreadFrameChain.cpp new file mode 100644 index 0000000000..d9c491090f --- /dev/null +++ b/core/libs/tests/GuestPthreadFrameChain.cpp @@ -0,0 +1,70 @@ +#include "SceTypes.hpp" +#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); +} + +static constexpr int SCE_OK = 0; +static constexpr std::uintptr_t ChainReach = std::uintptr_t{64} << 10; + +static void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "guest pthread frame chain: %s\n", what); + std::abort(); +} + +struct Walk { + int frames = 0; + bool leftStack = false; + bool ended = false; + bool endedOnZeroRecord = false; +}; + +static Walk g_walk; + +[[gnu::noinline]] static void WalkFrameChain() { + auto* frame = static_cast(__builtin_frame_address(0)); + const auto low = reinterpret_cast(frame); + for (int depth = 0; depth < 100; ++depth) { + const auto address = reinterpret_cast(frame); + if (address < low || address - low > ChainReach) { + g_walk.leftStack = true; + return; + } + ++g_walk.frames; + if (frame[1] == 0) { + g_walk.ended = true; + g_walk.endedOnZeroRecord = frame[0] == 0; + return; + } + auto* next = reinterpret_cast(frame[0]); + if (reinterpret_cast(next) <= address) { + g_walk.leftStack = true; + return; + } + frame = next; + } +} + +static void* APS5_VABI WalkingEntry(void* arg) { + WalkFrameChain(); + return arg; +} + +int main() { + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, WalkingEntry, &g_walk, "frame chain") == SCE_OK, "create a guest thread"); + void* result = nullptr; + Require(scePthreadJoin(thread, &result) == SCE_OK, "join the guest thread"); + Require(result == &g_walk, "the guest entry gets its argument and its return value reaches scePthreadJoin"); + Require(!g_walk.leftStack, "the frame chain of a guest thread stays on its stack and climbs it"); + Require(g_walk.ended, "the frame chain of a guest thread ends at a frame whose return address is 0"); + Require(g_walk.endedOnZeroRecord, "the last frame of a guest thread's chain also has a saved frame pointer of 0"); + Require(g_walk.frames == 3, "the chain is the walker's frame, the guest entry's frame and the zero record"); + std::puts("guest pthread frame chain tests passed"); + return 0; +} 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/GuestPthreadJoinDetach.cpp b/core/libs/tests/GuestPthreadJoinDetach.cpp new file mode 100644 index 0000000000..48a982989e --- /dev/null +++ b/core/libs/tests/GuestPthreadJoinDetach.cpp @@ -0,0 +1,75 @@ +#include "prx/libkernel/Pthread/include/Pthread.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +extern "C" int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +extern "C" int APS5_VABI scePthreadDetach(Pthread thread); + +static constexpr int SCE_OK = 0; + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::exit(1); + } +} + +struct Joinee { + Pthread thread = nullptr; + std::atomic release{false}; +}; + +struct Joiner { + Pthread target = nullptr; + void* result = nullptr; + int status = -1; +}; + +static void* APS5_VABI Wait(void* opaque) { + auto* joinee = static_cast(opaque); + while (!joinee->release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + return opaque; +} + +static void* APS5_VABI Join(void* opaque) { + auto* joiner = static_cast(opaque); + joiner->status = scePthreadJoin(joiner->target, &joiner->result); + return nullptr; +} + +static bool Joining(Pthread thread) { + std::lock_guard lock(thread->_join_mtx); + return thread->_joining; +} + +int main() { + Joinee joinee; + Require(scePthreadCreate(&joinee.thread, nullptr, Wait, &joinee, "joinee") == SCE_OK, "create the joinee"); + Joiner joiner{joinee.thread}; + Pthread joinerThread = nullptr; + Require(scePthreadCreate(&joinerThread, nullptr, Join, &joiner, "joiner") == SCE_OK, "create the joiner"); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (!Joining(joinee.thread)) { + Require(std::chrono::steady_clock::now() < deadline, "the joiner waits in scePthreadJoin"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + Require(scePthreadDetach(joinee.thread) == SCE_KERNEL_ERROR_EINVAL, "detaching a thread with a joiner returns EINVAL"); + Require(scePthreadJoin(joinee.thread, nullptr) == SCE_KERNEL_ERROR_EOPNOTSUPP, "a second joiner gets EOPNOTSUPP"); + joinee.release.store(true); + Require(scePthreadJoin(joinerThread, nullptr) == SCE_OK, "join the joiner"); + Require(joiner.status == SCE_OK && joiner.result == &joinee, "the first join completes with the return value"); + + Joinee detached; + Require(scePthreadCreate(&detached.thread, nullptr, Wait, &detached, "detached") == SCE_OK, "create a thread to detach"); + Require(scePthreadDetach(detached.thread) == SCE_OK, "detach a thread without a joiner"); + Require(scePthreadJoin(detached.thread, nullptr) == SCE_KERNEL_ERROR_EINVAL, "joining a detached thread returns EINVAL"); + detached.release.store(true); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + return 0; +} 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/GuestPthreadMutexErrors.cpp b/core/libs/tests/GuestPthreadMutexErrors.cpp index 064469d42d..da1083b121 100644 --- a/core/libs/tests/GuestPthreadMutexErrors.cpp +++ b/core/libs/tests/GuestPthreadMutexErrors.cpp @@ -2,6 +2,7 @@ #include #include #include +#include extern "C" { int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); @@ -12,14 +13,131 @@ int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* a int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexGetspinloops(PthreadMutex* mutex, int* count); +int APS5_VABI scePthreadMutexSetspinloops(PthreadMutex* mutex, int count); +int APS5_VABI scePthreadMutexGetyieldloops(PthreadMutex* mutex, int* count); +int APS5_VABI scePthreadMutexSetyieldloops(PthreadMutex* mutex, int count); +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_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_reltimedlock_np_nid_postfix(PthreadMutex* mutex, KernelUseconds usec); +int APS5_VABI pthread_mutex_isowned_np_nid_postfix(PthreadMutex* mutex); +int APS5_VABI pthread_mutex_getspinloops_np_nid_postfix(PthreadMutex* mutex, int* count); +int APS5_VABI pthread_mutex_setspinloops_np_nid_postfix(PthreadMutex* mutex, int count); +int APS5_VABI pthread_mutex_getyieldloops_np_nid_postfix(PthreadMutex* mutex, int* count); +int APS5_VABI pthread_mutex_setyieldloops_np_nid_postfix(PthreadMutex* mutex, int count); } static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016u); static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010u); +static constexpr int POSIX_EDEADLK = 11; +static constexpr int POSIX_EINVAL = 22; +static constexpr int POSIX_ETIMEDOUT = 60; +static const PthreadMutex AdaptiveInitializer = reinterpret_cast(std::uintptr_t{1}); +static const PthreadMutex DestroyedMutex = reinterpret_cast(std::uintptr_t{2}); + static void Require(bool value) { if (!value) std::abort(); } +template +static bool Throws(Call call) { + try { + call(); + } catch (const std::exception&) { + return true; + } + return false; +} + +struct OtherThread { + PthreadMutex* mutex; + int isowned = -1; + int reltimedlock = -1; +}; + +static void* APS5_VABI CheckFromOtherThread(void* arg) { + auto& other = *static_cast(arg); + other.isowned = pthread_mutex_isowned_np_nid_postfix(other.mutex); + other.reltimedlock = pthread_mutex_reltimedlock_np_nid_postfix(other.mutex, 1000); + return nullptr; +} + +static void CheckOwnershipAndRelativeLock() { + PthreadMutex mutex = nullptr; + Require(pthread_mutex_isowned_np_nid_postfix(&mutex) == 0 && mutex == nullptr); + PthreadMutex adaptive = AdaptiveInitializer; + Require(pthread_mutex_isowned_np_nid_postfix(&adaptive) == 0 && adaptive == AdaptiveInitializer); + Require(pthread_mutex_reltimedlock_np_nid_postfix(&mutex, 1000) == 0 && mutex != nullptr); + Require(pthread_mutex_isowned_np_nid_postfix(&mutex) == 1); + Require(pthread_mutex_reltimedlock_np_nid_postfix(&mutex, 1000) == POSIX_EDEADLK); + OtherThread other{&mutex}; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, CheckFromOtherThread, &other, nullptr) == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(other.isowned == 0 && other.reltimedlock == POSIX_ETIMEDOUT); + Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_isowned_np_nid_postfix(&mutex) == 0); + Require(pthread_mutex_reltimedlock_np_nid_postfix(&mutex, 0) == 0); + Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_destroy_nid_postfix(&mutex) == 0); + Require(mutex == DestroyedMutex); + Require(pthread_mutex_isowned_np_nid_postfix(&mutex) == 0); + Require(pthread_mutex_reltimedlock_np_nid_postfix(&mutex, 1000) == POSIX_EINVAL); + Require(Throws([] { pthread_mutex_isowned_np_nid_postfix(nullptr); })); +} + +static void CheckLoops(PthreadMutexattr* attr) { + PthreadMutex mutex = nullptr; + int count = -1; + Require(pthread_mutex_getspinloops_np_nid_postfix(&mutex, &count) == 0 && count == 0 && mutex != nullptr); + Require(pthread_mutex_getyieldloops_np_nid_postfix(&mutex, &count) == 0 && count == 0); + Require(pthread_mutex_destroy_nid_postfix(&mutex) == 0); + + PthreadMutex adaptive = AdaptiveInitializer; + Require(scePthreadMutexGetspinloops(&adaptive, &count) == SCE_OK && count == 2000 && adaptive != AdaptiveInitializer); + Require(scePthreadMutexGetyieldloops(&adaptive, &count) == SCE_OK && count == 0); + Require(scePthreadMutexDestroy(&adaptive) == SCE_OK); + + Require(scePthreadMutexattrSettype(attr, 4) == SCE_OK); + Require(scePthreadMutexInit(&mutex, attr, "adaptive") == SCE_OK); + Require(scePthreadMutexGetspinloops(&mutex, &count) == SCE_OK && count == 2000); + Require(scePthreadMutexSetspinloops(&mutex, 7) == SCE_OK); + Require(pthread_mutex_getspinloops_np_nid_postfix(&mutex, &count) == 0 && count == 7); + Require(pthread_mutex_setspinloops_np_nid_postfix(&mutex, -3) == 0); + Require(scePthreadMutexGetspinloops(&mutex, &count) == SCE_OK && count == -3); + Require(scePthreadMutexSetyieldloops(&mutex, 9) == SCE_OK); + Require(pthread_mutex_getyieldloops_np_nid_postfix(&mutex, &count) == 0 && count == 9); + Require(pthread_mutex_setyieldloops_np_nid_postfix(&mutex, 11) == 0); + Require(scePthreadMutexGetyieldloops(&mutex, &count) == SCE_OK && count == 11); + Require(pthread_mutex_lock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_lock_nid_postfix(&mutex) == POSIX_EDEADLK); + Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); + Require(Throws([&] { scePthreadMutexGetspinloops(&mutex, nullptr); })); + Require(Throws([&] { scePthreadMutexGetyieldloops(&mutex, nullptr); })); + Require(scePthreadMutexDestroy(&mutex) == SCE_OK); + + count = -1; + Require(scePthreadMutexGetspinloops(&mutex, &count) == SCE_KERNEL_ERROR_EINVAL && count == -1); + Require(scePthreadMutexSetspinloops(&mutex, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexGetyieldloops(&mutex, &count) == SCE_KERNEL_ERROR_EINVAL && count == -1); + Require(pthread_mutex_getspinloops_np_nid_postfix(&mutex, &count) == POSIX_EINVAL); + Require(pthread_mutex_setspinloops_np_nid_postfix(&mutex, 1) == POSIX_EINVAL); + Require(pthread_mutex_getyieldloops_np_nid_postfix(&mutex, &count) == POSIX_EINVAL && count == -1); + Require(scePthreadMutexSetyieldloops(&mutex, 1) == SCE_OK); + Require(pthread_mutex_setyieldloops_np_nid_postfix(&mutex, 1) == 0); + Require(mutex == DestroyedMutex); + + for (const int type : {1, 2, 3}) { + Require(scePthreadMutexattrSettype(attr, type) == SCE_OK); + Require(scePthreadMutexInit(&mutex, attr, nullptr) == SCE_OK); + Require(scePthreadMutexGetspinloops(&mutex, &count) == SCE_OK && count == 0); + Require(scePthreadMutexDestroy(&mutex) == SCE_OK); + } +} + int main() { PthreadMutexattr attr = nullptr; Require(scePthreadMutexattrInit(&attr) == SCE_OK); @@ -46,5 +164,7 @@ int main() { Require(scePthreadMutexInit(&mutex, &attr, "attr") == SCE_OK); Require(scePthreadMutexDestroy(&mutex) == SCE_OK); + CheckLoops(&attr); + CheckOwnershipAndRelativeLock(); Require(scePthreadMutexattrDestroy(&attr) == SCE_OK); } diff --git a/core/libs/tests/GuestPthreadMutexattr.cpp b/core/libs/tests/GuestPthreadMutexattr.cpp index 410aa02ba0..48dd291a5b 100644 --- a/core/libs/tests/GuestPthreadMutexattr.cpp +++ b/core/libs/tests/GuestPthreadMutexattr.cpp @@ -1,12 +1,16 @@ #include "SceTypes.hpp" #include #include +#include #include extern "C" { int APS5_VABI pthread_mutexattr_init_nid_postfix(PthreadMutexattr* attr); int APS5_VABI pthread_mutexattr_destroy_nid_postfix(PthreadMutexattr* attr); +int APS5_VABI pthread_mutexattr_getpshared_nid_postfix(const PthreadMutexattr* attr, int* pshared); +int APS5_VABI pthread_mutexattr_setpshared_nid_postfix(PthreadMutexattr* attr, int pshared); int APS5_VABI pthread_mutexattr_setprotocol_nid_postfix(PthreadMutexattr* attr, int protocol); +int APS5_VABI pthread_mutexattr_getprotocol_nid_postfix(const PthreadMutexattr* attr, int* protocol); int APS5_VABI pthread_mutexattr_setprioceiling_nid_postfix(PthreadMutexattr* attr, int prioceiling); int APS5_VABI pthread_mutexattr_getprioceiling_nid_postfix(const PthreadMutexattr* attr, int* prioceiling); int APS5_VABI pthread_mutexattr_settype_nid_postfix(PthreadMutexattr* attr, int type); @@ -76,17 +80,69 @@ static void CheckType() { Require(pthread_mutexattr_gettype_nid_postfix(nullptr, &type) == GUEST_EINVAL && type == -1); } +static void CheckProcessSharing() { + PthreadMutexattr attr = nullptr; + int sharing = -1; + Require(pthread_mutexattr_getpshared_nid_postfix(nullptr, &sharing) == GUEST_EINVAL && sharing == -1); + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == GUEST_EINVAL && sharing == -1); + Require(pthread_mutexattr_setpshared_nid_postfix(nullptr, 0) == GUEST_EINVAL); + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 0) == GUEST_EINVAL); + Require(pthread_mutexattr_init_nid_postfix(&attr) == 0); + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); + Require(pthread_mutexattr_settype_nid_postfix(&attr, 2) == 0); + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 0) == 0); + for (const int invalid : {1, -1, 2, 2147483647}) + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, invalid) == GUEST_EINVAL); + 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); })); + PthreadMutex mutex = nullptr; + Require(pthread_mutex_init_nid_postfix(&mutex, &attr) == 0); + Require(pthread_mutex_lock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_lock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_destroy_nid_postfix(&mutex) == 0); + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 0) == 0); + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); + Require(pthread_mutexattr_destroy_nid_postfix(&attr) == 0); + sharing = -1; + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == GUEST_EINVAL && sharing == -1); + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 0) == GUEST_EINVAL); +} + +static void CheckInvalidPriorityAttribute(PthreadMutexattr* attr) { + int ceiling = -1; + int protocol = -1; + Require(pthread_mutexattr_setprotocol_nid_postfix(attr, PRIO_PROTECT) == GUEST_EINVAL); + Require(pthread_mutexattr_setprotocol_nid_postfix(attr, -1) == GUEST_EINVAL); + Require(pthread_mutexattr_getprotocol_nid_postfix(attr, &protocol) == GUEST_EINVAL && protocol == -1); + Require(pthread_mutexattr_getprotocol_nid_postfix(attr, nullptr) == GUEST_EINVAL); + Require(pthread_mutexattr_setprioceiling_nid_postfix(attr, 300) == GUEST_EINVAL); + Require(pthread_mutexattr_getprioceiling_nid_postfix(attr, &ceiling) == GUEST_EINVAL && ceiling == -1); + Require(pthread_mutexattr_getprioceiling_nid_postfix(attr, nullptr) == GUEST_EINVAL); +} + int main() { CheckType(); + CheckProcessSharing(); CheckTimedlockInvalidDeadline(); PthreadMutexattr attr = nullptr; + CheckInvalidPriorityAttribute(nullptr); + CheckInvalidPriorityAttribute(&attr); Require(pthread_mutexattr_init_nid_postfix(&attr) == 0); int ceiling = -1; + int protocol = -1; + Require(pthread_mutexattr_getprotocol_nid_postfix(&attr, &protocol) == 0 && protocol == PRIO_NONE); Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == GUEST_EINVAL && ceiling == -1); Require(pthread_mutexattr_setprioceiling_nid_postfix(&attr, 300) == GUEST_EINVAL); Require(pthread_mutexattr_setprotocol_nid_postfix(&attr, PRIO_INHERIT) == 0); + Require(pthread_mutexattr_getprotocol_nid_postfix(&attr, &protocol) == 0 && protocol == PRIO_INHERIT); Require(pthread_mutexattr_setprioceiling_nid_postfix(&attr, 300) == GUEST_EINVAL); Require(pthread_mutexattr_setprotocol_nid_postfix(&attr, PRIO_PROTECT) == 0); + Require(pthread_mutexattr_getprotocol_nid_postfix(&attr, &protocol) == 0 && protocol == PRIO_PROTECT); Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == 0 && ceiling == sched_get_priority_max_nid_postfix(SCHED_RR)); Require(pthread_mutexattr_setprioceiling_nid_postfix(&attr, 300) == 0); Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == 0 && ceiling == 300); @@ -96,12 +152,14 @@ int main() { Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); Require(pthread_mutex_destroy_nid_postfix(&mutex) == 0); Require(pthread_mutexattr_setprotocol_nid_postfix(&attr, PRIO_PROTECT) == 0); + Require(pthread_mutexattr_getprotocol_nid_postfix(&attr, &protocol) == 0 && protocol == PRIO_PROTECT); Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == 0 && ceiling == sched_get_priority_max_nid_postfix(SCHED_RR)); Require(pthread_mutexattr_setprotocol_nid_postfix(&attr, PRIO_NONE) == 0); + Require(pthread_mutexattr_getprotocol_nid_postfix(&attr, &protocol) == 0 && protocol == PRIO_NONE); Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == GUEST_EINVAL); + Require(Throws([&] { pthread_mutexattr_getprotocol_nid_postfix(&attr, nullptr); })); Require(Throws([&] { pthread_mutexattr_getprioceiling_nid_postfix(&attr, nullptr); })); Require(pthread_mutexattr_destroy_nid_postfix(&attr) == 0); - 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..91c219a1cb 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); @@ -21,6 +23,8 @@ int APS5_VABI pthread_rwlock_tryrdlock_nid_postfix(PthreadRwlock* rwlock); int APS5_VABI pthread_rwlock_trywrlock_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_rwlock_reltimedrdlock_np_nid_postfix(PthreadRwlock* rwlock, KernelUseconds usec); +int APS5_VABI pthread_rwlock_reltimedwrlock_np_nid_postfix(PthreadRwlock* rwlock, KernelUseconds usec); int APS5_VABI scePthreadRwlockInit(PthreadRwlock* rwlock, const PthreadRwlockattr* attr, const char* name); int APS5_VABI scePthreadRwlockDestroy(PthreadRwlock* rwlock); int APS5_VABI scePthreadRwlockRdlock(PthreadRwlock* rwlock); @@ -39,6 +43,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,10 +314,95 @@ static void CheckWaitingWriterHoldsBackNewReaders() { Require(pthread_rwlock_destroy_nid_postfix(&recursive) == 0); } +using RelativeLock = int (APS5_VABI *)(PthreadRwlock*, KernelUseconds); + +static void ExpectRelativeTimeout(RelativeLock lock, PthreadRwlock* rwlock) { + const auto start = std::chrono::steady_clock::now(); + Require(lock(rwlock, 20000) == GUEST_ETIMEDOUT); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); + Require(lock(rwlock, 0) == GUEST_ETIMEDOUT); +} + +static void CheckRelativeTimeouts() { + PthreadRwlock fresh[2] = {}; + Require(pthread_rwlock_reltimedrdlock_np_nid_postfix(&fresh[0], 0) == 0); + Require(pthread_rwlock_reltimedwrlock_np_nid_postfix(&fresh[1], 0) == 0); + for (auto& lock : fresh) { + Require(lock != nullptr && lock != DestroyedRwlock); + Require(pthread_rwlock_unlock_nid_postfix(&lock) == 0); + Require(pthread_rwlock_destroy_nid_postfix(&lock) == 0); + Require(pthread_rwlock_reltimedrdlock_np_nid_postfix(&lock, 0) == GUEST_EINVAL); + Require(pthread_rwlock_reltimedwrlock_np_nid_postfix(&lock, 0) == GUEST_EINVAL); + } + + PthreadRwlock rwlock = nullptr; + Require(pthread_rwlock_reltimedwrlock_np_nid_postfix(&rwlock, 5000000) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + + Holder reader{&rwlock, false}; + Start(reader); + Require(pthread_rwlock_reltimedrdlock_np_nid_postfix(&rwlock, 0) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + ExpectRelativeTimeout(pthread_rwlock_reltimedwrlock_np_nid_postfix, &rwlock); + reader.release.store(true); + Require(pthread_rwlock_reltimedwrlock_np_nid_postfix(&rwlock, 5000000) == 0); + Require(scePthreadJoin(reader.thread, nullptr) == SCE_OK); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + + Holder writer{&rwlock, true}; + Start(writer); + ExpectRelativeTimeout(pthread_rwlock_reltimedrdlock_np_nid_postfix, &rwlock); + ExpectRelativeTimeout(pthread_rwlock_reltimedwrlock_np_nid_postfix, &rwlock); + writer.release.store(true); + Require(pthread_rwlock_reltimedrdlock_np_nid_postfix(&rwlock, 5000000) == 0); + Require(scePthreadJoin(writer.thread, nullptr) == SCE_OK); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 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(); + CheckRelativeTimeouts(); const KernelTimespec invalid{0, NANOS_PER_SECOND}; PthreadRwlock fresh[4] = {}; Require(pthread_rwlock_tryrdlock_nid_postfix(&fresh[0]) == 0); @@ -379,10 +469,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/GuestPthreadSchedparam.cpp b/core/libs/tests/GuestPthreadSchedparam.cpp index 6b6b07b2b8..171d3fc5be 100644 --- a/core/libs/tests/GuestPthreadSchedparam.cpp +++ b/core/libs/tests/GuestPthreadSchedparam.cpp @@ -1,5 +1,7 @@ #include "SceTypes.hpp" #include +#include +#include #include extern "C" { @@ -9,24 +11,53 @@ Pthread APS5_VABI scePthreadSelf(void); int APS5_VABI scePthreadGetschedparam(Pthread thread, int* policy, KernelSchedParam* param); int APS5_VABI scePthreadSetschedparam(Pthread thread, int policy, const KernelSchedParam* param); int APS5_VABI pthread_getschedparam_nid_postfix(Pthread thread, int* policy, KernelSchedParam* param); +int APS5_VABI pthread_getprio_nid_postfix(Pthread thread); +int* APS5_VABI __error_nid_postfix(); int APS5_VABI pthread_setschedparam_nid_postfix(Pthread thread, int policy, const KernelSchedParam* param); int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr); +int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr); +int APS5_VABI scePthreadAttrGetschedpolicy(const PthreadAttr* attr, int* policy); +int APS5_VABI scePthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedParam* param); int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy); +int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); } static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003); static constexpr int GUEST_EINVAL = 22; -static constexpr int SCHED_FIFO = 1; -static constexpr int SCHED_OTHER = 2; -static constexpr int SCHED_RR = 3; +static constexpr int GUEST_ESRCH = 3; +static constexpr int GUEST_SCHED_FIFO = 1; +static constexpr int GUEST_SCHED_OTHER = 2; +static constexpr int GUEST_SCHED_RR = 3; static constexpr int EXPLICIT_SCHED = 0; static constexpr int INHERIT_SCHED = 4; static void Require(bool value) { if (!value) std::abort(); } +static void RequireAttributes(Pthread thread, int policy, int priority) { + *__error_nid_postfix() = 1234; + Require(pthread_getprio_nid_postfix(thread) == priority); + Require(*__error_nid_postfix() == 1234); + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + for (auto query : {scePthreadAttrGet, pthread_attr_get_np_nid_postfix}) { + for (int previous : {GUEST_SCHED_FIFO, GUEST_SCHED_RR}) { + Require(scePthreadAttrSetschedpolicy(&attr, previous) == SCE_OK); + Require(query(thread, &attr) == SCE_OK); + int current = -1; + KernelSchedParam param{}; + Require(scePthreadAttrGetschedpolicy(&attr, ¤t) == SCE_OK); + Require(scePthreadAttrGetschedparam(&attr, ¶m) == SCE_OK); + Require(current == policy && param.sched_priority == priority); + } + } + Require(scePthreadAttrDestroy(&attr) == SCE_OK); +} + static void RequireSched(int policy, int priority) { int current = -1; KernelSchedParam param{}; @@ -36,6 +67,7 @@ static void RequireSched(int policy, int priority) { param.sched_priority = -1; Require(pthread_getschedparam_nid_postfix(scePthreadSelf(), ¤t, ¶m) == 0); Require(current == policy && param.sched_priority == priority); + RequireAttributes(scePthreadSelf(), policy, priority); } static int SceSet(int policy, int priority) { @@ -52,36 +84,44 @@ static void* APS5_VABI Run(void*) { int policy = -1; KernelSchedParam param{}; Require(scePthreadGetschedparam(scePthreadSelf(), &policy, ¶m) == SCE_OK); - Require(policy == SCHED_FIFO); + Require(policy == GUEST_SCHED_FIFO); for (int priority : {256, 767, 896, 959}) { - Require(SceSet(SCHED_FIFO, priority) == SCE_OK); - RequireSched(SCHED_FIFO, priority); + Require(SceSet(GUEST_SCHED_FIFO, priority) == SCE_OK); + RequireSched(GUEST_SCHED_FIFO, priority); } for (int priority : {-1, 0, 255, 768, 895, 960, 1023, 4096}) { - Require(SceSet(SCHED_FIFO, priority) == SCE_KERNEL_ERROR_EINVAL); - Require(PosixSet(SCHED_FIFO, priority) == GUEST_EINVAL); - RequireSched(SCHED_FIFO, 959); + Require(SceSet(GUEST_SCHED_FIFO, priority) == SCE_KERNEL_ERROR_EINVAL); + Require(PosixSet(GUEST_SCHED_FIFO, priority) == GUEST_EINVAL); + RequireSched(GUEST_SCHED_FIFO, 959); } - Require(SceSet(SCHED_RR, 300) == SCE_OK); - RequireSched(SCHED_RR, 300); - for (int other : {-1, 0, SCHED_OTHER, 4}) { + Require(SceSet(GUEST_SCHED_RR, 300) == SCE_OK); + RequireSched(GUEST_SCHED_RR, 300); + for (int other : {-1, 0, GUEST_SCHED_OTHER, 4}) { Require(SceSet(other, 700) == SCE_KERNEL_ERROR_EINVAL); Require(PosixSet(other, 700) == GUEST_EINVAL); - RequireSched(SCHED_RR, 300); + RequireSched(GUEST_SCHED_RR, 300); } - Require(PosixSet(SCHED_FIFO, 700) == 0); - RequireSched(SCHED_FIFO, 700); - Require(scePthreadSetschedparam(scePthreadSelf(), SCHED_FIFO, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(PosixSet(GUEST_SCHED_FIFO, 700) == 0); + RequireSched(GUEST_SCHED_FIFO, 700); + Require(scePthreadSetschedparam(scePthreadSelf(), GUEST_SCHED_FIFO, nullptr) == SCE_KERNEL_ERROR_EINVAL); return nullptr; } +struct PolicyRequest { + std::promise ready; + std::future released; +}; + static void* APS5_VABI ReportPolicy(void* arg) { + auto& request = *static_cast(arg); int policy = -1; KernelSchedParam param{}; Require(scePthreadGetschedparam(scePthreadSelf(), &policy, ¶m) == SCE_OK); - *static_cast(arg) = policy; + RequireAttributes(scePthreadSelf(), policy, param.sched_priority); + request.ready.set_value(policy); + request.released.wait(); return nullptr; } @@ -90,19 +130,87 @@ static int CreatedPolicy(int inheritSched, int policy) { Require(scePthreadAttrInit(&attr) == SCE_OK); Require(scePthreadAttrSetinheritsched(&attr, inheritSched) == SCE_OK); Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_OK); - int reported = -1; + std::promise release; + PolicyRequest request{{}, release.get_future()}; + auto ready = request.ready.get_future(); Pthread thread = nullptr; - Require(scePthreadCreate(&thread, &attr, ReportPolicy, &reported, "policy") == SCE_OK); + Require(scePthreadCreate(&thread, &attr, ReportPolicy, &request, "policy") == SCE_OK); + const int reported = ready.get(); + int current = -1; + KernelSchedParam param{}; + Require(scePthreadGetschedparam(thread, ¤t, ¶m) == SCE_OK); + Require(current == reported); + RequireAttributes(thread, reported, param.sched_priority); + for (int updated : {GUEST_SCHED_RR, GUEST_SCHED_FIFO}) { + const KernelSchedParam requested{450}; + Require(scePthreadSetschedparam(thread, updated, &requested) == SCE_OK); + RequireAttributes(thread, updated, requested.sched_priority); + } + release.set_value(); Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(scePthreadAttrSetschedpolicy(&attr, GUEST_SCHED_RR) == SCE_OK); + Require(scePthreadAttrGet(thread, &attr) == SCE_KERNEL_ERROR_ESRCH); + current = -1; + Require(scePthreadAttrGetschedpolicy(&attr, ¤t) == SCE_OK && current == GUEST_SCHED_RR); Require(scePthreadAttrDestroy(&attr) == SCE_OK); return reported; } +static void* APS5_VABI ReportPriority(void* arg) { + int policy = -1; + KernelSchedParam param{}; + Require(scePthreadGetschedparam(scePthreadSelf(), &policy, ¶m) == SCE_OK); + *static_cast(arg) = param.sched_priority; + return nullptr; +} + +static int CreatedPriority(const PthreadAttr* attr) { + int reported = -1; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, attr, ReportPriority, &reported, "priority") == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + return reported; +} + +static int CreatedPriority(int inheritSched, const KernelSchedParam* param) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(scePthreadAttrSetinheritsched(&attr, inheritSched) == SCE_OK); + Require(scePthreadAttrSetschedpolicy(&attr, GUEST_SCHED_RR) == SCE_OK); + if (param) Require(scePthreadAttrSetschedparam(&attr, param) == SCE_OK); + const int reported = CreatedPriority(&attr); + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + return reported; +} + +static void* APS5_VABI Spawn(void*) { + const KernelSchedParam param{300}; + for (int priority : {450, 600}) { + Require(SceSet(GUEST_SCHED_FIFO, priority) == SCE_OK); + Require(CreatedPriority(INHERIT_SCHED, ¶m) == priority); + Require(CreatedPriority(INHERIT_SCHED, nullptr) == priority); + Require(CreatedPriority(nullptr) == priority); + Require(CreatedPriority(EXPLICIT_SCHED, ¶m) == 300); + Require(CreatedPriority(EXPLICIT_SCHED, nullptr) == 700); + } + Require(SceSet(GUEST_SCHED_RR, 300) == SCE_OK); + Require(CreatedPolicy(INHERIT_SCHED, GUEST_SCHED_FIFO) == GUEST_SCHED_RR); + return nullptr; +} + int main() { + *__error_nid_postfix() = 1234; + Require(pthread_getprio_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(pthread_getprio_nid_postfix(reinterpret_cast(std::uintptr_t{1})) == -1 && *__error_nid_postfix() == GUEST_ESRCH); Pthread thread = nullptr; Require(scePthreadCreate(&thread, nullptr, Run, nullptr, "schedparam") == SCE_OK); + const Pthread retired = thread; + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + *__error_nid_postfix() = 1234; + Require(pthread_getprio_nid_postfix(retired) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(CreatedPolicy(EXPLICIT_SCHED, GUEST_SCHED_RR) == GUEST_SCHED_RR); + Require(CreatedPolicy(EXPLICIT_SCHED, GUEST_SCHED_FIFO) == GUEST_SCHED_FIFO); + Require(CreatedPolicy(INHERIT_SCHED, GUEST_SCHED_RR) == GUEST_SCHED_FIFO); + Require(scePthreadCreate(&thread, nullptr, Spawn, nullptr, "spawn") == SCE_OK); Require(scePthreadJoin(thread, nullptr) == SCE_OK); - Require(CreatedPolicy(EXPLICIT_SCHED, SCHED_RR) == SCHED_RR); - Require(CreatedPolicy(EXPLICIT_SCHED, SCHED_FIFO) == SCHED_FIFO); - Require(CreatedPolicy(INHERIT_SCHED, SCHED_RR) == SCHED_FIFO); } diff --git a/core/libs/tests/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..f27a089c63 100644 --- a/core/libs/tests/GuestQsort.cpp +++ b/core/libs/tests/GuestQsort.cpp @@ -1,15 +1,60 @@ +#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*)); +int APS5_VABI qsort_s_nid_postfix(void*, std::size_t, std::size_t, int (APS5_VABI*)(const void*, const void*, void*), void*); +void* APS5_VABI set_constraint_handler_s_nid_postfix(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; @@ -27,6 +72,86 @@ int APS5_VABI CompareFirstByte(const void* left, const void* right) { return (first > second) - (first < second); } +int APS5_VABI CompareKeysWithDirection(const void* left, const void* right, void* context) { + return *static_cast(context) * CompareKeys(left, right); +} + +bool ExpectOrderS(const char* name, const std::vector& keys, const std::vector& tags) { + std::vector elements; + for (std::size_t index = 0; index < keys.size(); ++index) elements.push_back({keys[index], static_cast(index)}); + int direction = 1; + if (qsort_s_nid_postfix(elements.data(), elements.size(), sizeof(Element), CompareKeysWithDirection, &direction) != 0) { + std::fprintf(stderr, "%s: qsort_s failed\n", name); + return false; + } + for (std::size_t index = 0; index < tags.size(); ++index) { + if (elements[index].tag != tags[index]) { + std::fprintf(stderr, "%s: qsort_s element %zu has tag %d, expected %d\n", name, index, elements[index].tag, tags[index]); + return false; + } + } + return true; +} + +int constraintCalls = 0; +int constraintError = 0; +const char* constraintMessage = nullptr; + +void APS5_VABI CountConstraint(const char* message, void*, int error) { + ++constraintCalls; + constraintMessage = message; + constraintError = error; +} + +bool ExpectConstraint(const char* name, void* base, std::size_t count, std::size_t size, bool comparator) { + int direction = 1; + const int calls = constraintCalls; + const int result = qsort_s_nid_postfix(base, count, size, comparator ? CompareKeysWithDirection : nullptr, &direction); + if (result != 22 || constraintCalls != calls + 1 || constraintError != 22 || !constraintMessage || std::strcmp(constraintMessage, name) != 0) { + std::fprintf(stderr, "qsort_s did not report \"%s\" through the installed handler (result %d, calls %d)\n", name, result, constraintCalls - calls); + return false; + } + return true; +} + +bool ExpectQsortSConstraints() { + void* previous = set_constraint_handler_s_nid_postfix(reinterpret_cast(CountConstraint)); + Element element{}; + constexpr std::size_t huge = (static_cast(-1) >> 1) + 1; + bool correct = ExpectConstraint("qsort_s : n > RSIZE_MAX", &element, huge, sizeof(Element), true); + correct &= ExpectConstraint("qsort_s : es > RSIZE_MAX", &element, 1, huge, true); + correct &= ExpectConstraint("qsort_s : a == NULL", nullptr, 2, sizeof(Element), true); + correct &= ExpectConstraint("qsort_s : cmp == NULL", &element, 2, sizeof(Element), false); + if (set_constraint_handler_s_nid_postfix(previous) != reinterpret_cast(CountConstraint)) { + std::fprintf(stderr, "set_constraint_handler_s did not return the installed handler\n"); + correct = false; + } + return correct; +} + +bool ExpectQsortS() { + std::vector elements; + for (int index = 0; index < 50; ++index) elements.push_back({(index * 13) % 17, index}); + int direction = -1; + if (qsort_s_nid_postfix(elements.data(), elements.size(), sizeof(Element), CompareKeysWithDirection, &direction) != 0) return false; + for (std::size_t index = 1; index < elements.size(); ++index) { + if (elements[index - 1].key < elements[index].key) { + std::fprintf(stderr, "qsort_s with a descending context is not sorted at %zu\n", index); + return false; + } + } + if (qsort_s_nid_postfix(nullptr, 0, sizeof(Element), nullptr, nullptr) != 0) { + std::fprintf(stderr, "qsort_s of no elements failed\n"); + return false; + } + unsigned char bytes[] = {3, 1, 2}; + if (qsort_s_nid_postfix(bytes, 3, 0, CompareKeysWithDirection, &direction) != 0 || bytes[0] != 3 || bytes[1] != 1 || bytes[2] != 2) { + std::fprintf(stderr, "qsort_s with zero-size elements changed them\n"); + return false; + } + return true; +} + bool ExpectOrder(const char* name, const std::vector& keys, const std::vector& tags) { std::vector elements; for (std::size_t index = 0; index < keys.size(); ++index) elements.push_back({keys[index], static_cast(index)}); @@ -66,12 +191,41 @@ 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() { bool correct = true; correct &= ExpectOrder("insertion sort", {1, 0, 1, 0}, {1, 3, 0, 2}); correct &= ExpectOrder("median of three", {2, 1, 2, 0, 1, 2, 0, 1, 0, 2, 1, 0}, {11, 3, 6, 8, 10, 1, 7, 4, 9, 5, 0, 2}); + correct &= ExpectOrderS("insertion sort", {1, 0, 1, 0}, {1, 3, 0, 2}); + correct &= ExpectOrderS("median of three", {2, 1, 2, 0, 1, 2, 0, 1, 0, 2, 1, 0}, {11, 3, 6, 8, 10, 1, 7, 4, 9, 5, 0, 2}); + correct &= ExpectQsortS(); + correct &= ExpectQsortSConstraints(); std::vector keys; for (int index = 0; index < 48; ++index) keys.push_back((index * 7 + 3) % 5); correct &= ExpectOrder("pseudomedian of nine", keys, @@ -80,5 +234,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 ee3bde7706..fd74e36771 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -33,13 +34,14 @@ int APS5_VABI scePthreadRwlockDestroy(PthreadRwlock* rwlock); int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock); int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock); int APS5_VABI sceKernelSyncOnAddressWait(std::uint32_t* address, std::uint32_t expected, const KernelUseconds* timeout, const char* name); +int APS5_VABI sceKernelSyncOnAddressWake(void* address, std::int32_t count); } static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003); static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003c); static constexpr int SIGUSR1 = 30; -static constexpr int Repeats = 100; +static constexpr int Repeats = 50; static constexpr std::size_t R12Offset = 0xa0; static constexpr std::size_t R13Offset = 0xa8; static constexpr std::size_t Xmm8Offset = 0x140 + 160 + 8 * 16; @@ -170,7 +172,7 @@ static void* APS5_VABI HostBlocked(void* arg) { return nullptr; } -static constexpr int LeavingRounds = 200; +static constexpr int LeavingRounds = 100; static std::atomic leavingRound{0}; static void* APS5_VABI Leaving(void* arg) { @@ -196,7 +198,7 @@ static LONG CALLBACK ContinueRaised(EXCEPTION_POINTERS* info) { return EXCEPTION_CONTINUE_EXECUTION; } -static constexpr int SelfingRounds = 100; +static constexpr int SelfingRounds = 50; static std::atomic selfSink{nullptr}; static void* APS5_VABI Selfing(void* arg) { @@ -237,7 +239,7 @@ static void* APS5_VABI Looping(void* arg) { return nullptr; } -static constexpr int VectorRounds = 100; +static constexpr int VectorRounds = 50; static constexpr std::size_t VectorBytes = 16 * 32; alignas(32) static std::uint8_t vectorPattern[VectorBytes]; alignas(32) static std::uint8_t vectorResult[VectorBytes]; @@ -402,6 +404,87 @@ static void NestedWaitKeepsOuterWake() { Require(sceKernelDeleteSema(sem) == 0); } +#ifdef _WIN32 +static constexpr int AddressRounds = 50; +static std::uint32_t addressWord = 0; +static std::uint32_t otherAddressWord = 0; +static std::atomic holdingEntered{false}; +static std::atomic holdingRelease{false}; +static std::atomic holdingLeft{false}; +static std::atomic holdingThread; + +static void APS5_VABI HoldingHandler(int signum, void*) { + Require(signum == SIGUSR1); + holdingThread.store(std::this_thread::get_id()); + holdingEntered.store(true); + while (!holdingRelease.load()) std::this_thread::yield(); + holdingLeft.store(true); +} + +static void* APS5_VABI AddressWaking(void* arg) { + auto& worker = *static_cast(arg); + worker.id = std::this_thread::get_id(); + worker.started.store(true); + while (!worker.stop.load()) Require(sceKernelSyncOnAddressWake(&addressWord, 1) == 0); + return nullptr; +} + +static void* APS5_VABI AddressWaiting(void* arg) { + auto& worker = *static_cast(arg); + worker.id = std::this_thread::get_id(); + worker.started.store(true); + while (!worker.stop.load()) { + KernelUseconds timeout = 20; + const int result = sceKernelSyncOnAddressWait(&addressWord, 0, &timeout, "address waiting"); + Require(result == 0 || result == SCE_KERNEL_ERROR_ETIMEDOUT); + } + return nullptr; +} + +static bool AwaitWithin(const std::atomic& flag, std::chrono::milliseconds limit) { + const auto deadline = std::chrono::steady_clock::now() + limit; + while (!flag.load()) { + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::yield(); + } + return true; +} + +static void RequireInRound(bool value, const char* name, int round, const char* check) { + if (value) return; + std::fprintf(stderr, "%s round %d: %s\n", name, round, check); + std::fflush(stderr); + std::abort(); +} + +static void HandlerLeavesAddressLock(PthreadEntry entry, const char* name) { + Worker worker; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, entry, &worker, name) == 0); + while (!worker.started.load()) std::this_thread::yield(); + for (int round = 0; round < AddressRounds; ++round) { + holdingEntered.store(false); + holdingLeft.store(false); + holdingRelease.store(false); + RequireInRound(sceKernelRaiseException(thread, SIGUSR1) == 0, name, round, "the raise failed"); + RequireInRound(AwaitWithin(holdingEntered, std::chrono::seconds(5)), name, round, "the handler did not run within 5 s"); + RequireInRound(holdingThread.load() == worker.id, name, round, "the handler ran on another thread"); + std::atomic woke{false}; + std::thread waker([&] { + Require(sceKernelSyncOnAddressWake(&otherAddressWord, 1) == 0); + woke.store(true); + }); + const bool returned = AwaitWithin(woke, std::chrono::milliseconds(500)); + holdingRelease.store(true); + waker.join(); + RequireInRound(returned, name, round, "a wake on another address blocked for 500 ms while the handler ran"); + RequireInRound(AwaitWithin(holdingLeft, std::chrono::seconds(5)), name, round, "the handler did not return within 5 s of its release"); + } + worker.stop.store(true); + Require(scePthreadJoin(thread, nullptr) == 0); +} +#endif + static void ExpectDelivery(int before, std::thread::id thread) { for (int attempt = 0; attempt < 5000 && calls.load() == before; ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(1)); Require(calls.load() == before + 1); @@ -530,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); @@ -660,4 +744,11 @@ int main() { NestedWaitKeepsLaterWaiters(); NestedWaitKeepsOuterWake(); Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0); + +#ifdef _WIN32 + Require(sceKernelInstallExceptionHandler(SIGUSR1, reinterpret_cast(&HoldingHandler)) == 0); + HandlerLeavesAddressLock(AddressWaking, "address waking"); + HandlerLeavesAddressLock(AddressWaiting, "address waiting"); + Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0); +#endif } diff --git a/core/libs/tests/GuestRaiseInHostCode.cpp b/core/libs/tests/GuestRaiseInHostCode.cpp new file mode 100644 index 0000000000..0a41f83ca8 --- /dev/null +++ b/core/libs/tests/GuestRaiseInHostCode.cpp @@ -0,0 +1,74 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread*, const PthreadAttr*, PthreadEntry, void*, const char*); +int APS5_VABI scePthreadJoin(Pthread, void**); +int APS5_VABI sceKernelInstallExceptionHandler(int, void*); +int APS5_VABI sceKernelRemoveExceptionHandler(int); +int APS5_VABI sceKernelRaiseException(Pthread, int); +} + +static constexpr int SigUsr1 = 30; +static constexpr int Raises = 100; +static void Require(bool value) { if (!value) std::abort(); } + +static std::FILE* shared = nullptr; +static std::atomic deliveries{0}; +static std::atomic released{0}; +static std::atomic stop{false}; +static std::atomic started{false}; + +static void APS5_VABI Handler(int, void*) { + const auto delivery = deliveries.fetch_add(1) + 1; + while (released.load() < delivery) std::this_thread::yield(); +} + +static void* APS5_VABI Writer(void*) { + const std::string block(1u << 20u, 'x'); + started.store(true); + while (!stop.load()) { + Require(std::fwrite(block.data(), 1, block.size(), shared) == block.size()); + Require(std::fseek(shared, 0, SEEK_SET) == 0); + volatile std::uint64_t spin = 0; + for (std::uint64_t i = 0; i < 200000; ++i) spin = spin + i; + } + return nullptr; +} + +int main() { + std::atomic done{false}; + std::thread watchdog([&done] { + for (int i = 0; i < 20000 && !done.load(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (!done.load()) std::abort(); + }); + shared = std::tmpfile(); + Require(shared != nullptr); + Require(sceKernelInstallExceptionHandler(SigUsr1, reinterpret_cast(&Handler)) == 0); + Pthread writer = nullptr; + Require(scePthreadCreate(&writer, nullptr, Writer, nullptr, "host writer") == 0); + while (!started.load()) std::this_thread::yield(); + auto longest = std::chrono::steady_clock::duration::zero(); + for (int raise = 1; raise <= Raises; ++raise) { + const auto raisedAt = std::chrono::steady_clock::now(); + Require(sceKernelRaiseException(writer, SigUsr1) == 0); + longest = std::max(longest, std::chrono::steady_clock::now() - raisedAt); + while (deliveries.load() < raise) std::this_thread::yield(); + Require(std::fputc('y', shared) == 'y'); + released.store(raise); + } + stop.store(true); + Require(scePthreadJoin(writer, nullptr) == 0); + Require(sceKernelRemoveExceptionHandler(SigUsr1) == 0); + std::fclose(shared); + std::printf("longest raise: %.2f ms\n", std::chrono::duration(longest).count()); + done.store(true); + watchdog.join(); +} diff --git a/core/libs/tests/GuestRaiseInSystemCall.cpp b/core/libs/tests/GuestRaiseInSystemCall.cpp new file mode 100644 index 0000000000..834594d260 --- /dev/null +++ b/core/libs/tests/GuestRaiseInSystemCall.cpp @@ -0,0 +1,86 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread*, const PthreadAttr*, PthreadEntry, void*, const char*); +int APS5_VABI scePthreadJoin(Pthread, void**); +int APS5_VABI sceKernelInstallExceptionHandler(int, void*); +int APS5_VABI sceKernelRemoveExceptionHandler(int); +int APS5_VABI sceKernelRaiseException(Pthread, int); +} + +static constexpr int SigUsr1 = 30; +static constexpr int Raises = 10; +static constexpr DWORD PipeBytes = 4096; +static void Require(bool value) { if (!value) std::abort(); } + +static std::FILE* shared = nullptr; +static std::atomic deliveries{0}; +static std::atomic released{0}; +static std::atomic stop{false}; +static std::atomic blocks{0}; + +static void APS5_VABI Handler(int, void*) { + const auto delivery = deliveries.fetch_add(1) + 1; + while (released.load() < delivery) std::this_thread::yield(); +} + +static void* APS5_VABI Writer(void*) { + const std::string block(16u * PipeBytes, 'x'); + while (!stop.load()) { + Require(std::fwrite(block.data(), 1, block.size(), shared) == block.size()); + blocks.fetch_add(1); + volatile std::uint64_t spin = 0; + for (std::uint64_t i = 0; i < 200000; ++i) spin = spin + i; + } + return nullptr; +} + +int main() { + std::atomic done{false}; + std::thread watchdog([&done] { + for (int i = 0; i < 25000 && !done.load(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (!done.load()) std::abort(); + }); + HANDLE read = nullptr; + HANDLE write = nullptr; + Require(CreatePipe(&read, &write, nullptr, PipeBytes) != 0); + shared = _fdopen(_open_osfhandle(reinterpret_cast(write), _O_WRONLY | _O_BINARY), "wb"); + Require(shared != nullptr); + std::atomic draining{true}; + std::thread reader([&] { + char chunk[PipeBytes]; + DWORD got = 0; + while (ReadFile(read, chunk, sizeof(chunk), &got, nullptr) != 0 && got != 0) { + if (draining.load()) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + }); + Require(sceKernelInstallExceptionHandler(SigUsr1, reinterpret_cast(&Handler)) == 0); + Pthread writer = nullptr; + Require(scePthreadCreate(&writer, nullptr, Writer, nullptr, "pipe writer") == 0); + while (blocks.load() == 0) std::this_thread::yield(); + for (int raise = 1; raise <= Raises; ++raise) { + Require(sceKernelRaiseException(writer, SigUsr1) == 0); + while (deliveries.load() < raise) std::this_thread::yield(); + Require(std::fputc('y', shared) == 'y'); + released.store(raise); + } + stop.store(true); + draining.store(false); + Require(scePthreadJoin(writer, nullptr) == 0); + Require(sceKernelRemoveExceptionHandler(SigUsr1) == 0); + std::fclose(shared); + reader.join(); + CloseHandle(read); + done.store(true); + watchdog.join(); +} diff --git a/core/libs/tests/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..71511241be 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; @@ -59,7 +61,7 @@ static void CheckRand48() { Require(jrand48_nid_postfix(seed) == 1517566982 && SeedIs(seed, 0x2a23, 0x3c06, 0x5a74), "jrand48 third value"); Require(erand48_nid_postfix(seed) == 0x72534abf62f2p-48 && SeedIs(seed, 0x62f2, 0x4abf, 0x7253), "erand48 fourth value"); Require(nrand48_nid_postfix(seed) == 684387517 && SeedIs(seed, 0x8d15, 0xd97a, 0x5195), "nrand48 fifth value"); - Require(jrand48_nid_postfix(seed) == -487786166 && SeedIs(seed, 0xeffc, 0xf94a, 0xe2ec), "jrand48 negative value"); + Require(jrand48_nid_postfix(seed) == 3807181130 && SeedIs(seed, 0xeffc, 0xf94a, 0xe2ec), "jrand48 keeps bit 31 without sign extension"); std::uint16_t zero[3] = {}; Require(nrand48_nid_postfix(zero) == 0 && SeedIs(zero, 0x000b, 0, 0), "rand48 zero seed adds the increment"); @@ -74,20 +76,47 @@ 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"); + + std::uint16_t measured[3] = {0x000b, 0x0016, 0x0021}; + Require(jrand48_nid_postfix(measured) == 3702088075 && SeedIs(measured, 0xe6ba, 0x618b, 0xdca9), "jrand48 matches the console for a result with bit 31 set"); + std::uint16_t topBit[3] = {0, 0, 0x8000}; + Require(jrand48_nid_postfix(topBit) == 2147483648, "jrand48 matches the console for 0x80000000"); + std::uint16_t ones[3] = {0xffff, 0xffff, 0xffff}; + Require(jrand48_nid_postfix(ones) == 4294582547, "jrand48 matches the console for an all-ones seed"); + constexpr std::array consoleMrand48 {178800969, 1952030186, 3585512650, 1443049011, 2428758494, 7588830}; + srand48_nid_postfix(1); + for (const std::int64_t expected : consoleMrand48) Require(mrand48_nid_postfix() == expected, "mrand48 matches the console after srand48(1)"); + #ifndef _WIN32 std::uint16_t guest[3] = {0x1234, 0x5678, 0x9abc}; unsigned short host[3] = {0x1234, 0x5678, 0x9abc}; for (int round = 0; round < 64; ++round) { Require(erand48_nid_postfix(guest) == ::erand48(host), "erand48 matches the host"); Require(nrand48_nid_postfix(guest) == ::nrand48(host), "nrand48 matches the host"); - Require(jrand48_nid_postfix(guest) == ::jrand48(host), "jrand48 matches the host"); + Require(jrand48_nid_postfix(guest) == static_cast(::jrand48(host)), "jrand48 matches the host's 32 bits"); } ::srand48(1234567); srand48_nid_postfix(1234567); for (int round = 0; round < 32; ++round) { Require(drand48_nid_postfix() == ::drand48(), "drand48 matches the host"); Require(lrand48_nid_postfix() == ::lrand48(), "lrand48 matches the host"); - Require(mrand48_nid_postfix() == ::mrand48(), "mrand48 matches the host"); + Require(mrand48_nid_postfix() == static_cast(::mrand48()), "mrand48 matches the host's 32 bits"); } #endif } diff --git a/core/libs/tests/GuestRazorCpu.cpp b/core/libs/tests/GuestRazorCpu.cpp index 85f04438fc..69f35dbfa0 100644 --- a/core/libs/tests/GuestRazorCpu.cpp +++ b/core/libs/tests/GuestRazorCpu.cpp @@ -1,19 +1,33 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include +#include +#include extern "C" { std::uint32_t APS5_VABI sceRazorCpuIsCapturing(void); -int APS5_VABI sceRazorCpuJobManagerDispatch(const void* args); -int APS5_VABI sceRazorCpuJobManagerJob(const void* args); -int APS5_VABI sceRazorCpuJobManagerSequence(const void* args); +int APS5_VABI sceRazorCpuJobManagerDispatch(const char* label, std::uint16_t firstId, std::uint16_t secondId, std::uint16_t firstCount, std::uint16_t secondCount, std::uint64_t firstValue, std::uint64_t secondValue); +int APS5_VABI sceRazorCpuJobManagerJob(std::uint16_t firstId, std::uint16_t secondId, std::uint16_t thirdId, std::uint16_t event); +int APS5_VABI sceRazorCpuJobManagerSequence(const char* label, std::uint16_t firstId, std::uint16_t secondId, std::uint16_t event); 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); +bool APS5_VABI sceRazorCpuFlushOccurred(std::uint64_t* timeSpentInFlush); int APS5_VABI sceRazorCpuPlotValue(const char* series, float value); int APS5_VABI sceRazorCpuWriteBookmark(const char* label, const char* description); int APS5_VABI sceRazorCpuSync(void); +void APS5_VABI sceRazorCpuDisableFiberUserMarkers(void); +int APS5_VABI sceRazorCpuInitDataTags(void* buffer, std::size_t size); +int APS5_VABI sceRazorCpuShutdownDataTags(void); +std::size_t APS5_VABI sceRazorCpuGetDataTagStorageSize(std::uint32_t tagCount); +int APS5_VABI sceRazorCpuTagArray(const char* label, std::uint32_t category, void* array, std::size_t bufferSize, std::size_t elementSize); +int APS5_VABI sceRazorCpuTagBuffer(const char* label, std::uint32_t category, void* buffer, std::size_t bufferSize); +int APS5_VABI sceRazorCpuResizeTaggedBuffer(std::uint32_t category, void* buffer, std::size_t newBufferSize); +int APS5_VABI sceRazorCpuUnTagBuffer(std::uint32_t category, void* buffer); +int APS5_VABI sceRazorCpuBeginLogicalFileAccess(const char* filename, std::uint64_t tag, std::uint64_t size, std::uint8_t operation); +int APS5_VABI sceRazorCpuEndLogicalFileAccess(void); } namespace { @@ -24,9 +38,10 @@ void Require(bool value) { if (!value) std::abort(); } int main() { Require(sceRazorCpuIsCapturing() == 0); - Require(sceRazorCpuJobManagerDispatch(nullptr) == 0); - Require(sceRazorCpuJobManagerJob(nullptr) == 0); - Require(sceRazorCpuJobManagerSequence(nullptr) == 0); + Require(sceRazorCpuJobManagerDispatch(nullptr, 1, 2, 3, 4, 5, 6) == 0); + Require(sceRazorCpuJobManagerJob(1, 2, 3, 16) == 0); + Require(sceRazorCpuJobManagerSequence(nullptr, 1, 2, 3) == 0); + Require(sceRazorCpuNamedSync("frame") == 0); Require(sceRazorCpuPushMarkerStatic("outer", 0x80ffffffu, 2) == 0); Require(sceRazorCpuPushMarkerStatic("inner", 0x80ffffffu, 2) == 0); Require(sceRazorCpuPopMarker() == 0); @@ -37,7 +52,7 @@ int main() { Require(sceRazorCpuIsCapturing() == 0); std::uint64_t timeSpentInFlush = 0x123456789abcdef0ull; Require(sceRazorCpuFlushOccurred(&timeSpentInFlush) == 0); - Require(timeSpentInFlush == 0); + Require(timeSpentInFlush == 0x123456789abcdef0ull); Require(sceRazorCpuFlushOccurred(nullptr) == 0); Require(sceRazorCpuPlotValue("read time (ms)", 1.5f) == 0); Require(sceRazorCpuWriteBookmark("read timeout", "lba=0x10") == 0); @@ -45,4 +60,39 @@ int main() { Require(sceRazorCpuSync() == 0); Require(sceRazorCpuSync() == 0); Require(sceRazorCpuIsCapturing() == 0); + + sceRazorCpuDisableFiberUserMarkers(); + sceRazorCpuDisableFiberUserMarkers(); + Require(sceRazorCpuPushMarker("thread", 0x80ffffffu, 0) == 0); + Require(sceRazorCpuPopMarker() == 0); + + Require(sceRazorCpuBeginLogicalFileAccess("data/level.pak", 0x0123456789abcdefull, 4096, 0) == 0); + Require(sceRazorCpuEndLogicalFileAccess() == 0); + Require(sceRazorCpuBeginLogicalFileAccess("save/slot0.bin", 0, 512, 1) == 0); + Require(sceRazorCpuEndLogicalFileAccess() == 0); + Require(sceRazorCpuEndLogicalFileAccess() == 0); + + unsigned char storage[256]; + std::memset(storage, 0x5a, sizeof(storage)); + int elements[16]{}; + unsigned char buffer[64]{}; + Require(sceRazorCpuInitDataTags(storage, sizeof(storage)) == 0); + Require(sceRazorCpuTagArray("elements", 7, elements, sizeof(elements), sizeof(elements[0])) == 0); + Require(sceRazorCpuTagBuffer("buffer", 7, buffer, sizeof(buffer)) == 0); + Require(sceRazorCpuResizeTaggedBuffer(7, buffer, sizeof(buffer) / 2) == 0); + Require(sceRazorCpuUnTagBuffer(7, buffer) == 0); + Require(sceRazorCpuUnTagBuffer(7, elements) == 0); + Require(sceRazorCpuShutdownDataTags() == 0); + for (const unsigned char byte : storage) Require(byte == 0x5a); + Require(sceRazorCpuTagBuffer("untracked", 3, buffer, sizeof(buffer)) == 0); + Require(sceRazorCpuUnTagBuffer(3, buffer) == 0); + Require(sceRazorCpuIsCapturing() == 0); + + bool threw = false; + try { + sceRazorCpuGetDataTagStorageSize(16); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); } diff --git a/core/libs/tests/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/GuestRegMgr.cpp b/core/libs/tests/GuestRegMgr.cpp new file mode 100644 index 0000000000..dfdf074535 --- /dev/null +++ b/core/libs/tests/GuestRegMgr.cpp @@ -0,0 +1,27 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +std::int32_t APS5_VABI sceRegMgrNonSysGetInt(std::int64_t key, std::int32_t unk, std::int32_t* data); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static bool ReportsNotImplemented(const char* name, TCall call) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string(error.what()) == std::string(name) + " not implemented"; + } + return false; +} + +int main() { + std::int32_t value = 0; + Require(ReportsNotImplemented("sceRegMgrNonSysGetInt", [&] { sceRegMgrNonSysGetInt(0, 0, &value); })); + Require(ReportsNotImplemented("sceRegMgrNonSysGetInt", [&] { sceRegMgrNonSysGetInt(-1, 1, nullptr); })); +} diff --git a/core/libs/tests/GuestRenameDot.cpp b/core/libs/tests/GuestRenameDot.cpp new file mode 100644 index 0000000000..ed44b81e35 --- /dev/null +++ b/core/libs/tests/GuestRenameDot.cpp @@ -0,0 +1,104 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#ifndef _WIN32 +#include +#endif + +extern "C" { +int APS5_VABI mkdir_nid_postfix(const char*, int); +int APS5_VABI rmdir_nid_postfix(const char*); +int APS5_VABI rename_nid_postfix(const char*, const char*); +int APS5_VABI sceKernelRename(const char*, const char*); +int APS5_VABI stat_nid_postfix(const char*, void*); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int ErrorNoEntry = 2; +constexpr int ErrorNotDirectory = 20; +constexpr int ErrorInvalid = 22; +constexpr int ErrorLoop = 62; +constexpr char Source[] = "anyps5_rename_dot_source"; +constexpr char Target[] = "anyps5_rename_dot_target"; +constexpr char Moved[] = "anyps5_rename_dot_moved"; +constexpr char PlainFile[] = "anyps5_rename_dot_file"; +constexpr char Loop[] = "anyps5_rename_dot_loop"; + +void Cleanup() { + rmdir_nid_postfix(Source); + rmdir_nid_postfix(Target); + rmdir_nid_postfix(Moved); + std::remove(PlainFile); + std::remove(Loop); +} + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + Cleanup(); + std::abort(); +} + +bool Fails(long result, int error) { return result == -1 && *__error_nid_postfix() == error; } + +bool Exists(const char* path) { + unsigned char status[256]; + return stat_nid_postfix(path, status) == 0; +} + +} + +int main() { + Cleanup(); + Require(mkdir_nid_postfix(Source, 0755) == 0 && mkdir_nid_postfix(Target, 0755) == 0, "mkdir failed"); + const std::string dotted = std::string(Source) + "/."; + const std::string dottedSlash = std::string(Source) + "/./"; + const std::string intoTarget = std::string(Target) + "/."; + Require(Fails(rename_nid_postfix(dotted.c_str(), Moved), ErrorInvalid), "rename from dir/. must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix(dottedSlash.c_str(), Moved), ErrorInvalid), "rename from dir/./ must fail with EINVAL (22)"); + Require(Exists(Source) && !Exists(Moved), "a failed rename from dir/. must leave the directory in place"); + Require(Fails(rename_nid_postfix(Source, intoTarget.c_str()), ErrorInvalid), "rename to dir/. must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix(".", Moved), ErrorInvalid), "rename of . must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix(Source, "."), ErrorInvalid), "rename onto . must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix("anyps5_rename_dot_missing/.", Moved), ErrorNoEntry), "rename from missing/. must fail with ENOENT (2)"); + const std::string dotDot = std::string(Source) + "/.."; + const std::string dotDotSlash = std::string(Source) + "/../"; + Require(Fails(rename_nid_postfix(dotDot.c_str(), Moved), ErrorInvalid), "rename from dir/.. must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix(dotDotSlash.c_str(), Moved), ErrorInvalid), "rename from dir/../ must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix(Source, (std::string(Target) + "/..").c_str()), ErrorInvalid), "rename to dir/.. must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix("..", Moved), ErrorInvalid), "rename of .. must fail with EINVAL (22)"); + Require(Fails(rename_nid_postfix("anyps5_rename_dot_missing/..", Moved), ErrorNoEntry), "rename from missing/.. must fail with ENOENT (2)"); + const std::string backslashDot = std::string(Source) + "\\."; + const std::string backslashDotDot = std::string(Source) + "\\.."; + Require(Fails(rename_nid_postfix(backslashDot.c_str(), Moved), ErrorInvalid), "rename from dir\\. must fail with EINVAL (22), the guest resolver turns the backslash into a separator"); + Require(Fails(rename_nid_postfix(backslashDotDot.c_str(), Moved), ErrorInvalid), "rename from dir\\.. must fail with EINVAL (22)"); + Require(Exists(Source) && !Exists(Moved), "failed renames from dir\\. and dir\\.. must leave the directory in place"); + Require(sceKernelRename(dotDot.c_str(), Moved) == static_cast(0x80020016u), "sceKernelRename from dir/.. must return EINVAL"); + Require(sceKernelRename(dotted.c_str(), Moved) == static_cast(0x80020016u), "sceKernelRename from dir/. must return EINVAL"); + Require(sceKernelRename(Source, intoTarget.c_str()) == static_cast(0x80020016u), "sceKernelRename to dir/. must return EINVAL"); + Require(Exists(Source) && Exists(Target) && !Exists(Moved), "failed renames must change nothing"); + + { + std::FILE* file = std::fopen(PlainFile, "w"); + Require(file != nullptr && std::fclose(file) == 0, "creating the plain file failed"); + } + const std::string throughFile = std::string(PlainFile) + "/."; + Require(Fails(rename_nid_postfix(throughFile.c_str(), Moved), ErrorNotDirectory), "rename from file/. must fail with ENOTDIR (20)"); + Require(Fails(rename_nid_postfix(Source, throughFile.c_str()), ErrorNotDirectory), "rename to file/. must fail with ENOTDIR (20)"); + std::remove(PlainFile); +#ifndef _WIN32 + Require(::symlink(Loop, Loop) == 0, "creating the symbolic link loop failed"); + const std::string throughLoop = std::string(Loop) + "/."; + Require(Fails(rename_nid_postfix(throughLoop.c_str(), Moved), ErrorLoop), "rename from loop/. must fail with ELOOP (62)"); + std::remove(Loop); +#endif + + Require(rename_nid_postfix(Source, Moved) == 0 && Exists(Moved) && !Exists(Source), "an ordinary rename must still work"); + Require(sceKernelRename(Moved, Source) == 0 && Exists(Source) && !Exists(Moved), "an ordinary sceKernelRename must still work"); + Cleanup(); + return 0; +} diff --git a/core/libs/tests/GuestRenderingWait.cpp b/core/libs/tests/GuestRenderingWait.cpp index 71d3cbe986..a17c039713 100644 --- a/core/libs/tests/GuestRenderingWait.cpp +++ b/core/libs/tests/GuestRenderingWait.cpp @@ -9,6 +9,12 @@ #include #include extern "C" std::uint32_t APS5_VABI sceAgcDriverGetWaitRenderingPacketSizeInDwords(); +extern "C" std::uint32_t APS5_VABI sceAgcDriverGetSetFlipPacketSizeInDwords(); +extern "C" std::uint32_t APS5_VABI sceAgcDriverGetSetWorkloadsActivePacketSize(); +extern "C" std::uint32_t APS5_VABI sceAgcDriverGetSetWorkloadCompletePacketSize(); +extern "C" std::uint32_t APS5_VABI sceAgcDriverGetSetWorkloadStreamInactivePacketSize(); +extern "C" std::uint32_t APS5_VABI sceAgcDriverNopDataGetPacketSize(std::uint32_t); +extern "C" std::uint32_t APS5_VABI sceAgcDriverUserDataGetPacketSize(std::uint32_t); extern "C" std::uint32_t APS5_VABI sceAgcDriverWaitUntilSafeForRendering(std::uint32_t**, std::uint32_t, std::uint32_t, std::uint32_t, int); static void Require(bool value) { if (!value) std::abort(); } class Output final : public AgcDriver::IVideoOutput, public AgcDriver::IRenderingWait, @@ -54,6 +60,16 @@ int main() { auto* cursor = words.data(); const auto size = sceAgcDriverGetWaitRenderingPacketSizeInDwords(); Require(size == AgcDriver::RenderingWaitPacketWords); + const auto flipPacketSize = sceAgcDriverGetSetFlipPacketSizeInDwords(); + Require(flipPacketSize == AgcDriver::FlipPacketWords); + Require(flipPacketSize == 6); + Require(sceAgcDriverGetSetWorkloadsActivePacketSize() == 18); + Require(sceAgcDriverGetSetWorkloadCompletePacketSize() == 12); + Require(sceAgcDriverGetSetWorkloadStreamInactivePacketSize() == 9); + Require(sceAgcDriverNopDataGetPacketSize(0) == 2 && sceAgcDriverNopDataGetPacketSize(1) == 3 && sceAgcDriverNopDataGetPacketSize(4) == 3); + Require(sceAgcDriverNopDataGetPacketSize(5) == 4 && sceAgcDriverNopDataGetPacketSize(0xffffffffu) == 0x40000002u); + Require(sceAgcDriverUserDataGetPacketSize(0) == 3 && sceAgcDriverUserDataGetPacketSize(1) == 4 && sceAgcDriverUserDataGetPacketSize(4) == 4); + Require(sceAgcDriverUserDataGetPacketSize(5) == 9 && sceAgcDriverUserDataGetPacketSize(0xffffffffu) == 0x40000007u); bool rejected = false; try { sceAgcDriverWaitUntilSafeForRendering(&cursor, size - 1, 0, 7, 0); } catch (const std::invalid_argument&) { rejected = true; } @@ -62,6 +78,7 @@ int main() { Require(cursor == words.data() + size); Require(words[0] == AgcDriver::RenderingWaitPacketHeader && words[1] == 7 && words[2] == 0 && words[3] == 0); const std::uint32_t flip[] = {AgcDriver::FlipPacketHeader, 7, 0, 1, 0, 0}; + Require(sizeof(flip) / sizeof(flip[0]) == flipPacketSize); for (auto word : flip) *cursor++ = word; Require(words.back() == 0x12345678); Packet packet{words.data(), 10, 0, {}}; diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 96273f2b73..13d1678125 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -17,6 +17,9 @@ int APS5_VABI sceRtcGetTickResolution(void); int APS5_VABI sceRtcGetTick(const RtcDateTime*, RtcTick*); int APS5_VABI sceRtcSetTick(RtcDateTime*, const RtcTick*); int APS5_VABI sceRtcGetCurrentTick(RtcTick*); +int APS5_VABI sceRtcGetCurrentNetworkTick(RtcTick*); +int APS5_VABI sceRtcGetCurrentClock(RtcDateTime*, int); +int APS5_VABI sceRtcGetCurrentClockLocalTime(RtcDateTime*); int APS5_VABI sceRtcConvertUtcToLocalTime(const RtcTick*, RtcTick*); int APS5_VABI sceRtcConvertLocalTimeToUtc(const RtcTick*, RtcTick*); int APS5_VABI sceRtcGetTime_t(const RtcDateTime*, std::int64_t*); @@ -25,17 +28,47 @@ int APS5_VABI sceRtcGetWin32FileTime(const RtcDateTime*, std::uint64_t*); int APS5_VABI sceRtcSetWin32FileTime(RtcDateTime*, std::uint64_t); int APS5_VABI sceRtcGetDosTime(const RtcDateTime*, std::uint32_t*); int APS5_VABI sceRtcSetDosTime(RtcDateTime*, std::uint32_t); +int APS5_VABI sceRtcGetYear(const RtcDateTime*); +int APS5_VABI sceRtcSetYear(RtcDateTime*, int); +int APS5_VABI sceRtcGetMonth(const RtcDateTime*); +int APS5_VABI sceRtcSetMonth(RtcDateTime*, int); +int APS5_VABI sceRtcGetDay(const RtcDateTime*); +int APS5_VABI sceRtcSetDay(RtcDateTime*, int); +int APS5_VABI sceRtcGetHour(const RtcDateTime*); +int APS5_VABI sceRtcSetHour(RtcDateTime*, int); +int APS5_VABI sceRtcGetMinute(const RtcDateTime*); +int APS5_VABI sceRtcSetMinute(RtcDateTime*, int); +int APS5_VABI sceRtcGetSecond(const RtcDateTime*); +int APS5_VABI sceRtcSetSecond(RtcDateTime*, int); +int APS5_VABI sceRtcGetMicrosecond(const RtcDateTime*); +int APS5_VABI sceRtcSetMicrosecond(RtcDateTime*, int); int APS5_VABI sceRtcFormatRFC3339(char*, const RtcTick*, int); int APS5_VABI sceRtcParseRFC3339(RtcTick*, const char*); int APS5_VABI sceRtcParseDateTime(RtcTick*, const char*); int APS5_VABI sceRtcTickAddTicks(RtcTick*, const RtcTick*, std::int64_t); +int APS5_VABI sceRtcTickAddMicroseconds(RtcTick*, const RtcTick*, std::int64_t); int APS5_VABI sceRtcTickAddSeconds(RtcTick*, const RtcTick*, std::int64_t); +int APS5_VABI sceRtcTickAddMinutes(RtcTick*, const RtcTick*, std::int64_t); +int APS5_VABI sceRtcTickAddHours(RtcTick*, const RtcTick*, std::int32_t); int APS5_VABI sceRtcTickAddDays(RtcTick*, const RtcTick*, std::int32_t); +int APS5_VABI sceRtcTickAddWeeks(RtcTick*, const RtcTick*, std::int32_t); int APS5_VABI sceRtcTickAddMonths(RtcTick*, const RtcTick*, std::int32_t); int APS5_VABI sceRtcTickAddYears(RtcTick*, const RtcTick*, std::int16_t); int APS5_VABI sceRtcFormatRFC3339LocalTime(char*, const RtcTick*); int APS5_VABI sceRtcFormatRFC2822(char*, const RtcTick*, int); int APS5_VABI sceRtcFormatRFC2822LocalTime(char*, const RtcTick*); +int APS5_VABI sceRtcFormatRFC3339Precise(char*, const RtcTick*, int, int); +int APS5_VABI sceRtcFormatRFC3339PreciseLocalTime(char*, const RtcTick*, int); +int APS5_VABI sceRtcCompareTick(const RtcTick*, const RtcTick*); +int APS5_VABI sceRtcGetCurrentAdNetworkTick(RtcTick*); +int APS5_VABI sceRtcGetCurrentRawNetworkTick(RtcTick*); +int APS5_VABI sceRtcGetCurrentDebugNetworkTick(RtcTick*); +int APS5_VABI sceRtcSetCurrentTick(const RtcTick*); +int APS5_VABI sceRtcSetCurrentNetworkTick(const RtcTick*); +int APS5_VABI sceRtcSetCurrentAdNetworkTick(const RtcTick*); +int APS5_VABI sceRtcSetCurrentDebugNetworkTick(const RtcTick*); +int APS5_VABI sceRtcInit(void); +int APS5_VABI sceRtcEnd(void); } static void Require(bool value) { if (!value) std::abort(); } @@ -55,6 +88,31 @@ static bool Equal(const RtcDateTime& left, const RtcDateTime& right) { && left.minute == right.minute && left.second == right.second && left.microsecond == right.microsecond; } +enum class Add { Ticks, Microseconds, Seconds, Minutes, Hours, Days, Weeks, Months, Years }; + +struct AddCase { + Add function; + std::uint64_t source; + std::int64_t amount; + bool written; + std::uint64_t result; +}; + +static int CallAdd(Add function, RtcTick* destination, const RtcTick* source, std::int64_t amount) { + switch (function) { + case Add::Ticks: return sceRtcTickAddTicks(destination, source, amount); + case Add::Microseconds: return sceRtcTickAddMicroseconds(destination, source, amount); + case Add::Seconds: return sceRtcTickAddSeconds(destination, source, amount); + case Add::Minutes: return sceRtcTickAddMinutes(destination, source, amount); + case Add::Hours: return sceRtcTickAddHours(destination, source, static_cast(amount)); + case Add::Days: return sceRtcTickAddDays(destination, source, static_cast(amount)); + case Add::Weeks: return sceRtcTickAddWeeks(destination, source, static_cast(amount)); + case Add::Months: return sceRtcTickAddMonths(destination, source, static_cast(amount)); + case Add::Years: return sceRtcTickAddYears(destination, source, static_cast(amount)); + } + std::abort(); +} + int main() { constexpr int invalidPointer = static_cast(0x80B50002); constexpr int invalidValue = static_cast(0x80B50003); @@ -71,8 +129,12 @@ int main() { Require(sceRtcGetTickResolution() == 1000000); Require(sceRtcIsLeapYear(2000) == 1 && sceRtcIsLeapYear(1900) == 0 && sceRtcIsLeapYear(2024) == 1); Require(sceRtcIsLeapYear(0) == invalidYear); + Require(sceRtcIsLeapYear(10000) == 1); + Require(sceRtcIsLeapYear(9999) == 0 && sceRtcIsLeapYear(9996) == 1); Require(sceRtcGetDaysInMonth(2023, 2) == 28 && sceRtcGetDaysInMonth(2024, 2) == 29 && sceRtcGetDaysInMonth(2024, 4) == 30); Require(sceRtcGetDaysInMonth(2024, 13) == invalidMonth); + Require(sceRtcGetDaysInMonth(10000, 1) == 31); + Require(sceRtcGetDaysInMonth(9999, 1) == 31); Require(sceRtcGetDayOfWeek(1, 1, 1) == 1 && sceRtcGetDayOfWeek(2026, 9, 26) == 6); Require(sceRtcGetDayOfWeek(2023, 2, 29) == invalidDay); @@ -98,6 +160,13 @@ int main() { tick.tick = maxTick + 1; Require(sceRtcSetTick(&converted, &tick) == invalidValue); + Require(sceRtcGetCurrentClock(nullptr, 0) == invalidPointer); + Require(sceRtcGetCurrentClock(&converted, 0) == 0 && sceRtcCheckValid(&converted) == 0); + Require(sceRtcGetCurrentClock(&converted, 60) == 0 && sceRtcCheckValid(&converted) == 0); + Require(sceRtcGetCurrentClock(&converted, -300) == 0 && sceRtcCheckValid(&converted) == 0); + Require(sceRtcGetCurrentClockLocalTime(nullptr) == invalidPointer); + Require(sceRtcGetCurrentClockLocalTime(&converted) == 0 && sceRtcCheckValid(&converted) == 0); + RtcDateTime epoch{1970, 1, 1, 0, 0, 0, 0}; std::int64_t seconds = -1; Require(sceRtcGetTime_t(&epoch, &seconds) == 0 && seconds == 0); @@ -123,6 +192,59 @@ int main() { Require(sceRtcGetDosTime(&dosEarly, &dosTime) == invalidYear && dosTime == 0); const RtcDateTime dosLate{2108, 1, 1, 0, 0, 0, 0}; Require(sceRtcGetDosTime(&dosLate, &dosTime) == invalidYear && dosTime == 0xff9fbf7du); + + Require(sceRtcGetYear(nullptr) == invalidPointer); + Require(sceRtcSetYear(nullptr, 2024) == invalidPointer); + Require(sceRtcSetYear(&converted, 0) == invalidYear); + Require(sceRtcSetYear(&converted, 10000) == invalidYear); + Require(sceRtcSetYear(&converted, 1) == 0 && sceRtcGetYear(&converted) == 1); + Require(sceRtcSetYear(&converted, 9999) == 0 && sceRtcGetYear(&converted) == 9999); + Require(sceRtcSetYear(&converted, 2026) == 0 && sceRtcGetYear(&converted) == 2026); + + Require(sceRtcGetMonth(nullptr) == invalidPointer); + Require(sceRtcSetMonth(nullptr, 6) == invalidPointer); + Require(sceRtcSetMonth(&converted, 0) == invalidMonth); + Require(sceRtcSetMonth(&converted, 13) == invalidMonth); + Require(sceRtcSetMonth(&converted, 1) == 0 && sceRtcGetMonth(&converted) == 1); + Require(sceRtcSetMonth(&converted, 12) == 0 && sceRtcGetMonth(&converted) == 12); + + Require(sceRtcGetDay(nullptr) == invalidPointer); + Require(sceRtcSetDay(nullptr, 15) == invalidPointer); + Require(sceRtcSetDay(&converted, 0) == invalidDay); + Require(sceRtcSetDay(&converted, 32) == invalidDay); + Require(sceRtcSetDay(&converted, 1) == 0 && sceRtcGetDay(&converted) == 1); + Require(sceRtcSetDay(&converted, 31) == 0 && sceRtcGetDay(&converted) == 31); + + Require(sceRtcGetHour(nullptr) == invalidPointer); + Require(sceRtcSetHour(nullptr, 12) == invalidPointer); + Require(sceRtcSetHour(&converted, -1) == invalidHour); + Require(sceRtcSetHour(&converted, 24) == invalidHour); + Require(sceRtcSetHour(&converted, 0) == 0 && sceRtcGetHour(&converted) == 0); + Require(sceRtcSetHour(&converted, 23) == 0 && sceRtcGetHour(&converted) == 23); + + constexpr int invalidMinute = static_cast(0x80B5000C); + constexpr int invalidSecond = static_cast(0x80B5000D); + + Require(sceRtcGetMinute(nullptr) == invalidPointer); + Require(sceRtcSetMinute(nullptr, 30) == invalidPointer); + Require(sceRtcSetMinute(&converted, -1) == invalidMinute); + Require(sceRtcSetMinute(&converted, 60) == invalidMinute); + Require(sceRtcSetMinute(&converted, 0) == 0 && sceRtcGetMinute(&converted) == 0); + Require(sceRtcSetMinute(&converted, 59) == 0 && sceRtcGetMinute(&converted) == 59); + + Require(sceRtcGetSecond(nullptr) == invalidPointer); + Require(sceRtcSetSecond(nullptr, 45) == invalidPointer); + Require(sceRtcSetSecond(&converted, -1) == invalidSecond); + Require(sceRtcSetSecond(&converted, 60) == invalidSecond); + Require(sceRtcSetSecond(&converted, 0) == 0 && sceRtcGetSecond(&converted) == 0); + Require(sceRtcSetSecond(&converted, 59) == 0 && sceRtcGetSecond(&converted) == 59); + + Require(sceRtcGetMicrosecond(nullptr) == invalidPointer); + Require(sceRtcSetMicrosecond(nullptr, 123456) == invalidPointer); + Require(sceRtcSetMicrosecond(&converted, -1) == invalidMicrosecond); + Require(sceRtcSetMicrosecond(&converted, 1000000) == invalidMicrosecond); + Require(sceRtcSetMicrosecond(&converted, 0) == 0 && sceRtcGetMicrosecond(&converted) == 0); + Require(sceRtcSetMicrosecond(&converted, 999999) == 0 && sceRtcGetMicrosecond(&converted) == 999999); converted = RtcDateTime{1, 1, 1, 1, 1, 1, 1}; Require(sceRtcSetDosTime(&converted, 0x585d645cu) == 0 && Equal(converted, RtcDateTime{2024, 2, 29, 12, 34, 56, 0})); Require(sceRtcSetDosTime(&converted, 0x7f9fbf7du) == 0 && Equal(converted, RtcDateTime{2043, 12, 31, 23, 59, 58, 0})); @@ -151,7 +273,14 @@ int main() { Require(std::memcmp(text, original, sizeof(text)) == 0); } Require(sceRtcFormatRFC3339(nullptr, &tick, 1440) == invalidPointer); - Require(sceRtcFormatRFC3339(text, nullptr, 1440) == invalidPointer); + Require(sceRtcFormatRFC3339(text, nullptr, 1440) == invalidValue); + { + RtcTick before{}, after{}, parsed{}; + Require(sceRtcGetCurrentTick(&before) == 0); + Require(sceRtcFormatRFC3339(text, nullptr, 0) == 0); + Require(sceRtcGetCurrentTick(&after) == 0); + Require(sceRtcParseRFC3339(&parsed, text) == 0 && parsed.tick + 10000 > before.tick && parsed.tick <= after.tick); + } Require(sceRtcParseRFC3339(&tick, "2024-02-29T14:04:56.789+01:30") == 0 && tick.tick == leapDayTick); Require(sceRtcParseRFC3339(&tick, "2024-02-29t12:34:56.789z") == 0 && tick.tick == leapDayTick); Require(sceRtcParseRFC3339(&tick, "1970-01-01T00:00:00Z") == 0 && tick.tick == unixEpochTick); @@ -246,6 +375,11 @@ int main() { {"Thu, 01 Mar 2024 12:34:56 nzdt", 0, march, 780}, {"Thu, 01 Mar 2024 12:34:56 KST", 0, march, 540}, {"Thu, 01 Mar 2024 12:34:56 HST", 0, march, 420}, + {"Thu, 01 Mar 2024 12:34:56 DNT", 0, march, 60}, + {"Thu, 01 Mar 2024 12:34:56 NFT", 0, march, -150}, + {"Thu, 01 Mar 2024 12:34:56 NOR", 0, march, 60}, + {"Thu, 01 Mar 2024 12:34:56 SET", 0, march, 60}, + {"Thu, 01 Mar 2024 12:34:56 SETX", 0, march, 60}, {"Thu, 01 Mar 2024 12:34:56 Jt", 0, march, 450}, {"Thu, 01 Mar 2024 12:34:56 ut", 0, march, -420}, {"Thu, 01 Mar 2024 12:34:56 xT", 0, march, 0}, @@ -306,6 +440,8 @@ int main() { const char* name; std::int64_t minutes; } additionalZones[] = { + {"LIGT", 600}, {"AWSST", 540}, {"CETDST", 120}, + {"DNT", 60}, {"NOR", 60}, {"SET", 60}, {"NFT", -150}, {"SWT", 60}, {"MEZ", 60}, {"WETDST", 60}, {"METDST", 120}, {"EETDST", 180}, {"AESST", 660}, {"ACSST", 630}, {"CAST", 570}, {"CADT", 630}, {"SAST", 570}, {"SADT", 630}, {"WST", 480}, {"WDT", 540}, }; @@ -331,6 +467,20 @@ int main() { Require(tick.tick == at(2000, 3, 1, 8, 9, 10) - static_cast(zone.minutes * 60000000)); } + const struct { + const char* name; + std::int64_t minutes; + } secondLetterZones[] = { + {"MT", 0}, {"mt", 510}, {"IT", 0}, {"it", 210}, + {"BT", 0}, {"bt", 180}, {"NT", 0}, {"nt", -660}, + }; + for (const auto& zone : secondLetterZones) { + char text[80]; + std::snprintf(text, sizeof(text), "Thu, 29 Feb 2024 12:34:56 %s", zone.name); + Require(sceRtcParseDateTime(&tick, text) == 0); + Require(tick.tick == leapDayTick - 789000ull - static_cast(zone.minutes * 60000000)); + } + RtcTick source{leapDayTick}; RtcTick result{}; Require(sceRtcTickAddSeconds(&result, &source, 3600) == 0 && result.tick == leapDayTick + 3600000000ull); @@ -342,31 +492,20 @@ int main() { Require(sceRtcGetTick(&endOfJanuary, &source) == 0); Require(sceRtcTickAddMonths(&result, &source, 1) == 0); Require(sceRtcSetTick(&converted, &result) == 0 && Equal(converted, RtcDateTime{2024, 2, 29, 8, 0, 0, 0})); - Require(sceRtcTickAddMonths(&result, &source, -12 * 2024) == invalidValue); source.tick = 5; - Require(sceRtcTickAddTicks(&result, &source, -6) == invalidValue); Require(sceRtcTickAddTicks(&result, &source, -5) == 0 && result.tick == 0); source.tick = maxTick; - Require(sceRtcTickAddTicks(&result, &source, 1) == invalidValue); Require(sceRtcTickAddTicks(nullptr, &source, 1) == invalidPointer); - - const std::uint64_t invalidTicks[] = {maxTick + 1, std::numeric_limits::max()}; - for (std::uint64_t invalidTick : invalidTicks) { - source.tick = invalidTick; - result.tick = 123; - Require(sceRtcTickAddTicks(&result, &source, 0) == invalidValue && result.tick == 123); - Require(sceRtcTickAddTicks(&result, &source, 1) == invalidValue && result.tick == 123); - Require(sceRtcTickAddTicks(&result, &source, -1) == invalidValue && result.tick == 123); - Require(sceRtcTickAddSeconds(&result, &source, 0) == invalidValue && result.tick == 123); - Require(sceRtcTickAddDays(&result, &source, -1) == invalidValue && result.tick == 123); - Require(sceRtcTickAddMonths(&result, &source, 0) == invalidValue && result.tick == 123); - Require(sceRtcTickAddYears(&result, &source, -1) == invalidValue && result.tick == 123); - Require(sceRtcTickAddTicks(&source, &source, 0) == invalidValue && source.tick == invalidTick); - Require(sceRtcTickAddMonths(&source, &source, -1) == invalidValue && source.tick == invalidTick); - Require(sceRtcTickAddTicks(nullptr, &source, 0) == invalidPointer); - Require(sceRtcTickAddMonths(nullptr, &source, 0) == invalidPointer); - } - + source.tick = leapDayTick; + result.tick = 123; + Require(sceRtcTickAddMicroseconds(&result, &source, 1500) == 0 && result.tick == leapDayTick + 1500); + Require(sceRtcTickAddMinutes(&result, &source, 90) == 0 && result.tick == leapDayTick + 5400000000ull); + Require(sceRtcTickAddHours(&result, &source, -2) == 0 && result.tick == leapDayTick - 7200000000ull); + Require(sceRtcTickAddWeeks(&result, &source, 1) == 0 && result.tick == leapDayTick + 604800000000ull); + Require(sceRtcSetTick(&converted, &result) == 0 && converted.month == 3 && converted.day == 7); + result.tick = 123; + Require(sceRtcTickAddHours(nullptr, &source, 1) == invalidPointer); + Require(sceRtcTickAddMicroseconds(&result, nullptr, 1) == invalidPointer); source.tick = maxTick; Require(sceRtcTickAddTicks(&source, &source, 0) == 0 && source.tick == maxTick); Require(sceRtcTickAddTicks(&source, &source, -1) == 0 && source.tick == maxTick - 1); @@ -374,13 +513,242 @@ int main() { Require(sceRtcTickAddMonths(&source, &source, 0) == 0 && source.tick == maxTick); Require(sceRtcTickAddYears(&result, &source, -1) == 0); Require(sceRtcSetTick(&converted, &result) == 0 && Equal(converted, RtcDateTime{9998, 12, 31, 23, 59, 59, 999999})); - result.tick = 123; - Require(sceRtcTickAddMonths(&result, &source, 1) == invalidValue && result.tick == 123); - Require(sceRtcTickAddTicks(&result, &source, std::numeric_limits::min()) == invalidValue && result.tick == 123); - Require(sceRtcTickAddSeconds(&result, &source, std::numeric_limits::max()) == invalidValue && result.tick == 123); source.tick = 0; Require(sceRtcTickAddMonths(&source, &source, 0) == 0 && source.tick == 0); - Require(sceRtcTickAddMonths(&result, &source, -1) == invalidValue && result.tick == 123); + + constexpr std::int64_t min64 = std::numeric_limits::min(); + constexpr std::int64_t max64 = std::numeric_limits::max(); + constexpr std::uint64_t addSentinel = 0xA5A5A5A5A5A5A5A5ull; + const AddCase addCases[] = { + {Add::Ticks, 0, -1, true, 0xffffffffffffffffull}, + {Add::Ticks, 1ull, -1, true, 0x0000000000000000ull}, + {Add::Ticks, 5, -1, true, 0x0000000000000004ull}, + {Add::Ticks, leapDayTick, 1, true, 0x00e2d2837bd62609ull}, + {Add::Ticks, maxTick - 1ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Ticks, maxTick, 1, true, 0x0461040bcb9f2000ull}, + {Add::Ticks, maxTick + 1, 1, true, 0x0461040bcb9f2001ull}, + {Add::Ticks, ~0ull, 1, true, 0x0000000000000000ull}, + {Add::Ticks, 0, max64, true, 0x7fffffffffffffffull}, + {Add::Ticks, 0, min64, true, 0x8000000000000000ull}, + {Add::Ticks, maxTick, max64, true, 0x8461040bcb9f1ffeull}, + {Add::Ticks, ~0ull, min64, true, 0x7fffffffffffffffull}, + {Add::Microseconds, 0, -1, true, 0xffffffffffffffffull}, + {Add::Microseconds, 1ull, -1, true, 0x0000000000000000ull}, + {Add::Microseconds, 5, -1, true, 0x0000000000000004ull}, + {Add::Microseconds, leapDayTick, 1, true, 0x00e2d2837bd62609ull}, + {Add::Microseconds, maxTick - 1ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Microseconds, maxTick, 1, true, 0x0461040bcb9f2000ull}, + {Add::Microseconds, maxTick + 1, 1, true, 0x0461040bcb9f2001ull}, + {Add::Microseconds, ~0ull, 1, true, 0x0000000000000000ull}, + {Add::Microseconds, 0, max64, true, 0x7fffffffffffffffull}, + {Add::Microseconds, 0, min64, true, 0x8000000000000000ull}, + {Add::Microseconds, maxTick, max64, true, 0x8461040bcb9f1ffeull}, + {Add::Microseconds, ~0ull, min64, true, 0x7fffffffffffffffull}, + {Add::Seconds, 0, -1, true, 0xfffffffffff0bdc0ull}, + {Add::Seconds, 1000000ull, -1, true, 0x0000000000000000ull}, + {Add::Seconds, 999999ull, -1, true, 0xffffffffffffffffull}, + {Add::Seconds, leapDayTick, 1, true, 0x00e2d2837be56848ull}, + {Add::Seconds, maxTick - 1000000ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Seconds, maxTick + 1 - 1000000ull, 1, true, 0x0461040bcb9f2000ull}, + {Add::Seconds, maxTick + 1, 1, true, 0x0461040bcbae6240ull}, + {Add::Seconds, 0ull - 1000000ull, 1, true, 0x0000000000000000ull}, + {Add::Seconds, 0, max64, true, 0xfffffffffff0bdc0ull}, + {Add::Seconds, 0, min64, true, 0x0000000000000000ull}, + {Add::Seconds, maxTick, max64, true, 0x0461040bcb8fddbfull}, + {Add::Seconds, ~0ull, min64, true, 0xffffffffffffffffull}, + {Add::Minutes, 0, -1, true, 0xfffffffffc6c7900ull}, + {Add::Minutes, 60000000ull, -1, true, 0x0000000000000000ull}, + {Add::Minutes, 59999999ull, -1, true, 0xffffffffffffffffull}, + {Add::Minutes, leapDayTick, 1, true, 0x00e2d2837f69ad08ull}, + {Add::Minutes, maxTick - 60000000ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Minutes, maxTick + 1 - 60000000ull, 1, true, 0x0461040bcb9f2000ull}, + {Add::Minutes, maxTick + 1, 1, true, 0x0461040bcf32a700ull}, + {Add::Minutes, 0ull - 60000000ull, 1, true, 0x0000000000000000ull}, + {Add::Minutes, 0, max64, true, 0xfffffffffc6c7900ull}, + {Add::Minutes, 0, min64, true, 0x0000000000000000ull}, + {Add::Minutes, maxTick, max64, true, 0x0461040bc80b98ffull}, + {Add::Minutes, ~0ull, min64, true, 0xffffffffffffffffull}, + {Add::Hours, 0, -1, true, 0xffffffff296c5c00ull}, + {Add::Hours, 3600000000ull, -1, true, 0x0000000000000000ull}, + {Add::Hours, 3599999999ull, -1, true, 0xffffffffffffffffull}, + {Add::Hours, leapDayTick, 1, true, 0x00e2d2845269ca08ull}, + {Add::Hours, maxTick - 3600000000ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Hours, maxTick + 1 - 3600000000ull, 1, true, 0x0461040bcb9f2000ull}, + {Add::Hours, maxTick + 1, 1, true, 0x0461040ca232c400ull}, + {Add::Hours, 0ull - 3600000000ull, 1, true, 0x0000000000000000ull}, + {Add::Hours, 0, 2147483647, true, 0x6b49d1ff296c5c00ull}, + {Add::Hours, 0, -2147483648, true, 0x94b62e0000000000ull}, + {Add::Hours, maxTick, 2147483647, true, 0x6faad60af50b7bffull}, + {Add::Hours, ~0ull, -2147483648, true, 0x94b62dffffffffffull}, + {Add::Days, 0, -1, true, 0xffffffebe228a000ull}, + {Add::Days, 86400000000ull, -1, true, 0x0000000000000000ull}, + {Add::Days, 86399999999ull, -1, true, 0xffffffffffffffffull}, + {Add::Days, leapDayTick, 1, true, 0x00e2d29799ad8608ull}, + {Add::Days, maxTick - 86400000000ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Days, maxTick + 1 - 86400000000ull, 1, true, 0x0461040bcb9f2000ull}, + {Add::Days, maxTick + 1, 1, true, 0x0461041fe9768000ull}, + {Add::Days, 0ull - 86400000000ull, 1, true, 0x0000000000000000ull}, + {Add::Days, 0, 2147483647, true, 0x0eebafebe228a000ull}, + {Add::Days, 0, -2147483648, true, 0xf114500000000000ull}, + {Add::Days, maxTick, 2147483647, true, 0x134cb3f7adc7bfffull}, + {Add::Days, ~0ull, -2147483648, true, 0xf1144fffffffffffull}, + {Add::Weeks, 0, -1, true, 0xffffff732f1c6000ull}, + {Add::Weeks, 604800000000ull, -1, true, 0x0000000000000000ull}, + {Add::Weeks, 604799999999ull, -1, true, 0xffffffffffffffffull}, + {Add::Weeks, leapDayTick, 1, true, 0x00e2d3104cb9c608ull}, + {Add::Weeks, maxTick - 604800000000ull, 1, true, 0x0461040bcb9f1fffull}, + {Add::Weeks, maxTick + 1 - 604800000000ull, 1, true, 0x0461040bcb9f2000ull}, + {Add::Weeks, maxTick + 1, 1, true, 0x046104989c82c000ull}, + {Add::Weeks, 0ull - 604800000000ull, 1, true, 0x0000000000000000ull}, + {Add::Weeks, 0, 2147483647, true, 0x6871cf732f1c6000ull}, + {Add::Weeks, 0, -2147483648, true, 0x978e300000000000ull}, + {Add::Weeks, maxTick, 2147483647, true, 0x6cd2d37efabb7fffull}, + {Add::Weeks, ~0ull, -2147483648, true, 0x978e2fffffffffffull}, + {Add::Months, 0, -1, false, 0}, + {Add::Months, 0, 0, true, 0x0000000000000000ull}, + {Add::Months, 0, 1, true, 0x0000026f9d14a000ull}, + {Add::Months, 0, -12, false, 0}, + {Add::Months, 0, 12, true, 0x00001cae8c13e000ull}, + {Add::Months, 0, 119988, false, 0}, + {Add::Months, 0, 2147483647, false, 0}, + {Add::Months, 0, -2147483648, false, 0}, + {Add::Months, 0, -1239376452, true, 0x017942765fc16000ull}, + {Add::Months, 0, -1239376451, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), -1, true, 0x0000000000000000ull}, + {Add::Months, at(1, 2, 1, 0, 0, 0), 0, true, 0x0000026f9d14a000ull}, + {Add::Months, at(1, 2, 1, 0, 0, 0), 1, true, 0x000004a2e0a32000ull}, + {Add::Months, at(1, 2, 1, 0, 0, 0), -12, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), 12, true, 0x00001f1e29288000ull}, + {Add::Months, at(1, 2, 1, 0, 0, 0), 119988, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), 2147483647, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), -2147483648, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), -1239376452, false, 0}, + {Add::Months, at(1, 2, 1, 0, 0, 0), -1239376451, false, 0}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), -1, true, 0x00e2cdc8a612a000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), 0, true, 0x00e2d03843274000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), 1, true, 0x00e2d27fa48d2000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), -12, true, 0x00e2b389b7136000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), 12, true, 0x00e2ecfaed128000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), 119988, false, 0}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), 2147483647, false, 0}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), -2147483648, false, 0}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), -1239376452, true, 0x025c12c2c0c00000ull}, + {Add::Months, at(2024, 1, 31, 8, 0, 0), -1239376451, false, 0}, + {Add::Months, leapDayTick, -1, true, 0x00e2d013dec18608ull}, + {Add::Months, leapDayTick, 0, true, 0x00e2d2837bd62608ull}, + {Add::Months, leapDayTick, 1, true, 0x00e2d4cadd3c0608ull}, + {Add::Months, leapDayTick, -12, true, 0x00e2b5c0d1eae608ull}, + {Add::Months, leapDayTick, 12, true, 0x00e2ef3207ea0608ull}, + {Add::Months, leapDayTick, 119988, false, 0}, + {Add::Months, leapDayTick, 2147483647, false, 0}, + {Add::Months, leapDayTick, -2147483648, false, 0}, + {Add::Months, leapDayTick, -1239376452, false, 0}, + {Add::Months, leapDayTick, -1239376451, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), -1, true, 0x0460e4eda276a000ull}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), 0, true, 0x0460e75d3f8b4000ull}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), 1, true, 0x0460e9ccdc9fe000ull}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), -12, true, 0x0460caaeb3776000ull}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), 12, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), 119988, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), 2147483647, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), -2147483648, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), -1239376452, false, 0}, + {Add::Months, at(9999, 1, 1, 0, 0, 0), -1239376451, false, 0}, + {Add::Months, maxTick, -1, true, 0x0461019c2e8a7fffull}, + {Add::Months, maxTick, 0, true, 0x0461040bcb9f1fffull}, + {Add::Months, maxTick, 1, false, 0}, + {Add::Months, maxTick, -12, true, 0x0460e75d3f8b3fffull}, + {Add::Months, maxTick, 12, false, 0}, + {Add::Months, maxTick, 119988, false, 0}, + {Add::Months, maxTick, 2147483647, false, 0}, + {Add::Months, maxTick, -2147483648, false, 0}, + {Add::Months, maxTick, -1239376452, false, 0}, + {Add::Months, maxTick, -1239376451, false, 0}, + {Add::Months, maxTick + 1, -1, true, 0x0461019c2e8a8000ull}, + {Add::Months, maxTick + 1, 0, false, 0}, + {Add::Months, maxTick + 1, 1, false, 0}, + {Add::Months, maxTick + 1, -12, true, 0x0460e75d3f8b4000ull}, + {Add::Months, maxTick + 1, 12, false, 0}, + {Add::Months, maxTick + 1, 119988, false, 0}, + {Add::Months, maxTick + 1, 2147483647, false, 0}, + {Add::Months, maxTick + 1, -2147483648, false, 0}, + {Add::Months, maxTick + 1, -1239376452, false, 0}, + {Add::Months, maxTick + 1, -1239376451, false, 0}, + {Add::Months, ~0ull, -1, false, 0}, + {Add::Months, ~0ull, 0, false, 0}, + {Add::Months, ~0ull, 1, false, 0}, + {Add::Months, ~0ull, -12, false, 0}, + {Add::Months, ~0ull, 12, false, 0}, + {Add::Months, ~0ull, 119988, true, 0x02122fd805921fffull}, + {Add::Months, ~0ull, 2147483647, false, 0}, + {Add::Months, ~0ull, -2147483648, false, 0}, + {Add::Months, ~0ull, -1239376452, false, 0}, + {Add::Months, ~0ull, -1239376451, false, 0}, + {Add::Years, 0, -1, false, 0}, + {Add::Years, 0, 0, true, 0x0000000000000000ull}, + {Add::Years, 0, 1, true, 0x00001cae8c13e000ull}, + {Add::Years, 0, 9998, true, 0x0460e75d3f8b4000ull}, + {Add::Years, 0, 32767, false, 0}, + {Add::Years, 0, -32768, false, 0}, + {Add::Years, at(1, 2, 1, 0, 0, 0), -1, false, 0}, + {Add::Years, at(1, 2, 1, 0, 0, 0), 0, true, 0x0000026f9d14a000ull}, + {Add::Years, at(1, 2, 1, 0, 0, 0), 1, true, 0x00001f1e29288000ull}, + {Add::Years, at(1, 2, 1, 0, 0, 0), 9998, true, 0x0460e9ccdc9fe000ull}, + {Add::Years, at(1, 2, 1, 0, 0, 0), 32767, false, 0}, + {Add::Years, at(1, 2, 1, 0, 0, 0), -32768, false, 0}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), -1, true, 0x00e2b389b7136000ull}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), 0, true, 0x00e2d03843274000ull}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), 1, true, 0x00e2ecfaed128000ull}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), 9998, false, 0}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), 32767, false, 0}, + {Add::Years, at(2024, 1, 31, 8, 0, 0), -32768, false, 0}, + {Add::Years, leapDayTick, -1, true, 0x00e2b5c0d1eae608ull}, + {Add::Years, leapDayTick, 0, true, 0x00e2d2837bd62608ull}, + {Add::Years, leapDayTick, 1, true, 0x00e2ef3207ea0608ull}, + {Add::Years, leapDayTick, 9998, false, 0}, + {Add::Years, leapDayTick, 32767, false, 0}, + {Add::Years, leapDayTick, -32768, false, 0}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), -1, true, 0x0460caaeb3776000ull}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), 0, true, 0x0460e75d3f8b4000ull}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), 1, false, 0}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), 9998, false, 0}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), 32767, false, 0}, + {Add::Years, at(9999, 1, 1, 0, 0, 0), -32768, false, 0}, + {Add::Years, maxTick, -1, true, 0x0460e75d3f8b3fffull}, + {Add::Years, maxTick, 0, true, 0x0461040bcb9f1fffull}, + {Add::Years, maxTick, 1, false, 0}, + {Add::Years, maxTick, 9998, false, 0}, + {Add::Years, maxTick, 32767, false, 0}, + {Add::Years, maxTick, -32768, false, 0}, + {Add::Years, maxTick + 1, -1, true, 0x0460e75d3f8b4000ull}, + {Add::Years, maxTick + 1, 0, false, 0}, + {Add::Years, maxTick + 1, 1, false, 0}, + {Add::Years, maxTick + 1, 9998, false, 0}, + {Add::Years, maxTick + 1, 32767, false, 0}, + {Add::Years, maxTick + 1, -32768, false, 0}, + {Add::Years, ~0ull, -1, false, 0}, + {Add::Years, ~0ull, 0, false, 0}, + {Add::Years, ~0ull, 1, false, 0}, + {Add::Years, ~0ull, 9998, true, 0x02121329797e3fffull}, + {Add::Years, ~0ull, 32767, false, 0}, + {Add::Years, ~0ull, -32768, false, 0}, + }; + for (const AddCase& entry : addCases) { + RtcTick destination{addSentinel}; + const RtcTick origin{entry.source}; + Require(CallAdd(entry.function, &destination, &origin, entry.amount) == 0); + Require(destination.tick == (entry.written ? entry.result : addSentinel)); + RtcTick inPlace{entry.source}; + Require(CallAdd(entry.function, &inPlace, &inPlace, entry.amount) == 0); + Require(inPlace.tick == (entry.written ? entry.result : entry.source)); + } + for (Add function : {Add::Ticks, Add::Microseconds, Add::Seconds, Add::Minutes, Add::Hours, Add::Days, Add::Weeks, Add::Months, Add::Years}) { + RtcTick destination{addSentinel}; + const RtcTick origin{5}; + Require(CallAdd(function, nullptr, &origin, 1) == invalidPointer); + Require(CallAdd(function, &destination, nullptr, 1) == invalidPointer && destination.tick == addSentinel); + Require(CallAdd(function, nullptr, nullptr, 1) == invalidPointer); + } RtcTick now{}; Require(sceRtcGetCurrentTick(&now) == 0 && now.tick > leapDayTick); @@ -419,7 +787,15 @@ int main() { Require(std::memcmp(rfc2822, original, sizeof(rfc2822)) == 0); } Require(sceRtcFormatRFC2822(nullptr, &tick, 0) == invalidPointer); - Require(sceRtcFormatRFC2822(rfc2822, nullptr, 0) == invalidPointer); + { + RtcTick before{}, after{}, parsed{}; + Require(sceRtcGetCurrentTick(&before) == 0); + Require(sceRtcFormatRFC2822(rfc2822, nullptr, 60) == 0); + Require(sceRtcGetCurrentTick(&after) == 0); + Require(std::strlen(rfc2822) == 31 && std::strcmp(rfc2822 + 26, "+0100") == 0); + Require(sceRtcParseDateTime(&parsed, rfc2822) == 0 && parsed.tick + 1000000 > before.tick && parsed.tick <= after.tick); + Require(sceRtcFormatRFC2822(rfc2822, nullptr, 1440) == invalidValue); + } SetTimeZone("XXX-5:30"); Require(sceRtcFormatRFC2822LocalTime(rfc2822, &tick) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 18:04:56 +0530") == 0); @@ -431,9 +807,16 @@ int main() { Require(sceRtcFormatRFC2822LocalTime(rfc2822, &tick) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 12:34:56 +0000") == 0); Require(sceRtcFormatRFC3339LocalTime(text, &tick) == 0 && std::strcmp(text, "2024-02-29T12:34:56.78Z") == 0); Require(sceRtcFormatRFC2822LocalTime(nullptr, &tick) == invalidPointer); - Require(sceRtcFormatRFC2822LocalTime(rfc2822, nullptr) == invalidPointer); Require(sceRtcFormatRFC3339LocalTime(nullptr, &tick) == invalidPointer); - Require(sceRtcFormatRFC3339LocalTime(text, nullptr) == invalidPointer); + { + RtcTick before{}, after{}, parsed{}; + Require(sceRtcGetCurrentTick(&before) == 0); + Require(sceRtcFormatRFC2822LocalTime(rfc2822, nullptr) == 0); + Require(sceRtcFormatRFC3339LocalTime(text, nullptr) == 0); + Require(sceRtcGetCurrentTick(&after) == 0); + Require(sceRtcParseDateTime(&parsed, rfc2822) == 0 && parsed.tick + 1000000 > before.tick && parsed.tick <= after.tick); + Require(sceRtcParseRFC3339(&parsed, text) == 0 && parsed.tick + 10000 > before.tick && parsed.tick <= after.tick); + } for (const char* zone : {"UTC0", "XXX-5:30", "XXX+3"}) { SetTimeZone(zone); for (std::uint64_t invalidTick : {maxTick + 1, std::uint64_t{1} << 63u, std::numeric_limits::max()}) { @@ -481,4 +864,51 @@ int main() { Require(sceRtcFormatRFC2822LocalTime(rfc2822, &tick) == 0 && std::strcmp(rfc2822, "Wed, 31 Dec 1969 23:59:59 +0000") == 0); SetTimeZone("UTC0"); #endif + + { + char precise[40]; + RtcTick first{leapDayTick}, second{leapDayTick + 1}; + Require(sceRtcFormatRFC3339Precise(precise, &first, 0, 0) == 0 && std::strcmp(precise, "2024-02-29T12:34:56Z") == 0); + Require(sceRtcFormatRFC3339Precise(precise, &first, 0, 3) == 0 && std::strcmp(precise, "2024-02-29T12:34:56.789Z") == 0); + Require(sceRtcFormatRFC3339Precise(precise, &first, 0, 6) == 0 && std::strcmp(precise, "2024-02-29T12:34:56.789000Z") == 0); + Require(sceRtcFormatRFC3339Precise(precise, &second, 90, 6) == 0 && std::strcmp(precise, "2024-02-29T14:04:56.789001+01:30") == 0); + Require(sceRtcFormatRFC3339Precise(precise, &first, -300, 1) == 0 && std::strcmp(precise, "2024-02-29T07:34:56.7-05:00") == 0); + Require(sceRtcFormatRFC3339Precise(precise, &first, 0, 7) == invalidValue); + Require(sceRtcFormatRFC3339Precise(precise, &first, 0, -1) == invalidValue); + Require(sceRtcFormatRFC3339Precise(precise, &first, 1440, 0) == invalidValue); + Require(sceRtcFormatRFC3339Precise(precise, &first, -1440, 0) == invalidValue); + Require(sceRtcFormatRFC3339Precise(nullptr, &first, 0, 0) == invalidPointer); + Require(sceRtcFormatRFC3339Precise(precise, nullptr, 0, 6) == 0 && precise[4] == '-'); + SetTimeZone("UTC0"); + Require(sceRtcFormatRFC3339PreciseLocalTime(precise, &first, 2) == 0 && std::strcmp(precise, "2024-02-29T12:34:56.78Z") == 0); + Require(sceRtcFormatRFC3339PreciseLocalTime(precise, &first, 7) == invalidValue); + Require(sceRtcFormatRFC3339PreciseLocalTime(nullptr, &first, 0) == invalidPointer); + Require(sceRtcCompareTick(&first, &second) == -1 && sceRtcCompareTick(&second, &first) == 1 && sceRtcCompareTick(&first, &first) == 0); + Require(sceRtcCompareTick(nullptr, &first) == invalidPointer && sceRtcCompareTick(&first, nullptr) == invalidPointer); + RtcTick current{}; + Require(sceRtcGetCurrentAdNetworkTick(¤t) == 0 && current.tick > unixEpochTick); + Require(sceRtcGetCurrentAdNetworkTick(nullptr) == invalidPointer); + RtcTick tooEarly{unixEpochTick - 1}; + Require(sceRtcSetCurrentTick(&tooEarly) == invalidValue && sceRtcSetCurrentTick(nullptr) == invalidPointer); + const RtcTick systemClock{leapDayTick}; + const RtcTick networkClock{leapDayTick + 10000000}; + const RtcTick adClock{leapDayTick + 20000000}; + const RtcTick debugClock{leapDayTick + 30000000}; + Require(sceRtcSetCurrentTick(&systemClock) == 0); + Require(sceRtcSetCurrentNetworkTick(&networkClock) == 0); + Require(sceRtcGetCurrentAdNetworkTick(¤t) == 0 && current.tick >= networkClock.tick && current.tick < networkClock.tick + 1000000); + Require(sceRtcGetCurrentDebugNetworkTick(¤t) == 0 && current.tick >= networkClock.tick && current.tick < networkClock.tick + 1000000); + Require(sceRtcSetCurrentAdNetworkTick(&adClock) == 0); + Require(sceRtcSetCurrentDebugNetworkTick(&debugClock) == 0); + Require(sceRtcSetCurrentNetworkTick(&tooEarly) == invalidValue && sceRtcSetCurrentNetworkTick(nullptr) == invalidPointer); + Require(sceRtcSetCurrentAdNetworkTick(&tooEarly) == invalidValue && sceRtcSetCurrentAdNetworkTick(nullptr) == invalidPointer); + Require(sceRtcSetCurrentDebugNetworkTick(&tooEarly) == invalidValue && sceRtcSetCurrentDebugNetworkTick(nullptr) == invalidPointer); + Require(sceRtcGetCurrentTick(¤t) == 0 && current.tick >= systemClock.tick && current.tick < systemClock.tick + 1000000); + Require(sceRtcGetCurrentNetworkTick(¤t) == 0 && current.tick >= networkClock.tick && current.tick < networkClock.tick + 1000000); + Require(sceRtcGetCurrentAdNetworkTick(¤t) == 0 && current.tick >= adClock.tick && current.tick < adClock.tick + 1000000); + Require(sceRtcGetCurrentDebugNetworkTick(¤t) == 0 && current.tick >= debugClock.tick && current.tick < debugClock.tick + 1000000); + Require(sceRtcGetCurrentRawNetworkTick(¤t) == 0 && current.tick > unixEpochTick); + } + Require(sceRtcInit() == 0); + Require(sceRtcEnd() == 0); } diff --git a/core/libs/tests/GuestRudpContextLifecycle.cpp b/core/libs/tests/GuestRudpContextLifecycle.cpp new file mode 100644 index 0000000000..51a119e0d8 --- /dev/null +++ b/core/libs/tests/GuestRudpContextLifecycle.cpp @@ -0,0 +1,217 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#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(std::int32_t, const void*, std::uint32_t); +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 sceRudpGetOption(int, int, 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*) {} + +static void CheckOptions(int context) { + constexpr int invalidArgument = static_cast(0x80770004u); + constexpr int invalidOption = static_cast(0x80770005u); + constexpr std::array, 16> defaults{{ + {1, 1346}, {2, 65536}, {3, 65536}, {4, 0}, {5, 1}, {6, 1}, {7, 1}, {8, 0}, + {9, 60000}, {10, 0}, {11, 30}, {15, 0}, {16, 0}, {17, 0}, {18, 0}, {19, 0}, + }}; + int other = -1; + Require(sceRudpCreateContext(Handler, nullptr, &other) == 0); + std::array buffer{}; + for (const auto& [option, initial] : defaults) { + std::uint32_t result = 0; + Require(sceRudpGetOption(context, option, &result, sizeof(result)) == 0); + Require(result == initial); + for (std::uint32_t value : {0u, 1u, 0x80000000u, 0xffffffffu}) { + buffer.fill(0xa5); + std::memcpy(buffer.data() + 1, &value, sizeof(value)); + Require(sceRudpSetOption(context, option, buffer.data() + 1, sizeof(value)) == 0); + buffer.fill(0xa5); + Require(sceRudpGetOption(context, option, buffer.data() + 1, sizeof(value)) == 0); + std::memcpy(&result, buffer.data() + 1, sizeof(result)); + Require(result == value && buffer.front() == 0xa5 && buffer.back() == 0xa5); + Require(sceRudpGetOption(other, option, &result, sizeof(result)) == 0); + Require(result == initial); + } + for (std::uint32_t length : {0u, 1u, 3u, 5u, 8u, std::numeric_limits::max()}) { + Require(sceRudpSetOption(context, option, &initial, length) == invalidArgument); + buffer.fill(0xa5); + Require(sceRudpGetOption(context, option, buffer.data() + 1, length) == invalidArgument); + for (auto byte : buffer) Require(byte == 0xa5); + } + Require(sceRudpSetOption(context, option, nullptr, sizeof(result)) == invalidArgument); + Require(sceRudpGetOption(context, option, nullptr, sizeof(result)) == invalidArgument); + Require(sceRudpGetOption(context, option, &result, sizeof(result)) == 0); + Require(result == 0xffffffffu); + } + for (int option : {std::numeric_limits::min(), -1, 0, 20, std::numeric_limits::max()}) { + Require(sceRudpSetOption(context, option, nullptr, 0) == invalidOption); + buffer.fill(0xa5); + Require(sceRudpGetOption(context, option, buffer.data() + 1, 4) == invalidOption); + for (auto byte : buffer) Require(byte == 0xa5); + } + for (int option : {12, 13, 14}) { + std::uint32_t value = 0xa5a5a5a5u; + bool threw = false; + try { + sceRudpGetOption(context, option, &value, sizeof(value)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && value == 0xa5a5a5a5u); + for (std::uint32_t input : {0u, 1u, 0x80000000u, 0xffffffffu}) { + buffer.fill(0xa5); + std::memcpy(buffer.data() + 1, &input, sizeof(input)); + Require(sceRudpSetOption(context, option, buffer.data() + 1, sizeof(input)) == 0); + Require(buffer.front() == 0xa5 && buffer.back() == 0xa5); + for (int current : {context, other}) { + threw = false; + try { + sceRudpGetOption(current, option, &value, sizeof(value)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && value == 0xa5a5a5a5u); + } + } + for (std::uint32_t length : {0u, 1u, 3u, 5u, 8u, std::numeric_limits::max()}) { + Require(sceRudpSetOption(context, option, &value, length) == invalidArgument); + } + Require(sceRudpSetOption(context, option, nullptr, sizeof(value)) == invalidArgument); + } + Require(sceRudpTerminate(other) == 0); +} + +int main() { + constexpr int notInitialized = static_cast(0x80770001u); + constexpr int invalidContextId = static_cast(0x80770003u); + constexpr int invalidOption = static_cast(0x80770005u); + constexpr int connRefused = static_cast(0x8077000Au); + constexpr int connReset = static_cast(0x80770009u); + alignas(16) static unsigned char pool[0x10000]; + + Require(sceRudpActivate(-1, nullptr, 0) == static_cast(0x80770001)); + + 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(sceRudpGetOption(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(-1, nullptr, 0) == static_cast(0x80770004)); + + 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(sceRudpGetOption(12345, 0, nullptr, 0) == invalidContextId); + Require(sceRudpGetOption(-1, 1, nullptr, 4) == invalidContextId); + Require(sceRudpSetOption(-1, 1, nullptr, 4) == 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(sceRudpActivate(ctx, nullptr, 0) == static_cast(0x80770004)); + unsigned char address[34]{}; + Require(sceRudpActivate(-1, address, sizeof(address)) == static_cast(0x80770004)); + bool activationUnsupported = false; + try { + sceRudpActivate(ctx, address, sizeof(address)); + } catch (const std::runtime_error&) { + activationUnsupported = true; + } + Require(activationUnsupported); + + Require(sceRudpBind(ctx, 0, 0, 0) == 0); + + Require(sceRudpSetOption(ctx, 0, nullptr, 0) == invalidOption); + CheckOptions(ctx); + + 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(sceRudpGetOption(ctx, 1, nullptr, 4) == invalidContextId); + Require(sceRudpSetOption(ctx, 1, nullptr, 4) == invalidContextId); + Require(sceRudpEnd() == 0); + Require(sceRudpGetOption(ctx, 1, nullptr, 4) == notInitialized); + Require(sceRudpSetOption(ctx, 1, nullptr, 4) == notInitialized); + Require(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); + Require(sceRudpGetOption(ctx, 1, nullptr, 4) == invalidContextId); + Require(sceRudpCreateContext(Handler, nullptr, &ctx) == 0); + CheckOptions(ctx); + Require(sceRudpEnd() == 0); + return 0; +} diff --git a/core/libs/tests/GuestRudpPoll.cpp b/core/libs/tests/GuestRudpPoll.cpp new file mode 100644 index 0000000000..4ff92c482f --- /dev/null +++ b/core/libs/tests/GuestRudpPoll.cpp @@ -0,0 +1,125 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +using GuestRudpEventHandler = void(APS5_VABI*)(int, int, int, void*); +struct GuestRudpPollEvent { + int ctxId; + std::uint16_t reqEvents; + std::uint16_t rtnEvents; +}; +int APS5_VABI sceRudpInit_nid_postfix(void*, int); +int APS5_VABI sceRudpCreateContext(GuestRudpEventHandler, void*, int*); +int APS5_VABI sceRudpTerminate(int); +int APS5_VABI sceRudpEnd(); +int APS5_VABI sceRudpPollCreate(std::size_t); +int APS5_VABI sceRudpPollControl(int, int, int, std::uint16_t); +int APS5_VABI sceRudpPollWait(int, GuestRudpPollEvent*, std::size_t, std::uint64_t); +int APS5_VABI sceRudpPollDestroy(int); +} + +static void Require(bool value) { + if (!value) std::abort(); +} + +static void APS5_VABI Handler(int, int, int, void*) {} + +int main() { + constexpr int notInitialized = static_cast(0x80770001u); + constexpr int invalidContext = static_cast(0x80770003u); + constexpr int invalidArgument = static_cast(0x80770004u); + constexpr int invalidPoll = static_cast(0x8077001Fu); + constexpr int tooManyContexts = static_cast(0x80770020u); + constexpr int add = 1; + constexpr int modify = 2; + constexpr int remove = 3; + constexpr std::uint16_t read = 0x0001; + constexpr std::uint16_t write = 0x0002; + constexpr std::uint64_t forever = std::numeric_limits::max(); + alignas(16) static std::array pool{}; + std::array events{}; + const auto eventCount = static_cast(events.size()); + + Require(sceRudpPollCreate(1) == notInitialized); + Require(sceRudpPollControl(1, add, 0, read) == notInitialized); + Require(sceRudpPollWait(1, events.data(), eventCount, 0) == notInitialized); + Require(sceRudpPollDestroy(1) == notInitialized); + Require(sceRudpInit_nid_postfix(pool.data(), static_cast(pool.size())) == 0); + + Require(sceRudpPollCreate(0) == invalidArgument); + const int poll = sceRudpPollCreate(1); + Require(poll > 0); + const int other = sceRudpPollCreate(2); + Require(other > 0 && other != poll); + + int first = -1; + int second = -1; + Require(sceRudpCreateContext(Handler, nullptr, &first) == 0); + Require(sceRudpCreateContext(Handler, nullptr, &second) == 0); + + Require(sceRudpPollControl(poll, 0, first, read) == invalidArgument); + Require(sceRudpPollControl(poll, 4, first, read) == invalidArgument); + Require(sceRudpPollControl(-1, add, first, read) == invalidArgument); + Require(sceRudpPollControl(poll, add, -1, read) == invalidArgument); + Require(sceRudpPollControl(12345, add, first, read) == invalidPoll); + Require(sceRudpPollControl(poll, add, 12345, read) == invalidContext); + Require(sceRudpPollControl(poll, modify, first, read) == invalidContext); + Require(sceRudpPollControl(poll, remove, first, read) == invalidContext); + + Require(sceRudpPollControl(poll, add, first, read) == 0); + Require(sceRudpPollControl(poll, add, first, read) == tooManyContexts); + Require(sceRudpPollControl(poll, add, second, read) == tooManyContexts); + Require(sceRudpPollControl(other, add, first, read) == 0); + Require(sceRudpPollControl(other, add, first, read) == invalidContext); + Require(sceRudpPollControl(poll, modify, first, read | write) == 0); + Require(sceRudpPollControl(poll, remove, first, 0) == 0); + Require(sceRudpPollControl(poll, remove, first, 0) == invalidContext); + Require(sceRudpPollControl(poll, add, second, write) == 0); + + Require(sceRudpPollWait(-1, events.data(), eventCount, 0) == invalidArgument); + Require(sceRudpPollWait(poll, nullptr, eventCount, 0) == invalidArgument); + Require(sceRudpPollWait(poll, events.data(), 0, 0) == invalidArgument); + Require(sceRudpPollWait(12345, events.data(), eventCount, 0) == invalidPoll); + Require(sceRudpPollWait(poll, events.data(), eventCount, 0) == 0); + const auto start = std::chrono::steady_clock::now(); + Require(sceRudpPollWait(poll, events.data(), eventCount, 20000) == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(20)); + + Require(sceRudpTerminate(second) == 0); + Require(sceRudpPollControl(poll, add, first, read) == 0); + Require(sceRudpTerminate(first) == 0); + Require(sceRudpPollControl(poll, add, first, read) == invalidContext); + int third = -1; + Require(sceRudpCreateContext(Handler, nullptr, &third) == 0); + Require(sceRudpPollControl(poll, add, third, read) == 0); + Require(sceRudpPollControl(other, add, third, read) == 0); + + int destroyedResult = 0; + std::thread destroyedWaiter([&] { destroyedResult = sceRudpPollWait(poll, events.data(), eventCount, forever); }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(sceRudpPollDestroy(poll) == 0); + destroyedWaiter.join(); + Require(destroyedResult == invalidPoll); + Require(sceRudpPollDestroy(poll) == invalidPoll); + Require(sceRudpPollDestroy(-1) == invalidArgument); + Require(sceRudpPollControl(poll, add, third, read) == invalidPoll); + + int endedResult = 0; + std::thread endedWaiter([&] { endedResult = sceRudpPollWait(other, events.data(), eventCount, forever); }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(sceRudpEnd() == 0); + endedWaiter.join(); + Require(endedResult == notInitialized); + + Require(sceRudpInit_nid_postfix(pool.data(), static_cast(pool.size())) == 0); + Require(sceRudpPollDestroy(other) == invalidPoll); + Require(sceRudpEnd() == 0); + return 0; +} diff --git a/core/libs/tests/GuestRudpPump.cpp b/core/libs/tests/GuestRudpPump.cpp index 1e8ec7ab29..2cc7f5586f 100644 --- a/core/libs/tests/GuestRudpPump.cpp +++ b/core/libs/tests/GuestRudpPump.cpp @@ -1,18 +1,19 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include -#include #include extern "C" { int APS5_VABI sceRudpInit_nid_postfix(void*, int); int APS5_VABI sceRudpEnd(); -int APS5_VABI sceRudpProcessEvents(); +int APS5_VABI sceRudpProcessEvents(std::uint64_t timeout); int APS5_VABI sceRudpNetFlush(); int APS5_VABI sceRudpGetMaxSegmentSize(std::uint16_t*); -int APS5_VABI sceRudpNetReceived(); +int APS5_VABI sceRudpNetReceived(int socketId, const void* data, std::size_t dataSize, const void* sourceAddress, std::uint32_t sourceAddressSize); +int APS5_VABI sceRudpEnableInternalIOThread2(std::uint32_t, std::uint32_t, std::uint64_t); } static void Require(bool value, int line) { @@ -27,16 +28,20 @@ int main() { constexpr int notInitialized = static_cast(0x80770001); constexpr int invalidArgument = static_cast(0x80770004); std::uint16_t segmentSize = 0x5a5a; - CHECK(sceRudpProcessEvents() == notInitialized); + CHECK(sceRudpProcessEvents(0) == notInitialized); CHECK(sceRudpNetFlush() == notInitialized); + CHECK(sceRudpNetReceived(0, nullptr, 0, nullptr, 0) == notInitialized); + CHECK(sceRudpEnableInternalIOThread2(0, 0, 0) == notInitialized); CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); CHECK(segmentSize == 0x5a5a); CHECK(sceRudpGetMaxSegmentSize(nullptr) == notInitialized); alignas(8) static unsigned char pool[0xC58]; CHECK(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); - CHECK(sceRudpProcessEvents() == 0); + CHECK(sceRudpProcessEvents(0) == 0); CHECK(sceRudpNetFlush() == 0); + CHECK(sceRudpNetReceived(0, nullptr, 0, nullptr, 0) == 0); + CHECK(sceRudpEnableInternalIOThread2(0x4000, 0, 0x3) == 0); CHECK(sceRudpGetMaxSegmentSize(nullptr) == invalidArgument); CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == 0); CHECK(segmentSize == 1410); @@ -49,12 +54,6 @@ int main() { CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); CHECK(segmentSize == 0x5a5a); - bool threw = false; - try { - sceRudpNetReceived(); - } catch (const std::runtime_error&) { - threw = true; - } - CHECK(threw); + CHECK(sceRudpNetReceived(0, nullptr, 0, nullptr, 0) == notInitialized); return 0; } diff --git a/core/libs/tests/GuestRuneLocale.cpp b/core/libs/tests/GuestRuneLocale.cpp index 86d5de99b0..e677153ff7 100644 --- a/core/libs/tests/GuestRuneLocale.cpp +++ b/core/libs/tests/GuestRuneLocale.cpp @@ -1,4 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include #include #include @@ -38,6 +40,12 @@ int APS5_VABI iswctype_nid_postfix(std::uint32_t, std::uint64_t); std::uint64_t APS5_VABI wctype_nid_postfix(const char*); std::uint32_t APS5_VABI towlower_nid_postfix(std::uint32_t); std::uint32_t APS5_VABI towupper_nid_postfix(std::uint32_t); +std::uint64_t APS5_VABI wctrans_nid_postfix(const char*); +std::uint32_t APS5_VABI towctrans_nid_postfix(std::uint32_t, std::uint64_t); +std::uint32_t APS5_VABI _Towlower_nid_postfix(std::uint32_t, const void*); +std::uint32_t APS5_VABI _Towupper_nid_postfix(std::uint32_t, const void*); +int APS5_VABI _Tolower_nid_postfix(int, const void*); +int APS5_VABI _Toupper_nid_postfix(int, const void*); std::uint32_t APS5_VABI btowc_nid_postfix(int); int APS5_VABI wctob_nid_postfix(std::uint32_t); } @@ -125,5 +133,19 @@ int main() { Require(btowc_nid_postfix('A') == 'A' && btowc_nid_postfix(0xe9) == 0xe9 && btowc_nid_postfix(-23) == 0xe9); Require(wctob_nid_postfix('A') == 'A' && wctob_nid_postfix(0xe9) == 0xe9 && wctob_nid_postfix(0) == 0); Require(wctob_nid_postfix(0x100) == -1 && wctob_nid_postfix(0x20ac) == -1 && wctob_nid_postfix(0xffffffff) == -1); + Require(wctrans_nid_postfix("tolower") == 1 && wctrans_nid_postfix("toupper") == 2); + Require(wctrans_nid_postfix("") == 0 && wctrans_nid_postfix("TOLOWER") == 0 && wctrans_nid_postfix("tolowe") == 0); + for (std::int64_t c = -300; c < 70000; ++c) { + const auto wide = static_cast(c); + const bool ascii = c >= 0 && c < 128; + const auto lower = ascii ? static_cast(std::tolower(static_cast(c))) : wide; + const auto upper = ascii ? static_cast(std::toupper(static_cast(c))) : wide; + Require(towctrans_nid_postfix(wide, 1) == lower && towctrans_nid_postfix(wide, 2) == upper); + Require(_Towlower_nid_postfix(wide, nullptr) == lower && _Towupper_nid_postfix(wide, nullptr) == upper); + Require(_Tolower_nid_postfix(static_cast(c), nullptr) == static_cast(lower)); + Require(_Toupper_nid_postfix(static_cast(c), nullptr) == static_cast(upper)); + } + errno = 0; + Require(towctrans_nid_postfix('A', 0) == 'A' && towctrans_nid_postfix('a', 3) == 'a' && errno == 0); return 0; } diff --git a/core/libs/tests/GuestRwlockRetire.cpp b/core/libs/tests/GuestRwlockRetire.cpp new file mode 100644 index 0000000000..f622a0ee6d --- /dev/null +++ b/core/libs/tests/GuestRwlockRetire.cpp @@ -0,0 +1,53 @@ +#include "SceTypes.hpp" +#include "prx/libkernel/Pthread/include/Rwlock.hpp" +#include +#include +#include + +static void Require(bool result) { + if (!result) std::abort(); +} + +static void CheckHandoff(bool write) { + constexpr int rounds = 2000; + std::atomic phase{0}; + PthreadRwlock published = nullptr; + std::thread successor([&] { + for (int round = 0; round < rounds; ++round) { + phase.wait(0); + auto slot = published; + phase.store(2); + phase.notify_one(); + for (;;) { + const int result = scePthreadRwlockTrywrlock(&slot); + if (result == 0) break; + Require(result == static_cast(0x80020010u)); + std::this_thread::yield(); + } + Require(scePthreadRwlockUnlock(&slot) == 0); + Require(scePthreadRwlockDestroy(&slot) == 0); + phase.store(3); + phase.notify_one(); + phase.wait(3); + } + }); + for (int round = 0; round < rounds; ++round) { + PthreadRwlock slot = nullptr; + Require(scePthreadRwlockInit(&slot, nullptr, "retire") == 0); + Require((write ? scePthreadRwlockWrlock(&slot) : scePthreadRwlockRdlock(&slot)) == 0); + published = slot; + phase.store(1); + phase.notify_one(); + phase.wait(1); + Require(scePthreadRwlockUnlock(&slot) == 0); + while (phase.load() != 3) phase.wait(2); + phase.store(0); + phase.notify_one(); + } + successor.join(); +} + +int main() { + CheckHandoff(false); + CheckHandoff(true); +} 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..2c76c900ba --- /dev/null +++ b/core/libs/tests/GuestSandboxCertificates.cpp @@ -0,0 +1,107 @@ +#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" + "-----BEGIN CERTIFICATE-----\r\nMIIBAg==\r\n-----END CERTIFICATE-----\r\n"; + const std::string input = "Bag Attributes\r\n friendlyName: should-not-be-copied\r\n" + "-----BEGIN PRIVATE KEY-----\r\nprivate-key-material\r\n-----END PRIVATE KEY-----\r\n" + + expected + "subject=should-not-be-copied\r\n" + "-----BEGIN RSA PRIVATE KEY-----\r\nmore-private-key-material\r\n-----END RSA PRIVATE KEY-----\r\n"; + { std::ofstream file(bundle, std::ios::binary | std::ios::trunc); file << input; } + Require(setenv("SSL_CERT_FILE", bundle.c_str(), 1) == 0); +#endif + const std::string directory = std::string("/") + sceKernelGetFsSandboxRandomWord() + "/common/cert"; + 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/GuestSaveDataCancel.cpp b/core/libs/tests/GuestSaveDataCancel.cpp index 9b8087c031..ce9c2e8c20 100644 --- a/core/libs/tests/GuestSaveDataCancel.cpp +++ b/core/libs/tests/GuestSaveDataCancel.cpp @@ -1,13 +1,14 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include -extern "C" int APS5_VABI sceSaveDataCancel(); +extern "C" int APS5_VABI sceSaveDataCancel(std::uint32_t cancelArgument); int main() { try { - sceSaveDataCancel(); + sceSaveDataCancel(0); } catch (const std::runtime_error& error) { if (std::string(error.what()) == "sceSaveDataCancel not implemented") return 0; std::fprintf(stderr, "sceSaveDataCancel threw: %s\n", error.what()); diff --git a/core/libs/tests/GuestSaveDataDelete.cpp b/core/libs/tests/GuestSaveDataDelete.cpp index 71b19c03dd..0ebbce1462 100644 --- a/core/libs/tests/GuestSaveDataDelete.cpp +++ b/core/libs/tests/GuestSaveDataDelete.cpp @@ -103,8 +103,10 @@ int main() { Check(mountResultCode == 0, "the existing save mounts read/write"); if (mountResultCode == 0) { Check(WriteMountedFile(mountedPayload, sizeof(mountedPayload)), "the guest alias writes the mounted save"); + Check(Delete("absent", sizeof("absent")) == 0, "a missing save can be deleted while another save is mounted"); std::error_code caseAliasError; - const bool caseAlias = std::filesystem::equivalent(kept.parent_path(), work / "_sd" / "Kept", caseAliasError); + const auto casePath = work / "_sd" / "Kept"; + const bool caseAlias = std::filesystem::exists(casePath) && std::filesystem::equivalent(kept.parent_path(), casePath, caseAliasError); if (caseAliasError && caseAliasError != std::errc::no_such_file_or_directory) { Check(false, "case-alias host identity probe has no unexpected filesystem error"); } else if (caseAlias) { diff --git a/core/libs/tests/GuestSaveDataDialog.cpp b/core/libs/tests/GuestSaveDataDialog.cpp index 5d8e063710..1c3ffc702d 100644 --- a/core/libs/tests/GuestSaveDataDialog.cpp +++ b/core/libs/tests/GuestSaveDataDialog.cpp @@ -34,14 +34,32 @@ static void RequireRunning() { } int main() { + SaveDataDialogParam param{}; + param.size = sizeof(param); + + Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_NONE, "initial status"); + Require(sceSaveDataDialogTerminate() == SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED, "terminate before init"); + Require(sceSaveDataDialogOpen(¶m) == SAVE_DATA_DIALOG_ERROR_INVALID_STATE, "open before init"); + Require(sceSaveDataDialogClose(nullptr) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "close before init"); + SaveDataDialogResult uninitResult{}; + Require(sceSaveDataDialogGetResult(&uninitResult) == SAVE_DATA_DIALOG_ERROR_NOT_FINISHED, "get result before init"); + 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(sceSaveDataDialogOpen(nullptr) == SAVE_DATA_DIALOG_ERROR_ARG_NULL, "open with null param"); + Require(sceSaveDataDialogClose(nullptr) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "close while initialized not running"); + Require(sceSaveDataDialogGetResult(&uninitResult) == SAVE_DATA_DIALOG_ERROR_NOT_FINISHED, "get result before open"); + Require(sceSaveDataDialogProgressBarInc(0, 10) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress inc while initialized not running"); + Require(sceSaveDataDialogProgressBarSetValue(0, 50) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress set value while initialized not running"); SaveDataDirName names[2]{{}, {"SDU1"}}; SaveDataDialogItems items{}; items.dir_names = names; items.dir_names_num = 2; int context = 42; - SaveDataDialogParam param{}; + param = {}; param.size = sizeof(param); param.mode = SAVE_DATA_DIALOG_MODE_PROGRESS_BAR; param.items = &items; @@ -55,8 +73,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"); @@ -64,15 +85,27 @@ int main() { SaveDataDialogResult result{}; result.dir_name = &selected; Require(sceSaveDataDialogGetResult(&result) == 0, "read completed result"); + Require(sceSaveDataDialogGetResult(nullptr) == SAVE_DATA_DIALOG_ERROR_ARG_NULL, + "get result with null result pointer"); Require(result.mode == param.mode && result.result == SAVE_DATA_DIALOG_RESULT_OK && result.user_data == &context && std::strcmp(selected.data, "SDU1") == 0, "save result must preserve its mode, directory and caller context"); } + Require(sceSaveDataDialogOpen(¶m) == 0, "open save progress for terminate test"); + RequireRunning(); + Require(sceSaveDataDialogTerminate() == 0, "terminate while running"); + Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_NONE, "terminated state"); + Require(sceSaveDataDialogTerminate() == SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED, "terminate after terminate"); + + Require(sceSaveDataDialogInitialize() == 0, "re-initialize"); param.mode = 1; Require(sceSaveDataDialogOpen(¶m) == 0, "open selection dialog"); Require(sceSaveDataDialogUpdateStatus() == SAVE_DATA_DIALOG_STATUS_FINISHED, "selection dialog must retain the existing automatic response"); Require(sceSaveDataDialogTerminate() == 0, "terminate"); + Require(sceSaveDataDialogTerminate() == SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED, "terminate when already terminated"); Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_NONE, "terminated state"); + Require(sceSaveDataDialogProgressBarInc(0, 10) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress inc after terminate"); + Require(sceSaveDataDialogProgressBarSetValue(0, 50) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress set value after terminate"); } diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index 9249ab3838..88557e574d 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -11,6 +12,12 @@ 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*); +int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint*, std::uint32_t, const void*, std::size_t); +int APS5_VABI sceSaveDataLoadIcon(const SaveDataMountPoint*, SaveDataIcon*); +int APS5_VABI sceSaveDataSaveIcon(const SaveDataMountPoint*, const SaveDataIcon*); +int APS5_VABI sceSaveDataSaveIconByPath(const SaveDataMountPoint*, const char*); } namespace { @@ -18,6 +25,8 @@ namespace { constexpr std::uint32_t MountReadWrite = 2; constexpr std::uint32_t MountCreate = 4; constexpr std::uint32_t MountCreate2 = 32; +constexpr int SaveDataErrorParameter = -2137063424; +constexpr int SaveDataErrorNotMounted = -2137063420; constexpr int SaveDataErrorBusy = -2137063421; constexpr int SaveDataErrorNotFound = -2137063416; @@ -29,6 +38,69 @@ void Check(bool condition, const std::string& what) { ++failures; } +void CheckUnmountedCalls() { + SaveDataMountPoint point{}; + std::memcpy(point.data, "test", sizeof("test")); + SaveDataMountInfo info{}; + SaveDataIcon icon{}; + char buf[128]{}; + std::size_t gotSize = 0; + Check(sceSaveDataUmount2(0, nullptr) == SaveDataErrorParameter, "umount2 null point is PARAMETER"); + Check(sceSaveDataUmount2(0, &point) == SaveDataErrorNotMounted, "umount2 unmounted point is NOT_MOUNTED"); + Check(sceSaveDataLoadIcon(nullptr, &icon) == SaveDataErrorParameter, "loadIcon null point is PARAMETER"); + Check(sceSaveDataLoadIcon(&point, &icon) == SaveDataErrorNotMounted, "loadIcon unmounted point is NOT_MOUNTED"); + Check(sceSaveDataSaveIcon(nullptr, &icon) == SaveDataErrorParameter, "saveIcon null point is PARAMETER"); + Check(sceSaveDataSaveIcon(&point, &icon) == SaveDataErrorNotMounted, "saveIcon unmounted point is NOT_MOUNTED"); + Check(sceSaveDataSaveIconByPath(&point, "test.png") == 0, "saveIconByPath succeeds"); + Check(sceSaveDataGetMountInfo(nullptr, &info) == SaveDataErrorParameter, "getMountInfo null point is PARAMETER"); + Check(sceSaveDataGetMountInfo(&point, nullptr) == SaveDataErrorParameter, "getMountInfo null info is PARAMETER"); + Check(sceSaveDataGetMountInfo(&point, &info) == SaveDataErrorNotMounted, "getMountInfo unmounted point is NOT_MOUNTED"); + Check(sceSaveDataGetParam(nullptr, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null point is PARAMETER"); + Check(sceSaveDataGetParam(&point, 1, nullptr, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null buf is PARAMETER"); + Check(sceSaveDataGetParam(&point, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorNotMounted, "getParam unmounted point is NOT_MOUNTED"); + Check(sceSaveDataSetParam(nullptr, 1, buf, sizeof(buf)) == SaveDataErrorParameter, "setParam null point is PARAMETER"); + Check(sceSaveDataSetParam(&point, 1, nullptr, sizeof(buf)) == SaveDataErrorParameter, "setParam null buf is PARAMETER"); + Check(sceSaveDataSetParam(&point, 1, buf, sizeof(buf)) == SaveDataErrorNotMounted, "setParam unmounted point is NOT_MOUNTED"); +} + +void CheckMountInfoAndParam(const SaveDataMountPoint* point) { + SaveDataMountInfo info{}; + SaveDataIcon icon{}; + char buf[128]{}; + char smallBuf[64]{}; + std::size_t gotSize = 0; + SaveDataMountPoint unmounted{}; + std::memcpy(unmounted.data, "unmounted", sizeof("unmounted")); + const char newTitle[] = "Mounted Title"; + char readTitle[128]{}; + Check(sceSaveDataUmount2(0, nullptr) == SaveDataErrorParameter, "umount2 null point is PARAMETER"); + Check(sceSaveDataLoadIcon(nullptr, &icon) == SaveDataErrorParameter, "loadIcon null point is PARAMETER"); + Check(sceSaveDataLoadIcon(point, &icon) == 0, "loadIcon on mounted save succeeds"); + Check(sceSaveDataSaveIcon(nullptr, &icon) == SaveDataErrorParameter, "saveIcon null point is PARAMETER"); + Check(sceSaveDataSaveIcon(point, &icon) == 0, "saveIcon on mounted save succeeds"); + Check(sceSaveDataSaveIconByPath(point, "icon.png") == 0, "saveIconByPath on mounted save succeeds"); + Check(sceSaveDataGetMountInfo(nullptr, &info) == SaveDataErrorParameter, "getMountInfo null point is PARAMETER"); + Check(sceSaveDataGetMountInfo(point, nullptr) == SaveDataErrorParameter, "getMountInfo null info is PARAMETER"); + Check(sceSaveDataGetMountInfo(&unmounted, &info) == SaveDataErrorNotMounted, "getMountInfo unmounted point is NOT_MOUNTED"); + 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"); + Check(sceSaveDataSetParam(nullptr, 1, newTitle, sizeof(newTitle)) == SaveDataErrorParameter, "setParam null point is PARAMETER"); + Check(sceSaveDataSetParam(point, 1, nullptr, sizeof(newTitle)) == SaveDataErrorParameter, "setParam null buf is PARAMETER"); + Check(sceSaveDataSetParam(point, 999, newTitle, sizeof(newTitle)) == SaveDataErrorParameter, "setParam invalid type is PARAMETER"); + Check(sceSaveDataSetParam(point, 1, newTitle, 0) == SaveDataErrorParameter, "setParam zero size is PARAMETER"); + Check(sceSaveDataSetParam(point, 4, smallBuf, 1) == SaveDataErrorParameter, "setParam short user param is PARAMETER"); + Check(sceSaveDataSetParam(&unmounted, 1, newTitle, sizeof(newTitle)) == SaveDataErrorNotMounted, "setParam unmounted point is NOT_MOUNTED"); + Check(sceSaveDataSetParam(point, 1, newTitle, sizeof(newTitle)) == 0, "setParam on mounted save succeeds"); + Check(sceSaveDataGetParam(point, 1, readTitle, sizeof(readTitle), &gotSize) == 0, "getParam reads back set title"); + Check(std::strcmp(readTitle, newTitle) == 0, "readTitle matches newTitle"); +} + void MountAndCheck(const char* name, std::uint32_t mode, std::uint32_t expectedStatus, const std::string& what) { SceSaveDataDirName dirName{}; std::memcpy(dirName.data, name, std::strlen(name) + 1); @@ -40,6 +112,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"); } @@ -60,9 +133,91 @@ void CheckFailedMountKeepsResult() { mount.mount_mode = MountReadWrite; Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorNotFound, "open of a missing save is NOT_FOUND"); Check(std::memcmp(&result, &mounted, sizeof(result)) == 0, "a NOT_FOUND mount leaves the result as it was"); + MountAndCheck("new-create", MountReadWrite | MountCreate, 1u, "CREATE while another save is mounted"); + MountAndCheck("new-create2", MountReadWrite | MountCreate2, 1u, "CREATE2 while another save is mounted"); + Check(std::filesystem::remove(std::filesystem::path("_sd") / "held"), "the mounted backing directory is removed"); + mount.dir_name = &held; + Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorBusy, "the same name remains BUSY after its backing directory disappears"); + Check(std::memcmp(&result, &mounted, sizeof(result)) == 0, "a missing backing directory leaves the BUSY result unchanged"); + MountAndCheck("made", MountReadWrite, 0u, "open of another save while a mounted backing directory is missing"); Check(sceSaveDataUmount2(0, &result.mount_point) == 0, "the kept mount point unmounts the save"); } +void CheckEquivalentMounts() { + SceSaveDataDirName original{}; + std::memcpy(original.data, "identity", sizeof("identity")); + SaveDataMount3 mount{}; + mount.dir_name = &original; + mount.mount_mode = MountReadWrite | MountCreate2; + SaveDataMountResult first{}; + Check(sceSaveDataMount3(&mount, &first) == 0, "original save mounts"); + + const auto originalPath = std::filesystem::path("_sd") / "identity"; + std::error_code error; + const auto casePath = std::filesystem::path("_sd") / "IDENTITY"; + const bool caseAlias = std::filesystem::exists(casePath) && std::filesystem::equivalent(originalPath, casePath, error); + Check(!error || error == std::errc::no_such_file_or_directory, "case-alias identity check succeeds"); + std::filesystem::create_directory_symlink("identity", std::filesystem::path("_sd") / "linked", error); + const bool symlinkAlias = !error; + Check(caseAlias || symlinkAlias, "a host alias is available to check mount identity"); + + const auto checkAlias = [&](const char* name) { + SceSaveDataDirName alias{}; + std::memcpy(alias.data, name, std::strlen(name) + 1); + mount.dir_name = &alias; + mount.mount_mode = MountReadWrite; + SaveDataMountResult result; + std::memset(&result, 0xCD, sizeof(result)); + const SaveDataMountResult before = result; + Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorBusy, std::string(name) + " cannot mount the same directory twice"); + Check(std::memcmp(&result, &before, sizeof(result)) == 0, std::string(name) + " leaves the result unchanged"); + }; + if (caseAlias) checkAlias("IDENTITY"); + if (symlinkAlias) checkAlias("linked"); + + Check(sceSaveDataUmount2(0, &first.mount_point) == 0, "original save unmounts"); + if (caseAlias) MountAndCheck("IDENTITY", MountReadWrite, 0u, "case alias after unmount"); + if (symlinkAlias) { + MountAndCheck("linked", MountReadWrite, 0u, "symlink alias after unmount"); + std::filesystem::remove(std::filesystem::path("_sd") / "linked", error); + Check(!error, "symlink alias is removed"); + } +} + +std::set Tree() { + std::set paths; + for (const auto& entry : std::filesystem::recursive_directory_iterator(std::filesystem::current_path())) paths.insert(entry.path()); + return paths; +} + +void CheckInvalidNameIsRejected(const char* data, std::size_t size, const std::string& label) { + for (const std::uint32_t mode : {MountReadWrite | MountCreate2, MountReadWrite | MountCreate, MountReadWrite}) { + SceSaveDataDirName dirName{}; + std::memcpy(dirName.data, data, size); + SaveDataMount3 mount{}; + mount.dir_name = &dirName; + mount.mount_mode = mode; + SaveDataMountResult result; + std::memset(&result, 0xCD, sizeof(result)); + const SaveDataMountResult before = result; + const std::string what = label + " with mount mode " + std::to_string(mode); + Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorParameter, what + " is a parameter error"); + Check(std::memcmp(&result, &before, sizeof(result)) == 0, what + " leaves the result as it was"); + } +} + +void CheckInvalidNamesAreRejected() { + const auto before = Tree(); + for (const char* invalid : {"", ".", "..", "a/b", "a\\b", "c:d"}) { + CheckInvalidNameIsRejected(invalid, std::strlen(invalid) + 1, std::string("\"") + invalid + "\""); + } + char unterminated[sizeof(SceSaveDataDirName::data)]; + std::memset(unterminated, 'a', sizeof(unterminated)); + CheckInvalidNameIsRejected(unterminated, sizeof(unterminated), "an unterminated name"); + Check(Tree() == before, "rejected names create nothing"); + MountAndCheck("after", MountReadWrite | MountCreate2, 1u, "CREATE2 after the rejected names"); +} + } int main() { @@ -70,12 +225,15 @@ int main() { std::filesystem::create_directories(root); const auto previous = std::filesystem::current_path(); std::filesystem::current_path(root); + CheckUnmountedCalls(); Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); MountAndCheck("fresh", MountReadWrite | MountCreate2, 1u, "CREATE2 of a missing save"); MountAndCheck("fresh", MountReadWrite | MountCreate2, 0u, "CREATE2 of an existing save"); MountAndCheck("made", MountReadWrite | MountCreate, 1u, "CREATE of a missing save"); MountAndCheck("made", MountReadWrite, 0u, "open of an existing save"); CheckFailedMountKeepsResult(); + CheckEquivalentMounts(); + CheckInvalidNamesAreRejected(); std::filesystem::current_path(previous); std::error_code error; std::filesystem::remove_all(root, error); diff --git a/core/libs/tests/GuestSaveDataPs4.cpp b/core/libs/tests/GuestSaveDataPs4.cpp index 39e7b14d1e..6ee6868bd4 100644 --- a/core/libs/tests/GuestSaveDataPs4.cpp +++ b/core/libs/tests/GuestSaveDataPs4.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -10,10 +11,16 @@ extern "C" int APS5_VABI sceSaveDataTransferringMountPs4(const SaveDataTransferringMount*, SaveDataMountResult*); extern "C" int APS5_VABI sceSaveDataDirNameSearchPs4(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); extern "C" int APS5_VABI sceSaveDataDirNameSearch(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); +extern "C" int APS5_VABI sceSaveDataInitialize(const void*); +extern "C" int APS5_VABI sceSaveDataTerminate(); +extern "C" int APS5_VABI sceSaveDataSetupSaveDataMemory(std::int32_t, std::size_t, const SaveDataParam*); +extern "C" int APS5_VABI sceSaveDataSetSaveDataMemory(std::int32_t, void*, std::size_t, std::int64_t); +extern "C" int APS5_VABI sceSaveDataGetSaveDataMemory(std::int32_t, void*, std::size_t, std::int64_t); namespace { constexpr int SaveDataErrorNotFound = -2137063416; +constexpr int SaveDataErrorNotInitialized = -2137063423; int failures = 0; @@ -73,6 +80,22 @@ void TestDirNameSearchPs4() { Check(ThrowsRuntimeError([&] { sceSaveDataDirNameSearchPs4(&cond, nullptr); }), "null result throws"); } +void TestSaveDataMemoryV1() { + Check(sceSaveDataSetupSaveDataMemory(7, 0x1000, nullptr) == SaveDataErrorNotInitialized, "v1 SetupSaveDataMemory before v1 Initialize"); + Check(sceSaveDataInitialize(nullptr) == 0, "v1 Initialize"); + Check(sceSaveDataSetupSaveDataMemory(7, 0x1000, nullptr) == 0, "v1 SetupSaveDataMemory"); + char written[16] = "settings-v1"; + Check(sceSaveDataSetSaveDataMemory(7, written, sizeof(written), 0x100) == 0, "v1 SetSaveDataMemory at an offset"); + char read[16] = {}; + Check(sceSaveDataGetSaveDataMemory(7, read, sizeof(read), 0x100) == 0, "v1 GetSaveDataMemory"); + Check(std::memcmp(read, written, sizeof(read)) == 0, "v1 GetSaveDataMemory returns the bytes v1 set"); + std::ifstream slot("_sd_mem/u7/slot0.bin", std::ios::binary); + slot.seekg(0x100); + char stored[16] = {}; + Check(static_cast(slot.read(stored, sizeof(stored))) && std::memcmp(stored, written, sizeof(stored)) == 0, "v1 SetSaveDataMemory stores into slot 0 of the user"); + Check(sceSaveDataTerminate() == 0, "Terminate after v1 Initialize"); +} + } int main() { @@ -83,6 +106,7 @@ int main() { TestTransferringMountPs4(); TestDirNameSearchPs4(); + TestSaveDataMemoryV1(); std::filesystem::current_path(previous); std::error_code error; diff --git a/core/libs/tests/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index 6cea92d921..47613690d3 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -27,6 +27,16 @@ static void Require(bool value, const char* message) { if (!value) throw std::runtime_error(message); } +template +static bool ThrowsRuntimeError(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + static std::vector Search(std::uint32_t key, std::uint32_t order, std::uint32_t capacity, std::uint32_t* hits) { SceSaveDataDirName pattern{}; std::snprintf(pattern.data, sizeof(pattern.data), "SORT_%%"); @@ -187,7 +197,7 @@ static void SearchAndMount() { for (std::size_t i = 0; i < 2; ++i) { SaveDataSearchInfo info{}; std::memcpy(&info, infos.data() + i * 48, sizeof(info)); - Require(info.blocks == 32768 && info.free_blocks == 32768, "search info must report the save's blocks and free blocks"); + Require(info.blocks == 16384 && info.free_blocks == 16384, "search info must report the save's blocks and free blocks"); for (const auto byte : info.reserved) Require(byte == 0, "search info reserved bytes must be zero"); } std::array found{}; @@ -246,20 +256,9 @@ static std::vector SizeSearch(std::uint32_t key, std::vector return {infos.begin(), infos.end()}; } -static void SaveSize() { - CreateSized("SIZE_A", 200, 0); - CreateSized("SIZE_B", 96, 40000); - std::vector found; - auto infos = SizeSearch(0, found); - Require(found == std::vector{"SIZE_A", "SIZE_B"}, "the sized saves must sort by name"); - Require(infos[0].blocks == 200 && infos[0].free_blocks == 200, "an empty save must report all its blocks free"); - Require(infos[1].blocks == 96 && infos[1].free_blocks == 94, "a save must report the blocks it was created with, less those its files take"); - SizeSearch(2, found); - Require(found == std::vector{"SIZE_B", "SIZE_A"}, "saves must sort by blocks"); - infos = SizeSearch(5, found); - Require(found == std::vector{"SIZE_B", "SIZE_A"} && infos[0].free_blocks < infos[1].free_blocks, "saves must sort by free blocks"); +static SaveDataMountInfo MountInfo(const char* name) { SceSaveDataDirName dirName{}; - std::snprintf(dirName.data, sizeof(dirName.data), "SIZE_B"); + std::snprintf(dirName.data, sizeof(dirName.data), "%s", name); SaveDataMount3 mount{}; mount.user_id = 1; mount.dir_name = &dirName; @@ -268,8 +267,45 @@ static void SaveSize() { Require(sceSaveDataMount3(&mount, &mounted) == 0, "mounting the sized save failed"); SaveDataMountInfo info{}; Require(sceSaveDataGetMountInfo(&mounted.mount_point, &info) == 0, "mount info failed"); - Require(info.blocks == 96 && info.free_blocks == 94, "mount info must report the save's blocks and free blocks"); Require(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "unmount failed"); + return info; +} + +static void SaveSize() { + CreateSized("SIZE_A", 200, 0); + CreateSized("SIZE_B", 96, 40000); + std::vector found; + auto infos = SizeSearch(0, found); + Require(found == std::vector{"SIZE_A", "SIZE_B"}, "the sized saves must sort by name"); + Require(infos[0].blocks == 200 && infos[0].free_blocks == 200, "an empty save must report all its blocks free"); + Require(infos[1].blocks == 96 && infos[1].free_blocks == 95, "a save must report the blocks it was created with, less those its files take"); + SizeSearch(2, found); + Require(found == std::vector{"SIZE_B", "SIZE_A"}, "saves must sort by blocks"); + infos = SizeSearch(5, found); + Require(found == std::vector{"SIZE_B", "SIZE_A"} && infos[0].free_blocks < infos[1].free_blocks, "saves must sort by free blocks"); + const auto info = MountInfo("SIZE_B"); + Require(info.blocks == 96 && info.free_blocks == 95, "mount info must report the save's blocks and free blocks"); +} + +static void CheckNullArguments() { + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + SaveDataDirNameSearchResult result{}; + Require(ThrowsRuntimeError([&] { sceSaveDataDirNameSearch(nullptr, &result); }), "null cond throws in sceSaveDataDirNameSearch"); + Require(ThrowsRuntimeError([&] { sceSaveDataDirNameSearch(&cond, nullptr); }), "null result throws in sceSaveDataDirNameSearch"); + Require(ThrowsRuntimeError([&] { sceSaveDataDirNameSearch2(nullptr, &result); }), "null cond throws in sceSaveDataDirNameSearch2"); + Require(ThrowsRuntimeError([&] { sceSaveDataDirNameSearch2(&cond, nullptr); }), "null result throws in sceSaveDataDirNameSearch2"); + cond.order = 2; + Require(sceSaveDataDirNameSearch2(&cond, &result) == SaveDataErrorParameter, "invalid order must be rejected in sceSaveDataDirNameSearch2"); +} + +static void BlockSize() { + CreateSized("BLOCK_A", 48, 70000); + auto info = MountInfo("BLOCK_A"); + Require(info.blocks == 48 && info.free_blocks == 46, "a file must take whole 64 KiB blocks"); + CreateSized("BLOCK_B", 32768, 0); + info = MountInfo("BLOCK_B"); + Require(info.blocks == 16384 && info.free_blocks == 16384, "a save must hold at most 16384 blocks"); } int main() { @@ -285,6 +321,8 @@ int main() { OtherTitle(); TrailingWildcards(); SaveSize(); + BlockSize(); + CheckNullArguments(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); status = 1; diff --git a/core/libs/tests/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 c117778e49..60298ce61c 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -5,12 +5,15 @@ #include #else #include +#include #include #include #include #endif #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/Socket/include/SocketSelectDeadline.hpp" +#include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libSceNet/src/ResolverTestHooks.hpp" #include "SceTypes.hpp" #include #include @@ -24,17 +27,23 @@ #include #include #include +#include extern "C" { extern const std::uint8_t in6addr_any_nid_postfix[16]; extern const std::uint8_t in6addr_loopback_nid_postfix[16]; int APS5_VABI sceNetInit_nid_postfix(void); int APS5_VABI sceNetShowNetstatWithMemory(int); +int APS5_VABI sceNetShowNetstat(void); +int APS5_VABI sceNetShowIfconfig(void); +int APS5_VABI sceNetShowRoute(void); +int APS5_VABI sceNetShowRoute6(void); int APS5_VABI sceNetSocket(const char*, int, int, int); int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI sceNetListen(int, int); int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); int APS5_VABI sceNetGetpeername(int, void*, std::uint32_t*); +int APS5_VABI sceNetGetSockInfo(int, void*, int, int); int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); std::int64_t APS5_VABI sceNetSend(int, const void*, std::size_t, int); @@ -42,10 +51,13 @@ 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*); int* APS5_VABI sceNetErrnoLoc(void); +int APS5_VABI sceNetEtherNtostr(const NetEtherAddr*, char*, std::size_t); +int APS5_VABI sceNetEtherStrton(const char*, NetEtherAddr*); int APS5_VABI sceNetEpollCreate(const char*, int); int APS5_VABI sceNetEpollControl(int, int, int, const NetEpollEvent*); int APS5_VABI sceNetEpollWait(int, NetEpollEvent*, int, int); @@ -60,11 +72,18 @@ int APS5_VABI sceNetResolverStartNtoa6(int, const char*, void*, int, int, int); int APS5_VABI sceNetResolverStartNtoaMultipleRecordsEx(int, const char*, void*, int, int, int); int APS5_VABI sceNetResolverStartNtoaMultipleRecords(int, const char*, void*, int, int, int); int APS5_VABI sceNetResolverStartAton(int, const void*, char*, int, int, int, int); +int APS5_VABI sceNetResolverStartAton6(int, const void*, char*, int, int, int, int); int APS5_VABI sceNetResolverAbort(int, int); int APS5_VABI sceNetResolverDestroy(int); int APS5_VABI sceNetResolverGetError(int, int*); +int APS5_VABI sceNetClearDnsCache(int); +int APS5_VABI sceNetGetDnsInfo(std::uint32_t*, int); +int APS5_VABI sceNetCtlInit(void); int APS5_VABI sceNetCtlGetState(int*); +int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat*); +int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback, void*, int*); +int APS5_VABI sceNetCtlUnregisterCallback(int); int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback, void*, int*); int APS5_VABI sceNetCtlUnregisterCallbackV6(int); std::uint64_t NetCtlReadByteCounter_nid_no_patch(const char*); @@ -73,9 +92,20 @@ int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI close_nid_postfix(int); +bool GuestSocketIsOpen_nid_no_patch(int); int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +struct GuestPollEntry { + int descriptor; + short events; + short revents; +}; +int APS5_VABI poll_nid_postfix(GuestPollEntry*, std::uint32_t, int); int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceNetInetPton(int, const char*, void*); +int APS5_VABI sceNetInetPtonEx(int, const char*, void*, int); +int APS5_VABI sceNetUsleep(int); +void APS5_VABI sceNetGetSystemTime(std::uint64_t*); const char* APS5_VABI sceNetInetNtop(int, const void*, char*, std::uint32_t); int* APS5_VABI sceNetErrnoLoc(void); } @@ -107,16 +137,40 @@ std::int64_t APS5_VABI sceNetRecvmsg(int, NetMsghdr*, int); int APS5_VABI sceNetPoolCreate(const char*, int, int); int APS5_VABI sceNetPoolDestroy(int); int APS5_VABI sceNetGetMemoryPoolStats(int, NetMemoryPoolStats*); +const char* APS5_VABI sceNetGetIfName(std::uint32_t); +int APS5_VABI sceNetGetRandom(std::uint32_t*); +void* APS5_VABI sceNetMemoryAllocate(std::int32_t, int); +void APS5_VABI sceNetMemoryFree(void*); } static void Require(bool condition) { if (!condition) std::abort(); } +static bool FailResolverLookup(const char*, int, std::vector>&) { + return false; +} + +class ScopedResolverLookupOverride { +public: + explicit ScopedResolverLookupOverride(GuestSceNetTest::ResolverLookup lookup) { + GuestSceNetTest::SetResolverLookupForTests_nid_no_patch(lookup); + } + ~ScopedResolverLookupOverride() { + GuestSceNetTest::SetResolverLookupForTests_nid_no_patch(nullptr); + } +}; + +static void APS5_VABI IgnoreNetCtlEvent(int, void*) {} + static bool Failed(std::int64_t result, int error) { return result == static_cast(0x80410100u | static_cast(error)) && *sceNetErrnoLoc() == error; } +static void CheckSocketHostErrors() { + Require(Failed(sceNetSocket(nullptr, 2, 1, 17), 43)); +} + static void CheckStreamSender(int family) { const auto addressSize = family == 2 ? 16u : 28u; const int listener = sceNetSocket(nullptr, family, 1, 6); @@ -203,6 +257,17 @@ static std::string CaptureNetstat(int pool) { } static void CheckNetstat() { + for (const auto diagnostic : {sceNetShowNetstat, sceNetShowIfconfig}) { + bool unsupported = false; + try { + diagnostic(); + } catch (const std::runtime_error&) { + unsupported = true; + } + Require(unsupported); + } + Require(sceNetShowRoute() == 0); + Require(sceNetShowRoute6() == 0); Require(Failed(sceNetShowNetstatWithMemory(-1), 9)); const int pool = sceNetPoolCreate("netstat", 4096, 0); Require(pool > 0); @@ -239,6 +304,64 @@ static void CheckPoolStats() { Require(Failed(sceNetGetMemoryPoolStats(pool, &stats), 9)); } +static void CheckIfName() { + constexpr std::array names = { + "lo0", "eth0", "eth1", "dbg0", "wlan0", "wlan1", "gbe0", "bt0", "phone0", "pppoe0"}; + for (std::uint32_t index = 0; index < names.size(); ++index) { + Require(std::strcmp(sceNetGetIfName(index), names[index]) == 0); + } + *sceNetErrnoLoc() = 0; + const char* missing = sceNetGetIfName(10); + Require(missing != nullptr && missing[0] == '\0' && *sceNetErrnoLoc() == 22); + *sceNetErrnoLoc() = 0; + missing = sceNetGetIfName(0xffffffffu); + Require(missing != nullptr && missing[0] == '\0' && *sceNetErrnoLoc() == 22); +} + +static void CheckRandom() { + Require(Failed(sceNetGetRandom(nullptr), 22)); + std::array values{}; + for (auto& value : values) Require(sceNetGetRandom(&value) == 0); + std::array guarded{0x5a5a5a5au, 0x5a5a5a5au}; + Require(sceNetGetRandom(guarded.data()) == 0 && guarded[1] == 0x5a5a5a5au && *sceNetErrnoLoc() == 22); + Require(std::any_of(values.begin() + 1, values.end(), [&](std::uint32_t value) { return value != values[0]; })); +} + +static void CheckNetMemory() { + *sceNetErrnoLoc() = 33; + for (const std::int32_t size : {0, -1, INT32_MIN}) { + Require(sceNetMemoryAllocate(size, 0) == nullptr); + Require(sceNetMemoryAllocate(size, 2) == nullptr); + Require(sceNetMemoryAllocate(size, 4) == nullptr); + Require(sceNetMemoryAllocate(size, -1) == nullptr); + } + Require(*sceNetErrnoLoc() == 33); + auto* zeroed = static_cast(sceNetMemoryAllocate(4096, 2)); + Require(zeroed != nullptr); + Require(std::all_of(zeroed, zeroed + 4096, [](std::uint8_t byte) { return byte == 0; })); + std::memset(zeroed, 0xa5, 4096); + auto* plain = static_cast(sceNetMemoryAllocate(64, 0)); + Require(plain != nullptr && plain != zeroed); + std::memset(plain, 0x5a, 64); + Require(zeroed[4095] == 0xa5); + sceNetMemoryFree(plain); + sceNetMemoryFree(zeroed); + for (const int flags : {1, 3, 4, 6, 8, 0x10, 0x100, 0x40000001, -5, INT32_MIN}) { + auto* block = static_cast(sceNetMemoryAllocate(16, flags)); + Require(block != nullptr && reinterpret_cast(block) % 64 == 0); + if ((flags & 2) != 0) Require(std::all_of(block, block + 16, [](std::uint8_t byte) { return byte == 0; })); + std::memset(block, 0x5a, 16); + sceNetMemoryFree(block); + } + for (const std::int32_t size : {1, 7, 256, 65536}) { + void* block = sceNetMemoryAllocate(size, 0); + Require(block != nullptr && reinterpret_cast(block) % 64 == 0); + sceNetMemoryFree(block); + } + sceNetMemoryFree(nullptr); + Require(*sceNetErrnoLoc() == 33); +} + static void CheckUnboundSocketName() { for (const int family : {2, 28}) { for (const int type : {1, 2}) { @@ -257,6 +380,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); @@ -306,17 +564,19 @@ 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}; -#ifdef _WIN32 - Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait), 45)); - Require(Failed(sceNetRecvfrom(receiver, buffer, sizeof(buffer), DontWait, nullptr, nullptr), 45)); - Require(Failed(sceNetRecvmsg(receiver, &receive, DontWait), 45)); - Require(Failed(sceNetSendto(sender, "x", 1, DontWait, address.data(), address.size()), 45)); -#else const auto start = std::chrono::steady_clock::now(); + const int unbound = sceNetSocket("dontwait-unbound", 2, 2, 17); + Require(unbound >= 0); + const auto unboundStart = std::chrono::steady_clock::now(); + Require(Failed(sceNetRecv(unbound, buffer, sizeof(buffer), DontWait), 35)); + Require(Failed(sceNetRecvfrom(unbound, buffer, sizeof(buffer), DontWait, nullptr, nullptr), 35)); + Require(Failed(sceNetRecvmsg(unbound, &receive, DontWait), 35)); + Require(std::chrono::steady_clock::now() - unboundStart < std::chrono::seconds(1)); + Require(sceNetSocketClose(unbound) == 0); Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait), 35)); Require(Failed(sceNetRecvfrom(receiver, buffer, sizeof(buffer), DontWait, nullptr, nullptr), 35)); Require(Failed(sceNetRecvmsg(receiver, &receive, DontWait), 35)); @@ -333,15 +593,17 @@ static void CheckDontWait() { Require(sceNetSend(sender, datagram, sizeof(datagram), DontWait) == sizeof(datagram)); Require(sceNetRecvmsg(receiver, &receive, 0) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait), 35)); -#endif Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), 0x1), 45)); Require(Failed(sceNetSend(sender, "x", 1, Peek), 45)); Require(sceNetSocketClose(receiver) == 0 && sceNetSocketClose(sender) == 0); } 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(); @@ -353,10 +615,40 @@ static void CheckReceiveTimeout(int receiver, int sender, const std::array= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0 && sceNetListen(listener, 1) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0); + const int client = sceNetSocket("stream-timeout-client", 2, 1, 6); + Require(client >= 0 && sceNetConnect(client, address.data(), address.size()) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + const KernelTimeval timeout{0, 50000}; + Require(sceNetSetsockopt(client, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); + char buffer[8]{}; + NetIovec in[] = {{buffer, sizeof(buffer)}}; + NetMsghdr receive{nullptr, 0, in, 1, nullptr, 0, 0}; + const auto start = std::chrono::steady_clock::now(); + Require(Failed(sceNetRecv(client, buffer, sizeof(buffer), 0), 35)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + Require(Failed(sceNetRecvfrom(client, buffer, sizeof(buffer), 0, nullptr, nullptr), 35)); + Require(Failed(sceNetRecvmsg(client, &receive, 0), 35)); + const char payload[] = "late"; + Require(sceNetSend(accepted, payload, sizeof(payload), 0) == sizeof(payload)); + Require(sceNetRecv(client, buffer, sizeof(buffer), 0) == sizeof(payload) && std::strcmp(buffer, payload) == 0); + Require(sceNetSend(client, payload, sizeof(payload), 0) == sizeof(payload)); + std::memset(buffer, 0, sizeof(buffer)); + Require(sceNetRecv(accepted, buffer, sizeof(buffer), 0) == sizeof(payload) && std::strcmp(buffer, payload) == 0); + for (const int descriptor : {client, accepted, listener}) Require(sceNetSocketClose(descriptor) == 0); +} + static void CheckMicrosecondTimeouts(int receiver) { const int receiveTimeout = 50000; const int sendTimeout = 70000; @@ -464,12 +756,15 @@ static void CheckEpollWritable() { } static void RequireTimeout(int socket, int option, int expected) { + KernelTimeval timeout{-1, -1}; + std::uint32_t size = sizeof(timeout); + Require(sceNetGetsockopt(socket, 0xffff, option, &timeout, &size) == 0 && size == sizeof(timeout)); int microseconds = -1; - std::uint32_t size = sizeof(microseconds); - Require(sceNetGetsockopt(socket, 0xffff, option, µseconds, &size) == 0 && size == sizeof(microseconds)); - if (microseconds != expected) { - std::fprintf(stderr, "setsockopt(0x%x) with a struct timeval on a libSceNet socket set %d microseconds, expected %d\n", - option, microseconds, expected); + std::uint32_t length = sizeof(microseconds); + Require(sceNetGetsockopt(socket, 0xffff, option == 0x1006 ? 0x1106 : 0x1105, µseconds, &length) == 0 && length == sizeof(microseconds)); + if (timeout.tv_sec * 1000000 + timeout.tv_usec != expected || microseconds != expected) { + std::fprintf(stderr, "option 0x%x on a libSceNet socket reads back {%lld, %lld} and %d microseconds, expected %d\n", + option, static_cast(timeout.tv_sec), static_cast(timeout.tv_usec), microseconds, expected); std::abort(); } } @@ -491,7 +786,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}; @@ -506,6 +812,26 @@ static void CheckKernelTimeouts() { Require(sceNetSocketClose(descriptor) == 0); } +static void CheckKernelStatusFlags() { + const int descriptor = sceNetSocket("kernel-status", 2, 1, 6); + Require(descriptor >= 0); + Require(fcntl_nid_postfix(descriptor, 3) == 2); + Require(fcntl_nid_postfix(descriptor, 4, 2 | 4) == 0); + Require(fcntl_nid_postfix(descriptor, 3) == (2 | 4)); + int enabled = 0; + std::uint32_t length = sizeof(enabled); + Require(sceNetGetsockopt(descriptor, 0xffff, 0x1200, &enabled, &length) == 0 && enabled == 1); + Require(fcntl_nid_postfix(descriptor, 4, 2) == 0); + Require(fcntl_nid_postfix(descriptor, 3) == 2); + length = sizeof(enabled); + Require(sceNetGetsockopt(descriptor, 0xffff, 0x1200, &enabled, &length) == 0 && enabled == 0); + *__error_nid_postfix() = 0; + Require(fcntl_nid_postfix(descriptor, 4, 0x100) == -1 && *__error_nid_postfix() == 45); + *__error_nid_postfix() = 0; + Require(fcntl_nid_postfix(descriptor, 1) == -1 && *__error_nid_postfix() == 22); + Require(close_nid_postfix(descriptor) == 0); +} + static void CheckAddressText(int family, const char* text) { std::array address{}; Require(sceNetInetPton(family, text, address.data()) == 1); @@ -526,6 +852,213 @@ static void CheckAddressText(int family, const char* text) { Require(sceNetInetNtop(family, address.data(), nullptr, output.size()) == nullptr && *sceNetErrnoLoc() == 22); } +static void CheckInetPtonRejects(int family, const char* text) { + std::array address{}; + address.fill(0xaa); + *sceNetErrnoLoc() = 123; + Require(sceNetInetPton(family, text, address.data()) == 0 && *sceNetErrnoLoc() == 123); + Require(std::all_of(address.begin(), address.end(), [](std::uint8_t byte) { return byte == 0xaa; })); +} + +static void CheckInetPtonWhitespace() { + for (const char* text : {"1.2.3.4 ", "1.2.3.4\t", "1.2.3.4\n", "1.2.3.4\v", "1.2.3.4\f", "1.2.3.4\r", "1.2.3.4 5", "1.2.3.4 x"}) { + std::array address{}; + address.fill(0xaa); + *sceNetErrnoLoc() = 123; + Require(sceNetInetPton(2, text, address.data()) == 1 && *sceNetErrnoLoc() == 123); + Require(address[0] == 1 && address[1] == 2 && address[2] == 3 && address[3] == 4 && address[4] == 0xaa); + } + for (const char* text : {" 1.2.3.4", "1.2.3.4x", "1.2.3.4.", "1.2.3.4\x01", "1.2.3.4\xa0", "1.2. 3.4", "1.2.3", "1.2.3 ", "01.2.3.4", "256.0.0.1"}) { + CheckInetPtonRejects(2, text); + } + for (const char* text : {"::1 ", "::1\t", " ::1", "::1x"}) { + CheckInetPtonRejects(28, text); + } +} + +struct AtonCase { + const char* text; + std::uint32_t address; +}; + +static void CheckInetPtonExParts() { + for (const AtonCase& c : {AtonCase{"0", 0}, AtonCase{"1", 1}, AtonCase{"4294967295", 0xffffffff}, AtonCase{"4294967296", 0}, + AtonCase{"0xffffffff", 0xffffffff}, AtonCase{"0x100000000", 0}, AtonCase{"1.16777215", 0x01ffffff}, + AtonCase{"1.2.65535", 0x0102ffff}, AtonCase{"1.2.3.255", 0x010203ff}, AtonCase{"0x7F.1", 0x7f000001}, + AtonCase{"0X7f.1", 0x7f000001}, AtonCase{"0x", 0}, AtonCase{"0x.1.2.3", 0x00010203}, AtonCase{"0x8.1", 0x08000001}, AtonCase{"0x9", 9}, AtonCase{"1.2.3", 0x01020003}, + AtonCase{"127.1", 0x7f000001}, AtonCase{"010.0.0.1", 0x08000001}, AtonCase{"0377.1", 0xff000001}, + AtonCase{"00000000001.2.3.4", 0x01020304}, AtonCase{"1.2.3.4 x", 0x01020304}, AtonCase{"1.2.3.4\tx", 0x01020304}, + AtonCase{"1.2.3.4\v", 0x01020304}, AtonCase{"1 .2.3.4", 1}, AtonCase{"0.8", 8}, AtonCase{"00.1", 1}, + AtonCase{"09", 9}, AtonCase{"09.1", 0x09000001}, AtonCase{"1.09", 0x01000009}, AtonCase{"0.09", 9}, + AtonCase{"09.09", 0x09000009}, AtonCase{"0919", 919}, AtonCase{"0989", 989}, AtonCase{"0998", 998}, + AtonCase{"1.4294967296", 0x01000000}, AtonCase{"4294967297.1", 0x01000001}, AtonCase{"1.2.3.4294967297", 0x01020301}, + AtonCase{"18446744073709551617", 1}, AtonCase{"0x1ffffffff", 0xffffffff}, AtonCase{"0x100000001.1", 0x01000001}, + AtonCase{"1.0xffffff", 0x01ffffff}, AtonCase{"1.2.0xffff", 0x0102ffff}}) { + std::array address{}; + address.fill(0xaa); + *sceNetErrnoLoc() = 123; + Require(sceNetInetPtonEx(2, c.text, address.data(), 0) == 1 && *sceNetErrnoLoc() == 123); + Require(address[0] == (c.address >> 24) && address[1] == ((c.address >> 16) & 0xff) && address[2] == ((c.address >> 8) & 0xff) && + address[3] == (c.address & 0xff) && address[4] == 0xaa); + } + for (const char* text : {"", "1.16777216", "1.2.65536", "1.2.3.256", "256.1", "0400.1", "0xg.1", "08", "018", "019", "0199", "00919", + "080", "08.1.2.3", "1.08", "0.08", "0919.1", "09x", "1.0x1000000", "1.2.0x10000", "1.2.3.4.5", "1..2.3", + ".1.2.3", "1.2.3.", "1.2.3.4x", " 1.2.3.4", "1.2.3.4\x01", "1.2.3.4\xa0", "+1.2.3.4", "-1.2.3.4"}) { + std::array address{}; + address.fill(0xaa); + *sceNetErrnoLoc() = 123; + Require(sceNetInetPtonEx(2, text, address.data(), 0) == 0 && *sceNetErrnoLoc() == 123); + Require(std::all_of(address.begin(), address.end(), [](std::uint8_t byte) { return byte == 0xaa; })); + } + for (const char* text : {"::1 ", "::1x", " ::1", "1.2.3.4", "::1.2.3", "1:2:3:4:5:6:7:8:9"}) { + std::array address{}; + address.fill(0xaa); + Require(sceNetInetPtonEx(28, text, address.data(), 0) == 0); + Require(std::all_of(address.begin(), address.end(), [](std::uint8_t byte) { return byte == 0xaa; })); + } +} + +static void CheckInetPtonEx() { + std::array address{}; + Require(sceNetInetPtonEx(2, "192.168.1.20", address.data(), 0) == 1); + Require(address[0] == 192 && address[1] == 168 && address[2] == 1 && address[3] == 20); + std::array expected{}; + Require(sceNetInetPton(28, "fe80::1:2", expected.data()) == 1); + address.fill(0); + Require(sceNetInetPtonEx(28, "fe80::1:2", address.data(), 0) == 1 && address == expected); + Require(sceNetInetPtonEx(2, "not an address", address.data(), 0) == 0); + Require(Failed(sceNetInetPtonEx(99, "127.0.0.1", address.data(), 0), 47)); + Require(Failed(sceNetInetPtonEx(2, nullptr, address.data(), 0), 22)); + Require(Failed(sceNetInetPtonEx(2, "127.0.0.1", nullptr, 0), 22)); + for (const int flags : {1, 0x100, -1}) { + address.fill(0); + Require(sceNetInetPtonEx(2, "1.2.3", address.data(), flags) == 1); + Require(address[0] == 1 && address[1] == 2 && address[2] == 0 && address[3] == 3); + } +} + +static void CheckEtherAddress() { + struct GuardedAddress { + std::uint8_t before; + NetEtherAddr address; + std::uint8_t after; + }; + static_assert(sizeof(GuardedAddress) == 8); + const GuardedAddress original{0x12, {{0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5}}, 0x34}; + for (const char* text : {"00:01:02:0A:0b:0F", "00-01-02-0A-0b-0F", "00.01 02+0A/0b_0F", "00:01:02:0A:0b:0F trailing"}) { + auto output = original; + const std::array expected{0, 1, 2, 10, 11, 15}; + *sceNetErrnoLoc() = 33; + Require(sceNetEtherStrton(text, &output.address) == 0); + Require(std::memcmp(output.address.data, expected.data(), expected.size()) == 0); + Require(output.before == original.before && output.after == original.after && *sceNetErrnoLoc() == 33); + } + for (unsigned value = 0; value < 256; ++value) { + NetEtherAddr input{}; + for (unsigned index = 0; index < 6; ++index) input.data[index] = static_cast(value + index * 43); + char text[18]{}; + Require(sceNetEtherNtostr(&input, text, sizeof(text)) == 0); + auto output = original; + Require(sceNetEtherStrton(text, &output.address) == 0); + Require(std::memcmp(input.data, output.address.data, sizeof(input.data)) == 0); + Require(output.before == original.before && output.after == original.after); + } + const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:-. +"; + for (std::size_t index = 0; index + 1 < sizeof(digits); ++index) { + const unsigned digit = index < 36 ? index : index < 62 ? index - 26 : 0x7f; + for (std::size_t position = 0; position < 2; ++position) { + std::string text = "01:23:45:67:89:ab"; + text[position] = digits[index]; + const std::array expected{static_cast(position == 0 ? digit * 16 + 1 : digit), 0x23, 0x45, 0x67, 0x89, 0xab}; + auto output = original; + *sceNetErrnoLoc() = 33; + Require(sceNetEtherStrton(text.c_str(), &output.address) == 0); + Require(std::memcmp(output.address.data, expected.data(), expected.size()) == 0); + Require(output.before == original.before && output.after == original.after && *sceNetErrnoLoc() == 33); + } + } + for (const char* text : {"", "0", "0:1:2:a:B:f", "0-1-2-a-B-f", "0:1-2:a-B:f", "0:1:2:3:4", "0:1:2:3:4:", + "0:1:2:3:4:5:6", "000:1:2:3:4:5", "0:1:2:3:4:555", ":0:1:2:3:4:5", "0::1:2:3:4:5", "0.1.2.3.4.5", + "0x0:1:2:3:4:5", "0:1:2:3:4:g", "+0:1:2:3:4:5", " 0:1:2:3:4:5", "0:1:2:3:4:5 ", "0:1:2:3:4:5\n", "001a2bcdeff0"}) { + auto output = original; + Require(Failed(sceNetEtherStrton(text, &output.address), 22)); + Require(std::memcmp(&output, &original, sizeof(output)) == 0); + } + const std::string valid = "00:11:22:33:44:55"; + std::vector fixed(valid.begin(), valid.end()); + auto fixedOutput = original; + Require(sceNetEtherStrton(fixed.data(), &fixedOutput.address) == 0); + const std::array fixedExpected{0, 0x11, 0x22, 0x33, 0x44, 0x55}; + Require(std::memcmp(fixedOutput.address.data, fixedExpected.data(), fixedExpected.size()) == 0); + Require(fixedOutput.before == original.before && fixedOutput.after == original.after); + for (std::size_t length = 0; length < valid.size(); ++length) { + std::vector text(length + 1, '\0'); + std::memcpy(text.data(), valid.data(), length); + auto output = original; + Require(Failed(sceNetEtherStrton(text.data(), &output.address), 22)); + Require(std::memcmp(&output, &original, sizeof(output)) == 0); + } + for (std::size_t position = 0; position < valid.size(); ++position) { + auto text = valid; + text[position] = '\0'; + auto output = original; + Require(Failed(sceNetEtherStrton(text.c_str(), &output.address), 22)); + Require(std::memcmp(&output, &original, sizeof(output)) == 0); + } + auto output = original; + Require(Failed(sceNetEtherStrton(nullptr, &output.address), 22)); + Require(std::memcmp(&output, &original, sizeof(output)) == 0); + Require(Failed(sceNetEtherStrton(valid.c_str(), nullptr), 22)); + Require(Failed(sceNetEtherStrton(nullptr, nullptr), 22)); + for (std::size_t position = 0; position < valid.size(); ++position) { + auto text = valid; + text[position] = static_cast(0xff); + output = original; + bool threw = false; + try { + Require(sceNetEtherStrton(text.c_str(), &output.address) == 0); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw == (position % 3 != 2)); + if (threw) Require(std::memcmp(&output, &original, sizeof(output)) == 0); + else Require(std::memcmp(output.address.data, fixedExpected.data(), fixedExpected.size()) == 0); + Require(output.before == original.before && output.after == original.after); + } +} + +static void CheckSystemTime() { + *sceNetErrnoLoc() = 22; + sceNetGetSystemTime(nullptr); + Require(*sceNetErrnoLoc() == 22); + std::array output{0x123456789abcdef0ULL, UINT64_MAX, 0xfedcba9876543210ULL}; + std::uint64_t previous = 0; + for (int i = 0; i < 64; ++i) { + output[1] = UINT64_MAX; + const auto before = sceKernelGetProcessTime(); + sceNetGetSystemTime(&output[1]); + const auto after = sceKernelGetProcessTime(); + Require(before <= output[1] && output[1] <= after && previous <= output[1]); + Require(output[0] == 0x123456789abcdef0ULL && output[2] == 0xfedcba9876543210ULL); + Require(*sceNetErrnoLoc() == 22); + previous = output[1]; + } + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + sceNetGetSystemTime(&output[1]); + Require(output[1] > previous); +} + +static void CheckUsleep() { + Require(sceNetUsleep(0) == 0); + const auto start = std::chrono::steady_clock::now(); + Require(sceNetUsleep(2000) == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::microseconds(2000)); + *sceNetErrnoLoc() = 0; + Require(Failed(sceNetUsleep(-1), 22)); + *sceNetErrnoLoc() = 0; + Require(Failed(sceNetUsleep(INT32_MIN), 22)); +} + static void CheckEpollIdent() { std::array address{16, 2, 0, 0, 127, 0, 0, 1}; const int receiver = sceNetSocket("epoll-ident", 2, 2, 17); @@ -552,6 +1085,27 @@ static void CheckEpollIdent() { Require(sceNetSocketClose(sender) == 0 && sceNetSocketClose(receiver) == 0); } +static void CheckTcpNoDelay() { + const int stream = sceNetSocket("tcp-nodelay", 2, 1, 6); + const int datagram = sceNetSocket("tcp-nodelay-udp", 2, 2, 17); + Require(stream >= 0 && datagram >= 0); + int value = -1; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(stream, 6, 1, &value, &size) == 0 && size == sizeof(value) && value == 0); + const int on = 1; + const int off = 0; + Require(sceNetSetsockopt(stream, 6, 1, &on, sizeof(on)) == 0); + Require(sceNetGetsockopt(stream, 6, 1, &value, &size) == 0 && value == 4); + Require(setsockopt_nid_postfix(stream, 6, 1, &off, sizeof(off)) == 0); + Require(sceNetGetsockopt(stream, 6, 1, &value, &size) == 0 && value == 0); + Require(Failed(sceNetSetsockopt(stream, 6, 2, &on, sizeof(on)), 42)); + Require(Failed(sceNetGetsockopt(stream, 6, 2, &value, &size), 42)); + Require(Failed(sceNetSetsockopt(datagram, 6, 1, &on, sizeof(on)), 22)); + Require(Failed(sceNetGetsockopt(datagram, 6, 1, &value, &size), 22)); + Require(Failed(sceNetSetsockopt(datagram, 6, 2, &on, sizeof(on)), 22)); + Require(sceNetSocketClose(stream) == 0 && sceNetSocketClose(datagram) == 0); +} + static bool ipv6Unavailable = false; static bool HostIpv6LoopbackUnavailable() { @@ -671,6 +1225,56 @@ static void CheckStreamAddressLengths() { for (const int descriptor : {accepted, client, listener}) Require(sceNetSocketClose(descriptor) == 0); } +static void CheckKernelSocketOption() { + const int kernel = socket_nid_postfix(2, 1, 0); + Require(kernel >= 0); + const int size = 65536; + Require(sceNetSetsockopt(kernel, 0xffff, 0x1001, &size, sizeof(size)) == 0); + const int one = 1; + Require(sceNetSetsockopt(kernel, 6, 1, &one, sizeof(one)) == 0); + Require(close_nid_postfix(kernel) == 0); + Require(Failed(sceNetSetsockopt(kernel, 0xffff, 0x1001, &size, sizeof(size)), 9)); +} + +static void CheckBufferSizes() { + const int socket = sceNetSocket("buffer-sizes", 2, 2, 17); + Require(socket >= 0); + for (const int option : {0x1001, 0x1002}) { + int value = -1; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, option, &value, &size) == 0 && size == sizeof(value) && value > 0); + } + for (const auto [option, requested] : {std::array{0x1001, 32768}, std::array{0x1002, 65536}}) { + Require(sceNetSetsockopt(socket, 0xffff, option, &requested, sizeof(requested)) == 0); + int value = -1; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, option, &value, &size) == 0 && size == sizeof(value) && value == requested); + } + 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); @@ -698,7 +1302,7 @@ static void CheckUnspecifiedIpv6() { const char payload[] = "IPv6 wildcard receive"; const auto sent = sceNetSendto(sender, payload, sizeof(payload), 0, address.data(), address.size()); - if (sent < 0 && *sceNetErrnoLoc() == 51 && HostIpv6LoopbackUnavailable()) { + if (sent < 0 && (*sceNetErrnoLoc() == 49 || *sceNetErrnoLoc() == 51 || *sceNetErrnoLoc() == 65) && HostIpv6LoopbackUnavailable()) { Require(sceNetSocketClose(sender) == 0); Require(sceNetSocketClose(receiver) == 0); ipv6Unavailable = true; @@ -719,6 +1323,71 @@ static void CheckUnspecifiedIpv6() { Require(sceNetSocketClose(receiver) == 0); } +static void CheckAcceptedNonblocking(int family, bool nonblocking) { + const auto addressSize = family == 2 ? 16u : 28u; + std::array address{}; + address[0] = static_cast(addressSize); + address[1] = static_cast(family); + if (family == 2) { address[4] = 127; address[7] = 1; } + else address[23] = 1; + const int listener = sceNetSocket("accept-mode", family, 1, 6); + Require(listener >= 0 && sceNetBind_nid_postfix(listener, address.data(), addressSize) == 0); + Require(sceNetListen(listener, 1) == 0); + const int mode = nonblocking ? 1 : 0; + Require(sceNetSetsockopt(listener, 0xffff, 0x1200, &mode, sizeof(mode)) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0 && size == addressSize); + const int client = sceNetSocket("accept-client", family, 1, 6); + Require(client >= 0 && sceNetConnect(client, address.data(), addressSize) == 0); + int accepted = -1; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + do { + accepted = sceNetAccept(listener, nullptr, nullptr); + if (accepted >= 0) break; + Require(nonblocking && Failed(accepted, 35)); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } while (std::chrono::steady_clock::now() < deadline); + Require(accepted >= 0); + int inherited = -1; + size = sizeof(inherited); + Require(sceNetGetsockopt(accepted, 0xffff, 0x1200, &inherited, &size) == 0 && size == sizeof(inherited)); + const int status = fcntl_nid_postfix(accepted, 3); + std::printf("Accepted IPv%d mode=%d SO_NBIO=%d F_GETFL=%d\n", family == 2 ? 4 : 6, mode, inherited, status); + std::fflush(stdout); + Require(inherited == mode && status == (2 | (nonblocking ? 4 : 0))); + const int changed = 1 - mode; + Require(sceNetSetsockopt(listener, 0xffff, 0x1200, &changed, sizeof(changed)) == 0); + size = sizeof(inherited); + Require(sceNetGetsockopt(accepted, 0xffff, 0x1200, &inherited, &size) == 0 && inherited == mode); + struct ReceiveResult { std::int64_t bytes; int error; char value; }; + auto receive = std::async(std::launch::async, [accepted] { + char byte = '?'; + const auto received = sceNetRecv(accepted, &byte, sizeof(byte), 0); + return ReceiveResult{received, *sceNetErrnoLoc(), byte}; + }); + const bool ready = receive.wait_for(nonblocking ? std::chrono::milliseconds(2000) : std::chrono::milliseconds(50)) == std::future_status::ready; + const char payload = 'n'; + Require(sceNetSend(client, &payload, sizeof(payload), 0) == sizeof(payload)); + const auto received = receive.get(); + Require(ready == nonblocking); + if (nonblocking) { + Require(received.bytes == static_cast(0x80410123) && received.error == 35 && received.value == '?'); + char byte = '?'; + std::int64_t count = -1; + const auto arrival = std::chrono::steady_clock::now() + std::chrono::seconds(2); + do { + count = sceNetRecv(accepted, &byte, sizeof(byte), 0); + if (count >= 0) break; + Require(Failed(count, 35)); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } while (std::chrono::steady_clock::now() < arrival); + Require(count == sizeof(byte) && byte == payload); + } else { + Require(received.bytes == sizeof(payload) && received.value == payload); + } + for (const int descriptor : {accepted, client, listener}) Require(sceNetSocketClose(descriptor) == 0); +} + static void CheckIpv6SocketsAreIpv6Only() { const int inet = sceNetSocket("ipv4-any", 2, 2, 17); Require(inet >= 0); @@ -768,6 +1437,120 @@ static void CheckSocketError() { Require(sceNetSocketClose(refused) == 0); } +static void CheckCloseKernelSocket() { + const int kernel = socket_nid_postfix(2, 1, 0); + Require(kernel >= 0 && GuestSocketIsOpen_nid_no_patch(kernel)); + Require(sceNetSocketClose(kernel) == 0); + Require(!GuestSocketIsOpen_nid_no_patch(kernel)); + Require(Failed(sceNetSocketClose(kernel), 9)); +} + +static std::array RequireSockInfo(int socket, int type, int flags, const std::array& local, + const std::array& remote) { + std::array records; + records.fill(0xa5); + Require(sceNetGetSockInfo(socket, records.data(), 2, 0) == 1); + std::array expected{}; + std::memcpy(expected.data() + 0x24, &socket, sizeof(socket)); + expected[0x28] = static_cast(type); + std::memcpy(expected.data() + 0x34, local.data() + 4, 4); + std::memcpy(expected.data() + 0x38, remote.data() + 4, 4); + std::memcpy(expected.data() + 0x3c, local.data() + 2, 2); + std::memcpy(expected.data() + 0x3e, remote.data() + 2, 2); + std::memcpy(expected.data() + 0x48, &flags, sizeof(flags)); + for (const auto [offset, option] : {std::array{0x64, 0x1002}, std::array{0x68, 0x1001}}) { + int size = 0; + std::uint32_t length = sizeof(size); + Require(sceNetGetsockopt(socket, 0xffff, option, &size, &length) == 0 && size > 0); + std::memcpy(expected.data() + offset, &size, sizeof(size)); + } + Require(std::equal(expected.begin(), expected.end(), records.begin())); + Require(std::all_of(records.begin() + 0xa0, records.end(), [](auto byte) { return byte == 0xa5; })); + return expected; +} + +static std::vector RequireSockInfoList(int flags) { + const int count = sceNetGetSockInfo(-1, nullptr, 0, flags); + Require(count >= 0); + std::vector records(static_cast(count + 1) * 0xa0, 0xa5); + Require(sceNetGetSockInfo(-1, records.data(), count + 1, flags) == count); + Require(std::all_of(records.end() - 0xa0, records.end(), [](auto byte) { return byte == 0xa5; })); + std::vector sockets; + for (int index = 0; index < count; ++index) { + const auto listed = records.begin() + index * 0xa0; + int socket = 0; + std::memcpy(&socket, &listed[0x24], sizeof(socket)); + Require(sockets.empty() || socket > sockets.back()); + std::array single; + Require(sceNetGetSockInfo(socket, single.data(), 1, 0) == 1 && std::equal(single.begin(), single.end(), listed)); + sockets.push_back(socket); + } + const int one = std::min(count, 1); + std::array first; + first.fill(0xa5); + Require(sceNetGetSockInfo(-1, first.data(), 1, flags) == one && std::equal(first.begin(), first.begin() + one * 0xa0, records.begin())); + Require(std::all_of(first.begin() + one * 0xa0, first.end(), [](auto byte) { return byte == 0xa5; })); + return sockets; +} + +static void CheckSockInfo() { + const auto earlier = RequireSockInfoList(0); + const std::array unnamed{}; + const int datagram = sceNetSocket("sockinfo-datagram", 2, 2, 17); + Require(datagram >= 0); + RequireSockInfo(datagram, 2, 1, unnamed, unnamed); + std::array datagramAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(datagram, datagramAddress.data(), datagramAddress.size()) == 0); + std::uint32_t size = datagramAddress.size(); + Require(sceNetGetsockname(datagram, datagramAddress.data(), &size) == 0 && size == 16); + RequireSockInfo(datagram, 2, 1, datagramAddress, unnamed); + const int listener = sceNetSocket("sockinfo-listener", 2, 1, 6); + std::array listening{16, 2, 0, 0, 127, 0, 0, 1}; + Require(listener >= 0 && sceNetBind_nid_postfix(listener, listening.data(), listening.size()) == 0 && sceNetListen(listener, 1) == 0); + size = listening.size(); + Require(sceNetGetsockname(listener, listening.data(), &size) == 0 && size == 16); + RequireSockInfo(listener, 1, 1, listening, unnamed); + const int client = sceNetSocket("sockinfo-client", 2, 1, 6); + Require(client >= 0 && sceNetConnect(client, listening.data(), listening.size()) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + std::array clientAddress{}; + size = clientAddress.size(); + Require(sceNetGetsockname(client, clientAddress.data(), &size) == 0 && size == 16); + RequireSockInfo(client, 1, 1, clientAddress, listening); + RequireSockInfo(accepted, 1, 1, listening, clientAddress); + Require(sceNetConnect(datagram, listening.data(), listening.size()) == 0); + RequireSockInfo(datagram, 2, 1, datagramAddress, listening); + const int nonblocking = 1; + const std::array buffers{65536, 32768}; + Require(sceNetSetsockopt(datagram, 0xffff, 0x1200, &nonblocking, sizeof(nonblocking)) == 0); + Require(sceNetSetsockopt(datagram, 0xffff, 0x1002, &buffers[0], sizeof(buffers[0])) == 0); + Require(sceNetSetsockopt(datagram, 0xffff, 0x1001, &buffers[1], sizeof(buffers[1])) == 0); + const auto configured = RequireSockInfo(datagram, 2, 0x200001, datagramAddress, listening); + Require(std::memcmp(configured.data() + 0x64, buffers.data(), sizeof(buffers)) == 0); + std::array record; + record.fill(0xa5); + Require(Failed(sceNetGetSockInfo(datagram, nullptr, 1, 0), 22)); + Require(Failed(sceNetGetSockInfo(datagram, record.data(), 0, 0), 22)); + Require(Failed(sceNetGetSockInfo(-1, record.data(), 0, 0), 22)); + for (const int flags : {1, 0x41, 0x1000}) { + Require(Failed(sceNetGetSockInfo(datagram, record.data(), 1, flags), 22)); + Require(Failed(sceNetGetSockInfo(-1, nullptr, 0, flags), 22)); + } + const int inet6 = sceNetSocket("sockinfo-inet6", 28, 2, 17); + Require(inet6 >= 0 ? Failed(sceNetGetSockInfo(inet6, record.data(), 1, 0), 47) : *sceNetErrnoLoc() == 47); + Require(std::all_of(record.begin(), record.end(), [](auto byte) { return byte == 0xa5; })); + std::vector expected = earlier; + expected.insert(expected.end(), {datagram, listener, client, accepted}); + std::sort(expected.begin(), expected.end()); + Require(RequireSockInfoList(0) == expected && RequireSockInfoList(0x40) == expected); + Require(sceNetGetSockInfo(datagram, record.data(), 1, 0x40) == 1 && std::equal(configured.begin(), configured.end(), record.begin())); + if (inet6 >= 0) Require(sceNetSocketClose(inet6) == 0); + for (const int descriptor : {accepted, client, listener, datagram}) Require(sceNetSocketClose(descriptor) == 0); + Require(Failed(sceNetGetSockInfo(datagram, record.data(), 1, 0), 9)); + Require(RequireSockInfoList(0) == earlier); +} + static void CheckDatagramDisconnect() { const int peer = sceNetSocket("disconnect-peer", 2, 2, 17); const int other = sceNetSocket("disconnect-other", 2, 2, 17); @@ -796,7 +1579,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]{}; @@ -806,6 +1589,7 @@ static void CheckDatagramDisconnect() { } int main() { + CheckEtherAddress(); const auto epoch = std::chrono::steady_clock::time_point{}; Require(KernelSocketSelect::DeadlineAfter(epoch, 2, 250000) == epoch + std::chrono::milliseconds(2250)); Require(KernelSocketSelect::DeadlineAfter(epoch, INT64_MAX, 0) == std::chrono::steady_clock::time_point::max()); @@ -817,9 +1601,25 @@ int main() { Require(in6addr_any_nid_postfix[i] == 0); Require(in6addr_loopback_nid_postfix[i] == (i == 15 ? 1 : 0)); } + for (const int flags : {0, 1, -1}) { + Require(sceNetClearDnsCache(flags) == static_cast(0x804101C8)); + Require(*sceNetErrnoLoc() == 200); + } + std::uint32_t randomOutput = 0x12345678; + Require(Failed(sceNetGetRandom(&randomOutput), 200)); + Require(randomOutput == 0x12345678); + Require(Failed(sceNetGetRandom(nullptr), 200)); + Require(Failed(sceNetUsleep(0), 200)); + Require(Failed(sceNetUsleep(-1), 200)); + Require(Failed(sceNetShowNetstat(), 200)); + Require(Failed(sceNetShowIfconfig(), 200)); Require(sceNetInit_nid_postfix() == 0); CheckEpollWritable(); + CheckSocketHostErrors(); CheckPoolStats(); + CheckIfName(); + CheckRandom(); + CheckNetMemory(); CheckNetstat(); CheckEmptyEpoll(); CheckDontWait(); @@ -827,12 +1627,29 @@ int main() { CheckAddressText(2, "255.255.255.255"); CheckAddressText(28, "::1"); CheckAddressText(28, "1234:5678:9abc:def0:1234:5678:9abc:def0"); + CheckInetPtonWhitespace(); + CheckInetPtonExParts(); + CheckInetPtonEx(); + CheckUsleep(); + CheckSystemTime(); CheckEpollIdent(); + CheckTcpNoDelay(); CheckLinkLocalAllNodesIpv6(); CheckUnboundSocketName(); + CheckUnboundDatagramReceive(); + CheckUnboundDatagramShutdown(); + CheckUnboundDatagramLaterAddress(); + CheckUnboundDatagramTimeoutAfterAddress(); CheckUnspecifiedIpv6(); if (!ipv6Unavailable) CheckIpv6SocketsAreIpv6Only(); + for (const int family : {2, 28}) { + if (family == 28 && ipv6Unavailable) continue; + CheckAcceptedNonblocking(family, false); + CheckAcceptedNonblocking(family, true); + } CheckDatagramDisconnect(); + CheckCloseKernelSocket(); + CheckSockInfo(); const int closed = sceNetSocket("closed-by-kernel", 2, 2, 17); Require(closed >= 0); @@ -844,6 +1661,7 @@ int main() { Require(Failed(sceNetSocketClose(closed), 9)); CheckKernelTimeouts(); CheckSocketError(); + CheckKernelStatusFlags(); const int listener = sceNetSocket(nullptr, 2, 1, 6); Require(listener >= 0); @@ -876,6 +1694,9 @@ int main() { CheckDatagramAddressLengths(2); CheckDatagramAddressLengths(28); CheckStreamAddressLengths(); + CheckBufferSizes(); + CheckOnesBroadcast(); + CheckKernelSocketOption(); const int epoll = sceNetEpollCreate("guest-sce-net", 0); Require(epoll >= 0); @@ -986,6 +1807,13 @@ int main() { Require(select_nid_postfix(std::max(kernel_socket, udp_sender) + 1, nullptr, mixed_writable, nullptr, wait_second) == 2); Require(((mixed_writable[kernel_socket / 64] >> (kernel_socket % 64)) & 1u) != 0); Require(((mixed_writable[udp_sender / 64] >> (udp_sender % 64)) & 1u) != 0); + GuestPollEntry net_read{udp_receiver, 1, 0}; + Require(poll_nid_postfix(&net_read, 1, 0) == 0 && net_read.revents == 0); + Require(sceNetSendto(udp_sender, datagram, sizeof(datagram), 0, address.data(), address.size()) == sizeof(datagram)); + Require(poll_nid_postfix(&net_read, 1, 1000) == 1 && net_read.revents == 1); + Require(sceNetRecvfrom(udp_receiver, datagram_result, sizeof(datagram_result), 0, nullptr, nullptr) == sizeof(datagram_result)); + GuestPollEntry mixed_poll[2]{{kernel_socket, 4, 0}, {udp_sender, 4, 0}}; + Require(poll_nid_postfix(mixed_poll, 2, 1000) == 2 && mixed_poll[0].revents == 4 && mixed_poll[1].revents == 4); Require(close_nid_postfix(kernel_socket) == 0); std::uint64_t stdio_readable[16]{1}; std::uint64_t stdio_writable[16]{6}; @@ -994,6 +1822,7 @@ int main() { stdio_readable[0] = 1; Require(select_nid_postfix(1, stdio_readable, nullptr, nullptr, poll_now) == 0 && stdio_readable[0] == 0); CheckReceiveTimeout(udp_receiver, udp_sender, address); + CheckStreamReceiveTimeout(); CheckMicrosecondTimeouts(udp_receiver); CheckMessages(udp_receiver, udp_sender, address); Require(sceNetSocketClose(udp_sender) == 0); @@ -1004,8 +1833,13 @@ int main() { int resolver_error = -1; Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); std::array ipv4{}; - Require(sceNetResolverStartNtoa(resolver, "guest-sce-net.invalid.", ipv4.data(), 5000000, 1, 0) == - static_cast(0x804101E1)); + ipv4.fill(0xA5); + { + ScopedResolverLookupOverride lookupOverride(FailResolverLookup); + Require(sceNetResolverStartNtoa(resolver, "guest-sce-net.invalid.", ipv4.data(), 5000000, 1, 0) == + static_cast(0x804101E1)); + } + Require(std::all_of(ipv4.begin(), ipv4.end(), [](std::uint8_t byte) { return byte == 0xA5; })); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x804101E1)); Require(sceNetResolverStartNtoa(resolver, nullptr, ipv4.data(), 5000000, 1, 0) == static_cast(0x80410116)); @@ -1019,7 +1853,12 @@ int main() { Require(std::all_of(resolved6.begin(), resolved6.end() - 1, [](std::uint8_t byte) { return byte == 0; }) && resolved6[15] == 1); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); Require(sceNetResolverStartNtoa6(resolver, nullptr, resolved6.data(), 5000000, 1, 0) == static_cast(0x80410116)); - Require(sceNetResolverStartNtoa6(resolver, "127.0.0.1", resolved6.data(), 5000000, 1, 0) == static_cast(0x804101E1)); + const auto resolved6BeforeFailure = resolved6; + { + ScopedResolverLookupOverride lookupOverride(FailResolverLookup); + Require(sceNetResolverStartNtoa6(resolver, "127.0.0.1", resolved6.data(), 5000000, 1, 0) == static_cast(0x804101E1)); + } + Require(resolved6 == resolved6BeforeFailure); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x804101E1)); Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); @@ -1036,6 +1875,7 @@ int main() { Require(std::all_of(records.begin() + 32 * record_count, records.begin() + 320, [](std::uint8_t byte) { return byte == 0; })); Require(std::all_of(records.begin() + 328, records.begin() + 384, [](std::uint8_t byte) { return byte == 0; })); Require(std::all_of(records.begin() + 384, records.end(), [](std::uint8_t byte) { return byte == 0xA5; })); + const auto resolvedRecords = records; for (int first = 0; first < record_count; ++first) { for (int second = first + 1; second < record_count; ++second) { Require(std::memcmp(records.data() + 32 * first, records.data() + 32 * second, 4) != 0); @@ -1046,8 +1886,12 @@ int main() { static_cast(0x80410116) && *sceNetErrnoLoc() == 22 && records == resolved); Require(sceNetResolverStartNtoaMultipleRecordsEx(resolver, "localhost", nullptr, 5000000, 1, 0) == static_cast(0x80410116) && *sceNetErrnoLoc() == 22); - Require(sceNetResolverStartNtoaMultipleRecordsEx(resolver, "guest-sce-net.invalid.", records.data(), 5000000, 1, 0) == - static_cast(0x804101E1) && records == resolved); + { + ScopedResolverLookupOverride lookupOverride(FailResolverLookup); + Require(sceNetResolverStartNtoaMultipleRecordsEx(resolver, "guest-sce-net.invalid.", records.data(), 5000000, 1, 0) == + static_cast(0x804101E1)); + } + Require(records == resolvedRecords); std::array plain{}; plain.fill(0xA5); @@ -1058,8 +1902,13 @@ int main() { static_cast(0x80410116) && *sceNetErrnoLoc() == 22 && plain == resolved); Require(sceNetResolverStartNtoaMultipleRecords(resolver, "localhost", nullptr, 5000000, 1, 0) == static_cast(0x80410116) && *sceNetErrnoLoc() == 22); - Require(sceNetResolverStartNtoaMultipleRecords(resolver, "guest-sce-net.invalid", plain.data(), 5000000, 1, 0) == - static_cast(0x804101E1) && plain == resolved); + const auto resolvedPlain = plain; + { + ScopedResolverLookupOverride lookupOverride(FailResolverLookup); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, "guest-sce-net.invalid", plain.data(), 5000000, 1, 0) == + static_cast(0x804101E1)); + } + Require(plain == resolvedPlain); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x804101E1)); for (const int flags : {0x1, 0x10000}) { bool threw = false; @@ -1110,6 +1959,53 @@ int main() { } Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); + std::array loopback6{}; + loopback6[15] = 1; + std::memset(reverse, 0x5A, sizeof(reverse)); + const int reverse6_result = sceNetResolverStartAton6(resolver, loopback6.data(), reverse, sizeof(reverse), 5000000, 1, 0); + Require(reverse6_result == 0 || reverse6_result == static_cast(0x804101E1)); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == reverse6_result); + char host_reverse6[256]{}; + sockaddr_in6 host_loopback6{}; + host_loopback6.sin6_family = AF_INET6; + std::memcpy(&host_loopback6.sin6_addr, loopback6.data(), loopback6.size()); + const bool host_named6 = ::getnameinfo(reinterpret_cast(&host_loopback6), sizeof(host_loopback6), + host_reverse6, sizeof(host_reverse6), nullptr, 0, NI_NAMEREQD) == 0; + Require(host_named6 == (reverse6_result == 0)); + if (reverse6_result == 0) { + Require(std::strcmp(reverse, host_reverse6) == 0); + const auto name_length = static_cast(std::strlen(host_reverse6)); + char exact[256]; + std::memset(exact, 0x5A, sizeof(exact)); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), exact, name_length + 1, 5000000, 1, 0) == 0 && + std::strcmp(exact, host_reverse6) == 0 && static_cast(exact[name_length + 1]) == 0x5A); + std::memset(exact, 0x5A, sizeof(exact)); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), exact, name_length, 5000000, 1, 0) == + static_cast(0x804101E1) && static_cast(exact[0]) == 0x5A); + } else { + Require(static_cast(reverse[0]) == 0x5A); + } + std::memset(reverse, 0x5A, sizeof(reverse)); + Require(sceNetResolverAbort(resolver, 2) == 0); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), reverse, sizeof(reverse), 5000000, 1, 0) == + static_cast(0x80410104) && *sceNetErrnoLoc() == 4 && static_cast(reverse[0]) == 0x5A); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x80410104)); + Require(sceNetResolverAbort(resolver, 1) == 0); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), reverse, sizeof(reverse), 5000000, 1, 0) == reverse6_result); + Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == static_cast(0x80410104)); + Require(sceNetResolverStartAton6(resolver, nullptr, reverse, sizeof(reverse), 5000000, 1, 0) == static_cast(0x80410116) && + *sceNetErrnoLoc() == 22); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), nullptr, sizeof(reverse), 5000000, 1, 0) == static_cast(0x80410116)); + Require(sceNetResolverStartAton6(resolver, loopback6.data(), reverse, 0, 5000000, 1, 0) == static_cast(0x80410116)); + Require(sceNetResolverStartAton6(resolver + 1000, loopback6.data(), reverse, sizeof(reverse), 5000000, 1, 0) == + static_cast(0x80410109) && *sceNetErrnoLoc() == 9); + for (const int flags : {0x1, 0x10000, -1}) { + bool threw = false; + try { sceNetResolverStartAton6(resolver, loopback6.data(), reverse, sizeof(reverse), 5000000, 1, flags); } + catch (const std::runtime_error&) { threw = true; } + Require(threw); + } + const int other = sceNetResolverCreate("guest-sce-net-other", 0, 0); Require(other >= 0 && other != resolver); Require(sceNetResolverAbort(other, 3) == 0); @@ -1118,6 +2014,14 @@ int main() { Require(sceNetResolverAbort(other, 0) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); Require(sceNetResolverAbort(-1, 1) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); Require(sceNetResolverGetError(resolver, nullptr) == static_cast(0x80410116) && *sceNetErrnoLoc() == 22); + Require(sceNetClearDnsCache(0) == 0); + ipv4.fill(0xA5); + Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0 && ipv4[0] == 127); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); + for (const int flags : {1, 2, 0x10000, -1}) { + Require(sceNetClearDnsCache(flags) == static_cast(0x80410116)); + Require(*sceNetErrnoLoc() == 22); + } Require(sceNetResolverDestroy(resolver) == 0); resolver_error = -1; Require(sceNetResolverGetError(resolver, &resolver_error) == static_cast(0x80410109) && @@ -1138,6 +2042,7 @@ int main() { if (!ipv6Unavailable) CheckStreamSender(28); int state = -1; + Require(sceNetCtlInit() == 0); Require(sceNetCtlGetState(&state) == 0); Require(state == 0 || state == 3); Require(sceNetCtlGetIfStat(nullptr) == static_cast(0x80412107)); @@ -1157,6 +2062,34 @@ int main() { } Require(missing); } + { + std::array dns{}; + dns.fill(0xA5A5A5A5u); + const int count = sceNetGetDnsInfo(dns.data(), 0); + Require(count >= 0 && count <= 2 && dns[2] == 0xA5A5A5A5u); + Require(count == 0 || state == 3); + for (int index = count; index < 2; ++index) Require(dns[index] == 0); + for (int index = 0; index < 2; ++index) { + NetCtlInfo info{}; + const int result = sceNetCtlGetInfo(17 + index, &info); + if (result != 0) { + Require(result == static_cast(0x80412108) && count == 0); + continue; + } + const auto* bytes = reinterpret_cast(&dns[index]); + char text[16] = {}; + std::snprintf(text, sizeof(text), "%u.%u.%u.%u", static_cast(bytes[0]), static_cast(bytes[1]), static_cast(bytes[2]), static_cast(bytes[3])); + Require(std::strcmp(text, reinterpret_cast(info.opaque)) == 0); + } + for (const int flags : {0x1, -1}) { + bool threw = false; + try { sceNetGetDnsInfo(dns.data(), flags); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + } + bool threw = false; + try { sceNetGetDnsInfo(nullptr, 0); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + } NetCtlIfStat first{}; first.device = 7; first.reserved[7] = 7; @@ -1169,25 +2102,45 @@ int main() { Require(second.tx_bytes >= first.tx_bytes && second.rx_bytes >= first.rx_bytes); } { - const auto dummyCallback = [](int, void*) {}; int cid = -1; Require(sceNetCtlRegisterCallbackV6(nullptr, nullptr, &cid) == static_cast(0x80412107)); - Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, nullptr) == static_cast(0x80412107)); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, nullptr) == static_cast(0x80412107)); int cids[8] = {}; for (int i = 0; i < 8; ++i) { - Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, &cids[i]) == 0); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, &cids[i]) == 0); Require(cids[i] == i); } - Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, &cid) == static_cast(0x80412103)); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, &cid) == static_cast(0x80412103)); Require(sceNetCtlUnregisterCallbackV6(-1) == static_cast(0x80412105)); Require(sceNetCtlUnregisterCallbackV6(8) == static_cast(0x80412105)); Require(sceNetCtlUnregisterCallbackV6(3) == 0); - Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, &cid) == 0); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, &cid) == 0); Require(cid == 3); for (int i = 0; i < 8; ++i) { Require(sceNetCtlUnregisterCallbackV6(i) == 0); } } + { + int cid = -1; + Require(sceNetCtlRegisterCallback(nullptr, nullptr, &cid) == static_cast(0x80412107)); + Require(sceNetCtlRegisterCallback(IgnoreNetCtlEvent, nullptr, nullptr) == static_cast(0x80412107)); + int cids[8] = {}; + for (int i = 0; i < 8; ++i) { + Require(sceNetCtlRegisterCallback(IgnoreNetCtlEvent, nullptr, &cids[i]) == 0); + Require(cids[i] == i); + } + Require(sceNetCtlRegisterCallback(IgnoreNetCtlEvent, nullptr, &cid) == static_cast(0x80412103)); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, &cid) == 0); + Require(sceNetCtlUnregisterCallback(-1) == static_cast(0x80412105)); + Require(sceNetCtlUnregisterCallback(8) == static_cast(0x80412105)); + Require(sceNetCtlUnregisterCallback(3) == 0); + Require(sceNetCtlRegisterCallback(IgnoreNetCtlEvent, nullptr, &cid) == 0); + Require(cid == 3); + for (int i = 0; i < 8; ++i) { + Require(sceNetCtlUnregisterCallback(i) == 0); + } + Require(sceNetCtlUnregisterCallbackV6(0) == 0); + } if (ipv6Unavailable) { std::puts("skipped the IPv6 socket checks, the host has no usable IPv6 loopback"); return 77; diff --git a/core/libs/tests/GuestSchedPriority.cpp b/core/libs/tests/GuestSchedPriority.cpp index 8da97ce710..5c175d9f37 100644 --- a/core/libs/tests/GuestSchedPriority.cpp +++ b/core/libs/tests/GuestSchedPriority.cpp @@ -1,11 +1,21 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include +#include #include extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" { int APS5_VABI sched_get_priority_max_nid_postfix(int policy); int APS5_VABI sched_get_priority_min_nid_postfix(int policy); int APS5_VABI getpriority_nid_postfix(int which, int who); +int APS5_VABI setpriority_nid_postfix(int which, int who, int niceval); +struct GuestRtPrio { + std::uint16_t type; + std::uint16_t prio; +}; +int APS5_VABI rtprio_thread_nid_postfix(int function, int lwpid, GuestRtPrio* priority); +int APS5_VABI pthread_setconcurrency_nid_postfix(int newLevel); +int APS5_VABI pthread_getconcurrency_nid_postfix(void); int APS5_VABI getpid_nid_postfix(void); int* APS5_VABI __error_nid_postfix(); } @@ -27,6 +37,14 @@ static void RequireError(int which, int who, int error) { *__error_nid_postfix() = 0; Require(getpriority_nid_postfix(which, who) == -1 && *__error_nid_postfix() == error); } +static void RequireSet(int which, int who, int nice) { + *__error_nid_postfix() = 13; + Require(setpriority_nid_postfix(which, who, nice) == 0 && *__error_nid_postfix() == 13); +} +static void RequireSetError(int which, int who, int nice, int error) { + *__error_nid_postfix() = 0; + Require(setpriority_nid_postfix(which, who, nice) == -1 && *__error_nid_postfix() == error); +} int main() { Require(Rejects(-1) && Rejects(2) && Rejects(5)); for (int policy : {1, 3}) { @@ -46,4 +64,38 @@ int main() { RequireError(PrioUser, 1000, 3); RequireError(3, 0, 22); RequireError(-1, self, 22); + RequireSet(PrioProcess, 0, 0); + RequireSet(PrioProcess, self, 5); + RequireSet(PrioProcess, self, -20); + RequireSet(PrioGroup, 0, 20); + RequireSet(PrioUser, 0, -1); + RequireSetError(PrioProcess, self + 1, 0, 3); + RequireSetError(PrioProcess, -1, 0, 3); + RequireSetError(PrioGroup, self + 1, 0, 3); + RequireSetError(PrioUser, 1000, 0, 3); + RequireSetError(3, 0, 0, 22); + RequireSetError(-1, self, 0, 22); + Require(setpriority_nid_postfix(PrioProcess, 0, 10) == 0 && getpriority_nid_postfix(PrioProcess, 0) == 10); + Require(setpriority_nid_postfix(PrioProcess, 0, 99) == 0 && getpriority_nid_postfix(PrioProcess, 0) == 20); + Require(setpriority_nid_postfix(PrioProcess, 0, -99) == 0 && getpriority_nid_postfix(PrioProcess, 0) == -20); + Require(setpriority_nid_postfix(PrioProcess, 0, 0) == 0 && getpriority_nid_postfix(PrioProcess, 0) == 0); + GuestRtPrio rtprio{7, 7}; + bool rtprioThrew = false; + try { rtprio_thread_nid_postfix(0, 0, &rtprio); } catch (const std::exception&) { rtprioThrew = true; } + Require(rtprioThrew); + rtprioThrew = false; + try { rtprio_thread_nid_postfix(1, 0, &rtprio); } catch (const std::exception&) { rtprioThrew = true; } + Require(rtprioThrew); + *__error_nid_postfix() = 0; + Require(rtprio_thread_nid_postfix(2, 0, &rtprio) == -1 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(rtprio_thread_nid_postfix(0, 0, nullptr) == -1 && *__error_nid_postfix() == 14); + *__error_nid_postfix() = 0; + Require(rtprio_thread_nid_postfix(0, self + 1, &rtprio) == -1 && *__error_nid_postfix() == 3); + Require(pthread_getconcurrency_nid_postfix() == 0); + *__error_nid_postfix() = 13; + Require(pthread_setconcurrency_nid_postfix(4) == 0 && pthread_getconcurrency_nid_postfix() == 4 && *__error_nid_postfix() == 13); + *__error_nid_postfix() = 13; + Require(pthread_setconcurrency_nid_postfix(-1) == 22 && *__error_nid_postfix() == 13); + Require(pthread_setconcurrency_nid_postfix(0) == 0 && pthread_getconcurrency_nid_postfix() == 0); } diff --git a/core/libs/tests/GuestScreenShot.cpp b/core/libs/tests/GuestScreenShot.cpp new file mode 100644 index 0000000000..836b70da94 --- /dev/null +++ b/core/libs/tests/GuestScreenShot.cpp @@ -0,0 +1,123 @@ +#include "ProcessParametersFixture.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceScreenShotDisable(); +int APS5_VABI sceScreenShotEnable(); +int APS5_VABI sceScreenShotIsDisabled(int id, int* isDisabled); +int APS5_VABI sceScreenShotDisableNotification(); +int APS5_VABI sceScreenShotEnableNotification(); +int APS5_VABI sceScreenShotSetOverlayImage(const char* path, int offsetX, int offsetY); +int APS5_VABI sceScreenShotSetOverlayImageWithOrigin(const char* path, int offsetX, int offsetY, int origin); +} + +namespace { + +constexpr int ErrorArgument = static_cast(0x80BE0001); +constexpr int ErrorOverlayFile = static_cast(0x80BE0003); +constexpr int ErrorOverlaySize = static_cast(0x80BE000E); + +alignas(16) std::array process{}; +std::string pathPrefix; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "ScreenShot: %s\n", message); + std::abort(); +} + +void InstallProcessParameters(std::uint32_t sdkVersion) { + 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)); + std::memcpy(process.data() + 0x10, &sdkVersion, sizeof(sdkVersion)); + Require(ProcessParametersFixture::Install(process.data(), process.size()), "process parameters were not installed"); +} + +class TempPath { +public: + explicit TempPath(const char* tag) : name(pathPrefix + tag) {} + ~TempPath() { + std::error_code error; + std::filesystem::remove_all(name, error); + } + const char* c_str() const { return name.c_str(); } + void Fill(std::size_t bytes) const { + std::ofstream file(name, std::ios::binary | std::ios::trunc); + const std::string chunk(4096, 'x'); + for (std::size_t written = 0; written < bytes; written += chunk.size()) { + file.write(chunk.data(), static_cast(std::min(chunk.size(), bytes - written))); + } + } + +private: + std::string name; +}; + +void TestDisableState() { + int value = -1; + Require(sceScreenShotIsDisabled(0, &value) == 0 && value == 0, "capture starts enabled"); + Require(sceScreenShotDisable() == 0, "disable"); + Require(sceScreenShotIsDisabled(0, &value) == 0 && value == 1, "disable is reported"); + Require(sceScreenShotEnable() == 0, "enable"); + Require(sceScreenShotIsDisabled(0, &value) == 0 && value == 0, "enable is reported"); + value = 7; + Require(sceScreenShotIsDisabled(-1, &value) == ErrorArgument && value == 7, "id -1 is rejected"); + Require(sceScreenShotIsDisabled(0, nullptr) == ErrorArgument, "null result is rejected"); + Require(sceScreenShotDisableNotification() == 0 && sceScreenShotEnableNotification() == 0, "notification toggles"); +} + +void TestOverlay(bool large) { + const int maxX = large ? 3839 : 1919; + const int maxY = large ? 2159 : 1079; + const std::size_t maxSize = large ? 8u << 20 : 2u << 20; + TempPath image("image"); + image.Fill(4096); + Require(sceScreenShotSetOverlayImage(image.c_str(), 0, 0) == 0, "overlay at the origin"); + Require(sceScreenShotSetOverlayImage(image.c_str(), maxX, maxY) == 0, "overlay at the last offsets"); + Require(sceScreenShotSetOverlayImage(image.c_str(), maxX + 1, 0) == ErrorArgument, "x past the screen"); + Require(sceScreenShotSetOverlayImage(image.c_str(), 0, maxY + 1) == ErrorArgument, "y past the screen"); + Require(sceScreenShotSetOverlayImage(image.c_str(), -1, 0) == ErrorArgument, "negative x"); + Require(sceScreenShotSetOverlayImage(nullptr, 0, 0) == ErrorArgument, "null path"); + const std::string longPath(1024, 'a'); + Require(sceScreenShotSetOverlayImage(longPath.c_str(), 0, 0) == ErrorArgument, "path of 1024 characters"); + TempPath missing("missing"); + Require(sceScreenShotSetOverlayImage(missing.c_str(), 0, 0) == ErrorOverlayFile, "missing file"); + TempPath directory("directory"); + std::filesystem::create_directory(directory.c_str()); + Require(sceScreenShotSetOverlayImage(directory.c_str(), 0, 0) == ErrorOverlayFile, "directory"); + TempPath empty("empty"); + empty.Fill(0); + Require(sceScreenShotSetOverlayImage(empty.c_str(), 0, 0) == ErrorOverlaySize, "empty file"); + TempPath full("full"); + full.Fill(maxSize); + Require(sceScreenShotSetOverlayImage(full.c_str(), 0, 0) == 0, "file at the size limit"); + full.Fill(maxSize + 1); + Require(sceScreenShotSetOverlayImage(full.c_str(), 0, 0) == ErrorOverlaySize, "file past the size limit"); + for (int origin = 1; origin <= 9; ++origin) Require(sceScreenShotSetOverlayImageWithOrigin(image.c_str(), 0, 0, origin) == 0, "origins 1 to 9"); + Require(sceScreenShotSetOverlayImageWithOrigin(image.c_str(), 0, 0, 0) == ErrorArgument, "origin 0"); + Require(sceScreenShotSetOverlayImageWithOrigin(image.c_str(), 0, 0, 10) == ErrorArgument, "origin 10"); + Require(sceScreenShotSetOverlayImageWithOrigin(nullptr, 0, 0, 0) == ErrorArgument, "origin checked first"); +} + +} + +int main(int argc, char** argv) { + const bool large = argc > 1 && std::strcmp(argv[1], "large") == 0; + pathPrefix = std::string("anyps5-screenshot-") + (large ? "large-" : "small-") + std::to_string(std::random_device{}()) + "-"; + InstallProcessParameters(large ? 0x3500000u : 0x34fffffu); + TestDisableState(); + TestOverlay(large); + return 0; +} diff --git a/core/libs/tests/GuestSdkHeap.cpp b/core/libs/tests/GuestSdkHeap.cpp new file mode 100644 index 0000000000..9c02699aa9 --- /dev/null +++ b/core/libs/tests/GuestSdkHeap.cpp @@ -0,0 +1,52 @@ +#include + +extern "C" void HeapTestSdkInitialize(); +extern "C" void HeapTestSdkFinalize(); +extern "C" void* HeapTestAllocatorEntry(unsigned index); + +namespace { +struct SdkLifetime { + SdkLifetime() { HeapTestSdkInitialize(); } + ~SdkLifetime() { HeapTestSdkFinalize(); } +}; +SdkLifetime sdkLifetime; +} + +extern "C" void* SdkMalloc(std::size_t size) { + return reinterpret_cast(HeapTestAllocatorEntry(0))(size); +} +extern "C" void SdkFree(void* pointer) { + reinterpret_cast(HeapTestAllocatorEntry(1))(pointer); +} +extern "C" void* SdkCalloc(std::size_t count, std::size_t size) { + return reinterpret_cast(HeapTestAllocatorEntry(2))(count, size); +} +extern "C" void* SdkRealloc(void* pointer, std::size_t size) { + return reinterpret_cast(HeapTestAllocatorEntry(3))(pointer, size); +} +extern "C" void* SdkMemalign(std::size_t alignment, std::size_t size) { + return reinterpret_cast(HeapTestAllocatorEntry(4))(alignment, size); +} +extern "C" void* SdkReallocalign(void* pointer, std::size_t size, std::size_t alignment) { + return reinterpret_cast(HeapTestAllocatorEntry(5))(pointer, size, alignment); +} +extern "C" int SdkPosixMemalign(void** pointer, std::size_t alignment, std::size_t size) { + return reinterpret_cast(HeapTestAllocatorEntry(6))(pointer, alignment, size); +} +extern "C" int SdkStats(void*) { return 0; } +extern "C" int SdkStatsFast(void*) { return 0; } +extern "C" std::size_t SdkUsableSize(void*) { return 256; } + +#define SDK_EXPORT(function, nid) asm(".globl \"" nid "#guest\"\n.set \"" nid "#guest\", " #function "\n") +SDK_EXPORT(SdkFree, "tIhsqj0qsFE"); +#ifndef SDK_HEAP_PARTIAL +SDK_EXPORT(SdkMalloc, "gQX+4GDQjpM"); +SDK_EXPORT(SdkCalloc, "2X5agFjKxMc"); +SDK_EXPORT(SdkRealloc, "Y7aJ1uydPMo"); +SDK_EXPORT(SdkMemalign, "Ujf3KzMvRmI"); +SDK_EXPORT(SdkReallocalign, "OGybVuPAhAY"); +SDK_EXPORT(SdkPosixMemalign, "cVSk9y8URbc"); +SDK_EXPORT(SdkStats, "CC-BLMBu9-I"); +SDK_EXPORT(SdkStatsFast, "KuOuD58hqn4"); +SDK_EXPORT(SdkUsableSize, "NDcSfcYZRC8"); +#endif 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/GuestSemCanceledWake.cpp b/core/libs/tests/GuestSemCanceledWake.cpp new file mode 100644 index 0000000000..526888bcb0 --- /dev/null +++ b/core/libs/tests/GuestSemCanceledWake.cpp @@ -0,0 +1,166 @@ +#include "prx/libkernel/Pthread/include/Pthread.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread*, const PthreadAttr*, PthreadEntry, void*, const char*); +int APS5_VABI scePthreadJoin(Pthread, void**); +int APS5_VABI scePthreadCancel(Pthread); +int APS5_VABI scePthreadSetcancelstate(int, int*); +int APS5_VABI scePthreadSemInit(PthreadSem*, int, unsigned, const char*); +int APS5_VABI scePthreadSemDestroy(PthreadSem*); +int APS5_VABI scePthreadSemPost(PthreadSem*); +int APS5_VABI scePthreadSemWait(PthreadSem*); +int APS5_VABI scePthreadSemTimedwait(PthreadSem*, KernelUseconds); +int APS5_VABI scePthreadSemGetvalue(PthreadSem*, int*); +} + +namespace { + +void Require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +template +bool Until(TPredicate predicate, int milliseconds = 3000) { + const auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(milliseconds); + while (!predicate()) { + if (std::chrono::steady_clock::now() >= end) return false; + std::this_thread::yield(); + } + return true; +} + +struct Context { + PthreadSem* semaphore; + bool timed; + bool disabled; + std::atomic returned{false}; +}; + +void* APS5_VABI Waiter(void* argument) { + auto& context = *static_cast(argument); + if (context.disabled) Require(scePthreadSetcancelstate(GUEST_CANCEL_DISABLE, nullptr) == 0, "disable cancellation"); + const auto result = context.timed ? scePthreadSemTimedwait(context.semaphore, 10000000) : scePthreadSemWait(context.semaphore); + Require(result == 0, "semaphore wait failed"); + context.returned = true; + return &context; +} + +Pthread Start(Context& context) { + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Waiter, &context, nullptr) == 0, "create waiter"); + Require(Until([&] { + std::lock_guard semaphoreLock((*context.semaphore)->_mutex); + std::lock_guard cancellationLock(thread->cancelLock); + return thread->cancelWait == &(*context.semaphore)->_cv; + }), "waiter did not enter semaphore wait"); + return thread; +} + +bool Awakened(Pthread thread) { + std::lock_guard lock(thread->cancelLock); + return thread->cancelWait == nullptr; +} + +void Join(Pthread thread, void* expected) { + void* result = nullptr; + Require(scePthreadJoin(thread, &result) == 0 && result == expected, "unexpected waiter result"); +} + +int Value(PthreadSem* semaphore) { + int value = -1; + Require(scePthreadSemGetvalue(semaphore, &value) == 0, "get semaphore value"); + return value; +} + +enum class Outcome { Passed, Failed, Retry }; + +Outcome PostThenCancel(bool timed, bool disabled) { + PthreadSem semaphore = nullptr; + Require(scePthreadSemInit(&semaphore, 0, 0, nullptr) == 0, "initialize semaphore"); + Context contexts[2]{{&semaphore, timed, disabled}, {&semaphore, timed, disabled}}; + const auto first = Start(contexts[0]); + const auto second = Start(contexts[1]); + std::unique_lock firstGate(first->cancelLock, std::defer_lock); + std::unique_lock secondGate(second->cancelLock, std::defer_lock); + Require(Until([&] { + firstGate.lock(); + secondGate.lock(); + if (first->cancelWait == &semaphore->_cv && second->cancelWait == &semaphore->_cv) return true; + secondGate.unlock(); + firstGate.unlock(); + return false; + }), "waiters did not remain registered before post"); + Require(scePthreadSemPost(&semaphore) == 0, "post semaphore"); + std::unique_lock semaphoreGate(semaphore->_mutex); + firstGate.unlock(); + secondGate.unlock(); + Require(Until([&] { return Awakened(first) || Awakened(second); }), "no waiter left its condition wait"); + const bool firstAwoke = Awakened(first); + const auto canceledThread = firstAwoke ? first : second; + const auto survivingThread = firstAwoke ? second : first; + auto& canceled = contexts[firstAwoke ? 0 : 1]; + auto& survivor = contexts[firstAwoke ? 1 : 0]; + if (Awakened(survivingThread)) { + semaphoreGate.unlock(); + Require(scePthreadSemPost(&semaphore) == 0, "release a spurious wake attempt"); + Join(first, &contexts[0]); + Join(second, &contexts[1]); + Require(Value(&semaphore) == 0, "spurious wake changed token count"); + Require(scePthreadSemDestroy(&semaphore) == 0, "destroy spurious wake attempt"); + return Outcome::Retry; + } + Require(scePthreadCancel(canceledThread) == 0, "cancel awakened waiter"); + semaphoreGate.unlock(); + Join(canceledThread, disabled ? static_cast(&canceled) : GUEST_CANCELED); + Require(canceled.returned.load() == disabled, "cancellation consumed a semaphore token"); + const bool completed = disabled ? survivor.returned.load() : Until([&] { return survivor.returned.load(); }, 250); + const int value = Value(&semaphore); + std::printf("timed=%d disabled=%d survivor_returned=%d unclaimed_tokens=%d\n", timed, disabled, completed, value); + if (!completed) Require(scePthreadSemPost(&semaphore) == 0, "release surviving waiter"); + Join(survivingThread, &survivor); + Require(scePthreadSemDestroy(&semaphore) == 0, "destroy semaphore"); + return (disabled ? !completed && value == 0 : completed && value == 0) ? Outcome::Passed : Outcome::Failed; +} + +void CancelWithoutPost(bool timed) { + PthreadSem semaphore = nullptr; + Require(scePthreadSemInit(&semaphore, 0, 0, nullptr) == 0, "initialize cancellation-only semaphore"); + Context canceled{&semaphore, timed, false}, survivor{&semaphore, timed, false}; + const auto first = Start(canceled); + const auto second = Start(survivor); + Require(scePthreadCancel(first) == 0, "cancel without post"); + Join(first, GUEST_CANCELED); + Require(!canceled.returned.load() && !survivor.returned.load() && Value(&semaphore) == 0, "cancellation created a semaphore token"); + Require(scePthreadSemPost(&semaphore) == 0, "release cancellation-only survivor"); + Join(second, &survivor); + Require(Value(&semaphore) == 0, "cancellation-only survivor lost a token"); + Require(scePthreadSemDestroy(&semaphore) == 0, "destroy cancellation-only semaphore"); +} + +} + +int main() { + try { + bool correct = true; + for (bool timed : {false, true}) { + CancelWithoutPost(timed); + for (bool disabled : {false, true}) { + Outcome outcome = Outcome::Retry; + for (int attempt = 0; attempt < 8 && outcome == Outcome::Retry; ++attempt) outcome = PostThenCancel(timed, disabled); + Require(outcome != Outcome::Retry, "could not isolate a single awakened waiter"); + correct = outcome == Outcome::Passed && correct; + } + } + Require(correct, "a canceled awakened waiter stranded an available semaphore token"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestSetrlimit.cpp b/core/libs/tests/GuestSetrlimit.cpp index 383b48d06f..a869eec59c 100644 --- a/core/libs/tests/GuestSetrlimit.cpp +++ b/core/libs/tests/GuestSetrlimit.cpp @@ -19,6 +19,10 @@ int* APS5_VABI __error_nid_postfix(); static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EINVAL = 22; static constexpr int GUEST_RLIMIT_DATA = 2; +static constexpr int GUEST_RLIMIT_STACK = 3; +static constexpr int GUEST_RLIMIT_RSS = 5; +static constexpr int GUEST_RLIMIT_MEMLOCK = 6; +static constexpr int GUEST_RLIMIT_NPROC = 7; static constexpr int GUEST_RLIMIT_NOFILE = 8; static constexpr int GUEST_RLIM_NLIMITS = 15; static constexpr std::int64_t GUEST_RLIM_INFINITY = std::numeric_limits::max(); @@ -47,6 +51,20 @@ int main() { RequireError(-1, &infinity, GUEST_EINVAL); RequireError(GUEST_RLIM_NLIMITS, &infinity, GUEST_EINVAL); + static constexpr int unchanged[] = {GUEST_RLIMIT_STACK, GUEST_RLIMIT_RSS, GUEST_RLIMIT_MEMLOCK}; + for (int resource : unchanged) { + ResourceLimit before{}; + Require(getrlimit_nid_postfix(resource, &before) == 0); + *__error_nid_postfix() = 13; + Require(setrlimit_nid_postfix(resource, &before) == 0 && *__error_nid_postfix() == 13); + ResourceLimit after{}; + Require(getrlimit_nid_postfix(resource, &after) == 0 && after.rlim_cur == before.rlim_cur && after.rlim_max == before.rlim_max); + } + ResourceLimit processes{}; + Require(getrlimit_nid_postfix(GUEST_RLIMIT_NPROC, &processes) == 0 && processes.rlim_cur == 1604 && processes.rlim_max == 1604); + Require(Throws(GUEST_RLIMIT_NPROC, processes)); + Require(Throws(GUEST_RLIMIT_STACK, ResourceLimit{0x100000, 0x200000})); + ResourceLimit current{}; Require(getrlimit_nid_postfix(GUEST_RLIMIT_DATA, ¤t) == 0); *__error_nid_postfix() = 13; diff --git a/core/libs/tests/GuestShaderLink.cpp b/core/libs/tests/GuestShaderLink.cpp index 775f2fe1ff..f5b510652b 100644 --- a/core/libs/tests/GuestShaderLink.cpp +++ b/core/libs/tests/GuestShaderLink.cpp @@ -36,14 +36,16 @@ int main() { using namespace ShaderRegs; auto* defaults = static_cast(sceAgcGetRegisterDefaults()); Require(defaults && defaults == sceAgcGetRegisterDefaults()); - Require(defaults == sceAgcGetRegisterDefaults2(0)); + auto* versionedDefaults = static_cast(sceAgcGetRegisterDefaults2(13)); + Require(versionedDefaults != nullptr); + Require(std::memcmp(defaults + 0x20, versionedDefaults + 0x20, 4 * sizeof(std::uint32_t)) == 0); // These are the byte offsets used by native callers, independent of the // private C++ structure used to construct the defaults object. ShaderRegister** contextBlocks = nullptr; std::uint32_t contextCount = 0; std::memcpy(&contextBlocks, defaults, sizeof(contextBlocks)); std::memcpy(&contextCount, defaults + 0x20, sizeof(contextCount)); - Require(contextBlocks && contextBlocks[0] && contextCount == 523); + Require(contextBlocks && contextBlocks[0] && contextCount == 488); bool hasRenderTarget = false; bool hasRasterizer = false; for (std::uint32_t i = 0; i < contextCount; ++i) { diff --git a/core/libs/tests/GuestShare.cpp b/core/libs/tests/GuestShare.cpp index 698b357e1e..0ef5a51bc5 100644 --- a/core/libs/tests/GuestShare.cpp +++ b/core/libs/tests/GuestShare.cpp @@ -3,6 +3,7 @@ #include #include #include +#include extern "C" { int APS5_VABI sceShareCaptureScreenshotExtended(const void* extended_param, std::int32_t* req_id); @@ -27,17 +28,25 @@ int main() { using Capture = int (APS5_VABI*)(const void*, std::int32_t*); std::int32_t reqId = 7; + constexpr std::int32_t invalidParam = static_cast(0x81960002); for (Capture capture : {sceShareCaptureScreenshotExtended, sceShareCaptureVideoClipExtended}) { + std::uint64_t fields[5]{}; + const std::uint64_t size = capture == sceShareCaptureScreenshotExtended ? 24 : 40; reqId = 7; - Require(capture(param, &reqId) == notSupported); - Require(reqId == -1); - - reqId = 7; - Require(capture(nullptr, &reqId) == notSupported); - Require(reqId == -1); - - Require(capture(param, nullptr) == notSupported); - Require(capture(nullptr, nullptr) == notSupported); + Require(capture(fields, &reqId) == invalidParam && reqId == 7); + Require(capture(nullptr, &reqId) == invalidParam && reqId == 7); + fields[0] = size; + Require(capture(fields, &reqId) == notSupported && reqId == -1); + Require(capture(fields, nullptr) == notSupported); + if (size == 40) { + fields[2] = 1; + reqId = 7; + Require(capture(fields, &reqId) == invalidParam && reqId == -1); + fields[3] = 1; + Require(capture(fields, nullptr) == notSupported); + fields[3] |= std::uint64_t{1} << 32; + Require(capture(fields, nullptr) == invalidParam); + } } for (Capture capture : {sceShareCaptureScreenshot, sceShareCaptureVideoClip}) { @@ -53,7 +62,6 @@ int main() { Require(sceShareOpenMenuForContent(param) == notSupported); Require(sceShareOpenMenuForContent(nullptr) == notSupported); - constexpr std::int32_t invalidParam = static_cast(0x81960002); std::uint8_t status[18]; std::memset(status, 0x5a, sizeof(status)); Require(sceShareGetCurrentStatus(1, status) == 0); @@ -71,5 +79,10 @@ int main() { Require(sceShareGetRunningStatus(nullptr) == invalidParam); Require(sceShareSetContentParamForApplicationTitle("title") == 0); Require(sceShareSetContentParamForApplicationTitle(nullptr) == invalidParam); + Require(sceShareSetContentParamForApplicationTitle("") == invalidParam); + std::string title(5119, 'a'); + Require(sceShareSetContentParamForApplicationTitle(title.c_str()) == 0); + title.push_back('a'); + Require(sceShareSetContentParamForApplicationTitle(title.c_str()) == invalidParam); return 0; } diff --git a/core/libs/tests/GuestSharePlay.cpp b/core/libs/tests/GuestSharePlay.cpp index fe2819c276..269d05fb4d 100644 --- a/core/libs/tests/GuestSharePlay.cpp +++ b/core/libs/tests/GuestSharePlay.cpp @@ -4,12 +4,13 @@ #include #include #include +#include extern "C" { int APS5_VABI sceSharePlayInitialize(void*, std::size_t); int APS5_VABI sceSharePlayTerminate(void); int APS5_VABI sceSharePlayGetCurrentConnectionInfoA(SharePlayConnectionInfoA*); -int APS5_VABI sceSharePlayProhibitController(int); +int APS5_VABI sceSharePlayProhibitController(std::uint8_t); } static void Require(bool value) { if (!value) std::abort(); } @@ -26,12 +27,19 @@ static bool Throws(TCall call) { int main() { alignas(16) static unsigned char heap[0x1800]; + constexpr int notInitialized = static_cast(0x810E0004); + constexpr int alreadyInitialized = static_cast(0x810E0003); + constexpr int invalidArgument = static_cast(0x810E0001); + Require(sceSharePlayTerminate() == notInitialized); + Require(sceSharePlayGetCurrentConnectionInfoA(nullptr) == notInitialized); + Require(sceSharePlayProhibitController(2) == notInitialized); Require(sceSharePlayInitialize(nullptr, sizeof(heap)) == 0); + Require(sceSharePlayInitialize(heap, 0) == alreadyInitialized); Require(sceSharePlayTerminate() == 0); Require(sceSharePlayInitialize(heap, sizeof(heap)) == 0); Require(sceSharePlayTerminate() == 0); - Require(Throws([&] { sceSharePlayInitialize(heap, 0); })); - Require(Throws([&] { sceSharePlayInitialize(nullptr, 0); })); + for (const std::size_t size : {std::size_t{0}, std::size_t{1}, sizeof(heap) - 1}) Require(sceSharePlayInitialize(heap, size) == invalidArgument); + Require(sceSharePlayInitialize(nullptr, 0) == 0); SharePlayConnectionInfoA info; std::memset(&info, 0x5a, sizeof(info)); Require(sceSharePlayGetCurrentConnectionInfoA(&info) == 0); @@ -43,5 +51,10 @@ int main() { Require(sceSharePlayProhibitController(1) == 0); Require(sceSharePlayProhibitController(0) == 0); Require(Throws([] { sceSharePlayProhibitController(2); })); - Require(Throws([] { sceSharePlayProhibitController(-1); })); + Require(Throws([] { sceSharePlayProhibitController(255); })); + Require(sceSharePlayTerminate() == 0); + std::memset(&info, 0x5a, sizeof(info)); + Require(sceSharePlayGetCurrentConnectionInfoA(&info) == notInitialized); + for (const auto value : std::as_bytes(std::span(&info, 1))) Require(value == std::byte{0x5a}); + Require(sceSharePlayTerminate() == notInitialized); } diff --git a/core/libs/tests/GuestShmOpen.cpp b/core/libs/tests/GuestShmOpen.cpp index c4f3c16f46..ff4f8fb16c 100644 --- a/core/libs/tests/GuestShmOpen.cpp +++ b/core/libs/tests/GuestShmOpen.cpp @@ -10,6 +10,8 @@ int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); std::int64_t APS5_VABI pread_nid_postfix(int, void*, std::size_t, std::int64_t); std::int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int, const void*, std::size_t, std::int64_t); int APS5_VABI close_nid_postfix(int); +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 __error_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } @@ -37,7 +39,21 @@ int main() { Require(pread_nid_postfix(first, &last, 1, 0x3fff) == 1 && last == 0); Require(pread_nid_postfix(first, &last, 1, 0x4000) == 0); Require(pread_nid_postfix(second, back, sizeof(back), 0) == 0); + auto* view = static_cast(mmap_nid_postfix(nullptr, 0x4000, 3, 0x1, first, 0)); + Require(view != reinterpret_cast(static_cast(-1))); + Require(view[0x1000] == 'a' && view[0x3fff] == 0); + view[0x2000] = 'm'; + Require(pread_nid_postfix(first, &last, 1, 0x2000) == 1 && last == 'm'); + Require(munmap_nid_postfix(const_cast(view), 0x4000) == 0); #ifndef _WIN32 + const unsigned char code[] = {0xb8, 0x2a, 0, 0, 0, 0xc3}; + Require(pwrite_nid_disambig1_nid_postfix(first, code, sizeof(code), 0) == static_cast(sizeof(code))); + void* executable = mmap_nid_postfix(nullptr, 0x4000, 5, 0x1, first, 0); + Require(executable != reinterpret_cast(static_cast(-1))); + int (*function)(); + std::memcpy(&function, &executable, sizeof(function)); + Require(function() == 42); + Require(munmap_nid_postfix(executable, 0x4000) == 0); Require(pwrite_nid_disambig1_nid_postfix(third, text, sizeof(text), 0) == 0); #endif Require(ftruncate_nid_postfix(third, 0x4000) == 0); 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/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index 93a5311f51..a5ca92484b 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -1,9 +1,11 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include #include +#include #include #include using Handler = void (APS5_VABI *)(int); @@ -13,12 +15,17 @@ struct GuestSignalSet { extern "C" { Handler APS5_VABI signal_nid_postfix(int, Handler); int APS5_VABI raise_nid_postfix(int); +int APS5_VABI kill_nid_postfix(int, int); +int APS5_VABI getpid_nid_postfix(); int APS5_VABI pthread_kill_nid_postfix(void*, int); void* APS5_VABI scePthreadSelf(); +int APS5_VABI scePthreadCreate(void** thread, const void* attr, void* (APS5_VABI *entry)(void*), void* arg, const char* name); +int APS5_VABI scePthreadJoin(void* thread, void** retval); int APS5_VABI sigaction_nid_postfix(int, const void*, void*); int APS5_VABI sigprocmask_nid_postfix(int, const void*, void*); int APS5_VABI pthread_sigmask_nid_postfix(int, const void*, void*); int* APS5_VABI __error_nid_postfix(); +int APS5_VABI sigsuspend_nid_postfix(const GuestSignalSet*); int APS5_VABI _is_signal_return_nid_postfix(std::uint64_t); unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int); int APS5_VABI sigpending_nid_postfix(GuestSignalSet*); @@ -29,16 +36,51 @@ struct GuestSigaction { GuestSignalSet mask; }; static_assert(sizeof(GuestSigaction) == 32); +extern GuestSignalSet _sigintr_nid_postfix; static bool Same(const GuestSigaction& left, const GuestSigaction& right) { return left.handler == right.handler && left.flags == right.flags && std::memcmp(&left.mask, &right.mask, sizeof(left.mask)) == 0; } volatile std::sig_atomic_t received = 0; volatile std::uintptr_t handlerReturn = 0; void APS5_VABI InfoCallback(int, void*, void*) {} +struct SuspendWait { + const GuestSignalSet* mask = nullptr; + int result = 0; + int error = 0; +}; +void* APS5_VABI SuspendEntry(void* arg) { + auto& wait = *static_cast(arg); + wait.result = sigsuspend_nid_postfix(wait.mask); + wait.error = *__error_nid_postfix(); + return nullptr; +} +struct SuspendSender { + void* target = nullptr; + int result = -1; +}; +struct EndingThread { + std::atomic release{false}; +}; +void* APS5_VABI EndingEntry(void* arg) { + auto& state = *static_cast(arg); + while (!state.release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + return nullptr; +} +void* APS5_VABI SuspendSend(void* arg) { + auto& sender = *static_cast(arg); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + sender.result = pthread_kill_nid_postfix(sender.target, 30); + return nullptr; +} void APS5_VABI Callback(int value) { received = value; handlerReturn = reinterpret_cast(__builtin_return_address(0)); } +GuestSignalSet maskInHandler{}; +void APS5_VABI MaskCallback(int value) { + received = value; + sigprocmask_nid_postfix(1, nullptr, &maskInHandler); +} static void Require(bool value) { if (!value) std::abort(); } int main() { const auto invalid = reinterpret_cast(static_cast(-1)); @@ -62,12 +104,32 @@ int main() { Require(pthread_kill_nid_postfix(nullptr, 128) == 3); Require(pthread_kill_nid_postfix(nullptr, 129) == 22); Require(pthread_kill_nid_postfix(reinterpret_cast(std::uintptr_t{0x10}), 15) == 3); + Require(pthread_kill_nid_postfix(reinterpret_cast(std::uintptr_t{0x10}), 0) == 3); + { + EndingThread ending; + void* thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, EndingEntry, &ending, nullptr) == 0); + Require(pthread_kill_nid_postfix(thread, 0) == 0); + ending.release.store(true); + int answer = 0; + for (int attempt = 0; attempt < 2000 && (answer = pthread_kill_nid_postfix(thread, 0)) != 3; ++attempt) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(answer == 3); + Require(pthread_kill_nid_postfix(thread, 15) == 3); + Require(scePthreadJoin(thread, nullptr) == 0); + Require(pthread_kill_nid_postfix(thread, 0) == 3); + } Require(received == 0); Require(signal_nid_postfix(15, ignore) != invalid); received = 0; Require(raise_nid_postfix(15) == 0 && received == 0); Require(signal_nid_postfix(15, nullptr) == ignore); Require(raise_nid_postfix(100) == -1 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + received = 0; + Require(raise_nid_postfix(0) == 0 && received == 0 && *__error_nid_postfix() == 0); + Require(raise_nid_postfix(129) == -1 && *__error_nid_postfix() == 22); + Require(raise_nid_postfix(-1) == -1 && *__error_nid_postfix() == 22); GuestSignalSet blocked{{0x20, 0, 0, 0}}; GuestSignalSet previous{{}}; Require(sigprocmask_nid_postfix(3, &blocked, &previous) == 0); @@ -87,6 +149,17 @@ int main() { Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == 0x20); Require(pthread_sigmask_nid_postfix(2, &blocked, &previous) == 0 && previous.bits[0] == 0x20); Require(pthread_sigmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == 0); + const GuestSignalSet withKillAndStop{{(1u << 8) | (1u << 16) | 0x20, 0, 0, 0}}; + const GuestSignalSet noSignals{{0, 0, 0, 0}}; + Require(sigprocmask_nid_postfix(1, &withKillAndStop, nullptr) == 0); + Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == 0x20); + Require(sigprocmask_nid_postfix(3, &noSignals, nullptr) == 0); + Require(sigprocmask_nid_postfix(3, &withKillAndStop, nullptr) == 0); + Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == 0x20); + Require(sigprocmask_nid_postfix(3, &noSignals, nullptr) == 0); + Require(pthread_sigmask_nid_postfix(1, &withKillAndStop, nullptr) == 0); + Require(pthread_sigmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == 0x20); + Require(pthread_sigmask_nid_postfix(3, &noSignals, nullptr) == 0); const auto callback = reinterpret_cast(Callback); const auto infoCallback = reinterpret_cast(InfoCallback); @@ -105,6 +178,15 @@ int main() { Require(signal_nid_postfix(15, Callback) == nullptr); Require(sigaction_nid_postfix(15, nullptr, ¤t) == 0 && current.handler == callback && current.flags == 0x2); Require(signal_nid_postfix(15, nullptr) == Callback); + for (const int flags : {0x80, 0x100, 0x8000, 0x40 | 0x80}) { + const GuestSigaction undefinedFlags{callback, flags, {}}; + *__error_nid_postfix() = 0; + Require(sigaction_nid_postfix(15, &undefinedFlags, nullptr) == -1 && *__error_nid_postfix() == 22); + Require(sigaction_nid_postfix(15, nullptr, ¤t) == 0 && current.handler == 0 && current.flags == 0x2); + } + const GuestSigaction ignoredWithFlags{1, 0x8000, {}}; + Require(sigaction_nid_postfix(15, &ignoredWithFlags, nullptr) == 0); + Require(sigaction_nid_postfix(15, &defaults, nullptr) == 0); const GuestSigaction crash{infoCallback, 0x40 | 0x10, {}}; for (int recorded : {10, 1, 64, 128}) { @@ -146,6 +228,60 @@ int main() { Require(raise_nid_postfix(15) == 0 && received == 15); Require(sigprocmask_nid_postfix(2, &urgMask, nullptr) == 0); + *__error_nid_postfix() = 0; + bool nullRejected = false; + try { sigsuspend_nid_postfix(nullptr); } catch (const std::invalid_argument&) { nullRejected = true; } + Require(nullRejected); + Require(signal_nid_postfix(30, Callback) != invalid); + const GuestSignalSet user{{1u << 29, 0, 0, 0}}; + const GuestSignalSet empty{{0, 0, 0, 0}}; + Require(sigprocmask_nid_postfix(3, &user, &previous) == 0); + received = 0; + Require(raise_nid_postfix(30) == 0 && received == 0); + *__error_nid_postfix() = 0; + Require(sigsuspend_nid_postfix(&empty) == -1 && *__error_nid_postfix() == 4 && received == 30); + Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == user.bits[0]); + Require(signal_nid_postfix(30, MaskCallback) != invalid); + const GuestSignalSet suspendWithKillAndStop{{(1u << 8) | (1u << 16) | (1u << 28), 0, 0, 0}}; + received = 0; + Require(raise_nid_postfix(30) == 0 && received == 0); + Require(sigsuspend_nid_postfix(&suspendWithKillAndStop) == -1 && received == 30); + Require(maskInHandler.bits[0] == (1u << 28)); + Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == user.bits[0]); + Require(signal_nid_postfix(30, Callback) != invalid); + received = 0; + SuspendWait wait{&empty, 0, 0}; + void* suspender = nullptr; + Require(scePthreadCreate(&suspender, nullptr, SuspendEntry, &wait, nullptr) == 0); + SuspendSender sender{suspender, -1}; + void* senderThread = nullptr; + Require(scePthreadCreate(&senderThread, nullptr, SuspendSend, &sender, nullptr) == 0); + Require(scePthreadJoin(senderThread, nullptr) == 0 && sender.result == 0); + Require(scePthreadJoin(suspender, nullptr) == 0); + Require(wait.result == -1); + Require(wait.error == 4); + Require(received == 30); + Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == user.bits[0]); + Require(sigprocmask_nid_postfix(2, &user, nullptr) == 0); + const GuestSignalSet highBlock{{1u << 29, 1u << 7, 0, 0}}; + const GuestSignalSet suspendMask{{0, 0, 1u << 8, 0}}; + Require(sigprocmask_nid_postfix(3, &highBlock, nullptr) == 0); + Require(signal_nid_postfix(73, Callback) != invalid); + received = 0; + SuspendWait highWait{&suspendMask, 0, 0}; + void* highSuspender = nullptr; + Require(scePthreadCreate(&highSuspender, nullptr, SuspendEntry, &highWait, nullptr) == 0); + SuspendSender highSender{highSuspender, -1}; + void* highSenderThread = nullptr; + Require(scePthreadCreate(&highSenderThread, nullptr, SuspendSend, &highSender, nullptr) == 0); + Require(scePthreadJoin(highSenderThread, nullptr) == 0 && highSender.result == 0); + Require(scePthreadJoin(highSuspender, nullptr) == 0); + Require(highWait.result == -1 && highWait.error == 4 && received == 30); + Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0 && std::memcmp(&previous, &highBlock, sizeof(highBlock)) == 0); + Require(sigprocmask_nid_postfix(3, &empty, nullptr) == 0); + Require(signal_nid_postfix(73, nullptr) == Callback); + signal_nid_postfix(30, nullptr); + Require(GuestAlarm_nid_no_patch(0) == 0); Require(GuestAlarm_nid_no_patch(5) == 0); Require(GuestAlarm_nid_no_patch(3) == 5); @@ -221,4 +357,66 @@ int main() { Require(sigpending_nid_postfix(&pendingSignals) == 0); Require(pendingSignals.bits[0] == 0 && pendingSignals.bits[1] == 0 && pendingSignals.bits[2] == 0 && pendingSignals.bits[3] == 0); Require(signal_nid_postfix(15, nullptr) == Callback); + + const GuestSignalSet highSignals{{0, 1u << 7, 1u << 8, 1u << 31}}; + for (int guest : {40, 73, 128}) Require(signal_nid_postfix(guest, Callback) != invalid); + received = 0; + GuestSignalSet previousHighSignals{}; + Require(sigprocmask_nid_postfix(1, &highSignals, &previousHighSignals) == 0); + Require(previousHighSignals.bits[0] == 0 && previousHighSignals.bits[1] == 0 && + previousHighSignals.bits[2] == 0 && previousHighSignals.bits[3] == 0); + for (int guest : {40, 73, 128}) Require(raise_nid_postfix(guest) == 0 && received == 0); + Require(sigpending_nid_postfix(&pendingSignals) == 0 && + std::memcmp(&pendingSignals, &highSignals, sizeof(highSignals)) == 0); + Require(sigprocmask_nid_postfix(2, &highSignals, &previousHighSignals) == 0); + Require(std::memcmp(&previousHighSignals, &highSignals, sizeof(highSignals)) == 0); + Require(received == 128); + Require(sigpending_nid_postfix(&pendingSignals) == 0 && + pendingSignals.bits[0] == 0 && pendingSignals.bits[1] == 0 && + pendingSignals.bits[2] == 0 && pendingSignals.bits[3] == 0); + for (int guest : {40, 73, 128}) Require(signal_nid_postfix(guest, nullptr) == Callback); + for (const int ignoredByDefault : {16, 19, 20, 23, 28, 29}) { + *__error_nid_postfix() = 0; + received = 0; + Require(raise_nid_postfix(ignoredByDefault) == 0 && received == 0 && *__error_nid_postfix() == 0); + Require(kill_nid_postfix(getpid_nid_postfix(), ignoredByDefault) == 0 && *__error_nid_postfix() == 0); + } + const GuestSignalSet ignoredByDefaultSet{{(1u << 15) | (1u << 19) | (1u << 22) | (1u << 27) | (1u << 28), 0, 0, 0}}; + GuestSignalSet pendingIgnored{}; + Require(sigprocmask_nid_postfix(3, &ignoredByDefaultSet, nullptr) == 0); + for (const int ignoredByDefault : {16, 20, 23, 28, 29}) Require(raise_nid_postfix(ignoredByDefault) == 0); + Require(sigpending_nid_postfix(&pendingIgnored) == 0 && pendingIgnored.bits[0] == 0); + const GuestSignalSet noneBlocked{{0, 0, 0, 0}}; + Require(sigprocmask_nid_postfix(3, &noneBlocked, nullptr) == 0); + const GuestSigaction ignored{1, 0, {}}; + const GuestSigaction restored{0, 0, {}}; + const GuestSignalSet userSignal{{1u << 29, 0, 0, 0}}; + Require(sigaction_nid_postfix(30, &ignored, nullptr) == 0); + *__error_nid_postfix() = 0; + Require(raise_nid_postfix(30) == 0 && kill_nid_postfix(getpid_nid_postfix(), 30) == 0 && *__error_nid_postfix() == 0); + Require(sigprocmask_nid_postfix(3, &userSignal, nullptr) == 0); + Require(raise_nid_postfix(30) == 0); + Require(sigpending_nid_postfix(&pendingIgnored) == 0 && pendingIgnored.bits[0] == 0); + Require(sigprocmask_nid_postfix(3, &noneBlocked, nullptr) == 0); + Require(sigaction_nid_postfix(30, &restored, nullptr) == 0); + Require(_sigintr_nid_postfix.bits[0] == 0 && _sigintr_nid_postfix.bits[1] == 0 && _sigintr_nid_postfix.bits[2] == 0 && + _sigintr_nid_postfix.bits[3] == 0); + GuestSigaction interruptible{}; + Require(signal_nid_postfix(30, Callback) == nullptr); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && interruptible.flags == 0x2); + _sigintr_nid_postfix.bits[0] = 1u << 29; + Require(signal_nid_postfix(30, Callback) == Callback); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && + interruptible.handler == reinterpret_cast(Callback) && interruptible.flags == 0); + signal_nid_postfix(31, Callback); + Require(sigaction_nid_postfix(31, nullptr, &interruptible) == 0 && interruptible.flags == 0x2); + _sigintr_nid_postfix.bits[3] = 1u << 31; + Require(signal_nid_postfix(128, Callback) == nullptr); + Require(sigaction_nid_postfix(128, nullptr, &interruptible) == 0 && interruptible.flags == 0); + _sigintr_nid_postfix.bits[0] = 0; + _sigintr_nid_postfix.bits[3] = 0; + Require(signal_nid_postfix(30, Callback) == Callback); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && interruptible.flags == 0x2); + Require(signal_nid_postfix(30, nullptr) == Callback && signal_nid_postfix(31, nullptr) == Callback && + signal_nid_postfix(128, nullptr) == Callback); } diff --git a/core/libs/tests/GuestSigtimedwait.cpp b/core/libs/tests/GuestSigtimedwait.cpp index 52a65f57d5..0663a3ce2f 100644 --- a/core/libs/tests/GuestSigtimedwait.cpp +++ b/core/libs/tests/GuestSigtimedwait.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -79,6 +80,44 @@ static Pthread StartSenderWithoutDelay(Sender& sender) { static void JoinSender(Pthread thread, const Sender& sender) { Require(scePthreadJoin(thread, nullptr) == 0 && sender.result == 0); } +struct TimeoutSender { + Pthread target; + std::atomic finished{false}; +}; +static void* APS5_VABI SendUnrelatedUntilTimeout(void* arg) { + auto& sender = *static_cast(arg); + for (int round = 0; round < 80 && !sender.finished.load(); ++round) { + Require(pthread_kill_nid_postfix(sender.target, 2) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(25)); + } + if (!sender.finished.load()) Require(pthread_kill_nid_postfix(sender.target, 13) == 0); + return nullptr; +} +static void CheckTimeoutWithUnrelatedSignals() { + const GuestSignalSet blocked{{(1u << 1) | (1u << 12), 0, 0, 0}}; + const GuestSignalSet pipe{{1u << 12, 0, 0, 0}}; + const GuestSignalSet interrupt{{1u << 1, 0, 0, 0}}; + const GuestTimespec timeout{0, 200000000}; + const GuestTimespec poll{0, 0}; + GuestSignalSet previous{}; + Require(_sigprocmask_nid_postfix(1, &blocked, &previous) == 0); + TimeoutSender sender{scePthreadSelf()}; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, SendUnrelatedUntilTimeout, &sender, nullptr) == 0); + GuestSiginfo info{}; + const auto begin = std::chrono::steady_clock::now(); + const int result = sigtimedwait_nid_postfix(&pipe, &info, &timeout); + const int error = *__error_nid_postfix(); + const auto elapsed = std::chrono::steady_clock::now() - begin; + sender.finished.store(true); + Require(scePthreadJoin(thread, nullptr) == 0); + Require(result == -1 && error == 35); + Require(elapsed >= std::chrono::milliseconds(200)); + RequireUntouched(info); + Require(sigtimedwait_nid_postfix(&interrupt, nullptr, &poll) == 2); + Require(sigtimedwait_nid_postfix(&pipe, nullptr, &poll) == -1 && *__error_nid_postfix() == 35); + Require(_sigprocmask_nid_postfix(3, &previous, nullptr) == 0); +} int main() { const GuestSignalSet empty{{0, 0, 0, 0}}; const GuestSignalSet pipe{{1u << 12, 0, 0, 0}}; @@ -149,6 +188,10 @@ int main() { senderThread = StartSender(sender); Require(sigtimedwait_nid_postfix(&pipe, nullptr, nullptr) == 13); JoinSender(senderThread, sender); + senderThread = StartSender(sender); + const GuestTimespec patience{1, 0}; + Require(sigtimedwait_nid_postfix(&pipe, &info, &patience) == 13 && info.signo == 13); + JoinSender(senderThread, sender); Require(signal_nid_postfix(15, Callback) != reinterpret_cast(static_cast(-1))); senderThread = StartSender(sender = Sender{self, 15, -1}); received = 0; @@ -169,4 +212,5 @@ int main() { JoinSender(senderThread, tight); } Require(_sigprocmask_nid_postfix(3, &previous, nullptr) == 0); + CheckTimeoutWithUnrelatedSignals(); } diff --git a/core/libs/tests/GuestSleepCancel.cpp b/core/libs/tests/GuestSleepCancel.cpp new file mode 100644 index 0000000000..463a4a0cbe --- /dev/null +++ b/core/libs/tests/GuestSleepCancel.cpp @@ -0,0 +1,108 @@ +#include "SceTypes.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** result); +int APS5_VABI scePthreadCancel(Pthread thread); +int APS5_VABI scePthreadSetcancelstate(int state, int* oldState); +void APS5_VABI scePthreadTestcancel(); +unsigned int APS5_VABI sleep_nid_postfix(unsigned int seconds); +} + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "%s:%u: requirement failed\n", location.file_name(), location.line()); + std::abort(); +} + +static constexpr auto ObservationLimit = std::chrono::seconds(3); +static constexpr unsigned LongSleep = 10; +static std::atomic ready{false}; +static std::atomic release{false}; +static std::atomic returned{false}; +static bool disabled = false; +static bool pending = false; +static unsigned seconds = LongSleep; +static std::chrono::steady_clock::duration elapsed{}; + +static void* APS5_VABI Sleeper(void*) { + if (disabled) Require(scePthreadSetcancelstate(1, nullptr) == 0); + ready.store(true); + ready.notify_all(); + if (pending) release.wait(false); + const auto start = std::chrono::steady_clock::now(); + Require(sleep_nid_postfix(seconds) == 0); + elapsed = std::chrono::steady_clock::now() - start; + returned.store(true); + if (disabled) { + Require(scePthreadSetcancelstate(0, nullptr) == 0); + scePthreadTestcancel(); + Require(false); + } + return nullptr; +} + +static bool WaitForCompletion(Pthread thread) { + const auto limit = std::chrono::steady_clock::now() + ObservationLimit; + while (!thread->_finished.load(std::memory_order_acquire)) { + if (std::chrono::steady_clock::now() >= limit) return false; + std::this_thread::yield(); + } + return true; +} + +static bool WaitForSleep(Pthread thread) { + const auto limit = std::chrono::steady_clock::now() + ObservationLimit; + for (;;) { + { + std::lock_guard lock(thread->cancelLock); + if (thread->cancelWait != nullptr) return true; + } + if (std::chrono::steady_clock::now() >= limit) return false; + std::this_thread::yield(); + } +} + +int main(int count, char** arguments) { + Require(count == 2); + const std::string_view mode(arguments[1]); + disabled = mode == "disabled"; + const bool zero = mode == "pending"; + pending = zero || disabled; + const bool normal = mode == "normal"; + Require(mode == "blocked" || disabled || pending || normal); + if (disabled || normal) seconds = 1; + if (zero) seconds = 0; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Sleeper, nullptr, nullptr) == 0); + ready.wait(false); + bool wasSleeping = false; + if (!normal) { + if (!pending) wasSleeping = WaitForSleep(thread); + Require(scePthreadCancel(thread) == 0); + release.store(true); + release.notify_all(); + } + const bool completed = WaitForCompletion(thread); + void* result = nullptr; + Require(scePthreadJoin(thread, &result) == 0); + if (!completed) std::fputs("sleep stayed blocked after cancellation\n", stderr); + Require(completed); + if (normal) { + Require(result == nullptr && returned.load()); + } else { + Require(result == reinterpret_cast(std::uintptr_t{1})); + Require(returned.load() == disabled); + if (!pending) Require(wasSleeping); + } + if (normal || disabled) Require(elapsed >= std::chrono::seconds(1)); +} diff --git a/core/libs/tests/GuestSleepWake.cpp b/core/libs/tests/GuestSleepWake.cpp new file mode 100644 index 0000000000..177ef4b990 --- /dev/null +++ b/core/libs/tests/GuestSleepWake.cpp @@ -0,0 +1,130 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); +void KernelWakeSleepers_nid_no_patch(); +void KernelNoteGpuSubmit_nid_no_patch(); +} + +static constexpr KernelUseconds WAKEABLE_MICROS = 1000; +static constexpr KernelUseconds LONG_MICROS = 2000; +static constexpr unsigned EARLY_WAKES_PER_RUN = 8; +static constexpr unsigned TRIES = 20; +static constexpr auto EARLY = std::chrono::microseconds(900); + +static void Require(bool value) { if (!value) std::abort(); } + +static std::chrono::microseconds Usleep(KernelUseconds microseconds) { + const auto start = std::chrono::steady_clock::now(); + Require(sceKernelUsleep_nid_postfix(microseconds) == 0); + return std::chrono::duration_cast(std::chrono::steady_clock::now() - start); +} + +static void Idle(std::chrono::microseconds length) { + const auto until = std::chrono::steady_clock::now() + length; + while (std::chrono::steady_clock::now() < until) std::this_thread::yield(); +} + +class Waker { +public: + Waker() : thread([this] { + while (!stop.load()) { + KernelWakeSleepers_nid_no_patch(); + std::this_thread::sleep_for(std::chrono::microseconds(50)); + } + }) {} + ~Waker() { + stop.store(true); + thread.join(); + } + +private: + std::atomic stop{false}; + std::thread thread; +}; + +#ifdef _WIN32 +static void CoarseWaitsKeepEverySleepFull() { + Waker waker; + for (unsigned i = 0; i < 12; ++i) { + KernelNoteGpuSubmit_nid_no_patch(); + Require(Usleep(WAKEABLE_MICROS) >= std::chrono::microseconds(WAKEABLE_MICROS)); + } +} +#endif + +static void SleepWithoutWakeIsFull() { + for (unsigned i = 0; i < 4; ++i) Require(Usleep(WAKEABLE_MICROS) >= std::chrono::microseconds(WAKEABLE_MICROS)); +} + +static void WakeEndsShortSleep() { + Waker waker; + bool early = false; + for (unsigned i = 0; i < TRIES && !early; ++i) { + KernelNoteGpuSubmit_nid_no_patch(); + early = Usleep(WAKEABLE_MICROS) < EARLY; + } + Require(early); +} + +static void WakeLeavesLongSleep() { + Waker waker; + for (unsigned i = 0; i < 4; ++i) { + KernelNoteGpuSubmit_nid_no_patch(); + Require(Usleep(LONG_MICROS) >= std::chrono::microseconds(LONG_MICROS)); + } +} + +static void RunOfSleepsEndsEarlyAtMostEightTimes() { + Waker waker; + KernelNoteGpuSubmit_nid_no_patch(); + unsigned early = 0; + for (unsigned i = 0; i < EARLY_WAKES_PER_RUN + 8; ++i) { + if (Usleep(WAKEABLE_MICROS) < std::chrono::microseconds(WAKEABLE_MICROS)) ++early; + } + Require(early <= EARLY_WAKES_PER_RUN); + for (unsigned i = 0; i < 4; ++i) Require(Usleep(WAKEABLE_MICROS) >= std::chrono::microseconds(WAKEABLE_MICROS)); +} + +static void SubmitStartsNewRun() { + Waker waker; + for (unsigned i = 0; i < EARLY_WAKES_PER_RUN + 4; ++i) Usleep(WAKEABLE_MICROS); + bool early = false; + for (unsigned i = 0; i < TRIES && !early; ++i) { + KernelNoteGpuSubmit_nid_no_patch(); + early = Usleep(WAKEABLE_MICROS) < EARLY; + } + Require(early); +} + +static void GapStartsNewRun() { + Waker waker; + for (unsigned i = 0; i < EARLY_WAKES_PER_RUN + 4; ++i) Usleep(WAKEABLE_MICROS); + bool early = false; + for (unsigned i = 0; i < TRIES && !early; ++i) { + Idle(std::chrono::microseconds(WAKEABLE_MICROS + 200)); + early = Usleep(WAKEABLE_MICROS) < EARLY; + } + Require(early); +} + +int main() { +#ifdef _WIN32 + if (std::getenv("APS5_COARSE_TIMED_WAITS") != nullptr) { + CoarseWaitsKeepEverySleepFull(); + return 0; + } +#endif + SleepWithoutWakeIsFull(); + WakeEndsShortSleep(); + WakeLeavesLongSleep(); + RunOfSleepsEndsEarlyAtMostEightTimes(); + SubmitStartsNewRun(); + GapStartsNewRun(); + return 0; +} diff --git a/core/libs/tests/GuestSocialScreenDialog.cpp b/core/libs/tests/GuestSocialScreenDialog.cpp new file mode 100644 index 0000000000..61d42e6488 --- /dev/null +++ b/core/libs/tests/GuestSocialScreenDialog.cpp @@ -0,0 +1,72 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceSocialScreenDialogInitialize(void); +int APS5_VABI sceSocialScreenDialogOpen(const void* param); +int APS5_VABI sceSocialScreenDialogUpdateStatus(void); +int APS5_VABI sceSocialScreenDialogGetStatus(void); +int APS5_VABI sceSocialScreenDialogGetResult(void* result); +int APS5_VABI sceSocialScreenDialogClose(void); +int APS5_VABI sceSocialScreenDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + std::int64_t param = 0; + std::int32_t result[2] = {-1, -1}; + + Require(sceSocialScreenDialogTerminate() == kErrNotInitialized); + Require(sceSocialScreenDialogOpen(¶m) == kErrNotInitialized); + Require(sceSocialScreenDialogClose() == kErrNotInitialized); + Require(sceSocialScreenDialogGetResult(result) == kErrNotInitialized); + Require(sceSocialScreenDialogGetStatus() == kStatusNone); + Require(sceSocialScreenDialogUpdateStatus() == kStatusNone); + + Require(sceSocialScreenDialogInitialize() == 0); + Require(sceSocialScreenDialogInitialize() == kErrAlreadyInitialized); + Require(sceSocialScreenDialogGetStatus() == kStatusInitialized); + Require(sceSocialScreenDialogUpdateStatus() == kStatusInitialized); + + Require(sceSocialScreenDialogOpen(nullptr) == kErrArgNull); + Require(sceSocialScreenDialogGetResult(result) == kErrNotFinished); + + Require(sceSocialScreenDialogOpen(¶m) == 0); + Require(sceSocialScreenDialogGetStatus() == kStatusFinished); + Require(sceSocialScreenDialogUpdateStatus() == kStatusFinished); + + Require(sceSocialScreenDialogGetResult(nullptr) == kErrArgNull); + Require(sceSocialScreenDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(sceSocialScreenDialogClose() == 0); + Require(sceSocialScreenDialogTerminate() == 0); + Require(sceSocialScreenDialogTerminate() == kErrNotInitialized); + Require(sceSocialScreenDialogGetStatus() == kStatusNone); + Require(sceSocialScreenDialogUpdateStatus() == kStatusNone); + + Require(sceSocialScreenDialogInitialize() == 0); + Require(sceSocialScreenDialogUpdateStatus() == kStatusInitialized); + Require(sceSocialScreenDialogClose() == 0); + Require(sceSocialScreenDialogUpdateStatus() == kStatusFinished); + result[0] = -1; + Require(sceSocialScreenDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(sceSocialScreenDialogTerminate() == 0); +} diff --git a/core/libs/tests/GuestSocketAddressLength.cpp b/core/libs/tests/GuestSocketAddressLength.cpp new file mode 100644 index 0000000000..d18dd5910c --- /dev/null +++ b/core/libs/tests/GuestSocketAddressLength.cpp @@ -0,0 +1,87 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int Stream = 1; +constexpr int Datagram = 2; +constexpr int ErrorFault = 14; +constexpr int ErrorInvalid = 22; +constexpr int ErrorNameTooLong = 63; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + std::abort(); +} + +bool Fails(long result, int error) { return result == -1 && *__error_nid_postfix() == error; } + +std::array Loopback() { + std::array address{16, 2}; + address[4] = 127; + address[7] = 1; + return address; +} + +void ShortAddressesAreInvalid() { + const int stream = socket_nid_postfix(2, Stream, 0); + const int datagram = socket_nid_postfix(2, Datagram, 0); + Require(stream >= 0 && datagram >= 0, "socket failed"); + const auto address = Loopback(); + Require(Fails(connect_nid_postfix(stream, address.data(), 1), ErrorInvalid), "connect with a length below the size of sa_family must fail with EINVAL (22)"); + Require(Fails(bind_nid_postfix(stream, address.data(), 0), ErrorInvalid), "bind with length 0 must fail with EINVAL (22)"); + Require(Fails(connect_nid_postfix(stream, address.data(), 3), ErrorInvalid), "connect with an AF_INET address shorter than sockaddr_in must fail with EINVAL (22)"); + Require(Fails(bind_nid_postfix(stream, address.data(), 15), ErrorInvalid), "bind with an AF_INET address shorter than sockaddr_in must fail with EINVAL (22)"); + Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, address.data(), 8), ErrorInvalid), "sendto with an AF_INET address shorter than sockaddr_in must fail with EINVAL (22)"); + std::array v6{28, 28}; + const int stream6 = socket_nid_postfix(28, Stream, 0); + Require(stream6 >= 0, "socket failed"); + Require(Fails(connect_nid_postfix(stream6, v6.data(), 20), ErrorInvalid), "connect with an AF_INET6 address shorter than sockaddr_in6 must fail with EINVAL (22)"); + Require(Fails(bind_nid_postfix(stream, address.data(), 256), ErrorNameTooLong), "an address longer than SOCK_MAXADDRLEN (255) must fail with ENAMETOOLONG (63)"); + Require(Fails(bind_nid_postfix(stream, nullptr, address.size()), ErrorFault), "a null address must fail with EFAULT (14)"); + Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0 && close_nid_postfix(stream6) == 0, "close failed"); +} + +void ArgumentsAreCheckedBeforeTheDescriptor() { + const auto address = Loopback(); + constexpr int ErrorBadDescriptor = 9; + Require(Fails(bind_nid_postfix(-1, address.data(), 1), ErrorInvalid), "bind(-1) with a 1-byte address must fail with EINVAL (22) before the descriptor is looked up"); + Require(Fails(bind_nid_postfix(-1, address.data(), 256), ErrorNameTooLong), "bind(-1) with a 256-byte address must fail with ENAMETOOLONG (63)"); + Require(Fails(bind_nid_postfix(-1, nullptr, address.size()), ErrorFault), "bind(-1) with a null address must fail with EFAULT (14)"); + Require(Fails(connect_nid_postfix(-1, address.data(), 1), ErrorInvalid), "connect(-1) with a 1-byte address must fail with EINVAL (22)"); + Require(Fails(sendto_nid_postfix(-1, "x", 1, 0, address.data(), 1), ErrorInvalid), "sendto(-1) with a 1-byte address must fail with EINVAL (22)"); + Require(Fails(bind_nid_postfix(-1, address.data(), address.size()), ErrorBadDescriptor), "bind(-1) with a valid address must fail with EBADF (9)"); + Require(Fails(connect_nid_postfix(-1, address.data(), address.size()), ErrorBadDescriptor), "connect(-1) with a valid address must fail with EBADF (9)"); + Require(Fails(sendto_nid_postfix(-1, "x", 1, 0, address.data(), address.size()), ErrorBadDescriptor), "sendto(-1) with a valid address must fail with EBADF (9)"); +} + +void FullAddressesStillWork() { + const int stream = socket_nid_postfix(2, Stream, 0); + Require(stream >= 0, "socket failed"); + const auto address = Loopback(); + Require(bind_nid_postfix(stream, address.data(), address.size()) == 0, "bind of a full sockaddr_in failed"); + Require(close_nid_postfix(stream) == 0, "close failed"); +} + +} + +int main() { + ShortAddressesAreInvalid(); + ArgumentsAreCheckedBeforeTheDescriptor(); + FullAddressesStillWork(); + return 0; +} diff --git a/core/libs/tests/GuestSocketConnectListening.cpp b/core/libs/tests/GuestSocketConnectListening.cpp new file mode 100644 index 0000000000..0f42c12bdb --- /dev/null +++ b/core/libs/tests/GuestSocketConnectListening.cpp @@ -0,0 +1,79 @@ +#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; +constexpr int ErrorInvalid = 22; + +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"); + const int connected = socket_nid_postfix(Inet, Stream, 0); + Require(connected >= 0 && connect_nid_postfix(connected, address.data(), address.size()) == 0, "connect to the listener failed"); + *__error_nid_postfix() = 0; + Require(listen_nid_postfix(connected, 1) == -1 && *__error_nid_postfix() == ErrorInvalid, + "listen on a connected socket must fail with EINVAL (22)"); + Require(close_nid_postfix(connected) == 0, "close failed"); + Require(close_nid_postfix(plain) == 0, "close failed"); + std::array wrongFamily{28, 28}; + *__error_nid_postfix() = 0; + 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/GuestSocketDescriptorFlags.cpp b/core/libs/tests/GuestSocketDescriptorFlags.cpp new file mode 100644 index 0000000000..ef8772f738 --- /dev/null +++ b/core/libs/tests/GuestSocketDescriptorFlags.cpp @@ -0,0 +1,109 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI dup_nid_postfix(int); +int APS5_VABI dup2_nid_postfix(int, int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int DuplicateDescriptor = 0; +constexpr int GetDescriptorFlags = 1; +constexpr int SetDescriptorFlags = 2; +constexpr int Duplicate2Descriptor = 10; +constexpr int DuplicateDescriptorCloseOnExec = 17; +constexpr int Duplicate2DescriptorCloseOnExec = 18; +constexpr int CloseOnExec = 1; +constexpr int Stream = 1; +constexpr int SocketCloseOnExec = 0x10000000; +constexpr int GuestEbadf = 9; +constexpr int GuestEinval = 22; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + std::abort(); +} + +} + +int main() { + const int plain = socket_nid_postfix(2, Stream, 0); + Require(plain >= 0, "socket failed"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "a new socket must not be close-on-exec (F_GETFD 0)"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a socket must succeed"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_GETFD must read back FD_CLOEXEC"); + const int copy = dup_nid_postfix(plain); + Require(copy >= 0 && fcntl_nid_postfix(copy, GetDescriptorFlags) == 0, "dup must clear FD_CLOEXEC on the copy"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "dup must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0 && fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + Require(close_nid_postfix(copy) == 0, "close failed"); + + const int target = socket_nid_postfix(2, Stream, 0); + Require(target >= 0, "socket failed"); + Require(fcntl_nid_postfix(target, SetDescriptorFlags, CloseOnExec) == 0 && fcntl_nid_postfix(target, GetDescriptorFlags) == CloseOnExec, "F_SETFD on the dup2 target must succeed"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a socket must succeed"); + Require(dup2_nid_postfix(plain, target) == target, "dup2 onto another socket must return the target"); + Require(fcntl_nid_postfix(target, GetDescriptorFlags) == 0, "dup2 must clear FD_CLOEXEC on the target"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "dup2 must leave the flag of the original"); + Require(dup2_nid_postfix(plain, plain) == plain && fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "dup2 onto the same descriptor must leave the flag"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + Require(close_nid_postfix(target) == 0, "close failed"); + + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a socket must succeed"); + const int minimumDup = fcntl_nid_postfix(plain, DuplicateDescriptor, plain + 4); + Require(minimumDup >= plain + 4 && fcntl_nid_postfix(minimumDup, GetDescriptorFlags) == 0, "F_DUPFD must honor the minimum and clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUPFD must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + const int cloexecDup = fcntl_nid_postfix(plain, DuplicateDescriptorCloseOnExec, minimumDup + 1); + Require(cloexecDup >= minimumDup + 1 && fcntl_nid_postfix(cloexecDup, GetDescriptorFlags) == CloseOnExec, "F_DUPFD_CLOEXEC must honor the minimum and set FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_DUPFD_CLOEXEC must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, DuplicateDescriptor, -1) == -1 && *__error_nid_postfix() == GuestEinval, "F_DUPFD with a negative minimum must fail with EINVAL"); + Require(fcntl_nid_postfix(plain, DuplicateDescriptorCloseOnExec, -1) == -1 && *__error_nid_postfix() == GuestEinval, "F_DUPFD_CLOEXEC with a negative minimum must fail with EINVAL"); + Require(close_nid_postfix(cloexecDup) == 0 && close_nid_postfix(minimumDup) == 0, "close failed"); + + const int dup2Target = socket_nid_postfix(2, Stream, 0); + Require(dup2Target >= 0, "socket failed"); + Require(fcntl_nid_postfix(dup2Target, SetDescriptorFlags, CloseOnExec) == 0 && fcntl_nid_postfix(plain, SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a socket must succeed"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, dup2Target) == dup2Target, "F_DUP2FD onto another socket must return the target"); + Require(fcntl_nid_postfix(dup2Target, GetDescriptorFlags) == 0, "F_DUP2FD must clear FD_CLOEXEC on the target"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, plain) == plain && fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD onto the same descriptor must preserve FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, plain) == plain && fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_DUP2FD onto the same descriptor must preserve a cleared flag"); + Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, dup2Target) == dup2Target, "F_DUP2FD_CLOEXEC onto another socket must return the target"); + Require(fcntl_nid_postfix(dup2Target, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD_CLOEXEC must set FD_CLOEXEC on the target"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_DUP2FD_CLOEXEC must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, plain) == plain && fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD_CLOEXEC onto the same descriptor must set FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, -1) == -1 && *__error_nid_postfix() == GuestEbadf, "F_DUP2FD with a negative target must fail with EBADF"); + Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, -1) == -1 && *__error_nid_postfix() == GuestEbadf, "F_DUP2FD_CLOEXEC with a negative target must fail with EBADF"); + Require(close_nid_postfix(dup2Target) == 0, "close failed"); + + int pipeDescriptors[2] = {-1, -1}; + Require(pipe_nid_postfix(pipeDescriptors) == 0, "pipe failed"); + Require(fcntl_nid_postfix(pipeDescriptors[0], SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a pipe must succeed"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a socket must succeed"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, pipeDescriptors[0]) == pipeDescriptors[0], "F_DUP2FD onto a pipe must return the target"); + Require(fcntl_nid_postfix(pipeDescriptors[0], GetDescriptorFlags) == 0, "F_DUP2FD onto a pipe must clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, pipeDescriptors[1]) == pipeDescriptors[1], "F_DUP2FD_CLOEXEC onto a pipe must return the target"); + Require(fcntl_nid_postfix(pipeDescriptors[1], GetDescriptorFlags) == CloseOnExec, "F_DUP2FD_CLOEXEC onto a pipe must set FD_CLOEXEC"); + Require(close_nid_postfix(pipeDescriptors[0]) == 0 && close_nid_postfix(pipeDescriptors[1]) == 0, "close failed"); + + const int marked = socket_nid_postfix(2, Stream | SocketCloseOnExec, 0); + Require(marked >= 0 && fcntl_nid_postfix(marked, GetDescriptorFlags) == CloseOnExec, "SOCK_CLOEXEC must set FD_CLOEXEC"); + Require(close_nid_postfix(marked) == 0, "close failed"); + const int reused = socket_nid_postfix(2, Stream, 0); + Require(reused >= 0 && fcntl_nid_postfix(reused, GetDescriptorFlags) == 0, "the flag of a closed socket must not reach the next socket"); + Require(close_nid_postfix(reused) == 0 && close_nid_postfix(plain) == 0, "close failed"); + return 0; +} diff --git a/core/libs/tests/GuestSocketDescriptors.cpp b/core/libs/tests/GuestSocketDescriptors.cpp index e8340085ae..9a0ac4f5e6 100644 --- a/core/libs/tests/GuestSocketDescriptors.cpp +++ b/core/libs/tests/GuestSocketDescriptors.cpp @@ -42,6 +42,8 @@ int APS5_VABI close_nid_postfix(int); int APS5_VABI sceKernelClose(int); std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); int APS5_VABI dup_nid_postfix(int); +int APS5_VABI dup2_nid_postfix(int, int); +int APS5_VABI fcntl_nid_postfix(int, int, ...); int APS5_VABI open_nid_postfix(const char*, int, int); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); @@ -58,6 +60,8 @@ static void Check(bool value, int line) { #define Require(value) Check((value), __LINE__) constexpr int DescriptorSetSize = 1024; +constexpr int DuplicateCommand = 0; +constexpr int DuplicateCloseOnExec = 17; constexpr int Inet = 2; constexpr int Datagram = 2; constexpr short In = 0x1; @@ -156,7 +160,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,9 +185,50 @@ static void DuplicateKeepsSocket() { Require(close_nid_postfix(sender) == 0); } +static void SocketNumbersStayInDescriptorSet() { + const int socket = Socket(); + std::vector files; + for (;;) { + const int file = open_nid_postfix("/dev/urandom", 0, 0); + Require(file >= 0); + if (file >= DescriptorSetSize) { + Require(close_nid_postfix(file) == 0); + break; + } + files.push_back(file); + } + Require(dup_nid_postfix(socket) == -1 && *__error_nid_postfix() == 24); + Require(fcntl_nid_postfix(socket, DuplicateCommand, 0) == -1 && *__error_nid_postfix() == 24); + Require(fcntl_nid_postfix(socket, DuplicateCommand, DescriptorSetSize) == -1 && *__error_nid_postfix() == 24); + Require(fcntl_nid_postfix(socket, DuplicateCloseOnExec, 0) == -1 && *__error_nid_postfix() == 24); + Require(fcntl_nid_postfix(socket, DuplicateCloseOnExec, DescriptorSetSize) == -1 && *__error_nid_postfix() == 24); + Require(socket_nid_postfix(Inet, Datagram, 0) == -1 && *__error_nid_postfix() == 24); + Require(dup2_nid_postfix(socket, DescriptorSetSize) == -1 && *__error_nid_postfix() == 24); + for (const int file : files) Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(socket) == 0); +} + +static void SocketTypeErrors() { + constexpr int PrototypeError = 41; + constexpr int ProtocolError = 43; + 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(); + SocketNumbersStayInDescriptorSet(); return 0; } diff --git a/core/libs/tests/GuestSocketEvents.cpp b/core/libs/tests/GuestSocketEvents.cpp index 94bf0656db..1ef4eea859 100644 --- a/core/libs/tests/GuestSocketEvents.cpp +++ b/core/libs/tests/GuestSocketEvents.cpp @@ -16,6 +16,9 @@ int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); int APS5_VABI sceKernelWaitEqueue(KernelEqueue eq, KernelEvent* ev, int num, int* out, const KernelUseconds* timo); @@ -44,7 +47,57 @@ static bool Rejects(int (APS5_VABI *add)(KernelEqueue, int, std::size_t, void*), return false; } +static void PipeReadEvents() { + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "pipe") == SCE_OK); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + const KernelUseconds shortTimeout = 20000; + const KernelUseconds timeout = 1000000; + int marker = 0; + int count = 0; + std::array events{}; + + Require(sceKernelAddReadEvent(eq, pipe[0], 64, &marker) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &shortTimeout) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(count == 0); + Require(write_nid_postfix(pipe[1], "abc", 3) == 3); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].ident == static_cast(pipe[0]) && events[0].filter == EVFILT_READ); + Require(events[0].data == 3 && events[0].udata == &marker && (events[0].flags & EV_EOF) == 0); + char buffer[4]{}; + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 3); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &shortTimeout) == SCE_KERNEL_ERROR_ETIMEDOUT); + + const auto start = std::chrono::steady_clock::now(); + std::thread writer([&pipe] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(write_nid_postfix(pipe[1], "d", 1) == 1); + }); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, nullptr) == SCE_OK); + writer.join(); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + Require(count == 1 && events[0].data == 1); + + Require(close_nid_postfix(pipe[1]) == 0); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].data == 1 && (events[0].flags & EV_EOF) != 0); + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 1); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].data == 0 && (events[0].flags & EV_EOF) != 0); + Require(sceKernelDeleteReadEvent(eq, pipe[0]) == SCE_OK); + Require(sceKernelDeleteReadEvent(eq, pipe[0]) == SCE_KERNEL_ERROR_ENOENT); + + int oneWay[2]; + Require(pipe_nid_postfix(oneWay) == 0); + Require(Rejects(sceKernelAddWriteEvent, eq, oneWay[1], 1)); + Require(close_nid_postfix(oneWay[0]) == 0 && close_nid_postfix(oneWay[1]) == 0); + Require(close_nid_postfix(pipe[0]) == 0); + Require(sceKernelDeleteEqueue(eq) == SCE_OK); +} + int main() { + PipeReadEvents(); const int listener = socket_nid_postfix(2, 1, 6); Require(listener >= 0); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; @@ -73,8 +126,22 @@ int main() { Require(events[0].ident == static_cast(listener)); Require(events[0].filter == EVFILT_READ); Require(events[0].udata == &marker); + Require(events[0].data == 1); + const int second = socket_nid_postfix(2, 1, 0); + Require(second >= 0 && connect_nid_postfix(second, address.data(), address.size()) == 0); +#ifdef __linux__ + const auto queued = std::chrono::steady_clock::now() + std::chrono::seconds(1); + do { + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].ident == static_cast(listener)); + } while (events[0].data != 2 && std::chrono::steady_clock::now() < queued); + Require(events[0].data == 2); +#endif const int accepted = accept_nid_postfix(listener, nullptr, nullptr); Require(accepted >= 0); + const int acceptedSecond = accept_nid_postfix(listener, nullptr, nullptr); + Require(acceptedSecond >= 0); + Require(close_nid_postfix(acceptedSecond) == 0 && close_nid_postfix(second) == 0); Require(sceKernelDeleteReadEvent(eq, listener) == SCE_OK); Require(sceKernelDeleteReadEvent(eq, listener) == SCE_KERNEL_ERROR_ENOENT); @@ -104,11 +171,11 @@ int main() { const int reader = accept_nid_postfix(listener, nullptr, nullptr); Require(reader >= 0); Require(sceKernelAddReadEvent(blocking, reader, 1, nullptr) == SCE_OK); + const auto start = std::chrono::steady_clock::now(); std::thread sender([&] { std::this_thread::sleep_for(std::chrono::milliseconds(50)); Require(send_nid_postfix(writer, message, sizeof(message), 0) == sizeof(message)); }); - const auto start = std::chrono::steady_clock::now(); Require(sceKernelWaitEqueue(blocking, events.data(), events.size(), &count, nullptr) == SCE_OK); sender.join(); Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); diff --git a/core/libs/tests/GuestSocketHostWrite.cpp b/core/libs/tests/GuestSocketHostWrite.cpp index 06e23b986e..df022d1f8a 100644 --- a/core/libs/tests/GuestSocketHostWrite.cpp +++ b/core/libs/tests/GuestSocketHostWrite.cpp @@ -5,12 +5,29 @@ #include #include #include +#include #include #include #include #include #include +struct NetIovec { + void* base; + std::uint64_t length; +}; + +struct NetMsghdr { + void* name; + std::uint32_t nameLength; + NetIovec* iov; + int iovLength; + void* control; + std::uint32_t controlLength; + int flags; +}; +static_assert(sizeof(NetMsghdr) == 48 && offsetof(NetMsghdr, iov) == 16 && offsetof(NetMsghdr, flags) == 44); + extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); @@ -36,6 +53,7 @@ int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); std::int64_t APS5_VABI sceNetSendto(int, const void*, std::size_t, int, const void*, std::uint32_t); std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); std::int64_t APS5_VABI sceNetRecvfrom(int, void*, std::size_t, int, void*, std::uint32_t*); +std::int64_t APS5_VABI sceNetRecvmsg(int, NetMsghdr*, int); int APS5_VABI sceNetSocketClose(int); int* APS5_VABI sceNetErrnoLoc(); int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); @@ -61,7 +79,7 @@ static DWORD Protection(const void* address) { int main(int argc, char** argv) { constexpr std::size_t page = 0x4000; constexpr std::size_t capacity = 32; - constexpr const char* names[] = {"recv", "recvfrom", "read", "sceKernelRead", "sceNetRecv", "sceNetRecvfrom"}; + constexpr const char* names[] = {"recv", "recvfrom", "read", "sceKernelRead", "sceNetRecv", "sceNetRecvfrom", "sceNetRecvmsg"}; Require(argc == 2 || argc == 3, "receive API argument required"); Require(argc != 3 || std::strcmp(argv[2], "tcp") == 0, "unknown transport"); const bool stream = argc == 3; @@ -121,7 +139,13 @@ int main(int argc, char** argv) { case 2: return read_nid_postfix(receiver, buffer, bytes); case 3: return sceKernelRead(receiver, buffer, bytes); case 4: return sceNetRecv(receiver, buffer, bytes, flags); - default: return sceNetRecvfrom(receiver, buffer, bytes, flags, address, addressLength); + case 5: return sceNetRecvfrom(receiver, buffer, bytes, flags, address, addressLength); + default: { + const auto first = std::min(bytes, 3); + NetIovec vectors[]{{buffer, first}, {nullptr, 0}, {static_cast(buffer) + first, bytes - first}}; + NetMsghdr message{nullptr, 0, vectors, 3, nullptr, 0, 0}; + return sceNetRecvmsg(receiver, &message, flags); + } } }; const auto waitForPacket = [&] { @@ -243,6 +267,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"); @@ -257,6 +326,42 @@ int main(int argc, char** argv) { collect(); Require(Protection(buffer) == PAGE_READONLY && Protection(buffer + capacity - 1) == PAGE_READONLY, "failed receive releases the host write guard"); + if (api == 6) { + Require(sceKernelMprotect(static_cast(mapping) + page, page, 1) == 0, + "make the later receive vector read-only"); + send(); + waitForPacket(); + Require(receive(buffer, capacity, 0, nullptr, nullptr) == static_cast(0x8041010eu) && *error == 14, + "a later read-only vector fails with EFAULT"); + Require(std::all_of(buffer - 1, buffer + capacity + 1, [](char value) { return value == '!'; }), + "a rejected vector list leaves every destination unchanged"); + collect(); + Require(Protection(buffer) == PAGE_READONLY, "a later vector failure releases earlier guards"); + std::array recovery{}; + Require(receive(recovery.data(), recovery.size(), 0, nullptr, nullptr) == sizeof(payload), + "a rejected vector list preserves queued data"); + Require(sceKernelMprotect(static_cast(mapping) + page, page, 3) == 0, + "restore the later receive vector"); + NetIovec vectors[]{{buffer, 3}, {nullptr, 0}, {buffer + 3, capacity - 3}}; + auto* message = reinterpret_cast(static_cast(twin) + 32); + const NetMsghdr initial{nullptr, 0, vectors, 3, nullptr, 123, -1}; + { + const GuestArena::HostWrite destination(message, sizeof(*message)); + Require(destination.Open(), "initialize the receive message"); + std::memcpy(message, &initial, sizeof(initial)); + } + Require(sceKernelMprotect(twin, page, 1) == 0, "make the receive message read-only"); + send(); + waitForPacket(); + Require(sceNetRecvmsg(receiver, message, 0) == static_cast(0x8041010eu) && *error == 14, + "read-only message outputs fail with EFAULT"); + Require(message->flags == -1 && message->controlLength == 123, "rejected message outputs unchanged"); + Require(receive(recovery.data(), recovery.size(), 0, nullptr, nullptr) == sizeof(payload), + "a rejected message preserves queued data"); + Require(sceKernelMprotect(twin, page, 3) == 0, "restore receive message write permission"); + reset(); + collect(); + } Require(sceKernelMprotect(mapping, page * 2, 1) == 0, "make guest memory read-only"); send(); waitForPacket(); diff --git a/core/libs/tests/GuestSocketOnNonSocket.cpp b/core/libs/tests/GuestSocketOnNonSocket.cpp new file mode 100644 index 0000000000..953bd473d0 --- /dev/null +++ b/core/libs/tests/GuestSocketOnNonSocket.cpp @@ -0,0 +1,73 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI shutdown_nid_postfix(int, int); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int APS5_VABI getsockopt_nid_postfix(int, int, int, void*, std::uint32_t*); +std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int ErrorBadDescriptor = 9; +constexpr int ErrorNotSocket = 38; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + std::abort(); +} + +bool Fails(long result, int error) { return result == -1 && *__error_nid_postfix() == error; } + +void CallsOnAnOpenPipeAreNotSocketCalls() { + int ends[2]; + Require(pipe_nid_postfix(ends) == 0, "pipe failed"); + std::array address{16, 2}; + std::uint32_t size = address.size(); + char buffer[4]; + int option = 0; + std::uint32_t optionSize = sizeof(option); + Require(Fails(bind_nid_postfix(ends[0], address.data(), address.size()), ErrorNotSocket), "bind on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(listen_nid_postfix(ends[0], 1), ErrorNotSocket), "listen on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(accept_nid_postfix(ends[0], nullptr, nullptr), ErrorNotSocket), "accept on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(connect_nid_postfix(ends[0], address.data(), address.size()), ErrorNotSocket), "connect on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(shutdown_nid_postfix(ends[0], 1), ErrorNotSocket), "shutdown on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(getsockname_nid_postfix(ends[0], address.data(), &size), ErrorNotSocket), "getsockname on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(setsockopt_nid_postfix(ends[0], 0xffff, 4, &option, sizeof(option)), ErrorNotSocket), "setsockopt on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(getsockopt_nid_postfix(ends[0], 0xffff, 4, &option, &optionSize), ErrorNotSocket), "getsockopt on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(send_nid_postfix(ends[1], "x", 1, 0), ErrorNotSocket), "send on a pipe must fail with ENOTSOCK (38)"); + Require(Fails(recv_nid_postfix(ends[0], buffer, sizeof(buffer), 0), ErrorNotSocket), "recv on a pipe must fail with ENOTSOCK (38)"); + Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0, "close failed"); +} + +void ClosedAndInvalidDescriptorsAreBad() { + int ends[2]; + Require(pipe_nid_postfix(ends) == 0, "pipe failed"); + Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0, "close failed"); + std::array address{16, 2}; + Require(Fails(bind_nid_postfix(ends[0], address.data(), address.size()), ErrorBadDescriptor), "bind on a closed descriptor must fail with EBADF (9)"); + Require(Fails(listen_nid_postfix(-1, 1), ErrorBadDescriptor), "listen on -1 must fail with EBADF (9)"); + Require(Fails(connect_nid_postfix(1 << 20, address.data(), address.size()), ErrorBadDescriptor), "connect on a descriptor that was never open must fail with EBADF (9)"); +} + +} + +int main() { + CallsOnAnOpenPipeAreNotSocketCalls(); + ClosedAndInvalidDescriptorsAreBad(); + return 0; +} diff --git a/core/libs/tests/GuestSocketOptions.cpp b/core/libs/tests/GuestSocketOptions.cpp index c9b97d1b4a..7ab20ba2f4 100644 --- a/core/libs/tests/GuestSocketOptions.cpp +++ b/core/libs/tests/GuestSocketOptions.cpp @@ -152,7 +152,9 @@ void BooleanOptionsReportTheirBit() { Require(GetInt(stream, SoKeepAlive) == 0, "SO_KEEPALIVE must turn off"); SetInt(datagram, SoBroadcast, 1); Require(GetInt(datagram, SoBroadcast) == SoBroadcast, "SO_BROADCAST reads back as its bit"); - for (const int option : {SoNoSigpipe, SoReusePort, SoDebug, SoUseLoopback, SoTimestamp}) { + SetInt(stream, SoNoSigpipe, 1); + Require(GetInt(stream, SoNoSigpipe) == SoNoSigpipe, "SO_NOSIGPIPE reads back as its bit"); + for (const int option : {SoReusePort, SoDebug, SoUseLoopback, SoTimestamp}) { const int one = 1; Require(SetFails(stream, SolSocket, option, &one, sizeof(one), ErrorNoProtocolOption), "an option no title needs must fail with ENOPROTOOPT (42)"); int value = 0; diff --git a/core/libs/tests/GuestSocketPair.cpp b/core/libs/tests/GuestSocketPair.cpp index 15dabd1ef6..0b827364c4 100644 --- a/core/libs/tests/GuestSocketPair.cpp +++ b/core/libs/tests/GuestSocketPair.cpp @@ -5,6 +5,7 @@ #include #include #include +#include struct PollDescriptor { int descriptor; @@ -41,11 +42,24 @@ int APS5_VABI poll_nid_postfix(PollDescriptor*, std::uint32_t, int); int APS5_VABI close_nid_postfix(int); std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); int APS5_VABI sceKernelFstat(int, FileStat*); +int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); +int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); +int APS5_VABI sceKernelMunmap(void*, std::size_t); int* APS5_VABI __error_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } +template +static bool ThrowsRuntimeError(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + constexpr int Unix = 1; constexpr int Stream = 1; constexpr int Datagram = 2; @@ -90,7 +104,83 @@ static bool Unnamed(int descriptor, int (APS5_VABI *query)(int, void*, std::uint return query(descriptor, name, &length) == 0 && length == 4 && name[0] == 16 && name[1] == 1 && name[4] == 0xaa; } -int main() { +static bool ReceiveFaults(int descriptor, Msghdr* message) { + *__error_nid_postfix() = 0; + return recvmsg_nid_postfix(descriptor, message, 0) == -1 && *__error_nid_postfix() == 14; +} + +static bool Queue(int descriptor, int receiver, const char* text) { + const auto length = static_cast(std::strlen(text)); + return send_nid_postfix(descriptor, text, static_cast(length), 0) == length && Readable(receiver); +} + +static void CheckReceiveMessageFaults() { + constexpr std::size_t page = 0x4000; + void* memory = nullptr; + Require(sceKernelMapFlexibleMemory(&memory, page * 3, 3, 0) == 0 && memory != nullptr); + auto* writable = static_cast(memory); + auto* readOnly = writable + page; + auto* inaccessible = writable + 2 * page; + char data[16]{}; + Iovec good{data, sizeof(data)}; + auto* readOnlyMessage = reinterpret_cast(readOnly); + *readOnlyMessage = {nullptr, 0, &good, 1, nullptr, 0, -1}; + auto* readOnlyVector = reinterpret_cast(readOnly + 64); + *readOnlyVector = {data, sizeof(data)}; + Require(sceKernelMprotect(readOnly, page, 1) == 0 && sceKernelMprotect(inaccessible, page, 0) == 0); + int pair[2] = {-1, -1}; + Require(socketpair_nid_postfix(Unix, Datagram, 0, pair) == 0); + + Require(Queue(pair[0], pair[1], "x")); + Require(ReceiveFaults(pair[1], reinterpret_cast(0x1000))); + Require(ReceiveFaults(pair[1], reinterpret_cast(inaccessible))); + Msghdr message{nullptr, 0, reinterpret_cast(0x1000), 1, nullptr, 0, -1}; + Require(ReceiveFaults(pair[1], &message)); + message.iov = reinterpret_cast(inaccessible); + Require(ReceiveFaults(pair[1], &message)); + char peeked[4]{}; + Require(recv_nid_postfix(pair[1], peeked, sizeof(peeked), Peek) == 1 && peeked[0] == 'x'); + + Iovec vector{reinterpret_cast(0x1000), sizeof(data)}; + message = {nullptr, 0, &vector, 1, nullptr, 0, -1}; + Require(ReceiveFaults(pair[1], &message)); + vector.base = inaccessible; + Require(Queue(pair[0], pair[1], "x") && ReceiveFaults(pair[1], &message)); + vector.base = readOnly + 128; + Require(Queue(pair[0], pair[1], "x") && ReceiveFaults(pair[1], &message)); + message = {reinterpret_cast(0x1000), 16, &good, 1, nullptr, 0, -1}; + Require(Queue(pair[0], pair[1], "x") && ReceiveFaults(pair[1], &message)); + message.name = readOnly + 128; + Require(Queue(pair[0], pair[1], "x") && ReceiveFaults(pair[1], &message)); + Require(Queue(pair[0], pair[1], "x") && ReceiveFaults(pair[1], readOnlyMessage)); + Require(fcntl_nid_postfix(pair[1], 4, 4) == 0); + Require(recv_nid_postfix(pair[1], peeked, sizeof(peeked), 0) == -1 && *__error_nid_postfix() == 35); + Require(fcntl_nid_postfix(pair[1], 4, 0) == 0); + + std::uint8_t name[32]; + std::memset(name, 0xa5, sizeof(name)); + vector = {writable, 1u << 20}; + message = {name, UINT32_MAX, &vector, 1, nullptr, 7, -1}; + Require(Queue(pair[0], pair[1], "abcd") && recvmsg_nid_postfix(pair[1], &message, 0) == 4); + Require(std::memcmp(writable, "abcd", 4) == 0 && message.flags == 0 && message.controlLength == 0); + Require(message.nameLength == 16 && name[0] == 16 && name[1] == Unix && name[16] == 0xa5); + Iovec vectors[2]{{writable, 2}, {inaccessible, 16}}; + message = {nullptr, 0, vectors, 2, nullptr, 0, -1}; + Require(Queue(pair[0], pair[1], "ef") && recvmsg_nid_postfix(pair[1], &message, 0) == 2 && std::memcmp(writable, "ef", 2) == 0); + message = {readOnly + 128, 0, readOnlyVector, 1, nullptr, 0, -1}; + Require(Queue(pair[0], pair[1], "gh") && recvmsg_nid_postfix(pair[1], &message, 0) == 2); + Require(std::memcmp(data, "gh", 2) == 0 && message.nameLength == 0); + + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + Require(sceKernelMprotect(memory, page * 3, 3) == 0 && sceKernelMunmap(memory, page * 3) == 0); +} + +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "recvmsg-memory") == 0) { + CheckReceiveMessageFaults(); + return 0; + } + Require(argc == 1); int pair[2] = {-1, -1}; Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); Require(pair[0] >= 0 && pair[0] < DescriptorSetSize && pair[1] >= 0 && pair[1] < DescriptorSetSize && pair[0] != pair[1]); @@ -113,6 +203,7 @@ int main() { Require(std::strcmp(received, ping) == 0); Require(fcntl_nid_postfix(pair[1], 3) == 2); + Require(fcntl_nid_postfix(pair[0], 1) == 0 && fcntl_nid_postfix(pair[1], 1) == 0); Require(fcntl_nid_postfix(pair[1], 4, 4) == 0); Require(fcntl_nid_postfix(pair[1], 3) == 6); Require(recv_nid_postfix(pair[1], received, sizeof(received), 0) == -1 && *__error_nid_postfix() == 35); @@ -122,14 +213,15 @@ int main() { Require(Exchange(pair[1], pair[0], "still open")); Require(close_nid_postfix(pair[0]) == 0); Require(Readable(pair[1], In | Hup) && recv_nid_postfix(pair[1], received, sizeof(received), 0) == 0); -#ifndef _WIN32 - Require(send_nid_postfix(pair[1], ping, sizeof(ping), NoSignal) == -1 && *__error_nid_postfix() == 32); -#endif + std::int64_t sent = 0; + for (int i = 0; i < 100 && sent >= 0; ++i) sent = send_nid_postfix(pair[1], ping, sizeof(ping), NoSignal); + Require(sent == -1 && *__error_nid_postfix() == 32); Require(close_nid_postfix(pair[1]) == 0); Require(close_nid_postfix(pair[1]) == -1 && *__error_nid_postfix() == 9); Require(socketpair_nid_postfix(Unix, Stream | NonBlocking | CloseOnExec, 0, pair) == 0); Require(fcntl_nid_postfix(pair[0], 3) == 6 && fcntl_nid_postfix(pair[1], 3) == 6); + Require(fcntl_nid_postfix(pair[0], 1) == 1 && fcntl_nid_postfix(pair[1], 1) == 1); Require(recv_nid_postfix(pair[0], received, sizeof(received), 0) == -1 && *__error_nid_postfix() == 35); Require(Exchange(pair[1], pair[0], "wake up")); Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); @@ -170,7 +262,9 @@ int main() { Require(Exchange(pair[1], pair[0], "back")); Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); -#ifndef _WIN32 +#ifdef __APPLE__ + Require(ThrowsRuntimeError([&] { socketpair_nid_postfix(Unix, SequencedPacket, 0, pair); })); +#elif !defined(_WIN32) Require(socketpair_nid_postfix(Unix, SequencedPacket, 0, pair) == 0); Require(send_nid_postfix(pair[0], "a", 1, 0) == 1); Require(send_nid_postfix(pair[0], "bb", 2, 0) == 2); @@ -194,6 +288,7 @@ int main() { Require(recvmsg_nid_postfix(pair[1], &message, 0) == 4 && message.flags == 0 && std::memcmp(whole, "next", 4) == 0); Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); #endif + CheckReceiveMessageFaults(); Require(Fails(2, Stream, 0, 45)); Require(Fails(28, Datagram, 0, 45)); diff --git a/core/libs/tests/GuestSocketPortZero.cpp b/core/libs/tests/GuestSocketPortZero.cpp new file mode 100644 index 0000000000..070b9c8982 --- /dev/null +++ b/core/libs/tests/GuestSocketPortZero.cpp @@ -0,0 +1,85 @@ +#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 connect_nid_postfix(int, const void*, std::uint32_t); +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 getpeername_nid_postfix(int, void*, std::uint32_t*); +std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s (errno %d)\n", message, *__error_nid_postfix()); + std::exit(1); + } +} + +using Address = std::array; + +static Address Loopback(int family, std::uint32_t size) { + Address address{}; + address[0] = static_cast(size); + address[1] = static_cast(family); + if (family == 2) { address[4] = 127; address[7] = 1; } + else address[23] = 1; + return address; +} + +static void CheckDatagram(int family) { + const std::uint32_t size = family == 2 ? 16 : 28; + const int probe = socket_nid_postfix(family, 2, 0); + if (probe < 0 && family == 28 && *__error_nid_postfix() == 47) return; + Require(probe >= 0, "socket"); + auto address = Loopback(family, size); + Address name{}; + std::uint32_t length = name.size(); + Require(connect_nid_postfix(probe, address.data(), size) == -1 && *__error_nid_postfix() == 49, + "connect to port 0 on an unconnected datagram socket must fail with EADDRNOTAVAIL (49)"); + Require(getpeername_nid_postfix(probe, name.data(), &length) == -1 && *__error_nid_postfix() == 57, "still unconnected"); + Require(close_nid_postfix(probe) == 0, "close"); +} + +static void CheckStream(int family) { + const std::uint32_t size = family == 2 ? 16 : 28; + const int listener = socket_nid_postfix(family, 1, 0); + if (listener < 0 && family == 28 && *__error_nid_postfix() == 47) return; + Require(listener >= 0, "listener"); + auto address = Loopback(family, size); + const int bound = bind_nid_postfix(listener, address.data(), size); + if (bound < 0 && family == 28 && *__error_nid_postfix() == 49) { + Require(close_nid_postfix(listener) == 0, "close"); + return; + } + Require(bound == 0 && listen_nid_postfix(listener, 1) == 0, "listen"); + std::uint32_t length = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &length) == 0, "listener name"); + auto zero = Loopback(family, size); + Require(connect_nid_postfix(listener, zero.data(), size) == -1 && *__error_nid_postfix() == 45, + "connect on a listening stream socket must fail with EOPNOTSUPP (45)"); + const int client = socket_nid_postfix(family, 1, 0); + Require(client >= 0, "client"); + Require(connect_nid_postfix(client, zero.data(), size) == -1 && *__error_nid_postfix() == 49, + "connect to port 0 on an unconnected stream socket must fail with EADDRNOTAVAIL (49)"); + Require(connect_nid_postfix(client, address.data(), size) == 0, "connect"); + Require(connect_nid_postfix(client, zero.data(), size) == -1 && *__error_nid_postfix() == 56, + "connect to port 0 on a connected stream socket must fail with EISCONN (56)"); + Require(sendto_nid_postfix(client, "x", 1, 0, zero.data(), size) == 1, "sendto port 0 on a connected stream socket"); + Require(close_nid_postfix(client) == 0 && close_nid_postfix(listener) == 0, "close"); +} + +int main() { + CheckDatagram(2); + CheckDatagram(28); + CheckStream(2); + CheckStream(28); + return 0; +} diff --git a/core/libs/tests/GuestSocketReadWrite.cpp b/core/libs/tests/GuestSocketReadWrite.cpp index 5a8d48781b..4af5913b09 100644 --- a/core/libs/tests/GuestSocketReadWrite.cpp +++ b/core/libs/tests/GuestSocketReadWrite.cpp @@ -148,11 +148,9 @@ int main() { Require(close_nid_postfix(client) == 0); Require(Readable(server, In | Hup) && read_nid_postfix(server, received, sizeof(received)) == 0); Require(sceKernelRead(server, received, sizeof(received)) == 0); -#ifndef _WIN32 std::int64_t written = 0; for (int i = 0; i < 100 && written >= 0; ++i) written = write_nid_postfix(server, request, sizeof(request)); Require(written == -1 && *__error_nid_postfix() == 32); -#endif Require(close_nid_postfix(server) == 0); *__error_nid_postfix() = 0; Require(read_nid_postfix(server, received, sizeof(received)) == -1 && *__error_nid_postfix() == 9); diff --git a/core/libs/tests/GuestSocketStreamOptions.cpp b/core/libs/tests/GuestSocketStreamOptions.cpp new file mode 100644 index 0000000000..f85a693f17 --- /dev/null +++ b/core/libs/tests/GuestSocketStreamOptions.cpp @@ -0,0 +1,180 @@ +#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 socketpair_nid_postfix(int, 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 shutdown_nid_postfix(int, int); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int APS5_VABI getsockopt_nid_postfix(int, int, int, void*, std::uint32_t*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +int APS5_VABI close_nid_postfix(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 write_nid_postfix(int, const char*, std::int64_t); +int* APS5_VABI __error_nid_postfix(); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "socket stream option check failed at line %d, errno %d\n", line, *__error_nid_postfix()); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +constexpr int SocketLevel = 0xffff; +constexpr int TcpLevel = 6; +constexpr int TcpNoDelay = 1; +constexpr int TcpMaximumSegment = 2; +constexpr int Ipv6Level = 41; +constexpr int Ipv6UnicastHops = 4; +constexpr int Ipv6Only = 27; +constexpr int NoSigpipe = 0x800; +constexpr int NonBlockingOption = 0x1200; +constexpr int NoSignalFlag = 0x20000; +constexpr int GetStatusFlags = 3; +constexpr int NonBlockingFlag = 4; +constexpr int Invalid = 22; +constexpr int BrokenPipe = 32; +constexpr int Again = 35; +constexpr int NoProtocolOption = 42; + +#ifndef _WIN32 +static volatile std::sig_atomic_t brokenPipes = 0; +static void CountBrokenPipe(int) { + brokenPipes = brokenPipes + 1; +} +#endif + +static int BrokenPipes() { +#ifdef _WIN32 + return 0; +#else + return brokenPipes; +#endif +} + +static bool Fails(std::int64_t result, int error) { + return result == -1 && *__error_nid_postfix() == error; +} + +static bool Set(int descriptor, int level, int option, int value) { + return setsockopt_nid_postfix(descriptor, level, option, &value, sizeof(value)) == 0; +} + +static int Get(int descriptor, int level, int option) { + int value = -1; + std::uint32_t length = sizeof(value); + Require(getsockopt_nid_postfix(descriptor, level, option, &value, &length) == 0 && length == sizeof(value)); + return value; +} + +int main() { + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0); + Require(listen_nid_postfix(listener, 4) == 0); + std::uint32_t addressSize = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &addressSize) == 0); + const int client = socket_nid_postfix(2, 1, 0); + Require(client >= 0 && connect_nid_postfix(client, address.data(), address.size()) == 0); + const int server = accept_nid_postfix(listener, nullptr, nullptr); + Require(server >= 0); + Require(Set(listener, SocketLevel, NoSigpipe, 1) && Set(listener, TcpLevel, TcpNoDelay, 1)); + const int secondClient = socket_nid_postfix(2, 1, 0); + Require(secondClient >= 0 && connect_nid_postfix(secondClient, address.data(), address.size()) == 0); + const int inheriting = accept_nid_postfix(listener, nullptr, nullptr); + Require(inheriting >= 0); + Require(Get(inheriting, SocketLevel, NoSigpipe) == NoSigpipe && Get(inheriting, TcpLevel, TcpNoDelay) == 4); + Require(Get(server, SocketLevel, NoSigpipe) == 0 && Get(server, TcpLevel, TcpNoDelay) == 0); + Require(close_nid_postfix(inheriting) == 0 && close_nid_postfix(secondClient) == 0); + const int datagram = socket_nid_postfix(2, 2, 0); + std::array datagramAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(datagram >= 0 && bind_nid_postfix(datagram, datagramAddress.data(), datagramAddress.size()) == 0); + int local[2]{-1, -1}; + Require(socketpair_nid_postfix(1, 1, 0, local) == 0); + + int one = 1; + Require(Get(client, TcpLevel, TcpNoDelay) == 0); + Require(Set(client, TcpLevel, TcpNoDelay, 7) && Get(client, TcpLevel, TcpNoDelay) == 4); + unsigned char byte = 0x7f; + std::uint32_t length = sizeof(byte); + Require(getsockopt_nid_postfix(client, TcpLevel, TcpNoDelay, &byte, &length) == 0 && length == sizeof(byte) && byte == 4); + int value = 0; + Require(Set(client, TcpLevel, TcpNoDelay, 0) && Get(client, TcpLevel, TcpNoDelay) == 0); + Require(Fails(setsockopt_nid_postfix(client, TcpLevel, TcpNoDelay, &one, sizeof(one) - 1), Invalid)); + Require(Fails(setsockopt_nid_postfix(client, TcpLevel, TcpMaximumSegment, &one, sizeof(one)), NoProtocolOption)); + length = sizeof(value); + Require(Fails(setsockopt_nid_postfix(datagram, TcpLevel, TcpNoDelay, &one, sizeof(one)), Invalid)); + Require(Fails(getsockopt_nid_postfix(datagram, TcpLevel, TcpNoDelay, &value, &length), Invalid)); + Require(Fails(setsockopt_nid_postfix(local[0], TcpLevel, TcpNoDelay, &one, sizeof(one)), Invalid)); + Require(Fails(getsockopt_nid_postfix(local[0], TcpLevel, TcpNoDelay, &value, &length), Invalid)); + + char received = 0; + Require(Get(server, SocketLevel, NonBlockingOption) == 0); + Require((fcntl_nid_postfix(server, GetStatusFlags) & NonBlockingFlag) == 0); + Require(Set(server, SocketLevel, NonBlockingOption, 5) && Get(server, SocketLevel, NonBlockingOption) == 1); + Require((fcntl_nid_postfix(server, GetStatusFlags) & NonBlockingFlag) != 0); + Require(Fails(recv_nid_postfix(server, &received, sizeof(received), 0), Again)); + Require(Set(server, SocketLevel, NonBlockingOption, 0) && Get(server, SocketLevel, NonBlockingOption) == 0); + Require((fcntl_nid_postfix(server, GetStatusFlags) & NonBlockingFlag) == 0); + Require(Fails(setsockopt_nid_postfix(server, SocketLevel, NonBlockingOption, &one, sizeof(one) - 1), Invalid)); + Require(Set(datagram, SocketLevel, NonBlockingOption, 1) && Get(datagram, SocketLevel, NonBlockingOption) == 1); + Require(Fails(recv_nid_postfix(datagram, &received, sizeof(received), 0), Again)); + +#ifndef _WIN32 + std::signal(SIGPIPE, CountBrokenPipe); +#endif + Require(Get(client, SocketLevel, NoSigpipe) == 0); + Require(Set(client, SocketLevel, NoSigpipe, 3) && Get(client, SocketLevel, NoSigpipe) == NoSigpipe); + Require(Fails(setsockopt_nid_postfix(client, SocketLevel, NoSigpipe, &one, sizeof(one) - 1), Invalid)); + Require(shutdown_nid_postfix(client, 1) == 0); + Require(Fails(send_nid_postfix(client, "x", 1, 0), BrokenPipe)); + Require(Fails(write_nid_postfix(client, "x", 1), BrokenPipe)); + Require(BrokenPipes() == 0); + Require(Set(client, SocketLevel, NoSigpipe, 0) && Get(client, SocketLevel, NoSigpipe) == 0); + Require(Fails(send_nid_postfix(client, "x", 1, NoSignalFlag), BrokenPipe)); + Require(BrokenPipes() == 0); +#ifndef _WIN32 + Require(Fails(send_nid_postfix(client, "x", 1, 0), BrokenPipe)); + Require(BrokenPipes() == 1); +#endif + + const int dualStack = socket_nid_postfix(28, 1, 0); + Require(dualStack >= 0 && Get(dualStack, Ipv6Level, Ipv6Only) == 1); + Require(Set(dualStack, Ipv6Level, Ipv6Only, 0) && Get(dualStack, Ipv6Level, Ipv6Only) == 0); + std::array mapped{28, 28, address[2], address[3], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 127, 0, 0, 1}; + Require(connect_nid_postfix(dualStack, mapped.data(), mapped.size()) == 0); + const int mappedPeer = accept_nid_postfix(listener, nullptr, nullptr); + Require(mappedPeer >= 0 && close_nid_postfix(mappedPeer) == 0); + const int datagram6 = socket_nid_postfix(28, 2, 0); + Require(datagram6 >= 0 && Get(datagram6, Ipv6Level, Ipv6Only) == 1); + Require(Set(datagram6, Ipv6Level, Ipv6Only, 9) && Get(datagram6, Ipv6Level, Ipv6Only) == 1); + Require(Fails(setsockopt_nid_postfix(datagram6, Ipv6Level, Ipv6Only, &one, sizeof(one) - 1), Invalid)); + Require(Fails(setsockopt_nid_postfix(datagram6, Ipv6Level, Ipv6UnicastHops, &one, sizeof(one)), NoProtocolOption)); + length = sizeof(value); + Require(Fails(getsockopt_nid_postfix(datagram6, Ipv6Level, Ipv6UnicastHops, &value, &length), NoProtocolOption)); + std::array unspecified6{28, 28}; + Require(bind_nid_postfix(datagram6, unspecified6.data(), unspecified6.size()) == 0); + Require(Fails(setsockopt_nid_postfix(datagram6, Ipv6Level, Ipv6Only, &one, sizeof(one)), Invalid)); + Require(Fails(setsockopt_nid_postfix(datagram, Ipv6Level, Ipv6Only, &one, sizeof(one)), Invalid)); + Require(Fails(getsockopt_nid_postfix(datagram, Ipv6Level, Ipv6Only, &value, &length), Invalid)); + Require(Fails(setsockopt_nid_postfix(local[0], Ipv6Level, Ipv6Only, &one, sizeof(one)), Invalid)); + Require(close_nid_postfix(datagram6) == 0 && close_nid_postfix(dualStack) == 0); + + Require(close_nid_postfix(local[0]) == 0 && close_nid_postfix(local[1]) == 0); + Require(close_nid_postfix(datagram) == 0 && close_nid_postfix(server) == 0); + Require(close_nid_postfix(client) == 0 && close_nid_postfix(listener) == 0); + return 0; +} diff --git a/core/libs/tests/GuestSocketTimeouts.cpp b/core/libs/tests/GuestSocketTimeouts.cpp new file mode 100644 index 0000000000..2ff28a2173 --- /dev/null +++ b/core/libs/tests/GuestSocketTimeouts.cpp @@ -0,0 +1,165 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +struct Timeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +struct Iovec { + void* base; + std::uint64_t length; +}; + +struct Msghdr { + void* name; + std::uint32_t nameLength; + Iovec* iov; + int iovLength; + void* control; + std::uint32_t controlLength; + int flags; +}; + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int APS5_VABI getsockopt_nid_postfix(int, int, int, void*, std::uint32_t*); +int APS5_VABI close_nid_postfix(int); +std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); +std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); +std::int64_t APS5_VABI recvmsg_nid_postfix(int, Msghdr*, int); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +int* APS5_VABI __error_nid_postfix(); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "socket timeout check failed at line %d, errno %d\n", line, *__error_nid_postfix()); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +constexpr int SocketLevel = 0xffff; +constexpr int TcpLevel = 6; +constexpr int SendBuffer = 0x1001; +constexpr int ReceiveBuffer = 0x1002; +constexpr int SendTimeout = 0x1005; +constexpr int ReceiveTimeout = 0x1006; +constexpr int DontWait = 0x80; +constexpr int Invalid = 22; +constexpr int Domain = 33; +constexpr int WouldBlock = 35; +constexpr int NoProtocolOption = 42; + +using Clock = std::chrono::steady_clock; + +static bool Fails(std::int64_t result, int error) { + return result == -1 && *__error_nid_postfix() == error; +} + +static int Set(int descriptor, int option, std::int64_t seconds, std::int64_t microseconds) { + const Timeval time{seconds, microseconds}; + return setsockopt_nid_postfix(descriptor, SocketLevel, option, &time, sizeof(time)); +} + +static bool Reads(int descriptor, int option, std::int64_t seconds, std::int64_t microseconds) { + Timeval time{-1, -1}; + std::uint32_t length = sizeof(time); + return getsockopt_nid_postfix(descriptor, SocketLevel, option, &time, &length) == 0 && length == sizeof(time) && + time.seconds == seconds && time.microseconds == microseconds; +} + +int main() { + const int datagram = socket_nid_postfix(2, 2, 0); + std::array own{16, 2, 0, 0, 127, 0, 0, 1}; + Require(datagram >= 0 && bind_nid_postfix(datagram, own.data(), own.size()) == 0); + std::uint32_t ownSize = own.size(); + Require(getsockname_nid_postfix(datagram, own.data(), &ownSize) == 0); + + Require(Reads(datagram, ReceiveTimeout, 0, 0) && Reads(datagram, SendTimeout, 0, 0)); + Require(Set(datagram, ReceiveTimeout, 2, 0) == 0 && Reads(datagram, ReceiveTimeout, 2, 0)); + Require(Reads(datagram, SendTimeout, 0, 0)); + Require(Set(datagram, SendTimeout, 1, 500000) == 0 && Reads(datagram, SendTimeout, 1, 499999)); + Require(Set(datagram, SendTimeout, 0, 1) == 0 && Reads(datagram, SendTimeout, 0, 0)); + Require(Set(datagram, SendTimeout, std::int64_t{1} << 31, 7) == 0 && Reads(datagram, SendTimeout, 0x7fffffff, 999999)); + Require(Set(datagram, SendTimeout, 0, 0) == 0 && Reads(datagram, SendTimeout, 0, 0)); + Require(Fails(Set(datagram, ReceiveTimeout, -1, 0), Domain)); + Require(Fails(Set(datagram, ReceiveTimeout, 0, -1), Domain)); + Require(Fails(Set(datagram, ReceiveTimeout, 0, 1000000), Domain)); + Timeval time{0, 0}; + Require(Fails(setsockopt_nid_postfix(datagram, SocketLevel, ReceiveTimeout, &time, sizeof(time) - 1), Invalid)); + Require(Reads(datagram, ReceiveTimeout, 2, 0)); + std::int64_t seconds = -1; + std::uint32_t length = sizeof(seconds); + Require(getsockopt_nid_postfix(datagram, SocketLevel, ReceiveTimeout, &seconds, &length) == 0 && length == sizeof(seconds) && seconds == 2); + length = sizeof(time); + Require(Fails(getsockopt_nid_postfix(datagram, TcpLevel, ReceiveTimeout, &time, &length), Invalid)); + + char byte = 0; + Require(Set(datagram, ReceiveTimeout, 0, 60000) == 0); + auto started = Clock::now(); + Require(Fails(recv_nid_postfix(datagram, &byte, sizeof(byte), 0), WouldBlock)); + Require(Clock::now() - started >= std::chrono::milliseconds(59)); + Require(Fails(recvfrom_nid_postfix(datagram, &byte, sizeof(byte), 0, nullptr, nullptr), WouldBlock)); + Iovec part{&byte, sizeof(byte)}; + Msghdr message{nullptr, 0, &part, 1, nullptr, 0, 0}; + Require(Fails(recvmsg_nid_postfix(datagram, &message, 0), WouldBlock)); + Require(Fails(read_nid_postfix(datagram, &byte, sizeof(byte)), WouldBlock)); + Require(Set(datagram, ReceiveTimeout, 60, 0) == 0); + Require(Fails(recv_nid_postfix(datagram, &byte, sizeof(byte), DontWait), WouldBlock)); + Require(Fails(recvfrom_nid_postfix(datagram, &byte, sizeof(byte), DontWait, nullptr, nullptr), WouldBlock)); + Require(Fails(recvmsg_nid_postfix(datagram, &message, DontWait), WouldBlock)); + Require(Set(datagram, ReceiveTimeout, 0, 60000) == 0); + Require(sendto_nid_postfix(datagram, "d", 1, 0, own.data(), own.size()) == 1); + Require(recv_nid_postfix(datagram, &byte, sizeof(byte), 0) == 1 && byte == 'd'); + + const int listener = socket_nid_postfix(2, 1, 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(listener >= 0 && bind_nid_postfix(listener, address.data(), address.size()) == 0); + int small = 4096; + Require(setsockopt_nid_postfix(listener, SocketLevel, ReceiveBuffer, &small, sizeof(small)) == 0); + Require(Set(listener, ReceiveTimeout, 3, 0) == 0); + Require(listen_nid_postfix(listener, 4) == 0); + std::uint32_t addressSize = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &addressSize) == 0); + const int client = socket_nid_postfix(2, 1, 0); + Require(client >= 0 && setsockopt_nid_postfix(client, SocketLevel, SendBuffer, &small, sizeof(small)) == 0); + Require(connect_nid_postfix(client, address.data(), address.size()) == 0); + const int server = accept_nid_postfix(listener, nullptr, nullptr); + Require(server >= 0 && Reads(server, ReceiveTimeout, 3, 0) && Reads(server, SendTimeout, 0, 0)); + + Require(Set(client, ReceiveTimeout, 0, 60000) == 0); + started = Clock::now(); + Require(Fails(recv_nid_postfix(client, &byte, sizeof(byte), 0), WouldBlock)); + Require(Clock::now() - started >= std::chrono::milliseconds(59)); + + Require(Set(client, SendTimeout, 0, 60000) == 0); + const std::vector chunk(1024, 'x'); + std::int64_t sent = 0; + bool stopped = false; + for (int round = 0; round < 200000 && !stopped; ++round) { + const auto result = send_nid_postfix(client, chunk.data(), chunk.size(), 0); + if (result > 0) sent += result; + else stopped = Fails(result, WouldBlock); + Require(result > 0 || stopped); + } + Require(stopped && sent > 0); + + Require(close_nid_postfix(server) == 0 && close_nid_postfix(client) == 0); + Require(close_nid_postfix(listener) == 0 && close_nid_postfix(datagram) == 0); + return 0; +} diff --git a/core/libs/tests/GuestSsl.cpp b/core/libs/tests/GuestSsl.cpp index 1046e7ee55..4aa00482b6 100644 --- a/core/libs/tests/GuestSsl.cpp +++ b/core/libs/tests/GuestSsl.cpp @@ -1,8 +1,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "ProcessParametersFixture.hpp" +#include +#include #include #include #include #include +#include extern "C" { int APS5_VABI sceSslInit_nid_postfix(std::size_t); @@ -18,6 +22,12 @@ int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, int APS5_VABI sceSslConnect(int); int APS5_VABI sceSslRead(int, void*, std::size_t, int); int APS5_VABI sceSslWrite(int, void*, std::size_t, int); +int APS5_VABI sceSslClose(int); +int APS5_VABI sceSslEnableVerifyOption(int, std::uint32_t); +int APS5_VABI sceSslDisableVerifyOption(int, std::uint32_t); +int APS5_VABI sceSslSetMinSslVersion(int, int); +int APS5_VABI sceSslSetAlpn(int, const char*); +const char* APS5_VABI sceSslGetAlpnSelected(int); } struct SslMemoryPoolStats { @@ -55,6 +65,13 @@ struct SslCaList { }; int main() { + static std::array parameters{}; + const std::uint64_t processSize = parameters.size(); + const std::uint32_t processMagic = 0x4942524f; + std::memcpy(parameters.data(), &processSize, sizeof(processSize)); + std::memcpy(parameters.data() + 8, &processMagic, sizeof(processMagic)); + Require(ProcessParametersFixture::Install(parameters.data(), parameters.size())); + constexpr int invalidValue = static_cast(0x8095F007); constexpr int notFound = static_cast(0x8095F004); constexpr int invalidArg = static_cast(0x8095177A); int marker = 0; @@ -63,7 +80,7 @@ int main() { SslMemoryPoolStats stats{1, 1, 1, 1}; Require(sceSslGetMemoryPoolStats(context, &stats) == 0); Require(stats.pool_size == 0x10000 && stats.max_inuse_size == 0 && stats.current_inuse_size == 0 && stats.reserved == 0); - Require(sceSslGetCaCerts(context, nullptr) == invalidArg); + Require(sceSslGetCaCerts(context, nullptr) == invalidValue); Require(sceSslFreeCaCerts(context, nullptr) == invalidArg); SslCaCerts certs{&marker, 3, &marker}; @@ -74,7 +91,7 @@ int main() { Require(sceSslFreeCaCerts(context, &certs) == 0); Require(certs.certs == nullptr && certs.num == 0 && certs.pool == nullptr); - Require(sceSslGetCaList(context, nullptr) == invalidArg); + Require(sceSslGetCaList(context, nullptr) == invalidValue); Require(sceSslFreeCaList(context, nullptr) == invalidArg); void* handle = ▮ @@ -96,32 +113,87 @@ int main() { Require(list.certs == nullptr && list.num == 0); constexpr int invalidId = static_cast(0x8095F006); - Require(Throws([&] { sceSslCreateConnection(context, 0, nullptr); })); + constexpr int badLength = static_cast(0x80951773); + constexpr int badDescriptor = static_cast(0x80410109); + Require(sceSslInit_nid_postfix(0) == static_cast(0x80020016)); + Require(sceSslCreateConnection(context, 0, nullptr) == invalidValue); Require(sceSslCreateConnection(context + 1000, 0, "example.com") == invalidId); const int connection = sceSslCreateConnection(context, 0, "example.com"); 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(sceSslEnableVerifyOption(connection, 0x40) == invalidValue); + Require(sceSslDisableVerifyOption(connection, 0x1000) == invalidValue); + Require(sceSslSetMinSslVersion(connection, 0) == invalidValue); + Require(sceSslSetMinSslVersion(connection, 1) == invalidValue); + Require(sceSslSetMinSslVersion(connection, 2) == 0); + Require(sceSslSetMinSslVersion(connection, 5) == 0); + Require(sceSslSetMinSslVersion(connection, -1) == invalidValue); + Require(sceSslSetMinSslVersion(connection, 6) == invalidValue); + Require(sceSslGetAlpnSelected(connection) == nullptr); + Require(sceSslSetAlpn(connection, "http/1.1") == 0); + Require(sceSslSetAlpn(connection, "") == badLength); + Require(sceSslSetAlpn(connection, ",h2") == badLength); + Require(sceSslSetAlpn(connection, "h2,") == badLength); + Require(sceSslSetAlpn(connection, "h2,,http/1.1") == badLength); + Require(sceSslSetAlpn(connection, std::string(255, 'a').c_str()) == 0); + Require(sceSslSetAlpn(connection, std::string(256, 'a').c_str()) == badLength); + Require(sceSslSetAlpn(connection, std::string(65535, 'a').c_str()) == badLength); + Require(sceSslSetAlpn(0, nullptr) == invalidId); + Require(sceSslSetAlpn(connection, nullptr) == 0); + Require(sceSslGetAlpnSelected(connection) == nullptr); char buffer[4]{}; - Require(Throws([&] { sceSslConnect(connection); })); - Require(Throws([&] { sceSslRead(connection, buffer, sizeof(buffer), 0); })); - Require(Throws([&] { sceSslWrite(connection, buffer, sizeof(buffer), 0); })); + Require(sceSslConnect(connection) == badDescriptor); + Require(sceSslRead(connection, buffer, sizeof(buffer), 0) == badDescriptor); + Require(sceSslWrite(connection, buffer, sizeof(buffer), 0) == badDescriptor); + Require(sceSslWrite(connection, buffer, sizeof(buffer), 1) == invalidValue); + Require(sceSslClose(connection) == 0); const int unknown = connection + 1000; - Require(Throws([&] { sceSslSetVerifyCallback(unknown, nullptr, nullptr); })); - Require(Throws([&] { sceSslDeleteConnection(unknown); })); + Require(sceSslSetVerifyCallback(unknown, nullptr, nullptr) == invalidId); + Require(sceSslEnableVerifyOption(unknown, serverVerify) == invalidId); + Require(sceSslDisableVerifyOption(unknown, serverVerify) == invalidId); + Require(sceSslSetMinSslVersion(unknown, 5) == invalidId); + Require(sceSslSetAlpn(unknown, "http/1.1") == invalidId); + Require(sceSslGetAlpnSelected(unknown) == nullptr); + Require(sceSslConnect(unknown) == invalidId); + Require(sceSslRead(unknown, buffer, sizeof(buffer), 0) == invalidId); + Require(sceSslWrite(unknown, buffer, sizeof(buffer), 0) == invalidId); + Require(sceSslClose(unknown) == invalidId); + Require(sceSslDeleteConnection(unknown) == invalidId); Require(sceSslTerm_nid_postfix(context) == 0); - Require(Throws([&] { sceSslSetVerifyCallback(connection, nullptr, nullptr); })); + Require(sceSslTerm_nid_postfix(context) == invalidId); + Require(sceSslSetVerifyCallback(connection, nullptr, nullptr) == invalidId); + Require(sceSslEnableVerifyOption(connection, serverVerify) == invalidId); + Require(sceSslDisableVerifyOption(connection, serverVerify) == invalidId); + Require(sceSslSetMinSslVersion(connection, 5) == invalidId); + Require(sceSslSetAlpn(connection, "http/1.1") == invalidId); + Require(sceSslGetAlpnSelected(connection) == nullptr); + Require(sceSslConnect(connection) == invalidId); + Require(sceSslClose(connection) == invalidId); Require(sceSslCreateConnection(context, 0, "example.com") == invalidId); - Require(sceSslDeleteConnection(connection) == 0); - Require(Throws([&] { sceSslDeleteConnection(connection); })); + Require(sceSslDeleteConnection(connection) == invalidId); - Require(sceSslGetCaList(context, nullptr) == invalidArg); + Require(sceSslGetCaList(context, nullptr) == invalidValue); Require(sceSslFreeCaList(context, nullptr) == invalidArg); list = {&handle, 3}; - Require(Throws([&] { sceSslGetCaList(context, &list); })); + Require(sceSslGetCaList(context, &list) == invalidId); + Require(list.certs == &handle && list.num == 3); + Require(sceSslGetCaCerts(context, nullptr) == invalidId); + certs = {&marker, 3, &marker}; + Require(sceSslGetCaCerts(context, &certs) == invalidId); + Require(certs.certs == &marker && certs.num == 3 && certs.pool == &marker); Require(Throws([&] { sceSslFreeCaList(context, &list); })); - Require(Throws([&] { sceSslUnloadCert(context); })); + for (const std::uint32_t sdkVersion : {0u, 0x074fffffu, 0x07500000u, 0x09000000u}) { + std::memcpy(parameters.data() + 16, &sdkVersion, sizeof(sdkVersion)); + Require(sceSslUnloadCert(context) == (sdkVersion < 0x07500000 ? 0 : invalidId)); + Require(sceSslUnloadCert(0) == (sdkVersion < 0x07500000 ? 0 : invalidId)); + } } diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp index aaa93f2962..b59ad7c6c5 100644 --- a/core/libs/tests/GuestSslLoad.cpp +++ b/core/libs/tests/GuestSslLoad.cpp @@ -23,11 +23,23 @@ 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*); +int APS5_VABI sceSslGetPem(int, void*, char*, std::size_t*); } namespace { constexpr int NotFound = static_cast(0x8095F004); +constexpr int InvalidId = static_cast(0x8095F006); void Check(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); @@ -109,8 +121,8 @@ void ConnectionLifetime() { Check(sceSslSetVerifyCallback(retained, nullptr, nullptr) == 0, "credential unload invalidated the connection"); Check(sceSslFreeCaCerts(id, &bytes) == 0 && Stats(id).currentInuseSize == 0, "CA snapshot leaked with a retained connection"); Check(sceSslTerm_nid_postfix(id) == 0, "context termination failed with a retained connection"); - Throws([&] { sceSslSetVerifyCallback(retained, nullptr, nullptr); }); - Check(sceSslDeleteConnection(retained) == 0, "terminated context's connection cannot be released"); + Check(sceSslSetVerifyCallback(retained, nullptr, nullptr) == InvalidId, "terminated context's connection did not report an invalid id"); + Check(sceSslDeleteConnection(retained) == InvalidId, "terminated context's connection was still deletable"); Check(sceSslGetCaCerts(other, &empty) == NotFound, "unrelated context was invalidated"); Check(sceSslTerm_nid_postfix(other) == 0, "unrelated context termination failed"); } @@ -129,14 +141,14 @@ void AtomicFailures() { Check(sceSslGetCaCerts(id, &bytes) == 0, "CA vanished after failure"); CheckRoot(bytes); Check(sceSslFreeCaCerts(id, &bytes) == 0, "snapshot free failed"); - Throws([&] { sceSslLoadCert(id, -1, nullptr, nullptr, nullptr); }); - Throws([&] { sceSslLoadCert(id, 1, nullptr, nullptr, nullptr); }); + Check(sceSslLoadCert(id, -1, nullptr, nullptr, nullptr) == 0, "negative CA count was not ignored"); + Check(sceSslLoadCert(id, 1, nullptr, nullptr, nullptr) == 0, "null CA list was not ignored"); SceSsl::Data* absent = nullptr; Throws([&] { sceSslLoadCert(id, 1, &absent, nullptr, nullptr); }); - Throws([&] { sceSslLoadCert(id, 0, nullptr, &der, nullptr); }); + Check(Stats(id).currentInuseSize == before, "argument failure changed retained CA"); Check(sceSslTerm_nid_postfix(id) == 0, "term failed"); - Throws([&] { sceSslLoadCert(id, 1, &pointer, nullptr, nullptr); }); + Check(sceSslLoadCert(id, 1, &pointer, nullptr, nullptr) == InvalidId, "invalid load context error mismatch"); const int tiny = sceSslInit_nid_postfix(32); Throws([&] { sceSslLoadCert(tiny, 1, &pointer, nullptr, nullptr); }); Check(Stats(tiny).currentInuseSize == 0, "failed small-pool load leaked"); @@ -151,12 +163,200 @@ void ClientIdentity() { Check(sceSslLoadCert(id, 0, nullptr, &client, &key) == 0, "client identity load failed"); const auto before = Stats(id).currentInuseSize; Check(before > 0, "identity not retained"); - Throws([&] { sceSslLoadCert(id, 0, nullptr, &client, &wrong); }); - Check(Stats(id).currentInuseSize == before, "mismatched key changed identity"); + Check(sceSslLoadCert(id, 0, nullptr, &client, &wrong) == 0, "key pair was checked during loading"); + Check(sceSslLoadCert(id, 0, nullptr, &client, nullptr) == 0, "certificate-only load failed"); + const auto certificateBytes = Stats(id).currentInuseSize; + Check(certificateBytes > 0 && certificateBytes < before, "certificate replacement retained the old key"); + Check(sceSslLoadCert(id, 0, nullptr, nullptr, &key) == 0, "independent key load failed"); + Check(Stats(id).currentInuseSize == before, "separate identity load retained different data"); + auto arbitraryKey = Data("invalid key encoding"); + Check(sceSslLoadCert(id, 0, nullptr, nullptr, &arbitraryKey) == 0, "key was parsed during loading"); + Check(Stats(id).currentInuseSize == certificateBytes + arbitraryKey.size, "raw key bytes were not retained"); + SceSsl::Data missing{}; + Check(sceSslLoadCert(id, 0, nullptr, &missing, nullptr) == static_cast(0x80951771), "null certificate payload error mismatch"); + Check(sceSslLoadCert(id, 0, nullptr, nullptr, &missing) == static_cast(0x80951771), "null key payload error mismatch"); + Check(Stats(id).currentInuseSize == certificateBytes + arbitraryKey.size, "null payload cleared the identity"); + Check(sceSslLoadCert(-1, -1, nullptr, &missing, &missing) == InvalidId, "load context validation precedence mismatch"); Check(sceSslUnloadCert(id) == 0 && Stats(id).currentInuseSize == 0, "identity release failed"); 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 == 0, "zero-len serial copy changed length"); + querySize = sizeof(shortBuf); + Check(sceSslGetSerialNumber(id, cert, shortBuf, &querySize) == 0 && querySize == 2 && shortBuf[0] == 0 && shortBuf[1] == 0x80, + "short serial copy mismatch"); + unsigned char serialData[8]{}; + std::size_t serialLen = sizeof(serialData); + Check(sceSslGetSerialNumber(id, cert, serialData, &serialLen) == 0, "get serial failed"); + Check(serialLen == sizeof(serialData) && serialData[3] == 0, "a large enough serial buffer changed the caller's length"); + Check(serialData[0] == 0x00 && serialData[1] == 0x80 && serialData[2] == 0xa1, "serial bytes mismatch"); + Throws([&] { sceSslGetSerialNumber(id, cert, serialData, nullptr); }); + Check(sceSslGetSerialNumber(id, cert, nullptr, nullptr) == 0, "empty serial query failed"); + Check(sceSslGetSerialNumber(id, nullptr, serialData, &serialLen) == static_cast(0x8095f007), "null certificate error mismatch"); + void* fake = &serialLen; + Throws([&] { sceSslGetSerialNumber(id, fake, serialData, &serialLen); }); + Check(sceSslGetSerialNumber(9999, cert, serialData, &serialLen) == InvalidId, "unknown context error mismatch"); + + void* subject = sceSslGetSubjectName(id, cert); + Check(subject != nullptr, "get subject failed"); + Check(sceSslGetSubjectName(id, cert) == subject, "subject handle identity changed"); + void* issuer = sceSslGetIssuerName(id, cert); + Check(issuer != nullptr, "get issuer failed"); + Check(sceSslGetSubjectName(id, nullptr) == nullptr, "invalid name query returned a handle"); + Check(sceSslGetIssuerName(id, nullptr) == nullptr, "invalid name query returned a handle"); + Throws([&] { sceSslGetSubjectName(id, fake); }); + Throws([&] { sceSslGetIssuerName(id, fake); }); + Check(sceSslGetSubjectName(9999, cert) == nullptr, "invalid name query returned a handle"); + Check(sceSslGetIssuerName(9999, cert) == nullptr, "invalid name query returned a handle"); + + Check(sceSslGetNameEntryCount(id, subject) == 3, "subject entry count mismatch"); + Check(sceSslGetNameEntryCount(id, issuer) == 3, "issuer entry count mismatch"); + Check(sceSslGetNameEntryCount(id, nullptr) == static_cast(0x8095f007), "null cert name count accepted"); + Throws([&] { sceSslGetNameEntryCount(id, fake); }); + Check(sceSslGetNameEntryCount(9999, subject) == InvalidId, "unknown name context error mismatch"); + + 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, "countryName") == 0 && std::strcmp(val, "GB") == 0 && vlen == 2, "entry 0 mismatch"); + Check(sceSslGetNameEntryInfo(id, subject, 1, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 1 info failed"); + Check(std::strcmp(oid, "organizationName") == 0 && std::strcmp(val, "AnyPS5 Tests") == 0 && vlen == 12, "entry 1 mismatch"); + Check(sceSslGetNameEntryInfo(id, subject, 2, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 2 info failed"); + Check(std::strcmp(oid, "commonName") == 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, oid, 0, smallVal, sizeof(smallVal), &fullLen) == 0, "truncated query failed"); + Check(std::memcmp(smallVal, "AnyP", 4) == 0 && fullLen == 4, "buffer truncation mismatch"); + char shortOid[5] = {'x', 'x', 'x', 'x', 'x'}; + char shortValue[5] = {'y', 'y', 'y', 'y', 'y'}; + Check(sceSslGetNameEntryInfo(id, subject, 1, shortOid, 4, shortValue, 4, &fullLen) == 0, "bounded entry query failed"); + Check(std::memcmp(shortOid, "orga", 4) == 0 && shortOid[4] == 'x', "oid copy or guard mismatch"); + Check(std::memcmp(shortValue, "AnyP", 4) == 0 && shortValue[4] == 'y' && fullLen == 4, "value copy or guard mismatch"); + Check(sceSslGetNameEntryInfo(id, subject, 1, shortOid, 0, shortValue, 0, &fullLen) == 0 && fullLen == 0, "zero capacity query failed"); + Check(sceSslGetNameEntryInfo(id, subject, 1, nullptr, 0, shortValue, 0, &fullLen) == static_cast(0x8095f007), "null oid accepted"); + Check(sceSslGetNameEntryInfo(id, subject, 1, shortOid, 0, nullptr, 0, &fullLen) == static_cast(0x8095f007), "null value accepted"); + Check(sceSslGetNameEntryInfo(id, subject, 1, shortOid, 0, shortValue, 0, nullptr) == static_cast(0x8095f007), "null value length accepted"); + + Check(sceSslGetNameEntryInfo(id, subject, -1, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095f007), "negative index accepted"); + Check(sceSslGetNameEntryInfo(id, subject, 3, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095f007), "out of bounds index accepted"); + Check(sceSslGetNameEntryInfo(id, nullptr, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095f007), "null certName accepted"); + Throws([&] { sceSslGetNameEntryInfo(id, fake, 0, oid, sizeof(oid), val, sizeof(val), &vlen); }); + Check(sceSslGetNameEntryInfo(9999, subject, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == InvalidId, "unknown entry context error mismatch"); + + Check(sceSslFreeSslCertName(id, subject) == 0, "free subject name failed"); + Check(sceSslFreeSslCertName(id, issuer) == 0, "free issuer name failed"); + Check(sceSslGetNameEntryCount(id, subject) == 3, "name release invalidated a live certificate name"); + Check(sceSslFreeSslCertName(id, nullptr) == static_cast(0x8095f007), "null cert name free accepted"); + Check(sceSslFreeSslCertName(id, subject) == 0, "non-null name release failed"); + Check(sceSslFreeSslCertName(id, fake) == 0, "non-null name release failed"); + Check(sceSslFreeSslCertName(9999, subject) == InvalidId, "invalid name release context error mismatch"); + + SceSsl::RtcTick begin{}; + Check(sceSslGetNotBefore(0, nullptr, nullptr) == InvalidId, "date validation precedence mismatch"); + Check(sceSslGetNotAfter(0, nullptr, nullptr) == InvalidId, "date validation precedence mismatch"); + Check(sceSslGetFingerprint(0, nullptr, -1, nullptr) == InvalidId, "fingerprint validation precedence mismatch"); + 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(0x8095f007), "null begin accepted"); + Check(sceSslGetNotBefore(id, nullptr, &begin) == static_cast(0x8095f007), "null certificate date error mismatch"); + Throws([&] { sceSslGetNotBefore(id, fake, &begin); }); + Check(sceSslGetNotBefore(9999, cert, &begin) == InvalidId, "invalid date context error mismatch"); + + 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(0x8095f007), "null limit accepted"); + Check(sceSslGetNotAfter(id, nullptr, &limit) == static_cast(0x8095f007), "null certificate date error mismatch"); + Throws([&] { sceSslGetNotAfter(id, fake, &limit); }); + Check(sceSslGetNotAfter(9999, cert, &limit) == InvalidId, "invalid date context error mismatch"); + + 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(0x8095f007), "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"); + Check(sceSslGetFingerprint(id, nullptr, 3, sha256) == static_cast(0x8095f007), "null fingerprint certificate error mismatch"); + Throws([&] { sceSslGetFingerprint(id, fake, 3, sha256); }); + Check(sceSslGetFingerprint(9999, cert, 3, sha256) == InvalidId, "invalid fingerprint context error mismatch"); + + std::size_t pemLen = 0; + Check(sceSslGetPem(id, cert, nullptr, &pemLen) == 0, "query pem size failed"); + Check(pemLen > 0, "pem length is 0"); + std::vector pemBuf(pemLen); + std::size_t writtenLen = pemBuf.size(); + Check(sceSslGetPem(id, cert, pemBuf.data(), &writtenLen) == 0, "write pem failed"); + Check(writtenLen == pemLen, "written pem length mismatch"); + Check(std::strstr(pemBuf.data(), "-----BEGIN CERTIFICATE-----\r\n") != nullptr, "pem header missing"); + Check(std::strstr(pemBuf.data(), "-----END CERTIFICATE-----\r\n") != nullptr, "pem footer missing"); + + char smallPem[8]{}; + std::size_t smallPemLen = sizeof(smallPem); + Check(sceSslGetPem(id, cert, smallPem, &smallPemLen) == static_cast(0x8095f008) && smallPemLen == pemLen, "short PEM size error mismatch"); + Check(sceSslGetPem(id, cert, pemBuf.data(), nullptr) == static_cast(0x8095f007), "null pem len accepted"); + Check(sceSslGetPem(id, nullptr, pemBuf.data(), &writtenLen) == static_cast(0x8095f007), "null PEM certificate error mismatch"); + Throws([&] { sceSslGetPem(id, fake, pemBuf.data(), &writtenLen); }); + + Check(sceSslClose(1) == InvalidId, "closing an unknown connection did not report an invalid id"); + + Check(sceSslFreeCaList(id, &handles) == 0, "free ca list failed"); + Check(sceSslTerm_nid_postfix(id) == 0, "term failed"); + Check(sceSslGetSerialNumber(id, cert, serialData, &serialLen) == InvalidId, "terminated context error mismatch"); + Check(sceSslGetSubjectName(id, cert) == nullptr, "invalid name query returned a handle"); + Check(sceSslGetIssuerName(id, cert) == nullptr, "invalid name query returned a handle"); + Check(sceSslGetNotBefore(id, cert, &begin) == InvalidId, "invalid date context error mismatch"); + Check(sceSslGetNotAfter(id, cert, &limit) == InvalidId, "invalid date context error mismatch"); + Check(sceSslGetFingerprint(id, cert, 3, sha256) == InvalidId, "invalid fingerprint context error mismatch"); + Check(sceSslGetPem(id, cert, pemBuf.data(), &writtenLen) == InvalidId, "terminated PEM context error mismatch"); + + 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); }); + Throws([&] { sceSslGetPem(id2, cert2, pemBuf.data(), &writtenLen); }); + Check(sceSslTerm_nid_postfix(id2) == 0, "term failed"); + Check(sceSslGetNameEntryCount(id2, subject2) == InvalidId, "terminated name context error mismatch"); + Check(sceSslFreeSslCertName(id2, subject2) == InvalidId, "invalid name release context error mismatch"); +} + } int main() { @@ -165,6 +365,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/GuestSslPs5Readers.cpp b/core/libs/tests/GuestSslPs5Readers.cpp new file mode 100644 index 0000000000..3075a601e9 --- /dev/null +++ b/core/libs/tests/GuestSslPs5Readers.cpp @@ -0,0 +1,109 @@ +#include "prx/libSceSsl/SslTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SslCertificateFixture.hpp" + +#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 sceSslLoadCert(int, int, SceSsl::Data**, SceSsl::Data*, SceSsl::Data*); +int APS5_VABI sceSslUnloadCert(int); +int APS5_VABI sceSslGetCaList(int, SceSsl::CaList*); +int APS5_VABI sceSslFreeCaList(int, SceSsl::CaList*); +int APS5_VABI 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 sceSslGetPem(int, void*, char*, std::size_t*); +} + +namespace { + +void Check(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +template +void Throws(Function function) { + try { function(); } catch (const std::exception&) { return; } + throw std::runtime_error("unsupported reader arguments were accepted"); +} + +void Readers() { + const int context = sceSslInit_nid_postfix(1024 * 1024); + const int other = sceSslInit_nid_postfix(1024 * 1024); + SceSsl::Data data{reinterpret_cast(const_cast(RootPem)), std::strlen(RootPem)}; + auto* pointer = &data; + Check(sceSslLoadCert(context, 1, &pointer, nullptr, nullptr) == 0, "certificate load failed"); + SceSsl::CaList list{}; + Check(sceSslGetCaList(context, &list) == 0 && list.num == 1, "certificate handle absent"); + void* certificate = list.certs[0]; + std::size_t length = SIZE_MAX; + Check(sceSslGetSerialNumber(context, certificate, nullptr, &length) == 0 && length == 3, "PS5 serial size query failed"); + std::array serial; + serial.fill(0xa5); + Check(sceSslGetSerialNumber(context, certificate, serial.data(), &length) == 0 && length == 3, "PS5 serial copy failed"); + Check(serial == std::array{0x00, 0x80, 0xa1, 0xa5, 0xa5}, "serial copy changed its bytes or guards"); + serial.fill(0xa5); + length = 2; + Check(sceSslGetSerialNumber(context, certificate, serial.data(), &length) == 0 && length == 2, "short serial copy changed length"); + Check(serial == std::array{0x00, 0x80, 0xa5, 0xa5, 0xa5}, "short serial copy changed guard bytes"); + Throws([&] { sceSslGetSerialNumber(other, certificate, nullptr, &length); }); + + auto* subject = sceSslGetSubjectName(context, certificate); + auto* issuer = sceSslGetIssuerName(context, certificate); + Check(subject && issuer && subject != issuer, "certificate name absent"); + Check(sceSslGetNameEntryCount(context, subject) == 3 && sceSslGetNameEntryCount(context, issuer) == 3, "PS5 name entry count failed"); + Throws([&] { sceSslGetSubjectName(other, certificate); }); + Throws([&] { sceSslGetNameEntryCount(other, subject); }); + Check(sceSslFreeSslCertName(other, subject) == 0, "non-null name release checked ownership"); + std::array oid{}, value{}; + length = SIZE_MAX; + Check(sceSslGetNameEntryInfo(context, subject, 1, oid.data(), oid.size(), value.data(), value.size(), &length) == 0, "PS5 eight-argument name query failed"); + Check(std::strcmp(oid.data(), "organizationName") == 0 && std::strcmp(value.data(), "AnyPS5 Tests") == 0 && length == 12, "name output or full-width length changed"); + + std::string expectedPem; + for (const char byte : std::string(RootPem)) { + if (byte == '\n') expectedPem += '\r'; + expectedPem += byte; + } + length = SIZE_MAX; + Check(sceSslGetPem(context, certificate, nullptr, &length) == 0 && length == expectedPem.size() + 1, "PS5 PEM size query failed"); + std::vector pem(length + 2, static_cast(0x5a)); + Check(sceSslGetPem(context, certificate, pem.data(), &length) == 0 && length == expectedPem.size() + 1, "PS5 PEM copy failed"); + Check(std::strcmp(pem.data(), expectedPem.c_str()) == 0 && pem[length] == 0x5a && pem[length + 1] == 0x5a, "PEM copy changed bytes or guards"); + std::array small{'x', 'x', 'x', 'x'}; + length = 2; + Check(sceSslGetPem(context, certificate, small.data(), &length) == static_cast(0x8095f008) && length == expectedPem.size() + 1, "short PEM error or size mismatch"); + Check(small == std::array{0, 0, 'x', 'x'}, "short PEM clearing or guards mismatch"); + Throws([&] { sceSslGetPem(other, certificate, nullptr, &length); }); + Check(sceSslUnloadCert(context) == 0, "unload failed"); + Check(sceSslGetSerialNumber(context, certificate, nullptr, &length) == 0 && length == 3, "CA snapshot lost its certificate on unload"); + Check(sceSslFreeCaList(context, &list) == 0, "CA list free failed"); + Throws([&] { sceSslGetSerialNumber(context, certificate, nullptr, &length); }); + Throws([&] { sceSslGetNameEntryCount(context, subject); }); + Check(sceSslFreeSslCertName(context, subject) == 0, "name free failed"); + Throws([&] { sceSslGetNameEntryCount(context, subject); }); + Check(sceSslTerm_nid_postfix(context) == 0, "context term failed"); + Check(sceSslGetNameEntryCount(context, issuer) == static_cast(0x8095f006), "terminated context count error mismatch"); + Check(sceSslTerm_nid_postfix(other) == 0, "other context term failed"); +} + +} + +int main() { + try { Readers(); } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestSslTransport.cpp b/core/libs/tests/GuestSslTransport.cpp new file mode 100644 index 0000000000..4d956c68be --- /dev/null +++ b/core/libs/tests/GuestSslTransport.cpp @@ -0,0 +1,581 @@ +#ifdef _WIN32 +#include +#else +#include +#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 +#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 sceSslRecv(int, void*, std::size_t); +int APS5_VABI sceSslSend(int, void*, std::size_t); +int APS5_VABI sceSslClose(int); +int APS5_VABI sceSslEnableVerifyOption(int, std::uint32_t); +int APS5_VABI sceSslDisableVerifyOption(int, std::uint32_t); +int APS5_VABI sceSslSetMinSslVersion(int, int); +int APS5_VABI sceSslSetAlpn(int, const char*); +const char* APS5_VABI sceSslGetAlpnSelected(int); +void* APS5_VABI sceSslGetSubjectName(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 socket_nid_postfix(int, int, int); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI setsockopt_nid_postfix(int, int, 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 sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +int APS5_VABI sceNetSocketClose(int); +} + +namespace { + +constexpr int ParallelConnections = 4; +constexpr int ErrorInvalidId = static_cast(0x8095f006); +constexpr int ErrorInvalidValue = static_cast(0x8095f007); +constexpr int ErrorCnCheck = static_cast(0x8095f00b); +constexpr int ErrorUnknownCa = static_cast(0x8095f00c); +constexpr int ErrorNotAfterCheck = static_cast(0x8095f00d); +constexpr int ErrorNotBeforeCheck = static_cast(0x8095f00e); +constexpr int ErrorFatalAlert = static_cast(0x8095f010); +constexpr int ErrorWantPollIn = static_cast(0x8095f013); +constexpr int ErrorWantPollOut = static_cast(0x8095f014); +constexpr int ErrorCallbackCheck = static_cast(0x8095f0fd); +constexpr int ErrorSocketClosed = static_cast(0x8095170d); +constexpr int ErrorNetBadDescriptor = static_cast(0x80410109); +constexpr int ErrorNetPipe = static_cast(0x80410120); +constexpr int ErrorNetReset = static_cast(0x80410136); +constexpr int ErrorBadLength = static_cast(0x80951773); +constexpr std::uint32_t ServerVerify = 0x1, CnCheck = 0x4, NotAfterCheck = 0x8, NotBeforeCheck = 0x10, KnownCaCheck = 0x20; +constexpr int VersionTls12 = 5; +constexpr int GuestSocketLevel = 0xffff; +constexpr int GuestNonBlocking = 0x1200; +constexpr int GuestReceiveTimeout = 0x1006; +constexpr int WaitRounds = 2000; +const char* ServerProtocols[] = {"http/1.1", nullptr}; + +enum class Serve { Close, Exchange, Drop, Reset, Alert }; +enum class Leaf { Valid, Expired, Future }; + +struct GuestTimeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +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; +} + +void Settle() { std::this_thread::sleep_for(std::chrono::milliseconds(5)); } + +struct CallbackRecord { + int context; + int result; + int id = 0; + unsigned int error = 0; + int count = 0; + int calls = 0; + void* leaf = nullptr; + std::string commonName; +}; + +int APS5_VABI RecordingCallback(int id, unsigned int error, void** certificates, int count, void* argument) { + auto& record = *static_cast(argument); + record.id = id; + record.error = error; + record.count = count; + ++record.calls; + if (count > 0 && certificates && certificates[0]) { + record.leaf = certificates[0]; + void* name = sceSslGetSubjectName(record.context, record.leaf); + Require(name != nullptr, "the peer certificate handle has no subject name"); + const int entries = sceSslGetNameEntryCount(record.context, name); + Require(entries > 0, "the peer subject has no entries"); + char oid[16]{}; + char value[64]{}; + std::size_t valueLength = 0; + Require(sceSslGetNameEntryInfo(record.context, name, entries - 1, oid, sizeof(oid), value, sizeof(value), &valueLength) == 0, + "read the peer subject's last entry"); + record.commonName = std::string(oid) + "=" + value; + Require(sceSslFreeSslCertName(record.context, name) == 0, "free the peer subject name"); + } + return record.result; +} + +struct Server { + mbedtls_net_context listener; + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context drbg; + mbedtls_x509_crt certificate; + mbedtls_x509_crt expiredCertificate; + mbedtls_x509_crt futureCertificate; + mbedtls_x509_crt authority; + mbedtls_pk_context key; + mbedtls_pk_context expiredKey; + mbedtls_pk_context futureKey; + mbedtls_ssl_config configs[3]; + 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(&expiredCertificate); + mbedtls_x509_crt_init(&futureCertificate); + mbedtls_x509_crt_init(&authority); + mbedtls_pk_init(&key); + mbedtls_pk_init(&expiredKey); + mbedtls_pk_init(&futureKey); + for (auto& config : configs) mbedtls_ssl_config_init(&config); + Require(mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, nullptr, 0) == 0, "seed server random generator"); + Require(mbedtls_x509_crt_parse(&certificate, reinterpret_cast(ServerPem), sizeof(ServerPem)) == 0, + "parse server certificate"); + 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_x509_crt_parse(&expiredCertificate, reinterpret_cast(ExpiredServerPem), sizeof(ExpiredServerPem)) == 0, + "parse expired server certificate"); + Require(mbedtls_pk_parse_key(&expiredKey, reinterpret_cast(ExpiredServerKey), sizeof(ExpiredServerKey), nullptr, 0, + mbedtls_ctr_drbg_random, &drbg) == 0, "parse expired server key"); + Require(mbedtls_x509_crt_parse(&futureCertificate, reinterpret_cast(FutureServerPem), sizeof(FutureServerPem)) == 0, + "parse future server certificate"); + Require(mbedtls_pk_parse_key(&futureKey, reinterpret_cast(FutureServerKey), sizeof(FutureServerKey), nullptr, 0, + mbedtls_ctr_drbg_random, &drbg) == 0, "parse future server key"); + mbedtls_x509_crt* leaves[] = {&certificate, &expiredCertificate, &futureCertificate}; + mbedtls_pk_context* keys[] = {&key, &expiredKey, &futureKey}; + for (int index = 0; index < 3; ++index) { + auto& config = configs[index]; + Require(mbedtls_ssl_config_defaults(&config, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT) == 0, + "configure server"); + mbedtls_ssl_conf_rng(&config, mbedtls_ctr_drbg_random, &drbg); + Require(mbedtls_ssl_conf_own_cert(&config, leaves[index], keys[index]) == 0, "set server certificate"); + mbedtls_ssl_conf_authmode(&config, MBEDTLS_SSL_VERIFY_REQUIRED); + mbedtls_ssl_conf_ca_chain(&config, &authority, nullptr); + Require(mbedtls_ssl_conf_alpn_protocols(&config, ServerProtocols) == 0, "set server protocols"); + } + Require(mbedtls_net_bind(&listener, "127.0.0.1", "0", MBEDTLS_NET_PROTO_TCP) == 0, "bind loopback listener"); + sockaddr_in address{}; +#ifdef _WIN32 + 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() { + for (auto& config : configs) mbedtls_ssl_config_free(&config); + mbedtls_pk_free(&futureKey); + mbedtls_pk_free(&expiredKey); + mbedtls_pk_free(&key); + mbedtls_x509_crt_free(&authority); + mbedtls_x509_crt_free(&futureCertificate); + mbedtls_x509_crt_free(&expiredCertificate); + mbedtls_x509_crt_free(&certificate); + mbedtls_ctr_drbg_free(&drbg); + mbedtls_entropy_free(&entropy); + mbedtls_net_free(&listener); + } + + void Serve(Serve mode, bool requireClientCertificate = true, Leaf leaf = Leaf::Valid) { + auto& config = configs[static_cast(leaf)]; + mbedtls_ssl_conf_authmode(&config, requireClientCertificate ? MBEDTLS_SSL_VERIFY_REQUIRED : MBEDTLS_SSL_VERIFY_NONE); + mbedtls_net_context client; + mbedtls_net_init(&client); + 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 if (mode == Serve::Alert) { + Alert(client); + } else { + mbedtls_ssl_set_bio(&ssl, &client, mbedtls_net_send, mbedtls_net_recv, nullptr); + const int handshake = mbedtls_ssl_handshake(&ssl); + if (handshake != 0) failure = "server handshake failed, error " + std::to_string(handshake); + else if (mode == Serve::Exchange) Exchange(ssl); + else if (mode == Serve::Close) mbedtls_ssl_close_notify(&ssl); + else if (mode == Serve::Reset) Reset(ssl, client); + } + mbedtls_ssl_free(&ssl); + mbedtls_net_free(&client); + } + + void Alert(mbedtls_net_context& client) { + std::array hello{}; + if (mbedtls_net_recv(&client, hello.data(), hello.size()) <= 0) { + failure = "server did not receive the client hello before the alert"; + return; + } + const unsigned char alert[] = {0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x28}; + if (mbedtls_net_send(&client, alert, sizeof(alert)) != sizeof(alert)) failure = "server could not send the alert"; + } + + void Reset(mbedtls_ssl_context& ssl, mbedtls_net_context& client) { + unsigned char signal = 0; + if (mbedtls_ssl_read(&ssl, &signal, 1) != 1) { + failure = "server did not receive the byte before the reset"; + return; + } + linger immediate{1, 0}; + setsockopt(client.fd, SOL_SOCKET, SO_LINGER, reinterpret_cast(&immediate), sizeof(immediate)); +#ifdef _WIN32 + closesocket(client.fd); +#else + close(client.fd); +#endif + client.fd = -1; + } + + 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 Set(int descriptor, int option, const void* value, std::uint32_t length) const { + return net ? sceNetSetsockopt(descriptor, GuestSocketLevel, option, value, length) + : setsockopt_nid_postfix(descriptor, GuestSocketLevel, option, value, length); + } + + 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(Serve::Close, false); + server.Serve(Serve::Close, false); + server.Serve(Serve::Close, false); + server.Serve(Serve::Close); + server.Serve(Serve::Close); + server.Serve(Serve::Close); + server.Serve(Serve::Close); + server.Serve(Serve::Exchange); + server.Serve(Serve::Exchange); + server.Serve(Serve::Drop); + server.Serve(Serve::Reset); + server.Serve(Serve::Alert); + server.Serve(Serve::Close, true, Leaf::Expired); + server.Serve(Serve::Close, true, Leaf::Expired); + server.Serve(Serve::Close, true, Leaf::Future); + server.Serve(Serve::Close, true, Leaf::Future); + if (sockets.net) server.Serve(Serve::Exchange); + for (int index = 0; index < ParallelConnections; ++index) server.Serve(Serve::Exchange); + }); + + Require(sceSslInit_nid_postfix(0) == static_cast(0x80020016), "an empty pool was accepted"); + const int empty = sceSslInit_nid_postfix(256 * 1024); + const int untrustedSocket = sockets.Open(server.port); + const int untrusted = sceSslCreateConnection(empty, untrustedSocket, "localhost"); + Require(sceSslConnect(untrusted) == ErrorUnknownCa, "a server certificate without a loaded CA passed the initial verify options"); + Require(sceSslConnect(untrusted) == ErrorUnknownCa, "a second connect did not repeat the verification result"); + std::array scratch{}; + Require(Throws([&] { sceSslRead(untrusted, scratch.data(), 1, 0); }), "a connection that failed verification was readable"); + Require(sceSslClose(untrusted) == 0, "close a connection that failed verification"); + Require(sceSslDeleteConnection(untrusted) == 0 && sockets.Close(untrustedSocket) == 0, "release the untrusted connection"); + Require(sceSslDeleteConnection(untrusted) == ErrorInvalidId && sceSslClose(untrusted) == ErrorInvalidId + && sceSslConnect(untrusted) == ErrorInvalidId && sceSslRead(untrusted, scratch.data(), 1, 0) == ErrorInvalidId + && sceSslWrite(untrusted, scratch.data(), 1, 0) == ErrorInvalidId && sceSslSetAlpn(untrusted, "h2") == ErrorInvalidId + && sceSslEnableVerifyOption(untrusted, CnCheck) == ErrorInvalidId && sceSslDisableVerifyOption(untrusted, CnCheck) == ErrorInvalidId + && sceSslSetMinSslVersion(untrusted, VersionTls12) == ErrorInvalidId && sceSslSetVerifyCallback(untrusted, nullptr, nullptr) == ErrorInvalidId, + "a deleted connection did not report an invalid id"); + + const int unverifiedSocket = sockets.Open(server.port); + const int unverified = sceSslCreateConnection(empty, unverifiedSocket, "localhost"); + Require(sceSslDisableVerifyOption(unverified, ServerVerify) == 0, "disable server verification"); + Require(sceSslConnect(unverified) == 0, "with server verification disabled no loaded CA is no failure"); + Require(sceSslGetAlpnSelected(unverified) == nullptr, "a protocol was selected without an ALPN list"); + Require(sceSslRead(unverified, scratch.data(), scratch.size(), 0) == 0, "close notify did not end the unverified stream"); + Require(sceSslDeleteConnection(unverified) == 0 && sockets.Close(unverifiedSocket) == 0, "release the unverified connection"); + + CallbackRecord accepting{empty, 0}; + const int acceptedSocket = sockets.Open(server.port); + const int accepted = sceSslCreateConnection(empty, acceptedSocket, "localhost"); + Require(sceSslSetVerifyCallback(accepted, RecordingCallback, &accepting) == 0, "set the accepting verify callback"); + Require(sceSslConnect(accepted) == 0, "a verify callback that returns 0 did not accept the connection"); + Require(accepting.id == empty && accepting.error == KnownCaCheck, "the verify callback did not receive the context id and the failed option"); + Require(accepting.count == 1 && accepting.commonName == "commonName=localhost", "the verify callback did not receive the peer chain"); + Require(sceSslConnect(accepted) == 0 && accepting.calls == 1, "a second connect after an accepted callback ran the callback again"); + Require(sceSslRead(accepted, scratch.data(), scratch.size(), 0) == 0, "an accepted connection was not readable"); + Require(sceSslDeleteConnection(accepted) == 0 && sockets.Close(acceptedSocket) == 0, "release the accepted connection"); + Require(Throws([&] { sceSslGetSubjectName(empty, accepting.leaf); }), "a peer certificate handle outlived its connection"); + Require(sceSslTerm_nid_postfix(empty) == 0, "release the context without CA"); + Require(sceSslTerm_nid_postfix(empty) == ErrorInvalidId, "a second context termination did not report an invalid id"); + + const int context = sceSslInit_nid_postfix(1024 * 1024); + std::string root = RootPem; + 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); + + Require(sceSslCreateConnection(context, 1000, nullptr) == ErrorInvalidValue, "a null host was accepted"); + const int closed = sceSslCreateConnection(context, 1000, "localhost"); + Require(sceSslConnect(closed) == ErrorNetBadDescriptor, "a handshake over a closed descriptor did not report the bad descriptor"); + Require(sceSslClose(closed) == 0, "close a connection that never connected"); + Require(sceSslDeleteConnection(closed) == 0, "delete connection over a closed descriptor"); + + const int mismatchSocket = sockets.Open(server.port); + const int mismatch = sceSslCreateConnection(context, mismatchSocket, "example.com"); + Require(sceSslConnect(mismatch) == ErrorCnCheck, "a certificate for another host passed the host name check"); + Require(sceSslConnect(mismatch) == ErrorCnCheck, "a second connect did not repeat the host name result"); + Require(Throws([&] { sceSslRead(mismatch, root.data(), 1, 0); }), "a failed verification left the connection usable"); + Require(sceSslDeleteConnection(mismatch) == 0 && sockets.Close(mismatchSocket) == 0, "release the mismatched connection"); + + const int lenientSocket = sockets.Open(server.port); + const int lenient = sceSslCreateConnection(context, lenientSocket, "example.com"); + Require(sceSslDisableVerifyOption(lenient, CnCheck) == 0, "disable the host name check"); + Require(sceSslConnect(lenient) == 0, "a disabled host name check still failed the handshake"); + Require(sceSslDeleteConnection(lenient) == 0 && sockets.Close(lenientSocket) == 0, "release the lenient connection"); + + CallbackRecord rejecting{context, -1}; + const int rejectedSocket = sockets.Open(server.port); + const int rejected = sceSslCreateConnection(context, rejectedSocket, "localhost"); + Require(sceSslSetVerifyCallback(rejected, RecordingCallback, &rejecting) == 0, "set the rejecting verify callback"); + Require(sceSslConnect(rejected) == ErrorCallbackCheck, "a negative verify callback result did not fail the connect"); + Require(rejecting.error == 0 && rejecting.count == 1 && rejecting.id == context, "the verify callback did not see a passed chain"); + Require(sceSslConnect(rejected) == ErrorCallbackCheck && rejecting.calls == 2, "a second connect did not run the callback again"); + Require(sceSslRead(rejected, scratch.data(), 1, 0) == ErrorCallbackCheck && rejecting.calls == 3, "a read after the rejection did not run the callback again"); + Require(sceSslWrite(rejected, scratch.data(), 1, 0) == ErrorCallbackCheck && rejecting.calls == 4, "a write after the rejection did not run the callback again"); + Require(sceSslClose(rejected) == 0, "close the rejected connection"); + Require(sceSslDeleteConnection(rejected) == 0 && sockets.Close(rejectedSocket) == 0, "release the rejected connection"); + + const int earlySocket = sockets.Open(server.port); + const int early = sceSslCreateConnection(context, earlySocket, "localhost"); + Require(sceSslClose(early) == 0, "close before the handshake"); + Require(sceSslRead(early, scratch.data(), scratch.size(), 0) == 0, "a read before the handshake did not run it and reach the close notify"); + Require(sceSslConnect(early) == 0, "a connect after the read-driven handshake did not return 0"); + Require(sceSslDeleteConnection(early) == 0 && sockets.Close(earlySocket) == 0, "release the early connection"); + + const int descriptor = sockets.Open(server.port); + const int connection = sceSslCreateConnection(context, descriptor, "localhost"); + std::array buffer{}; + Require(sceSslSetMinSslVersion(connection, 0) == ErrorInvalidValue && sceSslSetMinSslVersion(connection, 6) == ErrorInvalidValue + && sceSslSetMinSslVersion(connection, 2) == 0, "the accepted minimum versions changed"); + Require(sceSslEnableVerifyOption(connection, 0x40) == ErrorInvalidValue && sceSslDisableVerifyOption(connection, 0x1000) == ErrorInvalidValue + && sceSslEnableVerifyOption(connection, 0x3f) == 0, "the verify option mask changed"); + Require(sceSslSetAlpn(connection, "") == ErrorBadLength, "an empty ALPN list was accepted"); + Require(sceSslSetMinSslVersion(connection, VersionTls12) == 0 && sceSslSetAlpn(connection, "h2,http/1.1") == 0, "set version and ALPN"); + Require(sceSslGetAlpnSelected(connection) == nullptr, "a protocol was selected before the handshake"); + Require(sceSslConnect(connection) == 0, "handshake with the loopback server"); + Require(InUse(context) > loaded, "the connection is not charged to the context pool"); + Require(sceSslConnect(connection) == 0, "a second connect after success did not return the same result"); + const char* selected = sceSslGetAlpnSelected(connection); + Require(selected && std::strcmp(selected, "http/1.1") == 0, "the negotiated ALPN protocol is not reported"); + Require(sceSslWrite(connection, buffer.data(), 1, 1) == ErrorInvalidValue, "unknown write flags were accepted"); + char request[] = "ping"; + Require(sceSslSend(connection, request, 4) == 4, "send the request"); + Require(sceSslRead(connection, buffer.data(), 2, 1) == 2 && std::memcmp(buffer.data(), "po", 2) == 0, "a partial read did not return the available bytes"); + Require(sceSslRecv(connection, buffer.data(), 2) == 2 && std::memcmp(buffer.data(), "ng", 2) == 0, "receive did not fill the remaining reply"); + Require(sceSslRecv(connection, buffer.data(), buffer.size()) == 0, "receive did not stop at the end of the stream"); + Require(sceSslRead(connection, buffer.data(), buffer.size(), 1) == 0, "close notify did not end the stream"); + Require(sceSslClose(connection) == 0, "close the connection"); + Require(sceSslRead(connection, buffer.data(), buffer.size(), 0) == 0, "a read after the close did not return 0"); + Require(sceSslConnect(connection) == 0, "a connect after the close did not return 0"); + Require(selected && std::strcmp(sceSslGetAlpnSelected(connection), "http/1.1") == 0, "the ALPN protocol was forgotten after the close"); + Require(sceSslClose(connection) == 0, "a second close failed"); + Require(sceSslDeleteConnection(connection) == 0, "delete the connection"); + Require(InUse(context) == loaded, "the connection's pool memory was not returned"); + Require(sockets.Close(descriptor) == 0, "the socket did not stay open after the connection was deleted"); + + const int pollingSocket = sockets.Open(server.port); + const int enabled = 1; + Require(sockets.Set(pollingSocket, GuestNonBlocking, &enabled, sizeof(enabled)) == 0, "make the guest socket non-blocking"); + const int polling = sceSslCreateConnection(context, pollingSocket, "localhost"); + int rounds = 0; + for (int result; (result = sceSslConnect(polling)) != 0; Settle()) { + Require(result == ErrorWantPollIn || result == ErrorWantPollOut, "a non-blocking handshake failed instead of asking to poll"); + Require(++rounds < WaitRounds, "a non-blocking handshake did not finish"); + } + Require(sceSslRead(polling, buffer.data(), buffer.size(), 0) == ErrorWantPollIn, "a non-blocking read with nothing to read did not ask to poll"); + Require(sceSslRead(polling, buffer.data(), buffer.size(), 1) == ErrorWantPollIn, "a non-blocking partial read with nothing to read did not ask to poll"); + Require(sceSslWrite(polling, request, 4, 0) == 4, "write the non-blocking request"); + int received; + for (rounds = 0; (received = sceSslRead(polling, buffer.data(), buffer.size(), 1)) == ErrorWantPollIn; Settle()) + Require(++rounds < WaitRounds, "the non-blocking reply did not arrive"); + Require(received == 4 && std::memcmp(buffer.data(), "pong", 4) == 0, "read the non-blocking reply"); + for (rounds = 0; (received = sceSslRead(polling, buffer.data(), buffer.size(), 0)) == ErrorWantPollIn; Settle()) + Require(++rounds < WaitRounds, "the non-blocking close notify did not arrive"); + Require(received == 0, "close notify did not end the non-blocking stream"); + Require(sceSslClose(polling) == 0 && sceSslDeleteConnection(polling) == 0 && sockets.Close(pollingSocket) == 0, + "release the non-blocking connection"); + + const int droppedSocket = sockets.Open(server.port); + const int dropped = sceSslCreateConnection(context, droppedSocket, "localhost"); + Require(sceSslConnect(dropped) == 0, "handshake before the server drops the connection"); + Require(sceSslRead(dropped, buffer.data(), buffer.size(), 0) == 0, "a stream that ended without close notify did not end the read"); + Require(sceSslRead(dropped, buffer.data(), buffer.size(), 1) == 0, "a second read after the drop did not return 0"); + Require(sceSslClose(dropped) == 0, "close after the drop"); + Require(sceSslDeleteConnection(dropped) == 0 && sockets.Close(droppedSocket) == 0, "release the dropped connection"); + + const int resetSocket = sockets.Open(server.port); + const int reset = sceSslCreateConnection(context, resetSocket, "localhost"); + Require(sceSslConnect(reset) == 0, "handshake before the server resets the connection"); + Require(sceSslWrite(reset, request, 1, 0) == 1, "write the byte the server resets after"); + Require(sceSslRead(reset, buffer.data(), buffer.size(), 0) == ErrorNetReset, "a reset did not report the libSceNet reset code"); + Require(sceSslRead(reset, buffer.data(), buffer.size(), 1) == ErrorNetReset, "a second read after the reset changed the code"); + Require(sceSslWrite(reset, request, 4, 0) == ErrorNetPipe, "a write after the reset did not report the broken pipe"); + Require(sceSslConnect(reset) == 0, "a connect after the reset did not return 0"); + Require(sceSslClose(reset) == ErrorNetReset && sceSslClose(reset) == ErrorNetReset, "a close after the reset did not report the reset twice"); + Require(sceSslDeleteConnection(reset) == 0 && sockets.Close(resetSocket) == 0, "release the reset connection"); + + const int alertedSocket = sockets.Open(server.port); + const int alerted = sceSslCreateConnection(context, alertedSocket, "localhost"); + Require(sceSslConnect(alerted) == ErrorFatalAlert, "a fatal alert during the handshake did not report it"); + Require(sceSslConnect(alerted) == 0, "a connect after the fatal alert did not return 0"); + Require(sceSslRead(alerted, buffer.data(), buffer.size(), 0) == 0, "a read after the fatal alert did not return 0"); + Require(sceSslWrite(alerted, request, 4, 0) == ErrorSocketClosed, "a write after the fatal alert did not report the closed socket"); + Require(sceSslClose(alerted) == 0 && sceSslClose(alerted) == 0, "close after the fatal alert"); + Require(sceSslDeleteConnection(alerted) == 0 && sockets.Close(alertedSocket) == 0, "release the alerted connection"); + + const int expiredSocket = sockets.Open(server.port); + const int expired = sceSslCreateConnection(context, expiredSocket, "localhost"); + Require(sceSslConnect(expired) == 0, "an expired server certificate failed with the initial verify options"); + Require(sceSslRead(expired, buffer.data(), buffer.size(), 0) == 0, "close notify did not end the expired stream"); + Require(sceSslDeleteConnection(expired) == 0 && sockets.Close(expiredSocket) == 0, "release the expired connection"); + const int expiredCheckedSocket = sockets.Open(server.port); + const int expiredChecked = sceSslCreateConnection(context, expiredCheckedSocket, "localhost"); + Require(sceSslEnableVerifyOption(expiredChecked, NotAfterCheck) == 0, "enable the expiry check"); + Require(sceSslConnect(expiredChecked) == ErrorNotAfterCheck, "an expired server certificate passed the enabled expiry check"); + Require(sceSslDeleteConnection(expiredChecked) == 0 && sockets.Close(expiredCheckedSocket) == 0, "release the checked expired connection"); + const int futureSocket = sockets.Open(server.port); + const int future = sceSslCreateConnection(context, futureSocket, "localhost"); + Require(sceSslEnableVerifyOption(future, NotAfterCheck) == 0, "enable the expiry check on the future connection"); + Require(sceSslConnect(future) == 0, "a not yet valid server certificate failed without the start check"); + Require(sceSslDeleteConnection(future) == 0 && sockets.Close(futureSocket) == 0, "release the future connection"); + const int futureCheckedSocket = sockets.Open(server.port); + const int futureChecked = sceSslCreateConnection(context, futureCheckedSocket, "localhost"); + Require(sceSslEnableVerifyOption(futureChecked, NotBeforeCheck) == 0, "enable the start check"); + Require(sceSslConnect(futureChecked) == ErrorNotBeforeCheck, "a not yet valid server certificate passed the enabled start check"); + Require(sceSslDeleteConnection(futureChecked) == 0 && sockets.Close(futureCheckedSocket) == 0, "release the checked future connection"); + + if (sockets.net) { + const int timedSocket = sockets.Open(server.port); + const GuestTimeval timeout{0, 200000}; + Require(sockets.Set(timedSocket, GuestReceiveTimeout, &timeout, sizeof(timeout)) == 0, "set the receive timeout"); + const int timed = sceSslCreateConnection(context, timedSocket, "localhost"); + Require(sceSslConnect(timed) == 0, "handshake over the socket with a receive timeout"); + Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == ErrorWantPollIn, "an expired receive timeout did not ask to poll"); + Require(sceSslWrite(timed, request, 4, 0) == 4, "write the request after a timeout"); + Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == 4 && std::memcmp(buffer.data(), "pong", 4) == 0, "read the reply after a timeout"); + Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == 0, "close notify did not end the timed stream"); + Require(sceSslDeleteConnection(timed) == 0 && sockets.Close(timedSocket) == 0, "release the timed connection"); + } + + 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(sceSslClose(parallel) == 0 && 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 83b97d5ecd..c37c580e6b 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -21,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*); @@ -34,6 +38,10 @@ extern FileStream* __stdoutp_nid_postfix; extern FileStream* __stderrp_nid_postfix; extern int __isthreaded_nid_postfix; int APS5_VABI fprintf_nid_postfix(FileStream*, const char*, ...); +int APS5_VABI fputs_nid_postfix(const char*, FileStream*); +int APS5_VABI printf_nid_postfix(const char*, ...); +int APS5_VABI vprintf_nid_postfix(const char*, void*); +int APS5_VABI puts_nid_postfix(const char*); int APS5_VABI vfprintf_nid_postfix(FileStream*, const char*, void*); int APS5_VABI vsprintf_nid_postfix(char*, const char*, void*); int APS5_VABI fgetc_nid_postfix(FileStream*); @@ -42,6 +50,13 @@ int APS5_VABI getc_unlocked_nid_postfix(FileStream*); int APS5_VABI putc_unlocked_nid_postfix(int, FileStream*); int APS5_VABI fputwc_nid_postfix(char16_t, FileStream*); int APS5_VABI fputws_nid_postfix(const char16_t*, FileStream*); +std::int32_t APS5_VABI getwc_nid_postfix(FileStream*); +std::int32_t APS5_VABI getwchar_nid_postfix(); +int APS5_VABI putwc_nid_postfix(char16_t, FileStream*); +int APS5_VABI putwchar_nid_postfix(char16_t); +char16_t* APS5_VABI fgetws_nid_postfix(char16_t*, int, FileStream*); +int APS5_VABI getchar_unlocked_nid_postfix(); +int APS5_VABI putchar_unlocked_nid_postfix(int); int APS5_VABI fscanf_nid_postfix(FileStream*, const char*, ...); int APS5_VABI __srget_nid_postfix(FileStream*); int APS5_VABI __swbuf_nid_postfix(int, FileStream*); @@ -49,6 +64,8 @@ int APS5_VABI ungetc_nid_postfix(int, FileStream*); char* APS5_VABI fgets_nid_postfix(char*, int, FileStream*); int APS5_VABI feof_nid_postfix(FileStream*); int APS5_VABI ferror_nid_postfix(FileStream*); +int APS5_VABI feof_unlocked_nid_postfix(FileStream*); +int APS5_VABI ferror_unlocked_nid_postfix(FileStream*); int APS5_VABI fileno_nid_postfix(FileStream*); void APS5_VABI clearerr_nid_postfix(FileStream*); void APS5_VABI rewind_nid_postfix(FileStream*); @@ -80,6 +97,22 @@ static int APS5_VABI WriteFormatted(FileStream* stream, const char* format, ...) #endif return result; } +static int APS5_VABI PrintStandard(const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vprintf_nid_postfix(format, args); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} static int APS5_VABI FormatString(char* buffer, const char* format, ...) { #ifdef _WIN32 __builtin_sysv_va_list args; @@ -136,11 +169,41 @@ static void CheckUnlockedStreams() { { std::ofstream seed(filename, std::ios::binary); seed.put('A'); Require(seed.good()); } FileStream readOnly(std::fopen(filename.c_str(), "rb")); Require(putc_unlocked_nid_postfix('X', &readOnly) == EOF && (readOnly.GuestState().flags & 0x40)); + Require(ferror_unlocked_nid_postfix(&readOnly) == 1 && feof_unlocked_nid_postfix(&readOnly) == 0); + clearerr_unlocked_nid_postfix(&readOnly); + Require(ferror_unlocked_nid_postfix(&readOnly) == 0); readOnly.Close(); Require(CheckFileBytes(filename, "A", "rb", false)); Require(std::filesystem::remove(filename)); } +static void CheckUnlockedQueries() { + FileStream stream(std::tmpfile()); + Require(feof_unlocked_nid_postfix(&stream) == 0 && ferror_unlocked_nid_postfix(&stream) == 0); + Require(std::fgetc(stream.GetHandle()) == EOF); + Require(feof_unlocked_nid_postfix(&stream) == 1 && ferror_unlocked_nid_postfix(&stream) == 0); + Require((stream.GuestState().flags & 0x60) == 0x20); + stream.SetError(); + Require(feof_unlocked_nid_postfix(&stream) == 1 && ferror_unlocked_nid_postfix(&stream) == 1); + clearerr_unlocked_nid_postfix(&stream); + Require(feof_unlocked_nid_postfix(&stream) == 0 && ferror_unlocked_nid_postfix(&stream) == 0); + Require(std::fgetc(stream.GetHandle()) == EOF && std::ungetc('Q', stream.GetHandle()) == 'Q'); + Require(feof_unlocked_nid_postfix(&stream) == 0 && ferror_unlocked_nid_postfix(&stream) == 0); + Require(std::fgetc(stream.GetHandle()) == 'Q'); + Require(fputc_nid_postfix(0x7f, &stream) == 0x7f); + std::rewind(stream.GetHandle()); + Require(fgetc_nid_postfix(&stream) == 0x7f && feof_nid_postfix(&stream) == 0); + 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 = [&] { @@ -192,7 +255,7 @@ static void CheckTryLock() { Require(threw); } -static void CheckUnlockedStreamLock(bool write) { +static void CheckUnlockedStreamLock(bool write, int (APS5_VABI *query)(FileStream*) = nullptr) { FileStream stream(std::tmpfile()); Require(std::fputc('A', stream.GetHandle()) == 'A'); std::rewind(stream.GetHandle()); @@ -213,7 +276,7 @@ static void CheckUnlockedStreamLock(bool write) { } std::thread caller([&] { entered = true; - result = write ? putc_unlocked_nid_postfix('B', &stream) : getc_unlocked_nid_postfix(&stream); + result = query ? query(&stream) : write ? putc_unlocked_nid_postfix('B', &stream) : getc_unlocked_nid_postfix(&stream); done = true; }); const auto enteredDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); @@ -230,7 +293,7 @@ static void CheckUnlockedStreamLock(bool write) { release = true; owner.join(); caller.join(); - Require(completedWhileHeld && result == (write ? 'B' : 'A')); + Require(completedWhileHeld && result == (query ? 0 : write ? 'B' : 'A')); stream.Close(); } @@ -493,7 +556,72 @@ static void CheckDescriptorAccessModes() { Require(std::filesystem::remove(filename)); } -int main() { +static void CheckClosedStreamWrites() { + FileStream stream(std::tmpfile()); + Require(freopen_nid_postfix("missing-closed-stream-dir/out.log", "w", &stream) == nullptr); + Require(fputs_nid_postfix("text", &stream) == -1 && *__error_nid_postfix() == 9); + Require(fprintf_nid_postfix(&stream, "%d", 1) == -1 && *__error_nid_postfix() == 9); + Require(fwrite_nid_postfix("text", 1, 4, &stream) == 0 && *__error_nid_postfix() == 9); + Require(fputc_nid_postfix('t', &stream) == -1 && *__error_nid_postfix() == 9); + Require(putc_unlocked_nid_postfix('t', &stream) == -1 && *__error_nid_postfix() == 9); + Require(fflush_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); + char buffer[8] = {}; + Require(fread_nid_postfix(buffer, 1, sizeof(buffer), &stream) == 0 && *__error_nid_postfix() == 9); + Require(fgetc_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); + Require(fgets_nid_postfix(buffer, sizeof(buffer), &stream) == nullptr && *__error_nid_postfix() == 9); + Require(fseeko_nid_postfix(&stream, 0, SEEK_SET) == -1 && *__error_nid_postfix() == 9); + Require(ftello_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); + Require(fileno_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); + Require(ferror_nid_postfix(&stream) == 0 && feof_nid_postfix(&stream) == 0); + Require(fclose_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); +} + +static int CheckClosedStandardOutput() { + Require(freopen_nid_postfix("missing-closed-stream-dir/out.log", "w", __stdoutp_nid_postfix) == nullptr); + Require(printf_nid_postfix("%d", 1) == -1 && *__error_nid_postfix() == 9); + Require(puts_nid_postfix("text") == -1 && *__error_nid_postfix() == 9); + Require(PrintStandard("%d", 1) == -1 && *__error_nid_postfix() == 9); + Require(fputs_nid_postfix("text", __stdoutp_nid_postfix) == -1 && *__error_nid_postfix() == 9); + Require(std::fputs("host output\n", stdout) >= 0 && std::fflush(stdout) == 0 && !std::ferror(stdout)); + Require(freopen_nid_postfix("missing-closed-stream-dir/err.log", "a", __stderrp_nid_postfix) == nullptr); + Require(std::fputs("host error\n", stderr) >= 0 && std::fflush(stderr) == 0 && !std::ferror(stderr)); + return 0; +} + +static int CheckExclusiveStandardOutput() { + const auto filename = "anyps5-exclusive-stdout-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + Require(freopen_nid_postfix(filename.c_str(), "wx", __stdoutp_nid_postfix) == __stdoutp_nid_postfix); + Require(fputs_nid_postfix("exclusive\n", __stdoutp_nid_postfix) >= 0 && fflush_nid_postfix(__stdoutp_nid_postfix) == 0); + Require(fclose_nid_postfix(__stdoutp_nid_postfix) == 0); + Require(CheckFileBytes(filename, "exclusive\n", "wx", true)); + Require(std::filesystem::remove(filename)); + return 0; +} + +#ifdef _WIN32 +static void CheckStandardStreamDeleteSharing() { + int index = 0; + for (auto* stream : {__stdinp_nid_postfix, __stderrp_nid_postfix}) { + const std::string filename = "anyps5-standard-delete-" + std::to_string(index++) + "-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + auto* const native = stream->GetHandle(); + const int descriptor = ::_fileno(native); + Require(freopen_nid_postfix(filename.c_str(), "w+b", stream) == stream); + Require(stream->GetHandle() == native && ::_fileno(stream->GetHandle()) == descriptor); + Require(fputc_nid_postfix('A', stream) == 'A' && fflush_nid_postfix(stream) == 0); + const std::string renamed = filename + "-moved"; + std::filesystem::rename(filename, renamed); + Require(fputc_nid_postfix('B', stream) == 'B' && fflush_nid_postfix(stream) == 0); + Require(std::filesystem::remove(renamed)); + Require(fseeko_nid_postfix(stream, 0, SEEK_SET) == 0); + Require(fgetc_nid_postfix(stream) == 'A' && fgetc_nid_postfix(stream) == 'B'); + } +} +#endif + +int main(int argc, char** argv) { + if (argc > 1 && std::string_view(argv[1]) == "--closed-stdout") return CheckClosedStandardOutput(); + if (argc > 1 && std::string_view(argv[1]) == "--exclusive-stdout") return CheckExclusiveStandardOutput(); + CheckClosedStreamWrites(); CheckDescriptorAccessModes(); Require(_Getmbcurmax_nid_postfix() == 1 && _Getmbcurmax_nid_postfix() == ___mb_cur_max_nid_postfix()); CheckFlushWhileReading(); @@ -543,6 +671,25 @@ int main() { Require(fileno_nid_postfix(__stdinp_nid_postfix) == 0); Require(fileno_nid_postfix(__stdoutp_nid_postfix) == 1); Require(fileno_nid_postfix(__stderrp_nid_postfix) == 2); + auto* const savedInput = __stdinp_nid_postfix; + auto* const savedOutput = __stdoutp_nid_postfix; + FileStream customIn(std::tmpfile()); + Require(fputc_nid_postfix('T', &customIn) == 'T'); + std::rewind(customIn.GetHandle()); + __stdinp_nid_postfix = &customIn; + Require(getchar_unlocked_nid_postfix() == 'T'); + Require(getchar_unlocked_nid_postfix() == EOF); + Require(ungetc_nid_postfix('W', &customIn) == 'W'); + Require(getwchar_nid_postfix() == 'W'); + FileStream customOut(std::tmpfile()); + __stdoutp_nid_postfix = &customOut; + Require(putchar_unlocked_nid_postfix('U') == 'U'); + Require(putwchar_nid_postfix(u'V') == u'V'); + std::rewind(customOut.GetHandle()); + Require(fgetc_nid_postfix(&customOut) == 'U'); + Require(fgetc_nid_postfix(&customOut) == 'V'); + __stdinp_nid_postfix = savedInput; + __stdoutp_nid_postfix = savedOutput; FileStream stream(std::tmpfile()); auto& guest = *reinterpret_cast(&stream); Require(&guest == &stream.GuestState()); @@ -579,6 +726,62 @@ int main() { Require(std::memcmp(wideBytes, "AB\xC3\xA9", 4) == 0); wide.Close(); + FileStream wideIo(std::tmpfile()); + Require(putwc_nid_postfix(u'H', &wideIo) == u'H'); + Require(putwc_nid_postfix(u'i', &wideIo) == u'i'); + Require(putwc_nid_postfix(u'\n', &wideIo) == u'\n'); + Require(fputws_nid_postfix(u"Next\n", &wideIo) == 5); + Require(fputws_nid_postfix(u"End", &wideIo) == 3); + std::rewind(wideIo.GetHandle()); + Require(getwc_nid_postfix(&wideIo) == 'H'); + std::rewind(wideIo.GetHandle()); + char16_t wideBuf[16]{}; + Require(fgetws_nid_postfix(wideBuf, 16, &wideIo) == wideBuf); + Require(wideBuf[0] == u'H' && wideBuf[1] == u'i' && wideBuf[2] == u'\n' && wideBuf[3] == 0); + Require(fgetws_nid_postfix(wideBuf, 16, &wideIo) == wideBuf); + Require(wideBuf[0] == u'N' && wideBuf[1] == u'e' && wideBuf[2] == u'x' && wideBuf[3] == u't' && wideBuf[4] == u'\n' && wideBuf[5] == 0); + Require(fgetws_nid_postfix(wideBuf, 16, &wideIo) == wideBuf); + Require(wideBuf[0] == u'E' && wideBuf[1] == u'n' && wideBuf[2] == u'd' && wideBuf[3] == 0); + Require(fgetws_nid_postfix(wideBuf, 16, &wideIo) == nullptr); + Require(feof_nid_postfix(&wideIo)); + Require(fgetws_nid_postfix(wideBuf, 0, &wideIo) == nullptr && *__error_nid_postfix() == 22); + Require(ferror_nid_postfix(&wideIo)); + clearerr_nid_postfix(&wideIo); + Require(fgetws_nid_postfix(nullptr, 16, &wideIo) == nullptr && *__error_nid_postfix() == 22); + Require(ferror_nid_postfix(&wideIo)); + clearerr_nid_postfix(&wideIo); + Require(fgetws_nid_postfix(wideBuf, 16, nullptr) == nullptr && *__error_nid_postfix() == 22); + wideBuf[0] = u'X'; + wideBuf[1] = u'Y'; + std::rewind(wideIo.GetHandle()); + Require(fgetws_nid_postfix(wideBuf, 1, &wideIo) == wideBuf && wideBuf[0] == 0 && wideBuf[1] == u'Y'); + Require(fgetc_nid_postfix(&wideIo) == 'H'); + wideIo.Close(); + + FileStream roundTrip(std::tmpfile()); + Require(putwc_nid_postfix(u'é', &roundTrip) == u'é'); + Require(putwc_nid_postfix(u'\u20ac', &roundTrip) == u'\u20ac'); + Require(putwc_nid_postfix(u'\n', &roundTrip) == u'\n'); + std::rewind(roundTrip.GetHandle()); + Require(getwc_nid_postfix(&roundTrip) == static_cast(u'é')); + Require(getwc_nid_postfix(&roundTrip) == static_cast(u'\u20ac')); + Require(getwc_nid_postfix(&roundTrip) == static_cast(u'\n')); + Require(getwc_nid_postfix(&roundTrip) == -1); + std::rewind(roundTrip.GetHandle()); + char16_t utfBuf[16]{}; + Require(fgetws_nid_postfix(utfBuf, 16, &roundTrip) == utfBuf); + Require(utfBuf[0] == u'é' && utfBuf[1] == u'\u20ac' && utfBuf[2] == u'\n' && utfBuf[3] == 0); + roundTrip.Close(); + + FileStream midErr(std::tmpfile()); + Require(fputc_nid_postfix('A', &midErr) == 'A'); + Require(fputc_nid_postfix(0xff, &midErr) == 0xff); + std::rewind(midErr.GetHandle()); + char16_t midBuf[16]{}; + Require(fgetws_nid_postfix(midBuf, 16, &midErr) == nullptr); + Require(ferror_nid_postfix(&midErr)); + midErr.Close(); + FileStream formatted(std::tmpfile()); const char expected[] = "guest 4294967297 1.25 1 2 3 4 5 6 7 8\n"; Require(fprintf_nid_postfix(&formatted, "%s %ld %.2f %d %d %d %d %d %d %d %d\n", @@ -708,6 +911,34 @@ int main() { CheckUnlockedStreams(); CheckUnlockedStreamLock(false); CheckUnlockedStreamLock(true); + CheckUnlockedStreamLock(false, feof_unlocked_nid_postfix); + CheckUnlockedStreamLock(false, ferror_unlocked_nid_postfix); CheckTryLock(); - return CheckBinaryModes() ? 0 : 1; + CheckUnlockedQueries(); + if (!CheckBinaryModes()) return 1; +#ifdef _WIN32 + CheckStandardStreamDeleteSharing(); + const std::string standardFilename = "anyps5-standard-reopen-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + Require(freopen_nid_postfix(standardFilename.c_str(), "w", __stdoutp_nid_postfix) == __stdoutp_nid_postfix); + Require(printf_nid_postfix("standard output") == 15 && fflush_nid_postfix(__stdoutp_nid_postfix) == 0); + { std::ifstream input(standardFilename, std::ios::binary); std::string contents; std::getline(input, contents); + Require(contents == "standard output"); } + const std::string movedFilename = standardFilename + "-moved"; + std::error_code renameError; + std::filesystem::rename(standardFilename, movedFilename, renameError); + Require(!renameError && !std::filesystem::exists(standardFilename) && std::filesystem::exists(movedFilename)); + Require(printf_nid_postfix(" continued") == 10 && fflush_nid_postfix(__stdoutp_nid_postfix) == 0); + { std::ifstream input(movedFilename, std::ios::binary); std::string contents; std::getline(input, contents); + Require(contents == "standard output continued"); } + Require(std::filesystem::remove(movedFilename)); + Require(!std::filesystem::exists(movedFilename)); + Require(printf_nid_postfix(" after delete") == 13 && fflush_nid_postfix(__stdoutp_nid_postfix) == 0); + const std::string exclusiveFilename = "anyps5-standard-exclusive-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + { std::ofstream out(exclusiveFilename, std::ios::binary); out << "existing"; } + Require(freopen_nid_postfix(exclusiveFilename.c_str(), "wx", __stdoutp_nid_postfix) == nullptr && *__error_nid_postfix() == 17); + Require(std::filesystem::remove(exclusiveFilename)); + Require(printf_nid_postfix("closed") == -1 && *__error_nid_postfix() == 9); + Require(std::fputs("host stdout intact\n", stdout) >= 0 && std::fflush(stdout) == 0); +#endif + return 0; } diff --git a/core/libs/tests/GuestStatvfs.cpp b/core/libs/tests/GuestStatvfs.cpp index a71484b6f8..2019801dc0 100644 --- a/core/libs/tests/GuestStatvfs.cpp +++ b/core/libs/tests/GuestStatvfs.cpp @@ -64,7 +64,12 @@ static std::size_t OpenHandles() { Require(GetProcessHandleCount(GetCurrentProcess(), &count) != 0); return count; #else - return static_cast(std::distance(std::filesystem::directory_iterator("/proc/self/fd"), +#ifdef __APPLE__ + const std::filesystem::path descriptors = "/dev/fd"; +#else + const std::filesystem::path descriptors = "/proc/self/fd"; +#endif + return static_cast(std::distance(std::filesystem::directory_iterator(descriptors), std::filesystem::directory_iterator{})); #endif } @@ -97,6 +102,8 @@ int main() { RemovePathAlias_nid_no_patch("/anyps5-statvfs-unavailable"); Require(statvfs_nid_postfix("/anyps5-statvfs-test/file.txt", nullptr) == -1); Require(*__error_nid_postfix() == 14); + Require(statvfs_nid_postfix("/anyps5-statvfs-test/missing", nullptr) == -1); + Require(*__error_nid_postfix() == 14); const auto unicodeFile = root / std::filesystem::path(u8"\u65e5.txt"); { diff --git a/core/libs/tests/GuestStdioVariadic.cpp b/core/libs/tests/GuestStdioVariadic.cpp new file mode 100644 index 0000000000..62ead8be7f --- /dev/null +++ b/core/libs/tests/GuestStdioVariadic.cpp @@ -0,0 +1,114 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +extern FileStream* __stdinp_nid_postfix; +int APS5_VABI vfscanf_nid_postfix(FileStream*, const char*, void*); +int APS5_VABI vscanf_nid_postfix(const char*, void*); +int APS5_VABI scanf_nid_postfix(const char*, ...); +int APS5_VABI vfwprintf_nid_postfix(FileStream*, const char16_t*, void*); +int APS5_VABI fwprintf_nid_postfix(FileStream*, const char16_t*, ...); +int APS5_VABI vswscanf_nid_postfix(const char16_t*, const char16_t*, void*); +int APS5_VABI vswscanf_s_nid_postfix(const char16_t*, const char16_t*, void*); +} + +#ifdef _WIN32 +#define APS5_VA_BEGIN(last) __builtin_sysv_va_list args; __builtin_sysv_va_start(args, last) +#define APS5_VA_END() __builtin_sysv_va_end(args) +#else +#define APS5_VA_BEGIN(last) std::va_list args; va_start(args, last) +#define APS5_VA_END() va_end(args) +#endif + +static void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", what); + std::abort(); +} + +static int APS5_VABI ScanStream(FileStream* stream, const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfscanf_nid_postfix(stream, format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanInput(const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vscanf_nid_postfix(format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI PrintWide(FileStream* stream, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfwprintf_nid_postfix(stream, format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanWide(const char16_t* input, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vswscanf_nid_postfix(input, format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanWideSized(const char16_t* input, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vswscanf_s_nid_postfix(input, format, args); + APS5_VA_END(); + return result; +} + +static std::string Contents(FileStream& stream) { + std::fflush(stream.GetHandle()); + std::rewind(stream.GetHandle()); + std::string text; + for (int c; (c = std::fgetc(stream.GetHandle())) != EOF;) text.push_back(static_cast(c)); + return text; +} + +int main() { + FileStream scanned(std::tmpfile()); + Require(std::fputs("42 answer 7", scanned.GetHandle()) >= 0, "seed the scanned stream"); + std::rewind(scanned.GetHandle()); + int number = 0; + char word[16]{}; + Require(ScanStream(&scanned, "%d %15s", &number, word) == 2, "vfscanf assigns both conversions"); + Require(number == 42 && std::strcmp(word, "answer") == 0, "vfscanf values"); + Require(ScanStream(&scanned, "%d", &number) == 1 && number == 7, "vfscanf continues from the stream position"); + Require(ScanStream(&scanned, "%d", &number) == EOF && number == 7, "vfscanf at end of file"); + scanned.Close(); + + FileStream input(std::tmpfile()); + Require(std::fputs("-5 12", input.GetHandle()) >= 0, "seed the standard input stream"); + std::rewind(input.GetHandle()); + FileStream* const standardInput = __stdinp_nid_postfix; + __stdinp_nid_postfix = &input; + int first = 0, second = 0; + Require(ScanInput("%d", &first) == 1 && first == -5, "vscanf reads the guest standard input"); + Require(scanf_nid_postfix("%d", &second) == 1 && second == 12, "scanf reads the guest standard input"); + __stdinp_nid_postfix = standardInput; + input.Close(); + + FileStream written(std::tmpfile()); + Require(fwprintf_nid_postfix(&written, u"%d:%ls|", 7, u"été") == 6, "fwprintf counts wide characters"); + Require(PrintWide(&written, u"%5.1f", 2.25) == 5, "vfwprintf counts wide characters"); + Require(Contents(written) == "7:\xc3\xa9t\xc3\xa9| 2.2", "the stream holds the formatted text"); + written.Close(); + + std::int32_t a = 0, b = 0; + Require(ScanWide(u" 31 -4", u"%d%d", &a, &b) == 2 && a == 31 && b == -4, "vswscanf"); + Require(ScanWide(u"", u"%d", &a) == EOF, "vswscanf on empty input"); + char16_t letters[4]{}; + Require(ScanWideSized(u"abcdef", u"%3ls", letters, 4u) == 1 && std::u16string(letters) == u"abc", "vswscanf_s %ls with a size"); + Require(ScanWideSized(u"abcdef", u"%ls", letters, 4u) == 0 && letters[0] == 0, "vswscanf_s rejects a too small buffer"); + return 0; +} diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index a1c5feafc3..a30f57eb0a 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -1,22 +1,29 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include +#include #include #include #include #include extern "C" { +void APS5_VABI bcopy_nid_postfix(const void*, void*, std::size_t); char* APS5_VABI basename_nid_postfix(const char*); +char* APS5_VABI basename_r_nid_postfix(const char*, char*); char* APS5_VABI __inet_ntoa_nid_postfix(unsigned int); char* APS5_VABI dirname_nid_postfix(const char*); int* APS5_VABI __error_nid_postfix(); +long long APS5_VABI strtonum_nid_postfix(const char*, long long, long long, const char**); int APS5_VABI strcasecmp_nid_postfix(const char*, const char*); int APS5_VABI strncasecmp_nid_postfix(const char*, const char*, std::size_t); std::size_t APS5_VABI strnlen_nid_postfix(const char*, std::size_t); std::size_t APS5_VABI strnlen_s_nid_postfix(const char*, std::size_t); char* APS5_VABI strncat_nid_postfix(char*, const char*, std::size_t); char* APS5_VABI strpbrk_nid_postfix(const char*, const char*); +const char* APS5_VABI strchr_nid_postfix(const char*, int); +char* APS5_VABI strrchr_nid_postfix(const char*, int); std::size_t APS5_VABI strcspn_nid_postfix(const char*, const char*); std::size_t APS5_VABI strlcat_nid_postfix(char*, const char*, std::size_t); char* APS5_VABI stpcpy_nid_postfix(char*, const char*); @@ -25,11 +32,14 @@ char* APS5_VABI strtok_nid_postfix(char*, const char*); char* APS5_VABI strsep_nid_postfix(char**, const char*); char* APS5_VABI strcasestr_nid_postfix(const char*, const char*); int APS5_VABI strcpy_s_nid_postfix(char*, std::size_t, const char*); +int APS5_VABI strncpy_s_nid_postfix(char*, std::size_t, const char*, std::size_t); int APS5_VABI strcat_s_nid_postfix(char*, std::size_t, const char*); int APS5_VABI strncat_s_nid_postfix(char*, std::size_t, const char*, std::size_t); int APS5_VABI memcpy_s_nid_postfix(void*, std::size_t, const void*, std::size_t); int APS5_VABI memmove_s_nid_postfix(void*, std::size_t, const void*, std::size_t); int APS5_VABI memset_s_nid_postfix(void*, std::size_t, int, std::size_t); +void* APS5_VABI memccpy_nid_postfix(void*, const void*, int, std::size_t); +void* APS5_VABI memmem_nid_postfix(const void*, std::size_t, const void*, std::size_t); char* APS5_VABI strnstr_nid_postfix(const char*, const char*, std::size_t); std::size_t APS5_VABI strxfrm_nid_postfix(char*, const char*, std::size_t); int APS5_VABI snprintf_s_nid_postfix(char*, std::size_t, const char*, ...); @@ -121,6 +131,13 @@ static void CheckBoundsCheckedFunctions() { Require(memcpy_s_nid_postfix(bytes, 2, source, 4) == 34 && bytes[0] == 0 && bytes[1] == 0 && bytes[2] == 3); char overlap[6] = "abcde"; Require(memmove_s_nid_postfix(overlap + 1, 5, overlap, 3) == 0 && std::strcmp(overlap, "aabce") == 0); + char strOverlap[16] = "abcdef"; + Require(strncpy_s_nid_postfix(strOverlap + 1, 10, strOverlap, 3) == 22 && strOverlap[1] == '\0'); + char catOverlap[16] = "abcdef"; + Require(strncat_s_nid_postfix(catOverlap + 3, 10, catOverlap, 3) == 22 && catOverlap[3] == '\0'); + char nonOverlap[16] = {}; + std::strcpy(nonOverlap + 8, "cd"); + Require(strcpy_s_nid_postfix(nonOverlap, SIZE_MAX >> 1, nonOverlap + 8) == 0 && std::strcmp(nonOverlap, "cd") == 0); Require(memset_s_nid_postfix(bytes, sizeof(bytes), 9, 8) == 22 && bytes[0] == 9 && bytes[3] == 9); char sentinel[4] = {1, 2, 3, 4}; Require(memset_s_nid_postfix(sentinel, sizeof(sentinel), 7, 2) == 0 && sentinel[1] == 7 && sentinel[2] == 3); @@ -135,6 +152,33 @@ static void CheckBoundsCheckedFunctions() { Require(snprintf_s_nid_postfix(formatted, sizeof(formatted), "%d-%s", 42, "x") == 4 && std::strcmp(formatted, "42-x") == 0); } +static void CheckSecureStringSizeLimits() { + constexpr std::size_t RsizeMax = SIZE_MAX >> 1; + const std::array original = {'!', 'a', 'b', '\0', 'x', 'y', 'z', '!'}; + for (const auto size : {RsizeMax + 1, SIZE_MAX}) { + for (const char* source : {"cd", "", static_cast(nullptr)}) { + for (const auto copy : {strcpy_s_nid_postfix, strcat_s_nid_postfix}) { + auto bytes = original; + Require(copy(bytes.data() + 1, size, source) != 0); + Require(bytes == original); + } + for (const auto copy : {strncpy_s_nid_postfix, strncat_s_nid_postfix}) { + for (const auto count : {std::size_t{0}, std::size_t{1}, std::size_t{3}}) { + auto bytes = original; + Require(copy(bytes.data() + 1, size, source, count) != 0); + Require(bytes == original); + } + } + } + } + auto bytes = original; + Require(strcpy_s_nid_postfix(bytes.data() + 1, RsizeMax, "cd") == 0); + Require(bytes[0] == '!' && std::strcmp(bytes.data() + 1, "cd") == 0 && bytes[4] == 'x'); + bytes = original; + Require(strcat_s_nid_postfix(bytes.data() + 1, RsizeMax, "cd") == 0); + Require(bytes[0] == '!' && std::strcmp(bytes.data() + 1, "abcd") == 0 && bytes[6] == 'z'); +} + static void CheckCaseInsensitiveComparison() { Require(strcasecmp_nid_postfix("", "") == 0 && strcasecmp_nid_postfix("AbC", "aBc") == 0); Require(strcasecmp_nid_postfix("A", "b") == 'a' - 'b' && strcasecmp_nid_postfix("b", "A") == 'b' - 'a'); @@ -212,8 +256,88 @@ static bool CheckDirnameLengths() { return passed; } +static void CheckBcopy() { + constexpr std::array original = {0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187}; + for (std::size_t source = 0; source < original.size(); ++source) { + for (std::size_t destination = 0; destination < original.size(); ++destination) { + for (std::size_t count = 0; count <= original.size() - std::max(source, destination); ++count) { + auto actual = original; + auto expected = original; + for (std::size_t i = 0; i < count; ++i) expected[destination + i] = original[source + i]; + bcopy_nid_postfix(actual.data() + source, actual.data() + destination, count); + Require(actual == expected); + } + } + } + auto destination = original; + destination.fill(0xff); + bcopy_nid_postfix(original.data(), destination.data(), original.size()); + Require(destination == original); + bcopy_nid_postfix(nullptr, nullptr, 0); +} + +static void CheckStrtonum() { + struct Valid { + const char* text; + long long value; + }; + for (const auto& test : {Valid{"0", 0}, Valid{"-0", 0}, Valid{" +42", 42}, Valid{"010", 10}, + Valid{"9223372036854775807", LLONG_MAX}, Valid{"-9223372036854775808", LLONG_MIN}}) { + const char* error = "unchanged"; + *__error_nid_postfix() = 13; + Require(strtonum_nid_postfix(test.text, LLONG_MIN, LLONG_MAX, &error) == test.value); + Require(error == nullptr && *__error_nid_postfix() == 13); + } + for (const auto& test : {Valid{"-5", -5}, Valid{"5", 5}, Valid{"0", 0}}) { + const char* error = "unchanged"; + Require(strtonum_nid_postfix(test.text, -5, 5, &error) == test.value && error == nullptr); + Require(strtonum_nid_postfix(test.text, test.value, test.value, &error) == test.value && error == nullptr); + } + const auto fails = [](const char* text, long long minimum, long long maximum, const char* expected, int number) { + const char* error = nullptr; + *__error_nid_postfix() = 13; + Require(strtonum_nid_postfix(text, minimum, maximum, &error) == 0); + Require(error != nullptr && std::strcmp(error, expected) == 0 && *__error_nid_postfix() == number); + *__error_nid_postfix() = 13; + Require(strtonum_nid_postfix(text, minimum, maximum, nullptr) == 0 && *__error_nid_postfix() == number); + }; + for (const char* text : {"", " ", "+", "-", "--1", "0x10", "0b10", "1x", "42 ", "1\n", + "9223372036854775808x", "-9223372036854775809x"}) + fails(text, LLONG_MIN, LLONG_MAX, "invalid", 22); + fails("0", 1, -1, "invalid", 22); + fails(nullptr, 1, -1, "invalid", 22); + fails("9223372036854775808", 1, -1, "invalid", 22); + fails("-6", -5, 5, "too small", 34); + fails("6", -5, 5, "too large", 34); + fails("0", 1, 5, "too small", 34); + fails("0", -5, -1, "too large", 34); + fails("-9223372036854775809", LLONG_MIN, LLONG_MAX, "too small", 34); + fails("9223372036854775808", LLONG_MIN, LLONG_MAX, "too large", 34); + fails("-9999999999999999999999999999999999", LLONG_MIN, LLONG_MAX, "too small", 34); + fails("9999999999999999999999999999999999", LLONG_MIN, LLONG_MAX, "too large", 34); + *__error_nid_postfix() = 34; + Require(strtonum_nid_postfix("42", 0, 100, nullptr) == 42 && *__error_nid_postfix() == 34); +} + +static void CheckCharacterConversionInSearches() { + const char bytes[] = "a\xe9" "b\xe9" "c"; + Require(strchr_nid_postfix(bytes, 0xe9) == bytes + 1); + Require(strchr_nid_postfix(bytes, 0x1e9) == bytes + 1); + Require(strchr_nid_postfix(bytes, -23) == bytes + 1); + Require(strrchr_nid_postfix(bytes, 0xe9) == bytes + 3); + Require(strrchr_nid_postfix(bytes, 0x1e9) == bytes + 3); + Require(strrchr_nid_postfix(bytes, -23) == bytes + 3); + const char path[] = "a/b/c"; + Require(strrchr_nid_postfix(path, 0x12f) == path + 3); + Require(strrchr_nid_postfix(path, 0) == path + 5); + Require(strrchr_nid_postfix(path, 'z') == nullptr); +} + int main() { + CheckStrtonum(); + CheckBcopy(); if (!CheckDirnameLengths()) return 1; + CheckCharacterConversionInSearches(); CheckBoundsCheckedFunctions(); CheckCaseInsensitiveComparison(); CheckSscanfS(); @@ -246,6 +370,15 @@ int main() { Require(std::strcmp(basename_nid_postfix("one\\two"), "one\\two") == 0); const std::string longName(1024, 'x'); Require(basename_nid_postfix(longName.c_str()) == nullptr && *__error_nid_postfix() == 63); + char baseBuffer[1024]; + Require(basename_r_nid_postfix(path, baseBuffer) == baseBuffer && std::strcmp(baseBuffer, "two") == 0); + Require(basename_r_nid_postfix(nullptr, baseBuffer) == baseBuffer && std::strcmp(baseBuffer, ".") == 0); + Require(basename_r_nid_postfix("///", baseBuffer) == baseBuffer && std::strcmp(baseBuffer, "/") == 0); + Require(basename_r_nid_postfix("plain", baseBuffer) == baseBuffer && std::strcmp(baseBuffer, "plain") == 0); + const std::string longest(1023, 'y'); + Require(basename_r_nid_postfix(("/a/" + longest).c_str(), baseBuffer) == baseBuffer && std::strlen(baseBuffer) == 1023); + *__error_nid_postfix() = 0; + Require(basename_r_nid_postfix(longName.c_str(), baseBuffer) == nullptr && *__error_nid_postfix() == 63); const unsigned char loopbackBytes[4] = {127, 0, 0, 1}; unsigned int loopback; std::memcpy(&loopback, loopbackBytes, sizeof(loopback)); @@ -399,6 +532,17 @@ int main() { Require(strcasestr_nid_postfix("", "a") == nullptr); const char highBytes[] = {static_cast(0xff), 'A', 0}; Require(strcasestr_nid_postfix(highBytes, "a") == highBytes + 1); + const char hay[] = "hello world"; + Require(memmem_nid_postfix(hay, 11, "world", 5) == hay + 6); + Require(memmem_nid_postfix(hay, 11, "world", 0) == hay); + Require(memmem_nid_postfix(hay, 5, "world", 5) == nullptr); + Require(memmem_nid_postfix(hay, 11, "xyz", 3) == nullptr); + char dest[16] = {}; + Require(memccpy_nid_postfix(dest, "abc:def", ':', 8) == dest + 4); + Require(std::memcmp(dest, "abc:", 4) == 0); + Require(memccpy_nid_postfix(dest, "abcdef", ':', 6) == nullptr); + Require(memccpy_nid_postfix(dest, "abcdef", ':', 0) == nullptr); CheckSecureCopySizeLimits(); + CheckSecureStringSizeLimits(); return CheckMemcpyOverlap() ? 0 : 1; } diff --git a/core/libs/tests/GuestSysarch.cpp b/core/libs/tests/GuestSysarch.cpp new file mode 100644 index 0000000000..f2c4551138 --- /dev/null +++ b/core/libs/tests/GuestSysarch.cpp @@ -0,0 +1,91 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sysarch(int number, void* args); +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +} + +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 {}; +} + +#if defined(__linux__) && defined(__x86_64__) +static void* APS5_VABI ReadGsBase(void* arg) { + auto* read = static_cast(arg); + if (sysarch(130, read) != 0) *read = 0; + return nullptr; +} + +static void RequireLowHalf(int operation, std::uint64_t base) { + unsigned char bytes[8]; + std::memset(bytes, 0xaa, sizeof(bytes)); + Require(sysarch(operation, bytes) == 0); + std::uint32_t low = 0; + std::memcpy(&low, bytes, sizeof(low)); + Require(low == static_cast(base)); + for (int i = 4; i < 8; ++i) Require(bytes[i] == 0xaa); +} +#endif + +int main() { + Require(Thrown([] { sysarch(129, nullptr); }) == "sysarch: operation 129 not implemented"); + Require(Thrown([] { sysarch(132, nullptr); }) == "sysarch: operation 132 not implemented"); + for (const int operation : {2, 5, 6, 12, 127, 133, 134, 1000, -1}) { + *__error_nid_postfix() = 0; + Require(sysarch(operation, nullptr) == -1 && *__error_nid_postfix() == 22); + } +#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); + RequireLowHalf(7, fs); + RequireLowHalf(9, base); + *__error_nid_postfix() = 0; + Require(sysarch(7, nullptr) == -1 && *__error_nid_postfix() == 14); + *__error_nid_postfix() = 0; + Require(sysarch(9, nullptr) == -1 && *__error_nid_postfix() == 14); + std::uint64_t inherited = 0; + Pthread child = nullptr; + Require(scePthreadCreate(&child, nullptr, ReadGsBase, &inherited, "sysarch_gs") == 0); + Require(scePthreadJoin(child, nullptr) == 0 && inherited == base); + 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"); + Require(Thrown([&base] { sysarch(7, &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..f98548f2ce 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 @@ -18,7 +24,9 @@ int APS5_VABI sceSysmoduleLoadModule(std::uint16_t id); int APS5_VABI sceSysmoduleUnloadModule(std::uint16_t id); int APS5_VABI sceSysmoduleLoadModuleInternal(std::uint32_t id); int APS5_VABI sceSysmoduleUnloadModuleInternal(std::uint32_t id); +int APS5_VABI sceSysmoduleIsLoadedInternal(std::uint32_t id); int APS5_VABI sceSysmoduleLoadModuleInternalWithArg(std::uint32_t id, int argc, void* argv, std::uint64_t unk, int* ret); +int APS5_VABI sceSysmoduleUnloadModuleInternalWithArg(std::uint32_t id, int argc, void* argv, std::uint64_t unk, int* ret); int APS5_VABI sceSysmoduleGetModuleInfoForUnwind(std::uint64_t addr, int flags, ModuleInfoForUnwind* info); int APS5_VABI sceSysmoduleGetModuleHandleInternal(std::uint32_t id, std::int32_t* handle); int APS5_VABI sceKernelDlsym(std::int32_t handle, const char* symbol, void** addr); @@ -46,8 +54,14 @@ constexpr std::uint32_t kInternalFlag = 0x80000000u; constexpr std::uint16_t kAcmModuleId = 0x010b; constexpr std::uint16_t kTrophy2ModuleId = 0x0110; constexpr std::uint16_t kVoiceChatModuleId = 0x0114; +constexpr std::uint16_t kPerfModuleId = 0x0005; +constexpr std::uint16_t kVrSetupDialogModuleId = 0x0130; +constexpr std::uint16_t kProprietaryVoiceChatHelperModuleId = 0x0132; constexpr std::uint16_t kPsmlModuleId = 0x0137; +constexpr std::uint32_t kAgcDriverModuleId = 0x80000080; +constexpr std::uint32_t kNgs2InternalModuleId = 0x80000090; constexpr std::uint32_t kAgcModuleId = 0x80000094; +constexpr std::uint32_t kHdrScopesInternalModuleId = 0x800000bd; #ifdef __linux__ struct Mapping { @@ -103,13 +117,103 @@ 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); + int unloadResult = 0x5a5a5a5a; + Require(ThrowsRuntimeError([internalId, &unloadResult] { sceSysmoduleUnloadModuleInternalWithArg(internalId, 0, nullptr, 0, &unloadResult); })); + Require(unloadResult == 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); for (std::uint32_t id : {0u, kInternalFlag}) { + Require(sceSysmoduleIsLoadedInternal(id) == kInvalidValue); Require(sceSysmoduleLoadModuleInternal(id) == kInvalidValue); Require(sceSysmoduleUnloadModuleInternal(id) == kInvalidValue); int result = 0x5a5a5a5a; @@ -117,8 +221,11 @@ int main() { Require(result == 0x5a5a5a5a); } + Require(ThrowsRuntimeError([] { sceSysmoduleIsLoadedInternal(0x00ffffffu); })); + Require(sceSysmoduleIsLoadedInternal(kFiberModuleId) == kModuleUnloaded); Require(sceSysmoduleIsLoaded(kFiberModuleId) == kModuleUnloaded); Require(sceSysmoduleLoadModuleInternal(kFiberModuleId) == 0); + Require(sceSysmoduleIsLoadedInternal(kFiberModuleId) == 0); Require(sceSysmoduleIsLoaded(kFiberModuleId) == 0); Require(sceSysmoduleLoadModuleInternal(kFiberModuleId) == 0); Require(sceSysmoduleUnloadModuleInternal(kFiberModuleId) == 0); @@ -172,13 +279,68 @@ int main() { Require(sceSysmoduleUnloadModule(kPsmlModuleId) == 0); Require(sceSysmoduleIsLoaded(kPsmlModuleId) == kModuleUnloaded); + Require(sceSysmoduleIsLoaded(kProprietaryVoiceChatHelperModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModule(kProprietaryVoiceChatHelperModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kProprietaryVoiceChatHelperModuleId) == 0); + Require(sceSysmoduleIsLoaded(kProprietaryVoiceChatHelperModuleId) == 0); + Require(sceSysmoduleUnloadModule(kProprietaryVoiceChatHelperModuleId) == 0); + Require(sceSysmoduleIsLoaded(kProprietaryVoiceChatHelperModuleId) == kModuleUnloaded); + + Require(sceSysmoduleIsLoaded(kPerfModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModule(kPerfModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kPerfModuleId) == 0); + Require(sceSysmoduleIsLoaded(kPerfModuleId) == 0); + Require(sceSysmoduleUnloadModule(kPerfModuleId) == 0); + Require(sceSysmoduleIsLoaded(kPerfModuleId) == kModuleUnloaded); + + Require(sceSysmoduleIsLoaded(kVrSetupDialogModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModule(kVrSetupDialogModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kVrSetupDialogModuleId) == 0); + Require(sceSysmoduleIsLoaded(kVrSetupDialogModuleId) == 0); + Require(sceSysmoduleUnloadModule(kVrSetupDialogModuleId) == 0); + Require(sceSysmoduleIsLoaded(kVrSetupDialogModuleId) == kModuleUnloaded); + + Require(sceSysmoduleUnloadModuleInternal(kAgcDriverModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModuleInternal(kAgcDriverModuleId) == 0); + Require(sceSysmoduleLoadModuleInternal(kAgcDriverModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternal(kAgcDriverModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternal(kAgcDriverModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternal(kAgcDriverModuleId) == kModuleUnloaded); + + Require(sceSysmoduleUnloadModuleInternal(kNgs2InternalModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModuleInternal(kNgs2InternalModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternal(kNgs2InternalModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternal(kNgs2InternalModuleId) == kModuleUnloaded); + + Require(sceSysmoduleUnloadModuleInternal(kHdrScopesInternalModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModuleInternal(kHdrScopesInternalModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternal(kHdrScopesInternalModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternal(kHdrScopesInternalModuleId) == kModuleUnloaded); + + int unloadArgResult = -1; + Require(sceSysmoduleUnloadModuleInternalWithArg(0, 0, nullptr, 0, &unloadArgResult) == kInvalidValue); + Require(unloadArgResult == -1); + Require(sceSysmoduleUnloadModuleInternalWithArg(kAgcDriverModuleId, 0, nullptr, 0, &unloadArgResult) == kModuleUnloaded); + Require(unloadArgResult == -1); + int loadArgResult = -1; + Require(sceSysmoduleLoadModuleInternalWithArg(kAgcDriverModuleId, 0, nullptr, 0, &loadArgResult) == 0); + Require(loadArgResult == 0); + Require(sceSysmoduleIsLoadedInternal(kAgcDriverModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternalWithArg(kAgcDriverModuleId, 0, nullptr, 0, &unloadArgResult) == 0); + Require(unloadArgResult == 0); + Require(sceSysmoduleIsLoadedInternal(kAgcDriverModuleId) == kModuleUnloaded); + + Require(sceSysmoduleIsLoadedInternal(kAgcModuleId) == kModuleUnloaded); Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == kModuleUnloaded); Require(sceSysmoduleLoadModuleInternal(kAgcModuleId) == 0); + Require(sceSysmoduleIsLoadedInternal(kAgcModuleId) == 0); Require(sceSysmoduleLoadModuleInternal(kAgcModuleId) == 0); Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == 0); + Require(sceSysmoduleIsLoadedInternal(kAgcModuleId) == 0); Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == 0); + Require(sceSysmoduleIsLoadedInternal(kAgcModuleId) == kModuleUnloaded); 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 8a30bbf37f..c6726534a9 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -1,10 +1,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include #include #include #include #include #include +#include struct SceKernelSystemSwVersion { std::uint64_t size; char versionString[0x1C]; @@ -20,9 +22,17 @@ int APS5_VABI getpagesize_nid_postfix(); int* APS5_VABI __error_nid_postfix(); int APS5_VABI sysctl_nid_postfix(const int*, std::uint32_t, void*, std::size_t*, const void*, std::size_t); int APS5_VABI sysctlbyname_nid_postfix(const char*, void*, std::size_t*, const void*, std::size_t); +int APS5_VABI sysctlnametomib_nid_postfix(const char*, int*, std::size_t*); int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion*); int APS5_VABI getpid_nid_postfix(); +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t); +int APS5_VABI munmap_nid_postfix(void*, std::size_t); std::uint64_t APS5_VABI sceKernelGetTscFrequency(void); +struct GuestRlimitTest { + std::int64_t rlim_cur; + std::int64_t rlim_max; +}; +int APS5_VABI getrlimit_nid_postfix(int resource, GuestRlimitTest* limit); extern char** environ_nid_postfix; } static void Require(bool value) { if (!value) std::abort(); } @@ -51,13 +61,6 @@ static void CheckProcessorCountSysctl() { Require(sysctl_nid_postfix(nullptr, 2, &value, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 14); Require(sysctlbyname_nid_postfix(nullptr, &value, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 14); bool threw = false; - try { - sysctlbyname_nid_postfix("kern.osreldate", &value, &length, nullptr, 0); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw); - threw = false; const int unknown[] = {6, 99}; try { sysctl_nid_postfix(unknown, 2, &value, &length, nullptr, 0); @@ -66,6 +69,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; @@ -122,13 +145,109 @@ static void CheckTscFrequencySysctl() { length = sizeof(value); Require(sysctlbyname_nid_postfix("machdep.tsc_freq", &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); } -static void CheckMachdepCachelineSizeSysctl() { +static bool Untouched(const unsigned char* bytes, std::size_t size) { + for (std::size_t i = 0; i < size; ++i) + if (bytes[i] != 0xaa) return false; + return true; +} +template +static bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} +static void CheckUnansweredSysctl() { + static constexpr const char* names[] = { + "hw.byteorder", + "hw.cacheline_size", + "hw.machine_arch", + "hw.usermem", + "kern.clockrate", + "kern.maxfiles", + "kern.maxfilesperproc", + "kern.maxproc", + "kern.ps_strings", + "kern.sched.name", + "kern.smp.maxcpus", + "kern.stackprot", + "kern.timecounter.hardware", + "machdep.cacheline_size", + }; + unsigned char buffer[64]; + for (const char* name : names) { + std::memset(buffer, 0xaa, sizeof(buffer)); + std::size_t length = sizeof(buffer); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + Require(length == sizeof(buffer) && Untouched(buffer, sizeof(buffer))); + length = 77; + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2 && length == 77); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, buffer, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + Require(Untouched(buffer, sizeof(buffer))); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, nullptr, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + if (std::strcmp(name, "machdep.cacheline_size") == 0) continue; + length = sizeof(buffer); + Require(Throws([&] { sysctlbyname_nid_postfix(name, buffer, &length, buffer, sizeof(buffer)); })); + } + struct Numbered { + const char* name; + int mib[2]; + }; + static constexpr Numbered numbered[] = { + {"hw.byteorder", {6, 4}}, + {"hw.machine_arch", {6, 11}}, + {"kern.clockrate", {1, 12}}, + {"kern.maxfiles", {1, 7}}, + {"kern.maxfilesperproc", {1, 27}}, + {"kern.maxproc", {1, 6}}, + {"kern.ps_strings", {1, 32}}, + }; + for (const auto& entry : numbered) { + int mib[4] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + std::size_t size = 4; + *__error_nid_postfix() = 13; + Require(sysctlnametomib_nid_postfix(entry.name, mib, &size) == 0 && size == 2); + Require(mib[0] == entry.mib[0] && mib[1] == entry.mib[1] && mib[2] == 0x5a5a5a5a); + Require(*__error_nid_postfix() == 13); + std::memset(buffer, 0xaa, sizeof(buffer)); + std::size_t length = sizeof(buffer); + Require(sysctl_nid_postfix(mib, 2, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + Require(length == sizeof(buffer) && Untouched(buffer, sizeof(buffer))); + length = 77; + *__error_nid_postfix() = 13; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2 && length == 77); + *__error_nid_postfix() = 13; + Require(sysctl_nid_postfix(mib, 2, buffer, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 2 && Untouched(buffer, sizeof(buffer))); + *__error_nid_postfix() = 13; + Require(sysctl_nid_postfix(mib, 2, nullptr, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + length = sizeof(buffer); + Require(Throws([&] { sysctl_nid_postfix(mib, 2, buffer, &length, buffer, sizeof(buffer)); })); + } + int mib[4] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + std::size_t size = 0; *__error_nid_postfix() = 13; - int value = 0x5a5a5a5a; - std::size_t length = sizeof(value); - Require(sysctlbyname_nid_postfix("machdep.cacheline_size", &value, &length, nullptr, 0) == -1); - Require(*__error_nid_postfix() == 2); - Require(value == 0x5a5a5a5a); + Require(sysctlnametomib_nid_postfix("kern.maxproc", nullptr, &size) == 0 && size == 2 && *__error_nid_postfix() == 13); + size = 1; + Require(sysctlnametomib_nid_postfix("kern.maxproc", mib, &size) == -1 && *__error_nid_postfix() == 12); + Require(size == 1 && mib[0] == 1 && mib[1] == 0x5a5a5a5a); + mib[0] = 0x5a5a5a5a; + size = 0; + Require(sysctlnametomib_nid_postfix("kern.maxproc", mib, &size) == -1 && *__error_nid_postfix() == 12); + Require(size == 0 && mib[0] == 0x5a5a5a5a); + static constexpr const char* unresolved[] = {"hw.cacheline_size", "hw.usermem", "machdep.cacheline_size"}; + for (const char* name : unresolved) { + int untouched[4] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + size = 4; + *__error_nid_postfix() = 13; + Require(sysctlnametomib_nid_postfix(name, untouched, &size) == -1 && *__error_nid_postfix() == 2); + Require(size == 4 && untouched[0] == 0x5a5a5a5a && untouched[1] == 0x5a5a5a5a && untouched[2] == 0x5a5a5a5a); + } } static void CheckPathnameSysctl() { *__error_nid_postfix() = 13; @@ -176,8 +295,96 @@ static void CheckPathnameSysctl() { length = sizeof(buffer); Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); } +struct SystemString { + int mib[2]; + const char* name; + const char* value; +}; +static const SystemString SystemStrings[] = { + {{1, 1}, "kern.ostype", "FreeBSD"}, + {{1, 2}, "kern.osrelease", "11.4-RELEASE"}, + {{1, 4}, "kern.version", "FreeBSD 11.4-RELEASE\n"}, + {{1, 10}, "kern.hostname", "AnyPS5"}, + {{6, 1}, "hw.machine", "amd64"}, +}; +static void CheckSystemStringSysctl() { + for (const auto& entry : SystemStrings) { + const std::size_t size = std::strlen(entry.value) + 1; + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctl_nid_postfix(entry.mib, 2, nullptr, &length, nullptr, 0) == 0 && length == size); + char buffer[32]; + std::memset(buffer, 0xaa, sizeof(buffer)); + length = sizeof(buffer); + Require(sysctl_nid_postfix(entry.mib, 2, buffer, &length, nullptr, 0) == 0 && length == size); + Require(std::strcmp(buffer, entry.value) == 0 && buffer[size] == static_cast(0xaa)); + Require(*__error_nid_postfix() == 13); + std::memset(buffer, 0xaa, sizeof(buffer)); + length = 2; + Require(sysctl_nid_postfix(entry.mib, 2, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 2 && std::memcmp(buffer, entry.value, 2) == 0 && buffer[2] == static_cast(0xaa)); + length = sizeof(buffer); + Require(sysctl_nid_postfix(entry.mib, 2, buffer, &length, "Prospero", sizeof("Prospero")) == -1 && *__error_nid_postfix() == 1); + const int deeper[] = {entry.mib[0], entry.mib[1], 0}; + length = sizeof(buffer); + Require(sysctl_nid_postfix(deeper, 3, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 20); + Require(length == sizeof(buffer)); + } +} +static void CheckSystemStringNames() { + for (const auto& entry : SystemStrings) { + *__error_nid_postfix() = 13; + char buffer[32]; + std::memset(buffer, 0xaa, sizeof(buffer)); + std::size_t length = sizeof(buffer); + Require(sysctlbyname_nid_postfix(entry.name, buffer, &length, nullptr, 0) == 0 && length == std::strlen(entry.value) + 1); + Require(std::strcmp(buffer, entry.value) == 0 && *__error_nid_postfix() == 13); + Require(sysctlbyname_nid_postfix(entry.name, buffer, &length, "Prospero", sizeof("Prospero")) == -1 && *__error_nid_postfix() == 1); + int mib[3] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + std::size_t size = 3; + Require(sysctlnametomib_nid_postfix(entry.name, mib, &size) == 0 && size == 2); + Require(mib[0] == entry.mib[0] && mib[1] == entry.mib[1] && mib[2] == 0x5a5a5a5a); + } +} +static void CheckOsReleaseDateSysctl() { + const int mib[] = {1, 24}; + constexpr std::int32_t expected = 1104001; + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == sizeof(expected)); + std::int32_t value = 0; + length = sizeof(value); + Require(sysctl_nid_postfix(mib, 2, &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(value)); + Require(*__error_nid_postfix() == 13); + + value = 0; + length = sizeof(value); + Require(sysctlbyname_nid_postfix("kern.osreldate", &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(value)); + Require(*__error_nid_postfix() == 13); + + int namedMib[3] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + std::size_t mibLength = 3; + Require(sysctlnametomib_nid_postfix("kern.osreldate", namedMib, &mibLength) == 0 && mibLength == 2); + Require(namedMib[0] == mib[0] && namedMib[1] == mib[1] && namedMib[2] == 0x5a5a5a5a); + value = 0; + length = sizeof(value); + Require(sysctl_nid_postfix(namedMib, static_cast(mibLength), &value, &length, nullptr, 0) == 0 && value == expected); + + unsigned char truncated[4] = {0xaa, 0xaa, 0xaa, 0xaa}; + length = 2; + Require(sysctlbyname_nid_postfix("kern.osreldate", truncated, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 2 && truncated[0] == static_cast(expected) && truncated[1] == static_cast(expected >> 8) && truncated[2] == 0xaa && truncated[3] == 0xaa); + + length = sizeof(value); + Require(sysctl_nid_postfix(mib, 2, &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); + Require(sysctlbyname_nid_postfix("kern.osreldate", &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); + + const int deeper[] = {1, 24, 0}; + length = sizeof(value); + Require(sysctl_nid_postfix(deeper, 3, &value, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 20); +} static void CheckLeafWithTrailingComponent() { - const int leaves[][2] = {{6, 3}, {6, 2}, {6, 5}, {1, 37}}; + const int leaves[][2] = {{6, 3}, {6, 2}, {6, 5}, {6, 7}, {1, 24}, {1, 37}, {1, 50}}; for (const auto& leaf : leaves) { const int mib[] = {leaf[0], leaf[1], 0}; char buffer[8] = {}; @@ -188,6 +395,49 @@ static void CheckLeafWithTrailingComponent() { Require(length == sizeof(buffer)); } } +static void CheckMeasuredAbsentSysctlMibs() { + static constexpr struct { + int mib[3]; + std::uint32_t length; + } cases[] = { + {{1, 2147482816, 2147482814}, 3}, + {{1, 2147483151}, 2}, + {{1, 2147483182, 2147483181}, 3}, + {{1, 2147482838, 2147482834}, 3}, + }; + for (const auto& entry : cases) { + unsigned char buffer[64]; + std::memset(buffer, 0xaa, sizeof(buffer)); + std::size_t length = sizeof(buffer); + *__error_nid_postfix() = 77; + Require(sysctl_nid_postfix(entry.mib, entry.length, buffer, &length, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2 && length == sizeof(buffer)); + for (const auto byte : buffer) Require(byte == 0xaa); + + length = 77; + *__error_nid_postfix() = 77; + Require(sysctl_nid_postfix(entry.mib, entry.length, nullptr, &length, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2 && length == 77); + + std::memset(buffer, 0xaa, sizeof(buffer)); + *__error_nid_postfix() = 77; + Require(sysctl_nid_postfix(entry.mib, entry.length, buffer, nullptr, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2); + for (const auto byte : buffer) Require(byte == 0xaa); + + *__error_nid_postfix() = 77; + Require(sysctl_nid_postfix(entry.mib, entry.length, nullptr, nullptr, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2); + + bool threw = false; + try { + sysctl_nid_postfix(entry.mib, entry.length, buffer, &length, buffer, sizeof(buffer)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + } +} static void CheckSystemSwVersion() { *__error_nid_postfix() = 13; Require(sceKernelGetSystemSwVersion_nid_postfix(nullptr) == SCE_KERNEL_ERROR_EFAULT && *__error_nid_postfix() == 13); @@ -212,23 +462,50 @@ static void CheckSystemSwVersion() { } } static void CheckPseudoRandomSysctl() { + unsigned char first[128]; + unsigned char second[128]; + const auto untouchedFrom = [&first](std::size_t from) { + for (std::size_t i = from; i < sizeof(first); ++i) + if (first[i] != 0xaa) return false; + return true; + }; *__error_nid_postfix() = 13; - unsigned char first[64] = {}; - unsigned char second[64] = {}; - std::size_t length = sizeof(first); - Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, nullptr, 0) == 0 && length == sizeof(first)); - length = sizeof(second); - Require(sysctlbyname_nid_postfix("kern.rng_pseudo", second, &length, nullptr, 0) == 0 && length == sizeof(second)); - Require(std::memcmp(first, second, sizeof(first)) != 0); + std::memset(first, 0xaa, sizeof(first)); + std::memset(second, 0xaa, sizeof(second)); + std::size_t length = 64; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, nullptr, 0) == 0 && length == 64 && untouchedFrom(64) && !untouchedFrom(0)); + length = 64; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", second, &length, nullptr, 0) == 0 && length == 64); + Require(std::memcmp(first, second, 64) != 0); Require(*__error_nid_postfix() == 13); - Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, first, sizeof(first)) == -1 && *__error_nid_postfix() == 1); - bool threw = false; - try { - sysctlbyname_nid_postfix("kern.rng_pseudo", nullptr, &length, nullptr, 0); - } catch (const std::runtime_error&) { - threw = true; + static constexpr std::size_t larger[] = {65, 128}; + for (std::size_t asked : larger) { + std::memset(first, 0xaa, sizeof(first)); + length = asked; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, nullptr, 0) == 0 && length == 64 && untouchedFrom(64) && !untouchedFrom(0)); } - Require(threw); + Require(*__error_nid_postfix() == 13); + static constexpr std::size_t smaller[] = {0, 1, 63}; + for (std::size_t asked : smaller) { + std::memset(first, 0xaa, sizeof(first)); + length = asked; + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == asked && untouchedFrom(asked)); + if (asked == 63) Require(!untouchedFrom(0)); + } + static constexpr std::size_t presets[] = {0, 1, 64, 100, 4096}; + for (std::size_t preset : presets) { + length = preset; + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", nullptr, &length, nullptr, 0) == 0 && length == 64 && *__error_nid_postfix() == 13); + } + std::memset(first, 0xaa, sizeof(first)); + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 12 && untouchedFrom(0)); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", nullptr, nullptr, nullptr, 0) == 0 && *__error_nid_postfix() == 13); + length = 64; + Require(sysctlbyname_nid_postfix("kern.rng_pseudo", first, &length, first, 64) == -1 && *__error_nid_postfix() == 1); } static void CheckOvercommitSysctl() { *__error_nid_postfix() = 13; @@ -301,25 +578,240 @@ static void CheckArandomSysctl() { Require(sysctl_nid_postfix(mib, 2, first, nullptr, nullptr, 0) == 0); Require(*__error_nid_postfix() == 13); } +struct VmRange { + std::uint64_t start; + std::uint64_t end; + std::int32_t protection; +}; +static std::vector ReadVmmap(const int* mib) { + std::size_t length = 0; + Require(sysctl_nid_postfix(mib, 4, nullptr, &length, nullptr, 0) == 0 && length != 0 && length % 8 == 0); + std::vector buffer(length * 4 / 3); + length = buffer.size(); + Require(sysctl_nid_postfix(mib, 4, buffer.data(), &length, nullptr, 0) == 0 && length != 0 && length <= buffer.size()); + std::vector ranges; + for (std::size_t at = 0; at < length;) { + std::int32_t size = 0; + std::memcpy(&size, buffer.data() + at, sizeof(size)); + Require(size > 136 && size % 8 == 0 && at + static_cast(size) <= length); + Require(std::memchr(buffer.data() + at + 136, 0, static_cast(size) - 136) != nullptr); + VmRange range{}; + std::memcpy(&range.start, buffer.data() + at + 8, sizeof(range.start)); + std::memcpy(&range.end, buffer.data() + at + 16, sizeof(range.end)); + std::memcpy(&range.protection, buffer.data() + at + 56, sizeof(range.protection)); + Require(range.start < range.end && (ranges.empty() || ranges.back().end <= range.start)); + ranges.push_back(range); + at += static_cast(size); + } + return ranges; +} +static void CheckVmmapSysctl() { + const int self = getpid_nid_postfix(); + const int mib[] = {1, 14, 32, self}; + constexpr std::size_t Bytes = 0x10000; + *__error_nid_postfix() = 13; + void* mapped = mmap_nid_postfix(nullptr, Bytes, 3, 0x1002, -1, 0); + Require(mapped != reinterpret_cast(static_cast(-1))); + const auto address = static_cast(reinterpret_cast(mapped)); + auto ranges = ReadVmmap(mib); + const auto covering = std::find_if(ranges.begin(), ranges.end(), [&](const VmRange& range) { return range.start <= address && address + Bytes <= range.end; }); + Require(covering != ranges.end() && (covering->protection & 3) == 3); + Require(munmap_nid_postfix(mapped, Bytes) == 0); + ranges = ReadVmmap(mib); + Require(std::none_of(ranges.begin(), ranges.end(), [&](const VmRange& range) { return range.start < address + Bytes && address < range.end; })); + Require(*__error_nid_postfix() == 13); + + unsigned char truncated[32]; + std::memset(truncated, 0xaa, sizeof(truncated)); + std::size_t length = 16; + Require(sysctl_nid_postfix(mib, 4, truncated, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 16 && truncated[16] == 0xaa); + length = sizeof(truncated); + Require(sysctl_nid_postfix(mib, 4, truncated, &length, truncated, sizeof(truncated)) == -1 && *__error_nid_postfix() == 1); + const int foreign[] = {1, 14, 32, self + 1000}; + Require(sysctl_nid_postfix(foreign, 4, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 3); + const int current[] = {1, 14, 32, -1}; + Require(sysctl_nid_postfix(current, 4, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 3); + Require(sysctl_nid_postfix(mib, 3, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + const int tooLong[] = {1, 14, 32, self, 0}; + Require(sysctl_nid_postfix(tooLong, 5, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); +} +static void CheckPseudoRandomMibSysctl() { + const int mib[] = {1, 50}; + *__error_nid_postfix() = 13; + std::size_t length = 7; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == 64); + Require(*__error_nid_postfix() == 13); + Require(sysctl_nid_postfix(mib, 2, nullptr, nullptr, nullptr, 0) == 0); + unsigned char bytes[80]; + std::memset(bytes, 0xaa, sizeof(bytes)); + length = 64; + Require(sysctl_nid_postfix(mib, 2, bytes, &length, nullptr, 0) == 0 && length == 64); + bool changed = false; + for (std::size_t i = 0; i < 64; ++i) changed = changed || bytes[i] != 0xaa; + Require(changed && bytes[64] == 0xaa && bytes[79] == 0xaa); + unsigned char other[64] = {}; + length = sizeof(other); + Require(sysctl_nid_postfix(mib, 2, other, &length, nullptr, 0) == 0 && length == sizeof(other)); + Require(std::memcmp(bytes, other, sizeof(other)) != 0); + + unsigned char shortBuffer[64]; + std::memset(shortBuffer, 0xaa, sizeof(shortBuffer)); + length = 63; + Require(sysctl_nid_postfix(mib, 2, shortBuffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + bool shortChanged = false; + for (std::size_t i = 0; i < 63; ++i) shortChanged = shortChanged || shortBuffer[i] != 0xaa; + Require(length == 63 && shortChanged && shortBuffer[63] == 0xaa); + std::memset(shortBuffer, 0xaa, sizeof(shortBuffer)); + Require(sysctl_nid_postfix(mib, 2, shortBuffer, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + for (unsigned char byte : shortBuffer) Require(byte == 0xaa); + int value = 0; + length = sizeof(value); + bool threw = false; + try { + sysctl_nid_postfix(mib, 2, &value, &length, &value, sizeof(value)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); +} +static void CheckNameToMib() { + struct Expected { + const char* name; + int mib[3]; + std::size_t length; + }; + 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}, + {"kern.rng_pseudo", {1, 50}, 2}, + {"kern.proc.pathname", {1, 14, 12}, 3}, + }; + *__error_nid_postfix() = 13; + for (const auto& entry : expected) { + std::size_t size = 0; + Require(sysctlnametomib_nid_postfix(entry.name, nullptr, &size) == 0 && size == entry.length); + int mib[5] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + size = 5; + Require(sysctlnametomib_nid_postfix(entry.name, mib, &size) == 0 && size == entry.length); + Require(std::memcmp(mib, entry.mib, entry.length * sizeof(int)) == 0 && mib[entry.length] == 0x5a5a5a5a); + } + Require(*__error_nid_postfix() == 13); + + int mib[4] = {}; + std::size_t size = 4; + Require(sysctlnametomib_nid_postfix("hw.ncpu", mib, &size) == 0 && size == 2); + int processors = 0; + std::size_t length = sizeof(processors); + Require(sysctl_nid_postfix(mib, static_cast(size), &processors, &length, nullptr, 0) == 0); + Require(processors == static_cast(sysconf_nid_postfix(58))); + size = 4; + Require(sysctlnametomib_nid_postfix("hw.realmem", mib, &size) == 0 && size == 2); + std::uint64_t realmem = 0; + length = sizeof(realmem); + Require(sysctl_nid_postfix(mib, static_cast(size), &realmem, &length, nullptr, 0) == 0); + std::uint64_t namedRealmem = 0; + length = sizeof(namedRealmem); + Require(sysctlbyname_nid_postfix("hw.realmem", &namedRealmem, &length, nullptr, 0) == 0 && realmem == namedRealmem); + size = 4; + Require(sysctlnametomib_nid_postfix("kern.proc.pathname", mib, &size) == 0 && size == 3); + mib[3] = -1; + char path[32] = {}; + length = sizeof(path); + Require(sysctl_nid_postfix(mib, 4, path, &length, nullptr, 0) == 0 && std::strcmp(path, "/app0/eboot.bin") == 0); + + int truncated[3] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + size = 1; + Require(sysctlnametomib_nid_postfix("hw.ncpu", truncated, &size) == -1 && *__error_nid_postfix() == 12); + Require(size == 1 && truncated[0] == 6 && truncated[1] == 0x5a5a5a5a); + size = 0; + Require(sysctlnametomib_nid_postfix("hw.ncpu", truncated, &size) == -1 && *__error_nid_postfix() == 12); + Require(size == 0 && truncated[0] == 6); + + int rngMib[3] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + size = 3; + Require(sysctlnametomib_nid_postfix("kern.rng_pseudo", rngMib, &size) == 0 && size == 2); + Require(rngMib[0] == 1 && rngMib[1] == 50 && rngMib[2] == 0x5a5a5a5a); + size = 2; + Require(sysctl_nid_postfix(rngMib, static_cast(size), nullptr, &length, nullptr, 0) == 0 && length == 64); + rngMib[0] = rngMib[1] = rngMib[2] = 0x5a5a5a5a; + size = 1; + Require(sysctlnametomib_nid_postfix("kern.rng_pseudo", rngMib, &size) == -1 && *__error_nid_postfix() == 12); + Require(size == 1 && rngMib[0] == 1 && rngMib[1] == 0x5a5a5a5a); + + size = 4; + Require(sysctlnametomib_nid_postfix("", mib, &size) == -1 && *__error_nid_postfix() == 2); + Require(sysctlnametomib_nid_postfix("machdep.cacheline_size", mib, &size) == -1 && *__error_nid_postfix() == 2); + char tooLong[1025]; + std::memset(tooLong, 'a', sizeof(tooLong) - 1); + tooLong[sizeof(tooLong) - 1] = 0; + Require(sysctlnametomib_nid_postfix(tooLong, mib, &size) == -1 && *__error_nid_postfix() == 63); + tooLong[1023] = 0; + bool threw = false; + try { + sysctlnametomib_nid_postfix(tooLong, mib, &size); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + threw = false; + try { + sysctlnametomib_nid_postfix(nullptr, mib, &size); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + threw = false; + try { + sysctlnametomib_nid_postfix("hw.ncpu", mib, nullptr); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + threw = false; + try { + sysctlnametomib_nid_postfix("kern.cp_times", mib, &size); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); +} int main() { CheckProcessorCountSysctl(); + CheckPageSizeSysctl(); CheckPhysicalMemorySysctl(5, "hw.physmem"); CheckPhysicalMemorySysctl(12, "hw.realmem"); #if defined(__x86_64__) || defined(__i386__) CheckModelSysctl(); #endif CheckTscFrequencySysctl(); - CheckMachdepCachelineSizeSysctl(); + CheckUnansweredSysctl(); CheckPathnameSysctl(); + CheckSystemStringSysctl(); + CheckSystemStringNames(); + CheckOsReleaseDateSysctl(); CheckLeafWithTrailingComponent(); + CheckMeasuredAbsentSysctlMibs(); CheckSystemSwVersion(); CheckPseudoRandomSysctl(); CheckSdkVersionSysctl(); CheckOvercommitSysctl(); CheckArandomSysctl(); + CheckVmmapSysctl(); + CheckPseudoRandomMibSysctl(); + CheckNameToMib(); *__error_nid_postfix() = 13; + Require(sysconf_nid_postfix(1) == 262144); Require(sysconf_nid_postfix(3) == 128); + GuestRlimitTest nofileLimit{}; + Require(getrlimit_nid_postfix(8, &nofileLimit) == 0); + Require(sysconf_nid_postfix(5) == nofileLimit.rlim_cur); Require(sysconf_nid_postfix(47) == 0x4000); + Require(sysconf_nid_postfix(56) == 1024); Require(getpagesize_nid_postfix() == sysconf_nid_postfix(47)); Require(sysconf_nid_postfix(57) > 0); Require(sysconf_nid_postfix(58) > 0); diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 2f712816f2..4cf3ad3931 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libSceSystemService/SystemService.hpp" +#include #include #include #include @@ -17,6 +18,10 @@ #include #include #endif +#ifdef __APPLE__ +#include +#include +#endif extern "C" int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*); extern "C" int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dsoHandle); extern "C" void APS5_VABI _Exit_nid_postfix(int); @@ -26,14 +31,31 @@ using GuestQuickExitCallback = void (APS5_VABI*)(); extern "C" int APS5_VABI at_quick_exit_nid_postfix(GuestQuickExitCallback); extern "C" void APS5_VABI quick_exit_nid_postfix(int); namespace { -bool cleaned = false; -bool finalized = false; -void Cleanup() { cleaned = true; } -void APS5_VABI Finalize(void*) { finalized = true; } -void VerifyExit() { - if (!cleaned || !finalized) std::_Exit(1); - std::puts("Guest shutdown and atexit completed"); +std::atomic awaiting{false}; +void AwaitExit() { + awaiting = true; + awaiting.notify_one(); + try { + LibcAwaitExit_nid_postfix(); + } catch (const ProcessShutdown&) { + std::puts("Guest thread resumed during exit"); + std::fflush(stdout); + std::_Exit(6); + } } +void Cleanup() { + std::thread(AwaitExit).detach(); + awaiting.wait(false); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::puts("Guest shutdown completed"); + std::fflush(stdout); +} +void ExitHandlerRan() { + std::puts("Guest exit handler ran"); + std::fflush(stdout); + std::_Exit(5); +} +void APS5_VABI UnexpectedFinalizer(void*) { ExitHandlerRan(); } int quickOrder = 0; void APS5_VABI QuickSecond() { quickOrder = 1; } void APS5_VABI QuickFirst() { @@ -61,6 +83,12 @@ std::filesystem::path OwnExecutable() { std::wstring executable(32768, L'\0'); executable.resize(GetModuleFileNameW(nullptr, executable.data(), static_cast(executable.size()))); return executable; +#elif defined(__APPLE__) + std::uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + std::vector executable(size); + Require(_NSGetExecutablePath(executable.data(), &size) == 0); + return std::filesystem::canonical(executable.data()); #else return std::filesystem::read_symlink("/proc/self/exe"); #endif @@ -70,10 +98,29 @@ 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 DelayedExitDrain() { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + AppendMarker("drain"); +} +void ExitFlushAfterDrain() { AppendMarker("flush"); } +void ThrowingExitDrain() { throw std::runtime_error("exit drain test failure"); } +void BlockedExitDrain() { + for (;;) std::this_thread::sleep_for(std::chrono::seconds(1)); +} +void APS5_VABI CheckShutdownStillRunning(void*) { + if (LibcShutdownToken_nid_postfix().stop_requested()) std::_Exit(8); + AppendMarker("guest finalizer"); +} 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)); @@ -112,10 +159,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()); @@ -138,9 +202,45 @@ int DriveRelaunch() { std::puts("Guest relaunch completed"); return 0; } + +int DriveExitDrain() { + const auto self = OwnExecutable(); + const auto marker = std::filesystem::temp_directory_path() / ("anyps5-exit-drain-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); + SetVariable(RelaunchMarker, marker.string().c_str()); + Require(RunChild(self, "--exit-drain") == 7); + std::ifstream file(marker); + const std::string text{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + file.close(); + std::filesystem::remove(marker); + Require(text == "drain\nflush\n"); + Require(RunChild(self, "--exit-drain-throws") == 7); + const auto started = std::chrono::steady_clock::now(); + Require(RunChild(self, "--exit-drain-blocked") == 7); + const auto elapsed = std::chrono::steady_clock::now() - started; + Require(elapsed >= std::chrono::seconds(8) && elapsed < std::chrono::seconds(12)); + std::puts("Host exit drain completed before immediate termination and stayed bounded"); + return 0; +} } int main(int argc, char** argv) { - if (argc > 1 && std::strcmp(argv[1], "--immediate") == 0) { + const int firstArgument = argc > 0 && std::strncmp(argv[0], "--", 2) == 0 ? 0 : 1; + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--exit-drain-driver") == 0) return DriveExitDrain(); + if (argc > firstArgument && std::strncmp(argv[firstArgument], "--exit-drain", 12) == 0) { + LibcRegisterShutdown_nid_postfix(UnexpectedCleanup); + Require(std::atexit(UnexpectedCleanup) == 0); + if (std::strcmp(argv[firstArgument], "--exit-drain-throws") == 0) LibcSetExitDrain_nid_no_patch(ThrowingExitDrain); + else if (std::strcmp(argv[firstArgument], "--exit-drain-blocked") == 0) LibcSetExitDrain_nid_no_patch(BlockedExitDrain); + else { + LibcSetExitDrain_nid_no_patch(DelayedExitDrain); + LibcSetEndFlush_nid_no_patch(ExitFlushAfterDrain); + } + _Exit_nid_postfix(7); + } + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--requested-exit-driver") == 0) { + Require(RunChild(OwnExecutable(), "--requested-exit") == 0); + return 0; + } + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--immediate") == 0) { LibcRegisterShutdown_nid_postfix(UnexpectedCleanup); Require(std::atexit(UnexpectedCleanup) == 0); #ifdef _WIN32 @@ -150,54 +250,70 @@ int main(int argc, char** argv) { _Exit_nid_postfix(0); return 2; } - if (argc > 1 && std::strcmp(argv[1], "--return-from-main") == 0) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--return-from-main") == 0) { catchReturnFromMain_nid_postfix(0); return 2; } - if (argc > 1 && std::strcmp(argv[1], "--quick-exit") == 0) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--quick-exit") == 0) { Require(at_quick_exit_nid_postfix(QuickFirst) == 0); Require(at_quick_exit_nid_postfix(QuickSecond) == 0); quick_exit_nid_postfix(0); return 2; } - if (argc > 1 && std::strcmp(argv[1], "--nested-exit-driver") == 0) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--nested-exit-driver") == 0) { return DriveNestedExit(); } - if (argc > 1 && std::strcmp(argv[1], "--nested-exit") == 0) { + 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 > 1 && std::strcmp(argv[1], "--relaunch-driver") == 0) { + 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(); } - if (argc > 1 && std::strcmp(argv[1], "--relaunch") == 0) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--relaunch") == 0) { LibcRegisterShutdown_nid_postfix(MainShutdown); const char* const arguments[] = {"--secondary", "two words", "quote\"d\\", "", nullptr}; sceSystemServiceLoadExec("/app0/picker/picker.bin", arguments); return 2; } - if (argc > 1 && std::strcmp(argv[1], "--secondary") == 0) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--secondary") == 0) { Require(OwnExecutable().filename().string().starts_with("picker.picker.bin")); std::string received = "arguments:"; - for (int index = 1; index < argc; ++index) received += std::string(index == 1 ? " " : "|") + argv[index]; + for (int index = firstArgument; index < argc; ++index) received += std::string(index == firstArgument ? " " : "|") + argv[index]; AppendMarker(received); LibcRegisterShutdown_nid_postfix(SecondaryShutdown); const char* const arguments[] = {"--relaunched", nullptr}; sceSystemServiceLoadExec("/app0/eboot.bin", arguments); return 2; } - if (argc > 1 && std::strcmp(argv[1], "--relaunched") == 0) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--relaunched") == 0) { const char* main = std::getenv(MainExecutable); Require(main != nullptr && std::filesystem::equivalent(OwnExecutable(), main)); AppendMarker("relaunched"); return 0; } - if (argc > 1) { + if (argc > firstArgument) { LibcRegisterShutdown_nid_postfix(Cleanup); - Require(__cxa_atexit_nid_postfix(Finalize, nullptr, nullptr) == 0); - Require(std::atexit(VerifyExit) == 0); + 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; } @@ -206,7 +322,7 @@ int main(int argc, char** argv) { for (const char* path : {"/app0/another.bin", "/data/eboot.bin", "/app0/../eboot.bin", "/app0//eboot.bin"}) { bool rejected = false; try { sceSystemServiceLoadExec(path, nullptr); } - catch (const std::runtime_error&) { rejected = true; } + catch (const std::runtime_error& error) { rejected = std::string(error.what()).find(path) != std::string::npos; } Require(rejected); } #ifdef _WIN32 diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp index ec2f807855..392d3ad044 100644 --- a/core/libs/tests/GuestSystemFont.cpp +++ b/core/libs/tests/GuestSystemFont.cpp @@ -5,11 +5,17 @@ #include #include #include +#include #include #include #include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif extern "C" { int APS5_VABI sceFontMemoryInit(FontMemory*, void*, std::uint32_t, const FontMemoryInterface*, void*, FontMemoryDestroyFunction, void*); @@ -24,7 +30,16 @@ int APS5_VABI sceFontCloseFont(FontHandle); int APS5_VABI sceFontSetScalePixel(FontHandle, float, float); int APS5_VABI sceFontGetCharGlyphMetrics(FontHandle, std::uint32_t, FontGlyphMetrics*); int APS5_VABI sceFontGetHorizontalLayout(FontHandle, FontHorizontalLayout*); +int APS5_VABI sceFontGetKerning(FontHandle, std::uint32_t, std::uint32_t, FontKerning*); +int APS5_VABI sceFontCreateRenderer(const FontMemory*, const void*, FontRenderer*); +int APS5_VABI sceFontDestroyRenderer(FontRenderer*); +int APS5_VABI sceFontBindRenderer(FontHandle, FontRenderer); +int APS5_VABI sceFontUnbindRenderer(FontHandle); +int APS5_VABI sceFontSetupRenderScalePixel(FontHandle, float, float); +void APS5_VABI sceFontRenderSurfaceInit(FontRenderSurface*, void*, int, int, int, int); +int APS5_VABI sceFontRenderCharGlyphImageHorizontal(FontHandle, std::uint32_t, FontRenderSurface*, float, float, FontGlyphMetrics*, FontRenderOutput*); const void* APS5_VABI sceFontSelectLibraryFt(int); +const void* APS5_VABI sceFontSelectRendererFt(int); } static void Check(bool value, int line) { @@ -112,18 +127,76 @@ static void WriteFile(const std::filesystem::path& path, const std::vector +static std::string CaptureStderr(const std::filesystem::path& path, TRun run) { + std::fflush(stderr); +#ifdef _WIN32 + const int saved = _dup(2); +#else + const int saved = dup(2); +#endif + std::FILE* file = std::fopen(path.string().c_str(), "wb"); + Require(saved >= 0 && file != nullptr); +#ifdef _WIN32 + Require(_dup2(_fileno(file), 2) == 0); +#else + Require(dup2(fileno(file), 2) == 2); +#endif + run(); + std::fflush(stderr); +#ifdef _WIN32 + Require(_dup2(saved, 2) == 0); + _close(saved); +#else + Require(dup2(saved, 2) == 2); + close(saved); +#endif + std::fclose(file); + std::ifstream input(path, std::ios::binary); + return std::string(std::istreambuf_iterator(input), std::istreambuf_iterator()); +} + +static std::size_t Occurrences(const std::string& text, const std::string& pattern) { + std::size_t count = 0; + for (auto at = text.find(pattern); at != std::string::npos; at = text.find(pattern, at + pattern.size())) ++count; + return count; +} + static float AdvanceOf(FontHandle font, std::uint32_t code) { FontGlyphMetrics metrics{}; Require(sceFontGetCharGlyphMetrics(font, code, &metrics) == SCE_FONT_OK); return metrics.Horizontal.advance; } +static float SetAdvanceOf(FontLibrary library, std::uint32_t fontSetType, std::uint32_t code) { + FontHandle set = nullptr; + Require(sceFontOpenFontSet(library, fontSetType, 1, nullptr, &set) == SCE_FONT_OK); + Require(sceFontSetScalePixel(set, 100.0f, 100.0f) == SCE_FONT_OK); + const float advance = AdvanceOf(set, code); + Require(sceFontCloseFont(set) == SCE_FONT_OK); + return advance; +} + +static int RenderedPixels(FontHandle font, std::uint32_t code, float& advance) { + std::vector pixels(128 * 128); + FontRenderSurface surface{}; + sceFontRenderSurfaceInit(&surface, pixels.data(), 128, 1, 128, 128); + FontGlyphMetrics metrics{}; + FontRenderOutput result{}; + const int rc = sceFontRenderCharGlyphImageHorizontal(font, code, &surface, 10.0f, 100.0f, &metrics, &result); + advance = metrics.Horizontal.advance; + if (rc != SCE_FONT_OK) return rc; + int lit = 0; + for (const std::uint8_t pixel : pixels) lit += pixel != 0 ? 1 : 0; + return lit; +} + int main() { constexpr std::uint32_t EuropeanLight = 0x180700C3u; constexpr std::uint32_t EuropeanBold = 0x180700C7u; @@ -135,7 +208,7 @@ int main() { const std::filesystem::path root = std::filesystem::temp_directory_path() / ("anyps5_guest_system_font-" + std::to_string(std::random_device{}())); std::filesystem::remove_all(root); const std::filesystem::path empty = root / "empty"; - const std::filesystem::path fonts = root / "fonts"; + const std::filesystem::path fonts = root / std::filesystem::path(u8"fonts-a\u00e7\u00e3o-\u65e5\u672c"); std::filesystem::create_directories(empty); std::filesystem::create_directories(fonts); WriteFile(fonts / "SST-Bold.otf", SquareGlyphFont('A', 700)); @@ -155,8 +228,13 @@ int main() { FontHandle font = nullptr; SetFontDirectory(empty); - Require(sceFontOpenFontSet(library, EuropeanBold, 1, nullptr, &font) == SCE_FONT_ERROR_FONT_OPEN_FAILED && font == nullptr); - Require(sceFontOpenFontSet(library, 0x18070046u, 1, nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FONTSET && font == nullptr); + const std::string warnings = CaptureStderr(root / "stderr.txt", [&] { + Require(sceFontOpenFontSet(library, EuropeanBold, 1, nullptr, &font) == SCE_FONT_ERROR_FONT_OPEN_FAILED && font == nullptr); + Require(sceFontOpenFontSet(library, EuropeanLight, 1, nullptr, &font) == SCE_FONT_ERROR_FONT_OPEN_FAILED && font == nullptr); + Require(sceFontOpenFontSet(library, 0x18070046u, 1, nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FONTSET && font == nullptr); + }); + Require(Occurrences(warnings, "[libSceFont] no file for system font set") == 1); + Require(Occurrences(warnings, "0x180700C7 in " + empty.string()) == 1 && Occurrences(warnings, "ANYPS5_SYSTEM_FONTS") == 1); SetFontDirectory(root / "missing"); bool threw = false; try { @@ -218,7 +296,52 @@ int main() { Require(AdvanceOf(memoryFont, 'A') == 90.0f); Require(sceFontGetHorizontalLayout(memoryFont, &layout) == SCE_FONT_OK); - for (FontHandle handle : {font, instance, substitute, memoryFont}) Require(sceFontCloseFont(handle) == SCE_FONT_OK); + constexpr std::uint32_t EuropeanJpAr = 0x180704C4u; + constexpr std::uint32_t EuropeanJpCjk = 0x1807A444u; + constexpr std::uint32_t EuropeanGbCjk = 0x1807AC44u; + constexpr std::uint32_t European = 0x18070044u; + const auto fallbackFonts = root / "fallback-fonts"; + std::filesystem::create_directories(fallbackFonts); + WriteFile(fallbackFonts / "SST-Roman.otf", SquareGlyphFont('A', 600)); + WriteFile(fallbackFonts / "SSTJpPro-Regular.otf", SquareGlyphFont(0x4E00, 630)); + WriteFile(fallbackFonts / "DFHEI5-SONY.ttf", SquareGlyphFont(0x4E00, 640)); + WriteFile(fallbackFonts / "SCEPS4Yoongd-Medium.otf", SquareGlyphFont(0xAC00, 650)); + WriteFile(fallbackFonts / "NotoSansArabic-Regular.ttf", SquareGlyphFont(0x0627, 660)); + SetFontDirectory(fallbackFonts); + Require(SetAdvanceOf(library, EuropeanJpAr, 'A') == 60.0f); + Require(SetAdvanceOf(library, EuropeanJpAr, 0x4E00) == 63.0f); + Require(SetAdvanceOf(library, EuropeanJpAr, 0x0627) == 66.0f); + Require(SetAdvanceOf(library, EuropeanJpCjk, 0x4E00) == 63.0f); + Require(SetAdvanceOf(library, EuropeanJpCjk, 0xAC00) == 65.0f); + Require(SetAdvanceOf(library, EuropeanGbCjk, 0x4E00) == 64.0f); + WriteFile(fallbackFonts / "NotoSans-Regular.ttf", SquareGlyphFont(0x1EA0, 670)); + Require(SetAdvanceOf(library, 0x18070454u, 0x1EA0) == 67.0f); + FontHandle plain = nullptr; + Require(sceFontOpenFontSet(library, European, 1, nullptr, &plain) == SCE_FONT_OK); + Require(sceFontGetCharGlyphMetrics(plain, 0x4E00, &metrics) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + Require(sceFontCloseFont(plain) == SCE_FONT_OK); + + FontHandle mixed = nullptr; + Require(sceFontOpenFontSet(library, EuropeanJpAr, 1, nullptr, &mixed) == SCE_FONT_OK); + FontHandle mixedInstance = nullptr; + Require(sceFontOpenFontInstance(mixed, nullptr, &mixedInstance) == SCE_FONT_OK); + Require(sceFontSetScalePixel(mixedInstance, 100.0f, 100.0f) == SCE_FONT_OK); + Require(AdvanceOf(mixedInstance, 0x0627) == 66.0f); + FontKerning kerning{1.0f, 1.0f, 1.0f, 1.0f}; + Require(sceFontGetKerning(mixed, 'A', 0x4E00, &kerning) == SCE_FONT_OK && kerning.offsetX == 0.0f); + FontRenderer renderer = nullptr; + Require(sceFontCreateRenderer(&memory, sceFontSelectRendererFt(0), &renderer) == SCE_FONT_OK); + Require(sceFontBindRenderer(mixed, renderer) == SCE_FONT_OK); + Require(sceFontSetupRenderScalePixel(mixed, 100.0f, 100.0f) == SCE_FONT_OK); + float advance = 0.0f; + Require(RenderedPixels(mixed, 'A', advance) > 0 && advance == 60.0f); + Require(RenderedPixels(mixed, 0x4E00, advance) > 0 && advance == 63.0f); + Require(RenderedPixels(mixed, 0x0627, advance) > 0 && advance == 66.0f); + Require(RenderedPixels(mixed, 0xAC00, advance) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + Require(sceFontUnbindRenderer(mixed) == SCE_FONT_OK); + Require(sceFontDestroyRenderer(&renderer) == SCE_FONT_OK); + + for (FontHandle handle : {font, instance, substitute, memoryFont, mixed, mixedInstance}) Require(sceFontCloseFont(handle) == SCE_FONT_OK); Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); Require(allocations == 0); std::filesystem::remove_all(root); diff --git a/core/libs/tests/GuestSystemGesture.cpp b/core/libs/tests/GuestSystemGesture.cpp index 037b9e885a..656f943aca 100644 --- a/core/libs/tests/GuestSystemGesture.cpp +++ b/core/libs/tests/GuestSystemGesture.cpp @@ -1,30 +1,227 @@ +#include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include +#include +#include +#include extern "C" { +int APS5_VABI sceSystemGestureClose(std::int32_t); std::int32_t APS5_VABI sceSystemGestureOpen(std::int32_t, const void*); int APS5_VABI sceSystemGestureGetPrimitiveTouchEvents(std::int32_t, void*, std::uint32_t, std::uint32_t*); int APS5_VABI sceSystemGestureGetPrimitiveTouchEventsCount(std::int32_t); int APS5_VABI sceSystemGestureGetTouchEvents(std::int32_t, const void*, void*, std::uint32_t, std::uint32_t*); int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByIndex(std::int32_t, std::uint32_t, void*); +int APS5_VABI sceSystemGestureGetTouchEventByIndex(std::int32_t, const void*, std::uint32_t, void*); +int APS5_VABI sceSystemGestureGetTouchEventByEventID(std::int32_t, const void*, std::uint32_t, void*); int APS5_VABI sceSystemGestureAppendTouchRecognizer(std::int32_t, void*); +int APS5_VABI sceSystemGestureCreateTouchRecognizer(std::int32_t, void*, std::int32_t, const void*, const void*); +int APS5_VABI sceSystemGestureGetTouchRecognizerInformation(std::int32_t, const void*, void*); int APS5_VABI sceSystemGestureRemoveTouchRecognizer(std::int32_t, void*); int APS5_VABI sceSystemGestureResetTouchRecognizer(std::int32_t, void*); int APS5_VABI sceSystemGestureUpdateTouchRecognizer(std::int32_t, void*); int APS5_VABI sceSystemGestureUpdateTouchRecognizerRectangle(std::int32_t, void*, const void*); int APS5_VABI sceSystemGestureResetPrimitiveTouchRecognizer(std::int32_t); int APS5_VABI sceSystemGestureUpdateAllTouchRecognizer(std::int32_t); -int APS5_VABI sceSystemGestureUpdatePrimitiveTouchRecognizer(std::int32_t, const void*); +int APS5_VABI sceSystemGestureUpdatePrimitiveTouchRecognizer(std::int32_t, const SystemGestureTouchPadData*); +int APS5_VABI sceSystemGestureInitializePrimitiveTouchRecognizer(); +int APS5_VABI sceSystemGestureFinalizePrimitiveTouchRecognizer(void); +int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(std::int32_t, std::uint16_t, void*); } static void Require(bool value) { if (!value) std::abort(); } +static bool SameRectangle(const SystemGestureRectangle& left, const SystemGestureRectangle& right) { + return std::memcmp(&left, &right, sizeof(SystemGestureRectangle)) == 0; +} + +template +static bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +static void TestTouchRecognizerInformation(std::int32_t handle, int invalidArgument, int invalidHandle) { + constexpr std::int32_t gestureType = 2; + const SystemGestureRectangle area{10.0f, 20.0f, 300.0f, 400.0f, {}}; + const SystemGestureRectangle moved{40.0f, 50.0f, 60.0f, 70.0f, {}}; + + SystemGestureTouchRecognizer recognizer{}; + Require(sceSystemGestureCreateTouchRecognizer(handle, &recognizer, gestureType, &area, nullptr) == 0); + + SystemGestureTouchRecognizerInformation information; + std::memset(&information, 0x5a, sizeof(information)); + Require(sceSystemGestureGetTouchRecognizerInformation(handle, &recognizer, &information) == 0); + Require(information.gesture_type == gestureType); + Require(SameRectangle(information.rectangle, area)); + Require(information.updated_time == 0); + Require(std::all_of(std::begin(information.reserve), std::end(information.reserve), [](std::uint8_t value) { return value == 0x5a; })); + + Require(sceSystemGestureUpdateTouchRecognizerRectangle(handle, &recognizer, &moved) == 0); + Require(sceSystemGestureGetTouchRecognizerInformation(handle, &recognizer, &information) == 0); + Require(information.gesture_type == gestureType); + Require(SameRectangle(information.rectangle, moved)); + + Require(sceSystemGestureGetTouchRecognizerInformation(handle + 1, &recognizer, &information) == invalidHandle); + Require(sceSystemGestureGetTouchRecognizerInformation(handle, nullptr, &information) == invalidArgument); + Require(sceSystemGestureGetTouchRecognizerInformation(handle, &recognizer, nullptr) == invalidArgument); + + SystemGestureTouchRecognizer uncreated{}; + Require(Throws([&] { sceSystemGestureGetTouchRecognizerInformation(handle, &uncreated, &information); })); + + SystemGestureTouchRecognizer withoutRectangle{}; + Require(sceSystemGestureCreateTouchRecognizer(handle, &withoutRectangle, gestureType, nullptr, nullptr) == 0); + Require(Throws([&] { sceSystemGestureGetTouchRecognizerInformation(handle, &withoutRectangle, &information); })); + Require(sceSystemGestureUpdateTouchRecognizerRectangle(handle, &withoutRectangle, &area) == 0); + Require(sceSystemGestureGetTouchRecognizerInformation(handle, &withoutRectangle, &information) == 0); + Require(SameRectangle(information.rectangle, area)); +} + +static PadData TouchReport(std::uint64_t timestamp, std::initializer_list> touches) { + PadData report{}; + report.timestamp = timestamp; + for (const auto& touch : touches) { + if (report.touch_data_touch_num == 0) { + report.touch_data_touch0_id = static_cast(touch[0]); + report.touch_data_touch0_x = touch[1]; + report.touch_data_touch0_y = touch[2]; + } else { + report.touch_data_touch1_id = static_cast(touch[0]); + report.touch_data_touch1_x = touch[1]; + report.touch_data_touch1_y = touch[2]; + } + ++report.touch_data_touch_num; + } + return report; +} + +static int UpdateWithReports(std::int32_t handle, const PadData* reports, std::int32_t count) { + SystemGestureTouchPadData input{}; + input.pad_handle = 1; + input.report_number = count; + input.pad_data_buffer = reports; + return sceSystemGestureUpdatePrimitiveTouchRecognizer(handle, &input); +} + +static SystemGesturePrimitiveTouchEvent SingleEvent(std::int32_t handle) { + SystemGesturePrimitiveTouchEvent event; + std::memset(&event, 0x5a, sizeof(event)); + Require(sceSystemGestureGetPrimitiveTouchEventByIndex(handle, 0, &event) == 0); + return event; +} + +static void TestPrimitiveTouchEvents(std::int32_t handle, int invalidArgument, int invalidHandle, int indexOutOfArray) { + Require(sceSystemGestureInitializePrimitiveTouchRecognizer() == 0); + + const PadData pressed = TouchReport(1000, {{7, 480, 471}}); + Require(UpdateWithReports(handle, &pressed, 1) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 1); + SystemGesturePrimitiveTouchEvent event = SingleEvent(handle); + Require(event.event_state == 1); + Require(event.primitive_id == 7); + Require(event.is_updated == 1); + Require(event.pressed_position.x == 480.0f && event.pressed_position.y == 471.0f); + Require(event.current_position.x == 480.0f && event.current_position.y == 471.0f); + Require(event.delta_vector.x == 0.0f && event.delta_vector.y == 0.0f); + Require(event.delta_time == 0 && event.elapsed_time == 0); + + const PadData moved = TouchReport(2000, {{7, 500, 480}}); + Require(UpdateWithReports(handle, &moved, 1) == 0); + event = SingleEvent(handle); + Require(event.event_state == 2); + Require(event.is_updated == 1); + Require(event.pressed_position.x == 480.0f && event.pressed_position.y == 471.0f); + Require(event.current_position.x == 500.0f && event.current_position.y == 480.0f); + Require(event.delta_vector.x == 20.0f && event.delta_vector.y == 9.0f); + Require(event.delta_time == 1000 && event.elapsed_time == 1000); + + const PadData held = TouchReport(3000, {{7, 500, 480}}); + Require(UpdateWithReports(handle, &held, 1) == 0); + event = SingleEvent(handle); + Require(event.event_state == 2); + Require(event.is_updated == 0); + Require(event.delta_vector.x == 0.0f && event.delta_vector.y == 0.0f); + Require(event.delta_time == 1000 && event.elapsed_time == 2000); + + const PadData released = TouchReport(4000, {}); + Require(UpdateWithReports(handle, &released, 1) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 1); + event = SingleEvent(handle); + Require(event.event_state == 3); + Require(event.is_updated == 1); + Require(event.current_position.x == 500.0f && event.current_position.y == 480.0f); + Require(event.delta_time == 1000 && event.elapsed_time == 3000); + + const PadData empty = TouchReport(5000, {}); + Require(UpdateWithReports(handle, &empty, 1) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventByIndex(handle, 0, &event) == indexOutOfArray); + + const PadData pair = TouchReport(6000, {{3, 100, 200}, {4, 300, 400}}); + Require(UpdateWithReports(handle, &pair, 1) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 2); + + SystemGesturePrimitiveTouchEvent byId{}; + Require(sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(handle, 4, &byId) == 0); + Require(byId.primitive_id == 4); + Require(byId.current_position.x == 300.0f && byId.current_position.y == 400.0f); + Require(sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(handle, 4, nullptr) == invalidArgument); + Require(sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(handle + 1, 4, &byId) == invalidHandle); + Require(sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(handle, 9, &byId) == static_cast(0x80890005)); + Require(sceSystemGestureGetPrimitiveTouchEventByIndex(handle + 1, 0, nullptr) == invalidArgument); + + std::array events{}; + std::uint32_t reported = 7; + Require(sceSystemGestureGetPrimitiveTouchEvents(handle, events.data(), 1, &reported) == 0); + Require(reported == 1); + Require(sceSystemGestureGetPrimitiveTouchEvents(handle, events.data(), 0, &reported) == invalidArgument); + Require(events[0].primitive_id == 3); + reported = 7; + Require(sceSystemGestureGetPrimitiveTouchEvents(handle, events.data(), 2, &reported) == 0); + Require(reported == 2 && events[1].primitive_id == 4); + Require(sceSystemGestureGetPrimitiveTouchEvents(handle, nullptr, 2, &reported) == invalidArgument); + Require(sceSystemGestureGetPrimitiveTouchEventByIndex(handle, 2, &event) == indexOutOfArray); + + Require(UpdateWithReports(handle + 1, &pair, 1) == invalidHandle); + Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle, nullptr) == invalidArgument); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 2); + + const std::array sequence{TouchReport(7000, {{3, 110, 200}}), TouchReport(8000, {{3, 130, 210}})}; + Require(UpdateWithReports(handle, sequence.data(), 2) == invalidArgument); + Require(UpdateWithReports(handle, sequence.data(), -1) == invalidArgument); + Require(UpdateWithReports(handle, sequence.data(), 1) == 0); + Require(UpdateWithReports(handle, sequence.data() + 1, 1) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 1); + event = SingleEvent(handle); + Require(event.primitive_id == 3); + Require(event.current_position.x == 130.0f && event.current_position.y == 210.0f); + Require(event.delta_vector.x == 20.0f && event.delta_vector.y == 10.0f); + + Require(sceSystemGestureResetPrimitiveTouchRecognizer(handle) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 0); + + Require(UpdateWithReports(handle, &pressed, 1) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 1); + Require(sceSystemGestureFinalizePrimitiveTouchRecognizer() == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == static_cast(0x80890003)); + Require(sceSystemGestureInitializePrimitiveTouchRecognizer() == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 1); +} + int main() { - constexpr int invalidArgument = static_cast(0x80D10002); - constexpr int invalidHandle = static_cast(0x80D10003); + constexpr int invalidArgument = static_cast(0x80890001); + constexpr int invalidHandle = static_cast(0x80890006); + Require(sceSystemGestureOpen(0, nullptr) == static_cast(0x80890003)); + Require(sceSystemGestureInitializePrimitiveTouchRecognizer() == 0); + Require(sceSystemGestureOpen(1, nullptr) == invalidArgument); const std::int32_t handle = sceSystemGestureOpen(0, nullptr); std::array buffer; @@ -43,15 +240,21 @@ int main() { Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 0); Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle + 1) == invalidHandle); - constexpr int indexOutOfArray = static_cast(0x80D10005); - int recognizer = 0; - int rectangle = 0; + constexpr int indexOutOfArray = static_cast(0x80890004); + SystemGestureTouchRecognizer recognizer{}; + SystemGestureRectangle rectangle{}; count = 7; Require(sceSystemGestureGetTouchEvents(handle, &recognizer, buffer.data(), 4, &count) == 0 && count == 0); Require(sceSystemGestureGetTouchEvents(handle, &recognizer, buffer.data(), 4, nullptr) == invalidArgument); 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(sceSystemGestureGetTouchEventByEventID(handle, &recognizer, 0, buffer.data()) == indexOutOfArray); + Require(sceSystemGestureGetTouchEventByEventID(handle + 1, &recognizer, 0, buffer.data()) == invalidHandle); + Require(sceSystemGestureGetTouchEventByEventID(handle, nullptr, 0, buffer.data()) == invalidArgument); Require(buffer == original); Require(sceSystemGestureAppendTouchRecognizer(handle, &recognizer) == 0); @@ -61,7 +264,7 @@ int main() { Require(sceSystemGestureUpdateTouchRecognizerRectangle(handle, &recognizer, &rectangle) == 0); Require(sceSystemGestureResetPrimitiveTouchRecognizer(handle) == 0); Require(sceSystemGestureUpdateAllTouchRecognizer(handle) == 0); - Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle, nullptr) == 0); + Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle, nullptr) == invalidArgument); Require(sceSystemGestureAppendTouchRecognizer(handle, nullptr) == invalidArgument); Require(sceSystemGestureUpdateTouchRecognizerRectangle(handle, &recognizer, nullptr) == invalidArgument); Require(sceSystemGestureAppendTouchRecognizer(handle + 1, &recognizer) == invalidHandle); @@ -75,6 +278,26 @@ int main() { Require(sceSystemGestureUpdateTouchRecognizerRectangle(handle + 1, &recognizer, &rectangle) == invalidHandle); Require(sceSystemGestureResetPrimitiveTouchRecognizer(handle + 1) == invalidHandle); Require(sceSystemGestureUpdateAllTouchRecognizer(handle + 1) == invalidHandle); - Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle + 1, nullptr) == invalidHandle); + Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle + 1, nullptr) == invalidArgument); + + TestTouchRecognizerInformation(handle, invalidArgument, invalidHandle); + TestPrimitiveTouchEvents(handle, invalidArgument, invalidHandle, indexOutOfArray); + std::array otherHandles{}; + for (auto& otherHandle : otherHandles) { + otherHandle = sceSystemGestureOpen(0, nullptr); + Require(otherHandle > 0 && otherHandle != handle); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(otherHandle) == 0); + } + Require(sceSystemGestureOpen(0, nullptr) == static_cast(0x8089000A)); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 1); + Require(sceSystemGestureClose(otherHandles[0]) == 0); + Require(sceSystemGestureClose(otherHandles[0]) == invalidHandle); + const auto replacement = sceSystemGestureOpen(0, nullptr); + Require(replacement > 0 && replacement != otherHandles[0]); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(otherHandles[0]) == invalidHandle); + Require(sceSystemGestureClose(replacement) == 0); + for (std::size_t index = 1; index < otherHandles.size(); ++index) Require(sceSystemGestureClose(otherHandles[index]) == 0); + Require(sceSystemGestureClose(handle) == 0); + Require(sceSystemGestureFinalizePrimitiveTouchRecognizer() == 0); return 0; } diff --git a/core/libs/tests/GuestSystemKillApp.cpp b/core/libs/tests/GuestSystemKillApp.cpp index 300d6524f6..3ebfe28013 100644 --- a/core/libs/tests/GuestSystemKillApp.cpp +++ b/core/libs/tests/GuestSystemKillApp.cpp @@ -3,9 +3,12 @@ #include #include #include +#include #include extern "C" int APS5_VABI sceSystemServiceGetAppIdOfRunningBigApp(void); extern "C" int APS5_VABI sceSystemServiceKillApp(int, int, int, int); +extern "C" int APS5_VABI sceLncUtilKillApp(unsigned); +extern "C" int APS5_VABI sceLncUtilKillAppWithReason(unsigned, int); namespace { bool cleaned = false; bool finished = false; @@ -27,6 +30,12 @@ bool Rejects(int appId, int how, int reason, int coreDump, const char* expected) catch (const std::runtime_error& error) { return std::strstr(error.what(), expected) != nullptr; } return false; } +template +bool RejectsCall(TCall call, const char* expected) { + try { call(); } + catch (const std::runtime_error& error) { return std::strstr(error.what(), expected) != nullptr; } + return false; +} } int main(int argc, char** argv) { const int appId = sceSystemServiceGetAppIdOfRunningBigApp(); @@ -37,6 +46,13 @@ int main(int argc, char** argv) { sceSystemServiceKillApp(appId, -1, 0, 0); std::_Exit(4); } + if (argc > 1 && (std::strcmp(argv[1], "--lnc-kill") == 0 || std::strcmp(argv[1], "--lnc-kill-reason") == 0)) { + LibcRegisterShutdown_nid_postfix(Cleanup); + Require(std::atexit(VerifyExit) == 0); + if (std::strcmp(argv[1], "--lnc-kill") == 0) sceLncUtilKillApp(static_cast(appId)); + else sceLncUtilKillAppWithReason(static_cast(appId), 0); + std::_Exit(4); + } LibcRegisterShutdown_nid_postfix(UnexpectedShutdown); Require(std::atexit(UnexpectedShutdown) == 0); constexpr char otherApp[] = "sceSystemServiceKillApp: application other than the running title"; @@ -48,5 +64,17 @@ int main(int argc, char** argv) { Require(Rejects(appId, -2, 0, 0, otherArguments)); Require(Rejects(appId, -1, 1, 0, otherArguments)); Require(Rejects(appId, -1, 0, 1, otherArguments)); + constexpr char lncOtherApp[] = "sceLncUtilKillApp: application other than the running title"; + constexpr char reasonOtherApp[] = "sceLncUtilKillAppWithReason: application other than the running title"; + constexpr char otherReason[] = "sceLncUtilKillAppWithReason: a reason other than 0"; + const auto running = static_cast(appId); + for (const unsigned other : {running + 1u, 0u, 0xffffffffu, running | 0x80000000u}) { + Require(RejectsCall([other] { sceLncUtilKillApp(other); }, lncOtherApp)); + Require(RejectsCall([other] { sceLncUtilKillAppWithReason(other, 0); }, reasonOtherApp)); + Require(RejectsCall([other] { sceLncUtilKillAppWithReason(other, 1); }, reasonOtherApp)); + } + for (const int reason : {1, -1, 2, 0x7fffffff}) { + Require(RejectsCall([running, reason] { sceLncUtilKillAppWithReason(running, reason); }, otherReason)); + } finished = true; } diff --git a/core/libs/tests/GuestSystemService.cpp b/core/libs/tests/GuestSystemService.cpp index e4e181e204..9ad6c43d4c 100644 --- a/core/libs/tests/GuestSystemService.cpp +++ b/core/libs/tests/GuestSystemService.cpp @@ -3,11 +3,15 @@ #include #include #include +#include +#include #include extern "C" int APS5_VABI sceSystemServiceGetHdrToneMapLuminance(SystemServiceHdrToneMapLuminance* luminance); extern "C" int APS5_VABI sceSystemServiceParamGetString(int paramId, char* buf, std::size_t bufSize); extern "C" int APS5_VABI sceSystemServiceLaunchApp(const char* titleId, const char* const* arguments, const void* param); +extern "C" int APS5_VABI sceSystemServiceAddLocalProcessForPsmKit(const char* path, const char* const* arguments, const void* param); +extern "C" int APS5_VABI sceSystemServiceKillLocalProcessForPsmKit(int handle); namespace { @@ -15,9 +19,10 @@ constexpr int browserUnavailable = static_cast(0x8002002Du); void Require(bool value) { if (!value) std::abort(); } -bool ParamGetStringThrows(int paramId, char* buf, std::size_t bufSize) { +template +bool Throws(TCall call) { try { - static_cast(sceSystemServiceParamGetString(paramId, buf, bufSize)); + static_cast(call()); } catch (const std::runtime_error&) { return true; } @@ -36,14 +41,46 @@ bool LaunchAppThrows(const char* titleId, const char* const* arguments, const vo } extern "C" int APS5_VABI sceSystemServicePowerTick(void); +extern "C" int APS5_VABI sceSystemServiceIsBgmPlaying(void); +extern "C" int APS5_VABI sceShellCoreUtilIsBgmPlaying(void); extern "C" int APS5_VABI sceSystemServiceReportAbnormalTermination(const void* info); extern "C" int APS5_VABI sceSystemServiceDisableMusicPlayer(void); extern "C" int APS5_VABI sceSystemServiceReenableMusicPlayer(void); extern "C" int APS5_VABI sceSystemServiceDisableMediaPlay(void); extern "C" int APS5_VABI sceSystemServiceReenableMediaPlay(void); -extern "C" int APS5_VABI sceSystemServiceLaunchWebBrowser(const char* uri, void* param); +extern "C" int APS5_VABI sceSystemServiceLaunchWebBrowser(const char* uri); extern "C" int APS5_VABI sceSystemServiceShowControllerSettings(void); +extern "C" int APS5_VABI sceSystemServiceShowDisplaySafeAreaSettings(void); extern "C" int APS5_VABI sceSystemServiceOpenChallengeActivity(void); +extern "C" int APS5_VABI sceSystemServiceGetStatus(SystemServiceStatus* status); +extern "C" int APS5_VABI sceSystemServiceGetDisplaySafeAreaInfo(SystemServiceDisplaySafeAreaInfo* info); +extern "C" int APS5_VABI sceSystemServiceGetNoticeScreenSkipFlag(bool* value); +extern "C" int APS5_VABI sceSystemServiceSetNoticeScreenSkipFlag(void); +extern "C" int APS5_VABI sceSystemServiceDisableNoticeScreenSkipFlagAutoSet(void); +extern "C" int APS5_VABI sceSystemServiceHideSplashScreen(void); +extern "C" int APS5_VABI sceSystemServiceReceiveEvent(SystemServiceEvent* event); +extern "C" void APS5_VABI sceSystemServiceInitializePlayerDialogParam(SystemServicePlayerDialogParam* param); + +namespace { + +void TestPlayerDialogParam() { + alignas(8) unsigned char buffer[0x100]; + std::memset(buffer, 0xab, sizeof(buffer)); + sceSystemServiceInitializePlayerDialogParam(reinterpret_cast(buffer)); + unsigned char expected[0x40] = {0x40}; + std::memset(expected + 0xc, 0xff, 4); + Require(std::memcmp(buffer, expected, sizeof(expected)) == 0); + for (std::size_t i = sizeof(expected); i < sizeof(buffer); ++i) Require(buffer[i] == 0xab); + bool threw = false; + try { + sceSystemServiceInitializePlayerDialogParam(nullptr); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); +} + +} int main() { Require(sceSystemServicePowerTick() == SYSTEM_SERVICE_OK); @@ -54,20 +91,25 @@ int main() { Require(sceSystemServiceDisableMusicPlayer() == SYSTEM_SERVICE_OK); Require(sceSystemServiceDisableMusicPlayer() == SYSTEM_SERVICE_OK); Require(sceSystemServiceReenableMusicPlayer() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceIsBgmPlaying() == 0 && sceShellCoreUtilIsBgmPlaying() == 0); Require(sceSystemServiceReenableMusicPlayer() == SYSTEM_SERVICE_OK); Require(sceSystemServiceDisableMediaPlay() == SYSTEM_SERVICE_OK); Require(sceSystemServiceDisableMediaPlay() == SYSTEM_SERVICE_OK); Require(sceSystemServiceReenableMediaPlay() == SYSTEM_SERVICE_OK); Require(sceSystemServiceReenableMediaPlay() == SYSTEM_SERVICE_OK); Require(sceSystemServiceShowControllerSettings() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceShowDisplaySafeAreaSettings() == SYSTEM_SERVICE_OK); Require(sceSystemServiceOpenChallengeActivity() == SYSTEM_SERVICE_OK); - Require(sceSystemServiceLaunchWebBrowser("http://127.0.0.1:8780/video?v=0", nullptr) == browserUnavailable); - unsigned char browserParam[64]; - std::memset(browserParam, 0x5a, sizeof(browserParam)); - Require(sceSystemServiceLaunchWebBrowser("https://example.com/", browserParam) == browserUnavailable); - for (unsigned char byte : browserParam) Require(byte == 0x5a); - Require(sceSystemServiceLaunchWebBrowser("", nullptr) == browserUnavailable); - Require(sceSystemServiceLaunchWebBrowser(nullptr, nullptr) == browserUnavailable); + Require(sceSystemServiceLaunchWebBrowser("http:") == browserUnavailable); + Require(sceSystemServiceLaunchWebBrowser("https:") == browserUnavailable); + Require(sceSystemServiceLaunchWebBrowser("psno:") == browserUnavailable); + Require(sceSystemServiceLaunchWebBrowser("") == static_cast(0x80a10008)); + Require(sceSystemServiceLaunchWebBrowser("HTTP:") == static_cast(0x80a10008)); + Require(sceSystemServiceLaunchWebBrowser(nullptr) == static_cast(0x80a10003)); + std::string browserUri = "http:" + std::string(6010, 'a'); + Require(sceSystemServiceLaunchWebBrowser(browserUri.c_str()) == browserUnavailable); + browserUri += 'a'; + Require(sceSystemServiceLaunchWebBrowser(browserUri.c_str()) == static_cast(0x80a10007)); Require(LaunchAppThrows("PPSA99109", nullptr, nullptr)); Require(LaunchAppThrows(nullptr, nullptr, nullptr)); const char* launchArguments[] = {"--backup", nullptr}; @@ -76,6 +118,13 @@ int main() { Require(LaunchAppThrows("PPSA99110", launchArguments, launchParam)); for (unsigned char byte : launchParam) Require(byte == 0x5a); Require(std::strcmp(launchArguments[0], "--backup") == 0 && launchArguments[1] == nullptr); + const char* childArguments[] = {"/app0/child.bin", nullptr}; + unsigned char processParam[0x90]; + std::memset(processParam, 0x5a, sizeof(processParam)); + Require(Throws([&] { return sceSystemServiceAddLocalProcessForPsmKit("/app0/child.bin", childArguments, processParam); })); + for (unsigned char byte : processParam) Require(byte == 0x5a); + Require(Throws([] { return sceSystemServiceAddLocalProcessForPsmKit(nullptr, nullptr, nullptr); })); + Require(Throws([] { return sceSystemServiceKillLocalProcessForPsmKit(1); })); Require(sceSystemServiceGetHdrToneMapLuminance(nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); SystemServiceHdrToneMapLuminance luminance{-1.0f, -1.0f, -1.0f}; Require(sceSystemServiceGetHdrToneMapLuminance(&luminance) == SYSTEM_SERVICE_OK); @@ -83,13 +132,49 @@ int main() { Require(luminance.max_tone_map_luminance == 100.0f); Require(luminance.min_tone_map_luminance == 0.0f); - char name[SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH]; + char name[SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH * 2]; + const auto untouched = [&name] { + for (const char byte : name) { + if (byte != 'x') return false; + } + return true; + }; std::memset(name, 'x', sizeof(name)); - Require(sceSystemServiceParamGetString(SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME, nullptr, sizeof(name)) == SYSTEM_SERVICE_ERROR_PARAMETER); - Require(sceSystemServiceParamGetString(SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME, name, 0) == SYSTEM_SERVICE_ERROR_PARAMETER); - Require(ParamGetStringThrows(SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME, name, sizeof(name) - 1)); - Require(ParamGetStringThrows(SYSTEM_SERVICE_PARAM_ID_LANG, name, sizeof(name))); - Require(name[0] == 'x'); - Require(sceSystemServiceParamGetString(SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME, name, sizeof(name)) == SYSTEM_SERVICE_OK); + Require(sceSystemServiceParamGetString(SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME, nullptr, SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH) == SYSTEM_SERVICE_ERROR_PARAMETER); + for (std::size_t size = 0; size < SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH; ++size) { + Require(sceSystemServiceParamGetString(SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME, name, size) == SYSTEM_SERVICE_ERROR_PARAMETER); + } + for (const int paramId : {-1, 0, 1, 5, 7, 40, 100, 1000, 0x7fffffff}) { + Require(sceSystemServiceParamGetString(paramId, name, sizeof(name)) == SYSTEM_SERVICE_ERROR_PARAMETER); + } + Require(sceSystemServiceParamGetString(SYSTEM_SERVICE_PARAM_ID_LANG, name, sizeof(name)) == SYSTEM_SERVICE_ERROR_PARAMETER); + Require(untouched()); + Require(sceSystemServiceParamGetString(SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME, name, SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH + 3) == SYSTEM_SERVICE_OK); Require(std::strcmp(name, "PS5") == 0); + for (std::size_t index = 4; index < SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH + 3; ++index) Require(name[index] == '\0'); + for (std::size_t index = SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH + 3; index < sizeof(name); ++index) Require(name[index] == 'x'); + + Require(sceSystemServiceGetStatus(nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); + SystemServiceStatus status; + std::memset(&status, 0x5a, sizeof(status)); + Require(sceSystemServiceGetStatus(&status) == SYSTEM_SERVICE_OK); + Require(status.event_num == 0 && !status.is_system_ui_overlaid && !status.is_in_background_execution && !status.is_vr_play_area_overlaid); + for (unsigned char byte : status.reserved) Require(byte == 0); + Require(sceSystemServiceGetDisplaySafeAreaInfo(nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); + SystemServiceDisplaySafeAreaInfo safeArea; + std::memset(&safeArea, 0x5a, sizeof(safeArea)); + Require(sceSystemServiceGetDisplaySafeAreaInfo(&safeArea) == SYSTEM_SERVICE_OK); + Require(safeArea.ratio == 1.0f); + for (unsigned char byte : safeArea.reserved) Require(byte == 0); + Require(sceSystemServiceGetNoticeScreenSkipFlag(nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); + bool skip = true; + Require(sceSystemServiceGetNoticeScreenSkipFlag(&skip) == SYSTEM_SERVICE_OK && !skip); + Require(sceSystemServiceSetNoticeScreenSkipFlag() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceDisableNoticeScreenSkipFlagAutoSet() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceHideSplashScreen() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceReceiveEvent(nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); + SystemServiceEvent event; + event.event_type = 7; + Require(sceSystemServiceReceiveEvent(&event) == SYSTEM_SERVICE_ERROR_NO_EVENT); + TestPlayerDialogParam(); } diff --git a/core/libs/tests/GuestSystemServiceLanguage.cpp b/core/libs/tests/GuestSystemServiceLanguage.cpp index efd37f499b..4c4e769974 100644 --- a/core/libs/tests/GuestSystemServiceLanguage.cpp +++ b/core/libs/tests/GuestSystemServiceLanguage.cpp @@ -1,6 +1,7 @@ #include "SceTypes.hpp" #include "prx/libSceSystemService/SystemService.hpp" #include +#include #include #include @@ -45,6 +46,28 @@ bool Rejected(const char* value) { } int main() { + for (int paramId = -1; paramId <= 1001; ++paramId) { + Require(sceSystemServiceParamGetInt(paramId, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); + if ((paramId >= 1 && paramId <= 5) || paramId == SYSTEM_SERVICE_PARAM_ID_100 || paramId == 400) continue; + int value = -12345; + if ((paramId >= 101 && paramId <= 111) || (paramId >= 200 && paramId <= 213) || paramId == 300) { + bool threw = false; + try { + static_cast(sceSystemServiceParamGetInt(paramId, &value)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + } else { + Require(sceSystemServiceParamGetInt(paramId, &value) == SYSTEM_SERVICE_ERROR_PARAMETER); + } + Require(value == -12345); + } + for (int paramId : {std::numeric_limits::min(), std::numeric_limits::max()}) { + int value = -12345; + Require(sceSystemServiceParamGetInt(paramId, &value) == SYSTEM_SERVICE_ERROR_PARAMETER); + Require(value == -12345); + } SetLanguage(nullptr); Require(Language() == SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US); SetLanguage(""); @@ -62,7 +85,14 @@ int main() { SetLanguage("arabic"); Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_LANG, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_400, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); - int enterButton = -1; - Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN, &enterButton) == SYSTEM_SERVICE_OK); - Require(enterButton == SYSTEM_SERVICE_PARAM_ENTER_BUTTON_CROSS); + for (int paramId : {0, SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME, SYSTEM_SERVICE_PARAM_ID_GAME_PARENTAL_LEVEL, 8, 20, 401, 999, + SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN, 1001, 1008}) { + int unknown = -1; + Require(sceSystemServiceParamGetInt(paramId, &unknown) == SYSTEM_SERVICE_ERROR_PARAMETER); + Require(unknown == -1); + Require(sceSystemServiceParamGetInt(paramId, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); + } + int param100 = -1; + Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_100, ¶m100) == SYSTEM_SERVICE_OK); + Require(param100 == 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/GuestTcp.cpp b/core/libs/tests/GuestTcp.cpp index 2b211538ed..11fe4a9366 100644 --- a/core/libs/tests/GuestTcp.cpp +++ b/core/libs/tests/GuestTcp.cpp @@ -17,6 +17,7 @@ int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); int APS5_VABI getpeername_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI shutdown_nid_postfix(int, int); std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); @@ -137,10 +138,87 @@ static void UnboundSocketNameIsZero() { } } +static void WaitAllReceivesTheWholeRequest() { + constexpr int WaitAll = 0x40; + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0 && listen_nid_postfix(listener, 4) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0); + const int client = socket_nid_postfix(2, 1, 0); + Require(connect_nid_postfix(client, address.data(), address.size()) == 0); + const int accepted = accept_nid_postfix(listener, nullptr, nullptr); + Require(accepted >= 0); + + Require(send_nid_postfix(client, "abcd", 4, 0) == 4); + std::thread sender([client] { + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + Require(send_nid_postfix(client, "efgh", 4, 0) == 4); + }); + char received[8]{}; + Require(recv_nid_postfix(accepted, received, sizeof(received), WaitAll) == 8); + sender.join(); + Require(std::memcmp(received, "abcdefgh", 8) == 0); + + Require(send_nid_postfix(client, "xy", 2, 0) == 2 && shutdown_nid_postfix(client, 1) == 0); + char tail[8]{}; + Require(recvfrom_nid_postfix(accepted, tail, sizeof(tail), WaitAll, nullptr, nullptr) == 2); + Require(std::memcmp(tail, "xy", 2) == 0); + + const int datagram = socket_nid_postfix(2, 2, 0); + Require(datagram >= 0); + std::array local{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(datagram, local.data(), local.size()) == 0); + size = local.size(); + Require(getsockname_nid_postfix(datagram, local.data(), &size) == 0); + Require(sendto_nid_postfix(datagram, "dgram", 5, 0, local.data(), local.size()) == 5); + char record[16]{}; + Require(recv_nid_postfix(datagram, record, sizeof(record), WaitAll) == 5 && std::memcmp(record, "dgram", 5) == 0); + for (const int descriptor : {datagram, client, accepted, listener}) Require(close_nid_postfix(descriptor) == 0); +} + +static void ReceiveAnsweredWithoutTheHost() { + constexpr int DontWait = 0x80; + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0 && listen_nid_postfix(listener, 4) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0); + const int unconnected = socket_nid_postfix(2, 1, 0); + char byte; + Require(recv_nid_postfix(unconnected, &byte, 0, DontWait) == -1 && *__error_nid_postfix() == 57); + const int client = socket_nid_postfix(2, 1, 0); + Require(connect_nid_postfix(client, address.data(), address.size()) == 0); + const int accepted = accept_nid_postfix(listener, nullptr, nullptr); + Require(accepted >= 0); + + Require(recv_nid_postfix(accepted, &byte, 0, DontWait) == 0); + Require(recv_nid_postfix(accepted, &byte, 0, 0) == 0); + std::array sender; + sender.fill(0xaa); + std::uint32_t senderSize = sender.size(); + Require(recvfrom_nid_postfix(accepted, &byte, 0, 0, sender.data(), &senderSize) == 0 && senderSize == 0); + +#ifdef _WIN32 + Require(shutdown_nid_postfix(accepted, 0) == 0); + Require(send_nid_postfix(client, "x", 1, 0) == 1); + char received[4]; + Require(recv_nid_postfix(accepted, received, sizeof(received), 0) == 0); + Require(recv_nid_postfix(accepted, received, sizeof(received), DontWait) == 0); + senderSize = sender.size(); + Require(recvfrom_nid_postfix(accepted, received, sizeof(received), 0, sender.data(), &senderSize) == 0 && senderSize == 0); +#endif + for (const int descriptor : {unconnected, client, accepted, listener}) Require(close_nid_postfix(descriptor) == 0); +} + int main() { StreamReceiveReportsNoSender(); ListenersShareAPortAcrossFamilies(); AcceptedSocketsFollowListenerBlocking(); + WaitAllReceivesTheWholeRequest(); + ReceiveAnsweredWithoutTheHost(); UnboundSocketNameIsZero(); const int listener = socket_nid_postfix(2, 1, 6); Require(listener >= 0); @@ -199,11 +277,9 @@ int main() { Require(send_nid_postfix(client, request, sizeof(request), 0x20000) == -1 && *__error_nid_postfix() == 32); Require(close_nid_postfix(accepted) == 0); -#ifndef _WIN32 std::int64_t sent = 0; for (int i = 0; i < 100 && sent >= 0; ++i) sent = send_nid_postfix(client, request, sizeof(request), 0x20000); Require(sent == -1 && *__error_nid_postfix() == 32); -#endif Require(close_nid_postfix(client) == 0); Require(close_nid_postfix(listener) == 0); Require(close_nid_postfix(listener) == -1 && *__error_nid_postfix() == 9); diff --git a/core/libs/tests/GuestTermiosSpeed.cpp b/core/libs/tests/GuestTermiosSpeed.cpp new file mode 100644 index 0000000000..cc0a4c76b4 --- /dev/null +++ b/core/libs/tests/GuestTermiosSpeed.cpp @@ -0,0 +1,101 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +extern "C" int* APS5_VABI __error_nid_postfix(); + +namespace { + +using SetSpeed = int (APS5_VABI*)(void*, std::uint32_t); + +void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "cfsetspeed: %s\n", message); + std::abort(); + } +} + +std::uint32_t Read(const unsigned char* data) { + std::uint32_t result; + std::memcpy(&result, data, sizeof(result)); + return result; +} + +void Check(SetSpeed setSpeed, unsigned char* attributes, std::uint32_t speed) { + std::array before; + std::memcpy(before.data(), attributes, before.size()); + *__error_nid_postfix() = 123; + Require(setSpeed(attributes, speed) == 0, "valid call failed"); + Require(*__error_nid_postfix() == 123, "successful call changed errno"); + Require(Read(attributes + 36) == speed, "incorrect input speed"); + Require(Read(attributes + 40) == speed, "incorrect output speed"); + Require(std::memcmp(before.data(), attributes, 36) == 0, "flags or control characters changed"); +} + +} + +int main(int argc, char** argv) { + Require(argc == 2, "expected the kernel library path"); +#ifdef _WIN32 + const auto library = LoadLibraryA(argv[1]); + Require(library != nullptr, "could not load libkernel"); + const auto setSpeed = reinterpret_cast(GetProcAddress(library, "cfsetspeed_nid_postfix")); + SYSTEM_INFO info{}; + GetSystemInfo(&info); + const std::size_t page = info.dwPageSize; + auto* memory = static_cast(VirtualAlloc(nullptr, 2 * page, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(memory != nullptr, "could not allocate boundary fixture"); + DWORD previous = 0; + Require(VirtualProtect(memory + page, page, PAGE_NOACCESS, &previous) != 0, "could not protect boundary fixture"); +#else + const auto library = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); + Require(library != nullptr, "could not load libkernel"); + const auto setSpeed = reinterpret_cast(dlsym(library, "cfsetspeed_nid_postfix")); + const long pageSize = sysconf(_SC_PAGESIZE); + Require(pageSize > 0, "could not query page size"); + const auto page = static_cast(pageSize); + auto* memory = static_cast(mmap(nullptr, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(memory != MAP_FAILED, "could not allocate boundary fixture"); + Require(mprotect(memory + page, page, PROT_NONE) == 0, "could not protect boundary fixture"); +#endif + Require(setSpeed != nullptr, "export is missing"); + for (const auto speed : {0u, 1u, 50u, 9600u, 115200u, 0x12345678u, 0x80000000u, 0xffffffffu}) { + alignas(8) std::array bytes; + bytes.fill(0xa5); + Check(setSpeed, bytes.data() + 8, speed); + for (std::size_t i = 0; i < 8; ++i) { + Require(bytes[i] == 0xa5 && bytes[52 + i] == 0xa5, "write escaped the termios object"); + } + auto* boundary = memory + page - 44; + std::memset(boundary - 8, 0x5a, 52); + Check(setSpeed, boundary, speed); + for (std::size_t i = 0; i < 8; ++i) Require(boundary[-8 + static_cast(i)] == 0x5a, "write preceded the termios object"); + } + bool threw = false; + try { + static_cast(setSpeed(nullptr, 9600)); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw, "null pointer must be rejected explicitly"); +#ifdef _WIN32 + Require(VirtualFree(memory, 0, MEM_RELEASE) != 0, "could not free boundary fixture"); + FreeLibrary(library); +#else + Require(munmap(memory, 2 * page) == 0, "could not free boundary fixture"); + dlclose(library); +#endif + return 0; +} diff --git a/core/libs/tests/GuestTextToSpeech2.cpp b/core/libs/tests/GuestTextToSpeech2.cpp index 9bac0096e0..93de9dc755 100644 --- a/core/libs/tests/GuestTextToSpeech2.cpp +++ b/core/libs/tests/GuestTextToSpeech2.cpp @@ -6,26 +6,26 @@ extern "C" { std::int32_t APS5_VABI sceTextToSpeech2Initialize(const void* param); -int APS5_VABI sceTextToSpeech2Open(); +int APS5_VABI sceTextToSpeech2Open(const void* parameter); int APS5_VABI sceTextToSpeech2Cancel(); int APS5_VABI sceTextToSpeech2Close(); -int APS5_VABI sceTextToSpeech2GetSpeechStatus(); -int APS5_VABI sceTextToSpeech2Speak(); +int APS5_VABI sceTextToSpeech2GetSpeechStatus(std::int32_t* status); +int APS5_VABI sceTextToSpeech2Speak(const void* parameter); int APS5_VABI sceTextToSpeech2Terminate(); -int APS5_VABI sceTextToSpeech2GetSystemStatus(); +int APS5_VABI sceTextToSpeech2GetSystemStatus(std::int32_t* status); } int main() { constexpr std::int32_t unsupported = static_cast(0x8002002D); - const std::uint32_t param[12]{0x2000000, 0x26c}; + const std::uint32_t param[16]{0x2000000, 0x26c}; if (sceTextToSpeech2Initialize(param) != unsupported) { std::puts("TextToSpeech2: initialization did not report the unsupported operation"); std::abort(); } - if (sceTextToSpeech2Open() != unsupported || sceTextToSpeech2Cancel() != unsupported || - sceTextToSpeech2Close() != unsupported || sceTextToSpeech2GetSpeechStatus() != unsupported || - sceTextToSpeech2Speak() != unsupported || sceTextToSpeech2Terminate() != unsupported || - sceTextToSpeech2GetSystemStatus() != unsupported) { + if (sceTextToSpeech2Open(nullptr) != unsupported || sceTextToSpeech2Cancel() != unsupported || + sceTextToSpeech2Close() != unsupported || sceTextToSpeech2GetSpeechStatus(nullptr) != unsupported || + sceTextToSpeech2Speak(nullptr) != unsupported || sceTextToSpeech2Terminate() != unsupported || + sceTextToSpeech2GetSystemStatus(nullptr) != unsupported) { std::puts("TextToSpeech2: later calls did not report the unsupported operation"); std::abort(); } diff --git a/core/libs/tests/GuestThermalSensors.cpp b/core/libs/tests/GuestThermalSensors.cpp index 4694194aeb..3c051911a6 100644 --- a/core/libs/tests/GuestThermalSensors.cpp +++ b/core/libs/tests/GuestThermalSensors.cpp @@ -1,16 +1,19 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include #include #include #include #include #include +#include #include extern "C" { void ReadThermalSensors_nid_no_patch(const char* root, int celsius[3]); int APS5_VABI sceKernelGetCpuTemperature(int* outCelsius); int APS5_VABI sceKernelGetSocSensorTemperature(int sensorId, int* outCelsius); +int APS5_VABI sceKernelGetSocPowerConsumption(void* consumption); } static void Require(bool value) { @@ -70,6 +73,15 @@ 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]; + std::fill(std::begin(consumption), std::end(consumption), 0xa5a5a5a5u); + Require(sceKernelGetSocPowerConsumption(consumption) == SCE_KERNEL_ERROR_EINVAL); + Require(std::all_of(std::begin(consumption), std::end(consumption), [](std::uint32_t word) { return word == 0xa5a5a5a5u; })); std::filesystem::remove_all(root); return 0; diff --git a/core/libs/tests/GuestThreadSelf.cpp b/core/libs/tests/GuestThreadSelf.cpp index 9c59471e91..87e38f6c0a 100644 --- a/core/libs/tests/GuestThreadSelf.cpp +++ b/core/libs/tests/GuestThreadSelf.cpp @@ -2,6 +2,12 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); @@ -31,6 +37,8 @@ int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr); int APS5_VABI scePthreadAttrGetstackaddr(const PthreadAttr* attr, void** stack_addr); int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, size_t* stack_size); +int APS5_VABI mprotect_nid_postfix(void* address, std::size_t length, int protection) noexcept; +int APS5_VABI sceKernelIsStack(void* addr, void** start, void** end); } static bool StackContains(Pthread thread, const void* address) { @@ -58,6 +66,72 @@ static constexpr std::intptr_t WorkerRetval = 0x1234; static void Require(bool value) { if (!value) std::abort(); } +#ifdef _WIN32 +static constexpr std::uintptr_t GuestPage = 0x4000; +static constexpr int GuestProtNone = 0; +static constexpr int GuestProtReadWrite = 3; + +static void ReportedStack(Pthread thread, std::uintptr_t* begin, std::uintptr_t* end) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK && scePthreadAttrGet(thread, &attr) == SCE_OK); + void* stack = nullptr; + size_t size = 0; + Require(scePthreadAttrGetstackaddr(&attr, &stack) == SCE_OK && scePthreadAttrGetstacksize(&attr, &size) == SCE_OK); + scePthreadAttrDestroy(&attr); + *begin = reinterpret_cast(stack); + *end = *begin + size; +} + +static void RequireGuardable(std::uintptr_t begin, std::uintptr_t end) { + Require(begin % GuestPage == 0 && end - begin >= GuestPage); + for (auto cursor = begin; cursor < end;) { + MEMORY_BASIC_INFORMATION memory{}; + Require(VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) == sizeof(memory)); + Require(memory.State == MEM_COMMIT && memory.Protect == PAGE_READWRITE); + cursor = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; + } + auto* lowest = reinterpret_cast(begin); + Require(mprotect_nid_postfix(lowest, GuestPage, GuestProtReadWrite) == 0); + Require(mprotect_nid_postfix(lowest, GuestPage, GuestProtNone) == 0); + Require(mprotect_nid_postfix(lowest, GuestPage, GuestProtReadWrite) == 0); +} + +static void RequireGuardableStack(Pthread thread) { + std::uintptr_t begin = 0; + std::uintptr_t end = 0; + ReportedStack(thread, &begin, &end); + RequireGuardable(begin, end); +} + +static DWORD WINAPI DefaultSizeHostThread(void*) { + int local = 0; + void* start = nullptr; + void* end = nullptr; + Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); + RequireGuardable(reinterpret_cast(start), reinterpret_cast(end)); + RequireGuardableStack(scePthreadSelf()); + return 0; +} + +static DWORD WINAPI PartlyCommittedHostThread(void*) { + ULONG_PTR low = 0; + ULONG_PTR high = 0; + GetCurrentThreadStackLimits(&low, &high); + std::uintptr_t begin = 0; + std::uintptr_t end = 0; + ReportedStack(scePthreadSelf(), &begin, &end); + Require(begin == low && end == high); + return 0; +} + +static void RunHostThread(LPTHREAD_START_ROUTINE routine, SIZE_T committed) { + const HANDLE thread = CreateThread(nullptr, committed, routine, nullptr, 0, nullptr); + Require(thread != nullptr); + Require(WaitForSingleObject(thread, INFINITE) == WAIT_OBJECT_0); + Require(CloseHandle(thread) != 0); +} +#endif + static std::int64_t ThreadIdField(Pthread thread) { std::int64_t tid = 0; std::memcpy(&tid, static_cast(thread), sizeof(tid)); @@ -110,7 +184,13 @@ 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); +#ifdef _WIN32 + RequireGuardableStack(mainSelf); + RunHostThread(DefaultSizeHostThread, 0); + RunHostThread(PartlyCommittedHostThread, 0x10000); +#endif + 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/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index edbd4b1f83..7b11ebe2ef 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include extern "C" { @@ -24,8 +26,11 @@ std::int64_t APS5_VABI mktime_nid_postfix(GuestTm*); std::size_t APS5_VABI strftime_nid_postfix(char*, std::size_t, const char*, const GuestTm*); void APS5_VABI tzset_nid_postfix(void); extern char* tzname_nid_postfix[2]; +extern std::int64_t timezone_nid_postfix; +extern int daylight_nid_postfix; char* APS5_VABI ctime_nid_postfix(const std::int64_t*); int* APS5_VABI __error_nid_postfix(); +int APS5_VABI setenv_nid_postfix(const char*, const char*, int); double APS5_VABI libc_difftime_nid_postfix(std::int64_t, std::int64_t); double APS5_VABI difftime_nid_postfix(std::int64_t, std::int64_t); } @@ -57,7 +62,7 @@ static void CheckConcurrent(Converter convert, BufferedConverter buffered, const for (std::size_t index = 0; index < workers.size(); ++index) { workers[index] = std::thread([&, index] { start.arrive_and_wait(); - for (int iteration = 0; iteration < 50000; ++iteration) { + for (int iteration = 0; iteration < 10000; ++iteration) { const auto* result = convert(&timers[index]); if (result == nullptr || !Equal(*result, expected[index])) { matches = false; @@ -297,14 +302,131 @@ static void CheckLocalRange() { Require(localtime_s_nid_postfix(&overflow, &scratch) == nullptr, "Unrepresentable negative local time was accepted"); } -int main() { -#ifdef _WIN32 - _putenv_s("TZ", "UTC-2"); - _tzset(); -#else - setenv("TZ", "UTC-2", 1); - tzset(); +#ifndef _WIN32 +static void CheckRegionZones() { + constexpr std::array zones{"America/Los_Angeles", "Europe/Berlin", "Asia/Kolkata", "America/Argentina/Buenos_Aires"}; + constexpr std::array timers{-62135553600, -12609691200, -5016340800, -5000011200, -2732356800, -2209032000, -2208988800, -2208945600, -631108800}; + for (const char* zone : zones) { + Require(setenv_nid_postfix("TZ", zone, 1) == 0, "Could not set the region time zone"); + tzset_nid_postfix(); + for (const std::int64_t timer : timers) { + GuestTm local{}; + Require(localtime_s_nid_postfix(&timer, &local) == &local, "Local conversion in a region zone failed"); + GuestTm normalized = local; + normalized.tm_wday = -1; + normalized.tm_yday = -1; + if (mktime_nid_postfix(&normalized) != timer || !Equal(normalized, local)) { + std::fprintf(stderr, "mktime did not invert localtime in %s at %lld (offset %ld): got %lld\n", zone, static_cast(timer), static_cast(local.tm_gmtoff), static_cast(mktime_nid_postfix(&local))); + std::abort(); + } + GuestTm unknown = local; + unknown.tm_isdst = -1; + Require(mktime_nid_postfix(&unknown) == timer && unknown.tm_isdst == local.tm_isdst, "mktime with an unknown daylight flag did not invert localtime in a region zone"); + } + } + Require(setenv_nid_postfix("TZ", "UTC-2", 1) == 0, "Could not restore the guest time zone"); + tzset_nid_postfix(); +} #endif + +static void CheckGuestZone() { + const std::int64_t moment = 172800; + GuestTm result{}; + Require(setenv_nid_postfix("TZ", "UTC-5", 1) == 0, "Could not set the guest time zone"); + Require(localtime_s_nid_postfix(&moment, &result) == &result && result.tm_hour == 5 && result.tm_gmtoff == 18000, + "localtime ignored a guest TZ change made without tzset"); + GuestTm calendar{}; + calendar.tm_year = 70; + calendar.tm_mday = 3; + calendar.tm_hour = 5; + calendar.tm_isdst = -1; + Require(mktime_nid_postfix(&calendar) == moment, "mktime ignored the guest TZ"); + Require(setenv_nid_postfix("TZ", "UTC+3", 1) == 0, "Could not change the guest time zone"); + tzset_nid_postfix(); + Require(localtime_s_nid_postfix(&moment, &result) == &result && result.tm_hour == 21 && result.tm_mday == 2 && result.tm_gmtoff == -10800, + "tzset ignored the guest TZ"); + Require(timezone_nid_postfix == 10800 && daylight_nid_postfix == 1, "tzset did not set timezone and daylight for UTC+3"); + Require(setenv_nid_postfix("TZ", "", 1) == 0, "Could not empty the guest time zone"); + tzset_nid_postfix(); + Require(localtime_s_nid_postfix(&moment, &result) == &result && result.tm_hour == 0 && result.tm_mday == 3 && result.tm_gmtoff == 0, + "An empty guest TZ was not treated as UTC"); + Require(timezone_nid_postfix == 0 && daylight_nid_postfix == 1, "tzset did not set timezone and daylight for UTC"); + Require(setenv_nid_postfix("TZ", "EST5EDT", 1) == 0, "Could not set a daylight saving guest time zone"); + tzset_nid_postfix(); + Require(timezone_nid_postfix == 18000 && daylight_nid_postfix == 1, "timezone is not the standard offset of a daylight saving zone"); + Require(setenv_nid_postfix("TZ", "UTC-2", 1) == 0, "Could not restore the guest time zone"); + tzset_nid_postfix(); + Require(setenv_nid_postfix("TZ", "EST5", 1) == 0, "Could not set the guest time zone without tzset"); + Require(localtime_s_nid_postfix(&moment, &result) == &result && result.tm_hour == 19 && result.tm_gmtoff == -18000, + "localtime ignored a guest TZ change made without tzset"); + Require(std::strcmp(tzname_nid_postfix[0], "EST") == 0, "Implicit guest TZ update did not refresh tzname"); + Require(setenv_nid_postfix("TZ", "UTC-2", 1) == 0, "Could not restore the guest time zone"); + tzset_nid_postfix(); +} + +static bool ZoneFieldsConsistent(const GuestTm& time, std::int64_t instant, int hour) { + const std::int64_t localSeconds = ((instant + time.tm_gmtoff) % 86400 + 86400) % 86400; + if (time.tm_hour * 3600 + time.tm_min * 60 + time.tm_sec != localSeconds || time.tm_hour != hour) return false; + char expected[16]; + const std::int64_t offset = time.tm_gmtoff < 0 ? -time.tm_gmtoff : time.tm_gmtoff; + std::snprintf(expected, sizeof(expected), "%02d %c%04d", hour, time.tm_gmtoff < 0 ? '-' : '+', static_cast(offset / 3600 * 100)); + char formatted[16]{}; + return strftime_nid_postfix(formatted, sizeof(formatted), "%H %z", &time) == 8 && std::strcmp(formatted, expected) == 0; +} + +static void CheckZoneRace() { + const std::int64_t moment = 172800; + std::atomic stop{false}; + std::atomic torn{0}; + std::thread flipper([&] { + for (bool west = false; !stop.load(); west = !west) setenv_nid_postfix("TZ", west ? "UTC-5" : "UTC+3", 1); + }); + std::array readers; + for (auto& reader : readers) { + reader = std::thread([&] { + for (int iteration = 0; iteration < 2500; ++iteration) { + GuestTm result{}; + if (localtime_s_nid_postfix(&moment, &result) != &result || !ZoneFieldsConsistent(result, moment, result.tm_hour)) ++torn; + GuestTm calendar{}; + calendar.tm_year = 70; + calendar.tm_mday = 3; + calendar.tm_hour = 12; + calendar.tm_isdst = -1; + const std::int64_t instant = mktime_nid_postfix(&calendar); + if (instant == -1 || calendar.tm_mday != 3 || !ZoneFieldsConsistent(calendar, instant, 12)) ++torn; + } + }); + } + for (auto& reader : readers) reader.join(); + stop = true; + flipper.join(); + Require(torn == 0, "A time conversion mixed the fields of two time zones while TZ changed"); + Require(setenv_nid_postfix("TZ", "UTC-2", 1) == 0, "Could not restore the guest time zone"); + tzset_nid_postfix(); +} + +#ifdef _WIN32 +static void CheckRejectedZone() { + const std::int64_t moment = 172800; + const std::string oversized = "UTC-5" + std::string(40000, 'x'); + Require(setenv_nid_postfix("TZ", oversized.c_str(), 1) == 0, "The guest environment rejected a long TZ"); + GuestTm result{}; + bool thrown = false; + try { + localtime_s_nid_postfix(&moment, &result); + } catch (const std::runtime_error&) { + thrown = true; + } + Require(thrown, "localtime accepted a TZ the host cannot hold"); + Require(setenv_nid_postfix("TZ", "UTC-2", 1) == 0, "Could not restore the guest time zone"); + tzset_nid_postfix(); + Require(localtime_s_nid_postfix(&moment, &result) == &result && result.tm_gmtoff == 7200, "localtime did not recover after a rejected TZ"); +} +#endif + +int main() { + Require(setenv_nid_postfix("TZ", "UTC-2", 1) == 0, "Could not set the guest time zone"); + tzset_nid_postfix(); constexpr std::array utcCases{{ {0, 70, 0, 1, 0, 0, 0, 4, 0}, {-1, 69, 11, 31, 23, 59, 59, 3, 364}, @@ -386,28 +508,28 @@ int main() { CheckFreeBsdConversions(); CheckLocalRange(); +#ifndef _WIN32 + CheckRegionZones(); +#endif CheckLiteralConversions(); CheckConcurrent(libc_gmtime_nid_postfix, gmtime_s_nid_postfix, "Concurrent libc_gmtime returned another thread's date"); CheckConcurrent(gmtime_nid_postfix, gmtime_s_nid_postfix, "Concurrent gmtime returned another thread's date"); CheckConcurrent(libc_localtime_nid_postfix, localtime_s_nid_postfix, "Concurrent libc_localtime returned another thread's date"); CheckConcurrent(localtime_nid_postfix, localtime_s_nid_postfix, "Concurrent localtime returned another thread's date"); + CheckGuestZone(); + CheckZoneRace(); #ifdef _WIN32 - _putenv_s("TZ", "UTC-5"); -#else - setenv("TZ", "UTC-5", 1); + CheckRejectedZone(); #endif + Require(setenv_nid_postfix("TZ", "UTC-5", 1) == 0, "Could not set the guest time zone"); tzset_nid_postfix(); GuestTm shifted{}; Require(localtime_s_nid_postfix(&epoch, &shifted) == &shifted && shifted.tm_hour == 5 && shifted.tm_gmtoff == 18000, "tzset did not pick up the new TZ"); Require(tzname_nid_postfix[0] != nullptr && std::strcmp(tzname_nid_postfix[0], "UTC") == 0 && tzname_nid_postfix[1] != nullptr, "tzname does not name the zone tzset set"); -#ifdef _WIN32 - _putenv_s("TZ", "EST5EDT"); -#else - setenv("TZ", "EST5EDT", 1); -#endif + Require(setenv_nid_postfix("TZ", "EST5EDT", 1) == 0, "Could not set the guest time zone"); tzset_nid_postfix(); Require(std::strcmp(tzname_nid_postfix[0], "EST") == 0 && std::strcmp(tzname_nid_postfix[1], "EDT") == 0, "tzname does not name the standard and daylight zones"); diff --git a/core/libs/tests/GuestTimeExtras.cpp b/core/libs/tests/GuestTimeExtras.cpp new file mode 100644 index 0000000000..6fc9d4cde0 --- /dev/null +++ b/core/libs/tests/GuestTimeExtras.cpp @@ -0,0 +1,163 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestTime.hpp" +#include +#include +#include +#include +#include +#include + +struct Xtime { + std::int64_t sec; + std::int64_t nsec; +}; + +extern "C" { +GuestTm* APS5_VABI gmtime_s_nid_postfix(const std::int64_t*, GuestTm*); +GuestTm* APS5_VABI localtime_s_nid_postfix(const std::int64_t*, GuestTm*); +GuestTm* APS5_VABI gmtime_r_nid_postfix(const std::int64_t*, GuestTm*); +GuestTm* APS5_VABI localtime_r_nid_postfix(const std::int64_t*, GuestTm*); +std::int64_t APS5_VABI timegm_nid_postfix(GuestTm*); +void APS5_VABI tzset_nid_postfix(); +int APS5_VABI setenv_nid_postfix(const char*, const char*, int); +int APS5_VABI xtime_get_nid_postfix(Xtime*, int); +Xtime APS5_VABI _Xtime_to_ts_nid_postfix(const Xtime*); +Xtime APS5_VABI _Xtime_diff_to_ts_nid_postfix(const Xtime*); +} + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +static bool Equal(const GuestTm& left, const GuestTm& right) { + return left.tm_sec == right.tm_sec && left.tm_min == right.tm_min && left.tm_hour == right.tm_hour + && left.tm_mday == right.tm_mday && left.tm_mon == right.tm_mon && left.tm_year == right.tm_year + && left.tm_wday == right.tm_wday && left.tm_yday == right.tm_yday && left.tm_isdst == right.tm_isdst; +} + +struct TimegmCase { + int sec, min, hour, mday, mon, year; + std::int64_t expected; + int normalizedMday, normalizedMon, normalizedYear, wday, yday; +}; + +static void CheckTimegm(const TimegmCase& test) { + GuestTm time{}; + time.tm_sec = test.sec; + time.tm_min = test.min; + time.tm_hour = test.hour; + time.tm_mday = test.mday; + time.tm_mon = test.mon; + time.tm_year = test.year; + time.tm_wday = 9; + time.tm_yday = 999; + time.tm_isdst = 5; + time.tm_gmtoff = 77; + if (timegm_nid_postfix(&time) != test.expected) { + std::fprintf(stderr, "timegm(%d-%d-%d) returned the wrong time\n", test.year, test.mon, test.mday); + std::abort(); + } + Require(time.tm_mday == test.normalizedMday && time.tm_mon == test.normalizedMon && time.tm_year == test.normalizedYear + && time.tm_wday == test.wday && time.tm_yday == test.yday && time.tm_isdst == 0 && time.tm_gmtoff == 0, + "timegm did not normalize the calendar fields"); +} + +static void CheckReentrantConversions() { + constexpr std::int64_t timers[] = {0, -1, 86399, 951782400, 4107542400, 253402300799, -62135596800}; + for (const std::int64_t timer : timers) { + GuestTm expected{}; + GuestTm actual{}; + Require(gmtime_s_nid_postfix(&timer, &expected) == &expected, "gmtime_s rejected a representable time"); + Require(gmtime_r_nid_postfix(&timer, &actual) == &actual && Equal(actual, expected), "gmtime_r differs from gmtime_s"); + Require(localtime_s_nid_postfix(&timer, &expected) == &expected, "localtime_s rejected a representable time"); + Require(localtime_r_nid_postfix(&timer, &actual) == &actual && Equal(actual, expected) && actual.tm_gmtoff == 7200, + "localtime_r differs from localtime_s"); + } + const std::int64_t epoch = 0; + Require(gmtime_r_nid_postfix(&epoch, nullptr) == nullptr, "gmtime_r accepted a null result"); + Require(localtime_r_nid_postfix(&epoch, nullptr) == nullptr, "localtime_r accepted a null result"); + const std::int64_t underflow = std::numeric_limits::min(); + GuestTm scratch{}; + Require(gmtime_r_nid_postfix(&underflow, &scratch) == nullptr, "gmtime_r accepted an unrepresentable time"); +} + +static void CheckTimegmCases() { + constexpr TimegmCase cases[] = { + {0, 0, 0, 1, 0, 70, 0, 1, 0, 70, 4, 0}, + {59, 59, 23, 31, 11, 69, -1, 31, 11, 69, 3, 364}, + {7, 14, 3, 19, 9, 126, 1792379647, 19, 9, 126, 1, 291}, + {0, 0, 0, 0, 0, 70, -86400, 31, 11, 69, 3, 364}, + {0, 0, 0, 1, 12, 69, 0, 1, 0, 70, 4, 0}, + {0, 0, 0, 1, -1, 71, 28857600, 1, 11, 70, 2, 334}, + {0, 0, 0, 1, -13, 71, -2678400, 1, 11, 69, 1, 334}, + {0, 0, 0, 32, 1, 100, 952041600, 3, 2, 100, 5, 62}, + {31536000, 0, 0, 1, 0, 70, 31536000, 1, 0, 71, 5, 0}, + {0, 0, 0, 1, 2, 200, 4107542400, 1, 2, 200, 1, 59}, + {59, 59, 23, 31, 11, 8099, 253402300799, 31, 11, 8099, 5, 364}, + {0, 0, 0, 1, 0, -1899, -62135596800, 1, 0, -1899, 1, 0}, + }; + for (const auto& test : cases) CheckTimegm(test); + + GuestTm beyond{}; + beyond.tm_year = std::numeric_limits::max(); + beyond.tm_mon = 12; + beyond.tm_mday = 1; + const GuestTm before = beyond; + Require(timegm_nid_postfix(&beyond) == -1 && Equal(beyond, before), "timegm accepted a year past the int range"); + + std::mt19937_64 random(7); + for (int iteration = 0; iteration < 100000; ++iteration) { + const std::int64_t timer = static_cast(random() % 600000000000ull) - 300000000000; + GuestTm fields{}; + Require(gmtime_r_nid_postfix(&timer, &fields) == &fields, "gmtime_r rejected a representable time"); + GuestTm copy = fields; + Require(timegm_nid_postfix(©) == timer && Equal(copy, fields), "timegm did not invert gmtime_r"); + } +} + +static void CheckXtime() { + Xtime time{-5, -5}; + Require(xtime_get_nid_postfix(&time, 0) == 0 && time.sec == -5 && time.nsec == -5, "xtime_get accepted base 0"); + Require(xtime_get_nid_postfix(&time, 2) == 0 && time.sec == -5 && time.nsec == -5, "xtime_get accepted base 2"); + Require(xtime_get_nid_postfix(nullptr, 1) == 0, "xtime_get accepted a null xtime"); + const std::int64_t before = static_cast(std::time(nullptr)); + Require(xtime_get_nid_postfix(&time, 1) == 1, "xtime_get rejected TIME_UTC"); + Require(time.sec >= before - 1 && time.sec <= before + 2 && time.nsec >= 0 && time.nsec < 1000000000 && time.nsec % 1000 == 0, + "xtime_get did not return the wall clock in microseconds"); + + const Xtime unnormalized{7, -3}; + const Xtime copied = _Xtime_to_ts_nid_postfix(&unnormalized); + Require(copied.sec == 7 && copied.nsec == -3, "_Xtime_to_ts changed the time"); + + Xtime now{}; + Require(xtime_get_nid_postfix(&now, 1) == 1, "xtime_get rejected TIME_UTC"); + const Xtime past{now.sec - 1, now.nsec}; + const Xtime remaining = _Xtime_diff_to_ts_nid_postfix(&past); + Require(remaining.sec == 0 && remaining.nsec == 0, "_Xtime_diff_to_ts did not clamp a past time to zero"); + const Xtime epoch{0, 0}; + const Xtime none = _Xtime_diff_to_ts_nid_postfix(&epoch); + Require(none.sec == 0 && none.nsec == 0, "_Xtime_diff_to_ts did not clamp the epoch to zero"); + const Xtime later{now.sec + 100, now.nsec}; + const Xtime wait = _Xtime_diff_to_ts_nid_postfix(&later); + Require(wait.sec >= 98 && wait.sec <= 100 && wait.nsec >= 0 && wait.nsec < 1000000000, "_Xtime_diff_to_ts returned the wrong wait"); + const Xtime carried{now.sec, now.nsec + 5000000000}; + const Xtime carriedWait = _Xtime_diff_to_ts_nid_postfix(&carried); + Require(carriedWait.sec >= 3 && carriedWait.sec <= 5 && carriedWait.nsec >= 0 && carriedWait.nsec < 1000000000, + "_Xtime_diff_to_ts did not carry whole seconds out of nsec"); + const Xtime borrowed{now.sec + 10, -1}; + const Xtime borrowedWait = _Xtime_diff_to_ts_nid_postfix(&borrowed); + Require(borrowedWait.sec >= 8 && borrowedWait.sec <= 9 && borrowedWait.nsec >= 0 && borrowedWait.nsec < 1000000000, + "_Xtime_diff_to_ts did not borrow a second for a negative nsec"); +} + +int main() { + Require(setenv_nid_postfix("TZ", "UTC-2", 1) == 0, "Could not set the time zone"); + tzset_nid_postfix(); + CheckReentrantConversions(); + CheckTimegmCases(); + CheckXtime(); + return 0; +} diff --git a/core/libs/tests/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/GuestTruncateExtend.cpp b/core/libs/tests/GuestTruncateExtend.cpp new file mode 100644 index 0000000000..66dcffd80e --- /dev/null +++ b/core/libs/tests/GuestTruncateExtend.cpp @@ -0,0 +1,92 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#endif + +extern "C" { +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::uint64_t); +std::int64_t APS5_VABI pread_nid_postfix(int, void*, std::uint64_t, std::int64_t); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +int APS5_VABI truncate_nid_postfix(const char*, std::int64_t); +int APS5_VABI unlink_nid_postfix(const char*); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int ReadOnly = 0; +constexpr int ReadWrite = 2; +constexpr int Create = 0x200; +constexpr std::int64_t Length = 64ll * 1024 * 1024; +constexpr char FileName[] = "anyps5_truncate_extend_file"; + +void Cleanup() { + unlink_nid_postfix(FileName); +} + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + Cleanup(); + std::abort(); +} + +std::uint64_t BytesWritten() { +#ifdef _WIN32 + IO_COUNTERS counters{}; + return GetProcessIoCounters(GetCurrentProcess(), &counters) ? counters.WriteTransferCount : 0; +#else + return 0; +#endif +} + +} + +int main() { + Cleanup(); + const int descriptor = open_nid_postfix(FileName, Create | ReadWrite, 0644); + Require(descriptor >= 0, "open failed"); + Require(write_nid_postfix(descriptor, "head", 4) == 4, "write failed"); + + const std::uint64_t before = BytesWritten(); + Require(ftruncate_nid_postfix(descriptor, Length) == 0, "ftruncate to 64 MiB failed"); + const std::uint64_t written = BytesWritten() - before; + std::printf("extending by 64 MiB wrote %llu bytes\n", static_cast(written)); +#ifdef _WIN32 + Require(written < static_cast(Length) / 16, "extending a file must not write its new length as zero bytes: the process wrote about as many bytes as the new length"); +#endif + + Require(lseek_nid_postfix(descriptor, 0, 2) == Length, "the extended file has the wrong size"); + Require(lseek_nid_postfix(descriptor, 0, 1) == Length, "ftruncate must not move the file offset"); + char head[4] = {}; + char tail[8] = {1, 1, 1, 1, 1, 1, 1, 1}; + Require(pread_nid_postfix(descriptor, head, 4, 0) == 4 && std::memcmp(head, "head", 4) == 0, "extending must keep the existing bytes"); + Require(pread_nid_postfix(descriptor, tail, 8, Length - 8) == 8 && tail[0] == 0 && tail[7] == 0, "the extension must read as zero bytes"); + + Require(ftruncate_nid_postfix(descriptor, 2) == 0, "shrinking failed"); + Require(lseek_nid_postfix(descriptor, 0, 2) == 2, "the shrunk file has the wrong size"); + Require(ftruncate_nid_postfix(descriptor, -1) == -1 && *__error_nid_postfix() == 22, "a negative length must fail with EINVAL (22)"); + Require(close_nid_postfix(descriptor) == 0, "close failed"); + + const int readOnly = open_nid_postfix(FileName, ReadOnly, 0); + Require(readOnly >= 0, "reopen failed"); + Require(ftruncate_nid_postfix(readOnly, 8) == -1 && *__error_nid_postfix() == 22, "ftruncate on a read-only descriptor must fail with EINVAL (22)"); + Require(close_nid_postfix(readOnly) == 0, "close failed"); + + Require(truncate_nid_postfix(FileName, Length) == 0, "truncate to 64 MiB failed"); + const int check = open_nid_postfix(FileName, ReadOnly, 0); + Require(check >= 0 && lseek_nid_postfix(check, 0, 2) == Length, "truncate by path left the wrong size"); + Require(close_nid_postfix(check) == 0, "close failed"); + Require(truncate_nid_postfix(FileName, -1) == -1 && *__error_nid_postfix() == 22, "truncate by path with negative length must fail with EINVAL (22)"); + Require(truncate_nid_postfix(FileName, -100) == -1 && *__error_nid_postfix() == 22, "truncate by path with negative length must fail with EINVAL (22)"); + Cleanup(); + return 0; +} diff --git a/core/libs/tests/GuestTruncateRace.cpp b/core/libs/tests/GuestTruncateRace.cpp new file mode 100644 index 0000000000..29f2f3a3e1 --- /dev/null +++ b/core/libs/tests/GuestTruncateRace.cpp @@ -0,0 +1,125 @@ +#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*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +int APS5_VABI sceKernelFtruncate(int, std::int64_t); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +int APS5_VABI dup_nid_postfix(int); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI pread_nid_postfix(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Truncate race check failed at line %d\n", line); + std::abort(); + } +} + +#define Require(value) Check((value), __LINE__) + +static bool CheckRace(const std::filesystem::path& file, bool duplicate) { + constexpr std::size_t PayloadBytes = 8u << 20; + constexpr std::int64_t Cursor = 137; + constexpr unsigned Rounds = 4; + constexpr unsigned Reads = 32; + constexpr unsigned Truncates = 256; + std::vector payload(PayloadBytes); + for (std::size_t i = 0; i < payload.size(); ++i) payload[i] = static_cast(i % 251); + { + std::ofstream stream(file, std::ios::binary); + stream.write(reinterpret_cast(payload.data()), static_cast(payload.size())); + Require(stream.good()); + } + const int descriptor = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDWR, 0); + Require(descriptor >= 0); + const int resizer = duplicate ? dup_nid_postfix(descriptor) : descriptor; + Require(resizer >= 0); + const auto truncate = duplicate ? ftruncate_nid_postfix : sceKernelFtruncate; + const auto pread = duplicate ? pread_nid_postfix : sceKernelPread; + unsigned truncateFailures = 0; + unsigned preadCountFailures = 0; + unsigned preadDataFailures = 0; + unsigned cursorFailures = 0; + unsigned readFailures = 0; + std::vector output(PayloadBytes); + for (unsigned round = 0; round < Rounds; ++round) { + Require(sceKernelFtruncate(descriptor, PayloadBytes) == 0); + Require(sceKernelLseek(descriptor, Cursor, 0) == Cursor); + std::barrier start(2); + std::thread resizing([&] { + start.arrive_and_wait(); + for (unsigned iteration = 0; iteration < Truncates; ++iteration) { + const auto length = static_cast(PayloadBytes + ((iteration & 1u) ? 4096 : 0)); + if (truncate(resizer, length) != 0) ++truncateFailures; + } + }); + start.arrive_and_wait(); + for (unsigned iteration = 0; iteration < Reads; ++iteration) { + if (pread(descriptor, output.data(), output.size(), 0) != static_cast(output.size())) { + ++preadCountFailures; + } else if (output != payload) { + ++preadDataFailures; + } + } + resizing.join(); + const auto position = sceKernelLseek(descriptor, 0, 1); + if (position != Cursor || sceKernelLseek(resizer, 0, 1) != Cursor) ++cursorFailures; + unsigned char byte = 0xff; + if (sceKernelRead(descriptor, &byte, 1) != 1 || byte != payload[Cursor]) ++readFailures; + std::printf("%s round=%u cursor=%lld expected=%lld next_byte=%u expected_byte=%u\n", + duplicate ? "posix-dup" : "kernel-direct", round, static_cast(position), + static_cast(Cursor), byte, payload[Cursor]); + } + if (duplicate) Require(sceKernelClose(resizer) == 0); + Require(sceKernelClose(descriptor) == 0); + const auto size = std::filesystem::file_size(file); + std::ifstream stream(file, std::ios::binary); + std::vector stored(PayloadBytes); + stream.read(reinterpret_cast(stored.data()), static_cast(stored.size())); + const auto count = stream.gcount(); + stream.close(); + std::size_t wrongBytes = 0; + std::size_t firstWrong = PayloadBytes; + for (std::size_t i = 0; i < stored.size(); ++i) { + if (stored[i] != payload[i]) { + if (wrongBytes == 0) firstWrong = i; + ++wrongBytes; + } + } + std::printf("%s preads=%u truncates=%u truncate_failures=%u pread_count_failures=%u pread_data_failures=%u " + "cursor_failures=%u read_failures=%u stored_bytes=%lld wrong_bytes=%zu first_wrong=%lld " + "file_size=%llu expected_size=%zu\n", + duplicate ? "posix-dup" : "kernel-direct", Rounds * Reads, Rounds * Truncates, + truncateFailures, preadCountFailures, preadDataFailures, cursorFailures, readFailures, + static_cast(count), wrongBytes, wrongBytes == 0 ? -1ll : static_cast(firstWrong), + static_cast(size), PayloadBytes + 4096); + Require(std::filesystem::remove(file)); + return truncateFailures == 0 && preadCountFailures == 0 && preadDataFailures == 0 && + cursorFailures == 0 && readFailures == 0 && count == static_cast(PayloadBytes) && + wrongBytes == 0 && size == PayloadBytes + 4096; +} + +int main() { + const std::filesystem::path root("anyps5-truncate-race-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const bool direct = CheckRace(root / "direct.bin", false); + const bool duplicate = CheckRace(root / "duplicate.bin", true); + Require(std::filesystem::remove(root)); + return direct && duplicate ? 0 : 1; +} diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index d9da3cf349..7efa4731f4 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -19,6 +19,7 @@ int APS5_VABI getpeername_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); int APS5_VABI _ioctl_nid_postfix(int, std::uint64_t, void*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); int APS5_VABI close_nid_postfix(int); @@ -98,9 +99,69 @@ static void CheckAddressLengths(int family) { Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); } +static void CheckSendToPortZero() { + const int receiver = socket_nid_postfix(2, 2, 0); + const int sender = socket_nid_postfix(2, 2, 0); + Require(receiver >= 0 && sender >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(receiver, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(receiver, address.data(), &size) == 0); + std::array portZero{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sendto_nid_postfix(sender, "x", 1, 0, portZero.data(), portZero.size()) == -1 && *__error_nid_postfix() == 49); + char buffer[4]; + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); + Require(sendto_nid_postfix(sender, "x", 1, 0, address.data(), address.size()) == 1); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0) == 1); + const int bound = socket_nid_postfix(2, 2, 0); + Require(bound >= 0); + std::array bindAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(bound, bindAddress.data(), bindAddress.size()) == 0); + Require(sendto_nid_postfix(bound, "x", 1, 0, portZero.data(), portZero.size()) == 1); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); + std::array anyDestination{16, 2, 0, 0, 0, 0, 0, 0}; + Require(sendto_nid_postfix(bound, "x", 1, 0, anyDestination.data(), anyDestination.size()) == -1 && *__error_nid_postfix() == 49); + Require(close_nid_postfix(bound) == 0 && close_nid_postfix(receiver) == 0 && close_nid_postfix(sender) == 0); +} + +static void CheckDontWait() { + const int receiver = socket_nid_postfix(2, 2, 0); + const int sender = socket_nid_postfix(2, 2, 0); + Require(receiver >= 0 && sender >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(receiver, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(receiver, address.data(), &size) == 0); + char buffer[16]{}; + Iovec vector{buffer, sizeof(buffer)}; + Msghdr message{nullptr, 0, &vector, 1, nullptr, 0, 0}; + const auto start = std::chrono::steady_clock::now(); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); + Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0x80, nullptr, nullptr) == -1 && *__error_nid_postfix() == 35); + Require(recvmsg_nid_postfix(receiver, &message, 0x80) == -1 && *__error_nid_postfix() == 35); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x82) == -1 && *__error_nid_postfix() == 35); + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(1)); + const char datagram[] = "nowait"; + Require(sendto_nid_postfix(sender, datagram, sizeof(datagram), 0x80, address.data(), address.size()) == sizeof(datagram)); + Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 2, nullptr, nullptr) == sizeof(datagram)); + std::memset(buffer, 0, sizeof(buffer)); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x82) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + std::memset(buffer, 0, sizeof(buffer)); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + Require(connect_nid_postfix(sender, address.data(), address.size()) == 0); + Require(send_nid_postfix(sender, datagram, sizeof(datagram), 0x80 | 0x20000) == sizeof(datagram)); + std::memset(buffer, 0, sizeof(buffer)); + Require(recvmsg_nid_postfix(receiver, &message, 0) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x1) == -1 && *__error_nid_postfix() == 45); + Require(close_nid_postfix(receiver) == 0 && close_nid_postfix(sender) == 0); +} + int main() { CheckAddressLengths(2); CheckAddressLengths(28); + CheckDontWait(); + CheckSendToPortZero(); const int receiver = socket_nid_postfix(2, 2, 0); const int sender = socket_nid_postfix(2, 2, 17); Require(receiver >= 0 && sender >= 0 && receiver != sender); @@ -157,7 +218,7 @@ int main() { Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0, nullptr, nullptr) == -1); Require(*__error_nid_postfix() == 35); const char message[] = "guest UDP loopback"; - Require(sendto_nid_postfix(sender, message, sizeof(message), 0x20000, destination.data(), destination.size()) == sizeof(message)); + Require(send_nid_postfix(sender, message, sizeof(message), 0x20000) == sizeof(message)); int queued = 0; for (int i = 0; i < 100 && queued == 0; ++i) { Require(ioctl_nid_postfix(receiver, 0x4004667f, &queued) == 0); @@ -177,7 +238,7 @@ int main() { char head[] = "scatter "; char tail[] = "gather"; Iovec parts[2]{{head, 8}, {tail, 6}}; - Msghdr outgoing{destination.data(), static_cast(destination.size()), parts, 2, nullptr, 0, 0}; + Msghdr outgoing{nullptr, 0, parts, 2, nullptr, 0, 0}; Require(sendmsg_nid_postfix(sender, &outgoing, 0) == 14); const auto wait = [&] { queued = 0; diff --git a/core/libs/tests/GuestUltCondition.cpp b/core/libs/tests/GuestUltCondition.cpp new file mode 100644 index 0000000000..3922c753ef --- /dev/null +++ b/core/libs/tests/GuestUltCondition.cpp @@ -0,0 +1,245 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceUltFinalize(); +int APS5_VABI sceUltMutexCreate(void*, const char*, void*, const UltMutexOptParam*, std::uint32_t); +int APS5_VABI sceUltMutexLock(void*); +int APS5_VABI sceUltMutexUnlock(void*); +int APS5_VABI sceUltMutexDestroy(void*); +int APS5_VABI sceUltConditionVariableCreate(void*, const char*, void*, const void*, std::uint32_t); +int APS5_VABI _sceUltConditionVariableCreate(void*, const char*, void*, const void*, std::uint32_t); +int APS5_VABI sceUltConditionVariableWait(void*); +int APS5_VABI sceUltConditionVariableSignal(void*); +int APS5_VABI sceUltConditionVariableDestroy(void*); +} + +constexpr int Null = -2139029503; +constexpr int Invalid = -2139029500; +constexpr int Permission = -2139029499; +constexpr int State = -2139029498; +constexpr int Busy = -2139029497; + +static void Require(bool ok, int line) { + if (!ok) { + std::fprintf(stderr, "ULT condition check failed at line %d\n", line); + std::abort(); + } +} + +#define CHECK(value) Require((value), __LINE__) + +struct alignas(8) Object { std::array bytes{}; }; + +struct Fixture { + Object mutex; + Object condition; + + explicit Fixture(bool recursive = false) { + UltMutexOptParam options{}; + options.attribute = recursive ? 1 : 0; + CHECK(sceUltMutexCreate(&mutex, "mutex", nullptr, &options, 0x12000000) == 0); + CHECK(_sceUltConditionVariableCreate(&condition, "condition", &mutex, nullptr, 0x12000000) == 0); + } + + void Lock() { CHECK(sceUltMutexLock(&mutex) == 0); } + void Unlock() { CHECK(sceUltMutexUnlock(&mutex) == 0); } + void Wait() { CHECK(sceUltConditionVariableWait(&condition) == 0); } + void Signal() { CHECK(sceUltConditionVariableSignal(&condition) == 0); } + + ~Fixture() { + CHECK(sceUltConditionVariableDestroy(&condition) == 0); + CHECK(sceUltMutexDestroy(&mutex) == 0); + } +}; + +static void Validation() { + Fixture f(true); + Object unused; + CHECK(sceUltMutexDestroy(nullptr) == Null); + CHECK(sceUltMutexDestroy(&unused) == State); + CHECK(sceUltConditionVariableCreate(nullptr, nullptr, nullptr, nullptr, 0) == Null); + CHECK(sceUltConditionVariableCreate(&unused, nullptr, &unused, nullptr, 0) == Invalid); + CHECK(sceUltConditionVariableCreate(&f.condition, nullptr, &f.mutex, nullptr, 0) == State); + CHECK(_sceUltConditionVariableCreate(nullptr, nullptr, nullptr, nullptr, 0) == Null); + CHECK(_sceUltConditionVariableCreate(&unused, nullptr, &unused, nullptr, 0) == Invalid); + CHECK(_sceUltConditionVariableCreate(&f.condition, nullptr, &f.mutex, nullptr, 0) == State); + CHECK(sceUltConditionVariableDestroy(nullptr) == Null); + CHECK(sceUltConditionVariableDestroy(&unused) == State); + CHECK(sceUltConditionVariableSignal(nullptr) == Null); + CHECK(sceUltConditionVariableSignal(&unused) == State); + CHECK(sceUltConditionVariableWait(nullptr) == Null); + CHECK(sceUltConditionVariableWait(&unused) == State); + CHECK(_sceUltConditionVariableCreate(&unused, nullptr, nullptr, nullptr, 0) == Null); + CHECK(sceUltConditionVariableWait(&f.condition) == State); + CHECK(sceUltMutexUnlock(&f.mutex) == Permission); + bool unsupported = false; + try { _sceUltConditionVariableCreate(&unused, nullptr, &f.mutex, &unused, 0); } + catch (const std::runtime_error&) { unsupported = true; } + CHECK(unsupported); + f.Lock(); + CHECK(sceUltMutexDestroy(&f.mutex) == State); + CHECK(sceUltMutexCreate(&f.mutex, nullptr, nullptr, nullptr, 0) == State); + auto other = std::async(std::launch::async, [&] { + CHECK(sceUltMutexUnlock(&f.mutex) == Permission); + CHECK(sceUltConditionVariableWait(&f.condition) == State); + }); + other.get(); + f.Lock(); + unsupported = false; + try { f.Wait(); } catch (const std::runtime_error&) { unsupported = true; } + CHECK(unsupported); + f.Unlock(); + CHECK(sceUltMutexDestroy(&f.mutex) == State); + f.Unlock(); + CHECK(sceUltMutexDestroy(&f.mutex) == State); + CHECK(sceUltConditionVariableDestroy(&f.condition) == 0); + CHECK(sceUltMutexDestroy(&f.mutex) == 0); + CHECK(sceUltMutexLock(&f.mutex) == State); + CHECK(sceUltMutexCreate(&f.mutex, nullptr, nullptr, nullptr, 0) == 0); + CHECK(sceUltConditionVariableDestroy(&f.condition) == State); + CHECK(sceUltConditionVariableSignal(&f.condition) == State); + CHECK(_sceUltConditionVariableCreate(&f.condition, nullptr, &f.mutex, nullptr, 0) == 0); +} + +static void Await(const std::atomic& count, int expected) { + const auto end = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (count.load() != expected) { + CHECK(std::chrono::steady_clock::now() < end); + std::this_thread::yield(); + } +} + +static void Handoff() { + Fixture f; + f.Signal(); + std::atomic entered = 0; + int payload = 0; + auto waiter = std::async(std::launch::async, [&] { + f.Lock(); + entered = 1; + f.Wait(); + CHECK(payload == 42); + CHECK(sceUltMutexDestroy(&f.mutex) == State); + f.Unlock(); + }); + Await(entered, 1); + f.Lock(); + CHECK(sceUltConditionVariableDestroy(&f.condition) == Busy); + CHECK(sceUltMutexDestroy(&f.mutex) == State); + CHECK(waiter.wait_for(std::chrono::milliseconds(30)) == std::future_status::timeout); + f.Unlock(); + f.Lock(); + payload = 42; + f.Signal(); + CHECK(waiter.wait_for(std::chrono::milliseconds(30)) == std::future_status::timeout); + CHECK(sceUltConditionVariableDestroy(&f.condition) == Busy); + f.Unlock(); + CHECK(waiter.wait_for(std::chrono::seconds(3)) == std::future_status::ready); + waiter.get(); +} + +static void SignalOne() { + Fixture f; + std::atomic entered = 0, finished = 0; + std::array, 3> waiters; + for (auto& waiter : waiters) waiter = std::async(std::launch::async, [&] { + f.Lock(); + ++entered; + f.Wait(); + ++finished; + f.Unlock(); + }); + Await(entered, 3); + f.Lock(); + f.Signal(); + f.Unlock(); + Await(finished, 1); + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + CHECK(finished == 1); + f.Lock(); + f.Signal(); + f.Signal(); + f.Unlock(); + for (auto& waiter : waiters) { + CHECK(waiter.wait_for(std::chrono::seconds(3)) == std::future_status::ready); + waiter.get(); + } + CHECK(finished == 3); +} + +static void Stress() { + Fixture f; + Object consumed; + CHECK(_sceUltConditionVariableCreate(&consumed, nullptr, &f.mutex, nullptr, 0) == 0); + int payload = 0; + auto consumer = std::async(std::launch::async, [&] { + for (int expected = 1; expected <= 5000; ++expected) { + f.Lock(); + while (payload == 0) f.Wait(); + CHECK(payload == expected); + payload = 0; + CHECK(sceUltConditionVariableSignal(&consumed) == 0); + f.Unlock(); + } + }); + for (int value = 1; value <= 5000; ++value) { + f.Lock(); + while (payload != 0) CHECK(sceUltConditionVariableWait(&consumed) == 0); + payload = value; + f.Signal(); + f.Unlock(); + } + CHECK(consumer.wait_for(std::chrono::seconds(3)) == std::future_status::ready); + consumer.get(); + CHECK(sceUltConditionVariableDestroy(&consumed) == 0); +} + +static void Finalize(bool signaled) { + Object mutex, condition; + CHECK(sceUltMutexCreate(&mutex, "mutex", nullptr, nullptr, 0) == 0); + CHECK(_sceUltConditionVariableCreate(&condition, "condition", &mutex, nullptr, 0) == 0); + std::atomic entered = 0; + auto waiter = std::async(std::launch::async, [&] { + CHECK(sceUltMutexLock(&mutex) == 0); + entered = 1; + CHECK(sceUltConditionVariableWait(&condition) == State); + CHECK(sceUltMutexUnlock(&mutex) == State); + }); + Await(entered, 1); + CHECK(sceUltMutexLock(&mutex) == 0); + if (signaled) CHECK(sceUltConditionVariableSignal(&condition) == 0); + CHECK(waiter.wait_for(std::chrono::milliseconds(30)) == std::future_status::timeout); + CHECK(sceUltConditionVariableDestroy(&condition) == Busy); + CHECK(sceUltFinalize() == 0); + CHECK(waiter.wait_for(std::chrono::seconds(3)) == std::future_status::ready); + waiter.get(); + CHECK(sceUltConditionVariableWait(&condition) == State); + CHECK(sceUltMutexLock(&mutex) == State); + CHECK(sceUltMutexCreate(&mutex, "reopened", nullptr, nullptr, 0) == 0); + CHECK(_sceUltConditionVariableCreate(&condition, "reopened", &mutex, nullptr, 0) == 0); + CHECK(sceUltConditionVariableDestroy(&condition) == 0); + CHECK(sceUltMutexDestroy(&mutex) == 0); +} + +int main(int argc, char** argv) { + if (argc != 2) return 1; + const std::string test = argv[1]; + if (test == "validation") Validation(); + else if (test == "handoff") Handoff(); + else if (test == "signal-one") SignalOne(); + else if (test == "stress") Stress(); + else if (test == "finalize-queued") Finalize(false); + else if (test == "finalize-reacquire") Finalize(true); + else return 1; + CHECK(sceUltFinalize() == 0); + std::puts("ULT condition tests passed"); +} diff --git a/core/libs/tests/GuestUltQueue.cpp b/core/libs/tests/GuestUltQueue.cpp index 2a93f78c0c..4cfcf13881 100644 --- a/core/libs/tests/GuestUltQueue.cpp +++ b/core/libs/tests/GuestUltQueue.cpp @@ -13,6 +13,7 @@ extern "C" { int APS5_VABI sceUltInitialize(); int APS5_VABI sceUltFinalize(); +std::uint64_t APS5_VABI sceUltWaitingQueueResourcePoolGetWorkAreaSize(std::uint32_t, std::uint32_t); std::uint64_t APS5_VABI sceUltQueueDataResourcePoolGetWorkAreaSize(std::uint32_t, std::uint64_t, std::uint32_t); int APS5_VABI sceUltQueueDataResourcePoolCreate(void*, const char*, std::uint32_t, std::uint64_t, std::uint32_t, void*, void*, const void*, std::uint32_t); int APS5_VABI sceUltQueueDataResourcePoolDestroy(void*); @@ -81,7 +82,7 @@ static void Validation() { CHECK(sceUltQueueTryPop(&fixture.first, &saved) == 0 && saved == value); CHECK(sceUltQueueCreate(&extra, nullptr, 9, nullptr, &fixture.pool, nullptr, 0) == Invalid); CHECK(sceUltQueueCreate(&extra, nullptr, 8, &extra, &fixture.pool, nullptr, 0) == Invalid); - CHECK(sceUltQueueCreate(&extra, nullptr, 8, nullptr, &fixture.pool, nullptr, 0) == Again); + CHECK(sceUltQueueCreate(&extra, nullptr, 8, nullptr, &fixture.pool, nullptr, 0) == Busy); CHECK(sceUltQueueCreate(nullptr, nullptr, 8, nullptr, &fixture.pool, nullptr, 0) == Null); CHECK(sceUltQueueCreate(extra.bytes.data() + 1, nullptr, 8, nullptr, &fixture.pool, nullptr, 0) == Alignment); CHECK(sceUltQueueCreate(&extra, nullptr, 0, nullptr, &fixture.pool, nullptr, 0) == Range); @@ -93,7 +94,11 @@ static void Validation() { CHECK(sceUltQueueDataResourcePoolDestroy(&fixture.pool) == Busy); CHECK(sceUltQueueDataResourcePoolDestroy(nullptr) == Null); CHECK(sceUltQueueDestroy(nullptr) == Null); - CHECK(sceUltQueueDataResourcePoolGetWorkAreaSize(3, 9, 2) == 1072); + CHECK(sceUltQueueDataResourcePoolGetWorkAreaSize(3, 9, 2) == 379); + CHECK(sceUltQueueDataResourcePoolGetWorkAreaSize(0, 0, 0) == 96); + CHECK(sceUltWaitingQueueResourcePoolGetWorkAreaSize(0, 0) == 64); + CHECK(sceUltWaitingQueueResourcePoolGetWorkAreaSize(3, 2) == 320); + CHECK(sceUltWaitingQueueResourcePoolGetWorkAreaSize(0, 0x08000000u) == 0x100000040ull); bool overflow = false; try { sceUltQueueDataResourcePoolGetWorkAreaSize(1, UINT64_MAX, 1); } catch (const std::out_of_range&) { overflow = true; } @@ -151,13 +156,13 @@ static void Blocking() { CHECK(sceUltQueuePush(&fixture.first, &first) == 0); auto producer = std::async(std::launch::async, [&] { return sceUltQueuePush(&fixture.first, &second); }); WaitUntilBlocked(producer); - CHECK(sceUltQueueDestroy(&fixture.first) == Busy); + CHECK(sceUltQueueDestroy(&fixture.first) == State); CHECK(sceUltQueuePop(&fixture.first, &output) == 0 && output == first); CHECK(producer.get() == 0); CHECK(sceUltQueuePop(&fixture.first, &output) == 0 && output == second); auto consumer = std::async(std::launch::async, [&] { return sceUltQueuePop(&fixture.first, &output); }); WaitUntilBlocked(consumer); - CHECK(sceUltQueueDestroy(&fixture.first) == Busy); + CHECK(sceUltQueueDestroy(&fixture.first) == State); CHECK(sceUltQueuePush(&fixture.first, &first) == 0); CHECK(consumer.get() == 0 && output == first); fixture.Destroy(); @@ -175,7 +180,7 @@ static void SharedPool() { CHECK(sceUltQueueTryPop(&fixture.second, &output) == Again); auto producer = std::async(std::launch::async, [&] { return sceUltQueuePush(&fixture.second, &second); }); WaitUntilBlocked(producer); - CHECK(sceUltQueueDestroy(&fixture.second) == Busy); + CHECK(sceUltQueueDestroy(&fixture.second) == State); CHECK(sceUltQueueDestroy(&fixture.first) == 0); CHECK(producer.get() == 0); CHECK(consumer.get() == 0 && output == second); @@ -196,8 +201,8 @@ static void Finalize() { auto consumer = std::async(std::launch::async, [&] { return sceUltQueuePop(&fixture.second, &output); }); WaitUntilBlocked(producer); WaitUntilBlocked(consumer); - CHECK(sceUltQueueDestroy(&fixture.first) == Busy); - CHECK(sceUltQueueDestroy(&fixture.second) == Busy); + CHECK(sceUltQueueDestroy(&fixture.first) == State); + CHECK(sceUltQueueDestroy(&fixture.second) == State); CHECK(sceUltFinalize() == 0); CHECK(producer.get() == State); CHECK(consumer.get() == State && output == 123); diff --git a/core/libs/tests/GuestUltSync.cpp b/core/libs/tests/GuestUltSync.cpp new file mode 100644 index 0000000000..7b2e1e51ed --- /dev/null +++ b/core/libs/tests/GuestUltSync.cpp @@ -0,0 +1,325 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceUltInitialize(); +int APS5_VABI sceUltFinalize(); +int APS5_VABI sceUltMutexCreate(void*, const char*, void*, const UltMutexOptParam*, std::uint32_t); +int APS5_VABI sceUltMutexLock(void*); +int APS5_VABI sceUltMutexTryLock(void*); +int APS5_VABI sceUltMutexUnlock(void*); +int APS5_VABI sceUltMutexDestroy(void*); +int APS5_VABI sceUltSemaphoreOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltQueueOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltQueueDataResourcePoolOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltWaitingQueueResourcePoolOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltUlthreadOptParamInitialize(void*, std::uint32_t); +int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*); +int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void*); +int APS5_VABI sceUltReaderWriterLockOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltConditionVariableOptParamInitialize(void*, std::uint32_t); +int APS5_VABI _sceUltConditionVariableOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltReaderWriterLockCreate(void*, const char*, void*, const void*, std::uint32_t); +int APS5_VABI sceUltReaderWriterLockDestroy(void*); +int APS5_VABI sceUltReaderWriterLockLockRead(void*); +int APS5_VABI sceUltReaderWriterLockTryLockRead(void*); +int APS5_VABI sceUltReaderWriterLockUnlockRead(void*); +int APS5_VABI sceUltReaderWriterLockLockWrite(void*); +int APS5_VABI sceUltReaderWriterLockTryLockWrite(void*); +int APS5_VABI sceUltReaderWriterLockUnlockWrite(void*); +std::uint64_t APS5_VABI sceUltUlthreadRuntimeGetWorkAreaSize(std::uint32_t, std::uint32_t); +int APS5_VABI sceUltUlthreadRuntimeCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*, std::uint32_t); +int APS5_VABI sceUltUlthreadRuntimeDestroy(void*); +int APS5_VABI sceUltUlthreadCreate(void*, const char*, UltUlthreadEntry, std::uint64_t, void*, std::uint64_t, void*, const void*, std::uint32_t); +int APS5_VABI sceUltUlthreadJoin(void*, std::int32_t*); +int APS5_VABI sceUltUlthreadTryJoin(void*, std::int32_t*); +int APS5_VABI sceUltUlthreadGetSelf(void**); +int APS5_VABI sceUltUlthreadYield(); +int APS5_VABI sceUltUlthreadExit(std::int32_t); +} + +namespace { + +constexpr int Ok = 0; +constexpr int Null = -2139029503; +constexpr int Invalid = -2139029500; +constexpr int Permission = -2139029499; +constexpr int Again = -2139029496; +constexpr int State = -2139029498; +constexpr int Busy = -2139029497; + +void Require(bool value, int line) { + if (value) return; + std::fprintf(stderr, "ULT sync check failed at line %d\n", line); + std::abort(); +} + +#define CHECK(value) Require((value), __LINE__) + +struct alignas(8) Object { + std::uint8_t bytes[256]; +}; + +void MutexTryLock() { + Object mutex{}; + CHECK(sceUltMutexTryLock(nullptr) == Null); + CHECK(sceUltMutexTryLock(&mutex) == State); + UltMutexOptParam options{}; + options.attribute = 1; + CHECK(sceUltMutexCreate(&mutex, "trylock", nullptr, &options, 0) == Ok); + CHECK(sceUltMutexTryLock(&mutex) == Ok); + CHECK(sceUltMutexTryLock(&mutex) == Ok); + std::atomic other{0}; + std::thread([&] { other = sceUltMutexTryLock(&mutex); }).join(); + CHECK(other == Busy); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + std::thread([&] { other = sceUltMutexTryLock(&mutex); }).join(); + CHECK(other == Busy); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + CHECK(sceUltMutexUnlock(&mutex) == Permission); + std::thread([&] { + other = sceUltMutexTryLock(&mutex); + if (other == Ok) other = sceUltMutexUnlock(&mutex); + }).join(); + CHECK(other == Ok); + CHECK(sceUltMutexLock(&mutex) == Ok); + CHECK(sceUltMutexDestroy(&mutex) == State); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + CHECK(sceUltMutexDestroy(&mutex) == Ok); + CHECK(sceUltMutexTryLock(&mutex) == State); +} + +void OptParams() { + using Initializer = int (APS5_VABI *)(void*, std::uint32_t); + for (const Initializer initialize : {sceUltSemaphoreOptParamInitialize, sceUltQueueOptParamInitialize, sceUltQueueDataResourcePoolOptParamInitialize, + sceUltWaitingQueueResourcePoolOptParamInitialize, sceUltUlthreadOptParamInitialize, sceUltReaderWriterLockOptParamInitialize, + sceUltConditionVariableOptParamInitialize, _sceUltConditionVariableOptParamInitialize}) { + std::uint8_t bytes[136]; + std::memset(bytes, 0xa5, sizeof(bytes)); + CHECK(initialize(nullptr, 0) == Null); + CHECK(initialize(bytes, 0) == Ok); + for (std::size_t index = 0; index < 128; ++index) CHECK(bytes[index] == 0); + for (std::size_t index = 128; index < sizeof(bytes); ++index) CHECK(bytes[index] == 0xa5); + } +} + + +void WaitUntil(const std::atomic& flag) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!flag.load() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + CHECK(flag.load()); +} + +void Settle() { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); +} + +void ReaderWriterValidation() { + Object rwlock{}; + Object pool{}; + std::memset(&rwlock, 0x5a, sizeof(rwlock)); + CHECK(sceUltReaderWriterLockCreate(nullptr, "rwlock", nullptr, nullptr, 0) == Null); + CHECK(sceUltReaderWriterLockCreate(&rwlock, "rwlock", &pool, nullptr, 0) == Invalid); + for (const auto call : {sceUltReaderWriterLockDestroy, sceUltReaderWriterLockLockRead, sceUltReaderWriterLockTryLockRead, sceUltReaderWriterLockUnlockRead, + sceUltReaderWriterLockLockWrite, sceUltReaderWriterLockTryLockWrite, sceUltReaderWriterLockUnlockWrite}) { + CHECK(call(nullptr) == Null); + CHECK(call(&rwlock) == State); + } + CHECK(sceUltReaderWriterLockCreate(&rwlock, "rwlock", nullptr, nullptr, 0) == Ok); + for (const auto byte : rwlock.bytes) CHECK(byte == 0); + CHECK(sceUltReaderWriterLockCreate(&rwlock, "rwlock", nullptr, nullptr, 0) == State); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == State); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == State); + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Busy); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == State); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == State && sceUltReaderWriterLockTryLockWrite(&rwlock) == State); + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == State && sceUltReaderWriterLockTryLockRead(&rwlock) == State); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Busy); + std::atomic other{0}; + std::thread([&] { other = sceUltReaderWriterLockUnlockWrite(&rwlock); }).join(); + CHECK(other == State); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == State && sceUltReaderWriterLockDestroy(&rwlock) == State); +} + +void ReaderWriterReaders() { + Object rwlock{}; + CHECK(sceUltReaderWriterLockCreate(&rwlock, "readers", nullptr, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == Ok); + std::atomic other{0}; + std::thread([&] { + other = sceUltReaderWriterLockTryLockRead(&rwlock); + if (other == Ok) other = sceUltReaderWriterLockLockRead(&rwlock); + }).join(); + CHECK(other == Ok); + CHECK(sceUltReaderWriterLockTryLockWrite(&rwlock) == Busy); + std::atomic started{false}; + std::atomic written{false}; + std::thread writer([&] { + started = true; + other = sceUltReaderWriterLockLockWrite(&rwlock); + written = true; + if (other == Ok) other = sceUltReaderWriterLockUnlockWrite(&rwlock); + }); + WaitUntil(started); + for (int held = 3; held > 0; --held) { + Settle(); + CHECK(!written.load()); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == Ok); + } + writer.join(); + CHECK(written.load() && other == Ok); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == State); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); +} + +void ReaderWriterWriter() { + Object rwlock{}; + CHECK(sceUltReaderWriterLockCreate(&rwlock, "writer", nullptr, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockTryLockWrite(&rwlock) == Ok); + std::atomic other{0}; + std::thread([&] { other = sceUltReaderWriterLockTryLockRead(&rwlock); }).join(); + CHECK(other == Busy); + std::thread([&] { other = sceUltReaderWriterLockTryLockWrite(&rwlock); }).join(); + CHECK(other == Busy); + std::atomic started{false}; + std::atomic read{false}; + std::thread reader([&] { + started = true; + other = sceUltReaderWriterLockLockRead(&rwlock); + read = true; + if (other == Ok) other = sceUltReaderWriterLockUnlockRead(&rwlock); + }); + WaitUntil(started); + Settle(); + CHECK(!read.load()); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Busy); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + reader.join(); + CHECK(read.load() && other == Ok); + CHECK(sceUltReaderWriterLockTryLockWrite(&rwlock) == Ok && sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); +} + +void ReaderWriterPool() { + Object pool{}; + Object rwlock{}; + std::uint8_t workArea[2048]; + CHECK(_sceUltWaitingQueueResourcePoolCreate(&pool, "pool", 2, 2, workArea, nullptr) == Ok); + CHECK(sceUltReaderWriterLockCreate(&rwlock, "pooled", &pool, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == Ok); + std::atomic started{false}; + std::atomic other{0}; + std::thread reader([&] { + started = true; + other = sceUltReaderWriterLockLockRead(&rwlock); + if (other == Ok) other = sceUltReaderWriterLockUnlockRead(&rwlock); + }); + WaitUntil(started); + Settle(); + CHECK(sceUltWaitingQueueResourcePoolDestroy(&pool) == Busy); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + reader.join(); + CHECK(other == Ok); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); + CHECK(sceUltWaitingQueueResourcePoolDestroy(&pool) == Ok); +} + +void ReaderWriterFinalize() { + Object rwlock{}; + CHECK(sceUltReaderWriterLockCreate(&rwlock, "finalize", nullptr, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == Ok); + std::atomic started{false}; + std::atomic other{0}; + std::thread waiter([&] { + started = true; + other = sceUltReaderWriterLockLockWrite(&rwlock); + }); + WaitUntil(started); + Settle(); + CHECK(sceUltFinalize() == Ok); + waiter.join(); + CHECK(other == State); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == State); + CHECK(sceUltInitialize() == Ok); +} + +struct alignas(8) UlthreadObject { + std::uint8_t bytes[512]; +}; + +struct alignas(8) UlthreadRuntimeObject { + std::uint8_t bytes[4096]; +}; + +std::atomic gSelf{nullptr}; +std::atomic gYield{-1}; +std::atomic gPastExit{false}; + +std::int32_t APS5_VABI ExitingEntry(std::uint64_t status) { + void* self = nullptr; + if (sceUltUlthreadGetSelf(&self) == Ok) gSelf = self; + gYield = sceUltUlthreadYield(); + sceUltUlthreadExit(static_cast(status)); + gPastExit = true; + return -1; +} + +void Ulthreads() { + void* self = &gSelf; + CHECK(sceUltUlthreadGetSelf(nullptr) == Null); + CHECK(sceUltUlthreadGetSelf(&self) == State && self == &gSelf); + CHECK(sceUltUlthreadYield() == State); + CHECK(sceUltUlthreadExit(1) == State); + std::vector workArea(sceUltUlthreadRuntimeGetWorkAreaSize(2, 1)); + UlthreadRuntimeObject runtime{}; + CHECK(sceUltUlthreadRuntimeCreate(&runtime, "runtime", 2, 1, workArea.data(), nullptr, 0) == Ok); + UlthreadObject first{}; + std::uint8_t context[4096]; + CHECK(sceUltUlthreadCreate(&first, "exits", ExitingEntry, 42, context, sizeof(context), &runtime, nullptr, 0) == Ok); + std::int32_t status = 0; + CHECK(sceUltUlthreadJoin(&first, &status) == Ok); + CHECK(status == 42 && gSelf.load() == &first && gYield.load() == Ok && !gPastExit.load()); + UlthreadObject second{}; + gSelf = nullptr; + CHECK(sceUltUlthreadCreate(&second, "tryjoin", ExitingEntry, 7, context, sizeof(context), &runtime, nullptr, 0) == Ok); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + int joined = Again; + while (joined == Again && std::chrono::steady_clock::now() < deadline) { + joined = sceUltUlthreadTryJoin(&second, &status); + if (joined == Again) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + CHECK(joined == Ok && status == 7 && gSelf.load() == &second); + CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == Ok); +} + +} + +int main(int argc, char** argv) { + if (argc != 2) return 2; + CHECK(sceUltInitialize() == Ok); + if (std::strcmp(argv[1], "mutex-trylock") == 0) MutexTryLock(); + else if (std::strcmp(argv[1], "opt-params") == 0) OptParams(); + else if (std::strcmp(argv[1], "rwlock-validation") == 0) ReaderWriterValidation(); + else if (std::strcmp(argv[1], "rwlock-readers") == 0) ReaderWriterReaders(); + else if (std::strcmp(argv[1], "rwlock-writer") == 0) ReaderWriterWriter(); + else if (std::strcmp(argv[1], "rwlock-pool") == 0) ReaderWriterPool(); + else if (std::strcmp(argv[1], "rwlock-finalize") == 0) ReaderWriterFinalize(); + else if (std::strcmp(argv[1], "ulthreads") == 0) Ulthreads(); + else return 2; + CHECK(sceUltFinalize() == Ok); + std::puts("ULT sync tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index fdf347e64e..32b8be2972 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -4,7 +4,12 @@ #include #include #include +#include #include +#include + +struct UltConditionVariableInfo; +struct UltWaitingThreadInfo; extern "C" { int APS5_VABI sceUltInitialize(); @@ -16,26 +21,39 @@ int APS5_VABI sceUltUlthreadJoin(void*, std::int32_t*); int APS5_VABI sceUltUlthreadTryJoin(void*, std::int32_t*); int APS5_VABI sceUltUlthreadRuntimeDestroy(void*); int APS5_VABI _sceUltUlthreadRuntimeOptParamInitialize(void*, std::uint32_t); -int APS5_VABI _sceUltUlthreadRuntimeCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*, std::uint32_t); -int APS5_VABI _sceUltUlthreadCreate(void*, const char*, UltUlthreadEntry, std::uint64_t, void*, std::uint64_t, void*, const void*, std::uint32_t); -int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*, std::uint32_t); -int APS5_VABI _sceUltSemaphoreCreate(void*, const char*, std::int32_t, void*, const void*, std::uint32_t); +int APS5_VABI _sceUltUlthreadRuntimeCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*); +int APS5_VABI _sceUltUlthreadCreate(void*, const char*, UltUlthreadEntry, std::uint64_t, void*, std::uint64_t, void*, const void*); +int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*); +int APS5_VABI _sceUltSemaphoreCreate(void*, const char*, std::uint32_t, void*, const void*); +int APS5_VABI sceUltSemaphoreTryAcquire(void*, std::int32_t); int APS5_VABI sceUltSemaphoreAcquire(void*, std::int32_t); int APS5_VABI sceUltSemaphoreRelease(void*, std::int32_t); int APS5_VABI _sceUltMutexOptParamInitialize(void*, std::uint32_t); -int APS5_VABI _sceUltMutexCreate(void*, const char*, void*, const void*, std::uint32_t); +int APS5_VABI _sceUltMutexCreate(void*, const char*, void*, const void*); int APS5_VABI sceUltMutexLock(void*); int APS5_VABI sceUltMutexUnlock(void*); -int APS5_VABI _sceUltQueueDataResourcePoolCreate(void*, const char*, std::uint32_t, std::uint64_t, std::uint32_t, void*, void*, const void*, std::uint32_t); -int APS5_VABI _sceUltQueueCreate(void*, const char*, std::uint64_t, void*, void*, const void*, std::uint32_t); +int APS5_VABI _sceUltQueueDataResourcePoolCreate(void*, const char*, std::uint32_t, std::uint64_t, std::uint32_t, void*, void*, const void*); +int APS5_VABI _sceUltQueueCreate(void*, const char*, std::uint64_t, void*, void*, const void*); int APS5_VABI sceUltQueuePush(void*, const void*); int APS5_VABI sceUltQueuePop(void*, void*); +int APS5_VABI sceUltMutexDestroy(void*); +int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void*); +int APS5_VABI sceUltSemaphoreDestroy(void*); +int APS5_VABI sceUltQueueDestroy(void*); +int APS5_VABI sceUltQueueDataResourcePoolDestroy(void*); +int APS5_VABI sceUltConditionVariableDestroy(void*); +int APS5_VABI sceUltReaderWriterLockDestroy(void*); +int APS5_VABI sceUltGetConditionVariableInfo(void*, UltConditionVariableInfo*, UltWaitingThreadInfo*, std::uint32_t); } static constexpr int Ok = 0; static constexpr int Null = -2139029503; +static constexpr int Alignment = -2139029502; +static constexpr int Invalid = -2139029500; +static constexpr int Permission = -2139029499; static constexpr int State = -2139029498; static constexpr int Busy = -2139029497; +static constexpr int Again = -2139029496; static void Require(bool value, int line) { if (!value) { @@ -99,27 +117,40 @@ static void Validation() { std::int32_t status = 0; CHECK(sceUltUlthreadJoin(&thread, &status) == Ok && status == 0x27); CHECK(sceUltFinalize() == Ok); - CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 256u + 16u * 1024u); + CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 1184); + CHECK(sceUltConditionVariableDestroy(nullptr) == -2139029503); + CHECK(sceUltReaderWriterLockDestroy(nullptr) == Null); + bool threwInfo = false; + try { + sceUltGetConditionVariableInfo(nullptr, nullptr, nullptr, 0); + } catch (const std::runtime_error&) { + threwInfo = true; + } + CHECK(threwInfo); } static void TryJoin() { + CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(0, 0) == 384); + CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 1184); + CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(3, 2) == 2128); + CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(0x80000000u, 0) == 0xA00000180ull); Runtime runtime; Thread thread; std::uint8_t optParam[128]; std::memset(optParam, 0xff, sizeof(optParam)); CHECK(_sceUltUlthreadRuntimeOptParamInitialize(nullptr, 0) == Null); CHECK(_sceUltUlthreadRuntimeOptParamInitialize(optParam, 0) == Ok && optParam[0] == 0); - CHECK(_sceUltUlthreadRuntimeCreate(&runtime, "runtime", 1, 1, nullptr, nullptr, 0) == Ok); - CHECK(_sceUltUlthreadCreate(&thread, "parked", Parked, 0x3C, nullptr, 0, &runtime, nullptr, 0) == Ok); + CHECK(_sceUltUlthreadRuntimeCreate(&runtime, "runtime", 1, 1, nullptr, nullptr) == Ok); + CHECK(_sceUltUlthreadCreate(&thread, "parked", Parked, 0x3C, nullptr, 0, &runtime, nullptr) == Ok); while (!gStarted.load(std::memory_order_acquire)) std::this_thread::yield(); std::int32_t status = 0; const int running = sceUltUlthreadTryJoin(&thread, &status); gRelease.store(true, std::memory_order_release); int joined; - while ((joined = sceUltUlthreadTryJoin(&thread, &status)) == Busy) std::this_thread::yield(); + while ((joined = sceUltUlthreadTryJoin(&thread, &status)) == Again) std::this_thread::yield(); - CHECK(running == Busy); + CHECK(running == Again); CHECK(joined == Ok && status == 0x3C); CHECK(sceUltUlthreadTryJoin(&thread, &status) == State); CHECK(sceUltUlthreadTryJoin(nullptr, &status) == Null); @@ -132,16 +163,16 @@ static void Destroy() { Thread thread; CHECK(sceUltUlthreadRuntimeDestroy(nullptr) == Null); CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == State); - CHECK(_sceUltUlthreadRuntimeCreate(&runtime, "runtime", 1, 1, nullptr, nullptr, 0) == Ok); - CHECK(_sceUltUlthreadRuntimeCreate(&other, "other", 1, 1, nullptr, nullptr, 0) == Ok); - CHECK(_sceUltUlthreadCreate(&thread, "thread", Immediate, 0x11, nullptr, 0, &runtime, nullptr, 0) == Ok); + CHECK(_sceUltUlthreadRuntimeCreate(&runtime, "runtime", 1, 1, nullptr, nullptr) == Ok); + CHECK(_sceUltUlthreadRuntimeCreate(&other, "other", 1, 1, nullptr, nullptr) == Ok); + CHECK(_sceUltUlthreadCreate(&thread, "thread", Immediate, 0x11, nullptr, 0, &runtime, nullptr) == Ok); CHECK(sceUltUlthreadRuntimeDestroy(&other) == Ok); - CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == Busy); + CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == State); std::int32_t status = 0; CHECK(sceUltUlthreadJoin(&thread, &status) == Ok && status == 0x11); CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == Ok); CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == State); - CHECK(_sceUltUlthreadCreate(&thread, "thread", Immediate, 0, nullptr, 0, &runtime, nullptr, 0) == State); + CHECK(_sceUltUlthreadCreate(&thread, "thread", Immediate, 0, nullptr, 0, &runtime, nullptr) == State); CHECK(sceUltFinalize() == Ok); } @@ -149,34 +180,125 @@ static void UnderscoredCreate() { alignas(8) static std::uint8_t pool[256]; alignas(8) static std::uint8_t semaphore[256]; alignas(8) static std::uint8_t unknownPool[256]; - CHECK(_sceUltWaitingQueueResourcePoolCreate(nullptr, "pool", 1, 1, nullptr, nullptr, 0) == Null); - CHECK(_sceUltWaitingQueueResourcePoolCreate(pool, "pool", 1, 1, nullptr, nullptr, 0) == Ok); - CHECK(_sceUltSemaphoreCreate(nullptr, "semaphore", 1, pool, nullptr, 0) == Null); - CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", -1, pool, nullptr, 0) != Ok); - CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, unknownPool, nullptr, 0) != Ok); - CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, pool, nullptr, 0) == Ok); - CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, pool, nullptr, 0) == State); + CHECK(_sceUltWaitingQueueResourcePoolCreate(nullptr, "pool", 1, 1, nullptr, nullptr) == Null); + alignas(8) std::uint8_t workArea[1024]{}; + alignas(8) std::uint32_t commonOptions[32]{}; + commonOptions[0] = 0x6C696275; + commonOptions[1] = 0x6C746D61; + commonOptions[2] = 0x67696300; + CHECK(_sceUltWaitingQueueResourcePoolCreate(pool, "pool", 1, 1, nullptr, nullptr) == Null); + CHECK(_sceUltWaitingQueueResourcePoolCreate(pool, "pool", 1, 1, workArea + 1, nullptr) == Alignment); + commonOptions[31] = 1; + CHECK(_sceUltWaitingQueueResourcePoolCreate(pool, "pool", 1, 1, workArea, commonOptions) == Invalid); + commonOptions[31] = 0; + CHECK(_sceUltWaitingQueueResourcePoolCreate(pool, "pool", 1, 1, workArea, commonOptions) == Ok); + CHECK(_sceUltSemaphoreCreate(nullptr, "semaphore", 1, pool, nullptr) == Null); + CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", UINT32_MAX, pool, nullptr) == Ok); + CHECK(sceUltSemaphoreTryAcquire(semaphore, 1) == Again); + CHECK(sceUltSemaphoreRelease(semaphore, 1) == Ok); + CHECK(sceUltSemaphoreTryAcquire(semaphore, 1) == Again); + CHECK(sceUltSemaphoreDestroy(semaphore) == Ok); + CHECK(sceUltSemaphoreAcquire(nullptr, 0) == Null); + CHECK(sceUltSemaphoreTryAcquire(nullptr, 0) == Null); + CHECK(sceUltSemaphoreRelease(nullptr, 0) == Null); + CHECK(sceUltSemaphoreAcquire(semaphore + 1, 0) == Alignment); + CHECK(sceUltSemaphoreTryAcquire(semaphore + 1, 0) == Alignment); + CHECK(sceUltSemaphoreRelease(semaphore + 1, 0) == Alignment); + CHECK(sceUltSemaphoreDestroy(semaphore + 1) == Alignment); + CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, unknownPool, nullptr) != Ok); + commonOptions[0] = 0; + CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, pool, commonOptions) == Invalid); + commonOptions[0] = 0x6C696275; + CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, pool, commonOptions) == Ok); + CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, pool, nullptr) == State); CHECK(sceUltSemaphoreAcquire(semaphore, 2) == Ok); CHECK(sceUltSemaphoreRelease(semaphore, 2) == Ok); - alignas(8) static std::uint8_t mutexOptParam[128]; + alignas(8) static std::uint8_t mutexOptParam[136]; alignas(8) static std::uint8_t mutex[256]; std::memset(mutexOptParam, 0xFF, sizeof(mutexOptParam)); CHECK(_sceUltMutexOptParamInitialize(nullptr, 0) == Null); + CHECK(_sceUltMutexOptParamInitialize(mutexOptParam + 1, 0) == Alignment); + CHECK(mutexOptParam[0] == 0xFF && mutexOptParam[127] == 0xFF); + CHECK(_sceUltMutexOptParamInitialize(mutexOptParam, 0x12345678) == Ok); + UltMutexOptParam options{}; + std::memcpy(&options, mutexOptParam, sizeof(options)); + CHECK(options.header[0] == 0x6C696275 && options.header[1] == 0x6C746D61 && options.header[2] == 0x67696300); + CHECK(options.buildVersion == 0x12345678 && options.attribute == 0); + for (std::size_t index = 16; index < 128; ++index) CHECK(mutexOptParam[index] == 0); + for (std::size_t index = 128; index < sizeof(mutexOptParam); ++index) CHECK(mutexOptParam[index] == 0xFF); + CHECK(_sceUltMutexCreate(mutex, nullptr, pool, mutexOptParam) == Null); + CHECK(_sceUltMutexCreate(mutex, "mutex", nullptr, mutexOptParam) == Null); + CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam + 1) == Alignment); + mutexOptParam[127] = 1; + CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam) == Invalid); + mutexOptParam[127] = 0; + mutexOptParam[16] = 2; + CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam) == Invalid); + mutexOptParam[16] = 0; + mutexOptParam[0] = 0; + CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam) == Invalid); CHECK(_sceUltMutexOptParamInitialize(mutexOptParam, 0) == Ok); - CHECK(mutexOptParam[8] == 0 && mutexOptParam[15] == 0); - CHECK(_sceUltMutexCreate(nullptr, "mutex", pool, mutexOptParam, 0) == Null); - CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam, 0) == Ok); + CHECK(_sceUltMutexCreate(nullptr, "mutex", pool, mutexOptParam) == Null); + CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam) == Ok); CHECK(sceUltMutexLock(mutex) == Ok); + CHECK(sceUltMutexLock(mutex) == State); + std::thread foreignUnlock([&] { CHECK(sceUltMutexUnlock(mutex) == Permission); }); + foreignUnlock.join(); + CHECK(sceUltMutexUnlock(mutex) == Ok); + CHECK(sceUltMutexUnlock(mutex) == Permission); + CHECK(sceUltMutexDestroy(mutex) == Ok); + mutexOptParam[16] = 1; + CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam) == Ok); + CHECK(sceUltMutexLock(mutex) == Ok); + CHECK(sceUltMutexLock(mutex) == Ok); + CHECK(sceUltMutexUnlock(mutex) == Ok); + CHECK(sceUltMutexDestroy(mutex) == State); CHECK(sceUltMutexUnlock(mutex) == Ok); alignas(8) static std::uint8_t dataPool[512]; alignas(8) static std::uint8_t queue[512]; - CHECK(_sceUltQueueDataResourcePoolCreate(nullptr, "data", 1, 8, 1, pool, nullptr, nullptr, 0) == Null); - CHECK(_sceUltQueueDataResourcePoolCreate(dataPool, "data", 1, 8, 1, pool, nullptr, nullptr, 0) == Ok); - CHECK(_sceUltQueueCreate(nullptr, "queue", 8, pool, dataPool, nullptr, 0) == Null); - CHECK(_sceUltQueueCreate(queue, "queue", 8, pool, dataPool, nullptr, 0) == Ok); + CHECK(_sceUltQueueDataResourcePoolCreate(nullptr, "data", 1, 8, 1, pool, nullptr, nullptr) == Null); + CHECK(_sceUltQueueDataResourcePoolCreate(dataPool, "data", 1, 8, 1, pool, nullptr, nullptr) == Null); + commonOptions[4] = 1; + CHECK(_sceUltQueueDataResourcePoolCreate(dataPool, "data", 1, 8, 1, pool, workArea, commonOptions) == Invalid); + commonOptions[4] = 0; + CHECK(_sceUltQueueDataResourcePoolCreate(dataPool, "data", 1, 8, 1, pool, workArea, commonOptions) == Ok); + CHECK(_sceUltQueueCreate(nullptr, "queue", 8, pool, dataPool, nullptr) == Null); + commonOptions[31] = 1; + CHECK(_sceUltQueueCreate(queue, "queue", 8, pool, dataPool, commonOptions) == Invalid); + commonOptions[31] = 0; + CHECK(_sceUltQueueCreate(queue, "queue", 8, pool, dataPool, reinterpret_cast(commonOptions) + 1) == Alignment); + CHECK(_sceUltQueueCreate(queue, "queue", 8, pool, dataPool, commonOptions) == Ok); std::uint64_t value = 0x1122334455667788ull, output = 0; CHECK(sceUltQueuePush(queue, &value) == Ok); CHECK(sceUltQueuePop(queue, &output) == Ok && output == value); + CHECK(sceUltWaitingQueueResourcePoolDestroy(nullptr) == Null); + CHECK(sceUltWaitingQueueResourcePoolDestroy(unknownPool) == State); + CHECK(sceUltMutexDestroy(nullptr) == Null); + CHECK(sceUltMutexLock(mutex) == Ok); + CHECK(sceUltMutexDestroy(mutex) == State); + std::atomic waiterStarted{false}; + std::atomic waiterAcquired{false}; + std::thread waiter([&] { + waiterStarted.store(true, std::memory_order_release); + CHECK(sceUltMutexLock(mutex) == Ok); + waiterAcquired.store(true, std::memory_order_release); + CHECK(sceUltMutexUnlock(mutex) == Ok); + }); + while (!waiterStarted.load(std::memory_order_acquire)) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + CHECK(!waiterAcquired.load(std::memory_order_acquire)); + CHECK(sceUltMutexDestroy(mutex) == State); + CHECK(sceUltWaitingQueueResourcePoolDestroy(pool) == State); + CHECK(sceUltMutexUnlock(mutex) == Ok); + waiter.join(); + CHECK(waiterAcquired.load(std::memory_order_acquire)); + CHECK(sceUltWaitingQueueResourcePoolDestroy(pool) == Ok); + CHECK(sceUltWaitingQueueResourcePoolDestroy(pool) == State); + CHECK(sceUltMutexDestroy(mutex) == Ok); + CHECK(sceUltMutexDestroy(mutex) == State); + CHECK(sceUltSemaphoreDestroy(semaphore) == Ok); + CHECK(sceUltQueueDestroy(queue) == Ok); + CHECK(sceUltQueueDataResourcePoolDestroy(dataPool) == Ok); } int main(int argc, char** argv) { diff --git a/core/libs/tests/GuestUmtxOp.cpp b/core/libs/tests/GuestUmtxOp.cpp index 48ec61d508..97eef1d837 100644 --- a/core/libs/tests/GuestUmtxOp.cpp +++ b/core/libs/tests/GuestUmtxOp.cpp @@ -20,6 +20,8 @@ static constexpr int UMTX_OP_MUTEX_LOCK = 5; static constexpr int UMTX_OP_WAIT_UINT = 11; static constexpr int UMTX_OP_WAIT_UINT_PRIVATE = 15; static constexpr int UMTX_OP_WAKE_PRIVATE = 16; +static constexpr int UMTX_OP_SEM_WAIT = 19; +static constexpr int UMTX_OP_SEM_WAKE = 20; static constexpr int UMTX_OP_NWAKE_PRIVATE = 21; static constexpr int UMTX_OP_SEM2_WAIT = 23; static constexpr int UMTX_OP_MAX = 27; @@ -123,19 +125,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 +149,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 +174,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 +230,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); @@ -269,6 +272,61 @@ static void NwakeReadsOnlyTheCountedEntries() { Require(Umtx(addresses, UMTX_OP_NWAKE_PRIVATE, 1) == 0); } +struct GuestSemaphore { + std::uint32_t hasWaiters; + std::uint32_t count; + std::uint32_t flags; +}; + +static void SemWaitReturnsAtOnceWhileTheCountIsNonzero() { + GuestSemaphore sem{0, 2, 0}; + KernelTimespec none{0, 0}; + Require(Umtx(&sem, UMTX_OP_SEM_WAIT, 0, SizeOf(sizeof(none)), &none) == 0); + Require(sem.hasWaiters == 1); + Require(sem.count == 2); +} + +static void SemWaitTimesOutWhileTheCountIsZero() { + GuestSemaphore sem{0, 0, 0}; + KernelTimespec relative{0, 20000000}; + auto start = std::chrono::steady_clock::now(); + Require(FailsWith(Umtx(&sem, UMTX_OP_SEM_WAIT, 0, SizeOf(sizeof(relative)), &relative), GUEST_ETIMEDOUT)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); + Require(sem.hasWaiters == 1); +} + +static void SemWakeReleasesTheWaiterAndClearsTheFlag() { + GuestSemaphore sem{0, 0, 0}; + std::atomic finished{0}; + int result = -2; + std::thread waiter([&] { + KernelTimespec failsafe = FailsafeTimeout(); + result = Umtx(&sem, UMTX_OP_SEM_WAIT, 0, SizeOf(sizeof(failsafe)), &failsafe); + ++finished; + }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (std::atomic_ref(sem.hasWaiters).load() == 0) { + Require(std::chrono::steady_clock::now() < deadline); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + std::this_thread::sleep_for(SETTLE); + Require(finished.load() == 0); + + std::atomic_ref(sem.count).store(1); + Require(Umtx(&sem, UMTX_OP_SEM_WAKE, 0) == 0); + waiter.join(); + Require(result == 0); + Require(sem.hasWaiters == 0); +} + +static void SemWakeWithoutWaitersSucceeds() { + GuestSemaphore sem{0, 0, 0}; + Require(Umtx(&sem, UMTX_OP_SEM_WAKE, 0) == 0); + Require(sem.hasWaiters == 0); + Require(FailsWith(Umtx(nullptr, UMTX_OP_SEM_WAKE, 0), GUEST_EFAULT)); + Require(FailsWith(Umtx(nullptr, UMTX_OP_SEM_WAIT, 0), GUEST_EFAULT)); +} + static void RejectsBadObjectsAndOperations() { Require(FailsWith(Umtx(nullptr, UMTX_OP_WAIT, 0), GUEST_EFAULT)); Require(FailsWith(Umtx(nullptr, UMTX_OP_WAIT_UINT, 0), GUEST_EFAULT)); @@ -280,11 +338,14 @@ static void RejectsBadObjectsAndOperations() { Require(FailsWith(Umtx(&value, UMTX_OP_MAX, 0), GUEST_EINVAL)); Require(FailsWith(Umtx(&value, -1, 0), GUEST_EINVAL)); + for (const std::uint64_t owner : {std::uint64_t{0}, std::uint64_t{123456}, (std::uint64_t{1} << 63) | 123456}) { + std::uint64_t word = owner; + Require(FailsWith(Umtx(&word, 1, 123456), GUEST_EOPNOTSUPP)); + Require(FailsWith(Umtx(&word, 0, 123456), GUEST_EOPNOTSUPP)); + Require(word == owner); + } + bool threw = false; - try { Umtx(&value, 1, 0); } - catch (const std::runtime_error&) { threw = true; } - Require(threw); - threw = false; try { Umtx(&value, UMTX_OP_MUTEX_LOCK, 0); } catch (const std::runtime_error&) { threw = true; } Require(threw); @@ -304,5 +365,9 @@ int main() { WakeWithoutWaitersSucceeds(); NwakeWakesEveryWaiterOnEachListedAddress(); NwakeReadsOnlyTheCountedEntries(); + SemWaitReturnsAtOnceWhileTheCountIsNonzero(); + SemWaitTimesOutWhileTheCountIsZero(); + SemWakeReleasesTheWaiterAndClearsTheFlag(); + SemWakeWithoutWaitersSucceeds(); RejectsBadObjectsAndOperations(); } diff --git a/core/libs/tests/GuestUsbStorageDialog.cpp b/core/libs/tests/GuestUsbStorageDialog.cpp new file mode 100644 index 0000000000..e21bcc50eb --- /dev/null +++ b/core/libs/tests/GuestUsbStorageDialog.cpp @@ -0,0 +1,132 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include "ProcessParametersFixture.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" +#include "prx/libSceUserService/UserService.hpp" + +extern "C" { +int APS5_VABI sceUserServiceInitialize(const UserServiceInitParam* param); +int APS5_VABI sceUserServiceTerminate(); +int APS5_VABI sceUsbStorageDialogInitialize(void); +int APS5_VABI sceUsbStorageDialogOpen(const UsbStorageDialogParam* param); +int APS5_VABI sceUsbStorageDialogUpdateStatus(void); +int APS5_VABI sceUsbStorageDialogGetStatus(void); +int APS5_VABI sceUsbStorageDialogGetResult(UsbStorageDialogResult* result); +int APS5_VABI sceUsbStorageDialogClose(void); +int APS5_VABI sceUsbStorageDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusRunning = 2; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrBusy = static_cast(0x80B80007); +constexpr int kErrNotRunning = static_cast(0x80B8000B); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + static std::array process{}; + Require(ProcessParametersFixture::Install(process.data(), process.size())); + std::uint32_t sdkVersion = 0x04500000; + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + Require(sceUserServiceInitialize(nullptr) == 0); + UsbStorageDialogParam param{}; + param.base.size = sizeof(param.base); + param.base.magic = static_cast(reinterpret_cast(¶m)) - 1060003575u; + param.size = sizeof(param); + param.userId = USER_SERVICE_INITIAL_USER_ID; + std::strcpy(param.path, "test"); + UsbStorageDialogResult result; + std::memset(&result, 0x5a, sizeof(result)); + result.size = sizeof(result); + result.result = kErrBusy; + + Require(sceUsbStorageDialogTerminate() == kErrNotInitialized); + Require(sceUsbStorageDialogOpen(¶m) == kErrNotInitialized); + Require(sceUsbStorageDialogClose() == kErrNotInitialized); + Require(sceUsbStorageDialogGetResult(&result) == kErrNotInitialized); + Require(sceUsbStorageDialogGetStatus() == kStatusNone); + Require(sceUsbStorageDialogUpdateStatus() == kStatusNone); + + Require(sceUsbStorageDialogInitialize() == 0); + Require(sceUsbStorageDialogInitialize() == kErrAlreadyInitialized); + Require(sceUsbStorageDialogGetStatus() == kStatusInitialized); + + Require(sceUsbStorageDialogOpen(nullptr) == kErrArgNull); + Require(sceUsbStorageDialogGetResult(&result) == kErrNotFinished); + + param.path[1023] = 'a'; + Require(sceUsbStorageDialogOpen(¶m) == static_cast(0x80B8000Au)); + param.path[1023] = 0; + std::strcpy(param.path, "a..b"); + Require(sceUsbStorageDialogOpen(¶m) == static_cast(0x812D0003u)); + for (const char invalid : std::string(",*?\"<>|")) { + param.path[0] = invalid; + param.path[1] = 0; + Require(sceUsbStorageDialogOpen(¶m) == static_cast(0x812D0003u)); + } + std::strcpy(param.path, "test"); + Require(sceUsbStorageDialogOpen(¶m) == 0); + Require(sceUsbStorageDialogOpen(¶m) == kErrBusy); + Require(sceUsbStorageDialogGetStatus() == kStatusRunning); + Require(sceUsbStorageDialogGetResult(&result) == kErrNotFinished); + + Require(sceUsbStorageDialogUpdateStatus() == kStatusFinished); + Require(sceUsbStorageDialogGetStatus() == kStatusFinished); + + Require(sceUsbStorageDialogGetResult(nullptr) == kErrArgNull); + result.size = 79; + Require(sceUsbStorageDialogGetResult(&result) == static_cast(0x80B8000Au)); + result.size = sizeof(result); + result.result = 0; + Require(sceUsbStorageDialogGetResult(&result) == static_cast(0x80B8000Au)); + result.result = kErrBusy; + Require(sceUsbStorageDialogGetResult(&result) == kResultUserCanceled); + Require(result.result == kResultUserCanceled); + Require(result.size == sizeof(result) && result.status == 0x5a5a5a5a); + for (const auto value : result.reserved) Require(value == 0x5a); + + Require(sceUsbStorageDialogClose() == kErrNotRunning); + Require(sceUsbStorageDialogTerminate() == 0); + Require(sceUsbStorageDialogTerminate() == kErrNotInitialized); + Require(sceUsbStorageDialogGetStatus() == kStatusNone); + + Require(sceUsbStorageDialogInitialize() == 0); + Require(sceUsbStorageDialogOpen(¶m) == 0); + Require(sceUsbStorageDialogGetStatus() == kStatusRunning); + Require(sceUsbStorageDialogClose() == 0); + Require(sceUsbStorageDialogGetStatus() == kStatusFinished); + result.result = 0; + Require(sceUsbStorageDialogGetResult(&result) == 0); + Require(result.result == kErrNotFinished && result.status == 0); + Require(sceUsbStorageDialogTerminate() == 0); + sdkVersion = 0x04400000; + std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); + Require(sceUsbStorageDialogInitialize() == 0); + std::strcpy(param.path, "a..b"); + Require(sceUsbStorageDialogOpen(¶m) == 0); + Require(sceUsbStorageDialogUpdateStatus() == kStatusFinished); + result.size = 0; + result.result = 0; + Require(sceUsbStorageDialogGetResult(&result) == kResultUserCanceled); + Require(result.size == 0 && result.result == kResultUserCanceled); + Require(sceUsbStorageDialogTerminate() == 0); + Require(sceUsbStorageDialogOpen(nullptr) == kErrArgNull); + Require(sceUsbStorageDialogGetResult(nullptr) == kErrArgNull); + Require(sceUserServiceTerminate() == 0); + return 0; +} diff --git a/core/libs/tests/GuestUsbd.cpp b/core/libs/tests/GuestUsbd.cpp index 59e59542b0..358e369eea 100644 --- a/core/libs/tests/GuestUsbd.cpp +++ b/core/libs/tests/GuestUsbd.cpp @@ -50,6 +50,7 @@ void APS5_VABI sceUsbdFillInterruptTransfer(void* transfer, void* deviceHandle, std::int32_t APS5_VABI sceUsbdCancelTransfer(void* transfer); std::int32_t APS5_VABI sceUsbdEventHandlingOk(); void APS5_VABI sceUsbdFreeConfigDescriptor(void* config); +void APS5_VABI sceUsbdFillControlSetup(std::uint8_t* buffer, std::uint8_t requestType, std::uint8_t request, std::uint16_t value, std::uint16_t index, std::uint16_t length); } namespace { @@ -293,6 +294,13 @@ int main(int argc, char** argv) { sceUsbdFreeTransfer(nullptr); Require(sceUsbdEventHandlingOk() == 1, "event handling not allowed"); sceUsbdFreeConfigDescriptor(nullptr); + Require(Throws([] { sceUsbdFillControlSetup(nullptr, 0x80, 6, 0x0100, 0, 18); }), "null control setup buffer accepted"); + std::uint8_t setupBuffer[8] = {}; + sceUsbdFillControlSetup(setupBuffer, 0x80, 6, 0x0100, 0x0409, 18); + Require(setupBuffer[0] == 0x80 && setupBuffer[1] == 6, "request type or request not filled"); + Require(setupBuffer[2] == 0x00 && setupBuffer[3] == 0x01, "value not filled little-endian"); + Require(setupBuffer[4] == 0x09 && setupBuffer[5] == 0x04, "index not filled little-endian"); + Require(setupBuffer[6] == 18 && setupBuffer[7] == 0, "length not filled little-endian"); if (argc == 2 && std::strcmp(argv[1], "device") == 0) Device(); sceUsbdExit(); std::puts("USBD tests passed"); diff --git a/core/libs/tests/GuestUserContext.cpp b/core/libs/tests/GuestUserContext.cpp index 304b77b34d..c616e2260d 100644 --- a/core/libs/tests/GuestUserContext.cpp +++ b/core/libs/tests/GuestUserContext.cpp @@ -149,6 +149,7 @@ Stack g_jobStack; Stack g_exitStack; std::uint64_t g_arguments[6] = {}; int g_entries = 0; +bool g_contextChainEnded = false; Fibre g_dispatcher = {}; Fibre g_job = {}; int g_jobRuns = 0; @@ -193,6 +194,23 @@ void ThrowAndCatch() { Require(caught, "a host exception is caught on a context stack"); } +[[gnu::noinline]] void WalkContextChain() { + auto* frame = static_cast(__builtin_frame_address(0)); + const auto low = reinterpret_cast(g_contextStack.bytes); + const auto high = low + StackBytes; + for (int depth = 0; depth < 100; ++depth) { + const auto address = reinterpret_cast(frame); + if (address < low || address + 2 * sizeof(std::uintptr_t) > high) return; + if (frame[1] == 0) { + g_contextChainEnded = frame[0] == 0; + return; + } + auto* next = reinterpret_cast(frame[0]); + if (reinterpret_cast(next) <= address) return; + frame = next; + } +} + [[gnu::noinline]] void GrowStack(int depth) { volatile unsigned char page[16 * 1024]; page[0] = static_cast(depth); @@ -341,6 +359,7 @@ extern "C" void APS5_VABI ContextBody(std::uint64_t first, std::uint64_t second, g_arguments[4] = fifth; g_arguments[5] = sixth; CheckStackBounds(&g_contextStack, "the stack bounds follow setcontext onto the context stack"); + WalkContextChain(); ThrowAndCatch(); ++g_entries; } @@ -401,6 +420,7 @@ void CheckMakecontext() { Require(false, "setcontext into the made context returned"); } Require(g_entries == 1, "the made context runs once and resumes uc_link"); + Require(g_contextChainEnded, "the frame chain of a made context ends on its stack at a frame whose saved frame pointer and return address are 0"); for (unsigned index = 0; index < 6; ++index) Require(g_arguments[index] == 0x8000000000000001ull + index, "makecontext passes six 64-bit arguments"); const auto stackLow = reinterpret_cast(g_contextStack.bytes); Require(EntryStackPointer > stackLow && EntryStackPointer < stackLow + StackBytes, "the made context runs on its own stack"); diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 83d7afc731..3e6d7559da 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -2,17 +2,22 @@ #include "prx/libSceUserService/UserService.hpp" #include #include +#include extern "C" { +int APS5_VABI sceUserServiceGetUserNumber(int user_id, int32_t* number); +int APS5_VABI sceUserServiceGetPlatformPrivacyWs1(int32_t user_id, int32_t* value); int APS5_VABI sceUserServiceGetForegroundUser(int* user_id); int APS5_VABI sceUserServiceGetRegisteredUserIdList(UserServiceRegisteredUserIdList* user_id_list); 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); } @@ -25,6 +30,17 @@ void Require(bool value) { if (!value) std::abort(); } int main() { Require(sceUserServiceGetForegroundUser(nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + int32_t userNumber = -1; + Require(sceUserServiceGetUserNumber(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserNumber(123, &userNumber) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserNumber(USER_SERVICE_INITIAL_USER_ID, &userNumber) == USER_SERVICE_OK); + Require(userNumber == 1); + + int32_t privacy = -1; + Require(sceUserServiceGetPlatformPrivacyWs1(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetPlatformPrivacyWs1(USER_SERVICE_INITIAL_USER_ID, &privacy) == USER_SERVICE_OK); + Require(privacy == 0); + int foreground = -1; Require(sceUserServiceGetForegroundUser(&foreground) == USER_SERVICE_OK); Require(foreground == USER_SERVICE_INITIAL_USER_ID); @@ -83,4 +99,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/GuestUserServiceLifecycle.cpp b/core/libs/tests/GuestUserServiceLifecycle.cpp new file mode 100644 index 0000000000..550acb5353 --- /dev/null +++ b/core/libs/tests/GuestUserServiceLifecycle.cpp @@ -0,0 +1,88 @@ +#include "SceTypes.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceUserServiceInitialize(const UserServiceInitParam* params); +int APS5_VABI sceUserServiceInitialize2(std::int32_t priority, KernelCpumask affinityMask); +int APS5_VABI sceUserServiceTerminate(void); +int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* user_id_list); +int APS5_VABI sceUserServiceGetEvent(SceUserServiceEvent* event); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +void RequireNotInitialized() { + UserServiceLoginUserIdList list{}; + Require(sceUserServiceGetLoginUserIdList(&list) == USER_SERVICE_ERROR_NOT_INITIALIZED); + SceUserServiceEvent event{}; + std::memset(&event, 0xAA, sizeof(event)); + Require(sceUserServiceGetEvent(&event) == USER_SERVICE_ERROR_NOT_INITIALIZED); + Require(event.user_id == static_cast(0xAAAAAAAAu)); + Require(sceUserServiceGetEvent(nullptr) == USER_SERVICE_ERROR_NOT_INITIALIZED); +} + +void RequireLoginOnce() { + SceUserServiceEvent event{}; + Require(sceUserServiceGetEvent(&event) == USER_SERVICE_OK); + Require(event.event_type == 0 && event.user_id == USER_SERVICE_INITIAL_USER_ID); + Require(sceUserServiceGetEvent(&event) == USER_SERVICE_ERROR_NO_EVENT); +} + +int InitializeWithPriority(int priority) { + const UserServiceInitParam param{priority}; + return sceUserServiceInitialize(¶m); +} + +void TestThreadPriority() { + for (int priority : {0, 0xFF, 0x300, -1}) { + Require(InitializeWithPriority(priority) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceInitialize2(priority, 1) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + } + RequireNotInitialized(); + + Require(sceUserServiceInitialize2(0x100, 1) == USER_SERVICE_OK); + RequireLoginOnce(); + for (int priority : {0x100, 700, 0x2FF}) { + Require(InitializeWithPriority(priority) == USER_SERVICE_ERROR_ALREADY_INITIALIZED); + Require(sceUserServiceInitialize2(priority, 1) == USER_SERVICE_ERROR_ALREADY_INITIALIZED); + } + for (int priority : {0, 0xFF, 0x300, -1}) { + Require(InitializeWithPriority(priority) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceInitialize2(priority, 1) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + } + Require(sceUserServiceInitialize(nullptr) == USER_SERVICE_ERROR_ALREADY_INITIALIZED); + Require(sceUserServiceTerminate() == USER_SERVICE_OK); + + Require(InitializeWithPriority(0x2FF) == USER_SERVICE_OK); + RequireLoginOnce(); + Require(sceUserServiceTerminate() == USER_SERVICE_OK); +} + +} + +int main() { + RequireNotInitialized(); + Require(sceUserServiceTerminate() == USER_SERVICE_ERROR_NOT_INITIALIZED); + + Require(sceUserServiceInitialize(nullptr) == USER_SERVICE_OK); + Require(sceUserServiceInitialize(nullptr) == USER_SERVICE_ERROR_ALREADY_INITIALIZED); + UserServiceLoginUserIdList list{}; + Require(sceUserServiceGetLoginUserIdList(&list) == USER_SERVICE_OK); + Require(list.user_id[0] == USER_SERVICE_INITIAL_USER_ID); + RequireLoginOnce(); + + Require(sceUserServiceTerminate() == USER_SERVICE_OK); + Require(sceUserServiceTerminate() == USER_SERVICE_ERROR_NOT_INITIALIZED); + RequireNotInitialized(); + + Require(sceUserServiceInitialize(nullptr) == USER_SERVICE_OK); + RequireLoginOnce(); + Require(sceUserServiceTerminate() == USER_SERVICE_OK); + + TestThreadPriority(); +} diff --git a/core/libs/tests/GuestVdecsw.cpp b/core/libs/tests/GuestVdecsw.cpp index 99a11fe967..6bb929a1e0 100644 --- a/core/libs/tests/GuestVdecsw.cpp +++ b/core/libs/tests/GuestVdecsw.cpp @@ -2,11 +2,14 @@ #include #include #include +#include #include #include #include #include "prx/libc/include/General.hpp" +#include "Vp9Fixture.hpp" #include "H264Fixture.hpp" +#include "HevcFixture.hpp" struct ComputeMemoryInfo { std::uint64_t thisSize; @@ -89,6 +92,28 @@ struct OutputInfo { std::uint32_t framePitchInBytes; }; +struct Vp9PictureInfo { + std::uint64_t thisSize; + bool isValid; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; + std::uint32_t profile; + std::uint32_t level; + std::uint32_t keyFrameFlag; + std::uint32_t intraOnly; + std::uint8_t bitDepth; + std::uint8_t colorRange; + std::uint32_t colorSpace; + std::uint32_t numFrames; + std::uint32_t frameWidth; + std::uint32_t frameHeight; + std::uint32_t renderWidth; + std::uint32_t renderHeight; +}; + +static_assert(sizeof(Vp9PictureInfo) == 0x58 && offsetof(Vp9PictureInfo, ptsData) == 0x10 && offsetof(Vp9PictureInfo, profile) == 0x28 && offsetof(Vp9PictureInfo, bitDepth) == 0x38 && offsetof(Vp9PictureInfo, colorSpace) == 0x3c && offsetof(Vp9PictureInfo, renderHeight) == 0x50); + struct AvcPictureInfo { std::uint64_t thisSize; bool isValid; @@ -109,6 +134,51 @@ struct AvcPictureInfo { std::uint8_t vui[44]; }; +struct HevcPictureInfo { + std::uint64_t thisSize; + bool isValid; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; + std::uint32_t picWidthInLumaSamples; + std::uint32_t picHeightInLumaSamples; + std::uint8_t bitDepthLumaMinus8; + std::uint8_t bitDepthChromaMinus8; + std::uint8_t timingInfoPresentFlag; + std::uint32_t numUnitsInTick; + std::uint32_t timeScale; + std::uint32_t aspectRatioInfoPresentFlag; + std::uint8_t aspectRatioIdc; + std::uint16_t sarWidth; + std::uint16_t sarHeight; + std::uint8_t videoSignal[8]; + std::uint32_t picStruct; + std::uint32_t sourceScanType; + std::uint32_t duplicateFlag; + std::uint32_t conformanceWindowFlag; + std::uint32_t confWinLeftOffset; + std::uint32_t confWinRightOffset; + std::uint32_t confWinTopOffset; + std::uint32_t confWinBottomOffset; + std::uint32_t defaultDisplayWindow[5]; + std::uint8_t chromaLocation[4]; + std::uint8_t presentFlags[11]; + std::uint8_t idrPictureFlag; + std::uint8_t irapPictureFlag; + std::uint8_t generalProfileSpace; + std::uint8_t generalTierFlag; + std::uint8_t generalProfileIdc; + std::uint8_t generalSourceFlags[3]; + std::uint8_t generalLevelIdc; + std::uint8_t subLayer[9]; + std::uint8_t frameCroppingFlag; + std::uint32_t frameCropLeftOffset; + std::uint32_t frameCropRightOffset; + std::uint32_t frameCropTopOffset; + std::uint32_t frameCropBottomOffset; +}; + +static_assert(sizeof(HevcPictureInfo) == 0xb8 && offsetof(HevcPictureInfo, idrPictureFlag) == 0x93 && offsetof(HevcPictureInfo, frameCroppingFlag) == 0xa5); static_assert(sizeof(DecoderConfigInfo) == 0x50 && sizeof(OutputInfo) == 0x38 && offsetof(OutputInfo, isLastFrame) == 9 && offsetof(OutputInfo, pictureCount) == 0xb && sizeof(AvcPictureInfo) == 0x78); extern "C" { @@ -120,9 +190,13 @@ 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 sceVdecswGetVp9PictureInfo(const OutputInfo*, Vp9PictureInfo*); int APS5_VABI sceVdecswGetAvcPictureInfo(const OutputInfo*, AvcPictureInfo*, AvcPictureInfo*); +int APS5_VABI sceVdecswGetHevcPictureInfo(const OutputInfo*, HevcPictureInfo*); int APS5_VABI sceVdecswFinalizeDecodeSequence(std::uint64_t); int APS5_VABI sceVdecswResetDecoder(std::uint64_t); } @@ -130,9 +204,15 @@ int APS5_VABI sceVdecswResetDecoder(std::uint64_t); namespace { using namespace H264Fixture; +using namespace HevcFixture; +using namespace Vp9Fixture; constexpr int OutputPending = static_cast(0x81510115u); constexpr int InputQueueEmpty = static_cast(0x81510116u); +constexpr std::uint32_t CodecAvc = 1; +constexpr std::uint32_t CodecHevc = 974921; +constexpr std::uint32_t CodecVp9 = 2382845; +constexpr std::array HevcDisplayOrderUnits{0, 3, 2, 1, 5, 4}; void check(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); @@ -143,12 +223,12 @@ struct Session { std::uint64_t decoder = 0; DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; - Session() { + explicit Session(std::uint32_t codec = CodecAvc, std::uint32_t profile = 100, std::uint32_t level = 42) { ComputeMemoryInfo compute{sizeof(ComputeMemoryInfo)}; check(sceVdecswQueryComputeMemoryInfo(&compute) == 0 && compute.cpuGpuMemorySize != 0, "compute memory query failed"); const ComputeConfigInfo computeConfig{sizeof(ComputeConfigInfo), 3, 3, true}; check(sceVdecswAllocateComputeQueue(&computeConfig, &compute, &queue) == 0, "compute queue allocation failed"); - DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, 1, 100, 42, static_cast(Width), static_cast(Height), 4, 3, queue, 0x3f, 0, true, {}}; + DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, codec, profile, level, static_cast(Width), static_cast(Height), 4, 3, queue, 0x3f, 0, true, {}}; check(sceVdecswQueryDecoderMemoryInfo(&config, &memory) == 0 && memory.maxFrameBufferSize != 0, "decoder memory query failed"); check(sceVdecswCreateDecoder(&config, &memory, &decoder) == 0, "decoder creation failed"); } @@ -188,13 +268,13 @@ void testDecode() { } check(pictures < PictureHashes.size(), "more pictures than access units"); check(output.isLastFrame == (pictures + 1 == PictureHashes.size()), "only the last picture of the finalized sequence is marked last"); - check(!output.isErrorFrame && !output.isDiscardedFrame && output.pictureCount == 1 && output.frameWidth == Width && output.frameHeight == Height && output.frameBuffer == buffers[pictures % 2].data(), "unexpected picture geometry"); + check(!output.isErrorFrame && !output.isDiscardedFrame && output.pictureCount == 1 && output.frameWidth == Width && output.frameHeight == CodedHeight && output.frameBuffer == buffers[pictures % 2].data(), "unexpected picture geometry"); AvcPictureInfo info{sizeof(AvcPictureInfo)}; check(sceVdecswGetAvcPictureInfo(&output, &info, nullptr) == 0 && info.isValid, "picture info missing"); const auto unit = DisplayOrderUnits[pictures]; check(info.ptsData == 1000 + unit && info.dtsData == unit && info.attachedData == 0xa0 + unit && info.idrPictureFlag == (pictures == 0 ? 1 : 0) && info.profileIdc == 100, "pictures out of display order"); - check(info.picWidthInMbsMinus1 == Width / 16 - 1 && info.picHeightInMapUnitsMinus1 == (Height + 15) / 16 - 1 && info.frameMbsOnlyFlag == 1 && info.frameCroppingFlag == 1 && info.frameCropRightOffset == 0 && info.frameCropBottomOffset == ((Height + 15) / 16 * 16 - Height) / 2, "picture geometry in the picture info is wrong"); - check(hashNv12(static_cast(output.frameBuffer), output.framePitch) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference"); + check(info.picWidthInMbsMinus1 == Width / 16 - 1 && info.picHeightInMapUnitsMinus1 == CodedHeight / 16 - 1 && info.frameMbsOnlyFlag == 1 && info.frameCroppingFlag == 1 && info.frameCropLeftOffset == 0 && info.frameCropRightOffset == 0 && info.frameCropTopOffset == 0 && info.frameCropBottomOffset == (CodedHeight - Height) / 2, "picture geometry in the picture info is wrong"); + check(hashNv12(static_cast(output.frameBuffer), output.framePitch, output.frameHeight) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference"); ++pictures; } }; @@ -215,6 +295,76 @@ void testDecode() { check(sceVdecswTrySyncDecodeInput(session.decoder, &result) == InputQueueEmpty, "a finalized decoder did not report an empty input queue"); } +void testHevc() { + Session session(CodecHevc, 1, 30); + const auto units = hevcUnits(HevcStream.data(), HevcStream.size()); + check(units.size() == HevcHashes.size(), "unexpected HEVC access unit count"); + std::vector buffer(session.memory.maxFrameBufferSize); + std::size_t pictures = 0; + bool ended = false; + const auto drain = [&] { + for (;;) { + const FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size()}; + check(sceVdecswSetDecodeOutput(session.decoder, &frame) == 0, "HEVC output buffer was rejected"); + OutputInfo output{sizeof(OutputInfo)}; + const auto status = sceVdecswTrySyncDecodeOutput(session.decoder, &output); + if (status == OutputPending) return; + check(status == 0, "HEVC output sync failed"); + if (!output.isValid) { + ended = true; + return; + } + check(pictures < HevcHashes.size() && output.codecType == CodecHevc && output.pictureCount == 1 && output.frameWidth == Width && output.frameHeight == Height, "unexpected HEVC picture geometry"); + HevcPictureInfo info{sizeof(HevcPictureInfo)}; + check(sceVdecswGetHevcPictureInfo(&output, &info) == 0 && info.isValid, "HEVC picture info missing"); + const auto unit = HevcDisplayOrderUnits[pictures]; + check(info.ptsData == 1000 + unit && info.dtsData == unit && info.attachedData == 0xa0 + unit, "HEVC pictures out of display order"); + check(info.idrPictureFlag == (pictures == 0 ? 1 : 0) && info.irapPictureFlag == (pictures == 0 ? 1 : 0), "HEVC random access flags differ"); + check(info.generalProfileIdc == 1 && info.generalLevelIdc == 30 && info.bitDepthLumaMinus8 == 0 && info.bitDepthChromaMinus8 == 0, "HEVC profile, level or bit depth differs"); + check(info.picWidthInLumaSamples == Width && info.picHeightInLumaSamples == Height && info.conformanceWindowFlag == 0 && info.frameCroppingFlag == 0, "HEVC picture geometry in the picture info is wrong"); + AvcPictureInfo avc{sizeof(AvcPictureInfo)}; + bool avcThrows = false; + try { + sceVdecswGetAvcPictureInfo(&output, &avc, nullptr); + } catch (const std::exception&) { + avcThrows = true; + } + check(avcThrows, "AVC picture info was returned for an HEVC picture"); + check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == HevcHashes[pictures], "HEVC picture " + std::to_string(pictures) + " differs from the reference"); + ++pictures; + } + }; + 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}; + check(sceVdecswSetDecodeInput(session.decoder, &input) == 0, "HEVC decode input was rejected"); + InputResult consumed{sizeof(InputResult)}; + check(sceVdecswTrySyncDecodeInput(session.decoder, &consumed) == 0 && consumed.decodedAu == units[unit].data(), "an HEVC access unit was not consumed"); + drain(); + } + check(sceVdecswFinalizeDecodeSequence(session.decoder) == 0, "HEVC finalize failed"); + while (!ended) drain(); + check(pictures == HevcHashes.size(), "missing HEVC pictures after finalizing the sequence"); +} + +void testHevcCrop() { + Session session(CodecHevc, 1, 30); + std::vector buffer(session.memory.maxFrameBufferSize); + const FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size()}; + check(sceVdecswSetDecodeOutput(session.decoder, &frame) == 0, "cropped HEVC output buffer was rejected"); + const InputData input{sizeof(InputData), HevcCroppedStream.data(), HevcCroppedStream.size(), 1000, 0, 0xa0}; + check(sceVdecswSetDecodeInput(session.decoder, &input) == 0, "cropped HEVC decode input was rejected"); + InputResult consumed{sizeof(InputResult)}; + check(sceVdecswSyncDecodeInput(session.decoder, &consumed) == 0, "the cropped HEVC access unit was not consumed"); + check(sceVdecswFinalizeDecodeSequence(session.decoder) == 0, "cropped HEVC finalize failed"); + OutputInfo output{sizeof(OutputInfo)}; + check(sceVdecswSyncDecodeOutput(session.decoder, &output) == 0 && output.isValid && output.frameWidth == 64 && output.frameHeight == 40, "unexpected cropped HEVC picture geometry"); + HevcPictureInfo info{sizeof(HevcPictureInfo)}; + check(sceVdecswGetHevcPictureInfo(&output, &info) == 0 && info.isValid, "cropped HEVC picture info missing"); + check(info.picWidthInLumaSamples == 64 && info.picHeightInLumaSamples == 40, "cropped HEVC coded size differs"); + check(info.conformanceWindowFlag == 1 && info.confWinLeftOffset == 0 && info.confWinRightOffset == 0 && info.confWinTopOffset == 0 && info.confWinBottomOffset == 2, "HEVC conformance window differs"); + check(info.frameCroppingFlag == 1 && info.frameCropLeftOffset == 0 && info.frameCropRightOffset == 0 && info.frameCropTopOffset == 0 && info.frameCropBottomOffset == 2, "HEVC frame cropping differs"); +} + void testWithoutOutput() { Session session; const auto units = accessUnits(false); @@ -233,12 +383,228 @@ 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"); + HevcPictureInfo hevc{sizeof(HevcPictureInfo)}; + bool hevcThrows = false; + try { + sceVdecswGetHevcPictureInfo(&output, &hevc); + } catch (const std::exception&) { + hevcThrows = true; + } + check(hevcThrows, "HEVC picture info was returned for an H.264 picture"); +} + +struct Vp9Session { + std::uint64_t queue = 0; + std::uint64_t decoder = 0; + DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; + + Vp9Session() { + ComputeMemoryInfo compute{sizeof(ComputeMemoryInfo)}; + check(sceVdecswQueryComputeMemoryInfo(&compute) == 0 && compute.cpuGpuMemorySize != 0, "compute memory query failed"); + const ComputeConfigInfo computeConfig{sizeof(ComputeConfigInfo), 3, 3, true}; + check(sceVdecswAllocateComputeQueue(&computeConfig, &compute, &queue) == 0, "compute queue allocation failed"); + DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, CodecVp9, 0, 0, static_cast(Width), static_cast(Height), 4, 3, queue, 0x3f, 0, true, {}}; + check(sceVdecswQueryDecoderMemoryInfo(&config, &memory) == 0 && memory.maxFrameBufferSize != 0, "VP9 decoder memory query failed"); + check(sceVdecswCreateDecoder(&config, &memory, &decoder) == 0, "VP9 decoder creation failed"); + } + + ~Vp9Session() { + sceVdecswDeleteDecoder(decoder); + sceVdecswReleaseComputeQueue(queue); + } +}; + +template +bool throws(TAction action) { + try { + action(); + } catch (const std::exception&) { + return true; + } + return false; +} + +Vp9PictureInfo sentinelVp9Info(std::uint64_t size = sizeof(Vp9PictureInfo)) { + Vp9PictureInfo info; + std::memset(&info, 0xcc, sizeof(info)); + info.thisSize = size; + return info; +} + +bool sameBytes(const Vp9PictureInfo& left, const Vp9PictureInfo& right) { + return std::memcmp(&left, &right, sizeof(Vp9PictureInfo)) == 0; +} + +void checkVp9Info(const Vp9PictureInfo& info, std::size_t picture) { + const std::string name = "VP9 picture " + std::to_string(picture); + check(info.thisSize == sizeof(Vp9PictureInfo) && info.isValid, name + " has no picture info"); + check(info.ptsData == 1000 + picture && info.dtsData == picture && info.attachedData == 0xa0 + picture, name + " timestamps differ"); + check(info.profile == 0 && info.bitDepth == 8 && info.colorRange == 0 && info.colorSpace == 0 && info.numFrames == 1 && info.intraOnly == 0, name + " coded header fields differ"); + check(info.keyFrameFlag == (picture == 0 ? 1u : 0u), name + " key frame flag differs"); + check(info.frameWidth == Width && info.frameHeight == Height && info.renderWidth == Width && info.renderHeight == Height, name + " dimensions differ"); +} + +void testVp9PictureInfo() { + Vp9Session session; + const auto units = ivfFrames(); + check(units.size() == Vp9Hashes.size(), "unexpected VP9 access unit count"); + std::array, Vp9Hashes.size()> buffers; + for (auto& buffer : buffers) buffer.resize(session.memory.maxFrameBufferSize); + std::array outputs{}; + std::size_t pictures = 0; + bool outputSet = false; + bool ended = false; + const auto drain = [&] { + for (;;) { + auto& target = buffers[pictures % buffers.size()]; + if (!outputSet) { + const FrameBuffer frame{sizeof(FrameBuffer), target.data(), target.size()}; + check(sceVdecswSetDecodeOutput(session.decoder, &frame) == 0, "VP9 output buffer was rejected"); + outputSet = true; + } + OutputInfo output{sizeof(OutputInfo)}; + const auto status = sceVdecswTrySyncDecodeOutput(session.decoder, &output); + if (status == OutputPending) return; + check(status == 0, "VP9 output sync failed"); + outputSet = false; + if (!output.isValid) { + ended = true; + return; + } + check(pictures < Vp9Hashes.size(), "more VP9 pictures than access units"); + check(output.codecType == CodecVp9 && output.pictureCount == 1 && output.frameWidth == Width && output.frameHeight == Height && output.frameBuffer == target.data(), "unexpected VP9 picture geometry"); + auto info = sentinelVp9Info(); + check(sceVdecswGetVp9PictureInfo(&output, &info) == 0, "VP9 picture info query failed"); + checkVp9Info(info, pictures); + check(hashNv12(target.data(), output.framePitch, output.frameHeight) == Vp9Hashes[pictures], "VP9 picture " + std::to_string(pictures) + " differs from the reference"); + outputs[pictures] = output; + ++pictures; + } + }; + 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}; + check(sceVdecswSetDecodeInput(session.decoder, &input) == 0, "VP9 decode input was rejected"); + InputResult consumed{sizeof(InputResult)}; + check(sceVdecswTrySyncDecodeInput(session.decoder, &consumed) == 0 && consumed.decodedAu == units[unit].data(), "a VP9 access unit was not consumed"); + drain(); + } + check(sceVdecswFinalizeDecodeSequence(session.decoder) == 0, "VP9 finalize failed"); + while (!ended) drain(); + check(pictures == Vp9Hashes.size(), "missing VP9 pictures after finalizing the sequence"); + + for (std::size_t picture = 0; picture < outputs.size(); ++picture) { + auto info = sentinelVp9Info(); + check(sceVdecswGetVp9PictureInfo(&outputs[picture], &info) == 0, "VP9 picture info of an earlier picture was lost"); + checkVp9Info(info, picture); + } + + const auto unchanged = sentinelVp9Info(); + auto info = unchanged; + check(throws([&] { sceVdecswGetVp9PictureInfo(nullptr, &info); }) && sameBytes(info, unchanged), "a null output info was accepted"); + check(throws([&] { sceVdecswGetVp9PictureInfo(&outputs[0], nullptr); }), "a null VP9 picture info was accepted"); + auto tooSmall = sentinelVp9Info(7); + const auto tooSmallBefore = tooSmall; + check(throws([&] { sceVdecswGetVp9PictureInfo(&outputs[0], &tooSmall); }) && sameBytes(tooSmall, tooSmallBefore), "a VP9 picture info smaller than its size field was accepted"); + auto doubled = outputs[0]; + doubled.pictureCount = 2; + check(throws([&] { sceVdecswGetVp9PictureInfo(&doubled, &info); }) && sameBytes(info, unchanged), "an output with two pictures was accepted"); + auto empty = outputs[0]; + empty.pictureCount = 0; + check(sceVdecswGetVp9PictureInfo(&empty, &info) == 0 && sameBytes(info, unchanged), "an output without a picture wrote a VP9 picture info"); + std::uint8_t foreign = 0; + auto unknown = outputs[0]; + unknown.frameBuffer = &foreign; + check(throws([&] { sceVdecswGetVp9PictureInfo(&unknown, &info); }) && sameBytes(info, unchanged), "a frame buffer no decoder returned was accepted"); + unknown.frameBuffer = nullptr; + check(throws([&] { sceVdecswGetVp9PictureInfo(&unknown, &info); }) && sameBytes(info, unchanged), "a null frame buffer was accepted"); + + const auto head = offsetof(Vp9PictureInfo, profile); + auto partial = sentinelVp9Info(head); + check(sceVdecswGetVp9PictureInfo(&outputs[2], &partial) == 0, "a VP9 picture info with a short size field was rejected"); + check(partial.thisSize == head && partial.isValid && partial.ptsData == 1002 && partial.dtsData == 2 && partial.attachedData == 0xa2, "the leading VP9 picture info fields were not written"); + check(partial.profile == unchanged.profile && partial.frameWidth == unchanged.frameWidth && partial.renderHeight == unchanged.renderHeight, "a VP9 picture info with a short size field was written beyond it"); + + struct Oversized { + Vp9PictureInfo info; + std::uint8_t tail[64]; + } oversized; + std::memset(&oversized, 0xcc, sizeof(oversized)); + oversized.info.thisSize = sizeof(Oversized); + check(sceVdecswGetVp9PictureInfo(&outputs[3], &oversized.info) == 0, "a VP9 picture info with a long size field was rejected"); + check(oversized.info.thisSize == sizeof(Oversized), "a long VP9 picture info size field was changed"); + oversized.info.thisSize = sizeof(Vp9PictureInfo); + checkVp9Info(oversized.info, 3); + for (const auto byte : oversized.tail) check(byte == 0xcc, "a VP9 picture info with a long size field was written beyond the structure"); + + AvcPictureInfo avc{sizeof(AvcPictureInfo)}; + check(throws([&] { sceVdecswGetAvcPictureInfo(&outputs[0], &avc, nullptr); }), "AVC picture info was returned for a VP9 picture"); + + check(sceVdecswResetDecoder(session.decoder) == 0, "VP9 reset failed"); + check(throws([&] { sceVdecswGetVp9PictureInfo(&outputs[0], &info); }) && sameBytes(info, unchanged), "VP9 picture info survived a decoder reset"); +} + +void testVp9PictureInfoOfAvcPicture() { + Session session; + 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, "the access unit was not consumed"); + 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.pictureCount == 1, "sync output did not return the finalized picture"); + const auto unchanged = sentinelVp9Info(); + auto info = unchanged; + check(throws([&] { sceVdecswGetVp9PictureInfo(&output, &info); }) && sameBytes(info, unchanged), "VP9 picture info was returned for an H.264 picture"); +} + +} + +void testHevcMain10() { + Session session(CodecHevc, 2, 30); + std::vector buffer(session.memory.maxFrameBufferSize); + const FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size()}; + check(sceVdecswSetDecodeOutput(session.decoder, &frame) == 0, "Main 10 HEVC output buffer was rejected"); + const InputData input{sizeof(InputData), HevcMain10Stream.data(), HevcMain10Stream.size(), 1000, 0, 0xa0}; + check(sceVdecswSetDecodeInput(session.decoder, &input) == 0, "Main 10 HEVC decode input was rejected"); + InputResult consumed{sizeof(InputResult)}; + check(sceVdecswSyncDecodeInput(session.decoder, &consumed) == 0, "the Main 10 HEVC access unit was not consumed"); + check(sceVdecswFinalizeDecodeSequence(session.decoder) == 0, "Main 10 HEVC finalize failed"); + OutputInfo output{sizeof(OutputInfo)}; + check(sceVdecswSyncDecodeOutput(session.decoder, &output) == 0 && output.isValid, "the Main 10 HEVC picture was not output"); + HevcPictureInfo info{sizeof(HevcPictureInfo)}; + check(sceVdecswGetHevcPictureInfo(&output, &info) == 0 && info.isValid, "Main 10 HEVC picture info missing"); + check(info.generalProfileIdc == 2 && info.bitDepthLumaMinus8 == 2 && info.bitDepthChromaMinus8 == 2, "Main 10 HEVC profile or bit depth differs"); } int main() { try { testDecode(); + testHevc(); + testHevcCrop(); + testHevcMain10(); testWithoutOutput(); + testSyncWrappers(); + testVp9PictureInfo(); + testVp9PictureInfoOfAvcPicture(); std::puts("Vdecsw tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/tests/GuestVectorAlgorithms.cpp b/core/libs/tests/GuestVectorAlgorithms.cpp new file mode 100644 index 0000000000..c3b858bac6 --- /dev/null +++ b/core/libs/tests/GuestVectorAlgorithms.cpp @@ -0,0 +1,96 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +struct MinMaxPointers { + const void* minimum; + const void* maximum; +}; + +extern "C" { +const void* APS5_VABI __std_find_trivial_2_nid_postfix(const void*, const void*, std::uint16_t); +const void* APS5_VABI __std_find_trivial_unsized_1_nid_postfix(const void*, std::uint8_t); +const void* APS5_VABI __std_find_trivial_unsized_4_nid_postfix(const void*, std::uint32_t); +std::size_t APS5_VABI __std_count_trivial_8_nid_postfix(const void*, const void*, std::uint64_t); +const void* APS5_VABI __std_min_element_1_nid_postfix(const void*, const void*, bool); +const void* APS5_VABI __std_max_element_4_nid_postfix(const void*, const void*, bool); +MinMaxPointers APS5_VABI __std_minmax_element_2_nid_postfix(const void*, const void*, bool); +MinMaxPointers APS5_VABI __std_minmax_element_8_nid_postfix(const void*, const void*, bool); +void APS5_VABI __std_reverse_trivially_swappable_2_nid_postfix(void*, void*); +void APS5_VABI __std_reverse_copy_trivially_copyable_8_nid_postfix(const void*, const void*, void*); +void APS5_VABI __std_swap_ranges_trivially_swappable_noalias_nid_postfix(void*, void*, void*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static void CheckFindAndCount() { + const std::uint16_t halves[] = {5, 7, 9, 7}; + Require(__std_find_trivial_2_nid_postfix(halves, halves + 4, 7) == halves + 1); + Require(__std_find_trivial_2_nid_postfix(halves, halves + 4, 8) == halves + 4); + Require(__std_find_trivial_2_nid_postfix(halves, halves, 5) == halves); + const std::uint64_t wides[] = {1, 2, 1, 1}; + Require(__std_count_trivial_8_nid_postfix(wides, wides + 4, 1) == 3 && __std_count_trivial_8_nid_postfix(wides, wides, 1) == 0); + alignas(32) std::uint32_t words[16] = {}; + words[3] = 42; + words[12] = 42; + Require(__std_find_trivial_unsized_4_nid_postfix(words + 8, 42) == words + 12); +} + +static void CheckUnsizedRescan() { + alignas(32) unsigned char bytes[96] = {}; + bytes[3] = 'x'; + bytes[70] = 'x'; + Require(__std_find_trivial_unsized_1_nid_postfix(bytes + 8, 'x') == bytes + 3); + bytes[20] = 'x'; + Require(__std_find_trivial_unsized_1_nid_postfix(bytes + 8, 'x') == bytes + 20); + bytes[3] = 0; + bytes[20] = 0; + Require(__std_find_trivial_unsized_1_nid_postfix(bytes + 8, 'x') == bytes + 70); +} + +static void CheckMinMax() { + const unsigned char bytes[] = {3, 0x80, 1, 0x80, 1, 0xff}; + Require(__std_min_element_1_nid_postfix(bytes, bytes + 6, false) == bytes + 2); + Require(__std_min_element_1_nid_postfix(bytes, bytes + 6, true) == bytes + 1); + Require(__std_min_element_1_nid_postfix(bytes, bytes, true) == bytes); + const std::uint32_t words[] = {7, 0xffffffffu, 9, 0xffffffffu, 9}; + Require(__std_max_element_4_nid_postfix(words, words + 5, false) == words + 1); + Require(__std_max_element_4_nid_postfix(words, words + 5, true) == words + 2); + const std::int16_t halves[] = {4, -3, 8, -3, 8, 2}; + const MinMaxPointers signedHalves = __std_minmax_element_2_nid_postfix(halves, halves + 6, true); + Require(signedHalves.minimum == halves + 1 && signedHalves.maximum == halves + 4); + const MinMaxPointers unsignedHalves = __std_minmax_element_2_nid_postfix(halves, halves + 6, false); + Require(unsignedHalves.minimum == halves + 5 && unsignedHalves.maximum == halves + 3); + const std::uint64_t wides[] = {5}; + const MinMaxPointers single = __std_minmax_element_8_nid_postfix(wides, wides + 1, false); + Require(single.minimum == wides && single.maximum == wides); + const MinMaxPointers empty = __std_minmax_element_8_nid_postfix(wides, wides, false); + Require(empty.minimum == wides && empty.maximum == wides); +} + +static void CheckReverseAndSwap() { + std::uint16_t halves[] = {1, 2, 3, 4, 5}; + __std_reverse_trivially_swappable_2_nid_postfix(halves, halves + 5); + Require(halves[0] == 5 && halves[1] == 4 && halves[2] == 3 && halves[3] == 2 && halves[4] == 1); + const std::uint64_t wides[] = {1, 2, 3}; + std::uint64_t reversed[4] = {9, 9, 9, 9}; + __std_reverse_copy_trivially_copyable_8_nid_postfix(wides, wides + 3, reversed); + Require(reversed[0] == 3 && reversed[1] == 2 && reversed[2] == 1 && reversed[3] == 9); + unsigned char left[37], right[37]; + for (int i = 0; i < 37; ++i) { + left[i] = static_cast(i); + right[i] = static_cast(100 + i); + } + __std_swap_ranges_trivially_swappable_noalias_nid_postfix(left, left + 37, right); + for (int i = 0; i < 37; ++i) Require(left[i] == 100 + i && right[i] == i); +} + +int main() { + CheckFindAndCount(); + CheckUnsizedRescan(); + CheckMinMax(); + CheckReverseAndSwap(); + return 0; +} 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/GuestVideoOutCursor.cpp b/core/libs/tests/GuestVideoOutCursor.cpp new file mode 100644 index 0000000000..214293bdd2 --- /dev/null +++ b/core/libs/tests/GuestVideoOutCursor.cpp @@ -0,0 +1,35 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVideoOutCursorEnable(std::int32_t handle, std::int32_t index, const void* address); +int APS5_VABI sceVideoOutCursorDisable(std::int32_t handle, std::int32_t index); +int APS5_VABI sceVideoOutCursorSetImageAddress(std::int32_t handle, std::int32_t index, const void* address); +int APS5_VABI sceVideoOutCursorSetPosition(std::int32_t handle, std::int32_t index, std::uint32_t x, std::uint32_t y); +} + +static bool ReportsNotImplemented(const std::string& name, const std::function& call) { + try { + call(); + } catch (const std::runtime_error& error) { + if (error.what() == name + " not implemented") return true; + std::fprintf(stderr, "%s threw: %s\n", name.c_str(), error.what()); + return false; + } + std::fprintf(stderr, "%s returned instead of reporting that it is not implemented\n", name.c_str()); + return false; +} + +int main() { + static const std::uint32_t image[64 * 64] = {}; + bool passed = ReportsNotImplemented("sceVideoOutCursorEnable", [] { return sceVideoOutCursorEnable(1, 0, image); }); + passed = ReportsNotImplemented("sceVideoOutCursorSetImageAddress", [] { return sceVideoOutCursorSetImageAddress(1, 0, image); }) && passed; + passed = ReportsNotImplemented("sceVideoOutCursorSetPosition", [] { return sceVideoOutCursorSetPosition(1, 0, 960, 540); }) && passed; + passed = ReportsNotImplemented("sceVideoOutCursorDisable", [] { return sceVideoOutCursorDisable(1, 0); }) && passed; + return passed ? 0 : 1; +} diff --git a/core/libs/tests/GuestVideoOutHdr.cpp b/core/libs/tests/GuestVideoOutHdr.cpp new file mode 100644 index 0000000000..a6e78ed34d --- /dev/null +++ b/core/libs/tests/GuestVideoOutHdr.cpp @@ -0,0 +1,120 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "prx/libkernel/Equeue/Equeue.hpp" +#include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp" +#include "prx/libSceSystemService/SystemService.hpp" +#include "tests/VideoOutTestEnvironment.hpp" +#include +#include +#include +#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 sceVideoOutGetOutputStatus(int handle, VideoOutOutputStatus* status); +int APS5_VABI sceVideoOutGetOutputStatusWithVrr(int handle, void* status); +int APS5_VABI sceVideoOutAddOutputModeEvent(KernelEqueue eq, int handle, void* udata); +int APS5_VABI sceVideoOutGetEventId(const KernelEvent* ev); +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 sceSystemServiceGetHdrToneMapLuminance(SystemServiceHdrToneMapLuminance* luminance); +} + +namespace { + +constexpr int SystemUser = 255; +constexpr int MainBus = 0; +constexpr int OutputModeEvent = 8; + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "FAIL: %s\n", what); + std::abort(); +} + +VideoOutOutputStatus Status(int handle) { + VideoOutOutputStatus status{}; + Require(sceVideoOutGetOutputStatus(handle, &status) == 0, "sceVideoOutGetOutputStatus failed"); + std::array withVrr{}; + Require(sceVideoOutGetOutputStatusWithVrr(handle, withVrr.data()) == 0, "sceVideoOutGetOutputStatusWithVrr failed"); + Require(std::memcmp(withVrr.data(), &status, sizeof(status)) == 0, "the output status with VRR differs from the output status"); + return status; +} + +SystemServiceHdrToneMapLuminance ToneMap() { + SystemServiceHdrToneMapLuminance luminance{-1.0f, -1.0f, -1.0f}; + Require(sceSystemServiceGetHdrToneMapLuminance(&luminance) == SYSTEM_SERVICE_OK, "sceSystemServiceGetHdrToneMapLuminance failed"); + return luminance; +} + +bool OutputModeEventArrives(KernelEqueue queue, KernelUseconds timeout) { + KernelEvent event{}; + int count = 0; + return sceKernelWaitEqueue(queue, &event, 1, &count, &timeout) == 0 && count == 1 && sceVideoOutGetEventId(&event) == OutputModeEvent; +} + +} + +int main() { + VideoOutTestEnvironment environment; + int handle = 0; + try { + handle = sceVideoOutOpen(SystemUser, MainBus, 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, "sceVideoOutOpen failed"); + AgcDriverSetHostDisplayHdr_nid_postfix({}); + AgcDriverSetSurfaceHdr10_nid_postfix(false); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto status = Status(handle); + Require(status.dynamicRange == 1 && status.flags == 0, "an SDR host reports HDR output"); + auto luminance = ToneMap(); + Require(luminance.max_tone_map_luminance == 100.0f && luminance.max_full_frame_tone_map_luminance == 100.0f && luminance.min_tone_map_luminance == 0.0f, "an SDR host does not report SDR reference white"); + + KernelEqueue queue = 0; + Require(sceKernelCreateEqueue(&queue, "HDR test") == 0, "event queue creation failed"); + Require(sceVideoOutAddOutputModeEvent(queue, handle, nullptr) == 0, "output mode subscription failed"); + Require(OutputModeEventArrives(queue, 0), "the output mode subscription is not triggered once"); + + AgcDriverSetHostDisplayHdr_nid_postfix(AgcDriver::DisplayHdrFromOutput(true, 1015.0f, 0.005f, 640.0f)); + status = Status(handle); + Require(status.dynamicRange == 1 && status.flags == 0, "an HDR10 display without an HDR10 surface reports HDR output"); + Require(ToneMap().max_tone_map_luminance == 100.0f, "an HDR10 display without an HDR10 surface reports its luminance"); + Require(!OutputModeEventArrives(queue, 200'000), "an HDR10 display without an HDR10 surface fires the output mode event"); + + AgcDriverSetSurfaceHdr10_nid_postfix(true); + Require(OutputModeEventArrives(queue, 2'000'000), "HDR output becoming available does not fire the output mode event"); + status = Status(handle); + Require(status.dynamicRange == 2 && status.flags == 1, "HDR output is not reported"); + Require(status.resolution != 0 && status.refreshRate == 3 && status.reserved[0] == 0 && status.reserved[1] == 0 && status.reserved[2] == 0, "the rest of the output status changed with HDR"); + luminance = ToneMap(); + Require(luminance.max_tone_map_luminance == 1015.0f && luminance.max_full_frame_tone_map_luminance == 640.0f && luminance.min_tone_map_luminance == 0.005f, "the display's luminance is not the tone-map luminance"); + Require(AgcDriver::CurrentHostHdr().reportedToneMapPeak == 1015.0f, "the tone-map peak given to the title is not recorded for the SDR conversion"); + + AgcDriverSetHostDisplayHdr_nid_postfix(AgcDriver::DisplayHdrFromOutput(true, 400.0f, 0.05f, 300.0f)); + Require(OutputModeEventArrives(queue, 2'000'000), "a new display luminance does not fire the output mode event"); + Require(ToneMap().max_tone_map_luminance == 400.0f && Status(handle).dynamicRange == 2, "the new display luminance is not reported"); + + AgcDriverSetHostDisplayHdr_nid_postfix({}); + Require(OutputModeEventArrives(queue, 2'000'000), "HDR output going away does not fire the output mode event"); + status = Status(handle); + Require(status.dynamicRange == 1 && status.flags == 0, "HDR output is reported after the display left HDR mode"); + Require(ToneMap().max_tone_map_luminance == 100.0f && AgcDriver::CurrentHostHdr().reportedToneMapPeak == 100.0f, "SDR reference white is not reported after the display left HDR mode"); + + Require(sceVideoOutClose(handle) == 0, "sceVideoOutClose failed"); + Require(sceKernelDeleteEqueue(queue) == 0, "event queue deletion failed"); + std::printf("guest VideoOut HDR tests passed\n"); + LibcRunShutdown_nid_postfix(); + return 0; +} diff --git a/core/libs/tests/GuestVideoOutOutputMode.cpp b/core/libs/tests/GuestVideoOutOutputMode.cpp index 4bed080a11..82ce4bc2fa 100644 --- a/core/libs/tests/GuestVideoOutOutputMode.cpp +++ b/core/libs/tests/GuestVideoOutOutputMode.cpp @@ -18,6 +18,7 @@ int APS5_VABI sceVideoOutConfigureOutput(int handle, std::uint64_t mode, const V static constexpr int SYSTEM_USER = 255; static constexpr int MAIN_BUS = 0; +static constexpr int HMD2_MAIN_BUS = 0x20; static constexpr int INVALID_VALUE = static_cast(0x80290001); static constexpr int INVALID_ADDRESS = static_cast(0x80290002); static constexpr int INVALID_HANDLE = static_cast(0x8029000B); @@ -38,6 +39,11 @@ int main() { for (std::size_t index = 0; index < raw.size(); ++index) Require(raw[index] == (index == 2 ? 0xFF : 0)); Require(sceVideoOutInitializeOutputOptions(nullptr) == INVALID_ADDRESS); + std::array zeroedOpenParam{}; + Require(sceVideoOutOpen(SYSTEM_USER, HMD2_MAIN_BUS, 0, nullptr) == INVALID_VALUE); + Require(sceVideoOutOpen(SYSTEM_USER, HMD2_MAIN_BUS, 0, zeroedOpenParam.data()) == INVALID_VALUE); + Require(sceVideoOutOpen(0, HMD2_MAIN_BUS, 0, nullptr) == INVALID_VALUE); + int handle = 0; try { for (int unopened : {-1, 0, 1, 0x7FFFFFFF}) { @@ -64,6 +70,10 @@ int main() { for (std::uint64_t mode : {0x0ull, 0x2ull, 0x3ull, 0x5ull, 0x6ull, 0x9ull, 0xAull, 0xBull, 0x14ull, 0x20ull, 0x3Full, 0x100ull, 0xD000000Aull, 0x8000000000000001ull, ~0ull}) { Require(sceVideoOutIsOutputSupported(handle, mode, &options, nullptr, 0) == UNKNOWN_OUTPUT_MODE); } + for (std::uint64_t mode : {0x20001ull, 0x2000Cull, 0x2000Dull, 0x2000Eull, 0x2000Full, 0x20010ull}) { + Require(sceVideoOutIsOutputSupported(handle, mode, nullptr, nullptr, 0) == UNKNOWN_OUTPUT_MODE); + Require(sceVideoOutConfigureOutput(handle, mode, nullptr, nullptr, 0) == UNKNOWN_OUTPUT_MODE); + } bool thrown = false; try { sceVideoOutIsOutputSupported(handle, 0x12, &options, nullptr, 0); diff --git a/core/libs/tests/GuestVideoRecording.cpp b/core/libs/tests/GuestVideoRecording.cpp new file mode 100644 index 0000000000..3338094cac --- /dev/null +++ b/core/libs/tests/GuestVideoRecording.cpp @@ -0,0 +1,67 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include + +extern "C" { +void APS5_VABI _sceVideoRecordingQueryParam_nid_postfix(void* param); +int APS5_VABI sceVideoRecordingGetStatus(void); +int APS5_VABI sceVideoRecordingQueryMemSize(const void* param); +int APS5_VABI sceVideoRecordingOpen(const char* path, const void* param, void* heap, int heapSize); +int APS5_VABI sceVideoRecordingStart(void); +int APS5_VABI sceVideoRecordingStop(void); +int APS5_VABI sceVideoRecordingClose(int discard); +} + +namespace { + +constexpr int StatusNone = 0; +constexpr std::uint8_t Sentinel = 0xA5; + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "VideoRecording: %s\n", message); + std::abort(); + } +} + +template +bool Throws(TCall call) { + try { + call(); + } catch (const std::exception&) { + return true; + } + return false; +} + +} + +int main() { + Require(sceVideoRecordingGetStatus() == StatusNone, "a recorder that was never opened reported a status"); + Require(sceVideoRecordingGetStatus() == StatusNone, "the status changed between two reads"); + + std::array param{}; + param.fill(Sentinel); + std::array heap{}; + heap.fill(Sentinel); + + Require(Throws([&] { _sceVideoRecordingQueryParam_nid_postfix(param.data()); }), "the default parameters were produced"); + Require(Throws([&] { sceVideoRecordingQueryMemSize(param.data()); }), "a heap size was reported"); + Require(Throws([&] { sceVideoRecordingQueryMemSize(nullptr); }), "a heap size was reported for no parameters"); + Require(Throws([&] { sceVideoRecordingOpen("clip.mp4", param.data(), heap.data(), static_cast(heap.size())); }), "the recorder opened"); + Require(Throws([&] { sceVideoRecordingStart(); }), "the encoder started"); + Require(Throws([&] { sceVideoRecordingStop(); }), "the encoder stopped"); + Require(Throws([&] { sceVideoRecordingClose(0); }), "the recorder closed"); + Require(Throws([&] { sceVideoRecordingClose(1); }), "the recorder closed with discard"); + + for (const std::uint8_t value : param) Require(value == Sentinel, "the parameters were written"); + for (const std::uint8_t value : heap) Require(value == Sentinel, "the heap was written"); + Require(sceVideoRecordingGetStatus() == StatusNone, "a failed open changed the status"); + + std::puts("VideoRecording tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestVideodec.cpp b/core/libs/tests/GuestVideodec.cpp new file mode 100644 index 0000000000..894d6f96ee --- /dev/null +++ b/core/libs/tests/GuestVideodec.cpp @@ -0,0 +1,320 @@ +#include +#include +#include +#include +#include +#include "prx/libc/include/General.hpp" +#include "H264Fixture.hpp" + +struct ConfigInfo { + std::uint64_t thisSize; + std::uint32_t codecType; + std::uint32_t profile; + std::uint32_t maxLevel; + std::int32_t maxFrameWidth; + std::int32_t maxFrameHeight; + std::int32_t maxDpbFrameCount; + std::uint64_t videodecFlags; +}; + +struct ResourceInfo { + std::uint64_t thisSize; + std::uint64_t cpuMemorySize; + void* cpuMemory; + std::uint64_t cpuGpuMemorySize; + void* cpuGpuMemory; + std::uint64_t maxFrameBufferSize; + std::uint32_t frameBufferAlignment; +}; + +struct Ctrl { + std::uint64_t thisSize; + void* handle; + std::uint64_t version; +}; + +struct FrameBuffer { + std::uint64_t thisSize; + void* frameBuffer; + std::uint64_t frameBufferSize; +}; + +struct AvcInfo { + std::uint32_t numUnitsInTick; + std::uint32_t timeScale; + std::uint8_t fixedFrameRateFlag; + std::uint8_t aspectRatioIdc; + std::uint16_t sarWidth; + std::uint16_t sarHeight; + std::uint8_t colourPrimaries; + std::uint8_t transferCharacteristics; + std::uint8_t matrixCoefficients; + std::uint8_t videoFullRangeFlag; + std::uint32_t frameCropLeftOffset; + std::uint32_t frameCropRightOffset; + std::uint32_t frameCropTopOffset; + std::uint32_t frameCropBottomOffset; +}; + +union CodecInfo { + std::uint8_t reserved[64]; + AvcInfo avc; +}; + +struct PictureInfo { + std::uint64_t thisSize; + std::uint32_t isValid; + std::uint32_t codecType; + std::uint32_t frameWidth; + std::uint32_t framePitch; + std::uint32_t frameHeight; + std::uint32_t isErrorPic; + std::uint64_t ptsData; + std::uint64_t attachedData; + CodecInfo codec; +}; + +struct InputData { + std::uint64_t thisSize; + const std::uint8_t* auData; + std::uint64_t auSize; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; +}; + +struct Decoder2ConfigInfo { + std::uint64_t thisSize; + std::uint32_t resourceType; + std::uint32_t codecType; + std::uint32_t profile; + std::uint32_t maxLevel; + std::int32_t maxFrameWidth; + std::int32_t maxFrameHeight; + std::int32_t maxDpbFrameCount; + std::uint32_t decodePipelineDepth; +}; + +struct Decoder2MemoryInfo { + std::uint64_t thisSize; + std::uint64_t cpuMemorySize; + void* cpuMemory; + std::uint64_t gpuMemorySize; + void* gpuMemory; + std::uint64_t cpuGpuMemorySize; + void* cpuGpuMemory; + std::uint64_t maxFrameBufferSize; + std::uint32_t frameBufferAlignment; +}; + +static_assert(sizeof(PictureInfo) == 112 && sizeof(AvcInfo) == 36); +static_assert(sizeof(ConfigInfo) == 40 && sizeof(ResourceInfo) == 56 && sizeof(Ctrl) == 24); +static_assert(sizeof(Decoder2ConfigInfo) == 40 && sizeof(Decoder2MemoryInfo) == 72); + +extern "C" { +int APS5_VABI sceVideodecCreateDecoder(const ConfigInfo*, const ResourceInfo*, Ctrl*); +int APS5_VABI sceVideodecDecode(Ctrl*, const InputData*, FrameBuffer*, PictureInfo*); +int APS5_VABI sceVideodecDeleteDecoder(Ctrl*); +int APS5_VABI sceVideodecFlush(Ctrl*, FrameBuffer*, PictureInfo*); +int APS5_VABI sceVideodecReset(Ctrl*); +int APS5_VABI sceVideodecQueryResourceInfo(const ConfigInfo*, ResourceInfo*); +int APS5_VABI sceVideodec2QueryDecoderMemoryInfo_nid_postfix(const Decoder2ConfigInfo*, Decoder2MemoryInfo*); +} + +namespace { + +using namespace H264Fixture; + +constexpr int ErrorApiFail = static_cast(0x80C10000); +constexpr int ErrorStructSize = static_cast(0x80C10002); +constexpr int ErrorHandle = static_cast(0x80C10003); +constexpr int ErrorAuSize = static_cast(0x80C10009); +constexpr int ErrorAuPointer = static_cast(0x80C1000A); +constexpr int ErrorFrameBufferSize = static_cast(0x80C1000B); +constexpr int ErrorFrameBufferPointer = static_cast(0x80C1000C); +constexpr int ErrorConfigInfo = static_cast(0x80C1000E); +constexpr int ErrorArgumentPointer = static_cast(0x80C1000F); + +constexpr std::uint32_t Pitch = 64; +constexpr std::uint32_t PictureHeight = 48; +constexpr std::size_t FrameBytes = static_cast(Pitch) * PictureHeight * 3 / 2; + +void check(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +struct Session { + Ctrl ctrl{sizeof(Ctrl)}; + std::vector buffer; + + Session() : buffer(FrameBytes) { + const ConfigInfo config{sizeof(ConfigInfo), 1, 100, 42, static_cast(Width), + static_cast(CodedHeight), 4, 0}; + const ResourceInfo resource{sizeof(ResourceInfo), 16u << 20u, nullptr, 16u << 20u, nullptr, + FrameBytes, 256}; + check(sceVideodecCreateDecoder(&config, &resource, &ctrl) == 0, "decoder creation failed"); + check(ctrl.handle != nullptr && ctrl.version == 1, "the control block was not filled"); + } + + ~Session() { sceVideodecDeleteDecoder(&ctrl); } + + FrameBuffer MakeTarget() { return FrameBuffer{sizeof(FrameBuffer), buffer.data(), buffer.size()}; } +}; + +void testDecode(const std::vector>& units, std::uint32_t cropTop, std::uint32_t cropBottom) { + Session session; + check(units.size() == PictureHashes.size(), "unexpected access unit count"); + std::vector hashes; + std::vector timestamps; + const auto collect = [&](PictureInfo& picture) { + if (!picture.isValid) return; + check(picture.framePitch == Pitch && picture.frameHeight == PictureHeight, "unexpected picture geometry"); + check(picture.frameWidth == Width, "unexpected picture width"); + check(picture.codecType == 0 && !picture.isErrorPic, "unexpected picture flags"); + check(picture.codec.avc.frameCropTopOffset == cropTop && picture.codec.avc.frameCropBottomOffset == cropBottom, "unexpected crop"); + hashes.push_back(hashNv12(session.buffer.data(), Pitch, PictureHeight)); + timestamps.push_back(picture.ptsData); + }; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), 1000 + unit, unit, 0xa0 + unit}; + auto target = session.MakeTarget(); + PictureInfo picture{sizeof(PictureInfo)}; + const auto status = sceVideodecDecode(&session.ctrl, &input, &target, &picture); + check(status == 0, "decode of unit " + std::to_string(unit) + " failed with " + std::to_string(status)); + collect(picture); + } + for (;;) { + auto target = session.MakeTarget(); + PictureInfo picture{sizeof(PictureInfo)}; + check(sceVideodecFlush(&session.ctrl, &target, &picture) == 0, "flush failed"); + if (!picture.isValid) break; + collect(picture); + } + check(hashes.size() == PictureHashes.size(), "missing pictures"); + for (std::size_t index = 0; index < hashes.size(); ++index) { + check(hashes[index] == PictureHashes[index], "picture " + std::to_string(index) + " differs from the reference"); + const auto unit = DisplayOrderUnits[index]; + check(timestamps[index] == 1000 + unit, "pictures out of display order"); + } +} + +void testDecode() { + testDecode(accessUnits(false), 0, (CodedHeight - Height) / 2); +} + +void testDecodeCropped() { + const auto stream = croppedStream(); + testDecode(accessUnits(stream.data(), stream.size(), false), 4, 4); +} + +void testErrors() { + const ConfigInfo config{sizeof(ConfigInfo), 1, 100, 42, static_cast(Width), + static_cast(CodedHeight), 4, 0}; + const ResourceInfo resource{sizeof(ResourceInfo), 16u << 20u, nullptr, 16u << 20u, nullptr, FrameBytes, 256}; + Ctrl ctrl{sizeof(Ctrl)}; + check(sceVideodecCreateDecoder(nullptr, &resource, &ctrl) == ErrorArgumentPointer, "a null configuration was accepted"); + check(sceVideodecCreateDecoder(&config, &resource, nullptr) == ErrorArgumentPointer, "a null control block was accepted"); + const ConfigInfo shortConfig{sizeof(ConfigInfo) - 8}; + check(sceVideodecCreateDecoder(&shortConfig, &resource, &ctrl) == ErrorStructSize, "a short configuration was accepted"); + check(sceVideodecCreateDecoder(&config, &resource, &ctrl) == 0, "decoder creation failed"); + Ctrl foreign{sizeof(Ctrl)}; + foreign.handle = reinterpret_cast(static_cast(0x1234)); + check(sceVideodecDeleteDecoder(&foreign) == ErrorHandle, "an unknown decoder was deleted"); + check(sceVideodecReset(&foreign) == ErrorHandle, "an unknown decoder was reset"); + const InputData empty{sizeof(InputData), nullptr, 0, 0, 0, 0}; + FrameBuffer frame{sizeof(FrameBuffer), nullptr, 0}; + PictureInfo picture{sizeof(PictureInfo)}; + check(sceVideodecDecode(nullptr, &empty, &frame, &picture) == ErrorArgumentPointer, "a null control block was accepted"); + check(sceVideodecDecode(&ctrl, &empty, &frame, &picture) == ErrorAuPointer, "a null access unit was accepted"); + const InputData sized{sizeof(InputData), reinterpret_cast(&config), 0, 0, 0, 0}; + check(sceVideodecDecode(&ctrl, &sized, &frame, &picture) == ErrorAuSize, "an empty access unit was accepted"); + FrameBuffer missing{sizeof(FrameBuffer), nullptr, FrameBytes}; + const auto units = accessUnits(false); + const InputData input{sizeof(InputData), units[0].data(), units[0].size(), 0, 0, 0}; + check(sceVideodecDecode(&ctrl, &input, &missing, &picture) == ErrorFrameBufferPointer, "a null frame buffer was accepted"); + std::vector storage(static_cast(FrameBytes)); + FrameBuffer shortBuffer{sizeof(FrameBuffer), storage.data(), FrameBytes - 1}; + int refusals = 0; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + const InputData unit0{sizeof(InputData), units[unit].data(), units[unit].size(), 1000 + unit, unit, 0xa0 + unit}; + const auto status = sceVideodecDecode(&ctrl, &unit0, &shortBuffer, &picture); + check(status == 0 || status == ErrorFrameBufferSize, "a decode failed"); + if (status == ErrorFrameBufferSize) ++refusals; + } + check(refusals > 0, "no picture was refused for the short frame buffer"); + check(sceVideodecDeleteDecoder(&ctrl) == 0, "the decoder was not deleted"); + check(sceVideodecDeleteDecoder(&ctrl) == ErrorHandle, "the decoder was deleted twice"); +} + +void testReset() { + Session session; + const auto units = accessUnits(false); + const InputData input{sizeof(InputData), units[0].data(), units[0].size(), 1000, 0, 0xa0}; + auto target = session.MakeTarget(); + PictureInfo picture{sizeof(PictureInfo)}; + check(sceVideodecDecode(&session.ctrl, &input, &target, &picture) == 0, "decode failed"); + check(sceVideodecReset(&session.ctrl) == 0, "reset failed"); + check(sceVideodecFlush(&session.ctrl, &target, &picture) == 0, "flush failed"); + check(!picture.isValid, "a reset decoder produced a picture"); +} + +void testQueryResourceInfo(std::int32_t width, std::int32_t height, std::uint32_t profile, std::uint32_t level) { + const ConfigInfo config{sizeof(ConfigInfo), 1, profile, level, width, height, 4, 0}; + ResourceInfo resource{sizeof(ResourceInfo)}; + check(sceVideodecQueryResourceInfo(&config, &resource) == 0, "the resource query failed"); + const Decoder2ConfigInfo mapped{sizeof(Decoder2ConfigInfo), 1, 1, profile, level, width, height, 4, 1}; + Decoder2MemoryInfo memory{sizeof(Decoder2MemoryInfo)}; + check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&mapped, &memory) == 0, "the Videodec2 memory query failed"); + check(resource.thisSize == sizeof(ResourceInfo), "the resource information size was changed"); + check(resource.cpuMemorySize == memory.cpuMemorySize, "the CPU memory size differs from libSceVideodec2"); + check(resource.cpuGpuMemorySize == memory.cpuGpuMemorySize, "the CPU-GPU memory size differs from libSceVideodec2"); + check(resource.maxFrameBufferSize == memory.maxFrameBufferSize, "the frame buffer size differs from libSceVideodec2"); + check(resource.frameBufferAlignment == memory.frameBufferAlignment, "the frame buffer alignment differs from libSceVideodec2"); +} + +void testQueryResourceInfo() { + testQueryResourceInfo(static_cast(Width), static_cast(CodedHeight), 100, 42); + testQueryResourceInfo(1920, 1080, 100, 42); + testQueryResourceInfo(3840, 2160, 100, 51); + const ConfigInfo config{sizeof(ConfigInfo), 1, 100, 42, static_cast(Width), + static_cast(CodedHeight), 4, 0}; + ResourceInfo resource{sizeof(ResourceInfo)}; + check(sceVideodecQueryResourceInfo(&config, &resource) == 0, "the resource query failed"); + check(resource.maxFrameBufferSize >= FrameBytes, "the frame buffer size does not hold a decoded picture"); + Ctrl ctrl{sizeof(Ctrl)}; + check(sceVideodecCreateDecoder(&config, &resource, &ctrl) == 0, "the queried resources were refused"); + check(sceVideodecDeleteDecoder(&ctrl) == 0, "the decoder was not deleted"); + check(sceVideodecQueryResourceInfo(nullptr, &resource) == ErrorArgumentPointer, "a null configuration was accepted"); + check(sceVideodecQueryResourceInfo(&config, nullptr) == ErrorArgumentPointer, "null resource information was accepted"); + const ResourceInfo sentinel{sizeof(ResourceInfo), 7, nullptr, 7, nullptr, 7, 7}; + auto refused = sentinel; + auto shortConfig = config; + shortConfig.thisSize = sizeof(ConfigInfo) - 8; + check(sceVideodecQueryResourceInfo(&shortConfig, &refused) == ErrorStructSize, "a short configuration was accepted"); + auto shortResource = sentinel; + shortResource.thisSize = sizeof(ResourceInfo) - 8; + check(sceVideodecQueryResourceInfo(&config, &shortResource) == ErrorStructSize, "short resource information was accepted"); + auto unsized = config; + unsized.maxFrameWidth = 0; + check(sceVideodecQueryResourceInfo(&unsized, &refused) == ErrorConfigInfo, "a configuration without a frame size was accepted"); + check(refused.cpuMemorySize == 7 && refused.cpuGpuMemorySize == 7 && refused.maxFrameBufferSize == 7 && refused.frameBufferAlignment == 7, + "a refused query wrote the resource information"); + check(shortResource.cpuMemorySize == 7 && shortResource.maxFrameBufferSize == 7, "a refused query wrote the resource information"); +} + +} + +int main() { + try { + testDecode(); + testDecodeCropped(); + testErrors(); + testReset(); + testQueryResourceInfo(); + std::puts("Videodec tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index 45c19b9901..565f4454b4 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -3,12 +3,16 @@ #include #include #include +#include #include #include #include +#include #include #include "prx/libc/include/General.hpp" #include "H264Fixture.hpp" +#include "HevcFixture.hpp" +#include "Vp9Fixture.hpp" struct DecoderConfigInfo { std::uint64_t thisSize; @@ -78,6 +82,29 @@ struct AvcPictureInfo { std::uint32_t picHeightInLumaSamples; }; +struct Vp9PictureInfo { + std::uint64_t thisSize; + bool isValid; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; + std::uint32_t profile; + std::uint32_t level; + std::uint32_t keyFrameFlag; + std::uint32_t intraOnly; + std::uint8_t bitDepth; + std::uint8_t colorRange; + std::uint32_t colorSpace; + std::uint32_t numFrames; + std::uint32_t frameWidth; + std::uint32_t frameHeight; + std::uint32_t renderWidth; + std::uint32_t renderHeight; +}; + +static_assert(sizeof(Vp9PictureInfo) == 88); +static_assert(offsetof(Vp9PictureInfo, colorSpace) == 60); + extern "C" { int APS5_VABI sceVideodec2QueryDecoderMemoryInfo_nid_postfix(const DecoderConfigInfo*, DecoderMemoryInfo*); int APS5_VABI sceVideodec2CreateDecoder_nid_postfix(const DecoderConfigInfo*, const DecoderMemoryInfo*, std::uint64_t*); @@ -86,185 +113,61 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t, const InputData*, Fr int APS5_VABI sceVideodec2Flush_nid_postfix(std::uint64_t, FrameBuffer*, OutputInfo*); int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t); int APS5_VABI sceVideodec2GetPictureInfo_nid_postfix(const OutputInfo*, AvcPictureInfo*, void*); +int APS5_VABI sceVideodec2GetVp9PictureInfo_nid_postfix(const OutputInfo*, Vp9PictureInfo*); int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const OutputInfo*, AvcPictureInfo*, AvcPictureInfo*); } namespace { using namespace H264Fixture; - +using namespace HevcFixture; constexpr std::uint32_t CodecHevc = 974921; constexpr std::uint32_t CodecVp9 = 2382845; -constexpr std::array HevcStream{ - 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, - 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, - 0x95, 0x90, 0x09, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, - 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, 0xa0, 0x20, 0x82, 0x96, 0x59, 0x59, 0x2e, - 0x4c, 0xaf, 0x01, 0x68, 0x08, 0x00, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x03, 0x00, 0xf0, 0x40, - 0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, 0xd0, 0x89, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, - 0x08, 0xe0, 0xbb, 0xcb, 0xc1, 0x71, 0x26, 0x0f, 0xbf, 0x6c, 0xcb, 0x4d, 0x2b, 0x2c, 0xf4, 0x08, - 0xe0, 0x17, 0xeb, 0x16, 0xcc, 0x07, 0xd8, 0xdd, 0x11, 0xf8, 0x5e, 0x73, 0x79, 0x57, 0xcb, 0x69, - 0xa3, 0x1c, 0x9c, 0x43, 0x14, 0xdb, 0x61, 0x0e, 0xc5, 0xc6, 0x9c, 0x9c, 0xf8, 0xf2, 0x07, 0x35, - 0xf1, 0x98, 0x6c, 0x1a, 0x86, 0x53, 0x5a, 0xcd, 0x4d, 0xda, 0xff, 0xfb, 0x01, 0xda, 0xc3, 0x61, - 0xb7, 0x8c, 0xae, 0xc9, 0x76, 0xe4, 0x05, 0xd6, 0xfd, 0xe4, 0xcd, 0x93, 0x2c, 0xb1, 0x41, 0x29, - 0xce, 0x22, 0xf7, 0x77, 0x64, 0x0f, 0x1f, 0xd2, 0xc3, 0x68, 0x32, 0x2f, 0x83, 0xd7, 0x53, 0xbe, - 0x23, 0xff, 0x8c, 0x46, 0x18, 0x33, 0x90, 0x28, 0xa3, 0xef, 0xd7, 0xb1, 0x46, 0x84, 0xa0, 0xc2, - 0xd4, 0xd9, 0xce, 0xdf, 0x0a, 0x05, 0xec, 0xad, 0x63, 0xa4, 0x78, 0x27, 0x80, 0x4e, 0x3b, 0x4c, - 0xa6, 0x42, 0xbc, 0xe3, 0x39, 0x4d, 0xc4, 0xd3, 0x44, 0xf1, 0x22, 0xd0, 0x64, 0x9f, 0xd0, 0x88, - 0x7f, 0x39, 0x6d, 0x06, 0xc7, 0x79, 0x75, 0x2a, 0x44, 0x3d, 0x21, 0x41, 0x53, 0x60, 0x8d, 0x08, - 0x9f, 0xd0, 0x34, 0xa6, 0x72, 0x70, 0x3b, 0xa9, 0xfa, 0x62, 0xb1, 0x19, 0x9b, 0x0b, 0xa2, 0xfc, - 0xb4, 0xb3, 0x2c, 0x58, 0xad, 0xc6, 0xf3, 0xea, 0xc4, 0xd0, 0xf9, 0xea, 0x9e, 0xa7, 0xa4, 0x87, - 0xb8, 0x19, 0x68, 0xf1, 0xc1, 0x6b, 0x87, 0xa5, 0x99, 0xa0, 0x69, 0x24, 0x28, 0x80, 0xba, 0xd2, - 0x57, 0xc6, 0xc0, 0x1e, 0x69, 0x1b, 0xce, 0x96, 0xcd, 0x85, 0x9d, 0x59, 0x32, 0x92, 0x71, 0xe7, - 0x15, 0xf2, 0x9b, 0x71, 0x4b, 0xd4, 0xb6, 0x99, 0xb0, 0x71, 0x9b, 0x9b, 0xdb, 0x33, 0x28, 0x86, - 0x5e, 0x44, 0xf3, 0x5d, 0x5d, 0x7b, 0x87, 0x6c, 0x99, 0x8a, 0x6d, 0xaf, 0xf8, 0xdd, 0x39, 0x14, - 0xc0, 0x10, 0x85, 0xaf, 0x3a, 0x77, 0x60, 0x8b, 0xb7, 0x5f, 0x67, 0x45, 0xb4, 0xa7, 0xe7, 0x44, - 0x77, 0x0a, 0xb9, 0xe1, 0xb2, 0xbe, 0xde, 0x1b, 0x5a, 0xb8, 0xc8, 0x9b, 0x1f, 0x15, 0x37, 0xdc, - 0xeb, 0x55, 0x12, 0x37, 0xa1, 0xab, 0x9c, 0xcd, 0xdd, 0x42, 0x5d, 0x90, 0x89, 0xc5, 0x99, 0x54, - 0x6e, 0x9a, 0xd2, 0x71, 0x07, 0xe3, 0x81, 0x20, 0xad, 0xd4, 0x7c, 0xad, 0x69, 0xaf, 0xc7, 0x2d, - 0x7c, 0x84, 0xf1, 0x5d, 0x43, 0x4a, 0x3c, 0xf6, 0xf8, 0x7e, 0x6a, 0xb2, 0xdd, 0x00, 0xda, 0x30, - 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x30, 0x00, 0x00, 0x01, 0x02, 0x01, 0xd0, 0x19, 0x5f, 0x84, - 0x31, 0x84, 0x50, 0x62, 0xcd, 0x44, 0xa6, 0x4e, 0xbb, 0x2e, 0x87, 0xda, 0x21, 0x25, 0x1c, 0x13, - 0xfe, 0xc2, 0x97, 0xa3, 0xd5, 0xfd, 0xe2, 0x62, 0xd6, 0xe5, 0x50, 0x7a, 0xda, 0x98, 0x8d, 0xcf, - 0xbe, 0x40, 0x5d, 0x69, 0xb6, 0x5f, 0xdc, 0x32, 0x68, 0x05, 0x67, 0x70, 0xfd, 0xa0, 0xd3, 0xcd, - 0x88, 0xeb, 0x12, 0x2e, 0xac, 0xf5, 0x57, 0xa2, 0xc2, 0x81, 0x22, 0x6b, 0x8d, 0x69, 0x6d, 0xac, - 0x41, 0x05, 0x5b, 0x0b, 0x79, 0xe0, 0x86, 0x05, 0x86, 0x8b, 0x02, 0x51, 0x7d, 0x6e, 0x13, 0x63, - 0x36, 0xe6, 0x89, 0x22, 0x75, 0xd9, 0xb0, 0x39, 0x27, 0xcd, 0xac, 0x78, 0xe0, 0x3a, 0x6a, 0x17, - 0x15, 0x05, 0x83, 0x3a, 0xe9, 0x38, 0x7c, 0x75, 0x38, 0xf3, 0xd8, 0x42, 0x57, 0xa1, 0xef, 0x68, - 0xb8, 0xb5, 0x44, 0xfe, 0x4d, 0x66, 0xa1, 0x09, 0xc5, 0x62, 0xff, 0x87, 0x7e, 0xdb, 0x41, 0x6f, - 0x4d, 0x5f, 0x16, 0x49, 0xef, 0xca, 0xda, 0x67, 0x01, 0x29, 0x0b, 0xd6, 0xd8, 0xfc, 0x0b, 0x22, - 0xc8, 0x28, 0xff, 0xb4, 0xb3, 0xa9, 0xf8, 0x9b, 0x78, 0x7d, 0x33, 0xbf, 0xa6, 0x9b, 0x39, 0x64, - 0x57, 0x41, 0x1f, 0x27, 0x05, 0xfc, 0xcd, 0x44, 0xcd, 0xb2, 0xea, 0x79, 0xe0, 0x00, 0x00, 0x00, - 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, 0x02, 0x01, 0xe0, 0x44, 0x97, 0xe1, 0x8f, 0x40, 0x3d, - 0x73, 0xde, 0xdf, 0xf5, 0x1c, 0xff, 0xf6, 0xe2, 0x12, 0x16, 0x3d, 0xb6, 0x75, 0xad, 0x9e, 0xb5, - 0x2b, 0xe7, 0x4a, 0x40, 0xe9, 0x91, 0xd6, 0x86, 0x86, 0x2d, 0xcb, 0xe4, 0xb6, 0x58, 0x4a, 0x2d, - 0x7b, 0x1b, 0xdb, 0xc5, 0x48, 0x2d, 0x09, 0x4d, 0x32, 0x30, 0xde, 0xaa, 0x61, 0xc2, 0x27, 0x64, - 0x54, 0x71, 0x89, 0x32, 0x05, 0xc0, 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, - 0x00, 0x01, 0xe0, 0x24, 0xff, 0xfa, 0x2c, 0x6e, 0xdc, 0x78, 0x3d, 0x08, 0x6d, 0x3b, 0xcf, 0xcc, - 0xb6, 0x16, 0xeb, 0x9d, 0x5f, 0x46, 0x0b, 0x24, 0x0e, 0x97, 0x99, 0x06, 0x00, 0x00, 0x00, 0x01, - 0x46, 0x01, 0x30, 0x00, 0x00, 0x01, 0x02, 0x01, 0xd0, 0x28, 0x95, 0xd7, 0xdc, 0x41, 0xc0, 0x61, - 0x14, 0xd6, 0xee, 0xd4, 0xe8, 0x24, 0x3e, 0x7f, 0x82, 0x22, 0xb0, 0x20, 0xb9, 0xf1, 0x69, 0x9b, - 0xfe, 0xcc, 0xb7, 0x2e, 0xbd, 0xdb, 0x20, 0xfa, 0xe5, 0x97, 0xd9, 0x95, 0x1d, 0x7e, 0x49, 0x35, - 0x69, 0xdb, 0x40, 0xa3, 0x5c, 0x0f, 0x74, 0x6f, 0x00, 0x3d, 0x36, 0x05, 0xc0, 0xc4, 0x37, 0x15, - 0xf9, 0x01, 0x2d, 0xdf, 0xcb, 0x70, 0x3d, 0xfa, 0x47, 0xec, 0x80, 0x07, 0x48, 0x08, 0xc8, 0x54, - 0xd3, 0xe3, 0x00, 0xa3, 0xe4, 0xce, 0x40, 0x05, 0x0b, 0x51, 0x73, 0x56, 0x33, 0x82, 0xa8, 0x0a, - 0xc0, 0xd3, 0xc5, 0xf1, 0xeb, 0x32, 0x8e, 0x4e, 0xa7, 0x6f, 0xb2, 0x64, 0x43, 0x54, 0x71, 0xd4, - 0xaf, 0xc0, 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, 0x00, 0x01, 0xe0, 0x82, - 0x2f, 0x5f, 0xdc, 0x63, 0x70, 0x3f, 0xaf, 0x66, 0x75, 0x52, 0xe7, 0x7f, 0x62, 0xfb, 0x84, 0x9c, - 0xef, 0x60, 0x8f, 0xfa, 0x7e, 0x5f, 0x8b, 0x40, 0x15, 0x34, 0x69, 0xe2, 0xc4, 0xfc, 0xff, 0x53, - 0xb8, 0x1e, 0xc7, 0x20, 0x55, 0xdb, 0x5e, 0x05, 0xbd, 0x7a, 0x85, 0xb4, 0xb7, 0xc1, 0x8d, 0xdb, - 0x0f, 0xa6, 0xab, 0xb9, 0x00, 0x2a, 0xdb, 0x83, 0xa0, 0xb0, 0xc5, 0x76, 0x6e, 0xe1, 0xb8, 0x72, - 0xfd, 0x09, 0x61, 0x19, 0xe0, -}; - -constexpr std::array HevcHashes{ - 0x7291c31ee57c5065ull, 0x37b15adc47810098ull, 0xc84737b65a65b247ull, - 0xe39bc891f8286267ull, 0x734caf1ba961b3c2ull, 0x7deb44bd1ed30531ull, -}; - -constexpr std::array Vp9Stream{ - 0x44, 0x4b, 0x49, 0x46, 0x00, 0x00, 0x20, 0x00, 0x56, 0x50, 0x39, 0x30, 0x40, 0x00, 0x28, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xe9, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x82, 0x49, 0x83, 0x42, - 0x00, 0x03, 0xf0, 0x02, 0x76, 0x08, 0x38, 0x24, 0x1c, 0x18, 0x0e, 0x00, 0x01, 0xc0, 0x6f, 0xb1, - 0xfb, 0x17, 0xa6, 0xd2, 0x0f, 0x5f, 0xee, 0xf7, 0xec, 0xbf, 0xb4, 0x0c, 0x93, 0xbe, 0x87, 0x11, - 0x35, 0x1c, 0x51, 0x84, 0x8b, 0x70, 0xad, 0xe4, 0x00, 0x00, 0x79, 0x3b, 0xff, 0xff, 0xf5, 0x94, - 0xad, 0x57, 0x6e, 0xae, 0x8e, 0x1b, 0xfe, 0x64, 0xff, 0xc9, 0x64, 0xba, 0xca, 0x1d, 0x7f, 0x45, - 0x16, 0x95, 0x13, 0x8d, 0xd0, 0x61, 0x19, 0x80, 0xb7, 0xdf, 0xff, 0xff, 0xd6, 0x52, 0x1e, 0x7b, - 0x3c, 0x16, 0x3f, 0xff, 0xfb, 0x52, 0xe7, 0x26, 0xab, 0xd6, 0x3a, 0x6a, 0xff, 0xf8, 0x1b, 0x47, - 0xcd, 0xe2, 0xf5, 0x48, 0x65, 0x1e, 0x9f, 0x5c, 0x1a, 0x74, 0x0a, 0x99, 0xc0, 0x04, 0x41, 0x02, - 0xa9, 0x86, 0x36, 0x1f, 0xff, 0xf3, 0x4f, 0x71, 0xdf, 0x8a, 0xcd, 0x8a, 0x88, 0xdd, 0x37, 0xa2, - 0x27, 0x3a, 0x20, 0xb8, 0xf0, 0x4f, 0x67, 0x59, 0x8a, 0x82, 0xa2, 0xde, 0x08, 0xe3, 0xae, 0x87, - 0xff, 0xf0, 0x36, 0x8f, 0x9b, 0xc5, 0xea, 0x90, 0xca, 0x3d, 0x3e, 0xb8, 0x34, 0xe8, 0x15, 0x33, - 0x80, 0x08, 0x82, 0x05, 0x53, 0x0c, 0x6c, 0x3f, 0xff, 0xe6, 0x9e, 0xe3, 0xbf, 0x15, 0x9d, 0xec, - 0xdf, 0xe9, 0xce, 0x60, 0x88, 0x09, 0x48, 0x81, 0xa2, 0xb6, 0x27, 0x3e, 0xf9, 0xa9, 0x7e, 0xa7, - 0xe1, 0x9d, 0xdd, 0x3a, 0xa0, 0x39, 0x3f, 0x8a, 0xba, 0x73, 0x50, 0xde, 0xcf, 0x29, 0x5c, 0x2a, - 0x92, 0x01, 0xec, 0x06, 0xbe, 0x8b, 0x1e, 0x14, 0x34, 0x6b, 0xb1, 0x3a, 0x01, 0x47, 0x08, 0xe7, - 0xbe, 0xb0, 0xa7, 0x03, 0x77, 0xeb, 0xf0, 0xed, 0xd8, 0xa1, 0x4e, 0x7c, 0xd9, 0xe4, 0x7c, 0x9d, - 0x67, 0xfe, 0x36, 0xab, 0x5e, 0xa9, 0x5e, 0x81, 0x74, 0x68, 0xa1, 0x4b, 0x61, 0x81, 0xe2, 0xe4, - 0x4c, 0x6a, 0x4f, 0xe5, 0x65, 0x3f, 0x4c, 0x78, 0xea, 0x68, 0x5c, 0xf9, 0xc6, 0x58, 0x1f, 0x58, - 0xe0, 0x40, 0x4b, 0x88, 0x6d, 0x15, 0x94, 0x8c, 0x83, 0x4a, 0xa3, 0xf3, 0xd5, 0x80, 0x7f, 0x83, - 0x52, 0x7d, 0xbe, 0xa6, 0x9f, 0xb4, 0x89, 0xda, 0xb6, 0x5a, 0xdf, 0x1f, 0x78, 0xd8, 0xbf, 0xfe, - 0x7f, 0x52, 0xfa, 0xfd, 0xa4, 0xeb, 0xda, 0x91, 0x9c, 0xb6, 0xd0, 0xc3, 0xfd, 0x8e, 0x04, 0x04, - 0xb8, 0x86, 0xd1, 0x56, 0xa2, 0x24, 0xe6, 0xbc, 0x5b, 0xbf, 0x82, 0x36, 0xb5, 0xa7, 0x87, 0x73, - 0xd8, 0xbd, 0x30, 0x33, 0x3f, 0x7c, 0xbb, 0x4f, 0x70, 0xed, 0x3f, 0x8d, 0x2b, 0xe2, 0xce, 0x62, - 0x09, 0xfa, 0x04, 0x31, 0x67, 0xff, 0x09, 0xc1, 0x5c, 0x29, 0xf8, 0x77, 0xfa, 0xb6, 0xa7, 0xde, - 0xaa, 0xc1, 0x87, 0xd9, 0xb4, 0x10, 0x0d, 0xda, 0x5d, 0xf7, 0xdd, 0x7d, 0x08, 0x7a, 0xbe, 0x60, - 0xeb, 0x85, 0x00, 0x08, 0x21, 0x8e, 0xa0, 0x4d, 0xff, 0x73, 0x66, 0x55, 0xb8, 0x61, 0x69, 0xc5, - 0xf2, 0xef, 0x96, 0x27, 0x16, 0x2f, 0x0d, 0x93, 0xcd, 0xcd, 0xba, 0x2a, 0x80, 0x59, 0x90, 0x2b, - 0xc5, 0x43, 0x49, 0x25, 0x49, 0x01, 0x15, 0x9f, 0xf4, 0xdd, 0x02, 0x31, 0x8b, 0xf0, 0xbe, 0x28, - 0x87, 0xbd, 0x3f, 0xce, 0xb1, 0x6e, 0xd2, 0x7f, 0x77, 0x41, 0x09, 0x8a, 0x04, 0x17, 0x87, 0x57, - 0x1b, 0x72, 0x79, 0x69, 0x41, 0x7d, 0xc5, 0x78, 0xb7, 0x53, 0xe7, 0x80, 0xd2, 0x7f, 0x1f, 0xb9, - 0xd5, 0x0f, 0xff, 0xf5, 0xb8, 0xd0, 0xc5, 0x47, 0x30, 0x09, 0x36, 0x6e, 0xb1, 0xfd, 0xe1, 0xe9, - 0xf9, 0x81, 0x1d, 0x98, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x86, 0x00, 0x40, 0x92, 0x9c, 0x10, 0x46, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x76, 0x21, - 0x2d, 0x37, 0xff, 0xff, 0xc3, 0x7f, 0xff, 0xff, 0xff, 0xd8, 0x59, 0x6b, 0xff, 0xa9, 0xff, 0xff, - 0xff, 0xda, 0x3d, 0xca, 0x5b, 0xc7, 0xff, 0xfd, 0x52, 0xf6, 0x2e, 0x02, 0xe0, 0x37, 0xe3, 0x5f, - 0xff, 0xea, 0x82, 0x5b, 0xff, 0xff, 0xfa, 0x7b, 0x85, 0xdf, 0x66, 0xbf, 0xff, 0xe6, 0xde, 0xef, - 0xed, 0x97, 0xff, 0xf9, 0x38, 0x4a, 0x7f, 0x61, 0xb5, 0xeb, 0x56, 0x73, 0xa7, 0xff, 0xff, 0x31, - 0x8b, 0xa1, 0xcf, 0xff, 0x5b, 0x90, 0x07, 0x17, 0xff, 0xd6, 0xe1, 0x74, 0x39, 0xff, 0xeb, 0x72, - 0x5d, 0x14, 0xff, 0xf5, 0xb8, 0x95, 0x54, 0x7f, 0xf7, 0x5d, 0x1e, 0x26, 0x68, 0x00, 0x7f, 0x00, - 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86, 0x00, 0x40, 0x92, 0x9c, 0x10, - 0x45, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x7d, 0xb8, 0xff, 0xff, 0xb4, 0x7f, 0xff, 0xff, 0xfe, - 0x4d, 0x81, 0xbf, 0xff, 0x98, 0x9f, 0xff, 0xff, 0xe4, 0xd8, 0x1b, 0xff, 0xf9, 0x89, 0xff, 0xff, - 0xfe, 0x4d, 0x81, 0xbf, 0xd6, 0xbf, 0xff, 0xf9, 0x36, 0x0c, 0x1f, 0xff, 0xfc, 0x10, 0x3e, 0xfe, - 0x1f, 0xa1, 0x76, 0x5c, 0x22, 0x5f, 0x3b, 0x56, 0x57, 0xa0, 0x3c, 0x43, 0xf0, 0xc8, 0x00, 0x00, - 0x00, 0x19, 0xbb, 0xff, 0xff, 0xd8, 0x43, 0x10, 0x26, 0x67, 0xff, 0xff, 0xfe, 0x4a, 0x07, 0x8f, - 0x80, 0x7f, 0xff, 0xff, 0x94, 0xd2, 0x59, 0x02, 0xff, 0xff, 0x0d, 0x89, 0x18, 0x02, 0xff, 0xff, - 0x0d, 0x84, 0xb2, 0x05, 0xff, 0xfe, 0x1b, 0x14, 0xdd, 0x42, 0xff, 0xff, 0x0d, 0x82, 0xc8, 0xb1, - 0xff, 0xf9, 0x5c, 0x32, 0xfe, 0x00, 0x78, 0x60, 0x00, 0x56, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x86, 0x00, 0x40, 0x92, 0x9c, 0x20, 0x46, 0x00, 0x00, 0x03, 0x60, - 0x00, 0x00, 0x73, 0xaf, 0xff, 0xff, 0xc3, 0x7f, 0xff, 0xff, 0xff, 0xd8, 0x19, 0x3f, 0xff, 0xe6, - 0x1b, 0xfb, 0x80, 0x95, 0x82, 0x3b, 0xd9, 0x4c, 0x81, 0xce, 0x52, 0x2b, 0xff, 0xff, 0xa0, 0xad, - 0xc9, 0x0e, 0x62, 0xff, 0xff, 0xff, 0x60, 0xf8, 0x75, 0x5f, 0xff, 0xff, 0x31, 0x92, 0xaa, 0x8f, - 0xfe, 0xeb, 0xa4, 0x4e, 0x5f, 0xff, 0x5b, 0x89, 0x55, 0x47, 0xff, 0x75, 0xd4, 0xd9, 0xef, 0xff, - 0xe4, 0x51, 0x64, 0x58, 0xff, 0xee, 0xba, 0xfc, 0x8d, 0x8f, 0x00, 0x95, 0x00, 0x00, 0x00, 0x04, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86, 0x00, 0x40, 0x92, 0x9c, 0x10, 0x44, 0x00, 0x00, - 0x03, 0x60, 0x00, 0x00, 0x73, 0xaf, 0xff, 0xff, 0xfb, 0x50, 0x3f, 0xff, 0xff, 0xff, 0xea, 0xc5, - 0x0d, 0xff, 0xff, 0xe0, 0x09, 0xfc, 0xad, 0x23, 0x5f, 0xf8, 0xa3, 0x64, 0x9b, 0x40, 0x76, 0x8d, - 0xd6, 0xd1, 0x83, 0x6f, 0x24, 0xd5, 0xff, 0xff, 0xd4, 0xa9, 0x45, 0xac, 0xda, 0xdc, 0xd2, 0xbf, - 0xff, 0xff, 0xfd, 0x58, 0xbf, 0x7f, 0x8a, 0x7f, 0xff, 0xff, 0xad, 0xb9, 0x2d, 0x3c, 0xbf, 0xff, - 0xd4, 0xca, 0x46, 0x7d, 0x7f, 0xff, 0xe1, 0x64, 0x96, 0x9e, 0x5f, 0xff, 0xea, 0x65, 0x4d, 0xe9, - 0x97, 0xff, 0xfa, 0x99, 0x16, 0x71, 0xaf, 0xff, 0xfc, 0x2c, 0x99, 0x7f, 0x5b, 0x8b, 0x50, 0x9c, - 0xdc, 0x43, 0xa8, 0xb7, 0xd5, 0x68, 0x6a, 0xbc, 0x78, 0xef, 0xc2, 0x3d, 0xfb, 0x3f, 0xcc, 0x51, - 0x40, 0x86, 0x54, 0x02, 0xa4, 0xb2, 0xbd, 0x79, 0x93, 0xa2, 0x4a, 0x35, 0x36, 0xc8, 0x7c, 0xdb, - 0xb0, 0x1d, 0xc1, 0xd0, 0x1d, 0x51, 0x9a, 0xe4, 0x19, 0xe6, 0x08, 0x00, 0x95, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86, 0x00, 0x40, 0x92, 0x9c, 0x10, 0x46, 0x00, - 0x00, 0x03, 0x60, 0x00, 0x00, 0x73, 0xaf, 0xff, 0xff, 0xa9, 0xef, 0xff, 0xff, 0xff, 0xd8, 0x59, - 0x9f, 0xff, 0x53, 0xff, 0xff, 0xff, 0xb4, 0x78, 0x24, 0x37, 0xff, 0xfb, 0x89, 0x98, 0x26, 0xc0, - 0xe3, 0x5f, 0x50, 0x5c, 0x2f, 0xff, 0xff, 0xf1, 0xd2, 0xd2, 0x4d, 0xfd, 0xf2, 0x7f, 0xff, 0x9f, - 0xff, 0x17, 0x85, 0xd2, 0xab, 0x4b, 0xbe, 0x3d, 0xff, 0xff, 0xff, 0xdf, 0x55, 0xbf, 0xff, 0xff, - 0xff, 0xfe, 0x6e, 0xe3, 0xe4, 0x07, 0x53, 0xd2, 0x5f, 0xff, 0xff, 0xff, 0xbe, 0xab, 0x7f, 0xff, - 0xff, 0xff, 0xcd, 0xdc, 0x1a, 0xbf, 0xff, 0xfe, 0xfa, 0xad, 0xff, 0xff, 0xff, 0xcd, 0xdc, 0x4e, - 0x1d, 0x57, 0xff, 0xff, 0xe0, 0x44, 0xaa, 0xa3, 0xff, 0xd3, 0x89, 0x13, 0x97, 0xff, 0xe5, 0xa2, - 0x55, 0x51, 0xff, 0xe9, 0xc4, 0xba, 0x29, 0xff, 0xeb, 0x71, 0x2a, 0xa8, 0xff, 0xee, 0xb9, 0xc4, - 0x7f, 0xff, 0xfb, 0xea, 0xb7, 0xff, 0xff, 0xff, 0x37, 0x71, 0x29, 0xfa, 0x00, -}; - -constexpr std::array Vp9Hashes{ - 0x552fd3c320b37408ull, 0xb2fb78bac8d2d7dfull, 0x312469345c491c3dull, - 0x9aeb0fb400c95a75ull, 0xe02e07646faa48beull, 0x1399072031abb375ull, -}; - -constexpr std::array HevcMain10Stream{ - 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, - 0xff, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, - 0x95, 0x98, 0x09, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, - 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, 0xa0, 0x20, 0x82, 0x93, 0x65, 0x95, 0x9a, - 0xe4, 0xca, 0xf0, 0x16, 0x80, 0x80, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x0f, 0x04, 0x00, - 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, 0xd0, 0x89, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, 0x08, - 0x60, 0xf2, 0xfb, 0x04, 0xef, 0x67, 0x67, 0xdc, 0xc7, 0x8e, 0xc7, 0x42, 0x63, 0xeb, 0x7d, 0x28, - 0x73, 0x95, 0x14, 0x0c, 0x71, 0x10, 0x8b, 0x65, 0x04, 0xbd, 0x03, 0xe6, 0x04, 0x35, 0xb9, 0x9b, - 0xec, 0x60, 0x2e, 0xea, 0xcf, 0xd0, 0xdd, 0x2d, 0x3a, 0x62, 0x33, 0x9d, 0x7a, 0x74, 0xb0, 0xeb, - 0x0c, 0x06, 0xe6, 0xa5, 0x30, 0xa7, 0xea, 0xed, 0xc2, 0xa5, 0x8e, 0x7c, 0xda, 0x1b, 0x9d, 0xe4, - 0xac, 0x2b, 0x38, 0xdb, 0x9c, 0xe8, 0x51, 0x34, 0x46, 0xd2, 0x04, 0x1a, 0x77, 0x87, 0x7a, 0x19, - 0x70, 0xf9, 0x4f, 0xcb, 0x03, 0x3c, 0x74, 0x25, 0xb9, 0x92, 0x3b, 0x95, 0xe9, 0xd8, 0xaa, 0x9b, - 0x6b, 0x9b, 0x1f, 0x70, 0x25, 0x2a, 0xe7, 0x6c, 0xc3, 0x7e, 0x55, 0x39, 0xf5, 0x06, 0x5a, 0x23, - 0xb1, 0x2e, 0xf6, 0x01, 0xac, 0x71, 0xb9, 0x20, 0xb4, 0xef, 0x1d, 0x42, 0x7e, 0x0e, 0x01, 0x1d, - 0xaf, 0x0a, 0x13, 0x04, 0x8e, 0x03, 0x4d, 0x1e, 0xc4, 0xe0, -}; +constexpr std::array AvcIpppStream{ + 0x00, 0x00, 0x00, 0x01, 0x09, 0x10, 0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0xc0, 0x0a, 0xda, 0x11, + 0xec, 0x04, 0x40, 0x00, 0x00, 0x03, 0x00, 0x40, 0x00, 0x00, 0x0f, 0x03, 0xc4, 0x89, 0xa8, 0x00, + 0x00, 0x00, 0x01, 0x68, 0xce, 0x0f, 0x2c, 0x80, 0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x09, 0x7f, + 0xe4, 0x5e, 0xd2, 0xf1, 0x40, 0x00, 0x40, 0x07, 0xc5, 0x00, 0x02, 0x50, 0x06, 0x46, 0x28, 0xcd, + 0x0f, 0x80, 0x38, 0x07, 0xff, 0xc0, 0x07, 0xd8, 0x76, 0x33, 0x23, 0x81, 0xce, 0x7d, 0x73, 0x08, + 0x2f, 0xc0, 0xff, 0x1f, 0xc1, 0x61, 0x01, 0x2f, 0xc1, 0x00, 0x01, 0xf0, 0x19, 0x8d, 0x80, 0x38, + 0xd1, 0x0d, 0xcc, 0x7a, 0x99, 0xdf, 0xff, 0xda, 0x00, 0x3d, 0x82, 0x8e, 0xf2, 0x8f, 0x22, 0x18, + 0x94, 0x07, 0x24, 0x94, 0x45, 0x22, 0x44, 0x5c, 0xb8, 0x7f, 0xe1, 0xa2, 0x99, 0xa3, 0xef, 0xe2, + 0x80, 0x01, 0x9f, 0x03, 0x81, 0x0e, 0x01, 0x80, 0x70, 0x00, 0x10, 0x05, 0x00, 0x04, 0x00, 0x02, + 0x00, 0x6e, 0xbf, 0x6f, 0x66, 0xdb, 0x0c, 0x3f, 0x7c, 0x5b, 0xcc, 0xf6, 0x6c, 0xcc, 0x30, 0xe1, + 0x8c, 0x1c, 0x00, 0x04, 0x04, 0x80, 0x00, 0x42, 0x73, 0xfd, 0x1f, 0xfe, 0x20, 0x00, 0x13, 0x00, + 0x01, 0x08, 0xf8, 0x80, 0x00, 0x20, 0x2c, 0x1f, 0x04, 0xbb, 0xf3, 0xc0, 0x23, 0xdd, 0x63, 0x6f, + 0xf9, 0xe1, 0x04, 0x82, 0x53, 0x90, 0x01, 0x89, 0xe5, 0x89, 0x44, 0x5a, 0xf8, 0x39, 0xe0, 0x03, + 0x20, 0x10, 0x50, 0x00, 0x25, 0xf9, 0x8d, 0xe0, 0x04, 0xf3, 0x62, 0xfd, 0x35, 0x7d, 0x77, 0x7e, + 0x0b, 0xe1, 0x4d, 0xfe, 0x85, 0x2b, 0xef, 0xe8, 0xf0, 0x80, 0xa2, 0x80, 0x03, 0x04, 0x00, 0x06, + 0xc0, 0x72, 0x80, 0x02, 0x8c, 0x14, 0xe0, 0x9d, 0xcd, 0xe8, 0x3f, 0xe9, 0xb5, 0x9b, 0x06, 0x4d, + 0x5e, 0x80, 0xa2, 0x1f, 0x06, 0xda, 0xdf, 0x36, 0xfd, 0xa1, 0x0f, 0xfe, 0x08, 0xb0, 0x26, 0x48, + 0xaf, 0x17, 0xe9, 0xc5, 0x9f, 0xbf, 0xba, 0x2e, 0x69, 0xa9, 0xe6, 0x7c, 0xbf, 0x4f, 0xfe, 0xc1, + 0x64, 0x0a, 0xaa, 0x69, 0xda, 0x43, 0xff, 0xf9, 0xfd, 0xfc, 0xff, 0x77, 0xe9, 0x1e, 0x20, 0x18, + 0x30, 0x1f, 0x80, 0x12, 0xe4, 0x51, 0x4d, 0x9b, 0xff, 0x9b, 0x83, 0xa0, 0x01, 0x72, 0xd1, 0x7f, + 0x83, 0xa2, 0xe9, 0x82, 0x73, 0x13, 0xd3, 0x8d, 0x59, 0x95, 0x1f, 0xa6, 0x70, 0x4a, 0xeb, 0xb5, + 0xe0, 0x42, 0x10, 0xc0, 0x02, 0x00, 0x1c, 0x20, 0x00, 0x28, 0x14, 0x68, 0xe0, 0x1a, 0x82, 0xd4, + 0x87, 0x10, 0x00, 0x46, 0x54, 0x41, 0xc1, 0x81, 0x9a, 0x0c, 0x5d, 0xd0, 0x03, 0xd8, 0x0f, 0x52, + 0x07, 0x2c, 0x00, 0x7a, 0x92, 0x12, 0x02, 0xdc, 0x01, 0x67, 0x3a, 0xc1, 0xa4, 0x5a, 0x83, 0x95, + 0xc0, 0x30, 0x02, 0xf8, 0x49, 0xe2, 0x1e, 0x2b, 0xd5, 0x4a, 0x29, 0xa9, 0x6d, 0xf2, 0xc0, 0x7b, + 0x27, 0x2c, 0xb3, 0xde, 0x18, 0x00, 0x11, 0x71, 0xe0, 0x70, 0x02, 0x0c, 0x34, 0x07, 0x06, 0x01, + 0xa2, 0x90, 0x71, 0x40, 0x05, 0x12, 0x98, 0x01, 0x01, 0x63, 0xe2, 0xa8, 0xc0, 0xb8, 0xe0, 0xe0, + 0xf0, 0x06, 0x8a, 0x41, 0xd0, 0x01, 0x59, 0x12, 0x08, 0x30, 0x7d, 0x39, 0x02, 0x1d, 0x5e, 0x01, + 0x80, 0x26, 0x39, 0x0f, 0xc1, 0xef, 0x83, 0xc3, 0xf8, 0x3c, 0x7c, 0x4f, 0x86, 0xdd, 0x50, 0x6d, + 0x69, 0xd0, 0xef, 0xe9, 0x70, 0x35, 0xc4, 0xb3, 0x84, 0x40, 0x00, 0x40, 0x48, 0x00, 0x54, 0x84, + 0x00, 0x0d, 0x51, 0x81, 0xc3, 0x80, 0x3d, 0xd2, 0x1c, 0x20, 0x1c, 0xca, 0xa0, 0x10, 0x1a, 0x46, + 0x38, 0x04, 0x06, 0xd0, 0x75, 0x00, 0x2f, 0x48, 0x1e, 0x80, 0x3f, 0x49, 0x14, 0xc0, 0x3f, 0xc4, + 0x52, 0x01, 0x7e, 0x78, 0x36, 0x19, 0x08, 0x43, 0xf6, 0xc5, 0xc0, 0x01, 0xf8, 0x9f, 0x6e, 0x65, + 0xa4, 0xff, 0xff, 0x86, 0x0e, 0xf8, 0x00, 0x8b, 0xfb, 0xe4, 0xf4, 0x00, 0x57, 0x5c, 0x07, 0x4b, + 0x80, 0x66, 0xe3, 0xd2, 0xcd, 0xa7, 0x03, 0xa0, 0x41, 0x81, 0x20, 0x00, 0x20, 0x24, 0x80, 0x73, + 0x80, 0x0e, 0x52, 0x0e, 0xe5, 0xc0, 0x27, 0xd9, 0x08, 0x9c, 0x6c, 0xa6, 0x58, 0x86, 0xc5, 0xe9, + 0x0e, 0x40, 0x0c, 0xc8, 0x17, 0x5c, 0x50, 0x1a, 0xc1, 0x74, 0x47, 0x9f, 0xf0, 0x81, 0x0d, 0xb0, + 0x00, 0x00, 0x00, 0x01, 0x09, 0x30, 0x00, 0x00, 0x01, 0x41, 0x9a, 0x20, 0x25, 0xde, 0x49, 0xba, + 0xbd, 0x70, 0x47, 0xbd, 0x5a, 0xa0, 0x00, 0x00, 0x00, 0x01, 0x09, 0x30, 0x00, 0x00, 0x01, 0x41, + 0x9a, 0x40, 0x2d, 0xc4, 0xf0, 0x47, 0x55, 0xf7, 0xfa, 0xdf, 0xe0, 0x97, 0xbb, 0xee, 0xad, 0xf0, + 0x43, 0xd5, 0x7b, 0x00, 0x00, 0x00, 0x01, 0x09, 0x30, 0x00, 0x00, 0x01, 0x41, 0x9a, 0x60, 0x35, + 0xc6, 0xc0}; void check(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); @@ -320,27 +223,8 @@ void testFailures() { check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); } -std::vector> hevcUnits(const std::uint8_t* stream, std::size_t size) { - std::vector starts; - for (std::size_t index = 0; index + 4 < size; ++index) { - if (stream[index] == 0 && stream[index + 1] == 0 && stream[index + 2] == 0 && stream[index + 3] == 1 && ((stream[index + 4] >> 1u) & 0x3fu) == 35) starts.push_back(index); - } - starts.push_back(size); - std::vector> units; - for (std::size_t unit = 0; unit + 1 < starts.size(); ++unit) units.emplace_back(stream + starts[unit], stream + starts[unit + 1]); - return units; -} - -std::vector> ivfFrames() { - std::vector> frames; - for (std::size_t index = 32; index + 12 <= Vp9Stream.size();) { - const std::size_t length = Vp9Stream[index] | (Vp9Stream[index + 1] << 8u) | (Vp9Stream[index + 2] << 16u) | (static_cast(Vp9Stream[index + 3]) << 24u); - check(index + 12 + length <= Vp9Stream.size(), "truncated IVF frame"); - frames.emplace_back(Vp9Stream.begin() + static_cast(index + 12), Vp9Stream.begin() + static_cast(index + 12 + length)); - index += 12 + length; - } - return frames; -} +using Vp9Fixture::Vp9Hashes; +using Vp9Fixture::ivfFrames; void testClip(std::uint32_t codec, std::uint32_t profile, const std::vector>& units, const std::array& hashes) { const DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, codec, profile, 0, static_cast(Width), static_cast(Height), 4, 1}; @@ -357,7 +241,21 @@ void testClip(std::uint32_t codec, std::uint32_t profile, const std::vector(&generic.info), nullptr) == 0, "generic VP9 picture info failed"); + const auto& same = generic.info; + check(same.thisSize == sizeof(Vp9PictureInfo) && same.isValid && same.ptsData == vp9.ptsData && same.dtsData == vp9.dtsData && same.attachedData == vp9.attachedData && same.profile == vp9.profile && same.level == vp9.level && + same.keyFrameFlag == vp9.keyFrameFlag && same.intraOnly == vp9.intraOnly && same.bitDepth == vp9.bitDepth && same.colorRange == vp9.colorRange && same.colorSpace == vp9.colorSpace && same.numFrames == vp9.numFrames && + same.frameWidth == vp9.frameWidth && same.frameHeight == vp9.frameHeight && same.renderWidth == vp9.renderWidth && same.renderHeight == vp9.renderHeight && generic.guard == 0x123456789abcdef0ull, "generic picture info differs from the VP9 layout"); + } else checkThrows([&] { sceVideodec2GetVp9PictureInfo_nid_postfix(&output, &vp9); }); + check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == hashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference"); ++pictures; return true; }; @@ -379,8 +277,90 @@ void testClip(std::uint32_t codec, std::uint32_t profile, const std::vector(Width), static_cast(Height), 4, 1}; + DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; + check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0, "VP9 memory query failed"); + std::uint64_t handle = 0; + check(sceVideodec2CreateDecoder_nid_postfix(&config, &memory, &handle) == 0, "VP9 create failed"); + const auto units = ivfFrames(); + std::vector firstBuffer(memory.maxFrameBufferSize), secondBuffer(memory.maxFrameBufferSize); + OutputInfo firstOutput{sizeof(OutputInfo)}, secondOutput{sizeof(OutputInfo)}; + const auto decode = [&](const std::vector& bytes, std::vector& buffer, OutputInfo& output, std::uint64_t tag) { + const InputData input{sizeof(InputData), bytes.data(), bytes.size(), tag, tag + 1, tag + 2}; + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "VP9 metadata decode failed"); + }; + const auto query = [&](const OutputInfo& output) { + struct Guarded { Vp9PictureInfo info{sizeof(Vp9PictureInfo)}; std::uint64_t guard = 0x123456789abcdef0ull; } guarded; + check(sceVideodec2GetVp9PictureInfo_nid_postfix(&output, &guarded.info) == 0, "VP9 metadata query failed"); + check(guarded.guard == 0x123456789abcdef0ull && guarded.info.thisSize == sizeof(Vp9PictureInfo), "VP9 info overwrote caller storage"); + return guarded.info; + }; + Vp9PictureInfo info{sizeof(Vp9PictureInfo)}; + checkThrows([&] { sceVideodec2GetVp9PictureInfo_nid_postfix(nullptr, &info); }); + checkThrows([&] { sceVideodec2GetVp9PictureInfo_nid_postfix(&firstOutput, nullptr); }); + info.thisSize = sizeof(Vp9PictureInfo) - 1; + checkThrows([&] { sceVideodec2GetVp9PictureInfo_nid_postfix(&firstOutput, &info); }); + check(!query(firstOutput).isValid, "VP9 unknown buffer reported a picture"); + std::vector superframe; + for (std::size_t index = 0; index < 2; ++index) superframe.insert(superframe.end(), units[index].begin(), units[index].end()); + superframe.push_back(0xc9); + for (std::size_t index = 0; index < 2; ++index) { + superframe.push_back(static_cast(units[index].size())); + superframe.push_back(static_cast(units[index].size() >> 8u)); + } + superframe.push_back(0xc9); + decode(superframe, firstBuffer, firstOutput, 9000); + check(firstOutput.isValid && hashNv12(firstBuffer.data(), firstOutput.framePitch, firstOutput.frameHeight) == Vp9Hashes[0], "VP9 first superframe picture differs"); + const auto firstInfo = query(firstOutput); + check(firstInfo.isValid && firstInfo.numFrames == 2 && firstInfo.keyFrameFlag == 1 && firstInfo.ptsData == 9000 && firstInfo.dtsData == 9001 && firstInfo.attachedData == 9002, "VP9 first superframe metadata differs"); + FrameBuffer frame{sizeof(FrameBuffer), secondBuffer.data(), secondBuffer.size(), false}; + check(sceVideodec2Flush_nid_postfix(handle, &frame, &secondOutput) == 0 && secondOutput.isValid, "VP9 second superframe picture missing"); + check(hashNv12(secondBuffer.data(), secondOutput.framePitch, secondOutput.frameHeight) == Vp9Hashes[1], "VP9 second superframe picture differs"); + const auto secondInfo = query(secondOutput); + check(secondInfo.numFrames == 2 && secondInfo.keyFrameFlag == 0 && secondInfo.ptsData == 9000, "VP9 second superframe metadata differs"); + firstOutput.frameWidth = 1; + check(query(firstOutput).frameWidth == Width && query(firstOutput).keyFrameFlag == 1, "VP9 retained picture metadata changed"); + check(sceVideodec2Reset_nid_postfix(handle) == 0 && !query(firstOutput).isValid, "VP9 reset retained old picture ownership"); + auto hidden = units[0]; + hidden[0] &= static_cast(~2u); + decode(hidden, firstBuffer, firstOutput, 9100); + check(!firstOutput.isValid, "VP9 hidden keyframe was presented"); + decode(std::vector{0x88}, secondBuffer, secondOutput, 9200); + info = query(secondOutput); + check(info.isValid && info.ptsData == 9200 && info.keyFrameFlag == 0 && info.frameWidth == Width, "VP9 existing reference metadata differs"); + check(hashNv12(secondBuffer.data(), secondOutput.framePitch, secondOutput.frameHeight) == Vp9Hashes[0], "VP9 existing reference pixels differ"); + check(sceVideodec2Reset_nid_postfix(handle) == 0, "VP9 reset failed"); + checkThrows([&] { decode(std::vector{0x88}, firstBuffer, firstOutput, 9300); }); + checkThrows([&] { decode(std::vector{0}, firstBuffer, firstOutput, 9400); }); + checkThrows([&] { decode(std::vector{0x82}, firstBuffer, firstOutput, 9500); }); + checkThrows([&] { decode(std::vector{0xc1, 0, 0, 0xc1}, firstBuffer, firstOutput, 9550); }); + std::vector bits; + for (const auto byte : units[0]) for (int shift = 7; shift >= 0; --shift) bits.push_back(((byte >> shift) & 1u) != 0); + check(bits.size() > 69, "VP9 test keyframe header is truncated"); + bits[35] = true; + bits[68] = true; + std::vector render; + for (const auto value : {static_cast(Width * 2 - 1), static_cast(Height * 2 - 1)}) + for (int shift = 15; shift >= 0; --shift) render.push_back(((value >> shift) & 1u) != 0); + bits.insert(bits.begin() + 69, render.begin(), render.end()); + std::vector resized(bits.size() / 8); + for (std::size_t index = 0; index < bits.size(); ++index) if (bits[index]) resized[index / 8] |= static_cast(1u << (7u - index % 8)); + decode(resized, firstBuffer, firstOutput, 9600); + info = query(firstOutput); + check(info.isValid && info.bitDepth == 8 && info.colorRange == 1 && info.frameWidth == Width && info.frameHeight == Height && info.renderWidth == Width * 2 && info.renderHeight == Height * 2, "VP9 render size or color range metadata differs"); + check(hashNv12(firstBuffer.data(), firstOutput.framePitch, firstOutput.frameHeight) == Vp9Hashes[0], "VP9 render metadata changed coded pixels"); + check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0 && !query(firstOutput).isValid, "VP9 delete retained picture ownership"); +} + void testTenBit() { for (const auto codec : {CodecHevc, CodecVp9}) { + for (const auto [profile, expected] : {std::pair{1, 2048ull * 1088 * 3 / 2}, {2, 3840ull * 1088 * 3 / 2}}) { + const DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, codec, profile, 0, 1920, 1088, 4, 1}; + DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; + check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0 && memory.maxFrameBufferSize == expected, "1920x1088 frame size differs for profile " + std::to_string(profile)); + } const DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, codec, 2, 0, static_cast(Width), static_cast(Height), 4, 1}; DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0, "memory query failed"); @@ -391,16 +371,29 @@ void testTenBit() { const InputData input{sizeof(InputData), HevcMain10Stream.data(), HevcMain10Stream.size(), 0, 0, 0}; FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; OutputInfo output{sizeof(OutputInfo)}; - checkThrows([&] { - sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output); - sceVideodec2Flush_nid_postfix(handle, &frame, &output); - }); + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "ten-bit decode failed"); + if (!output.isValid) check(sceVideodec2Flush_nid_postfix(handle, &frame, &output) == 0, "ten-bit flush failed"); + check(output.isValid && frame.isAccepted && output.frameWidth == Width && output.frameHeight == Height && output.framePitch == 128 && output.framePitchInBytes == 256 && output.frameFormat == 0, "ten-bit picture geometry differs"); + std::uint64_t hash = 0xcbf29ce484222325ull; + std::uint16_t largest = 0; + const auto mix = [&](const std::uint8_t* row) { + for (std::uint32_t column = 0; column < Width; ++column) { + std::uint16_t word = 0; + std::memcpy(&word, row + column * 2, 2); + largest = std::max(largest, word); + hash = (hash ^ word) * 0x100000001b3ull; + } + }; + for (std::uint32_t row = 0; row < Height; ++row) mix(buffer.data() + static_cast(row) * output.framePitchInBytes); + for (std::uint32_t row = 0; row < Height / 2; ++row) mix(buffer.data() + static_cast(output.framePitchInBytes) * Height + static_cast(row) * output.framePitchInBytes); + check(largest == 1023, "ten-bit samples are not in the low bits of their words"); + check(hash == 0x453fd1cd69e47611ull, "ten-bit picture differs from the reference"); } check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); } } -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"); @@ -412,7 +405,7 @@ void testDecode(bool lengthPrefixed) { check(output.isValid == frame.isAccepted, "frame buffer acceptance disagrees with the output"); if (!output.isValid) return false; check(pictures < PictureHashes.size(), "more pictures than access units"); - check(!output.isErrorFrame && output.frameWidth == Width && output.frameHeight == Height && output.framePitch == 256 && output.frameBuffer == buffer.data(), "unexpected picture geometry"); + check(!output.isErrorFrame && output.frameWidth == Width && output.frameHeight == CodedHeight && output.framePitch == 256 && output.frameBuffer == buffer.data(), "unexpected picture geometry"); AvcPictureInfo info{sizeof(AvcPictureInfo)}; check(sceVideodec2GetPictureInfo_nid_postfix(&output, &info, nullptr) == 0 && info.isValid, "picture info missing"); const auto unit = DisplayOrderUnits[pictures]; @@ -420,13 +413,34 @@ void testDecode(bool lengthPrefixed) { check(info.idrPictureFlag == (pictures == 0 ? 1 : 0) && info.profileIdc == 100, "picture flags mismatch"); AvcPictureInfo avc{sizeof(AvcPictureInfo)}; check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr) == 0 && avc.isValid, "avc picture info missing"); - check(avc.ptsData == 1000 + unit && avc.dtsData == unit && avc.attachedData == 0xa0 + unit && avc.idrPictureFlag == (pictures == 0 ? 1 : 0) && avc.profileIdc == 100 && avc.picWidthInLumaSamples == Width && avc.picHeightInLumaSamples == Height, "avc picture info mismatch"); - check(hashNv12(buffer.data(), output.framePitch) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference"); + check(avc.ptsData == 1000 + unit && avc.dtsData == unit && avc.attachedData == 0xa0 + unit && avc.idrPictureFlag == (pictures == 0 ? 1 : 0) && avc.profileIdc == 100 && avc.picWidthInLumaSamples == Width && avc.picHeightInLumaSamples == CodedHeight, "avc picture info mismatch"); + check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference"); ++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}; @@ -446,6 +460,103 @@ void testDecode(bool lengthPrefixed) { checkThrows([&] { sceVideodec2DeleteDecoder_nid_postfix(handle); }); } +void decodeInOrder(const std::vector>& units) { + const DecoderConfigInfo config{sizeof(DecoderConfigInfo), 0, 1, 100, 0, static_cast(Width), static_cast(Height), 4, 1}; + DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; + check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0, "memory query failed"); + std::uint64_t handle = 0; + check(sceVideodec2CreateDecoder_nid_postfix(&config, &memory, &handle) == 0, "decoder creation failed"); + std::vector buffer(memory.maxFrameBufferSize); + std::size_t pictures = 0; + const auto take = [&](const OutputInfo& output) { + if (!output.isValid) return false; + check(pictures < PictureHashes.size(), "more pictures than access units"); + check(!output.isErrorFrame && output.frameWidth == Width && output.frameHeight == CodedHeight, "unexpected picture geometry"); + check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference"); + ++pictures; + return true; + }; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), 1000 + unit, unit, 0xa0 + unit}; + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "decode failed"); + take(output); + } + for (;;) { + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Flush_nid_postfix(handle, &frame, &output) == 0, "flush failed"); + if (!take(output)) break; + } + check(pictures == PictureHashes.size(), "missing pictures after flush"); + check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); +} + +bool readsAsOneLengthPrefix(const std::vector& unit) { + const std::size_t length = (static_cast(unit[0]) << 24u) | (static_cast(unit[1]) << 16u) | (static_cast(unit[2]) << 8u) | unit[3]; + return length + 4 == unit.size(); +} + +std::vector lengthPrefixedFiller(std::size_t length) { + std::vector filler; + for (int shift = 24; shift >= 0; shift -= 8) filler.push_back(static_cast(length >> shift)); + filler.push_back(0x0c); + filler.insert(filler.end(), length - 1, 0xff); + return filler; +} + +void testLengthPrefixLooksLikeStartCode() { + auto units = accessUnits(true); + const auto filler = lengthPrefixedFiller(0x101); + check(!(units[0][0] == 0 && units[0][1] == 0 && (units[0][2] == 1 || units[0][3] == 1)), "the first length-prefixed unit already begins with start-code bytes"); + units[1].insert(units[1].begin(), filler.begin(), filler.end()); + check(units[1][0] == 0 && units[1][1] == 0 && units[1][2] == 1, "the crafted unit does not begin with start-code bytes"); + decodeInOrder(units); + + auto mixed = accessUnits(false); + const auto mixedFiller = lengthPrefixedFiller(0x123); + mixed[1] = accessUnits(true)[1]; + mixed[1].insert(mixed[1].begin(), mixedFiller.begin(), mixedFiller.end()); + check(mixed[0][2] == 0 && mixed[0][3] == 1 && mixed[1][0] == 0 && mixed[1][1] == 0 && mixed[1][2] == 1, "the length-prefixed unit does not follow an Annex-B unit with a four-byte start code"); + decodeInOrder(mixed); +} + +void testStartCodeLooksLikeLengthPrefix() { + auto units = accessUnits(false); + for (auto& unit : units) { + check(unit[0] == 0 && unit[1] == 0 && unit[2] == 0 && unit[3] == 1, "an access unit does not open with a four-byte start code"); + unit.erase(unit.begin()); + } + auto& first = units.front(); + std::size_t sequence = 0; + std::size_t afterPicture = 0; + bool picture = false; + for (std::size_t index = 0; index + 3 < first.size(); ++index) { + if (first[index] != 0 || first[index + 1] != 0 || first[index + 2] != 1) continue; + const auto type = first[index + 3] & 0x1fu; + if (picture) { + afterPicture = first[index - 1] == 0 ? index - 1 : index; + break; + } + if (type == 7 && sequence == 0) sequence = index; + if (type == 8 && sequence != 0) picture = true; + index += 2; + } + check(sequence != 0 && afterPicture > sequence, "the first access unit has no SPS and PPS before another NAL"); + const std::vector delimiter(first.begin(), first.begin() + static_cast(first[sequence - 1] == 0 ? sequence - 1 : sequence)); + check(delimiter.size() >= 4 && delimiter[2] == 1 && !readsAsOneLengthPrefix(delimiter), "the access unit delimiter is not an unambiguous unit with a three-byte start code"); + std::vector parameters(first.begin() + static_cast(sequence), first.begin() + static_cast(afterPicture)); + first.erase(first.begin(), first.begin() + static_cast(afterPicture)); + if (first[2] == 0) first.erase(first.begin()); + const std::size_t padded = 4 + ((std::size_t{1} << 8u) | parameters[3]); + check(parameters.size() < padded, "the parameter sets are too long to pad"); + parameters.resize(padded, 0); + check(readsAsOneLengthPrefix(parameters), "the padded parameter sets do not read as one length-prefixed NAL"); + units.insert(units.begin(), {delimiter, parameters}); + decodeInOrder(units); +} + std::uint64_t createDecoder(std::uint32_t codec, std::uint32_t profile) { const DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, codec, profile, 0, static_cast(Width), static_cast(Height), 4, 1}; DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; @@ -516,6 +627,129 @@ std::uint64_t frameBufferBytes() { return memory.maxFrameBufferSize; } +void testEmptyAccessUnit() { + constexpr int AccessUnitSizeError = static_cast(0x811D010Du); + constexpr int AccessUnitPointerError = static_cast(0x811D010Eu); + std::vector buffer(frameBufferBytes()); + const auto handle = createDecoder(CodecHevc, 1); + const auto units = hevcUnits(HevcStream.data(), HevcStream.size()); + std::size_t pictures = 0; + const auto take = [&](const OutputInfo& output, const FrameBuffer& frame) { + check(output.isValid == frame.isAccepted, "frame buffer acceptance disagrees with the output"); + if (!output.isValid) return false; + check(pictures < HevcHashes.size(), "more pictures than access units"); + check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == HevcHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference after a refused access unit"); + ++pictures; + return true; + }; + const auto refuse = [&](const std::uint8_t* data, std::uint64_t size, int expected, const char* message) { + const InputData input{sizeof(InputData), data, size, 2000, 0, 0}; + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), true}; + OutputInfo output{sizeof(OutputInfo)}; + output.isValid = true; + output.pictureCount = 1; + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == expected, message); + check(!output.isValid && output.pictureCount == 0 && !frame.isAccepted, "a refused access unit handed out a picture"); + }; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + if (unit == 2) { + refuse(units[unit].data(), 0, AccessUnitSizeError, "an empty access unit did not return ACCESS_UNIT_SIZE"); + refuse(nullptr, units[unit].size(), AccessUnitPointerError, "a null access unit did not return ACCESS_UNIT_POINTER"); + } + const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), 1000 + unit, unit, 0}; + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "decode failed"); + take(output, frame); + } + 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, frame)) break; + } + check(pictures == HevcHashes.size(), "refused access units cost pictures"); + check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); +} + +void testFlushAfterEveryUnit() { + std::vector> units; + for (std::size_t index = 0; index + 4 < AvcIpppStream.size(); ++index) { + if (AvcIpppStream[index] == 0 && AvcIpppStream[index + 1] == 0 && AvcIpppStream[index + 2] == 0 && AvcIpppStream[index + 3] == 1 && (AvcIpppStream[index + 4] & 0x1fu) == 9) units.emplace_back(); + check(!units.empty(), "the IPPP stream does not start with an access unit delimiter"); + units.back().push_back(AvcIpppStream[index]); + } + for (std::size_t index = AvcIpppStream.size() - 4; index < AvcIpppStream.size(); ++index) units.back().push_back(AvcIpppStream[index]); + check(units.size() == 4, "unexpected IPPP access unit count"); + std::vector buffer(frameBufferBytes()); + const auto decode = [&](bool flushEachUnit) { + const auto handle = createDecoder(1, 66); + std::vector hashes; + const auto take = [&](const OutputInfo& output, const FrameBuffer& frame) { + if (!output.isValid) return false; + check(frame.isAccepted && output.frameWidth == Width && output.frameHeight == CodedHeight, "unexpected IPPP picture"); + hashes.push_back(hashNv12(buffer.data(), output.framePitch, output.frameHeight)); + return true; + }; + const auto flush = [&] { + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Flush_nid_postfix(handle, &frame, &output) == 0, "flush failed"); + return take(output, frame); + }; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), 3000 + unit, unit, 0}; + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "IPPP decode failed"); + take(output, frame); + if (flushEachUnit) flush(); + } + while (flush()) {} + check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); + return hashes; + }; + const auto reference = decode(false); + check(reference.size() == units.size(), "an IPPP picture is missing without flushes"); + check(decode(true) == reference, "a flush after each access unit lost or changed the pictures that follow it"); +} + +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()); { @@ -618,6 +852,9 @@ int main() { testFailures(); testDecode(false); testDecode(true); + testLengthPrefixLooksLikeStartCode(); + testStartCodeLooksLikeLengthPrefix(); + testDecode(false, true); const auto hevc = hevcUnits(HevcStream.data(), HevcStream.size()); testClip(CodecHevc, 1, hevc, HevcHashes); std::vector> hevcLengthPrefixed; @@ -641,7 +878,11 @@ int main() { } testClip(CodecHevc, 1, hevcLengthPrefixed, HevcHashes); testClip(CodecVp9, 0, ivfFrames(), Vp9Hashes); + testVp9PictureMetadata(); testTenBit(); + testDecodeAfterFlush(); + testEmptyAccessUnit(); + testFlushAfterEveryUnit(); testPictureOwnership(); testConcurrentOwnership(); std::puts("Videodec2 tests passed"); diff --git a/core/libs/tests/GuestVideodec2Unimplemented.cpp b/core/libs/tests/GuestVideodec2Unimplemented.cpp new file mode 100644 index 0000000000..6f2fc0ef0a --- /dev/null +++ b/core/libs/tests/GuestVideodec2Unimplemented.cpp @@ -0,0 +1,51 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVideodec2TermLibrary(); +int APS5_VABI sceVideodec2QueryLibraryInfo(); +int APS5_VABI sceVideodec2QueryDecoderInfo(); +int APS5_VABI sceVideodec2MapMemory(); +int APS5_VABI sceVideodec2MapDirectMemory(void* decoder, const void* memory); +int APS5_VABI sceVideodec2PresetDecoderConfigInfo(); +int APS5_VABI sceVideodec2PresetDecoderConfigInfoAvc(); +int APS5_VABI sceVideodec2PresetDecoderConfigInfoHevc(); +int APS5_VABI sceVideodec2PresetDecoderConfigInfoVp9(); +int APS5_VABI sceVideodec2CreateDecoderBid(); +int APS5_VABI sceVideodec2CreateHevcDecoder(); +int APS5_VABI sceVideodec2QueryHevcDecoderMemoryInfo(); +int APS5_VABI sceVideodec2GetHevcPictureInfo(); +} + +static bool ReportsNotImplemented(const std::string& name, const std::function& call) { + try { + call(); + } catch (const std::runtime_error& error) { + if (error.what() == name + " not implemented") return true; + std::fprintf(stderr, "%s threw: %s\n", name.c_str(), error.what()); + return false; + } + std::fprintf(stderr, "%s returned instead of reporting that it is not implemented\n", name.c_str()); + return false; +} + +int main() { + bool passed = ReportsNotImplemented("sceVideodec2TermLibrary", [] { return sceVideodec2TermLibrary(); }); + passed = ReportsNotImplemented("sceVideodec2QueryLibraryInfo", [] { return sceVideodec2QueryLibraryInfo(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2QueryDecoderInfo", [] { return sceVideodec2QueryDecoderInfo(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2MapMemory", [] { return sceVideodec2MapMemory(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2MapDirectMemory", [] { return sceVideodec2MapDirectMemory(nullptr, nullptr); }) && passed; + passed = ReportsNotImplemented("sceVideodec2PresetDecoderConfigInfo", [] { return sceVideodec2PresetDecoderConfigInfo(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2PresetDecoderConfigInfoAvc", [] { return sceVideodec2PresetDecoderConfigInfoAvc(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2PresetDecoderConfigInfoHevc", [] { return sceVideodec2PresetDecoderConfigInfoHevc(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2PresetDecoderConfigInfoVp9", [] { return sceVideodec2PresetDecoderConfigInfoVp9(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2CreateDecoderBid", [] { return sceVideodec2CreateDecoderBid(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2CreateHevcDecoder", [] { return sceVideodec2CreateHevcDecoder(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2QueryHevcDecoderMemoryInfo", [] { return sceVideodec2QueryHevcDecoderMemoryInfo(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2GetHevcPictureInfo", [] { return sceVideodec2GetHevcPictureInfo(); }) && passed; + return passed ? 0 : 1; +} diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp new file mode 100644 index 0000000000..2b9e024dcc --- /dev/null +++ b/core/libs/tests/GuestVoice.cpp @@ -0,0 +1,500 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVoiceInit(const VoiceInitParam*, std::int32_t); +int APS5_VABI sceVoiceSetThreadsParams(const VoiceThreadsParams*); +int APS5_VABI sceVoiceStart(const VoiceStartParam*); +int APS5_VABI sceVoiceStop(); +int APS5_VABI sceVoiceEnd_nid_postfix(); +int APS5_VABI sceVoiceCreatePort(std::uint32_t*, const VoicePortParam*); +int APS5_VABI sceVoiceDeletePort(std::uint32_t); +int APS5_VABI sceVoiceConnectIPortToOPort(std::uint32_t, std::uint32_t); +int APS5_VABI sceVoiceDisconnectIPortFromOPort(std::uint32_t, std::uint32_t); +int APS5_VABI sceVoiceGetVolume(std::uint32_t, float*); +int APS5_VABI sceVoiceSetMuteFlag(std::uint32_t, std::uint8_t); +int APS5_VABI sceVoiceSetVolume(std::uint32_t, float); +int APS5_VABI sceVoiceGetBitRate(std::uint32_t, std::uint32_t*); +int APS5_VABI sceVoiceGetMuteFlag(std::uint32_t, bool*); +int APS5_VABI sceVoiceGetPortInfo(std::uint32_t, VoicePortInfo*); +int APS5_VABI sceVoiceReadFromOPort(std::uint32_t, void*, std::uint32_t*); +int APS5_VABI sceVoiceResetPort(std::uint32_t); +int APS5_VABI sceVoicePausePort(std::uint32_t); +int APS5_VABI sceVoiceResumePort(std::uint32_t); +int APS5_VABI sceVoicePausePortAll(void); +int APS5_VABI sceVoiceResumePortAll(void); +int APS5_VABI sceVoiceGetResourceInfo(VoiceResourceInfo*); +int APS5_VABI sceVoiceEnableChat(void); +int APS5_VABI sceVoiceDisableChat(void); +int APS5_VABI sceVoiceInitHQ(const VoiceInitParam*, std::int32_t); +int APS5_VABI sceVoiceVADSetVersion(std::int32_t); +int APS5_VABI sceVoiceSetMuteFlagAll(bool); +int APS5_VABI sceVoiceSetBitRate(std::uint32_t, std::uint32_t); +int APS5_VABI sceVoiceUpdatePort(std::uint32_t, const VoicePortParam*); +} + +namespace { + +constexpr int NotInitialized = static_cast(0x804E0801u); +constexpr int AlreadyInitialized = static_cast(0x804E0802u); + +void Require(bool value) { if (!value) std::abort(); } + +template +void RequireThrows(TFunction function) { + try { function(); } catch (const std::logic_error&) { return; } + std::abort(); +} + +void TestNotInitialized() { + Require(sceVoiceStart(nullptr) == NotInitialized); + VoiceStartParam start{}; + Require(sceVoiceStart(&start) == NotInitialized); + Require(sceVoiceCreatePort(nullptr, nullptr) == NotInitialized); + VoicePortParam badType{}; + badType.port_type = 99; + std::uint32_t id = 0xA5A5A5A5u; + Require(sceVoiceCreatePort(&id, &badType) == NotInitialized); + VoicePortParam param{}; + param.port_type = 3; + Require(sceVoiceCreatePort(&id, ¶m) == NotInitialized); + Require(id == 0xA5A5A5A5u); + Require(sceVoiceGetPortInfo(0, nullptr) == NotInitialized); + VoicePortInfo info{}; + std::memset(&info, 0xA5, sizeof(info)); + VoicePortInfo sentinel = info; + Require(sceVoiceGetPortInfo(0, &info) == NotInitialized); + Require(std::memcmp(&info, &sentinel, sizeof(info)) == 0); + Require(sceVoicePausePortAll() == NotInitialized); + Require(sceVoiceResumePortAll() == NotInitialized); + Require(sceVoiceSetMuteFlagAll(true) == NotInitialized); + Require(sceVoiceSetBitRate(0, 16000) == NotInitialized); + Require(sceVoiceUpdatePort(0, nullptr) == NotInitialized); +} + +std::uint32_t CreatePort(std::int32_t type, float volume) { + VoicePortParam param{}; + param.port_type = type; + param.volume = volume; + if (type == 2 || type == 4) param.voice.bitrate = 16000; + if (type == 1 || type == 3) { + param.pcmaudio.buffer_size = 1024; + param.pcmaudio.sample_rate = 16000; + } + if (type == 5) param.device.type = 2; + std::uint32_t id = 0; + Require(sceVoiceCreatePort(&id, ¶m) == 0); + return id; +} + +} // namespace + +int main() { + TestNotInitialized(); + constexpr auto notInitialized = static_cast(0x804e0801u); + constexpr auto invalidPort = static_cast(0x804e0804u); + constexpr auto invalidArgument = static_cast(0x804e0805u); + constexpr auto portNotFound = static_cast(0x804e0807u); + Require(sceVoiceGetVolume(128, nullptr) == notInitialized); + Require(sceVoiceGetBitRate(128, nullptr) == notInitialized); + Require(sceVoiceGetMuteFlag(128, nullptr) == notInitialized); + Require(sceVoiceSetVolume(128, 1.0f) == notInitialized); + Require(sceVoiceSetMuteFlag(128, 1) == notInitialized); + Require(sceVoiceDeletePort(128) == notInitialized); + Require(sceVoiceResetPort(0) == notInitialized); + Require(sceVoicePausePort(0) == notInitialized); + Require(sceVoiceResumePort(0) == notInitialized); + Require(sceVoiceGetResourceInfo(nullptr) == notInitialized); + Require(sceVoiceEnableChat() == notInitialized); + Require(sceVoiceDisableChat() == notInitialized); + Require(sceVoiceSetThreadsParams(nullptr) == notInitialized); + VoiceThreadsParams threads{}; + Require(sceVoiceSetThreadsParams(&threads) == notInitialized); + VoiceInitParam init{}; + Require(sceVoiceInit(nullptr, 100) == invalidArgument); + for (const auto mode : {-1, 0, 1, 3, 5, 0x07ffffff, 0x08000001, 0x10000001, 0x20000001, INT32_MAX}) { + init.app_type = mode; + Require(sceVoiceInit(&init, 100) == invalidArgument); + Require(sceVoiceGetResourceInfo(nullptr) == notInitialized); + } + init.app_type = 0x10000000; + for (const auto version : {-1, 0, 99, 101, INT32_MAX}) { + Require(sceVoiceInit(&init, version) == invalidArgument); + Require(sceVoiceGetResourceInfo(nullptr) == notInitialized); + } + VoiceStartParam start{}; + Require(sceVoiceStart(nullptr) == notInitialized); + Require(sceVoiceStart(&start) == notInitialized); + Require(sceVoiceStop() == notInitialized); + Require(sceVoiceEnd_nid_postfix() == notInitialized); + std::vector voiceMemory(0xc0000); + Require(sceVoiceInit(&init, 100) == 0); + Require(sceVoiceInit(nullptr, 0) == static_cast(0x804e0802u)); + Require(sceVoiceInit(&init, 100) == static_cast(0x804e0802u)); + Require(sceVoiceSetThreadsParams(nullptr) == invalidArgument); + Require(sceVoiceSetThreadsParams(&threads) == 0); + threads.mode = 2; + Require(sceVoiceSetThreadsParams(&threads) == invalidArgument); + threads.mode = UINT32_MAX; + Require(sceVoiceSetThreadsParams(&threads) == invalidArgument); + threads.mode = 1; + RequireThrows([&] { sceVoiceSetThreadsParams(&threads); }); + VoiceThreadsParams scheduled{1, 700, 0x7f, 700, 0, 0x7f, 700, 0, 0x7f}; + Require(sceVoiceSetThreadsParams(&scheduled) == 0); + scheduled = {1, 256, 0x1fff, 767, 0, 1, 512, 0, 0x1000}; + Require(sceVoiceSetThreadsParams(&scheduled) == 0); + for (const auto priority : {255, 768, 0, -1}) { + for (int slot = 0; slot < 3; ++slot) { + VoiceThreadsParams outside{1, 700, 0x7f, 700, 0, 0x7f, 700, 0, 0x7f}; + (slot == 0 ? outside.firstPriority : slot == 1 ? outside.secondPriority : outside.thirdPriority) = priority; + RequireThrows([&] { sceVoiceSetThreadsParams(&outside); }); + } + } + for (const std::uint64_t affinity : {std::uint64_t{0}, std::uint64_t{0x2000}, std::uint64_t{0x3fff}, UINT64_MAX}) { + for (int slot = 0; slot < 3; ++slot) { + VoiceThreadsParams outside{1, 700, 0x7f, 700, 0, 0x7f, 700, 0, 0x7f}; + (slot == 0 ? outside.firstAffinity : slot == 1 ? outside.secondAffinity : outside.thirdAffinity) = affinity; + RequireThrows([&] { sceVoiceSetThreadsParams(&outside); }); + } + } + threads.mode = 0; + Require(sceVoiceStart(nullptr) == invalidArgument); + Require(sceVoiceStart(&start) == invalidArgument); + start.container = voiceMemory.data(); + Require(sceVoiceStart(&start) == static_cast(0x804e0806u)); + start.mem_size = 0x40000; + Require(sceVoiceStart(&start) == 0); + start.mem_size = 0; + Require(sceVoiceStart(&start) == 0); + Require(sceVoiceStart(nullptr) == invalidArgument); + start.container = nullptr; + Require(sceVoiceStart(&start) == invalidArgument); + start.container = voiceMemory.data(); + start.mem_size = 0x40000; + Require(sceVoiceSetThreadsParams(nullptr) == invalidArgument); + Require(sceVoiceSetThreadsParams(&threads) == static_cast(0x804e080bu)); + scheduled = {1, 700, 0x7f, 700, 0, 0x7f, 700, 0, 0x7f}; + Require(sceVoiceSetThreadsParams(&scheduled) == static_cast(0x804e080bu)); + Require(sceVoiceGetResourceInfo(nullptr) == invalidArgument); + struct ResourceOutput { + VoiceResourceInfo info; + std::uint32_t guard; + } resourceOutput{{}, 0xa5a5a5a5u}; + Require(sceVoiceGetResourceInfo(&resourceOutput.info) == 0); + Require(resourceOutput.info.maxVoiceInputPorts == 15); + Require(resourceOutput.info.maxVoiceOutputPorts == 4); + Require(resourceOutput.info.maxDeviceInputPorts == 4); + Require(resourceOutput.info.maxDeviceOutputPorts == 4); + Require(resourceOutput.guard == 0xa5a5a5a5u); + RequireThrows([] { sceVoiceEnableChat(); }); + RequireThrows([] { sceVoiceDisableChat(); }); + const auto input = CreatePort(2, 0.5f); + const auto output = CreatePort(3, 0.25f); + Require(sceVoiceConnectIPortToOPort(input, output) == 0); + Require(sceVoiceSetMuteFlag(output, true) == 0); + std::uint32_t portIdForInfo = 0; + VoicePortInfo before{}; + before.edge = &portIdForInfo; + before.edge_count = 0x1234; + before.reserved = 0x5678; + Require(sceVoiceGetPortInfo(input, &before) == 0); + Require(before.frame_size == 20 && before.byte_count == 0); + Require(before.edge == &portIdForInfo && before.edge_count == 0x1234 && before.reserved == 0x5678); + VoicePortInfo pcmInfo{}; + Require(sceVoiceGetPortInfo(output, &pcmInfo) == 0 && pcmInfo.frame_size == 2); + + Require(sceVoiceResetPort(input) == 0); + Require(sceVoiceResetPort(output) == 0); + Require(sceVoiceResetPort(output + 1) == portNotFound); + Require(sceVoiceResetPort(128) == invalidPort); + + Require(sceVoicePausePort(input) == 0); + Require(sceVoicePausePort(input) == 0); + Require(sceVoicePausePort(output) == 0); + VoicePortInfo paused{}; + Require(sceVoiceGetPortInfo(input, &paused) == 0); + Require(paused.port_type == before.port_type && paused.state == before.state && paused.frame_size == before.frame_size); + Require(sceVoiceResumePort(input) == 0); + Require(sceVoiceResumePort(input) == 0); + Require(sceVoiceResumePort(output) == 0); + Require(sceVoicePausePortAll() == 0); + Require(sceVoicePausePortAll() == 0); + Require(sceVoiceGetPortInfo(input, &paused) == 0); + Require(paused.port_type == before.port_type && paused.state == before.state && paused.frame_size == before.frame_size); + Require(sceVoiceResumePortAll() == 0); + Require(sceVoiceResumePortAll() == 0); + Require(sceVoicePausePort(output + 1) == portNotFound); + Require(sceVoicePausePort(128) == invalidPort); + Require(sceVoiceResumePort(output + 1) == portNotFound); + Require(sceVoiceResumePort(128) == invalidPort); + + VoicePortInfo after{}; + Require(sceVoiceGetPortInfo(input, &after) == 0); + Require(after.port_type == before.port_type && after.state == before.state && after.frame_size == before.frame_size); + float volume = 0; + bool muted = false; + Require(sceVoiceGetVolume(output, &volume) == 0 && volume == 0.25f); + Require(sceVoiceGetMuteFlag(output, &muted) == 0 && muted); + std::uint8_t data[64]{}; + std::uint32_t size = sizeof(data); + Require(sceVoiceReadFromOPort(output, data, &size) == 0 && size == 0); + Require(sceVoiceDisconnectIPortFromOPort(input, output) == 0); + + Require(sceVoiceGetMuteFlag(input, &muted) == 0 && !muted); + Require(sceVoiceSetMuteFlagAll(true) == 0); + Require(sceVoiceGetMuteFlag(input, &muted) == 0 && muted); + Require(sceVoiceSetMuteFlagAll(false) == 0); + Require(sceVoiceGetMuteFlag(input, &muted) == 0 && !muted); + Require(sceVoiceGetMuteFlag(output, &muted) == 0 && muted); + + std::uint32_t portBitrate = 0; + Require(sceVoiceSetBitRate(input, 7300) == 0); + Require(sceVoiceGetBitRate(input, &portBitrate) == 0 && portBitrate == 7300); + VoicePortInfo rateInfo{}; + Require(sceVoiceGetPortInfo(input, &rateInfo) == 0 && rateInfo.frame_size == 19); + Require(sceVoiceSetBitRate(output + 1, 7300) == portNotFound); + Require(sceVoiceSetBitRate(128, 7300) == invalidPort); + RequireThrows([&] { sceVoiceSetBitRate(output, 7300); }); + RequireThrows([&] { sceVoiceSetBitRate(input, 7301); }); + RequireThrows([&] { sceVoiceSetBitRate(input, 0x80000000u); }); + Require(sceVoiceGetBitRate(input, &portBitrate) == 0 && portBitrate == 7300); + + VoicePortParam update{}; + update.port_type = 2; + update.mute = 1; + update.volume = 0.75f; + update.voice.bitrate = 14400; + Require(sceVoiceUpdatePort(input, &update) == 0); + Require(sceVoiceGetMuteFlag(input, &muted) == 0 && muted); + Require(sceVoiceGetVolume(input, &volume) == 0 && volume == 0.75f); + Require(sceVoiceGetBitRate(input, &portBitrate) == 0 && portBitrate == 14400); + Require(sceVoiceGetPortInfo(input, &rateInfo) == 0 && rateInfo.frame_size == 27); + update.voice.bitrate = 1; + RequireThrows([&] { sceVoiceUpdatePort(input, &update); }); + Require(sceVoiceGetBitRate(input, &portBitrate) == 0 && portBitrate == 14400); + update.voice.bitrate = 14400; + update.port_type = 3; + RequireThrows([&] { sceVoiceUpdatePort(input, &update); }); + RequireThrows([&] { sceVoiceUpdatePort(input, nullptr); }); + Require(sceVoiceUpdatePort(output + 1, &update) == portNotFound); + Require(sceVoiceUpdatePort(128, &update) == invalidPort); + VoicePortParam pcm{}; + pcm.port_type = 3; + pcm.volume = std::numeric_limits::quiet_NaN(); + Require(sceVoiceUpdatePort(output, &pcm) == 0); + Require(sceVoiceGetMuteFlag(output, &muted) == 0 && !muted); + Require(sceVoiceGetVolume(output, &volume) == 0 && volume == 0.0f); + pcm.volume = 0.25f; + pcm.mute = 1; + Require(sceVoiceUpdatePort(output, &pcm) == 0); + + Require(sceVoiceSetMuteFlagAll(true) == 0); + + Require(sceVoiceDeletePort(input) == 0); + Require(sceVoiceResetPort(input) == portNotFound); + Require(sceVoicePausePort(input) == portNotFound); + Require(sceVoiceResetPort(output) == 0); + Require(sceVoiceEnd_nid_postfix() == 0); + TestNotInitialized(); + Require(sceVoiceResetPort(output) == notInitialized); + Require(sceVoiceResumePort(output) == notInitialized); + Require(sceVoiceGetResourceInfo(nullptr) == notInitialized); + Require(sceVoiceEnableChat() == notInitialized); + Require(sceVoiceDisableChat() == notInitialized); + for (const auto mode : {4, 0x08000000, 0x10000000, 0x20000000}) { + init.app_type = mode; + Require(sceVoiceInit(&init, 100) == 0); + const std::uint32_t memorySize = mode == 4 || mode == 0x08000000 ? 0xc0000 : 0x40000; + start.mem_size = memorySize - 1; + Require(sceVoiceStart(&start) == static_cast(0x804e0806u)); + start.mem_size = memorySize + 1; + Require(sceVoiceStart(&start) == static_cast(0x804e0806u)); + start.mem_size = memorySize; + Require(sceVoiceStart(&start) == 0); + Require(sceVoiceStop() == 0); + Require(sceVoiceStop() == 0); + Require(sceVoiceSetThreadsParams(&threads) == 0); + VoicePortParam portParam{}; + portParam.port_type = 0; + std::uint32_t port = UINT32_MAX; + Require(sceVoiceCreatePort(&port, &portParam) == 0 && port == 0); + Require(sceVoiceGetResourceInfo(&resourceOutput.info) == 0); + Require(resourceOutput.info.maxVoiceInputPorts == (mode == 4 ? 0 : mode == 0x20000000 ? 7 : 15)); + Require(resourceOutput.info.maxVoiceOutputPorts == (mode == 4 ? 0 : 4)); + Require(resourceOutput.info.maxDeviceInputPorts == 4); + Require(resourceOutput.info.maxDeviceOutputPorts == (mode == 4 ? 5 : 4)); + Require(resourceOutput.guard == 0xa5a5a5a5u); + Require(sceVoiceEnd_nid_postfix() == 0); + } + + constexpr auto resourceFull = static_cast(0x804e0808u); + std::uint32_t portId = 123; + VoicePortParam param{}; + Require(sceVoiceCreatePort(nullptr, nullptr) == notInitialized); + Require(sceVoiceCreatePort(&portId, ¶m) == notInitialized && portId == 123); + for (const auto mode : {4, 0x08000000, 0x10000000, 0x20000000}) { + init.app_type = mode; + Require(sceVoiceInit(&init, 100) == 0); + Require(sceVoiceCreatePort(&portId, nullptr) == invalidArgument && portId == 123); + Require(sceVoiceCreatePort(nullptr, ¶m) == invalidArgument); + for (const auto type : {-1, 6, INT32_MAX}) { + param = {}; + param.port_type = type; + Require(sceVoiceCreatePort(&portId, ¶m) == invalidArgument && portId == 255); + } + for (const auto type : {0, 2, 4, 5}) { + param = {}; + param.port_type = type; + if (type == 2 || type == 4) param.voice.bitrate = 16000; + if (type == 5) param.device.type = 14; + const auto limit = type == 0 ? 4 : type == 5 ? (mode == 4 ? 5 : 4) : mode == 4 ? 0 : type == 4 ? 4 : mode == 0x20000000 ? 7 : 15; + std::vector ports; + for (int i = 0; i < limit; ++i) { + Require(sceVoiceCreatePort(&portId, ¶m) == 0 && portId < 128); + ports.push_back(portId); + } + Require(sceVoiceCreatePort(&portId, ¶m) == resourceFull && portId == 255); + if (type == 0 || type == 5) { + param.device.type = 1; + Require(sceVoiceCreatePort(&portId, ¶m) == resourceFull); + param.device.type = type == 0 ? 0 : 2; + } + for (auto id : ports) Require(sceVoiceDeletePort(id) == 0); + if (limit != 0) { + Require(sceVoiceCreatePort(&portId, ¶m) == 0); + Require(sceVoiceDeletePort(portId) == 0); + } + } + param = {}; + param.port_type = 3; + param.pcmaudio.sample_rate = 16000; + param.pcmaudio.buffer_size = 1024; + const auto limit = mode == 4 ? 128 : mode == 0x08000000 ? 30 : mode == 0x10000000 ? 10 : 14; + std::vector ports; + for (int i = 0; i < limit; ++i) { + Require(sceVoiceCreatePort(&portId, ¶m) == 0 && portId < 128); + for (auto id : ports) Require(id != portId); + ports.push_back(portId); + } + Require(sceVoiceCreatePort(&portId, ¶m) == resourceFull && portId == 255); + const auto released = ports.front(); + Require(sceVoiceDeletePort(released) == 0); + Require(sceVoiceCreatePort(&portId, ¶m) == 0); + if (mode == 4) Require(portId == released); + Require(sceVoiceEnd_nid_postfix() == 0); + portId = 123; + } + init.app_type = 0x08000000; + Require(sceVoiceInit(&init, 100) == 0); + for (const auto type : {0, 5}) { + param = {}; + param.port_type = type; + for (const auto deviceType : {-1, 0, 1, 2, 3, 13, 14, 15, INT32_MAX}) { + param.device.type = deviceType; + const bool valid = type == 0 ? deviceType == 0 : deviceType == 2 || deviceType == 14; + Require(sceVoiceCreatePort(&portId, ¶m) == (valid ? 0 : invalidArgument)); + if (valid) Require(sceVoiceDeletePort(portId) == 0); + else Require(portId == 255); + } + } + for (const auto type : {1, 3}) { + param = {}; + param.port_type = type; + param.pcmaudio.buffer_size = 1024; + Require(sceVoiceCreatePort(&portId, ¶m) == invalidArgument); + param.pcmaudio.sample_rate = 16000; + param.pcmaudio.data_type = 1; + Require(sceVoiceCreatePort(&portId, ¶m) == invalidArgument); + param.pcmaudio.data_type = 0; + for (const std::uint32_t bytes : {0u, 511u, 512u, 1023u, 1024u, 1535u, 64512u, 65023u, 65024u, 65535u, 65536u, 66560u, UINT32_MAX}) { + param.pcmaudio.buffer_size = bytes; + const auto rounded = static_cast(bytes - bytes % 512); + const bool valid = rounded >= 1024 && rounded <= 64512; + Require(sceVoiceCreatePort(&portId, ¶m) == (valid ? 0 : invalidArgument)); + if (valid) Require(sceVoiceDeletePort(portId) == 0); + } + param.pcmaudio.buffer_size = 1024; + param.threshold = 33; + Require(sceVoiceCreatePort(&portId, ¶m) == (type == 1 ? resourceFull : 0)); + if (type == 3) Require(sceVoiceDeletePort(portId) == 0); + param.threshold = 32; + Require(sceVoiceCreatePort(&portId, ¶m) == 0); + Require(sceVoiceDeletePort(portId) == 0); + } + for (const auto type : {2, 4}) { + param = {}; + param.port_type = type; + for (const auto bitrate : {-1, 0, 3849, 3850, 4650, 5700, 7300, 11800, 12200, 14400, 16000, 16001, 22533, 22534}) { + param.voice.bitrate = bitrate; + const bool valid = bitrate == 3850 || bitrate == 4650 || bitrate == 5700 || bitrate == 7300 || bitrate == 11800 || bitrate == 12200 || bitrate == 14400 || bitrate == 16000 || bitrate == 22533; + Require(sceVoiceCreatePort(&portId, ¶m) == (valid ? 0 : invalidArgument)); + if (valid) Require(sceVoiceDeletePort(portId) == 0); + } + param.voice.bitrate = 16000; + param.threshold = 1021; + Require(sceVoiceCreatePort(&portId, ¶m) == (type == 2 ? resourceFull : 0)); + if (type == 4) Require(sceVoiceDeletePort(portId) == 0); + param.threshold = 1020; + param.mute = 1; + Require(sceVoiceCreatePort(&portId, ¶m) == 0); + Require(sceVoiceGetMuteFlag(portId, &muted) == 0 && muted); + Require(sceVoiceDeletePort(portId) == 0); + } + Require(sceVoiceGetVolume(128, nullptr) == invalidPort); + Require(sceVoiceGetBitRate(128, nullptr) == invalidPort); + Require(sceVoiceGetMuteFlag(128, nullptr) == invalidPort); + Require(sceVoiceSetVolume(128, 1.0f) == invalidPort); + Require(sceVoiceSetMuteFlag(128, 1) == invalidPort); + Require(sceVoiceDeletePort(128) == invalidPort); + Require(sceVoiceGetVolume(127, nullptr) == invalidArgument); + Require(sceVoiceGetBitRate(127, nullptr) == invalidArgument); + Require(sceVoiceGetMuteFlag(127, nullptr) == invalidArgument); + volume = 123.0f; + std::uint32_t bitrate = 123; + muted = true; + Require(sceVoiceGetVolume(127, &volume) == portNotFound && volume == 123.0f); + Require(sceVoiceGetBitRate(127, &bitrate) == portNotFound && bitrate == 123); + Require(sceVoiceGetMuteFlag(127, &muted) == portNotFound && muted); + Require(sceVoiceSetVolume(127, 1.0f) == portNotFound); + Require(sceVoiceSetMuteFlag(127, 1) == portNotFound); + Require(sceVoiceDeletePort(127) == portNotFound); + for (const auto type : {0, 1, 2, 3, 4, 5}) { + const auto id = CreatePort(type, -2.0f); + Require(sceVoiceGetBitRate(id, &bitrate) == (type == 2 || type == 4 ? 0 : portNotFound)); + if (type == 2 || type == 4) Require(bitrate == 16000); + for (const float value : {-2.0f, -0.0f, 0.25f, 2.0f, std::numeric_limits::max(), std::numeric_limits::infinity(), -std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()}) { + Require(sceVoiceSetVolume(id, value) == 0); + Require(sceVoiceGetVolume(id, &volume) == 0); + Require(volume == (std::isfinite(value) ? value : 0.0f)); + if (value == 0.0f) Require(std::signbit(volume) == std::signbit(value)); + } + for (const std::uint8_t value : {0, 1, 2, 128, 255}) { + Require(sceVoiceSetMuteFlag(id, value) == 0); + Require(sceVoiceGetMuteFlag(id, &muted) == 0 && muted == (value != 0)); + } + Require(sceVoiceDeletePort(id) == 0); + } + Require(sceVoiceEnd_nid_postfix() == 0); + + Require(sceVoiceVADSetVersion(0) == 0 && sceVoiceVADSetVersion(1) == 0); + Require(sceVoiceVADSetVersion(2) == invalidArgument && sceVoiceVADSetVersion(-1) == invalidArgument); + init.app_type = 0x10000000; + start.mem_size = 0x40000; + Require(sceVoiceInitHQ(nullptr, 100) == invalidArgument && sceVoiceInitHQ(&init, 0) == invalidArgument); + Require(sceVoiceInitHQ(&init, 100) == 0); + Require(sceVoiceInitHQ(&init, 100) == AlreadyInitialized); + Require(sceVoiceInit(&init, 100) == AlreadyInitialized); + Require(sceVoiceStart(&start) == 0); + Require(sceVoiceEnd_nid_postfix() == 0); + Require(sceVoiceInit(&init, 100) == 0); + Require(sceVoiceInitHQ(&init, 100) == AlreadyInitialized); + Require(sceVoiceEnd_nid_postfix() == 0); +} diff --git a/core/libs/tests/GuestVoiceChat.cpp b/core/libs/tests/GuestVoiceChat.cpp new file mode 100644 index 0000000000..0a94bcefa8 --- /dev/null +++ b/core/libs/tests/GuestVoiceChat.cpp @@ -0,0 +1,56 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include + +extern "C" { +int APS5_VABI sceVoiceChatInitialize(void* param); +int APS5_VABI sceVoiceChatTerminate(void); +int APS5_VABI sceVoiceChatRegisterHandlers(void* a); +int APS5_VABI sceVoiceChatRegisterMicEventHandler(void* a, void* b); +int APS5_VABI sceVoiceChatRegisterChannelEventHandler(void* a, void* b); +int APS5_VABI sceVoiceChatUnregisterMicEventHandler(void* a); +int APS5_VABI sceVoiceChatProcessEvent(void); +int APS5_VABI sceVoiceChatStartChannelEvent(void* a); +int APS5_VABI sceVoiceChatStopChannelEvent(void* a); +int APS5_VABI sceVoiceChatGetMicState(int32_t* state); +int APS5_VABI sceVoiceChatGetUserVoiceVolumeGain(void* a, int32_t* gain); +int APS5_VABI sceVoiceChatSetUserVoiceVolumeGain(void* a, int32_t gain); +int APS5_VABI sceVoiceChatGetActiveChannel(void* channel); +int APS5_VABI sceVoiceChatCreateRequest(void* a, void* b); +int APS5_VABI sceVoiceChatRequestAcsTts(int requestId, void* a, void* b); +int APS5_VABI sceVoiceChatAbortRequest(int requestId); +} + +static void Require(bool value, const char* what) { + if (value) return; + std::printf("VoiceChat: %s\n", what); + std::abort(); +} + +int main() { + constexpr int kSignedOut = static_cast(0x80550006u); + + Require(sceVoiceChatInitialize(nullptr) == 0, "initialization failed"); + Require(sceVoiceChatProcessEvent() == 0, "event processing failed"); + Require(sceVoiceChatRegisterHandlers(nullptr) == 0, "register handlers failed"); + Require(sceVoiceChatRegisterMicEventHandler(nullptr, nullptr) == 0, "register mic event handler failed"); + Require(sceVoiceChatRegisterChannelEventHandler(nullptr, nullptr) == 0, "register channel event handler failed"); + Require(sceVoiceChatUnregisterMicEventHandler(nullptr) == 0, "unregister mic event handler failed"); + Require(sceVoiceChatStartChannelEvent(nullptr) == 0, "start channel event failed"); + Require(sceVoiceChatStopChannelEvent(nullptr) == 0, "stop channel event failed"); + + Require(sceVoiceChatGetMicState(nullptr) == kSignedOut, "get mic state did not return signed out"); + Require(sceVoiceChatGetUserVoiceVolumeGain(nullptr, nullptr) == kSignedOut, "get volume gain did not return signed out"); + Require(sceVoiceChatSetUserVoiceVolumeGain(nullptr, 50) == kSignedOut, "set volume gain did not return signed out"); + + Require(sceVoiceChatGetActiveChannel(nullptr) == kSignedOut, "get active channel did not return signed out"); + Require(sceVoiceChatCreateRequest(nullptr, nullptr) == kSignedOut, "create request did not return signed out"); + Require(sceVoiceChatRequestAcsTts(1, nullptr, nullptr) == kSignedOut, "tts request did not return signed out"); + Require(sceVoiceChatAbortRequest(1) == 0, "abort request failed"); + Require(sceVoiceChatTerminate() == 0, "termination failed"); + + std::puts("VoiceChat tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestVoicePortAttr.cpp b/core/libs/tests/GuestVoicePortAttr.cpp new file mode 100644 index 0000000000..ceaa036b65 --- /dev/null +++ b/core/libs/tests/GuestVoicePortAttr.cpp @@ -0,0 +1,72 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVoiceInit(const 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(TCall 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 auto notInitialized = static_cast(0x804e0801u); + constexpr auto invalidPort = static_cast(0x804e0804u); + constexpr auto invalidArgument = static_cast(0x804e0805u); + Require(sceVoiceGetPortAttr(128, 1001, nullptr, 1) == notInitialized); + VoiceInitParam init{}; + init.app_type = 0x10000000; + Require(sceVoiceInit(&init, 100) == 0); + VoicePortParam param{}; + param.volume = 1.0f; + std::array buffer{0xa5, 0xa5, 0xa5, 0xa5, 0xa5, 0xa5}; + const auto original = buffer; + Require(sceVoiceGetPortAttr(128, 1001, nullptr, 1) == invalidPort); + Require(sceVoiceGetPortAttr(0, 1001, nullptr, 1) == invalidArgument); + for (std::int32_t type = 0; type <= 5; ++type) { + param = {}; + param.port_type = type; + if (type == 1 || type == 3) { + param.pcmaudio.buffer_size = 1024; + param.pcmaudio.sample_rate = 16000; + } + if (type == 2 || type == 4) param.voice.bitrate = 16000; + if (type == 5) param.device.type = 2; + std::uint32_t port = 0; + Require(sceVoiceCreatePort(&port, ¶m) == 0); + for (const auto attr : {1000, 1001, 1002, 1003, 2000, 2001, 2002}) { + const auto size = attr == 1003 ? 2 : attr == 1002 || attr == 2001 ? 4 : 1; + Require(sceVoiceGetPortAttr(port, attr, buffer.data() + 1, size + 1) == invalidArgument); + if (type == (attr == 1003 ? 5 : 0)) { + Require(Throws([&] { sceVoiceGetPortAttr(port, attr, buffer.data() + 1, size); }, "device attribute backend")); + } else { + Require(sceVoiceGetPortAttr(port, attr, buffer.data() + 1, size) == invalidArgument); + } + Require(buffer == original); + } + Require(sceVoiceGetPortAttr(port, 1, buffer.data() + 1, 1) == invalidArgument); + Require(sceVoiceDeletePort(port) == 0); + Require(sceVoiceGetPortAttr(port, 1001, buffer.data() + 1, 1) == invalidPort); + } + Require(sceVoiceEnd_nid_postfix() == 0); + return 0; +} diff --git a/core/libs/tests/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp index 44aa996344..b4d318548e 100644 --- a/core/libs/tests/GuestVoiceQoS.cpp +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -1,15 +1,48 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include 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); +int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute(std::int32_t, std::int32_t, void*, std::int32_t); +int APS5_VABI sceVoiceQoSGetRemoteEndpointAttribute(std::int32_t, std::int32_t, void*, std::int32_t); +int APS5_VABI sceVoiceQoSGetConnectionAttribute(std::int32_t, std::int32_t, void*, std::int32_t); +int APS5_VABI sceVoiceQoSGetLocalEndpoint(std::int32_t, std::int32_t*); +int APS5_VABI sceVoiceQoSGetRemoteEndpoint(std::int32_t, std::int32_t*); +int APS5_VABI sceVoiceQoSEnableChat(); +int APS5_VABI sceVoiceQoSDisableChat(); +int APS5_VABI sceVoiceQoSGetStatus(std::int32_t, std::int32_t, void*, std::int32_t); +int APS5_VABI sceVoiceQoSDebugGetStatus(std::int32_t, std::int32_t, void*, std::int32_t); +int APS5_VABI sceVoiceQoSInitHQ(void*, std::uint32_t, std::int32_t); +int APS5_VABI sceVoiceQoSSetMode(); +int APS5_VABI sceVoiceQoSTerminate(); +int APS5_VABI sceVoiceQoSReadPacket(std::int32_t, void*, std::uint32_t*); +int APS5_VABI sceVoiceQoSWritePacket(std::int32_t, const void*, std::uint32_t*); } static void Require(bool value) { if (!value) std::abort(); } +template +static bool NotImplemented(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + int main(int argc, char** argv) { Require(argc == 2); constexpr int argumentInvalid = static_cast(0x804E0902); @@ -18,6 +51,10 @@ int main(int argc, char** argv) { const auto appType = static_cast(std::strtoul(argv[1], nullptr, 16)); std::vector memory(0x40000, 0xAA); Require(sceVoiceQoSEnd() == notInitialized); + std::int32_t uninitLocal = -1; + std::int32_t uninitRemote = -1; + Require(sceVoiceQoSGetLocalEndpoint(1, &uninitLocal) == notInitialized); + Require(sceVoiceQoSGetRemoteEndpoint(1, &uninitRemote) == notInitialized); Require(sceVoiceQoSInit(nullptr, 0x40000, appType) == argumentInvalid); Require(sceVoiceQoSInit(nullptr, 0, appType) == argumentInvalid); for (std::uint32_t size : {0u, 1u, 0x100u, 0x3FFFFu, 0x40001u, 0x80000u, 0xFFFFFFFFu}) Require(sceVoiceQoSInit(memory.data(), size, appType) == argumentInvalid); @@ -28,11 +65,96 @@ 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); + Require(sceVoiceQoSConnect(&connection, 1, remote) == argumentInvalid); + Require(sceVoiceQoSConnect(&connection, local, remote) == 0 && connection == 0); + std::int32_t queriedLocal = -1; + std::int32_t queriedRemote = -1; + Require(sceVoiceQoSGetLocalEndpoint(connection, nullptr) == argumentInvalid); + Require(sceVoiceQoSGetRemoteEndpoint(connection, nullptr) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpoint(connection + 100, &queriedLocal) == argumentInvalid); + Require(sceVoiceQoSGetRemoteEndpoint(connection + 100, &queriedRemote) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpoint(connection, &queriedLocal) == 0 && queriedLocal == local); + Require(sceVoiceQoSGetRemoteEndpoint(connection, &queriedRemote) == 0 && queriedRemote == remote); + std::int32_t duplicate = -1; + Require(sceVoiceQoSConnect(&duplicate, local, remote) == 0 && duplicate > 0 && duplicate != connection); + Require(sceVoiceQoSDisconnect(duplicate) == 0); + + constexpr int limitReached = static_cast(0x804E0907); + std::int32_t otherLocal = 12; + Require(sceVoiceQoSCreateLocalEndpoint(&otherLocal, 1, 0, 0) == limitReached && otherLocal == -1); + Require(sceVoiceQoSCreateLocalEndpoint(&otherLocal, 2, 0, 0) == limitReached && otherLocal == -1); + + const std::uint32_t level = 0x1234; + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, sizeof(level)) == argumentInvalid); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, nullptr, sizeof(level)) == argumentInvalid); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, 0) == argumentInvalid); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, -1) == argumentInvalid); + Require(sceVoiceQoSSetLocalEndpointAttribute(local + 100, 3, &level, sizeof(level)) == argumentInvalid); + + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, sizeof(level)) == argumentInvalid); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, nullptr, sizeof(level)) == argumentInvalid); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, 0) == argumentInvalid); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, -1) == argumentInvalid); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote + 100, 3, &level, sizeof(level)) == argumentInvalid); + + std::vector packet(1024, 0); + std::uint32_t packetSize = static_cast(packet.size()); + Require(sceVoiceQoSReadPacket(connection, packet.data(), &packetSize) == 0 && packetSize == 0); + Require(sceVoiceQoSReadPacket(connection, nullptr, &packetSize) == argumentInvalid); + Require(sceVoiceQoSReadPacket(connection, packet.data(), nullptr) == argumentInvalid); + Require(sceVoiceQoSReadPacket(connection + 100, packet.data(), &packetSize) == argumentInvalid); + + packetSize = 100; + Require(sceVoiceQoSWritePacket(connection, packet.data(), &packetSize) == 0); + Require(sceVoiceQoSWritePacket(connection, nullptr, &packetSize) == argumentInvalid); + Require(sceVoiceQoSWritePacket(connection, packet.data(), nullptr) == argumentInvalid); + Require(sceVoiceQoSWritePacket(connection + 100, packet.data(), &packetSize) == argumentInvalid); + + Require(NotImplemented([&] { sceVoiceQoSSetLocalEndpointAttribute(local, 0, &level, 4); })); + Require(NotImplemented([&] { sceVoiceQoSSetRemoteEndpointAttribute(remote, 7, &level, 1); })); + Require(sceVoiceQoSDisconnect(connection) == 0); + Require(sceVoiceQoSDisconnect(connection) == 0); + Require(sceVoiceQoSGetLocalEndpoint(connection, &queriedLocal) == 0 && queriedLocal == -1); + Require(sceVoiceQoSGetRemoteEndpoint(connection, &queriedRemote) == 0 && queriedRemote == -1); + Require(sceVoiceQoSDeleteLocalEndpoint(local) == 0); + Require(sceVoiceQoSDeleteLocalEndpoint(local) == argumentInvalid); + Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == 0); + + const int endpointLimit = appType == 0x10000000 ? 15 : 7; + for (int index = 0; index < endpointLimit; ++index) Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == 0 && remote == index); + Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == limitReached && remote == -1); + Require(sceVoiceQoSDeleteRemoteEndpoint(2) == 0); + Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == 0 && remote == 2); + Require(sceVoiceQoSEnd() == 0); + Require(sceVoiceQoSEnd() == notInitialized); + Require(sceVoiceQoSGetLocalEndpoint(connection, &queriedLocal) == notInitialized); + Require(sceVoiceQoSGetRemoteEndpoint(connection, &queriedRemote) == notInitialized); + Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == notInitialized); + Require(sceVoiceQoSCreateLocalEndpoint(&local, 1, 0, 0) == notInitialized); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, sizeof(level)) == notInitialized); + Require(sceVoiceQoSReadPacket(connection, packet.data(), &packetSize) == notInitialized); + Require(sceVoiceQoSWritePacket(connection, packet.data(), &packetSize) == notInitialized); + Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); + Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == argumentInvalid); Require(sceVoiceQoSEnd() == 0); Require(sceVoiceQoSEnd() == notInitialized); Require(sceVoiceQoSInit(nullptr, 0x40000, appType) == argumentInvalid); - Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); - Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == initialized); - Require(sceVoiceQoSEnd() == 0); - Require(sceVoiceQoSEnd() == notInitialized); + + Require(NotImplemented([] { sceVoiceQoSGetLocalEndpointAttribute(0, 0, nullptr, 0); })); + Require(NotImplemented([] { sceVoiceQoSGetRemoteEndpointAttribute(1, 1, nullptr, 0); })); + Require(NotImplemented([] { sceVoiceQoSGetConnectionAttribute(1, 1, nullptr, 0); })); + Require(NotImplemented([] { sceVoiceQoSEnableChat(); })); + Require(NotImplemented([] { sceVoiceQoSDisableChat(); })); + Require(NotImplemented([] { sceVoiceQoSGetStatus(1, 1, nullptr, 0); })); + Require(NotImplemented([] { sceVoiceQoSDebugGetStatus(0, 200, nullptr, 4); })); + Require(NotImplemented([] { sceVoiceQoSInitHQ(nullptr, 0xc0000, 0x80000); })); + Require(NotImplemented([] { sceVoiceQoSSetMode(); })); + Require(NotImplemented([] { sceVoiceQoSTerminate(); })); } diff --git a/core/libs/tests/GuestVorbisDec.cpp b/core/libs/tests/GuestVorbisDec.cpp new file mode 100644 index 0000000000..d34eb5d9ad --- /dev/null +++ b/core/libs/tests/GuestVorbisDec.cpp @@ -0,0 +1,650 @@ +#include "prx/libSceVorbisDec/include/GuestVorbisFile.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace VorbisDec; + +extern "C" { +int APS5_VABI sceVorbisDecOpenCallbacks(void* datasource, void* vf, const char* initial, std::int64_t ibytes, GuestCallbacks callbacks); +int APS5_VABI sceVorbisDecClear(void* vf); +std::int64_t APS5_VABI sceVorbisDecBitrate(void* vf, int link); +std::int64_t APS5_VABI sceVorbisDecBitrateInstant(void* vf); +std::int64_t APS5_VABI sceVorbisDecStreams(void* vf); +std::int64_t APS5_VABI sceVorbisDecSeekable(void* vf); +std::int64_t APS5_VABI sceVorbisDecSerialNumber(void* vf, int link); +std::int64_t APS5_VABI sceVorbisDecRawTotal(void* vf, int link); +std::int64_t APS5_VABI sceVorbisDecPcmTotal(void* vf, int link); +double APS5_VABI sceVorbisDecTimeTotal(void* vf, int link); +int APS5_VABI sceVorbisDecRawSeek(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecPcmSeek(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecPcmSeekPage(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecTimeSeek(void* vf, double position); +int APS5_VABI sceVorbisDecTimeSeekPage(void* vf, double position); +int APS5_VABI sceVorbisDecRawSeekLap(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecPcmSeekLap(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecPcmSeekPageLap(void* vf, std::int64_t position); +int APS5_VABI sceVorbisDecTimeSeekLap(void* vf, double position); +int APS5_VABI sceVorbisDecTimeSeekPageLap(void* vf, double position); +std::int64_t APS5_VABI sceVorbisDecRawTell(void* vf); +std::int64_t APS5_VABI sceVorbisDecPcmTell(void* vf); +double APS5_VABI sceVorbisDecTimeTell(void* vf); +GuestInfo* APS5_VABI sceVorbisDecInfo(void* vf, int link); +GuestComment* APS5_VABI sceVorbisDecComment(void* vf, int link); +std::int64_t APS5_VABI sceVorbisDecRead(void* vf, char* buffer, int length, int bigEndian, int word, int isSigned, int* bitstream); +std::int64_t APS5_VABI sceVorbisDecReadFloat(void* vf, float*** pcmChannels, int samples, int* bitstream); +std::int64_t APS5_VABI sceVorbisDecReadFilter(void* vf, char* buffer, int length, int bigEndian, int word, int isSigned, int* bitstream, GuestFilter filter, void* filterParam); +int APS5_VABI sceVorbisDecCrossLap(void* first, void* second); +int APS5_VABI sceVorbisDecHalfrate(void* vf, int flag); +int APS5_VABI sceVorbisDecHalfrateP(void* vf); +int APS5_VABI sceVorbisDecGetDecodePriority(void); +int APS5_VABI sceVorbisDecSetDecodePriority(void); +int APS5_VABI sceVorbisDecGetLastDecodeError(void); +} + +namespace { + +constexpr int FirstSerial = 0x1234abcd; +constexpr int SecondSerial = -0x61c88647; +constexpr int FirstChannels = 3; +constexpr int SecondChannels = 2; +constexpr long FirstRate = 44100; +constexpr long SecondRate = 22050; +constexpr long FirstSamples = 44100; +constexpr long SecondSamples = 11025; +constexpr std::int64_t DecodedBytes = (FirstSamples * FirstChannels + SecondSamples * SecondChannels) * 2; +constexpr unsigned char Pattern = 0xA5; + +void Check(bool value, int line) { + if (value) return; + std::fprintf(stderr, "VorbisDec check failed at line %d\n", line); + std::abort(); +} +#define Require(value) Check((value), __LINE__) + +template +bool Throws(TCall call) { + try { + call(); + } catch (const TException&) { + return true; + } + return false; +} + +struct Source { + const std::vector* bytes = nullptr; + std::size_t position = 0; + int closes = 0; + void* probe = nullptr; + std::int64_t probed = -1; + bool throwOnRead = false; + bool throwOnClose = false; +}; + +std::size_t ReadBytes(Source& source, void* buffer, std::size_t size, std::size_t count) { + if (source.throwOnRead) throw std::runtime_error("read callback"); + if (source.probe) source.probed = sceVorbisDecStreams(source.probe); + if (size == 0) return 0; + const std::size_t items = std::min(count, (source.bytes->size() - source.position) / size); + if (items != 0) std::memcpy(buffer, source.bytes->data() + source.position, items * size); + source.position += items * size; + return items; +} + +int SeekBytes(Source& source, std::int64_t offset, int whence) { + const auto size = static_cast(source.bytes->size()); + const std::int64_t base = whence == SEEK_SET ? 0 : whence == SEEK_CUR ? static_cast(source.position) : size; + if (offset < -base || offset > size - base) return -1; + source.position = static_cast(base + offset); + return 0; +} + +std::size_t APS5_VABI GuestRead(void* buffer, std::size_t size, std::size_t count, void* source) { + return ReadBytes(*static_cast(source), buffer, size, count); +} + +int APS5_VABI GuestSeek(void* source, std::int64_t offset, int whence) { + return SeekBytes(*static_cast(source), offset, whence); +} + +int APS5_VABI GuestClose(void* source) { + auto& closing = *static_cast(source); + ++closing.closes; + if (closing.throwOnClose) throw std::runtime_error("close callback"); + return 0; +} + +std::int64_t APS5_VABI GuestTell(void* source) { + return static_cast(static_cast(source)->position); +} + +std::size_t HostRead(void* buffer, std::size_t size, std::size_t count, void* source) { + return ReadBytes(*static_cast(source), buffer, size, count); +} + +int HostSeek(void* source, ogg_int64_t offset, int whence) { + return SeekBytes(*static_cast(source), offset, whence); +} + +int HostClose(void* source) { + ++static_cast(source)->closes; + return 0; +} + +long HostTell(void* source) { + return static_cast(static_cast(source)->position); +} + +constexpr GuestCallbacks SeekableCallbacks{GuestRead, GuestSeek, GuestClose, GuestTell}; +constexpr GuestCallbacks StreamingCallbacks{GuestRead, nullptr, GuestClose, nullptr}; +const ov_callbacks HostSeekableCallbacks{HostRead, HostSeek, HostClose, HostTell}; +const ov_callbacks HostStreamingCallbacks{HostRead, nullptr, HostClose, nullptr}; + +struct GuestFile { + alignas(16) std::array bytes; + + GuestFile() { bytes.fill(Pattern); } + + void* Get() { return bytes.data(); } + + bool Untouched() const { + return std::all_of(bytes.begin(), bytes.end(), [](unsigned char value) { return value == Pattern; }); + } +}; + +struct FilterLog { + int calls = 0; + std::int64_t firstChannels = 0; + std::int64_t lastChannels = 0; + std::int64_t samples = 0; +}; + +void Halve(float** pcm, std::int64_t channels, std::int64_t samples) { + for (std::int64_t channel = 0; channel < channels; ++channel) + for (std::int64_t index = 0; index < samples; ++index) pcm[channel][index] *= 0.5f; +} + +void APS5_VABI GuestHalve(float** pcm, std::int64_t channels, std::int64_t samples, void* param) { + auto* log = static_cast(param); + if (log->calls++ == 0) log->firstChannels = channels; + log->lastChannels = channels; + log->samples += samples; + Halve(pcm, channels, samples); +} + +void HostHalve(float** pcm, long channels, long samples, void*) { + Halve(pcm, channels, samples); +} + +void AppendPage(std::vector& out, const ogg_page& page) { + out.insert(out.end(), page.header, page.header + page.header_len); + out.insert(out.end(), page.body, page.body + page.body_len); +} + +void EncodeLink(std::vector& out, int serial, int channels, long rate, long samples, const char* title) { + vorbis_info info; + vorbis_info_init(&info); + Require(vorbis_encode_init_vbr(&info, channels, rate, 0.1f) == 0); + vorbis_comment comment; + vorbis_comment_init(&comment); + vorbis_comment_add_tag(&comment, "TITLE", title); + vorbis_comment_add_tag(&comment, "ARTIST", "AnyPS5"); + vorbis_dsp_state dsp; + Require(vorbis_analysis_init(&dsp, &info) == 0); + vorbis_block block; + vorbis_block_init(&dsp, &block); + ogg_stream_state stream; + ogg_stream_init(&stream, serial); + ogg_packet identification; + ogg_packet comments; + ogg_packet codebooks; + vorbis_analysis_headerout(&dsp, &comment, &identification, &comments, &codebooks); + ogg_stream_packetin(&stream, &identification); + ogg_stream_packetin(&stream, &comments); + ogg_stream_packetin(&stream, &codebooks); + ogg_page page; + while (ogg_stream_flush(&stream, &page) != 0) AppendPage(out, page); + for (long done = 0;;) { + const long count = std::min(1024L, samples - done); + if (count > 0) { + float** buffer = vorbis_analysis_buffer(&dsp, static_cast(count)); + for (int channel = 0; channel < channels; ++channel) { + const double frequency = 220.0 * (channel + 1) + 37.0 * serial / 1e9; + for (long index = 0; index < count; ++index) { + buffer[channel][index] = static_cast(0.3 * std::sin(6.283185307179586 * frequency * static_cast(done + index) / static_cast(rate))); + } + } + } + vorbis_analysis_wrote(&dsp, static_cast(count)); + done += count; + while (vorbis_analysis_blockout(&dsp, &block) == 1) { + vorbis_analysis(&block, nullptr); + vorbis_bitrate_addblock(&block); + ogg_packet packet; + while (vorbis_bitrate_flushpacket(&dsp, &packet) == 1) { + ogg_stream_packetin(&stream, &packet); + while (ogg_stream_pageout_fill(&stream, &page, 1024) != 0) AppendPage(out, page); + } + } + if (count == 0) break; + } + while (ogg_stream_flush(&stream, &page) != 0) AppendPage(out, page); + ogg_stream_clear(&stream); + vorbis_block_clear(&block); + vorbis_dsp_clear(&dsp); + vorbis_comment_clear(&comment); + vorbis_info_clear(&info); +} + +std::int64_t RequireSameRead(void* vf, OggVorbis_File& reference, int bigEndian = 0, int word = 2, int isSigned = 1) { + std::array guest{}; + std::array host{}; + int guestStream = -1; + int hostStream = -1; + const std::int64_t got = sceVorbisDecRead(vf, guest.data(), static_cast(guest.size()), bigEndian, word, isSigned, &guestStream); + const long expected = ov_read(&reference, host.data(), static_cast(host.size()), bigEndian, word, isSigned, &hostStream); + Require(got == expected); + Require(got <= 0 || (guestStream == hostStream && std::memcmp(guest.data(), host.data(), static_cast(got)) == 0)); + return got; +} + +std::int64_t DecodeTogether(void* vf, OggVorbis_File& reference) { + std::int64_t total = 0; + for (;;) { + const std::int64_t got = RequireSameRead(vf, reference); + Require(got >= 0 || got == OV_HOLE); + if (got == 0) return total; + if (got > 0) total += got; + } +} + +void RequireSameTells(void* vf, OggVorbis_File& reference) { + Require(sceVorbisDecPcmTell(vf) == ov_pcm_tell(&reference)); + Require(sceVorbisDecRawTell(vf) == ov_raw_tell(&reference)); + Require(sceVorbisDecTimeTell(vf) == ov_time_tell(&reference)); +} + +void TestSeekable(const std::vector& stream) { + GuestFile guest; + GuestFile probe; + void* vf = guest.Get(); + Source source{&stream}; + source.probe = probe.Get(); + Require(sceVorbisDecOpenCallbacks(&source, vf, nullptr, 0, SeekableCallbacks) == 0); + Require(source.probed == 0 && source.closes == 0); + Source referenceSource{&stream}; + OggVorbis_File reference{}; + Require(ov_open_callbacks(&referenceSource, &reference, nullptr, 0, HostSeekableCallbacks) == 0); + + Require(sceVorbisDecStreams(vf) == 2 && sceVorbisDecSeekable(vf) == 1); + Require(sceVorbisDecSerialNumber(vf, 0) == FirstSerial && sceVorbisDecSerialNumber(vf, 1) == SecondSerial); + Require(sceVorbisDecSerialNumber(vf, -1) == FirstSerial && sceVorbisDecSerialNumber(vf, 7) == SecondSerial); + Require(sceVorbisDecPcmTotal(vf, 0) == FirstSamples && sceVorbisDecPcmTotal(vf, 1) == SecondSamples); + Require(sceVorbisDecPcmTotal(vf, -1) == FirstSamples + SecondSamples && sceVorbisDecPcmTotal(vf, 2) == OV_EINVAL); + Require(sceVorbisDecRawTotal(vf, -1) == ov_raw_total(&reference, -1) && sceVorbisDecRawTotal(vf, -1) > 0); + Require(sceVorbisDecRawTotal(vf, 0) == ov_raw_total(&reference, 0) && sceVorbisDecRawTotal(vf, 1) == ov_raw_total(&reference, 1)); + Require(sceVorbisDecTimeTotal(vf, 0) == 1.0 && sceVorbisDecTimeTotal(vf, 1) == 0.5 && sceVorbisDecTimeTotal(vf, -1) == 1.5); + Require(sceVorbisDecBitrate(vf, -1) == ov_bitrate(&reference, -1) && sceVorbisDecBitrate(vf, 1) == ov_bitrate(&reference, 1)); + Require(sceVorbisDecBitrate(vf, -1) > 0 && sceVorbisDecBitrate(vf, 2) == OV_EINVAL); + Require(sceVorbisDecBitrateInstant(vf) == OV_FALSE); + + GuestInfo* first = sceVorbisDecInfo(vf, 0); + GuestInfo* second = sceVorbisDecInfo(vf, 1); + Require(first && second && first != second && sceVorbisDecInfo(vf, 0) == first); + Require(sceVorbisDecInfo(vf, -1) == first && sceVorbisDecInfo(vf, 2) == nullptr); + Require(first->version == 0 && first->channels == FirstChannels && first->rate == FirstRate); + Require(second->version == 0 && second->channels == SecondChannels && second->rate == SecondRate); + for (int link = 0; link < 2; ++link) { + const GuestInfo* info = sceVorbisDecInfo(vf, link); + const vorbis_info* host = ov_info(&reference, link); + Require(info->bitrate_upper == host->bitrate_upper && info->bitrate_nominal == host->bitrate_nominal); + Require(info->bitrate_lower == host->bitrate_lower && info->bitrate_window == host->bitrate_window && info->codec_setup != nullptr); + } + const GuestComment* comment = sceVorbisDecComment(vf, 1); + Require(comment && comment->comments == 2 && comment->comment_lengths[0] == 12 && comment->comment_lengths[1] == 13); + Require(std::strcmp(comment->user_comments[0], "TITLE=second") == 0 && std::strcmp(comment->user_comments[1], "ARTIST=AnyPS5") == 0); + Require(std::strcmp(comment->vendor, ov_comment(&reference, 1)->vendor) == 0); + Require(sceVorbisDecComment(vf, -1) == sceVorbisDecComment(vf, 0) && sceVorbisDecComment(vf, 2) == nullptr); + Require(std::strcmp(sceVorbisDecComment(vf, 0)->user_comments[0], "TITLE=first") == 0); + + const std::int64_t firstRead = RequireSameRead(vf, reference); + Require(firstRead > 0 && sceVorbisDecBitrateInstant(vf) == ov_bitrate_instant(&reference)); + Require(sceVorbisDecInfo(vf, -1) == first); + Require(firstRead + DecodeTogether(vf, reference) == DecodedBytes); + Require(sceVorbisDecPcmTell(vf) == FirstSamples + SecondSamples && sceVorbisDecTimeTell(vf) == 1.5); + RequireSameTells(vf, reference); + Require(sceVorbisDecInfo(vf, -1) == second && sceVorbisDecSerialNumber(vf, -1) == SecondSerial); + + Require(sceVorbisDecPcmSeek(vf, 0) == 0 && ov_pcm_seek(&reference, 0) == 0); + Require(DecodeTogether(vf, reference) == DecodedBytes); + + Require(sceVorbisDecPcmSeek(vf, 1000) == 0 && ov_pcm_seek(&reference, 1000) == 0); + Require(sceVorbisDecPcmTell(vf) == 1000); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && RequireSameRead(vf, reference, 1, 2, 0) > 0 && RequireSameRead(vf, reference, 0, 1, 0) > 0); + Require(sceVorbisDecPcmSeekPage(vf, FirstSamples + 3000) == ov_pcm_seek_page(&reference, FirstSamples + 3000)); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && sceVorbisDecInfo(vf, -1) == second); + Require(sceVorbisDecTimeSeek(vf, 0.25) == 0 && ov_time_seek(&reference, 0.25) == 0); + Require(sceVorbisDecPcmTell(vf) == FirstRate / 4); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && sceVorbisDecInfo(vf, -1) == first); + Require(sceVorbisDecTimeSeekPage(vf, 1.2) == ov_time_seek_page(&reference, 1.2)); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0); + const auto middle = static_cast(stream.size() / 2); + Require(sceVorbisDecRawSeek(vf, middle) == 0 && ov_raw_seek(&reference, middle) == 0); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0); + + Require(sceVorbisDecPcmSeekLap(vf, 5000) == 0 && ov_pcm_seek_lap(&reference, 5000) == 0); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && RequireSameRead(vf, reference) > 0); + Require(sceVorbisDecPcmSeekPageLap(vf, 30000) == ov_pcm_seek_page_lap(&reference, 30000)); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0); + Require(sceVorbisDecRawSeekLap(vf, middle / 2) == ov_raw_seek_lap(&reference, middle / 2)); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0); + Require(sceVorbisDecTimeSeekLap(vf, 1.1) == 0 && ov_time_seek_lap(&reference, 1.1) == 0); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && sceVorbisDecInfo(vf, -1) == second); + Require(sceVorbisDecTimeSeekPageLap(vf, 0.75) == ov_time_seek_page_lap(&reference, 0.75)); + RequireSameTells(vf, reference); + Require(RequireSameRead(vf, reference) > 0 && sceVorbisDecInfo(vf, -1) == first); + + Require(sceVorbisDecPcmSeek(vf, -1) == OV_EINVAL && sceVorbisDecPcmSeek(vf, FirstSamples + SecondSamples + 1) == OV_EINVAL); + Require(sceVorbisDecTimeSeek(vf, -0.5) == OV_EINVAL && sceVorbisDecTimeSeek(vf, 2.0) == OV_EINVAL); + Require(sceVorbisDecRawSeek(vf, -1) == OV_EINVAL && sceVorbisDecRawSeek(vf, middle * 3) == OV_EINVAL); + + Require(source.closes == 0 && sceVorbisDecClear(vf) == 0 && source.closes == 1); + Require(sceVorbisDecStreams(vf) == 0 && sceVorbisDecInfo(vf, 0) == nullptr && sceVorbisDecPcmTell(vf) == OV_EINVAL); + Require(sceVorbisDecClear(vf) == 0 && source.closes == 1); + Require(ov_clear(&reference) == 0 && referenceSource.closes == 1); + Require(source.probed == 0 && guest.Untouched() && probe.Untouched()); +} + +void TestStreaming(const std::vector& stream) { + GuestFile guest; + void* vf = guest.Get(); + const auto* initial = reinterpret_cast(stream.data()); + Source source{&stream, 64}; + Require(sceVorbisDecOpenCallbacks(&source, vf, initial, 64, StreamingCallbacks) == 0); + Source referenceSource{&stream, 64}; + OggVorbis_File reference{}; + Require(ov_open_callbacks(&referenceSource, &reference, initial, 64, HostStreamingCallbacks) == 0); + + Require(sceVorbisDecSeekable(vf) == 0 && sceVorbisDecStreams(vf) == 1); + Require(sceVorbisDecPcmTotal(vf, -1) == OV_EINVAL && sceVorbisDecRawTotal(vf, 0) == OV_EINVAL && sceVorbisDecTimeTotal(vf, -1) == OV_EINVAL); + Require(sceVorbisDecRawSeek(vf, 0) == OV_ENOSEEK && sceVorbisDecPcmSeek(vf, 0) == OV_ENOSEEK && sceVorbisDecTimeSeekPage(vf, 0.0) == OV_ENOSEEK); + Require(sceVorbisDecBitrate(vf, 0) == ov_bitrate(&reference, 0) && sceVorbisDecBitrate(vf, 1) == ov_bitrate(&reference, 1)); + Require(sceVorbisDecSerialNumber(vf, 3) == FirstSerial); + GuestInfo* info = sceVorbisDecInfo(vf, 0); + Require(info && info == sceVorbisDecInfo(vf, 5) && info == sceVorbisDecInfo(vf, -1)); + Require(info->channels == FirstChannels && info->rate == FirstRate); + const GuestComment* comment = sceVorbisDecComment(vf, 4); + Require(comment && std::strcmp(comment->user_comments[0], "TITLE=first") == 0); + + Require(DecodeTogether(vf, reference) == DecodedBytes); + Require(info->channels == SecondChannels && info->rate == SecondRate && sceVorbisDecInfo(vf, 0) == info); + Require(std::strcmp(comment->user_comments[0], "TITLE=second") == 0); + Require(sceVorbisDecSerialNumber(vf, 0) == SecondSerial); + RequireSameTells(vf, reference); + Require(sceVorbisDecClear(vf) == 0 && source.closes == 1); + Require(ov_clear(&reference) == 0 && referenceSource.closes == 1); + Require(guest.Untouched()); +} + +void TestFloatAndFilter(const std::vector& stream) { + GuestFile guest; + void* vf = guest.Get(); + Source source{&stream}; + Require(sceVorbisDecOpenCallbacks(&source, vf, nullptr, 0, SeekableCallbacks) == 0); + Source referenceSource{&stream}; + OggVorbis_File reference{}; + Require(ov_open_callbacks(&referenceSource, &reference, nullptr, 0, HostSeekableCallbacks) == 0); + + for (int round = 0; round < 6; ++round) { + float** pcm = nullptr; + float** hostPcm = nullptr; + int guestStream = -1; + int hostStream = -1; + const std::int64_t got = sceVorbisDecReadFloat(vf, &pcm, 300, &guestStream); + const long expected = ov_read_float(&reference, &hostPcm, 300, &hostStream); + Require(got == expected && got > 0 && got <= 300 && guestStream == hostStream && guestStream == 0); + for (int channel = 0; channel < FirstChannels; ++channel) { + Require(std::memcmp(pcm[channel], hostPcm[channel], static_cast(got) * sizeof(float)) == 0); + } + } + Require(sceVorbisDecReadFloat(vf, nullptr, 50, nullptr) == ov_read_float(&reference, nullptr, 50, nullptr)); + RequireSameTells(vf, reference); + + FilterLog log; + std::int64_t total = 0; + for (;;) { + std::array guestBytes{}; + std::array hostBytes{}; + int guestStream = -1; + int hostStream = -1; + const std::int64_t got = sceVorbisDecReadFilter(vf, guestBytes.data(), static_cast(guestBytes.size()), 0, 2, 1, &guestStream, GuestHalve, &log); + const long expected = ov_read_filter(&reference, hostBytes.data(), static_cast(hostBytes.size()), 0, 2, 1, &hostStream, HostHalve, nullptr); + Require(got == expected && got >= 0); + if (got == 0) break; + Require(guestStream == hostStream && std::memcmp(guestBytes.data(), hostBytes.data(), static_cast(got)) == 0); + total += got; + } + Require(total > 0 && log.calls > 0 && log.samples > 0 && log.firstChannels == FirstChannels && log.lastChannels == SecondChannels); + + Require(sceVorbisDecPcmSeek(vf, 0) == 0 && ov_pcm_seek(&reference, 0) == 0); + std::array filtered{}; + std::array plain{}; + int bitstream = -1; + const int calls = log.calls; + const std::int64_t got = sceVorbisDecReadFilter(vf, filtered.data(), static_cast(filtered.size()), 0, 2, 1, &bitstream, nullptr, &log); + Require(got > 0 && got == ov_read(&reference, plain.data(), static_cast(plain.size()), 0, 2, 1, &bitstream)); + Require(std::memcmp(filtered.data(), plain.data(), static_cast(got)) == 0 && log.calls == calls); + Require(sceVorbisDecClear(vf) == 0 && source.closes == 1 && ov_clear(&reference) == 0); + Require(guest.Untouched()); +} + +void TestHalfrate(const std::vector& stream) { + GuestFile guest; + void* vf = guest.Get(); + Source source{&stream}; + Require(sceVorbisDecOpenCallbacks(&source, vf, nullptr, 0, SeekableCallbacks) == 0); + Source referenceSource{&stream}; + OggVorbis_File reference{}; + Require(ov_open_callbacks(&referenceSource, &reference, nullptr, 0, HostSeekableCallbacks) == 0); + + Require(sceVorbisDecHalfrateP(vf) == 0); + Require(RequireSameRead(vf, reference) > 0 && RequireSameRead(vf, reference) > 0); + Require(sceVorbisDecHalfrate(vf, 1) == 0 && ov_halfrate(&reference, 1) == 0); + Require(sceVorbisDecHalfrateP(vf) == 1 && ov_halfrate_p(&reference) == 1); + RequireSameTells(vf, reference); + const std::int64_t half = DecodeTogether(vf, reference); + Require(half > 0 && half < DecodedBytes / 2); + RequireSameTells(vf, reference); + Require(sceVorbisDecPcmTell(vf) > FirstSamples); + Require(sceVorbisDecHalfrate(vf, 0) == 0 && sceVorbisDecHalfrateP(vf) == 0); + Require(sceVorbisDecClear(vf) == 0 && source.closes == 1 && ov_clear(&reference) == 0); + Require(guest.Untouched()); +} + +void TestCrossLap(const std::vector& stream) { + GuestFile first; + GuestFile second; + GuestFile closed; + GuestFile otherClosed; + Source firstSource{&stream}; + Source secondSource{&stream}; + Require(sceVorbisDecOpenCallbacks(&secondSource, second.Get(), nullptr, 0, SeekableCallbacks) == 0); + firstSource.probe = second.Get(); + Require(sceVorbisDecOpenCallbacks(&firstSource, first.Get(), nullptr, 0, SeekableCallbacks) == 0); + Require(firstSource.probed == 2); + Source firstReferenceSource{&stream}; + Source secondReferenceSource{&stream}; + OggVorbis_File firstReference{}; + OggVorbis_File secondReference{}; + Require(ov_open_callbacks(&firstReferenceSource, &firstReference, nullptr, 0, HostSeekableCallbacks) == 0); + Require(ov_open_callbacks(&secondReferenceSource, &secondReference, nullptr, 0, HostSeekableCallbacks) == 0); + + Require(RequireSameRead(first.Get(), firstReference) > 0 && RequireSameRead(first.Get(), firstReference) > 0); + Require(sceVorbisDecPcmSeek(second.Get(), FirstSamples + 2000) == 0 && ov_pcm_seek(&secondReference, FirstSamples + 2000) == 0); + Require(sceVorbisDecCrossLap(first.Get(), second.Get()) == 0 && ov_crosslap(&firstReference, &secondReference) == 0); + for (int round = 0; round < 3; ++round) { + Require(RequireSameRead(second.Get(), secondReference) > 0); + Require(RequireSameRead(first.Get(), firstReference) > 0); + } + RequireSameTells(first.Get(), firstReference); + RequireSameTells(second.Get(), secondReference); + Require(sceVorbisDecCrossLap(first.Get(), first.Get()) == 0); + Require(sceVorbisDecCrossLap(first.Get(), closed.Get()) == OV_EINVAL && sceVorbisDecCrossLap(closed.Get(), second.Get()) == OV_EINVAL); + Require(sceVorbisDecCrossLap(closed.Get(), otherClosed.Get()) == OV_EINVAL && sceVorbisDecCrossLap(closed.Get(), closed.Get()) == 0); + Require(sceVorbisDecCrossLap(nullptr, nullptr) == 0); + OggVorbis_File zeroed{}; + Require(sceVorbisDecCrossLap(closed.Get(), nullptr) == OV_EINVAL && ov_crosslap(&zeroed, nullptr) == OV_EINVAL); + Require(Throws([&] { sceVorbisDecCrossLap(first.Get(), nullptr); })); + Require(Throws([&] { sceVorbisDecCrossLap(nullptr, closed.Get()); })); + Require(sceVorbisDecClear(first.Get()) == 0 && sceVorbisDecClear(second.Get()) == 0); + Require(firstSource.closes == 1 && secondSource.closes == 1); + Require(ov_clear(&firstReference) == 0 && ov_clear(&secondReference) == 0); + Require(first.Untouched() && second.Untouched() && closed.Untouched() && otherClosed.Untouched()); +} + +void TestNeverOpened() { + GuestFile guest; + void* vf = guest.Get(); + Require(sceVorbisDecStreams(vf) == 0 && sceVorbisDecSeekable(vf) == 0); + Require(sceVorbisDecSerialNumber(vf, -1) == 0 && sceVorbisDecSerialNumber(vf, 0) == 0 && sceVorbisDecSerialNumber(vf, 3) == 0); + Require(sceVorbisDecBitrate(vf, -1) == OV_EINVAL && sceVorbisDecBitrate(vf, 0) == OV_EINVAL && sceVorbisDecBitrateInstant(vf) == OV_EINVAL); + Require(sceVorbisDecRawTotal(vf, -1) == OV_EINVAL && sceVorbisDecPcmTotal(vf, 0) == OV_EINVAL && sceVorbisDecTimeTotal(vf, -1) == OV_EINVAL); + Require(sceVorbisDecRawSeek(vf, 0) == OV_EINVAL && sceVorbisDecPcmSeek(vf, 0) == OV_EINVAL && sceVorbisDecPcmSeekPage(vf, 0) == OV_EINVAL); + Require(sceVorbisDecTimeSeek(vf, 0.0) == OV_EINVAL && sceVorbisDecTimeSeekPage(vf, 0.0) == OV_EINVAL); + Require(sceVorbisDecRawSeekLap(vf, 0) == OV_EINVAL && sceVorbisDecPcmSeekLap(vf, 0) == OV_EINVAL && sceVorbisDecPcmSeekPageLap(vf, 0) == OV_EINVAL); + Require(sceVorbisDecTimeSeekLap(vf, 0.0) == OV_EINVAL && sceVorbisDecTimeSeekPageLap(vf, 0.0) == OV_EINVAL); + Require(sceVorbisDecRawTell(vf) == OV_EINVAL && sceVorbisDecPcmTell(vf) == OV_EINVAL && sceVorbisDecTimeTell(vf) == OV_EINVAL); + Require(sceVorbisDecInfo(vf, -1) == nullptr && sceVorbisDecInfo(vf, 0) == nullptr); + Require(sceVorbisDecComment(vf, -1) == nullptr && sceVorbisDecComment(vf, 0) == nullptr); + std::array buffer{}; + int bitstream = 7; + Require(sceVorbisDecRead(vf, buffer.data(), static_cast(buffer.size()), 0, 2, 1, &bitstream) == OV_EINVAL && bitstream == 7); + float** pcm = nullptr; + Require(sceVorbisDecReadFloat(vf, &pcm, 64, &bitstream) == OV_EINVAL && pcm == nullptr && bitstream == 7); + FilterLog log; + Require(sceVorbisDecReadFilter(vf, buffer.data(), static_cast(buffer.size()), 0, 2, 1, &bitstream, GuestHalve, &log) == OV_EINVAL); + Require(log.calls == 0 && bitstream == 7); + Require(sceVorbisDecHalfrate(vf, 1) == OV_EINVAL && sceVorbisDecHalfrateP(vf) == OV_EINVAL); + Require(sceVorbisDecClear(vf) == 0 && sceVorbisDecClear(nullptr) == 0); + Require(guest.Untouched()); + + Source source; + Require(Throws([&] { sceVorbisDecOpenCallbacks(&source, nullptr, nullptr, 0, SeekableCallbacks); })); + Require(Throws([] { sceVorbisDecStreams(nullptr); })); + Require(Throws([] { sceVorbisDecInfo(nullptr, 0); })); + Require(Throws([] { sceVorbisDecPcmSeek(nullptr, 0); })); +} + +void TestFailedOpens(const std::vector& stream) { + GuestFile guest; + void* vf = guest.Get(); + std::vector noise(8192); + std::uint32_t state = 0x2545f491; + for (auto& value : noise) { + state = state * 1664525u + 1013904223u; + value = static_cast(state >> 24); + } + Source noiseSource{&noise}; + Require(sceVorbisDecOpenCallbacks(&noiseSource, vf, nullptr, 0, SeekableCallbacks) == OV_ENOTVORBIS); + Require(noiseSource.closes == 0 && sceVorbisDecStreams(vf) == 0 && sceVorbisDecInfo(vf, 0) == nullptr); + Require(sceVorbisDecClear(vf) == 0 && noiseSource.closes == 0); + + Source untold{&stream}; + constexpr GuestCallbacks NoTell{GuestRead, GuestSeek, GuestClose, nullptr}; + Require(sceVorbisDecOpenCallbacks(&untold, vf, nullptr, 0, NoTell) == OV_EINVAL); + Require(untold.closes == 0 && sceVorbisDecStreams(vf) == 0 && sceVorbisDecClear(vf) == 0 && untold.closes == 0); + + constexpr GuestCallbacks NoRead{nullptr, GuestSeek, GuestClose, GuestTell}; + Source unread{&stream}; + OggVorbis_File reference{}; + const ov_callbacks hostNoRead{nullptr, HostSeek, HostClose, HostTell}; + Require(sceVorbisDecOpenCallbacks(&unread, vf, nullptr, 0, NoRead) == ov_open_callbacks(&unread, &reference, nullptr, 0, hostNoRead)); + Require(unread.closes == 0 && sceVorbisDecStreams(vf) == 0); + + Require(sceVorbisDecOpenCallbacks(nullptr, vf, nullptr, 0, SeekableCallbacks) == OV_ENOTVORBIS); + + Source previous{&stream}; + Source next{&stream}; + Require(sceVorbisDecOpenCallbacks(&previous, vf, nullptr, 0, SeekableCallbacks) == 0); + Require(sceVorbisDecOpenCallbacks(&next, vf, nullptr, 0, StreamingCallbacks) == 0); + Require(previous.closes == 0 && sceVorbisDecSeekable(vf) == 0 && sceVorbisDecStreams(vf) == 1); + Require(sceVorbisDecClear(vf) == 0 && previous.closes == 0 && next.closes == 1); + + Source unsized{&stream}; + Require(sceVorbisDecOpenCallbacks(&unsized, vf, nullptr, std::int64_t{1} << 32, SeekableCallbacks) == 0); + Require(sceVorbisDecStreams(vf) == 2 && sceVorbisDecClear(vf) == 0 && unsized.closes == 1); + + const auto* initial = reinterpret_cast(stream.data()); + const auto size = static_cast(stream.size()); + Require(sceVorbisDecOpenCallbacks(nullptr, vf, initial, size, SeekableCallbacks) == 0); + Require(ov_open_callbacks(nullptr, &reference, initial, static_cast(size), HostSeekableCallbacks) == 0); + Require(sceVorbisDecSeekable(vf) == 0 && DecodeTogether(vf, reference) == DecodedBytes); + Require(sceVorbisDecClear(vf) == 0 && ov_clear(&reference) == 0); + Require(guest.Untouched()); +} + +void TestThrowingCallbacks(const std::vector& stream) { + GuestFile guest; + Source source{&stream}; + Require(sceVorbisDecOpenCallbacks(&source, guest.Get(), nullptr, 0, SeekableCallbacks) == 0); + std::array buffer{}; + int bitstream = -1; + source.throwOnRead = true; + bool threw = false; + for (int call = 0; call < 1024 && !threw; ++call) { + threw = Throws([&] { sceVorbisDecRead(guest.Get(), buffer.data(), static_cast(buffer.size()), 0, 2, 1, &bitstream); }); + } + Require(threw); + source.throwOnRead = false; + source.throwOnClose = true; + Require(Throws([&] { sceVorbisDecClear(guest.Get()); })); + Require(source.closes == 1 && sceVorbisDecStreams(guest.Get()) == 0); + Require(sceVorbisDecClear(guest.Get()) == 0 && source.closes == 1); + Require(guest.Untouched()); +} + +} + +int main() { + std::vector stream; + EncodeLink(stream, FirstSerial, FirstChannels, FirstRate, FirstSamples, "first"); + EncodeLink(stream, SecondSerial, SecondChannels, SecondRate, SecondSamples, "second"); + + TestSeekable(stream); + TestStreaming(stream); + TestFloatAndFilter(stream); + TestHalfrate(stream); + TestCrossLap(stream); + TestNeverOpened(); + TestFailedOpens(stream); + TestThrowingCallbacks(stream); + + Require(Throws([] { sceVorbisDecGetDecodePriority(); })); + Require(Throws([] { sceVorbisDecSetDecodePriority(); })); + Require(Throws([] { sceVorbisDecGetLastDecodeError(); })); + + std::puts("All VorbisDec guest tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestVrServiceDialog.cpp b/core/libs/tests/GuestVrServiceDialog.cpp new file mode 100644 index 0000000000..8d72100186 --- /dev/null +++ b/core/libs/tests/GuestVrServiceDialog.cpp @@ -0,0 +1,72 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceVrServiceDialogInitialize(void); +int APS5_VABI sceVrServiceDialogOpen(const void* param); +int APS5_VABI sceVrServiceDialogUpdateStatus(void); +int APS5_VABI sceVrServiceDialogGetStatus(void); +int APS5_VABI sceVrServiceDialogGetResult(void* result); +int APS5_VABI sceVrServiceDialogClose(void); +int APS5_VABI sceVrServiceDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + std::int64_t param = 0; + std::int32_t result[2] = {-1, -1}; + + Require(sceVrServiceDialogTerminate() == kErrNotInitialized); + Require(sceVrServiceDialogOpen(¶m) == kErrNotInitialized); + Require(sceVrServiceDialogClose() == kErrNotInitialized); + Require(sceVrServiceDialogGetResult(result) == kErrNotInitialized); + Require(sceVrServiceDialogGetStatus() == kStatusNone); + Require(sceVrServiceDialogUpdateStatus() == kStatusNone); + + Require(sceVrServiceDialogInitialize() == 0); + Require(sceVrServiceDialogInitialize() == kErrAlreadyInitialized); + Require(sceVrServiceDialogGetStatus() == kStatusInitialized); + Require(sceVrServiceDialogUpdateStatus() == kStatusInitialized); + + Require(sceVrServiceDialogOpen(nullptr) == kErrArgNull); + Require(sceVrServiceDialogGetResult(result) == kErrNotFinished); + + Require(sceVrServiceDialogOpen(¶m) == 0); + Require(sceVrServiceDialogGetStatus() == kStatusFinished); + Require(sceVrServiceDialogUpdateStatus() == kStatusFinished); + + Require(sceVrServiceDialogGetResult(nullptr) == kErrArgNull); + Require(sceVrServiceDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(sceVrServiceDialogClose() == 0); + Require(sceVrServiceDialogTerminate() == 0); + Require(sceVrServiceDialogTerminate() == kErrNotInitialized); + Require(sceVrServiceDialogGetStatus() == kStatusNone); + Require(sceVrServiceDialogUpdateStatus() == kStatusNone); + + Require(sceVrServiceDialogInitialize() == 0); + Require(sceVrServiceDialogUpdateStatus() == kStatusInitialized); + Require(sceVrServiceDialogClose() == 0); + Require(sceVrServiceDialogUpdateStatus() == kStatusFinished); + result[0] = -1; + Require(sceVrServiceDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(sceVrServiceDialogTerminate() == 0); +} diff --git a/core/libs/tests/GuestVrSetupDialog.cpp b/core/libs/tests/GuestVrSetupDialog.cpp index a3afc479f3..decf0c81e2 100644 --- a/core/libs/tests/GuestVrSetupDialog.cpp +++ b/core/libs/tests/GuestVrSetupDialog.cpp @@ -1,12 +1,14 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include "prx/libSceCommonDialog/CommonDialog.hpp" extern "C" { int APS5_VABI sceVrSetupDialogInitialize(void); -int APS5_VABI sceVrSetupDialogOpen(const void* param); +int APS5_VABI sceVrSetupDialogOpen(const VrSetupDialogParam* param); int APS5_VABI sceVrSetupDialogUpdateStatus(void); -int APS5_VABI sceVrSetupDialogGetResult(void* result); +int APS5_VABI sceVrSetupDialogGetStatus(void); +int APS5_VABI sceVrSetupDialogGetResult(std::int32_t* result); int APS5_VABI sceVrSetupDialogClose(void); int APS5_VABI sceVrSetupDialogTerminate(void); } @@ -15,30 +17,52 @@ namespace { void Require(bool value) { if (!value) std::abort(); } +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; constexpr int kStatusFinished = 3; -constexpr int kErrNotInitialized = static_cast(0x80B80003); -constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); -constexpr int kErrNotFinished = static_cast(0x80B80005); -constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kErrNotInitialized = static_cast(0x8A7C0001); +constexpr int kErrAlreadyInitialized = static_cast(0x8A7C0002); +constexpr int kErrNotFinished = static_cast(0x8A7C0005); +constexpr int kErrArgNull = static_cast(0x8A7C0003); constexpr int kResultUserCanceled = 1; } int main() { - std::int64_t param = 0; + VrSetupDialogParam param{}; + param.size = sizeof(param); std::int32_t result[2] = {-1, -1}; Require(sceVrSetupDialogTerminate() == kErrNotInitialized); Require(sceVrSetupDialogOpen(¶m) == kErrNotInitialized); + Require(sceVrSetupDialogClose() == kErrNotInitialized); + Require(sceVrSetupDialogGetResult(nullptr) == kErrArgNull); Require(sceVrSetupDialogGetResult(result) == kErrNotInitialized); + Require(result[0] == -1 && result[1] == -1); + Require(sceVrSetupDialogGetStatus() == kStatusNone); + Require(sceVrSetupDialogUpdateStatus() == kStatusNone); Require(sceVrSetupDialogInitialize() == 0); Require(sceVrSetupDialogInitialize() == kErrAlreadyInitialized); + Require(sceVrSetupDialogGetStatus() == kStatusInitialized); + Require(sceVrSetupDialogUpdateStatus() == kStatusInitialized); - Require(sceVrSetupDialogOpen(nullptr) == kErrArgNull); + Require(sceVrSetupDialogOpen(nullptr) == static_cast(0x8A7C0003u)); Require(sceVrSetupDialogGetResult(result) == kErrNotFinished); + Require(result[0] == kResultUserCanceled && result[1] == -1); + param.size = 55; + Require(sceVrSetupDialogOpen(¶m) == static_cast(0x8A7C0003u)); + param.size = sizeof(param); + param.reserved8 = 1; + Require(sceVrSetupDialogOpen(¶m) == static_cast(0x8A7C0003u)); + param.reserved8 = 0; + param.reserved9 = 1; + Require(sceVrSetupDialogOpen(¶m) == static_cast(0x8A7C0003u)); + param.reserved9 = 0; + param.data[45] = 255; Require(sceVrSetupDialogOpen(¶m) == 0); + Require(sceVrSetupDialogGetStatus() == kStatusFinished); Require(sceVrSetupDialogUpdateStatus() == kStatusFinished); Require(sceVrSetupDialogGetResult(nullptr) == kErrArgNull); @@ -49,4 +73,14 @@ int main() { Require(sceVrSetupDialogClose() == 0); Require(sceVrSetupDialogTerminate() == 0); Require(sceVrSetupDialogTerminate() == kErrNotInitialized); + Require(sceVrSetupDialogGetStatus() == kStatusNone); + Require(sceVrSetupDialogUpdateStatus() == kStatusNone); + + Require(sceVrSetupDialogInitialize() == 0); + Require(sceVrSetupDialogUpdateStatus() == kStatusInitialized); + Require(sceVrSetupDialogClose() == 0); + Require(sceVrSetupDialogUpdateStatus() == kStatusInitialized); + Require(sceVrSetupDialogGetResult(result) == kErrNotFinished); + Require(result[0] == kResultUserCanceled && result[1] == -1); + Require(sceVrSetupDialogTerminate() == 0); } diff --git a/core/libs/tests/GuestVrTracker2.cpp b/core/libs/tests/GuestVrTracker2.cpp new file mode 100644 index 0000000000..0bbe3a869e --- /dev/null +++ b/core/libs/tests/GuestVrTracker2.cpp @@ -0,0 +1,143 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "ProcessParametersFixture.hpp" + +#include +#include +#include +#include +#include +#include + +extern "C" { +std::int32_t APS5_VABI VrTracker2NotInitialized(); +std::int32_t APS5_VABI sceVrTracker2Finalize(); +std::int32_t APS5_VABI VrTracker2ClearWideResult(void* result); +std::int32_t APS5_VABI VrTracker2ClearNarrowResult(void* result); +std::int32_t APS5_VABI VrTracker2CheckSecondThenReportNotInitialized(const void* first, const void* second); +std::int32_t APS5_VABI VrTracker2Accept(); +std::int32_t APS5_VABI VrTracker2FillDefaults(void* output); +std::int32_t APS5_VABI VrTracker2ClearWord(void* output); +std::int32_t APS5_VABI sceVrTracker2QueryMemory(const void* parameter, void* requirement); +struct VrTracker2InitializeParameter; +std::int32_t APS5_VABI sceVrTracker2Initialize(const VrTracker2InitializeParameter* parameter); +} + +namespace { + +alignas(16) std::array process{}; + +constexpr std::int32_t NotInitialized = static_cast(0x8A740001u); +constexpr std::int32_t InvalidArgument = static_cast(0x8A740006u); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "VrTracker2: %s\n", message); + std::abort(); + } +} + +std::array Filled() { + std::array buffer{}; + buffer.fill(0xAA); + return buffer; +} + +bool AllEqual(const std::uint8_t* begin, const std::uint8_t* end, std::uint8_t value) { + for (; begin != end; ++begin) + if (*begin != value) return false; + return true; +} + +template +bool Throws(TCall call) { + try { + call(); + } catch (const std::exception&) { + return true; + } + return false; +} + +} + +int main() { + Require(VrTracker2NotInitialized() == NotInitialized, "plain export"); + Require(sceVrTracker2Finalize() == NotInitialized, "named export"); + + auto wide = Filled(); + Require(VrTracker2ClearWideResult(wide.data()) == NotInitialized, "wide result return"); + Require(AllEqual(wide.data(), wide.data() + 24, 0) && AllEqual(wide.data() + 24, wide.data() + 32, 0xAA), "wide result clears 24 bytes"); + Require(VrTracker2ClearWideResult(nullptr) == InvalidArgument, "wide result null"); + + auto narrow = Filled(); + Require(VrTracker2ClearNarrowResult(narrow.data()) == NotInitialized, "narrow result return"); + Require(AllEqual(narrow.data(), narrow.data() + 16, 0) && AllEqual(narrow.data() + 16, narrow.data() + 32, 0xAA), "narrow result clears 16 bytes"); + Require(VrTracker2ClearNarrowResult(nullptr) == InvalidArgument, "narrow result null"); + + auto second = Filled(); + Require(VrTracker2CheckSecondThenReportNotInitialized(nullptr, second.data()) == NotInitialized, "second checked, first null"); + Require(VrTracker2CheckSecondThenReportNotInitialized(second.data(), nullptr) == InvalidArgument, "second null"); + Require(AllEqual(second.data(), second.data() + 32, 0xAA), "second untouched"); + + Require(VrTracker2Accept() == 0, "accepting export"); + + auto defaults = Filled(); + Require(VrTracker2FillDefaults(defaults.data()) == 0, "defaults return"); + const std::array expected{0xAA, 0xAA, 0xAA, 0xAA, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x40, 0x00}; + Require(std::memcmp(defaults.data(), expected.data(), expected.size()) == 0, "defaults layout"); + Require(AllEqual(defaults.data() + 16, defaults.data() + 32, 0xAA), "defaults stop at 16 bytes"); + Require(Throws([] { VrTracker2FillDefaults(nullptr); }), "defaults null"); + + auto word = Filled(); + Require(VrTracker2ClearWord(word.data()) == 0, "clear word return"); + Require(AllEqual(word.data(), word.data() + 4, 0) && AllEqual(word.data() + 4, word.data() + 32, 0xAA), "clear word clears 4 bytes"); + Require(Throws([] { VrTracker2ClearWord(nullptr); }), "clear word null"); + + std::array parameter{}; + parameter[0] = 0x20; + parameter.back() = 0xAA; + std::array requirement{}; + requirement.fill(0xAA); + Require(sceVrTracker2QueryMemory(parameter.data(), requirement.data()) == 0, "query return"); + const std::array fields{0x20, 0, 0, 0, 0, 0, 0, 0x02, 0, 0, 0x01, 0}; + Require(std::memcmp(requirement.data(), fields.data(), fields.size()) == 0, "query fields"); + Require(AllEqual(requirement.data() + 12, requirement.data() + 32, 0) && AllEqual(requirement.data() + 32, requirement.data() + 40, 0xAA), "query writes 32 bytes"); + Require(sceVrTracker2QueryMemory(nullptr, requirement.data()) == InvalidArgument, "query null parameter"); + Require(sceVrTracker2QueryMemory(parameter.data(), nullptr) == InvalidArgument, "query null requirement"); + for (std::size_t offset = 4; offset < 32; ++offset) { + auto rejected = parameter; + rejected[offset] = 1; + requirement.fill(0xAA); + Require(sceVrTracker2QueryMemory(rejected.data(), requirement.data()) == InvalidArgument, "query reserved byte"); + Require(AllEqual(requirement.data(), requirement.data() + 40, 0xAA), "query rejection leaves output"); + } + const std::uint64_t processSize = process.size(); + const std::uint32_t magic = 0x4942524f; + std::memcpy(process.data(), &processSize, sizeof(processSize)); + std::memcpy(process.data() + 8, &magic, sizeof(magic)); + Require(ProcessParametersFixture::Install(process.data(), process.size()), "process parameters"); + for (const std::uint32_t version : {0u, 0xffffffu, 0x1000000u, 0x7000000u}) { + std::memcpy(process.data() + 0x10, &version, sizeof(version)); + for (const std::uint32_t mode : {1000u, 1001u}) { + std::memcpy(parameter.data() + 4, &mode, sizeof(mode)); + requirement.fill(0xAA); + const auto result = sceVrTracker2QueryMemory(parameter.data(), requirement.data()); + if (version < 0x1000000) { + Require(result == InvalidArgument, "query SDK restriction"); + Require(AllEqual(requirement.data(), requirement.data() + requirement.size(), 0xAA), "SDK rejection preserves output"); + } else { + Require(result == 0, "query alternate mode"); + std::uint32_t memorySize; + std::memcpy(&memorySize, requirement.data() + 4, sizeof(memorySize)); + Require(memorySize == (mode == 1000 ? 7u << 20 : 3u << 20), "query alternate memory size"); + Require(AllEqual(requirement.data() + 12, requirement.data() + 32, 0), "query alternate reserved fields"); + Require(AllEqual(requirement.data() + 32, requirement.data() + 40, 0xAA), "query alternate output boundary"); + } + } + } + parameter[0] = 0x21; + Require(sceVrTracker2QueryMemory(parameter.data(), requirement.data()) == InvalidArgument, "query size"); + + Require(Throws([] { sceVrTracker2Initialize(nullptr); }), "unknown export"); + return 0; +} diff --git a/core/libs/tests/GuestWcsrtombsS.cpp b/core/libs/tests/GuestWcsrtombsS.cpp index 758ff5d33c..44a4206540 100644 --- a/core/libs/tests/GuestWcsrtombsS.cpp +++ b/core/libs/tests/GuestWcsrtombsS.cpp @@ -5,6 +5,7 @@ extern "C" int APS5_VABI wcsrtombs_s_nid_postfix( std::size_t*, char*, std::size_t, const std::uint16_t**, std::size_t, void*); +extern "C" int APS5_VABI wcstombs_s_nid_postfix(std::size_t*, char*, std::size_t, const std::uint16_t*, std::size_t); static void Require(bool value) { if (!value) std::abort(); } constexpr auto Failed = static_cast(-1); @@ -75,4 +76,22 @@ int main() { result = 7; Require(wcsrtombs_s_nid_postfix(&result, out, 4, &source, 4, nullptr) == 22 && result == Failed && out[0] == '\0'); Require(source == text && std::memcmp(out, "\0xxx", 4) == 0); + + char narrow[8]; + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, sizeof(narrow), text, sizeof(narrow)) == 0 && result == 3); + Require(std::memcmp(narrow, "abc\0x", 5) == 0); + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, sizeof(narrow), text, 2) == 0 && result == 2); + Require(std::memcmp(narrow, "ab\0x", 4) == 0); + Require(wcstombs_s_nid_postfix(&result, nullptr, 0, text, 0) == 0 && result == 3); + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, 3, text, 3) == 34 && result == Failed && narrow[0] == '\0'); + Require(wcstombs_s_nid_postfix(&result, nullptr, 4, text, 4) == 22 && result == Failed); + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, sizeof(narrow), nullptr, sizeof(narrow)) == 22); + Require(result == Failed && narrow[0] == '\0'); + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, sizeof(narrow), wide, sizeof(narrow)) == 86 && result == Failed); + Require(std::memcmp(narrow, "a\0xx", 4) == 0); } diff --git a/core/libs/tests/GuestWebBrowserDialog.cpp b/core/libs/tests/GuestWebBrowserDialog.cpp index 9c5ef4b12b..bb391077a8 100644 --- a/core/libs/tests/GuestWebBrowserDialog.cpp +++ b/core/libs/tests/GuestWebBrowserDialog.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include #include @@ -6,10 +7,11 @@ 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, const WebBrowserDialogAllowList* allowList); int APS5_VABI sceWebBrowserDialogClose(void); int APS5_VABI sceWebBrowserDialogGetStatus(void); int APS5_VABI sceWebBrowserDialogUpdateStatus(void); -int APS5_VABI sceWebBrowserDialogGetResult(void* result); +int APS5_VABI sceWebBrowserDialogGetResult(WebBrowserDialogResult* result); } namespace { @@ -28,21 +30,28 @@ void Require(bool value) { if (!value) std::abort(); } int main() { std::uint8_t param[64] = {}; - std::int32_t result = 0x5a5a5a5a; + WebBrowserDialogAllowList allowList{}; + allowList.size = sizeof(allowList); + allowList.addresses[0] = "test.invalid"; + WebBrowserDialogResult result{}; + result.result = 0x5a5a5a5a; + Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(sceWebBrowserDialogClose() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogOpen(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogGetResult(&result) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); - Require(result == 0x5a5a5a5a); + Require(result.result == 0x5a5a5a5a); Require(sceWebBrowserDialogInitialize() == 0); + Require(sceWebBrowserDialogInitialize() == COMMON_DIALOG_ERROR_ALREADY_INITIALIZED); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(sceWebBrowserDialogUpdateStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(sceWebBrowserDialogGetResult(&result) == COMMON_DIALOG_ERROR_NOT_FINISHED); - Require(result == 0x5a5a5a5a); + Require(result.result == 0x5a5a5a5a); Require(sceWebBrowserDialogTerminate() == 0); + Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(sceWebBrowserDialogInitialize() == 0); @@ -53,11 +62,72 @@ int main() { Require(sceWebBrowserDialogGetResult(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); Require(sceWebBrowserDialogGetResult(&result) == 0); - Require(result == COMMON_DIALOG_RESULT_USER_CANCELED); + Require(result.result == COMMON_DIALOG_RESULT_USER_CANCELED); - Require(sceWebBrowserDialogClose() == 0); + Require(sceWebBrowserDialogClose() == static_cast(0x80B8000Bu)); + Require(sceWebBrowserDialogTerminate() == 0); + Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + + Require(sceWebBrowserDialogOpenForPredeterminedContent(param, &allowList) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogInitialize() == 0); + Require(sceWebBrowserDialogOpenForPredeterminedContent(nullptr, &allowList) == COMMON_DIALOG_ERROR_ARG_NULL); + Require(sceWebBrowserDialogOpenForPredeterminedContent(param, nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); + allowList.addresses[0] = nullptr; + Require(sceWebBrowserDialogOpenForPredeterminedContent(param, &allowList) == static_cast(0x80B8000Au)); + allowList.addresses[19] = "test.invalid"; + allowList.reserved[255] = 1; + Require(sceWebBrowserDialogOpenForPredeterminedContent(param, &allowList) == static_cast(0x80B8000Au)); + allowList.reserved[255] = 0; + allowList.addresses[19] = ""; + Require(sceWebBrowserDialogOpenForPredeterminedContent(param, &allowList) == static_cast(0x80B8000Au)); + char longAddress[257]; + std::fill(std::begin(longAddress), std::end(longAddress), 'a'); + longAddress[256] = '\0'; + allowList.addresses[19] = longAddress; + Require(sceWebBrowserDialogOpenForPredeterminedContent(param, &allowList) == static_cast(0x80B8000Au)); + longAddress[255] = '\0'; + Require(sceWebBrowserDialogOpenForPredeterminedContent(param, &allowList) == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); + Require(sceWebBrowserDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); + + result.result = 0x5a5a5a5a; + Require(sceWebBrowserDialogGetResult(&result) == 0); + Require(result.result == COMMON_DIALOG_RESULT_USER_CANCELED); + + Require(sceWebBrowserDialogClose() == static_cast(0x80B8000Bu)); Require(sceWebBrowserDialogTerminate() == 0); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + Require(sceWebBrowserDialogInitialize() == 0); + Require(sceWebBrowserDialogClose() == static_cast(0x80B8000Bu)); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); + Require(sceWebBrowserDialogGetResult(&result) == COMMON_DIALOG_ERROR_NOT_FINISHED); + Require(sceWebBrowserDialogTerminate() == 0); + Require(sceWebBrowserDialogGetResult(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); + result.reserved[239] = 1; + Require(sceWebBrowserDialogGetResult(&result) == static_cast(0x80B8000Au)); + result.reserved[239] = 0; + WebBrowserDialogExtendedResult extended{}; + result.extended = &extended; + Require(sceWebBrowserDialogGetResult(&result) == static_cast(0x80B8000Au)); + extended.size = sizeof(extended); + Require(sceWebBrowserDialogGetResult(&result) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogInitialize() == 0); + Require(sceWebBrowserDialogOpen(param) == 0); + extended.reserved0[3] = 1; + Require(sceWebBrowserDialogGetResult(&result) == static_cast(0x80B8000Au)); + extended.reserved0[3] = 0; + extended.reserved1[19] = 1; + Require(sceWebBrowserDialogGetResult(&result) == static_cast(0x80B8000Au)); + extended.reserved1[19] = 0; + extended.type = 0x5a5a5a5a; + extended.content[2047] = 'x'; + result.padding = 0x5a5a5a5a; + Require(sceWebBrowserDialogGetResult(&result) == 0); + Require(result.result == COMMON_DIALOG_RESULT_USER_CANCELED); + Require(result.padding == 0x5a5a5a5a && result.extended == &extended); + Require(extended.type == 0x5a5a5a5a && extended.content[2047] == 'x'); + Require(sceWebBrowserDialogTerminate() == 0); return 0; } diff --git a/core/libs/tests/GuestWebBrowserDialogCookies.cpp b/core/libs/tests/GuestWebBrowserDialogCookies.cpp new file mode 100644 index 0000000000..723bada25d --- /dev/null +++ b/core/libs/tests/GuestWebBrowserDialogCookies.cpp @@ -0,0 +1,146 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +struct ResetCookieParam { + std::uint64_t size; + std::uint8_t reserved[256]; +}; + +struct SetCookieParam { + std::uint64_t size; + const char* url; + const char* cookie; + std::uint8_t reserved[256]; +}; + +extern "C" { +int APS5_VABI sceWebBrowserDialogInitialize(void); +int APS5_VABI sceWebBrowserDialogTerminate(void); +int APS5_VABI sceWebBrowserDialogOpen(const void* param); +int APS5_VABI sceWebBrowserDialogClose(void); +int APS5_VABI sceWebBrowserDialogUpdateStatus(void); +int APS5_VABI sceWebBrowserDialogSetCookie(const SetCookieParam* param); +int APS5_VABI sceWebBrowserDialogResetCookie(const ResetCookieParam* param); +} + +namespace { + +constexpr int NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int INVALID_STATE = static_cast(0x80B80006u); +constexpr int NOT_RUNNING = static_cast(0x80B8000Bu); +constexpr int PARAM_INVALID = static_cast(0x80B8000Au); +constexpr int ARG_NULL = static_cast(0x80B8000Du); + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "FAIL: %s\n", what); + std::abort(); +} + +SetCookieParam Cookie(const char* url, const char* cookie) { + SetCookieParam param {}; + param.size = sizeof(param); + param.url = url; + param.cookie = cookie; + return param; +} + +ResetCookieParam Reset() { + ResetCookieParam param {}; + param.size = sizeof(param); + return param; +} + +void CheckSetCookieParameters() { + auto valid = Cookie("https://example.com", "a=b"); + Require(sceWebBrowserDialogSetCookie(&valid) == 0, "valid SetCookie"); + Require(sceWebBrowserDialogSetCookie(nullptr) == ARG_NULL, "SetCookie null"); + + auto wrongSize = valid; + wrongSize.size = sizeof(ResetCookieParam); + Require(sceWebBrowserDialogSetCookie(&wrongSize) == PARAM_INVALID, "SetCookie size"); + auto noUrl = Cookie(nullptr, "a=b"); + Require(sceWebBrowserDialogSetCookie(&noUrl) == PARAM_INVALID, "SetCookie null url"); + auto emptyUrl = Cookie("", "a=b"); + Require(sceWebBrowserDialogSetCookie(&emptyUrl) == PARAM_INVALID, "SetCookie empty url"); + auto noCookie = Cookie("https://example.com", nullptr); + Require(sceWebBrowserDialogSetCookie(&noCookie) == PARAM_INVALID, "SetCookie null cookie"); + auto emptyCookie = Cookie("https://example.com", ""); + Require(sceWebBrowserDialogSetCookie(&emptyCookie) == PARAM_INVALID, "SetCookie empty cookie"); + + const std::string longestUrl(0x7ff, 'u'); + const std::string longUrl(0x800, 'u'); + auto longest = Cookie(longestUrl.c_str(), "a=b"); + Require(sceWebBrowserDialogSetCookie(&longest) == 0, "SetCookie 2047-byte url"); + auto tooLong = Cookie(longUrl.c_str(), "a=b"); + Require(sceWebBrowserDialogSetCookie(&tooLong) == PARAM_INVALID, "SetCookie 2048-byte url"); + + const std::string longestCookie(0xfff, 'c'); + const std::string longCookie(0x1000, 'c'); + auto longestValue = Cookie("https://example.com", longestCookie.c_str()); + Require(sceWebBrowserDialogSetCookie(&longestValue) == 0, "SetCookie 4095-byte cookie"); + auto tooLongValue = Cookie("https://example.com", longCookie.c_str()); + Require(sceWebBrowserDialogSetCookie(&tooLongValue) == PARAM_INVALID, "SetCookie 4096-byte cookie"); + + auto reserved = valid; + reserved.reserved[255] = 1; + Require(sceWebBrowserDialogSetCookie(&reserved) == PARAM_INVALID, "SetCookie reserved"); +} + +void CheckResetCookieParameters() { + auto valid = Reset(); + Require(sceWebBrowserDialogResetCookie(&valid) == 0, "valid ResetCookie"); + Require(sceWebBrowserDialogResetCookie(nullptr) == ARG_NULL, "ResetCookie null"); + auto wrongSize = valid; + wrongSize.size = sizeof(SetCookieParam); + Require(sceWebBrowserDialogResetCookie(&wrongSize) == PARAM_INVALID, "ResetCookie size"); + auto reserved = valid; + reserved.reserved[0] = 1; + Require(sceWebBrowserDialogResetCookie(&reserved) == PARAM_INVALID, "ResetCookie reserved"); +} + +void CheckOpenDialog() { + std::uint8_t open[64] = {}; + auto cookie = Cookie("https://example.com", "a=b"); + auto reset = Reset(); + Require(sceWebBrowserDialogOpen(open) == 0, "Open"); + Require(sceWebBrowserDialogSetCookie(&cookie) == INVALID_STATE, "SetCookie while open"); + Require(sceWebBrowserDialogSetCookie(nullptr) == INVALID_STATE, "SetCookie checks the open dialog before null"); + Require(sceWebBrowserDialogResetCookie(nullptr) == ARG_NULL, "ResetCookie checks null before the open dialog"); + Require(sceWebBrowserDialogResetCookie(&reset) == INVALID_STATE, "ResetCookie while open"); + Require(sceWebBrowserDialogUpdateStatus() == 3, "UpdateStatus finished"); + Require(sceWebBrowserDialogSetCookie(&cookie) == 0, "SetCookie after UpdateStatus"); + Require(sceWebBrowserDialogResetCookie(&reset) == 0, "ResetCookie after UpdateStatus"); + + Require(sceWebBrowserDialogOpen(open) == 0, "second Open"); + Require(sceWebBrowserDialogClose() == NOT_RUNNING, "Close of a finished dialog"); + Require(sceWebBrowserDialogSetCookie(&cookie) == 0, "SetCookie after Close"); + + Require(sceWebBrowserDialogOpen(open) == 0, "third Open"); + Require(sceWebBrowserDialogTerminate() == 0, "Terminate"); + Require(sceWebBrowserDialogInitialize() == 0, "Initialize again"); + Require(sceWebBrowserDialogResetCookie(&reset) == 0, "ResetCookie after Terminate"); +} + +} + +int main() { + auto cookie = Cookie("https://example.com", "a=b"); + auto reset = Reset(); + Require(sceWebBrowserDialogSetCookie(&cookie) == NOT_INITIALIZED, "SetCookie before Initialize"); + Require(sceWebBrowserDialogSetCookie(nullptr) == NOT_INITIALIZED, "SetCookie null before Initialize"); + Require(sceWebBrowserDialogResetCookie(&reset) == NOT_INITIALIZED, "ResetCookie before Initialize"); + Require(sceWebBrowserDialogResetCookie(nullptr) == NOT_INITIALIZED, "ResetCookie null before Initialize"); + + Require(sceWebBrowserDialogInitialize() == 0, "Initialize"); + CheckSetCookieParameters(); + CheckResetCookieParameters(); + CheckOpenDialog(); + Require(sceWebBrowserDialogTerminate() == 0, "final Terminate"); + std::puts("guest web browser dialog cookies: PASS"); + return 0; +} diff --git a/core/libs/tests/GuestWideConversion.cpp b/core/libs/tests/GuestWideConversion.cpp index 8cd823acbe..1ade68c23f 100644 --- a/core/libs/tests/GuestWideConversion.cpp +++ b/core/libs/tests/GuestWideConversion.cpp @@ -69,7 +69,7 @@ static void CheckStoul() { CheckStoul(u"017", 0, 15, 3, 0, "octal with base 0"); CheckStoul(u"z", 36, 35, 1, 0, "base 36"); CheckStoul(u"0b1", 0, 0, 1, 0, "no binary prefix"); - CheckStoul(u"0xg", 16, 0, 1, 0, "prefix without digits"); + CheckStoul(u"0xg", 16, 0, 0, 0, "prefix without digits"); CheckStoul(u"12é3", 10, 12, 2, 0, "stops at a non-ASCII unit"); CheckStoul(u" 5", 10, 0, 0, 0, "non-ASCII space is not skipped"); CheckStoul(u"", 10, 0, 0, 0, "empty string"); @@ -118,6 +118,8 @@ static void CheckScan() { Require(swscanf_s_nid_postfix(u"-2", u"%hd", &half) == 1 && half == -2, "%hd"); unsigned value = 0; Require(swscanf_s_nid_postfix(u"0xg", u"%x", &value) == 0, "%x prefix without digits is a matching failure"); + void* pointer = nullptr; + Require(swscanf_s_nid_postfix(u"0x7ffe12345678 5", u"%p%d", &pointer, &first) == 2 && reinterpret_cast(pointer) == 0x7ffe12345678u && first == 5, "%p takes no size"); char16_t text[6] = {u'?'}; Require(swscanf_s_nid_postfix(u"hello world", u"%ls", text, std::size_t{6}) == 1 && std::u16string(text) == u"hello", "%ls fits"); @@ -137,13 +139,15 @@ static void CheckScan() { double real = 0; Require(swscanf_s_nid_postfix(u"1.5e3", u"%lf", &real) == 1 && real == 1500.0, "%lf"); - Require(swscanf_s_nid_postfix(u"1e+x", u"%lf", &real) == 0, "%lf incomplete exponent is a matching failure"); + Require(swscanf_s_nid_postfix(u"1e+", u"%lf", &real) == 1 && real == 1.0, "%lf converts the mantissa of an incomplete exponent"); long double extended = 0; Require(swscanf_s_nid_postfix(u"2.25", u"%Lf", &extended) == 1 && extended == 2.25L, "%Lf"); float single = 0; Require(swscanf_s_nid_postfix(u"-inf", u"%f", &single) == 1 && single == -std::numeric_limits::infinity(), "%f infinity"); Require(swscanf_s_nid_postfix(u"a", u"b%d", &first) == 0, "literal mismatch is a matching failure"); + Require(swscanf_s_nid_postfix(u"", u"x") == 0, "literal at end of input returns zero"); + Require(swscanf_s_nid_postfix(u"", u"%%") == -1, "percent literal at end of input is an input failure"); Require(swscanf_s_nid_postfix(u"", u"%d", &first) == -1, "empty input is an input failure"); Require(swscanf_s_nid_postfix(u" ", u"%ls", text, std::size_t{6}) == -1, "white space only is an input failure"); Require(swscanf_s_nid_postfix(u"5", u"%d%d", &first, &second) == 1, "input failure after a conversion"); @@ -169,6 +173,14 @@ static void CheckUnsizedScan() { Require(swscanf_nid_postfix(u"-2", u"%hd", &half) == 1 && half == -2, "swscanf %hd"); unsigned value = 0; Require(swscanf_nid_postfix(u"0xg", u"%x", &value) == 0, "swscanf %x prefix without digits is a matching failure"); + void* pointer = nullptr; + Require(swscanf_nid_postfix(u"0x7ffe12345678", u"%p", &pointer) == 1 && reinterpret_cast(pointer) == 0x7ffe12345678u, "swscanf %p stores a whole pointer"); + Require(swscanf_nid_postfix(u"DeadBeef", u"%p", &pointer) == 1 && reinterpret_cast(pointer) == 0xdeadbeefu, "swscanf %p without a prefix"); + void* next = nullptr; + Require(swscanf_nid_postfix(u"0x1234", u"%4p%p", &pointer, &next) == 2 && reinterpret_cast(pointer) == 0x12u && reinterpret_cast(next) == 0x34u, "swscanf %p width"); + Require(swscanf_nid_postfix(u"0xff 7", u"%*p%d", &first) == 1 && first == 7, "swscanf suppressed %p"); + Require(swscanf_nid_postfix(u"-1", u"%p", &pointer) == 1 && reinterpret_cast(pointer) == std::numeric_limits::max(), "swscanf %p converts as unsigned"); + Require(swscanf_nid_postfix(u"0xg", u"%p", &pointer) == 0, "swscanf %p prefix without digits is a matching failure"); char16_t text[16] = {u'?'}; Require(swscanf_nid_postfix(u"hello world", u"%ls", text) == 1 && std::u16string(text) == u"hello", "swscanf %ls"); @@ -193,13 +205,15 @@ static void CheckUnsizedScan() { double real = 0; Require(swscanf_nid_postfix(u"1.5e3", u"%lf", &real) == 1 && real == 1500.0, "swscanf %lf"); - Require(swscanf_nid_postfix(u"1e+x", u"%lf", &real) == 0, "swscanf %lf incomplete exponent is a matching failure"); + Require(swscanf_nid_postfix(u"1e+", u"%lf", &real) == 1 && real == 1.0, "swscanf %lf converts the mantissa of an incomplete exponent"); long double extended = 0; Require(swscanf_nid_postfix(u"2.25", u"%Lf", &extended) == 1 && extended == 2.25L, "swscanf %Lf"); float single = 0; Require(swscanf_nid_postfix(u"-inf", u"%f", &single) == 1 && single == -std::numeric_limits::infinity(), "swscanf %f infinity"); Require(swscanf_nid_postfix(u"a", u"b%d", &first) == 0, "swscanf literal mismatch is a matching failure"); + Require(swscanf_nid_postfix(u"", u"x") == 0, "swscanf literal at end of input returns zero"); + Require(swscanf_nid_postfix(u"", u"%%") == -1, "swscanf percent literal at end of input is an input failure"); Require(swscanf_nid_postfix(u"3 x", u"%d%d", &first, &second) == 1 && first == 3, "swscanf partial match"); Require(swscanf_nid_postfix(u"", u"%d", &first) == -1, "swscanf empty input is an input failure"); Require(swscanf_nid_postfix(u"", u"%ls", text) == -1, "swscanf empty input before %ls"); @@ -209,6 +223,7 @@ static void CheckUnsizedScan() { Require(swscanf_nid_postfix(u"5", nullptr) == -1, "swscanf null format"); Require(Throws([&] { swscanf_nid_postfix(u"abc", u"%l[a-c]", text); }), "swscanf scanset range throws"); Require(Throws([&] { swscanf_nid_postfix(u"5", u"%Ld", &wide); }), "swscanf %Ld throws"); + Require(Throws([&] { swscanf_nid_postfix(u"1", u"%lp", &pointer); }), "swscanf %lp throws"); Require(Throws([&] { swscanf_nid_postfix(u"Ā", u"%s", narrow); }), "swscanf unconvertible narrow %s throws"); } diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index f372fd47e1..8ebfa2753e 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -114,10 +114,13 @@ static void CheckPositional() { static void CheckPointer() { char16_t buffer[64]; const auto address = [](std::uintptr_t value) { return reinterpret_cast(value); }; - Require(Format(buffer, 64, u"%p|%p|%p", address(0), address(0x1234), address(0xdeadbeef12)) == 23 && - buffer == std::u16string(u"0x0|0x1234|0xdeadbeef12"), "vswprintf %p uses the 0x form"); - Require(Format(buffer, 64, u"[%8p][%-8p][%08p][%.6p]", address(0x1a), address(0x1a), address(0x1a), address(0x1a)) == 40 && - buffer == std::u16string(u"[ 0x1a][0x1a ][0x00001a][0x00001a]"), "vswprintf %p width and precision"); + Require(Format(buffer, 64, u"%p|%p|%p", address(0), address(0x1234), address(0xdeadbeef12)) == 17 && + buffer == std::u16string(u"0|1234|deadbeef12"), "vswprintf %p uses the console form"); + Require(Format(buffer, 64, u"[%10p][%-10p][%.8p][%08p]", address(0x1234), address(0x1234), address(0x1234), address(0x1a)) == 44 && + buffer == std::u16string(u"[ 1234][1234 ][00001234][0000001a]"), "vswprintf %p width and precision"); + Require(Format(buffer, 64, u"[%.0p][%p]", address(0), address(0)) == 5 && + buffer == std::u16string(u"[][0]"), "vswprintf %p with a zero precision prints nothing"); + Require(Format(buffer, 64, u"[%lp]", address(0x1234)) == -1, "vswprintf %lp is refused"); } static void CheckCount() { @@ -273,7 +276,33 @@ static void CheckWcsftime() { Require(WcsftimeThrows(buffer, u"%Y", nullptr), "wcsftime null time"); } +static void CheckAlternateOctalZero() { + struct Case { const char16_t* format; const char16_t* expected; unsigned value; }; + for (const auto& test : { + Case{u"%#.0o", u"0", 0}, Case{u"%#.o", u"0", 0}, Case{u"%#.00o", u"0", 0}, + Case{u"[%#5.0o]", u"[ 0]", 0}, Case{u"[%-#5.0o]", u"[0 ]", 0}, + Case{u"[%#05.0o]", u"[ 0]", 0}, Case{u"%.0o", u"", 0}, Case{u"%#.0x", u"", 0}, + Case{u"%#.0o", u"01", 1}, Case{u"%#.1o", u"0", 0}, Case{u"%#.3o", u"000", 0}, + Case{u"%#.0hho", u"0", 256}, Case{u"%#.0ho", u"0", 65536} + }) { + char16_t buffer[32]; + const auto expected = std::u16string(test.expected); + Require(swprintf_nid_postfix(buffer, 32, test.format, test.value) == static_cast(expected.size()) && buffer == expected, "swprintf alternate octal zero"); + Require(Format(buffer, 32, test.format, test.value) == static_cast(expected.size()) && buffer == expected, "vswprintf alternate octal zero"); + Require(snwprintf_s_nid_postfix(buffer, 32, test.format, test.value) == static_cast(expected.size()) && buffer == expected, "snwprintf_s alternate octal zero"); + } + char16_t buffer[16]; + Require(swprintf_nid_postfix(buffer, 16, u"%#.*o", 0, 0u) == 1 && buffer == std::u16string(u"0"), "octal zero dynamic precision"); + Require(swprintf_nid_postfix(buffer, 16, u"%#.*o", -1, 0u) == 1 && buffer == std::u16string(u"0"), "octal zero omitted precision"); + Require(swprintf_nid_postfix(buffer, 16, u"%2$#.*1$o", 0, 0u) == 1 && buffer == std::u16string(u"0"), "octal zero positional precision"); + Require(swprintf_nid_postfix(buffer, 16, u"%#.0llo", 0ull) == 1 && buffer == std::u16string(u"0"), "octal zero long long"); + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(snwprintf_s_nid_postfix(buffer, 1, u"%#.0o", 0u) == 1 && buffer[0] == 0 && buffer[1] == u'y', "octal zero bounded output"); +} + int main() { + CheckAlternateOctalZero(); CheckBounded(); CheckPositional(); CheckPointer(); diff --git a/core/libs/tests/GuestWideStreamScan.cpp b/core/libs/tests/GuestWideStreamScan.cpp new file mode 100644 index 0000000000..632a989759 --- /dev/null +++ b/core/libs/tests/GuestWideStreamScan.cpp @@ -0,0 +1,176 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +extern FileStream* __stdinp_nid_postfix; +int APS5_VABI vfwscanf_nid_postfix(FileStream*, const char16_t*, void*); +int APS5_VABI vfwscanf_s_nid_postfix(FileStream*, const char16_t*, void*); +int APS5_VABI vwscanf_nid_postfix(const char16_t*, void*); +int APS5_VABI vwscanf_s_nid_postfix(const char16_t*, void*); +int APS5_VABI fwscanf_nid_postfix(FileStream*, const char16_t*, ...); +int APS5_VABI fwscanf_s_nid_postfix(FileStream*, const char16_t*, ...); +int APS5_VABI wscanf_nid_postfix(const char16_t*, ...); +int APS5_VABI wscanf_s_nid_postfix(const char16_t*, ...); +std::int32_t APS5_VABI fgetwc_nid_postfix(FileStream*); +} + +#ifdef _WIN32 +#define APS5_VA_BEGIN(last) __builtin_sysv_va_list args; __builtin_sysv_va_start(args, last) +#define APS5_VA_END() __builtin_sysv_va_end(args) +#else +#define APS5_VA_BEGIN(last) std::va_list args; va_start(args, last) +#define APS5_VA_END() va_end(args) +#endif + +static void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", what); + std::abort(); +} + +static int APS5_VABI ScanStream(FileStream* stream, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfwscanf_nid_postfix(stream, format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanStreamSized(FileStream* stream, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfwscanf_s_nid_postfix(stream, format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanInput(const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vwscanf_nid_postfix(format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanInputSized(const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vwscanf_s_nid_postfix(format, args); + APS5_VA_END(); + return result; +} + +static FileStream* Open(const char* text) { + auto* stream = new FileStream(std::tmpfile()); + Require(std::fputs(text, stream->GetHandle()) >= 0, "seed the stream"); + std::rewind(stream->GetHandle()); + return stream; +} + +static void Close(FileStream* stream) { + stream->Close(); + delete stream; +} + +static void CheckConsumption() { + std::int32_t value = -1; + std::int64_t count = -1; + char16_t word[8]{}; + + FileStream* stream = Open(" 42 rest"); + Require(ScanStream(stream, u"%d", &value) == 1 && value == 42, "an integer after leading space"); + Require(fgetwc_nid_postfix(stream) == u' ', "the character after the field stays in the stream"); + Require(fwscanf_nid_postfix(stream, u"%ls", word) == 1 && std::u16string(word) == u"rest", "a following fwscanf continues"); + Require(fwscanf_nid_postfix(stream, u"%d", &value) == EOF, "end of file before any conversion"); + Close(stream); + + stream = Open("0xg"); + std::uint32_t hex = 7; + Require(ScanStream(stream, u"%x", &hex) == 0 && hex == 7, "a hexadecimal prefix without digits is a matching failure"); + Require(fgetwc_nid_postfix(stream) == u'g', "the prefix is consumed and the next character is pushed back"); + Close(stream); + + stream = Open("12abc"); + Require(ScanStream(stream, u"%d%ln", &value, &count) == 1 && value == 12 && count == 2, "%ln counts the consumed characters"); + Require(fgetwc_nid_postfix(stream) == u'a', "the field stops before the first letter"); + Close(stream); + + stream = Open("ab"); + Require(ScanStream(stream, u"ac") == 0, "a literal mismatch"); + Require(fgetwc_nid_postfix(stream) == u'b', "the mismatching character is pushed back"); + Close(stream); + + stream = Open(""); + Require(ScanStream(stream, u"x") == 0, "a literal at end of input returns zero"); + Close(stream); + + stream = Open(""); + Require(ScanStream(stream, u"%%") == -1, "a percent literal at end of input is an input failure"); + Close(stream); + + stream = Open("x"); + Require(ScanStream(stream, u"%d", &value) == 0, "a letter is not an integer"); + Require(fgetwc_nid_postfix(stream) == u'x', "the rejected letter is pushed back"); + Close(stream); + + stream = Open("-"); + Require(ScanStream(stream, u"%d", &value) == 0, "a lone sign is a matching failure"); + Require(fgetwc_nid_postfix(stream) == -1, "the sign is consumed"); + Close(stream); + + stream = Open(" "); + Require(ScanStream(stream, u"%d", &value) == EOF, "only white space"); + Close(stream); + + stream = Open("2.5x"); + double real = 0; + Require(ScanStream(stream, u"%lf", &real) == 1 && real == 2.5, "a float"); + Require(fgetwc_nid_postfix(stream) == u'x', "the character after the float"); + Close(stream); + + stream = Open("1e+"); + Require(ScanStream(stream, u"%lf", &real) == 1 && real == 1.0, "an incomplete exponent converts its mantissa"); + Close(stream); +} + +static void CheckWideText() { + std::int32_t value = 0; + char16_t letters[8]{}; + FileStream* stream = Open("5 ab\xc3\xa9t\xc3\xa9"); + Require(ScanStream(stream, u"%d %l[abc]", &value, letters) == 2 && value == 5 && std::u16string(letters) == u"ab", "a scanset stops at a non-ASCII character"); + Require(fgetwc_nid_postfix(stream) == 0xe9, "a non-ASCII lookahead goes back as UTF-8"); + Require(fwscanf_nid_postfix(stream, u"%ls", letters) == 1 && std::u16string(letters) == u"té", "the rest decodes"); + Close(stream); + + stream = Open("hello world"); + char16_t small[4]{}; + Require(ScanStreamSized(stream, u"%3ls", small, 4u) == 1 && std::u16string(small) == u"hel", "vfwscanf_s %ls with a size"); + Require(fwscanf_s_nid_postfix(stream, u"%ls", small, 4u) == 1 && std::u16string(small) == u"lo", "fwscanf_s %ls that fits"); + Require(fwscanf_s_nid_postfix(stream, u"%ls", small, 4u) == 0 && small[0] == 0, "fwscanf_s rejects a too small buffer"); + Close(stream); + + Require(fwscanf_nid_postfix(nullptr, u"%d", &value) == EOF, "null stream"); + Require(vfwscanf_s_nid_postfix(nullptr, u"%d", nullptr) == EOF, "null stream with sizes"); +} + +static void CheckStandardInput() { + FileStream* const standardInput = __stdinp_nid_postfix; + FileStream* stream = Open("-5 12 ab cd"); + __stdinp_nid_postfix = stream; + std::int32_t first = 0, second = 0; + char16_t word[4]{}; + Require(ScanInput(u"%d", &first) == 1 && first == -5, "vwscanf reads the guest standard input"); + Require(wscanf_nid_postfix(u"%d", &second) == 1 && second == 12, "wscanf reads the guest standard input"); + Require(ScanInputSized(u"%ls", word, 4u) == 1 && std::u16string(word) == u"ab", "vwscanf_s"); + Require(wscanf_s_nid_postfix(u"%ls", word, 4u) == 1 && std::u16string(word) == u"cd", "wscanf_s"); + __stdinp_nid_postfix = standardInput; + Close(stream); +} + +int main() { + CheckConsumption(); + CheckWideText(); + CheckStandardInput(); + return 0; +} diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 101ea50e68..c55aac8ebb 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -8,23 +8,42 @@ #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); char16_t* APS5_VABI wmemmove_nid_postfix(char16_t* dest, const char16_t* src, std::size_t n); std::size_t APS5_VABI wcslen_nid_postfix(const char16_t* s); +std::size_t APS5_VABI wcsnlen_s_nid_postfix(const char16_t* s, std::size_t maxlen); int APS5_VABI wcscmp_nid_postfix(const char16_t* s1, const char16_t* s2); int APS5_VABI wcsncmp_nid_postfix(const char16_t* s1, const char16_t* s2, std::size_t n); char16_t* APS5_VABI wcscpy_nid_postfix(char16_t* dest, const char16_t* src); char16_t* APS5_VABI wcscat_nid_postfix(char16_t* dest, const char16_t* src); char16_t* APS5_VABI wcsncpy_nid_postfix(char16_t* dest, const char16_t* src, std::size_t n); +int APS5_VABI wmemcpy_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src, std::size_t count); +int APS5_VABI wmemmove_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src, std::size_t count); +int APS5_VABI wcsncpy_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src, std::size_t count); +int APS5_VABI wcscpy_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src); +void* APS5_VABI set_constraint_handler_s_nid_postfix(void* handler); +void APS5_VABI ignore_handler_s_nid_postfix(const char* message, void* pointer, int error); +char16_t* APS5_VABI wcsncat_nid_postfix(char16_t* dest, const char16_t* src, std::size_t n); +int APS5_VABI wcsncat_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src, std::size_t count); +int APS5_VABI wcscat_s_nid_postfix(char16_t* dest, std::size_t destsz, const char16_t* src); const char16_t* APS5_VABI wcschr_nid_postfix(const char16_t* s, char16_t c); const char16_t* APS5_VABI wcsrchr_nid_postfix(const char16_t* s, char16_t c); 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); @@ -32,6 +51,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); @@ -54,11 +78,66 @@ 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); require(wcslen_nid_postfix(split) == 2); + require(wcsnlen_s_nid_postfix(nullptr, 10) == 0); + require(wcsnlen_s_nid_postfix(u"", 5) == 0); + require(wcsnlen_s_nid_postfix(u"héllo", 0) == 0); + require(wcsnlen_s_nid_postfix(u"héllo", 3) == 3); + require(wcsnlen_s_nid_postfix(u"héllo", 10) == 5); + require(wcsnlen_s_nid_postfix(split, 4) == 2); + require(wcscmp_nid_postfix(u"abc", u"abc") == 0); require(wcscmp_nid_postfix(u"abc", u"abd") < 0); require(wcscmp_nid_postfix(u"abd", u"abc") > 0); @@ -92,6 +171,82 @@ int main() { const char16_t truncated[] = {u'a', u'b', u'c', 0xaaaa}; require(same(buffer, truncated, 4)); + char16_t memBuf[16]; + require(wmemcpy_s_nid_postfix(memBuf, 16, u"test", 4) == 0 && same(memBuf, u"test", 4)); + require(wmemcpy_s_nid_postfix(memBuf, 16, u"abc", 0) == 0); + require(wmemcpy_s_nid_postfix(nullptr, 16, u"abc", 3) == 22); + require(wmemcpy_s_nid_postfix(memBuf, 16, nullptr, 3) == 22 && memBuf[0] == 0); + wmemset_nid_postfix(memBuf, u'Z', 16); + require(wmemcpy_s_nid_postfix(memBuf, 4, u"12345", 5) == 34 && memBuf[0] == 0 && memBuf[3] == 0); + require(wmemcpy_s_nid_postfix(memBuf, ((SIZE_MAX >> 1) / sizeof(char16_t)) + 1, u"a", 1) == 34); + wmemcpy_s_nid_postfix(memBuf, 16, u"abcdefgh", 8); + require(wmemcpy_s_nid_postfix(memBuf + 2, 8, memBuf, 4) == 22 && memBuf[2] == 0); + + wmemcpy_s_nid_postfix(memBuf, 16, u"0123456789", 10); + require(wmemmove_s_nid_postfix(memBuf + 2, 10, memBuf, 5) == 0); + require(same(memBuf, u"0101234789", 10)); + require(wmemmove_s_nid_postfix(memBuf, 10, memBuf + 2, 5) == 0); + require(same(memBuf, u"0123434789", 10)); + require(wmemmove_s_nid_postfix(nullptr, 16, memBuf, 3) == 22); + require(wmemmove_s_nid_postfix(memBuf, 16, nullptr, 3) == 22 && memBuf[0] == 0); + require(wmemmove_s_nid_postfix(memBuf, 4, u"12345", 5) == 34 && memBuf[0] == 0); + require(wmemmove_s_nid_postfix(memBuf, ((SIZE_MAX >> 1) / sizeof(char16_t)) + 1, u"a", 1) == 34); + + char16_t strBuf[16]; + require(wcsncpy_s_nid_postfix(strBuf, 16, u"hello", 5) == 0 && same(strBuf, u"hello", 6)); + require(wcsncpy_s_nid_postfix(strBuf, 16, u"hello world", 5) == 0 && same(strBuf, u"hello", 6)); + require(wcsncpy_s_nid_postfix(strBuf, 16, u"hi", 10) == 0 && same(strBuf, u"hi", 3)); + require(wcsncpy_s_nid_postfix(nullptr, 16, u"hi", 2) == 22); + require(wcsncpy_s_nid_postfix(strBuf, 0, u"hi", 2) == 22); + strBuf[0] = u'X'; + require(wcsncpy_s_nid_postfix(strBuf, 16, nullptr, 2) == 22 && strBuf[0] == 0); + strBuf[0] = u'X'; + require(wcsncpy_s_nid_postfix(strBuf, 4, u"hello", 5) == 34 && strBuf[0] == 0); + strBuf[0] = u'X'; + require(wcsncpy_s_nid_postfix(strBuf, 5, u"hello", 5) == 34 && strBuf[0] == 0); + require(wcsncpy_s_nid_postfix(strBuf, ((SIZE_MAX >> 1) / sizeof(char16_t)) + 1, u"a", 1) == 34); + wcsncpy_s_nid_postfix(strBuf, 16, u"abcdef", 6); + require(wcsncpy_s_nid_postfix(strBuf + 1, 10, strBuf, 3) == 22 && strBuf[1] == 0); + + char16_t cpyBuf[16]; + require(wcscpy_s_nid_postfix(cpyBuf, 16, u"wide") == 0 && same(cpyBuf, u"wide", 5)); + void* previousHandler = set_constraint_handler_s_nid_postfix(reinterpret_cast(&ignore_handler_s_nid_postfix)); + require(wcscpy_s_nid_postfix(nullptr, 16, u"wide") == 22); + require(wcscpy_s_nid_postfix(cpyBuf, 0, u"wide") == 22); + require(wcscpy_s_nid_postfix(cpyBuf, 16, nullptr) == 22 && cpyBuf[0] == 0); + require(wcscpy_s_nid_postfix(cpyBuf, 4, u"wide") == 34 && cpyBuf[0] == 0); + cpyBuf[0] = 0x1234; + require(wcscpy_s_nid_postfix(cpyBuf, ((SIZE_MAX >> 1) / sizeof(char16_t)) + 1, u"a") == 34 && cpyBuf[0] == 0x1234); + wcscpy_s_nid_postfix(cpyBuf, 16, u"abcdef"); + require(wcscpy_s_nid_postfix(cpyBuf + 1, 10, cpyBuf) == 22 && cpyBuf[1] == 0); + set_constraint_handler_s_nid_postfix(previousHandler); + + char16_t ncatBuf[16]; + wmemset_nid_postfix(ncatBuf, 0xaaaa, 16); + ncatBuf[0] = 0; + require(wcsncat_nid_postfix(ncatBuf, u"hello", 3) == ncatBuf); + require(same(ncatBuf, u"hel", 4) && ncatBuf[4] == 0xaaaa); + require(wcsncat_nid_postfix(ncatBuf, u"lo world", 10) == ncatBuf); + require(same(ncatBuf, u"hello world", 12) && ncatBuf[12] == 0xaaaa); + + wcsncpy_s_nid_postfix(strBuf, 16, u"hello", 5); + require(wcsncat_s_nid_postfix(strBuf, 16, u" world", 6) == 0 && same(strBuf, u"hello world", 12)); + wcsncpy_s_nid_postfix(strBuf, 16, u"foo", 3); + require(wcsncat_s_nid_postfix(strBuf, 16, u"barbaz", 3) == 0 && same(strBuf, u"foobar", 7)); + wcsncpy_s_nid_postfix(strBuf, 8, u"abc", 3); + require(wcsncat_s_nid_postfix(strBuf, 8, u"1234", 4) == 0 && same(strBuf, u"abc1234", 8)); + wcsncpy_s_nid_postfix(strBuf, 8, u"abc", 3); + require(wcsncat_s_nid_postfix(strBuf, 8, u"12345", 5) == 34 && strBuf[0] == 0); + wmemset_nid_postfix(strBuf, u'x', 4); + require(wcsncat_s_nid_postfix(strBuf, 4, u"a", 1) == 34 && strBuf[0] == 0); + wcsncpy_s_nid_postfix(strBuf, 16, u"abc", 3); + require(wcscat_s_nid_postfix(strBuf, 16, u"def") == 0 && same(strBuf, u"abcdef", 7)); + wcsncpy_s_nid_postfix(strBuf, 8, u"12345", 5); + require(wcscat_s_nid_postfix(strBuf, 8, u"6789") == 34 && strBuf[0] == 0); + require(wcscat_s_nid_postfix(nullptr, 8, u"a") == 22); + wcsncpy_s_nid_postfix(strBuf, 8, u"123", 3); + require(wcscat_s_nid_postfix(strBuf, 8, nullptr) == 22 && strBuf[0] == 0); + const char16_t* text = u"a世b世c"; require(wcschr_nid_postfix(text, u'世') == text + 1); require(wcsrchr_nid_postfix(text, u'世') == text + 3); @@ -110,6 +265,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; @@ -180,6 +336,22 @@ int main() { require(wcstoull_nid_postfix(binary, &end, base) == 0 && end == binary + 3); } require(wcstoll_nid_postfix(binary + 2, &end, 16) == 0xb101 && end == binary + 7); + struct SignedCase { + const char16_t* text; + int base; + long long value; + std::ptrdiff_t consumed; + int error; + }; + for (const SignedCase& item : {SignedCase{u"- 1", 10, -1, 3, 0}, SignedCase{u"+\t-1", 10, LLONG_MAX, 4, 34}, SignedCase{u" - -1", 10, LLONG_MIN, 5, 34}, + SignedCase{u"-18446744073709551615", 10, LLONG_MIN, 21, 34}, SignedCase{u"-9223372036854775808", 10, LLONG_MIN, 20, 0}, + SignedCase{u"-9223372036854775809", 10, LLONG_MIN, 20, 34}, SignedCase{u"+-9223372036854775808", 10, LLONG_MAX, 21, 34}, SignedCase{u" -0x", 16, 0, 0, 0}, + SignedCase{u"- ", 10, 0, 0, 0}}) { + for (auto* parse : {wcstol_nid_postfix, wcstoll_nid_postfix, wcstoimax_nid_postfix}) { + *__error_nid_postfix() = 0; + require(parse(item.text, &end, item.base) == item.value && end - item.text == item.consumed && *__error_nid_postfix() == item.error); + } + } const char16_t* scientific = u"3.5e2!"; require(wcstod_nid_postfix(scientific, &end) == 350.0 && end == scientific + 5); @@ -188,12 +360,12 @@ int main() { const char16_t* half = u"-1.5é"; require(wcstold_nid_postfix(half, &end) == -1.5L && end == half + 4); const char16_t* wideNan = u" -nan(0x123)é"; - require(std::bit_cast(wcstod_nid_postfix(wideNan, &end)) == 0xfff8000000000000u && end == wideNan + 12); - require(std::bit_cast(wcstof_nid_postfix(wideNan, &end)) == 0xffc00000u && end == wideNan + 12); + require(std::bit_cast(wcstod_nid_postfix(wideNan, &end)) == 0xfff8000000000123u && end == wideNan + 12); + require(std::bit_cast(wcstof_nid_postfix(wideNan, &end)) == 0x7fc00000u && end == wideNan + 12); const long double wideExtended = wcstold_nid_postfix(wideNan, &end); std::uint64_t significand; std::memcpy(&significand, &wideExtended, sizeof(significand)); - require(std::isnan(wideExtended) && std::signbit(wideExtended) && significand == 0xc000000000000000u && end == wideNan + 12); + require(std::isnan(wideExtended) && !std::signbit(wideExtended) && significand == 0xc000000000000000u && end == wideNan + 12); require(wcscoll_nid_postfix(u"a", u"b") < 0); require(wcscoll_nid_postfix(u"￿", u"a") > 0); @@ -230,5 +402,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/GuestWorkingDirectory.cpp b/core/libs/tests/GuestWorkingDirectory.cpp index df874768e8..de3ea75fc1 100644 --- a/core/libs/tests/GuestWorkingDirectory.cpp +++ b/core/libs/tests/GuestWorkingDirectory.cpp @@ -3,9 +3,11 @@ #include "prx/libc/include/GuestHeap.hpp" #include #include +#include #include #include #include +#include extern "C" { int APS5_VABI chdir_nid_postfix(const char*); char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); @@ -41,6 +43,14 @@ int main() { Require(allocated && std::strcmp(allocated, path) == 0 && allocations == 1); ApplicationHeapFree_nid_no_patch(allocated); Require(releases == 1); + for (const auto size : {std::size_t{1}, std::size_t{2}, std::size_t{1024}, std::numeric_limits::max()}) { + const int before = allocations; + allocated = getcwd_nid_postfix(nullptr, size); + if (!allocated) std::fputs("getcwd(NULL, size) must ignore size when allocating its result\n", stderr); + Require(allocated && std::strcmp(allocated, path) == 0 && allocations == before + 1); + ApplicationHeapFree_nid_no_patch(allocated); + Require(releases == allocations); + } Require(chdir_nid_postfix("sample.txt") == -1 && *__error_nid_postfix() == 20); Require(chdir_nid_postfix("missing") == -1 && *__error_nid_postfix() == 2); Require(chdir_nid_postfix("..") == 0); diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp index 1245c9169c..e893ab95e5 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include @@ -14,7 +16,11 @@ std::int32_t APS5_VABI sceZlibWaitForDone(std::uint64_t*, std::uint32_t*); std::int32_t APS5_VABI sceZlibGetResult(std::uint64_t, std::uint32_t*, std::int32_t*); } -static void Require(bool value) { if (!value) std::abort(); } +static void Require(bool value, const std::source_location& location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "Zlib assertion failed at line %u\n", location.line()); + std::abort(); +} template static void RequireThrows(TAction action) { @@ -33,12 +39,20 @@ int main() { std::int32_t status = 0; Require(sceZlibFinalize() == static_cast(0x81120032)); Require(sceZlibInflate(nullptr, 0, nullptr, 0, nullptr) == static_cast(0x81120032)); - RequireThrows([&] { sceZlibInitialize(nullptr, 1); }); + Require(sceZlibInitialize(nullptr, 1) == static_cast(0x81120016)); + Require(sceZlibInitialize(output, 0) == static_cast(0x81120016)); Require(sceZlibInitialize(nullptr, 0) == 0); Require(sceZlibInitialize(nullptr, 0) == static_cast(0x81120033)); - RequireThrows([&] { sceZlibInflate(compressed.data(), compressed.size(), output + 1, expectedLength, &id); }); - RequireThrows([&] { sceZlibInflate(compressed.data(), compressed.size(), output, 65537, &id); }); - RequireThrows([&] { sceZlibInflate(nullptr, compressed.size(), output, expectedLength, &id); }); + RequireThrows([&] { sceZlibInflate(compressed.data(), compressed.size(), output + 1, 8192, &id); }); + for (const auto capacity : {0u, 1u, 2047u, 2049u, 65535u, 65537u}) { + Require(sceZlibInflate(compressed.data(), compressed.size(), output, capacity, &id) == static_cast(0x81120016)); + Require(id == 0); + } + Require(sceZlibInflate(nullptr, compressed.size(), output, 8192, &id) == static_cast(0x81120016)); + Require(sceZlibWaitForDone(nullptr, nullptr) == static_cast(0x81120016)); + Require(sceZlibGetResult(0, nullptr, &status) == static_cast(0x81120016)); + Require(sceZlibGetResult(0, &produced, nullptr) == static_cast(0x81120016)); + Require(sceZlibGetResult(0, &produced, &status) == static_cast(0x81120002)); std::uint64_t previous = 0; auto decode = [&](const void* input, std::uint32_t inputLength, std::uint32_t capacity, std::int32_t expectedStatus) { storage.fill(0xA5); @@ -46,32 +60,38 @@ int main() { Require(id > previous); previous = id; RequireThrows([&] { sceZlibInflate(input, inputLength, output, capacity, &id); }); - RequireThrows([&] { sceZlibFinalize(); }); std::uint32_t timeout = 0; RequireThrows([&] { sceZlibWaitForDone(&id, &timeout); }); std::uint64_t completed; Require(sceZlibWaitForDone(&completed, nullptr) == 0); Require(completed == id); - RequireThrows([&] { sceZlibGetResult(id + 1, &produced, &status); }); + Require(sceZlibGetResult(id + 1, &produced, &status) == static_cast(0x81120002)); Require(sceZlibGetResult(id, &produced, &status) == 0); + if (status != expectedStatus) std::fprintf(stderr, "input=%u capacity=%u status=%x expected=%x produced=%u\n", inputLength, capacity, static_cast(status), static_cast(expectedStatus), produced); Require(status == expectedStatus && produced <= capacity); RequireThrows([&] { sceZlibGetResult(id, &produced, &status); }); Require(std::all_of(storage.begin(), storage.begin() + 2048, [](auto byte) { return byte == 0xA5; })); Require(std::all_of(storage.begin() + 2048 + capacity, storage.end(), [](auto byte) { return byte == 0xA5; })); }; - decode(compressed.data(), compressed.size(), expectedLength, 0); + decode(compressed.data(), compressed.size(), 8192, 0); Require(produced == expectedLength); for (std::uint32_t offset = 0; offset < produced; offset += sizeof(text) - 1) Require(std::memcmp(output + offset, text, sizeof(text) - 1) == 0); - decode(compressed.data(), compressed.size(), expectedLength - 1, static_cast(0x8112001C)); + decode(compressed.data(), compressed.size(), 2048, static_cast(0x8112001C)); auto corrupt = compressed; corrupt.back() ^= 1; - decode(corrupt.data(), corrupt.size(), expectedLength, static_cast(0x811200FF)); + decode(corrupt.data(), corrupt.size(), 8192, static_cast(0x811200FF)); decode(compressed.data(), compressed.size() - 1, 65536, static_cast(0x811200FF)); + decode(compressed.data(), compressed.size() - 4, 8192, static_cast(0x811200FF)); + decode(compressed.data(), compressed.size() - 1, 8192, static_cast(0x811200FF)); Require(sceZlibFinalize() == 0); Require(sceZlibGetResult(id, &produced, &status) == static_cast(0x81120032)); std::array scratch{}; - Require(sceZlibInitialize(scratch.data(), scratch.size()) == 0); - decode(compressed.data(), compressed.size(), 65536, 0); + RequireThrows([&] { sceZlibInitialize(scratch.data(), scratch.size()); }); + Require(sceZlibInitialize(nullptr, 0) == 0); + Require(sceZlibInflate(compressed.data(), compressed.size(), output, 65536, &id) == 0); + Require(sceZlibFinalize() == 0); + Require(sceZlibInitialize(nullptr, 0) == 0); + Require(sceZlibGetResult(id, &produced, &status) == static_cast(0x81120002)); Require(sceZlibFinalize() == 0); } diff --git a/core/libs/tests/H264Fixture.hpp b/core/libs/tests/H264Fixture.hpp index f63718b676..c9124ada99 100644 --- a/core/libs/tests/H264Fixture.hpp +++ b/core/libs/tests/H264Fixture.hpp @@ -9,6 +9,7 @@ namespace H264Fixture { inline constexpr std::uint32_t Width = 64; inline constexpr std::uint32_t Height = 40; +inline constexpr std::uint32_t CodedHeight = 48; inline constexpr std::array Stream{ 0x00, 0x00, 0x00, 0x01, 0x09, 0x10, 0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x00, 0x0a, 0xac, 0xd9, @@ -107,38 +108,50 @@ inline constexpr std::array Stream{ }; inline constexpr std::array PictureHashes{ - 0x5871fdc85dfa274eull, 0x26ce4afb974cffc4ull, 0xd2e739633733d5baull, - 0x54feb3d290e028fdull, 0xfa1342a4ba1819c8ull, 0x870125dd7f079a4cull, + 0x51b6aa8b48a59d5full, 0x2d097708ed7c0f66ull, 0xdee989a52806e938ull, + 0x4bfd091eacb8679full, 0x290cf60ce8be58bcull, 0x809cb73819503632ull, }; inline constexpr std::array DisplayOrderUnits{0, 2, 3, 1, 5, 4}; -inline std::uint64_t hashNv12(const std::uint8_t* buffer, std::uint32_t pitch) { +inline constexpr std::size_t SpsOffset = 10; +inline constexpr std::array CroppedSps{ + 0x67, 0x64, 0x00, 0x0a, 0xac, 0xd9, 0x44, 0x7f, 0x29, 0x70, 0x11, 0x00, 0x00, + 0x03, 0x00, 0x01, 0x00, 0x00, 0x03, 0x00, 0x3c, 0x0f, 0x12, 0x25, 0x96, +}; + +inline std::vector croppedStream() { + std::vector stream(Stream.begin(), Stream.end()); + for (std::size_t index = 0; index < CroppedSps.size(); ++index) stream[SpsOffset + index] = CroppedSps[index]; + return stream; +} + +inline std::uint64_t hashNv12(const std::uint8_t* buffer, std::uint32_t pitch, std::uint32_t height) { std::uint64_t hash = 0xcbf29ce484222325ull; const auto mix = [&](const std::uint8_t* row) { for (std::uint32_t column = 0; column < Width; ++column) hash = (hash ^ row[column]) * 0x100000001b3ull; }; - for (std::uint32_t row = 0; row < Height; ++row) mix(buffer + static_cast(row) * pitch); - const auto* chroma = buffer + static_cast(pitch) * Height; - for (std::uint32_t row = 0; row < Height / 2; ++row) mix(chroma + static_cast(row) * pitch); + for (std::uint32_t row = 0; row < height; ++row) mix(buffer + static_cast(row) * pitch); + const auto* chroma = buffer + static_cast(pitch) * height; + for (std::uint32_t row = 0; row < height / 2; ++row) mix(chroma + static_cast(row) * pitch); return hash; } -inline std::vector> accessUnits(bool lengthPrefixed) { +inline std::vector> accessUnits(const std::uint8_t* data, std::size_t size, bool lengthPrefixed) { std::vector starts; - for (std::size_t index = 0; index + 4 < Stream.size(); ++index) { - if (Stream[index] == 0 && Stream[index + 1] == 0 && Stream[index + 2] == 0 && Stream[index + 3] == 1 && (Stream[index + 4] & 0x1fu) == 9) starts.push_back(index); + for (std::size_t index = 0; index + 4 < size; ++index) { + if (data[index] == 0 && data[index + 1] == 0 && data[index + 2] == 0 && data[index + 3] == 1 && (data[index + 4] & 0x1fu) == 9) starts.push_back(index); } - starts.push_back(Stream.size()); + starts.push_back(size); std::vector> units; for (std::size_t unit = 0; unit + 1 < starts.size(); ++unit) { if (!lengthPrefixed) { - units.emplace_back(Stream.begin() + static_cast(starts[unit]), Stream.begin() + static_cast(starts[unit + 1])); + units.emplace_back(data + starts[unit], data + starts[unit + 1]); continue; } std::vector nals; for (std::size_t index = starts[unit]; index + 3 < starts[unit + 1]; ++index) { - if (Stream[index] == 0 && Stream[index + 1] == 0 && Stream[index + 2] == 1) { + if (data[index] == 0 && data[index + 1] == 0 && data[index + 2] == 1) { nals.push_back(index + 3); index += 2; } @@ -146,14 +159,18 @@ inline std::vector> accessUnits(bool lengthPrefixed) { std::vector converted; for (std::size_t nal = 0; nal < nals.size(); ++nal) { auto end = nal + 1 < nals.size() ? nals[nal + 1] - 3 : starts[unit + 1]; - while (end > nals[nal] && Stream[end - 1] == 0) --end; + while (end > nals[nal] && data[end - 1] == 0) --end; const auto length = end - nals[nal]; for (int shift = 24; shift >= 0; shift -= 8) converted.push_back(static_cast(length >> shift)); - converted.insert(converted.end(), Stream.begin() + static_cast(nals[nal]), Stream.begin() + static_cast(end)); + converted.insert(converted.end(), data + nals[nal], data + end); } units.push_back(std::move(converted)); } return units; } +inline std::vector> accessUnits(bool lengthPrefixed) { + return accessUnits(Stream.data(), Stream.size(), lengthPrefixed); +} + } diff --git a/core/libs/tests/HevcFixture.hpp b/core/libs/tests/HevcFixture.hpp new file mode 100644 index 0000000000..7bacee3a02 --- /dev/null +++ b/core/libs/tests/HevcFixture.hpp @@ -0,0 +1,133 @@ +#pragma once + +#include +#include +#include +#include + +namespace HevcFixture { + +inline constexpr std::array HevcStream{ + 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, + 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, + 0x95, 0x90, 0x09, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, + 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, 0xa0, 0x20, 0x82, 0x96, 0x59, 0x59, 0x2e, + 0x4c, 0xaf, 0x01, 0x68, 0x08, 0x00, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x03, 0x00, 0xf0, 0x40, + 0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, 0xd0, 0x89, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, + 0x08, 0xe0, 0xbb, 0xcb, 0xc1, 0x71, 0x26, 0x0f, 0xbf, 0x6c, 0xcb, 0x4d, 0x2b, 0x2c, 0xf4, 0x08, + 0xe0, 0x17, 0xeb, 0x16, 0xcc, 0x07, 0xd8, 0xdd, 0x11, 0xf8, 0x5e, 0x73, 0x79, 0x57, 0xcb, 0x69, + 0xa3, 0x1c, 0x9c, 0x43, 0x14, 0xdb, 0x61, 0x0e, 0xc5, 0xc6, 0x9c, 0x9c, 0xf8, 0xf2, 0x07, 0x35, + 0xf1, 0x98, 0x6c, 0x1a, 0x86, 0x53, 0x5a, 0xcd, 0x4d, 0xda, 0xff, 0xfb, 0x01, 0xda, 0xc3, 0x61, + 0xb7, 0x8c, 0xae, 0xc9, 0x76, 0xe4, 0x05, 0xd6, 0xfd, 0xe4, 0xcd, 0x93, 0x2c, 0xb1, 0x41, 0x29, + 0xce, 0x22, 0xf7, 0x77, 0x64, 0x0f, 0x1f, 0xd2, 0xc3, 0x68, 0x32, 0x2f, 0x83, 0xd7, 0x53, 0xbe, + 0x23, 0xff, 0x8c, 0x46, 0x18, 0x33, 0x90, 0x28, 0xa3, 0xef, 0xd7, 0xb1, 0x46, 0x84, 0xa0, 0xc2, + 0xd4, 0xd9, 0xce, 0xdf, 0x0a, 0x05, 0xec, 0xad, 0x63, 0xa4, 0x78, 0x27, 0x80, 0x4e, 0x3b, 0x4c, + 0xa6, 0x42, 0xbc, 0xe3, 0x39, 0x4d, 0xc4, 0xd3, 0x44, 0xf1, 0x22, 0xd0, 0x64, 0x9f, 0xd0, 0x88, + 0x7f, 0x39, 0x6d, 0x06, 0xc7, 0x79, 0x75, 0x2a, 0x44, 0x3d, 0x21, 0x41, 0x53, 0x60, 0x8d, 0x08, + 0x9f, 0xd0, 0x34, 0xa6, 0x72, 0x70, 0x3b, 0xa9, 0xfa, 0x62, 0xb1, 0x19, 0x9b, 0x0b, 0xa2, 0xfc, + 0xb4, 0xb3, 0x2c, 0x58, 0xad, 0xc6, 0xf3, 0xea, 0xc4, 0xd0, 0xf9, 0xea, 0x9e, 0xa7, 0xa4, 0x87, + 0xb8, 0x19, 0x68, 0xf1, 0xc1, 0x6b, 0x87, 0xa5, 0x99, 0xa0, 0x69, 0x24, 0x28, 0x80, 0xba, 0xd2, + 0x57, 0xc6, 0xc0, 0x1e, 0x69, 0x1b, 0xce, 0x96, 0xcd, 0x85, 0x9d, 0x59, 0x32, 0x92, 0x71, 0xe7, + 0x15, 0xf2, 0x9b, 0x71, 0x4b, 0xd4, 0xb6, 0x99, 0xb0, 0x71, 0x9b, 0x9b, 0xdb, 0x33, 0x28, 0x86, + 0x5e, 0x44, 0xf3, 0x5d, 0x5d, 0x7b, 0x87, 0x6c, 0x99, 0x8a, 0x6d, 0xaf, 0xf8, 0xdd, 0x39, 0x14, + 0xc0, 0x10, 0x85, 0xaf, 0x3a, 0x77, 0x60, 0x8b, 0xb7, 0x5f, 0x67, 0x45, 0xb4, 0xa7, 0xe7, 0x44, + 0x77, 0x0a, 0xb9, 0xe1, 0xb2, 0xbe, 0xde, 0x1b, 0x5a, 0xb8, 0xc8, 0x9b, 0x1f, 0x15, 0x37, 0xdc, + 0xeb, 0x55, 0x12, 0x37, 0xa1, 0xab, 0x9c, 0xcd, 0xdd, 0x42, 0x5d, 0x90, 0x89, 0xc5, 0x99, 0x54, + 0x6e, 0x9a, 0xd2, 0x71, 0x07, 0xe3, 0x81, 0x20, 0xad, 0xd4, 0x7c, 0xad, 0x69, 0xaf, 0xc7, 0x2d, + 0x7c, 0x84, 0xf1, 0x5d, 0x43, 0x4a, 0x3c, 0xf6, 0xf8, 0x7e, 0x6a, 0xb2, 0xdd, 0x00, 0xda, 0x30, + 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x30, 0x00, 0x00, 0x01, 0x02, 0x01, 0xd0, 0x19, 0x5f, 0x84, + 0x31, 0x84, 0x50, 0x62, 0xcd, 0x44, 0xa6, 0x4e, 0xbb, 0x2e, 0x87, 0xda, 0x21, 0x25, 0x1c, 0x13, + 0xfe, 0xc2, 0x97, 0xa3, 0xd5, 0xfd, 0xe2, 0x62, 0xd6, 0xe5, 0x50, 0x7a, 0xda, 0x98, 0x8d, 0xcf, + 0xbe, 0x40, 0x5d, 0x69, 0xb6, 0x5f, 0xdc, 0x32, 0x68, 0x05, 0x67, 0x70, 0xfd, 0xa0, 0xd3, 0xcd, + 0x88, 0xeb, 0x12, 0x2e, 0xac, 0xf5, 0x57, 0xa2, 0xc2, 0x81, 0x22, 0x6b, 0x8d, 0x69, 0x6d, 0xac, + 0x41, 0x05, 0x5b, 0x0b, 0x79, 0xe0, 0x86, 0x05, 0x86, 0x8b, 0x02, 0x51, 0x7d, 0x6e, 0x13, 0x63, + 0x36, 0xe6, 0x89, 0x22, 0x75, 0xd9, 0xb0, 0x39, 0x27, 0xcd, 0xac, 0x78, 0xe0, 0x3a, 0x6a, 0x17, + 0x15, 0x05, 0x83, 0x3a, 0xe9, 0x38, 0x7c, 0x75, 0x38, 0xf3, 0xd8, 0x42, 0x57, 0xa1, 0xef, 0x68, + 0xb8, 0xb5, 0x44, 0xfe, 0x4d, 0x66, 0xa1, 0x09, 0xc5, 0x62, 0xff, 0x87, 0x7e, 0xdb, 0x41, 0x6f, + 0x4d, 0x5f, 0x16, 0x49, 0xef, 0xca, 0xda, 0x67, 0x01, 0x29, 0x0b, 0xd6, 0xd8, 0xfc, 0x0b, 0x22, + 0xc8, 0x28, 0xff, 0xb4, 0xb3, 0xa9, 0xf8, 0x9b, 0x78, 0x7d, 0x33, 0xbf, 0xa6, 0x9b, 0x39, 0x64, + 0x57, 0x41, 0x1f, 0x27, 0x05, 0xfc, 0xcd, 0x44, 0xcd, 0xb2, 0xea, 0x79, 0xe0, 0x00, 0x00, 0x00, + 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, 0x02, 0x01, 0xe0, 0x44, 0x97, 0xe1, 0x8f, 0x40, 0x3d, + 0x73, 0xde, 0xdf, 0xf5, 0x1c, 0xff, 0xf6, 0xe2, 0x12, 0x16, 0x3d, 0xb6, 0x75, 0xad, 0x9e, 0xb5, + 0x2b, 0xe7, 0x4a, 0x40, 0xe9, 0x91, 0xd6, 0x86, 0x86, 0x2d, 0xcb, 0xe4, 0xb6, 0x58, 0x4a, 0x2d, + 0x7b, 0x1b, 0xdb, 0xc5, 0x48, 0x2d, 0x09, 0x4d, 0x32, 0x30, 0xde, 0xaa, 0x61, 0xc2, 0x27, 0x64, + 0x54, 0x71, 0x89, 0x32, 0x05, 0xc0, 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, + 0x00, 0x01, 0xe0, 0x24, 0xff, 0xfa, 0x2c, 0x6e, 0xdc, 0x78, 0x3d, 0x08, 0x6d, 0x3b, 0xcf, 0xcc, + 0xb6, 0x16, 0xeb, 0x9d, 0x5f, 0x46, 0x0b, 0x24, 0x0e, 0x97, 0x99, 0x06, 0x00, 0x00, 0x00, 0x01, + 0x46, 0x01, 0x30, 0x00, 0x00, 0x01, 0x02, 0x01, 0xd0, 0x28, 0x95, 0xd7, 0xdc, 0x41, 0xc0, 0x61, + 0x14, 0xd6, 0xee, 0xd4, 0xe8, 0x24, 0x3e, 0x7f, 0x82, 0x22, 0xb0, 0x20, 0xb9, 0xf1, 0x69, 0x9b, + 0xfe, 0xcc, 0xb7, 0x2e, 0xbd, 0xdb, 0x20, 0xfa, 0xe5, 0x97, 0xd9, 0x95, 0x1d, 0x7e, 0x49, 0x35, + 0x69, 0xdb, 0x40, 0xa3, 0x5c, 0x0f, 0x74, 0x6f, 0x00, 0x3d, 0x36, 0x05, 0xc0, 0xc4, 0x37, 0x15, + 0xf9, 0x01, 0x2d, 0xdf, 0xcb, 0x70, 0x3d, 0xfa, 0x47, 0xec, 0x80, 0x07, 0x48, 0x08, 0xc8, 0x54, + 0xd3, 0xe3, 0x00, 0xa3, 0xe4, 0xce, 0x40, 0x05, 0x0b, 0x51, 0x73, 0x56, 0x33, 0x82, 0xa8, 0x0a, + 0xc0, 0xd3, 0xc5, 0xf1, 0xeb, 0x32, 0x8e, 0x4e, 0xa7, 0x6f, 0xb2, 0x64, 0x43, 0x54, 0x71, 0xd4, + 0xaf, 0xc0, 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x50, 0x00, 0x00, 0x01, 0x00, 0x01, 0xe0, 0x82, + 0x2f, 0x5f, 0xdc, 0x63, 0x70, 0x3f, 0xaf, 0x66, 0x75, 0x52, 0xe7, 0x7f, 0x62, 0xfb, 0x84, 0x9c, + 0xef, 0x60, 0x8f, 0xfa, 0x7e, 0x5f, 0x8b, 0x40, 0x15, 0x34, 0x69, 0xe2, 0xc4, 0xfc, 0xff, 0x53, + 0xb8, 0x1e, 0xc7, 0x20, 0x55, 0xdb, 0x5e, 0x05, 0xbd, 0x7a, 0x85, 0xb4, 0xb7, 0xc1, 0x8d, 0xdb, + 0x0f, 0xa6, 0xab, 0xb9, 0x00, 0x2a, 0xdb, 0x83, 0xa0, 0xb0, 0xc5, 0x76, 0x6e, 0xe1, 0xb8, 0x72, + 0xfd, 0x09, 0x61, 0x19, 0xe0, +}; + +inline constexpr std::array HevcMain10Stream{ + 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, + 0xff, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, + 0x95, 0x98, 0x09, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, + 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, 0xa0, 0x20, 0x82, 0x93, 0x65, 0x95, 0x9a, + 0xe4, 0xca, 0xf0, 0x16, 0x80, 0x80, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x0f, 0x04, 0x00, + 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, 0xd0, 0x89, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, 0x08, + 0x60, 0xf2, 0xfb, 0x04, 0xef, 0x67, 0x67, 0xdc, 0xc7, 0x8e, 0xc7, 0x42, 0x63, 0xeb, 0x7d, 0x28, + 0x73, 0x95, 0x14, 0x0c, 0x71, 0x10, 0x8b, 0x65, 0x04, 0xbd, 0x03, 0xe6, 0x04, 0x35, 0xb9, 0x9b, + 0xec, 0x60, 0x2e, 0xea, 0xcf, 0xd0, 0xdd, 0x2d, 0x3a, 0x62, 0x33, 0x9d, 0x7a, 0x74, 0xb0, 0xeb, + 0x0c, 0x06, 0xe6, 0xa5, 0x30, 0xa7, 0xea, 0xed, 0xc2, 0xa5, 0x8e, 0x7c, 0xda, 0x1b, 0x9d, 0xe4, + 0xac, 0x2b, 0x38, 0xdb, 0x9c, 0xe8, 0x51, 0x34, 0x46, 0xd2, 0x04, 0x1a, 0x77, 0x87, 0x7a, 0x19, + 0x70, 0xf9, 0x4f, 0xcb, 0x03, 0x3c, 0x74, 0x25, 0xb9, 0x92, 0x3b, 0x95, 0xe9, 0xd8, 0xaa, 0x9b, + 0x6b, 0x9b, 0x1f, 0x70, 0x25, 0x2a, 0xe7, 0x6c, 0xc3, 0x7e, 0x55, 0x39, 0xf5, 0x06, 0x5a, 0x23, + 0xb1, 0x2e, 0xf6, 0x01, 0xac, 0x71, 0xb9, 0x20, 0xb4, 0xef, 0x1d, 0x42, 0x7e, 0x0e, 0x01, 0x1d, + 0xaf, 0x0a, 0x13, 0x04, 0x8e, 0x03, 0x4d, 0x1e, 0xc4, 0xe0, +}; + +inline constexpr std::array HevcCroppedStream{ + 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, + 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, 0x95, 0x98, 0x09, 0x00, 0x00, 0x00, 0x01, + 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, + 0x00, 0x1e, 0xa0, 0x20, 0x82, 0x9f, 0x79, 0x65, 0x66, 0xab, 0xca, 0xf0, 0x16, 0x80, 0x80, 0x00, + 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x0f, 0x04, 0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, + 0xd1, 0x89, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, 0x08, 0x59, 0xf2, 0xec, 0x80, 0xf9, 0x31, 0xa3, + 0xfa, 0x90, 0xca, 0x97, 0x42, 0xb5, 0x6f, 0xcb, 0xc8, 0xd2, 0x51, 0xa2, 0x3f, 0xe0, 0x0d, 0x82, + 0x6f, 0x1f, 0x37, 0xd4, 0x65, 0x18, 0x78, 0x60, 0x51, 0x3b, 0x98, 0xd0, 0x44, 0xfa, 0xff, 0x0b, + 0xd5, 0xdc, 0x09, 0xea, 0xf7, 0x79, 0xb4, 0xd2, 0xbc, 0x97, 0x53, 0xf2, 0x7d, 0x10, 0x5c, 0xc8, + 0x3a, 0xe0, 0xa7, 0xd8, 0xe2, 0xfc, 0x1d, 0x80, 0x09, 0xcd, 0x31, 0x09, 0xa4, 0xa9, 0x05, 0x5d, + 0x08, 0x74, 0x50, 0xf5, 0xbe, 0xf9, 0xc8, 0xa6, 0xef, 0xe4, 0xff, 0xd6, 0x86, 0x02, 0xb6, 0xa2, + 0x3f, 0x73, 0x80, 0xdb, 0x4e, 0x76, 0xa8, 0xc7, 0xac, 0x4b, 0x5b, 0xbf, 0xe1, 0x88, 0x18, 0xce, + 0x14, 0x5e, 0xf4, 0xb3, 0x57, 0xe6, 0x83, 0xe1, 0x94, 0x8f, 0x55, 0x77, 0xbb, 0x1c, 0x77, 0xd3, + 0x9e, 0xdc, 0xd5, 0x8a, 0x61, 0xb2, 0xf4, 0x77, 0xb8, 0xd1, 0x27, 0x5c, 0x41, 0x75, 0x0a, 0x82, + 0xf5, 0xe8, 0x32, 0x45, 0xae, 0x94, 0x07, 0xcb, 0xde, 0x35, 0x53, 0x3c, 0x0d, 0xf6, 0x5d, 0x1f, + 0x97, 0x0b, 0x04, 0x90, 0x4b, 0xd3, 0xf6, 0x04, 0x58, 0x26, 0xe2, 0x08, 0xe5, 0xb3, 0xa3, 0x05, + 0x5a, 0x8f, 0x6b, 0x03, 0x4b, 0x0a, 0xd4, 0x63, 0x9d, 0x2e, 0xe2, 0x19, 0xa3, 0xde, 0xb5, 0x53, + 0xf2, 0x07, 0xdc, 0xc8, 0xda, 0x2c, 0xfa, 0x2e, 0x62, 0x39, 0xf3, 0xfa, 0x2a, 0x77, 0xc2, 0xb9, + 0x76, 0x63, 0x58, 0xd3, 0xf8, 0xa6, 0xcb, 0x2b, 0xa4, 0xa8, 0xc8, 0x4a, 0xed, 0xbe, 0x8e, 0xf0, + 0xc9, 0x12, 0xf4, 0xb2, 0x91, 0xad, 0xd2, 0x18, 0xcc, 0x0c, 0x4d, 0x56, 0xd9, 0xa3, 0x09, 0x6c, + 0x01, 0x3a, 0xa4, 0xa5, 0x10, 0x98, 0x4d, 0x85, 0x92, 0xa2, 0xfa, 0x50, 0xfe, 0x2d, 0x42, 0xab, + 0xf3, 0x98, 0xe7, 0x16, 0xf1, 0x0b, 0xac, 0x74, 0x5d, 0xcd, 0xfa, 0xe3, 0x75, 0xf3, 0x66, 0x1e, + 0xf0, 0xa3, 0xad, 0x82, 0x8e, 0x25, 0x2c, 0x8f, 0xe1, 0xac, 0xcb, 0xca, 0x0b, 0x19, 0x32, 0x83, + 0x4a, 0xfd, 0x30, 0xc4, 0xe5, 0x97, 0x73, 0xfd, 0x43, 0xd0, +}; + +inline constexpr std::array HevcHashes{ + 0x7291c31ee57c5065ull, 0x37b15adc47810098ull, 0xc84737b65a65b247ull, + 0xe39bc891f8286267ull, 0x734caf1ba961b3c2ull, 0x7deb44bd1ed30531ull, +}; + +inline std::vector> hevcUnits(const std::uint8_t* stream, std::size_t size) { + std::vector starts; + for (std::size_t index = 0; index + 4 < size; ++index) { + if (stream[index] == 0 && stream[index + 1] == 0 && stream[index + 2] == 0 && stream[index + 3] == 1 && ((stream[index + 4] >> 1u) & 0x3fu) == 35) starts.push_back(index); + } + starts.push_back(size); + std::vector> units; + for (std::size_t unit = 0; unit + 1 < starts.size(); ++unit) units.emplace_back(stream + starts[unit], stream + starts[unit + 1]); + return units; +} + +} 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/ImageSampleGather.cpp b/core/libs/tests/ImageSampleGather.cpp index 6fa633e3a7..3d3b40d608 100644 --- a/core/libs/tests/ImageSampleGather.cpp +++ b/core/libs/tests/ImageSampleGather.cpp @@ -72,8 +72,25 @@ static void CheckUnsupportedOpcodeDiagnostic(std::uint32_t encoding, std::string } Require(false); } +static void CheckSampleCompareOffsetAdjust() { + const std::array code{0xf0e00109u, 0x00801e33u}; + const RdnaInstruction instruction = DecodeRdnaMimg(0u, code, 0u); + Require(instruction.op == RdnaOpcode::ImageSample); + Require(instruction.imageOpcodeId == 0xb8u); + Require(instruction.imageSampleFlags == (RdnaImageSampleFlagCompare | RdnaImageSampleFlagOffset | RdnaImageSampleFlagAdjust)); + Require(instruction.imageAddressComponents == 4u); + Require(GetRdnaImageAddressComponentLayout(instruction.imageSampleFlags, 1u).bitOffset == 32u); + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + TranslationContext context(program, block, 256); + context.TranslateInstruction(instruction); + Require(program.Resources().memoryInfo.size() == 1u); + Require(program.Resources().memoryInfo.back().imageSampleFlags == instruction.imageSampleFlags); +} int main() { CheckSamplerVccTuple(); + CheckSampleCompareOffsetAdjust(); CheckSamplerVccTupleOverflow(); Check(0x26u, RdnaOpcode::ImageSampleBCl, 0xfu); Check(0x36u, RdnaOpcode::ImageSampleBClO, 0xfu); diff --git a/core/libs/tests/InterpolationReferenceRange.cpp b/core/libs/tests/InterpolationReferenceRange.cpp new file mode 100644 index 0000000000..594adbd1d2 --- /dev/null +++ b/core/libs/tests/InterpolationReferenceRange.cpp @@ -0,0 +1,55 @@ +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "Optimization/ShaderInfoCollector.hpp" +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +std::string Collect(std::uint32_t attribute, std::uint32_t component, std::uint32_t mode, std::uint32_t inputNum) { + IrProgram program; + program.Resources().stage = IrShaderStage::Pixel; + program.Resources().resourceTrackingComplete = true; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& parameter = builder.Emit(IrOpcode::GetInterpolationParameter, IrType::U32, {&builder.Constant(attribute), &builder.Constant(component), &builder.Constant(mode)}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {¶meter})); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ShaderPixelInputInfo pixel {}; + pixel.inputNum = inputNum; + try { + ShaderInfoCollector().Collect(program, ShaderStageInputInfo {nullptr, &pixel, nullptr}); + } catch (const std::runtime_error& error) { + return error.what(); + } + return {}; +} + +} + +int main() { + int failures = 0; + const auto expect = [&](bool value, const char* name, const std::string& message) { + if (!value) { + std::fprintf(stderr, "%s: %s\n", name, message.c_str()); + ++failures; + } + }; + const auto attribute = Collect(5u, 1u, 0u, 2u); + expect(attribute.find("interpolation parameter reference") == std::string::npos, "an attribute past the pixel inputs becomes a default", attribute); + const auto empty = Collect(0u, 0u, 0u, 0u); + expect(empty.find("interpolation parameter reference") == std::string::npos, "attribute 0 with no SPI inputs becomes a default", empty); + const auto component = Collect(0u, 4u, 2u, 1u); + expect(component.find("(attribute 0 component 4 mode 2, 1 inputs)") != std::string::npos, "a component past w", component); + const auto mode = Collect(1u, 3u, 3u, 3u); + expect(mode.find("(attribute 1 component 3 mode 3, 3 inputs)") != std::string::npos, "a reserved interpolation mode", mode); + const auto inRange = Collect(1u, 3u, 2u, 2u); + expect(inRange.find("interpolation parameter reference") == std::string::npos, "an in-range reference", inRange); + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/tests/JsonCopyFailure.cpp b/core/libs/tests/JsonCopyFailure.cpp new file mode 100644 index 0000000000..6f95b142e3 --- /dev/null +++ b/core/libs/tests/JsonCopyFailure.cpp @@ -0,0 +1,282 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +struct Value { void* node; }; +struct String { void* text; }; +struct Array { void* items; }; +struct Object { void* items; }; +struct Iterator { void* state; }; +struct Pair { String key; std::uint64_t reserved; Value value; }; + +extern "C" { +Iterator* APS5_VABI _ZNK3sce4Json5Array5beginEv(Iterator*, const Array*); +Iterator* APS5_VABI _ZNK3sce4Json6Object5beginEv(Iterator*, const Object*); +void APS5_VABI _ZN3sce4Json5Array8iteratorC1ERKS2_(Iterator*, const Iterator*); +void APS5_VABI _ZN3sce4Json6Object8iteratorC1ERKS2_(Iterator*, const Iterator*); +void APS5_VABI _ZN3sce4Json5Array8iteratorD1Ev(Iterator*); +void APS5_VABI _ZN3sce4Json6Object8iteratorD1Ev(Iterator*); +void APS5_VABI _ZN3sce4Json5Value4swapERS1_(Value*, Value*); +void APS5_VABI _ZN3sce4Json5ValueC1Ev(Value*); +void APS5_VABI _ZN3sce4Json5ValueC1EPKc(Value*, const char*); +void APS5_VABI _ZN3sce4Json5ValueC1ERKNS0_6StringE(Value*, const String*); +void APS5_VABI _ZN3sce4Json5ValueC1ERKNS0_5ArrayE(Value*, const Array*); +void APS5_VABI _ZN3sce4Json5ValueC1ERKNS0_6ObjectE(Value*, const Object*); +void APS5_VABI _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(Value*, std::int32_t); +void APS5_VABI _ZN3sce4Json5ValueC1ERKS1_(Value*, const Value*); +void APS5_VABI _ZN3sce4Json5ValueD1Ev(Value*); +Value* APS5_VABI _ZN3sce4Json5ValueaSERKS1_(Value*, const Value*); +int APS5_VABI _ZN3sce4Json5Value3setERKNS0_5ArrayE(Value*, const Array*); +int APS5_VABI _ZN3sce4Json5Value3setERKNS0_6ObjectE(Value*, const Object*); +int APS5_VABI _ZN3sce4Json5Value3setENS0_9ValueTypeE(Value*, std::int32_t); +std::int32_t APS5_VABI _ZNK3sce4Json5Value7getTypeEv(const Value*); +const Array* APS5_VABI _ZNK3sce4Json5Value8getArrayEv(const Value*); +const Object* APS5_VABI _ZNK3sce4Json5Value9getObjectEv(const Value*); +int APS5_VABI _ZN3sce4Json5Value9serializeERNS0_6StringE(Value*, String*); +int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value*, const char*, std::size_t); +void APS5_VABI _ZN3sce4Json6StringC1Ev(String*); +void APS5_VABI _ZN3sce4Json6StringC1EPKc(String*, const char*); +void APS5_VABI _ZN3sce4Json6StringD1Ev(String*); +const char* APS5_VABI _ZNK3sce4Json6String5c_strEv(const String*); +void APS5_VABI _ZN3sce4Json5ArrayC1Ev(Array*); +void APS5_VABI _ZN3sce4Json5ArrayC1ERKS1_(Array*, const Array*); +void APS5_VABI _ZN3sce4Json5ArrayD1Ev(Array*); +void APS5_VABI _ZN3sce4Json5Array9push_backERKNS0_5ValueE(Array*, const Value*); +std::size_t APS5_VABI _ZNK3sce4Json5Array4sizeEv(const Array*); +const Value* APS5_VABI _ZNK3sce4Json5Array5frontEv(const Array*); +void APS5_VABI _ZN3sce4Json6ObjectC1Ev(Object*); +void APS5_VABI _ZN3sce4Json6ObjectC1ERKS1_(Object*, const Object*); +void APS5_VABI _ZN3sce4Json6ObjectD1Ev(Object*); +Object* APS5_VABI _ZN3sce4Json6ObjectaSERKS1_(Object*, const Object*); +std::size_t APS5_VABI _ZNK3sce4Json6Object4sizeEv(const Object*); +void APS5_VABI _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(Pair*, const String*, const Value*); +void APS5_VABI _ZN3sce4Json6Object4PairD1Ev(Pair*); +} + +static std::ptrdiff_t allocationBudget = -1; +static std::size_t liveAllocations = 0; +static const char* activeCase = nullptr; +static unsigned activeBudget = 0; + +#ifndef _WIN32 +extern "C" void NotImplemented_nid_no_patch(const char* name) { + std::fprintf(stderr, "unexpected unimplemented JSON export: %s\n", name); + std::abort(); +} +#endif + +void* operator new(std::size_t bytes) { + if (allocationBudget == 0) throw std::bad_alloc(); + if (allocationBudget > 0) --allocationBudget; + if (void* pointer = std::malloc(bytes ? bytes : 1)) { + ++liveAllocations; + return pointer; + } + throw std::bad_alloc(); +} +void operator delete(void* pointer) noexcept { + if (pointer) --liveAllocations; + std::free(pointer); +} +void operator delete(void* pointer, std::size_t) noexcept { ::operator delete(pointer); } + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s budget=%u: %s (live=%zu)\n", activeCase, activeBudget, message, liveAllocations); + std::abort(); + } +} + +static void CheckValue(const Value& value, const char* expected) { + String output{}; + _ZN3sce4Json6StringC1Ev(&output); + Require(_ZN3sce4Json5Value9serializeERNS0_6StringE(const_cast(&value), &output) == 0, "serialization failed"); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&output), expected) == 0, "value changed"); + _ZN3sce4Json6StringD1Ev(&output); +} + +enum class Operation { + ValueCopy, ArrayCopy, ObjectCopy, ValueAssign, ObjectAssign, ArrayAppend, + ValueArray, ValueObject, ValueString, ValueText, ValueTypeString, ValueTypeArray, + ValueTypeObject, SetArray, SetObject, SetTypeArray, SetTypeObject, SetTypeString, PairCopy +}; + +static bool Run(Operation operation, const Value& source, const char* expected, const Array& array, const Object& object, const String& text) { + Value result{}; + Array arrayResult{}; + Object objectResult{}; + Pair pair{}; + bool valueReady = false, arrayReady = false, objectReady = false, pairReady = false; + const bool typeSetter = operation == Operation::SetTypeArray || operation == Operation::SetTypeObject || operation == Operation::SetTypeString; + if (operation == Operation::ValueAssign || operation == Operation::SetArray || operation == Operation::SetObject || typeSetter) { + _ZN3sce4Json5ValueC1EPKc(&result, "previous"); + valueReady = true; + } + if (operation == Operation::ArrayAppend) { _ZN3sce4Json5ArrayC1Ev(&arrayResult); arrayReady = true; } + if (operation == Operation::ObjectAssign) { _ZN3sce4Json6ObjectC1Ev(&objectResult); objectReady = true; } + bool succeeded = false; + allocationBudget = activeBudget; + try { + switch (operation) { + case Operation::ValueCopy: _ZN3sce4Json5ValueC1ERKS1_(&result, &source); valueReady = true; break; + case Operation::ArrayCopy: _ZN3sce4Json5ArrayC1ERKS1_(&arrayResult, &array); arrayReady = true; break; + case Operation::ObjectCopy: _ZN3sce4Json6ObjectC1ERKS1_(&objectResult, &object); objectReady = true; break; + case Operation::ValueAssign: _ZN3sce4Json5ValueaSERKS1_(&result, &source); break; + case Operation::ObjectAssign: _ZN3sce4Json6ObjectaSERKS1_(&objectResult, &object); break; + case Operation::ArrayAppend: _ZN3sce4Json5Array9push_backERKNS0_5ValueE(&arrayResult, &source); break; + case Operation::ValueArray: _ZN3sce4Json5ValueC1ERKNS0_5ArrayE(&result, &array); valueReady = true; break; + case Operation::ValueObject: _ZN3sce4Json5ValueC1ERKNS0_6ObjectE(&result, &object); valueReady = true; break; + case Operation::ValueString: _ZN3sce4Json5ValueC1ERKNS0_6StringE(&result, &text); valueReady = true; break; + case Operation::ValueText: _ZN3sce4Json5ValueC1EPKc(&result, _ZNK3sce4Json6String5c_strEv(&text)); valueReady = true; break; + case Operation::ValueTypeString: _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(&result, 5); valueReady = true; break; + case Operation::ValueTypeArray: _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(&result, 6); valueReady = true; break; + case Operation::ValueTypeObject: _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(&result, 7); valueReady = true; break; + case Operation::SetArray: _ZN3sce4Json5Value3setERKNS0_5ArrayE(&result, &array); break; + case Operation::SetObject: _ZN3sce4Json5Value3setERKNS0_6ObjectE(&result, &object); break; + case Operation::SetTypeArray: _ZN3sce4Json5Value3setENS0_9ValueTypeE(&result, 6); break; + case Operation::SetTypeObject: _ZN3sce4Json5Value3setENS0_9ValueTypeE(&result, 7); break; + case Operation::SetTypeString: _ZN3sce4Json5Value3setENS0_9ValueTypeE(&result, 5); break; + case Operation::PairCopy: _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&pair, &text, &source); pairReady = true; break; + } + succeeded = true; + } catch (const std::bad_alloc&) { + } + allocationBudget = -1; + if (valueReady) { + if (succeeded) CheckValue(result, expected); + else { + Require(_ZNK3sce4Json5Value7getTypeEv(&result) == 5, "failed setter changed the destination type"); + CheckValue(result, "\"previous\""); + } + _ZN3sce4Json5ValueD1Ev(&result); + } + if (arrayReady) { + const auto count = _ZNK3sce4Json5Array4sizeEv(&arrayResult); + Require(count == (succeeded ? (operation == Operation::ArrayAppend ? 1 : 3) : 0), "array contents changed"); + if (succeeded && operation == Operation::ArrayAppend) CheckValue(*_ZNK3sce4Json5Array5frontEv(&arrayResult), expected); + _ZN3sce4Json5ArrayD1Ev(&arrayResult); + } + if (objectReady) { + Require(_ZNK3sce4Json6Object4sizeEv(&objectResult) == (succeeded ? 2 : 0), "object contents changed"); + _ZN3sce4Json6ObjectD1Ev(&objectResult); + } + if (pairReady) { CheckValue(pair.value, expected); _ZN3sce4Json6Object4PairD1Ev(&pair); } + return succeeded; +} + +int main() { + constexpr const char* objectJson = "{\"long\":\"a sufficiently long string to require its own allocation\",\"nested\":[{\"leaf\":\"another sufficiently long string\"},[true,null,17]]}"; + constexpr const char* arrayJson = "[1,{\"long\":\"a sufficiently long string to require its own allocation\"},[false,null]]"; + constexpr const char* stringJson = "\"a sufficiently long string to require its own allocation\""; + const auto initial = liveAllocations; + Value objectValue{}, arrayValue{}; + String text{}; + _ZN3sce4Json5ValueC1Ev(&objectValue); + _ZN3sce4Json5ValueC1Ev(&arrayValue); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&objectValue, objectJson, std::strlen(objectJson)) == 0, "prepare object"); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&arrayValue, arrayJson, std::strlen(arrayJson)) == 0, "prepare array"); + _ZN3sce4Json6StringC1EPKc(&text, "a sufficiently long string to require its own allocation"); + const auto* array = _ZNK3sce4Json5Value8getArrayEv(&arrayValue); + const auto* object = _ZNK3sce4Json5Value9getObjectEv(&objectValue); + struct Case { const char* name; Operation operation; const Value* source; const char* expected; }; + const Case cases[] = { + {"value object copy", Operation::ValueCopy, &objectValue, objectJson}, + {"value array copy", Operation::ValueCopy, &arrayValue, arrayJson}, + {"array copy", Operation::ArrayCopy, &arrayValue, arrayJson}, + {"object copy", Operation::ObjectCopy, &objectValue, objectJson}, + {"value assignment", Operation::ValueAssign, &objectValue, objectJson}, + {"object assignment", Operation::ObjectAssign, &objectValue, objectJson}, + {"array append", Operation::ArrayAppend, &objectValue, objectJson}, + {"value from array", Operation::ValueArray, &arrayValue, arrayJson}, + {"value from object", Operation::ValueObject, &objectValue, objectJson}, + {"value from string", Operation::ValueString, &objectValue, stringJson}, + {"value from text", Operation::ValueText, &objectValue, stringJson}, + {"empty string value", Operation::ValueTypeString, &objectValue, "\"\""}, + {"empty array value", Operation::ValueTypeArray, &objectValue, "[]"}, + {"empty object value", Operation::ValueTypeObject, &objectValue, "{}"}, + {"set array", Operation::SetArray, &arrayValue, arrayJson}, + {"set object", Operation::SetObject, &objectValue, objectJson}, + {"set empty array", Operation::SetTypeArray, &objectValue, "[]"}, + {"set empty object", Operation::SetTypeObject, &objectValue, "{}"}, + {"set empty string", Operation::SetTypeString, &objectValue, "\"\""}, + {"pair copy", Operation::PairCopy, &objectValue, objectJson}, + }; + unsigned failures = 0; + for (const auto& test : cases) { + activeCase = test.name; + bool succeeded = false; + for (activeBudget = 0; activeBudget < 256 && !succeeded; ++activeBudget) { + const auto before = liveAllocations; + succeeded = Run(test.operation, *test.source, test.expected, *array, *object, text); + if (liveAllocations != before) std::fprintf(stderr, "expected %zu live allocations, got %zu\n", before, liveAllocations); + Require(liveAllocations == before, "copy leaked allocations"); + failures += !succeeded; + CheckValue(objectValue, objectJson); + CheckValue(arrayValue, arrayJson); + } + Require(succeeded, "copy never succeeded"); + } + for (bool objectIterator : {false, true}) { + activeCase = objectIterator ? "object iterator copy" : "array iterator copy"; + const auto before = liveAllocations; + Iterator source{}; + if (objectIterator) _ZNK3sce4Json6Object5beginEv(&source, object); + else _ZNK3sce4Json5Array5beginEv(&source, array); + for (activeBudget = 0; activeBudget <= 1; ++activeBudget) { + Iterator copy{}; + bool succeeded = false; + allocationBudget = activeBudget; + try { + if (objectIterator) _ZN3sce4Json6Object8iteratorC1ERKS2_(©, &source); + else _ZN3sce4Json5Array8iteratorC1ERKS2_(©, &source); + succeeded = true; + } catch (const std::bad_alloc&) { + ++failures; + } + allocationBudget = -1; + Require(succeeded == (activeBudget == 1), "iterator allocation failure ignored"); + if (succeeded) Require(copy.state != source.state, "iterator copy shares owned storage"); + if (objectIterator) _ZN3sce4Json6Object8iteratorD1Ev(©); + else _ZN3sce4Json5Array8iteratorD1Ev(©); + } + if (objectIterator) _ZN3sce4Json6Object8iteratorD1Ev(&source); + else _ZN3sce4Json5Array8iteratorD1Ev(&source); + Require(liveAllocations == before, "iterator copy leaked allocations"); + } + activeCase = "value swap"; + bool swapSucceeded = false; + for (activeBudget = 0; activeBudget < 256 && !swapSucceeded; ++activeBudget) { + const auto before = liveAllocations; + Value first{}, second{}; + _ZN3sce4Json5ValueC1ERKS1_(&first, &objectValue); + _ZN3sce4Json5ValueC1ERKS1_(&second, &arrayValue); + allocationBudget = activeBudget; + try { + _ZN3sce4Json5Value4swapERS1_(&first, &second); + swapSucceeded = true; + } catch (const std::bad_alloc&) { + ++failures; + } + allocationBudget = -1; + if (swapSucceeded) { + CheckValue(first, arrayJson); + CheckValue(second, objectJson); + } + _ZN3sce4Json5ValueD1Ev(&first); + _ZN3sce4Json5ValueD1Ev(&second); + Require(liveAllocations == before, "swap leaked allocations"); + CheckValue(objectValue, objectJson); + CheckValue(arrayValue, arrayJson); + } + Require(swapSucceeded, "swap never succeeded"); + _ZN3sce4Json6StringD1Ev(&text); + _ZN3sce4Json5ValueD1Ev(&arrayValue); + _ZN3sce4Json5ValueD1Ev(&objectValue); + Require(liveAllocations == initial, "source cleanup leaked allocations"); + std::printf("JSON copy cleanup: %zu operations, %u allocation failures, no leaks\n", sizeof(cases) / sizeof(cases[0]) + 3, failures); +} diff --git a/core/libs/tests/KernelError.cpp b/core/libs/tests/KernelError.cpp new file mode 100644 index 0000000000..9355bd4b64 --- /dev/null +++ b/core/libs/tests/KernelError.cpp @@ -0,0 +1,28 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include +#include +#include +#include + +extern "C" std::int32_t APS5_VABI sceKernelError(std::int32_t); +extern "C" int* APS5_VABI __error_nid_postfix(); + +static void Require(bool value) { if (!value) throw std::runtime_error("Kernel error conversion check failed"); } + +int main() { + *__error_nid_postfix() = 123; + const std::array, 6> cases{{ + {0, 0}, {1, SCE_KERNEL_ERROR_EPERM}, {2, SCE_KERNEL_ERROR_ENOENT}, + {4, SCE_KERNEL_ERROR_EINTR}, {14, SCE_KERNEL_ERROR_EFAULT}, {22, SCE_KERNEL_ERROR_EINVAL} + }}; + for (const auto [input, expected] : cases) { + Require(sceKernelError(input) == expected); + Require(*__error_nid_postfix() == 123); + } + Require(sceKernelError(-1) == static_cast(0x8001ffffu)); + Require(sceKernelError(std::numeric_limits::max()) == 0x1ffff); + Require(sceKernelError(std::numeric_limits::min()) == 0x20000); + Require(*__error_nid_postfix() == 123); +} diff --git a/core/libs/tests/LogMacros.cpp b/core/libs/tests/LogMacros.cpp index 7106aa212d..9217c5e8b5 100644 --- a/core/libs/tests/LogMacros.cpp +++ b/core/libs/tests/LogMacros.cpp @@ -38,7 +38,13 @@ std::vector CaptureWrites(TAction action) { }); #else int sockets[2]; +#ifdef __APPLE__ + Require(socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets) == 0, "message socket pair"); + constexpr int bufferBytes = 1 << 20; + Require(setsockopt(sockets[0], SOL_SOCKET, SO_SNDBUF, &bufferBytes, sizeof(bufferBytes)) == 0 && setsockopt(sockets[1], SOL_SOCKET, SO_RCVBUF, &bufferBytes, sizeof(bufferBytes)) == 0, "message socket buffers"); +#else Require(socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets) == 0, "message socket pair"); +#endif std::FILE* stream = fdopen(sockets[0], "w"); std::thread reader([&] { std::string buffer(1 << 20, '\0'); @@ -65,25 +71,45 @@ std::string Join(const std::vector& messages) { return text; } -bool HasLine(const std::string& text, const std::string& tail) { +bool HasLine(const std::string& text, const std::string& tail, bool timestamped) { const std::string head = "[tests/LogMacros.cpp:"; - return text.size() > head.size() + tail.size() && text.compare(0, head.size(), head) == 0 && text.compare(text.size() - tail.size(), tail.size(), tail) == 0 && text.find('\n') == text.size() - 1; + std::size_t headStart = 0; + if (timestamped) { + const auto timestampEnd = text.find(" s] "); + if (text.rfind("[+", 0) != 0 || timestampEnd == std::string::npos) return false; + const auto dot = text.find('.', 2); + if (dot == std::string::npos || dot > timestampEnd || timestampEnd - dot != 7) return false; + headStart = timestampEnd + 4; + } + return text.size() > headStart + head.size() + tail.size() && text.compare(headStart, head.size(), head) == 0 && text.compare(text.size() - tail.size(), tail.size(), tail) == 0 && text.find('\n') == text.size() - 1; +} + +void SetTimestamps(bool enabled) { +#ifdef _WIN32 + Require(_putenv_s("APS5_LOG_TIMESTAMPS", enabled ? "1" : "") == 0, "timestamp environment"); +#else + Require(enabled ? setenv("APS5_LOG_TIMESTAMPS", "1", 1) == 0 : unsetenv("APS5_LOG_TIMESTAMPS") == 0, "timestamp environment"); +#endif } } -int main() { +int main(int argc, char** argv) { + Require(argc <= 2, "unexpected argument"); + const bool timestamped = argc == 2 && std::string(argv[1]) == "timestamps"; + Require(argc == 1 || timestamped, "unknown argument"); + SetTimestamps(timestamped); const auto shortLine = CaptureWrites([](std::FILE* stream) { Require(_APS5_LOG_IMPL(stream, "value %d of %s", 7, "seven") == 0, "a short line was not flushed"); }); - Require(HasLine(Join(shortLine), "] value 7 of seven\n"), "a short line differs"); + Require(HasLine(Join(shortLine), "] value 7 of seven\n", timestamped), "a short line differs"); Require(shortLine.size() <= 2, "a short line took more than two writes"); const std::string text(20000, 'r'); const auto longLine = CaptureWrites([&](std::FILE* stream) { Require(_APS5_LOG_IMPL(stream, "%s", text.c_str()) == 0, "a long line was not flushed"); }); - Require(HasLine(Join(longLine), "] " + text + "\n"), "a long line differs"); + Require(HasLine(Join(longLine), "] " + text + "\n", timestamped), "a long line differs"); Require(longLine.size() <= 2, "a 20000-character line took more than two writes"); const auto plainLine = CaptureWrites([](std::FILE* stream) { Require(_APS5_LOG_IMPL_NF(stream, "plain") == 0, "a line without arguments was not flushed"); }); - Require(HasLine(Join(plainLine), "] plain\n"), "a line without arguments differs"); + Require(HasLine(Join(plainLine), "] plain\n", timestamped), "a line without arguments differs"); Require(plainLine.size() <= 2, "a line without arguments took more than two writes"); return 0; } diff --git a/core/libs/tests/Mspace.cpp b/core/libs/tests/Mspace.cpp index 386435ded6..0741744299 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,17 @@ 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*); +void* APS5_VABI sceLibcPafMspaceMemalign_nid_postfix(void*, std::size_t, std::size_t, const void*); +int APS5_VABI sceLibcPafMspacePosixMemalign_nid_postfix(void*, void**, std::size_t, std::size_t, const void*); +int APS5_VABI sceLibcPafMspaceMallocStats_nid_postfix(void*, void*); +int APS5_VABI sceLibcPafMspaceMallocStatsFast_nid_postfix(void*, void*); } struct MallocManagedSize { std::uint16_t size; @@ -35,6 +47,49 @@ 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); + auto* aligned = static_cast(sceLibcPafMspaceMemalign_nid_postfix(arena, 128, 50, &owner)); + Require(aligned != nullptr && reinterpret_cast(aligned) % 128 == 0); + Require(sceLibcPafMspaceMemalign_nid_postfix(arena, 0, 50, &owner) == nullptr && *__error_nid_postfix() == 22); + Require(sceLibcPafMspaceMemalign_nid_postfix(arena, 15, 50, &owner) == nullptr && *__error_nid_postfix() == 22); + void* posixAligned = nullptr; + Require(sceLibcPafMspacePosixMemalign_nid_postfix(arena, &posixAligned, 256, 60, &owner) == 0); + Require(posixAligned != nullptr && reinterpret_cast(posixAligned) % 256 == 0); + Require(sceLibcPafMspacePosixMemalign_nid_postfix(arena, nullptr, 256, 60, &owner) == 22); + Require(sceLibcPafMspacePosixMemalign_nid_postfix(arena, &posixAligned, 15, 60, &owner) == 22); + MallocManagedSize viaPaf{}, viaMspace{}, fastPaf{}, fastMspace{}; + viaPaf.size = viaMspace.size = fastPaf.size = fastMspace.size = sizeof(MallocManagedSize); + viaPaf.version = viaMspace.version = fastPaf.version = fastMspace.version = 1; + 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, aligned, &owner); + sceLibcPafMspaceFree_nid_postfix(arena, posixAligned, &owner); + sceLibcPafMspaceFree_nid_postfix(arena, nullptr, &owner); + Require(sceLibcPafMspaceDestroy_nid_postfix(arena) == 0); + Require(sceLibcPafMspaceDestroy_nid_postfix(arena) == -1); +} static void CheckAlignment() { alignas(32) static std::array storage{}; void* arena = sceLibcMspaceCreate_nid_postfix("aligned", storage.data() + 16, 65536, 0); @@ -52,7 +107,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 +157,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 +182,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 +250,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/NetBlockingSend.cpp b/core/libs/tests/NetBlockingSend.cpp new file mode 100644 index 0000000000..466cbc279a --- /dev/null +++ b/core/libs/tests/NetBlockingSend.cpp @@ -0,0 +1,149 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +static int InterceptSend(SOCKET socket, const char* buffer, int length, int flags); +static int InterceptSendto(SOCKET socket, const char* buffer, int length, int flags, const sockaddr* address, int addressLength); +#define send(...) InterceptSend(__VA_ARGS__) +#define sendto(...) InterceptSendto(__VA_ARGS__) +#endif +#include "../prx/libSceNet/Export.cpp" +#ifdef _WIN32 +#undef send +#undef sendto +#endif +#include +#include +#include + +#ifdef _WIN32 +static int InterceptSend(SOCKET socket, const char* buffer, int length, int flags) { + return ::send(socket, buffer, std::min(length, 16384), flags); +} +static int InterceptSendto(SOCKET socket, const char* buffer, int length, int flags, const sockaddr* address, int addressLength) { + return ::sendto(socket, buffer, std::min(length, 16384), flags, address, addressLength); +} +#endif + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "blocking send: %s\n", message); + std::exit(1); +} + +static void SetOption(int descriptor, int option, int value) { + Require(sceNetSetsockopt(descriptor, NET_SOL_SOCKET, option, &value, sizeof(value)) == 0, "set socket option"); +} + +struct Connection { + int listener = sceNetSocket("blocking-listener", 2, 1, 0); + int sender = sceNetSocket("blocking-sender", 2, 1, 0); + int receiver = -1; + + Connection() { + Require(listener >= 0 && sender >= 0, "create TCP sockets"); + SetOption(listener, NET_SO_RCVBUF, 4096); + SetOption(sender, NET_SO_SNDBUF, 4096); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0, "bind listener"); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0, "get listener address"); + Require(sceNetListen(listener, 1) == 0, "listen"); + Require(sceNetConnect(sender, address.data(), size) == 0, "connect"); + receiver = sceNetAccept(listener, nullptr, nullptr); + Require(receiver >= 0, "accept"); + SetOption(receiver, NET_SO_RCVBUF, 1024 * 1024); + SetOption(receiver, 0x1106, 2000000); + } + + ~Connection() { + for (const int descriptor : {receiver, sender, listener}) + Require(sceNetSocketClose(descriptor) == 0, "close socket"); + } +}; + +static std::int64_t Send(std::string_view api, int descriptor, std::vector& bytes, int flags = 0) { + if (api == "send") return sceNetSend(descriptor, bytes.data(), bytes.size(), flags); + std::array address{}; + std::uint32_t size = address.size(); + Require(sceNetGetpeername(descriptor, address.data(), &size) == 0, "get destination address"); + if (api == "sendto") return sceNetSendto(descriptor, bytes.data(), bytes.size(), flags, address.data(), size); + Require(api == "sendmsg", "unknown API"); + std::array vectors{{{bytes.data(), bytes.size() / 2}, + {bytes.data() + bytes.size() / 2, bytes.size() - bytes.size() / 2}}}; + const NetMsghdr message{address.data(), size, vectors.data(), static_cast(vectors.size()), nullptr, 0, 0}; + return sceNetSendmsg(descriptor, &message, flags); +} + +int main(int argc, char** argv) { + Require(argc == 2, "one API argument required"); + const std::string_view api(argv[1]); + std::vector bytes(4 * 1024 * 1024); + for (std::size_t index = 0; index < bytes.size(); ++index) + bytes[index] = static_cast((index * 31 + index / 65536) % 256); + { + Connection connection; + auto sending = std::async(std::launch::async, [&] { return Send(api, connection.sender, bytes); }); + const auto early = sending.wait_for(std::chrono::milliseconds(50)); +#ifdef _WIN32 + if (early == std::future_status::ready) { + std::fprintf(stderr, "early send returned %lld of %zu bytes\n", static_cast(sending.get()), bytes.size()); + Require(false, "send returned before the peer drained its buffer"); + } +#else + static_cast(early); +#endif + std::array buffer{}; + std::size_t received = 0; + while (received < bytes.size()) { + const auto count = sceNetRecv(connection.receiver, buffer.data(), std::min(buffer.size(), bytes.size() - received), 0); + Require(count > 0, "send did not deliver the whole payload"); + Require(std::equal(buffer.begin(), buffer.begin() + count, bytes.begin() + received), "send changed the payload"); + received += static_cast(count); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + Require(sending.wait_for(std::chrono::seconds(2)) == std::future_status::ready, "send did not complete"); + Require(sending.get() == static_cast(bytes.size()), "send returned a short count"); + std::printf("%.*s: sent=%zu, delivered=%zu\n", static_cast(api.size()), api.data(), bytes.size(), received); + } + { + Connection connection; +#ifndef _WIN32 + SetOption(connection.receiver, NET_SO_RCVBUF, 4096); +#endif + SetOption(connection.sender, 0x1105, 100000); + auto started = std::chrono::steady_clock::now(); + const auto partial = Send(api, connection.sender, bytes); + Require(partial > 0 && partial < static_cast(bytes.size()), "timeout must return queued bytes"); + Require(std::chrono::steady_clock::now() - started >= std::chrono::milliseconds(80), "partial send returned before its timeout"); + bool emptyTimeout = false; + for (int attempt = 0; attempt < 64; ++attempt) { + const auto result = Send(api, connection.sender, bytes); + if (result < 0) { + Require(*sceNetErrnoLoc() == NET_EAGAIN, "timeout without progress must return EAGAIN"); + emptyTimeout = true; + break; + } + Require(result > 0 && result < static_cast(bytes.size()), "invalid timed byte count"); + } + Require(emptyTimeout, "peer never exhausted the send buffers"); + } + { + Connection connection; + auto sending = std::async(std::launch::async, [&] { return Send(api, connection.sender, bytes); }); + Require(sending.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, "send did not wait before abort"); + Require(sceNetSocketAbort(connection.sender, 0) == 0, "abort a blocked send"); + Require(sending.wait_for(std::chrono::seconds(2)) == std::future_status::ready, "abort did not wake a blocked send"); + const auto partial = sending.get(); + Require(partial > 0 && partial < static_cast(bytes.size()), "error discarded queued bytes"); + } + { + Connection connection; + SetOption(connection.sender, NET_SO_NBIO, 1); + const auto partial = Send(api, connection.sender, bytes); + Require(partial > 0 && partial <= static_cast(bytes.size()), "invalid guest nonblocking send result"); +#ifdef _WIN32 + Require(partial <= 16384, "guest nonblocking send did not return its first host result"); +#endif + } + std::puts("timeout, abort and guest nonblocking checks passed"); +} diff --git a/core/libs/tests/NetDontWait.cpp b/core/libs/tests/NetDontWait.cpp new file mode 100644 index 0000000000..7507f2fde5 --- /dev/null +++ b/core/libs/tests/NetDontWait.cpp @@ -0,0 +1,239 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct NetIovec { + void* base; + std::uint64_t length; +}; + +struct NetMsghdr { + void* name; + std::uint32_t nameLength; + NetIovec* iov; + int iovLength; + void* control; + std::uint32_t controlLength; + int flags; +}; + +extern "C" { +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetSocketClose(int); +int APS5_VABI sceNetSocketAbort(int, int); +int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); +int APS5_VABI sceNetListen(int, int); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); +std::int64_t APS5_VABI sceNetSend(int, const void*, std::size_t, int); +std::int64_t APS5_VABI sceNetSendto(int, const void*, std::size_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI sceNetSendmsg(int, const NetMsghdr*, int); +std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); +std::int64_t APS5_VABI sceNetRecvfrom(int, void*, std::size_t, int, void*, std::uint32_t*); +std::int64_t APS5_VABI sceNetRecvmsg(int, NetMsghdr*, int); +int* APS5_VABI sceNetErrnoLoc(); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +} + +namespace { + +constexpr int DontWait = 0x80; + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "net dontwait: %s\n", message); + std::exit(1); +} + +void SetOption(int descriptor, int option, int value) { + Require(sceNetSetsockopt(descriptor, 0xffff, option, &value, sizeof(value)) == 0, "set socket option"); +} + +struct Connection { + int listener = sceNetSocket("dontwait-listener", 2, 1, 0); + int sender = sceNetSocket("dontwait-sender", 2, 1, 0); + int receiver = -1; + + Connection() { + Require(listener >= 0 && sender >= 0, "create TCP sockets"); + SetOption(listener, 0x1002, 4096); + SetOption(sender, 0x1001, 4096); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0, "bind listener"); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0, "get listener address"); + Require(sceNetListen(listener, 1) == 0, "listen"); + Require(sceNetConnect(sender, address.data(), size) == 0, "blocking connect"); + receiver = sceNetAccept(listener, nullptr, nullptr); + Require(receiver >= 0, "blocking accept"); + SetOption(receiver, 0x1002, 4096); + SetOption(receiver, 0x1106, 2000000); + } + + ~Connection() { + for (const int descriptor : {receiver, sender, listener}) + Require(sceNetSocketClose(descriptor) == 0, "close socket"); + } + + void Abort() { + sceNetSocketAbort(sender, 0); + sceNetSocketAbort(receiver, 0); + } +}; + +std::int64_t Send(std::string_view api, int descriptor, const std::vector& bytes) { + if (api == "send" || api == "duplex") return sceNetSend(descriptor, bytes.data(), bytes.size(), DontWait); + if (api == "sendto") return sceNetSendto(descriptor, bytes.data(), bytes.size(), DontWait, nullptr, 0); + std::array vectors{{{const_cast(bytes.data()), bytes.size() / 2}, + {const_cast(bytes.data()) + bytes.size() / 2, bytes.size() - bytes.size() / 2}}}; + const NetMsghdr message{nullptr, 0, vectors.data(), static_cast(vectors.size()), nullptr, 0, 0}; + return sceNetSendmsg(descriptor, &message, DontWait); +} + +void CheckSend(std::string_view api) { + Connection connection; + std::vector bytes(16 * 1024 * 1024, 'x'); + std::future reading; + char reply = 0; + if (api == "duplex") { + reading = std::async(std::launch::async, [&] { return sceNetRecv(connection.sender, &reply, 1, 0); }); + Require(reading.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, + "ordinary receive must wait while a DONTWAIT send runs on the same socket"); + } + auto sending = std::async(std::launch::async, [&] { return Send(api, connection.sender, bytes); }); + const bool completed = sending.wait_for(std::chrono::milliseconds(500)) == std::future_status::ready; + if (!completed) connection.Abort(); + const auto sent = sending.get(); + Require(completed, "MSG_DONTWAIT send blocked behind a slow peer or an ordinary receive"); + Require(sent > 0 && sent <= static_cast(bytes.size()), "large send returned an invalid byte count"); + int nonblocking = -1; + std::uint32_t size = sizeof(nonblocking); + Require(sceNetGetsockopt(connection.sender, 0xffff, 0x1200, &nonblocking, &size) == 0 && nonblocking == 0, + "DONTWAIT changed the socket option"); + Require(fcntl_nid_postfix(connection.sender, 3) == 2, "DONTWAIT changed descriptor flags"); + if (reading.valid()) { + Require(reading.wait_for(std::chrono::milliseconds(0)) == std::future_status::timeout, + "DONTWAIT interrupted an ordinary receive"); + const char value = 'r'; + Require(sceNetSend(connection.receiver, &value, 1, 0) == 1, "send reply"); + if (reading.wait_for(std::chrono::seconds(2)) != std::future_status::ready) connection.Abort(); + Require(reading.get() == 1 && reply == value, "ordinary receive did not complete with its reply"); + } + std::int64_t received = 0; + SetOption(connection.receiver, 0x1002, 1024 * 1024); + std::array buffer{}; + while (received < sent) { + const auto count = sceNetRecv(connection.receiver, buffer.data(), + std::min(buffer.size(), static_cast(sent - received)), 0); + Require(count > 0, "reported send bytes were not delivered"); + for (std::int64_t index = 0; index < count; ++index) Require(buffer[index] == 'x', "payload changed"); + received += count; + } + Require(sceNetRecv(connection.receiver, buffer.data(), buffer.size(), DontWait) < 0 && *sceNetErrnoLoc() == 35, + "send left unreported bytes or receive did not return EAGAIN"); + std::printf("%.*s: sent=%lld, delivered=%lld, blocking flags preserved\n", + static_cast(api.size()), api.data(), static_cast(sent), static_cast(received)); +} + +std::pair Receive(std::string_view api, int descriptor) { + char byte = 0; + std::int64_t result; + if (api == "recv") result = sceNetRecv(descriptor, &byte, 1, DontWait); + else if (api == "recvfrom") result = sceNetRecvfrom(descriptor, &byte, 1, DontWait, nullptr, nullptr); + else { + NetIovec vector{&byte, 1}; + NetMsghdr message{nullptr, 0, &vector, 1, nullptr, 0, 0}; + result = sceNetRecvmsg(descriptor, &message, DontWait); + } + if (result == 1) Require(byte == 'x', "received payload changed"); + return {result, *sceNetErrnoLoc()}; +} + +void CheckReceive(std::string_view api) { + Connection connection; + SetOption(connection.receiver, 0x1106, 0); + constexpr int readers = 8; + for (int iteration = 0; iteration < 32; ++iteration) { + const char byte = 'x'; + Require(sceNetSend(connection.sender, &byte, 1, 0) == 1, "send race payload"); + std::barrier start(readers + 1); + std::vector>> receiving; + for (int index = 0; index < readers; ++index) + receiving.push_back(std::async(std::launch::async, [&] { + start.arrive_and_wait(); + return Receive(api, connection.receiver); + })); + start.arrive_and_wait(); + int successes = 0; + for (auto& future : receiving) { + const bool completed = future.wait_for(std::chrono::milliseconds(500)) == std::future_status::ready; + if (!completed) connection.Abort(); + const auto [result, error] = future.get(); + Require(completed, "MSG_DONTWAIT receive blocked after another reader consumed the byte"); + if (result == 1) ++successes; + else Require(result < 0 && error == 35, "empty receive must return EAGAIN"); + } + Require(successes == 1, "one byte must be delivered to exactly one reader"); + } + std::printf("%.*s: 32 races, one successful reader and seven EAGAIN results each\n", + static_cast(api.size()), api.data()); +} + +void CheckBlocking() { + const int listener = sceNetSocket("blocking-listener", 2, 1, 0); + const int sender = sceNetSocket("blocking-sender", 2, 1, 0); + Require(listener >= 0 && sender >= 0, "create blocking sockets"); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0, "bind blocking listener"); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0, "get blocking listener address"); + Require(sceNetListen(listener, 1) == 0, "listen before accept"); + auto accepting = std::async(std::launch::async, [&] { return sceNetAccept(listener, nullptr, nullptr); }); + Require(accepting.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, "accept did not wait for a connection"); + Require(sceNetConnect(sender, address.data(), size) == 0, "connect to waiting accept"); + if (accepting.wait_for(std::chrono::seconds(2)) != std::future_status::ready) sceNetSocketAbort(listener, 0); + const int receiver = accepting.get(); + Require(receiver >= 0, "waiting accept failed"); + SetOption(receiver, 0x1200, 1); + char byte = 0; + Require(sceNetRecv(receiver, &byte, 1, 0) < 0 && *sceNetErrnoLoc() == 35, "socket nonblocking option did not affect receive"); + SetOption(receiver, 0x1200, 0); + auto receiving = std::async(std::launch::async, [&] { return sceNetRecv(receiver, &byte, 1, 0); }); + Require(receiving.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, "blocking receive did not wait"); + Require(sceNetSocketClose(receiver) == 0, "close a socket with a waiting receive"); + Require(receiving.wait_for(std::chrono::seconds(2)) == std::future_status::ready, "close did not wake the receive"); + const auto closedResult = receiving.get(); + std::fprintf(stderr, "closed receive result=%lld\n", static_cast(closedResult)); + Require(closedResult <= 0, "closed receive returned data that was never sent"); + Require(sceNetSocketClose(sender) == 0 && sceNetSocketClose(listener) == 0, "close blocking fixture"); + std::puts("blocking: accept waited, socket mode toggled, close woke the pending receive"); +} + +} + +int main(int argc, char** argv) { + Require(argc == 2, "one API argument required"); + const std::string_view api(argv[1]); + if (api == "blocking") { + CheckBlocking(); + return 0; + } + if (api == "send" || api == "sendto" || api == "sendmsg" || api == "duplex") CheckSend(api); + else { + Require(api == "recv" || api == "recvfrom" || api == "recvmsg", "unknown API"); + CheckReceive(api); + } +} diff --git a/core/libs/tests/NetEpollLifetime.cpp b/core/libs/tests/NetEpollLifetime.cpp new file mode 100644 index 0000000000..47d551e7e1 --- /dev/null +++ b/core/libs/tests/NetEpollLifetime.cpp @@ -0,0 +1,212 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +static int InterceptPoll(WSAPOLLFD* descriptors, ULONG count, INT timeout); +#define WSAPoll InterceptPoll +#else +#include +static int InterceptPoll(pollfd* descriptors, nfds_t count, int timeout); +#define poll InterceptPoll +#endif +#include "../prx/libSceNet/Export.cpp" +#ifdef _WIN32 +#undef WSAPoll +#else +#undef poll +#endif +#include +#include + +static void Require(bool value, const std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "requirement failed at %s:%u\n", location.file_name(), location.line()); + std::abort(); +} + +struct PollGate { + std::promise arrived; + std::future released; + int result = 0; +}; + +static std::atomic pollGate{nullptr}; + +#ifdef _WIN32 +static int InterceptPoll(WSAPOLLFD* descriptors, ULONG count, INT timeout) { + const int result = ::WSAPoll(descriptors, count, timeout); +#else +static int InterceptPoll(pollfd* descriptors, nfds_t count, int timeout) { + const int result = ::poll(descriptors, count, timeout); +#endif + if (auto* gate = pollGate.exchange(nullptr)) { + gate->result = result; + gate->arrived.set_value(); + gate->released.wait(); + } + return result; +} + +class ReuseSlot { +public: +#ifndef _WIN32 + ReuseSlot() : descriptor(::open("/dev/null", O_RDONLY | O_CLOEXEC)) { Require(descriptor >= 0); } + ~ReuseSlot() { if (descriptor >= 0) ::close(descriptor); } + void Release() { Require(::close(descriptor) == 0); descriptor = -1; } +private: + int descriptor; +#else + void Release() {} +#endif +}; + +static constexpr std::uint64_t OldData = 0x123456789abcdef0; +static constexpr std::uint64_t NewData = 0xfedcba9876543210; + +static int Socket() { + const int descriptor = sceNetSocket("epoll-lifetime", 2, 2, 17); + Require(descriptor >= 0); + return descriptor; +} + +static std::array Bind(int descriptor) { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(descriptor, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(descriptor, address.data(), &size) == 0 && size == address.size()); + return address; +} + +static void Send(int sender, const std::array& address) { + const char payload[] = "epoll-lifetime"; + Require(sceNetSendto(sender, payload, sizeof(payload), 0, address.data(), address.size()) == sizeof(payload)); +} + +static void Register(int epoll, int descriptor, int operation, std::uint64_t data, std::uint32_t events = 1) { + NetEpollEvent event{}; + event.events = events; + event.ident = 0xaaaaaaaaaaaaaaaa; + event.reserved = 0x55555555; + event.data.u64 = data; + Require(sceNetEpollControl(epoll, operation, descriptor, &event) == 0); +} + +static void CheckEvent(const NetEpollEvent& event, int descriptor, std::uint64_t data, std::uint32_t events = 1) { + Require(event.ident == static_cast(descriptor)); + Require(event.data.u64 == data && event.events == events && event.reserved == 0); +} + +static void CheckReuse() { + ReuseSlot slot; + const int original = Socket(); + const auto retained = g_socks.at(original).native; + const int sender = Socket(); + std::array epolls{sceNetEpollCreate("reuse-a", 0), sceNetEpollCreate("reuse-b", 0)}; + for (const int epoll : epolls) { + Require(epoll >= 0); + Register(epoll, original, 1, OldData); + } + Require(sceNetSocketClose(original) == 0); + slot.Release(); + const int replacement = Socket(); + Require(replacement == original); + Send(sender, Bind(replacement)); + for (const int epoll : epolls) { + NetEpollEvent event{}; + Require(sceNetEpollWait(epoll, &event, 1, 0) == 0); + Register(epoll, replacement, 1, NewData); + Require(sceNetEpollWait(epoll, &event, 1, 1000000) == 1); + CheckEvent(event, replacement, NewData); + Require(sceNetEpollDestroy(epoll) == 0); + } + Require(sceNetSocketClose(replacement) == 0 && sceNetSocketClose(sender) == 0); +} + +static void CheckControl() { + const int descriptor = Socket(); + const int epoll = sceNetEpollCreate("control", 0); + Require(epoll >= 0); + Require(sceNetEpollControl(epoll, 3, descriptor, nullptr) == static_cast(0x80410102)); + Require(*sceNetErrnoLoc() == 2); + Register(epoll, descriptor, 1, OldData); + Require(sceNetSocketClose(descriptor) == 0); + NetEpollEvent event{}; + event.events = 1; + for (const int closed : {descriptor, -1}) { + for (const int operation : {3, 1, 2}) { + Require(sceNetEpollControl(epoll, operation, closed, operation == 3 ? nullptr : &event) == static_cast(0x80410109)); + Require(*sceNetErrnoLoc() == 9); + } + } + Require(sceNetEpollDestroy(epoll) == 0); +} + +struct WaitResult { + int count = -1; + int error = 0; + NetEpollEvent event{}; +}; + +static void CheckWait(const std::string& operation) { + ReuseSlot slot; + int receiver = Socket(); + const int sender = Socket(); + Send(sender, Bind(receiver)); + const int epoll = sceNetEpollCreate("inflight", 0); + Require(epoll >= 0); + Register(epoll, receiver, 1, OldData); + std::promise release; + PollGate gate{{}, release.get_future()}; + auto arrived = gate.arrived.get_future(); + pollGate.store(&gate); + auto waiter = std::async(std::launch::async, [epoll] { + WaitResult result; + result.count = sceNetEpollWait(epoll, &result.event, 1, 200000); + result.error = *sceNetErrnoLoc(); + return result; + }); + Require(arrived.wait_for(std::chrono::seconds(2)) == std::future_status::ready); + Require(gate.result == 1); + if (operation == "close") { + const int original = receiver; + Require(sceNetSocketClose(original) == 0); + slot.Release(); + receiver = Socket(); + Require(receiver == original); + Send(sender, Bind(receiver)); + Register(epoll, receiver, 1, NewData); + } else if (operation == "delete" || operation == "replace") { + Require(sceNetEpollControl(epoll, 3, receiver, nullptr) == 0); + if (operation == "replace") Register(epoll, receiver, 1, NewData); + } else if (operation == "modify") { + Register(epoll, receiver, 2, NewData, 2); + } else if (operation == "abort") { + Require(sceNetEpollAbort(epoll, 0) == 0); + } else { + Require(operation == "destroy"); + Require(sceNetEpollDestroy(epoll) == 0); + } + release.set_value(); + Require(waiter.wait_for(std::chrono::seconds(2)) == std::future_status::ready); + const auto result = waiter.get(); + if (operation == "delete") { + Require(result.count == 0); + } else if (operation == "abort" || operation == "destroy") { + Require(result.count < 0 && result.error == (operation == "abort" ? 53 : 9)); + } else { + Require(result.count == 1); + CheckEvent(result.event, receiver, NewData, operation == "modify" ? 2 : 1); + } + if (operation != "destroy") Require(sceNetEpollDestroy(epoll) == 0); + Require(sceNetSocketClose(receiver) == 0 && sceNetSocketClose(sender) == 0); +} + +int main(int argc, char** argv) { + Require(argc == 2); + Require(sceNetInit_nid_postfix() == 0); + for (int iteration = 0; iteration != 3; ++iteration) { + if (std::string(argv[1]) == "reuse") CheckReuse(); + else if (std::string(argv[1]) == "control") CheckControl(); + else CheckWait(argv[1]); + } + std::printf("epoll lifetime %s: 3 rounds passed\n", argv[1]); +} diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index 719a0852ff..8ec7173a5a 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -81,6 +81,12 @@ static void CheckUserDefinedParams(const std::filesystem::path& path) { } } +static void CheckContentVersion(const std::filesystem::path& path) { + Require(ParseText(path, R"({"titleId":"PPSA00000","contentVersion":"01.002.000","localizedParameters":{"en-US":{"titleName":"Example"}}})").contentVersion == "01.002.000", "Incorrect content version"); + Require(ParseText(path, R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})").contentVersion.empty(), "A missing content version was not empty"); + Require(ParseText(path, R"({"titleId":"PPSA00000","contentVersion":1,"localizedParameters":{"en-US":{"titleName":"Example"}}})").contentVersion.empty(), "A content version that is not a string was kept"); +} + static void CheckNesting(const std::filesystem::path& path) { const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}},"extra":)"; const auto nested = [&](std::size_t arrays) { @@ -102,11 +108,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; } @@ -225,6 +232,7 @@ int main() { } CheckLanguages(path); CheckUserDefinedParams(path); + CheckContentVersion(path); CheckOutputPermission(path); CheckNesting(path); } catch (const std::exception& error) { diff --git a/core/libs/tests/ParamSfoParser.cpp b/core/libs/tests/ParamSfoParser.cpp new file mode 100644 index 0000000000..d4f9ff1c12 --- /dev/null +++ b/core/libs/tests/ParamSfoParser.cpp @@ -0,0 +1,144 @@ +#include "prx/libkernel/AppMetadata/include/ParamSfoParser.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Entry { + std::string key; + std::uint16_t format; + std::vector data; +}; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +Entry Text(const std::string& key, const std::string& value) { + std::vector data(value.begin(), value.end()); + data.push_back(0); + return {key, 0x0204, data}; +} + +Entry Integer(const std::string& key, std::int32_t value) { + std::vector data(4); + std::memcpy(data.data(), &value, 4); + return {key, 0x0404, data}; +} + +void Put32(std::vector& bytes, std::size_t offset, std::uint32_t value) { + std::memcpy(bytes.data() + offset, &value, 4); +} + +std::vector Build(const std::vector& entries) { + std::vector keys; + std::vector data; + std::vector bytes(20 + entries.size() * 16); + for (std::size_t index = 0; index < entries.size(); ++index) { + const auto entry = 20 + index * 16; + const auto keyOffset = static_cast(keys.size()); + std::memcpy(bytes.data() + entry, &keyOffset, 2); + std::memcpy(bytes.data() + entry + 2, &entries[index].format, 2); + Put32(bytes, entry + 4, static_cast(entries[index].data.size())); + Put32(bytes, entry + 8, static_cast(entries[index].data.size())); + Put32(bytes, entry + 12, static_cast(data.size())); + keys.insert(keys.end(), entries[index].key.begin(), entries[index].key.end()); + keys.push_back(0); + data.insert(data.end(), entries[index].data.begin(), entries[index].data.end()); + } + while (keys.size() % 4 != 0) keys.push_back(0); + std::memcpy(bytes.data(), "\0PSF", 4); + Put32(bytes, 4, 0x0101); + Put32(bytes, 8, static_cast(bytes.size())); + Put32(bytes, 12, static_cast(bytes.size() + keys.size())); + Put32(bytes, 16, static_cast(entries.size())); + bytes.insert(bytes.end(), keys.begin(), keys.end()); + bytes.insert(bytes.end(), data.begin(), data.end()); + return bytes; +} + +ParsedParamJson Parse(const std::filesystem::path& path, const std::vector& bytes) { + { + std::ofstream file(path, std::ios::binary); + file.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + Require(static_cast(file), "Cannot write param.sfo fixture"); + } + return parseParamSfo(path); +} + +void Rejects(const std::filesystem::path& path, const std::vector& bytes, const std::string& expected) { + try { + Parse(path, bytes); + } catch (const std::exception& error) { + Require(std::string(error.what()).find(expected) != std::string::npos, "Unexpected error for " + expected + ": " + error.what()); + return; + } + throw std::runtime_error("Accepted invalid param.sfo: " + expected); +} + +std::vector Valid() { + return {Text("CATEGORY", "gd"), Integer("DOWNLOAD_DATA_SIZE", 64), Text("TITLE", "Example"), Text("TITLE_ID", "CUSA00000"), + Integer("USER_DEFINED_PARAM_1", 23566), Integer("USER_DEFINED_PARAM_3", -7), {"PUBTOOLINFO", 0x0004, {1, 2, 3}}}; +} + +} + +int main() { + const auto path = std::filesystem::temp_directory_path() / ("anyps5-param-sfo-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".sfo"); + int result = 0; + try { + const auto parsed = Parse(path, Build(Valid())); + Require(parsed.title == "Example" && parsed.titleId == "CUSA00000", "Incorrect title metadata"); + Require(parsed.downloadDataSizeMiB == 64, "Incorrect download data size"); + Require(parsed.userDefinedParams[0] == 23566 && parsed.userDefinedParams[1] == 0 && parsed.userDefinedParams[2] == -7 && parsed.userDefinedParams[3] == 0, "Incorrect user defined params"); + + auto entries = Valid(); + entries.erase(entries.begin() + 2); + Rejects(path, Build(entries), "missing TITLE"); + entries = Valid(); + entries.erase(entries.begin() + 3); + Rejects(path, Build(entries), "missing TITLE_ID"); + entries = Valid(); + entries[1] = Integer("DOWNLOAD_DATA_SIZE", -1); + Rejects(path, Build(entries), "DOWNLOAD_DATA_SIZE is negative"); + entries = Valid(); + entries[2].data.back() = 'x'; + Rejects(path, Build(entries), "TITLE is not a terminated string"); + entries = Valid(); + entries[4].data.pop_back(); + Rejects(path, Build(entries), "USER_DEFINED_PARAM_1 is not a 32-bit integer"); + entries = Valid(); + entries.push_back(Text("TITLE", "Again")); + Rejects(path, Build(entries), "repeats TITLE"); + + auto bytes = Build(Valid()); + bytes[1] = 'X'; + Rejects(path, bytes, "no PSF header"); + bytes = Build(Valid()); + Put32(bytes, 16, 1000); + Rejects(path, bytes, "tables are out of bounds"); + bytes = Build(Valid()); + Put32(bytes, 20 + 2 * 16 + 12, 0x10000); + Rejects(path, bytes, "entry is out of bounds"); + bytes = Build(Valid()); + Put32(bytes, 20 + 4, 0x10000); + Rejects(path, bytes, "entry is out of bounds"); + bytes = Build(Valid()); + bytes.resize(bytes.size() - 64); + Rejects(path, bytes, "out of bounds"); + Rejects(path, std::vector(8), "no PSF header"); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + result = 1; + } + std::filesystem::remove(path); + if (result == 0) std::cout << "param.sfo parser tests passed\n"; + return result; +} diff --git a/core/libs/tests/PixelAncillary.cpp b/core/libs/tests/PixelAncillary.cpp index d84f2714a5..c4c02e81fa 100644 --- a/core/libs/tests/PixelAncillary.cpp +++ b/core/libs/tests/PixelAncillary.cpp @@ -14,6 +14,7 @@ static void Require(bool value) { if (!value) throw std::runtime_error("packed p constexpr std::uint32_t UnmodelledBits = 0xE000F0FFu; constexpr std::uint32_t SelectOther = 0x5A3C9E17u; +static bool Iterated = true; struct Row { std::uint32_t sample; @@ -41,6 +42,7 @@ static void RequireField(const IrValue& user, StageInputKind kind, std::uint32_t static IrBlock& Begin(IrProgram& program) { program.Resources().stage = IrShaderStage::Pixel; + program.Metadata().pixelIteratesSamples = Iterated; program.Resources().resourceTrackingComplete = true; auto& block = program.CreateBlock(); program.SetEntryBlock(block); @@ -81,6 +83,7 @@ static void Lower(IrProgram& program) { static void ExtractVector(std::uint32_t offset, std::uint32_t count, StageInputKind kind, std::uint32_t fieldOffset) { IrProgram program; program.Resources().stage = IrShaderStage::Pixel; + program.Metadata().pixelIteratesSamples = Iterated; program.Resources().resourceTrackingComplete = true; auto& block = program.CreateBlock(); program.SetEntryBlock(block); @@ -102,6 +105,7 @@ static void ExtractVector(std::uint32_t offset, std::uint32_t count, StageInputK static void ExtractVectorMaskedCopy(std::uint32_t offset, std::uint32_t count, StageInputKind kind, std::uint32_t fieldOffset) { IrProgram program; program.Resources().stage = IrShaderStage::Pixel; + program.Metadata().pixelIteratesSamples = Iterated; program.Resources().resourceTrackingComplete = true; auto& block = program.CreateBlock(); program.SetEntryBlock(block); @@ -177,7 +181,7 @@ static std::uint32_t Evaluate(const IrValue* value, const Row& row) { throw std::runtime_error("packed pixel ancillary evaluation reached an unsupported value"); } static std::uint32_t PackedWord(const Row& row) { - return UnmodelledBits | (row.sample << 8u) | (row.layerExported ? row.layer << 16u : 0u); + return UnmodelledBits | ((Iterated ? row.sample : 0u) << 8u) | (row.layerExported ? row.layer << 16u : 0u); } static void Values(IrOpcode opcode, std::uint32_t offset, std::uint32_t count, bool throughSelect) { IrProgram program; @@ -443,6 +447,16 @@ static bool BuiltinRemains(const IrProgram& program, StageInputKind kind) { static bool AncillaryRemains(const IrProgram& program) { return BuiltinRemains(program, StageInputKind::PackedAncillary); } +static void PerPixelSampleNumber(bool throughSelect) { + IrProgram program; + auto& user = Build(program, IrOpcode::BitFieldUExtract, 8u, 4u, throughSelect); + Lower(program); + Require(!BuiltinRemains(program, StageInputKind::SampleId)); + for (const Row& row : Rows) { + const std::uint32_t word = throughSelect && !row.frontFacing ? SelectOther : PackedWord(row); + Require(Evaluate(&user, row) == BitField(word, 8u, 4u, false)); + } +} static void ExportedWholeWord() { IrProgram program; auto& entry = Begin(program); @@ -476,12 +490,12 @@ static void ExportedWholeWord() { static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); LowerMasked(program, true); Require(!AncillaryRemains(program) && program.Info().rasterPrimitive); - Require(BuiltinRemains(program, StageInputKind::LayerExported)); + Require(BuiltinRemains(program, StageInputKind::LayerExported) && BuiltinRemains(program, StageInputKind::SampleId) == Iterated); const IrValue* word = carried.Argument(0)->Resolve(); constexpr std::array words {{{0u, 0u, false, RasterPrimitive::Triangle}, {0u, 0u, false, RasterPrimitive::Rectangle}, {0u, 0u, false, RasterPrimitive::Line}, {15u, 0x1FFFu, false, RasterPrimitive::Point}, {3u, 0x7FFu, false, RasterPrimitive::Triangle}, {15u, 0x7FFu, false, RasterPrimitive::Point, true}, {3u, 5u, false, RasterPrimitive::Triangle, true}}}; for (const Row& row : words) { - Require(Evaluate(word, row) == (static_cast(row.primitive) | (row.sample << 8u) | (row.layerExported ? row.layer << 16u : 0u))); + Require(Evaluate(word, row) == (static_cast(row.primitive) | ((Iterated ? row.sample : 0u) << 8u) | (row.layerExported ? row.layer << 16u : 0u))); } } static void WholeWordReplacesRefusal(HelperRead read) { @@ -618,4 +632,12 @@ int main() { StartingQuadRefused(StartingQuad::BallotAfterBranch); StartingQuadRefused(StartingQuad::BallotOfOtherLanes); StartingQuadOutsidePixel(); + Iterated = false; + PerPixelSampleNumber(false); + PerPixelSampleNumber(true); + Values(IrOpcode::BitFieldUExtract, 8u, 4u, false); + Values(IrOpcode::BitFieldUExtract, 16u, 13u, false); + MergedValues(IrOpcode::BitFieldUExtract, 8u, 4u, false); + ExtractVector(16u, 11u, StageInputKind::Layer, 0u); + ExportedWholeWord(); } diff --git a/core/libs/tests/PreciseWait.cpp b/core/libs/tests/PreciseWait.cpp index 4089cb4abb..d9120b173c 100644 --- a/core/libs/tests/PreciseWait.cpp +++ b/core/libs/tests/PreciseWait.cpp @@ -1,14 +1,187 @@ #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; + } + + PreciseSleepUntil(std::chrono::steady_clock::time_point{}); + const auto coarseDeadline = std::chrono::ceil( + std::chrono::steady_clock::now() + std::chrono::microseconds(25)); + PreciseSleepUntil(coarseDeadline); + if (std::chrono::steady_clock::now() < coarseDeadline) { + std::cerr << "PreciseSleepUntil returned before coarse steady_clock 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/SaveDataFileWriteFailure.cpp b/core/libs/tests/SaveDataFileWriteFailure.cpp index d9046fb753..933392c62a 100644 --- a/core/libs/tests/SaveDataFileWriteFailure.cpp +++ b/core/libs/tests/SaveDataFileWriteFailure.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -17,46 +18,160 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) +static std::vector ReadFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + Require(file.is_open()); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +static void WriteFile(const std::filesystem::path& path, const std::vector& data) { + std::ofstream file(path, std::ios::binary); + Require(file.is_open()); + file.write(data.data(), static_cast(data.size())); + Require(static_cast(file)); +} + +static bool ReplaceFile(const std::filesystem::path& temporary, const std::filesystem::path& destination) { +#ifdef _WIN32 + return MoveFileExW(temporary.c_str(), destination.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0; +#else + std::error_code error; + std::filesystem::rename(temporary, destination, error); + return !error; +#endif +} + int main() { const auto root = std::filesystem::temp_directory_path() / ("anyps5-savedata-write-failure-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); - const auto savePath = root / "memory.dat"; + const auto savePath = root / std::filesystem::path(u8"m\u00e9moire-\u65e5\u672c.dat"); const std::vector oldData{'o', 'l', 'd'}; const std::vector replacement{'n', 'e', 'w'}; - { - std::ofstream file(savePath, std::ios::binary); - Require(file.is_open()); - file.write(oldData.data(), static_cast(oldData.size())); - Require(static_cast(file)); - } + WriteFile(savePath, oldData); int replaceAttempts = 0; + std::filesystem::path failedTemporary; const bool replaced = savedata::replace_file_with( savePath, replacement.data(), replacement.size(), - [&replaceAttempts, &savePath](const std::filesystem::path& temporary, - const std::filesystem::path& destination) { + [&](const std::filesystem::path& temporary, const std::filesystem::path& destination) { ++replaceAttempts; Require(destination == savePath); - auto expectedTemporary = savePath; - expectedTemporary += ".tmp"; - Require(temporary == expectedTemporary); + Require(temporary != destination && temporary.parent_path() == destination.parent_path()); + Require(ReadFile(temporary) == replacement); + failedTemporary = temporary; return false; }); Require(!replaced); Require(replaceAttempts == 1); - std::vector savedData; - { - std::ifstream savedFile(savePath, std::ios::binary); - savedData.assign(std::istreambuf_iterator(savedFile), std::istreambuf_iterator()); - } - Require(savedData == oldData); + Require(ReadFile(savePath) == oldData); + Require(!std::filesystem::exists(failedTemporary)); + auto temporary = savePath; temporary += ".tmp"; - Require(!std::filesystem::exists(temporary)); + WriteFile(temporary, oldData); + Require(savedata::replace_file(savePath, replacement.data(), replacement.size())); + Require(ReadFile(savePath) == replacement); + Require(std::filesystem::exists(temporary)); + Require(ReadFile(temporary) == oldData); + std::filesystem::remove(temporary); + + const auto unrelated = root / "unrelated.dat"; + WriteFile(unrelated, oldData); + std::filesystem::create_hard_link(unrelated, temporary); + Require(savedata::replace_file(savePath, replacement.data(), replacement.size())); + Require(ReadFile(unrelated) == oldData); + Require(std::filesystem::equivalent(unrelated, temporary)); + Require(!std::filesystem::equivalent(unrelated, savePath)); + std::filesystem::remove(temporary); + + std::error_code symlinkError; +#ifdef _WIN32 + if (!CreateSymbolicLinkW(temporary.c_str(), unrelated.c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE)) { + symlinkError = std::error_code(GetLastError(), std::system_category()); + } +#else + std::filesystem::create_symlink(unrelated, temporary, symlinkError); +#endif +#ifdef _WIN32 + if (symlinkError == std::errc::permission_denied || + symlinkError == std::error_code(ERROR_PRIVILEGE_NOT_HELD, std::system_category())) { + std::printf("Symlink case skipped: creating Windows symlinks requires developer mode or privilege\n"); + } else +#endif + { + Require(!symlinkError); + Require(savedata::replace_file(savePath, replacement.data(), replacement.size())); + Require(ReadFile(unrelated) == oldData); +#ifdef _WIN32 + const auto linkAttributes = GetFileAttributesW(temporary.c_str()); + const auto saveAttributes = GetFileAttributesW(savePath.c_str()); + Require(linkAttributes != INVALID_FILE_ATTRIBUTES && (linkAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + Require(saveAttributes != INVALID_FILE_ATTRIBUTES && (saveAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0); +#else + Require(std::filesystem::is_symlink(temporary)); + Require(!std::filesystem::is_symlink(savePath)); +#endif + Require(ReadFile(savePath) == replacement); + std::filesystem::remove(temporary); + } + + Require(std::filesystem::create_directory(temporary)); + Require(savedata::replace_file(savePath, replacement.data(), replacement.size())); + Require(std::filesystem::is_directory(temporary)); + std::filesystem::remove(temporary); + + std::filesystem::path outerTemporary; + Require(savedata::replace_file_with(savePath, replacement.data(), replacement.size(), + [&](const auto& outer, const auto& destination) { + outerTemporary = outer; + Require(savedata::replace_file_with(savePath, oldData.data(), oldData.size(), + [&](const auto& inner, const auto& innerDestination) { + Require(inner != outer); + Require(ReadFile(outer) == replacement); + Require(ReadFile(inner) == oldData); + return ReplaceFile(inner, innerDestination); + })); + Require(ReadFile(destination) == oldData); + Require(ReadFile(outer) == replacement); + return ReplaceFile(outer, destination); + })); + Require(ReadFile(savePath) == replacement); + Require(!std::filesystem::exists(outerTemporary)); + + Require(savedata::replace_file_with(savePath, replacement.data(), replacement.size(), + [&](const auto& created, const auto& destination) { + Require(ReplaceFile(created, destination)); + outerTemporary = created; + WriteFile(created, oldData); + return true; + })); + Require(ReadFile(outerTemporary) == oldData); + std::filesystem::remove(outerTemporary); + + std::filesystem::path thrownTemporary; + bool threw = false; + try { + savedata::replace_file_with(savePath, oldData.data(), oldData.size(), + [&](const auto& created, const auto&) -> bool { + thrownTemporary = created; + throw std::runtime_error("injected replacement failure"); + }); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && !thrownTemporary.empty()); + Require(!std::filesystem::exists(thrownTemporary)); + Require(ReadFile(savePath) == replacement); + + Require(!savedata::replace_file(savePath, nullptr, 1)); + Require(ReadFile(savePath) == replacement); + Require(savedata::replace_file(savePath, nullptr, 0)); + Require(ReadFile(savePath).empty()); + Require(std::distance(std::filesystem::directory_iterator(root), std::filesystem::directory_iterator{}) == 2); std::filesystem::remove_all(root); } diff --git a/core/libs/tests/SaveDataMemoryJournal.cpp b/core/libs/tests/SaveDataMemoryJournal.cpp new file mode 100644 index 0000000000..e661a1fd1f --- /dev/null +++ b/core/libs/tests/SaveDataMemoryJournal.cpp @@ -0,0 +1,121 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceSaveDataInitialize3(const void*); +int APS5_VABI sceSaveDataTerminate(); +int APS5_VABI sceSaveDataGetSaveDataMemory2(SaveDataMemoryGet2*); +int APS5_VABI sceSaveDataSetSaveDataMemory2(const SaveDataMemorySet2*); +int APS5_VABI sceSaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2*, SaveDataMemorySetupResult*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Save-data memory journal check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static std::vector Read(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + Require(file.is_open()); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +static void Write(const std::filesystem::path& path, const std::vector& bytes) { + std::ofstream file(path, std::ios::binary | std::ios::trunc); + file.write(bytes.data(), static_cast(bytes.size())); + Require(static_cast(file)); +} + +static std::vector Journal(std::uint64_t offset, const std::string& text) { + std::vector journal{'A', 'P', 'S', '5', 'M', 'J', 'N', '1'}; + const std::uint64_t size = text.size(); + const std::size_t at = journal.size(); + journal.resize(at + sizeof(offset) + sizeof(size) + text.size()); + std::memcpy(journal.data() + at, &offset, sizeof(offset)); + std::memcpy(journal.data() + at + sizeof(offset), &size, sizeof(size)); + std::memcpy(journal.data() + at + sizeof(offset) + sizeof(size), text.data(), text.size()); + return journal; +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-memory-journal-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + std::filesystem::current_path(root); + constexpr std::int32_t userId = 7531; + const auto memoryPath = std::filesystem::path("_sd_mem/u7531/slot0.bin"); + const auto journalPath = std::filesystem::path("_sd_mem/u7531/slot0.journal"); + const auto temporaryPath = std::filesystem::path("_sd_mem/u7531/slot0.bin.tmp"); + const auto aliasPath = std::filesystem::path("alias.bin"); + + Require(sceSaveDataInitialize3(nullptr) == 0); + SaveDataMemorySetup2 setup{}; + setup.user_id = userId; + setup.memory_size = 64; + SaveDataMemorySetupResult result{}; + Require(sceSaveDataSetupSaveDataMemory2(&setup, &result) == 0); + std::vector expected(64, 0); + Require(Read(memoryPath) == expected); + std::filesystem::create_hard_link(memoryPath, aliasPath); + + std::array first{'a', 'b', 'c', 'd'}; + std::array second{'x', 'y', 'z'}; + std::array data{{{first.data(), first.size(), 8}, {second.data(), second.size(), 61}}}; + SaveDataMemorySet2 set{}; + set.user_id = userId; + set.data = data.data(); + set.data_num = static_cast(data.size()); + Require(sceSaveDataSetSaveDataMemory2(&set) == 0); + std::copy(first.begin(), first.end(), expected.begin() + 8); + std::copy(second.begin(), second.end(), expected.begin() + 61); + Require(Read(memoryPath) == expected); + Require(Read(aliasPath) == expected); + Require(!std::filesystem::exists(journalPath)); + Require(!std::filesystem::exists(temporaryPath)); + + Require(std::filesystem::create_directory(temporaryPath)); + first.fill('q'); + Require(sceSaveDataSetSaveDataMemory2(&set) == 0); + std::copy(first.begin(), first.end(), expected.begin() + 8); + Require(Read(memoryPath) == expected && Read(aliasPath) == expected); + Require(!std::filesystem::exists(journalPath)); + Require(std::filesystem::is_directory(temporaryPath)); + Require(std::filesystem::remove(temporaryPath)); + const auto temporaryPrefix = temporaryPath.filename().string(); + for (const auto& entry : std::filesystem::directory_iterator(memoryPath.parent_path())) { + Require(!entry.path().filename().string().starts_with(temporaryPrefix)); + } + + Write(journalPath, Journal(16, "jour")); + std::array got{}; + SaveDataMemoryData range{got.data(), got.size(), 16}; + SaveDataMemoryGet2 get{}; + get.user_id = userId; + get.data = ⦥ + Require(sceSaveDataGetSaveDataMemory2(&get) == 0); + Require(std::string(got.data(), got.size()) == "jour"); + std::copy_n("jour", 4, expected.begin() + 16); + Require(Read(memoryPath) == expected); + Require(Read(aliasPath) == expected); + Require(!std::filesystem::exists(journalPath)); + + Require(sceSaveDataTerminate() == 0); + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); +} diff --git a/core/libs/tests/SaveDataMemoryMetadata.cpp b/core/libs/tests/SaveDataMemoryMetadata.cpp index 8f40cccf5c..b3acb20559 100644 --- a/core/libs/tests/SaveDataMemoryMetadata.cpp +++ b/core/libs/tests/SaveDataMemoryMetadata.cpp @@ -2,15 +2,25 @@ #include #include #include +#include #include #include +#include #include #include #include #include #include #include -#ifndef _WIN32 +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else #include #include #endif @@ -18,10 +28,17 @@ 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 sceSaveDataSyncSaveDataMemory(const void*); 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); @@ -36,7 +53,54 @@ static std::vector Read(const std::filesystem::path& path) { return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; } +static void RequireNoTemporary(const std::filesystem::path& path) { + const auto prefix = path.filename().string() + ".tmp"; + for (const auto& entry : std::filesystem::directory_iterator(path.parent_path())) { + Require(!entry.path().filename().string().starts_with(prefix)); + } +} + +template +static int BlockDestination(const std::filesystem::path& path, Operation&& operation) { + Require(std::filesystem::create_directory(path)); + const int result = operation(); + Require(std::filesystem::is_directory(path)); + Require(std::filesystem::remove(path)); + RequireNoTemporary(path); + return result; +} + +template +static int FailWrite(const std::filesystem::path& path, [[maybe_unused]] std::size_t sizeLimit, + Operation&& operation) { +#ifdef _WIN32 + const auto locked = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + Require(locked != INVALID_HANDLE_VALUE); + const int result = operation(); + Require(CloseHandle(locked) != 0); +#else + rlimit previousLimit{}; + Require(getrlimit(RLIMIT_FSIZE, &previousLimit) == 0); + auto writeLimit = previousLimit; + writeLimit.rlim_cur = sizeLimit; + const auto previousHandler = std::signal(SIGXFSZ, SIG_IGN); + Require(previousHandler != SIG_ERR); + Require(setrlimit(RLIMIT_FSIZE, &writeLimit) == 0); + const int result = operation(); + Require(setrlimit(RLIMIT_FSIZE, &previousLimit) == 0); + Require(std::signal(SIGXFSZ, previousHandler) != SIG_ERR); +#endif + RequireNoTemporary(path); + return result; +} + int main() { + Require(sceSaveDataCreateTransactionResource(1024) == kSaveDataNotInitialized); + Require(sceSaveDataDeleteTransactionResource(1) == kSaveDataNotInitialized); + const std::uint32_t syncParamUninit[2] = {7531u, 0u}; + Require(sceSaveDataSyncSaveDataMemory(nullptr) == static_cast(0x809F0000u)); + Require(sceSaveDataSyncSaveDataMemory(syncParamUninit) == static_cast(0x809F0001u)); 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())); @@ -52,6 +116,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{}; @@ -62,12 +132,9 @@ int main() { const auto original = Read(path); Require(original.size() == sizeof(param)); param.user_param = 42; - const auto temporary = path.string() + ".tmp"; - Require(std::filesystem::create_directory(temporary)); - const int status = sceSaveDataSetSaveDataMemory2(&set); + const int status = FailWrite(path, 16, [&] { return sceSaveDataSetSaveDataMemory2(&set); }); Require(Read(path) == original); Require(status == static_cast(0x809F000Bu)); - Require(!std::filesystem::exists(temporary)); Require(sceSaveDataSetSaveDataMemory2(&set) == 0); const auto* bytes = reinterpret_cast(¶m); Require(Read(path) == std::vector(bytes, bytes + sizeof(param))); @@ -86,26 +153,9 @@ int main() { Require(sceSaveDataSetSaveDataMemory2(&set) == static_cast(0x809F0000u)); Require(Read(memoryPath) == memoryOriginal); data[1].offset = 24; - const auto memoryTemporary = memoryPath.string() + ".tmp"; - Require(std::filesystem::create_directory(memoryTemporary)); - Require(sceSaveDataSetSaveDataMemory2(&set) == static_cast(0x809F000Bu)); - Require(Read(memoryPath) == memoryOriginal); - Require(!std::filesystem::exists(memoryTemporary)); -#ifndef _WIN32 - rlimit previousLimit{}; - Require(getrlimit(RLIMIT_FSIZE, &previousLimit) == 0); - auto writeLimit = previousLimit; - writeLimit.rlim_cur = 16; - const auto previousHandler = std::signal(SIGXFSZ, SIG_IGN); - Require(previousHandler != SIG_ERR); - Require(setrlimit(RLIMIT_FSIZE, &writeLimit) == 0); - const int writeStatus = sceSaveDataSetSaveDataMemory2(&set); - Require(setrlimit(RLIMIT_FSIZE, &previousLimit) == 0); - Require(std::signal(SIGXFSZ, previousHandler) != SIG_ERR); + const int writeStatus = FailWrite(memoryPath, 16, [&] { return sceSaveDataSetSaveDataMemory2(&set); }); Require(writeStatus == static_cast(0x809F000Bu)); Require(Read(memoryPath) == memoryOriginal); - Require(!std::filesystem::exists(memoryTemporary)); -#endif Require(sceSaveDataSetSaveDataMemory2(&set) == 0); auto memoryExpected = memoryOriginal; std::copy(first.begin(), first.end(), memoryExpected.begin() + 4); @@ -116,11 +166,27 @@ int main() { Require(sceSaveDataSetSaveDataMemory2(&set) == 0); std::copy(first.begin(), first.end(), memoryExpected.begin() + 4); Require(Read(memoryPath) == memoryExpected); + std::array third{'p', 'q', 'r', 's'}; + std::array fourth{'t', 'u', 'v', 'w'}; + constexpr std::size_t GuestMemoryDataSize = 64; + alignas(SaveDataMemoryData) std::array guestData{}; + const auto putGuestData = [&guestData](std::size_t index, const char* buffer, std::uint64_t size, std::uint64_t offset) { + unsigned char* entry = guestData.data() + index * GuestMemoryDataSize; + std::memcpy(entry, &buffer, sizeof(buffer)); + std::memcpy(entry + 8, &size, sizeof(size)); + std::memcpy(entry + 16, &offset, sizeof(offset)); + }; + putGuestData(0, third.data(), third.size(), 0); + putGuestData(1, fourth.data(), fourth.size(), 16); + set.data = reinterpret_cast(guestData.data()); + set.data_num = 2; + Require(sceSaveDataSetSaveDataMemory2(&set) == 0); + std::copy(third.begin(), third.end(), memoryExpected.begin()); + std::copy(fourth.begin(), fourth.end(), memoryExpected.begin() + 16); + Require(Read(memoryPath) == memoryExpected); const auto setupParamPath = std::filesystem::path("_sd_mem/u42/slot0.param"); const auto setupBinPath = std::filesystem::path("_sd_mem/u42/slot0.bin"); - const auto setupParamTemp = setupParamPath.string() + ".tmp"; Require(std::filesystem::create_directories(setupParamPath.parent_path())); - Require(std::filesystem::create_directory(setupParamTemp)); SaveDataParam setupParam{}; setupParam.user_param = 100; SaveDataMemorySetup2 setup2{}; @@ -131,11 +197,10 @@ int main() { setup2.init_param = &setupParam; SaveDataMemorySetupResult setupResult{}; setupResult.existed_memory_size = 555; - Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(BlockDestination(setupParamPath, [&] { return sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult); }) == static_cast(0x809F000Bu)); Require(setupResult.existed_memory_size == 555); Require(!std::filesystem::exists(setupBinPath)); Require(!std::filesystem::exists(setupParamPath)); - Require(!std::filesystem::exists(setupParamTemp)); Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); Require(setupResult.existed_memory_size == 0); Require(Read(setupBinPath) == std::vector(128, 0)); @@ -150,18 +215,15 @@ int main() { seededFile.write(seededBin.data(), static_cast(seededBin.size())); Require(static_cast(seededFile)); } - const auto growthParamTemp = setupParamPath.string() + ".tmp"; - Require(std::filesystem::create_directory(growthParamTemp)); SaveDataParam previousParam = setupParam; setupParam.user_param = 200; setup2.memory_size = 256; setupResult.existed_memory_size = 999; - Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(FailWrite(setupParamPath, 16, [&] { return sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult); }) == static_cast(0x809F000Bu)); Require(setupResult.existed_memory_size == 999); Require(Read(setupBinPath) == seededBin); const auto* previousBytes = reinterpret_cast(&previousParam); Require(Read(setupParamPath) == std::vector(previousBytes, previousBytes + sizeof(previousParam))); - Require(!std::filesystem::exists(growthParamTemp)); Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); Require(setupResult.existed_memory_size == 128); std::vector expectedGrownBin = seededBin; @@ -169,28 +231,24 @@ int main() { Require(Read(setupBinPath) == expectedGrownBin); const auto* updatedBytes = reinterpret_cast(&setupParam); Require(Read(setupParamPath) == std::vector(updatedBytes, updatedBytes + sizeof(setupParam))); - const auto growthBinTemp = setupBinPath.string() + ".tmp"; - Require(std::filesystem::create_directory(growthBinTemp)); setupParam.user_param = 300; - setup2.memory_size = 512; + constexpr std::size_t GrownMemorySize = 2 * sizeof(SaveDataParam); + setup2.memory_size = GrownMemorySize; setupResult.existed_memory_size = 888; - Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(FailWrite(setupBinPath, sizeof(SaveDataParam), [&] { return sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult); }) == static_cast(0x809F000Bu)); Require(setupResult.existed_memory_size == 888); Require(Read(setupBinPath) == expectedGrownBin); const auto* param300Bytes = reinterpret_cast(&setupParam); Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); - Require(!std::filesystem::exists(growthBinTemp)); Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); Require(setupResult.existed_memory_size == 256); - std::vector expectedBlob512 = expectedGrownBin; - expectedBlob512.resize(512, 0); - Require(Read(setupBinPath) == expectedBlob512); + std::vector expectedFinalBlob = expectedGrownBin; + expectedFinalBlob.resize(GrownMemorySize, 0); + Require(Read(setupBinPath) == expectedFinalBlob); Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); const auto freshFailBinPath = std::filesystem::path("_sd_mem/u55/slot0.bin"); const auto freshFailParamPath = std::filesystem::path("_sd_mem/u55/slot0.param"); Require(std::filesystem::create_directories(freshFailBinPath.parent_path())); - const auto freshFailBinTemp = freshFailBinPath.string() + ".tmp"; - Require(std::filesystem::create_directory(freshFailBinTemp)); SaveDataParam freshFailParam{}; freshFailParam.user_param = 55; SaveDataMemorySetup2 freshFailSetup{}; @@ -201,12 +259,11 @@ int main() { freshFailSetup.init_param = &freshFailParam; SaveDataMemorySetupResult freshFailResult{}; freshFailResult.existed_memory_size = 777; - Require(sceSaveDataSetupSaveDataMemory2(&freshFailSetup, &freshFailResult) == static_cast(0x809F000Bu)); + Require(BlockDestination(freshFailBinPath, [&] { return sceSaveDataSetupSaveDataMemory2(&freshFailSetup, &freshFailResult); }) == static_cast(0x809F000Bu)); Require(freshFailResult.existed_memory_size == 777); Require(!std::filesystem::exists(freshFailBinPath)); const auto* freshFailBytes = reinterpret_cast(&freshFailParam); Require(Read(freshFailParamPath) == std::vector(freshFailBytes, freshFailBytes + sizeof(freshFailParam))); - Require(!std::filesystem::exists(freshFailBinTemp)); Require(sceSaveDataSetupSaveDataMemory2(&freshFailSetup, &freshFailResult) == 0); Require(freshFailResult.existed_memory_size == 0); Require(Read(freshFailBinPath) == std::vector(64, 0)); @@ -278,15 +335,17 @@ int main() { const auto* orphanBytes = reinterpret_cast(&orphanParam); Require(Read(orphanParamPath) == std::vector(orphanBytes, orphanBytes + sizeof(orphanParam))); Require(sceSaveDataTerminate() == 0); - Require(Read(setupBinPath) == expectedBlob512); + Require(Read(setupBinPath) == expectedFinalBlob); Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); Require(sceSaveDataInitialize3(nullptr) == 0); setupResult.existed_memory_size = 111; Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); - Require(setupResult.existed_memory_size == 512); - Require(Read(setupBinPath) == expectedBlob512); + Require(setupResult.existed_memory_size == GrownMemorySize); + Require(Read(setupBinPath) == expectedFinalBlob); Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); Require(sceSaveDataTerminate() == 0); + Require(sceSaveDataCreateTransactionResource(1024) == kSaveDataNotInitialized); + Require(sceSaveDataDeleteTransactionResource(tx) == kSaveDataNotInitialized); std::filesystem::current_path(previous); std::filesystem::remove_all(root); } diff --git a/core/libs/tests/ScalarCall.cpp b/core/libs/tests/ScalarCall.cpp index e797b7aeba..c4d38b87c4 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -151,10 +151,8 @@ static void CheckUserDataCalls() { Require(std::equal(addFive.begin(), addFive.end(), linked.begin() + 12) && std::equal(addSixtyFour.begin(), addSixtyFour.end(), linked.begin() + 14)); auto readsPc = caller; readsPc[1] = 0xbe941f00u; - ExpectThrow("cannot have user-data callees linked", [&] { - const auto readsPcProgram = RdnaInstructionDecoder{}.Decode(readsPc); - static_cast(LinkUserDataCalls(readsPcProgram, FindUserDataCalls(readsPcProgram, swappc), callees)); - }); + const auto readsPcProgram = RdnaInstructionDecoder{}.Decode(readsPc); + Require(LinkUserDataCalls(readsPcProgram, FindUserDataCalls(readsPcProgram, swappc), callees).size() == 17u); const auto linkedProgram = RdnaInstructionDecoder{}.Decode(linked); const auto cfg = GraphBuilder{}.Build(linkedProgram, &swappc); @@ -181,6 +179,196 @@ static void CheckUserDataCalls() { ExpectThrow("is shared with another call", [&] { static_cast(GraphBuilder{}.Build(RdnaInstructionDecoder{}.Decode(sameCopy), &swappc)); }); } +static std::vector TableCaller() { + return {0x8f148414u, 0xf4080406u, 0xfa000270u, 0xbf8cc07fu, 0xf4280208u, 0x28000000u, 0xbf8cc07fu, 0xbe8e0308u, 0xbe8f0309u, 0xbe8e210eu, 0xbf810000u}; +} + +static ControlFlowGraph BuildCode(std::span code, const SwappcInfo& swappc) { + return GraphBuilder{}.Build(RdnaInstructionDecoder{}.Decode(code), &swappc); +} + +static IrProgram TranslateCode(std::span code, const SwappcInfo& swappc) { + const auto program = RdnaInstructionDecoder{}.Decode(code); + const auto cfg = GraphBuilder{}.Build(program, &swappc); + ShaderComputeInputInfo computeInfo{}; + TranslateOptions options{}; + options.stage = ShaderStageKind::Compute; + options.inputInfo.compute = &computeInfo; + options.userDataCount = swappc.userDataCount; + return InstructionTranslator{}.Translate(program, cfg, options); +} + +static void CheckConstantTableCall() { + const std::vector caller{0xbeea03ffu, 0xe0040000u, 0xbeeb038fu, 0xf4080135u, 0xfa000000u, 0xbf8cc07fu, 0xf4240382u, 0xfa000020u, 0xbf8cc07fu, 0xbf130e80u, + 0xbf840004u, 0xf4240402u, 0xfa000028u, 0xbf8cc07fu, 0xbe8e210eu, 0xbf810000u}; + const SwappcInfo swappc{false, 0u, 16u}; + const auto calls = FindTableCalls(RdnaInstructionDecoder{}.Decode(caller), swappc); + Require(calls.size() == 1u); + Require(calls[0].constantPointer == std::optional(0xfe0040000ull) && calls[0].descriptorOffset == 0u && calls[0].entryOffset == 0x20u && calls[0].entryStride == 0u); + Require(calls[0].linkRegister == 14u && calls[0].targetRegister == 14u); + auto changedHigh = caller; + changedHigh[2] = 0xbeeb0380u; + const auto zero = FindTableCalls(RdnaInstructionDecoder{}.Decode(changedHigh), swappc); + Require(zero.size() == 1u && zero[0].constantPointer == std::optional(0xe0040000ull)); + auto computed = caller; + computed[2] = 0xbeeb0302u; + Require(FindTableCalls(RdnaInstructionDecoder{}.Decode(computed), swappc).empty()); +} + +static void CheckTableCalls() { + CheckConstantTableCall(); + const SwappcInfo swappc{false, 0u, 16u}; + const auto caller = TableCaller(); + const auto program = RdnaInstructionDecoder{}.Decode(caller); + const auto calls = FindTableCalls(program, swappc); + Require(calls.size() == 1u); + Require(calls[0].programCounter == 0x24u && calls[0].linkRegister == 14u && calls[0].targetRegister == 14u && calls[0].userDataRegister == 12u); + Require(calls[0].descriptorOffset == 0x270u && calls[0].entryOffset == 0u && calls[0].entryStride == 16u); + Require(FindUserDataCalls(program, swappc).empty()); + const auto table = [&](std::size_t word, std::uint32_t value, std::uint32_t userDataCount = 16u) { + auto code = caller; + code[word] = value; + return FindTableCalls(RdnaInstructionDecoder{}.Decode(code), SwappcInfo{false, 0u, userDataCount}); + }; + Require(table(0u, 0x8f148114u).empty()); + Require(table(2u, 0x28000270u).empty()); + Require(table(8u, 0xbe8f030au).empty()); + Require(table(10u, 0xbf810000u, 12u).empty()); + Require(table(5u, 0x28000008u).size() == 1u && table(5u, 0x28000008u)[0].entryOffset == 8u); + Require(table(0u, 0xbe940380u).empty()); + auto readsLane = caller; + readsLane.insert(readsLane.begin(), {0xd4c2000bu, 0x00020300u}); + const auto readsLaneProgram = RdnaInstructionDecoder{}.Decode(readsLane); + Require(FindTableCalls(readsLaneProgram, SwappcInfo{false, 0u, 16u, 32u}).size() == 1u); + Require(FindTableCalls(readsLaneProgram, SwappcInfo{false, 0u, 16u, 64u}).empty()); + + ExpectThrow("not statically resolvable", [&] { static_cast(BuildCode(caller, swappc)); }); + const std::array fallback{0x24u}; + SwappcInfo listed = swappc; + listed.linkedCallFallbacks = fallback; + Require(BuildCode(caller, listed).unlinkedCallProgramCounters == std::vector{0x24u}); + const auto unlinked = TranslateCode(caller, listed); + Require(unlinked.Info().usesFaultBuffer && ProgramToString(unlinked).find("UnlinkedCall") != std::string::npos); + + const std::vector first{0xbf068000u, 0x4a020285u, 0xbe80200eu}; + const std::vector second{0x4a0202c0u, 0xbf068001u, 0xbe80200eu}; + const std::vector> callees{{{0x1122334455ull, first}, {0x66778899aaull, second}}}; + const auto linkedCalls = LinkCalls(program, {}, {}, calls, callees); + const auto& linked = linkedCalls.code; + Require(linkedCalls.fallbacks == std::vector{50u * 4u}); + ExpectThrow("is shared with another call", [&] { static_cast(BuildCode(linked, swappc)); }); + SwappcInfo linkedSwappc = swappc; + linkedSwappc.linkedCallFallbacks = linkedCalls.fallbacks; + Require(linked.size() == 59u && linked[9] == 0xbf820001u && linked[11] == 0xbf850010u && linked[58] == 0xbf9f0000u); + Require(linked[12] == 0xbf07ff0eu && linked[13] == 0x22334455u && linked[14] == 0xbf850004u && linked[15] == 0xbf07ff0fu && linked[16] == 0x11u && linked[17] == 0xbf850001u && linked[18] == 0xbf82001bu); + Require(linked[20] == 0x778899aau && linked[26] == 0xbf078080u && linked[27] == 0xbf820016u && linked[34] == 0xbf068080u && linked[35] == 0xbf82000au && linked[44] == 0xbf068080u && linked[45] == 0xbf820004u); + Require(linked[46] == 0xbb0e0005u && linked[47] == 0xbf82ffdau && linked[48] == 0xbb0e0006u && linked[49] == 0xbf82ffd8u && linked[50] == 0xbe8e210eu && linked[51] == 0xbf82ffd6u); + Require(std::equal(first.begin(), first.end(), linked.begin() + 52) && std::equal(second.begin(), second.end(), linked.begin() + 55)); + const auto cfg = BuildCode(linked, linkedSwappc); + const auto block = [&](std::uint32_t word) { return cfg.FindBlockByProgramCounter(word * 4u).id; }; + Require(cfg.FindBlockByProgramCounter(46u * 4u).terminator.trueBlock == block(52u)); + Require(cfg.FindBlockByProgramCounter(52u * 4u).terminator.trueBlock == block(47u)); + Require(cfg.FindBlockByProgramCounter(48u * 4u).terminator.trueBlock == block(55u)); + Require(cfg.FindBlockByProgramCounter(55u * 4u).terminator.trueBlock == block(49u)); + Require(cfg.unlinkedCallProgramCounters == std::vector{50u * 4u}); + static_cast(TranslateCode(linked, linkedSwappc)); + + std::vector large(0x8000u, 0xbf800000u); + const std::vector> far{{{0x10ull, large}, {0x20ull, first}}}; + ExpectThrow("out of s_call_b64 range", [&] { static_cast(LinkCalls(program, {}, {}, calls, far)); }); + + const std::vector jumpTable{ + 0xbe8c1f00u, 0x800cff0cu, 0x00000058u, 0x820d800du, 0x83928202u, 0xd4c40016u, 0x02000500u, 0x8f128212u, 0xf4000386u, 0x24000000u, + 0xbf8cc07fu, 0x7d820003u, 0x808c0e0cu, 0x828d800du, 0xbefd210cu, 0x7e020281u, 0xbf820003u, 0x7e020282u, 0xbf820001u, 0x7e020283u, + 0xe0700000u, 0x80000100u, 0xbf810000u, 0x00000020u, 0x00000018u, 0x00000010u}; + ExpectThrow("jumps through a table in its code", [&] { static_cast(LinkCalls(RdnaInstructionDecoder{}.Decode(jumpTable), {}, {}, {}, {})); }); + const std::array compare{0xbf068008u}; + const auto comparesLink = Build({DecodeRdnaSopc(0u, compare, 0u), Call(4u, 8u, 1u), Sopp(8u, 1u), Sop1(12u, 0u, 0x20u, 8u)}); + Require(comparesLink.FindBlockByProgramCounter(0u).terminator.trueBlock == comparesLink.FindBlockByProgramCounter(12u).id); + const auto readsFirst = Build({Sop1(0u, 0u, 3u, 8u), Call(4u, 8u, 1u), Sopp(8u, 1u), Sop1(12u, 0u, 0x20u, 8u)}); + Require(readsFirst.FindBlockByProgramCounter(0u).terminator.trueBlock == readsFirst.FindBlockByProgramCounter(12u).id); + ExpectThrow("escapes the constant-offset call/return model", [] { + Build({Sop1(0u, 0u, 3u, 8u), Call(4u, 8u, 2u), Sopp(8u, 2u, 0xfffdu), Sopp(12u, 1u), Sop1(16u, 0u, 0x20u, 8u)}); + }); +} + +enum class HoistedShape { InLoop, Hoisted, SecondWriterOnArm, LoadOnArmOnly, WriterAfterCall, WriterAfterLoop, CallOnArm, ConstantPointer, SkipsNullTarget, LongBranchWriter }; + +static std::vector HoistedTableCaller(HoistedShape shape) { + std::vector code; + const auto descriptorLoad = [&] { code.insert(code.end(), {0xf4081800u, 0xfa000010u, 0xbf8cc07fu}); }; + if (shape == HoistedShape::ConstantPointer) code.insert(code.end(), {0xbeea03ffu, 0xe0040000u, 0xbeeb038fu, 0xf4081835u, 0xfa000000u, 0xbf8cc07fu}); + else if (shape != HoistedShape::InLoop && shape != HoistedShape::LoadOnArmOnly) descriptorLoad(); + code.push_back(0xbf068002u); + code.push_back(shape == HoistedShape::LoadOnArmOnly ? 0xbf850003u : 0xbf850001u); + if (shape == HoistedShape::LoadOnArmOnly) descriptorLoad(); + else code.push_back(shape == HoistedShape::SecondWriterOnArm ? 0xbee00380u : shape == HoistedShape::CallOnArm ? 0xbb1e0000u : shape == HoistedShape::LongBranchWriter ? 0xbf820000u : 0x7e020280u); + const auto arm = code.size() - 1u; + code.push_back(0xbee5037eu); + const auto loop = code.size(); + code.push_back(0xbee4037eu); + if (shape == HoistedShape::InLoop) descriptorLoad(); + code.insert(code.end(), {0x7ed40500u, 0x7da4006au, 0x8f6a846au, 0xf4280430u, 0xd4000000u, 0xbf8cc07fu, 0xbe8e0310u, 0xbe8f0311u, 0xbe900312u, 0xbe910313u}); + if (shape == HoistedShape::SkipsNullTarget) code.insert(code.end(), {0xbf130e80u, 0xbf840001u}); + code.push_back(0xbe8e210eu); + if (shape == HoistedShape::WriterAfterCall) code.push_back(0xbee10301u); + code.push_back(0x897e7e64u); + code.push_back(0xbf890000u | ((static_cast(loop) - static_cast(code.size()) - 1u) & 0xffffu)); + code.push_back(0xbefe0365u); + if (shape == HoistedShape::WriterAfterLoop) code.push_back(0xbee00380u); + code.push_back(0xbf810000u); + if (shape == HoistedShape::CallOnArm) { + code[arm] |= static_cast(code.size() - arm - 1u); + code.insert(code.end(), {0xbe80201eu, 0xbf9f0000u}); + } + if (shape == HoistedShape::LongBranchWriter) { + code[arm] |= static_cast(code.size() - arm - 1u); + const auto getpc = code.size() + 3u; + code.insert(code.end(), {0xf4081800u, 0xfa000020u, 0xbf8cc07fu, 0xbe841f00u, 0x8084ff04u, static_cast((getpc - arm) * 4u), 0xbe802004u, 0xbf9f0000u}); + } + return code; +} + +static void CheckHoistedTableCalls() { + const std::vector callee{0x4a0202c0u, 0xbe80200eu}; + const std::vector> callees{{{0x1122334455ull, callee}}}; + for (const auto shape : {HoistedShape::InLoop, HoistedShape::Hoisted, HoistedShape::SecondWriterOnArm, HoistedShape::LoadOnArmOnly, HoistedShape::WriterAfterCall, HoistedShape::WriterAfterLoop, HoistedShape::CallOnArm, + HoistedShape::ConstantPointer, HoistedShape::SkipsNullTarget, HoistedShape::LongBranchWriter}) { + const bool linkable = shape == HoistedShape::InLoop || shape == HoistedShape::Hoisted || shape == HoistedShape::WriterAfterLoop || shape == HoistedShape::ConstantPointer || + shape == HoistedShape::SkipsNullTarget; + for (const std::uint32_t waveSize : {32u, 64u}) { + const auto code = HoistedTableCaller(shape); + const auto program = RdnaInstructionDecoder{}.Decode(code); + SwappcInfo swappc{false, 0u, 16u, waveSize}; + const auto calls = FindTableCalls(program, swappc); + if (!linkable) { + Require(calls.empty()); + ExpectThrow("not statically resolvable", [&] { static_cast(BuildCode(code, swappc)); }); + continue; + } + const bool constant = shape == HoistedShape::ConstantPointer; + Require(calls.size() == 1u && calls[0].userDataRegister == 0u && calls[0].descriptorOffset == (constant ? 0u : 0x10u) && calls[0].entryOffset == 0u && calls[0].entryStride == 16u); + Require(calls[0].constantPointer == (constant ? std::optional(0xfe0040000ull) : std::nullopt)); + const auto linked = LinkCalls(program, {}, {}, calls, callees); + swappc.linkedCallFallbacks = linked.fallbacks; + static_cast(TranslateCode(linked.code, swappc)); + } + } + const std::vector calledEntry{ + 0xf4081800u, 0xfa000000u, 0xbf8cc07fu, 0xbb1e0001u, 0xbf810000u, 0xbf840003u, 0xf4081800u, 0xfa000010u, + 0xbf8cc07fu, 0xf4280430u, 0xfa000000u, 0xbf8cc07fu, 0xbe8e0310u, 0xbe8f0311u, 0xbe8e210eu, 0xbe80201eu, 0xbf9f0000u}; + Require(FindTableCalls(RdnaInstructionDecoder{}.Decode(calledEntry), SwappcInfo{false, 0u, 16u}).empty()); + const std::vector fallsIntoEntry{ + 0xbf840002u, 0xbb1e0003u, 0xbf810000u, 0xf4081800u, 0xfa000010u, 0xbf8cc07fu, 0xf4280430u, 0xfa000000u, + 0xbf8cc07fu, 0xbe8e0310u, 0xbe8f0311u, 0xbe8e210eu, 0xbe80201eu, 0xbf9f0000u}; + Require(FindTableCalls(RdnaInstructionDecoder{}.Decode(fallsIntoEntry), SwappcInfo{false, 0u, 16u}).empty()); + const std::vector callBetween{ + 0xf4081800u, 0xfa000010u, 0xbf8cc07fu, 0xbb1e0007u, 0xf4280430u, 0xfa000000u, 0xbf8cc07fu, 0xbe8e0310u, + 0xbe8f0311u, 0xbe8e210eu, 0xbf810000u, 0xbe80201eu, 0xbf9f0000u}; + const auto between = FindTableCalls(RdnaInstructionDecoder{}.Decode(callBetween), SwappcInfo{false, 0u, 16u}); + Require(between.size() == 1u && between[0].descriptorOffset == 0x10u); +} + static void CheckRejections() { ExpectThrow("invalid instruction boundary", [] { Build({Call(0u, 8u, 16u), Sopp(4u, 1u)}); @@ -280,6 +468,8 @@ int main(int argc, char** argv) { CheckCallAfterEnd(); CheckRejections(); CheckUserDataCalls(); + CheckTableCalls(); + CheckHoistedTableCalls(); CheckGuards(""); } else { Require(argc == 2); diff --git a/core/libs/tests/ScalarSwappc.cpp b/core/libs/tests/ScalarSwappc.cpp index 6ab8dc4f05..7776183052 100644 --- a/core/libs/tests/ScalarSwappc.cpp +++ b/core/libs/tests/ScalarSwappc.cpp @@ -135,7 +135,7 @@ static void CheckThrows() { ExpectThrow("escapes the constant-offset call/return model", [] { Build(Program({ Sop1(0x00u, 4u, 0x1fu, 0u), - Sop2Literal(0x04u, 4u, 0x00u, 4u, 0x18u), + Sop2Literal(0x04u, 4u, 0x00u, 4u, 0x20u), Sop2Literal(0x0cu, 5u, 0x04u, 5u, 0u), Sop1(0x14u, 8u, 0x21u, 4u), Sop1(0x18u, 8u, 0x03u, 8u), diff --git a/core/libs/tests/SemaphoreTrace.cpp b/core/libs/tests/SemaphoreTrace.cpp new file mode 100644 index 0000000000..e49947b56e --- /dev/null +++ b/core/libs/tests/SemaphoreTrace.cpp @@ -0,0 +1,34 @@ +#include "prx/libkernel/Semaphore/include/Semaphore.hpp" +#include "prx/libkernel/Time/include/Time.hpp" +#include +#include +#include + +extern "C" void KernelTraceWait_nid_postfix(const char*, const void*, std::uint64_t, bool) {} + +int main() { + KernelSema sem = nullptr; + if (sceKernelCreateSema(&sem, "trace", 0, 0, 1, nullptr) != KERNEL_SEMA_OK) return 1; + + std::atomic result{-1}; + std::thread waiter([&] { result.store(sceKernelWaitSema(sem, 1, nullptr)); }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + for (;;) { + { + std::lock_guard lock(sem->mutex); + if (sem->waiterCount != 0) break; + } + if (std::chrono::steady_clock::now() >= deadline) { + sceKernelSignalSema(sem, 1); + waiter.join(); + sceKernelDeleteSema(sem); + return 2; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + const int signaled = sceKernelSignalSema(sem, 1); + waiter.join(); + const int deleted = sceKernelDeleteSema(sem); + return signaled == KERNEL_SEMA_OK && result.load() == KERNEL_SEMA_OK && deleted == KERNEL_SEMA_OK ? 0 : 3; +} diff --git a/core/libs/tests/SslCertificateFixture.hpp b/core/libs/tests/SslCertificateFixture.hpp index 82d0ad962b..0c86103e10 100644 --- a/core/libs/tests/SslCertificateFixture.hpp +++ b/core/libs/tests/SslCertificateFixture.hpp @@ -71,3 +71,61 @@ 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"; +inline constexpr char ExpiredServerPem[] = R"FIXTURE(-----BEGIN CERTIFICATE----- +MIIBxjCCAWygAwIBAgICEAMwCgYIKoZIzj0EAwIwQjELMAkGA1UEBhMCR0IxFTAT +BgNVBAoMDEFueVBTNSBUZXN0czEcMBoGA1UEAwwTQW55UFM1IGZpeHR1cmUgcm9v +dDAeFw0yNTA5MTQwMDAwMDBaFw0yNjA5MTkwMDAwMDBaMDgxCzAJBgNVBAYTAkdC +MRUwEwYDVQQKDAxBbnlQUzUgVGVzdHMxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABDvmM3W4JGu6KyrQLp/qYYvTX4MBUwWGIP9+ +VcRgPCO7jwtb0Aoaf6tSaG1Reax8J2rvTRRDRibTwZhg/6a7LKSjXDBaMBQGA1Ud +EQQNMAuCCWxvY2FsaG9zdDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUF +BwMBMB8GA1UdIwQYMBaAFFZolbTcGqYVMtT6BD37g9yqaAzcMAoGCCqGSM49BAMC +A0gAMEUCIFOAMPrcpBrmsJrbXaWzyCDe1UweszENyWAW4pZqs/hmAiEA/vehYXPI +exj4IIRU7Zt80wBXEjJNBk8KXmhO/UvPh/A= +-----END CERTIFICATE----- +)FIXTURE"; +inline constexpr char ExpiredServerKey[] = R"FIXTURE(-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg4TeyA0YK3Bq0Rt2l +On7kv/wcghGMcWvA4f5xVsGJUcahRANCAAQ75jN1uCRruisq0C6f6mGL01+DAVMF +hiD/flXEYDwju48LW9AKGn+rUmhtUXmsfCdq700UQ0Ym08GYYP+muyyk +-----END PRIVATE KEY----- +)FIXTURE"; +inline constexpr char FutureServerPem[] = R"FIXTURE(-----BEGIN CERTIFICATE----- +MIIBxzCCAWygAwIBAgICEAQwCgYIKoZIzj0EAwIwQjELMAkGA1UEBhMCR0IxFTAT +BgNVBAoMDEFueVBTNSBUZXN0czEcMBoGA1UEAwwTQW55UFM1IGZpeHR1cmUgcm9v +dDAeFw0zNzExMjMwMDAwMDBaFw0zODEyMjcwMDAwMDBaMDgxCzAJBgNVBAYTAkdC +MRUwEwYDVQQKDAxBbnlQUzUgVGVzdHMxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABP1SqW8j3y1k1uDBPD2cFW3vDPOb09zGYm6g +mUpt2UXlx6WTsiohSzv9emkUSe/YJez3mLoEB4c/ohL0rNhdGTqjXDBaMBQGA1Ud +EQQNMAuCCWxvY2FsaG9zdDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUF +BwMBMB8GA1UdIwQYMBaAFFZolbTcGqYVMtT6BD37g9yqaAzcMAoGCCqGSM49BAMC +A0kAMEYCIQDG0pZG6c9W7tVIA9AoG8YVyQbBZ0krnjw4SlAfy1I3swIhAMIqV5JD +tjTwhuPqzDgkmObSwb6GGD1abVRLCDJJMjsZ +-----END CERTIFICATE----- +)FIXTURE"; +inline constexpr char FutureServerKey[] = R"FIXTURE(-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgecaFHjyMpE80Z1ja +I7gmZUa8yQKqbgS9PZq3PQPD3h+hRANCAAT9UqlvI98tZNbgwTw9nBVt7wzzm9Pc +xmJuoJlKbdlF5celk7IqIUs7/XppFEnv2CXs95i6BAeHP6IS9KzYXRk6 +-----END PRIVATE KEY----- +)FIXTURE"; diff --git a/core/libs/tests/ThreadName.cpp b/core/libs/tests/ThreadName.cpp new file mode 100644 index 0000000000..c5035a890a --- /dev/null +++ b/core/libs/tests/ThreadName.cpp @@ -0,0 +1,17 @@ +#include "prx/libc/include/general/ThreadName.hpp" +#include + +int main() { + constexpr const char* name = "AnyPS5 queue worker 0x2a"; + const HRESULT set = ThreadName::SetCurrent(name); + if (set == S_FALSE) return 0; + if (FAILED(set)) return 1; + using GetThreadDescriptionFunction = HRESULT(WINAPI*)(HANDLE, PWSTR*); + const auto getThreadDescription = ThreadName::Resolve("GetThreadDescription"); + if (getThreadDescription == nullptr) return 0; + PWSTR description = nullptr; + if (FAILED(getThreadDescription(GetCurrentThread(), &description))) return 2; + const bool matches = description != nullptr && std::wstring(description) == L"AnyPS5 queue worker 0x2a"; + if (description != nullptr) LocalFree(description); + return matches ? 0 : 3; +} diff --git a/core/libs/tests/UnwindCallbackAbi.cpp b/core/libs/tests/UnwindCallbackAbi.cpp new file mode 100644 index 0000000000..18fb85df1d --- /dev/null +++ b/core/libs/tests/UnwindCallbackAbi.cpp @@ -0,0 +1,132 @@ +#include "prx/libc/include/exceptions/Unwind.hpp" + +#include +#include +#include +#include + +extern "C" _Unwind_Reason_Code APS5_VABI _Unwind_Backtrace_nid_postfix(_Unwind_Trace_Fn trace, void* argument); +extern "C" _Unwind_Reason_Code APS5_VABI _Unwind_ForcedUnwind_nid_postfix(_Unwind_Exception* exception, _Unwind_Stop_Fn stop, void* argument); +extern "C" _Unwind_Ptr APS5_VABI _Unwind_GetRegionStart_nid_postfix(_Unwind_Context* context); +extern "C" void* APS5_VABI __cxa_begin_catch_nid_postfix(void* exception); +extern "C" void APS5_VABI __cxa_end_catch_nid_postfix(); +extern "C" [[gnu::noinline]] void BacktraceFromHere(); +extern "C" [[gnu::noinline]] void ForceUnwindFromHere(); + +namespace { + +constexpr std::uint64_t ForeignClass = 0x54455354464f5200; +constexpr std::uint64_t GnuClass = 0x474e5543432b2b00; + +using Cleanup = decltype(_Unwind_Exception::exception_cleanup); + +struct Call { + int count; + int version; + _Unwind_Action actions; + std::uint64_t exceptionClass; + _Unwind_Exception* exception; + void* argument; + _Unwind_Ptr region; + _Unwind_Reason_Code reason; +}; + +Call trace; +Call stop; +Call cleanup; +int traceArgument; +int stopArgument; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "unwind callback check failed: %s\n", message); + std::exit(1); +} + +_Unwind_Reason_Code APS5_VABI Trace(_Unwind_Context* context, void* argument) { + ++trace.count; + trace.argument = argument; + trace.region = argument == &traceArgument ? _Unwind_GetRegionStart_nid_postfix(context) : 0; + return _URC_NORMAL_STOP; +} + +_Unwind_Reason_Code APS5_VABI Stop(int version, _Unwind_Action actions, std::uint64_t exceptionClass, _Unwind_Exception* exception, + _Unwind_Context* context, void* argument) { + ++stop.count; + stop.version = version; + stop.actions = actions; + stop.exceptionClass = exceptionClass; + stop.exception = exception; + stop.argument = argument; + stop.region = argument == &stopArgument ? _Unwind_GetRegionStart_nid_postfix(context) : 0; + return _URC_NORMAL_STOP; +} + +void APS5_VABI GuestCleanup(_Unwind_Reason_Code reason, _Unwind_Exception* exception) { + ++cleanup.count; + cleanup.reason = reason; + cleanup.exception = exception; +} + +void HostCleanup(_Unwind_Reason_Code reason, _Unwind_Exception* exception) { + ++cleanup.count; + cleanup.reason = reason; + cleanup.exception = exception; +} + +void CheckCleanup(const _Unwind_Exception& exception) { + Require(cleanup.count == 1, "exception_cleanup is called once"); + Require(cleanup.reason == _URC_FOREIGN_EXCEPTION_CAUGHT, "exception_cleanup gets _URC_FOREIGN_EXCEPTION_CAUGHT"); + Require(cleanup.exception == &exception, "exception_cleanup gets the deleted exception"); +} + +void CheckDeleteException() { + _Unwind_Exception exception {}; + exception.exception_class = ForeignClass; + exception.exception_cleanup = GuestCleanup; + _Unwind_DeleteException_nid_postfix(&exception); + CheckCleanup(exception); +} + +void CheckEndCatch(std::uint64_t exceptionClass, Cleanup function) { + _Unwind_Exception exception {}; + exception.exception_class = exceptionClass; + exception.exception_cleanup = function; + __cxa_begin_catch_nid_postfix(&exception); + __cxa_end_catch_nid_postfix(); + CheckCleanup(exception); +} + +} + +extern "C" void BacktraceFromHere() { + Require(_Unwind_Backtrace_nid_postfix(Trace, &traceArgument) == _URC_NORMAL_STOP, "_Unwind_Backtrace stops when the callback asks"); + Require(trace.count == 1, "_Unwind_Backtrace calls the callback once"); + Require(trace.argument == &traceArgument, "the trace callback gets the argument given to _Unwind_Backtrace"); + Require(trace.region == reinterpret_cast<_Unwind_Ptr>(&BacktraceFromHere), "the trace callback gets the context of the caller of _Unwind_Backtrace"); +} + +extern "C" void ForceUnwindFromHere() { + _Unwind_Exception exception {}; + exception.exception_class = ForeignClass; + _Unwind_ForcedUnwind_nid_postfix(&exception, Stop, &stopArgument); + Require(stop.count == 1, "_Unwind_ForcedUnwind calls the stop function once"); + Require(stop.version == 1, "the stop function gets version 1"); + Require(stop.actions == (_UA_CLEANUP_PHASE | _UA_FORCE_UNWIND), "the stop function gets _UA_CLEANUP_PHASE | _UA_FORCE_UNWIND"); + Require(stop.exceptionClass == ForeignClass, "the stop function gets the exception class"); + Require(stop.exception == &exception, "the stop function gets the exception"); + Require(stop.argument == &stopArgument, "the stop function gets the argument given to _Unwind_ForcedUnwind"); + Require(stop.region == reinterpret_cast<_Unwind_Ptr>(&ForceUnwindFromHere), "the stop function gets the context of the caller of _Unwind_ForcedUnwind"); +} + +int main(int argc, char** argv) { + const std::string mode = argc > 1 ? argv[1] : ""; + if (mode == "backtrace") BacktraceFromHere(); + else if (mode == "forced_unwind") ForceUnwindFromHere(); + else if (mode == "delete_exception") CheckDeleteException(); + else if (mode == "end_catch") CheckEndCatch(ForeignClass, GuestCleanup); + else if (mode == "end_catch_gnu") CheckEndCatch(GnuClass, reinterpret_cast(&HostCleanup)); + else Require(false, "unknown mode"); + std::printf("unwind callback ABI %s test passed\n", mode.c_str()); + return 0; +} diff --git a/core/libs/tests/Vp9Fixture.hpp b/core/libs/tests/Vp9Fixture.hpp new file mode 100644 index 0000000000..9878c4a620 --- /dev/null +++ b/core/libs/tests/Vp9Fixture.hpp @@ -0,0 +1,108 @@ +#ifndef CORE_LIBS_TESTS_VP9FIXTURE_HPP +#define CORE_LIBS_TESTS_VP9FIXTURE_HPP + +#include +#include +#include +#include + +namespace Vp9Fixture { + +inline constexpr std::array Vp9Stream{ + 0x44, 0x4b, 0x49, 0x46, 0x00, 0x00, 0x20, 0x00, 0x56, 0x50, 0x39, 0x30, 0x40, 0x00, 0x28, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xe9, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x82, 0x49, 0x83, 0x42, + 0x00, 0x03, 0xf0, 0x02, 0x76, 0x08, 0x38, 0x24, 0x1c, 0x18, 0x0e, 0x00, 0x01, 0xc0, 0x6f, 0xb1, + 0xfb, 0x17, 0xa6, 0xd2, 0x0f, 0x5f, 0xee, 0xf7, 0xec, 0xbf, 0xb4, 0x0c, 0x93, 0xbe, 0x87, 0x11, + 0x35, 0x1c, 0x51, 0x84, 0x8b, 0x70, 0xad, 0xe4, 0x00, 0x00, 0x79, 0x3b, 0xff, 0xff, 0xf5, 0x94, + 0xad, 0x57, 0x6e, 0xae, 0x8e, 0x1b, 0xfe, 0x64, 0xff, 0xc9, 0x64, 0xba, 0xca, 0x1d, 0x7f, 0x45, + 0x16, 0x95, 0x13, 0x8d, 0xd0, 0x61, 0x19, 0x80, 0xb7, 0xdf, 0xff, 0xff, 0xd6, 0x52, 0x1e, 0x7b, + 0x3c, 0x16, 0x3f, 0xff, 0xfb, 0x52, 0xe7, 0x26, 0xab, 0xd6, 0x3a, 0x6a, 0xff, 0xf8, 0x1b, 0x47, + 0xcd, 0xe2, 0xf5, 0x48, 0x65, 0x1e, 0x9f, 0x5c, 0x1a, 0x74, 0x0a, 0x99, 0xc0, 0x04, 0x41, 0x02, + 0xa9, 0x86, 0x36, 0x1f, 0xff, 0xf3, 0x4f, 0x71, 0xdf, 0x8a, 0xcd, 0x8a, 0x88, 0xdd, 0x37, 0xa2, + 0x27, 0x3a, 0x20, 0xb8, 0xf0, 0x4f, 0x67, 0x59, 0x8a, 0x82, 0xa2, 0xde, 0x08, 0xe3, 0xae, 0x87, + 0xff, 0xf0, 0x36, 0x8f, 0x9b, 0xc5, 0xea, 0x90, 0xca, 0x3d, 0x3e, 0xb8, 0x34, 0xe8, 0x15, 0x33, + 0x80, 0x08, 0x82, 0x05, 0x53, 0x0c, 0x6c, 0x3f, 0xff, 0xe6, 0x9e, 0xe3, 0xbf, 0x15, 0x9d, 0xec, + 0xdf, 0xe9, 0xce, 0x60, 0x88, 0x09, 0x48, 0x81, 0xa2, 0xb6, 0x27, 0x3e, 0xf9, 0xa9, 0x7e, 0xa7, + 0xe1, 0x9d, 0xdd, 0x3a, 0xa0, 0x39, 0x3f, 0x8a, 0xba, 0x73, 0x50, 0xde, 0xcf, 0x29, 0x5c, 0x2a, + 0x92, 0x01, 0xec, 0x06, 0xbe, 0x8b, 0x1e, 0x14, 0x34, 0x6b, 0xb1, 0x3a, 0x01, 0x47, 0x08, 0xe7, + 0xbe, 0xb0, 0xa7, 0x03, 0x77, 0xeb, 0xf0, 0xed, 0xd8, 0xa1, 0x4e, 0x7c, 0xd9, 0xe4, 0x7c, 0x9d, + 0x67, 0xfe, 0x36, 0xab, 0x5e, 0xa9, 0x5e, 0x81, 0x74, 0x68, 0xa1, 0x4b, 0x61, 0x81, 0xe2, 0xe4, + 0x4c, 0x6a, 0x4f, 0xe5, 0x65, 0x3f, 0x4c, 0x78, 0xea, 0x68, 0x5c, 0xf9, 0xc6, 0x58, 0x1f, 0x58, + 0xe0, 0x40, 0x4b, 0x88, 0x6d, 0x15, 0x94, 0x8c, 0x83, 0x4a, 0xa3, 0xf3, 0xd5, 0x80, 0x7f, 0x83, + 0x52, 0x7d, 0xbe, 0xa6, 0x9f, 0xb4, 0x89, 0xda, 0xb6, 0x5a, 0xdf, 0x1f, 0x78, 0xd8, 0xbf, 0xfe, + 0x7f, 0x52, 0xfa, 0xfd, 0xa4, 0xeb, 0xda, 0x91, 0x9c, 0xb6, 0xd0, 0xc3, 0xfd, 0x8e, 0x04, 0x04, + 0xb8, 0x86, 0xd1, 0x56, 0xa2, 0x24, 0xe6, 0xbc, 0x5b, 0xbf, 0x82, 0x36, 0xb5, 0xa7, 0x87, 0x73, + 0xd8, 0xbd, 0x30, 0x33, 0x3f, 0x7c, 0xbb, 0x4f, 0x70, 0xed, 0x3f, 0x8d, 0x2b, 0xe2, 0xce, 0x62, + 0x09, 0xfa, 0x04, 0x31, 0x67, 0xff, 0x09, 0xc1, 0x5c, 0x29, 0xf8, 0x77, 0xfa, 0xb6, 0xa7, 0xde, + 0xaa, 0xc1, 0x87, 0xd9, 0xb4, 0x10, 0x0d, 0xda, 0x5d, 0xf7, 0xdd, 0x7d, 0x08, 0x7a, 0xbe, 0x60, + 0xeb, 0x85, 0x00, 0x08, 0x21, 0x8e, 0xa0, 0x4d, 0xff, 0x73, 0x66, 0x55, 0xb8, 0x61, 0x69, 0xc5, + 0xf2, 0xef, 0x96, 0x27, 0x16, 0x2f, 0x0d, 0x93, 0xcd, 0xcd, 0xba, 0x2a, 0x80, 0x59, 0x90, 0x2b, + 0xc5, 0x43, 0x49, 0x25, 0x49, 0x01, 0x15, 0x9f, 0xf4, 0xdd, 0x02, 0x31, 0x8b, 0xf0, 0xbe, 0x28, + 0x87, 0xbd, 0x3f, 0xce, 0xb1, 0x6e, 0xd2, 0x7f, 0x77, 0x41, 0x09, 0x8a, 0x04, 0x17, 0x87, 0x57, + 0x1b, 0x72, 0x79, 0x69, 0x41, 0x7d, 0xc5, 0x78, 0xb7, 0x53, 0xe7, 0x80, 0xd2, 0x7f, 0x1f, 0xb9, + 0xd5, 0x0f, 0xff, 0xf5, 0xb8, 0xd0, 0xc5, 0x47, 0x30, 0x09, 0x36, 0x6e, 0xb1, 0xfd, 0xe1, 0xe9, + 0xf9, 0x81, 0x1d, 0x98, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x86, 0x00, 0x40, 0x92, 0x9c, 0x10, 0x46, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x76, 0x21, + 0x2d, 0x37, 0xff, 0xff, 0xc3, 0x7f, 0xff, 0xff, 0xff, 0xd8, 0x59, 0x6b, 0xff, 0xa9, 0xff, 0xff, + 0xff, 0xda, 0x3d, 0xca, 0x5b, 0xc7, 0xff, 0xfd, 0x52, 0xf6, 0x2e, 0x02, 0xe0, 0x37, 0xe3, 0x5f, + 0xff, 0xea, 0x82, 0x5b, 0xff, 0xff, 0xfa, 0x7b, 0x85, 0xdf, 0x66, 0xbf, 0xff, 0xe6, 0xde, 0xef, + 0xed, 0x97, 0xff, 0xf9, 0x38, 0x4a, 0x7f, 0x61, 0xb5, 0xeb, 0x56, 0x73, 0xa7, 0xff, 0xff, 0x31, + 0x8b, 0xa1, 0xcf, 0xff, 0x5b, 0x90, 0x07, 0x17, 0xff, 0xd6, 0xe1, 0x74, 0x39, 0xff, 0xeb, 0x72, + 0x5d, 0x14, 0xff, 0xf5, 0xb8, 0x95, 0x54, 0x7f, 0xf7, 0x5d, 0x1e, 0x26, 0x68, 0x00, 0x7f, 0x00, + 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86, 0x00, 0x40, 0x92, 0x9c, 0x10, + 0x45, 0x00, 0x00, 0x03, 0x60, 0x00, 0x00, 0x7d, 0xb8, 0xff, 0xff, 0xb4, 0x7f, 0xff, 0xff, 0xfe, + 0x4d, 0x81, 0xbf, 0xff, 0x98, 0x9f, 0xff, 0xff, 0xe4, 0xd8, 0x1b, 0xff, 0xf9, 0x89, 0xff, 0xff, + 0xfe, 0x4d, 0x81, 0xbf, 0xd6, 0xbf, 0xff, 0xf9, 0x36, 0x0c, 0x1f, 0xff, 0xfc, 0x10, 0x3e, 0xfe, + 0x1f, 0xa1, 0x76, 0x5c, 0x22, 0x5f, 0x3b, 0x56, 0x57, 0xa0, 0x3c, 0x43, 0xf0, 0xc8, 0x00, 0x00, + 0x00, 0x19, 0xbb, 0xff, 0xff, 0xd8, 0x43, 0x10, 0x26, 0x67, 0xff, 0xff, 0xfe, 0x4a, 0x07, 0x8f, + 0x80, 0x7f, 0xff, 0xff, 0x94, 0xd2, 0x59, 0x02, 0xff, 0xff, 0x0d, 0x89, 0x18, 0x02, 0xff, 0xff, + 0x0d, 0x84, 0xb2, 0x05, 0xff, 0xfe, 0x1b, 0x14, 0xdd, 0x42, 0xff, 0xff, 0x0d, 0x82, 0xc8, 0xb1, + 0xff, 0xf9, 0x5c, 0x32, 0xfe, 0x00, 0x78, 0x60, 0x00, 0x56, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x86, 0x00, 0x40, 0x92, 0x9c, 0x20, 0x46, 0x00, 0x00, 0x03, 0x60, + 0x00, 0x00, 0x73, 0xaf, 0xff, 0xff, 0xc3, 0x7f, 0xff, 0xff, 0xff, 0xd8, 0x19, 0x3f, 0xff, 0xe6, + 0x1b, 0xfb, 0x80, 0x95, 0x82, 0x3b, 0xd9, 0x4c, 0x81, 0xce, 0x52, 0x2b, 0xff, 0xff, 0xa0, 0xad, + 0xc9, 0x0e, 0x62, 0xff, 0xff, 0xff, 0x60, 0xf8, 0x75, 0x5f, 0xff, 0xff, 0x31, 0x92, 0xaa, 0x8f, + 0xfe, 0xeb, 0xa4, 0x4e, 0x5f, 0xff, 0x5b, 0x89, 0x55, 0x47, 0xff, 0x75, 0xd4, 0xd9, 0xef, 0xff, + 0xe4, 0x51, 0x64, 0x58, 0xff, 0xee, 0xba, 0xfc, 0x8d, 0x8f, 0x00, 0x95, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86, 0x00, 0x40, 0x92, 0x9c, 0x10, 0x44, 0x00, 0x00, + 0x03, 0x60, 0x00, 0x00, 0x73, 0xaf, 0xff, 0xff, 0xfb, 0x50, 0x3f, 0xff, 0xff, 0xff, 0xea, 0xc5, + 0x0d, 0xff, 0xff, 0xe0, 0x09, 0xfc, 0xad, 0x23, 0x5f, 0xf8, 0xa3, 0x64, 0x9b, 0x40, 0x76, 0x8d, + 0xd6, 0xd1, 0x83, 0x6f, 0x24, 0xd5, 0xff, 0xff, 0xd4, 0xa9, 0x45, 0xac, 0xda, 0xdc, 0xd2, 0xbf, + 0xff, 0xff, 0xfd, 0x58, 0xbf, 0x7f, 0x8a, 0x7f, 0xff, 0xff, 0xad, 0xb9, 0x2d, 0x3c, 0xbf, 0xff, + 0xd4, 0xca, 0x46, 0x7d, 0x7f, 0xff, 0xe1, 0x64, 0x96, 0x9e, 0x5f, 0xff, 0xea, 0x65, 0x4d, 0xe9, + 0x97, 0xff, 0xfa, 0x99, 0x16, 0x71, 0xaf, 0xff, 0xfc, 0x2c, 0x99, 0x7f, 0x5b, 0x8b, 0x50, 0x9c, + 0xdc, 0x43, 0xa8, 0xb7, 0xd5, 0x68, 0x6a, 0xbc, 0x78, 0xef, 0xc2, 0x3d, 0xfb, 0x3f, 0xcc, 0x51, + 0x40, 0x86, 0x54, 0x02, 0xa4, 0xb2, 0xbd, 0x79, 0x93, 0xa2, 0x4a, 0x35, 0x36, 0xc8, 0x7c, 0xdb, + 0xb0, 0x1d, 0xc1, 0xd0, 0x1d, 0x51, 0x9a, 0xe4, 0x19, 0xe6, 0x08, 0x00, 0x95, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86, 0x00, 0x40, 0x92, 0x9c, 0x10, 0x46, 0x00, + 0x00, 0x03, 0x60, 0x00, 0x00, 0x73, 0xaf, 0xff, 0xff, 0xa9, 0xef, 0xff, 0xff, 0xff, 0xd8, 0x59, + 0x9f, 0xff, 0x53, 0xff, 0xff, 0xff, 0xb4, 0x78, 0x24, 0x37, 0xff, 0xfb, 0x89, 0x98, 0x26, 0xc0, + 0xe3, 0x5f, 0x50, 0x5c, 0x2f, 0xff, 0xff, 0xf1, 0xd2, 0xd2, 0x4d, 0xfd, 0xf2, 0x7f, 0xff, 0x9f, + 0xff, 0x17, 0x85, 0xd2, 0xab, 0x4b, 0xbe, 0x3d, 0xff, 0xff, 0xff, 0xdf, 0x55, 0xbf, 0xff, 0xff, + 0xff, 0xfe, 0x6e, 0xe3, 0xe4, 0x07, 0x53, 0xd2, 0x5f, 0xff, 0xff, 0xff, 0xbe, 0xab, 0x7f, 0xff, + 0xff, 0xff, 0xcd, 0xdc, 0x1a, 0xbf, 0xff, 0xfe, 0xfa, 0xad, 0xff, 0xff, 0xff, 0xcd, 0xdc, 0x4e, + 0x1d, 0x57, 0xff, 0xff, 0xe0, 0x44, 0xaa, 0xa3, 0xff, 0xd3, 0x89, 0x13, 0x97, 0xff, 0xe5, 0xa2, + 0x55, 0x51, 0xff, 0xe9, 0xc4, 0xba, 0x29, 0xff, 0xeb, 0x71, 0x2a, 0xa8, 0xff, 0xee, 0xb9, 0xc4, + 0x7f, 0xff, 0xfb, 0xea, 0xb7, 0xff, 0xff, 0xff, 0x37, 0x71, 0x29, 0xfa, 0x00, +}; + +inline constexpr std::array Vp9Hashes{ + 0x552fd3c320b37408ull, 0xb2fb78bac8d2d7dfull, 0x312469345c491c3dull, + 0x9aeb0fb400c95a75ull, 0xe02e07646faa48beull, 0x1399072031abb375ull, +}; + +inline std::vector> ivfFrames() { + std::vector> frames; + for (std::size_t index = 32; index + 12 <= Vp9Stream.size();) { + const std::size_t length = Vp9Stream[index] | (Vp9Stream[index + 1] << 8u) | (Vp9Stream[index + 2] << 16u) | (static_cast(Vp9Stream[index + 3]) << 24u); + if (index + 12 + length > Vp9Stream.size()) return {}; + frames.emplace_back(Vp9Stream.begin() + static_cast(index + 12), Vp9Stream.begin() + static_cast(index + 12 + length)); + index += 12 + length; + } + return frames; +} + +} + +#endif diff --git a/core/libs/tests/Wave32WideSubgroup.cpp b/core/libs/tests/Wave32WideSubgroup.cpp index af28fb59d9..7513618632 100644 --- a/core/libs/tests/Wave32WideSubgroup.cpp +++ b/core/libs/tests/Wave32WideSubgroup.cpp @@ -63,6 +63,7 @@ public: case spv::OpConstantTrue: value[0] = 1; break; case spv::OpConstantFalse: value[0] = 0; break; case spv::OpLoad: value = arg(3); break; + case spv::OpCopyObject: value = arg(3); break; case spv::OpBitwiseAnd: value[0] = arg(3)[0] & arg(4)[0]; break; case spv::OpBitwiseOr: value[0] = arg(3)[0] | arg(4)[0]; break; case spv::OpBitwiseXor: value[0] = arg(3)[0] ^ arg(4)[0]; break; diff --git a/core/libs/tests/WindowsBacktraceReport.cpp b/core/libs/tests/WindowsBacktraceReport.cpp index a5d93edc43..d16340e2b3 100644 --- a/core/libs/tests/WindowsBacktraceReport.cpp +++ b/core/libs/tests/WindowsBacktraceReport.cpp @@ -1,11 +1,53 @@ +#include #include #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); +} + +LONG WINAPI EndAfterReport(EXCEPTION_POINTERS* info) { + if (info->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) TerminateProcess(GetCurrentProcess(), 3); + return EXCEPTION_CONTINUE_SEARCH; +} + +[[gnu::noinline]] void FaultHoldingObject() { + AddVectoredExceptionHandler(0, EndAfterReport); + auto* object = static_cast(VirtualAlloc(nullptr, 0x10000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)); + if (object == nullptr) std::exit(1); + for (std::uint64_t i = 0; i < 8; ++i) object[i] = 0xc0ffee0000000000ull + i; + std::uint64_t* volatile held = object; + volatile int* volatile target = nullptr; + *target = static_cast(reinterpret_cast(held)); +} + +} + +int main(int argc, char** argv) { if (__error_nid_postfix() == nullptr) return 1; + if (argc == 2 && std::strcmp(argv[1], "fault") == 0) FaultHoldingObject(); + 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..c83acd49c2 100644 --- a/core/libs/tests/WindowsExceptions.cpp +++ b/core/libs/tests/WindowsExceptions.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -57,7 +58,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"); @@ -74,7 +75,7 @@ static int RunThrowChild() { return 100; } -static void testUnhandledThrowSite() { +static std::string RunChild(const wchar_t* argument, DWORD& exitCode) { SECURITY_ATTRIBUTES attributes{}; attributes.nLength = sizeof(attributes); attributes.bInheritHandle = TRUE; @@ -84,7 +85,6 @@ static void testUnhandledThrowSite() { RequireHandle(SetHandleInformation(readPipe, HANDLE_FLAG_INHERIT, 0) != 0); std::array image{}; RequireHandle(GetModuleFileNameW(nullptr, image.data(), static_cast(image.size())) != 0); - const wchar_t* argument = L"\" throw-uncaught"; std::array command{}; const std::size_t imageLength = std::wcslen(image.data()); const std::size_t argumentLength = std::wcslen(argument); @@ -110,22 +110,67 @@ static void testUnhandledThrowSite() { } CloseHandle(readPipe); RequireHandle(WaitForSingleObject(child.hProcess, 20000) == WAIT_OBJECT_0); - DWORD exitCode = 0; RequireHandle(GetExitCodeProcess(child.hProcess, &exitCode) != 0); CloseHandle(child.hProcess); output[outputLength < output.size() ? outputLength : output.size() - 1] = 0; + return std::string(output.data()); +} + +static void testUnhandledThrowSite() { + DWORD exitCode = 0; + const std::string output = RunChild(L"\" throw-uncaught", exitCode); RequireHandle(exitCode == 0xC0000409); - const char* probeMarker = std::strstr(output.data(), "probe "); + const char* probeMarker = std::strstr(output.c_str(), "probe "); RequireHandle(probeMarker != nullptr); const auto probe = static_cast(std::strtoull(probeMarker + 6, nullptr, 16)); RequireHandle(probe != 0); - const char* marker = std::strstr(output.data(), "thrown from "); + const char* marker = std::strstr(output.c_str(), "thrown from "); RequireHandle(marker != nullptr); const auto logged = static_cast(std::strtoull(marker + 13, nullptr, 16)); RequireHandle(logged != 0); if (logged <= probe || logged - probe >= 0x1000) throw std::runtime_error("unhandled exception named the wrapper instead of the throw site"); } +[[gnu::noinline, noreturn]] static void GuestAbortProbe() { + __asm__ volatile("int $0x45"); + __builtin_unreachable(); +} + +static int RunAbortChild() { + std::fprintf(stderr, "abort probe\n"); + std::fflush(stderr); + GuestAbortProbe(); +} + +static void testGuestAbortReport() { + DWORD exitCode = 0; + const std::string output = RunChild(L"\" guest-abort", exitCode); + char code[16]; + std::snprintf(code, sizeof(code), "0x%08lx", static_cast(exitCode)); + if (exitCode != 0xC0000005 || output.find("guest abort() (int 0x45)") == std::string::npos) + throw std::runtime_error(std::string("guest abort child exited with ") + code + " and printed:\n" + output); +} + +[[gnu::noinline, noreturn]] static void GuestAssertProbe() { + __asm__ volatile("int $0x41"); + __builtin_unreachable(); +} + +static int RunAssertChild() { + std::fprintf(stderr, "assert probe\n"); + std::fflush(stderr); + GuestAssertProbe(); +} + +static void testGuestAssertReport() { + DWORD exitCode = 0; + const std::string output = RunChild(L"\" guest-assert", exitCode); + char code[16]; + std::snprintf(code, sizeof(code), "0x%08lx", static_cast(exitCode)); + if (exitCode != 0xC0000005 || output.find("guest assert/break (int 0x41)") == std::string::npos) + throw std::runtime_error(std::string("guest assert child exited with ") + code + " and printed:\n" + output); +} + static int destroyed; struct Guard { ~Guard() { ++destroyed; } }; static void ThrowNested() { @@ -273,10 +318,14 @@ static void testUnwindThroughRuntimeModules() { int main(int argc, char** argv) { if (argc > 1) { if (std::strcmp(argv[1], "throw-uncaught") == 0) return RunThrowChild(); + if (std::strcmp(argv[1], "guest-abort") == 0) return RunAbortChild(); + if (std::strcmp(argv[1], "guest-assert") == 0) return RunAssertChild(); return 100; } TestTypeInfoVtables(); testUnhandledThrowSite(); + testGuestAbortReport(); + testGuestAssertReport(); testExceptionPointer(); testFutureException(); testMadeExceptionPointer(); diff --git a/core/libs/tests/WindowsMappingsRelease.cpp b/core/libs/tests/WindowsMappingsRelease.cpp new file mode 100644 index 0000000000..6b2f016c97 --- /dev/null +++ b/core/libs/tests/WindowsMappingsRelease.cpp @@ -0,0 +1,81 @@ +#include "prx/libc/include/WindowsMappings.hpp" +#include +#include +#include + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (!value) throw std::runtime_error("Windows resident release regression at " + std::to_string(location.line())); +} + +static void CheckMetadataMismatch(unsigned mode) { + constexpr std::size_t page = 65536; + auto& mappings = GuestArena::WindowsMappings::Get(); + auto* base = static_cast(mappings.Reserve(nullptr, 3 * page)); + Require(base != nullptr); + const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, 3 * page, nullptr); + Require(section != nullptr); + auto* address = base + page; + mappings.Map(address, page, section, 0, PAGE_READWRITE, true, true); + Require(VirtualFree(address, 0, MEM_RELEASE)); + if (mode == 0) { + Require(VirtualFree(base, 0, MEM_RELEASE)); + Require(VirtualFree(base + 2 * page, 0, MEM_RELEASE)); + Require(VirtualAlloc(base, 3 * page, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) == base); + } else if (mode == 1) { + Require(VirtualFree(base + 2 * page, 0, MEM_RELEASE)); + Require(VirtualAlloc(address, 2 * page, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) == address); + } else { + Require(MapViewOfFileEx(section, FILE_MAP_ALL_ACCESS, 0, 0, page, address) == address); + } + address[0] = 73; + bool rejected = false; + try { mappings.Reset(address, page); } + catch (const std::runtime_error&) { rejected = true; } + std::uint32_t protection = 0; + Require(rejected && address[0] == 73 && mappings.Protection(reinterpret_cast(address), &protection)); + if (mode == 2) Require(UnmapViewOfFile(address)); + else Require(VirtualFree(mode == 0 ? base : address, 0, MEM_RELEASE)); + CloseHandle(section); +} + +static void CheckValidRelease() { + constexpr std::size_t bytes = 2 * 1024 * 1024; + auto& mappings = GuestArena::WindowsMappings::Get(); + auto* reserved = static_cast(mappings.Reserve(nullptr, 2 * bytes)); + Require(reserved != nullptr); + const auto start = (reinterpret_cast(reserved) + bytes - 1) & ~static_cast(bytes - 1); + auto* base = reinterpret_cast(start); + const auto oneRun = [&] { + std::uintptr_t first = 0; + std::uintptr_t last = 0; + return mappings.ResidentSpan(start + bytes - 1, 1, &first, &last) && first == start && last == start + bytes; + }; + const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, bytes, nullptr); + Require(section != nullptr); + mappings.Map(base, bytes, section, 0, PAGE_READWRITE, true, true); + Require(oneRun()); + base[0] = 31; + base[bytes - 1] = 89; + DWORD previous; + Require(VirtualProtect(base + 65536, 65536, PAGE_READONLY, &previous)); + mappings.Reset(base, bytes); + mappings.Map(base, bytes, section, 0, PAGE_READWRITE, true, true); + Require(oneRun()); + Require(base[0] == 31 && base[bytes - 1] == 89); + mappings.Reset(base, bytes); + Require(VirtualFree(reserved, 2 * bytes, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)); + Require(VirtualFree(reserved, 0, MEM_RELEASE)); + mappings.ForgetSection(section); + CloseHandle(section); +} + +int main(int argc, char** argv) { + try { + if (argc == 2) CheckMetadataMismatch(static_cast(argv[1][0] - '0')); + else CheckValidRelease(); + } catch (const std::exception& failure) { + std::fprintf(stderr, "%s\n", failure.what()); + return 1; + } + return 0; +} diff --git a/core/libs/tests/WriteWatch.cpp b/core/libs/tests/WriteWatch.cpp index 045b6c10b0..0588e469d0 100644 --- a/core/libs/tests/WriteWatch.cpp +++ b/core/libs/tests/WriteWatch.cpp @@ -170,6 +170,43 @@ void CheckHostWrite() { ::close(descriptors[1]); } +void CheckEmptyRanges() { + Region region(1); + for (const auto offset : {std::size_t{0}, std::size_t{1}, region.page - 1}) { + auto* pointer = region.base + offset; + GuestWriteWatchRegister_nid_postfix(pointer, 0); + Require(!GuestWriteWatchCovers_nid_postfix(region.Address(0), region.bytes), "an empty registration watches a page"); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(region.Collect() == All(1), "a new range is not reported in full"); + + Require(!GuestWriteWatchUnregister_nid_postfix(pointer, 0), "an empty unregister removes a page"); + Require(GuestWriteWatchCovers_nid_postfix(region.Address(0), region.bytes), "an empty unregister changes coverage"); + GuestWriteWatchRegister_nid_postfix(pointer, 0); + Require(region.Collect().empty(), "an empty registration marks a watched page fresh"); + + GuestWriteWatchBeginHostWrite_nid_postfix(pointer, 0); + Require(KernelProtection(region.base) == PROT_READ, "an empty host write unprotects a page"); + Require(region.Collect().empty(), "an empty host write reports a dirty page"); + GuestWriteWatchEndHostWrite_nid_postfix(pointer, 0); + + GuestWriteWatchBeginHostWrite_nid_postfix(pointer, 1); + GuestWriteWatchEndHostWrite_nid_postfix(pointer, 0); + Require(region.Collect() == All(1), "an active host write is not reported"); + Require(KernelProtection(region.base) == (PROT_READ | PROT_WRITE), "an empty end host write releases an active host write"); + GuestWriteWatchEndHostWrite_nid_postfix(pointer, 1); + Require(region.Collect() == All(1), "a finished host write is not reported"); + Require(region.Collect().empty(), "a finished host write is reported again"); + + GuestWriteWatchProtectionChanged_nid_postfix(pointer, 0, PROT_NONE); + std::uintptr_t runEnd = 0; + Require(GuestWriteWatchProtection_nid_postfix(region.Address(0), region.Address(1), &runEnd) == (PROT_READ | PROT_WRITE), "an empty protection change changes the recorded protection"); + Require(region.Collect().empty(), "an empty protection change reports a dirty page"); + region.base[offset] = 1; + Require(region.Collect() == All(1), "an empty range operation prevents a later write from being tracked"); + Require(GuestWriteWatchUnregister_nid_postfix(region.base, region.bytes), "cannot unregister the test region"); + } +} + void CheckProtection() { Region region(6); std::uintptr_t runEnd = 0; @@ -194,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; @@ -232,7 +284,7 @@ void CheckThreads() { int main(int argc, char** argv) { try { - Require(argc == 2, "expected collect, fresh, armed, register, mapped, host, protection or threads"); + Require(argc == 2, "expected collect, fresh, armed, register, mapped, host, empty, protection, overlaps or threads"); const std::string_view mode(argv[1]); if (mode == "collect") CheckCollect(); else if (mode == "fresh") CheckFresh(); @@ -240,7 +292,9 @@ int main(int argc, char** argv) { else if (mode == "register") CheckRegisterAgain(); else if (mode == "mapped") CheckRegisterMapped(); else if (mode == "host") CheckHostWrite(); + else if (mode == "empty") CheckEmptyRanges(); else if (mode == "protection") CheckProtection(); + else if (mode == "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/guest_arena/CMakeLists.txt b/core/libs/tests/guest_arena/CMakeLists.txt new file mode 100644 index 0000000000..e9a10f6a4c --- /dev/null +++ b/core/libs/tests/guest_arena/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.20) + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + project(GuestArenaTests LANGUAGES CXX) + enable_testing() + set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") +endif() + +find_package(Threads REQUIRED) +add_executable(guest_arena_tests ../GuestArenaHost.cpp ../../prx/libc/src/GuestArena.cpp ../../prx/libc/src/GuestWriteWatch.cpp) +target_include_directories(guest_arena_tests PRIVATE ../..) +target_compile_features(guest_arena_tests PRIVATE cxx_std_20) +target_link_libraries(guest_arena_tests PRIVATE Threads::Threads) +set_target_properties(guest_arena_tests PROPERTIES + CXX_EXTENSIONS OFF + RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") + +foreach(mode available unmappable host gap commit) + add_test(NAME guest_arena_${mode} COMMAND guest_arena_tests ${mode}) + set_tests_properties(guest_arena_${mode} PROPERTIES TIMEOUT 30) +endforeach() diff --git a/core/libs/tests/numeric-locale/comma.charmap b/core/libs/tests/numeric-locale/comma.charmap new file mode 100644 index 0000000000..cbfea3c784 --- /dev/null +++ b/core/libs/tests/numeric-locale/comma.charmap @@ -0,0 +1,135 @@ + ANYPS5_COMMA + % + / + 1 + 1 +CHARMAP + /x00 + /x01 + /x02 + /x03 + /x04 + /x05 + /x06 + /x07 + /x08 + /x09 + /x0a + /x0b + /x0c + /x0d + /x0e + /x0f + /x10 + /x11 + /x12 + /x13 + /x14 + /x15 + /x16 + /x17 + /x18 + /x19 + /x1a + /x1b + /x1c + /x1d + /x1e + /x1f + /x20 + /x21 + /x22 + /x23 + /x24 + /x25 + /x26 + /x27 + /x28 + /x29 + /x2a + /x2b + /x2c + /x2d + /x2e + /x2f + /x30 + /x31 + /x32 + /x33 + /x34 + /x35 + /x36 + /x37 + /x38 + /x39 + /x3a + /x3b + /x3c + /x3d + /x3e + /x3f + /x40 + /x41 + /x42 + /x43 + /x44 + /x45 + /x46 + /x47 + /x48 + /x49 + /x4a + /x4b + /x4c + /x4d + /x4e + /x4f + /x50 + /x51 + /x52 + /x53 + /x54 + /x55 + /x56 + /x57 + /x58 + /x59 + /x5a + /x5b + /x5c + /x5d + /x5e + /x5f + /x60 + /x61 + /x62 + /x63 + /x64 + /x65 + /x66 + /x67 + /x68 + /x69 + /x6a + /x6b + /x6c + /x6d + /x6e + /x6f + /x70 + /x71 + /x72 + /x73 + /x74 + /x75 + /x76 + /x77 + /x78 + /x79 + /x7a + /x7b + /x7c + /x7d + /x7e + /x7f +END CHARMAP diff --git a/core/libs/tests/numeric-locale/comma.locale b/core/libs/tests/numeric-locale/comma.locale new file mode 100644 index 0000000000..3207685c62 --- /dev/null +++ b/core/libs/tests/numeric-locale/comma.locale @@ -0,0 +1,5 @@ +LC_NUMERIC +decimal_point "" +thousands_sep "" +grouping -1 +END LC_NUMERIC 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/test_semaphore_trace.py b/core/libs/tests/test_semaphore_trace.py new file mode 100644 index 0000000000..004692b8ce --- /dev/null +++ b/core/libs/tests/test_semaphore_trace.py @@ -0,0 +1,46 @@ +import os +import re +import subprocess +import sys + + +def run(executable, enabled): + environment = os.environ.copy() + environment.pop("APS5_TRACE_SYNC", None) + if enabled: + environment["APS5_TRACE_SYNC"] = "1" + return subprocess.run([executable], capture_output=True, check=False, env=environment, text=True, timeout=15) + + +def main(): + executable = sys.argv[1] + quiet = run(executable, False) + if quiet.returncode != 0 or "[sema]" in quiet.stderr: + raise SystemExit("semaphore test failed or traced without APS5_TRACE_SYNC") + + traced = run(executable, True) + if traced.returncode != 0: + raise SystemExit(traced.stderr or f"semaphore test exited {traced.returncode}") + + lines = traced.stderr.splitlines() + create = next((line for line in lines if "[sema] create " in line and "'trace'" in line), None) + if create is None: + raise SystemExit("semaphore creation trace is missing") + handle_match = re.search(r"\[sema\] create (\S+)", create) + if handle_match is None: + raise SystemExit("semaphore creation trace has no handle") + handle = handle_match.group(1) + expected = { + "signal": f"[sema] signal {handle} 'trace'", + "wait begin": f"[sema] wait begin {handle} 'trace'", + "wait end": f"[sema] wait end {handle} 'trace'", + } + for event, prefix in expected.items(): + if not any(line.startswith(prefix) for line in lines): + raise SystemExit(f"semaphore {event} trace for the created handle is missing") + if not any(line.startswith(expected["wait begin"]) and "timeout=-1" in line for line in lines): + raise SystemExit("indefinite semaphore wait is not identified in the trace") + + +if __name__ == "__main__": + main() diff --git a/core/libs/tests/write_watch/CMakeLists.txt b/core/libs/tests/write_watch/CMakeLists.txt index 0824ee5009..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 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 b5a2a2b685..4c9e165fc0 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -31,6 +31,7 @@ add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileIdentity.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/Amd64OnlyConverter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/RedZoneFramer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp @@ -51,11 +52,14 @@ add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp ${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 ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CodeInstructionCollector.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SceCodeExtent.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/RelativeJumpTables.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp @@ -68,9 +72,13 @@ add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/ValidationPolicy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/output/SysVDynamicSectionBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/output/CallRegistryWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/patching/GamePatch.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/pipeline/Conversion.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/pipeline/RelinkerPipeline.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/src/NidCompute.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/src/Sha1.cpp ) +target_include_directories(relinker_core PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/include) target_include_directories(relinker_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include @@ -81,12 +89,24 @@ target_include_directories(relinker_core PUBLIC target_compile_features(relinker_core PUBLIC cxx_std_20) set_target_properties(relinker_core PROPERTIES CXX_EXTENSIONS OFF) +if(WIN32) + set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileIdentity.cpp PROPERTIES COMPILE_DEFINITIONS _WIN32_WINNT=0x0602) +endif() if(BUILD_TESTING) + add_test_executable(andn_lowering_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/AndnLoweringTests.cpp) + 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) set_tests_properties(relinker_conversion PROPERTIES TIMEOUT 20) + add_test_executable(relinker_game_patch_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/GamePatchTests.cpp) + target_link_libraries(relinker_game_patch_tests PRIVATE relinker_core) + add_test(NAME relinker_game_patch COMMAND relinker_game_patch_tests) + set_tests_properties(relinker_game_patch PROPERTIES TIMEOUT 30) endif() if(ANYPS5_BUILD_WASM) @@ -141,6 +161,16 @@ if(WIN32) add_test(NAME windows_dependency_diagnostics COMMAND windows_dependency_diagnostics_tests) endif() + if(NOT ANYPS5_RELINKER_ONLY) + add_test_executable(windows_import_identity_tests EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsImportIdentityTests.cpp) + target_link_libraries(windows_import_identity_tests PRIVATE relinker_core) + if(BUILD_TESTING) + set_target_properties(windows_import_identity_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_dependencies(windows_import_identity_tests libs) + add_test(NAME windows_import_identity COMMAND windows_import_identity_tests "${CMAKE_BINARY_DIR}/core/libs/libs") + endif() + endif() + add_test_executable(amd64_only_windows_tests EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/Amd64OnlyWindowsTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp @@ -168,6 +198,8 @@ if(WIN32) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp ${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) @@ -217,6 +249,11 @@ if(BUILD_TESTING) add_test(NAME sdk_mman_syscall COMMAND sdk_mman_syscall_tests) set_tests_properties(sdk_mman_syscall PROPERTIES TIMEOUT 20) + add_test_executable(runtime_layout_tests ${CMAKE_CURRENT_SOURCE_DIR}/cli/tests/RuntimeLayoutTests.cpp) + target_include_directories(runtime_layout_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/cli/include) + add_test(NAME runtime_layout COMMAND runtime_layout_tests) + set_tests_properties(runtime_layout PROPERTIES TIMEOUT 20) + add_test_executable(autorun_exit_tests ${CMAKE_CURRENT_SOURCE_DIR}/cli/tests/AutorunExitTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp @@ -278,6 +315,14 @@ if(BUILD_TESTING) target_include_directories(buffer_bounds_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/io/include) add_test(NAME buffer_bounds COMMAND buffer_bounds_tests) + add_test_executable(file_identity_tests + ${CMAKE_CURRENT_SOURCE_DIR}/io/tests/FileIdentityTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileIdentity.cpp + ) + target_include_directories(file_identity_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/io/include) + add_test(NAME file_identity COMMAND file_identity_tests) + set_tests_properties(file_identity PROPERTIES TIMEOUT 20) + add_test_executable(param_json_utf8_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libs/tests/ParamJsonParser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp) @@ -329,6 +374,29 @@ if(BUILD_TESTING) add_test(NAME section_header_ranges COMMAND section_header_ranges_tests) set_tests_properties(section_header_ranges PROPERTIES TIMEOUT 20) + add_test_executable(linux_load_pages_tests + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/LinuxLoadPageTests.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 + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/SectionHeaderTableBuilder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/StubBodyBuilder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/src/NidCompute.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/src/Sha1.cpp + ) + target_include_directories(linux_load_pages_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/include + ) + add_test(NAME linux_load_pages COMMAND linux_load_pages_tests) + set_tests_properties(linux_load_pages PROPERTIES TIMEOUT 20) + add_test_executable(amd64_only_converter_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/Amd64OnlyConverterTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/Amd64OnlyConverter.cpp @@ -350,6 +418,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp ${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 @@ -357,13 +427,19 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/SectionHeaderTableBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/src/NidCompute.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/src/Sha1.cpp ) target_include_directories(amd64_only_converter_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/nid/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 @@ -394,6 +470,9 @@ if(BUILD_TESTING) add_test(NAME windows_fixed_base COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_fixed_base.py $) set_tests_properties(windows_fixed_base PROPERTIES TIMEOUT 20) + add_test(NAME linux_gnu_segments COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_gnu_segments.py $) + set_tests_properties(linux_gnu_segments PROPERTIES TIMEOUT 20) add_test(NAME elf_offsets COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_offsets.py $) add_test(NAME conversion_diagnostics COMMAND ${Python3_EXECUTABLE} @@ -408,10 +487,28 @@ if(BUILD_TESTING) add_test(NAME guest_none_relocations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_none_relocations.py $) set_tests_properties(guest_none_relocations PROPERTIES TIMEOUT 20) + add_test(NAME interpreter_in_code COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_interpreter_in_code.py $) + set_tests_properties(interpreter_in_code PROPERTIES TIMEOUT 20) + add_test(NAME guest_relative_section_symbols COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_relative_section_symbols.py $) + set_tests_properties(guest_relative_section_symbols PROPERTIES TIMEOUT 20) + add_test(NAME overlapping_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_overlapping_relocations.py $) + set_tests_properties(overlapping_relocations PROPERTIES TIMEOUT 30) + add_test(NAME guest_own_tls_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_own_tls_relocations.py $) + set_tests_properties(guest_own_tls_relocations PROPERTIES TIMEOUT 30) add_test(NAME optional_plt COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) + add_test(NAME optional_rela COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_rela.py $) add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_empty_tls.py $) + add_test(NAME windows_tls_zero_fill COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_tls_zero_fill.py $) + add_test(NAME windows_tls_protected_entries COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_tls_protected_entries.py $) set(tlsCoverageGroups metadata padding register_loads displacement_bounds alu_execution register_address_loads moved_branch rejected) 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}) @@ -419,6 +516,7 @@ if(BUILD_TESTING) foreach(opcode IN ITEMS add or adc sbb and sub xor cmp) list(APPEND tlsCoverageGroups alu_${opcode}) endforeach() + list(APPEND tlsCoverageGroups gs_traps gs_traps_implicit) foreach(group IN LISTS tlsCoverageGroups) add_test(NAME tls_function_coverage_${group} COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_tls_function_coverage.py $ ${group}) @@ -426,6 +524,12 @@ if(BUILD_TESTING) endforeach() add_test(NAME tls_diagnostics COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_tls_diagnostics.py $) + add_test(NAME call_zero_padding COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_call_zero_padding.py $) + set_tests_properties(call_zero_padding PROPERTIES TIMEOUT 60) + add_test(NAME macos_tls_alu COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_tls_alu.py $) + set_tests_properties(macos_tls_alu PROPERTIES TIMEOUT 60) add_test(NAME linux_load_alignment COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_load_alignment.py $) add_test(NAME linux_eh_frame_header COMMAND ${Python3_EXECUTABLE} @@ -440,6 +544,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_executable_exports.py $) add_test(NAME executable_tls_relocations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_tls_relocations.py $) + add_test(NAME macos_empty_tls COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_empty_tls.py $) + set_tests_properties(macos_empty_tls PROPERTIES TIMEOUT 60) add_test(NAME linux_entry_argv COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_entry_argv.py $) add_test(NAME linux_fixed_base COMMAND ${Python3_EXECUTABLE} @@ -457,12 +564,16 @@ if(BUILD_TESTING) set_tests_properties(windows_runpath PROPERTIES TIMEOUT 30) add_test(NAME windows_trace_imports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_trace_imports.py $) + add_test(NAME linux_trace_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_trace_imports.py $) add_test(NAME windows_address_space COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_address_space.py $) add_test(NAME windows_tls_thread_pointer COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_tls_thread_pointer.py $) add_test(NAME windows_bss COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_bss.py $) + add_test(NAME windows_sparse_load COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_sparse_load.py $) add_test(NAME windows_icon COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_icon.py $) add_test(NAME linux_desktop_entry_output COMMAND ${Python3_EXECUTABLE} @@ -481,6 +592,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_directories.py $) add_test(NAME relinker_executables COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executables.py $) + add_test(NAME game_patches COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_game_patches.py $) add_test(NAME sce_module_path COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_module_path.py $) add_test(NAME linux_guest_stack COMMAND ${Python3_EXECUTABLE} @@ -489,10 +602,25 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_symbol_names.py $) add_test(NAME guest_registry COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_registry.py $) + add_test(NAME needed_libraries COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_needed_libraries.py $) + set_tests_properties(needed_libraries PROPERTIES TIMEOUT 30) add_test(NAME macos_output COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_output.py $) + add_test(NAME macos_tls_overlap COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_tls_overlap.py $) + set_tests_properties(macos_tls_overlap PROPERTIES TIMEOUT 60) + add_test(NAME macos_gs_traps COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_gs_traps.py $) + set_tests_properties(macos_gs_traps PROPERTIES TIMEOUT 60) + add_test(NAME macos_fixed_base COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_fixed_base.py $) add_test(NAME guest_host_libc COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_host_libc.py $) + add_test(NAME windows_named_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_named_imports.py $) + add_test(NAME linux_named_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_named_imports.py $) add_library(guest_module_args_host SHARED elfpatcher/tests/GuestModuleArgsHost.cpp) target_include_directories(guest_module_args_host PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../libs) set_target_properties(guest_module_args_host PROPERTIES @@ -500,15 +628,33 @@ if(BUILD_TESTING) add_test(NAME guest_module_args COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_args.py $ $) + add_test(NAME guest_deferred_dependencies COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_deferred_dependencies.py + $ $) add_test(NAME guest_absolute_symbols COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $) add_test(NAME guest_module_identity COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_identity.py $) + add_test(NAME guest_unused_import_dependencies COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_unused_import_dependencies.py + $ $) + set_property(TEST guest_unused_import_dependencies APPEND PROPERTY LABELS relinker) add_test(NAME guest_export_lookup COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_export_lookup.py $) set_tests_properties(guest_export_lookup PROPERTIES TIMEOUT 30) + 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 $) + add_test(NAME guest_zero_initializer COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_zero_initializer.py + $ $) + set_tests_properties(guest_zero_initializer PROPERTIES TIMEOUT 30) + add_asan_test_options(guest_zero_initializer verify_asan_link_order=0) set(windows_stdio_host_arguments) if(WIN32) if(MINGW) @@ -550,10 +696,16 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_header.py $) add_test(NAME weak_imports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_weak_imports.py $) + add_test(NAME windows_guest_weak_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_guest_weak_imports.py $) add_test(NAME input_magic COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_magic.py $) add_test(NAME input_preservation COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_preservation.py $) + add_test(NAME output_collisions COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_output_collisions.py $) + add_test(NAME secondary_output_collisions COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_secondary_output_collisions.py $) add_test(NAME input_file COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_file.py $) add_test(NAME null_symbol_relocations COMMAND ${Python3_EXECUTABLE} @@ -569,6 +721,10 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_code_extent.py $) add_test(NAME sce_relro COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_relro.py $) + add_test(NAME sce_module_code_extent COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_module_code_extent.py $) + add_test(NAME sce_code_gaps COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_code_gaps.py $) add_test(NAME call_registry COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_call_registry.py $) add_test(NAME jmprel_table_bounds COMMAND ${Python3_EXECUTABLE} @@ -593,6 +749,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_syscall_number.py $) add_test(NAME unused_filter_indirect COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_unused_filter_indirect.py $) + add_test(NAME unused_filter_fallthrough_padding COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_unused_filter_fallthrough_padding.py $) else() message(STATUS "Python3 interpreter not found; skipping Python relinker tests") endif() @@ -624,18 +782,17 @@ if(BUILD_TESTING) ) add_test(NAME windows_icon_resource COMMAND windows_icon_resource_tests) - add_test_executable(entry_stub_tests - ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/EntryStubTests.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/EntryStubBuilder.cpp - ) - target_include_directories(entry_stub_tests PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include - ${CMAKE_CURRENT_SOURCE_DIR}/domain/include - ) + add_test_executable(windows_trampoline_builder_tests ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsTrampolineBuilderTests.cpp) + target_link_libraries(windows_trampoline_builder_tests PRIVATE relinker_core) + add_test(NAME windows_trampoline_builder COMMAND windows_trampoline_builder_tests) + + add_test_executable(entry_stub_tests ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/EntryStubTests.cpp) + target_link_libraries(entry_stub_tests PRIVATE relinker_core) add_test(NAME entry_stub COMMAND entry_stub_tests) add_test_executable(control_flow_graph_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ControlFlowGraphTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/RelativeJumpTables.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp @@ -703,4 +860,19 @@ 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_executable_exports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_executable_exports.py $) + set_tests_properties(macos_executable_exports PROPERTIES TIMEOUT 30 LABELS relinker) + add_test(NAME macos_guest_weak_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_guest_weak_imports.py $) + set_property(TEST macos_guest_weak_imports APPEND PROPERTY LABELS relinker) endif() diff --git a/core/relinker/cli/include/Cli.hpp b/core/relinker/cli/include/Cli.hpp index 68c5c86753..2625b44aac 100644 --- a/core/relinker/cli/include/Cli.hpp +++ b/core/relinker/cli/include/Cli.hpp @@ -27,10 +27,15 @@ struct Args { std::optional windowsFixedBase; bool traceImports = false; std::optional linuxFixedBase; + std::optional macosFixedBase; std::uint32_t unusedFilterLevel = 0; std::string inputPath; std::string outputPath; std::string sceModulePath; + std::string patchesPath; + bool noPatches = false; + std::string verifyPatchesPath; + bool verifyOutput = false; std::string runPath = "$ORIGIN/libs"; std::set excludedSceModules; std::vector moduleDirectories; 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/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index 7e809c530f..47166d9972 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -5,18 +5,37 @@ #include #include #include +#include +#include #include #include namespace Cli { const char* Usage() { - return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--trace-imports] [--windows-fixed-base
] [--linux-fixed-base
] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... [--to-intel] [--to-rosetta] [--frame-red-zone] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" + return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--trace-imports] [--windows-fixed-base
] [--linux-fixed-base
] [--macos-fixed-base
] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... [--to-intel] [--to-rosetta] [--frame-red-zone] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] [--patches | --no-patches] \n" + " relinker --verify-patches [--verify-output] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... \n" "Example: relinker input.elf output.elf"; } -static std::uint64_t ParseLinuxFixedBase(std::string_view value) { - constexpr std::uint64_t lowest = 0x10000; +namespace { + +constexpr std::string_view kVerifyRejectedOptions[] = { + "--windows", "--macos", "--windows-gui", "--windows-diagnostics", "--windows-fixed-base", "--trace-imports", "--linux-fixed-base", "--macos-fixed-base", + "--to-intel", "--to-rosetta", "--skip-syscall-check", "--trap-sdk-syscall-stubs", "unused-filter=", + "--rpath", "--registry", "--lazy-binding", "--autorun", "--patches", "--no-patches", +}; + +constexpr std::string_view kModuleDiscoveryOptions[] = {"--sce-module-path", "--module-dir", "--exclude-sce-module", "--skip-sce-module"}; + +template +bool IsOneOf(const std::string& option, const std::string_view (&names)[Count]) { + return std::find(std::begin(names), std::end(names), option) != std::end(names); +} + +} + +static std::uint64_t ParseFixedBase(std::string_view value, const std::uint64_t lowest, const char* error) { constexpr std::uint64_t highest = 0x7fffffff0000; constexpr std::uint64_t alignment = 0x10000; const bool hexadecimal = value.starts_with("0x") || value.starts_with("0X"); @@ -24,7 +43,7 @@ static std::uint64_t ParseLinuxFixedBase(std::string_view value) { std::uint64_t address = 0; const auto parsed = std::from_chars(value.data(), value.data() + value.size(), address, hexadecimal ? 16 : 10); if (value.empty() || parsed.ec != std::errc{} || parsed.ptr != value.data() + value.size() || address < lowest || address > highest || address % alignment != 0) - throw std::runtime_error("--linux-fixed-base requires a 64 KiB-aligned address from 0x10000 through 0x7fffffff0000"); + throw std::runtime_error(error); return address; } @@ -32,9 +51,12 @@ Args ParseArgs(int argc, char* argv[]) { Args args; bool unusedFilterSpecified = false; bool sceModulePathSpecified = false; + std::vector givenOptions; for (int i = 1; i < argc; ++i) { const std::string arg = argv[i]; + if (arg.rfind("--", 0) == 0) givenOptions.push_back(arg); + else if (arg.rfind("unused-filter=", 0) == 0) givenOptions.push_back("unused-filter="); if (arg == "--help") { args.showHelp = true; } else if (arg == "--windows-fixed-base") { @@ -53,7 +75,12 @@ Args ParseArgs(int argc, char* argv[]) { if (args.linuxFixedBase) throw std::runtime_error("--linux-fixed-base cannot be repeated"); if (i + 1 >= argc || std::string_view(argv[i + 1]).empty() || std::string_view(argv[i + 1]).starts_with("--")) throw std::runtime_error("--linux-fixed-base requires an address"); - args.linuxFixedBase = ParseLinuxFixedBase(argv[++i]); + args.linuxFixedBase = ParseFixedBase(argv[++i], 0x10000, "--linux-fixed-base requires a 64 KiB-aligned address from 0x10000 through 0x7fffffff0000"); + } else if (arg == "--macos-fixed-base") { + if (args.macosFixedBase) throw std::runtime_error("--macos-fixed-base cannot be repeated"); + if (i + 1 >= argc || std::string_view(argv[i + 1]).empty() || std::string_view(argv[i + 1]).starts_with("--")) + throw std::runtime_error("--macos-fixed-base requires an address"); + args.macosFixedBase = ParseFixedBase(argv[++i], 0x20000, "--macos-fixed-base requires a 64 KiB-aligned address from 0x20000 through 0x7fffffff0000"); } else if (arg == "--skip-syscall-check") { args.skipSyscallCheck = true; } else if (arg == "--trap-sdk-syscall-stubs") { @@ -89,6 +116,18 @@ Args ParseArgs(int argc, char* argv[]) { throw std::runtime_error("unused-filter must be specified once with a value of 0, 1 or 2"); args.unusedFilterLevel = static_cast(value[0] - '0'); unusedFilterSpecified = true; + } else if (arg == "--patches") { + if (!args.patchesPath.empty()) throw std::runtime_error("--patches cannot be repeated"); + if (i + 1 >= argc || std::string(argv[i + 1]).empty() || std::string(argv[i + 1]).starts_with("--")) throw std::runtime_error("--patches requires a file"); + args.patchesPath = argv[++i]; + } else if (arg == "--no-patches") { + args.noPatches = true; + } else if (arg == "--verify-patches") { + if (!args.verifyPatchesPath.empty()) throw std::runtime_error("--verify-patches cannot be repeated"); + if (i + 1 >= argc || std::string(argv[i + 1]).empty() || std::string(argv[i + 1]).starts_with("--")) throw std::runtime_error("--verify-patches requires a file"); + args.verifyPatchesPath = argv[++i]; + } else if (arg == "--verify-output") { + args.verifyOutput = true; } else if (arg == "--registry") { args.writeRegistry = true; } else if (arg == "--rpath") { @@ -122,6 +161,19 @@ Args ParseArgs(int argc, char* argv[]) { } } + const bool verifying = !args.verifyPatchesPath.empty(); + if (args.verifyOutput && !verifying) throw std::runtime_error("--verify-output requires --verify-patches"); + if (!args.patchesPath.empty() && args.noPatches) throw std::runtime_error("--patches conflicts with --no-patches"); + if (verifying) { + for (const auto& option : givenOptions) { + if (IsOneOf(option, kVerifyRejectedOptions)) + throw std::runtime_error(option + " cannot be used with --verify-patches"); + if (args.verifyOutput && IsOneOf(option, kModuleDiscoveryOptions)) + throw std::runtime_error("module discovery options have no effect with --verify-output"); + } + if (!args.outputPath.empty()) throw std::runtime_error("unexpected argument: " + args.outputPath); + } + if (args.skipSceModule && sceModulePathSpecified) throw std::runtime_error("--sce-module-path conflicts with --skip-sce-module"); if (args.skipSyscallCheck && args.trapSdkSyscallStubs) throw std::runtime_error("--skip-syscall-check conflicts with --trap-sdk-syscall-stubs"); @@ -147,20 +199,22 @@ Args ParseArgs(int argc, char* argv[]) { if (args.windowsFixedBase && !args.toWindows) throw std::runtime_error("--windows-fixed-base requires --windows"); - if (args.traceImports && !args.toWindows) - throw std::runtime_error("--trace-imports requires --windows"); + if (args.traceImports && args.toMacos) + throw std::runtime_error("--trace-imports cannot be used with --macos"); if (args.traceImports && args.lazyBinding) throw std::runtime_error("--trace-imports conflicts with --lazy-binding"); if (args.linuxFixedBase && (args.toWindows || args.toMacos)) throw std::runtime_error("--linux-fixed-base cannot be used with --windows or --macos"); if (args.linuxFixedBase && args.lazyBinding) throw std::runtime_error("--linux-fixed-base conflicts with --lazy-binding"); + if (args.macosFixedBase && !args.toMacos) + throw std::runtime_error("--macos-fixed-base requires --macos"); - if (!args.showHelp && (args.inputPath.empty() || args.outputPath.empty())) + if (!args.showHelp && (args.inputPath.empty() || (!verifying && args.outputPath.empty()))) throw std::runtime_error(Usage()); - if (!sceModulePathSpecified && !args.skipSceModule && !args.inputPath.empty()) args.sceModulePath = Io::Utf8Path(std::filesystem::absolute(Io::NativePath(args.inputPath)).parent_path()); + if (!sceModulePathSpecified && !args.skipSceModule && !args.inputPath.empty() && !args.verifyOutput) args.sceModulePath = Io::Utf8Path(std::filesystem::absolute(Io::NativePath(args.inputPath)).parent_path()); return args; } diff --git a/core/relinker/cli/src/NativeModules.cpp b/core/relinker/cli/src/NativeModules.cpp index 4943091c64..f01ac374d4 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -1,12 +1,19 @@ #include #include #include +#include #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 +27,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 +39,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); } @@ -58,7 +65,7 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil continue; } if (!entry.is_regular_file()) continue; - if (std::filesystem::equivalent(entry.path(), inputPath)) continue; + if (Io::SameFile(entry.path(), inputPath)) continue; bool self = false; if (isElf(entry.path(), self)) paths.push_back(entry.path()); else if (self) @@ -72,6 +79,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, true).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/cli/tests/CliArgsTests.cpp b/core/relinker/cli/tests/CliArgsTests.cpp index 6d0bd857a3..8f18c4c963 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -241,6 +241,24 @@ void linuxFixedBase() { expectThrows({"--lazy-binding", "--linux-fixed-base", "0x400000", "input.elf", "output.elf"}, "--linux-fixed-base conflicts with --lazy-binding"); } +void macosFixedBase() { + require(!parse({"--macos", "input.elf", "output"}).macosFixedBase, "fixed base was enabled by default"); + for (const auto& value : {"0x400000", "0X400000", "4194304"}) + require(parse({"--macos-fixed-base", value, "--macos", "input.elf", "output"}).macosFixedBase == 0x400000, "fixed base was parsed incorrectly"); + require(parse({"--macos", "input.elf", "--macos-fixed-base", "0x20000", "output"}).macosFixedBase == 0x20000, "lowest fixed base was rejected"); + require(parse({"--macos", "--macos-fixed-base", "0x7fffffff0000", "input.elf", "output"}).macosFixedBase == 0x7fffffff0000, "highest fixed base was rejected"); + require(parse({"--macos", "--macos-fixed-base", "0x400000000", "input.elf", "output"}).macosFixedBase == 0x400000000, "fixed base was truncated to 32 bits"); + const std::string range = "--macos-fixed-base requires a 64 KiB-aligned address from 0x20000 through 0x7fffffff0000"; + for (const auto& value : {"0", "0x10000", "0x400001", "0x408000", "0x", "-4194304", "+4194304", " 4194304", "0x400000junk", "18446744073709551616", "0x800000000000"}) + expectThrows({"--macos", "--macos-fixed-base", value, "input.elf", "output"}, range); + expectThrows({"--macos-fixed-base"}, "--macos-fixed-base requires an address"); + expectThrows({"--macos-fixed-base", "", "--macos", "input.elf", "output"}, "--macos-fixed-base requires an address"); + expectThrows({"--macos-fixed-base", "--macos", "input.elf", "output"}, "--macos-fixed-base requires an address"); + expectThrows({"--macos", "--macos-fixed-base", "0x400000", "--macos-fixed-base", "0x400000", "input.elf", "output"}, "--macos-fixed-base cannot be repeated"); + expectThrows({"--macos-fixed-base", "0x400000", "input.elf", "output.elf"}, "--macos-fixed-base requires --macos"); + expectThrows({"--windows", "--macos-fixed-base", "0x400000", "input.elf", "output.exe"}, "--macos-fixed-base requires --macos"); +} + } void moduleDirectories() { @@ -278,6 +296,44 @@ void targetSelection() { expectThrows({"--to-rosetta", "--windows", "input.elf", "output.elf"}, rosettaConflict); } +void patchOptions() { + auto args = parse({"--patches", "p.toml", "in.elf", "out.elf"}); + require(args.patchesPath == "p.toml" && !args.noPatches, "--patches value"); + args = parse({"--no-patches", "in.elf", "out.elf"}); + require(args.noPatches && args.patchesPath.empty(), "--no-patches"); + expectThrows({"--patches", "p.toml", "--no-patches", "in.elf", "out.elf"}, "--patches conflicts with --no-patches"); + expectThrows({"--patches", "p.toml", "--patches", "q.toml", "in.elf", "out.elf"}, "--patches cannot be repeated"); + expectThrows({"in.elf", "out.elf", "--patches"}, "--patches requires a file"); +} + +void verifyMode() { + auto args = parse({"--verify-patches", "p.toml", "in.elf"}); + require(args.verifyPatchesPath == "p.toml" && args.inputPath == "in.elf" && args.outputPath.empty() && !args.verifyOutput, "verify positional"); + require(!args.sceModulePath.empty(), "input mode keeps the default module path"); + args = parse({"in.elf", "--verify-patches", "p.toml", "--sce-module-path", "m", "--module-dir", "d", "--exclude-sce-module", "x.prx", "--executable", "a/b.bin"}); + require(args.sceModulePath == "m" && args.executables.size() == 1, "input mode options"); + args = parse({"--verify-patches", "p.toml", "--skip-sce-module", "in.elf"}); + require(args.skipSceModule, "input mode --skip-sce-module"); + args = parse({"--verify-patches", "p.toml", "--verify-output", "out.elf", "--executable", "a/b.bin"}); + require(args.verifyOutput && args.sceModulePath.empty() && args.executables.size() == 1, "output mode accepts --executable"); + const std::string noEffect = "module discovery options have no effect with --verify-output"; + expectThrows({"--verify-patches", "p.toml", "--verify-output", "out.elf", "--sce-module-path", "m"}, noEffect); + expectThrows({"--verify-patches", "p.toml", "--verify-output", "out.elf", "--module-dir", "d"}, noEffect); + expectThrows({"--verify-patches", "p.toml", "--verify-output", "out.elf", "--exclude-sce-module", "x.prx"}, noEffect); + expectThrows({"--verify-patches", "p.toml", "--verify-output", "out.elf", "--skip-sce-module"}, noEffect); + expectThrows({"--verify-patches", "p.toml", "in.elf", "extra"}, "unexpected argument: extra"); + for (const auto* option : {"--windows", "--macos", "--to-intel", "--to-rosetta", "--trace-imports", "--lazy-binding", "--autorun", "--registry", "--skip-syscall-check", "--no-patches"}) + expectThrows({"--verify-patches", "p.toml", option, "in.elf"}, std::string(option) + " cannot be used with --verify-patches"); + expectThrows({"--verify-patches", "p.toml", "--rpath", "x", "in.elf"}, "--rpath cannot be used with --verify-patches"); + expectThrows({"--verify-patches", "p.toml", "--patches", "q.toml", "in.elf"}, "--patches cannot be used with --verify-patches"); + expectThrows({"--verify-patches", "p.toml", "unused-filter=1", "in.elf"}, "unused-filter= cannot be used with --verify-patches"); + expectThrows({"--verify-patches", "p.toml", "--linux-fixed-base", "0x10000", "in.elf"}, "--linux-fixed-base cannot be used with --verify-patches"); + expectThrows({"--verify-patches", "p.toml", "--macos-fixed-base", "0x20000", "in.elf"}, "--macos-fixed-base cannot be used with --verify-patches"); + expectThrows({"--verify-output", "in.elf", "out.elf"}, "--verify-output requires --verify-patches"); + expectThrows({"--verify-patches"}, "--verify-patches requires a file"); + expectUsage({"--verify-patches", "p.toml"}); +} + int main() { try { defaults(); @@ -308,6 +364,9 @@ int main() { missingPositionals(); windowsFixedBase(); linuxFixedBase(); + patchOptions(); + verifyMode(); + macosFixedBase(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; diff --git a/core/relinker/cli/tests/RuntimeLayoutTests.cpp b/core/relinker/cli/tests/RuntimeLayoutTests.cpp new file mode 100644 index 0000000000..b2281f2d08 --- /dev/null +++ b/core/relinker/cli/tests/RuntimeLayoutTests.cpp @@ -0,0 +1,84 @@ +#include +#include +#include +#include +#include +#include + +namespace { + +const std::string Header = + "Arrange the files like this:\n" + "folder/\n"; + +const std::string Libraries = + " |-- libs/\n" + " |-- *.prx (system libraries)\n" + " |-- app0/\n" + " |-- \n"; + +std::string print(const std::string& executable, const std::vector& modules, const std::vector& extraExecutables = {}) { + std::ostringstream stream; + Cli::RuntimeLayout::Print(stream, executable, modules, extraExecutables); + return stream.str(); +} + +void requireText(const std::string& actual, const std::string& expected, const std::string& what) { + if (actual != expected) throw std::runtime_error(what + ":\n--- got ---\n" + actual + "--- expected ---\n" + expected); +} + +void withoutModules() { + requireText(print("eboot.elf", {}), Header + " |-- eboot.elf\n" + Libraries + "\n", "a layout without modules"); +} + +void extraExecutables() { + requireText(print("eboot.exe", {}, {"picker.picker.bin.exe", "es-de.es-de.bin.exe"}), + Header + " |-- eboot.exe\n |-- picker.picker.bin.exe\n |-- es-de.es-de.bin.exe\n" + Libraries + "\n", + "extra executables were not listed in order after the main one"); +} + +void moduleTree() { + const std::vector modules{ + "sce_module/libc.prx", + "Media/Plugins/a.prx", + "sce_module/libSceFios2.prx", + "Media/Plugins/b.prx", + "sce_module/libc.prx", + }; + requireText(print("eboot.elf", modules), + Header + " |-- eboot.elf\n" + Libraries + + " |-- Media/\n" + " |-- Plugins/\n" + " |-- a.prx\n" + " |-- b.prx\n" + " |-- sce_module/\n" + " |-- libSceFios2.prx\n" + " |-- libc.prx\n" + "\n", + "modules were not merged into one sorted tree"); +} + +void topLevelModule() { + requireText(print("eboot.elf", {"plugin.prx", "prx/x.prx"}), + Header + " |-- eboot.elf\n" + Libraries + + " |-- plugin.prx\n" + " |-- prx/\n" + " |-- x.prx\n" + "\n", + "a module outside any directory was not listed as a file"); +} + +} + +int main() { + try { + withoutModules(); + extraExecutables(); + moduleTree(); + topLevelModule(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + return 0; +} diff --git a/core/relinker/codegen/include/codegen/CodegenTypes.hpp b/core/relinker/codegen/include/codegen/CodegenTypes.hpp index d2d5ea0d19..9b116f3657 100644 --- a/core/relinker/codegen/include/codegen/CodegenTypes.hpp +++ b/core/relinker/codegen/include/codegen/CodegenTypes.hpp @@ -68,9 +68,11 @@ struct DecodedInstructionInfo { bool AddressSize32; std::size_t RipRelativeDispOffset; bool HasModRm; + bool HasMemoryOperand; 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/IInstructionScanner.hpp b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp index 969ea443b8..a09771a326 100644 --- a/core/relinker/codegen/include/codegen/IInstructionScanner.hpp +++ b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp @@ -14,9 +14,12 @@ public: virtual ~IInstructionScanner() = default; [[nodiscard]] virtual std::vector ScanCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize) const = 0; + virtual void VisitCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize, const std::function& visit) const = 0; }; -std::unique_ptr MakeInstructionScanner(std::function progress = {}); +std::vector MergeCodeRanges(std::vector ranges); + +std::unique_ptr MakeInstructionScanner(std::function progress = {}, std::vector unwindRanges = {}); } diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp index 7f09e114ad..c3df3388cc 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp @@ -40,8 +40,10 @@ inline constexpr Entry kRdpru = {"RDPRU", nullptr, 0}; inline constexpr Entry kMcommit = {"MCOMMIT", nullptr, 0}; inline constexpr Entry kRdseed = {"RDSEED", nullptr, 0}; inline constexpr Entry kRdpid = {"RDPID", nullptr, 0}; +inline constexpr Entry kRdtscp = {"RDTSCP", nullptr, 0}; inline constexpr Entry kClwb = {"CLWB", nullptr, 0}; inline constexpr Entry kWideStore = {"256-bit store", nullptr, 0}; +inline constexpr Entry kScalarCompare = {"scalar compare", nullptr, 0}; inline constexpr Entry kExtrq = {"EXTRQ", nullptr, 0}; inline constexpr Entry kInsertq = {"INSERTQ", nullptr, 0}; inline constexpr Entry kExtrqRegisterForm = {"EXTRQ register form", nullptr, 0}; @@ -63,6 +65,7 @@ inline constexpr Entry kSha1msg2 = {"SHA1MSG2", nullptr, 0}; inline constexpr std::uint8_t kMovsStoreOpcode = 0x11; inline constexpr std::uint8_t kPshufbZero = 0x80; inline constexpr std::uint8_t kTrapFill = 0xCC; +inline constexpr std::uint8_t kRdpruTrampolineBytes[] = {0x9C, 0x85, 0xC9, 0x74, 0x0B, 0x83, 0xF9, 0x01, 0x74, 0x06, 0x31, 0xC0, 0x31, 0xD2, 0xEB, 0x02, 0x0F, 0x31, 0x9D}; inline constexpr std::size_t kStubAlignment = 16; inline constexpr std::size_t kRedZoneSpillFrame = 0x90; diff --git a/core/relinker/codegen/include/codegen/x86/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/include/codegen/x86/DecodedInstruction.hpp b/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp index 798128e36c..4176cc4cff 100644 --- a/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp +++ b/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp @@ -25,6 +25,7 @@ struct DecodedInstruction { [[nodiscard]] bool IsMovntsd() const; [[nodiscard]] bool IsRdseed() const; [[nodiscard]] bool IsRdpid() const; + [[nodiscard]] bool IsRdtscp() const; [[nodiscard]] bool IsClwb() const; private: diff --git a/core/relinker/codegen/include/codegen/x86/ScalarCompareOperands.hpp b/core/relinker/codegen/include/codegen/x86/ScalarCompareOperands.hpp new file mode 100644 index 0000000000..458f799ce4 --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/ScalarCompareOperands.hpp @@ -0,0 +1,19 @@ +#ifndef CODEGEN_X86_SCALARCOMPAREOPERANDS_HPP +#define CODEGEN_X86_SCALARCOMPAREOPERANDS_HPP + +#include +#include +#include + +namespace Codegen { + +struct ScalarCompareOperands { + std::optional RipDisplacementOffset; + std::int64_t RipTarget = 0; +}; + +[[nodiscard]] std::optional DecodeScalarCompare(const std::uint8_t* data, std::size_t length); + +} + +#endif diff --git a/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp b/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp index f2ff3c3652..72fb24c1c9 100644 --- a/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp +++ b/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp @@ -38,7 +38,6 @@ struct MemoryOperand { [[nodiscard]] MemoryOperand DecodeMemoryOperand(const std::uint8_t* data, std::size_t length, std::size_t modRmOffset, std::uint8_t rex, std::vector prefixes); void EmitSse(std::vector& out, std::uint8_t prefix, std::initializer_list opcode, std::uint8_t dst, std::uint8_t src); -void EmitShiftImm(std::vector& out, std::uint8_t extension, std::uint8_t reg, std::uint8_t imm); void ApplyStubRelocations(std::span body, std::span relocations, std::uint64_t siteAddress, std::uint64_t stubAddress, Domain::FileByteOffset failureOffset); class StubBodyBuilder { diff --git a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp index 7a9a14a364..c6db778a9e 100644 --- a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp +++ b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp @@ -146,6 +146,7 @@ inline constexpr std::uint8_t TwoByteMovdMovq = 0x7E; inline constexpr std::uint8_t TwoByteMovqMmxXmm = 0x6F; inline constexpr std::uint8_t TwoByteMovdqu = 0x7F; inline constexpr std::uint8_t TwoByteGrp7 = 0x01; +inline constexpr std::uint8_t TwoByteMaskmov = 0xF7; inline constexpr std::uint8_t TwoByteShiftImm8Min = 0x70; inline constexpr std::uint8_t TwoByteShiftImm8Max = 0x73; inline constexpr std::uint8_t ThreeByteEscape38 = 0x38; diff --git a/core/relinker/codegen/src/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index f92c5edc93..8f312031fe 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -55,12 +56,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 +113,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 { @@ -256,6 +261,15 @@ std::set Amd64OnlyConverter::_collectBranchTargets( const auto match = std::lower_bound(matches.begin(), matches.end(), offset, [](const InstructionMatch& item, const std::uint64_t value) { return item.Offset < value; }); return match != matches.end() && match->Offset == offset; }; + const auto pastPrefixes = [&](const std::uint64_t address) { + const auto offset = address - ph.MappedAddress; + const auto after = std::upper_bound(matches.begin(), matches.end(), offset, [](const std::uint64_t value, const InstructionMatch& item) { return value < item.Offset; }); + if (after == matches.begin()) + return false; + const auto& match = *std::prev(after); + const auto inside = offset - match.Offset; + return inside != 0 && inside < match.Length && inside > DecodedInstruction{seg.data() + match.Offset, match.Length}.OpcodeOffset(); + }; const X64InstructionDecoder decoder; std::set targets; for (const auto& match : matches) { @@ -263,7 +277,7 @@ std::set Amd64OnlyConverter::_collectBranchTargets( const auto next = static_cast(ph.MappedAddress + match.Offset + info.Length); if (info.HasBranchTarget && !info.HasRipRelativeDisp) { const auto target = next + info.BranchDisp; - if (target >= 0) + if (target >= 0 && !pastPrefixes(static_cast(target))) targets.insert(static_cast(target)); } else if (info.HasRipRelativeDisp && info.Opcode == kLea) { std::int32_t displacement; @@ -382,6 +396,8 @@ void Amd64OnlyConverter::_convertSegment( for (auto next = item.Index + 1; siteLength < Amd64OnlySubstitutionTable::kJmpRel32.Size; ++next) { if (next >= matches.size() || matches[next].Offset != match.Offset + siteLength) throw CodegenException("AMD-only instruction too short for a jump and not followed by an instruction", fileOffset); + if (claimed.contains(next)) + throw CodegenException("AMD-only instruction too short for a jump is followed by an instruction another stub already moved", ph.Offset + matches[next].Offset); const auto& following = matches[next]; const std::span bytes(seg.data() + following.Offset, following.Length); const auto info = decoder.DecodeInstruction(bytes.data(), bytes.size()); diff --git a/core/relinker/codegen/src/InstructionScanner.cpp b/core/relinker/codegen/src/InstructionScanner.cpp index 3cd662a590..28f8544994 100644 --- a/core/relinker/codegen/src/InstructionScanner.cpp +++ b/core/relinker/codegen/src/InstructionScanner.cpp @@ -8,27 +8,46 @@ namespace Codegen { +std::vector MergeCodeRanges(std::vector ranges) { + std::erase_if(ranges, [](const Domain::CodeRange& range) { return range.End <= range.Begin; }); + std::sort(ranges.begin(), ranges.end(), [](const Domain::CodeRange& left, const Domain::CodeRange& right) { return left.Begin < right.Begin; }); + std::vector merged; + for (const auto& range : ranges) { + if (!merged.empty() && range.Begin <= merged.back().End) + merged.back().End = std::max(merged.back().End, range.End); + else + merged.push_back(range); + } + return merged; +} + class InstructionScanner : public IInstructionScanner { public: - explicit InstructionScanner(std::function progress) : progress(std::move(progress)) {} + InstructionScanner(std::function progress, std::vector unwindRanges) : progress(std::move(progress)), unwindRanges(MergeCodeRanges(std::move(unwindRanges))) {} [[nodiscard]] std::vector ScanCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize) const override; + void VisitCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize, const std::function& visit) const override; private: std::function progress; + std::vector unwindRanges; }; -std::vector InstructionScanner::ScanCodeSection(const std::vector& codeSection, const Domain::FileByteOffset codeSectionOffset, const Domain::FileByteOffset codeSectionSize) const { +template +void ScanCodeSection(const std::vector& codeSection, const Domain::FileByteOffset codeSectionOffset, const Domain::FileByteOffset codeSectionSize, const std::function& progress, const std::vector& unwindRanges, TVisitor&& visit) { if (codeSectionSize > codeSection.size()) throw CodegenException("Code section exceeds the input buffer", codeSectionOffset); if (codeSectionSize > std::numeric_limits::max() - codeSectionOffset) throw CodegenException("Code section offset overflow", codeSectionOffset); const auto limit = static_cast(codeSectionSize); const X64InstructionDecoder decoder; - std::vector matches; std::size_t i = 0; + std::size_t range = 0; if (progress) progress(0, limit); std::size_t lastReport = 0; while (i < limit) { + while (range < unwindRanges.size() && unwindRanges[range].End <= i) ++range; + const bool outsideUnwindRanges = !unwindRanges.empty() && (range == unwindRanges.size() || i < unwindRanges[range].Begin); + const std::size_t nextUnwindRange = outsideUnwindRanges && range < unwindRanges.size() ? unwindRanges[range].Begin : limit; const std::uint8_t* cursor = codeSection.data() + i; const std::size_t available = limit - i; @@ -37,10 +56,18 @@ std::vector InstructionScanner::ScanCodeSection(const std::vec length = decoder.Decode(cursor, available); } catch (const CodegenException& e) { if (std::all_of(cursor, cursor + available, [](const std::uint8_t b) { return b == 0; })) break; - throw CodegenException(std::string("Cannot decode instruction: ") + e.what(), codeSectionOffset + i); + if (!outsideUnwindRanges) throw CodegenException(std::string("Cannot decode instruction: ") + e.what(), codeSectionOffset + i); + ++i; + continue; } - matches.push_back({codeSectionOffset + i, length}); + if (length > nextUnwindRange - i) { + i = nextUnwindRange; + continue; + } + + const InstructionMatch match{codeSectionOffset + i, length}; + visit(match); i += length; if (progress && i - lastReport >= 65536) { @@ -50,11 +77,20 @@ std::vector InstructionScanner::ScanCodeSection(const std::vec } if (progress) progress(limit, limit); +} + +std::vector InstructionScanner::ScanCodeSection(const std::vector& codeSection, const Domain::FileByteOffset codeSectionOffset, const Domain::FileByteOffset codeSectionSize) const { + std::vector matches; + Codegen::ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize, progress, unwindRanges, [&](const InstructionMatch& match) { matches.push_back(match); }); return matches; } -std::unique_ptr MakeInstructionScanner(std::function progress) { - return std::make_unique(std::move(progress)); +void InstructionScanner::VisitCodeSection(const std::vector& codeSection, const Domain::FileByteOffset codeSectionOffset, const Domain::FileByteOffset codeSectionSize, const std::function& visit) const { + Codegen::ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize, progress, unwindRanges, visit); +} + +std::unique_ptr MakeInstructionScanner(std::function progress, std::vector unwindRanges) { + return std::make_unique(std::move(progress), std::move(unwindRanges)); } } diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index edf59c514c..739a3aea7f 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -12,12 +12,15 @@ #include #include #include +#include #include #include +#include #include #include #include #include +#include #include #include #include @@ -106,6 +109,22 @@ Amd64OnlyMatch _inPlace(const Entry& entry, const std::size_t length, std::vecto return Amd64OnlyMatch{entry.Name, length, Amd64OnlyLowering::InPlace, std::move(replacement), {}, 0}; } +void _emitRdpru(StubBodyBuilder& body) { + body.Raw(std::span(kLeaRspBelowRedZone.Bytes, kLeaRspBelowRedZone.Size)); + body.Raw(std::span(kRdpruTrampolineBytes, sizeof(kRdpruTrampolineBytes))); + body.Raw(std::span(kLeaRspRestore.Bytes, kLeaRspRestore.Size)); +} + +constexpr std::uint8_t kMcommitStc = 0xF9; + +void _emitScalarCompare(StubBodyBuilder& body, const DecodedInstruction& instr, const ScalarCompareOperands& operands) { + const std::span bytes(instr.Data, instr.Length); + if (operands.RipDisplacementOffset) + body.RipRelative(bytes, *operands.RipDisplacementOffset, operands.RipTarget); + else + body.Raw(bytes); +} + bool _isClzeroOpcode(const DecodedInstruction& instr) { const auto pos = instr.OpcodeOffset(); return pos + 2 < instr.Length && instr.Data[pos] == X64OpcodeConstants::TwoByteOpcodeEscape && instr.Data[pos + 1] == X64OpcodeConstants::TwoByteGrp7 && instr.Data[pos + 2] == 0xFC; @@ -124,6 +143,13 @@ std::vector _rdpidReplacement(const DecodedInstruction& instr) { return bytes; } +std::vector _rdtscpReplacement(const DecodedInstruction& instr) { + std::vector bytes(instr.Data, instr.Data + instr.Length); + bytes.push_back(0xB9); + bytes.insert(bytes.end(), 4, 0); + return bytes; +} + bool _validWait(const DecodedInstruction& instr) { const auto opcode = instr.Data + instr.OpcodeOffset(); return instr.Length <= kMaxInstructionLength && std::find(instr.Data, opcode, X64OpcodeConstants::PrefixLock) == opcode; @@ -163,8 +189,11 @@ private: [[nodiscard]] Amd64OnlyMatch _matchSha256(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] Amd64OnlyMatch _matchSha1(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] Amd64OnlyMatch _matchClzero(const DecodedInstruction& instr, std::span trailing) const; + [[nodiscard]] Amd64OnlyMatch _matchRdpru(const DecodedInstruction& instr, std::span trailing) const; + [[nodiscard]] Amd64OnlyMatch _matchMcommit(const DecodedInstruction& instr) const; [[nodiscard]] std::optional _matchRosetta(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] const char* _emitOutOfLine(StubBodyBuilder& body, const DecodedInstruction& instr) const; + [[nodiscard]] const char* _emitInPlace(StubBodyBuilder& body, const DecodedInstruction& instr) const; }; Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchMovnts(const DecodedInstruction& instr, const Entry& entry) const { @@ -205,9 +234,23 @@ Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchClzero(const DecodedInstructio return _trampoline(kClzero.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _clzeroLowering.EmitOutOfLine(body, operands); })); } +Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchRdpru(const DecodedInstruction& instr, std::span trailing) const { + if (!_validWait(instr)) + return _unsupported(kRdpru, instr.Length); + return _trampoline(kRdpru.Name, instr.Length, _outOfLine(instr.Length, trailing, [](StubBodyBuilder& body) { _emitRdpru(body); })); +} + +Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchMcommit(const DecodedInstruction& instr) const { + if (!_validWait(instr)) + return _unsupported(kMcommit, instr.Length); + return _inPlace(kMcommit, instr.Length, {kMcommitStc}); +} + std::optional Amd64OnlyInstructionMatcher::_matchRosetta(const DecodedInstruction& instr, std::span trailing) const { if (_target != Amd64OnlyTarget::Rosetta) return std::nullopt; + 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)); @@ -217,8 +260,12 @@ std::optional Amd64OnlyInstructionMatcher::_matchRosetta(const D return _inPlace(kClwb, instr.Length, {}); if (instr.IsRdpid()) return _trampoline(kRdpid.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { body.Raw(_rdpidReplacement(instr)); }), true); + if (instr.IsRdtscp()) + return _trampoline(kRdtscp.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { body.Raw(_rdtscpReplacement(instr)); }), true); if (const auto wide = DecodeVexWideStore(instr.Data, instr.Length)) return _trampoline(kWideStore.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _wideStoreLowering.EmitOutOfLine(body, *wide); }), false, true); + if (const auto compare = DecodeScalarCompare(instr.Data, instr.Length)) + return _trampoline(kScalarCompare.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _emitScalarCompare(body, instr, *compare); }), true); return std::nullopt; } @@ -246,14 +293,65 @@ const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, c _reciprocalLowering.EmitOutOfLine(body, *reciprocal); return _reciprocalEntry(*reciprocal).Name; } + if (instr.IsRdpru()) { + if (!_validWait(instr)) + return nullptr; + _emitRdpru(body); + return kRdpru.Name; + } + if (instr.IsMcommit()) { + if (!_validWait(instr)) + return nullptr; + body.Raw(std::span(&kMcommitStc, 1)); + return kMcommit.Name; + } if (_target == Amd64OnlyTarget::Rosetta && instr.IsRdpid()) { body.Raw(_rdpidReplacement(instr)); return kRdpid.Name; } + if (const auto andn = _target == Amd64OnlyTarget::Rosetta ? DecodeAndn(instr.Data, instr.Length) : std::nullopt) { + EmitAndn(body, *andn); + return "ANDN"; + } + if (_target == Amd64OnlyTarget::Rosetta && instr.IsRdtscp()) { + body.Raw(_rdtscpReplacement(instr)); + return kRdtscp.Name; + } if (const auto wide = _target == Amd64OnlyTarget::Rosetta ? DecodeVexWideStore(instr.Data, instr.Length) : std::nullopt) { _wideStoreLowering.EmitOutOfLine(body, *wide); return kWideStore.Name; } + if (const auto compare = _target == Amd64OnlyTarget::Rosetta ? DecodeScalarCompare(instr.Data, instr.Length) : std::nullopt) { + _emitScalarCompare(body, instr, *compare); + return kScalarCompare.Name; + } + return nullptr; +} + +const char* Amd64OnlyInstructionMatcher::_emitInPlace(StubBodyBuilder& body, const DecodedInstruction& instr) const { + if (const auto inPlace = Match(instr.Data, instr.Length); inPlace && inPlace->Lowering == Amd64OnlyLowering::InPlace) { + for (const auto* entry : {&kMovntss, &kMovntsd, &kMonitorx, &kMwaitx, &kRdseed, &kClwb}) { + if (inPlace->InstructionName != entry->Name) + continue; + const X64InstructionDecoder decoder; + const std::span replacement(inPlace->ReplacementBytes); + for (std::size_t offset = 0; offset < replacement.size();) { + const auto info = decoder.DecodeInstruction(replacement.data() + offset, replacement.size() - offset); + if (info.Length == 0 || info.Length > replacement.size() - offset) + throw CodegenException("In-place replacement does not split into whole instructions"); + const auto part = replacement.subspan(offset, info.Length); + if (info.HasRipRelativeDisp) { + std::int32_t displacement = 0; + std::memcpy(&displacement, part.data() + info.RipRelativeDispOffset, sizeof(displacement)); + body.RipRelative(part, info.RipRelativeDispOffset, static_cast(offset + info.Length) + displacement); + } else { + body.Raw(part); + } + offset += info.Length; + } + return entry->Name; + } + } return nullptr; } @@ -273,7 +371,9 @@ std::optional Amd64OnlyInstructionMatcher::MatchSequence( const char* name = nullptr; for (const auto& instruction : instructions) { const DecodedInstruction instr{instruction.data(), instruction.size()}; - const auto emitted = _emitOutOfLine(body, instr); + auto emitted = _emitOutOfLine(body, instr); + if (emitted == nullptr && name != nullptr) + emitted = _emitInPlace(body, instr); if (emitted == nullptr) return std::nullopt; if (name == nullptr) @@ -282,7 +382,7 @@ std::optional Amd64OnlyInstructionMatcher::MatchSequence( } _moveTrailing(body, trailing); const DecodedInstruction first{instructions.front().data(), instructions.front().size()}; - const bool optional = DecodeReciprocal(first.Data, first.Length).has_value() || (_target == Amd64OnlyTarget::Rosetta && first.IsRdpid()); + const bool optional = DecodeReciprocal(first.Data, first.Length).has_value() || (_target == Amd64OnlyTarget::Rosetta && (first.IsRdpid() || first.IsRdtscp())); return _trampoline(name, instructions.front().size(), body.Finish(), optional); } @@ -328,10 +428,10 @@ std::optional Amd64OnlyInstructionMatcher::Match( return _unsupported(kClzero, length); if (instr.IsRdpru()) - return _unsupported(kRdpru, length); + return _matchRdpru(instr, trailing); if (instr.IsMcommit()) - return _unsupported(kMcommit, length); + return _matchMcommit(instr); return _matchRosetta(instr, trailing); } diff --git a/core/relinker/codegen/src/x86/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/DecodedInstruction.cpp b/core/relinker/codegen/src/x86/DecodedInstruction.cpp index 3bdb906efe..d54ce541bc 100644 --- a/core/relinker/codegen/src/x86/DecodedInstruction.cpp +++ b/core/relinker/codegen/src/x86/DecodedInstruction.cpp @@ -292,6 +292,23 @@ bool DecodedInstruction::IsRdpid() const { ((modrm >> ModRmModShift) & ModRmModMask) == ModRmModRegister && ((modrm >> ModRmRegShift) & ModRmRegMask) == 7; } +bool DecodedInstruction::IsRdtscp() const { + bool operandSizeOverride = false; + bool repnePrefix = false; + bool repPrefix = false; + const std::size_t pos = _skipPrefixesAndRex(&operandSizeOverride, &repnePrefix, &repPrefix); + + if (operandSizeOverride || repnePrefix || repPrefix) { + return false; + } + + if (pos + 2 >= Length) { + return false; + } + + return Data[pos] == TwoByteOpcodeEscape && Data[pos + 1] == TwoByteGrp7 && Data[pos + 2] == 0xF9; +} + bool DecodedInstruction::IsClwb() const { bool operandSizeOverride = false; bool repnePrefix = false; diff --git a/core/relinker/codegen/src/x86/ScalarCompareOperands.cpp b/core/relinker/codegen/src/x86/ScalarCompareOperands.cpp new file mode 100644 index 0000000000..0f1e217d5c --- /dev/null +++ b/core/relinker/codegen/src/x86/ScalarCompareOperands.cpp @@ -0,0 +1,79 @@ +#include +#include + +namespace Codegen { + +namespace { + +constexpr std::uint8_t kVexMap0F = 0x01; +constexpr std::uint8_t kPpF3 = 0x02; +constexpr std::uint8_t kCompareOpcode = 0xC2; +constexpr std::uint8_t kPredicateLeOs = 0x02; + +bool _segmentPrefix(const std::uint8_t prefix) { + using namespace X64OpcodeConstants; + return prefix == PrefixSegCs || prefix == PrefixSegSs || prefix == PrefixSegDs || prefix == PrefixSegEs || prefix == PrefixSegFs || prefix == PrefixSegGs; +} + +} + +std::optional DecodeScalarCompare(const std::uint8_t* data, const std::size_t length) { + using namespace X64OpcodeConstants; + std::size_t pos = 0; + while (pos < length && _segmentPrefix(data[pos])) + ++pos; + std::size_t modRmOffset = 0; + if (pos < length && data[pos] == OneByteVex2) { + if (pos + 4 > length || (data[pos + 1] & 0x03) < kPpF3 || data[pos + 2] != kCompareOpcode) + return std::nullopt; + modRmOffset = pos + 3; + } else if (pos < length && data[pos] == OneByteVex3) { + if (pos + 5 > length || (data[pos + 1] & Vex3MapMask) != kVexMap0F || (data[pos + 2] & 0x03) < kPpF3 || data[pos + 3] != kCompareOpcode) + return std::nullopt; + modRmOffset = pos + 4; + } else { + 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); + const auto rm = static_cast(modrm & ModRmRmMask); + auto cursor = modRmOffset + 1; + std::optional sib; + if (mod != ModRmModRegister && rm == ModRmRmSibPresent) { + if (cursor >= length) + return std::nullopt; + sib = data[cursor++]; + } + const bool ripRelative = mod == ModRmModIndirect && rm == ModRmRmRipRelative; + const bool absolute = mod == ModRmModIndirect && sib.has_value() && (*sib & SibBaseMask) == SibBaseDisp32; + const std::size_t displacementSize = mod == ModRmModDisp8 ? Disp8Size : (mod == ModRmModDisp32 || ripRelative || absolute) ? Disp32Size : 0; + if (cursor + displacementSize + ImmSize8 != length || data[length - 1] <= kPredicateLeOs) + return std::nullopt; + ScalarCompareOperands operands; + if (ripRelative) { + std::uint32_t displacement = 0; + for (std::size_t index = 0; index < Disp32Size; ++index) + displacement |= static_cast(data[cursor + index]) << (index * 8); + operands.RipDisplacementOffset = cursor; + operands.RipTarget = static_cast(length) + static_cast(displacement); + } + return operands; +} + +} diff --git a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp index e68805215f..3210fc1e7c 100644 --- a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp +++ b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp @@ -100,10 +100,6 @@ void EmitSse(std::vector& out, const std::uint8_t prefix, const st _emit(out, prefix, dst, src, opcode, static_cast(kModRmRegister | ((dst & 7) << 3) | (src & 7))); } -void EmitShiftImm(std::vector& out, const std::uint8_t extension, const std::uint8_t reg, const std::uint8_t imm) { - _shiftImm(out, kShiftQwords, extension, reg, imm); -} - void StubBodyBuilder::Sse(const std::uint8_t prefix, const std::initializer_list opcode, const std::uint8_t dst, const std::uint8_t src) { EmitSse(_bytes, prefix, opcode, dst, src); } @@ -210,6 +206,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 c4768b31e1..0c2f5571c9 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,8 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( info.Length = Decode(data, available); std::size_t pos = 0; + bool operandSizeOverride = false; + bool repPrefix = false; while (pos < info.Length) { std::uint8_t b = data[pos]; @@ -371,6 +373,10 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( info.SegmentPrefix = b; if (b == PrefixAddressSize) info.AddressSize32 = true; + if (b == PrefixOperandSize) + operandSizeOverride = true; + if (b == PrefixRep) + repPrefix = true; info.RexPrefix = 0; ++pos; continue; @@ -391,6 +397,8 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( info.ModRmRegField = (modrm >> ModRmRegShift) & ModRmRegMask; const std::uint8_t mod = registerOnly ? ModRmModRegister : (modrm >> ModRmModShift) & ModRmModMask; const std::uint8_t rm = modrm & ModRmRmMask; + if (mod != ModRmModRegister) + info.HasMemoryOperand = true; if (mod == ModRmModIndirect && rm == ModRmRmRipRelative) { info.HasRipRelativeDisp = true; info.RipRelativeDispOffset = modrmPos + 1; @@ -402,9 +410,17 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( const bool xop = op == XopPrefix && pos < info.Length && (data[pos] & Vex3MapMask) >= XopMapMin; if (op == OneByteVex2 || op == OneByteVex3 || op == EvexPrefix || xop) { - pos += (op == OneByteVex2 ? 1 : (op == OneByteVex3 || xop ? 2 : EvexPrefixLength - 1)) + 1; - if (pos < info.Length) + const bool vex = op == OneByteVex2 || op == OneByteVex3; + const std::size_t vectorOpcodeOffset = pos + (op == OneByteVex2 ? 1 : (op == OneByteVex3 || xop ? 2 : EvexPrefixLength - 1)); + const std::uint8_t vexMap = op == OneByteVex2 ? 1 : (data[pos] & (op == EvexPrefix ? EvexMapMask : Vex3MapMask)); + const std::uint8_t vectorOpcode = vectorOpcodeOffset < info.Length ? data[vectorOpcodeOffset] : 0; + pos = vectorOpcodeOffset + 1; + if (pos < info.Length) { readModRm(pos, false); + if (vex && vexMap == 1 && vectorOpcode == TwoByteMaskmov && + ((info.ModRmByte >> ModRmModShift) & ModRmModMask) == ModRmModRegister) + info.HasMemoryOperand = true; + } info.FlowKind = ControlFlowKind::Sequential; return info; } @@ -418,6 +434,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; @@ -466,6 +495,8 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( info.ModRmRegField = reg; std::uint8_t mod = (modrm >> ModRmModShift) & ModRmModMask; std::uint8_t rm = modrm & ModRmRmMask; + if (mod != ModRmModRegister) + info.HasMemoryOperand = true; if (reg == 2) { if (mod == ModRmModIndirect && rm == ModRmRmRipRelative) { info.FlowKind = ControlFlowKind::Call; @@ -490,12 +521,12 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( } return info; } - if (reg == 3) { - info.FlowKind = ControlFlowKind::IndirectCall; - return info; - } - if (reg == 5) { - info.FlowKind = ControlFlowKind::IndirectJump; + if (reg == 3 || reg == 5) { + if (mod == ModRmModIndirect && rm == ModRmRmRipRelative) { + info.HasRipRelativeDisp = true; + info.RipRelativeDispOffset = pos + 1; + } + info.FlowKind = reg == 3 ? ControlFlowKind::IndirectCall : ControlFlowKind::IndirectJump; return info; } } @@ -578,6 +609,21 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( if (hasModRm && pos < info.Length) readModRm(pos, twoByteOpcode && op >= TwoByteMovCrDrMin && op <= TwoByteMovCrDrMax); + if (info.HasModRm && twoByteOpcode) { + const std::uint8_t mod = (info.ModRmByte >> ModRmModShift) & ModRmModMask; + const std::uint8_t reg = (info.ModRmByte >> ModRmRegShift) & ModRmRegMask; + if (op == TwoByteMaskmov && mod == ModRmModRegister) + info.HasMemoryOperand = true; + else if (op == TwoByteGrp7 && (info.ModRmByte == 0xC8 || info.ModRmByte == 0xFA || info.ModRmByte == 0xFC)) + info.HasMemoryOperand = true; + else if (op == TwoByteGrp15 && repPrefix && reg == 6 && mod == ModRmModRegister) + info.HasMemoryOperand = true; + } + + if (!twoByteOpcode && ((op >= 0x6c && op <= 0x6f) || (op >= 0xa0 && op <= 0xa7) || + (op >= 0xaa && op <= 0xaf) || op == 0xd7)) + info.HasMemoryOperand = true; + info.FlowKind = ControlFlowKind::Sequential; return info; } diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 8c4e4e0090..a03384cea7 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -24,7 +25,7 @@ #include #include #include -#ifdef __linux__ +#if defined(__linux__) || defined(__APPLE__) #include #endif #include @@ -349,7 +350,16 @@ void matcherSubstitutions() { require(prefixedClzero && prefixedClzero->Lowering == Codegen::Amd64OnlyLowering::Unsupported, "CLZERO with a 66, F2 or F3 prefix was not reported as unsupported"); } const auto rdpru = match({0x0F, 0x01, 0xFD}); - require(rdpru && rdpru->Lowering == Codegen::Amd64OnlyLowering::Unsupported && rdpru->InstructionName == "RDPRU", "RDPRU was not reported as unsupported"); + require(rdpru && rdpru->Lowering == Codegen::Amd64OnlyLowering::Trampoline && rdpru->InstructionName == "RDPRU" && rdpru->StubBody.size() == 40 && rdpru->ReturnBranchOffset == 35 && rdpru->Relocations.empty(), "RDPRU was not lowered through a runtime-checked stub"); + require(Bytes(rdpru->StubBody.begin(), rdpru->StubBody.begin() + 35) == Bytes{0x48, 0x8D, 0xA4, 0x24, 0x70, 0xFF, 0xFF, 0xFF, 0x9C, 0x85, 0xC9, 0x74, 0x0B, 0x83, 0xF9, 0x01, 0x74, 0x06, 0x31, 0xC0, 0x31, 0xD2, 0xEB, 0x02, 0x0F, 0x31, 0x9D, 0x48, 0x8D, 0xA4, 0x24, 0x90, 0x00, 0x00, 0x00}, "RDPRU stub does not frame the red zone or dispatch ECX to TSC or zero"); + const auto mcommit = match({0xF3, 0x0F, 0x01, 0xFA}); + require(mcommit && mcommit->Lowering == Codegen::Amd64OnlyLowering::InPlace && mcommit->InstructionName == "MCOMMIT" && mcommit->ReplacementBytes == Bytes{0xF9, 0x0F, 0x1F, 0x00}, "MCOMMIT was not replaced by STC plus NOP"); + const auto prefixedMcommit = match({0x2E, 0xF3, 0x0F, 0x01, 0xFA}); + require(prefixedMcommit && prefixedMcommit->Lowering == Codegen::Amd64OnlyLowering::InPlace && prefixedMcommit->ReplacementBytes == Bytes{0xF9, 0x0F, 0x1F, 0x40, 0x00}, "Prefixed MCOMMIT was not padded to its length"); + const auto lockedMcommit = match({0xF0, 0xF3, 0x0F, 0x01, 0xFA}); + require(lockedMcommit && lockedMcommit->Lowering == Codegen::Amd64OnlyLowering::Unsupported, "LOCK MCOMMIT was replaced instead of failing"); + const auto lockedRdpru = match({0xF0, 0x0F, 0x01, 0xFD}); + require(lockedRdpru && lockedRdpru->Lowering == Codegen::Amd64OnlyLowering::Unsupported, "LOCK RDPRU was replaced instead of failing"); const auto registerForm = match({0x66, 0x0F, 0x79, 0xCA}); require(registerForm && registerForm->Lowering == Codegen::Amd64OnlyLowering::Trampoline && registerForm->InstructionName == "EXTRQ register form", "EXTRQ register form was not lowered through a stub"); const auto insertqRegisterForm = match({0xF2, 0x0F, 0x79, 0xCA}); @@ -489,12 +499,12 @@ void converterSegment() { auto branchInside = file; branchInside[0x207] = 0x02; requireFailure([&] { (void)converter->Convert(branchInside, {segmentHeader(20)}); }, "Branch into an AMD-only instruction was accepted"); - auto rdpru = file; - rdpru[0x20F] = 0x0F; - rdpru[0x210] = 0x01; - rdpru[0x211] = 0xFD; - rdpru[0x212] = 0x90; - requireFailure([&] { (void)converter->Convert(rdpru, {segmentHeader(20)}); }, "RDPRU was silently kept"); + auto lockedRdpru = file; + lockedRdpru[0x20F] = 0xF0; + lockedRdpru[0x210] = 0x0F; + lockedRdpru[0x211] = 0x01; + lockedRdpru[0x212] = 0xFD; + requireFailure([&] { (void)converter->Convert(lockedRdpru, {segmentHeader(20)}); }, "LOCK RDPRU was accepted"); auto registerForm = file; const Bytes extrqRegister = {0x66, 0x0F, 0x79, 0xCA}; std::copy(extrqRegister.begin(), extrqRegister.end(), registerForm.begin() + 0x20F); @@ -611,15 +621,39 @@ void converterSha1() { void converterMonitorWait() { const auto converter = Codegen::MakeAmd64OnlyConverter(); Bytes file(0x300, 0xCC); - const Bytes text = {0x0F, 0x01, 0xFA, 0x0F, 0x01, 0xFB, 0x2E, 0x0F, 0x01, 0xFB, 0xC3}; + const Bytes text = {0x0F, 0x01, 0xFA, 0x0F, 0x01, 0xFB, 0x2E, 0x0F, 0x01, 0xFB, 0xF3, 0x0F, 0x01, 0xFA, 0xC3}; std::copy(text.begin(), text.end(), file.begin() + 0x200); const auto result = converter->Convert(file, {segmentHeader(text.size())}); - require(result.ReplacedCount == 3 && result.Trampolines.empty() && result.Reports.size() == 3, "MONITORX/MWAITX were not replaced in place"); + require(result.ReplacedCount == 4 && result.Trampolines.empty() && result.Reports.size() == 4, "MONITORX/MWAITX/MCOMMIT were not replaced in place"); require(result.Reports[0].InstructionName == "MONITORX" && result.Reports[1].InstructionName == "MWAITX" && result.Reports[2].Offset == 0x206 && result.Reports[2].ReplacementLength == 4 && result.Reports[2].Lowering == Codegen::Amd64OnlyLowering::InPlace, "MONITORX/MWAITX reports are wrong"); + require(result.Reports[3].InstructionName == "MCOMMIT" && result.Reports[3].Offset == 0x20A && result.Reports[3].ReplacementLength == 4 && result.Reports[3].Lowering == Codegen::Amd64OnlyLowering::InPlace, "MCOMMIT report is wrong"); auto expected = file; - const Bytes replaced = {0x0F, 0x1F, 0x00, 0xF3, 0x90, 0x90, 0xF3, 0x90, 0x66, 0x90, 0xC3}; + const Bytes replaced = {0x0F, 0x1F, 0x00, 0xF3, 0x90, 0x90, 0xF3, 0x90, 0x66, 0x90, 0xF9, 0x0F, 0x1F, 0x00, 0xC3}; std::copy(replaced.begin(), replaced.end(), expected.begin() + 0x200); - require(result.Bytes == expected, "MONITORX/MWAITX were replaced with the wrong bytes"); + require(result.Bytes == expected, "MONITORX/MWAITX/MCOMMIT were replaced with the wrong bytes"); +} + +void converterInPlaceFollower() { + const auto converter = Codegen::MakeAmd64OnlyConverter(); + const auto convert = [&](const Bytes& following) { + Bytes file(0x300, 0xCC); + Bytes text = {0x66, 0x0F, 0x79, 0xCA}; + text.insert(text.end(), following.begin(), following.end()); + text.push_back(0xC3); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + return converter->Convert(file, {segmentHeader(text.size())}); + }; + const auto tail = [](const Codegen::TrampolineSite& site, const std::size_t length) { + return Bytes(site.Body.begin() + static_cast(site.ReturnBranchOffset - length), site.Body.begin() + static_cast(site.ReturnBranchOffset)); + }; + const auto store = convert({0xF3, 0x0F, 0x2B, 0x07}); + require(store.Trampolines.size() == 1 && store.Trampolines[0].Length == 8 && tail(store.Trampolines[0], 4) == Bytes{0xF3, 0x0F, 0x11, 0x07}, "MOVNTSS after a short EXTRQ was not moved into its stub as MOVSS"); + const auto ripStore = convert({0xF2, 0x0F, 0x2B, 0x05, 0x40, 0x00, 0x00, 0x00}); + require(ripStore.Trampolines.size() == 1 && ripStore.Trampolines[0].Length == 12, "RIP-relative MOVNTSD after a short EXTRQ was not moved into its stub"); + const auto& ripSite = ripStore.Trampolines[0]; + require(tail(ripSite, 8) == Bytes{0xF2, 0x0F, 0x11, 0x05, 0x00, 0x00, 0x00, 0x00} && ripSite.Relocations.size() == 1 && ripSite.Relocations[0].SiteTarget == 12 + 0x40 && ripSite.Relocations[0].InstructionEnd == ripSite.ReturnBranchOffset, "Moved MOVNTSD has the wrong bytes or relocation"); + const auto monitor = convert({0x0F, 0x01, 0xFA}); + require(monitor.Trampolines.size() == 1 && monitor.Trampolines[0].Length == 7 && tail(monitor.Trampolines[0], 3) == Bytes{0x0F, 0x1F, 0x00}, "MONITORX after a short EXTRQ was not moved into its stub as a NOP"); } void converterClzero() { @@ -655,8 +689,8 @@ void rosettaSubstitutions() { const auto intel = Codegen::MakeAmd64OnlyInstructionMatcher(); const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); const auto match = [](const auto& matcher, const Bytes& bytes) { return matcher->Match(bytes.data(), bytes.size()); }; - for (const auto& bytes : {Bytes{0x0F, 0xC7, 0xF8}, Bytes{0xF3, 0x0F, 0xC7, 0xF8}, Bytes{0x66, 0x0F, 0xAE, 0x30}}) - require(!match(intel, bytes).has_value(), "RDSEED, RDPID or CLWB was converted for an Intel host"); + for (const auto& bytes : {Bytes{0x0F, 0xC7, 0xF8}, Bytes{0xF3, 0x0F, 0xC7, 0xF8}, Bytes{0x66, 0x0F, 0xAE, 0x30}, Bytes{0x0F, 0x01, 0xF9}}) + require(!match(intel, bytes).has_value(), "RDSEED, RDPID, CLWB or RDTSCP was converted for an Intel host"); const auto rdseed = match(rosetta, {0x48, 0x0F, 0xC7, 0xF9}); require(rdseed && rdseed->Lowering == Amd64OnlyLowering::InPlace && rdseed->ReplacementBytes == Bytes{0x48, 0x0F, 0xC7, 0xF1} && rdseed->InstructionName == "RDSEED", "RDSEED was not rewritten to RDRAND"); const auto rdseed16 = match(rosetta, {0x66, 0x41, 0x0F, 0xC7, 0xFA}); @@ -669,6 +703,11 @@ void rosettaSubstitutions() { const auto rdpid = match(rosetta, {0xF3, 0x41, 0x0F, 0xC7, 0xFB}); require(rdpid && rdpid->Lowering == Amd64OnlyLowering::Trampoline && rdpid->Optional && rdpid->InstructionName == "RDPID", "RDPID was not lowered through an optional stub"); require(rdpid->ReturnBranchOffset == 6 && Bytes(rdpid->StubBody.begin(), rdpid->StubBody.begin() + 7) == Bytes{0x41, 0xBB, 0, 0, 0, 0, 0xE9}, "RDPID r11 stub does not load 0 before returning"); + const auto rdtscp = match(rosetta, {0x0F, 0x01, 0xF9}); + require(rdtscp && rdtscp->Lowering == Amd64OnlyLowering::Trampoline && rdtscp->Optional && rdtscp->InstructionName == "RDTSCP", "RDTSCP was not lowered through an optional stub"); + require(rdtscp->ReturnBranchOffset == 8 && Bytes(rdtscp->StubBody.begin(), rdtscp->StubBody.begin() + 9) == Bytes{0x0F, 0x01, 0xF9, 0xB9, 0, 0, 0, 0, 0xE9}, "RDTSCP stub does not load 0 into ECX after reading the counter"); + require(!match(rosetta, {0x0F, 0x01, 0xF8}).has_value(), "SWAPGS (0F 01 F8) was taken for RDTSCP"); + require(!match(rosetta, {0x66, 0x0F, 0x01, 0xF9}).has_value(), "RDTSCP with an operand size prefix was lowered"); } void wideStoreSplit() { @@ -825,6 +864,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()); @@ -867,6 +919,14 @@ void converterWideStoreWindows() { expected.push_back(0xC3); require(returnIsland.Trampolines.size() == 2 && returnIsland.Trampolines[0].Offset == 0x20B && body(returnIsland.Trampolines[0]) == expected, "Instructions moved for an island that end in a return jump back after it"); + const auto intoStore = convert(withStore({0x73, 0x01}, {0xC3})); + expected = halves; + expected.push_back(0xC3); + require(intoStore.Trampolines.size() == 1 && intoStore.Trampolines[0].Offset == 0x202 && intoStore.Trampolines[0].Length == 5 && body(intoStore.Trampolines[0]) == expected, "A branch into the middle of a 256-bit store, where no scanned instruction starts, kept the store"); + + const auto intoFollower = convert(withStore({0x73, 0x06}, {0x48, 0x83, 0xC1, 0x01, 0xC3})); + require(intoFollower.Trampolines.size() == 1 && intoFollower.Trampolines[0].Offset == 0x202 && intoFollower.Trampolines[0].Length == 8, "A branch into the middle of an instruction moved with a 256-bit store kept the store"); + const auto afterBranch = convert(withStore({0x75, 0x10}, {0xFF, 0xD0})); expected = {0x0F, 0x85, 0, 0, 0, 0}; expected.insert(expected.end(), halves.begin(), halves.end()); @@ -886,6 +946,32 @@ void converterWideStoreWindows() { requireFailure([&] { (void)convert(withStore(far, tail)); }, "A 256-bit store whose movable neighbours are all beyond a short jump was kept"); } +void converterClaimedFollower() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + for (const auto& [shortInstruction, name] : {std::pair{Bytes{0x0F, 0x53, 0xCA}, "RCPPS"}, std::pair{Bytes{0xF3, 0x0F, 0xC7, 0xF8}, "RDPID"}}) { + Bytes file(0x300, 0xCC); + Bytes text = {0x74, 0x06, 0xEB, 0x00, 0xC5, 0xFC, 0x11, 0x07}; + text.insert(text.end(), shortInstruction.begin(), shortInstruction.end()); + for (int index = 0; index < 4; ++index) + text.insert(text.end(), {0x48, 0x01, 0xC8}); + text.insert(text.end(), 8, 0x90); + text.push_back(0xC3); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + const auto result = converter->Convert(file, {segmentHeader(text.size())}); + for (std::size_t left = 0; left < result.Trampolines.size(); ++left) { + for (std::size_t right = left + 1; right < result.Trampolines.size(); ++right) { + const auto& a = result.Trampolines[left]; + const auto& b = result.Trampolines[right]; + require(a.Offset + a.Length <= b.Offset || b.Offset + b.Length <= a.Offset, "A short instruction's site overlaps instructions another stub already moved"); + } + } + bool kept = false; + for (const auto& report : result.Reports) + kept = kept || (report.InstructionName == name && report.Lowering == Codegen::Amd64OnlyLowering::Kept); + require(result.KeptCount == 1 && kept, "An optional short instruction followed by moved instructions was not kept"); + } +} + void converterJumpTableTargets() { const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); const Bytes leaTable = {0x48, 0x8D, 0x0D, 0xF9, 0x1F, 0x00, 0x00}; @@ -952,6 +1038,108 @@ void converterRosetta() { require(intel.Reports.empty() && intel.Bytes == file, "The Intel target converted RDSEED, RDPID or CLWB"); } +void rosettaScalarCompares() { + const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const auto match = [&](const Bytes& bytes) { return rosetta->Match(bytes.data(), bytes.size()); }; + const Bytes compare = {0xC5, 0xEA, 0xC2, 0xCA, 0x03}; + const auto lowered = match(compare); + require(lowered && lowered->Lowering == Codegen::Amd64OnlyLowering::Trampoline && lowered->Optional && !lowered->RelocatableWindow && lowered->InstructionName == "scalar compare", "A Rosetta-hazard scalar compare was not lowered through an optional stub"); + require(lowered->ReturnBranchOffset == 5 && lowered->StubBody.size() == 10 && Bytes(lowered->StubBody.begin(), lowered->StubBody.begin() + 5) == compare && lowered->StubBody[5] == 0xE9 && lowered->Relocations.empty(), "The scalar compare stub does not run the compare unchanged before its return jump"); + const auto unaligned = match(Bytes{0xC5, 0xEA, 0xC2, 0x47, 0x68, 0x03}); + require(unaligned && Bytes(unaligned->StubBody.begin(), unaligned->StubBody.begin() + 6) == Bytes{0xC5, 0xEA, 0xC2, 0x47, 0x68, 0x03} && unaligned->Relocations.empty(), "A memory-form scalar compare does not move unchanged"); + const Bytes rip = {0xC5, 0xEA, 0xC2, 0x0D, 0x00, 0x01, 0x00, 0x00, 0x03}; + const auto ripLowered = match(rip); + require(ripLowered && ripLowered->Relocations.size() == 1 && ripLowered->Relocations[0].DisplacementOffset == 4 && ripLowered->Relocations[0].InstructionEnd == 9 && ripLowered->Relocations[0].SiteTarget == 9 + 0x100, "A RIP-relative scalar compare was not relocated from its place in the site"); + require(Bytes(ripLowered->StubBody.begin() + 4, ripLowered->StubBody.begin() + 8) == Bytes(4, 0), "The RIP-relative scalar compare keeps its displacement in the stub"); + const auto vex3 = match(Bytes{0xC4, 0xE1, 0xEB, 0xC2, 0xCB, 0x03}); + 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}}) + 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)}; + const auto sequence = rosetta->MatchSequence(pair, {}); + 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); + const Bytes text = { + 0xC5, 0xFA, 0x10, 0x15, 0x10, 0x00, 0x00, 0x00, + 0xC5, 0xF8, 0x28, 0xC2, + 0xC5, 0xEA, 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.ReplacedCount == 0 && result.KeptCount == 0 && result.Trampolines.size() == 1 && result.Reports.size() == 1 && result.Reports[0].InstructionName == "scalar compare" && result.Reports[0].Offset == 0x20C && result.Reports[0].Lowering == Codegen::Amd64OnlyLowering::Trampoline, "The Rosetta-hazard compare in the fmax shape was not the only moved instruction"); + const auto& site = result.Trampolines[0]; + require(site.Offset == 0x20C && site.Length == 5 && site.ReturnBranchOffset == 5 && Bytes(site.Body.begin(), site.Body.begin() + 5) == Bytes(text.begin() + 12, text.begin() + 17) && Bytes(site.Body.begin() + 5, site.Body.begin() + 10) == Bytes{0xE9, 0, 0, 0, 0} && site.Relocations.empty(), "The compare stub does not run the compare unchanged and return"); + require(result.Bytes == file, "Moving the compare changed the file image"); + auto branchInside = file; + const Bytes jumped = {0xEB, 0x01, 0xF3, 0x0F, 0xC2, 0xCA, 0x03, 0xC3}; + std::copy(jumped.begin(), jumped.end(), branchInside.begin() + 0x200); + const auto kept = converter->Convert(branchInside, {segmentHeader(jumped.size())}); + require(kept.Trampolines.empty() && kept.KeptCount == 1 && kept.Reports.size() == 1 && kept.Reports[0].Lowering == Codegen::Amd64OnlyLowering::Kept && kept.Bytes == branchInside, "A branch past the prefix of the compare did not keep it native"); + auto branchIntoVex = file; + const Bytes vexJumped = {0xEB, 0x02, 0xC5, 0xEA, 0xC2, 0xCA, 0x03, 0xC3}; + std::copy(vexJumped.begin(), vexJumped.end(), branchIntoVex.begin() + 0x200); + const auto moved = converter->Convert(branchIntoVex, {segmentHeader(vexJumped.size())}); + require(moved.Trampolines.size() == 1 && moved.Trampolines[0].Offset == 0x202 && moved.KeptCount == 0, "A branch past the VEX prefix of the compare, where no instruction starts, kept the compare native"); + auto ripSource = file; + const Bytes ripText = {0xC5, 0xEA, 0xC2, 0x0D, 0x00, 0x01, 0x00, 0x00, 0x03, 0xC3}; + std::copy(ripText.begin(), ripText.end(), ripSource.begin() + 0x200); + const auto ripMoved = converter->Convert(ripSource, {segmentHeader(ripText.size())}); + require(ripMoved.Trampolines.size() == 1 && ripMoved.Trampolines[0].Relocations.size() == 1 && ripMoved.Trampolines[0].Relocations[0].SiteTarget == 9 + 0x100, "A RIP-relative compare source was not relocated"); + const auto intel = Codegen::MakeAmd64OnlyConverter()->Convert(file, {segmentHeader(text.size())}); + require(intel.Reports.empty() && intel.Bytes == file, "The Intel target moved a scalar compare"); +} + void reciprocalOperands() { const auto vex2 = Codegen::DecodeReciprocal(Bytes{0xC5, 0xF8, 0x52, 0xD5}.data(), 4); require(vex2 && vex2->Operation == Codegen::ReciprocalOperation::ReciprocalSquareRoot && vex2->Destination == 2 && vex2->Source == 5, "VEX2 VRSQRTPS was not decoded"); @@ -1086,10 +1274,10 @@ void converterFailureOffsets() { auto movntsRegister = file; movntsRegister[0x212] = 0xC1; require(failureOffset([&] { (void)converter->Convert(movntsRegister, {segmentHeader(20)}); }, "MOVNTSS register form was accepted") == 0x20F, "MOVNTSS failure does not carry the file offset"); - auto rdpru = file; - const Bytes rdpruBytes = {0x0F, 0x01, 0xFD, 0x90}; - std::copy(rdpruBytes.begin(), rdpruBytes.end(), rdpru.begin() + 0x20F); - require(failureOffset([&] { (void)converter->Convert(rdpru, {segmentHeader(20)}); }, "RDPRU was accepted") == 0x20F, "Unsupported instruction failure does not carry the file offset"); + auto lockedWait = file; + const Bytes lockedWaitBytes = {0xF0, 0x0F, 0x01, 0xFB}; + std::copy(lockedWaitBytes.begin(), lockedWaitBytes.end(), lockedWait.begin() + 0x20F); + require(failureOffset([&] { (void)converter->Convert(lockedWait, {segmentHeader(20)}); }, "LOCK MWAITX was accepted") == 0x20F, "Unsupported instruction failure does not carry the file offset"); } Bytes elfFixture(const Bytes& text) { @@ -1140,9 +1328,10 @@ Elfpatcher::Linux::LinuxElfPatcher linuxPatcher() { } std::uint64_t executableFileOffset(const Bytes& output, const std::uint64_t address) { + const auto phOff = read(output, 32); const auto phNum = read(output, 56); for (std::uint16_t index = 0; index < phNum; ++index) { - const auto header = 64 + index * 56; + const auto header = phOff + index * 56; if (read(output, header) != 1) continue; const auto vaddr = read(output, header + 16); const auto memSize = read(output, header + 40); @@ -1190,9 +1379,61 @@ void linuxRipRelativePlacement() { require(operand == 0x2040, "Relocated RIP-relative operand does not reach the original address"); } +std::vector> stackHeaders(const Bytes& output) { + const auto phOff = read(output, 32); + const auto phNum = read(output, 56); + std::vector> found; + for (std::uint16_t index = 0; index < phNum; ++index) { + const auto header = static_cast(phOff) + index * 56; + if (read(output, header) != Elfpatcher::PT_GNU_STACK) continue; + found.push_back({read(output, header), read(output, header + 4), + read(output, header + 8), read(output, header + 16), + read(output, header + 24), read(output, header + 32), + read(output, header + 40), read(output, header + 48)}); + } + return found; } -#if defined(__linux__) && defined(__x86_64__) +void linuxStackSynthesis() { + const auto patchOutput = [](std::uint16_t slots, std::vector headers) { + auto elf = elfFixture({0xC3}); + write(elf, 56, slots); + auto patcher = linuxPatcher(); + return patcher.Patch(std::move(elf), std::move(headers), {}, 0, "$ORIGIN/libs", true, false, {}); + }; + const Domain::ProgramHeader inputStack{Elfpatcher::PT_GNU_STACK, 6, 0, 0, 0, 0, 0x20000, 16}; + const std::vector> synthetic = {{Elfpatcher::PT_GNU_STACK, 6, 0, 0, 0, 0, 0, 16}}; + const std::vector> preserved = {{Elfpatcher::PT_GNU_STACK, 6, 0, 0, 0, 0, 0x20000, 16}}; + + const auto exactFit = patchOutput(7, elfHeaders()); + require(read(exactFit, 32) >= 0x400, "a full input table must move to make room for the synthetic stack"); + require(read(exactFit, 56) == 8, "the output must use the reserved synthetic stack slot"); + require(stackHeaders(exactFit) == synthetic, "the synthetic stack must be PT_GNU_STACK with PF_R|PF_W and alignment 16"); + + const auto spareSlot = patchOutput(8, elfHeaders()); + require(read(spareSlot, 32) == 64, "the required count must not move a table that has a free slot"); + require(read(spareSlot, 56) == 8, "the free slot must be filled by the synthetic stack"); + require(stackHeaders(spareSlot) == synthetic, "the synthetic stack must be PT_GNU_STACK with PF_R|PF_W and alignment 16"); + + const auto roomyTable = patchOutput(9, elfHeaders()); + require(read(roomyTable, 32) == 64, "the required count must not move a table with spare slots"); + require(read(roomyTable, 56) == 8, "the output count must match the required count"); + require(stackHeaders(roomyTable) == synthetic, "the synthetic stack must be PT_GNU_STACK with PF_R|PF_W and alignment 16"); + + const auto keptStackExact = patchOutput(8, {elfHeaders()[0], elfHeaders()[1], inputStack}); + require(read(keptStackExact, 32) == 64, "an input stack must not over-reserve a header slot"); + require(read(keptStackExact, 56) == 8, "an input stack must not grow the output count"); + require(stackHeaders(keptStackExact) == preserved, "a kept PT_GNU_STACK must stay unchanged"); + + const auto keptStackShort = patchOutput(7, {elfHeaders()[0], elfHeaders()[1], inputStack}); + require(read(keptStackShort, 32) >= 0x400, "a full input table must move when the kept stack does not fit"); + require(read(keptStackShort, 56) == 8, "the moved table must carry the kept stack"); + require(stackHeaders(keptStackShort) == preserved, "a kept PT_GNU_STACK must stay unchanged"); +} + +} + +#if (defined(__linux__) || defined(__APPLE__)) && defined(__x86_64__) 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); @@ -1570,7 +1811,13 @@ void sha1Execution() { else require(run.Xmm[reg][0] == xmmIn[reg][0] && run.Xmm[reg][1] == xmmIn[reg][1], "SHA-1 stub clobbered an xmm register"); } - require(run.Rax == rax && run.Rcx == 0x1122334455667788ull && (run.Flags & 0x8D5) == (0x8D7 & 0x8D5), "SHA-1 stub changed a general register or RFLAGS"); + if (run.Rax != rax || run.Rcx != 0x1122334455667788ull || (run.Flags & 0x8D5) != (0x8D7 & 0x8D5)) { + std::cerr << "SHA-1 sample=" << sample << " site=" << std::hex; + for (const auto byte : site) + std::cerr << static_cast(byte) << ' '; + std::cerr << "rax=" << run.Rax << " expected=" << rax << " rcx=" << run.Rcx << " flags=" << run.Flags << std::dec << '\n'; + throw std::runtime_error("SHA-1 stub changed a general register or RFLAGS"); + } } } } @@ -1625,6 +1872,92 @@ void ripRelativeExecution() { require(movedRun.Rax == movedRun.SiteAddress + 4096 + 0x30, "Moved RIP-relative LEA did not compute the original address"); unchanged(movedRun, (1u << 1) | (1u << 3), "Stub with moved RIP-relative instructions clobbered a register or RFLAGS"); } + +void rdpruExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(); + const Bytes site = {0x0F, 0x01, 0xFD}; + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "RDPRU stub was not produced"); + auto body = match->StubBody; + const auto ret = body.size(); + body.push_back(0xC3); + const auto displacement = static_cast(ret - (match->ReturnBranchOffset + 5)); + std::memcpy(body.data() + match->ReturnBranchOffset + 1, &displacement, sizeof(displacement)); + void* code = mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + require(code != MAP_FAILED, "cannot map executable memory for the RDPRU stub"); + std::memcpy(code, body.data(), body.size()); + for (const std::uint32_t selector : {0u, 1u, 2u, 0xFFFFFFFFu}) { + std::uint64_t raxOut = 0; + std::uint64_t rdxOut = 0; + std::uint64_t rcxOut = 0; + std::uint64_t flagsOut = 0; + std::uint64_t canaryOut = 0; + asm volatile( + "sub $128, %%rsp\n\t" + "movabs $0xA5A5A5A5A5A5A5A5, %%rax\n\t" + "lea -136(%%rsp), %%rdi\n\t" + "mov $16, %%ecx\n\t" + "rep stosq\n\t" + "movl %[selector], %%ecx\n\t" + "pushq $0x8D7\n\t" + "popfq\n\t" + "call *%[code]\n\t" + "mov %%rax, %[rax]\n\t" + "mov %%rdx, %[rdx]\n\t" + "mov %%rcx, %[rcx]\n\t" + "pushfq\n\t" + "popq %[flags]\n\t" + "xor %%r8, %%r8\n\t" + "lea -136(%%rsp), %%rsi\n\t" + "mov $16, %%ecx\n\t" + "1:\n\t" + "xor (%%rsi), %%r8\n\t" + "add $8, %%rsi\n\t" + "dec %%ecx\n\t" + "jnz 1b\n\t" + "mov %%r8, %[canary]\n\t" + "add $128, %%rsp\n\t" + : [rax] "=r"(raxOut), [rdx] "=r"(rdxOut), [rcx] "=r"(rcxOut), [flags] "=r"(flagsOut), [canary] "=r"(canaryOut) + : [selector] "r"(selector), [code] "r"(code) + : "rax", "rcx", "rdx", "rsi", "rdi", "r8", "memory", "cc"); + require(canaryOut == 0, "RDPRU stub clobbered the red zone"); + require(rcxOut == selector, "RDPRU stub changed ecx"); + require((flagsOut & 0x8D5) == (0x8D7 & 0x8D5), "RDPRU stub changed RFLAGS"); + if (selector <= 1) + require((raxOut | rdxOut) != 0, "RDPRU MPERF/APERF selector did not yield the TSC approximation"); + else + require(raxOut == 0 && rdxOut == 0, "RDPRU with an undefined selector did not yield zero"); + } + munmap(code, 4096); +} + +void mcommitExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(); + const Bytes site = {0xF3, 0x0F, 0x01, 0xFA}; + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::InPlace, "MCOMMIT lowering was not produced"); + auto body = match->ReplacementBytes; + body.push_back(0xC3); + void* code = mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + require(code != MAP_FAILED, "cannot map executable memory for the MCOMMIT lowering"); + std::memcpy(code, body.data(), body.size()); + for (const std::uint64_t flagsIn : {std::uint64_t{0x8D6}, std::uint64_t{0x8D7}}) { + std::uint64_t flagsOut = 0; + asm volatile( + "mov %[in], %%rax\n\t" + "pushq %%rax\n\t" + "popfq\n\t" + "call *%[code]\n\t" + "pushfq\n\t" + "popq %[out]\n\t" + : [out] "=r"(flagsOut) + : [in] "r"(flagsIn), [code] "r"(code) + : "rax", "memory", "cc"); + require((flagsOut & 1) == 1, "MCOMMIT lowering did not report committed"); + require((flagsOut & 0x8D4) == (flagsIn & 0x8D4), "MCOMMIT lowering changed flags other than CF"); + } + munmap(code, 4096); +} #else void reciprocalExecution() {} void registerFormExecution() {} @@ -1633,6 +1966,42 @@ void sha256Execution() {} void clzeroExecution() {} void immediateFormExecution() {} void ripRelativeExecution() {} +void rdpruExecution() {} +void mcommitExecution() {} +#endif + +#if defined(__x86_64__) && (defined(__linux__) || defined(__APPLE__)) +void rdtscpExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes site = {0x0F, 0x01, 0xF9}; + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "RDTSCP stub was not produced"); + auto body = match->StubBody; + body[match->ReturnBranchOffset] = 0xC3; + void* code = mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + require(code != MAP_FAILED, "cannot map executable memory for the stub"); + std::memcpy(code, body.data(), body.size()); + std::uint64_t low = 0; + std::uint64_t high = 0; + std::uint64_t processor = 0; + std::uint8_t carry = 0; + asm volatile( + "mov $-1, %%rcx\n\t" + "sub $128, %%rsp\n\t" + "stc\n\t" + "call *%[code]\n\t" + "setc %[carry]\n\t" + "add $128, %%rsp\n\t" + : "=&a"(low), "=&d"(high), "=&c"(processor), [carry] "=&q"(carry) + : [code] "r"(code) + : "memory", "cc"); + munmap(code, 4096); + require(((high << 32) | low) != 0, "RDTSCP stub did not read the time stamp counter"); + require(processor == 0, "RDTSCP stub left a value other than processor 0 in RCX"); + require(carry == 1, "RDTSCP stub changed the flags"); +} +#else +void rdtscpExecution() {} #endif void scannerZeroTail() { @@ -1643,6 +2012,18 @@ void scannerZeroTail() { requireFailure([&] { (void)scanner->ScanCodeSection(truncated, 0, truncated.size()); }, "Truncated non-zero tail must still fail"); } +void scannerUnwindRanges() { + const Bytes slot{0x48, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x2E, 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC3}; + const auto gap = Codegen::MakeInstructionScanner({}, {{16, 17}})->ScanCodeSection(slot, 0x1000, slot.size()); + require(gap.size() == 2 && gap[0].Offset == 0x1001 && gap[0].Length == 15 && gap[1].Offset == 0x1010 && gap[1].Length == 1, "An undecodable byte outside every unwind range must resume the scan at the next byte"); + requireFailure([&] { (void)Codegen::MakeInstructionScanner({}, {{0, 17}})->ScanCodeSection(slot, 0, slot.size()); }, "An undecodable byte inside an unwind range must still fail"); + requireFailure([&] { (void)Codegen::MakeInstructionScanner()->ScanCodeSection(slot, 0, slot.size()); }, "An undecodable byte without unwind ranges must still fail"); + const Bytes crossing{0xB8, 0x0F, 0x05, 0xC3, 0x90}; + const auto cut = Codegen::MakeInstructionScanner({}, {{3, 4}, {1, 3}})->ScanCodeSection(crossing, 0, crossing.size()); + require(cut.size() == 3 && cut[0].Offset == 1 && cut[0].Length == 2 && cut[1].Offset == 3 && cut[2].Offset == 4, "A gap instruction that crosses into an unwind range must end at the range"); + require(Codegen::MakeInstructionScanner()->ScanCodeSection(crossing, 0, crossing.size()).front().Length == 5, "Without unwind ranges the crossing instruction must decode whole"); +} + int main() { try { decoderLengths(); @@ -1664,16 +2045,25 @@ int main() { clzeroExecution(); reciprocalExecution(); ripRelativeExecution(); + rdpruExecution(); + mcommitExecution(); converterSegment(); converterRipRelativeFollower(); converterSha256(); converterSha1(); converterMonitorWait(); + converterInPlaceFollower(); converterClzero(); rosettaSubstitutions(); converterRosetta(); + rosettaScalarCompares(); + converterRosettaScalarCompare(); + rosettaLegacyScalarCompares(); + converterRosettaLegacyScalarCompare(); + rdtscpExecution(); wideStoreSplit(); converterWideStoreWindows(); + converterClaimedFollower(); converterJumpTableTargets(); converterReciprocal(); converterStrayRex(); @@ -1682,7 +2072,9 @@ int main() { converterFailureOffsets(); linuxPlacement(); linuxRipRelativePlacement(); + linuxStackSynthesis(); scannerZeroTail(); + scannerUnwindRanges(); std::cout << "AMD64-only converter tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/codegen/tests/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 e141e1539e..3ad1299b0d 100644 --- a/core/relinker/codegen/tests/DecoderLengthTests.cpp +++ b/core/relinker/codegen/tests/DecoderLengthTests.cpp @@ -1,4 +1,5 @@ #include "DecoderLengthCases.hpp" +#include "DecoderMetadataCases.hpp" #include #include #include @@ -77,10 +78,71 @@ int main() { } catch (const Codegen::CodegenException&) { } } + for (const auto& testCase : kDecoderRipRelativeCases) { + const std::string name = testCase.Bytes; + auto bytes = parse(testCase.Bytes); + require(bytes.size() == testCase.Length, "RIP-relative case " + name + " does not hold its own length"); + bytes.insert(bytes.end(), 15, 0x90); + require(decodedLength(decoder, bytes, name) == testCase.Length, "Wrong length for " + name); + const auto info = decoder.DecodeInstruction(bytes.data(), bytes.size()); + require(info.HasRipRelativeDisp, "RIP-relative operand of " + name + " was not reported"); + require(info.RipRelativeDispOffset == testCase.DisplacementOffset, "Wrong RIP-relative displacement offset for " + name); + } + for (const auto& testCase : kDecoderBranchCases) { + const std::string name = testCase.Bytes; + auto bytes = parse(testCase.Bytes); + require(bytes.size() == testCase.Length, "Branch case " + name + " does not hold its own length"); + bytes.insert(bytes.end(), 15, 0x90); + const auto info = decoder.DecodeInstruction(bytes.data(), bytes.size()); + require(info.Length == testCase.Length, "Wrong length for " + name); + require(info.FlowKind == testCase.FlowKind, "Wrong control flow kind for " + name); + require(info.HasBranchTarget == testCase.HasBranchTarget, "Wrong branch target presence for " + name); + require(!testCase.HasBranchTarget || info.BranchDisp == testCase.BranchDisp, "Wrong branch displacement for " + name); + } + 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"); require(threeDNowInfo.HasRipRelativeDisp && threeDNowInfo.RipRelativeDispOffset == 3, "3DNow! RIP-relative displacement was not reported"); + for (const auto& testCase : std::vector>{ + {"6590", false}, + {"654889c8", false}, + {"65488b0c2578030000", true}, + {"65a4", true}, + {"650fa2", false}, + {"650ff7c1", true}, + {"65660ff7c1", true}, + {"65c5f9f7c1", true}, + {"65c4e179f7c1", true}, + {"650f01c8", true}, + {"650f01fa", true}, + {"650f01fc", true}, + {"650f01c9", false}, + {"650f01fd", false}, + {"65f30faef0", true}, + {"f20faef0", false}, + {"660faef0", false}, + {"f30faee8", false}, + {"ff10", true}, + {"ffe0", false}, + }) { + const auto bytes = parse(testCase.first); + require(decoder.DecodeInstruction(bytes.data(), bytes.size()).HasMemoryOperand == testCase.second, + "Wrong memory operand presence for " + std::string(testCase.first)); + } std::cout << "Decoder length tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/codegen/tests/DecoderMetadataCases.hpp b/core/relinker/codegen/tests/DecoderMetadataCases.hpp new file mode 100644 index 0000000000..d29d0c0191 --- /dev/null +++ b/core/relinker/codegen/tests/DecoderMetadataCases.hpp @@ -0,0 +1,819 @@ +#ifndef CODEGEN_TESTS_DECODERMETADATACASES_HPP +#define CODEGEN_TESTS_DECODERMETADATACASES_HPP + +#include +#include +#include + +struct DecoderRipRelativeCase { + const char* Bytes; + std::size_t Length; + std::size_t DisplacementOffset; +}; + +struct DecoderBranchCase { + const char* Bytes; + std::size_t Length; + Codegen::ControlFlowKind FlowKind; + bool HasBranchTarget; + std::int64_t BranchDisp; +}; + +inline constexpr DecoderRipRelativeCase kDecoderRipRelativeCases[] = { + {"ff0511111111", 6, 2}, + {"ff0d11111111", 6, 2}, + {"ff1511111111", 6, 2}, + {"ff1d11111111", 6, 2}, + {"ff2511111111", 6, 2}, + {"ff2d11111111", 6, 2}, + {"ff3511111111", 6, 2}, + {"000511111111", 6, 2}, + {"010511111111", 6, 2}, + {"020511111111", 6, 2}, + {"030511111111", 6, 2}, + {"080511111111", 6, 2}, + {"090511111111", 6, 2}, + {"0a0511111111", 6, 2}, + {"0b0511111111", 6, 2}, + {"100511111111", 6, 2}, + {"110511111111", 6, 2}, + {"120511111111", 6, 2}, + {"130511111111", 6, 2}, + {"180511111111", 6, 2}, + {"190511111111", 6, 2}, + {"1a0511111111", 6, 2}, + {"1b0511111111", 6, 2}, + {"200511111111", 6, 2}, + {"210511111111", 6, 2}, + {"220511111111", 6, 2}, + {"230511111111", 6, 2}, + {"280511111111", 6, 2}, + {"290511111111", 6, 2}, + {"2a0511111111", 6, 2}, + {"2b0511111111", 6, 2}, + {"300511111111", 6, 2}, + {"310511111111", 6, 2}, + {"320511111111", 6, 2}, + {"330511111111", 6, 2}, + {"380511111111", 6, 2}, + {"390511111111", 6, 2}, + {"3a0511111111", 6, 2}, + {"3b0511111111", 6, 2}, + {"630511111111", 6, 2}, + {"666905111111111111", 9, 3}, + {"69051111111111111111", 10, 2}, + {"6b051111111111", 7, 2}, + {"80051111111111", 7, 2}, + {"668105111111111111", 9, 3}, + {"81051111111111111111", 10, 2}, + {"83051111111111", 7, 2}, + {"840511111111", 6, 2}, + {"850511111111", 6, 2}, + {"860511111111", 6, 2}, + {"870511111111", 6, 2}, + {"880511111111", 6, 2}, + {"890511111111", 6, 2}, + {"8a0511111111", 6, 2}, + {"8b0511111111", 6, 2}, + {"8c0511111111", 6, 2}, + {"8d0511111111", 6, 2}, + {"8e0511111111", 6, 2}, + {"c0051111111111", 7, 2}, + {"c1051111111111", 7, 2}, + {"c6051111111111", 7, 2}, + {"66c705111111111111", 9, 3}, + {"c7051111111111111111", 10, 2}, + {"d00511111111", 6, 2}, + {"d10511111111", 6, 2}, + {"d20511111111", 6, 2}, + {"d30511111111", 6, 2}, + {"d80511111111", 6, 2}, + {"d90511111111", 6, 2}, + {"da0511111111", 6, 2}, + {"db0511111111", 6, 2}, + {"dc0511111111", 6, 2}, + {"dd0511111111", 6, 2}, + {"de0511111111", 6, 2}, + {"df0511111111", 6, 2}, + {"f61511111111", 6, 2}, + {"f6051111111111", 7, 2}, + {"f71511111111", 6, 2}, + {"66f705111111111111", 9, 3}, + {"f7051111111111111111", 10, 2}, + {"fe0511111111", 6, 2}, + {"0f000511111111", 7, 3}, + {"0f010511111111", 7, 3}, + {"0f020511111111", 7, 3}, + {"0f030511111111", 7, 3}, + {"0f0d0511111111", 7, 3}, + {"0f100511111111", 7, 3}, + {"0f110511111111", 7, 3}, + {"0f120511111111", 7, 3}, + {"0f130511111111", 7, 3}, + {"0f140511111111", 7, 3}, + {"0f150511111111", 7, 3}, + {"0f160511111111", 7, 3}, + {"0f170511111111", 7, 3}, + {"0f180511111111", 7, 3}, + {"0f190511111111", 7, 3}, + {"0f1a0511111111", 7, 3}, + {"0f1b0511111111", 7, 3}, + {"0f1c0511111111", 7, 3}, + {"0f1d0511111111", 7, 3}, + {"0f1e0511111111", 7, 3}, + {"0f1f0511111111", 7, 3}, + {"0f280511111111", 7, 3}, + {"0f290511111111", 7, 3}, + {"0f2a0511111111", 7, 3}, + {"0f2b0511111111", 7, 3}, + {"0f2c0511111111", 7, 3}, + {"0f2d0511111111", 7, 3}, + {"0f2e0511111111", 7, 3}, + {"0f2f0511111111", 7, 3}, + {"0f400511111111", 7, 3}, + {"0f410511111111", 7, 3}, + {"0f420511111111", 7, 3}, + {"0f430511111111", 7, 3}, + {"0f440511111111", 7, 3}, + {"0f450511111111", 7, 3}, + {"0f460511111111", 7, 3}, + {"0f470511111111", 7, 3}, + {"0f480511111111", 7, 3}, + {"0f490511111111", 7, 3}, + {"0f4a0511111111", 7, 3}, + {"0f4b0511111111", 7, 3}, + {"0f4c0511111111", 7, 3}, + {"0f4d0511111111", 7, 3}, + {"0f4e0511111111", 7, 3}, + {"0f4f0511111111", 7, 3}, + {"0f510511111111", 7, 3}, + {"0f520511111111", 7, 3}, + {"0f530511111111", 7, 3}, + {"0f540511111111", 7, 3}, + {"0f550511111111", 7, 3}, + {"0f560511111111", 7, 3}, + {"0f570511111111", 7, 3}, + {"0f580511111111", 7, 3}, + {"0f590511111111", 7, 3}, + {"0f5a0511111111", 7, 3}, + {"0f5b0511111111", 7, 3}, + {"0f5c0511111111", 7, 3}, + {"0f5d0511111111", 7, 3}, + {"0f5e0511111111", 7, 3}, + {"0f5f0511111111", 7, 3}, + {"0f600511111111", 7, 3}, + {"0f610511111111", 7, 3}, + {"0f620511111111", 7, 3}, + {"0f630511111111", 7, 3}, + {"0f640511111111", 7, 3}, + {"0f650511111111", 7, 3}, + {"0f660511111111", 7, 3}, + {"0f670511111111", 7, 3}, + {"0f680511111111", 7, 3}, + {"0f690511111111", 7, 3}, + {"0f6a0511111111", 7, 3}, + {"0f6b0511111111", 7, 3}, + {"660f6c0511111111", 8, 4}, + {"660f6d0511111111", 8, 4}, + {"0f6e0511111111", 7, 3}, + {"0f6f0511111111", 7, 3}, + {"0f70051111111111", 8, 3}, + {"0f740511111111", 7, 3}, + {"0f750511111111", 7, 3}, + {"0f760511111111", 7, 3}, + {"660f7c0511111111", 8, 4}, + {"660f7d0511111111", 8, 4}, + {"0f7e0511111111", 7, 3}, + {"0f7f0511111111", 7, 3}, + {"0f900511111111", 7, 3}, + {"0f910511111111", 7, 3}, + {"0f920511111111", 7, 3}, + {"0f930511111111", 7, 3}, + {"0f940511111111", 7, 3}, + {"0f950511111111", 7, 3}, + {"0f960511111111", 7, 3}, + {"0f970511111111", 7, 3}, + {"0f980511111111", 7, 3}, + {"0f990511111111", 7, 3}, + {"0f9a0511111111", 7, 3}, + {"0f9b0511111111", 7, 3}, + {"0f9c0511111111", 7, 3}, + {"0f9d0511111111", 7, 3}, + {"0f9e0511111111", 7, 3}, + {"0f9f0511111111", 7, 3}, + {"0fa30511111111", 7, 3}, + {"0fa4051111111111", 8, 3}, + {"0fa50511111111", 7, 3}, + {"0fab0511111111", 7, 3}, + {"0fac051111111111", 8, 3}, + {"0fad0511111111", 7, 3}, + {"0fae0511111111", 7, 3}, + {"0faf0511111111", 7, 3}, + {"0fb00511111111", 7, 3}, + {"0fb10511111111", 7, 3}, + {"0fb20511111111", 7, 3}, + {"0fb30511111111", 7, 3}, + {"0fb40511111111", 7, 3}, + {"0fb50511111111", 7, 3}, + {"0fb60511111111", 7, 3}, + {"0fb70511111111", 7, 3}, + {"f30fb80511111111", 8, 4}, + {"0fb90511111111", 7, 3}, + {"0fba251111111111", 8, 3}, + {"0fbb0511111111", 7, 3}, + {"0fbc0511111111", 7, 3}, + {"0fbd0511111111", 7, 3}, + {"0fbe0511111111", 7, 3}, + {"0fbf0511111111", 7, 3}, + {"0fc00511111111", 7, 3}, + {"0fc10511111111", 7, 3}, + {"0fc2051111111111", 8, 3}, + {"0fc30511111111", 7, 3}, + {"0fc4051111111111", 8, 3}, + {"0fc6051111111111", 8, 3}, + {"0fc70d11111111", 7, 3}, + {"660fd00511111111", 8, 4}, + {"0fd10511111111", 7, 3}, + {"0fd20511111111", 7, 3}, + {"0fd30511111111", 7, 3}, + {"0fd40511111111", 7, 3}, + {"0fd50511111111", 7, 3}, + {"660fd60511111111", 8, 4}, + {"0fd80511111111", 7, 3}, + {"0fd90511111111", 7, 3}, + {"0fda0511111111", 7, 3}, + {"0fdb0511111111", 7, 3}, + {"0fdc0511111111", 7, 3}, + {"0fdd0511111111", 7, 3}, + {"0fde0511111111", 7, 3}, + {"0fdf0511111111", 7, 3}, + {"0fe00511111111", 7, 3}, + {"0fe10511111111", 7, 3}, + {"0fe20511111111", 7, 3}, + {"0fe30511111111", 7, 3}, + {"0fe40511111111", 7, 3}, + {"0fe50511111111", 7, 3}, + {"660fe60511111111", 8, 4}, + {"0fe70511111111", 7, 3}, + {"0fe80511111111", 7, 3}, + {"0fe90511111111", 7, 3}, + {"0fea0511111111", 7, 3}, + {"0feb0511111111", 7, 3}, + {"0fec0511111111", 7, 3}, + {"0fed0511111111", 7, 3}, + {"0fee0511111111", 7, 3}, + {"0fef0511111111", 7, 3}, + {"f20ff00511111111", 8, 4}, + {"0ff10511111111", 7, 3}, + {"0ff20511111111", 7, 3}, + {"0ff30511111111", 7, 3}, + {"0ff40511111111", 7, 3}, + {"0ff50511111111", 7, 3}, + {"0ff60511111111", 7, 3}, + {"0ff80511111111", 7, 3}, + {"0ff90511111111", 7, 3}, + {"0ffa0511111111", 7, 3}, + {"0ffb0511111111", 7, 3}, + {"0ffc0511111111", 7, 3}, + {"0ffd0511111111", 7, 3}, + {"0ffe0511111111", 7, 3}, + {"0f38000511111111", 8, 4}, + {"0f38010511111111", 8, 4}, + {"0f38020511111111", 8, 4}, + {"0f38030511111111", 8, 4}, + {"0f38040511111111", 8, 4}, + {"0f38050511111111", 8, 4}, + {"0f38060511111111", 8, 4}, + {"0f38070511111111", 8, 4}, + {"0f38080511111111", 8, 4}, + {"0f38090511111111", 8, 4}, + {"0f380a0511111111", 8, 4}, + {"0f380b0511111111", 8, 4}, + {"660f38100511111111", 9, 5}, + {"660f38140511111111", 9, 5}, + {"660f38150511111111", 9, 5}, + {"660f38170511111111", 9, 5}, + {"0f381c0511111111", 8, 4}, + {"0f381d0511111111", 8, 4}, + {"0f381e0511111111", 8, 4}, + {"660f38200511111111", 9, 5}, + {"660f38210511111111", 9, 5}, + {"660f38220511111111", 9, 5}, + {"660f38230511111111", 9, 5}, + {"660f38240511111111", 9, 5}, + {"660f38250511111111", 9, 5}, + {"660f38280511111111", 9, 5}, + {"660f38290511111111", 9, 5}, + {"660f382a0511111111", 9, 5}, + {"660f382b0511111111", 9, 5}, + {"660f38300511111111", 9, 5}, + {"660f38310511111111", 9, 5}, + {"660f38320511111111", 9, 5}, + {"660f38330511111111", 9, 5}, + {"660f38340511111111", 9, 5}, + {"660f38350511111111", 9, 5}, + {"660f38370511111111", 9, 5}, + {"660f38380511111111", 9, 5}, + {"660f38390511111111", 9, 5}, + {"660f383a0511111111", 9, 5}, + {"660f383b0511111111", 9, 5}, + {"660f383c0511111111", 9, 5}, + {"660f383d0511111111", 9, 5}, + {"660f383e0511111111", 9, 5}, + {"660f383f0511111111", 9, 5}, + {"660f38400511111111", 9, 5}, + {"660f38410511111111", 9, 5}, + {"660f38820511111111", 9, 5}, + {"0f38c80511111111", 8, 4}, + {"0f38c90511111111", 8, 4}, + {"0f38ca0511111111", 8, 4}, + {"0f38cb0511111111", 8, 4}, + {"0f38cc0511111111", 8, 4}, + {"0f38cd0511111111", 8, 4}, + {"f30f38d80511111111", 9, 5}, + {"660f38db0511111111", 9, 5}, + {"660f38dc0511111111", 9, 5}, + {"660f38dd0511111111", 9, 5}, + {"660f38de0511111111", 9, 5}, + {"660f38df0511111111", 9, 5}, + {"0f38f00511111111", 8, 4}, + {"0f38f10511111111", 8, 4}, + {"660f38f60511111111", 9, 5}, + {"660f3a08051111111111", 10, 5}, + {"660f3a09051111111111", 10, 5}, + {"660f3a0a051111111111", 10, 5}, + {"660f3a0b051111111111", 10, 5}, + {"660f3a0c051111111111", 10, 5}, + {"660f3a0d051111111111", 10, 5}, + {"660f3a0e051111111111", 10, 5}, + {"0f3a0f051111111111", 9, 4}, + {"660f3a14051111111111", 10, 5}, + {"660f3a15051111111111", 10, 5}, + {"660f3a16051111111111", 10, 5}, + {"660f3a17051111111111", 10, 5}, + {"660f3a20051111111111", 10, 5}, + {"660f3a21051111111111", 10, 5}, + {"660f3a22051111111111", 10, 5}, + {"660f3a40051111111111", 10, 5}, + {"660f3a41051111111111", 10, 5}, + {"660f3a42051111111111", 10, 5}, + {"660f3a44051111111111", 10, 5}, + {"660f3a60051111111111", 10, 5}, + {"660f3a61051111111111", 10, 5}, + {"660f3a62051111111111", 10, 5}, + {"660f3a63051111111111", 10, 5}, + {"0f3acc051111111111", 9, 4}, + {"660f3adf051111111111", 10, 5}, + {"c5f8100511111111", 8, 4}, + {"c5f8110511111111", 8, 4}, + {"c5f8120511111111", 8, 4}, + {"c5f8130511111111", 8, 4}, + {"c5f8140511111111", 8, 4}, + {"c5f8150511111111", 8, 4}, + {"c5f8160511111111", 8, 4}, + {"c5f8170511111111", 8, 4}, + {"c5f8280511111111", 8, 4}, + {"c5f8290511111111", 8, 4}, + {"c5fa2a0511111111", 8, 4}, + {"c5f82b0511111111", 8, 4}, + {"c5fa2c0511111111", 8, 4}, + {"c5fa2d0511111111", 8, 4}, + {"c5f82e0511111111", 8, 4}, + {"c5f82f0511111111", 8, 4}, + {"c5f8510511111111", 8, 4}, + {"c5f8520511111111", 8, 4}, + {"c5f8530511111111", 8, 4}, + {"c5f8540511111111", 8, 4}, + {"c5f8550511111111", 8, 4}, + {"c5f8560511111111", 8, 4}, + {"c5f8570511111111", 8, 4}, + {"c5f8580511111111", 8, 4}, + {"c5f8590511111111", 8, 4}, + {"c5f85a0511111111", 8, 4}, + {"c5f85b0511111111", 8, 4}, + {"c5f85c0511111111", 8, 4}, + {"c5f85d0511111111", 8, 4}, + {"c5f85e0511111111", 8, 4}, + {"c5f85f0511111111", 8, 4}, + {"c5f9600511111111", 8, 4}, + {"c5f9610511111111", 8, 4}, + {"c5f9620511111111", 8, 4}, + {"c5f9630511111111", 8, 4}, + {"c5f9640511111111", 8, 4}, + {"c5f9650511111111", 8, 4}, + {"c5f9660511111111", 8, 4}, + {"c5f9670511111111", 8, 4}, + {"c5f9680511111111", 8, 4}, + {"c5f9690511111111", 8, 4}, + {"c5f96a0511111111", 8, 4}, + {"c5f96b0511111111", 8, 4}, + {"c5f96c0511111111", 8, 4}, + {"c5f96d0511111111", 8, 4}, + {"c5f96e0511111111", 8, 4}, + {"c5f96f0511111111", 8, 4}, + {"c5f970051111111111", 9, 4}, + {"c5f9740511111111", 8, 4}, + {"c5f9750511111111", 8, 4}, + {"c5f9760511111111", 8, 4}, + {"c5f97c0511111111", 8, 4}, + {"c5f97d0511111111", 8, 4}, + {"c5f97e0511111111", 8, 4}, + {"c5f97f0511111111", 8, 4}, + {"c5f8ae1511111111", 8, 4}, + {"c5f8c2051111111111", 9, 4}, + {"c5f9c4051111111111", 9, 4}, + {"c5f8c6051111111111", 9, 4}, + {"c5f9d00511111111", 8, 4}, + {"c5f9d10511111111", 8, 4}, + {"c5f9d20511111111", 8, 4}, + {"c5f9d30511111111", 8, 4}, + {"c5f9d40511111111", 8, 4}, + {"c5f9d50511111111", 8, 4}, + {"c5f9d60511111111", 8, 4}, + {"c5f9d80511111111", 8, 4}, + {"c5f9d90511111111", 8, 4}, + {"c5f9da0511111111", 8, 4}, + {"c5f9db0511111111", 8, 4}, + {"c5f9dc0511111111", 8, 4}, + {"c5f9dd0511111111", 8, 4}, + {"c5f9de0511111111", 8, 4}, + {"c5f9df0511111111", 8, 4}, + {"c5f9e00511111111", 8, 4}, + {"c5f9e10511111111", 8, 4}, + {"c5f9e20511111111", 8, 4}, + {"c5f9e30511111111", 8, 4}, + {"c5f9e40511111111", 8, 4}, + {"c5f9e50511111111", 8, 4}, + {"c5f9e60511111111", 8, 4}, + {"c5f9e70511111111", 8, 4}, + {"c5f9e80511111111", 8, 4}, + {"c5f9e90511111111", 8, 4}, + {"c5f9ea0511111111", 8, 4}, + {"c5f9eb0511111111", 8, 4}, + {"c5f9ec0511111111", 8, 4}, + {"c5f9ed0511111111", 8, 4}, + {"c5f9ee0511111111", 8, 4}, + {"c5f9ef0511111111", 8, 4}, + {"c5fbf00511111111", 8, 4}, + {"c5f9f10511111111", 8, 4}, + {"c5f9f20511111111", 8, 4}, + {"c5f9f30511111111", 8, 4}, + {"c5f9f40511111111", 8, 4}, + {"c5f9f50511111111", 8, 4}, + {"c5f9f60511111111", 8, 4}, + {"c5f9f80511111111", 8, 4}, + {"c5f9f90511111111", 8, 4}, + {"c5f9fa0511111111", 8, 4}, + {"c5f9fb0511111111", 8, 4}, + {"c5f9fc0511111111", 8, 4}, + {"c5f9fd0511111111", 8, 4}, + {"c5f9fe0511111111", 8, 4}, + {"c4e178100511111111", 9, 5}, + {"c4e178110511111111", 9, 5}, + {"c4e178120511111111", 9, 5}, + {"c4e178130511111111", 9, 5}, + {"c4e178140511111111", 9, 5}, + {"c4e178150511111111", 9, 5}, + {"c4e178160511111111", 9, 5}, + {"c4e178170511111111", 9, 5}, + {"c4e178280511111111", 9, 5}, + {"c4e178290511111111", 9, 5}, + {"c4e17a2a0511111111", 9, 5}, + {"c4e1782b0511111111", 9, 5}, + {"c4e17a2c0511111111", 9, 5}, + {"c4e17a2d0511111111", 9, 5}, + {"c4e1782e0511111111", 9, 5}, + {"c4e1782f0511111111", 9, 5}, + {"c4e178510511111111", 9, 5}, + {"c4e178520511111111", 9, 5}, + {"c4e178530511111111", 9, 5}, + {"c4e178540511111111", 9, 5}, + {"c4e178550511111111", 9, 5}, + {"c4e178560511111111", 9, 5}, + {"c4e178570511111111", 9, 5}, + {"c4e178580511111111", 9, 5}, + {"c4e178590511111111", 9, 5}, + {"c4e1785a0511111111", 9, 5}, + {"c4e1785b0511111111", 9, 5}, + {"c4e1785c0511111111", 9, 5}, + {"c4e1785d0511111111", 9, 5}, + {"c4e1785e0511111111", 9, 5}, + {"c4e1785f0511111111", 9, 5}, + {"c4e179600511111111", 9, 5}, + {"c4e179610511111111", 9, 5}, + {"c4e179620511111111", 9, 5}, + {"c4e179630511111111", 9, 5}, + {"c4e179640511111111", 9, 5}, + {"c4e179650511111111", 9, 5}, + {"c4e179660511111111", 9, 5}, + {"c4e179670511111111", 9, 5}, + {"c4e179680511111111", 9, 5}, + {"c4e179690511111111", 9, 5}, + {"c4e1796a0511111111", 9, 5}, + {"c4e1796b0511111111", 9, 5}, + {"c4e1796c0511111111", 9, 5}, + {"c4e1796d0511111111", 9, 5}, + {"c4e1796e0511111111", 9, 5}, + {"c4e1796f0511111111", 9, 5}, + {"c4e17970051111111111", 10, 5}, + {"c4e179740511111111", 9, 5}, + {"c4e179750511111111", 9, 5}, + {"c4e179760511111111", 9, 5}, + {"c4e1797c0511111111", 9, 5}, + {"c4e1797d0511111111", 9, 5}, + {"c4e1797e0511111111", 9, 5}, + {"c4e1797f0511111111", 9, 5}, + {"c4e178ae1511111111", 9, 5}, + {"c4e178c2051111111111", 10, 5}, + {"c4e179c4051111111111", 10, 5}, + {"c4e178c6051111111111", 10, 5}, + {"c4e179d00511111111", 9, 5}, + {"c4e179d10511111111", 9, 5}, + {"c4e179d20511111111", 9, 5}, + {"c4e179d30511111111", 9, 5}, + {"c4e179d40511111111", 9, 5}, + {"c4e179d50511111111", 9, 5}, + {"c4e179d60511111111", 9, 5}, + {"c4e179d80511111111", 9, 5}, + {"c4e179d90511111111", 9, 5}, + {"c4e179da0511111111", 9, 5}, + {"c4e179db0511111111", 9, 5}, + {"c4e179dc0511111111", 9, 5}, + {"c4e179dd0511111111", 9, 5}, + {"c4e179de0511111111", 9, 5}, + {"c4e179df0511111111", 9, 5}, + {"c4e179e00511111111", 9, 5}, + {"c4e179e10511111111", 9, 5}, + {"c4e179e20511111111", 9, 5}, + {"c4e179e30511111111", 9, 5}, + {"c4e179e40511111111", 9, 5}, + {"c4e179e50511111111", 9, 5}, + {"c4e179e60511111111", 9, 5}, + {"c4e179e70511111111", 9, 5}, + {"c4e179e80511111111", 9, 5}, + {"c4e179e90511111111", 9, 5}, + {"c4e179ea0511111111", 9, 5}, + {"c4e179eb0511111111", 9, 5}, + {"c4e179ec0511111111", 9, 5}, + {"c4e179ed0511111111", 9, 5}, + {"c4e179ee0511111111", 9, 5}, + {"c4e179ef0511111111", 9, 5}, + {"c4e17bf00511111111", 9, 5}, + {"c4e179f10511111111", 9, 5}, + {"c4e179f20511111111", 9, 5}, + {"c4e179f30511111111", 9, 5}, + {"c4e179f40511111111", 9, 5}, + {"c4e179f50511111111", 9, 5}, + {"c4e179f60511111111", 9, 5}, + {"c4e179f80511111111", 9, 5}, + {"c4e179f90511111111", 9, 5}, + {"c4e179fa0511111111", 9, 5}, + {"c4e179fb0511111111", 9, 5}, + {"c4e179fc0511111111", 9, 5}, + {"c4e179fd0511111111", 9, 5}, + {"c4e179fe0511111111", 9, 5}, + {"c4e279000511111111", 9, 5}, + {"c4e279010511111111", 9, 5}, + {"c4e279020511111111", 9, 5}, + {"c4e279030511111111", 9, 5}, + {"c4e279040511111111", 9, 5}, + {"c4e279050511111111", 9, 5}, + {"c4e279060511111111", 9, 5}, + {"c4e279070511111111", 9, 5}, + {"c4e279080511111111", 9, 5}, + {"c4e279090511111111", 9, 5}, + {"c4e2790a0511111111", 9, 5}, + {"c4e2790b0511111111", 9, 5}, + {"c4e2790c0511111111", 9, 5}, + {"c4e2790d0511111111", 9, 5}, + {"c4e2790e0511111111", 9, 5}, + {"c4e2790f0511111111", 9, 5}, + {"c4e279130511111111", 9, 5}, + {"c4e27d160511111111", 9, 5}, + {"c4e279170511111111", 9, 5}, + {"c4e279180511111111", 9, 5}, + {"c4e27d190511111111", 9, 5}, + {"c4e27d1a0511111111", 9, 5}, + {"c4e2791c0511111111", 9, 5}, + {"c4e2791d0511111111", 9, 5}, + {"c4e2791e0511111111", 9, 5}, + {"c4e279200511111111", 9, 5}, + {"c4e279210511111111", 9, 5}, + {"c4e279220511111111", 9, 5}, + {"c4e279230511111111", 9, 5}, + {"c4e279240511111111", 9, 5}, + {"c4e279250511111111", 9, 5}, + {"c4e279280511111111", 9, 5}, + {"c4e279290511111111", 9, 5}, + {"c4e2792a0511111111", 9, 5}, + {"c4e2792b0511111111", 9, 5}, + {"c4e2792c0511111111", 9, 5}, + {"c4e2792d0511111111", 9, 5}, + {"c4e2792e0511111111", 9, 5}, + {"c4e2792f0511111111", 9, 5}, + {"c4e279300511111111", 9, 5}, + {"c4e279310511111111", 9, 5}, + {"c4e279320511111111", 9, 5}, + {"c4e279330511111111", 9, 5}, + {"c4e279340511111111", 9, 5}, + {"c4e279350511111111", 9, 5}, + {"c4e27d360511111111", 9, 5}, + {"c4e279370511111111", 9, 5}, + {"c4e279380511111111", 9, 5}, + {"c4e279390511111111", 9, 5}, + {"c4e2793a0511111111", 9, 5}, + {"c4e2793b0511111111", 9, 5}, + {"c4e2793c0511111111", 9, 5}, + {"c4e2793d0511111111", 9, 5}, + {"c4e2793e0511111111", 9, 5}, + {"c4e2793f0511111111", 9, 5}, + {"c4e279400511111111", 9, 5}, + {"c4e279410511111111", 9, 5}, + {"c4e279450511111111", 9, 5}, + {"c4e279460511111111", 9, 5}, + {"c4e279470511111111", 9, 5}, + {"c4e279580511111111", 9, 5}, + {"c4e279590511111111", 9, 5}, + {"c4e27d5a0511111111", 9, 5}, + {"c4e279780511111111", 9, 5}, + {"c4e279790511111111", 9, 5}, + {"c4e2798c0511111111", 9, 5}, + {"c4e2798e0511111111", 9, 5}, + {"c4e279960511111111", 9, 5}, + {"c4e279970511111111", 9, 5}, + {"c4e279980511111111", 9, 5}, + {"c4e279990511111111", 9, 5}, + {"c4e2799a0511111111", 9, 5}, + {"c4e2799b0511111111", 9, 5}, + {"c4e2799c0511111111", 9, 5}, + {"c4e2799d0511111111", 9, 5}, + {"c4e2799e0511111111", 9, 5}, + {"c4e2799f0511111111", 9, 5}, + {"c4e279a60511111111", 9, 5}, + {"c4e279a70511111111", 9, 5}, + {"c4e279a80511111111", 9, 5}, + {"c4e279a90511111111", 9, 5}, + {"c4e279aa0511111111", 9, 5}, + {"c4e279ab0511111111", 9, 5}, + {"c4e279ac0511111111", 9, 5}, + {"c4e279ad0511111111", 9, 5}, + {"c4e279ae0511111111", 9, 5}, + {"c4e279af0511111111", 9, 5}, + {"c4e279b60511111111", 9, 5}, + {"c4e279b70511111111", 9, 5}, + {"c4e279b80511111111", 9, 5}, + {"c4e279b90511111111", 9, 5}, + {"c4e279ba0511111111", 9, 5}, + {"c4e279bb0511111111", 9, 5}, + {"c4e279bc0511111111", 9, 5}, + {"c4e279bd0511111111", 9, 5}, + {"c4e279be0511111111", 9, 5}, + {"c4e279bf0511111111", 9, 5}, + {"c4e279db0511111111", 9, 5}, + {"c4e279dc0511111111", 9, 5}, + {"c4e279dd0511111111", 9, 5}, + {"c4e279de0511111111", 9, 5}, + {"c4e279df0511111111", 9, 5}, + {"c4e278f20511111111", 9, 5}, + {"c4e278f30d11111111", 9, 5}, + {"c4e278f50511111111", 9, 5}, + {"c4e27bf60511111111", 9, 5}, + {"c4e278f70511111111", 9, 5}, + {"c4e3fd00051111111111", 10, 5}, + {"c4e3fd01051111111111", 10, 5}, + {"c4e37902051111111111", 10, 5}, + {"c4e37904051111111111", 10, 5}, + {"c4e37905051111111111", 10, 5}, + {"c4e37d06051111111111", 10, 5}, + {"c4e37908051111111111", 10, 5}, + {"c4e37909051111111111", 10, 5}, + {"c4e3790a051111111111", 10, 5}, + {"c4e3790b051111111111", 10, 5}, + {"c4e3790c051111111111", 10, 5}, + {"c4e3790d051111111111", 10, 5}, + {"c4e3790e051111111111", 10, 5}, + {"c4e3790f051111111111", 10, 5}, + {"c4e37914051111111111", 10, 5}, + {"c4e37915051111111111", 10, 5}, + {"c4e37916051111111111", 10, 5}, + {"c4e37917051111111111", 10, 5}, + {"c4e37d18051111111111", 10, 5}, + {"c4e37d19051111111111", 10, 5}, + {"c4e3791d051111111111", 10, 5}, + {"c4e37920051111111111", 10, 5}, + {"c4e37921051111111111", 10, 5}, + {"c4e37922051111111111", 10, 5}, + {"c4e37d38051111111111", 10, 5}, + {"c4e37d39051111111111", 10, 5}, + {"c4e37940051111111111", 10, 5}, + {"c4e37941051111111111", 10, 5}, + {"c4e37942051111111111", 10, 5}, + {"c4e37944051111111111", 10, 5}, + {"c4e37d46051111111111", 10, 5}, + {"c4e3794a051111111111", 10, 5}, + {"c4e3794b051111111111", 10, 5}, + {"c4e3794c051111111111", 10, 5}, + {"c4e37960051111111111", 10, 5}, + {"c4e37961051111111111", 10, 5}, + {"c4e37962051111111111", 10, 5}, + {"c4e37963051111111111", 10, 5}, + {"c4e379df051111111111", 10, 5}, + {"c4e37bf0051111111111", 10, 5}, +}; + +inline constexpr DecoderBranchCase kDecoderBranchCases[] = { + {"0f0b", 2, Codegen::ControlFlowKind::Trap, false, 0}, + {"0f80c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f81c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f82c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f83c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f84c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f85c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f86c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f87c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f88c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f89c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8ac0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8bc0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8cc0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8dc0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8ec0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8fc0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0fb9c0", 3, Codegen::ControlFlowKind::Trap, false, 0}, + {"480fb9c0", 4, Codegen::ControlFlowKind::Trap, false, 0}, + {"48cac011", 4, Codegen::ControlFlowKind::Return, false, 0}, + {"48cb", 2, Codegen::ControlFlowKind::Return, false, 0}, + {"660fb9c0", 4, Codegen::ControlFlowKind::Trap, false, 0}, + {"66c2c011", 4, Codegen::ControlFlowKind::Return, false, 0}, + {"66c3", 2, Codegen::ControlFlowKind::Return, false, 0}, + {"66cac011", 4, Codegen::ControlFlowKind::Return, false, 0}, + {"66cb", 2, Codegen::ControlFlowKind::Return, false, 0}, + {"66ff18", 3, Codegen::ControlFlowKind::IndirectCall, false, 0}, + {"66ff28", 3, Codegen::ControlFlowKind::IndirectJump, false, 0}, + {"66ffd0", 3, Codegen::ControlFlowKind::IndirectCall, false, 0}, + {"66ffe0", 3, Codegen::ControlFlowKind::IndirectJump, false, 0}, + {"70c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"71c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"72c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"73c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"74c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"75c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"76c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"77c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"78c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"79c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7ac0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7bc0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7cc0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7dc0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7ec0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7fc0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"c2c011", 3, Codegen::ControlFlowKind::Return, false, 0}, + {"c3", 1, Codegen::ControlFlowKind::Return, false, 0}, + {"cac011", 3, Codegen::ControlFlowKind::Return, false, 0}, + {"cb", 1, Codegen::ControlFlowKind::Return, false, 0}, + {"e0c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"e1c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"e2c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"e3c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"e8c0111111", 5, Codegen::ControlFlowKind::Call, true, 286331328}, + {"e9c0111111", 5, Codegen::ControlFlowKind::UnconditionalJump, true, 286331328}, + {"ebc0", 2, Codegen::ControlFlowKind::UnconditionalJump, true, -64}, + {"ff1511111111", 6, Codegen::ControlFlowKind::Call, false, 0}, + {"ff1d11111111", 6, Codegen::ControlFlowKind::IndirectCall, false, 0}, + {"ff2511111111", 6, Codegen::ControlFlowKind::UnconditionalJump, true, 286331153}, + {"ff2d11111111", 6, Codegen::ControlFlowKind::IndirectJump, false, 0}, + {"ff18", 2, Codegen::ControlFlowKind::IndirectCall, false, 0}, + {"ff28", 2, Codegen::ControlFlowKind::IndirectJump, false, 0}, + {"ffd0", 2, Codegen::ControlFlowKind::IndirectCall, false, 0}, + {"ffe0", 2, Codegen::ControlFlowKind::IndirectJump, false, 0}, + {"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/domain/include/domain/GuestRuntime.hpp b/core/relinker/domain/include/domain/GuestRuntime.hpp index 5f1e25c734..d4828544a6 100644 --- a/core/relinker/domain/include/domain/GuestRuntime.hpp +++ b/core/relinker/domain/include/domain/GuestRuntime.hpp @@ -15,6 +15,7 @@ struct GuestImport { std::uint32_t RelocationType = 1; std::string Library; bool Function = false; + bool Weak = false; }; struct GuestRuntime { diff --git a/core/relinker/domain/include/domain/ImportModule.hpp b/core/relinker/domain/include/domain/ImportModule.hpp index 37bab4c73e..18b018f770 100644 --- a/core/relinker/domain/include/domain/ImportModule.hpp +++ b/core/relinker/domain/include/domain/ImportModule.hpp @@ -16,6 +16,12 @@ inline std::string_view ModuleSuffix(const std::string_view name) { return suffix == ModuleSuffixes.end() ? std::string_view{} : *suffix; } +inline std::string NeededLibraryName(std::string name) { + const auto suffix = ModuleSuffix(name); + if (!suffix.empty() && suffix != ".prx") name.replace(name.size() - suffix.size(), suffix.size(), ".prx"); + return name; +} + inline std::string ImportModuleName(std::string name) { name.resize(name.size() - ModuleSuffix(name).size()); if (name.ends_with("-module")) name.resize(name.size() - 7); diff --git a/core/relinker/domain/include/domain/Types.hpp b/core/relinker/domain/include/domain/Types.hpp index 31405091c5..6757f8507a 100644 --- a/core/relinker/domain/include/domain/Types.hpp +++ b/core/relinker/domain/include/domain/Types.hpp @@ -16,6 +16,12 @@ using FileByteOffset = std::uint64_t; using VirtualAddress = std::uint64_t; using ByteCount = std::uint64_t; +struct CodeRange { + ByteCount Begin; + ByteCount End; + bool operator==(const CodeRange&) const = default; +}; + struct ElfHeader { std::uint16_t Machine; std::uint16_t Type; @@ -100,7 +106,9 @@ struct SysVDynamicSection { std::vector GuestModules; std::map ImportModules; std::set FunctionImports; + std::set NamedImports; std::set UnmatchedImportModules; + std::vector NeededLibraries; }; struct CallRegistryEntry { @@ -112,6 +120,7 @@ struct CallRegistryEntry { FileByteOffset TargetOffset; std::vector CallSites; bool CallSitesResolved; + bool Weak = false; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp index 95099d7477..9da009bc08 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp @@ -1,6 +1,7 @@ #ifndef ELFPATCHER_DOMAIN_ELFCONSTANTS_HPP #define ELFPATCHER_DOMAIN_ELFCONSTANTS_HPP +#include #include namespace Elfpatcher { @@ -93,6 +94,9 @@ inline constexpr std::int64_t DT_RELASZ = 8; inline constexpr std::int64_t DT_RELAENT = 9; inline constexpr std::int64_t DT_STRSZ = 10; inline constexpr std::int64_t DT_SYMENT = 11; +inline constexpr std::int64_t DT_REL = 17; +inline constexpr std::int64_t DT_RELSZ = 18; +inline constexpr std::int64_t DT_RELENT = 19; inline constexpr std::int64_t DT_PLTREL = 20; inline constexpr std::int64_t DT_JMPREL = 23; inline constexpr std::int64_t DT_INIT = 12; @@ -105,7 +109,11 @@ inline constexpr std::int64_t DT_RUNPATH = 29; inline constexpr std::int64_t DT_FLAGS = 30; inline constexpr std::int64_t DT_PREINIT_ARRAY = 32; inline constexpr std::int64_t DT_PREINIT_ARRAYSZ = 33; +inline constexpr std::int64_t DT_RELRSZ = 35; +inline constexpr std::int64_t DT_RELR = 36; +inline constexpr std::int64_t DT_RELRENT = 37; inline constexpr std::uint64_t DF_BIND_NOW = 0x8; +inline constexpr std::uint64_t DF_SYMBOLIC = 0x2; inline constexpr std::int64_t DT_OS_INIT = 0x6000000c; inline constexpr std::int64_t DT_OS_FINI = 0x6000000d; diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp index a59418beea..5ca491e9bf 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp @@ -2,14 +2,16 @@ #define ELFPATCHER_GUESTMODULEWRITER_HPP #include +#include +#include namespace Elfpatcher { class GuestModuleWriter { public: std::vector WriteLinux(const Relinker::GuestImage& image, const std::vector& dependencies, const std::string& runPath) const; - std::vector WriteWindows(const Relinker::GuestImage& image, Domain::GuestRuntime& runtime) const; - std::vector WriteMacOs(const Relinker::GuestImage& image, const std::vector& dependencies, const std::string& runPath) const; + std::vector WriteWindows(const Relinker::GuestImage& image, Domain::GuestRuntime& runtime, const std::function& message = {}) const; + std::vector WriteMacOs(const Relinker::GuestImage& image, const std::vector& dependencies, const std::string& runPath, const std::function& message = {}) const; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/IElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/IElfPatcher.hpp index f01272daa6..5b1e8dbb62 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/IElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/IElfPatcher.hpp @@ -14,7 +14,7 @@ public: virtual ~IElfPatcher() = default; virtual std::vector Patch( - const std::vector& sourceElf, + std::vector sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp index 189256c86d..4d89ad7698 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp @@ -17,6 +17,8 @@ namespace Elfpatcher { std::uint64_t ComputeExtraBlockVaddr(const std::vector& originalHeaders, std::uint64_t extraBlockOffset) const override; + std::uint64_t GuestLoadPageEnd(const std::vector& originalHeaders) const; + std::size_t RequiredProgramHeaderCount(const std::vector& originalHeaders) const override; std::uint16_t WriteLayout( @@ -28,13 +30,13 @@ namespace Elfpatcher { std::shared_ptr _segmentFilter; std::shared_ptr _byteWriter; - [[nodiscard]] Domain::ProgramHeader _makeLoadHeader(std::uint64_t offset, std::uint64_t vaddr, std::uint64_t size) const; - [[nodiscard]] Domain::ProgramHeader _makeHeaderBlockLoad(std::uint64_t vaddr, std::uint64_t size, std::uint64_t align) const; + [[nodiscard]] Domain::ProgramHeader _makeLoadHeader(std::uint64_t offset, std::uint64_t vaddr, std::uint64_t size, std::uint64_t align, std::uint64_t memorySize, std::uint32_t flags) const; [[nodiscard]] Domain::ProgramHeader _makePhdrHeader(std::uint64_t offset, std::uint64_t vaddr, std::uint64_t size) const; [[nodiscard]] Domain::ProgramHeader _makeDynamicHeader(std::uint64_t offset, std::uint64_t vaddr, std::uint64_t size) const; [[nodiscard]] Domain::ProgramHeader _makeInterpHeader(std::uint64_t offset, std::uint64_t vaddr, std::uint64_t size) const; [[nodiscard]] std::uint32_t _fixLoadFlags(std::uint32_t originalFlags) const; [[nodiscard]] std::vector _keptHeaders(const std::vector& originalHeaders) const; + [[nodiscard]] std::vector _outputHeaders(const std::vector& originalHeaders) const; void _sortLoadHeaders(std::vector& headers) const; void _writeProgramHeader(std::vector& buf, std::size_t offset, const Domain::ProgramHeader& ph) const; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/SegmentFilter.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/SegmentFilter.hpp index 4d404c68d4..76b2e1c6f7 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/SegmentFilter.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/SegmentFilter.hpp @@ -7,9 +7,11 @@ namespace Elfpatcher { class SegmentFilter : public ISegmentFilter { public: + explicit SegmentFilter(bool lazyBinding = false) : _lazyBinding(lazyBinding) {} bool ShouldSkip(const Domain::ProgramHeader& ph) const override; private: + bool _lazyBinding; [[nodiscard]] bool _isSceSpecificSegment(std::uint32_t type) const; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp index 19719a7b0f..58aebbcd67 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp @@ -14,6 +14,8 @@ namespace Elfpatcher::Linux { +inline constexpr std::uint64_t DefaultFixedBase = 0x400000; + class LinuxElfPatcher : public IElfPatcher { public: LinuxElfPatcher( @@ -21,11 +23,12 @@ public: std::shared_ptr programHeaderLayoutBuilder, std::shared_ptr sectionHeaderTableBuilder, std::shared_ptr byteWriter, - std::optional fixedBase = std::nullopt + std::optional fixedBase = std::nullopt, + bool traceImports = false ); std::vector Patch( - const std::vector& sourceElf, + std::vector sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, @@ -41,6 +44,7 @@ private: std::shared_ptr _sectionHeaderTableBuilder; std::shared_ptr _byteWriter; std::optional _fixedBase; + bool _traceImports; void _appendDynEntry(std::vector& buf, std::int64_t tag, std::uint64_t val) const; void _appendTrampoline(std::vector& buf, const Codegen::TrampolineSite& site, std::uint64_t extraBlockOffset, const std::function& vaddrOfExtraBlockOffset) const; diff --git a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp index ed2ad7e750..99bc214e54 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp @@ -5,20 +5,27 @@ #include #include #include +#include #include +#include #include namespace Elfpatcher::MacOs { +inline constexpr std::uint64_t ExportKindAbsolute = 2; +inline constexpr std::uint64_t ExportWeakDefinition = 4; + struct MacOsBind { std::string Symbol; std::uint32_t Rva; std::int64_t Addend = 0; + bool Weak = false; }; struct MacOsExport { std::string Symbol; - std::uint32_t Rva; + std::uint64_t Value; + std::uint64_t Flags = 0; }; struct MacOsTlsImport { @@ -31,6 +38,7 @@ struct MacOsTlsImport { struct MacOsTlsExport { std::string Symbol; std::uint64_t Offset; + std::uint64_t Flags = 0; }; struct MacOsImageInput { @@ -53,6 +61,7 @@ struct MacOsImageInput { std::vector TlsImports; std::vector TlsExports; const std::vector* Trampolines = nullptr; + std::function Message; }; std::uint64_t GuestTlsBlockSize(const Domain::ProgramHeader& tls); @@ -60,6 +69,7 @@ std::uint64_t GuestTlsBlockSize(const Domain::ProgramHeader& tls); std::vector WriteMacOsImage(MacOsImageInput& input); std::string MachOLoadPath(const std::string& path, const std::string& origin); +std::vector MachORunPaths(const std::string& paths, const std::string& origin); std::string MachODependency(const std::string& name, const std::string& origin); diff --git a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp index 2fe4475716..2f2ffbc485 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp @@ -2,13 +2,19 @@ #define ELFPATCHER_MACOS_MACHOPATCHER_HPP #include +#include +#include +#include +#include namespace Elfpatcher::MacOs { class MacOsMachOPatcher final : public IElfPatcher { public: + explicit MacOsMachOPatcher(std::optional fixedBase = std::nullopt, std::function message = {}); + std::vector Patch( - const std::vector& sourceElf, + std::vector sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, @@ -17,6 +23,10 @@ public: bool dependencyDiagnostics, const std::vector& trampolines ) override; + +private: + std::optional fixedBase; + std::function message; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp index c9918ebb50..07db6f888a 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp @@ -6,15 +6,16 @@ #include #include #include +#include namespace Elfpatcher::Windows { class WindowsPePatcher : public IElfPatcher { public: - explicit WindowsPePatcher(bool windowsGui = false, std::optional> icon = std::nullopt, std::filesystem::path iconName = "icon0.png", std::function progress = {}, std::optional fixedBase = std::nullopt, bool traceImports = false); + explicit WindowsPePatcher(bool windowsGui = false, std::optional> icon = std::nullopt, std::filesystem::path iconName = "icon0.png", std::function progress = {}, std::optional fixedBase = std::nullopt, bool traceImports = false, std::function message = {}); const std::string& GetDiagnostics() const; - std::vector Patch(const std::vector& sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector& trampolines) override; + std::vector Patch(std::vector sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector& trampolines) override; private: std::function progress; @@ -24,6 +25,7 @@ private: std::filesystem::path iconName; std::optional fixedBase; bool traceImports; + std::function message; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp index dc6d8cf6e6..fa2ef59297 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp @@ -19,8 +19,16 @@ public: std::vector BuildSections() const; private: + struct Block { + std::uint64_t Offset; + std::vector Bytes; + }; + + Block& backBlock(std::uint64_t offset, std::uint64_t length); + std::vector segments; - std::vector data; + std::vector blocks; + std::uint64_t size; std::vector pageFlags; std::uint64_t firstAddress; std::uint64_t imageBase; diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp index 237b762c60..790dfdf3ae 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp @@ -1,6 +1,7 @@ #ifndef ELFPATCHER_WINDOWS_PEFORMAT_HPP #define ELFPATCHER_WINDOWS_PEFORMAT_HPP +#include #include #include #include @@ -21,6 +22,21 @@ struct PeSection { std::uint32_t Rva; std::uint32_t Characteristics; std::vector Data; + std::uint32_t VirtualSize = 0; + + std::uint64_t MemorySize() const { + return std::max(Data.size(), VirtualSize); + } + + bool Writable(const std::uint32_t rva, const std::uint64_t size) { + if (rva < Rva || rva - Rva > MemorySize() || size > MemorySize() - (rva - Rva)) + return false; + if (rva - Rva + size > Data.size()) { + VirtualSize = static_cast(MemorySize()); + Data.resize(static_cast(rva - Rva + size)); + } + return true; + } }; struct PeDirectory { diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp index 207ef639bf..39b4a54f87 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp @@ -3,12 +3,13 @@ #include #include +#include namespace Elfpatcher::Windows { class WindowsTlsBuilder { public: - PeDirectory Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva = nullptr, const std::function& progress = {}) const; + PeDirectory Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva = nullptr, const std::function& progress = {}, const std::function& message = {}) const; }; } diff --git a/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp b/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp index c02856a8f4..fc23e28dea 100644 --- a/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp +++ b/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp @@ -6,10 +6,49 @@ #include #include +#include #include namespace Elfpatcher { +namespace { + +bool hasWritableLoadCoverage( + const std::vector& headers, + const Domain::ProgramHeader& relro +) { + if (relro.MemorySize == 0) return true; + if (relro.MemorySize > std::numeric_limits::max() - relro.MappedAddress) return false; + const std::uint64_t pageMask = kDefaultLoadAlignment - 1; + const std::uint64_t relroStart = relro.MappedAddress & ~pageMask; + const std::uint64_t relroEndBytes = relro.MappedAddress + relro.MemorySize; + if (relroEndBytes > std::numeric_limits::max() - pageMask) return false; + const std::uint64_t relroEnd = (relroEndBytes + pageMask) & ~pageMask; + if (relroStart == relroEnd) return true; + + std::vector> writableRanges; + for (const auto& header : headers) { + if (header.Type != PT_LOAD || (header.Flags & PF_W) == 0) continue; + const std::uint64_t size = std::max(header.FileSize, header.MemorySize); + if (size > std::numeric_limits::max() - header.MappedAddress) continue; + const std::uint64_t start = header.MappedAddress & ~pageMask; + const std::uint64_t endBytes = header.MappedAddress + size; + if (endBytes > std::numeric_limits::max() - pageMask) continue; + const std::uint64_t end = (endBytes + pageMask) & ~pageMask; + if (start < end) writableRanges.emplace_back(start, end); + } + std::sort(writableRanges.begin(), writableRanges.end()); + std::uint64_t coveredUntil = relroStart; + for (const auto& [start, end] : writableRanges) { + if (start > coveredUntil) break; + if (end > coveredUntil) coveredUntil = end; + if (coveredUntil >= relroEnd) return true; + } + return false; +} + +} + ProgramHeaderLayoutBuilder::ProgramHeaderLayoutBuilder( std::shared_ptr segmentFilter, std::shared_ptr byteWriter @@ -30,28 +69,15 @@ void ProgramHeaderLayoutBuilder::_writeProgramHeader(std::vector& _byteWriter->WriteU64(buf, offset + kPhdrAlignOffset, ph.Alignment); } -Domain::ProgramHeader ProgramHeaderLayoutBuilder::_makeLoadHeader(std::uint64_t offset, std::uint64_t vaddr, std::uint64_t size) const { +Domain::ProgramHeader ProgramHeaderLayoutBuilder::_makeLoadHeader(std::uint64_t offset, std::uint64_t vaddr, std::uint64_t size, std::uint64_t align, std::uint64_t memorySize, std::uint32_t flags) const { Domain::ProgramHeader ph{}; ph.Type = PT_LOAD; - ph.Flags = PF_R | PF_W | PF_X; + ph.Flags = flags; ph.Offset = offset; ph.MappedAddress = vaddr; ph.PhysicalAddress = vaddr; ph.FileSize = size; - ph.MemorySize = size; - ph.Alignment = kDefaultLoadAlignment; - return ph; -} - -Domain::ProgramHeader ProgramHeaderLayoutBuilder::_makeHeaderBlockLoad(std::uint64_t vaddr, std::uint64_t size, std::uint64_t align) const { - Domain::ProgramHeader ph{}; - ph.Type = PT_LOAD; - ph.Flags = PF_R; - ph.Offset = 0; - ph.MappedAddress = vaddr; - ph.PhysicalAddress = vaddr; - ph.FileSize = size; - ph.MemorySize = size; + ph.MemorySize = memorySize < size ? size : memorySize; ph.Alignment = align; return ph; } @@ -103,6 +129,38 @@ std::uint32_t ProgramHeaderLayoutBuilder::_fixLoadFlags(std::uint32_t originalFl std::uint64_t ProgramHeaderLayoutBuilder::ComputeExtraBlockVaddr( const std::vector& originalHeaders, std::uint64_t extraBlockOffset +) const { + const std::uint64_t pageFloor = GuestLoadPageEnd(originalHeaders); + + const std::uint64_t offsetRemainder = extraBlockOffset & (kDefaultLoadAlignment - 1); + if (pageFloor > std::numeric_limits::max() - offsetRemainder) + throw Domain::RelinkerException("Extra block vaddr computation overflowed"); + return pageFloor + offsetRemainder; +} + +std::vector ProgramHeaderLayoutBuilder::_keptHeaders(const std::vector& originalHeaders) const { + std::vector kept; + for (const auto& ph : originalHeaders) { + if (_segmentFilter->ShouldSkip(ph)) continue; + if (ph.Type == PT_GNU_RELRO && !hasWritableLoadCoverage(originalHeaders, ph)) continue; + kept.push_back(ph); + } + return kept; +} + +std::vector ProgramHeaderLayoutBuilder::_outputHeaders(const std::vector& originalHeaders) const { + auto kept = _keptHeaders(originalHeaders); + if (std::none_of(kept.begin(), kept.end(), [](const Domain::ProgramHeader& ph) { return ph.Type == PT_GNU_STACK; })) + kept.push_back(Domain::ProgramHeader{PT_GNU_STACK, PF_R | PF_W, 0, 0, 0, 0, 0, 16}); + return kept; +} + +std::size_t ProgramHeaderLayoutBuilder::RequiredProgramHeaderCount(const std::vector& originalHeaders) const { + return _outputHeaders(originalHeaders).size() + kSyntheticProgramHeaderCount; +} + +std::uint64_t ProgramHeaderLayoutBuilder::GuestLoadPageEnd( + const std::vector& originalHeaders ) const { bool foundLoad = false; std::uint64_t highestVaddrEnd = 0; @@ -112,6 +170,8 @@ std::uint64_t ProgramHeaderLayoutBuilder::ComputeExtraBlockVaddr( if (ph.Type != PT_LOAD) continue; foundLoad = true; + if (ph.FileSize > ph.MemorySize) + throw Domain::RelinkerException("PT_LOAD file size exceeds memory size", ph.Offset); if (ph.MemorySize > std::numeric_limits::max() - ph.MappedAddress) throw Domain::RelinkerException("Load segment virtual range overflowed"); const std::uint64_t vaddrEnd = ph.MappedAddress + ph.MemorySize; @@ -120,36 +180,16 @@ std::uint64_t ProgramHeaderLayoutBuilder::ComputeExtraBlockVaddr( } if (!foundLoad) throw Domain::RelinkerException("No kept PT_LOAD segment to anchor the extra block against"); - - const std::uint64_t align = kDefaultLoadAlignment; - const std::uint64_t offsetRemainder = extraBlockOffset & (align - 1); - const std::uint64_t alignedFloor = highestVaddrEnd & ~(align - 1); - std::uint64_t vaddr = alignedFloor | offsetRemainder; - if (vaddr < highestVaddrEnd) - vaddr += align; - if (vaddr < highestVaddrEnd) + if (highestVaddrEnd > std::numeric_limits::max() - (kDefaultLoadAlignment - 1)) throw Domain::RelinkerException("Extra block vaddr computation overflowed"); - return vaddr; -} - -std::vector ProgramHeaderLayoutBuilder::_keptHeaders(const std::vector& originalHeaders) const { - std::vector kept; - for (const auto& ph : originalHeaders) { - if (!_segmentFilter->ShouldSkip(ph)) - kept.push_back(ph); - } - return kept; -} - -std::size_t ProgramHeaderLayoutBuilder::RequiredProgramHeaderCount(const std::vector& originalHeaders) const { - return _keptHeaders(originalHeaders).size() + kSyntheticProgramHeaderCount; + return (highestVaddrEnd + kDefaultLoadAlignment - 1) & ~(kDefaultLoadAlignment - 1); } std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout( std::vector& buf, const ProgramHeaderLayoutRequest& request ) const { - auto kept = _keptHeaders(request.OriginalHeaders); + auto kept = _outputHeaders(request.OriginalHeaders); const auto frameCount = std::count_if(kept.begin(), kept.end(), [](const Domain::ProgramHeader& ph) { return ph.Type == PT_GNU_EH_FRAME; }); const bool keepFrames = kept.size() + kSyntheticProgramHeaderCount <= request.PhNum; @@ -175,8 +215,12 @@ std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout( if (tableEnd > buf.size()) throw Domain::RelinkerException("Program header table exceeds the output file"); const bool tableInExtraBlock = request.PhOff >= request.ExtraBlockOffset; - if (tableInExtraBlock && tableEnd - request.ExtraBlockOffset > request.ExtraBlockSize) - throw Domain::RelinkerException("Program header table does not fit within the extra block"); + if (tableInExtraBlock) { + if (tableEnd - request.ExtraBlockOffset > request.ExtraBlockSize) + throw Domain::RelinkerException("Program header table does not fit within the extra block"); + } else if (tableEnd > request.ExtraBlockOffset) { + throw Domain::RelinkerException("Program header table overlaps the extra block"); + } const std::uint64_t headerBlockSize = tableInExtraBlock ? kElfHeaderSize : tableEnd; if (request.ExtraBlockOffset > buf.size() || request.ExtraBlockSize > buf.size() - request.ExtraBlockOffset) throw Domain::RelinkerException("Extra block exceeds the output file"); @@ -185,8 +229,12 @@ std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout( if (ph.Type == PT_LOAD && !_segmentFilter->ShouldSkip(ph) && ph.Alignment > headerBlockAlign && (ph.Alignment & (ph.Alignment - 1)) == 0) headerBlockAlign = ph.Alignment; } + + const std::uint64_t extraBlockVaddr = ComputeExtraBlockVaddr(request.OriginalHeaders, request.ExtraBlockOffset); + if (request.ExtraBlockVaddr != extraBlockVaddr) + throw Domain::RelinkerException("Extra block vaddr does not match its file offset"); const std::uint64_t headerBlockVaddr = - checkedEnd(checkedEnd(request.ExtraBlockVaddr, request.ExtraBlockSize), headerBlockAlign - 1) & ~(headerBlockAlign - 1); + checkedEnd(checkedEnd(extraBlockVaddr, request.ExtraBlockSize), headerBlockAlign - 1) & ~(headerBlockAlign - 1); checkedEnd(headerBlockVaddr, headerBlockSize); if (request.DynamicSegmentOffset < request.ExtraBlockOffset) @@ -200,11 +248,11 @@ std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout( request.InterpSize > request.ExtraBlockSize - (request.InterpOffset - request.ExtraBlockOffset)) throw Domain::RelinkerException("Interp data does not fit within the extra block"); - const std::uint64_t dynamicSegmentVaddr = request.ExtraBlockVaddr + (request.DynamicSegmentOffset - request.ExtraBlockOffset); - const std::uint64_t interpVaddr = request.ExtraBlockVaddr + (request.InterpOffset - request.ExtraBlockOffset); + const std::uint64_t dynamicSegmentVaddr = extraBlockVaddr + (request.DynamicSegmentOffset - request.ExtraBlockOffset); + const std::uint64_t interpVaddr = extraBlockVaddr + (request.InterpOffset - request.ExtraBlockOffset); std::vector layout; - const std::uint64_t phdrVaddr = tableInExtraBlock ? request.ExtraBlockVaddr + (request.PhOff - request.ExtraBlockOffset) : headerBlockVaddr + request.PhOff; + const std::uint64_t phdrVaddr = tableInExtraBlock ? extraBlockVaddr + (request.PhOff - request.ExtraBlockOffset) : headerBlockVaddr + request.PhOff; layout.push_back(_makePhdrHeader(request.PhOff, phdrVaddr, static_cast(neededPh) * request.PhEntSize)); layout.push_back(_makeInterpHeader(request.InterpOffset, interpVaddr, request.InterpSize)); for (auto ph : kept) { @@ -212,8 +260,9 @@ std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout( ph.Flags = _fixLoadFlags(ph.Flags); layout.push_back(ph); } - layout.push_back(_makeLoadHeader(request.ExtraBlockOffset, request.ExtraBlockVaddr, request.ExtraBlockSize)); - layout.push_back(_makeHeaderBlockLoad(headerBlockVaddr, headerBlockSize, headerBlockAlign)); + const std::uint64_t extraBlockPageEnd = checkedEnd(checkedEnd(extraBlockVaddr, request.ExtraBlockSize), kDefaultLoadAlignment - 1) & ~(kDefaultLoadAlignment - 1); + layout.push_back(_makeLoadHeader(request.ExtraBlockOffset, extraBlockVaddr, request.ExtraBlockSize, kDefaultLoadAlignment, extraBlockPageEnd - extraBlockVaddr, PF_R | PF_W | PF_X)); + layout.push_back(_makeLoadHeader(0, headerBlockVaddr, headerBlockSize, headerBlockAlign, headerBlockSize, PF_R)); layout.push_back(_makeDynamicHeader(request.DynamicSegmentOffset, dynamicSegmentVaddr, request.DynamicSegmentSize)); for (std::size_t index = 0; index < layout.size(); ++index) diff --git a/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp b/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp index a7c4cc1f0f..e07068514f 100644 --- a/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp +++ b/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp @@ -1,8 +1,10 @@ #include #include #include -#include +#include #include +#include +#include namespace Elfpatcher { @@ -41,14 +43,39 @@ void SectionHeaderTableBuilder::WriteTable( std::vector& buf, const SectionHeaderTableRequest& request ) const { + if (buf.size() < kElfHeaderSize) + throw std::runtime_error("Output program header table is out of bounds"); const auto phOffset = Io::ReadU64(buf, kEhdrPhOffOffset); const auto phCount = Io::ReadU16(buf, kEhdrPhNumOffset); const auto phStride = Io::ReadU16(buf, kEhdrPhEntSizeOffset); - if (phStride < 56 || phOffset > buf.size() || phCount > (buf.size() - phOffset) / phStride) + if (phStride < 56 || (phCount > 0 && phOffset < kElfHeaderSize) || phOffset > buf.size() || phCount > (buf.size() - phOffset) / phStride) throw std::runtime_error("Output program header table is out of bounds"); + const auto phTableEnd = phOffset + static_cast(phCount) * phStride; + const std::array, 4> sections{{ + {request.DynStrOffset, request.DynStrSize}, + {request.DynSymOffset, request.DynSymSize}, + {request.DynamicSegmentOffset, request.DynamicSegmentSize}, + {request.StubOffset, request.StubSize}, + }}; + for (std::size_t i = 0; i < sections.size(); ++i) { + if (sections[i].second == 0) continue; + for (std::size_t j = i + 1; j < sections.size(); ++j) { + if (sections[j].second == 0) continue; + const auto startA = sections[i].first; + const auto endA = startA + sections[i].second; + const auto startB = sections[j].first; + const auto endB = startB + sections[j].second; + if (startA < endB && startB < endA) + throw std::runtime_error("Allocated output sections overlap"); + } + } const auto mappedAddress = [&](std::uint64_t offset, std::uint64_t sectionSize) { if (offset > buf.size() || sectionSize > buf.size() - offset) throw std::runtime_error("Allocated output section exceeds file"); + if (offset < kElfHeaderSize && sectionSize > 0) + throw std::runtime_error("Allocated output section overlaps the ELF header"); + if (sectionSize > 0 && phCount > 0 && offset < phTableEnd && phOffset < offset + sectionSize) + throw std::runtime_error("Allocated output section overlaps the program header table"); for (std::uint16_t index = 0; index < phCount; ++index) { const auto header = phOffset + index * phStride; if (Io::ReadU32(buf, header) != PT_LOAD) continue; diff --git a/core/relinker/elfpatcher/src/general/SegmentFilter.cpp b/core/relinker/elfpatcher/src/general/SegmentFilter.cpp index f387b4e7c0..114c7e1a3c 100644 --- a/core/relinker/elfpatcher/src/general/SegmentFilter.cpp +++ b/core/relinker/elfpatcher/src/general/SegmentFilter.cpp @@ -13,6 +13,8 @@ bool SegmentFilter::_isSceSpecificSegment(const std::uint32_t type) const { bool SegmentFilter::ShouldSkip(const Domain::ProgramHeader& ph) const { if (ph.Type == PT_OS_PROCPARAM) return false; if (ph.Type == PT_GNU_EH_FRAME) return false; + if (ph.Type == PT_GNU_RELRO && _lazyBinding) return true; + if (ph.Type == PT_GNU_STACK || ph.Type == PT_GNU_RELRO) return false; if (_isSceSpecificSegment(ph.Type)) return true; if (ph.Type == PT_DYNAMIC || ph.Type == PT_PHDR || ph.Type == PT_INTERP) return true; if (ph.Type == PT_NOTE && ph.MappedAddress == 0 && ph.FileSize > 0) return true; diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 60d7c8b690..7453d82aca 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -4,12 +4,18 @@ #include #include #include +#include #include +#include #include #include +#include +#include #include #include #include +#include +#include namespace Elfpatcher::Linux { @@ -92,6 +98,165 @@ std::vector RebasedRelocations(const std::vector& ta return rebased; } +void RequireFileRange(const std::vector& buf, const Domain::ProgramHeader& load) { + if (load.Offset > buf.size() || load.FileSize > buf.size() - load.Offset) + throw Domain::RelinkerException("PT_LOAD at " + Hex(load.MappedAddress) + " has file bytes outside the input"); +} + +void SeparateSharedLoadPages(std::vector& buf, std::vector& headers) { + std::vector loads; + for (std::size_t index = 0; index < headers.size(); ++index) { + if (headers[index].Type == PT_LOAD && headers[index].MemorySize != 0) { + RequireFileRange(buf, headers[index]); + loads.push_back(index); + } + } + std::stable_sort(loads.begin(), loads.end(), [&headers](const std::size_t left, const std::size_t right) { + return headers[left].MappedAddress < headers[right].MappedAddress; + }); + for (std::size_t position = 0; position < loads.size(); ++position) { + auto& load = headers[loads[position]]; + const auto prefix = load.MappedAddress % kDefaultLoadAlignment; + if (prefix == 0 || load.FileSize == 0 || load.Offset % kDefaultLoadAlignment != prefix) + continue; + const auto pageStart = load.MappedAddress - prefix; + std::vector image(static_cast(prefix), 0); + std::vector covered(static_cast(prefix), false); + for (std::size_t below = 0; below < position; ++below) { + const auto& other = headers[loads[below]]; + const auto end = std::min(other.MappedAddress + other.MemorySize, load.MappedAddress); + for (auto address = std::max(other.MappedAddress, pageStart); address < end; ++address) { + const auto relative = address - other.MappedAddress; + covered[static_cast(address - pageStart)] = true; + image[static_cast(address - pageStart)] = relative < other.FileSize ? buf[static_cast(other.Offset + relative)] : 0; + } + } + const auto mapped = load.Offset - prefix; + bool differs = false; + for (std::size_t index = 0; index < image.size() && !differs; ++index) + differs = covered[index] && buf[static_cast(mapped) + index] != image[index]; + if (!differs) + continue; + const auto alignment = std::max(load.Alignment, kDefaultLoadAlignment); + while ((buf.size() + prefix) % alignment != load.MappedAddress % alignment) + buf.push_back(0); + buf.insert(buf.end(), image.begin(), image.end()); + const auto offset = static_cast(buf.size()); + buf.resize(static_cast(offset + load.FileSize)); + std::copy_n(buf.begin() + static_cast(load.Offset), static_cast(load.FileSize), buf.begin() + static_cast(offset)); + load.Offset = offset; + } +} + +struct TracedImport { + std::uint64_t Slot; + std::string Name; + std::string Library; +}; + +std::string ReadDynamicString(const std::vector& strings, const std::uint32_t offset) { + if (offset >= strings.size()) + throw Domain::RelinkerException("Import symbol name lies outside the dynamic string table"); + std::string value; + for (std::size_t index = offset; index < strings.size() && strings[index] != 0; ++index) + value.push_back(static_cast(strings[index])); + return value; +} + +std::vector TracedImports(const Domain::SysVDynamicSection& dynSection) { + std::vector imports; + const auto collect = [&](const std::vector& table) { + RequireWholeEntries(table, kRelaEntrySize, "Relocation table"); + for (std::size_t entry = 0; entry < table.size(); entry += kRelaEntrySize) { + const auto info = Io::ReadU64(table, entry + 8); + const auto type = static_cast(info); + const auto symbol = info >> 32; + const auto target = Io::ReadU64(table, entry); + if ((type != R_X86_64_GLOB_DAT && type != R_X86_64_JUMP_SLOT) || symbol == 0 || Io::ReadU64(table, entry + 16) != 0) + continue; + if (type == R_X86_64_GLOB_DAT && !dynSection.FunctionImports.contains(target)) + continue; + if (symbol >= dynSection.DynSymData.size() / kSymEntrySize) + throw Domain::RelinkerException("Invalid import symbol index", target); + const auto symbolOffset = static_cast(symbol * kSymEntrySize); + if (Io::ReadU16(dynSection.DynSymData, symbolOffset + 6) != SHN_UNDEF || dynSection.DynSymData[symbolOffset + 4] >> 4 == STB_WEAK) + continue; + auto name = ReadDynamicString(dynSection.DynStrData, Io::ReadU32(dynSection.DynSymData, symbolOffset)); + if (name.empty()) + continue; + const auto library = dynSection.ImportModules.find(target); + imports.push_back({target, std::move(name), library == dynSection.ImportModules.end() ? std::string{} : library->second}); + } + }; + collect(dynSection.RelaData); + collect(dynSection.RelaPltData); + return imports; +} + +Domain::SysVDynamicSection ImportsByNid(Domain::SysVDynamicSection dynSection) { + RequireWholeEntries(dynSection.DynamicSegmentData, kDynEntrySize, "Executable dependency table"); + for (std::size_t entry = 0; entry < dynSection.DynamicSegmentData.size(); entry += kDynEntrySize) { + if (static_cast(Io::ReadU64(dynSection.DynamicSegmentData, entry)) != DT_NEEDED) + continue; + const auto offset = Io::ReadU64(dynSection.DynamicSegmentData, entry + 8); + if (offset >= dynSection.DynStrData.size()) + throw Domain::RelinkerException("DT_NEEDED string offset is out of bounds", offset); + const auto name = ReadDynamicString(dynSection.DynStrData, static_cast(offset)); + const auto library = Domain::NeededLibraryName(name); + if (library == name) + continue; + Io::WriteU64(dynSection.DynamicSegmentData, entry + 8, dynSection.DynStrData.size()); + Io::AppendString(dynSection.DynStrData, library); + } + for (auto& library : dynSection.NeededLibraries) + library = Domain::NeededLibraryName(library); + + std::set symbols; + for (const auto* table : {&dynSection.RelaData, &dynSection.RelaPltData}) { + RequireWholeEntries(*table, kRelaEntrySize, "Relocation table"); + for (std::size_t entry = 0; entry < table->size(); entry += kRelaEntrySize) + if (dynSection.NamedImports.contains(Io::ReadU64(*table, entry))) + symbols.insert(Io::ReadU64(*table, entry + 8) >> 32); + } + for (const auto symbol : symbols) { + if (symbol >= dynSection.DynSymData.size() / kSymEntrySize) + throw Domain::RelinkerException("Invalid import symbol index", symbol); + const auto symbolOffset = static_cast(symbol * kSymEntrySize); + const auto name = ReadDynamicString(dynSection.DynStrData, Io::ReadU32(dynSection.DynSymData, symbolOffset)); + if (name.empty() || name.find('#') != std::string::npos) + continue; + if (dynSection.DynStrData.size() > std::numeric_limits::max()) + throw Domain::RelinkerException("Dynamic string table too large"); + Io::WriteU32(dynSection.DynSymData, symbolOffset, static_cast(dynSection.DynStrData.size())); + Io::AppendString(dynSection.DynStrData, Nid::ComputeNid(name, "")); + } + return dynSection; +} + +std::vector TracePrologue(const std::uint64_t vaddr, const std::uint64_t table, const std::uint64_t tableEnd, const std::uint64_t function) { + std::vector code = {0x52, 0x53, 0x41, 0x54, 0x48, 0x83, 0xec, 0x08}; + const auto rip = [&](std::initializer_list opcode, const std::uint64_t target) { + code.insert(code.end(), opcode); + const auto displacement = static_cast(target) - static_cast(vaddr + code.size() + 4); + if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) + throw Domain::RelinkerException("Import trace table exceeds rel32 range"); + Io::AppendU32(code, static_cast(displacement)); + }; + rip({0x48, 0x8d, 0x1d}, table); + rip({0x4c, 0x8d, 0x25}, tableEnd); + const auto loop = code.size(); + code.insert(code.end(), {0x4c, 0x39, 0xe3, 0x73, 0x00}); + const auto exit = code.size(); + code.insert(code.end(), {0x48, 0x8b, 0x03, 0x48, 0x8b, 0x3c, 0x03, 0x48, 0x8b, 0x73, 0x08, 0x48, 0x01, 0xde, 0x48, 0x8b, 0x53, 0x10, 0x48, 0x01, 0xda}); + rip({0xff, 0x15}, function); + code.insert(code.end(), {0x48, 0x8b, 0x3b, 0x48, 0x01, 0xdf, 0x48, 0x81, 0xe7, 0x00, 0xf0, 0xff, 0xff, 0xbe, 0x00, 0x10, 0x00, 0x00, 0xba, 0x07, 0x00, 0x00, 0x00, 0x50, 0xb8, 0x0a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x58}); + code.insert(code.end(), {0x48, 0x8b, 0x0b, 0x48, 0x89, 0x04, 0x0b, 0x48, 0x83, 0xc3, 0x18, 0xeb}); + code.push_back(static_cast(static_cast(static_cast(loop) - static_cast(code.size() + 1)))); + code[exit - 1] = static_cast(code.size() - exit); + code.insert(code.end(), {0x48, 0x83, 0xc4, 0x08, 0x41, 0x5c, 0x5b, 0x5a}); + return code; +} + void RebaseProgramHeaders(std::vector& buf, const std::uint64_t tableOffset, const std::uint16_t entrySize, const std::uint16_t count, const std::uint64_t base) { for (std::uint16_t index = 0; index < count; ++index) { const auto header = static_cast(tableOffset + static_cast(index) * entrySize); @@ -119,13 +284,15 @@ LinuxElfPatcher::LinuxElfPatcher( std::shared_ptr programHeaderLayoutBuilder, std::shared_ptr sectionHeaderTableBuilder, std::shared_ptr byteWriter, - std::optional fixedBase + std::optional fixedBase, + bool traceImports ) : _entryStubBuilder(std::move(entryStubBuilder)) , _programHeaderLayoutBuilder(std::move(programHeaderLayoutBuilder)) , _sectionHeaderTableBuilder(std::move(sectionHeaderTableBuilder)) , _byteWriter(std::move(byteWriter)) , _fixedBase(fixedBase) + , _traceImports(traceImports) { } @@ -171,21 +338,27 @@ void LinuxElfPatcher::_appendTrampoline( } std::vector LinuxElfPatcher::Patch( - const std::vector& sourceElf, + std::vector sourceElf, const std::vector& originalHeaders, - const Domain::SysVDynamicSection& dynSection, + const Domain::SysVDynamicSection& sourceDynSection, const std::uint64_t originalPltGotVaddr, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector& trampolines) { + const auto dynSection = ImportsByNid(sourceDynSection); if (dependencyDiagnostics) throw Domain::RelinkerException("Linux target does not support --windows-diagnostics"); if (_fixedBase && lazyBinding) throw Domain::RelinkerException("A fixed Linux base requires eager binding; --lazy-binding is incompatible"); + if (_traceImports && lazyBinding) + throw Domain::RelinkerException("--trace-imports conflicts with --lazy-binding"); + if (_traceImports && std::find(dynSection.NeededLibraries.begin(), dynSection.NeededLibraries.end(), "libc.prx") == dynSection.NeededLibraries.end()) + throw Domain::RelinkerException("--trace-imports requires libc.prx among the libraries"); if (_fixedBase) RequireOnlyDependencies(dynSection.DynamicSegmentData); + const auto traced = _traceImports ? TracedImports(dynSection) : std::vector{}; const std::uint64_t base = _fixedBase.value_or(0); const auto address = [this, base](const std::uint64_t vaddr) { return _fixedBase ? Rebased(base, vaddr, "Address") : vaddr; @@ -195,7 +368,7 @@ std::vector LinuxElfPatcher::Patch( return relocations ? RebasedRelocations(data, base) : RebasedSymbols(data, base); }; - std::vector buf = sourceElf; + std::vector buf = std::move(sourceElf); const std::uint32_t type = _fixedBase ? ET_EXEC : ET_DYN; buf[kEhdrOsAbiOffset] = 0; @@ -222,17 +395,32 @@ std::vector LinuxElfPatcher::Patch( for (char c : runPath) buf.push_back(static_cast(c)); buf.push_back(0); + const std::uint64_t traceSymbolStrOff = static_cast(buf.size()) - dynStrOff; + if (_traceImports) { + for (char c : std::string_view("TraceImport_nid_no_patch")) + buf.push_back(static_cast(c)); + buf.push_back(0); + } const std::uint64_t dynStrSize = static_cast(buf.size()) - dynStrOff; alignBuf(buf, kDynStrAlignment); const auto dynSymOff = static_cast(buf.size()); - const auto symbols = table(dynSection.DynSymData, false); + auto symbols = table(dynSection.DynSymData, false); + const auto traceSymbol = symbols.size() / kSymEntrySize; + if (_traceImports) { + Io::AppendU32(symbols, static_cast(traceSymbolStrOff)); + symbols.push_back(static_cast((STB_GLOBAL << 4) | STT_FUNC)); + symbols.push_back(0); + Io::AppendU16(symbols, SHN_UNDEF); + Io::AppendU64(symbols, 0); + Io::AppendU64(symbols, 0); + } for (std::uint8_t b : symbols) buf.push_back(b); alignBuf(buf, kDynSymAlignment); const auto hashOff = static_cast(buf.size()); - const auto symbolCount = static_cast(dynSection.DynSymData.size() / kSymEntrySize); + const auto symbolCount = static_cast(symbols.size() / kSymEntrySize); Io::AppendU32(buf, 1); Io::AppendU32(buf, symbolCount); Io::AppendU32(buf, symbolCount > 1 ? 1 : 0); @@ -241,7 +429,13 @@ std::vector LinuxElfPatcher::Patch( alignBuf(buf, kDynSymAlignment); const auto relaOff = static_cast(buf.size()); - const auto relocations = table(dynSection.RelaData, true); + auto relocations = table(dynSection.RelaData, true); + const auto traceRelocation = relocations.size(); + if (_traceImports) { + Io::AppendU64(relocations, 0); + Io::AppendU64(relocations, (static_cast(traceSymbol) << 32) | R_X86_64_GLOB_DAT); + Io::AppendU64(relocations, 0); + } for (std::uint8_t b : relocations) buf.push_back(b); alignBuf(buf, kRelaAlignment); @@ -270,9 +464,9 @@ std::vector LinuxElfPatcher::Patch( _appendDynEntry(dynSegBuf, DT_SYMENT, kSymEntrySize); _appendDynEntry(dynSegBuf, DT_HASH, address(vaddrOfExtraBlockOffset(hashOff))); _appendDynEntry(dynSegBuf, DT_DEBUG, 0); - if (!dynSection.RelaData.empty()) { + if (!relocations.empty()) { _appendDynEntry(dynSegBuf, DT_RELA, address(vaddrOfExtraBlockOffset(relaOff))); - _appendDynEntry(dynSegBuf, DT_RELASZ, dynSection.RelaData.size()); + _appendDynEntry(dynSegBuf, DT_RELASZ, relocations.size()); _appendDynEntry(dynSegBuf, DT_RELAENT, kRelaEntrySize); } if (!dynSection.RelaPltData.empty()) { @@ -290,12 +484,52 @@ std::vector LinuxElfPatcher::Patch( for (std::uint8_t b : dynSegBuf) buf.push_back(b); + std::uint64_t traceFunctionVaddr = 0; + std::uint64_t traceTableVaddr = 0; + std::uint64_t traceTableEndVaddr = 0; + std::vector traceEntries; + if (_traceImports) { + alignBuf(buf, 8); + const auto functionOff = static_cast(buf.size()); + traceFunctionVaddr = vaddrOfExtraBlockOffset(functionOff); + Io::AppendU64(buf, 0); + Io::WriteU64(buf, static_cast(relaOff + traceRelocation), address(traceFunctionVaddr)); + traceTableVaddr = vaddrOfExtraBlockOffset(buf.size()); + for (std::size_t index = 0; index < traced.size(); ++index) { + traceEntries.push_back(buf.size()); + buf.resize(buf.size() + 24); + } + traceTableEndVaddr = vaddrOfExtraBlockOffset(buf.size()); + std::map strings; + const auto addString = [&](const std::string& value) { + if (const auto existing = strings.find(value); existing != strings.end()) return existing->second; + const auto vaddr = vaddrOfExtraBlockOffset(buf.size()); + for (char c : value) + buf.push_back(static_cast(c)); + buf.push_back(0); + strings.emplace(value, vaddr); + return vaddr; + }; + for (std::size_t index = 0; index < traced.size(); ++index) { + const auto entryVaddr = vaddrOfExtraBlockOffset(traceEntries[index]); + const auto relative = [entryVaddr](const std::uint64_t vaddr) { return vaddr - entryVaddr; }; + Io::WriteU64(buf, traceEntries[index], relative(traced[index].Slot)); + Io::WriteU64(buf, traceEntries[index] + 8, relative(addString(traced[index].Name))); + Io::WriteU64(buf, traceEntries[index] + 16, relative(addString(traced[index].Library))); + } + } + const std::uint64_t realEntryVaddr = *reinterpret_cast(buf.data() + kEhdrEntryOffset); const auto stubOff = static_cast(buf.size()); const auto stubVaddr = vaddrOfExtraBlockOffset(stubOff); - const auto stubBytes = _entryStubBuilder->BuildEntryStub(address(stubVaddr), address(realEntryVaddr)); - for (std::uint8_t b : stubBytes) + if (_traceImports) { + for (std::uint8_t b : TracePrologue(stubVaddr, traceTableVaddr, traceTableEndVaddr, traceFunctionVaddr)) + buf.push_back(b); + } + const auto entryStubVaddr = vaddrOfExtraBlockOffset(buf.size()); + for (std::uint8_t b : _entryStubBuilder->BuildEntryStub(address(entryStubVaddr), address(realEntryVaddr))) buf.push_back(b); + const auto stubSize = static_cast(buf.size()) - stubOff; _byteWriter->WriteU64(buf, kEhdrEntryOffset, address(stubVaddr)); for (const auto& site : trampolines) @@ -319,11 +553,14 @@ std::vector LinuxElfPatcher::Patch( const std::uint64_t extraBlockSize = static_cast(buf.size()) - extraBlockOff; + auto headers = originalHeaders; + SeparateSharedLoadPages(buf, headers); + ProgramHeaderLayoutRequest layoutRequest{}; layoutRequest.PhOff = tableOff; layoutRequest.PhEntSize = phEntSize; layoutRequest.PhNum = tableSlots; - layoutRequest.OriginalHeaders = originalHeaders; + layoutRequest.OriginalHeaders = headers; layoutRequest.ExtraBlockOffset = extraBlockOff; layoutRequest.ExtraBlockVaddr = extraBlockVaddr; layoutRequest.ExtraBlockSize = extraBlockSize; @@ -335,17 +572,17 @@ 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; sectionRequest.DynStrSize = dynStrSize; sectionRequest.DynSymOffset = dynSymOff; - sectionRequest.DynSymSize = dynSection.DynSymData.size(); + sectionRequest.DynSymSize = symbols.size(); sectionRequest.DynamicSegmentOffset = dynSegOff; sectionRequest.DynamicSegmentSize = dynSegBuf.size(); sectionRequest.StubOffset = stubOff; - sectionRequest.StubSize = stubBytes.size(); + sectionRequest.StubSize = stubSize; _sectionHeaderTableBuilder->WriteTable(buf, sectionRequest); diff --git a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp index 76598e35ae..76b91f97dc 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp @@ -24,12 +24,18 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma } if (std::none_of(headers.begin(), headers.end(), [&](const auto& header) { return header.Type == PT_GNU_STACK; })) headers.push_back({PT_GNU_STACK, PF_R | PF_W, 0, 0, 0, 0, 0, 16}); - if (end > std::numeric_limits::max() - 0x4000) throw Domain::RelinkerException("Guest virtual address overflow"); + std::vector loads; + for (const auto& header : headers) + if (header.Type == 1) loads.push_back(header); + std::stable_sort(loads.begin(), loads.end(), [](const auto& left, const auto& right) { return left.MappedAddress < right.MappedAddress; }); + for (std::size_t index = 0, next = 0; index < headers.size(); ++index) + if (headers[index].Type == 1) headers[index] = loads[next++]; + if (end >std::numeric_limits::max() - 0x4000) throw Domain::RelinkerException("Guest virtual address overflow"); Io::AlignBuffer(bytes, 0x4000); const auto extraOffset = bytes.size(); const auto extraAddress = Io::AlignUp64(end, 0x4000); const auto address = [&] { return extraAddress + bytes.size() - extraOffset; }; - const bool passesModuleArgs = image.Init != 0; + const bool passesModuleArgs = image.HasInit; std::uint64_t argsSlot = 0; std::uint64_t resultSlot = 0; if (passesModuleArgs) { @@ -55,7 +61,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)); @@ -122,7 +127,7 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma return static_cast(static_cast(distance)); }; const auto lifecycle = [&](std::uint64_t target) { - if (target == 0) return std::uint64_t{}; + if (!image.HasInit) return std::uint64_t{}; const auto start = address(); std::vector stub(kStubSize, 0xc3); stub[0x00] = 0x50; @@ -141,18 +146,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(); @@ -180,7 +185,7 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma if (!image.FiniArray.empty()) { tag(26, image.FiniArray.front()); tag(28, image.FiniArray.size() * 8); } if (init != 0) tag(12, init); if (fini != 0) tag(13, fini); - tag(30, 8); + tag(30, DF_BIND_NOW | (image.Symbolic ? DF_SYMBOLIC : 0)); tag(0, 0); const auto dynamicSize = bytes.size() - dynamicOffset; using namespace Codegen::Amd64OnlySubstitutionTable; diff --git a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp index b8c3f7b06f..7f8032b78a 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp @@ -21,7 +21,7 @@ std::string DynamicName(const Relinker::GuestImage& guest, const std::size_t ind } -std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestImage& guest, const std::vector& dependencies, const std::string& runPath) const { +std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestImage& guest, const std::vector& dependencies, const std::string& runPath, const std::function& message) const { using namespace MacOs; Windows::WindowsLoadImage image(guest.Bytes, guest.Headers, false); MacOsImageInput input; @@ -30,6 +30,7 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma input.Source = &guest.Bytes; input.Headers = &guest.Headers; input.Image = ℑ + input.Message = message; const auto tls = std::find_if(guest.Headers.begin(), guest.Headers.end(), [](const auto& header) { return header.Type == 7; }); std::map targets; @@ -83,7 +84,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); } @@ -106,10 +107,10 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma for (const auto& dependency : dependencies) input.Dylibs.push_back(MachODependency(dependency, "@loader_path")); if (std::find(input.Dylibs.begin(), input.Dylibs.end(), "@rpath/libkernel.prx") == input.Dylibs.end()) input.Dylibs.push_back("@rpath/libkernel.prx"); - input.RunPaths.push_back(MachOLoadPath(runPath, "@loader_path")); + input.RunPaths = MachORunPaths(runPath, "@loader_path"); for (const auto slot : guest.InitArray) input.InitArrayRvas.push_back(image.GetRva(slot, 8)); for (const auto slot : guest.FiniArray) input.FiniArrayRvas.push_back(image.GetRva(slot, 8)); - input.InitRva = guest.Init == 0 ? 0 : image.GetRva(guest.Init); + input.InitRva = guest.HasInit ? image.GetRva(guest.Init) : 0; input.FiniRva = guest.Fini == 0 ? 0 : image.GetRva(guest.Fini); input.Trampolines = &guest.Trampolines; try { diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index c8db078cd8..43a00cd7af 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -19,10 +20,10 @@ namespace Elfpatcher::MacOs { namespace { -using Windows::ImageBase; using Windows::LoadRva; constexpr std::uint64_t PageSize = 0x1000; +constexpr std::uint64_t UserAddressEnd = 0x7fffffe00000; constexpr std::uint32_t MhMagic64 = 0xfeedfacf; constexpr std::uint32_t CpuTypeX8664 = 0x01000007; @@ -32,6 +33,7 @@ constexpr std::uint32_t MhDylib = 6; constexpr std::uint32_t MhNoUndefs = 0x1; constexpr std::uint32_t MhDyldLink = 0x4; constexpr std::uint32_t MhTwoLevel = 0x80; +constexpr std::uint32_t MhWeakDefines = 0x8000; constexpr std::uint32_t LcSegment64 = 0x19; constexpr std::uint32_t LcSymtab = 0x2; @@ -59,6 +61,7 @@ constexpr std::uint8_t RebaseOpcodeDoRebaseImmTimes = 0x50; constexpr std::uint8_t BindTypePointer = 1; constexpr std::uint8_t BindSpecialDylibFlatLookup = 0x0e; +constexpr std::uint8_t BindSymbolFlagsWeakImport = 0x1; constexpr std::uint8_t BindOpcodeDone = 0x00; constexpr std::uint8_t BindOpcodeSetDylibSpecialImm = 0x30; constexpr std::uint8_t BindOpcodeSetSymbolTrailingFlagsImm = 0x40; @@ -152,6 +155,33 @@ void PadTo(std::vector& out, const std::size_t alignment) { while (out.size() % alignment != 0) out.push_back(0); } +void FitDyldSegmentLimit(std::vector& sections, const std::size_t limit) { + while (sections.size() > limit) { + auto best = sections.end(); + std::uint64_t bestGap = 0; + for (auto section = sections.begin(); std::next(section) != sections.end(); ++section) { + const auto next = std::next(section); + if (section->Name == ".amdstub" || next->Name == ".amdstub" || section->Characteristics != next->Characteristics || + section->Rva + section->MemorySize() != next->Rva) + continue; + const auto gap = next->Rva - section->Rva - section->Data.size(); + if (best == sections.end() || gap < bestGap) { + best = section; + bestGap = gap; + } + } + if (best == sections.end()) return; + const auto next = std::next(best); + const auto memorySize = next->Rva + next->MemorySize() - best->Rva; + if (!next->Data.empty()) { + best->Data.resize(next->Rva - best->Rva); + best->Data.insert(best->Data.end(), next->Data.begin(), next->Data.end()); + } + best->VirtualSize = static_cast(memorySize); + sections.erase(next); + } +} + std::pair Locate(const std::vector& segments, const std::uint64_t rva, const std::uint64_t size) { for (std::size_t index = 0; index < segments.size(); ++index) { const auto& segment = segments[index]; @@ -164,7 +194,9 @@ std::pair Locate(const std::vector& segmen Segment& SegmentAt(std::vector& segments, const std::uint64_t rva, const std::uint64_t size) { for (auto& segment : segments) { - if (rva >= segment.Rva && rva - segment.Rva + size <= segment.Data.size()) return segment; + if (rva < segment.Rva || rva - segment.Rva + size > segment.MemorySize) continue; + if (rva - segment.Rva + size > segment.Data.size()) segment.Data.resize(rva - segment.Rva + size); + return segment; } throw Domain::RelinkerException("Mach-O image patch is outside the image data", rva); } @@ -192,6 +224,11 @@ void AppendPathCommand(std::vector& out, const std::uint32_t cmd, out.resize(start + size, 0); } +struct GsTrap { + std::uint32_t Rva; + std::size_t Length; +}; + struct TlsAccess { std::uint32_t Rva; std::size_t Length; @@ -199,34 +236,48 @@ struct TlsAccess { std::uint32_t Immediate; std::uint8_t Register; std::uint32_t Displacement; + std::uint8_t AluOpcode; }; +bool IsAluReadOpcode(std::uint8_t opcode) { + return opcode == 0x03 || opcode == 0x0b || opcode == 0x13 || opcode == 0x1b || opcode == 0x23 || opcode == 0x2b || opcode == 0x33 || opcode == 0x3b; +} + struct TlsScan { const Domain::ProgramHeader* Tls = nullptr; std::vector Accesses; std::set BranchTargets; + std::vector GsTraps; }; -TlsScan ScanTls(const std::vector& source, const std::vector& headers, const Windows::WindowsLoadImage& image) { +TlsScan ScanTls(const std::vector& source, const std::vector& headers, const Windows::WindowsLoadImage& image, const std::function& message) { TlsScan scan; const Codegen::X64InstructionDecoder decoder; - const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers); + const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, {}, nullptr, message); for (const auto& header : headers) { if (header.Type == 7) { if (scan.Tls != nullptr) throw Domain::RelinkerException("Multiple ELF TLS segments"); scan.Tls = &header; } if (header.Type != 1 || (header.Flags & 1) == 0) continue; + std::uint64_t decodedEnd = 0; for (auto instruction = instructions.lower_bound(header.MappedAddress); instruction != instructions.end() && *instruction - header.MappedAddress < header.FileSize; ++instruction) { const auto offset = *instruction - header.MappedAddress; const auto* bytes = source.data() + header.Offset + offset; const auto info = decoder.DecodeInstruction(bytes, header.FileSize - offset); const auto rva = image.GetRva(header.MappedAddress + offset, info.Length); + const bool insidePrevious = *instruction < decodedEnd; + decodedEnd = std::max(decodedEnd, *instruction + info.Length); if (info.HasBranchTarget && !info.HasRipRelativeDisp) { const auto target = static_cast(rva) + static_cast(info.Length) + info.BranchDisp; if (target >= 0 && target <= std::numeric_limits::max()) scan.BranchTargets.insert(static_cast(target)); } - if (info.SegmentPrefix == 0) continue; + if (info.SegmentPrefix == 0 || (insidePrevious && info.SegmentPrefix != 0x64)) continue; + if (info.SegmentPrefix == 0x65) { + if (info.HasMemoryOperand) + scan.GsTraps.push_back({rva, info.Length}); + continue; + } const auto position = info.OpcodeOffset; bool hasOperandSizePrefix = false; bool supportedPrefixes = info.SegmentPrefix == 0x64; @@ -238,14 +289,15 @@ TlsScan ScanTls(const std::vector& source, const std::vector(((bytes[position + 1] >> 3) & 7) | ((info.RexPrefix & 4) << 1)) : std::uint8_t{4}; const bool loadValue = supportedPrefixes && (info.RexPrefix == 0x48 || info.RexPrefix == 0x4c) && loadRegister != 4 && bytes[position] == 0x8b && (bytes[position + 1] & 0xc7) == 0x04 && bytes[position + 2] == 0x25; const bool storeImmediate = supportedPrefixes && !hasOperandSizePrefix && (info.RexPrefix == 0 || info.RexPrefix == 0x40) && info.Length - position == 11 && bytes[position] == 0xc7 && bytes[position + 1] == 0x04 && bytes[position + 2] == 0x25 && Io::ReadU32(source, header.Offset + offset + position + 3) == 0x28; - if (!loadValue && !storeImmediate) { + const bool aluRead = supportedPrefixes && (info.RexPrefix == 0x48 || info.RexPrefix == 0x4c) && loadRegister != 4 && IsAluReadOpcode(bytes[position]) && (bytes[position + 1] & 0xc7) == 0x04 && bytes[position + 2] == 0x25; + if (!loadValue && !storeImmediate && !aluRead) { std::ostringstream message; message << "Unsupported macOS guest TLS instruction (bytes:" << std::hex << std::setfill('0'); for (std::size_t index = 0; index < info.Length; ++index) message << ' ' << std::setw(2) << static_cast(bytes[index]); message << ')'; throw Domain::RelinkerException(message.str(), header.Offset + offset); } - scan.Accesses.push_back({rva, 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)}); + scan.Accesses.push_back({rva, 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}}); } } return scan; @@ -290,7 +342,7 @@ public: private: struct Node { std::vector>> Children; - std::optional Rva; + std::optional Export; std::uint64_t Offset = 0; }; @@ -322,8 +374,8 @@ private: rest = {}; } } - if (node->Rva) throw Domain::RelinkerException("Duplicate Mach-O export: " + entry.Symbol); - node->Rva = entry.Rva; + if (node->Export) throw Domain::RelinkerException("Duplicate Mach-O export: " + entry.Symbol); + node->Export = entry; } static void Collect(Node* node, std::vector& order) { @@ -333,9 +385,9 @@ private: static std::vector TerminalBytes(const Node& node) { std::vector bytes; - if (!node.Rva) return bytes; - AppendUleb(bytes, 0); - AppendUleb(bytes, *node.Rva); + if (!node.Export) return bytes; + AppendUleb(bytes, node.Export->Flags); + AppendUleb(bytes, node.Export->Value); return bytes; } @@ -359,8 +411,9 @@ std::uint64_t GuestTlsBlockSize(const Domain::ProgramHeader& tls) { std::vector WriteMacOsImage(MacOsImageInput& input) { auto& image = *input.Image; const auto& headers = *input.Headers; - const auto tls = ScanTls(*input.Source, headers, image); - const bool hasTls = tls.Tls != nullptr && tls.Tls->MemorySize != 0; + const auto imageBase = image.GetImageBase(); + const auto tls = ScanTls(*input.Source, headers, image, input.Message); + const bool hasTls = tls.Tls != nullptr && (tls.Tls->MemorySize != 0 || !tls.Accesses.empty()); if (!hasTls && !tls.Accesses.empty()) throw Domain::RelinkerException("Guest TLS access without a non-empty PT_TLS"); if (!hasTls && !input.TlsModuleSlots.empty()) throw Domain::RelinkerException("Guest TLS module relocation without a non-empty PT_TLS"); @@ -369,6 +422,23 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { segments.push_back({"__TEXT", 0, LoadRva, VmProtRead | VmProtExecute, {}, {}}); const auto guestSegments = segments.size(); auto sections = image.BuildSections(); + for (const auto& trap : tls.GsTraps) { + if (input.Trampolines != nullptr) { + for (const auto& site : *input.Trampolines) { + const auto siteRva = image.GetRva(site.Address, site.Length); + if (siteRva < trap.Rva + trap.Length && trap.Rva < siteRva + site.Length) + throw Domain::RelinkerException("AMD-only instruction is also a guest GS access", siteRva); + } + } + const auto section = std::find_if(sections.begin(), sections.end(), [&](const auto& candidate) { + return trap.Rva >= candidate.Rva && trap.Rva - candidate.Rva < candidate.Data.size(); + }); + if (section == sections.end()) throw Domain::RelinkerException("Guest GS instruction is outside the image", trap.Rva); + const auto offset = trap.Rva - section->Rva; + if (trap.Length > section->Data.size() - offset) throw Domain::RelinkerException("Guest GS instruction crosses a section boundary", trap.Rva); + std::fill_n(section->Data.begin() + offset, trap.Length, 0x90); + std::copy(std::begin(kStubOpUd2), std::end(kStubOpUd2), section->Data.begin() + offset); + } if (input.Trampolines != nullptr && !input.Trampolines->empty()) { for (const auto& access : tls.Accesses) { for (const auto& site : *input.Trampolines) { @@ -380,6 +450,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { auto stubRva = Windows::AlignRva(image.GetEndRva()); Windows::WindowsTrampolineBuilder().Build(*input.Trampolines, image, sections, stubRva); } + FitDyldSegmentLimit(sections, 16 - guestSegments - 2); std::uint64_t guestEnd = image.GetEndRva(); for (auto& section : sections) { std::uint32_t protection = 0; @@ -387,8 +458,8 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { if (section.Characteristics & Windows::SectionWrite) protection |= VmProtWrite; if (section.Characteristics & Windows::SectionExecute) protection |= VmProtExecute; auto name = section.Name == ".amdstub" ? std::string("__AMDSTUB") : "__ELF" + std::to_string(segments.size() - guestSegments); - guestEnd = std::max(guestEnd, section.Rva + section.Data.size()); - segments.push_back({std::move(name), section.Rva, section.Data.size(), protection, std::move(section.Data), {}}); + guestEnd = std::max(guestEnd, section.Rva + section.MemorySize()); + segments.push_back({std::move(name), section.Rva, section.MemorySize(), protection, std::move(section.Data), {}}); } const auto metaRva = static_cast(AlignUp(guestEnd, PageSize)); @@ -397,7 +468,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { Io::WriteU64(meta, 0, MetaVersion); const auto firstLoad = std::find_if(headers.begin(), headers.end(), [](const auto& header) { return header.Type == 1; }); if (firstLoad == headers.end()) throw Domain::RelinkerException("No PT_LOAD segments found"); - Io::WriteU64(meta, MetaLoadBias, ImageBase + image.GetRva(firstLoad->MappedAddress) - firstLoad->MappedAddress); + Io::WriteU64(meta, MetaLoadBias, imageBase + image.GetRva(firstLoad->MappedAddress) - firstLoad->MappedAddress); input.Rebases.push_back(metaRva + MetaLoadBias); Io::WriteU64(meta, MetaHeaderCount, headers.size()); for (std::size_t index = 0; index < headers.size(); ++index) { @@ -439,10 +510,10 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { if (!hasTls) throw Domain::RelinkerException("Guest TLS export has no TLS block: " + variable.Symbol); if (variable.Offset >= tls.Tls->MemorySize) throw Domain::RelinkerException("Guest TLS export is outside the TLS block: " + variable.Symbol); const auto at = tlsExports + index * 16; - Io::WriteU64(meta, at, ImageBase + descriptor); + Io::WriteU64(meta, at, imageBase + descriptor); Io::WriteU64(meta, at + 8, variable.Offset); input.Rebases.push_back(metaRva + static_cast(at)); - input.Exports.push_back({variable.Symbol, metaRva + static_cast(at)}); + input.Exports.push_back({variable.Symbol, metaRva + static_cast(at), variable.Flags}); } const auto codeRva = static_cast(AlignUp(metaRva + metaEnd, PageSize)); Windows::WindowsStubEmitter code(codeRva); @@ -459,7 +530,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { } Io::WriteU64(meta, MetaTlsDescriptor, 1); if (header.FileSize != 0) { - Io::WriteU64(meta, MetaTlsDescriptor + 8, ImageBase + image.GetRva(header.MappedAddress, header.FileSize)); + Io::WriteU64(meta, MetaTlsDescriptor + 8, imageBase + image.GetRva(header.MappedAddress, header.FileSize)); input.Rebases.push_back(descriptor + 8); } Io::WriteU64(meta, MetaTlsDescriptor + 0x10, header.FileSize); @@ -469,7 +540,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { input.Binds.push_back({"_Aps5GuestTlsRegister_nid_no_patch", metaRva + static_cast(MetaRegisterSlot)}); for (const auto slot : input.TlsModuleSlots) { auto& segment = SegmentAt(segments, slot, 8); - Io::WriteU64(segment.Data, slot - segment.Rva, ImageBase + descriptor); + Io::WriteU64(segment.Data, slot - segment.Rva, imageBase + descriptor); input.Rebases.push_back(slot); } @@ -480,8 +551,13 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { const auto stub = code.GetRva(); const bool preserveAccumulator = access.StoreImmediate || access.Register != 0; code.Emit({0x48, 0x8d, 0x64, 0x24, 0x80}); - code.Emit({0x9c}); - if (preserveAccumulator) code.Emit({0x50}); + if (access.AluOpcode != 0) { + code.Emit({0x50}); + code.Emit({0x9c}); + } else { + code.Emit({0x9c}); + if (preserveAccumulator) code.Emit({0x50}); + } code.Rip({0x48, 0x8b, 0x05}, metaRva + static_cast(MetaTlsKey)); code.Emit({0x65, 0x48, 0x8b, 0x04, 0xc5, 0, 0, 0, 0}); code.Emit({0x48, 0x85, 0xc0}); @@ -496,10 +572,21 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { code.Emit({0x48, 0x8b, 0x80}); code.U32(access.Displacement); } - if (!access.StoreImmediate && access.Register != 0) - code.Emit({static_cast(0x48 | (access.Register >> 3)), 0x89, static_cast(0xc0 | (access.Register & 7))}); - if (preserveAccumulator) code.Emit({0x58}); - code.Emit({0x9d}); + if (access.AluOpcode != 0 && access.Register == 0) { + code.Emit({0x9d}); + code.Emit({0x48, 0x87, 0x04, 0x24}); + code.Emit({0x48, access.AluOpcode, 0x04, 0x24}); + code.Emit({0x48, 0x8d, 0x64, 0x24, 0x08}); + } else if (access.AluOpcode != 0) { + code.Emit({0x9d}); + code.Emit({static_cast(0x48 | ((access.Register >> 3) << 2)), access.AluOpcode, static_cast(0xc0 | ((access.Register & 7) << 3))}); + code.Emit({0x58}); + } else { + if (!access.StoreImmediate && access.Register != 0) + code.Emit({static_cast(0x48 | (access.Register >> 3)), 0x89, static_cast(0xc0 | (access.Register & 7))}); + if (preserveAccumulator) code.Emit({0x58}); + code.Emit({0x9d}); + } code.Emit({0x48, 0x8d, 0xa4, 0x24, 0x80, 0, 0, 0}); code.Rip({0xe9}, static_cast(access.Rva + access.Length)); @@ -568,7 +655,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { code.Rip({0xff, 0x15}, metaRva + static_cast(MetaCxaAtexitSlot)); } code.Emit({0x5d, 0xc3}); - Io::WriteU64(meta, initPointer, ImageBase + initStub); + Io::WriteU64(meta, initPointer, imageBase + initStub); input.Rebases.push_back(metaRva + static_cast(initPointer)); dataSections.push_back({"__mod_init_func", initPointer, 8, SectionModInitFuncPointers, 3}); } @@ -609,7 +696,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { bind.push_back(BindOpcodeSetTypeImm | BindTypePointer); for (const auto& item : input.Binds) { const auto [segment, offset] = Locate(segments, item.Rva, 8); - bind.push_back(BindOpcodeSetSymbolTrailingFlagsImm); + bind.push_back(item.Weak ? BindOpcodeSetSymbolTrailingFlagsImm | BindSymbolFlagsWeakImport : BindOpcodeSetSymbolTrailingFlagsImm); AppendCString(bind, item.Symbol); bind.push_back(BindOpcodeSetAddendSleb); AppendSleb(bind, item.Addend); @@ -759,6 +846,8 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { const auto imageEnd = segments.back().Rva + AlignUp(segments.back().MemorySize, PageSize); if (file.size() > std::numeric_limits::max()) throw Domain::RelinkerException("Mach-O output exceeds 4 GiB"); + if (imageBase + imageEnd + AlignUp(linkeditSize, PageSize) > UserAddressEnd) + throw Domain::RelinkerException("Mach-O image ends outside the macOS user address space"); const auto writeSegment = [&](const std::size_t at, const std::uint64_t vmaddr, const std::uint64_t vmsize, const std::uint64_t fileoff, const std::uint64_t filesize) { Io::WriteU64(commands, at + 24, vmaddr); @@ -767,19 +856,19 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { Io::WriteU64(commands, at + 48, filesize); }; std::size_t index = 0; - if (input.Executable) writeSegment(segmentCommandOffsets[index++], 0, ImageBase, 0, 0); - writeSegment(segmentCommandOffsets[index++], ImageBase, LoadRva, 0, LoadRva); + if (input.Executable) writeSegment(segmentCommandOffsets[index++], 0, imageBase, 0, 0); + writeSegment(segmentCommandOffsets[index++], imageBase, LoadRva, 0, LoadRva); for (; index < segments.size(); ++index) { const auto& segment = segments[index]; - writeSegment(segmentCommandOffsets[index], ImageBase + segment.Rva, AlignUp(segment.MemorySize, PageSize), segment.FileOffset, segment.Data.size()); + writeSegment(segmentCommandOffsets[index], imageBase + segment.Rva, AlignUp(segment.MemorySize, PageSize), segment.FileOffset, segment.Data.size()); for (std::size_t sectionIndex = 0; sectionIndex < segment.Sections.size(); ++sectionIndex) { const auto& section = segment.Sections[sectionIndex]; const auto at = segmentCommandOffsets[index] + 72 + sectionIndex * 80; - Io::WriteU64(commands, at + 32, ImageBase + segment.Rva + section.Offset); + Io::WriteU64(commands, at + 32, imageBase + segment.Rva + section.Offset); Io::WriteU32(commands, at + 48, static_cast(segment.FileOffset + section.Offset)); } } - writeSegment(linkeditCommand, ImageBase + imageEnd, AlignUp(linkeditSize, PageSize), linkeditOffset, linkeditSize); + writeSegment(linkeditCommand, imageBase + imageEnd, AlignUp(linkeditSize, PageSize), linkeditOffset, linkeditSize); Io::WriteU32(commands, dyldInfoCommand + 8, static_cast(rebaseOffset)); Io::WriteU32(commands, dyldInfoCommand + 12, static_cast(rebase.size())); Io::WriteU32(commands, dyldInfoCommand + 16, static_cast(bindOffset)); @@ -803,7 +892,8 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { Io::WriteU32(file, 12, input.Executable ? MhExecute : MhDylib); Io::WriteU32(file, 16, commandCount); Io::WriteU32(file, 20, static_cast(commands.size())); - Io::WriteU32(file, 24, MhNoUndefs | MhDyldLink | MhTwoLevel); + const bool weakDefinitions = std::any_of(input.Exports.begin(), input.Exports.end(), [](const auto& entry) { return (entry.Flags & ExportWeakDefinition) != 0; }); + Io::WriteU32(file, 24, MhNoUndefs | MhDyldLink | MhTwoLevel | (weakDefinitions ? MhWeakDefines : 0)); std::copy(commands.begin(), commands.end(), file.begin() + HeaderSize); return file; } diff --git a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp index 2a56daf6dc..ff62ba6e2d 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -15,6 +16,19 @@ std::string MachOLoadPath(const std::string& path, const std::string& origin) { return path; } +std::vector MachORunPaths(const std::string& paths, const std::string& origin) { + std::vector result; + for (std::size_t begin = 0;;) { + const auto end = paths.find(':', begin); + const auto path = paths.substr(begin, end == std::string::npos ? end : end - begin); + if (path.empty()) throw Domain::RelinkerException("Empty macOS run path component is not supported"); + result.push_back(MachOLoadPath(path, origin)); + if (end == std::string::npos) break; + begin = end + 1; + } + return result; +} + std::vector ReadNeededLibraries(const Domain::SysVDynamicSection& dynamicSection) { const auto& bytes = dynamicSection.DynamicSegmentData; if (bytes.size() % 16 != 0) throw Domain::RelinkerException("Invalid dynamic segment size"); @@ -28,7 +42,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; } @@ -38,12 +51,17 @@ std::string MachODependency(const std::string& name, const std::string& origin) return "@rpath/" + name; } -std::vector MacOsMachOPatcher::Patch(const std::vector& sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, const std::uint64_t originalPltGotVaddr, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector& trampolines) { +MacOsMachOPatcher::MacOsMachOPatcher(const std::optional fixedBase, std::function message) : fixedBase(fixedBase), message(std::move(message)) { + if (fixedBase && (*fixedBase < 0x20000 || *fixedBase > 0x7fffffff0000 || (*fixedBase & 0xffff) != 0)) + throw Domain::RelinkerException("Invalid fixed macOS guest image base"); +} + +std::vector MacOsMachOPatcher::Patch(std::vector sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, const std::uint64_t originalPltGotVaddr, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector& trampolines) { if (dependencyDiagnostics) throw Domain::RelinkerException("macOS target does not support --windows-diagnostics"); static_cast(lazyBinding); - Windows::WindowsLoadImage image(sourceElf, originalHeaders); + Windows::WindowsLoadImage image(sourceElf, originalHeaders, true, fixedBase ? *fixedBase - Windows::LoadRva : Windows::ImageBase); if (originalPltGotVaddr != 0) image.GetRva(originalPltGotVaddr, 8); const auto relocations = Windows::WindowsRelocationBuilder().Apply(image, dynamicSection, originalHeaders); @@ -53,14 +71,41 @@ std::vector MacOsMachOPatcher::Patch(const std::vector> 4) == 0 || visibility == 1 || visibility == 2) continue; + if (((info >> 4) != STB_GLOBAL && (info >> 4) != STB_WEAK) || + ((info & 15) != STT_NOTYPE && (info & 15) != 1 && (info & 15) != STT_FUNC && (info & 15) != STT_TLS)) + throw Domain::RelinkerException("Unsupported macOS executable export attributes"); + const auto name = "_" + Windows::ReadString(dynamicSection.DynStrData, Io::ReadU32(symbols, offset)); + if (name.size() == 1) throw Domain::RelinkerException("Empty macOS executable export"); + const auto value = Io::ReadU64(symbols, offset + 8); + const auto flags = (info >> 4) == STB_WEAK ? ExportWeakDefinition : std::uint64_t{0}; + if ((info & 15) == STT_TLS) { + if (section >= SHN_LORESERVE) throw Domain::RelinkerException("Unsupported macOS executable TLS export section: " + name); + const auto tls = std::find_if(originalHeaders.begin(), originalHeaders.end(), [](const auto& header) { return header.Type == 7; }); + if (tls != originalHeaders.end() && (value > tls->MemorySize || Io::ReadU64(symbols, offset + 16) > tls->MemorySize - value)) + throw Domain::RelinkerException("macOS executable TLS export exceeds PT_TLS: " + name); + input.TlsExports.push_back({name, value, flags}); + } else if (section == SHN_ABS) input.Exports.push_back({name, value, flags | ExportKindAbsolute}); + else { + if (section >= SHN_LORESERVE) throw Domain::RelinkerException("Unsupported macOS executable export section: " + name); + input.Exports.push_back({name, image.GetRva(value, std::max(Io::ReadU64(symbols, offset + 16), 1)), flags}); + } + } input.Rebases = relocations.BaseRelocations; for (const auto& import : relocations.Imports) - input.Binds.push_back({"_" + import.Name, import.TargetRva, static_cast(import.Addend)}); + 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"); - input.RunPaths.push_back(MachOLoadPath(runPath, "@executable_path")); + 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; return WriteMacOsImage(input); diff --git a/core/relinker/elfpatcher/src/windows/WindowsDependencyStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsDependencyStubBuilder.cpp index 5b1dda8c76..3affd7853e 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsDependencyStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsDependencyStubBuilder.cpp @@ -31,6 +31,7 @@ WindowsDependencyStubBuilder::WindowsDependencyStubBuilder(PeSection& data) { {"limit", "FAIL: dependency diagnostics capacity exceeded\n"}, {"io", "FAIL: dependency diagnostics Windows API failed\n"}, {"missingModule", "FAIL: dependency module not found: "}, + {"notImage", "FAIL: dependency module is not a Windows PE image: "}, {"moduleImporter", "\nImporter: "}, {"importer", "FAIL: unresolved dependency import\nImporter: "}, {"provider", "\nProvider: "}, @@ -212,6 +213,7 @@ WindowsDependencyStub WindowsDependencyStubBuilder::Build(WindowsStubEmitter& co a.Begin("register"); a.Mov(Si, Cx); + a.Mov(R12, Dx); a.Global(Bx, "nodes"); a.Global(Di, "count"); a.Mark("registerSearch"); @@ -248,7 +250,34 @@ WindowsDependencyStub WindowsDependencyStubBuilder::Build(WindowsStubEmitter& co a.Value(R8, 0x20); a.Api("LoadLibraryExW"); a.Test(Ax); - a.Jump("io", 0x84); + a.Jump("registerLoaded", 0x85); + a.Api("GetLastError"); + a.Data(Cx, "missingModule"); + a.CompareValue(Ax, 2); + a.Jump("registerFailed", 0x84); + a.CompareValue(Ax, 3); + a.Jump("registerFailed", 0x84); + a.Data(Cx, "notImage"); + a.CompareValue(Ax, 193); + a.Jump("io", 0x85); + a.Mark("registerFailed"); + a.Call("write"); + a.Address(Cx, Bx, 64); + a.Call("write"); + a.Text("moduleImporter"); + a.Global(Cx, "executable"); + a.Test(R12); + a.Jump("registerImporter", 0x84); + a.Address(Cx, R12, 64); + a.Mark("registerImporter"); + a.Call("write"); + a.Text("chain"); + a.Store(Bx, 40, R12); + a.Mov(Cx, Bx); + a.Call("chain"); + a.Text("newline"); + a.Jump("terminate"); + a.Mark("registerLoaded"); a.Mask(Ax, 0xfffffffcu); a.Mark("registerMapped"); a.Store(Bx, 0, Ax); @@ -312,6 +341,7 @@ WindowsDependencyStub WindowsDependencyStubBuilder::Build(WindowsStubEmitter& co a.Jump("resolvePath", 0x84); a.Mark(std::string("resolveNext") + directory); } + a.Mark("resolveMissing"); a.Text("missingModule"); a.Mov(Cx, Bx); a.Call("write"); @@ -332,7 +362,11 @@ WindowsDependencyStub WindowsDependencyStubBuilder::Build(WindowsStubEmitter& co a.Value(R8, 0x800); a.Api("LoadLibraryExA"); a.Test(Ax); - a.Jump("io", 0x84); + a.Jump("resolveLoaded", 0x85); + a.Api("GetLastError"); + a.CompareValue(Ax, 126); + a.Jump("resolveMissing", 0x84); + a.Jump("io"); a.Mark("resolveLoaded"); a.Mov(Cx, Ax); a.Data(Dx, "wide"); @@ -346,6 +380,7 @@ WindowsDependencyStub WindowsDependencyStubBuilder::Build(WindowsStubEmitter& co a.Call("narrow"); a.Mark("resolvePath"); a.Data(Cx, "scratch"); + a.Mov(Dx, Si); a.Call("register"); a.End(); @@ -694,6 +729,7 @@ WindowsDependencyStub WindowsDependencyStubBuilder::Build(WindowsStubEmitter& co a.Jump("io", 0x84); a.Save("nodes", Ax); a.Mov(Cx, Bx); + a.Value(Dx, 0); a.Call("register"); a.Save("root", Ax); a.Mov(Cx, Ax); diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index 14eb23c206..dbcca232a2 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -2,7 +2,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -97,6 +99,10 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con return rva; }; + const auto parentVariable = addWideString(u"ANYPS5_LOADEXEC_PARENT"); + const auto allocatorVariable = addWideString(u"APS5_MIMALLOC"); + const auto parentText = reserve(34); + const auto skipFirstArgument = reserve(4); const auto programPath = reserve(PathCapacity * 2); const auto modulePath = reserve(PathCapacity * 2); const auto argumentBlock = reserve(8); @@ -116,6 +122,17 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con const auto unwindRva = CheckedRva(dataRva + data.size()); data.insert(data.end(), {1, 10, 6, 0, 10, 0xb2, 6, 0xc0, 4, 0x70, 3, 0x60, 2, 0x50, 1, 0x30}); + std::vector allocatorPaths; + std::vector allocatorUnits; + for (const std::string library : {"mimalloc.dll"}) { + const auto name = path + library; + const auto wideName = toUtf16(name); + if (wideName.size() + 1 >= PathCapacity) + throw Domain::RelinkerException("Windows library path exceeds the startup buffer: " + name); + allocatorPaths.push_back(addWideString(wideName)); + allocatorUnits.push_back(wideName.size()); + } + std::vector libraryPaths; std::vector libraryUnits; for (std::size_t index = 0; index < libraries.size(); ++index) { @@ -129,8 +146,12 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con } std::vector symbolNames; - for (const auto& import : imports) + std::vector> nidNames; + for (const auto& import : imports) { symbolNames.push_back(addString(import.Name)); + const bool named = import.Library.empty() && !import.Name.empty() && import.Name.find('#') == std::string::npos; + nidNames.push_back(named ? std::optional(addString(Nid::ComputeNid(import.Name, ""))) : std::nullopt); + } std::vector searchTables; std::vector searchCounts; @@ -138,22 +159,28 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con std::vector imported(libraries.size()); const bool unscopedImports = std::any_of(imports.begin(), imports.end(), [](const auto& import) { return import.Library.empty(); }); for (const auto& import : imports) { + const auto library = Domain::NeededLibraryName(import.Library); std::vector order; for (std::size_t index = 0; index < libraries.size(); ++index) { auto name = std::filesystem::path(libraries[index]).filename().string(); if (name.ends_with(".guest.prx")) name.resize(name.size() - 10); const bool guestName = index < guestModules.size() && std::find(guestModules[index].Names.begin(), guestModules[index].Names.end(), import.Library) != guestModules[index].Names.end(); - if (import.Library.empty() || name == import.Library || guestName) order.push_back(CheckedRva(index)); + if (import.Library.empty() || name == library || guestName) order.push_back(CheckedRva(index)); } if (order.empty()) throw Domain::RelinkerException("Import module is not a dependency: " + import.Library); const bool hostModule = !import.Library.empty() && std::any_of(order.begin(), order.end(), [&](auto index) { return index >= guestModules.size(); }); if (hostModule) { - for (std::size_t index = guestModules.size(); index < libraries.size(); ++index) { - if (libraries[index] == "libc.prx" && import.Library != "libc.prx") order.push_back(CheckedRva(index)); + if (library == "libkernel_sys.prx" || library == "libkernel_web.prx") { + for (std::size_t index = guestModules.size(); index < libraries.size(); ++index) { + if (libraries[index] == "libkernel.prx") order.push_back(CheckedRva(index)); + } } - if (import.Library == "libSceLibcInternal.prx") { + for (std::size_t index = guestModules.size(); index < libraries.size(); ++index) { + if (libraries[index] == "libc.prx" && library != "libc.prx") order.push_back(CheckedRva(index)); + } + if (library == "libSceLibcInternal.prx") { for (std::size_t index = 0; index < guestModules.size(); ++index) { const auto& names = guestModules[index].Names; if (std::find(names.begin(), names.end(), "libc.prx") != names.end() && !guestModules[index].ZeroReturnExports.contains(import.Name)) @@ -207,6 +234,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con } const auto lastError = reserve(4); + const auto unresolvedCount = reserve(4); const auto errorDigits = reserve(11); const auto errorMessage = reserve(ErrorMessageCapacity); const auto loading = addString("Loading PRX: "); @@ -233,6 +261,8 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con (import.Library.empty() ? std::string{} : " from " + import.Library) + "\n"); const auto argumentError = errors.size(); errors.push_back("FAIL: cannot prepare command-line arguments\n"); + const auto parentError = errors.size(); + errors.push_back("FAIL: cannot wait for the previous LoadExec process\n"); const auto directoryError = errors.size(); errors.push_back("FAIL: cannot make the executable directory the current directory\n"); const auto traceError = errors.size(); @@ -395,6 +425,49 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con }; code.Emit({0x53, 0x55, 0x56, 0x57, 0x41, 0x54, 0x48, 0x83, 0xec, 0x60}); + code.Rip({0x48, 0x8d, 0x0d}, parentVariable); + code.Rip({0x48, 0x8d, 0x15}, parentText); + code.Emit({0x41, 0xb8, 17, 0, 0, 0}); + call("GetEnvironmentVariableW"); + code.Emit({0x85, 0xc0}); + const auto noParent = code.Branch({0x0f, 0x84}); + code.Emit({0x83, 0xf8, 16}); + const auto shortValue = code.Branch({0x0f, 0x86}); + fail(parentError, 1); + code.PatchBranch(shortValue, code.GetRva()); + code.Rip({0xc7, 0x05}, skipFirstArgument - 4); + code.U32(1); + code.Rip({0x48, 0x8d, 0x35}, parentText); + code.Emit({0x31, 0xff}); + const auto digit = code.GetRva(); + code.Emit({0x0f, 0xb7, 0x06, 0x85, 0xc0}); + const auto parsed = code.Branch({0x0f, 0x84}); + code.Emit({0x83, 0xe8, 0x30, 0x83, 0xf8, 9}); + const auto decimal = code.Branch({0x0f, 0x86}); + code.Emit({0x83, 0xe8, 0x31, 0x83, 0xf8, 5}); + const auto hex = code.Branch({0x0f, 0x86}); + fail(parentError, 1); + code.PatchBranch(hex, code.GetRva()); + code.Emit({0x83, 0xc0, 10}); + code.PatchBranch(decimal, code.GetRva()); + code.Emit({0x48, 0xc1, 0xe7, 4, 0x48, 0x09, 0xc7, 0x48, 0x83, 0xc6, 2}); + code.Rip({0xe9}, digit); + code.PatchBranch(parsed, code.GetRva()); + code.Emit({0x48, 0x89, 0xf9, 0xba, 0xff, 0xff, 0xff, 0xff}); + call("WaitForSingleObject"); + code.Emit({0x85, 0xc0}); + const auto waited = code.Branch({0x0f, 0x84}); + fail(parentError, 1); + code.PatchBranch(waited, code.GetRva()); + code.Emit({0x48, 0x89, 0xf9}); + call("CloseHandle"); + requireNonzero(parentError, 1); + code.Rip({0x48, 0x8d, 0x0d}, parentVariable); + code.Emit({0x31, 0xd2}); + call("SetEnvironmentVariableW"); + requireNonzero(parentError, 1); + code.PatchBranch(noParent, code.GetRva()); + code.Emit({0x31, 0xc9}); code.Rip({0x48, 0x8d, 0x15}, programPath); code.Emit({0x41, 0xb8}); @@ -430,6 +503,35 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con writeUtf8(programText); } + code.Rip({0x48, 0x8d, 0x0d}, allocatorVariable); + code.Emit({0x31, 0xd2, 0x45, 0x31, 0xc0}); + call("GetEnvironmentVariableW"); + code.Emit({0x85, 0xc0}); + const auto noAllocator = code.Branch({0x0f, 0x84}); + for (std::size_t index = 0; index < allocatorPaths.size(); ++index) { + std::optional tooLong; + if (absolutePath) { + code.Rip({0x48, 0x8d, 0x0d}, allocatorPaths[index]); + } else { + const auto units = CheckedRva(allocatorUnits[index] + 1); + code.Emit({0x4c, 0x89, 0xe0, 0x48, 0x29, 0xd8, 0x48, 0x05}); + code.U32(units * 2); + code.Emit({0x48, 0x3d}); + code.U32(PathCapacity * 2); + tooLong = code.Branch({0x0f, 0x87}); + code.Emit({0x4c, 0x89, 0xe7}); + code.Rip({0x48, 0x8d, 0x35}, allocatorPaths[index]); + code.Emit({0xb9}); + code.U32(units); + code.Emit({0x66, 0xf3, 0xa5}); + code.Rip({0x48, 0x8d, 0x0d}, modulePath); + } + code.Emit({0x31, 0xd2, 0x41, 0xb8, 0, 0x11, 0, 0}); + call("LoadLibraryExW"); + if (tooLong) code.PatchBranch(*tooLong, code.GetRva()); + } + code.PatchBranch(noAllocator, code.GetRva()); + for (std::size_t index = 0; index < libraries.size(); ++index) { if (index >= guestModules.size() && !imported[index] && !unscopedImports) continue; if (absolutePath && index >= guestModules.size()) { @@ -528,6 +630,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con }; std::vector unresolvedBranches; + bool anyUnresolvable = false; std::vector lazyUnresolvedImports; std::vector tlsResolverAddresses; for (std::size_t index = 0; index < imports.size(); ++index) { @@ -546,9 +649,23 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.Emit({0x8b, 0x0f, 0x48, 0x8b, 0x0c, 0xcb, 0x48, 0x89, 0xf2}); call("GetProcAddress"); code.Emit({0x48, 0x85, 0xc0}); - const auto resolved = code.Branch({0x0f, 0x85}); + std::vector resolved = {code.Branch({0x0f, 0x85})}; code.Emit({0x48, 0x83, 0xc7, 4, 0xff, 0xcd}); code.Rip({0x0f, 0x85}, search); + if (nidNames[index]) { + code.Rip({0x48, 0x8d, 0x35}, *nidNames[index]); + code.Rip({0x48, 0x8d, 0x3d}, searchTables[index]); + code.Emit({0xbd}); + code.U32(searchCounts[index]); + const auto nidSearch = code.GetRva(); + code.Emit({0x8b, 0x0f, 0x48, 0x8b, 0x0c, 0xcb, 0x48, 0x89, 0xf2}); + call("GetProcAddress"); + code.Emit({0x48, 0x85, 0xc0}); + resolved.push_back(code.Branch({0x0f, 0x85})); + code.Emit({0x48, 0x83, 0xc7, 4, 0xff, 0xcd}); + code.Rip({0x0f, 0x85}, nidSearch); + } + const auto patchResolved = [&] { for (const auto branch : resolved) code.PatchBranch(branch, code.GetRva()); }; if (imports[index].Weak) { code.Emit({0x31, 0xc0}); const auto writeWeak = code.GetRva(); @@ -559,7 +676,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con } guestStartup.WriteImport(code, imports[index], handles); const auto weakDone = code.Branch({0xe9}); - code.PatchBranch(resolved, code.GetRva()); + patchResolved(); code.Rip({0xe9}, writeWeak); code.PatchBranch(weakDone, code.GetRva()); continue; @@ -567,7 +684,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con if (lazyBinding) { lazyUnresolvedImports.push_back(index); const auto skipGotWrite = code.Branch({0xe9}); - code.PatchBranch(resolved, code.GetRva()); + patchResolved(); if (imports[index].Addend != 0) { code.Emit({0x48, 0xba}); code.U64(imports[index].Addend); @@ -579,8 +696,9 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con } captureLastError(); writeString(errorRvas.at(3 + index), true); - unresolvedBranches.push_back(code.Branch({0xe9})); - code.PatchBranch(resolved, code.GetRva()); + code.Rip({0xff, 0x05}, unresolvedCount); + const auto nextImport = code.Branch({0xe9}); + patchResolved(); if (imports[index].RelocationType == 16) code.Emit({0x48, 0x8b, 0x00}); if (imports[index].RelocationType == 17) code.Emit({0x48, 0x8b, 0x40, 8}); if (imports[index].Addend != 0) { @@ -590,8 +708,15 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con } trace(imports[index], symbolNames[index]); guestStartup.WriteImport(code, imports[index], handles); + code.PatchBranch(nextImport, code.GetRva()); + anyUnresolvable = true; } + if (anyUnresolvable) { + code.Rip({0x8b, 0x05}, unresolvedCount); + code.Emit({0x85, 0xc0}); + unresolvedBranches.push_back(code.Branch({0x0f, 0x85})); + } guestStartup.Initialize(code, guestModules, handles); writeString(enteringElf); code.Rip({0x48, 0x8d, 0x0d}, shellLibrary); @@ -611,6 +736,13 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con requireNonzero(argumentError, 0xc000000du); code.Rip({0x48, 0x89, 0x05}, wideArguments); code.Emit({0x48, 0x89, 0xc3}); + code.Rip({0x83, 0x3d}, skipFirstArgument - 1); + code.Emit({0}); + const auto retainProgramName = code.Branch({0x0f, 0x84}); + code.Emit({0x48, 0x83, 0xc3, 8}); + code.Rip({0x83, 0x2d}, argumentCount - 1); + code.Emit({1}); + code.PatchBranch(retainProgramName, code.GetRva()); code.Rip({0x8b, 0x2d}, argumentCount); code.Emit({0x8d, 0x45, 0xff, 0x3d}); code.U32(ArgumentCapacity - 1); diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index ded45f6d76..0b284a4bcd 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -13,8 +13,9 @@ namespace Elfpatcher { -std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestImage& guest, Domain::GuestRuntime& runtime) const { +std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestImage& guest, Domain::GuestRuntime& runtime, const std::function& message) const { using namespace Windows; + if (guest.Symbolic) throw Domain::RelinkerException("DF_SYMBOLIC guest modules are not supported by the Windows writer"); WindowsLoadImage image(guest.Bytes, guest.Headers, false); std::vector relocations; std::vector> tlsModules; @@ -71,7 +72,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI } else { if (symbol.Name.empty()) throw Domain::RelinkerException("Empty guest import", target); image.WritePointer(target, 0); - runtime.Imports.push_back({symbol.Name, rva, addend, 1, symbol.Library, type == 7 || (type == 6 && (symbol.Info & 15) == 2)}); + runtime.Imports.push_back({symbol.Name, rva, addend, 1, symbol.Library, type == 7 || (type == 6 && (symbol.Info & 15) == 2), (symbol.Info >> 4) == 2}); } } else throw Domain::RelinkerException("Unsupported Windows guest relocation " + std::to_string(type), target); } @@ -83,7 +84,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI std::array directories{}; std::uint32_t tlsIndex = 0; try { - directories[9] = WindowsTlsBuilder().Build(guest.Bytes, guest.Headers, image, sections, relocations, nextRva, &tlsIndex); + directories[9] = WindowsTlsBuilder().Build(guest.Bytes, guest.Headers, image, sections, relocations, nextRva, &tlsIndex, {}, message); } catch (Domain::RelinkerException& error) { if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(guest.SourcePath); throw; @@ -93,7 +94,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI if (tlsIndex == 0) throw Domain::RelinkerException("Guest TLS relocation has no TLS block", target); bool written = false; for (auto& section : sections) { - if (rva < section.Rva || rva - section.Rva > section.Data.size() || section.Data.size() - (rva - section.Rva) < 8) continue; + if (!section.Writable(rva, 8)) continue; Io::WriteU64(section.Data, rva - section.Rva, ImageBase + tlsIndex); written = true; break; @@ -149,7 +150,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI } if (runtime.DeferInitialization) { PeSection lifecycle{".ginit", nextRva, SectionRead | 0x40u, {}}; - Io::AppendU32(lifecycle.Data, guest.Init == 0 ? 0 : image.GetRva(guest.Init)); + Io::AppendU32(lifecycle.Data, guest.HasInit ? image.GetRva(guest.Init) : 0); Io::AppendU32(lifecycle.Data, CheckedRva(guest.InitArray.size())); for (const auto slot : guest.InitArray) Io::AppendU32(lifecycle.Data, image.GetRva(slot, 8)); if (!exports.emplace(Relinker::GuestInitializeExport, nextRva).second) throw Domain::RelinkerException("Duplicate guest export: " + std::string(Relinker::GuestInitializeExport)); @@ -202,7 +203,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI sections.push_back({".dllmain", entry, SectionRead | SectionExecute | 0x20u, {0xb8, 1, 0, 0, 0, 0xc3}}); for (const auto slot : guest.InitArray) runtime.InitArrayRvas.push_back(image.GetRva(slot, 8)); for (const auto slot : guest.FiniArray) runtime.FiniArrayRvas.push_back(image.GetRva(slot, 8)); - runtime.InitRva = guest.Init == 0 ? 0 : image.GetRva(guest.Init); + runtime.InitRva = guest.HasInit ? image.GetRva(guest.Init) : 0; runtime.UnmatchedImportModules.assign(guest.Dynamic.UnmatchedImportModules.begin(), guest.Dynamic.UnmatchedImportModules.end()); runtime.FiniRva = guest.Fini == 0 ? 0 : image.GetRva(guest.Fini); auto result = WindowsPeWriter().Write(sections, entry, directories); diff --git a/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp index aabd25ac42..4a547373bd 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp @@ -16,7 +16,10 @@ std::uint32_t readBigEndian(const std::vector& bytes, const std::s } void validatePng(const std::vector& bytes, const std::filesystem::path& path) { - const auto fail = [&path](const std::string& reason) { throw Domain::RelinkerException("Invalid Windows icon PNG '" + path.string() + "': " + reason); }; + const auto fail = [&path](const std::string& reason) { + const auto utf8 = path.u8string(); + throw Domain::RelinkerException("Invalid Windows icon PNG '" + std::string(utf8.begin(), utf8.end()) + "': " + reason); + }; constexpr std::array signature{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}; if (bytes.size() < 33 || !std::equal(signature.begin(), signature.end(), bytes.begin())) fail("missing PNG signature or IHDR"); if (readBigEndian(bytes, 8) != 13 || readBigEndian(bytes, 12) != 0x49484452) fail("invalid IHDR size or type"); @@ -38,6 +41,9 @@ void validatePng(const std::vector& bytes, const std::filesystem:: const auto length = readBigEndian(bytes, offset); if (length > bytes.size() - offset - 12) fail("truncated chunk data"); const auto type = readBigEndian(bytes, offset + 4); + for (std::size_t index = offset + 4; index < offset + 8; ++index) + if (!((bytes[index] >= 'A' && bytes[index] <= 'Z') || (bytes[index] >= 'a' && bytes[index] <= 'z'))) + fail("invalid chunk type"); std::uint32_t crc = 0xffffffffu; for (std::size_t index = offset + 4; index < offset + 8 + length; ++index) { crc ^= bytes[index]; @@ -71,6 +77,12 @@ void validatePng(const std::vector& bytes, const std::filesystem:: } PeDirectory WindowsIconResourceBuilder::Build(const std::vector& png, const std::filesystem::path& iconPath, std::vector& sections, const std::uint32_t nextRva) const { + if (nextRva < LoadRva || nextRva % SectionAlignment != 0) + throw Domain::RelinkerException("Invalid Windows resource section RVA", nextRva); + if (std::any_of(sections.begin(), sections.end(), [](const auto& section) { return section.Name == ".rsrc"; })) + throw Domain::RelinkerException("Duplicate Windows resource section"); + if (!sections.empty() && nextRva < AlignRva(static_cast(sections.back().Rva) + sections.back().Data.size())) + throw Domain::RelinkerException("Windows resource section overlaps preceding section", nextRva); if (png.size() > 0x7fffffffu - 184) throw Domain::RelinkerException("Invalid Windows icon size"); validatePng(png, iconPath); std::vector data(184 + png.size()); diff --git a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp index 71ce4f2bad..f2d45060be 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp @@ -1,11 +1,12 @@ #include +#include #include #include namespace Elfpatcher::Windows { WindowsImports WindowsImportBuilder::Build(const std::uint32_t sectionRva) const { - const std::vector names = {"CreateFileW", "ExitProcess", "FormatMessageA", "FreeLibrary", "GetCommandLineW", "GetFileAttributesW", "GetLastError", "GetModuleFileNameW", "GetModuleHandleA", "GetModuleHandleW", "GetProcAddress", "GetStdHandle", "GetSystemDirectoryW", "LoadLibraryExA", "LoadLibraryExW", "LocalFree", "MultiByteToWideChar", "RaiseException", "SetCurrentDirectoryW", "SetStdHandle", "VirtualAlloc", "WideCharToMultiByte", "WriteFile", "lstrcatA", "lstrcmpA", "lstrcmpiA", "lstrcpyA", "lstrlenA"}; + const std::vector names = {"CloseHandle", "CreateFileW", "ExitProcess", "FormatMessageA", "FreeLibrary", "GetCommandLineW", "GetEnvironmentVariableW", "GetFileAttributesW", "GetLastError", "GetModuleFileNameW", "GetModuleHandleA", "GetModuleHandleW", "GetProcAddress", "GetStdHandle", "GetSystemDirectoryW", "LoadLibraryExA", "LoadLibraryExW", "LocalFree", "MultiByteToWideChar", "RaiseException", "SetCurrentDirectoryW", "SetEnvironmentVariableW", "SetStdHandle", "VirtualAlloc", "WaitForSingleObject", "WideCharToMultiByte", "WriteFile", "lstrcatA", "lstrcmpA", "lstrcmpiA", "lstrcpyA", "lstrlenA"}; WindowsImports result{{".idata", sectionRva, SectionRead | SectionWrite | 0x40u, std::vector(40)}, {sectionRva, 40}, {}, {}}; auto& bytes = result.Section.Data; const auto lookupOffset = bytes.size(); @@ -43,7 +44,7 @@ std::vector WindowsImportBuilder::ReadLibraries(const Domain::SysVD const auto nameOffset = Io::ReadU64(bytes, offset + 8); if (nameOffset >= dynamicSection.DynStrData.size()) throw Domain::RelinkerException("DT_NEEDED string offset is out of bounds", nameOffset); - auto name = ReadString(dynamicSection.DynStrData, static_cast(nameOffset)); + auto name = Domain::NeededLibraryName(ReadString(dynamicSection.DynStrData, static_cast(nameOffset))); if (name.empty() || name.find_first_of("/\\:") != std::string::npos) throw Domain::RelinkerException("Invalid DT_NEEDED library: " + name); if (unique.insert(name).second) @@ -52,8 +53,15 @@ 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")) - result.push_back("libc.prx"); + const auto addIfMissing = [&](const char* library) { + if (unique.insert(library).second) result.emplace_back(library); + }; + if (unique.contains("libSceLibcInternal.prx") || unique.contains("libkernel.prx")) + addIfMissing("libc.prx"); + if (unique.contains("libkernel_sys.prx") || unique.contains("libkernel_web.prx")) { + addIfMissing("libkernel.prx"); + addIfMissing("libc.prx"); + } return result; } diff --git a/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp b/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp index ab78c51b1a..9f100e655c 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp @@ -34,12 +34,15 @@ WindowsLoadImage::WindowsLoadImage(const std::vector& source, cons throw Domain::RelinkerException("Overlapping PT_LOAD memory ranges", segment.MappedAddress); end = segment.MappedAddress + segment.MemorySize; } - data.resize(AlignRva(end - firstAddress)); - CheckedRva(LoadRva + static_cast(data.size())); - pageFlags.resize(data.size() / SectionAlignment); + size = AlignRva(end - firstAddress); + CheckedRva(LoadRva + size); + pageFlags.resize(static_cast(size / SectionAlignment)); for (const auto& segment : segments) { const auto offset = static_cast(segment.MappedAddress - firstAddress); - std::copy_n(source.begin() + static_cast(segment.Offset), static_cast(segment.FileSize), data.begin() + static_cast(offset)); + if (segment.FileSize != 0) { + auto& block = backBlock(offset, segment.FileSize); + std::copy_n(source.begin() + static_cast(segment.Offset), static_cast(segment.FileSize), block.Bytes.begin() + static_cast(offset - block.Offset)); + } std::uint32_t flags = 0; if ((segment.Flags & 1) != 0) flags |= SectionExecute | SectionRead; @@ -68,7 +71,7 @@ std::uint32_t WindowsLoadImage::GetRva(const std::uint64_t address, const std::u } std::uint32_t WindowsLoadImage::GetEndRva() const { - return CheckedRva(LoadRva + data.size()); + return CheckedRva(LoadRva + size); } std::uint32_t WindowsLoadImage::GetEntryRva() const { @@ -76,7 +79,7 @@ std::uint32_t WindowsLoadImage::GetEntryRva() const { } std::uint64_t WindowsLoadImage::GetRelocatedAddress(const std::uint64_t address) const { - if (address < firstAddress || address - firstAddress > data.size()) + if (address < firstAddress || address - firstAddress > size) throw Domain::RelinkerException("Relative relocation addend is outside the ELF image", address); return imageBase + LoadRva + address - firstAddress; } @@ -85,8 +88,31 @@ std::uint64_t WindowsLoadImage::GetImageBase() const { return imageBase; } +WindowsLoadImage::Block& WindowsLoadImage::backBlock(const std::uint64_t offset, const std::uint64_t length) { + const auto start = offset / SectionAlignment * SectionAlignment; + const auto end = AlignRva(offset + length); + auto index = static_cast(std::upper_bound(blocks.begin(), blocks.end(), offset, [](const std::uint64_t value, const Block& block) { return value < block.Offset; }) - blocks.begin()); + if (index == 0 || blocks[index - 1].Offset + blocks[index - 1].Bytes.size() < start) + blocks.insert(blocks.begin() + static_cast(index), Block{start, {}}); + else + --index; + auto& block = blocks[index]; + if (block.Offset + block.Bytes.size() < end) + block.Bytes.resize(end - block.Offset); + while (index + 1 < blocks.size() && blocks[index + 1].Offset <= block.Offset + block.Bytes.size()) { + const auto& next = blocks[index + 1]; + const auto nextOffset = static_cast(next.Offset - block.Offset); + block.Bytes.resize(std::max(block.Bytes.size(), nextOffset + next.Bytes.size())); + std::copy(next.Bytes.begin(), next.Bytes.end(), block.Bytes.begin() + static_cast(nextOffset)); + blocks.erase(blocks.begin() + static_cast(index + 1)); + } + return block; +} + void WindowsLoadImage::WritePointer(const std::uint64_t address, const std::uint64_t value) { - Io::WriteU64(data, GetRva(address, 8) - LoadRva, value); + const auto offset = GetRva(address, 8) - LoadRva; + auto& block = backBlock(offset, 8); + Io::WriteU64(block.Bytes, offset - block.Offset, value); } void WindowsLoadImage::RequireWritable(const std::uint64_t address, const std::uint64_t size) const { @@ -99,15 +125,26 @@ void WindowsLoadImage::RequireWritable(const std::uint64_t address, const std::u std::vector WindowsLoadImage::BuildSections() const { std::vector sections; + auto block = blocks.begin(); for (std::size_t first = 0; first < pageFlags.size();) { + while (block != blocks.end() && block->Offset + block->Bytes.size() <= first * SectionAlignment) + ++block; + const auto backed = block != blocks.end() && block->Offset <= first * SectionAlignment; + const auto dataEnd = backed ? static_cast((block->Offset + block->Bytes.size()) / SectionAlignment) : first; + const auto following = backed ? std::next(block) : block; + const auto stop = following != blocks.end() ? following->Offset / SectionAlignment : pageFlags.size(); auto last = first + 1; - while (last < pageFlags.size() && pageFlags[last] == pageFlags[first]) + while (last < stop && pageFlags[last] == pageFlags[first]) ++last; const auto characteristics = pageFlags[first] | ((pageFlags[first] & SectionExecute) != 0 ? 0x20u : 0x40u); - sections.push_back({".elf" + std::to_string(sections.size()), CheckedRva(LoadRva + first * SectionAlignment), characteristics, {data.begin() + static_cast(first * SectionAlignment), data.begin() + static_cast(last * SectionAlignment)}}); + PeSection section{".elf" + std::to_string(sections.size()), CheckedRva(LoadRva + first * SectionAlignment), characteristics, {}, CheckedRva((last - first) * SectionAlignment)}; + if (backed) { + const auto begin = block->Bytes.begin() + static_cast(first * SectionAlignment - block->Offset); + section.Data.assign(begin, begin + static_cast((std::min(last, dataEnd) - first) * SectionAlignment)); + } + sections.push_back(std::move(section)); first = last; } return sections; } - } diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index c506a7a1a8..063af7924b 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -18,7 +18,7 @@ namespace { void writeGotStub(std::vector& sections, const std::uint32_t targetRva, const std::uint32_t stubRva, const std::uint64_t imageBase) { for (auto& section : sections) { - if (section.Data.size() < 8 || targetRva < section.Rva || targetRva - section.Rva > section.Data.size() - 8) + if (!section.Writable(targetRva, 8)) continue; Io::WriteU64(section.Data, targetRva - section.Rva, imageBase + stubRva); return; @@ -28,14 +28,14 @@ void writeGotStub(std::vector& sections, const std::uint32_t targetRv } -WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional> icon, std::filesystem::path iconName, std::function progress, const std::optional fixedBase, const bool traceImports) : progress(std::move(progress)), windowsGui(windowsGui), icon(std::move(icon)), iconName(std::move(iconName)), fixedBase(fixedBase), traceImports(traceImports) { +WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional> icon, std::filesystem::path iconName, std::function progress, const std::optional fixedBase, const bool traceImports, std::function message) : progress(std::move(progress)), windowsGui(windowsGui), icon(std::move(icon)), iconName(std::move(iconName)), fixedBase(fixedBase), traceImports(traceImports), message(std::move(message)) { if (fixedBase && (*fixedBase < 0x20000 || *fixedBase > 0x7fffffff0000 || (*fixedBase & 0xffff) != 0)) throw Domain::RelinkerException("Invalid fixed Windows guest image base"); } const std::string& WindowsPePatcher::GetDiagnostics() const { return diagnostics; } -std::vector WindowsPePatcher::Patch(const std::vector& sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, const std::uint64_t originalPltGotVaddr, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector& trampolines) { +std::vector WindowsPePatcher::Patch(std::vector sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, const std::uint64_t originalPltGotVaddr, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector& trampolines) { const auto report = [&](std::uint64_t completed) { if (progress) progress(completed, 10000); }; report(0); diagnostics.clear(); @@ -68,7 +68,7 @@ std::vector WindowsPePatcher::Patch(const std::vector WindowsPePatcher::Patch(const std::vector(index), import.RelocationType, import.Library}); relocations.Imports.back().Function = import.Function; + relocations.Imports.back().Weak = import.Weak; } } libraries.insert(libraries.begin(), guestPaths.begin(), guestPaths.end()); diff --git a/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp index 0b64a5a2d5..48d867f0f5 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp @@ -44,14 +44,14 @@ std::vector WindowsPeWriter::Write(const std::vector& s bool foundEntry = false; for (std::size_t index = 0; index < sections.size(); ++index) { const auto& section = sections[index]; - if (section.Name.empty() || section.Name.size() > 8 || section.Data.empty() || section.Rva != endRva) + if (section.Name.empty() || section.Name.size() > 8 || section.MemorySize() == 0 || section.Rva != endRva) throw Domain::RelinkerException("Invalid PE section layout: " + section.Name, section.Rva); const auto initialized = static_cast(std::find_if(section.Data.rbegin(), section.Data.rend(), [](const std::uint8_t value) { return value != 0; }).base() - section.Data.begin()); const auto rawSize = Io::AlignUp(CheckedRva(initialized), FileAlignment); const auto rawOffset = rawSize == 0 ? 0u : CheckedRva(result.size()); const auto header = sectionTable + index * 40; std::copy(section.Name.begin(), section.Name.end(), result.begin() + static_cast(header)); - Io::WriteU32(result, header + 8, CheckedRva(section.Data.size())); + Io::WriteU32(result, header + 8, CheckedRva(section.MemorySize())); Io::WriteU32(result, header + 12, section.Rva); Io::WriteU32(result, header + 16, rawSize); Io::WriteU32(result, header + 20, rawOffset); @@ -68,7 +68,7 @@ std::vector WindowsPeWriter::Write(const std::vector& s result.insert(result.end(), section.Data.begin(), section.Data.begin() + static_cast(initialized)); result.resize(static_cast(rawOffset) + rawSize); } - endRva = AlignRva(section.Rva + static_cast(section.Data.size())); + endRva = AlignRva(section.Rva + section.MemorySize()); } if (!foundEntry) throw Domain::RelinkerException("PE entry point is not inside executable section data", entryRva); diff --git a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp index e7cd5aec37..71b149d37f 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -25,6 +26,8 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom const auto symbol = info >> 32; if ((plt && type != 7) || (type != 1 && type != 6 && type != 7 && type != 8 && type != 16 && type != 17 && type != 18)) throw Domain::RelinkerException("Unsupported Windows relocation type " + std::to_string(type), target); + if (target > std::numeric_limits::max() - 8) + throw Domain::RelinkerException("Relocation target address overflow", target); const auto rva = image.GetRva(target, 8); const auto next = ranges.lower_bound(target); if ((next != ranges.end() && next->first < target + 8) || (next != ranges.begin() && std::prev(next)->second > target)) @@ -82,8 +85,14 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom std::vector WindowsRelocationBuilder::BuildBaseRelocations(const std::vector& targets) const { std::map> pages; - for (const auto target : targets) - pages[target & ~0xfffu].push_back(static_cast(0xa000u | (target & 0xfffu))); + for (const auto target : targets) { + if ((target & 0xfffu) > 0x1000u - 8) + throw Domain::RelinkerException("Base relocation target crosses a page boundary", target); + auto& entries = pages[target & ~0xfffu]; + const auto entry = static_cast(0xa000u | (target & 0xfffu)); + if (std::find(entries.begin(), entries.end(), entry) == entries.end()) + entries.push_back(entry); + } std::vector result; for (const auto& [page, entries] : pages) { Io::AppendU32(result, page); diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index 2a017013c4..b78a010b40 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -90,16 +91,32 @@ void patchAccess(std::vector& sections, const TlsAccess& access, cons throw Domain::RelinkerException("TLS instruction is outside the PE image", access.Rva); } +void trapInstruction(std::vector& sections, const std::uint32_t rva, const std::size_t length) { + for (auto& section : sections) { + if (rva < section.Rva || rva - section.Rva >= section.Data.size()) + continue; + const auto offset = rva - section.Rva; + if (length > section.Data.size() - offset) + throw Domain::RelinkerException("Guest GS instruction crosses a PE section boundary", rva); + std::fill_n(section.Data.begin() + offset, length, 0x90); + std::copy(std::begin(kStubOpUd2), std::end(kStubOpUd2), section.Data.begin() + offset); + return; + } + throw Domain::RelinkerException("Guest GS instruction is outside the PE image", rva); } -PeDirectory WindowsTlsBuilder::Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva, const std::function& progress) const { +} + +PeDirectory WindowsTlsBuilder::Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva, const std::function& progress, const std::function& message) const { const auto report = [&](std::uint64_t completed) { if (progress) progress(completed, 10000); }; const Domain::ProgramHeader* tls = nullptr; const Codegen::X64InstructionDecoder decoder; std::vector accesses; - std::set branchTargets; - const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, [&](auto completed, auto total) { report(7000 * completed / total); }); + std::vector> gsTraps; + std::vector branchTargets; + std::set entryRoots; + const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, [&](auto completed, auto total) { report(7000 * completed / total); }, &entryRoots, message); std::uint64_t scanned = 0; for (const auto& header : headers) { @@ -120,13 +137,23 @@ 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; if (target >= 0 && target <= std::numeric_limits::max()) - branchTargets.insert(static_cast(target)); + branchTargets.push_back(static_cast(target)); } + if (info.SegmentPrefix == 0x65 && !insidePrevious) { + if (info.HasMemoryOperand) + gsTraps.emplace_back(rva, info.Length); + continue; + } if (info.SegmentPrefix != 0 && !(insidePrevious && info.SegmentPrefix != 0x64)) { const auto position = info.OpcodeOffset; bool hasOperandSizePrefix = false; @@ -143,7 +170,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 +179,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); @@ -167,7 +194,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co if (!next.IsTwoByteOpcode && next.Opcode != 0xe9) { movedBytes = next.Opcode == 0xeb ? std::vector{0xe9} : std::vector{0x0f, static_cast(0x80 | (next.Opcode & 0xf))}; branchDisplacementOffset = movedBytes.size(); - movedBytes.insert(movedBytes.end(), 4, 0); + movedBytes.resize(movedBytes.size() + 4); } else { branchDisplacementOffset = next.Length - 4; } @@ -175,6 +202,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,12 +214,15 @@ 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}}); } } } report(9000); + for (const auto& [rva, length] : gsTraps) + trapInstruction(sections, rva, length); if (tls == nullptr) { if (!accesses.empty()) throw Domain::RelinkerException("Guest TLS access without PT_TLS"); @@ -217,13 +248,13 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co if (displacement != stackGuardDisplacement && (displacement > 0 || displacement < -static_cast(threadPointerOffset))) throw Domain::RelinkerException("Windows guest TLS load displacement " + std::to_string(displacement) + " is not in the thread TLS block, 0 or the stack guard at 0x28", access.FileOffset); } - PeSection data{".gtls", nextRva, SectionRead | SectionWrite | 0x40u, std::vector(templateOffset + blockSize + threadControlBlockSize)}; + PeSection data{".gtls", nextRva, SectionRead | SectionWrite | 0x40u, std::vector(templateOffset + tls->FileSize), CheckedRva(templateOffset + blockSize + threadControlBlockSize)}; const auto indexRva = nextRva + 40; if (tlsIndexRva != nullptr) *tlsIndexRva = indexRva; const auto callbackTableRva = nextRva + 48; const auto templateRva = nextRva + templateOffset; - const auto codeRva = AlignRva(nextRva + data.Data.size()); + const auto codeRva = AlignRva(nextRva + data.MemorySize()); WindowsStubEmitter code(codeRva); const auto loadPointer = [&] { @@ -243,11 +274,14 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co code.PatchBranch(skipCallback, code.GetRva()); code.Emit({0xc3}); + std::sort(branchTargets.begin(), branchTargets.end()); + branchTargets.erase(std::unique(branchTargets.begin(), branchTargets.end()), branchTargets.end()); + std::uint64_t patched = 0; for (const auto& access : accesses) { if (patched % 256 == 0) report(9000 + 900 * patched / accesses.size()); ++patched; - const auto target = branchTargets.upper_bound(access.Rva); + const auto target = std::upper_bound(branchTargets.begin(), branchTargets.end(), access.Rva); if (target != branchTargets.end() && *target < access.Rva + access.Length) throw Domain::RelinkerException("Branch enters a guest TLS instruction", *target); patchAccess(sections, access, code.GetRva()); diff --git a/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp index f8d06aa53d..1fcf78fad7 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp @@ -5,18 +5,28 @@ #include #include #include +#include #include namespace Elfpatcher::Windows { namespace { +struct SitePatch { + PeSection* Section = nullptr; + std::ptrdiff_t Offset = 0; + std::size_t Length = 0; + std::vector JumpBytes; +}; + PeSection& findSection(std::vector& sections, const std::uint32_t rva, const std::size_t length) { for (auto& section : sections) { if (rva < section.Rva || rva - section.Rva >= section.Data.size()) continue; if (length > section.Data.size() - (rva - section.Rva)) throw Domain::RelinkerException("AMD-only instruction crosses a PE section boundary", rva); + if ((section.Characteristics & SectionExecute) == 0) + throw Domain::RelinkerException("AMD-only instruction is in a non-executable PE section", rva); return section; } throw Domain::RelinkerException("AMD-only instruction is outside the PE image", rva); @@ -28,12 +38,26 @@ void WindowsTrampolineBuilder::Build(const std::vector& using namespace Codegen::Amd64OnlySubstitutionTable; if (sites.empty()) return; + if (nextRva < LoadRva || nextRva % SectionAlignment != 0) + throw Domain::RelinkerException("Invalid Windows trampoline section RVA", nextRva); + if (std::any_of(sections.begin(), sections.end(), [](const auto& section) { return section.Name == ".amdstub"; })) + throw Domain::RelinkerException("Duplicate Windows trampoline section"); + if (!sections.empty() && nextRva < AlignRva(static_cast(sections.back().Rva) + sections.back().MemorySize())) + throw Domain::RelinkerException("Windows trampoline section overlaps preceding section", nextRva); const auto sectionRva = nextRva; std::vector bytes; + std::map ranges; + std::vector patches; + patches.reserve(sites.size()); for (const auto& site : sites) { const auto siteRva = image.GetRva(site.Address, site.Length); if (site.Length < kJmpRel32.Size || site.OriginalBytes.size() != site.Length || site.Body.size() < kJmpRel32.Size || site.ReturnBranchOffset > site.Body.size() - kJmpRel32.Size || site.Body[site.ReturnBranchOffset] != kJmpRel32.Bytes[0]) throw Domain::RelinkerException("Invalid AMD-only trampoline site", siteRva); + const auto siteEndRva = static_cast(siteRva) + site.Length; + const auto next = ranges.lower_bound(siteRva); + if ((next != ranges.end() && next->first < siteEndRva) || (next != ranges.begin() && std::prev(next)->second > siteRva)) + throw Domain::RelinkerException("Overlapping AMD-only trampoline sites", siteRva); + ranges.emplace(siteRva, siteEndRva); auto& section = findSection(sections, siteRva, site.Length); const auto offset = static_cast(siteRva - section.Rva); if (!std::equal(site.OriginalBytes.begin(), site.OriginalBytes.end(), section.Data.begin() + offset)) @@ -43,19 +67,22 @@ void WindowsTrampolineBuilder::Build(const std::vector& const auto bodyOffset = bytes.size(); bytes.insert(bytes.end(), site.Body.begin(), site.Body.end()); Codegen::ApplyStubRelocations(std::span(bytes.data() + bodyOffset, site.ReturnBranchOffset), site.Relocations, siteRva, stubRva, site.Offset); - const auto returnRva = CheckedRva(siteRva + site.Length); + const auto returnRva = CheckedRva(siteEndRva); const auto displacement = static_cast(returnRva) - (static_cast(stubRva) + static_cast(site.ReturnBranchOffset + kJmpRel32.Size)); if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) throw Domain::RelinkerException("AMD-only stub return exceeds rel32 range", returnRva); Io::WriteU32(bytes, bodyOffset + site.ReturnBranchOffset + 1, static_cast(displacement)); WindowsStubEmitter jump(siteRva); jump.Rip({0xe9}, stubRva); - const auto jumpBytes = jump.TakeBytes(); - std::fill_n(section.Data.begin() + offset, site.Length, kNop1.Bytes[0]); - std::copy(jumpBytes.begin(), jumpBytes.end(), section.Data.begin() + offset); + patches.push_back({§ion, offset, site.Length, jump.TakeBytes()}); + } + const auto endRva = AlignRva(sectionRva + bytes.size()); + for (const auto& patch : patches) { + std::fill_n(patch.Section->Data.begin() + patch.Offset, patch.Length, kNop1.Bytes[0]); + std::copy(patch.JumpBytes.begin(), patch.JumpBytes.end(), patch.Section->Data.begin() + patch.Offset); } sections.push_back({".amdstub", sectionRva, SectionRead | SectionExecute | 0x20u, std::move(bytes)}); - nextRva = AlignRva(sectionRva + sections.back().Data.size()); + nextRva = endRva; } } diff --git a/core/relinker/elfpatcher/tests/EntryStubTests.cpp b/core/relinker/elfpatcher/tests/EntryStubTests.cpp index cd3737ed0e..1fe8dc32e9 100644 --- a/core/relinker/elfpatcher/tests/EntryStubTests.cpp +++ b/core/relinker/elfpatcher/tests/EntryStubTests.cpp @@ -1,9 +1,14 @@ #include +#include +#include +#include +#include #include #include #include #include #include +#include #include namespace { @@ -36,11 +41,138 @@ void stubTerminatesTheFrameChain() { require(bytesAt(stub, offset, {0x0f, 0x0b}) && offset == stub.size(), "the stub does not end with ud2 after the call"); } +std::uint32_t wideStringRva(const Elfpatcher::Windows::PeSection& section, const std::u16string& text) { + std::vector needle; + for (const auto unit : text) { + needle.push_back(static_cast(unit)); + needle.push_back(static_cast(unit >> 8)); + } + needle.insert(needle.end(), {0, 0}); + for (std::size_t offset = 0; offset + needle.size() <= section.Data.size(); offset += 2) { + if (std::memcmp(section.Data.data() + offset, needle.data(), needle.size()) == 0) return section.Rva + static_cast(offset); + } + throw std::runtime_error("the stub data does not hold the expected path"); +} + +std::size_t ripReference(const Elfpatcher::Windows::PeSection& code, const std::vector& opcode, const std::uint32_t target, const std::size_t from = 0) { + for (std::size_t offset = from; offset + opcode.size() + 4 <= code.Data.size(); ++offset) { + if (std::memcmp(code.Data.data() + offset, opcode.data(), opcode.size()) != 0) continue; + std::int32_t displacement = 0; + std::memcpy(&displacement, code.Data.data() + offset + opcode.size(), sizeof(displacement)); + if (code.Rva + offset + opcode.size() + 4 + static_cast(displacement) == target) return offset; + } + return std::string::npos; +} + +Elfpatcher::Windows::WindowsEntryStub windowsStub(const Elfpatcher::Windows::WindowsImports& nativeImports, const std::string& runPath, const std::vector& libraries) { + std::vector imports; + for (std::size_t index = 0; index < libraries.size(); ++index) { + imports.push_back({.Name = "import" + std::to_string(index), .TargetRva = static_cast(0x2000 + index * 8), .Addend = 0, .Library = libraries[index]}); + } + return Elfpatcher::Windows::WindowsEntryStubBuilder().Build(0x40000, 0x1000, nativeImports, libraries, imports, runPath, false, false); +} + +void windowsStubLoadsTheAllocatorFirst(const std::string& runPath, const std::u16string& directory, const std::vector& pathOpcode) { + const auto nativeImports = Elfpatcher::Windows::WindowsImportBuilder().Build(0x1000); + const auto stub = windowsStub(nativeImports, runPath, {"libc.prx", "libkernel.prx"}); + const auto allocator = ripReference(stub.Code, pathOpcode, wideStringRva(stub.Data, directory + u"mimalloc.dll")); + const auto library = ripReference(stub.Code, pathOpcode, wideStringRva(stub.Data, directory + u"libc.prx")); + require(allocator != std::string::npos, runPath + ": the stub does not use mimalloc.dll's path"); + require(library != std::string::npos, runPath + ": the stub does not use the first library's path"); + const auto load = ripReference(stub.Code, {0xff, 0x15}, nativeImports.Functions.at("LoadLibraryExW"), allocator); + require(allocator < library && load < library, runPath + ": the stub loads a library before mimalloc.dll"); + std::size_t flags = load - 8; + require(bytesAt(stub.Code.Data, flags, {0x31, 0xd2, 0x41, 0xb8, 0x00, 0x11, 0x00, 0x00}), runPath + ": mimalloc.dll is not loaded from the stub's search directories"); +} + +void windowsStubLoadsTheAllocatorOnRequest(const std::string& runPath, const std::u16string& directory, const std::vector& pathOpcode) { + const auto nativeImports = Elfpatcher::Windows::WindowsImportBuilder().Build(0x1000); + const auto stub = windowsStub(nativeImports, runPath, {"libc.prx"}); + const auto variable = ripReference(stub.Code, {0x48, 0x8d, 0x0d}, wideStringRva(stub.Data, u"APS5_MIMALLOC")); + const auto allocator = ripReference(stub.Code, pathOpcode, wideStringRva(stub.Data, directory + u"mimalloc.dll")); + require(variable != std::string::npos && allocator != std::string::npos && variable < allocator, runPath + ": the stub does not read APS5_MIMALLOC before loading mimalloc.dll"); + std::size_t cursor = variable + 7; + require(bytesAt(stub.Code.Data, cursor, {0x31, 0xd2, 0x45, 0x31, 0xc0}) && ripReference(stub.Code, {0xff, 0x15}, nativeImports.Functions.at("GetEnvironmentVariableW"), cursor) == cursor, runPath + ": APS5_MIMALLOC is not read with GetEnvironmentVariableW"); + cursor += 6; + require(bytesAt(stub.Code.Data, cursor, {0x85, 0xc0, 0x0f, 0x84}), runPath + ": an unset APS5_MIMALLOC does not branch"); + std::int32_t displacement = 0; + std::memcpy(&displacement, stub.Code.Data.data() + cursor, sizeof(displacement)); + const auto target = static_cast(cursor) + 4 + displacement; + const auto load = ripReference(stub.Code, {0xff, 0x15}, nativeImports.Functions.at("LoadLibraryExW"), allocator); + require(load != std::string::npos && target == static_cast(load) + 6, runPath + ": an unset APS5_MIMALLOC does not skip exactly the load of mimalloc.dll"); +} + +void windowsStubRejectsAnOverlongAllocatorPath() { + const auto nativeImports = Elfpatcher::Windows::WindowsImportBuilder().Build(0x1000); + const auto runPath = [](const std::size_t length) { return "C:\\" + std::string(length - 4, 'a') + "\\"; }; + constexpr std::size_t PathUnits = 32768; + const auto longest = PathUnits - 1 - std::string_view("mimalloc.dll").size() - 1; + static_cast(windowsStub(nativeImports, runPath(longest), {"libc.prx"})); + try { + static_cast(windowsStub(nativeImports, runPath(longest + 1), {"libc.prx"})); + } catch (const Domain::RelinkerException& error) { + const std::string message = error.what(); + require(message.starts_with("Windows library path exceeds the startup buffer: ") && message.ends_with("\\mimalloc.dll"), "an overlong allocator path fails with another error: " + message); + return; + } + throw std::runtime_error("an allocator path past the startup buffer was accepted"); +} + +void windowsLibSceAmprObjectImportsFallBackToTheLibcProvider() { + using namespace Elfpatcher::Windows; + const auto nativeImports = WindowsImportBuilder().Build(0x1000); + const std::vector libraries{"libSceAmpr.prx", "libc.prx"}; + const std::vector amprImport{{.Name = "-mLzBSk-VGs", .TargetRva = 0x2000, .Addend = 0, .Library = "libSceAmpr.prx"}}; + const auto amprStub = WindowsEntryStubBuilder().Build(0x40000, 0x1000, nativeImports, libraries, amprImport, "$ORIGIN/libs", false, false); + const std::vector fallbackOrder{0, 0, 0, 0, 1, 0, 0, 0}; + require(std::search(amprStub.Data.Data.begin(), amprStub.Data.Data.end(), fallbackOrder.begin(), fallbackOrder.end()) != amprStub.Data.Data.end(), + "a libSceAmpr-qualified locale object import does not fall back to libc"); + + const std::vector libcImport{{.Name = "-mLzBSk-VGs", .TargetRva = 0x2000, .Addend = 0, .Library = "libc.prx"}}; + const auto libcStub = WindowsEntryStubBuilder().Build(0x40000, 0x1000, nativeImports, libraries, libcImport, "$ORIGIN/libs", false, false); + const std::vector libcOrder{1, 0, 0, 0}; + require(std::search(libcStub.Data.Data.begin(), libcStub.Data.Data.end(), libcOrder.begin(), libcOrder.end()) != libcStub.Data.Data.end(), + "a libc-qualified locale object import does not resolve through libc"); +} + +void windowsKernelVariantImportsSearchCanonicalKernelAfterTheRequestedVariant() { + using namespace Elfpatcher::Windows; + const auto nativeImports = WindowsImportBuilder().Build(0x1000); + const std::vector libraries{"libkernel_sys.prx", "libc.prx", "libkernel.prx"}; + const std::vector imports{{.Name = "getpagesize", .TargetRva = 0x2000, .Addend = 0, .Library = "libkernel_sys.prx"}}; + const auto stub = WindowsEntryStubBuilder().Build(0x40000, 0x1000, nativeImports, libraries, imports, "$ORIGIN/libs", false, false); + const std::vector expected{0, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0}; + require(std::search(stub.Data.Data.begin(), stub.Data.Data.end(), expected.begin(), expected.end()) != stub.Data.Data.end(), + "a scoped libkernel_sys import does not search the requested variant before canonical libkernel and libc"); +} + +void windowsKernelVariantDependenciesLoadCanonicalKernel() { + using namespace Elfpatcher::Windows; + for (const auto& variant : {std::string("libkernel_sys.prx"), std::string("libkernel_web.prx")}) { + Domain::SysVDynamicSection dynamic; + dynamic.DynStrData.insert(dynamic.DynStrData.end(), variant.begin(), variant.end()); + dynamic.DynStrData.push_back(0); + dynamic.DynamicSegmentData.resize(16); + Io::WriteU64(dynamic.DynamicSegmentData, 0, 1); + Io::WriteU64(dynamic.DynamicSegmentData, 8, 0); + require(WindowsImportBuilder().ReadLibraries(dynamic) == std::vector{variant, "libkernel.prx", "libc.prx"}, + variant + ": canonical kernel fallback and its libc provider are not loaded after the requested module"); + } +} + } int main() { try { stubTerminatesTheFrameChain(); + windowsStubLoadsTheAllocatorFirst("C:/Games/Title/libs", u"C:\\Games\\Title\\libs\\", {0x48, 0x8d, 0x0d}); + windowsStubLoadsTheAllocatorFirst("$ORIGIN/libs", u"libs\\", {0x48, 0x8d, 0x35}); + windowsStubLoadsTheAllocatorOnRequest("C:/Games/Title/libs", u"C:\\Games\\Title\\libs\\", {0x48, 0x8d, 0x0d}); + windowsStubLoadsTheAllocatorOnRequest("$ORIGIN/libs", u"libs\\", {0x48, 0x8d, 0x35}); + windowsStubRejectsAnOverlongAllocatorPath(); + windowsLibSceAmprObjectImportsFallBackToTheLibcProvider(); + windowsKernelVariantImportsSearchCanonicalKernelAfterTheRequestedVariant(); + windowsKernelVariantDependenciesLoadCanonicalKernel(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; 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/LinuxDesktopEntryTests.cpp b/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp index bfbaf19eb6..0c473271cd 100644 --- a/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp +++ b/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -131,7 +132,8 @@ void run(const std::filesystem::path& root) { } int main() { - const auto root = std::filesystem::temp_directory_path() / "anyps5-desktop-entry-tests"; + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-desktop-entry-tests-" + std::to_string(std::random_device{}())); std::filesystem::remove_all(root); try { run(root); diff --git a/core/relinker/elfpatcher/tests/LinuxLoadPageTests.cpp b/core/relinker/elfpatcher/tests/LinuxLoadPageTests.cpp new file mode 100644 index 0000000000..d96fb4cebb --- /dev/null +++ b/core/relinker/elfpatcher/tests/LinuxLoadPageTests.cpp @@ -0,0 +1,177 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Bytes = std::vector; + +constexpr std::uint64_t kPage = 0x1000; + +void Require(const bool ok, const std::string& what) { + if (!ok) throw std::runtime_error("linux load pages: " + what); +} + +template +T Read(const Bytes& bytes, const std::uint64_t offset) { + T value{}; + std::memcpy(&value, bytes.data() + offset, sizeof(T)); + return value; +} + +template +void Write(Bytes& bytes, const std::uint64_t offset, const T value) { + std::memcpy(bytes.data() + offset, &value, sizeof(T)); +} + +Bytes Fixture(const std::size_t size) { + Bytes bytes(size); + bytes[0] = 0x7F; bytes[1] = 'E'; bytes[2] = 'L'; bytes[3] = 'F'; + bytes[4] = 2; bytes[5] = 1; bytes[6] = 1; + Write(bytes, 16, 3); + Write(bytes, 18, 62); + Write(bytes, 24, 0x400); + Write(bytes, 32, 64); + Write(bytes, 54, 56); + Write(bytes, 56, 1); + bytes[0x400] = 0xC3; + return bytes; +} + +Domain::ProgramHeader Load(const std::uint64_t offset, const std::uint64_t vaddr, const std::uint64_t fileSize, const std::uint64_t memorySize, const std::uint32_t flags) { + return {1, flags, offset, vaddr, vaddr, fileSize, memorySize, 0x1000}; +} + +Elfpatcher::Linux::LinuxElfPatcher Patcher() { + const auto byteWriter = std::make_shared(); + return Elfpatcher::Linux::LinuxElfPatcher( + std::make_shared(), + std::make_shared(std::make_shared(), byteWriter), + std::make_shared(byteWriter), + byteWriter); +} + +std::vector OutputLoads(const Bytes& output) { + std::vector loads; + const auto table = Read(output, 32); + const auto count = Read(output, 56); + for (std::uint16_t index = 0; index < count; ++index) { + const auto header = table + index * 56u; + if (Read(output, header) != 1) continue; + loads.push_back({1, Read(output, header + 4), Read(output, header + 8), Read(output, header + 16), + Read(output, header + 24), Read(output, header + 32), Read(output, header + 40), Read(output, header + 48)}); + } + return loads; +} + +std::map MapLikeLinux(const Bytes& output) { + std::map memory; + for (const auto& load : OutputLoads(output)) { + const auto prefix = load.MappedAddress % kPage; + Require(load.Offset % kPage == prefix, "PT_LOAD offset and address differ within a page"); + const auto fileEnd = load.MappedAddress + load.FileSize; + const auto filePageEnd = (fileEnd + kPage - 1) & ~(kPage - 1); + for (auto address = load.MappedAddress - prefix; address < filePageEnd; ++address) { + const auto offset = load.Offset - prefix + (address - (load.MappedAddress - prefix)); + memory[address] = offset < output.size() ? output[offset] : 0; + } + if (load.MemorySize > load.FileSize) { + const auto memoryPageEnd = (load.MappedAddress + load.MemorySize + kPage - 1) & ~(kPage - 1); + for (auto address = fileEnd; address < memoryPageEnd; ++address) + memory[address] = 0; + } + } + return memory; +} + +void RequireImage(const Bytes& output, const Bytes& source, const std::vector& headers, const std::string& what) { + const auto memory = MapLikeLinux(output); + for (const auto& load : headers) { + for (std::uint64_t relative = 0; relative < load.MemorySize; ++relative) { + const auto address = load.MappedAddress + relative; + const auto found = memory.find(address); + Require(found != memory.end(), what + ": address not mapped"); + const std::uint8_t expected = relative < load.FileSize ? source[load.Offset + relative] : 0; + if (found->second != expected) { + char text[96]; + std::snprintf(text, sizeof(text), ": byte at 0x%llx reads 0x%02x, not 0x%02x", static_cast(address), found->second, expected); + Require(false, what + text); + } + } + } +} + +std::uint64_t OutputOffset(const Bytes& output, const std::uint64_t vaddr) { + for (const auto& load : OutputLoads(output)) + if (load.MappedAddress == vaddr) return load.Offset; + throw std::runtime_error("linux load pages: no output PT_LOAD at the segment's address"); +} + +void ZeroFillBelowTheNextSegment() { + auto source = Fixture(0x3000); + std::fill(source.begin() + 0x1000, source.begin() + 0x1100, std::uint8_t{0xAA}); + std::fill(source.begin() + 0x1100, source.begin() + 0x1c20, std::uint8_t{0x55}); + std::fill(source.begin() + 0x1c20, source.begin() + 0x2000, std::uint8_t{0x77}); + const std::vector headers{ + Load(0x400, 0x400, 0x10, 0x10, 5), + Load(0x1000, 0x1000, 0x100, 0x1c20, 6), + Load(0x1c20, 0x2c20, 0x3e0, 0x3e0, 0), + }; + const auto output = Patcher().Patch(source, headers, {}, 0, "$ORIGIN/libs", true, false, {}); + RequireImage(output, source, headers, "bss sharing a page with the next segment"); +} + +void DataAndZeroFillBelowTheNextSegment() { + auto source = Fixture(0x4000); + std::fill(source.begin() + 0x1000, source.begin() + 0x2400, std::uint8_t{0xAA}); + std::fill(source.begin() + 0x2400, source.begin() + 0x2c20, std::uint8_t{0x55}); + std::fill(source.begin() + 0x2c20, source.begin() + 0x3000, std::uint8_t{0x77}); + const std::vector headers{ + Load(0x400, 0x400, 0x10, 0x10, 5), + Load(0x1000, 0x1000, 0x1400, 0x1c20, 6), + Load(0x2c20, 0x2c20, 0x3e0, 0x3e0, 0), + }; + const auto output = Patcher().Patch(source, headers, {}, 0, "$ORIGIN/libs", true, false, {}); + RequireImage(output, source, headers, "data and bss sharing a page with the next segment"); +} + +void ZeroFileBytesStayInPlace() { + auto source = Fixture(0x3000); + std::fill(source.begin() + 0x1000, source.begin() + 0x1100, std::uint8_t{0xAA}); + std::fill(source.begin() + 0x2c20, source.begin() + 0x3000, std::uint8_t{0x77}); + const std::vector headers{ + Load(0x400, 0x400, 0x10, 0x10, 5), + Load(0x1000, 0x1000, 0x100, 0x1c20, 6), + Load(0x2c20, 0x2c20, 0x3e0, 0x3e0, 0), + }; + const auto output = Patcher().Patch(source, headers, {}, 0, "$ORIGIN/libs", true, false, {}); + RequireImage(output, source, headers, "zero bytes below the next segment"); + Require(OutputOffset(output, 0x2c20) == 0x2c20, "a segment whose page already reads right was moved"); +} + +} + +int main() { + try { + ZeroFillBelowTheNextSegment(); + DataAndZeroFillBelowTheNextSegment(); + ZeroFileBytesStayInPlace(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + return 0; +} diff --git a/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp b/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp index 3cf152c379..18748830de 100644 --- a/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp +++ b/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp @@ -1,30 +1,90 @@ +#include #include #include +#include #include #include #include +#include class KeepAll : public Elfpatcher::ISegmentFilter { public: bool ShouldSkip(const Domain::ProgramHeader&) const override { return false; } }; -static void Require(bool ok) { if (!ok) throw std::runtime_error("Program layout range validation failed"); } +static void Require(bool ok, const char* what) { if (!ok) throw std::runtime_error(std::string("program layout: ") + what); } + +static Domain::ProgramHeader Load(std::uint64_t vaddr, std::uint64_t memorySize, std::uint64_t fileSize, std::uint32_t flags) { + Domain::ProgramHeader load{}; + load.Type = 1; load.Flags = flags; load.Offset = vaddr; load.MappedAddress = vaddr; + load.PhysicalAddress = vaddr; load.FileSize = fileSize; load.MemorySize = memorySize; load.Alignment = 0x1000; + return load; +} + +static std::vector MappedLoads(const std::vector& buf, std::uint64_t tableOffset, std::uint16_t count) { + std::vector loads; + for (std::uint16_t index = 0; index < count; ++index) { + const auto* entry = buf.data() + tableOffset + index * 56; + if (*reinterpret_cast(entry) != 1) + continue; + Domain::ProgramHeader ph{}; + ph.Flags = *reinterpret_cast(entry + 4); + ph.Offset = *reinterpret_cast(entry + 8); + ph.MappedAddress = *reinterpret_cast(entry + 16); + ph.FileSize = *reinterpret_cast(entry + 32); + ph.MemorySize = *reinterpret_cast(entry + 40); + ph.Alignment = *reinterpret_cast(entry + 48); + loads.push_back(ph); + } + return loads; +} + +static void RequireExtraBlockStartsOnItsOwnPage(std::vector loads, std::uint64_t extraFileOffset) { + std::sort(loads.begin(), loads.end(), [](const Domain::ProgramHeader& left, const Domain::ProgramHeader& right) { + return left.MappedAddress < right.MappedAddress; + }); + std::size_t extraIndex = loads.size(); + for (std::size_t index = 0; index < loads.size(); ++index) { + Require(loads[index].FileSize <= loads[index].MemorySize, "load file size above its memory size"); + Require(loads[index].MappedAddress % loads[index].Alignment == loads[index].Offset % loads[index].Alignment, "load vaddr and offset differ modulo alignment"); + if (loads[index].Offset == extraFileOffset) + extraIndex = index; + } + Require(extraIndex < loads.size(), "no load at the extra block offset"); + const auto& extraBlock = loads[extraIndex]; + Require(extraBlock.MemorySize >= extraBlock.FileSize, "extra block memory size below its file size"); + for (std::size_t index = 0; index < loads.size(); ++index) { + if (index == extraIndex) + continue; + const auto& other = loads[index]; + if (other.MappedAddress < extraBlock.MappedAddress) { + Require((other.MappedAddress + other.FileSize - 1) / 0x1000 < extraBlock.MappedAddress / 0x1000, "load below the extra block ends on its page"); + } else { + Require(other.MappedAddress >= extraBlock.MappedAddress + extraBlock.FileSize, "load above the extra block overlaps it"); + } + } +} int main() { try { - Elfpatcher::ProgramHeaderLayoutBuilder builder(std::make_shared(), std::make_shared()); + auto byteWriter = std::make_shared(); + Elfpatcher::ProgramHeaderLayoutBuilder builder(std::make_shared(), byteWriter); constexpr auto maximum = std::numeric_limits::max(); Domain::ProgramHeader load{}; load.Type = 1; load.MappedAddress = maximum - 3; load.MemorySize = 8; bool rejected = false; try { builder.ComputeExtraBlockVaddr({load}, 0x1000); } catch (const std::exception&) { rejected = true; } - Require(rejected); + Require(rejected, "overflowing load end accepted"); load.MappedAddress = 0x1000; load.MemorySize = 0x1000; - Require(builder.ComputeExtraBlockVaddr({load}, 0x1003) == 0x2003); - Elfpatcher::ProgramHeaderLayoutRequest valid{64, 56, 6, {load}, 0x1000, 0x2000, 0x100, 0x1000, 16, 0x1010, 8}; + Require(builder.ComputeExtraBlockVaddr({load}, 0x1003) == 0x2003, "vaddr for aligned end 0x2000, offset 0x1003"); + load.MemorySize = 0x1800; + Require(builder.ComputeExtraBlockVaddr({load}, 0x8A00) == 0x3A00, "vaddr for end 0x2800, offset 0x8A00"); + Require(builder.ComputeExtraBlockVaddr({load}, 0x8800) == 0x3800, "vaddr for end 0x2800, offset 0x8800"); + load.MappedAddress = 0x1000; load.MemorySize = 0x1000; + Require(builder.ComputeExtraBlockVaddr({load}, 0x1000) == 0x2000, "vaddr for aligned end 0x2000, offset 0x1000"); + Elfpatcher::ProgramHeaderLayoutRequest valid{64, 56, 7, {load}, 0x1000, 0x2000, 0x100, 0x1000, 16, 0x1010, 8}; std::vector output(0x1100, 0xA5); - Require(builder.WriteLayout(output, valid) == 6); + Require(builder.WriteLayout(output, valid) == 7, "header count for a valid request"); for (int kind = 0; kind < 11; ++kind) { auto request = valid; switch (kind) { @@ -38,14 +98,57 @@ int main() { case 7: request.InterpOffset = maximum; break; case 8: request.ExtraBlockOffset = maximum; break; case 9: request.ExtraBlockSize = 0x1000; break; - case 10: request.PhOff = 0xF00; request.ExtraBlockVaddr = maximum - 0x1fff; break; + case 10: request.PhOff = 0xF00; break; } std::vector bytes(0x1100, 0xA5); const auto before = bytes; rejected = false; try { builder.WriteLayout(bytes, request); } catch (const std::exception&) { rejected = true; } - Require(rejected && bytes == before); + Require(rejected && bytes == before, "invalid request accepted or output changed"); } - std::cout << "14 program layout cases passed\n"; + const std::uint16_t tableCount = 3 + 5; + const auto writeLayout = [&builder, tableCount](const std::vector& headers, std::uint64_t extraFileOffset, std::uint64_t extraFileSize) { + const std::uint64_t tableSize = tableCount * 56; + std::vector bytes(std::max(0xB000, extraFileOffset + tableSize + extraFileSize), 0); + Elfpatcher::ProgramHeaderLayoutRequest request{extraFileOffset, 56, tableCount, headers, extraFileOffset, + builder.ComputeExtraBlockVaddr(headers, extraFileOffset), tableSize + extraFileSize, + extraFileOffset, 0x20, extraFileOffset + 0x20, 8}; + const std::uint16_t written = builder.WriteLayout(bytes, request); + Require(written == tableCount, "written header count"); + RequireExtraBlockStartsOnItsOwnPage(MappedLoads(bytes, extraFileOffset, written), extraFileOffset); + }; + writeLayout({Load(0x1000, 0x1800, 0x1800, 6), Load(0x3000, 0x1000, 0x1000, 4)}, 0x8A00, 0x400); + writeLayout({Load(0x1000, 0x8000, 0x800, 6), Load(0xA000, 0x1000, 0x1000, 4)}, 0x8A00, 0x400); + writeLayout({Load(0x1000, 0x8000, 0x800, 6), Load(0xA000, 0x1000, 0x1000, 4)}, 0x8000, 0x400); + writeLayout({Load(0x1000, 0x2000, 0x800, 6), Load(0x3000, 0x3000, 0x1000, 4)}, 0x8A00, 0x400); + writeLayout({Load(0x1000, 0x1200, 0x1200, 5), Load(0x2400, 0x1000, 0x1000, 6)}, 0x8A00, 0x400); + writeLayout({Load(0x1000, 0x3000, 0x1000, 5), Load(0x3000, 0x4000, 0x1000, 6)}, 0x8A00, 0x400); + writeLayout({Load(0x1000, 0x1000, 0x1000, 5), Load(0x2000, 0x1000, 0x0400, 6)}, 0x8800, 0x400); + const std::vector singleLoad{Load(0x1000, 0x1800, 0x1800, 6)}; + Require(builder.RequiredProgramHeaderCount(singleLoad) == 7, "required count must reserve a slot for the synthetic stack"); + const auto writeSingle = [&builder, &singleLoad](std::uint16_t slots) { + std::vector table(0xB000, 0); + Elfpatcher::ProgramHeaderLayoutRequest request{0x8A00, 56, slots, singleLoad, 0x8A00, + builder.ComputeExtraBlockVaddr(singleLoad, 0x8A00), static_cast(slots) * 56 + 0x400, 0x8A00, 0x20, 0x8A20, 8}; + const std::uint16_t written = builder.WriteLayout(table, request); + return std::pair(written, std::move(table)); + }; + const auto [written, table] = writeSingle(builder.RequiredProgramHeaderCount(singleLoad)); + Require(written == 7, "layout must write exactly the required header count"); + std::size_t stacks = 0; + for (std::uint16_t index = 0; index < written; ++index) { + const auto entry = 0x8A00 + index * 56; + if (*reinterpret_cast(table.data() + entry) != Elfpatcher::PT_GNU_STACK) continue; + ++stacks; + Require(*reinterpret_cast(table.data() + entry + 4) == (Elfpatcher::PF_R | Elfpatcher::PF_W) + && *reinterpret_cast(table.data() + entry + 16) == 0 + && *reinterpret_cast(table.data() + entry + 48) == 16, + "synthetic stack must be RW with alignment 16 and no mapping"); + } + Require(stacks == 1, "layout must synthesize exactly one PT_GNU_STACK when the input has none"); + bool stackSlotRejected = false; + try { (void)writeSingle(6); } catch (const std::exception&) { stackSlotRejected = true; } + Require(stackSlotRejected, "layout must refuse to drop the synthetic stack when its slot is missing"); + std::cout << "21 program layout cases passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } } diff --git a/core/relinker/elfpatcher/tests/SectionRangeTests.cpp b/core/relinker/elfpatcher/tests/SectionRangeTests.cpp index 7f718b66ce..c75c4e9773 100644 --- a/core/relinker/elfpatcher/tests/SectionRangeTests.cpp +++ b/core/relinker/elfpatcher/tests/SectionRangeTests.cpp @@ -29,7 +29,7 @@ int main() { auto empty = valid; empty.StubOffset = 384; empty.StubSize = 0; bytes = Fixture(); builder.WriteTable(bytes, empty); Require(Io::ReadU64(bytes, Io::ReadU64(bytes, 40) + 5 * 64 + 16) == 0x1100); - for (int kind = 0; kind < 8; ++kind) { + for (int kind = 0; kind < 11; ++kind) { bytes = Fixture(); auto request = valid; switch (kind) { case 0: request.DynStrSize = 257; break; @@ -40,12 +40,15 @@ int main() { case 5: Io::WriteU64(bytes, 32, std::numeric_limits::max()); break; case 6: Io::WriteU64(bytes, 80, std::numeric_limits::max() - 4); break; case 7: Io::WriteU16(bytes, 56, 20); break; + case 8: request.DynStrSize = 16; break; + case 9: bytes.resize(40); break; + case 10: Io::WriteU64(bytes, 32, 48); Io::WriteU32(bytes, 48, 1); Io::WriteU64(bytes, 56, 128); break; } const auto before = bytes; bool rejected = false; - try { builder.WriteTable(bytes, request); } catch (const std::exception&) { rejected = true; } + try { builder.WriteTable(bytes, request); } catch (const std::runtime_error&) { rejected = true; } Require(rejected && bytes == before); } - std::cout << "10 section range cases passed\n"; + std::cout << "13 section range cases passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } } diff --git a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp index c1c649a8e5..15197aa868 100644 --- a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -46,16 +47,46 @@ 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({"libkernel_sys.prx"}, {"libkernel_sys.prx", "libkernel.prx", "libc.prx"}); + check({"libkernel_web.prx"}, {"libkernel_web.prx", "libkernel.prx", "libc.prx"}); check({"libSceVideoOut.prx", "libSceVideoOut.prx"}, {"libSceVideoOut.prx"}); } +template +void openFixture(TStream& stream, const Fs::path& path, std::ios::openmode mode) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + for (;;) { + errno = 0; + _set_doserrno(0); + stream.clear(); + stream.open(path, mode); + if (stream.is_open() || _doserrno != ERROR_SHARING_VIOLATION || std::chrono::steady_clock::now() >= deadline) + return; + Sleep(10); + } +} + +void removeFixtures(const Fs::path& directory) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + for (;;) { + std::error_code error; + Fs::remove_all(directory, error); + if (!error) + return; + if (error != std::errc::permission_denied || std::chrono::steady_clock::now() >= deadline) + throw Fs::filesystem_error("cannot remove diagnostic fixtures", directory, error); + Sleep(10); + } +} + void writeFile(const Fs::path& path, const std::vector& bytes) { const auto fail = [&](const char* phase) { const auto error = errno; @@ -65,9 +96,8 @@ void writeFile(const Fs::path& path, const std::vector& bytes) { " native_error=" + std::to_string(nativeError) + " bytes=" + std::to_string(bytes.size())); }; - errno = 0; - _set_doserrno(0); - std::ofstream stream(path, std::ios::binary); + std::ofstream stream; + openFixture(stream, path, std::ios::binary); if (!stream.is_open()) fail("open"); if (!stream.write(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) @@ -184,7 +214,7 @@ void expectDiagnostic(const Fs::path& runner, const std::vector& ex if (!CreateProcessW(filename.c_str(), nullptr, nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, runner.parent_path().wstring().c_str(), &startup, &process)) throw std::runtime_error("Cannot start diagnostic test: " + std::to_string(GetLastError())); CloseHandle(output); - if (WaitForSingleObject(process.hProcess, 20000) != WAIT_OBJECT_0) { + if (WaitForSingleObject(process.hProcess, 120000) != WAIT_OBJECT_0) { TerminateProcess(process.hProcess, 1); throw std::runtime_error("Diagnostic test timed out"); } @@ -243,12 +273,15 @@ int main() { expectDiagnostic(runner, {"Symbol: MissingInCycle", "Importer: " + leaf.string()}); createImage(root, {{"diagnostic-absent.dll", "Missing"}}, {}); expectDiagnostic(runner, {"dependency module not found", "Importer: " + root.string()}); + createImage(root, {{"api-ms-win-core-relinkerdiagnostics-l1-1-0.dll", "Missing"}}, {}); + expectDiagnostic(runner, {"dependency module not found: api-ms-win-core-relinkerdiagnostics-l1-1-0.dll", "Importer: " + root.string(), runner.string() + " -> " + root.string() + " -> api-ms-win-core-relinkerdiagnostics-l1-1-0.dll"}); createImage(root, {{"KERNEL32.dll", "CreateRemoteThreadEx"}}, {}); expectDiagnostic(runner, {"Dependency tables contain no missing imports"}); createImage(root, {{"KERNEL32.dll", "RelinkerDiagnosticsMissingExport"}}, {}); expectDiagnostic(runner, {"Symbol: RelinkerDiagnosticsMissingExport"}); { - std::fstream corrupt(root, std::ios::binary | std::ios::in | std::ios::out); + std::fstream corrupt; + openFixture(corrupt, root, std::ios::binary | std::ios::in | std::ios::out); const std::array invalidRva{static_cast(0xf0), static_cast(0xff), static_cast(0xff), static_cast(0xff)}; corrupt.seekp(LoadRva + 0x300); if (!corrupt.write(invalidRva.data(), invalidRva.size())) @@ -256,8 +289,20 @@ int main() { } expectDiagnostic(runner, {"invalid or unsupported PE dependency metadata"}); writeFile(root, {0, 1, 2}); + expectDiagnostic(runner, {"dependency module is not a Windows PE image: " + root.string()}); + std::vector elf{0x7f, 'E', 'L', 'F', 2, 1, 1}; + elf.resize(64); + writeFile(root, elf); + expectDiagnostic(runner, {"dependency module is not a Windows PE image: " + root.string(), "Importer: " + runner.string(), "Chain: " + runner.string() + " -> " + root.string() + "\n"}); + Fs::remove(root); + expectDiagnostic(runner, {"dependency module not found: " + root.string(), "Importer: " + runner.string(), "Chain: " + runner.string() + " -> " + root.string() + "\n"}); + Fs::create_directory(root); expectDiagnostic(runner, {"Windows API failed"}); - Fs::remove_all(directory); + Fs::remove(root); + createImage(root, {{middle.filename().string(), "Middle"}}, {}); + writeFile(middle, elf); + expectDiagnostic(runner, {"dependency module is not a Windows PE image: " + middle.string(), "Importer: " + root.string(), "Chain: " + runner.string() + " -> " + root.string() + " -> " + middle.string() + "\n"}); + removeFixtures(directory); std::cout << "Windows dependency machine-code tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp b/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp index 772189083e..a3813723aa 100644 --- a/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp @@ -54,13 +54,14 @@ void checkChunks() { } for (const auto& value : chunks) image.insert(image.end(), value.begin(), value.end()); image.insert(image.end(), Png.end() - 12, Png.end()); - const std::filesystem::path path = std::filesystem::path(name) / "icon.png"; + const std::filesystem::path path = std::filesystem::path(std::u8string(name.begin(), name.end())) / "icon.png"; + const auto utf8 = path.u8string(); std::vector sections; bool rejected = false; try { builder.Build(image, path, sections, 0x10000); } catch (const Domain::RelinkerException& error) { rejected = true; - require(std::string(error.what()).find(path.string()) != std::string::npos, "Icon failure omitted the path"); + require(std::string(error.what()).find(std::string(utf8.begin(), utf8.end())) != std::string::npos, "Icon failure omitted the path"); } require(rejected != valid, "Unexpected validation result for " + name); require(sections.size() == (valid ? 1u : 0u), "Unexpected section mutation for " + name); @@ -70,6 +71,10 @@ void checkChunks() { const auto note = chunk("tEXt", {'n', 0, 'v'}); check("unknown-critical", {chunk("ABCD", {}), idat}, false); check("ancillary", {note, idat}, true); + check("private-ancillary", {chunk("vpAg", {}), idat}, true); + check("reserved-ancillary", {chunk("tExT", {}), idat}, true); + for (const auto& type : {"t1Xt", "tEX!", "tEX\x80"}) + check("invalid-chunk-type", {chunk(type, {}), idat}, false); const auto first = chunk("IDAT", {data.begin(), data.begin() + 6}); const auto last = chunk("IDAT", {data.begin() + 6, data.end()}); check("consecutive", {first, last}, true); @@ -115,18 +120,59 @@ void run() { require(readU32(data, 174) == Png.size(), "Unexpected icon size in group entry"); require(std::equal(Png.begin(), Png.end(), data.begin() + 184), "Icon bytes changed"); - const auto badPath = std::filesystem::path("bad/icon0.png"); + const std::filesystem::path badPath(u8"\u65e5\u672c/bad/icon0.png"); + const auto badUtf8 = badPath.u8string(); auto truncated = Png; truncated.resize(40); std::vector badSections; + bool caughtTruncated = false; try { builder.Build(truncated, badPath, badSections, nextRva); } catch (const Domain::RelinkerException& e) { - require(std::string(e.what()).find(badPath.string()) != std::string::npos, std::string("Failure does not name the icon: ") + e.what()); + require(std::string(e.what()).find(std::string(badUtf8.begin(), badUtf8.end())) != std::string::npos, std::string("Failure does not name the icon: ") + e.what()); require(badSections.empty(), "Section added despite the failure"); - return; + caughtTruncated = true; } - throw std::runtime_error("Build succeeded for a malformed icon"); + require(caughtTruncated, "Build succeeded for a malformed icon"); + + for (const auto badRva : {0u, 0x8000u, 0x10001u, 0x10800u}) { + std::vector invalidRvaSections; + bool caught = false; + try { + builder.Build(Png, iconPath, invalidRvaSections, badRva); + } catch (const Domain::RelinkerException& error) { + caught = true; + require(std::string(error.what()).find("Invalid Windows resource section RVA") != std::string::npos, "Unexpected error for bad RVA"); + require(invalidRvaSections.empty(), "Section added on bad RVA"); + } + require(caught, "Build succeeded for invalid resource section RVA"); + } + + std::vector duplicateSections{ + {".rsrc", 0x10000, Elfpatcher::Windows::SectionRead | 0x40u, std::vector(0x1000, 0)} + }; + bool caughtDuplicate = false; + try { + builder.Build(Png, iconPath, duplicateSections, 0x20000); + } catch (const Domain::RelinkerException& error) { + caughtDuplicate = true; + require(std::string(error.what()).find("Duplicate Windows resource section") != std::string::npos, "Unexpected error for duplicate resource section"); + require(duplicateSections.size() == 1, "Section added on duplicate resource section"); + } + require(caughtDuplicate, "Build succeeded with duplicate resource section"); + + std::vector overlappingSections{ + {".text", 0x10000, Elfpatcher::Windows::SectionRead | 0x20u, std::vector(0x50000, 0)} + }; + bool caughtOverlap = false; + try { + builder.Build(Png, iconPath, overlappingSections, 0x50000); + } catch (const Domain::RelinkerException& error) { + caughtOverlap = true; + require(std::string(error.what()).find("Windows resource section overlaps preceding section") != std::string::npos, "Unexpected error for overlapping section"); + require(overlappingSections.size() == 1, "Section added on overlapping section"); + } + require(caughtOverlap, "Build succeeded with overlapping resource section"); } } diff --git a/core/relinker/elfpatcher/tests/WindowsImportIdentityTests.cpp b/core/relinker/elfpatcher/tests/WindowsImportIdentityTests.cpp new file mode 100644 index 0000000000..ffe753bfaf --- /dev/null +++ b/core/relinker/elfpatcher/tests/WindowsImportIdentityTests.cpp @@ -0,0 +1,162 @@ +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +namespace Fs = std::filesystem; +using namespace Elfpatcher::Windows; + +constexpr std::uint32_t GotRva = LoadRva + 0x1000; +constexpr std::array LocaleObjects{"-mLzBSk-VGs", "a54t8+k7KpY", "KfcTPbeaOqg"}; + +void require(const bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +Fs::path createFixtureDirectory(const Fs::path& parent) { + std::random_device random; + for (int attempt = 0; attempt < 100; ++attempt) { + auto name = Fs::path("windows-import-identity-"); + name += std::to_string(GetCurrentProcessId()) + "-" + std::to_string(random()); + const auto directory = parent / name; + if (Fs::create_directory(directory)) return directory; + } + throw std::runtime_error("Cannot create a unique import identity fixture directory"); +} + +void appendDisplacement(std::vector& code, const std::int32_t displacement) { + for (int i = 0; i < 4; ++i) code.push_back(static_cast((static_cast(displacement) >> (8 * i)) & 0xffu)); +} + +// For each pair of GOT slots: mov rax,[a]; cmp rax,[b]; jne fail. Then xor eax,eax; ret. fail: mov eax,1; ret. +std::vector compareCode(const std::vector>& pairs) { + std::vector code; + const auto failOffset = static_cast(pairs.size() * 16 + 3); + for (std::size_t i = 0; i < pairs.size(); ++i) { + const auto base = static_cast(i * 16); + code.insert(code.end(), {0x48, 0x8b, 0x05}); + appendDisplacement(code, static_cast(GotRva + 8u * static_cast(pairs[i].first)) - static_cast(LoadRva + base + 7)); + code.insert(code.end(), {0x48, 0x3b, 0x05}); + appendDisplacement(code, static_cast(GotRva + 8u * static_cast(pairs[i].second)) - static_cast(LoadRva + base + 14)); + code.push_back(0x75); + code.push_back(static_cast(failOffset - static_cast(base + 16))); + } + code.insert(code.end(), {0x31, 0xc0, 0xc3}); + code.insert(code.end(), {0xb8, 0x01, 0x00, 0x00, 0x00, 0xc3}); + return code; +} + +std::uint32_t slot(const int index) { + return GotRva + 8u * static_cast(index); +} + +void generateProbe(const Fs::path& file, const std::string& libraries, const std::vector& peImports, const std::vector>& pairs) { + const std::vector loaded{"libSceAmpr.prx", "libkernel.prx", "libc.prx"}; + PeSection entry{".guest", LoadRva, SectionRead | SectionExecute | 0x20u, compareCode(pairs)}; + PeSection got{".got", GotRva, SectionRead | SectionWrite | 0x40u, std::vector(8 * 8)}; + const auto imports = WindowsImportBuilder().Build(LoadRva + 0x2000); + const auto startup = WindowsEntryStubBuilder().Build(LoadRva + 0x3000, entry.Rva, imports, loaded, peImports, libraries, false, false); + std::array directories{}; + directories[1] = imports.Directory; + directories[3] = startup.ExceptionDirectory; + directories[12] = imports.AddressTable; + const auto image = WindowsPeWriter().Write({entry, got, imports.Section, startup.Data, startup.Code}, startup.Code.Rva, directories, false, ImageBase, false); + std::ofstream stream(file, std::ios::binary); + stream.write(reinterpret_cast(image.data()), static_cast(image.size())); + require(static_cast(stream), "Cannot write " + file.string()); +} + +DWORD runProbe(const Fs::path& probe, std::string& output) { + SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; + HANDLE input = nullptr; + HANDLE written = nullptr; + require(CreatePipe(&input, &written, &security, 0) && SetHandleInformation(input, HANDLE_FLAG_INHERIT, 0), "Cannot create the probe's pipe"); + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + startup.dwFlags = STARTF_USESTDHANDLES; + startup.hStdOutput = written; + startup.hStdError = written; + startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + PROCESS_INFORMATION process{}; + const auto filename = probe.wstring(); + require(CreateProcessW(filename.c_str(), nullptr, nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, probe.parent_path().wstring().c_str(), &startup, &process) != 0, + "Cannot start " + probe.filename().string() + ": " + std::to_string(GetLastError())); + CloseHandle(written); + if (WaitForSingleObject(process.hProcess, 120000) != WAIT_OBJECT_0) { + TerminateProcess(process.hProcess, 1); + throw std::runtime_error(probe.filename().string() + " timed out"); + } + DWORD status = 0; + require(GetExitCodeProcess(process.hProcess, &status) != 0, "Cannot read the probe's exit code"); + char buffer[4096]; + DWORD size = 0; + while (ReadFile(input, buffer, sizeof(buffer), &size, nullptr) && size != 0) output.append(buffer, size); + CloseHandle(input); + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + return status; +} + +// The generated probes load libc.prx next to the other libraries; its compiler runtime DLLs sit next to this test. +void copyRuntime(const Fs::path& directory) { + wchar_t self[MAX_PATH]; + const auto length = GetModuleFileNameW(nullptr, self, MAX_PATH); + if (length == 0 || length >= MAX_PATH) return; + const auto own = Fs::path(self).parent_path(); + for (const char* name : {"libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll"}) { + std::error_code error; + if (Fs::exists(own / name, error)) Fs::copy_file(own / name, directory / name, Fs::copy_options::overwrite_existing, error); + } +} + +} + +int main(int argc, char** argv) { + if (argc != 2) { + std::cerr << "usage: windows_import_identity_tests \n"; + return 2; + } + try { + const auto libraries = Fs::absolute(argv[1]).string(); + const auto directory = createFixtureDirectory(Fs::temp_directory_path()); + copyRuntime(directory); + std::vector same; + for (int i = 0; i < 3; ++i) { + same.push_back({.Name = LocaleObjects[i], .TargetRva = slot(2 * i), .Addend = 0, .Library = "libSceAmpr.prx"}); + same.push_back({.Name = LocaleObjects[i], .TargetRva = slot(2 * i + 1), .Addend = 0, .Library = "libc.prx"}); + } + generateProbe(directory / "same.exe", libraries, same, {{0, 1}, {2, 3}, {4, 5}}); + const std::vector different{ + {.Name = LocaleObjects[0], .TargetRva = slot(0), .Addend = 0, .Library = "libSceAmpr.prx"}, + {.Name = "k+AXqu2-eBc", .TargetRva = slot(6), .Addend = 0, .Library = "libkernel.prx"}, + }; + generateProbe(directory / "different.exe", libraries, different, {{0, 6}}); + std::string output; + const auto sameStatus = runProbe(directory / "same.exe", output); + require(sameStatus == 0, "A libSceAmpr.prx-qualified import of the locale objects did not resolve to libc's object (exit " + std::to_string(sameStatus) + ")\n" + output); + output.clear(); + const auto differentStatus = runProbe(directory / "different.exe", output); + require(differentStatus == 1, "The probe did not tell two different addresses apart (exit " + std::to_string(differentStatus) + ")\n" + output); + std::error_code error; + Fs::remove_all(directory, error); + std::cout << "Windows import identity tests passed\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + return 0; +} diff --git a/core/relinker/elfpatcher/tests/WindowsTrampolineBuilderTests.cpp b/core/relinker/elfpatcher/tests/WindowsTrampolineBuilderTests.cpp new file mode 100644 index 0000000000..b710c96f05 --- /dev/null +++ b/core/relinker/elfpatcher/tests/WindowsTrampolineBuilderTests.cpp @@ -0,0 +1,118 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace Elfpatcher::Windows; +using Bytes = std::vector; + +void require(const bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +template +void write(Bytes& bytes, const std::size_t offset, const TValue value) { + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +const Bytes SiteBytes{0xF2, 0x0F, 0x78, 0xDB, 0x08, 0x08}; +const Bytes StubBody{0x90, 0xE9, 0x00, 0x00, 0x00, 0x00}; + +Bytes elfFixture() { + Bytes bytes(0x400); + bytes[0] = 0x7F; + bytes[1] = 'E'; + bytes[2] = 'L'; + bytes[3] = 'F'; + bytes[4] = 2; + bytes[5] = 1; + bytes[6] = 1; + write(bytes, 16, 3); + write(bytes, 18, 62); + write(bytes, 24, 0x1000); + std::fill(bytes.begin() + 0x200, bytes.begin() + 0x300, 0xCC); + std::copy(SiteBytes.begin(), SiteBytes.end(), bytes.begin() + 0x202); + std::copy(SiteBytes.begin(), SiteBytes.end(), bytes.begin() + 0x210); + std::copy(SiteBytes.begin(), SiteBytes.end(), bytes.begin() + 0x302); + return bytes; +} + +std::vector elfHeaders() { + return {{1, 5, 0x200, 0x1000, 0, 0x100, 0x100, 0x1000}, {1, 6, 0x300, 0x2000, 0, 0x100, 0x100, 0x1000}}; +} + +const Bytes Source = elfFixture(); + +Codegen::TrampolineSite site(const std::uint64_t address) { + const auto offset = static_cast(address & 0xFFF) + (address < 0x2000 ? 0x200 : 0x300); + return {static_cast(offset), address, SiteBytes.size(), {Source.begin() + offset, Source.begin() + offset + static_cast(SiteBytes.size())}, StubBody, 1}; +} + +void requireRejected(const WindowsLoadImage& image, const std::vector& sites, std::vector sections, std::uint32_t nextRva, const std::string& expected) { + const auto original = sections; + const auto originalRva = nextRva; + try { + WindowsTrampolineBuilder().Build(sites, image, sections, nextRva); + } catch (const Domain::RelinkerException& error) { + require(std::string(error.what()).find(expected) != std::string::npos, "Unexpected error '" + std::string(error.what()) + "', expected '" + expected + "'"); + require(sections.size() == original.size() && nextRva == originalRva, "Rejected build changed the section list for '" + expected + "'"); + for (std::size_t index = 0; index < sections.size(); ++index) + require(sections[index].Data == original[index].Data, "Rejected build patched section " + sections[index].Name + " for '" + expected + "'"); + return; + } + throw std::runtime_error("Build succeeded, expected '" + expected + "'"); +} + +void run() { + const WindowsLoadImage image(Source, elfHeaders()); + const auto endRva = image.GetEndRva(); + + auto sections = image.BuildSections(); + auto nextRva = endRva; + WindowsTrampolineBuilder().Build({site(0x1002), site(0x1010)}, image, sections, nextRva); + require(sections.size() == 3 && sections.back().Name == ".amdstub" && sections.back().Rva == endRva, "Stub section was not appended"); + require(nextRva == AlignRva(static_cast(endRva) + sections.back().Data.size()), "Next RVA does not follow the stub section"); + require(sections[0].Data[2] == 0xE9 && sections[0].Data[7] == 0x90 && sections[0].Data[0x10] == 0xE9 && sections[0].Data[0x15] == 0x90, "Sites were not replaced by jumps"); + + const auto fresh = image.BuildSections(); + requireRejected(image, {site(0x1002), site(0x1004)}, fresh, endRva, "Overlapping AMD-only trampoline sites"); + requireRejected(image, {site(0x1004), site(0x1002)}, fresh, endRva, "Overlapping AMD-only trampoline sites"); + requireRejected(image, {site(0x1002), site(0x1002)}, fresh, endRva, "Overlapping AMD-only trampoline sites"); + requireRejected(image, {site(0x2002)}, fresh, endRva, "AMD-only instruction is in a non-executable PE section"); + + auto changed = site(0x1010); + changed.OriginalBytes[3] = 0xDC; + requireRejected(image, {site(0x1002), changed}, fresh, endRva, "AMD-only site bytes changed before patching"); + + requireRejected(image, {site(0x1002)}, fresh, endRva + 0x10, "Invalid Windows trampoline section RVA"); + requireRejected(image, {site(0x1002)}, fresh, LoadRva - SectionAlignment, "Invalid Windows trampoline section RVA"); + requireRejected(image, {site(0x1002)}, fresh, endRva - SectionAlignment, "Windows trampoline section overlaps preceding section"); + requireRejected(image, {site(0x1002)}, sections, nextRva, "Duplicate Windows trampoline section"); + + auto untouched = image.BuildSections(); + auto untouchedRva = endRva; + WindowsTrampolineBuilder().Build({}, image, untouched, untouchedRva); + require(untouched.size() == fresh.size() && untouchedRva == endRva, "Empty site list changed the image"); +} + +} + +int main() { + try { + run(); + } catch (const std::exception& e) { + std::cerr << e.what() << '\n'; + return 1; + } + return 0; +} diff --git a/core/relinker/io/include/io/FileIdentity.hpp b/core/relinker/io/include/io/FileIdentity.hpp new file mode 100644 index 0000000000..e33cfc5337 --- /dev/null +++ b/core/relinker/io/include/io/FileIdentity.hpp @@ -0,0 +1,12 @@ +#ifndef IO_FILEIDENTITY_HPP +#define IO_FILEIDENTITY_HPP + +#include + +namespace Io { + +bool SameFile(const std::filesystem::path& first, const std::filesystem::path& second); + +} + +#endif diff --git a/core/relinker/io/include/io/FileReader.hpp b/core/relinker/io/include/io/FileReader.hpp index bf5d2fa85a..1056ab33a1 100644 --- a/core/relinker/io/include/io/FileReader.hpp +++ b/core/relinker/io/include/io/FileReader.hpp @@ -2,11 +2,13 @@ #define IO_FILEREADER_HPP #include +#include namespace Io { class FileReader : public IFileReader { public: + std::vector ReadPrefix(const std::string& path, std::size_t size) const; std::vector Read(const std::string& path) override; }; diff --git a/core/relinker/io/src/FileIdentity.cpp b/core/relinker/io/src/FileIdentity.cpp new file mode 100644 index 0000000000..351058935a --- /dev/null +++ b/core/relinker/io/src/FileIdentity.cpp @@ -0,0 +1,54 @@ +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#include +#include +#include +#endif + +namespace Io { + +#ifdef _WIN32 +namespace { + +using Identity = std::pair>; + +Identity FileIdentity(const std::filesystem::path& path) { + const auto handle = CreateFileW(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) + throw std::filesystem::filesystem_error("Cannot open file identity", path, std::error_code(static_cast(GetLastError()), std::system_category())); + Identity identity{}; + FILE_ID_INFO idInformation{}; + BY_HANDLE_FILE_INFORMATION information{}; + bool read = GetFileInformationByHandleEx(handle, FileIdInfo, &idInformation, sizeof(idInformation)) != 0; + if (read) { + identity.first = idInformation.VolumeSerialNumber; + std::copy(std::begin(idInformation.FileId.Identifier), std::end(idInformation.FileId.Identifier), identity.second.begin()); + } else { + read = GetFileInformationByHandle(handle, &information) != 0; + identity.first = information.dwVolumeSerialNumber; + const std::uint64_t index = (static_cast(information.nFileIndexHigh) << 32) | information.nFileIndexLow; + std::memcpy(identity.second.data(), &index, sizeof(index)); + } + const auto error = GetLastError(); + CloseHandle(handle); + if (!read) throw std::filesystem::filesystem_error("Cannot read file identity", path, std::error_code(static_cast(error), std::system_category())); + return identity; +} + +} +#endif + +bool SameFile(const std::filesystem::path& first, const std::filesystem::path& second) { +#ifdef _WIN32 + return FileIdentity(first) == FileIdentity(second); +#else + return std::filesystem::equivalent(first, second); +#endif +} + +} diff --git a/core/relinker/io/src/FileReader.cpp b/core/relinker/io/src/FileReader.cpp index 228903ab48..4f012529d3 100644 --- a/core/relinker/io/src/FileReader.cpp +++ b/core/relinker/io/src/FileReader.cpp @@ -3,9 +3,29 @@ #include #include #include +#include namespace Io { +std::vector FileReader::ReadPrefix(const std::string& path, const std::size_t size) const { + if (size > static_cast(std::numeric_limits::max())) + throw Domain::RelinkerException("File prefix is too large: " + path); + const auto native = NativePath(path); + std::error_code error; + if (!std::filesystem::is_regular_file(native, error)) + throw Domain::RelinkerException("Cannot open file: " + path); + std::ifstream f(native, std::ios::binary); + if (!f) + throw Domain::RelinkerException("Cannot open file: " + path); + std::vector buf(size); + f.read(reinterpret_cast(buf.data()), static_cast(size)); + const auto count = f.gcount(); + if (f.bad()) + throw Domain::RelinkerException("Cannot read file: " + path); + buf.resize(static_cast(count)); + return buf; +} + std::vector FileReader::Read(const std::string& path) { const auto native = NativePath(path); std::error_code error; diff --git a/core/relinker/io/src/FileWriter.cpp b/core/relinker/io/src/FileWriter.cpp index e80448f503..49df133411 100644 --- a/core/relinker/io/src/FileWriter.cpp +++ b/core/relinker/io/src/FileWriter.cpp @@ -1,28 +1,110 @@ #include #include #include +#include #include +#include +#ifndef _WIN32 +#include +#include +#include +#include +#include +#include +#endif namespace Io { -void FileWriter::Write(const std::string& path, const std::vector& data) { - std::ofstream f(NativePath(path), std::ios::binary); +namespace { + +bool writeFile(const std::filesystem::path& file, const std::string& path, const std::ios::openmode mode, const char* data, const std::size_t size) { + std::ofstream f(file, mode); if (!f) - throw Domain::RelinkerException("Cannot open output file: " + path); - f.write(reinterpret_cast(data.data()), static_cast(data.size())); + return false; + f.write(data, static_cast(size)); f.close(); if (!f) throw Domain::RelinkerException("Failed to write file: " + path); + return true; +} + +#ifndef _WIN32 +int createTemporary(const std::filesystem::path& target, const mode_t mode, std::filesystem::path& temporary) { + constexpr std::string_view characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + std::random_device device; + for (int attempt = 0; attempt < 100; ++attempt) { + std::string suffix = "."; + for (int index = 0; index < 6; ++index) + suffix += characters[device() % characters.size()]; + temporary = target; + temporary += suffix; + const int descriptor = open(temporary.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode); + if (descriptor >= 0 || errno != EEXIST) + return descriptor; + } + return -1; +} + +bool writeAll(const int descriptor, const char* data, std::size_t size) { + while (size != 0) { + const auto written = write(descriptor, data, size); + if (written < 0 && errno == EINTR) + continue; + if (written <= 0) + return false; + data += written; + size -= static_cast(written); + } + return true; +} + +bool replaceFile(const std::filesystem::path& target, const std::string& path, const char* data, const std::size_t size) { + std::error_code error; + const auto previous = std::filesystem::symlink_status(target, error); + const bool regular = std::filesystem::is_regular_file(previous); + if (!regular && previous.type() != std::filesystem::file_type::not_found) + return false; + const auto mode = regular ? static_cast(previous.permissions() & std::filesystem::perms::all) : static_cast(0666); + std::filesystem::path temporary; + const int descriptor = createTemporary(target, mode, temporary); + if (descriptor < 0) + return false; + if (regular && fchmod(descriptor, mode) != 0) { + close(descriptor); + std::filesystem::remove(temporary, error); + return false; + } + const bool written = writeAll(descriptor, data, size); + if (close(descriptor) != 0 || !written) { + std::filesystem::remove(temporary, error); + throw Domain::RelinkerException("Failed to write file: " + path); + } + std::filesystem::rename(temporary, target, error); + if (!error) + return true; + std::filesystem::remove(temporary, error); + return false; +} +#endif + +void writeOutput(const std::string& path, const std::ios::openmode mode, const char* data, const std::size_t size) { + const auto target = NativePath(path); +#ifndef _WIN32 + if (replaceFile(target, path, data, size)) + return; +#endif + if (!writeFile(target, path, mode, data, size)) + throw Domain::RelinkerException("Cannot open output file: " + path); +} + +} + +void FileWriter::Write(const std::string& path, const std::vector& data) { + writeOutput(path, std::ios::binary, reinterpret_cast(data.data()), data.size()); } void FileWriter::Write(const std::string& path, const std::string& content) { - std::ofstream f(NativePath(path)); - if (!f) - throw Domain::RelinkerException("Cannot open output file: " + path); - f << content; - f.close(); - if (!f) - throw Domain::RelinkerException("Failed to write file: " + path); + writeOutput(path, std::ios::out, content.data(), content.size()); } } diff --git a/core/relinker/io/tests/FileIdentityTests.cpp b/core/relinker/io/tests/FileIdentityTests.cpp new file mode 100644 index 0000000000..41e6bdc5e7 --- /dev/null +++ b/core/relinker/io/tests/FileIdentityTests.cpp @@ -0,0 +1,64 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void write(const std::filesystem::path& path, const std::string& text) { + std::ofstream(path, std::ios::binary) << text; + require(std::filesystem::is_regular_file(path), "Cannot create " + path.string()); +} + +void requireMissing(const std::filesystem::path& first, const std::filesystem::path& second) { + try { + Io::SameFile(first, second); + } catch (const std::filesystem::filesystem_error&) { + return; + } + throw std::runtime_error("SameFile accepted a missing file"); +} + +void run(const std::filesystem::path& directory) { + const auto first = directory / "first.bin"; + const auto link = directory / "link.bin"; + const auto copy = directory / "copy.bin"; + const auto folder = directory / "folder"; + write(first, "same"); + write(copy, "same"); + std::filesystem::create_hard_link(first, link); + std::filesystem::create_directory(folder); + require(Io::SameFile(first, first), "A file differs from itself"); + require(Io::SameFile(first, folder / ".." / "first.bin"), "Two spellings of one file differ"); + require(Io::SameFile(first, link), "A hard link differs from its target"); + require(!Io::SameFile(first, copy), "A copy with the same contents is the same file"); + require(Io::SameFile(folder, directory / "folder" / "."), "Two spellings of one directory differ"); + require(!Io::SameFile(first, folder), "A file is the same as a directory"); + requireMissing(directory / "missing.bin", first); + requireMissing(first, directory / "missing.bin"); +} + +} + +int main() { + const auto directory = std::filesystem::temp_directory_path() / + ("anyps5-file-identity-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + try { + std::filesystem::create_directories(directory); + run(directory); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + std::error_code ignored; + std::filesystem::remove_all(directory, ignored); + return 1; + } + std::filesystem::remove_all(directory); + return 0; +} diff --git a/core/relinker/io/tests/FileWriterTests.cpp b/core/relinker/io/tests/FileWriterTests.cpp index 8edc710d55..8ec3b79abd 100644 --- a/core/relinker/io/tests/FileWriterTests.cpp +++ b/core/relinker/io/tests/FileWriterTests.cpp @@ -1,9 +1,19 @@ #include #include +#include +#include +#include #include +#include #include #include +#include #include +#include +#include +#include +#include +#include namespace { @@ -21,19 +31,163 @@ void requireWriteFailure(const TValue& data) { throw std::runtime_error("Write to /dev/full reported success"); } +std::string readFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +std::vector siblings(const std::filesystem::path& output) { + std::vector names; + for (const auto& entry : std::filesystem::directory_iterator(output.parent_path())) { + const auto name = entry.path().filename().string(); + if (name.starts_with(output.filename().string() + ".")) + names.push_back(name); + } + return names; +} + +template +void requireRunningReplacement(const std::filesystem::path& directory, const TValue& data, const bool leftover) { + const auto output = directory / (leftover ? "leftover" : "running"); + if (leftover) + std::ofstream(directory / "leftover.tmp", std::ios::binary) << "killed"; + 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); + const pid_t child = fork(); + if (child < 0) + throw std::runtime_error("Cannot start the previous executable"); + if (child == 0) { + execl(output.c_str(), output.c_str(), "wait", nullptr); + _exit(127); + } + std::error_code error; + bool started = false; + for (int attempt = 0; attempt < 2000 && !started; ++attempt) { + started = std::filesystem::read_symlink("/proc/" + std::to_string(child) + "/exe", error) == output; + if (!started) + usleep(1000); + } + const int descriptor = open(output.c_str(), O_RDONLY); + std::string failure; + if (started) { + const auto mask = umask(077); + try { + Io::FileWriter().Write(output.string(), data); + } catch (const std::exception& e) { + failure = e.what(); + } + umask(mask); + } + std::string kept(previous.size(), '\0'); + const bool keptPrevious = pread(descriptor, kept.data(), kept.size(), 0) == static_cast(kept.size()) && kept == previous; + close(descriptor); + const bool running = waitpid(child, nullptr, WNOHANG) == 0; + if (running) { + kill(child, SIGKILL); + waitpid(child, nullptr, 0); + } + if (!started) + throw std::runtime_error("The previous executable did not start"); + if (!failure.empty()) + throw std::runtime_error("Replacing a running executable failed: " + failure); + if (readFile(output) != std::string(data.begin(), data.end())) + throw std::runtime_error("Running executable was not replaced"); + if ((std::filesystem::status(output).permissions() & std::filesystem::perms::all) != permissions) + throw std::runtime_error("Replacement did not keep the previous permissions"); + if (!keptPrevious) + throw std::runtime_error("An open descriptor no longer reads the previous file"); + if (!running) + throw std::runtime_error("Running executable stopped"); + if (siblings(output) != (leftover ? std::vector{"leftover.tmp"} : std::vector{})) + throw std::runtime_error("Temporary file left behind"); + if (leftover && readFile(directory / "leftover.tmp") != "killed") + throw std::runtime_error("A file left by an earlier run was changed"); +} + +template +void requireFailedReplacement(const std::filesystem::path& directory, const TValue& data) { + const auto output = directory / "limited"; + std::ofstream(output, std::ios::binary) << "previous"; + rlimit limit{}; + getrlimit(RLIMIT_FSIZE, &limit); + rlimit lowered = limit; + lowered.rlim_cur = 1; + const auto handler = std::signal(SIGXFSZ, SIG_IGN); + setrlimit(RLIMIT_FSIZE, &lowered); + std::string failure; + try { + Io::FileWriter().Write(output.string(), data); + } catch (const Domain::RelinkerException& e) { + failure = e.what(); + } + setrlimit(RLIMIT_FSIZE, &limit); + std::signal(SIGXFSZ, handler); + if (failure != "Failed to write file: " + output.string()) + throw std::runtime_error("Write past the file size limit did not fail: " + failure); + if (readFile(output) != "previous" || !siblings(output).empty()) + throw std::runtime_error("Failed write did not keep the previous file"); +} + +template +void requireInPlaceWrites(const std::filesystem::path& directory, const TValue& data) { + const std::string expected(data.begin(), data.end()); + const auto target = directory / "target"; + const auto link = directory / "link"; + std::ofstream(target, std::ios::binary) << "previous"; + std::filesystem::create_symlink(target, link); + Io::FileWriter().Write(link.string(), data); + if (!std::filesystem::is_symlink(link) || readFile(target) != expected) + throw std::runtime_error("Symbolic link output was not written through"); + const auto reserved = directory / "reserved"; + std::ofstream(reserved, std::ios::binary) << "previous"; + std::ofstream(directory / "reserved.tmp", std::ios::binary) << "unrelated"; + Io::FileWriter().Write(reserved.string(), data); + if (readFile(reserved) != expected || readFile(directory / "reserved.tmp") != "unrelated") + throw std::runtime_error("Existing temporary file name was not left alone"); +} + +template +void requireReplacement(const TValue& data) { + const auto directory = std::filesystem::canonical(std::filesystem::temp_directory_path()) / ("anyps5-file-writer-" + std::to_string(getpid())); + std::error_code error; + std::filesystem::remove_all(directory, error); + std::filesystem::create_directory(directory); + try { + requireRunningReplacement(directory, data, false); + requireRunningReplacement(directory, data, true); + requireFailedReplacement(directory, data); + requireInPlaceWrites(directory, data); + } catch (...) { + std::filesystem::remove_all(directory, error); + throw; + } + std::filesystem::remove_all(directory, error); +} + } 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") - requireWriteFailure(std::vector(size, 0x41)); - else if (mode == "text") - requireWriteFailure(std::string(size, 'A')); - else + if (mode == "binary") { + const std::vector data(size, 0x41); + requireWriteFailure(data); + requireReplacement(data); + } else if (mode == "text") { + const std::string data(size, 'A'); + requireWriteFailure(data); + requireReplacement(data); + } else { throw std::runtime_error("Unknown test mode: " + mode); + } } } catch (const std::exception& e) { std::cerr << e.what() << '\n'; diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 991107a0a7..9b3f177b6a 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -6,14 +6,17 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include #include +#include #include #include @@ -31,16 +34,19 @@ struct ConvertedExecutable { std::vector GuestArtifacts; std::vector Bytes; std::string Diagnostics; + std::vector AppliedPatches; + std::optional LinuxBase; }; -ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, const std::string& absPath, const bool mainExecutable, Cli::ConsoleProgress& progress) { +ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, const std::string& absPath, const bool mainExecutable, const std::string& targetName, const std::vector& patches, Cli::ConsoleProgress& progress) { Io::FileReader fileReader; Relinker::ConversionInput input; + Relinker::ElfReader(fileReader.ReadPrefix(inputPath, 0x40)).ValidateMagic(); input.Executable = fileReader.Read(inputPath); input.InputPath = std::filesystem::absolute(Io::NativePath(inputPath)).lexically_normal(); input.OutputPath = Io::NativePath(absPath); if (!args.skipSceModule) { - auto modules = Cli::ReadModules(input.InputPath, Io::NativePath(args.sceModulePath), args.excludedSceModules, args.moduleDirectories); + auto modules = Cli::ReadModules(std::filesystem::absolute(Io::NativePath(args.inputPath)).lexically_normal(), Io::NativePath(args.sceModulePath), args.excludedSceModules, args.moduleDirectories); input.ModuleRoot = std::move(modules.Root); input.Modules = std::move(modules.Modules); input.RuntimeDirectories = std::move(modules.RuntimeDirectories); @@ -62,12 +68,183 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, options.WindowsFixedBase = mainExecutable ? args.windowsFixedBase : std::nullopt; options.WindowsDiagnostics = args.windowsDiagnostics && mainExecutable; options.LinuxFixedBase = args.linuxFixedBase; + options.MacosFixedBase = args.macosFixedBase; options.ExcludedModules = args.excludedSceModules; + options.Patches = patches; + options.TargetName = targetName; progress.IncludeFile(Io::Utf8Path(input.InputPath)); progress.IncludeFile(Io::Utf8Path(input.OutputPath)); for (const auto& module : input.Modules) progress.IncludeFile(Io::Utf8Path(module.Path)); auto converted = Relinker::Conversion(progress).Convert(std::move(input), options); - return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics)}; + return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics), std::move(converted.AppliedPatches), converted.LinuxBase}; +} + +class OutputIdentityDirectory { +public: + OutputIdentityDirectory(const OutputIdentityDirectory&) = delete; + OutputIdentityDirectory& operator=(const OutputIdentityDirectory&) = delete; + + explicit OutputIdentityDirectory(const std::filesystem::path& parent) { + std::random_device random; + for (unsigned attempt = 0; attempt < 64; ++attempt) { + const auto candidate = parent / (".anyps5-output-identity-" + std::to_string(random()) + "-" + std::to_string(random())); + std::error_code error; + if (std::filesystem::create_directory(candidate, error)) { + directory = candidate; + return; + } + if (error && error != std::errc::file_exists) + throw std::filesystem::filesystem_error("Cannot create output identity directory", candidate, error); + } + throw Domain::RelinkerException("Cannot reserve an output identity directory in " + Io::Utf8Path(parent)); + } + + ~OutputIdentityDirectory() { + if (directory.empty()) return; + try { + Remove(); + } catch (const std::exception& error) { + std::cerr << "FAIL: Output identity cleanup: " << error.what() << '\n'; + } + } + + const std::filesystem::path& GetDirectory() const { return directory; } + + void Remove() { + std::filesystem::remove(directory); + directory.clear(); + } + +private: + std::filesystem::path directory; +}; + +bool SameOutputFile(const std::filesystem::path& first, const std::filesystem::path& second) { + return Io::SameFile(first, second); +} + +bool SameOutputDestination(const std::filesystem::path& first, const std::filesystem::path& second) { + const auto firstPath = std::filesystem::weakly_canonical(first); + const auto secondPath = std::filesystem::weakly_canonical(second); + if (firstPath == secondPath) return true; + if (std::filesystem::exists(first) && std::filesystem::exists(second)) return SameOutputFile(first, second); +#ifdef _WIN32 + const auto firstName = firstPath.filename().native(); + const auto secondName = secondPath.filename().native(); + const auto comparison = CompareStringOrdinal(firstName.c_str(), -1, secondName.c_str(), -1, TRUE); + if (comparison == 0) throw Domain::RelinkerException("Cannot compare output filenames"); + if (comparison == CSTR_EQUAL && std::filesystem::exists(firstPath.parent_path()) && + std::filesystem::exists(secondPath.parent_path()) && std::filesystem::equivalent(firstPath.parent_path(), secondPath.parent_path())) { + OutputIdentityDirectory probe(firstPath.parent_path()); + auto alternateName = Io::Utf8Path(probe.GetDirectory().filename()); + for (auto& character : alternateName) + if (character >= 'a' && character <= 'z') character = static_cast(character - ('a' - 'A')); + const auto alternate = firstPath.parent_path() / Io::NativePath(alternateName); + const bool same = std::filesystem::exists(alternate) && std::filesystem::equivalent(probe.GetDirectory(), alternate); + probe.Remove(); + return same; + } +#else + if (std::filesystem::exists(firstPath.parent_path()) && std::filesystem::exists(secondPath.parent_path()) && + std::filesystem::equivalent(firstPath.parent_path(), secondPath.parent_path())) { + OutputIdentityDirectory probe(firstPath.parent_path()); + const auto name = probe.GetDirectory() / firstPath.filename(); + const auto alternate = probe.GetDirectory() / secondPath.filename(); + std::filesystem::create_directory(name); + const bool same = std::filesystem::exists(alternate) && std::filesystem::equivalent(name, alternate); + std::filesystem::remove(name); + probe.Remove(); + return same; + } +#endif + return false; +} + +std::string ReadText(const std::string& path) { + const auto bytes = Io::FileReader().Read(path); + return std::string(bytes.begin(), bytes.end()); +} + +std::filesystem::path ExecutableRelative(const std::string& executable) { + const auto relative = Io::NativePath(executable).lexically_normal(); + if (relative.empty() || relative.has_root_path() || *relative.begin() == ".." || !relative.has_filename()) + throw Domain::RelinkerException("--executable must be a file inside the guest module parent directory: " + executable); + return relative; +} + +std::vector ModuleDirectories(const Cli::Args& args, const std::filesystem::path& moduleRoot) { + std::vector moduleDirectories; + for (const auto* name : {"sce_module", "sce_modules", "prx"}) moduleDirectories.push_back(moduleRoot / name); + for (const auto& name : args.moduleDirectories) moduleDirectories.push_back((moduleRoot / Io::NativePath(name)).lexically_normal()); + for (auto& directory : moduleDirectories) if (!directory.has_filename()) directory = directory.parent_path(); + return moduleDirectories; +} + +std::filesystem::path ResolveExtraExecutable(const std::filesystem::path& moduleRoot, const std::vector& moduleDirectories, const std::filesystem::path& relative, const std::string& 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::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); + return input; +} + +std::vector LoadPatches(const Cli::Args& args) { + if (args.noPatches) return {}; + std::filesystem::path path; + if (!args.patchesPath.empty()) { + path = Io::NativePath(args.patchesPath); + if (!std::filesystem::is_regular_file(path)) throw Domain::RelinkerException("Patch file not found: " + args.patchesPath); + } else { + path = Io::NativePath(args.inputPath).parent_path() / Relinker::GamePatch::PatchFileName; + if (!std::filesystem::is_regular_file(path)) return {}; + } + std::cout << "Patches: " << Io::Utf8Path(path) << '\n'; + return Relinker::GamePatch::ParsePatchFile(ReadText(Io::Utf8Path(path))).Entries; +} + +int RunVerifyPatches(const Cli::Args& args) { + using namespace Relinker::GamePatch; + const auto file = ParsePatchFile(ReadText(args.verifyPatchesPath)); + Io::FileReader reader; + std::vector executables; + std::vector modules; + const auto inputPath = Io::NativePath(args.inputPath); + if (!std::filesystem::is_regular_file(inputPath)) + throw Domain::RelinkerException((args.verifyOutput ? "output not found: " : "input not found: ") + args.inputPath); + if (args.verifyOutput) { + executables.push_back({file.MainTarget.value_or(std::string()), reader.Read(args.inputPath)}); + for (const auto& executable : args.executables) { + const auto relative = ExecutableRelative(executable); + const auto output = OutputExecutablePath(inputPath, executable); + if (!std::filesystem::is_regular_file(output)) throw Domain::RelinkerException("output not found: " + Io::Utf8Path(output)); + executables.push_back({relative.generic_string(), reader.Read(Io::Utf8Path(output))}); + } + } else { + executables.push_back({Io::Utf8Path(inputPath.filename()), reader.Read(args.inputPath)}); + std::filesystem::path moduleRoot = args.executables.empty() ? std::filesystem::path() : std::filesystem::absolute(Io::NativePath(args.sceModulePath)).lexically_normal(); + if (!args.skipSceModule) { + auto native = Cli::ReadModules(std::filesystem::absolute(inputPath).lexically_normal(), Io::NativePath(args.sceModulePath), args.excludedSceModules, args.moduleDirectories); + for (auto& module : native.Modules) modules.push_back({ModuleTargetName(module.Path, native.Root), std::move(module.Bytes)}); + } + const auto moduleDirectories = ModuleDirectories(args, moduleRoot); + for (const auto& executable : args.executables) { + const auto relative = ExecutableRelative(executable); + const auto input = ResolveExtraExecutable(moduleRoot, moduleDirectories, relative, executable); + executables.push_back({relative.generic_string(), reader.Read(Io::Utf8Path(input))}); + } + } + const auto results = VerifyPatches(file, executables, modules, args.verifyOutput); + std::size_t mismatches = 0; + std::size_t notChecked = 0; + for (const auto& result : results) { + std::cout << StatusName(result.Status) << ": " << (result.Name.empty() ? std::string() : result.Name + " ") << result.Target << " va=0x" << std::hex << result.VirtualAddress; + if (result.Status != VerifyStatus::NotChecked) std::cout << " off=0x" << result.Offset; + std::cout << std::dec << ' ' << (result.Status == VerifyStatus::NotChecked ? "(" + result.Detail + ")" : result.Detail) << '\n'; + if (result.Status == VerifyStatus::Mismatch) ++mismatches; + if (result.Status == VerifyStatus::NotChecked) ++notChecked; + } + std::cout << "Verified " << results.size() << " patches: " << results.size() - mismatches - notChecked << " ok, " << notChecked << " not-checked, " << mismatches << " mismatch\n"; + return mismatches == 0 ? 0 : 1; } int Run(const int argc, char* argv[]) { @@ -86,12 +263,13 @@ int Run(const int argc, char* argv[]) { Cli::ConsoleProgress progress(std::cout); try { + if (!args.verifyPatchesPath.empty()) return RunVerifyPatches(args); auto extension = Io::Utf8Path(Io::NativePath(args.outputPath).extension()); for (auto& character : extension) if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); if (!args.toWindows && !args.toMacos && extension == ".exe") std::cerr << "WARNING: Output filename ends with .exe, but --windows was not specified. The output will be a Linux ELF executable.\n"; Io::FileWriter fileWriter; - if (std::filesystem::exists(Io::NativePath(args.inputPath)) && std::filesystem::exists(Io::NativePath(args.outputPath)) && std::filesystem::equivalent(Io::NativePath(args.inputPath), Io::NativePath(args.outputPath))) + if (std::filesystem::exists(Io::NativePath(args.inputPath)) && std::filesystem::exists(Io::NativePath(args.outputPath)) && SameOutputFile(Io::NativePath(args.inputPath), Io::NativePath(args.outputPath))) throw Domain::RelinkerException("Executable output would overwrite the input executable"); if (Io::NativePath(args.outputPath).filename().empty() || std::filesystem::is_directory(Io::NativePath(args.outputPath))) throw Domain::RelinkerException("Executable output is a directory: " + args.outputPath); @@ -106,30 +284,22 @@ int Run(const int argc, char* argv[]) { std::vector extraExecutables; const auto outputDirectory = Io::NativePath(absPath).parent_path(); const auto moduleRoot = args.executables.empty() ? std::filesystem::path() : std::filesystem::absolute(Io::NativePath(args.sceModulePath)).lexically_normal(); - std::vector moduleDirectories; - for (const auto* name : {"sce_module", "sce_modules", "prx"}) moduleDirectories.push_back(moduleRoot / name); - for (const auto& name : args.moduleDirectories) moduleDirectories.push_back((moduleRoot / Io::NativePath(name)).lexically_normal()); - for (auto& directory : moduleDirectories) if (!directory.has_filename()) directory = directory.parent_path(); + const auto moduleDirectories = ModuleDirectories(args, moduleRoot); for (const auto& executable : args.executables) { - const auto relative = Io::NativePath(executable).lexically_normal(); - if (relative.empty() || relative.has_root_path() || *relative.begin() == ".." || !relative.has_filename()) - throw Domain::RelinkerException("--executable must be a file inside the guest module parent directory: " + executable); - const auto input = moduleRoot / relative; - if (!std::filesystem::is_regular_file(input)) throw Domain::RelinkerException("--executable is not a file: " + Io::Utf8Path(input)); - if (std::find(moduleDirectories.begin(), moduleDirectories.end(), input.parent_path()) != moduleDirectories.end()) - throw Domain::RelinkerException("--executable is in a guest module directory: " + executable); - std::string name; - for (const auto& part : relative) name += (name.empty() ? "" : ".") + Io::Utf8Path(part); - const auto output = outputDirectory / Io::NativePath(name + Io::Utf8Path(Io::NativePath(absPath).extension())); - if (std::filesystem::equivalent(Io::NativePath(args.inputPath), input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); + const auto relative = ExecutableRelative(executable); + const auto input = ResolveExtraExecutable(moduleRoot, moduleDirectories, relative, executable); + const auto output = Relinker::GamePatch::OutputExecutablePath(Io::NativePath(absPath), executable); + if (Io::SameFile(Io::NativePath(args.inputPath), input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); if (output.lexically_normal() == Io::NativePath(absPath).lexically_normal()) throw Domain::RelinkerException("Executable outputs collide: " + Io::Utf8Path(output)); for (const auto& extra : extraExecutables) { - if (std::filesystem::equivalent(extra.Input, input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); + if (Io::SameFile(extra.Input, input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); if (extra.Output.lexically_normal() == output.lexically_normal()) throw Domain::RelinkerException("Executable outputs collide: " + Io::Utf8Path(output)); } extraExecutables.push_back({executable, relative, input, output}); } + const auto patches = LoadPatches(args); + const std::string mainTarget = Io::Utf8Path(Io::NativePath(args.inputPath).filename()); progress.IncludeFile(args.inputPath); progress.IncludeFile(absPath); for (const auto& extra : extraExecutables) { @@ -137,10 +307,10 @@ int Run(const int argc, char* argv[]) { progress.IncludeFile(Io::Utf8Path(extra.Output)); } std::vector executables; - executables.push_back(Convert(args, args.inputPath, absPath, true, progress)); + executables.push_back(Convert(args, args.inputPath, absPath, true, mainTarget, patches, progress)); for (const auto& [executable, relative, input, output] : extraExecutables) { std::cout << "Executable: " << relative.generic_string() << '\n'; - auto converted = Convert(args, Io::Utf8Path(input), Io::Utf8Path(output), false, progress); + auto converted = Convert(args, Io::Utf8Path(input), Io::Utf8Path(output), false, relative.generic_string(), patches, progress); const auto& mainArtifacts = executables.front().GuestArtifacts; const bool shared = converted.GuestArtifacts.size() == mainArtifacts.size() && std::equal(mainArtifacts.begin(), mainArtifacts.end(), converted.GuestArtifacts.begin(), [](const auto& left, const auto& right) { return left.Path == right.Path && left.Bytes == right.Bytes; @@ -149,6 +319,23 @@ int Run(const int argc, char* argv[]) { executables.push_back(std::move(converted)); } const auto& guestArtifacts = executables.front().GuestArtifacts; + if (!patches.empty()) { + std::vector targetNames{mainTarget}; + for (const auto& extra : extraExecutables) targetNames.push_back(extra.Relative.generic_string()); + if (!guestArtifacts.empty()) { + const auto patchModuleRoot = std::filesystem::absolute(Io::NativePath(args.sceModulePath)).lexically_normal(); + for (const auto& artifact : guestArtifacts) targetNames.push_back(Relinker::GamePatch::ModuleTargetName(artifact.SourcePath, patchModuleRoot)); + } + Relinker::GamePatch::MatchTargets(patches, targetNames); + } + std::vector> appliedRecords; + for (const auto& executable : executables) appliedRecords.push_back(executable.AppliedPatches); + const auto appliedPatches = Relinker::GamePatch::AggregateManifest(appliedRecords); + const auto patchFile = args.patchesPath.empty() ? Io::NativePath(args.inputPath).parent_path() / Relinker::GamePatch::PatchFileName : Io::NativePath(args.patchesPath); + std::filesystem::path manifestPath; + std::string manifestText; + if (!appliedPatches.empty()) manifestPath = Io::NativePath(absPath).parent_path() / Io::NativePath(Io::Utf8Path(Io::NativePath(absPath).stem()) + Relinker::GamePatch::ManifestSuffix); + if (!manifestPath.empty()) manifestText = Relinker::GamePatch::WriteManifest(appliedPatches, mainTarget, Io::Utf8Path(patchFile), executables.front().LinuxBase); const Elfpatcher::Linux::LinuxDesktopEntryWriter desktopEntryWriter; const auto desktopEntry = args.toWindows || args.toMacos ? std::nullopt : desktopEntryWriter.Prepare(Io::NativePath(args.inputPath).parent_path() / "sce_sys", Io::NativePath(absPath)); @@ -156,10 +343,10 @@ int Run(const int argc, char* argv[]) { const auto checkOutputInputs = [&](const std::filesystem::path& path, const char* kind) { if (!std::filesystem::exists(path)) return; for (const auto& executable : executables) - if (std::filesystem::equivalent(Io::NativePath(executable.InputPath), path)) + if (SameOutputFile(Io::NativePath(executable.InputPath), path)) throw Domain::RelinkerException(std::string(kind) + " output would overwrite the input executable: " + Io::Utf8Path(path)); for (const auto& artifact : guestArtifacts) - if (std::filesystem::equivalent(artifact.SourcePath, path)) + if (SameOutputFile(artifact.SourcePath, path)) throw Domain::RelinkerException(std::string(kind) + " output would overwrite an input module: " + Io::Utf8Path(path)); }; for (std::size_t index = 1; index < executables.size(); ++index) checkOutputInputs(Io::NativePath(executables[index].OutputPath), "Executable"); @@ -171,6 +358,7 @@ int Run(const int argc, char* argv[]) { checkOutputInputs(std::filesystem::absolute("windows-diagnostics-imports.txt"), "Windows import diagnostics"); std::vector registryPaths; + std::vector neededPaths; if (args.writeRegistry) { for (const auto& executable : executables) { const std::filesystem::path outFsPath = Io::NativePath(executable.OutputPath); @@ -180,11 +368,42 @@ int Run(const int argc, char* argv[]) { for (const auto& artifact : guestArtifacts) registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + "." + Io::Utf8Path(artifact.Path.filename()) + ".registry.json")); for (const auto& path : registryPaths) checkOutputInputs(path, "Registry"); + for (const auto& path : registryPaths) { + auto name = Io::Utf8Path(path.filename()); + name.resize(name.size() - std::string(".registry.json").size()); + neededPaths.push_back(path.parent_path() / Io::NativePath(name + ".needed.json")); + } + for (const auto& path : neededPaths) checkOutputInputs(path, "Needed libraries"); } checkOutputInputs(Io::NativePath(absPath), "Executable"); + if (!manifestPath.empty()) { + checkOutputInputs(manifestPath, "Patch manifest"); + if (std::filesystem::exists(manifestPath) && std::filesystem::exists(patchFile) && std::filesystem::equivalent(manifestPath, patchFile)) + throw Domain::RelinkerException("Patch manifest output would overwrite the patch file: " + Io::Utf8Path(manifestPath)); + } for (const auto& artifact : guestArtifacts) checkOutputInputs(artifact.Path, "Guest module"); + std::vector outputPaths; + for (const auto& executable : executables) outputPaths.push_back(Io::NativePath(executable.OutputPath)); + for (const auto& artifact : guestArtifacts) outputPaths.push_back(artifact.Path); + outputPaths.insert(outputPaths.end(), registryPaths.begin(), registryPaths.end()); + outputPaths.insert(outputPaths.end(), neededPaths.begin(), neededPaths.end()); + if (!manifestPath.empty()) outputPaths.push_back(manifestPath); + if (desktopEntry) { + outputPaths.push_back(desktopEntry->IconPath); + outputPaths.push_back(desktopEntry->EntryPath); + } + if (args.windowsDiagnostics) outputPaths.push_back(std::filesystem::absolute("windows-diagnostics-imports.txt")); + for (std::size_t index = 0; index < outputPaths.size(); ++index) { + for (std::size_t previous = 0; previous < index; ++previous) { + const auto& first = outputPaths[index]; + const auto& second = outputPaths[previous]; + if (SameOutputDestination(first, second)) + throw Domain::RelinkerException("Output paths collide: " + Io::Utf8Path(first) + " and " + Io::Utf8Path(second)); + } + } + if (args.windowsDiagnostics) fileWriter.Write("windows-diagnostics-imports.txt", executables.front().Diagnostics); const auto outputCount = guestArtifacts.size() + executables.size(); progress.Report({Domain::ProgressStage::Write, absPath, 0, outputCount}); @@ -196,16 +415,27 @@ int Run(const int argc, char* argv[]) { } for (const auto& executable : executables) { fileWriter.Write(executable.OutputPath, executable.Bytes); + if (!args.toWindows) { + std::error_code error; + std::filesystem::permissions(Io::NativePath(executable.OutputPath), std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec, std::filesystem::perm_options::add, error); + if (error) throw Domain::RelinkerException("Cannot make the output executable: " + executable.OutputPath + " (" + error.message() + ")"); + } progress.Report({Domain::ProgressStage::Write, absPath, ++written, outputCount}); } if (args.writeRegistry) { const auto registryWriter = std::make_shared(); - for (std::size_t index = 0; index < executables.size(); ++index) + for (std::size_t index = 0; index < executables.size(); ++index) { fileWriter.Write(Io::Utf8Path(registryPaths[index]), registryWriter->WriteCallRegistry(executables[index].Result.RegistryEntries)); - for (std::size_t index = 0; index < guestArtifacts.size(); ++index) + fileWriter.Write(Io::Utf8Path(neededPaths[index]), registryWriter->WriteNeededLibraries(executables[index].Result.DynamicSection.NeededLibraries)); + } + for (std::size_t index = 0; index < guestArtifacts.size(); ++index) { fileWriter.Write(Io::Utf8Path(registryPaths[executables.size() + index]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports)); + fileWriter.Write(Io::Utf8Path(neededPaths[executables.size() + index]), registryWriter->WriteNeededLibraries(guestArtifacts[index].NeededLibraries)); + } } + if (!manifestPath.empty()) fileWriter.Write(Io::Utf8Path(manifestPath), manifestText); progress.Finish(); + if (!manifestPath.empty()) std::cout << "Patch manifest: " << Io::Utf8Path(manifestPath) << '\n'; if (desktopEntry) std::cout << "Desktop entry: " << Io::Utf8Path(desktopEntryWriter.Write(*desktopEntry)) << '\n'; std::cout << "\nConversion complete\n" << "Output file: " << absPath << '\n' @@ -218,7 +448,7 @@ int Run(const int argc, char* argv[]) { } std::vector modulePaths; for (const auto& artifact : guestArtifacts) { - std::string relative = Io::Utf8Path(artifact.Path.lexically_relative(Io::NativePath(absPath).parent_path() / "app0")); + std::string relative = Io::Utf8Path(artifact.Path.lexically_relative(Io::NativePath(absPath).parent_path().lexically_normal() / "app0")); std::replace(relative.begin(), relative.end(), '\\', '/'); modulePaths.push_back(std::move(relative)); } diff --git a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp index 9169186da7..cdd140af64 100644 --- a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp +++ b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp @@ -4,12 +4,13 @@ #include #include #include +#include namespace Relinker { class CodeInstructionCollector { public: - std::set Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress = {}) const; + std::set Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress = {}, std::set* entryRoots = nullptr, const std::function& message = {}) const; }; } diff --git a/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp b/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp new file mode 100644 index 0000000000..b80aae4651 --- /dev/null +++ b/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp @@ -0,0 +1,24 @@ +#ifndef RELINKER_ANALYSIS_RELATIVEJUMPTABLES_HPP +#define RELINKER_ANALYSIS_RELATIVEJUMPTABLES_HPP + +#include +#include +#include +#include +#include +#include + +namespace Relinker { + +struct RelativeJumpTable { + VirtualAddress Function; + VirtualAddress Begin; + VirtualAddress End; +}; + +std::size_t FindTrailingRelativeJumpTable(std::span code, VirtualAddress codeAddress = 0, std::size_t* tableEnd = nullptr, std::size_t unwindEnd = 0); +std::vector FindRelativeJumpTables(std::span segment, VirtualAddress segmentVAddr, const std::vector>& functions); + +} + +#endif diff --git a/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp b/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp index c43fd18cd6..4e897a7a0b 100644 --- a/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp +++ b/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp @@ -8,6 +8,7 @@ namespace Relinker { std::optional FindSceCodeEnd(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions, VirtualAddress pltGot, ByteCount pltRelocationsSize); +std::vector SceUnwindRanges(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions); } diff --git a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IControlFlowGraph.hpp b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IControlFlowGraph.hpp index 77e6466fc2..2bd06b8cfd 100644 --- a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IControlFlowGraph.hpp +++ b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IControlFlowGraph.hpp @@ -28,7 +28,7 @@ std::unique_ptr BuildControlFlowGraph( bool followCodeAddresses = false ); -std::unique_ptr BuildControlFlowGraph(std::span text, VirtualAddress textVaddr, VirtualAddress entryVaddr, const std::vector& extraEntries, const IRelativeRelocationIndex& relativeRelocations, bool followCodeAddresses, std::unordered_set& reachable, const std::function& progress = {}); +std::unique_ptr BuildControlFlowGraph(std::span text, VirtualAddress textVaddr, VirtualAddress entryVaddr, const std::vector& extraEntries, const IRelativeRelocationIndex& relativeRelocations, bool followCodeAddresses, std::unordered_set& reachable, const std::function& progress = {}, const std::function& isData = {}); std::unique_ptr BuildControlFlowGraph( const std::vector& text, diff --git a/core/relinker/relinker/include/relinker/analysis/ValidationPolicy.hpp b/core/relinker/relinker/include/relinker/analysis/ValidationPolicy.hpp index 857487505e..15c2397a1b 100644 --- a/core/relinker/relinker/include/relinker/analysis/ValidationPolicy.hpp +++ b/core/relinker/relinker/include/relinker/analysis/ValidationPolicy.hpp @@ -10,17 +10,9 @@ class ValidationPolicy : public IValidationPolicy { public: ValidationPolicy() = default; - void ValidateSyscallAbsence() override; void ValidateRelocationTypeSupported(std::uint32_t relocationTypeValue, FileByteOffset fileByteOffset) override; - void ValidateNidBelongsToLibrary(const std::string& nid, const std::string& library) override; - void ValidateSceStructureSize(ByteCount expectedSize, ByteCount actualSize, FileByteOffset fileByteOffset) override; - void ValidateDynamicFieldInterpretable(const std::string& fieldName, FileByteOffset fileByteOffset) override; - void ValidateNoSyscallInstructions(const std::vector& codeSection, FileByteOffset codeOffset) override; - - void RegisterLibraryImport(const std::string& library); private: - std::set _importedLibraries; std::set _supportedRelocationTypes; void _initializeSupportedRelocationTypes(); diff --git a/core/relinker/relinker/include/relinker/domain/ICallRegistryWriter.hpp b/core/relinker/relinker/include/relinker/domain/ICallRegistryWriter.hpp index d528667e97..57f2e9c218 100644 --- a/core/relinker/relinker/include/relinker/domain/ICallRegistryWriter.hpp +++ b/core/relinker/relinker/include/relinker/domain/ICallRegistryWriter.hpp @@ -13,6 +13,7 @@ public: virtual std::string WriteCallRegistry(const std::vector& entries) = 0; virtual std::string WriteModuleImports(const std::vector& entries) = 0; + virtual std::string WriteNeededLibraries(const std::vector& libraries) = 0; }; } diff --git a/core/relinker/relinker/include/relinker/domain/ICallSiteResolver.hpp b/core/relinker/relinker/include/relinker/domain/ICallSiteResolver.hpp index 93e53570cd..8a3e5df7dc 100644 --- a/core/relinker/relinker/include/relinker/domain/ICallSiteResolver.hpp +++ b/core/relinker/relinker/include/relinker/domain/ICallSiteResolver.hpp @@ -14,7 +14,8 @@ public: const std::vector& textSection, FileByteOffset textSectionVAddr, VirtualAddress targetGotOrPltAddress, - ByteCount targetGotOrPltSize + ByteCount targetGotOrPltSize, + const std::vector& unwindRanges = {} ) = 0; }; diff --git a/core/relinker/relinker/include/relinker/domain/IElfReader.hpp b/core/relinker/relinker/include/relinker/domain/IElfReader.hpp index f528b39451..14eca6cbe1 100644 --- a/core/relinker/relinker/include/relinker/domain/IElfReader.hpp +++ b/core/relinker/relinker/include/relinker/domain/IElfReader.hpp @@ -16,7 +16,6 @@ public: [[nodiscard]] virtual std::vector ReadSectionHeaders() const = 0; [[nodiscard]] virtual std::vector ReadDynamicTags(const ProgramHeader& dynamicHeader) const = 0; [[nodiscard]] virtual FileByteOffset TranslateVirtualAddress(VirtualAddress address) const = 0; - [[nodiscard]] virtual std::vector ReadSection(const SectionHeader& header) const = 0; [[nodiscard]] virtual std::vector ReadSegment(const ProgramHeader& header) const = 0; [[nodiscard]] virtual std::uint64_t GetFileSize() const = 0; [[nodiscard]] virtual const std::vector& GetRawBytes() const = 0; diff --git a/core/relinker/relinker/include/relinker/domain/IRelinkerPipeline.hpp b/core/relinker/relinker/include/relinker/domain/IRelinkerPipeline.hpp index 33466e36b4..6c1513e6ff 100644 --- a/core/relinker/relinker/include/relinker/domain/IRelinkerPipeline.hpp +++ b/core/relinker/relinker/include/relinker/domain/IRelinkerPipeline.hpp @@ -2,8 +2,6 @@ #define RELINKER_DOMAIN_IRELINKERPIPELINE_HPP #include -#include -#include namespace Relinker { @@ -11,7 +9,7 @@ class IRelinkerPipeline { public: virtual ~IRelinkerPipeline() = default; - virtual RelinkResult Relink(const std::vector& sourceElf) = 0; + virtual RelinkResult Relink() = 0; }; } diff --git a/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp b/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp index 71a9160da0..a4adf4a77c 100644 --- a/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp +++ b/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp @@ -12,7 +12,7 @@ class ISyscallScanner { public: virtual ~ISyscallScanner() = default; - virtual std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input = {}) = 0; + virtual std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input = {}, const std::vector& unwindRanges = {}) = 0; }; } diff --git a/core/relinker/relinker/include/relinker/domain/IValidationPolicy.hpp b/core/relinker/relinker/include/relinker/domain/IValidationPolicy.hpp index 74e7100937..cacab6d8c0 100644 --- a/core/relinker/relinker/include/relinker/domain/IValidationPolicy.hpp +++ b/core/relinker/relinker/include/relinker/domain/IValidationPolicy.hpp @@ -9,12 +9,7 @@ class IValidationPolicy { public: virtual ~IValidationPolicy() = default; - virtual void ValidateSyscallAbsence() = 0; virtual void ValidateRelocationTypeSupported(std::uint32_t relocationTypeValue, FileByteOffset fileByteOffset) = 0; - virtual void ValidateNidBelongsToLibrary(const std::string& nid, const std::string& library) = 0; - virtual void ValidateSceStructureSize(ByteCount expectedSize, ByteCount actualSize, FileByteOffset fileByteOffset) = 0; - virtual void ValidateDynamicFieldInterpretable(const std::string& fieldName, FileByteOffset fileByteOffset) = 0; - virtual void ValidateNoSyscallInstructions(const std::vector& codeSection, FileByteOffset codeOffset) = 0; }; } diff --git a/core/relinker/relinker/include/relinker/domain/Types.hpp b/core/relinker/relinker/include/relinker/domain/Types.hpp index 0cae3ed174..856d6a0154 100644 --- a/core/relinker/relinker/include/relinker/domain/Types.hpp +++ b/core/relinker/relinker/include/relinker/domain/Types.hpp @@ -8,6 +8,7 @@ namespace Relinker { using FileByteOffset = Domain::FileByteOffset; using VirtualAddress = Domain::VirtualAddress; using ByteCount = Domain::ByteCount; +using CodeRange = Domain::CodeRange; using ElfHeader = Domain::ElfHeader; using ProgramHeader = Domain::ProgramHeader; using SectionHeader = Domain::SectionHeader; diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index bef96dbe10..6be45157af 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +44,7 @@ struct GuestImage { std::vector ModuleNames; std::vector Bytes; std::vector Trampolines; + std::vector PatchedRanges; std::vector Headers; std::vector Symbols; std::vector Dependencies; @@ -50,14 +52,15 @@ struct GuestImage { std::vector InitArray; std::vector FiniArray; std::uint64_t Init = 0; + bool HasInit = false; std::uint64_t Fini = 0; std::uint64_t Got = 0; bool UsePlatformTlsResolver = true; + bool Symbolic = false; }; class GuestImageReader { public: - std::vector ReadModuleNames(const std::vector& bytes) const; GuestImage Read(const std::filesystem::path& path, std::vector bytes, bool windows = false) const; }; @@ -66,11 +69,13 @@ struct GuestArtifact { std::vector Bytes; std::vector Imports; std::filesystem::path SourcePath; + std::vector NeededLibraries; }; struct GuestModuleInput { std::filesystem::path Path; std::vector Bytes; + std::vector PatchedRanges; }; class GuestModuleBuilder { diff --git a/core/relinker/relinker/include/relinker/output/CallRegistryWriter.hpp b/core/relinker/relinker/include/relinker/output/CallRegistryWriter.hpp index 79b196f869..5c16c9057b 100644 --- a/core/relinker/relinker/include/relinker/output/CallRegistryWriter.hpp +++ b/core/relinker/relinker/include/relinker/output/CallRegistryWriter.hpp @@ -9,6 +9,7 @@ class CallRegistryWriter : public ICallRegistryWriter { public: std::string WriteCallRegistry(const std::vector& entries) override; std::string WriteModuleImports(const std::vector& entries) override; + std::string WriteNeededLibraries(const std::vector& libraries) override; }; } diff --git a/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp b/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp index b0cb4d0f76..b8fe9513c3 100644 --- a/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp +++ b/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp @@ -10,15 +10,16 @@ class ElfReader : public IElfReader { public: explicit ElfReader(std::vector fileBuffer); + void ValidateMagic() const; ElfHeader ReadHeader() const override; std::vector ReadProgramHeaders() const override; std::vector ReadSectionHeaders() const override; std::vector ReadDynamicTags(const ProgramHeader& dynamicHeader) const override; FileByteOffset TranslateVirtualAddress(VirtualAddress address) const override; - std::vector ReadSection(const SectionHeader& header) const override; std::vector ReadSegment(const ProgramHeader& header) const override; std::uint64_t GetFileSize() const override; const std::vector& GetRawBytes() const override; + std::vector TakeBytes(); private: std::vector _fileBuffer; diff --git a/core/relinker/relinker/include/relinker/patching/GamePatch.hpp b/core/relinker/relinker/include/relinker/patching/GamePatch.hpp new file mode 100644 index 0000000000..6b76048dfb --- /dev/null +++ b/core/relinker/relinker/include/relinker/patching/GamePatch.hpp @@ -0,0 +1,132 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Relinker::GamePatch { + +inline constexpr char PatchFileName[] = "relinker-patches.toml"; +inline constexpr char ManifestSuffix[] = ".patches.toml"; + +struct PatchInteger { + bool Negative = false; + std::uint64_t Magnitude = 0; +}; + +struct PatchEntry { + std::string Target; + std::uint64_t VirtualAddress = 0; + std::vector Expected; + std::vector Value; + std::string Description; + std::string Name; + std::string Type; + PatchInteger ExpectedNumber; + PatchInteger ValueNumber; + std::optional Min; + std::optional Max; + std::optional Align; + std::size_t Line = 0; +}; + +struct PatchFile { + std::vector Entries; + std::optional MainTarget; + std::optional LinuxBase; +}; + +struct PatchedRange { + std::uint64_t Offset = 0; + std::uint64_t Length = 0; + std::uint64_t VirtualAddress = 0; +}; + +struct AppliedPatch { + std::string Target; + std::uint64_t VirtualAddress = 0; + std::uint64_t Offset = 0; + std::vector Expected; + std::vector Value; + std::string Description; + std::string Name; + std::string Type; + PatchInteger ExpectedNumber; + PatchInteger ValueNumber; + std::optional Min; + std::optional Max; + std::optional Align; +}; + +struct PatchedTarget { + std::vector Applied; + std::vector Ranges; +}; + +struct WriterRange { + std::uint64_t Offset = 0; + std::uint64_t Length = 0; + std::string Writer; +}; + +struct WriterSites { + std::span Reports; + std::span Trampolines; + std::span RelinkPatches; + std::span Traps; + std::span RelaData; + std::span RelaPltData; +}; + +enum class VerifyStatus { Applicable, Applied, NotChecked, Mismatch }; + +struct VerifyTarget { + std::string Name; + std::vector Bytes; +}; + +struct VerifyResult { + std::string Name; + std::string Target; + std::uint64_t VirtualAddress = 0; + std::uint64_t Offset = 0; + VerifyStatus Status = VerifyStatus::Mismatch; + std::string Detail; +}; + +PatchFile ParsePatchFile(std::string_view text); + +std::vector ResolveRanges(const ElfReader& reader, const std::vector& entries); + +std::vector ApplyPatches(std::vector& bytes, const std::string& target, const std::vector& entries, const std::vector& ranges); + +PatchedTarget PatchTarget(std::vector& bytes, const std::string& target, const std::vector& entries); + +void CheckOverlaps(const std::string& target, const std::vector& patched, const WriterSites& writers); + +void MatchTargets(const std::vector& entries, const std::vector& targetNames); + +std::vector AggregateManifest(const std::vector>& records); + +std::string WriteManifest(const std::vector& patches, const std::string& mainTarget, const std::string& configPath, std::optional linuxBase = std::nullopt); + +std::vector VerifyPatches(const PatchFile& file, const std::vector& executables, const std::vector& modules, bool outputMode); + +std::string_view StatusName(VerifyStatus status); + +std::filesystem::path OutputExecutablePath(const std::filesystem::path& outputPath, const std::string& executableArgument); + +std::string Hex(std::span bytes); + +std::string FormatApplied(const AppliedPatch& patch); + +std::string ModuleTargetName(const std::filesystem::path& modulePath, const std::filesystem::path& moduleRoot); + +} diff --git a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp index cd7db5edda..3a45a1ea10 100644 --- a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp +++ b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include namespace Relinker { @@ -23,8 +24,11 @@ struct ConversionOptions { std::optional WindowsFixedBase; bool WindowsDiagnostics = false; std::optional LinuxFixedBase; + std::optional MacosFixedBase; std::string RunPath = "$ORIGIN/libs"; std::set ExcludedModules; + std::vector Patches; + std::string TargetName; }; struct ConversionInput { @@ -42,6 +46,8 @@ struct ConversionOutput { std::vector Modules; RelinkResult Analysis; std::string WindowsDiagnostics; + std::vector AppliedPatches; + std::optional LinuxBase; }; class Conversion { diff --git a/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp b/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp index 43080f49af..26e20d2420 100644 --- a/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp +++ b/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp @@ -17,7 +17,7 @@ class RelinkerPipeline : public IRelinkerPipeline { public: RelinkerPipeline(std::shared_ptr elfReader, std::shared_ptr syscallScanner, std::shared_ptr callSiteResolver, std::shared_ptr validationPolicy, std::shared_ptr dynamicSectionBuilder, std::shared_ptr unusedNidFilter, std::uint32_t unusedFilterLevel, Domain::IProgress& progress); - RelinkResult Relink(const std::vector& sourceElf) override; + RelinkResult Relink() override; private: std::shared_ptr elfReader; diff --git a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp index 120e7abac6..91fe531f9c 100644 --- a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp +++ b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -18,95 +19,72 @@ std::int32_t readDisp32(const std::vector& text, std::size_t offse return static_cast(v); } -bool isRipRelativeFF(const std::vector& text, const Codegen::InstructionMatch& m) { - std::size_t cur = static_cast(m.Offset); - std::size_t end = cur + m.Length; - while (cur < end) { - std::uint8_t b = text[cur]; - if (b >= RexMin && b <= RexMax) { ++cur; continue; } - if (b == OneByteGrp5Rm && cur + 2 <= end) { - std::uint8_t modrm = text[cur + 1]; - std::uint8_t mod = (modrm >> ModRmModShift) & ModRmModMask; - std::uint8_t rm = modrm & ModRmRmMask; - const auto operation = (modrm >> ModRmRegShift) & ModRmRegMask; - return mod == ModRmModIndirect && rm == ModRmRmRipRelative && (operation >= 2 && operation <= 6); - } - break; - } - return false; -} - -std::size_t ffDispOffset(const std::vector& text, const Codegen::InstructionMatch& m) { - std::size_t cur = static_cast(m.Offset); - while (text[cur] >= RexMin && text[cur] <= RexMax) ++cur; - return cur + 2; -} - -bool isRipRelativeMov64(const std::vector& text, const Codegen::InstructionMatch& m) { +std::optional ripRelativeDispOffset(const std::vector& text, const Codegen::InstructionMatch& m) { std::size_t cur = static_cast(m.Offset); std::size_t end = cur + m.Length; bool hasRexW = false; while (cur < end) { std::uint8_t b = text[cur]; - if (b >= RexMin && b <= RexMax) { if (b & RexWBit) hasRexW = true; ++cur; continue; } - if (hasRexW && b == OneByteModRmRangeJMax && cur + 2 <= end) { - std::uint8_t modrm = text[cur + 1]; - std::uint8_t mod = (modrm >> ModRmModShift) & ModRmModMask; - std::uint8_t rm = modrm & ModRmRmMask; - return mod == ModRmModIndirect && rm == ModRmRmRipRelative; + if (b >= RexMin && b <= RexMax) { + if (b & RexWBit) hasRexW = true; + ++cur; + continue; } break; } - return false; -} - -std::size_t mov64DispOffset(const std::vector& text, const Codegen::InstructionMatch& m) { - std::size_t cur = static_cast(m.Offset); - while (text[cur] >= RexMin && text[cur] <= RexMax) ++cur; - return cur + 2; + if (cur + 2 > end) return std::nullopt; + const std::uint8_t opcode = text[cur]; + if (opcode != OneByteGrp5Rm && (!hasRexW || opcode != OneByteModRmRangeJMax)) + return std::nullopt; + const std::uint8_t modrm = text[cur + 1]; + const std::uint8_t mod = (modrm >> ModRmModShift) & ModRmModMask; + const std::uint8_t rm = modrm & ModRmRmMask; + if (mod != ModRmModIndirect || rm != ModRmRmRipRelative) return std::nullopt; + const auto operation = (modrm >> ModRmRegShift) & ModRmRegMask; + if (opcode == OneByteGrp5Rm && operation >= 2 && operation <= 6) + return cur + 2; + if (opcode == OneByteModRmRangeJMax && hasRexW) + return cur + 2; + return std::nullopt; } } class CallSiteResolver : public ICallSiteResolver { public: - std::vector ResolveCallSites(const std::vector& textSection, FileByteOffset textSectionVAddr, VirtualAddress targetGotOrPltAddress, ByteCount targetGotOrPltSize) override; + std::vector ResolveCallSites(const std::vector& textSection, FileByteOffset textSectionVAddr, VirtualAddress targetGotOrPltAddress, ByteCount targetGotOrPltSize, const std::vector& unwindRanges) override; private: const std::vector* _cachedTextPtr = nullptr; std::vector> _cachedTargetToInstr; FileByteOffset _cachedTextVAddr = 0; + std::vector _cachedUnwindRanges; }; -std::vector CallSiteResolver::ResolveCallSites(const std::vector& textSection, const FileByteOffset textSectionVAddr, const VirtualAddress targetGotOrPltAddress, const ByteCount targetGotOrPltSize) { +std::vector CallSiteResolver::ResolveCallSites(const std::vector& textSection, const FileByteOffset textSectionVAddr, const VirtualAddress targetGotOrPltAddress, const ByteCount targetGotOrPltSize, const std::vector& unwindRanges) { if (targetGotOrPltSize > std::numeric_limits::max() - targetGotOrPltAddress) throw RelinkerException("Relocation target range exceeds the address space"); if (textSection.size() > std::numeric_limits::max() - textSectionVAddr) throw RelinkerException("Text section range exceeds the address space"); if (textSection.empty()) return {}; - if (_cachedTextPtr != &textSection || _cachedTextVAddr != textSectionVAddr) { - auto scanner = Codegen::MakeInstructionScanner(); - auto instructions = scanner->ScanCodeSection(textSection, 0, textSection.size()); + if (_cachedTextPtr != &textSection || _cachedTextVAddr != textSectionVAddr || _cachedUnwindRanges != unwindRanges) { + auto scanner = Codegen::MakeInstructionScanner({}, unwindRanges); _cachedTargetToInstr.clear(); - const auto referenceCount = std::count_if(instructions.begin(), instructions.end(), [&](const auto& match) { - return isRipRelativeFF(textSection, match) || isRipRelativeMov64(textSection, match); + const auto candidateCount = std::count_if(textSection.begin(), textSection.end(), [](std::uint8_t b) { + return b == OneByteGrp5Rm || b == OneByteModRmRangeJMax; }); - _cachedTargetToInstr.reserve(static_cast(referenceCount)); - for (const auto& m : instructions) { - std::size_t dispOff = 0; - if (isRipRelativeFF(textSection, m)) - dispOff = ffDispOffset(textSection, m); - else if (isRipRelativeMov64(textSection, m)) - dispOff = mov64DispOffset(textSection, m); - else - continue; - if (dispOff + 4 > textSection.size()) continue; - std::int32_t disp = readDisp32(textSection, dispOff); + constexpr std::size_t MinRipRelativeInstructionBytes = 6; + _cachedTargetToInstr.reserve(std::min(static_cast(candidateCount), textSection.size() / MinRipRelativeInstructionBytes)); + scanner->VisitCodeSection(textSection, 0, textSection.size(), [&](const Codegen::InstructionMatch& m) { + const auto dispOff = ripRelativeDispOffset(textSection, m); + if (!dispOff || *dispOff + 4 > textSection.size()) return; + std::int32_t disp = readDisp32(textSection, *dispOff); VirtualAddress instrEnd = textSectionVAddr + m.Offset + static_cast(m.Length); VirtualAddress target = instrEnd + static_cast(static_cast(disp)); _cachedTargetToInstr.emplace_back(target, m.Offset); - } + }); std::sort(_cachedTargetToInstr.begin(), _cachedTargetToInstr.end()); _cachedTextPtr = &textSection; _cachedTextVAddr = textSectionVAddr; + _cachedUnwindRanges = unwindRanges; } std::vector sites; diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index c5ffda7b73..645dd46f3a 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include #include #include @@ -11,8 +13,11 @@ #include #include #include +#include #include +#include #include +#include namespace Relinker { @@ -35,7 +40,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::function& message) const { std::uint64_t lastProgress = 0; const auto report = [&](std::uint64_t completed) { if (completed <= lastProgress) return; @@ -93,28 +98,112 @@ std::set CodeInstructionCollector::Collect(const std::ve range(offset, size); return offset; }; + std::vector sceFunctions; + const auto trimToCode = [&](Domain::ProgramHeader& header, std::uint64_t end, bool interpreterAnywhere) { + const std::span segment(bytes.data() + header.Offset, header.FileSize); + auto begin = header.MappedAddress; + for (const auto& interpreter : headers) + if (interpreter.Type == 3 && interpreter.MappedAddress < end && + (interpreterAnywhere ? interpreter.MappedAddress >= begin : interpreter.MappedAddress == begin)) + begin = interpreter.MappedAddress + std::min(interpreter.FileSize, end - interpreter.MappedAddress); + const std::string_view loaderPath("/libexec/ld-elf.so.1", 21); + if (begin == header.MappedAddress && end - begin > loaderPath.size() && std::equal(loaderPath.begin(), loaderPath.end(), segment.begin())) begin += loaderPath.size(); + header.Offset += begin - header.MappedAddress; + header.MappedAddress = begin; + header.FileSize = end - begin; + }; if (sceBase) { const auto unwindFunctions = UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); + for (const auto& function : unwindFunctions) sceFunctions.push_back({function.Begin, function.End}); + sceFunctions = Codegen::MergeCodeRanges(std::move(sceFunctions)); for (auto& header : codeHeaders) { if (header.Type != 1 || (header.Flags & 1) == 0) continue; range(header.Offset, header.FileSize); const std::span segment(bytes.data() + header.Offset, header.FileSize); - const auto end = FindSceCodeEnd(segment, header.MappedAddress, unwindFunctions, value(3, 0x61000027).value_or(0), value(2, 0x6100002d).value_or(0)).value_or(header.MappedAddress + header.FileSize); - auto begin = header.MappedAddress; - for (const auto& interpreter : headers) - if (interpreter.Type == 3 && interpreter.MappedAddress >= begin && interpreter.MappedAddress < end) begin = interpreter.MappedAddress + std::min(interpreter.FileSize, end - interpreter.MappedAddress); - header.Offset += begin - header.MappedAddress; - header.MappedAddress = begin; - header.FileSize = end - begin; + trimToCode(header, FindSceCodeEnd(segment, header.MappedAddress, unwindFunctions, value(3, 0x61000027).value_or(0), value(2, 0x6100002d).value_or(0)).value_or(header.MappedAddress + header.FileSize), true); + } + } else { + for (auto& header : codeHeaders) { + if (header.Type != 1 || (header.Flags & 1) == 0) continue; + trimToCode(header, header.MappedAddress + header.FileSize, false); } } std::set roots; std::map functions; - const auto addRoot = [&](std::uint64_t address) { if (isCode(address)) roots.insert(address); }; + std::map tables; + const auto inTable = [&](std::uint64_t address) { const auto after = tables.upper_bound(address); return after != tables.begin() && address < std::prev(after)->second; }; + const auto addRoot = [&](std::uint64_t address) { if (isCode(address) && !inTable(address)) roots.insert(address); }; + const auto sceFunctionAfter = [&](std::uint64_t address) { + return std::upper_bound(sceFunctions.begin(), sceFunctions.end(), address, [](std::uint64_t value, const Domain::CodeRange& function) { return value < function.Begin; }); + }; + const auto insideSceFunction = [&](std::uint64_t address) { + const auto after = sceFunctionAfter(address); + return after != sceFunctions.begin() && address < std::prev(after)->End; + }; + const auto nearSceFunctionEnd = [&](std::uint64_t address) { + const auto after = sceFunctionAfter(address); + if (after == sceFunctions.begin()) return false; + const auto end = std::prev(after)->End; + return address >= end && address - end < 256; + }; + const auto codeAt = [&](std::uint64_t address) { + for (const auto& header : codeHeaders) + if (header.Type == 1 && (header.Flags & 1) != 0 && address >= header.MappedAddress && address - header.MappedAddress < header.FileSize) + return std::span(bytes.data() + header.Offset + (address - header.MappedAddress), header.FileSize - (address - header.MappedAddress)); + return std::span(); + }; + const auto absoluteFsAccess = [](const std::uint8_t* code, const Codegen::DecodedInstructionInfo& info) { + return info.SegmentPrefix == 0x64 && info.OpcodeOffset + 2 < info.Length && (code[info.OpcodeOffset + 1] & 0xc7) == 0x04 && code[info.OpcodeOffset + 2] == 0x25; + }; + struct GapRun { + bool Transfer = false; + bool AbsoluteFsAccess = false; + }; + const auto decodeGapRun = [&](std::uint64_t address) { + const auto after = sceFunctionAfter(address); + const auto limit = after == sceFunctions.end() ? std::numeric_limits::max() : after->Begin; + const Codegen::X64InstructionDecoder probe; + GapRun run; + for (std::size_t count = 0; count < 256 && address < limit; ++count) { + const auto code = codeAt(address); + if (code.empty()) return run; + const auto available = std::min(code.size(), limit - address); + Codegen::DecodedInstructionInfo info; + try { + info = probe.DecodeInstruction(code.data(), available); + } catch (const Codegen::CodegenException&) { + return run; + } + if (info.Length == 0 || info.Length > available) return run; + if (absoluteFsAccess(code.data(), info)) run.AbsoluteFsAccess = true; + switch (info.FlowKind) { + case Codegen::ControlFlowKind::UnconditionalJump: + case Codegen::ControlFlowKind::Return: + case Codegen::ControlFlowKind::IndirectJump: + case Codegen::ControlFlowKind::Trap: + run.Transfer = true; + return run; + default: + break; + } + address += info.Length; + } + return run; + }; + std::set dataTargets; + const auto addPointerRoot = [&](std::uint64_t address) { + if (sceFunctions.empty() || !isCode(address) || insideSceFunction(address)) { + addRoot(address); + return; + } + const auto run = decodeGapRun(address); + if (run.AbsoluteFsAccess || (address % 16 == 0 && nearSceFunctionEnd(address) && run.Transfer)) addRoot(address); + else dataTargets.insert(address); + }; const auto addAddressTakenRoot = [&](std::uint64_t address, const std::uint8_t* code, const Codegen::DecodedInstructionInfo& info) { if (!info.HasRipRelativeDisp || code[info.OpcodeOffset] != 0x8d || !(info.RexPrefix & 8)) return; const auto displacement = std::bit_cast(Io::ReadU32(bytes, fileOffset(address, info.Length) + info.RipRelativeDispOffset)); - addRoot(address + info.Length + static_cast(displacement)); + addPointerRoot(address + info.Length + static_cast(displacement)); }; const auto addFunction = [&](std::uint64_t begin, std::uint64_t size, bool symbolAlias) { if (size == 0) return; @@ -138,7 +227,8 @@ std::set CodeInstructionCollector::Collect(const std::ve }; range(0, 64); if (const auto entry = Io::ReadU64(bytes, 24); entry != 0) addRoot(entry); - for (const auto tag : {12u, 13u}) if (const auto address = value(tag, 0x60000000 + tag); address && *address != 0) addRoot(*address); + if (const auto init = value(12, 0x6000000c)) addRoot(*init); + if (const auto fini = value(13, 0x6000000d); fini && *fini != 0) addRoot(*fini); const auto symbolBytes = value(0x6100003f, 0xffffffffffffffffull); std::uint64_t symbolCount = 0; if (symbolBytes) { @@ -191,40 +281,53 @@ std::set CodeInstructionCollector::Collect(const std::ve } CodePointers pointers; std::set importSlots; - const auto readRelocations = [&](std::uint64_t addressTag, std::uint64_t sceAddressTag, std::uint64_t sizeTag, std::uint64_t sceSizeTag) { + const auto relocationRange = [&](std::uint64_t addressTag, std::uint64_t sceAddressTag, std::uint64_t sizeTag, std::uint64_t sceSizeTag) -> std::pair { const auto size = value(sizeTag, sceSizeTag); - if (!size) return; + if (!size) return {0, 0}; if (*size % 24 != 0) throw Domain::RelinkerException("Code analysis: invalid relocation table size"); - if (*size == 0) return; - const auto start = table(addressTag, sceAddressTag, *size); - for (std::uint64_t offset = start; offset < start + *size; offset += 24) { + if (*size == 0) return {0, 0}; + return {table(addressTag, sceAddressTag, *size), *size}; + }; + const auto [relaOffset, relaSize] = relocationRange(7, 0x6100002f, 8, 0x61000031); + const auto [pltOffset, pltSize] = relocationRange(23, 0x61000029, 2, 0x6100002d); + if (relaSize != 0 && pltSize != 0 && relaOffset < pltOffset + pltSize && pltOffset < relaOffset + relaSize + && relaOffset % 24 != pltOffset % 24) + throw Domain::RelinkerException("Code analysis: unaligned overlapping relocation tables", pltOffset); + const auto readRelocations = [&](std::uint64_t start, std::uint64_t size, bool plt) { + for (std::uint64_t offset = start; offset < start + size; offset += 24) { + if (!plt && offset >= pltOffset && offset - pltOffset < pltSize) continue; const auto slot = Io::ReadU64(bytes, offset); const auto info = Io::ReadU64(bytes, offset + 8); const auto type = static_cast(info); const auto index = info >> 32; const auto addend = Io::ReadU64(bytes, offset + 16); if (type == 8) { - const auto zeroSectionSymbol = index != 0 && index < symbolCount && (bytes[symbols + index * 24 + 4] & 15) == 3 && Io::ReadU64(bytes, symbols + index * 24 + 8) == 0; - if ((index != 0 && !zeroSectionSymbol) || !pointers.Values.emplace(slot, addend).second) throw Domain::RelinkerException("Code analysis: invalid relative relocation", slot); - addRoot(addend); + const auto hasSymbol = index != 0 && index < symbolCount; + const auto symbol = hasSymbol ? symbols + index * 24 : 0; + const auto zeroSectionSymbol = hasSymbol && (bytes[symbol + 4] & 15) == 3 && Io::ReadU64(bytes, symbol + 8) == 0; + const auto matchingDefinedSymbol = hasSymbol && Io::ReadU16(bytes, symbol + 6) != 0 + && addend <= static_cast(std::numeric_limits::max()) + && Io::ReadU64(bytes, symbol + 8) == addend; + if ((index != 0 && !zeroSectionSymbol && !matchingDefinedSymbol) || !pointers.Values.emplace(slot, addend).second) throw Domain::RelinkerException("Code analysis: invalid relative relocation", slot); + addPointerRoot(addend); } else if (type == 1 || type == 6 || type == 7) { - if (index == 0 || index >= symbolCount) throw Domain::RelinkerException("Code analysis: invalid relocation symbol", slot); + if (index != 0 && index >= symbolCount) throw Domain::RelinkerException("Code analysis: invalid relocation symbol", slot); const auto symbol = symbols + index * 24; - if (Io::ReadU16(bytes, symbol + 6) == 0) importSlots.insert(slot); + if (index == 0 || Io::ReadU16(bytes, symbol + 6) == 0) importSlots.insert(slot); else { const auto target = Io::ReadU64(bytes, symbol + 8) + addend; if (!pointers.Values.emplace(slot, target).second) throw Domain::RelinkerException("Code analysis: duplicate pointer relocation", slot); - addRoot(target); + addPointerRoot(target); } } } }; - readRelocations(7, 0x6100002f, 8, 0x61000031); - readRelocations(23, 0x61000029, 2, 0x6100002d); + readRelocations(relaOffset, relaSize, false); + readRelocations(pltOffset, pltSize, true); for (const auto& [addressTag, sizeTag] : {std::pair{25u, 27u}, std::pair{26u, 28u}, std::pair{32u, 33u}}) { const auto address = value(addressTag, 0x60000000 + addressTag); const auto size = value(sizeTag, 0x60000000 + sizeTag); - if (address.has_value() != size.has_value()) throw Domain::RelinkerException("Code analysis: incomplete initializer array"); + if (size && !address) throw Domain::RelinkerException("Code analysis: incomplete initializer array"); if (!size || *size == 0) continue; if (*size % 8 != 0) throw Domain::RelinkerException("Code analysis: invalid initializer array size"); const auto offset = fileOffset(*address, *size); @@ -241,6 +344,19 @@ std::set CodeInstructionCollector::Collect(const std::ve addFunction(function.Begin, function.End - function.Begin, false); for (const auto target : function.ExtraTargets) addRoot(target); } + if (sceBase) { + const std::vector> ranges(functions.begin(), functions.end()); + for (const auto& header : codeHeaders) { + if (header.Type != 1 || (header.Flags & 1) == 0) continue; + const std::span segment(bytes.data() + header.Offset, header.FileSize); + for (const auto& table : FindRelativeJumpTables(segment, header.MappedAddress, ranges)) { + tables.emplace(table.Begin, table.End); + auto& end = functions.at(table.Function); + end = std::min(end, table.Begin); + } + } + std::erase_if(roots, inTable); + } if (roots.empty()) throw Domain::RelinkerException("Code analysis: no code entry points"); std::set instructions; const Codegen::X64InstructionDecoder decoder; @@ -272,7 +388,7 @@ std::set CodeInstructionCollector::Collect(const std::ve } if (info.Length == 0 || info.Length > end - address) throw Domain::RelinkerException("Code analysis: instruction crosses function boundary", address); - instructions.insert(address); + instructions.emplace_hint(instructions.end(), address); addAddressTakenRoot(address, bytes.data() + offset + address - begin, info); if (info.HasBranchTarget && !info.HasRipRelativeDisp) { const auto target = address + info.Length + static_cast(info.BranchDisp); @@ -333,8 +449,7 @@ std::set CodeInstructionCollector::Collect(const std::ve position = crossed; continue; } - const auto opcode = position + info.OpcodeOffset; - if (info.SegmentPrefix == 0x64 && opcode + 2 < next && (text[opcode + 1] & 0xc7) == 0x04 && text[opcode + 2] == 0x25) addRoot(header.MappedAddress + position); + if (absoluteFsAccess(text + position, info)) addRoot(header.MappedAddress + position); afterTransfer = info.FlowKind != Codegen::ControlFlowKind::Sequential && info.FlowKind != Codegen::ControlFlowKind::ConditionalBranch; position = next; } @@ -372,7 +487,7 @@ std::set CodeInstructionCollector::Collect(const std::ve for (const auto root : roots) if (root >= header.MappedAddress && root - header.MappedAddress < header.FileSize) entries.push_back(root); if (entries.empty()) continue; const std::span text(bytes.data() + header.Offset, header.FileSize); - const auto graph = UnusedNidFilter::BuildControlFlowGraph(text, header.MappedAddress, entries.front(), entries, pointers, true, sectionReachable[i], [&](std::uint64_t count) { report(6000 + 2500 * count / std::max(scanBytes, 1)); }); + const auto graph = UnusedNidFilter::BuildControlFlowGraph(text, header.MappedAddress, entries.front(), entries, pointers, sceFunctions.empty(), sectionReachable[i], [&](std::uint64_t count) { report(6000 + 2500 * count / std::max(scanBytes, 1)); }, inTable); for (const auto address : graph->ReachableVaddrs()) { if (!decodedInstructions.insert(address).second) continue; instructions.insert(address); @@ -386,43 +501,49 @@ std::set CodeInstructionCollector::Collect(const std::ve report(8500); std::uint64_t checked = 0; std::uint64_t previousEnd = 0; + const Domain::ProgramHeader* cachedHeader = nullptr; + const auto findCodeHeader = [&](std::uint64_t addr) -> const Domain::ProgramHeader* { + if (cachedHeader != nullptr && cachedHeader->Type == 1 && (cachedHeader->Flags & 1) != 0 && addr >= cachedHeader->MappedAddress && addr - cachedHeader->MappedAddress < cachedHeader->FileSize) + return cachedHeader; + for (const auto& header : headers) { + if (header.Type == 1 && (header.Flags & 1) != 0 && addr >= header.MappedAddress && addr - header.MappedAddress < header.FileSize) { + cachedHeader = &header; + return cachedHeader; + } + } + return nullptr; + }; for (const auto address : instructions) { if (progress && checked % 4096 == 0) report(8500 + 1499 * checked / instructions.size()); ++checked; if (address < previousEnd) { std::uint64_t spanEnd = previousEnd; try { - for (const auto& header : headers) { - if (header.Type != 1 || (header.Flags & 1) == 0 || address < header.MappedAddress || address - header.MappedAddress >= header.FileSize) continue; - const auto offset = address - header.MappedAddress; - const auto length = decoder.DecodeInstruction(bytes.data() + header.Offset + offset, header.FileSize - offset).Length; - if (length == 0 || length > header.FileSize - offset) break; - spanEnd = std::max(spanEnd, address + length); - break; + if (const auto* header = findCodeHeader(address)) { + const auto offset = address - header->MappedAddress; + const auto length = decoder.DecodeInstruction(bytes.data() + header->Offset + offset, header->FileSize - offset).Length; + if (length != 0 && length <= header->FileSize - offset) + spanEnd = std::max(spanEnd, address + length); } } catch (const Codegen::CodegenException&) { } bool spanHasFsPrefix = false; - for (const auto& header : headers) { - if (header.Type != 1 || (header.Flags & 1) == 0 || address < header.MappedAddress || address - header.MappedAddress >= header.FileSize) continue; - const auto base = header.Offset + address - header.MappedAddress; + if (const auto* header = findCodeHeader(address)) { + const auto base = header->Offset + address - header->MappedAddress; for (auto probe = base; probe < base + (spanEnd - address) && probe < bytes.size(); ++probe) { if (bytes[probe] == 0x64) { spanHasFsPrefix = true; break; } } - break; } if (!spanHasFsPrefix) { const auto previous = instructions.lower_bound(address); if (previous != instructions.begin()) { const auto previousStart = *std::prev(previous); - for (const auto& header : headers) { - if (header.Type != 1 || (header.Flags & 1) == 0 || previousStart < header.MappedAddress || previousStart - header.MappedAddress >= header.FileSize) continue; - const auto base = header.Offset + previousStart - header.MappedAddress; + if (const auto* header = findCodeHeader(previousStart)) { + const auto base = header->Offset + previousStart - header->MappedAddress; if (decoder.DecodeInstruction(bytes.data() + base, bytes.size() - base).SegmentPrefix == 0x64) spanHasFsPrefix = true; - break; } } } @@ -430,14 +551,22 @@ std::set CodeInstructionCollector::Collect(const std::ve previousEnd = spanEnd; continue; } - for (const auto& header : headers) { - if (header.Type != 1 || (header.Flags & 1) == 0 || address < header.MappedAddress || address - header.MappedAddress >= header.FileSize) continue; - const auto offset = address - header.MappedAddress; - previousEnd = address + decoder.DecodeInstruction(bytes.data() + header.Offset + offset, header.FileSize - offset).Length; - break; + if (const auto* header = findCodeHeader(address)) { + const auto offset = address - header->MappedAddress; + previousEnd = address + decoder.DecodeInstruction(bytes.data() + header->Offset + offset, header->FileSize - offset).Length; } } report(10000); + std::erase_if(dataTargets, [&](std::uint64_t address) { return roots.contains(address) || inTable(address); }); + if (!dataTargets.empty() && message) { + std::ostringstream text; + text << "Took " << std::dec << dataTargets.size() << " pointer target(s) outside the unwind functions as data, the first at 0x" << std::hex << *dataTargets.begin() << '\n'; + message(text.str()); + } + if (entryRoots) { + *entryRoots = std::move(roots); + entryRoots->insert(dataTargets.begin(), dataTargets.end()); + } return instructions; } diff --git a/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp b/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp new file mode 100644 index 0000000000..7c99111960 --- /dev/null +++ b/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp @@ -0,0 +1,138 @@ +#include +#include +#include +#include +#include + +namespace Relinker { + +namespace { + +constexpr VirtualAddress MaxTableGapAfterFunction = 0x10000; + +struct TableLoad { + std::size_t At; + std::size_t Target; +}; + +bool IsNop(std::span code, std::size_t at, std::size_t length) { + auto position = at; + const auto end = at + length; + while (position < end && (code[position] == 0x66 || code[position] == 0x2e || code[position] == 0x3e)) ++position; + if (position >= end) return false; + if (code[position] == 0x90) return position + 1 == end; + return code[position] == 0x0f && position + 1 < end && code[position + 1] == 0x1f; +} + +bool OnlyPadding(std::span code, std::size_t from, std::size_t to, const Codegen::X64InstructionDecoder& decoder) { + for (auto position = from; position < to;) { + if (code[position] == 0x00 || code[position] == 0xcc || code[position] == 0x90) { + ++position; + continue; + } + try { + const auto length = decoder.DecodeInstruction(code.data() + position, to - position).Length; + if (length == 0 || length > to - position || !IsNop(code, position, length)) return false; + position += length; + } catch (const Codegen::CodegenException&) { + return false; + } + } + return true; +} + +std::size_t TableEntries(std::span code, std::size_t table, std::size_t end, std::size_t firstTable, const std::vector& instructionStarts) { + std::size_t entries = 0; + while (table + (entries + 1) * 4 <= end) { + std::int32_t relative = 0; + std::memcpy(&relative, code.data() + table + entries * 4, sizeof(relative)); + const auto destination = static_cast(table) + relative; + if (destination < 0 || destination >= static_cast(firstTable) || !instructionStarts[static_cast(destination)]) break; + ++entries; + } + return entries; +} + +std::size_t TablesEnd(std::span code, std::size_t firstTable, const std::vector& loads, const std::vector& instructionStarts) { + auto table = firstTable; + while (table < code.size()) { + auto end = code.size(); + for (const auto& load : loads) + if (load.Target > table && load.Target < end) end = load.Target; + const auto entries = TableEntries(code, table, end, firstTable, instructionStarts); + if (entries == 0 || (table == firstTable && entries < 2)) return table == firstTable ? 0 : table; + table += entries * 4; + if (std::none_of(loads.begin(), loads.end(), [&](const auto& load) { return load.Target == table; })) return table; + } + return table; +} + +} + +std::size_t FindTrailingRelativeJumpTable(std::span code, VirtualAddress codeAddress, std::size_t* tableEnd, std::size_t unwindEnd) { + const Codegen::X64InstructionDecoder decoder; + std::vector loads; + std::vector instructionStarts(code.size() + 1, false); + bool flowEnded = false; + bool sawIndirectJump = false; + std::size_t lastIndirectJump = 0; + for (std::size_t position = 0; position < code.size();) { + instructionStarts[position] = true; + if (flowEnded && sawIndirectJump) { + for (const auto& load : loads) { + if (load.Target != position || load.At >= lastIndirectJump) continue; + const auto end = TablesEnd(code, position, loads, instructionStarts); + if (end == 0) continue; + const auto aligned = std::min(code.size(), static_cast(((codeAddress + end + 15) & ~VirtualAddress{15}) - codeAddress)); + const auto checkedTo = std::max(aligned, std::min(code.size(), unwindEnd)); + const bool toRangeEnd = OnlyPadding(code, end, code.size(), decoder); + if (!toRangeEnd && !OnlyPadding(code, end, checkedTo, decoder)) continue; + if (tableEnd) *tableEnd = toRangeEnd ? code.size() : checkedTo; + return position; + } + } + Codegen::DecodedInstructionInfo info; + try { + info = decoder.DecodeInstruction(code.data() + position, code.size() - position); + } catch (const Codegen::CodegenException&) { + break; + } + if (info.Length == 0 || info.Length > code.size() - position) break; + const auto next = position + info.Length; + if (info.HasRipRelativeDisp && !info.IsTwoByteOpcode && info.OpcodeOffset < info.Length && info.Length >= 4 && code[position + info.OpcodeOffset] == 0x8d) { + std::int32_t displacement = 0; + std::memcpy(&displacement, code.data() + next - 4, sizeof(displacement)); + const auto target = static_cast(next) + displacement; + if (target > static_cast(next) && target < static_cast(code.size())) loads.push_back({position, static_cast(target)}); + } + if (info.FlowKind == Codegen::ControlFlowKind::IndirectJump) { + sawIndirectJump = true; + lastIndirectJump = position; + } + if (!IsNop(code, position, info.Length)) { + const bool directJumpOrCall = (info.Length == 5 && (code[position] == 0xe8 || code[position] == 0xe9)) || (info.Length == 2 && code[position] == 0xeb); + flowEnded = directJumpOrCall || info.FlowKind == Codegen::ControlFlowKind::Return || info.FlowKind == Codegen::ControlFlowKind::IndirectJump || info.FlowKind == Codegen::ControlFlowKind::Trap; + } + position = next; + } + return code.size(); +} + +std::vector FindRelativeJumpTables(std::span segment, VirtualAddress segmentVAddr, const std::vector>& functions) { + std::vector tables; + const auto segmentEnd = segmentVAddr + segment.size(); + for (std::size_t index = 0; index < functions.size(); ++index) { + const auto [begin, end] = functions[index]; + if (begin < segmentVAddr || begin >= segmentEnd || end <= begin) continue; + auto inspectEnd = std::min(end, segmentEnd); + if (index + 1 < functions.size() && functions[index + 1].first > inspectEnd && functions[index + 1].first - inspectEnd <= MaxTableGapAfterFunction) + inspectEnd = std::min(functions[index + 1].first, segmentEnd); + const auto code = segment.subspan(begin - segmentVAddr, inspectEnd - begin); + std::size_t tableEnd = code.size(); + if (const auto tail = FindTrailingRelativeJumpTable(code, begin, &tableEnd, std::min(end, segmentEnd) - begin); tail != 0 && tail < code.size()) + tables.push_back({begin, begin + tail, begin + tableEnd}); + } + return tables; +} + +} diff --git a/core/relinker/relinker/src/analysis/SceCodeExtent.cpp b/core/relinker/relinker/src/analysis/SceCodeExtent.cpp index 106528e542..3de6710b89 100644 --- a/core/relinker/relinker/src/analysis/SceCodeExtent.cpp +++ b/core/relinker/relinker/src/analysis/SceCodeExtent.cpp @@ -20,21 +20,39 @@ std::optional FindSceCodeEnd(std::span segme std::memcpy(&displacement, segment.data() + at + 2, sizeof(displacement)); return segmentVAddr + at + 6 + static_cast(static_cast(displacement)); }; + const auto isPlt = [&](const std::size_t at) { + return segment[at] == 0xff && segment[at + 1] == 0x35 && segment[at + 6] == 0xff && segment[at + 7] == 0x25 + && target(at) == pltGot + 8 && target(at + 6) == pltGot + 16; + }; const std::size_t lastFunctionEnd = codeEnd - segmentVAddr; - std::optional plt; - for (std::size_t at = lastFunctionEnd; segment.size() >= 16 && at <= segment.size() - 16; ++at) { - if (segment[at] == 0xff && segment[at + 1] == 0x35 && segment[at + 6] == 0xff && segment[at + 7] == 0x25 - && target(at) == pltGot + 8 && target(at + 6) == pltGot + 16) { - plt = at; - break; - } - } - if (!plt) - throw RelinkerException("PLT not found after the last unwind function", codeEnd); const ByteCount pltSize = 16 * (1 + pltRelocationsSize / 24); - if (pltSize > segment.size() - *plt) - throw RelinkerException("PLT exceeds executable segment", segmentVAddr + *plt); - return segmentVAddr + *plt + pltSize; + for (std::size_t at = lastFunctionEnd; segment.size() >= 16 && at <= segment.size() - 16; ++at) { + if (!isPlt(at)) + continue; + if (pltSize > segment.size() - at) + throw RelinkerException("PLT exceeds executable segment", segmentVAddr + at); + return segmentVAddr + at + pltSize; + } + for (std::size_t at = 0; at < lastFunctionEnd && at + 12 <= segment.size(); ++at) { + if (!isPlt(at)) + continue; + if (pltSize > segment.size() - at) + throw RelinkerException("PLT exceeds executable segment", segmentVAddr + at); + const VirtualAddress pltBegin = segmentVAddr + at; + for (const auto& function : functions) + if (function.Begin < pltBegin + pltSize && pltBegin < function.End) + throw RelinkerException("PLT overlaps an unwind function", pltBegin); + return codeEnd; + } + throw RelinkerException("PLT not found before or after the unwind functions", codeEnd); +} + +std::vector SceUnwindRanges(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions) { + std::vector ranges; + for (const auto& function : functions) + if (function.Begin >= segmentVAddr && function.Begin - segmentVAddr < segment.size() && function.End > function.Begin) + ranges.push_back({function.Begin - segmentVAddr, std::min(function.End - segmentVAddr, segment.size())}); + return ranges; } } diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index 128ededb0c..6003cda8e4 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -5,10 +5,14 @@ #include #include #include +#include +#include #include #include +#include #include +#include namespace Relinker { @@ -70,18 +74,37 @@ static constexpr std::array SDK_MMAP_EXEC_INLINE{ 0x49, 0x89, 0xC7, 0x49, 0xF7, 0xDF, 0x84, 0xC9, 0x4C, 0x0F, 0x45, 0xF8, 0x4D, 0x85, 0xFF, }; +struct RecentInstructions { + static constexpr std::size_t Capacity = 64; + std::array matches{}; + std::size_t count = 0; + + void Push(const Codegen::InstructionMatch& match) { + matches[count % Capacity] = match; + ++count; + } + + [[nodiscard]] const Codegen::InstructionMatch& Before(std::size_t step) const { + return matches[(count - 1 - step) % Capacity]; + } + + [[nodiscard]] bool Contains(FileByteOffset offset) const { + for (std::size_t step = 0; step < std::min(count, Capacity); ++step) + if (Before(step).Offset == offset) return true; + return false; + } +}; + template -static bool MatchesSdkInlineStub(const std::vector& code, std::size_t syscallOffset, std::size_t limit, const std::array& pattern, std::size_t beforeSyscall, const std::vector& matches, FileByteOffset codeOffset) { +static bool MatchesSdkInlineStub(const std::vector& code, std::size_t syscallOffset, std::size_t limit, const std::array& pattern, std::size_t beforeSyscall, const RecentInstructions& recent, FileByteOffset codeOffset) { if (syscallOffset < beforeSyscall || syscallOffset > limit) return false; const auto start = syscallOffset - beforeSyscall; if (pattern.size() > limit - start || !std::equal(pattern.begin(), pattern.end(), code.begin() + static_cast(start))) return false; - const auto instruction = std::lower_bound(matches.begin(), matches.end(), codeOffset + start, - [](const Codegen::InstructionMatch& match, FileByteOffset offset) { return match.Offset < offset; }); - return instruction != matches.end() && instruction->Offset == codeOffset + start; + return recent.Contains(codeOffset + start); } -static bool IsSdkMmanSyscallStub(const std::vector& code, std::size_t syscallOffset, std::size_t limit, const std::vector& matches, FileByteOffset codeOffset) { - return MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MPROTECT_INLINE, 42, matches, codeOffset) || MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MMAP_INLINE, 17, matches, codeOffset) || MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MMAP_EXEC_INLINE, 23, matches, codeOffset); +static bool IsSdkMmanSyscallStub(const std::vector& code, std::size_t syscallOffset, std::size_t limit, const RecentInstructions& recent, FileByteOffset codeOffset) { + return MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MPROTECT_INLINE, 42, recent, codeOffset) || MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MMAP_INLINE, 17, recent, codeOffset) || MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MMAP_EXEC_INLINE, 23, recent, codeOffset); } static ForbiddenInstruction ClassifyForbiddenInstruction(const std::vector& codeSection, const std::size_t opcodeOffset, const std::size_t limit) { @@ -125,9 +148,13 @@ static std::optional ConstantRaxLoad(const std::uint8_t* bytes, c return std::nullopt; } -static std::optional ConstantSyscallNumber(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const std::vector& matches, const std::size_t syscallIndex) { - for (std::size_t step = 1; step <= SYSCALL_NUMBER_LOOKBACK && step <= syscallIndex; ++step) { - const auto& match = matches[syscallIndex - step]; +static std::optional ConstantSyscallNumber(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const RecentInstructions& recent) { + FileByteOffset next = recent.Before(0).Offset; + for (std::size_t step = 1; step <= SYSCALL_NUMBER_LOOKBACK && step < recent.count; ++step) { + const auto& match = recent.Before(step); + if (match.Offset + match.Length != next) + return std::nullopt; + next = match.Offset; const std::size_t start = static_cast(match.Offset - codeSectionOffset); if (start >= codeSection.size() || match.Length > codeSection.size() - start) @@ -150,30 +177,110 @@ static std::optional ConstantSyscallNumber(const std::vector ConstantRaxSetEndingAt(const std::vector& code, const std::size_t end) { + if (end >= 10 && code[end - 10] == 0x48 && code[end - 9] == 0xB8) { + std::uint64_t value = 0; + std::memcpy(&value, code.data() + end - 8, sizeof(value)); + return value; + } + std::uint32_t immediate = 0; + if (end >= 7 && code[end - 7] == 0x48 && code[end - 6] == 0xC7 && code[end - 5] == 0xC0) { + std::memcpy(&immediate, code.data() + end - 4, sizeof(immediate)); + return static_cast(static_cast(static_cast(immediate))); + } + if (end >= 5 && code[end - 5] == 0xB8) { + std::memcpy(&immediate, code.data() + end - 4, sizeof(immediate)); + return immediate; + } + if (end >= 2 && (code[end - 2] == 0x31 || code[end - 2] == 0x33) && code[end - 1] == 0xC0) + return 0; + return std::nullopt; +} + +struct LowRaxWrite { + std::size_t Length; + std::uint64_t Mask; + std::uint64_t Value; +}; + +static std::optional ImmediateLowRaxWriteEndingAt(const std::vector& code, const std::size_t end) { + if (end >= 2 && code[end - 2] == 0xB0) + return LowRaxWrite{2, 0xFF, code[end - 1]}; + if (end >= 4 && code[end - 4] == 0x66 && code[end - 3] == 0xB8) + return LowRaxWrite{4, 0xFFFF, static_cast(code[end - 2]) | static_cast(code[end - 1]) << 8}; + return std::nullopt; +} + +static bool MovesOtherRegisterThanRax(const std::uint8_t* bytes, const std::size_t length) { + const std::uint8_t rex = length == 3 ? bytes[0] : 0; + if (length == 3 && (rex & 0xF0) != 0x40) + return false; + const std::uint8_t opcode = bytes[length - 2]; + const std::uint8_t modrm = bytes[length - 1]; + if ((modrm & 0xC0) != 0xC0) + return false; + const bool writesRm = opcode == 0x89 || opcode == 0x31; + const bool writesReg = opcode == 0x8B || opcode == 0x33; + if (!writesRm && !writesReg) + return false; + const unsigned written = writesRm ? (modrm & 7u) | ((rex & 1u) << 3) : ((modrm >> 3) & 7u) | ((rex & 4u) << 1); + return written != 0; +} + +static std::optional ConstantSyscallNumberInBytes(const std::vector& code, const std::size_t syscall) { + using Step = std::tuple; + std::vector pending{{syscall, 0, 0}}; + std::set seen(pending.begin(), pending.end()); + const auto visit = [&](const Step& step) { + if (seen.insert(step).second) + pending.push_back(step); + }; + while (!pending.empty()) { + const auto [end, mask, low] = pending.back(); + pending.pop_back(); + if (const auto value = ConstantRaxSetEndingAt(code, end)) + return (*value & ~mask) | low; + if (const auto write = ImmediateLowRaxWriteEndingAt(code, end)) + visit({end - write->Length, mask | write->Mask, low | (write->Value & ~mask)}); + for (const std::size_t length : {std::size_t{2}, std::size_t{3}}) + if (end >= length && MovesOtherRegisterThanRax(code.data() + end - length, length)) + visit({end - length, mask, low}); + } + return std::nullopt; +} + class SyscallScanner : public ISyscallScanner { public: SyscallScanner(Domain::IProgress& progress, const bool trapSdkStubs) : progress(progress), trapSdkStubs(trapSdkStubs) {} - std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input) override; + std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input, const std::vector& unwindRanges) override; private: Domain::IProgress& progress; bool trapSdkStubs; }; -std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const FileByteOffset codeSectionSize, std::string_view input) { +std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const FileByteOffset codeSectionSize, std::string_view input, const std::vector& unwindRanges) { if (codeSectionSize > codeSection.size()) throw RelinkerException("Code section exceeds the input buffer", codeSectionOffset); + if (codeSectionSize > std::numeric_limits::max() - codeSectionOffset) throw RelinkerException("Code section offset overflow", codeSectionOffset); const auto limit = static_cast(codeSectionSize); - const auto scanner = Codegen::MakeInstructionScanner([&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Decode, std::string(input), completed, total}); }); - const auto matches = scanner->ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize); - - progress.Report({Domain::ProgressStage::Syscalls, std::string(input), 0, matches.size()}); + const std::string source(input); + const auto scanner = Codegen::MakeInstructionScanner([&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Decode, source, completed, total}); }, unwindRanges); + RecentInstructions recent; std::vector trapped; - for (std::size_t index = 0; index < matches.size(); ++index) { - if (index != 0 && index % 4096 == 0) progress.Report({Domain::ProgressStage::Syscalls, std::string(input), index, matches.size()}); - const auto& match = matches[index]; + const auto functions = Codegen::MergeCodeRanges(unwindRanges); + const auto insideFunction = [&](const std::size_t offset) { + const auto after = std::upper_bound(functions.begin(), functions.end(), offset, [](const std::size_t value, const CodeRange& range) { return value < range.Begin; }); + return after != functions.begin() && offset < std::prev(after)->End; + }; + std::size_t skipped = 0; + std::optional firstSkipped; + scanner->VisitCodeSection(codeSection, codeSectionOffset, codeSectionSize, [&](const Codegen::InstructionMatch& match) { + recent.Push(match); + if (match.Offset < codeSectionOffset) throw RelinkerException("Instruction match precedes code section offset", match.Offset); const std::size_t start = static_cast(match.Offset - codeSectionOffset); + if (start > codeSection.size() || match.Length > codeSection.size() - start) throw RelinkerException("Instruction match exceeds code section", match.Offset); const auto opcodeOffset = Codegen::DecodedInstruction{codeSection.data() + start, match.Length}.OpcodeOffset(); const std::size_t opcode = start + opcodeOffset; const std::size_t spanLimit = std::min(limit, start + static_cast(match.Length)); @@ -181,27 +288,46 @@ std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::v const auto kind = ClassifyForbiddenInstruction(codeSection, opcode, spanLimit); if (kind == ForbiddenInstruction::None) - continue; + return; if (trapSdkStubs && kind == ForbiddenInstruction::Invocation && - (IsSdkSyscallStub(codeSection, opcode) || IsSdkMmanSyscallStub(codeSection, opcode, limit, matches, codeSectionOffset))) { + (IsSdkSyscallStub(codeSection, opcode) || IsSdkMmanSyscallStub(codeSection, opcode, limit, recent, codeSectionOffset))) { trapped.push_back(opcode); std::ostringstream message; message << "Trapping SDK syscall instruction at code offset 0x" << std::hex << match.Offset << '\n'; progress.Message(message.str()); - continue; + return; + } + + std::optional number; + if (kind == ForbiddenInstruction::Invocation) + number = ConstantSyscallNumber(codeSection, codeSectionOffset, recent); + if (!functions.empty() && !number && !insideFunction(start)) { + if (kind == ForbiddenInstruction::Invocation) + number = ConstantSyscallNumberInBytes(codeSection, opcode); + if (!number) { + ++skipped; + if (!firstSkipped) firstSkipped = match.Offset; + return; + } } std::ostringstream msg; msg << "Forbidden syscall instruction at code offset 0x" << std::hex << match.Offset; - if (kind == ForbiddenInstruction::Invocation) - if (const auto number = ConstantSyscallNumber(codeSection, codeSectionOffset, matches, index)) - msg << ": syscall number " << std::dec << *number; + if (number) + msg << ": syscall number " << std::dec << *number; - throw RelinkerException(msg.str(), match.Offset); + RelinkerException error(msg.str(), match.Offset); + error.InputPath = source; + throw error; + }); + if (skipped != 0) { + std::ostringstream message; + message << "Skipped " << std::dec << skipped << " syscall byte pattern(s) without a syscall number outside the unwind functions, the first at code offset 0x" << std::hex << *firstSkipped << '\n'; + progress.Message(message.str()); } - progress.Report({Domain::ProgressStage::Syscalls, std::string(input), matches.size(), matches.size()}); + progress.Report({Domain::ProgressStage::Syscalls, source, limit, limit}); return trapped; } @@ -211,7 +337,7 @@ std::unique_ptr MakeSyscallScanner(Domain::IProgress& progress, class NullSyscallScanner : public ISyscallScanner { public: - std::vector ScanCodeSectionForSyscalls(const std::vector&, FileByteOffset, FileByteOffset, std::string_view) override { return {}; } + std::vector ScanCodeSectionForSyscalls(const std::vector&, FileByteOffset, FileByteOffset, std::string_view, const std::vector&) override { return {}; } }; std::unique_ptr MakeNullSyscallScanner() { diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp index 3dbe4ab5c4..3bcedf3e05 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace Relinker { @@ -41,7 +42,7 @@ public: std::vector result; for (const auto& ref : nidRefs) - if (index->IsGotSlotAccessed(ref.RelocationAddress)) + if (ref.RelocationTypeValue != Elfpatcher::R_X86_64_GLOB_DAT || index->IsGotSlotAccessed(ref.RelocationAddress)) result.push_back(ref); return result; } diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp index bf4c1f76d9..da812ba58e 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp @@ -1,9 +1,11 @@ #include #include #include +#include #include #include #include +#include namespace Relinker::UnusedNidFilter { @@ -29,7 +31,11 @@ private: namespace { -void buildGraph(std::span text, VirtualAddress textVaddr, VirtualAddress entryVaddr, const std::vector& extraEntries, const IRelativeRelocationIndex& relativeRelocations, const std::vector* data, bool followCodeAddresses, std::unordered_set& reachable, const std::function& progress) { +void buildGraph(std::span text, VirtualAddress textVaddr, VirtualAddress entryVaddr, const std::vector& extraEntries, const IRelativeRelocationIndex& relativeRelocations, const std::vector* data, bool followCodeAddresses, std::unordered_set& reachable, const std::function& progress, const std::function& isData) { + if (text.size() > std::numeric_limits::max() - textVaddr) + throw RelinkerException("Text section range exceeds the address space"); + std::size_t codeSize = text.size(); + while (codeSize > 0 && text[codeSize - 1] == 0) --codeSize; const Codegen::X64InstructionDecoder decoder; std::unordered_set visited; std::queue worklist; @@ -38,11 +44,18 @@ void buildGraph(std::span text, VirtualAddress textVaddr, Vi auto enqueue = [&](VirtualAddress va) { if (reachable.count(va) > 0) return; - if (va < textVaddr || va >= textVaddr + static_cast(text.size())) return; + if (isData && isData(va)) return; + if (va < textVaddr || va - textVaddr >= text.size()) return; if (!visited.insert(va).second) return; worklist.push(va); }; + auto zeroPaddingFollows = [&](VirtualAddress va) { + const auto offset = static_cast(va - textVaddr); + const auto aligned = (offset + 15) & ~std::size_t{15}; + return aligned != offset && aligned <= text.size() && std::all_of(text.begin() + offset, text.begin() + aligned, [](std::uint8_t value) { return value == 0; }); + }; + auto takeAddress = [&](VirtualAddress va) { if (data == nullptr) return; if (speculative) @@ -64,8 +77,9 @@ void buildGraph(std::span text, VirtualAddress textVaddr, Vi VirtualAddress va = worklist.front(); worklist.pop(); + const VirtualAddress blockEntry = va; - while (va >= textVaddr && va - textVaddr < text.size() && !reachable.contains(va)) { + while (va >= textVaddr && va - textVaddr < text.size() && (va == blockEntry || va - textVaddr < codeSize) && !reachable.contains(va) && !(isData && isData(va))) { std::size_t bufOff = static_cast(va - textVaddr); std::size_t available = text.size() - bufOff; if (available == 0) throw RelinkerException("CFG: zero available bytes at target", va); @@ -89,13 +103,13 @@ void buildGraph(std::span text, VirtualAddress textVaddr, Vi if (followCodeAddresses && info.HasRipRelativeDisp && text[bufOff + info.OpcodeOffset] == 0x8d && (info.RexPrefix & 8)) { std::int32_t displacement = 0; std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); - enqueue(static_cast(static_cast(nextVaddr) + displacement)); + enqueue(nextVaddr + static_cast(static_cast(displacement))); } if (data != nullptr && info.HasRipRelativeDisp && !info.IsTwoByteOpcode && info.Opcode == 0x8D) { std::int32_t displacement = 0; std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); - const auto base = static_cast(static_cast(nextVaddr) + displacement); + const auto base = nextVaddr + static_cast(static_cast(displacement)); takeAddress(base); for (VirtualAddress target : ReadRelativeTableTargets(*data, base, textVaddr, text.size())) takeAddress(target); } @@ -107,38 +121,36 @@ void buildGraph(std::span text, VirtualAddress textVaddr, Vi break; case ConditionalBranch: if (info.HasBranchTarget) - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + enqueue(nextVaddr + static_cast(info.BranchDisp)); break; case UnconditionalJump: fallThrough = false; if (info.HasRipRelativeDisp) { std::int32_t disp = 0; std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); - VirtualAddress slotVaddr = static_cast( - static_cast(nextVaddr) + disp - ); + VirtualAddress slotVaddr = nextVaddr + static_cast(static_cast(disp)); auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); if (resolved.has_value()) enqueue(*resolved); } else if (info.HasBranchTarget) { - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + enqueue(nextVaddr + static_cast(info.BranchDisp)); } break; case Call: if (info.HasRipRelativeDisp) { std::int32_t disp = 0; std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); - VirtualAddress slotVaddr = static_cast( - static_cast(nextVaddr) + disp - ); + VirtualAddress slotVaddr = nextVaddr + static_cast(static_cast(disp)); auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); if (resolved.has_value()) enqueue(*resolved); } else if (info.HasBranchTarget) { - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + enqueue(nextVaddr + static_cast(info.BranchDisp)); } + fallThrough = !zeroPaddingFollows(nextVaddr); break; case IndirectCall: + fallThrough = !zeroPaddingFollows(nextVaddr); break; case Return: case IndirectJump: @@ -154,8 +166,8 @@ void buildGraph(std::span text, VirtualAddress textVaddr, Vi } -std::unique_ptr BuildControlFlowGraph(std::span text, VirtualAddress textVaddr, VirtualAddress entryVaddr, const std::vector& extraEntries, const IRelativeRelocationIndex& relativeRelocations, bool followCodeAddresses, std::unordered_set& reachable, const std::function& progress) { - buildGraph(text, textVaddr, entryVaddr, extraEntries, relativeRelocations, nullptr, followCodeAddresses, reachable, progress); +std::unique_ptr BuildControlFlowGraph(std::span text, VirtualAddress textVaddr, VirtualAddress entryVaddr, const std::vector& extraEntries, const IRelativeRelocationIndex& relativeRelocations, bool followCodeAddresses, std::unordered_set& reachable, const std::function& progress, const std::function& isData) { + buildGraph(text, textVaddr, entryVaddr, extraEntries, relativeRelocations, nullptr, followCodeAddresses, reachable, progress, isData); return std::make_unique(&reachable); } @@ -192,7 +204,7 @@ std::unique_ptr BuildAddressTakenControlFlowGraph( const std::vector& data ) { std::unordered_set reachable; - buildGraph(std::span(text.data(), text.size()), textVaddr, entryVaddr, extraEntries, relativeRelocations, &data, false, reachable, {}); + buildGraph(std::span(text.data(), text.size()), textVaddr, entryVaddr, extraEntries, relativeRelocations, &data, false, reachable, {}, {}); return std::make_unique(std::move(reachable)); } diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp index fd61157b6a..5f1b754c86 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp @@ -39,7 +39,8 @@ public: const auto begin = encoded(position, tableEncoding, segment.MappedAddress); const auto fde = encoded(position, tableEncoding, segment.MappedAddress); if (position < segment.MappedAddress || position - segment.MappedAddress > segment.FileSize) throw RelinkerException("Strict filter: EH frame table exceeds segment", position); - result.push_back(readFde(begin, fde)); + auto region = readFde(begin, fde); + if (region.End != region.Begin) result.push_back(std::move(region)); } } return result; @@ -126,13 +127,22 @@ private: default: throw RelinkerException("Strict filter: unsupported unwind pointer format", field); } if (value == 0) return 0; + VirtualAddress base = 0; switch (encoding & 0x70) { case 0: break; - case 0x10: value += field; break; - case 0x30: value += dataBase; break; - case 0x40: value += functionBase; break; + case 0x10: base = field; break; + case 0x30: base = dataBase; break; + case 0x40: base = functionBase; break; default: throw RelinkerException("Strict filter: unsupported unwind pointer base", field); } + if ((encoding & 0x70) != 0) { + if ((encoding & 0x08) != 0) { + const bool negative = (value >> 63) != 0; + if (negative ? base < std::uint64_t{0} - value : value > std::numeric_limits::max() - base) + throw RelinkerException("Strict filter: unwind pointer address overflowed", field); + } + value = base + value; + } if ((encoding & 0x80) != 0 && !importSlots.contains(value)) { const auto pointer = pointers.find(value); if (pointer != pointers.end()) value = pointer->second; @@ -198,11 +208,11 @@ private: const auto end = checkedRangeEnd(position, length); const auto cieField = position; const auto cieOffset = read(position); - if (cieOffset > cieField) throw RelinkerException("Strict filter: invalid FDE CIE offset", address); + if (cieOffset == 0 || cieOffset > cieField) throw RelinkerException("Strict filter: invalid FDE CIE offset", address); const auto cie = readCie(cieField - cieOffset); const auto begin = encoded(position, cie.PointerEncoding); const auto size = encoded(position, cie.PointerEncoding & 0x0F); - if (begin != expectedBegin || size == 0 || size > std::numeric_limits::max() - begin) + if (begin != expectedBegin || size > std::numeric_limits::max() - begin) throw RelinkerException("Strict filter: inconsistent FDE function range", address); StrictCodeRegion region{begin, begin + size, {}}; if (cie.Personality != 0) region.ExtraTargets.push_back(cie.Personality); diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp index e95afa383b..947521c429 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp @@ -39,7 +39,7 @@ struct LoadSegment { }; bool inText(VirtualAddress va, VirtualAddress textVaddr, std::size_t textSize) { - return va >= textVaddr && va < textVaddr + static_cast(textSize); + return va >= textVaddr && va - textVaddr < static_cast(textSize); } } @@ -52,6 +52,9 @@ public: std::size_t textSize, const IRelativeRelocationIndex& relativeRelocations ) const override { + if (textSize > std::numeric_limits::max() - textVaddr) { + throw RelinkerException("Text section range exceeds the address space"); + } if (elfBytes.size() < 64) throw RelinkerException("ELF too small for header"); if (elfBytes[0] != 0x7f || elfBytes[1] != 'E' || elfBytes[2] != 'L' || elfBytes[3] != 'F') { diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp index cf9f0b94d5..9d1e5a3c2e 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp @@ -2,6 +2,7 @@ #include #include #include +#include namespace Relinker::UnusedNidFilter { @@ -23,11 +24,14 @@ std::unique_ptr BuildGotAccessIndex( const std::vector& text, VirtualAddress textVaddr ) { + if (text.size() > std::numeric_limits::max() - textVaddr) { + throw RelinkerException("Text section range exceeds the address space"); + } const Codegen::X64InstructionDecoder decoder; std::unordered_set accessed; for (VirtualAddress va : cfg.ReachableVaddrs()) { - if (va < textVaddr || va >= textVaddr + static_cast(text.size())) + if (va < textVaddr || va - textVaddr >= text.size()) continue; std::size_t bufOff = static_cast(va - textVaddr); std::size_t available = text.size() - bufOff; @@ -39,9 +43,10 @@ std::unique_ptr BuildGotAccessIndex( VirtualAddress nextVaddr = va + static_cast(info.Length); std::int32_t disp = 0; std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); - VirtualAddress target = static_cast( - static_cast(nextVaddr) + disp - ); + if (disp < 0 && nextVaddr < static_cast(-static_cast(disp))) continue; + if (disp > 0 && std::numeric_limits::max() - nextVaddr < static_cast(disp)) continue; + VirtualAddress target = nextVaddr + static_cast(static_cast(disp)); + if (info.AddressSize32) target = static_cast(target); accessed.insert(target); } diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/PltCompactor.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/PltCompactor.cpp index c21acd03ba..6632f2e0ae 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/PltCompactor.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/PltCompactor.cpp @@ -22,20 +22,29 @@ CompactedPlt CompactPlt(const std::vector& originalReferences, con if (reference.RelocationTypeValue != 7) continue; if (!originalSlots.contains(reference.RelocationAddress) || !newSlots.emplace(reference.RelocationAddress, result.SlotCount).second) throw RelinkerException("Strict filter: invalid retained PLT slot", reference.RelocationAddress); - reference.RelocationTableOffset = tableOffset + static_cast(result.SlotCount++) * 24; + const auto slotOffset = static_cast(result.SlotCount++) * 24; + if (tableOffset > std::numeric_limits::max() - slotOffset) + throw RelinkerException("Strict filter: PLT relocation table offset overflow", tableOffset); + reference.RelocationTableOffset = tableOffset + slotOffset; } std::unordered_set found; for (std::size_t offset = 0; offset + 16 <= text.size(); ++offset) { if (text[offset] != 0xFF || text[offset + 1] != 0x25 || text[offset + 6] != 0x68 || text[offset + 11] != 0xE9) continue; + if (offset + 6 > std::numeric_limits::max() - textVaddr) continue; + const auto next = textVaddr + offset + 6; std::int32_t displacement; std::memcpy(&displacement, text.data() + offset + 2, sizeof(displacement)); - const auto slot = textVaddr + offset + 6 + static_cast(static_cast(displacement)); + if (displacement < 0 && next < static_cast(-static_cast(displacement))) continue; + if (displacement > 0 && std::numeric_limits::max() - next < static_cast(displacement)) continue; + const auto slot = next + static_cast(static_cast(displacement)); const auto original = originalSlots.find(slot); if (original == originalSlots.end()) continue; std::uint32_t index; std::memcpy(&index, text.data() + offset + 7, sizeof(index)); if (index != original->second) throw RelinkerException("Strict filter: PLT thunk index disagrees with its relocation", textVaddr + offset); found.insert(slot); + if (textOffset > std::numeric_limits::max() - 7 || offset > std::numeric_limits::max() - textOffset - 7) + throw RelinkerException("Strict filter: PLT patch offset overflow", textOffset); if (const auto kept = newSlots.find(slot); kept != newSlots.end()) { std::vector bytes(sizeof(std::uint32_t)); std::memcpy(bytes.data(), &kept->second, bytes.size()); diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp index 1d0be749bc..d6a5eb9ffe 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace Relinker::UnusedNidFilter { @@ -90,7 +91,12 @@ void collectRelativeEntries( if (relType != R_X86_64_RELATIVE) continue; - out[static_cast(rOffset)] = static_cast(rAddend); + if (rAddend < 0) + throw RelinkerException("Relative relocation target address is negative", rOffset); + + auto [it, inserted] = out.emplace(static_cast(rOffset), static_cast(rAddend)); + if (!inserted && it->second != static_cast(rAddend)) + throw RelinkerException("Conflicting relative relocation targets for slot", rOffset); } } @@ -155,6 +161,7 @@ std::unique_ptr BuildRelativeRelocationIndex( } std::unordered_map targets; + bool hasDynamic = false; for (std::uint16_t i = 0; i < phCount; ++i) { std::size_t phPos = static_cast(phOff) + i * phEntSize; @@ -162,43 +169,107 @@ std::unique_ptr BuildRelativeRelocationIndex( std::uint32_t type = read32(elfBytes, phPos); if (type != PT_DYNAMIC) continue; + if (hasDynamic) throw RelinkerException("Multiple PT_DYNAMIC segments", phPos); + hasDynamic = true; std::uint64_t segOff = read64(elfBytes, phPos + 8); std::uint64_t segSz = read64(elfBytes, phPos + 32); if (segOff > elfBytes.size() || segSz > elfBytes.size() - segOff) throw RelinkerException("PT_DYNAMIC segment out of bounds"); + if (segSz % 16 != 0) throw RelinkerException("Invalid dynamic segment size", segOff); - VirtualAddress relaVa = 0; - std::uint64_t relaSz = 0; - VirtualAddress jmprelVa = 0; - std::uint64_t pltrelsz = 0; - bool relaSce = false; - bool jmprelSce = false; + std::optional relaVa; + std::optional relaSz; + std::optional jmprelVa; + std::optional pltrelsz; + bool hasSysvRela = false; + bool hasOsRela = false; + bool hasSysvRelaSz = false; + bool hasOsRelaSz = false; + bool hasSysvJmprel = false; + bool hasOsJmprel = false; + bool hasSysvPltrelsz = false; + bool hasOsPltrelsz = false; + bool terminated = false; for (std::uint64_t off = 0; off <= segSz && segSz - off >= 16; off += 16) { std::size_t pos = static_cast(segOff + off); std::int64_t tag = static_cast(read64(elfBytes, pos)); std::uint64_t val = read64(elfBytes, pos + 8); - if (tag == DT_NULL) break; - if (tag == DT_RELA || tag == DT_OS_RELA) { + if (tag == DT_NULL) { + terminated = true; + break; + } + if (tag == DT_RELA) { + if (hasOsRela) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_RELA"); + if (hasSysvRela && relaVa.value() != val) throw RelinkerException("Conflicting duplicate DT_RELA tags"); + hasSysvRela = true; relaVa = val; - relaSce = tag == DT_OS_RELA; } - if (tag == DT_RELASZ || tag == DT_OS_RELASZ) relaSz = val; - if (tag == DT_JMPREL || tag == DT_OS_JMPREL) { + if (tag == DT_OS_RELA) { + if (hasSysvRela) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_RELA"); + if (hasOsRela && relaVa.value() != val) throw RelinkerException("Conflicting duplicate DT_OS_RELA tags"); + hasOsRela = true; + relaVa = val; + } + if (tag == DT_RELASZ) { + if (hasOsRelaSz) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_RELASZ"); + if (hasSysvRelaSz && relaSz.value() != val) throw RelinkerException("Conflicting duplicate DT_RELASZ tags"); + hasSysvRelaSz = true; + relaSz = val; + } + if (tag == DT_OS_RELASZ) { + if (hasSysvRelaSz) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_RELASZ"); + if (hasOsRelaSz && relaSz.value() != val) throw RelinkerException("Conflicting duplicate DT_OS_RELASZ tags"); + hasOsRelaSz = true; + relaSz = val; + } + if (tag == DT_JMPREL) { + if (hasOsJmprel) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_JMPREL"); + if (hasSysvJmprel && jmprelVa.value() != val) throw RelinkerException("Conflicting duplicate DT_JMPREL tags"); + hasSysvJmprel = true; jmprelVa = val; - jmprelSce = tag == DT_OS_JMPREL; } - if (tag == DT_PLTRELSZ || tag == DT_OS_PLTRELSZ) pltrelsz = val; + if (tag == DT_OS_JMPREL) { + if (hasSysvJmprel) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_JMPREL"); + if (hasOsJmprel && jmprelVa.value() != val) throw RelinkerException("Conflicting duplicate DT_OS_JMPREL tags"); + hasOsJmprel = true; + jmprelVa = val; + } + if (tag == DT_PLTRELSZ) { + if (hasOsPltrelsz) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_PLTRELSZ"); + if (hasSysvPltrelsz && pltrelsz.value() != val) throw RelinkerException("Conflicting duplicate DT_PLTRELSZ tags"); + hasSysvPltrelsz = true; + pltrelsz = val; + } + if (tag == DT_OS_PLTRELSZ) { + if (hasSysvPltrelsz) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_PLTRELSZ"); + if (hasOsPltrelsz && pltrelsz.value() != val) throw RelinkerException("Conflicting duplicate DT_OS_PLTRELSZ tags"); + hasOsPltrelsz = true; + pltrelsz = val; + } } - collectRelativeEntries(elfBytes, loads, relaVa, relaSz, relaSce, sceTableBase, targets); - collectRelativeEntries(elfBytes, loads, jmprelVa, pltrelsz, jmprelSce, sceTableBase, targets); + if (!terminated) throw RelinkerException("Unterminated dynamic segment", segOff); - break; + if ((hasSysvRela && hasOsRelaSz) || (hasOsRela && hasSysvRelaSz)) + throw RelinkerException("Mismatched DT_RELA and DT_RELASZ tag variants"); + if (relaVa.has_value() != relaSz.has_value()) + throw RelinkerException("Incomplete relocation table metadata"); + + if ((hasSysvJmprel && hasOsPltrelsz) || (hasOsJmprel && hasSysvPltrelsz)) + throw RelinkerException("Mismatched DT_JMPREL and DT_PLTRELSZ tag variants"); + if (jmprelVa.has_value() != pltrelsz.has_value()) + throw RelinkerException("Incomplete PLT relocation table metadata"); + + if (relaVa.has_value()) + collectRelativeEntries(elfBytes, loads, *relaVa, *relaSz, hasOsRela, sceTableBase, targets); + if (jmprelVa.has_value()) + collectRelativeEntries(elfBytes, loads, *jmprelVa, *pltrelsz, hasOsJmprel, sceTableBase, targets); } return std::make_unique(std::move(targets)); } } + diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp index c2a24c806f..92b5f148e1 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp @@ -98,8 +98,8 @@ public: for (const auto& [addressTag, sizeTag] : {std::pair{25, 27}, std::pair{26, 28}, std::pair{32, 33}}) { const auto array = value(addressTag, 0x60000000 + addressTag); const auto size = value(sizeTag, 0x60000000 + sizeTag); - if (array.has_value() != size.has_value()) throw RelinkerException("Strict filter: incomplete constructor array metadata"); - if (!array) continue; + if (size && !array) throw RelinkerException("Strict filter: incomplete constructor array metadata"); + if (!size || *size == 0) continue; if (*size % 8 != 0) throw RelinkerException("Strict filter: invalid constructor array size", *array); for (std::uint64_t offset = 0; offset < *size; offset += 8) { const auto slot = *array + offset; diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp index 21229806dd..21667729d7 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp @@ -43,6 +43,8 @@ public: for (const auto& [slot, target] : input.Pointers) addAddressTaken(target); collectDataPointers(); collectRelativeTables(); + std::sort(addressTaken.begin(), addressTaken.end()); + addressTaken.erase(std::unique(addressTaken.begin(), addressTaken.end()), addressTaken.end()); std::deque pending; std::set live; const auto enqueue = [&](VirtualAddress target) { @@ -75,19 +77,22 @@ private: const StrictReachabilityInput& input; Codegen::X64InstructionDecoder decoder; std::map regions; - std::set addressTaken; + std::vector addressTaken; std::set tableBases; StrictReachabilityResult result; + Region* lastOwner = nullptr; bool isCode(VirtualAddress address) const { return address >= input.TextVaddr && address - input.TextVaddr < input.Text.size(); } Region& owner(VirtualAddress address) { + if (lastOwner != nullptr && address >= lastOwner->Begin && address < lastOwner->End) return *lastOwner; auto position = regions.upper_bound(address); if (position == regions.begin() || std::prev(position)->second.End <= address) throw RelinkerException("Strict filter: code address has no region", address); - return std::prev(position)->second; + lastOwner = &std::prev(position)->second; + return *lastOwner; } Instruction decode(VirtualAddress address, VirtualAddress end) const { @@ -141,7 +146,7 @@ private: } void addAddressTaken(VirtualAddress target) { - if (isCode(target) || input.ImportSlots.contains(target)) addressTaken.insert(target); + if (isCode(target) || input.ImportSlots.contains(target)) addressTaken.push_back(target); } void buildEdges() { @@ -202,6 +207,7 @@ private: void collectRelativeTables() { for (const auto base : tableBases) { + for (auto slot = base; input.ImportSlots.contains(slot) || input.Pointers.contains(slot); slot += sizeof(std::uint64_t)) addAddressTaken(slot); for (const auto target : ReadRelativeTableTargets(input.Data, base, input.TextVaddr, input.Text.size())) addAddressTaken(target); } } diff --git a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp index 6cf1c76066..dfd0a11798 100644 --- a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp +++ b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp @@ -6,15 +6,6 @@ namespace Relinker { using namespace Elfpatcher; -static constexpr std::uint8_t SYSCALL_BYTE0 = 0x0F; -static constexpr std::uint8_t SYSCALL_BYTE1 = 0x05; -static constexpr std::uint8_t INT80_BYTE0 = 0xCD; -static constexpr std::uint8_t INT80_BYTE1 = 0x80; -static constexpr std::uint8_t SYSENTER_BYTE0 = 0x0F; -static constexpr std::uint8_t SYSENTER_BYTE1 = 0x34; -static constexpr std::uint8_t SYSRET_BYTE0 = 0x0F; -static constexpr std::uint8_t SYSRET_BYTE1 = 0x07; - void ValidationPolicy::_initializeSupportedRelocationTypes() { _supportedRelocationTypes = { R_X86_64_NONE, @@ -37,16 +28,6 @@ void ValidationPolicy::_initializeSupportedRelocationTypes() { }; } -void ValidationPolicy::RegisterLibraryImport(const std::string& library) { - if (_supportedRelocationTypes.empty()) { - _initializeSupportedRelocationTypes(); - } - _importedLibraries.insert(library); -} - -void ValidationPolicy::ValidateSyscallAbsence() { -} - void ValidationPolicy::ValidateRelocationTypeSupported(const std::uint32_t relocationTypeValue, const FileByteOffset fileByteOffset) { if (_supportedRelocationTypes.empty()) { _initializeSupportedRelocationTypes(); @@ -59,49 +40,4 @@ void ValidationPolicy::ValidateRelocationTypeSupported(const std::uint32_t reloc } } -void ValidationPolicy::ValidateNidBelongsToLibrary(const std::string& Nid, const std::string& library) { - if (_importedLibraries.find(library) == _importedLibraries.end()) { - std::ostringstream msg; - msg << "NID \"" << Nid << "\" references library \"" << library - << "\" which is not in the NEEDED list"; - throw RelinkerException(msg.str(), 0); - } -} - -void ValidationPolicy::ValidateSceStructureSize(const ByteCount expectedSize, const ByteCount actualSize, const FileByteOffset fileByteOffset) { - if (actualSize != expectedSize) { - std::ostringstream msg; - msg << "SCE structure size mismatch at offset 0x" << std::hex << fileByteOffset - << ": expected " << std::dec << expectedSize << ", got " << actualSize; - throw RelinkerException(msg.str(), fileByteOffset); - } -} - -void ValidationPolicy::ValidateDynamicFieldInterpretable(const std::string& fieldName, const FileByteOffset fileByteOffset) { - std::ostringstream msg; - msg << "Dynamic field \"" << fieldName - << "\" cannot be interpreted at offset 0x" << std::hex << fileByteOffset; - throw RelinkerException(msg.str(), fileByteOffset); -} - -void ValidationPolicy::ValidateNoSyscallInstructions( - const std::vector& CodeSection, const FileByteOffset codeOffset) { - for (std::size_t i = 0; i + 1 < CodeSection.size(); ++i) { - const std::uint8_t b0 = CodeSection[i]; - const std::uint8_t b1 = CodeSection[i + 1]; - - bool isSyscall = (b0 == SYSCALL_BYTE0 && b1 == SYSCALL_BYTE1); - bool isInt80 = (b0 == INT80_BYTE0 && b1 == INT80_BYTE1); - bool isSysenter = (b0 == SYSENTER_BYTE0 && b1 == SYSENTER_BYTE1); - bool isSysret = (b0 == SYSRET_BYTE0 && b1 == SYSRET_BYTE1); - - if (isSyscall || isInt80 || isSysenter || isSysret) { - FileByteOffset instrOffset = codeOffset + i; - std::ostringstream msg; - msg << "Forbidden syscall instruction at offset 0x" << std::hex << instrOffset; - throw RelinkerException(msg.str(), instrOffset); - } - } -} - } diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index f1734eeba5..49a209c9d4 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -8,86 +8,11 @@ #include #include #include +#include #include namespace Relinker { -std::vector GuestImageReader::ReadModuleNames(const std::vector& bytes) const { - if (bytes.size() < 64 || Io::ReadU32(bytes, 0) != 0x464c457f || bytes[4] != 2 || bytes[5] != 1 || bytes[6] != 1 || Io::ReadU16(bytes, 18) != 62) - throw Domain::RelinkerException("Expected little-endian ELF64 x86-64 module metadata"); - const auto type = Io::ReadU16(bytes, 16); - if (type != 3 && type != 0xfe18) return {}; - if (Io::ReadU16(bytes, 52) != 64 || Io::ReadU16(bytes, 54) != 56 || Io::ReadU16(bytes, 56) == 0) - throw Domain::RelinkerException("Invalid module metadata program headers"); - const auto range = [&](std::uint64_t offset, std::uint64_t size) { - if (offset > bytes.size() || size > bytes.size() - offset) throw Domain::RelinkerException("Module metadata exceeds file"); - }; - range(Io::ReadU64(bytes, 32), static_cast(Io::ReadU16(bytes, 56)) * 56); - ElfReader reader(bytes); - const auto headers = reader.ReadProgramHeaders(); - const Domain::ProgramHeader* dynamic = nullptr; - const Domain::ProgramHeader* dynlib = nullptr; - for (const auto& header : headers) { - if (header.Type == 2) { - if (dynamic) throw Domain::RelinkerException("Duplicate module metadata dynamic segment"); - dynamic = &header; - } - if (header.Type == 0x61000000) { - if (dynlib) throw Domain::RelinkerException("Duplicate module metadata dynamic data"); - dynlib = &header; - } - } - if (!dynamic) throw Domain::RelinkerException("Missing module metadata dynamic segment"); - std::map strings; - std::vector modules; - for (const auto& tag : reader.ReadDynamicTags(*dynamic)) { - if (tag.Tag == 0x6100000d || tag.Tag == 0x61000043) modules.push_back(tag.Value); - else if (tag.Tag == 5 || tag.Tag == 10 || tag.Tag == 0x61000035 || tag.Tag == 0x61000037) { - if (!strings.emplace(tag.Tag, tag.Value).second) throw Domain::RelinkerException("Duplicate module metadata string tag"); - } - } - if (modules.empty()) return {}; - const auto get = [&](std::uint64_t standard, std::uint64_t sce) { - if (strings.contains(standard) == strings.contains(sce)) throw Domain::RelinkerException("Missing or ambiguous module metadata string tag"); - return strings.at(strings.contains(standard) ? standard : sce); - }; - const auto size = get(10, 0x61000037); - const auto address = get(5, 0x61000035); - std::uint64_t offset = 0; - if (strings.contains(5)) { - bool found = false; - for (const auto& header : headers) { - if (header.Type != 1 || address < header.MappedAddress || address - header.MappedAddress > header.FileSize || size > header.FileSize - (address - header.MappedAddress)) continue; - range(header.Offset, header.FileSize); - if (found) throw Domain::RelinkerException("Ambiguous module metadata string mapping"); - offset = header.Offset + address - header.MappedAddress; - found = true; - } - if (!found) throw Domain::RelinkerException("Unmapped module metadata strings"); - } else { - if (!dynlib || address > dynlib->FileSize || size > dynlib->FileSize - address) throw Domain::RelinkerException("Module metadata strings exceed dynamic data"); - range(dynlib->Offset, dynlib->FileSize); - offset = dynlib->Offset + address; - } - range(offset, size); - if (size == 0 || bytes[offset] != 0) throw Domain::RelinkerException("Invalid module metadata strings"); - std::vector names; - std::set ids; - for (const auto value : modules) { - const auto nameOffset = value & 0xffffffffu; - if (nameOffset >= size) throw Domain::RelinkerException("Module metadata string offset out of bounds"); - const auto start = bytes.begin() + static_cast(offset + nameOffset); - const auto end = bytes.begin() + static_cast(offset + size); - const auto zero = std::find(start, end, 0); - if (zero == end) throw Domain::RelinkerException("Unterminated module metadata string"); - const std::string name(start, zero); - if (name.empty() || name.find_first_of("/\\:$\r\n") != std::string::npos) throw Domain::RelinkerException("Invalid module metadata name"); - if (!ids.insert(value >> 48).second) throw Domain::RelinkerException("Duplicate module metadata export ID"); - names.push_back(name); - } - return names; -} - GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector bytes, const bool windows) const try { const auto fail = [&](const std::string& message) { throw Domain::RelinkerException(Io::Utf8Path(path) + ": " + message); }; const auto range = [&](std::uint64_t offset, std::uint64_t size) { @@ -140,6 +65,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector std::map tags; std::vector needed; std::vector moduleImports; + std::vector ps4ModuleImports; std::vector moduleExports; bool terminated = false; for (std::uint64_t offset = dynamic->Offset; offset < dynamic->Offset + dynamic->FileSize; offset += 16) { @@ -148,6 +74,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (tag == 0) { terminated = true; break; } if (tag == 1) needed.push_back(value); else if (tag == 0x61000045) moduleImports.push_back(value); + else if (tag == 0x6100000f) ps4ModuleImports.push_back(value); else if (tag == 0x6100000d || tag == 0x61000043) moduleExports.push_back(value); else if (tag < 0x60000000 || tag == 0x6ffffef5 || tag == 0x6100003f || (tag >= 0x61000027 && tag <= 0x6100003b)) { if (!tags.emplace(tag, value).second) fail("Duplicate dynamic tag " + std::to_string(tag)); @@ -157,7 +84,8 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector for (const auto unsupported : {15u, 16u, 17u, 18u, 19u, 22u, 35u, 36u, 37u}) { if (tags.contains(unsupported)) fail("Unsupported dynamic tag " + std::to_string(unsupported)); } - if (tags.contains(30) && (tags.at(30) & ~8ull) != 0) fail("Unsupported guest dynamic flags"); + if (tags.contains(30) && (tags.at(30) & ~(2ull | 8ull)) != 0) fail("Unsupported guest dynamic flags"); + image.Symbolic = tags.contains(30) && (tags.at(30) & 2ull) != 0; const auto mapped = [&](std::uint64_t address, std::uint64_t size, std::uint32_t flags) { for (const auto& header : image.Headers) { if (header.Type == 1 && (header.Flags & flags) == flags && address >= header.MappedAddress && address - header.MappedAddress <= header.MemorySize && size <= header.MemorySize - (address - header.MappedAddress)) return; @@ -229,7 +157,8 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector image.Dynamic.DynSymData.assign(bytes.begin() + symOffset, bytes.begin() + symOffset + symSize); image.Dynamic.DynStrData.push_back(0); std::map importModules; - for (const auto value : moduleImports) + if (!moduleImports.empty() && !ps4ModuleImports.empty()) fail("Guest module mixes PS4 and PS5 needed-module tags"); + for (const auto value : moduleImports.empty() ? ps4ModuleImports : moduleImports) if (!importModules.emplace(value >> 48, string(value & 0xffffffffu)).second) fail("Duplicate import module ID"); std::vector neededLibraries; for (const auto offset : needed) neededLibraries.push_back(string(offset)); @@ -253,7 +182,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector image.Symbols.push_back({{}, 0, 0, 0, 0, 0}); continue; } - if (offset != 0 && ((info >> 4) > 2 || (visibility & ~3u) != 0 || ((info & 15) != 0 && (info & 15) != 1 && (info & 15) != 2 && (info & 15) != 6))) fail("Unsupported symbol attributes: " + name); + if (offset != 0 && ((info >> 4) > 2 || (visibility & ~3u) != 0 || ((info & 15) != 0 && (info & 15) != 1 && (info & 15) != 2 && info != 3 && (info & 15) != 6))) fail("Unsupported symbol attributes: " + name); if (section != 0 && (info >> 4) != 0 && visibility != 1 && visibility != 2) { const auto [found, inserted] = exports.emplace(name, image.Symbols.size()); if (name.empty() || (!inserted && (!windows || !IdenticalOrdinaryFunctionExport(image.Symbols[found->second], symbol)))) fail("Duplicate or empty export after stripping #: " + name); @@ -285,15 +214,26 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (name.empty() || name.find_first_of("\\:$\r\n") != std::string::npos) fail("Invalid dependency: " + name); if (dependencies.insert(name).second) image.Dependencies.push_back(name); } - std::map relocationTargets; - const auto copyRelocations = [&](std::uint64_t addressTag, std::uint64_t sceAddressTag, std::uint64_t sizeTag, std::uint64_t sceSizeTag, std::vector& output, bool plt) { - if (!tags.contains(addressTag) && !tags.contains(sceAddressTag) && !tags.contains(sizeTag) && !tags.contains(sceSizeTag)) return; + const auto relocationRange = [&](std::uint64_t addressTag, std::uint64_t sceAddressTag, std::uint64_t sizeTag, std::uint64_t sceSizeTag) -> std::pair { + if (!tags.contains(addressTag) && !tags.contains(sceAddressTag) && !tags.contains(sizeTag) && !tags.contains(sceSizeTag)) return {0, 0}; const auto size = get(sizeTag, sceSizeTag); if (size % 24 != 0) fail("Invalid relocation table size"); - const auto offset = table(addressTag, sceAddressTag, size); - if (plt && size != 0 && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); + return {table(addressTag, sceAddressTag, size), size}; + }; + if (tags.contains(7) || tags.contains(0x6100002f)) { + if (get(9, 0x61000033) != 24) fail("Invalid RELA entry size"); + } + const auto [relaOffset, relaSize] = relocationRange(7, 0x6100002f, 8, 0x61000031); + const auto [pltOffset, pltSize] = relocationRange(23, 0x61000029, 2, 0x6100002d); + if (pltSize != 0 && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); + if (relaSize != 0 && pltSize != 0 && relaOffset < pltOffset + pltSize && pltOffset < relaOffset + relaSize + && relaOffset % 24 != pltOffset % 24) + fail("Unaligned overlapping relocation tables"); + std::map relocationTargets; + const auto copyRelocations = [&](std::uint64_t offset, std::uint64_t size, std::vector& output, bool plt) { output.reserve(size); for (std::uint64_t index = 0; index < size; index += 24) { + if (!plt && offset + index >= pltOffset && offset + index - pltOffset < pltSize) continue; const auto info = Io::ReadU64(bytes, offset + index + 8); const auto relocation = static_cast(info); if (relocation == 0) continue; @@ -304,28 +244,43 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const auto next = relocationTargets.lower_bound(target); if ((next != relocationTargets.end() && next->first < target + 8) || (next != relocationTargets.begin() && std::prev(next)->second > target)) fail("Overlapping guest relocations"); relocationTargets.emplace(target, target + 8); + bool ownTlsBlock = false; if (relocation == 8) { - if ((info >> 32) != 0) fail("RELATIVE relocation has a symbol"); + if ((info >> 32) != 0) { + const auto& symbol = image.Symbols[info >> 32]; + if (symbol.Info == 3) { + if (symbol.Value != 0) fail("RELATIVE relocation has a symbol"); + } else if (symbol.Section == 0 || addend > static_cast(std::numeric_limits::max()) || symbol.Value != addend) + fail("RELATIVE relocation has a symbol"); + mapped(addend, 0, 0); + Io::AppendU64(output, target); + Io::AppendU64(output, relocation); + Io::AppendU64(output, addend); + continue; + } mapped(addend, 0, 0); } else if (relocation == 1 || relocation == 6 || relocation == 7) { const auto& symbol = image.Symbols[info >> 32]; if ((info >> 32) == 0 || (symbol.Info & 15) == 6 || (relocation != 1 && addend != 0) || (symbol.Section == 0 && symbol.Name.empty())) fail("Invalid guest symbol relocation"); } else { const auto& symbol = image.Symbols[info >> 32]; - if (((info >> 32) == 0 && tls == nullptr) || ((info >> 32) != 0 && (symbol.Info & 15) != 6) || (relocation == 16 && addend != 0)) fail("Invalid guest TLS relocation"); + ownTlsBlock = (info >> 32) != 0 && (relocation == 16 || relocation == 17) && symbol.Info == 3 && symbol.Section == 0 && symbol.Value == 0 && tls != nullptr; + if (((info >> 32) == 0 && tls == nullptr) || ((info >> 32) != 0 && (symbol.Info & 15) != 6 && !ownTlsBlock) || (relocation == 16 && addend != 0)) fail("Invalid guest TLS relocation"); } + const auto position = output.size(); output.insert(output.end(), bytes.begin() + offset + index, bytes.begin() + offset + index + 24); + if (ownTlsBlock) Io::WriteU64(output, position + 8, relocation); } }; - if (tags.contains(7) || tags.contains(0x6100002f)) { - if (get(9, 0x61000033) != 24) fail("Invalid RELA entry size"); - } - copyRelocations(7, 0x6100002f, 8, 0x61000031, image.Dynamic.RelaData, false); - copyRelocations(23, 0x61000029, 2, 0x6100002d, image.Dynamic.RelaPltData, true); + copyRelocations(relaOffset, relaSize, image.Dynamic.RelaData, false); + copyRelocations(pltOffset, pltSize, image.Dynamic.RelaPltData, true); if (tags.contains(3) || tags.contains(0x61000027)) image.Got = get(3, 0x61000027); - if (tags.contains(12)) image.Init = tags.at(12); + if (tags.contains(12)) { + image.Init = tags.at(12); + image.HasInit = true; + } if (tags.contains(13)) image.Fini = tags.at(13); - if (image.Init != 0) mapped(image.Init, 1, 1); + if (image.HasInit) mapped(image.Init, 1, 1); if (image.Fini != 0) mapped(image.Fini, 1, 1); const auto readLifecycleArray = [&](std::uint64_t addressTag, std::uint64_t sizeTag, std::vector& entries) { if (!tags.contains(addressTag) && !tags.contains(sizeTag)) return; @@ -359,7 +314,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector }; readLifecycleArray(25, 27, image.InitArray); readLifecycleArray(26, 28, image.FiniArray); - if (tags.contains(33) && tags.at(33) != 0 && image.Init == 0) fail("Guest PREINIT_ARRAY has no module initializer"); + if (tags.contains(33) && tags.at(33) != 0 && !image.HasInit) fail("Guest PREINIT_ARRAY has no module initializer"); image.Bytes = std::move(bytes); return image; } catch (Domain::RelinkerException& error) { diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index d7598f0a26..1ffd34e19d 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include #include @@ -68,7 +70,9 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (excludedModules.contains(Io::Utf8Path(path.filename()))) throw Domain::RelinkerException("Excluded module supplied to converter: " + Io::Utf8Path(path)); if (discovered.contains(path)) throw Domain::RelinkerException("Duplicate module path: " + Io::Utf8Path(path)); paths.push_back(path); - discovered.emplace(path, GuestImageReader().Read(path, std::move(module.Bytes), windows)); + auto image = GuestImageReader().Read(path, std::move(module.Bytes), windows); + image.PatchedRanges = std::move(module.PatchedRanges); + discovered.emplace(path, std::move(image)); } const auto neededNames = ReadNeededNames(dynamic); std::set missingNeeded; @@ -95,20 +99,25 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path std::set outputNames; for (const auto& path : paths) { auto image = std::move(discovered.at(path)); - if (image.OutputName.find_first_of("$\r\n") != std::string::npos) throw Domain::RelinkerException("Unsupported guest filename: " + image.OutputName); + const auto fail = [&](const std::string& message) { + Domain::RelinkerException error(message); + error.InputPath = Io::Utf8Path(path); + throw error; + }; + if (image.OutputName.find_first_of("$\r\n") != std::string::npos) fail("Unsupported guest filename: " + image.OutputName); std::string folded = image.OutputName; if (windows) { for (auto& value : folded) { - if (static_cast(value) >= 128 || value == ':' || value == '$') throw Domain::RelinkerException("Unsupported Windows guest filename: " + image.OutputName); + if (static_cast(value) >= 128 || value == ':' || value == '$') fail("Unsupported Windows guest filename: " + image.OutputName); if (value >= 'A' && value <= 'Z') value = static_cast(value + ('a' - 'A')); } } - if (!outputNames.insert(folded).second) throw Domain::RelinkerException("Conflicting guest output filename: " + image.OutputName); + if (!outputNames.insert(folded).second) fail("Conflicting guest output filename: " + image.OutputName); for (const auto& symbol : image.Symbols) { if (symbol.Section == 0 || symbol.Section == AbsoluteSection || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; auto& providers = exports[symbol.Name]; const bool repeated = !providers.empty() && providers.back().Provider == images.size(); - if (!windows && repeated) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + Io::Utf8Path(path) + " and " + Io::Utf8Path(images[providers.front().Provider].SourcePath)); + if (!windows && repeated) fail("Duplicate guest export after stripping #: " + symbol.Name + " in " + Io::Utf8Path(path) + " and " + Io::Utf8Path(images[providers.front().Provider].SourcePath)); if (!repeated) providers.push_back({images.size(), static_cast(symbol.Info & 15)}); if (!windows && providers.size() > 1) for (const auto& provider : providers) sharedExports[symbol.Name].insert(provider.Provider); @@ -167,10 +176,14 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto declaresHost = [&](const std::string& library) { return !windows && !library.empty() && findGuest(library) == guestNames.end(); }; if (dynamic.DynSymData.size() % 24 != 0) throw Domain::RelinkerException("Invalid executable symbol table"); std::vector executableModules(dynamic.DynSymData.size() / 24); + std::vector executableReferences(executableModules.size()); + std::set executableProviders; for (const auto* table : {&dynamic.RelaData, &dynamic.RelaPltData}) { if (table->size() % 24 != 0) throw Domain::RelinkerException("Invalid executable relocation table"); for (std::size_t offset = 0; offset < table->size(); offset += 24) { - const auto symbol = Io::ReadU64(*table, offset + 8) >> 32; + const auto info = Io::ReadU64(*table, offset + 8); + const auto symbol = info >> 32; + if (symbol != 0 && symbol < executableReferences.size() && (info & 0xffffffffu) != 0) executableReferences[symbol] = true; const auto module = dynamic.ImportModules.find(Io::ReadU64(*table, offset)); if (symbol != 0 && symbol < executableModules.size() && module != dynamic.ImportModules.end()) executableModules[symbol] = module->second; } @@ -197,7 +210,11 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } } if (needed != 1) rejectSharedImport(base, Io::Utf8Path(inputPath)); - if (!windows && exports.contains(name)) rename(dynamic.DynSymData, dynamic.DynStrData, offset / 24, name); + const auto found = exports.find(name); + if (!windows && found != exports.end()) { + rename(dynamic.DynSymData, dynamic.DynStrData, offset / 24, name); + if (executableReferences[offset / 24] && found->second.size() == 1) executableProviders.insert(found->second.front().Provider); + } } std::vector> dependencies(images.size()); std::vector> systemImports(images.size()); @@ -211,9 +228,20 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto found = findGuest(name); if (found != guestNames.end() && found->second != index) dependencies[index].insert(found->second); } + std::set referencedSymbols; + for (const auto* table : {&images[index].Dynamic.RelaData, &images[index].Dynamic.RelaPltData}) { + for (std::size_t offset = 0; offset < table->size(); offset += 24) { + const auto info = Io::ReadU64(*table, offset + 8); + if ((info & 0xffffffffu) != 0) referencedSymbols.insert(info >> 32); + } + } for (std::size_t symbolIndex = 0; symbolIndex < images[index].Symbols.size(); ++symbolIndex) { const auto& symbol = images[index].Symbols[symbolIndex]; - if (symbol.Section != 0 || symbol.Name.empty() || declaresHost(symbol.Library)) continue; + if (symbol.Section != 0 || symbol.Name.empty()) continue; + if (declaresHost(symbol.Library)) { + systemImports[index].insert(symbolIndex); + continue; + } const auto declared = symbol.Library.empty() ? guestNames.end() : findGuest(symbol.Library); const auto shared = sharedExports.find(symbol.Name); if (shared == sharedExports.end() || declared == guestNames.end() || !shared->second.contains(declared->second)) @@ -229,19 +257,35 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } if (provider != nullptr) { if ((symbol.Info & 15) != 0 && (symbol.Info & 15) != provider->Type) throw Domain::RelinkerException("Guest import/export type mismatch: " + symbol.Name); - if (provider->Provider != index) dependencies[index].insert(provider->Provider); + if (provider->Provider != index && referencedSymbols.contains(symbolIndex)) dependencies[index].insert(provider->Provider); } else if (windows && (symbol.Info & 15) == 6) throw Domain::RelinkerException("Windows guest TLS import requires a guest TLS export: " + symbol.Name); if (provider == nullptr) systemImports[index].insert(symbolIndex); } } std::vector required(images.size()); + const std::function require = [&](const std::size_t index) { + if (required[index]) return; + required[index] = true; + for (const auto dependency : dependencies[index]) require(dependency); + }; + const auto isRuntime = [&](const std::size_t index) { + return std::find(runtimeDirectories.begin(), runtimeDirectories.end(), images[index].SourcePath.parent_path().lexically_normal()) != runtimeDirectories.end(); + }; + const auto standardDirectory = [&](const std::size_t index) { + const auto directory = images[index].SourcePath.parent_path().lexically_normal(); + return directory == (root / "sce_module").lexically_normal() || directory == (root / "sce_modules").lexically_normal() || directory == (root / "prx").lexically_normal(); + }; for (const auto& name : neededNames) { const auto found = findGuest(name); - if (found != guestNames.end()) required[found->second] = true; + if (found != guestNames.end() && (!windows || standardDirectory(found->second))) require(found->second); + } + for (const auto provider : executableProviders) require(provider); + for (std::size_t index = 0; index < images.size(); ++index) { + if (!isRuntime(index)) require(index); + if (windows) for (const auto dependency : dependencies[index]) require(dependency); } - for (const auto& needed : dependencies) for (const auto dependency : needed) required[dependency] = true; const auto deferred = [&](const std::size_t index) { - return !required[index] && std::find(runtimeDirectories.begin(), runtimeDirectories.end(), images[index].SourcePath.parent_path().lexically_normal()) != runtimeDirectories.end(); + return !required[index] && isRuntime(index); }; const auto appDirectory = [&](const GuestImage& image) { const auto directory = image.SourcePath.parent_path().lexically_relative(root).generic_string(); @@ -268,15 +312,29 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path }; for (std::size_t index = 0; index < images.size(); ++index) visit(index); std::vector hostLibraries; + std::vector> moduleHostLibraries(images.size()); std::set uniqueHosts; - const auto addHost = [&](const std::string& name) { + const auto addHost = [&](const std::string& name, const bool startup) { if (findGuest(name) != guestNames.end()) return; if (name.empty() || name.find_first_of("/\\:$") != std::string::npos) throw Domain::RelinkerException("Invalid host dependency: " + name); - if (uniqueHosts.insert(name).second) hostLibraries.push_back(name); + if (startup && uniqueHosts.insert(name).second) hostLibraries.push_back(name); }; - for (const auto& name : neededNames) addHost(name); - for (const auto& image : images) for (const auto& dependency : image.Dependencies) addHost(dependency); + for (const auto& name : neededNames) addHost(name, true); + for (std::size_t index = 0; index < images.size(); ++index) { + auto& hosts = moduleHostLibraries[index]; + for (const auto& dependency : images[index].Dependencies) { + addHost(dependency, windows || !deferred(index)); + if (findGuest(dependency) == guestNames.end()) hosts.push_back(dependency); + } + if (!windows && !macos && images[index].HasInit) { + if (std::find(hosts.begin(), hosts.end(), "libkernel.prx") == hosts.end()) hosts.push_back("libkernel.prx"); + if (!deferred(index) && uniqueHosts.insert("libkernel.prx").second) hostLibraries.push_back("libkernel.prx"); + } + if (std::find(hosts.begin(), hosts.end(), "libSceLibcInternal.prx") != hosts.end() && + std::find(hosts.begin(), hosts.end(), "libc.prx") == hosts.end()) hosts.push_back("libc.prx"); + } if (uniqueHosts.contains("libSceLibcInternal.prx") && uniqueHosts.insert("libc.prx").second) hostLibraries.push_back("libc.prx"); + dynamic.NeededLibraries = hostLibraries; dynamic.DynamicSegmentData.clear(); const auto addNeeded = [&](const std::string& name) { Io::AppendU64(dynamic.DynamicSegmentData, 1); @@ -288,12 +346,6 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !runPath.starts_with("$ORIGIN:") && !runPath.starts_with('/')) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); const auto outputDirectory = outputPath.parent_path().lexically_normal(); std::vector artifacts; - std::vector moduleHostLibraries; - std::set uniqueModuleHosts; - for (const auto& image : images) for (const auto& dependency : image.Dependencies) { - if (findGuest(dependency) == guestNames.end() && uniqueModuleHosts.insert(dependency).second) moduleHostLibraries.push_back(dependency); - } - if (uniqueModuleHosts.contains("libSceLibcInternal.prx") && uniqueModuleHosts.insert("libc.prx").second) moduleHostLibraries.push_back("libc.prx"); for (const auto index : order) { auto& image = images[index]; progress.Report({Domain::ProgressStage::Modules, Io::Utf8Path(image.SourcePath), artifacts.size(), order.size()}); @@ -303,12 +355,24 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path auto converted = Codegen::MakeAmd64OnlyConverter(toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel, [&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Lowering, Io::Utf8Path(image.SourcePath), completed, total}); })->Convert(std::move(image.Bytes), image.Headers); image.Trampolines = std::move(converted.Trampolines); image.Bytes = std::move(converted.Bytes); + if (!image.PatchedRanges.empty()) GamePatch::CheckOverlaps(Io::Utf8Path(image.SourcePath), image.PatchedRanges, {.Reports = converted.Reports, .Trampolines = image.Trampolines}); } + if (!image.PatchedRanges.empty()) GamePatch::CheckOverlaps(Io::Utf8Path(image.SourcePath), image.PatchedRanges, {.RelaData = image.Dynamic.RelaData, .RelaPltData = image.Dynamic.RelaPltData}); + const bool sceImage = std::any_of(image.Headers.begin(), image.Headers.end(), [](const Domain::ProgramHeader& header) { return header.Type == 0x61000000; }); + const auto unwindFunctions = sceImage ? UnusedNidFilter::ReadExceptionFunctions(image.Bytes, image.Headers, {}, {}) : std::vector{}; for (const auto& header : codeHeaders) { - const std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); - for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize, Io::Utf8Path(image.SourcePath))) { + std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); + if (sceImage) + if (const auto end = FindSceCodeEnd(code, header.MappedAddress, unwindFunctions, image.Got, image.Dynamic.RelaPltData.size())) + code.resize(*end - header.MappedAddress); + const auto unwindRanges = sceImage ? SceUnwindRanges(code, header.MappedAddress, unwindFunctions) : std::vector{}; + for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, code.size(), Io::Utf8Path(image.SourcePath), unwindRanges)) { if (offset > code.size() || code.size() - offset < 2u) throw Domain::RelinkerException("Syscall trap patch exceeds guest executable segment", header.MappedAddress + offset); + if (!image.PatchedRanges.empty()) { + const GamePatch::WriterRange trap{header.Offset + offset, 2, "syscall trap"}; + GamePatch::CheckOverlaps(Io::Utf8Path(image.SourcePath), image.PatchedRanges, {.Traps = std::span(&trap, 1)}); + } image.Bytes[header.Offset + offset] = 0x0F; image.Bytes[header.Offset + offset + 1] = 0x0B; } @@ -323,7 +387,8 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path runtime.Names = {Io::Utf8Path(image.SourcePath.filename()), image.Soname}; runtime.DeferInitialization = deferred(index); std::vector output; - if (windows) output = Elfpatcher::GuestModuleWriter().WriteWindows(image, runtime); + const auto message = [&](std::string_view text) { progress.Message(text); }; + if (windows) output = Elfpatcher::GuestModuleWriter().WriteWindows(image, runtime, message); else { std::string guestRunPath = runPath; const auto relativeRoot = outputDirectory.lexically_relative(destination.lexically_normal()).generic_string(); @@ -338,8 +403,8 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto dependencyPath = images[dependency].SourcePath.parent_path() / images[dependency].OutputName; needed.push_back("$ORIGIN/" + dependencyPath.lexically_relative(image.SourcePath.parent_path()).generic_string()); } - needed.insert(needed.end(), moduleHostLibraries.begin(), moduleHostLibraries.end()); - output = macos ? Elfpatcher::GuestModuleWriter().WriteMacOs(image, needed, guestRunPath) : Elfpatcher::GuestModuleWriter().WriteLinux(image, needed, guestRunPath); + needed.insert(needed.end(), moduleHostLibraries[index].begin(), moduleHostLibraries[index].end()); + output = macos ? Elfpatcher::GuestModuleWriter().WriteMacOs(image, needed, guestRunPath, message) : Elfpatcher::GuestModuleWriter().WriteLinux(image, needed, guestRunPath); } if (windows) { for (const auto& [name, provider] : guestNames) { @@ -349,14 +414,19 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path std::vector imports; for (const auto* table : {&image.Dynamic.RelaData, &image.Dynamic.RelaPltData}) { for (std::size_t position = 0; position < table->size(); position += 24) { - const auto symbolIndex = static_cast(Io::ReadU64(*table, position + 8) >> 32); + const auto info = Io::ReadU64(*table, position + 8); + if ((info & 0xffffffffu) == 0) continue; + const auto symbolIndex = static_cast(info >> 32); if (!systemImports[index].contains(symbolIndex)) continue; const auto& symbol = image.Symbols[symbolIndex]; - imports.push_back({symbol.Name, symbol.Library, {}, 0, {}, Io::ReadU64(*table, position), {}, false}); + imports.push_back({symbol.Name, symbol.Library, {}, 0, {}, Io::ReadU64(*table, position), {}, false, (symbol.Info >> 4) == 2}); } } dynamic.GuestModules.push_back(std::move(runtime)); - artifacts.push_back({target, std::move(output), std::move(imports), image.SourcePath}); + std::vector neededLibraries; + for (const auto& name : image.Dependencies) + if (findGuest(name) == guestNames.end()) neededLibraries.push_back(name); + artifacts.push_back({target, std::move(output), std::move(imports), image.SourcePath, std::move(neededLibraries)}); progress.Report({Domain::ProgressStage::Modules, Io::Utf8Path(image.SourcePath), artifacts.size(), order.size()}); } return artifacts; diff --git a/core/relinker/relinker/src/output/CallRegistryWriter.cpp b/core/relinker/relinker/src/output/CallRegistryWriter.cpp index e598056d6e..8878c6496c 100644 --- a/core/relinker/relinker/src/output/CallRegistryWriter.cpp +++ b/core/relinker/relinker/src/output/CallRegistryWriter.cpp @@ -1,4 +1,5 @@ #include +#include #include namespace Relinker { @@ -7,19 +8,57 @@ static std::string _jsonString(const std::string& s) { std::string out; out.reserve(s.size() + 2); out.push_back('"'); - for (char c : s) { - if (c == '"') out += "\\\""; - else if (c == '\\') out += "\\\\"; - else if (c == '\n') out += "\\n"; - else if (c == '\r') out += "\\r"; - else if (c == '\t') out += "\\t"; - else if (static_cast(c) < 0x20) { - constexpr char digits[] = "0123456789abcdef"; - const auto value = static_cast(c); - out += "\\u00"; - out.push_back(digits[value >> 4]); - out.push_back(digits[value & 0x0f]); - } else out.push_back(c); + for (std::size_t i = 0; i < s.size();) { + const auto c = static_cast(s[i]); + if (c < 0x80) { + if (c == '"') out += "\\\""; + else if (c == '\\') out += "\\\\"; + else if (c == '\n') out += "\\n"; + else if (c == '\r') out += "\\r"; + else if (c == '\t') out += "\\t"; + else if (c < 0x20) { + constexpr char digits[] = "0123456789abcdef"; + out += "\\u00"; + out.push_back(digits[c >> 4]); + out.push_back(digits[c & 0x0f]); + } else out.push_back(static_cast(c)); + ++i; + continue; + } + unsigned count; + std::uint32_t codepoint; + std::uint32_t minimum; + if (c >= 0xC2 && c <= 0xDF) { + count = 1; + codepoint = c & 0x1F; + minimum = 0x80; + } else if (c >= 0xE0 && c <= 0xEF) { + count = 2; + codepoint = c & 0x0F; + minimum = 0x800; + } else if (c >= 0xF0 && c <= 0xF4) { + count = 3; + codepoint = c & 0x07; + minimum = 0x10000; + } else { + throw RelinkerException("Invalid UTF-8 lead byte in call registry string"); + } + if (count > s.size() - (i + 1)) { + throw RelinkerException("Truncated UTF-8 sequence in call registry string"); + } + for (unsigned index = 0; index < count; ++index) { + const auto byte = static_cast(s[i + 1 + index]); + if ((byte & 0xC0) != 0x80) { + throw RelinkerException("Invalid UTF-8 continuation byte in call registry string"); + } + codepoint = (codepoint << 6) | (byte & 0x3F); + } + if (codepoint < minimum || codepoint > 0x10FFFF || (codepoint >= 0xD800 && codepoint <= 0xDFFF)) { + throw RelinkerException("Invalid UTF-8 codepoint in call registry string"); + } + out.push_back(static_cast(c)); + out.append(s, i + 1, count); + i += count + 1; } out.push_back('"'); return out; @@ -52,7 +91,8 @@ std::string CallRegistryWriter::WriteCallRegistry(const std::vector& libraries) { + std::ostringstream out; + out << "[\n"; + for (std::size_t index = 0; index < libraries.size(); ++index) { + out << " " << _jsonString(libraries[index]); + if (index + 1 < libraries.size()) out << ","; + out << "\n"; + } + out << "]\n"; + return out.str(); +} + } diff --git a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp index 32cb6523c4..a0044b4765 100644 --- a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp +++ b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include namespace Relinker { @@ -68,6 +70,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( std::uint32_t originalJmprelCount) { SysVDynamicSection result; + result.NeededLibraries = neededLibraries; result.DynStrData.push_back(0); std::vector neededOffsets; @@ -76,6 +79,9 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( _appendElfSym(result.DynSymData, 0, 0, 0, 0, 0, 0); + const bool consoleFileNames = std::any_of(neededLibraries.begin(), neededLibraries.end(), + [](const std::string& name) { return Domain::NeededLibraryName(name) != name; }); + auto stripHashSuffix = [](const std::string& value) -> std::string { const auto hashPos = value.find('#'); if (hashPos == std::string::npos) return value; @@ -97,6 +103,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( for (const auto& ref : nidReferences) { if (!ref.Library.empty()) result.ImportModules.emplace(ref.RelocationAddress, ref.Library); if (ref.Function) result.FunctionImports.insert(ref.RelocationAddress); + if (consoleFileNames && ref.Library.empty() && !ref.Nid.empty() && ref.Nid.find('#') == std::string::npos) result.NamedImports.insert(ref.RelocationAddress); std::uint32_t relType = ref.RelocationTypeValue; if (relType == 0) relType = R_X86_64_JUMP_SLOT; diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index 81c62ebd8e..3f9da02d4c 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -2,7 +2,9 @@ #include #include #include +#include #include +#include namespace Relinker { @@ -25,6 +27,10 @@ const std::vector& ElfReader::GetRawBytes() const { return _fileBuffer; } +std::vector ElfReader::TakeBytes() { + return std::move(_fileBuffer); +} + std::string ElfReader::formatMagic() const { static constexpr char digits[] = "0123456789abcdef"; std::string text; @@ -70,7 +76,7 @@ std::uint64_t ElfReader::_readU64At(FileByteOffset fileByteOffset) const { return value; } -ElfHeader ElfReader::ReadHeader() const { +void ElfReader::ValidateMagic() const { if (_fileBuffer.size() < 0x40) { throw RelinkerException("File too small for ELF header"); } @@ -84,6 +90,10 @@ ElfHeader ElfReader::ReadHeader() const { _fileBuffer[2] != 'L' || _fileBuffer[3] != 'F') { throw RelinkerException("Invalid ELF magic number: " + formatMagic()); } +} + +ElfHeader ElfReader::ReadHeader() const { + ValidateMagic(); if (_fileBuffer[4] != 2 || _fileBuffer[5] != 1 || _fileBuffer[6] != 1) throw RelinkerException("Expected little-endian ELF64 version 1"); if (_readU16At(0x12) != 62) @@ -132,6 +142,8 @@ std::vector ElfReader::ReadProgramHeaders() const { if (ph.Type == PT_LOAD && ph.FileSize > ph.MemorySize) throw RelinkerException("PT_LOAD file size exceeds memory size", ph.Offset); + if (ph.Type == PT_LOAD && !_rangeFits(ph.Offset, ph.FileSize, _fileBuffer.size())) + throw RelinkerException("Segment offset out of bounds", ph.Offset); headers.push_back(ph); offset += header.ProgramHeaderEntrySize; @@ -149,11 +161,34 @@ std::vector ElfReader::ReadProgramHeaders() const { covered = true; } if (!covered) { + if (relro.FileSize > relro.MemorySize) + throw RelinkerException("PT_SCE_RELRO file size exceeds memory size", relro.Offset); + if (!_rangeFits(relro.Offset, relro.FileSize, _fileBuffer.size())) + throw RelinkerException("Segment offset out of bounds", relro.Offset); relro.Type = PT_LOAD; relro.Flags = PF_R | PF_W; } } + bool hasTls = false; + for (const auto& tls : headers) { + if (tls.Type != PT_TLS) continue; + if (hasTls) + throw RelinkerException("Multiple ELF TLS segments", tls.Offset); + hasTls = true; + if (tls.FileSize > tls.MemorySize || (tls.Alignment > 1 && !std::has_single_bit(tls.Alignment))) + throw RelinkerException("Invalid or unsupported ELF TLS layout", tls.Offset); + if (tls.FileSize == 0) continue; + if (!_rangeFits(tls.Offset, tls.FileSize, _fileBuffer.size())) + throw RelinkerException("PT_TLS initialization image is outside the file", tls.Offset); + const bool covered = std::any_of(headers.begin(), headers.end(), [&](const ProgramHeader& load) { + return load.Type == PT_LOAD && tls.MappedAddress >= load.MappedAddress && tls.MappedAddress - load.MappedAddress <= load.MemorySize + && tls.FileSize <= load.MemorySize - (tls.MappedAddress - load.MappedAddress); + }); + if (!covered) + throw RelinkerException("PT_TLS initialization image is outside PT_LOAD memory", tls.Offset); + } + return headers; } @@ -256,27 +291,13 @@ std::vector ElfReader::ReadDynamicTags(const ProgramHeader& dynamicH FileByteOffset ElfReader::TranslateVirtualAddress(VirtualAddress address) const { for (const auto& segment : ReadProgramHeaders()) { if (segment.Type == PT_LOAD && address >= segment.MappedAddress && - address - segment.MappedAddress < segment.FileSize) { - if (!_rangeFits(segment.Offset, segment.FileSize, _fileBuffer.size())) { - throw RelinkerException("PT_LOAD file mapping out of bounds", segment.Offset); - } + address - segment.MappedAddress < segment.FileSize) return segment.Offset + (address - segment.MappedAddress); - } } throw RelinkerException("Virtual address not mapped by any PT_LOAD segment", address); } -std::vector ElfReader::ReadSection(const SectionHeader& header) const { - if (!_rangeFits(header.Offset, header.SectionSize, _fileBuffer.size())) { - throw RelinkerException("Section offset out of bounds", header.Offset); - } - - return std::vector( - _fileBuffer.begin() + header.Offset, - _fileBuffer.begin() + header.Offset + header.SectionSize); -} - std::vector ElfReader::ReadSegment(const ProgramHeader& header) const { if (!_rangeFits(header.Offset, header.FileSize, _fileBuffer.size())) { throw RelinkerException("Segment offset out of bounds", header.Offset); diff --git a/core/relinker/relinker/src/patching/GamePatch.cpp b/core/relinker/relinker/src/patching/GamePatch.cpp new file mode 100644 index 0000000000..403264c79b --- /dev/null +++ b/core/relinker/relinker/src/patching/GamePatch.cpp @@ -0,0 +1,623 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Relinker::GamePatch { + +namespace { + +using Integer = PatchInteger; + +struct RawValue { + bool IsString = false; + std::string Text; + Integer Number; + std::size_t Line = 0; +}; + +struct IntegerType { + std::string_view Name; + unsigned Bits; + bool Signed; +}; + +constexpr std::array kIntegerTypes{{ + {"u8", 8, false}, {"u16", 16, false}, {"u32", 32, false}, {"u64", 64, false}, + {"i8", 8, true}, {"i16", 16, true}, {"i32", 32, true}, {"i64", 64, true}, +}}; + +constexpr std::array kIgnoredTopLevelKeys{"relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "linux_base", "settings", "settings_sha256"}; +constexpr std::array kReservedNames{"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "linux_base", "settings", "settings_sha256"}; +constexpr std::array kEntryKeys{"name", "target", "va", "type", "expected", "value", "min", "max", "align", "description"}; + +std::string HexValue(const std::uint64_t value) { + std::ostringstream stream; + stream << "0x" << std::hex << value; + return stream.str(); +} + +std::string IntegerText(const Integer& number) { + return number.Negative ? "-" + std::to_string(number.Magnitude) : HexValue(number.Magnitude); +} + +[[noreturn]] void FailLine(const std::size_t line, const std::string& message, const std::string_view text) { + throw Domain::RelinkerException("patch file line " + std::to_string(line) + ": " + message + ": `" + std::string(text) + "`"); +} + +std::string_view Trim(std::string_view text) { + constexpr std::string_view whitespace = " \t\r"; + const auto first = text.find_first_not_of(whitespace); + if (first == std::string_view::npos) return {}; + return text.substr(first, text.find_last_not_of(whitespace) - first + 1); +} + +std::string_view StripComment(const std::string_view line) { + bool inString = false; + for (std::size_t index = 0; index < line.size(); ++index) { + if (line[index] == '"') inString = !inString; + else if (line[index] == '#' && !inString) return line.substr(0, index); + } + return line; +} + +bool Contains(const auto& names, const std::string_view name) { + return std::find(names.begin(), names.end(), name) != names.end(); +} + +bool IsKeyCharacter(const char character) { + return (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') || (character >= '0' && character <= '9') || character == '_'; +} + +int HexDigit(const char character) { + if (character >= '0' && character <= '9') return character - '0'; + if (character >= 'a' && character <= 'f') return character - 'a' + 10; + if (character >= 'A' && character <= 'F') return character - 'A' + 10; + return -1; +} + +Integer ParseInteger(const std::string_view text, const std::size_t line, const std::string_view whole) { + std::string_view digits = text; + Integer number; + if (digits.starts_with('-')) { + number.Negative = true; + digits.remove_prefix(1); + } + if (digits.empty()) FailLine(line, "unsupported value", whole); + const bool hexadecimal = digits.starts_with("0x"); + if (hexadecimal) { + if (number.Negative) FailLine(line, "hexadecimal integers cannot be negative", whole); + digits.remove_prefix(2); + if (digits.empty()) FailLine(line, "unsupported value", whole); + } + if (!hexadecimal && digits.size() > 1 && digits.front() == '0') FailLine(line, "decimal integers cannot have leading zeros", whole); + for (const char character : digits) { + const int digit = HexDigit(character); + if (digit < 0 || (!hexadecimal && digit > 9)) { + if (!hexadecimal && (character == '.' || character == 'e' || character == 'E')) FailLine(line, "floats are not supported", whole); + FailLine(line, "unsupported value", whole); + } + const std::uint64_t base = hexadecimal ? 16 : 10; + if (number.Magnitude > (UINT64_MAX - static_cast(digit)) / base) FailLine(line, "integer is out of range", whole); + number.Magnitude = number.Magnitude * base + static_cast(digit); + } + if (number.Magnitude == 0) number.Negative = false; + return number; +} + +RawValue ParseValue(const std::string_view text, const std::size_t line, const std::string_view whole) { + RawValue value; + value.Line = line; + if (text.empty()) FailLine(line, "missing value", whole); + if (text.front() == '"') { + if (text.starts_with("\"\"\"")) FailLine(line, "multiline strings are not supported", whole); + const auto end = text.find('"', 1); + if (end == std::string_view::npos) FailLine(line, "unterminated string", whole); + if (end + 1 != text.size()) FailLine(line, "unexpected text after string", whole); + const auto content = text.substr(1, end - 1); + if (content.find('\\') != std::string_view::npos) FailLine(line, "escapes are not supported in strings", whole); + for (const char character : content) + if (static_cast(character) < 0x20 && character != '\t') FailLine(line, "control characters are not supported in strings", whole); + value.IsString = true; + value.Text = std::string(content); + return value; + } + if (text.front() == '\'') FailLine(line, "single-quoted strings are not supported", whole); + if (text.front() == '[') FailLine(line, "arrays are not supported", whole); + if (text.front() == '{') FailLine(line, "inline tables are not supported", whole); + if (text == "true" || text == "false") FailLine(line, "booleans are not supported", whole); + if (text == "inf" || text == "nan" || text.front() == '+') FailLine(line, "unsupported value", whole); + value.Number = ParseInteger(text, line, whole); + return value; +} + +std::vector ParseHexBytes(const RawValue& value, const std::string_view key, const std::string_view whole) { + if (!value.IsString) FailLine(value.Line, std::string(key) + " must be a hex byte string without a type", whole); + if (value.Text.empty() || value.Text.size() % 2 != 0) FailLine(value.Line, std::string(key) + " must be a nonempty even-length hex byte string", value.Text); + std::vector bytes; + for (std::size_t index = 0; index < value.Text.size(); index += 2) { + const int high = HexDigit(value.Text[index]); + const int low = HexDigit(value.Text[index + 1]); + if (high < 0 || low < 0) FailLine(value.Line, std::string(key) + " contains a non-hex character", value.Text); + bytes.push_back(static_cast(high * 16 + low)); + } + return bytes; +} + +bool IntegerLess(const Integer& left, const Integer& right) { + if (left.Negative != right.Negative) return left.Negative; + return left.Negative ? left.Magnitude > right.Magnitude : left.Magnitude < right.Magnitude; +} + +bool FitsType(const Integer& number, const IntegerType& type) { + if (!type.Signed) return !number.Negative && (type.Bits == 64 || number.Magnitude < (std::uint64_t{1} << type.Bits)); + const std::uint64_t limit = std::uint64_t{1} << (type.Bits - 1); + return number.Negative ? number.Magnitude <= limit : number.Magnitude <= limit - 1; +} + +std::vector EncodeInteger(const Integer& number, const IntegerType& type) { + const std::uint64_t raw = number.Negative ? std::uint64_t{0} - number.Magnitude : number.Magnitude; + std::vector bytes; + for (unsigned index = 0; index < type.Bits / 8; ++index) bytes.push_back(static_cast(raw >> (8 * index))); + return bytes; +} + +using Table = std::map; + +const RawValue* Find(const Table& table, const std::string& key) { + const auto found = table.find(key); + return found == table.end() ? nullptr : &found->second; +} + +PatchEntry BuildEntry(const Table& table, const std::size_t headerLine, std::set& names) { + PatchEntry entry; + entry.Line = headerLine; + const auto require = [&](const char* key) -> const RawValue& { + const auto* value = Find(table, key); + if (!value) FailLine(headerLine, std::string("missing key `") + key + "`", "[[patch]]"); + return *value; + }; + if (const auto* name = Find(table, "name")) { + if (!name->IsString || name->Text.empty() || !std::all_of(name->Text.begin(), name->Text.end(), [](const char c) { return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'; })) + FailLine(name->Line, "name must be a nonempty string of [a-z0-9_]", name->Text); + if (Contains(kReservedNames, name->Text)) FailLine(name->Line, "name is reserved", name->Text); + if (!names.insert(name->Text).second) FailLine(name->Line, "duplicate name", name->Text); + entry.Name = name->Text; + } + const auto& target = require("target"); + if (!target.IsString || target.Text.empty()) FailLine(target.Line, "target must be a nonempty string", target.Text); + entry.Target = target.Text; + const auto& address = require("va"); + if (address.IsString || address.Number.Negative) FailLine(address.Line, "va must be a nonnegative integer", address.Text); + entry.VirtualAddress = address.Number.Magnitude; + const auto& expected = require("expected"); + const auto& replacement = require("value"); + if (const auto* description = Find(table, "description")) { + if (!description->IsString) FailLine(description->Line, "description must be a string", description->Text); + entry.Description = description->Text; + } + const auto* type = Find(table, "type"); + if (!type) { + for (const char* key : {"min", "max", "align"}) + if (const auto* value = Find(table, key)) FailLine(value->Line, std::string(key) + " requires a type", key); + entry.Expected = ParseHexBytes(expected, "expected", "expected"); + entry.Value = ParseHexBytes(replacement, "value", "value"); + if (entry.Expected.size() != entry.Value.size()) FailLine(replacement.Line, "expected and value must have the same width", "value"); + return entry; + } + if (!type->IsString) FailLine(type->Line, "type must be a string", type->Text); + const auto found = std::find_if(kIntegerTypes.begin(), kIntegerTypes.end(), [&](const IntegerType& candidate) { return candidate.Name == type->Text; }); + if (found == kIntegerTypes.end()) FailLine(type->Line, "unknown type", type->Text); + const auto integer = [&](const RawValue& value, const char* key) -> const Integer& { + if (value.IsString) FailLine(value.Line, std::string(key) + " must be an integer for a typed entry", value.Text); + return value.Number; + }; + const auto& expectedNumber = integer(expected, "expected"); + const auto& valueNumber = integer(replacement, "value"); + if (!FitsType(expectedNumber, *found)) FailLine(expected.Line, "expected does not fit " + std::string(found->Name), "expected"); + if (!FitsType(valueNumber, *found)) FailLine(replacement.Line, "value does not fit " + std::string(found->Name), "value"); + if (const auto* minimum = Find(table, "min"); minimum && IntegerLess(valueNumber, integer(*minimum, "min"))) FailLine(minimum->Line, "value is below min", "min"); + if (const auto* maximum = Find(table, "max"); maximum && IntegerLess(integer(*maximum, "max"), valueNumber)) FailLine(maximum->Line, "value is above max", "max"); + if (const auto* align = Find(table, "align")) { + const auto& alignment = integer(*align, "align"); + if (alignment.Negative || alignment.Magnitude == 0) FailLine(align->Line, "align must be positive", "align"); + if (valueNumber.Magnitude % alignment.Magnitude != 0) FailLine(align->Line, "value is not a multiple of align", "align"); + } + entry.Type = type->Text; + entry.ExpectedNumber = expectedNumber; + entry.ValueNumber = valueNumber; + if (const auto* minimum = Find(table, "min")) entry.Min = minimum->Number; + if (const auto* maximum = Find(table, "max")) entry.Max = maximum->Number; + if (const auto* align = Find(table, "align")) entry.Align = align->Number; + entry.Expected = EncodeInteger(expectedNumber, *found); + entry.Value = EncodeInteger(valueNumber, *found); + return entry; +} + +struct EntryRange { + std::optional Range; + std::string Reason; +}; + +bool TouchesHeaderArea(const ElfHeader& header, const std::uint64_t offset, const std::uint64_t length) { + const auto overlaps = [offset, length](const std::uint64_t start, const std::uint64_t size) { + return offset < start + size && start < offset + length; + }; + if (overlaps(0, Elfpatcher::kElfHeaderSize)) return true; + if (header.ProgramHeaderCount == 0 || header.ProgramHeaderEntrySize == 0) return false; + const std::uint64_t tableSize = static_cast(header.ProgramHeaderCount) * header.ProgramHeaderEntrySize; + if (tableSize > std::numeric_limits::max() - header.ProgramHeaderOffset) return true; + return overlaps(header.ProgramHeaderOffset, tableSize); +} + +EntryRange ResolveEntry(const std::vector& segments, const std::uint64_t fileSize, const ElfHeader& header, const PatchEntry& entry) { + const std::uint64_t length = entry.Expected.size(); + for (const auto& segment : segments) { + if (segment.Type != Elfpatcher::PT_LOAD) continue; + if (entry.VirtualAddress < segment.MappedAddress) continue; + const std::uint64_t delta = entry.VirtualAddress - segment.MappedAddress; + if (delta >= segment.FileSize || length > segment.FileSize - delta) continue; + if (segment.Offset > fileSize || segment.FileSize > fileSize - segment.Offset) return {std::nullopt, "PT_LOAD file mapping is out of bounds"}; + const PatchedRange range{segment.Offset + delta, length, entry.VirtualAddress}; + if (TouchesHeaderArea(header, range.Offset, length)) return {std::nullopt, "range overlaps the ELF header or program header table"}; + return {range, {}}; + } + return {std::nullopt, "range is not inside the file bytes of one PT_LOAD"}; +} + +std::vector ResolveEach(const ElfReader& reader, const std::vector& entries) { + const auto segments = reader.ReadProgramHeaders(); + const auto header = reader.ReadHeader(); + std::vector resolved; + for (const auto& entry : entries) resolved.push_back(ResolveEntry(segments, reader.GetFileSize(), header, entry)); + return resolved; +} + +std::string Describe(const PatchEntry& entry) { + return "patch " + entry.Target + " va=" + HexValue(entry.VirtualAddress) + " (line " + std::to_string(entry.Line) + ")"; +} + +bool Overlaps(const std::uint64_t firstOffset, const std::uint64_t firstLength, const std::uint64_t secondOffset, const std::uint64_t secondLength) { + return firstOffset < secondOffset + secondLength && secondOffset < firstOffset + firstLength; +} + +void RejectEntryOverlaps(const std::vector& entries, const std::vector& resolved) { + for (std::size_t index = 0; index < entries.size(); ++index) { + if (!resolved[index].Range) continue; + for (std::size_t previous = 0; previous < index; ++previous) { + if (!resolved[previous].Range) continue; + const auto& first = *resolved[previous].Range; + const auto& second = *resolved[index].Range; + if (Overlaps(first.Offset, first.Length, second.Offset, second.Length)) + throw Domain::RelinkerException(Describe(entries[index]) + " overlaps " + Describe(entries[previous])); + } + } +} + +bool IsElfOfType(const std::vector& bytes, const std::uint16_t type) { + return bytes.size() >= 18 && bytes[0] == 0x7f && bytes[1] == 'E' && bytes[2] == 'L' && bytes[3] == 'F' && bytes[16] == (type & 0xff) && bytes[17] == (type >> 8); +} + +VerifyResult VerifyOne(const PatchEntry& entry, const EntryRange& resolved, const std::vector& bytes, const bool outputMode) { + VerifyResult result; + result.Name = entry.Name; + result.Target = entry.Target; + result.VirtualAddress = entry.VirtualAddress; + if (!resolved.Range) { + result.Detail = resolved.Reason; + return result; + } + result.Offset = resolved.Range->Offset; + const auto first = bytes.begin() + static_cast(resolved.Range->Offset); + const std::vector found(first, first + static_cast(resolved.Range->Length)); + const auto& wanted = outputMode ? entry.Value : entry.Expected; + const auto& other = outputMode ? entry.Expected : entry.Value; + if (found == wanted) { + result.Status = outputMode ? VerifyStatus::Applied : VerifyStatus::Applicable; + result.Detail = Hex(found); + } else if (found == other) { + result.Detail = outputMode ? "patch is not applied: found " + Hex(found) : "input already holds the patched value " + Hex(found); + } else { + result.Detail = "expected " + Hex(wanted) + " found " + Hex(found); + } + return result; +} + +const VerifyTarget* FindTarget(const std::vector& targets, const std::string& name) { + const auto found = std::find_if(targets.begin(), targets.end(), [&](const VerifyTarget& target) { return target.Name == name; }); + return found == targets.end() ? nullptr : &*found; +} + +} + +std::string Hex(const std::span bytes) { + constexpr char digits[] = "0123456789abcdef"; + std::string text; + for (const auto byte : bytes) { + text += digits[byte >> 4]; + text += digits[byte & 15]; + } + return text; +} + +std::string FormatApplied(const AppliedPatch& patch) { + std::string text = "Patch: " + patch.Target + (patch.Name.empty() ? std::string() : " [" + patch.Name + "]") + " va=" + HexValue(patch.VirtualAddress) + " off=" + HexValue(patch.Offset) + " " + Hex(patch.Expected) + " -> " + Hex(patch.Value); + if (!patch.Description.empty()) text += " (" + patch.Description + ")"; + return text; +} + +std::string ModuleTargetName(const std::filesystem::path& modulePath, const std::filesystem::path& moduleRoot) { + auto name = Io::Utf8Path(modulePath.lexically_normal().lexically_relative(moduleRoot.lexically_normal())); + std::replace(name.begin(), name.end(), '\\', '/'); + return name; +} + +PatchFile ParsePatchFile(const std::string_view text) { + PatchFile file; + Table top; + Table current; + std::size_t entryLine = 0; + std::set names; + bool inEntry = false; + const auto finishEntry = [&] { + if (inEntry) file.Entries.push_back(BuildEntry(current, entryLine, names)); + current.clear(); + }; + std::size_t lineNumber = 0; + std::size_t position = 0; + while (position <= text.size()) { + auto end = text.find('\n', position); + if (end == std::string_view::npos) end = text.size(); + const auto whole = text.substr(position, end - position); + position = end + 1; + ++lineNumber; + const auto line = Trim(StripComment(whole)); + if (line.empty()) continue; + if (line.front() == '[') { + if (line != "[[patch]]") FailLine(lineNumber, "unsupported table header", Trim(whole)); + finishEntry(); + inEntry = true; + entryLine = lineNumber; + continue; + } + std::size_t keyEnd = 0; + while (keyEnd < line.size() && IsKeyCharacter(line[keyEnd])) ++keyEnd; + const std::string key(line.substr(0, keyEnd)); + const auto rest = Trim(line.substr(keyEnd)); + if (key.empty() || rest.empty() || rest.front() != '=') { + if (!rest.empty() && rest.front() == '.') FailLine(lineNumber, "dotted keys are not supported", line); + if (key.empty() && (line.front() == '"' || line.front() == '\'')) FailLine(lineNumber, "quoted keys are not supported", line); + FailLine(lineNumber, "expected `key = value`", line); + } + auto value = ParseValue(Trim(rest.substr(1)), lineNumber, line); + auto& table = inEntry ? current : top; + if (inEntry ? !Contains(kEntryKeys, key) : !(key == "format" || Contains(kIgnoredTopLevelKeys, key))) + FailLine(lineNumber, inEntry ? "unknown key" : "unknown top-level key", line); + if (table.contains(key)) FailLine(lineNumber, "duplicate key", line); + table.emplace(key, std::move(value)); + } + finishEntry(); + const auto* format = Find(top, "format"); + if (!format) throw Domain::RelinkerException("patch file: `format = 1` is required"); + if (format->IsString || format->Number.Negative || format->Number.Magnitude != 1) FailLine(format->Line, "unsupported format", "format"); + if (const auto* mainTarget = Find(top, "main_target")) { + if (!mainTarget->IsString || mainTarget->Text.empty()) FailLine(mainTarget->Line, "main_target must be a nonempty string", "main_target"); + file.MainTarget = mainTarget->Text; + } + if (const auto* linuxBase = Find(top, "linux_base")) { + if (linuxBase->IsString || linuxBase->Number.Negative) FailLine(linuxBase->Line, "linux_base must be a non-negative integer", "linux_base"); + file.LinuxBase = linuxBase->Number.Magnitude; + } + return file; +} + +std::vector ResolveRanges(const ElfReader& reader, const std::vector& entries) { + const auto resolved = ResolveEach(reader, entries); + std::vector ranges; + for (std::size_t index = 0; index < entries.size(); ++index) { + if (!resolved[index].Range) throw Domain::RelinkerException(Describe(entries[index]) + ": " + resolved[index].Reason); + ranges.push_back(*resolved[index].Range); + } + RejectEntryOverlaps(entries, resolved); + return ranges; +} + +std::vector ApplyPatches(std::vector& bytes, const std::string& target, const std::vector& entries, const std::vector& ranges) { + if (entries.size() != ranges.size()) throw Domain::RelinkerException("Patch ranges do not match patch entries"); + for (std::size_t index = 0; index < entries.size(); ++index) { + const auto& range = ranges[index]; + if (range.Offset > bytes.size() || range.Length > bytes.size() - range.Offset) throw Domain::RelinkerException(Describe(entries[index]) + ": range exceeds the image", range.Offset); + const auto first = bytes.begin() + static_cast(range.Offset); + if (!std::equal(entries[index].Expected.begin(), entries[index].Expected.end(), first)) + throw Domain::RelinkerException(Describe(entries[index]) + " off=" + HexValue(range.Offset) + ": expected " + Hex(entries[index].Expected) + " found " + Hex(std::span(&*first, range.Length)), range.Offset); + } + std::vector applied; + for (std::size_t index = 0; index < entries.size(); ++index) { + std::copy(entries[index].Value.begin(), entries[index].Value.end(), bytes.begin() + static_cast(ranges[index].Offset)); + applied.push_back({target, entries[index].VirtualAddress, ranges[index].Offset, entries[index].Expected, entries[index].Value, entries[index].Description, entries[index].Name, entries[index].Type, entries[index].ExpectedNumber, entries[index].ValueNumber, entries[index].Min, entries[index].Max, entries[index].Align}); + } + return applied; +} + +PatchedTarget PatchTarget(std::vector& bytes, const std::string& target, const std::vector& entries) { + std::vector own; + for (const auto& entry : entries) if (entry.Target == target) own.push_back(entry); + if (own.empty()) return {}; + ElfReader reader(std::move(bytes)); + PatchedTarget patched; + try { + patched.Ranges = ResolveRanges(reader, own); + } catch (...) { + bytes = reader.TakeBytes(); + throw; + } + bytes = reader.TakeBytes(); + patched.Applied = ApplyPatches(bytes, target, own, patched.Ranges); + return patched; +} + +void CheckOverlaps(const std::string& target, const std::vector& patched, const WriterSites& writers) { + if (patched.empty()) return; + std::vector sites; + for (const auto& report : writers.Reports) + if (report.Lowering != Codegen::Amd64OnlyLowering::Kept) sites.push_back({report.Offset, report.OriginalLength, "Intel in-place report"}); + for (const auto& site : writers.Trampolines) sites.push_back({site.Offset, site.Length, "Intel trampoline site"}); + for (const auto& patch : writers.RelinkPatches) sites.push_back({patch.Offset, patch.Bytes.size(), "relinker patch"}); + sites.insert(sites.end(), writers.Traps.begin(), writers.Traps.end()); + const auto relaViolation = [&](const std::span table, const char* name, const PatchedRange& range) { + for (std::size_t position = 0; position + 24 <= table.size(); position += 24) { + std::uint64_t relocation = 0; + for (unsigned byte = 0; byte < 8; ++byte) relocation |= static_cast(table[position + byte]) << (8 * byte); + if (Overlaps(range.VirtualAddress, range.Length, relocation, 8)) + throw Domain::RelinkerException("patch " + target + " va=" + HexValue(range.VirtualAddress) + " off=" + HexValue(range.Offset) + " overlaps " + name + " relocation target at va=" + HexValue(relocation), range.Offset); + } + }; + for (const auto& range : patched) { + relaViolation(writers.RelaData, "RelaData", range); + relaViolation(writers.RelaPltData, "RelaPltData", range); + } + for (const auto& range : patched) + for (const auto& site : sites) + if (Overlaps(range.Offset, range.Length, site.Offset, site.Length)) + throw Domain::RelinkerException("patch " + target + " va=" + HexValue(range.VirtualAddress) + " off=" + HexValue(range.Offset) + " overlaps " + site.Writer + " at off=" + HexValue(site.Offset) + " length " + std::to_string(site.Length), range.Offset); +} + +void MatchTargets(const std::vector& entries, const std::vector& targetNames) { + for (const auto& entry : entries) { + const auto count = std::count(targetNames.begin(), targetNames.end(), entry.Target); + if (count == 0) throw Domain::RelinkerException("unmatched patch target: " + entry.Target + " (patch file line " + std::to_string(entry.Line) + ")"); + if (count > 1) throw Domain::RelinkerException("ambiguous patch target: " + entry.Target + " (patch file line " + std::to_string(entry.Line) + ")"); + } +} + +std::vector AggregateManifest(const std::vector>& records) { + std::vector merged; + for (const auto& group : records) { + for (const auto& patch : group) { + const bool known = std::any_of(merged.begin(), merged.end(), [&](const AppliedPatch& existing) { + return existing.Target == patch.Target && existing.VirtualAddress == patch.VirtualAddress && existing.Offset == patch.Offset; + }); + if (!known) merged.push_back(patch); + } + } + return merged; +} + +std::string WriteManifest(const std::vector& patches, const std::string& mainTarget, const std::string& configPath, const std::optional linuxBase) { + const auto holdable = [](const std::string& text) { + return std::none_of(text.begin(), text.end(), [](const char character) { + return character == '"' || character == '\\' || (static_cast(character) < 0x20 && character != '\t') || character == 0x7f; + }); + }; + const auto quoted = [&](const std::string& text, const char* field) { + if (!holdable(text)) throw Domain::RelinkerException(std::string("Patch manifest cannot hold the ") + field + ": " + text); + return "\"" + text + "\""; + }; + std::string config = std::filesystem::path(Io::NativePath(configPath)).generic_string(); + if (!config.empty() && !holdable(config)) config = std::filesystem::path(Io::NativePath(configPath)).filename().generic_string(); + std::ostringstream manifest; + manifest << "format = 1\nmain_target = " << quoted(mainTarget, "main_target") << '\n'; + if (linuxBase) manifest << "linux_base = " << HexValue(*linuxBase) << '\n'; + if (!config.empty()) manifest << "config = " << quoted(config, "config") << '\n'; + for (const auto& patch : patches) { + manifest << "\n[[patch]]\n"; + if (!patch.Name.empty()) manifest << "name = " << quoted(patch.Name, "name") << '\n'; + manifest << "target = " << quoted(patch.Target, "target") << "\nva = " << HexValue(patch.VirtualAddress) << '\n'; + if (patch.Type.empty()) { + manifest << "expected = \"" << Hex(patch.Expected) << "\"\nvalue = \"" << Hex(patch.Value) << "\"\n"; + } else { + manifest << "type = " << quoted(patch.Type, "type") << "\nexpected = " << IntegerText(patch.ExpectedNumber) << "\nvalue = " << IntegerText(patch.ValueNumber) << '\n'; + if (patch.Min) manifest << "min = " << IntegerText(*patch.Min) << '\n'; + if (patch.Max) manifest << "max = " << IntegerText(*patch.Max) << '\n'; + if (patch.Align) manifest << "align = " << IntegerText(*patch.Align) << '\n'; + } + if (!patch.Description.empty()) manifest << "description = " << quoted(patch.Description, "description") << '\n'; + } + return manifest.str(); +} + +std::string_view StatusName(const VerifyStatus status) { + switch (status) { + case VerifyStatus::Applicable: return "applicable"; + case VerifyStatus::Applied: return "applied"; + case VerifyStatus::NotChecked: return "not-checked"; + case VerifyStatus::Mismatch: break; + } + return "mismatch"; +} + +std::vector VerifyPatches(const PatchFile& file, const std::vector& executables, const std::vector& modules, const bool outputMode) { + if (outputMode) { + if (!file.MainTarget) throw Domain::RelinkerException("The patch file has no main_target, so its entries cannot be matched to an output: verify the input executable, or verify the manifest written by the conversion"); + for (std::size_t index = 0; index < executables.size(); ++index) + for (std::size_t other = index + 1; other < executables.size(); ++other) + if (executables[index].Name == executables[other].Name) throw Domain::RelinkerException("ambiguous patch target: " + executables[index].Name); + for (const auto& executable : executables) { + const bool fixed = IsElfOfType(executable.Bytes, Elfpatcher::ET_EXEC); + if (!fixed && !IsElfOfType(executable.Bytes, Elfpatcher::ET_DYN)) throw Domain::RelinkerException(executable.Name + " is not a Linux ELF output (PE and Mach-O outputs are not supported): verify the input instead"); + if (fixed && !file.LinuxBase) throw Domain::RelinkerException(executable.Name + " is an ET_EXEC Linux ELF output, but the patch file has no linux_base: verify the manifest written by its conversion"); + if (!fixed && file.LinuxBase) throw Domain::RelinkerException(executable.Name + " is an ET_DYN Linux ELF output, but the patch file has linux_base = " + HexValue(*file.LinuxBase) + ": verify the output of the conversion that wrote it"); + } + } else { + std::vector names; + for (const auto& executable : executables) names.push_back(executable.Name); + for (const auto& module : modules) names.push_back(module.Name); + try { + MatchTargets(file.Entries, names); + } catch (const Domain::RelinkerException& error) { + if (std::string_view(error.what()).starts_with("unmatched")) throw Domain::RelinkerException(std::string(error.what()) + "; to check a converted output, use --verify-output"); + throw; + } + } + std::vector results(file.Entries.size()); + std::vector done(file.Entries.size()); + for (std::size_t index = 0; index < file.Entries.size(); ++index) { + if (done[index]) continue; + const auto& name = file.Entries[index].Target; + const auto* target = FindTarget(executables, name); + if (!target && !outputMode) target = FindTarget(modules, name); + std::vector indices; + for (std::size_t other = index; other < file.Entries.size(); ++other) if (file.Entries[other].Target == name) indices.push_back(other); + if (!target) { + for (const auto other : indices) { + const auto& entry = file.Entries[other]; + results[other] = {entry.Name, entry.Target, entry.VirtualAddress, 0, VerifyStatus::NotChecked, "module, or executable not named with --executable"}; + done[other] = true; + } + continue; + } + const std::uint64_t base = outputMode && IsElfOfType(target->Bytes, Elfpatcher::ET_EXEC) ? *file.LinuxBase : 0; + std::vector own; + for (const auto other : indices) { + own.push_back(file.Entries[other]); + if (own.back().VirtualAddress > std::numeric_limits::max() - base) throw Domain::RelinkerException(Describe(own.back()) + " lies outside the address space at linux_base = " + HexValue(base)); + own.back().VirtualAddress += base; + } + const ElfReader reader(target->Bytes); + const auto resolved = ResolveEach(reader, own); + RejectEntryOverlaps(own, resolved); + for (std::size_t position = 0; position < indices.size(); ++position) { + results[indices[position]] = VerifyOne(own[position], resolved[position], target->Bytes, outputMode); + results[indices[position]].VirtualAddress = file.Entries[indices[position]].VirtualAddress; + done[indices[position]] = true; + } + } + return results; +} + +std::filesystem::path OutputExecutablePath(const std::filesystem::path& outputPath, const std::string& executableArgument) { + const auto relative = Io::NativePath(executableArgument).lexically_normal(); + if (relative.empty() || relative.has_root_path() || *relative.begin() == ".." || !relative.has_filename()) + throw Domain::RelinkerException("--executable must be a file inside the guest module parent directory: " + executableArgument); + std::string name; + for (const auto& part : relative) name += (name.empty() ? "" : ".") + Io::Utf8Path(part); + return outputPath.parent_path() / Io::NativePath(name + Io::Utf8Path(outputPath.extension())); +} + +} diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index de255c214d..a35436a09b 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -30,6 +30,7 @@ #include #include #include +#include namespace Relinker { @@ -47,18 +48,30 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti if (!input.Modules.empty() && input.ModuleRoot.empty()) throw Domain::RelinkerException("Module root is required"); if (options.WindowsDiagnostics && options.Target != TargetSystem::Windows) throw Domain::RelinkerException("Windows diagnostics require the Windows target"); if (options.LinuxFixedBase && options.Target != TargetSystem::Linux) throw Domain::RelinkerException("A fixed Linux base requires the Linux target"); + if (options.MacosFixedBase && options.Target != TargetSystem::MacOs) throw Domain::RelinkerException("A fixed macOS base requires the macOS target"); const bool windows = options.Target == TargetSystem::Windows; const bool macos = options.Target == TargetSystem::MacOs; inputProgress.Report({Domain::ProgressStage::Executable, Io::Utf8Path(input.InputPath), 0, 1}); auto sourceBytes = std::move(input.Executable); + std::vector appliedPatches; + std::vector patchedRanges; + if (!options.Patches.empty()) { + auto patched = GamePatch::PatchTarget(sourceBytes, options.TargetName, options.Patches); + patchedRanges = std::move(patched.Ranges); + for (auto& patch : patched.Applied) { + inputProgress.Message(GamePatch::FormatApplied(patch) + "\n"); + appliedPatches.push_back(std::move(patch)); + } + } std::vector trampolines; if (options.ToIntel) { const auto segments = Relinker::ElfReader(sourceBytes).ReadProgramHeaders(); auto converted = Codegen::MakeAmd64OnlyConverter(options.ToRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel, [&](auto completed, auto total) { inputProgress.Report({Domain::ProgressStage::Lowering, {}, completed, total}); })->Convert(std::move(sourceBytes), segments); sourceBytes = std::move(converted.Bytes); trampolines = std::move(converted.Trampolines); + GamePatch::CheckOverlaps(options.TargetName, patchedRanges, {.Reports = converted.Reports, .Trampolines = trampolines}); std::map stubsByName; - const auto counted = [](const std::string& name) { return name == "VRSQRTPS" || name == "VRCPPS" || name == "256-bit store"; }; + const auto counted = [](const std::string& name) { return name == "VRSQRTPS" || name == "VRCPPS" || name == "256-bit store" || name == "scalar compare"; }; for (const auto& report : converted.Reports) { if (report.Lowering == Codegen::Amd64OnlyLowering::Kept) { std::ostringstream message; message << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) kept: no room for a jump\n"; inputProgress.Message(message.str()); } @@ -72,7 +85,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti { std::ostringstream message; message << "Intel conversion: " << converted.ReplacedCount << " in place, " << trampolines.size() << " stubs, " << converted.KeptCount << " kept\n"; inputProgress.Message(message.str()); } } - auto elfReader = std::make_shared(sourceBytes); + auto elfReader = std::make_shared(std::move(sourceBytes)); const std::shared_ptr syscallScanner = options.SkipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(inputProgress, options.TrapSdkSyscallStubs); const auto pipeline = std::make_shared( @@ -87,7 +100,8 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti { std::ostringstream message; message << "System: " << (windows ? "Windows" : macos ? "macOS" : "Linux") << "; unused-filter=" << options.UnusedFilterLevel << "\n"; inputProgress.Message(message.str()); } for (const auto& name : options.ExcludedModules) { std::ostringstream message; message << "Guest module excluded: " << name << '\n'; inputProgress.Message(message.str()); } - auto result = pipeline->Relink(sourceBytes); + auto result = pipeline->Relink(); + sourceBytes = elfReader->TakeBytes(); for (const auto& patch : result.Patches) { if (patch.Offset > sourceBytes.size() || patch.Bytes.size() > sourceBytes.size() - patch.Offset) throw Domain::RelinkerException("Relinker patch exceeds source image", patch.Offset); @@ -108,6 +122,18 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti for (const auto& [reason, count] : report.Skipped) { std::ostringstream message; message << "Red-zone framing: skipped " << count << ": " << reason << '\n'; inputProgress.Message(message.str()); } trampolines.insert(trampolines.end(), std::make_move_iterator(framed.begin()), std::make_move_iterator(framed.end())); } + GamePatch::CheckOverlaps(options.TargetName, patchedRanges, {.RelinkPatches = result.Patches, .RelaData = result.DynamicSection.RelaData, .RelaPltData = result.DynamicSection.RelaPltData}); + + if (!options.Patches.empty()) { + for (auto& module : input.Modules) { + auto patched = GamePatch::PatchTarget(module.Bytes, GamePatch::ModuleTargetName(module.Path, input.ModuleRoot), options.Patches); + module.PatchedRanges = std::move(patched.Ranges); + for (auto& patch : patched.Applied) { + inputProgress.Message(GamePatch::FormatApplied(patch) + "\n"); + appliedPatches.push_back(std::move(patch)); + } + } + } std::vector guestArtifacts; if (!input.Modules.empty()) { @@ -116,28 +142,34 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti auto byteWriter = std::make_shared(); + std::optional linuxBase; + if (!windows && !macos) + linuxBase = options.LinuxFixedBase || options.LazyBinding ? options.LinuxFixedBase : std::optional(Elfpatcher::Linux::DefaultFixedBase); + if (!windows && !macos && options.LazyBinding) + inputProgress.Message("Warning: with --lazy-binding the Linux executable stays position independent and is not placed at 0x400000; a title whose own sceLibcMspaceCreate checks the 40-bit address window stops at start-up\n"); std::shared_ptr patcher; if (macos) { - patcher = std::make_shared(); + patcher = std::make_shared(options.MacosFixedBase, [&](std::string_view text) { inputProgress.Message(text); }); } else if (windows) { - patcher = std::make_shared(options.WindowsGui, std::move(input.Icon), input.InputPath.parent_path() / "sce_sys" / "icon0.png", [&](auto completed, auto total) { inputProgress.Report({Domain::ProgressStage::Image, {}, completed, total}); }, options.WindowsFixedBase, options.TraceImports); + patcher = std::make_shared(options.WindowsGui, std::move(input.Icon), input.InputPath.parent_path() / "sce_sys" / "icon0.png", [&](auto completed, auto total) { inputProgress.Report({Domain::ProgressStage::Image, {}, completed, total}); }, options.WindowsFixedBase, options.TraceImports, [&](std::string_view text) { inputProgress.Message(text); }); } else { patcher = std::make_shared( std::make_shared(), std::make_shared( - std::make_shared(), + std::make_shared(options.LazyBinding), byteWriter ), std::make_shared(byteWriter), byteWriter, - options.LinuxFixedBase + linuxBase, + options.TraceImports ); } if (!windows) inputProgress.Report({Domain::ProgressStage::Image, Io::Utf8Path(input.InputPath), 0, 1}); std::vector executableBytes; try { - executableBytes = patcher->Patch(sourceBytes, result.OriginalHeaders, result.DynamicSection, result.OriginalPltGotVaddr, options.RunPath, options.LazyBinding, options.WindowsDiagnostics, trampolines); + executableBytes = patcher->Patch(std::move(sourceBytes), result.OriginalHeaders, result.DynamicSection, result.OriginalPltGotVaddr, options.RunPath, options.LazyBinding, options.WindowsDiagnostics, trampolines); } catch (Domain::RelinkerException& error) { if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(input.InputPath); throw; @@ -147,7 +179,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti std::string diagnostics; if (windows) diagnostics = std::static_pointer_cast(patcher)->GetDiagnostics(); inputProgress.Report({Domain::ProgressStage::Executable, Io::Utf8Path(input.InputPath), 1, 1}); - return {std::move(executableBytes), std::move(guestArtifacts), std::move(result), std::move(diagnostics)}; + return {std::move(executableBytes), std::move(guestArtifacts), std::move(result), std::move(diagnostics), std::move(appliedPatches), linuxBase}; } catch (Domain::RelinkerException& error) { if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(input.InputPath); throw; diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 79bef2ca9e..78ce7cd35a 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -1,9 +1,9 @@ #include #include -#include #include #include #include +#include #include #include @@ -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; } @@ -54,9 +56,14 @@ std::string RelinkerPipeline::relocationTypeName(std::uint32_t type) { } } -RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf) { - if (elfReader->ReadHeader().Type == kEtRel) +RelinkResult RelinkerPipeline::Relink() { + 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(); @@ -70,8 +77,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf if (ph.Type == PT_LOAD && (ph.Flags & PF_X) != 0) { auto segment = elfReader->ReadSegment(ph); if (!segment.empty()) { - if (textSection.empty()) { - textSection = segment; + if (executableSegments.empty()) { + if (unusedFilterLevel != 0) textSection = segment; textVAddr = ph.MappedAddress; } executableSegments.emplace_back(std::move(segment), ph.MappedAddress, ph.Offset); @@ -207,12 +214,25 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf } const ByteCount dynJmpRelSize = gotSize; - const FileByteOffset dynRelaOffset = readAsOffset(DT_OS_RELA, DT_RELA, "DT_RELA"); - const ByteCount dynRelaSize = readAsSize(DT_OS_RELASZ, DT_RELASZ, "DT_RELASZ"); + if (hasTag(DT_REL) || hasTag(DT_RELSZ) || hasTag(DT_RELENT)) + throw RelinkerException("DT_REL relocations are not supported"); + if (hasTag(DT_RELR) || hasTag(DT_RELRSZ) || hasTag(DT_RELRENT)) + throw RelinkerException("DT_RELR relocations are not supported"); + + const bool hasRelaTable = hasTag(DT_OS_RELA) || hasTag(DT_RELA) + || hasTag(DT_OS_RELASZ) || hasTag(DT_RELASZ); + FileByteOffset dynRelaOffset = 0; + ByteCount dynRelaSize = 0; + if (hasRelaTable) { + dynRelaOffset = readAsOffset(DT_OS_RELA, DT_RELA, "DT_RELA"); + dynRelaSize = readAsSize(DT_OS_RELASZ, DT_RELASZ, "DT_RELASZ"); + } constexpr std::size_t relaEntSize = 24; - const auto relocationEntrySize = readAsSize(DT_OS_RELAENT, DT_RELAENT, "DT_RELAENT"); - if (relocationEntrySize != relaEntSize) - throw RelinkerException("Unsupported DT_RELAENT value: " + std::to_string(relocationEntrySize) + "; expected 24 bytes"); + if (hasRelaTable || hasTag(DT_OS_RELAENT) || hasTag(DT_RELAENT)) { + const auto relocationEntrySize = readAsSize(DT_OS_RELAENT, DT_RELAENT, "DT_RELAENT"); + if (relocationEntrySize != relaEntSize) + throw RelinkerException("Unsupported DT_RELAENT value: " + std::to_string(relocationEntrySize) + "; expected 24 bytes"); + } if (dynRelaSize % relaEntSize != 0) throw RelinkerException("Invalid DT_RELASZ value: " + std::to_string(dynRelaSize) + " bytes; expected a multiple of 24 bytes"); @@ -223,7 +243,6 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::vector nidRefs; std::vector neededLibraries; - auto policy = std::dynamic_pointer_cast(validationPolicy); const std::vector& raw = elfReader->GetRawBytes(); const std::optional dynSymTabSize = hasTag(DT_OS_SYMTABSZ) @@ -238,6 +257,11 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf if (dynRelaOffset > raw.size() || dynRelaSize > raw.size() - dynRelaOffset) throw RelinkerException("Relocation table is out of bounds", dynRelaOffset); + if (dynRelaSize != 0 && dynJmpRelSize != 0 + && dynRelaOffset < dynJmpRelOffset + dynJmpRelSize && dynJmpRelOffset < dynRelaOffset + dynRelaSize + && dynRelaOffset % relaEntSize != dynJmpRelOffset % relaEntSize) + throw RelinkerException("Unaligned overlapping relocation tables", dynJmpRelOffset); + auto readCStr = [&](FileByteOffset strOff) -> std::string { if (strOff >= dynStrTabSize) throw RelinkerException("Dynamic string offset is outside DT_STRSZ", strOff); @@ -254,7 +278,6 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf for (auto& [fst, snd] : neededLibraryNamesByStrOffset) { snd = readCStr(fst); neededLibraries.push_back(snd); - if (policy) policy->RegisterLibraryImport(snd); } std::map importModules; @@ -308,9 +331,11 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf return definedTlsSymbolValue(symIdx, pos).has_value(); }; - auto extractRela = [&](const FileByteOffset relaOff, const ByteCount relaSize) { + auto extractRela = [&](const FileByteOffset relaOff, const ByteCount relaSize, const bool plt) { for (ByteCount off = 0; relaSize >= relaEntSize && off <= relaSize - relaEntSize; off += relaEntSize) { const FileByteOffset pos = relaEntryPos(relaOff, off); + if (!plt && pos >= dynJmpRelOffset && pos - dynJmpRelOffset < dynJmpRelSize) + continue; std::uint64_t rOffset = 0, rInfo = 0; std::int64_t rAddend = 0; @@ -349,33 +374,48 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf } }; - extractRela(dynRelaOffset, dynRelaSize); - extractRela(dynJmpRelOffset, dynJmpRelSize); + extractRela(dynRelaOffset, dynRelaSize, false); + extractRela(dynJmpRelOffset, dynJmpRelSize, true); for (const auto& ref : nidRefs) validationPolicy->ValidateRelocationTypeSupported(ref.RelocationTypeValue, ref.RelocationTableOffset); + std::vector> unwindRanges; + std::vector> segmentUnwindRanges(executableSegments.size()); if (std::any_of(programHeaders.begin(), programHeaders.end(), [](const ProgramHeader& ph) { return ph.Type == PT_SCE_DYNLIBDATA; })) { const auto functions = UnusedNidFilter::ReadExceptionFunctions(raw, programHeaders, {}, {}); - for (auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { + for (const auto& function : functions) unwindRanges.emplace_back(function.Begin, function.End); + std::sort(unwindRanges.begin(), unwindRanges.end()); + for (std::size_t index = 0; index < executableSegments.size(); ++index) { + auto& [segment, segmentVAddr, segmentFileOffset] = executableSegments[index]; static_cast(segmentFileOffset); if (const auto codeEnd = FindSceCodeEnd(segment, segmentVAddr, functions, gotVAddr, dynJmpRelSize)) segment.resize(*codeEnd - segmentVAddr); + segmentUnwindRanges[index] = SceUnwindRanges(segment, segmentVAddr, functions); } } + std::vector> scanSegments; + for (const auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { + static_cast(segmentFileOffset); + auto& scanSegment = scanSegments.emplace_back(segment); + for (const auto& table : FindRelativeJumpTables(segment, segmentVAddr, unwindRanges)) + std::fill(scanSegment.begin() + static_cast(table.Begin - segmentVAddr), scanSegment.begin() + static_cast(table.End - segmentVAddr), std::uint8_t{0x90}); + } + std::vector syscallPatches; - for (auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { - for (const auto offset : syscallScanner->ScanCodeSectionForSyscalls(segment, segmentVAddr, segment.size())) { + for (std::size_t index = 0; index < executableSegments.size(); ++index) { + auto& [segment, segmentVAddr, segmentFileOffset] = executableSegments[index]; + auto& scanSegment = scanSegments[index]; + for (const auto offset : syscallScanner->ScanCodeSectionForSyscalls(scanSegment, segmentVAddr, scanSegment.size(), {}, segmentUnwindRanges[index])) { if (offset > segment.size() || segment.size() - offset < 2u) throw RelinkerException("Syscall trap patch exceeds executable segment", segmentVAddr + offset); - segment[offset] = 0x0F; - segment[offset + 1] = 0x0B; + segment[offset] = scanSegment[offset] = 0x0F; + segment[offset + 1] = scanSegment[offset + 1] = 0x0B; syscallPatches.push_back({segmentFileOffset + offset, {0x0F, 0x0B}}); } } - validationPolicy->ValidateSyscallAbsence(); std::vector> exportedSymbols; std::vector exportedFunctions; @@ -478,9 +518,11 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::memcpy(buf.data() + pos + 16, &addend, 8); }; - auto extractLocal = [&](const FileByteOffset relaOff, const ByteCount relaSize) { + auto extractLocal = [&](const FileByteOffset relaOff, const ByteCount relaSize, const bool plt) { for (ByteCount off = 0; relaSize >= relaEntSize && off <= relaSize - relaEntSize; off += relaEntSize) { const FileByteOffset pos = relaEntryPos(relaOff, off); + if (!plt && pos >= dynJmpRelOffset && pos - dynJmpRelOffset < dynJmpRelSize) + continue; std::uint64_t rOffset = 0, rInfo = 0; std::int64_t rAddend = 0; std::memcpy(&rOffset, raw.data() + pos, 8); @@ -510,8 +552,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf } }; - extractLocal(dynRelaOffset, dynRelaSize); - extractLocal(dynJmpRelOffset, dynJmpRelSize); + extractLocal(dynRelaOffset, dynRelaSize, false); + extractLocal(dynJmpRelOffset, dynJmpRelSize, true); std::vector entries; entries.reserve(nidRefs.size()); @@ -525,13 +567,14 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf entry.TargetSection = ".got"; entry.TargetOffset = ref.RelocationAddress; entry.CallSitesResolved = false; + entry.Weak = ref.Weak; entries.push_back(std::move(entry)); } - for (const auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { - static_cast(segmentFileOffset); + for (std::size_t index = 0; index < executableSegments.size(); ++index) { + const auto& segmentVAddr = std::get<1>(executableSegments[index]); for (auto& entry : entries) { - auto segmentSites = callSiteResolver->ResolveCallSites(segment, segmentVAddr, entry.TargetOffset, 8); + auto segmentSites = callSiteResolver->ResolveCallSites(scanSegments[index], segmentVAddr, entry.TargetOffset, 8, segmentUnwindRanges[index]); entry.CallSites.insert(entry.CallSites.end(), segmentSites.begin(), segmentSites.end()); entry.CallSitesResolved = !entry.CallSites.empty(); } diff --git a/core/relinker/relinker/tests/CallSiteResolverTests.cpp b/core/relinker/relinker/tests/CallSiteResolverTests.cpp index f780307eb3..f06fab07fa 100644 --- a/core/relinker/relinker/tests/CallSiteResolverTests.cpp +++ b/core/relinker/relinker/tests/CallSiteResolverTests.cpp @@ -29,6 +29,10 @@ int main() { check("far indirect call", {0xff, 0x1d, 0xfa, 0, 0, 0}, true); check("far indirect jump", {0xff, 0x2d, 0xfa, 0, 0, 0}, true); check("RIP-relative function pointer load", {0x48, 0x8b, 0x05, 0xf9, 0, 0, 0}, true); + check("REX-prefixed indirect call", {0x48, 0xff, 0x15, 0xf9, 0, 0, 0}, true); + check("multiple REX prefixes on pointer load", {0x40, 0x48, 0x8b, 0x05, 0xf8, 0, 0, 0}, true); + check("pointer load without REX.W", {0x40, 0x8b, 0x05, 0xf9, 0, 0, 0}, false); + check("register pointer load", {0x48, 0x8b, 0x00}, false); check("increment memory", {0xff, 0x05, 0xfa, 0, 0, 0}, false); check("decrement memory", {0xff, 0x0d, 0xfa, 0, 0, 0}, false); check("push memory", {0xff, 0x35, 0xfa, 0, 0, 0}, true); diff --git a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp index 5870e3c5e7..94a34990c1 100644 --- a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp +++ b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -6,11 +7,13 @@ #include #include #include +#include namespace { using namespace Relinker::UnusedNidFilter; using Relinker::VirtualAddress; +using Relinker::RelinkerException; using Bytes = std::vector; constexpr VirtualAddress TextAddress = 0xec00001000; @@ -83,6 +86,48 @@ void CheckCalls() { RequireOffsets(*indirect, {0, 2, 3}); } +void CheckCallsBeforeZeroPadding() { + const Pointers pointers; + Bytes padded(0x21, 0); + padded[0] = 0xe8; + SetDisplacement(padded, 1, 5, TextAddress + 0x20); + const Bytes next{0x75, 0x64, 0x55, 0x48, 0x89, 0xe5, 0xc3}; + std::copy(next.begin(), next.end(), padded.begin() + 16); + padded[0x20] = 0xc3; + RequireOffsets(*BuildControlFlowGraph(padded, TextAddress, TextAddress, {}, pointers), {0, 0x20}); + RequireOffsets(*BuildControlFlowGraph(padded, TextAddress, TextAddress, {TextAddress + 16}, pointers), {0, 16, 18, 19, 22, 0x20}); + + Bytes indirect(0x11, 0); + indirect[0] = 0xff; + indirect[1] = 0xd0; + indirect[16] = 0xc3; + RequireOffsets(*BuildControlFlowGraph(indirect, TextAddress, TextAddress, {}, pointers), {0}); +} + +void CheckCallsThatStillFallThrough() { + const Pointers pointers; + Bytes boundary(0x13, 0x90); + boundary[11] = 0xe8; + SetDisplacement(boundary, 12, 16, TextAddress + 0x12); + boundary[16] = 0; + boundary[17] = 0; + boundary[18] = 0xc3; + RequireOffsets(*BuildControlFlowGraph(boundary, TextAddress, TextAddress, {}, pointers), {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 16, 18}); + + Bytes partial(0x11, 0x90); + partial[0] = 0xe8; + SetDisplacement(partial, 1, 5, TextAddress + 0x10); + partial[5] = 0; + partial[6] = 0; + partial[7] = 0xc3; + partial[0x10] = 0xc3; + RequireOffsets(*BuildControlFlowGraph(partial, TextAddress, TextAddress, {}, pointers), {0, 5, 7, 0x10}); + + Bytes truncated{0xe8, 0, 0, 0, 0, 0, 0xc3}; + SetDisplacement(truncated, 1, 5, TextAddress + 6); + RequireOffsets(*BuildControlFlowGraph(truncated, TextAddress, TextAddress, {}, pointers), {0, 5, 6}); +} + void CheckRelocatedTransfers() { constexpr auto slot = TextAddress + 0x1000; Bytes text{0xff, 0x15, 0, 0, 0, 0, 0x90, 0xc3, 0xff, 0x25, 0, 0, 0, 0, 0x90, 0x90, 0xc3}; @@ -138,6 +183,44 @@ void CheckSpeculativeRoots() { Require(threw, "Malformed mandatory root was treated as speculative"); } +void CheckTrailingZeroRoots() { + const Pointers pointers; + const Bytes lone{0x00}; + bool threw = false; + try { + BuildControlFlowGraph(lone, TextAddress, TextAddress, {}, pointers); + } catch (const Codegen::CodegenException&) { + threw = true; + } + Require(threw, "A lone zero instruction at the entry was not decoded"); + + const Bytes trailing{0xc3, 0x00}; + threw = false; + try { + BuildControlFlowGraph(trailing, TextAddress, TextAddress, {TextAddress + 1}, pointers); + } catch (const Codegen::CodegenException&) { + threw = true; + } + Require(threw, "A zero instruction at an extra entry was not decoded"); + + const Bytes jump{0xeb, 0x00, 0x00}; + threw = false; + try { + BuildControlFlowGraph(jump, TextAddress, TextAddress, {}, pointers); + } catch (const Codegen::CodegenException&) { + threw = true; + } + Require(threw, "A branch into a truncated zero instruction was not decoded"); + + const Bytes pair{0x00, 0x00}; + const auto complete = BuildControlFlowGraph(pair, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*complete, {0}); + + const Bytes immediate{0xb8, 0x00, 0x00, 0x00, 0x00}; + const auto zeroImmediate = BuildControlFlowGraph(immediate, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*zeroImmediate, {0}); +} + void CheckBorrowedReachability() { const Pointers pointers; const Bytes text{0x90, 0x90, 0xc3, 0x90, 0xc3}; @@ -151,6 +234,116 @@ void CheckBorrowedReachability() { RequireOffsets(*first, {0, 1, 3, 4}); } +void CheckAddressOverflow() { + const Pointers pointers; + const Bytes text{0x90, 0xc3}; + bool threw = false; + try { + BuildControlFlowGraph(text, std::numeric_limits::max(), std::numeric_limits::max(), {}, pointers); + } catch (const RelinkerException&) { + threw = true; + } + Require(threw, "Text section address space overflow was not rejected"); +} + +void CheckHighVirtualAddress() { + constexpr VirtualAddress highVaddr = 0xf00000001000; + const Pointers pointers; + Bytes text{0xeb, 0x02, 0x90, 0x90, 0xc3}; + const auto graph = BuildControlFlowGraph(text, highVaddr, highVaddr, {}, pointers); + std::unordered_set expected{highVaddr, highVaddr + 4}; + Require(graph->ReachableVaddrs() == expected, "Unexpected reachable instruction set at high virtual address"); + Require(graph->IsReachable(highVaddr), "Missing high entry address"); + Require(graph->IsReachable(highVaddr + 4), "Missing high branch target address"); +} + +void CheckRelativeJumpTables() { + constexpr std::size_t tableOffset = 16; + Bytes text(32, 0x90); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, TextAddress + tableOffset); + text[7] = 0xff; + text[8] = 0xe0; + const std::int32_t targets[]{-16, -9}; + std::memcpy(text.data() + tableOffset, targets, sizeof(targets)); + Require(Relinker::FindTrailingRelativeJumpTable(text) == tableOffset, "Relative jump table was not found"); + + const std::vector> functions{{TextAddress, TextAddress + 9}, {TextAddress + text.size(), TextAddress + text.size() + 8}}; + const auto tables = Relinker::FindRelativeJumpTables(text, TextAddress, functions); + Require(tables.size() == 1, "Expected one relative jump table"); + Require(tables[0].Function == TextAddress && tables[0].Begin == TextAddress + tableOffset && tables[0].End == TextAddress + text.size(), "Relative jump table range was incorrect"); + + Bytes oneEntry = text; + const std::int32_t singleTarget[]{-16}; + std::memcpy(oneEntry.data() + tableOffset, singleTarget, sizeof(singleTarget)); + const std::int32_t invalidSecondTarget[]{0x100}; + std::memcpy(oneEntry.data() + tableOffset + 4, invalidSecondTarget, sizeof(invalidSecondTarget)); + Require(Relinker::FindTrailingRelativeJumpTable(oneEntry) == oneEntry.size(), "Single-entry table was accepted"); + + Bytes invalidTarget = text; + const std::int32_t invalidTargets[]{0x100, 0x104}; + std::memcpy(invalidTarget.data() + tableOffset, invalidTargets, sizeof(invalidTargets)); + Require(Relinker::FindTrailingRelativeJumpTable(invalidTarget) == invalidTarget.size(), "Table with non-instruction targets was accepted"); + + Bytes nonPadding = text; + nonPadding.back() = 0x55; + Require(Relinker::FindTrailingRelativeJumpTable(nonPadding) == nonPadding.size(), "Table followed by non-padding bytes was accepted"); + + Bytes codeAfter = text; + codeAfter.resize(48, 0xcc); + codeAfter[32] = 0x55; + codeAfter[33] = 0x5d; + codeAfter[34] = 0xc3; + std::size_t tableEnd = 0; + Require(Relinker::FindTrailingRelativeJumpTable(codeAfter, TextAddress, &tableEnd) == tableOffset && tableEnd == 32, "Table padded to 16 bytes before more code was not found"); + const std::vector> codeAfterFunctions{{TextAddress, TextAddress + 9}, {TextAddress + codeAfter.size(), TextAddress + codeAfter.size() + 8}}; + const auto codeAfterTables = Relinker::FindRelativeJumpTables(codeAfter, TextAddress, codeAfterFunctions); + Require(codeAfterTables.size() == 1 && codeAfterTables[0].Begin == TextAddress + tableOffset && codeAfterTables[0].End == TextAddress + 32, "Table before more code did not end at its padding"); + + std::size_t shiftedEnd = 0; + Require(Relinker::FindTrailingRelativeJumpTable(nonPadding, TextAddress + 1, &shiftedEnd) == tableOffset && shiftedEnd == 31, "Table boundary ignored the code address"); + const auto shifted = Relinker::FindRelativeJumpTables(nonPadding, TextAddress + 1, {{TextAddress + 1, TextAddress + 10}, {TextAddress + 33, TextAddress + 41}}); + Require(shifted.size() == 1 && shifted[0].Begin == TextAddress + 17 && shifted[0].End == TextAddress + 32, "Function address was not passed to the table search"); + Require(Relinker::FindTrailingRelativeJumpTable(nonPadding, TextAddress + 8) == tableOffset, "Table ending on a 16-byte boundary was rejected"); + + Bytes padded = text; + padded.resize(48, 0xcc); + const auto paddedTables = Relinker::FindRelativeJumpTables(padded, TextAddress, {{TextAddress, TextAddress + 9}, {TextAddress + 48, TextAddress + 56}}); + Require(paddedTables.size() == 1 && paddedTables[0].End == TextAddress + 48, "Padding to the end of the range was cut at the boundary"); + + Bytes nopAtRangeEnd = text; + const std::uint8_t fourByteNop[]{0x0f, 0x1f, 0x40, 0x00}; + std::memcpy(nopAtRangeEnd.data() + 28, fourByteNop, sizeof(fourByteNop)); + const auto cut = std::span(nopAtRangeEnd).first(31); + Require(Relinker::FindTrailingRelativeJumpTable(cut) == cut.size(), "Padding check read past the inspected range"); + + Bytes longNops = text; + longNops.resize(48); + const std::uint8_t fifteenByteNop[]{0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x2e, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00}; + const std::uint8_t nineByteNop[]{0x66, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00}; + std::memcpy(longNops.data() + 24, fifteenByteNop, sizeof(fifteenByteNop)); + std::memcpy(longNops.data() + 39, nineByteNop, sizeof(nineByteNop)); + std::size_t longNopsEnd = 0; + Require(Relinker::FindTrailingRelativeJumpTable(longNops, TextAddress, &longNopsEnd) == tableOffset && longNopsEnd == 48, "Table followed by a NOP across the boundary was not found"); + Bytes longNopsCodeAfter = longNops; + longNopsCodeAfter.resize(64, 0xcc); + longNopsCodeAfter[48] = 0x55; + longNopsCodeAfter[49] = 0x5d; + longNopsCodeAfter[50] = 0xc3; + Require(Relinker::FindTrailingRelativeJumpTable(longNopsCodeAfter, TextAddress) == longNopsCodeAfter.size(), "Table ending inside a NOP was accepted"); + + Bytes cutShort = text; + cutShort.resize(56, 0xcc); + const std::int32_t cutShortTargets[]{-16, -7, -6, -5, 0x20, 0x20}; + std::memcpy(cutShort.data() + tableOffset, cutShortTargets, sizeof(cutShortTargets)); + cutShort[48] = 0x55; + cutShort[49] = 0x5d; + cutShort[50] = 0xc3; + Require(Relinker::FindRelativeJumpTables(cutShort, TextAddress, {{TextAddress, TextAddress + cutShort.size()}}).empty(), "Table followed by more of its function was accepted"); +} + } int main() { @@ -158,10 +351,16 @@ int main() { CheckFallthrough(); CheckBranches(); CheckCalls(); + CheckCallsBeforeZeroPadding(); + CheckCallsThatStillFallThrough(); CheckRelocatedTransfers(); CheckCodeAddresses(); CheckSpeculativeRoots(); + CheckTrailingZeroRoots(); CheckBorrowedReachability(); + CheckAddressOverflow(); + CheckHighVirtualAddress(); + CheckRelativeJumpTables(); std::cout << "Control-flow graph tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index 5a5e0b20e3..3357892f5e 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -1,14 +1,17 @@ #include "TestProgress.hpp" #include +#include #include #include #include #include #include +#include #include #include #include #include +#include namespace { @@ -22,7 +25,7 @@ void reject(TAction action) { throw std::runtime_error("Invalid conversion was accepted"); } -std::vector fixture() { +std::vector fixture(const std::string& needed = {}) { std::vector bytes(0x3000); const std::array magic{0x7f, 'E', 'L', 'F', 2, 1, 1}; std::copy(magic.begin(), magic.end(), bytes.begin()); @@ -50,7 +53,12 @@ std::vector fixture() { header(1, 1, 6, 0x2000, 0x1000); for (std::size_t index = 3; index < 8; ++index) header(index, 0x6fffff01, 0, 0, 0); bytes[0x1000] = 0xc3; - const std::array, 9> tags{{{5, 0x2600}, {10, 1}, {6, 0x2620}, {11, 24}, {7, 0x2700}, {8, 0}, {9, 24}, {4, 0x2680}, {0, 0}}}; + std::vector> tags{{5, 0x2600}, {10, needed.empty() ? 1 : needed.size() + 2}, {6, 0x2620}, {11, 24}, {7, 0x2700}, {8, 0}, {9, 24}, {4, 0x2680}}; + if (!needed.empty()) { + std::copy(needed.begin(), needed.end(), bytes.begin() + 0x2601); + tags.push_back({1, 1}); + } + tags.push_back({0, 0}); header(2, 2, 6, 0x2000, tags.size() * 16); for (std::size_t index = 0; index < tags.size(); ++index) { Io::WriteU64(bytes, 0x2000 + index * 16, tags[index].first); @@ -124,14 +132,22 @@ void fixedBase() { const Relinker::Conversion converter(progress); auto source = input(); source.Modules.push_back({"virtual/title/sce_module/plugin.prx", fixture()}); - const auto plain = converter.Convert(source, {}); + const auto standard = converter.Convert(source, {}); + Relinker::ConversionOptions lazy; + lazy.LazyBinding = true; + const auto independent = converter.Convert(input(), lazy); Relinker::ConversionOptions options; options.LinuxFixedBase = 0x400000000; const auto fixed = converter.Convert(source, options); - require(Io::ReadU16(plain.Executable, 16) == 3 && Io::ReadU16(fixed.Executable, 16) == 2, "Fixed base did not select ET_EXEC"); - require(Io::ReadU64(fixed.Executable, 24) == Io::ReadU64(plain.Executable, 24) + *options.LinuxFixedBase, "Fixed entry was not rebased above 4 GiB"); - require(fixed.Modules.front().Bytes == plain.Modules.front().Bytes, "Executable fixed base changed a bundled module"); + require(Io::ReadU16(standard.Executable, 16) == 2 && Io::ReadU16(fixed.Executable, 16) == 2, "Fixed base did not select ET_EXEC"); + require(Io::ReadU16(independent.Executable, 16) == 3, "Lazy binding did not keep ET_DYN"); + require(standard.LinuxBase == 0x400000 && fixed.LinuxBase == *options.LinuxFixedBase && !independent.LinuxBase, "Conversion reported the wrong Linux base"); + require(Io::ReadU64(fixed.Executable, 24) == Io::ReadU64(standard.Executable, 24) + *options.LinuxFixedBase - 0x400000, "Fixed entry was not rebased above 4 GiB"); + require(fixed.Modules.front().Bytes == standard.Modules.front().Bytes, "Executable fixed base changed a bundled module"); for (const auto target : {Relinker::TargetSystem::Windows, Relinker::TargetSystem::MacOs}) { + Relinker::ConversionOptions other; + other.Target = target; + require(!converter.Convert(source, other).LinuxBase, "A non-Linux conversion reported a Linux base"); options.Target = target; reject([&] { converter.Convert(source, options); }); } @@ -232,6 +248,58 @@ void cancellation() { } } +void baseRelocations() { + Elfpatcher::Windows::WindowsRelocationBuilder builder; + const auto relocs = builder.BuildBaseRelocations({0x10000, 0x10000, 0x10008}); + require(relocs.size() == 12, "Base relocation duplicates were not deduplicated"); + require(Io::ReadU32(relocs, 0) == 0x10000, "Wrong base relocation page RVA"); + require(Io::ReadU32(relocs, 4) == 12, "Wrong base relocation block size"); + require(Io::ReadU16(relocs, 8) == 0xa000, "Wrong first relocation entry"); + require(Io::ReadU16(relocs, 10) == 0xa008, "Wrong second relocation entry"); + reject([&] { (void)builder.BuildBaseRelocations({0x10ffc}); }); + reject([&] { (void)builder.BuildBaseRelocations({0x10ffd}); }); + reject([&] { (void)builder.BuildBaseRelocations({0x10fff}); }); +} + +std::size_t fileOffset(const std::vector& pe, std::uint32_t address) { + const auto header = Io::ReadU32(pe, 0x3c); + const auto sections = header + 24 + Io::ReadU16(pe, header + 20); + for (std::size_t index = 0; index < Io::ReadU16(pe, header + 6); ++index) { + const auto at = sections + index * 40; + const auto start = Io::ReadU32(pe, at + 12); + if (address >= start && address - start < Io::ReadU32(pe, at + 16)) return Io::ReadU32(pe, at + 20) + address - start; + } + throw std::runtime_error("Unmapped PE address"); +} + +bool exports(const std::vector& pe, std::string_view name) { + const auto address = Io::ReadU32(pe, Io::ReadU32(pe, 0x3c) + 24 + 112); + if (address == 0) return false; + const auto directory = fileOffset(pe, address); + const auto names = fileOffset(pe, Io::ReadU32(pe, directory + 32)); + for (std::size_t index = 0; index < Io::ReadU32(pe, directory + 24); ++index) { + if (reinterpret_cast(pe.data() + fileOffset(pe, Io::ReadU32(pe, names + index * 4))) == name) return true; + } + return false; +} + +void runtimeDirectories() { + TestProgress progress; + const Relinker::Conversion converter(progress); + Relinker::ConversionOptions options; + options.Target = Relinker::TargetSystem::Windows; + for (const auto* directory : {"sce_module", "sce_modules", "prx", "Media/Modules"}) { + auto source = input(); + const auto parent = source.ModuleRoot / directory; + source.Executable = fixture("needed.prx"); + source.Modules.push_back({parent / "needed.prx", fixture()}); + source.RuntimeDirectories.push_back(parent); + const auto output = converter.Convert(std::move(source), options); + require(output.Modules.size() == 1, "Module was not converted"); + require(exports(output.Modules.front().Bytes, Relinker::GuestInitializeExport) == (std::string_view(directory) == "Media/Modules"), "Wrong start of a runtime module the executable needs"); + } +} + } int main() { @@ -241,6 +309,8 @@ int main() { scanning(); diagnosticContext(); cancellation(); + baseRelocations(); + runtimeDirectories(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; diff --git a/core/relinker/relinker/tests/ElfFixture.hpp b/core/relinker/relinker/tests/ElfFixture.hpp index daa1f322cb..2ed1a52cae 100644 --- a/core/relinker/relinker/tests/ElfFixture.hpp +++ b/core/relinker/relinker/tests/ElfFixture.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -117,12 +118,17 @@ public: const auto phOffset = Read(elf, 32); const auto phSize = Read(elf, 54); const auto phCount = Read(elf, 56); + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + linkBase = std::numeric_limits::max(); + std::uint64_t last = 0; for (std::uint16_t index = 0; index < phCount; ++index) { const auto header = phOffset + index * phSize; if (Read(elf, header) != 1) continue; - size = std::max(size, Read(elf, header + 16) + Read(elf, header + 40)); + linkBase = std::min(linkBase, Read(elf, header + 16) / page * page); + last = std::max(last, Read(elf, header + 16) + Read(elf, header + 40)); } - if (size == 0) throw std::runtime_error("ELF has no PT_LOAD segment"); + if (last == 0) throw std::runtime_error("ELF has no PT_LOAD segment"); + size = static_cast(last - linkBase); base = mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (base == MAP_FAILED) throw std::runtime_error("Cannot map executable memory for the ELF image"); for (std::uint16_t index = 0; index < phCount; ++index) { @@ -131,15 +137,14 @@ public: const auto offset = Read(elf, header + 8); const auto fileSize = Read(elf, header + 32); if (offset > elf.size() || fileSize > elf.size() - offset) throw std::runtime_error("PT_LOAD exceeds the ELF file"); - std::memcpy(static_cast(base) + Read(elf, header + 16), elf.data() + offset, fileSize); + std::memcpy(static_cast(base) + (Read(elf, header + 16) - linkBase), elf.data() + offset, fileSize); } if (mprotect(base, size, PROT_NONE) != 0) throw std::runtime_error("Cannot protect the ELF image"); - const auto page = static_cast(sysconf(_SC_PAGESIZE)); for (std::uint16_t index = 0; index < phCount; ++index) { const auto header = phOffset + index * phSize; if (Read(elf, header) != 1) continue; const auto flags = Read(elf, header + 4); - const auto vaddr = Read(elf, header + 16); + const auto vaddr = Read(elf, header + 16) - linkBase; const auto start = vaddr / page * page; const auto end = (vaddr + Read(elf, header + 40) + page - 1) / page * page; const int protection = ((flags & 4) != 0 ? PROT_READ : 0) | ((flags & 2) != 0 ? PROT_WRITE : 0) | ((flags & 1) != 0 ? PROT_EXEC : 0); @@ -163,6 +168,7 @@ public: private: void* base = nullptr; std::size_t size = 0; + std::uint64_t linkBase = 0; }; } // namespace RelinkerTests diff --git a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp index 065688d6d0..154238155d 100644 --- a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp +++ b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -54,6 +55,22 @@ Bytes Fixture() { return bytes; } +Bytes TlsFixture(const std::uint64_t address, const std::uint64_t fileSize, const std::uint64_t memorySize, const std::uint64_t alignment) { + auto bytes = Fixture(); + std::memcpy(bytes.data() + 0x260, bytes.data() + 64, 56); + Write(bytes, 32, 0x260); + Write(bytes, 56, 2); + Write(bytes, 0x298, 7); + Write(bytes, 0x29c, 4); + Write(bytes, 0x2a0, 0x248); + Write(bytes, 0x2a8, address); + Write(bytes, 0x2b0, address); + Write(bytes, 0x2b8, fileSize); + Write(bytes, 0x2c0, memorySize); + Write(bytes, 0x2c8, alignment); + return bytes; +} + void Require(const bool condition) { if (!condition) throw std::runtime_error("Unexpected parsed value"); } @@ -76,10 +93,103 @@ void RejectAddress(Bytes bytes, const std::uint64_t address) { throw std::runtime_error("Invalid file mapping was accepted"); } +void RejectProgramHeaders(Bytes bytes) { + try { + Relinker::ElfReader(std::move(bytes)).ReadProgramHeaders(); + } catch (const Relinker::RelinkerException&) { + return; + } + throw std::runtime_error("Invalid program header was accepted"); +} + } int main() { const std::vector>> cases = { + {"load file range past end of file", [] { + auto bytes = Fixture(); + Write(bytes, 96, 0xd0); + Write(bytes, 104, 0xd0); + RejectProgramHeaders(std::move(bytes)); + }}, + {"load and uncovered SCE RELRO ending at end of file", [] { + for (const std::uint32_t type : {1u, 0x61000010u}) { + auto bytes = Fixture(); + Write(bytes, 64, type); + Write(bytes, 96, 0xc0); + Write(bytes, 104, 0xc0); + const auto headers = Relinker::ElfReader(std::move(bytes)).ReadProgramHeaders(); + Require(headers.size() == 1 && headers[0].Offset + headers[0].FileSize == 0x300); + } + }}, + {"uncovered SCE RELRO file size exceeds memory size", [] { + auto bytes = Fixture(); + Write(bytes, 64, 0x61000010); + Write(bytes, 96, 64); + Write(bytes, 104, 32); + RejectProgramHeaders(std::move(bytes)); + }}, + {"uncovered SCE RELRO file range past end of file", [] { + auto bytes = Fixture(); + Write(bytes, 64, 0x61000010); + Write(bytes, 96, 0xd0); + Write(bytes, 104, 0xd0); + RejectProgramHeaders(std::move(bytes)); + }}, + {"TLS image inside load memory and empty TLS placeholders", [] { + for (const auto& [address, fileSize, memorySize, alignment] : {std::tuple{0x1008, 8, 16, 8}, {0x1030, 16, 16, 16}, {0x9000, 0, 0, 0}, {0x9000, 0, 16, 16}}) { + const auto headers = Relinker::ElfReader(TlsFixture(address, fileSize, memorySize, alignment)).ReadProgramHeaders(); + Require(headers.size() == 2 && headers[1].Type == 7 && headers[1].MappedAddress == address); + } + }}, + {"TLS image inside uncovered SCE RELRO", [] { + auto bytes = TlsFixture(0x1008, 8, 16, 8); + Write(bytes, 0x260, 0x61000010); + const auto headers = Relinker::ElfReader(std::move(bytes)).ReadProgramHeaders(); + Require(headers.size() == 2 && headers[0].Type == 1 && headers[1].Type == 7 && headers[1].MappedAddress == 0x1008); + }}, + {"TLS image ending at end of file", [] { + auto bytes = TlsFixture(0x1008, 8, 16, 8); + Write(bytes, 0x2a0, 0x2f8); + const auto headers = Relinker::ElfReader(std::move(bytes)).ReadProgramHeaders(); + Require(headers.size() == 2 && headers[1].Offset + headers[1].FileSize == 0x300); + }}, + {"empty TLS image beyond end of file", [] { + constexpr std::uint64_t offsets[] = {0x300, 0x301, std::numeric_limits::max()}; + for (const std::uint64_t offset : offsets) { + auto bytes = TlsFixture(0x9000, 0, 16, 16); + Write(bytes, 0x2a0, offset); + const auto headers = Relinker::ElfReader(std::move(bytes)).ReadProgramHeaders(); + Require(headers.size() == 2 && headers[1].Offset == offset && headers[1].FileSize == 0); + } + }}, + {"TLS file range past end of file", [] { + constexpr std::uint64_t offsets[] = {0x2f9, 0x300, 0x301, std::numeric_limits::max() - 3, std::numeric_limits::max()}; + for (const std::uint64_t offset : offsets) { + auto bytes = TlsFixture(0x1008, 8, 16, 1); + Write(bytes, 0x2a0, offset); + RejectProgramHeaders(std::move(bytes)); + } + }}, + {"TLS image outside load memory", [] { + constexpr std::uint64_t addresses[] = {0x0ff8, 0x103c, 0x2000, std::numeric_limits::max() - 3}; + for (const std::uint64_t address : addresses) { + RejectProgramHeaders(TlsFixture(address, 8, 8, 1)); + } + }}, + {"TLS file size exceeds memory size", [] { + RejectProgramHeaders(TlsFixture(0x1008, 16, 8, 8)); + }}, + {"TLS alignment is not a power of two", [] { + RejectProgramHeaders(TlsFixture(0x1008, 8, 8, 3)); + }}, + {"second TLS segment", [] { + auto bytes = TlsFixture(0x1008, 8, 16, 8); + bytes.resize(0x308); + std::memcpy(bytes.data() + 0x2d0, bytes.data() + 0x298, 56); + Write(bytes, 56, 3); + RejectProgramHeaders(std::move(bytes)); + }}, {"section entry size field", [] { const auto sections = Relinker::ElfReader(Fixture()).ReadSectionHeaders(); Require(sections.size() == 3 && sections[1].Name == ".text" && sections[1].EntrySize == 24); diff --git a/core/relinker/relinker/tests/GamePatchTests.cpp b/core/relinker/relinker/tests/GamePatchTests.cpp new file mode 100644 index 0000000000..bde267682d --- /dev/null +++ b/core/relinker/relinker/tests/GamePatchTests.cpp @@ -0,0 +1,543 @@ +#include "TestProgress.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace Relinker; +using namespace Relinker::GamePatch; + +void require(const bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +template +void rejects(TAction action, const std::string& fragment) { + try { + action(); + } catch (const Domain::RelinkerException& error) { + require(std::string(error.what()).find(fragment) != std::string::npos, "wrong error, expected `" + fragment + "` in: " + error.what()); + return; + } + throw std::runtime_error("accepted: expected `" + fragment + "`"); +} + +std::string header(const std::string& body) { + return "format = 1\n" + body; +} + +void rejectsText(const std::string& text, const std::size_t line, const std::string& fragment = {}) { + rejects([&] { ParsePatchFile(text); }, "line " + std::to_string(line) + ":"); + if (!fragment.empty()) rejects([&] { ParsePatchFile(text); }, fragment); +} + +PatchEntry entry(const std::string& target, const std::uint64_t address, std::vector expected, std::vector value, const std::string& name = {}) { + PatchEntry result; + result.Target = target; + result.VirtualAddress = address; + result.Expected = std::move(expected); + result.Value = std::move(value); + result.Name = name; + result.Line = 1; + return result; +} + +std::vector twoSegmentFixture() { + std::vector bytes(0x3000); + const std::array magic{0x7f, 'E', 'L', 'F', 2, 1, 1}; + std::copy(magic.begin(), magic.end(), bytes.begin()); + Io::WriteU16(bytes, 16, 3); + Io::WriteU16(bytes, 18, 62); + Io::WriteU32(bytes, 20, 1); + Io::WriteU64(bytes, 32, 64); + Io::WriteU16(bytes, 52, 64); + Io::WriteU16(bytes, 54, 56); + Io::WriteU16(bytes, 56, 2); + const auto segment = [&](const std::size_t index, const std::uint64_t offset, const std::uint64_t address, const std::uint64_t fileSize, const std::uint64_t memorySize) { + const auto at = 64 + index * 56; + Io::WriteU32(bytes, at, 1); + Io::WriteU32(bytes, at + 4, 6); + Io::WriteU64(bytes, at + 8, offset); + Io::WriteU64(bytes, at + 16, address); + Io::WriteU64(bytes, at + 24, address); + Io::WriteU64(bytes, at + 32, fileSize); + Io::WriteU64(bytes, at + 40, memorySize); + Io::WriteU64(bytes, at + 48, 8); + }; + segment(0, 0, 0, 0x1100, 0x1100); + segment(1, 0x2000, 0x1100, 0x100, 0x300); + for (std::size_t index = 0; index < 0x100; ++index) { + bytes[0x1000 + index] = static_cast(index); + bytes[0x2000 + index] = static_cast(0x80 + index); + } + return bytes; +} + +std::vector conversionFixture() { + std::vector bytes(0x3000); + const std::array magic{0x7f, 'E', 'L', 'F', 2, 1, 1}; + std::copy(magic.begin(), magic.end(), bytes.begin()); + Io::WriteU16(bytes, 16, 3); + Io::WriteU16(bytes, 18, 62); + Io::WriteU32(bytes, 20, 1); + Io::WriteU64(bytes, 24, 0x1000); + Io::WriteU64(bytes, 32, 64); + Io::WriteU16(bytes, 52, 64); + Io::WriteU16(bytes, 54, 56); + Io::WriteU16(bytes, 56, 8); + Io::WriteU16(bytes, 58, 64); + const auto programHeader = [&](std::size_t index, unsigned type, unsigned flags, unsigned offset, unsigned size) { + const auto at = 64 + index * 56; + Io::WriteU32(bytes, at, type); + Io::WriteU32(bytes, at + 4, flags); + Io::WriteU64(bytes, at + 8, offset); + Io::WriteU64(bytes, at + 16, offset); + Io::WriteU64(bytes, at + 24, offset); + Io::WriteU64(bytes, at + 32, size); + Io::WriteU64(bytes, at + 40, size); + Io::WriteU64(bytes, at + 48, 8); + }; + programHeader(0, 1, 5, 0x1000, 8); + programHeader(1, 1, 6, 0x2000, 0x1000); + for (std::size_t index = 3; index < 8; ++index) programHeader(index, 0x6fffff01, 0, 0, 0); + bytes[0x1000] = 0xc3; + const std::array, 9> tags{{{5, 0x2600}, {10, 1}, {6, 0x2620}, {11, 24}, {7, 0x2700}, {8, 0}, {9, 24}, {4, 0x2680}, {0, 0}}}; + programHeader(2, 2, 6, 0x2000, tags.size() * 16); + for (std::size_t index = 0; index < tags.size(); ++index) { + Io::WriteU64(bytes, 0x2000 + index * 16, tags[index].first); + Io::WriteU64(bytes, 0x2008 + index * 16, tags[index].second); + } + Io::WriteU32(bytes, 0x2680, 1); + Io::WriteU32(bytes, 0x2684, 1); + return bytes; +} + +ConversionInput conversionInput(const std::vector& executable) { + ConversionInput result; + result.Executable = executable; + result.InputPath = "virtual/title/custom-name.bin"; + result.OutputPath = "virtual/output/title"; + result.ModuleRoot = "virtual/title"; + result.Modules.push_back({"virtual/title/sce_module/plugin.prx", conversionFixture()}); + return result; +} + +void grammarAccepted() { + const auto file = ParsePatchFile( + "# leading comment\n" + "format = 1 # trailing comment\n" + "relinker_version = \"x\"\n" + "relinker_commit = \"y\"\n" + "config = \"z\"\n" + "config_sha256 = \"w\"\n" + "main_target = \"eboot.elf\"\n" + "\n" + "[[patch]]\n" + "name = \"pool_size\"\n" + "target = \"eboot.elf\"\n" + "va = 0x1234\n" + "type = \"i32\"\n" + "expected = 0x10000000\n" + "value = 0x18000000\n" + "max = 0x7f000000\n" + "align = 0x1000\n" + "description = \"has # inside\"\n" + "\n" + "[[patch]]\n" + "target = \"sce_module/libc.prx\"\n" + "va = 4096\n" + "expected = \"00ff\"\n" + "value = \"AB12\"\n"); + require(file.MainTarget == std::string("eboot.elf"), "main_target was not read"); + require(file.Entries.size() == 2, "wrong entry count"); + const auto& typed = file.Entries[0]; + require(typed.Name == "pool_size" && typed.Target == "eboot.elf" && typed.VirtualAddress == 0x1234, "typed entry fields"); + require(typed.Expected == std::vector({0, 0, 0, 0x10}) && typed.Value == std::vector({0, 0, 0, 0x18}), "typed encoding is not little-endian"); + require(typed.Description == "has # inside" && typed.Line == 9, "description or line"); + const auto& raw = file.Entries[1]; + require(raw.Name.empty() && raw.VirtualAddress == 4096, "raw entry fields"); + require(raw.Expected == std::vector({0x00, 0xff}) && raw.Value == std::vector({0xab, 0x12}), "raw hex bytes"); + require(!ParsePatchFile("format = 1\n").MainTarget && ParsePatchFile("format = 1\n").Entries.empty(), "empty file"); +} + +void grammarRejected() { + rejects([] { ParsePatchFile(""); }, "format = 1"); + rejectsText("format = 2\n", 1, "unsupported format"); + rejectsText(header("flavour = 1\n"), 2, "unknown top-level key"); + rejectsText(header("[[patch]]\ntarget = \"a\"\nva = 1\nexpected = \"00\"\nvalue = \"01\"\ncolour = 1\n"), 7, "unknown key"); + rejectsText(header("[[patch]]\ntarget = \"a\"\ntarget = \"b\"\n"), 4, "duplicate key"); + rejectsText(header("[patch]\n"), 2, "unsupported table header"); + rejectsText(header("[other]\n"), 2, "unsupported table header"); + rejectsText(header("[[patch]]\ndescription = \"a\\nb\"\n"), 3, "escapes"); + rejectsText(header("[[patch]]\ndescription = [1, 2]\n"), 3, "arrays"); + rejectsText(header("[[patch]]\ndescription = 'x'\n"), 3, "single-quoted"); + rejectsText(header("[[patch]]\ndescription = { a = 1 }\n"), 3, "inline tables"); + rejectsText(header("[[patch]]\ndescription = \"\"\"x\"\"\"\n"), 3, "multiline"); + rejectsText(header("[[patch]]\nva = 1.5\n"), 3, "floats"); + rejectsText(header("[[patch]]\nva = true\n"), 3, "booleans"); + rejectsText(header("[[patch]]\nsub.key = 1\n"), 3, "dotted keys"); + rejectsText(header("[[patch]]\n\"quoted\" = 1\n"), 3, "quoted keys"); + rejectsText(header("[[patch]]\nva\n"), 3, "key = value"); + rejectsText(header("[[patch]]\nva = 1_000\n"), 3); + rejectsText(header("[[patch]]\nva = \"unterminated\n"), 3, "unterminated"); +} + +std::string typedEntry(const std::string& type, const std::string& expected, const std::string& value, const std::string& extra = {}) { + return header("[[patch]]\ntarget = \"a\"\nva = 1\ntype = \"" + type + "\"\nexpected = " + expected + "\nvalue = " + value + "\n" + extra); +} + +void integerRanges() { + struct Case { const char* Type; const char* Fits; const char* Overflows; std::size_t Width; }; + for (const auto& c : {Case{"u8", "255", "256", 1}, Case{"u16", "65535", "65536", 2}, Case{"u32", "0xffffffff", "0x100000000", 4}, Case{"u64", "0xffffffffffffffff", "0x10000000000000000", 8}, + Case{"i8", "127", "128", 1}, Case{"i16", "32767", "32768", 2}, Case{"i32", "0x7fffffff", "0x80000000", 4}, Case{"i64", "0x7fffffffffffffff", "0x8000000000000000", 8}}) { + const auto accepted = ParsePatchFile(typedEntry(c.Type, "0", c.Fits)); + require(accepted.Entries.front().Value.size() == c.Width, std::string("width of ") + c.Type); + rejects([&] { ParsePatchFile(typedEntry(c.Type, "0", c.Overflows)); }, "line 7"); + } + require(ParsePatchFile(typedEntry("i32", "-1", "-2147483648")).Entries.front().Value == std::vector({0, 0, 0, 0x80}), "i32 minimum"); + require(ParsePatchFile(typedEntry("i8", "-128", "-1")).Entries.front().Value == std::vector({0xff}), "i8 -1"); + require(ParsePatchFile(typedEntry("i64", "-1", "-1")).Entries.front().Expected == std::vector(8, 0xff), "i64 -1"); + rejects([] { ParsePatchFile(typedEntry("i8", "0", "-129")); }, "line 7"); + rejects([] { ParsePatchFile(typedEntry("u8", "0", "-1")); }, "line 7"); + rejects([] { ParsePatchFile(typedEntry("i16", "40000", "0")); }, "line 6"); + rejects([] { ParsePatchFile(typedEntry("f32", "0", "0")); }, "unknown type"); + rejects([] { ParsePatchFile(typedEntry("u32", "\"00\"", "1")); }, "integer"); +} + +void boundsAndWidth() { + ParsePatchFile(typedEntry("u32", "0", "8", "min = 8\nmax = 8\nalign = 8\n")); + rejects([] { ParsePatchFile(typedEntry("u32", "0", "7", "min = 8\n")); }, "below min"); + rejects([] { ParsePatchFile(typedEntry("u32", "0", "9", "max = 8\n")); }, "above max"); + rejects([] { ParsePatchFile(typedEntry("u32", "0", "12", "align = 8\n")); }, "multiple of align"); + rejects([] { ParsePatchFile(typedEntry("u32", "0", "8", "align = 0\n")); }, "align must be positive"); + ParsePatchFile(typedEntry("i32", "0", "-2097152", "align = 2097152\n")); + rejects([] { ParsePatchFile(typedEntry("i32", "0", "-1048576", "align = 2097152\n")); }, "multiple of align"); + rejects([] { ParsePatchFile(typedEntry("u32", "0", "768MiB")); }, "line 7"); + rejects([] { ParsePatchFile(typedEntry("u32", "0", "007")); }, "leading zeros"); + ParsePatchFile(typedEntry("u32", "0", "0")); + ParsePatchFile(typedEntry("i32", "-0", "10")); + rejects([] { ParsePatchFile(header("[[patch]]\ntarget = \"a\"\nva = 1\nexpected = \"00\"\nvalue = \"0000\"\nmin = 1\n")); }, "requires a type"); + rejects([] { ParsePatchFile(header("[[patch]]\ntarget = \"a\"\nva = 1\nexpected = \"00\"\nvalue = \"0000\"\n")); }, "same width"); + rejects([] { ParsePatchFile(header("[[patch]]\ntarget = \"a\"\nva = 1\nexpected = \"0\"\nvalue = \"1\"\n")); }, "even-length"); + rejects([] { ParsePatchFile(header("[[patch]]\ntarget = \"a\"\nva = 1\nexpected = \"zz\"\nvalue = \"00\"\n")); }, "non-hex"); + rejects([] { ParsePatchFile(header("[[patch]]\nva = 1\nexpected = \"00\"\nvalue = \"01\"\n")); }, "missing key `target`"); + rejects([] { ParsePatchFile(header("[[patch]]\ntarget = \"a\"\nva = -1\nexpected = \"00\"\nvalue = \"01\"\n")); }, "va must be"); +} + +std::string namedEntry(const std::string& name, const std::string& extra = {}) { + return header("[[patch]]\nname = \"" + name + "\"\ntarget = \"a\"\nva = 1\nexpected = \"00\"\nvalue = \"01\"\n" + extra); +} + +void names() { + require(ParsePatchFile(namedEntry("pool_size_2")).Entries.front().Name == "pool_size_2", "valid name"); + for (const char* bad : {"Upper", "has-dash", "has space", "", "dot.ted", "\xc3\xa9"}) rejects([&] { ParsePatchFile(namedEntry(bad)); }, "line 3: name must be"); + rejects([] { ParsePatchFile(header("[[patch]]\nname = 5\ntarget = \"a\"\nva = 1\nexpected = \"00\"\nvalue = \"01\"\n")); }, "line 3: name must be"); + rejects([] { ParsePatchFile(namedEntry("same") + "\n[[patch]]\nname = \"same\"\ntarget = \"a\"\nva = 9\nexpected = \"00\"\nvalue = \"01\"\n"); }, "line 10: duplicate name"); + ParsePatchFile(namedEntry("one") + "\n[[patch]]\nname = \"two\"\ntarget = \"a\"\nva = 9\nexpected = \"00\"\nvalue = \"01\"\n"); +} + +void ranges() { + const ElfReader reader(twoSegmentFixture()); + const auto resolved = ResolveRanges(reader, {entry("t", 0x1000, {0, 1}, {9, 9}), entry("t", 0x1100, {0x80}, {1}), entry("t", 0x11fc, {0xfc, 0xfd, 0xfe, 0xff}, {0, 0, 0, 0})}); + require(resolved[0].Offset == 0x1000 && resolved[0].Length == 2, "first segment translation"); + require(resolved[1].Offset == 0x2000, "second segment translation"); + require(resolved[2].Offset == 0x20fc, "second segment end"); + rejects([&] { ResolveRanges(reader, {entry("t", 0x10fe, {0, 0, 0, 0}, {1, 1, 1, 1})}); }, "one PT_LOAD"); + rejects([&] { ResolveRanges(reader, {entry("t", 0x1200, {0}, {1})}); }, "one PT_LOAD"); + rejects([&] { ResolveRanges(reader, {entry("t", 0x11fe, {0, 0, 0}, {1, 1, 1})}); }, "one PT_LOAD"); + rejects([&] { ResolveRanges(reader, {entry("t", 0x5000, {0}, {1})}); }, "one PT_LOAD"); + rejects([&] { ResolveRanges(reader, {entry("t", 0x1000, {0, 1, 2}, {1, 1, 1}), entry("t", 0x1002, {2, 3}, {1, 1})}); }, "overlaps"); + ResolveRanges(reader, {entry("t", 0x1000, {0, 1}, {1, 1}), entry("t", 0x1002, {2, 3}, {1, 1})}); +} + +void headerRange() { + const ElfReader reader(twoSegmentFixture()); + rejects([&] { ResolveRanges(reader, {entry("t", 0x0, {0x7f}, {0})}); }, "program header table"); + rejects([&] { ResolveRanges(reader, {entry("t", 0x40, {0}, {1})}); }, "program header table"); + rejects([&] { ResolveRanges(reader, {entry("t", 0xaf, {0}, {1})}); }, "program header table"); + const auto ok = ResolveRanges(reader, {entry("t", 0xb0, {0}, {1})}); + require(ok.front().Offset == 0xb0, "range after the header table was rejected"); +} + +void applyAndMismatch() { + auto bytes = twoSegmentFixture(); + const auto original = bytes; + const std::vector entries{entry("t", 0x1000, {0, 1}, {9, 8}, "first"), entry("t", 0x1100, {0x80}, {1})}; + const auto patched = PatchTarget(bytes, "t", entries); + require(patched.Applied.size() == 2 && patched.Applied[0].Offset == 0x1000 && patched.Applied[0].Name == "first", "applied records"); + require(bytes[0x1000] == 9 && bytes[0x1001] == 8 && bytes[0x2000] == 1, "values were not written"); + for (std::size_t index = 0; index < bytes.size(); ++index) + if (index != 0x1000 && index != 0x1001 && index != 0x2000) require(bytes[index] == original[index], "byte outside the patch changed"); + + bytes = original; + rejects([&] { PatchTarget(bytes, "t", {entry("t", 0x1000, {0, 1}, {9, 8}), entry("t", 0x1100, {0x81}, {1})}); }, "expected 81 found 80"); + require(bytes == original, "a mismatch wrote bytes"); + require(PatchTarget(bytes, "other", entries).Applied.empty() && bytes == original, "entry hit another target"); +} + +void overlapWithWriters() { + const std::vector patched{{0x1010, 2, 0x401010}}; + const auto reject = [&](const WriterSites& sites, const std::string& writer) { rejects([&] { CheckOverlaps("t", patched, sites); }, writer); }; + const std::vector inPlace{{"VPSHUFB", 0x100f, 4, 4, Codegen::Amd64OnlyLowering::InPlace}}; + reject({.Reports = inPlace}, "Intel in-place report"); + const std::vector kept{{"VPSHUFB", 0x100f, 4, 4, Codegen::Amd64OnlyLowering::Kept}}; + CheckOverlaps("t", patched, {.Reports = kept}); + const std::vector distant{{"VPSHUFB", 0x1000, 4, 4, Codegen::Amd64OnlyLowering::InPlace}}; + CheckOverlaps("t", patched, {.Reports = distant}); + const std::vector absorbed{{0x1008, 0x401008, 0x10, {}, {}, 0}}; + reject({.Trampolines = absorbed}, "Intel trampoline site"); + const std::vector shortSite{{0x1008, 0x401008, 4, {}, {}, 0}}; + CheckOverlaps("t", patched, {.Trampolines = shortSite}); + const std::vector relink{{0x1011, {1, 2, 3}}}; + reject({.RelinkPatches = relink}, "relinker patch"); + const std::vector relinkAfter{{0x1012, {1, 2, 3}}}; + CheckOverlaps("t", patched, {.RelinkPatches = relinkAfter}); + const std::vector trap{{0x1011, 2, "syscall trap"}}; + reject({.Traps = trap}, "syscall trap"); + const std::vector trapBefore{{0x100e, 2, "syscall trap"}}; + CheckOverlaps("t", patched, {.Traps = trapBefore}); + const std::vector window{{0x100f, 4, "window"}}; + reject({.Traps = window}, "window"); +} + +void conversion() { + TestProgress progress; + const Conversion converter(progress); + const auto executable = conversionFixture(); + ConversionOptions plain; + plain.TargetName = "custom-name.bin"; + const auto baseline = converter.Convert(conversionInput(executable), plain); + require(baseline.AppliedPatches.empty(), "patches applied without entries"); + + ConversionOptions patched = plain; + patched.Patches = {entry("custom-name.bin", 0x1001, {0}, {0x90}, "nop"), entry("sce_module/plugin.prx", 0x1002, {0}, {0x41})}; + const auto output = converter.Convert(conversionInput(executable), patched); + require(output.AppliedPatches.size() == 2, "executable and module patches were not both applied"); + require(output.AppliedPatches[0].Target == "custom-name.bin" && output.AppliedPatches[1].Target == "sce_module/plugin.prx", "applied targets"); + require(output.Executable != baseline.Executable && output.Modules.front().Bytes != baseline.Modules.front().Bytes, "patches changed nothing"); + require(progress.Messages.find("Patch: custom-name.bin [nop] va=0x1001 off=0x1001 00 -> 90\n") != std::string::npos, "progress line: " + progress.Messages); + require(progress.Messages.find("Patch: sce_module/plugin.prx va=0x1002 off=0x1002 00 -> 41\n") != std::string::npos, "module progress line"); + + ConversionOptions other = patched; + other.TargetName = "other.bin"; + auto different = executable; + different[0x1005] = 0x90; + const auto second = converter.Convert(conversionInput(different), other); + require(second.Modules.size() == output.Modules.size() && second.Modules.front().Bytes == output.Modules.front().Bytes, "shared modules differ between executables"); + require(second.AppliedPatches.size() == 1, "an executable entry hit another target"); + + ConversionOptions failing = plain; + failing.Patches = {entry("custom-name.bin", 0x1001, {7}, {1})}; + rejects([&] { converter.Convert(conversionInput(executable), failing); }, "expected 07 found 00"); + failing.Patches = {entry("sce_module/plugin.prx", 0x1002, {7}, {1})}; + rejects([&] { converter.Convert(conversionInput(executable), failing); }, "expected 07 found 00"); + failing.Patches = {entry("custom-name.bin", 0x1000, {0xc3, 0}, {0, 0}), entry("custom-name.bin", 0x1001, {0}, {1})}; + rejects([&] { converter.Convert(conversionInput(executable), failing); }, "overlaps"); + + const auto repeated = converter.Convert(conversionInput(executable), plain); + require(repeated.Executable == baseline.Executable && repeated.Modules.front().Bytes == baseline.Modules.front().Bytes, "no entries changed the output"); +} + +void targets() { + const std::vector entries{entry("a.elf", 1, {0}, {1}), entry("sce_module/libc.prx", 1, {0}, {1})}; + MatchTargets(entries, {"a.elf", "b.elf", "sce_module/libc.prx"}); + rejects([&] { MatchTargets(entries, {"a.elf", "b.elf"}); }, "unmatched patch target: sce_module/libc.prx"); + rejects([&] { MatchTargets(entries, {"a.elf", "a.elf", "sce_module/libc.prx"}); }, "ambiguous patch target: a.elf"); + MatchTargets({}, {}); +} + +void verify() { + auto bytes = twoSegmentFixture(); + const std::vector executables{{"main.elf", bytes}}; + auto file = ParsePatchFile(header("main_target = \"main.elf\"\n[[patch]]\nname = \"one\"\ntarget = \"main.elf\"\nva = 0x1000\nexpected = \"0001\"\nvalue = \"0908\"\n" + "[[patch]]\ntarget = \"main.elf\"\nva = 0x1100\nexpected = \"ff\"\nvalue = \"01\"\n" + "[[patch]]\ntarget = \"sce_module/libc.prx\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n")); + const auto before = bytes; + const std::vector modules{{"sce_module/libc.prx", twoSegmentFixture()}}; + auto input = VerifyPatches(file, executables, modules, false); + require(input.size() == 3 && input[0].Status == VerifyStatus::Applicable && input[0].Name == "one" && input[0].Offset == 0x1000, "input applicable"); + require(input[1].Status == VerifyStatus::Mismatch && input[1].Detail.find("expected ff found 80") != std::string::npos, "input mismatch"); + require(input[2].Status == VerifyStatus::Applicable, "module entry in input mode"); + require(bytes == before && executables.front().Bytes == before, "verify wrote bytes"); + + auto patchedBytes = twoSegmentFixture(); + patchedBytes[0x1000] = 9; + patchedBytes[0x1001] = 8; + patchedBytes[0x2000] = 1; + const std::vector outputs{{"main.elf", patchedBytes}}; + auto output = VerifyPatches(file, outputs, {}, true); + require(output[0].Status == VerifyStatus::Applied && output[1].Status == VerifyStatus::Applied, "output applied"); + require(output[2].Status == VerifyStatus::NotChecked && output[2].Detail == "module, or executable not named with --executable", "output module not-checked"); + rejects([&] { VerifyPatches(file, {{"main.elf", patchedBytes}, {"main.elf", patchedBytes}}, {}, true); }, "ambiguous patch target: main.elf"); + rejects([&] { VerifyPatches(file, {{"main.elf", patchedBytes}, {"bin/a.elf", patchedBytes}, {"bin/a.elf", patchedBytes}}, {}, true); }, "ambiguous patch target: bin/a.elf"); + output = VerifyPatches(file, {{"main.elf", twoSegmentFixture()}}, {}, true); + require(output[0].Status == VerifyStatus::Mismatch && output[0].Detail.find("not applied") != std::string::npos, "unpatched output"); + + rejects([&] { VerifyPatches(ParsePatchFile(header("[[patch]]\ntarget = \"typo.prx\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n")), executables, modules, false); }, "unmatched patch target: typo.prx"); + rejects([&] { VerifyPatches(ParsePatchFile(header("[[patch]]\ntarget = \"typo.prx\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n")), executables, modules, false); }, "--verify-output"); + rejects([&] { VerifyPatches(ParsePatchFile(header("[[patch]]\ntarget = \"main.elf\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n")), outputs, {}, true); }, "no main_target"); + auto exec = twoSegmentFixture(); + Io::WriteU16(exec, 16, 2); + rejects([&] { VerifyPatches(file, {{"main.elf", exec}}, {}, true); }, "has no linux_base"); + const auto based = ParsePatchFile(header("main_target = \"main.elf\"\nlinux_base = 0x400000\n[[patch]]\nname = \"one\"\ntarget = \"main.elf\"\nva = 0x1000\nexpected = \"0001\"\nvalue = \"0908\"\n" + "[[patch]]\ntarget = \"main.elf\"\nva = 0x1100\nexpected = \"ff\"\nvalue = \"01\"\n")); + require(based.LinuxBase == 0x400000, "linux_base was not read"); + auto rebased = patchedBytes; + Io::WriteU16(rebased, 16, 2); + for (const std::size_t field : {64 + 16, 64 + 24, 120 + 16, 120 + 24}) Io::WriteU64(rebased, field, Io::ReadU64(rebased, field) + 0x400000); + output = VerifyPatches(based, {{"main.elf", rebased}}, {}, true); + require(output.size() == 2 && output[0].Status == VerifyStatus::Applied && output[1].Status == VerifyStatus::Applied, "fixed-base output applied"); + require(output[0].VirtualAddress == 0x1000 && output[0].Offset == 0x1000 && output[1].Offset == 0x2000, "fixed-base output reported the rebased address"); + output = VerifyPatches(based, {{"main.elf", exec}}, {}, true); + require(output[0].Status == VerifyStatus::Mismatch && output[0].Detail.find("PT_LOAD") != std::string::npos, "linux_base was not applied"); + rejects([&] { VerifyPatches(based, outputs, {}, true); }, "has linux_base = 0x400000"); + rejects([&] { VerifyPatches(ParsePatchFile(header("main_target = \"main.elf\"\nlinux_base = 0x400000\n[[patch]]\ntarget = \"main.elf\"\nva = 0xffffffffffffff00\nexpected = \"00\"\nvalue = \"01\"\n")), {{"main.elf", rebased}}, {}, true); }, "lies outside the address space at linux_base"); + rejects([&] { VerifyPatches(file, {{"main.elf", {'M', 'Z', 0x90, 0}}}, {}, true); }, "verify the input instead"); + rejects([&] { VerifyPatches(file, {{"main.elf", {0xcf, 0xfa, 0xed, 0xfe, 7, 0, 0, 1}}}, {}, true); }, "verify the input instead"); + rejects([&] { VerifyPatches(file, {{"main.elf", {}}}, {}, true); }, "verify the input instead"); + + auto sce = twoSegmentFixture(); + Io::WriteU16(sce, 16, 0xfe10); + require(VerifyPatches(ParsePatchFile(header("[[patch]]\ntarget = \"main.elf\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n")), {{"main.elf", sce}}, {}, false).front().Status == VerifyStatus::Applicable, + "input mode must accept any e_type"); + + require(OutputExecutablePath("out/app.elf", "bin/tool.elf") == std::filesystem::path("out/bin.tool.elf.elf"), "output executable name"); + require(OutputExecutablePath("out/app.exe", "./a/b/c.bin") == std::filesystem::path("out/a.b.c.bin.exe"), "output executable extension"); + for (const char* bad : {"../x", "/abs/x", "dir/", ""}) rejects([&] { OutputExecutablePath("out/app.elf", bad); }, "--executable must be a file"); +} + +void rangeFailureInVerify() { + const auto file = ParsePatchFile(header("[[patch]]\ntarget = \"main.elf\"\nva = 0x1200\nexpected = \"00\"\nvalue = \"01\"\n")); + const auto results = VerifyPatches(file, {{"main.elf", twoSegmentFixture()}}, {}, false); + require(results.front().Status == VerifyStatus::Mismatch && results.front().Detail.find("PT_LOAD") != std::string::npos, "range failure should be a mismatch"); +} + +void manifest() { + const AppliedPatch first{"main.elf", 0x1000, 0x1000, {0, 1}, {9, 8}, "set \"x\" later", "alpha"}; + const AppliedPatch second{"sce_module/libc.prx", 0x2000, 0x2000, {5}, {6}, "", ""}; + AppliedPatch duplicate = first; + const auto merged = AggregateManifest({{first, second}, {duplicate, second}}); + require(merged.size() == 2, "records were not deduplicated"); + rejects([&] { WriteManifest(merged, "main.elf", ""); }, "Patch manifest cannot hold the description: set \"x\" later"); + + const AppliedPatch plain{"main.elf", 0x1000, 0x1000, {0, 1}, {9, 8}, "plain description", "alpha"}; + const auto text = WriteManifest(AggregateManifest({{plain}, {second}}), "main.elf", ""); + const auto parsed = ParsePatchFile(text); + require(parsed.MainTarget == std::string("main.elf"), "main_target did not round-trip"); + require(parsed.Entries.size() == 2, "entry count did not round-trip"); + require(parsed.Entries[0].Name == "alpha" && parsed.Entries[0].Target == "main.elf" && parsed.Entries[0].VirtualAddress == 0x1000, "first entry"); + require(parsed.Entries[0].Expected == plain.Expected && parsed.Entries[0].Value == plain.Value && parsed.Entries[0].Description == "plain description", "first bytes"); + require(parsed.Entries[1].Name.empty() && parsed.Entries[1].Target == "sce_module/libc.prx" && parsed.Entries[1].Description.empty(), "second entry"); + require(text.find("name = \"alpha\"") != std::string::npos && text.find("main_target = \"main.elf\"") != std::string::npos, "manifest text"); + require(WriteManifest(AggregateManifest({{plain}, {second}}), "main.elf", "") == text, "manifest is not deterministic"); + require(text.find("linux_base") == std::string::npos && !parsed.LinuxBase, "a manifest without a base wrote linux_base"); + const auto based = WriteManifest({plain}, "main.elf", "", 0x400000); + require(based.find("main_target = \"main.elf\"\nlinux_base = 0x400000\n") != std::string::npos && ParsePatchFile(based).LinuxBase == 0x400000, "linux_base did not round-trip"); + rejectsText(header("linux_base = \"0x400000\"\n"), 2, "linux_base must be a non-negative integer"); + rejectsText(header("linux_base = -1\n"), 2, "linux_base must be a non-negative integer"); +} + +void reservedNames() { + for (const char* reserved : {"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "linux_base", "settings", "settings_sha256"}) + rejects([&] { ParsePatchFile(namedEntry(reserved)); }, "line 3: name is reserved"); + ParsePatchFile(header("settings = \"a\"\nsettings_sha256 = \"b\"\n")); +} + +void relocationOverlap() { + std::vector table(48); + Io::WriteU64(table, 0, 0x2010); + Io::WriteU64(table, 24, 0x2100); + const std::vector hit{{0x1010, 2, 0x2017}}; + const std::vector adjacent{{0x1010, 2, 0x2018}}; + const std::vector before{{0x1010, 2, 0x200f}}; + rejects([&] { CheckOverlaps("t", hit, {.RelaData = table}); }, "RelaData relocation target"); + rejects([&] { CheckOverlaps("t", hit, {.RelaPltData = table}); }, "RelaPltData relocation target"); + rejects([&] { CheckOverlaps("t", {{0x1010, 1, 0x2100}}, {.RelaData = table}); }, "relocation target at va=0x2100"); + rejects([&] { CheckOverlaps("t", before, {.RelaPltData = table}); }, "RelaPltData"); + CheckOverlaps("t", adjacent, {.RelaData = table, .RelaPltData = table}); + CheckOverlaps("t", {{0x1010, 2, 0x200c}}, {.RelaData = table}); +} + +void manifestConfig() { + const AppliedPatch patch{"main.elf", 0x1000, 0x1000, {0}, {1}, "", ""}; + const auto config = [&](const std::string& path) { return WriteManifest({patch}, "main.elf", path); }; + require(config("").find("config =") == std::string::npos, "empty config was recorded"); + require(config("dir/p.toml").find("config = \"dir/p.toml\"\n") != std::string::npos, "forward-slash path"); + rejects([&] { config("dir/p\x7f.toml"); }, "Patch manifest cannot hold the config"); + rejects([&] { config("dir/p\x01.toml"); }, "Patch manifest cannot hold the config: p\x01.toml"); + const auto quoted = config("we\"ird/p.toml"); + require(quoted.find("config = \"p.toml\"\n") != std::string::npos, "quote path was not reduced to the file name"); + require(ParsePatchFile(quoted).Entries.size() == 1, "manifest with a reduced config did not parse"); + rejects([&] { config("dir/a\"b.toml"); }, "Patch manifest cannot hold the config"); + const auto hold = [&](AppliedPatch bad, const std::string& field) { rejects([&] { WriteManifest({bad}, "main.elf", ""); }, "Patch manifest cannot hold the " + field + ": "); }; + auto bad = patch; + bad.Name = "a\"b"; + hold(bad, "name"); + bad = patch; + bad.Name = "a\\b"; + hold(bad, "name"); + bad = patch; + bad.Target = "a\"b"; + hold(bad, "target"); + bad = patch; + bad.Description = "a\"b"; + hold(bad, "description"); +} + +void typedManifest() { + const auto file = ParsePatchFile(header("main_target = \"main.elf\"\n[[patch]]\nname = \"neg\"\ntarget = \"main.elf\"\nva = 0x1000\ntype = \"i32\"\nexpected = -1\nvalue = 0x18000000\nmin = -5\nmax = 0x7f000000\nalign = 0x1000\n" + "[[patch]]\ntarget = \"main.elf\"\nva = 0x1100\ntype = \"u8\"\nexpected = 128\nvalue = 1\n" + "[[patch]]\ntarget = \"main.elf\"\nva = 0x1104\nexpected = \"84\"\nvalue = \"02\"\n")); + auto bytes = twoSegmentFixture(); + Io::WriteU32(bytes, 0x1000, 0xffffffff); + const auto before = bytes; + const auto patched = PatchTarget(bytes, "main.elf", file.Entries); + require(patched.Applied.size() == 3 && patched.Applied[0].Type == "i32" && patched.Applied[0].Min && patched.Applied[0].Min->Negative, "typed fields are not carried"); + const auto text = WriteManifest(AggregateManifest({patched.Applied}), "main.elf", ""); + require(text.find("type = \"i32\"") != std::string::npos && text.find("expected = -1\n") != std::string::npos && text.find("min = -5\n") != std::string::npos + && text.find("max = 0x7f000000\n") != std::string::npos && text.find("align = 0x1000\n") != std::string::npos && text.find("value = 0x18000000\n") != std::string::npos, "typed manifest text: " + text); + require(text.find("expected = \"84\"") != std::string::npos, "raw entry stayed raw"); + const auto reparsed = ParsePatchFile(text); + require(reparsed.Entries.size() == 3 && reparsed.Entries[0].Name == "neg" && reparsed.Entries[0].Type == "i32", "typed round-trip"); + for (std::size_t index = 0; index < 3; ++index) + require(reparsed.Entries[index].Expected == file.Entries[index].Expected && reparsed.Entries[index].Value == file.Entries[index].Value, "round-trip bytes"); + auto again = before; + PatchTarget(again, "main.elf", reparsed.Entries); + require(again == bytes, "re-applying the manifest gives different bytes"); +} + +} + +int main() { + try { + grammarAccepted(); + grammarRejected(); + integerRanges(); + boundsAndWidth(); + names(); + reservedNames(); + relocationOverlap(); + typedManifest(); + manifestConfig(); + ranges(); + headerRange(); + applyAndMismatch(); + overlapWithWriters(); + conversion(); + targets(); + verify(); + rangeFailureInVerify(); + manifest(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + return 0; +} diff --git a/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp b/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp index 1ede68a821..f929c7837b 100644 --- a/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp +++ b/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -73,6 +74,20 @@ void CheckFixtures() { Require(Rejected(embedded, true, embedded.size()), "SDK matching accepted a prefix that starts inside an immediate"); for (const auto& code : std::vector{{0xB8, 0xDD, 0x01, 0x00, 0x00, 0x0F, 0x05, 0xC3}, {0xCD, 0x80}, {0x0F, 0x34}, {0x0F, 0x07}}) Require(Rejected(code, true, code.size()), "SDK trap mode accepted an unrelated privileged instruction"); + bool overflowRejected = false; + try { + Relinker::MakeSyscallScanner(testProgress, true)->ScanCodeSectionForSyscalls(Fixtures.front().code, std::numeric_limits::max(), 1); + } catch (const Domain::RelinkerException& error) { + overflowRejected = std::string(error.what()) == "Code section offset overflow"; + } + Require(overflowRejected, "Scanner accepted code section offset overflow"); + bool sizeExceededRejected = false; + try { + Relinker::MakeSyscallScanner(testProgress, true)->ScanCodeSectionForSyscalls(Fixtures.front().code, 0, Fixtures.front().code.size() + 1); + } catch (const Domain::RelinkerException& error) { + sizeExceededRejected = std::string(error.what()) == "Code section exceeds the input buffer"; + } + Require(sizeExceededRejected, "Scanner accepted code section size exceeding input"); std::cout << "SDK inline syscall checks passed, " << rejectedMutations << " near-match rejections\n"; } diff --git a/core/relinker/relinker/tests/StrictNidFilterTests.cpp b/core/relinker/relinker/tests/StrictNidFilterTests.cpp index 4da45f6d6e..349da2d7a0 100644 --- a/core/relinker/relinker/tests/StrictNidFilterTests.cpp +++ b/core/relinker/relinker/tests/StrictNidFilterTests.cpp @@ -187,6 +187,14 @@ void relativeTableAndWholeFunction() { require(AnalyzeStrictReachability(input).ImportSlots.contains(0x2000), "Unknown switch target inside a live function was removed"); } +void indexedTableImports() { + auto input = fixture(); + ripOperand(input, 0, {0x48, 0x8D, 0x05}, 0x2000); + emit(input, 7, {0xFF, 0x14, 0xF8, 0xC3}); + input.Data.push_back({0x2000, std::vector(16)}); + require(AnalyzeStrictReachability(input).ImportSlots == std::set{0x2000, 0x2008}, "Import slot reached by indexing a live table base was removed"); +} + void exceptionLeb128Bounds() { const auto check = [](const std::vector& codeAlignment, const std::vector& dataAlignment, bool overflowing) { std::vector bytes(0x400); @@ -679,6 +687,7 @@ int main() { conservativeIndirectTargets(); isolatedFunctionCycle(); relativeTableAndWholeFunction(); + indexedTableImports(); exceptionLeb128Bounds(); vectorInstructionLengths(); exceptionLandingPads(); diff --git a/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp index 0210073af7..31f110f1a8 100644 --- a/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp +++ b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp @@ -26,7 +26,18 @@ int main() { auto second = load; second.MappedAddress = 0x1048; second.Offset = 72; second.FileSize = 56; auto first = load; first.FileSize = 72; Require(Relinker::UnusedNidFilter::ReadExceptionFunctions(Fixture(), {first, second, eh}, {}, {}).size() == 1); - for (int kind = 0; kind < 7; ++kind) { + auto belowBytes = Fixture(); + belowBytes[3] = 0x30; + Io::WriteU64(belowBytes, 16, 0x800 - 0x1000); + Io::WriteU64(belowBytes, 24, 0x1060 - 0x1000); + Io::WriteU64(belowBytes, 104, 0x800); + Domain::ProgramHeader funcSeg{}; funcSeg.Type = 1; funcSeg.MappedAddress = 0x800; funcSeg.FileSize = 0x20; + const auto below = Relinker::UnusedNidFilter::ReadExceptionFunctions(belowBytes, {funcSeg, load, eh}, {}, {}); + Require(below.size() == 1 && below[0].Begin == 0x800 && below[0].End == 0x810); + auto empty = Fixture(); + Io::WriteU64(empty, 112, 0); + Require(Relinker::UnusedNidFilter::ReadExceptionFunctions(empty, {load, eh}, {}, {}).empty()); + for (int kind = 0; kind < 10; ++kind) { auto bytes = Fixture(); auto segment = load; switch (kind) { case 0: Io::WriteU32(bytes, 64, 0x1000); break; @@ -36,12 +47,26 @@ int main() { case 4: segment.Offset = std::numeric_limits::max() - 4095; segment.MappedAddress = 0; segment.FileSize = 0x1100; break; case 5: segment.FileSize = 119; break; case 6: Io::WriteU32(bytes, 64, 8); break; + case 7: Io::WriteU32(bytes, 100, 0); break; + case 8: { + bytes[3] = 0x1B; + Io::WriteU32(bytes, 16, 0xFFFFE000); + Io::WriteU32(bytes, 20, 0x1060 - 0x1014); + Io::WriteU64(bytes, 104, 0xFFFFFFFFFFFFF010ULL); + break; + } + case 9: bytes[3] = 0x1C; Io::WriteU64(bytes, 16, 0x8000000000000000ULL); break; } bool rejected = false; - try { Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, {segment, eh}, {}, {}); } + std::vector headers = {segment, eh}; + if (kind == 8) { + Domain::ProgramHeader high{}; high.Type = 1; high.MappedAddress = 0xFFFFFFFFFFFFF000ULL; high.FileSize = 0x100; + headers.push_back(high); + } + try { Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); } catch (const std::exception&) { rejected = true; } Require(rejected); } - std::cout << "9 unwind record cases passed\n"; + std::cout << "14 unwind record cases passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } } diff --git a/core/relinker/relinker/tests/WindowsExportAliasTests.cpp b/core/relinker/relinker/tests/WindowsExportAliasTests.cpp index 78cc146c81..8bfc0400e6 100644 --- a/core/relinker/relinker/tests/WindowsExportAliasTests.cpp +++ b/core/relinker/relinker/tests/WindowsExportAliasTests.cpp @@ -68,6 +68,7 @@ int main() { const std::array,7> changes={{{4,0x22},{4,0x11},{5,0},{6,2},{8,0x28},{16,2},{6,Relinker::AbsoluteSection}}}; for(const auto& [field,value]:changes){auto b=Fixture();auto off=0x2400+48+field;if(field<6)b[off]=value;else if(field==6)Io::WriteU16(b,off,value);else Io::WriteU64(b,off,value);Reject([&]{Relinker::GuestImageReader().Read("generic.prx",b,true);},"Duplicate or empty export");} auto good=Relinker::GuestImageReader().Read("generic.prx",Fixture(),true); + auto symbolic=good;symbolic.Symbolic=true;Domain::GuestRuntime symbolicRuntime;Reject([&]{Elfpatcher::GuestModuleWriter().WriteWindows(symbolic,symbolicRuntime);},"DF_SYMBOLIC"); for(unsigned kind=0;kind<10;++kind){auto g=good;auto& s=g.Symbols[2];switch(kind){case 0:s.Value+=8;break;case 1:s.Size+=1;break;case 2:s.Info=0x22;break;case 3:s.Info=0x11;break;case 4:s.Visibility=0;break;case 5:s.Section=2;break;case 6:g.Symbols[1].Info=s.Info=0x11;break;case 7:g.Symbols[1].Section=s.Section=Relinker::AbsoluteSection;break;case 8:g.Symbols[1].Info=s.Info=0x16;g.Dynamic.RelaData.clear();break;case 9:g.Symbols[1].Info=s.Info=0x10;break;}Check(!Relinker::IdenticalOrdinaryFunctionExport(g.Symbols[1],s),"nonordinary/conflicting tuple accepted");Domain::GuestRuntime r;Reject([&]{Elfpatcher::GuestModuleWriter().WriteWindows(g,r);},kind==8?"TLS":"Duplicate guest export");} Check(Domain::DeclaredImportModule("same#A#B",{{1,"one"},{2,"two"}})=="one"&&Domain::DeclaredImportModule("same#A#C",{{1,"one"},{2,"two"}})=="two","module-qualified IDs merged"); std::cout<<"WINDOWS_EXPORT_ALIAS_CASES_PASSED\npreserved-indices reversed-order conflicting-fields strict-nonwindows qualified-import writer-guard\n"; diff --git a/core/relinker/relinker/tests/guest_import_fixtures.py b/core/relinker/relinker/tests/guest_import_fixtures.py index ac961cecf7..45c69cbfb4 100644 --- a/core/relinker/relinker/tests/guest_import_fixtures.py +++ b/core/relinker/relinker/tests/guest_import_fixtures.py @@ -1,19 +1,46 @@ +import contextlib +import shutil import struct +import tempfile +import time from test_guest_intel_trampolines import main_fixture +@contextlib.contextmanager +def fixture_directory(prefix): + directory = tempfile.mkdtemp(prefix=prefix) + try: + yield directory + finally: + deadline = time.monotonic() + 10 + while True: + try: + shutil.rmtree(directory) + break + except PermissionError: + if time.monotonic() >= deadline: + raise + time.sleep(0.05) + + def dynamic(image, offset, phoff, tags): for index, tag in enumerate(tags + [(0, 0)]): struct.pack_into('= 1 else 0 + resource.setrlimit(resource.RLIMIT_CORE, (limit, limit)) + + +def run_without_core_dump(command, **options): + return subprocess.run(command, preexec_fn=without_core_dump, **options) diff --git a/core/relinker/relinker/tests/test_call_registry.py b/core/relinker/relinker/tests/test_call_registry.py index c12ad56ff0..5b9a61ac30 100644 --- a/core/relinker/relinker/tests/test_call_registry.py +++ b/core/relinker/relinker/tests/test_call_registry.py @@ -8,36 +8,110 @@ import tempfile from test_string_table_bounds import fixture +def make_image(strings, needed_offset=1, symbol_offset=8): + image = fixture(str_size=len(strings), needed_offset=needed_offset, symbol_offset=symbol_offset, table_bytes=strings) + struct.pack_into(" bytearray: return image -def run_case(relinker: Path, work: Path, name: str, args, code: bytes, with_import=False): +def run_case(relinker: Path, work: Path, name: str, args, code: bytes, with_import=False, trailing_data=b""): source = work / (name + ".elf") output = work / (name + ".exe") - source.write_bytes(fixture(code, with_import)) + image = fixture(code, with_import) + end = 0x800 + len(code) + image[end:end + len(trailing_data)] = trailing_data + source.write_bytes(image) result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", *args, str(source), str(output)], capture_output=True, text=True, timeout=20) @@ -72,6 +75,21 @@ def main(): assert result.returncode == 2 and not output.exists(), (name, result.stdout, result.stderr) assert "Forbidden syscall instruction at code offset 0x1801" in result.stderr, (name, result.stderr) + for name, instruction in forbidden: + result, output = run_case(relinker, work, name + "-at-end", [], b"\x90" + instruction) + assert result.returncode == 2 and not output.exists(), (name, result.stdout, result.stderr) + assert "Forbidden syscall instruction at code offset 0x1801" in result.stderr, (name, result.stderr) + + for name, instruction in forbidden: + result, output = run_case(relinker, work, name + "-outside", [], b"\x90\xc3", + trailing_data=instruction) + assert result.returncode == 0 and output.exists(), (name, result.stdout, result.stderr) + + result, output = run_case(relinker, work, name + "-truncated", [], b"\x90" + instruction[:-1], + trailing_data=instruction[-1:]) + assert result.returncode == 2 and not output.exists(), (name, result.stdout, result.stderr) + assert "Cannot decode instruction" in result.stderr and "offset 0x1801" in result.stderr, (name, result.stderr) + result, output = run_case(relinker, work, "immediate-decoys", [], b"\x48\xb8\x0f\x05\xcd\x80\x0f\x34\x0f\x07\xc3") assert result.returncode == 0 and output.exists(), (result.stdout, result.stderr) diff --git a/core/relinker/relinker/tests/test_executable_tls_relocations.py b/core/relinker/relinker/tests/test_executable_tls_relocations.py index fd6ceff424..5ce918047b 100644 --- a/core/relinker/relinker/tests/test_executable_tls_relocations.py +++ b/core/relinker/relinker/tests/test_executable_tls_relocations.py @@ -20,6 +20,7 @@ PT_TLS = 7 DTPMOD64, DTPOFF64, TPOFF64, TPOFF32 = 16, 17, 18, 23 TLS_MEMSZ, TLS_ALIGN = 0x24, 16 TLS_BLOCK = 0x30 +DEFAULT_BASE = 0x400000 STRINGS = b"\0Tls\0Data\0ImportedTls\0" TLS_NAME, DATA_NAME, IMPORT_NAME = 1, 5, 10 @@ -107,8 +108,9 @@ def main(): elf = output.read_bytes() relocations = dynamic_relocations(elf) tls = sorted(relocation for relocation in relocations if relocation[1] in (DTPMOD64, DTPOFF64, TPOFF64)) - assert tls == [(0x1000, TPOFF64, "", 0, 4), (0x1008, DTPOFF64, "", 0, 8), (0x1010, TPOFF64, "", 0, 0x12), - (0x1018, DTPMOD64, "", 0, 0), (0x1020, TPOFF64, "ImportedTls", 6, 0)], relocations + assert tls == [(DEFAULT_BASE + 0x1000, TPOFF64, "", 0, 4), (DEFAULT_BASE + 0x1008, DTPOFF64, "", 0, 8), + (DEFAULT_BASE + 0x1010, TPOFF64, "", 0, 0x12), (DEFAULT_BASE + 0x1018, DTPMOD64, "", 0, 0), + (DEFAULT_BASE + 0x1020, TPOFF64, "ImportedTls", 6, 0)], relocations assert any(struct.unpack_from(" {expected ^ 1:02x} (flip one bit)\n" in result.stdout, result.stdout + manifest = output.parent / "app.patches.toml" + text = manifest.read_text() + assert 'main_target = "eboot.bin"' in text and 'name = "flip_bit"' in text, text + assert 'main_target = "eboot.bin"\nlinux_base = 0x400000\n' in text, text + assert f"Patch manifest: {manifest}\n" in result.stdout + assert f'config = "{source.parent.as_posix()}/relinker-patches.toml"\n' in text, text + + if os.name != "nt": + quoted = work / "quoted" + (quoted / "out").mkdir(parents=True) + (quoted / "eboot.bin").write_bytes(image) + (quoted / 'we"ird.toml').write_text(patch_text(address, expected)) + result = run("--skip-sce-module", "--patches", str(quoted / 'we"ird.toml'), str(quoted / "eboot.bin"), str(quoted / "out" / "app.elf")) + assert result.returncode == 2 and "Patch manifest cannot hold the config" in result.stderr, (result.stdout, result.stderr) + assert not list((quoted / "out").iterdir()), "output written despite the manifest error" + + reduced = work / 'we"ird' + (reduced / "out").mkdir(parents=True) + (reduced / "eboot.bin").write_bytes(image) + (reduced / "p.toml").write_text(patch_text(address, expected)) + result = run("--skip-sce-module", "--patches", str(reduced / "p.toml"), str(reduced / "eboot.bin"), str(reduced / "out" / "app.elf")) + assert result.returncode == 0, (result.stdout, result.stderr) + assert 'config = "p.toml"\n' in (reduced / "out" / "app.patches.toml").read_text() + + result = run("--verify-patches", str(manifest), str(source), "--skip-sce-module") + assert result.returncode == 0 and "applicable: flip_bit eboot.bin" in result.stdout, (result.stdout, result.stderr) + before = sorted(str(path) for path in work.rglob("*")) + result = run("--verify-patches", str(manifest), str(output), "--verify-output") + assert result.returncode == 0 and "applied: flip_bit eboot.bin" in result.stdout, (result.stdout, result.stderr) + assert sorted(str(path) for path in work.rglob("*")) == before, "verify wrote files" + renamed = work / "renamed" + renamed.mkdir() + (renamed / "eboot.bin").write_bytes(patched) + result = run("--verify-patches", str(manifest), str(renamed / "eboot.bin"), "--skip-sce-module") + assert result.returncode == 1 and "mismatch: flip_bit eboot.bin" in result.stdout, (result.stdout, result.stderr) + + other = work / "reproduce" + other.mkdir() + (source.parent / "relinker-patches.toml").unlink() + result = run("--skip-sce-module", "--patches", str(manifest), str(source), str(other / "app.elf")) + assert result.returncode == 0 and (other / "app.elf").read_bytes() == patched, result.stderr + + clash = work / "clash" + (clash / "in").mkdir(parents=True) + (clash / "in" / "eboot.bin").write_bytes(image) + (clash / "in" / "app.patches.toml").write_text(patch_text(address, expected)) + result = run("--skip-sce-module", "--patches", str(clash / "in" / "app.patches.toml"), str(clash / "in" / "eboot.bin"), str(clash / "in" / "app.elf")) + assert result.returncode == 2 and "would overwrite the patch file" in result.stderr, (result.stdout, result.stderr) + assert not (clash / "in" / "app.elf").exists() + + hand = work / "hand.toml" + hand.write_text(patch_text(address, expected)) + result = run("--verify-patches", str(hand), str(output), "--verify-output") + assert result.returncode == 2 and "no main_target" in result.stderr, result.stderr + + root, source, output = case("wrong-bytes") + (root / "p.toml").write_text(patch_text(address, expected ^ 0x55)) + result = run("--skip-sce-module", "--patches", str(root / "p.toml"), str(source), str(output)) + assert result.returncode == 2 and "expected" in result.stderr and "found" in result.stderr, result.stderr + assert not list(output.parent.iterdir()), "output written despite a failed patch" + result = run("--verify-patches", str(root / "p.toml"), str(source), "--skip-sce-module") + assert result.returncode == 1 and "mismatch: eboot.bin" in result.stdout, result.stdout + + root, source, output = case("unmatched") + (root / "p.toml").write_text(patch_text(address, expected, target="missing.prx")) + result = run("--skip-sce-module", "--patches", str(root / "p.toml"), str(source), str(output)) + assert result.returncode == 2 and "unmatched patch target: missing.prx" in result.stderr, result.stderr + assert not list(output.parent.iterdir()) + result = run("--verify-patches", str(root / "p.toml"), str(source), "--skip-sce-module") + assert result.returncode == 2 and "--verify-output" in result.stderr, result.stderr + + root, source, output = case("extra") + (root / "in" / "picker").mkdir() + (root / "in" / "picker" / "picker.bin").write_bytes(image) + (root / "p.toml").write_text(patch_text(address, expected, target="picker/picker.bin")) + (root / "in" / "sce_module").mkdir() + result = run("--executable", "picker/picker.bin", "--patches", str(root / "p.toml"), str(source), str(output)) + assert result.returncode == 0, (result.stdout, result.stderr) + manifest = output.parent / "app.patches.toml" + result = run("--verify-patches", str(manifest), str(output), "--verify-output", "--executable", "picker/picker.bin") + assert result.returncode == 0 and "applied: picker/picker.bin" in result.stdout, (result.stdout, result.stderr) + (output.parent / "picker.picker.bin.elf").unlink() + result = run("--verify-patches", str(manifest), str(output), "--verify-output", "--executable", "picker/picker.bin") + assert result.returncode == 2 and "output not found" in result.stderr, result.stderr + + relocation_overlap(relinker, work) + writer_overlap(relinker, work) + + +def relocation_overlap(relinker, work): + def run(*arguments): + return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, timeout=60) + + def patch_file(path, target, address, expected): + path.write_text(f'format = 1\n\n[[patch]]\ntarget = "{target}"\nva = {address:#x}\nexpected = "{expected}"\nvalue = "ffff"\n') + + for kind, rela, plt in (("rela", [RELATIVE], []), ("plt", [], [JUMP_SLOT])): + image = bytes(module(rela, plt)) + for offset, overlaps in ((TARGET + 4, True), (TARGET + 8, False)): + case = work / f"mod-{kind}-{offset:x}" + (case / "in" / "sce_module").mkdir(parents=True) + (case / "in" / "sce_module" / "module.prx").write_bytes(image) + (case / "in" / "eboot.bin").write_bytes(main_fixture()) + (case / "out").mkdir() + expected = image[0x2000 + (offset - 0x2000):0x2000 + (offset - 0x2000) + 2].hex() + patch_file(case / "p.toml", "sce_module/module.prx", offset, expected) + result = run("--patches", str(case / "p.toml"), str(case / "in" / "eboot.bin"), str(case / "out" / "app.elf")) + if overlaps: + assert result.returncode == 2 and "relocation target" in result.stderr, (kind, result.stdout, result.stderr) + assert not list((case / "out").iterdir()), "output written despite the overlap" + else: + assert result.returncode == 0, (kind, result.stdout, result.stderr) + + image = bytearray(importing()) + struct.pack_into("\n' + '#include \n' + '#include \n' + '#include \n' + '\n' + 'int main(int argc, char **argv) {\n' + ' if (argc != 5) return 4;\n' + ' void *host = dlopen(argv[1], RTLD_NOW | RTLD_GLOBAL);\n' + ' if (!host) { fprintf(stderr, "%s\\n", dlerror()); return 1; }\n' + ' void (*set)(size_t, const void *) = (void (*)(size_t, const void *))dlsym(host, "SetPendingArgs");\n' + ' if (!set) { fprintf(stderr, "%s\\n", dlerror()); return 2; }\n' + ' set((size_t)strtoull(argv[3], NULL, 0), (const void *)(size_t)strtoull(argv[4], NULL, 0));\n' + ' if (!dlopen(argv[2], RTLD_NOW | RTLD_LOCAL)) { fprintf(stderr, "%s\\n", dlerror()); return 3; }\n' + ' return 0;\n' + '}\n' + ) + result = subprocess.run(['xcrun', 'clang', '-arch', 'x86_64', str(source), '-o', str(loader)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + return (['/usr/bin/arch', '-x86_64'] if platform.machine() == 'arm64' else []) + [str(loader)] + + +def load_and_record(work, provider, module, loader=None): events = work / 'events.txt' - run = subprocess.run([sys.executable, __file__, '--load', str(provider), str(module)], + command = loader + [str(provider), str(module), hex(ARGS), hex(ARGP)] if loader else [sys.executable, __file__, '--load', str(provider), str(module)] + run = subprocess.run(command, env={**os.environ, 'ANYPS5_GUEST_MODULE_ARGS': str(events)}, capture_output=True, text=True, timeout=30) assert run.returncode == 0, (run.returncode, run.stdout, run.stderr) @@ -82,9 +166,11 @@ def main(): relinker = Path(sys.argv[1]).resolve() host = Path(sys.argv[2]).resolve() linux = sys.platform == 'linux' and os.uname().machine == 'x86_64' - macos = sys.platform == 'darwin' and platform.machine() == 'x86_64' + macos = sys.platform == 'darwin' and macho_x86_64(host) with tempfile.TemporaryDirectory(prefix='anyps5-guest-args-') as directory: work = Path(directory) + for deferred in (False, True): + check_lifecycle_host(relinker, host, work, linux, deferred) modules = work / 'sce_module' modules.mkdir() name = 'args.prx' @@ -130,6 +216,7 @@ def main(): if linux: libraries = work / 'libs' libraries.mkdir() + shutil.copyfile(host, libraries / 'libkernel.prx') shutil.copyfile(host, libraries / 'libc.prx') load_and_record(work, libraries / 'libc.prx', converted) elif macos: @@ -137,7 +224,7 @@ def main(): libraries.mkdir() shutil.copyfile(host, libraries / 'libkernel.prx') (libraries / 'libc.prx').symlink_to('libkernel.prx') - load_and_record(mach, libraries / 'libkernel.prx', mach_converted) + load_and_record(mach, libraries / 'libkernel.prx', mach_converted, macos_loader(work)) else: print('Guest module argument tests passed: native execution skipped') return diff --git a/core/relinker/relinker/tests/test_guest_module_dir.py b/core/relinker/relinker/tests/test_guest_module_dir.py index 4c3cabf70b..7f2022a918 100644 --- a/core/relinker/relinker/tests/test_guest_module_dir.py +++ b/core/relinker/relinker/tests/test_guest_module_dir.py @@ -1,4 +1,5 @@ from pathlib import Path +import json import os import struct import subprocess @@ -7,7 +8,8 @@ import tempfile from test_guest_intel_trampolines import main_fixture, pe_sections from test_guest_module_directories import module_with_symbol, needed_libraries -from test_guest_needed_modules import executable_with_needed +from test_guest_needed_modules import executable_with_needed, module_with_needed +from test_macos_output import MachO def pe_export_names(data): @@ -34,13 +36,96 @@ def pe_export_names(data): return result +def host_module(names=()): + image = module_with_needed(names, tuple(range(len(names)))) + aligned = bytearray(0x3000) + aligned[:0x400] = image[:0x400] + for index in range(3): + offset = 64 + index * 56 + header = list(struct.unpack_from("> 32 else entry + for entry in rela if entry[1] & 0xffffffff] expected_plt = [entry for entry in plt if entry[1] & 0xffffffff] relocated = bool(expected_rela or expected_plt) if system == "linux": @@ -108,7 +112,8 @@ def check_output(data, system, rela, plt, initial): def invalid_cases(sce): - yield "relative-symbol", module([NOOP, (TARGET, (1 << 32) | 8, FUNCTION)], sce=sce), "RELATIVE relocation has a symbol" + yield "relative-symbol-mismatched-value", module([(TARGET, (1 << 32) | 8, FUNCTION + 8)], sce=sce), "RELATIVE relocation has a symbol" + yield "relative-symbol-undefined", module([(TARGET, (1 << 32) | 8, 0)], sce=sce, exported=False), "RELATIVE relocation has a symbol" yield "invalid-symbol", module([NOOP, (TARGET, (99 << 32) | 6, 0)], sce=sce), "Unsupported relocation or symbol index" yield "invalid-target", module([NOOP, (0xffffffffffffffff, 8, FUNCTION)], sce=sce), "Unmapped or inaccessible guest address" yield "overlapping-real", module([NOOP, RELATIVE, RELATIVE], sce=sce), "Overlapping guest relocations" @@ -142,6 +147,8 @@ def main(): return relinker = Path(sys.argv[1]).resolve() cases = (("baseline", [RELATIVE], [], False, UNCHANGED), + ("relative-defined-symbol", [RELATIVE_DEFINED], [], False, UNCHANGED), + ("relative-defined-object", [RELATIVE_DEFINED], [], False, UNCHANGED), ("mixed", [NOOP, OVERLAP, RELATIVE, IGNORED, (TARGET, 0, -1)], [], False, UNCHANGED), ("only-none", [NOOP, IGNORED, OVERLAP], [], False, UNCHANGED), ("plt-mixed", [], [NOOP, OVERLAP, JUMP_SLOT, IGNORED], False, UNCHANGED), @@ -168,7 +175,8 @@ def main(): for sce in (False, True): for name, rela, plt, lifecycle, initial in cases: - image = module(rela, plt, sce, lifecycle, initial) + symbol_info = 0x11 if name == "relative-defined-object" else 0x12 + image = module(rela, plt, sce, lifecycle, initial, symbol_info=symbol_info) for system in ("linux", "windows", "macos"): result, artifact, output = convert(f"{sce}-{name}", system, image) assert result.returncode == 0, (sce, name, system, result.stdout, result.stderr) diff --git a/core/relinker/relinker/tests/test_guest_own_tls_relocations.py b/core/relinker/relinker/tests/test_guest_own_tls_relocations.py new file mode 100644 index 0000000000..add91dcf70 --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_own_tls_relocations.py @@ -0,0 +1,78 @@ +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture +from test_guest_none_relocations import TARGET, change_tag, module +from test_guest_symbol_names import dynamic_tags, file_offset + + +DTPMOD64 = 16 +DTPOFF64 = 17 +TPOFF64 = 18 +TEMPLATE = 0x2780 + + +def fixture(sce=False, tls=True, info=3, value=0, kind=DTPMOD64, addend=0, section=0): + image = module([(TARGET, (3 << 32) | kind, addend)], sce=sce) + struct.pack_into(" 0; filtered=0" in result.stdout, result.stdout + live_regions = 2 if name == "nonempty" else 1 + assert f"Function graph: {live_regions}/" in result.stdout, result.stdout + if windows and os.name == "nt": + assert subprocess.run([str(output)], capture_output=True, timeout=20).returncode == 42 + + def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-init-array-") as directory: @@ -82,6 +130,8 @@ def main(): ("init-after-dynamic", image(DT_INIT_ARRAY, DT_INIT_ARRAYSZ, AFTER_DYNAMIC, [FUNCTION]), KEPT), ("init-os-tags", image(DT_OS_INIT_ARRAY, DT_OS_INIT_ARRAYSZ, BEFORE_DYNAMIC, [FUNCTION]), KEPT), ("init-second-slot", image(DT_INIT_ARRAY, DT_INIT_ARRAYSZ, BEFORE_DYNAMIC, [0, FUNCTION]), KEPT), + ("init-without-size", image(DT_INIT_ARRAY, None, BEFORE_DYNAMIC, [FUNCTION]), DROPPED), + ("init-without-size-unread", image(DT_INIT_ARRAY, None, BEFORE_DYNAMIC, [0x7FFF0000]), DROPPED), ("init-terminators", image(DT_INIT_ARRAY, DT_INIT_ARRAYSZ, BEFORE_DYNAMIC, [(1 << 64) - 1, 0]), DROPPED), ("init-relative-addend", image(DT_INIT_ARRAY, DT_INIT_ARRAYSZ, BEFORE_DYNAMIC, [0], [(BEFORE_DYNAMIC, FUNCTION)]), KEPT), @@ -103,6 +153,7 @@ def main(): image(DT_INIT_ARRAY, DT_INIT_ARRAYSZ, LOAD_END + 0x100, [FUNCTION], load_size=LOAD_END)) assert result.returncode == 2 and "array is not file-backed" in result.stderr and not output.exists(), ( result.returncode, result.stdout, result.stderr) + strict_array_metadata(relinker, work) print("Init array root tests passed") diff --git a/core/relinker/relinker/tests/test_input_magic.py b/core/relinker/relinker/tests/test_input_magic.py index e388e2d813..e26ccf337c 100644 --- a/core/relinker/relinker/tests/test_input_magic.py +++ b/core/relinker/relinker/tests/test_input_magic.py @@ -1,6 +1,7 @@ """Reject inputs that are not plain ELF files with a message that names what was read.""" from pathlib import Path +import platform import subprocess import sys import tempfile @@ -23,6 +24,27 @@ def main(): convert("eboot.self.kernel", b"\x54\x14\xf5\xee" + bytes(0x1000), "The input is a SELF container, not an ELF") convert("eboot.pkg", b"\x7fCNT" + bytes(0x1000), "Invalid ELF magic number: 7f 43 4e 54") convert("eboot.short", b"\x7fEL", "File too small for ELF header") + unsupported_version = bytearray(64) + unsupported_version[:4] = b"\x7fELF" + unsupported_version[4:7] = b"\x02\x01\x01" + unsupported_version[0x12:0x14] = b"\x3e\x00" + convert("eboot.unsupported-version", unsupported_version, "Unsupported ELF object version: expected 1") + if platform.system() == "Linux": + import resource + + source = work / "large-unsupported.pkg" + output = work / "large-unsupported.out" + with source.open("wb") as stream: + stream.write(b"\x7fFIH") + stream.truncate(8 * 1024**3) + + def limit_address_space(): + resource.setrlimit(resource.RLIMIT_AS, (512 * 1024**2, 512 * 1024**2)) + resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) + + result = subprocess.run([str(relinker), "--skip-sce-module", str(source), str(output)], + capture_output=True, text=True, timeout=20, preexec_fn=limit_address_space) + assert result.returncode == 2 and "Invalid ELF magic number: 7f 46 49 48" in result.stderr and not output.exists(), result print("Input magic diagnostics tests passed") diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py index 10e7c51178..241dd1f4bc 100644 --- a/core/relinker/relinker/tests/test_input_preservation.py +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -1,11 +1,77 @@ import os from pathlib import Path +import stat import subprocess import sys import tempfile from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture from test_linux_desktop_entry_output import png +from test_secondary_output_collisions import prepare_secondary + + +def check_executable_hardlinks(relinker, work): + previous = b"previous output" + for windows in (False, True): + for output_name in ("executable", "secondary-one"): + for input_name in ("input.elf", "sample.prx", "secondary-one.bin"): + for readonly in ((False, True) if os.name == "nt" else (False,)): + case = work / f"hardlink-{windows}-{output_name}-{input_name}-{readonly}" + command, outputs, inputs = prepare_secondary(case, windows) + protected = next(path for path in inputs if path.name == input_name) + target = outputs[output_name] + os.link(protected, target) + for path in outputs.values(): + if path != target: + path.write_bytes(previous) + if readonly: + protected.chmod(stat.S_IREAD) + mode = protected.stat().st_mode + links = protected.stat().st_nlink + try: + result = subprocess.run([str(relinker), *command], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "output would overwrite" in result.stderr, ( + windows, output_name, input_name, readonly, result.stdout, result.stderr) + for path, original in inputs.items(): + assert path.read_bytes() == original, path + for path in outputs.values(): + assert path.read_bytes() == (inputs[protected] if path == target else previous), path + assert protected.stat().st_mode == mode + assert protected.stat().st_nlink == links + assert os.path.samefile(protected, target) + assert not list(case.rglob(".anyps5-output-identity-*")) + finally: + if readonly: + protected.chmod(stat.S_IWRITE) + + for readonly in ((False, True) if os.name == "nt" else (False,)): + case = work / f"distinct-input-hardlinks-{readonly}" + command, outputs, inputs = prepare_secondary(case, True) + links = case / "separate-links" + links.mkdir() + for index, path in enumerate(inputs): + os.link(path, links / f"input-{index}") + original = next(iter(inputs.values())) + for name, path in outputs.items(): + path.write_bytes(original) + os.link(path, links / name) + if readonly: + for path in inputs: + path.chmod(stat.S_IREAD) + try: + result = subprocess.run([str(relinker), *command], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (readonly, result.stdout, result.stderr) + for name in ("executable", "secondary-one", "secondary-two"): + assert outputs[name].read_bytes().startswith(b"MZ") + for path, original in inputs.items(): + assert path.read_bytes() == original and path.stat().st_nlink == 2, path + assert not list(case.rglob(".anyps5-output-identity-*")) + finally: + if readonly: + for path in inputs: + path.chmod(stat.S_IWRITE) def check_diagnostic_inputs(relinker, work): @@ -74,21 +140,22 @@ def check_diagnostic_inputs(relinker, work): assert diagnostics.read_bytes() == (b"" if enabled else original) -def check_registry_inputs(relinker, work, windows): +def check_registry_inputs(relinker, work, windows, needed=False): options = ["--windows"] if windows else [] for module_registry in (False, True): for bundled in (False, True): for alias in ("direct", "hardlink", "symlink"): if bundled and alias == "direct": continue - case = work / f"registry-{windows}-{module_registry}-{bundled}-{alias}" + case = work / f"registry-{windows}-{module_registry}-{bundled}-{alias}-{needed}" modules = case / "sce_module" modules.mkdir(parents=True) module = modules / "sample.prx" module_original = bytes(guest_fixture(PLAIN_SITE)) module.write_bytes(module_original) output = case / ("output.exe" if windows else "output.elf") - registries = [case / "output.registry.json", case / "output.sample.prx.guest.prx.registry.json"] + suffix = ".needed.json" if needed else ".registry.json" + registries = [case / ("output" + suffix), case / ("output.sample.prx.guest.prx" + suffix)] registry = registries[int(module_registry)] source = registry if alias == "direct" else case / "input.elf" original = bytes(main_fixture()) @@ -111,7 +178,8 @@ def check_registry_inputs(relinker, work, windows): output.write_bytes(previous_output) result = subprocess.run([str(relinker), *options, "--registry", str(source), str(output)], capture_output=True, text=True, timeout=20) - expected = "Registry output would overwrite an input module" if bundled else "Registry output would overwrite the input executable" + kind = "Needed libraries" if needed else "Registry" + expected = f"{kind} output would overwrite an input module" if bundled else f"{kind} output would overwrite the input executable" assert result.returncode == 2 and expected in result.stderr, (result.returncode, result.stdout, result.stderr) assert source.read_bytes() == original and module.read_bytes() == module_original assert output.read_bytes() == previous_output and untouched.read_bytes() == previous_registry @@ -130,7 +198,8 @@ def check_registry_after_failed_patch(relinker, work): output = case / "output.exe" previous_output = b"previous executable" output.write_bytes(previous_output) - registries = [case / "output.registry.json", case / "output.sample.prx.guest.prx.registry.json"] + registries = [case / "output.registry.json", case / "output.sample.prx.guest.prx.registry.json", + case / "output.needed.json", case / "output.sample.prx.guest.prx.needed.json"] previous_registry = b"previous registry" for registry in registries: registry.write_bytes(previous_registry) @@ -221,6 +290,7 @@ def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-input-preservation-") as directory: work = Path(directory) + check_executable_hardlinks(relinker, work) check_diagnostic_inputs(relinker, work) check_desktop_inputs(relinker, work) check_registry_after_failed_patch(relinker, work) @@ -228,6 +298,7 @@ def main(): for windows in (False, True): options = ["--windows"] if windows else [] check_registry_inputs(relinker, work, windows) + check_registry_inputs(relinker, work, windows, needed=True) for bundled in (False, True): for alias in ("direct", "normalized", "hardlink", "symlink"): case = work / f"{windows}-{bundled}-{alias}" diff --git a/core/relinker/relinker/tests/test_interpreter_in_code.py b/core/relinker/relinker/tests/test_interpreter_in_code.py new file mode 100644 index 0000000000..7ec2c4ceba --- /dev/null +++ b/core/relinker/relinker/tests/test_interpreter_in_code.py @@ -0,0 +1,59 @@ +"""Keep an interpreter path at the start of a code segment out of the scan, and only there.""" + +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +INTERPRETER = b"/libexec/ld-elf.so.1\x00" +CODE_OFFSET = 0x1000 +DATA_ADDRESS = 0x8000 +ENTRY = 0x100 +TRAILING_ADDRESS = 0xF00 + + +def fixture(interpreter=True, trailing=False): + image = bytearray(0x2000) + image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) + struct.pack_into("= len(image), "The grown program header table must be moved past the input" assert "PT_GNU_EH_FRAME" not in result.stderr, result.stderr assert elf[table_end:table_end + 56] == image[table_end:table_end + 56], "Bytes after the input program header table changed" @@ -113,7 +116,8 @@ def main(): image[table_end:table_end + 56] = bytes([0xa5]) * 56 result, elf = relink(relinker, directory, "no-frame-exact-slots", image) headers = program_headers(elf) - assert len(headers) == 6, headers + assert len(headers) == 7, headers + assert [header for header in headers if header[0] == PT_GNU_STACK] == [(PT_GNU_STACK, 6, 0, 0, 0, 0, 0, 16)], headers assert "PT_GNU_EH_FRAME" not in result.stderr, result.stderr assert elf[table_end:table_end + 56] == image[table_end:table_end + 56], "Bytes after the program header table changed" @@ -122,8 +126,8 @@ def main(): image[64 + 5 * 56:64 + 6 * 56] = bytes([0xa5]) * 56 result, elf = relink(relinker, directory, "few-slots-" + str(with_frame), image) headers = program_headers(elf) - assert len(headers) == (7 if with_frame else 6), headers - assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == ([EH_FRAME_HEADER] if with_frame else []), headers + assert len(headers) == (8 if with_frame else 7), headers + assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == ([EH_FRAME_OUTPUT] if with_frame else []), headers assert struct.unpack_from("= len(image), "The grown program header table must be moved past the input" assert elf[64 + 5 * 56:64 + 6 * 56] == image[64 + 5 * 56:64 + 6 * 56], "Bytes after the input program header table changed" @@ -138,7 +142,7 @@ def main(): source.write_bytes(image) result = subprocess.run([str(relinker), "--skip-sce-module", str(source), str(output)], capture_output=True, text=True, timeout=20) assert result.returncode == 2, (result.returncode, result.stdout, result.stderr) - assert "Not enough program header slots: need 65536, available 65534" in result.stderr, result.stderr + assert "Not enough program header slots: need 65537, available 65534" in result.stderr, result.stderr assert not output.exists(), "An unrepresentable header count must not produce an executable" print("Linux eh_frame header test passed") diff --git a/core/relinker/relinker/tests/test_linux_entry_argv.py b/core/relinker/relinker/tests/test_linux_entry_argv.py index 82c043c19f..87604a5743 100644 --- a/core/relinker/relinker/tests/test_linux_entry_argv.py +++ b/core/relinker/relinker/tests/test_linux_entry_argv.py @@ -8,9 +8,11 @@ import subprocess import sys import tempfile +from signal_run import run_without_core_dump from test_linux_load_alignment import PT_LOAD, PT_SCE_VERSION, fixture, loads ENTRY = 0x10 +DEFAULT_BASE = 0x400000 ARGV_CHECK = bytes.fromhex( "833f02" "751e" "48837f0800" "7417" @@ -69,7 +71,7 @@ def main(): for executable in (output, relocated): executable.chmod(0o755) for arguments, expected in ((["Z"], -signal.SIGTRAP), (["Z", "extra"], -signal.SIGILL)): - executed = subprocess.run([str(executable), *arguments], capture_output=True, timeout=20) + executed = run_without_core_dump([str(executable), *arguments], capture_output=True, timeout=20) assert executed.returncode == expected, (executable.name, arguments, executed.returncode) for address, error in ((0x7FF00000, None), (0x80000000, "Entry stub call exceeds rel32 range")): source.write_bytes(far_load_fixture(address)) @@ -78,7 +80,7 @@ def main(): capture_output=True, text=True, timeout=20) if error is None: assert result.returncode == 0, (address, result.stdout, result.stderr) - assert entry_call_target(far.read_bytes()) == ENTRY, hex(address) + assert entry_call_target(far.read_bytes()) == DEFAULT_BASE + ENTRY, hex(address) else: assert result.returncode == 2 and error in result.stderr and not far.exists(), (address, result) print("Linux entry argv test passed") diff --git a/core/relinker/relinker/tests/test_linux_fixed_base.py b/core/relinker/relinker/tests/test_linux_fixed_base.py index 61dd57d077..ee31ad59ef 100644 --- a/core/relinker/relinker/tests/test_linux_fixed_base.py +++ b/core/relinker/relinker/tests/test_linux_fixed_base.py @@ -1,4 +1,4 @@ -"""Check that --linux-fixed-base places the executable at the requested address and rebases every address it holds.""" +"""Check that the Linux executable is placed at 0x400000 by default or at the --linux-fixed-base address, with every address it holds rebased, that --lazy-binding keeps it position independent and warns, and that the largest image the relinker accepts ends below the guest arena.""" from pathlib import Path import platform @@ -8,6 +8,7 @@ import subprocess import sys import tempfile +from signal_run import run_without_core_dump from test_linux_load_alignment import PT_SCE_VERSION PT_NULL = 0 @@ -15,9 +16,13 @@ PT_LOAD = 1 PT_DYNAMIC = 2 PT_GNU_STACK = 0x6474E551 BASE = 0x400000 +HIGH_BASE = 0x400000000 +SHIFT = HIGH_BASE - BASE ENTRY = 0x10 SLOT = 0x800 ADDRESS_TAGS = {3, 4, 5, 6, 7, 23} +GUEST_ARENA_START = 0x200000000 +LAZY_WARNING = "with --lazy-binding the Linux executable stays position independent" def check_code(base): @@ -31,14 +36,14 @@ def check_code(base): + b"\xcc" + b"\x0f\x0b") -def fixture(relocation_type=8, addend=ENTRY, alignment=0x4000, memory_size=0x1000): +def fixture(relocation_type=8, addend=ENTRY, alignment=0x4000, memory_size=0x1000, slots=6): image = bytearray(0x8000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) - struct.pack_into(" struct.unpack_from("\n" + f"int exported(void) __asm__(\"\\\"{symbol}\\\"\");\n" + f"int exported(void) {{ _exit({value}); }}\n") + output = work / (name + ".so") + subprocess.run([os.environ.get("CC", "cc"), "-shared", "-fPIC", str(source), "-o", str(output)], check=True) + return output.read_bytes() + + +def convert(relinker, case, image, module=None): + case.mkdir() + if module is not None: + (case / "sce_module").mkdir() + (case / "sce_module" / "bundled.prx").write_bytes(module) + source = case / "input.elf" + source.write_bytes(image) + output = case / "output.elf" + options = [] if module is not None else ["--skip-sce-module"] + result = subprocess.run([str(relinker), *options, str(source), str(output)], capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (case.name, result.stdout, result.stderr) + return output + + +def run(output, library): + (output.parent / "libs").mkdir() + (output.parent / "libs" / "available.prx").write_bytes(library) + output.chmod(0o755) + return subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + + +def main(): + relinker = Path(sys.argv[1]).resolve() + runnable = sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64") + with tempfile.TemporaryDirectory(prefix="anyps5-linux-named-imports-") as directory: + work = Path(directory) + cases = { + "sprx": convert(relinker, work / "sprx", executable("available.sprx", "shared")), + "prx": convert(relinker, work / "prx", executable("available.prx", "shared")), + "qualified": convert(relinker, work / "qualified", executable("available.sprx", "shared#A#B")), + "name": convert(relinker, work / "name", executable("available.sprx", "shared")), + "guest": convert(relinker, work / "guest", executable("bundled.prx", "shared", extra_dependencies=("available.sprx",)), + module_with_symbol(True)), + } + image = cases["sprx"].read_bytes() + assert imported_symbols(image) == {nid("shared")}, imported_symbols(image) + assert needed_libraries(image) == ["available.prx"], needed_libraries(image) + assert imported_symbols(cases["prx"].read_bytes()) == {"shared"} + assert imported_symbols(cases["qualified"].read_bytes()) == {"shared"} + image = cases["guest"].read_bytes() + assert imported_symbols(image) == {"shared#guest"}, imported_symbols(image) + assert "available.prx" in needed_libraries(image), needed_libraries(image) + if not runnable: + return + by_nid = library(work, "by-nid", nid("shared"), 22) + by_name = library(work, "by-name", "shared", 44) + result = run(cases["sprx"], by_nid) + assert result.returncode == 22, (result.returncode, result.stdout, result.stderr) + result = run(cases["prx"], by_name) + assert result.returncode == 44, (result.returncode, result.stdout, result.stderr) + result = run(cases["qualified"], by_nid) + assert result.returncode != 0 and "undefined symbol: shared" in result.stderr, (result.returncode, result.stderr) + result = run(cases["name"], by_name) + assert result.returncode != 0 and f"undefined symbol: {nid('shared')}" in result.stderr, (result.returncode, result.stderr) + print("Linux named import tests passed") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_linux_program_header_order.py b/core/relinker/relinker/tests/test_linux_program_header_order.py index 71e5af4d66..3a8be9aca1 100644 --- a/core/relinker/relinker/tests/test_linux_program_header_order.py +++ b/core/relinker/relinker/tests/test_linux_program_header_order.py @@ -8,6 +8,9 @@ import subprocess import sys import tempfile +from signal_run import run_without_core_dump +from test_guest_intel_trampolines import main_fixture +from test_guest_module_directories import module_with_symbol from test_linux_load_alignment import fixture as single_load_fixture PT_LOAD = 1 @@ -15,19 +18,22 @@ PT_DYNAMIC = 2 PT_INTERP = 3 PT_PHDR = 6 PT_OS_PROCPARAM = 0x61000001 +PT_GNU_STACK = 0x6474E551 PT_SCE_VERSION = 0x6FFFFF01 PAGE = 0x1000 +DEFAULT_BASE = 0x400000 TEXT = (PT_LOAD, 5, 0x4000, 0x0000, 0x1000, 0x1000, 0x1000) RODATA = (PT_LOAD, 4, 0x5000, 0x1000, 0x0800, 0x0800, 0x1000) DATA = (PT_LOAD, 6, 0x6000, 0x2000, 0x1000, 0x1800, 0x1000) +TAIL = (PT_LOAD, 6, 0x8400, 0x2400, 0x0C00, 0x1C00, 0x1000) PROCPARAM = (PT_OS_PROCPARAM, 4, 0x5400, 0x1400, 0x0040, 0x0040, 0x10) DYNAMIC_OFFSET = 0x6400 TAGS = [(5, 0x2600), (10, 1), (6, 0x2620), (11, 24), (7, 0x2700), (8, 0), (9, 24), (0, 0)] -def multi_load_fixture(segments): - image = bytearray(0x8000) +def multi_load_fixture(segments, size=0x8000): + image = bytearray(size) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) headers = [*segments, (PT_DYNAMIC, 6, DYNAMIC_OFFSET, 0x2400, len(TAGS) * 16, len(TAGS) * 16, 8)] headers += [(PT_SCE_VERSION, 0, 0, 0, 0, 0, 1)] * 4 @@ -74,7 +80,7 @@ def run_expecting_trap(name, output): if not (sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64")): return output.chmod(0o755) - executed = subprocess.run([str(output)], capture_output=True, timeout=20) + executed = run_without_core_dump([str(output)], capture_output=True, timeout=20) assert executed.returncode == -signal.SIGTRAP, (name, executed.returncode, executed.stderr) @@ -97,18 +103,53 @@ def main(): ordered = convert("ordered-loads", multi_load_fixture([TEXT, RODATA, PROCPARAM, DATA])) headers = check_order("ordered-loads", ordered.read_bytes()) - assert (PT_OS_PROCPARAM, PROCPARAM[3]) in [(h[0], h[3]) for h in headers], headers - assert [h[3] for h in headers if h[0] == PT_LOAD][:3] == [TEXT[3], RODATA[3], DATA[3]], headers + assert (PT_OS_PROCPARAM, DEFAULT_BASE + PROCPARAM[3]) in [(h[0], h[3]) for h in headers], headers + assert [h[3] for h in headers if h[0] == PT_LOAD][:3] == [DEFAULT_BASE + h[3] for h in (TEXT, RODATA, DATA)], headers run_expecting_trap("ordered-loads", ordered) unordered = convert("unordered-loads", multi_load_fixture([DATA, PROCPARAM, TEXT, RODATA])) headers = check_order("unordered-loads", unordered.read_bytes()) - assert [h[3] for h in headers if h[0] == PT_LOAD][:3] == [TEXT[3], RODATA[3], DATA[3]], headers - assert kinds_between_loads(headers) == [PT_OS_PROCPARAM], headers + assert [h[3] for h in headers if h[0] == PT_LOAD][:3] == [DEFAULT_BASE + h[3] for h in (TEXT, RODATA, DATA)], headers + assert kinds_between_loads(headers) == [PT_OS_PROCPARAM, PT_GNU_STACK], headers run_expecting_trap("unordered-loads", unordered) + + tailed = convert("unaligned-tail", multi_load_fixture([TEXT, RODATA, PROCPARAM, TAIL], 0x9400)) + headers = check_order("unaligned-tail", tailed.read_bytes()) + check_extra_block_page("unaligned-tail", headers) + run_expecting_trap("unaligned-tail", tailed) + + guest = work / "guest" + (guest / "sce_module").mkdir(parents=True) + module = module_with_symbol(True) + module[64:176] = module[120:176] + module[64:120] + assert [h[3] for h in program_headers(module) if h[0] == PT_LOAD] == [0x2000, 0x1000] + (guest / "sce_module" / "provider.prx").write_bytes(module) + (guest / "input.elf").write_bytes(main_fixture()) + result = subprocess.run([str(relinker), str(guest / "input.elf"), str(guest / "output.elf")], + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, ("guest-module", result.stdout, result.stderr) + outputs = list(guest.rglob("provider.prx.guest.prx")) + assert len(outputs) == 1, outputs + addresses = [h[3] for h in program_headers(outputs[0].read_bytes()) if h[0] == PT_LOAD] + assert addresses[:2] == [0x1000, 0x2000] and addresses == sorted(addresses), [hex(a) for a in addresses] print("Linux program header order test passed") +def check_extra_block_page(name, headers): + page = 0x1000 + loads = [header for header in headers if header[0] == PT_LOAD] + block = max(loads, key=lambda header: header[2]) + header_block = min(loads, key=lambda header: header[2]) + assert header_block[2] == 0 and header_block is not block, (name, header_block) + assert block[2] % page == block[3] % page and block[3] % page != 0, (name, block) + assert block[6] > block[5] and (block[3] + block[6]) % page == 0, (name, block) + for header in loads: + if header is block or header is header_block: + continue + assert (header[3] + header[5] - 1) // page < block[3] // page, (name, header, block) + assert header_block[3] >= block[3] + block[6], (name, header_block, block) + + def kinds_between_loads(headers): kinds = [header[0] for header in headers] first = kinds.index(PT_LOAD) diff --git a/core/relinker/relinker/tests/test_linux_trace_imports.py b/core/relinker/relinker/tests/test_linux_trace_imports.py new file mode 100644 index 0000000000..f8febde7c2 --- /dev/null +++ b/core/relinker/relinker/tests/test_linux_trace_imports.py @@ -0,0 +1,89 @@ +"""--trace-imports passes each Linux function import through libc.prx's TraceImport_nid_no_patch before the guest entry.""" + +import os +from pathlib import Path +import platform +import subprocess +import sys +import tempfile + +from guest_import_fixtures import executable + + +PROVIDER = "#include \nint shared(void) { _exit(11); }\n" +HOOK = ("#include \n" + "#include \n" + "static int replacement(void) { _exit(77); }\n" + "void* TraceImport_nid_no_patch(void* target, const char* name, const char* library) {\n" + " return strcmp(name, \"shared\") == 0 && strcmp(library, \"a.prx\") == 0 ? (void*)replacement : target;\n" + "}\n") +PLAIN = "int Unrelated(void) { return 0; }\n" + + +def relink(relinker, source, output, *options): + return subprocess.run([str(relinker), "--skip-sce-module", *options, str(source), str(output)], + capture_output=True, text=True, timeout=30) + + +def library(work, name, code): + source = work / (name + ".c") + source.write_text(code) + output = work / (name + ".so") + subprocess.run([os.environ.get("CC", "cc"), "-shared", "-fPIC", str(source), "-o", str(output)], check=True) + return output.read_bytes() + + +def main(): + relinker = Path(sys.argv[1]).resolve() + runnable = sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64") + with tempfile.TemporaryDirectory(prefix="anyps5-linux-trace-imports-") as directory: + work = Path(directory) + libraries = {} + if runnable: + libraries = {name: library(work, name, code) for name, code in (("a", PROVIDER), ("hook", HOOK), ("plain", PLAIN))} + + for name, options, libc, symbol_info, expected in ( + ("untraced", (), "hook", 0x12, 11), + ("traced", ("--trace-imports",), "hook", 0x12, 77), + ("fixed-base", ("--trace-imports", "--linux-fixed-base", "0x400000"), "hook", 0x12, 77), + ("data", ("--trace-imports",), "hook", 0x11, 11), + ("weak", ("--trace-imports",), "hook", 0x22, 11), + ("unsupported", ("--trace-imports",), "plain", 0x12, None)): + case = work / name + case.mkdir() + image = executable("a.prx", extra_dependencies=("libc.prx",)) + image[0x4898 + 4] = symbol_info + source = case / "input.elf" + source.write_bytes(image) + output = case / "output.elf" + result = relink(relinker, source, output, *options) + assert result.returncode == 0, (name, result.stdout, result.stderr) + if not runnable: + continue + (case / "libs").mkdir() + (case / "libs" / "a.prx").write_bytes(libraries["a"]) + (case / "libs" / "libc.prx").write_bytes(libraries[libc]) + output.chmod(0o755) + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + if expected is None: + assert run.returncode != 0 and "TraceImport_nid_no_patch" in run.stderr, (name, run.returncode, run.stdout, run.stderr) + else: + assert run.returncode == expected, (name, run.returncode, run.stdout, run.stderr) + + case = work / "without-libc" + case.mkdir() + source = case / "input.elf" + source.write_bytes(executable("a.prx")) + result = relink(relinker, source, case / "output.elf", "--trace-imports") + assert result.returncode == 2 and "--trace-imports requires libc.prx" in result.stderr, (result.returncode, result.stderr) + assert not (case / "output.elf").exists() + result = relink(relinker, source, case / "output.elf", "--trace-imports", "--lazy-binding") + assert result.returncode != 0 and "--trace-imports conflicts with --lazy-binding" in result.stderr, (result.returncode, result.stderr) + result = relink(relinker, source, case / "output", "--trace-imports", "--macos") + assert result.returncode != 0 and "--trace-imports cannot be used with --macos" in result.stderr, (result.returncode, result.stderr) + + print("Linux import tracing tests passed") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_loadexec_arguments.py b/core/relinker/relinker/tests/test_loadexec_arguments.py new file mode 100644 index 0000000000..40cca05df1 --- /dev/null +++ b/core/relinker/relinker/tests/test_loadexec_arguments.py @@ -0,0 +1,107 @@ +from pathlib import Path +import os +import shutil +import struct +import subprocess +import sys +import tempfile +import time + +from test_windows_import_modules import executable + + +def fixture(symbol): + image = executable('guest_loadexec_host.prx', symbol) + struct.pack_into(' 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__': + main() diff --git a/core/relinker/relinker/tests/test_macos_empty_tls.py b/core/relinker/relinker/tests/test_macos_empty_tls.py new file mode 100644 index 0000000000..7d4619ec23 --- /dev/null +++ b/core/relinker/relinker/tests/test_macos_empty_tls.py @@ -0,0 +1,70 @@ +"""Check `relinker --macos` on guests whose PT_TLS is empty.""" +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_macos_output import MachO +from test_tls_function_coverage import make_image + + +def empty_tls_image(body): + image = make_image("register", "unwind", body=body + b"\xc3") + struct.pack_into("\n' + '#include \n' + '#include \n' + 'extern int FixtureExport(void);\n' + 'extern int FixtureData;\n' + 'void Aps5StartGuest_nid_no_patch(void) {\n' + ' int (*weak)(void) = dlsym(RTLD_DEFAULT, "FixtureWeak");\n' + ' int (*protected)(void) = dlsym(RTLD_DEFAULT, "FixtureProtected");\n' + ' if (!weak || weak() != 9 || !protected || protected() != 42) _exit(1);\n' + f' if ((uintptr_t)dlsym(RTLD_DEFAULT, "FixtureAbsolute") != {ABSOLUTE}ULL) _exit(2);\n' + ' if (dlsym(RTLD_DEFAULT, "FixtureHidden") || dlsym(RTLD_DEFAULT, "FixtureLocal")) _exit(3);\n' + ' _exit(FixtureExport() + FixtureData - 5);\n' + '}\n') + for name in ('libkernel.prx', 'libc.prx'): + subprocess.run(['xcrun', 'clang', '-arch', 'x86_64', '-dynamiclib', str(source), + '-undefined', 'dynamic_lookup', '-Wl,-install_name,@rpath/' + name, + '-o', str(libraries / name)], check=True, capture_output=True, timeout=20) + source.write_text('int FixtureHostLibc(void) { return 0; }\n') + output.chmod(0o755) + command = (['/usr/bin/arch', '-x86_64'] if platform.machine() == 'arm64' else []) + [str(output)] + run = subprocess.run(command, capture_output=True, timeout=10) + if run.returncode != 42 and b'Bad CPU type in executable' in run.stderr: + assert os.environ.get('GITHUB_ACTIONS') != 'true', 'macOS CI must execute the x86_64 export probe' + return + assert run.returncode == 42, (run.returncode, run.stdout, run.stderr) + print('macOS executable exports: native x86_64 imports and dlsym passed') + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix='anyps5-macos-executable-exports-') as directory: + work = Path(directory) + run, output = convert(relinker, work, 'exports', executable()) + assert run.returncode == 0, (run.stdout, run.stderr) + data = output.read_bytes() + entries = exports(data) + base = MachO(data).segment('__ELF0')['vmaddr'] + assert entries == { + '_FixtureExport': (0, base + 0x210 - 0x140000000), + '_FixtureData': (0, base + 0x280 - 0x140000000), + '_FixtureWeak': (4, base + 0x220 - 0x140000000), + '_FixtureAbsolute': (2, ABSOLUTE), + '_FixtureProtected': (0, base + 0x210 - 0x140000000), + }, entries + assert MachO(data).flags & 0x8000 + native_execution(work, output) + tls_symbols = (('FixtureTls#A#B', 0x16, 0, 1, 4, 4),) + for info, flags in ((0x16, 0), (0x26, 4)): + run, output = convert(relinker, work, f'tls-{info}', executable((('FixtureTls#A#B', info, 0, 1, 4, 4),), tls=True)) + assert run.returncode == 0, (run.stdout, run.stderr) + data = output.read_bytes() + entry, = exports(data).values() + assert entry[0] == flags + assert bool(MachO(data).flags & 0x8000) == bool(flags) + meta = MachO(data).segment('__APS5DATA') + slot = meta['fileoff'] + entry[1] + 0x140000000 - meta['vmaddr'] + assert struct.unpack_from('\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 d6c58adad2..694ed2e2eb 100644 --- a/core/relinker/relinker/tests/test_macos_output.py +++ b/core/relinker/relinker/tests/test_macos_output.py @@ -14,6 +14,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent / "macos")) from import_fixture import fixture as import_fixture from nid import nid from tls_fixture import fixture as tls_fixture +from test_guest_module_directories import module_with_symbol MH_MAGIC_64 = 0xFEEDFACF CPU_TYPE_X86_64 = 0x01000007 @@ -124,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) @@ -145,12 +157,44 @@ def check_options(relinker, directory): check(result.returncode != 0 and "--windows conflicts with --macos" in result.stdout + result.stderr, "--macos together with --windows was accepted") +def check_runpath_lists(relinker, directory): + source = Path(directory) / "runpaths.elf" + source.write_bytes(bytes(import_fixture())) + modules = Path(directory) / "sce_module" + modules.mkdir() + (modules / "module.prx").write_bytes(module_with_symbol(True)) + for index, (configured, expected) in enumerate(( + ("$ORIGIN/first:$ORIGIN/second", ["@executable_path/first", "@executable_path/second"]), + ("$ORIGIN:$ORIGIN/libs:$ORIGIN", ["@executable_path", "@executable_path/libs", "@executable_path"]), + ("/first:/second", ["/first", "/second"]))): + output = Path(directory) / f"runpaths-{index}" + options = ["--skip-sce-module"] if configured.startswith("/") else [] + result = subprocess.run([str(relinker), "--macos", *options, "--rpath", configured, + str(source), str(output)], capture_output=True, text=True, timeout=20) + check(result.returncode == 0, f"run paths: {result.stdout}{result.stderr}") + actual = MachO(output.read_bytes()).rpaths + check(actual == expected, f"run paths: {actual}") + if not options: + guest = MachO((Path(directory) / "app0/sce_module/module.prx.guest.prx").read_bytes()).rpaths + expected_guest = [path.replace("@executable_path", "@loader_path/../..") for path in expected] + check(guest == expected_guest, f"guest run paths: {guest}") + for configured in (":$ORIGIN/libs", "$ORIGIN/libs:", "$ORIGIN/first::$ORIGIN/second"): + output = Path(directory) / "empty-runpaths" + result = subprocess.run([str(relinker), "--macos", "--skip-sce-module", "--rpath", configured, + str(source), str(output)], capture_output=True, text=True, timeout=20) + check(result.returncode != 0 and "Empty macOS run path component" in result.stdout + result.stderr, + f"empty run path accepted: {configured}") + check(not output.exists(), "empty run path created an output") + + 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) print("macOS output checks passed") diff --git a/core/relinker/relinker/tests/test_macos_tls_alu.py b/core/relinker/relinker/tests/test_macos_tls_alu.py new file mode 100644 index 0000000000..e0199cecf3 --- /dev/null +++ b/core/relinker/relinker/tests/test_macos_tls_alu.py @@ -0,0 +1,58 @@ +"""Check that `relinker --macos` rewrites ALU instructions that read guest TLS through %fs.""" +from pathlib import Path +import subprocess +import sys +import tempfile + +from test_macos_output import MachO +from test_tls_function_coverage import make_image + + +FORMS = { + "sub-rax-stack-guard": (bytes.fromhex("64 48 2b 04 25 28 00 00 00"), bytes.fromhex("9d 48 87 04 24 48 2b 04 24 48 8d 64 24 08")), + "sub-rcx-stack-guard": (bytes.fromhex("64 48 2b 0c 25 28 00 00 00"), bytes.fromhex("9d 48 2b c8 58")), + "xor-r10-stack-guard": (bytes.fromhex("64 4c 33 14 25 28 00 00 00"), bytes.fromhex("9d 4c 33 d0 58")), + "adc-rdx-tls-block": (bytes.fromhex("64 48 13 14 25 f8 ff ff ff"), bytes.fromhex("9d 48 13 d0 58")), + "cmp-r8-thread-pointer": (bytes.fromhex("64 4c 3b 04 25 00 00 00 00"), bytes.fromhex("9d 4c 3b c0 58")), +} + + +def convert(relinker, directory, name, image): + source = Path(directory) / f"{name}.elf" + output = Path(directory) / name + source.write_bytes(bytes(image)) + result = subprocess.run([str(relinker), "--macos", "--skip-sce-module", str(source), str(output)], + capture_output=True, text=True, timeout=60) + if result.returncode != 0: + raise SystemExit(f"{name}: relinker failed: {result.stdout}{result.stderr}") + data = output.read_bytes() + return b"".join(data[lo:hi] for lo, hi in MachO(data).executable_ranges()) + + +def check_outside_block_refused(relinker, directory): + image = make_image("register", "unwind", body=bytes.fromhex("64 48 2b 04 25 00 ff ff ff c3")) + source = Path(directory) / "outside-block.elf" + output = Path(directory) / "outside-block" + source.write_bytes(bytes(image)) + result = subprocess.run([str(relinker), "--macos", "--skip-sce-module", str(source), str(output)], + capture_output=True, text=True, timeout=60) + message = result.stdout + result.stderr + if result.returncode != 2 or "is not in the thread TLS block" not in message or output.exists(): + raise SystemExit(f"outside-block: an ALU read outside the thread TLS block was not refused: {message}") + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-macos-tls-alu-") as directory: + for name, (instruction, operation) in FORMS.items(): + code = convert(relinker, directory, name, make_image("register", "unwind", body=instruction + b"\xc3")) + if instruction in code: + raise SystemExit(f"{name}: the %fs access was left in the code") + if operation not in code: + raise SystemExit(f"{name}: the stub does not apply the operation to the guest TLS value") + check_outside_block_refused(relinker, directory) + print("macOS TLS ALU checks passed") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_macos_tls_overlap.py b/core/relinker/relinker/tests/test_macos_tls_overlap.py new file mode 100644 index 0000000000..cadbda39c3 --- /dev/null +++ b/core/relinker/relinker/tests/test_macos_tls_overlap.py @@ -0,0 +1,53 @@ +"""Check the macOS TLS scan against decodings that start inside another instruction.""" +from pathlib import Path +import subprocess +import sys +import tempfile + +from test_macos_output import MachO +from test_tls_function_coverage import TLS_LOAD, add_alias, make_image + + +def convert(relinker, directory, name, image): + source = Path(directory) / f"{name}.elf" + output = Path(directory) / name + source.write_bytes(bytes(image)) + result = subprocess.run([str(relinker), "--macos", "--skip-sce-module", str(source), str(output)], + capture_output=True, text=True, timeout=60) + return result, output + + +def check_split_function(relinker, directory): + immediate = bytes.fromhex("48 b9 00 00 00 65 2e 62 69 6e") + body = TLS_LOAD + bytes.fromhex("eb 00 8b 40 f0") + immediate + b"\xc3" * 8 + image = add_alias(make_image("register", "symbol", 0x55, body), 0x0c, 0x1256) + result, output = convert(relinker, directory, "function-begins-inside-instruction", image) + if result.returncode != 0: + raise SystemExit(f"function-begins-inside-instruction: relinker failed: {result.stdout}{result.stderr}") + data = output.read_bytes() + code = b"".join(data[lo:hi] for lo, hi in MachO(data).executable_ranges()) + if immediate not in code: + raise SystemExit("function-begins-inside-instruction: the instruction around the GS-prefixed decoding changed") + + +def check_gs_access_trapped(relinker, directory): + instruction = bytes.fromhex("65 48 8b 04 25 28 00 00 00") + result, output = convert(relinker, directory, "gs-access", make_image("register", "unwind", body=instruction + b"\xc3")) + if result.returncode != 0: + raise SystemExit(f"gs-access: relinker failed: {result.stdout}{result.stderr}") + data = output.read_bytes() + code = b"".join(data[lo:hi] for lo, hi in MachO(data).executable_ranges()) + if b"\x0f\x0b" + b"\x90" * (len(instruction) - 2) + b"\xc3" not in code: + raise SystemExit("gs-access: a GS access at an instruction start was not trapped") + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-macos-tls-overlap-") as directory: + check_split_function(relinker, directory) + check_gs_access_trapped(relinker, directory) + print("macOS TLS overlap checks passed") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_needed_libraries.py b/core/relinker/relinker/tests/test_needed_libraries.py new file mode 100644 index 0000000000..9bf39efb8f --- /dev/null +++ b/core/relinker/relinker/tests/test_needed_libraries.py @@ -0,0 +1,92 @@ +import json +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture +from test_guest_symbol_names import importing_executable + +sys.path.insert(0, str(Path(__file__).resolve().parents[4] / "tools")) +import import_audit + + +def named_module(tag): + image = guest_fixture(PLAIN_SITE) + strings = b"\0Party.prx\0" + image[0x800:0x800 + len(strings)] = strings + struct.pack_into("> 32 for _, info, _ in actual) + return + pointers = local + plt if guest else local + if target == "windows": + assert data.startswith(b"MZ") + sections = pe_sections(data) + code = next(section for section in sections if section[0] == b".elf0") + writable = next(section for section in sections if section[0] == (b".elf1" if guest else b".elf0")) + header, = struct.unpack_from(" FUNCTION else FUNCTION + FUNCTION_SIZE + if functions is None: + functions = [(EARLY_FUNCTION, FUNCTION_SIZE)] if early_function else [] + functions.append((FUNCTION, FUNCTION_SIZE)) interpreter = FUNCTION - len(INTERPRETER) image = bytearray(0x1000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) - struct.pack_into(" 1; filtered=1" in result.stdout, (n_nops, result.stdout) + + entry_tail = bytearray(build(0)) + struct.pack_into(" 1; filtered=1" in result.stdout, result.stdout + print("Unused filter fallthrough padding tests passed") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_unused_filter_indirect.py b/core/relinker/relinker/tests/test_unused_filter_indirect.py index 7528f0434b..eda91aa3a0 100644 --- a/core/relinker/relinker/tests/test_unused_filter_indirect.py +++ b/core/relinker/relinker/tests/test_unused_filter_indirect.py @@ -17,16 +17,16 @@ def rel(next_address, target): return struct.pack("> 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): @@ -37,7 +162,31 @@ def import_bindings(elf): return bindings +def macho_bind_opcodes(data): + macho = MachO(data) + bind_offset, bind_size = macho.dyld_info[2], macho.dyld_info[3] + stream = data[bind_offset:bind_offset + bind_size] + opcodes = {} + for symbol in ('puts', 'exit'): + name = ('_' + nid(symbol)).encode() + b'\0' + at = stream.find(name) + assert at > 0, (symbol, stream) + opcodes[symbol] = stream[at - 1] + return opcodes + + 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) @@ -68,6 +217,20 @@ def main(): run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) assert run.returncode == expected, (name, run.returncode, run.stdout, run.stderr) + weak_puts = macos_fixture() + weak_puts[0x5B00 + 24 + 4] = (STB_WEAK << 4) | 2 + for name, image, expected in (('macos-weak', weak_puts, {'puts': 0x41, 'exit': 0x40}), + ('macos-global', macos_fixture(), {'puts': 0x40, 'exit': 0x40})): + source = work / (name + '.elf') + source.write_bytes(bytes(image)) + output = work / name + result = subprocess.run([str(relinker), '--macos', '--skip-sce-module', str(source), str(output)], + capture_output=True, text=True, timeout=30) + 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_bss.py b/core/relinker/relinker/tests/test_windows_bss.py index 7cb1dac6ad..088ebc5a33 100644 --- a/core/relinker/relinker/tests/test_windows_bss.py +++ b/core/relinker/relinker/tests/test_windows_bss.py @@ -65,8 +65,65 @@ def main(): if os.name == "nt": executed = subprocess.run([str(output)], capture_output=True, text=True, timeout=20) assert executed.returncode == 42, f"exit code {executed.returncode:#x}" + check_relocated_zero_fill(relinker, work) + check_scattered_relocations_macos(relinker, work) print("Windows zero-filled memory tests passed") +def check_relocated_zero_fill(relinker, work): + image = fixture() + file_size = struct.unpack_from("= LOAD_RVA + FAR_ADDRESS + 0x1000, hex(image_size) + gap = next(section for section in table if section[2] <= LOAD_RVA + SECTION_ALIGNMENT * 2 < section[2] + section[1]) + name, virtual_size, rva, raw_size, raw = gap + assert raw_size == 0 and raw == 0 and rva + virtual_size <= LOAD_RVA + FAR_ADDRESS, gap + far = next(section for section in table if section[2] == LOAD_RVA + FAR_ADDRESS) + assert far[1] == 0x1000, far + if os.name == "nt": + runnable = work / "runnable.elf" + runnable.write_bytes(sparse_fixture(RUNNABLE_FAR_ADDRESS)) + runnable_output = work / "runnable.exe" + result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(runnable), str(runnable_output)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + executed = subprocess.run([str(runnable_output)], capture_output=True, text=True, timeout=20) + assert executed.returncode == 42, f"exit code {executed.returncode:#x}" + macho = work / "output.macho" + result = subprocess.run([str(relinker), "--skip-sce-module", "--macos", str(source), str(macho)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + peak = child_peak_memory() + assert peak is None or peak < PEAK_MEMORY_LIMIT, f"{peak} bytes of memory for a {FAR_ADDRESS:#x} byte address gap" + assert macho.stat().st_size < 1 << 20, macho.stat().st_size + print("Windows sparse load image tests passed") + + +if __name__ == "__main__": + main() 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(" && std::is_trivially_copyable_v
&& sizeof(Header) == 16); static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v && sizeof(Range) == 32); static_assert(offsetof(Range, deviceAddress) == 16 && offsetof(Range, permissions) == 24); static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v && sizeof(Fault) == 32); -static_assert(offsetof(Fault, address) == 8 && offsetof(Fault, instruction) == 24); +static_assert(offsetof(Fault, address) == 8 && offsetof(Fault, instruction) == 24 && offsetof(Fault, otherFaults) == 28); inline constexpr std::uint32_t WrittenPageShift = 12; inline constexpr std::uint32_t WrittenChunkShift = 17; @@ -56,7 +63,9 @@ inline constexpr std::uint32_t WrittenOverflowWord = sizeof(Fault) / sizeof(std: inline constexpr std::uint32_t WrittenCountWord = WrittenOverflowWord + 1; inline constexpr std::uint32_t WrittenSlotsWord = WrittenCountWord + 1; inline constexpr std::uint32_t WrittenListWord = WrittenSlotsWord + 2 * WrittenPageSlots; -inline constexpr std::size_t FaultBufferBytes = (WrittenListWord + WrittenPageSlots) * sizeof(std::uint32_t); +inline constexpr std::uint32_t SrtGuardWord = WrittenListWord + WrittenPageSlots; +inline constexpr std::uint32_t SrtGuardStageShift = 24; +inline constexpr std::size_t FaultBufferBytes = (SrtGuardWord + 1) * sizeof(std::uint32_t); static_assert((WrittenPageSlots & (WrittenPageSlots - 1)) == 0); static_assert(WrittenChunkPages == 32); diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 7598e4aad9..652d255115 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -35,12 +35,15 @@ function(add_shader_recompiler target runtimeTarget) add_library(${target} STATIC EXCLUDE_FROM_ALL ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Recompiler.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/StencilReplay.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ShaderDiskCache.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ShaderCachePack.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/RectListShaders.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/BarycentricGeometryShader.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/RdnaDecoder/src/RdnaOpcode.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/RdnaDecoder/src/RdnaProgram.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/RdnaDecoder/src/RdnaInstructionDecoder.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/RdnaDecoder/src/GcnInstructionDecoder.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/RdnaDecoder/src/RdnaScalarOpDecoder.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/RdnaDecoder/src/RdnaVectorOpDecoder.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp @@ -73,6 +76,7 @@ function(add_shader_recompiler target runtimeTarget) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Translation/src/TranslationContext/SubwordPacking.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Translation/src/TranslationContext/ExecMask.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Translation/src/ScalarInstructions.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Translation/src/RoundingMode.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Translation/src/VectorInstructions.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Translation/src/FloatInstructions.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Translation/src/Float64Instructions.cpp @@ -109,6 +113,7 @@ function(add_shader_recompiler target runtimeTarget) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/ShaderInfoCollector.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SharedMemoryBarrierInserter.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/DenormalFlushEliminator.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/VectorWindowStoreEliminator.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/RedundantValueEliminator.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtAddressArithmetic.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp @@ -118,6 +123,7 @@ function(add_shader_recompiler target runtimeTarget) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtPlanBuilder.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtRuntimeValidator.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtTape.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtWalker.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvModule.cpp @@ -164,16 +170,25 @@ function(add_shader_recompiler target runtimeTarget) ) target_compile_features(${target} PUBLIC cxx_std_20) + if(WIN32) + target_compile_options(${target} PRIVATE "SHELL:-include ${PROJECT_SOURCE_DIR}/core/libs/prx/libc/include/HostStandardStreams.hpp") + endif() + + if(ANYPS5_ENABLE_SPIRV_TOOLS) + target_sources(${target} PRIVATE ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvOptimizer.cpp) + endif() set(shaderCacheGeneratedDir "${CMAKE_CURRENT_BINARY_DIR}/${target}_generated") - set(shaderCacheVersionHeader "${shaderCacheGeneratedDir}/ShaderCacheVersion.hpp") + set(shaderCacheVersionSource "${shaderCacheGeneratedDir}/ShaderCacheVersion.cpp") file(GLOB_RECURSE shaderCacheRecompilerSources CONFIGURE_DEPENDS - "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/*.cpp" "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/*.hpp" "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/*.h" "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/*.cmake" "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/CMakeLists.txt" ) + get_target_property(shaderCacheCompiledSources ${target} SOURCES) + list(APPEND shaderCacheRecompilerSources ${shaderCacheCompiledSources}) + list(REMOVE_DUPLICATES shaderCacheRecompilerSources) set(shaderCacheKeySources "${PROJECT_SOURCE_DIR}/core/libs/prx/libc/include/HostThreadLocal.hpp") foreach(source IN LISTS shaderCacheKeySources) if(NOT EXISTS "${source}") @@ -217,13 +232,25 @@ function(add_shader_recompiler target runtimeTarget) string(APPEND shaderCacheExtra " system-deps=${ANYPS5_SYSTEM_DEPS_STAMP}") endif() set(shaderCacheReviewedNames + BdaAbi::RangesOrdered BuildBarycentricGeometryShader + BuildBarycentricTessellationShaders BuildPreparedShaderKey BuildRectListShaders CaptureResources DebugProbeActive DepthBitsTextureWidth HashBytes + LayoutBarycentricEmulation + MeshBarycentricLayoutOf + NativeCachePath_nid_no_patch + ImageTableAbi::PoisonCode + ImageTableAbi::PoisonReasonName + ImageTableAbi::TableHeader + IsFmaskTextureFormat + PipelineSpecialization::IsWorkgroupOffset + PipelineSpecialization::WorkgroupOffsetBase + PipelineSpecialization::WorkgroupOffsetWords PixelInput PixelInputBit RayTracingStrict @@ -240,21 +267,42 @@ function(add_shader_recompiler target runtimeTarget) MaterializeShader RegistrationResult ResolveSource + ResourceMaterializer::EvaluateNanoseconds + ResourceMaterializer::MaterializeNanoseconds + ReleaseUndrawnResourcePlan ResourceMaterializer::SpecializationNanoseconds RuntimeAbi::RequireVersion + GcnGuestShaders + SetGcnGuestShaders + GcnImageTilingIndex + GcnImagePitch + GcnImagePow2Pad RuntimeAbi::HeapCapacity + SampledTextureNumericClass SetDebugProbeActive ShaderCacheDirectory + ShaderDiskCache::Shutdown + ShaderStage + HashSpirvWords + ShaderDiskCache::DecodeResult + ShaderDiskCache::Enabled + ShaderDiskCache::EncodeResult + ShaderDiskCache::EntryDirectory + ShaderDiskCache::SourceVersion SrgbDecodeBit SrgbUnormFormat FindUserDataTail LinkUserDataTail ThreadOwned + ProgramRole UnresolvableSwappcTarget ValidateAndOptimizeSpirv FindUserDataCalls UserDataCalleeLength LinkUserDataCalls + FindTableCalls + LinkCalls + LoadStamp WriteFileAtomically ) set(shaderCacheManifest) @@ -277,21 +325,20 @@ function(add_shader_recompiler target runtimeTarget) set(shaderCacheListFile "${shaderCacheGeneratedDir}/sources.txt") file(CONFIGURE OUTPUT "${shaderCacheListFile}" CONTENT "${shaderCacheManifest}" @ONLY) add_custom_command( - OUTPUT "${shaderCacheVersionHeader}" + OUTPUT "${shaderCacheVersionSource}" COMMAND ${CMAKE_COMMAND} "-DROOT=${PROJECT_SOURCE_DIR}" "-DLIST_FILE=${shaderCacheListFile}" "-DRECOMPILER_DIR=${CMAKE_CURRENT_FUNCTION_LIST_DIR}" "-DGIT=${GIT_EXECUTABLE}" "-DEXTRA=${shaderCacheExtra}" - "-DOUTPUT=${shaderCacheVersionHeader}" + "-DOUTPUT=${shaderCacheVersionSource}" -P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ShaderCacheVersion.cmake" DEPENDS ${shaderCacheRecompilerSources} ${shaderCacheGuardSources} ${shaderCacheSubmoduleStamps} "${shaderCacheListFile}" "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ShaderCacheVersion.cmake" COMMENT "Hashing the shader recompiler sources for the shader disk cache" VERBATIM ) - target_sources(${target} PRIVATE "${shaderCacheVersionHeader}") - target_include_directories(${target} PRIVATE "${shaderCacheGeneratedDir}") + target_sources(${target} PRIVATE "${shaderCacheVersionSource}") set_target_properties(${target} PROPERTIES POSITION_INDEPENDENT_CODE ON @@ -305,8 +352,20 @@ function(add_shader_recompiler target runtimeTarget) ) if(ANYPS5_ENABLE_SPIRV_TOOLS) - target_sources(${target} PRIVATE ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvOptimizer.cpp) target_link_libraries(${target} PRIVATE SPIRV-Tools-opt SPIRV-Tools-static) endif() target_compile_definitions(${target} PUBLIC ANYPS5_ENABLE_SPIRV_TOOLS=$) + if(BUILD_TESTING AND ANYPS5_ENABLE_SPIRV_TOOLS AND NOT ANYPS5_USE_SYSTEM_DEPS AND NOT TARGET spirv_validator_exit_tests) + add_test_executable(spirv_validator_exit_tests "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/tests/SpirvValidatorExit.cpp") + target_link_libraries(spirv_validator_exit_tests PRIVATE SPIRV-Tools-static) + set_target_properties(spirv_validator_exit_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(spirv_validator_exit_tests) + add_test(NAME spirv_validator_exit COMMAND spirv_validator_exit_tests) + set_tests_properties(spirv_validator_exit PROPERTIES TIMEOUT 30) + if(MINGW) + target_link_libraries(spirv_validator_exit_tests PRIVATE libc) + set_tests_properties(spirv_validator_exit PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endif() endfunction() diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 33e899a818..cf6d2f2fe5 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -4,6 +4,7 @@ #include "Recompiler.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "prx/libc/include/HostThreadLocal.hpp" +#include #include #include #include @@ -52,6 +53,8 @@ public: append(key, request.context.pixel); append(key, request.context.vertex); append(key, request.context.floatMode); + append(key, request.context.convertedSamples); + appendFallbacks(key, request); appendMesh(key, request); appendTessellation(key, request); std::uint64_t hash = 0xcbf29ce484222325ull; @@ -84,6 +87,8 @@ private: append(key, request.context.pixel); append(key, request.context.vertex); append(key, request.context.floatMode); + append(key, request.context.convertedSamples); + appendFallbacks(key, request); appendMesh(key, request); appendTessellation(key, request); append(key, request.target); @@ -92,12 +97,33 @@ private: append(key, RayTracingMiss()); } + static void appendFallbacks(std::vector& key, const RecompileRequest& request) { + append(key, request.context.linkedCallFallbacks.size()); + for (const auto programCounter : request.context.linkedCallFallbacks) append(key, programCounter); + } + static void appendMesh(std::vector& key, const RecompileRequest& request) { + if (request.shader.stage == ShaderStage::Fragment) { + append(key, request.graphics && request.graphics->mesh); + return; + } if (request.shader.stage != ShaderStage::Mesh) return; const auto* mesh = request.graphics && request.graphics->mesh ? &*request.graphics->mesh : nullptr; append(key, mesh != nullptr); if (mesh == nullptr) return; - for (const auto value : {mesh->inputPrimitive, mesh->primitivesPerGroup, mesh->verticesPerGroup, mesh->maxVertices, mesh->maxPrimitives, mesh->threadsPerGroup, mesh->ldsSizeDwords, mesh->provokingVertex, mesh->esgsItemSize}) 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); + const auto& barycentrics = request.graphics->meshBarycentrics; + append(key, barycentrics.has_value()); + if (!barycentrics.has_value()) return; + append(key, barycentrics->emulation.active); + append(key, barycentrics->emulation.smooth); + append(key, barycentrics->emulation.linear); + append(key, barycentrics->parameters.size()); + for (const auto& parameter : barycentrics->parameters) { + append(key, parameter.location); + append(key, parameter.sourceLocation); + append(key, parameter.perVertex); + } } static void appendTessellation(std::vector& key, const RecompileRequest& request) { @@ -106,10 +132,11 @@ private: append(key, tessellation != nullptr); if (tessellation == nullptr) return; for (const auto value : {tessellation->inputControlPoints, tessellation->outputControlPoints, tessellation->domain, tessellation->partitioning, tessellation->outputTopology}) append(key, value); + if (request.shader.stage != ShaderStage::TessellationEvaluation) append(key, tessellation->mergedHull); if (request.shader.stage == ShaderStage::Local) return; append(key, tessellation->patchesPerGroup); append(key, tessellation->primitiveGroupSize); - append(key, tessellation->breakAtInstance); + append(key, TessellationInstancePatches(request)); } template @@ -169,6 +196,7 @@ private: append(key, value.posZ); append(key, value.posW); append(key, value.frontFace); + append(key, value.frontFaceAllBits); append(key, value.ancillary); append(key, value.sampleShading); append(key, value.noPerspective); @@ -182,8 +210,15 @@ private: append(key, value.orderedPixelShader); append(key, value.alphaToCoverage); append(key, value.stencilRefExportEnable); + append(key, value.stencilOperationValueExport); append(key, value.ditheredAlphaToCoverage); + append(key, value.quadPixelMask); + append(key, value.quadSampleMask); + append(key, value.iteratesSamples); + append(key, value.positionLocation); + append(key, value.sampleLocations); append(key, value.targetOutputMode); + append(key, value.sampleOrder); } static void append(std::vector& key, const ShaderVertexResourceDestination& value) { @@ -198,6 +233,11 @@ private: append(key, value.fetchBufferReg); append(key, value.fetchEmbedded); append(key, value.paClVsOutCntl); + append(key, value.depthRemapMode); + append(key, value.pointList); + append(key, std::bit_cast(value.pointSize)); + append(key, std::bit_cast(value.pointSizeMin)); + append(key, std::bit_cast(value.pointSizeMax)); if (value.resourcesNum > value.resources.size()) throw std::runtime_error("Shader cache: invalid vertex resource count"); append(key, value.resourcesNum); for (std::uint32_t i = 0; i < value.resourcesNum; ++i) { @@ -247,10 +287,16 @@ private: append(key, value.srgbDecodeFormats); append(key, value.narrowSubgroupClock); append(key, value.float64DenormPreserve); + append(key, value.depthRangeRemap); append(key, value.registrationModules); append(key, value.registrationWordLimit); append(key, value.vertexSubgroups); append(key, value.float16DenormPreserve); + append(key, value.spannedWaves); + append(key, value.robustBufferAccess2); + append(key, value.float32FlushToZero); + append(key, value.waveLdsMemoryBarriers); + append(key, value.float32DenormPreserve); } }; diff --git a/core/shader/recompiler/ControlFlow/include/ControlFlow/ControlFlowGraph.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/ControlFlowGraph.hpp index d93905ba94..c9ee6e2fc7 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/ControlFlowGraph.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/ControlFlowGraph.hpp @@ -107,6 +107,9 @@ struct ControlFlowGraph { std::vector components; std::vector codeTableLoadProgramCounters; std::vector codeTableLoads; + std::vector unlinkedCallProgramCounters; + std::uint32_t linkEscapeWaveSize = 0; + std::string linkEscapeReason; std::uint32_t entryBlock = InvalidControlFlowId; bool hasFetchCall = false; std::uint32_t fetchCallProgramCounter = 0; diff --git a/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp index 0b7cb0fb7d..1f333297f7 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp @@ -13,6 +13,7 @@ namespace ShaderRecompiler { struct UserDataTail { std::uint32_t programCounter; std::uint32_t userDataRegister; + std::optional constantHigh; }; [[nodiscard]] std::optional FindUserDataTail(const RdnaProgram& program, std::uint32_t userDataBase, std::uint32_t userDataCount); @@ -22,6 +23,8 @@ struct SwappcInfo { bool fetchCallAllowed = false; std::uint32_t userDataBaseRegister = 0; std::uint32_t userDataCount = 0; + std::uint32_t waveSize = 0; + std::span linkedCallFallbacks; }; struct SwappcCall { @@ -40,10 +43,34 @@ struct UserDataCall { std::uint32_t userDataRegister = 0; }; +struct TableCall { + std::uint32_t programCounter = 0; + std::uint32_t linkRegister = 0; + std::uint32_t targetRegister = 0; + std::uint32_t userDataRegister = 0; + std::uint32_t descriptorOffset = 0; + std::uint32_t entryOffset = 0; + std::uint32_t entryStride = 0; + std::optional constantPointer; +}; + +struct TableCallee { + std::uint64_t target = 0; + std::vector code; +}; + [[nodiscard]] std::optional UnresolvableSwappcTarget(const RdnaProgram& program, const SwappcInfo* swappc); [[nodiscard]] std::vector FindUserDataCalls(const RdnaProgram& program, const SwappcInfo& swappc); [[nodiscard]] std::optional UserDataCalleeLength(std::span callee, std::uint32_t linkRegister); [[nodiscard]] std::vector LinkUserDataCalls(const RdnaProgram& program, std::span calls, std::span> callees); +[[nodiscard]] std::vector FindTableCalls(const RdnaProgram& program, const SwappcInfo& swappc); +struct LinkedCalls { + std::vector code; + std::vector fallbacks; +}; + +[[nodiscard]] LinkedCalls LinkCalls(const RdnaProgram& program, std::span userDataCalls, std::span> userDataCallees, + std::span tableCalls, std::span> tableCallees); class GraphBuilder { public: diff --git a/core/shader/recompiler/ControlFlow/include/ControlFlow/RequestSerializer.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/RequestSerializer.hpp index 0f59c68558..bf76e576bd 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/RequestSerializer.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/RequestSerializer.hpp @@ -33,6 +33,7 @@ namespace ShaderRecompiler { std::optional vertex; std::vector> memoryBytesStorage; std::vector memory; + std::vector linkedCallFallbacks; std::vector supportedCapabilities; std::vector supportedExtensionStorage; std::vector supportedExtensions; diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index 48bdd33cd0..afe341bb79 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -2,10 +2,12 @@ #include "ControlFlow/ControlFlowHelpers.hpp" #include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include +#include #include #include #include #include +#include #include #include #include @@ -194,6 +196,8 @@ bool addsImmediate(const RdnaInstruction& instruction, std::uint32_t reg, std::u (isRegister(instruction.source1, RdnaOperandKind::ScalarRegister, reg) && isImmediate(instruction.source0, immediate))); } +bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize = 0u, std::uint32_t skipBranchesPast = 0u); + bool resolveJumpTable(const RdnaProgram& program, std::uint32_t index, BoundedJumpTable& result) { if (index < 9u) return false; const auto& branch = program.instructions[index]; @@ -222,8 +226,13 @@ bool resolveJumpTable(const RdnaProgram& program, std::uint32_t index, BoundedJu const auto& basePc = program.instructions[loadIndex - 3u]; const auto& baseLow = program.instructions[loadIndex - 2u]; const auto& baseHigh = program.instructions[loadIndex - 1u]; - const auto& shift = program.instructions[loadIndex - 4u]; - const auto& bound = program.instructions[loadIndex - 5u]; + const auto offsetReg = scalarIndex(load.source1); + std::uint32_t shiftIndex = 0, boundIndex = 0; + if (offsetReg == NoScalarRegister || + !findLastWriter(program, loadIndex - 3u, {offsetReg}, shiftIndex) || + !findLastWriter(program, shiftIndex, {offsetReg}, boundIndex)) return false; + const auto& shift = program.instructions[shiftIndex]; + const auto& bound = program.instructions[boundIndex]; const auto baseReg = load.source0.reg; std::uint32_t displacement = 0, carry = 0, shiftAmount = 0, maximum = 0; if (basePc.op != RdnaOpcode::SGetpcB64 || !isRegister(basePc.destination, RdnaOperandKind::ScalarRegister, baseReg) || @@ -257,7 +266,7 @@ bool resolveJumpTable(const RdnaProgram& program, std::uint32_t index, BoundedJu return false; } -bool writesScalar(const RdnaInstruction& instruction, std::uint32_t index) { +bool writesScalar(const RdnaInstruction& instruction, std::uint32_t index, std::uint32_t waveSize = 0u) { std::uint32_t count = instruction.dataDwordCount; switch (instruction.family) { case RdnaInstructionFamily::SOP1: @@ -273,7 +282,7 @@ bool writesScalar(const RdnaInstruction& instruction, std::uint32_t index) { case RdnaInstructionFamily::VOP3: case RdnaInstructionFamily::VOP3P: case RdnaInstructionFamily::VOPC: - count = 2u; + count = instruction.op == RdnaOpcode::VReadlaneB32 || instruction.op == RdnaOpcode::VReadfirstlaneB32 || waveSize == 32u ? 1u : 2u; break; case RdnaInstructionFamily::SOPC: case RdnaInstructionFamily::SOPP: @@ -293,11 +302,12 @@ bool writesScalar(const RdnaInstruction& instruction, std::uint32_t index) { return covers(instruction.destination) || covers(instruction.destination2); } -bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer) { +bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize, std::uint32_t skipBranchesPast) { for (auto position = end; position-- > 0u;) { const auto& instruction = program.instructions[position]; - if (IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SSetpcB64 || instruction.op == RdnaOpcode::SEndpgm) return false; - if (std::ranges::any_of(indices, [&](std::uint32_t index) { return writesScalar(instruction, index); })) { + const bool skipped = skipBranchesPast != 0u && IsDirectBranchOpcode(instruction.op) && instruction.op != RdnaOpcode::SBranch && instruction.branchTarget > skipBranchesPast; + if ((IsDirectBranchOpcode(instruction.op) && !skipped) || instruction.op == RdnaOpcode::SSetpcB64 || instruction.op == RdnaOpcode::SEndpgm) return false; + if (std::ranges::any_of(indices, [&](std::uint32_t index) { return writesScalar(instruction, index, waveSize); })) { writer = position; return true; } @@ -469,13 +479,323 @@ bool isFetchCall(const RdnaProgram& program, std::uint32_t index, const SwappcIn const std::uint32_t targetRegister = scalarIndex(instruction.source0); const bool userDataPair = swappc != nullptr && targetRegister != NoScalarRegister && targetRegister >= swappc->userDataBaseRegister && targetRegister + 1u < swappc->userDataBaseRegister + swappc->userDataCount && - std::none_of(program.instructions.begin(), program.instructions.end(), [&](const RdnaInstruction& other) { + std::none_of(program.instructions.begin(), GcnGuestShaders() ? program.instructions.begin() + index : program.instructions.end(), [&](const RdnaInstruction& other) { return writesScalar(other, targetRegister) || writesScalar(other, targetRegister + 1u); }); return swappc != nullptr && swappc->fetchCallAllowed && positional && (userDataPair || outsideProgram); } -std::vector analyzeSwappcCalls(const RdnaProgram& program, const SwappcInfo* swappc) { +bool overwritesScalar(const RdnaInstruction& instruction, std::uint32_t reg, std::uint32_t waveSize) { + const auto first = scalarIndex(instruction.destination); + switch (instruction.family) { + case RdnaInstructionFamily::SMEM: + return isScalarLoad(instruction.op) && first != NoScalarRegister && reg >= first && reg - first < instruction.dataDwordCount; + case RdnaInstructionFamily::SOP1: + switch (instruction.op) { + case RdnaOpcode::SMovreldB32: + case RdnaOpcode::SMovreldB64: + case RdnaOpcode::SMovrelsd2B32: + case RdnaOpcode::SCmovB32: + case RdnaOpcode::SCmovB64: + case RdnaOpcode::SBitset0B32: + case RdnaOpcode::SBitset0B64: + case RdnaOpcode::SBitset1B32: + case RdnaOpcode::SBitset1B64: + case RdnaOpcode::SSwappcB64: + case RdnaOpcode::SSetpcB64: return false; + case RdnaOpcode::SMovB64: + case RdnaOpcode::SGetpcB64: return first != NoScalarRegister && reg >= first && reg - first < 2u; + default: return first != NoScalarRegister && reg == first; + } + case RdnaInstructionFamily::SOP2: return first != NoScalarRegister && reg == first; + case RdnaInstructionFamily::SOPK: return instruction.op == RdnaOpcode::SMovkI32 && reg == first; + case RdnaInstructionFamily::VOP1: + case RdnaInstructionFamily::VOP2: + case RdnaInstructionFamily::VOP3: + case RdnaInstructionFamily::VOP3P: + case RdnaInstructionFamily::VOPC: { + const std::uint32_t count = instruction.op != RdnaOpcode::VReadlaneB32 && instruction.op != RdnaOpcode::VReadfirstlaneB32 && waveSize == 64u ? 2u : 1u; + return std::ranges::any_of(std::array{&instruction.destination, &instruction.destination2}, [&](const RdnaOperand* operand) { + const auto slot = scalarIndex(*operand); + return slot != NoScalarRegister && reg >= slot && reg - slot < count; + }); + } + default: return false; + } +} + +bool readsVectorSources64(RdnaOpcode op) { + switch (op) { + case RdnaOpcode::VMadU64U32: + case RdnaOpcode::VMadI64I32: + case RdnaOpcode::VFmaF64: + case RdnaOpcode::VAddF64: + case RdnaOpcode::VMulF64: + case RdnaOpcode::VMinF64: + case RdnaOpcode::VMaxF64: + case RdnaOpcode::VLdexpF64: + case RdnaOpcode::VCvtI32F64: + case RdnaOpcode::VCvtF64I32: + case RdnaOpcode::VCvtF32F64: + case RdnaOpcode::VCvtF64F32: + case RdnaOpcode::VCvtU32F64: + case RdnaOpcode::VCvtF64U32: + case RdnaOpcode::VTruncF64: + case RdnaOpcode::VCeilF64: + case RdnaOpcode::VRndneF64: + case RdnaOpcode::VFloorF64: + case RdnaOpcode::VFrexpExpI32F64: + case RdnaOpcode::VFrexpMantF64: + case RdnaOpcode::VFractF64: + case RdnaOpcode::VRcpF64: + case RdnaOpcode::VRsqF64: + case RdnaOpcode::VSqrtF64: + case RdnaOpcode::VTrigPreopF64: + case RdnaOpcode::VDivScaleF64: + case RdnaOpcode::VDivFmasF64: + case RdnaOpcode::VDivFixupF64: + case RdnaOpcode::VLshlrevB64: + case RdnaOpcode::VLshrrevB64: + case RdnaOpcode::VAshrrevI64: + case RdnaOpcode::VCmpEqI64: + case RdnaOpcode::VCmpLtU64: + case RdnaOpcode::VCmpEqU64: + case RdnaOpcode::VCmpGtU64: + case RdnaOpcode::VCmpNeU64: + case RdnaOpcode::VCmpxNeI64: + case RdnaOpcode::VCmpxNeU64: + case RdnaOpcode::VCmpFI64: + case RdnaOpcode::VCmpLtI64: + case RdnaOpcode::VCmpLeI64: + case RdnaOpcode::VCmpGtI64: + case RdnaOpcode::VCmpNeI64: + case RdnaOpcode::VCmpGeI64: + case RdnaOpcode::VCmpTI64: + case RdnaOpcode::VCmpxFI64: + case RdnaOpcode::VCmpxLtI64: + case RdnaOpcode::VCmpxEqI64: + case RdnaOpcode::VCmpxLeI64: + case RdnaOpcode::VCmpxGtI64: + case RdnaOpcode::VCmpxGeI64: + case RdnaOpcode::VCmpxTI64: + case RdnaOpcode::VCmpFU64: + case RdnaOpcode::VCmpLeU64: + case RdnaOpcode::VCmpGeU64: + case RdnaOpcode::VCmpTU64: + case RdnaOpcode::VCmpxFU64: + case RdnaOpcode::VCmpxLtU64: + case RdnaOpcode::VCmpxEqU64: + case RdnaOpcode::VCmpxLeU64: + case RdnaOpcode::VCmpxGtU64: + case RdnaOpcode::VCmpxGeU64: + case RdnaOpcode::VCmpxTU64: + case RdnaOpcode::VCmpFF64: + case RdnaOpcode::VCmpLtF64: + case RdnaOpcode::VCmpEqF64: + case RdnaOpcode::VCmpLeF64: + case RdnaOpcode::VCmpGtF64: + case RdnaOpcode::VCmpLgF64: + case RdnaOpcode::VCmpGeF64: + case RdnaOpcode::VCmpOF64: + case RdnaOpcode::VCmpUF64: + case RdnaOpcode::VCmpNgeF64: + case RdnaOpcode::VCmpNlgF64: + case RdnaOpcode::VCmpNgtF64: + case RdnaOpcode::VCmpNleF64: + case RdnaOpcode::VCmpNeqF64: + case RdnaOpcode::VCmpNltF64: + case RdnaOpcode::VCmpTruF64: + case RdnaOpcode::VCmpClassF64: + case RdnaOpcode::VCmpxFF64: + case RdnaOpcode::VCmpxLtF64: + case RdnaOpcode::VCmpxEqF64: + case RdnaOpcode::VCmpxLeF64: + case RdnaOpcode::VCmpxGtF64: + case RdnaOpcode::VCmpxLgF64: + case RdnaOpcode::VCmpxGeF64: + case RdnaOpcode::VCmpxOF64: + case RdnaOpcode::VCmpxUF64: + case RdnaOpcode::VCmpxNgeF64: + case RdnaOpcode::VCmpxNlgF64: + case RdnaOpcode::VCmpxNgtF64: + case RdnaOpcode::VCmpxNleF64: + case RdnaOpcode::VCmpxNeqF64: + case RdnaOpcode::VCmpxNltF64: + case RdnaOpcode::VCmpxTruF64: + case RdnaOpcode::VCmpxClassF64: + return true; + default: return false; + } +} + +bool readsScalarDword(const RdnaInstruction& instruction, std::uint32_t reg, std::uint32_t waveSize) { + if (instruction.op == RdnaOpcode::SMovrelsB32 || instruction.op == RdnaOpcode::SMovrelsB64) return true; + const auto overlaps = [&](const RdnaOperand& operand, std::uint32_t count) { + const auto slot = scalarIndex(operand); + return slot != NoScalarRegister && slot <= reg && reg - slot < count; + }; + std::uint32_t source0Count = 1u, source1Count = 1u, source2Count = 1u, source3Count = 1u; + switch (instruction.family) { + case RdnaInstructionFamily::SMEM: + switch (instruction.op) { + case RdnaOpcode::SBufferLoadDword: + case RdnaOpcode::SBufferLoadDwordx2: + case RdnaOpcode::SBufferLoadDwordx4: + case RdnaOpcode::SBufferLoadDwordx8: + case RdnaOpcode::SBufferLoadDwordx16: source0Count = 4u; break; + default: source0Count = 2u; break; + } + break; + case RdnaInstructionFamily::MUBUF: + case RdnaInstructionFamily::MTBUF: source1Count = 4u; break; + case RdnaInstructionFamily::MIMG: + source1Count = instruction.imageR128 ? 4u : 8u; + source2Count = 4u; + break; + case RdnaInstructionFamily::FLAT: + source0Count = 2u; + source1Count = 2u; + break; + case RdnaInstructionFamily::SOPK: + if (instruction.op != RdnaOpcode::SMovkI32 && instruction.op != RdnaOpcode::SCallB64 && overlaps(instruction.destination, 1u)) return true; + break; + case RdnaInstructionFamily::SOP1: + case RdnaInstructionFamily::SOP2: + case RdnaInstructionFamily::SOPC: + switch (instruction.op) { + case RdnaOpcode::SBcnt0I32B64: + case RdnaOpcode::SBcnt1I32B64: + case RdnaOpcode::SFf0I32B64: + case RdnaOpcode::SFf1I32B64: + case RdnaOpcode::SFlbitI32B64: + case RdnaOpcode::SFlbitI32I64: + case RdnaOpcode::SCmpEqU64: + case RdnaOpcode::SCmpLgU64: + case RdnaOpcode::SBitcmp0B64: + case RdnaOpcode::SBitcmp1B64: source0Count = source1Count = 2u; break; + default: source0Count = source1Count = instruction.family == RdnaInstructionFamily::SOPC ? 1u : std::max(instruction.dataDwordCount, 1u); break; + } + break; + case RdnaInstructionFamily::VOP1: + case RdnaInstructionFamily::VOP2: + case RdnaInstructionFamily::VOP3: + case RdnaInstructionFamily::VOP3P: + case RdnaInstructionFamily::VOPC: + source0Count = source1Count = source2Count = source3Count = readsVectorSources64(instruction.op) ? 2u : 1u; + switch (instruction.op) { + case RdnaOpcode::VCndmaskB32: + case RdnaOpcode::VAddcU32: + case RdnaOpcode::VSubCoCiU32: + case RdnaOpcode::VSubrevCoCiU32: + case RdnaOpcode::VDivFmasF32: + case RdnaOpcode::VDivFmasF64: source2Count = std::max(source2Count, waveSize == 32u ? 1u : 2u); break; + default: break; + } + break; + default: break; + } + switch (instruction.op) { + case RdnaOpcode::SBitset0B32: + case RdnaOpcode::SBitset0B64: + case RdnaOpcode::SBitset1B32: + case RdnaOpcode::SBitset1B64: + case RdnaOpcode::SCmovB32: + case RdnaOpcode::SCmovB64: + if (overlaps(instruction.destination, 2u)) return true; + break; + default: break; + } + return overlaps(instruction.source0, source0Count) || overlaps(instruction.source1, source1Count) || overlaps(instruction.source2, source2Count) || overlaps(instruction.source3, source3Count); +} + +struct LinkEscape { + std::uint32_t waveSize = 0; + std::string reason; +}; + +template +LinkEscape requireReturnAddressesStayInCalls(const RdnaProgram& program, const std::vector& calls, const std::vector& unlinked, std::uint32_t knownWaveSize, const TError& escapeError) { + const bool gcn = GcnGuestShaders(); + std::set links; + for (const auto& call : calls) { + if (!call.fetch || gcn) links.insert(call.linkRegister); + } + const auto size = static_cast(program.instructions.size()); + const auto successors = [&](std::uint32_t index) { + std::vector next; + const auto& instruction = program.instructions[index]; + if (instruction.op == RdnaOpcode::SEndpgm || instruction.op == RdnaOpcode::SCodeEnd) return next; + if (IsDirectBranchOpcode(instruction.op)) { + next.push_back(instructionIndexOfProgramCounter(program, instruction.branchTarget)); + if (IsConditionalBranchOpcode(instruction.op)) next.push_back(index + 1u); + } else if (instruction.op == RdnaOpcode::SSwappcB64 || instruction.op == RdnaOpcode::SCallB64) { + const SwappcCall* call = findCallAt(calls, index); + next.push_back(call == nullptr || call->fetch ? index + 1u : call->targetIndex); + } else if (instruction.op == RdnaOpcode::SSetpcB64) { + std::uint32_t target = 0; + if (const SwappcCall* call = findReturnAt(calls, index); call != nullptr) { + next.push_back(call->callIndex + 1u); + } else if (resolveSetpcTarget(program, index, target)) { + next.push_back(instructionIndexOfProgramCounter(program, target)); + } + } else { + next.push_back(index + 1u); + } + std::erase_if(next, [&](std::uint32_t successor) { return successor >= size; }); + return next; + }; + const auto escapeAt = [&](std::uint32_t waveSize) -> std::optional { + for (const auto link : links) { + const auto writesLink = [&](std::uint32_t index) { + const SwappcCall* call = findCallAt(calls, index); + return (call != nullptr && call->linkRegister == link) || (std::ranges::find(unlinked, index) != unlinked.end() && scalarIndex(program.instructions[index].destination) == link); + }; + std::vector holds(size, 0u); + std::vector work; + for (std::uint32_t index = 0; index < size; ++index) { + if (writesLink(index)) work.push_back(index); + } + const auto after = [&](std::uint32_t index) -> std::uint8_t { + if (writesLink(index)) return 3u; + std::uint8_t value = holds[index]; + if (overwritesScalar(program.instructions[index], link, waveSize)) value &= 2u; + if (overwritesScalar(program.instructions[index], link + 1u, waveSize)) value &= 1u; + return value; + }; + while (!work.empty()) { + const auto index = work.back(); + work.pop_back(); + const auto value = after(index); + for (const auto successor : successors(index)) { + if ((holds[successor] | value) == holds[successor]) continue; + holds[successor] |= value; + work.push_back(successor); + } + } + for (std::uint32_t index = 0; index < size; ++index) { + const auto& instruction = program.instructions[index]; + if (holds[index] == 0u || writesLink(index) || instruction.family == RdnaInstructionFamily::SOPC || findReturnAt(calls, index) != nullptr) continue; + const bool inside = std::ranges::any_of(calls, [&](const SwappcCall& call) { return !call.fetch && call.linkRegister == link && index >= call.targetIndex && index <= call.returnIndex; }); + if (inside || !(((holds[index] & 1u) != 0u && readsScalarDword(instruction, link, waveSize)) || ((holds[index] & 2u) != 0u && readsScalarDword(instruction, link + 1u, waveSize)))) continue; + const auto owner = std::ranges::find_if(calls, [&](const SwappcCall& call) { return (!call.fetch || gcn) && call.linkRegister == link; }); + return escapeError(*owner, instruction); + } + } + return std::nullopt; + }; + if (knownWaveSize != 0u) { + if (const auto escape = escapeAt(knownWaveSize)) throw *escape; + return {}; + } + const auto narrow = escapeAt(32u); + const auto wide = escapeAt(64u); + if (narrow && wide) throw *narrow; + if (narrow) return {32u, narrow->what()}; + if (wide) return {64u, wide->what()}; + return {}; +} + +std::vector analyzeSwappcCalls(const RdnaProgram& program, const SwappcInfo* swappc, std::vector& unlinked, LinkEscape& linkEscape) { std::vector calls; for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { const auto& instruction = program.instructions[index]; @@ -502,6 +822,10 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa continue; } if (!staticTarget || call.targetIndex == InvalidControlFlowId) { + if (swappc != nullptr && std::ranges::find(swappc->linkedCallFallbacks, instruction.programCounter) != swappc->linkedCallFallbacks.end()) { + unlinked.push_back(index); + continue; + } if (std::ranges::any_of(calls, [&](const SwappcCall& other) { return other.linkRegister == call.linkRegister; })) { throw std::invalid_argument("unclosable scalar call/return pairing at program counter " + toHexString(instruction.programCounter) + ": link register s[" + std::to_string(call.linkRegister) + "] is shared with another call"); @@ -574,7 +898,8 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa continue; } const bool outsideRegion = regionEnd != InvalidControlFlowId && (index < call.targetIndex || index > regionEnd); - if (touchesLinkRegister(instruction, call.linkRegister) && !(outsideRegion && onlyWritesLinkRegister(instruction, call.linkRegister))) { + if (outsideRegion || (call.fetch && GcnGuestShaders())) continue; + if (touchesLinkRegister(instruction, call.linkRegister)) { throw escapeError(call, instruction); } } @@ -609,14 +934,12 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa if (std::ranges::any_of(calls, [&](const SwappcCall& other) { return other.callIndex == index && other.linkRegister == call.linkRegister; })) continue; const auto& instruction = program.instructions[index]; const auto region = std::ranges::find_if(calls, [&](const SwappcCall& other) { return other.linkRegister == call.linkRegister && index >= other.targetIndex && index <= other.returnIndex; }); - if (region != calls.end()) { - if (index != region->returnIndex && touchesLinkRegister(instruction, call.linkRegister)) throw escapeError(*region, instruction); - } else if (touchesLinkRegister(instruction, call.linkRegister) && !onlyWritesLinkRegister(instruction, call.linkRegister)) { - throw escapeError(call, instruction); - } + if (region != calls.end() && index != region->returnIndex && touchesLinkRegister(instruction, call.linkRegister)) throw escapeError(*region, instruction); } } + linkEscape = requireReturnAddressesStayInCalls(program, calls, unlinked, swappc != nullptr ? swappc->waveSize : 0u, escapeError); + for (std::size_t first = 0; first < calls.size(); ++first) { if (!calls[first].fetch && calls[first].callIndex >= calls[first].targetIndex && calls[first].callIndex <= calls[first].returnIndex) { throw std::invalid_argument("recursive scalar call at program counter " + toHexString(program.instructions[calls[first].callIndex].programCounter) + @@ -664,7 +987,7 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa } if (instruction.op == RdnaOpcode::SSwappcB64 || instruction.op == RdnaOpcode::SCallB64) { const SwappcCall* call = findCallAt(calls, index); - transfers.push_back(Transfer{index, call->fetch ? index + 1u : call->targetIndex}); + transfers.push_back(Transfer{index, call == nullptr || call->fetch ? index + 1u : call->targetIndex}); continue; } if (instruction.op != RdnaOpcode::SEndpgm && instruction.op != RdnaOpcode::SCodeEnd) transfers.push_back(Transfer{index, index + 1u}); @@ -958,23 +1281,119 @@ std::optional UnresolvableSwappcTarget(const RdnaProgram& program return std::nullopt; } -std::vector FindUserDataCalls(const RdnaProgram& program, const SwappcInfo& swappc) { +namespace { + +std::set branchLabels(const RdnaProgram& program) { std::set labels; for (const auto& instruction : program.instructions) { if (IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SCallB64) labels.insert(instruction.branchTarget); } - const auto userData = [&](std::uint32_t reg) { return reg >= swappc.userDataBaseRegister && reg - swappc.userDataBaseRegister < swappc.userDataCount; }; - const auto unchangedUntil = [&](std::uint32_t reg, std::uint32_t reader) { - for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { - if (!writesScalar(program.instructions[index], reg)) continue; - if (index <= reader) return false; - for (std::uint32_t later = index; later < program.instructions.size(); ++later) { - const auto& instruction = program.instructions[later]; - if ((IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SCallB64) && instruction.branchTarget <= program.instructions[reader].programCounter) return false; - } + return labels; +} + +bool unchangedUntil(const RdnaProgram& program, std::uint32_t reg, std::uint32_t reader, std::uint32_t waveSize) { + for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { + const bool readerWritesAfterReading = index == reader && GcnGuestShaders(); + if (readerWritesAfterReading || !writesScalar(program.instructions[index], reg, waveSize)) continue; + if (index <= reader) return false; + for (std::uint32_t later = index; later < program.instructions.size(); ++later) { + const auto& instruction = program.instructions[later]; + if ((IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SCallB64) && instruction.branchTarget <= program.instructions[reader].programCounter) return false; } - return true; + } + return true; +} + +bool isScalarBufferLoad(RdnaOpcode op) { + switch (op) { + case RdnaOpcode::SBufferLoadDword: + case RdnaOpcode::SBufferLoadDwordx2: + case RdnaOpcode::SBufferLoadDwordx4: + case RdnaOpcode::SBufferLoadDwordx8: + case RdnaOpcode::SBufferLoadDwordx16: return true; + default: return false; + } +} + +bool loadedDword(const RdnaProgram& program, std::uint32_t reg, std::uint32_t before, std::uint32_t waveSize, std::uint32_t skipBranchesPast, std::uint32_t& load, std::uint32_t& dword) { + for (std::uint32_t copies = 0; copies < 4u; ++copies) { + std::uint32_t writer = 0; + if (!findLastWriter(program, before, {reg}, writer, waveSize, skipBranchesPast)) return false; + const auto& instruction = program.instructions[writer]; + const auto destination = scalarIndex(instruction.destination); + const auto source = scalarIndex(instruction.source0); + if (instruction.op == RdnaOpcode::SMovB32 && destination == reg && source != NoScalarRegister) { + reg = source; + } else if (instruction.op == RdnaOpcode::SMovB64 && destination != NoScalarRegister && reg - destination < 2u && source != NoScalarRegister) { + reg = source + (reg - destination); + } else if (isScalarBufferLoad(instruction.op) && destination != NoScalarRegister && reg >= destination && reg - destination < instruction.dataDwordCount) { + load = writer; + dword = reg - destination; + return true; + } else { + return false; + } + before = writer; + } + return false; +} + +bool reachingWriter(const RdnaProgram& program, std::uint32_t reader, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize) { + std::multimap branches; + std::set callTargets; + for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { + const auto& instruction = program.instructions[index]; + std::uint32_t target = 0; + if (IsDirectBranchOpcode(instruction.op)) branches.emplace(instruction.branchTarget, index); + else if (instruction.op == RdnaOpcode::SSetpcB64 && resolveSetpcTarget(program, index, target)) branches.emplace(target, index); + else if (instruction.op == RdnaOpcode::SCallB64) callTargets.insert(instruction.branchTarget); + } + std::vector seen(program.instructions.size()); + std::vector pending; + const auto predecessors = [&](std::uint32_t index) { + if (index == 0u || callTargets.contains(program.instructions[index].programCounter)) return false; + const auto before = pending.size(); + const auto op = program.instructions[index - 1u].op; + if (op != RdnaOpcode::SBranch && op != RdnaOpcode::SEndpgm && op != RdnaOpcode::SCodeEnd && op != RdnaOpcode::SSetpcB64) pending.push_back(index - 1u); + const auto [first, last] = branches.equal_range(program.instructions[index].programCounter); + for (auto branch = first; branch != last; ++branch) pending.push_back(branch->second); + return pending.size() != before; }; + if (!predecessors(reader)) return false; + std::optional found; + while (!pending.empty()) { + const auto index = pending.back(); + pending.pop_back(); + if (seen[index]) continue; + seen[index] = true; + const auto& instruction = program.instructions[index]; + std::uint32_t target = 0; + if (instruction.op == RdnaOpcode::SCallB64 || (instruction.op == RdnaOpcode::SSwappcB64 && resolveSetpcTarget(program, index, target))) return false; + if (std::ranges::any_of(indices, [&](std::uint32_t reg) { return writesScalar(instruction, reg, waveSize); })) { + if (found.has_value() && *found != index) return false; + found = index; + continue; + } + if (!predecessors(index)) return false; + } + if (!found.has_value()) return false; + writer = *found; + return true; +} + +std::uint32_t branchWord(std::uint32_t opcode, std::size_t from, std::size_t to, const char* what) { + const auto offset = static_cast(to) - static_cast(from) - 1; + if (offset < -0x8000 || offset > 0x7fff) throw std::invalid_argument(std::string(what) + " lands out of the 16-bit branch range"); + return opcode | (static_cast(offset) & 0xffffu); +} + +} + + +std::vector FindUserDataCalls(const RdnaProgram& program, const SwappcInfo& swappc) { + const auto labels = branchLabels(program); + const auto userData = [&](std::uint32_t reg) { return reg >= swappc.userDataBaseRegister && reg - swappc.userDataBaseRegister < swappc.userDataCount; }; + const auto unchanged = [&](std::uint32_t reg, std::uint32_t reader) { return unchangedUntil(program, reg, reader, swappc.waveSize); }; std::vector calls; for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { const auto& instruction = program.instructions[index]; @@ -985,7 +1404,7 @@ std::vector FindUserDataCalls(const RdnaProgram& program, const Sw const auto source = scalarIndex(instruction.source0); if (link == NoScalarRegister || source == NoScalarRegister || source % 2u != 0u) continue; std::uint32_t pair = NoScalarRegister; - if (source != link && userData(source) && userData(source + 1u) && unchangedUntil(source, index) && unchangedUntil(source + 1u, index)) { + if ((source != link || GcnGuestShaders()) && userData(source) && userData(source + 1u) && unchanged(source, index) && unchanged(source + 1u, index)) { pair = source; } else { std::uint32_t low = 0, high = 0; @@ -1003,7 +1422,7 @@ std::vector FindUserDataCalls(const RdnaProgram& program, const Sw from = scalarIndex(lowWriter.source0); if (from != NoScalarRegister && !isScalar(highWriter.source0, from + 1u)) from = NoScalarRegister; } - if (!sameBlock || from == NoScalarRegister || from % 2u != 0u || !userData(from) || !userData(from + 1u) || !unchangedUntil(from, low) || !unchangedUntil(from + 1u, high)) continue; + if (!sameBlock || from == NoScalarRegister || from % 2u != 0u || !userData(from) || !userData(from + 1u) || !unchanged(from, low) || !unchanged(from + 1u, high)) continue; pair = from; } calls.push_back({instruction.programCounter, link, pair}); @@ -1016,19 +1435,28 @@ std::optional FindUserDataTail(const RdnaProgram& program, std::ui const auto& tail = program.instructions[program.instructions.size() - 2u]; if (tail.op != RdnaOpcode::SSetpcB64 || tail.source0.kind != RdnaOperandKind::ScalarRegister) return std::nullopt; const auto reg = tail.source0.reg; - if (reg % 2u != 0u || reg < userDataBase || userDataCount < 2u || reg - userDataBase >= userDataCount - 1u) return std::nullopt; + if (reg % 2u != 0u || reg < userDataBase || reg - userDataBase >= userDataCount) return std::nullopt; + bool highUnchanged = true; + std::optional constantHigh; for (const auto& instruction : program.instructions) { if (instruction.programCounter >= tail.programCounter) break; if (IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SCallB64 || instruction.op == RdnaOpcode::SSwappcB64 || instruction.op == RdnaOpcode::SSetpcB64 || instruction.op == RdnaOpcode::SEndpgm || instruction.op == RdnaOpcode::SCodeEnd || - writesScalar(instruction, reg) || writesScalar(instruction, reg + 1u)) return std::nullopt; + writesScalar(instruction, reg)) return std::nullopt; + if (writesScalar(instruction, reg + 1u)) { + highUnchanged = false; + std::uint32_t value; + if (instruction.op == RdnaOpcode::SMovB32 && isScalar(instruction.destination, reg + 1u) && isImmediate(instruction.source0, value)) constantHigh = value; + else constantHigh.reset(); + } } - return UserDataTail{tail.programCounter, reg}; + if (!constantHigh && (!highUnchanged || reg - userDataBase + 1u >= userDataCount)) return std::nullopt; + return UserDataTail{tail.programCounter, reg, constantHigh}; } std::vector LinkUserDataTail(const RdnaProgram& program, const UserDataTail& tail, const RdnaProgram& continuation) { const auto recognized = FindUserDataTail(program, tail.userDataRegister, 2u); - if (!recognized || recognized->programCounter != tail.programCounter) throw std::invalid_argument("user-data tail is not a linear jump through an unchanged register pair"); + if (!recognized || recognized->programCounter != tail.programCounter || recognized->constantHigh != tail.constantHigh) throw std::invalid_argument("user-data tail is not a linear jump through a proven user-data address"); for (const auto& instruction : continuation.instructions) { if (instruction.op == RdnaOpcode::SGetpcB64) throw std::invalid_argument("user-data tail continuation reads its program counter, which a linked copy does not keep"); } @@ -1068,32 +1496,147 @@ std::optional UserDataCalleeLength(std::span } std::vector LinkUserDataCalls(const RdnaProgram& program, std::span calls, std::span> callees) { - if (calls.size() != callees.size()) throw std::logic_error("user-data calls and callees differ in number"); - for (const auto& instruction : program.instructions) { - if (instruction.op == RdnaOpcode::SGetpcB64) { - throw std::invalid_argument("a program that reads its program counter (s_getpc_b64 at program counter " + toHexString(instruction.programCounter) + ") cannot have user-data callees linked after it"); + return LinkCalls(program, calls, callees, {}, {}).code; +} + +std::vector FindTableCalls(const RdnaProgram& program, const SwappcInfo& swappc) { + const auto labels = branchLabels(program); + const auto userData = [&](std::uint32_t reg) { return reg >= swappc.userDataBaseRegister && reg - swappc.userDataBaseRegister < swappc.userDataCount; }; + const auto sameBlock = [&](std::uint32_t first, std::uint32_t last) { + return std::none_of(labels.begin(), labels.end(), [&](std::uint32_t label) { + return label > program.instructions[first].programCounter && label <= program.instructions[last].programCounter; + }); + }; + std::vector calls; + for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { + const auto& instruction = program.instructions[index]; + if (instruction.op != RdnaOpcode::SSwappcB64) continue; + std::uint32_t ignored = 0; + if (resolveSetpcTarget(program, index, ignored)) continue; + const auto link = scalarIndex(instruction.destination); + const auto target = scalarIndex(instruction.source0); + if (link == NoScalarRegister || link % 2u != 0u || link > 104u || target == NoScalarRegister || target % 2u != 0u || target > 104u) continue; + std::uint32_t lowLoad = 0, lowDword = 0, highLoad = 0, highDword = 0; + const auto reader = instruction.programCounter; + if (!loadedDword(program, target, index, swappc.waveSize, reader, lowLoad, lowDword) || !loadedDword(program, target + 1u, index, swappc.waveSize, reader, highLoad, highDword)) continue; + if (lowLoad != highLoad || highDword != lowDword + 1u || !sameBlock(lowLoad, index)) continue; + const auto& load = program.instructions[lowLoad]; + const auto descriptor = scalarIndex(load.source0); + if (load.source0.kind != RdnaOperandKind::ScalarRegister || descriptor % 4u != 0u || load.memoryOffset >= 0x100000u || (load.memoryOffset & 3u) != 0u) continue; + std::uint32_t descriptorLoad = 0; + const bool sameBlockWriter = findLastWriter(program, lowLoad, {descriptor, descriptor + 1u, descriptor + 2u, descriptor + 3u}, descriptorLoad, swappc.waveSize, reader) && sameBlock(descriptorLoad, index); + if (!sameBlockWriter && !reachingWriter(program, lowLoad, {descriptor, descriptor + 1u, descriptor + 2u, descriptor + 3u}, descriptorLoad, swappc.waveSize)) continue; + const auto& table = program.instructions[descriptorLoad]; + const auto pointer = scalarIndex(table.source0); + if (table.op != RdnaOpcode::SLoadDwordx4 || !isScalar(table.destination, descriptor) || pointer == NoScalarRegister || pointer % 2u != 0u || table.source1.kind != RdnaOperandKind::Null || + table.memoryOffset >= 0x100000u || (table.memoryOffset & 3u) != 0u) continue; + std::optional constantPointer; + if (!(userData(pointer) && userData(pointer + 1u) && unchangedUntil(program, pointer, descriptorLoad, swappc.waveSize) && unchangedUntil(program, pointer + 1u, descriptorLoad, swappc.waveSize))) { + std::uint32_t lowWriter = 0, highWriter = 0, low = 0, high = 0; + if (!findLastWriter(program, descriptorLoad, {pointer}, lowWriter, swappc.waveSize, reader) || !findLastWriter(program, descriptorLoad, {pointer + 1u}, highWriter, swappc.waveSize, reader) || + !sameBlock(std::min(lowWriter, highWriter), descriptorLoad)) continue; + const auto& lowMove = program.instructions[lowWriter]; + const auto& highMove = program.instructions[highWriter]; + if (lowMove.op != RdnaOpcode::SMovB32 || highMove.op != RdnaOpcode::SMovB32 || !isScalar(lowMove.destination, pointer) || !isScalar(highMove.destination, pointer + 1u) || + !isImmediate(lowMove.source0, low) || !isImmediate(highMove.source0, high)) continue; + constantPointer = static_cast(high) << 32u | low; + } + std::uint32_t stride = 0; + if (load.source1.kind != RdnaOperandKind::Null) { + const auto offset = scalarIndex(load.source1); + std::uint32_t shift = 0, shiftIndex = 0; + if (offset == NoScalarRegister || !findLastWriter(program, lowLoad, {offset}, shiftIndex, swappc.waveSize, reader) || !sameBlock(shiftIndex, index)) continue; + const auto& shifted = program.instructions[shiftIndex]; + if (shifted.op != RdnaOpcode::SLshlB32 || !isScalar(shifted.destination, offset) || !isImmediate(shifted.source1, shift) || shift < 2u || shift > 24u) continue; + stride = 1u << shift; + } + calls.push_back({instruction.programCounter, link, target, constantPointer ? 0u : pointer, table.memoryOffset, load.memoryOffset + lowDword * 4u, stride, constantPointer}); + } + return calls; +} + +LinkedCalls LinkCalls(const RdnaProgram& program, std::span userDataCalls, std::span> userDataCallees, + std::span tableCalls, std::span> tableCallees) { + if (userDataCalls.size() != userDataCallees.size()) throw std::logic_error("user-data calls and callees differ in number"); + if (tableCalls.size() != tableCallees.size()) throw std::logic_error("table calls and callees differ in number"); + for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { + BoundedJumpTable table; + if (program.instructions[index].op == RdnaOpcode::SSetpcB64 && resolveBoundedJumpTable(program, index, table)) { + throw std::invalid_argument("a program that jumps through a table in its code (s_setpc_b64 at program counter " + toHexString(program.instructions[index].programCounter) + ") cannot have callees linked over the words after its last instruction"); } } const auto end = instructionEndProgramCounter(program.instructions.back()) / 4u; std::vector linked(program.code.begin(), program.code.begin() + end); - for (std::size_t call = 0; call < calls.size(); ++call) { - const auto index = instructionIndexOfProgramCounter(program, calls[call].programCounter); + std::vector fallbacks; + const auto callWord = [&](std::uint32_t programCounter, const char* kind) { + const auto index = instructionIndexOfProgramCounter(program, programCounter); if (index == InvalidControlFlowId || program.instructions[index].op != RdnaOpcode::SSwappcB64 || program.instructions[index].wordCount != 1u) { - throw std::logic_error("user-data call does not name an s_swappc_b64"); + throw std::logic_error(std::string(kind) + " call does not name an s_swappc_b64"); } - const auto word = calls[call].programCounter / 4u; + return programCounter / 4u; + }; + for (std::size_t call = 0; call < userDataCalls.size(); ++call) { + const auto word = callWord(userDataCalls[call].programCounter, "user-data"); const auto offset = static_cast(linked.size()) - static_cast(word) - 1; - if (offset > 0x7fff) throw std::invalid_argument("user-data callee of the call at program counter " + toHexString(calls[call].programCounter) + " lands out of s_call_b64 range"); - linked[word] = 0xbb000000u | (calls[call].linkRegister << 16u) | static_cast(offset); - linked.insert(linked.end(), callees[call].begin(), callees[call].end()); + if (offset > 0x7fff) throw std::invalid_argument("user-data callee of the call at program counter " + toHexString(userDataCalls[call].programCounter) + " lands out of s_call_b64 range"); + linked[word] = 0xbb000000u | (userDataCalls[call].linkRegister << 16u) | static_cast(offset); + linked.insert(linked.end(), userDataCallees[call].begin(), userDataCallees[call].end()); + } + for (std::size_t call = 0; call < tableCalls.size(); ++call) { + const auto& table = tableCalls[call]; + const auto& callees = tableCallees[call]; + const auto word = callWord(table.programCounter, "table"); + const auto at = "the table callees of the call at program counter " + toHexString(table.programCounter); + const auto original = linked[word]; + const auto count = callees.size(); + const auto dispatch = linked.size(); + const auto zero = dispatch + 1u; + const auto one = zero + count * 7u + 2u; + const auto calls = one + count * 8u + 2u; + const auto fault = calls + count * 2u; + const auto branch = [&](std::uint32_t opcode, std::size_t target) { linked.push_back(branchWord(opcode, linked.size(), target, at.c_str())); }; + linked[word] = branchWord(0xbf820000u, word, dispatch, at.c_str()); + branch(0xbf850000u, one); + for (const bool sccOne : {false, true}) { + for (std::size_t callee = 0; callee < count; ++callee) { + const auto next = linked.size() + (sccOne ? 8u : 7u); + linked.insert(linked.end(), {0xbf07ff00u | table.targetRegister, static_cast(callees[callee].target)}); + branch(0xbf850000u, next); + linked.insert(linked.end(), {0xbf07ff00u | (table.targetRegister + 1u), static_cast(callees[callee].target >> 32u)}); + branch(0xbf850000u, next); + if (sccOne) linked.push_back(0xbf068080u); + branch(0xbf820000u, calls + callee * 2u); + } + linked.push_back(sccOne ? 0xbf068080u : 0xbf078080u); + branch(0xbf820000u, fault); + } + for (std::size_t callee = 0; callee < count; ++callee) { + linked.push_back(0u); + branch(0xbf820000u, word + 1u); + } + fallbacks.push_back(static_cast(linked.size() * 4u)); + linked.push_back(original); + branch(0xbf820000u, word + 1u); + for (std::size_t callee = 0; callee < count; ++callee) { + const auto site = calls + callee * 2u; + const auto offset = static_cast(linked.size()) - static_cast(site) - 1; + if (offset > 0x7fff) throw std::invalid_argument(at + " land out of s_call_b64 range"); + linked[site] = 0xbb000000u | (table.linkRegister << 16u) | static_cast(offset); + linked.insert(linked.end(), callees[callee].code.begin(), callees[callee].code.end()); + } } linked.push_back(0xbf9f0000u); - return linked; + return {std::move(linked), std::move(fallbacks)}; } ControlFlowGraph GraphBuilder::Build(const RdnaProgram& program, const SwappcInfo* swappc) const { - const std::vector calls = analyzeSwappcCalls(program, swappc); + std::vector unlinked; + LinkEscape linkEscape; + const std::vector calls = analyzeSwappcCalls(program, swappc, unlinked, linkEscape); ControlFlowGraph graph; + for (const auto index : unlinked) graph.unlinkedCallProgramCounters.push_back(program.instructions[index].programCounter); + graph.linkEscapeWaveSize = linkEscape.waveSize; + graph.linkEscapeReason = std::move(linkEscape.reason); graph.blocks = splitIntoBlocks(program, calls); linkBlocks(graph.blocks, program, calls); for (const auto& call : calls) { diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 93b1024051..cf3880a037 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -1,4 +1,5 @@ #include "ControlFlow/RequestSerializer.hpp" +#include #include namespace ShaderRecompiler { @@ -239,7 +240,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); @@ -253,6 +254,14 @@ RasterPrimitive readRasterPrimitive(Reader& reader) { return static_cast(value); } +PixelPositionLocation readPixelPositionLocation(Reader& reader) { + const auto value = reader.ReadU8(); + if (value > static_cast(PixelPositionLocation::UpperLeft)) { + throw std::runtime_error("RequestSerializer: invalid PixelPositionLocation value"); + } + return static_cast(value); +} + void writeShaderBinary(Writer& writer, const ShaderBinary& binary) { writer.WriteU8(static_cast(binary.stage)); writer.WriteU64(binary.codeAddress); @@ -339,6 +348,15 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteU8(static_cast(info.rasterPrimitive)); writer.WriteBool(info.layerExported); writer.WriteBool(info.ditheredAlphaToCoverage); + writer.WriteBool(info.frontFaceAllBits); + writer.WriteU8(info.quadPixelMask); + for (const std::uint16_t mask : info.quadSampleMask) writer.WriteU32(mask); + writer.WriteU32(info.reversedBlend); + writer.WriteBool(info.iteratesSamples); + writer.WriteU8(static_cast(info.positionLocation)); + for (const std::uint8_t location : info.sampleLocations) writer.WriteU8(location); + writer.WriteBool(info.stencilOperationValueExport); + for (const std::uint32_t order : info.sampleOrder) writer.WriteU32(order); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -391,6 +409,25 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { if (version >= 19u) info.rasterPrimitive = readRasterPrimitive(reader); if (version >= 23u) info.layerExported = reader.ReadBool(); if (version >= 25u) info.ditheredAlphaToCoverage = reader.ReadBool(); + if (version >= 27u) info.frontFaceAllBits = reader.ReadBool(); + if (version >= 28u) info.quadPixelMask = reader.ReadU8(); + if (version >= 29u) { + for (std::uint16_t& mask : info.quadSampleMask) { + const auto value = reader.ReadU32(); + if (value > 0xffffu) throw std::runtime_error("invalid recompile request quad sample mask"); + mask = static_cast(value); + } + } + if (version >= 30u) info.reversedBlend = reader.ReadU32(); + if (version >= 34u) { + info.iteratesSamples = reader.ReadBool(); + info.positionLocation = readPixelPositionLocation(reader); + for (std::uint8_t& location : info.sampleLocations) location = reader.ReadU8(); + } + if (version >= 37u) info.stencilOperationValueExport = reader.ReadBool(); + if (version >= 42u) { + for (std::uint32_t& order : info.sampleOrder) order = reader.ReadU32(); + } if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; info.inputAddr = input(PixelInput::PerspectiveSample, info.hasPerspectiveCenterVgpr && inputAddrOrCenterVgpr == 2u) | input(PixelInput::PerspectiveCenter, info.hasPerspectiveCenterVgpr) | @@ -504,6 +541,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) { @@ -517,6 +555,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; } @@ -529,6 +568,7 @@ void writeTessellationConfiguration(Writer& writer, const TessellationConfigurat writer.WriteU32(configuration.patchesPerGroup); writer.WriteU32(configuration.primitiveGroupSize); writer.WriteBool(configuration.breakAtInstance); + writer.WriteBool(configuration.mergedHull); } TessellationConfiguration readTessellationConfiguration(Reader& reader, std::uint32_t version) { @@ -543,6 +583,7 @@ TessellationConfiguration readTessellationConfiguration(Reader& reader, std::uin configuration.primitiveGroupSize = reader.ReadU32(); configuration.breakAtInstance = reader.ReadBool(); } + if (version >= 39u) configuration.mergedHull = reader.ReadBool(); return configuration; } @@ -715,6 +756,19 @@ void writeGraphicsCompileContext(Writer& writer, const GraphicsCompileContext& g writer.WriteU32(graphics.draw.indexCount); writer.WriteU32(graphics.draw.indexElementBytes); writer.WriteU32(graphics.draw.instanceCount); + writer.WriteBool(graphics.meshBarycentrics.has_value()); + if (graphics.meshBarycentrics.has_value()) { + const auto& barycentrics = *graphics.meshBarycentrics; + writer.WriteBool(barycentrics.emulation.active); + writer.WriteBool(barycentrics.emulation.smooth); + writer.WriteBool(barycentrics.emulation.linear); + writer.WriteU64(barycentrics.parameters.size()); + for (const auto& parameter : barycentrics.parameters) { + writer.WriteU32(parameter.location); + writer.WriteU32(parameter.sourceLocation); + writer.WriteBool(parameter.perVertex); + } + } } GraphicsCompileContext readGraphicsCompileContext(Reader& reader, DeserializedGraphicsCompileContext& storage, std::uint32_t version) { @@ -753,6 +807,22 @@ GraphicsCompileContext readGraphicsCompileContext(Reader& reader, DeserializedGr storage.draw.indexElementBytes = reader.ReadU32(); storage.draw.instanceCount = reader.ReadU32(); graphics.draw = storage.draw; + if (version >= 38u && reader.ReadBool()) { + MeshBarycentricLayout barycentrics; + barycentrics.emulation.active = reader.ReadBool(); + barycentrics.emulation.smooth = reader.ReadBool(); + barycentrics.emulation.linear = reader.ReadBool(); + const auto count = reader.ReadU64(); + reader.Require(count, 9u); + for (std::uint64_t i = 0; i < count; i++) { + FragmentParameter parameter{}; + parameter.location = reader.ReadU32(); + parameter.sourceLocation = reader.ReadU32(); + parameter.perVertex = reader.ReadBool(); + barycentrics.parameters.push_back(parameter); + } + graphics.meshBarycentrics = std::move(barycentrics); + } return graphics; } @@ -762,7 +832,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(25u); + writer.WriteU32(43u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -786,6 +856,20 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteU32(request.target.registrationWordLimit); writer.WriteBool(request.target.vertexSubgroups); writer.WriteBool(request.target.float16DenormPreserve); + writer.WriteU32Span(request.context.linkedCallFallbacks); + writer.WriteBool(request.target.spannedWaves); + writer.WriteBool(request.target.depthRangeRemap); + if (request.context.vertex.has_value()) { + writer.WriteU32(request.context.vertex->depthRemapMode); + } + writer.WriteBool(request.target.float32FlushToZero); + writer.WriteBool(request.target.float32DenormPreserve); + if (request.context.vertex.has_value()) { + writer.WriteBool(request.context.vertex->pointList); + writer.WriteU32(std::bit_cast(request.context.vertex->pointSize)); + writer.WriteU32(std::bit_cast(request.context.vertex->pointSizeMin)); + writer.WriteU32(std::bit_cast(request.context.vertex->pointSizeMax)); + } return base64Encode(buffer); } @@ -794,7 +878,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 > 25u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 43u) 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); @@ -820,14 +904,35 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const if (version >= 18u) result.request.target.vertexSubgroups = reader.ReadBool(); if (version >= 21u) { if (version >= 22u) reader.Require(1u); - if (reader.Remaining() == 1u) { + if (reader.Remaining() != 0u) { const auto float16DenormPreserve = reader.ReadU8(); if (float16DenormPreserve > 1u) throw std::runtime_error("RequestSerializer: invalid float16DenormPreserve value"); result.request.target.float16DenormPreserve = float16DenormPreserve != 0u; - } else if (reader.Remaining() != 0u) { - throw std::runtime_error("RequestSerializer: unexpected trailing data"); } } + if (version >= 26u) { + result.linkedCallFallbacks = reader.ReadU32Vector(); + result.request.context.linkedCallFallbacks = result.linkedCallFallbacks; + } + if (version >= 32u) result.request.target.spannedWaves = reader.ReadBool(); + if (version >= 35u) { + result.request.target.depthRangeRemap = reader.ReadBool(); + if (result.vertex.has_value()) { + result.vertex->depthRemapMode = reader.ReadU32(); + result.request.context.vertex = result.vertex; + } + } + if (version >= 36u) result.request.target.float32FlushToZero = reader.ReadBool(); + if (version >= 41u) result.request.target.float32DenormPreserve = reader.ReadBool(); + if (version >= 43u && result.request.context.vertex.has_value()) { + auto& vertex = *result.request.context.vertex; + vertex.pointList = reader.ReadBool(); + vertex.pointSize = std::bit_cast(reader.ReadU32()); + vertex.pointSizeMin = std::bit_cast(reader.ReadU32()); + vertex.pointSizeMax = std::bit_cast(reader.ReadU32()); + result.vertex = vertex; + } + 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..3267461138 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; + } } } } @@ -441,41 +530,70 @@ std::uint32_t mergeBesideReturns(const ControlFlowGraph& graph, const BasicBlock } } const auto count = static_cast(graph.blocks.size()); - const auto returns = [&](std::uint32_t blockId) { - if (!graph.Dominates(header.id, blockId) || !hasLinearPathToTerminal(graph, blockId)) { - return false; + enum class Returning : std::uint8_t { Unknown, Visiting, No, Yes }; + std::vector returning(count, Returning::Unknown); + const auto returns = [&](std::uint32_t start) { + std::vector pending{start}; + while (!pending.empty()) { + const auto blockId = pending.back(); + const auto& block = graph.FindBlock(blockId); + if (returning[blockId] == Returning::Unknown) { + if (block.predecessors.size() == 1u && graph.Dominates(header.id, blockId)) { + returning[blockId] = Returning::Visiting; + std::copy_if(block.successors.begin(), block.successors.end(), std::back_inserter(pending), [&](std::uint32_t successor) { return returning[successor] == Returning::Unknown; }); + continue; + } + returning[blockId] = Returning::No; + } else if (returning[blockId] == Returning::Visiting) { + const bool privateReturn = std::all_of(block.successors.begin(), block.successors.end(), [&](std::uint32_t successor) { return returning[successor] == Returning::Yes; }); + returning[blockId] = privateReturn ? Returning::Yes : Returning::No; + } + pending.pop_back(); } - const auto tail = linearTail(graph, blockId); - return std::all_of(tail.begin(), tail.end(), [&](std::uint32_t member) { return graph.FindBlock(member).predecessors.size() == 1u; }); + return returning[start] == Returning::Yes; }; 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 +650,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 +2143,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 +2227,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 +2261,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/Deserialize.cpp b/core/shader/recompiler/Deserialize.cpp index 6f1b3a0ca9..5bc2aec696 100644 --- a/core/shader/recompiler/Deserialize.cpp +++ b/core/shader/recompiler/Deserialize.cpp @@ -165,6 +165,7 @@ int main(int argc, char** argv) { translateOptions.stage = stageKind; translateOptions.waveSize = request.context.waveSize; translateOptions.userDataBaseRegister = request.context.userDataBaseRegister; + translateOptions.gcnLegacyVertexInputs = GcnGuestShaders() && request.context.userDataBaseRegister == 0u; translateOptions.userDataCount = static_cast(request.context.userData.size()); translateOptions.scratchDwords = request.context.compute.has_value() ? request.context.compute->scratchDwords : 0u; translateOptions.sharedMemoryBytes = request.context.compute.has_value() && static_cast(request.context.compute->ldsSizeDwords) * 4u + 4u > request.target.maxWorkgroupSharedMemoryBytes ? request.context.compute->ldsSizeDwords * 4u : 0u; diff --git a/core/shader/recompiler/FragmentOutputSpecialization.hpp b/core/shader/recompiler/FragmentOutputSpecialization.hpp index 8bd331e9a3..00894c6af1 100644 --- a/core/shader/recompiler/FragmentOutputSpecialization.hpp +++ b/core/shader/recompiler/FragmentOutputSpecialization.hpp @@ -3,6 +3,8 @@ #include "PipelineSpecialization.hpp" #include "Recompiler.hpp" +#include +#include #include #include #include @@ -15,18 +17,25 @@ namespace ShaderRecompiler { inline std::vector SpecializeFragmentOutputs(std::vector words, std::span constants, const std::set& specializedIds) { + namespace Code = ReversedBlendCode; std::set packed; bool dualSource = false; + std::uint32_t reversed = 0; for (const auto& constant : constants) { if (constant.id == PipelineSpecialization::DualSourceBlend) { if (constant.value > 1u) throw std::runtime_error("invalid prepared dual-source blend flag"); dualSource = constant.value != 0u; } + if (constant.id == PipelineSpecialization::ReversedBlend) { + const auto known = Code::Enable | (Code::Routes << Code::ColorShift) | (Code::Routes << Code::SecondShift); + if ((constant.value & ~known) != 0u || (constant.value != 0u && (constant.value & Code::Enable) == 0u)) throw std::runtime_error("invalid prepared reversed-order blend"); + reversed = constant.value; + } if (constant.id < PipelineSpecialization::ExportPackingBase || constant.id >= 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) return words; + if (packed.empty() && !dualSource && reversed == 0u) return words; if (words.size() < 5u || words[0] != spv::MagicNumber) throw std::runtime_error("invalid fragment output specialization module"); std::map locations; std::set indexed; @@ -72,14 +81,27 @@ inline std::vector SpecializeFragmentOutputs(std::vector::max()) throw std::runtime_error("fragment output specialization module ID overflow"); + return bound++; + }; std::vector> declarations; const auto declare = [&](std::vector declaration) { const auto found = types.find(declaration); @@ -95,8 +117,22 @@ inline std::vector SpecializeFragmentOutputs(std::vector result(words.begin(), words.begin() + 5); std::vector> variables; + if (blended != 0u) { + declarations.push_back({(4u << 16u) | spv::OpConstant, floatType, zero, 0u}); + declarations.push_back({(4u << 16u) | spv::OpConstant, floatType, one, 0x3f800000u}); + variables.push_back({(4u << 16u) | spv::OpVariable, outputs.at(blended), secondColor, spv::StorageClassOutput}); + } + const auto emit = [&](spv::Op opcode, std::initializer_list operands) { + result.push_back((static_cast(operands.size() + 1u) << 16u) | opcode); + result.insert(result.end(), operands); + }; bool inserted = false; for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; @@ -115,6 +151,19 @@ inline std::vector SpecializeFragmentOutputs(std::vector SpecializeFragmentOutputs(std::vector= 3u && blended != 0u && operand(1u) == blended) { + std::array source{}; + for (std::uint32_t component = 0; component < source.size(); ++component) { + source[component] = allocate(); + emit(spv::OpCompositeExtract, {floatType, source[component], operand(2u), component}); + } + const auto routed = [&](std::uint32_t shift) { + std::array picked{}; + for (std::uint32_t component = 0; component < picked.size(); ++component) { + switch ((reversed >> (shift + Code::RouteBits * component)) & Code::RouteMask) { + case Code::Own: picked[component] = source[component]; break; + case Code::Component0: picked[component] = source[0]; break; + case Code::Zero: picked[component] = zero; break; + default: picked[component] = one; break; + } + } + const auto value = allocate(); + emit(spv::OpCompositeConstruct, {colorType, value, picked[0], picked[1], picked[2], picked[3]}); + return value; + }; + emit(spv::OpStore, {secondColor, routed(Code::SecondShift)}); + const auto stored = ((reversed >> Code::ColorShift) & Code::Routes) == 0u ? operand(2u) : routed(Code::ColorShift); + result.insert(result.end(), words.begin() + cursor, words.begin() + cursor + count); + result[result.size() - count + 2u] = stored; + cursor += count; + continue; + } + const auto pointerUse = [&](auto&& matches) { + return ((op == spv::OpLoad || op == spv::OpAccessChain || op == spv::OpInBoundsAccessChain || op == spv::OpPtrAccessChain || op == spv::OpInBoundsPtrAccessChain || op == spv::OpCopyObject) && matches(operand(3u))) || + ((op == spv::OpCopyMemory || op == spv::OpCopyMemorySized) && (matches(operand(1u)) || matches(operand(2u)))); + }; + if (pointerUse([&](std::uint32_t id) { return retyped.contains(id); })) throw std::runtime_error("a packed fragment output is accessed other than by whole stores"); + if (blended != 0u && pointerUse([&](std::uint32_t id) { return id == blended; })) throw std::runtime_error("a reversed-order blended fragment output is accessed other than by whole stores"); result.insert(result.end(), words.begin() + cursor, words.begin() + cursor + count); cursor += count; } diff --git a/core/shader/recompiler/ImageTableAbi.hpp b/core/shader/recompiler/ImageTableAbi.hpp new file mode 100644 index 0000000000..9b434141d8 --- /dev/null +++ b/core/shader/recompiler/ImageTableAbi.hpp @@ -0,0 +1,155 @@ +#ifndef CORE_SHADER_RECOMPILER_IMAGETABLEABI_HPP +#define CORE_SHADER_RECOMPILER_IMAGETABLEABI_HPP + +#include +#include +#include + +namespace ShaderRecompiler::ImageTableAbi { + +inline constexpr std::uint32_t Version = 2; + +inline constexpr std::uint32_t HeaderVersion = 0; +inline constexpr std::uint32_t HeaderElementBase = 1; +inline constexpr std::uint32_t HeaderElementCount = 2; +inline constexpr std::uint32_t HeaderSamplerCount = 3; +inline constexpr std::uint32_t HeaderPoisonCount = 4; +inline constexpr std::uint32_t HeaderElementFaults = 5; +inline constexpr std::uint32_t HeaderSamplerFaults = 6; +inline constexpr std::uint32_t HeaderTableCount = 7; +inline constexpr std::uint32_t HeaderWords = 8; + +inline constexpr std::uint32_t TableMapStart = 0; +inline constexpr std::uint32_t TableKeyCount = 1; +inline constexpr std::uint32_t TableSizeLow = 2; +inline constexpr std::uint32_t TableSizeHigh = 3; +inline constexpr std::uint32_t TableBaseLow = 4; +inline constexpr std::uint32_t TableBaseHigh = 5; +inline constexpr std::uint32_t TableOutsideCode = 6; +inline constexpr std::uint32_t TableFault = 7; +inline constexpr std::uint32_t TableWords = 8; + +inline constexpr std::uint32_t ImageRecordFlags = 0; +inline constexpr std::uint32_t ImageRecordDword3 = 1; +inline constexpr std::uint32_t ImageRecordElement = 2; +inline constexpr std::uint32_t ImageRecordWords = 3; + +inline constexpr std::uint32_t SamplerRecordFlags = 0; +inline constexpr std::uint32_t SamplerRecordElement = 1; +inline constexpr std::uint32_t SamplerRecordWords = 2; +inline constexpr std::uint32_t SamplerEntryElements = 4; + +inline constexpr std::uint32_t NullCode = 0; +inline constexpr std::uint32_t PoisonFlag = 0x80000000u; +inline constexpr std::uint32_t ModeShift = 1; +inline constexpr std::uint32_t ModeMask = 0xffu; + +inline constexpr std::uint32_t OutsideSnapshotPoison = 0; +inline constexpr std::uint32_t OutsideDomainPoison = 1; +inline constexpr std::uint32_t FirstEntryPoison = 2; + +inline constexpr std::uint32_t MaxKeys = 65536; +inline constexpr std::uint32_t MaxModes = 256; +inline constexpr std::uint32_t ImageElementBudget = 65536; +inline constexpr std::uint32_t SamplerElementBudget = 1024; + +enum class PoisonReason : std::uint32_t { + OutsideSnapshot, + OutsideDomain, + Unmapped, + Inactive, + NotBuffer, + NotImage, + InvalidFormat, + Dimension, + Multisampled, + Fmask, + ColorCompare, + DepthBits16, + Conversion, + SrgbUnsupported, + NumericClass, + Budget, + Reduction, + Unnormalized, + HeapWritten, + DriverRejected, + UnsupportedOperation, + OffsetCarry, + Count +}; + +constexpr std::string_view PoisonReasonName(PoisonReason reason) { + switch (reason) { + case PoisonReason::OutsideSnapshot: return "outside the snapshot"; + case PoisonReason::OutsideDomain: return "outside the key domain"; + case PoisonReason::Unmapped: return "unmapped"; + case PoisonReason::Inactive: return "inactive source"; + case PoisonReason::NotBuffer: return "table V# is not a buffer"; + case PoisonReason::NotImage: return "not an image"; + case PoisonReason::InvalidFormat: return "invalid format"; + case PoisonReason::Dimension: return "dimension"; + case PoisonReason::Multisampled: return "multisampled"; + case PoisonReason::Fmask: return "FMASK"; + case PoisonReason::ColorCompare: return "color texture under comparison"; + case PoisonReason::DepthBits16: return "depth bits with 16-bit results"; + case PoisonReason::Conversion: return "format conversion"; + case PoisonReason::SrgbUnsupported: return "sRGB decode"; + case PoisonReason::NumericClass: return "numeric class"; + case PoisonReason::Budget: return "over budget"; + case PoisonReason::Reduction: return "min or max reduction"; + case PoisonReason::Unnormalized: return "unnormalized coordinates"; + case PoisonReason::HeapWritten: return "table written by its dispatch"; + case PoisonReason::DriverRejected: return "rejected by the driver"; + case PoisonReason::UnsupportedOperation: return "unsupported operation"; + case PoisonReason::OffsetCarry: return "record offset carries past 32 bits"; + case PoisonReason::Count: break; + } + return "unknown"; +} + +constexpr std::uint32_t PoisonCode(std::uint32_t poison) { + return PoisonFlag | poison; +} + +constexpr std::uint32_t TableHeader(std::uint32_t table) { + return HeaderWords + TableWords * table; +} + +constexpr std::optional ScalarBufferDword(std::uint32_t offset, std::uint32_t immediate, std::uint64_t size) { + const std::uint64_t address = static_cast(offset) + immediate; + if (address > 0xffffffffull) return std::nullopt; + const std::uint32_t dword = static_cast(address) & ~3u; + if (static_cast(dword) + 4u > size) return std::nullopt; + return dword; +} + +constexpr std::optional ScalarAddressDword(std::uint64_t base, std::uint32_t offset, std::uint32_t immediate) { + const std::uint64_t first = base + (offset & ~3u); + if (first < base) return std::nullopt; + const std::int64_t delta = static_cast(immediate & ~3u); + const std::uint64_t magnitude = delta < 0 ? static_cast(-delta) : static_cast(delta); + if (delta < 0) return magnitude > first ? std::nullopt : std::optional(first - magnitude); + return first + magnitude < first ? std::nullopt : std::optional(first + magnitude); +} + +constexpr std::uint32_t ScalarAddressRecords(std::uint32_t stride) { + return stride <= 1u ? 0xffffffffu * stride : 0xffffffffu / stride + 1u; +} + +constexpr std::uint32_t KeyRecords(std::uint32_t maxKey, std::uint32_t stride) { + return maxKey != 0xffffffffu && static_cast(maxKey) * stride <= 0xffffffffull ? maxKey + 1u : 0xffffffffu; +} + +constexpr std::uint32_t ScalarBufferRecords(std::uint32_t addend, std::uint32_t stride, std::uint32_t immediate, std::uint64_t size) { + const std::uint64_t start = static_cast(addend) + immediate; + const std::uint64_t rounded = size & ~std::uint64_t{3u}; + const std::uint64_t limit = rounded < 0x100000000ull ? rounded : 0x100000000ull; + if (stride == 0u || start >= limit) return 0u; + const std::uint64_t records = (limit - start + stride - 1u) / stride; + return records > 0xffffffffull ? 0xffffffffu : static_cast(records); +} + +} + +#endif diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp index f9df378d48..32f615dfcb 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp @@ -19,26 +19,51 @@ struct BlockInfo { IrValue* indirectTarget = nullptr; }; +struct KeyDomain { + std::uint32_t source = 0; + std::uint32_t offset = 0; + std::uint32_t stride = 4; + bool value = false; + + bool operator==(const KeyDomain& other) const = default; +}; + +struct PointerHop { + std::uint32_t offset = 0; + std::uint32_t immediate = 0; + + bool operator==(const PointerHop& other) const = default; +}; + +struct TableColumn { + std::uint32_t heapSource = 0; + std::uint32_t stride = 0; + std::uint32_t addend = 0; + std::uint32_t offset = 0; + std::uint32_t dwordCount = 0; + std::uint32_t maxKey = 0xffffffffu; + bool sampler = false; + bool address = false; + std::vector keyDomains; + bool unkeyed = false; + bool indirect = false; + std::uint32_t indirectOffset = 0; + std::uint32_t indirectImmediate = 0; + std::vector pointerHops; + std::uint32_t foldedAddend = 0; + std::uint32_t foldedCarry = 0; + bool selected = false; + std::uint32_t selection = 0; + + bool operator==(const TableColumn& other) const { + return heapSource == other.heapSource && stride == other.stride && addend == other.addend && offset == other.offset && dwordCount == other.dwordCount && sampler == other.sampler && address == other.address && indirect == other.indirect && indirectOffset == other.indirectOffset && indirectImmediate == other.indirectImmediate && pointerHops == other.pointerHops && foldedAddend == other.foldedAddend && foldedCarry == other.foldedCarry && selected == other.selected && selection == other.selection; + } +}; + struct DescriptorSource { - // A T# loaded from a table buffer (`heapSource`) at `entryOffset + key * 32`, the key a - // wave-uniform runtime value (handle argument `keyArg`). With `hasMaterial` the key is itself - // `M[readfirstlane(i) * selectorStride + selectorOffset]` over `materialSource`, so the key - // set can be enumerated from the material records (see ResourceMaterializer). - struct IndirectImage { - std::uint32_t materialSource = 0; - std::uint32_t heapSource = 0; - std::uint32_t selectorStride = 0; - std::uint32_t selectorOffset = 0; - std::uint32_t keyArg = 0; - std::uint32_t entryOffset = 0; - bool hasMaterial = false; - - bool operator==(const IndirectImage& other) const = default; - }; - std::array dwords {}; std::uint32_t dwordCount = 0; - std::optional indirectImage; + std::optional tableColumn; bool operator==(const DescriptorSource& other) const = default; }; @@ -63,6 +88,7 @@ struct ResourceBlock { std::vector successors; std::vector sources; std::vector images; + std::vector flatSlots; }; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp index 976a74f8e3..809c3e0932 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp @@ -39,6 +39,7 @@ struct IrDescriptorBinding { struct IrBindingLayout { std::uint32_t pushDataStartDword = PushData::NoStart; + std::uint32_t stencilReferenceDword = PushData::NoStart; std::uint32_t memoryOffsetDword = 0; std::uint32_t memoryOffsetCount = 0; bool dispatchThreadLimit = false; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp index 3a5e28dd46..ae85d35ec0 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp @@ -19,6 +19,8 @@ struct IrProgramMetadata { std::vector exportInfo; std::uint32_t pixelLinearInputs = 0; std::uint32_t pixelPerspectiveInputs = 0; + std::uint32_t pixelVertexOrderInputs = 0; + bool pixelIteratesSamples = false; bool barycentricEmulation = false; bool invalidatesCaches = false; std::vector dynamicReads; @@ -31,6 +33,7 @@ struct IrProgramMetadata { bool vectorWindowIndexedWrites = false; std::uint32_t tessellationControlStride = 0; std::uint32_t tessellationPatchStride = 0; + std::uint32_t tessellationSharedStride = 0; }; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp index 7c8d5d26d1..e340873ac7 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp @@ -51,6 +51,7 @@ struct MemoryInfo { bool coherent = false; bool gpuDescriptor = false; bool flushDenormals = false; + bool flushDenormalResults = false; bool operator==(const MemoryInfo& other) const = default; }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp index cb9565fb4f..08b2529bc0 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp @@ -9,8 +9,17 @@ #include "IntermediateRepresentation/IrMetadata/ShaderInfo.hpp" #include "IntermediateRepresentation/IrMetadata/ShaderStage.hpp" #include +#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include namespace ShaderRecompiler { @@ -18,10 +27,69 @@ namespace ShaderRecompiler { struct DescriptorValue { std::array dwords = {}; std::uint32_t dwordCount = 0; + std::uint8_t availableDwordMask = 0; bool operator==(const DescriptorValue& other) const = default; }; +template +class SharedValues { +public: + SharedValues() = default; + explicit SharedValues(std::vector values) : block(values.empty() ? nullptr : std::make_shared(std::move(values))) {} + + [[nodiscard]] std::size_t size() const { return block != nullptr ? block->values.size() : 0u; } + [[nodiscard]] bool empty() const { return block == nullptr; } + [[nodiscard]] const T* data() const { return block != nullptr ? block->values.data() : nullptr; } + [[nodiscard]] const T* begin() const { return data(); } + [[nodiscard]] const T* end() const { return data() + size(); } + [[nodiscard]] const T& operator[](std::size_t index) const { return block->values[index]; } + [[nodiscard]] const T& at(std::size_t index) const { + if (index >= size()) throw std::out_of_range("SharedValues::at"); + return block->values[index]; + } + [[nodiscard]] const T& front() const { return at(0); } + [[nodiscard]] const T& back() const { return at(size() - 1u); } + [[nodiscard]] std::span Span() const { return {data(), size()}; } + [[nodiscard]] std::vector Copy() const { return {begin(), end()}; } + [[nodiscard]] bool Shares(const SharedValues& other) const { return block == other.block; } + + [[nodiscard]] std::uint64_t Hash() const { + constexpr std::uint64_t Basis = 0xcbf29ce484222325ull; + if (block == nullptr) return Basis; + if (const auto known = block->hash.load(std::memory_order_relaxed); known != 0u) return known; + std::uint64_t hash = Basis; + const auto mix = [&](std::uint64_t value) { hash = (hash ^ value) * 0x100000001b3ull; }; + mix(block->values.size()); + for (const auto& value : block->values) { + if constexpr (std::is_same_v) { + mix(value.dwordCount); + mix(value.availableDwordMask); + for (const auto dword : value.dwords) mix(dword); + } else if constexpr (std::is_integral_v) { + mix(value); + } else { + mix(value.first); + mix(value.second); + } + } + hash = hash != 0u ? hash : 1u; + block->hash.store(hash, std::memory_order_relaxed); + return hash; + } + + friend bool operator==(const SharedValues& left, const SharedValues& right) { return left.block == right.block || std::ranges::equal(left.Span(), right.Span()); } + friend bool operator==(const SharedValues& left, const std::vector& right) { return std::ranges::equal(left.Span(), right); } + +private: + struct Block { + explicit Block(std::vector values) : values(std::move(values)) {} + std::vector values; + mutable std::atomic hash{0}; + }; + std::shared_ptr block; +}; + enum class UniformFillKind { None, Buffer, Image }; struct UniformFill { @@ -34,6 +102,28 @@ struct UniformFill { bool operator==(const UniformFill& other) const = default; }; +struct ImageTableColumnSnapshot { + std::uint64_t base = 0; + std::uint64_t size = 0; + std::uint32_t records = 0; + std::uint32_t keys = 0; + std::uint32_t fault = 0; + bool outside = false; + SharedValues codes; + SharedValues words; + SharedValues> readRanges; + + bool operator==(const ImageTableColumnSnapshot& other) const = default; +}; + +struct ImageTableSnapshot { + std::vector tables; + std::vector> ranges; + std::uint64_t shader = 0; + + bool operator==(const ImageTableSnapshot& other) const = default; +}; + struct ResourceSnapshot { std::vector buffers; std::vector images; @@ -43,6 +133,7 @@ struct ResourceSnapshot { UniformFill uniformFill; std::vector srtPoison; std::uint32_t nullRootReads = 0; + ImageTableSnapshot tables; }; struct UniformFillPlan { @@ -53,13 +144,90 @@ struct UniformFillPlan { inline constexpr std::uint32_t NativePushSlotSize = sizeof(PushData); inline constexpr std::uint32_t NativePushConstantSize = 2u * NativePushSlotSize; +class IrValueArena { +public: + IrValueArena() = default; + IrValueArena(const IrValueArena&) = delete; + IrValueArena& operator=(const IrValueArena&) = delete; + IrValueArena(IrValueArena&& other) noexcept : values(std::exchange(other.values, nullptr)), count(std::exchange(other.count, 0)), capacity(std::exchange(other.capacity, 0)) {} + IrValueArena& operator=(IrValueArena&& other) noexcept { + if (this != &other) { + release(); + values = std::exchange(other.values, nullptr); + count = std::exchange(other.count, 0); + capacity = std::exchange(other.capacity, 0); + } + return *this; + } + ~IrValueArena() { release(); } + + void Reserve(std::size_t size) { + if (values != nullptr) throw std::runtime_error("IrValueArena::Reserve called twice"); + if (size == 0) return; + values = static_cast(::operator new(size * sizeof(IrValue), std::align_val_t{alignof(IrValue)})); + capacity = size; + } + IrValue* Create(IrOpcode opcode, IrType type, std::uint32_t id) { + if (count == capacity) throw std::runtime_error("IrValueArena::Create exceeds the reserved size"); + auto* value = new (values + count) IrValue(opcode, type, id); + ++count; + return value; + } + [[nodiscard]] std::size_t Size() const { return count; } + [[nodiscard]] IrValue& operator[](std::size_t index) { return values[index]; } + [[nodiscard]] const IrValue& operator[](std::size_t index) const { return values[index]; } + [[nodiscard]] IrValue* begin() { return values; } + [[nodiscard]] IrValue* end() { return values + count; } + [[nodiscard]] const IrValue* begin() const { return values; } + [[nodiscard]] const IrValue* end() const { return values + count; } + +private: + void release() { + if (values == nullptr) return; + for (std::size_t index = count; index > 0; --index) values[index - 1].~IrValue(); + ::operator delete(values, std::align_val_t{alignof(IrValue)}); + values = nullptr; + count = 0; + capacity = 0; + } + IrValue* values = nullptr; + std::size_t count = 0; + std::size_t capacity = 0; +}; + +struct CompactPlanValue { + std::uint64_t immediate = 0; + std::uint64_t flags = 0; + std::uint32_t firstArgument = 0; + std::uint32_t registerIndex = 0; + IrType type = IrType::Void; + IrOpcode opcode = IrOpcode::Void; + std::uint8_t argumentCount = 0; + bool hasImmediate = false; +}; + +struct CompactResourcePlan { + static constexpr std::uint32_t NoValue = 0xffffffffu; + std::vector values; + std::vector arguments; + std::vector sourceDwords; + std::vector srtReads; + 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; std::uint32_t userDataBase = 0; std::uint32_t userDataCount = 64; std::uint32_t srgbDecodeFormats = 0; - std::vector> valueStorage; + IrValueArena valueStorage; std::vector> blockStorage; std::vector memoryInfo; std::vector descriptorSources; @@ -72,11 +240,14 @@ struct IrResourcePlan { // One byte per srtReads slot, 1 when the CPU walk never consumes the slot's value (see // Detail::ComputePureFlatSlots): a driver may reuse a capture whose words differ only there. std::vector pureFlatSlots; + bool denseValueIds = false; + CompactResourcePlan compact; bool requiresSpecializationMemory = false; bool srtPlanComplete = false; 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 79cbff90ce..307b83cdff 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -33,6 +33,8 @@ struct BufferResource { enum class ImageMipMode { None, DynamicStorage }; namespace EmulatedCompare { +inline constexpr std::uint32_t ConvertedSampleUnsupported = 1u << 27u; +inline constexpr std::uint32_t ConvertedSampleRequiresSingleLevel = 1u << 28u; inline constexpr std::uint32_t NativeOffsetUnsupported = 1u << 29u; inline constexpr std::uint32_t Unsupported = 1u << 31u; inline constexpr std::uint32_t RequiresSingleLevel = 1u << 30u; @@ -57,9 +59,11 @@ inline constexpr std::uint32_t ReferenceZero = 3u; [[nodiscard]] inline std::uint32_t Reference(std::uint32_t state) { return (state >> ReferenceShift) & 0x3u; } } -struct ImageResource { - static constexpr std::uint32_t NoIndirectImage = std::numeric_limits::max(); +inline constexpr std::uint32_t NoTable = std::numeric_limits::max(); +enum class TableOperation : std::uint8_t { Supported, Unsupported }; + +struct ImageResource { std::uint32_t source = 0; std::uint32_t firstUsePc = 0; ImageResourceClass resourceClass = ImageResourceClass::None; @@ -86,17 +90,21 @@ struct ImageResource { bool depthBitsCompatible = true; bool constantSwizzle = false; bool constantSwizzleCompatible = true; + bool cubeFaceCompatible = true; bool flatVolumeCompatible = true; + bool singleMipPlaneBiasCompatible = false; + bool volumeArraySampleCompatible = false; bool flatLineCompatible = true; + bool flatVolumeLineCompatible = true; bool foldTexelOffsetsOnly = false; + bool convertedSamples = false; + bool remappedDepth1D = false; std::uint32_t byElements = 0; std::uint32_t byComponents = 0; IrBufferFormat packedFormat = IrBufferFormat::Invalid; std::uint32_t emulatedCompare = 0; - std::uint32_t indirectRoot = NoIndirectImage; - std::uint32_t indirectMappingOffset = 0; - std::uint32_t indirectSearchIterations = 0; - std::vector indirectResources; + std::uint32_t table = NoTable; + TableOperation tableOperation = TableOperation::Supported; bool operator==(const ImageResource& other) const = default; }; @@ -127,16 +135,17 @@ struct SamplerResource { bool depthCompare = false; std::uint16_t uses = 0; bool foldTexelOffsetsOnly = false; + std::uint32_t table = NoTable; bool operator==(const SamplerResource& other) const = default; }; inline bool FoldsTexelOffsets(const SamplerResource& sampler) { - return (sampler.uses & SamplerUseOffset) != 0u && (sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseOffset | SamplerUseNonZeroLod)) == 0u && !sampler.depthCompare; + return sampler.table == NoTable && (sampler.uses & SamplerUseOffset) != 0u && (sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseOffset | SamplerUseNonZeroLod)) == 0u && !sampler.depthCompare; } inline bool SamplesBaseLevelIfUnnormalized(const SamplerResource& sampler) { - return (sampler.uses & (SamplerUseImplicitLod | SamplerUseGradient)) != 0u && (sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseImplicitLod | SamplerUseGradient | SamplerUseNonZeroLod)) == 0u && !sampler.depthCompare; + 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/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp index 8879b166be..1440effcb2 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp @@ -42,7 +42,7 @@ struct ShaderInfo { }; [[nodiscard]] inline std::uint32_t WorkgroupMemoryStrideDwords(const ShaderInfo& info) { - return info.sharedMemoryBytes == 0u ? 0u : info.sharedMemoryBytes / 4u + 1u; + return info.sharedMemoryBytes == 0u ? 0u : (info.sharedMemoryBytes / 4u + 2u) & ~1u; } } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp index 96c7d51533..bfa9785b83 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -14,7 +14,10 @@ enum class TessellationAttribute { EvaluationInput, PatchOutput, Factor, - PatchInput + PatchInput, + InstanceOutput, + InstanceInput, + ControlShared }; enum class TessellationBaseKind { @@ -46,8 +49,32 @@ enum class StageInputKind { HelperInvocation, RasterPrimitive, LayerExported, + BaseInstance, + BaryCoordSmoothCenter, + BaryCoordNoPerspectiveCenter, + BaryCoordSmoothSample, + BaryCoordNoPerspectiveSample, + SampleMask, }; +constexpr StageInputKind BarycentricInputOf(StageInputKind kind) { + switch (kind) { + case StageInputKind::BaryCoordSmoothCenter: + case StageInputKind::BaryCoordSmoothSample: + return StageInputKind::BaryCoordSmooth; + case StageInputKind::BaryCoordNoPerspectiveCenter: + case StageInputKind::BaryCoordNoPerspectiveSample: + return StageInputKind::BaryCoordNoPerspective; + default: + return kind; + } +} + +constexpr bool IsBarycentricInput(StageInputKind kind) { + const auto input = BarycentricInputOf(kind); + return input == StageInputKind::BaryCoordSmooth || input == StageInputKind::BaryCoordNoPerspective; +} + enum class StageOutputKind { Position, Parameter, diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index 6ed27287ab..c1ed5e72ed 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -73,6 +73,7 @@ enum class IrOpcode : std::uint16_t { Void, Reference, ReferenceU32, + UnlinkedCall, GetUserData, GetShaderBase, ShaderClock, @@ -431,6 +432,8 @@ enum class IrOpcode : std::uint16_t { SharedAtomicMskor64, DataAppend, DataConsume, + DataOrderedCount, + DataOrderedSwap, SwizzleU32, ImageQueryDimensions, ImageQueryLod, diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp index baeba29a7a..ccdda16ed9 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -35,7 +36,7 @@ public: [[nodiscard]] IrType Type() const; [[nodiscard]] std::uint32_t Id() const; [[nodiscard]] const std::vector& Arguments() const; - [[nodiscard]] const std::vector& Uses() const; + [[nodiscard]] auto Uses() const; [[nodiscard]] IrBlock* Parent() const; [[nodiscard]] bool HasImmediate() const; [[nodiscard]] std::uint32_t ImmediateU32() const; @@ -86,6 +87,7 @@ public: void SetImmediateU16(std::uint16_t value); void SetRegister(const GuestRegister& reg); void AddPhiOperand(IrBlock* predecessor, IrValue* value); + void SetDetachedOperands(std::vector arguments, std::vector phiBlocks); void ReplaceOpcode(IrOpcode opcode); void Invalidate(); void ReplaceUsesWith(IrValue* replacement, bool preserve); @@ -102,10 +104,13 @@ private: std::vector operandUses; bool hasImmediate; std::vector arguments; - std::vector uses; IrBlock* parent; }; +inline auto IrValue::Uses() const { + return std::views::transform(operandUses, [](const IrUse& use) { return use.user; }); +} + inline IrOpcode IrValue::Opcode() const { return opcode; } diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index cdb2a2d881..b3622f4c6a 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -114,6 +114,7 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("Void", Void), makeMeta("Reference", Void, U1), makeMeta("ReferenceU32", Void, U32), + makeMeta("UnlinkedCall", Void, U32, U32, U32), makeMeta("GetUserData", U32, ScalarReg), makeMeta("GetShaderBase", U64), makeMeta("ShaderClock", U64), @@ -472,13 +473,15 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("SharedAtomicMskor64", U64, U32, U64, U64, U1), makeMeta("DataAppend", U32, U32, U1, U32, U32), makeMeta("DataConsume", U32, U32, U1, U32, U32), + makeMeta("DataOrderedCount", U32, U32, U1, U32, U32, U32), + makeMeta("DataOrderedSwap", U32, U32, U1, U32, U32, U32), makeMeta("SwizzleU32", U32, U32, U32, U1), - makeMeta("ImageQueryDimensions", U32x4, ImageResource, ImageAddress), - makeMeta("ImageQueryLod", U32x4, ImageResource, SamplerResource, ImageAddress), + makeMeta("ImageQueryDimensions", U32x4, ImageResource, ImageAddress, U1), + makeMeta("ImageQueryLod", U32x4, ImageResource, SamplerResource, ImageAddress, U1), makeMeta("ImageRead", U32x4, ImageResource, ImageAddress, U1), makeMeta("ImageWrite", Void, ImageResource, ImageAddress, U32x4, U1), - makeMeta("ImageSampleRaw", U32x4, ImageResource, SamplerResource, ImageAddress), - makeMeta("ImageGatherRaw", U32x4, ImageResource, SamplerResource, ImageAddress), + makeMeta("ImageSampleRaw", U32x4, ImageResource, SamplerResource, ImageAddress, U1), + makeMeta("ImageGatherRaw", U32x4, ImageResource, SamplerResource, ImageAddress, U1), makeMeta("ImageAtomicSwap32", U32, ImageResource, ImageAddress, U32, U1), makeMeta("ImageAtomicIAdd32", U32, ImageResource, ImageAddress, U32, U1), makeMeta("ImageAtomicUMin32", U32, ImageResource, ImageAddress, U32, U1), @@ -695,6 +698,8 @@ SharedAccess SharedAccessOf(IrOpcode opcode) { case IrOpcode::SharedAtomicMskor64: return SharedAccess::Atomic; case IrOpcode::DataAppend: + case IrOpcode::DataOrderedCount: + case IrOpcode::DataOrderedSwap: return SharedAccess::Append; case IrOpcode::DataConsume: return SharedAccess::Consume; @@ -888,6 +893,7 @@ bool IrOpcodeHasSideEffects(IrOpcode opcode) { case IrOpcode::Discard: case IrOpcode::Reference: case IrOpcode::ReferenceU32: + case IrOpcode::UnlinkedCall: case IrOpcode::SetTessellationAttribute: case IrOpcode::InterpolateHostP2: case IrOpcode::SetThreadBitScalarRegister: diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp index 7d0a7a6c98..08dba6b1d9 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp @@ -208,9 +208,22 @@ std::string valueToString(const std::map& ids, cons } std::size_t IrProgram::ReclaimUnusedValues() { - std::unordered_set live; + std::vector owned(nextValueId, nullptr); + for (const auto& value : values) owned[value->Id()] = value.get(); + std::vector live(nextValueId, false); + std::unordered_set liveElsewhere; std::vector pending; - const auto keep = [&](IrValue* value) { if (value != nullptr && live.insert(value).second) pending.push_back(value); }; + const auto keep = [&](IrValue* value) { + if (value == nullptr) return; + if (const auto id = value->Id(); id < owned.size() && owned[id] == value) { + if (live[id]) return; + live[id] = true; + } else if (!liveElsewhere.insert(value).second) { + return; + } + pending.push_back(value); + }; + const auto isLive = [&](const IrValue& value) { return live[value.Id()]; }; const auto keepBlocks = [&](const auto& storage) { for (const auto& block : storage) { for (IrValue* instruction : block->Instructions()) keep(instruction); @@ -222,7 +235,7 @@ std::size_t IrProgram::ReclaimUnusedValues() { }; keepBlocks(blocks); keepBlocks(resourcePlan.blockStorage); - for (const auto& value : resourcePlan.valueStorage) keep(value.get()); + for (auto& value : resourcePlan.valueStorage) keep(&value); for (const auto& source : resourcePlan.descriptorSources) for (IrValue* word : source.dwords) keep(word); for (const auto& read : resourcePlan.srtReads) keep(read.value); for (const auto& block : resourcePlan.controlFlow) keep(block.condition); @@ -235,13 +248,13 @@ std::size_t IrProgram::ReclaimUnusedValues() { for (std::size_t index = 0; index < value->ArgumentCount(); ++index) keep(value->Argument(index)); } for (const auto& value : values) { - if (live.contains(value.get())) continue; + if (isLive(*value)) continue; for (std::size_t index = 0; index < value->ArgumentCount(); ++index) value->ReplaceArgument(index, nullptr); } for (const auto& value : values) { - if (!live.contains(value.get()) && value->HasUses()) throw std::runtime_error("an unused IR value has an external user"); + if (!isLive(*value) && value->HasUses()) throw std::runtime_error("an unused IR value has an external user"); } - const auto removed = std::erase_if(values, [&](const auto& value) { return !live.contains(value.get()); }); + const auto removed = std::erase_if(values, [&](const auto& value) { return !isLive(*value); }); values.shrink_to_fit(); return removed; } diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp index 4a1a662b69..a7cc1db50d 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp @@ -6,6 +6,7 @@ namespace ShaderRecompiler { IrValue::IrValue(IrOpcode opcode, IrType type, std::uint32_t id) : opcode(opcode), type(type), id(id), hasImmediate(false), parent(nullptr) { + arguments.reserve(IrOpcodeOperandCount(opcode)); } std::uint32_t IrValue::Id() const { @@ -16,10 +17,6 @@ const std::vector& IrValue::Arguments() const { return arguments; } -const std::vector& IrValue::Uses() const { - return uses; -} - IrBlock* IrValue::Parent() const { return parent; } @@ -33,7 +30,6 @@ void IrValue::AddArgument(IrValue* argument) { if (argument == nullptr) { return; } - argument->uses.push_back(this); argument->operandUses.push_back(IrUse{this, arguments.size() - 1}); } @@ -43,10 +39,6 @@ void IrValue::ReplaceArgument(std::size_t index, IrValue* argument) { } IrValue* previous = arguments[index]; if (previous != nullptr) { - const auto useIt = std::find(previous->uses.begin(), previous->uses.end(), this); - if (useIt != previous->uses.end()) { - previous->uses.erase(useIt); - } const auto operandIt = std::find_if(previous->operandUses.begin(), previous->operandUses.end(), [this, index](const IrUse& use) { return use.user == this && use.operand == index; }); @@ -56,7 +48,6 @@ void IrValue::ReplaceArgument(std::size_t index, IrValue* argument) { } arguments[index] = argument; if (argument != nullptr) { - argument->uses.push_back(this); argument->operandUses.push_back(IrUse{this, index}); } } @@ -65,10 +56,27 @@ 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->operandUses); } + for (auto use = operandUses.rbegin(); use != operandUses.rend(); ++use) { + use->user->arguments[use->operand] = replacement; + if (replacement != nullptr) { + replacement->operandUses.push_back(*use); + } + } + operandUses.clear(); } void IrValue::SetImmediateU32(std::uint32_t value) { @@ -93,11 +101,11 @@ bool IrValue::MayHaveSideEffects() const { } bool IrValue::HasUses() const { - return !uses.empty(); + return !operandUses.empty(); } std::size_t IrValue::UseCount() const { - return uses.size(); + return operandUses.size(); } std::size_t IrValue::PhiBlockCount() const { @@ -188,12 +196,23 @@ void IrValue::AddPhiOperand(IrBlock* predecessor, IrValue* value) { AddArgument(value); } +void IrValue::SetDetachedOperands(std::vector arguments, std::vector phiBlocks) { + if (!this->arguments.empty() || !this->phiBlocks.empty()) { + throw std::runtime_error("IrValue::SetDetachedOperands called on a value with operands"); + } + if (IsPhi() ? phiBlocks.size() != arguments.size() : !phiBlocks.empty()) { + throw std::runtime_error("IrValue::SetDetachedOperands phi blocks do not match the arguments"); + } + this->arguments = std::move(arguments); + this->phiBlocks = std::move(phiBlocks); +} + void IrValue::ReplaceOpcode(IrOpcode opcode) { this->opcode = opcode; } void IrValue::Invalidate() { - if (!uses.empty()) { + if (!operandUses.empty()) { throw std::runtime_error("IrValue::Invalidate called on a value that still has uses"); } for (std::size_t index = 0; index < arguments.size(); index++) { diff --git a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp new file mode 100644 index 0000000000..0849790845 --- /dev/null +++ b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp @@ -0,0 +1,245 @@ +#include "IntermediateRepresentation/IrBlock.hpp" +#include +#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(std::ranges::equal(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 < 1; ++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 <= 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 0944d23535..7396e8e96d 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp @@ -19,11 +19,12 @@ struct BindingAllocationResult : CompiledBindingLayout { std::vector specialization; std::vector bindings; std::vector pushConstants; + std::vector shaderDataUserWords; }; class BindingAllocator { public: - [[nodiscard]] BindingAllocationResult Allocate(IrProgram& program, const BindingLayout& layout, bool registrationModules = false) const; + [[nodiscard]] BindingAllocationResult Allocate(IrProgram& program, const BindingLayout& layout, bool registrationModules = false, bool stencilReplay = false) const; [[nodiscard]] const IrDescriptorBinding& FindBinding(const IrBindingLayout& layout, DescriptorBindingKind kind) const; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp b/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp index d2df4ed3cc..758920681c 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp @@ -14,10 +14,9 @@ public: void Fold(IrProgram& program, std::span blocks) const; - [[nodiscard]] bool ComposePackedAncillary(IrProgram& program) const; + [[nodiscard]] bool FoldVertexLaneBounds(IrProgram& program, std::uint32_t hostSubgroupSize, std::uint32_t spirvVersion) const; -private: - [[nodiscard]] bool tryFoldValue(IrProgram& program, IrValue& value) const; + [[nodiscard]] bool ComposePackedAncillary(IrProgram& program) const; }; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index 84341598a9..9d48693aba 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -3,6 +3,7 @@ #include "IntermediateRepresentation/IrProgram.hpp" #include "Optimization/BindingAllocator.hpp" +#include "Optimization/ResourceMaterializer.hpp" #include "Recompiler.hpp" #include @@ -15,6 +16,7 @@ struct PreparedDescriptorBinding { std::vector identityImageSwizzle; std::vector samplerFilterElements; std::vector inactive; + std::vector tableSamplerMasks; std::size_t writtenBuffers = 0; std::size_t readOnlyBuffers = 0; }; @@ -37,7 +39,7 @@ class DescriptorBindingBuilder { public: DescriptorBindingPlan Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings = {}, bool registrationModules = false, bool fragmentDescriptorSet = false) const; DescriptorBindingPlan Select(const DescriptorBindingPlan& plan, std::span bindings) const; - void Populate(BindingAllocationResult& allocation, const CompiledBindingLayout& compiled, const DescriptorBindingPlan& plan, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; + void Populate(BindingAllocationResult& allocation, const CompiledBindingLayout& compiled, const DescriptorBindingPlan& plan, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads, const ResolvedImageTables* tables = nullptr) const; void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads, std::span exportMappings = {}) const; void Populate(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads, std::span exportMappings = {}) const; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp index 2e0b5bd5b5..e8b873610f 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp @@ -3,6 +3,7 @@ #include "IntermediateRepresentation/IrProgram.hpp" #include +#include namespace ShaderRecompiler { @@ -16,6 +17,9 @@ public: [[nodiscard]] MaskedSelectEliminationStats Eliminate(IrProgram& program) const; }; +[[nodiscard]] bool GuardImplies(const IrProgram& program, const IrValue* condition, const IrValue* consequence); +[[nodiscard]] bool GuardImpliesMaskOnPath(const IrProgram& program, const IrValue* condition, const IrValue* low, const IrValue* high, std::span path); + } #endif diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index 2dbf39823a..1425a5ec58 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -3,28 +3,86 @@ #include "IntermediateRepresentation/IrProgram.hpp" #include "Optimization/SrtWalker.hpp" +#include "ImageTableAbi.hpp" +#include #include +#include +#include +#include +#include #include namespace ShaderRecompiler { -// Why a bindless image table (a T# loaded from a table buffer at a runtime key) was not bound; -// counted on the [bindless] line (APS5_PROFILE_DRAW). -enum class BindlessRejection { Capacity, MaterialScan, NoEntry, Storage, NonUniform, ImageSlots, Count }; +struct ImageTableElement { + std::array words{}; + RdnaImageDimension dimension = RdnaImageDimension::Unknown; + bool depthCompare = false; + DescriptorBindingKind kind = DescriptorBindingKind::Buffers; + std::uint32_t samplers = 0; +}; + +struct ImageTableSamplerElement { + std::array words{}; + bool compare = false; + bool point = false; +}; + +struct ImageTablePoisonRecord { + std::array words{}; + std::uint32_t dwordCount = 0; + std::uint32_t resource = 0; + ImageTableAbi::PoisonReason reason = ImageTableAbi::PoisonReason::OutsideSnapshot; +}; + +struct ResolvedImageTables { + std::vector map; + std::vector elements; + std::vector samplers; + std::vector poison; + std::uint32_t faults = 0; +}; + +struct ImageTableReuseCounts { + std::uint64_t unwritten = 0; + std::uint64_t compared = 0; + std::uint64_t wordsRead = 0; + std::uint64_t shared = 0; + std::uint64_t columns = 0; + std::uint64_t resolutions = 0; + std::uint64_t resolutionsShared = 0; +}; + +struct RuntimeImageModeMatch { + std::optional mode; + ImageTableAbi::PoisonReason reason = ImageTableAbi::PoisonReason::InvalidFormat; + std::string detail; +}; + +class ConvertedSampleRequired : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; class ResourceMaterializer { public: static std::uint32_t EmulatedCompareState(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint32_t index); void ApplyStaticInterface(IrProgram& program, bool nativeSampleOffsets = true) const; static std::vector RuntimeImageModes(const ImageResource& image); + static RuntimeImageModeMatch TryRuntimeImageMode(const ImageResource& image, const DescriptorValue& descriptor, std::span modes); static std::uint32_t RuntimeImageMode(const ImageResource& image, const DescriptorValue& descriptor, std::span modes); static void PrepareImageModes(ShaderInfo& info); static const char* UnnormalizedSampleMismatch(const ImageResource& image); + static std::shared_ptr ResolveImageTablesShared(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint64_t memoKey); + static bool NullImageDescriptor(const ImageResource& image, const DescriptorValue& descriptor); + static ResolvedImageTables ResolveImageTables(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint64_t memoKey = 0); [[nodiscard]] IrResourcePlan ExtractPlan(const IrProgram& program) const; + static void CompactPlan(IrResourcePlan& plan); void Materialize(const IrResourcePlan& program, const SrtRuntime& runtime, ResourceSnapshot& snapshot) const; static std::uint64_t SpecializationNanoseconds(); - static std::uint32_t BindlessSlots(); - static void CountBindlessRejection(BindlessRejection reason); + static ImageTableReuseCounts TableReuse(); + static std::uint64_t EvaluateNanoseconds(); + static std::uint64_t MaterializeNanoseconds(); }; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp index 77328b8672..7067a1fc6c 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; @@ -80,6 +81,7 @@ void BuildPreparedShaderKey(const RecompileRequest& request, std::vector RegistrationResult(const RecompileRequest& request, const SourceHandle& handle); [[nodiscard]] std::shared_ptr MaterializeShader(const RecompileRequest& request, const ResourceCapture& capture, const SourceHandle& handle); [[nodiscard]] std::shared_ptr ResolveSource(const RecompileRequest& request); +void ReleaseUndrawnResourcePlan(const SourceHandle& handle); [[nodiscard]] std::shared_ptr CaptureResources(const RecompileRequest& request, const SrtRuntime& runtime, const SourceHandle& handle); } diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 77e52d36de..3a9286f578 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -3,6 +3,7 @@ #include "IntermediateRepresentation/IrMetadata.hpp" #include "Recompiler.hpp" +#include #include #include #include @@ -68,6 +69,10 @@ struct ShaderStageRuntime { } }; +struct ShaderDepthRemap { + std::uint32_t mode = 0; +}; + struct ShaderClipSpaceTransform { float scale[2] = {}; float offset[2] = {}; @@ -91,6 +96,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; @@ -115,6 +121,9 @@ struct ShaderTessellationInputInfo { std::uint32_t patchesPerGroup = 256; std::uint32_t primitiveGroupSize = 0; bool breakAtInstance = false; + std::uint32_t instancePatches = 0; + bool drawParameters = false; + bool mergedHull = false; }; struct ShaderVertexInputInfo { @@ -132,7 +141,12 @@ struct ShaderVertexInputInfo { std::uint32_t scratchSizeDwords = 0; std::uint32_t hostSubgroupSize = 64; std::uint32_t paClVsOutCntl = 0; + bool pointList = false; + float pointSize = 1.0f; + float pointSizeMin = 0.0f; + float pointSizeMax = 0.0f; ShaderClipSpaceTransform clipSpace; + ShaderDepthRemap depthRemap; ShaderMeshInputInfo mesh; ShaderTessellationInputInfo tess; bool fetchExternal = false; @@ -165,12 +179,14 @@ struct ShaderPixelInputInfo { bool psPosZ = false; bool psPosW = false; bool psFrontFace = false; + bool psFrontFaceAllBits = false; bool psAncillary = false; bool psNoPerspective = false; bool psPixelKillEnable = false; bool psDepthExportEnable = false; bool psSampleMaskExportEnable = false; bool psStencilRefExportEnable = false; + bool psStencilOperationValueExport = false; bool psSampleShading = false; bool psEarlyZ = false; bool psExecuteOnNoop = false; @@ -178,12 +194,41 @@ struct ShaderPixelInputInfo { bool psOrderedPixelShader = false; bool psAlphaToCoverage = false; bool psDitheredAlphaToCoverage = false; + std::uint8_t quadPixelMask = 0xf; + std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; + bool psIteratesSamples = false; + PixelPositionLocation psPositionLocation = PixelPositionLocation::Center; + std::array sampleLocations{}; + std::array sampleOrder{}; ShaderStageRuntime stage; bool HasPositionInput() const { return psPosX || psPosY || psPosZ || psPosW; } + [[nodiscard]] bool SampleLocationsOffCenter() const { + return std::ranges::any_of(sampleLocations, [](std::uint8_t location) { return location != 0u; }); + } + + [[nodiscard]] bool SampleLocationsVary() const { + for (std::size_t index = 8; index < sampleLocations.size(); ++index) { + if (sampleLocations[index] != sampleLocations[index % 8u]) return true; + } + return false; + } + + [[nodiscard]] bool ReordersSamples() const { + return std::ranges::any_of(sampleOrder, [](std::uint32_t order) { return order != 0u && order != 0x76543210u; }); + } + + [[nodiscard]] bool QuadSampleMaskPartial() const { + return std::ranges::any_of(quadSampleMask, [](std::uint16_t mask) { return mask != 0xffffu; }); + } + + [[nodiscard]] bool QuadMasked() const { + return quadPixelMask != 0xf || QuadSampleMaskPartial(); + } + [[nodiscard]] bool InputIsDefault(std::uint32_t input) const { return input < inputNum && input < 32u && (interpolatorSettings[input] & 0x420u) == 0x20u; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp index 4b80539b01..79269617fe 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp @@ -2,8 +2,11 @@ #define CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_SRTWALKER_HPP #include "IntermediateRepresentation/IrProgram.hpp" +#include "UserDataUse.hpp" +#include #include +#include #include #include #include @@ -12,6 +15,52 @@ namespace ShaderRecompiler { using SrtMemoryReader = bool (*)(void* userData, std::uint64_t address, std::uint32_t* value); using SrtMemoryProbe = bool (*)(void* userData, std::uint64_t address, std::uint64_t bytes); +using SrtMemoryBulkReader = bool (*)(void* userData, std::uint64_t address, std::uint32_t* values, std::uint64_t count); + +struct SrtWriteStamp { + std::uint64_t generation = 0; + std::uint64_t layout = 0; +}; + +struct SrtStampCell { + std::atomic sequence{0}; + std::atomic generation{0}; + std::atomic layout{0}; +}; + +struct SrtGuestWords { + std::vector> runs; + std::vector words; + std::shared_ptr stamp; + std::uint64_t low = 0; + std::uint64_t high = 0; + bool ordered = false; +}; + +inline SrtWriteStamp LoadStamp(const SrtGuestWords& words) { + if (words.stamp == nullptr) return {}; + const auto& cell = *words.stamp; + const auto sequence = cell.sequence.load(std::memory_order_acquire); + if ((sequence & 1u) != 0u) return {}; + const SrtWriteStamp stamp{cell.generation.load(std::memory_order_relaxed), cell.layout.load(std::memory_order_relaxed)}; + std::atomic_thread_fence(std::memory_order_acquire); + return cell.sequence.load(std::memory_order_relaxed) == sequence ? stamp : SrtWriteStamp{}; +} + +inline void StoreStamp(const SrtGuestWords& words, const SrtWriteStamp& stamp) { + if (words.stamp == nullptr || stamp.generation == 0u) return; + auto& cell = *words.stamp; + auto sequence = cell.sequence.load(std::memory_order_relaxed); + if ((sequence & 1u) != 0u || !cell.sequence.compare_exchange_strong(sequence, sequence + 1u, std::memory_order_relaxed)) return; + std::atomic_thread_fence(std::memory_order_release); + cell.generation.store(stamp.generation, std::memory_order_relaxed); + cell.layout.store(stamp.layout, std::memory_order_relaxed); + cell.sequence.store(sequence + 2u, std::memory_order_release); +} + +using SrtMemoryStamper = SrtWriteStamp (*)(void* userData, std::uint64_t address, std::uint64_t bytes); +using SrtMemoryUnchanged = bool (*)(void* userData, std::uint64_t address, std::uint64_t bytes, const SrtWriteStamp& stamp); +using SrtMemorySupplier = bool (*)(void* userData, const std::shared_ptr& words); // The guest addresses a walk dereferenced (Detail::Evaluator::EvaluateRawRead): the leaf read of // each pure flat slot (IrResourcePlan::pureFlatSlots) as (flat offset, address), set by the @@ -32,6 +81,12 @@ struct SrtRuntime { SrtMemoryReader readSpecializationMemory = nullptr; SrtReadTrace* readTrace = nullptr; SrtMemoryProbe accessible = nullptr; + SrtMemoryProbe pendingWrite = nullptr; + SrtMemoryBulkReader readWords = nullptr; + SrtMemoryStamper stampWrites = nullptr; + SrtMemoryUnchanged unchangedSince = nullptr; + SrtMemorySupplier supplyWords = 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 8332618a6b..d666acd0f8 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp @@ -5,7 +5,10 @@ #include "Optimization/SrtWalker.hpp" #include +#include +#include #include +#include #include namespace ShaderRecompiler::Detail { @@ -71,36 +74,139 @@ struct InaccessibleRead { std::uint64_t address = 0; }; -class Evaluator { +class DenseValues { public: - Evaluator(const IrResourcePlan& program, const SrtRuntime& runtime, std::span cleanFlatSlots = {}, Evaluator* cleanEvaluator = nullptr, IrValue* activeMask = nullptr) : _program(program), _runtime(runtime), _cleanFlatSlots(cleanFlatSlots), _cleanEvaluator(cleanEvaluator), _activeMask(activeMask != nullptr ? activeMask->Resolve() : nullptr) {} + struct Slot { + std::uint64_t stamp = 0; + std::uint64_t value = 0; + }; + explicit DenseValues(std::size_t values); + ~DenseValues(); + DenseValues(const DenseValues&) = delete; + DenseValues& operator=(const DenseValues&) = delete; + Slot& At(std::uint32_t id) { return _slots[id]; } + [[nodiscard]] std::size_t Size() const { return _size; } + [[nodiscard]] std::uint64_t Done() const { return _stamp; } + [[nodiscard]] std::uint64_t Visiting() const { return _stamp + 1u; } - bool Evaluate(IrValue* value, std::uint32_t& result); - bool EvaluateWide(IrValue* raw, std::uint64_t& result); +private: + std::vector* _array = nullptr; + Slot* _slots = nullptr; + std::size_t _size = 0; + std::uint64_t _stamp = 0; +}; + +class IrNode { +public: + IrNode() = default; + explicit IrNode(IrValue* value) : _value(value->Resolve()) {} + static IrNode From(const IrResourcePlan&, IrValue* value) { return IrNode(value); } + static IrNode SrtRead(const IrResourcePlan& program, std::uint32_t slot) { return IrNode(program.srtReads[slot].value); } + static IrNode SourceDword(const IrResourcePlan& program, std::uint32_t source, std::uint32_t dword) { return IrNode(program.descriptorSources[source].dwords[dword]); } + static bool Condition(const IrResourcePlan& program, std::uint32_t block, IrNode& condition) { + if (program.controlFlow[block].condition == nullptr) return false; + condition = IrNode(program.controlFlow[block].condition); + return true; + } + [[nodiscard]] IrOpcode Opcode() const { return _value->Opcode(); } + [[nodiscard]] IrType Type() const { return _value->Type(); } + [[nodiscard]] bool HasImmediate() const { return _value->HasImmediate(); } + [[nodiscard]] std::uint64_t ImmediateBits() const { return _value->ImmediateU64(); } + [[nodiscard]] std::size_t ArgumentCount() const { return _value->ArgumentCount(); } + [[nodiscard]] IrNode Argument(std::size_t index) const { return IrNode(_value->Argument(index)); } + template + [[nodiscard]] TValue Flags() const { return _value->Flags(); } + [[nodiscard]] std::uint32_t RegisterIndex() const { return _value->Register().index; } + [[nodiscard]] std::uint32_t Id() const { return _value->Id(); } + [[nodiscard]] IrValue* Value() const { return _value; } + bool operator==(const IrNode& other) const { return _value == other._value; } + +private: + IrValue* _value = nullptr; +}; + +class CompactNode { +public: + CompactNode() = default; + CompactNode(const IrResourcePlan& program, std::uint32_t id) : _program(&program), _id(id) {} + static CompactNode From(const IrResourcePlan& program, IrValue* value) { return {program, value->Resolve()->Id()}; } + static CompactNode SrtRead(const IrResourcePlan& program, std::uint32_t slot) { return {program, program.compact.srtReads[slot]}; } + static CompactNode SourceDword(const IrResourcePlan& program, std::uint32_t source, std::uint32_t dword) { return {program, program.compact.sourceDwords[source * 8u + dword]}; } + static bool Condition(const IrResourcePlan& program, std::uint32_t block, CompactNode& condition) { + if (program.compact.conditions[block] == CompactResourcePlan::NoValue) return false; + condition = {program, program.compact.conditions[block]}; + return true; + } + [[nodiscard]] IrOpcode Opcode() const { return entry().opcode; } + [[nodiscard]] IrType Type() const { return entry().type; } + [[nodiscard]] bool HasImmediate() const { return entry().hasImmediate; } + [[nodiscard]] std::uint64_t ImmediateBits() const { return entry().immediate; } + [[nodiscard]] std::size_t ArgumentCount() const { return entry().argumentCount; } + [[nodiscard]] CompactNode Argument(std::size_t index) const { + const auto& value = entry(); + if (index >= value.argumentCount) throw std::out_of_range("IrValue::Argument index is out of range"); + return {*_program, _program->compact.arguments[value.firstArgument + index]}; + } + template + [[nodiscard]] TValue Flags() const { + TValue result{}; + std::memcpy(&result, &entry().flags, sizeof(result)); + return result; + } + [[nodiscard]] std::uint32_t RegisterIndex() const { return entry().registerIndex; } + [[nodiscard]] std::uint32_t Id() const { return _id; } + [[nodiscard]] IrValue* Value() const { return const_cast(&_program->valueStorage[_id]); } + bool operator==(const CompactNode& other) const { return _id == other._id; } + +private: + [[nodiscard]] const CompactPlanValue& entry() const { return _program->compact.values[_id]; } + + const IrResourcePlan* _program = nullptr; + std::uint32_t _id = 0; +}; + +template +class BasicEvaluator { +public: + BasicEvaluator(const IrResourcePlan& program, const SrtRuntime& runtime, std::span cleanFlatSlots = {}, BasicEvaluator* cleanEvaluator = nullptr, const TNode* activeMask = nullptr) : _program(program), _runtime(runtime), _cleanFlatSlots(cleanFlatSlots), _cleanEvaluator(cleanEvaluator), _activeMask(activeMask != nullptr ? *activeMask : TNode{}), _masked(activeMask != nullptr), _dense(program.denseValueIds ? program.valueStorage.Size() : 0u) {} + BasicEvaluator(const BasicEvaluator&) = delete; + BasicEvaluator& operator=(const BasicEvaluator&) = delete; + + 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); - bool Arg(IrValue& inst, std::size_t index, std::uint64_t& result); - bool EvaluatePhi(IrValue& inst, std::uint64_t& result); - bool EvaluateExtract(IrValue& inst, std::uint64_t& result); - bool EvaluateRawRead(IrValue& inst, std::uint64_t& result); - bool EvaluateInst(IrValue& inst, std::uint64_t& result); + bool Arg(const TNode& inst, std::size_t index, std::uint64_t& result); + bool EvaluatePhi(const TNode& inst, std::uint64_t& result); + bool EvaluateExtract(const TNode& inst, std::uint64_t& result); + bool EvaluateRawRead(const TNode& inst, std::uint64_t& result); + bool EvaluateInst(const TNode& inst, std::uint64_t& result); const IrResourcePlan& _program; const SrtRuntime& _runtime; std::span _cleanFlatSlots; - Evaluator* _cleanEvaluator = nullptr; - IrValue* _activeMask = nullptr; + 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; std::vector _visiting; + DenseValues _dense; }; +using Evaluator = BasicEvaluator; +using CompactEvaluator = BasicEvaluator; + } #endif diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtInstructionPredicates.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtInstructionPredicates.hpp index 00c598e3bd..fa1310cff9 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtInstructionPredicates.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtInstructionPredicates.hpp @@ -3,14 +3,38 @@ #include "IntermediateRepresentation/IrProgram.hpp" +#include +#include + namespace ShaderRecompiler::Detail { bool IsRawRead(const IrResourcePlan& program, const IrValue& inst); +bool IsRawRead(const IrResourcePlan& program, IrOpcode op, std::uint32_t memoryIndex); bool LoadedFromMemory(const IrValue& handle); bool IsDescriptorHandle(IrOpcode opcode); bool IsRuntimeSelect(IrOpcode opcode); bool IsRuntimeUniformOp(IrOpcode opcode); +bool ActiveMaskImplies(const IrValue* mask, const IrValue* condition); + +inline bool ActiveMaskImplies(IrValue* mask, IrValue* condition) { + return ActiveMaskImplies(static_cast(mask), static_cast(condition)); +} + +inline bool ActiveMaskImplies(const IrValue* mask, IrValue* condition) { + return ActiveMaskImplies(mask, static_cast(condition)); +} + +inline bool ActiveMaskImplies(IrValue* mask, const IrValue* condition) { + return ActiveMaskImplies(static_cast(mask), condition); +} + +template +requires (!std::is_pointer_v>) +bool ActiveMaskImplies(const TNode& mask, const TNode& condition) { + return ActiveMaskImplies(mask.Value(), condition.Value()); +} + } #endif diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtPlanBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtPlanBuilder.hpp index f79ffe82e0..35e35a4554 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtPlanBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtPlanBuilder.hpp @@ -3,7 +3,10 @@ #include "IntermediateRepresentation/IrProgram.hpp" +#include #include +#include +#include #include namespace ShaderRecompiler::Detail { @@ -22,11 +25,13 @@ private: }; void Collect(IrValue* raw, std::uint32_t usePc); + void Record(IrValue* inst); void PatchReads(); IrProgram& _program; std::vector _visiting; - std::vector _visited; + std::unordered_map _visitingIndex; + std::unordered_set _visited; std::vector _patches; }; 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/include/Optimization/TessellationLowering.hpp b/core/shader/recompiler/Optimization/include/Optimization/TessellationLowering.hpp index 9c524f45e0..7032578c3b 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/TessellationLowering.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/TessellationLowering.hpp @@ -3,12 +3,13 @@ #include "IntermediateRepresentation/IrProgram.hpp" #include "Optimization/ShaderStageInputInfo.hpp" +#include "Recompiler.hpp" namespace ShaderRecompiler { class TessellationLowering { public: - void Lower(IrProgram& program, const ShaderTessellationInputInfo& tessellation) const; + void Lower(IrProgram& program, const ShaderTessellationInputInfo& tessellation, const TessellationTargetLimits* limits = nullptr) const; }; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/VectorWindowStoreEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/VectorWindowStoreEliminator.hpp new file mode 100644 index 0000000000..fd8d5f6ac7 --- /dev/null +++ b/core/shader/recompiler/Optimization/include/Optimization/VectorWindowStoreEliminator.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_VECTORWINDOWSTOREELIMINATOR_HPP +#define CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_VECTORWINDOWSTOREELIMINATOR_HPP + +#include "IntermediateRepresentation/IrProgram.hpp" +#include + +namespace ShaderRecompiler { + +struct VectorWindowStoreEliminationStats { + std::uint32_t removedStores = 0; +}; + +class VectorWindowStoreEliminator { +public: + [[nodiscard]] VectorWindowStoreEliminationStats Eliminate(IrProgram& program) const; +}; + +} + +#endif diff --git a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp index ceaca33ffd..1f6c140f8d 100644 --- a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp +++ b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp @@ -68,7 +68,7 @@ bool usesGds(const IrProgram& program) { } -BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const BindingLayout& layout, bool registrationModules) const { +BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const BindingLayout& layout, bool registrationModules, bool stencilReplay) const { IrProgramMetadata& metadata = program.Metadata(); if (!metadata.shaderInfoComplete || metadata.bindingLayoutComplete) { fail(metadata.shaderInfoComplete ? "shader binding layout failed: binding layout already allocated" @@ -102,7 +102,7 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin next.dispatchThreadLimit = info.dispatchThreadLimit; if (info.buffers.size() > RuntimeAbi::BufferCapacity || info.images.size() > RuntimeAbi::ImageCapacity || info.samplers.size() > RuntimeAbi::SamplerHeapCapacity) fail("shader binding layout exceeds runtime metadata capacity"); if (std::ranges::any_of(next.userDataRegisters, [](auto reg) { return reg >= RuntimeAbi::UserDataCapacity; })) fail("shader user data exceeds runtime ABI capacity"); - if (registrationModules || std::ranges::any_of(info.images, [](const auto& image) { return image.indirectRoot != ImageResource::NoIndirectImage; })) next.runtimeImageCount = static_cast(info.images.size()); + if (registrationModules) next.runtimeImageCount = static_cast(info.images.size()); if (registrationModules && program.Resources().stage == IrShaderStage::Pixel) next.runtimeStateDwords = RuntimeAbi::StateDwords; if (registrationModules && program.Resources().stage != IrShaderStage::Pixel) { std::uint32_t attributes = 0u; @@ -115,6 +115,11 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin } const auto pushSize = layout.pushConstantSizeBytes / 4u; next.pushDataStartDword = next.runtimeImageCount == 0u && next.ShaderDataDwords() != 0u && next.ShaderDataDwords() <= pushSize ? layout.pushConstantOffsetBytes / 4u : PushData::NoStart; + if (stencilReplay) { + const auto offset = next.UsesPushData() ? next.ShaderDataDwords() : 0u; + if (offset >= pushSize) fail("stencil replay requires an unused push constant dword"); + next.stencilReferenceDword = offset; + } if (!info.buffers.empty()) { std::vector resources(info.buffers.size()); @@ -136,22 +141,9 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin imageGroups[group].insert(imageGroups[group].end(), count, i); } }; - // A bindless table's slots follow their root as consecutive elements: the SPIR-V indexes the - // binding with element(root) + slot. for (std::uint32_t i = 0; i < info.images.size(); i++) { - const auto root = info.images[i].indirectRoot; - if (root != ImageResource::NoIndirectImage && root != i) { - continue; - } - place(i); - if (root != i) { - continue; - } - for (const auto slot : info.images[i].indirectResources) { - if (slot >= info.images.size() || info.images[slot].indirectRoot != i) { - fail("shader binding layout failed: image " + std::to_string(i) + " has an inconsistent table slot"); - } - if (slot != i) place(slot); + if (info.images[i].table == NoTable) { + place(i); } } for (std::uint32_t i = 0; i < imageGroups.size(); i++) { @@ -161,13 +153,15 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin } } - if (!info.samplers.empty()) { - if (info.samplers.size() > RuntimeAbi::SamplerHeapCapacity / 2u) fail("shader sampler pairs exceed runtime heap capacity"); - std::vector resources(info.samplers.size() * 2u); - for (std::uint32_t i = 0; i < resources.size(); i++) { - resources[i] = i / 2u; + std::vector samplers; + for (std::uint32_t i = 0; i < info.samplers.size(); i++) { + if (info.samplers[i].table == NoTable) { + samplers.insert(samplers.end(), 2u, i); } - addBinding(next, DescriptorBindingKind::Samplers, std::move(resources)); + } + if (!samplers.empty()) { + 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)) { addBinding(next, DescriptorBindingKind::Gds); @@ -179,13 +173,23 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin addBinding(next, DescriptorBindingKind::FaultBuffer); } - const bool usesFlattenedRuntime = !program.Resources().srtReads.empty() || - std::ranges::any_of(info.images, [](const ImageResource& image) { - return image.indirectSearchIterations != 0u; - }); - if (usesFlattenedRuntime) { + if (!program.Resources().srtReads.empty()) { addBinding(next, DescriptorBindingKind::FlattenedSrt); } + const bool imageTables = std::ranges::any_of(info.images, [](const ImageResource& image) { return image.table != NoTable; }); + const bool samplerTables = std::ranges::any_of(info.samplers, [](const SamplerResource& sampler) { return sampler.table != NoTable; }); + if (samplerTables) { + addBinding(next, DescriptorBindingKind::SamplerTable); + } + if (imageTables || samplerTables) { + addBinding(next, DescriptorBindingKind::ImageTableMap); + } + 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); @@ -200,6 +204,10 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin result.pushConstantOffsetBytes = result.layout.pushDataStartDword * static_cast(sizeof(std::uint32_t)); result.pushConstantSizeBytes = result.layout.ShaderDataDwords() * static_cast(sizeof(std::uint32_t)); } + if (stencilReplay) { + result.pushConstantOffsetBytes = layout.pushConstantOffsetBytes; + result.pushConstantSizeBytes += sizeof(std::uint32_t); + } return result; } diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 3505f640f4..1eb0dd3650 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include @@ -81,6 +83,152 @@ bool isLaneIndex(const IrValue& value) { return value.Opcode() == IrOpcode::LaneId; } +std::uint32_t possibleBits(const IrValue& value, unsigned depth = 0) { + if (value.HasImmediate() && value.Type() == IrType::U32) return value.ImmediateU32(); + if (depth >= 8u || value.Type() != IrType::U32) return UINT32_MAX; + const auto arg = [&](std::size_t index) -> const IrValue& { return *value.Argument(index)->Resolve(); }; + const auto immediate = [&](std::size_t index) { return arg(index).HasImmediate() && arg(index).Type() == IrType::U32; }; + switch (value.Opcode()) { + case IrOpcode::BitwiseAnd32: return possibleBits(arg(0), depth + 1) & possibleBits(arg(1), depth + 1); + case IrOpcode::BitwiseOr32: + case IrOpcode::BitwiseXor32: return possibleBits(arg(0), depth + 1) | possibleBits(arg(1), depth + 1); + case IrOpcode::BitFieldUExtract: + if (immediate(2) && arg(2).ImmediateU32() < 32u) return (std::uint32_t{1} << arg(2).ImmediateU32()) - 1u; + return UINT32_MAX; + case IrOpcode::ShiftLeftLogical32: + if (immediate(1) && arg(1).ImmediateU32() < 32u) return possibleBits(arg(0), depth + 1) << arg(1).ImmediateU32(); + return UINT32_MAX; + case IrOpcode::ShiftRightLogical32: + if (immediate(1) && arg(1).ImmediateU32() < 32u) return possibleBits(arg(0), depth + 1) >> arg(1).ImmediateU32(); + return UINT32_MAX; + case IrOpcode::ConvertU32U16: return 0xffffu; + case IrOpcode::ConvertU32U8: return 0xffu; + case IrOpcode::SelectU32: return possibleBits(arg(1), depth + 1) | possibleBits(arg(2), depth + 1); + default: return UINT32_MAX; + } +} + +bool readThroughUnusedBits(IrValue& value, std::size_t index, std::uint32_t demanded) { + bool changed = false; + for (unsigned step = 0; step < 16u; ++step) { + auto& operand = resolveArg(value, index); + if (operand.Type() != IrType::U32) break; + IrValue* next = nullptr; + for (std::size_t side = 0; side < 2u && next == nullptr; ++side) { + if (operand.Opcode() == IrOpcode::BitwiseAnd32) { + auto& mask = resolveArg(operand, side); + if (isImmediate(mask, IrType::U32) && (demanded & ~mask.ImmediateU32()) == 0u) next = &resolveArg(operand, 1u - side); + } else if (operand.Opcode() == IrOpcode::BitwiseOr32 || operand.Opcode() == IrOpcode::BitwiseXor32) { + if ((possibleBits(resolveArg(operand, side)) & demanded) == 0u) next = &resolveArg(operand, 1u - side); + } + } + if (next == nullptr) break; + value.ReplaceArgument(index, next); + changed = true; + } + return changed; +} + +bool maskedPart(IrValue& value, IrValue*& source, std::uint32_t& mask) { + const auto immediate = [&](IrValue& owner, std::size_t index, std::uint32_t& result) { + auto& argument = resolveArg(owner, index); + if (!isImmediate(argument, IrType::U32)) return false; + result = argument.ImmediateU32(); + return true; + }; + std::uint32_t first = 0; + std::uint32_t second = 0; + switch (value.Opcode()) { + case IrOpcode::BitwiseAnd32: + for (std::size_t side = 0; side < 2u; ++side) { + if (immediate(value, side, mask)) { + source = &resolveArg(value, 1u - side); + return !isImmediate(*source, IrType::U32); + } + } + return false; + case IrOpcode::BitFieldUExtract: + if (!immediate(value, 1, first) || !immediate(value, 2, second) || first != 0u || second == 0u || second >= 32u) return false; + source = &resolveArg(value, 0); + mask = (std::uint32_t{1} << second) - 1u; + return true; + case IrOpcode::ShiftLeftLogical32: { + if (!immediate(value, 1, first) || first == 0u || first >= 32u) return false; + auto& inner = resolveArg(value, 0); + std::uint32_t offset = 0; + std::uint32_t count = 0; + if (inner.Opcode() == IrOpcode::BitFieldUExtract && immediate(inner, 1, offset) && immediate(inner, 2, count) && offset == first && count > 0u && count <= 32u - offset) { + source = &resolveArg(inner, 0); + mask = (count == 32u - offset ? UINT32_MAX : (std::uint32_t{1} << (offset + count)) - 1u) & (UINT32_MAX << offset); + return true; + } + if (inner.Opcode() == IrOpcode::ShiftRightLogical32 && immediate(inner, 1, offset) && offset == first) { + source = &resolveArg(inner, 0); + mask = UINT32_MAX << first; + return true; + } + return false; + } + default: return false; + } +} + +bool mergeMaskedParts(IrProgram& program, IrValue& value) { + IrValue* left = nullptr; + IrValue* right = nullptr; + std::uint32_t leftMask = 0; + std::uint32_t rightMask = 0; + if (!maskedPart(resolveArg(value, 0), left, leftMask) || !maskedPart(resolveArg(value, 1), right, rightMask) || left != right) return false; + if ((leftMask | rightMask) == UINT32_MAX) { + replaceWith(value, *left); + return true; + } + if (value.Opcode() != IrOpcode::BitwiseOr32) return false; + IrBuilder builder(program); + value.ReplaceOpcode(IrOpcode::BitwiseAnd32); + value.ReplaceArgument(0, left); + value.ReplaceArgument(1, &builder.Constant(leftMask | rightMask)); + return true; +} + +bool foldRedundantMask(IrValue& value) { + for (std::size_t side = 0; side < 2u; ++side) { + auto& mask = resolveArg(value, side); + auto& operand = resolveArg(value, 1u - side); + if (!isImmediate(mask, IrType::U32)) continue; + const auto keep = mask.ImmediateU32(); + if ((possibleBits(operand) & ~keep) == 0u) { + replaceWith(value, operand); + return true; + } + if (operand.Opcode() == IrOpcode::BitwiseAnd32) { + for (std::size_t part = 0; part < 2u; ++part) { + auto& inner = resolveArg(operand, part); + if (isImmediate(inner, IrType::U32) && (keep & ~inner.ImmediateU32()) == 0u) { + value.ReplaceArgument(1u - side, &resolveArg(operand, 1u - part)); + foldRedundantMask(value); + return true; + } + } + } + if (operand.Opcode() == IrOpcode::BitwiseOr32) { + for (std::size_t part = 0; part < 2u; ++part) { + if ((possibleBits(resolveArg(operand, part)) & keep) == 0u) { + value.ReplaceArgument(1u - side, &resolveArg(operand, 1u - part)); + foldRedundantMask(value); + return true; + } + } + } + } + return false; +} + +bool feedsMultiply(const IrValue& value) { + const auto& uses = value.Uses(); + return std::any_of(uses.begin(), uses.end(), [](const IrValue* use) { return use->Opcode() == IrOpcode::IMul32; }); +} + bool isImmediateU32(const IrValue& value, std::uint32_t immediate) { return isImmediate(value, IrType::U32) && value.ImmediateU32() == immediate; } @@ -164,6 +312,295 @@ template bool foldU32Compare(IrBuilder& builder, IrValue& i return true; } +constexpr std::size_t ValueSetLimit = 64u; +constexpr std::uint32_t ValueSetDepth = 6u; + +using ValueSet = std::optional>; + +ValueSet normalizedSet(std::vector values) { + std::sort(values.begin(), values.end()); + values.erase(std::unique(values.begin(), values.end()), values.end()); + if (values.size() > ValueSetLimit) return std::nullopt; + return values; +} + +ValueSet rangeSet(std::uint32_t count) { + std::vector values(count); + for (std::uint32_t index = 0u; index < count; ++index) values[index] = index; + return values; +} + +template ValueSet mappedSet(const ValueSet& source, TFunction function) { + if (!source) return std::nullopt; + std::vector values; + values.reserve(source->size()); + for (const std::uint32_t value : *source) values.push_back(static_cast(function(value))); + return normalizedSet(std::move(values)); +} + +struct KnownBits { + std::uint32_t zero = 0u; + std::uint32_t one = 0u; +}; + +constexpr std::uint32_t KnownBitsDepth = 6u; +constexpr std::uint32_t FactBudget = 2048u; + +std::uint32_t lowMask(std::uint32_t bits) { + return bits >= 32u ? 0xffffffffu : (std::uint32_t{1} << bits) - 1u; +} + +KnownBits commonBits(const KnownBits& lhs, const KnownBits& rhs) { + return {lhs.zero & rhs.zero, lhs.one & rhs.one}; +} + +KnownBits knownBits(const IrValue& value, std::uint32_t depth, std::uint32_t& budget) { + if (isImmediate(value, IrType::U32)) return {~value.ImmediateU32(), value.ImmediateU32()}; + if (value.Type() != IrType::U32 || depth == 0u || budget == 0u) return {}; + --budget; + const auto argument = [&](std::size_t index) { return knownBits(resolveArg(value, index), depth - 1u, budget); }; + switch (value.Opcode()) { + case IrOpcode::ShiftLeftLogical32: { + auto& amount = resolveArg(value, 1); + if (!isImmediate(amount, IrType::U32) || amount.ImmediateU32() >= 32u) return {}; + const std::uint32_t shift = amount.ImmediateU32(); + const KnownBits source = argument(0); + return {(source.zero << shift) | lowMask(shift), source.one << shift}; + } + case IrOpcode::ShiftRightLogical32: { + auto& amount = resolveArg(value, 1); + if (!isImmediate(amount, IrType::U32) || amount.ImmediateU32() >= 32u) return {}; + const std::uint32_t shift = amount.ImmediateU32(); + const KnownBits source = argument(0); + return {(source.zero >> shift) | ~(0xffffffffu >> shift), source.one >> shift}; + } + case IrOpcode::BitFieldUExtract: { + auto& offset = resolveArg(value, 1); + auto& count = resolveArg(value, 2); + if (!isImmediate(offset, IrType::U32) || !isImmediate(count, IrType::U32) || count.ImmediateU32() > 32u || offset.ImmediateU32() > 32u - count.ImmediateU32()) return {}; + const std::uint32_t mask = lowMask(count.ImmediateU32()); + if (mask == 0u) return {0xffffffffu, 0u}; + const KnownBits source = argument(0); + const std::uint32_t shift = offset.ImmediateU32(); + return {((source.zero >> shift) & mask) | ~mask, (source.one >> shift) & mask}; + } + case IrOpcode::BitwiseAnd32: { + const KnownBits lhs = argument(0); + const KnownBits rhs = argument(1); + return {lhs.zero | rhs.zero, lhs.one & rhs.one}; + } + case IrOpcode::BitwiseOr32: { + const KnownBits lhs = argument(0); + const KnownBits rhs = argument(1); + return {lhs.zero & rhs.zero, lhs.one | rhs.one}; + } + case IrOpcode::BitwiseXor32: { + const KnownBits lhs = argument(0); + const KnownBits rhs = argument(1); + return {(lhs.zero & rhs.zero) | (lhs.one & rhs.one), (lhs.zero & rhs.one) | (lhs.one & rhs.zero)}; + } + case IrOpcode::IAdd32: { + const KnownBits lhs = argument(0); + const KnownBits rhs = argument(1); + const std::uint32_t mask = lowMask(static_cast(std::countr_one((lhs.zero | lhs.one) & (rhs.zero | rhs.one)))); + const std::uint32_t sum = lhs.one + rhs.one; + return {~sum & mask, sum & mask}; + } + case IrOpcode::IMul32: { + const KnownBits lhs = argument(0); + const KnownBits rhs = argument(1); + return {lowMask(static_cast(std::countr_one(lhs.zero) + std::countr_one(rhs.zero))), 0u}; + } + case IrOpcode::UMin32: { + const KnownBits lhs = argument(0); + const KnownBits rhs = argument(1); + KnownBits result = commonBits(lhs, rhs); + result.zero |= ~lowMask(static_cast(std::bit_width(std::min(~lhs.zero, ~rhs.zero)))); + return result; + } + case IrOpcode::SelectU32: + return commonBits(argument(1), argument(2)); + case IrOpcode::ReadFirstLane: + case IrOpcode::ReadLane: + return argument(0); + case IrOpcode::Phi: { + std::optional result; + for (std::size_t index = 0; index < value.ArgumentCount(); index++) { + const IrValue* incoming = value.Argument(index)->Resolve(); + if (incoming == &value) continue; + if (budget == 0u) return {}; + --budget; + const KnownBits bits = knownBits(*incoming, depth - 1u, budget); + result = result ? commonBits(*result, bits) : bits; + if (result->zero == 0u && result->one == 0u) return {}; + } + return result.value_or(KnownBits{}); + } + default: + return {}; + } +} + +ValueSet possibleValues(const IrValue& value, std::uint32_t depth, std::uint32_t& budget); + +bool immediateOperand(const IrValue& inst, const IrValue*& other, std::uint32_t& immediate) { + auto& lhs = resolveArg(inst, 0); + auto& rhs = resolveArg(inst, 1); + if (isImmediate(rhs, IrType::U32)) { + other = &lhs; + immediate = rhs.ImmediateU32(); + return true; + } + if (isImmediate(lhs, IrType::U32)) { + other = &rhs; + immediate = lhs.ImmediateU32(); + return true; + } + return false; +} + +ValueSet possibleValues(const IrValue& value, std::uint32_t depth, std::uint32_t& budget) { + if (isImmediate(value, IrType::U32)) return std::vector{value.ImmediateU32()}; + if (value.Type() != IrType::U32 || depth == 0u || budget == 0u) return std::nullopt; + --budget; + const IrValue* other = nullptr; + std::uint32_t immediate = 0u; + switch (value.Opcode()) { + case IrOpcode::ShiftRightLogical32: { + auto& amount = resolveArg(value, 1); + if (!isImmediate(amount, IrType::U32) || amount.ImmediateU32() >= 32u) return std::nullopt; + const std::uint32_t shift = amount.ImmediateU32(); + if (ValueSet source = possibleValues(resolveArg(value, 0), depth - 1u, budget)) return mappedSet(source, [shift](std::uint32_t v) { return v >> shift; }); + if (shift < 26u) return std::nullopt; + return rangeSet(std::uint32_t{1} << (32u - shift)); + } + case IrOpcode::ShiftLeftLogical32: { + auto& amount = resolveArg(value, 1); + if (!isImmediate(amount, IrType::U32) || amount.ImmediateU32() >= 32u) return std::nullopt; + const std::uint32_t shift = amount.ImmediateU32(); + return mappedSet(possibleValues(resolveArg(value, 0), depth - 1u, budget), [shift](std::uint32_t v) { return v << shift; }); + } + case IrOpcode::BitFieldUExtract: { + auto& offset = resolveArg(value, 1); + auto& count = resolveArg(value, 2); + if (!isImmediate(offset, IrType::U32) || !isImmediate(count, IrType::U32) || count.ImmediateU32() == 0u || count.ImmediateU32() > 6u || offset.ImmediateU32() > 32u - count.ImmediateU32()) return std::nullopt; + const std::uint32_t shift = offset.ImmediateU32(); + const std::uint32_t mask = (std::uint32_t{1} << count.ImmediateU32()) - 1u; + if (ValueSet source = possibleValues(resolveArg(value, 0), depth - 1u, budget)) return mappedSet(source, [shift, mask](std::uint32_t v) { return (v >> shift) & mask; }); + return rangeSet(mask + 1u); + } + case IrOpcode::BitwiseAnd32: { + if (!immediateOperand(value, other, immediate)) return std::nullopt; + if (ValueSet source = possibleValues(*other, depth - 1u, budget)) return mappedSet(source, [immediate](std::uint32_t v) { return v & immediate; }); + const KnownBits bits = knownBits(*other, depth - 1u, budget); + const std::uint32_t mask = immediate & ~(bits.zero | bits.one); + const std::uint32_t fixed = immediate & bits.one; + if (std::popcount(mask) > 6) return std::nullopt; + std::vector values; + std::uint32_t subset = 0u; + do { + values.push_back(subset | fixed); + subset = (subset - mask) & mask; + } while (subset != 0u); + return normalizedSet(std::move(values)); + } + case IrOpcode::BitwiseOr32: + if (!immediateOperand(value, other, immediate)) return std::nullopt; + return mappedSet(possibleValues(*other, depth - 1u, budget), [immediate](std::uint32_t v) { return v | immediate; }); + case IrOpcode::BitwiseXor32: + if (!immediateOperand(value, other, immediate)) return std::nullopt; + return mappedSet(possibleValues(*other, depth - 1u, budget), [immediate](std::uint32_t v) { return v ^ immediate; }); + case IrOpcode::IMul32: + if (!immediateOperand(value, other, immediate)) return std::nullopt; + return mappedSet(possibleValues(*other, depth - 1u, budget), [immediate](std::uint32_t v) { return v * immediate; }); + case IrOpcode::IAdd32: { + const ValueSet lhs = possibleValues(resolveArg(value, 0), depth - 1u, budget); + const ValueSet rhs = lhs ? possibleValues(resolveArg(value, 1), depth - 1u, budget) : std::nullopt; + if (!rhs || lhs->size() + rhs->size() - 1u > ValueSetLimit) return std::nullopt; + std::vector values; + for (const std::uint32_t left : *lhs) { + for (const std::uint32_t right : *rhs) values.push_back(left + right); + } + return normalizedSet(std::move(values)); + } + case IrOpcode::UMin32: { + if (!immediateOperand(value, other, immediate)) return std::nullopt; + const std::uint32_t limit = immediate; + if (ValueSet source = possibleValues(*other, depth - 1u, budget)) return mappedSet(source, [limit](std::uint32_t v) { return std::min(v, limit); }); + if (limit >= ValueSetLimit) return std::nullopt; + return rangeSet(limit + 1u); + } + case IrOpcode::SelectU32: { + ValueSet lhs = possibleValues(resolveArg(value, 1), depth - 1u, budget); + const ValueSet rhs = lhs ? possibleValues(resolveArg(value, 2), depth - 1u, budget) : std::nullopt; + if (!rhs) return std::nullopt; + lhs->insert(lhs->end(), rhs->begin(), rhs->end()); + return normalizedSet(std::move(*lhs)); + } + default: + return std::nullopt; + } +} + +class OperandFacts { +public: + KnownBits Bits(const IrValue& value) { + if (const auto found = bits.find(&value); found != bits.end()) return found->second; + std::uint32_t budget = FactBudget; + return bits.emplace(&value, knownBits(value, KnownBitsDepth, budget)).first->second; + } + + const ValueSet& Values(const IrValue& value) { + if (const auto found = values.find(&value); found != values.end()) return found->second; + std::uint32_t budget = FactBudget; + return values.emplace(&value, possibleValues(value, ValueSetDepth, budget)).first->second; + } + +private: + std::unordered_map bits; + std::unordered_map values; +}; + +bool foldEqualityByValueSet(IrBuilder& builder, IrValue& inst, bool equal, OperandFacts& facts) { + const IrValue* other = nullptr; + std::uint32_t immediate = 0u; + if (!immediateOperand(inst, other, immediate)) return false; + const KnownBits bits = facts.Bits(*other); + if (((immediate & bits.zero) | (~immediate & bits.one)) != 0u) { + replaceWith(inst, builder.ConstantBool(!equal)); + return true; + } + const ValueSet& values = facts.Values(*other); + if (!values) return false; + const bool contains = std::binary_search(values->begin(), values->end(), immediate); + if (contains && values->size() != 1u) return false; + replaceWith(inst, builder.ConstantBool(contains == equal)); + return true; +} + +bool foldULessThanByRange(IrBuilder& builder, IrValue& inst, OperandFacts& facts) { + auto& lhs = resolveArg(inst, 0); + auto& rhs = resolveArg(inst, 1); + if (!isImmediate(rhs, IrType::U32) || isImmediate(lhs, IrType::U32)) return false; + const std::uint32_t limit = rhs.ImmediateU32(); + const KnownBits bits = facts.Bits(lhs); + std::uint32_t lowest = bits.one; + std::uint32_t highest = ~bits.zero; + if (const ValueSet& values = facts.Values(lhs); values && !values->empty()) { + lowest = std::max(lowest, values->front()); + highest = std::min(highest, values->back()); + } + if (highest < limit) { + replaceWith(inst, builder.ConstantBool(true)); + return true; + } + if (lowest >= limit) { + replaceWith(inst, builder.ConstantBool(false)); + return true; + } + return false; +} + template bool foldU64Compare(IrBuilder& builder, IrValue& inst, TFunction function) { auto& lhs = resolveArg(inst, 0); auto& rhs = resolveArg(inst, 1); @@ -200,17 +637,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 +734,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) { @@ -338,6 +782,10 @@ IrValue& insertBuiltin(IrProgram& program, IrBuilder& builder, IrValue& next, St return insertBefore(program, next, IrOpcode::GetBuiltin, builder.Constant(static_cast(kind)), builder.Constant(0u)); } +IrValue& sampleNumber(IrProgram& program, IrBuilder& builder, IrValue& next) { + return program.Metadata().pixelIteratesSamples ? insertBuiltin(program, builder, next, StageInputKind::SampleId) : builder.Constant(0u); +} + void lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancillary) { if (!isPackedAncillary(ancillary)) { return; @@ -386,7 +834,7 @@ void lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancil const auto field = [&](std::size_t index) -> IrValue& { if (fields[index] == nullptr && index == 1u) fields[index] = &insertBefore(program, ancillary, IrOpcode::IMul32, insertBuiltin(program, builder, ancillary, StageInputKind::Layer), insertBuiltin(program, builder, ancillary, StageInputKind::LayerExported)); if (fields[index] == nullptr) { - fields[index] = &insertBuiltin(program, builder, ancillary, StageInputKind::SampleId); + fields[index] = &sampleNumber(program, builder, ancillary); } return *fields[index]; }; @@ -411,9 +859,7 @@ void lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancil } } -} // namespace - -bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { +bool tryFoldValue(IrProgram& program, IrValue& value, OperandFacts& facts) { IrBuilder builder(program); switch (value.Opcode()) { case IrOpcode::Phi: return foldPhi(value); @@ -438,6 +884,20 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { } case IrOpcode::BitFieldUExtract: case IrOpcode::BitFieldSExtract: { + if (isImmediate(resolveArg(value, 1), IrType::U32) && isImmediate(resolveArg(value, 2), IrType::U32)) { + const auto fieldOffset = resolveArg(value, 1).ImmediateU32(); + const auto fieldCount = resolveArg(value, 2).ImmediateU32(); + if (fieldCount > 0u && fieldCount < 32u && fieldOffset < 32u && fieldCount <= 32u - fieldOffset) readThroughUnusedBits(value, 0, ((std::uint32_t{1} << fieldCount) - 1u) << fieldOffset); + } + if (resolveArg(value, 0).Opcode() == IrOpcode::ShiftLeftLogical32 && isImmediate(resolveArg(value, 1), IrType::U32) && isImmediate(resolveArg(value, 2), IrType::U32)) { + auto& shifted = resolveArg(value, 0); + auto& amount = resolveArg(shifted, 1); + const auto fieldOffset = resolveArg(value, 1).ImmediateU32(); + if (isImmediate(amount, IrType::U32) && amount.ImmediateU32() > 0u && amount.ImmediateU32() < 32u && fieldOffset >= amount.ImmediateU32() && fieldOffset < 32u) { + value.ReplaceArgument(1, &builder.Constant(fieldOffset - amount.ImmediateU32())); + value.ReplaceArgument(0, &resolveArg(shifted, 0)); + } + } auto& source = resolveArg(value, 0); auto& offset = resolveArg(value, 1); auto& count = resolveArg(value, 2); @@ -448,6 +908,10 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { return true; } } + if (value.Opcode() == IrOpcode::BitFieldUExtract && isImmediateU32(offset, 0u) && isImmediate(count, IrType::U32) && count.ImmediateU32() < 32u && !isImmediate(source, IrType::U32) && (possibleBits(source) >> count.ImmediateU32()) == 0u && !feedsMultiply(value)) { + replaceWith(value, source); + return true; + } if (!isImmediate(source, IrType::U32) || !isImmediate(offset, IrType::U32) || !isImmediate(count, IrType::U32) || offset.ImmediateU32() > 32u || count.ImmediateU32() > 32u - offset.ImmediateU32()) { return false; } @@ -470,6 +934,7 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { case IrOpcode::BitCastU32F32: return foldBitCast(builder, value, IrOpcode::BitCastF32U32); case IrOpcode::BitCastF32U32: return foldBitCast(builder, value, IrOpcode::BitCastU32F32); case IrOpcode::ConvertU16U32: { + readThroughUnusedBits(value, 0, 0xffffu); auto& operand = resolveArg(value, 0); if (!isImmediate(operand, IrType::U32)) { return false; @@ -607,6 +1072,7 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { if (foldU32(builder, value, [](std::uint32_t a, std::uint32_t b) { return a << (b & 31u); })) { return true; } + if (isImmediate(resolveArg(value, 1), IrType::U32)) readThroughUnusedBits(value, 0, UINT32_MAX >> (resolveArg(value, 1).ImmediateU32() & 31u)); auto& shift = resolveArg(value, 1); if (isImmediate(shift, IrType::U32) && (shift.ImmediateU32() & 31u) == 0u) { replaceWith(value, resolveArg(value, 0)); @@ -623,9 +1089,26 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { replaceWith(value, resolveArg(value, 0)); return true; } + if (!isImmediate(shift, IrType::U32)) return false; + const auto amount = shift.ImmediateU32() & 31u; + readThroughUnusedBits(value, 0, UINT32_MAX << amount); + auto& shifted = resolveArg(value, 0); + if (shifted.Opcode() == IrOpcode::ShiftLeftLogical32 && isImmediate(resolveArg(shifted, 1), IrType::U32) && (resolveArg(shifted, 1).ImmediateU32() & 31u) == amount) { + auto& inner = resolveArg(shifted, 0); + if ((possibleBits(inner) & ~(UINT32_MAX >> amount)) == 0u) { + replaceWith(value, inner); + return true; + } + value.ReplaceOpcode(IrOpcode::BitwiseAnd32); + value.ReplaceArgument(0, &inner); + value.ReplaceArgument(1, &builder.Constant(UINT32_MAX >> amount)); + foldRedundantMask(value); + return true; + } return false; } case IrOpcode::ShiftRightArithmetic32: { + if (isImmediate(resolveArg(value, 1), IrType::U32)) readThroughUnusedBits(value, 0, UINT32_MAX << (resolveArg(value, 1).ImmediateU32() & 31u)); return foldU32(builder, value, [](std::uint32_t a, std::uint32_t b) { return static_cast(std::bit_cast(a) >> (b & 31u)); }); } case IrOpcode::ShiftLeftLogical64: { @@ -657,7 +1140,14 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { if (foldU32(builder, value, [](std::uint32_t a, std::uint32_t b) { return a & b; })) { return true; } - return replaceBinaryIdentity(value, IrType::U32, 0xffffffffu); + if (replaceBinaryIdentity(value, IrType::U32, 0xffffffffu)) return true; + if (foldRedundantMask(value)) return true; + bool changed = false; + for (std::size_t side = 0; side < 2u; ++side) { + if (isImmediate(resolveArg(value, side), IrType::U32)) changed |= readThroughUnusedBits(value, 1u - side, resolveArg(value, side).ImmediateU32()); + } + if (changed) foldRedundantMask(value); + return changed; } case IrOpcode::BitwiseAnd64: { if (foldU64(builder, value, [](std::uint64_t a, std::uint64_t b) { return a & b; })) { @@ -671,7 +1161,8 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { if (foldU32(builder, value, [opcode](std::uint32_t a, std::uint32_t b) { return opcode == IrOpcode::BitwiseOr32 ? a | b : a ^ b; })) { return true; } - return replaceBinaryIdentity(value, IrType::U32, 0u); + if (replaceBinaryIdentity(value, IrType::U32, 0u)) return true; + return opcode == IrOpcode::BitwiseOr32 && mergeMaskedParts(program, value); } case IrOpcode::BitwiseNot32: { auto& operand = resolveArg(value, 0); @@ -729,9 +1220,9 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { case IrOpcode::UMax32: { return foldU32(builder, value, [](std::uint32_t a, std::uint32_t b) { return std::max(a, b); }); } - case IrOpcode::IEqual32: return foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a == b; }); - case IrOpcode::INotEqual32: return foldConstantBitTest(builder, value) || foldStartingQuadBitTest(program, builder, value) || foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a != b; }); - case IrOpcode::ULessThan32: return foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a < b; }); + case IrOpcode::IEqual32: return foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a == b; }) || foldEqualityByValueSet(builder, value, true, facts); + case IrOpcode::INotEqual32: return foldConstantBitTest(builder, value) || foldStartingQuadBitTest(program, builder, value) || foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a != b; }) || foldEqualityByValueSet(builder, value, false, facts); + case IrOpcode::ULessThan32: return foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a < b; }) || foldULessThanByRange(builder, value, facts); case IrOpcode::ULessThanEqual32: return foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a <= b; }); case IrOpcode::UGreaterThan32: return foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a > b; }); case IrOpcode::UGreaterThanEqual32: return foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a >= b; }); @@ -822,10 +1313,30 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { } } +} // namespace + void ConstantFolder::Fold(IrProgram& program, std::span blocks) const { + OperandFacts facts; for (IrBlock* block : blocks) { for (IrValue* inst : block->Instructions()) { - const auto success = tryFoldValue(program, *inst); + const auto success = tryFoldValue(program, *inst, facts); + } + } + std::vector phis; + for (IrBlock* block : blocks) { + for (IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::Phi) { + phis.push_back(inst); + } + } + } + bool folded = true; + while (folded) { + folded = false; + for (IrValue* phi : phis) { + if (phi->Opcode() == IrOpcode::Phi && foldPhi(*phi)) { + folded = true; + } } } std::vector words; @@ -846,6 +1357,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()) { @@ -858,7 +1385,7 @@ bool ConstantFolder::ComposePackedAncillary(IrProgram& program) const { IrBuilder builder(program); for (IrValue* word : words) { IrValue& primitive = insertBuiltin(program, builder, *word, StageInputKind::RasterPrimitive); - IrValue& sample = insertBefore(program, *word, IrOpcode::ShiftLeftLogical32, insertBuiltin(program, builder, *word, StageInputKind::SampleId), builder.Constant(8u)); + IrValue& sample = insertBefore(program, *word, IrOpcode::ShiftLeftLogical32, sampleNumber(program, builder, *word), builder.Constant(8u)); IrValue& exportedLayer = insertBefore(program, *word, IrOpcode::IMul32, insertBuiltin(program, builder, *word, StageInputKind::Layer), insertBuiltin(program, builder, *word, StageInputKind::LayerExported)); IrValue& layer = insertBefore(program, *word, IrOpcode::ShiftLeftLogical32, exportedLayer, builder.Constant(16u)); IrValue& fields = insertBefore(program, *word, IrOpcode::BitwiseOr32, sample, layer); 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 6081365137..170b97d6b8 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -35,9 +37,15 @@ struct BufferWrittenCounts { BufferWrittenCounts bufferWrittenCounts; DescriptorKind PhysicalKindFor(DescriptorBindingKind kind) { - if (kind == DescriptorBindingKind::Samplers) { + if (kind == DescriptorBindingKind::Samplers || kind == DescriptorBindingKind::SamplerTable) { return DescriptorKind::Sampler; } + 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; @@ -70,6 +78,18 @@ DescriptorRole RoleFor(DescriptorBindingKind kind) { if (kind == DescriptorBindingKind::ShaderData) { return DescriptorRole::ShaderData; } + if (kind == DescriptorBindingKind::ImageTable) { + return DescriptorRole::ImageTable; + } + if (kind == DescriptorBindingKind::SamplerTable) { + return DescriptorRole::SamplerTable; + } + if (kind == DescriptorBindingKind::ImageTableMap) { + return DescriptorRole::ImageTableMap; + } + if (kind == DescriptorBindingKind::ImageStorageTable) { + return DescriptorRole::ImageStorageTable; + } if (ImageBindingResourceClass(kind) != ImageResourceClass::None) { return DescriptorRole::GuestImages; } @@ -79,7 +99,7 @@ DescriptorRole RoleFor(DescriptorBindingKind kind) { DescriptorImageShape ImageShapeForResource(const ImageResource& image) { const RdnaImageDimensionInfo& info = RdnaImageDimensionInfoFor(image.dimension); if (info.multisampled != 0u) { - fail("DescriptorBindingBuilder::Populate multisampled image resources have no descriptor image shape"); + return info.arrayed != 0u ? DescriptorImageShape::Image2DMultisampleArray : DescriptorImageShape::Image2DMultisample; } if (info.spirvDimension == spv::Dim1D) { return info.arrayed != 0u ? DescriptorImageShape::Image1DArray : DescriptorImageShape::Image1D; @@ -161,13 +181,24 @@ struct UnnormalizedProof { std::vector samplers; std::vector images; std::vector imagesAtBaseLevel; + std::vector samplersIgnoreCoordinates; }; UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapshot& snapshot) { - UnnormalizedProof proof{std::vector(info.samplers.size()), std::vector(info.images.size()), std::vector(info.images.size())}; + UnnormalizedProof proof{std::vector(info.samplers.size()), std::vector(info.images.size()), std::vector(info.images.size()), std::vector(info.samplers.size())}; for (std::uint32_t r = 0; r < info.samplers.size(); r++) { if (snapshot.samplers.at(r).dwordCount != 4u) fail("sampler descriptor must contain four dwords"); const auto& sampler = info.samplers[r]; + if (sampler.table != NoTable) continue; + for (const auto& pair : info.sampledPairs) { + if (pair.sampler != r) continue; + const auto& image = info.images.at(pair.image); + if (!image.singleMipPlaneBiasCompatible || image.dimension != RdnaImageDimension::Dim2D) continue; + const auto mode = ResourceMaterializer::RuntimeImageMode(image, snapshot.images.at(pair.image), info.runtimeImageModes.at(pair.image)); + if (info.runtimeImageModes.at(pair.image).at(mode).dimension != RdnaImageDimension::Dim3D) continue; + const auto& words = snapshot.samplers.at(r).dwords; + if ((words[0] & ForceUnnormalizedBit) != 0u || ((words[0] >> 29u) & 3u) > 2u || ((words[2] >> 20u) & 0xfu) != 0u || ((words[2] >> 26u) & 3u) > 1u) fail("2D biased sampling of a one-slice volume requires a normalized point sampler"); + } if ((snapshot.samplers[r].dwords[0] & ForceUnnormalizedBit) == 0u) { if (sampler.foldTexelOffsetsOnly) { fail("DescriptorBindingBuilder::Populate normalized guest sampler is used with a texel offset that is not a constant, which image sampling takes only with VK_KHR_maintenance8 and shaderImageGatherExtended"); @@ -181,18 +212,34 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh if (sampler.depthCompare) { failUnnormalized("is used with depth comparison"); } + bool sampled = false; + const char* nullMismatch = nullptr; for (const auto& pair : info.sampledPairs) { if (pair.sampler != r) { continue; } const auto& base = info.images.at(pair.image); + if (base.table != NoTable) { + failUnnormalized("samples an image selected at run time"); + } const auto& image = info.runtimeImageModes.at(pair.image).at(ResourceMaterializer::RuntimeImageMode(base, snapshot.images.at(pair.image), info.runtimeImageModes.at(pair.image))); - if (const auto* reason = ResourceMaterializer::UnnormalizedSampleMismatch(image)) { + const auto* reason = ResourceMaterializer::UnnormalizedSampleMismatch(image); + if (reason != nullptr && ResourceMaterializer::NullImageDescriptor(base, snapshot.images.at(pair.image))) { + if (nullMismatch == nullptr) nullMismatch = reason; + continue; + } + if (reason != nullptr) { failUnnormalized(reason); } if (image.constantSwizzle) continue; + sampled = true; proof.images[pair.image] = true; } + if (!sampled && nullMismatch != nullptr) { + proof.samplersIgnoreCoordinates[r] = true; + continue; + } + if (nullMismatch != nullptr) failUnnormalized(nullMismatch); proof.samplers[r] = true; } for (std::uint32_t resource = 0; resource < info.images.size(); resource++) { @@ -201,7 +248,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; } @@ -233,9 +280,12 @@ std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, st } +bool ReducesBetweenTexels(std::uint32_t word0, std::uint32_t filter) { + return ((word0 >> 29u) & 3u) != 0u && (((filter >> 20u) & 0xfu) != 0u || ((filter >> 26u) & 3u) == 2u); +} + std::uint32_t PointFilteredSamplerWord(std::uint32_t word0, std::uint32_t filter) { - const bool reduced = ((word0 >> 29u) & 3u) != 0u; - if (reduced && (((filter >> 20u) & 0xfu) != 0u || ((filter >> 26u) & 3u) == 2u)) { + if (ReducesBetweenTexels(word0, filter)) { fail("DescriptorBindingBuilder: a min or max reduction sampler that filters between texels or mip levels samples an image that needs point filtering (sint, converted or depth-bits format), which is not implemented"); } const bool mipmapped = ((filter >> 26u) & 3u) != 0u; @@ -248,7 +298,8 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads, std::span exportMappings) const { auto plan = Prepare(allocation.layout, info, stage, snapshot, exportMappings); - Populate(allocation, allocation, plan, userDataBase, snapshot, partialThreads); + const auto tables = ResourceMaterializer::ResolveImageTables(info, snapshot); + Populate(allocation, allocation, plan, userDataBase, snapshot, partialThreads, &tables); allocation.specialization = std::move(plan.specialization); } @@ -263,14 +314,17 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l } std::vector compareStates(info.images.size()); for (std::uint32_t index = 0; index < info.images.size(); ++index) { - if (!info.images[index].depthCompare) continue; + const bool convertedSample = !info.images[index].srgbDecodeCompatible && index < info.runtimeImageModes.size() && std::ranges::any_of(info.runtimeImageModes.at(index), [](const ImageResource& mode) { return mode.conversionFormat == IrBufferFormat::Format11_11_10UNorm; }); + if (!info.images[index].depthCompare && !convertedSample) continue; const auto compare = compareStates[index] = ResourceMaterializer::EmulatedCompareState(info, snapshot, index); const auto first = PipelineSpecialization::CompareBase + index * PipelineSpecialization::CompareWords; const std::array values{EmulatedCompare::Function(compare), (compare & EmulatedCompare::Linear) != 0u, EmulatedCompare::AddressX(compare), EmulatedCompare::AddressY(compare), EmulatedCompare::Reference(compare), (compare & EmulatedCompare::BorderWhite) != 0u}; for (std::uint32_t word = 0; word < values.size() && !registrationModules; ++word) plan.specialization.push_back({first + word, values[word]}); } std::vector imageModes(info.images.size()); - for (std::size_t index = 0; index < info.images.size(); ++index) imageModes[index] = ResourceMaterializer::RuntimeImageMode(info.images[index], snapshot.images.at(index), info.runtimeImageModes.at(index)); + for (std::size_t index = 0; index < info.images.size(); ++index) { + if (info.images[index].table == NoTable) imageModes[index] = ResourceMaterializer::RuntimeImageMode(info.images[index], snapshot.images.at(index), info.runtimeImageModes.at(index)); + } for (std::uint32_t index = 0; index < info.buffers.size(); ++index) { const auto& descriptor = snapshot.buffers.at(index); if (descriptor.dwordCount != 4u) fail("buffer specialization requires four descriptor words"); @@ -279,7 +333,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"); @@ -301,10 +354,26 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l } } for (std::uint32_t index = 0; index < info.images.size() && !registrationModules; ++index) { - if (info.images[index].indirectRoot != ImageResource::NoIndirectImage) continue; + if (info.images[index].table != NoTable) continue; const auto first = PipelineSpecialization::ImageBase + index * PipelineSpecialization::ImageWords; plan.specialization.push_back({first, imageModes[index]}); for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({first + 1u + component, (snapshot.images.at(index).dwords[3] >> (component * 3u)) & 7u}); + const auto& image = info.images[index]; + if (image.resourceClass != ImageResourceClass::Storage || !image.written || image.atomic) continue; + const auto code = (snapshot.images.at(index).dwords[1] >> 20u) & 0x1ffu; + const auto format = code >= 1u && code <= 77u ? GetFormatInfo(static_cast(code)) : SpirvBufferFormatInfo{}; + const bool snorm = format.type == SpirvFormatComponentType::Snorm; + const auto bits = format.componentBits[0]; + const bool uniform = format.componentCount != 0u && !format.packedBitfield && bits >= 2u && bits <= 16u && std::all_of(format.componentBits, format.componentBits + format.componentCount, [&](std::uint32_t width) { return width == bits; }); + const bool quantized = uniform && (format.type == SpirvFormatComponentType::Unorm || snorm); + const auto scale = quantized ? static_cast((1u << (snorm ? bits - 1u : bits)) - 1u) : 1.0f; + auto reciprocal = 1.0f / scale; + while (static_cast(reciprocal) * static_cast(scale) < 1.0) reciprocal = std::nextafter(reciprocal, 2.0f); + const auto quantize = PipelineSpecialization::StoreQuantizeBase + index * PipelineSpecialization::StoreQuantizeWords; + plan.specialization.push_back({quantize, quantized ? 1u : 0u}); + plan.specialization.push_back({quantize + 1u, std::bit_cast(scale)}); + plan.specialization.push_back({quantize + 2u, std::bit_cast(reciprocal)}); + plan.specialization.push_back({quantize + 3u, quantized && snorm ? 1u : 0u}); } for (std::uint32_t target = 0; target < exportMappings.size() && !registrationModules; ++target) { for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({PipelineSpecialization::ExportBase + target * 4u + component, (exportMappings[target] >> (component * 2u)) & 3u}); @@ -312,7 +381,8 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l std::vector samplerModes(info.samplers.size(), 0u); std::vector samplerIntegerModes(info.samplers.size(), 0u); std::vector samplerFloatModes(info.samplers.size(), 0u); - const auto samplerMode = [](const ImageResource& image) { return !image.constantSwizzle && (image.numericClass == IrTextureNumericClass::Sint || image.conversionFormat != IrBufferFormat::Invalid || image.depthBits) ? 2u : 1u; }; + std::vector directSamplerModes(info.samplers.size(), 0u); + const auto samplerMode = [](const ImageResource& image) { return !image.constantSwizzle && (image.numericClass == IrTextureNumericClass::Uint || image.numericClass == IrTextureNumericClass::Sint || image.conversionFormat != IrBufferFormat::Invalid || image.depthBits) ? 2u : 1u; }; const auto pairSampler = [&](std::uint32_t sampler, const ImageResource& image) { const auto mode = samplerMode(image); samplerModes.at(sampler) |= mode; @@ -320,10 +390,12 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l }; for (const auto& pair : info.sampledPairs) { const auto& image = info.images.at(pair.image); - if (image.indirectRoot == ImageResource::NoIndirectImage) pairSampler(pair.sampler, info.runtimeImageModes.at(pair.image).at(imageModes[pair.image])); - else { - const auto& root = info.images.at(image.indirectRoot); - for (const auto slot : root.indirectResources) pairSampler(pair.sampler, info.runtimeImageModes.at(slot).at(imageModes[slot])); + if (image.table == NoTable) { + const auto mode = samplerMode(info.runtimeImageModes.at(pair.image).at(imageModes[pair.image])); + pairSampler(pair.sampler, info.runtimeImageModes.at(pair.image).at(imageModes[pair.image])); + directSamplerModes.at(pair.sampler) |= mode; + } else { + samplerModes.at(pair.sampler) |= 3u; } } for (std::uint32_t index = 0; index < info.images.size(); ++index) { @@ -334,19 +406,6 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l if (last < first || last - first >= RuntimeAbi::StorageMipSlots) fail("invalid dynamic storage mip range"); if (!registrationModules) plan.specialization.push_back({PipelineSpecialization::MipCountBase + index, last - first + 1u}); } - for (std::uint32_t resource = 0; resource < info.images.size(); ++resource) { - const auto& image = info.images[resource]; - if (image.indirectRoot == ImageResource::NoIndirectImage) continue; - const auto& modes = info.runtimeImageModes.at(resource); - if (modes.size() > PipelineSpecialization::ImageModeStride) fail("runtime image mode specialization capacity exceeded"); - std::vector active(modes.size(), false); - for (const auto slot : info.images.at(image.indirectRoot).indirectResources) { - const auto mode = ResourceMaterializer::RuntimeImageMode(image, snapshot.images.at(slot), modes); - if (mode != imageModes[slot]) fail("indirect image mode numbering disagrees with its root"); - active[mode] = true; - } - for (std::uint32_t mode = 0; mode < modes.size() && !registrationModules; ++mode) plan.specialization.push_back({PipelineSpecialization::ImageModeBase + resource * PipelineSpecialization::ImageModeStride + mode, active[mode] ? 1u : 0u}); - } plan.bindings.reserve(layout.descriptors.size()); for (const auto& logical : layout.descriptors) { const bool imageHeap = ImageBindingResourceClass(logical.kind) != ImageResourceClass::None; @@ -361,12 +420,13 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l bool active = true; if (imageHeap) { const auto& image = info.images.at(resource); + if (image.table != NoTable) fail("an image table resource was placed in a typed heap"); const auto& mode = info.runtimeImageModes.at(resource).at(imageModes[resource]); mip = previous == resource ? mip + 1u : 0u; previous = resource; { const auto word = snapshot.images.at(resource).dwords[3]; - active = DescriptorBindingForImage(mode) == logical.kind && mode.packedFormat != IrBufferFormat::Fmask8_S2_F1 && !mode.constantSwizzle && (image.mipMode != ImageMipMode::DynamicStorage || mip <= ((word >> 16u) & 0xfu) - ((word >> 12u) & 0xfu)); + active = DescriptorBindingForImage(mode) == logical.kind && !mode.constantSwizzle && (image.mipMode != ImageMipMode::DynamicStorage || mip <= ((word >> 16u) & 0xfu) - ((word >> 12u) & 0xfu)); } } else if (samplerHeap) { active = (samplerModes.at(resource) & (1u << (element & 1u))) != 0u; @@ -393,6 +453,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l physical.bufferAtomic.push_back(buffer.atomic); physical.bufferWritten.push_back(buffer.written || buffer.atomic); physical.bufferRead.push_back(buffer.read); + physical.bufferExtent.push_back(buffer.maxByteExtent); if (buffer.written || buffer.atomic) ++entry.writtenBuffers; else ++entry.readOnlyBuffers; } @@ -413,18 +474,6 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l physical.imageUnnormalizedBaseLevel.push_back(unnormalized.imagesAtBaseLevel.at(resource)); physical.imageSamplers.push_back(0u); physical.imageAtomic64.push_back(image.atomic64); - if (layout.runtimeImageCount == 0u || image.indirectRoot == ImageResource::NoIndirectImage || inactive[element]) continue; - if (resource >= layout.runtimeImageCount) fail("runtime image metadata exceeds compact layout"); - const auto& descriptor = snapshot.images.at(resource); - RuntimeAbi::ResourceMetadata metadata{}; - if (descriptor.dwordCount != metadata.descriptor.size()) fail("invalid runtime image descriptor width"); - metadata.binding = static_cast(compact.kind); - metadata.firstElement = static_cast(element); - metadata.elementCount = 1u; - metadata.flags = imageModes[resource] << 1u | (registrationModules ? (compareStates[resource] & RuntimeAbi::CompareStateMask) << RuntimeAbi::CompareStateShift : 0u); - if (descriptor.dwords[0] == 0u && (descriptor.dwords[1] & 0xffu) == 0u) metadata.flags |= 1u; - const auto offset = layout.ImageMetadataDword() + resource * static_cast(sizeof(metadata) / sizeof(std::uint32_t)); - plan.imageMetadata.push_back({resource, offset, metadata}); } } else if (physical.role == DescriptorRole::GuestSamplers) { for (std::uint32_t element = 0; element < compact.resources.size(); ++element) { @@ -433,9 +482,12 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l for (const auto& pair : info.sampledPairs) if (pair.sampler == resource) compare |= info.images.at(pair.image).depthCompare && compareStates.at(pair.image) == 0u; physical.samplerDepthCompare.push_back(compare); physical.samplerUnnormalized.push_back(unnormalized.samplers.at(resource)); + physical.samplerCoordinatesIgnored.push_back(unnormalized.samplersIgnoreCoordinates.at(resource)); const auto mode = 1u << (originals[element] & 1u); physical.samplerIntegerBorder.push_back((samplerIntegerModes.at(resource) & mode) != 0u && (samplerFloatModes.at(resource) & mode) == 0u); - if ((originals[element] & 1u) != 0u && !inactive[element]) entry.samplerFilterElements.push_back(element); + const auto& words = snapshot.samplers.at(resource).dwords; + const bool tablePointOnly = (directSamplerModes.at(resource) & 2u) == 0u; + if ((originals[element] & 1u) != 0u && !inactive[element] && !(tablePointOnly && ReducesBetweenTexels(words[0], words[2]))) entry.samplerFilterElements.push_back(element); } } else if (physical.role == DescriptorRole::ShaderData && layout.UsesPushData()) { fail("DescriptorBindingBuilder::Populate shader-data binding must not exist when push data is used"); @@ -446,7 +498,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l plan.bindings.push_back(std::move(entry)); } for (std::uint32_t resource = 0; resource < info.images.size() && registrationModules && layout.runtimeImageCount != 0u; ++resource) { - if (info.images[resource].indirectRoot != ImageResource::NoIndirectImage) continue; + if (info.images[resource].table != NoTable) continue; if (resource >= layout.runtimeImageCount) fail("runtime image metadata exceeds compact layout"); const auto& descriptor = snapshot.images.at(resource); RuntimeAbi::ResourceMetadata metadata{}; @@ -474,10 +526,13 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l for (std::size_t element = 0; element < binding.resources.size(); ++element) { const auto resource = binding.resources[element]; for (const auto& pair : info.sampledPairs) { - if (pair.image == resource || pair.image == info.images.at(resource).indirectRoot) binding.descriptor.imageSamplers[element] |= samplerMasks.at(pair.sampler); + if (pair.image == resource) binding.descriptor.imageSamplers[element] |= samplerMasks.at(pair.sampler); } } } + for (auto& binding : plan.bindings) { + if (binding.descriptor.role == DescriptorRole::ImageTable || binding.descriptor.role == DescriptorRole::ImageStorageTable) binding.tableSamplerMasks = samplerMasks; + } return plan; } @@ -495,6 +550,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Select(const DescriptorBindingPl if (!hasSamplers) { for (auto& binding : selected.bindings) { std::fill(binding.descriptor.imageSamplers.begin(), binding.descriptor.imageSamplers.end(), 0u); + binding.tableSamplerMasks.clear(); } } selected.imageMetadata = plan.imageMetadata; @@ -502,10 +558,14 @@ DescriptorBindingPlan DescriptorBindingBuilder::Select(const DescriptorBindingPl return selected; } -void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const CompiledBindingLayout& compiled, const DescriptorBindingPlan& plan, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const { +void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const CompiledBindingLayout& compiled, const DescriptorBindingPlan& plan, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads, const ResolvedImageTables* tables) const { 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; @@ -539,6 +599,41 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con filter = PointFilteredSamplerWord(physical.guestDescriptor.at(element * 4u), filter); } 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) { + physical.guestDescriptor.insert(physical.guestDescriptor.end(), element.words.begin(), element.words.end()); + ImageResource shape; + shape.dimension = element.dimension; + physical.imageShapes.push_back(ImageShapeForResource(shape)); + physical.imageDepthCompare.push_back(element.depthCompare); + 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) { + if ((element.samplers & (1u << sampler)) != 0u) samplers |= entry.tableSamplerMasks[sampler]; + } + physical.imageSamplers.push_back(samplers); + } + physical.count = static_cast(tables->elements.size()); + break; + case DescriptorRole::SamplerTable: + if (tables == nullptr) fail("DescriptorBindingBuilder::Populate sampler table elements were not resolved"); + physical.guestDescriptor.reserve(tables->samplers.size() * 4u); + for (const auto& element : tables->samplers) { + const auto first = physical.guestDescriptor.size(); + physical.guestDescriptor.insert(physical.guestDescriptor.end(), element.words.begin(), element.words.end()); + if (element.point) physical.guestDescriptor[first + 2u] = PointFilteredSamplerWord(physical.guestDescriptor[first], physical.guestDescriptor[first + 2u]); + physical.samplerDepthCompare.push_back(element.compare); + } + physical.count = static_cast(tables->samplers.size()); + break; + case DescriptorRole::ImageTableMap: + if (tables == nullptr || tables->map.empty()) fail("DescriptorBindingBuilder::Populate image table map was not resolved"); + physical.guestDescriptor = tables->map; + break; case DescriptorRole::FlattenedSrt: if (snapshot.flattenedSrt.empty()) { fail("DescriptorBindingBuilder::Populate flattened SRT snapshot is empty"); diff --git a/core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp b/core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp index 2100af1bd2..db4de9830a 100644 --- a/core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp +++ b/core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp @@ -31,8 +31,7 @@ const IrValue* partialOf(const IrValue& interpolation) { bool isBarycentric(const IrValue& value) { if (value.Opcode() != IrOpcode::GetBuiltin) return false; - const auto kind = static_cast(value.Argument(0)->Resolve()->ImmediateU32()); - return kind == StageInputKind::BaryCoordSmooth || kind == StageInputKind::BaryCoordNoPerspective; + return IsBarycentricInput(static_cast(value.Argument(0)->Resolve()->ImmediateU32())); } bool isBarycentric(const IrValue& value, StageInputKind kind, std::uint32_t component) { diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index 61a65c21b4..a88fa3e288 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -781,6 +781,208 @@ MaskFamily familyOf(const std::vector& regions, const IrValue* mask, std return MaskFamily{mask->HasImmediate() || regionOf(regions, mask->Parent()) == Region::Once, wholeQuadSource(mask, waveSize) != nullptr}; } +class GuardProof { +public: + GuardProof(const IrProgram& program, const std::vector& regions, const BlockSet& uniform, const IrValue* observation, std::span path = {}) : program(program), regions(regions), uniform(uniform), observation(observation), path(path), waveSize(program.WaveSize()) {} + + bool ImpliesMask(const IrValue* condition, const IrValue* low, const IrValue* high) { + return within(condition, Words{low, high}, 0u); + } + + bool Implies(const IrValue* left, const IrValue* right, unsigned depth = 0) { + left = resolve(left); + right = resolve(right); + if (!follow(left, depth) || !follow(right, depth)) return false; + if (left == right || truth(left, false) || truth(right, true)) return true; + if (right->Opcode() == IrOpcode::LogicalAnd) return Implies(left, right->Argument(0), depth + 1u) && Implies(left, right->Argument(1), depth + 1u); + if (right->Opcode() == IrOpcode::LogicalOr) return Implies(left, right->Argument(0), depth + 1u) || Implies(left, right->Argument(1), depth + 1u); + if (left->Opcode() == IrOpcode::LogicalAnd) return Implies(left->Argument(0), right, depth + 1u) || Implies(left->Argument(1), right, depth + 1u); + if (left->Opcode() == IrOpcode::LogicalOr) return Implies(left->Argument(0), right, depth + 1u) && Implies(left->Argument(1), right, depth + 1u); + if (left->IsPhi() && merges(*left)) { + return std::all_of(left->Arguments().begin(), left->Arguments().end(), [&](const IrValue* incoming) { return Implies(incoming, right, depth + 1u); }); + } + if (left->Opcode() == IrOpcode::SelectU1) return Implies(left->Argument(1), right, depth + 1u) && Implies(left->Argument(2), right, depth + 1u); + if (const auto bit = threadBit(right, waveSize)) return within(left, Words{bit->low, bit->high}, depth + 1u); + if (const auto bit = threadBit(left, waveSize)) return fromWords(Words{bit->low, bit->high}, right, depth + 1u); + return false; + } + +private: + struct Words { + const IrValue* low; + const IrValue* high; + }; + + bool follow(const IrValue* value, unsigned depth) { + if (depth > 64u || remaining == 0u) return false; + --remaining; + return value->HasImmediate() || regionOf(regions, value->Parent()) == Region::Once; + } + + static bool truth(const IrValue* value, bool expected) { + return value->Type() == IrType::Bool && value->HasImmediate() && value->ImmediateBool() == expected; + } + + bool constant(Words words, std::uint32_t value) const { + return isImmediate(words.low, value) && (waveSize == 32u || isImmediate(words.high, value)); + } + + bool same(Words left, Words right) const { + return left.low == right.low && (waveSize == 32u || left.high == right.high); + } + + bool merges(const IrValue& phi) const { + if (regionOf(regions, phi.Parent()) != Region::Once || phi.ArgumentCount() == 0u || phi.ArgumentCount() != phi.PhiBlockCount()) return false; + return std::all_of(phi.Arguments().begin(), phi.Arguments().end(), [&](const IrValue* incoming) { return incoming->Resolve()->HasImmediate() || regionOf(regions, incoming->Resolve()->Parent()) == Region::Once; }); + } + + static const IrValue* incomingFrom(const IrValue& phi, const IrBlock* predecessor) { + for (std::size_t index = 0; index < phi.PhiBlockCount(); ++index) if (phi.PhiBlock(index) == predecessor) return phi.Argument(index)->Resolve(); + return nullptr; + } + + const IrValue* resolve(const IrValue* value) const { + for (unsigned depth = 0; depth < 64u; ++depth) { + value = value->Resolve(); + if (!value->IsPhi()) return value; + const auto block = std::find(path.begin(), path.end(), value->Parent()); + if (block == path.end() || block == path.begin()) return value; + const IrValue* incoming = incomingFrom(*value, *(block - 1)); + if (incoming == nullptr) return value; + value = incoming; + } + return value; + } + + bool capturedHere(const IrValue* value) const { + if (uniform.contains(value->Parent())) return true; + const IrBlock* observedBlock = path.empty() ? observation->Parent() : path.back(); + if (!truth(value->Argument(0)->Resolve(), true) || observedBlock == nullptr || value->Parent() == nullptr) return false; + if (value->Parent() == observedBlock) { + if (!path.empty() && observation->Parent() != observedBlock) return true; + const auto& instructions = value->Parent()->Instructions(); + const auto captured = std::find(instructions.begin(), instructions.end(), value); + const auto observed = std::find(instructions.begin(), instructions.end(), observation); + return captured != instructions.end() && observed != instructions.end() && std::distance(instructions.begin(), captured) < std::distance(instructions.begin(), observed); + } + BlockSet visited; + std::vector pending{program.BlockOrder().front()}; + while (!pending.empty()) { + const IrBlock* block = pending.back(); + pending.pop_back(); + if (block == value->Parent() || !visited.insert(block).second) continue; + if (block == observedBlock) return false; + pending.insert(pending.end(), block->Successors().begin(), block->Successors().end()); + } + return true; + } + + const IrValue* ballot(const IrValue* word, std::uint32_t half) const { + if (word->Opcode() != IrOpcode::CompositeExtractU32x4 || !isImmediate(word->Argument(1)->Resolve(), half)) return nullptr; + const IrValue* value = word->Argument(0)->Resolve(); + return value->Opcode() == IrOpcode::Ballot ? value : nullptr; + } + + std::optional quadSource(Words words) const { + if (words.low->Opcode() != IrOpcode::CompositeExtractU64 || !isImmediate(words.low->Argument(1)->Resolve(), 0u)) return std::nullopt; + const IrValue* wqm = words.low->Argument(0)->Resolve(); + if (waveSize != 32u && (words.high->Opcode() != IrOpcode::CompositeExtractU64 || words.high->Argument(0)->Resolve() != wqm || !isImmediate(words.high->Argument(1)->Resolve(), 1u))) return std::nullopt; + if (wqm->Opcode() != IrOpcode::WqmU64) return std::nullopt; + const IrValue* source = wqm->Argument(0)->Resolve(); + if (source->Opcode() != IrOpcode::CompositeConstructU64 || source->ArgumentCount() != 2u) return std::nullopt; + return Words{source->Argument(0)->Resolve(), source->Argument(1)->Resolve()}; + } + + std::vector parts(Words words) const { + const bool lowAnd = words.low->Opcode() == IrOpcode::BitwiseAnd32; + const bool highAnd = waveSize != 32u && words.high->Opcode() == IrOpcode::BitwiseAnd32; + std::vector result; + if (!lowAnd && !highAnd) return result; + for (unsigned low = 0; low < (lowAnd ? 2u : 1u); ++low) { + for (unsigned high = 0; high < (highAnd ? 2u : 1u); ++high) { + result.push_back(Words{lowAnd ? words.low->Argument(low)->Resolve() : words.low, highAnd ? words.high->Argument(high)->Resolve() : words.high}); + } + } + return result; + } + + template + std::optional intersection(Words words, TProof proof) { + const bool lowAnd = words.low->Opcode() == IrOpcode::BitwiseAnd32; + const bool highAnd = waveSize != 32u && words.high->Opcode() == IrOpcode::BitwiseAnd32; + if (!lowAnd && !highAnd) return std::nullopt; + const auto part = [&](unsigned low, unsigned high) { + return Words{lowAnd ? words.low->Argument(low)->Resolve() : words.low, highAnd ? words.high->Argument(high)->Resolve() : words.high}; + }; + if (!lowAnd || !highAnd) return proof(part(0u, 0u)) && proof(part(lowAnd ? 1u : 0u, highAnd ? 1u : 0u)); + return (proof(part(0u, 0u)) && proof(part(1u, 1u))) || (proof(part(0u, 1u)) && proof(part(1u, 0u))); + } + + bool within(const IrValue* condition, Words words, unsigned depth) { + condition = resolve(condition); + words = Words{resolve(words.low), resolve(words.high)}; + if (!follow(condition, depth) || !follow(words.low, depth) || (waveSize != 32u && !follow(words.high, depth))) return false; + if (truth(condition, false) || constant(words, 0xffffffffu)) return true; + if (const auto bit = threadBit(condition, waveSize)) return subset(Words{bit->low, bit->high}, words, depth + 1u); + if (const auto result = intersection(words, [&](Words part) { return within(condition, part, depth + 1u); })) return *result; + if (const auto source = quadSource(words)) return within(condition, *source, depth + 1u); + const IrValue* lowBallot = ballot(words.low, 0u); + const IrValue* highBallot = waveSize == 32u ? lowBallot : ballot(words.high, 1u); + if (lowBallot && highBallot && capturedHere(lowBallot) && capturedHere(highBallot)) return Implies(condition, lowBallot->Argument(0), depth + 1u) && Implies(condition, highBallot->Argument(0), depth + 1u); + if (condition->Opcode() == IrOpcode::LogicalAnd) return within(condition->Argument(0), words, depth + 1u) || within(condition->Argument(1), words, depth + 1u); + if (condition->Opcode() == IrOpcode::LogicalOr) return within(condition->Argument(0), words, depth + 1u) && within(condition->Argument(1), words, depth + 1u); + if (condition->IsPhi() && merges(*condition)) return std::all_of(condition->Arguments().begin(), condition->Arguments().end(), [&](const IrValue* incoming) { return within(incoming, words, depth + 1u); }); + return false; + } + + bool fromWords(Words words, const IrValue* condition, unsigned depth) { + condition = resolve(condition); + words = Words{resolve(words.low), resolve(words.high)}; + if (!follow(condition, depth) || !follow(words.low, depth) || (waveSize != 32u && !follow(words.high, depth))) return false; + if (truth(condition, true) || constant(words, 0u)) return true; + if (const auto bit = threadBit(condition, waveSize)) return subset(words, Words{bit->low, bit->high}, depth + 1u); + if (condition->Opcode() == IrOpcode::LogicalAnd) return fromWords(words, condition->Argument(0), depth + 1u) && fromWords(words, condition->Argument(1), depth + 1u); + if (condition->Opcode() == IrOpcode::LogicalOr) return fromWords(words, condition->Argument(0), depth + 1u) || fromWords(words, condition->Argument(1), depth + 1u); + const auto intersections = parts(words); + if (!intersections.empty()) return std::any_of(intersections.begin(), intersections.end(), [&](Words part) { return fromWords(part, condition, depth + 1u); }); + const IrValue* lowBallot = ballot(words.low, 0u); + const IrValue* highBallot = waveSize == 32u ? lowBallot : ballot(words.high, 1u); + return lowBallot && highBallot && Implies(lowBallot->Argument(0), condition, depth + 1u) && Implies(highBallot->Argument(0), condition, depth + 1u); + } + + bool subset(Words left, Words right, unsigned depth) { + left = Words{resolve(left.low), resolve(left.high)}; + right = Words{resolve(right.low), resolve(right.high)}; + if (!follow(left.low, depth) || !follow(right.low, depth) || (waveSize != 32u && (!follow(left.high, depth) || !follow(right.high, depth)))) return false; + if (same(left, right) || constant(left, 0u) || constant(right, 0xffffffffu)) return true; + if (const auto result = intersection(right, [&](Words part) { return subset(left, part, depth + 1u); })) return *result; + const auto leftParts = parts(left); + if (!leftParts.empty() && std::any_of(leftParts.begin(), leftParts.end(), [&](Words part) { return subset(part, right, depth + 1u); })) return true; + if (const auto source = quadSource(right); source && subset(left, *source, depth + 1u)) return true; + if (left.low->IsPhi() && merges(*left.low) && (waveSize == 32u || (left.high->IsPhi() && left.high->Parent() == left.low->Parent() && merges(*left.high)))) { + for (std::size_t index = 0; index < left.low->PhiBlockCount(); ++index) { + const IrValue* highIncoming = waveSize == 32u ? left.high : incomingFrom(*left.high, left.low->PhiBlock(index)); + if (highIncoming == nullptr || !subset(Words{left.low->Argument(index)->Resolve(), highIncoming}, right, depth + 1u)) return false; + } + return true; + } + const IrValue* lowBallot = ballot(left.low, 0u); + const IrValue* highBallot = waveSize == 32u ? lowBallot : ballot(left.high, 1u); + if (lowBallot && highBallot && within(lowBallot->Argument(0), right, depth + 1u) && within(highBallot->Argument(0), right, depth + 1u)) return true; + lowBallot = ballot(right.low, 0u); + highBallot = waveSize == 32u ? lowBallot : ballot(right.high, 1u); + return lowBallot && highBallot && capturedHere(lowBallot) && capturedHere(highBallot) && fromWords(left, lowBallot->Argument(0), depth + 1u) && fromWords(left, highBallot->Argument(0), depth + 1u); + } + + const IrProgram& program; + const std::vector& regions; + const BlockSet& uniform; + const IrValue* observation; + std::span path; + std::uint32_t waveSize; + std::uint32_t remaining = 8192u; +}; + class UnreadLanes; using FirstLaneReads = std::array, 4>; @@ -982,15 +1184,21 @@ private: bool solving = false; }; -std::uint32_t removeFullWaveSelects(IrProgram& program) { +std::uint32_t removeFullWaveSelects(IrProgram& program, const std::vector& regions, std::vector& selects) { std::uint32_t removed = 0; + selects.clear(); for (auto blockIt = program.BlockOrder().rbegin(); blockIt != program.BlockOrder().rend(); ++blockIt) { + const bool single = regionOf(regions, *blockIt) == Region::Once; auto& instructions = (*blockIt)->Instructions(); auto it = instructions.end(); while (it != instructions.begin()) { --it; IrValue* inst = *it; - if (!isSelect(inst->Opcode()) || inst->ArgumentCount() != 3u || !inst->HasUses() || !alwaysTrue(inst->Argument(0), program.WaveSize())) continue; + if (!isSelect(inst->Opcode()) || inst->ArgumentCount() != 3u) continue; + if (!inst->HasUses() || !alwaysTrue(inst->Argument(0), program.WaveSize())) { + if (single) selects.push_back(inst); + continue; + } inst->ReplaceAllUsesWith(inst->Argument(1)); inst->Invalidate(); inst->SetParent(nullptr); @@ -1001,17 +1209,22 @@ std::uint32_t removeFullWaveSelects(IrProgram& program) { return removed; } +bool hasUnreadCandidate(const std::vector& selects, const std::vector& regions, MaskFamily family, std::uint32_t waveSize) { + return std::any_of(selects.begin(), selects.end(), [&](const IrValue* select) { return select->Parent() != nullptr && select->HasUses() && familyOf(regions, select->Argument(0)->Resolve(), waveSize) == family; }); +} + } MaskedSelectEliminationStats MaskedSelectEliminator::Eliminate(IrProgram& program) const { MaskedSelectEliminationStats stats; const auto regions = blockRegions(program); const BlockSet uniform = uniformEntryBlocks(program); + std::vector selects; std::uint32_t removed = 1; while (removed != 0u) { - removed = removeFullWaveSelects(program); + removed = removeFullWaveSelects(program, regions, selects); for (const MaskFamily family : {MaskFamily{true, false}, MaskFamily{true, true}, MaskFamily{false, false}, MaskFamily{false, true}}) { - removed += UnreadLanes(program, regions, uniform, family, stats.steps).Remove(); + if (hasUnreadCandidate(selects, regions, family, program.WaveSize())) removed += UnreadLanes(program, regions, uniform, family, stats.steps).Remove(); } stats.removedSelects += removed; if (removed != 0u) DeadCodeEliminator{}.Eliminate(program); @@ -1019,4 +1232,36 @@ MaskedSelectEliminationStats MaskedSelectEliminator::Eliminate(IrProgram& progra return stats; } +bool GuardImplies(const IrProgram& program, const IrValue* condition, const IrValue* consequence) { + if (condition == nullptr || consequence == nullptr) return false; + condition = condition->Resolve(); + consequence = consequence->Resolve(); + if (condition->Type() != IrType::Bool || consequence->Type() != IrType::Bool) return false; + if (condition == consequence || (condition->HasImmediate() && !condition->ImmediateBool()) || (consequence->HasImmediate() && consequence->ImmediateBool())) return true; + const auto regions = blockRegions(program); + if (std::ranges::find(regions, Region::Loop) != regions.end()) return false; + if ((!condition->HasImmediate() && regionOf(regions, condition->Parent()) != Region::Once) || (!consequence->HasImmediate() && regionOf(regions, consequence->Parent()) != Region::Once)) return false; + const BlockSet uniform = uniformEntryBlocks(program); + std::uint64_t steps = 0; + MaskSets sets(steps); + ImpliedMasks implied(regions, uniform, program.WaveSize(), familyOf(regions, consequence, program.WaveSize()), sets, steps); + implied.AddMask(consequence); + return sets.Contains(implied.Of(condition), consequence->Id()) || GuardProof(program, regions, uniform, condition).Implies(condition, consequence); +} + +bool GuardImpliesMaskOnPath(const IrProgram& program, const IrValue* condition, const IrValue* low, const IrValue* high, std::span path) { + if (condition == nullptr || low == nullptr || high == nullptr || path.empty() || program.BlockOrder().empty() || path.front() != program.BlockOrder().front()) return false; + if (condition->Resolve()->Type() != IrType::Bool || low->Resolve()->Type() != IrType::U32 || high->Resolve()->Type() != IrType::U32) return false; + BlockSet visited; + for (std::size_t index = 0; index < path.size(); ++index) { + if (path[index] == nullptr || !visited.insert(path[index]).second) return false; + if (index != 0u && std::ranges::find(path[index - 1u]->Successors(), path[index]) == path[index - 1u]->Successors().end()) return false; + } + if (!condition->Resolve()->HasImmediate() && std::ranges::find(path, condition->Resolve()->Parent()) == path.end()) return false; + const auto regions = blockRegions(program); + if (std::ranges::find(regions, Region::Loop) != regions.end()) return false; + const auto uniform = uniformEntryBlocks(program); + return GuardProof(program, regions, uniform, condition->Resolve(), path).ImpliesMask(condition, low, high); +} + } diff --git a/core/shader/recompiler/Optimization/src/ReadLaneEliminator.cpp b/core/shader/recompiler/Optimization/src/ReadLaneEliminator.cpp index b4ec817c1a..97fbe47cb2 100644 --- a/core/shader/recompiler/Optimization/src/ReadLaneEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/ReadLaneEliminator.cpp @@ -1,5 +1,6 @@ #include "Optimization/ReadLaneEliminator.hpp" #include +#include #include #include #include @@ -12,15 +13,184 @@ namespace { struct ChainResult { IrValue* value = nullptr; IrValue* write = nullptr; + IrValue* scalar = nullptr; + bool cycle = false; }; +ChainResult searchChain(IrValue* value, std::uint32_t lane, std::uint32_t waveSize); + bool isImmediateU32(const IrValue& value) { return value.HasImmediate() && value.Type() == IrType::U32; } +struct WindowLane { + IrValue* value = nullptr; + bool known = false; +}; + +struct LaneState { + enum class Kind : std::uint8_t { Unreached, Known, Unknown }; + Kind kind = Kind::Unreached; + IrValue* value = nullptr; + + bool operator==(const LaneState& other) const { + return kind == other.kind && (kind != Kind::Known || value->Resolve() == other.value->Resolve()); + } +}; + +LaneState meetLanes(const LaneState& left, const LaneState& right) { + if (left.kind == LaneState::Kind::Unreached) return right; + if (right.kind == LaneState::Kind::Unreached) return left; + if (left.kind == LaneState::Kind::Unknown || right.kind == LaneState::Kind::Unknown) return {LaneState::Kind::Unknown, nullptr}; + return left.value->Resolve() == right.value->Resolve() ? left : LaneState{LaneState::Kind::Unknown, nullptr}; +} + +struct LaneFlow { + std::unordered_map exits; + std::unordered_map loads; +}; + +struct WindowFlows { + const IrProgram* program = nullptr; + std::unordered_set spillSlots; + std::unordered_map flows; + std::unordered_set computing; +}; + +thread_local WindowFlows* windowFlows = nullptr; + +std::unordered_set collectLaneSpillSlots(const IrProgram& program) { + std::unordered_set candidates; + std::unordered_set rejected; + const auto immediateSlot = [](const IrValue& inst, std::uint32_t& slot) { + const IrValue* index = inst.Argument(0)->Resolve(); + if (!isImmediateU32(*index)) return false; + slot = index->ImmediateU32(); + return true; + }; + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + std::uint32_t slot = 0; + if (inst->Opcode() == IrOpcode::StoreVectorWindow) { + if (!immediateSlot(*inst, slot)) continue; + candidates.insert(slot); + if (inst->Argument(1)->Resolve()->Opcode() != IrOpcode::WriteLane) rejected.insert(slot); + } else if (inst->Opcode() == IrOpcode::LoadVectorWindow) { + if (!immediateSlot(*inst, slot)) continue; + candidates.insert(slot); + for (const IrUse& use : inst->OperandUses()) { + const bool laneRead = use.user->Opcode() == IrOpcode::ReadLane && use.operand == 0u; + const bool chained = use.user->Opcode() == IrOpcode::WriteLane && use.operand == 2u; + if (!laneRead && !chained) rejected.insert(slot); + } + } + } + } + for (const auto slot : rejected) candidates.erase(slot); + return candidates; +} + +LaneState windowLoadLane(const IrValue* load, std::uint32_t lane, std::uint32_t waveSize); + +LaneState storedLane(IrValue* value, std::uint32_t slot, std::uint32_t lane, std::uint32_t waveSize, const LaneFlow& flow) { + for (;;) { + value = value->Resolve(); + if (value->Opcode() == IrOpcode::WriteLane) { + IrValue* selector = value->Argument(1)->Resolve(); + if (!isImmediateU32(*selector)) return {LaneState::Kind::Unknown, nullptr}; + if (selector->ImmediateU32() % waveSize == lane) return {LaneState::Kind::Known, value->Argument(0)}; + value = value->Argument(2); + continue; + } + if (value->Opcode() != IrOpcode::LoadVectorWindow) return {LaneState::Kind::Unknown, nullptr}; + const IrValue* index = value->Argument(0)->Resolve(); + if (!isImmediateU32(*index)) return {LaneState::Kind::Unknown, nullptr}; + if (index->ImmediateU32() != slot) return windowLoadLane(value, lane, waveSize); + const auto found = flow.loads.find(value); + return found == flow.loads.end() ? LaneState{} : found->second; + } +} + +const LaneFlow* laneFlow(std::uint32_t slot, std::uint32_t lane, std::uint32_t waveSize) { + auto& flows = *windowFlows; + const auto key = (static_cast(slot) << 32u) | lane; + if (const auto found = flows.flows.find(key); found != flows.flows.end()) return &found->second; + if (!flows.computing.insert(key).second) return nullptr; + LaneFlow flow; + const bool spill = flows.spillSlots.contains(slot); + const auto& blocks = flows.program->Blocks(); + constexpr int MaxPasses = 64; + bool settled = false; + for (int pass = 0; pass < MaxPasses && !settled; ++pass) { + settled = true; + for (const auto& block : blocks) { + LaneState running{}; + if (block->Predecessors().empty()) { + running = {LaneState::Kind::Unknown, nullptr}; + } else { + for (const IrBlock* predecessor : block->Predecessors()) { + const auto exit = flow.exits.find(predecessor); + running = meetLanes(running, exit == flow.exits.end() ? LaneState{} : exit->second); + } + } + for (IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::LoadVectorWindow) { + const IrValue* index = inst->Argument(0)->Resolve(); + if (!isImmediateU32(*index) || index->ImmediateU32() != slot) continue; + auto& seen = flow.loads[inst]; + if (!(seen == running)) { + seen = running; + settled = false; + } + } else if (inst->Opcode() == IrOpcode::StoreVectorWindow) { + const IrValue* index = inst->Argument(0)->Resolve(); + if (!isImmediateU32(*index)) { + if (!spill) running = {LaneState::Kind::Unknown, nullptr}; + continue; + } + if (index->ImmediateU32() != slot) continue; + running = storedLane(inst->Argument(1), slot, lane, waveSize, flow); + } + } + auto& exit = flow.exits[block.get()]; + if (!(exit == running)) { + exit = running; + settled = false; + } + } + } + flows.computing.erase(key); + if (!settled) { + for (auto& [load, state] : flow.loads) state = {LaneState::Kind::Unknown, nullptr}; + } + return &(flows.flows[key] = std::move(flow)); +} + +LaneState windowLoadLane(const IrValue* load, std::uint32_t lane, std::uint32_t waveSize) { + const IrValue* index = load->Argument(0)->Resolve(); + if (windowFlows == nullptr || !isImmediateU32(*index) || load->Parent() == nullptr) return {LaneState::Kind::Unknown, nullptr}; + const LaneFlow* flow = laneFlow(index->ImmediateU32(), lane, waveSize); + if (flow == nullptr) return {LaneState::Kind::Unknown, nullptr}; + const auto found = flow->loads.find(load); + return found == flow->loads.end() ? LaneState{LaneState::Kind::Unknown, nullptr} : found->second; +} + +WindowLane windowLane(IrValue* load, std::uint32_t lane, std::uint32_t waveSize) { + const LaneState state = windowLoadLane(load, lane, waveSize); + if (state.kind == LaneState::Kind::Known) return {state.value, true}; + return {}; +} + ChainResult searchChain(IrValue* value, std::uint32_t lane, std::uint32_t waveSize) { for (;;) { value = value->Resolve(); + if (value->Opcode() == IrOpcode::LoadVectorWindow) { + const WindowLane found = windowLane(value, lane, waveSize); + if (found.known && found.value != nullptr) { + return ChainResult {value, value, found.value}; + } + return ChainResult {value, nullptr, nullptr, found.known}; + } if (value->Opcode() != IrOpcode::WriteLane) { return ChainResult {value, nullptr}; } @@ -29,7 +199,7 @@ ChainResult searchChain(IrValue* value, std::uint32_t lane, std::uint32_t waveSi return ChainResult {value, nullptr}; } if (selector->ImmediateU32() % waveSize == lane) { - return ChainResult {value, value}; + return ChainResult {value, value, value->Argument(0)}; } value = value->Argument(2); } @@ -65,7 +235,7 @@ using PhiMap = std::unordered_map; IrValue* getRealValue(IrProgram& program, PhiMap& phiMap, IrValue* source, std::uint32_t lane, std::uint32_t waveSize) { const ChainResult chain = searchChain(source, lane, waveSize); if (chain.write != nullptr) { - return chain.write->Argument(0); + return chain.scalar; } IrValue* inst = chain.value; @@ -190,6 +360,13 @@ ReadLaneEliminationStats ReadLaneEliminator::Eliminate(IrProgram& program, std:: if (waveSize != 32u && waveSize != 64u) { return stats; } + WindowFlows flows; + flows.program = &program; + flows.spillSlots = collectLaneSpillSlots(program); + struct FlowScope { + ~FlowScope() { windowFlows = nullptr; } + } flowScope; + windowFlows = &flows; for (const auto& block : program.Blocks()) { for (IrValue* inst : block->Instructions()) { @@ -204,7 +381,7 @@ ReadLaneEliminationStats ReadLaneEliminator::Eliminate(IrProgram& program, std:: const std::uint32_t lane = selector->ImmediateU32() % waveSize; const ChainResult chain = searchChain(inst->Argument(0), lane, waveSize); if (chain.write != nullptr) { - inst->ReplaceUsesWith(chain.write->Argument(0), true); + inst->ReplaceUsesWith(chain.scalar, true); stats.rewrittenReads++; continue; } diff --git a/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp b/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp index a580617abd..6e8c4e269d 100644 --- a/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp @@ -1,7 +1,9 @@ #include "Optimization/RedundantValueEliminator.hpp" +#include #include #include #include +#include #include #include #include @@ -134,31 +136,38 @@ struct Operand { bool operator==(const Operand& other) const = default; }; +Operand OperandOf(const IrValue* resolved) { + if (resolved->Parent() == nullptr && resolved->HasImmediate()) return {nullptr, resolved->Type(), resolved->ImmediateU64()}; + return {resolved, resolved->Type(), 0u}; +} + struct ValueKey { IrOpcode opcode; IrType type; std::uint64_t flags; - std::vector operands; + std::array operands{}; + std::size_t operandCount = 0; - bool operator==(const ValueKey& other) const = default; + bool operator==(const ValueKey& other) const { + if (opcode != other.opcode || type != other.type || flags != other.flags || operandCount != other.operandCount) return false; + for (std::size_t index = 0; index < operandCount; ++index) { + if (operands[index] != other.operands[index] && OperandOf(operands[index]) != OperandOf(other.operands[index])) return false; + } + return true; + } }; struct ValueKeyHash { std::size_t operator()(const ValueKey& key) const { std::size_t hash = static_cast(key.opcode) * 0x9e3779b97f4a7c15ull ^ static_cast(key.type) ^ std::hash{}(key.flags) * 31u; - for (const auto& operand : key.operands) { + for (const auto* argument : std::span(key.operands).first(key.operandCount)) { + const auto operand = OperandOf(argument); hash = hash * 1099511628211ull ^ std::hash{}(operand.value) ^ std::hash{}(operand.immediate) * 7u ^ static_cast(operand.type); } return hash; } }; -Operand OperandOf(const IrValue* argument) { - const IrValue* resolved = argument->Resolve(); - if (resolved->Parent() == nullptr && resolved->HasImmediate()) return {nullptr, resolved->Type(), resolved->ImmediateU64()}; - return {resolved, resolved->Type(), 0u}; -} - std::vector ReversePostOrder(IrBlock& entry) { std::vector order; std::unordered_map visited; @@ -216,15 +225,13 @@ RedundantValueEliminationStats RedundantValueEliminator::Eliminate(IrProgram& pr std::unordered_map available; std::vector added; std::unordered_map replacements; - ValueKey key; const auto visit = [&](IrBlock& block) { for (auto* inst : block.Instructions()) { if (!Recomputable(inst->Opcode()) || inst->HasImmediate() || !inst->HasUses()) continue; - key.opcode = inst->Opcode(); - key.type = inst->Type(); - key.flags = inst->Flags(); - key.operands.clear(); - for (std::size_t index = 0; index < inst->ArgumentCount(); ++index) key.operands.push_back(OperandOf(inst->Argument(index))); + ValueKey key{inst->Opcode(), inst->Type(), inst->Flags()}; + key.operandCount = inst->ArgumentCount(); + if (key.operandCount > key.operands.size()) continue; + for (std::size_t index = 0; index < key.operandCount; ++index) key.operands[index] = inst->Argument(index)->Resolve(); const auto [found, inserted] = available.try_emplace(key, inst); if (inserted) { added.push_back(&found->first); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 37da1a30cf..45b3a7ea1e 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1,9 +1,14 @@ #include "Optimization/ResourceMaterializer.hpp" #include "Optimization/SrtWalker/SrtFlatSlotClasses.hpp" +#include "Optimization/SrtWalker/SrtEvaluator.hpp" #include "Optimization/ShaderStageInputInfo.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "RdnaDecoder/RdnaImageOpDecoder.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" +#include "ThreadOwned.hpp" +#include "ImageTableAbi.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include #include @@ -12,8 +17,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -27,7 +34,11 @@ namespace ShaderRecompiler { namespace { +using ImageTableAbi::PoisonReason; + std::atomic specializationNanoseconds{0}; +std::atomic evaluateNanoseconds{0}; +std::atomic materializeNanoseconds{0}; bool MaterializeProfiled() { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; @@ -67,6 +78,11 @@ ImageType rawImageType(const DescriptorValue& descriptor) { return static_cast((descriptor.dwords[3] >> 28u) & 0xfu); } +bool nullImageDescriptor(const ImageResource& image, const DescriptorValue& descriptor) { + if (nullImageDescriptor(descriptor)) return true; + return static_cast(rawImageType(descriptor)) <= 1u && image.table == NoTable && image.read && !image.written && !image.atomic; +} + IrBufferFormat rawImageFormat(const DescriptorValue& descriptor) { return static_cast((descriptor.dwords[1] >> 20u) & 0x1ffu); } @@ -103,6 +119,15 @@ RdnaImageDimension descriptorDimension(const DescriptorValue& descriptor, RdnaIm } } +std::uint32_t imageCoordinates(RdnaImageDimension dimension) { + switch (dimension) { + case RdnaImageDimension::Dim1D: return 1u; + case RdnaImageDimension::Dim1DArray: case RdnaImageDimension::Dim2D: return 2u; + case RdnaImageDimension::Dim2DMsaaArray: return 4u; + default: return 3u; + } +} + bool validImageDescriptor(const DescriptorValue& descriptor, bool r128) { const auto type = rawImageType(descriptor); const auto format = rawImageFormat(descriptor); @@ -122,7 +147,7 @@ bool validImageDescriptor(const DescriptorValue& descriptor, bool r128) { } std::uint32_t storageMipCount(const ImageResource& base, const DescriptorValue& descriptor) { - if (base.mipMode != ImageMipMode::DynamicStorage || nullImageDescriptor(descriptor)) { + if (base.mipMode != ImageMipMode::DynamicStorage || nullImageDescriptor(base, descriptor)) { return 1u; } const auto mipBase = (descriptor.dwords[3] >> 12u) & 0xfu; @@ -130,42 +155,84 @@ std::uint32_t storageMipCount(const ImageResource& base, const DescriptorValue& return mipBase <= mipLast ? mipLast - mipBase + 1u : 0u; } -DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const ImageResource& base, std::uint32_t srgbDecodeFormats) { +struct ImageDecode { DecodedImage decoded; + std::optional reason; + std::string detail; +}; + +ImageDecode rejectImage(PoisonReason reason, std::string detail) { + ImageDecode result; + result.reason = reason; + result.detail = std::move(detail); + return result; +} + +PoisonReason invalidImageReason(const DescriptorValue& descriptor) { + const auto type = rawImageType(descriptor); + if (type < ImageType::Color1D) { + return PoisonReason::NotImage; + } + if (rawImageFormat(descriptor) == IrBufferFormat::Invalid) { + return PoisonReason::InvalidFormat; + } + if (type == ImageType::Color2DMsaa || type == ImageType::Color2DMsaaArray) { + return PoisonReason::Multisampled; + } + return PoisonReason::Dimension; +} + +ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const ImageResource& base, std::uint32_t srgbDecodeFormats) { + ImageDecode result; + auto& decoded = result.decoded; decoded.mipCount = storageMipCount(base, descriptor); if (decoded.mipCount == 0u) { - throw std::runtime_error("storage image descriptor has an invalid mip range"); + return rejectImage(PoisonReason::InvalidFormat, "storage image descriptor has an invalid mip range"); } - if (nullImageDescriptor(descriptor)) { + if (nullImageDescriptor(base, descriptor)) { decoded.numericClass = base.atomic ? IrTextureNumericClass::Uint : IrTextureNumericClass::Float; decoded.dimension = RdnaImageDimension::Dim2D; decoded.cube = false; - return decoded; + return result; } if (base.resourceClass == ImageResourceClass::None || (base.atomic && base.resourceClass != ImageResourceClass::Storage)) { throw std::runtime_error("image resource has an invalid class"); } if (!validImageDescriptor(descriptor, base.r128)) { - throw std::runtime_error("image descriptor is invalid"); + return rejectImage(invalidImageReason(descriptor), "image descriptor is invalid"); } decoded.dimension = descriptorDimension(descriptor, base.dimension); + if (base.depthCompare) { + if (decoded.dimension == RdnaImageDimension::Dim1D) { + decoded.dimension = RdnaImageDimension::Dim2D; + } else if (decoded.dimension == RdnaImageDimension::Dim1DArray) { + decoded.dimension = RdnaImageDimension::Dim2DArray; + } + } + if (GcnGuestShaders() && imageCoordinates(decoded.dimension) != imageCoordinates(base.dimension)) { + throw std::runtime_error("GCN image instruction on a descriptor of type " + std::to_string(static_cast(rawImageType(descriptor))) + " needs a different address count than its inferred dimension; not implemented"); + } decoded.cube = descriptorIsCube(descriptor); const auto format = rawImageFormat(descriptor); if (base.atomic64 && format != IrBufferFormat::Format32_32UInt && format != IrBufferFormat::Format32_32SInt && format != IrBufferFormat::Format32_32Float) { - throw std::runtime_error("64-bit atomic image descriptor uses an unsupported format " + std::to_string(static_cast(format))); + return rejectImage(PoisonReason::InvalidFormat, "64-bit atomic image descriptor uses an unsupported format " + std::to_string(static_cast(format))); } if (base.atomic && !base.atomic64 && format != IrBufferFormat::Format32UInt && format != IrBufferFormat::Format32SInt && format != IrBufferFormat::Format32Float) { - throw std::runtime_error("atomic image descriptor uses an unsupported format " + std::to_string(static_cast(format))); + return rejectImage(PoisonReason::InvalidFormat, "atomic image descriptor uses an unsupported format " + std::to_string(static_cast(format))); } const bool storage = base.resourceClass == ImageResourceClass::Storage; + const bool table = base.table != NoTable; decoded.fmask = IsFmaskTextureFormat(format); - if (decoded.fmask && !base.fmaskCompatible) throw std::runtime_error("FMASK requires a direct 32-bit image load"); - if (decoded.fmask && (storage || base.depthCompare || base.indirectRoot != ImageResource::NoIndirectImage)) { - throw std::runtime_error("FMASK requires a direct sampled image load"); + if (decoded.fmask && !base.fmaskCompatible) return rejectImage(PoisonReason::Fmask, "FMASK requires a direct 32-bit image load"); + if (decoded.fmask && (storage || base.depthCompare || table)) { + return rejectImage(PoisonReason::Fmask, "FMASK requires a direct sampled image load"); } + if (decoded.fmask && format != IrBufferFormat::Fmask32_S8_F8) return rejectImage(PoisonReason::Fmask, "FMASK format is not implemented"); + if (decoded.fmask && ((descriptor.dwords[6] & (1u << 20u)) != 0u || ((descriptor.dwords[6] & (1u << 21u)) != 0u && (descriptor.dwords[6] >> 24u) == 0u && descriptor.dwords[7] == 0u))) return rejectImage(PoisonReason::Fmask, "FMASK CMASK compression is not implemented without readable metadata"); + if (decoded.fmask && (base.packed || base.byElements != 0u)) return rejectImage(PoisonReason::Fmask, "packed FMASK image reads are not implemented"); if (base.packed) { - if (base.indirectRoot != ImageResource::NoIndirectImage || (!storage && descriptorImageSwizzle(descriptor) != ShaderImageIdentitySwizzle)) { - throw std::runtime_error("packed image access requires a direct image, with identity swizzle when sampled"); + if (table || (!storage && descriptorImageSwizzle(descriptor) != ShaderImageIdentitySwizzle)) { + return rejectImage(PoisonReason::InvalidFormat, "packed image access requires a direct image, with identity swizzle when sampled"); } decoded.packedFormat = format; } @@ -177,23 +244,30 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag const bool packedSixteen = format == IrBufferFormat::Format16UNorm || format == IrBufferFormat::Format16UInt || format == IrBufferFormat::Format16SInt || format == IrBufferFormat::Format8_8UNorm || format == IrBufferFormat::Format8_8UInt || format == IrBufferFormat::Format8_8SInt; const bool measured = base.packed ? packedEight || (base.byElements == 2u && packedSixteen) : base.byElements == 4u ? base.byComponents == 1u && eightBit : base.byElements == 2u && (base.byComponents == 1u ? eightBit || sixteenBit : base.byComponents == 2u && eightBitPair); - if (!measured || descriptorImageSwizzle(descriptor) != ShaderImageIdentitySwizzle || rawImageType(descriptor) != ImageType::Color2D || base.indirectRoot != ImageResource::NoIndirectImage) { - throw std::runtime_error(base.packed ? "MIMG PCK2/PCK4 requires a direct, identity-swizzled 2D UNORM, UINT or SINT image whose elements fill one dword: R8, R16 or RG8 for PCK2, R8 for PCK4" - : "MIMG BY2/BY4 requires a direct, identity-swizzled 2D image whose elements fill one dword: R8, R16 or RG8 for BY2, R8 for BY4"); + if (!measured || descriptorImageSwizzle(descriptor) != ShaderImageIdentitySwizzle || rawImageType(descriptor) != ImageType::Color2D || table) { + return rejectImage(PoisonReason::InvalidFormat, base.packed ? "MIMG PCK2/PCK4 requires a direct, identity-swizzled 2D UNORM, UINT or SINT image whose elements fill one dword: R8, R16 or RG8 for PCK2, R8 for PCK4" + : "MIMG BY2/BY4 requires a direct, identity-swizzled 2D image whose elements fill one dword: R8, R16 or RG8 for BY2, R8 for BY4"); } } - decoded.conversionFormat = RemapTextureFormat(format) != format ? format : IrBufferFormat::Invalid; - if (format == IrBufferFormat::Format11_11_10UNorm || format == IrBufferFormat::Format10_11_11Float) { + const bool nativeRepacked = format == IrBufferFormat::Format10_11_11Float && !storage && !base.packed; + decoded.conversionFormat = RemapTextureFormat(format) != format && !nativeRepacked ? format : IrBufferFormat::Invalid; + if (format == IrBufferFormat::Format11_11_10UNorm || (format == IrBufferFormat::Format10_11_11Float && !nativeRepacked)) { const bool floating = format == IrBufferFormat::Format10_11_11Float; - if (!base.srgbDecodeCompatible) throw std::runtime_error(floating ? "samples or gathers a converted float image, or queries its LOD, which is not implemented" : "sampling, gathering or querying LOD of a converted unorm image is not implemented"); - if (!base.depthBitsCompatible) throw std::runtime_error(floating ? "reads or writes a converted float image with 16-bit data, which is not implemented" : "reads or writes a converted unorm image with 16-bit data, which is not implemented"); + if (!base.srgbDecodeCompatible) { + if (floating) return rejectImage(PoisonReason::Conversion, "samples or gathers a converted float image, or queries its LOD, which is not implemented"); + if (storage || base.depthCompare || (base.emulatedCompare & EmulatedCompare::ConvertedSampleUnsupported) != 0u) return rejectImage(PoisonReason::Conversion, "sampling, gathering or querying LOD of a converted unorm image is not implemented"); + if (base.table != NoTable) return rejectImage(PoisonReason::Conversion, "samples a converted unorm image selected through an image table, which is not implemented"); + if (decoded.cube || (decoded.dimension != RdnaImageDimension::Dim2D && decoded.dimension != RdnaImageDimension::Dim2DArray)) return rejectImage(PoisonReason::Conversion, "samples a converted unorm image that is not a 2D or 2D array view, which is not implemented"); + if ((base.emulatedCompare & EmulatedCompare::ConvertedSampleRequiresSingleLevel) != 0u && ((descriptor.dwords[3] >> 12u) & 0xfu) != ((descriptor.dwords[3] >> 16u) & 0xfu)) return rejectImage(PoisonReason::Conversion, "samples a converted unorm image with several mip levels at a LOD that is not 0, which is not implemented"); + } + if (!base.depthBitsCompatible) return rejectImage(PoisonReason::Conversion, floating ? "reads or writes a converted float image with 16-bit data, which is not implemented" : "reads or writes a converted unorm image with 16-bit data, which is not implemented"); for (std::uint32_t component = 0; component < 4u; ++component) { - if (((descriptorImageSwizzle(descriptor) >> (component * 3u)) & 7u) == 7u) throw std::runtime_error("selects a channel the converted image format does not have"); + if (((descriptorImageSwizzle(descriptor) >> (component * 3u)) & 7u) == 7u) return rejectImage(PoisonReason::Conversion, "selects a channel the converted image format does not have"); } } decoded.srgbDecode = !storage && !base.depthCompare && (srgbDecodeFormats & SrgbDecodeBit(format)) != 0u; if (decoded.srgbDecode && !base.srgbDecodeCompatible) { - throw std::runtime_error("samples or gathers an sRGB image the device cannot sample, which is not implemented"); + return rejectImage(PoisonReason::SrgbUnsupported, "samples or gathers an sRGB image the device cannot sample, which is not implemented"); } if (storage || decoded.conversionFormat != IrBufferFormat::Invalid) { decoded.shaderSwizzle = descriptorImageSwizzle(descriptor); @@ -201,71 +275,119 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag const bool wideSint = format == IrBufferFormat::Format32SInt || format == IrBufferFormat::Format32_32SInt || format == IrBufferFormat::Format32_32_32_32SInt; const bool narrowSint = format == IrBufferFormat::Format16SInt || format == IrBufferFormat::Format8_8SInt || format == IrBufferFormat::Format16_16SInt || format == IrBufferFormat::Format8_8_8_8SInt || format == IrBufferFormat::Format16_16_16_16SInt; const bool rawSintStorage = storage && (wideSint || (narrowSint && !base.packed)) && base.written && !base.read && !base.atomic; - decoded.numericClass = base.atomic ? IrTextureNumericClass::Uint : SampledTextureNumericClass(format); + decoded.numericClass = base.atomic || decoded.fmask ? IrTextureNumericClass::Uint : nativeRepacked ? IrTextureNumericClass::Float : SampledTextureNumericClass(format); if (!storage && !base.depthCompare && IsDepthBitsTexture(descriptor.dwords[1], descriptor.dwords[3])) { decoded.depthBits = true; - if (!base.depthBitsCompatible) throw std::runtime_error("runtime image reads depth bits as unsupported 16-bit results"); + if (!base.depthBitsCompatible) return rejectImage(PoisonReason::DepthBits16, "runtime image reads depth bits as unsupported 16-bit results"); decoded.depthUnorm16 = DepthBitsTextureWidth(descriptor.dwords[1], descriptor.dwords[3]) == 16u; decoded.numericClass = IrTextureNumericClass::Float; decoded.shaderSwizzle = descriptorImageSwizzle(descriptor); } if (storage) { if ((!rawSintStorage && decoded.numericClass == IrTextureNumericClass::Sint) || decoded.numericClass == IrTextureNumericClass::Unsupported) { - throw std::runtime_error("storage image descriptor uses an unsupported format"); + return rejectImage(PoisonReason::NumericClass, "storage image descriptor uses an unsupported format"); } if (rawSintStorage) { decoded.numericClass = IrTextureNumericClass::Uint; } if ((rawSintStorage || (base.atomic && format == IrBufferFormat::Format32SInt)) && !base.packed) { - if (!base.depthBitsCompatible) throw std::runtime_error("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)) { - throw std::runtime_error("sampled image descriptor uses an unsupported format"); + char words[96]; + std::snprintf(words, sizeof(words), " (T# %08x %08x %08x %08x %08x %08x %08x %08x)", descriptor.dwords[0], descriptor.dwords[1], descriptor.dwords[2], descriptor.dwords[3], descriptor.dwords[4], descriptor.dwords[5], descriptor.dwords[6], descriptor.dwords[7]); + return rejectImage(PoisonReason::NumericClass, std::string("sampled image descriptor uses an unsupported format") + words); } - return decoded; + return result; } -constexpr std::uint32_t TableEntryBytes = 32; +DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const ImageResource& base, std::uint32_t srgbDecodeFormats) { + auto result = inspectImageDescriptor(descriptor, base, srgbDecodeFormats); + if (result.reason.has_value()) { + throw std::runtime_error(result.detail); + } + return result.decoded; +} -std::uint32_t MaterialScanLimit() { +bool requiresPointSampler(const ImageResource& mode) { + return mode.numericClass == IrTextureNumericClass::Uint || mode.numericClass == IrTextureNumericClass::Sint || mode.conversionFormat != IrBufferFormat::Invalid || mode.depthBits; +} + +bool reducesBetweenTexels(std::uint32_t word0, std::uint32_t filter) { + return ((word0 >> 29u) & 3u) != 0u && (((filter >> 20u) & 0xfu) != 0u || ((filter >> 26u) & 3u) == 2u); +} + +std::string hexText(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "%llx", static_cast(value)); + return text; +} + +std::uint32_t TableKeyScanLimit() { static const std::uint32_t limit = [] { - const char* text = std::getenv("APS5_BINDLESS_MATERIAL_SCAN"); + const char* text = std::getenv("APS5_IMAGE_TABLE_KEY_SCAN"); return text != nullptr ? static_cast(std::strtoul(text, nullptr, 0)) : 256u; }(); return limit; } -bool BindlessTraced() { - static const bool traced = std::getenv("APS5_TRACE_BINDLESS") != nullptr; +bool TableStrict() { + static const bool strict = [] { + const char* text = std::getenv("APS5_IMAGE_TABLE_STRICT"); + return text != nullptr && std::strcmp(text, "0") != 0; + }(); + return strict; +} + +bool TableTraced() { + static const bool traced = std::getenv("APS5_TRACE_IMAGE_TABLE") != nullptr; return traced; } -struct BindlessCounters { - std::atomic tablesMaterial{0}; - std::atomic tablesWhole{0}; +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}; std::atomic keys{0}; - std::atomic paddedNull{0}; - std::atomic paddedShape{0}; - std::atomic paddedConversion{0}; - std::atomic outOfRange{0}; - std::array, static_cast(BindlessRejection::Count)> rejected{}; + 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 reusedUnwritten{0}; + std::atomic reusedCompared{0}; + std::atomic nanoseconds{0}; std::atomic lastReport{0}; }; -BindlessCounters& bindlessCounters() { - static BindlessCounters counters; +TableCounters& tableCounters() { + static TableCounters counters; return counters; } -[[noreturn]] void rejectTable(BindlessRejection reason, const std::string& message) { - ResourceMaterializer::CountBindlessRejection(reason); - throw std::runtime_error(message); +struct TableReuseTotals { + std::atomic unwritten{0}; + std::atomic compared{0}; + std::atomic wordsRead{0}; + std::atomic shared{0}; + std::atomic columns{0}; + std::atomic resolutions{0}; + std::atomic resolutionsShared{0}; +}; + +TableReuseTotals& tableReuseTotals() { + static TableReuseTotals totals; + return totals; } -void reportBindless() { +void reportTables() { if (!MaterializeProfiled()) return; - auto& counters = bindlessCounters(); + auto& counters = tableCounters(); const auto now = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); auto last = counters.lastReport.load(std::memory_order_relaxed); if (last == 0) { @@ -273,154 +395,878 @@ void reportBindless() { return; } if (now - last < 10'000'000'000ll || !counters.lastReport.compare_exchange_strong(last, now, std::memory_order_relaxed)) return; - const auto material = counters.tablesMaterial.exchange(0, std::memory_order_relaxed); - const auto whole = counters.tablesWhole.exchange(0, std::memory_order_relaxed); - const auto keys = counters.keys.exchange(0, std::memory_order_relaxed); - std::array(BindlessRejection::Count)> rejected{}; - std::uint64_t rejections = 0; - for (std::size_t i = 0; i < rejected.size(); i++) { - rejected[i] = counters.rejected[i].exchange(0, std::memory_order_relaxed); - rejections += rejected[i]; + const auto snapshots = counters.snapshots.exchange(0, std::memory_order_relaxed); + if (snapshots == 0) return; + std::string poison; + for (std::size_t reason = 0; reason < PoisonReasonCount; reason++) { + const auto count = counters.poison[reason].exchange(0, std::memory_order_relaxed); + if (count == 0) continue; + poison += " " + std::string(ImageTableAbi::PoisonReasonName(static_cast(reason))) + "=" + std::to_string(count); } - if (material + whole + rejections == 0) return; - const auto tables = material + whole; - std::fprintf(stderr, "[bindless] (10 s): tables bound %llu (mode M %llu, mode T %llu), slots %u, keys avg %.1f, entries unmapped (sample zeros): null/invalid %llu, shape %llu, conversion %llu, out of range %llu; rejected: capacity %llu, material scan %llu, no entry %llu, storage %llu, non-uniform %llu, image slots %llu\n", - static_cast(tables), static_cast(material), static_cast(whole), ResourceMaterializer::BindlessSlots(), tables != 0 ? static_cast(keys) / static_cast(tables) : 0.0, - static_cast(counters.paddedNull.exchange(0, std::memory_order_relaxed)), static_cast(counters.paddedShape.exchange(0, std::memory_order_relaxed)), static_cast(counters.paddedConversion.exchange(0, std::memory_order_relaxed)), static_cast(counters.outOfRange.exchange(0, std::memory_order_relaxed)), - static_cast(rejected[0]), static_cast(rejected[1]), static_cast(rejected[2]), static_cast(rejected[3]), static_cast(rejected[4]), static_cast(rejected[5])); + 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, reused %llu unwritten / %llu compared, %.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.reusedUnwritten.exchange(0, std::memory_order_relaxed)), static_cast(counters.reusedCompared.exchange(0, std::memory_order_relaxed)), static_cast(counters.nanoseconds.exchange(0, std::memory_order_relaxed)) / 1e6); } -struct TableResolution { - std::vector slots; - std::vector> mapping; +enum class RecordState : std::uint8_t { OutsideDomain, Read, Unmapped, Carried }; + +struct ColumnContents { + SharedValues codes; + SharedValues words; + SharedValues> readRanges; + std::uint32_t poisoned = 0; + std::uint32_t unmapped = 0; + std::uint32_t carried = 0; +}; + +struct ColumnRecords { + std::uint64_t base = 0; + std::uint64_t size = 0; + std::uint32_t records = 0; + std::uint32_t keys = 0; + std::uint32_t fault = 0; + bool narrowed = false; + bool outside = false; + bool sealed = false; + std::vector states; + std::vector words; + std::vector distinct; + std::vector> readRanges; + ColumnContents contents; }; struct TableTrace { - bool material = false; - std::uint32_t entries = 0; - std::uint32_t materialEntries = 0; + std::uint64_t base = 0; + std::uint64_t size = 0; + std::uint32_t records = 0; std::uint32_t keys = 0; + std::uint32_t poison = 0; + bool narrowed = false; bool operator==(const TableTrace& other) const = default; }; -void traceTable(const IrResourcePlan& plan, const ImageResource& image, const DescriptorSource::IndirectImage& table, std::uint64_t heapBase, std::uint64_t materialBase, const TableTrace& trace) { +void traceTable(const IrResourcePlan& plan, std::uint32_t table, const TableColumn& column, const TableTrace& trace) { static std::mutex mutex; static std::map, TableTrace> seen; std::lock_guard lock(mutex); - auto& last = seen[{plan.shaderHash, image.firstUsePc}]; + auto& last = seen[{plan.shaderHash, table}]; if (last == trace) return; last = trace; - std::fprintf(stderr, "[bindless] table at pc 0x%x of shader %llx: heap V# base 0x%llx entries %u, material V# base 0x%llx entries %u stride 0x%x offset 0x%x, mode %c, %u keys\n", image.firstUsePc, static_cast(plan.shaderHash), static_cast(heapBase), trace.entries, static_cast(materialBase), trace.materialEntries, table.selectorStride, table.selectorOffset, trace.material ? 'M' : 'T', trace.keys); + 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); } -// The slots of the table image `imageIndex` (see TableResolution). Mode M enumerates the keys the -// dispatch can reach from the material records; mode T binds the whole table when it fits. -void resolveTableImage(const IrResourcePlan& plan, std::uint32_t imageIndex, const DescriptorSource::IndirectImage& table, const SrtRuntime& runtime, SrtWalker& walker, DescriptorValue& resolved, TableResolution& resolution) { - const auto& image = plan.info.images.at(imageIndex); - if (runtime.readMemory == nullptr) { - throw std::runtime_error("bindless image table resolution requires runtime memory access"); - } - if (image.resourceClass != ImageResourceClass::Sampled) { - rejectTable(BindlessRejection::Storage, "bindless storage image tables are unsupported"); - } - if (image.packed || image.byElements != 0u) { - throw std::runtime_error("bindless packed and BY2/BY4 image tables are unsupported"); - } - const auto slots = ResourceMaterializer::BindlessSlots(); - DescriptorValue heapValue; - walker.EvaluateDescriptorSource(plan, table.heapSource, runtime, heapValue); - const ShaderBufferResource heap = decodeBufferDescriptor(heapValue); - const std::uint64_t heapSize = heap.GetSize(); - if (heap.Type() != 0u || heap.Base48() > std::numeric_limits::max() - heapSize) throw std::runtime_error("bindless heap buffer is invalid"); - if (heapSize != 0u && (heapSize < table.entryOffset || (heapSize - table.entryOffset) % TableEntryBytes != 0u)) throw std::runtime_error("bindless heap contains a partial descriptor"); - const auto entries = heapSize > table.entryOffset ? static_cast(std::min((heapSize - table.entryOffset) / TableEntryBytes, std::numeric_limits::max())) : 0u; - const auto readWord = [&](std::uint64_t address, std::uint32_t& word) { - if (!runtime.readMemory(runtime.userContext, address, &word)) { - throw std::runtime_error("failed to read a bindless image table from memory"); - } - }; +bool TableSnapshotReuse() { + static const bool enabled = std::getenv("APS5_NO_TABLE_SNAPSHOT_REUSE") == nullptr; + return enabled; +} - auto& counters = bindlessCounters(); - std::vector keys; - bool materialMode = false; - std::uint32_t materialEntries = 0; - std::uint64_t materialBase = 0; - std::uint32_t outOfRange = 0; - if (table.hasMaterial) { - if (table.selectorStride < sizeof(std::uint32_t) || table.selectorOffset > table.selectorStride - sizeof(std::uint32_t)) throw std::runtime_error("bindless material selector is outside its record"); - DescriptorValue materialValue; - walker.EvaluateDescriptorSource(plan, table.materialSource, runtime, materialValue); - const ShaderBufferResource material = decodeBufferDescriptor(materialValue); - materialBase = material.Base48(); - const std::uint64_t materialSize = material.GetSize(); - if (material.Type() != 0u || materialSize % table.selectorStride != 0u) throw std::runtime_error("bindless material buffer has an invalid layout"); - materialEntries = static_cast(std::min(materialSize / table.selectorStride, std::numeric_limits::max())); - if (materialSize / table.selectorStride > MaterialScanLimit()) rejectTable(BindlessRejection::MaterialScan, "bindless material scan capacity exceeded"); - if (materialEntries <= MaterialScanLimit()) { - materialMode = true; - for (std::uint32_t record = 0; record < materialEntries; record++) { - const std::uint64_t offset = static_cast(record) * table.selectorStride + table.selectorOffset; - if (offset + sizeof(std::uint32_t) > materialSize) throw std::runtime_error("bindless material selector exceeds its buffer"); - std::uint32_t key = 0; - readWord(materialBase + offset, key); - if (key >= entries) { - outOfRange++; +struct ColumnKey { + std::uint64_t base = 0; + std::uint64_t size = 0; + std::uint32_t records = 0; + std::uint32_t keys = 0; + std::uint32_t stride = 0; + std::uint32_t addend = 0; + std::uint32_t offset = 0; + std::uint32_t dwordCount = 0; + std::uint32_t maxKey = 0; + std::uint32_t foldedAddend = 0; + std::uint32_t foldedCarry = 0; + bool address = false; + bool sampler = false; + + bool operator==(const ColumnKey& other) const = default; +}; + +struct ColumnMemo { + ColumnKey key; + std::shared_ptr read; + ColumnContents contents; + std::uint64_t low = 0; + std::uint64_t high = 0; + + std::size_t Footprint() const { + return read->words.size() + read->runs.size() * 3u + contents.codes.size() + contents.words.size() * 9u + contents.readRanges.size() * 4u; + } +}; + +constexpr std::size_t ColumnMemoEntries = 16; +constexpr std::size_t ColumnMemoWords = std::size_t{1} << 22u; + +std::vector& ColumnMemos() { + thread_local std::vector* slot = nullptr; + return ThreadOwned(slot); +} + +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 Clear() { + lease.Clear(); + entries = 0; + } + + 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) {} + + void Run() { + tables = ImageTableSnapshot{}; + std::uint32_t count = 0; + for (const auto& image : plan.info.images) { + if (image.table != NoTable) count = std::max(count, image.table + 1u); + } + for (const auto& sampler : plan.info.samplers) { + if (sampler.table != NoTable) count = std::max(count, sampler.table + 1u); + } + if (count == 0u) { + return; + } + if (runtime.readMemory == nullptr) { + throw std::runtime_error("image table snapshot requires runtime memory access"); + } + const auto started = std::chrono::steady_clock::now(); + tables.shader = plan.shaderHash; + tables.tables.resize(count); + for (const auto& image : plan.info.images) { + if (image.table != NoTable) Snapshot(image.source, image.table); + } + for (const auto& sampler : plan.info.samplers) { + if (sampler.table != NoTable) Snapshot(sampler.source, sampler.table); + } + auto& counters = tableCounters(); + counters.snapshots.fetch_add(1, std::memory_order_relaxed); + counters.tables.fetch_add(count, std::memory_order_relaxed); + counters.nanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); + if (TableStrict()) { + for (std::uint32_t table = 0; table < tables.tables.size(); table++) { + const auto& column = tables.tables[table]; + const auto poisoned = std::find_if(column.codes.begin(), column.codes.end(), [](std::uint32_t code) { return (code & ImageTableAbi::PoisonFlag) != 0u && (code & ~ImageTableAbi::PoisonFlag) != static_cast(PoisonReason::OutsideDomain); }); + if (column.fault != 0u || poisoned != column.codes.end()) { + const auto reason = static_cast((column.fault != 0u ? column.fault : *poisoned) & ~ImageTableAbi::PoisonFlag); + throw std::runtime_error("image table " + std::to_string(table) + " at 0x" + hexText(column.base) + " holds a record that cannot be read (" + std::string(ImageTableAbi::PoisonReasonName(reason)) + "), and APS5_IMAGE_TABLE_STRICT is set"); + } + } + } + } + +private: + static constexpr std::uint64_t PageBytes = 4096u; + + bool Accessible(std::uint64_t address, std::uint64_t bytes) const { + return runtime.accessible == nullptr || runtime.accessible(runtime.userContext, address, bytes); + } + + bool PageAccessible(std::uint64_t address, std::uint64_t begin, std::uint64_t end) { + const auto page = address & ~(PageBytes - 1u); + if (const auto found = pages.find(page); found != pages.end()) return found->second; + const auto first = std::max(page, begin); + const auto last = std::min(page + PageBytes, end); + const bool accessible = first < last && Accessible(first, last - first); + pages.emplace(page, accessible); + return accessible; + } + + bool AddressAccessible(std::uint64_t address) { + const auto page = address & ~(PageBytes - 1u); + const auto [found, inserted] = wholePages.try_emplace(page, false); + if (inserted) found->second = Accessible(page, PageBytes); + return found->second || Accessible(address, sizeof(std::uint32_t)); + } + + std::uint32_t Read(std::uint64_t address) const { + std::uint32_t word = 0; + if (!runtime.readMemory(runtime.userContext, address, &word)) { + throw std::runtime_error("image table: failed to read guest memory at 0x" + hexText(address)); + } + return word; + } + + ShaderBufferResource EvaluateBuffer(std::uint32_t source) const { + DescriptorValue value; + walker.EvaluateDescriptorSource(plan, source, runtime, value); + return decodeBufferDescriptor(value); + } + + void Narrow(const TableColumn& column, ColumnRecords& records) { + 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; + for (const auto& domain : column.keyDomains) { + if (domain.value) { + DescriptorValue key; + try { + walker.EvaluateDescriptorSource(plan, domain.source, runtime, key); + } catch (const std::exception&) { + skip(NarrowSkip::Value); + return; + } + values.push_back(key.dwords[0]); + continue; + } + const auto keyBuffer = EvaluateBuffer(domain.source); + if (keyBuffer.Type() != 0u) { + skip(NarrowSkip::NotBuffer); + return; + } + const auto base = keyBuffer.Base48(); + const auto size = keyBuffer.GetSize(); + 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) { + 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(NarrowSkip::Pending); + 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)); + } + } + 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 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 (!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(records, value); + if (low < high) tables.ranges.emplace_back(low, high - low); + } + + std::uint32_t WordsIndex(ColumnRecords& records, const DescriptorValue& value) { + 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 records.distinct[index] == value; })) return *found; + const auto index = static_cast(records.distinct.size()); + records.distinct.push_back(value); + wordsIndex.Insert(hash, index); + return index; + } + + void Seal(ColumnRecords& records) { + auto& contents = records.contents; + std::vector codes(records.keys, 0u); + for (std::uint32_t key = 0; key < records.keys; key++) { + switch (records.states[key]) { + case RecordState::Read: + codes[key] = records.words[key]; + continue; + case RecordState::OutsideDomain: + codes[key] = ImageTableAbi::PoisonCode(static_cast(PoisonReason::OutsideDomain)); + break; + case RecordState::Unmapped: + codes[key] = ImageTableAbi::PoisonCode(static_cast(PoisonReason::Unmapped)); + contents.unmapped++; + break; + case RecordState::Carried: + codes[key] = ImageTableAbi::PoisonCode(static_cast(PoisonReason::OffsetCarry)); + contents.carried++; + break; + } + contents.poisoned++; + } + contents.codes = SharedValues(std::move(codes)); + contents.words = SharedValues(std::move(records.distinct)); + contents.readRanges = SharedValues>(std::move(records.readRanges)); + records.sealed = true; + tableReuseTotals().columns.fetch_add(1, std::memory_order_relaxed); + } + + ColumnRecords& Column(std::uint32_t source) { + if (const auto found = columns.find(source); found != columns.end()) { + return found->second; + } + auto& records = columns[source]; + ReadColumn(*plan.descriptorSources.at(source).tableColumn, source, records); + if (!records.sealed) Seal(records); + return records; + } + + void ReadColumn(const TableColumn& column, std::uint32_t source, ColumnRecords& records) { + stamp = {}; + comparedRuns.clear(); + comparedWords.clear(); + wordsIndex.Clear(); + const bool active = source >= activeSources.size() || activeSources[source] != 0u; + if (!active) { + records.fault = ImageTableAbi::PoisonCode(static_cast(PoisonReason::Inactive)); + return; + } + if (column.address) { + DescriptorValue pointer; + walker.EvaluateDescriptorSource(plan, column.heapSource, runtime, pointer); + records.base = (static_cast(pointer.dwords[1]) << 32u) | pointer.dwords[0]; + records.records = ImageTableAbi::ScalarAddressRecords(column.stride); + } else { + const auto heap = EvaluateBuffer(column.heapSource); + if (heap.Type() != 0u) { + records.fault = ImageTableAbi::PoisonCode(static_cast(PoisonReason::NotBuffer)); + return; + } + records.base = heap.Base48(); + records.size = heap.GetSize(); + if (column.selected) { + Select(column, records); + return; + } + 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); + } + records.records = std::min(records.records, ImageTableAbi::KeyRecords(column.maxKey, column.stride)); + records.keys = std::min(records.records, ImageTableAbi::MaxKeys); + records.outside = records.keys < records.records; + if (!column.keyDomains.empty() && records.keys != 0u) { + Narrow(column, records); + } + const auto unread = [&] { + if (!records.narrowed) records.states.assign(records.keys, RecordState::Read); + records.words.assign(records.keys, 0u); + }; + const auto end = records.base + records.size; + if (column.indirect) { + unread(); + for (std::uint32_t record = 0; record < records.keys; record++) { + if (records.states[record] != RecordState::Read) continue; + const auto offset = column.addend + record * column.stride; + const auto lowWord = ImageTableAbi::ScalarBufferDword(offset, column.offset, records.size); + const auto highWord = ImageTableAbi::ScalarBufferDword(offset, column.offset + 4u, records.size); + if (!lowWord.has_value() || !highWord.has_value() || !PageAccessible(records.base + *lowWord, records.base, end) || !PageAccessible(records.base + *highWord, records.base, end)) { + records.states[record] = RecordState::Unmapped; continue; } - keys.push_back(key); + std::uint64_t pointer = (static_cast(Read(records.base + *highWord)) << 32u) | Read(records.base + *lowWord); + for (const auto& hop : column.pointerHops) { + const auto low = ImageTableAbi::ScalarAddressDword(pointer, hop.offset, hop.immediate); + const auto high = ImageTableAbi::ScalarAddressDword(pointer, hop.offset, hop.immediate + 4u); + if (!low.has_value() || !high.has_value() || (*low & 3u) != 0u || *high != *low + sizeof(std::uint32_t) || !AddressAccessible(*low) || !AddressAccessible(*high)) { + records.states[record] = RecordState::Unmapped; + break; + } + pointer = (static_cast(Read(*high)) << 32u) | Read(*low); + tables.ranges.emplace_back(*low, 2u * sizeof(std::uint32_t)); + } + if (records.states[record] != RecordState::Read) continue; + DescriptorValue value; + value.dwordCount = column.sampler ? 4u : 8u; + std::uint64_t first = std::numeric_limits::max(); + std::uint64_t past = 0; + for (std::uint32_t dword = 0; dword < column.dwordCount; dword++) { + auto address = ImageTableAbi::ScalarAddressDword(pointer, column.indirectOffset, column.indirectImmediate + dword * 4u); + if (address.has_value() && (*address & 3u) != 0u) address.reset(); + if (!address.has_value() || !AddressAccessible(*address)) { + records.states[record] = RecordState::Unmapped; + break; + } + value.dwords[dword] = Read(*address); + value.availableDwordMask |= static_cast(1u << dword); + first = std::min(first, *address); + past = std::max(past, *address + sizeof(std::uint32_t)); + } + if (records.states[record] != RecordState::Read) continue; + records.words[record] = WordsIndex(records, value); + tables.ranges.emplace_back(first, past - first); } - std::sort(keys.begin(), keys.end()); - keys.erase(std::unique(keys.begin(), keys.end()), keys.end()); - if (keys.size() > slots) { - rejectTable(BindlessRejection::Capacity, "bindless image table: " + std::to_string(keys.size()) + " distinct keys exceed the " + std::to_string(slots) + " slots"); + return; + } + const bool memoized = TableSnapshotReuse() && !records.narrowed && records.keys != 0u; + const ColumnKey key{records.base, records.size, records.records, records.keys, column.stride, column.addend, column.offset, column.dwordCount, column.maxKey, column.foldedAddend, column.foldedCarry, column.address, column.sampler}; + if (memoized && Reuse(key, column, records, false)) return; + if (memoized && !column.address && records.size != 0u && runtime.stampWrites != nullptr) { + const auto span = std::min(records.size, static_cast(column.addend) + column.offset + static_cast(records.keys) * column.stride + column.dwordCount * 4u); + stamp = runtime.stampWrites(runtime.userContext, records.base, span); + } + if (memoized && Reuse(key, column, records, true)) return; + unread(); + constexpr auto Unread = std::numeric_limits::max(); + std::vector whole(records.keys, Unread); + std::vector>> split; + std::vector> runs; + std::uint32_t total = 0; + std::uint64_t low = std::numeric_limits::max(); + std::uint64_t high = 0; + std::uint64_t checkedPage = std::numeric_limits::max(); + bool checkedAccessible = false; + const auto accessible = [&](std::uint64_t address) { + if (column.address) return AddressAccessible(address); + if ((address & ~(PageBytes - 1u)) != checkedPage) { + checkedPage = address & ~(PageBytes - 1u); + checkedAccessible = PageAccessible(address, records.base, end); + } + return checkedAccessible; + }; + const auto wholeAccessible = [&](std::uint64_t address) { + if (!column.address) return accessible(address); + const auto [found, inserted] = wholePages.try_emplace(address & ~(PageBytes - 1u), false); + if (inserted) found->second = Accessible(address & ~(PageBytes - 1u), PageBytes); + return found->second; + }; + const auto locate = [&](std::uint32_t offset, std::uint32_t immediate) -> std::optional { + if (column.address) return ImageTableAbi::ScalarAddressDword(records.base, offset, immediate); + const auto position = ImageTableAbi::ScalarBufferDword(offset, immediate, records.size); + return position.has_value() ? std::optional(records.base + *position) : std::nullopt; + }; + const auto take = [&](std::uint64_t address, std::uint32_t words) { + if (!runs.empty() && address > runs.back().first && address - runs.back().first == static_cast(runs.back().second) * 4u) { + runs.back().second += words; + } else { + runs.emplace_back(address, words); + } + if (column.address) records.readRanges.emplace_back(address, static_cast(words) * sizeof(std::uint32_t)); + low = std::min(low, address); + high = std::max(high, address + static_cast(words) * sizeof(std::uint32_t)); + total += words; + return total - words; + }; + const auto last = (column.dwordCount - 1u) * 4u; + for (std::uint32_t record = 0; record < records.keys; record++) { + if (records.states[record] != RecordState::Read) continue; + const auto offset = column.addend + record * column.stride; + if (offset >= column.foldedAddend && offset > 0xffffffffu - column.foldedCarry) { + records.states[record] = RecordState::Carried; + continue; + } + const auto head = locate(offset, column.offset); + const auto tail = locate(offset, column.offset + last); + if (head.has_value() && tail.has_value() && (*head & 3u) == 0u && *tail > *head && *tail - *head == last && wholeAccessible(*head) && wholeAccessible(*tail)) { + whole[record] = take(*head, column.dwordCount); + continue; + } + std::array slots; + slots.fill(Unread); + for (std::uint32_t dword = 0; dword < column.dwordCount; dword++) { + auto address = locate(offset, column.offset + dword * 4u); + if (column.address && address.has_value() && (*address & 3u) != 0u) address.reset(); + if (!address.has_value() && !column.address) continue; + if (!address.has_value() || !accessible(*address)) { + records.states[record] = RecordState::Unmapped; + break; + } + slots[dword] = take(*address, 1u); + } + if (records.states[record] == RecordState::Read) split.emplace_back(record, slots); + } + std::vector read; + if (!comparedWords.empty() && runs == comparedRuns) { + read = std::move(comparedWords); + } else { + read.resize(total); + ReadRuns(runs, read); + } + auto next = split.begin(); + for (std::uint32_t record = 0; record < records.keys; record++) { + if (records.states[record] != RecordState::Read) continue; + DescriptorValue value; + value.dwordCount = column.sampler ? 4u : 8u; + if (whole[record] != Unread) { + std::copy_n(read.begin() + whole[record], column.dwordCount, value.dwords.begin()); + value.availableDwordMask = static_cast((1u << column.dwordCount) - 1u); + } else { + for (std::uint32_t dword = 0; dword < column.dwordCount; dword++) { + if (next->second[dword] != Unread) { + value.dwords[dword] = read[next->second[dword]]; + value.availableDwordMask |= static_cast(1u << dword); + } + } + ++next; + } + records.words[record] = WordsIndex(records, value); + } + if (column.address && low < high) tables.ranges.emplace_back(low, high - low); + std::sort(records.readRanges.begin(), records.readRanges.end()); + std::size_t ranges = 0; + for (const auto& range : records.readRanges) { + if (ranges != 0u && range.first - records.readRanges[ranges - 1u].first <= records.readRanges[ranges - 1u].second) { + auto& previous = records.readRanges[ranges - 1u]; + previous.second = std::max(previous.second, range.first - previous.first + range.second); + } else { + records.readRanges[ranges++] = range; } } + records.readRanges.resize(ranges); + Seal(records); + if (memoized) Remember(key, records, std::move(runs), std::move(read), column.address && low < high ? low : 0u, column.address && low < high ? high : 0u); } - if (!materialMode) { - if (entries > slots) { - rejectTable(table.hasMaterial ? BindlessRejection::MaterialScan : BindlessRejection::Capacity, "bindless image table has " + std::to_string(entries) + " entries (" + std::to_string(materialEntries) + " materials), limit " + std::to_string(slots)); - } - keys.resize(entries); - for (std::uint32_t key = 0; key < entries; key++) keys[key] = key; - } - counters.outOfRange.fetch_add(outOfRange, std::memory_order_relaxed); - const std::uint64_t heapBase = heap.Base48(); - std::vector candidates(keys.size()); - std::vector valid(keys.size(), 0u); - std::uint32_t paddedNull = 0u; - for (std::size_t i = 0u; i < keys.size(); ++i) { - auto& candidate = candidates[i]; - candidate.dwordCount = 8u; - const std::uint64_t address = heapBase + table.entryOffset + static_cast(keys[i]) * TableEntryBytes; - for (std::uint32_t dword = 0u; dword < 8u; ++dword) readWord(address + dword * sizeof(std::uint32_t), candidate.dwords[dword]); - if (nullImageDescriptor(candidate)) { - ++paddedNull; - continue; - } - if (!validImageDescriptor(candidate, image.r128)) throw std::runtime_error("bindless image table contains an invalid descriptor"); - const auto decoded = decodeImageDescriptor(candidate, image, plan.srgbDecodeFormats); - if (decoded.fmask) throw std::runtime_error("bindless FMASK images are unsupported"); - static_cast(ResourceMaterializer::RuntimeImageMode(image, candidate, plan.info.runtimeImageModes.at(imageIndex))); - valid[i] = 1u; - } - resolution.mapping.clear(); - for (std::size_t i = 0u; i < keys.size(); ++i) { - if (valid[i] != 0u) resolution.mapping.emplace_back(keys[i], static_cast(i)); - } - DescriptorValue nullDescriptor; - nullDescriptor.dwordCount = 8u; - resolution.slots = std::move(candidates); - resolution.slots.resize(slots, nullDescriptor); - resolved = resolution.slots[0]; - (materialMode ? counters.tablesMaterial : counters.tablesWhole).fetch_add(1, std::memory_order_relaxed); - counters.keys.fetch_add(resolution.mapping.size(), std::memory_order_relaxed); - counters.paddedNull.fetch_add(paddedNull, std::memory_order_relaxed); - if (BindlessTraced()) traceTable(plan, image, table, heapBase, materialBase, {materialMode, entries, materialEntries, static_cast(resolution.mapping.size())}); -} + void ReadRuns(const std::vector>& runs, std::vector& read) const { + std::size_t at = 0; + for (const auto& [address, count] : runs) { + if (runtime.readWords != nullptr) { + if (!runtime.readWords(runtime.userContext, address, read.data() + at, count)) throw std::runtime_error("image table: failed to read guest memory at 0x" + hexText(address)); + } else { + for (std::uint32_t word = 0; word < count; word++) read[at + word] = Read(address + word * 4u); + } + at += count; + } + tableReuseTotals().wordsRead.fetch_add(read.size(), std::memory_order_relaxed); + } -void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, SrtWalker& walker, ResourceSnapshot& snapshot, std::vector& tables) { + bool WholePageAccessible(std::uint64_t address) { + const auto [found, inserted] = wholePages.try_emplace(address & ~(PageBytes - 1u), false); + if (inserted) found->second = Accessible(address & ~(PageBytes - 1u), PageBytes); + return found->second; + } + + bool Unwritten(const ColumnMemo& memo) const { + if (runtime.unchangedSince == nullptr || runtime.supplyWords == nullptr || memo.read->low >= memo.read->high) return false; + const auto vouched = LoadStamp(*memo.read); + if (vouched.generation == 0u || !runtime.unchangedSince(runtime.userContext, memo.read->low, memo.read->high - memo.read->low, vouched)) return false; + return runtime.supplyWords(runtime.userContext, memo.read); + } + + bool Compared(const TableColumn& column, const ColumnRecords& records, ColumnMemo& memo) { + const auto end = records.base + records.size; + for (const auto& [address, count] : memo.read->runs) { + const auto past = address + static_cast(count) * sizeof(std::uint32_t); + for (auto page = address & ~(PageBytes - 1u); page < past; page += PageBytes) { + const auto first = std::max(page, address); + if (column.address ? !WholePageAccessible(first) : !PageAccessible(first, records.base, end)) return false; + } + } + std::vector read(memo.read->words.size()); + ReadRuns(memo.read->runs, read); + if (read != memo.read->words) { + comparedRuns = memo.read->runs; + comparedWords = std::move(read); + return false; + } + StoreStamp(*memo.read, stamp); + return true; + } + + void Apply(const TableColumn& column, ColumnRecords& records, const ColumnMemo& memo) { + records.contents = memo.contents; + records.sealed = true; + if (column.address && memo.low < memo.high) tables.ranges.emplace_back(memo.low, memo.high - memo.low); + } + + bool Reuse(const ColumnKey& key, const TableColumn& column, ColumnRecords& records, bool compare) { + auto& memos = ColumnMemos(); + const auto found = std::find_if(memos.begin(), memos.end(), [&](const ColumnMemo& entry) { return entry.key == key; }); + if (found == memos.end()) return false; + auto& counters = tableCounters(); + auto& totals = tableReuseTotals(); + if (!compare && Unwritten(*found)) { + counters.reusedUnwritten.fetch_add(1, std::memory_order_relaxed); + totals.unwritten.fetch_add(1, std::memory_order_relaxed); + } else if (compare && Compared(column, records, *found)) { + counters.reusedCompared.fetch_add(1, std::memory_order_relaxed); + totals.compared.fetch_add(1, std::memory_order_relaxed); + } else { + return false; + } + Apply(column, records, *found); + totals.shared.fetch_add(1, std::memory_order_relaxed); + std::rotate(memos.begin(), found, found + 1); + return true; + } + + void Remember(const ColumnKey& key, const ColumnRecords& records, std::vector>&& runs, std::vector&& words, std::uint64_t low, std::uint64_t high) { + if (records.contents.poisoned != records.contents.carried) return; + auto& memos = ColumnMemos(); + auto found = std::find_if(memos.begin(), memos.end(), [&](const ColumnMemo& entry) { return entry.key == key; }); + if (found == memos.end()) found = memos.emplace(memos.end()); + auto& memo = *found; + memo.key = key; + std::uint64_t first = std::numeric_limits::max(); + std::uint64_t past = 0; + std::uint64_t total = 0; + bool ordered = !runs.empty(); + for (const auto& [address, count] : runs) { + const auto bytes = static_cast(count) * sizeof(std::uint32_t); + ordered = ordered && count != 0u && address % sizeof(std::uint32_t) == 0u && address >= past && address <= std::numeric_limits::max() - bytes; + first = std::min(first, address); + past = std::max(past, address + bytes); + total += count; + } + ordered = ordered && total == words.size(); + memo.read = std::make_shared(SrtGuestWords{std::move(runs), std::move(words), std::make_shared(), first < past ? first : 0u, first < past ? past : 0u, ordered}); + StoreStamp(*memo.read, stamp); + memo.contents = records.contents; + memo.low = low; + memo.high = high; + if (memo.Footprint() > ColumnMemoWords) { + memos.erase(found); + return; + } + std::rotate(memos.begin(), found, found + 1); + std::size_t kept = 0; + std::size_t footprint = 0; + while (kept < memos.size() && kept < ColumnMemoEntries && footprint + memos[kept].Footprint() <= ColumnMemoWords) footprint += memos[kept++].Footprint(); + memos.resize(kept); + } + + void Snapshot(std::uint32_t source, std::uint32_t table) { + const auto& column = *plan.descriptorSources.at(source).tableColumn; + const auto& records = Column(source); + auto& out = tables.tables.at(table); + out.base = records.base; + out.size = records.size; + out.records = records.records; + out.keys = records.keys; + out.fault = records.fault; + out.outside = records.outside; + out.codes = records.contents.codes; + out.words = records.contents.words; + out.readRanges = records.contents.readRanges; + auto& counters = tableCounters(); + if (records.fault != 0u) counters.poison[records.fault & ~ImageTableAbi::PoisonFlag].fetch_add(1, std::memory_order_relaxed); + if (records.contents.unmapped != 0u) counters.poison[static_cast(PoisonReason::Unmapped)].fetch_add(records.contents.unmapped, std::memory_order_relaxed); + if (records.contents.carried != 0u) counters.poison[static_cast(PoisonReason::OffsetCarry)].fetch_add(records.contents.carried, std::memory_order_relaxed); + counters.keys.fetch_add(records.keys, std::memory_order_relaxed); + if (TableTraced()) traceTable(plan, table, column, {records.base, records.size, records.records, records.keys, records.contents.poisoned, records.narrowed}); + } + + const IrResourcePlan& plan; + const SrtRuntime& runtime; + SrtWalker& walker; + const std::vector& activeSources; + ImageTableSnapshot& tables; + std::map columns; + HashScratchIndex wordsIndex{threadScratch()}; + std::unordered_map pages; + std::unordered_map wholePages; + SrtWriteStamp stamp; + std::vector> comparedRuns; + std::vector comparedWords; + std::vector keyWords; +}; + +void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, SrtWalker& walker, ResourceSnapshot& snapshot, std::vector& activeSources) { snapshot = ResourceSnapshot{}; if (plan.uniformFill.fill.kind != UniformFillKind::None) { const auto words = plan.uniformFill.fill.words; @@ -442,41 +1288,34 @@ void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, } snapshot.userData.assign(runtime.userData.begin(), runtime.userData.begin() + plan.userDataCount); - std::vector values; - std::vector activeSources; + struct ValuesStorage {}; + auto& values = HostThreadLocal, ValuesStorage>(); + values.clear(); + const auto evaluateStarted = MaterializeProfiled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; walker.EvaluateRuntimeSources(plan, plan.materializationSources, runtime, values, snapshot.flattenedSrt, plan.cleanFlatSlots, activeSources, &snapshot.srtPoison, &snapshot.nullRootReads); + if (MaterializeProfiled()) evaluateNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - evaluateStarted).count()), std::memory_order_relaxed); std::size_t cursor = 0; if (values.size() < plan.info.buffers.size()) { throw std::runtime_error("materialization sources are missing buffer descriptors"); } snapshot.buffers.assign(values.begin(), values.begin() + plan.info.buffers.size()); + if (GcnGuestShaders()) { + for (auto& buffer : snapshot.buffers) { + buffer.dwords[1] &= GcnBufferWord1Mask; + buffer.dwords[3] = GcnBufferWord3(buffer.dwords[1], buffer.dwords[3]); + } + } cursor += plan.info.buffers.size(); snapshot.images.resize(plan.info.images.size()); - tables.assign(plan.info.images.size(), {}); - std::uint32_t tableCount = 0; for (std::uint32_t i = 0; i < plan.info.images.size(); i++) { const auto& image = plan.info.images[i]; if (image.source >= plan.descriptorSources.size()) { throw std::runtime_error("image resource references an unknown descriptor source"); } - if (plan.descriptorSources[image.source].indirectImage.has_value()) tableCount++; - } - const auto tableSlots = ResourceMaterializer::BindlessSlots(); - if (tableCount != 0u && plan.info.images.size() + static_cast(tableSlots - 1u) * tableCount > ShaderInfo::MaxImages) { - rejectTable(BindlessRejection::ImageSlots, "bindless image tables need " + std::to_string(plan.info.images.size() + static_cast(tableSlots - 1u) * tableCount) + " image slots, limit " + std::to_string(ShaderInfo::MaxImages)); - } - for (std::uint32_t i = 0; i < plan.info.images.size(); i++) { - const auto& image = plan.info.images[i]; - const auto& source = plan.descriptorSources[image.source]; - if (source.indirectImage.has_value()) { - if (image.source < activeSources.size() && activeSources[image.source] == 0u) { - snapshot.images[i].dwordCount = 8u; - tables[i].slots.assign(tableSlots, snapshot.images[i]); - continue; - } - resolveTableImage(plan, i, *source.indirectImage, runtime, walker, snapshot.images[i], tables[i]); + if (image.table != NoTable) { + snapshot.images[i].dwordCount = 8u; continue; } if (cursor >= values.size()) { @@ -487,51 +1326,106 @@ void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, if (descriptor.dwordCount != 8u) { throw std::runtime_error("image descriptor has an invalid width"); } + if (GcnGuestShaders()) { + const auto words = GcnImageWords(std::span(descriptor.dwords.data(), 8u)); + std::copy(words.begin(), words.end(), descriptor.dwords.begin()); + } const auto activity = plan.descriptorSources.size() + i; if (activity < activeSources.size() && activeSources[activity] == 0u) { descriptor = DescriptorValue{}; descriptor.dwordCount = 8u; } - if (!validImageDescriptor(descriptor, image.r128) && !nullImageDescriptor(descriptor)) { + if (!validImageDescriptor(descriptor, image.r128) && !nullImageDescriptor(image, descriptor)) { throw std::runtime_error("runtime image descriptor is invalid"); } snapshot.images[i] = descriptor; } - if (values.size() < cursor + plan.info.samplers.size()) { - throw std::runtime_error("materialization sources are missing sampler descriptors"); + snapshot.samplers.resize(plan.info.samplers.size()); + for (std::uint32_t i = 0; i < plan.info.samplers.size(); i++) { + if (plan.info.samplers[i].table != NoTable) { + snapshot.samplers[i].dwordCount = 4u; + continue; + } + if (cursor >= values.size()) { + throw std::runtime_error("materialization sources are missing sampler descriptors"); + } + snapshot.samplers[i] = values[cursor]; + cursor++; + } +} + +void materializeForcedDegamma(const IrResourcePlan& plan, ResourceSnapshot& snapshot) { + for (std::uint32_t index = 0; index < plan.info.images.size(); ++index) { + auto& descriptor = snapshot.images.at(index); + if (((descriptor.dwords[1] >> 20u) & 0x1ffu) != static_cast(IrBufferFormat::Format8_8_8_8UInt)) continue; + bool forced = false; + bool plain = false; + for (const auto& pair : plan.info.sampledPairs) { + if (pair.image != index) continue; + const auto& sampler = snapshot.samplers.at(pair.sampler); + if (sampler.dwordCount != 4u) throw std::runtime_error("FORCE_DEGAMMA requires four sampler descriptor words"); + const bool enabled = (sampler.dwords[0] & (1u << 20u)) != 0u; + forced = forced || enabled; + plain = plain || !enabled; + } + if (!forced) continue; + const auto& image = plan.info.images.at(index); + if (!image.singleMipPlaneBiasCompatible || image.dimension != RdnaImageDimension::Dim2D || image.table != NoTable || image.resourceClass != ImageResourceClass::Sampled || image.depthCompare || image.packed) throw std::runtime_error("FORCE_DEGAMMA on RGBA8_UINT requires a direct plain biased pixel sample"); + if (plain) throw std::runtime_error("FORCE_DEGAMMA on RGBA8_UINT with mixed sampler states is not implemented"); + if ((descriptor.dwords[3] >> 28u) != 10u || (descriptor.dwords[4] & 0x1fffu) != 0u || (descriptor.dwords[3] & 0xff000u) != 0u || (descriptor.dwords[5] & 0xf0u) != 0u) throw std::runtime_error("FORCE_DEGAMMA on RGBA8_UINT requires a one-slice volume at mip zero"); + descriptor.dwords[1] = (descriptor.dwords[1] & ~(0x1ffu << 20u)) | (static_cast(IrBufferFormat::Format8_8_8_8Srgb) << 20u); } - snapshot.samplers.assign(values.begin() + cursor, values.begin() + cursor + plan.info.samplers.size()); } std::uint32_t colorCompareReference(IrBufferFormat format) { - switch (format) { - case IrBufferFormat::Format16_16UNorm: - case IrBufferFormat::Format11_11_10UNorm: case IrBufferFormat::Format10_11_11UNorm: case IrBufferFormat::Format2_10_10_10UNorm: - case IrBufferFormat::Format10_10_10_2UNorm: case IrBufferFormat::Format16_16_16_16UNorm: - return EmulatedCompare::ReferenceUnorm; - case IrBufferFormat::Format8UNorm: case IrBufferFormat::Format8_8UNorm: case IrBufferFormat::Format8_8_8_8UNorm: - case IrBufferFormat::Format8Srgb: case IrBufferFormat::Format8_8Srgb: case IrBufferFormat::Format8_8_8_8Srgb: - return EmulatedCompare::ReferenceZero; - case IrBufferFormat::Format8SNorm: case IrBufferFormat::Format16SNorm: case IrBufferFormat::Format8_8SNorm: case IrBufferFormat::Format16_16SNorm: - case IrBufferFormat::Format11_11_10SNorm: case IrBufferFormat::Format10_11_11SNorm: case IrBufferFormat::Format2_10_10_10SNorm: - case IrBufferFormat::Format10_10_10_2SNorm: case IrBufferFormat::Format8_8_8_8SNorm: case IrBufferFormat::Format16_16_16_16SNorm: - return EmulatedCompare::ReferenceSnorm; - case IrBufferFormat::Format16Float: case IrBufferFormat::Format16_16Float: case IrBufferFormat::Format11_11_10Float: - case IrBufferFormat::Format10_11_11Float: case IrBufferFormat::Format32_32Float: case IrBufferFormat::Format16_16_16_16Float: - return EmulatedCompare::ReferenceFloat; - default: - throw std::runtime_error("comparison sampling of a color texture is implemented only for float, unorm and snorm formats (format " + std::to_string(static_cast(format)) + ")"); + const auto value = static_cast(format); + if (((value >= 1u && value <= 77u) || (value >= 128u && value <= 130u)) && SampledTextureNumericClass(format) == IrTextureNumericClass::Float && format != IrBufferFormat::Format11_11_10UNorm && format != IrBufferFormat::Format10_11_11Float) return EmulatedCompare::ReferenceZero; + throw std::runtime_error("comparison sampling of a color texture is implemented only for floating-point, UNORM and SNORM formats (format " + std::to_string(value) + ")"); +} + +bool convertedSampleCompatible(const ImageResource& image) { + return image.resourceClass != ImageResourceClass::Storage && !image.depthCompare && !image.atomic && !image.packed && image.table == NoTable && image.depthBitsCompatible && (image.emulatedCompare & EmulatedCompare::ConvertedSampleUnsupported) == 0u && (image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray); +} + +std::uint32_t convertedSampleState(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint32_t index) { + const auto& image = info.images[index]; + if (image.srgbDecodeCompatible || image.depthCompare || image.resourceClass == ImageResourceClass::Storage || index >= snapshot.images.size()) return 0u; + const auto& descriptor = snapshot.images[index]; + if (descriptor.dwordCount != 8u || nullImageDescriptor(image, descriptor)) return 0u; + const auto format = rawImageFormat(descriptor); + if (format != IrBufferFormat::Format11_11_10UNorm) return 0u; + std::optional samplerState; + for (const auto& pair : info.sampledPairs) { + if (pair.image != index) continue; + if (pair.sampler >= snapshot.samplers.size() || snapshot.samplers[pair.sampler].dwordCount != 4u) throw std::runtime_error("sampling of a converted image has no sampler descriptor"); + const auto& words = snapshot.samplers[pair.sampler].dwords; + if (((words[0] >> 29u) & 0x3u) != 0u) throw std::runtime_error("sampling of a converted image through a min or max reduction sampler is not implemented"); + if (((words[0] >> 15u) & 0x1u) != 0u) throw std::runtime_error("sampling of a converted image does not implement unnormalized coordinates"); + const auto magFilter = (words[2] >> 20u) & 0x3u; + const auto minFilter = (words[2] >> 22u) & 0x3u; + if (magFilter != minFilter || magFilter > 1u) throw std::runtime_error("sampling of a converted image is implemented only with equal point or bilinear minification and magnification filters"); + const auto addressMode = [](std::uint32_t clamp) { + if (clamp == 0u) return EmulatedCompare::AddressWrap; + if (clamp == 2u) return EmulatedCompare::AddressEdge; + throw std::runtime_error("sampling of a converted image is implemented only with wrap or clamp-to-edge addressing"); + }; + const auto state = EmulatedCompare::Enabled | (magFilter == 1u ? EmulatedCompare::Linear : 0u) | (addressMode(words[0] & 0x7u) << EmulatedCompare::ClampXShift) | (addressMode((words[0] >> 3u) & 0x7u) << EmulatedCompare::ClampYShift); + if (samplerState.has_value() && *samplerState != state) throw std::runtime_error("sampling of a converted image through samplers that disagree is not implemented"); + samplerState = state; } + if (!samplerState.has_value()) throw std::runtime_error("sampling of a converted image has no paired sampler"); + return *samplerState; } std::uint32_t emulatedCompareState(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint32_t index) { const auto& image = info.images[index]; const auto& descriptor = snapshot.images[index]; - if (!image.depthCompare || descriptor.dwordCount != 8u || nullImageDescriptor(descriptor)) return 0u; + if (!image.depthCompare) return convertedSampleState(info, snapshot, index); + if (descriptor.dwordCount != 8u || nullImageDescriptor(image, descriptor)) return 0u; const auto format = rawImageFormat(descriptor); if (format == IrBufferFormat::Format32Float || format == IrBufferFormat::Format16UNorm || IsDepthBitsTexture(descriptor.dwords[1], descriptor.dwords[3])) return 0u; - if (image.indirectRoot != ImageResource::NoIndirectImage || (image.emulatedCompare & EmulatedCompare::Unsupported) != 0u) throw std::runtime_error("unsupported color comparison image instructions"); + if (image.table != NoTable || (image.emulatedCompare & EmulatedCompare::Unsupported) != 0u) throw std::runtime_error("unsupported color comparison image instructions"); if ((image.emulatedCompare & EmulatedCompare::RequiresSingleLevel) != 0u && ((descriptor.dwords[3] >> 12u) & 0xfu) != ((descriptor.dwords[3] >> 16u) & 0xfu)) throw std::runtime_error("color comparison requires a single mip level"); const auto reference = colorCompareReference(format); const auto type = rawImageType(descriptor); @@ -548,6 +1442,7 @@ std::uint32_t emulatedCompareState(const ShaderInfo& info, const ResourceSnapsho std::optional samplerState; for (const auto& pair : info.sampledPairs) { if (pair.image != index) continue; + if (pair.sampler >= info.samplers.size() || info.samplers[pair.sampler].table != NoTable) throw std::runtime_error("comparison sampling of a color texture through a sampler table is not implemented"); if (pair.sampler >= snapshot.samplers.size() || snapshot.samplers[pair.sampler].dwordCount != 4u) throw std::runtime_error("comparison sampling of a color texture has no sampler descriptor"); const auto& words = snapshot.samplers[pair.sampler].dwords; const auto clampX = words[0] & 0x7u; @@ -584,7 +1479,7 @@ std::uint32_t emulatedCompareState(const ShaderInfo& info, const ResourceSnapsho return *samplerState | (reference << EmulatedCompare::ReferenceShift) | (singleLevel ? EmulatedCompare::SingleLevel : 0u); } -void materializeTables(const IrResourcePlan& plan, ResourceSnapshot& snapshot, const std::vector& tables) { +void materializeTables(const IrResourcePlan& plan, ResourceSnapshot& snapshot) { for (std::uint32_t i = 0u; i < plan.info.buffers.size(); ++i) { const auto& buffer = plan.info.buffers[i]; const bool scalarOnly = buffer.scalar && !buffer.written && !buffer.atomic && !buffer.formatted && buffer.imageAlias == BufferResource::NoImageAlias @@ -594,29 +1489,23 @@ void materializeTables(const IrResourcePlan& plan, ResourceSnapshot& snapshot, c snapshot.buffers.at(i).dwords[3] = 0u; } const auto decoded = decodeBufferDescriptor(snapshot.buffers.at(i)); - if (decoded.Type() != 0u) throw std::runtime_error("buffer descriptor uses an unsupported type"); + if (decoded.Type() != 0u) { + const auto& words = snapshot.buffers.at(i).dwords; + char detail[64]; + std::snprintf(detail, sizeof(detail), " (buffer %u V# %08x %08x %08x %08x)", i, words[0], words[1], words[2], words[3]); + throw std::runtime_error(std::string("buffer descriptor uses an unsupported type") + detail); + } if (plan.stage != IrShaderStage::Compute && decoded.AddTid()) throw std::runtime_error("buffer ADD_TID is only valid for compute shaders"); } if (snapshot.flattenedSrt.size() != plan.srtReads.size()) throw std::runtime_error("runtime SRT size differs from the static interface"); for (std::uint32_t i = 0u; i < plan.info.images.size(); ++i) { const auto& image = plan.info.images[i]; - static_cast(ResourceMaterializer::RuntimeImageMode(image, snapshot.images.at(i), plan.info.runtimeImageModes.at(i))); - const auto& table = tables.at(i); - if (!plan.descriptorSources.at(image.source).indirectImage.has_value()) { - if (!table.slots.empty()) throw std::runtime_error("direct image has runtime table slots"); - continue; + if (image.table != NoTable) continue; + const auto& descriptor = snapshot.images.at(i); + if (!image.convertedSamples && !image.srgbDecodeCompatible && convertedSampleCompatible(image) && descriptor.dwordCount == 8u && !nullImageDescriptor(image, descriptor)) { + if (rawImageFormat(descriptor) == IrBufferFormat::Format11_11_10UNorm) throw ConvertedSampleRequired("samples a converted image through a shader prepared without converted-sample modes"); } - const auto slots = ResourceMaterializer::BindlessSlots(); - if (table.slots.size() != slots) throw std::runtime_error("runtime table size differs from the static interface"); - const auto mappingOffset = snapshot.flattenedSrt.size(); - snapshot.flattenedSrt.push_back(static_cast(table.mapping.size())); - for (const auto& [key, slot] : table.mapping) { - if (slot >= slots) throw std::runtime_error("runtime table slot exceeds its static capacity"); - snapshot.flattenedSrt.push_back(key); - snapshot.flattenedSrt.push_back(slot == 0u ? i : static_cast(snapshot.images.size()) + slot - 1u); - } - snapshot.flattenedSrt.resize(mappingOffset + 1u + 2u * slots, 0u); - snapshot.images.insert(snapshot.images.end(), table.slots.begin() + 1u, table.slots.end()); + static_cast(ResourceMaterializer::RuntimeImageMode(image, descriptor, plan.info.runtimeImageModes.at(i))); } } @@ -633,9 +1522,334 @@ void materializeSrtGuards(const IrResourcePlan& plan, ResourceSnapshot& snapshot } } +struct DecodeMemoKey { + std::uint64_t artifact = 0; + std::uint32_t resource = 0; + std::array words{}; + + bool operator==(const DecodeMemoKey& other) const = default; +}; + +struct DecodeMemoHash { + std::size_t operator()(const DecodeMemoKey& key) const { + std::uint64_t hash = 0xcbf29ce484222325ull; + const auto mix = [&](std::uint64_t value) { + hash ^= value; + hash *= 0x100000001b3ull; + }; + mix(key.artifact); + mix(key.resource); + for (const auto word : key.words) mix(word); + return static_cast(hash); + } +}; + +struct DecodeMemo { + std::mutex mutex; + std::unordered_map entries; +}; + +DecodeMemo& decodeMemo() { + static DecodeMemo memo; + return memo; +} + +constexpr std::uint32_t DecodeMemoEntries = 1u << 20u; +constexpr std::uint32_t DecodeFailed = 0x80000000u; + +std::uint32_t decodeTableEntry(const ImageResource& image, std::span modes, const DescriptorValue& words, std::uint64_t memoKey, std::uint32_t resource) { + DecodeMemoKey key{memoKey, resource, words.dwords}; + if (memoKey != 0u) { + auto& memo = decodeMemo(); + std::lock_guard lock(memo.mutex); + if (const auto found = memo.entries.find(key); found != memo.entries.end()) return found->second; + } + const auto match = ResourceMaterializer::TryRuntimeImageMode(image, words, modes); + const auto value = match.mode.has_value() ? *match.mode : DecodeFailed | static_cast(match.reason); + if (memoKey != 0u) { + auto& memo = decodeMemo(); + std::lock_guard lock(memo.mutex); + if (memo.entries.size() >= DecodeMemoEntries) memo.entries.clear(); + memo.entries.emplace(key, value); + } + return value; +} + +std::vector reducingSamplers(const ShaderInfo& info, const ResourceSnapshot& snapshot) { + std::vector reducing(info.images.size(), false); + for (const auto& pair : info.sampledPairs) { + if (info.samplers.at(pair.sampler).table != NoTable) continue; + const auto& words = snapshot.samplers.at(pair.sampler).dwords; + if (reducesBetweenTexels(words[0], words[2])) reducing.at(pair.image) = true; + } + return reducing; +} + +bool TableResolutionReuse() { + static const bool enabled = std::getenv("APS5_NO_TABLE_RESOLUTION_REUSE") == nullptr; + return enabled; +} + +struct ResolutionMemoEntry { + std::uint64_t memoKey = 0; + std::vector tables; + std::vector reducing; + std::shared_ptr resolved; + std::size_t footprint = 0; +}; + +struct ResolutionMemo { + std::mutex mutex; + std::vector entries; +}; + +ResolutionMemo& resolutionMemo() { + static ResolutionMemo memo; + return memo; +} + +constexpr std::size_t ResolutionMemoEntries = 256; +constexpr std::size_t ResolutionMemoVariantEntries = 4; +constexpr std::size_t ResolutionMemoWords = std::size_t{1} << 23u; + +bool sameColumns(const std::vector& left, const std::vector& right) { + if (left.size() != right.size()) return false; + for (std::size_t index = 0; index < left.size(); index++) { + const auto& a = left[index]; + const auto& b = right[index]; + if (a.base != b.base || a.size != b.size || a.records != b.records || a.keys != b.keys || a.fault != b.fault || a.outside != b.outside || !a.codes.Shares(b.codes) || !a.words.Shares(b.words)) return false; + } + return true; +} + +std::size_t resolutionFootprint(const ResolvedImageTables& resolved, const std::vector& tables) { + std::size_t footprint = resolved.map.size() + (resolved.elements.size() * sizeof(ImageTableElement) + resolved.samplers.size() * sizeof(ImageTableSamplerElement) + resolved.poison.size() * sizeof(ImageTablePoisonRecord)) / sizeof(std::uint32_t); + for (const auto& table : tables) footprint += 16u + table.codes.size() + table.words.size() * 9u; + return footprint; +} + +} + +ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint64_t memoKey) { + namespace Abi = ImageTableAbi; + ResolvedImageTables out; + std::uint32_t count = 0; + for (const auto& image : info.images) { + if (image.table != NoTable) count = std::max(count, image.table + 1u); + } + for (const auto& sampler : info.samplers) { + if (sampler.table != NoTable) count = std::max(count, sampler.table + 1u); + } + if (count == 0u) return out; + const auto& tables = snapshot.tables; + if (tables.tables.size() != count) throw std::runtime_error("image table snapshot disagrees with the static tables"); + if (info.runtimeImageModes.size() != info.images.size()) throw std::runtime_error("prepared runtime image modes are missing"); + out.map.assign(Abi::HeaderWords + Abi::TableWords * count, 0u); + out.map[Abi::HeaderVersion] = Abi::Version; + out.poison.push_back({{}, 0u, NoTable, PoisonReason::OutsideSnapshot}); + out.poison.push_back({{}, 0u, NoTable, PoisonReason::OutsideDomain}); + 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 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); + }; + HashScratchIndex elementIndex{threadScratch()}; + std::vector directSamplers(info.images.size(), 0u); + for (const auto& pair : info.sampledPairs) { + if (info.samplers.at(pair.sampler).table == NoTable) directSamplers.at(pair.image) |= 1u << pair.sampler; + } + const auto reducingSampler = reducingSamplers(info, snapshot); + const auto imageRecord = [&](std::uint32_t resource, const DescriptorValue& words) { + const auto& image = info.images[resource]; + const auto& modes = info.runtimeImageModes[resource]; + std::array record{0u, words.dwords[3], 0u}; + if ((words.availableDwordMask & (1u << 3u)) == 0u) { + record[Abi::ImageRecordFlags] = poison(resource, PoisonReason::OutsideSnapshot, &words); + const auto offset = static_cast(out.map.size()); + out.map.insert(out.map.end(), record.begin(), record.end()); + return offset; + } + const auto decoded = decodeTableEntry(image, modes, words, memoKey, resource); + if ((decoded & DecodeFailed) != 0u) { + record[Abi::ImageRecordFlags] = poison(resource, static_cast(decoded & ~DecodeFailed), &words); + } else { + const auto& mode = modes.at(decoded); + if (mode.dimension == RdnaImageDimension::Dim2DMsaa || mode.dimension == RdnaImageDimension::Dim2DMsaaArray) { + record[Abi::ImageRecordFlags] = poison(resource, PoisonReason::Multisampled, &words); + } else if (requiresPointSampler(mode) && reducingSampler[resource]) { + record[Abi::ImageRecordFlags] = poison(resource, PoisonReason::Reduction, &words); + } else { + auto element = words.dwords; + if (mode.conversionFormat != IrBufferFormat::Invalid || mode.depthBits) element[3] = (element[3] & ~0xfffu) | ShaderImageIdentitySwizzle; + const auto kind = DescriptorBindingForImage(mode); + 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 (!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] = *index; + } + } + } + const auto offset = static_cast(out.map.size()); + out.map.insert(out.map.end(), record.begin(), record.end()); + return offset; + }; + const auto samplerRecord = [&](std::uint32_t resource, const DescriptorValue& words) { + std::array record{0u, 0u}; + if ((words.dwords[0] & (1u << 15u)) != 0u) { + record[Abi::SamplerRecordFlags] = poison(resource, PoisonReason::Unnormalized, &words); + } else if (((words.dwords[0] >> 29u) & 3u) != 0u) { + record[Abi::SamplerRecordFlags] = poison(resource, PoisonReason::Reduction, &words); + } else if (out.samplers.size() + Abi::SamplerEntryElements > Abi::SamplerElementBudget) { + record[Abi::SamplerRecordFlags] = poison(resource, PoisonReason::Budget, &words); + } else { + record[Abi::SamplerRecordElement] = static_cast(out.samplers.size()); + for (std::uint32_t variant = 0; variant < 2u; variant++) { + const bool compare = variant == 1u; + for (std::uint32_t point = 0; point < 2u; point++) { + ImageTableSamplerElement element; + std::copy_n(words.dwords.begin(), 4u, element.words.begin()); + element.compare = compare; + element.point = point != 0u; + out.samplers.push_back(element); + } + } + } + const auto offset = static_cast(out.map.size()); + out.map.insert(out.map.end(), record.begin(), record.end()); + return offset; + }; + std::vector outside(count, false); + std::size_t recordCodes = 0; + for (const auto& column : tables.tables) recordCodes = std::max(recordCodes, column.words.size()); + DenseScratchMap records(threadScratch(), recordCodes); + 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); + out.map[header + Abi::TableKeyCount] = column.keys; + out.map[header + Abi::TableSizeLow] = static_cast(column.size); + out.map[header + Abi::TableSizeHigh] = static_cast(column.size >> 32u); + out.map[header + Abi::TableBaseLow] = static_cast(column.base); + out.map[header + Abi::TableBaseHigh] = static_cast(column.base >> 32u); + if (column.codes.size() != column.keys) throw std::runtime_error("image table snapshot has a truncated map"); + if (column.fault != 0u) { + out.map[header + Abi::TableFault] = poison(resource, static_cast(column.fault & ~Abi::PoisonFlag), nullptr); + } else if (!sampler && (info.images[resource].tableOperation == TableOperation::Unsupported || info.runtimeImageModes[resource].empty())) { + out.map[header + Abi::TableFault] = poison(resource, PoisonReason::UnsupportedOperation, nullptr); + } + outside[table] = column.outside; + const auto mapStart = static_cast(out.map.size()); + out.map[header + Abi::TableMapStart] = mapStart; + out.map.resize(out.map.size() + column.keys, 0u); + records.Clear(); + for (std::uint32_t key = 0; key < column.keys; key++) { + const auto code = column.codes[key]; + std::uint32_t mapped = Abi::NullCode; + if ((code & Abi::PoisonFlag) != 0u) { + 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) { + if (const auto known = records.Find(code)) { + mapped = *known; + } else { + const auto& words = column.words.at(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; + } + if (sampler && out.map[header + Abi::TableFault] == 0u) { + DescriptorValue zero; + zero.dwordCount = 4u; + out.map[header + Abi::TableOutsideCode] = samplerRecord(resource, zero); + } + }; + for (std::uint32_t resource = 0; resource < info.images.size(); resource++) { + if (info.images[resource].table != NoTable) fill(resource, info.images[resource].table, false); + } + for (std::uint32_t resource = 0; resource < info.samplers.size(); resource++) { + if (info.samplers[resource].table != NoTable) fill(resource, info.samplers[resource].table, true); + } + out.map[Abi::HeaderElementCount] = static_cast(out.elements.size()); + out.map[Abi::HeaderSamplerCount] = static_cast(out.samplers.size()); + out.map[Abi::HeaderPoisonCount] = static_cast(out.poison.size()); + out.map[Abi::HeaderTableCount] = count; + out.map[Abi::HeaderElementFaults] = static_cast(out.map.size()); + 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(expectedCount + static_cast(std::count(outside.begin(), outside.end(), true))); + return out; +} + +std::shared_ptr ResourceMaterializer::ResolveImageTablesShared(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint64_t memoKey) { + if (memoKey == 0u || snapshot.tables.tables.empty() || !TableResolutionReuse()) return std::make_shared(ResolveImageTables(info, snapshot, memoKey)); + auto reducing = reducingSamplers(info, snapshot); + auto& memo = resolutionMemo(); + auto& totals = tableReuseTotals(); + { + std::lock_guard lock(memo.mutex); + const auto found = std::find_if(memo.entries.begin(), memo.entries.end(), [&](const ResolutionMemoEntry& entry) { return entry.memoKey == memoKey && entry.reducing == reducing && sameColumns(entry.tables, snapshot.tables.tables); }); + if (found != memo.entries.end()) { + std::rotate(memo.entries.begin(), found, found + 1); + totals.resolutionsShared.fetch_add(1, std::memory_order_relaxed); + return memo.entries.front().resolved; + } + } + auto resolved = std::make_shared(ResolveImageTables(info, snapshot, memoKey)); + totals.resolutions.fetch_add(1, std::memory_order_relaxed); + const auto footprint = resolutionFootprint(*resolved, snapshot.tables.tables); + if (footprint > ResolutionMemoWords) return resolved; + std::lock_guard lock(memo.mutex); + memo.entries.insert(memo.entries.begin(), ResolutionMemoEntry{memoKey, snapshot.tables.tables, std::move(reducing), resolved, footprint}); + std::size_t kept = 0; + std::size_t variantEntries = 0; + std::size_t total = 0; + for (std::size_t index = 0; index < memo.entries.size(); index++) { + auto& entry = memo.entries[index]; + const bool sameVariant = entry.memoKey == memoKey; + if ((sameVariant && variantEntries == ResolutionMemoVariantEntries) || kept == ResolutionMemoEntries || total + entry.footprint > ResolutionMemoWords) continue; + if (sameVariant) variantEntries++; + total += entry.footprint; + if (kept != index) memo.entries[kept] = std::move(entry); + kept++; + } + memo.entries.resize(kept); + return resolved; } std::vector ResourceMaterializer::RuntimeImageModes(const ImageResource& image) { + const bool table = image.table != NoTable; + if (table && image.tableOperation == TableOperation::Unsupported) return {}; std::vector modes; const bool storage = image.resourceClass == ImageResourceClass::Storage; const auto append = [&](IrTextureNumericClass numeric, IrBufferFormat conversion, IrBufferFormat packed, bool depth, bool unorm16) { @@ -652,11 +1866,24 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe modes.push_back(mode); const bool volumeLoad = (image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray) && image.fmaskCompatible; const bool volumeStore = image.dimension == RdnaImageDimension::Dim2DArray && storage && image.written && !image.read && !image.atomic && image.mipMode != ImageMipMode::DynamicStorage; - if ((volumeLoad || volumeStore) && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { + const bool volumePlaneBias = image.dimension == RdnaImageDimension::Dim2D && image.singleMipPlaneBiasCompatible && !storage && image.table == NoTable && conversion == IrBufferFormat::Invalid; + const bool volumeArraySample = image.dimension == RdnaImageDimension::Dim2DArray && image.volumeArraySampleCompatible && !storage && image.table == NoTable && conversion == IrBufferFormat::Invalid; + if ((volumeLoad || volumeStore || volumePlaneBias || volumeArraySample) && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { auto volume = mode; volume.dimension = RdnaImageDimension::Dim3D; modes.push_back(volume); } + const bool arrayStore = image.dimension == RdnaImageDimension::Dim3D && storage && image.written && !image.read && !image.atomic && image.mipMode != ImageMipMode::DynamicStorage; + if (arrayStore && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { + auto array = mode; + array.dimension = RdnaImageDimension::Dim2DArray; + modes.push_back(array); + } + if (image.dimension == RdnaImageDimension::Dim2DMsaa && image.fmaskCompatible && image.cubeFaceCompatible && !table && !storage && !depth && conversion == IrBufferFormat::Invalid && packed == IrBufferFormat::Invalid && image.byElements == 0u) { + auto plane = mode; + plane.dimension = RdnaImageDimension::Dim2D; + modes.push_back(plane); + } if (image.dimension == RdnaImageDimension::Dim3D && image.flatVolumeCompatible && !storage && !depth && conversion == IrBufferFormat::Invalid && packed == IrBufferFormat::Invalid) { for (const auto dimension : {RdnaImageDimension::Dim2D, RdnaImageDimension::Dim2DArray}) { auto plane = mode; @@ -664,12 +1891,24 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe modes.push_back(plane); } } + if (image.dimension == RdnaImageDimension::Dim3D && image.flatVolumeLineCompatible && !storage && !depth && conversion == IrBufferFormat::Invalid && packed == IrBufferFormat::Invalid) { + auto line = mode; + line.dimension = RdnaImageDimension::Dim1D; + modes.push_back(line); + } if (image.dimension == RdnaImageDimension::Dim2D && storage && image.written && !image.read && !image.atomic && image.mipMode != ImageMipMode::DynamicStorage && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { auto line = mode; line.dimension = RdnaImageDimension::Dim1D; modes.push_back(line); } - if (image.dimension == RdnaImageDimension::Dim2D && image.flatLineCompatible && !storage && !depth && conversion == IrBufferFormat::Invalid && packed == IrBufferFormat::Invalid) { + const bool cubeFaceStorage = image.written && !image.read && !image.atomic && image.mipMode != ImageMipMode::DynamicStorage; + if (image.dimension == RdnaImageDimension::Dim2D && image.cubeFaceCompatible && (!storage || cubeFaceStorage) && !depth && conversion == IrBufferFormat::Invalid && packed == IrBufferFormat::Invalid) { + auto face = mode; + face.dimension = RdnaImageDimension::Dim2DArray; + face.cube = true; + modes.push_back(face); + } + 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); @@ -684,7 +1923,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe plain.dimension = image.dimension == RdnaImageDimension::Dim1DArray ? RdnaImageDimension::Dim1D : image.dimension == RdnaImageDimension::Dim2DArray ? RdnaImageDimension::Dim2D : RdnaImageDimension::Dim2DMsaa; modes.push_back(plain); } - if (image.dimension == RdnaImageDimension::Dim2DArray) { + if (image.dimension == RdnaImageDimension::Dim2DArray && !image.remappedDepth1D) { mode.cube = true; modes.push_back(mode); } @@ -713,27 +1952,24 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe append(IrTextureNumericClass::Uint, IrBufferFormat::Invalid, IrBufferFormat::Invalid, false, false); if (!storage) append(IrTextureNumericClass::Sint, IrBufferFormat::Invalid, IrBufferFormat::Invalid, false, false); append(IrTextureNumericClass::Uint, IrBufferFormat::Format11_11_10UInt, IrBufferFormat::Invalid, false, false); - if (image.srgbDecodeCompatible && image.depthBitsCompatible) { - append(IrTextureNumericClass::Uint, IrBufferFormat::Format11_11_10UNorm, IrBufferFormat::Invalid, false, false); - append(IrTextureNumericClass::Uint, IrBufferFormat::Format10_11_11Float, IrBufferFormat::Invalid, false, false); - } + if ((image.srgbDecodeCompatible || (image.convertedSamples && convertedSampleCompatible(image))) && image.depthBitsCompatible) append(IrTextureNumericClass::Uint, IrBufferFormat::Format11_11_10UNorm, IrBufferFormat::Invalid, false, false); + if (image.srgbDecodeCompatible && image.depthBitsCompatible) append(IrTextureNumericClass::Uint, IrBufferFormat::Format10_11_11Float, IrBufferFormat::Invalid, false, false); if (!storage) { if (image.depthBitsCompatible) { append(IrTextureNumericClass::Float, IrBufferFormat::Invalid, IrBufferFormat::Invalid, true, false); append(IrTextureNumericClass::Float, IrBufferFormat::Invalid, IrBufferFormat::Invalid, true, true); } - if (image.fmaskCompatible && image.indirectRoot == ImageResource::NoIndirectImage) append(IrTextureNumericClass::Float, IrBufferFormat::Invalid, IrBufferFormat::Fmask8_S2_F1, false, false); } } } - 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; append(IrTextureNumericClass::Uint, format, IrBufferFormat::Invalid, false, false); } } - if (image.depthCompare && image.indirectRoot == ImageResource::NoIndirectImage && (image.emulatedCompare & EmulatedCompare::Unsupported) == 0u && (image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray)) { + if (image.depthCompare && !table && (image.emulatedCompare & EmulatedCompare::Unsupported) == 0u && (image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray)) { auto mode = image; mode.numericClass = IrTextureNumericClass::Float; mode.depthCompare = false; @@ -743,7 +1979,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe mode.packedFormat = IrBufferFormat::Invalid; mode.shaderSwizzle = ShaderImageIdentitySwizzle; modes.push_back(mode); - if (image.dimension == RdnaImageDimension::Dim2DArray) { + if (image.dimension == RdnaImageDimension::Dim2DArray && !image.remappedDepth1D) { auto plain = mode; plain.dimension = RdnaImageDimension::Dim2D; modes.push_back(plain); @@ -761,7 +1997,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe modes.push_back(mode); } } - if (image.constantSwizzleCompatible && !storage && !image.depthCompare && !image.packed && image.indirectRoot == ImageResource::NoIndirectImage) { + if (image.constantSwizzleCompatible && !storage && !image.depthCompare && !image.packed && !table) { for (const auto numeric : {IrTextureNumericClass::Float, IrTextureNumericClass::Uint, IrTextureNumericClass::Sint}) { auto mode = image; mode.numericClass = numeric; @@ -776,15 +2012,19 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe modes.push_back(mode); } } - if (modes.empty()) throw std::runtime_error("image instruction has no supported runtime modes"); + if (modes.empty() && !table) throw std::runtime_error("image instruction has no supported runtime modes"); if (image.foldTexelOffsetsOnly && std::ranges::any_of(modes, [](const ImageResource& mode) { return UnnormalizedSampleMismatch(mode) == nullptr; })) { std::erase_if(modes, [](const ImageResource& mode) { return UnnormalizedSampleMismatch(mode) != nullptr; }); } return modes; } +bool ResourceMaterializer::NullImageDescriptor(const ImageResource& image, const DescriptorValue& descriptor) { + return nullImageDescriptor(image, descriptor); +} + const char* ResourceMaterializer::UnnormalizedSampleMismatch(const ImageResource& image) { - if (image.indirectRoot != ImageResource::NoIndirectImage) return "samples an image selected at run time"; + if (image.table != NoTable) return "samples an image selected at run time"; if (image.constantSwizzle) return nullptr; if ((image.dimension != RdnaImageDimension::Dim1D && image.dimension != RdnaImageDimension::Dim2D) || image.cube) return "samples a 1D-array, 2D-array, 3D, cube or multisampled image"; if (image.depthCompare) return "is used with depth comparison"; @@ -792,43 +2032,59 @@ const char* ResourceMaterializer::UnnormalizedSampleMismatch(const ImageResource return nullptr; } -std::uint32_t ResourceMaterializer::RuntimeImageMode(const ImageResource& image, const DescriptorValue& descriptor, std::span modes) { +RuntimeImageModeMatch ResourceMaterializer::TryRuntimeImageMode(const ImageResource& image, const DescriptorValue& descriptor, std::span modes) { + RuntimeImageModeMatch result; + const auto reject = [&](PoisonReason reason, std::string detail) { + result.reason = reason; + result.detail = std::move(detail); + return result; + }; if (descriptor.dwordCount != 8u) throw std::runtime_error("runtime image descriptor must contain eight dwords"); if (modes.empty()) throw std::runtime_error("prepared runtime image modes are missing"); - if (nullImageDescriptor(descriptor)) return 0u; - const auto decoded = decodeImageDescriptor(descriptor, image, image.srgbDecodeFormats); + if (nullImageDescriptor(image, descriptor)) { + result.mode = 0u; + return result; + } + const auto inspected = inspectImageDescriptor(descriptor, image, image.srgbDecodeFormats); + if (inspected.reason.has_value()) return reject(*inspected.reason, inspected.detail); + const auto& decoded = inspected.decoded; const auto format = rawImageFormat(descriptor); const bool emulated = image.depthCompare && format != IrBufferFormat::Format32Float && format != IrBufferFormat::Format16UNorm && !IsDepthBitsTexture(descriptor.dwords[1], descriptor.dwords[3]); - if (!emulated && (image.emulatedCompare & EmulatedCompare::NativeOffsetUnsupported) != 0u) throw std::runtime_error("native comparison with a nonconstant texel offset requires VK_KHR_maintenance8 and shaderImageGatherExtended"); + if (!emulated && (image.emulatedCompare & EmulatedCompare::NativeOffsetUnsupported) != 0u) return reject(PoisonReason::UnsupportedOperation, "native comparison with a nonconstant texel offset requires VK_KHR_maintenance8 and shaderImageGatherExtended"); if (image.packed && decoded.packedFormat != IrBufferFormat::Invalid) { - const auto format = GetFormatInfo(decoded.packedFormat); - if (format.packedBitfield) throw std::runtime_error("runtime packed image accesses a bitfield format"); + const auto packedFormat = GetFormatInfo(decoded.packedFormat); + if (packedFormat.packedBitfield) return reject(PoisonReason::InvalidFormat, "runtime packed image accesses a bitfield format"); const bool storage = image.resourceClass == ImageResourceClass::Storage; - if (storage && format.byteSize == 12u) throw std::runtime_error("runtime packed image uses a format the hardware does not write"); - for (std::uint32_t component = 0u; component < format.componentCount; ++component) { - const auto bits = format.componentBits[component]; - const bool exact = storage ? format.type == SpirvFormatComponentType::Uint || (bits == 32u && (format.type == SpirvFormatComponentType::Sint || format.type == SpirvFormatComponentType::Float)) : format.type == SpirvFormatComponentType::Uint || format.type == SpirvFormatComponentType::Sint || (format.type == SpirvFormatComponentType::Unorm && bits <= 16u) || (format.type == SpirvFormatComponentType::Float && bits == 32u); - if (!exact) throw std::runtime_error(storage ? "runtime packed image bits are not reproducible through the view" : "runtime packed image bits are not recoverable from the view"); + if (storage && packedFormat.byteSize == 12u) return reject(PoisonReason::InvalidFormat, "runtime packed image uses a format the hardware does not write"); + for (std::uint32_t component = 0u; component < packedFormat.componentCount; ++component) { + const auto bits = packedFormat.componentBits[component]; + const bool exact = storage ? packedFormat.type == SpirvFormatComponentType::Uint || (bits == 32u && (packedFormat.type == SpirvFormatComponentType::Sint || packedFormat.type == SpirvFormatComponentType::Float)) : packedFormat.type == SpirvFormatComponentType::Uint || packedFormat.type == SpirvFormatComponentType::Sint || (packedFormat.type == SpirvFormatComponentType::Unorm && bits <= 16u) || (packedFormat.type == SpirvFormatComponentType::Float && bits == 32u); + if (!exact) return reject(PoisonReason::InvalidFormat, storage ? "runtime packed image bits are not reproducible through the view" : "runtime packed image bits are not recoverable from the view"); } } - if (decoded.mipCount > (image.mipMode == ImageMipMode::DynamicStorage ? RuntimeAbi::StorageMipSlots : 1u)) throw std::runtime_error("runtime storage image mip capacity exceeded"); + if (decoded.mipCount > (image.mipMode == ImageMipMode::DynamicStorage ? RuntimeAbi::StorageMipSlots : 1u)) return reject(PoisonReason::InvalidFormat, "runtime storage image mip capacity exceeded"); if ((descriptorImageSwizzle(descriptor) & 06666u) == 0u && !decoded.fmask && !decoded.depthBits && decoded.conversionFormat == IrBufferFormat::Invalid && !decoded.srgbDecode) { for (std::uint32_t index = 0u; index < modes.size(); ++index) { - if (modes[index].constantSwizzle && modes[index].numericClass == decoded.numericClass) return index; + if (modes[index].constantSwizzle && modes[index].numericClass == decoded.numericClass) { + result.mode = index; + return result; + } } } + if (image.singleMipPlaneBiasCompatible && image.dimension == RdnaImageDimension::Dim2D && decoded.dimension == RdnaImageDimension::Dim3D) { + if ((descriptor.dwords[4] & 0x1fffu) != 0u || (descriptor.dwords[3] & 0xff000u) != 0u || (descriptor.dwords[5] & 0xf0u) != 0u) return reject(PoisonReason::Dimension, "2D biased sampling of a volume requires one slice and mip level zero only"); + } for (std::uint32_t index = 0u; index < modes.size(); ++index) { const auto& mode = modes[index]; if (mode.constantSwizzle) continue; if (((mode.emulatedCompare & EmulatedCompare::Enabled) != 0u) != emulated) continue; - if (decoded.fmask) { - if (mode.packedFormat == IrBufferFormat::Fmask8_S2_F1) return index; - continue; + if (mode.numericClass == decoded.numericClass && mode.dimension == decoded.dimension && mode.conversionFormat == decoded.conversionFormat && mode.packedFormat == decoded.packedFormat && mode.cube == decoded.cube && mode.depthBits == decoded.depthBits && mode.depthUnorm16 == decoded.depthUnorm16 && mode.srgbDecode == decoded.srgbDecode) { + result.mode = index; + return result; } - if (mode.numericClass == decoded.numericClass && mode.dimension == decoded.dimension && mode.conversionFormat == decoded.conversionFormat && mode.packedFormat == decoded.packedFormat && mode.cube == decoded.cube && mode.depthBits == decoded.depthBits && mode.depthUnorm16 == decoded.depthUnorm16 && mode.srgbDecode == decoded.srgbDecode) return index; } - if (image.dimension == RdnaImageDimension::Dim1D && decoded.dimension != RdnaImageDimension::Dim1D) throw std::runtime_error("image address has too few coordinate components"); - if (image.foldTexelOffsetsOnly) throw std::runtime_error("image sample with a texel offset that is not a constant takes only images an unnormalized S# can sample without VK_KHR_maintenance8 and shaderImageGatherExtended"); + if (image.dimension == RdnaImageDimension::Dim1D && decoded.dimension != RdnaImageDimension::Dim1D) return reject(PoisonReason::Dimension, "image address has too few coordinate components"); + if (image.foldTexelOffsetsOnly) return reject(PoisonReason::UnsupportedOperation, "image sample with a texel offset that is not a constant takes only images an unnormalized S# can sample without VK_KHR_maintenance8 and shaderImageGatherExtended"); const auto describe = [](IrTextureNumericClass numeric, RdnaImageDimension dimension, IrBufferFormat conversion, IrBufferFormat packed, bool cube, bool depthBits, bool depthUnorm16, bool srgb) { return "{class " + std::to_string(static_cast(numeric)) + ", dim " + std::to_string(static_cast(dimension)) + ", conversion " + std::to_string(static_cast(conversion)) + ", packed " + std::to_string(static_cast(packed)) + ", cube " + std::to_string(cube) + ", depthBits " + std::to_string(depthBits) + ", unorm16 " + std::to_string(depthUnorm16) + ", srgb " + std::to_string(srgb) + "}"; }; @@ -836,7 +2092,21 @@ std::uint32_t ResourceMaterializer::RuntimeImageMode(const ImageResource& image, for (const auto& mode : modes) message += " " + describe(mode.numericClass, mode.dimension, mode.conversionFormat, mode.packedFormat, mode.cube, mode.depthBits, mode.depthUnorm16, mode.srgbDecode); char words[96]; std::snprintf(words, sizeof(words), "; T# %08x %08x %08x %08x %08x %08x %08x %08x", descriptor.dwords[0], descriptor.dwords[1], descriptor.dwords[2], descriptor.dwords[3], descriptor.dwords[4], descriptor.dwords[5], descriptor.dwords[6], descriptor.dwords[7]); - throw std::runtime_error(message + words); + const auto reason = emulated ? PoisonReason::ColorCompare + : decoded.fmask ? PoisonReason::Fmask + : decoded.depthBits ? PoisonReason::DepthBits16 + : decoded.srgbDecode ? PoisonReason::SrgbUnsupported + : decoded.conversionFormat != IrBufferFormat::Invalid ? PoisonReason::Conversion + : decoded.dimension == RdnaImageDimension::Dim2DMsaa || decoded.dimension == RdnaImageDimension::Dim2DMsaaArray ? PoisonReason::Multisampled + : std::none_of(modes.begin(), modes.end(), [&](const ImageResource& mode) { return mode.dimension == decoded.dimension && mode.cube == decoded.cube; }) ? PoisonReason::Dimension + : PoisonReason::NumericClass; + return reject(reason, message + words); +} + +std::uint32_t ResourceMaterializer::RuntimeImageMode(const ImageResource& image, const DescriptorValue& descriptor, std::span modes) { + const auto match = TryRuntimeImageMode(image, descriptor, modes); + if (!match.mode.has_value()) throw std::runtime_error(match.detail); + return *match.mode; } std::uint32_t ResourceMaterializer::EmulatedCompareState(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint32_t index) { @@ -859,47 +2129,34 @@ void ResourceMaterializer::ApplyStaticInterface(IrProgram& program, bool nativeS if (component.bitWidth == 32u && argument < address->ArgumentCount() && address->Argument(argument)->Resolve()->HasImmediate()) continue; if ((memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0u) { images.at(memory.resource).emulatedCompare |= EmulatedCompare::NativeOffsetUnsupported; - } else { + } else if (resources.info.samplers.at(memory.sampler).table == NoTable) { images.at(memory.resource).foldTexelOffsetsOnly = true; resources.info.samplers.at(memory.sampler).foldTexelOffsetsOnly = true; } } } } - const auto directCount = static_cast(images.size()); - auto mappingOffset = static_cast(resources.srtReads.size()); - const auto slots = BindlessSlots(); - for (std::uint32_t index = 0u; index < directCount; ++index) { - auto image = images[index]; + bool tables = false; + for (auto& image : images) { image.srgbDecodeFormats = resources.srgbDecodeFormats; - if (image.indirectRoot != ImageResource::NoIndirectImage) throw std::runtime_error("static image interface was already expanded"); image.numericClass = image.atomic ? IrTextureNumericClass::Uint : IrTextureNumericClass::Float; image.mipCount = image.mipMode == ImageMipMode::DynamicStorage ? RuntimeAbi::StorageMipSlots : 1u; - if (resources.descriptorSources.at(image.source).indirectImage.has_value()) { - if (images.size() + slots - 1u > ShaderInfo::MaxImages) throw std::runtime_error("static bindless image capacity exceeded"); - image.indirectRoot = index; - image.indirectResources.push_back(index); - for (std::uint32_t slot = 1u; slot < slots; ++slot) { - auto entry = image; - entry.indirectResources.clear(); - image.indirectResources.push_back(static_cast(images.size())); - images.push_back(std::move(entry)); - } - image.indirectMappingOffset = mappingOffset; - image.indirectSearchIterations = static_cast(std::bit_width(slots)); - mappingOffset += 1u + 2u * slots; - } - images[index] = std::move(image); + if (image.table == NoTable) continue; + tables = true; + const auto modes = RuntimeImageModes(image); + 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"); auto& sampler = resources.info.samplers[pair.sampler]; sampler.depthCompare = sampler.depthCompare || images[pair.image].depthCompare; + tables = tables || sampler.table != NoTable; } resources.info.images = std::move(images); resources.guardedSrtSlots = Detail::ComputeGuardedFlatSlots(resources); - resources.srtGuardOffset = mappingOffset; - if (!resources.guardedSrtSlots.empty()) resources.info.usesFaultBuffer = true; + resources.srtGuardOffset = static_cast(resources.srtReads.size()); + if (tables || !resources.guardedSrtSlots.empty()) resources.info.usesFaultBuffer = true; PrepareImageModes(resources.info); } @@ -935,13 +2192,13 @@ void ownPlanValues(IrResourcePlan& plan) { if (argument != nullptr) pending.push_back(argument); } } + plan.valueStorage.Reserve(order.size()); for (const auto* value : order) { - auto clone = std::make_unique(value->Opcode(), value->Type(), value->Id()); + auto* clone = plan.valueStorage.Create(value->Opcode(), value->Type(), static_cast(plan.valueStorage.Size())); clone->SetFlags(value->Flags()); if (value->HasImmediate()) clone->SetImmediateU64(value->ImmediateU64()); clone->SetRegister(value->Register()); - clones[value] = clone.get(); - plan.valueStorage.push_back(std::move(clone)); + clones[value] = clone; } std::unordered_map blocks; const auto blockFor = [&](const IrBlock* block) { @@ -953,20 +2210,73 @@ void ownPlanValues(IrResourcePlan& plan) { return clone; }; for (const auto* value : order) { - auto* clone = clones.at(value); + std::vector arguments; + std::vector phiBlocks; + arguments.reserve(value->ArgumentCount()); + if (value->IsPhi()) phiBlocks.reserve(value->ArgumentCount()); for (std::size_t index = 0; index < value->ArgumentCount(); index++) { const auto* argument = value->Argument(index); - auto* mapped = argument == nullptr ? nullptr : clones.at(argument); - if (value->IsPhi()) { - clone->AddPhiOperand(blockFor(value->PhiBlock(index)), mapped); - } else { - clone->AddArgument(mapped); - } + arguments.push_back(argument == nullptr ? nullptr : clones.at(argument)); + if (value->IsPhi()) phiBlocks.push_back(blockFor(value->PhiBlock(index))); } + clones.at(value)->SetDetachedOperands(std::move(arguments), std::move(phiBlocks)); } for (auto* root : roots) { if (*root != nullptr) *root = clones.at(*root); } + plan.denseValueIds = true; +} + +void buildCompactPlan(IrResourcePlan& plan) { + if (!plan.denseValueIds || plan.valueStorage.Size() >= CompactResourcePlan::NoValue) return; + CompactResourcePlan compact; + const auto idOf = [&](const IrValue* value, std::uint32_t& id) { + if (value == nullptr) return false; + const auto* resolved = value->Resolve(); + id = resolved->Id(); + return id < plan.valueStorage.Size() && &plan.valueStorage[id] == resolved; + }; + try { + compact.values.reserve(plan.valueStorage.Size()); + for (const auto& stored : plan.valueStorage) { + const auto* value = &stored; + if (value->ArgumentCount() > 0xffu) return; + CompactPlanValue entry; + entry.immediate = value->ImmediateU64(); + entry.flags = value->Flags(); + entry.firstArgument = static_cast(compact.arguments.size()); + entry.registerIndex = value->Register().index; + entry.type = value->Type(); + entry.opcode = value->Opcode(); + entry.argumentCount = static_cast(value->ArgumentCount()); + entry.hasImmediate = value->HasImmediate(); + for (std::size_t index = 0; index < value->ArgumentCount(); index++) { + std::uint32_t id = 0; + if (!idOf(value->Argument(index), id)) return; + compact.arguments.push_back(id); + } + compact.values.push_back(entry); + } + compact.sourceDwords.assign(plan.descriptorSources.size() * 8u, CompactResourcePlan::NoValue); + for (std::size_t source = 0; source < plan.descriptorSources.size(); source++) { + const auto& descriptor = plan.descriptorSources[source]; + if (descriptor.dwordCount > 8u) return; + for (std::uint32_t dword = 0; dword < descriptor.dwordCount; dword++) { + if (!idOf(descriptor.dwords[dword], compact.sourceDwords[source * 8u + dword])) return; + } + } + compact.srtReads.resize(plan.srtReads.size()); + for (std::size_t slot = 0; slot < plan.srtReads.size(); slot++) { + if (!idOf(plan.srtReads[slot].value, compact.srtReads[slot])) return; + } + compact.conditions.assign(plan.controlFlow.size(), CompactResourcePlan::NoValue); + for (std::size_t block = 0; block < plan.controlFlow.size(); block++) { + if (plan.controlFlow[block].condition != nullptr && !idOf(plan.controlFlow[block].condition, compact.conditions[block])) return; + } + } catch (const std::exception&) { + return; + } + plan.compact = std::move(compact); } } @@ -993,6 +2303,9 @@ IrResourcePlan ResourceMaterializer::ExtractPlan(const IrProgram& program) const plan.info = source.info; PrepareImageModes(plan.info); plan.uniformFill = source.uniformFill; + for (auto& block : plan.controlFlow) { + if (block.condition != nullptr && !Detail::Evaluator::MayEvaluate(source, block.condition)) block.condition = nullptr; + } ownPlanValues(plan); const auto addSource = [&plan](std::uint32_t index) { if (index >= plan.descriptorSources.size()) { @@ -1005,53 +2318,76 @@ IrResourcePlan ResourceMaterializer::ExtractPlan(const IrProgram& program) const if (image.source >= plan.descriptorSources.size()) { throw std::runtime_error("ResourceMaterializer::ExtractPlan image references an unknown descriptor source"); } - if (plan.descriptorSources[image.source].indirectImage.has_value()) { + if (image.table != NoTable) { plan.requiresSpecializationMemory = true; } else { addSource(image.source); } } - for (const auto& sampler : plan.info.samplers) addSource(sampler.source); + for (const auto& sampler : plan.info.samplers) { + if (sampler.table != NoTable) { + plan.requiresSpecializationMemory = true; + } else { + addSource(sampler.source); + } + } plan.pureFlatSlots = Detail::ComputePureFlatSlots(plan); plan.guardedSrtSlots = Detail::ComputeGuardedFlatSlots(plan); + buildCompactPlan(plan); return plan; } +void ResourceMaterializer::CompactPlan(IrResourcePlan& plan) { + plan.compact = {}; + buildCompactPlan(plan); +} + void ResourceMaterializer::Materialize(const IrResourcePlan& program, const SrtRuntime& runtime, ResourceSnapshot& snapshot) const { const IrResourcePlan& plan = program; if (!plan.resourceTrackingComplete) { throw std::runtime_error("ResourceMaterializer::Materialize requires a completed resource plan"); } if (plan.requiresSpecializationMemory && runtime.readMemory == nullptr) { - throw std::runtime_error("ResourceMaterializer::Materialize requires runtime memory access for indirect images"); + throw std::runtime_error("ResourceMaterializer::Materialize requires runtime memory access for image tables"); } + const auto materializeStarted = MaterializeProfiled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; SrtWalker walker; ResourceSnapshot nextSnapshot; - std::vector tables; + struct ActiveSourcesStorage {}; + auto& activeSources = HostThreadLocal, ActiveSourcesStorage>(); + activeSources.clear(); try { - materializeSnapshot(plan, runtime, walker, nextSnapshot, tables); + materializeSnapshot(plan, runtime, walker, nextSnapshot, activeSources); + TableSnapshotter(plan, runtime, walker, activeSources, nextSnapshot.tables).Run(); } catch (...) { - reportBindless(); + reportTables(); throw; } const auto started = MaterializeProfiled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - materializeTables(plan, nextSnapshot, tables); + materializeForcedDegamma(plan, nextSnapshot); + materializeTables(plan, nextSnapshot); materializeSrtGuards(plan, nextSnapshot); if (MaterializeProfiled()) specializationNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); snapshot = std::move(nextSnapshot); - reportBindless(); + reportTables(); + if (MaterializeProfiled()) materializeNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - materializeStarted).count()), std::memory_order_relaxed); } std::uint64_t ResourceMaterializer::SpecializationNanoseconds() { return specializationNanoseconds.load(std::memory_order_relaxed); } -std::uint32_t ResourceMaterializer::BindlessSlots() { - return RuntimeAbi::BindlessTableSlots; +ImageTableReuseCounts ResourceMaterializer::TableReuse() { + auto& totals = tableReuseTotals(); + return {totals.unwritten.load(std::memory_order_relaxed), totals.compared.load(std::memory_order_relaxed), totals.wordsRead.load(std::memory_order_relaxed), totals.shared.load(std::memory_order_relaxed), totals.columns.load(std::memory_order_relaxed), totals.resolutions.load(std::memory_order_relaxed), totals.resolutionsShared.load(std::memory_order_relaxed)}; } -void ResourceMaterializer::CountBindlessRejection(BindlessRejection reason) { - if (reason < BindlessRejection::Count) bindlessCounters().rejected[static_cast(reason)].fetch_add(1, std::memory_order_relaxed); +std::uint64_t ResourceMaterializer::EvaluateNanoseconds() { + return evaluateNanoseconds.load(std::memory_order_relaxed); +} + +std::uint64_t ResourceMaterializer::MaterializeNanoseconds() { + return materializeNanoseconds.load(std::memory_order_relaxed); } } diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index ee4eaee5f8..0ffeb3be74 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -82,6 +83,10 @@ std::uint32_t possibleU32Bits(const IrValue* value) { const IrValue* shift = value->Argument(1)->Resolve(); return shift->HasImmediate() && shift->Type() == IrType::U32 ? possibleU32Bits(value->Argument(0)) << (shift->ImmediateU32() & 31u) : std::numeric_limits::max(); } + case IrOpcode::ReadFirstLane: + return value->ArgumentCount() >= 1u ? possibleU32Bits(value->Argument(0)) : std::numeric_limits::max(); + case IrOpcode::SelectU32: + return value->ArgumentCount() == 3u ? (possibleU32Bits(value->Argument(1)) | possibleU32Bits(value->Argument(2))) : std::numeric_limits::max(); default: return std::numeric_limits::max(); } @@ -113,7 +118,7 @@ public: fail("SRT plan is not ready"); } SplitDescriptorPhis(); - PlanIndirectImages(); + PlanTableColumns(); std::unordered_map blockIndices; for (const IrBlock* block : m_program.BlockOrder()) { blockIndices.emplace(block, static_cast(blockIndices.size())); @@ -130,6 +135,8 @@ public: m_currentBlock = std::numeric_limits::max(); PlanResourceControlFlow(); if (m_bdaTraces > bdaTraceLimit) std::fprintf(stderr, "[bda] %u more address accesses in this program not shown\n", m_bdaTraces - bdaTraceLimit); + CheckTableStores(); + AssignTables(); LinkImageAliases(); for (const auto& patch : m_handlePatches) { patch.handle->SetFlags(patch.resource); @@ -141,23 +148,27 @@ public: memory.sampler = patch.sampler; } } - for (const auto& plan : m_indirectImages) { - plan.handle->ReplaceArgument(0, plan.key); - for (std::uint32_t dword = 0; dword < 4u; dword++) { - plan.handle->ReplaceArgument(dword + 1u, plan.roots[dword]); + std::vector referenced; + for (const auto& plan : m_tableColumns) { + 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); } - for (std::uint32_t dword = 5u; dword < plan.roots.size(); dword++) { - plan.handle->ReplaceArgument(dword, plan.key); + KeepSourceAlive(*plan.handle, m_sources[column.heapSource], referenced); + if (column.selected) { + KeepSourceAlive(*plan.handle, m_sources[column.selection], referenced); } - for (const auto index : plan.memory) { - m_program.Resources().memoryInfo[index].planningOnly = true; + if (plan.keyDomain.has_value()) { + KeepSourceAlive(*plan.handle, m_sources[plan.keyDomain->source], referenced); } } + for (const auto index : m_tablePlanningMemory) { + m_program.Resources().memoryInfo[index].planningOnly = true; + } std::erase_if(m_program.Metadata().dynamicReads, [&](IrValue* value) { const IrValue* inst = value->Resolve(); - return std::ranges::any_of(m_indirectImages, [&](const IndirectImagePlan& plan) { - return std::ranges::find(plan.reads, inst) != plan.reads.end(); - }); + return std::ranges::find(m_tablePlanningReads, inst) != m_tablePlanningReads.end(); }); m_program.Resources().descriptorSources = std::move(m_sources); m_program.Resources().info = std::move(m_info); @@ -177,13 +188,15 @@ private: bool hasSampler = false; }; - struct IndirectImagePlan { + struct TableColumnPlan { IrValue* handle = nullptr; std::uint32_t source = 0; - IrValue* key = nullptr; - std::array roots {}; - std::array memory {}; - std::array reads {}; + IrValue* offset = nullptr; + std::vector memory; + std::vector reads; + std::vector hopMemory; + std::vector hopReads; + std::optional keyDomain; }; void MakeSource(const IrValue& handle, std::uint32_t width, bool sampler, bool sampleAdjust, DescriptorSource& descriptor) { @@ -206,6 +219,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; } @@ -213,9 +229,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) { @@ -243,7 +256,7 @@ private: std::uint32_t InternSource(const DescriptorSource& descriptor) { for (std::uint32_t candidate = 0; candidate < m_sources.size(); candidate++) { const auto& current = m_sources[candidate]; - if (current.dwordCount != descriptor.dwordCount || current.indirectImage != descriptor.indirectImage) { + if (current.dwordCount != descriptor.dwordCount || current.tableColumn != descriptor.tableColumn) { continue; } bool same = true; @@ -313,6 +326,12 @@ private: } block.sources = m_blockSources[index]; block.images = m_blockImages[index]; + for (const IrValue* inst : order[index]->Instructions()) { + if (inst->Opcode() != IrOpcode::ReadConst || inst->ArgumentCount() != 2u || inst->Argument(0)->Resolve()->Opcode() != IrOpcode::GetSrtResource) continue; + const IrValue* slot = inst->Argument(1)->Resolve(); + if (!slot->HasImmediate() || slot->Type() != IrType::U32 || slot->ImmediateU32() >= m_program.Resources().srtReads.size()) fail("resource control flow has an invalid flat SRT slot"); + if (std::ranges::find(block.flatSlots, slot->ImmediateU32()) == block.flatSlots.end()) block.flatSlots.push_back(slot->ImmediateU32()); + } } std::vector unconditional(order.size()); for (std::uint32_t index = 0; index < order.size() && unconditional[index] == 0u;) { @@ -324,6 +343,7 @@ private: } std::vector reached(m_sources.size()); std::vector reachedImages(m_info.images.size()); + std::vector reachedFlat(m_program.Resources().srtReads.size()); for (std::uint32_t index = 0; index < order.size(); index++) { if (unconditional[index] == 0u) { continue; @@ -334,6 +354,9 @@ private: for (const auto image : controlFlow[index].images) { reachedImages[image] = 1u; } + for (const auto slot : controlFlow[index].flatSlots) { + reachedFlat[slot] = 1u; + } } bool guarded = false; for (const auto& block : controlFlow) { @@ -343,6 +366,9 @@ private: for (const auto image : block.images) { guarded = guarded || reachedImages[image] == 0u; } + for (const auto slot : block.flatSlots) { + guarded = guarded || reachedFlat[slot] == 0u; + } } if (guarded) { m_program.Resources().controlFlow = std::move(controlFlow); @@ -358,20 +384,6 @@ private: return true; } - static bool usedOnlyAsImage(const IrValue& value, const std::array& reads) { - return !value.Uses().empty() && std::ranges::all_of(value.Uses(), [&](const IrValue* user) { - if (user->Opcode() != IrOpcode::GetImageResource || user->ArgumentCount() != reads.size()) { - return false; - } - for (std::uint32_t dword = 0; dword < reads.size(); dword++) { - if (user->Argument(dword)->Resolve() != reads[dword]) { - return false; - } - } - return true; - }); - } - const MemoryInfo* ScalarReadMemory(const IrValue& read, std::uint32_t& index) const { if (read.Opcode() != IrOpcode::ReadConstBuffer || read.ArgumentCount() != 2u) { return nullptr; @@ -384,6 +396,21 @@ private: return memory.kind == ResourceKind::ScalarBuffer && memory.dataBits == 32u && memory.dataDwords == 1u ? &memory : nullptr; } + const MemoryInfo* ScalarAddressMemory(const IrValue& read, std::uint32_t& index) const { + if (read.Opcode() != IrOpcode::LoadAddressU32 || read.ArgumentCount() != 4u) { + return nullptr; + } + index = read.Flags().index; + if (index >= m_program.Resources().memoryInfo.size()) { + return nullptr; + } + const auto& memory = m_program.Resources().memoryInfo[index]; + std::uint32_t high = 0; + const IrValue* active = read.Argument(3)->Resolve(); + const bool always = active->HasImmediate() && active->Type() == IrType::U1 && active->ImmediateBool(); + return memory.kind == ResourceKind::ScalarAddress && !memory.addressIsFull && memory.dataBits == 32u && memory.dataDwords == 1u && immediateU32(read.Argument(2), high) && high == 0u && always ? &memory : nullptr; + } + static bool AccessesMemory(IrOpcode op) { return BufferAccessOf(op) != BufferAccess::None || AddressOpcodeInfoOf(op).access != AddressAccess::None || ImageOpcodeInfoOf(op).access != ImageAccess::None; } @@ -417,11 +444,11 @@ private: return true; } - bool MakeRuntimeBufferSource(const IrValue& handle, std::uint32_t& source, DescriptorSource& descriptor) { - if (handle.Opcode() != IrOpcode::GetBufferResource) { + bool MakeRuntimeSource(const IrValue& handle, IrOpcode expected, std::uint32_t width, std::uint32_t& source, DescriptorSource& descriptor) { + if (handle.Opcode() != expected) { return false; } - MakeSource(handle, 4u, false, false, descriptor); + MakeSource(handle, width, false, false, descriptor); std::uint32_t badDword = 0; if (!ValidateSource(descriptor, badDword)) { return false; @@ -430,177 +457,409 @@ private: return true; } - bool MatchMaterialOffset(IrValue* value, IrValue*& selector, std::uint32_t& stride, std::uint32_t& offset) const { + static bool MatchScale(IrValue* value, IrValue*& key, std::uint32_t& stride) { value = value->Resolve(); - offset = 0; - IrValue* candidate = value; - if (candidate->Opcode() == IrOpcode::IAdd32 && candidate->ArgumentCount() == 2u) { - std::uint32_t immediate = 0; - if (immediateU32(candidate->Argument(0), immediate)) { - value = candidate->Argument(1)->Resolve(); - } else if (immediateU32(candidate->Argument(1), immediate)) { - value = candidate->Argument(0)->Resolve(); - } else { - return false; - } - offset = immediate; - } - IrValue* multiply = value; - if (multiply->Opcode() != IrOpcode::IMul32 || multiply->ArgumentCount() != 2u) { - return false; - } - if (immediateU32(multiply->Argument(0), stride)) { - selector = multiply->Argument(1)->Resolve(); - } else if (immediateU32(multiply->Argument(1), stride)) { - selector = multiply->Argument(0)->Resolve(); - } else { - return false; - } - return stride != 0u && selector->Opcode() == IrOpcode::ReadFirstLane; - } - - // The table entry offset `key * 32` (a shift or a multiply), optionally plus an immediate. - static bool MatchTableOffset(IrValue* value, IrValue*& key, std::uint32_t& entryOffset) { - value = value->Resolve(); - entryOffset = 0; - if (value->Opcode() == IrOpcode::IAdd32 && value->ArgumentCount() == 2u) { - std::uint32_t immediate = 0; - if (immediateU32(value->Argument(0), immediate)) { - value = value->Argument(1)->Resolve(); - } else if (immediateU32(value->Argument(1), immediate)) { - value = value->Argument(0)->Resolve(); - } else { - return false; - } - entryOffset = immediate; - } if (value->ArgumentCount() != 2u) { return false; } - std::uint32_t scale = 0; + std::uint32_t immediate = 0; if (value->Opcode() == IrOpcode::ShiftLeftLogical32) { - if (!immediateU32(value->Argument(1), scale) || scale != 5u) { + if (!immediateU32(value->Argument(1), immediate) || immediate >= 32u) { return false; } key = value->Argument(0)->Resolve(); + stride = 1u << immediate; return true; } if (value->Opcode() != IrOpcode::IMul32) { return false; } - if (immediateU32(value->Argument(1), scale)) { + if (immediateU32(value->Argument(1), immediate)) { key = value->Argument(0)->Resolve(); - } else if (immediateU32(value->Argument(0), scale)) { + } else if (immediateU32(value->Argument(0), immediate)) { key = value->Argument(1)->Resolve(); } else { return false; } - return scale == 32u; - } - - // A T# whose eight dwords are scalar reads of one V# (the table) at `entryOffset + key * 32` - // with a wave-uniform runtime key: a bindless image table. The key stays an ordinary value - // (the SPIR-V selects the bound slot from it); the eight reads become planning-only. - bool TryMakeTableImage(IrValue& handle, IndirectImagePlan& plan) { - if (handle.Opcode() != IrOpcode::GetImageResource || handle.ArgumentCount() != 8u) { - return false; - } - - std::array heapReads {}; - IrValue* heapHandle = nullptr; - IrValue* heapOffset = nullptr; - std::uint32_t immediateOffset = 0; - for (std::uint32_t dword = 0; dword < heapReads.size(); dword++) { - heapReads[dword] = handle.Argument(dword)->Resolve(); - std::uint32_t memoryIndex = 0; - const MemoryInfo* memory = ScalarReadMemory(*heapReads[dword], memoryIndex); - if (memory == nullptr || !MemoryIndexBelongsTo(memoryIndex, *heapReads[dword])) { - return false; - } - if (dword == 0u) { - immediateOffset = memory->offset; - } else if (memory->offset != immediateOffset + dword * sizeof(std::uint32_t)) { - return false; - } - IrValue* currentHandle = heapReads[dword]->Argument(0)->Resolve(); - if (heapHandle != nullptr && currentHandle != heapHandle) { - return false; - } - heapHandle = currentHandle; - if (dword == 0u) { - heapOffset = heapReads[dword]->Argument(1)->Resolve(); - } else if (!EquivalentValue(m_program.Resources(), heapOffset, heapReads[dword]->Argument(1))) { - return false; - } - plan.memory[dword] = memoryIndex; - plan.reads[dword] = heapReads[dword]; - } - - IrValue* key = nullptr; - std::uint32_t entryOffset = 0; - if (!MatchTableOffset(heapOffset, key, entryOffset) || key->Type() != IrType::U32) { - return false; - } - entryOffset += immediateOffset; - - for (const auto* read : heapReads) { - if (!usedOnlyAsImage(*read, heapReads)) { - return false; - } - } - - DescriptorSource heapSource; - std::uint32_t heapSourceIndex = 0; - if (!MakeRuntimeBufferSource(*heapHandle, heapSourceIndex, heapSource)) { - return false; - } - - DescriptorSource::IndirectImage table; - table.heapSource = heapSourceIndex; - table.materialSource = heapSourceIndex; - table.entryOffset = entryOffset; - DescriptorSource materialSource = heapSource; - std::uint32_t materialMemoryIndex = 0; - const MemoryInfo* materialMemory = ScalarReadMemory(*key, materialMemoryIndex); - if (materialMemory != nullptr && MemoryIndexBelongsTo(materialMemoryIndex, *key)) { - IrValue* selector = nullptr; - std::uint32_t selectorStride = 0; - std::uint32_t selectorOffset = 0; - DescriptorSource candidateSource; - std::uint32_t candidateIndex = 0; - if (MatchMaterialOffset(key->Argument(1), selector, selectorStride, selectorOffset) && MakeRuntimeBufferSource(*key->Argument(0)->Resolve(), candidateIndex, candidateSource)) { - table.hasMaterial = true; - table.materialSource = candidateIndex; - table.selectorStride = selectorStride; - table.selectorOffset = selectorOffset + materialMemory->offset; - materialSource = candidateSource; - } - } - - DescriptorSource imageSource; - imageSource.dwordCount = 8u; - std::copy(heapSource.dwords.begin(), heapSource.dwords.begin() + 4u, imageSource.dwords.begin()); - std::copy(materialSource.dwords.begin(), materialSource.dwords.begin() + 4u, imageSource.dwords.begin() + 4u); - imageSource.indirectImage = table; - - plan.handle = &handle; - plan.source = InternSource(imageSource); - plan.key = key; - plan.roots = imageSource.dwords; + stride = immediate; return true; } - const IndirectImagePlan* FindIndirectImage(const IrValue& handle) const { - const auto found = std::ranges::find_if(m_indirectImages, [&](const IndirectImagePlan& plan) { - return plan.handle == &handle; - }); - return found == m_indirectImages.end() ? nullptr : &*found; + static bool MatchAffineOffset(IrValue* value, IrValue*& key, std::uint32_t& stride, std::uint32_t& addend) { + value = value->Resolve(); + addend = 0; + bool matched = MatchScale(value, key, stride); + if (!matched && value->Opcode() == IrOpcode::IAdd32 && value->ArgumentCount() == 2u) { + if (immediateU32(value->Argument(0), addend)) { + matched = MatchScale(value->Argument(1), key, stride); + } else if (immediateU32(value->Argument(1), addend)) { + matched = MatchScale(value->Argument(0), key, stride); + } + } + return matched && stride >= 4u && stride % 4u == 0u && key->Type() == IrType::U32; } - bool IsIndirectPlanningMemory(std::uint32_t index) const { - return std::ranges::any_of(m_indirectImages, [&](const IndirectImagePlan& plan) { - return std::ranges::find(plan.memory, index) != plan.memory.end(); + static bool MatchMaskedOffset(IrValue* value, IrValue*& key, std::uint32_t& stride) { + value = value->Resolve(); + if (value->Opcode() != IrOpcode::BitwiseAnd32 || value->ArgumentCount() != 2u) { + return false; + } + std::uint32_t mask = 0; + IrValue* scaled = nullptr; + if (immediateU32(value->Argument(1), mask)) { + scaled = value->Argument(0); + } else if (immediateU32(value->Argument(0), mask)) { + scaled = value->Argument(1); + } else { + return false; + } + return MatchScale(scaled, key, stride) && std::has_single_bit(stride) && stride >= 4u && (mask & ~(stride - 1u)) != 0u && key->Type() == IrType::U32; + } + + static bool ReadsThroughDescriptor(const IrValue* value) { + std::vector pending{value}; + std::vector seen; + while (!pending.empty()) { + const IrValue* current = pending.back()->Resolve(); + pending.pop_back(); + if (std::find(seen.begin(), seen.end(), current) != seen.end()) continue; + seen.push_back(current); + if (current->Opcode() == IrOpcode::ReadConstBuffer || current->Opcode() == IrOpcode::LoadAddressU32) return true; + for (std::size_t index = 0; index < current->ArgumentCount(); index++) pending.push_back(current->Argument(index)); + } + return false; + } + + std::optional MatchKeyDomain(IrValue* key) { + key = key->Resolve(); + DescriptorSource direct; + direct.dwordCount = 1u; + direct.dwords[0] = key; + std::uint32_t badDword = 0; + if (ValidateSource(direct, badDword) && !ReadsThroughDescriptor(key)) { + KeyDomain domain{InternSource(direct), 0u, 4u}; + domain.value = true; + return domain; + } + std::uint32_t memoryIndex = 0; + const MemoryInfo* memory = ScalarReadMemory(*key, memoryIndex); + if (memory == nullptr) { + return std::nullopt; + } + DescriptorSource source; + std::uint32_t sourceIndex = 0; + if (!MakeRuntimeSource(*key->Argument(0)->Resolve(), IrOpcode::GetBufferResource, 4u, sourceIndex, source)) { + return std::nullopt; + } + IrValue* selector = nullptr; + std::uint32_t stride = 0; + std::uint32_t addend = 0; + if (MatchAffineOffset(key->Argument(1), selector, stride, addend)) { + return KeyDomain{sourceIndex, memory->offset + addend, stride}; + } + const auto bits = possibleU32Bits(key->Argument(1)); + const auto zeros = bits == 0u ? 31u : std::min(static_cast(std::countr_zero(bits)), 31u); + return KeyDomain{sourceIndex, memory->offset, std::max(4u, 1u << zeros)}; + } + + static IrValue* SplitImmediateAddend(IrValue* value, std::uint32_t& addend) { + value = value->Resolve(); + addend = 0; + 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; + } + return value; + } + + struct FirstLaneOffset { + const IrValue* lane = nullptr; + const IrValue* mask = nullptr; + std::uint32_t shift = 0; + std::uint32_t addend = 0; + }; + + static bool ParseFirstLaneOffset(IrValue* value, const IrValue* mask, FirstLaneOffset& form, std::uint32_t depth) { + std::uint32_t addend = 0; + value = SplitImmediateAddend(value, addend); + while (mask != nullptr && Detail::IsRuntimeSelect(value->Opcode()) && value->ArgumentCount() == 3u && Detail::ActiveMaskImplies(mask, value->Argument(0))) { + value = SplitImmediateAddend(value->Argument(1), addend); + } + std::uint32_t shift = 0; + if (depth != 0u && value->Opcode() == IrOpcode::ShiftLeftLogical32 && value->ArgumentCount() == 2u && immediateU32(value->Argument(1), shift) && shift < 32u) { + if (!ParseFirstLaneOffset(value->Argument(0)->Resolve(), mask, form, depth - 1u) || form.shift + shift >= 32u) return false; + form.shift += shift; + form.addend <<= shift; + } else if (depth != 0u && mask == nullptr && value->Opcode() == IrOpcode::ReadFirstLane && value->ArgumentCount() == 2u) { + if (!ParseFirstLaneOffset(value->Argument(0)->Resolve(), value->Argument(1)->Resolve(), form, depth - 1u)) return false; + } else { + if (mask == nullptr) return false; + form = FirstLaneOffset{value, mask, 0u, 0u}; + } + form.addend += addend; + return true; + } + + bool SameFirstLaneOffset(const FirstLaneOffset& left, const FirstLaneOffset& right) const { + return left.shift == right.shift && EquivalentValue(m_program.Resources(), left.mask, right.mask) && EquivalentValue(m_program.Resources(), left.lane, right.lane); + } + + bool TryMakeTableColumn(IrValue& handle, bool sampler, bool r128, TableColumnPlan& plan) { + const auto expected = sampler ? IrOpcode::GetSamplerResource : IrOpcode::GetImageResource; + const std::uint32_t width = sampler ? 4u : 8u; + if (handle.Opcode() != expected || handle.ArgumentCount() != width) { + return false; + } + std::uint32_t words = width; + if (!sampler && r128) { + bool zeroTail = true; + for (std::uint32_t dword = 4u; dword < 8u; dword++) { + std::uint32_t immediate = 0; + zeroTail = zeroTail && immediateU32(handle.Argument(dword), immediate) && immediate == 0u; + } + if (zeroTail) { + words = 4u; + } + } + IrValue* heapHandle = nullptr; + IrValue* offset = nullptr; + IrValue* base = nullptr; + std::uint32_t baseAddend = 0; + std::uint32_t carry = 0; + bool folded = false; + std::uint32_t first = 0; + bool address = false; + FirstLaneOffset baseLane; + bool laneBase = false; + for (std::uint32_t dword = 0; dword < words; dword++) { + IrValue* read = ResolveInvariantPhi(m_program.Resources(), handle.Argument(dword)); + if (read == nullptr) return false; + std::uint32_t memoryIndex = 0; + const MemoryInfo* memory = ScalarReadMemory(*read, memoryIndex); + const bool addressRead = memory == nullptr && (memory = ScalarAddressMemory(*read, memoryIndex)) != nullptr; + if (memory == nullptr || !MemoryIndexBelongsTo(memoryIndex, *read)) { + return false; + } + std::uint32_t readAddend = 0; + IrValue* readBase = SplitImmediateAddend(read->Argument(1), readAddend); + bool sameBase = dword == 0u || EquivalentValue(m_program.Resources(), base, readBase); + if (!sameBase && laneBase) { + FirstLaneOffset lane; + if (ParseFirstLaneOffset(read->Argument(1)->Resolve(), nullptr, lane, phiSearchDepth) && SameFirstLaneOffset(baseLane, lane)) { + readAddend = lane.addend; + sameBase = true; + } + } + if (dword == 0u) { + first = memory->offset; + address = addressRead; + base = readBase; + baseAddend = readAddend; + laneBase = ParseFirstLaneOffset(read->Argument(1)->Resolve(), nullptr, baseLane, phiSearchDepth) && baseLane.addend == baseAddend; + } else if (addressRead != address || memory->offset + readAddend != first + baseAddend + dword * static_cast(sizeof(std::uint32_t))) { + return false; + } else if (readAddend != baseAddend) { + if (readAddend < baseAddend || static_cast(memory->offset) + (readAddend - baseAddend) > 0x7fffffffu) { + return false; + } + folded = true; + carry = std::max(carry, readAddend - baseAddend); + } + IrValue* currentHandle = read->Argument(0)->Resolve(); + if (heapHandle != nullptr && currentHandle != heapHandle && !EquivalentValue(m_program.Resources(), heapHandle, currentHandle)) { + return false; + } + if (heapHandle == nullptr) heapHandle = currentHandle; + if (dword == 0u) { + offset = read->Argument(1)->Resolve(); + } else if (!sameBase) { + return false; + } + plan.memory.push_back(memoryIndex); + plan.reads.push_back(read); + } + if (address) { + DescriptorSource direct; + MakeSource(handle, width, false, false, direct); + std::uint32_t badDword = 0; + if (ValidateSource(direct, badDword)) { + return false; + } + } + IrValue* key = nullptr; + std::uint32_t stride = 0; + std::uint32_t addend = 0; + bool masked = false; + std::uint32_t descriptorOffset = 0; + if (address && immediateU32(offset, descriptorOffset)) { + return TryMakeIndirectColumn(handle, *heapHandle, sampler, words, descriptorOffset, first, plan); + } + if (!MatchAffineOffset(offset, key, stride, addend)) { + if (!MatchMaskedOffset(offset, key, stride)) { + return false; + } + masked = true; + } + DescriptorSource heapSource; + std::uint32_t heapIndex = 0; + if (!MakeRuntimeSource(*heapHandle, address ? IrOpcode::GetAddressResource : IrOpcode::GetBufferResource, address ? 2u : 4u, heapIndex, heapSource)) { + return false; + } + TableColumn column; + column.heapSource = heapIndex; + column.stride = stride; + column.addend = addend; + column.offset = first; + column.dwordCount = words; + column.maxKey = possibleU32Bits(key); + column.sampler = sampler; + column.address = address; + column.foldedAddend = folded ? baseAddend : 0u; + column.foldedCarry = folded ? carry : 0u; + if (!address && !sampler && std::ranges::find(m_unselectedHandles, &handle) == m_unselectedHandles.end()) { + 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; + plan.handle = &handle; + plan.source = InternSource(columnSource); + plan.keyDomain = domain; + auto& shared = *m_sources[plan.source].tableColumn; + shared.maxKey = std::max(shared.maxKey, column.maxKey); + if (!domain.has_value()) { + shared.unkeyed = true; + shared.keyDomains.clear(); + } else if (!shared.unkeyed && std::find(shared.keyDomains.begin(), shared.keyDomains.end(), *domain) == shared.keyDomains.end()) { + shared.keyDomains.push_back(*domain); + } + plan.offset = offset; + return true; + } + + bool TryMakeIndirectColumn(IrValue& handle, IrValue& descriptorPointer, bool sampler, std::uint32_t words, std::uint32_t descriptorOffset, std::uint32_t descriptorImmediate, TableColumnPlan& plan) { + std::vector hops; + IrValue* pointer = &descriptorPointer; + while (pointer->Opcode() == IrOpcode::GetAddressResource && pointer->ArgumentCount() == 2u) { + std::uint32_t memoryIndex = 0; + if (ScalarAddressMemory(*pointer->Argument(0)->Resolve(), memoryIndex) == nullptr) { + break; + } + PointerHop hop; + IrValue* next = nullptr; + for (std::uint32_t dword = 0; dword < 2u; dword++) { + IrValue* read = pointer->Argument(dword)->Resolve(); + const MemoryInfo* memory = ScalarAddressMemory(*read, memoryIndex); + std::uint32_t readOffset = 0; + if (memory == nullptr || !MemoryIndexBelongsTo(memoryIndex, *read) || !immediateU32(read->Argument(1), readOffset)) { + return false; + } + IrValue* readPointer = read->Argument(0)->Resolve(); + if (dword == 0u) { + hop = PointerHop{readOffset, memory->offset}; + next = readPointer; + } else if (readOffset != hop.offset || memory->offset != hop.immediate + static_cast(sizeof(std::uint32_t)) || (readPointer != next && !EquivalentValue(m_program.Resources(), next, readPointer))) { + return false; + } + plan.hopMemory.push_back(memoryIndex); + plan.hopReads.push_back(read); + } + hops.insert(hops.begin(), hop); + pointer = next; + } + if (pointer->Opcode() != IrOpcode::GetAddressResource || pointer->ArgumentCount() != 2u) { + return false; + } + IrValue* bufferHandle = nullptr; + IrValue* offset = nullptr; + std::uint32_t first = 0; + for (std::uint32_t dword = 0; dword < 2u; dword++) { + IrValue* read = pointer->Argument(dword)->Resolve(); + std::uint32_t memoryIndex = 0; + const MemoryInfo* memory = ScalarReadMemory(*read, memoryIndex); + if (memory == nullptr || !MemoryIndexBelongsTo(memoryIndex, *read)) { + return false; + } + if (dword == 0u) { + first = memory->offset; + bufferHandle = read->Argument(0)->Resolve(); + offset = read->Argument(1)->Resolve(); + } else if (memory->offset != first + static_cast(sizeof(std::uint32_t)) || read->Argument(0)->Resolve() != bufferHandle || !EquivalentValue(m_program.Resources(), offset, read->Argument(1))) { + return false; + } + } + IrValue* key = nullptr; + std::uint32_t stride = 0; + std::uint32_t addend = 0; + if (!MatchAffineOffset(offset, key, stride, addend)) { + return false; + } + DescriptorSource heapSource; + std::uint32_t heapIndex = 0; + if (!MakeRuntimeSource(*bufferHandle, IrOpcode::GetBufferResource, 4u, heapIndex, heapSource)) { + return false; + } + TableColumn column; + column.heapSource = heapIndex; + column.stride = stride; + column.addend = addend; + column.offset = first; + column.dwordCount = words; + column.maxKey = possibleU32Bits(key); + column.sampler = sampler; + column.indirect = true; + column.indirectOffset = descriptorOffset; + column.indirectImmediate = descriptorImmediate; + column.pointerHops = std::move(hops); + const auto domain = MatchKeyDomain(key); + DescriptorSource columnSource = heapSource; + columnSource.tableColumn = column; + plan.handle = &handle; + plan.source = InternSource(columnSource); + plan.keyDomain = domain; + auto& shared = *m_sources[plan.source].tableColumn; + shared.maxKey = std::max(shared.maxKey, column.maxKey); + if (!domain.has_value()) { + shared.unkeyed = true; + shared.keyDomains.clear(); + } else if (!shared.unkeyed && std::find(shared.keyDomains.begin(), shared.keyDomains.end(), *domain) == shared.keyDomains.end()) { + shared.keyDomains.push_back(*domain); + } + plan.offset = offset; + return true; + } + + const TableColumnPlan* FindTableColumn(const IrValue& handle) const { + const auto found = std::ranges::find_if(m_tableColumns, [&](const TableColumnPlan& plan) { + return plan.handle == &handle; }); + return found == m_tableColumns.end() ? nullptr : &*found; + } + + bool PlanTableColumn(IrValue& handle, bool sampler, bool r128) { + if (FindTableColumn(handle) != nullptr) { + return true; + } + TableColumnPlan plan; + if (!TryMakeTableColumn(handle, sampler, r128, plan)) { + return false; + } + m_tableColumns.push_back(std::move(plan)); + return true; } static constexpr std::uint32_t phiSearchDepth = 8u; @@ -1121,33 +1380,175 @@ private: } } - void PlanIndirectImages() { - // Kill switch: APS5_NO_BINDLESS_IMAGES=1 leaves table-loaded T#s unplanned (GetHandle then - // rejects them as before). - static const bool enabled = std::getenv("APS5_NO_BINDLESS_IMAGES") == nullptr; + void PlanTableColumns() { + static const bool enabled = std::getenv("APS5_NO_IMAGE_TABLES") == nullptr; if (!enabled) { return; } + const auto sources = m_sources; + const auto columns = m_tableColumns; + const auto reads = m_tablePlanningReads; + const auto memory = m_tablePlanningMemory; + PlanTableColumnPass(); + while (!PlannedWithinLimits()) { + const auto group = LargestSelectedGroup(); + if (group.empty()) { + return; + } + m_unselectedHandles.insert(m_unselectedHandles.end(), group.begin(), group.end()); + m_sources = sources; + m_tableColumns = columns; + m_tablePlanningReads = reads; + m_tablePlanningMemory = memory; + PlanTableColumnPass(); + } + } + + bool PlannedWithinLimits() { + std::vector images; + std::vector> pairs; + std::vector directImages; + std::vector> directSamplers; + const auto direct = std::numeric_limits::max() / 2u; + for (auto& block : m_program.Blocks()) { + for (IrValue* inst : block->Instructions()) { + const auto imageInfo = ImageOpcodeInfoOf(inst->Opcode()); + if (inst->Opcode() == IrOpcode::ImageBvhIntersectRay || imageInfo.access == ImageAccess::None || imageInfo.resourceClass == ImageResourceClass::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]; + if (memory.kind != ResourceKind::Image || memory.planningOnly || IsTablePlanningMemory(flags.index)) { + continue; + } + const IrValue* handle = inst->Argument(0)->Resolve(); + const TableColumnPlan* column = FindTableColumn(*handle); + std::uint32_t source = 0; + if (column != nullptr) { + source = column->source; + } else { + const auto found = std::ranges::find_if(directImages, [&](const IrValue* other) { return SameHandle(*other, *handle); }); + source = direct + static_cast(found - directImages.begin()); + if (found == directImages.end()) directImages.push_back(handle); + } + const auto key = ImageKey(source, memory, inst->Opcode()); + const auto image = std::ranges::find_if(images, [&](const ImageResource& other) { return SameImageKey(other, key); }); + const auto imageIndex = static_cast(image - images.begin()); + if (image == images.end()) images.push_back(key); + if (!imageInfo.needsSampler || inst->ArgumentCount() < 2u) { + continue; + } + const IrValue* samplerHandle = inst->Argument(1)->Resolve(); + const TableColumnPlan* samplerColumn = FindTableColumn(*samplerHandle); + std::uint32_t sampler = 0; + if (samplerColumn != nullptr) { + sampler = samplerColumn->source; + } else { + const bool sampleAdjust = (memory.imageSampleFlags & RdnaImageSampleFlagAdjust) != 0; + const auto found = std::ranges::find_if(directSamplers, [&](const auto& other) { return other.second == sampleAdjust && SameHandle(*other.first, *samplerHandle); }); + sampler = direct + static_cast(found - directSamplers.begin()); + if (found == directSamplers.end()) directSamplers.emplace_back(samplerHandle, sampleAdjust); + } + if (std::ranges::find(pairs, std::pair{imageIndex, sampler}) == pairs.end()) pairs.emplace_back(imageIndex, sampler); + } + } + return images.size() <= ShaderInfo::MaxImages && pairs.size() <= ShaderInfo::MaxSampledPairs; + } + + std::vector LargestSelectedGroup() const { + const auto unselected = [](TableColumn column) { + column.selected = false; + column.selection = 0u; + return column; + }; + std::vector> groups; + for (const auto& plan : m_tableColumns) { + const auto& column = m_sources[plan.source].tableColumn; + if (!column.has_value()) { + continue; + } + const auto group = std::ranges::find_if(groups, [&](const std::vector& members) { return unselected(*m_sources[members.front()].tableColumn) == unselected(*column); }); + if (group == groups.end()) { + groups.push_back({plan.source}); + } else if (std::ranges::find(*group, plan.source) == group->end()) { + group->push_back(plan.source); + } + } + const std::vector* largest = nullptr; + for (const auto& group : groups) { + const bool selected = std::ranges::any_of(group, [&](std::uint32_t source) { return m_sources[source].tableColumn->selected; }); + if (selected && group.size() > 1u && (largest == nullptr || group.size() > largest->size())) { + largest = &group; + } + } + std::vector handles; + if (largest == nullptr) { + return handles; + } + for (const auto& plan : m_tableColumns) { + if (std::ranges::find(*largest, plan.source) != largest->end()) { + handles.push_back(plan.handle); + } + } + return handles; + } + + void PlanTableColumnPass() { IndexMemoryUsers(); + const auto directImages = DirectImageHandles(); 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; } - IrValue* handle = inst->Argument(0)->Resolve(); - const IndirectImagePlan* planned = FindIndirectImage(*handle); - IndirectImagePlan plan; - if (planned == nullptr && !TryMakeTableImage(*handle, plan)) { + const auto flags = inst->Flags(); + if (flags.index >= m_program.Resources().memoryInfo.size()) { continue; } - // A store through a table would mark every bound slot pending write-back. - if (imageInfo.resourceClass == ImageResourceClass::Storage) { - ResourceMaterializer::CountBindlessRejection(BindlessRejection::Storage); - fail("bindless storage image tables are unsupported"); + const auto memory = m_program.Resources().memoryInfo[flags.index]; + 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 (planned == nullptr) { - m_indirectImages.push_back(std::move(plan)); + if (imageInfo.needsSampler && inst->ArgumentCount() > 1u && !DirectSampler(*inst->Argument(1)->Resolve())) { + static_cast(PlanTableColumn(*inst->Argument(1)->Resolve(), true, false)); + } + } + } + for (const auto& plan : m_tableColumns) { + for (std::size_t index = 0; index < plan.reads.size(); index++) { + const IrValue* read = plan.reads[index]; + const bool planned = std::ranges::all_of(read->Uses(), [&](const IrValue* user) { + return FindTableColumn(*user) != nullptr; + }); + if (planned && std::ranges::find(m_tablePlanningReads, read) == m_tablePlanningReads.end()) { + m_tablePlanningReads.push_back(read); + m_tablePlanningMemory.push_back(plan.memory[index]); + } + } + } + for (bool marked = true; marked;) { + marked = false; + for (const auto& plan : m_tableColumns) { + for (std::size_t index = 0; index < plan.hopReads.size(); index++) { + const IrValue* read = plan.hopReads[index]; + if (std::ranges::find(m_tablePlanningReads, read) != m_tablePlanningReads.end()) { + continue; + } + const bool planned = std::ranges::all_of(read->Uses(), [&](const IrValue* pointer) { + return pointer->Opcode() == IrOpcode::GetAddressResource && std::ranges::all_of(pointer->Uses(), [&](const IrValue* user) { + return std::ranges::find(m_tablePlanningReads, user) != m_tablePlanningReads.end(); + }); + }); + if (planned) { + m_tablePlanningReads.push_back(read); + m_tablePlanningMemory.push_back(plan.hopMemory[index]); + marked = true; + } } } } @@ -1155,6 +1556,168 @@ private: m_memoryUsers.clear(); } + void KeepSourceAlive(IrValue& position, const DescriptorSource& source, std::vector& referenced) { + for (std::uint32_t dword = 0; dword < source.dwordCount; dword++) { + IrValue* value = source.dwords[dword]->Resolve(); + if (value->HasImmediate() || std::ranges::find(referenced, value) != referenced.end()) { + continue; + } + referenced.push_back(value); + const std::array arguments{value}; + EmitBefore(position, IrOpcode::ReferenceU32, IrType::Void, arguments); + } + } + + bool DirectSampler(const IrValue& handle) { + if (handle.Opcode() != IrOpcode::GetSamplerResource || handle.ArgumentCount() != 4u || FindTableColumn(handle) != nullptr) { + return false; + } + DescriptorSource descriptor; + MakeSource(handle, 4u, true, false, descriptor); + std::uint32_t badDword = 0; + 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(); + } + + bool TableSource(std::uint32_t source) const { + 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; + } + auto& column = *source.tableColumn; + const bool written = std::any_of(column.keyDomains.begin(), column.keyDomains.end(), [&](const KeyDomain& domain) { + return std::any_of(m_info.buffers.begin(), m_info.buffers.end(), [&](const auto& buffer) { return (buffer.written || buffer.atomic) && buffer.source == domain.source; }); + }); + if (written) { + column.keyDomains.clear(); + } + } + } + + void AssignTables() { + std::uint32_t next = 0; + for (auto& image : m_info.images) { + if (!TableSource(image.source)) { + continue; + } + image.table = next++; + if (image.atomic || image.packed || image.byElements != 0u) { + image.tableOperation = TableOperation::Unsupported; + } + } + for (auto& sampler : m_info.samplers) { + if (TableSource(sampler.source)) { + sampler.table = next++; + } + } + } + void GetHandle(IrValue* value, IrOpcode expected, std::uint32_t width, IrValue*& handle, std::uint32_t& source, bool sampler = false, bool sampleAdjust = false) { handle = value->Resolve(); if (handle->Opcode() != expected) { @@ -1239,13 +1802,32 @@ private: resource.scalar = resource.scalar || op == IrOpcode::ReadConstBuffer || memory.kind == ResourceKind::ScalarBuffer; } + static ImageResource ImageKey(std::uint32_t source, const MemoryInfo& memory, IrOpcode op) { + ImageResource image; + image.source = source; + image.resourceClass = ImageOpcodeInfoOf(op).resourceClass; + image.depthCompare = (memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0; + image.remappedDepth1D = image.depthCompare && (memory.imageDimension == RdnaImageDimension::Dim1D || memory.imageDimension == RdnaImageDimension::Dim1DArray); + image.dimension = image.remappedDepth1D + ? (memory.imageDimension == RdnaImageDimension::Dim1D ? RdnaImageDimension::Dim2D : RdnaImageDimension::Dim2DArray) + : memory.imageDimension; + image.mipMode = image.resourceClass == ImageResourceClass::Storage && memory.imageHasMip ? ImageMipMode::DynamicStorage : ImageMipMode::None; + image.r128 = memory.imageR128; + image.packed = memory.imagePacked; + image.byElements = memory.imageByElements; + image.byComponents = memory.imageByElements != 0u ? memory.dataDwords / memory.imageByElements : 0u; + return image; + } + + static bool SameImageKey(const ImageResource& image, const ImageResource& key) { + return image.source == key.source && image.resourceClass == key.resourceClass && image.dimension == key.dimension && image.mipMode == key.mipMode && image.depthCompare == key.depthCompare && image.remappedDepth1D == key.remappedDepth1D && image.r128 == key.r128 && image.packed == key.packed && image.byElements == key.byElements && image.byComponents == key.byComponents; + } + std::uint32_t AddImage(std::uint32_t source, const MemoryInfo& memory, IrOpcode op, std::uint32_t pc) { - const auto resourceClass = ImageOpcodeInfoOf(op).resourceClass; - const auto mip = resourceClass == ImageResourceClass::Storage && memory.imageHasMip ? ImageMipMode::DynamicStorage : ImageMipMode::None; - const bool depth = (memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0; + const auto key = ImageKey(source, memory, op); for (std::uint32_t i = 0; i < m_info.images.size(); i++) { auto& image = m_info.images[i]; - if (image.source == source && image.resourceClass == resourceClass && image.dimension == memory.imageDimension && image.mipMode == mip && image.depthCompare == depth && image.r128 == memory.imageR128 && image.packed == memory.imagePacked && image.byElements == memory.imageByElements && image.byComponents == (memory.imageByElements != 0u ? memory.dataDwords / memory.imageByElements : 0u)) { + if (SameImageKey(image, key)) { Merge(image, memory, op, pc); return i; } @@ -1253,17 +1835,10 @@ private: if (m_info.images.size() >= ShaderInfo::MaxImages) { return std::numeric_limits::max(); } - ImageResource image; - image.source = source; + ImageResource image = key; + image.singleMipPlaneBiasCompatible = m_program.Resources().stage == IrShaderStage::Pixel; + image.volumeArraySampleCompatible = true; image.firstUsePc = pc; - image.resourceClass = resourceClass; - image.dimension = memory.imageDimension; - image.mipMode = mip; - image.depthCompare = depth; - image.r128 = memory.imageR128; - image.packed = memory.imagePacked; - image.byElements = memory.imageByElements; - image.byComponents = memory.imageByElements != 0u ? memory.dataDwords / memory.imageByElements : 0u; Merge(image, memory, op, pc); m_info.images.push_back(image); return static_cast(m_info.images.size() - 1); @@ -1284,19 +1859,33 @@ private: image.atomic = image.atomic || atomic; image.srgbDecodeCompatible = image.srgbDecodeCompatible && !ImageOpcodeInfoOf(op).needsSampler; image.fmaskCompatible = image.fmaskCompatible && op == IrOpcode::ImageRead && memory.dataBits == 32u; + image.singleMipPlaneBiasCompatible = image.singleMipPlaneBiasCompatible && op == IrOpcode::ImageSampleRaw && memory.imageSampleFlags == RdnaImageSampleFlagBias && memory.dataBits == 32u && !memory.imageR128 && !memory.imagePacked && memory.imageByElements == 0u; image.depthBitsCompatible = image.depthBitsCompatible && memory.dataBits == 32u; image.constantSwizzleCompatible = image.constantSwizzleCompatible && (op == IrOpcode::ImageSampleRaw || op == IrOpcode::ImageGatherRaw); + const bool cubeFaceLoad = op == IrOpcode::ImageRead && !memory.imageHasMip && memory.imageSampleFlags == 0u; + const bool cubeFaceSample = op == IrOpcode::ImageSampleRaw && (memory.imageSampleFlags == 0u || memory.imageSampleFlags == RdnaImageSampleFlagLevelZero || memory.imageSampleFlags == RdnaImageSampleFlagLod); + const bool cubeFaceStore = op == IrOpcode::ImageWrite && !memory.imageHasMip; + image.cubeFaceCompatible = image.cubeFaceCompatible && (cubeFaceLoad || cubeFaceSample || cubeFaceStore) && !memory.imagePacked && memory.imageByElements == 0u; + image.volumeArraySampleCompatible = image.volumeArraySampleCompatible && cubeFaceSample && !memory.imageR128 && !memory.imagePacked && memory.imageByElements == 0u; const bool flatVolumeLoad = op == IrOpcode::ImageRead && memory.imageSampleFlags == 0u; const bool flatVolumeSample = op == IrOpcode::ImageSampleRaw && memory.imageSampleFlags == RdnaImageSampleFlagLevelZero; const bool flatVolumeQuery = op == IrOpcode::ImageQueryDimensions; image.flatVolumeCompatible = image.flatVolumeCompatible && (flatVolumeLoad || flatVolumeSample || flatVolumeQuery) && !memory.imagePacked && memory.imageByElements == 0u; const bool flatLineLoad = op == IrOpcode::ImageRead && !memory.imageHasMip && memory.imageSampleFlags == 0u; - const bool flatLineSample = op == IrOpcode::ImageSampleRaw && (memory.imageSampleFlags == RdnaImageSampleFlagLevelZero || memory.imageSampleFlags == 0u); - image.flatLineCompatible = image.flatLineCompatible && (flatLineLoad || flatLineSample) && !memory.imagePacked && memory.imageByElements == 0u; + 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; + 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; if (op == IrOpcode::ImageGatherRaw || (memory.imageSampleFlags & unsupported) != 0u) image.emulatedCompare |= EmulatedCompare::Unsupported; if ((memory.imageSampleFlags & RdnaImageSampleFlagLevelZero) == 0u) image.emulatedCompare |= EmulatedCompare::RequiresSingleLevel; + } else if (ImageOpcodeInfoOf(op).needsSampler) { + if (op != IrOpcode::ImageSampleRaw) image.emulatedCompare |= EmulatedCompare::ConvertedSampleUnsupported; + if ((memory.imageSampleFlags & RdnaImageSampleFlagLevelZero) == 0u) image.emulatedCompare |= EmulatedCompare::ConvertedSampleRequiresSingleLevel; } } @@ -1376,7 +1965,7 @@ private: fail("memory operation has no resource handle"); } const auto& memory = m_program.Resources().memoryInfo[flags.index]; - if (memory.planningOnly || IsIndirectPlanningMemory(flags.index)) { + if (memory.planningOnly || IsTablePlanningMemory(flags.index)) { return; } IrValue* handle = nullptr; @@ -1422,9 +2011,9 @@ private: fail("image operation has invalid resource kind"); } handle = inst.Argument(0)->Resolve(); - const IndirectImagePlan* indirect = FindIndirectImage(*handle); - if (indirect != nullptr) { - source = indirect->source; + const TableColumnPlan* column = FindTableColumn(*handle); + if (column != nullptr) { + source = column->source; } else { GetHandle(inst.Argument(0), IrOpcode::GetImageResource, 8, handle, source); } @@ -1439,10 +2028,15 @@ private: if (inst.ArgumentCount() < 2) { fail("sampled image operation has no sampler handle"); } - IrValue* samplerHandle = nullptr; + IrValue* samplerHandle = inst.Argument(1)->Resolve(); std::uint32_t samplerSource = 0; - const bool sampleAdjust = (memory.imageSampleFlags & RdnaImageSampleFlagAdjust) != 0; - GetHandle(inst.Argument(1), IrOpcode::GetSamplerResource, 4, samplerHandle, samplerSource, true, sampleAdjust); + const TableColumnPlan* samplerColumn = FindTableColumn(*samplerHandle); + if (samplerColumn != nullptr) { + samplerSource = samplerColumn->source; + } else { + const bool sampleAdjust = (memory.imageSampleFlags & RdnaImageSampleFlagAdjust) != 0; + GetHandle(inst.Argument(1), IrOpcode::GetSamplerResource, 4, samplerHandle, samplerSource, true, sampleAdjust); + } sampler = AddSampler(samplerSource, flags.pc); if (sampler == std::numeric_limits::max()) { fail("sampler resource limit exceeded"); @@ -1465,7 +2059,7 @@ private: } for (std::uint32_t image = 0; image < m_info.images.size(); image++) { const DescriptorSource* imageSource = Source(m_info.images[image].source); - if (imageSource == nullptr || imageSource->dwordCount != 8 || imageSource->indirectImage.has_value()) { + if (imageSource == nullptr || imageSource->dwordCount != 8 || imageSource->tableColumn.has_value()) { continue; } bool alias = true; @@ -1491,9 +2085,12 @@ private: std::vector m_memoryPatches; // APS5_TRACE_BDA: address accesses seen by Collect (the first bdaTraceLimit are printed). unsigned m_bdaTraces = 0; - std::vector m_indirectImages; std::unordered_map> m_memoryUsers; bool m_memoryUsersIndexed = false; + std::vector m_tableColumns; + std::vector m_tablePlanningReads; + std::vector m_tablePlanningMemory; + std::vector m_unselectedHandles; struct EdgeSelectorEntry { IrBlock* block = nullptr; std::vector edges; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 6ded41848d..a111de6891 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -73,6 +73,35 @@ void ValidateOptions(const IrProgram& program, ShaderStageInputInfo inputInfo) { } } +void EnsurePixelInterpolatorCoverage(const IrProgram& program, ShaderPixelInputInfo& pixel) { + std::uint32_t needed = pixel.inputNum; + const auto consider = [&](std::uint32_t attribute) { + if (attribute < 32u && attribute + 1u > needed) needed = attribute + 1u; + }; + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + switch (inst->Opcode()) { + case IrOpcode::GetAttribute: + case IrOpcode::GetInterpolationParameter: + case IrOpcode::GetInterpolationParameterF16: + case IrOpcode::InterpolateHostP1: + case IrOpcode::InterpolateHostP2: { + const IrValue* attribute = inst->Argument(0)->Resolve(); + if (attribute->HasImmediate() && attribute->Type() == IrType::U32) consider(attribute->ImmediateU32()); + break; + } + default: + break; + } + } + } + if (needed <= pixel.inputNum) return; + for (std::uint32_t input = pixel.inputNum; input < needed; ++input) { + pixel.interpolatorSettings[input] = 0x20u; + } + pixel.inputNum = needed; +} + void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inputInfo) { for (const auto& block : program.Blocks()) { for (const IrValue* inst : block->Instructions()) { @@ -102,7 +131,7 @@ void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inpu return Fail("interpolation parameter reference is invalid"); } if (input->ImmediateU32() >= inputInfo.pixel->inputNum || component->ImmediateU32() >= 4u || mode->ImmediateU32() >= 3u) { - return Fail("interpolation parameter reference is out of range"); + return Fail("interpolation parameter reference is out of range (attribute " + std::to_string(input->ImmediateU32()) + " component " + std::to_string(component->ImmediateU32()) + " mode " + std::to_string(mode->ImmediateU32()) + ", " + std::to_string(inputInfo.pixel->inputNum) + " inputs)"); } break; } @@ -143,6 +172,7 @@ void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inpu } case StageInputKind::Layer: case StageInputKind::SampleId: + case StageInputKind::SampleMask: case StageInputKind::RasterPrimitive: case StageInputKind::LayerExported: if (program.Resources().stage != IrShaderStage::Pixel || component != 0u) { @@ -151,6 +181,7 @@ void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inpu break; case StageInputKind::VertexIndex: case StageInputKind::InstanceIndex: + case StageInputKind::BaseInstance: case StageInputKind::InvocationId: case StageInputKind::PrimitiveId: case StageInputKind::FrontFacing: @@ -171,6 +202,14 @@ void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inpu return Fail("typed barycentric component is out of range"); } break; + case StageInputKind::BaryCoordSmoothCenter: + case StageInputKind::BaryCoordNoPerspectiveCenter: + case StageInputKind::BaryCoordSmoothSample: + case StageInputKind::BaryCoordNoPerspectiveSample: + if (program.Resources().stage != IrShaderStage::Pixel || component >= 2u) { + return Fail("typed offset barycentric input is invalid"); + } + break; case StageInputKind::TessCoord: case StageInputKind::WorkgroupId: case StageInputKind::LocalInvocationId: @@ -236,7 +275,7 @@ bool IsPixelParameterFlat(const ShaderPixelInputInfo& pixel, std::uint32_t input } void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pixel, ShaderInfo& info) { - if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage) { + if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage || pixel->QuadMasked() || pixel->SampleLocationsVary() || pixel->ReordersSamples()) { AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); } if (pixel->psFrontFace) { @@ -246,6 +285,8 @@ void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pi std::array perVertex {}; std::array interpolated {}; std::array halfRead {}; + std::array readsP10 {}; + std::array readsP20 {}; for (const auto& block : program.Blocks()) { for (const IrValue* inst : block->Instructions()) { if (inst->Opcode() == IrOpcode::GetAttribute) { @@ -259,7 +300,9 @@ void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pi const auto input = inst->Argument(0)->Resolve()->ImmediateU32(); const auto mode = inst->Argument(2)->Resolve()->ImmediateU32(); read[input] = true; - perVertex[input] = perVertex[input] || mode == 1u || !IsPixelParameterFlat(*pixel, input); + readsP10[input] = readsP10[input] || mode == 0u; + readsP20[input] = readsP20[input] || mode == 1u; + perVertex[input] = perVertex[input] || (readsP10[input] && readsP20[input]) || !IsPixelParameterFlat(*pixel, input); } else if (inst->Opcode() == IrOpcode::GetInterpolationParameterF16) { const auto input = inst->Argument(0)->Resolve()->ImmediateU32(); const bool high = inst->Argument(3)->Resolve()->ImmediateU32() != 0u; @@ -326,10 +369,16 @@ void CollectComputeInputs(const ShaderComputeInputInfo* compute, ShaderInfo& inf } } +bool OwnControlPointWrite(const IrValue& inst) { + if (inst.Opcode() != IrOpcode::SetTessellationAttribute) return false; + const auto kind = static_cast(inst.Argument(0)->ImmediateU32()); + return kind == TessellationAttribute::ControlOutput || kind == TessellationAttribute::ControlShared; +} + void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { for (const auto& block : program.Blocks()) { for (const IrValue* inst : block->Instructions()) { - if (program.Resources().stage == IrShaderStage::TessellationControl && inst->Opcode() == IrOpcode::LaneId) { + if (program.Resources().stage == IrShaderStage::TessellationControl && (inst->Opcode() == IrOpcode::LaneId || OwnControlPointWrite(*inst))) { AddInput(info, StageInputKind::InvocationId, 0, 1, "gl_InvocationID"); } if (inst->Opcode() != IrOpcode::GetBuiltin) { @@ -343,6 +392,9 @@ void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { case StageInputKind::InstanceIndex: AddInput(info, kind, 0, 1, "gl_InstanceIndex"); break; + case StageInputKind::BaseInstance: + AddInput(info, kind, 0, 1, "gl_BaseInstance"); + break; case StageInputKind::InvocationId: AddInput(info, kind, 0, 1, "gl_InvocationID"); break; @@ -367,11 +419,20 @@ void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { case StageInputKind::SampleId: AddInput(info, kind, 0, 1, "gl_SampleID"); break; + case StageInputKind::SampleMask: + AddInput(info, kind, 0, 1, "gl_SampleMaskIn"); + break; case StageInputKind::BaryCoordSmooth: - AddInput(info, kind, 0, 3, "gl_BaryCoordKHR"); + case StageInputKind::BaryCoordSmoothCenter: + case StageInputKind::BaryCoordSmoothSample: + AddInput(info, StageInputKind::BaryCoordSmooth, 0, 3, "gl_BaryCoordKHR"); + if (kind == StageInputKind::BaryCoordSmoothSample && program.Metadata().pixelIteratesSamples) AddInput(info, StageInputKind::SampleId, 0, 1, "gl_SampleID"); break; case StageInputKind::BaryCoordNoPerspective: - AddInput(info, kind, 0, 3, "gl_BaryCoordNoPerspKHR"); + case StageInputKind::BaryCoordNoPerspectiveCenter: + case StageInputKind::BaryCoordNoPerspectiveSample: + AddInput(info, StageInputKind::BaryCoordNoPerspective, 0, 3, "gl_BaryCoordNoPerspKHR"); + if (kind == StageInputKind::BaryCoordNoPerspectiveSample && program.Metadata().pixelIteratesSamples) AddInput(info, StageInputKind::SampleId, 0, 1, "gl_SampleID"); break; case StageInputKind::WorkgroupId: AddInput(info, kind, 0, 3, "gl_WorkGroupID"); @@ -521,6 +582,9 @@ void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo if (!program.Resources().resourceTrackingComplete || program.Metadata().shaderInfoComplete) { return Fail(!program.Resources().resourceTrackingComplete ? "shader resources were not tracked" : "shader info already collected"); } + if (program.Resources().stage == IrShaderStage::Pixel && inputInfo.pixel != nullptr) { + EnsurePixelInterpolatorCoverage(program, const_cast(*inputInfo.pixel)); + } ValidateOptions(program, inputInfo); ValidateValueReferences(program, inputInfo); auto next = program.Info(); @@ -531,6 +595,12 @@ void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo return inst->Opcode() == IrOpcode::BitwiseXor32; }); }); + const bool ldsAtomic64 = std::ranges::any_of(program.Blocks(), [](const auto& block) { + return std::ranges::any_of(block->Instructions(), [](const IrValue* inst) { + return SharedAccessOf(inst->Opcode()) == SharedAccess::Atomic && inst->Type() == IrType::U64; + }); + }); + next.usesFaultBuffer = next.usesFaultBuffer || (next.sharedMemoryBytes != 0u && ldsAtomic64); switch (program.Resources().stage) { case IrShaderStage::Vertex: case IrShaderStage::Local: @@ -554,6 +624,7 @@ void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo } CollectBuiltinInputs(program, next); CollectOutputs(program, inputInfo, next); + if ((program.Resources().stage == IrShaderStage::Vertex || program.Resources().stage == IrShaderStage::Mesh) && inputInfo.vertex != nullptr && inputInfo.vertex->pointList) AddOutput(next, StageOutputKind::PointSize, 0, 0, "gl_PointSize"); CollectSamplerUses(program, next); program.Info() = std::move(next); program.Metadata().shaderInfoComplete = true; diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp index 690bad731a..e216d7b3a1 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp @@ -1,10 +1,14 @@ #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 @@ -43,9 +47,8 @@ const DescriptorSource* Source(const IrResourcePlan& program, std::uint32_t sour return &program.descriptorSources[source]; } -} - -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) { +template +bool evaluateRuntimeSources(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) { failureReason().clear(); static const bool debug = std::getenv("APS5_SRT_DEBUG") != nullptr; if (debug) { @@ -59,11 +62,13 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span cleanEvaluator(program, cleanRuntime); + BasicEvaluator evaluator(program, runtime, cleanFlatSlots, &cleanEvaluator); std::vector active; + std::vector activeFlat; if (evaluateFlat) { active.assign(program.descriptorSources.size() + program.info.images.size(), 1u); + activeFlat.assign(program.srtReads.size(), 1u); } if (evaluateFlat && !program.controlFlow.empty()) { for (const auto& block : program.controlFlow) { @@ -73,6 +78,7 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span visited(program.controlFlow.size()); std::vector pending {0}; @@ -94,9 +100,11 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::spandwordCount; index++) { inaccessible = {}; - if (!evaluator.Evaluate(source->dwords[index], value.dwords[index])) { + if (!evaluator.Evaluate(TNode::SourceDword(program, sourceIndex, index), value.dwords[index])) { if (inaccessible.read != nullptr) { if (debug) std::fprintf(stderr, "[srt] descriptor source %u reads inaccessible 0x%llx at pc 0x%x: zero words\n", sourceIndex, static_cast(inaccessible.address), inaccessible.read->Flags().pc); value.dwords.fill(0u); @@ -152,7 +160,9 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span poisoned; if (evaluateFlat) { flattened.resize(program.srtReads.size()); - for (const auto& read : program.srtReads) { + for (std::uint32_t slot = 0; slot < program.srtReads.size(); ++slot) { + const auto& read = program.srtReads[slot]; + if (read.flatOffset < activeFlat.size() && activeFlat[read.flatOffset] == 0u) continue; const bool clean = read.flatOffset < cleanFlatSlots.size() && cleanFlatSlots[read.flatOffset] != 0u; auto& selected = clean ? cleanEvaluator : evaluator; // A pure slot's raw read is reachable from no root, so it was not evaluated (nor @@ -161,11 +171,11 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::spanleaf = read.value->Resolve(); + trace->leaf = TNode::SrtRead(program, slot).Value(); trace->leafSlot = read.flatOffset; } inaccessible = {}; - const bool evaluated = read.flatOffset < flattened.size() && selected.Evaluate(read.value, flattened[read.flatOffset]); + const bool evaluated = read.flatOffset < flattened.size() && selected.Evaluate(TNode::SrtRead(program, slot), flattened[read.flatOffset]); if (pure) trace->leaf = nullptr; if (!evaluated && inaccessible.read != nullptr) { if (debug) std::fprintf(stderr, "[srt] flat offset %u reads inaccessible 0x%llx at pc 0x%x: poisoned\n", read.flatOffset, static_cast(inaccessible.address), inaccessible.read->Flags().pc); @@ -178,7 +188,7 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::spanFlags().pc; + const auto pc = read.read->template Flags().pc; if (std::none_of(poisoned.begin(), poisoned.end(), [&](const SrtReadPoison& entry) { return entry.pc == pc && entry.address == read.address; })) { char address[64]; std::snprintf(address, sizeof(address), "0x%llx at pc 0x%x", static_cast(read.address), pc); @@ -201,6 +211,141 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span(data); + for (std::size_t offset = 0; offset < std::min(bytes, 16384u); offset += 64) __builtin_prefetch(bytesData + offset); + }; + prefetch(compact.values.data(), compact.values.size() * sizeof(CompactPlanValue)); + prefetch(compact.arguments.data(), compact.arguments.size() * sizeof(std::uint32_t)); + prefetch(compact.sourceDwords.data(), compact.sourceDwords.size() * sizeof(std::uint32_t)); + prefetch(compact.srtReads.data(), compact.srtReads.size() * sizeof(std::uint32_t)); +} + +std::string describeDifference(const std::vector& compactResults, const std::vector& irResults, const std::vector& compactFlat, const std::vector& irFlat) { + for (std::size_t index = 0; index < std::min(compactResults.size(), irResults.size()); ++index) { + for (std::size_t dword = 0; dword < compactResults[index].dwords.size(); ++dword) { + if (compactResults[index].dwords[dword] != irResults[index].dwords[dword]) return "source " + std::to_string(index) + " dword " + std::to_string(dword) + ": compact " + std::to_string(compactResults[index].dwords[dword]) + ", IR " + std::to_string(irResults[index].dwords[dword]); + } + } + for (std::size_t index = 0; index < std::min(compactFlat.size(), irFlat.size()); ++index) { + if (compactFlat[index] != irFlat[index]) return "flat offset " + std::to_string(index) + ": compact " + std::to_string(compactFlat[index]) + ", IR " + std::to_string(irFlat[index]); + } + 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 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); + static const bool verify = std::getenv("APS5_VERIFY_SRT_COMPACT") != nullptr; + if (verify) { + const auto compactReason = RuntimeSourceFailureReason(); + SrtRuntime untraced = runtime; + untraced.readTrace = nullptr; + std::vector irResults; + std::vector irFlat = flat; + std::vector irActive; + std::vector irPoison; + std::uint32_t irNullRoots = 0; + const bool irEvaluated = evaluateRuntimeSources(program, sources, untraced, irResults, irFlat, evaluateFlat, cleanFlatSlots, irActive, poison != nullptr ? &irPoison : nullptr, nullRootReads != nullptr ? &irNullRoots : nullptr); + if (evaluated != irEvaluated) throw std::runtime_error("SRT compact walk " + std::string(evaluated ? "succeeded" : "failed (" + compactReason + ")") + " where the IR walk " + (irEvaluated ? "succeeded" : "failed (" + RuntimeSourceFailureReason() + ")")); + if (evaluated && (results != irResults || (evaluateFlat && flat != irFlat) || activeSources != irActive)) throw std::runtime_error("SRT compact walk differs from the IR walk at " + describeDifference(results, irResults, flat, irFlat)); + if (!evaluated) failureReason() = compactReason; + } + return evaluated; +} + +bool sameUserDataUse(const SrtUserDataUse& tape, const SrtUserDataUse& interpreted) { + if (tape.asBase != interpreted.asBase || tape.asValue != interpreted.asValue) return false; + auto tapeReads = tape.reads; + auto interpretedReads = interpreted.reads; + for (auto* reads : {&tapeReads, &interpretedReads}) { + std::sort(reads->begin(), 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 36baa22dc5..3369a7457a 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp @@ -2,6 +2,7 @@ #include "Optimization/SrtWalker/SrtAddressArithmetic.hpp" #include "Optimization/SrtWalker/SrtInstructionPredicates.hpp" #include "IntermediateRepresentation/IrBuilder.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include @@ -10,10 +11,53 @@ #include #include #include +#include +#include +#include +#include +#include +#include namespace ShaderRecompiler::Detail { -bool Evaluator::Evaluate(IrValue* value, std::uint32_t& result) { +namespace { + +struct DenseValuePool { + std::vector>> arrays; + std::vector*> free; + std::uint64_t stamp = 0; +}; + +DenseValuePool& denseValuePool() { + struct DenseValuePoolStorage {}; + return HostThreadLocal(); +} + +} + +DenseValues::DenseValues(std::size_t values) : _size(values) { + if (values == 0u) return; + auto& pool = denseValuePool(); + if (pool.free.empty()) { + pool.arrays.push_back(std::make_unique>()); + _array = pool.arrays.back().get(); + } else { + _array = pool.free.back(); + pool.free.pop_back(); + } + if (_array->size() < values) _array->resize(values); + _slots = _array->data(); + pool.stamp += 2u; + _stamp = pool.stamp; +} + +DenseValues::~DenseValues() { + if (_array == nullptr) return; + denseValuePool().free.push_back(_array); +} + +template +bool BasicEvaluator::Evaluate(TNode value, std::uint32_t& result) { std::uint64_t wide = 0; if (!EvaluateWide(value, wide)) { return false; @@ -22,63 +66,90 @@ bool Evaluator::Evaluate(IrValue* value, std::uint32_t& result) { return true; } -bool Evaluator::EvaluateWide(IrValue* raw, std::uint64_t& result) { - IrValue* value = raw->Resolve(); - if (value->HasImmediate()) { - switch (value->Type()) { - case IrType::Bool: result = value->ImmediateBool() ? 1u : 0u; return true; - case IrType::U8: result = value->ImmediateU8(); return true; - case IrType::U16: result = value->ImmediateU16(); return true; - case IrType::U32: result = value->ImmediateU32(); return true; - case IrType::U64: result = value->ImmediateU64(); return true; - case IrType::F32: result = Float32Bits(value->ImmediateF32()); return true; +template +bool BasicEvaluator::EvaluateWide(TNode value, std::uint64_t& result) { + if (value.HasImmediate()) { + const auto bits = value.ImmediateBits(); + switch (value.Type()) { + case IrType::Bool: result = bits != 0u ? 1u : 0u; return true; + case IrType::U8: result = static_cast(bits); return true; + case IrType::U16: result = static_cast(bits); return true; + case IrType::U32: result = static_cast(bits); return true; + case IrType::U64: result = bits; return true; + case IrType::F32: result = Float32Bits(std::bit_cast(static_cast(bits))); return true; default: return false; } } - if (value->Opcode() == IrOpcode::Void) { + if (value.Opcode() == IrOpcode::Void) { return false; } - IrValue* inst = value; - if (_activeMask != nullptr && IsRuntimeSelect(inst->Opcode()) && inst->ArgumentCount() == 3 && inst->Argument(0)->Resolve() == _activeMask) { - return EvaluateWide(inst->Argument(1), result); + const TNode& inst = value; + if (_masked && IsRuntimeSelect(inst.Opcode()) && inst.ArgumentCount() == 3 && ActiveMaskImplies(_activeMask, inst.Argument(0))) { + return EvaluateWide(inst.Argument(1), result); } - if (_cache.Find(inst, result)) { + if (inst.Id() < _dense.Size()) { + auto& slot = _dense.At(inst.Id()); + if (slot.stamp == _dense.Done()) { + result = slot.value; + return true; + } + if (slot.stamp == _dense.Visiting()) { + return false; + } + slot.stamp = _dense.Visiting(); + std::uint64_t out = 0; + if (!EvaluateInst(inst, out)) { + slot.stamp = 0; + static const bool debug = std::getenv("APS5_SRT_DEBUG") != nullptr; + if (debug) std::fprintf(stderr, "[srt] cannot evaluate %s (%zu arguments)\n", std::string(IrOpcodeName(inst.Opcode())).c_str(), inst.ArgumentCount()); + return false; + } + slot = {_dense.Done(), out}; + result = out; return true; } - if (std::find(_visiting.begin(), _visiting.end(), inst) != _visiting.end()) { + if (_cache.Find(inst.Value(), result)) { + return true; + } + if (std::find(_visiting.begin(), _visiting.end(), inst.Value()) != _visiting.end()) { return false; } - _visiting.push_back(inst); + _visiting.push_back(inst.Value()); std::uint64_t out = 0; - const bool evaluated = EvaluateInst(*inst, out); + const bool evaluated = EvaluateInst(inst, out); _visiting.pop_back(); if (!evaluated) { static const bool debug = std::getenv("APS5_SRT_DEBUG") != nullptr; - if (debug) std::fprintf(stderr, "[srt] cannot evaluate %s (%zu arguments)\n", std::string(IrOpcodeName(inst->Opcode())).c_str(), inst->ArgumentCount()); + if (debug) std::fprintf(stderr, "[srt] cannot evaluate %s (%zu arguments)\n", std::string(IrOpcodeName(inst.Opcode())).c_str(), inst.ArgumentCount()); return false; } - _cache.Insert(inst, out); + _cache.Insert(inst.Value(), out); result = out; return true; } -float Evaluator::Float32(std::uint64_t bits) { return std::bit_cast(static_cast(bits)); } +template +float BasicEvaluator::Float32(std::uint64_t bits) { return std::bit_cast(static_cast(bits)); } -std::uint64_t Evaluator::Float32Bits(float value) { return std::bit_cast(value); } +template +std::uint64_t BasicEvaluator::Float32Bits(float value) { return std::bit_cast(value); } -bool Evaluator::Arg(IrValue& inst, std::size_t index, std::uint64_t& result) { return EvaluateWide(inst.Argument(index), result); } +template +bool BasicEvaluator::Arg(const TNode& inst, std::size_t index, std::uint64_t& result) { return EvaluateWide(inst.Argument(index), result); } -bool Evaluator::EvaluatePhi(IrValue& inst, std::uint64_t& result) { - IrValue* value = ResolveInvariantPhi(_program, &inst); - return value != nullptr && EvaluateWide(value, result); +template +bool BasicEvaluator::EvaluatePhi(const TNode& inst, std::uint64_t& result) { + IrValue* value = ResolveInvariantPhi(_program, inst.Value()); + return value != nullptr && EvaluateWide(TNode::From(_program, value), result); } -bool Evaluator::EvaluateExtract(IrValue& inst, std::uint64_t& result) { - IrValue* index = inst.Argument(1)->Resolve(); - if (!index->HasImmediate() || index->Type() != IrType::U32) { +template +bool BasicEvaluator::EvaluateExtract(const TNode& inst, std::uint64_t& result) { + const TNode index = inst.Argument(1); + if (!index.HasImmediate() || index.Type() != IrType::U32) { return false; } - const auto component = index->ImmediateU32(); + const auto component = static_cast(index.ImmediateBits()); if (component >= 2u) { return false; } @@ -90,17 +161,17 @@ bool Evaluator::EvaluateExtract(IrValue& inst, std::uint64_t& result) { result = static_cast(packed >> (component * 32u)); return true; } - IrValue* source = inst.Argument(0)->Resolve(); - if (source->Opcode() == IrOpcode::Void) { + const TNode source = inst.Argument(0); + if (source.Opcode() == IrOpcode::Void) { return false; } - if (source->Opcode() == IrOpcode::CompositeConstructU32x2) { - return EvaluateWide(source->Argument(component), result); + if (source.Opcode() == IrOpcode::CompositeConstructU32x2) { + return EvaluateWide(source.Argument(component), result); } - if (source->Opcode() == IrOpcode::IAddCarry32) { + if (source.Opcode() == IrOpcode::IAddCarry32) { std::uint64_t lhs = 0; std::uint64_t rhs = 0; - if (!Arg(*source, 0, lhs) || !Arg(*source, 1, rhs)) { + if (!Arg(source, 0, lhs) || !Arg(source, 1, rhs)) { return false; } const auto sum = static_cast(static_cast(lhs)) + static_cast(rhs); @@ -110,20 +181,32 @@ bool Evaluator::EvaluateExtract(IrValue& inst, std::uint64_t& result) { return false; } -bool Evaluator::EvaluateRawRead(IrValue& inst, std::uint64_t& result) { - const auto flags = inst.Flags(); +template +bool BasicEvaluator::EvaluateRawRead(const TNode& inst, std::uint64_t& result) { + const auto flags = inst.template Flags(); if (flags.index >= _program.memoryInfo.size()) { return false; } const auto& mem = _program.memoryInfo[flags.index]; - IrValue* handle = inst.Argument(0)->Resolve(); - if (handle->Opcode() == IrOpcode::Void) { + const TNode handle = inst.Argument(0); + if (handle.Opcode() == IrOpcode::Void) { return false; } 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; @@ -132,7 +215,7 @@ bool Evaluator::EvaluateRawRead(IrValue& inst, std::uint64_t& result) { if (inst.Opcode() == IrOpcode::ReadConstBuffer) { std::uint64_t records = 0; std::uint64_t word3 = 0; - if (handle->ArgumentCount() != 4u || !Arg(*handle, 2, records) || !Arg(*handle, 3, word3)) { + if (handle.ArgumentCount() != 4u || !Arg(handle, 2, records) || !Arg(handle, 3, word3)) { return false; } if (immediate < 0) { @@ -153,17 +236,18 @@ bool Evaluator::EvaluateRawRead(IrValue& inst, std::uint64_t& result) { return false; } } - if (_nullRoots != nullptr && base == 0 && !LoadedFromMemory(*handle)) { + if (_nullRoots != nullptr && base == 0 && !LoadedFromMemory(*handle.Value())) { ++*_nullRoots; result = 0; return true; } - if (_inaccessible != nullptr && _runtime.accessible != nullptr && !_runtime.accessible(_runtime.userContext, address, sizeof(std::uint32_t)) && LoadedFromMemory(*handle)) { - *_inaccessible = {&inst, address}; + 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; } if (auto* trace = _runtime.readTrace; trace != nullptr) { - if (&inst == trace->leaf) trace->leaves.emplace_back(trace->leafSlot, address); + if (inst.Value() == trace->leaf) trace->leaves.emplace_back(trace->leafSlot, address); else trace->otherReads.push_back(address); } std::uint32_t word = 0; @@ -178,7 +262,193 @@ bool Evaluator::EvaluateRawRead(IrValue& inst, std::uint64_t& result) { return true; } -bool Evaluator::EvaluateInst(IrValue& inst, std::uint64_t& result) { +namespace { + +std::optional evaluatedArguments(IrOpcode opcode) { + switch (opcode) { + case IrOpcode::BitCastU32F32: + case IrOpcode::BitCastF32U32: + case IrOpcode::ConvertF32U32: + case IrOpcode::ConvertU32F32: + case IrOpcode::FPTrunc32: + case IrOpcode::FPIsNan32: + case IrOpcode::BitwiseNot32: + case IrOpcode::BitCount32: + case IrOpcode::FindILsb32: + case IrOpcode::FindUMsb32: + case IrOpcode::LogicalNot: return 1u; + case IrOpcode::CompositeConstructU64: + case IrOpcode::IAdd32: + case IrOpcode::IAdd64: + case IrOpcode::ISub32: + case IrOpcode::ISub64: + case IrOpcode::IMul32: + case IrOpcode::IMul64: + case IrOpcode::SMin32: + case IrOpcode::UMin32: + case IrOpcode::FPMul32: + case IrOpcode::FPOrdLessThanEqual32: + case IrOpcode::FPOrdGreaterThanEqual32: + case IrOpcode::BitwiseAnd32: + case IrOpcode::BitwiseAnd64: + case IrOpcode::BitwiseOr32: + case IrOpcode::BitwiseXor32: + case IrOpcode::ShiftLeftLogical32: + case IrOpcode::ShiftLeftLogical64: + case IrOpcode::ShiftRightLogical32: + case IrOpcode::ShiftRightLogical64: + case IrOpcode::ShiftRightArithmetic32: + case IrOpcode::ShiftRightArithmetic64: + case IrOpcode::IEqual32: + case IrOpcode::INotEqual32: + case IrOpcode::SLessThan32: + case IrOpcode::ULessThan32: + case IrOpcode::UGreaterThan32: + case IrOpcode::LogicalAnd: + case IrOpcode::LogicalOr: + case IrOpcode::LogicalXor: return 2u; + case IrOpcode::BitFieldUExtract: + case IrOpcode::BitFieldSExtract: + case IrOpcode::SelectU32: + case IrOpcode::SelectU1: + case IrOpcode::SelectF32: return 3u; + case IrOpcode::BitFieldInsert: return 4u; + default: return std::nullopt; + } +} + +std::optional evaluableAlone(const IrResourcePlan& program, const IrValue& value) { + if (value.HasImmediate()) { + switch (value.Type()) { + case IrType::Bool: + case IrType::U8: + case IrType::U16: + case IrType::U32: + case IrType::U64: + case IrType::F32: return true; + default: return false; + } + } + switch (value.Opcode()) { + case IrOpcode::GetUserData: + 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; + case IrOpcode::CompositeExtractU64: + case IrOpcode::CompositeExtractU32x2: { + if (value.ArgumentCount() < 2) return true; + const auto* index = value.Argument(1) != nullptr ? value.Argument(1)->Resolve() : nullptr; + if (index == nullptr || !index->HasImmediate() || index->Type() != IrType::U32 || index->ImmediateU32() >= 2u) return false; + if (value.Opcode() == IrOpcode::CompositeExtractU64) return std::nullopt; + const auto* source = value.Argument(0) != nullptr ? value.Argument(0)->Resolve() : nullptr; + if (source == nullptr || (source->Opcode() != IrOpcode::CompositeConstructU32x2 && source->Opcode() != IrOpcode::IAddCarry32)) return false; + return std::nullopt; + } + default: break; + } + const auto arguments = evaluatedArguments(value.Opcode()); + if (!arguments) return false; + if (value.ArgumentCount() < *arguments) return true; + return std::nullopt; +} + +} + +template +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 takesActiveArm = [activeMask](const IrValue* current) { + return activeMask != nullptr && IsRuntimeSelect(current->Opcode()) && current->ArgumentCount() == 3 && ActiveMaskImplies(activeMask, current->Argument(0)); + }; + 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; + } + const auto resolved = [](IrValue* value) { return value != nullptr ? value->Resolve() : nullptr; }; + if (current->Opcode() == IrOpcode::CompositeExtractU64) { + required.push_back(resolved(current->Argument(0))); + return required; + } + if (current->Opcode() == IrOpcode::CompositeExtractU32x2) { + auto* source = current->Argument(0)->Resolve(); + if (source->Opcode() == IrOpcode::CompositeConstructU32x2) { + required.push_back(resolved(source->Argument(current->Argument(1)->Resolve()->ImmediateU32()))); + } else { + required.push_back(resolved(source->Argument(0))); + required.push_back(resolved(source->Argument(1))); + } + return required; + } + const auto arguments = *evaluatedArguments(current->Opcode()); + for (std::size_t index = 0; index < arguments; ++index) { + auto* argument = current->Argument(index); + required.push_back(argument != nullptr ? argument->Resolve() : nullptr); + } + 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 (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); + for (auto* argument : required) { + if (argument != nullptr) pending.emplace_back(argument, false); + } + continue; + } + 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()); +} + +template +bool BasicEvaluator::EvaluateInst(const TNode& inst, std::uint64_t& result) { std::uint64_t a = 0; std::uint64_t b = 0; std::uint64_t c = 0; @@ -186,17 +456,24 @@ bool Evaluator::EvaluateInst(IrValue& inst, std::uint64_t& result) { const auto ternary = [&]() { return Arg(inst, 0, a) && Arg(inst, 1, b) && Arg(inst, 2, c); }; switch (inst.Opcode()) { case IrOpcode::GetUserData: { - const auto reg = RegIndex(static_cast(inst.Argument(0)->Register().index)); + const auto reg = RegIndex(static_cast(inst.Argument(0).RegisterIndex())); 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: { - Evaluator active(_program, _runtime, _cleanFlatSlots, _cleanEvaluator, inst.Argument(1)); + 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); @@ -212,18 +489,19 @@ bool Evaluator::EvaluateInst(IrValue& inst, std::uint64_t& result) { result = static_cast(a) | (static_cast(static_cast(b)) << 32u); return true; case IrOpcode::ReadConst: { - IrValue* slot = inst.Argument(1)->Resolve(); - if (!slot->HasImmediate() || slot->Type() != IrType::U32 || slot->ImmediateU32() >= _program.srtReads.size()) { + const TNode slotValue = inst.Argument(1); + const auto slot = static_cast(slotValue.ImmediateBits()); + if (!slotValue.HasImmediate() || slotValue.Type() != IrType::U32 || slot >= _program.srtReads.size()) { return false; } - if (slot->ImmediateU32() < _cleanFlatSlots.size() && _cleanFlatSlots[slot->ImmediateU32()] != 0u && _cleanEvaluator != nullptr) { - return _cleanEvaluator->EvaluateWide(_program.srtReads[slot->ImmediateU32()].value, result); + if (slot < _cleanFlatSlots.size() && _cleanFlatSlots[slot] != 0u && _cleanEvaluator != nullptr) { + return _cleanEvaluator->EvaluateWide(TNode::SrtRead(_program, slot), result); } - return EvaluateWide(_program.srtReads[slot->ImmediateU32()].value, result); + return EvaluateWide(TNode::SrtRead(_program, slot), result); } case IrOpcode::LoadAddressU32: case IrOpcode::ReadConstBuffer: - if (IsRawRead(_program, inst)) { + if (IsRawRead(_program, inst.Opcode(), inst.template Flags().index)) { return EvaluateRawRead(inst, result); } break; @@ -351,6 +629,26 @@ bool Evaluator::EvaluateInst(IrValue& inst, std::uint64_t& result) { return true; } return false; + case IrOpcode::BitCount32: + if (Arg(inst, 0, a)) { + result = static_cast(std::popcount(static_cast(a))); + return true; + } + return false; + case IrOpcode::FindILsb32: + if (Arg(inst, 0, a)) { + const auto val = static_cast(a); + result = val == 0u ? 0xffffffffu : static_cast(std::countr_zero(val)); + return true; + } + return false; + case IrOpcode::FindUMsb32: + if (Arg(inst, 0, a)) { + const auto val = static_cast(a); + result = val == 0u ? 0xffffffffu : static_cast(31u - std::countl_zero(val)); + return true; + } + return false; case IrOpcode::ShiftLeftLogical32: if (binary()) { result = static_cast(a) << (b & 31u); @@ -509,4 +807,7 @@ bool Evaluator::EvaluateInst(IrValue& inst, std::uint64_t& result) { return false; } +template class BasicEvaluator; +template class BasicEvaluator; + } diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtFlatSlotClasses.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtFlatSlotClasses.cpp index cca8ad6269..fe86e125ff 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtFlatSlotClasses.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtFlatSlotClasses.cpp @@ -11,7 +11,7 @@ std::vector ComputePureFlatSlots(const IrResourcePlan& plan) { std::vector pure(plan.srtReads.size(), 1u); if (pure.empty()) return pure; const bool disqualified = !plan.srtPlanComplete || plan.requiresSpecializationMemory - || std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.indirectImage.has_value(); }) + || std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value(); }) || (plan.uniformFill.fill.kind != UniformFillKind::None && plan.uniformFill.fill.words > plan.uniformFill.values.size()); if (disqualified) { std::fill(pure.begin(), pure.end(), 0u); diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp index 6ed2a7fdc3..855fd960af 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp @@ -6,11 +6,13 @@ namespace ShaderRecompiler::Detail { bool IsRawRead(const IrResourcePlan& program, const IrValue& inst) { - const auto op = inst.Opcode(); + return IsRawRead(program, inst.Opcode(), inst.Flags().index); +} + +bool IsRawRead(const IrResourcePlan& program, IrOpcode op, std::uint32_t index) { if (op != IrOpcode::LoadAddressU32 && op != IrOpcode::ReadConstBuffer) { return false; } - const auto index = inst.Flags().index; if (index >= program.memoryInfo.size()) { return false; } @@ -50,6 +52,17 @@ bool IsRuntimeSelect(IrOpcode opcode) { return opcode == IrOpcode::SelectU1 || opcode == IrOpcode::SelectU32 || opcode == IrOpcode::SelectF32; } +bool ActiveMaskImplies(const IrValue* mask, const IrValue* condition) { + if (mask == nullptr || condition == nullptr) return false; + mask = mask->Resolve(); + condition = condition->Resolve(); + if (mask == condition) return true; + if (mask->Opcode() == IrOpcode::LogicalAnd && mask->ArgumentCount() == 2) { + return ActiveMaskImplies(mask->Argument(0), condition) || ActiveMaskImplies(mask->Argument(1), condition); + } + return false; +} + bool IsRuntimeUniformOp(IrOpcode opcode) { switch (opcode) { case IrOpcode::BitCastU32F32: @@ -83,6 +96,9 @@ bool IsRuntimeUniformOp(IrOpcode opcode) { case IrOpcode::BitwiseOr32: case IrOpcode::BitwiseXor32: case IrOpcode::BitwiseNot32: + case IrOpcode::BitCount32: + case IrOpcode::FindILsb32: + case IrOpcode::FindUMsb32: case IrOpcode::SelectU1: case IrOpcode::SelectU32: case IrOpcode::SelectF32: diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp index 4da4c6009e..5cae7e7cf5 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp @@ -44,47 +44,68 @@ void PlanBuilder::Run() { } void PlanBuilder::Collect(IrValue* raw, std::uint32_t usePc) { - IrValue* value = raw->Resolve(); - if (value->Opcode() == IrOpcode::Void) { - return; - } - IrValue* inst = value; - const auto cycle = std::find(_visiting.begin(), _visiting.end(), inst); - if (cycle != _visiting.end()) { - const auto containsPhi = std::any_of(cycle, _visiting.end(), [](IrValue* candidate) { return candidate->Opcode() == IrOpcode::Phi; }); - if (containsPhi) { + struct Frame { + IrValue* inst = nullptr; + std::size_t next = 0; + }; + std::vector stack; + const auto enter = [&](IrValue* candidate) { + IrValue* inst = candidate->Resolve(); + if (inst->Opcode() == IrOpcode::Void) { return; } - Fail(_program.Resources(), usePc, "cyclic typed planning value " + std::string(IrOpcodeName(inst->Opcode())) + " without a phi"); + if (const auto cycle = _visitingIndex.find(inst); cycle != _visitingIndex.end()) { + const auto containsPhi = std::any_of(_visiting.begin() + static_cast(cycle->second), _visiting.end(), [](IrValue* candidate) { return candidate->Opcode() == IrOpcode::Phi; }); + if (containsPhi) { + return; + } + Fail(_program.Resources(), usePc, "cyclic typed planning value " + std::string(IrOpcodeName(inst->Opcode())) + " without a phi"); + } + if (_visited.contains(inst)) { + return; + } + _visitingIndex.emplace(inst, _visiting.size()); + _visiting.push_back(inst); + stack.push_back({inst, 0}); + }; + enter(raw); + while (!stack.empty()) { + auto& frame = stack.back(); + if (frame.next < frame.inst->ArgumentCount()) { + IrValue* argument = frame.inst->Argument(frame.next); + frame.next++; + enter(argument); + continue; + } + IrValue* inst = frame.inst; + stack.pop_back(); + _visiting.pop_back(); + _visitingIndex.erase(inst); + _visited.insert(inst); + Record(inst); } - if (std::find(_visited.begin(), _visited.end(), inst) != _visited.end()) { - return; - } - _visiting.push_back(inst); - for (std::size_t index = 0; index < inst->ArgumentCount(); index++) { - Collect(inst->Argument(index), usePc); - } - _visiting.pop_back(); - _visited.push_back(inst); +} + +void PlanBuilder::Record(IrValue* inst) { if (!IsRawRead(_program.Resources(), *inst)) { return; } IrValue* offset = inst->Argument(1)->Resolve(); const auto foldable = offset->HasImmediate() && offset->Type() == IrType::U32 && RuntimeValidator(_program.Resources(), RuntimeValueType::Any).Run(inst); if (!foldable) { - if (std::find(_program.Metadata().dynamicReads.begin(), _program.Metadata().dynamicReads.end(), value) == _program.Metadata().dynamicReads.end()) { - _program.Metadata().dynamicReads.push_back(value); + if (std::find(_program.Metadata().dynamicReads.begin(), _program.Metadata().dynamicReads.end(), inst) == _program.Metadata().dynamicReads.end()) { + _program.Metadata().dynamicReads.push_back(inst); } return; } for (std::uint32_t slot = 0; slot < _program.Resources().srtReads.size(); slot++) { - if (EquivalentValue(_program.Resources(), value, _program.Resources().srtReads[slot].value)) { + if (EquivalentValue(_program.Resources(), inst, _program.Resources().srtReads[slot].value)) { _patches.push_back({inst, slot, false}); return; } } const auto slot = static_cast(_program.Resources().srtReads.size()); - _program.Resources().srtReads.push_back({value, slot}); + _program.Resources().srtReads.push_back({inst, slot}); _patches.push_back({inst, slot, true}); } @@ -96,12 +117,12 @@ void PlanBuilder::PatchReads() { throw std::runtime_error("SrtWalker::BuildPlan patched value has no parent block"); } builder.SetInsertionPoint(*block); - IrValue& resource = builder.Emit(IrOpcode::GetSrtResource, IrType::SrtResource, {}); - block->RemoveInstruction(&resource); + IrValue& resource = _program.CreateValue(IrOpcode::GetSrtResource, IrType::SrtResource); block->InsertInstructionBefore(patch.inst, &resource); IrValue& slotConstant = builder.Constant(patch.slot); - IrValue& flat = builder.Emit(IrOpcode::ReadConst, IrType::U32, {&resource, &slotConstant}); - block->RemoveInstruction(&flat); + IrValue& flat = _program.CreateValue(IrOpcode::ReadConst, IrType::U32); + flat.AddArgument(&resource); + flat.AddArgument(&slotConstant); block->InsertInstructionBefore(patch.inst, &flat); patch.inst->ReplaceAllUsesWith(&flat); for (auto& info : _program.Metadata().blockInfo) { diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtRuntimeValidator.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtRuntimeValidator.cpp index 9fdf4415a4..eccca9155d 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtRuntimeValidator.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtRuntimeValidator.cpp @@ -54,7 +54,7 @@ bool RuntimeValidator::Validate(IrValue* raw, bool requireUniform) { } } if (!requireUniform) return finish(ValidateArguments(inst, false)); - if (_activeMask != nullptr && IsRuntimeSelect(op) && inst.ArgumentCount() == 3 && inst.Argument(0)->Resolve() == _activeMask) { + if (_activeMask != nullptr && IsRuntimeSelect(op) && inst.ArgumentCount() == 3 && ActiveMaskImplies(_activeMask, inst.Argument(0))) { if (_type == RuntimeValueType::Integer && !Validate(inst.Argument(2), false)) { return finish(false); } diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp new file mode 100644 index 0000000000..b18206cf38 --- /dev/null +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp @@ -0,0 +1,631 @@ +#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 && ActiveMaskImplies(mask, value->Argument(0))) return compile(value->Argument(1), context, depth + 1); + const auto key = std::make_pair(static_cast(value), context); + if (const auto found = built.find(key); found != built.end()) return found->second == Building ? fail() : found->second; + built.emplace(key, Building); + 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 activeFlat; + 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); + auto& activeFlat = scratch.activeFlat; + activeFlat.assign(plan.srtReads.size(), 1u); + if (!plan.controlFlow.empty()) { + for (const auto& block : plan.controlFlow) { + for (const auto source : block.sources) { + 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; + } + for (const auto slot : block.flatSlots) { + if (slot >= activeFlat.size()) return refuse(); + activeFlat[slot] = 0u; + } + } + auto& visited = scratch.visited; + visited.assign(plan.controlFlow.size(), 0u); + 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; + for (const auto slot : block.flatSlots) activeFlat[slot] = 1u; + std::uint64_t condition = 0; + const bool cleanEvaluable = block.condition != nullptr && runtime.readSpecializationMemory != nullptr && conditionRoots[index] != NoNode && evaluate(frame, conditionRoots[index], condition); + if (frame.nullRoot) return refuse(); + 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]; + if (read.flatOffset >= activeFlat.size()) return refuse(); + if (activeFlat[read.flatOffset] == 0u) continue; + const bool pure = trace != nullptr && read.flatOffset < plan.pureFlatSlots.size() && plan.pureFlatSlots[read.flatOffset] != 0u; + if (pure) { + trace->leaf = read.value->Resolve(); + 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/Optimization/src/SrtWalker/SrtWalker.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtWalker.cpp index 859b3dd838..8cba8936c8 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtWalker.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtWalker.cpp @@ -29,7 +29,7 @@ void SrtWalker::EvaluateUniformValues(const IrResourcePlan& program, std::span users = phi.Uses(); + std::vector users; + for (IrValue* user : phi.Uses()) users.push_back(user); phi.ReplaceUsesWith(same, true); for (IrValue* user : users) { if (user->IsPhi()) { diff --git a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp index 5ca2dc0cc6..79a616db94 100644 --- a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp +++ b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp @@ -39,6 +39,13 @@ bool Same(const Affine& left, const Affine& right) { return !left.dynamic && !right.dynamic && left == right; } +std::optional CommonMultiple(const Affine& left, const Affine& right) { + if (left.vertex != right.vertex || left.relative != right.relative || left.constant != right.constant) return std::nullopt; + const auto multiple = std::gcd(left.dynamic ? left.dynamicScale : 0, right.dynamic ? right.dynamicScale : 0); + if (multiple == 0) return std::nullopt; + return Affine{left.vertex, left.relative, left.constant, true, multiple, nullptr}; +} + [[noreturn]] void Fail(const char* reason, std::uint32_t pc) { char text[256]; std::snprintf(text, sizeof(text), "tessellation lowering: %s at pc 0x%x", reason, pc); @@ -55,6 +62,11 @@ bool IsBase(const IrValue* raw, TessellationBaseKind kind) { return value->Opcode() == IrOpcode::TessellationBase && IsImmediate(value->Argument(0), static_cast(kind)); } +bool OwnPatchRead(const IrValue* value) { + if (SharedAccessOf(value->Opcode()) == SharedAccess::Read) return true; + return BufferAccessOf(value->Opcode()) == BufferAccess::Read && value->ArgumentCount() > 3u && IsBase(value->Argument(3), TessellationBaseKind::OffChip); +} + std::int64_t FloorDiv(std::int64_t value, std::int64_t divisor) { const auto quotient = value / divisor; return quotient * divisor > value ? quotient - 1 : quotient; @@ -344,6 +356,7 @@ private: pending.push_back(value->Argument(1)->Resolve()); continue; } + if (value != start && OwnPatchRead(value)) continue; for (std::size_t index = 0; index < value->ArgumentCount(); ++index) { if (IrValue* argument = value->Argument(index)) pending.push_back(argument->Resolve()); } @@ -412,7 +425,7 @@ private: case IrOpcode::ShiftLeftLogical32: { const auto lhs = argument(0); const auto shift = scalar(1); - if (!lhs || !shift || *shift >= 31) return std::nullopt; + if (!lhs || !shift || *shift < 0 || *shift >= 31) return std::nullopt; return scale(*lhs, 1ll << *shift); } case IrOpcode::ShiftRightLogical32: { @@ -440,7 +453,7 @@ private: const auto source = argument(0); const auto offset = scalar(1); const auto count = scalar(2); - if (!source || !offset || !count) return std::nullopt; + if (!source || !offset || !count || *count < 0) return std::nullopt; if (*count == 0) return Affine{}; const auto shifted = shiftRight(*source, *offset); if (!shifted || *count >= 32) return shifted; @@ -450,18 +463,22 @@ private: if (active != nullptr && Implies(active, value->Argument(0))) return argument(1); const auto lhs = argument(1); const auto rhs = argument(2); - return lhs && rhs && Same(*lhs, *rhs) ? lhs : std::nullopt; + if (!lhs || !rhs) return std::nullopt; + if (Same(*lhs, *rhs)) return lhs; + return dynamic ? CommonMultiple(*lhs, *rhs) : std::nullopt; } default: break; } if (!value->IsPhi() || value->ArgumentCount() == 0) return std::nullopt; - const auto first = argument(0); - for (std::size_t index = 1; first && index < value->ArgumentCount(); ++index) { + auto result = argument(0); + for (std::size_t index = 1; result && index < value->ArgumentCount(); ++index) { const auto other = argument(index); - if (!other || !Same(*other, *first)) return std::nullopt; + if (!other) return std::nullopt; + if (Same(*result, *other)) continue; + result = dynamic ? CommonMultiple(*result, *other) : std::nullopt; } - return first && !first->dynamic ? first : std::nullopt; + return result && (dynamic || !result->dynamic) ? result : std::nullopt; } std::uint32_t vertexCount; @@ -547,10 +564,16 @@ std::uint32_t ComponentCount(IrOpcode opcode) { class Lowering { public: - Lowering(IrProgram& program, const ShaderTessellationInputInfo& tessellation) - : program(program), tessellation(tessellation), stage(program.Resources().stage), evaluator(stage == IrShaderStage::TessellationControl ? tessellation.outputControlPoints : 0u, stage == IrShaderStage::Local ? LocalVertexIndexCount : static_cast(tessellation.patchesPerGroup)) {} + Lowering(IrProgram& program, const ShaderTessellationInputInfo& tessellation, const TessellationTargetLimits* limits) + : program(program), tessellation(tessellation), limits(limits), stage(program.Resources().stage), merged(stage == IrShaderStage::TessellationControl && tessellation.mergedHull), + evaluator(stage == IrShaderStage::TessellationControl ? tessellation.outputControlPoints : 0u, stage == IrShaderStage::Local ? LocalVertexIndexCount : static_cast(tessellation.patchesPerGroup)), localStride(program.Metadata().tessellationLocalStride) {} void Run() { + if (merged) { + if (tessellation.inputControlPoints > tessellation.outputControlPoints) Fail("a merged hull program with more input than output control points runs its local part on lanes the hull stage does not have", 0u); + requireUniformBarriers(); + requireOwnLanes(); + } if (stage == IrShaderStage::TessellationControl) lowerOwnLaneBits(); collect(); derive(); @@ -574,11 +597,15 @@ public: } const bool data = usedAsData(relative); if (data) requireGroupedPatches(); + if (data) keepComputedReadsInPatch(*relative.front()); for (IrValue* base : relative) base->ReplaceAllUsesWith(data ? &groupedPatch(*base) : &constant(0u)); deadCodeEliminator.Eliminate(program); + if (stage == IrShaderStage::Local && tessellation.drawParameters) writeInstance(); program.Metadata().tessellationLocalStride = localStride; program.Metadata().tessellationControlStride = controlStride; program.Metadata().tessellationPatchStride = patchStride; + program.Metadata().tessellationSharedStride = sharedStride; + if (merged) requireMergedLimits(); } private: @@ -614,6 +641,8 @@ private: if (access.components == 0u || (shared != SharedAccess::Read && shared != SharedAccess::Write)) Fail("only 32-bit LDS reads and writes are supported in tessellation stages", flags.pc); if (stage == IrShaderStage::Local && access.write) { access.kind = TessellationAttribute::LocalOutput; + } else if (merged) { + access.kind = TessellationAttribute::ControlShared; } else if (stage == IrShaderStage::TessellationControl && !access.write) { access.kind = TessellationAttribute::ControlInput; } else { @@ -635,7 +664,7 @@ private: address = memory.offen ? inst->Argument(2) : nullptr; } if (address != nullptr) { - const auto evaluated = evaluator.Evaluate(address, access.predicate, access.kind == TessellationAttribute::ControlInput); + const auto evaluated = evaluator.Evaluate(address, access.predicate, access.kind == TessellationAttribute::ControlInput || access.kind == TessellationAttribute::EvaluationInput); if (!evaluated) Fail("the tessellation ring or LDS address is not affine in the control point and relative patch", flags.pc); access.address = *evaluated; access.addressValue = address; @@ -666,10 +695,16 @@ private: if (access.kind == TessellationAttribute::ControlInput && (access.address.dynamic || access.address.vertex == 0) && access.address.relative > 0 && access.address.relative % inputs == 0) agree(localStride, access.address.relative / inputs, access.pc); if (access.kind == TessellationAttribute::ControlOutput && access.address.vertex != 0) agree(controlStride, access.address.vertex, access.pc); if (access.kind == TessellationAttribute::ControlOutput && soleControlPoint(access)) agree(controlStride, access.address.relative, access.pc); + if (access.kind == TessellationAttribute::ControlShared) { + if (access.address.relative <= 0 || access.address.relative % inputs != 0) Fail("the merged hull program accesses LDS outside its patch", access.pc); + agree(sharedStride, access.address.relative / inputs, access.pc); + } if (access.kind == TessellationAttribute::EvaluationInput) { const auto end = access.address.constant + 4 * static_cast(access.components); if (access.address.relative > 0 && access.address.relative % outputs == 0 && access.address.constant >= 0 && end <= access.address.relative) { agree(controlStride, access.address.relative / outputs, access.pc); + } else if (access.address.dynamic) { + Fail("the domain shader reads off-chip memory at a computed offset outside its patch's control points", access.pc); } else { access.kind = TessellationAttribute::PatchInput; } @@ -708,6 +743,7 @@ private: break; case TessellationAttribute::EvaluationInput: if (address.constant < 0 || end > outputs * controlStride) Fail("the domain shader reads off-chip memory outside its patch's control points", access.pc); + if (address.dynamic && (address.dynamicScale == 0 || address.dynamicScale % controlStride != 0)) Fail("the domain shader reads off-chip memory at a computed offset that is not a whole number of control points", access.pc); break; case TessellationAttribute::Factor: { const bool quads = tessellation.domain == 2u; @@ -722,6 +758,13 @@ private: if (evaluator.Lowest(address) < static_cast(tessellation.patchesPerGroup) * outputs * controlStride) Fail("the domain shader reads off-chip memory that is neither its patch's control points nor a patch-constant record", access.pc); record(access, end); break; + case TessellationAttribute::ControlShared: + if (access.write) { + if (address.vertex != sharedStride || address.constant < 0 || end > sharedStride) Fail("the merged hull program writes LDS outside its own control point's record", access.pc); + } else if ((address.vertex != 0 && address.vertex != sharedStride) || address.constant < 0 || end + address.vertex * (outputs - 1) > inputs * sharedStride) { + Fail("the merged hull program reads LDS outside its patch's control point records", access.pc); + } + break; case TessellationAttribute::PatchOutput: break; } @@ -733,9 +776,12 @@ private: bool dynamicInputAddress(IrValue* user, IrValue* value) { if (user->Opcode() != IrOpcode::GetTessellationAttribute || user->Argument(1)->Resolve() != value->Resolve()) return false; - if (!IsImmediate(user->Argument(0), static_cast(TessellationAttribute::ControlInput))) return false; + const bool control = IsImmediate(user->Argument(0), static_cast(TessellationAttribute::ControlInput)); + const bool evaluation = IsImmediate(user->Argument(0), static_cast(TessellationAttribute::EvaluationInput)); + if (!control && !evaluation) return false; const auto address = evaluator.Evaluate(value, user->Argument(2), true); - return address && address->dynamic && address->relative == static_cast(tessellation.inputControlPoints) * localStride; + const auto stride = control ? static_cast(tessellation.inputControlPoints) * localStride : static_cast(tessellation.outputControlPoints) * controlStride; + return address && address->dynamic && address->relative == stride; } bool usedAsData(const std::vector& bases) { @@ -773,7 +819,9 @@ private: void requireGroupedPatches() const { if (stage == IrShaderStage::Local) Fail("the relative patch or vertex index is used outside a tessellation ring or LDS address", 0u); if (tessellation.primitiveGroupSize == 0u) Fail(stage == IrShaderStage::TessellationControl ? "the hull program uses its relative patch index as data, but GE_CNTL.PRIM_GRP_SIZE is 0 or not programmed" : "the domain program uses its relative patch index as data, but GE_CNTL.PRIM_GRP_SIZE is 0 or not programmed", 0u); - if (!tessellation.breakAtInstance) Fail(stage == IrShaderStage::TessellationControl ? "the hull program uses its relative patch index as data, but GE_CNTL.BREAK_WAVE_AT_EOI is clear, so the instances of a draw can share hull threadgroups" : "the domain program uses its relative patch index as data, but GE_CNTL.BREAK_WAVE_AT_EOI is clear, so the instances of a draw can share hull threadgroups", 0u); + if (tessellation.instancePatches == 0u || restartsPerInstance()) return; + if (stage == IrShaderStage::TessellationEvaluation) Fail("the domain program uses its relative patch index as data in an instanced draw whose instances share hull threadgroups (GE_CNTL.BREAK_WAVE_AT_EOI clear)", 0u); + if (!tessellation.drawParameters) Fail("the hull program uses its relative patch index as data in an instanced draw whose instances share hull threadgroups, which needs the device's shaderDrawParameters", 0u); } IrValue& groupedPatch(IrValue& base) { @@ -787,8 +835,14 @@ private: IrValue& count = constant(divisor); return emit(IrOpcode::ISub32, {&value, &emit(IrOpcode::IMul32, {&emit(IrOpcode::UDiv32, {&value, &count}), &count})}); }; - IrValue& primitive = emit(IrOpcode::GetBuiltin, {&constant(static_cast(StageInputKind::PrimitiveId)), &constant(0u)}); - return remainder(remainder(primitive, tessellation.primitiveGroupSize), tessellation.patchesPerGroup); + IrValue* primitive = &emit(IrOpcode::GetBuiltin, {&constant(static_cast(StageInputKind::PrimitiveId)), &constant(0u)}); + if (tessellation.instancePatches != 0u && !restartsPerInstance()) { + IrValue& always = program.CreateValue(IrOpcode::Void, IrType::Bool); + always.SetImmediateBool(true); + IrValue& instance = emit(IrOpcode::GetTessellationAttribute, {&constant(static_cast(TessellationAttribute::InstanceInput)), &constant(0u), &always}); + primitive = &emit(IrOpcode::IAdd32, {&emit(IrOpcode::IMul32, {&instance, &constant(tessellation.instancePatches)}), primitive}); + } + return remainder(remainder(*primitive, tessellation.primitiveGroupSize), tessellation.patchesPerGroup); } void removeUnobservedCycles() { @@ -821,6 +875,66 @@ private: } } + void requireUniformBarriers() const { + const auto& blocks = program.BlockOrder(); + for (IrBlock* block : blocks) { + if (std::none_of(block->Instructions().begin(), block->Instructions().end(), [](const IrValue* inst) { return inst->Opcode() == IrOpcode::Barrier; })) continue; + std::unordered_set seen{block}; + std::vector pending; + if (blocks.front() != block) pending.push_back(blocks.front()); + while (!pending.empty()) { + IrBlock* current = pending.back(); + pending.pop_back(); + if (!seen.insert(current).second) continue; + if (current->Successors().empty()) Fail("the merged hull program has an s_barrier that some invocations do not reach", 0u); + pending.insert(pending.end(), current->Successors().begin(), current->Successors().end()); + } + seen = {}; + pending.assign(block->Successors().begin(), block->Successors().end()); + while (!pending.empty()) { + IrBlock* current = pending.back(); + pending.pop_back(); + if (current == block) Fail("the merged hull program has an s_barrier inside a loop", 0u); + if (!seen.insert(current).second) continue; + pending.insert(pending.end(), current->Successors().begin(), current->Successors().end()); + } + } + } + + void requireOwnLanes() const { + for (IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + switch (inst->Opcode()) { + case IrOpcode::ReadLane: + case IrOpcode::WriteLane: + case IrOpcode::DppMoveU32: + case IrOpcode::DppUpdateU32: + case IrOpcode::Permlane16U32: + case IrOpcode::SwizzleU32: + case IrOpcode::PermuteU32: + case IrOpcode::BpermuteU32: + Fail("the merged hull program reads or writes another lane's register, which its hull stage does not hold at one lane per invocation", 0u); + default: + break; + } + } + } + } + + void requireMergedLimits() const { + if (limits == nullptr) Fail("the target has no tessellation limits for a merged hull program", 0u); + const auto vectors = [](std::uint32_t bytes) { return (bytes + 15u) / 16u; }; + const auto patch = vectors(patchStride); + const auto perVertex = vectors(controlStride) + vectors(sharedStride); + const auto locations = vectors(controlStride) + patch + vectors(sharedStride); + const auto total = 4ull * (static_cast(tessellation.outputControlPoints) * perVertex + patch); + if (4u * locations > limits->maxControlPerVertexOutputComponents || 4u * patch > limits->maxControlPerPatchOutputComponents || total > limits->maxControlTotalOutputComponents || 4u > limits->maxControlPerVertexInputComponents) { + char text[256]; + std::snprintf(text, sizeof(text), "the merged hull program's outputs exceed the device limits (%u per-vertex locations, %u patch vectors, %llu components)", locations, patch, static_cast(total)); + Fail(text, 0u); + } + } + void requireFirstInvocation(const Access& access) { if (tessellation.outputControlPoints == 1u) return; std::unordered_set path; @@ -833,6 +947,33 @@ private: return result; } + [[nodiscard]] bool restartsPerInstance() const { + const auto patches = tessellation.instancePatches; + const auto group = tessellation.primitiveGroupSize; + return group != 0u && (patches % group == 0u || (group % tessellation.patchesPerGroup == 0u && patches % tessellation.patchesPerGroup == 0u)); + } + + void writeInstance() { + IrBlock* entry = program.BlockOrder().front(); + IrValue* first = entry->Instructions().empty() ? nullptr : entry->Instructions().front(); + const auto place = [&](IrOpcode opcode, IrType type, std::initializer_list arguments) -> IrValue& { + IrValue& value = program.CreateValue(opcode, type); + for (IrValue* argument : arguments) value.AddArgument(argument); + if (first != nullptr) { + entry->InsertInstructionBefore(first, &value); + } else { + entry->AppendInstruction(&value); + } + return value; + }; + IrValue& index = place(IrOpcode::GetBuiltin, IrType::U32, {&constant(static_cast(StageInputKind::InstanceIndex)), &constant(0u)}); + IrValue& base = place(IrOpcode::GetBuiltin, IrType::U32, {&constant(static_cast(StageInputKind::BaseInstance)), &constant(0u)}); + IrValue& instance = place(IrOpcode::ISub32, IrType::U32, {&index, &base}); + IrValue& truth = program.CreateValue(IrOpcode::Void, IrType::Bool); + truth.SetImmediateBool(true); + static_cast(place(IrOpcode::SetTessellationAttribute, IrType::Void, {&constant(static_cast(TessellationAttribute::InstanceOutput)), &constant(0u), &instance, &truth})); + } + IrValue& insert(const Access& access, IrOpcode opcode, IrType type, std::initializer_list arguments) { IrValue& value = program.CreateValue(opcode, type); for (IrValue* argument : arguments) value.AddArgument(argument); @@ -841,16 +982,41 @@ private: } IrValue& componentAddress(const Access& access, std::uint32_t component) { - if (access.address.dynamic) return insert(access, IrOpcode::IAdd32, IrType::U32, {access.addressValue, &constant(access.memoryOffset + 4u * component)}); + if (access.address.dynamic) { + const std::uint32_t byteOffset = access.memoryOffset + 4u * component; + return byteOffset == 0u ? *access.addressValue : insert(access, IrOpcode::IAdd32, IrType::U32, {access.addressValue, &constant(byteOffset)}); + } auto address = access.address.constant + 4 * static_cast(component); if (access.kind == TessellationAttribute::PatchOutput || access.kind == TessellationAttribute::PatchInput) address -= FloorDiv(address, patchStride) * patchStride; IrValue& offset = constant(static_cast(address)); - if (access.kind != TessellationAttribute::ControlInput || access.address.vertex == 0) return offset; + const bool indexed = access.kind == TessellationAttribute::ControlInput || (access.kind == TessellationAttribute::ControlShared && !access.write); + if (!indexed || access.address.vertex == 0) return offset; IrValue& invocation = insert(access, IrOpcode::GetBuiltin, IrType::U32, {&constant(static_cast(StageInputKind::InvocationId)), &constant(0u)}); IrValue& scaled = insert(access, IrOpcode::IMul32, IrType::U32, {&invocation, &constant(static_cast(access.address.vertex))}); return insert(access, IrOpcode::IAdd32, IrType::U32, {&scaled, &offset}); } + void keepComputedReadsInPatch(IrValue& base) { + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + if (inst->Opcode() != IrOpcode::GetTessellationAttribute) continue; + const bool control = IsImmediate(inst->Argument(0), static_cast(TessellationAttribute::ControlInput)); + if (!control && !IsImmediate(inst->Argument(0), static_cast(TessellationAttribute::EvaluationInput))) continue; + const auto address = evaluator.Evaluate(inst->Argument(1), inst->Argument(2), true); + if (!address || !address->dynamic || address->relative == 0) continue; + IrValue& patch = program.CreateValue(IrOpcode::IMul32, IrType::U32); + patch.AddArgument(&base); + patch.AddArgument(&constant(static_cast(address->relative))); + block->InsertInstructionBefore(inst, &patch); + IrValue& inPatch = program.CreateValue(IrOpcode::ISub32, IrType::U32); + inPatch.AddArgument(inst->Argument(1)); + inPatch.AddArgument(&patch); + block->InsertInstructionBefore(inst, &inPatch); + inst->ReplaceArgument(1, &inPatch); + } + } + } + void rewrite(const Access& access) { IrValue& kind = constant(static_cast(access.kind)); const bool shared = SharedAccessOf(access.inst->Opcode()) != SharedAccess::None; @@ -904,20 +1070,23 @@ private: IrProgram& program; const ShaderTessellationInputInfo& tessellation; + const TessellationTargetLimits* limits; IrShaderStage stage; + bool merged; AddressEvaluator evaluator; std::vector accesses; - std::uint32_t localStride = 0; + std::uint32_t localStride; std::uint32_t controlStride = 0; std::uint32_t patchStride = 0; + std::uint32_t sharedStride = 0; }; } -void TessellationLowering::Lower(IrProgram& program, const ShaderTessellationInputInfo& tessellation) const { +void TessellationLowering::Lower(IrProgram& program, const ShaderTessellationInputInfo& tessellation, const TessellationTargetLimits* limits) const { const auto stage = program.Resources().stage; if (stage != IrShaderStage::Local && stage != IrShaderStage::TessellationControl && stage != IrShaderStage::TessellationEvaluation) return; - Lowering(program, tessellation).Run(); + Lowering(program, tessellation, limits).Run(); } } diff --git a/core/shader/recompiler/Optimization/src/VectorWindowStoreEliminator.cpp b/core/shader/recompiler/Optimization/src/VectorWindowStoreEliminator.cpp new file mode 100644 index 0000000000..32248e72f9 --- /dev/null +++ b/core/shader/recompiler/Optimization/src/VectorWindowStoreEliminator.cpp @@ -0,0 +1,78 @@ +#include "Optimization/VectorWindowStoreEliminator.hpp" +#include +#include + +namespace ShaderRecompiler { + +namespace { + +using LiveSlots = std::vector; + +const IrValue* constantSlot(const IrValue* inst, std::size_t slots) { + const IrValue* index = inst->Argument(0)->Resolve(); + return index->HasImmediate() && index->ImmediateU32() < slots ? index : nullptr; +} + +void transfer(const IrValue* inst, LiveSlots& live) { + if (inst->Opcode() == IrOpcode::LoadVectorWindow) { + if (const IrValue* slot = constantSlot(inst, live.size())) live[slot->ImmediateU32()] = true; + else live.assign(live.size(), true); + } else if (inst->Opcode() == IrOpcode::StoreVectorWindow) { + if (const IrValue* slot = constantSlot(inst, live.size())) live[slot->ImmediateU32()] = false; + } +} + +} + +VectorWindowStoreEliminationStats VectorWindowStoreEliminator::Eliminate(IrProgram& program) const { + VectorWindowStoreEliminationStats stats; + const std::size_t slots = program.Metadata().vectorWindowSize; + if (slots == 0u) return stats; + const auto& order = program.BlockOrder(); + std::unordered_map liveIn; + for (const IrBlock* block : order) liveIn.emplace(block, LiveSlots(slots, false)); + const auto liveOut = [&](const IrBlock* block) { + LiveSlots live(slots, false); + for (const IrBlock* successor : block->Successors()) { + const auto found = liveIn.find(successor); + if (found == liveIn.end()) return LiveSlots(slots, true); + for (std::size_t slot = 0; slot < slots; ++slot) live[slot] = live[slot] || found->second[slot]; + } + return live; + }; + for (bool changed = true; changed;) { + changed = false; + for (auto block = order.rbegin(); block != order.rend(); ++block) { + auto live = liveOut(*block); + const auto& instructions = (*block)->Instructions(); + for (auto inst = instructions.rbegin(); inst != instructions.rend(); ++inst) transfer(*inst, live); + auto& entry = liveIn[*block]; + if (live != entry) { + entry = std::move(live); + changed = true; + } + } + } + for (IrBlock* block : order) { + auto live = liveOut(block); + auto& instructions = block->Instructions(); + for (auto inst = instructions.end(); inst != instructions.begin();) { + --inst; + IrValue* value = *inst; + if (value->Opcode() == IrOpcode::StoreVectorWindow) { + const IrValue* slot = constantSlot(value, slots); + if (slot != nullptr && !live[slot->ImmediateU32()]) { + value->Invalidate(); + inst = instructions.erase(inst); + value->SetParent(nullptr); + ++stats.removedStores; + continue; + } + } + transfer(value, live); + } + } + return stats; +} + +} diff --git a/core/shader/recompiler/PatchSpirvTools.cmake b/core/shader/recompiler/PatchSpirvTools.cmake index 2a6e17df0e..4acc117b3b 100644 --- a/core/shader/recompiler/PatchSpirvTools.cmake +++ b/core/shader/recompiler/PatchSpirvTools.cmake @@ -3,31 +3,101 @@ set(spirvToolsRedundancySource "${spirvToolsSourceDir}/redundancy_elimination.cp set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${spirvToolsRedundancySource}") file(READ "${spirvToolsRedundancySource}" spirvToolsRedundancy) +set(spirvToolsLocalRedundancySource "${spirvToolsSourceDir}/local_redundancy_elimination.cpp") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${spirvToolsLocalRedundancySource}") +file(READ "${spirvToolsLocalRedundancySource}" spirvToolsLocalRedundancy) +set(spirvToolsExpectedElimination [=[ + auto func = [this, &vnTable, &modified, value_to_ids](Instruction* inst) { + if (inst->result_id() == 0) { + return; + } + + uint32_t value = vnTable.GetValueNumber(inst); + + if (value == 0) { + return; + } + + auto candidate = value_to_ids->insert({value, inst->result_id()}); + if (!candidate.second) { + context()->KillNamesAndDecorates(inst); + context()->ReplaceAllUsesWith(inst->result_id(), candidate.first->second); + context()->KillInst(inst); + modified = true; + } + }; + block->ForEachInst(func); +]=]) +string(FIND "${spirvToolsLocalRedundancy}" "${spirvToolsExpectedElimination}" spirvToolsEliminationPosition) +if(spirvToolsEliminationPosition EQUAL -1) + message(FATAL_ERROR "SPIRV-Tools value elimination has changed; review the local traversal fix") +endif() + set(spirvToolsOldTraversal [=[ + struct State { + DominatorTreeNode* node; + std::map value_to_id_map; + }; + std::vector todo; + todo.push_back({bb, std::map()}); + bool modified = false; + for (size_t next_node = 0; next_node < todo.size(); next_node++) { + modified |= EliminateRedundanciesInBB(todo[next_node].node->bb_, vnTable, + &todo[next_node].value_to_id_map); for (DominatorTreeNode* child : todo[next_node].node->children_) { todo.push_back({child, todo[next_node].value_to_id_map}); } + } ]=]) set(spirvToolsNewTraversal [=[ - const auto& children = todo[next_node].node->children_; - for (size_t child_index = 0; child_index < children.size(); ++child_index) { - if (child_index + 1 == children.size()) { - todo.push_back({children[child_index], - std::move(todo[next_node].value_to_id_map)}); - } else { - todo.push_back({children[child_index], todo[next_node].value_to_id_map}); - } + struct State { + DominatorTreeNode* node; + size_t next_child; + size_t added_begin; + bool entered; + }; + std::map value_to_id_map; + std::vector added_values; + std::vector todo; + todo.push_back({bb, 0, 0, false}); + bool modified = false; + while (!todo.empty()) { + auto& state = todo.back(); + if (!state.entered) { + state.entered = true; + state.node->bb_->ForEachInst([&](Instruction* inst) { + if (inst->result_id() == 0) return; + const uint32_t value = vnTable.GetValueNumber(inst); + if (value == 0) return; + auto candidate = value_to_id_map.insert({value, inst->result_id()}); + if (candidate.second) { + added_values.push_back(value); + } else { + context()->KillNamesAndDecorates(inst); + context()->ReplaceAllUsesWith(inst->result_id(), candidate.first->second); + context()->KillInst(inst); + modified = true; + } + }); } - todo[next_node].value_to_id_map.clear(); + const auto& children = state.node->children_; + if (state.next_child != children.size()) { + auto* child = children[state.next_child++]; + todo.push_back({child, 0, added_values.size(), false}); + continue; + } + while (added_values.size() != state.added_begin) { + value_to_id_map.erase(added_values.back()); + added_values.pop_back(); + } + todo.pop_back(); + } ]=]) string(FIND "${spirvToolsRedundancy}" "${spirvToolsOldTraversal}" spirvToolsTraversalPosition) if(spirvToolsTraversalPosition EQUAL -1) message(FATAL_ERROR "SPIRV-Tools redundancy traversal has changed; review the local memory fix") endif() string(REPLACE "${spirvToolsOldTraversal}" "${spirvToolsNewTraversal}" spirvToolsRedundancy "${spirvToolsRedundancy}") -string(REPLACE "#include \"source/opt/redundancy_elimination.h\"\n" - "#include \"source/opt/redundancy_elimination.h\"\n\n#include \n" - spirvToolsRedundancy "${spirvToolsRedundancy}") set(spirvToolsPatchedDir "${CMAKE_BINARY_DIR}/3rdparty/SPIRV-Tools/patched") set(spirvToolsPatchedSource "${spirvToolsPatchedDir}/redundancy_elimination.cpp") @@ -50,3 +120,49 @@ if(NOT spirvToolsReplacements EQUAL 1) message(FATAL_ERROR "SPIRV-Tools optimizer sources have changed; review the local memory fix") endif() set_property(TARGET SPIRV-Tools-opt PROPERTY SOURCES "${spirvToolsPatchedSources}") + +get_target_property(spirvToolsValidatorSourceDir SPIRV-Tools-static SOURCE_DIR) +set(spirvToolsValidatorSource "${spirvToolsValidatorSourceDir}/val/validate_id.cpp") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${spirvToolsValidatorSource}") +file(READ "${spirvToolsValidatorSource}" spirvToolsValidator) +string(FIND "${spirvToolsValidator}" "#include " spirvToolsValidatorInclude) +if(spirvToolsValidatorInclude EQUAL -1) + message(FATAL_ERROR "SPIRV-Tools validator includes have changed; review the local lifetime fix") +endif() +string(REPLACE "#include " "#include \n#include " spirvToolsValidator "${spirvToolsValidator}") +foreach(table IN ITEMS allow deny) + set(oldDeclaration "static std::unordered_set instruction_${table}_set{") + set(newDeclaration "static constexpr spv::Op instruction_${table}_set[]{") + set(oldLookup "instruction_${table}_set.count(opcode)") + set(newLookup "(std::find(std::begin(instruction_${table}_set),\n std::end(instruction_${table}_set), opcode) !=\n std::end(instruction_${table}_set))") + string(FIND "${spirvToolsValidator}" "${oldDeclaration}" declarationPosition) + string(FIND "${spirvToolsValidator}" "${oldLookup}" lookupPosition) + if(declarationPosition EQUAL -1 OR lookupPosition EQUAL -1) + message(FATAL_ERROR "SPIRV-Tools validator type-operand tables have changed; review the local lifetime fix") + endif() + string(REPLACE "${oldDeclaration}" "${newDeclaration}" spirvToolsValidator "${spirvToolsValidator}") + string(REPLACE "${oldLookup}" "${newLookup}" spirvToolsValidator "${spirvToolsValidator}") +endforeach() +set(spirvToolsPatchedValidator "${spirvToolsPatchedDir}/validate_id.cpp") +file(CONFIGURE OUTPUT "${spirvToolsPatchedValidator}" CONTENT "${spirvToolsValidator}" @ONLY) +foreach(validatorTarget IN ITEMS SPIRV-Tools-static SPIRV-Tools-shared) + if(NOT TARGET ${validatorTarget}) + continue() + endif() + get_target_property(validatorSources ${validatorTarget} SOURCES) + set(patchedValidatorSources) + set(validatorReplacements 0) + foreach(source IN LISTS validatorSources) + get_filename_component(sourcePath "${source}" ABSOLUTE BASE_DIR "${spirvToolsValidatorSourceDir}") + if(sourcePath STREQUAL spirvToolsValidatorSource) + list(APPEND patchedValidatorSources "${spirvToolsPatchedValidator}") + math(EXPR validatorReplacements "${validatorReplacements} + 1") + else() + list(APPEND patchedValidatorSources "${source}") + endif() + endforeach() + if(NOT validatorReplacements EQUAL 1) + message(FATAL_ERROR "SPIRV-Tools validator sources have changed; review the local lifetime fix") + endif() + set_property(TARGET ${validatorTarget} PROPERTY SOURCES "${patchedValidatorSources}") +endforeach() diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index 44e1e996fe..ae9ec178cf 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -28,12 +28,18 @@ inline constexpr std::uint32_t ExportPackingBase = 8224u; inline constexpr std::uint32_t DualSourceBlend = 8232u; inline constexpr std::uint32_t RasterPrimitive = 8233u; inline constexpr std::uint32_t LayerExported = 8234u; +inline constexpr std::uint32_t StencilReferenceOffset = 8235u; +inline constexpr std::uint32_t ReversedBlend = 8236u; +inline constexpr std::uint32_t DepthRemapBase = 8240u; +inline constexpr std::uint32_t DepthRemapWords = 1u; +inline constexpr std::uint32_t WorkgroupOffsetBase = 8241u; +inline constexpr std::uint32_t WorkgroupOffsetWords = 3u; inline constexpr std::uint32_t HeapCountBase = 8256u; inline constexpr std::uint32_t MipCountBase = 8320u; +inline constexpr std::uint32_t StoreQuantizeBase = 12288u; +inline constexpr std::uint32_t StoreQuantizeWords = 4u; inline constexpr std::uint32_t DescriptorIndexBase = 16384u; inline constexpr std::uint32_t DescriptorIndexStride = 128u; -inline constexpr std::uint32_t ImageModeBase = 32768u; -inline constexpr std::uint32_t ImageModeStride = 256u; inline constexpr std::uint32_t FoldOffsetBase = 57344u; inline constexpr std::uint32_t UnnormalizedBaseLevelBase = 57408u; @@ -41,6 +47,10 @@ inline constexpr std::uint32_t DescriptorIndex(std::uint32_t binding, std::uint3 return DescriptorIndexBase + binding * DescriptorIndexStride + element; } +inline constexpr bool IsWorkgroupOffset(std::uint32_t id) { + return id >= WorkgroupOffsetBase && id < WorkgroupOffsetBase + WorkgroupOffsetWords; +} + } } diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp index 5eb41904a0..ae3cbe0839 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp @@ -2,7 +2,9 @@ #define CORE_SHADER_RECOMPILIER_RDNADECODER_INCLUDE_RDNADECODER_RDNADESCRIPTORFORMAT_HPP #include "IntermediateRepresentation/IrMetadata.hpp" +#include #include +#include namespace ShaderRecompiler { @@ -24,6 +26,15 @@ enum class ImageType : std::uint32_t { [[nodiscard]] std::uint32_t DepthBitsTextureWidth(std::uint32_t word1, std::uint32_t word3); [[nodiscard]] bool IsDepthBitsTexture(std::uint32_t word1, std::uint32_t word3); +[[nodiscard]] const std::array& GcnBufferFormats(); +[[nodiscard]] std::uint32_t GcnBufferWord3(std::uint32_t word1, std::uint32_t word3); +inline constexpr std::uint32_t GcnBufferWord1Mask = 0xffff0fffu; +[[nodiscard]] std::uint32_t GcnImageFormat(std::uint32_t dataFormat, std::uint32_t numberFormat); +[[nodiscard]] std::array GcnImageWords(std::span words); +[[nodiscard]] constexpr std::uint32_t GcnImageTilingIndex(std::uint32_t word7) { return word7 & 0x1fu; } +[[nodiscard]] constexpr bool GcnImagePow2Pad(std::uint32_t word7) { return ((word7 >> 5u) & 1u) != 0u; } +[[nodiscard]] constexpr std::uint32_t GcnImagePitch(std::uint32_t word7) { return ((word7 >> 6u) & 0x3fffu) + 1u; } + [[nodiscard]] constexpr std::uint32_t SrgbDecodeBit(IrBufferFormat format) { switch (format) { case IrBufferFormat::Format8Srgb: return 1u; diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp index 9456f38cd1..d52379a7c3 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp @@ -21,6 +21,10 @@ private: RdnaInstructionFamily GetRdnaInstructionFamily(std::uint32_t word); RdnaInstruction DecodeRdnaInstruction(std::uint32_t programCounter, std::span code, std::uint32_t wordIndex); +RdnaInstruction DecodeRdnaEncodedInstruction(std::uint32_t programCounter, std::span code, std::uint32_t wordIndex); +RdnaInstruction DecodeGcnInstruction(std::uint32_t programCounter, std::span code, std::uint32_t wordIndex); +void SetGcnGuestShaders(bool enabled); +bool GcnGuestShaders(); RdnaProgram DecodeRdnaFrontProgram(std::span front); void DecodeRdnaProgram(std::span code, RdnaProgram& program); RdnaOperand DecodeRdnaScalarSource(std::uint32_t code, std::uint32_t programCounter); diff --git a/core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp new file mode 100644 index 0000000000..e5ccedb328 --- /dev/null +++ b/core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp @@ -0,0 +1,349 @@ +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include "GcnOpcodeTables.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ShaderRecompiler { + +namespace { + +std::atomic g_gcnGuestShaders{false}; + +constexpr std::uint32_t LiteralSource = 0xffu; +constexpr std::uint32_t VccSdst = 106u; +constexpr std::uint32_t NullSoffset = 0x7du; +constexpr std::uint32_t LinkedCallSopk = 0x16u; +constexpr std::uint32_t CodeEndSopp = 0x1fu; +constexpr std::uint32_t SetRegImmSopk = 0x15u; + +template +std::optional mapOpcode(const std::array, N>& table, std::uint32_t opcode) { + const auto found = std::lower_bound(table.begin(), table.end(), opcode, [](const auto& entry, std::uint32_t value) { return entry.first < value; }); + if (found == table.end() || found->first != opcode) return std::nullopt; + return found->second; +} + +[[noreturn]] void unsupported(const char* family, std::uint32_t opcode, std::uint32_t programCounter) { + throw UnsupportedInstructionError(std::string("GCN ") + family + " opcode " + std::to_string(opcode) + " at program counter " + std::to_string(programCounter) + " is not implemented"); +} + +std::uint32_t replaceBits(std::uint32_t word, std::uint32_t shift, std::uint32_t width, std::uint32_t value) { + const std::uint32_t mask = ((1u << width) - 1u) << shift; + return (word & ~mask) | ((value << shift) & mask); +} + +bool hasLiteral(std::uint32_t source) { + return source == LiteralSource; +} + +struct Encoded { + std::array words{}; + std::uint32_t rdnaWords = 0; + std::uint32_t gcnWords = 0; +}; + +std::uint32_t literalAt(std::span code, std::uint32_t index, std::uint32_t programCounter) { + if (index >= code.size()) throw std::out_of_range("GCN literal constant at program counter " + std::to_string(programCounter) + " is past the end of the code"); + return code[index]; +} + +Encoded encodeScalar(std::span code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + Encoded out; + out.gcnWords = 1; + std::uint32_t opcode = 0; + std::optional mapped; + bool literal = false; + const auto prefix9 = word >> 23u; + if (prefix9 == 0x17du) { + opcode = (word >> 8u) & 0xffu; + mapped = mapOpcode(GcnTables::Sop1, opcode); + if (!mapped) unsupported("SOP1", opcode, programCounter); + out.words[0] = replaceBits(word, 8, 8, *mapped); + literal = hasLiteral(word & 0xffu); + } else if (prefix9 == 0x17eu) { + opcode = (word >> 16u) & 0x7fu; + mapped = mapOpcode(GcnTables::Sopc, opcode); + if (!mapped) unsupported("SOPC", opcode, programCounter); + out.words[0] = replaceBits(word, 16, 7, *mapped); + literal = hasLiteral(word & 0xffu) || hasLiteral((word >> 8u) & 0xffu); + } else if (prefix9 == 0x17fu) { + opcode = (word >> 16u) & 0x7fu; + mapped = opcode == CodeEndSopp ? std::optional(CodeEndSopp) : mapOpcode(GcnTables::Sopp, opcode); + if (!mapped) unsupported("SOPP", opcode, programCounter); + out.words[0] = replaceBits(word, 16, 7, *mapped); + } else if ((word >> 28u) == 0xbu) { + opcode = (word >> 23u) & 0x1fu; + mapped = opcode == LinkedCallSopk ? std::optional(LinkedCallSopk) : mapOpcode(GcnTables::Sopk, opcode); + if (!mapped) unsupported("SOPK", opcode, programCounter); + out.words[0] = replaceBits(word, 23, 5, *mapped); + literal = opcode == SetRegImmSopk; + } else { + opcode = (word >> 23u) & 0x7fu; + mapped = mapOpcode(GcnTables::Sop2, opcode); + if (!mapped) unsupported("SOP2", opcode, programCounter); + out.words[0] = replaceBits(word, 23, 7, *mapped); + literal = hasLiteral(word & 0xffu) || hasLiteral((word >> 8u) & 0xffu); + } + out.rdnaWords = 1; + if (literal) { + out.words[1] = literalAt(code, index + 1, programCounter); + out.rdnaWords = out.gcnWords = 2; + } + return out; +} + +Encoded encodeSmrd(std::span code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + const auto opcode = (word >> 22u) & 0x1fu; + const auto mapped = mapOpcode(GcnTables::Smrd, opcode); + if (!mapped) unsupported("SMRD", opcode, programCounter); + const auto sdst = (word >> 15u) & 0x7fu; + const auto sbase = (word >> 9u) & 0x3fu; + const bool immediate = ((word >> 8u) & 1u) != 0; + const auto offset = word & 0xffu; + if (!immediate && offset == LiteralSource) unsupported("SMRD literal-offset", opcode, programCounter); + Encoded out; + out.gcnWords = 1; + out.rdnaWords = 2; + out.words[0] = (0x3du << 26u) | (*mapped << 18u) | (sdst << 6u) | sbase; + out.words[1] = immediate ? (NullSoffset << 25u) | (offset * 4u) : (offset << 25u); + return out; +} + +Encoded encodeVop3Words(std::uint32_t word0, std::uint32_t word1, std::uint32_t rdnaOpcode, bool carryOut) { + Encoded out; + out.gcnWords = 2; + out.rdnaWords = 2; + const auto vdst = word0 & 0xffu; + std::uint32_t encoded = (0x35u << 26u) | (rdnaOpcode << 16u) | vdst; + if (carryOut) { + encoded |= ((word0 >> 8u) & 0x7fu) << 8u; + } else { + encoded |= ((word0 >> 8u) & 0x7u) << 8u; + encoded |= ((word0 >> 11u) & 1u) << 15u; + } + out.words[0] = encoded; + out.words[1] = word1; + return out; +} + +std::optional reversedShift(std::uint32_t gcnVop3Opcode) { + switch (gcnVop3Opcode) { + case 0x115u: return 0x116u; + case 0x117u: return 0x118u; + case 0x119u: return 0x11au; + case 0x161u: return 0x2ffu; + case 0x162u: return 0x300u; + case 0x163u: return 0x301u; + default: return std::nullopt; + } +} + +bool compareWritesExec(std::uint32_t gcnVop3Opcode) { + return gcnVop3Opcode < 0x100u && (gcnVop3Opcode & 0x10u) != 0; +} + +std::uint32_t swapSources(std::uint32_t word1) { + const auto src0 = word1 & 0x1ffu; + const auto src1 = (word1 >> 9u) & 0x1ffu; + const auto neg0 = (word1 >> 29u) & 1u; + const auto neg1 = (word1 >> 30u) & 1u; + word1 = (word1 & ~0x3ffffu & ~(3u << 29u)) | src1 | (src0 << 9u) | (neg1 << 29u) | (neg0 << 30u); + return word1; +} + +std::uint32_t swapAbsolute(std::uint32_t word0) { + const auto abs0 = (word0 >> 8u) & 1u; + const auto abs1 = (word0 >> 9u) & 1u; + return (word0 & ~(3u << 8u)) | (abs1 << 8u) | (abs0 << 9u); +} + +bool gcnVop3bCarryOut(std::uint32_t opcode) { + switch (opcode) { + case 0x125u: case 0x126u: case 0x127u: case 0x128u: case 0x129u: case 0x12au: case 0x16du: case 0x16eu: case 0x176u: case 0x177u: return true; + default: return false; + } +} + +Encoded encodeVop3(std::span code, std::uint32_t index, std::uint32_t programCounter) { + if (index + 1 >= code.size()) throw std::out_of_range("truncated GCN VOP3 instruction"); + const auto word0 = code[index]; + const auto opcode = (word0 >> 17u) & 0x1ffu; + if (compareWritesExec(opcode)) unsupported("VOP3 v_cmpx (writes both the scalar destination and EXEC)", opcode, programCounter); + if (const auto shift = reversedShift(opcode)) return encodeVop3Words(swapAbsolute(word0), swapSources(code[index + 1]), *shift, false); + const auto mapped = mapOpcode(GcnTables::Vop3, opcode); + if (!mapped) unsupported("VOP3", opcode, programCounter); + return encodeVop3Words(word0, code[index + 1], *mapped, gcnVop3bCarryOut(opcode)); +} + +Encoded promoteToVop3(std::uint32_t gcnVop3Opcode, std::uint32_t vdst, std::uint32_t src0, std::uint32_t src1, std::uint32_t src2, bool carryOut, std::span code, std::uint32_t index, std::uint32_t programCounter) { + const auto shift = reversedShift(gcnVop3Opcode); + const auto mapped = shift ? shift : mapOpcode(GcnTables::Vop3, gcnVop3Opcode); + if (!mapped) unsupported("VOP3-promoted", gcnVop3Opcode, programCounter); + const std::uint32_t word0 = carryOut ? (vdst | (VccSdst << 8u)) : vdst; + const std::uint32_t word1 = shift ? (src1 | (src0 << 9u) | (src2 << 18u)) : (src0 | (src1 << 9u) | (src2 << 18u)); + auto out = encodeVop3Words(word0, word1, *mapped, carryOut); + out.gcnWords = 1; + if (hasLiteral(src0)) { + out.words[2] = literalAt(code, index + 1, programCounter); + out.rdnaWords = 3; + out.gcnWords = 2; + } + return out; +} + +Encoded encodeVop2(std::span code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + const auto opcode = (word >> 25u) & 0x3fu; + const auto src0 = word & 0x1ffu; + if (const auto mapped = mapOpcode(GcnTables::Vop2, opcode)) { + Encoded out; + out.words[0] = replaceBits(word, 25, 6, *mapped); + out.rdnaWords = out.gcnWords = 1; + const bool constantK = opcode == 0x20u || opcode == 0x21u; + if (hasLiteral(src0) || constantK) { + out.words[1] = literalAt(code, index + 1, programCounter); + out.rdnaWords = out.gcnWords = 2; + } + return out; + } + const auto vdst = (word >> 17u) & 0xffu; + const auto vsrc1 = (word >> 9u) & 0xffu; + const bool laneSelect = opcode == 0x01u || opcode == 0x02u; + const bool carryOut = opcode == 0x25u || opcode == 0x26u || opcode == 0x27u; + return promoteToVop3(0x100u + opcode, vdst, src0, laneSelect ? vsrc1 : 0x100u + vsrc1, 0, carryOut, code, index, programCounter); +} + +Encoded encodeVop1(std::span code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + const auto opcode = (word >> 9u) & 0xffu; + const auto src0 = word & 0x1ffu; + if (const auto mapped = mapOpcode(GcnTables::Vop1, opcode)) { + Encoded out; + out.words[0] = replaceBits(word, 9, 8, *mapped); + out.rdnaWords = out.gcnWords = 1; + if (hasLiteral(src0)) { + out.words[1] = literalAt(code, index + 1, programCounter); + out.rdnaWords = out.gcnWords = 2; + } + return out; + } + return promoteToVop3(0x180u + opcode, (word >> 17u) & 0xffu, src0, 0, 0, false, code, index, programCounter); +} + +Encoded encodeVopc(std::span code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + const auto opcode = (word >> 17u) & 0xffu; + if (compareWritesExec(opcode)) unsupported("VOPC v_cmpx (writes both VCC and EXEC)", opcode, programCounter); + const auto mapped = mapOpcode(GcnTables::Vopc, opcode); + if (!mapped) unsupported("VOPC", opcode, programCounter); + Encoded out; + out.words[0] = replaceBits(word, 17, 8, *mapped); + out.rdnaWords = out.gcnWords = 1; + if (hasLiteral(word & 0x1ffu)) { + out.words[1] = literalAt(code, index + 1, programCounter); + out.rdnaWords = out.gcnWords = 2; + } + return out; +} + +Encoded encodeDs(std::span code, std::uint32_t index, std::uint32_t programCounter) { + if (index + 1 >= code.size()) throw std::out_of_range("truncated GCN DS instruction"); + const auto word0 = code[index]; + const auto opcode = (word0 >> 18u) & 0xffu; + const auto mapped = mapOpcode(GcnTables::Ds, opcode); + if (!mapped) unsupported("DS", opcode, programCounter); + Encoded out; + out.words[0] = replaceBits(word0, 18, 8, *mapped); + out.words[1] = code[index + 1]; + out.rdnaWords = out.gcnWords = 2; + return out; +} + +Encoded encodeMubuf(std::span code, std::uint32_t index, std::uint32_t programCounter) { + if (index + 1 >= code.size()) throw std::out_of_range("truncated GCN MUBUF instruction"); + const auto word0 = code[index]; + const auto opcode = (word0 >> 18u) & 0x7fu; + const auto mapped = mapOpcode(GcnTables::Mubuf, opcode); + if (!mapped) unsupported("MUBUF", opcode, programCounter); + if (((word0 >> 15u) & 1u) != 0) unsupported("MUBUF addr64", opcode, programCounter); + Encoded out; + out.words[0] = replaceBits(replaceBits(word0, 18, 7, *mapped & 0x7fu), 25, 1, *mapped >> 7u); + out.words[1] = code[index + 1]; + out.rdnaWords = out.gcnWords = 2; + return out; +} + +Encoded encodeMimg(std::span code, std::uint32_t index, std::uint32_t programCounter) { + if (index + 1 >= code.size()) throw std::out_of_range("truncated GCN MIMG instruction"); + const auto word0 = code[index]; + const auto opcode = (word0 >> 18u) & 0x7fu; + const auto mapped = mapOpcode(GcnTables::Mimg, opcode); + if (!mapped) unsupported("MIMG", opcode, programCounter); + const bool array = ((word0 >> 14u) & 1u) != 0; + Encoded out; + out.words[0] = replaceBits(replaceBits(replaceBits(word0, 18, 7, *mapped & 0x7fu), 14, 1, 0), 0, 8, (*mapped >> 7u) | ((array ? 5u : 1u) << 3u)); + out.words[1] = code[index + 1] & 0x03ffffffu; + out.rdnaWords = out.gcnWords = 2; + return out; +} + +Encoded encodePassThrough(std::span code, std::uint32_t index, std::uint32_t words, const char* family) { + if (index + words > code.size()) throw std::out_of_range(std::string("truncated GCN ") + family + " instruction"); + Encoded out; + for (std::uint32_t i = 0; i < words; ++i) out.words[i] = code[index + i]; + out.rdnaWords = out.gcnWords = words; + return out; +} + +Encoded encodeGcn(std::span code, std::uint32_t index, std::uint32_t programCounter) { + const auto word = code[index]; + if ((word >> 31u) == 0) { + switch ((word >> 25u) & 0x3fu) { + case 0x3eu: return encodeVopc(code, index, programCounter); + case 0x3fu: return encodeVop1(code, index, programCounter); + default: return encodeVop2(code, index, programCounter); + } + } + if ((word >> 30u) == 2u) return encodeScalar(code, index, programCounter); + switch (word >> 26u) { + case 0x30u: case 0x31u: return encodeSmrd(code, index, programCounter); + case 0x32u: return encodePassThrough(code, index, 1, "VINTRP"); + case 0x34u: return encodeVop3(code, index, programCounter); + case 0x36u: return encodeDs(code, index, programCounter); + case 0x38u: return encodeMubuf(code, index, programCounter); + case 0x3eu: return encodePassThrough(code, index, 2, "EXP"); + case 0x37u: unsupported("FLAT", (word >> 18u) & 0x7fu, programCounter); + case 0x3au: unsupported("MTBUF", (word >> 16u) & 7u, programCounter); + case 0x3cu: return encodeMimg(code, index, programCounter); + default: break; + } + throw std::invalid_argument("unknown GCN instruction family at program counter " + std::to_string(programCounter) + " raw word " + std::to_string(word)); +} + +} + +void SetGcnGuestShaders(bool enabled) { + g_gcnGuestShaders.store(enabled, std::memory_order_release); +} + +bool GcnGuestShaders() { + return g_gcnGuestShaders.load(std::memory_order_acquire); +} + +RdnaInstruction DecodeGcnInstruction(std::uint32_t programCounter, std::span code, std::uint32_t wordIndex) { + if (wordIndex >= code.size()) throw std::out_of_range("word index is out of the code span bounds"); + const auto encoded = encodeGcn(code, wordIndex, programCounter); + auto instruction = DecodeRdnaEncodedInstruction(programCounter, std::span(encoded.words.data(), encoded.rdnaWords), 0); + instruction.wordCount = encoded.gcnWords; + return instruction; +} + +} diff --git a/core/shader/recompiler/RdnaDecoder/src/GcnOpcodeTables.hpp b/core/shader/recompiler/RdnaDecoder/src/GcnOpcodeTables.hpp new file mode 100644 index 0000000000..cbe30ec329 --- /dev/null +++ b/core/shader/recompiler/RdnaDecoder/src/GcnOpcodeTables.hpp @@ -0,0 +1,187 @@ +#ifndef CORE_SHADER_RECOMPILIER_RDNADECODER_SRC_GCNOPCODETABLES_HPP +#define CORE_SHADER_RECOMPILIER_RDNADECODER_SRC_GCNOPCODETABLES_HPP + +#include +#include +#include + +namespace ShaderRecompiler::GcnTables { + +inline constexpr std::array, 47> Sop1{{ + {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, + {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, {0x10, 0x10}, {0x11, 0x11}, {0x12, 0x12}, + {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, {0x18, 0x18}, {0x19, 0x19}, {0x1a, 0x1a}, + {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, {0x20, 0x20}, {0x21, 0x21}, {0x22, 0x22}, + {0x24, 0x24}, {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, {0x28, 0x28}, {0x29, 0x29}, {0x2a, 0x2a}, {0x2b, 0x2b}, + {0x2c, 0x2c}, {0x2d, 0x2d}, {0x2e, 0x2e}, {0x2f, 0x2f}, {0x30, 0x30}, {0x31, 0x31}, {0x34, 0x34}, +}}; + +inline constexpr std::array, 42> Sop2{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xe, 0xe}, {0xf, 0xf}, {0x10, 0x10}, {0x11, 0x11}, + {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, {0x18, 0x18}, {0x19, 0x19}, + {0x1a, 0x1a}, {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, {0x20, 0x20}, {0x21, 0x21}, + {0x22, 0x22}, {0x23, 0x23}, {0x24, 0x24}, {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, {0x28, 0x28}, {0x29, 0x29}, + {0x2a, 0x2a}, {0x2c, 0x2c}, +}}; + +inline constexpr std::array, 16> Sopc{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, +}}; + +inline constexpr std::array, 6> Sopk{{ + {0x0, 0x0}, {0x2, 0x2}, {0x10, 0x10}, {0x12, 0x12}, {0x13, 0x13}, {0x15, 0x15}, +}}; + +inline constexpr std::array, 21> Sopp{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, {0x8, 0x8}, + {0x9, 0x9}, {0xa, 0xa}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, {0x10, 0x10}, {0x11, 0x11}, + {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, +}}; + +inline constexpr std::array, 13> Smrd{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, + {0xb, 0xb}, {0xc, 0xc}, {0x1d, 0x21}, {0x1e, 0x24}, {0x1f, 0x21}, +}}; + +inline constexpr std::array, 58> Vop1{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, {0x10, 0x10}, + {0x11, 0x11}, {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, {0x18, 0x18}, + {0x19, 0x19}, {0x1a, 0x1a}, {0x20, 0x20}, {0x21, 0x21}, {0x22, 0x22}, {0x23, 0x23}, {0x24, 0x24}, {0x25, 0x25}, + {0x27, 0x27}, {0x2a, 0x2a}, {0x2b, 0x2b}, {0x2e, 0x2e}, {0x2f, 0x2f}, {0x31, 0x31}, {0x33, 0x33}, {0x34, 0x34}, + {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, {0x3b, 0x3b}, {0x3c, 0x3c}, + {0x3d, 0x3d}, {0x3e, 0x3e}, {0x3f, 0x3f}, {0x40, 0x40}, {0x41, 0x41}, {0x42, 0x42}, {0x43, 0x43}, {0x44, 0x44}, + {0x54, 0x54}, {0x55, 0x55}, +}}; + +inline constexpr std::array, 37> Vop2{{ + {0x0, 0x1}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, {0x8, 0x8}, {0x9, 0x9}, + {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xf, 0xf}, {0x10, 0x10}, {0x11, 0x11}, {0x12, 0x12}, {0x13, 0x13}, + {0x14, 0x14}, {0x16, 0x16}, {0x18, 0x18}, {0x1a, 0x1a}, {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1f, 0x1f}, + {0x20, 0x20}, {0x21, 0x21}, {0x28, 0x28}, {0x29, 0x29}, {0x2a, 0x2a}, {0x2f, 0x2f}, {0x32, 0x32}, {0x33, 0x33}, + {0x34, 0x34}, {0x35, 0x35}, {0x39, 0x39}, {0x3a, 0x3a}, {0x3b, 0x3b}, +}}; + +inline constexpr std::array, 216> Vopc{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, + {0x10, 0x10}, {0x11, 0x11}, {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, + {0x18, 0x18}, {0x19, 0x19}, {0x1a, 0x1a}, {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, + {0x20, 0x20}, {0x21, 0x21}, {0x22, 0x22}, {0x23, 0x23}, {0x24, 0x24}, {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, + {0x28, 0x28}, {0x29, 0x29}, {0x2a, 0x2a}, {0x2b, 0x2b}, {0x2c, 0x2c}, {0x2d, 0x2d}, {0x2e, 0x2e}, {0x2f, 0x2f}, + {0x30, 0x30}, {0x31, 0x31}, {0x32, 0x32}, {0x33, 0x33}, {0x34, 0x34}, {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, + {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, {0x3b, 0x3b}, {0x3c, 0x3c}, {0x3d, 0x3d}, {0x3e, 0x3e}, {0x3f, 0x3f}, + {0x40, 0x0}, {0x41, 0x1}, {0x42, 0x2}, {0x43, 0x3}, {0x44, 0x4}, {0x45, 0x5}, {0x46, 0x6}, {0x47, 0x7}, + {0x48, 0x8}, {0x49, 0x9}, {0x4a, 0xa}, {0x4b, 0xb}, {0x4c, 0xc}, {0x4d, 0xd}, {0x4e, 0xe}, {0x4f, 0xf}, + {0x50, 0x10}, {0x51, 0x11}, {0x52, 0x12}, {0x53, 0x13}, {0x54, 0x14}, {0x55, 0x15}, {0x56, 0x16}, {0x57, 0x17}, + {0x58, 0x18}, {0x59, 0x19}, {0x5a, 0x1a}, {0x5b, 0x1b}, {0x5c, 0x1c}, {0x5d, 0x1d}, {0x5e, 0x1e}, {0x5f, 0x1f}, + {0x60, 0x20}, {0x61, 0x21}, {0x62, 0x22}, {0x63, 0x23}, {0x64, 0x24}, {0x65, 0x25}, {0x66, 0x26}, {0x67, 0x27}, + {0x68, 0x28}, {0x69, 0x29}, {0x6a, 0x2a}, {0x6b, 0x2b}, {0x6c, 0x2c}, {0x6d, 0x2d}, {0x6e, 0x2e}, {0x6f, 0x2f}, + {0x70, 0x30}, {0x71, 0x31}, {0x72, 0x32}, {0x73, 0x33}, {0x74, 0x34}, {0x75, 0x35}, {0x76, 0x36}, {0x77, 0x37}, + {0x78, 0x38}, {0x79, 0x39}, {0x7a, 0x3a}, {0x7b, 0x3b}, {0x7c, 0x3c}, {0x7d, 0x3d}, {0x7e, 0x3e}, {0x7f, 0x3f}, + {0x80, 0x80}, {0x81, 0x81}, {0x82, 0x82}, {0x83, 0x83}, {0x84, 0x84}, {0x85, 0x85}, {0x86, 0x86}, {0x87, 0x87}, + {0x88, 0x88}, {0x89, 0x89}, {0x8a, 0x8a}, {0x8b, 0x8b}, {0x8c, 0x8c}, {0x8d, 0x8d}, {0x8e, 0x8e}, {0x90, 0x90}, + {0x91, 0x91}, {0x92, 0x92}, {0x93, 0x93}, {0x94, 0x94}, {0x95, 0x95}, {0x96, 0x96}, {0x97, 0x97}, {0x98, 0x98}, + {0xa0, 0xa0}, {0xa1, 0xa1}, {0xa2, 0xa2}, {0xa3, 0xa3}, {0xa4, 0xa4}, {0xa5, 0xa5}, {0xa6, 0xa6}, {0xa7, 0xa7}, + {0xa8, 0xa8}, {0xa9, 0xa9}, {0xaa, 0xaa}, {0xab, 0xab}, {0xac, 0xac}, {0xad, 0xad}, {0xae, 0xae}, {0xb0, 0xb0}, + {0xb1, 0xb1}, {0xb2, 0xb2}, {0xb3, 0xb3}, {0xb4, 0xb4}, {0xb5, 0xb5}, {0xb6, 0xb6}, {0xb7, 0xb7}, {0xb8, 0xb8}, + {0xc0, 0xc0}, {0xc1, 0xc1}, {0xc2, 0xc2}, {0xc3, 0xc3}, {0xc4, 0xc4}, {0xc5, 0xc5}, {0xc6, 0xc6}, {0xc7, 0xc7}, + {0xc8, 0xc8}, {0xc9, 0xc9}, {0xca, 0xca}, {0xcb, 0xcb}, {0xcc, 0xcc}, {0xcd, 0xcd}, {0xce, 0xce}, {0xcf, 0xcf}, + {0xd0, 0xd0}, {0xd1, 0xd1}, {0xd2, 0xd2}, {0xd3, 0xd3}, {0xd4, 0xd4}, {0xd5, 0xd5}, {0xd6, 0xd6}, {0xd7, 0xd7}, + {0xe0, 0xe0}, {0xe1, 0xe1}, {0xe2, 0xe2}, {0xe3, 0xe3}, {0xe4, 0xe4}, {0xe5, 0xe5}, {0xe6, 0xe6}, {0xe7, 0xe7}, + {0xf0, 0xf0}, {0xf1, 0xf1}, {0xf2, 0xf2}, {0xf3, 0xf3}, {0xf4, 0xf4}, {0xf5, 0xf5}, {0xf6, 0xf6}, {0xf7, 0xf7}, +}}; + +inline constexpr std::array, 324> Vop3{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0x10, 0x10}, + {0x11, 0x11}, {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, {0x18, 0x18}, + {0x19, 0x19}, {0x1a, 0x1a}, {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x20, 0x20}, {0x21, 0x21}, + {0x22, 0x22}, {0x23, 0x23}, {0x24, 0x24}, {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, {0x28, 0x28}, {0x29, 0x29}, + {0x2a, 0x2a}, {0x2b, 0x2b}, {0x2c, 0x2c}, {0x2d, 0x2d}, {0x2e, 0x2e}, {0x30, 0x30}, {0x31, 0x31}, {0x32, 0x32}, + {0x33, 0x33}, {0x34, 0x34}, {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, + {0x3b, 0x3b}, {0x3c, 0x3c}, {0x3d, 0x3d}, {0x3e, 0x3e}, {0x80, 0x80}, {0x81, 0x81}, {0x82, 0x82}, {0x83, 0x83}, + {0x84, 0x84}, {0x85, 0x85}, {0x86, 0x86}, {0x87, 0x87}, {0x88, 0x88}, {0x89, 0x89}, {0x8a, 0x8a}, {0x8b, 0x8b}, + {0x8c, 0x8c}, {0x8d, 0x8d}, {0x8e, 0x8e}, {0x90, 0x90}, {0x91, 0x91}, {0x92, 0x92}, {0x93, 0x93}, {0x94, 0x94}, + {0x95, 0x95}, {0x96, 0x96}, {0x97, 0x97}, {0x98, 0x98}, {0xa0, 0xa0}, {0xa1, 0xa1}, {0xa2, 0xa2}, {0xa3, 0xa3}, + {0xa4, 0xa4}, {0xa5, 0xa5}, {0xa6, 0xa6}, {0xa7, 0xa7}, {0xa8, 0xa8}, {0xa9, 0xa9}, {0xaa, 0xaa}, {0xab, 0xab}, + {0xac, 0xac}, {0xad, 0xad}, {0xae, 0xae}, {0xb0, 0xb0}, {0xb1, 0xb1}, {0xb2, 0xb2}, {0xb3, 0xb3}, {0xb4, 0xb4}, + {0xb5, 0xb5}, {0xb6, 0xb6}, {0xb7, 0xb7}, {0xb8, 0xb8}, {0xc0, 0xc0}, {0xc1, 0xc1}, {0xc2, 0xc2}, {0xc3, 0xc3}, + {0xc4, 0xc4}, {0xc5, 0xc5}, {0xc6, 0xc6}, {0xc7, 0xc7}, {0xc8, 0xc8}, {0xc9, 0xc9}, {0xca, 0xca}, {0xcb, 0xcb}, + {0xcc, 0xcc}, {0xcd, 0xcd}, {0xce, 0xce}, {0xcf, 0xcf}, {0xd0, 0xd0}, {0xd1, 0xd1}, {0xd2, 0xd2}, {0xd3, 0xd3}, + {0xd4, 0xd4}, {0xd5, 0xd5}, {0xd6, 0xd6}, {0xd7, 0xd7}, {0xe0, 0xe0}, {0xe1, 0xe1}, {0xe2, 0xe2}, {0xe3, 0xe3}, + {0xe4, 0xe4}, {0xe5, 0xe5}, {0xe6, 0xe6}, {0xe7, 0xe7}, {0xf0, 0xf0}, {0xf1, 0xf1}, {0xf2, 0xf2}, {0xf3, 0xf3}, + {0xf4, 0xf4}, {0xf5, 0xf5}, {0xf6, 0xf6}, {0xf7, 0xf7}, {0x100, 0x101}, {0x101, 0x360}, {0x102, 0x361}, {0x103, 0x103}, + {0x104, 0x104}, {0x105, 0x105}, {0x106, 0x106}, {0x107, 0x107}, {0x108, 0x108}, {0x109, 0x109}, {0x10a, 0x10a}, {0x10b, 0x10b}, + {0x10c, 0x10c}, {0x10f, 0x10f}, {0x110, 0x110}, {0x111, 0x111}, {0x112, 0x112}, {0x113, 0x113}, {0x114, 0x114}, {0x116, 0x116}, + {0x118, 0x118}, {0x11a, 0x11a}, {0x11b, 0x11b}, {0x11c, 0x11c}, {0x11d, 0x11d}, {0x11e, 0x363}, {0x11f, 0x11f}, {0x120, 0x120}, + {0x121, 0x121}, {0x122, 0x364}, {0x123, 0x365}, {0x124, 0x366}, {0x125, 0x30f}, {0x126, 0x310}, {0x127, 0x319}, {0x128, 0x128}, + {0x12b, 0x362}, {0x12d, 0x368}, {0x12e, 0x369}, {0x12f, 0x12f}, {0x130, 0x36a}, {0x131, 0x36b}, {0x132, 0x132}, {0x133, 0x133}, + {0x134, 0x134}, {0x135, 0x135}, {0x139, 0x139}, {0x13a, 0x13a}, {0x13b, 0x13b}, {0x140, 0x140}, {0x141, 0x141}, {0x142, 0x142}, + {0x143, 0x143}, {0x144, 0x144}, {0x145, 0x145}, {0x146, 0x146}, {0x147, 0x147}, {0x148, 0x148}, {0x149, 0x149}, {0x14a, 0x14a}, + {0x14b, 0x14b}, {0x14c, 0x14c}, {0x14d, 0x14d}, {0x14e, 0x14e}, {0x14f, 0x14f}, {0x150, 0x150}, {0x151, 0x151}, {0x152, 0x152}, + {0x153, 0x153}, {0x154, 0x154}, {0x155, 0x155}, {0x156, 0x156}, {0x157, 0x157}, {0x158, 0x158}, {0x159, 0x159}, {0x15a, 0x15a}, + {0x15b, 0x15b}, {0x15c, 0x15c}, {0x15d, 0x15d}, {0x15e, 0x15e}, {0x15f, 0x15f}, {0x160, 0x160}, {0x164, 0x164}, {0x165, 0x165}, + {0x166, 0x166}, {0x167, 0x167}, {0x168, 0x168}, {0x169, 0x169}, {0x16a, 0x16a}, {0x16b, 0x16b}, {0x16c, 0x16c}, {0x16d, 0x16d}, + {0x16e, 0x16e}, {0x16f, 0x16f}, {0x170, 0x170}, {0x171, 0x171}, {0x172, 0x172}, {0x173, 0x173}, {0x174, 0x174}, {0x175, 0x175}, + {0x176, 0x176}, {0x177, 0x177}, {0x180, 0x180}, {0x181, 0x181}, {0x182, 0x182}, {0x183, 0x183}, {0x184, 0x184}, {0x185, 0x185}, + {0x186, 0x186}, {0x187, 0x187}, {0x188, 0x188}, {0x18a, 0x18a}, {0x18b, 0x18b}, {0x18c, 0x18c}, {0x18d, 0x18d}, {0x18e, 0x18e}, + {0x18f, 0x18f}, {0x190, 0x190}, {0x191, 0x191}, {0x192, 0x192}, {0x193, 0x193}, {0x194, 0x194}, {0x195, 0x195}, {0x196, 0x196}, + {0x197, 0x197}, {0x198, 0x198}, {0x199, 0x199}, {0x19a, 0x19a}, {0x1a0, 0x1a0}, {0x1a1, 0x1a1}, {0x1a2, 0x1a2}, {0x1a3, 0x1a3}, + {0x1a4, 0x1a4}, {0x1a5, 0x1a5}, {0x1a7, 0x1a7}, {0x1aa, 0x1aa}, {0x1ab, 0x1ab}, {0x1ae, 0x1ae}, {0x1af, 0x1af}, {0x1b1, 0x1b1}, + {0x1b3, 0x1b3}, {0x1b4, 0x1b4}, {0x1b5, 0x1b5}, {0x1b6, 0x1b6}, {0x1b7, 0x1b7}, {0x1b8, 0x1b8}, {0x1b9, 0x1b9}, {0x1ba, 0x1ba}, + {0x1bb, 0x1bb}, {0x1bc, 0x1bc}, {0x1bd, 0x1bd}, {0x1be, 0x1be}, {0x1bf, 0x1bf}, {0x1c0, 0x1c0}, {0x1c1, 0x1c1}, {0x1c2, 0x1c2}, + {0x1c3, 0x1c3}, {0x1c4, 0x1c4}, {0x1d4, 0x1d4}, {0x1d5, 0x1d5}, {0x303, 0x303}, {0x307, 0x307}, {0x308, 0x308}, {0x30d, 0x30d}, + {0x30e, 0x30e}, {0x314, 0x314}, {0x346, 0x346}, {0x347, 0x347}, {0x351, 0x351}, {0x354, 0x354}, {0x357, 0x357}, {0x35e, 0x35e}, + {0x36d, 0x36d}, {0x36f, 0x36f}, {0x371, 0x371}, {0x372, 0x372}, +}}; + +inline constexpr std::array, 56> Mubuf{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, + {0x18, 0x18}, {0x1a, 0x1a}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, {0x30, 0x30}, {0x31, 0x31}, + {0x32, 0x32}, {0x33, 0x33}, {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, + {0x3b, 0x3b}, {0x3c, 0x3c}, {0x3d, 0x3d}, {0x3e, 0x3e}, {0x3f, 0x3f}, {0x40, 0x40}, {0x50, 0x50}, {0x51, 0x51}, + {0x52, 0x52}, {0x53, 0x53}, {0x55, 0x55}, {0x56, 0x56}, {0x57, 0x57}, {0x58, 0x58}, {0x59, 0x59}, {0x5a, 0x5a}, + {0x5b, 0x5b}, {0x5c, 0x5c}, {0x5d, 0x5d}, {0x5e, 0x5e}, {0x5f, 0x5f}, {0x60, 0x60}, {0x70, 0x71}, {0x71, 0x71}, +}}; + +inline constexpr std::array, 124> Ds{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x6, 0x6}, {0x7, 0x7}, + {0x8, 0x8}, {0x9, 0x9}, {0xa, 0xa}, {0xb, 0xb}, {0xc, 0xc}, {0xd, 0xd}, {0xe, 0xe}, {0xf, 0xf}, + {0x10, 0x10}, {0x11, 0x11}, {0x12, 0x12}, {0x13, 0x13}, {0x14, 0x14}, {0x18, 0x18}, {0x19, 0x19}, {0x1a, 0x1a}, + {0x1b, 0x1b}, {0x1c, 0x1c}, {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, {0x20, 0x20}, {0x21, 0x21}, {0x22, 0x22}, + {0x23, 0x23}, {0x24, 0x24}, {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, {0x28, 0x28}, {0x29, 0x29}, {0x2a, 0x2a}, + {0x2b, 0x2b}, {0x2c, 0x2c}, {0x2d, 0x2d}, {0x2e, 0x2e}, {0x2f, 0x2f}, {0x30, 0x30}, {0x31, 0x31}, {0x32, 0x32}, + {0x33, 0x33}, {0x34, 0x34}, {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, + {0x3b, 0x3b}, {0x3c, 0x3c}, {0x3d, 0x3d}, {0x3e, 0x3e}, {0x3f, 0x3f}, {0x40, 0x40}, {0x41, 0x41}, {0x42, 0x42}, + {0x43, 0x43}, {0x44, 0x44}, {0x45, 0x45}, {0x46, 0x46}, {0x47, 0x47}, {0x48, 0x48}, {0x49, 0x49}, {0x4a, 0x4a}, + {0x4b, 0x4b}, {0x4c, 0x4c}, {0x4d, 0x4d}, {0x4e, 0x4e}, {0x4f, 0x4f}, {0x50, 0x50}, {0x51, 0x51}, {0x52, 0x52}, + {0x53, 0x53}, {0x60, 0x60}, {0x61, 0x61}, {0x62, 0x62}, {0x63, 0x63}, {0x64, 0x64}, {0x65, 0x65}, {0x66, 0x66}, + {0x67, 0x67}, {0x68, 0x68}, {0x69, 0x69}, {0x6a, 0x6a}, {0x6b, 0x6b}, {0x6c, 0x6c}, {0x6d, 0x6d}, {0x6e, 0x6e}, + {0x6f, 0x6f}, {0x70, 0x70}, {0x71, 0x71}, {0x72, 0x72}, {0x73, 0x73}, {0x76, 0x76}, {0x77, 0x77}, {0x78, 0x78}, + {0x7e, 0x7e}, {0x80, 0x80}, {0x81, 0x81}, {0x82, 0x82}, {0x83, 0x83}, {0x84, 0x84}, {0x85, 0x85}, {0x86, 0x86}, + {0x87, 0x87}, {0x88, 0x88}, {0x89, 0x89}, {0x8a, 0x8a}, {0x8b, 0x8b}, {0x8d, 0x8d}, {0x92, 0x92}, {0x93, 0x93}, + {0xde, 0xde}, {0xdf, 0xdf}, {0xfe, 0xfe}, {0xff, 0xff}, +}}; + +inline constexpr std::array, 92> Mimg{{ + {0x0, 0x0}, {0x1, 0x1}, {0x2, 0x2}, {0x3, 0x3}, {0x4, 0x4}, {0x5, 0x5}, {0x8, 0x8}, {0x9, 0x9}, + {0xa, 0xa}, {0xb, 0xb}, {0xe, 0xe}, {0xf, 0xf}, {0x10, 0x10}, {0x11, 0x11}, {0x12, 0x12}, {0x14, 0x14}, + {0x15, 0x15}, {0x16, 0x16}, {0x17, 0x17}, {0x18, 0x18}, {0x19, 0x19}, {0x1a, 0x1a}, {0x1b, 0x1b}, {0x1c, 0x1c}, + {0x1d, 0x1d}, {0x1e, 0x1e}, {0x1f, 0x1f}, {0x20, 0x20}, {0x21, 0x21}, {0x22, 0x22}, {0x23, 0x23}, {0x24, 0x24}, + {0x25, 0x25}, {0x26, 0x26}, {0x27, 0x27}, {0x28, 0x28}, {0x29, 0x29}, {0x2a, 0x2a}, {0x2b, 0x2b}, {0x2c, 0x2c}, + {0x2d, 0x2d}, {0x2e, 0x2e}, {0x2f, 0x2f}, {0x30, 0x30}, {0x31, 0x31}, {0x32, 0x32}, {0x33, 0x33}, {0x34, 0x34}, + {0x35, 0x35}, {0x36, 0x36}, {0x37, 0x37}, {0x38, 0x38}, {0x39, 0x39}, {0x3a, 0x3a}, {0x3b, 0x3b}, {0x3c, 0x3c}, + {0x3d, 0x3d}, {0x3e, 0x3e}, {0x3f, 0x3f}, {0x40, 0x40}, {0x41, 0x41}, {0x44, 0x44}, {0x45, 0x45}, {0x46, 0x46}, + {0x47, 0x47}, {0x48, 0x48}, {0x49, 0x49}, {0x4c, 0x4c}, {0x4d, 0x4d}, {0x4e, 0x4e}, {0x4f, 0x4f}, {0x50, 0x50}, + {0x51, 0x51}, {0x54, 0x54}, {0x55, 0x55}, {0x56, 0x56}, {0x57, 0x57}, {0x58, 0x58}, {0x59, 0x59}, {0x5c, 0x5c}, + {0x5d, 0x5d}, {0x5e, 0x5e}, {0x5f, 0x5f}, {0x60, 0x60}, {0x68, 0x68}, {0x69, 0x69}, {0x6a, 0x6a}, {0x6b, 0x6b}, + {0x6c, 0x6c}, {0x6d, 0x6d}, {0x6e, 0x6e}, {0x6f, 0x6f}, +}}; + +} + +#endif diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index c3d1a68701..f28258ca1e 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -1,6 +1,7 @@ #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include #include +#include namespace ShaderRecompiler { @@ -139,6 +140,76 @@ bool IsDepthBitsTexture(std::uint32_t word1, std::uint32_t word3) { return DepthBitsTextureWidth(word1, word3) != 0u; } +const std::array& GcnBufferFormats() { + static const auto table = [] { + std::array formats{}; + struct Family { std::uint32_t data; std::uint32_t first; bool normalized; bool floating; }; + constexpr std::array normalizedFamilies{{ + {1u, 1u, true, false}, {2u, 7u, true, true}, {3u, 14u, true, false}, {5u, 23u, true, true}, {6u, 30u, true, true}, + {7u, 37u, true, true}, {8u, 44u, true, false}, {9u, 50u, true, false}, {10u, 56u, true, false}, {12u, 65u, true, true}, + }}; + for (const auto& family : normalizedFamilies) { + for (std::uint32_t number = 0; number < 6u; ++number) formats[(family.data << 3u) | number] = static_cast(family.first + number); + if (family.floating) formats[(family.data << 3u) | 7u] = static_cast(family.first + 6u); + } + constexpr std::array, 4> wideFamilies{{{4u, 20u}, {11u, 62u}, {13u, 72u}, {14u, 75u}}}; + for (const auto& [data, first] : wideFamilies) { + formats[(data << 3u) | 4u] = static_cast(first); + formats[(data << 3u) | 5u] = static_cast(first + 1u); + formats[(data << 3u) | 7u] = static_cast(first + 2u); + } + return formats; + }(); + return table; +} + +std::uint32_t GcnImageFormat(std::uint32_t dataFormat, std::uint32_t numberFormat) { + constexpr std::uint32_t Unorm = 0u, Snorm = 1u, Float = 7u, Srgb = 9u; + if (dataFormat < 16u && numberFormat < 8u) return GcnBufferFormats()[(dataFormat << 3u) | numberFormat]; + switch (dataFormat) { + case 1u: return numberFormat == Srgb ? 128u : 0u; + case 3u: return numberFormat == Srgb ? 129u : 0u; + case 10u: return numberFormat == Srgb ? 130u : 0u; + case 16u: return numberFormat == Unorm ? 133u : 0u; + case 17u: return numberFormat == Unorm ? 134u : 0u; + case 18u: return numberFormat == Unorm ? 135u : 0u; + case 19u: return numberFormat == Unorm ? 136u : 0u; + case 34u: return numberFormat == Float ? 132u : 0u; + case 35u: return numberFormat == Unorm ? 169u : numberFormat == Srgb ? 170u : 0u; + case 36u: return numberFormat == Unorm ? 171u : numberFormat == Srgb ? 172u : 0u; + case 37u: return numberFormat == Unorm ? 173u : numberFormat == Srgb ? 174u : 0u; + case 38u: return numberFormat == Unorm ? 175u : numberFormat == Snorm ? 176u : 0u; + case 39u: return numberFormat == Unorm ? 177u : numberFormat == Snorm ? 178u : 0u; + case 40u: return numberFormat == Unorm ? 179u : numberFormat == Snorm ? 180u : 0u; + case 41u: return numberFormat == Unorm ? 181u : numberFormat == Srgb ? 182u : 0u; + default: return 0u; + } +} + +std::array GcnImageWords(std::span gcn) { + const auto format = GcnImageFormat((gcn[1] >> 20u) & 0x3fu, (gcn[1] >> 26u) & 0xfu); + const auto width = gcn[2] & 0x3fffu; + const auto height = (gcn[2] >> 14u) & 0x3fffu; + const auto type = gcn[3] >> 28u; + const auto lastLevel = (gcn[3] >> 16u) & 0xfu; + const bool layered = type == 11u || type == 12u || type == 13u || type == 15u; + std::array words{}; + words[0] = gcn[0]; + words[1] = (gcn[1] & 0x000fffffu) | (format << 20u) | ((width & 3u) << 30u); + words[2] = (width >> 2u) | (height << 14u); + words[3] = (gcn[3] & 0x000fffffu) | (type << 28u); + words[4] = (layered ? (gcn[5] >> 13u) & 0x1fffu : gcn[4] & 0x1fffu) | ((gcn[5] & 0x1fffu) << 16u); + words[5] = lastLevel << 4u; + words[7] = ((gcn[3] >> 20u) & 0x1fu) | (((gcn[3] >> 25u) & 1u) << 5u) | (((gcn[4] >> 13u) & 0x3fffu) << 6u); + return words; +} + +std::uint32_t GcnBufferWord3(std::uint32_t word1, std::uint32_t word3) { + const std::uint32_t format = GcnBufferFormats()[(word3 >> 12u) & 0x7fu]; + const std::uint32_t outOfBounds = ((word1 >> 16u) & 0x3fffu) != 0u ? 1u : 3u; + return (word3 & 0xc0e00fffu) | (format << 12u) | (outOfBounds << 28u); +} + IrTextureNumericClass VertexInputNumericClass(IrBufferFormat format) { switch (format) { case IrBufferFormat::Format8UInt: diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index f64c3c6bd7..e3be618395 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp @@ -70,6 +70,7 @@ constexpr ImageOpcodeInfo imageOpcodes[] = { {0xa5u, RdnaOpcode::ImageSample, "image_sample_b_a", RdnaImageSampleFlagBias | RdnaImageSampleFlagAdjust, true, false, false}, {0xa8u, RdnaOpcode::ImageSample, "image_sample_c_a", RdnaImageSampleFlagCompare | RdnaImageSampleFlagAdjust, true, false, false}, {0xb0u, RdnaOpcode::ImageSample, "image_sample_o_a", RdnaImageSampleFlagOffset | RdnaImageSampleFlagAdjust, true, false, false}, + {0xb8u, RdnaOpcode::ImageSample, "image_sample_c_o_a", RdnaImageSampleFlagCompare | RdnaImageSampleFlagOffset | RdnaImageSampleFlagAdjust, true, false, false}, {0x3cu, RdnaOpcode::ImageSampleCLO, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagLod | RdnaImageSampleFlagOffset, true, false, false}, {0x3fu, RdnaOpcode::ImageSampleCLzO, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagLevelZero | RdnaImageSampleFlagOffset, true, false, false}, {0x3au, RdnaOpcode::ImageSampleCDO, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagDerivative | RdnaImageSampleFlagOffset, true, false, false}, diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp index abdb151c23..29cdbcc01b 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp @@ -90,6 +90,11 @@ RdnaInstructionFamily GetRdnaInstructionFamily(std::uint32_t word) { } RdnaInstruction DecodeRdnaInstruction(std::uint32_t programCounter, std::span code, std::uint32_t wordIndex) { + if (GcnGuestShaders()) return DecodeGcnInstruction(programCounter, code, wordIndex); + return DecodeRdnaEncodedInstruction(programCounter, code, wordIndex); +} + +RdnaInstruction DecodeRdnaEncodedInstruction(std::uint32_t programCounter, std::span code, std::uint32_t wordIndex) { if (wordIndex >= code.size()) { throw std::out_of_range("word index is out of the code span bounds"); } diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp index 77ea8f0322..98950f77cb 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp @@ -469,6 +469,9 @@ RdnaOperand scalarDestination(std::uint32_t code) { RdnaOperand scalarDescriptorBase(std::uint32_t reg, std::uint32_t registerCount, const char* reason) { const auto operand = scalarSource(reg); + if (registerCount == 4u && reg == 104u && operand.kind == RdnaOperandKind::ScalarRegister) { + return operand; + } if (registerCount == 2u && operand.kind == RdnaOperandKind::VccLo) { return operand; } diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index dd297de461..02ccb674b8 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -1935,7 +1935,7 @@ bool supportsNativeVop3SourceModifiers(RdnaOpcode opcode) { } bool supportsNativeVop3ResultModifiers(RdnaOpcode opcode) { - if (isVop1FloatResultOpcode(opcode) || isNativeVop3F16TernaryOpcode(opcode)) { + if (isVop1FloatResultOpcode(opcode) || isNativeVop3F16TernaryOpcode(opcode) || isCubeOpcode(opcode)) { return true; } switch (opcode) { @@ -1990,7 +1990,7 @@ bool supportsNativeVop3Clamp(RdnaOpcode opcode) { return true; } return supportsNativeVop3ResultModifiers(opcode) || usesInexactClampControl(opcode) || usesIntegerSaturateClamp(opcode) || - isVop3Integer16AddSubOpcode(opcode) || isF32DivisionStepOpcode(opcode) || isCubeOpcode(opcode); + isVop3Integer16AddSubOpcode(opcode) || isF32DivisionStepOpcode(opcode); } void checkNativeVop3Modifiers(RdnaOpcode opcode, bool permlane, bool carryInOut, bool scalarDst, std::uint32_t abs, std::uint32_t opSel, std::uint32_t clamp, std::uint32_t omod, std::uint32_t neg) { diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 1b83081ad7..dae9b85256 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1,5 +1,7 @@ #include "Recompiler.hpp" +#include "StencilReplay.hpp" #include +#include #include #include #include @@ -15,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +40,7 @@ #include "Optimization/include/Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" #include "Optimization/include/Optimization/ResourceTracker.hpp" +#include "Optimization/include/Optimization/VectorWindowStoreEliminator.hpp" #include "Optimization/include/Optimization/ShaderInfoCollector.hpp" #include "Optimization/include/Optimization/SrtWalker.hpp" #include "Optimization/include/Optimization/SsaBuilder.hpp" @@ -83,11 +87,17 @@ ShaderStageKind toShaderStageKind(ShaderStage stage) { namespace { +bool MergedHullStage(const RecompileRequest& request) { + const auto stage = request.shader.stage; + return (stage == ShaderStage::Local || stage == ShaderStage::TessellationControl) && request.graphics && request.graphics->tessellation && request.graphics->tessellation->mergedHull; +} + // The host subgroup width wave64 programs are laid out for. Debug aid: APS5_SINGLE_LANE= keeps the listed programs at one guest lane per invocation. std::uint32_t HostSubgroupSize(const RecompileRequest& request) { const bool vertexStage = request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Local; if (vertexStage && !request.target.vertexSubgroups) return 1u; + if (request.shader.stage == ShaderStage::TessellationControl && MergedHullStage(request)) return 1u; static const std::string list = [] { const char* text = std::getenv("APS5_SINGLE_LANE"); return text ? std::string(text) : std::string(); }(); if (!list.empty()) { if (list == "all") return 64u; @@ -98,20 +108,45 @@ std::uint32_t HostSubgroupSize(const RecompileRequest& request) { return request.target.subgroupSize; } -RdnaProgram DecodeStageProgram(ShaderStageKind stage, std::span code) { +RdnaProgram DecodeStageProgram(ShaderStageKind stage, std::span code, bool mergedHull) { + static constexpr std::array EndProgram{0xbf810000u}; + if (stage == ShaderStageKind::Local && mergedHull) { + RdnaProgram program; + program.code = EndProgram; + program.instructions.push_back(DecodeRdnaInstruction(0u, EndProgram, 0u)); + return program; + } if (stage != ShaderStageKind::Local) return RdnaInstructionDecoder{}.Decode(code); auto program = DecodeRdnaFrontProgram(code); - static constexpr std::array EndProgram{0xbf810000u}; program.instructions.back() = DecodeRdnaInstruction(program.instructions.back().programCounter, EndProgram, 0u); return program; } ShaderStageInputInfo RequestInputInfo(const RecompileRequest& request) { const auto* mesh = request.graphics && request.graphics->mesh ? &*request.graphics->mesh : nullptr; - const auto* tessellation = request.graphics && request.graphics->tessellation ? &*request.graphics->tessellation : nullptr; + std::optional tessellationCopy; + if (request.graphics && request.graphics->tessellation) { + tessellationCopy = *request.graphics->tessellation; + tessellationCopy->instancePatches = TessellationInstancePatches(request); + const auto& capabilities = request.target.supportedCapabilities; + tessellationCopy->drawParameters = std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityDrawParameters)) != capabilities.end(); + } + const auto* tessellation = tessellationCopy ? &*tessellationCopy : nullptr; return BuildShaderStageInputInfo(toShaderStageKind(request.shader.stage), request.context, HostSubgroupSize(request), mesh, tessellation); } +void ValidateRequestInputs(const RecompileRequest& request) { + const auto stage = toShaderStageKind(request.shader.stage); + const bool vertexFamily = stage == ShaderStageKind::Vertex || stage == ShaderStageKind::Local || stage == ShaderStageKind::TessellationControl || stage == ShaderStageKind::TessellationEvaluation || stage == ShaderStageKind::Mesh; + if (!vertexFamily) { + static_cast(RequestInputInfo(request)); + return; + } + const auto* mesh = request.graphics && request.graphics->mesh ? &*request.graphics->mesh : nullptr; + const auto* tessellation = request.graphics && request.graphics->tessellation ? &*request.graphics->tessellation : nullptr; + ValidateVertexStageInputs(stage, request.context, mesh, tessellation); +} + std::uint32_t DeviceMemoryLdsBytes(const RecompileRequest& request) { if (!request.context.compute.has_value()) return 0u; const auto bytes = static_cast(request.context.compute->ldsSizeDwords) * 4u; @@ -124,8 +159,8 @@ std::atomic frontEndBuilds{0}; struct PreparedControlFlow { PreparedControlFlow(const RecompileRequest& request, const SwappcInfo& swappcInfo) - : stage(request.shader.stage), swappc(swappcInfo), code(request.shader.code.begin(), request.shader.code.end()), - decoded(DecodeStageProgram(toShaderStageKind(request.shader.stage), code)), cfg(GraphBuilder{}.Build(decoded, &swappc)) { + : stage(request.shader.stage), mergedHull(MergedHullStage(request)), fallbacks(swappcInfo.linkedCallFallbacks.begin(), swappcInfo.linkedCallFallbacks.end()), swappc(WithFallbacks(swappcInfo, fallbacks)), code(request.shader.code.begin(), request.shader.code.end()), + decoded(DecodeStageProgram(toShaderStageKind(request.shader.stage), code, mergedHull)), cfg(GraphBuilder{}.Build(decoded, &swappc)) { Structurizer{}.Structurize(cfg); } @@ -133,12 +168,19 @@ struct PreparedControlFlow { PreparedControlFlow& operator=(const PreparedControlFlow&) = delete; bool Matches(const RecompileRequest& request, const SwappcInfo& swappcInfo) const { - return stage == request.shader.stage && swappc.fetchCallAllowed == swappcInfo.fetchCallAllowed && + return stage == request.shader.stage && mergedHull == MergedHullStage(request) && swappc.fetchCallAllowed == swappcInfo.fetchCallAllowed && swappc.userDataBaseRegister == swappcInfo.userDataBaseRegister && swappc.userDataCount == swappcInfo.userDataCount && - std::ranges::equal(code, request.shader.code); + std::ranges::equal(fallbacks, swappcInfo.linkedCallFallbacks) && std::ranges::equal(code, request.shader.code); + } + + static SwappcInfo WithFallbacks(SwappcInfo info, const std::vector& fallbacks) { + info.linkedCallFallbacks = fallbacks; + return info; } ShaderStage stage; + bool mergedHull; + std::vector fallbacks; SwappcInfo swappc; std::vector code; RdnaProgram decoded; @@ -147,7 +189,7 @@ struct PreparedControlFlow { std::shared_ptr ShaderPreparationContext::AcquireFrontend(const RecompileRequest& request) { const auto inputInfo = RequestInputInfo(request); - const SwappcInfo swappc{inputInfo.vertex != nullptr, request.context.userDataBaseRegister, static_cast(request.context.userData.size())}; + const SwappcInfo swappc{inputInfo.vertex != nullptr, request.context.userDataBaseRegister, static_cast(request.context.userData.size()), 0u, request.context.linkedCallFallbacks}; static const bool reuse = std::getenv("APS5_NO_PERF_FRONTEND_PAIR") == nullptr; if (reuse && frontend != nullptr && frontend->Matches(request, swappc)) { return frontend; @@ -189,19 +231,23 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat translateOptions.shaderHash = request.shader.codeAddress; translateOptions.waveSize = request.context.waveSize; translateOptions.userDataBaseRegister = request.context.userDataBaseRegister; + translateOptions.gcnLegacyVertexInputs = GcnGuestShaders() && request.context.userDataBaseRegister == 0u; translateOptions.userDataCount = static_cast(request.context.userData.size()); translateOptions.scratchDwords = request.context.compute.has_value() ? request.context.compute->scratchDwords : 0u; translateOptions.sharedMemoryBytes = DeviceMemoryLdsBytes(request); translateOptions.fragmentShaderBarycentricEnabled = request.target.fragmentShaderBarycentricEnabled; const auto supports = [&](spv::Capability capability) { return std::ranges::find(request.target.supportedCapabilities, static_cast(capability)) != request.target.supportedCapabilities.end(); }; translateOptions.nativeFloat16 = supports(spv::CapabilityFloat16) && supports(spv::CapabilityRoundingModeRTE) && supports(spv::CapabilityDenormPreserve); + translateOptions.interpolationFunctions = supports(spv::CapabilityInterpolationFunction); translateOptions.floatMode = request.context.floatMode; + translateOptions.float32FlushToZero = request.target.float32FlushToZero; translateOptions.inputInfo = inputInfo; constexpr InstructionTranslator translator; + if (frontend->mergedHull && cfg.hasFetchCall) throw std::runtime_error("a merged hull program that calls a fetch subroutine is unsupported"); EmbeddedFetchPlan embeddedFetch; - if ((stageKind == ShaderStageKind::Vertex || stageKind == ShaderStageKind::Local) && inputInfo.vertex != nullptr && inputInfo.vertex->fetchEmbedded) { + if ((stageKind == ShaderStageKind::Vertex || stageKind == ShaderStageKind::Local) && !frontend->mergedHull && inputInfo.vertex != nullptr && inputInfo.vertex->fetchEmbedded) { embeddedFetch = EmbeddedVertexFetchAnalyzer{}.Analyze(decoded, inputInfo.vertex->fetchAttribReg, inputInfo.vertex->fetchBufferReg, request.context.userDataBaseRegister, static_cast(request.context.userData.size()), request.context.waveSize); translateOptions.embeddedFetch = &embeddedFetch; } @@ -221,6 +267,7 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat constexpr DeadCodeEliminator deadCodeEliminator; constexpr ReadLaneEliminator readLaneEliminator; constexpr MaskedSelectEliminator maskedSelectEliminator; + constexpr VectorWindowStoreEliminator vectorWindowStoreEliminator; const auto simplify = [&] { dumpIr("translate"); ssaBuilder.Rewrite(program); @@ -240,6 +287,10 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat deadCodeEliminator.Eliminate(program); } + if (vectorWindowStoreEliminator.Eliminate(program).removedStores != 0u) { + deadCodeEliminator.Eliminate(program); + } + if (maskedSelectEliminator.Eliminate(program).removedSelects != 0u) { const bool liveAncillary = std::ranges::any_of(program.Blocks(), [](const auto& block) { return std::ranges::any_of(block->Instructions(), [](const IrValue* value) { @@ -272,7 +323,7 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat if (inputInfo.vertex != nullptr) { constexpr TessellationLowering tessellationLowering; - tessellationLowering.Lower(program, inputInfo.vertex->tess); + tessellationLowering.Lower(program, inputInfo.vertex->tess, request.target.tessellation ? &*request.target.tessellation : nullptr); } constexpr SrtWalker srtWalker; @@ -285,6 +336,9 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat deadCodeEliminator.Eliminate(program); dumpIr("resources"); program.Resources().srgbDecodeFormats = request.target.srgbDecodeFormats; + if (request.context.convertedSamples) { + for (auto& image : program.Resources().info.images) image.convertedSamples = true; + } return program; } @@ -293,9 +347,10 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat // shared immutable object every later capture that reproduces the snapshot receives, so Populate // and the per-request copy run once per distinct snapshot. struct ResultMemoEntry { - std::uint64_t variantId; - std::uint64_t hash; + std::uint64_t variantId = 0; + std::uint64_t hash = 0; std::shared_ptr result; + std::uint64_t used = 0; }; struct EmissionFailure { @@ -304,13 +359,19 @@ struct EmissionFailure { std::exception_ptr failure; }; +constexpr std::size_t ResultMemoSets = 64; +constexpr std::size_t ResultMemoWays = 4; +using ResultMemoTable = std::array; + struct SourceEntry : std::enable_shared_from_this { std::mutex mutex; // The code the entry was built for: the key carries only a hash of it, so a candidate entry is // accepted only when its code matches word for word. Owned here because the request's span // points into a registration the driver may replace while the entry lives on. std::vector code; + std::vector linkedCallFallbacks; std::shared_ptr plan; + bool runtimeCaptured = false; // A plan build that threw (an unsupported resource chain or control flow) is remembered and // rethrown: the front end ran every pass before failing, ~13 ms per dispatch of a shader the // title issues every frame (0x1048947300 at the intro video). APS5_NO_FAILURE_MEMO=1 rebuilds. @@ -318,9 +379,8 @@ struct SourceEntry : std::enable_shared_from_this { std::unique_ptr program; std::vector> variants; std::vector emissionFailures; - // The result memo, most recently used first, at most ResultMemoEntries (under mutex). - std::list memo; - std::unordered_map::iterator> memoIndex; + std::unique_ptr memo; + std::uint64_t memoClock = 0; }; namespace { @@ -381,7 +441,9 @@ bool diskPlans() { return enabled && ShaderDiskCache::Enabled() && !DebugProbeActive(); } -std::shared_ptr getSource(const RecompileRequest& request, ShaderPreparationContext* preparation = nullptr, bool reclaim = true) { +bool sameLayout(const BindingLayout& left, const BindingLayout& right); + +std::shared_ptr getSource(const RecompileRequest& request, ShaderPreparationContext* preparation = nullptr, bool reclaim = true, const SourceHandle* preparedHandle = nullptr) { static PrimedSharedMutex mutex; // Entries whose code hashes alike share a bucket; the code comparison picks the right one. static std::unordered_map, std::vector>, SourceKeyHash> sources; @@ -408,6 +470,7 @@ std::shared_ptr getSource(const RecompileRequest& request, ShaderPr if (source == nullptr) { source = std::make_shared(); source->code.assign(request.shader.code.begin(), request.shader.code.end()); + source->linkedCallFallbacks.assign(request.context.linkedCallFallbacks.begin(), request.context.linkedCallFallbacks.end()); auto& bucket = sources[key]; if (!bucket.empty()) { // A second entry under one key is a code hash collision (or the unhashed key with @@ -423,6 +486,8 @@ std::shared_ptr getSource(const RecompileRequest& request, ShaderPr { std::lock_guard lock(source->mutex); if (source->plan == nullptr) { + const bool hasPreparedArtifact = preparedHandle != nullptr && preparedHandle->artifact != nullptr; + if (hasPreparedArtifact && (preparedHandle->source != source || !MatchesPreparedShader(request, *preparedHandle))) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the resource plan source"); if (FailureMemo() && source->planFailure) std::rethrow_exception(source->planFailure); const bool disk = diskPlans(); std::vector planKey; @@ -438,7 +503,9 @@ std::shared_ptr getSource(const RecompileRequest& request, ShaderPr try { ResourceProgram resource(request, preparation, reclaim); source->plan = std::move(resource.plan); - source->program = std::move(resource.program); + if (!hasPreparedArtifact && std::ranges::none_of(source->variants, [&](const auto& variant) { return sameLayout(variant->layout, request.layout); })) { + source->program = std::move(resource.program); + } } catch (...) { if (FailureMemo()) source->planFailure = std::current_exception(); throw; @@ -450,6 +517,25 @@ std::shared_ptr getSource(const RecompileRequest& request, ShaderPr return source; } +std::shared_ptr acquirePlan(const RecompileRequest& request, const std::shared_ptr& source, bool runtimeCapture, const SourceHandle* preparedHandle = nullptr) { + for (;;) { + { + std::lock_guard lock(source->mutex); + if (source->plan != nullptr) { + if (runtimeCapture) source->runtimeCaptured = true; + return source->plan; + } + } + if (getSource(request, nullptr, true, preparedHandle) != source) throw std::runtime_error("ShaderRecompiler: resource plan source changed while reloading"); + } +} + +void releaseUndrawnPlan(const SourceHandle& handle) { + if (handle.source == nullptr || !diskPlans()) return; + std::lock_guard lock(handle.source->mutex); + if (!handle.source->runtimeCaptured) handle.source->plan.reset(); +} + std::array partialThreads(const RecompileRequest& request) { return request.context.compute ? request.context.compute->partialThreads : std::array{}; } @@ -478,6 +564,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)); @@ -492,7 +584,8 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) constexpr BindingAllocator bindingAllocator; bool registration = request.target.registrationModules && (request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Fragment); program.Metadata().bindingLayoutComplete = false; - auto bindings = bindingAllocator.Allocate(program, request.layout, registration); + const bool stencilReplay = request.shader.stage == ShaderStage::Fragment && std::ranges::any_of(program.Info().outputs, [](const StageOutput& output) { return output.kind == StageOutputKind::StencilRef; }) && std::ranges::find(request.target.supportedCapabilities, static_cast(spv::CapabilityStencilExportEXT)) == request.target.supportedCapabilities.end(); + auto bindings = bindingAllocator.Allocate(program, request.layout, registration, stencilReplay); SpirvTargetOptions targetOptions {}; targetOptions.vulkanVersion = request.target.vulkanVersion; @@ -504,32 +597,82 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.nonConstantImageOffsets = request.target.nonConstantImageOffsets; targetOptions.narrowSubgroupClock = request.target.narrowSubgroupClock; targetOptions.float64DenormPreserve = request.target.float64DenormPreserve; + targetOptions.depthRangeRemap = request.target.depthRangeRemap; targetOptions.registrationModules = registration; targetOptions.fragmentDescriptorSet = request.target.registrationModules; targetOptions.float16DenormPreserve = request.target.float16DenormPreserve; + targetOptions.spannedWaves = request.target.spannedWaves; + targetOptions.robustBufferAccess2 = request.target.robustBufferAccess2; + targetOptions.float32FlushToZero = request.target.float32FlushToZero; + targetOptions.float32DenormPreserve = request.target.float32DenormPreserve; + targetOptions.meshBarycentricPixel = request.shader.stage == ShaderStage::Fragment && request.graphics && request.graphics->mesh; + if (request.shader.stage == ShaderStage::Mesh && request.graphics && request.graphics->meshBarycentrics) { + const auto& paired = *request.graphics->meshBarycentrics; + const auto emulated = DescribeEmulatedInterface(paired.parameters, paired.emulation, true); + const auto& layout = emulated.layout; + targetOptions.meshBarycentricVertex = true; + targetOptions.barycentricSmoothLocation = layout.smoothLocation; + targetOptions.barycentricLinearLocation = layout.linearLocation; + targetOptions.barycentricPositionsLocation = layout.positionsLocation; + targetOptions.meshExportedLocations = emulated.passed; + for (const auto& parameter : emulated.perVertex) targetOptions.meshPerVertexLocations.emplace_back(parameter.sourceLocation, parameter.location); + if (!request.graphics->mesh || !request.target.mesh) throw std::runtime_error("barycentric emulation on the mesh path needs a mesh configuration and a device with mesh shaders"); + const auto& config = *request.graphics->mesh; + const auto& limits = *request.target.mesh; + if (config.provokingVertex != 0u) throw std::runtime_error("barycentric emulation on the mesh path is only measured with the first provoking vertex"); + const std::uint64_t perVertexParameters = emulated.perVertex.size(); + const std::uint64_t sharedBytes = 16ull * config.maxVertices * (1u + perVertexParameters) + DeviceMemoryLdsBytes(request); + if (sharedBytes > limits.maxSharedMemoryBytes) throw std::runtime_error("barycentric emulation on the mesh path needs " + std::to_string(sharedBytes) + " bytes of mesh shared memory for " + std::to_string(config.maxVertices) + " vertices, the device allows " + std::to_string(limits.maxSharedMemoryBytes)); + MeshOutputUse use; + std::set flatLocations; + for (const auto& output : program.Info().outputs) { + if (output.kind == StageOutputKind::Parameter && std::ranges::find(targetOptions.meshExportedLocations, output.location) != targetOptions.meshExportedLocations.end()) flatLocations.insert(output.location); + if (output.kind == StageOutputKind::ClipDistance) use.clipDistances = std::max(use.clipDistances, output.index + 1u); + if (output.kind == StageOutputKind::CullDistance) use.cullDistances = std::max(use.cullDistances, output.index + 1u); + if (output.kind == StageOutputKind::Layer) use.layer = true; + } + use.flatParameters = static_cast(flatLocations.size()); + use.perVertexParameters = static_cast(perVertexParameters); + use.clipVaryings = emulated.barycentrics; + const auto budget = MeshBarycentricOutputBudget(config, limits, use); + if (budget.components > limits.maxOutputComponents) throw std::runtime_error("barycentric emulation on the mesh path needs " + std::to_string(budget.components) + " mesh output components (" + std::to_string(budget.vertexLocations) + " per-vertex and " + std::to_string(budget.primitiveLocations) + " per-primitive locations), the device allows " + std::to_string(limits.maxOutputComponents)); + if (budget.bytes > limits.maxOutputMemoryBytes) throw std::runtime_error("barycentric emulation on the mesh path needs " + std::to_string(budget.bytes) + " bytes of mesh output memory, the device allows " + std::to_string(limits.maxOutputMemoryBytes)); + } + targetOptions.waveLdsMemoryBarriers = request.target.waveLdsMemoryBarriers; constexpr SpirvEmitter spirvEmitter; CompiledShaderArtifact result; 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; - bindings = bindingAllocator.Allocate(program, request.layout, false); + bindings = bindingAllocator.Allocate(program, request.layout, false, stencilReplay); targetOptions.registrationModules = false; result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); } result.registrationModule = registration; + if (request.shader.stage == ShaderStage::Mesh && request.graphics) result.meshBarycentrics = request.graphics->meshBarycentrics; + result.stencilReplaySafe = (request.shader.stage == ShaderStage::Vertex || stencilReplay) && StencilReplaySafe(program); + if (stencilReplay) { + result.stencilReferences = StencilReferenceValues(program, request.context.pixel.has_value() && request.context.pixel->stencilOperationValueExport ? 8u : 0u); + result.stencilReferenceDword = bindings.layout.stencilReferenceDword; + } result.bdaAbiVersion = program.Info().usesDma || program.Info().usesFaultBuffer ? request.target.bdaAbiVersion : 0u; result.memoryOffsetDword = bindings.layout.memoryOffsetDword; result.shaderDataDwords = bindings.layout.ShaderDataDwords(); result.imageMetadataDword = bindings.layout.ImageMetadataDword(); result.runtimeImageCount = bindings.layout.runtimeImageCount; - for (std::uint32_t index = 0; index < program.Info().images.size(); ++index) { - if (program.Info().images[index].indirectRoot != ImageResource::NoIndirectImage) result.runtimeImageResources.push_back(index); - } result.hostSubgroupSize = HostSubgroupSize(request); + result.fullSubgroups = emitted.fullSubgroups; result.vertexOffsetSgpr = program.Info().vertexOffsetSgpr; result.instanceOffsetSgpr = program.Info().instanceOffsetSgpr; result.vertexOffsetShared = program.Info().vertexOffsetShared; @@ -601,16 +744,32 @@ std::shared_ptr buildSpecializedModule(const CompiledSh if (!supplied.emplace(constant.id, constant.value).second) throw std::runtime_error("duplicate prepared specialization ID"); } std::map values; + std::set retained; std::set specializedIds; + std::set pushTypes; + std::optional replaySlotBytes; const auto& words = source.Words(); if (words.size() < 5u || words[0] != spv::MagicNumber) throw std::runtime_error("invalid prepared specialization module"); for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; const auto op = static_cast(words[cursor] & 0xffffu); if (count == 0u || count > words.size() - cursor) throw std::runtime_error("truncated prepared specialization instruction"); + if (!artifact.stencilReferences.empty() && op == spv::OpTypePointer && count == 4u && words[cursor + 2u] == spv::StorageClassPushConstant) pushTypes.insert(words[cursor + 3u]); if (op == spv::OpDecorate && count == 4u && words[cursor + 2u] == spv::DecorationSpecId) { + if (PipelineSpecialization::IsWorkgroupOffset(words[cursor + 3u])) { + if (supplied.contains(words[cursor + 3u])) throw std::runtime_error("a workgroup offset is supplied at dispatch, not prepared"); + if (!retained.insert(words[cursor + 1u]).second) throw std::runtime_error("duplicate workgroup offset specialization"); + cursor += count; + continue; + } const auto found = supplied.find(words[cursor + 3u]); - if (found == supplied.end() || !values.emplace(words[cursor + 1u], found->second).second) throw std::runtime_error("missing or duplicate prepared specialization value"); + if (found == supplied.end()) throw std::runtime_error("missing prepared specialization value"); + auto value = found->second; + if (!artifact.stencilReferences.empty()) { + if (found->first == PipelineSpecialization::StencilReferenceOffset) replaySlotBytes = value / PushData::DwordCount * NativePushSlotSize; + if (found->first == PipelineSpecialization::StencilReferenceOffset || found->first == PipelineSpecialization::PushDataOffset) value %= PushData::DwordCount; + } + if (!values.emplace(words[cursor + 1u], value).second) throw std::runtime_error("duplicate prepared specialization value"); specializedIds.insert(found->first); } cursor += count; @@ -619,9 +778,17 @@ std::shared_ptr buildSpecializedModule(const CompiledSh for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; const auto op = static_cast(words[cursor] & 0xffffu); - if (op == spv::OpSpecConstant) { + if (op == spv::OpSpecConstant && count == 4u && retained.contains(words[cursor + 2u])) { + materialized.insert(materialized.end(), words.begin() + cursor, words.begin() + cursor + count); + } else if (op == spv::OpDecorate && count == 4u && words[cursor + 2u] == spv::DecorationSpecId && retained.contains(words[cursor + 1u])) { + materialized.insert(materialized.end(), words.begin() + cursor, words.begin() + cursor + count); + } else if (op == spv::OpSpecConstant) { if (count != 4u || !values.contains(words[cursor + 2u])) throw std::runtime_error("invalid prepared specialization constant"); materialized.insert(materialized.end(), {(4u << 16u) | spv::OpConstant, words[cursor + 1u], words[cursor + 2u], values.at(words[cursor + 2u])}); + } else if (op == spv::OpMemberDecorate && count == 5u && words[cursor + 3u] == spv::DecorationOffset && pushTypes.contains(words[cursor + 1u])) { + if (!replaySlotBytes || words[cursor + 2u] != 0u || *replaySlotBytes >= NativePushConstantSize) throw std::runtime_error("invalid stencil replay push constant slot"); + materialized.insert(materialized.end(), words.begin() + cursor, words.begin() + cursor + count - 1u); + materialized.push_back(*replaySlotBytes); } else if (!(op == spv::OpDecorate && count == 4u && words[cursor + 2u] == spv::DecorationSpecId)) { materialized.insert(materialized.end(), words.begin() + cursor, words.begin() + cursor + count); } @@ -833,11 +1000,20 @@ 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, + .meshBarycentrics = artifact.meshBarycentrics, + .stencilReferences = artifact.stencilReferences, + .stencilReplaySafe = artifact.stencilReplaySafe, + .stencilReferenceDword = artifact.stencilReferenceDword, .variantId = artifact.variantId }; + for (const auto reg : variant.bindings.layout.userDataRegisters) { + const auto index = reg - request.context.userDataBaseRegister; + result.userDataRegisterReads |= reg >= request.context.userDataBaseRegister && index < 64u ? std::uint64_t{1} << index : ~std::uint64_t{0}; + } const auto plan = preparedBindingPlan(variant, snapshot, request.context.pixel ? std::span(request.context.pixel->targetExportMapping) : std::span{}, request.target.registrationModules); const DescriptorBindingPlan* bindingPlan = &plan->bindings; std::shared_ptr entry; @@ -845,12 +1021,21 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil auto& moduleKey = HostThreadLocal, LocalModuleKeyStorage>(); moduleKey.clear(); if (variant.bindings.layout.UsesPushData()) moduleKey.push_back(request.layout.pushConstantOffsetBytes / 4u); + if (!artifact.stencilReferences.empty()) moduleKey.push_back(request.layout.pushConstantOffsetBytes / 4u + artifact.stencilReferenceDword); if (request.context.pixel) { for (const auto packing : request.context.pixel->targetExportPacking) moduleKey.push_back(static_cast(packing)); moduleKey.push_back(request.context.pixel->dualSourceBlend ? 1u : 0u); + moduleKey.push_back(request.context.pixel->reversedBlend); if (variant.info.info.rasterPrimitive) moduleKey.push_back(static_cast(request.context.pixel->rasterPrimitive)); if (variant.info.info.layerExported) moduleKey.push_back(request.context.pixel->layerExported ? 1u : 0u); } + const bool depthRemap = request.target.depthRangeRemap && request.shader.stage == ShaderStage::Vertex && request.context.vertex.has_value(); + if (depthRemap) { + if (request.context.vertex->depthRemapMode != 0u && request.layout.pushConstantOffsetBytes % (RuntimeAbi::PushConstantDwords * 4u) + request.layout.pushConstantSizeBytes > DepthRemapPushOffsetBytes) { + throw std::runtime_error("a remapped vertex shader's push data must end before the depth remap words"); + } + moduleKey.push_back(request.context.vertex->depthRemapMode); + } result.vertexAttributes.reserve(result.vertexInputs.size()); std::array vertexClasses{}; std::array runtimeSelectors{}; @@ -879,7 +1064,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (artifact.registrationModule) runtimeSelectors.at(input.location / RuntimeAbi::VertexSelectorsPerDword) |= (selectors & 0xfffu) << (input.location % RuntimeAbi::VertexSelectorsPerDword * 16u); } } - if (!plan->bindings.specialization.empty() || variant.bindings.layout.UsesPushData() || !artifact.vertexInputPatches.empty() || request.context.pixel) { + if (!plan->bindings.specialization.empty() || variant.bindings.layout.UsesPushData() || !artifact.vertexInputPatches.empty() || request.context.pixel || depthRemap) { { std::shared_lock lock(plan->mutex); if (const auto found = plan->modules.find(moduleKey); found != plan->modules.end()) entry = found->second; @@ -893,12 +1078,17 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil auto constants = plan->bindings.specialization; std::size_t index = 0; if (variant.bindings.layout.UsesPushData()) constants.push_back({PipelineSpecialization::PushDataOffset, moduleKey[index++]}); + if (!artifact.stencilReferences.empty()) constants.push_back({PipelineSpecialization::StencilReferenceOffset, moduleKey[index++]}); if (request.context.pixel) { for (std::uint32_t target = 0; target < request.context.pixel->targetExportPacking.size(); ++target) constants.push_back({PipelineSpecialization::ExportPackingBase + target, moduleKey[index++]}); constants.push_back({PipelineSpecialization::DualSourceBlend, moduleKey[index++]}); + constants.push_back({PipelineSpecialization::ReversedBlend, moduleKey[index++]}); if (variant.info.info.rasterPrimitive) constants.push_back({PipelineSpecialization::RasterPrimitive, moduleKey[index++]}); if (variant.info.info.layerExported) constants.push_back({PipelineSpecialization::LayerExported, moduleKey[index++]}); } + if (depthRemap) { + for (std::uint32_t word = 0; word < PipelineSpecialization::DepthRemapWords; ++word) constants.push_back({PipelineSpecialization::DepthRemapBase + word, moduleKey[index++]}); + } if (!artifact.vertexInputPatches.empty()) { for (const auto& input : result.vertexInputs) { const auto selectors = moduleKey[index++]; @@ -930,11 +1120,21 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil } else { result.spirv = artifact.spirv; } + const auto resolved = ResourceMaterializer::ResolveImageTablesShared(variant.info.info, snapshot, artifact.variantId); + const auto& tables = *resolved; BindingAllocationResult bindings; - DescriptorBindingBuilder{}.Populate(bindings, variant.bindings, *bindingPlan, variant.info.userDataBase, snapshot, partialThreads(request)); + DescriptorBindingBuilder{}.Populate(bindings, variant.bindings, *bindingPlan, variant.info.userDataBase, snapshot, partialThreads(request), &tables); result.workgroupMemoryDwords = WorkgroupMemoryStrideDwords(variant.info.info); result.bindings = std::move(bindings.bindings); result.pushConstants = std::move(bindings.pushConstants); + result.readsDepthRemapPush = depthRemap; + if (request.shader.stage == ShaderStage::Compute) { + for (std::uint32_t component = 0; component < PipelineSpecialization::WorkgroupOffsetWords; ++component) result.specialization.push_back({PipelineSpecialization::WorkgroupOffsetBase + component, 0u}); + } + if (!artifact.stencilReferences.empty()) { + if (artifact.stencilReferenceDword * sizeof(std::uint32_t) != result.pushConstants.size() || result.pushConstants.size() + sizeof(std::uint32_t) > request.layout.pushConstantSizeBytes) throw std::runtime_error("stencil replay push constant lies outside the stage range"); + result.pushConstants.resize(result.pushConstants.size() + sizeof(std::uint32_t)); + } const auto& layout = variant.bindings.layout; if (artifact.registrationModule && request.shader.stage != ShaderStage::Fragment && layout.runtimeStateDwords != 0u) { const auto first = layout.RuntimeStateDword() + RuntimeAbi::StateVertexSelectors; @@ -948,7 +1148,17 @@ 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()); + for (const auto& poison : tables.poison) result.imageTablePoison.push_back({poison.words, poison.dwordCount, poison.resource, static_cast(poison.reason)}); + result.imageTableRanges = snapshot.tables.ranges; + result.imageTableReadRanges.reserve(snapshot.tables.tables.size()); + for (const auto& table : snapshot.tables.tables) result.imageTableReadRanges.push_back(table.readRanges.Copy()); + result.imageTableShader = snapshot.tables.shader; + result.imageTableFaults = tables.faults; + } return result; } @@ -958,6 +1168,26 @@ bool sameLayout(const BindingLayout& left, const BindingLayout& right) { } +MeshOutputBudget MeshBarycentricOutputBudget(const MeshConfiguration& mesh, const MeshTargetLimits& limits, const MeshOutputUse& use) { + const auto alignUp = [](std::uint64_t value, std::uint64_t granularity) { return granularity <= 1u ? value : (value + granularity - 1u) / granularity * granularity; }; + MeshOutputBudget budget; + budget.vertexLocations = 1u + use.flatParameters + use.clipVaryings + (use.clipDistances + 3u) / 4u + (use.cullDistances + 3u) / 4u; + budget.primitiveLocations = 3u + 3u * use.perVertexParameters + 1u + (use.layer ? 1u : 0u); + budget.components = 4ull * (budget.vertexLocations + budget.primitiveLocations); + budget.bytes = 16ull * (budget.vertexLocations * alignUp(mesh.maxVertices, limits.outputPerVertexGranularity) + budget.primitiveLocations * alignUp(mesh.maxPrimitives, limits.outputPerPrimitiveGranularity)); + return budget; +} + +std::optional MeshBarycentricLayoutOf(const RecompileResult& fragment) { + if (!fragment.barycentricEmulation.active) return std::nullopt; + MeshBarycentricLayout layout; + layout.emulation = fragment.barycentricEmulation; + for (const auto& parameter : fragment.fragmentParameters) { + layout.parameters.push_back({parameter.location, parameter.sourceLocation, false, parameter.perVertex}); + } + return layout; +} + struct PreparedInterface { static CompiledVariant Compile(SourceEntry& source, const RecompileRequest& request, ShaderPreparationContext& preparation, std::exception_ptr* emissionFailure) { if (preparation.interfaceSource.get() != &source || preparation.interfaceProgram->Resources().shaderHash != request.shader.codeAddress) { @@ -1039,7 +1269,7 @@ RecompileResult materializeVariant(SourceEntry& source, const RecompileRequest& return result; } -RecompileResult RecompileImpl(const RecompileRequest& request) { +RecompileResult recompileOnce(const RecompileRequest& request) { static_cast(RequestInputInfo(request)); RequestMemoryView memory(request.context.memory); const auto runtime = memory.MakeRuntime(request.context.userData, request.shader.codeAddress); @@ -1053,17 +1283,31 @@ RecompileResult RecompileImpl(const RecompileRequest& request) { return materializeResult(variant, request, snapshot); } const auto source = getSource(request); - materializer.Materialize(*source->plan, runtime, snapshot); + materializer.Materialize(*acquirePlan(request, source, true), runtime, snapshot); return materializeVariant(*source, request, snapshot); } +RecompileResult RecompileImpl(const RecompileRequest& request) { + try { + return recompileOnce(request); + } catch (const ConvertedSampleRequired&) { + if (request.context.convertedSamples) throw; + } + auto converted = request; + converted.context.convertedSamples = true; + return recompileOnce(converted); +} + // APS5_NO_RESULT_MEMO=1: every Recompile(request, capture) materializes its own result as before. bool ResultMemo() { static const bool resultMemo = std::getenv("APS5_NO_RESULT_MEMO") == nullptr; return resultMemo; } -constexpr std::size_t ResultMemoEntries = 256; +ResultMemoEntry* memoSet(SourceEntry& source, std::uint64_t index) { + if (source.memo == nullptr) return nullptr; + return source.memo->data() + static_cast((index ^ (index >> 32u)) % ResultMemoSets) * ResultMemoWays; +} struct ResultMemoCounters { std::atomic hits{0}, misses{0}, evictions{0}, populateNanoseconds{0}; @@ -1110,6 +1354,7 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh mix(values.size()); for (const auto& value : values) { mix(value.dwordCount); + mix(value.availableDwordMask); for (std::uint32_t i = 0; i < value.dwordCount && i < value.dwords.size(); ++i) mix(value.dwords[i]); } }; @@ -1123,12 +1368,32 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh for (const auto stride : snapshot.uniformFill.groupStride) mix(stride); mix(snapshot.uniformFill.words); mix(snapshot.uniformFill.value); + const auto& tables = snapshot.tables; + mix(tables.shader); + mix(tables.tables.size()); + for (const auto& table : tables.tables) { + mix(table.base); + mix(table.size); + mix(table.records); + mix(table.keys); + mix(table.fault); + mix(table.outside ? 1u : 0u); + mix(table.codes.Hash()); + mix(table.words.Hash()); + mix(table.readRanges.Hash()); + } + mix(tables.ranges.size()); + for (const auto& [base, size] : tables.ranges) { + mix(base); + mix(size); + } for (const auto threads : partialThreads(request)) mix(threads); mix(request.layout.pushConstantOffsetBytes); if (request.context.pixel) { for (const auto mapping : request.context.pixel->targetExportMapping) mix(mapping); for (const auto packing : request.context.pixel->targetExportPacking) mix(static_cast(packing)); mix(request.context.pixel->dualSourceBlend); + mix(request.context.pixel->reversedBlend); mix(static_cast(request.context.pixel->rasterPrimitive)); mix(request.context.pixel->layerExported); } @@ -1157,13 +1422,16 @@ std::shared_ptr materializePreparedMemoized(SourceEntry& { std::lock_guard lock(source.mutex); index = (variant->artifact.variantId * 0x9e3779b97f4a7c15ull) ^ hash; - const auto found = source.memoIndex.find(index); - if (found != source.memoIndex.end() && found->second->variantId == variant->artifact.variantId && found->second->hash == hash) { - source.memo.splice(source.memo.begin(), source.memo, found->second); - counters.hits.fetch_add(1, std::memory_order_relaxed); - if (memoHit != nullptr) *memoHit = true; - reportResultMemo(); - return found->second->result; + if (auto* set = memoSet(source, index)) { + for (std::size_t way = 0; way < ResultMemoWays; ++way) { + auto& entry = set[way]; + if (entry.result == nullptr || entry.variantId != variant->artifact.variantId || entry.hash != hash) continue; + entry.used = ++source.memoClock; + counters.hits.fetch_add(1, std::memory_order_relaxed); + if (memoHit != nullptr) *memoHit = true; + reportResultMemo(); + return entry.result; + } } } const auto started = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; @@ -1174,25 +1442,23 @@ std::shared_ptr materializePreparedMemoized(SourceEntry& std::shared_ptr shared = std::move(result); { std::lock_guard lock(source.mutex); - const auto found = source.memoIndex.find(index); - if (found != source.memoIndex.end()) { - if (found->second->variantId == variant->artifact.variantId && found->second->hash == hash) { - source.memo.splice(source.memo.begin(), source.memo, found->second); - shared = found->second->result; - } else { - source.memo.erase(found->second); - source.memoIndex.erase(found); + if (source.memo == nullptr) source.memo = std::make_unique(); + auto* set = memoSet(source, index); + auto* victim = set; + bool present = false; + for (std::size_t way = 0; way < ResultMemoWays && !present; ++way) { + auto& entry = set[way]; + if (entry.result != nullptr && entry.variantId == variant->artifact.variantId && entry.hash == hash) { + entry.used = ++source.memoClock; + shared = entry.result; + present = true; + } else if (entry.used < victim->used) { + victim = &entry; } } - if (source.memoIndex.find(index) == source.memoIndex.end()) { - source.memo.push_front({variant->artifact.variantId, hash, shared}); - source.memoIndex.emplace(index, source.memo.begin()); - while (source.memo.size() > ResultMemoEntries) { - const auto& last = source.memo.back(); - source.memoIndex.erase((last.variantId * 0x9e3779b97f4a7c15ull) ^ last.hash); - source.memo.pop_back(); - counters.evictions.fetch_add(1, std::memory_order_relaxed); - } + if (!present) { + if (victim->result != nullptr) counters.evictions.fetch_add(1, std::memory_order_relaxed); + *victim = {variant->artifact.variantId, hash, shared, ++source.memoClock}; } } reportResultMemo(); @@ -1238,21 +1504,29 @@ auto recompileReporting(const RecompileRequest& request, Impl&& impl) -> decltyp std::shared_ptr GetResourcePlan(const RecompileRequest& request) { static_cast(RequestInputInfo(request)); - if (request.useCache) return getSource(request)->plan; + if (request.useCache) { + const auto source = getSource(request); + return acquirePlan(request, source, false); + } return makeResourcePlan(request); } +void ReleaseUndrawnResourcePlan(const SourceHandle& handle) { + releaseUndrawnPlan(handle); +} + namespace { // The capture's materialization; with pure flat slots in the plan the walk's read addresses are // 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; @@ -1270,7 +1544,7 @@ std::shared_ptr CaptureResources(const RecompileRequest& auto capture = std::make_shared(); if (request.useCache) { capture->source = getSource(request); - capture->plan = capture->source->plan; + capture->plan = acquirePlan(request, capture->source, true); } else { capture->plan = makeResourcePlan(request); } @@ -1324,6 +1598,7 @@ bool MatchesPreparedShader(const RecompileRequest& request, const SourceHandle& const auto words = prepared.ShaderDataDwords(); const bool usesPush = prepared.runtimeImageCount == 0u && words != 0u && words <= layout.pushConstantSizeBytes / 4u; if (prepared.UsesPushData() != usesPush) return false; + if (!handle.artifact->artifact.stencilReferences.empty() && (prepared.stencilReferenceDword >= layout.pushConstantSizeBytes / 4u || prepared.stencilReferenceDword != (usesPush ? words : 0u))) return false; if (!std::ranges::equal(key, handle.staticKey) || handle.source->code.size() != request.shader.code.size()) return false; return handle.source->code.data() == request.shader.code.data() || std::ranges::equal(handle.source->code, request.shader.code); } @@ -1333,7 +1608,9 @@ std::span GetPreparedCode(const SourceHandle& handle) { return handle.source->code; } -PreparedShaderInvocation::PreparedShaderInvocation(const RecompileRequest& request, const std::shared_ptr& handle) : request(request), handle(handle) {} +PreparedShaderInvocation::PreparedShaderInvocation(const RecompileRequest& request, const std::shared_ptr& handle) : request(request), handle(handle) { + this->request.context.linkedCallFallbacks = handle->source->linkedCallFallbacks; +} std::optional PreparedShaderInvocation::TryCreate(const RecompileRequest& request, const std::shared_ptr& handle) { struct PreparedKeyStorage {}; @@ -1344,20 +1621,20 @@ std::optional PreparedShaderInvocation::TryCreate(cons std::optional PreparedShaderInvocation::TryCreate(const RecompileRequest& request, const std::shared_ptr& handle, std::span key) { if (handle == nullptr || !MatchesPreparedShader(request, *handle, key)) return std::nullopt; - if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) static_cast(RequestInputInfo(request)); + if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) ValidateRequestInputs(request); return PreparedShaderInvocation(request, handle); } std::shared_ptr PreparedShaderInvocation::Capture(const SrtRuntime& runtime) const { auto capture = std::make_shared(); capture->source = handle->source; - capture->plan = handle->source->plan; + capture->plan = acquirePlan(request, handle->source, true, handle.get()); materializeCapture(*capture, runtime); return capture; } std::shared_ptr PreparedShaderInvocation::Materialize(const ResourceCapture& capture) const { - if (capture.source != handle->source || capture.plan != handle->source->plan) throw std::runtime_error("ShaderRecompiler: resource capture belongs to another prepared shader"); + if (capture.source != handle->source) throw std::runtime_error("ShaderRecompiler: resource capture belongs to another prepared shader"); if (request.useCache && ResultMemo()) return materializePreparedMemoized(*handle->source, handle->artifact, request, capture.snapshot, true, nullptr); auto result = std::make_shared(materializeResult(*handle->artifact, request, capture.snapshot)); result->cacheHit = true; @@ -1385,7 +1662,7 @@ std::shared_ptr RegistrationResult(const RecompileRequest std::shared_ptr MaterializeShader(const RecompileRequest& request, const ResourceCapture& capture, const SourceHandle& handle) { if (!MatchesPreparedShader(request, handle)) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the static ABI"); - if (capture.source != handle.source || capture.plan != handle.source->plan) throw std::runtime_error("ShaderRecompiler: resource capture belongs to another prepared shader"); + if (capture.source != handle.source) throw std::runtime_error("ShaderRecompiler: resource capture belongs to another prepared shader"); if (request.useCache && ResultMemo()) return materializePreparedMemoized(*handle.source, handle.artifact, request, capture.snapshot, true, nullptr); auto result = std::make_shared(materializeResult(*handle.artifact, request, capture.snapshot)); result->cacheHit = true; @@ -1398,10 +1675,10 @@ std::shared_ptr CaptureResources(const RecompileRequest& static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto started = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; // The whole vertex family (Vertex, Local, TC, TE, Mesh) validates V# fields the memo key does not cover. - if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) static_cast(RequestInputInfo(request)); + if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) ValidateRequestInputs(request); auto capture = std::make_shared(); capture->source = handle.source; - capture->plan = handle.source->plan; + capture->plan = acquirePlan(request, handle.source, true, &handle); if (profile) capture->sourceNanoseconds = static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()); materializeCapture(*capture, runtime); return capture; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index f17e0ad63b..4cb5a69c8d 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 { @@ -121,6 +123,33 @@ enum class RasterPrimitive : std::uint8_t { Rectangle }; +enum class PixelPositionLocation : std::uint8_t { + Center, + Centroid, + Sample, + UpperLeft +}; + +constexpr float SampleLocationOffset(std::uint8_t location, std::uint32_t axis) { + const auto nibble = (static_cast(location) >> (4u * axis)) & 0xfu; + return static_cast(static_cast(nibble & 7u) - static_cast(nibble & 8u)) / 16.0f; +} + +namespace ReversedBlendCode { + +inline constexpr std::uint32_t Enable = 1u; +inline constexpr std::uint32_t ColorShift = 8u; +inline constexpr std::uint32_t SecondShift = 16u; +inline constexpr std::uint32_t RouteBits = 2u; +inline constexpr std::uint32_t RouteMask = 3u; +inline constexpr std::uint32_t Routes = 0xffu; +inline constexpr std::uint32_t Own = 0u; +inline constexpr std::uint32_t Component0 = 1u; +inline constexpr std::uint32_t Zero = 2u; +inline constexpr std::uint32_t One = 3u; + +} + struct ShaderPixelStageInfo { std::uint32_t interpolatorCount; std::array interpolatorSettings; @@ -133,6 +162,7 @@ struct ShaderPixelStageInfo { bool posZ; bool posW; bool frontFace; + bool frontFaceAllBits; bool ancillary; bool sampleShading; bool noPerspective; @@ -150,9 +180,17 @@ struct ShaderPixelStageInfo { bool dualSourceBlend; bool alphaToCoverage; bool stencilRefExportEnable; + bool stencilOperationValueExport = false; RasterPrimitive rasterPrimitive = RasterPrimitive::Triangle; bool layerExported = false; bool ditheredAlphaToCoverage = false; + std::uint8_t quadPixelMask = 0xf; + std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; + std::uint32_t reversedBlend = 0; + bool iteratesSamples = false; + PixelPositionLocation positionLocation = PixelPositionLocation::Center; + std::array sampleLocations{}; + std::array sampleOrder{}; }; struct ShaderVertexBufferResource { @@ -175,6 +213,11 @@ struct ShaderVertexStageInfo { std::uint32_t fetchBufferReg; bool fetchEmbedded; std::uint32_t paClVsOutCntl = 0; + std::uint32_t depthRemapMode = 0; + bool pointList = false; + float pointSize = 1.0f; + float pointSizeMin = 0.0f; + float pointSizeMax = 0.0f; }; struct ShaderFloatMode { @@ -200,6 +243,8 @@ struct GuestContext { std::optional vertex; std::span memory; std::optional floatMode; + std::span linkedCallFallbacks; + bool convertedSamples = false; }; struct MeshTargetLimits { @@ -246,6 +291,12 @@ struct SpirvTarget { std::uint32_t registrationWordLimit = 200000u; bool vertexSubgroups = true; bool float16DenormPreserve = false; + bool spannedWaves = false; + bool robustBufferAccess2 = false; + bool depthRangeRemap = false; + bool float32FlushToZero = false; + bool waveLdsMemoryBarriers = false; + bool float32DenormPreserve = false; }; struct BindingLayout { @@ -282,6 +333,7 @@ struct MeshConfiguration { std::uint32_t ldsSizeDwords; std::uint32_t provokingVertex; std::uint32_t esgsItemSize = 0; + std::uint32_t outputPrimitiveSize = 3; }; struct TessellationConfiguration { @@ -293,12 +345,17 @@ struct TessellationConfiguration { std::uint32_t patchesPerGroup = 256; std::uint32_t primitiveGroupSize = 0; bool breakAtInstance = false; + std::uint32_t instancePatches = 0; + bool drawParameters = false; + bool mergedHull = false; }; inline constexpr std::uint32_t MeshDrawPushOffsetBytes = 104; inline constexpr std::uint32_t MeshDrawPushBytes = 24; inline constexpr std::uint32_t TessellationLevelPushOffsetBytes = 120; inline constexpr std::uint32_t TessellationLevelPushBytes = 8; +inline constexpr std::uint32_t DepthRemapPushOffsetBytes = 104; +inline constexpr std::uint32_t DepthRemapPushBytes = 8; inline constexpr std::uint32_t MeshArgumentAddressDword = 4; inline constexpr std::uint32_t MeshArgumentIndexCountDword = 3; inline constexpr std::uint32_t MeshArgumentFirstIndexDword = 4; @@ -316,12 +373,38 @@ struct GraphicsDrawParameters { std::uint32_t instanceCount; }; +struct FragmentParameter { + std::uint32_t location; + std::uint32_t sourceLocation; + bool flat; + bool perVertex; + bool custom = false; + bool flatP10 = false; + bool vertexOrder = false; + bool flatP20 = false; + bool operator==(const FragmentParameter&) const = default; +}; + +struct BarycentricEmulation { + bool active = false; + bool smooth = false; + bool linear = false; + bool operator==(const BarycentricEmulation&) const = default; +}; + +struct MeshBarycentricLayout { + BarycentricEmulation emulation; + std::vector parameters; + bool operator==(const MeshBarycentricLayout&) const = default; +}; + struct GraphicsCompileContext { std::uint32_t firstUserSgpr; std::span linkedPrograms; std::optional mesh; std::optional tessellation; GraphicsDrawParameters draw; + std::optional meshBarycentrics; }; struct RecompileRequest { @@ -333,6 +416,11 @@ struct RecompileRequest { bool useCache = true; }; +inline std::uint32_t TessellationInstancePatches(const RecompileRequest& request) { + if (!request.graphics || !request.graphics->tessellation || request.graphics->draw.instanceCount <= 1u || request.graphics->tessellation->breakAtInstance) return 0u; + return request.graphics->draw.indexCount / request.graphics->tessellation->inputControlPoints; +} + enum class DescriptorKind { UniformBuffer, StorageBuffer, @@ -349,7 +437,9 @@ enum class DescriptorImageShape { Image2DArray, ImageCube, Image3D, - Image1DArray + Image1DArray, + Image2DMultisample, + Image2DMultisampleArray }; enum class DescriptorRole { @@ -360,7 +450,11 @@ enum class DescriptorRole { BdaPagetable, FaultBuffer, FlattenedSrt, - ShaderData + ShaderData, + ImageTable, + SamplerTable, + ImageTableMap, + ImageStorageTable }; struct DescriptorBinding { @@ -378,6 +472,7 @@ struct DescriptorBinding { std::vector imageDepthCompare; std::vector imageAtomic; std::vector imageAtomic64; + std::vector imageShapes; // Guest buffer elements the shader updates atomically (one entry per element of a GuestBuffers // binding, empty otherwise). An atomic on a host-imported range is a serialized PCIe round trip // (~0.4-0.5 us each on NVIDIA), so a driver may keep these elements in device-local memory. @@ -390,10 +485,19 @@ struct DescriptorBinding { std::vector bufferWritten; std::vector samplerUnnormalized; std::vector samplerIntegerBorder; + std::vector samplerCoordinatesIgnored; std::vector imageUnnormalized; std::vector imageUnnormalizedBaseLevel; std::vector imageSamplers; std::vector bufferRead; + std::vector bufferExtent; +}; + +struct ImageTableEntryPoison { + std::array words{}; + std::uint32_t dwordCount = 0; + std::uint32_t resource = 0; + std::uint32_t reason = 0; }; struct VertexAttribute { @@ -404,29 +508,60 @@ struct VertexAttribute { std::uint32_t formatComponents = 0; }; -struct FragmentParameter { - std::uint32_t location; - std::uint32_t sourceLocation; - bool flat; - bool perVertex; - bool custom = false; - bool flatP10 = false; -}; - -struct BarycentricEmulation { - bool active = false; - bool smooth = false; - bool linear = false; -}; - struct BarycentricEmulationLayout { static constexpr std::uint32_t NoLocation = 0xffffffffu; std::uint32_t smoothLocation = NoLocation; std::uint32_t linearLocation = NoLocation; + std::uint32_t positionsLocation = NoLocation; std::vector> perVertexLocations; }; -[[nodiscard]] BarycentricEmulationLayout LayoutBarycentricEmulation(std::span parameters, const BarycentricEmulation& emulation); +[[nodiscard]] BarycentricEmulationLayout LayoutBarycentricEmulation(std::span parameters, const BarycentricEmulation& emulation, bool meshPositions = false); + +struct EmulatedPerVertexParameter { + std::uint32_t sourceLocation; + std::uint32_t location; +}; + +struct EmulatedInterface { + std::vector passed; + std::vector passedFlat; + std::vector perVertex; + BarycentricEmulationLayout layout; + std::uint32_t barycentrics = 0; + std::uint32_t sources = 0; +}; + +[[nodiscard]] EmulatedInterface DescribeEmulatedInterface(std::span parameters, const BarycentricEmulation& emulation, bool meshPositions = false); + +// A 128-bit hash of a module's words (SharedSpirv::ContentHash), zero for an empty module. The +// mixing is pure integer arithmetic over the words in order, so the value is the same on every run, +// 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 @@ -434,19 +569,29 @@ struct BarycentricEmulationLayout { // 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(); } @@ -463,18 +608,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 { @@ -513,9 +667,14 @@ struct CompiledShaderArtifact { bool vertexOffsetConflict = false; bool instanceOffsetConflict = false; std::uint32_t hostSubgroupSize = 0; + bool fullSubgroups = false; std::vector parameterExports; std::vector fragmentParameters; BarycentricEmulation barycentricEmulation; + std::optional meshBarycentrics; + std::vector stencilReferences; + bool stencilReplaySafe = false; + std::uint32_t stencilReferenceDword = 0xffffffffu; std::uint64_t variantId = 0; }; @@ -524,16 +683,76 @@ struct ShaderInvocation { std::uint64_t specializationId = 0; std::vector bindings; std::vector pushConstants; + bool readsDepthRemapPush = false; std::vector vertexAttributes; + std::uint64_t userDataRegisterReads = 0; std::uint32_t poisonedSrtReads = 0; + std::vector imageTablePoison; + std::vector> imageTableRanges; + std::vector>> imageTableReadRanges; + std::uint64_t imageTableShader = 0; + 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]] std::optional MeshBarycentricLayoutOf(const RecompileResult& fragment); + +struct MeshOutputUse { + std::uint32_t flatParameters = 0; + std::uint32_t perVertexParameters = 0; + std::uint32_t clipVaryings = 0; + std::uint32_t clipDistances = 0; + std::uint32_t cullDistances = 0; + bool layer = false; +}; + +struct MeshOutputBudget { + std::uint32_t vertexLocations = 0; + std::uint32_t primitiveLocations = 0; + std::uint64_t components = 0; + std::uint64_t bytes = 0; +}; + +[[nodiscard]] MeshOutputBudget MeshBarycentricOutputBudget(const MeshConfiguration& mesh, const MeshTargetLimits& limits, const MeshOutputUse& use); + [[nodiscard]] RecompileResult Recompile(const RecompileRequest& request); struct ResourceCapture; @@ -550,6 +769,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); @@ -564,6 +784,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, std::uint32_t provokingVertex); + } #endif diff --git a/core/shader/recompiler/RuntimeAbi.hpp b/core/shader/recompiler/RuntimeAbi.hpp index eb5ce8cfc0..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 = 11u; +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; @@ -27,7 +27,11 @@ enum class Binding : std::uint32_t { FaultBuffer = 60u, FlattenedSrt = 61u, ShaderData = 62u, - Count = 63u + SamplerTable = 63u, + ImageTableMap = 64u, + Count = 65u, + ImageTable = 256u, + ImageStorageTable = 257u }; enum class Stage : std::uint32_t { Main, Fragment, TessellationControl, TessellationEvaluation }; @@ -37,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; @@ -84,16 +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 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); @@ -102,6 +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) && ImageStorageTableBinding == 260u && ImageTableBinding == 261u); } diff --git a/core/shader/recompiler/ShaderCacheDirectory.hpp b/core/shader/recompiler/ShaderCacheDirectory.hpp index ee9393e078..9b3893ce2a 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 @@ -32,18 +33,42 @@ namespace ShaderRecompiler { std::filesystem::path ShaderCacheDirectory(); +#ifdef _WIN32 +inline std::filesystem::path NativeCachePath_nid_no_patch(const std::filesystem::path& path, std::error_code& error) { + auto native = path; + native.make_preferred(); + if (native.native().starts_with(L"\\\\?\\") || native.native().starts_with(L"\\\\.\\")) return native; + const auto capacity = GetFullPathNameW(native.c_str(), 0, nullptr, nullptr); + if (capacity == 0) { error = std::error_code(GetLastError(), std::system_category()); return {}; } + std::wstring absolute(capacity, L'\0'); + const auto size = GetFullPathNameW(native.c_str(), capacity, absolute.data(), nullptr); + if (size == 0 || size >= capacity) { + error = std::error_code(size == 0 ? GetLastError() : ERROR_INSUFFICIENT_BUFFER, std::system_category()); + return {}; + } + absolute.resize(size); + if (absolute.starts_with(L"\\\\")) return std::filesystem::path(L"\\\\?\\UNC\\" + absolute.substr(2)); + return std::filesystem::path(L"\\\\?\\" + absolute); +} +#endif + inline bool WriteFileAtomically(const std::filesystem::path& path, std::span bytes) { std::error_code error; - std::filesystem::create_directories(path.parent_path(), error); +#ifdef _WIN32 + const auto destination = NativeCachePath_nid_no_patch(path, error); + if (error) return false; +#else + const auto& destination = path; +#endif + std::filesystem::create_directories(destination.parent_path(), error); static std::atomic serial{0}; const auto unique = std::hash{}(std::this_thread::get_id()) ^ static_cast(std::chrono::steady_clock::now().time_since_epoch().count()); - auto temporary = path; + auto temporary = destination; temporary += ".tmp." + std::to_string(unique) + "." + std::to_string(serial.fetch_add(1, std::memory_order_relaxed)); #ifdef _WIN32 struct RenameInfo { DWORD flags; HANDLE root; DWORD nameBytes; WCHAR name[1]; }; static_assert(sizeof(RenameInfo) == sizeof(FILE_RENAME_INFO) && offsetof(RenameInfo, name) == offsetof(FILE_RENAME_INFO, FileName)); - const auto destination = std::filesystem::absolute(path, error); - if (error || destination.native().size() > (MAXDWORD - sizeof(RenameInfo)) / sizeof(wchar_t)) return false; + if (destination.native().size() > (MAXDWORD - sizeof(RenameInfo)) / sizeof(wchar_t)) return false; const auto nameBytes = destination.native().size() * sizeof(wchar_t); std::vector renameBuffer(sizeof(RenameInfo) + nameBytes); auto* rename = reinterpret_cast(renameBuffer.data()); @@ -56,7 +81,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(); } @@ -83,7 +113,7 @@ inline bool WriteFileAtomically(const std::filesystem::path& path, std::span +#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 +#include +#include +#include +#endif + +namespace ShaderRecompiler::ShaderDiskCache { +namespace { +using Clock = std::chrono::steady_clock; +std::uint64_t elapsed(Clock::time_point start) { + return std::chrono::duration_cast(Clock::now() - start).count(); +} +} + +std::array PackKeyHash(std::span key) { + return {HashBytes(key, 0x5eed0001ull), HashBytes(key, 0x5eed0002ull)}; +} + +std::vector PrebuiltCachePaths(const std::filesystem::path& cacheRoot) { + if (cacheRoot.empty()) return {}; + return {cacheRoot / "prebuilt"}; +} + +CachePack::CachePack(const std::filesystem::path& path) { + static_assert(std::endian::native == std::endian::little); +#ifdef _WIN32 + std::error_code pathError; + const auto filename = NativeCachePath_nid_no_patch(path, pathError); + if (pathError) { error = "cannot resolve"; return; } + const auto handle = CreateFileW(filename.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (handle == INVALID_HANDLE_VALUE) { error = "cannot open"; return; } + LARGE_INTEGER size{}; + if (!GetFileSizeEx(handle, &size) || size.QuadPart < static_cast(sizeof(PackHeader)) || static_cast(size.QuadPart) > std::numeric_limits::max()) { + CloseHandle(handle); error = "invalid size"; return; + } + mapping = CreateFileMappingW(handle, nullptr, PAGE_READONLY, 0, 0, nullptr); + fileHandle = handle; + if (!mapping) { error = "cannot map"; return; } + const auto* data = static_cast(MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0)); + if (!data) { error = "cannot map view"; return; } + file = {data, static_cast(size.QuadPart)}; +#else + const auto fd = open(path.c_str(), O_RDONLY | O_CLOEXEC); + if (fd < 0) { error = "cannot open"; return; } + struct stat size{}; + if (fstat(fd, &size) != 0 || size.st_size < static_cast(sizeof(PackHeader)) || static_cast(size.st_size) > std::numeric_limits::max()) { + close(fd); error = "invalid size"; return; + } + auto* data = mmap(nullptr, size.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + if (data == MAP_FAILED) { error = "cannot map"; return; } + file = {static_cast(data), static_cast(size.st_size)}; +#endif + Validate(); +} + +CachePack::~CachePack() { +#ifdef _WIN32 + if (!file.empty()) UnmapViewOfFile(file.data()); + if (mapping) CloseHandle(mapping); + if (fileHandle) CloseHandle(fileHandle); +#else + if (!file.empty()) munmap(const_cast(file.data()), file.size()); +#endif +} + +void CachePack::Validate() { + PackHeader header; + std::memcpy(&header, file.data(), sizeof(header)); + if (header.magic != PackMagic) { error = "unknown pack format"; return; } + if (header.sourceVersion != SourceVersion()) { error = "different compiler source version"; return; } + if (header.fileBytes != file.size() || header.entryCount > (file.size() - sizeof(header)) / sizeof(PackRecord)) { error = "truncated index"; return; } + const auto indexBytes = static_cast(header.entryCount) * sizeof(PackRecord); + const auto index = file.subspan(sizeof(header), indexBytes); + if (HashBytes(index) != header.indexHash) { error = "index checksum mismatch"; return; } + const std::span checked(reinterpret_cast(index.data()), header.entryCount); + std::array previous{}; + for (const auto& record : checked) { + if (record.hash < previous || record.offset < sizeof(header) + indexBytes || record.offset > file.size() || record.size > file.size() - record.offset || record.size < 48) { error = "invalid index record"; return; } + previous = record.hash; + } + records = checked; +} + +LoadStatus CachePack::Load(std::span key, CompiledVariant& variant, PackLoadTimings& timings) const { + const auto start = Clock::now(); + const auto hash = PackKeyHash(key); + const auto first = std::lower_bound(records.begin(), records.end(), hash, [](const auto& record, const auto& value) { return record.hash < value; }); + timings.lookupNanoseconds += elapsed(start); + auto status = LoadStatus::Absent; + for (auto it = first; it != records.end() && it->hash == hash; ++it) { + const auto data = file.subspan(it->offset, it->size); + const auto decode = Clock::now(); + const auto result = DecodeEntry(data, key, variant); + timings.decodeNanoseconds += elapsed(decode); + timings.bytes += data.size(); + if (result == LoadStatus::Loaded) return result; + if (result == LoadStatus::Rejected) status = result; + } + return status; +} +} diff --git a/core/shader/recompiler/ShaderCachePack.hpp b/core/shader/recompiler/ShaderCachePack.hpp new file mode 100644 index 0000000000..24aff33610 --- /dev/null +++ b/core/shader/recompiler/ShaderCachePack.hpp @@ -0,0 +1,51 @@ +#pragma once +#include "ShaderDiskCache.hpp" +#include + +namespace ShaderRecompiler::ShaderDiskCache { + +inline constexpr std::uint64_t PackMagic = 0x314b434150535041ull; +struct PackHeader { + std::uint64_t magic = PackMagic; + std::uint64_t sourceVersion = 0; + std::uint64_t fileBytes = 0; + std::uint64_t entryCount = 0; + std::uint64_t indexHash = 0; +}; +struct PackRecord { + std::array hash{}; + std::uint64_t offset = 0; + std::uint64_t size = 0; +}; +static_assert(sizeof(PackHeader) == 40 && sizeof(PackRecord) == 32); + +struct PackLoadTimings { + std::uint64_t lookupNanoseconds = 0; + std::uint64_t decodeNanoseconds = 0; + std::uint64_t bytes = 0; +}; + +[[nodiscard]] std::array PackKeyHash(std::span key); +[[nodiscard]] std::vector PrebuiltCachePaths(const std::filesystem::path& cacheRoot); + +class CachePack { +public: + explicit CachePack(const std::filesystem::path& path); + ~CachePack(); + CachePack(const CachePack&) = delete; + CachePack& operator=(const CachePack&) = delete; + [[nodiscard]] bool Valid() const { return error.empty(); } + [[nodiscard]] const std::string& Error() const { return error; } + [[nodiscard]] std::size_t Count() const { return records.size(); } + [[nodiscard]] LoadStatus Load(std::span key, CompiledVariant& variant, PackLoadTimings& timings) const; + +private: + void Validate(); + std::span file; + std::span records; + std::string error; + void* mapping = nullptr; + void* fileHandle = nullptr; +}; + +} diff --git a/core/shader/recompiler/ShaderCacheSourceVersion.hpp b/core/shader/recompiler/ShaderCacheSourceVersion.hpp new file mode 100644 index 0000000000..d73de7db7f --- /dev/null +++ b/core/shader/recompiler/ShaderCacheSourceVersion.hpp @@ -0,0 +1,15 @@ +#ifndef CORE_SHADER_RECOMPILER_SHADERCACHESOURCEVERSION_HPP +#define CORE_SHADER_RECOMPILER_SHADERCACHESOURCEVERSION_HPP + +#include +#include +#include + +namespace ShaderRecompiler::ShaderDiskCache::Generated { + +extern const std::uint64_t SourceVersion; +extern const std::span RecompilerSwitches; + +} + +#endif diff --git a/core/shader/recompiler/ShaderCacheVersion.cmake b/core/shader/recompiler/ShaderCacheVersion.cmake index e952236aef..847a513496 100644 --- a/core/shader/recompiler/ShaderCacheVersion.cmake +++ b/core/shader/recompiler/ShaderCacheVersion.cmake @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) -file(STRINGS "${LIST_FILE}" entries) +file(STRINGS "${LIST_FILE}" entries ENCODING UTF-8) set(sources) set(guard) set(submodules) @@ -26,7 +26,6 @@ endforeach() list(SORT sources) list(SORT guard) get_filename_component(rootReal "${ROOT}" REALPATH) -get_filename_component(recompilerReal "${RECOMPILER_DIR}" REALPATH) set(sourcesReal) foreach(source IN LISTS sources) get_filename_component(real "${source}" REALPATH) @@ -46,7 +45,7 @@ foreach(source IN LISTS sources) if(NOT source MATCHES "\\.(cpp|hpp|h)$") continue() endif() - file(STRINGS "${source}" lines REGEX "\"APS5_[A-Z0-9_]+\"") + file(STRINGS "${source}" lines ENCODING UTF-8 REGEX "\"APS5_[A-Z0-9_]+\"") foreach(line IN LISTS lines) string(REGEX MATCHALL "\"APS5_[A-Z0-9_]+\"" names "${line}") list(APPEND switches ${names}) @@ -56,7 +55,7 @@ foreach(source IN LISTS sources) continue() endif() get_filename_component(sourceDirectory "${source}" DIRECTORY) - file(STRINGS "${source}" lines REGEX "^[ \t]*#[ \t]*include[ \t]*\"[^\"]+\"") + file(STRINGS "${source}" lines ENCODING UTF-8 REGEX "^[ \t]*#[ \t]*include[ \t]*\"[^\"]+\"") foreach(line IN LISTS lines) string(REGEX REPLACE "^[ \t]*#[ \t]*include[ \t]*\"([^\"]+)\".*$" "\\1" included "${line}") set(resolved) @@ -70,9 +69,8 @@ foreach(source IN LISTS sources) if(NOT resolved) continue() endif() - string(FIND "${resolved}" "${recompilerReal}/" resolvedInRecompiler) string(FIND "${resolved}" "${rootReal}/3rdparty/" resolvedInThirdParty) - if(resolvedInRecompiler EQUAL 0 OR resolvedInThirdParty EQUAL 0 OR resolved IN_LIST sourcesReal) + if(resolvedInThirdParty EQUAL 0 OR resolved IN_LIST sourcesReal) continue() endif() file(RELATIVE_PATH relativeResolved "${rootReal}" "${resolved}") @@ -109,7 +107,7 @@ foreach(source IN LISTS guard) continue() endif() file(RELATIVE_PATH relative "${ROOT}" "${source}") - file(STRINGS "${source}" lines REGEX "ShaderRecompiler|setenv|putenv") + file(STRINGS "${source}" lines ENCODING UTF-8 REGEX "ShaderRecompiler|setenv|putenv") foreach(line IN LISTS lines) if(line MATCHES "using[ \t]+namespace[ \t]+ShaderRecompiler" OR line MATCHES "namespace[ \t]+[A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*(::)?ShaderRecompiler[ \t]*;") list(APPEND failures "${relative} brings the ShaderRecompiler namespace in whole, which hides its calls from the shader disk cache key review: qualify them") @@ -140,23 +138,22 @@ list(REMOVE_DUPLICATES switches) list(SORT switches) list(JOIN switches ",\n " switchList) -set(content "#ifndef CORE_SHADER_RECOMPILER_SHADERCACHEVERSION_HPP -#define CORE_SHADER_RECOMPILER_SHADERCACHEVERSION_HPP - -#include -#include - +set(content "#include \"ShaderCacheSourceVersion.hpp\"\n") +string(APPEND content " namespace ShaderRecompiler::ShaderDiskCache::Generated { -inline constexpr std::uint64_t SourceVersion = 0x${version}ull; +namespace { -inline constexpr std::string_view RecompilerSwitches[] = { +constexpr std::string_view recompilerSwitches[] = { ${switchList} }; } -#endif +constinit const std::uint64_t SourceVersion = 0x${version}ull; +constinit const std::span RecompilerSwitches = recompilerSwitches; + +} ") if(EXISTS "${OUTPUT}") diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 355e720e76..2362ce871a 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -1,5 +1,7 @@ #include "ShaderDiskCache.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include "CacheKey.hpp" +#include "ShaderCachePack.hpp" #include "ShaderCacheDirectory.hpp" #include "ThreadOwned.hpp" #include "Optimization/ResourceMaterializer.hpp" @@ -20,7 +22,7 @@ #include #include #endif -#include "ShaderCacheVersion.hpp" +#include "ShaderCacheSourceVersion.hpp" #include #include #include @@ -83,33 +85,34 @@ std::filesystem::path ShaderCacheDirectory() { namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(CompiledShaderArtifact) == 232, "CompiledShaderArtifact changed: update the artifact encoder"); -static_assert(sizeof(ShaderInvocation) == 112, "ShaderInvocation changed: update the invocation encoder"); -static_assert(sizeof(RecompileResult) == 352, "RecompileResult changed: update EncodeResult and DecodeResult"); -static_assert(sizeof(DescriptorBinding) == 568, "DescriptorBinding changed: update the binding encoder"); +static_assert(sizeof(CompiledShaderArtifact) == 280, "CompiledShaderArtifact changed: update the artifact encoder"); +static_assert(sizeof(ShaderInvocation) == 216, "ShaderInvocation changed: update the invocation encoder"); +static_assert(sizeof(RecompileResult) == 528, "RecompileResult changed: update EncodeResult and DecodeResult"); +static_assert(sizeof(DescriptorBinding) == 656, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder"); -static_assert(sizeof(FragmentParameter) == 12, "FragmentParameter changed: update the parameter encoder"); -static_assert(sizeof(CompiledShaderInfo) == 344, "CompiledShaderInfo changed: update the info encoder"); +static_assert(sizeof(FragmentParameter) == 16, "FragmentParameter changed: update the parameter encoder"); +static_assert(sizeof(CompiledShaderInfo) == 352, "CompiledShaderInfo changed: update the info encoder"); static_assert(sizeof(ShaderInfo) == 232, "ShaderInfo changed: update the info encoder"); static_assert(sizeof(BufferResource) == 32, "BufferResource changed: update the info encoder"); -static_assert(sizeof(ImageResource) == 120, "ImageResource changed: update the info encoder"); -static_assert(sizeof(SamplerResource) == 20, "SamplerResource changed: update the info encoder"); +static_assert(sizeof(ImageResource) == 92, "ImageResource changed: update the info encoder"); +static_assert(sizeof(SamplerResource) == 24, "SamplerResource changed: update the info encoder"); static_assert(sizeof(SampledResourcePair) == 12, "SampledResourcePair changed: update the info encoder"); static_assert(sizeof(StageInput) == 56, "StageInput changed: update the info encoder"); static_assert(sizeof(StageOutput) == 48, "StageOutput changed: update the info encoder"); -static_assert(sizeof(IrBindingLayout) == 72, "IrBindingLayout changed: update the layout 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) == 152, "BindingAllocationResult changed: update the allocation encoder"); -static_assert(sizeof(CompiledBindingLayout) == 80, "CompiledBindingLayout 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) == 584, "IrResourcePlan changed: update EncodePlan and DecodePlan"); -static_assert(sizeof(IrValue) == 152, "IrValue changed: update the plan value encoder"); +static_assert(sizeof(IrResourcePlan) == 728, "IrResourcePlan changed: update EncodePlan and DecodePlan"); +static_assert(sizeof(IrValue) == 128, "IrValue changed: update the plan value encoder"); static_assert(sizeof(GuestRegister) == 8, "GuestRegister changed: update the plan value encoder"); static_assert(sizeof(MemoryInfo) == 80, "MemoryInfo changed: update the plan encoder"); -static_assert(sizeof(DescriptorSource) == 104, "DescriptorSource changed: update the plan encoder"); -static_assert(sizeof(DescriptorSource::IndirectImage) == 28, "IndirectImage changed: update the plan encoder"); -static_assert(sizeof(ResourceBlock) == 80, "ResourceBlock changed: update the plan encoder"); +static_assert(sizeof(DescriptorSource) == 192, "DescriptorSource changed: update the plan encoder"); +static_assert(sizeof(TableColumn) == 112, "TableColumn changed: update the plan encoder"); +static_assert(sizeof(KeyDomain) == 16, "KeyDomain changed: update the plan encoder"); +static_assert(sizeof(ResourceBlock) == 104, "ResourceBlock changed: update the plan encoder"); static_assert(sizeof(SrtRead) == 16, "SrtRead changed: update the plan encoder"); static_assert(sizeof(UniformFill) == 28, "UniformFill changed: update the plan encoder"); static_assert(sizeof(UniformFillPlan) == 64, "UniformFillPlan changed: update the plan encoder"); @@ -138,17 +141,18 @@ constexpr std::size_t fieldCount() { static_assert(fieldCount() == 23, "ShaderInfo changed: update the info encoder"); static_assert(fieldCount() == 12, "BufferResource changed: update the info encoder"); -static_assert(fieldCount() == 37, "ImageResource changed: update the info encoder"); -static_assert(fieldCount() == 7, "SamplerResource changed: update the info encoder"); +static_assert(fieldCount() == 41, "ImageResource changed: update the info encoder"); +static_assert(fieldCount() == 8, "SamplerResource changed: update the info encoder"); static_assert(fieldCount() == 3, "SampledResourcePair changed: update the info encoder"); static_assert(fieldCount() == 5, "StageInput changed: update the info encoder"); static_assert(fieldCount() == 4, "StageOutput changed: update the info encoder"); -static_assert(fieldCount() == 21, "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() == 30, "MemoryInfo changed: update the plan encoder"); +static_assert(fieldCount() == 31, "MemoryInfo changed: update the plan encoder"); static_assert(fieldCount() == 3, "DescriptorSource changed: update the plan encoder"); -static_assert(fieldCount() == 7, "IndirectImage changed: update the plan encoder"); -static_assert(fieldCount() == 4, "ResourceBlock changed: update the plan encoder"); +static_assert(fieldCount() == 18, "TableColumn changed: update the plan encoder"); +static_assert(fieldCount() == 4, "KeyDomain changed: update the plan encoder"); +static_assert(fieldCount() == 5, "ResourceBlock changed: update the plan encoder"); static_assert(fieldCount() == 2, "SrtRead changed: update the plan encoder"); static_assert(fieldCount() == 5, "UniformFill changed: update the plan encoder"); static_assert(fieldCount() == 2, "UniformFillPlan changed: update the plan encoder"); @@ -192,7 +196,10 @@ private: std::shared_ptr MapFile(const std::filesystem::path& path) { #ifdef _WIN32 - const HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + std::error_code error; + const auto filename = NativeCachePath_nid_no_patch(path, error); + if (error) return nullptr; + const HANDLE file = CreateFileW(filename.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) return nullptr; LARGE_INTEGER length{}; if (!GetFileSizeEx(file, &length) || length.QuadPart <= 0 || static_cast(length.QuadPart) > std::numeric_limits::max()) { @@ -417,14 +424,17 @@ void encodeBinding(Writer& writer, const DescriptorBinding& binding) { writer.Flags(binding.imageDepthCompare); writer.Flags(binding.imageAtomic); writer.Flags(binding.imageAtomic64); + writer.List(binding.imageShapes, [](Writer& out, DescriptorImageShape shape) { out.Value(shape); }); writer.Flags(binding.bufferAtomic); writer.Flags(binding.bufferWritten); writer.Flags(binding.samplerUnnormalized); writer.Flags(binding.imageUnnormalized); writer.Flags(binding.imageUnnormalizedBaseLevel); writer.Flags(binding.samplerIntegerBorder); + writer.Flags(binding.samplerCoordinatesIgnored); writer.Values(std::span(binding.imageSamplers)); writer.Flags(binding.bufferRead); + writer.Values(std::span(binding.bufferExtent)); } void decodeBinding(Reader& reader, DescriptorBinding& binding) { @@ -443,14 +453,17 @@ void decodeBinding(Reader& reader, DescriptorBinding& binding) { reader.Flags(binding.imageDepthCompare); reader.Flags(binding.imageAtomic); reader.Flags(binding.imageAtomic64); + reader.List(binding.imageShapes, 4, [](Reader& in, DescriptorImageShape& shape) { in.Value(shape); }); reader.Flags(binding.bufferAtomic); reader.Flags(binding.bufferWritten); reader.Flags(binding.samplerUnnormalized); reader.Flags(binding.imageUnnormalized); reader.Flags(binding.imageUnnormalizedBaseLevel); reader.Flags(binding.samplerIntegerBorder); + reader.Flags(binding.samplerCoordinatesIgnored); reader.Values(binding.imageSamplers); reader.Flags(binding.bufferRead); + reader.Values(binding.bufferExtent); } void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { @@ -481,6 +494,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); @@ -489,10 +503,26 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { out.Value(parameter.perVertex); out.Value(parameter.custom); out.Value(parameter.flatP10); + out.Value(parameter.vertexOrder); + out.Value(parameter.flatP20); }); writer.Value(result.barycentricEmulation.active); writer.Value(result.barycentricEmulation.smooth); writer.Value(result.barycentricEmulation.linear); + writer.Value(result.meshBarycentrics.has_value()); + if (result.meshBarycentrics) { + writer.Value(result.meshBarycentrics->emulation.active); + writer.Value(result.meshBarycentrics->emulation.smooth); + writer.Value(result.meshBarycentrics->emulation.linear); + writer.List(result.meshBarycentrics->parameters, [](Writer& out, const FragmentParameter& parameter) { + out.Value(parameter.location); + out.Value(parameter.sourceLocation); + out.Value(parameter.perVertex); + }); + } + writer.Values(std::span(result.stencilReferences)); + writer.Value(result.stencilReplaySafe); + writer.Value(result.stencilReferenceDword); } void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { @@ -523,6 +553,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); @@ -531,10 +562,30 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { in.Value(parameter.perVertex); in.Value(parameter.custom); in.Value(parameter.flatP10); + in.Value(parameter.vertexOrder); + in.Value(parameter.flatP20); }); reader.Value(result.barycentricEmulation.active); reader.Value(result.barycentricEmulation.smooth); reader.Value(result.barycentricEmulation.linear); + bool hasMeshBarycentrics = false; + reader.Value(hasMeshBarycentrics); + result.meshBarycentrics.reset(); + if (hasMeshBarycentrics) { + MeshBarycentricLayout layout; + reader.Value(layout.emulation.active); + reader.Value(layout.emulation.smooth); + reader.Value(layout.emulation.linear); + reader.List(layout.parameters, 9, [](Reader& in, FragmentParameter& parameter) { + in.Value(parameter.location); + in.Value(parameter.sourceLocation); + in.Value(parameter.perVertex); + }); + result.meshBarycentrics = std::move(layout); + } + reader.Values(result.stencilReferences); + reader.Value(result.stencilReplaySafe); + reader.Value(result.stencilReferenceDword); result.variantId = 0; } @@ -546,6 +597,7 @@ void encodeInvocation(Writer& writer, const ShaderInvocation& invocation) { writer.List(invocation.bindings, encodeBinding); writer.Value(invocation.pushConstants.size()); for (const auto byte : invocation.pushConstants) writer.Value(static_cast(byte)); + writer.Value(invocation.readsDepthRemapPush); writer.List(invocation.vertexAttributes, [](Writer& out, const VertexAttribute& attribute) { out.Value(attribute.location); out.Value(attribute.components); @@ -553,7 +605,26 @@ void encodeInvocation(Writer& writer, const ShaderInvocation& invocation) { out.Value(attribute.fetchIndex); out.Value(attribute.formatComponents); }); + writer.Value(invocation.userDataRegisterReads); writer.Value(invocation.poisonedSrtReads); + writer.List(invocation.imageTablePoison, [](Writer& out, const ImageTableEntryPoison& poison) { + for (const auto word : poison.words) out.Value(word); + out.Value(poison.dwordCount); + out.Value(poison.resource); + out.Value(poison.reason); + }); + writer.List(invocation.imageTableRanges, [](Writer& out, const std::pair& range) { + out.Value(range.first); + out.Value(range.second); + }); + writer.List(invocation.imageTableReadRanges, [](Writer& out, const auto& ranges) { + out.List(ranges, [](Writer& rangeWriter, const auto& range) { + rangeWriter.Value(range.first); + rangeWriter.Value(range.second); + }); + }); + writer.Value(invocation.imageTableShader); + writer.Value(invocation.imageTableFaults); } void decodeInvocation(Reader& reader, ShaderInvocation& invocation) { @@ -567,6 +638,7 @@ void decodeInvocation(Reader& reader, ShaderInvocation& invocation) { reader.Values(pushConstants); invocation.pushConstants.resize(pushConstants.size()); if (!pushConstants.empty()) std::memcpy(invocation.pushConstants.data(), pushConstants.data(), pushConstants.size()); + reader.Value(invocation.readsDepthRemapPush); reader.List(invocation.vertexAttributes, 32, [](Reader& in, VertexAttribute& attribute) { in.Value(attribute.location); in.Value(attribute.components); @@ -574,11 +646,31 @@ void decodeInvocation(Reader& reader, ShaderInvocation& invocation) { in.Value(attribute.fetchIndex); in.Value(attribute.formatComponents); }); + reader.Value(invocation.userDataRegisterReads); reader.Value(invocation.poisonedSrtReads); + reader.List(invocation.imageTablePoison, 44, [](Reader& in, ImageTableEntryPoison& poison) { + for (auto& word : poison.words) in.Value(word); + in.Value(poison.dwordCount); + in.Value(poison.resource); + in.Value(poison.reason); + }); + reader.List(invocation.imageTableRanges, 16, [](Reader& in, std::pair& range) { + in.Value(range.first); + in.Value(range.second); + }); + reader.List(invocation.imageTableReadRanges, 8, [](Reader& in, auto& ranges) { + in.List(ranges, 16, [](Reader& rangeReader, auto& range) { + rangeReader.Value(range.first); + rangeReader.Value(range.second); + }); + }); + reader.Value(invocation.imageTableShader); + reader.Value(invocation.imageTableFaults); } void encodeLayout(Writer& writer, const IrBindingLayout& layout) { writer.Value(layout.pushDataStartDword); + writer.Value(layout.stencilReferenceDword); writer.Value(layout.memoryOffsetDword); writer.Value(layout.memoryOffsetCount); writer.Value(layout.dispatchThreadLimit); @@ -593,6 +685,7 @@ void encodeLayout(Writer& writer, const IrBindingLayout& layout) { void decodeLayout(Reader& reader, IrBindingLayout& layout) { reader.Value(layout.pushDataStartDword); + reader.Value(layout.stencilReferenceDword); reader.Value(layout.memoryOffsetDword); reader.Value(layout.memoryOffsetCount); reader.Value(layout.dispatchThreadLimit); @@ -649,17 +742,21 @@ void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { out.Value(image.depthBitsCompatible); out.Value(image.constantSwizzle); out.Value(image.constantSwizzleCompatible); + out.Value(image.cubeFaceCompatible); out.Value(image.flatVolumeCompatible); + out.Value(image.singleMipPlaneBiasCompatible); + out.Value(image.volumeArraySampleCompatible); out.Value(image.flatLineCompatible); + out.Value(image.flatVolumeLineCompatible); out.Value(image.foldTexelOffsetsOnly); + out.Value(image.convertedSamples); + out.Value(image.remappedDepth1D); out.Value(image.byElements); out.Value(image.byComponents); out.Value(image.packedFormat); out.Value(image.emulatedCompare); - out.Value(image.indirectRoot); - out.Value(image.indirectMappingOffset); - out.Value(image.indirectSearchIterations); - out.Values(std::span(image.indirectResources)); + out.Value(image.table); + out.Value(image.tableOperation); }); writer.Value(!info.runtimeImageModes.empty()); writer.List(info.samplers, [](Writer& out, const SamplerResource& sampler) { @@ -670,6 +767,7 @@ void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { out.Value(sampler.depthCompare); out.Value(sampler.uses); out.Value(sampler.foldTexelOffsetsOnly); + out.Value(sampler.table); }); writer.List(info.sampledPairs, [](Writer& out, const SampledResourcePair& pair) { out.Value(pair.image); @@ -722,7 +820,7 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) { in.Value(buffer.scalar); in.Value(buffer.typedAlignment); }); - reader.List(info.images, 75, [](Reader& in, ImageResource& image) { + reader.List(info.images, 77, [](Reader& in, ImageResource& image) { in.Value(image.source); in.Value(image.firstUsePc); in.Value(image.resourceClass); @@ -749,22 +847,26 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) { in.Value(image.depthBitsCompatible); in.Value(image.constantSwizzle); in.Value(image.constantSwizzleCompatible); + in.Value(image.cubeFaceCompatible); in.Value(image.flatVolumeCompatible); + in.Value(image.singleMipPlaneBiasCompatible); + in.Value(image.volumeArraySampleCompatible); in.Value(image.flatLineCompatible); + in.Value(image.flatVolumeLineCompatible); in.Value(image.foldTexelOffsetsOnly); + in.Value(image.convertedSamples); + in.Value(image.remappedDepth1D); in.Value(image.byElements); in.Value(image.byComponents); in.Value(image.packedFormat); in.Value(image.emulatedCompare); - in.Value(image.indirectRoot); - in.Value(image.indirectMappingOffset); - in.Value(image.indirectSearchIterations); - in.Values(image.indirectResources); + in.Value(image.table); + in.Value(image.tableOperation); }); bool preparedImageModes = false; reader.Value(preparedImageModes); if (preparedImageModes) ResourceMaterializer::PrepareImageModes(info); - reader.List(info.samplers, 17, [](Reader& in, SamplerResource& sampler) { + reader.List(info.samplers, 21, [](Reader& in, SamplerResource& sampler) { in.Value(sampler.source); in.Value(sampler.firstUsePc); in.Value(sampler.copyOf); @@ -772,6 +874,7 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) { in.Value(sampler.depthCompare); in.Value(sampler.uses); in.Value(sampler.foldTexelOffsetsOnly); + in.Value(sampler.table); }); reader.List(info.sampledPairs, 12, [](Reader& in, SampledResourcePair& pair) { in.Value(pair.image); @@ -877,6 +980,7 @@ void encodeMemoryInfo(Writer& out, const MemoryInfo& memory) { out.Value(memory.coherent); out.Value(memory.gpuDescriptor); out.Value(memory.flushDenormals); + out.Value(memory.flushDenormalResults); } void decodeMemoryInfo(Reader& in, MemoryInfo& memory) { @@ -910,17 +1014,18 @@ void decodeMemoryInfo(Reader& in, MemoryInfo& memory) { in.Value(memory.coherent); in.Value(memory.gpuDescriptor); in.Value(memory.flushDenormals); + in.Value(memory.flushDenormalResults); } bool encodePlan(Writer& writer, const IrResourcePlan& plan) { - if (std::ranges::any_of(plan.valueStorage, [](const auto& value) { return value == nullptr; }) || std::ranges::any_of(plan.blockStorage, [](const auto& block) { return block == nullptr; })) return false; + if (std::ranges::any_of(plan.blockStorage, [](const auto& block) { return block == nullptr; })) return false; std::unordered_map values; std::unordered_map blocks; - values.reserve(plan.valueStorage.size()); + values.reserve(plan.valueStorage.Size()); blocks.reserve(plan.blockStorage.size()); - for (std::size_t index = 0; index < plan.valueStorage.size(); ++index) values.emplace(plan.valueStorage[index].get(), static_cast(index)); + for (std::size_t index = 0; index < plan.valueStorage.Size(); ++index) values.emplace(&plan.valueStorage[index], static_cast(index)); for (std::size_t index = 0; index < plan.blockStorage.size(); ++index) blocks.emplace(plan.blockStorage[index].get(), static_cast(index)); - bool owned = values.size() == plan.valueStorage.size() && blocks.size() == plan.blockStorage.size(); + bool owned = values.size() == plan.valueStorage.Size() && blocks.size() == plan.blockStorage.size(); std::uint32_t current = 0; const auto valueIndex = [&](const IrValue* value) { if (value == nullptr) return NoPlanIndex; @@ -937,48 +1042,70 @@ bool encodePlan(Writer& writer, const IrResourcePlan& plan) { writer.Value(plan.userDataCount); writer.Value(plan.srgbDecodeFormats); writer.List(plan.blockStorage, [](Writer& out, const std::unique_ptr& block) { out.Value(block->Id()); }); - writer.List(plan.valueStorage, [&](Writer& out, const std::unique_ptr& value) { + writer.Value(plan.valueStorage.Size()); + for (const auto& stored : plan.valueStorage) { + const auto* value = &stored; const auto bank = static_cast(value->Register().bank); if (bank > 0xfu) owned = false; - out.Compact(static_cast(value->Opcode())); - out.Compact(static_cast(value->Type())); - out.Compact(value->Id()); - out.Compact(value->Flags()); - out.Compact((static_cast(value->Register().index) << 4u) | (bank & 0xfu)); - out.Compact((static_cast(value->ArgumentCount()) << 1u) | (value->HasImmediate() ? 1u : 0u)); - if (value->HasImmediate()) out.Compact(value->ImmediateU64()); + writer.Compact(static_cast(value->Opcode())); + writer.Compact(static_cast(value->Type())); + writer.Compact(value->Id()); + writer.Compact(value->Flags()); + writer.Compact((static_cast(value->Register().index) << 4u) | (bank & 0xfu)); + writer.Compact((static_cast(value->ArgumentCount()) << 1u) | (value->HasImmediate() ? 1u : 0u)); + if (value->HasImmediate()) writer.Compact(value->ImmediateU64()); for (const auto* argument : value->Arguments()) { const auto index = valueIndex(argument); const auto delta = static_cast(index) - static_cast(current); - out.Compact(argument == nullptr ? 0u : ((static_cast(delta) << 1u) ^ static_cast(delta >> 63)) + 1u); + writer.Compact(argument == nullptr ? 0u : ((static_cast(delta) << 1u) ^ static_cast(delta >> 63)) + 1u); } if (value->IsPhi() && value->PhiBlockCount() != value->ArgumentCount()) owned = false; for (std::size_t operand = 0; value->IsPhi() && operand < value->ArgumentCount(); ++operand) { const auto found = operand < value->PhiBlockCount() ? blocks.find(value->PhiBlock(operand)) : blocks.end(); if (found == blocks.end()) owned = false; - out.Compact(found != blocks.end() ? found->second : 0u); + writer.Compact(found != blocks.end() ? found->second : 0u); } ++current; - }); + } writer.List(plan.memoryInfo, encodeMemoryInfo); writer.List(plan.descriptorSources, [&](Writer& out, const DescriptorSource& source) { for (const auto* dword : source.dwords) out.Value(valueIndex(dword)); out.Value(source.dwordCount); - const auto table = source.indirectImage.value_or(DescriptorSource::IndirectImage{}); - out.Value(source.indirectImage.has_value()); - out.Value(table.materialSource); - out.Value(table.heapSource); - out.Value(table.selectorStride); - out.Value(table.selectorOffset); - out.Value(table.keyArg); - out.Value(table.entryOffset); - out.Value(table.hasMaterial); + const auto column = source.tableColumn.value_or(TableColumn{}); + out.Value(source.tableColumn.has_value()); + out.Value(column.heapSource); + out.Value(column.stride); + out.Value(column.addend); + out.Value(column.offset); + out.Value(column.dwordCount); + out.Value(column.maxKey); + out.Value(column.sampler); + out.Value(column.address); + out.List(column.keyDomains, [](Writer& domains, const KeyDomain& domain) { + domains.Value(domain.source); + domains.Value(domain.offset); + domains.Value(domain.stride); + domains.Value(domain.value); + }); + out.Value(column.unkeyed); + out.Value(column.indirect); + out.Value(column.indirectOffset); + out.Value(column.indirectImmediate); + out.List(column.pointerHops, [](Writer& hops, const PointerHop& hop) { + hops.Value(hop.offset); + hops.Value(hop.immediate); + }); + 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)); out.Values(std::span(block.successors)); out.Values(std::span(block.sources)); out.Values(std::span(block.images)); + out.Values(std::span(block.flatSlots)); }); writer.Values(std::span(plan.materializationSources)); writer.List(plan.srtReads, [&](Writer& out, const SrtRead& read) { @@ -989,6 +1116,7 @@ bool encodePlan(Writer& writer, const IrResourcePlan& plan) { writer.Value(plan.srtGuardOffset); writer.Values(std::span(plan.cleanFlatSlots)); writer.Values(std::span(plan.pureFlatSlots)); + writer.Value(plan.denseValueIds); writer.Value(plan.requiresSpecializationMemory); writer.Value(plan.srtPlanComplete); writer.Value(plan.resourceTrackingComplete); @@ -1073,55 +1201,74 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { if (!reader.Ok()) return false; plan.blockStorage.reserve(blockIds.size()); for (const auto id : blockIds) plan.blockStorage.push_back(std::make_unique(id)); - plan.valueStorage.reserve(records.size()); + plan.valueStorage.Reserve(records.size()); for (const auto& record : records) { - auto value = std::make_unique(record.opcode, record.type, record.id); + auto* value = plan.valueStorage.Create(record.opcode, record.type, record.id); value->SetFlags(record.flags); if (record.hasImmediate) value->SetImmediateU64(record.immediate); value->SetRegister(record.registerValue); - plan.valueStorage.push_back(std::move(value)); } bool valid = true; const auto valueAt = [&](std::uint32_t index) -> IrValue* { if (index == NoPlanIndex) return nullptr; - if (index >= plan.valueStorage.size()) { + if (index >= plan.valueStorage.Size()) { valid = false; return nullptr; } - return plan.valueStorage[index].get(); + return &plan.valueStorage[index]; }; for (std::size_t index = 0; index < records.size(); ++index) { const auto& record = records[index]; - auto& value = *plan.valueStorage[index]; + auto& value = plan.valueStorage[index]; + std::vector arguments; + std::vector phiBlocks; + arguments.reserve(record.arguments.size()); for (std::size_t operand = 0; operand < record.arguments.size(); ++operand) { - auto* argument = valueAt(record.arguments[operand]); - if (value.IsPhi()) { - value.AddPhiOperand(plan.blockStorage[record.phiBlocks[operand]].get(), argument); - } else { - value.AddArgument(argument); - } + arguments.push_back(valueAt(record.arguments[operand])); + if (value.IsPhi()) phiBlocks.push_back(plan.blockStorage[record.phiBlocks[operand]].get()); } + value.SetDetachedOperands(std::move(arguments), std::move(phiBlocks)); } - reader.List(plan.memoryInfo, 78, decodeMemoryInfo); - reader.List(plan.descriptorSources, 62, [&](Reader& in, DescriptorSource& source) { + reader.List(plan.memoryInfo, 79, decodeMemoryInfo); + reader.List(plan.descriptorSources, 72, [&](Reader& in, DescriptorSource& source) { for (auto& dword : source.dwords) dword = valueAt(in.Get()); in.Value(source.dwordCount); - const bool indirect = in.Get(); - DescriptorSource::IndirectImage table; - in.Value(table.materialSource); - in.Value(table.heapSource); - in.Value(table.selectorStride); - in.Value(table.selectorOffset); - in.Value(table.keyArg); - in.Value(table.entryOffset); - in.Value(table.hasMaterial); - source.indirectImage = indirect ? std::optional(table) : std::nullopt; + const bool table = in.Get(); + TableColumn column; + in.Value(column.heapSource); + in.Value(column.stride); + in.Value(column.addend); + in.Value(column.offset); + in.Value(column.dwordCount); + in.Value(column.maxKey); + in.Value(column.sampler); + in.Value(column.address); + in.List(column.keyDomains, 13, [](Reader& domains, KeyDomain& domain) { + domains.Value(domain.source); + domains.Value(domain.offset); + domains.Value(domain.stride); + domains.Value(domain.value); + }); + in.Value(column.unkeyed); + in.Value(column.indirect); + in.Value(column.indirectOffset); + in.Value(column.indirectImmediate); + in.List(column.pointerHops, 8, [](Reader& hops, PointerHop& hop) { + hops.Value(hop.offset); + hops.Value(hop.immediate); + }); + 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) { + reader.List(plan.controlFlow, 36, [&](Reader& in, ResourceBlock& block) { block.condition = valueAt(in.Get()); in.Values(block.successors); in.Values(block.sources); in.Values(block.images); + in.Values(block.flatSlots); }); reader.Values(plan.materializationSources); reader.List(plan.srtReads, 8, [&](Reader& in, SrtRead& read) { @@ -1132,6 +1279,7 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { reader.Value(plan.srtGuardOffset); reader.Values(plan.cleanFlatSlots); reader.Values(plan.pureFlatSlots); + reader.Value(plan.denseValueIds); reader.Value(plan.requiresSpecializationMemory); reader.Value(plan.srtPlanComplete); reader.Value(plan.resourceTrackingComplete); @@ -1146,16 +1294,20 @@ 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.indirectImage.has_value() || (knownSource(source.indirectImage->heapSource) && knownSource(source.indirectImage->materialSource))); })) 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; - return std::ranges::all_of(plan.controlFlow, [&](const ResourceBlock& block) { + const bool blocksValid = std::ranges::all_of(plan.controlFlow, [&](const ResourceBlock& block) { return (block.condition == nullptr || block.successors.size() == 2u) && std::ranges::all_of(block.successors, [&](std::uint32_t successor) { return successor < plan.controlFlow.size(); }) && std::ranges::all_of(block.sources, knownSource) && - std::ranges::all_of(block.images, [&](std::uint32_t image) { return image < plan.info.images.size(); }); + std::ranges::all_of(block.images, [&](std::uint32_t image) { return image < plan.info.images.size(); }) && + std::ranges::all_of(block.flatSlots, [&](std::uint32_t slot) { return slot < plan.srtReads.size(); }); }); + if (!blocksValid) return false; + ResourceMaterializer::CompactPlan(plan); + return true; } constexpr std::string_view NeutralSwitches[] = { @@ -1169,6 +1321,8 @@ constexpr std::string_view NeutralSwitches[] = { "APS5_NO_PERF_ITERATION_SELECTION", "APS5_NO_PERF_LAYOUT_PAIR", "APS5_NO_RESULT_MEMO", + "APS5_NO_TABLE_RESOLUTION_REUSE", + "APS5_NO_TABLE_SNAPSHOT_REUSE", }; const std::vector& switchKey() { @@ -1222,20 +1376,27 @@ 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()); - std::thread([this] { run(); }).detach(); + discoverPrebuilt(); + 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(); } [[nodiscard]] const std::filesystem::path& Directory() const { return directory; } bool Load(std::span key, CompiledVariant& variant) { + if (loadPrebuilt(key, variant)) return true; return load(key, [&](std::span file, std::shared_ptr owner) { return DecodeEntry(file, key, variant, std::move(owner)); }, hits, misses, loadFailures); } @@ -1272,6 +1433,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); @@ -1285,16 +1459,123 @@ public: counters.planMisses = planMisses.load(std::memory_order_relaxed); counters.planWrites = planWrites.load(std::memory_order_relaxed); counters.planLoadFailures = planLoadFailures.load(std::memory_order_relaxed); + counters.prebuiltHits = prebuiltHits.load(std::memory_order_relaxed); + counters.prebuiltMisses = prebuiltMisses.load(std::memory_order_relaxed); + counters.prebuiltRejected = prebuiltRejected.load(std::memory_order_relaxed); + counters.lookupNanoseconds = lookupNanoseconds.load(std::memory_order_relaxed); + counters.readNanoseconds = readNanoseconds.load(std::memory_order_relaxed); + counters.decodeNanoseconds = decodeNanoseconds.load(std::memory_order_relaxed); + counters.loadNanoseconds = loadNanoseconds.load(std::memory_order_relaxed); + counters.maxLoadNanoseconds = maxLoadNanoseconds.load(std::memory_order_relaxed); + counters.prebuiltBytesRead = prebuiltBytesRead.load(std::memory_order_relaxed); + counters.prebuiltLoadNanoseconds = prebuiltLoadNanoseconds.load(std::memory_order_relaxed); return counters; } private: + void discoverPrebuilt() { + const auto start = std::chrono::steady_clock::now(); + std::size_t entries = 0; + const auto addPack = [&](const std::filesystem::path& path) { + auto pack = std::make_unique(path); + if (!pack->Valid()) { + std::fprintf(stderr, "[shader-prebuilt] ignored %s: %s\n", path.string().c_str(), pack->Error().c_str()); + return; + } + entries += pack->Count(); + packs.push_back(std::move(pack)); + }; + for (const auto& candidate : PrebuiltCachePaths(root)) { + std::error_code error; +#ifdef _WIN32 + const auto path = NativeCachePath_nid_no_patch(candidate, error); + if (error) continue; +#else + const auto& path = candidate; +#endif + if (std::filesystem::is_regular_file(path, error)) { addPack(path); continue; } + if (!std::filesystem::is_directory(path, error)) continue; + bool loose = false; + std::vector packPaths; + const auto collect = [&](const std::filesystem::path& entry) { + const auto extension = entry.extension(); + if (extension == ".pack") packPaths.push_back(entry); + if (extension == ".bin" && entry.stem().string().size() == 32) loose = true; + }; +#ifdef _WIN32 + WIN32_FIND_DATAW data{}; + const auto handle = FindFirstFileW((path / L"*").c_str(), &data); + if (handle != INVALID_HANDLE_VALUE) { + struct Search { HANDLE handle; ~Search() { FindClose(handle); } } search{handle}; + do { collect(path / data.cFileName); } while (FindNextFileW(handle, &data)); + } +#else + for (std::filesystem::directory_iterator it(path, error), end; !error && it != end; it.increment(error)) collect(it->path()); +#endif + std::sort(packPaths.begin(), packPaths.end()); + for (const auto& item : packPaths) addPack(item); + if (loose) looseDirectories.push_back(path); + const auto versioned = path / hex(SourceVersion()); + if (std::filesystem::is_directory(versioned, error)) looseDirectories.push_back(versioned); + } + if (!packs.empty() || !looseDirectories.empty()) std::fprintf(stderr, "[shader-prebuilt] mapped %zu packs, %zu entries, %zu loose folders in %.3f ms; exact source/ABI/request keys required\n", packs.size(), entries, looseDirectories.size(), std::chrono::duration(std::chrono::steady_clock::now() - start).count()); + } + struct Job { std::vector key; std::shared_ptr variant; std::shared_ptr plan; }; + bool loadPrebuilt(std::span key, CompiledVariant& variant) { + if (packs.empty() && looseDirectories.empty()) return false; + const auto started = std::chrono::steady_clock::now(); + const auto elapsed = [](auto start) { return static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count()); }; + bool loaded = false; + PackLoadTimings timing; + for (const auto& pack : packs) { + const auto status = pack->Load(key, variant, timing); + if (status == LoadStatus::Loaded) { loaded = true; break; } + if (status == LoadStatus::Rejected) prebuiltRejected.fetch_add(1, std::memory_order_relaxed); + } + lookupNanoseconds.fetch_add(timing.lookupNanoseconds, std::memory_order_relaxed); + decodeNanoseconds.fetch_add(timing.decodeNanoseconds, std::memory_order_relaxed); + prebuiltBytesRead.fetch_add(timing.bytes, std::memory_order_relaxed); + if (!loaded) { + const auto name = EntryName(key); + thread_local std::vector* fileSlot = nullptr; + auto& file = ThreadOwned(fileSlot); + for (const auto& path : looseDirectories) { + const auto start = std::chrono::steady_clock::now(); + const auto read = ReadWholeFile(path / name, file); + readNanoseconds.fetch_add(elapsed(start), std::memory_order_relaxed); + if (!read) continue; + const auto decode = std::chrono::steady_clock::now(); + const auto status = DecodeEntry(file, key, variant); + decodeNanoseconds.fetch_add(elapsed(decode), std::memory_order_relaxed); + prebuiltBytesRead.fetch_add(file.size(), std::memory_order_relaxed); + if (status == LoadStatus::Rejected) { + const auto failures = prebuiltRejected.fetch_add(1, std::memory_order_relaxed); + if (failures < 8) std::fprintf(stderr, "[shader-prebuilt] rejected %s (%zu bytes): truncated, corrupt or another format; recompiling\n", name.c_str(), file.size()); + } + if (status == LoadStatus::Loaded) { loaded = true; break; } + } + if (file.capacity() > (4u << 20u)) std::vector().swap(file); + } + if (loaded) { + const auto previous = prebuiltHits.fetch_add(1, std::memory_order_relaxed); + if (previous == 0) std::fprintf(stderr, "[shader-prebuilt] first exact-key hit loaded in %.3f ms\n", elapsed(started) / 1e6); + } else prebuiltMisses.fetch_add(1, std::memory_order_relaxed); + if (loaded) { + const auto duration = elapsed(started); + prebuiltLoadNanoseconds.fetch_add(duration, std::memory_order_relaxed); + auto maximum = maxLoadNanoseconds.load(std::memory_order_relaxed); + while (maximum < duration && !maxLoadNanoseconds.compare_exchange_weak(maximum, duration, std::memory_order_relaxed)) {} + report(false); + } + return loaded; + } + template bool load(std::span key, TDecode&& decode, std::atomic& hitCount, std::atomic& missCount, std::atomic& failureCount) { const auto started = std::chrono::steady_clock::now(); @@ -1338,7 +1619,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(); @@ -1349,7 +1635,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(); @@ -1361,10 +1647,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) { @@ -1384,6 +1672,11 @@ private: const auto stamp = directory / "last-used"; std::ofstream(stamp, std::ios::binary | std::ios::trunc).close(); std::filesystem::last_write_time(stamp, std::filesystem::file_time_type::clock::now(), error); +#ifdef _WIN32 + auto nativeRoot = root; + nativeRoot.make_preferred(); + if (nativeRoot.native().starts_with(L"\\\\?\\") || nativeRoot.native().starts_with(L"\\\\.\\")) return; +#endif const auto now = std::filesystem::file_time_type::clock::now(); for (std::filesystem::directory_iterator it(root, error), end; !error && it != end; it.increment(error)) { const auto name = it->path().filename().string(); @@ -1420,34 +1713,61 @@ private: const auto totals = Totals(); const auto loadNs = loadNanoseconds.load(std::memory_order_relaxed); const auto delta = [](std::uint64_t after, std::uint64_t before) { return static_cast(after - before); }; - if (totals.hits == reported.hits && totals.misses == reported.misses && totals.writes == reported.writes && totals.loadFailures == reported.loadFailures && totals.writeFailures == reported.writeFailures && totals.planHits == reported.planHits && totals.planMisses == reported.planMisses && totals.planWrites == reported.planWrites && totals.planLoadFailures == reported.planLoadFailures) return; - std::fprintf(stderr, "[shader-disk-cache] (10 s): %llu hits, %llu misses, %llu writes (%.1f MiB), %llu load failures, %llu write failures; resource plans: %llu hits, %llu misses, %llu writes, %llu load failures; %.1f MiB read in %.1f ms (totals: %llu hits, %llu misses, %llu writes; plans %llu hits, %llu misses)\n", + const auto baseChanged = totals.hits != reported.hits || totals.misses != reported.misses || totals.writes != reported.writes || totals.loadFailures != reported.loadFailures || totals.writeFailures != reported.writeFailures || totals.planHits != reported.planHits || totals.planMisses != reported.planMisses || totals.planWrites != reported.planWrites || totals.planLoadFailures != reported.planLoadFailures; + const auto prebuiltChanged = totals.prebuiltHits != reported.prebuiltHits || totals.prebuiltMisses != reported.prebuiltMisses || totals.prebuiltRejected != reported.prebuiltRejected; + if (!baseChanged && !prebuiltChanged) return; + if (baseChanged) std::fprintf(stderr, "[shader-disk-cache] (10 s): %llu hits, %llu misses, %llu writes (%.1f MiB), %llu load failures, %llu write failures; resource plans: %llu hits, %llu misses, %llu writes, %llu load failures; %.1f MiB read in %.1f ms (totals: %llu hits, %llu misses, %llu writes; plans %llu hits, %llu misses)\n", delta(totals.hits, reported.hits), delta(totals.misses, reported.misses), delta(totals.writes, reported.writes), static_cast(totals.bytesWritten - reported.bytesWritten) / (1024.0 * 1024.0), delta(totals.loadFailures, reported.loadFailures), delta(totals.writeFailures, reported.writeFailures), delta(totals.planHits, reported.planHits), delta(totals.planMisses, reported.planMisses), delta(totals.planWrites, reported.planWrites), delta(totals.planLoadFailures, reported.planLoadFailures), static_cast(totals.bytesRead - reported.bytesRead) / (1024.0 * 1024.0), static_cast(loadNs - reportedLoadNs) / 1e6, static_cast(totals.hits), static_cast(totals.misses), static_cast(totals.writes), static_cast(totals.planHits), static_cast(totals.planMisses)); + if (prebuiltChanged && (!packs.empty() || !looseDirectories.empty())) std::fprintf(stderr, "[shader-prebuilt] interval: %llu hits, %llu misses, %llu rejected; lookup %.3f ms, file read %.3f ms, decode %.3f ms; total load %.3f ms, max %.3f ms (lifetime hits %llu)\n", + delta(totals.prebuiltHits, reported.prebuiltHits), delta(totals.prebuiltMisses, reported.prebuiltMisses), delta(totals.prebuiltRejected, reported.prebuiltRejected), + (totals.lookupNanoseconds - reported.lookupNanoseconds) / 1e6, (totals.readNanoseconds - reported.readNanoseconds) / 1e6, (totals.decodeNanoseconds - reported.decodeNanoseconds) / 1e6, + (totals.prebuiltLoadNanoseconds - reported.prebuiltLoadNanoseconds) / 1e6, totals.maxLoadNanoseconds / 1e6, static_cast(totals.prebuiltHits)); reported = totals; reportedLoadNs = loadNs; } std::filesystem::path root; std::filesystem::path directory; + std::vector> packs; + std::vector looseDirectories; std::mutex mutex; std::condition_variable wake; 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}; std::atomic lastReport{0}; std::mutex reportMutex; Counters reported; 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; } @@ -1463,6 +1783,7 @@ void BuildKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, s writer.Value(FileMagic); writer.Value(FormatVersion); writer.Value(SourceVersion()); + writer.Value(static_cast(GcnGuestShaders())); thread_local std::vector* memoryKeySlot = nullptr; auto& memoryKey = ThreadOwned(memoryKeySlot); RecompileCacheKey::Build(request, memoryKey); @@ -1483,6 +1804,7 @@ void BuildPlanKey(const RecompileRequest& request, std::uint32_t hostSubgroupSiz writer.Value(FileMagic); writer.Value(FormatVersion); writer.Value(SourceVersion()); + writer.Value(static_cast(GcnGuestShaders())); writer.Value(PlanEntryKind); thread_local std::vector* memoryKeySlot = nullptr; auto& memoryKey = ThreadOwned(memoryKeySlot); @@ -1503,13 +1825,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; } @@ -1604,6 +1928,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 327eb2f30a..e5acfa71f9 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 31; +inline constexpr std::uint32_t FormatVersion = 44; enum class LoadStatus { Loaded, @@ -34,6 +34,16 @@ struct Counters { std::uint64_t planMisses = 0; std::uint64_t planWrites = 0; std::uint64_t planLoadFailures = 0; + std::uint64_t prebuiltHits = 0; + std::uint64_t prebuiltMisses = 0; + std::uint64_t prebuiltRejected = 0; + std::uint64_t lookupNanoseconds = 0; + std::uint64_t readNanoseconds = 0; + std::uint64_t decodeNanoseconds = 0; + std::uint64_t loadNanoseconds = 0; + std::uint64_t maxLoadNanoseconds = 0; + std::uint64_t prebuiltBytesRead = 0; + std::uint64_t prebuiltLoadNanoseconds = 0; }; [[nodiscard]] std::uint64_t SourceVersion(); @@ -43,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); @@ -63,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/SpirvAnalysis.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp index 5c63224c63..ee435ffc67 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp @@ -15,17 +15,22 @@ struct SpirvRequirements { bool subgroupShuffle = false; bool subgroupLocalInvocationId = false; bool computeDerivatives = false; + bool neighbourLaneReads = false; bool imageGatherExtended = false; bool functionLds = false; bool ldsLock = false; + bool ldsBufferInt64Atomics = false; std::uint32_t functionLdsDwords = 0; std::unordered_map functionLdsAddresses; bool functionScratch = false; bool pixelValidMask = false; bool bufferInt64Atomics = false; + bool coherentBufferPairs = false; + bool bufferU64View = false; bool imageInt64Atomics = false; bool sharedInt64Atomics = false; bool float64 = false; + bool float64Interpolation = false; bool coherentBuffers = false; bool coherentBufferAlias = false; std::vector capabilities; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp index 47ffeb796c..bce467f22c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp @@ -27,6 +27,7 @@ bool BdaInvocationsMayStop(const IrProgram& program); void StopBdaInvocationIf(SpirvEmitterState& state, std::uint32_t condition); std::uint32_t BdaInstructionPc(SpirvEmitterState& state, const IrValue& inst); std::uint32_t EmitBdaRead(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t bits); +std::uint32_t EmitBdaReadBytes(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t bytes); void EmitBdaWrite(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t value, std::uint32_t bits = 32u); void EmitBdaStore(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t value, std::uint32_t bits); void DefineBdaByteWriteFunctions(SpirvEmitterState& state); @@ -38,6 +39,15 @@ std::uint32_t EmitBdaAtomic(SpirvValueEmitContext& ctx, const IrValue& inst, std std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword = false, std::uint32_t usedDwords = 0u); bool BdaByteReadsForced(); std::uint32_t AddBdaAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, bool subtract); +bool RoutesApertures(const SpirvEmitterState& state, const MemoryInfo& mem); +std::uint32_t UsedBdaDwords(const IrValue& inst, std::uint32_t dwords); +std::vector BdaReadSites(const SpirvEmitterState& state); +void CollectBdaAnchors(SpirvEmitterState& state); +void DefineBdaAnchorFunction(SpirvEmitterState& state); +void EmitBdaAnchors(SpirvEmitterState& state); +void EmitBdaAnchorRange(SpirvEmitterState& state, SpirvEmitterState::BdaAnchor& anchor, std::uint32_t base); +const SpirvEmitterState::BdaAnchor* FindBdaAnchor(const SpirvEmitterState& state, const IrValue& inst); +std::array EmitAnchoredBdaRead(SpirvEmitterState& state, const IrValue& inst, const SpirvEmitterState::BdaAnchor& anchor, std::uint32_t low, std::int32_t immediate, std::uint32_t bits, std::uint32_t dwords, std::uint32_t usedDwords, const std::function()>& lookup); std::uint32_t AddBdaImmediate(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::int32_t immediate); } diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/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..8c8c56b4ad 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 @@ -21,15 +22,36 @@ struct SpirvTargetOptions { bool nonConstantImageOffsets = false; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; + bool depthRangeRemap = false; bool registrationModules = false; bool fragmentDescriptorSet = false; bool float16DenormPreserve = false; + bool spannedWaves = false; + bool robustBufferAccess2 = false; + bool float32FlushToZero = false; + bool float32DenormPreserve = false; + bool meshBarycentricPixel = false; + bool meshBarycentricVertex = false; + std::uint32_t barycentricSmoothLocation = 0xffffffffu; + std::uint32_t barycentricLinearLocation = 0xffffffffu; + std::uint32_t barycentricPositionsLocation = 0xffffffffu; + std::vector meshExportedLocations; + std::vector> meshPerVertexLocations; + bool waveLdsMemoryBarriers = false; +}; + +struct SpirvEmitInfo { + 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 64699e33b9..96951cf815 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -3,6 +3,7 @@ #include "SpirvBackend/SpirvEmitterState.hpp" #include "Optimization/BindingAllocator.hpp" +#include #include namespace ShaderRecompiler { @@ -12,6 +13,7 @@ IrShaderStage StageOf(const SpirvEmitterState& state); const ShaderVertexInputInfo& VertexInfo(const SpirvEmitterState& state); const ShaderPixelInputInfo& PixelInfo(const SpirvEmitterState& state); const ShaderWorkgroupInputInfo* ShaderWorkgroupInput(const SpirvEmitterState& state); +std::array WorkgroupLocalSize(const SpirvEmitterState& state, const ShaderWorkgroupInputInfo& workgroup); const RdnaImageDimensionInfo& RdnaImageDimensionInfoFor(RdnaImageDimension dimension); std::uint32_t TypeVoid(SpirvEmitterState& state); std::uint32_t TypeBool(SpirvEmitterState& state); @@ -61,9 +63,12 @@ std::uint32_t ImageScalarType(SpirvEmitterState& state, IrTextureNumericClass nu std::uint32_t ImageVectorType(SpirvEmitterState& state, IrTextureNumericClass numericClass, std::uint32_t components); std::uint32_t ImageType(SpirvEmitterState& state, const ImageResource& image); std::uint32_t ImageViewSizeType(SpirvEmitterState& state, RdnaImageDimension dimension); -std::uint32_t LoadSampledImageDescriptor(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t slotId = 0); +std::uint32_t LoadSampledImageDescriptor(SpirvEmitterState& state, std::uint32_t resource); std::uint32_t LoadSamplerDescriptor(SpirvEmitterState& state, std::uint32_t sampler); -std::uint32_t MakeSampledImage(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t sampler, std::uint32_t slotId = 0); +std::uint32_t MakeSampledImage(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t sampler); +void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id); +bool TablePointSampled(const ImageResource& image); +bool TableModeEmitted(const ImageResource& mode); std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint32_t resource); void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample); std::uint32_t ExecutionModelForStage(IrShaderStage stage); @@ -109,11 +114,16 @@ bool LdsInDeviceMemory(const SpirvEmitterState& state); std::uint32_t LdsMemorySemantics(const SpirvEmitterState& state); void EmitLdsBufferBase(SpirvEmitterState& state); std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); +std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index); MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const MemoryInfo& mem); std::uint32_t EmitMemoryElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t rawIndex); std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); bool InFunctionStorage(const SpirvEmitterState& state, ResourceKind kind); std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); +std::uint32_t 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); @@ -266,7 +276,7 @@ std::uint32_t AtomicUpdateTyped(SpirvEmitterState& state, std::uint32_t pointer, EmitLabel(state, cont); state.module.AddFunction(spv::OpBranch, header); EmitLabel(state, merge); - state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, scope), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | memory)); + if (kind != ResourceKind::Lds || !state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, scope), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | memory)); return observed; } diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp index 0ad1aefdb9..a8f03e192a 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -220,6 +220,7 @@ std::uint32_t EmitFPNeg32(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitFPCmpClass32(SpirvEmitterState& state, std::uint32_t value, std::uint32_t mask); void EmitReference(SpirvValueEmitContext& context); void EmitReferenceU32(SpirvValueEmitContext& context); +void EmitUnlinkedCall(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t low, std::uint32_t high, std::uint32_t programCounter); void EmitControlNop(SpirvValueEmitContext& context); void EmitWaitcnt(SpirvValueEmitContext& context); void EmitSendmsg(SpirvValueEmitContext& context); @@ -346,6 +347,8 @@ std::uint32_t EmitSharedAtomicCmpstF64(SpirvValueEmitContext& ctx, const IrValue std::uint32_t EmitSharedAtomicMskor64(SpirvValueEmitContext& ctx, const IrValue& inst); std::uint32_t EmitDataAppend(SpirvValueEmitContext& ctx, const IrValue& inst); std::uint32_t EmitDataConsume(SpirvValueEmitContext& ctx, const IrValue& inst); +std::uint32_t EmitDataOrderedCount(SpirvValueEmitContext& ctx, const IrValue& inst); +std::uint32_t EmitDataOrderedSwap(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageQueryDimensions(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageQueryLod(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageRead(SpirvValueEmitContext& ctx, const IrValue& inst); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index afd26653ac..92ea828d77 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -73,36 +73,67 @@ struct MemoryResourceAccess { std::uint32_t misalignment = 0; }; +struct SpirvTableImageVariable { + DescriptorBindingKind kind = DescriptorBindingKind::Buffers; + std::uint32_t variable = 0; +}; + +struct SpirvTableAccess { + std::uint32_t element = 0; + std::uint32_t dword3 = 0; + std::uint32_t samplerElement = 0; + bool active = false; + bool uniform = false; +}; + struct SpirvEmitterState { const ImageResource* runtimeImage = nullptr; - std::uint32_t runtimeImageMetadata = 0u; + SpirvTableAccess tableAccess; SpirvEmitterState(const IrProgram& program, const ShaderStageInputInfo& inputInfo); SpirvModule module; const IrProgram& program; ShaderStageInputInfo inputInfo; - std::array tessVariables {}; + std::array tessVariables {}; std::uint32_t tessInnerVariable = 0; SpirvRequirements requirements; 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 depthRangeRemap = false; + std::uint32_t depthRemapClipIndex = 0; bool registrationModules = false; bool fragmentDescriptorSet = false; bool float16DenormPreserve = false; + bool robustBufferAccess2 = false; + bool float32FlushToZero = false; + bool float32DenormPreserve = false; + bool meshBarycentricPixel = false; + bool meshBarycentricVertex = false; + std::uint32_t barycentricSmoothLocation = 0xffffffffu; + std::uint32_t barycentricLinearLocation = 0xffffffffu; + std::uint32_t barycentricPositionsLocation = 0xffffffffu; + std::vector meshExportedLocations; + std::vector> meshPerVertexLocations; + std::vector> meshVertexParameters; + std::uint32_t meshPositionStaging = 0; + std::uint32_t meshClipSmooth = 0; + std::uint32_t meshClipLinear = 0; + std::uint32_t meshFlatPositions = 0; + std::uint32_t pixelFlatPositions = 0; + std::map, std::uint32_t> solvedBarycentrics; + bool waveLdsMemoryBarriers = false; std::unordered_set sharedLaneValues; std::uint32_t laneHalf = 0; - // The target's SPIR-V version and what the device accepts, for capabilities an emitter adds - // only when needed (bindless image tables: see TableImageIndex). std::uint32_t spirvVersion = 0x00010300u; std::span supportedCapabilities; std::span supportedExtensions; bool nonConstantImageOffsets = false; - // A bindless table's runtime slot is wave-uniform; it is uniform over the invocation group - // only for a single-wave compute workgroup, elsewhere it needs the NonUniform decoration. bool tableIndexNonUniform = true; std::uint32_t storageBufferVariable = 0; std::uint32_t storageBufferU64Variable = 0; @@ -111,6 +142,7 @@ struct SpirvEmitterState { std::vector memoryByteOffsets; std::map, std::uint32_t> formattedBufferFunctions; std::map, std::uint32_t> formattedGpuBufferFunctions; + std::uint32_t bufferFormatTable = 0; std::uint32_t bdaPagetableVariable = 0; std::uint32_t faultBufferVariable = 0; std::uint32_t bdaPointerFunction = 0; @@ -126,9 +158,20 @@ struct SpirvEmitterState { bool nativeF16ModesEmitted = false; bool f16DenormPreserveEmitted = false; std::uint32_t dot2F32F16Function = 0; + std::map, std::uint32_t> interpolateFunctions; + bool integerInterpolation = false; std::array, 2> bdaDwordReadFunctions {}; std::array, 2> bdaSpanReadFunctions {}; std::unordered_map> bdaGroupedLoads; + struct BdaAnchor { + std::uint32_t lowRegister = 0; + std::uint32_t highRegister = 0; + std::uint32_t size = 0; + std::uint32_t device = 0; + std::uint32_t offset = 0; + }; + std::vector bdaAnchors; + std::uint32_t bdaAnchorFunction = 0; std::uint32_t bdaStopValue = 0; std::uint32_t bdaPcOverride = 0; // False for programs with workgroup barriers: faulting BDA accesses then continue (see BdaInvocationsMayStop). @@ -137,12 +180,14 @@ struct SpirvEmitterState { // Execution scope of the barriers that keep one guest wave's LDS accesses in program order across // host invocations (see WaveLdsScope); 0 when none are emitted. std::uint32_t waveLdsScope = 0; + bool insideLdsLock = false; // Debug aid (APS5_LOOP_GUARD=): after that many evaluated loop exits an invocation leaves its // loops, and the first loop that ran out is reported as a LoopLimit fault instead of hanging the GPU. std::uint32_t loopGuardLimit = 0; std::uint32_t loopGuardVisits = 0; std::uint32_t loopGuardPc = 0; std::uint32_t srgbTableVariable = 0; + std::uint32_t gcnBufferFormatVariable = 0; std::uint32_t gdsVariable = 0; std::uint32_t gdsLength = 0; std::uint32_t pushConstantVariable = 0; @@ -150,14 +195,23 @@ struct SpirvEmitterState { std::uint32_t flattenedSrtVariable = 0; std::uint32_t ldsVariable = 0; std::uint32_t ldsBufferVariable = 0; + std::uint32_t ldsBufferU64Variable = 0; std::uint32_t ldsBufferBase = 0; std::uint32_t numWorkgroupsVariable = 0; std::array scratchVariable {}; std::array vectorWindowVariable {}; + std::uint32_t vectorWindowIndexed = 0; + std::array, 2> vectorWindowSlots {}; std::array imageVariables {}; std::uint32_t samplerVariable = 0; + std::vector tableImageVariables; + std::uint32_t tableSamplerVariable = 0; + std::uint32_t tableMapVariable = 0; std::uint32_t mainFunc = 0; std::uint32_t meshGuestFunc = 0; + std::uint32_t pixelGuestFunc = 0; + std::uint32_t pixelStopFlag = 0; + bool pixelStopStored = false; std::uint32_t meshAllocation = 0; std::uint32_t meshPrimitiveData = 0; std::uint32_t meshPrimitives = 0; @@ -166,7 +220,12 @@ struct SpirvEmitterState { std::uint32_t currentLabel = 0; const IrBlock* currentBlock = nullptr; std::uint32_t pixelValidMaskVariable = 0; + std::uint32_t quadPixelUncovered = 0; + std::uint32_t sampleMaskInputVariable = 0; + std::uint32_t quadSampleCoverage = 0; + std::uint32_t sampleOrderWord = 0; std::uint32_t subgroupLocalInvocationIdVariable = 0; + std::uint32_t subgroupIdVariable = 0; std::uint32_t perVertexVariable = 0; std::uint32_t pointSizeVariable = 0; std::uint32_t clipDistanceVariable = 0; @@ -177,6 +236,7 @@ struct SpirvEmitterState { std::uint32_t depthVariable = 0; std::uint32_t sampleMaskVariable = 0; std::uint32_t stencilRefVariable = 0; + bool stencilReplay = false; std::vector inputs; std::vector outputs; std::vector interfaceVariables; @@ -192,6 +252,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/SpirvBufferAccess.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp index 03bd0330d6..53a851625b 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp @@ -15,6 +15,7 @@ bool LdsInDeviceMemory(const SpirvEmitterState& state); std::uint32_t LdsMemorySemantics(const SpirvEmitterState& state); void EmitLdsBufferBase(SpirvEmitterState& state); std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); +std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index); MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const MemoryInfo& mem); std::uint32_t EmitMemoryElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t rawIndex); std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp index 7f726e995c..b38633af2e 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp @@ -14,9 +14,12 @@ std::uint32_t ImageScalarType(SpirvEmitterState& state, IrTextureNumericClass nu std::uint32_t ImageVectorType(SpirvEmitterState& state, IrTextureNumericClass numericClass, std::uint32_t components); std::uint32_t ImageType(SpirvEmitterState& state, const ImageResource& image); std::uint32_t ImageViewSizeType(SpirvEmitterState& state, RdnaImageDimension dimension); -std::uint32_t LoadSampledImageDescriptor(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t slotId); +std::uint32_t LoadSampledImageDescriptor(SpirvEmitterState& state, std::uint32_t resource); std::uint32_t LoadSamplerDescriptor(SpirvEmitterState& state, std::uint32_t sampler); -std::uint32_t MakeSampledImage(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t sampler, std::uint32_t slotId); +std::uint32_t MakeSampledImage(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t sampler); +void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id); +bool TablePointSampled(const ImageResource& image); +bool TableModeEmitted(const ImageResource& mode); std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint32_t resource); void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample); const RdnaImageDimensionInfo& RdnaImageDimensionInfoFor(RdnaImageDimension dimension); 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/include/SpirvBackend/SpirvModule.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp index 21043b76c5..a6a4b13296 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp @@ -52,9 +52,11 @@ public: [[nodiscard]] std::uint32_t Import(const std::string& name); [[nodiscard]] std::uint32_t DefineGlobalVariable(std::uint32_t pointerType, std::uint32_t storageClass); void DefineGlobalVariable(std::uint32_t id, std::uint32_t pointerType, std::uint32_t storageClass); + [[nodiscard]] std::uint32_t DefineInitializedGlobalVariable(std::uint32_t pointerType, std::uint32_t storageClass, std::uint32_t initializer); void AddMemoryModel(std::uint32_t addressingModel, std::uint32_t memoryModel); void AddName(std::uint32_t target, const std::string& name); void AddFunction(std::span words); + void AddSubgroupBallot(std::uint32_t type, std::uint32_t result, std::uint32_t scope, std::uint32_t predicate); void BeginHelperFunction(); void EndHelperFunction(); [[nodiscard]] SpirvDeferredPhi AddDeferredPhi(std::uint32_t type, std::uint32_t result, std::size_t incomingCount); @@ -140,6 +142,7 @@ public: private: static constexpr std::size_t DeclarationCacheSize = 512; + static constexpr std::size_t DeclarationCacheProbes = 8; static constexpr std::size_t MaxCachedDeclarationWords = 4; struct DeclarationSlot { @@ -161,10 +164,11 @@ private: std::uint32_t interned(const std::uint32_t* words, std::uint32_t length, TDeclare&& declare) { const auto start = declarationCacheSlot(words, length); std::size_t target = start; - for (std::size_t probe = 0; probe < DeclarationCacheSize; ++probe) { - target = (start + probe) & (DeclarationCacheSize - 1u); - auto& slot = declarationCache[target]; + for (std::size_t probe = 0; probe < DeclarationCacheProbes; ++probe) { + const auto index = (start + probe) & (DeclarationCacheSize - 1u); + auto& slot = declarationCache[index]; if (slot.id == 0) { + target = index; break; } if (slot.length == length && std::equal(words, words + length, slot.words.begin())) { @@ -256,6 +260,12 @@ private: std::vector globalVariables; std::vector functionInstructions; std::vector helperFunctionInstructions; + struct BlockBallots { + std::map, std::uint32_t> results; + std::size_t scanned = 0; + }; + BlockBallots ballots; + BlockBallots helperBallots; bool inHelperFunction = false; }; diff --git a/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp index 7c86bc826b..6f86243daf 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" @@ -19,10 +20,15 @@ void require(bool condition, const std::string& message) { if (!condition) throw std::runtime_error("Barycentric geometry SPIR-V: " + message); } -struct PerVertexParameter { - std::uint32_t sourceLocation; - std::uint32_t location; -}; +using PerVertexParameter = EmulatedPerVertexParameter; + +EmulatedInterface CollectEmulatedInterface(const RecompileResult& vertex, const RecompileResult& fragment) { + require(fragment.barycentricEmulation.active, "the fragment shader does not emulate barycentrics"); + for (const auto& parameter : fragment.fragmentParameters) { + require(std::find(vertex.parameterExports.begin(), vertex.parameterExports.end(), parameter.sourceLocation) != vertex.parameterExports.end(), "fragment parameter " + std::to_string(parameter.location) + " is not exported by the vertex shader"); + } + return DescribeEmulatedInterface(fragment.fragmentParameters, fragment.barycentricEmulation); +} class BarycentricGeometryEmitter { public: @@ -140,9 +146,212 @@ 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)); + stageVertexType = perVertexType; + if (clipDistances != 0u) stageVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpMemberDecorate, {1u, spv::DecorationBuiltIn, spv::BuiltInClipDistance}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType, clipArrayType); + stageVertexArrayType = builder.Type(spv::OpTypeArray, stageVertexType, 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, stageVertexArrayType); + std::uint32_t clipIn = 0u; + if (clipDistances != 0u) { + clipIn = addInterface(spv::StorageClassInput, controlInputClipType); + decorate(clipIn, 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, glOut, invocation, constant(intType, 1u)), 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, std::uint32_t provokingVertex) { + begin(); + addDomain(); + const auto tessCoord = addInterface(spv::StorageClassInput, vec3FloatType); + decorate(tessCoord, spv::DecorationBuiltIn, spv::BuiltInTessCoord); + const auto glIn = addInterface(spv::StorageClassInput, stageVertexArrayType); + 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, stageVertexType); + 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, glOut, constant(intType, 1u)), load(clipArrayType, result(spv::OpAccessChain, ptrEvaluationInputClip, glIn, index, constant(intType, 1u)))); + for (std::size_t i = 0; i < passed.size(); ++i) store(passedOutputs[i], load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, inputs[passed[i]], passedFlat[i] ? constant(intType, provokingVertex) : 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 stageVertexType = 0; + std::uint32_t stageVertexArrayType = 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) { +BarycentricEmulationLayout LayoutBarycentricEmulation(std::span parameters, const BarycentricEmulation& emulation, bool meshPositions) { BarycentricEmulationLayout layout; if (!emulation.active) return layout; std::array used {}; @@ -160,45 +369,81 @@ BarycentricEmulationLayout LayoutBarycentricEmulation(std::span parameters, const BarycentricEmulation& emulation, bool meshPositions) { + EmulatedInterface emulated; + emulated.layout = LayoutBarycentricEmulation(parameters, emulation, meshPositions); + for (const auto& parameter : parameters) { + if (!parameter.perVertex) { + emulated.passed.push_back(parameter.sourceLocation); + emulated.passedFlat.push_back(parameter.flat); + continue; + } + const auto relocated = std::find_if(emulated.layout.perVertexLocations.begin(), emulated.layout.perVertexLocations.end(), [&](const auto& entry) { return entry.first == parameter.location; }); + require(relocated != emulated.layout.perVertexLocations.end(), "per-vertex parameter " + std::to_string(parameter.location) + " has no emulated location"); + emulated.perVertex.push_back({parameter.sourceLocation, relocated->second}); + } + emulated.barycentrics = (emulated.layout.smoothLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u) + (emulated.layout.linearLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u); + std::set sources(emulated.passed.begin(), emulated.passed.end()); + for (const auto& parameter : emulated.perVertex) sources.insert(parameter.sourceLocation); + emulated.sources = static_cast(sources.size()); + return emulated; +} + RecompileResult BuildBarycentricGeometryShader(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target, const std::optional& limits) { require(fragment.barycentricEmulation.active, "the fragment shader does not emulate barycentrics"); require(limits.has_value(), "geometry shaders are unavailable"); 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, std::uint32_t provokingVertex) { + require(fragment.barycentricEmulation.active, "the fragment shader does not emulate barycentrics"); + require(provokingVertex == 0u || provokingVertex == 2u, "the provoking vertex must be the first or the last of the triangle"); + require(target.tessellation.has_value(), "tessellation shaders are unavailable"); + require(target.spirvVersion >= 0x00010300u && target.spirvVersion <= 0x00010500u, "unsupported SPIR-V target version"); + require(std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityTessellation) != target.supportedCapabilities.end(), "tessellation capability is unavailable"); + 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, provokingVertex); +#if ANYPS5_ENABLE_SPIRV_TOOLS + shaders.control.spirv = ValidateAndOptimizeSpirv(shaders.control.spirv, target.vulkanVersion, target.spirvVersion); + shaders.evaluation.spirv = ValidateAndOptimizeSpirv(shaders.evaluation.spirv, target.vulkanVersion, target.spirvVersion); +#endif + return shaders; +} + } diff --git a/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp b/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp index 59757fa18c..bbf6dcc0f6 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); @@ -145,10 +165,16 @@ public: const auto x = load(floatType, access(ptrInputFloat, tessCoord, intConstant(0))); const auto y = load(floatType, access(ptrInputFloat, tessCoord, intConstant(1))); - const auto index = result(spv::OpIAdd, intType, result(spv::OpIMul, intType, result(spv::OpConvertFToS, intType, y), intConstant(2)), result(spv::OpConvertFToS, intType, x)); + const auto half = constant(floatType, std::bit_cast(0.5f)); + const auto xHalf = result(spv::OpFAdd, floatType, x, half); + const auto yHalf = result(spv::OpFAdd, floatType, y, half); + const auto xInt = result(spv::OpConvertFToS, intType, xHalf); + const auto yInt = result(spv::OpConvertFToS, intType, yHalf); + const auto index = result(spv::OpIAdd, intType, result(spv::OpIMul, intType, yInt, intConstant(2)), xInt); 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 +290,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 +303,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 +329,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 +350,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 +359,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 +379,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 +397,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 +436,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 +450,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/SpirvAluEmitterFloat64.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp index 241522188c..2cf74380c7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include namespace ShaderRecompiler { namespace { @@ -326,6 +328,142 @@ std::uint32_t Interpolate(SpirvEmitterState& state, std::uint32_t delta, std::ui return halfResult ? FlushDenormal32(state, result, flush16, 0x38800000u) : FlushDenormal32(state, result, flush32, 0x00800000u); } +std::uint32_t InterpolateInteger(SpirvEmitterState& state, std::uint32_t delta, std::uint32_t coordinate, std::uint32_t base, bool halfResult, std::uint32_t mode) { + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + const auto i32 = TypeI32(state); + const auto boolean = TypeBool(state); + const bool quietNans = halfResult || (mode & InterpolationQuiet) != 0u; + const bool flush32 = (mode & InterpolationFlush32) != 0u; + const bool flush16 = (mode & InterpolationFlush16) != 0u; + const std::uint32_t rounding16 = halfResult ? (mode >> InterpolationRound16Shift) & 3u : 0u; + delta = FlushDenormal32(state, delta, flush16, 0x38800000u); + coordinate = FlushDenormal32(state, coordinate, flush32, 0x00800000u); + base = halfResult ? FlushDenormal32(state, base, flush32, 0x00800000u) : FlushDenormal32(state, base, flush16, 0x38800000u); + const auto c32 = [&](std::uint32_t value) { return ConstantU32(state, value); }; + const auto c64 = [&](std::uint64_t value) { return state.module.Constant(spv::OpConstant, u64, static_cast(value), static_cast(value >> 32u)); }; + const auto ci = [&](std::int32_t value) { return ConstantI32(state, value); }; + const auto op = [&](spv::Op opcode, std::uint32_t type, std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, opcode, type, lhs, rhs); }; + const auto pick = [&](std::uint32_t type, std::uint32_t condition, std::uint32_t whenTrue, std::uint32_t whenFalse) { return Select(state, type, condition, whenTrue, whenFalse); }; + const auto all = [&](std::uint32_t lhs, std::uint32_t rhs) { return op(spv::OpLogicalAnd, boolean, lhs, rhs); }; + const auto any = [&](std::uint32_t lhs, std::uint32_t rhs) { return op(spv::OpLogicalOr, boolean, lhs, rhs); }; + const auto not_ = [&](std::uint32_t value) { return Unary(state, spv::OpLogicalNot, boolean, value); }; + const auto msb64 = [&](std::uint32_t value) { + const auto high = Unary(state, spv::OpUConvert, u32, op(spv::OpShiftRightLogical, u64, value, c32(32u))); + const auto low = Unary(state, spv::OpUConvert, u32, value); + return pick(i32, op(spv::OpINotEqual, boolean, high, c32(0u)), op(spv::OpIAdd, i32, Glsl(state, i32, GLSLstd450FindUMsb, high), ci(32)), Glsl(state, i32, GLSLstd450FindUMsb, low)); + }; + struct Decoded { + std::uint32_t sign; + std::uint32_t mantissa; + std::uint32_t exponent; + std::uint32_t zero; + }; + const auto decode = [&](std::uint32_t bits) { + const auto field = op(spv::OpBitwiseAnd, u32, op(spv::OpShiftRightLogical, u32, bits, c32(23u)), c32(0xffu)); + const auto fraction = op(spv::OpBitwiseAnd, u32, bits, c32(0x007fffffu)); + const auto normal = op(spv::OpINotEqual, boolean, field, c32(0u)); + const auto mantissa = Unary(state, spv::OpUConvert, u64, pick(u32, normal, op(spv::OpBitwiseOr, u32, fraction, c32(0x00800000u)), fraction)); + const auto exponent = op(spv::OpISub, i32, Unary(state, spv::OpBitcast, i32, pick(u32, normal, field, c32(1u))), ci(150)); + return Decoded{op(spv::OpShiftRightLogical, u32, bits, c32(31u)), mantissa, exponent, op(spv::OpIEqual, boolean, op(spv::OpBitwiseAnd, u32, bits, c32(0x7fffffffu)), c32(0u))}; + }; + const auto inputs = std::array{Unary(state, spv::OpBitcast, u32, delta), Unary(state, spv::OpBitcast, u32, coordinate), Unary(state, spv::OpBitcast, u32, base)}; + const auto d = decode(inputs[0]); + const auto c = decode(inputs[1]); + const auto b = decode(inputs[2]); + const auto productSign = op(spv::OpBitwiseXor, u32, d.sign, c.sign); + const auto productZero = any(d.zero, c.zero); + const auto normalize = [&](std::uint32_t mantissa, std::uint32_t exponent, std::uint32_t& shiftedExponent) { + const auto shift = op(spv::OpISub, i32, ci(62), msb64(mantissa)); + shiftedExponent = op(spv::OpISub, i32, exponent, shift); + return op(spv::OpShiftLeftLogical, u64, mantissa, shift); + }; + std::uint32_t productExponent = 0; + std::uint32_t baseExponent = 0; + const auto product = normalize(op(spv::OpIMul, u64, d.mantissa, c.mantissa), op(spv::OpIAdd, i32, d.exponent, c.exponent), productExponent); + const auto addend = normalize(b.mantissa, b.exponent, baseExponent); + const auto productLeads = any(b.zero, all(not_(productZero), op(spv::OpSGreaterThanEqual, boolean, productExponent, baseExponent))); + const auto leading = pick(u64, productLeads, product, addend); + const auto trailing = pick(u64, productLeads, pick(u64, b.zero, c64(0u), addend), pick(u64, productZero, c64(0u), product)); + const auto exponent = pick(i32, productLeads, productExponent, baseExponent); + const auto leadingSign = pick(u32, productLeads, productSign, b.sign); + const auto trailingSign = pick(u32, productLeads, b.sign, productSign); + const auto distance = Glsl(state, i32, GLSLstd450SClamp, op(spv::OpISub, i32, exponent, pick(i32, productLeads, baseExponent, productExponent)), ci(0), ci(63)); + const auto lost = op(spv::OpINotEqual, boolean, op(spv::OpBitwiseAnd, u64, trailing, op(spv::OpISub, u64, op(spv::OpShiftLeftLogical, u64, c64(1u), distance), c64(1u))), c64(0u)); + const auto aligned = op(spv::OpBitwiseOr, u64, op(spv::OpShiftRightLogical, u64, trailing, distance), pick(u64, lost, c64(1u), c64(0u))); + const auto sameSign = op(spv::OpIEqual, boolean, leadingSign, trailingSign); + const auto leadingLarger = op(spv::OpUGreaterThanEqual, boolean, leading, aligned); + const auto magnitude = pick(u64, sameSign, op(spv::OpIAdd, u64, leading, aligned), pick(u64, leadingLarger, op(spv::OpISub, u64, leading, aligned), op(spv::OpISub, u64, aligned, leading))); + const auto sign = pick(u32, any(sameSign, leadingLarger), leadingSign, trailingSign); + const std::int32_t precision = halfResult ? 11 : 24; + const std::int32_t smallestStep = halfResult ? -24 : -149; + const auto top = op(spv::OpIAdd, i32, msb64(magnitude), exponent); + const auto step = Glsl(state, i32, GLSLstd450SMax, op(spv::OpISub, i32, top, ci(precision - 1)), ci(smallestStep)); + const auto drop = op(spv::OpISub, i32, step, exponent); + const auto kept = op(spv::OpSLessThanEqual, boolean, drop, ci(0)); + const auto beyond = op(spv::OpSGreaterThan, boolean, drop, ci(64)); + const auto dropped = Glsl(state, i32, GLSLstd450SClamp, drop, ci(1), ci(64)); + const auto whole = op(spv::OpIEqual, boolean, dropped, ci(64)); + const auto shift = Glsl(state, i32, GLSLstd450SMin, dropped, ci(63)); + const auto widened = op(spv::OpShiftLeftLogical, u64, magnitude, Glsl(state, i32, GLSLstd450SClamp, Unary(state, spv::OpSNegate, i32, drop), ci(0), ci(63))); + const auto truncated = pick(u64, whole, c64(0u), op(spv::OpShiftRightLogical, u64, magnitude, shift)); + const auto remainder = pick(u64, whole, magnitude, op(spv::OpBitwiseAnd, u64, magnitude, op(spv::OpISub, u64, op(spv::OpShiftLeftLogical, u64, c64(1u), shift), c64(1u)))); + const auto halfway = op(spv::OpShiftLeftLogical, u64, c64(1u), op(spv::OpISub, i32, dropped, ci(1))); + const auto inexact = all(not_(kept), op(spv::OpINotEqual, boolean, remainder, c64(0u))); + std::uint32_t increment = 0; + if (rounding16 == 0u) { + const auto above = all(not_(beyond), op(spv::OpUGreaterThan, boolean, remainder, halfway)); + const auto tie = all(not_(beyond), op(spv::OpIEqual, boolean, remainder, halfway)); + increment = all(not_(kept), any(above, all(tie, op(spv::OpINotEqual, boolean, op(spv::OpBitwiseAnd, u64, truncated, c64(1u)), c64(0u))))); + } else if (rounding16 == 1u) { + increment = all(inexact, op(spv::OpIEqual, boolean, sign, c32(0u))); + } else if (rounding16 == 2u) { + increment = all(inexact, op(spv::OpIEqual, boolean, sign, c32(1u))); + } else { + increment = ConstantBool(state, false); + } + const auto rounded = Unary(state, spv::OpUConvert, u32, pick(u64, kept, widened, op(spv::OpIAdd, u64, truncated, pick(u64, increment, c64(1u), c64(0u))))); + const auto roundedTop = Glsl(state, i32, GLSLstd450FindUMsb, rounded); + const auto biased = op(spv::OpIAdd, i32, op(spv::OpIAdd, i32, roundedTop, step), ci(127)); + const auto normalFraction = pick(u32, op(spv::OpSGreaterThan, boolean, roundedTop, ci(23)), + op(spv::OpShiftRightLogical, u32, rounded, Glsl(state, i32, GLSLstd450SClamp, op(spv::OpISub, i32, roundedTop, ci(23)), ci(0), ci(31))), + op(spv::OpShiftLeftLogical, u32, rounded, Glsl(state, i32, GLSLstd450SClamp, op(spv::OpISub, i32, ci(23), roundedTop), ci(0), ci(31)))); + const auto normalBits = op(spv::OpBitwiseOr, u32, op(spv::OpShiftLeftLogical, u32, Unary(state, spv::OpBitcast, u32, Glsl(state, i32, GLSLstd450SClamp, biased, ci(1), ci(254))), c32(23u)), op(spv::OpBitwiseAnd, u32, normalFraction, c32(0x007fffffu))); + auto unsignedBits = pick(u32, op(spv::OpSGreaterThanEqual, boolean, biased, ci(1)), normalBits, rounded); + unsignedBits = pick(u32, op(spv::OpSGreaterThanEqual, boolean, biased, ci(255)), c32(0x7f800000u), unsignedBits); + unsignedBits = pick(u32, op(spv::OpIEqual, boolean, rounded, c32(0u)), c32(0u), unsignedBits); + if (halfResult) { + if (rounding16 == 0u) { + unsignedBits = pick(u32, op(spv::OpUGreaterThan, boolean, unsignedBits, c32(0x47800000u)), c32(0x47800000u), unsignedBits); + } else { + auto limit = c32(0x477fe000u); + const auto negative = op(spv::OpIEqual, boolean, sign, c32(1u)); + if (rounding16 == 1u) limit = pick(u32, negative, c32(0x477fe000u), c32(0x47800000u)); + if (rounding16 == 2u) limit = pick(u32, negative, c32(0x47800000u), c32(0x477fe000u)); + unsignedBits = pick(u32, op(spv::OpUGreaterThan, boolean, unsignedBits, c32(0x477fe000u)), limit, unsignedBits); + } + } + const auto bothZero = all(productZero, b.zero); + const auto zeroSign = rounding16 == 2u ? pick(u32, bothZero, op(spv::OpBitwiseOr, u32, productSign, b.sign), c32(1u)) : pick(u32, bothZero, op(spv::OpBitwiseAnd, u32, productSign, b.sign), c32(0u)); + const auto exactZero = op(spv::OpIEqual, boolean, magnitude, c64(0u)); + const auto finite = op(spv::OpBitwiseOr, u32, pick(u32, exactZero, c32(0u), unsignedBits), op(spv::OpShiftLeftLogical, u32, pick(u32, exactZero, zeroSign, sign), c32(31u))); + const auto infinite = [&](std::uint32_t bits) { return op(spv::OpIEqual, boolean, op(spv::OpBitwiseAnd, u32, bits, c32(0x7fffffffu)), c32(0x7f800000u)); }; + const auto invalidProduct = any(all(infinite(inputs[0]), c.zero), all(d.zero, infinite(inputs[1]))); + const auto productNan = any(any(Nan32(state, inputs[0]), Nan32(state, inputs[1])), invalidProduct); + const auto productInfinite = all(not_(productNan), any(infinite(inputs[0]), infinite(inputs[1]))); + const auto sumNan = any(any(productNan, Nan32(state, inputs[2])), all(all(productInfinite, infinite(inputs[2])), op(spv::OpINotEqual, boolean, productSign, b.sign))); + const auto infinitySign = pick(u32, productInfinite, productSign, b.sign); + auto special = pick(u32, sumNan, c32(0xffc00000u), op(spv::OpBitwiseOr, u32, op(spv::OpShiftLeftLogical, u32, infinitySign, c32(31u)), c32(0x7f800000u))); + auto nonFinite = NonFinite32(state, inputs[0]); + for (const auto input : {inputs[1], inputs[2]}) nonFinite = any(nonFinite, NonFinite32(state, input)); + const auto quiet = [&](std::uint32_t bits) { return quietNans ? op(spv::OpBitwiseOr, u32, bits, c32(0x00400000u)) : bits; }; + special = pick(u32, all(Nan32(state, inputs[2]), not_(invalidProduct)), quiet(inputs[2]), special); + special = pick(u32, Nan32(state, inputs[1]), quiet(inputs[1]), special); + special = pick(u32, Nan32(state, inputs[0]), quiet(inputs[0]), special); + const auto result = Unary(state, spv::OpBitcast, TypeF32(state), pick(u32, nonFinite, special, finite)); + return halfResult ? FlushDenormal32(state, result, flush16, 0x38800000u) : FlushDenormal32(state, result, flush32, 0x00800000u); +} + std::uint32_t FusedMultiplyAdd(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2, std::uint32_t scale) { const auto a = Classify(state, arg0); const auto b = Classify(state, arg1); @@ -806,12 +944,52 @@ std::uint32_t EmitFPDot2F32F16(SpirvEmitterState& state, std::uint32_t arg0, std }); } +namespace { + +std::uint32_t InterpolateFunction(SpirvEmitterState& state, bool halfResult, std::uint32_t mode) { + const auto key = std::make_pair(halfResult, mode); + if (const auto found = state.interpolateFunctions.find(key); found != state.interpolateFunctions.end()) return found->second; + const auto f32 = TypeF32(state); + const auto id = state.module.AllocateId(); + struct Restore { + SpirvEmitterState& state; + std::uint32_t label; + ~Restore() { + state.module.EndHelperFunction(); + state.currentLabel = label; + } + }; + state.module.BeginHelperFunction(); + const Restore restore{state, state.currentLabel}; + state.module.AddName(id, std::string(halfResult ? "interpolate_f16_mode_" : "interpolate_f32_mode_") + std::to_string(mode)); + state.module.AddFunction(spv::OpFunction, f32, id, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, f32, f32, f32, f32)); + std::array parameters{}; + for (auto& parameter : parameters) { + parameter = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, f32, parameter); + } + EmitLabel(state, state.module.AllocateId()); + state.module.AddFunction(spv::OpReturnValue, (state.integerInterpolation ? InterpolateInteger : Interpolate)(state, parameters[0], parameters[1], parameters[2], halfResult, mode)); + state.module.AddFunction(spv::OpFunctionEnd); + state.interpolateFunctions.emplace(key, id); + return id; +} + +std::uint32_t CallInterpolate(SpirvEmitterState& state, std::uint32_t delta, std::uint32_t coordinate, std::uint32_t base, bool halfResult, std::uint32_t mode) { + const auto function = InterpolateFunction(state, halfResult, mode); + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, TypeF32(state), result, function, delta, coordinate, base); + return result; +} + +} + std::uint32_t EmitFPInterpolateF32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2, const IrValue* mode) { - return Interpolate(state, arg0, arg1, arg2, false, mode->ImmediateU32()); + return CallInterpolate(state, arg0, arg1, arg2, false, mode->ImmediateU32()); } std::uint32_t EmitFPInterpolateF16(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2, const IrValue* mode) { - return Interpolate(state, arg0, arg1, arg2, true, mode->ImmediateU32()); + return CallInterpolate(state, arg0, arg1, arg2, true, mode->ImmediateU32()); } std::uint32_t EmitConvertF32F64(SpirvEmitterState& state, std::uint32_t arg0) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index 757a979fa5..0f4f8ac116 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -186,11 +187,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 +296,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]); } @@ -321,75 +323,127 @@ public: explicit EntryLaneWalk(IrShaderStage stage) : everyInvocationEnters(EveryInvocationEnters(stage)) {} bool ZeroBit(const IrValue* bit) { - bit = Resolved(bit); - if (bit == nullptr || !Spend()) return false; - if (bit->HasImmediate()) return bit->Type() == IrType::Bool && !bit->ImmediateBool(); - if (Is(bit, IrOpcode::LogicalAnd)) return HelperOrFalseConjunct(bit) || ZeroBit(bit->Argument(0)) || ZeroBit(bit->Argument(1)); - if (Is(bit, IrOpcode::SelectU1)) return ZeroBit(bit->Argument(1)) && ZeroBit(bit->Argument(2)); - if (Is(bit, IrOpcode::Phi)) return Incoming(bit, [this](const IrValue* incoming) { return ZeroBit(incoming); }); - if (Is(bit, IrOpcode::IEqual32)) return HelperTest(bit); - const auto words = LaneBitWords(bit); - return words && Word((*words)[0], 0u) && Word((*words)[1], 1u); + return Prove(bit, Proof::ZeroBit, [this](const IrValue* value) { + if (value->HasImmediate()) return value->Type() == IrType::Bool && !value->ImmediateBool(); + if (Is(value, IrOpcode::LogicalAnd)) return HelperOrFalseConjunct(value) || ZeroBit(value->Argument(0)) || ZeroBit(value->Argument(1)); + if (Is(value, IrOpcode::SelectU1)) return ZeroBit(value->Argument(1)) && ZeroBit(value->Argument(2)); + if (Is(value, IrOpcode::Phi)) return Incoming(value, [this](const IrValue* incoming) { return ZeroBit(incoming); }); + if (Is(value, IrOpcode::IEqual32)) return HelperTest(value); + const auto words = LaneBitWords(value); + return words && Word((*words)[0], 0u) && Word((*words)[1], 1u); + }); } private: + enum class Proof : std::uint8_t { ZeroBit, NotHelper, LowWord, HighWord, LowWord64, HighWord64 }; + using Key = std::pair; + struct Assumption { + std::size_t depth; + std::uint64_t order; + }; + static constexpr std::size_t Unbounded = static_cast(-1); + bool Spend() { if (budget == 0u) return false; --budget; return true; } - bool NotHelper(const IrValue* predicate) { - predicate = Resolved(predicate); - if (predicate == nullptr || !Spend()) return false; - if (predicate->HasImmediate()) return everyInvocationEnters && predicate->Type() == IrType::Bool && predicate->ImmediateBool(); - switch (predicate->Opcode()) { - case IrOpcode::LogicalAnd: return NotHelper(predicate->Argument(0)) || NotHelper(predicate->Argument(1)); - case IrOpcode::IEqual32: return HelperTest(predicate); - default: return ZeroBit(predicate); + template + bool Prove(const IrValue* value, Proof proof, TCheck&& check) { + value = Resolved(value); + if (value == nullptr) return false; + const Key key{value, proof}; + if (const auto found = settled.find(key); found != settled.end()) return found->second; + if (const auto found = active.find(key); found != active.end()) return Assume(found->second); + if (const auto found = assumed.find(key); found != assumed.end()) return Assume(found->second.depth); + if (!Spend()) return false; + const auto outer = dependency; + const auto depth = active.size(); + const auto mark = created; + dependency = Unbounded; + active.emplace(key, depth); + const bool result = check(value); + active.erase(key); + const auto inner = dependency; + const bool holds = result && inner >= depth; + for (auto entry = assumed.begin(); entry != assumed.end();) { + if (entry->second.order < mark) { + ++entry; + continue; + } + if (result && !holds) { + entry->second.depth = std::min(entry->second.depth, inner); + ++entry; + continue; + } + if (holds) settled.emplace(entry->first, true); + entry = assumed.erase(entry); } + if (result && !holds) assumed.emplace(key, Assumption{inner, created++}); + else settled.emplace(key, result); + dependency = result && !holds ? std::min(outer, inner) : outer; + return result; + } + + bool Assume(std::size_t depth) { + dependency = std::min(dependency, depth); + return true; + } + + bool NotHelper(const IrValue* predicate) { + return Prove(predicate, Proof::NotHelper, [this](const IrValue* value) { + if (value->HasImmediate()) return everyInvocationEnters && value->Type() == IrType::Bool && value->ImmediateBool(); + switch (value->Opcode()) { + case IrOpcode::LogicalAnd: return NotHelper(value->Argument(0)) || NotHelper(value->Argument(1)); + case IrOpcode::IEqual32: return HelperTest(value); + default: return ZeroBit(value); + } + }); } bool Word(const IrValue* word, std::uint32_t half) { - word = Resolved(word); - if (word == nullptr || !Spend()) return false; - if (word->HasImmediate()) return IsU32(word, 0u); - switch (word->Opcode()) { - case IrOpcode::CompositeExtractU32x4: { - const auto* ballot = Resolved(word->Argument(0)); - return IsU32(word->Argument(1), half) && Is(ballot, IrOpcode::Ballot) && NotHelper(ballot->Argument(0)); + return Prove(word, half == 0u ? Proof::LowWord : Proof::HighWord, [this, half](const IrValue* value) { + if (value->HasImmediate()) return IsU32(value, 0u); + switch (value->Opcode()) { + case IrOpcode::CompositeExtractU32x4: { + const auto* ballot = Resolved(value->Argument(0)); + return IsU32(value->Argument(1), half) && Is(ballot, IrOpcode::Ballot) && NotHelper(ballot->Argument(0)); + } + case IrOpcode::CompositeExtractU64: return IsU32(value->Argument(1), half) && Word64(value->Argument(0), half); + case IrOpcode::BitwiseAnd32: return Word(value->Argument(0), half) || Word(value->Argument(1), half); + case IrOpcode::BitwiseOr32: return Word(value->Argument(0), half) && Word(value->Argument(1), half); + case IrOpcode::Phi: return Incoming(value, [this, half](const IrValue* incoming) { return Word(incoming, half); }); + default: return false; } - case IrOpcode::CompositeExtractU64: return IsU32(word->Argument(1), half) && Word64(word->Argument(0), half); - case IrOpcode::BitwiseAnd32: return Word(word->Argument(0), half) || Word(word->Argument(1), half); - case IrOpcode::BitwiseOr32: return Word(word->Argument(0), half) && Word(word->Argument(1), half); - case IrOpcode::Phi: return Incoming(word, [this, half](const IrValue* incoming) { return Word(incoming, half); }); - default: return false; - } + }); } bool Word64(const IrValue* mask, std::uint32_t half) { - mask = Resolved(mask); - if (mask == nullptr || !Spend()) return false; - if (mask->HasImmediate()) return mask->Type() == IrType::U64 && static_cast(mask->ImmediateU64() >> (32u * half)) == 0u; - switch (mask->Opcode()) { - case IrOpcode::CompositeConstructU64: return Word(mask->Argument(half), half); - case IrOpcode::Phi: return Incoming(mask, [this, half](const IrValue* incoming) { return Word64(incoming, half); }); - default: return false; - } + return Prove(mask, half == 0u ? Proof::LowWord64 : Proof::HighWord64, [this, half](const IrValue* value) { + if (value->HasImmediate()) return value->Type() == IrType::U64 && static_cast(value->ImmediateU64() >> (32u * half)) == 0u; + switch (value->Opcode()) { + case IrOpcode::CompositeConstructU64: return Word(value->Argument(half), half); + case IrOpcode::Phi: return Incoming(value, [this, half](const IrValue* incoming) { return Word64(incoming, half); }); + default: return false; + } + }); } template - bool Incoming(const IrValue* phi, TCheck&& check) { - if (!phis.insert(phi).second) return true; + static bool Incoming(const IrValue* phi, TCheck&& check) { bool result = phi->ArgumentCount() != 0u; for (std::size_t index = 0; result && index < phi->ArgumentCount(); ++index) result = check(phi->Argument(index)); - phis.erase(phi); return result; } bool everyInvocationEnters; - std::unordered_set phis; - std::uint32_t budget = 512u; + std::map active; + std::map assumed; + std::map settled; + std::size_t dependency = Unbounded; + std::uint64_t created = 0u; + std::uint32_t budget = 2048u; }; bool EveryLaneBit(const IrValue* bit, bool constantTrue) { @@ -566,7 +620,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 +651,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) { @@ -608,9 +725,7 @@ std::uint32_t EmitConvertU8U32(SpirvEmitterState& state, std::uint32_t arg0) { } std::uint32_t EmitConvertF16F32(SpirvEmitterState& state, std::uint32_t arg0) { - const auto pair = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 2), pair, arg0, ConstantF32(state, 0u)); - return EmitPackHalf2x16(state, pair); + return EmitF32ToF16BitsRte(state, arg0); } std::uint32_t EmitConvertS32F32(SpirvEmitterState& state, std::uint32_t arg0) { @@ -969,7 +1084,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 +1148,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 +1195,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 +1223,9 @@ std::uint32_t EmitPermuteU32(SpirvValueEmitContext& ctx, const IrValue& inst) { auto result = ConstantU32(state, 0u); for (std::uint32_t source = 0; source < 32u; ++source) { const auto from = Binary(state, spv::OpBitwiseOr, TypeU32(state), base, ConstantU32(state, source)); - const auto value = ctx.Shuffle(inst, 0, from); - const auto address = ctx.Shuffle(inst, 1, from); - const auto active = ctx.Shuffle(inst, 2, from); + const auto value = ctx.ShuffleInHalf(inst, 0, from); + const auto address = ctx.ShuffleInHalf(inst, 1, from); + const auto active = ctx.ShuffleInHalf(inst, 2, from); const auto target = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)), ConstantU32(state, 31u)); const auto hit = Binary(state, spv::OpLogicalAnd, TypeBool(state), active, Binary(state, spv::OpIEqual, TypeBool(state), target, own)); result = Select(state, TypeU32(state), hit, value, result); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp index f24270d401..f38c0fe047 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp @@ -1,7 +1,10 @@ #include "SpirvBackend/SpirvAluEmitter.hpp" #include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvEmitterInstructions.hpp" +#include +#include #include +#include namespace ShaderRecompiler { @@ -45,21 +48,89 @@ void EmitSetVectorRegister(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitUnreachable(ctx, inst); } -std::uint32_t VectorWindowElement(SpirvValueEmitContext& ctx, const IrValue& inst) { - auto& state = ctx.state; +namespace { + +std::uint32_t VectorWindowElement(SpirvEmitterState& state, std::uint32_t index) { const auto pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), pointer, state.vectorWindowVariable.at(state.laneHalf), ctx.Arg(inst, 0u)); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), pointer, state.vectorWindowVariable.at(state.laneHalf), index); return pointer; } +std::uint32_t AccessNarrowedWindow(SpirvEmitterState& state, std::uint32_t index, const std::function& access) { + const auto& slots = state.vectorWindowSlots.at(state.laneHalf); + const auto arrayLabel = state.module.AllocateId(); + const auto slotsLabel = state.module.AllocateId(); + const auto slotsMerge = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + const auto inArray = Binary(state, spv::OpULessThan, TypeBool(state), index, ConstantU32(state, state.vectorWindowIndexed)); + state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, inArray, arrayLabel, slotsLabel); + EmitLabel(state, arrayLabel); + const auto fromArray = access(VectorWindowElement(state, index)); + const auto arrayExit = state.currentLabel; + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, slotsLabel); + std::vector labels(slots.size()); + std::vector words{spv::OpSwitch, index, slotsMerge}; + for (std::size_t slot = 0; slot < slots.size(); ++slot) { + labels[slot] = state.module.AllocateId(); + words.push_back(state.vectorWindowIndexed + static_cast(slot)); + words.push_back(labels[slot]); + } + state.module.AddFunction(spv::OpSelectionMerge, slotsMerge, spv::SelectionControlMaskNone); + state.module.AddFunction(std::span(words)); + std::vector incoming; + for (std::size_t slot = 0; slot < slots.size(); ++slot) { + EmitLabel(state, labels[slot]); + incoming.push_back(access(slots[slot])); + incoming.push_back(labels[slot]); + state.module.AddFunction(spv::OpBranch, slotsMerge); + } + incoming.push_back(ConstantU32(state, 0u)); + incoming.push_back(slotsLabel); + EmitLabel(state, slotsMerge); + const auto fromSlots = state.module.AllocateId(); + std::vector phi{spv::OpPhi, TypeU32(state), fromSlots}; + phi.insert(phi.end(), incoming.begin(), incoming.end()); + state.module.AddFunction(std::span(phi)); + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, merge); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, TypeU32(state), value, fromArray, arrayExit, fromSlots, slotsMerge); + return value; +} + +std::uint32_t DirectWindowElement(SpirvValueEmitContext& ctx, const IrValue& inst) { + auto& state = ctx.state; + const IrValue* index = inst.Argument(0)->Resolve(); + if (state.vectorWindowIndexed == 0u) return VectorWindowElement(state, ctx.Arg(inst, 0u)); + if (!index->HasImmediate()) return 0u; + if (index->ImmediateU32() < state.vectorWindowIndexed) return VectorWindowElement(state, ctx.Arg(inst, 0u)); + return state.vectorWindowSlots.at(state.laneHalf).at(index->ImmediateU32() - state.vectorWindowIndexed); +} + +} + void EmitLoadVectorWindow(SpirvValueEmitContext& ctx, const IrValue& inst) { - const auto value = ctx.state.module.AllocateId(); - ctx.state.module.AddFunction(spv::OpLoad, TypeU32(ctx.state), value, VectorWindowElement(ctx, inst)); - ctx.Define(inst, value); + auto& state = ctx.state; + const auto load = [&state](std::uint32_t pointer) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + return value; + }; + const auto pointer = DirectWindowElement(ctx, inst); + ctx.Define(inst, pointer != 0u ? load(pointer) : AccessNarrowedWindow(state, ctx.Arg(inst, 0u), load)); } void EmitStoreVectorWindow(SpirvValueEmitContext& ctx, const IrValue& inst) { - ctx.state.module.AddFunction(spv::OpStore, VectorWindowElement(ctx, inst), ctx.Arg(inst, 1u)); + auto& state = ctx.state; + const auto value = ctx.Arg(inst, 1u); + const auto store = [&state, value](std::uint32_t pointer) { + state.module.AddFunction(spv::OpStore, pointer, value); + return ConstantU32(state, 0u); + }; + if (const auto pointer = DirectWindowElement(ctx, inst); pointer != 0u) (void)store(pointer); + else (void)AccessNarrowedWindow(state, ctx.Arg(inst, 0u), store); } void EmitGetGotoVariable(SpirvValueEmitContext& ctx, const IrValue& inst) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index dd83663928..6514d92cc1 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvAnalysis.hpp" +#include "RdnaDecoder/RdnaInstruction.hpp" #include #include #include @@ -9,6 +10,30 @@ namespace ShaderRecompiler { namespace { +bool ReadsNeighbourLanes(const IrProgram& program, const IrValue& inst) { + switch (inst.Opcode()) { + case IrOpcode::ImageQueryLod: + case IrOpcode::DppMoveU32: + case IrOpcode::DppUpdateU32: + case IrOpcode::SwizzleU32: + case IrOpcode::BpermuteU32: + case IrOpcode::PermuteU32: + case IrOpcode::Permlane16U32: + case IrOpcode::ReadLane: + case IrOpcode::ReadFirstLane: + case IrOpcode::Ballot: + case IrOpcode::WqmU64: + return true; + case IrOpcode::ImageSampleImplicitLod: + case IrOpcode::ImageSampleRaw: { + const auto flags = program.Resources().memoryInfo.at(inst.Flags().index).imageSampleFlags; + return (flags & (RdnaImageSampleFlagDerivative | RdnaImageSampleFlagLod | RdnaImageSampleFlagLevelZero)) == 0u; + } + default: + return false; + } +} + bool UniformSource(const IrValue& value) { switch (value.Opcode()) { case IrOpcode::Ballot: @@ -49,6 +74,8 @@ bool LaneSource(const IrProgram& program, const IrValue& value) { case IrOpcode::DataAppend: case IrOpcode::DataConsume: case IrOpcode::LoadVectorWindow: + case IrOpcode::DataOrderedCount: + case IrOpcode::DataOrderedSwap: return true; case IrOpcode::GetBuiltin: { const auto* kind = value.Argument(0)->Resolve(); @@ -318,7 +345,9 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { if (IsImageAtomic64Opcode(inst->Opcode())) { requirements.imageInt64Atomics = true; } - if (IsFloat64Opcode(inst->Opcode())) { + if (inst->Opcode() == IrOpcode::FPInterpolateF32 || inst->Opcode() == IrOpcode::FPInterpolateF16) { + requirements.float64Interpolation = true; + } else if (IsFloat64Opcode(inst->Opcode())) { requirements.float64 = true; } if (addressAccess != AddressAccess::None) { @@ -347,6 +376,8 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { } const auto& memory = program.Resources().memoryInfo.at(memoryIndex); requirements.coherentBuffers = requirements.coherentBuffers || (memory.coherent && !memory.gpuDescriptor); + const bool pair = inst->Opcode() == IrOpcode::LoadBufferU32x2 || inst->Opcode() == IrOpcode::StoreBufferU32x2; + requirements.coherentBufferPairs = requirements.coherentBufferPairs || (pair && memory.coherent && !memory.gpuDescriptor); if (!memory.planningOnly) { requirements.subgroupLocalInvocationId = requirements.subgroupLocalInvocationId || program.Resources().stage == IrShaderStage::Compute; } @@ -376,6 +407,7 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { requirements.subgroupLocalInvocationId = true; } } + requirements.neighbourLaneReads = requirements.neighbourLaneReads || ReadsNeighbourLanes(program, *inst); switch (inst->Opcode()) { case IrOpcode::Ballot: requirements.subgroupBallot = true; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp index 4b0d60d921..53f65c7c1c 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp @@ -1,6 +1,8 @@ #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" +#include +#include #include namespace ShaderRecompiler { @@ -50,6 +52,10 @@ void DefineBdaFaultFunction(SpirvEmitterState& state) { const auto previous = state.module.AllocateId(); const auto scope = ConstantU32(state, spv::ScopeDevice); const auto relaxed = ConstantU32(state, spv::MemorySemanticsMaskNone); + const auto notUnaligned = Binary(state, spv::OpINotEqual, TypeBool(state), reason, ConstantU32(state, static_cast(BdaAbi::FaultReason::Unaligned))); + EmitIfCondition(state, notUnaligned, [&] { + state.module.AddFunction(spv::OpAtomicOr, u32, state.module.AllocateId(), BdaWord(state, state.faultBufferVariable, ConstantU32(state, static_cast(offsetof(BdaAbi::Fault, otherFaults) / sizeof(std::uint32_t)))), scope, relaxed, ConstantU32(state, 1u)); + }); state.module.AddFunction(spv::OpAtomicCompareExchange, TypeU32(state), previous, pointer, scope, relaxed, relaxed, ConstantU32(state, static_cast(BdaAbi::FaultState::Writing)), ConstantU32(state, 0u)); const auto won = Binary(state, spv::OpIEqual, TypeBool(state), previous, ConstantU32(state, 0u)); EmitIfCondition(state, won, [&] { @@ -62,7 +68,6 @@ void DefineBdaFaultFunction(SpirvEmitterState& state) { store(4, bytes); store(5, ConstantU32(state, static_cast(state.program.Resources().stage))); store(6, instruction); - store(7, ConstantU32(state, 0u)); const auto release = ConstantU32(state, spv::MemorySemanticsReleaseMask | spv::MemorySemanticsUniformMemoryMask); state.module.AddFunction(spv::OpAtomicStore, pointer, scope, release, ConstantU32(state, static_cast(BdaAbi::FaultState::Ready))); }); @@ -115,7 +120,18 @@ 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 (state.pixelStopFlag != 0u) { + state.module.AddFunction(spv::OpStore, state.pixelStopFlag, ConstantBool(state, true)); + state.module.AddFunction(spv::OpReturn); + state.pixelStopStored = true; + } 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/SpirvBdaLookup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp index b48e8775b5..d1889a85fe 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp @@ -5,23 +5,14 @@ #include #include #include +#include namespace ShaderRecompiler { namespace { -void ReturnBdaZeroIf(SpirvEmitterState& state, std::uint32_t condition) { - const auto failed = state.module.AllocateId(); - const auto next = state.module.AllocateId(); - state.module.AddFunction(spv::OpSelectionMerge, next, spv::SelectionControlMaskNone); - state.module.AddFunction(spv::OpBranchConditional, condition, failed, next); - EmitLabel(state, failed); - state.module.AddFunction(spv::OpReturnValue, BdaConstant(state, 0u)); - EmitLabel(state, next); -} - // (u64 address, u32 bytes, u32 instruction) -> u64 device address of the range holding all the bytes, -std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool recordFaults, std::uint32_t permission = BdaAbi::Read) { +std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool recordFaults, std::uint32_t permission = BdaAbi::Read, bool rangeResult = false) { const auto u32 = TypeU32(state); const auto u64 = TypeScalarU64(state); const auto boolean = TypeBool(state); @@ -29,7 +20,10 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r const auto binary = [&](std::uint32_t op, std::uint32_t type, std::uint32_t left, std::uint32_t right) { return Binary(state, op, type, left, right); }; const auto function = state.module.AllocateId(); state.module.AddName(function, name); - state.module.AddFunction(spv::OpFunction, u64, function, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, u64, u64, u32, u32)); + const auto rangeType = rangeResult ? state.module.Type(spv::OpTypeVector, u64, 3u) : 0u; + const auto resultType = rangeResult ? rangeType : u64; + const auto zero = rangeResult ? state.module.Constant(spv::OpConstantComposite, rangeType, BdaConstant(state, 0u), BdaConstant(state, 0u), BdaConstant(state, 0u)) : BdaConstant(state, 0u); + state.module.AddFunction(spv::OpFunction, resultType, function, rangeResult ? spv::FunctionControlMaskNone : spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, resultType, u64, u32, u32)); const auto address = state.module.AllocateId(); const auto bytes = state.module.AllocateId(); const auto instruction = state.module.AllocateId(); @@ -37,31 +31,38 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r state.module.AddFunction(spv::OpFunctionParameter, u32, bytes); state.module.AddFunction(spv::OpFunctionParameter, u32, instruction); EmitLabel(state, state.module.AllocateId()); - const auto fail = [&](std::uint32_t condition, BdaAbi::FaultReason reason) { - if (recordFaults) { - ReturnBdaFailureIf(state, condition, address, bytes, instruction, reason); - } else { - ReturnBdaZeroIf(state, condition); - } + const auto outer = state.module.AllocateId(); + const auto outerBody = state.module.AllocateId(); + const auto outerContinue = state.module.AllocateId(); + const auto outerMerge = state.module.AllocateId(); + state.module.AddFunction(spv::OpBranch, outer); + EmitLabel(state, outer); + state.module.AddFunction(spv::OpLoopMerge, outerMerge, outerContinue, spv::LoopControlMaskNone); + state.module.AddFunction(spv::OpBranch, outerBody); + EmitLabel(state, outerBody); + std::vector exits{spv::OpPhi, resultType, state.module.AllocateId()}; + const auto leave = [&](std::uint32_t value) { + exits.push_back(value); + exits.push_back(state.currentLabel); + state.module.AddFunction(spv::OpBranch, outerMerge); }; + const auto leaveIf = [&](std::uint32_t condition, std::uint32_t reason) { + const auto failed = state.module.AllocateId(); + const auto next = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, next, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, condition, failed, next); + EmitLabel(state, failed); + if (recordFaults) RecordBdaFaultReason(state, address, bytes, instruction, reason); + leave(zero); + EmitLabel(state, next); + }; + const auto fail = [&](std::uint32_t condition, BdaAbi::FaultReason reason) { leaveIf(condition, recordFaults ? constant(static_cast(reason)) : 0u); }; const auto failFirst = [&](std::initializer_list> checks) { auto reason = constant(0u); for (auto check = std::rbegin(checks); check != std::rend(checks); ++check) { reason = Select(state, u32, check->first, constant(static_cast(check->second)), reason); } - const auto failed = binary(spv::OpINotEqual, boolean, reason, constant(0u)); - if (!recordFaults) { - ReturnBdaZeroIf(state, failed); - return; - } - const auto failedLabel = state.module.AllocateId(); - const auto next = state.module.AllocateId(); - state.module.AddFunction(spv::OpSelectionMerge, next, spv::SelectionControlMaskNone); - state.module.AddFunction(spv::OpBranchConditional, failed, failedLabel, next); - EmitLabel(state, failedLabel); - RecordBdaFaultReason(state, address, bytes, instruction, reason); - state.module.AddFunction(spv::OpReturnValue, BdaConstant(state, 0u)); - EmitLabel(state, next); + leaveIf(binary(spv::OpINotEqual, boolean, reason, constant(0u)), reason); }; const auto length = state.module.AllocateId(); state.module.AddFunction(spv::OpArrayLength, u32, length, state.bdaPagetableVariable, 0u); @@ -124,7 +125,21 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r {binary(spv::OpULessThan, boolean, result, base), BdaAbi::FaultReason::Overflow}, {binary(spv::OpULessThanEqual, boolean, deviceEnd, result), BdaAbi::FaultReason::Overflow}, }); - state.module.AddFunction(spv::OpReturnValue, result); + if (rangeResult) { + const auto last = binary(spv::OpUGreaterThanEqual, boolean, index, count); + const auto next = BdaLoadAddress(state, binary(spv::OpIAdd, u32, constant(4), binary(spv::OpIMul, u32, Select(state, u32, last, binary(spv::OpISub, u32, index, constant(1)), index), constant(8)))); + const auto bound = Select(state, u64, binary(spv::OpLogicalOr, boolean, last, binary(spv::OpULessThan, boolean, finish, next)), finish, next); + const auto range = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, rangeType, range, begin, bound, base); + leave(range); + } else { + leave(result); + } + EmitLabel(state, outerContinue); + state.module.AddFunction(spv::OpBranch, outer); + EmitLabel(state, outerMerge); + state.module.AddFunction(exits); + state.module.AddFunction(spv::OpReturnValue, exits[2]); state.module.AddFunction(spv::OpFunctionEnd); return function; } @@ -212,6 +227,10 @@ static std::uint32_t DefineBdaNoteWrite(SpirvEmitterState& state) { return function; } +void DefineBdaAnchorFunction(SpirvEmitterState& state) { + if (state.bdaAnchorFunction == 0) state.bdaAnchorFunction = DefineBdaLookup(state, "bda_anchor_range", false, BdaAbi::Read, true); +} + void DefineGetBdaPointer(SpirvEmitterState& state) { if (state.faultBufferVariable != 0) DefineBdaFaultFunction(state); if (!state.program.Info().usesDma) return; @@ -219,6 +238,8 @@ void DefineGetBdaPointer(SpirvEmitterState& state) { if (!BdaByteReadsForced()) state.bdaProbeFunction = DefineBdaLookup(state, "probe_bda_pointer", false); DefineBdaDwordReadFunctions(state); DefineBdaSpanReadFunctions(state); + CollectBdaAnchors(state); + if (!state.bdaAnchors.empty()) DefineBdaAnchorFunction(state); if (state.program.Info().bdaWrites) { state.bdaWritePointerFunction = DefineBdaLookup(state, "get_bda_write_pointer", true, BdaAbi::Write); if (!BdaByteReadsForced()) state.bdaWriteProbeFunction = DefineBdaLookup(state, "probe_bda_write_pointer", false, BdaAbi::Write); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index b43f89f997..017a6b7514 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -1,12 +1,16 @@ #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" +#include "SpirvBackend/SpirvEmitterInstructions.hpp" #include #include +#include #include #include +#include #include #include +#include namespace ShaderRecompiler { @@ -101,23 +105,6 @@ std::uint32_t EmitBdaBytes(SpirvEmitterState& state, std::uint32_t address, std: return EmitBdaByteLoop(state, address, ConstantU32(state, bytes), instruction, accessMask); } -// The dwords of a wide load that the program extracts (all of them when the composite is used -// whole). Dead-code elimination has dropped the unused extracts, as it dropped unused dword loads -// when every dword was its own instruction. -std::uint32_t UsedBdaDwords(const IrValue& inst, std::uint32_t dwords) { - const auto all = (1u << dwords) - 1u; - if (dwords == 1u) return all; - std::uint32_t used = 0; - for (const IrValue* user : inst.Uses()) { - const auto opcode = user->Opcode(); - if (opcode != IrOpcode::CompositeExtractU32x2 && opcode != IrOpcode::CompositeExtractU32x3 && opcode != IrOpcode::CompositeExtractU32x4) return all; - const IrValue* index = user->ArgumentCount() > 1u ? user->Argument(1) : nullptr; - if (index == nullptr || !index->HasImmediate() || index->ImmediateU32() >= dwords) return all; - used |= 1u << index->ImmediateU32(); - } - return used; -} - void EmitBdaStoreAt(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t pointerType, std::uint32_t value, std::uint32_t instruction, std::uint32_t accessMask) { const auto physical = state.module.AllocateId(); state.module.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), physical, state.bdaWritePointerFunction, address, ConstantU32(state, bytes), instruction); @@ -375,6 +362,24 @@ bool BdaByteReadsForced() { return forced; } +std::uint32_t UsedBdaDwords(const IrValue& inst, std::uint32_t dwords) { + const auto all = (1u << dwords) - 1u; + if (dwords == 1u) return all; + std::uint32_t used = 0; + for (const IrValue* user : inst.Uses()) { + const auto opcode = user->Opcode(); + if (opcode != IrOpcode::CompositeExtractU32x2 && opcode != IrOpcode::CompositeExtractU32x3 && opcode != IrOpcode::CompositeExtractU32x4) return all; + const IrValue* index = user->ArgumentCount() > 1u ? user->Argument(1) : nullptr; + if (index == nullptr || !index->HasImmediate() || index->ImmediateU32() >= dwords) return all; + used |= 1u << index->ImmediateU32(); + } + return used; +} + +bool RoutesApertures(const SpirvEmitterState& state, const MemoryInfo& mem) { + return mem.kind == ResourceKind::Flat && state.program.Resources().stage == IrShaderStage::Compute; +} + std::uint32_t BdaInstructionPc(SpirvEmitterState& state, const IrValue& inst) { return state.bdaPcOverride != 0u ? state.bdaPcOverride : ConstantU32(state, inst.Flags().pc); } @@ -417,6 +422,215 @@ bool BdaInvocationsMayStop(const IrProgram& program) { return true; } +namespace { + +constexpr std::size_t MaxBdaAnchors = 4; +constexpr std::uint64_t BdaAnchorAddressLimit = ~std::uint64_t{0} << 32u; +constexpr std::uint64_t BdaAnchorSpanLimit = std::uint64_t{1} << 62u; + +std::optional> UserDataBase(const SpirvEmitterState& state, const IrValue& inst) { + if (AddressOpcodeInfoOf(inst.Opcode()).access != AddressAccess::Read || inst.ArgumentCount() < 2u) return std::nullopt; + const auto index = inst.Flags().index; + const auto& memory = state.program.Resources().memoryInfo; + if (index >= memory.size()) return std::nullopt; + const auto& info = memory[index]; + if (info.kind != ResourceKind::ScalarAddress && info.kind != ResourceKind::Flat && info.kind != ResourceKind::Global) return std::nullopt; + if (info.planningOnly || info.addressIsFull || RoutesApertures(state, info)) return std::nullopt; + const IrValue* handle = inst.Argument(0) != nullptr ? inst.Argument(0)->Resolve() : nullptr; + if (handle == nullptr || handle->Opcode() != IrOpcode::GetAddressResource || handle->ArgumentCount() != 2u) return std::nullopt; + const auto userData = [](const IrValue* value) -> std::optional { + value = value != nullptr ? value->Resolve() : nullptr; + if (value == nullptr || value->Opcode() != IrOpcode::GetUserData || value->ArgumentCount() == 0u || value->Argument(0) == nullptr) return std::nullopt; + return value->Argument(0)->Register().index; + }; + const auto low = userData(handle->Argument(0)); + const auto high = userData(handle->Argument(1)); + if (!low || !high) return std::nullopt; + return std::pair{*low, *high}; +} + +std::uint32_t EmitElectedBdaRange(SpirvEmitterState& state, std::uint32_t type, std::uint32_t zero, const std::function& search) { + const auto elected = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformElect, TypeBool(state), elected, ConstantU32(state, spv::ScopeSubgroup)); + const auto before = state.currentLabel; + const auto electedLabel = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, elected, electedLabel, merge); + EmitLabel(state, electedLabel); + const auto found = search(); + const auto electedExit = state.currentLabel; + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, merge); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, type, value, found, electedExit, zero, before); + const auto u64 = TypeScalarU64(state); + const auto pair = TypeU32Vector(state, 2); + std::array parts{}; + for (std::uint32_t component = 0; component < parts.size(); ++component) { + const auto part = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u64, part, value, component); + const auto shared = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformBroadcastFirst, pair, shared, ConstantU32(state, spv::ScopeSubgroup), Unary(state, spv::OpBitcast, pair, part)); + parts[component] = Unary(state, spv::OpBitcast, u64, shared); + } + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, type, result, parts[0], parts[1], parts[2]); + return result; +} + +std::uint32_t BdaAnchorHolds(SpirvEmitterState& state, const SpirvEmitterState::BdaAnchor& anchor, std::uint32_t low, std::int32_t immediate, std::uint32_t bytes, std::uint32_t& translated) { + const auto u64 = TypeScalarU64(state); + const auto boolean = TypeBool(state); + auto offset = Binary(state, spv::OpIAdd, u64, anchor.offset, Unary(state, spv::OpUConvert, u64, low)); + if (immediate != 0) offset = Binary(state, spv::OpIAdd, u64, offset, BdaConstant(state, static_cast(static_cast(immediate)))); + const auto width = BdaConstant(state, bytes); + const auto fits = Binary(state, spv::OpUGreaterThanEqual, boolean, anchor.size, width); + const auto inside = Binary(state, spv::OpULessThanEqual, boolean, offset, Binary(state, spv::OpISub, u64, anchor.size, width)); + translated = Binary(state, spv::OpIAdd, u64, anchor.device, offset); + return Binary(state, spv::OpLogicalAnd, boolean, fits, inside); +} + +} + +void CollectBdaAnchors(SpirvEmitterState& state) { + state.bdaAnchors.clear(); + if (!state.program.Info().usesDma || state.bdaProbeFunction == 0) return; + std::vector, std::size_t>> bases; + for (const IrValue* inst : BdaReadSites(state)) { + const auto base = UserDataBase(state, *inst); + if (!base) continue; + const auto found = std::find_if(bases.begin(), bases.end(), [&](const auto& entry) { return entry.first == *base; }); + if (found != bases.end()) ++found->second; + else bases.push_back({*base, 1u}); + } + std::stable_sort(bases.begin(), bases.end(), [](const auto& left, const auto& right) { return left.second > right.second; }); + for (const auto& [base, reads] : bases) { + if (reads < 2u || state.bdaAnchors.size() == MaxBdaAnchors) break; + state.bdaAnchors.push_back({base.first, base.second}); + } +} + +void EmitBdaAnchorRange(SpirvEmitterState& state, SpirvEmitterState::BdaAnchor& anchor, std::uint32_t base) { + if (state.bdaAnchorFunction == 0) throw std::runtime_error("the BDA anchor search function is missing"); + const auto u64 = TypeScalarU64(state); + const auto boolean = TypeBool(state); + const auto rangeType = state.module.Type(spv::OpTypeVector, u64, 3u); + const auto search = [&] { + const auto range = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, rangeType, range, state.bdaAnchorFunction, base, ConstantU32(state, 1u), ConstantU32(state, 0u)); + return range; + }; + const auto zero = state.module.Constant(spv::OpConstantComposite, rangeType, BdaConstant(state, 0u), BdaConstant(state, 0u), BdaConstant(state, 0u)); + const auto range = state.requirements.subgroupBallot ? EmitElectedBdaRange(state, rangeType, zero, search) : search(); + const auto extract = [&](std::uint32_t component) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u64, value, range, component); + return value; + }; + const auto begin = extract(0u); + const auto end = extract(1u); + anchor.device = extract(2u); + anchor.offset = Binary(state, spv::OpISub, u64, base, begin); + const auto span = Binary(state, spv::OpISub, u64, end, begin); + const auto deviceEnd = Binary(state, spv::OpIAdd, u64, anchor.device, span); + auto usable = Binary(state, spv::OpLogicalAnd, boolean, Binary(state, spv::OpINotEqual, boolean, anchor.device, BdaConstant(state, 0u)), Binary(state, spv::OpUGreaterThan, boolean, deviceEnd, anchor.device)); + usable = Binary(state, spv::OpLogicalAnd, boolean, usable, Binary(state, spv::OpULessThanEqual, boolean, end, BdaConstant(state, BdaAnchorAddressLimit))); + usable = Binary(state, spv::OpLogicalAnd, boolean, usable, Binary(state, spv::OpULessThanEqual, boolean, span, BdaConstant(state, BdaAnchorSpanLimit))); + anchor.size = Select(state, u64, usable, span, BdaConstant(state, 0u)); +} + +void EmitBdaAnchors(SpirvEmitterState& state) { + if (state.bdaAnchors.empty()) return; + const auto u64 = TypeScalarU64(state); + for (auto& anchor : state.bdaAnchors) { + const auto low = Unary(state, spv::OpUConvert, u64, EmitGetUserData(state, static_cast(anchor.lowRegister))); + const auto high = Unary(state, spv::OpUConvert, u64, EmitGetUserData(state, static_cast(anchor.highRegister))); + EmitBdaAnchorRange(state, anchor, Binary(state, spv::OpBitwiseOr, u64, low, Binary(state, spv::OpShiftLeftLogical, u64, high, BdaConstant(state, 32u)))); + } +} + +const SpirvEmitterState::BdaAnchor* FindBdaAnchor(const SpirvEmitterState& state, const IrValue& inst) { + if (state.bdaAnchors.empty()) return nullptr; + const auto base = UserDataBase(state, inst); + if (!base) return nullptr; + for (const auto& anchor : state.bdaAnchors) { + const bool emitted = anchor.size != 0u; + if (emitted && anchor.lowRegister == base->first && anchor.highRegister == base->second) return &anchor; + } + return nullptr; +} + +std::array EmitAnchoredBdaRead(SpirvEmitterState& state, const IrValue& inst, const SpirvEmitterState::BdaAnchor& anchor, std::uint32_t low, std::int32_t immediate, std::uint32_t bits, std::uint32_t dwords, std::uint32_t usedDwords, const std::function()>& lookup) { + if (bits != 8u && bits != 16u && bits != 32u) throw std::runtime_error("unsupported anchored BDA read width"); + if (bits == 32u && (dwords == 0u || dwords > 4u)) throw std::runtime_error("unsupported anchored BDA read dword count"); + const auto used = bits != 32u ? 1u : usedDwords != 0u ? usedDwords : UsedBdaDwords(inst, dwords); + if (used == 0u) return lookup(); + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + const auto boolean = TypeBool(state); + std::uint32_t first = 0; + std::uint32_t last = 0; + std::uint32_t bytes = bits / 8u; + if (bits == 32u) { + first = static_cast(std::countr_zero(used)); + last = 31u - static_cast(std::countl_zero(used)); + bytes = (last - first + 1u) * 4u; + } + std::uint32_t translated = 0; + auto direct = BdaAnchorHolds(state, anchor, low, static_cast(static_cast(immediate) + first * 4u), bytes, translated); + if (bits == 32u) direct = Binary(state, spv::OpLogicalAnd, boolean, direct, Unary(state, spv::OpLogicalNot, boolean, BdaAddressUnaligned(state, translated))); + const auto directLabel = state.module.AllocateId(); + const auto lookupLabel = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, direct, directLabel, lookupLabel); + EmitLabel(state, directLabel); + std::array directValues{}; + const bool coherent = BdaCoherent(state, inst); + if (bits == 32u) { + for (std::uint32_t dword = first; dword <= last; ++dword) { + if ((used & (1u << dword)) == 0u) continue; + const auto element = dword == first ? translated : Binary(state, spv::OpIAdd, u64, translated, BdaConstant(state, (dword - first) * 4u)); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer, element); + directValues[dword] = state.module.AllocateId(); + if (coherent) state.module.AddFunction(spv::OpAtomicLoad, u32, directValues[dword], pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, 0u)); + else state.module.AddFunction(spv::OpLoad, u32, directValues[dword], pointer, BdaAccessMask(false), 4u); + } + } else { + const auto byteType = state.module.Type(spv::OpTypeInt, 8u, 0u); + const auto bytePointer = TypePointer(state, spv::StorageClassPhysicalStorageBuffer, byteType); + auto value = ConstantU32(state, 0u); + for (std::uint32_t byte = 0; byte < bytes; ++byte) { + const auto element = byte == 0u ? translated : Binary(state, spv::OpIAdd, u64, translated, BdaConstant(state, byte)); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, bytePointer, pointer, element); + const auto loaded = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, byteType, loaded, pointer, BdaAccessMask(coherent || bits == 16u), 1u); + value = Binary(state, spv::OpBitwiseOr, u32, value, Binary(state, spv::OpShiftLeftLogical, u32, Unary(state, spv::OpUConvert, u32, loaded), ConstantU32(state, byte * 8u))); + } + directValues[0] = value; + } + const auto directExit = state.currentLabel; + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, lookupLabel); + const auto lookupValues = lookup(); + const auto lookupExit = state.currentLabel; + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, merge); + std::array values{}; + for (std::uint32_t dword = 0; dword < (bits == 32u ? dwords : 1u); ++dword) { + if ((used & (1u << dword)) == 0u) { + values[dword] = ConstantU32(state, 0u); + continue; + } + values[dword] = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, u32, values[dword], directValues[dword], directExit, lookupValues[dword], lookupExit); + } + return values; +} + std::uint32_t EmitBdaRead(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t bits) { auto& state = ctx.state; if (bits != 8u && bits != 16u && bits != 32u) ctx.Fail(inst, "unsupported BDA read width"); @@ -427,6 +641,17 @@ std::uint32_t EmitBdaRead(SpirvValueEmitContext& ctx, const IrValue& inst, std:: return EmitBdaBytes(state, address, bits / 8u, instruction, BdaAccessMask(state, inst)); } +std::uint32_t EmitBdaReadBytes(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t bytes) { + auto& state = ctx.state; + if (state.bdaPointerFunction == 0) ctx.Fail(inst, "BDA lookup function is missing"); + const auto instruction = BdaInstructionPc(state, inst); + const auto u64 = TypeScalarU64(state); + const auto overflow = Binary(state, spv::OpUGreaterThan, TypeBool(state), address, Binary(state, spv::OpISub, u64, BdaConstant(state, std::numeric_limits::max()), Unary(state, spv::OpUConvert, u64, bytes))); + EmitIfCondition(state, overflow, [&] { RecordBdaFault(state, address, bytes, instruction, BdaAbi::FaultReason::Overflow); }); + StopBdaInvocationIf(state, overflow); + return EmitBdaByteLoop(state, address, bytes, instruction, BdaAccessMask(state, inst)); +} + void EmitBdaWrite(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t value, std::uint32_t bits) { auto& state = ctx.state; if (state.bdaWritePointerFunction == 0 || state.bdaNoteWriteFunction == 0) ctx.Fail(inst, "BDA write functions are missing"); @@ -434,19 +659,9 @@ void EmitBdaWrite(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t if (bits != 8u && bits != 16u && bits != 32u) ctx.Fail(inst, "unsupported BDA write width"); const auto storeBytes = [&] { EmitBdaOverflowCheck(state, address, bits / 8u, instruction); - for (std::uint32_t byte = 0u; byte < bits / 8u; ++byte) { - const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), address, BdaConstant(state, byte)); - const auto physical = state.module.AllocateId(); - state.module.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), physical, state.bdaWritePointerFunction, guest, ConstantU32(state, 1u), instruction); - EmitIfCondition(state, Binary(state, spv::OpINotEqual, TypeBool(state), physical, BdaConstant(state, 0u)), [&] { - const auto byteType = state.module.Type(spv::OpTypeInt, 8u, 0u); - const auto pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpConvertUToPtr, TypePointer(state, spv::StorageClassPhysicalStorageBuffer, byteType), pointer, physical); - const auto shifted = Binary(state, spv::OpShiftRightLogical, TypeU32(state), value, ConstantU32(state, byte * 8u)); - state.module.AddFunction(spv::OpStore, pointer, Unary(state, spv::OpUConvert, byteType, shifted), BdaAccessMask(state, inst), 1u); - state.module.AddFunction(spv::OpFunctionCall, state.module.Type(spv::OpTypeVoid), state.module.AllocateId(), state.bdaNoteWriteFunction, guest); - }); - } + const auto function = state.bdaByteWriteFunctions[BdaCoherent(state, inst)]; + if (function == 0) ctx.Fail(inst, "BDA byte write function is missing"); + state.module.AddFunction(spv::OpFunctionCall, state.module.Type(spv::OpTypeVoid), state.module.AllocateId(), function, address, value, ConstantU32(state, bits / 8u), instruction); }; if (bits != 32u) { storeBytes(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 2bd877635a..17d2db36bf 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -18,8 +19,14 @@ namespace ShaderRecompiler { namespace { +std::string FailureText(const IrProgram& program) { + char hash[24]; + std::snprintf(hash, sizeof(hash), "%llx", static_cast(program.Resources().shaderHash)); + return std::string("SPIR-V emission failed: hash=0x") + hash + " stage=" + std::to_string(static_cast(program.Resources().stage)); +} + [[noreturn]] void FailProgram(const IrProgram& program, const char* reason) { - throw std::runtime_error("SPIR-V emission failed: hash=0x" + std::to_string(program.Resources().shaderHash) + " stage=" + std::to_string(static_cast(program.Resources().stage)) + " reason=" + reason); + throw std::runtime_error(FailureText(program) + " reason=" + reason); } const ShaderWorkgroupInputInfo* ShaderWorkgroupInputFor(const SpirvEmitterState& state) { @@ -126,7 +133,7 @@ std::uint32_t SpirvValueEmitContext::Ballot(const IrValue* predicate) { const auto lowWord = state.module.AllocateId(); const auto highWord = state.module.AllocateId(); const auto ballot = state.module.AllocateId(); - state.module.AddFunction(spv::OpGroupNonUniformBallot, ballotType, high, scope, half == 1u ? Def(predicate) : otherHalf->Def(predicate)); + state.module.AddSubgroupBallot(ballotType, high, scope, half == 1u ? Def(predicate) : otherHalf->Def(predicate)); state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), lowWord, low, 0u); state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), highWord, high, 0u); state.module.AddFunction(spv::OpCompositeConstruct, ballotType, ballot, lowWord, highWord, ConstantU32(state, 0u), ConstantU32(state, 0u)); @@ -173,6 +180,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; @@ -226,14 +240,14 @@ std::uint32_t SpirvValueEmitContext::Label(const IrBlock* block) const { } [[noreturn]] void SpirvValueEmitContext::Fail(const IrValue& inst, const char* reason) const { - throw std::runtime_error("SPIR-V emission failed: hash=0x" + std::to_string(state.program.Resources().shaderHash) + " stage=" + std::to_string(static_cast(state.program.Resources().stage)) + " opcode=" + std::string(IrOpcodeName(inst.Opcode())) + " reason=" + reason); + throw std::runtime_error(FailureText(state.program) + " opcode=" + std::string(IrOpcodeName(inst.Opcode())) + " reason=" + reason); } std::vector SpirvEmitter::Emit(const IrProgram& program, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target) const { 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"); } @@ -250,10 +264,28 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.nonConstantImageOffsets = target.nonConstantImageOffsets; state.narrowSubgroupClock = target.narrowSubgroupClock; state.float64DenormPreserve = target.float64DenormPreserve; + state.depthRangeRemap = target.depthRangeRemap; state.registrationModules = target.registrationModules; state.fragmentDescriptorSet = target.fragmentDescriptorSet; + state.meshBarycentricPixel = target.meshBarycentricPixel; + state.meshBarycentricVertex = target.meshBarycentricVertex; + state.barycentricSmoothLocation = target.barycentricSmoothLocation; + state.barycentricLinearLocation = target.barycentricLinearLocation; + state.barycentricPositionsLocation = target.barycentricPositionsLocation; + state.meshExportedLocations = target.meshExportedLocations; + state.meshPerVertexLocations = target.meshPerVertexLocations; state.float16DenormPreserve = target.float16DenormPreserve; + state.robustBufferAccess2 = target.robustBufferAccess2; + state.float32FlushToZero = target.float32FlushToZero; + state.waveLdsMemoryBarriers = target.waveLdsMemoryBarriers; + state.float32DenormPreserve = target.float32DenormPreserve; state.hostSubgroupSize = target.subgroupSize; + if (state.requirements.ldsLock && LdsInDeviceMemory(state) && std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), static_cast(spv::CapabilityInt64Atomics)) != target.supportedCapabilities.end()) { + state.requirements.ldsLock = false; + state.requirements.ldsBufferInt64Atomics = true; + } + const bool int64Atomics = std::ranges::find(state.supportedCapabilities, static_cast(spv::CapabilityInt64Atomics)) != state.supportedCapabilities.end(); + state.requirements.bufferU64View = state.requirements.bufferInt64Atomics || (state.requirements.coherentBufferPairs && int64Atomics); if (HostSubgroupSingleLane(state)) { state.requirements.subgroupBallot = false; state.requirements.subgroupShuffle = false; @@ -262,23 +294,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/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 769ec4c329..4cd661b16a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvFlowEmitter.hpp" +#include "PipelineSpecialization.hpp" #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvEmitterInstructions.hpp" #include "SpirvBackend/SpirvMemory/SpirvSubgroup.hpp" @@ -93,6 +94,19 @@ void EmitReturnTerminator(SpirvValueEmitContext& ctx) { state.module.AddFunction(spv::OpEndInvocationInterlockEXT); } EmitKillIfPixelValidMaskInactive(state); + if (state.quadPixelUncovered != 0u) EmitKillIfBoolFalse(state, Unary(state, spv::OpLogicalNot, TypeBool(state), state.quadPixelUncovered)); + if (state.stencilReplay) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeI32(state), value, state.stencilRefVariable); + const auto reference = Unary(state, spv::OpBitcast, TypeU32(state), value); + const auto masked = Binary(state, spv::OpBitwiseAnd, TypeU32(state), reference, ConstantU32(state, 0xffu)); + const auto offset = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::StencilReferenceOffset, state.program.Metadata().bindings.stencilReferenceDword); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePushConstantElementPointer(state), pointer, state.pushConstantVariable, ConstantU32(state, 0u), offset); + const auto expected = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), expected, pointer); + EmitKillIfBoolFalse(state, Binary(state, spv::OpIEqual, TypeBool(state), masked, expected)); + } state.module.AddFunction(spv::OpReturn); } @@ -293,6 +307,7 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::Void: return Invoke(EmitVoid, ctx, inst); case IrOpcode::Reference: return Invoke(EmitReference, ctx, inst); case IrOpcode::ReferenceU32: return Invoke(EmitReferenceU32, ctx, inst); + case IrOpcode::UnlinkedCall: return Invoke(EmitUnlinkedCall, ctx, inst); case IrOpcode::GetUserData: return Invoke(EmitGetUserData, ctx, inst); case IrOpcode::GetShaderBase: return Invoke(EmitGetShaderBase, ctx, inst); case IrOpcode::ShaderClock: return Invoke(EmitShaderClock, ctx, inst); @@ -651,6 +666,8 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::SharedAtomicMskor64: return Invoke(EmitSharedAtomicMskor64, ctx, inst); case IrOpcode::DataAppend: return Invoke(EmitDataAppend, ctx, inst); case IrOpcode::DataConsume: return Invoke(EmitDataConsume, ctx, inst); + case IrOpcode::DataOrderedCount: return Invoke(EmitDataOrderedCount, ctx, inst); + case IrOpcode::DataOrderedSwap: return Invoke(EmitDataOrderedSwap, ctx, inst); case IrOpcode::SwizzleU32: return Invoke(EmitSwizzleU32, ctx, inst); case IrOpcode::ImageQueryDimensions: return Invoke(EmitImageQueryDimensions, ctx, inst); case IrOpcode::ImageQueryLod: return Invoke(EmitImageQueryLod, ctx, inst); @@ -743,7 +760,9 @@ void EmitStructuredBlock(SpirvValueEmitContext& ctx, StructuredFunctionState& fu } else if (access != SharedAccess::None) { const bool writes = access != SharedAccess::Read; if (ldsWritten || (writes && ldsRead)) { - state.module.AddFunction(spv::OpControlBarrier, ConstantU32(state, state.waveLdsScope), ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | LdsMemorySemantics(state))); + const auto semantics = ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | LdsMemorySemantics(state)); + if (state.waveLdsMemoryBarriers && state.waveLdsScope == spv::ScopeSubgroup) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeSubgroup), semantics); + else state.module.AddFunction(spv::OpControlBarrier, ConstantU32(state, state.waveLdsScope), ConstantU32(state, spv::ScopeWorkgroup), semantics); ldsWritten = false; ldsRead = false; } @@ -793,6 +812,25 @@ bool CommunicatesThroughMemory(IrOpcode opcode) { return SharedAccessOf(opcode) != SharedAccess::None || buffer == BufferAccess::Write || buffer == BufferAccess::Atomic || address == AddressAccess::Write || address == AddressAccess::Atomic || image == ImageAccess::Write || image == ImageAccess::Atomic; } +bool AccessesMemory(IrOpcode opcode) { + return opcode == IrOpcode::ReadConst || opcode == IrOpcode::ReadConstBuffer || SharedAccessOf(opcode) != SharedAccess::None || BufferAccessOf(opcode) != BufferAccess::None || AddressOpcodeInfoOf(opcode).access != AddressAccess::None || ImageOpcodeInfoOf(opcode).access != ImageAccess::None; +} + +bool PrecedesEveryMemoryAccess(const IrProgram& program, const IrValue& barrier) { + const auto& blocks = program.BlockOrder(); + const IrBlock* block = blocks.empty() ? nullptr : blocks.front(); + for (std::size_t steps = 0; block != nullptr && steps < blocks.size(); ++steps) { + for (const auto* instruction : block->Instructions()) { + if (instruction == &barrier) return true; + if (AccessesMemory(instruction->Opcode())) return false; + } + if (block->Successors().size() != 1u) return false; + block = block->Successors().front(); + if (block->Predecessors().size() != 1u) return false; + } + return false; +} + } void EmitControlFlow(SpirvModule& module, const IrProgram& program) { @@ -849,6 +887,7 @@ void EmitVoid(SpirvValueEmitContext&) { void EmitBarrier(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; if (state.program.Resources().stage == IrShaderStage::Vertex) { + if (PrecedesEveryMemoryAccess(state.program, inst)) return; for (const auto* block : state.program.BlockOrder()) { for (const auto* instruction : block->Instructions()) { if (CommunicatesThroughMemory(instruction->Opcode())) ctx.Fail(inst, "s_barrier in a vertex program that accesses LDS or GDS or writes memory is not implemented"); @@ -874,6 +913,13 @@ void EmitReferenceU32(SpirvValueEmitContext& context) { EmitVoid(context); } +void EmitUnlinkedCall(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t low, std::uint32_t high, std::uint32_t programCounter) { + auto& state = ctx.state; + if (state.faultBufferVariable == 0) ctx.Fail(inst, "calls an unlinked target without a fault buffer"); + RecordBdaFaultWords(state, low, high, ConstantU32(state, 0u), programCounter, BdaAbi::FaultReason::UnlinkedCall); + StopBdaInvocationIf(state, ConstantBool(state, true)); +} + void EmitControlNop(SpirvValueEmitContext& context) { EmitVoid(context); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index b56eb7a847..de08a9338f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -7,6 +7,7 @@ #include "SpirvBackend/SpirvMemory/SpirvDescriptors.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "RdnaDecoder/RdnaImageOpDecoder.hpp" +#include "ImageTableAbi.hpp" #include #include #include @@ -24,20 +25,11 @@ namespace ShaderRecompiler { namespace { -// A bindless table root's runtime slot and whether the key is mapped (SPIR-V ids), both 0 for a -// direct image. -struct TableSelection { - std::uint32_t slot = 0; - std::uint32_t mapped = 0; -}; - struct ImageEmitAccess { const IrValue& inst; const MemoryInfo& mem; const ImageResource& image; const IrValue& address; - TableSelection table; - std::uint32_t slot = table.slot; }; struct SampleSetup { @@ -54,8 +46,10 @@ std::uint32_t RuntimeImageMode(SpirvEmitterState& state, std::uint32_t resource) } std::uint32_t RuntimeImageSwizzle(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t component) { - if (state.program.Info().images.at(resource).indirectRoot == ImageResource::NoIndirectImage && !state.registrationModules) return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + resource * PipelineSpecialization::ImageWords + 1u + component, component + 4u); - const auto descriptor = RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, descriptor) / sizeof(std::uint32_t) + 3u); + const bool table = state.program.Info().images.at(resource).table != NoTable; + if (!table && !state.registrationModules) return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + resource * PipelineSpecialization::ImageWords + 1u + component, component + 4u); + if (table && !state.tableAccess.active) FailEmit("image table swizzle requested outside a table access"); + const auto descriptor = table ? state.tableAccess.dword3 : RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, descriptor) / sizeof(std::uint32_t) + 3u); return Binary(state, spv::OpBitwiseAnd, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeU32(state), descriptor, ConstantU32(state, component * 3u)), ConstantU32(state, 7u)); } @@ -185,7 +179,8 @@ std::uint32_t CubeLayer(SpirvEmitterState& state, std::uint32_t value) { std::uint32_t CoordF32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t first, std::uint32_t components, std::uint32_t encoded, bool gradient = false) { const bool cube = access.image.cube; const auto present = std::min(encoded, components); - if (first == NoImageComponent || (encoded < components && (cube || encoded == 0u)) || access.mem.imageAddressComponents < first + present) { + const bool cubeFace = cube && access.mem.imageDimension == RdnaImageDimension::Dim2D; + if (first == NoImageComponent || (encoded < components && ((cube && !cubeFace) || encoded == 0u)) || access.mem.imageAddressComponents < first + present) { ctx.Fail(access.inst, "has an image address with too few coordinate components"); } const auto component = [&](std::uint32_t index) { return index < present ? AddressF32(ctx, access, first + index) : ZeroF32(ctx.state); }; @@ -198,9 +193,12 @@ std::uint32_t CoordF32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access x = CubeAxis(ctx.state, x); y = CubeAxis(ctx.state, y); } + if (!gradient && access.image.depthCompare && (access.image.remappedDepth1D || access.mem.imageDimension == RdnaImageDimension::Dim1D || access.mem.imageDimension == RdnaImageDimension::Dim1DArray)) { + y = ConstantF32(ctx.state, 0x3f000000u); + } const auto result = ctx.state.module.AllocateId(); if (components == 3u) { - auto z = component(2u); + auto z = (access.image.depthCompare && (access.mem.imageDimension == RdnaImageDimension::Dim1DArray || (access.image.remappedDepth1D && access.image.dimension == RdnaImageDimension::Dim2DArray))) ? component(1u) : component(2u); if (cube) { z = CubeLayer(ctx.state, z); } @@ -248,6 +246,10 @@ bool VolumeSliceAddress(const ImageEmitAccess& access) { return access.mem.imageDimension == RdnaImageDimension::Dim2DArray && access.image.dimension == RdnaImageDimension::Dim3D; } +bool ArraySliceAddress(const ImageEmitAccess& access) { + return access.mem.imageDimension == RdnaImageDimension::Dim3D && access.image.dimension == RdnaImageDimension::Dim2DArray; +} + std::uint32_t VolumeSliceInRange(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t image, std::optional lod) { auto& state = ctx.state; state.module.EmitCapability(spv::CapabilityImageQuery); @@ -290,15 +292,6 @@ std::uint32_t SampledComponentZero(SpirvEmitterState& state, IrTextureNumericCla throw std::runtime_error("invalid sampled image numeric class"); } -// An image op's u32x4 result through a bindless table: zeros when the key is not mapped. -std::uint32_t TableResult(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t result) { - if (access.table.mapped == 0) return result; - auto& state = ctx.state; - const auto mapped = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeConstruct, TypeBoolVector(state, 4), mapped, access.table.mapped, access.table.mapped, access.table.mapped, access.table.mapped); - return Select(state, TypeU32Vector(state, 4), mapped, result, ConstantU32CompositeZero(state, 4)); -} - std::uint32_t ResultVector(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t value, IrTextureNumericClass numericClass, bool dref, bool gather) { auto& state = ctx.state; const auto& mem = access.mem; @@ -405,13 +398,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(); @@ -420,7 +419,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); @@ -434,7 +433,7 @@ std::uint32_t QueryDimensions(SpirvValueEmitContext& ctx, const ImageEmitAccess& auto& state = ctx.state; const auto dimension = access.image.dimension; const auto& info = RdnaImageDimensionInfoFor(dimension); - const auto image = LoadSampledImageDescriptor(state, access.mem.resource, access.slot); + const auto image = LoadSampledImageDescriptor(state, access.mem.resource); const auto size = state.module.AllocateId(); if (info.multisampled != 0u) { state.module.AddFunction(spv::OpImageQuerySize, ImageViewSizeType(state, dimension), size, image); @@ -475,6 +474,9 @@ std::uint32_t PackedOffset(SpirvValueEmitContext& ctx, const ImageEmitAccess& ac values[index] = state.module.AllocateId(); state.module.AddFunction(spv::OpBitFieldSExtract, TypeI32(state), values[index], packed, ConstantU32(state, index * 8u), ConstantU32(state, 6)); } + if (access.image.remappedDepth1D && components >= 2u) { + values[1] = ConstantI32(state, 0); + } if (components == 1u) { return values[0]; } @@ -580,7 +582,7 @@ std::uint32_t UnpackImageTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess SpirvBufferFormatInfo PackedTexelFormat(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { const auto format = access.image.packedFormat; - if (format == IrBufferFormat::Invalid || access.table.mapped != 0 || access.image.depthBits || access.image.conversionFormat != IrBufferFormat::Invalid) { + if (format == IrBufferFormat::Invalid || access.image.table != NoTable || access.image.depthBits || access.image.conversionFormat != IrBufferFormat::Invalid) { ctx.Fail(access.inst, "accesses packed texels of an image without a packed format"); } const auto info = GetFormatInfo(format); @@ -665,7 +667,7 @@ std::uint32_t EmitOneDimensionalGatherLz(SpirvValueEmitContext& ctx, const Image const auto numericClass = access.image.numericClass; const bool arrayed = access.image.dimension == RdnaImageDimension::Dim1DArray; state.module.EmitCapability(spv::CapabilityImageQuery); - const auto image = LoadSampledImageDescriptor(state, access.mem.resource, access.slot); + const auto image = LoadSampledImageDescriptor(state, access.mem.resource); auto width = state.module.AllocateId(); state.module.AddFunction(spv::OpImageQuerySizeLod, arrayed ? TypeU32Vector(state, 2) : TypeU32(state), width, image, ConstantU32(state, 0)); auto coord = setup.coord; @@ -686,7 +688,7 @@ std::uint32_t EmitOneDimensionalGatherLz(SpirvValueEmitContext& ctx, const Image if (setup.layout.offset != NoImageComponent) { left = Binary(state, spv::OpFAdd, TypeF32(state), left, Unary(state, spv::OpConvertSToF, TypeF32(state), PackedOffset(ctx, access, setup.layout))); } - const auto sampled = MakeSampledImage(state, access.mem.resource, access.mem.sampler, access.slot); + const auto sampled = MakeSampledImage(state, access.mem.resource, access.mem.sampler); const auto vectorType = ImageVectorType(state, numericClass, 4); const auto scalarType = ImageScalarType(state, numericClass); const auto component = ImageConversionFormat(access.image).format == IrBufferFormat::Invalid ? ImageGatherComponent(EffectiveDmask(access.mem)) : 0u; @@ -755,9 +757,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++) { @@ -780,7 +779,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(); @@ -873,7 +872,7 @@ const ImageResource& ImageResourceOf(const SpirvEmitterState& state, const Memor void EmitQueryDimensionsOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { ctx.state.module.EmitCapability(spv::CapabilityImageQuery); - ctx.Define(access.inst, TableResult(ctx, access, QueryDimensions(ctx, access))); + ctx.Define(access.inst, QueryDimensions(ctx, access)); } void EmitQueryLodOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { @@ -885,7 +884,7 @@ void EmitQueryLodOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { ctx.Fail(access.inst, "queries the level of detail of a converted float image, which is not implemented"); } state.module.EmitCapability(spv::CapabilityImageQuery); - const auto sampled = MakeSampledImage(state, access.mem.resource, access.mem.sampler, access.slot); + const auto sampled = MakeSampledImage(state, access.mem.resource, access.mem.sampler); const auto coord = CoordF32(ctx, access, 0, RdnaImageDimensionInfoFor(access.image.dimension).spatialComponents, AddressDimension(access).spatialComponents); const auto lod = state.module.AllocateId(); state.module.AddFunction(spv::OpImageQueryLod, TypeF32Vector(state, 2), lod, sampled, coord); @@ -897,7 +896,7 @@ void EmitQueryLodOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { } const auto result = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4), result, values[0], values[1], values[2], values[3]); - ctx.Define(access.inst, TableResult(ctx, access, result)); + ctx.Define(access.inst, result); } void EmitByReadOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { @@ -911,7 +910,7 @@ void EmitByReadOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { state.module.EmitCapability(spv::CapabilityImageQuery); const auto condition = ctx.Arg(access.inst, 2); ctx.Define(access.inst, EmitValueOrDefaultIfCondition(state, condition, TypeU32Vector(state, 4), ConstantU32CompositeZero(state, 4), [&]() { - const auto image = LoadSampledImageDescriptor(state, access.mem.resource, access.slot); + const auto image = LoadSampledImageDescriptor(state, access.mem.resource); const auto x = AddressU32(ctx, access, 0); const auto y = AddressU32(ctx, access, 1); const auto lod = LodU32(ctx, access); @@ -938,7 +937,7 @@ void EmitByReadOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { texelMemory.dmask = (1u << components) - 1u; texelMemory.dataDwords = components; texelMemory.componentCount = components; - const ImageEmitAccess texelAccess{access.inst, texelMemory, access.image, access.address, access.table, access.slot}; + const ImageEmitAccess texelAccess{access.inst, texelMemory, access.image, access.address}; std::uint32_t words[4] = {ConstantU32(state, 0), ConstantU32(state, 0), ConstantU32(state, 0), ConstantU32(state, 0)}; for (std::uint32_t element = 0; element < elements; ++element) { const auto column = Binary(state, spv::OpIAdd, TypeU32(state), first, ConstantU32(state, element)); @@ -967,7 +966,7 @@ void EmitByReadOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { } const auto result = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4), result, words[0], words[1], words[2], words[3]); - return TableResult(ctx, access, result); + return result; })); } @@ -980,14 +979,14 @@ void EmitReadOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { const auto& dimensionInfo = RdnaImageDimensionInfoFor(access.image.dimension); const auto numericClass = access.image.numericClass; auto condition = ctx.Arg(access.inst, 2); - if (access.mem.imageDimension == RdnaImageDimension::Dim3D && access.image.dimension == RdnaImageDimension::Dim2D) { + if (access.mem.imageDimension == RdnaImageDimension::Dim3D && (access.image.dimension == RdnaImageDimension::Dim2D || access.image.dimension == RdnaImageDimension::Dim1D)) { condition = Binary(state, spv::OpLogicalAnd, TypeBool(state), condition, Binary(state, spv::OpIEqual, TypeBool(state), AddressU32(ctx, access, 2u), ConstantU32(state, 0u))); } if (VolumeSliceAddress(access)) { - condition = Binary(state, spv::OpLogicalAnd, TypeBool(state), condition, VolumeSliceInRange(ctx, access, LoadSampledImageDescriptor(state, access.mem.resource, access.slot), LodU32(ctx, access))); + condition = Binary(state, spv::OpLogicalAnd, TypeBool(state), condition, VolumeSliceInRange(ctx, access, LoadSampledImageDescriptor(state, access.mem.resource), LodU32(ctx, access))); } ctx.Define(access.inst, EmitValueOrDefaultIfCondition(state, condition, TypeU32Vector(state, 4), ConstantU32CompositeZero(state, 4), [&]() { - const auto descriptor = LoadSampledImageDescriptor(state, access.mem.resource, access.slot); + const auto descriptor = LoadSampledImageDescriptor(state, access.mem.resource); const auto color = state.module.AllocateId(); const auto coord = CoordU32(ctx, access); if (dimensionInfo.multisampled != 0u) { @@ -1002,7 +1001,7 @@ void EmitReadOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { if (access.mem.imagePacked) { return PackedImageTexel(ctx, access, color); } - return TableResult(ctx, access, ResultVector(ctx, access, UnpackImageTexel(ctx, access, color), numericClass, false, false)); + return ResultVector(ctx, access, UnpackImageTexel(ctx, access, color), numericClass, false, false); })); } @@ -1013,15 +1012,73 @@ std::uint32_t ImageSampleIndex(SpirvValueEmitContext& ctx, const ImageEmitAccess return AddressU32(ctx, access, addressInfo.coordinateComponents); } +std::uint32_t QuantizeStoreTexel(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t texel) { + const auto first = PipelineSpecialization::StoreQuantizeBase + resource * PipelineSpecialization::StoreQuantizeWords; + const auto f32 = TypeF32(state); + const auto vec4 = TypeF32Vector(state, 4u); + const auto bvec4 = state.module.Type(spv::OpTypeVector, TypeBool(state), 4u); + const auto splat = [&](std::uint32_t type, std::uint32_t scalar) { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, type, result, scalar, scalar, scalar, scalar); + return result; + }; + const auto word = [&](std::uint32_t index) { return state.module.SpecializationConstant(TypeU32(state), first + index, 0u); }; + const auto enabled = Binary(state, spv::OpINotEqual, TypeBool(state), word(0u), ConstantU32(state, 0u)); + const auto scale = splat(vec4, Unary(state, spv::OpBitcast, f32, word(1u))); + const auto reciprocal = splat(vec4, Unary(state, spv::OpBitcast, f32, word(2u))); + const auto snorm = Binary(state, spv::OpINotEqual, TypeBool(state), word(3u), ConstantU32(state, 0u)); + const auto lower = splat(vec4, Select(state, f32, snorm, ConstantF32(state, std::bit_cast(-1.0f)), ConstantF32(state, 0u))); + const auto zero = splat(vec4, ConstantF32(state, 0u)); + const auto one = splat(vec4, ConstantF32(state, std::bit_cast(1.0f))); + const auto finite = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelect, vec4, finite, Unary(state, spv::OpIsNan, bvec4, texel), zero, texel); + const auto clamped = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, vec4, clamped, GlslStd450(state), GLSLstd450FClamp, finite, lower, one); + const auto product = Binary(state, spv::OpFMul, vec4, clamped, scale); + state.module.AddAnnotation(spv::OpDecorate, product, spv::DecorationNoContraction); + const auto ext = [&](GLSLstd450 op, const auto&... operands) { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, vec4, result, GlslStd450(state), static_cast(op), operands...); + return result; + }; + const auto exact = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t rhs) { + const auto value = Binary(state, opcode, vec4, lhs, rhs); + state.module.AddAnnotation(spv::OpDecorate, value, spv::DecorationNoContraction); + return value; + }; + const auto spreadFactor = splat(vec4, ConstantF32(state, std::bit_cast(4097.0f))); + const auto high = [&](std::uint32_t value) { + const auto spread = exact(spv::OpFMul, value, spreadFactor); + return exact(spv::OpFSub, spread, exact(spv::OpFSub, spread, value)); + }; + const auto clampedHigh = high(clamped); + const auto scaleHigh = high(scale); + const auto clampedLow = exact(spv::OpFSub, clamped, clampedHigh); + const auto scaleLow = exact(spv::OpFSub, scale, scaleHigh); + auto error = exact(spv::OpFSub, exact(spv::OpFMul, clampedHigh, scaleHigh), product); + error = exact(spv::OpFAdd, error, exact(spv::OpFMul, clampedHigh, scaleLow)); + error = exact(spv::OpFAdd, error, exact(spv::OpFMul, clampedLow, scaleHigh)); + error = exact(spv::OpFAdd, error, exact(spv::OpFMul, clampedLow, scaleLow)); + const auto below = ext(GLSLstd450Floor, product); + const auto fraction = Binary(state, spv::OpFSub, vec4, product, below); + state.module.AddAnnotation(spv::OpDecorate, fraction, spv::DecorationNoContraction); + const auto tie = Binary(state, spv::OpFOrdEqual, bvec4, fraction, splat(vec4, ConstantF32(state, std::bit_cast(0.5f)))); + const auto above = Binary(state, spv::OpFAdd, vec4, below, one); + const auto even = ext(GLSLstd450RoundEven, product); + const auto broken = Select(state, vec4, Binary(state, spv::OpFOrdGreaterThan, bvec4, error, zero), above, Select(state, vec4, Binary(state, spv::OpFOrdLessThan, bvec4, error, zero), below, even)); + const auto level = Select(state, vec4, tie, broken, even); + const auto quantized = Binary(state, spv::OpFMul, vec4, level, reciprocal); + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelect, vec4, result, splat(bvec4, enabled), quantized, texel); + return result; +} + void EmitWriteOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { auto& state = ctx.state; - if (access.slot != 0) { - ctx.Fail(access.inst, "stores through a bindless image table are unsupported"); - } const bool uintImage = access.image.numericClass == IrTextureNumericClass::Uint; auto condition = ctx.Arg(access.inst, 3); - if (VolumeSliceAddress(access)) { - if (access.image.mipMode == ImageMipMode::DynamicStorage) ctx.Fail(access.inst, "stores a 2D-array address to a 3D image with dynamic mips, which is not implemented"); + if (VolumeSliceAddress(access) || ArraySliceAddress(access)) { + if (access.image.mipMode == ImageMipMode::DynamicStorage) ctx.Fail(access.inst, "stores a 2D-array address to a 3D image or a 3D address to a 2D-array image with dynamic mips, which is not implemented"); const auto image = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, ImageType(state, access.image), image, StorageImageDescriptorPointer(state, access.mem.resource)); condition = Binary(state, spv::OpLogicalAnd, TypeBool(state), condition, VolumeSliceInRange(ctx, access, image, std::nullopt)); @@ -1029,16 +1086,14 @@ void EmitWriteOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { EmitIfCondition(state, condition, [&]() { const auto mipLod = access.image.mipMode == ImageMipMode::DynamicStorage ? LodU32(ctx, access) : 0u; const auto coord = CoordU32(ctx, access); - const auto texel = access.mem.imagePacked ? PackedStoreTexel(ctx, access, ctx.Arg(access.inst, 2)) : StoreTexel(ctx, access, ctx.Arg(access.inst, 2), uintImage); + auto texel = access.mem.imagePacked ? PackedStoreTexel(ctx, access, ctx.Arg(access.inst, 2)) : StoreTexel(ctx, access, ctx.Arg(access.inst, 2), uintImage); + if (!uintImage && !access.mem.imagePacked && !access.image.atomic && access.image.table == NoTable && !state.registrationModules && access.image.conversionFormat == IrBufferFormat::Invalid) texel = QuantizeStoreTexel(state, access.mem.resource, texel); EmitStorageImageWrite(state, access.mem.resource, mipLod, coord, texel, ImageSampleIndex(ctx, access)); }); } void EmitAtomicOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { auto& state = ctx.state; - if (access.slot != 0) { - ctx.Fail(access.inst, "atomics through a bindless image table are unsupported"); - } const auto opcode = access.inst.Opcode(); const auto condition = ctx.Arg(access.inst, access.inst.ArgumentCount() - 1u); if (IsImageAtomic64Opcode(opcode)) { @@ -1098,7 +1153,8 @@ SampleSetup MakeSampleSetup(SpirvValueEmitContext& ctx, const ImageEmitAccess& a if (dref && access.image.conversionFormat != IrBufferFormat::Invalid) { ctx.Fail(access.inst, "uses depth comparison with a packed integer image"); } - if (ImageConversionFormat(access.image).type == SpirvFormatComponentType::Unorm) { + const bool filtered = !dref && access.inst.Opcode() == IrOpcode::ImageSampleRaw; + if (!filtered && ImageConversionFormat(access.image).type == SpirvFormatComponentType::Unorm) { ctx.Fail(access.inst, "samples or gathers a converted unorm image, which needs filtering in the shader and is not implemented"); } if (ImageConversionFormat(access.image).type == SpirvFormatComponentType::Float) { @@ -1131,7 +1187,7 @@ void EmitPackedHorizontalGatherOp(SpirvValueEmitContext& ctx, const ImageEmitAcc const bool twoDimensional = dimension == RdnaImageDimension::Dim2D; if (texelBits <= 32u) { state.module.EmitCapability(spv::CapabilityImageQuery); - const auto image = LoadSampledImageDescriptor(state, mem.resource, access.slot); + const auto image = LoadSampledImageDescriptor(state, mem.resource); const auto size = state.module.AllocateId(); state.module.AddFunction(spv::OpImageQuerySizeLod, twoDimensional ? TypeI32Vector(state, 2) : TypeI32(state), size, image, ConstantU32(state, 0)); const auto extent = [&](std::uint32_t axis) { @@ -1200,7 +1256,7 @@ void EmitPackedHorizontalGatherOp(SpirvValueEmitContext& ctx, const ImageEmitAcc } const auto result = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4), result, selected[0], selected[1], selected[2], selected[3]); - ctx.Define(access.inst, TableResult(ctx, access, result)); + ctx.Define(access.inst, result); } void EmitGatherOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const SampleSetup& setup) { @@ -1213,15 +1269,15 @@ void EmitGatherOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con if (setup.dref && (access.image.emulatedCompare & EmulatedCompare::Enabled) != 0u) { ctx.Fail(access.inst, "is a comparison gather of a color texture, which is not implemented"); } - if (dimension == RdnaImageDimension::Dim1D || dimension == RdnaImageDimension::Dim1DArray) { - if (setup.dref || !HasFlag(mem, RdnaImageSampleFlagLevelZero) || HasFlag(mem, RdnaImageSampleFlagGatherHorizontal)) { + if (dimension == RdnaImageDimension::Dim1D || dimension == RdnaImageDimension::Dim1DArray || access.image.remappedDepth1D) { + if (setup.dref || !HasFlag(mem, RdnaImageSampleFlagLevelZero) || HasFlag(mem, RdnaImageSampleFlagGatherHorizontal) || access.image.remappedDepth1D) { ctx.Fail(access.inst, "has an unsupported 1D gather variant"); } const auto sample = EmitOneDimensionalGatherLz(ctx, access, setup); - ctx.Define(access.inst, TableResult(ctx, access, ResultVector(ctx, access, UnpackImageGather(ctx, access, sample), setup.numericClass, false, true))); + ctx.Define(access.inst, ResultVector(ctx, access, UnpackImageGather(ctx, access, sample), setup.numericClass, false, true)); return; } - const auto sampled = MakeSampledImage(state, mem.resource, mem.sampler, access.slot); + const auto sampled = MakeSampledImage(state, mem.resource, mem.sampler); const auto sample = state.module.AllocateId(); std::vector words; if (setup.dref) { @@ -1257,44 +1313,10 @@ void EmitGatherOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con gathered = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, ImageVectorType(state, resultNumericClass, 4), gathered, component, component, component, component); } - ctx.Define(access.inst, TableResult(ctx, access, ResultVector(ctx, access, UnpackImageGather(ctx, access, gathered), resultNumericClass, false, true))); + ctx.Define(access.inst, ResultVector(ctx, access, UnpackImageGather(ctx, access, gathered), resultNumericClass, false, true)); } -TableSelection EmitIndirectImageSelector(SpirvValueEmitContext& ctx, const ImageResource& image, std::uint32_t key) { - auto& state = ctx.state; - const auto loadMapping = [&](std::uint32_t index) { - const auto pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.flattenedSrtVariable, ConstantU32(state, 0), index); - const auto value = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); - return value; - }; - const auto mapping = ConstantU32(state, image.indirectMappingOffset); - auto low = ConstantU32(state, 0u); - auto high = loadMapping(mapping); - auto selected = ConstantU32(state, image.indirectRoot); - auto mapped = ConstantBool(state, false); - for (std::uint32_t iteration = 0; iteration < image.indirectSearchIterations; iteration++) { - const auto active = Binary(state, spv::OpULessThan, TypeBool(state), low, high); - const auto mid = Binary(state, spv::OpShiftRightLogical, TypeU32(state), Binary(state, spv::OpIAdd, TypeU32(state), low, high), ConstantU32(state, 1u)); - const auto probe = Select(state, TypeU32(state), active, mid, ConstantU32(state, 0u)); - const auto entry = Binary(state, spv::OpIAdd, TypeU32(state), mapping, Binary(state, spv::OpIAdd, TypeU32(state), Binary(state, spv::OpShiftLeftLogical, TypeU32(state), probe, ConstantU32(state, 1u)), ConstantU32(state, 1u))); - const auto mappedKey = loadMapping(entry); - const auto candidate = loadMapping(Binary(state, spv::OpIAdd, TypeU32(state), entry, ConstantU32(state, 1u))); - const auto equal = Binary(state, spv::OpIEqual, TypeBool(state), mappedKey, key); - const auto match = Binary(state, spv::OpLogicalAnd, TypeBool(state), active, equal); - selected = Select(state, TypeU32(state), match, candidate, selected); - mapped = Binary(state, spv::OpLogicalOr, TypeBool(state), mapped, match); - const auto less = Binary(state, spv::OpULessThan, TypeBool(state), mappedKey, key); - const auto takeUpper = Binary(state, spv::OpLogicalAnd, TypeBool(state), active, less); - const auto takeLower = Binary(state, spv::OpLogicalAnd, TypeBool(state), active, Unary(state, spv::OpLogicalNot, TypeBool(state), less)); - low = Select(state, TypeU32(state), takeUpper, Binary(state, spv::OpIAdd, TypeU32(state), mid, ConstantU32(state, 1u)), low); - high = Select(state, TypeU32(state), takeLower, mid, high); - } - return {selected, mapped}; -} - -void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const SampleSetup& setup) { +void EmitEmulatedSample(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const SampleSetup& setup, bool converted) { auto& state = ctx.state; const auto& mem = access.mem; const auto& image = access.image; @@ -1305,10 +1327,9 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess return EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), compare, ConstantU32(state, fields.at(word).first)), fields.at(word).second); }; const auto equal = [&](std::uint32_t value, std::uint32_t literal) { return Binary(state, spv::OpIEqual, TypeBool(state), value, ConstantU32(state, literal)); }; - if (access.slot != 0) ctx.Fail(access.inst, "compares against a color texture through a bindless image table, which is not implemented"); - if (HasFlag(mem, RdnaImageSampleFlagDerivative) || HasFlag(mem, RdnaImageSampleFlagLod)) ctx.Fail(access.inst, "compares against a color texture with gradients or an explicit LOD, which is not implemented"); + if (!converted && (HasFlag(mem, RdnaImageSampleFlagDerivative) || HasFlag(mem, RdnaImageSampleFlagLod))) ctx.Fail(access.inst, "compares against a color texture with gradients or an explicit LOD, which is not implemented"); const bool arrayed = image.dimension == RdnaImageDimension::Dim2DArray; - if (image.dimension != RdnaImageDimension::Dim2D && !arrayed) ctx.Fail(access.inst, "compares against a color texture that is not a 2D or 2D array view, which is not implemented"); + if (image.dimension != RdnaImageDimension::Dim2D && !arrayed) ctx.Fail(access.inst, converted ? "samples a converted image that is not a 2D or 2D array view, which is not implemented" : "compares against a color texture that is not a 2D or 2D array view, which is not implemented"); const auto f32 = TypeF32(state); const auto i32 = TypeI32(state); const auto u32 = TypeU32(state); @@ -1325,7 +1346,7 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess return value; }; const auto f32Constant = [&](float value) { return ConstantF32(state, std::bit_cast(value)); }; - const auto descriptor = LoadSampledImageDescriptor(state, mem.resource, access.slot); + const auto descriptor = LoadSampledImageDescriptor(state, mem.resource); state.module.EmitCapability(spv::CapabilityImageQuery); const auto size = state.module.AllocateId(); state.module.AddFunction(spv::OpImageQuerySizeLod, ImageViewSizeType(state, image.dimension), size, descriptor, ConstantU32(state, 0u)); @@ -1337,9 +1358,11 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess const auto rounded = Unary(state, spv::OpConvertFToS, i32, ext(f32, GLSLstd450Floor, {Binary(state, spv::OpFAdd, f32, extract(f32, setup.coord, 2u), f32Constant(0.5f))})); layer = Unary(state, spv::OpBitcast, u32, ext(i32, GLSLstd450SClamp, {rounded, ConstantI32(state, 0), Binary(state, spv::OpISub, i32, layers, ConstantI32(state, 1))})); } - auto reference = DrefValueF32(ctx, access, setup.layout); - reference = Select(state, f32, equal(parameter(4u), EmulatedCompare::ReferenceUnorm), ext(f32, GLSLstd450FClamp, {reference, f32Constant(0.0f), f32Constant(1.0f)}), reference); - reference = Select(state, f32, equal(parameter(4u), EmulatedCompare::ReferenceSnorm), ext(f32, GLSLstd450FClamp, {reference, f32Constant(-1.0f), f32Constant(1.0f)}), reference); + auto reference = converted ? 0u : DrefValueF32(ctx, access, setup.layout); + if (!converted) { + reference = Select(state, f32, equal(parameter(4u), EmulatedCompare::ReferenceUnorm), ext(f32, GLSLstd450FClamp, {reference, f32Constant(0.0f), f32Constant(1.0f)}), reference); + reference = Select(state, f32, equal(parameter(4u), EmulatedCompare::ReferenceSnorm), ext(f32, GLSLstd450FClamp, {reference, f32Constant(-1.0f), f32Constant(1.0f)}), reference); + } const auto address = [&](std::uint32_t index, std::uint32_t extent, std::uint32_t mode) { const auto remainder = Binary(state, spv::OpSRem, i32, index, extent); const auto negative = Binary(state, spv::OpSLessThan, TypeBool(state), remainder, ConstantI32(state, 0)); @@ -1356,7 +1379,7 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess const auto one = f32Constant(1.0f); const auto zero = f32Constant(0.0f); const auto borderRed = Select(state, f32, equal(parameter(5u), 1u), one, zero); - const auto compareTexel = [&](std::uint32_t x, std::uint32_t y) { + const auto compareTexel = [&](std::uint32_t x, std::uint32_t y) -> std::array { const auto modeX = parameter(2u); const auto modeY = parameter(3u); const auto ux = Unary(state, spv::OpBitcast, u32, address(x, width, modeX)); @@ -1364,6 +1387,14 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess const auto coord = state.module.AllocateId(); if (arrayed) state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 3), coord, ux, uy, layer); else state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 2), coord, ux, uy); + if (converted) { + const auto raw = state.module.AllocateId(); + state.module.AddFunction(spv::OpImageFetch, ImageVectorType(state, IrTextureNumericClass::Uint, 4), raw, descriptor, coord, spv::ImageOperandsLodMask, ConstantU32(state, 0u)); + const auto unpacked = UnpackImageTexel(ctx, access, raw); + std::array channels{}; + for (std::uint32_t channel = 0; channel < 4u; ++channel) channels[channel] = Unary(state, spv::OpBitcast, f32, extract(u32, unpacked, channel)); + return channels; + } const auto texel = state.module.AllocateId(); state.module.AddFunction(spv::OpImageFetch, ImageVectorType(state, IrTextureNumericClass::Float, 4), texel, descriptor, coord, spv::ImageOperandsLodMask, ConstantU32(state, 0u)); auto red = extract(f32, texel, 3u); @@ -1377,7 +1408,7 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess result = Select(state, f32, equal(parameter(0u), index + 1u), compared, result); } result = Select(state, f32, equal(parameter(4u), EmulatedCompare::ReferenceZero), zero, result); - return result; + return {result, result, result, result}; }; const auto scaledU = Binary(state, spv::OpFMul, f32, extract(f32, setup.coord, 0u), Unary(state, spv::OpConvertSToF, f32, width)); const auto scaledV = Binary(state, spv::OpFMul, f32, extract(f32, setup.coord, 1u), Unary(state, spv::OpConvertSToF, f32, height)); @@ -1391,24 +1422,49 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess const auto texelIndex = [&](std::uint32_t coordinate, std::uint32_t offset) { return Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, ext(f32, GLSLstd450Floor, {coordinate})), offset); }; - const auto result = EmitValueIfElse(state, equal(parameter(1u), 0u), f32, [&] { - return compareTexel(texelIndex(scaledU, offsetX), texelIndex(scaledV, offsetY)); + const auto filteredType = converted ? TypeF32Vector(state, 4) : f32; + const auto vector = [&](const std::array& channels) { + if (!converted) return channels[0]; + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, filteredType, value, channels[0], channels[1], channels[2], channels[3]); + return value; + }; + const auto mix = [&](std::uint32_t first, std::uint32_t second, std::uint32_t weight) { + return ext(filteredType, GLSLstd450FMix, {first, second, vector({weight, weight, weight, weight})}); + }; + const auto filtered = EmitValueIfElse(state, equal(parameter(1u), 0u), filteredType, [&] { + return vector(compareTexel(texelIndex(scaledU, offsetX), texelIndex(scaledV, offsetY))); }, [&] { const auto half = f32Constant(0.5f); const auto centreU = Binary(state, spv::OpFSub, f32, scaledU, half); const auto centreV = Binary(state, spv::OpFSub, f32, scaledV, half); const auto floorU = ext(f32, GLSLstd450Floor, {centreU}); const auto floorV = ext(f32, GLSLstd450Floor, {centreV}); - const auto weightU = Binary(state, spv::OpFSub, f32, centreU, floorU); - const auto weightV = Binary(state, spv::OpFSub, f32, centreV, floorV); + const auto weight = [&](std::uint32_t centre, std::uint32_t floored) { + const auto fraction = Binary(state, spv::OpFSub, f32, centre, floored); + if (!converted) return fraction; + const auto steps = ext(f32, GLSLstd450RoundEven, {Binary(state, spv::OpFMul, f32, fraction, f32Constant(256.0f))}); + return Binary(state, spv::OpFMul, f32, steps, f32Constant(1.0f / 256.0f)); + }; + const auto weightU = weight(centreU, floorU); + const auto weightV = weight(centreV, floorV); const auto x0 = Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, floorU), offsetX); const auto y0 = Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, floorV), offsetY); const auto x1 = Binary(state, spv::OpIAdd, i32, x0, ConstantI32(state, 1)); const auto y1 = Binary(state, spv::OpIAdd, i32, y0, ConstantI32(state, 1)); - const auto top = ext(f32, GLSLstd450FMix, {compareTexel(x0, y0), compareTexel(x1, y0), weightU}); - const auto bottom = ext(f32, GLSLstd450FMix, {compareTexel(x0, y1), compareTexel(x1, y1), weightU}); - return ext(f32, GLSLstd450FMix, {top, bottom, weightV}); + const auto topLeft = vector(compareTexel(x0, y0)); + const auto topRight = vector(compareTexel(x1, y0)); + const auto top = mix(topLeft, topRight, weightU); + const auto bottomLeft = vector(compareTexel(x0, y1)); + const auto bottomRight = vector(compareTexel(x1, y1)); + const auto bottom = mix(bottomLeft, bottomRight, weightU); + return mix(top, bottom, weightV); }); + if (converted) { + ctx.Define(access.inst, ResultVector(ctx, access, filtered, IrTextureNumericClass::Float, false, false)); + return; + } + const auto result = filtered; const auto channelResult = [&](std::uint32_t channel) { const auto selector = RuntimeImageSwizzle(state, mem.resource, channel); return Select(state, f32, equal(selector, 0u), zero, Select(state, f32, equal(selector, 1u), one, result)); @@ -1417,7 +1473,7 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess for (std::uint32_t channel = 0; channel < 4u; ++channel) channels[channel] = channelResult(channel); const auto value = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4), value, channels[0], channels[1], channels[2], channels[3]); - ctx.Define(access.inst, TableResult(ctx, access, ResultVector(ctx, access, value, IrTextureNumericClass::Float, false, false))); + ctx.Define(access.inst, ResultVector(ctx, access, value, IrTextureNumericClass::Float, false, false)); } void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const SampleSetup& setup) { @@ -1425,7 +1481,11 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con const auto& mem = access.mem; const auto& image = access.image; if (setup.dref && (image.emulatedCompare & EmulatedCompare::Enabled) != 0u) { - EmitEmulatedCompareSample(ctx, access, setup); + EmitEmulatedSample(ctx, access, setup, false); + return; + } + if (!setup.dref && image.conversionFormat == IrBufferFormat::Format11_11_10UNorm) { + EmitEmulatedSample(ctx, access, setup, true); return; } const bool explicitLod = ImageSampleExplicitLod(mem.imageSampleFlags, state.program.Resources().stage); @@ -1495,6 +1555,9 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con const auto field = (bits >> (index * 8u)) & 0x3fu; values[index] = ConstantI32(state, static_cast(field ^ 0x20u) - 0x20); } + if (access.image.remappedDepth1D && setup.dimensionInfo.spatialComponents >= 2u) { + values[1] = ConstantI32(state, 0); + } const auto count = setup.dimensionInfo.spatialComponents; const auto offset = count == 1u ? values[0] : count == 2u ? state.module.Constant(spv::OpConstantComposite, TypeI32Vector(state, 2), values[0], values[1]) @@ -1504,7 +1567,7 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con } const auto emitSample = [&](std::uint32_t coord, std::uint32_t mask, const std::vector& arguments, std::uint32_t sampleOpcode) { - const auto sampled = MakeSampledImage(state, mem.resource, mem.sampler, access.slot); + const auto sampled = MakeSampledImage(state, mem.resource, mem.sampler); const auto sample = state.module.AllocateId(); std::vector words = {sampleOpcode, resultType, sample, sampled, coord}; if (setup.dref) { @@ -1523,7 +1586,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); }); @@ -1552,26 +1615,7 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con state.module.AddFunction(spv::OpPhi, resultType, sample, foldSample, foldEnd, operandSample, operandEnd); } const auto result = setup.dref ? sample : UnpackImageTexel(ctx, access, sample); - ctx.Define(access.inst, TableResult(ctx, access, ResultVector(ctx, access, result, setup.numericClass, setup.dref, false))); -} - -// A bindless table root's selection for this access (see EmitIndirectImageSelector), ids 0 for -// a direct image. -TableSelection TableSlot(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, const ImageResource& image) { - auto& state = ctx.state; - if (image.indirectRoot != mem.resource) { - return {}; - } - const auto* handle = inst.Argument(0); - const auto& sources = state.program.Resources().descriptorSources; - if (handle == nullptr || image.source >= sources.size() || !sources[image.source].indirectImage.has_value() || sources[image.source].indirectImage->keyArg >= handle->ArgumentCount()) { - ctx.Fail(inst, "has invalid bindless image table key provenance"); - } - if (state.flattenedSrtVariable == 0 || image.indirectSearchIterations == 0u) { - ctx.Fail(inst, "has no bindless image table runtime mapping"); - } - const auto key = ctx.Def(handle->Argument(sources[image.source].indirectImage->keyArg)); - return EmitIndirectImageSelector(ctx, image, key); + ctx.Define(access.inst, ResultVector(ctx, access, result, setup.numericClass, setup.dref, false)); } void EmitConstantSwizzleSample(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { @@ -1611,6 +1655,205 @@ void EmitSamplingOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { } } +constexpr std::uint32_t InvalidTableSelector = 0xffffffffu; + +struct ScalarBufferDword { + std::uint32_t byte = 0; + std::uint32_t inRange = 0; +}; + +ScalarBufferDword EmitScalarBufferDword(SpirvEmitterState& state, std::uint32_t offset, std::uint32_t immediate, std::uint32_t sizeLow, std::uint32_t sizeHigh) { + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + const auto sum = state.module.AllocateId(); + state.module.AddFunction(spv::OpIAddCarry, TypeU32Pair(state), sum, offset, immediate); + const auto address = state.module.AllocateId(); + const auto carry = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, address, sum, 0u); + state.module.AddFunction(spv::OpCompositeExtract, u32, carry, sum, 1u); + ScalarBufferDword dword; + dword.byte = Binary(state, spv::OpBitwiseAnd, u32, address, ConstantU32(state, ~3u)); + const auto below = Binary(state, spv::OpLogicalOr, boolean, Binary(state, spv::OpINotEqual, boolean, sizeHigh, ConstantU32(state, 0u)), Binary(state, spv::OpULessThan, boolean, dword.byte, Binary(state, spv::OpBitwiseAnd, u32, sizeLow, ConstantU32(state, ~3u)))); + dword.inRange = Binary(state, spv::OpLogicalAnd, boolean, Binary(state, spv::OpIEqual, boolean, carry, ConstantU32(state, 0u)), below); + return dword; +} + +std::uint32_t LoadTableMap(SpirvEmitterState& state, std::uint32_t index) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.tableMapVariable, ConstantU32(state, 0u), index); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + return value; +} + +std::uint32_t TableMapAt(SpirvEmitterState& state, std::uint32_t base, std::uint32_t offset) { + return LoadTableMap(state, offset == 0u ? base : Binary(state, spv::OpIAdd, TypeU32(state), base, ConstantU32(state, offset))); +} + +struct TableWord { + std::uint32_t code = 0; + std::uint32_t addressLow = 0; + std::uint32_t addressHigh = 0; + std::uint32_t poisoned = 0; +}; + +TableWord EmitTableWord(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t source, std::uint32_t table, std::size_t argument) { + auto& state = ctx.state; + const IrValue* handle = inst.Argument(argument)->Resolve(); + const auto& sources = state.program.Resources().descriptorSources; + if (handle->ArgumentCount() == 0u || source >= sources.size() || !sources[source].tableColumn.has_value()) ctx.Fail(inst, "has an image table handle without its column"); + const auto& column = *sources[source].tableColumn; + const auto offset = ctx.Def(handle->Argument(0)); + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + const auto header = ImageTableAbi::TableHeader(table); + const auto field = [&](std::uint32_t index) { return LoadTableMap(state, ConstantU32(state, header + index)); }; + const auto mapStart = field(ImageTableAbi::TableMapStart); + const auto keys = field(ImageTableAbi::TableKeyCount); + const auto sizeLow = field(ImageTableAbi::TableSizeLow); + const auto sizeHigh = field(ImageTableAbi::TableSizeHigh); + const auto baseLow = field(ImageTableAbi::TableBaseLow); + const auto baseHigh = field(ImageTableAbi::TableBaseHigh); + const auto outsideCode = field(ImageTableAbi::TableOutsideCode); + const auto tableFault = field(ImageTableAbi::TableFault); + const auto relative = Binary(state, spv::OpISub, u32, offset, ConstantU32(state, column.addend)); + const auto record = Binary(state, spv::OpUDiv, u32, relative, ConstantU32(state, column.stride)); + const auto remainder = Binary(state, spv::OpISub, u32, relative, Binary(state, spv::OpIMul, u32, record, ConstantU32(state, column.stride))); + const auto aligned = Binary(state, spv::OpIEqual, boolean, remainder, ConstantU32(state, 0u)); + const auto inMap = Binary(state, spv::OpLogicalAnd, boolean, aligned, Binary(state, spv::OpULessThan, boolean, record, keys)); + const auto mapped = LoadTableMap(state, Select(state, u32, inMap, Binary(state, spv::OpIAdd, u32, mapStart, record), ConstantU32(state, 0u))); + if (column.address) { + TableWord word; + word.code = Select(state, u32, Binary(state, spv::OpINotEqual, boolean, tableFault, ConstantU32(state, 0u)), tableFault, Select(state, u32, inMap, mapped, ConstantU32(state, ImageTableAbi::PoisonCode(ImageTableAbi::OutsideSnapshotPoison)))); + const auto immediate = column.offset & ~3u; + const auto first = state.module.AllocateId(); + state.module.AddFunction(spv::OpIAddCarry, TypeU32Pair(state), first, baseLow, Binary(state, spv::OpBitwiseAnd, u32, offset, ConstantU32(state, ~3u))); + const auto firstLow = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, firstLow, first, 0u); + const auto firstCarry = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, firstCarry, first, 1u); + const auto second = state.module.AllocateId(); + state.module.AddFunction(spv::OpIAddCarry, TypeU32Pair(state), second, firstLow, ConstantU32(state, immediate)); + word.addressLow = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, word.addressLow, second, 0u); + const auto secondCarry = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, secondCarry, second, 1u); + const auto extension = ConstantU32(state, (immediate & 0x80000000u) != 0u ? 0xffffffffu : 0u); + word.addressHigh = Binary(state, spv::OpIAdd, u32, Binary(state, spv::OpIAdd, u32, baseHigh, extension), Binary(state, spv::OpIAdd, u32, firstCarry, secondCarry)); + word.poisoned = Binary(state, spv::OpINotEqual, boolean, Binary(state, spv::OpBitwiseAnd, u32, word.code, ConstantU32(state, ImageTableAbi::PoisonFlag)), ConstantU32(state, 0u)); + return word; + } + const auto dword = EmitScalarBufferDword(state, offset, ConstantU32(state, column.offset), sizeLow, sizeHigh); + const auto outside = Select(state, u32, dword.inRange, ConstantU32(state, ImageTableAbi::PoisonCode(ImageTableAbi::OutsideSnapshotPoison)), outsideCode); + TableWord word; + word.code = Select(state, u32, Binary(state, spv::OpINotEqual, boolean, tableFault, ConstantU32(state, 0u)), tableFault, Select(state, u32, inMap, mapped, outside)); + const auto low = state.module.AllocateId(); + state.module.AddFunction(spv::OpIAddCarry, TypeU32Pair(state), low, baseLow, dword.byte); + const auto carry = state.module.AllocateId(); + word.addressLow = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, word.addressLow, low, 0u); + state.module.AddFunction(spv::OpCompositeExtract, u32, carry, low, 1u); + word.addressHigh = Binary(state, spv::OpIAdd, u32, baseHigh, carry); + word.poisoned = Binary(state, spv::OpINotEqual, boolean, Binary(state, spv::OpBitwiseAnd, u32, word.code, ConstantU32(state, ImageTableAbi::PoisonFlag)), ConstantU32(state, 0u)); + return word; +} + +void EmitTableFault(SpirvValueEmitContext& ctx, const IrValue& inst, const TableWord& word, std::uint32_t poison, std::uint32_t condition) { + auto& state = ctx.state; + EmitIfCondition(state, condition, [&] { + RecordBdaFaultWords(state, word.addressLow, word.addressHigh, Binary(state, spv::OpBitwiseAnd, TypeU32(state), poison, ConstantU32(state, ~ImageTableAbi::PoisonFlag)), ConstantU32(state, inst.Flags().pc), BdaAbi::FaultReason::ImageTable); + }); +} + +std::uint32_t ZeroResult(SpirvValueEmitContext& ctx, const IrValue& inst) { + auto& state = ctx.state; + switch (inst.Type()) { + case IrType::U32: return ConstantU32(state, 0u); + case IrType::U64: return ConstantU64(state, 0u); + case IrType::U32x4: return ConstantU32CompositeZero(state, 4u); + default: break; + } + ctx.Fail(inst, "has no zero result for an image table access"); +} + +struct TableAccessResult { + std::uint32_t valid = 0; + std::uint32_t flags = 0; + bool unsupported = false; +}; + +TableAccessResult EmitTableAccess(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& memory, const ImageResource& base, bool tableSampler) { + auto& state = ctx.state; + if (state.tableMapVariable == 0 || state.faultBufferVariable == 0) ctx.Fail(inst, "reads an image table without its map or fault buffer"); + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + const auto both = [&](std::uint32_t left, std::uint32_t right) { return Binary(state, spv::OpLogicalAnd, boolean, left, right); }; + const auto negate = [&](std::uint32_t value) { return Unary(state, spv::OpLogicalNot, boolean, value); }; + const auto flagged = [&](std::uint32_t value) { return Binary(state, spv::OpINotEqual, boolean, Binary(state, spv::OpBitwiseAnd, u32, value, ConstantU32(state, ImageTableAbi::PoisonFlag)), ConstantU32(state, 0u)); }; + const auto exec = ctx.Arg(inst, inst.ArgumentCount() - 1u); + TableAccessResult result; + result.valid = ConstantBool(state, true); + auto live = ConstantBool(state, true); + state.tableAccess = {}; + if (base.table != NoTable) { + const auto word = EmitTableWord(ctx, inst, base.source, base.table, 0u); + EmitTableFault(ctx, inst, word, word.code, both(exec, word.poisoned)); + if (base.tableOperation == TableOperation::Unsupported || state.program.Info().runtimeImageModes.at(memory.resource).empty()) { + if (inst.Type() != IrType::Void) ctx.Define(inst, ZeroResult(ctx, inst)); + result.unsupported = true; + return result; + } + const auto selected = both(Binary(state, spv::OpINotEqual, boolean, word.code, ConstantU32(state, ImageTableAbi::NullCode)), negate(word.poisoned)); + const auto record = Select(state, u32, selected, word.code, ConstantU32(state, 0u)); + const auto flags = TableMapAt(state, record, ImageTableAbi::ImageRecordFlags); + const auto dword3 = TableMapAt(state, record, ImageTableAbi::ImageRecordDword3); + const auto element = TableMapAt(state, record, ImageTableAbi::ImageRecordElement); + const auto rejected = both(selected, flagged(flags)); + EmitTableFault(ctx, inst, word, flags, both(exec, rejected)); + const auto inBounds = Binary(state, spv::OpULessThan, boolean, element, LoadTableMap(state, ConstantU32(state, ImageTableAbi::HeaderElementCount))); + const auto outside = both(both(selected, negate(rejected)), negate(inBounds)); + EmitTableFault(ctx, inst, word, ConstantU32(state, ImageTableAbi::OutsideSnapshotPoison), both(exec, outside)); + const auto checked = both(both(selected, negate(rejected)), inBounds); + const auto faults = LoadTableMap(state, ConstantU32(state, ImageTableAbi::HeaderElementFaults)); + const auto driverFault = LoadTableMap(state, Select(state, u32, checked, Binary(state, spv::OpIAdd, u32, faults, element), ConstantU32(state, 0u))); + const auto driverRejected = both(checked, flagged(driverFault)); + EmitTableFault(ctx, inst, word, driverFault, both(exec, driverRejected)); + result.valid = both(checked, negate(driverRejected)); + result.flags = flags; + live = selected; + state.tableAccess.element = Binary(state, spv::OpIAdd, u32, LoadTableMap(state, ConstantU32(state, ImageTableAbi::HeaderElementBase)), 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) { + const auto& sampler = state.program.Info().samplers.at(memory.sampler); + const auto word = EmitTableWord(ctx, inst, sampler.source, sampler.table, 1u); + EmitTableFault(ctx, inst, word, word.code, both(both(exec, live), word.poisoned)); + const auto selected = negate(word.poisoned); + const auto record = Select(state, u32, selected, word.code, ConstantU32(state, 0u)); + const auto flags = TableMapAt(state, record, ImageTableAbi::SamplerRecordFlags); + const auto element = TableMapAt(state, record, ImageTableAbi::SamplerRecordElement); + const auto rejected = both(selected, flagged(flags)); + EmitTableFault(ctx, inst, word, flags, both(both(exec, live), rejected)); + const auto end = Binary(state, spv::OpIAdd, u32, element, ConstantU32(state, ImageTableAbi::SamplerEntryElements)); + const auto inBounds = Binary(state, spv::OpULessThanEqual, boolean, end, LoadTableMap(state, ConstantU32(state, ImageTableAbi::HeaderSamplerCount))); + const auto outside = both(both(selected, negate(rejected)), negate(inBounds)); + EmitTableFault(ctx, inst, word, ConstantU32(state, ImageTableAbi::OutsideSnapshotPoison), both(both(exec, live), outside)); + const auto checked = both(both(selected, negate(rejected)), inBounds); + const auto entry = Binary(state, spv::OpShiftRightLogical, u32, element, ConstantU32(state, 2u)); + const auto driverFault = LoadTableMap(state, Select(state, u32, checked, Binary(state, spv::OpIAdd, u32, LoadTableMap(state, ConstantU32(state, ImageTableAbi::HeaderSamplerFaults)), entry), ConstantU32(state, 0u))); + const auto driverRejected = both(checked, flagged(driverFault)); + EmitTableFault(ctx, inst, word, driverFault, both(both(exec, live), driverRejected)); + result.valid = both(result.valid, both(checked, negate(driverRejected))); + state.tableAccess.samplerElement = element; + DecorateTableNonUniform(state, state.tableAccess.samplerElement); + } + state.tableAccess.active = true; + return result; +} + } void EmitImageOperation(SpirvModule& module, const IrValue& value, std::uint32_t resultId) { @@ -1621,7 +1864,7 @@ void EmitImageOperation(SpirvValueEmitContext& context, const IrValue& value, st throw std::logic_error("EmitImageOperation(SpirvValueEmitContext&) is not used: image instructions are emitted per opcode through EmitImage"); } -void EmitImageMode(SpirvValueEmitContext& ctx, const IrValue& inst, const ImageResource& image, const TableSelection& table) { +void EmitImageMode(SpirvValueEmitContext& ctx, const IrValue& inst, const ImageResource& image) { const auto irOpcode = inst.Opcode(); const auto imageInfo = ImageOpcodeInfoOf(irOpcode); const auto& mem = ctx.Memory(inst); @@ -1630,7 +1873,7 @@ void EmitImageMode(SpirvValueEmitContext& ctx, const IrValue& inst, const ImageR if (address == nullptr) { ctx.Fail(inst, "has no image address"); } - const ImageEmitAccess access{inst, mem, image, *address, table}; + const ImageEmitAccess access{inst, mem, image, *address}; if (imageInfo.access == ImageAccess::Atomic) { EmitAtomicOp(ctx, access); return; @@ -1661,48 +1904,61 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; const auto& memory = ctx.Memory(inst); const auto& base = ImageResourceOf(state, memory); - const auto modes = ResourceMaterializer::RuntimeImageModes(base); - state.runtimeImageMetadata = ConstantU32(state, memory.resource); - const auto table = TableSlot(ctx, inst, memory, base); - if (table.slot != 0u) { - state.runtimeImageMetadata = table.slot; + const auto imageInfo = ImageOpcodeInfoOf(inst.Opcode()); + const auto& samplers = state.program.Info().samplers; + const bool tableSampler = imageInfo.needsSampler && memory.sampler < samplers.size() && samplers[memory.sampler].table != NoTable; + const bool tables = base.table != NoTable || tableSampler; + const auto modes = base.table != NoTable ? state.program.Info().runtimeImageModes.at(memory.resource) : ResourceMaterializer::RuntimeImageModes(base); + std::uint32_t tableSelector = 0u; + std::uint32_t samplerGuard = 0u; + if (tables) { + const auto access = EmitTableAccess(ctx, inst, memory, base, tableSampler); + if (access.unsupported) { + state.tableAccess = {}; + return; + } + if (base.table != NoTable) { + const auto mode = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeU32(state), access.flags, ConstantU32(state, ImageTableAbi::ModeShift)), ConstantU32(state, ImageTableAbi::ModeMask)); + tableSelector = Select(state, TypeU32(state), access.valid, mode, ConstantU32(state, InvalidTableSelector)); + } else { + tableSelector = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + memory.resource * PipelineSpecialization::ImageWords, 0u); + samplerGuard = access.valid; + } } const auto emitMode = [&](const ImageResource& mode) { state.runtimeImage = &mode; - if (mode.packedFormat == IrBufferFormat::Fmask8_S2_F1) { - if (inst.Opcode() == IrOpcode::ImageRead && memory.dataBits == 32u) { - const auto value = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), value, ConstantU32(state, 0x76543210u), ConstantU32(state, 0xfedcba98u), ConstantU32(state, 0u), ConstantU32(state, 0u)); - const auto active = ctx.Arg(inst, 2u); - const auto mapped = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeConstruct, TypeBoolVector(state, 4u), mapped, active, active, active, active); - ctx.Define(inst, Select(state, TypeU32Vector(state, 4u), mapped, value, ConstantU32CompositeZero(state, 4u))); - } else { - ctx.Fail(inst, "FMASK requires a 32-bit image read"); - } - } else if (mode.depthBits && memory.dataBits == 16u) { + if (mode.depthBits && memory.dataBits == 16u) { ctx.Fail(inst, "depth bits require a 32-bit image result"); } else { - EmitImageMode(ctx, inst, mode, table); + EmitImageMode(ctx, inst, mode); } }; - if (modes.size() == 1u) { + if (modes.size() == 1u && !tables) { emitMode(modes.front()); state.runtimeImage = nullptr; - state.runtimeImageMetadata = 0u; return; } - const auto selector = base.indirectRoot == ImageResource::NoIndirectImage && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + memory.resource * PipelineSpecialization::ImageWords, 0u) : RuntimeImageMode(state, memory.resource); + const auto selector = tables ? tableSelector : !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + memory.resource * PipelineSpecialization::ImageWords, 0u) : RuntimeImageMode(state, memory.resource); const bool returnsValue = inst.Opcode() != IrOpcode::ImageWrite; - const auto resultId = returnsValue ? ctx.Result(inst) : 0u; + const auto guardedId = returnsValue ? ctx.Result(inst) : 0u; ctx.definitions.erase(&inst); const bool exits = !state.continueTarget; + const auto guardMerge = samplerGuard != 0u ? state.module.AllocateId() : 0u; + const auto guardSkip = samplerGuard != 0u ? state.module.AllocateId() : 0u; + if (samplerGuard != 0u) { + const auto guardBody = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, guardMerge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, samplerGuard, guardBody, guardSkip); + EmitLabel(state, guardBody); + } + const auto resultId = returnsValue && samplerGuard != 0u ? state.module.AllocateId() : guardedId; const auto merge = state.module.AllocateId(); - const auto invalid = exits ? state.module.AllocateId() : merge; - const auto undefined = returnsValue && !exits ? state.module.AllocateId() : 0u; + const auto invalid = tables || exits ? state.module.AllocateId() : merge; + const auto undefined = returnsValue && !tables && !exits ? state.module.AllocateId() : 0u; std::vector labels(modes.size()); std::vector words{spv::OpSwitch, selector, invalid}; for (std::uint32_t index = 0u; index < modes.size(); ++index) { + if (base.table != NoTable && !TableModeEmitted(modes[index])) continue; labels[index] = state.module.AllocateId(); words.insert(words.end(), {index, labels[index]}); } @@ -1714,40 +1970,20 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); state.module.AddFunction(words); for (std::uint32_t index = 0u; index < modes.size(); ++index) { + if (labels[index] == 0u) continue; EmitLabel(state, labels[index]); - std::uint32_t modeMerge = 0u; - if (base.indirectRoot != ImageResource::NoIndirectImage && !state.registrationModules) { - const auto enabled = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageModeBase + memory.resource * PipelineSpecialization::ImageModeStride + index, 1u); - const auto active = Binary(state, spv::OpINotEqual, TypeBool(state), enabled, ConstantU32(state, 0u)); - const auto activeLabel = state.module.AllocateId(); - modeMerge = state.module.AllocateId(); - const auto inactiveLabel = exits ? state.module.AllocateId() : modeMerge; - state.module.AddFunction(spv::OpSelectionMerge, modeMerge, spv::SelectionControlMaskNone); - state.module.AddFunction(spv::OpBranchConditional, active, activeLabel, inactiveLabel); - if (exits) { - EmitLabel(state, inactiveLabel); - state.module.AddFunction(spv::OpUnreachable); - } - EmitLabel(state, activeLabel); - } emitMode(modes[index]); - if (modeMerge != 0u) { - const auto activeEnd = state.currentLabel; - state.module.AddFunction(spv::OpBranch, modeMerge); - EmitLabel(state, modeMerge); - if (undefined != 0u) { - const auto joined = state.module.AllocateId(); - state.module.AddFunction(spv::OpPhi, TypeId(state, inst.Type()), joined, ctx.Def(&inst), activeEnd, undefined, labels[index]); - ctx.definitions.insert_or_assign(&inst, joined); - } - } if (returnsValue) incoming.insert(incoming.end(), {ctx.Def(&inst), state.currentLabel}); ctx.definitions.erase(&inst); state.module.AddFunction(spv::OpBranch, merge); } state.runtimeImage = nullptr; - state.runtimeImageMetadata = 0u; - if (exits) { + state.tableAccess = {}; + if (tables) { + EmitLabel(state, invalid); + if (returnsValue) incoming.insert(incoming.end(), {ConstantU32CompositeZero(state, 4u), invalid}); + state.module.AddFunction(spv::OpBranch, merge); + } else if (exits) { EmitLabel(state, invalid); state.module.AddFunction(spv::OpUnreachable); } @@ -1756,8 +1992,15 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { std::vector phi{spv::OpPhi, TypeId(state, inst.Type()), resultId}; phi.insert(phi.end(), incoming.begin(), incoming.end()); state.module.AddFunction(phi); - ctx.definitions.emplace(&inst, resultId); } + if (samplerGuard != 0u) { + state.module.AddFunction(spv::OpBranch, guardMerge); + EmitLabel(state, guardSkip); + state.module.AddFunction(spv::OpBranch, guardMerge); + EmitLabel(state, guardMerge); + if (returnsValue) state.module.AddFunction(spv::OpPhi, TypeId(state, inst.Type()), guardedId, resultId, merge, ConstantU32CompositeZero(state, 4u), guardSkip); + } + if (returnsValue) ctx.definitions.emplace(&inst, guardedId); } void EmitGetImageResource(SpirvValueEmitContext& context) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 44745b1c8b..192b60cc27 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"); } @@ -37,8 +37,8 @@ std::uint32_t EmitShaderDataDwordLoad(SpirvEmitterState& state, std::uint32_t dw const auto pointer = state.module.AllocateId(); const auto value = state.module.AllocateId(); if (layout.UsesPushData()) { - const auto base = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::PushDataOffset, layout.pushDataStartDword); - const auto index = Binary(state, spv::OpIAdd, TypeU32(state), Binary(state, spv::OpISub, TypeU32(state), base, ConstantU32(state, layout.PushSlotDword())), ConstantU32(state, dwordIndex)); + const auto base = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::PushDataOffset, state.stencilReplay ? layout.pushDataStartDword % PushData::DwordCount : layout.pushDataStartDword); + const auto index = Binary(state, spv::OpIAdd, TypeU32(state), Binary(state, spv::OpISub, TypeU32(state), base, ConstantU32(state, state.stencilReplay ? 0u : layout.PushSlotDword())), ConstantU32(state, dwordIndex)); state.module.AddFunction(spv::OpAccessChain, TypePushConstantElementPointer(state), pointer, state.pushConstantVariable, ConstantU32(state, 0), index); } else if (state.shaderDataStorageVariable != 0) { state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.shaderDataStorageVariable, ConstantU32(state, 0), ConstantU32(state, dwordIndex)); @@ -111,6 +111,15 @@ std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state) { return pointer; } +std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index) { + if (!state.requirements.ldsBufferInt64Atomics || state.ldsBufferU64Variable == 0u || state.ldsBufferBase == 0u) { + FailEmit("64-bit device memory LDS atomics were not prepared at function entry"); + } + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassStorageBuffer, TypeScalarU64(state)), pointer, state.ldsBufferU64Variable, ConstantU32(state, 0u), EmitBinaryU32(state, spv::OpShiftRightLogical, EmitAddU32(state, state.ldsBufferBase, index), ConstantU32(state, 1u))); + return pointer; +} + MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const MemoryInfo& mem) { MemoryResourceAccess access; access.kind = mem.kind; @@ -173,6 +182,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 4989ed437a..946baa6068 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -2,7 +2,6 @@ #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" #include "SpirvBackend/SpirvEmitterHelpers.hpp" -#include "Optimization/ResourceMaterializer.hpp" #include "PipelineSpecialization.hpp" #include #include @@ -56,7 +55,7 @@ std::uint32_t DescriptorElementPointer(SpirvEmitterState& state, std::uint32_t r } std::uint32_t RuntimeImageDword(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t member) { - const auto index = state.runtimeImageMetadata != 0u ? state.runtimeImageMetadata : ConstantU32(state, resource); + const auto index = ConstantU32(state, resource); const auto dword = Binary(state, spv::OpIAdd, TypeU32(state), ConstantU32(state, state.program.Metadata().bindings.ImageMetadataDword() + member), Binary(state, spv::OpIMul, TypeU32(state), index, ConstantU32(state, sizeof(RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t)))); const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.shaderDataStorageVariable, ConstantU32(state, 0u), dword); @@ -72,40 +71,29 @@ std::uint32_t RuntimeMipCount(SpirvEmitterState& state, std::uint32_t resource) return Binary(state, spv::OpIAdd, TypeU32(state), Binary(state, spv::OpISub, TypeU32(state), last, first), ConstantU32(state, 1u)); } -std::uint32_t TableImageIndex(SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource) { - const bool storage = ImageBindingResourceClass(kind) == ImageResourceClass::Storage; - const auto supported = [&](std::uint32_t capability) { - return std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), capability) != state.supportedCapabilities.end(); - }; - const auto dynamic = storage ? spv::CapabilityStorageImageArrayDynamicIndexing : spv::CapabilitySampledImageArrayDynamicIndexing; - if (!supported(dynamic)) { - ExitDescriptorBindingFailure(state, kind, resource, "bindless image table needs image array dynamic indexing, which the device lacks"); - } - state.module.EmitCapability(dynamic); - if (state.runtimeImageMetadata == 0u) FailEmit("bindless image has no runtime metadata index"); - const auto offset = Binary(state, spv::OpIAdd, TypeU32(state), ConstantU32(state, state.program.Metadata().bindings.ImageMetadataDword() + offsetof(RuntimeAbi::ResourceMetadata, firstElement) / sizeof(std::uint32_t)), Binary(state, spv::OpIMul, TypeU32(state), state.runtimeImageMetadata, ConstantU32(state, sizeof(RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t)))); - const auto metadata = state.module.AllocateId(); - state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), metadata, state.shaderDataStorageVariable, ConstantU32(state, 0u), offset); - const auto index = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, TypeU32(state), index, metadata); - if (!state.tableIndexNonUniform) { - return index; - } - const auto nonUniform = storage ? spv::CapabilityStorageImageArrayNonUniformIndexing : spv::CapabilitySampledImageArrayNonUniformIndexing; - const bool extension = state.spirvVersion < 0x00010500u; - if (!supported(spv::CapabilityShaderNonUniform) || !supported(nonUniform) || (extension && std::find(state.supportedExtensions.begin(), state.supportedExtensions.end(), "SPV_EXT_descriptor_indexing") == state.supportedExtensions.end())) { - ResourceMaterializer::CountBindlessRejection(BindlessRejection::NonUniform); - throw std::runtime_error("bindless image table: the slot is not uniform over the workgroup and the device lacks non-uniform image indexing"); - } +void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id) { + if (!state.tableIndexNonUniform) return; state.module.EmitCapability(spv::CapabilityShaderNonUniform); - state.module.EmitCapability(nonUniform); - if (extension) state.module.EmitExtension("SPV_EXT_descriptor_indexing"); - state.module.AddAnnotation(spv::OpDecorate, index, spv::DecorationNonUniform); - return index; + 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); } -void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t slotId, std::uint32_t id) { - if (slotId != 0 && state.tableIndexNonUniform) 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; +} + +bool TablePointSampled(const ImageResource& image) { + return image.numericClass == IrTextureNumericClass::Uint || image.numericClass == IrTextureNumericClass::Sint || image.conversionFormat != IrBufferFormat::Invalid || image.depthBits; } std::uint32_t ImageScalarType(SpirvEmitterState& state, IrTextureNumericClass numericClass) { @@ -165,52 +153,70 @@ std::uint32_t ImageViewSizeType(SpirvEmitterState& state, RdnaImageDimension dim } } -std::uint32_t LoadSampledImageDescriptor(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t slotId) { +std::uint32_t LoadSampledImageDescriptor(SpirvEmitterState& state, std::uint32_t resource) { const auto& imageResource = state.runtimeImage != nullptr ? *state.runtimeImage : state.program.Info().images.at(resource); if (imageResource.resourceClass != ImageResourceClass::Sampled) { FailEmit("sampled image descriptor requested for a non-sampled image"); } const auto kind = DescriptorBindingForImage(imageResource); - const auto arrayIndex = ResourceForDescriptor(state, kind, resource); - const auto variable = state.imageVariables.at(ImageBindingIndex(kind)); const auto imageType = ImageType(state, imageResource); const auto pointerType = state.module.Type(spv::OpTypePointer, spv::StorageClassUniformConstant, imageType); + if (imageResource.table != NoTable) { + 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 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); + if (!state.tableAccess.uniform) DecorateTableNonUniform(state, pointer); + const auto image = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, imageType, image, pointer); + if (!state.tableAccess.uniform) DecorateTableNonUniform(state, image); + return image; + } + const auto arrayIndex = ResourceForDescriptor(state, kind, resource); + const auto variable = state.imageVariables.at(ImageBindingIndex(kind)); if (variable == 0) { ExitDescriptorBindingFailure(state, kind, resource, "sampled image descriptor array was not emitted"); } - std::uint32_t pointer = 0; - if (slotId == 0) { - pointer = DescriptorElementPointer(state, pointerType, variable, arrayIndex, kind, resource, "sampled image descriptor array was not emitted"); - } else { - pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpAccessChain, pointerType, pointer, variable, TableImageIndex(state, kind, resource)); - DecorateTableNonUniform(state, slotId, pointer); - } + const auto pointer = DescriptorElementPointer(state, pointerType, variable, arrayIndex, kind, resource, "sampled image descriptor array was not emitted"); const auto image = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, imageType, image, pointer); - DecorateTableNonUniform(state, slotId, image); return image; } std::uint32_t LoadSamplerDescriptor(SpirvEmitterState& state, std::uint32_t sampler) { - const bool point = state.runtimeImage != nullptr && (state.runtimeImage->numericClass == IrTextureNumericClass::Sint || state.runtimeImage->conversionFormat != IrBufferFormat::Invalid || state.runtimeImage->depthBits); - const auto arrayIndex = ResourceForDescriptor(state, DescriptorBindingKind::Samplers, sampler) + (point ? 1u : 0u); + const auto& samplerResource = state.program.Info().samplers.at(sampler); + const auto& image = state.runtimeImage; + const bool table = samplerResource.table != NoTable || (image != nullptr && image->table != NoTable); + const bool point = image != nullptr && (table ? TablePointSampled(*image) : (image->numericClass == IrTextureNumericClass::Sint || image->conversionFormat != IrBufferFormat::Invalid || image->depthBits)); const auto samplerType = state.module.Type(spv::OpTypeSampler); const auto pointerType = state.module.Type(spv::OpTypePointer, spv::StorageClassUniformConstant, samplerType); + if (samplerResource.table != NoTable) { + if (state.tableSamplerVariable == 0 || !state.tableAccess.active) ExitDescriptorBindingFailure(state, DescriptorBindingKind::SamplerTable, sampler, "sampler table array was not emitted"); + const auto variant = image != nullptr && image->depthCompare ? 2u : 0u; + const auto element = Binary(state, spv::OpIAdd, TypeU32(state), state.tableAccess.samplerElement, ConstantU32(state, variant + (point ? 1u : 0u))); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, pointerType, pointer, state.tableSamplerVariable, element); + DecorateTableNonUniform(state, pointer); + const auto loaded = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, samplerType, loaded, pointer); + DecorateTableNonUniform(state, loaded); + return loaded; + } + const auto arrayIndex = ResourceForDescriptor(state, DescriptorBindingKind::Samplers, sampler) + (point ? 1u : 0u); const auto pointer = DescriptorElementPointer(state, pointerType, state.samplerVariable, arrayIndex, DescriptorBindingKind::Samplers, sampler, "sampler descriptor array was not emitted"); const auto samplerId = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, samplerType, samplerId, pointer); return samplerId; } -std::uint32_t MakeSampledImage(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t sampler, std::uint32_t slotId) { +std::uint32_t MakeSampledImage(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t sampler) { const auto& imageResource = state.runtimeImage != nullptr ? *state.runtimeImage : state.program.Info().images.at(resource); - const auto image = LoadSampledImageDescriptor(state, resource, slotId); + const auto image = LoadSampledImageDescriptor(state, resource); const auto samplerId = LoadSamplerDescriptor(state, sampler); 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); - DecorateTableNonUniform(state, slotId, sampledImage); + if ((imageResource.table != NoTable && !state.tableAccess.uniform) || state.program.Info().samplers.at(sampler).table != NoTable) DecorateTableNonUniform(state, sampledImage); return sampledImage; } @@ -220,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"); } @@ -235,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 9f1ca47c70..a8068f0415 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -1,6 +1,7 @@ #include "Optimization/ResourceMaterializer.hpp" #include "BdaAbi.hpp" #include "PipelineSpecialization.hpp" +#include "StencilReplay.hpp" #include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" @@ -53,10 +54,31 @@ const ShaderWorkgroupInputInfo* ShaderWorkgroupInput(const SpirvEmitterState& st } } +std::array WorkgroupLocalSize(const SpirvEmitterState& state, const ShaderWorkgroupInputInfo& workgroup) { + const std::uint32_t derivativeDefault = state.requirements.computeDerivatives ? 2u : 1u; + std::array size { + workgroup.threadsNum[0] != 0u ? workgroup.threadsNum[0] : derivativeDefault, + workgroup.threadsNum[1] != 0u ? workgroup.threadsNum[1] : derivativeDefault, + workgroup.threadsNum[2] != 0u ? workgroup.threadsNum[2] : 1u}; + if (state.spannedWave) { + size = {64u, 1u, 1u}; + } + if (state.laneCount == 2) { + size = {((size[0] * size[1] * size[2] + 63u) / 64u) * 32u, 1u, 1u}; + } + return size; +} + namespace { constexpr std::uint32_t NoBuiltIn = std::numeric_limits::max(); +void RequireTableIndexing(SpirvEmitterState& state) { + if (state.spirvVersion < 0x00010500u) state.module.EmitExtension("SPV_EXT_descriptor_indexing"); + state.module.EmitCapability(spv::CapabilityRuntimeDescriptorArray); + state.module.EmitCapability(spv::CapabilitySampledImageArrayDynamicIndexing); +} + std::uint32_t BuiltInForInput(StageInputKind kind) { switch (kind) { case StageInputKind::VertexIndex: return spv::BuiltInVertexIndex; @@ -64,11 +86,13 @@ std::uint32_t BuiltInForInput(StageInputKind kind) { case StageInputKind::PrimitiveId: return spv::BuiltInPrimitiveId; case StageInputKind::TessCoord: return spv::BuiltInTessCoord; case StageInputKind::InstanceIndex: return spv::BuiltInInstanceIndex; + case StageInputKind::BaseInstance: return spv::BuiltInBaseInstance; case StageInputKind::FragCoord: return spv::BuiltInFragCoord; case StageInputKind::FrontFacing: return spv::BuiltInFrontFacing; case StageInputKind::HelperInvocation: return spv::BuiltInHelperInvocation; case StageInputKind::Layer: return spv::BuiltInLayer; case StageInputKind::SampleId: return spv::BuiltInSampleId; + case StageInputKind::SampleMask: return spv::BuiltInSampleMask; case StageInputKind::BaryCoordSmooth: return spv::BuiltInBaryCoordKHR; case StageInputKind::BaryCoordNoPerspective: return spv::BuiltInBaryCoordNoPerspKHR; case StageInputKind::WorkgroupId: return spv::BuiltInWorkgroupId; @@ -157,7 +181,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; @@ -172,6 +196,11 @@ void DefineInputs(SpirvEmitterState& state) { addBuiltin(StageInputKind::WorkgroupId, 3u, "gl_WorkGroupID"); } } + if (state.requirements.ldsLock && std::none_of(state.inputs.begin(), state.inputs.end(), [](const SpirvInputBinding& input) { + return input.kind == StageInputKind::LocalInvocationIndex; + })) { + state.inputs.push_back(SpirvInputBinding {{StageInputKind::LocalInvocationIndex, 0u, 1u, "gl_LocalInvocationIndex", false}}); + } if (LdsInDeviceMemory(state)) { if (std::none_of(state.inputs.begin(), state.inputs.end(), [](const SpirvInputBinding& input) { return input.kind == StageInputKind::WorkgroupId; @@ -189,7 +218,14 @@ void DefineInputs(SpirvEmitterState& state) { const auto reads = [&](StageInputKind kind) { return std::any_of(state.inputs.begin(), state.inputs.end(), [&](const SpirvInputBinding& input) { return input.kind == kind; }); }; - emulation = LayoutBarycentricEmulation(parameters, {true, reads(StageInputKind::BaryCoordSmooth), reads(StageInputKind::BaryCoordNoPerspective)}); + emulation = LayoutBarycentricEmulation(parameters, {true, reads(StageInputKind::BaryCoordSmooth), reads(StageInputKind::BaryCoordNoPerspective)}, state.meshBarycentricPixel); + if (state.meshBarycentricPixel && emulation.positionsLocation != BarycentricEmulationLayout::NoLocation) { + state.pixelFlatPositions = DefineInterfaceVariable(state, state.module.Type(spv::OpTypeArray, TypeF32Vector(state, 4u), ConstantU32(state, 3u)), spv::StorageClassInput, "meshBarycentricPositions"); + state.module.AddAnnotation(spv::OpDecorate, state.pixelFlatPositions, spv::DecorationLocation, emulation.positionsLocation); + state.module.AddAnnotation(spv::OpDecorate, state.pixelFlatPositions, spv::DecorationPerPrimitiveEXT); + state.module.EmitCapability(spv::CapabilityMeshShadingEXT); + state.module.EmitExtension("SPV_EXT_mesh_shader"); + } } for (auto& input : state.inputs) { if (pixelStage && input.kind == StageInputKind::Parameter) { @@ -211,6 +247,7 @@ void DefineInputs(SpirvEmitterState& state) { case StageInputKind::InvocationId: case StageInputKind::PrimitiveId: case StageInputKind::InstanceIndex: + case StageInputKind::BaseInstance: case StageInputKind::Layer: case StageInputKind::SampleId: type = TypeI32(state); @@ -232,6 +269,9 @@ void DefineInputs(SpirvEmitterState& state) { case StageInputKind::HelperInvocation: type = TypeBool(state); break; + case StageInputKind::SampleMask: + type = SampleMaskArrayType(state); + break; case StageInputKind::Parameter: if (state.program.Resources().stage == IrShaderStage::Vertex || state.program.Resources().stage == IrShaderStage::Local) { type = VertexParameterScalarType(state, VertexParameterScalarKind(state, input.location)); @@ -249,7 +289,7 @@ void DefineInputs(SpirvEmitterState& state) { break; } input.variableId = DefineInterfaceVariable(state, type, spv::StorageClassInput, input.debugName.c_str()); - if (input.kind == StageInputKind::Layer || input.kind == StageInputKind::SampleId) { + if (input.kind == StageInputKind::Layer || input.kind == StageInputKind::SampleId || input.kind == StageInputKind::SampleMask) { state.module.AddAnnotation(spv::OpDecorate, input.variableId, spv::DecorationFlat); } if (input.kind == StageInputKind::Parameter) { @@ -261,7 +301,7 @@ void DefineInputs(SpirvEmitterState& state) { throw std::runtime_error("SPIR-V module emission failed: per-vertex parameter " + std::to_string(location) + " has no emulated location"); } location = relocated->second; - state.module.AddAnnotation(spv::OpDecorate, input.variableId, spv::DecorationFlat); + state.module.AddAnnotation(spv::OpDecorate, input.variableId, state.meshBarycentricPixel ? spv::DecorationPerPrimitiveEXT : spv::DecorationFlat); } else if (input.perVertex) { state.module.AddAnnotation(spv::OpDecorate, input.variableId, spv::DecorationPerVertexKHR); } else if (flat) { @@ -284,11 +324,25 @@ 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); } } + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->QuadSampleMaskPartial()) { + const auto read = std::find_if(state.inputs.begin(), state.inputs.end(), [](const SpirvInputBinding& input) { return input.kind == StageInputKind::SampleMask && input.variableId != 0u; }); + if (read != state.inputs.end()) { + state.sampleMaskInputVariable = read->variableId; + } else { + state.sampleMaskInputVariable = DefineInterfaceVariable(state, SampleMaskArrayType(state), spv::StorageClassInput, "gl_SampleMaskIn"); + state.module.AddAnnotation(spv::OpDecorate, state.sampleMaskInputVariable, spv::DecorationBuiltIn, spv::BuiltInSampleMask); + state.module.AddAnnotation(spv::OpDecorate, state.sampleMaskInputVariable, spv::DecorationFlat); + } + } } constexpr std::uint32_t MaxClipDistances = 8u; @@ -309,17 +363,19 @@ void DefineOutputs(SpirvEmitterState& state) { DefineMeshOutputs(state); return; } + const bool depthRemap = state.depthRangeRemap && state.program.Resources().stage == IrShaderStage::Vertex; + if (depthRemap) { + state.depthRemapClipIndex = clipDistanceCount; + clipDistanceCount += 2u; + } const auto stage = state.program.Resources().stage; const bool exportsPosition = std::ranges::any_of(state.outputs, [](const SpirvOutputBinding& output) { return output.kind == StageOutputKind::Position; }); - if (exportsPosition && (stage == IrShaderStage::Vertex || stage == IrShaderStage::TessellationEvaluation)) { + if (exportsPosition && (stage == IrShaderStage::Vertex || stage == IrShaderStage::TessellationEvaluation) && clipDistanceCount + cullDistanceCount < MaxClipDistances) { if (std::ranges::find(state.supportedCapabilities, spv::CapabilityClipDistance) == state.supportedCapabilities.end()) { FailEmit("clipping a primitive with an all-zero clip position needs shaderClipDistance on the target device"); } state.zeroPositionClipIndex = clipDistanceCount; clipDistanceCount += 1u; - if (clipDistanceCount > MaxClipDistances) { - FailEmit("guest clip distances plus the all-zero clip position distance exceed " + std::to_string(MaxClipDistances) + " clip distances"); - } } const auto BuiltIn = [&](std::uint32_t& variable, std::uint32_t type, const char* name, std::uint32_t builtin) { if (variable == 0u) { @@ -328,6 +384,7 @@ void DefineOutputs(SpirvEmitterState& state) { } return variable; }; + if (depthRemap) BuiltIn(state.clipDistanceVariable, F32ArrayType(state, clipDistanceCount), "gl_ClipDistance", spv::BuiltInClipDistance); for (auto& binding : state.outputs) { switch (binding.kind) { case StageOutputKind::Position: @@ -360,7 +417,18 @@ void DefineOutputs(SpirvEmitterState& state) { binding.variableId = BuiltIn(state.sampleMaskVariable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask); break; case StageOutputKind::StencilRef: - binding.variableId = BuiltIn(state.stencilRefVariable, TypeI32(state), "gl_FragStencilRefARB", spv::BuiltInFragStencilRefEXT); + if (std::ranges::find(state.supportedCapabilities, static_cast(spv::CapabilityStencilExportEXT)) == state.supportedCapabilities.end()) { + const auto values = StencilReferenceValues(state.program, PixelInfo(state).psStencilOperationValueExport ? 8u : 0u); + if (values.empty()) throw std::runtime_error("stencil reference export emulation found no stencil export"); + if (!StencilReferenceDefinedForKeptInvocations(state.program)) throw std::runtime_error("stencil reference export emulation requires a stencil export on every kept path"); + if (PixelInfo(state).psEarlyZ || PixelInfo(state).psOrderedPixelShader || !StencilReplaySafe(state.program)) throw std::runtime_error("stencil reference export emulation requires a replay-safe pixel shader with late tests"); + state.stencilReplay = true; + state.stencilRefVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPrivate, TypeI32(state)), spv::StorageClassPrivate); + if (state.spirvVersion >= 0x00010400u) state.interfaceVariables.push_back(state.stencilRefVariable); + binding.variableId = state.stencilRefVariable; + } else { + binding.variableId = BuiltIn(state.stencilRefVariable, TypeI32(state), "gl_FragStencilRefARB", spv::BuiltInFragStencilRefEXT); + } break; case StageOutputKind::Parameter: case StageOutputKind::Mrt: { @@ -379,12 +447,15 @@ void DefineOutputs(SpirvEmitterState& state) { if (state.inputInfo.pixel->psSampleMaskExportEnable) throw std::runtime_error("alpha-to-coverage with a sample-mask export is unsupported"); BuiltIn(state.sampleMaskVariable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask); } + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->QuadSampleMaskPartial()) { + BuiltIn(state.sampleMaskVariable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask); + } } void DefineDescriptors(SpirvEmitterState& state) { const IrBindingLayout& layout = state.program.Metadata().bindings; const IrShaderStage stage = state.program.Resources().stage; - if (layout.UsesPushData() || stage == IrShaderStage::Mesh || stage == IrShaderStage::TessellationControl) { + if (layout.UsesPushData() || state.stencilReplay || stage == IrShaderStage::Mesh || stage == IrShaderStage::TessellationControl || (state.depthRangeRemap && stage == IrShaderStage::Vertex)) { const auto type = PushConstantBlockType(state); state.pushConstantVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPushConstant, type), spv::StorageClassPushConstant); state.module.AddName(type, "BufferResource"); @@ -408,7 +479,7 @@ void DefineDescriptors(SpirvEmitterState& state) { case DescriptorBindingKind::Buffers: state.storageBufferVariable = Define(ArrayType(StorageBufferBlockType(state)), "buffers"); if (state.requirements.coherentBufferAlias) state.storageBufferCoherentVariable = Define(ArrayType(StorageBufferBlockType(state)), "buffers_coherent"); - if (state.requirements.bufferInt64Atomics) state.storageBufferU64Variable = Define(ArrayType(StorageBufferU64BlockType(state)), "buffers_u64"); + if (state.requirements.bufferU64View) state.storageBufferU64Variable = Define(ArrayType(StorageBufferU64BlockType(state)), "buffers_u64"); if (state.storageBufferU64Variable != 0u || state.storageBufferCoherentVariable != 0u) { for (const auto variable : {state.storageBufferVariable, state.storageBufferCoherentVariable, state.storageBufferU64Variable}) { if (variable != 0u) state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationAliased); @@ -421,6 +492,7 @@ void DefineDescriptors(SpirvEmitterState& state) { break; case DescriptorBindingKind::BdaPagetable: state.bdaPagetableVariable = Define(StorageBufferBlockType(state), "bda_pagetable"); + state.module.AddAnnotation(spv::OpDecorate, state.bdaPagetableVariable, spv::DecorationNonWritable); break; case DescriptorBindingKind::FaultBuffer: state.faultBufferVariable = Define(StorageBufferBlockType(state), "fault_buffer"); @@ -430,6 +502,7 @@ void DefineDescriptors(SpirvEmitterState& state) { break; case DescriptorBindingKind::FlattenedSrt: state.flattenedSrtVariable = Define(StorageBufferBlockType(state), "flattened_srt"); + state.module.AddAnnotation(spv::OpDecorate, state.flattenedSrtVariable, spv::DecorationNonWritable); break; case DescriptorBindingKind::Samplers: state.samplerVariable = Define(ArrayType(state.module.Type(spv::OpTypeSampler)), "samplers", spv::StorageClassUniformConstant); @@ -438,6 +511,32 @@ void DefineDescriptors(SpirvEmitterState& state) { state.gdsVariable = Define(StorageBufferBlockType(state), "gds"); 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(binding.kind == DescriptorBindingKind::ImageStorageTable ? spv::CapabilityImage1D : spv::CapabilitySampled1D); + } + } + break; + case DescriptorBindingKind::SamplerTable: + RequireTableIndexing(state); + state.tableSamplerVariable = Define(state.module.Type(spv::OpTypeRuntimeArray, state.module.Type(spv::OpTypeSampler)), "table_samplers", spv::StorageClassUniformConstant); + break; + case DescriptorBindingKind::ImageTableMap: + state.tableMapVariable = Define(StorageBufferBlockType(state), "table_map"); + state.module.AddAnnotation(spv::OpDecorate, state.tableMapVariable, spv::DecorationNonWritable); + break; default: { if (ImageBindingResourceClass(binding.kind) == ImageResourceClass::None) { FailEmit("descriptor binding has an unmapped image resource class"); @@ -461,6 +560,14 @@ void DefineDescriptors(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationDescriptorSet, WorkgroupMemoryDescriptorSet); state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationBinding, 0u); state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationCoherent); + if (state.requirements.ldsBufferInt64Atomics) { + state.ldsBufferU64Variable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassStorageBuffer, StorageBufferU64BlockType(state)), spv::StorageClassStorageBuffer); + state.module.AddName(state.ldsBufferU64Variable, "workgroup_memory_u64"); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferU64Variable, spv::DecorationDescriptorSet, WorkgroupMemoryDescriptorSet); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferU64Variable, spv::DecorationBinding, 0u); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferU64Variable, spv::DecorationCoherent); + for (const auto variable : {state.ldsBufferVariable, state.ldsBufferU64Variable}) state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationAliased); + } } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp index a7a465d86a..645d3d1677 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp @@ -49,11 +49,15 @@ namespace ShaderRecompiler const auto& pixel = *inputInfo.pixel; std::vector result; std::array readsP10{}; + std::array readsP20{}; for (const auto& block : program.Blocks()) { for (const auto* instruction : block->Instructions()) { - if (instruction->Opcode() != IrOpcode::GetInterpolationParameter || instruction->Argument(2)->Resolve()->ImmediateU32() != 0u) continue; + if (instruction->Opcode() != IrOpcode::GetInterpolationParameter) continue; + const auto mode = instruction->Argument(2)->Resolve()->ImmediateU32(); const auto attribute = instruction->Argument(0)->Resolve()->ImmediateU32(); - if (attribute < readsP10.size()) readsP10[attribute] = true; + if (attribute >= readsP10.size()) continue; + readsP10[attribute] = readsP10[attribute] || mode == 0u; + readsP20[attribute] = readsP20[attribute] || mode == 1u; } } for (const auto& input : program.Info().inputs) { @@ -61,12 +65,16 @@ namespace ShaderRecompiler const auto location = PixelInputLocation(pixel, input.location); const bool flat = PixelInputIsFlat(pixel, input.location); const bool flatP10 = flat && !input.perVertex && input.location < readsP10.size() && readsP10[input.location]; + const bool flatP20 = flat && !input.perVertex && input.location < readsP20.size() && readsP20[input.location]; + const bool vertexOrder = input.perVertex && (input.location >= 32u || (program.Metadata().pixelVertexOrderInputs >> input.location & 1u) != 0u); const auto existing = std::find_if(result.begin(), result.end(), [&](const auto& parameter) { return parameter.location == location; }); if (existing != result.end()) { existing->flatP10 = existing->flatP10 || flatP10; + existing->flatP20 = existing->flatP20 || flatP20; + existing->vertexOrder = existing->vertexOrder || vertexOrder; continue; } - result.push_back({location, location, flat, input.perVertex, PixelInputIsCustom(pixel, input.location), flatP10}); + result.push_back({location, location, flat, input.perVertex, PixelInputIsCustom(pixel, input.location), flatP10, vertexOrder, flatP20}); } return result; } @@ -194,6 +202,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 c454eee2c5..d997a174eb 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace ShaderRecompiler { @@ -134,36 +135,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) { @@ -203,6 +203,28 @@ std::uint32_t GuestAddress(SpirvValueEmitContext& ctx, const IrValue& inst, cons return AddBdaImmediate(ctx, inst, address, immediate); } +std::uint32_t GuestAddressLow(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem) { + const auto low = ctx.Arg(inst, 1); + if (mem.kind != ResourceKind::ScalarAddress) return low; + return Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), low, ConstantU32(ctx.state, ~3u)); +} + +std::array ReadBdaDwords(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t dwords, std::uint32_t used) { + const auto lookup = [&] { return EmitBdaDwordReads(ctx, inst, GuestAddressBase(ctx, inst, mem), mem.offset, dwords, false, used); }; + const auto* anchor = FindBdaAnchor(ctx.state, inst); + if (anchor == nullptr) return lookup(); + return EmitAnchoredBdaRead(ctx.state, inst, *anchor, GuestAddressLow(ctx, inst, mem), static_cast(mem.offset), 32u, dwords, used, lookup); +} + +std::uint32_t ReadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t bits) { + const auto lookup = [&] { return std::array{EmitBdaRead(ctx, inst, GuestAddress(ctx, inst, mem), bits)}; }; + const auto* anchor = FindBdaAnchor(ctx.state, inst); + if (anchor == nullptr) return lookup()[0]; + auto immediate = mem.offset; + if (mem.kind == ResourceKind::ScalarAddress) immediate &= ~3u; + return EmitAnchoredBdaRead(ctx.state, inst, *anchor, GuestAddressLow(ctx, inst, mem), static_cast(immediate), bits, 1u, 1u, lookup)[0]; +} + bool SameAddressValue(const IrValue* left, const IrValue* right, std::uint32_t depth = 0u) { if (left == right) return true; if (left == nullptr || right == nullptr || depth > 3u || left->Opcode() != right->Opcode() || left->ArgumentCount() != right->ArgumentCount()) return false; @@ -225,10 +247,10 @@ bool WritesMemory(IrOpcode opcode) { image == ImageAccess::Write || image == ImageAccess::Atomic || opcode == IrOpcode::Barrier || opcode == IrOpcode::StoreVectorWindow; } -std::vector> AdjacentBdaLoads(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem) { +std::vector> AdjacentBdaLoads(const SpirvEmitterState& state, const IrValue& inst, const MemoryInfo& mem) { std::vector> members; const IrBlock* block = inst.Parent(); - if (block == nullptr || ctx.state.laneCount != 1u || (mem.offset & 3u) != 0u) return members; + if (block == nullptr || state.laneCount != 1u || (mem.offset & 3u) != 0u) return members; const auto& instructions = block->Instructions(); auto next = std::find(instructions.begin(), instructions.end(), &inst); if (next == instructions.end()) return members; @@ -239,7 +261,7 @@ std::vector> AdjacentBdaLoads(SpirvValu if (WritesMemory(other->Opcode())) break; continue; } - const auto& otherMemory = ctx.Memory(*other); + const auto& otherMemory = state.program.Resources().memoryInfo.at(other->Flags().index); if (otherMemory.planningOnly || otherMemory.kind != mem.kind || otherMemory.addressIsFull != mem.addressIsFull || otherMemory.coherent != mem.coherent || other->ArgumentCount() != inst.ArgumentCount()) continue; bool sameArguments = true; for (std::size_t index = 0; index < inst.ArgumentCount(); ++index) { @@ -263,7 +285,7 @@ std::uint32_t LoadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const Mem state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, grouped->second.first, grouped->second.second); return value; } - const auto members = AdjacentBdaLoads(ctx, inst, mem); + const auto members = AdjacentBdaLoads(state, inst, mem); if (!members.empty()) { std::uint32_t used = 1u; for (const auto& member : members) used |= 1u << member.second; @@ -272,7 +294,7 @@ std::uint32_t LoadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const Mem const auto none = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, vector, none, zero, zero, zero, zero); const auto all = EmitValueOrDefaultIfCondition(state, ActiveArgument(ctx, inst), vector, none, [&]() { - const auto values = EmitBdaDwordReads(ctx, inst, GuestAddressBase(ctx, inst, mem), mem.offset, 4u, false, used); + const auto values = ReadBdaDwords(ctx, inst, mem, 4u, used); const auto read = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, vector, read, values[0], values[1], values[2], values[3]); return read; @@ -284,7 +306,7 @@ std::uint32_t LoadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const Mem } } return EmitValueOrZeroIfCondition(ctx.state, ActiveArgument(ctx, inst), [&]() { - return EmitBdaRead(ctx, inst, GuestAddress(ctx, inst, mem), bits); + return ReadBda(ctx, inst, mem, bits); }); } @@ -368,20 +390,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) { @@ -416,10 +437,34 @@ const MemoryInfo& SharedMemory(SpirvValueEmitContext& ctx, const IrValue& inst) return mem; } -bool RoutesApertures(const SpirvEmitterState& state, const MemoryInfo& mem) { - return mem.kind == ResourceKind::Flat && state.program.Resources().stage == IrShaderStage::Compute; + } +std::vector BdaReadSites(const SpirvEmitterState& state) { + std::vector sites; + std::unordered_set grouped; + const auto& memory = state.program.Resources().memoryInfo; + for (const IrBlock* block : state.program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + const auto info = AddressOpcodeInfoOf(inst->Opcode()); + const auto index = inst->Flags().index; + if (info.access != AddressAccess::Read || index >= memory.size() || grouped.contains(inst)) continue; + const auto& mem = memory[index]; + if (mem.planningOnly || RoutesApertures(state, mem)) continue; + if (mem.kind != ResourceKind::ScalarAddress && mem.kind != ResourceKind::Flat && mem.kind != ResourceKind::Global) continue; + if (info.dataBits == 32u && info.components == 1u) { + for (const auto& member : AdjacentBdaLoads(state, *inst, mem)) grouped.insert(member.first); + } else if (info.components > 1u && UsedBdaDwords(*inst, info.components) == 0u) { + continue; + } + sites.push_back(inst); + } + } + return sites; +} + +namespace { + struct ApertureAddress { std::uint32_t low = 0; std::uint32_t shared = 0; @@ -578,7 +623,7 @@ void LoadAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t std::uint32_t LoadBdaWide(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), [&]() { - return ConstructU32Composite(state, components, EmitBdaDwordReads(ctx, inst, GuestAddressBase(ctx, inst, mem), mem.offset, components)); + return ConstructU32Composite(state, components, ReadBdaDwords(ctx, inst, mem, components, 0u)); }); } @@ -763,6 +808,11 @@ std::uint32_t UpdateFunctionElement(SpirvEmitterState& state, std::uint32_t poin template std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, std::uint32_t type, std::uint32_t zero, TUpdate&& update); +template +std::uint32_t LdsBufferAtomic64(SpirvEmitterState& state, std::uint32_t low, TNext&& next) { + return Unary(state, spv::OpBitcast, TypeU64(state), AtomicUpdateTyped(state, EmitLdsBufferU64Pointer(state, low), ResourceKind::Lds, TypeScalarU64(state), next)); +} + template std::uint32_t EmitLdsAtomic32(SpirvEmitterState& state, const MemoryInfo& mem, TOperation&& operation) { if (mem.kind != ResourceKind::Lds || !state.requirements.ldsLock) return operation(); @@ -866,7 +916,7 @@ std::uint32_t Atomic32(SpirvValueEmitContext& ctx, const IrValue& inst, const Me const auto old = EmitAtomicOperation(ctx, inst, pointer, scope); if (lds) { const std::uint32_t semantics = spv::MemorySemanticsAcquireReleaseMask | LdsMemorySemantics(state); - state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, scope), ConstantU32(state, semantics)); + if (!state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, scope), ConstantU32(state, semantics)); } else { EmitDeviceAtomicMemoryBarrier(state); } @@ -985,7 +1035,7 @@ std::uint32_t SharedAtomicUpdate(SpirvValueEmitContext& ctx, const IrValue& inst if (InFunctionStorage(state, mem.kind)) return UpdateFunctionElement(state, pointer, next); const auto old = AtomicUpdate(state, pointer, mem.kind, next); const std::uint32_t semantics = spv::MemorySemanticsAcquireReleaseMask | LdsMemorySemantics(state); - state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, semantics)); + if (!state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, semantics)); return old; }); } @@ -1147,7 +1197,7 @@ std::uint32_t ApertureAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), byteOffset, ConstantU32(state, 7u)), ConstantU32(state, 0u)); EmitIfCondition(state, unaligned, [&] { RecordBdaFault(state, address, ConstantU32(state, 8u), instruction, BdaAbi::FaultReason::Unaligned); }); if (kind == ResourceKind::Scratch && state.program.Info().scratchDwords == 0u) return ConstantU64(state, 0u); - if (kind == ResourceKind::Lds && !state.requirements.ldsLock) ctx.Fail(inst, "64-bit LDS atomic reached through a flat address without the workgroup lock"); + if (kind == ResourceKind::Lds && !state.requirements.ldsLock && !state.requirements.ldsBufferInt64Atomics) ctx.Fail(inst, "64-bit LDS atomic reached through a flat address without the workgroup lock"); MemoryInfo storage{}; storage.kind = kind; const auto resource = PrepareMemoryResourceAccess(state, storage); @@ -1157,6 +1207,7 @@ std::uint32_t ApertureAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, const auto opcode = inst.Opcode(); return EmitValueOrDefaultIfCondition(state, aligned, TypeU64(state), ConstantU64(state, 0u), [&] { return EmitValueOrDefaultIfCondition(state, EmitMemoryElementInBounds(state, resource, high), TypeU64(state), ConstantU64(state, 0u), [&] { + if (kind == ResourceKind::Lds && state.requirements.ldsBufferInt64Atomics) return LdsBufferAtomic64(state, low, [&](std::uint32_t old) { return FlatAtomicNext64(state, opcode, old, value, comparator); }); const auto update = [&]() { const auto lowPointer = EmitMemoryElementPointer(state, resource, low); const auto highPointer = EmitMemoryElementPointer(state, resource, high); @@ -1250,7 +1301,7 @@ std::uint32_t AddressAtomic(SpirvValueEmitContext& ctx, const IrValue& inst) { }); } -std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, bool append) { +std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, bool append, bool ordered = false, bool swap = false) { auto& state = ctx.state; if (ctx.half == 1u) { if (ctx.otherHalf == nullptr) { @@ -1261,9 +1312,10 @@ std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, boo const auto& mem = SharedMemory(ctx, inst); const bool wave64 = state.laneCount == 2u; const bool gds = mem.kind == ResourceKind::Gds; + const bool m0Address = gds && !ordered; std::uint32_t rawIndex = ConstantU32(state, mem.offset >> 2u); std::uint32_t m0Bounds = 0; - if (gds) { + if (m0Address) { const auto m0 = ctx.Arg(inst, 0); const auto base = Binary(state, spv::OpShiftRightLogical, TypeU32(state), m0, ConstantU32(state, 16u)); const auto size = Binary(state, spv::OpBitwiseAnd, TypeU32(state), m0, ConstantU32(state, 0xffffu)); @@ -1282,17 +1334,18 @@ std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, boo const auto count = Binary(state, spv::OpIAdd, TypeU32(state), Unary(state, spv::OpBitCount, TypeU32(state), low), Unary(state, spv::OpBitCount, TypeU32(state), high)); const auto first = ctx.FirstLane(ballot); const auto sourceLane = wave64 ? Binary(state, spv::OpBitwiseAnd, TypeU32(state), first, ConstantU32(state, 31u)) : first; + const auto amount = ordered ? ctx.Shuffle(inst, 4, first) : count; const auto isFirst = Binary(state, spv::OpIEqual, TypeBool(state), EmitSubgroupLocalInvocationId(state), sourceLane); const auto storageBounds = EmitMemoryElementInBounds(state, access, index); - const auto bounds = gds ? AndCondition(state, storageBounds, m0Bounds) : storageBounds; + const auto bounds = m0Address ? AndCondition(state, storageBounds, m0Bounds) : storageBounds; const auto lanesActive = wave64 ? Binary(state, spv::OpINotEqual, TypeBool(state), count, ConstantU32(state, 0u)) : exec; const auto condition = AndCondition(state, isFirst, AndCondition(state, lanesActive, bounds)); const auto atomic = EmitValueOrZeroIfCondition(state, condition, [&]() { if (InFunctionStorage(state, mem.kind)) { - return UpdateFunctionElement(state, EmitMemoryElementPointer(state, access, index), [&](std::uint32_t old) { return Binary(state, append ? spv::OpIAdd : spv::OpISub, TypeU32(state), old, count); }); + return UpdateFunctionElement(state, EmitMemoryElementPointer(state, access, index), [&](std::uint32_t old) { return swap ? amount : Binary(state, append ? spv::OpIAdd : spv::OpISub, TypeU32(state), old, count); }); } const auto value = state.module.AllocateId(); - state.module.AddFunction(append ? spv::OpAtomicIAdd : spv::OpAtomicISub, TypeU32(state), value, EmitMemoryElementPointer(state, access, index), ConstantU32(state, gds ? spv::ScopeDevice : spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsMaskNone), count); + state.module.AddFunction(swap ? spv::OpAtomicExchange : append ? spv::OpAtomicIAdd : spv::OpAtomicISub, TypeU32(state), value, EmitMemoryElementPointer(state, access, index), ConstantU32(state, gds ? spv::ScopeDevice : spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsMaskNone), amount); return value; }); const auto result = state.module.AllocateId(); @@ -1339,8 +1392,11 @@ std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, std::uint32_t type, std: state.module.AddFunction(spv::OpSelectionMerge, attemptMerge, spv::SelectionControlMaskNone); state.module.AddFunction(spv::OpBranchConditional, acquired, critical, attemptMerge); EmitLabel(state, critical); + state.insideLdsLock = true; const auto old = update(); - state.module.AddFunction(spv::OpAtomicStore, lock, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsReleaseMask | LdsMemorySemantics(state)), ConstantU32(state, 0u)); + state.insideLdsLock = false; + const auto released = state.module.AllocateId(); + state.module.AddFunction(spv::OpAtomicExchange, TypeU32(state), released, lock, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsReleaseMask | LdsMemorySemantics(state)), ConstantU32(state, 0u)); const auto criticalExit = state.currentLabel; state.module.AddFunction(spv::OpBranch, attemptMerge); EmitLabel(state, attemptMerge); @@ -1359,7 +1415,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(); @@ -1383,6 +1439,12 @@ std::uint32_t SharedAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, TR const auto low = EmitMemoryElementIndex(state, resource, rawIndex); const auto high = EmitMemoryElementIndex(state, resource, Binary(state, spv::OpIAdd, TypeU32(state), rawIndex, ConstantU32(state, 1u))); return EmitValueOrDefaultIfCondition(state, EmitMemoryElementInBounds(state, resource, high), TypeU64(state), ConstantU64(state, 0u), [&]() { + if (state.requirements.ldsBufferInt64Atomics) { + const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), low, ConstantU32(state, 1u)), ConstantU32(state, 0u)); + EmitIfCondition(state, unaligned, [&] { RecordBdaFaultWords(state, Binary(state, spv::OpShiftLeftLogical, TypeU32(state), rawIndex, ConstantU32(state, 2u)), ConstantU32(state, 0u), ConstantU32(state, 8u), ConstantU32(state, inst.Flags().pc), BdaAbi::FaultReason::Unaligned); }); + const auto aligned = Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned); + return EmitValueOrDefaultIfCondition(state, aligned, TypeU64(state), ConstantU64(state, 0u), [&]() { return LdsBufferAtomic64(state, low, [&](std::uint32_t old) { return replacement(state, old); }); }); + } const auto update = [&]() { const auto lowPointer = EmitMemoryElementPointer(state, resource, low); const auto highPointer = EmitMemoryElementPointer(state, resource, high); @@ -1497,14 +1559,14 @@ std::uint32_t EmitReadConst(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto guard = static_cast(found - guarded.begin()); const auto flag = load(ConstantU32(state, resources.srtGuardOffset + guard)); const auto poisoned = Binary(state, spv::OpINotEqual, TypeBool(state), flag, ConstantU32(state, 0u)); + const auto records = resources.srtGuardOffset + static_cast(guarded.size()); + if (records + 3u * guarded.size() >= (1u << BdaAbi::SrtGuardStageShift)) { + ctx.Fail(inst, "has more guarded SRT records than the fault buffer's guard word can name"); + } EmitIfCondition(state, poisoned, [&] { - const auto records = resources.srtGuardOffset + static_cast(guarded.size()); const auto record = Binary(state, spv::OpIAdd, u32, ConstantU32(state, records), Binary(state, spv::OpIMul, u32, Binary(state, spv::OpISub, u32, flag, ConstantU32(state, 1u)), ConstantU32(state, 3u))); - const auto word = [&](std::uint32_t offset) { return load(Binary(state, spv::OpIAdd, u32, record, ConstantU32(state, offset))); }; - const auto pc = word(0u); - const auto low = word(1u); - const auto high = word(2u); - RecordBdaFaultWords(state, low, high, ConstantU32(state, 4u), pc, BdaAbi::FaultReason::Unmapped); + const auto stage = ConstantU32(state, static_cast(resources.stage) << BdaAbi::SrtGuardStageShift); + state.module.AddFunction(spv::OpAtomicStore, BdaWord(state, state.faultBufferVariable, ConstantU32(state, BdaAbi::SrtGuardWord)), ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone), Binary(state, spv::OpBitwiseOr, u32, record, stage)); }); StopBdaInvocationIf(state, poisoned); } @@ -1837,6 +1899,14 @@ std::uint32_t EmitDataConsume(SpirvValueEmitContext& ctx, const IrValue& inst) { return AppendConsume(ctx, inst, false); } +std::uint32_t EmitDataOrderedCount(SpirvValueEmitContext& ctx, const IrValue& inst) { + return AppendConsume(ctx, inst, true, true); +} + +std::uint32_t EmitDataOrderedSwap(SpirvValueEmitContext& ctx, const IrValue& inst) { + return AppendConsume(ctx, inst, true, true, true); +} + std::uint32_t EmitSharedAtomicRsub32(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto data = ctx.Arg(inst, 1); @@ -1847,10 +1917,21 @@ std::uint32_t EmitSharedAtomicRsub32(SpirvValueEmitContext& ctx, const IrValue& std::uint32_t EmitSharedAtomicFAdd32(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto data = ctx.Arg(inst, 1); + const auto& mem = SharedMemory(ctx, inst); + const bool flushInputs = mem.flushDenormals; + const bool flushResult = mem.flushDenormalResults; + const AtomicFloatBits dataBits{ctx.state, false}; + const auto dataOperand = Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), dataBits.flush(data, flushInputs)); + const auto dataNan = dataBits.nan(data); + const auto quietData = dataBits.op(spv::OpBitwiseOr, data, dataBits.constant(dataBits.quiet())); return SharedAtomicUpdate(ctx, inst, [&](SpirvEmitterState& state, std::uint32_t old) { - const auto sum = Binary(state, spv::OpFAdd, TypeF32(state), Unary(state, spv::OpBitcast, TypeF32(state), old), Unary(state, spv::OpBitcast, TypeF32(state), data)); + const AtomicFloatBits bits{state, false}; + const auto sum = Binary(state, spv::OpFAdd, TypeF32(state), Unary(state, spv::OpBitcast, TypeF32(state), bits.flush(old, flushInputs)), dataOperand); state.module.AddAnnotation(spv::OpDecorate, sum, spv::DecorationNoContraction); - return Unary(state, spv::OpBitcast, TypeU32(state), sum); + auto result = bits.flush(Unary(state, spv::OpBitcast, TypeU32(state), sum), flushResult); + result = bits.select(bits.nan(result), bits.constant(0xffc00000u), result); + result = bits.select(bits.nan(old), bits.op(spv::OpBitwiseOr, old, bits.constant(bits.quiet())), result); + return bits.select(dataNan, quietData, result); }); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index f4c1da3f2b..ddde52c8e9 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -5,6 +5,11 @@ #include "SpirvBackend/SpirvEmitterHelpers.hpp" #include #include +#include +#include +#include +#include +#include #include #include @@ -41,6 +46,170 @@ std::uint32_t DefineInterfaceVariable(SpirvEmitterState& state, std::uint32_t ty return variable; } +namespace { + +class WindowIndexBounds { +public: + std::optional Of(const IrValue& input) { + const IrValue* value = input.Resolve(); + if (value->HasImmediate()) return value->ImmediateU32(); + if (const auto found = memo.find(value); found != memo.end()) return found->second; + if (!active.insert(value).second || active.size() > 256u) return std::nullopt; + const auto bound = compute(*value); + active.erase(value); + memo.emplace(value, bound); + return bound; + } + +private: + using Bound = std::optional; + + Bound maxOf(std::initializer_list bounds) { + std::uint64_t result = 0; + for (const auto& bound : bounds) { + if (!bound) return std::nullopt; + result = std::max(result, *bound); + } + return result; + } + + static std::vector reachingStores(const IrValue& load, std::uint32_t slot) { + std::vector values; + std::unordered_set visited; + std::vector> pending {{load.Parent(), &load}}; + while (!pending.empty()) { + const auto [block, before] = pending.back(); + pending.pop_back(); + const auto& instructions = block->Instructions(); + auto it = instructions.rbegin(); + if (before != nullptr) it = std::next(std::find(instructions.rbegin(), instructions.rend(), before)); + bool stored = false; + for (; it != instructions.rend() && !stored; ++it) { + if ((*it)->Opcode() != IrOpcode::StoreVectorWindow) continue; + const IrValue* index = (*it)->Argument(0)->Resolve(); + if (!index->HasImmediate() || index->ImmediateU32() != slot) continue; + values.push_back((*it)->Argument(1)); + stored = true; + } + if (stored) continue; + for (IrBlock* predecessor : block->Predecessors()) { + if (visited.insert(predecessor).second) pending.emplace_back(predecessor, nullptr); + } + } + return values; + } + + Bound compute(const IrValue& value) { + const auto arg = [&](std::size_t index) { return Of(*value.Argument(index)); }; + const auto immediate = [&](std::size_t index) -> std::optional { + const IrValue* operand = value.Argument(index)->Resolve(); + if (!operand->HasImmediate()) return std::nullopt; + return operand->ImmediateU32(); + }; + const auto fits = [](std::uint64_t bound) -> Bound { return bound <= 0xffffffffull ? Bound(bound) : std::nullopt; }; + switch (value.Opcode()) { + case IrOpcode::BitFieldUExtract: { + const auto count = immediate(2); + if (count && *count < 32u) return (std::uint64_t {1} << *count) - 1u; + return arg(0); + } + case IrOpcode::BitwiseAnd32: { + const auto lhs = arg(0); + const auto rhs = arg(1); + if (lhs && rhs) return std::min(*lhs, *rhs); + return lhs ? lhs : rhs; + } + case IrOpcode::UMin32: { + const auto lhs = arg(0); + const auto rhs = arg(1); + if (!lhs || !rhs) return std::nullopt; + return std::min(*lhs, *rhs); + } + case IrOpcode::UMax32: + return maxOf({arg(0), arg(1)}); + case IrOpcode::BitwiseOr32: { + const auto both = maxOf({arg(0), arg(1)}); + if (!both) return std::nullopt; + std::uint64_t mask = 0; + while (mask < *both) mask = mask << 1u | 1u; + return mask; + } + case IrOpcode::IAdd32: { + const auto lhs = arg(0); + const auto rhs = arg(1); + if (!lhs || !rhs) return std::nullopt; + return fits(*lhs + *rhs); + } + case IrOpcode::ShiftRightLogical32: { + const auto shift = immediate(1); + const auto bound = arg(0); + if (!bound) return std::nullopt; + return shift ? *bound >> (*shift & 31u) : *bound; + } + case IrOpcode::ShiftLeftLogical32: { + const auto shift = immediate(1); + const auto bound = arg(0); + if (!bound || !shift) return std::nullopt; + return fits(*bound << (*shift & 31u)); + } + case IrOpcode::ConvertU32U16: + return 0xffffu; + case IrOpcode::Select: + case IrOpcode::SelectU32: + { + if (value.Argument(0)->Resolve()->Opcode() == IrOpcode::GetExec || value.Argument(2)->Resolve()->Opcode() == IrOpcode::LoadVectorWindow) return arg(1); + const auto lhs = arg(1); + const auto rhs = arg(2); + if (lhs && rhs) return std::max(*lhs, *rhs); + return lhs ? lhs : rhs; + } + case IrOpcode::ReadFirstLane: + return arg(0); + case IrOpcode::Identity: + case IrOpcode::Phi: { + Bound bound; + for (std::size_t index = 0; index < value.ArgumentCount(); ++index) { + if (const auto incoming = arg(index)) bound = std::max(bound.value_or(0u), *incoming); + } + return bound; + } + case IrOpcode::LoadVectorWindow: { + const IrValue* index = value.Argument(0)->Resolve(); + if (!index->HasImmediate()) return std::nullopt; + Bound bound = 0u; + for (const IrValue* stored : reachingStores(value, index->ImmediateU32())) { + bound = maxOf({bound, Of(*stored)}); + if (!bound) return std::nullopt; + } + return bound; + } + default: + return std::nullopt; + } + } + + std::unordered_map memo; + std::unordered_set active; +}; + +std::uint32_t IndexedWindowSlots(const IrProgram& program) { + const auto window = program.Metadata().vectorWindowSize; + WindowIndexBounds bounds; + std::uint64_t reached = 0; + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() != IrOpcode::LoadVectorWindow && inst->Opcode() != IrOpcode::StoreVectorWindow) continue; + const IrValue* index = inst->Argument(0)->Resolve(); + if (index->HasImmediate()) continue; + const auto bound = bounds.Of(*index); + if (bound && *bound < window / 2u) reached = std::max(reached, *bound + 1u); + } + } + return static_cast(reached); +} + +} + void DefineModule(SpirvEmitterState& state) { state.interfaceVariables.reserve(state.program.Info().inputs.size() + state.program.Info().outputs.size()); DefineInputs(state); @@ -55,9 +224,14 @@ void DefineModule(SpirvEmitterState& state) { state.scratchVariable.at(half) = state.module.AllocateId(); } } - if (state.program.Metadata().vectorWindowSize != 0u) { + if (const auto window = state.program.Metadata().vectorWindowSize; window != 0u) { + static const bool narrowWindow = std::getenv("APS5_NO_WINDOW_NARROWING") == nullptr; + state.vectorWindowIndexed = narrowWindow ? IndexedWindowSlots(state.program) : 0u; for (std::uint32_t half = 0; half < state.laneCount; half++) { state.vectorWindowVariable.at(half) = state.module.AllocateId(); + if (state.vectorWindowIndexed == 0u) continue; + auto& slots = state.vectorWindowSlots.at(half); + for (std::uint32_t slot = state.vectorWindowIndexed; slot < window; slot++) slots.push_back(state.module.AllocateId()); } } state.mainFunc = state.module.AllocateId(); @@ -66,7 +240,7 @@ void DefineModule(SpirvEmitterState& state) { state.meshGuestFunc = state.module.AllocateId(); state.module.EmitCapability(spv::CapabilityMeshShadingEXT); state.module.EmitExtension("SPV_EXT_mesh_shader"); - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOutputTrianglesEXT); + state.module.AddExecutionMode(state.mainFunc, mesh.outputPrimitiveSize == 1u ? spv::ExecutionModeOutputPoints : 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); } @@ -85,12 +259,18 @@ void DefineModule(SpirvEmitterState& state) { if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityInt64Atomics)) == state.supportedCapabilities.end()) { FailEmit("64-bit buffer atomics need shaderBufferInt64Atomics"); } + } + if (state.requirements.bufferU64View) { state.module.EmitCapability(spv::CapabilityInt64); state.module.EmitCapability(spv::CapabilityInt64Atomics); } if (state.requirements.sharedInt64Atomics) { state.module.EmitCapability(spv::CapabilityInt64); } + if (state.requirements.ldsBufferInt64Atomics) { + state.module.EmitCapability(spv::CapabilityInt64); + state.module.EmitCapability(spv::CapabilityInt64Atomics); + } if (state.requirements.imageInt64Atomics) { if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityInt64ImageEXT)) == state.supportedCapabilities.end()) { FailEmit("64-bit image atomics need VK_EXT_shader_image_atomic_int64"); @@ -115,7 +295,15 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitCapability(spv::CapabilityShaderLayer); } if (InputVariableForKind(state, StageInputKind::Layer) != 0) { - state.module.EmitCapability(spv::CapabilityGeometry); + const auto supported = [&state](spv::Capability capability) { + return std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(capability)) != state.supportedCapabilities.end(); + }; + if (supported(spv::CapabilityGeometry) || !supported(spv::CapabilityShaderViewportIndexLayerEXT)) { + state.module.EmitCapability(spv::CapabilityGeometry); + } else { + state.module.EmitExtension("SPV_EXT_shader_viewport_index_layer"); + state.module.EmitCapability(spv::CapabilityShaderViewportIndexLayerEXT); + } } if (state.viewportIndexVariable != 0) { state.module.RequireVersion(0x00010500u); @@ -137,9 +325,12 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitCapability(spv::CapabilityGroupNonUniformShuffle); } if (state.requirements.computeDerivatives && StageOf(state) == IrShaderStage::Compute) { - state.module.EmitCapability(spv::CapabilityComputeDerivativeGroupQuadsKHR); + state.module.EmitCapability(spv::CapabilityComputeDerivativeGroupLinearKHR); state.module.EmitExtension("SPV_KHR_compute_shader_derivatives"); } + if (std::any_of(state.inputs.begin(), state.inputs.end(), [](const SpirvInputBinding& input) { return input.kind == StageInputKind::BaseInstance; })) { + state.module.EmitCapability(spv::CapabilityDrawParameters); + } const bool fragmentBarycentric = StageOf(state) == IrShaderStage::Pixel && std::any_of(state.inputs.begin(), state.inputs.end(), [](const SpirvInputBinding& input) { return input.perVertex || input.kind == StageInputKind::BaryCoordSmooth || input.kind == StageInputKind::BaryCoordNoPerspective; }); @@ -150,8 +341,18 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitCapability(spv::CapabilitySignedZeroInfNanPreserve); state.module.EmitExtension("SPV_KHR_float_controls"); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeSignedZeroInfNanPreserve, 32u); - if (state.requirements.float64) { - if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityFloat64)) == state.supportedCapabilities.end()) { + if (state.float32FlushToZero && std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityDenormFlushToZero)) != state.supportedCapabilities.end()) { + state.module.EmitCapability(spv::CapabilityDenormFlushToZero); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDenormFlushToZero, 32u); + } else if (state.float32DenormPreserve && !state.float32FlushToZero) { + state.module.EmitCapability(spv::CapabilityDenormPreserve); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDenormPreserve, 32u); + } + const bool deviceFloat64 = std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityFloat64)) != state.supportedCapabilities.end(); + state.integerInterpolation = state.requirements.float64Interpolation && !deviceFloat64; + if (state.integerInterpolation) state.module.EmitCapability(spv::CapabilityInt64); + if (state.requirements.float64 || (state.requirements.float64Interpolation && deviceFloat64)) { + if (!deviceFloat64) { throw std::runtime_error("64-bit float instructions need the Float64 capability, which the device lacks"); } state.module.EmitCapability(spv::CapabilityFloat64); @@ -162,20 +363,8 @@ void DefineModule(SpirvEmitterState& state) { } } if (const auto* workgroup = ShaderWorkgroupInput(state)) { - const std::uint32_t derivativeDefault = state.requirements.computeDerivatives ? 2u : 1u; - 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.laneCount == 2) { - localX = ((localX * localY * localZ + 63u) / 64u) * 32u; - localY = 1u; - localZ = 1u; - if (state.requirements.computeDerivatives) { - localY = localX / 2u; - localX = 2u; - } - } - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeLocalSize, localX, localY, localZ); + const auto local = WorkgroupLocalSize(state, *workgroup); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeLocalSize, local[0], local[1], local[2]); } if (StageOf(state) == IrShaderStage::Pixel) { const auto& pixel = PixelInfo(state); @@ -189,7 +378,7 @@ void DefineModule(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDepthGreater); } } - if (state.stencilRefVariable != 0) { + if (state.stencilRefVariable != 0 && !state.stencilReplay) { if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityStencilExportEXT)) == state.supportedCapabilities.end()) { throw std::runtime_error("a pixel shader that exports the stencil reference needs VK_EXT_shader_stencil_export, which the device lacks"); } @@ -197,7 +386,7 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitExtension("SPV_EXT_shader_stencil_export"); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT); } - if (pixel.psEarlyZ && !pixel.psAlphaToCoverage) { + if (pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); } if (pixel.psOrderedPixelShader) { @@ -210,7 +399,7 @@ void DefineModule(SpirvEmitterState& state) { } } if (state.requirements.computeDerivatives && StageOf(state) == IrShaderStage::Compute) { - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDerivativeGroupQuadsKHR); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDerivativeGroupLinearKHR); } state.module.AddName(state.mainFunc, "main"); if (state.requirements.functionLds) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp index 6efac49c44..4c0c787f07 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,16 +86,37 @@ 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); state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), result, own, zero, zero, zero); return; } - state.module.AddFunction(spv::OpGroupNonUniformBallot, TypeU32Vector(state, 4u), result, ConstantU32(state, spv::ScopeSubgroup), predicate); + state.module.AddSubgroupBallot(TypeU32Vector(state, 4u), result, ConstantU32(state, spv::ScopeSubgroup), predicate); } 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/SpirvModule.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModule.cpp index 3dbdbc3003..f10a1cdd9e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModule.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModule.cpp @@ -114,12 +114,14 @@ std::vector SpirvModule::Finalize() const { void SpirvModule::BeginHelperFunction() { if (inHelperFunction) throw std::runtime_error("SpirvModule helper functions cannot nest"); std::swap(functionInstructions, helperFunctionInstructions); + std::swap(ballots, helperBallots); inHelperFunction = true; } void SpirvModule::EndHelperFunction() { if (!inHelperFunction) throw std::runtime_error("SpirvModule::EndHelperFunction without a helper function"); std::swap(functionInstructions, helperFunctionInstructions); + std::swap(ballots, helperBallots); inHelperFunction = false; } @@ -201,6 +203,12 @@ void SpirvModule::DefineGlobalVariable(std::uint32_t id, std::uint32_t pointerTy appendInstruction(declarations, spv::OpVariable, pointerType, id, storageClass); } +std::uint32_t SpirvModule::DefineInitializedGlobalVariable(std::uint32_t pointerType, std::uint32_t storageClass, std::uint32_t initializer) { + const auto id = AllocateId(); + appendInstruction(declarations, spv::OpVariable, pointerType, id, storageClass, initializer); + return id; +} + void SpirvModule::AddMemoryModel(std::uint32_t addressingModel, std::uint32_t memoryModel) { appendInstruction(this->memoryModel, spv::OpMemoryModel, addressingModel, memoryModel); } @@ -215,6 +223,33 @@ void SpirvModule::AddFunction(std::span words) { AppendInstructionWords(functionInstructions, words); } +void SpirvModule::AddSubgroupBallot(std::uint32_t type, std::uint32_t result, std::uint32_t scope, std::uint32_t predicate) { + if (ballots.scanned > functionInstructions.size()) throw std::runtime_error("SpirvModule function instructions shrank under a subgroup ballot"); + for (auto offset = ballots.scanned; offset < functionInstructions.size();) { + const auto wordCount = functionInstructions[offset] >> spv::WordCountShift; + if (wordCount == 0u || wordCount > functionInstructions.size() - offset) throw std::runtime_error("SpirvModule malformed function instruction before a subgroup ballot"); + switch (functionInstructions[offset] & spv::OpCodeMask) { + case spv::OpLabel: + case spv::OpFunction: + case spv::OpFunctionEnd: + case spv::OpFunctionCall: + case spv::OpDemoteToHelperInvocation: + ballots.results.clear(); + break; + default: + break; + } + offset += wordCount; + } + const auto [found, inserted] = ballots.results.try_emplace({type, scope, predicate}, result); + if (inserted) { + appendInstruction(functionInstructions, spv::OpGroupNonUniformBallot, type, result, scope, predicate); + } else { + appendInstruction(functionInstructions, spv::OpCopyObject, type, result, found->second); + } + ballots.scanned = functionInstructions.size(); +} + void SpirvModule::appendString(std::vector& words, const std::string& text) { const auto length = text.size(); const auto wordCount = (length + 1u + 3u) / 4u; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 395e365f6e..57eb96f9ce 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -9,6 +9,7 @@ #include "PipelineSpecialization.hpp" #include #include +#include #include #include @@ -27,6 +28,48 @@ bool UserDataDwordIndex(const SpirvEmitterState& state, ScalarReg reg, std::uint return true; } +bool MeshBarycentricPixel(const SpirvEmitterState& state) { + return state.meshBarycentricPixel && state.program.Metadata().barycentricEmulation && state.pixelFlatPositions != 0u; +} + +std::uint32_t EmitSolvedBarycentric(SpirvEmitterState& state, StageInputKind kind) { + const auto cached = state.solvedBarycentrics.find({state.currentLabel, static_cast(kind)}); + if (cached != state.solvedBarycentrics.end()) return cached->second; + const auto vec3 = TypeF32Vector(state, 3u); + const auto varying = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, vec3, varying, InputVariableForKind(state, kind)); + std::array corner {}; + for (std::uint32_t index = 0; index < 3u; index++) { + const auto pointer = state.module.AllocateId(); + const auto position = state.module.AllocateId(); + corner.at(index) = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeF32Vector(state, 4u)), pointer, state.pixelFlatPositions, ConstantU32(state, index)); + state.module.AddFunction(spv::OpLoad, TypeF32Vector(state, 4u), position, pointer); + state.module.AddFunction(spv::OpVectorShuffle, vec3, corner.at(index), position, position, 0u, 1u, 3u); + } + const auto cross = [&](std::uint32_t left, std::uint32_t right) { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, vec3, result, GlslStd450(state), GLSLstd450Cross, left, right); + return result; + }; + const auto dot = [&](std::uint32_t left, std::uint32_t right) { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpDot, TypeF32(state), result, left, right); + return result; + }; + const auto determinant = dot(corner[0], cross(corner[1], corner[2])); + std::array weight {}; + const std::array, 3> rows{{{1u, 2u}, {2u, 0u}, {0u, 1u}}}; + for (std::uint32_t index = 0; index < 3u; index++) { + weight.at(index) = state.module.AllocateId(); + state.module.AddFunction(spv::OpFDiv, TypeF32(state), weight.at(index), dot(varying, cross(corner[rows[index][0]], corner[rows[index][1]])), determinant); + } + const auto solved = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, vec3, solved, weight[0], weight[1], weight[2]); + state.solvedBarycentrics[{state.currentLabel, static_cast(kind)}] = solved; + return solved; +} + const ShaderWorkgroupInputInfo* ShaderWorkgroupInputFor(const SpirvEmitterState& state) { switch (state.program.Resources().stage) { case IrShaderStage::Compute: @@ -38,7 +81,88 @@ const ShaderWorkgroupInputInfo* ShaderWorkgroupInputFor(const SpirvEmitterState& } } +namespace { + +std::uint32_t GuestSampleIndex(SpirvEmitterState& state, std::uint32_t hostIndex) { + if (state.sampleOrderWord == 0u) return hostIndex; + const auto shift = EmitBinaryU32(state, spv::OpShiftLeftLogical, EmitBinaryU32(state, spv::OpBitwiseAnd, hostIndex, ConstantU32(state, 7u)), ConstantU32(state, 2u)); + return EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, state.sampleOrderWord, shift), ConstantU32(state, 7u)); +} + +std::uint32_t ReorderedSampleBits(SpirvEmitterState& state, std::uint32_t bits, bool toHost) { + auto reordered = EmitBinaryU32(state, spv::OpBitwiseAnd, bits, ConstantU32(state, ~0xffu)); + for (std::uint32_t sample = 0; sample < 8u; ++sample) { + const auto guest = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, state.sampleOrderWord, ConstantU32(state, sample * 4u)), ConstantU32(state, 7u)); + if (toHost) { + const auto bit = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, bits, guest), ConstantU32(state, 1u)); + reordered = EmitBinaryU32(state, spv::OpBitwiseOr, reordered, EmitBinaryU32(state, spv::OpShiftLeftLogical, bit, ConstantU32(state, sample))); + } else { + const auto bit = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, bits, ConstantU32(state, sample)), ConstantU32(state, 1u)); + reordered = EmitBinaryU32(state, spv::OpBitwiseOr, reordered, EmitBinaryU32(state, spv::OpShiftLeftLogical, bit, guest)); + } + } + return reordered; +} + +} + +std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std::uint32_t component); + +std::uint32_t EmitQuadPixel(SpirvEmitterState& state) { + const auto coordinate = [&](std::uint32_t component) { + const auto value = Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component)); + return EmitBinaryU32(state, spv::OpBitwiseAnd, Unary(state, spv::OpConvertFToU, TypeU32(state), value), ConstantU32(state, 1u)); + }; + return EmitBinaryU32(state, spv::OpBitwiseOr, coordinate(0u), EmitBinaryU32(state, spv::OpShiftLeftLogical, coordinate(1u), ConstantU32(state, 1u))); +} + +std::uint32_t InterpolationOffset(SpirvEmitterState& state, std::uint8_t location) { + const std::array components{ConstantF32Value(state, SampleLocationOffset(location, 0u)), ConstantF32Value(state, SampleLocationOffset(location, 1u))}; + return state.module.Constant(spv::OpConstantComposite, TypeF32Vector(state, 2u), std::span(components)); +} + +std::uint32_t EmitSampleOffset(SpirvEmitterState& state) { + const auto& locations = state.inputInfo.pixel->sampleLocations; + const bool vary = state.inputInfo.pixel->SampleLocationsVary(); + const bool iterated = state.program.Metadata().pixelIteratesSamples; + if (!iterated && !vary) return InterpolationOffset(state, locations[0]); + const auto count = vary ? static_cast(locations.size()) : 8u; + std::array offsets{}; + for (std::uint32_t index = 0; index < count; ++index) offsets[index] = InterpolationOffset(state, locations[index]); + const auto array = state.module.Type(spv::OpTypeArray, TypeF32Vector(state, 2u), ConstantU32(state, count)); + const auto table = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPrivate, array), spv::StorageClassPrivate); + state.module.AddFunction(spv::OpStore, table, state.module.Constant(spv::OpConstantComposite, array, std::span(offsets.data(), count))); + auto index = iterated ? EmitBuiltinU32(state, StageInputKind::SampleId, 0u) : ConstantU32(state, 0u); + if (vary) index = EmitBinaryU32(state, spv::OpBitwiseOr, index, EmitBinaryU32(state, spv::OpShiftLeftLogical, EmitQuadPixel(state), ConstantU32(state, 3u))); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassPrivate, TypeF32Vector(state, 2u)), pointer, table, index); + const auto offset = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeF32Vector(state, 2u), offset, pointer); + return offset; +} + +std::uint32_t EmitOffsetBarycentricU32(SpirvEmitterState& state, StageInputKind kind, std::uint32_t component) { + const auto variable = InputVariableForKind(state, BarycentricInputOf(kind)); + if (variable == 0u) { + throw std::runtime_error("an offset barycentric read lacks its barycentric input"); + } + if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityInterpolationFunction)) == state.supportedCapabilities.end()) { + throw std::runtime_error("barycentrics away from the fragment's default position need the device's sampleRateShading"); + } + state.module.EmitCapability(spv::CapabilityInterpolationFunction); + const bool center = kind == StageInputKind::BaryCoordSmoothCenter || kind == StageInputKind::BaryCoordNoPerspectiveCenter; + const auto offset = center ? InterpolationOffset(state, 0u) : EmitSampleOffset(state); + const auto weights = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32Vector(state, 3u), weights, GlslStd450(state), GLSLstd450InterpolateAtOffset, variable, offset); + const auto weight = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), weight, weights, component + 1u); + return Unary(state, spv::OpBitcast, TypeU32(state), weight); +} + std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std::uint32_t component) { + if (BarycentricInputOf(kind) != kind) { + return EmitOffsetBarycentricU32(state, kind, component); + } if (kind == StageInputKind::LocalInvocationIndex) { return EmitLocalInvocationIndex(state); } @@ -51,7 +175,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"); @@ -66,9 +190,18 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: if (kind == StageInputKind::LocalInvocationId) { return local; } - const auto group = EmitInputComponentU32(state, StageInputKind::WorkgroupId, component); + const auto group = EmitBuiltinU32(state, StageInputKind::WorkgroupId, component); return EmitBinaryU32(state, spv::OpIAdd, local, EmitBinaryU32(state, spv::OpIMul, group, ConstantU32(state, size))); } + if (StageOf(state) == IrShaderStage::Compute && (kind == StageInputKind::WorkgroupId || kind == StageInputKind::GlobalInvocationId)) { + const auto offset = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::WorkgroupOffsetBase + component, 0u); + const auto value = EmitInputComponentU32(state, kind, component); + if (kind == StageInputKind::WorkgroupId) { + return EmitBinaryU32(state, spv::OpIAdd, value, offset); + } + const auto local = WorkgroupLocalSize(state, *ShaderWorkgroupInput(state)); + return EmitBinaryU32(state, spv::OpIAdd, value, EmitBinaryU32(state, spv::OpIMul, offset, ConstantU32(state, local[component]))); + } const auto variable = InputVariableForKind(state, kind); if (variable == 0) { return ConstantU32(state, 0u); @@ -81,17 +214,31 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: return bits; } if (kind == StageInputKind::HelperInvocation) { - const auto value = state.module.AllocateId(); + auto value = state.module.AllocateId(); const auto bits = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeBool(state), value, variable); + if (state.quadPixelUncovered != 0u) { + const auto helper = state.module.AllocateId(); + state.module.AddFunction(spv::OpLogicalOr, TypeBool(state), helper, value, state.quadPixelUncovered); + value = helper; + } state.module.AddFunction(spv::OpSelect, TypeU32(state), bits, value, ConstantU32(state, 1u), ConstantU32(state, 0u)); return bits; } - if (kind == StageInputKind::VertexIndex || kind == StageInputKind::InstanceIndex || kind == StageInputKind::InvocationId || kind == StageInputKind::PrimitiveId || kind == StageInputKind::Layer || kind == StageInputKind::SampleId) { + if (kind == StageInputKind::VertexIndex || kind == StageInputKind::InstanceIndex || kind == StageInputKind::BaseInstance || kind == StageInputKind::InvocationId || kind == StageInputKind::PrimitiveId || kind == StageInputKind::Layer || kind == StageInputKind::SampleId) { const auto value = state.module.AllocateId(); const auto bits = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeI32(state), value, variable); state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value); + return kind == StageInputKind::SampleId ? GuestSampleIndex(state, bits) : bits; + } + if (kind == StageInputKind::SampleMask) { + const auto pointer = state.module.AllocateId(); + const auto value = state.module.AllocateId(); + const auto bits = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeI32(state)), pointer, variable, ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpLoad, TypeI32(state), value, pointer); + state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value); return bits; } if (kind == StageInputKind::FragCoord || kind == StageInputKind::TessCoord) { @@ -103,6 +250,13 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value); return bits; } + if ((kind == StageInputKind::BaryCoordSmooth || kind == StageInputKind::BaryCoordNoPerspective) && MeshBarycentricPixel(state)) { + const auto value = state.module.AllocateId(); + const auto bits = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), value, EmitSolvedBarycentric(state, kind), component + 1u); + state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value); + return bits; + } if (kind == StageInputKind::BaryCoordSmooth || kind == StageInputKind::BaryCoordNoPerspective) { const auto pointer = state.module.AllocateId(); const auto value = state.module.AllocateId(); @@ -149,12 +303,17 @@ std::uint32_t EmitAttributeValue(SpirvEmitterState& state, std::uint32_t attr, s throw std::runtime_error("per-vertex attribute interpolation lacks its barycentric input"); } std::uint32_t sum = 0; + const auto solved = MeshBarycentricPixel(state) ? EmitSolvedBarycentric(state, barycentricKind) : 0u; for (std::uint32_t vertex = 0; vertex < 3u; vertex++) { const auto pointer = state.module.AllocateId(); const auto weight = state.module.AllocateId(); const auto product = state.module.AllocateId(); - state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeF32(state)), pointer, barycentric, ConstantU32(state, vertex)); - state.module.AddFunction(spv::OpLoad, TypeF32(state), weight, pointer); + if (solved != 0u) { + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), weight, solved, vertex); + } else { + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeF32(state)), pointer, barycentric, ConstantU32(state, vertex)); + state.module.AddFunction(spv::OpLoad, TypeF32(state), weight, pointer); + } state.module.AddFunction(spv::OpFMul, TypeF32(state), product, loadPerVertex(vertex), weight); if (vertex == 0u) { sum = product; @@ -178,12 +337,15 @@ std::uint32_t EmitAttributeValue(SpirvEmitterState& state, std::uint32_t attr, s } std::uint32_t EmitInterpolationParameterValue(SpirvEmitterState& state, std::uint32_t attr, std::uint32_t chan, std::uint32_t mode) { - if (PixelParameterIsDefault(state, attr)) { - return mode == 2u ? EmitAttributeValue(state, attr, chan) : ConstantU32(state, 0u); + if (state.inputInfo.pixel == nullptr) { + throw std::runtime_error("interpolation parameter refers to an undefined attribute"); + } + if (attr >= state.inputInfo.pixel->inputNum || PixelParameterIsDefault(state, attr)) { + return mode == 2u ? ConstantU32(state, state.inputInfo.pixel->InputDefaultBits(attr, chan & 3u)) : ConstantU32(state, 0u); } const auto* input = SpirvInputBindingForParameter(state, attr); if (input == nullptr) { - throw std::runtime_error("interpolation parameter refers to an undefined attribute"); + return mode == 2u ? ConstantU32(state, state.inputInfo.pixel->InputDefaultBits(attr, chan & 3u)) : ConstantU32(state, 0u); } if (!input->perVertex) { return EmitAttributeValue(state, attr, chan); @@ -249,8 +411,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(); @@ -265,8 +427,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(); @@ -283,6 +446,8 @@ bool AlphaToCoverage(const SpirvEmitterState& state) { } void StoreCoverage(SpirvEmitterState& state, std::uint32_t mask) { + if (state.quadSampleCoverage != 0u) mask = Binary(state, spv::OpBitwiseAnd, TypeI32(state), mask, state.quadSampleCoverage); + if (state.sampleOrderWord != 0u) mask = Unary(state, spv::OpBitcast, TypeI32(state), ReorderedSampleBits(state, Unary(state, spv::OpBitcast, TypeU32(state), mask), true)); const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeI32(state)), pointer, state.sampleMaskVariable, ConstantU32(state, 0u)); state.module.AddFunction(spv::OpStore, pointer, mask); @@ -418,6 +583,10 @@ std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const return vector; } +float PointSizeFloor(const ShaderVertexInputInfo& vertex) { + return std::max(vertex.pointSizeMin, std::numeric_limits::min()); +} + void EmitAuxPositionExport(SpirvValueEmitContext& ctx, std::uint32_t data, const ExportInfo& exp) { auto& state = ctx.state; if (state.inputInfo.vertex == nullptr) { @@ -450,7 +619,13 @@ void EmitAuxPositionExport(SpirvValueEmitContext& ctx, std::uint32_t data, const const auto f32 = state.module.AllocateId(); state.module.AddFunction(spv::OpBitcast, TypeF32(state), f32, raw); if (output.pointSize) { - state.module.AddFunction(spv::OpStore, state.pointSizeVariable, f32); + auto size = f32; + if (state.inputInfo.vertex->pointList) { + size = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), size, GlslStd450(state), GLSLstd450FClamp, f32, ConstantF32(state, std::bit_cast(PointSizeFloor(*state.inputInfo.vertex))), ConstantF32(state, std::bit_cast(state.inputInfo.vertex->pointSizeMax))); + } + const auto pointer = state.program.Resources().stage == IrShaderStage::Mesh ? MeshOutputPointer(state, StageOutputKind::PointSize) : state.pointSizeVariable; + state.module.AddFunction(spv::OpStore, pointer, size); continue; } const auto storeDistance = [&](StageOutputKind kind, std::uint32_t variable, std::uint32_t index) { @@ -508,6 +683,59 @@ std::uint32_t ConvertClipCoordinate(SpirvEmitterState& state, std::uint32_t coor return ndc; } +std::uint32_t RemapPositionDepth(SpirvEmitterState& state, std::uint32_t position) { + const auto& remap = state.inputInfo.vertex->depthRemap; + const auto f32 = TypeF32(state); + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + std::array components{}; + for (std::uint32_t i = 0; i < 4u; i++) { + components.at(i) = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, f32, components.at(i), position, i); + } + const auto mode = state.module.SpecializationConstant(u32, PipelineSpecialization::DepthRemapBase, remap.mode); + if (state.pushConstantVariable == 0u) throw std::runtime_error("depth remap push constants were not declared"); + const auto pushFloat = [&](std::uint32_t dword) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePushConstantElementPointer(state), pointer, state.pushConstantVariable, ConstantU32(state, 0u), ConstantU32(state, DepthRemapPushOffsetBytes / 4u + dword)); + const auto bits = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, u32, bits, pointer); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitcast, f32, value, bits); + return value; + }; + const auto scale = pushFloat(0u); + const auto offset = pushFloat(1u); + const auto scaled = state.module.AllocateId(); + state.module.AddFunction(spv::OpFMul, f32, scaled, components[2], scale); + const auto shifted = state.module.AllocateId(); + state.module.AddFunction(spv::OpFMul, f32, shifted, components[3], offset); + const auto remapped = state.module.AllocateId(); + state.module.AddFunction(spv::OpFAdd, f32, remapped, scaled, shifted); + const auto active = state.module.AllocateId(); + state.module.AddFunction(spv::OpINotEqual, boolean, active, mode, ConstantU32(state, 0u)); + const auto z = Select(state, f32, active, remapped, components[2]); + const auto symmetric = state.module.AllocateId(); + state.module.AddFunction(spv::OpIEqual, boolean, symmetric, mode, ConstantU32(state, 2u)); + const auto nearSymmetric = state.module.AllocateId(); + state.module.AddFunction(spv::OpFAdd, f32, nearSymmetric, components[3], components[2]); + const auto near = Select(state, f32, symmetric, nearSymmetric, components[2]); + const auto far = state.module.AllocateId(); + state.module.AddFunction(spv::OpFSub, f32, far, components[3], components[2]); + const auto inside = ConstantF32Value(state, 1.0f); + const auto storeDistance = [&](std::uint32_t index, std::uint32_t distance) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, f32), pointer, state.clipDistanceVariable, ConstantU32(state, index)); + state.module.AddFunction(spv::OpStore, pointer, Select(state, f32, active, distance, inside)); + }; + if (state.clipDistanceVariable == 0u) throw std::runtime_error("depth remap clip distances were not declared"); + storeDistance(state.depthRemapClipIndex, near); + storeDistance(state.depthRemapClipIndex + 1u, far); + const auto converted = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4u), converted, components[0], components[1], z, components[3]); + return converted; +} + std::uint32_t ConvertPositionToClipSpace(SpirvEmitterState& state, std::uint32_t position) { if (state.inputInfo.vertex == nullptr) { throw std::runtime_error("vertex input info is missing for clip-space conversion"); @@ -555,7 +783,7 @@ std::uint32_t MeshOutputType(SpirvEmitterState& state, StageOutputKind kind) { if (kind == StageOutputKind::Layer) { return TypeU32(state); } - return IsMeshDistance(kind) ? TypeF32(state) : TypeF32Vector(state, 4u); + return IsMeshDistance(kind) || kind == StageOutputKind::PointSize ? TypeF32(state) : TypeF32Vector(state, 4u); } std::uint32_t MeshDistancePointer(SpirvEmitterState& state, const SpirvOutputBinding& output, std::uint32_t vertex) { @@ -579,9 +807,16 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins if (variable == 0u) { throw std::runtime_error("tessellation attribute has no interface variable"); } - const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput; + const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput || kind == TessellationAttribute::InstanceInput; const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput; + if (kind == TessellationAttribute::InstanceOutput) { + return variable; + } const auto pointer = state.module.AllocateId(); + if (kind == TessellationAttribute::InstanceInput) { + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, storage, TypeU32(state)), pointer, variable, ConstantU32(state, 0u)); + return pointer; + } if (kind == TessellationAttribute::Factor) { if (!inst.Argument(1)->HasImmediate()) { throw std::runtime_error("tessellation factor index must be immediate"); @@ -598,29 +833,46 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins const auto address = ctx.Arg(inst, 1); const bool patch = kind == TessellationAttribute::PatchOutput || kind == TessellationAttribute::PatchInput; const bool local = kind == TessellationAttribute::LocalOutput || kind == TessellationAttribute::ControlInput; - const auto stride = local ? state.program.Metadata().tessellationLocalStride : state.program.Metadata().tessellationControlStride; + const auto stride = local ? state.program.Metadata().tessellationLocalStride : kind == TessellationAttribute::ControlShared ? state.program.Metadata().tessellationSharedStride : state.program.Metadata().tessellationControlStride; const auto type = TypePointer(state, storage, TypeU32(state)); const auto inPatch = [&](std::uint32_t vertex) { - if (kind != TessellationAttribute::ControlInput) return vertex; + if (kind != TessellationAttribute::ControlInput && kind != TessellationAttribute::EvaluationInput) return vertex; + const auto& tess = state.inputInfo.vertex->tess; const auto clamped = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeU32(state), clamped, GlslStd450(state), GLSLstd450UMin, vertex, ConstantU32(state, state.inputInfo.vertex->tess.inputControlPoints - 1u)); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), clamped, GlslStd450(state), GLSLstd450UMin, vertex, ConstantU32(state, (kind == TessellationAttribute::ControlInput ? tess.inputControlPoints : tess.outputControlPoints) - 1u)); return clamped; }; + const bool sharedRead = kind == TessellationAttribute::ControlShared && inst.Opcode() == IrOpcode::GetTessellationAttribute; const auto* sum = inst.Argument(1); const auto* scaled = sum->Opcode() == IrOpcode::IAdd32 ? sum->Argument(0) : nullptr; - if (kind == TessellationAttribute::ControlInput && scaled != nullptr && sum->Argument(1)->HasImmediate() && scaled->Opcode() == IrOpcode::IMul32 && scaled->Argument(1)->HasImmediate() && scaled->Argument(1)->ImmediateU32() == stride) { + if ((kind == TessellationAttribute::ControlInput || sharedRead) && scaled != nullptr && sum->Argument(1)->HasImmediate() && scaled->Opcode() == IrOpcode::IMul32 && scaled->Argument(1)->HasImmediate() && scaled->Argument(1)->ImmediateU32() == stride) { const auto constant = sum->Argument(1)->ImmediateU32(); const auto vertex = inPatch(EmitBinaryU32(state, spv::OpIAdd, ctx.Arg(*scaled, 0), ConstantU32(state, constant / stride))); state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, ConstantU32(state, constant % stride / 16u), ConstantU32(state, constant % stride % 16u / 4u)); return pointer; } + if (inst.Argument(1)->HasImmediate()) { + const auto value = inst.Argument(1)->ImmediateU32(); + const auto offset = patch ? value : value % stride; + const auto attribute = ConstantU32(state, offset >> 4u); + const auto component = ConstantU32(state, (offset >> 2u) & 3u); + if (kind == TessellationAttribute::LocalOutput || patch) { + state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, attribute, component); + return pointer; + } + const bool own = kind == TessellationAttribute::ControlOutput || (kind == TessellationAttribute::ControlShared && !sharedRead); + const auto controlPoint = kind == TessellationAttribute::ControlInput ? std::min(value / stride, state.inputInfo.vertex->tess.inputControlPoints - 1u) : kind == TessellationAttribute::EvaluationInput ? std::min(value / stride, state.inputInfo.vertex->tess.outputControlPoints - 1u) : value / stride; + state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, own ? EmitLaneId(state) : ConstantU32(state, controlPoint), attribute, component); + return pointer; + } const auto offset = patch ? address : EmitBinaryU32(state, spv::OpUMod, address, ConstantU32(state, stride)); const auto attribute = EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 4u)); const auto component = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 2u)), ConstantU32(state, 3u)); if (kind == TessellationAttribute::LocalOutput || patch) { state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, attribute, component); } else { - const auto vertex = kind == TessellationAttribute::ControlOutput ? EmitLaneId(state) : inPatch(EmitBinaryU32(state, spv::OpUDiv, address, ConstantU32(state, stride))); + const bool own = kind == TessellationAttribute::ControlOutput || (kind == TessellationAttribute::ControlShared && !sharedRead); + const auto vertex = own ? EmitLaneId(state) : inPatch(EmitBinaryU32(state, spv::OpUDiv, address, ConstantU32(state, stride))); state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, attribute, component); } return pointer; @@ -629,7 +881,7 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins } void DefineTessellationInterfaces(SpirvEmitterState& state) { - std::array used {}; + std::array used {}; for (const IrBlock* block : state.program.BlockOrder()) { for (const IrValue* inst : block->Instructions()) { if (inst->Opcode() != IrOpcode::GetTessellationAttribute && inst->Opcode() != IrOpcode::SetTessellationAttribute) { @@ -650,31 +902,40 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { continue; } const auto kind = static_cast(index); - const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput; + const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput || kind == TessellationAttribute::InstanceInput; const bool patch = kind == TessellationAttribute::PatchOutput || kind == TessellationAttribute::PatchInput; const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput; + const bool instance = kind == TessellationAttribute::InstanceOutput || kind == TessellationAttribute::InstanceInput; std::uint32_t type; if (kind == TessellationAttribute::Factor) { type = array(TypeF32(state), 4u); + } else if (instance) { + type = kind == TessellationAttribute::InstanceInput ? array(TypeU32(state), tess.inputControlPoints) : TypeU32(state); } else if (patch) { type = array(TypeU32Vector(state, 4u), (state.program.Metadata().tessellationPatchStride + 15u) / 16u); } else { const bool local = kind == TessellationAttribute::LocalOutput || kind == TessellationAttribute::ControlInput; - const auto stride = local ? state.program.Metadata().tessellationLocalStride : state.program.Metadata().tessellationControlStride; + const auto stride = local ? state.program.Metadata().tessellationLocalStride : kind == TessellationAttribute::ControlShared ? state.program.Metadata().tessellationSharedStride : state.program.Metadata().tessellationControlStride; type = array(TypeU32Vector(state, 4u), (stride + 15u) / 16u); if (kind != TessellationAttribute::LocalOutput) { type = array(type, local ? tess.inputControlPoints : tess.outputControlPoints); } } auto& variable = state.tessVariables.at(index); - variable = DefineInterfaceVariable(state, type, storage, "tess_attributes"); - if (kind == TessellationAttribute::Factor) { + variable = DefineInterfaceVariable(state, type, storage, instance ? "tess_instance" : "tess_attributes"); + if (instance) { + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation, (state.program.Metadata().tessellationLocalStride + 15u) / 16u); + } else if (kind == TessellationAttribute::Factor) { state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBuiltIn, spv::BuiltInTessLevelOuter); state.tessInnerVariable = DefineInterfaceVariable(state, array(TypeF32(state), 2u), storage, "tess_inner"); state.module.AddAnnotation(spv::OpDecorate, state.tessInnerVariable, spv::DecorationBuiltIn, spv::BuiltInTessLevelInner); state.module.AddAnnotation(spv::OpDecorate, state.tessInnerVariable, spv::DecorationPatch); } else { - state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation, patch ? (state.program.Metadata().tessellationControlStride + 15u) / 16u : 0u); + const auto controlLocations = (state.program.Metadata().tessellationControlStride + 15u) / 16u; + const bool patchUsed = used[static_cast(TessellationAttribute::PatchOutput)] || used[static_cast(TessellationAttribute::PatchInput)]; + const auto patchLocations = patchUsed ? (state.program.Metadata().tessellationPatchStride + 15u) / 16u : 0u; + const auto location = patch ? controlLocations : kind == TessellationAttribute::ControlShared ? controlLocations + patchLocations : 0u; + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation, location); } if (kind == TessellationAttribute::Factor || patch) { state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationPatch); @@ -684,7 +945,7 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { void DefineTessellationExecutionModes(SpirvEmitterState& state) { const auto& tess = state.inputInfo.vertex->tess; - if ((tess.domain != 1u && tess.domain != 2u) || (tess.partitioning != 0u && tess.partitioning != 2u) || (tess.outputTopology != 2u && tess.outputTopology != 3u)) { + if ((tess.domain != 1u && tess.domain != 2u) || (tess.partitioning != 0u && tess.partitioning != 2u && tess.partitioning != 3u) || (tess.outputTopology != 2u && tess.outputTopology != 3u)) { throw std::runtime_error("unsupported tessellation domain, partitioning, or output topology"); } state.module.EmitCapability(spv::CapabilityTessellation); @@ -692,7 +953,7 @@ void DefineTessellationExecutionModes(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOutputVertices, tess.outputControlPoints); } else { state.module.AddExecutionMode(state.mainFunc, tess.domain == 2u ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles); - state.module.AddExecutionMode(state.mainFunc, tess.partitioning == 0u ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd); + state.module.AddExecutionMode(state.mainFunc, tess.partitioning == 0u ? spv::ExecutionModeSpacingEqual : tess.partitioning == 3u ? spv::ExecutionModeSpacingFractionalEven : spv::ExecutionModeSpacingFractionalOdd); state.module.AddExecutionMode(state.mainFunc, tess.outputTopology == 3u ? spv::ExecutionModeVertexOrderCcw : spv::ExecutionModeVertexOrderCw); } } @@ -726,34 +987,75 @@ void DefineMeshOutputs(SpirvEmitterState& state) { output.meshDataVariable = MeshArray(state, spv::StorageClassPrivate, TypeF32(state), state.laneCount); continue; } - if (output.kind != StageOutputKind::Position && output.kind != StageOutputKind::Parameter && output.kind != StageOutputKind::Layer) { + if (output.kind != StageOutputKind::Position && output.kind != StageOutputKind::Parameter && output.kind != StageOutputKind::Layer && output.kind != StageOutputKind::PointSize) { throw std::runtime_error("unsupported mesh output kind"); } const auto type = MeshOutputType(state, output.kind); - output.variableId = MeshArray(state, spv::StorageClassOutput, type, output.kind == StageOutputKind::Layer ? mesh.maxPrimitives : mesh.maxVertices); const bool shared = output.kind == StageOutputKind::Layer; output.meshDataVariable = MeshArray(state, shared ? spv::StorageClassWorkgroup : spv::StorageClassPrivate, type, shared ? mesh.maxVertices : state.laneCount); + if (state.meshBarycentricVertex && output.kind == StageOutputKind::Parameter && std::ranges::find(state.meshExportedLocations, output.location) == state.meshExportedLocations.end()) { + continue; + } + const auto layerBlock = output.kind == StageOutputKind::Layer ? state.module.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInLayer}}, {spv::OpMemberDecorate, {0u, spv::DecorationPerPrimitiveEXT}}, {spv::OpDecorate, {spv::DecorationBlock}}}, type) : 0u; + output.variableId = MeshArray(state, spv::StorageClassOutput, output.kind == StageOutputKind::Layer ? layerBlock : type, output.kind == StageOutputKind::Layer ? mesh.maxPrimitives : mesh.maxVertices); state.interfaceVariables.push_back(output.variableId); state.module.AddName(output.variableId, output.debugName); if (output.kind == StageOutputKind::Parameter) { state.module.AddAnnotation(spv::OpDecorate, output.variableId, spv::DecorationLocation, output.location); - } else { - state.module.AddAnnotation(spv::OpDecorate, output.variableId, spv::DecorationBuiltIn, output.kind == StageOutputKind::Layer ? spv::BuiltInLayer : spv::BuiltInPosition); - } - if (output.kind == StageOutputKind::Layer) { - state.module.AddAnnotation(spv::OpDecorate, output.variableId, spv::DecorationPerPrimitiveEXT); + } else if (output.kind != StageOutputKind::Layer) { + state.module.AddAnnotation(spv::OpDecorate, output.variableId, spv::DecorationBuiltIn, output.kind == StageOutputKind::PointSize ? spv::BuiltInPointSize : spv::BuiltInPosition); } if (output.kind == StageOutputKind::Position) { state.module.AddAnnotation(spv::OpDecorate, output.variableId, spv::DecorationInvariant); } } + if (state.meshBarycentricVertex) { + if (std::ranges::none_of(state.outputs, [](const SpirvOutputBinding& output) { return output.kind == StageOutputKind::Position; })) { + throw std::runtime_error("barycentric emulation on the mesh path needs the mesh program to export a position"); + } + const auto vec3 = TypeF32Vector(state, 3u); + state.meshPositionStaging = MeshArray(state, spv::StorageClassWorkgroup, TypeF32Vector(state, 4u), mesh.maxVertices); + const auto defineClip = [&](std::uint32_t location, bool linear, const char* name) { + if (location == BarycentricEmulationLayout::NoLocation) return 0u; + const auto variable = MeshArray(state, spv::StorageClassOutput, vec3, mesh.maxVertices); + state.interfaceVariables.push_back(variable); + state.module.AddName(variable, name); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation, location); + if (linear) state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationNoPerspective); + return variable; + }; + state.meshClipSmooth = defineClip(state.barycentricSmoothLocation, false, "meshBarycentricClipSmooth"); + state.meshClipLinear = defineClip(state.barycentricLinearLocation, true, "meshBarycentricClipLinear"); + if (state.barycentricPositionsLocation == BarycentricEmulationLayout::NoLocation) { + throw std::runtime_error("barycentric emulation on the mesh path has no location for the corner positions"); + } + const auto corners = state.module.Type(spv::OpTypeArray, TypeF32Vector(state, 4u), ConstantU32(state, 3u)); + state.meshFlatPositions = MeshArray(state, spv::StorageClassOutput, corners, mesh.maxPrimitives); + state.interfaceVariables.push_back(state.meshFlatPositions); + state.module.AddName(state.meshFlatPositions, "meshBarycentricPositions"); + state.module.AddAnnotation(spv::OpDecorate, state.meshFlatPositions, spv::DecorationLocation, state.barycentricPositionsLocation); + state.module.AddAnnotation(spv::OpDecorate, state.meshFlatPositions, spv::DecorationPerPrimitiveEXT); + for (const auto& [sourceLocation, relocated] : state.meshPerVertexLocations) { + const auto source = std::ranges::find_if(state.outputs, [sourceLocation = sourceLocation](const SpirvOutputBinding& output) { return output.kind == StageOutputKind::Parameter && output.location == sourceLocation; }); + if (source == state.outputs.end()) { + throw std::runtime_error("barycentric emulation on the mesh path: the mesh program does not export per-vertex parameter " + std::to_string(sourceLocation)); + } + const auto staging = MeshArray(state, spv::StorageClassWorkgroup, TypeF32Vector(state, 4u), mesh.maxVertices); + const auto perPrimitive = MeshArray(state, spv::StorageClassOutput, corners, mesh.maxPrimitives); + state.interfaceVariables.push_back(perPrimitive); + state.module.AddName(perPrimitive, "meshPerVertexParameter" + std::to_string(sourceLocation)); + state.module.AddAnnotation(spv::OpDecorate, perPrimitive, spv::DecorationLocation, relocated); + state.module.AddAnnotation(spv::OpDecorate, perPrimitive, spv::DecorationPerPrimitiveEXT); + state.meshVertexParameters.push_back({source->meshDataVariable, staging, perPrimitive}); + } + } state.meshAllocation = MeshArray(state, spv::StorageClassWorkgroup, TypeU32(state), 2u); state.meshPrimitiveData = MeshArray(state, spv::StorageClassPrivate, TypeU32(state), state.laneCount); - state.meshPrimitives = MeshArray(state, spv::StorageClassOutput, TypeU32Vector(state, 3u), mesh.maxPrimitives); + state.meshPrimitives = MeshArray(state, spv::StorageClassOutput, mesh.outputPrimitiveSize == 1u ? TypeU32(state) : 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 == 1u ? spv::BuiltInPrimitivePointIndicesEXT : 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) { @@ -762,11 +1064,45 @@ void DefineMeshOutputs(SpirvEmitterState& state) { } } +void EmitPixelEntryPoint(SpirvEmitterState& state) { + state.module.AddFunction(spv::OpFunction, TypeVoid(state), state.mainFunc, spv::FunctionControlMaskNone, TypeFunction(state)); + EmitLabel(state, state.module.AllocateId()); + state.module.AddFunction(spv::OpStore, state.pixelStopFlag, ConstantBool(state, false)); + state.module.AddFunction(spv::OpFunctionCall, TypeVoid(state), state.module.AllocateId(), state.pixelGuestFunc); + if (state.pixelStopStored) { + const auto stopped = state.module.AllocateId(); + const auto stop = state.module.AllocateId(); + const auto done = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeBool(state), stopped, state.pixelStopFlag); + state.module.AddFunction(spv::OpSelectionMerge, done, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, stopped, stop, done); + EmitLabel(state, stop); + 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, done); + } + state.module.AddFunction(spv::OpReturn); + state.module.AddFunction(spv::OpFunctionEnd); +} + void EmitMeshEntryPoint(SpirvEmitterState& state) { state.module.AddFunction(spv::OpFunction, TypeVoid(state), state.mainFunc, spv::FunctionControlMaskNone, TypeFunction(state)); EmitLabel(state, state.module.AllocateId()); for (std::uint32_t half = 0; half < state.laneCount; half++) { state.module.AddFunction(spv::OpStore, MeshElement(state, state.meshPrimitiveData, spv::StorageClassPrivate, TypeU32(state), ConstantU32(state, half)), ConstantU32(state, 0x80000000u)); + if (state.inputInfo.vertex->pointList) { + const auto& vertex = *state.inputInfo.vertex; + const bool fromVertex = (vertex.paClVsOutCntl & (1u << 16u)) != 0u; + if (!fromVertex && !(vertex.pointSize > 0.0f)) FailEmit("mesh points need a positive PA_SU_POINT_SIZE"); + if (fromVertex && !(vertex.pointSizeMax > 0.0f)) FailEmit("mesh point sizes need a positive PA_SU_POINT_MINMAX maximum"); + const auto size = fromVertex ? std::clamp(vertex.pointSize, PointSizeFloor(vertex), vertex.pointSizeMax) : vertex.pointSize; + state.laneHalf = half; + state.module.AddFunction(spv::OpStore, MeshOutputPointer(state, StageOutputKind::PointSize), ConstantF32(state, std::bit_cast(size))); + } } const auto firstInvocation = state.module.AllocateId(); state.laneHalf = 0; @@ -798,14 +1134,15 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) { const auto anyVertex = state.module.AllocateId(); state.module.AddFunction(spv::OpExtInst, TypeU32(state), anyVertex, GlslStd450(state), GLSLstd450UMax, vertices, ConstantU32(state, 1u)); const auto lastVertex = EmitBinaryU32(state, spv::OpISub, anyVertex, ConstantU32(state, 1u)); - for (std::uint32_t half = 0; half < state.laneCount; half++) { + const bool emulating = state.meshBarycentricVertex; + const auto emitVertexPart = [&](std::uint32_t half) { state.laneHalf = half; const auto index = EmitLocalInvocationIndex(state); const auto isVertex = state.module.AllocateId(); state.module.AddFunction(spv::OpULessThan, TypeBool(state), isVertex, index, vertices); EmitIfCondition(state, isVertex, [&]() { for (const auto& output : state.outputs) { - if (output.kind == StageOutputKind::Layer) { + if (output.kind == StageOutputKind::Layer || output.variableId == 0u) { continue; } const auto type = MeshOutputType(state, output.kind); @@ -813,22 +1150,52 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) { const auto pointer = IsMeshDistance(output.kind) ? MeshDistancePointer(state, output, index) : MeshElement(state, output.variableId, spv::StorageClassOutput, type, index); state.module.AddFunction(spv::OpStore, pointer, value); } + if (!emulating) { + return; + } + const auto position = std::ranges::find_if(state.outputs, [](const SpirvOutputBinding& output) { return output.kind == StageOutputKind::Position; }); + const auto value = MeshLoad(state, position->meshDataVariable, spv::StorageClassPrivate, TypeF32Vector(state, 4u), ConstantU32(state, half)); + state.module.AddFunction(spv::OpStore, MeshElement(state, state.meshPositionStaging, spv::StorageClassWorkgroup, TypeF32Vector(state, 4u), index), value); + const auto clip = state.module.AllocateId(); + state.module.AddFunction(spv::OpVectorShuffle, TypeF32Vector(state, 3u), clip, value, value, 0u, 1u, 3u); + for (const auto& parameter : state.meshVertexParameters) { + const auto staged = MeshLoad(state, parameter[0], spv::StorageClassPrivate, TypeF32Vector(state, 4u), ConstantU32(state, half)); + state.module.AddFunction(spv::OpStore, MeshElement(state, parameter[1], spv::StorageClassWorkgroup, TypeF32Vector(state, 4u), index), staged); + } + for (const auto variable : {state.meshClipSmooth, state.meshClipLinear}) { + if (variable != 0u) { + state.module.AddFunction(spv::OpStore, MeshElement(state, variable, spv::StorageClassOutput, TypeF32Vector(state, 3u), index), clip); + } + } }); + }; + const auto emitPrimitivePart = [&](std::uint32_t half) { + state.laneHalf = half; + const auto index = EmitLocalInvocationIndex(state); const auto isPrimitive = state.module.AllocateId(); state.module.AddFunction(spv::OpULessThan, TypeBool(state), isPrimitive, index, primitives); EmitIfCondition(state, isPrimitive, [&]() { 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++) { + if (mesh.outputPrimitiveSize == 1u) { 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); + state.module.AddFunction(spv::OpBitFieldUExtract, TypeU32(state), field, packed, ConstantU32(state, 0u), ConstantU32(state, 9u)); + vertex.at(0) = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), vertex.at(0), GlslStd450(state), GLSLstd450UMin, field, lastVertex); + const auto indicesPointer = MeshElement(state, state.meshPrimitives, spv::StorageClassOutput, TypeU32(state), index); + state.module.AddFunction(spv::OpStore, indicesPointer, vertex.at(0)); + } else { + 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 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 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 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)); @@ -838,10 +1205,45 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) { continue; } const auto layer = MeshLoad(state, output.meshDataVariable, spv::StorageClassWorkgroup, TypeU32(state), vertex.at(state.inputInfo.vertex->mesh.provokingVertex)); - const auto pointer = MeshElement(state, output.variableId, spv::StorageClassOutput, TypeU32(state), index); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeU32(state)), pointer, output.variableId, index, ConstantU32(state, 0u)); state.module.AddFunction(spv::OpStore, pointer, layer); } + if (emulating) { + const auto corners = state.module.Type(spv::OpTypeArray, TypeF32Vector(state, 4u), ConstantU32(state, 3u)); + std::array staged {}; + for (std::uint32_t component = 0; component < 3u; component++) { + staged.at(component) = MeshLoad(state, state.meshPositionStaging, spv::StorageClassWorkgroup, TypeF32Vector(state, 4u), vertex.at(component)); + } + const auto array = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, corners, array, staged[0], staged[1], staged[2]); + state.module.AddFunction(spv::OpStore, MeshElement(state, state.meshFlatPositions, spv::StorageClassOutput, corners, index), array); + for (const auto& parameter : state.meshVertexParameters) { + std::array values {}; + for (std::uint32_t component = 0; component < 3u; component++) { + values.at(component) = MeshLoad(state, parameter[1], spv::StorageClassWorkgroup, TypeF32Vector(state, 4u), vertex.at(component)); + } + const auto perVertex = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, corners, perVertex, values[0], values[1], values[2]); + state.module.AddFunction(spv::OpStore, MeshElement(state, parameter[2], spv::StorageClassOutput, corners, index), perVertex); + } + } }); + }; + if (!emulating) { + for (std::uint32_t half = 0; half < state.laneCount; half++) { + emitVertexPart(half); + emitPrimitivePart(half); + } + } else { + for (std::uint32_t half = 0; half < state.laneCount; half++) { + emitVertexPart(half); + } + state.laneHalf = 0; + state.module.AddFunction(spv::OpControlBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | spv::MemorySemanticsWorkgroupMemoryMask)); + for (std::uint32_t half = 0; half < state.laneCount; half++) { + emitPrimitivePart(half); + } } state.laneHalf = 0; state.module.AddFunction(spv::OpReturn); @@ -1125,10 +1527,16 @@ void EmitProgram(SpirvEmitterState& state) { break; } } - state.module.AddFunction(spv::OpFunction, TypeVoid(state), state.meshGuestFunc != 0u ? state.meshGuestFunc : state.mainFunc, spv::FunctionControlMaskNone, TypeFunction(state)); + if (program.Resources().stage == IrShaderStage::Pixel && state.bdaStopsInvocations && !OrderedPixelShader(state)) { + state.pixelGuestFunc = state.module.AllocateId(); + state.pixelStopFlag = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPrivate, TypeBool(state)), spv::StorageClassPrivate); + if (state.spirvVersion >= 0x00010400u) state.interfaceVariables.push_back(state.pixelStopFlag); + state.module.AddName(state.pixelStopFlag, "bda_stopped"); + } + state.module.AddFunction(spv::OpFunction, TypeVoid(state), state.meshGuestFunc != 0u ? state.meshGuestFunc : state.pixelGuestFunc != 0u ? state.pixelGuestFunc : 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++) { @@ -1136,10 +1544,14 @@ void EmitProgram(SpirvEmitterState& state) { } } if (const auto window = state.program.Metadata().vectorWindowSize; window != 0u) { - const auto array = state.module.Type(spv::OpTypeArray, TypeU32(state), ConstantU32(state, window)); + const auto indexed = state.vectorWindowIndexed != 0u ? state.vectorWindowIndexed : window; + const auto array = state.module.Type(spv::OpTypeArray, TypeU32(state), ConstantU32(state, indexed)); const auto zero = state.module.Constant(spv::OpConstantNull, array); for (std::uint32_t half = 0; half < state.laneCount; half++) { state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, array), state.vectorWindowVariable.at(half), spv::StorageClassFunction, zero); + for (const auto slot : state.vectorWindowSlots.at(half)) { + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), slot, spv::StorageClassFunction, ConstantU32(state, 0u)); + } } } if (state.pixelValidMaskVariable != 0u) { @@ -1153,7 +1565,11 @@ void EmitProgram(SpirvEmitterState& state) { } EmitLdsBufferBase(state); if (state.requirements.ldsLock) { - state.module.AddFunction(spv::OpStore, EmitLdsLockPointer(state), ConstantU32(state, 0u)); + const auto firstInvocation = state.module.AllocateId(); + state.module.AddFunction(spv::OpIEqual, TypeBool(state), firstInvocation, EmitLocalInvocationIndex(state), ConstantU32(state, 0u)); + EmitIfCondition(state, firstInvocation, [&] { + state.module.AddFunction(spv::OpStore, EmitLdsLockPointer(state), ConstantU32(state, 0u)); + }); state.module.AddFunction(spv::OpControlBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | LdsMemorySemantics(state))); } if (state.gdsVariable != 0u) { @@ -1163,11 +1579,79 @@ void EmitProgram(SpirvEmitterState& state) { if (state.pixelValidMaskVariable != 0u) { state.module.AddFunction(spv::OpStore, state.pixelValidMaskVariable, ConstantU32(state, 1u)); } + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->ReordersSamples()) { + const auto& order = state.inputInfo.pixel->sampleOrder; + const auto parity = [&](std::uint32_t component) { + const auto value = Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component)); + return EmitBinaryU32(state, spv::OpBitwiseAnd, Unary(state, spv::OpConvertFToU, TypeU32(state), value), ConstantU32(state, 1u)); + }; + const auto quad = EmitBinaryU32(state, spv::OpBitwiseOr, parity(0u), EmitBinaryU32(state, spv::OpShiftLeftLogical, parity(1u), ConstantU32(state, 1u))); + const auto word = [&](std::size_t pixel) { return ConstantU32(state, order[pixel] == 0u ? 0x76543210u : order[pixel]); }; + auto selected = word(3u); + for (std::uint32_t pixel = 3u; pixel-- > 0u;) selected = Select(state, TypeU32(state), EmitCompareU32Constant(state, spv::OpIEqual, quad, pixel), word(pixel), selected); + state.sampleOrderWord = selected; + } if (AlphaToCoverage(state)) StoreCoverage(state, ConstantI32(state, -1)); + EmitBdaAnchors(state); + if (state.pointSizeVariable != 0u && state.inputInfo.vertex != nullptr && state.inputInfo.vertex->pointList) { + const auto& vertex = *state.inputInfo.vertex; + const bool fromVertex = (vertex.paClVsOutCntl & (1u << 16u)) != 0u; + if (!fromVertex && !(vertex.pointSize > 0.0f)) FailEmit("a point list without a vertex point size needs a positive PA_SU_POINT_SIZE"); + if (fromVertex && !(vertex.pointSizeMax > 0.0f)) FailEmit("a vertex point size needs a positive PA_SU_POINT_MINMAX maximum"); + const auto size = fromVertex ? std::clamp(vertex.pointSize, PointSizeFloor(vertex), vertex.pointSizeMax) : vertex.pointSize; + state.module.AddFunction(spv::OpStore, state.pointSizeVariable, ConstantF32(state, std::bit_cast(size))); + } if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpBeginInvocationInterlockEXT); } EmitMemoryOffsets(state); + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->QuadMasked()) { + const auto& pixel = *state.inputInfo.pixel; + const auto quad = EmitQuadPixel(state); + auto bit = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, ConstantU32(state, pixel.quadPixelMask), quad), ConstantU32(state, 1u)); + if (pixel.QuadSampleMaskPartial()) { + const auto top = ConstantU32(state, static_cast(pixel.quadSampleMask[0]) | (static_cast(pixel.quadSampleMask[1]) << 16u)); + const auto bottom = ConstantU32(state, static_cast(pixel.quadSampleMask[2]) | (static_cast(pixel.quadSampleMask[3]) << 16u)); + const auto lower = EmitCompareU32Constant(state, spv::OpIEqual, EmitBinaryU32(state, spv::OpBitwiseAnd, quad, ConstantU32(state, 2u)), 2u); + const auto row = Select(state, TypeU32(state), lower, bottom, top); + const auto shift = EmitBinaryU32(state, spv::OpShiftLeftLogical, EmitBinaryU32(state, spv::OpBitwiseAnd, quad, ConstantU32(state, 1u)), ConstantU32(state, 4u)); + const auto covered = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, row, shift), ConstantU32(state, 0xffffu)); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeI32(state)), pointer, state.sampleMaskInputVariable, ConstantU32(state, 0u)); + const auto rasterized = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeI32(state), rasterized, pointer); + auto guestRasterized = Unary(state, spv::OpBitcast, TypeU32(state), rasterized); + if (state.sampleOrderWord != 0u) guestRasterized = ReorderedSampleBits(state, guestRasterized, false); + bit = EmitBinaryU32(state, spv::OpBitwiseAnd, guestRasterized, covered); + state.quadSampleCoverage = Unary(state, spv::OpBitcast, TypeI32(state), covered); + } + const auto mask = static_cast(pixel.quadPixelMask); + const bool demoteCapability = std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityDemoteToHelperInvocation)) != state.supportedCapabilities.end(); + const bool demoteCore = state.spirvVersion >= 0x00010600u; + const bool demoteExtension = std::find(state.supportedExtensions.begin(), state.supportedExtensions.end(), "SPV_EXT_demote_to_helper_invocation") != state.supportedExtensions.end(); + const bool demote = demoteCapability && (demoteCore || demoteExtension); + if (!demote && state.requirements.neighbourLaneReads) { + throw std::runtime_error("the PA_SC_AA_MASK quad pixel mask " + std::to_string(mask) + " leaves helper pixels whose lanes the pixel program reads (implicit-LOD sampling, whole-quad mode or cross-lane reads), which needs the device's shaderDemoteToHelperInvocation"); + } + const auto uncovered = state.module.AllocateId(); + state.module.AddFunction(spv::OpIEqual, TypeBool(state), uncovered, bit, ConstantU32(state, 0u)); + state.quadPixelUncovered = uncovered; + const auto killLabel = state.module.AllocateId(); + const auto mergeLabel = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, mergeLabel, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, uncovered, killLabel, mergeLabel); + EmitLabel(state, killLabel); + if (demote) { + state.module.EmitCapability(spv::CapabilityDemoteToHelperInvocation); + if (!demoteCore) state.module.EmitExtension("SPV_EXT_demote_to_helper_invocation"); + state.module.AddFunction(spv::OpDemoteToHelperInvocation); + state.module.AddFunction(spv::OpBranch, mergeLabel); + } else { + state.module.AddFunction(spv::OpKill); + } + EmitLabel(state, mergeLabel); + if (state.quadSampleCoverage != 0u) StoreCoverage(state, ConstantI32(state, -1)); + } if (program.BlockOrder().empty()) { if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpEndInvocationInterlockEXT); @@ -1193,6 +1677,7 @@ void EmitProgram(SpirvEmitterState& state) { EmitControlFlow(ctx, functionState, program); } state.module.AddFunction(spv::OpFunctionEnd); + if (state.pixelGuestFunc != 0u) EmitPixelEntryPoint(state); if (state.program.Resources().stage == IrShaderStage::Mesh) { EmitMeshEntryPoint(state); } @@ -1345,7 +1830,7 @@ void EmitSetTessellationAttribute(SpirvValueEmitContext& ctx, const IrValue& ins const auto raised = state.module.AllocateId(); state.module.AddFunction(spv::OpExtInst, TypeF32(state), raised, GlslStd450(state), GLSLstd450FMax, floating, lowest); const auto clamped = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMin, raised, TessellationLevelBound(state, TessellationLevel(state, 1u), GLSLstd450FMin, state.inputInfo.vertex->tess.partitioning == 0u ? 64.0f : 63.0f)); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMin, raised, TessellationLevelBound(state, TessellationLevel(state, 1u), GLSLstd450FMin, state.inputInfo.vertex->tess.partitioning == 2u ? 63.0f : 64.0f)); value = state.module.AllocateId(); state.module.AddFunction(spv::OpSelect, TypeF32(state), value, inside, clamped, outer ? floating : lowest); } @@ -1421,7 +1906,8 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { state.module.AddFunction(spv::OpStore, state.depthVariable, f32); } if ((exp.en & 2u) != 0u && state.stencilRefVariable != 0u) { - const auto raw = ExportRawComponent(ctx, data, 1); + auto raw = ExportRawComponent(ctx, data, 1); + if (state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psStencilOperationValueExport) raw = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, raw, ConstantU32(state, 8u)), ConstantU32(state, 0xffu)); const auto value = state.module.AllocateId(); state.module.AddFunction(spv::OpBitcast, TypeI32(state), value, raw); state.module.AddFunction(spv::OpStore, state.stencilRefVariable, value); @@ -1429,10 +1915,8 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { if ((exp.en & 4u) != 0u && state.sampleMaskVariable != 0u) { const auto raw = ExportRawComponent(ctx, data, 2); const auto value = state.module.AllocateId(); - const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpBitcast, TypeI32(state), value, raw); - state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeI32(state)), pointer, state.sampleMaskVariable, ConstantU32(state, 0u)); - state.module.AddFunction(spv::OpStore, pointer, value); + StoreCoverage(state, value); } return; } @@ -1465,6 +1949,9 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { if (state.inputInfo.vertex->clipSpace.enabled) { value = ConvertPositionToClipSpace(state, value); } + if (state.depthRangeRemap && state.program.Resources().stage == IrShaderStage::Vertex) { + value = RemapPositionDepth(state, value); + } } if (exp.kind == ExportTargetKind::Position && state.zeroPositionClipIndex) { StoreZeroPositionClipDistance(state, value); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp index a1b5e245b2..4a319c4f65 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp @@ -2,7 +2,9 @@ #include #include #include +#include #include +#include #include #include @@ -10,6 +12,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; @@ -54,6 +58,49 @@ bool HasLimitedUseTypes(std::span spirv) { return (int8 && !int8Capability) || (int16 && !int16Capability) || (float16 && !float16Capability); } +std::vector CollapseBlockBallots(std::span spirv) { + if (spirv.size() < 5) throw std::runtime_error("SPIRV-Tools: incomplete module header"); + const bool swapped = spirv[0] != spv::MagicNumber; + const auto native = [swapped](std::uint32_t word) { + return swapped ? (word >> 24u) | ((word >> 8u) & 0x0000ff00u) | + ((word << 8u) & 0x00ff0000u) | (word << 24u) + : word; + }; + std::vector collapsed(spirv.begin(), spirv.begin() + 5); + collapsed.reserve(spirv.size()); + std::map, std::uint32_t> ballots; + for (std::size_t offset = 5; offset < spirv.size();) { + const auto instruction = native(spirv[offset]); + const auto wordCount = instruction >> spv::WordCountShift; + const auto opcode = instruction & spv::OpCodeMask; + if (wordCount == 0 || wordCount > spirv.size() - offset || + (opcode == spv::OpGroupNonUniformBallot && wordCount != 5)) { + throw std::runtime_error("SPIRV-Tools: malformed instruction while collapsing block ballots"); + } + switch (opcode) { + case spv::OpLabel: + case spv::OpFunction: + case spv::OpFunctionEnd: + case spv::OpFunctionCall: + case spv::OpDemoteToHelperInvocation: + ballots.clear(); + break; + case spv::OpGroupNonUniformBallot: { + const auto [found, inserted] = ballots.try_emplace({spirv[offset + 1], spirv[offset + 3], spirv[offset + 4]}, spirv[offset + 2]); + if (!inserted) { + collapsed.insert(collapsed.end(), {native((4u << spv::WordCountShift) | spv::OpCopyObject), spirv[offset + 1], spirv[offset + 2], found->second}); + offset += wordCount; + continue; + } + break; + } + } + collapsed.insert(collapsed.end(), spirv.begin() + offset, spirv.begin() + offset + wordCount); + offset += wordCount; + } + return collapsed; +} + } std::vector ValidateAndOptimizeSpirv(std::span spirv, std::uint32_t vulkanVersion, std::uint32_t spirvVersion, bool allowOffsetTextureOperand, bool optimize, bool specialize) { @@ -90,11 +137,11 @@ std::vector ValidateAndOptimizeSpirv(std::span(spirv.begin(), spirv.end()); + if (!optimize) return CollapseBlockBallots(spirv); spvtools::Optimizer optimizer(environment); optimizer.SetMessageConsumer(consumer); static const char* mode = std::getenv("APS5_SPIRV_OPT"); - if (!specialize && mode != nullptr && std::string(mode) == "none") return std::vector(spirv.begin(), spirv.end()); + if (mode != nullptr && std::string(mode) == "none") return CollapseBlockBallots(spirv); // The recompiler emits helpers (BDA lookup, fault reporting) as functions called from every memory // access. Exhaustive inlining multiplies module size by ~10x and optimization time by ~20x, so the // default pipeline keeps the performance passes that work per function and leaves inlining to the driver. @@ -116,7 +163,7 @@ std::vector ValidateAndOptimizeSpirv(std::span ValidateAndOptimizeSpirv(std::span ValidateAndOptimizeSpirv(std::span ValidateAndOptimizeSpirv(std::span #include +#include +#include #include #include @@ -46,6 +50,23 @@ std::uint32_t nonzero(SpirvEmitterState& state, std::uint32_t value) { return Binary(state, spv::OpINotEqual, TypeBool(state), value, ConstantU32(state, 0u)); } +std::uint32_t gcnWord3(SpirvEmitterState& state, std::uint32_t word1, std::uint32_t word3) { + const auto u32 = TypeU32(state); + const auto& formats = GcnBufferFormats(); + std::array words{}; + for (std::size_t index = 0; index < words.size(); ++index) words[index] = ConstantU32(state, formats[index]); + const auto arrayType = state.module.Type(spv::OpTypeArray, u32, ConstantU32(state, static_cast(words.size()))); + if (state.gcnBufferFormatVariable == 0) state.gcnBufferFormatVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPrivate, arrayType), spv::StorageClassPrivate); + state.module.AddFunction(spv::OpStore, state.gcnBufferFormatVariable, state.module.Constant(spv::OpConstantComposite, arrayType, std::span(words))); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassPrivate, u32), pointer, state.gcnBufferFormatVariable, field(state, word3, 12u, 7u)); + const auto format = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, u32, format, pointer); + const auto outOfBounds = Select(state, u32, nonzero(state, field(state, word1, 16u, 14u)), ConstantU32(state, 1u << 28u), ConstantU32(state, 3u << 28u)); + const auto kept = Binary(state, spv::OpBitwiseAnd, u32, word3, ConstantU32(state, 0xc0e00fffu)); + return Binary(state, spv::OpBitwiseOr, u32, Binary(state, spv::OpBitwiseOr, u32, kept, Binary(state, spv::OpShiftLeftLogical, u32, format, ConstantU32(state, 12u))), outOfBounds); +} + void faultIf(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t condition, std::uint32_t address, std::uint32_t bytes, BdaAbi::FaultReason reason) { auto& state = context.state; EmitIfCondition(state, condition, [&] { RecordBdaFault(state, address, ConstantU32(state, bytes), BdaInstructionPc(state, instruction), reason); }); @@ -59,14 +80,16 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst const auto& memory = context.Memory(instruction); const bool runtime = memory.gpuDescriptor || state.registrationModules; const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; - const auto word1 = runtime ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u); + auto word1 = runtime ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u); + if (runtime && GcnGuestShaders()) word1 = Binary(state, spv::OpBitwiseAnd, TypeU32(state), word1, ConstantU32(state, GcnBufferWord1Mask)); const bool runtimeFormat = memory.gpuDescriptor || (state.registrationModules && !state.program.Info().buffers.at(memory.resource).descriptorFormatted); - const auto word3 = runtimeFormat ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u); + auto word3 = runtimeFormat ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u); + if (runtimeFormat && GcnGuestShaders()) word3 = gcnWord3(state, word1, word3); const auto u32 = TypeU32(state); auto base = ConstantU32(state, 0u); if (memory.gpuDescriptor || BufferAccessOf(instruction.Opcode()) == BufferAccess::Atomic) { const auto u64 = TypeScalarU64(state); - const auto high = Binary(state, spv::OpShiftLeftLogical, u64, Unary(state, spv::OpUConvert, u64, field(state, context.Arg(*handle, 1u), 0u, 16u)), BdaConstant(state, 32u)); + const auto high = Binary(state, spv::OpShiftLeftLogical, u64, Unary(state, spv::OpUConvert, u64, field(state, GcnGuestShaders() ? Binary(state, spv::OpBitwiseAnd, u32, context.Arg(*handle, 1u), ConstantU32(state, GcnBufferWord1Mask)) : context.Arg(*handle, 1u), 0u, 16u)), BdaConstant(state, 32u)); base = Binary(state, spv::OpBitwiseOr, u64, Unary(state, spv::OpUConvert, u64, context.Arg(*handle, 0u)), high); } auto invalid = nonzero(state, field(state, word3, 30u, 2u)); @@ -89,7 +112,7 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst return {base, context.Arg(*handle, 2u), stride, word3, both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u))), index, valid}; } -RuntimeAddress address(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t componentOffset, std::uint32_t bytes) { +RuntimeAddress byteAddress(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t offset, std::uint32_t bytes) { auto& state = context.state; const auto u32 = TypeU32(state); const auto boolean = TypeBool(state); @@ -98,9 +121,8 @@ RuntimeAddress address(SpirvValueEmitContext& context, const IrValue& instructio const auto mul = [&](std::uint32_t left, std::uint32_t right) { return Binary(state, spv::OpIMul, u32, left, right); }; const auto less = [&](std::uint32_t left, std::uint32_t right) { return Binary(state, spv::OpULessThan, boolean, left, right); }; const auto fits = [&](std::uint32_t offset, std::uint32_t size) { - return both(state, Binary(state, spv::OpUGreaterThanEqual, boolean, size, constant(bytes)), Binary(state, spv::OpULessThanEqual, boolean, offset, Binary(state, spv::OpISub, u32, size, constant(bytes)))); + return both(state, Binary(state, spv::OpUGreaterThanEqual, boolean, size, bytes), Binary(state, spv::OpULessThanEqual, boolean, offset, Binary(state, spv::OpISub, u32, size, bytes))); }; - const auto offset = resource.offset != 0u ? add(resource.offset, constant(componentOffset)) : add(context.Arg(instruction, 2u), constant(context.Memory(instruction).offset + componentOffset)); const auto soffset = resource.soffset != 0u ? resource.soffset : context.Arg(instruction, 3u); const auto indexShift = add(field(state, resource.word3, 21u, 2u), constant(3u)); const auto indices = Binary(state, spv::OpShiftLeftLogical, u32, constant(1u), indexShift); @@ -119,8 +141,15 @@ RuntimeAddress address(SpirvValueEmitContext& context, const IrValue& instructio auto inBounds = Select(state, boolean, equal(state, mode, constant(0u)), structured, indexInBounds); inBounds = Select(state, boolean, less(mode, constant(2u)), inBounds, Select(state, boolean, equal(state, mode, constant(2u)), nonzero(state, resource.records), raw)); inBounds = both(state, resource.valid, both(state, nonzero(state, field(state, resource.word3, 12u, 7u)), inBounds)); - if (!context.Memory(instruction).gpuDescriptor) return {byte, inBounds}; - return {AddBdaAddress(context, instruction, resource.base, Unary(state, spv::OpUConvert, TypeScalarU64(state), byte), false), inBounds}; + return {byte, inBounds}; +} + +RuntimeAddress address(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t componentOffset, std::uint32_t bytes) { + auto& state = context.state; + const auto offset = Binary(state, spv::OpIAdd, TypeU32(state), resource.offset != 0u ? resource.offset : context.Arg(instruction, 2u), ConstantU32(state, resource.offset != 0u ? componentOffset : context.Memory(instruction).offset + componentOffset)); + const auto bytesAddress = byteAddress(context, instruction, resource, offset, ConstantU32(state, bytes)); + if (!context.Memory(instruction).gpuDescriptor) return bytesAddress; + return {AddBdaAddress(context, instruction, resource.base, Unary(state, spv::OpUConvert, TypeScalarU64(state), bytesAddress.guest), false), bytesAddress.inBounds}; } MemoryResourceAccess directAccess(SpirvValueEmitContext& context, const IrValue& instruction, bool wide = false, bool plainLoad = false) { @@ -153,7 +182,7 @@ std::uint32_t directWordIndex(SpirvEmitterState& state, const MemoryResourceAcce return EmitAddU32(state, relative, base); } -std::uint32_t readBuffer(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t address, std::uint32_t bits) { +std::uint32_t readBuffer(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t address, std::uint32_t bits, std::uint32_t enable = 0u) { if (context.Memory(instruction).gpuDescriptor) return EmitBdaRead(context, instruction, address, bits); auto& state = context.state; const auto access = directAccess(context, instruction, false, !context.Memory(instruction).coherent); @@ -162,26 +191,37 @@ std::uint32_t readBuffer(SpirvValueEmitContext& context, const IrValue& instruct const auto unaligned = nonzero(state, misalignment); const auto next = EmitAddU32(state, index, ConstantU32(state, 1u)); const auto last = Select(state, TypeU32(state), unaligned, next, index); - return EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, access, last), [&] { - const auto load = [&](std::uint32_t word) { - const auto value = state.module.AllocateId(); - const auto pointer = EmitMemoryElementPointer(state, access, word); - if (access.memoryAccess != 0u) state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer, access.memoryAccess); - else state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); - return value; - }; - const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), misalignment, ConstantU32(state, 3u)); - const auto low = Binary(state, spv::OpShiftRightLogical, TypeU32(state), load(index), shift); - const auto high = EmitValueOrZeroIfCondition(state, unaligned, [&] { - return Binary(state, spv::OpShiftLeftLogical, TypeU32(state), load(next), Binary(state, spv::OpISub, TypeU32(state), ConstantU32(state, 32u), shift)); - }); + const auto bounds = EmitMemoryElementInBounds(state, access, last); + const auto inBounds = enable != 0u ? both(state, enable, bounds) : bounds; + const auto load = [&](std::uint32_t word) { + const auto value = state.module.AllocateId(); + const auto pointer = EmitMemoryElementPointer(state, access, word); + if (access.memoryAccess != 0u) state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer, access.memoryAccess); + else state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + return value; + }; + const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), misalignment, ConstantU32(state, 3u)); + const auto combine = [&](std::uint32_t low, std::uint32_t high) { auto value = EmitOrU32(state, low, high); if (bits != 32u) { const auto subwordShift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), EmitAndConstant(state, address, 3u), ConstantU32(state, 3u)); value = Binary(state, spv::OpShiftRightLogical, TypeU32(state), value, subwordShift); } return EmitAndConstant(state, value, bits == 32u ? 0xffffffffu : (1u << bits) - 1u); - }); + }; + const auto highShift = Binary(state, spv::OpISub, TypeU32(state), ConstantU32(state, 32u), shift); + if (!LoadsSelectable(state, access)) { + return EmitValueOrZeroIfCondition(state, inBounds, [&] { + const auto low = Binary(state, spv::OpShiftRightLogical, TypeU32(state), load(index), shift); + const auto high = EmitValueOrZeroIfCondition(state, unaligned, [&] { return Binary(state, spv::OpShiftLeftLogical, TypeU32(state), load(next), highShift); }); + return combine(low, high); + }); + } + const auto lowIndex = GuardedElementIndex(state, access, inBounds, index); + const auto highIndex = Select(state, TypeU32(state), both(state, inBounds, unaligned), next, lowIndex); + const auto low = Binary(state, spv::OpShiftRightLogical, TypeU32(state), load(lowIndex), shift); + const auto high = EmitValueOrZero(state, unaligned, Binary(state, spv::OpShiftLeftLogical, TypeU32(state), load(highIndex), highShift)); + return EmitValueOrZero(state, inBounds, combine(low, high)); } std::uint32_t byteBuffers(SpirvEmitterState& state) { @@ -361,6 +401,131 @@ std::uint32_t formatted(SpirvValueEmitContext& context, const IrValue& instructi return composite(state, components, outputs); } +std::uint32_t formatTable(SpirvEmitterState& state, std::uint32_t formats) { + if (state.bufferFormatTable != 0u) return state.bufferFormatTable; + const auto u32 = TypeU32(state); + const auto arrayType = state.module.Type(spv::OpTypeArray, u32, ConstantU32(state, 3u * (formats + 1u))); + std::vector elements; + for (std::uint32_t index = 0; index <= formats; ++index) { + std::array entry{}; + if (index != 0u) { + const auto info = GetFormatInfo(static_cast(index)); + entry[0] = info.componentCount | (info.packedBitfield ? 8u : 0u) | (static_cast(info.type) << 4u); + for (std::uint32_t component = 0; component < info.componentCount; ++component) { + entry[0] |= info.componentBits[component] << (8u + component * 6u); + entry[1] |= GetFormatComponentByteOffset(info, component) << (component * 8u); + entry[2] |= info.componentBitOffset[component] << (component * 8u); + } + } + for (const auto word : entry) elements.push_back(ConstantU32(state, word)); + } + const auto table = state.module.Constant(spv::OpConstantComposite, arrayType, elements); + state.bufferFormatTable = state.module.DefineInitializedGlobalVariable(TypePointer(state, spv::StorageClassPrivate, arrayType), spv::StorageClassPrivate, table); + return state.bufferFormatTable; +} + +std::uint32_t genericFormattedLoad(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t components, std::uint32_t format, std::uint32_t formats) { + auto& state = context.state; + const auto& memory = context.Memory(instruction); + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + const auto constant = [&](std::uint32_t value) { return ConstantU32(state, value); }; + const auto table = formatTable(state, formats); + const auto entry = [&](std::uint32_t word) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassPrivate, u32), pointer, table, Binary(state, spv::OpIAdd, u32, Binary(state, spv::OpIMul, u32, format, constant(3u)), constant(word))); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, u32, value, pointer); + return value; + }; + const auto layout = entry(0u); + const auto byteOffsets = entry(1u); + const auto bitOffsets = entry(2u); + const auto componentCount = field(state, layout, 0u, 3u); + const auto packed = nonzero(state, field(state, layout, 3u, 1u)); + const auto type = field(state, layout, 4u, 4u); + const auto isType = [&](SpirvFormatComponentType value) { return equal(state, type, constant(static_cast(value))); }; + const auto signedType = Binary(state, spv::OpLogicalOr, boolean, isType(SpirvFormatComponentType::Sint), Binary(state, spv::OpLogicalOr, boolean, isType(SpirvFormatComponentType::Snorm), isType(SpirvFormatComponentType::Sscaled))); + const auto integer = Binary(state, spv::OpLogicalOr, boolean, isType(SpirvFormatComponentType::Uint), isType(SpirvFormatComponentType::Sint)); + std::array bits{}; + std::array active{}; + for (std::uint32_t component = 0; component < 4u; ++component) { + bits[component] = field(state, layout, 8u + component * 6u, 6u); + active[component] = Binary(state, spv::OpULessThan, boolean, constant(component), componentCount); + } + std::array selectors{}; + std::array selected{}; + for (std::uint32_t output = 0; output < components; ++output) { + selectors[output] = field(state, resource.word3, output * 3u, 3u); + selected[output] = Binary(state, spv::OpUMod, u32, Binary(state, spv::OpISub, u32, selectors[output], constant(4u)), componentCount); + } + std::array required{}; + std::array addresses{}; + auto inBounds = ConstantBool(state, true); + for (std::uint32_t component = 0; component < 4u; ++component) { + required[component] = ConstantBool(state, false); + for (std::uint32_t output = 0; output < components; ++output) { + const auto fromMemory = Binary(state, spv::OpUGreaterThanEqual, boolean, selectors[output], constant(4u)); + required[component] = Binary(state, spv::OpLogicalOr, boolean, required[component], both(state, fromMemory, equal(state, selected[output], constant(component)))); + } + required[component] = both(state, active[component], required[component]); + const auto componentOffset = field(state, byteOffsets, component * 8u, 8u); + const auto bytes = Select(state, u32, packed, constant(4u), Binary(state, spv::OpShiftRightLogical, u32, bits[component], constant(3u))); + const auto offset = Binary(state, spv::OpIAdd, u32, resource.offset != 0u ? resource.offset : Binary(state, spv::OpIAdd, u32, context.Arg(instruction, 2u), constant(memory.offset)), componentOffset); + addresses[component] = byteAddress(context, instruction, resource, offset, bytes); + if (memory.gpuDescriptor) { + const auto u64 = TypeScalarU64(state); + const auto guest = Binary(state, spv::OpIAdd, u64, resource.base, Unary(state, spv::OpUConvert, u64, addresses[component].guest)); + const auto overflow = both(state, active[component], Binary(state, spv::OpULessThan, boolean, guest, resource.base)); + EmitIfCondition(state, overflow, [&] { RecordBdaFault(state, resource.base, constant(0u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::Overflow); }); + StopBdaInvocationIf(state, overflow); + addresses[component].guest = guest; + } + inBounds = both(state, inBounds, Binary(state, spv::OpLogicalOr, boolean, Unary(state, spv::OpLogicalNot, boolean, required[component]), addresses[component].inBounds)); + } + std::array values{}; + for (std::uint32_t component = 0; component < 4u; ++component) { + values[component] = EmitValueOrZeroIfCondition(state, both(state, inBounds, required[component]), [&] { + const auto width = Select(state, u32, packed, constant(32u), bits[component]); + const auto dword = equal(state, width, constant(32u)); + const auto raw = EmitValueIfElse(state, dword, u32, [&] { return readBuffer(context, instruction, addresses[component].guest, 32u); }, [&] { + return EmitBdaReadBytes(context, instruction, addresses[component].guest, Binary(state, spv::OpShiftRightLogical, u32, width, constant(3u))); + }); + const auto bitOffset = Select(state, u32, packed, field(state, bitOffsets, component * 8u, 8u), constant(0u)); + const auto unsignedField = EmitBitFieldUExtract(state, raw, bitOffset, bits[component]); + const auto signedField = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitFieldSExtract, TypeI32(state), signedField, Unary(state, spv::OpBitcast, TypeI32(state), raw), bitOffset, bits[component]); + const auto extracted = Select(state, u32, equal(state, bits[component], constant(32u)), raw, Select(state, u32, signedType, Unary(state, spv::OpBitcast, u32, signedField), unsignedField)); + std::vector> conversions; + for (std::uint32_t index = 1; index <= formats; ++index) { + const auto info = GetFormatInfo(static_cast(index)); + if (component >= info.componentCount) continue; + const std::pair key{info.type, info.componentBits[component]}; + if (std::find(conversions.begin(), conversions.end(), key) == conversions.end()) conversions.push_back(key); + } + auto value = constant(0u); + for (const auto& [conversionType, conversionBits] : conversions) { + SpirvBufferFormatInfo info{}; + info.type = conversionType; + info.componentCount = 1u; + info.componentBits[0] = conversionBits; + const auto converted = memory.d16 ? EmitD16FormatComponent(state, info, 0u, extracted) : NormalizeFormatComponent(state, info, 0u, extracted); + value = Select(state, u32, both(state, isType(conversionType), equal(state, bits[component], constant(conversionBits))), converted, value); + } + return value; + }); + } + const auto one = Select(state, u32, integer, constant(1u), constant(memory.d16 ? 0x3c00u : 0x3f800000u)); + std::array outputs{}; + for (std::uint32_t output = 0; output < components; ++output) { + auto value = values[0]; + for (std::uint32_t component = 1; component < 4u; ++component) value = Select(state, u32, equal(state, selected[output], constant(component)), values[component], value); + value = Select(state, u32, equal(state, selectors[output], constant(1u)), one, value); + outputs[output] = Select(state, u32, equal(state, selectors[output], constant(0u)), constant(0u), value); + } + return composite(state, components, outputs); +} + std::uint32_t formatSwitch(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t components, bool store, std::uint32_t data); std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t components, bool store, std::uint32_t data); std::uint32_t formattedGpuCall(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t components, bool store, std::uint32_t data); @@ -383,7 +548,8 @@ std::uint32_t formattedDispatch(SpirvValueEmitContext& context, const IrValue& i } const auto* word = instruction.Argument(0)->Resolve()->Argument(3)->Resolve(); if (memory.gpuDescriptor && word->HasImmediate()) { - const auto format = static_cast((word->ImmediateU32() >> 12u) & 0x7fu); + const auto bits = (word->ImmediateU32() >> 12u) & 0x7fu; + const auto format = static_cast(GcnGuestShaders() ? GcnBufferFormats()[bits] : bits); if (format == IrBufferFormat::Invalid) context.Fail(instruction, "formatted buffer instruction has an invalid constant format"); return formatted(context, instruction, resource, GetFormatInfo(format), components, store, data); } @@ -395,9 +561,20 @@ std::uint32_t formatSwitch(SpirvValueEmitContext& context, const IrValue& instru auto& state = context.state; const auto& memory = context.Memory(instruction); const auto format = field(state, resource.word3, 12u, 7u); + constexpr std::uint32_t count = static_cast(IrBufferFormat::Format32_32_32_32Float); + if (!store && memory.gpuDescriptor) { + const auto type = components == 1u ? TypeU32(state) : TypeU32Composite(state, components); + const auto zero = components == 1u ? ConstantU32(state, 0u) : ConstantU32CompositeZero(state, components); + const auto unsupported = Binary(state, spv::OpLogicalOr, TypeBool(state), equal(state, format, ConstantU32(state, 0u)), Binary(state, spv::OpUGreaterThan, TypeBool(state), format, ConstantU32(state, count))); + const auto result = EmitValueIfElse(state, unsupported, type, [&] { + RecordBdaFault(state, resource.base, ConstantU32(state, 0u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::InvalidDescriptor); + return zero; + }, [&] { return genericFormattedLoad(context, instruction, resource, components, format, count); }); + StopBdaInvocationIf(state, unsupported); + return result; + } const auto merge = state.module.AllocateId(); const auto invalid = state.module.AllocateId(); - constexpr std::uint32_t count = static_cast(IrBufferFormat::Format32_32_32_32Float); std::array labels{}; std::vector branch{spv::OpSwitch, format, invalid}; for (std::uint32_t index = 0; index < count; ++index) { @@ -569,6 +746,27 @@ std::uint32_t formattedGpuCall(SpirvValueEmitContext& context, const IrValue& in return value; } +struct CoherentPair { + MemoryResourceAccess wide; + std::uint32_t index; + std::uint32_t usable; +}; + +std::optional coherentPair(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t components) { + auto& state = context.state; + const auto& memory = context.Memory(instruction); + if (components != 2u || !memory.coherent || memory.gpuDescriptor || memory.dataBits != 32u || state.storageBufferU64Variable == 0u) return std::nullopt; + const auto low = address(context, instruction, resource, 0u, 4u); + const auto high = address(context, instruction, resource, 4u, 4u); + const auto wide = directAccess(context, instruction, true); + const auto byte = EmitAddU32(state, low.guest, wide.byteOffset); + const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byte, ConstantU32(state, 3u)); + const auto aligned = both(state, equal(state, baseMisalignment(context, instruction), ConstantU32(state, 0u)), equal(state, EmitAndConstant(state, byte, 7u), ConstantU32(state, 0u))); + const auto adjacent = equal(state, high.guest, EmitAddU32(state, low.guest, ConstantU32(state, 4u))); + const auto inBounds = both(state, both(state, low.inBounds, high.inBounds), EmitMemoryElementInBounds(state, wide, index)); + return CoherentPair{wide, index, both(state, both(state, aligned, adjacent), inBounds)}; +} + } std::uint32_t EmitRuntimeBufferLoad(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t components) { @@ -583,19 +781,15 @@ std::uint32_t EmitRuntimeBufferLoad(SpirvValueEmitContext& context, const IrValu std::array 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); }; - if (components != 2u || !memory.coherent || memory.gpuDescriptor || state.storageBufferU64Variable == 0u) return readComponents(); - const auto runtime = address(context, instruction, resource, 0u, 8u); - const auto wide = directAccess(context, instruction, true); - const auto byte = EmitAddU32(state, runtime.guest, wide.byteOffset); - const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byte, ConstantU32(state, 3u)); - const auto aligned = both(state, equal(state, baseMisalignment(context, instruction), ConstantU32(state, 0u)), equal(state, EmitAndConstant(state, byte, 7u), ConstantU32(state, 0u))); - const auto inBounds = both(state, runtime.inBounds, EmitMemoryElementInBounds(state, wide, index)); - return EmitValueIfElse(state, both(state, aligned, inBounds), type, [&] { - const auto pointer = EmitStorageBufferElementPointer(state, wide, index, TypeStorageBufferU64ElementPointer(state)); + const auto pair = coherentPair(context, instruction, resource, components); + if (!pair) return readComponents(); + return EmitValueIfElse(state, pair->usable, type, [&] { + const auto pointer = EmitStorageBufferElementPointer(state, pair->wide, pair->index, TypeStorageBufferU64ElementPointer(state)); const auto value = state.module.AllocateId(); state.module.AddFunction(spv::OpAtomicLoad, TypeScalarU64(state), value, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone)); const auto high = Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), value, BdaConstant(state, 32u)); @@ -614,10 +808,25 @@ void EmitRuntimeBufferStore(SpirvValueEmitContext& context, const IrValue& instr return; } const auto values = storeValues(state, storeData(context, instruction), components); - for (std::uint32_t component = 0; component < components; ++component) { - const auto access = address(context, instruction, resource, component * 4u, memory.dataBits / 8u); - EmitIfCondition(state, access.inBounds, [&] { writeBuffer(context, instruction, access.guest, values[component], memory.dataBits); }); + const auto writeComponents = [&] { + for (std::uint32_t component = 0; component < components; ++component) { + const auto access = address(context, instruction, resource, component * 4u, memory.dataBits / 8u); + EmitIfCondition(state, access.inBounds, [&] { writeBuffer(context, instruction, access.guest, values[component], memory.dataBits); }); + } + }; + const auto pair = coherentPair(context, instruction, resource, components); + if (!pair) { + writeComponents(); + return; } + EmitIfCondition(state, pair->usable, [&] { + const auto u64 = TypeScalarU64(state); + const auto high = Binary(state, spv::OpShiftLeftLogical, u64, Unary(state, spv::OpUConvert, u64, values[1]), BdaConstant(state, 32u)); + const auto value = Binary(state, spv::OpBitwiseOr, u64, Unary(state, spv::OpUConvert, u64, values[0]), high); + const auto pointer = EmitStorageBufferElementPointer(state, pair->wide, pair->index, TypeStorageBufferU64ElementPointer(state)); + state.module.AddFunction(spv::OpAtomicStore, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone), value); + }); + EmitIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), pair->usable), writeComponents); }); } @@ -633,7 +842,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); }); } @@ -656,33 +865,39 @@ std::uint32_t EmitRuntimeBufferAtomic(SpirvValueEmitContext& context, const IrVa if (!context.Memory(instruction).gpuDescriptor) { const auto storage = directAccess(context, instruction, bits == 64u); const auto byte = EmitAddU32(state, access.guest, storage.byteOffset); - const auto unaligned = nonzero(state, EmitAndConstant(state, byte, bits / 8u - 1u)); - const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), resource.base, Unary(state, spv::OpUConvert, TypeScalarU64(state), access.guest)); - EmitIfCondition(state, unaligned, [&] { - const auto high = Unary(state, spv::OpUConvert, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), guest, BdaConstant(state, 32u))); - RecordBdaFaultWords(state, Unary(state, spv::OpUConvert, TypeU32(state), guest), high, ConstantU32(state, bits / 8u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::Unaligned); - }); - StopBdaInvocationIf(state, unaligned); const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byte, ConstantU32(state, bits == 64u ? 3u : 2u)); - return EmitValueOrDefaultIfCondition(state, both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), EmitMemoryElementInBounds(state, storage, index)), type, zero, [&] { + auto valid = EmitMemoryElementInBounds(state, storage, index); + if (bits == 64u) { + const auto unaligned = nonzero(state, EmitAndConstant(state, byte, bits / 8u - 1u)); + const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), resource.base, Unary(state, spv::OpUConvert, TypeScalarU64(state), access.guest)); + EmitIfCondition(state, unaligned, [&] { + const auto high = Unary(state, spv::OpUConvert, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), guest, BdaConstant(state, 32u))); + RecordBdaFaultWords(state, Unary(state, spv::OpUConvert, TypeU32(state), guest), high, ConstantU32(state, bits / 8u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::Unaligned); + }); + StopBdaInvocationIf(state, unaligned); + valid = both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), valid); + } + return EmitValueOrDefaultIfCondition(state, valid, type, zero, [&] { const auto pointer = EmitStorageBufferElementPointer(state, storage, index, bits == 64u ? TypeStorageBufferU64ElementPointer(state) : TypeStorageBufferElementPointer(state)); return operation(pointer); }); } - const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), access.guest, BdaConstant(state, bits / 8u - 1u)), BdaConstant(state, 0u)); - faultIf(context, instruction, unaligned, access.guest, bits / 8u, BdaAbi::FaultReason::Unaligned); - return EmitValueOrDefaultIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), type, zero, [&] { + const auto atomicAt = [&](std::uint32_t guest) { const auto physical = state.module.AllocateId(); - state.module.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), physical, state.bdaAtomicPointerFunction, access.guest, ConstantU32(state, bits / 8u), BdaInstructionPc(state, instruction)); + state.module.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), physical, state.bdaAtomicPointerFunction, guest, ConstantU32(state, bits / 8u), BdaInstructionPc(state, instruction)); const auto mapped = Binary(state, spv::OpINotEqual, TypeBool(state), physical, BdaConstant(state, 0u)); return EmitValueOrDefaultIfCondition(state, mapped, type, zero, [&] { const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpConvertUToPtr, TypePointer(state, spv::StorageClassPhysicalStorageBuffer, scalar), pointer, physical); const auto result = operation(pointer); - state.module.AddFunction(spv::OpFunctionCall, state.module.Type(spv::OpTypeVoid), state.module.AllocateId(), state.bdaNoteWriteFunction, access.guest); + state.module.AddFunction(spv::OpFunctionCall, state.module.Type(spv::OpTypeVoid), state.module.AllocateId(), state.bdaNoteWriteFunction, guest); return result; }); - }); + }; + if (bits == 32u) return atomicAt(Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), access.guest, BdaConstant(state, ~std::uint64_t{3u}))); + const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), access.guest, BdaConstant(state, bits / 8u - 1u)), BdaConstant(state, 0u)); + faultIf(context, instruction, unaligned, access.guest, bits / 8u, BdaAbi::FaultReason::Unaligned); + return EmitValueOrDefaultIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), type, zero, [&] { return atomicAt(access.guest); }); }); }); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index 441a136b15..d77a063d72 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -5,15 +5,67 @@ #include #include #include +#include #include #include #include +#include #include namespace ShaderRecompiler { namespace { -using Instruction = std::vector; +class WordArena { +public: + explicit WordArena(std::size_t words) : next(std::max(words, 1024u)) {} + + std::uint32_t* Allocate(std::size_t count) { + if (count > available) { + const auto size = std::max(count, next); + chunks.push_back(std::make_unique(size)); + cursor = chunks.back().get(); + available = size; + next = size * 2u; + } + auto* words = cursor; + cursor += count; + available -= count; + return words; + } + +private: + std::vector> chunks; + std::uint32_t* cursor = nullptr; + std::size_t available = 0; + std::size_t next; +}; + +template +struct ArenaAllocator { + using value_type = T; + + ArenaAllocator() = default; + explicit ArenaAllocator(WordArena* arena) : arena(arena) {} + template + ArenaAllocator(const ArenaAllocator& other) : arena(other.arena) {} + + T* allocate(std::size_t count) { + static_assert(std::is_same_v); + return arena != nullptr ? arena->Allocate(count) : std::allocator{}.allocate(count); + } + + void deallocate(T* words, std::size_t count) { + if (arena == nullptr) std::allocator{}.deallocate(words, count); + } + + friend bool operator==(const ArenaAllocator& left, const ArenaAllocator& right) { + return left.arena == right.arena; + } + + WordArena* arena = nullptr; +}; + +using Instruction = std::vector>; spv::Op Opcode(const Instruction& instruction) { return instruction.empty() ? spv::OpNop : static_cast(instruction[0] & 0xffffu); @@ -518,7 +570,7 @@ private: class Specialization { public: - explicit Specialization(std::span words) { + explicit Specialization(std::span words) : arena(words.size()) { if (words.size() < 5u || words[0] != spv::MagicNumber) throw std::runtime_error("invalid prepared SPIR-V header"); header.assign(words.begin(), words.begin() + 5); bound = header[3]; @@ -527,10 +579,11 @@ public: resultTypes.Reset(bound); vectorTypes.Reset(bound); removed.Reset(bound); + aliases.Reset(bound); for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; if (count == 0u || count > words.size() - cursor) throw std::runtime_error("truncated prepared SPIR-V instruction"); - instructions.emplace_back(words.begin() + cursor, words.begin() + cursor + count); + instructions.emplace_back(words.begin() + cursor, words.begin() + cursor + count, ArenaAllocator(&arena)); const auto& instruction = instructions.back(); const auto op = Opcode(instruction); bool hasResult = false; @@ -542,6 +595,7 @@ public: if (op == spv::OpTypeVector) vectorTypes.insert(instruction.at(1)); if (op == spv::OpConstant && types.contains(instruction.at(1)) && types.at(instruction[1]).width <= 32u) values.emplace(instruction.at(2), instruction.at(3)); if (op == spv::OpConstantTrue || op == spv::OpConstantFalse) values.emplace(instruction.at(2), op == spv::OpConstantTrue ? 1u : 0u); + if ((op == spv::OpConstant || op == spv::OpConstantTrue || op == spv::OpConstantFalse) && values.contains(instruction.at(2))) canonical.emplace(std::pair{instruction.at(1), values.at(instruction[2])}, instruction[2]); cursor += count; } } @@ -553,6 +607,7 @@ public: changed |= prune(); changed |= propagateCopies(); } + shareConstants(); removeDeadComputations(); removeUnreachableBlocks(); removeUnusedConstants(); @@ -577,6 +632,11 @@ private: return found != nullptr ? std::optional(*found) : std::nullopt; } + bool sameType(std::uint32_t id, std::uint32_t type) const { + const auto* found = resultTypes.find(id); + return found != nullptr && *found == type; + } + std::optional evaluate(const Instruction& instruction) const { const auto op = Opcode(instruction); if (instruction.size() < 4u) return std::nullopt; @@ -628,6 +688,8 @@ private: bool function = false; IdTable> extracts; extracts.Reset(bound); + IdTable bitcasts; + bitcasts.Reset(bound); for (auto& instruction : instructions) { if (instruction.empty()) continue; auto op = Opcode(instruction); @@ -646,6 +708,16 @@ private: changed = true; } } + if (op == spv::OpBitcast && instruction.size() == 4u) { + const auto* chained = bitcasts.find(instruction[3]); + const auto source = chained != nullptr && sameType(*chained, instruction[1]) ? *chained : instruction[3]; + if (sameType(source, instruction[1])) { + instruction = Make(spv::OpCopyObject, {instruction[1], instruction[2], source}); + changed = true; + } else { + bitcasts.emplace(instruction[2], instruction[3]); + } + } op = Opcode(instruction); if (op == spv::OpCompositeExtract && instruction.size() == 5u) extracts.emplace(instruction[2], std::pair{instruction[3], instruction[4]}); if (op == spv::OpCompositeConstruct && instruction.size() == 7u) { @@ -669,11 +741,14 @@ private: changed = true; } } + if (op == spv::OpCopyObject && values.contains(instruction.at(2))) continue; const auto result = evaluate(instruction); if (!result) continue; const auto type = types.at(instruction[1]); const auto bits = type.width == 32u ? *result : *result & ((1u << type.width) - 1u); values[instruction[2]] = bits; + const auto [existing, created] = canonical.emplace(std::pair{instruction[1], bits}, instruction[2]); + if (!created) aliases.emplace(instruction[2], existing->second); constants.push_back(type.boolean ? Make(bits != 0u ? spv::OpConstantTrue : spv::OpConstantFalse, {instruction[1], instruction[2]}) : Make(spv::OpConstant, {instruction[1], instruction[2], bits})); instruction.clear(); changed = true; @@ -899,6 +974,22 @@ private: instructions = std::move(ordered); } + void shareConstants() { + if (aliases.empty()) return; + bool function = false; + for (auto& instruction : instructions) { + if (instruction.empty()) continue; + const auto op = Opcode(instruction); + if (op == spv::OpFunction) function = true; + if (op == spv::OpFunctionEnd) function = false; + if (!function) continue; + VisitInputs(instruction, [&](std::size_t index) { + const auto* found = aliases.find(instruction[index]); + if (found != nullptr) instruction[index] = *found; + }); + } + } + bool propagateCopies() { IdTable copies; copies.Reset(bound); @@ -991,6 +1082,7 @@ private: } std::vector header; + WordArena arena; std::vector instructions; std::vector constants; std::uint32_t bound = 0; @@ -999,6 +1091,8 @@ private: IdTable resultTypes; IdSet vectorTypes; IdSet removed; + IdTable aliases; + std::map, std::uint32_t> canonical; }; } diff --git a/core/shader/recompiler/StencilReplay.cpp b/core/shader/recompiler/StencilReplay.cpp new file mode 100644 index 0000000000..fae2057358 --- /dev/null +++ b/core/shader/recompiler/StencilReplay.cpp @@ -0,0 +1,752 @@ +#include "StencilReplay.hpp" +#include "Optimization/MaskedSelectEliminator.hpp" +#include "Optimization/SrtWalker/SrtInstructionPredicates.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ShaderRecompiler { + +namespace { + +bool Boolean(const IrValue* value, bool expected) { + if (value == nullptr) return expected; + value = value->Resolve(); + return value->Type() == IrType::U1 && value->HasImmediate() && value->ImmediateBool() == expected; +} + +bool Implies(const IrValue* left, const IrValue* right, unsigned depth = 0) { + if (depth > 32u) return false; + if (left) left = left->Resolve(); + if (right) right = right->Resolve(); + if (left == right || Boolean(left, false) || Boolean(right, true)) return true; + if (!left || !right) return false; + if (left->Opcode() == IrOpcode::LogicalAnd) return Implies(left->Argument(0), right, depth + 1u) || Implies(left->Argument(1), right, depth + 1u); + if (right->Opcode() == IrOpcode::LogicalAnd) return Implies(left, right->Argument(0), depth + 1u) && Implies(left, right->Argument(1), depth + 1u); + if (left->Opcode() == IrOpcode::LogicalOr) return Implies(left->Argument(0), right, depth + 1u) && Implies(left->Argument(1), right, depth + 1u); + if (right->Opcode() == IrOpcode::LogicalOr) return Implies(left, right->Argument(0), depth + 1u) || Implies(left, right->Argument(1), depth + 1u); + return false; +} + +struct ReplayMask { + const IrValue* low = nullptr; + const IrValue* high = nullptr; +}; + +bool Immediate(const IrValue* value, std::uint32_t expected) { + value = value->Resolve(); + return value->HasImmediate() && value->Type() == IrType::U32 && value->ImmediateU32() == expected; +} + +bool FaultControlFlowAcyclic(const IrProgram& program) { + const auto& blocks = program.BlockOrder(); + const auto& metadata = program.Metadata().blockInfo; + if (blocks.empty() || blocks.size() != metadata.size() || blocks.size() > 4096u) return false; + std::map indices; + std::set owned; + for (std::size_t index = 0; index < blocks.size(); ++index) if (!blocks[index] || !owned.insert(blocks[index]).second || !indices.emplace(metadata[index].id, index).second) return false; + std::vector> successors(blocks.size()); + for (std::size_t index = 0; index < blocks.size(); ++index) { + const auto& term = metadata[index].terminator; + if (term.loopHeader) return false; + const auto edge = [&](std::uint32_t id) { + const auto found = indices.find(id); + if (found == indices.end()) return false; + successors[index].push_back(found->second); + return true; + }; + if (term.kind == TerminatorKind::Branch) { if (!edge(term.trueBlock)) return false; } + else if (term.kind == TerminatorKind::ConditionalBranch) { if (!metadata[index].condition || !edge(term.trueBlock) || !edge(term.falseBlock)) return false; } + else if (term.kind != TerminatorKind::Return) return false; + std::set expected; + for (const auto next : successors[index]) expected.insert(blocks[next]); + const std::set actual(blocks[index]->Successors().begin(), blocks[index]->Successors().end()); + if (actual != expected) return false; + } + std::vector colors(blocks.size()); + std::vector> pending{{0u, 0u}}; + colors[0u] = 1u; + while (!pending.empty()) { + auto& [block, edge] = pending.back(); + if (edge == successors[block].size()) { colors[block] = 2u; pending.pop_back(); continue; } + const auto next = successors[block][edge++]; + if (colors[next] == 1u) return false; + if (colors[next] == 2u) continue; + colors[next] = 1u; + pending.emplace_back(next, 0u); + } + return true; +} + +const IrValue* Other(const IrValue* value, std::uint32_t immediate) { + if (value->ArgumentCount() != 2u) return nullptr; + const IrValue* left = value->Argument(0)->Resolve(); + const IrValue* right = value->Argument(1)->Resolve(); + if (Immediate(left, immediate)) return right; + if (Immediate(right, immediate)) return left; + return nullptr; +} + +class MaskReplaySafety { +public: + explicit MaskReplaySafety(const IrProgram& program) : program(program), waveSize(program.WaveSize()) {} + + bool Check() { + if (program.Resources().stage != IrShaderStage::Pixel || (waveSize != 32u && waveSize != 64u) || !paths()) return false; + for (const auto* block : program.BlockOrder()) { + for (const auto* value : block->Instructions()) { + if (const auto mask = own(value)) { + if (!maskStable(*mask, 0u)) return false; + ownMasks.emplace(value, *mask); + } + } + } + for (const auto& info : program.Metadata().blockInfo) { + if (info.terminator.kind != TerminatorKind::ConditionalBranch) continue; + if (!control(info)) return false; + } + for (const auto& [value, mask] : ownMasks) if (helperObserved(value)) return false; + for (const auto* block : program.BlockOrder()) { + for (const auto* value : block->Instructions()) { + if ((value->Opcode() == IrOpcode::LaneId || value->Opcode() == IrOpcode::Ballot || value->Opcode() == IrOpcode::WqmU64) && !restricted.contains(value)) return false; + } + } + for (const auto* value : restricted) { + for (const auto& use : value->OperandUses()) { + if (restricted.contains(use.user) || ownMasks.contains(use.user) || use.user->Opcode() == IrOpcode::Reference || use.user->Opcode() == IrOpcode::ReferenceU32) continue; + return false; + } + } + return true; + } + +private: + enum class HelperMask : unsigned { Visiting, RealOnly, QuadStable, Uncertain }; + + void restrict(const IrValue* value) { + if (value && !value->HasImmediate()) restricted.insert(value); + } + + std::optional own(const IrValue* value) { + value = value->Resolve(); + if (value->Opcode() != IrOpcode::INotEqual32) return std::nullopt; + const IrValue* bit = Other(value, 0u); + if (!bit || bit->Opcode() != IrOpcode::BitwiseAnd32) return std::nullopt; + const IrValue* shifted = Other(bit, 1u); + if (!shifted || shifted->Opcode() != IrOpcode::ShiftRightLogical32 || shifted->ArgumentCount() != 2u) return std::nullopt; + const IrValue* amount = shifted->Argument(1)->Resolve(); + if (amount->Opcode() != IrOpcode::BitwiseAnd32) return std::nullopt; + const IrValue* lane = Other(amount, 31u); + if (!lane || lane->Opcode() != IrOpcode::LaneId) return std::nullopt; + const IrValue* word = shifted->Argument(0)->Resolve(); + ReplayMask result{word, word}; + if (word->Opcode() == IrOpcode::SelectU32 && word->ArgumentCount() == 3u) { + const IrValue* half = word->Argument(0)->Resolve(); + if (half->Opcode() != IrOpcode::ULessThan32 || half->ArgumentCount() != 2u || half->Argument(0)->Resolve()->Opcode() != IrOpcode::LaneId || !Immediate(half->Argument(1), 32u)) return std::nullopt; + restrict(half); + restrict(half->Argument(0)->Resolve()); + restrict(word); + result = ReplayMask{word->Argument(1)->Resolve(), word->Argument(2)->Resolve()}; + } else if (waveSize != 32u && !word->HasImmediate()) return std::nullopt; + restrict(bit); + restrict(shifted); + restrict(amount); + restrict(lane); + return result; + } + + const IrValue* ballotWord(const IrValue* word, std::uint32_t half) const { + if (word->Opcode() != IrOpcode::CompositeExtractU32x4 || word->ArgumentCount() != 2u || !Immediate(word->Argument(1), half)) return nullptr; + const IrValue* ballot = word->Argument(0)->Resolve(); + return ballot->Opcode() == IrOpcode::Ballot && ballot->ArgumentCount() == 1u ? ballot : nullptr; + } + + bool maskStable(ReplayMask mask, unsigned depth) { + if (depth > 128u) return false; + mask = ReplayMask{mask.low->Resolve(), mask.high->Resolve()}; + const auto key = std::pair{mask.low->Id(), waveSize == 32u ? mask.low->Id() : mask.high->Id()}; + if (const auto found = masks.find(key); found != masks.end()) return found->second == 2u; + masks[key] = 1u; + bool valid = false; + const auto finish = [&](bool accepted) { + masks[key] = accepted ? 2u : 3u; + if (accepted) { restrict(mask.low); if (waveSize != 32u) restrict(mask.high); } + return accepted; + }; + if ((Immediate(mask.low, 0u) && (waveSize == 32u || Immediate(mask.high, 0u))) || (Immediate(mask.low, 0xffffffffu) && (waveSize == 32u || Immediate(mask.high, 0xffffffffu)))) return finish(true); + const IrValue* lowBallot = ballotWord(mask.low, 0u); + const IrValue* highBallot = waveSize == 32u ? lowBallot : ballotWord(mask.high, 1u); + if (lowBallot && highBallot) { + valid = stable(lowBallot->Argument(0), depth + 1u) && stable(highBallot->Argument(0), depth + 1u); + if (valid) { restrict(lowBallot); restrict(highBallot); } + } else if (mask.low->Opcode() == IrOpcode::BitwiseAnd32 && (waveSize == 32u || mask.high->Opcode() == IrOpcode::BitwiseAnd32)) { + const auto pair = [&](unsigned low, unsigned high) { return ReplayMask{mask.low->Argument(low)->Resolve(), waveSize == 32u ? mask.low->Argument(low)->Resolve() : mask.high->Argument(high)->Resolve()}; }; + valid = (maskStable(pair(0u, 0u), depth + 1u) && maskStable(pair(1u, 1u), depth + 1u)) || (waveSize != 32u && maskStable(pair(0u, 1u), depth + 1u) && maskStable(pair(1u, 0u), depth + 1u)); + } else if (mask.low->Opcode() == IrOpcode::CompositeExtractU64 && mask.low->ArgumentCount() == 2u && Immediate(mask.low->Argument(1), 0u)) { + const IrValue* wqm = mask.low->Argument(0)->Resolve(); + const bool paired = waveSize == 32u || (mask.high->Opcode() == IrOpcode::CompositeExtractU64 && mask.high->ArgumentCount() == 2u && mask.high->Argument(0)->Resolve() == wqm && Immediate(mask.high->Argument(1), 1u)); + if (paired && wqm->Opcode() == IrOpcode::WqmU64 && wqm->ArgumentCount() == 1u) { + const IrValue* source = wqm->Argument(0)->Resolve(); + valid = source->Opcode() == IrOpcode::CompositeConstructU64 && source->ArgumentCount() == 2u && maskStable(ReplayMask{source->Argument(0)->Resolve(), source->Argument(1)->Resolve()}, depth + 1u); + if (valid) { + const auto provenance = helpers(ReplayMask{source->Argument(0)->Resolve(), source->Argument(1)->Resolve()}, depth + 1u); + valid = provenance == HelperMask::RealOnly || provenance == HelperMask::QuadStable; + } + if (valid) { restrict(wqm); restrict(source); } + } + } else if (mask.low->IsPhi() && mask.low->ArgumentCount() != 0u && mask.low->ArgumentCount() == mask.low->PhiBlockCount() && (waveSize == 32u || (mask.high->IsPhi() && mask.high->Parent() == mask.low->Parent() && mask.high->ArgumentCount() == mask.high->PhiBlockCount()))) { + valid = true; + for (std::size_t index = 0; index < mask.low->PhiBlockCount(); ++index) { + const IrValue* high = mask.low->Argument(index)->Resolve(); + if (waveSize != 32u) { + high = nullptr; + for (std::size_t highIndex = 0; highIndex < mask.high->PhiBlockCount(); ++highIndex) if (mask.high->PhiBlock(highIndex) == mask.low->PhiBlock(index)) high = mask.high->Argument(highIndex)->Resolve(); + } + if (!high || !maskStable(ReplayMask{mask.low->Argument(index)->Resolve(), high}, depth + 1u)) { valid = false; break; } + } + } + return finish(valid); + } + + std::optional onHelper(const IrValue* value, unsigned depth) { + if (depth > 128u) return std::nullopt; + value = value->Resolve(); + if (value->HasImmediate()) { + if (value->Type() == IrType::Bool) return value->ImmediateBool() ? 1u : 0u; + if (value->Type() == IrType::U32) return value->ImmediateU32(); + return std::nullopt; + } + if (const auto found = helperValues.find(value); found != helperValues.end()) return found->second; + helperValues.emplace(value, std::nullopt); + std::optional result; + const auto argument = [&](unsigned index) { return onHelper(value->Argument(index), depth + 1u); }; + if (value->Opcode() == IrOpcode::GetBuiltin && value->ArgumentCount() == 2u && Immediate(value->Argument(0), static_cast(StageInputKind::HelperInvocation)) && Immediate(value->Argument(1), 0u)) result = 1u; + else if (const auto mask = own(value)) { + if (helpers(*mask, depth + 1u) == HelperMask::RealOnly) result = 0u; + } else if (value->Opcode() == IrOpcode::LogicalNot && value->ArgumentCount() == 1u) { + if (const auto source = argument(0u)) result = *source == 0u ? 1u : 0u; + } else if (value->Opcode() == IrOpcode::LogicalAnd && value->ArgumentCount() == 2u) { + const auto left = argument(0u), right = argument(1u); + if ((left && *left == 0u) || (right && *right == 0u)) result = 0u; + else if (left && right) result = 1u; + } else if (value->Opcode() == IrOpcode::LogicalOr && value->ArgumentCount() == 2u) { + const auto left = argument(0u), right = argument(1u); + if ((left && *left != 0u) || (right && *right != 0u)) result = 1u; + else if (left && right) result = 0u; + } else if ((value->Opcode() == IrOpcode::IEqual32 || value->Opcode() == IrOpcode::INotEqual32) && value->ArgumentCount() == 2u) { + const auto left = argument(0u), right = argument(1u); + if (left && right) result = (*left == *right) == (value->Opcode() == IrOpcode::IEqual32) ? 1u : 0u; + } else if ((value->Opcode() == IrOpcode::SelectU1 || value->Opcode() == IrOpcode::SelectU32) && value->ArgumentCount() == 3u) { + if (const auto condition = argument(0u)) result = argument(*condition != 0u ? 1u : 2u); + else { + const auto left = argument(1u), right = argument(2u); + if (left && right && left == right) result = left; + } + } else if (value->IsPhi() && value->ArgumentCount() != 0u && value->ArgumentCount() == value->PhiBlockCount()) { + result = argument(0u); + for (unsigned index = 1u; result && index < value->ArgumentCount(); ++index) if (argument(index) != result) result = std::nullopt; + } + helperValues[value] = result; + return result; + } + + HelperMask helpers(ReplayMask mask, unsigned depth) { + if (depth > 128u) return HelperMask::Uncertain; + mask = ReplayMask{mask.low->Resolve(), mask.high->Resolve()}; + const auto key = std::pair{mask.low->Id(), waveSize == 32u ? mask.low->Id() : mask.high->Id()}; + if (const auto found = helperMasks.find(key); found != helperMasks.end()) return found->second; + helperMasks[key] = HelperMask::Visiting; + HelperMask result = HelperMask::Uncertain; + if (Immediate(mask.low, 0u) && (waveSize == 32u || Immediate(mask.high, 0u))) result = HelperMask::RealOnly; + else if (Immediate(mask.low, 0xffffffffu) && (waveSize == 32u || Immediate(mask.high, 0xffffffffu))) result = HelperMask::QuadStable; + else { + const IrValue* lowBallot = ballotWord(mask.low, 0u); + const IrValue* highBallot = waveSize == 32u ? lowBallot : ballotWord(mask.high, 1u); + if (lowBallot && highBallot) { + const auto low = onHelper(lowBallot->Argument(0), depth + 1u), high = onHelper(highBallot->Argument(0), depth + 1u); + if (low && high && *low == 0u && *high == 0u) result = HelperMask::RealOnly; + } else if (mask.low->Opcode() == IrOpcode::BitwiseAnd32 && (waveSize == 32u || mask.high->Opcode() == IrOpcode::BitwiseAnd32)) { + const auto pair = [&](unsigned low, unsigned high) { return ReplayMask{mask.low->Argument(low)->Resolve(), waveSize == 32u ? mask.low->Argument(low)->Resolve() : mask.high->Argument(high)->Resolve()}; }; + const auto combine = [&](unsigned firstHigh, unsigned secondHigh) { + const auto first = pair(0u, firstHigh), second = pair(1u, secondHigh); + if (!maskStable(first, depth + 1u) || !maskStable(second, depth + 1u)) return HelperMask::Uncertain; + const auto left = helpers(first, depth + 1u), right = helpers(second, depth + 1u); + if (left == HelperMask::RealOnly || right == HelperMask::RealOnly) return HelperMask::RealOnly; + if (left == HelperMask::QuadStable && right == HelperMask::QuadStable) return HelperMask::QuadStable; + return HelperMask::Uncertain; + }; + result = combine(0u, 1u); + if (waveSize != 32u && result != HelperMask::RealOnly) { + const auto swapped = combine(1u, 0u); + if (swapped == HelperMask::RealOnly || (swapped == HelperMask::QuadStable && result == HelperMask::Uncertain)) result = swapped; + } + } else if (mask.low->Opcode() == IrOpcode::CompositeExtractU64 && mask.low->ArgumentCount() == 2u && Immediate(mask.low->Argument(1), 0u)) { + const IrValue* wqm = mask.low->Argument(0)->Resolve(); + const bool paired = waveSize == 32u || (mask.high->Opcode() == IrOpcode::CompositeExtractU64 && mask.high->ArgumentCount() == 2u && mask.high->Argument(0)->Resolve() == wqm && Immediate(mask.high->Argument(1), 1u)); + if (paired && wqm->Opcode() == IrOpcode::WqmU64 && wqm->ArgumentCount() == 1u) { + const IrValue* source = wqm->Argument(0)->Resolve(); + if (source->Opcode() == IrOpcode::CompositeConstructU64 && source->ArgumentCount() == 2u) { + const auto input = helpers(ReplayMask{source->Argument(0)->Resolve(), source->Argument(1)->Resolve()}, depth + 1u); + if (input == HelperMask::RealOnly || input == HelperMask::QuadStable) result = HelperMask::QuadStable; + } + } + } else if (mask.low->IsPhi() && mask.low->ArgumentCount() != 0u && mask.low->ArgumentCount() == mask.low->PhiBlockCount() && (waveSize == 32u || (mask.high->IsPhi() && mask.high->Parent() == mask.low->Parent() && mask.high->ArgumentCount() == mask.high->PhiBlockCount()))) { + result = HelperMask::RealOnly; + for (std::size_t index = 0; index < mask.low->PhiBlockCount(); ++index) { + const IrValue* high = mask.low->Argument(index)->Resolve(); + if (waveSize != 32u) { + high = nullptr; + for (std::size_t highIndex = 0; highIndex < mask.high->PhiBlockCount(); ++highIndex) if (mask.high->PhiBlock(highIndex) == mask.low->PhiBlock(index)) high = mask.high->Argument(highIndex)->Resolve(); + } + const auto incoming = high ? helpers(ReplayMask{mask.low->Argument(index)->Resolve(), high}, depth + 1u) : HelperMask::Uncertain; + if (incoming == HelperMask::Uncertain || incoming == HelperMask::Visiting) { result = HelperMask::Uncertain; break; } + if (incoming == HelperMask::QuadStable) result = HelperMask::QuadStable; + } + } + } + helperMasks[key] = result; + return result; + } + + bool stable(const IrValue* value, unsigned depth) { + if (depth > 512u) return false; + value = value->Resolve(); + if (value->HasImmediate()) return true; + if (const auto found = stableValues.find(value); found != stableValues.end()) return found->second == 2u; + stableValues[value] = 1u; + bool accepted = false; + if (const auto mask = own(value)) { + accepted = maskStable(*mask, depth + 1u); + if (accepted) ownMasks.emplace(value, *mask); + } else if (value->Opcode() != IrOpcode::Ballot && value->Opcode() != IrOpcode::LaneId && value->Opcode() != IrOpcode::WqmU64) { + accepted = std::all_of(value->Arguments().begin(), value->Arguments().end(), [&](const IrValue* argument) { return stable(argument, depth + 1u); }); + } + stableValues[value] = accepted ? 2u : 3u; + return accepted; + } + + bool uniform(const IrValue* value, unsigned depth = 0) { + if (depth > 128u) return false; + value = value->Resolve(); + if (value->HasImmediate()) return true; + if (const auto found = uniformValues.find(value); found != uniformValues.end()) return found->second == 2u; + uniformValues[value] = 1u; + bool accepted = value->Opcode() == IrOpcode::GetUserData || value->Opcode() == IrOpcode::GetShaderBase || value->Opcode() == IrOpcode::GetSrtResource; + const bool transfer = Detail::IsRuntimeUniformOp(value->Opcode()) || value->Opcode() == IrOpcode::ReadConst || value->Opcode() == IrOpcode::FPOrdLessThan32 || value->Opcode() == IrOpcode::FPOrdGreaterThan32; + if (!accepted && transfer && !value->MayHaveSideEffects() && value->ArgumentCount() != 0u) { + accepted = std::all_of(value->Arguments().begin(), value->Arguments().end(), [&](const IrValue* argument) { return uniform(argument, depth + 1u); }); + } + uniformValues[value] = accepted ? 2u : 3u; + return accepted; + } + + bool paths() { + const auto& blocks = program.BlockOrder(); + const auto& metadata = program.Metadata().blockInfo; + if (blocks.empty() || blocks.size() != metadata.size()) return false; + for (std::size_t index = 0; index < blocks.size(); ++index) { + if (!indices.emplace(metadata[index].id, index).second) return false; + blockIndices.emplace(blocks[index], index); + } + for (std::size_t index = 0; index < blocks.size(); ++index) { + const auto& term = metadata[index].terminator; + if (term.loopHeader) return false; + std::set expected; + const auto target = [&](std::uint32_t id) { + const auto found = indices.find(id); + if (found == indices.end()) return false; + expected.insert(blocks[found->second]); + return true; + }; + if (term.kind == TerminatorKind::Branch) { if (!target(term.trueBlock)) return false; } + else if (term.kind == TerminatorKind::ConditionalBranch) { if (!metadata[index].condition || !target(term.trueBlock) || !target(term.falseBlock)) return false; } + else if (term.kind != TerminatorKind::Return) return false; + const std::set actual(blocks[index]->Successors().begin(), blocks[index]->Successors().end()); + if (actual != expected) return false; + } + std::vector path; + std::set visiting; + std::function visit = [&](std::size_t index) { + if (path.size() >= 128u || allPaths.size() >= 256u || !visiting.insert(blocks[index]).second) return false; + path.push_back(blocks[index]); + const auto& term = metadata[index].terminator; + bool valid = !term.loopHeader; + const auto edge = [&](std::uint32_t id) { const auto found = indices.find(id); return found != indices.end() && visit(found->second); }; + if (term.kind == TerminatorKind::Return) allPaths.push_back(path); + else if (term.kind == TerminatorKind::Branch) valid = valid && edge(term.trueBlock); + else if (term.kind == TerminatorKind::ConditionalBranch) valid = valid && metadata[index].condition && edge(term.trueBlock) && edge(term.falseBlock); + else valid = false; + path.pop_back(); + visiting.erase(blocks[index]); + return valid; + }; + return visit(0u); + } + + const IrValue* live(const std::vector& path) const { + const IrValue* result = nullptr; + for (const auto* block : path) { + for (const auto* instruction : block->Instructions()) { + if (instruction->Opcode() != IrOpcode::SetAttribute) continue; + const auto& info = program.Metadata().exportInfo.at(instruction->Flags().index); + if (info.vm) result = instruction->Argument(1)->Resolve(); + } + } + return result; + } + + bool guaranteed(const IrBlock* branch, ReplayMask mask, std::optional continuing = {}) { + for (const auto& path : allPaths) { + const auto found = std::find(path.begin(), path.end(), branch); + if (found == path.end()) continue; + const IrValue* kept = live(path); + if (Boolean(kept, false)) continue; + if (!kept || (continuing && (found + 1 == path.end() || *(found + 1) != *continuing)) || !GuardImpliesMaskOnPath(program, kept, mask.low, mask.high, path)) return false; + } + return true; + } + + std::optional any(const IrValue* value) { + value = value->Resolve(); + if (value->Opcode() != IrOpcode::INotEqual32) return std::nullopt; + const IrValue* words = Other(value, 0u); + if (!words || words->Opcode() != IrOpcode::BitwiseOr32 || words->ArgumentCount() != 2u) return std::nullopt; + ReplayMask mask{words->Argument(0)->Resolve(), words->Argument(1)->Resolve()}; + if (!maskStable(mask, 0u)) { + mask = ReplayMask{mask.high, mask.low}; + if (!maskStable(mask, 0u)) return std::nullopt; + } + restrict(words); + restrict(value); + return mask; + } + + bool sameValue(const IrValue* left, const IrValue* right) const { + left = left->Resolve(); + right = right->Resolve(); + return left == right || (left->HasImmediate() && right->HasImmediate() && left->Type() == IrType::U32 && right->Type() == IrType::U32 && left->ImmediateU32() == right->ImmediateU32()); + } + + bool inactiveEquals(const IrValue* value, const IrValue* other, const IrValue* predicate, unsigned depth = 0) { + if (depth > 32u) return false; + value = value->Resolve(); + if (sameValue(value, other)) return true; + if ((value->Opcode() == IrOpcode::SelectU32 || value->Opcode() == IrOpcode::SelectF32 || value->Opcode() == IrOpcode::SelectU1) && value->ArgumentCount() == 3u && (value->Argument(0)->Resolve() == predicate->Resolve() || GuardImplies(program, value->Argument(0), predicate))) return inactiveEquals(value->Argument(2), other, predicate, depth + 1u); + return false; + } + + bool quadObserved(const IrValue* value) const { + std::set visited; + std::vector pending{value}; + while (!pending.empty()) { + const IrValue* current = pending.back(); + pending.pop_back(); + if (!visited.insert(current).second) continue; + for (const auto& info : program.Metadata().blockInfo) if (info.condition && info.condition->Resolve() == current) return true; + for (const auto& use : current->OperandUses()) { + const auto opcode = use.user->Opcode(); + if (ImageOpcodeInfoOf(opcode).needsSampler || opcode == IrOpcode::Ballot || opcode == IrOpcode::WqmU64 || opcode == IrOpcode::ReadLane || opcode == IrOpcode::ReadFirstLane || opcode == IrOpcode::DppMoveU32 || opcode == IrOpcode::DppUpdateU32 || opcode == IrOpcode::SwizzleU32 || opcode == IrOpcode::PermuteU32 || opcode == IrOpcode::BpermuteU32 || opcode == IrOpcode::Permlane16U32 || opcode == IrOpcode::WriteLane || SharedAccessOf(opcode) != SharedAccess::None) return true; + if (opcode == IrOpcode::SetAttribute || opcode == IrOpcode::Reference || opcode == IrOpcode::ReferenceU32) continue; + pending.push_back(use.user); + } + } + return false; + } + + bool helperObserved(const IrValue* value) const { + std::set visited; + std::vector pending{value}; + while (!pending.empty()) { + const IrValue* current = pending.back(); + pending.pop_back(); + if (!visited.insert(current).second) continue; + for (const auto& info : program.Metadata().blockInfo) { + if (!info.condition || info.condition->Resolve() != current) continue; + const auto kind = info.terminator.condition; + if (kind != BranchCondition::ExecZero && kind != BranchCondition::ExecNonZero && kind != BranchCondition::VccZero && kind != BranchCondition::VccNonZero) return true; + } + for (const auto& use : current->OperandUses()) { + const auto opcode = use.user->Opcode(); + if (ImageOpcodeInfoOf(opcode).needsSampler) return true; + if (restricted.contains(use.user) || opcode == IrOpcode::SetAttribute || opcode == IrOpcode::Reference || opcode == IrOpcode::ReferenceU32) continue; + pending.push_back(use.user); + } + } + return false; + } + + bool optionalBody(const IrBlock* branch, const IrBlock* body, const IrBlock* merge, const IrValue* predicate) { + const auto& bodyInfo = program.Metadata().blockInfo[blockIndices.at(body)]; + if (bodyInfo.terminator.kind != TerminatorKind::Branch || indices.at(bodyInfo.terminator.trueBlock) != blockIndices.at(merge)) return false; + std::set merged; + for (const auto* instruction : merge->Instructions()) { + if (!instruction->IsPhi()) continue; + if (instruction->ArgumentCount() != 2u || instruction->PhiBlockCount() != 2u) return false; + const IrValue* skipped = nullptr; + const IrValue* taken = nullptr; + for (std::size_t index = 0; index < 2u; ++index) { + if (instruction->PhiBlock(index) == branch) skipped = instruction->Argument(index)->Resolve(); + if (instruction->PhiBlock(index) == body) taken = instruction->Argument(index)->Resolve(); + } + if (!skipped || !taken || !inactiveEquals(taken, skipped, predicate) || quadObserved(instruction)) return false; + merged.insert(instruction); + } + for (const auto* instruction : body->Instructions()) { + if (instruction->Opcode() == IrOpcode::SetAttribute || instruction->Opcode() == IrOpcode::Discard) return false; + for (const auto& use : instruction->OperandUses()) { + if (use.user->Parent() == body || merged.contains(use.user) || use.user->Opcode() == IrOpcode::Reference || use.user->Opcode() == IrOpcode::ReferenceU32) continue; + return false; + } + } + return !merged.empty(); + } + + bool control(const BlockInfo& info) { + const auto* branch = program.BlockOrder()[indices.at(info.id)]; + const auto* trueBlock = program.BlockOrder()[indices.at(info.terminator.trueBlock)]; + const auto* falseBlock = program.BlockOrder()[indices.at(info.terminator.falseBlock)]; + const IrValue* condition = info.condition->Resolve(); + const bool waveZero = info.terminator.condition == BranchCondition::ExecZero || info.terminator.condition == BranchCondition::VccZero; + const bool waveNonZero = info.terminator.condition == BranchCondition::ExecNonZero || info.terminator.condition == BranchCondition::VccNonZero; + if (waveZero || waveNonZero) { + const IrValue* predicate = condition; + if (waveZero) { + if (condition->Opcode() != IrOpcode::LogicalNot || condition->ArgumentCount() != 1u) return false; + predicate = condition->Argument(0)->Resolve(); + } + if (!stable(predicate, 0u)) return false; + if (uniform(predicate)) return true; + if (predicate->Opcode() == IrOpcode::LogicalAnd && predicate->ArgumentCount() == 2u) { + for (unsigned index = 0; index < 2u; ++index) { + const auto mask = own(predicate->Argument(index)); + if (mask && uniform(predicate->Argument(1u - index)) && guaranteed(branch, *mask)) return true; + } + } + const auto* body = waveZero ? falseBlock : trueBlock; + const auto* merge = waveZero ? trueBlock : falseBlock; + return optionalBody(branch, body, merge, predicate); + } + bool inverted = false; + if (condition->Opcode() == IrOpcode::LogicalNot && condition->ArgumentCount() == 1u) { condition = condition->Argument(0)->Resolve(); inverted = true; } + if (const auto mask = any(condition)) { + if (inverted) restrict(info.condition->Resolve()); + return guaranteed(branch, *mask, inverted ? falseBlock : trueBlock); + } + return stable(info.condition, 0u); + } + + const IrProgram& program; + std::uint32_t waveSize; + std::set restricted; + std::map ownMasks; + std::map, unsigned> masks; + std::map, HelperMask> helperMasks; + std::unordered_map> helperValues; + std::unordered_map stableValues; + std::unordered_map uniformValues; + std::map indices; + std::map blockIndices; + std::vector> allPaths; +}; + +std::optional> Values(const IrProgram& program, const IrValue* value, std::set& visiting, unsigned depth, std::optional component = {}, const IrValue* active = nullptr) { + if (value == nullptr || depth > 64u) return std::nullopt; + value = value->Resolve(); + if (!visiting.insert(value).second) return std::nullopt; + const auto finish = [&](std::optional> result) { + visiting.erase(value); + if (result && result->size() > 16u) return std::optional>{}; + return result; + }; + const auto child = [&](const IrValue* next, std::optional part = {}) { return Values(program, next, visiting, depth + 1u, part, active); }; + if (component) { + if (value->Opcode() == IrOpcode::CompositeConstructU32x4 && *component < value->ArgumentCount()) return finish(child(value->Argument(*component))); + return finish(std::nullopt); + } + if (value->HasImmediate() && value->Type() == IrType::U32) return finish(std::set{value->ImmediateU32()}); + if (value->Opcode() == IrOpcode::CompositeExtractU32x4 && value->ArgumentCount() == 2u && value->Argument(1)->Resolve()->HasImmediate()) return finish(child(value->Argument(0), value->Argument(1)->Resolve()->ImmediateU32())); + if (value->Opcode() == IrOpcode::SelectU32 || value->Opcode() == IrOpcode::Select) { + if (value->ArgumentCount() != 3u) return finish(std::nullopt); + if (active != nullptr && (Implies(active, value->Argument(0)) || GuardImplies(program, active, value->Argument(0)))) return finish(child(value->Argument(1))); + auto left = child(value->Argument(1)); + auto right = child(value->Argument(2)); + if (!left || !right) return finish(std::nullopt); + left->insert(right->begin(), right->end()); + return finish(std::move(left)); + } + if (value->Opcode() == IrOpcode::Phi) { + std::set result; + for (const auto* argument : value->Arguments()) { + const auto values = child(argument); + if (!values) return finish(std::nullopt); + result.insert(values->begin(), values->end()); + } + return finish(std::move(result)); + } + return finish(std::nullopt); +} + +} + +std::vector StencilReferenceValues(const IrProgram& program) { + return StencilReferenceValues(program, 0u); +} + +std::vector StencilReferenceValues(const IrProgram& program, std::uint32_t shift) { + std::set result; + for (const auto& block : program.Blocks()) { + for (const auto* instruction : block->Instructions()) { + if (instruction->Opcode() != IrOpcode::SetAttribute) continue; + const auto& info = program.Metadata().exportInfo.at(instruction->Flags().index); + if (info.kind != ExportTargetKind::MrtZ || (info.en & 2u) == 0u) continue; + std::set visiting; + const auto values = Values(program, instruction->Argument(0), visiting, 0u, 1u, instruction->Argument(1)); + if (!values) { + std::vector all(256u); + std::iota(all.begin(), all.end(), 0u); + return all; + } + for (const auto value : *values) result.insert((value >> shift) & 0xffu); + } + } + return {result.begin(), result.end()}; +} + +bool StencilReferenceDefinedForKeptInvocations(const IrProgram& program) { + const auto& blocks = program.BlockOrder(); + const auto& metadata = program.Metadata().blockInfo; + if (blocks.empty() || blocks.size() != metadata.size()) return false; + std::map indices; + for (std::size_t index = 0; index < metadata.size(); ++index) if (!indices.emplace(metadata[index].id, index).second) return false; + std::vector> successors(blocks.size()), predecessors(blocks.size()); + for (std::size_t index = 0; index < metadata.size(); ++index) { + const auto& term = metadata[index].terminator; + if (term.loopHeader || (term.kind != TerminatorKind::Branch && term.kind != TerminatorKind::ConditionalBranch && term.kind != TerminatorKind::Return)) return false; + const auto edge = [&](std::uint32_t id) { + const auto found = indices.find(id); + if (found == indices.end()) return false; + successors[index].push_back(found->second); + predecessors[found->second].push_back(index); + return true; + }; + if (term.kind == TerminatorKind::Branch && !edge(term.trueBlock)) return false; + if (term.kind == TerminatorKind::ConditionalBranch && (!edge(term.trueBlock) || !edge(term.falseBlock))) return false; + } + std::vector reached(blocks.size()); + std::function visit = [&](std::size_t index) { + if (reached[index] == 1u) return false; + if (reached[index] == 2u) return true; + reached[index] = 1u; + for (const auto next : successors[index]) if (!visit(next)) return false; + reached[index] = 2u; + return true; + }; + if (!visit(0u)) return false; + struct State { + std::set written; + const IrValue* live = nullptr; + bool liveKnown = true; + }; + std::vector> states(blocks.size()); + bool valid = true; + std::function resolve = [&](std::size_t index) -> const State& { + if (states[index]) return *states[index]; + State state; + bool first = true; + for (const auto previous : predecessors[index]) { + if (reached[previous] != 2u) continue; + const auto& incoming = resolve(previous); + if (first) { state = incoming; first = false; } + else { + std::set common; + std::set_intersection(state.written.begin(), state.written.end(), incoming.written.begin(), incoming.written.end(), std::inserter(common, common.begin())); + state.written = std::move(common); + state.liveKnown = state.liveKnown && incoming.liveKnown && state.live == incoming.live; + } + } + for (const auto* instruction : blocks[index]->Instructions()) { + if (instruction->Opcode() != IrOpcode::SetAttribute) continue; + const auto& info = program.Metadata().exportInfo.at(instruction->Flags().index); + const auto* active = instruction->Argument(1)->Resolve(); + if (info.kind == ExportTargetKind::MrtZ && (info.en & 2u) != 0u && !Boolean(active, false)) state.written.insert(Boolean(active, true) ? nullptr : active); + if (info.vm) { + state.live = Boolean(active, true) ? nullptr : active; + state.liveKnown = true; + } + } + if (metadata[index].terminator.kind == TerminatorKind::Return) { + const bool killed = state.liveKnown && Boolean(state.live, false); + const bool written = state.written.contains(nullptr) || (state.liveKnown && std::ranges::any_of(state.written, [&](const IrValue* guard) { return Implies(state.live, guard) || GuardImplies(program, state.live, guard); })); + if (!killed && !written) valid = false; + } + states[index] = std::move(state); + return *states[index]; + }; + for (std::size_t index = 0; index < blocks.size(); ++index) if (reached[index] == 2u) resolve(index); + return valid; +} + +bool StencilReplaySafe(const IrProgram& program) { + if (program.Info().sharedMemoryBytes != 0u || program.Info().bdaWrites || program.Info().usesDma || (program.Info().usesFaultBuffer && program.Resources().guardedSrtSlots.empty())) return false; + if (program.Info().usesFaultBuffer && !FaultControlFlowAcyclic(program)) return false; + for (const auto& input : program.Info().inputs) if (input.kind == StageInputKind::PrimitiveId) return false; + bool masks = false; + for (const auto& info : program.Metadata().blockInfo) { + if (info.terminator.loopHeader) return false; + if (info.terminator.kind != TerminatorKind::ConditionalBranch) continue; + const auto kind = info.terminator.condition; + masks = masks || kind == BranchCondition::ExecZero || kind == BranchCondition::ExecNonZero || kind == BranchCondition::VccZero || kind == BranchCondition::VccNonZero; + } + for (const auto& block : program.Blocks()) { + for (const auto* instruction : block->Instructions()) { + const auto opcode = instruction->Opcode(); + if (opcode == IrOpcode::Loop || opcode == IrOpcode::LoopMerge || opcode == IrOpcode::ImageBvhIntersectRay || BufferAccessOf(opcode) == BufferAccess::Atomic || AddressOpcodeInfoOf(opcode).access == AddressAccess::Write || AddressOpcodeInfoOf(opcode).access == AddressAccess::Atomic || SharedAccessOf(opcode) != SharedAccess::None || ImageOpcodeInfoOf(opcode).access == ImageAccess::Write || ImageOpcodeInfoOf(opcode).access == ImageAccess::Atomic) return false; + if (opcode == IrOpcode::UndefU1 || opcode == IrOpcode::UndefU8 || opcode == IrOpcode::UndefU16 || opcode == IrOpcode::UndefU32 || opcode == IrOpcode::UndefU64) return false; + if (opcode == IrOpcode::ShaderClock || opcode == IrOpcode::RealtimeClock || opcode == IrOpcode::ReadFirstLane || opcode == IrOpcode::ReadLane || opcode == IrOpcode::WriteLane || opcode == IrOpcode::DppMoveU32 || opcode == IrOpcode::DppUpdateU32 || opcode == IrOpcode::Permlane16U32 || opcode == IrOpcode::PermuteU32 || opcode == IrOpcode::BpermuteU32 || opcode == IrOpcode::SwizzleU32) return false; + masks = masks || opcode == IrOpcode::Ballot || opcode == IrOpcode::LaneId || opcode == IrOpcode::WqmU64; + if (!instruction->MayHaveSideEffects()) continue; + switch (opcode) { + case IrOpcode::Branch: + case IrOpcode::BranchConditional: + case IrOpcode::Return: + case IrOpcode::Unreachable: + case IrOpcode::Discard: + case IrOpcode::Reference: + case IrOpcode::ReferenceU32: + case IrOpcode::SetAttribute: + case IrOpcode::InterpolateHostP2: + case IrOpcode::SetRegister: + case IrOpcode::SetThreadBitScalarRegister: + case IrOpcode::SetScalarMaskTag: + case IrOpcode::SetScalarRegister: + case IrOpcode::SetVectorRegister: + case IrOpcode::StoreVectorWindow: + case IrOpcode::SetGotoVariable: + case IrOpcode::SetScc: + case IrOpcode::SetExec: + case IrOpcode::SetExecLo: + case IrOpcode::SetExecHi: + case IrOpcode::SetVcc: + case IrOpcode::SetVccLo: + case IrOpcode::SetVccHi: + case IrOpcode::SetM0: + case IrOpcode::Waitcnt: + case IrOpcode::InstPrefetch: + break; + case IrOpcode::Sendmsg: + if (program.Resources().stage != IrShaderStage::Vertex || instruction->ArgumentCount() != 0u) return false; + break; + default: + return false; + } + } + } + return !masks || MaskReplaySafety(program).Check(); +} + +} diff --git a/core/shader/recompiler/StencilReplay.hpp b/core/shader/recompiler/StencilReplay.hpp new file mode 100644 index 0000000000..5c998b0056 --- /dev/null +++ b/core/shader/recompiler/StencilReplay.hpp @@ -0,0 +1,16 @@ +#ifndef CORE_SHADER_RECOMPILER_STENCILREPLAY_HPP +#define CORE_SHADER_RECOMPILER_STENCILREPLAY_HPP + +#include "IntermediateRepresentation/IrProgram.hpp" +#include + +namespace ShaderRecompiler { + +[[nodiscard]] std::vector StencilReferenceValues(const IrProgram& program); +[[nodiscard]] std::vector StencilReferenceValues(const IrProgram& program, std::uint32_t shift); +[[nodiscard]] bool StencilReplaySafe(const IrProgram& program); +[[nodiscard]] bool StencilReferenceDefinedForKeptInvocations(const IrProgram& program); + +} + +#endif diff --git a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp index c1aa7723ae..2c190b4239 100644 --- a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp +++ b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp @@ -33,7 +33,10 @@ struct TranslateOptions { std::uint32_t sharedMemoryBytes = 0; std::uint64_t shaderHash = 0; bool fragmentShaderBarycentricEnabled = false; + bool interpolationFunctions = false; bool nativeFloat16 = false; + bool float32FlushToZero = false; + bool gcnLegacyVertexInputs = false; std::optional floatMode; ShaderStageInputInfo inputInfo; const EmbeddedFetchPlan* embeddedFetch = nullptr; diff --git a/core/shader/recompiler/Translation/include/Translation/RoundingMode.hpp b/core/shader/recompiler/Translation/include/Translation/RoundingMode.hpp new file mode 100644 index 0000000000..5495b99408 --- /dev/null +++ b/core/shader/recompiler/Translation/include/Translation/RoundingMode.hpp @@ -0,0 +1,50 @@ +#ifndef CORE_SHADER_RECOMPILIER_TRANSLATION_INCLUDE_TRANSLATION_ROUNDINGMODE_HPP +#define CORE_SHADER_RECOMPILIER_TRANSLATION_INCLUDE_TRANSLATION_ROUNDINGMODE_HPP + +#include "ControlFlow/ControlFlowGraph.hpp" +#include "RdnaDecoder/RdnaOpcode.hpp" +#include "RdnaDecoder/RdnaProgram.hpp" +#include +#include +#include + +namespace ShaderRecompiler { + +inline constexpr std::uint32_t ModeRoundBits = 0xfu; +inline constexpr std::uint32_t ModeRound32Bits = 0x3u; +inline constexpr std::uint32_t ModeRound1664Bits = 0xcu; + +struct RoundingState { + std::uint8_t value = 0; + std::uint8_t unknown = 0; + + bool operator==(const RoundingState&) const = default; +}; + +struct RoundingWrite { + std::uint32_t mask = 0; + std::uint32_t value = 0; + bool known = false; +}; + +enum class RoundingUseKind { + Directed32, + Directed16, + SelfChecked, + Unmodelled +}; + +struct RoundingUse { + std::uint32_t fields = 0; + RoundingUseKind kind = RoundingUseKind::Unmodelled; +}; + +[[nodiscard]] std::optional DecodeRoundingWrite(const RdnaInstruction& instruction); +[[nodiscard]] RoundingState ApplyRoundingWrite(RoundingState state, const RoundingWrite& write); +[[nodiscard]] RoundingState JoinRoundingStates(RoundingState lhs, RoundingState rhs); +[[nodiscard]] RoundingUse RoundingUseOf(RdnaOpcode opcode); +[[nodiscard]] std::unordered_map ComputeEntryRounding(const ControlFlowGraph& cfg, const RdnaProgram& program, std::uint32_t initial); + +} + +#endif diff --git a/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp b/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp index 8fe4a01871..4f7c73ee5c 100644 --- a/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp +++ b/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp @@ -10,6 +10,7 @@ namespace ShaderRecompiler { // hostSubgroupSize is the device subgroup width; wave64 workgroups on 32-wide subgroups run each // invocation as two guest lanes so a guest wave stays one host subgroup. ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const GuestContext& context, std::uint32_t hostSubgroupSize, const MeshConfiguration* mesh = nullptr, const TessellationConfiguration* tessellation = nullptr); +void ValidateVertexStageInputs(ShaderStageKind stage, const GuestContext& context, const MeshConfiguration* mesh = nullptr, const TessellationConfiguration* tessellation = nullptr); } diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index bc79d3fd18..a7748044c4 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -2,6 +2,7 @@ #define CORE_SHADER_RECOMPILIER_TRANSLATION_INCLUDE_TRANSLATION_TRANSLATIONCONTEXT_HPP #include "Translation/InstructionTranslator.hpp" +#include "Translation/RoundingMode.hpp" #include #include #include @@ -16,10 +17,18 @@ public: void TranslateEmbeddedFetch(const RdnaInstruction& instruction, std::uint32_t attribute, std::uint32_t components); void TranslateInstruction(const RdnaInstruction& instruction); void SetPixelInput(const ShaderPixelInputInfo* info, bool barycentricEnabled) { pixelInput = info; fragmentShaderBarycentricEnabled = barycentricEnabled; } - void SetFloatMode(const std::optional& mode) { floatMode = mode; ieeeMode = mode.has_value() && mode->ieeeMode; } + void SetFloatMode(const std::optional& mode) { + declaredFloatMode = mode; + floatMode = mode; + ieeeMode = mode.has_value() && mode->ieeeMode; + rounding = RoundingState{static_cast(mode.has_value() ? mode->floatMode & ModeRoundBits : 0u), 0u}; + } + void SetEntryRounding(const RoundingState& state) { setRounding(state); } void SetNativeFloat16(bool enabled) { nativeFloat16 = enabled; } + void SetFloat32FlushToZero(bool enabled) { float32FlushToZero = enabled; } void AddBranchCondition(const BasicBlock& source, BlockInfo& info); void TranslateCodeTableLoad(const RdnaInstruction& instruction, const ControlFlowGraph::CodeTableLoad& table); + void TranslateUnlinkedCall(const RdnaInstruction& instruction); private: struct AddressOperands { @@ -49,23 +58,27 @@ private: IrU1 threadBit(const std::array& mask); void writeRawU32(const RdnaOperand& operand, IrU32 value); IrF32 applyF32ResultModifiers(const RdnaOperand& operand, IrF32 value); + IrF32 scaleF32Result(std::uint32_t omod, IrF32 value); IrF32 applyF16ResultModifiers(const RdnaOperand& operand, IrF32 value); bool outputModifierApplies(std::uint32_t denormalShift) const; bool dx10Clamp() const { return !floatMode.has_value() || floatMode->dx10Clamp; } 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); void writeOperand(const RdnaOperand& operand, IrValue* value); IrU32 packHalf2x16(IrF32 low, IrF32 high); void write16Bits(const RdnaOperand& operand, IrU32 value); - void writeF16(const RdnaOperand& operand, IrF32 value, std::initializer_list sources); + void writeF16(const RdnaOperand& operand, IrF32 value, std::initializer_list sources, IrValue* invalid = nullptr); bool directedF16Rounding() const; IrU32 flushF16Bits(IrU32 bits, std::uint32_t keepBit); IrU32 flushF16Input(IrU32 bits); 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); @@ -74,10 +87,13 @@ private: IrU32 quietNan16(IrU32 bits); IrValue* nanResultF32(std::initializer_list sources, IrValue* result, IrValue* invalidProduct = nullptr); IrValue& invalidProductF32(IrValue* lhs, IrValue* rhs); + IrValue& invalidOperationF32(std::initializer_list sources, IrValue& invalidProduct); std::array quietNan64(const std::array& bits); std::array readU32Pair(const RdnaOperand& operand); IrU64 readU64(const RdnaOperand& operand); std::array readF64Bits(const RdnaOperand& operand); + std::array flushF64Bits(const std::array& bits, std::uint32_t keepBit); + IrValue& flushTinyF64Result(IrOpcode opcode, IrValue& lhs, IrValue& rhs, IrValue& result); IrU64 readF64(const RdnaOperand& operand); IrF32 readF16LaneAsF32(const RdnaOperand& operand, bool highLane, bool packed = false); IrF32 readF16AsF32(const RdnaOperand& operand); @@ -145,6 +161,7 @@ private: bool dsAtomic64(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue); bool dsCondxchg32(const RdnaInstruction& inst); bool dsAppendConsume(const RdnaInstruction& inst, IrOpcode opcode); + bool dsOrderedCount(const RdnaInstruction& inst); bool dsAddtid(const RdnaInstruction& inst, bool write); bool globalAddtid(const RdnaInstruction& inst, bool write); bool dsSwizzleB32(const RdnaInstruction& inst); @@ -197,6 +214,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); @@ -221,7 +239,7 @@ private: bool vMullitF32(const RdnaInstruction& inst); void emitFloat16ClassCompare(const RdnaInstruction& inst, bool cmpx); bool float64Operation(const RdnaInstruction& inst, IrOpcode opcode); - bool nonIeeeMinMaxF64(const RdnaInstruction& inst, IrOpcode opcode); + bool minMaxF64(const RdnaInstruction& inst, IrOpcode opcode); bool float64Unary(const RdnaInstruction& inst, IrOpcode opcode); bool vCvtF64F32(const RdnaInstruction& inst); void writeF64Result(const RdnaOperand& operand, IrValue& value); @@ -238,6 +256,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); @@ -343,8 +364,12 @@ private: void vInterpP2F16(const RdnaInstruction& inst); void eXP(const RdnaInstruction& inst); bool emitScalar(const RdnaInstruction& inst); + void setRounding(const RoundingState& state); + void writeRounding(const RdnaInstruction& inst); + void checkRoundingUse(const RdnaInstruction& inst) const; bool emitVector(const RdnaInstruction& inst); std::uint32_t f32DenormalFlushFor(const RdnaInstruction& inst) const; + bool flushesInHardware(const RdnaInstruction& inst) const; bool emitInterpolation(const RdnaInstruction& inst); bool emitMemory(const RdnaInstruction& inst); @@ -352,6 +377,8 @@ private: const ShaderPixelInputInfo* pixelInput = nullptr; bool fragmentShaderBarycentricEnabled = false; std::optional floatMode; + std::optional declaredFloatMode; + RoundingState rounding; bool ieeeMode = false; bool nativeFloat16 = false; IrBuilder ir; @@ -359,6 +386,7 @@ private: IrU1 instructionBranchCondition; RdnaOpcode currentOpcode = RdnaOpcode::Unknown; std::uint32_t f32DenormalFlush = 0u; + bool float32FlushToZero = false; std::uint32_t currentProgramCounter = 0; std::uint32_t currentVectorLimit = 1; }; diff --git a/core/shader/recompiler/Translation/src/AttributeInstructions.cpp b/core/shader/recompiler/Translation/src/AttributeInstructions.cpp index f2878ed016..f06b151231 100644 --- a/core/shader/recompiler/Translation/src/AttributeInstructions.cpp +++ b/core/shader/recompiler/Translation/src/AttributeInstructions.cpp @@ -93,17 +93,16 @@ void TranslationContext::vInterpMovF32(const RdnaInstruction& inst) { if (inst.source0.value >= 3u) { throw std::runtime_error("v_interp_mov_f32 mode is reserved"); } + if (inst.source1.value < 32u) program.Metadata().pixelVertexOrderInputs |= 1u << inst.source1.value; IrValue& value = ir.Emit(IrOpcode::GetInterpolationParameter, IrType::U32, {&ir.Constant(inst.source1.value), &ir.Constant(inst.source2.value), &ir.Constant(inst.source0.value)}); writeOperand(inst.destination, &value); } 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"); } + if (inst.source1.value < 32u) program.Metadata().pixelVertexOrderInputs |= 1u << inst.source1.value; return IrF32(ir.Emit(IrOpcode::GetInterpolationParameterF16, IrType::F32, {&ir.Constant(inst.source1.value), &ir.Constant(inst.source2.value), &ir.Constant(mode), &ir.Constant(inst.source1.opSel ? 1u : 0u)})); } diff --git a/core/shader/recompiler/Translation/src/CompareInstructions.cpp b/core/shader/recompiler/Translation/src/CompareInstructions.cpp index 50bdf556cc..a562ccc2d0 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); @@ -225,8 +225,16 @@ IrU64 TranslationContext::float64OrderKey(const std::array& bits) { } void TranslationContext::emitFloat64Compare(const RdnaInstruction& inst, bool less, bool equal, bool greater, bool unordered, bool cmpx) { - const std::array lhs = readF64Bits(sourceAt(inst, 0u)); - const std::array rhs = readF64Bits(sourceAt(inst, 1u)); + std::array, 2> bits{readF64Bits(sourceAt(inst, 0u)), readF64Bits(sourceAt(inst, 1u))}; + if (floatMode.has_value() && (floatMode->floatMode & 0x40u) == 0u) { + for (auto& value : bits) { + const IrU1 zeroExponent(ir.IEqual(ir.BitwiseAnd(value[1].Value(), ir.Constant(0x7ff00000u)), ir.Constant(0u))); + value[0] = IrU32(ir.Select(zeroExponent.Value(), ir.Constant(0u), value[0].Value())); + value[1] = IrU32(ir.Select(zeroExponent.Value(), ir.BitwiseAnd(value[1].Value(), ir.Constant(0x80000000u)), value[1].Value())); + } + } + const auto& lhs = bits[0]; + const auto& rhs = bits[1]; const IrU1 lhsNan = float64IsNan(lhs); const IrU1 rhsNan = float64IsNan(rhs); IrValue& isUnordered = ir.LogicalOr(lhsNan.Value(), rhsNan.Value()); diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index 18bb250332..bb8094339a 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -28,8 +28,7 @@ void TranslationContext::sSubvectorLoop(const RdnaInstruction& inst, bool begin) IrU32 newCounter = counter; if (begin) { const IrU1 lowZero(ir.IEqual(execLo.Value(), zero.Value())); - const IrU1 highZero(ir.IEqual(execHi.Value(), zero.Value())); - const IrU1 execZero(ir.LogicalAnd(lowZero.Value(), highZero.Value())); + const IrU1 execZero(ir.IEqual(ir.Emit(IrOpcode::BitwiseOr32, IrType::U32, {&execLo.Value(), &execHi.Value()}), zero.Value())); const IrU32 entered(ir.Select(lowZero.Value(), execLo.Value(), execHi.Value())); newCounter = IrU32(ir.Select(execZero.Value(), counter.Value(), entered.Value())); newExecHi = IrU32(ir.Select(ir.LogicalOr(execZero.Value(), lowZero.Value()), execHi.Value(), zero.Value())); @@ -283,6 +282,13 @@ void TranslationContext::sSwappcB64(const RdnaInstruction& inst) { writeU32Pair(inst.destination, extractU64(link)); } +void TranslationContext::TranslateUnlinkedCall(const RdnaInstruction& inst) { + const std::array target = readU32Pair(inst.source0); + (void)ir.Emit(IrOpcode::UnlinkedCall, IrType::Void, {&target[0].Value(), &target[1].Value(), &ir.Constant(inst.programCounter)}); + program.Info().usesFaultBuffer = true; + sSwappcB64(inst); +} + void TranslationContext::sCselectB32(const RdnaInstruction& inst) { const IrU32 trueValue = readU32(sourceAt(inst, 0u)); const IrU32 falseValue = readU32(sourceAt(inst, 1u)); 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/DispatchInstructions.cpp b/core/shader/recompiler/Translation/src/DispatchInstructions.cpp index e2c9f9f7b5..ce731d97b9 100644 --- a/core/shader/recompiler/Translation/src/DispatchInstructions.cpp +++ b/core/shader/recompiler/Translation/src/DispatchInstructions.cpp @@ -24,6 +24,7 @@ void TranslationContext::TranslateInstruction(const RdnaInstruction& decoded) { if (instruction.op == RdnaOpcode::Unsupported) { throw std::runtime_error(instruction.unsupportedReason.empty() ? "unsupported decoded instruction at pc " + std::to_string(instruction.programCounter) : std::string(instruction.unsupportedReason)); } + checkRoundingUse(instruction); f32DenormalFlush = f32DenormalFlushFor(instruction); bool translated = false; switch (instruction.family) { diff --git a/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp b/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp index 1e2e2d5fcc..966973b0f8 100644 --- a/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp +++ b/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp @@ -186,7 +186,17 @@ std::uint32_t embeddedFetchDstSize(const RdnaInstruction& inst) { std::uint32_t scalarOperandWidth(const RdnaInstruction& inst, std::uint32_t operandIndex) { switch (inst.family) { case RdnaInstructionFamily::SMEM: - return operandIndex == 0u ? 4u : 1u; + if (operandIndex != 0u) return 1u; + switch (inst.op) { + case RdnaOpcode::SLoadDword: + case RdnaOpcode::SLoadDwordx2: + case RdnaOpcode::SLoadDwordx4: + case RdnaOpcode::SLoadDwordx8: + case RdnaOpcode::SLoadDwordx16: + return 2u; + default: + return 4u; + } case RdnaInstructionFamily::MUBUF: case RdnaInstructionFamily::MTBUF: return operandIndex == 1u ? 4u : 1u; @@ -217,6 +227,39 @@ bool touchesSgpr(const RdnaInstruction& inst, std::uint32_t sgpr) { return false; } +bool killsSgpr(const RdnaInstruction& inst, std::uint32_t sgpr) { + if (!isScalarOperand(inst.destination)) return false; + const auto first = scalarSlot(inst.destination); + switch (inst.op) { + case RdnaOpcode::SCmovB32: + case RdnaOpcode::SCmovB64: + case RdnaOpcode::SBitset0B32: + case RdnaOpcode::SBitset0B64: + case RdnaOpcode::SBitset1B32: + case RdnaOpcode::SBitset1B64: + case RdnaOpcode::SMovreldB32: + case RdnaOpcode::SMovreldB64: + return false; + case RdnaOpcode::SMovkI32: + case RdnaOpcode::VReadlaneB32: + case RdnaOpcode::VReadfirstlaneB32: + return sgpr == first; + case RdnaOpcode::SLoadDword: + case RdnaOpcode::SLoadDwordx2: + case RdnaOpcode::SLoadDwordx4: + case RdnaOpcode::SLoadDwordx8: + case RdnaOpcode::SLoadDwordx16: + case RdnaOpcode::SBufferLoadDword: + case RdnaOpcode::SBufferLoadDwordx2: + case RdnaOpcode::SBufferLoadDwordx4: + case RdnaOpcode::SBufferLoadDwordx8: + case RdnaOpcode::SBufferLoadDwordx16: + return sgpr >= first && sgpr < first + decodedDstSize(inst); + default: + return (inst.family == RdnaInstructionFamily::SOP1 || inst.family == RdnaInstructionFamily::SOP2) && sgpr == first; + } +} + bool isVectorAlu(const RdnaInstruction& inst) { switch (inst.family) { case RdnaInstructionFamily::VOP1: @@ -350,6 +393,12 @@ bool writesExec(const RdnaInstruction& inst) { } } +bool setsExecAllOnes(const RdnaInstruction& inst) { + const bool allOnes = inst.source0.kind == RdnaOperandKind::IntegerInlineConstant && inst.source0.signedVal == -1; + if (!allOnes || inst.destination.kind != RdnaOperandKind::ExecLo) return false; + return inst.op == RdnaOpcode::SMovB64; +} + bool writesSgprMask(const RdnaInstruction& inst, std::uint32_t sgpr, std::uint32_t waveSize) { const auto covers = [&](const RdnaOperand& operand, std::uint32_t width) { return isScalarOperand(operand) && sgpr >= scalarSlot(operand) && sgpr < scalarSlot(operand) + width; }; return covers(inst.destination2, 2u) || (isVectorAlu(inst) && covers(inst.destination, waveSize == 64u ? 2u : 1u)); @@ -357,6 +406,8 @@ bool writesSgprMask(const RdnaInstruction& inst, std::uint32_t sgpr, std::uint32 bool carryObserved(const RdnaProgram& program, std::size_t addIndex, std::uint32_t waveSize) { const auto& add = program.instructions[addIndex]; + if (add.op == RdnaOpcode::VAddNcU32) return false; + if (add.op == RdnaOpcode::VSadU32) return false; if (add.destination2.kind == RdnaOperandKind::None || add.destination2.kind == RdnaOperandKind::Null) return false; if (!isScalarOperand(add.destination2)) return true; const auto first = scalarSlot(add.destination2); @@ -408,8 +459,9 @@ EmbeddedFetchPlan EmbeddedVertexFetchAnalyzer::Analyze(const RdnaProgram& progra const bool vertexIndexAccumulator = isVectorOperand(inst.destination) && (inst.destination.reg == 0u || (userDataBaseRegister == 8u && inst.destination.reg == kVertexIndexVgpr)); const bool instanceIndexAccumulator = isVectorOperand(inst.destination) && (inst.destination.reg == (userDataBaseRegister == 8u ? kInstanceIndexVgpr : 3u)); std::uint32_t sadZero = 0u; + const bool plainNoCarryAdd = inst.op == RdnaOpcode::VAddNcU32 && inst.family == RdnaInstructionFamily::VOP2 && inst.wordCount == 1u; const bool indexOffsetAdd = (vertexIndexAccumulator || instanceIndexAccumulator) && isScalarOperand(inst.source0) && - ((inst.op == RdnaOpcode::VAddI32 && isVectorOperand(inst.source1) && inst.source1.reg == inst.destination.reg) || + (((inst.op == RdnaOpcode::VAddI32 || plainNoCarryAdd) && isVectorOperand(inst.source1) && inst.source1.reg == inst.destination.reg) || (userDataBaseRegister == 8u && (inst.destination.reg == kVertexIndexVgpr || inst.destination.reg == kInstanceIndexVgpr) && inst.op == RdnaOpcode::VSadU32 && isVectorOperand(inst.source2) && inst.source2.reg == inst.destination.reg && tryConstantOperand(sgprs, inst.source1, sadZero) && sadZero == 0u)); @@ -604,9 +656,41 @@ EmbeddedFetchPlan EmbeddedVertexFetchAnalyzer::Analyze(const RdnaProgram& progra plan.instanceOffsetConflict = instanceOffsetConflict; const auto shared = [&](std::int32_t sgpr, const std::vector& addPcs) { if (sgpr < 0) return false; + const auto reg = static_cast(sgpr); + const auto isAdd = [&](const RdnaInstruction& inst) { return std::find(addPcs.begin(), addPcs.end(), inst.programCounter) != addPcs.end(); }; + const auto loopsBack = [&](std::size_t from, std::uint32_t pc) { + for (std::size_t i = from; i < program.instructions.size(); i++) { + const auto& inst = program.instructions[i]; + if (IsDirectBranchOpcode(inst.op) && inst.branchTarget <= pc) return true; + if (inst.op != RdnaOpcode::SEndpgm && !IsDirectBranchOpcode(inst.op) && leavesStraightLine(inst)) return true; + } + return false; + }; + std::uint32_t skippedUntil = 0u; + for (std::size_t i = 0; i < program.instructions.size(); i++) { + const auto& inst = program.instructions[i]; + if (isAdd(inst)) continue; + if (inst.op == RdnaOpcode::SMovrelsB32 || inst.op == RdnaOpcode::SMovrelsB64 || inst.op == RdnaOpcode::SMovrelsd2B32 || inst.op == RdnaOpcode::SMovreldB32 || inst.op == RdnaOpcode::SMovreldB64) return true; + if (readsSgprRange(inst, reg, 1u)) return true; + if (killsSgpr(inst, reg)) { + if (inst.programCounter < skippedUntil) continue; + return std::any_of(addPcs.begin(), addPcs.end(), [&](std::uint32_t pc) { return pc > inst.programCounter; }) || loopsBack(i + 1u, inst.programCounter); + } + if (touchesSgpr(inst, reg)) return true; + if (IsDirectBranchOpcode(inst.op)) { + if (inst.branchTarget <= inst.programCounter) break; + skippedUntil = std::max(skippedUntil, inst.branchTarget); + continue; + } + if (inst.op == RdnaOpcode::SEndpgm) { + if (inst.programCounter >= skippedUntil) return false; + continue; + } + if (leavesStraightLine(inst)) break; + } for (const auto& inst : program.instructions) { - if (std::find(addPcs.begin(), addPcs.end(), inst.programCounter) != addPcs.end()) continue; - if (touchesSgpr(inst, static_cast(sgpr))) return true; + if (isAdd(inst)) continue; + if (touchesSgpr(inst, reg)) return true; } return false; }; @@ -622,7 +706,7 @@ EmbeddedFetchPlan EmbeddedVertexFetchAnalyzer::Analyze(const RdnaProgram& progra if (add == program.instructions.end() || add->destination.reg != indexVgpr || add->destination.clamp || add->clampResult) return true; if (std::any_of(program.instructions.begin(), program.instructions.end(), [&](const RdnaInstruction& inst) { return inst.programCounter != add->programCounter && writesSgprMask(inst, static_cast(sgpr), waveSize); })) return true; const auto reg = add->destination.reg; - if (std::any_of(program.instructions.begin(), add, [&](const RdnaInstruction& inst) { return writesExec(inst) || mayReachVgpr(inst, reg); })) return true; + if (std::any_of(program.instructions.begin(), add, [&](const RdnaInstruction& inst) { return (writesExec(inst) && !setsExecAllOnes(inst)) || mayReachVgpr(inst, reg); })) return true; return carryObserved(program, static_cast(add - program.instructions.begin()), waveSize); }; plan.vertexIndexObserved = observed(plan.vertexOffsetSgpr, vertexAddPcs, kVertexIndexVgpr); diff --git a/core/shader/recompiler/Translation/src/Float64Instructions.cpp b/core/shader/recompiler/Translation/src/Float64Instructions.cpp index 00a0bac5b3..1ae767d2af 100644 --- a/core/shader/recompiler/Translation/src/Float64Instructions.cpp +++ b/core/shader/recompiler/Translation/src/Float64Instructions.cpp @@ -10,18 +10,24 @@ IrU64 TranslationContext::readF64(const RdnaOperand& operand) { } bool TranslationContext::float64Operation(const RdnaInstruction& inst, IrOpcode opcode) { + const bool arithmetic = opcode == IrOpcode::FPAdd64 || opcode == IrOpcode::FPMul64; const std::size_t count = IrOpcodeOperandCount(opcode); std::array args{}; for (std::uint32_t index = 0u; index < count; ++index) { const RdnaOperand& operand = sourceAt(inst, index); switch (IrOpcodeArgumentType(opcode, index)) { - case IrType::U64: args[index] = &readF64(operand).Value(); break; + case IrType::U64: { + const std::array bits = opcode == IrOpcode::FPFrexpExp64 || arithmetic ? flushF64Bits(readF64Bits(operand), 0x40u) : readF64Bits(operand); + args[index] = &ir.ConstructU64(bits[0].Value(), bits[1].Value()); + break; + } case IrType::F32: args[index] = readOperand(operand, IrType::F32); break; default: args[index] = &readU32(operand).Value(); break; } } const IrType type = IrOpcodeType(opcode); IrValue* result = count == 1u ? &ir.Emit(opcode, type, {args[0]}) : count == 2u ? &ir.Emit(opcode, type, {args[0], args[1]}) : &ir.Emit(opcode, type, {args[0], args[1], args[2]}); + if (arithmetic) result = &flushTinyF64Result(opcode, *args[0], *args[1], *result); if (type == IrType::U64) { writeF64Result(inst.destination, *result); return true; @@ -33,13 +39,35 @@ bool TranslationContext::float64Operation(const RdnaInstruction& inst, IrOpcode return true; } -bool TranslationContext::nonIeeeMinMaxF64(const RdnaInstruction& inst, IrOpcode opcode) { - const std::array, 2> bits{readF64Bits(sourceAt(inst, 0u)), readF64Bits(sourceAt(inst, 1u))}; +IrValue& TranslationContext::flushTinyF64Result(IrOpcode opcode, IrValue& lhs, IrValue& rhs, IrValue& result) { + if (!floatMode.has_value() || (floatMode->floatMode & 0x80u) != 0u) return result; + const bool product = opcode == IrOpcode::FPMul64; + IrValue& tested = product ? ir.Emit(IrOpcode::FPMul64, IrType::U64, {&ir.Emit(IrOpcode::FPMul64, IrType::U64, {&lhs, &ir.ConstantU64(0x43f0000000000000ull)}), &rhs}) : result; + IrValue& magnitude = ir.Emit(IrOpcode::BitwiseAnd64, IrType::U64, {&tested, &ir.ConstantU64(0x7fffffffffffffffull)}); + IrValue& below = ir.Emit(IrOpcode::ISub64, IrType::U64, {&magnitude, &ir.ConstantU64(1u)}); + IrValue& tiny = ir.Emit(IrOpcode::ULessThan64, IrType::U1, {&below, &ir.ConstantU64(product ? 0x040fffffffffffffull : 0x000fffffffffffffull)}); + IrValue& sign = ir.BitwiseAnd(ir.CompositeExtract(result, 1u), ir.Constant(0x80000000u)); + return ir.ConstructU64(ir.Select(tiny, ir.Constant(0u), ir.CompositeExtract(result, 0u)), ir.Select(tiny, sign, ir.CompositeExtract(result, 1u))); +} + +bool TranslationContext::minMaxF64(const RdnaInstruction& inst, IrOpcode opcode) { + std::array, 2> bits{readF64Bits(sourceAt(inst, 0u)), readF64Bits(sourceAt(inst, 1u))}; + if (floatMode.has_value() && (floatMode->floatMode & 0x40u) == 0u) { + for (auto& value : bits) { + const IrU1 zeroExponent(ir.IEqual(ir.BitwiseAnd(value[1].Value(), ir.Constant(0x7ff00000u)), ir.Constant(0u))); + value[0] = IrU32(ir.Select(zeroExponent.Value(), ir.Constant(0u), value[0].Value())); + value[1] = IrU32(ir.Select(zeroExponent.Value(), ir.BitwiseAnd(value[1].Value(), ir.Constant(0x80000000u)), value[1].Value())); + } + } const auto isNan = [&](const std::array& value) { IrValue& magnitude = ir.BitwiseAnd(value[1].Value(), ir.Constant(0x7fffffffu)); return &ir.LogicalOr(ir.UGreaterThan(magnitude, ir.Constant(0x7ff00000u)), ir.LogicalAnd(ir.IEqual(magnitude, ir.Constant(0x7ff00000u)), ir.INotEqual(value[0].Value(), ir.Constant(0u)))); }; IrValue& plain = ir.Emit(opcode, IrType::U64, {&ir.ConstructU64(bits[0][0].Value(), bits[0][1].Value()), &ir.ConstructU64(bits[1][0].Value(), bits[1][1].Value())}); + if (ieeeMode) { + writeF64Result(inst.destination, plain); + return true; + } IrValue* lhsNan = isNan(bits[0]); IrValue* rhsNan = isNan(bits[1]); std::array words{}; @@ -52,14 +80,17 @@ bool TranslationContext::nonIeeeMinMaxF64(const RdnaInstruction& inst, IrOpcode } bool TranslationContext::float64Unary(const RdnaInstruction& inst, IrOpcode opcode) { - const auto bits = readF64Bits(sourceAt(inst, 0u)); + const bool flushesInput = opcode == IrOpcode::FPFloor64 || opcode == IrOpcode::FPCeil64 || opcode == IrOpcode::FPFract64 || opcode == IrOpcode::FPFrexpMant64; + const auto bits = flushesInput ? flushF64Bits(readF64Bits(sourceAt(inst, 0u)), 0x40u) : readF64Bits(sourceAt(inst, 0u)); IrValue& argument = ir.ConstructU64(bits[0].Value(), bits[1].Value()); IrValue& result = opcode == IrOpcode::FPLdexp64 ? ir.Emit(opcode, IrType::U64, {&argument, &readU32(sourceAt(inst, 1u)).Value()}) : ir.Emit(opcode, IrType::U64, {&argument}); + std::array words{IrU32(ir.CompositeExtract(result, 0u)), IrU32(ir.CompositeExtract(result, 1u))}; + if (opcode == IrOpcode::FPFract64) words = flushF64Bits(words, 0x80u); const IrU32 magnitude(ir.BitwiseAnd(bits[1].Value(), ir.Constant(0x7fffffffu))); const IrU1 nan(ir.LogicalOr(ir.UGreaterThan(magnitude.Value(), ir.Constant(0x7ff00000u)), ir.LogicalAnd(ir.IEqual(magnitude.Value(), ir.Constant(0x7ff00000u)), ir.INotEqual(bits[0].Value(), ir.Constant(0u))))); const auto quiet = quietNan64(bits); - writeF64Result(inst.destination, ir.ConstructU64(ir.Select(nan.Value(), quiet[0].Value(), ir.CompositeExtract(result, 0u)), ir.Select(nan.Value(), quiet[1].Value(), ir.CompositeExtract(result, 1u)))); + writeF64Result(inst.destination, ir.ConstructU64(ir.Select(nan.Value(), quiet[0].Value(), words[0].Value()), ir.Select(nan.Value(), quiet[1].Value(), words[1].Value()))); return true; } @@ -149,9 +180,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 e09885cb3f..0632342acb 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,14 +45,24 @@ 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); }; - const auto minMaxLane = [&](bool high) { return minMaxF16Bits(opcode == IrOpcode::FPMax32, laneBits(sourceAt(inst, 0u), high), laneBits(sourceAt(inst, 1u), high)); }; - const bool native = nativeFloat16 && inst.destination.omod == 0u && (opcode == IrOpcode::FPAdd32 || opcode == IrOpcode::FPMul32); + std::array, 3> sourceBits{}; + std::array, 3> sourceBitsReady{}; + const auto sourceLaneBits = [&](std::uint32_t index, bool high) { + const std::size_t lane = high ? 1u : 0u; + if (!sourceBitsReady[index][lane]) { + const RdnaOperand& operand = accumulator && index == 2u ? accumulatorOperand(inst) : sourceAt(inst, index); + sourceBits[index][lane] = laneBits(operand, high); + sourceBitsReady[index][lane] = true; + } + return sourceBits[index][lane]; + }; + const auto minMaxLane = [&](bool high) { return minMaxF16Bits(opcode == IrOpcode::FPMax32, sourceLaneBits(0u, high), sourceLaneBits(1u, high)); }; + const bool native = nativeFloat16 && !flushResults && inst.destination.omod == 0u && (opcode == IrOpcode::FPAdd32 || opcode == IrOpcode::FPMul32); const auto nativeResult = [&]() { - const auto packed = [&](const RdnaOperand& operand) { return packU16Lanes(laneBits(operand, false), laneBits(operand, true)); }; - const IrU32 lhs = packed(sourceAt(inst, 0u)); - const IrU32 rhs = packed(sourceAt(inst, 1u)); + const IrU32 lhs = packU16Lanes(sourceLaneBits(0u, false), sourceLaneBits(0u, true)); + const IrU32 rhs = packU16Lanes(sourceLaneBits(1u, false), sourceLaneBits(1u, true)); return IrU32(ir.Emit(opcode == IrOpcode::FPAdd32 ? IrOpcode::FPAdd16x2 : IrOpcode::FPMul16x2, IrType::U32, {&lhs.Value(), &rhs.Value()})); }; IrU32 result = quietSnan ? packU16Lanes(minMaxLane(false), minMaxLane(true)) : native ? nativeResult() : packHalf2x16(translateLane(false), translateLane(true)); @@ -67,7 +85,7 @@ bool TranslationContext::packedFloat16(const RdnaInstruction& inst, IrOpcode opc const std::uint32_t count = accumulator ? 3u : inst.sourceCount; std::array bits{}; for (std::uint32_t index = 0u; index < count; ++index) { - bits[index] = laneBits(accumulator && index == 2u ? accumulatorOperand(inst) : sourceAt(inst, index), high); + bits[index] = sourceLaneBits(index, high); } IrU32 lane(high ? ir.ShiftRightLogical(result.Value(), ir.Constant(16u)) : ir.BitwiseAnd(result.Value(), ir.Constant(0xffffu))); if (native && inst.destination.clamp && dx10Clamp()) return lane; @@ -106,7 +124,8 @@ bool TranslationContext::float16Unary(const RdnaInstruction& inst, IrOpcode opco const RdnaOperand& operand = sourceAt(inst, 0u); const IrF32 argument = flushF16InputValue(readF16AsF32(operand)); const auto [result, invalid] = unaryFloatSpecials(opcode, argument, IrF32(ir.Emit(opcode, IrType::F32, {&argument.Value()}))); - IrU32 bits = packHalf2x16(applyF16ResultModifiers(inst.destination, clampF16Overflow(result, {&argument.Value()})), IrF32(ir.ConstantF32(0.0f))); + const IrF16 rounded(ir.Emit(IrOpcode::ConvertF16F32, IrType::F16, {&applyF16ResultModifiers(inst.destination, clampF16Overflow(result, {&argument.Value()})).Value()})); + IrU32 bits(ir.Emit(IrOpcode::ConvertU32U16, IrType::U32, {&ir.Emit(IrOpcode::BitCastU16F16, IrType::U16, {&rounded.Value()})})); if (opcode == IrOpcode::FPFract32) { bits = IrU32(ir.Select(ir.IEqual(bits.Value(), ir.Constant(0x3c00u)), ir.Constant(0x3bffu), bits.Value())); } @@ -203,6 +222,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))); }; @@ -228,20 +250,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; } @@ -250,13 +268,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]}, &invalidOperationF32({args[0], args[1], args[2]}, invalidProductF32(args[0], args[1]))); return true; } if (mix) { @@ -268,6 +294,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()}); @@ -390,8 +435,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; @@ -594,6 +639,12 @@ bool TranslationContext::floatTernary(const RdnaInstruction& inst, IrOpcode opco if (opcode == IrOpcode::FPFma32) result = &flushTinyProduct(args[0], args[1], result, args[2]).Value(); const bool f32Sources = !mix || !(sourceAt(inst, 0u).opSelHi || sourceAt(inst, 1u).opSelHi || (!accumulator && sourceAt(inst, 2u).opSelHi)); if (opcode == IrOpcode::FPFma32 && f32Sources) result = nanResultF32({args[0], args[1], args[2]}, result, &invalidProductF32(args[0], args[1])); + if (opcode == IrOpcode::FPFma32 && !f32Sources) { + IrValue& bits = ir.BitCastU32(*result); + IrValue& nan = ir.UGreaterThan(ir.BitwiseAnd(bits, ir.Constant(0x7fffffffu)), ir.Constant(0x7f800000u)); + IrValue& invalid = invalidOperationF32({args[0], args[1], args[2]}, invalidProductF32(args[0], args[1])); + result = &ir.BitCastF32(ir.Select(ir.LogicalAnd(nan, invalid), ir.Constant(0xffc00000u), bits)); + } writeOperand(inst.destination, result); return true; } @@ -635,10 +686,7 @@ IrU32 TranslationContext::minMaxF16Bits(bool maxValue, IrU32 lhs, IrU32 rhs) { return ir.BitwiseXor(value, ir.BitwiseAnd(ir.ShiftRightArithmetic(value, ir.Constant(31u)), ir.Constant(0x7fffu))); }; const IrU1 ordered(ir.Emit(maxValue ? IrOpcode::SGreaterThanEqual32 : IrOpcode::SLessThan32, IrType::U1, {&order(lhs), &order(rhs)})); - const IrU32 either(ir.BitwiseOr(lhs.Value(), rhs.Value())); - const IrU1 bothZero(ir.IEqual(ir.BitwiseAnd(either.Value(), ir.Constant(0x7fffu)), ir.Constant(0u))); - const IrU32 zero = maxValue ? IrU32(ir.BitwiseAnd(lhs.Value(), rhs.Value())) : either; - return IrU32(ir.Select(bothZero.Value(), zero.Value(), ir.Select(ordered.Value(), lhs.Value(), rhs.Value()))); + return IrU32(ir.Select(ordered.Value(), lhs.Value(), rhs.Value())); } bool TranslationContext::minMaxF16(const RdnaInstruction& inst, IrOpcode opcode) { @@ -735,10 +783,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)))); @@ -763,9 +808,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())); @@ -896,7 +941,13 @@ bool TranslationContext::floatCube(const RdnaInstruction& inst, std::uint32_t re } const IrU32 bits(ir.BitCastU32(result.Value())); const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(0x7f800000u))); - writeOperand(inst.destination, &ir.BitCastF32(ir.Select(nan.Value(), quietNan32(bits).Value(), bits.Value()))); + result = IrF32(ir.BitCastF32(ir.Select(nan.Value(), quietNan32(bits).Value(), bits.Value()))); + RdnaOperand destination = inst.destination; + if (destination.omod != 0u && !ieeeMode) { + result = scaleF32Result(destination.omod, result); + } + destination.omod = 0u; + writeOperand(destination, &result.Value()); return true; } diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 5e108ff580..4a3849fbd6 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -86,6 +86,10 @@ const ShaderWorkgroupInputInfo* shaderWorkgroupInput(ShaderStageKind stage, cons } } +bool isUnlinkedCall(const ControlFlowGraph& cfg, std::uint32_t programCounter) { + return std::find(cfg.unlinkedCallProgramCounters.begin(), cfg.unlinkedCallProgramCounters.end(), programCounter) != cfg.unlinkedCallProgramCounters.end(); +} + bool isCodeTableLoad(const ControlFlowGraph& cfg, std::uint32_t programCounter) { return std::find(cfg.codeTableLoadProgramCounters.begin(), cfg.codeTableLoadProgramCounters.end(), programCounter) != cfg.codeTableLoadProgramCounters.end(); } @@ -184,6 +188,10 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO const auto tessellationBase = [&entryIr](TessellationBaseKind kind) -> IrValue& { return entryIr.Emit(IrOpcode::TessellationBase, IrOpcodeType(IrOpcode::TessellationBase), {&entryIr.Constant(static_cast(kind))}); }; + constexpr std::uint32_t MergedHullRecordBytes = 16u; + const auto mergedHullRecord = [&entryIr](IrValue& address) -> IrValue& { + return entryIr.Emit(IrOpcode::GetTessellationAttribute, IrOpcodeType(IrOpcode::GetTessellationAttribute), {&entryIr.Constant(static_cast(TessellationAttribute::ControlInput)), &address, &entryIr.ConstantBool(true)}); + }; for (std::uint32_t index = 0; index < options.userDataCount; index++) { const auto reg = static_cast(options.userDataBaseRegister + index); @@ -231,7 +239,7 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO const std::uint32_t waves = std::min((totalThreads + waveSize - 1u) / waveSize, 0x3fu); IrValue& localIndex = builtin(StageInputKind::LocalInvocationIndex); IrValue& waveId = entryIr.Emit(IrOpcode::UDiv32, IrOpcodeType(IrOpcode::UDiv32), {&localIndex, &entryIr.Constant(waveSize)}); - IrValue& waveBits = entryIr.ShiftLeftLogical(waveId, entryIr.Constant(20u)); + IrValue& waveBits = entryIr.BitwiseOr(entryIr.ShiftLeftLogical(waveId, entryIr.Constant(6u)), entryIr.ShiftLeftLogical(waveId, entryIr.Constant(20u))); IrValue& firstBit = entryIr.Select(entryIr.IEqual(waveId, entryIr.Constant(0u)), entryIr.Constant(0x80000000u), entryIr.Constant(0u)); entryIr.SetScalarReg(static_cast(cs->workgroupRegister + regOffset), entryIr.BitwiseOr(entryIr.BitwiseOr(waveBits, entryIr.Constant(waves)), firstBit)); } @@ -321,6 +329,13 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO setVector(static_cast(2), drawIndex(StageInputKind::VertexIndex)); setVector(static_cast(3), tessellationBase(TessellationBaseKind::RelativeIndex)); setVector(static_cast(5), drawIndex(StageInputKind::InstanceIndex)); + if (options.inputInfo.vertex->tess.mergedHull) { + const auto kind = static_cast(TessellationAttribute::LocalOutput); + for (const auto [offset, index] : {std::pair{0u, StageInputKind::VertexIndex}, std::pair{4u, StageInputKind::InstanceIndex}}) { + static_cast(entryIr.Emit(IrOpcode::SetTessellationAttribute, IrType::Void, {&entryIr.Constant(kind), &entryIr.Constant(offset), &builtin(index), &entryIr.ConstantBool(true)})); + } + program.Metadata().tessellationLocalStride = MergedHullRecordBytes; + } } else if (options.stage == ShaderStageKind::TessellationControl) { const auto& tess = options.inputInfo.vertex->tess; entryIr.SetScalarReg(static_cast(2), tessellationBase(TessellationBaseKind::OffChip)); @@ -328,6 +343,14 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO entryIr.SetScalarReg(static_cast(3), entryIr.Constant(0x81010000u | tess.inputControlPoints | (tess.outputControlPoints << 8u))); setVector(static_cast(0), builtin(StageInputKind::PrimitiveId)); setVector(static_cast(1), entryIr.BitwiseOr(entryIr.ShiftLeftLogical(builtin(StageInputKind::InvocationId), entryIr.Constant(8u)), tessellationBase(TessellationBaseKind::RelativeIndex))); + if (tess.mergedHull) { + IrValue& point = entryIr.Emit(IrOpcode::UMin32, IrOpcodeType(IrOpcode::UMin32), {&builtin(StageInputKind::InvocationId), &entryIr.Constant(tess.inputControlPoints - 1u)}); + IrValue& record = entryIr.IMul(point, entryIr.Constant(MergedHullRecordBytes)); + setVector(static_cast(2), mergedHullRecord(record)); + setVector(static_cast(3), entryIr.IAdd(entryIr.IMul(tessellationBase(TessellationBaseKind::RelativeIndex), entryIr.Constant(tess.inputControlPoints)), builtin(StageInputKind::InvocationId))); + setVector(static_cast(5), mergedHullRecord(entryIr.IAdd(record, entryIr.Constant(4u)))); + program.Metadata().tessellationLocalStride = MergedHullRecordBytes; + } } else if (options.stage == ShaderStageKind::TessellationEvaluation) { entryIr.SetScalarReg(static_cast(3), entryIr.Constant(64u)); entryIr.SetScalarReg(static_cast(4), tessellationBase(TessellationBaseKind::OffChip)); @@ -340,18 +363,53 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO const auto* ps = options.inputInfo.pixel; const auto vgpr = [&](PixelInput input) { return ps->psInputVgpr[static_cast(input)]; }; const auto loaded = [&](PixelInput input) { return vgpr(input) != ShaderPixelInputInfo::NoPixelInputVgpr; }; - for (const auto [input, kind] : {std::pair{PixelInput::PerspectiveSample, StageInputKind::BaryCoordSmooth}, std::pair{PixelInput::PerspectiveCenter, StageInputKind::BaryCoordSmooth}, - std::pair{PixelInput::PerspectiveCentroid, StageInputKind::BaryCoordSmooth}, std::pair{PixelInput::LinearSample, StageInputKind::BaryCoordNoPerspective}, - std::pair{PixelInput::LinearCenter, StageInputKind::BaryCoordNoPerspective}, std::pair{PixelInput::LinearCentroid, StageInputKind::BaryCoordNoPerspective}}) { + const bool iterated = ps->psIteratesSamples; + program.Metadata().pixelIteratesSamples = iterated; + const bool offsets = options.interpolationFunctions && options.fragmentShaderBarycentricEnabled; + const bool centers = iterated && offsets; + const bool sampleOffsets = offsets && ps->SampleLocationsOffCenter(); + const auto smoothSample = sampleOffsets ? StageInputKind::BaryCoordSmoothSample : StageInputKind::BaryCoordSmooth; + const auto linearSample = sampleOffsets ? StageInputKind::BaryCoordNoPerspectiveSample : StageInputKind::BaryCoordNoPerspective; + const auto smoothCenter = centers ? StageInputKind::BaryCoordSmoothCenter : StageInputKind::BaryCoordSmooth; + const auto linearCenter = centers ? StageInputKind::BaryCoordNoPerspectiveCenter : StageInputKind::BaryCoordNoPerspective; + for (const auto [input, kind] : {std::pair{PixelInput::PerspectiveSample, smoothSample}, std::pair{PixelInput::PerspectiveCenter, smoothCenter}, + std::pair{PixelInput::PerspectiveCentroid, StageInputKind::BaryCoordSmooth}, std::pair{PixelInput::LinearSample, linearSample}, + std::pair{PixelInput::LinearCenter, linearCenter}, std::pair{PixelInput::LinearCentroid, StageInputKind::BaryCoordNoPerspective}}) { if (!loaded(input)) continue; setVector(static_cast(vgpr(input)), builtin(kind, 0u)); setVector(static_cast(vgpr(input) + 1u), builtin(kind, 1u)); } + const auto firstSampleOffset = [&](std::uint32_t component) -> IrValue& { + const auto& locations = ps->sampleLocations; + if (!ps->SampleLocationsVary()) return entryIr.ConstantF32(SampleLocationOffset(locations[0], component)); + IrValue& x = entryIr.Emit(IrOpcode::ConvertU32F32, IrType::U32, {&entryIr.BitCastF32(builtin(StageInputKind::FragCoord, 0u))}); + IrValue& y = entryIr.Emit(IrOpcode::ConvertU32F32, IrType::U32, {&entryIr.BitCastF32(builtin(StageInputKind::FragCoord, 1u))}); + IrValue& quadPixel = entryIr.BitwiseOr(entryIr.BitwiseAnd(x, entryIr.Constant(1u)), entryIr.ShiftLeftLogical(entryIr.BitwiseAnd(y, entryIr.Constant(1u)), entryIr.Constant(1u))); + IrValue* offset = &entryIr.ConstantF32(SampleLocationOffset(locations[24], component)); + for (std::uint32_t pixel = 3u; pixel-- > 0u;) { + offset = &entryIr.Select(entryIr.IEqual(quadPixel, entryIr.Constant(pixel)), entryIr.ConstantF32(SampleLocationOffset(locations[pixel * 8u], component)), *offset); + } + return *offset; + }; + const auto position = [&](std::uint32_t component) -> IrValue& { + IrValue& coordinate = builtin(StageInputKind::FragCoord, component); + const auto location = ps->psPositionLocation; + if (location == PixelPositionLocation::Centroid || (location == PixelPositionLocation::Center && !iterated) || (location == PixelPositionLocation::Sample && (iterated || !ps->SampleLocationsOffCenter()))) { + return coordinate; + } + IrValue& value = entryIr.BitCastF32(coordinate); + if (location == PixelPositionLocation::Sample) { + return entryIr.BitCastU32(entryIr.Emit(IrOpcode::FPAdd32, IrOpcodeType(IrOpcode::FPAdd32), {&value, &firstSampleOffset(component)})); + } + IrValue& corner = entryIr.Emit(IrOpcode::FPFloor32, IrOpcodeType(IrOpcode::FPFloor32), {&value}); + if (location == PixelPositionLocation::UpperLeft) return entryIr.BitCastU32(corner); + return entryIr.BitCastU32(entryIr.Emit(IrOpcode::FPAdd32, IrOpcodeType(IrOpcode::FPAdd32), {&corner, &entryIr.ConstantF32(0.5f)})); + }; if (loaded(PixelInput::PositionX)) { - setVector(static_cast(vgpr(PixelInput::PositionX)), builtin(StageInputKind::FragCoord, 0u)); + setVector(static_cast(vgpr(PixelInput::PositionX)), position(0u)); } if (loaded(PixelInput::PositionY)) { - setVector(static_cast(vgpr(PixelInput::PositionY)), builtin(StageInputKind::FragCoord, 1u)); + setVector(static_cast(vgpr(PixelInput::PositionY)), position(1u)); } if (loaded(PixelInput::PositionZ)) { setVector(static_cast(vgpr(PixelInput::PositionZ)), builtin(StageInputKind::FragCoord, 2u)); @@ -363,11 +421,16 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO setVector(static_cast(vgpr(PixelInput::PositionW)), entryIr.BitCastU32(w)); } if (loaded(PixelInput::FrontFace)) { - setVector(static_cast(vgpr(PixelInput::FrontFace)), builtin(StageInputKind::FrontFacing)); + // The front value under FRONT_FACE_ALL_BITS is not measured (docs/dev/debt/shader.md). + IrValue& facing = builtin(StageInputKind::FrontFacing); + setVector(static_cast(vgpr(PixelInput::FrontFace)), ps->psFrontFaceAllBits ? entryIr.Select(entryIr.IEqual(facing, entryIr.Constant(0x3f800000u)), entryIr.Constant(0xffffffffu), entryIr.Constant(0u)) : facing); } if (loaded(PixelInput::Ancillary)) { setVector(static_cast(vgpr(PixelInput::Ancillary)), builtin(StageInputKind::PackedAncillary)); } + if (loaded(PixelInput::SampleCoverage)) { + setVector(static_cast(vgpr(PixelInput::SampleCoverage)), builtin(StageInputKind::SampleMask)); + } if (loaded(PixelInput::LineStipple)) { setVector(static_cast(vgpr(PixelInput::LineStipple)), entryIr.Constant(0u)); } @@ -383,8 +446,13 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO entryIr.SetScalarReg(static_cast(2), entryIr.Constant((lanes << 12u) | (lanes << 22u))); entryIr.SetScalarReg(static_cast(3), entryIr.Constant(lanes | (lanes << 8u))); } - setVector(static_cast(5), drawIndex(StageInputKind::VertexIndex)); - setVector(static_cast(8), drawIndex(StageInputKind::InstanceIndex)); + if (options.gcnLegacyVertexInputs) { + setVector(static_cast(0), drawIndex(StageInputKind::VertexIndex)); + for (std::uint32_t reg = 1u; reg <= 3u; reg++) setVector(static_cast(reg), drawIndex(StageInputKind::InstanceIndex)); + } else { + setVector(static_cast(5), drawIndex(StageInputKind::VertexIndex)); + setVector(static_cast(8), drawIndex(StageInputKind::InstanceIndex)); + } } } @@ -392,6 +460,9 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const ControlFlowGraph& cfg, const TranslateOptions& options) const { validateTranslateOptions(options); + if (cfg.linkEscapeWaveSize == options.waveSize) { + throw std::invalid_argument(cfg.linkEscapeReason); + } if (cfg.blocks.empty()) { throw std::runtime_error("cannot translate an empty control flow graph"); } @@ -518,12 +589,15 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con } } + const auto entryRounding = ComputeEntryRounding(cfg, decoded, options.floatMode.has_value() ? options.floatMode->floatMode : 0u); for (const auto& cfgBlock : cfg.blocks) { const auto typedIndex = blockIndices.at(cfgBlock.id); TranslationContext context(program, *blocks[typedIndex], vectorLimit); context.SetPixelInput(options.inputInfo.pixel, options.fragmentShaderBarycentricEnabled); context.SetFloatMode(options.floatMode); context.SetNativeFloat16(options.nativeFloat16); + context.SetFloat32FlushToZero(options.float32FlushToZero); + context.SetEntryRounding(entryRounding.at(cfgBlock.id)); for (std::uint32_t index = cfgBlock.instructionBegin; index < cfgBlock.instructionEnd; index++) { const auto& instruction = decoded.instructions[index]; if (isCodeTableLoad(cfg, instruction.programCounter)) { @@ -532,6 +606,10 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con context.TranslateCodeTableLoad(instruction, *table); continue; } + if (isUnlinkedCall(cfg, instruction.programCounter)) { + context.TranslateUnlinkedCall(instruction); + continue; + } if (options.embeddedFetch != nullptr) { const auto load = std::ranges::find(options.embeddedFetch->loads, instruction.programCounter, &EmbeddedFetchLoad::programCounter); if (load != options.embeddedFetch->loads.end()) { 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/MemoryInstructions/ImageInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp index f29c3029b9..c20f9b798c 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp @@ -123,7 +123,7 @@ bool TranslationContext::imageGetResinfo(const RdnaInstruction& inst) { const MemoryInfo memory = imageMemoryInfoFromInstruction(inst); IrValue* resource = getImageResource(memory); IrValue* address = makeImageAddress(inst, inst.source0); - IrValue& result = ir.Emit(IrOpcode::ImageQueryDimensions, IrOpcodeType(IrOpcode::ImageQueryDimensions), {resource, address}, addMemoryInfo(memory, inst.programCounter)); + IrValue& result = ir.Emit(IrOpcode::ImageQueryDimensions, IrOpcodeType(IrOpcode::ImageQueryDimensions), {resource, address, &ir.GetExec()}, addMemoryInfo(memory, inst.programCounter)); writeImageComponents(inst.destination, &result, memory, 4u); return true; } @@ -133,7 +133,7 @@ bool TranslationContext::imageGetLod(const RdnaInstruction& inst) { IrValue* resource = getImageResource(memory); IrValue* sampler = getSamplerResource(memory); IrValue* address = makeImageAddress(inst, inst.source0); - IrValue& result = ir.Emit(IrOpcode::ImageQueryLod, IrOpcodeType(IrOpcode::ImageQueryLod), {resource, sampler, address}, addMemoryInfo(memory, inst.programCounter)); + IrValue& result = ir.Emit(IrOpcode::ImageQueryLod, IrOpcodeType(IrOpcode::ImageQueryLod), {resource, sampler, address, &ir.GetExec()}, addMemoryInfo(memory, inst.programCounter)); writeImageComponents(inst.destination, &result, memory, 2u); return true; } @@ -203,7 +203,7 @@ bool TranslationContext::imageSample(const RdnaInstruction& inst) { IrValue* resource = getImageResource(memory); IrValue* sampler = getSamplerResource(memory); IrValue* address = makeImageAddress(inst, inst.source0); - IrValue& result = ir.Emit(IrOpcode::ImageSampleRaw, IrOpcodeType(IrOpcode::ImageSampleRaw), {resource, sampler, address}, addMemoryInfo(memory, inst.programCounter)); + IrValue& result = ir.Emit(IrOpcode::ImageSampleRaw, IrOpcodeType(IrOpcode::ImageSampleRaw), {resource, sampler, address, &ir.GetExec()}, addMemoryInfo(memory, inst.programCounter)); writeImageComponents(inst.destination, &result, memory, 4u); return true; } @@ -216,7 +216,7 @@ bool TranslationContext::imageGather(const RdnaInstruction& inst) { IrValue* resource = getImageResource(memory); IrValue* sampler = getSamplerResource(memory); IrValue* address = makeImageAddress(inst, inst.source0); - IrValue& result = ir.Emit(IrOpcode::ImageGatherRaw, IrOpcodeType(IrOpcode::ImageGatherRaw), {resource, sampler, address}, addMemoryInfo(memory, inst.programCounter)); + IrValue& result = ir.Emit(IrOpcode::ImageGatherRaw, IrOpcodeType(IrOpcode::ImageGatherRaw), {resource, sampler, address, &ir.GetExec()}, addMemoryInfo(memory, inst.programCounter)); for (std::uint32_t index = 0; index < memory.dataDwords; ++index) { writeOperand(offsetOperand(inst.destination, index), &ir.CompositeExtract(result, index)); } diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp index 276a3fbae4..b0680c202a 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp @@ -502,7 +502,7 @@ bool TranslationContext::emitMemory(const RdnaInstruction& inst) { case RdnaOpcode::DsGwsBarrier: throw std::runtime_error(std::string(globalWaveSyncName(inst.op)) + " at pc " + std::to_string(inst.programCounter) + ": global wave sync barriers and semaphores shared by waves of different workgroups are not modeled"); case RdnaOpcode::DsOrderedCount: - throw std::runtime_error("ds_ordered_count at pc " + std::to_string(inst.programCounter) + ": GDS counters updated in wave launch order are not modeled"); + return dsOrderedCount(inst); case RdnaOpcode::DsWriteAddtidB32: return dsAddtid(inst, true); case RdnaOpcode::DsReadAddtidB32: diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp index 04115cbb32..a781713ddc 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp @@ -33,6 +33,7 @@ MemoryInfo sharedMemoryInfoFromInstruction(const RdnaInstruction& inst) { bool TranslationContext::dsAtomic(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue) { MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); memory.flushDenormals = sharedFloatFlush(opcode); + memory.flushDenormalResults = opcode == IrOpcode::SharedAtomicFAdd32 && (!floatMode.has_value() || ((floatMode->floatMode >> 5u) & 1u) == 0u); const IrU32 address = readU32(inst.source0); const IrU32 value = readU32(inst.source1); IrValue& active = ir.GetExec(); @@ -217,6 +218,7 @@ bool TranslationContext::sharedFloatFlush(IrOpcode opcode) const { case IrOpcode::SharedAtomicFMin32: case IrOpcode::SharedAtomicFMax32: case IrOpcode::SharedAtomicCmpstF32: + case IrOpcode::SharedAtomicFAdd32: return !floatMode.has_value() || ((floatMode->floatMode >> 4u) & 1u) == 0u; case IrOpcode::SharedAtomicFMin64: case IrOpcode::SharedAtomicFMax64: @@ -293,6 +295,24 @@ bool TranslationContext::dsAppendConsume(const RdnaInstruction& inst, IrOpcode o return true; } +bool TranslationContext::dsOrderedCount(const RdnaInstruction& inst) { + const std::uint32_t flags = (inst.memoryOffset >> 8u) & 0xffu; + const auto location = "ds_ordered_count at pc " + std::to_string(inst.programCounter); + if (!inst.gds) throw std::runtime_error(location + " without GDS is not modeled"); + if (((flags >> 6u) & 3u) != 0u) throw std::runtime_error(location + ": counting more than one dword is not modeled"); + MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); + memory.offset = inst.memoryOffset & 0xffu; + IrValue& address = ir.Constant(0u); + const IrU32 amount = readU32(inst.source0); + IrValue& active = ir.GetExec(); + IrValue& activeLo = ir.GetExecLo(); + IrValue& activeHi = ir.GetExecHi(); + const auto opcode = (flags & 0x10u) != 0u ? IrOpcode::DataOrderedSwap : IrOpcode::DataOrderedCount; + IrValue& result = ir.Emit(opcode, IrOpcodeType(opcode), {&address, &active, &activeLo, &activeHi, &amount.Value()}, addMemoryInfo(memory, inst.programCounter)); + writeOperand(inst.destination, &result); + return true; +} + bool TranslationContext::dsAddtid(const RdnaInstruction& inst, bool write) { const MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); IrValue& base = ir.BitwiseAnd(readU32(makeM0Operand()).Value(), ir.Constant(0xffffu)); diff --git a/core/shader/recompiler/Translation/src/RoundingMode.cpp b/core/shader/recompiler/Translation/src/RoundingMode.cpp new file mode 100644 index 0000000000..0edeca52f0 --- /dev/null +++ b/core/shader/recompiler/Translation/src/RoundingMode.cpp @@ -0,0 +1,236 @@ +#include "Translation/RoundingMode.hpp" +#include "Translation/TranslationContext.hpp" +#include +#include +#include + +namespace ShaderRecompiler { + +namespace { + +constexpr std::uint32_t ModeRegisterId = 1u; + +struct ModeField { + std::uint32_t mask = 0; + std::uint32_t offset = 0; + std::uint32_t size = 0; +}; + +std::optional roundingField(std::uint32_t simm16) { + if ((simm16 & 0x3fu) != ModeRegisterId) return std::nullopt; + const std::uint32_t offset = (simm16 >> 6u) & 0x1fu; + const std::uint32_t size = ((simm16 >> 11u) & 0x1fu) + 1u; + if (offset + size > 4u) return std::nullopt; + return ModeField{((1u << size) - 1u) << offset, offset, size}; +} + +RoundingState transfer(RoundingState state, const BasicBlock& block, const RdnaProgram& program) { + for (std::uint32_t index = block.instructionBegin; index < block.instructionEnd; ++index) { + if (const auto write = DecodeRoundingWrite(program.instructions.at(index)); write.has_value()) { + state = ApplyRoundingWrite(state, *write); + } + } + return state; +} + +bool setsDirectedRounding(std::uint32_t value, std::uint32_t initial, std::uint32_t fields) { + for (const std::uint32_t group : {ModeRound32Bits, ModeRound1664Bits}) { + const std::uint32_t used = fields & group; + if (used != 0u && (value & group) != (initial & group) && (value & group) != 0u) return true; + } + return false; +} + +} + +std::optional DecodeRoundingWrite(const RdnaInstruction& instruction) { + switch (instruction.op) { + case RdnaOpcode::SRoundMode: + return RoundingWrite{ModeRoundBits, instruction.source0.value & ModeRoundBits, true}; + case RdnaOpcode::SSetregImm32B32: { + const auto field = roundingField(instruction.source1.value); + if (!field.has_value()) return std::nullopt; + const std::uint32_t written = instruction.source0.value & ((1u << field->size) - 1u); + return RoundingWrite{field->mask, written << field->offset, true}; + } + case RdnaOpcode::SSetregB32: { + const auto field = roundingField(instruction.source1.value); + if (!field.has_value()) return std::nullopt; + return RoundingWrite{field->mask, 0u, false}; + } + default: + return std::nullopt; + } +} + +RoundingState ApplyRoundingWrite(RoundingState state, const RoundingWrite& write) { + const auto mask = static_cast(write.mask & ModeRoundBits); + if (write.known) { + state.value = static_cast((state.value & ~mask) | (write.value & mask)); + state.unknown = static_cast(state.unknown & ~mask); + } else { + state.value = static_cast(state.value & ~mask); + state.unknown = static_cast(state.unknown | mask); + } + return state; +} + +RoundingState JoinRoundingStates(RoundingState lhs, RoundingState rhs) { + RoundingState joined; + joined.unknown = static_cast(lhs.unknown | rhs.unknown | (lhs.value ^ rhs.value)); + joined.value = static_cast(lhs.value & ~joined.unknown & ModeRoundBits); + return joined; +} + +RoundingUse RoundingUseOf(RdnaOpcode opcode) { + constexpr std::uint32_t round32 = ModeRound32Bits; + constexpr std::uint32_t round1664 = ModeRound1664Bits; + switch (opcode) { + case RdnaOpcode::VCvtF32I32: + case RdnaOpcode::VCvtF32U32: + case RdnaOpcode::VLdexpF32: + return {round32, RoundingUseKind::Directed32}; + case RdnaOpcode::VCvtF16F32: + case RdnaOpcode::VCvtF16U16: + case RdnaOpcode::VCvtF16I16: + case RdnaOpcode::VLdexpF16: + return {round1664, RoundingUseKind::Directed16}; + case RdnaOpcode::VMadMixloF16: + case RdnaOpcode::VMadMixhiF16: + case RdnaOpcode::VInterpP1llF16: + case RdnaOpcode::VInterpP1lvF16: + case RdnaOpcode::VInterpP2F16: + return {ModeRoundBits, RoundingUseKind::SelfChecked}; + case RdnaOpcode::VPkAddF16: + case RdnaOpcode::VPkMulF16: + case RdnaOpcode::VPkFmaF16: + case RdnaOpcode::VPkFmacF16: + return {round1664, RoundingUseKind::SelfChecked}; + case RdnaOpcode::VAddF32: + case RdnaOpcode::VSubF32: + case RdnaOpcode::VSubrevF32: + case RdnaOpcode::VMulF32: + case RdnaOpcode::VMulLegacyF32: + case RdnaOpcode::VMadF32: + case RdnaOpcode::VMacF32: + case RdnaOpcode::VMadLegacyF32: + case RdnaOpcode::VMacLegacyF32: + case RdnaOpcode::VMullitF32: + case RdnaOpcode::VFmaF32: + case RdnaOpcode::VMadmkF32: + case RdnaOpcode::VMadakF32: + case RdnaOpcode::VFractF32: + case RdnaOpcode::VCvtPkU8F32: + case RdnaOpcode::VCvtF32F64: + case RdnaOpcode::VDivScaleF32: + case RdnaOpcode::VDivFmasF32: + case RdnaOpcode::VDivFixupF32: + case RdnaOpcode::VCubemaF32: + case RdnaOpcode::VDot2F32F16: + case RdnaOpcode::VDot2cF32F16: + case RdnaOpcode::VInterpP1F32: + case RdnaOpcode::VInterpP2F32: + return {round32, RoundingUseKind::Unmodelled}; + case RdnaOpcode::VAddF16: + case RdnaOpcode::VSubF16: + case RdnaOpcode::VSubrevF16: + case RdnaOpcode::VMulF16: + case RdnaOpcode::VFmacF16: + case RdnaOpcode::VFmamkF16: + case RdnaOpcode::VFmaakF16: + case RdnaOpcode::VFmaF16: + case RdnaOpcode::VDivFixupF16: + case RdnaOpcode::VFractF16: + case RdnaOpcode::VFmaF64: + case RdnaOpcode::VDivFmasF64: + case RdnaOpcode::VAddF64: + case RdnaOpcode::VMulF64: + case RdnaOpcode::VLdexpF64: + case RdnaOpcode::VFractF64: + return {round1664, RoundingUseKind::Unmodelled}; + default: + return {}; + } +} + +std::unordered_map ComputeEntryRounding(const ControlFlowGraph& cfg, const RdnaProgram& program, std::uint32_t initial) { + std::unordered_map entries; + if (cfg.blocks.empty()) return entries; + std::unordered_map blocks; + for (const auto& block : cfg.blocks) blocks.emplace(block.id, &block); + + RoundingState start; + start.value = static_cast(initial & ModeRoundBits); + entries.emplace(cfg.blocks.front().id, start); + std::deque pending{cfg.blocks.front().id}; + while (!pending.empty()) { + const std::uint32_t id = pending.front(); + pending.pop_front(); + const BasicBlock& block = *blocks.at(id); + const RoundingState exit = transfer(entries.at(id), block, program); + for (const std::uint32_t successor : block.successors) { + const auto target = blocks.find(successor); + if (target == blocks.end()) throw std::runtime_error("control flow graph block " + std::to_string(id) + " has unknown successor " + std::to_string(successor)); + const auto existing = entries.find(successor); + if (existing == entries.end()) { + entries.emplace(successor, exit); + pending.push_back(successor); + continue; + } + const RoundingState joined = JoinRoundingStates(existing->second, exit); + if (joined == existing->second) continue; + existing->second = joined; + pending.push_back(successor); + } + } + for (const auto& block : cfg.blocks) entries.emplace(block.id, start); + return entries; +} + +void TranslationContext::setRounding(const RoundingState& state) { + rounding = state; + if (!declaredFloatMode.has_value()) return; + ShaderFloatMode mode = *declaredFloatMode; + mode.floatMode = (mode.floatMode & ~ModeRoundBits) | (state.value & ModeRoundBits); + floatMode = mode; +} + +void TranslationContext::writeRounding(const RdnaInstruction& inst) { + const auto write = DecodeRoundingWrite(inst); + const char* name = inst.op == RdnaOpcode::SRoundMode ? "s_round_mode" : inst.op == RdnaOpcode::SSetregB32 ? "s_setreg_b32" : "s_setreg_imm32_b32"; + if (!write.has_value()) { + throw std::runtime_error(std::string(name) + " at pc " + std::to_string(inst.programCounter) + " writes a hardware register field other than the MODE rounding fields"); + } + const RoundingState next = ApplyRoundingWrite(rounding, *write); + if (!declaredFloatMode.has_value() && (next.value != 0u || next.unknown != 0u)) { + throw std::runtime_error(std::string(name) + " at pc " + std::to_string(inst.programCounter) + " changes the rounding fields of a shader without a declared FLOAT_MODE: not implemented"); + } + setRounding(next); +} + +void TranslationContext::checkRoundingUse(const RdnaInstruction& inst) const { + const RoundingUse use = RoundingUseOf(inst.op); + if (use.fields == 0u) return; + const std::string where = "instruction (opcode " + std::to_string(inst.opcodeId) + ") at pc " + std::to_string(inst.programCounter); + if ((rounding.unknown & use.fields) != 0u) { + throw std::runtime_error(where + " reads MODE rounding bits that differ between the paths reaching it or were written from an SGPR: not implemented"); + } + const std::uint32_t initial = declaredFloatMode.has_value() ? declaredFloatMode->floatMode : 0u; + const std::uint32_t changed = (rounding.value ^ initial) & use.fields; + if (changed == 0u) return; + switch (use.kind) { + case RoundingUseKind::Directed32: + case RoundingUseKind::SelfChecked: + return; + case RoundingUseKind::Directed16: + if (((declaredFloatMode.has_value() ? declaredFloatMode->floatMode : 0xc0u) & 0xc0u) != 0xc0u) { + throw std::runtime_error(where + " runs with an f16 rounding mode set at run time and f16 denormals not kept: not implemented"); + } + return; + case RoundingUseKind::Unmodelled: + if (!setsDirectedRounding(rounding.value, initial, use.fields)) return; + throw std::runtime_error(where + " runs with MODE rounding " + std::to_string(rounding.value) + " set at run time, and the instruction does not model directed rounding"); + } +} + +} diff --git a/core/shader/recompiler/Translation/src/ScalarInstructions.cpp b/core/shader/recompiler/Translation/src/ScalarInstructions.cpp index 5762f88ccf..c1b8ccf0b6 100644 --- a/core/shader/recompiler/Translation/src/ScalarInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ScalarInstructions.cpp @@ -1,4 +1,5 @@ #include "Translation/ScalarInstructions.hpp" +#include "Translation/RoundingMode.hpp" #include "Translation/TranslationContext.hpp" #include #include @@ -87,7 +88,7 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { return true; case RdnaOpcode::SSetregB32: if ((inst.source1.value & 0x3fu) == 1u) { - throw std::runtime_error("s_setreg_b32 at pc " + std::to_string(inst.programCounter) + " writes MODE from an SGPR: runtime MODE changes are not implemented"); + writeRounding(inst); } emitControlNop(); return true; @@ -95,17 +96,7 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { emitControlNop(); return true; case RdnaOpcode::SSetregImm32B32: { - const std::uint32_t field = inst.source1.value; - const std::uint32_t offset = (field >> 6u) & 0x1fu; - const std::uint32_t size = ((field >> 11u) & 0x1fu) + 1u; - const std::uint64_t written = static_cast(inst.source0.value) & ((std::uint64_t{1} << size) - 1u); - if ((field & 0x3fu) != 1u || offset + size > 4u) { - throw std::runtime_error("s_setreg_imm32_b32 at pc " + std::to_string(inst.programCounter) + " writes a hardware register field other than the MODE rounding fields"); - } - const std::uint32_t initial = floatMode.has_value() ? floatMode->floatMode : 0u; - if (written != ((initial >> offset) & ((1u << size) - 1u))) { - throw std::runtime_error("s_setreg_imm32_b32 at pc " + std::to_string(inst.programCounter) + " changes the initial MODE rounding fields: runtime MODE changes are not implemented"); - } + writeRounding(inst); emitControlNop(); return true; } @@ -116,8 +107,11 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { if ((field & 0x3fu) != 1u || offset + size > 4u) { throw std::runtime_error("s_getreg_b32 at pc " + std::to_string(inst.programCounter) + " reads hardware register " + std::to_string(field & 0x3fu) + " bits " + std::to_string(offset) + ".." + std::to_string(offset + size - 1u) + ": only the MODE round mode fields are modeled"); } - const std::uint32_t initial = floatMode.has_value() ? floatMode->floatMode : 0u; - writeRawU32(inst.destination, IrU32(ir.Constant((initial >> offset) & ((1u << size) - 1u)))); + const std::uint32_t mask = ((1u << size) - 1u) << offset; + if ((rounding.unknown & mask) != 0u) { + throw std::runtime_error("s_getreg_b32 at pc " + std::to_string(inst.programCounter) + " reads MODE rounding bits that differ between the paths reaching it or were written from an SGPR: not implemented"); + } + writeRawU32(inst.destination, IrU32(ir.Constant((rounding.value >> offset) & (mask >> offset)))); return true; } case RdnaOpcode::SCmovkI32: { @@ -453,14 +447,10 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { case RdnaOpcode::SDecperflevel: emitControlNop(); return true; - case RdnaOpcode::SRoundMode: { - const std::uint32_t initial = floatMode.has_value() ? floatMode->floatMode : 0u; - if ((inst.source0.value & 0xfu) != (initial & 0xfu)) { - throw std::runtime_error("s_round_mode " + std::to_string(inst.source0.value) + " at pc " + std::to_string(inst.programCounter) + " changes the initial MODE rounding fields: runtime MODE changes are not implemented"); - } + case RdnaOpcode::SRoundMode: + writeRounding(inst); emitControlNop(); return true; - } case RdnaOpcode::SDenormMode: throw std::runtime_error("s_denorm_mode " + std::to_string(inst.source0.value) + " at pc " + std::to_string(inst.programCounter) + ": the recompiler does not model denormal modes"); case RdnaOpcode::SSethalt: diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 3e1f574040..f1f7de5a92 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -61,6 +61,18 @@ void _detectVertexBuffers(ShaderVertexInputInfo& info) { } } +void _validateTessellation(const TessellationConfiguration* tessellation) { + if (tessellation == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: tessellation configuration is missing"); + if (tessellation->inputControlPoints == 0u || tessellation->inputControlPoints > 32u || tessellation->outputControlPoints == 0u || tessellation->outputControlPoints > 32u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation control-point counts"); + if ((tessellation->domain != 1u && tessellation->domain != 2u) || (tessellation->partitioning != 0u && tessellation->partitioning != 2u && tessellation->partitioning != 3u) || (tessellation->outputTopology != 2u && tessellation->outputTopology != 3u)) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration"); + if (tessellation->patchesPerGroup == 0u || tessellation->patchesPerGroup > 256u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation patches per group"); +} + +void _validateMesh(const MeshConfiguration* mesh) { + if (mesh == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: a mesh-stage program has no mesh configuration"); + if (mesh->threadsPerGroup == 0u || mesh->maxVertices == 0u || mesh->maxPrimitives == 0u || (mesh->outputPrimitiveSize != 1u && mesh->outputPrimitiveSize != 2u && mesh->outputPrimitiveSize != 3u) || mesh->provokingVertex >= mesh->outputPrimitiveSize) throw std::runtime_error("ShaderInputInfoBuilder: invalid mesh configuration"); +} + } ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const GuestContext& context, std::uint32_t hostSubgroupSize, const MeshConfiguration* mesh, const TessellationConfiguration* tessellation) { @@ -123,6 +135,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues place(PixelInput::PositionW, pixel.posW); place(PixelInput::FrontFace, pixel.frontFace); place(PixelInput::Ancillary, pixel.ancillary); + place(PixelInput::SampleCoverage, (pixel.inputAddr & PixelInputBit(PixelInput::SampleCoverage)) != 0u); place(PixelInput::LineStipple, (pixel.inputAddr & PixelInputBit(PixelInput::LineStipple)) != 0u); place(PixelInput::PositionFixedPoint, (pixel.inputAddr & PixelInputBit(PixelInput::PositionFixedPoint)) != 0u); for (std::uint32_t i = 0; i < 8; ++i) { @@ -133,12 +146,14 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psPosZ = pixel.posZ; pixelStorage.psPosW = pixel.posW; pixelStorage.psFrontFace = pixel.frontFace; + pixelStorage.psFrontFaceAllBits = pixel.frontFaceAllBits; pixelStorage.psAncillary = pixel.ancillary; pixelStorage.psNoPerspective = pixel.noPerspective; pixelStorage.psPixelKillEnable = pixel.pixelKillEnable; pixelStorage.psDepthExportEnable = pixel.depthExportEnable; pixelStorage.psSampleMaskExportEnable = pixel.sampleMaskExportEnable; pixelStorage.psStencilRefExportEnable = pixel.stencilRefExportEnable; + pixelStorage.psStencilOperationValueExport = pixel.stencilOperationValueExport; pixelStorage.psSampleShading = pixel.sampleShading; pixelStorage.psEarlyZ = pixel.earlyZ; pixelStorage.psExecuteOnNoop = pixel.executeOnNoop; @@ -147,6 +162,12 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues if (pixel.ditheredAlphaToCoverage && !pixel.alphaToCoverage) throw std::runtime_error("dithered alpha-to-coverage requires enabled coverage"); pixelStorage.psAlphaToCoverage = pixel.alphaToCoverage; pixelStorage.psDitheredAlphaToCoverage = pixel.ditheredAlphaToCoverage; + pixelStorage.quadPixelMask = pixel.quadPixelMask; + pixelStorage.quadSampleMask = pixel.quadSampleMask; + pixelStorage.psIteratesSamples = pixel.iteratesSamples; + pixelStorage.psPositionLocation = pixel.positionLocation; + pixelStorage.sampleLocations = pixel.sampleLocations; + pixelStorage.sampleOrder = pixel.sampleOrder; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; @@ -168,6 +189,11 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues vertexStorage.hostSubgroupSize = hostSubgroupSize; vertexStorage.fetchEmbedded = vertex.fetchEmbedded; vertexStorage.paClVsOutCntl = vertex.paClVsOutCntl; + vertexStorage.depthRemap = {vertex.depthRemapMode}; + vertexStorage.pointList = vertex.pointList; + vertexStorage.pointSize = vertex.pointSize; + vertexStorage.pointSizeMin = vertex.pointSizeMin; + vertexStorage.pointSizeMax = vertex.pointSizeMax; vertexStorage.fetchExternal = false; vertexStorage.fetchAttribReg = static_cast(vertex.fetchAttribReg); vertexStorage.fetchBufferReg = static_cast(vertex.fetchBufferReg); @@ -182,10 +208,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues } _detectVertexBuffers(vertexStorage); if (stage == ShaderStageKind::Local || stage == ShaderStageKind::TessellationControl || stage == ShaderStageKind::TessellationEvaluation) { - if (tessellation == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: tessellation configuration is missing"); - if (tessellation->inputControlPoints == 0u || tessellation->inputControlPoints > 32u || tessellation->outputControlPoints == 0u || tessellation->outputControlPoints > 32u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation control-point counts"); - if ((tessellation->domain != 1u && tessellation->domain != 2u) || (tessellation->partitioning != 0u && tessellation->partitioning != 2u) || (tessellation->outputTopology != 2u && tessellation->outputTopology != 3u)) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration"); - if (tessellation->patchesPerGroup == 0u || tessellation->patchesPerGroup > 256u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation patches per group"); + _validateTessellation(tessellation); auto& target = vertexStorage.tess; target.inputControlPoints = tessellation->inputControlPoints; target.outputControlPoints = tessellation->outputControlPoints; @@ -195,9 +218,12 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues target.patchesPerGroup = tessellation->patchesPerGroup; target.primitiveGroupSize = tessellation->primitiveGroupSize; target.breakAtInstance = tessellation->breakAtInstance; + target.instancePatches = tessellation->instancePatches; + target.drawParameters = tessellation->drawParameters; + target.mergedHull = tessellation->mergedHull; } if (stage == ShaderStageKind::Mesh) { - if (mesh == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: a mesh-stage program has no mesh configuration"); + _validateMesh(mesh); auto& target = vertexStorage.mesh; target.threadsNum[0] = mesh->threadsPerGroup; target.threadsNum[1] = 1u; @@ -212,7 +238,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.outputPrimitiveSize == 1u) vertexStorage.pointList = true; } ShaderStageInputInfo result; result.vertex = &vertexStorage; @@ -225,4 +252,51 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues throw std::runtime_error("ShaderInputInfoBuilder: unexpected stage"); } +void ValidateVertexStageInputs(ShaderStageKind stage, const GuestContext& context, const MeshConfiguration* mesh, const TessellationConfiguration* tessellation) { + if (!context.vertex.has_value()) { + throw std::runtime_error("ShaderInputInfoBuilder: GuestContext.vertex is not set"); + } + const auto& vertex = *context.vertex; + if (vertex.resourcesNum > vertex.resources.size()) throw std::runtime_error("ShaderInputInfoBuilder: invalid vertex resource count"); + struct Buffer { + std::uint64_t addr; + std::uint32_t stride; + std::uint32_t numRecords; + std::uint32_t fetchIndex; + int attrNum; + }; + std::array buffers; + int buffersNum = 0; + const std::array embedded{}; + for (std::uint32_t ri = 0; ri < vertex.resourcesNum; ++ri) { + const auto& fields = vertex.fetchEmbedded ? embedded : vertex.resources[ri].fields; + const std::uint16_t stride = static_cast((fields[1] >> 16u) & 0x3fffu); + const std::uint64_t base = (static_cast(fields[0]) | (static_cast(fields[1]) << 32u)) & 0xffffffffffffull; + const std::uint32_t numRecords = fields[2]; + const std::uint32_t fetchIndex = vertex.fetchEmbedded ? 0u : vertex.resourcesDst[ri].fetchIndex; + bool merged = false; + for (int bi = 0; bi < buffersNum; ++bi) { + auto& b = buffers[bi]; + if (b.stride != stride || b.fetchIndex != fetchIndex) continue; + const auto low = base < b.addr ? base : b.addr; + if (base - low >= stride || b.addr - low >= stride) continue; + if (b.numRecords != numRecords) throw std::runtime_error("ShaderInputInfoBuilder: merged vertex buffers disagree on record count"); + b.addr = low; + if (b.attrNum >= ShaderVertexInputBuffer::MaxAttributes) throw std::runtime_error("ShaderInputInfoBuilder: vertex buffer attribute count exceeds the supported domain"); + ++b.attrNum; + merged = true; + break; + } + if (merged) continue; + if (buffersNum >= ShaderVertexInputInfo::MaxResources) throw std::runtime_error("ShaderInputInfoBuilder: vertex buffer count exceeds the supported domain"); + buffers[buffersNum++] = {base, stride, numRecords, fetchIndex, 1}; + } + if (stage == ShaderStageKind::Local || stage == ShaderStageKind::TessellationControl || stage == ShaderStageKind::TessellationEvaluation) { + _validateTessellation(tessellation); + } + if (stage == ShaderStageKind::Mesh) { + _validateMesh(mesh); + } +} + } diff --git a/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp b/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp index 0b0fd6ae8e..5a87cd02e4 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp @@ -15,7 +15,12 @@ IrU32 TranslationContext::hostExecWord(std::uint32_t half) { if (half != 0u && program.WaveSize() != 64u) { return word; } - return IrU32(ir.BitwiseAnd(word.Value(), ballotMask(IrU1(ir.ConstantBool(true)))[half].Value())); + auto active = ballotMask(IrU1(ir.ConstantBool(true))); + if (program.Resources().stage == IrShaderStage::Pixel) { + const IrU64 lanes(ir.ConstructU64(active[0].Value(), active[1].Value())); + active = extractU64(IrU64(ir.Emit(IrOpcode::WqmU64, IrType::U64, {&lanes.Value()}))); + } + return IrU32(ir.BitwiseAnd(word.Value(), active[half].Value())); } IrU1 TranslationContext::threadBit(const std::array& mask) { diff --git a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp index f82197547f..d54715a372 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp @@ -362,6 +362,15 @@ IrValue& TranslationContext::invalidProductF32(IrValue* lhs, IrValue* rhs) { return ir.LogicalOr(infZero(lhsMagnitude, rhsMagnitude), infZero(rhsMagnitude, lhsMagnitude)); } +IrValue& TranslationContext::invalidOperationF32(std::initializer_list sources, IrValue& invalidProduct) { + IrValue* noNan = &ir.ConstantBool(true); + for (IrValue* source : sources) { + IrValue& nan = ir.UGreaterThan(ir.BitwiseAnd(ir.BitCastU32(*source), ir.Constant(0x7fffffffu)), ir.Constant(0x7f800000u)); + noNan = &ir.LogicalAnd(*noNan, ir.LogicalNot(nan)); + } + return ir.LogicalOr(invalidProduct, *noNan); +} + IrU32 TranslationContext::quietNan16(IrU32 bits) { return ieeeMode ? IrU32(ir.BitwiseOr(bits.Value(), ir.Constant(0x0200u))) : bits; } @@ -370,21 +379,25 @@ std::array TranslationContext::quietNan64(const std::array& return ieeeMode ? std::array{bits[0], IrU32(ir.BitwiseOr(bits[1].Value(), ir.Constant(0x00080000u)))} : bits; } +IrF32 TranslationContext::scaleF32Result(std::uint32_t omod, IrF32 value) { + IrValue& bits = ir.BitCastU32(value.Value()); + IrValue& magnitude = ir.BitwiseAnd(bits, ir.Constant(0x7fffffffu)); + IrValue& sign = ir.BitwiseAnd(bits, ir.Constant(0x80000000u)); + IrValue* result = nullptr; + if (omod == 3u) { + result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x01000000u)), sign, ir.ISub(bits, ir.Constant(0x00800000u))); + } else { + const std::uint32_t exponentStep = omod == 1u ? 0x00800000u : 0x01000000u; + result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x7f800000u - exponentStep)), ir.IAdd(bits, ir.Constant(exponentStep)), ir.BitwiseOr(sign, ir.Constant(0x7f800000u))); + } + result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x00800000u)), ir.Constant(0u), *result); + result = &ir.Select(ir.UGreaterThan(magnitude, ir.Constant(0x7f7fffffu)), bits, *result); + return IrF32(ir.BitCastF32(*result)); +} + IrF32 TranslationContext::applyF32ResultModifiers(const RdnaOperand& operand, IrF32 value) { if (operand.omod != 0u && outputModifierApplies(4u)) { - IrValue& bits = ir.BitCastU32(value.Value()); - IrValue& magnitude = ir.BitwiseAnd(bits, ir.Constant(0x7fffffffu)); - IrValue& sign = ir.BitwiseAnd(bits, ir.Constant(0x80000000u)); - IrValue* result = nullptr; - if (operand.omod == 3u) { - result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x01000000u)), sign, ir.ISub(bits, ir.Constant(0x00800000u))); - } else { - const std::uint32_t exponentStep = operand.omod == 1u ? 0x00800000u : 0x01000000u; - result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x7f800000u - exponentStep)), ir.IAdd(bits, ir.Constant(exponentStep)), ir.BitwiseOr(sign, ir.Constant(0x7f800000u))); - } - result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x00800000u)), ir.Constant(0u), *result); - result = &ir.Select(ir.UGreaterThan(magnitude, ir.Constant(0x7f7fffffu)), bits, *result); - value = IrF32(ir.BitCastF32(*result)); + value = scaleF32Result(operand.omod, value); } if (operand.clamp) { const IrF32 saturated(ir.Emit(IrOpcode::FPSaturate32, IrType::F32, {&value.Value()})); diff --git a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp index cf6c3a789f..3ba6021ff2 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp @@ -52,6 +52,12 @@ std::array TranslationContext::readF64Bits(const RdnaOperand& operand) return bits; } +std::array TranslationContext::flushF64Bits(const std::array& bits, std::uint32_t keepBit) { + if (!floatMode.has_value() || (floatMode->floatMode & keepBit) != 0u) return bits; + const IrU1 tiny(ir.IEqual(ir.BitwiseAnd(bits[1].Value(), ir.Constant(0x7ff00000u)), ir.Constant(0u))); + return {IrU32(ir.Select(tiny.Value(), ir.Constant(0u), bits[0].Value())), IrU32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits[1].Value(), ir.Constant(0x80000000u)), bits[1].Value()))}; +} + std::array TranslationContext::extractU64(IrU64 value) { return {IrU32(ir.CompositeExtract(value.Value(), 0u)), IrU32(ir.CompositeExtract(value.Value(), 1u))}; } @@ -117,7 +123,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 +171,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 +190,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,18 +230,39 @@ 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) { +void TranslationContext::writeF16(const RdnaOperand& operand, IrF32 value, std::initializer_list sources, IrValue* invalid) { const IrF16 half(ir.Emit(IrOpcode::ConvertF16F32, IrType::F16, {&applyF16ResultModifiers(operand, clampF16Overflow(value, sources)).Value()})); const IrU16 bits(ir.Emit(IrOpcode::BitCastU16F16, IrType::U16, {&half.Value()})); - write16Bits(operand, IrU32(ir.Emit(IrOpcode::ConvertU32U16, IrType::U32, {&bits.Value()}))); + IrU32 word(ir.Emit(IrOpcode::ConvertU32U16, IrType::U32, {&bits.Value()})); + if (invalid != nullptr) { + const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(word.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); + word = IrU32(ir.Select(ir.LogicalAnd(nan.Value(), *invalid), ir.Constant(0xfe00u), word.Value())); + } + write16Bits(operand, word); } } diff --git a/core/shader/recompiler/Translation/src/VectorInstructions.cpp b/core/shader/recompiler/Translation/src/VectorInstructions.cpp index 645b98534e..c02cdba51e 100644 --- a/core/shader/recompiler/Translation/src/VectorInstructions.cpp +++ b/core/shader/recompiler/Translation/src/VectorInstructions.cpp @@ -23,6 +23,24 @@ bool roundsProductSeparately(const RdnaInstruction& inst) { } +bool TranslationContext::flushesInHardware(const RdnaInstruction& inst) const { + const std::uint32_t declaredRound = floatMode.has_value() ? floatMode->floatMode & 3u : 0u; + if (declaredRound != 0u || (rounding.value & 3u) != 0u || (rounding.unknown & 3u) != 0u || inst.family == RdnaInstructionFamily::VOP3P) return false; + switch (inst.op) { + case RdnaOpcode::VAddF32: + case RdnaOpcode::VSubF32: + case RdnaOpcode::VSubrevF32: + case RdnaOpcode::VMulF32: + case RdnaOpcode::VFloorF32: + case RdnaOpcode::VCeilF32: return true; + case RdnaOpcode::VFmaF32: + case RdnaOpcode::VMacF32: + case RdnaOpcode::VMadmkF32: + case RdnaOpcode::VMadakF32: return !roundsProductSeparately(inst); + default: return false; + } +} + std::uint32_t TranslationContext::f32DenormalFlushFor(const RdnaInstruction& inst) const { std::uint32_t flush = 0u; switch (inst.op) { @@ -75,6 +93,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: @@ -102,7 +121,8 @@ std::uint32_t TranslationContext::f32DenormalFlushFor(const RdnaInstruction& ins if (denormals == 1u || denormals == 2u) { throw std::runtime_error("f32 denormal mode " + std::to_string(denormals) + " at pc " + std::to_string(inst.programCounter) + " is not implemented"); } - return denormals == 0u ? flush : 0u; + if (denormals != 0u) return 0u; + return flush == 3u && float32FlushToZero && flushesInHardware(inst) ? 0u : flush; } bool TranslationContext::emitVector(const RdnaInstruction& inst) { @@ -855,7 +875,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: @@ -946,9 +966,9 @@ bool TranslationContext::emitVector(const RdnaInstruction& inst) { case RdnaOpcode::VFmaF64: return float64Operation(inst, IrOpcode::FPFma64); case RdnaOpcode::VMinF64: - return ieeeMode ? float64Operation(inst, IrOpcode::FPMin64) : nonIeeeMinMaxF64(inst, IrOpcode::FPMin64); + return minMaxF64(inst, IrOpcode::FPMin64); case RdnaOpcode::VMaxF64: - return ieeeMode ? float64Operation(inst, IrOpcode::FPMax64) : nonIeeeMinMaxF64(inst, IrOpcode::FPMax64); + return minMaxF64(inst, IrOpcode::FPMax64); case RdnaOpcode::VLdexpF64: return float64Unary(inst, IrOpcode::FPLdexp64); case RdnaOpcode::VTruncF64: @@ -1140,7 +1160,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/CacheFileReadTests.cpp b/core/shader/recompiler/tests/CacheFileReadTests.cpp index 29f5e61b3f..f0e936b798 100644 --- a/core/shader/recompiler/tests/CacheFileReadTests.cpp +++ b/core/shader/recompiler/tests/CacheFileReadTests.cpp @@ -16,11 +16,88 @@ void* operator new[](std::size_t size) { return ::operator new(size); } void operator delete[](void* memory) noexcept { ::operator delete(memory); } void operator delete[](void* memory, std::size_t) noexcept { ::operator delete(memory); } -static void Require(bool ok) { if (!ok) throw std::runtime_error("Cache file read failed"); } +static void Require(bool ok, const char* message = "Cache file read failed") { if (!ok) throw std::runtime_error(message); } + +#ifdef _WIN32 +static void RemoveCachePaths(const std::filesystem::path& path) { + WIN32_FIND_DATAW data{}; + const auto search = FindFirstFileW((path / L"*").c_str(), &data); + if (search != INVALID_HANDLE_VALUE) { + do { + const std::wstring name = data.cFileName; + if (name == L"." || name == L"..") continue; + const auto child = path / name; + if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) RemoveCachePaths(child); + else DeleteFileW(child.c_str()); + } while (FindNextFileW(search, &data)); + FindClose(search); + } + RemoveDirectoryW(path.c_str()); +} + +static void VerifyCachePaths(const std::filesystem::path& root) { + const auto extended = [](const std::filesystem::path& path) { + auto native = path; + native.make_preferred(); + return std::filesystem::path(L"\\\\?\\" + native.native()); + }; + const std::vector original{std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}}; + const std::vector replacement{std::byte{5}, std::byte{6}}; + const auto verify = [&](const std::filesystem::path& path) { + for (const auto& alias : {path, extended(path)}) { + Require(ShaderRecompiler::WriteFileAtomically(alias, original), "cannot write cache path"); + std::vector bytes; + Require(ShaderRecompiler::ReadWholeFile(path, bytes) && bytes == original, "cannot read ordinary cache path"); + Require(ShaderRecompiler::ReadWholeFile(extended(path), bytes) && bytes == original, "cannot read extended cache path"); + const auto old = CreateFileW(extended(path).c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + Require(old != INVALID_HANDLE_VALUE, "cannot retain the original cache reader"); + struct Close { HANDLE file; ~Close() { CloseHandle(file); } } close{old}; + Require(ShaderRecompiler::WriteFileAtomically(alias, replacement), "cannot replace cache path with retained reader"); + Require(ShaderRecompiler::ReadWholeFile(alias, bytes) && bytes == replacement, "replacement cache bytes do not match"); + std::byte retained[4]{}; + DWORD count = 0; + Require(ReadFile(old, retained, sizeof(retained), &count, nullptr) && count == original.size(), "cannot read the retained cache handle"); + Require(std::equal(original.begin(), original.end(), retained), "retained cache bytes changed after replacement"); + } + WIN32_FIND_DATAW data{}; + const auto temporary = FindFirstFileW((extended(path).parent_path() / L"*.tmp.*").c_str(), &data); + if (temporary != INVALID_HANDLE_VALUE) FindClose(temporary); + Require(temporary == INVALID_HANDLE_VALUE && GetLastError() == ERROR_FILE_NOT_FOUND, "cache temporary file was left behind"); + }; + auto directory = root / "paths"; + while (directory.native().size() < 150) directory /= std::wstring(std::min(40, 150 - directory.native().size()), L'd'); + for (const auto length : {180u, 235u, 269u, 320u}) { + const auto size = std::max(length, directory.native().size() + 12); + verify(directory / (std::wstring(size - directory.native().size() - 5, L'x') + L".bin")); + } + auto deep = root; + while (deep.native().size() < 300) deep /= std::wstring(40, L'd'); + verify(deep / L"cache \u00e4\u6f22.bin"); + std::filesystem::create_directories(root / "relative"); + struct RestoreDirectory { + std::filesystem::path previous = std::filesystem::current_path(); + ~RestoreDirectory() { + std::error_code error; + std::filesystem::current_path(previous, error); + } + } restoreDirectory; + std::filesystem::current_path(root); + auto relative = (root / "relative" / ".." / "relative.bin").lexically_relative(std::filesystem::current_path()); + Require(!relative.empty(), "cannot construct the cache relative path"); + Require(ShaderRecompiler::WriteFileAtomically(relative, original), "cannot write the cache relative path"); + std::vector bytes; + Require(ShaderRecompiler::ReadWholeFile(relative, bytes) && bytes == original, "cannot read the cache relative path"); + const auto missing = deep / "missing.bin"; + Require(!ShaderRecompiler::ReadWholeFile(missing, bytes) && bytes.empty(), "missing cache path did not clear the read buffer"); +} +#endif int main() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-cache-read-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + auto directory = path; + directory += "-paths"; int status = 0; try { std::vector bytes{std::byte{1}}; @@ -37,8 +114,15 @@ int main() { FailLargeAllocation = false; Require(!read && failed.empty()); std::cout << "5 cache file read cases passed\n"; +#ifdef _WIN32 + VerifyCachePaths(directory); + std::cout << "Windows cache path and retained-reader replacement cases passed\n"; +#endif } catch (const std::exception& error) { FailLargeAllocation = false; std::cerr << error.what() << '\n'; status = 1; } std::error_code error; std::filesystem::remove(path, error); +#ifdef _WIN32 + RemoveCachePaths(std::filesystem::path(L"\\\\?\\" + directory.native())); +#endif return status; } diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index c60d90b112..b1db645fe1 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -45,6 +45,23 @@ void Require(bool condition) { if (!condition) std::abort(); } +void InsertMeshBarycentricFlag(std::vector& bytes) { + constexpr std::array draw{0x00, 0x34, 0xef, 0xcd, 0xab, 0x00, 0x00, 0x00, 57u, 0u, 0u, 0u, 4u, 0u, 0u, 0u, 9u, 0u, 0u, 0u}; + const auto at = std::search(bytes.begin(), bytes.end(), draw.begin(), draw.end()); + Require(at != bytes.end()); + bytes.insert(at + static_cast(draw.size()), 0u); +} + +void InsertPixelDefaultsAfterFrontFace(std::vector& bytes, bool sampleOrder = true) { + bytes.insert(bytes.begin() + 261u, {0x0fu, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0u, 0u, 0u, 0u}); + bytes.insert(bytes.begin() + 282u, sampleOrder ? 51u : 35u, 0u); +} + +void AppendVertexTail(std::vector& bytes) { + bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); + bytes.insert(bytes.end(), {0u, 0u, 0u, 0x80u, 0x3fu, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u}); +} + std::vector DecodeBase64(std::string_view text) { constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; std::vector bytes; @@ -196,15 +213,18 @@ int main() { const auto& request = replay.request; Require(request.context.pixel.has_value() == fixture.pixel && request.context.vertex.has_value()); Require(request.context.vertex->fetchAttribReg == 9u && request.context.vertex->fetchBufferReg == 13u && request.context.vertex->fetchEmbedded && request.context.vertex->paClVsOutCntl == 0x00050a05u); - Require(!fixture.pixel || (request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle && request.context.pixel->alphaToCoverage && request.context.pixel->stencilRefExportEnable && request.context.pixel->targetOutputMode[0] == 9u && request.context.pixel->targetExportMapping[0] == 0xe4u && !request.context.pixel->layerExported && !request.context.pixel->ditheredAlphaToCoverage)); + Require(!fixture.pixel || (request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle && request.context.pixel->alphaToCoverage && request.context.pixel->stencilRefExportEnable && request.context.pixel->targetOutputMode[0] == 9u && request.context.pixel->targetExportMapping[0] == 0xe4u && !request.context.pixel->layerExported && !request.context.pixel->ditheredAlphaToCoverage && request.context.pixel->reversedBlend == 0u)); Require(request.graphics.has_value() && request.graphics->firstUserSgpr == 6u && request.graphics->tessellation.has_value() == fixture.tessellation); Require(!fixture.tessellation || (request.graphics->tessellation->patchesPerGroup == 63u && request.graphics->tessellation->domain == 2u && request.graphics->tessellation->outputTopology == 3u)); Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 25u; - if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u}); - if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 5u, 0u); + upgraded[4] = 43u; + InsertMeshBarycentricFlag(upgraded); + if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); + if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); + if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 6u, 0u); + AppendVertexTail(upgraded); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -227,7 +247,7 @@ int main() { layered[4] = 23u; layered.insert(layered.begin() + 258u, static_cast(layer)); const auto layerReplay = serializer.Deserialize(EncodeBase64(layered)); - Require(layerReplay.request.context.pixel->layerExported == layer && !layerReplay.request.context.pixel->ditheredAlphaToCoverage); + Require(layerReplay.request.context.pixel->layerExported == layer && !layerReplay.request.context.pixel->ditheredAlphaToCoverage && layerReplay.request.context.pixel->reversedBlend == 0u); Require(layerReplay.request.target.float16DenormPreserve == preserve && layerReplay.request.graphics->tessellation.has_value() == fixture.tessellation); Require(!fixture.tessellation || layerReplay.request.graphics->tessellation->patchesPerGroup == 63u); auto truncatedLayer = layered; @@ -239,9 +259,12 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 25u; - layered.insert(layered.begin() + 259u, 0u); - if (fixture.tessellation) layered.insert(layered.end() - 35u, 5u, 0u); + layered[4] = 43u; + InsertMeshBarycentricFlag(layered); + layered.insert(layered.begin() + 259u, {0u, 0u}); + InsertPixelDefaultsAfterFrontFace(layered); + if (fixture.tessellation) layered.insert(layered.end() - 35u, 6u, 0u); + AppendVertexTail(layered); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -258,12 +281,15 @@ int main() { const auto replay = serializer.Deserialize(encoded23); Require(replay.request.context.pixel.has_value() && replay.request.context.vertex.has_value()); Require(replay.request.context.pixel->layerExported == layer); - Require(!replay.request.context.pixel->ditheredAlphaToCoverage); + Require(!replay.request.context.pixel->ditheredAlphaToCoverage && replay.request.context.pixel->reversedBlend == 0u); Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 25u; - bytes.insert(bytes.begin() + 259u, 0u); + bytes[4] = 43u; + InsertMeshBarycentricFlag(bytes); + bytes.insert(bytes.begin() + 259u, {0u, 0u}); + InsertPixelDefaultsAfterFrontFace(bytes); + AppendVertexTail(bytes); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -378,15 +404,76 @@ int main() { const auto replay = serializer.Deserialize(EncodeBase64(bytes)); const auto& request = replay.request; Require(request.context.pixel.has_value() == fixture.pixel && request.context.vertex.has_value()); - Require(!fixture.pixel || (request.context.pixel->layerExported == layer && !request.context.pixel->ditheredAlphaToCoverage)); + Require(!fixture.pixel || (request.context.pixel->layerExported == layer && !request.context.pixel->ditheredAlphaToCoverage && request.context.pixel->reversedBlend == 0u)); Require(request.graphics.has_value() && request.graphics->tessellation.has_value() == fixture.tessellation); Require(!fixture.tessellation || (request.graphics->tessellation->patchesPerGroup == 63u && request.graphics->tessellation->primitiveGroupSize == 37u && request.graphics->tessellation->breakAtInstance)); Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); - auto upgraded = bytes; - upgraded[4] = 25u; - if (fixture.pixel) upgraded.insert(upgraded.begin() + 259u, 0u); + auto main38 = bytes; + main38[4] = 38u; + InsertMeshBarycentricFlag(main38); + if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); + if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38, false); + auto upgraded = main38; + upgraded[4] = 43u; + if (fixture.pixel) upgraded.insert(upgraded.begin() + 317u, 16u, 0u); + if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); + main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + AppendVertexTail(upgraded); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); + const auto replay38 = serializer.Deserialize(EncodeBase64(main38)); + Require(replay38.request.graphics.has_value() && replay38.request.graphics->tessellation.has_value() == fixture.tessellation); + Require(!fixture.tessellation || (!replay38.request.graphics->tessellation->mergedHull && replay38.request.graphics->tessellation->primitiveGroupSize == 37u && replay38.request.graphics->tessellation->breakAtInstance)); + Require(replay38.request.graphics->draw.indexAddress == 0xabcdef3400u && replay38.request.graphics->draw.indexCount == 57u && replay38.request.graphics->draw.indexElementBytes == 4u && replay38.request.graphics->draw.instanceCount == 9u); + Require(replay38.request.target.float16DenormPreserve == preserve); + Require(serializer.Serialize(replay38.request) == EncodeBase64(upgraded)); + if (fixture.tessellation) { + for (const bool merged : {false, true}) { + auto pointed = request; + pointed.graphics->tessellation->mergedHull = merged; + pointed.context.vertex->pointList = true; + pointed.context.vertex->pointSize = 3.0f; + pointed.context.vertex->pointSizeMin = 1.0f; + pointed.context.vertex->pointSizeMax = 8.0f; + auto version43 = DecodeBase64(serializer.Serialize(pointed)); + Require(version43.size() > 14u && version43[4] == 43u); + const auto replay43 = serializer.Deserialize(EncodeBase64(version43)); + Require(replay43.request.graphics->tessellation->mergedHull == merged && replay43.request.context.vertex->pointList && replay43.request.context.vertex->pointSize == 3.0f && replay43.request.context.vertex->pointSizeMin == 1.0f && replay43.request.context.vertex->pointSizeMax == 8.0f); + auto version42 = version43; + version42.resize(version42.size() - 13u); + version42[4] = 42u; + const auto replay42 = serializer.Deserialize(EncodeBase64(version42)); + Require(replay42.request.graphics->tessellation->mergedHull == merged && !replay42.request.context.vertex->pointList && replay42.request.context.vertex->pointSize == 1.0f && replay42.request.context.vertex->pointSizeMin == 0.0f && replay42.request.context.vertex->pointSizeMax == 0.0f); + auto version41 = version42; + if (fixture.pixel) version41.erase(version41.begin() + 317u, version41.begin() + 333u); + version41[4] = 41u; + const auto replay41 = serializer.Deserialize(EncodeBase64(version41)); + Require(replay41.request.graphics->tessellation->mergedHull == merged && !replay41.request.context.vertex->pointList && replay41.request.context.vertex->pointSize == 1.0f && replay41.request.context.vertex->pointSizeMin == 0.0f && replay41.request.context.vertex->pointSizeMax == 0.0f); + auto version39 = version41; + version39.pop_back(); + version39[4] = 39u; + const auto replay39 = serializer.Deserialize(EncodeBase64(version39)); + Require(replay39.request.graphics->tessellation->mergedHull == merged && !replay39.request.context.vertex->pointList && replay39.request.context.vertex->pointSize == 1.0f && replay39.request.context.vertex->pointSizeMin == 0.0f && replay39.request.context.vertex->pointSizeMax == 0.0f); + } + } + for (const bool dither : {false, true}) { + if (!fixture.pixel && dither) continue; + auto main25 = bytes; + main25[4] = 25u; + if (fixture.pixel) main25.insert(main25.begin() + 259u, static_cast(dither)); + 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] = 43u; + InsertMeshBarycentricFlag(main25); + if (fixture.pixel) { + main25.insert(main25.begin() + 260u, 0u); + InsertPixelDefaultsAfterFrontFace(main25); + } + if (fixture.tessellation) main25.insert(main25.end() - 35u, 1u, 0u); + AppendVertexTail(main25); + Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); + } } } } @@ -394,13 +481,20 @@ int main() { ShaderRecompiler::RecompileRequest request{}; for (const bool preserve : {false, true}) { request.target.float16DenormPreserve = preserve; + request.target.float32FlushToZero = !preserve; + request.target.float32DenormPreserve = preserve; const auto encoded = serializer.Serialize(request); const auto replay = serializer.Deserialize(encoded); - Require(replay.request.target.float16DenormPreserve == preserve); + Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 25u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 43u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + auto version40 = bytes; + version40.pop_back(); + version40[4] = 40u; + const auto replay40 = serializer.Deserialize(EncodeBase64(version40)); + Require(!replay40.request.target.float32DenormPreserve && replay40.request.target.float32FlushToZero == !preserve); auto future = bytes; - future[4] = 26u; + future[4] = 44u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); @@ -418,5 +512,124 @@ int main() { Require(rejected); } + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.iteratesSamples = true; + pixel.positionLocation = ShaderRecompiler::PixelPositionLocation::UpperLeft; + pixel.sampleLocations = {0xa5u, 0x5au, 0xc3u, 0x3cu}; + pixel.sampleLocations.back() = 0x77u; + request.context.pixel = pixel; + const auto pixelEncoded = serializer.Serialize(request); + const auto pixelReplay = serializer.Deserialize(pixelEncoded); + Require(pixelReplay.request.context.pixel.has_value() && pixelReplay.request.context.pixel->iteratesSamples && pixelReplay.request.context.pixel->positionLocation == ShaderRecompiler::PixelPositionLocation::UpperLeft && pixelReplay.request.context.pixel->sampleLocations == pixel.sampleLocations); + auto pixelBytes = DecodeBase64(pixelEncoded); + const auto marker = std::search(pixelBytes.begin(), pixelBytes.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4); + Require(marker != pixelBytes.end() && marker - pixelBytes.begin() >= 2 && *(marker - 1) == 3u && *(marker - 2) == 1u); + *(marker - 1) = 4u; + bool locationRejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(pixelBytes))); + } catch (const std::runtime_error& error) { + locationRejected = std::string_view(error.what()) == "RequestSerializer: invalid PixelPositionLocation value"; + } + Require(locationRejected); + pixel.sampleOrder = {0x76543210u, 0x01234567u, 0x10325476u, 0x67452301u}; + request.context.pixel = pixel; + const auto orderEncoded = serializer.Serialize(request); + Require(serializer.Deserialize(orderEncoded).request.context.pixel->sampleOrder == pixel.sampleOrder); + auto version41 = DecodeBase64(orderEncoded); + const auto order = std::search(version41.begin(), version41.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.end()) + pixel.sampleLocations.size() + 1; + Require(version41.end() - order > static_cast(sizeof(pixel.sampleOrder)) && *order == 0x10u); + version41.erase(order, order + sizeof(pixel.sampleOrder)); + version41[4] = 41u; + const auto replay41 = serializer.Deserialize(EncodeBase64(version41)); + Require(replay41.request.context.pixel.has_value() && replay41.request.context.pixel->sampleLocations == pixel.sampleLocations && replay41.request.context.pixel->sampleOrder == std::array{}); + pixel.sampleOrder = {}; + request.context.pixel = pixel; + for (const bool spanned : {false, true}) { + request.target.spannedWaves = spanned; + Require(serializer.Deserialize(serializer.Serialize(request)).request.target.spannedWaves == spanned); + request.target.float32FlushToZero = true; + auto version35 = DecodeBase64(serializer.Serialize(request)); + version35.pop_back(); + const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); + const auto pixelTail = static_cast(1u + sizeof(pixel.sampleOrder)); + Require(version35.end() - stencilOperationValue > pixelTail && std::all_of(stencilOperationValue, stencilOperationValue + pixelTail, [](std::uint8_t value) { return value == 0u; })); + version35.erase(stencilOperationValue, stencilOperationValue + pixelTail); + version35[4] = 35u; + version35.pop_back(); + const auto replay35 = serializer.Deserialize(EncodeBase64(version35)); + Require(replay35.request.target.spannedWaves == spanned && !replay35.request.target.float32FlushToZero); + } + ShaderRecompiler::RecompileRequest pointRequest{}; + pointRequest.context.vertex = ShaderRecompiler::ShaderVertexStageInfo{}; + pointRequest.context.vertex->paClVsOutCntl = 0x01210000u; + pointRequest.context.vertex->pointList = true; + pointRequest.context.vertex->pointSize = 4.0f; + pointRequest.context.vertex->pointSizeMin = 0.5f; + pointRequest.context.vertex->pointSizeMax = 8.0f; + const auto pointEncoded = serializer.Serialize(pointRequest); + const auto pointReplay = serializer.Deserialize(pointEncoded); + Require(pointReplay.request.context.vertex.has_value() && pointReplay.request.context.vertex->paClVsOutCntl == 0x01210000u && pointReplay.request.context.vertex->pointList); + Require(pointReplay.request.context.vertex->pointSize == 4.0f && pointReplay.request.context.vertex->pointSizeMin == 0.5f && pointReplay.request.context.vertex->pointSizeMax == 8.0f); + Require(serializer.Serialize(pointReplay.request) == pointEncoded); + auto pointBytes = DecodeBase64(pointEncoded); + pointBytes.pop_back(); + bool pointRejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(pointBytes))); + } catch (const std::runtime_error& error) { + pointRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(pointRejected); + + pointRequest.target.depthRangeRemap = true; + pointRequest.target.float32FlushToZero = true; + pointRequest.context.vertex->depthRemapMode = 2u; + const auto remappedPoint = serializer.Deserialize(serializer.Serialize(pointRequest)); + Require(remappedPoint.request.target.depthRangeRemap && remappedPoint.request.context.vertex->depthRemapMode == 2u); + Require(serializer.Serialize(remappedPoint.request) == serializer.Serialize(pointRequest)); + + for (const bool spanned : {false, true}) { + for (const std::uint8_t version : {32u, 33u, 34u, 35u, 36u, 37u, 38u}) { + pointRequest.target.spannedWaves = spanned; + auto legacy = DecodeBase64(serializer.Serialize(pointRequest)); + legacy.resize(legacy.size() - (version >= 36u ? 14u : version >= 35u ? 15u : 20u)); + legacy[4] = version; + const auto replayLegacy = serializer.Deserialize(EncodeBase64(legacy)); + Require(replayLegacy.request.target.spannedWaves == spanned); + Require(replayLegacy.request.target.float32FlushToZero == (version >= 36u)); + Require(replayLegacy.request.target.depthRangeRemap == (version >= 35u)); + Require(replayLegacy.request.context.vertex.has_value()); + Require(replayLegacy.request.context.vertex->depthRemapMode == (version >= 35u ? 2u : 0u)); + Require(replayLegacy.request.context.vertex->paClVsOutCntl == pointRequest.context.vertex->paClVsOutCntl); + Require(!replayLegacy.request.context.vertex->pointList && replayLegacy.request.context.vertex->pointSize == 1.0f); + Require(replayLegacy.request.context.vertex->pointSizeMin == 0.0f && replayLegacy.request.context.vertex->pointSizeMax == 0.0f); + const auto upgradedLegacy = DecodeBase64(serializer.Serialize(replayLegacy.request)); + Require(upgradedLegacy[4] == 43u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 14u : version >= 35u ? 15u : 20u)); + } + } + + pixel.sampleOrder = {0x76543210u, 0x01234567u, 0x10325476u, 0x67452301u}; + pointRequest.context.pixel = pixel; + const auto sampledPoint = serializer.Deserialize(serializer.Serialize(pointRequest)); + Require(sampledPoint.request.context.vertex->pointList && sampledPoint.request.context.pixel->sampleOrder == pixel.sampleOrder); + auto legacy42 = DecodeBase64(serializer.Serialize(pointRequest)); + legacy42.resize(legacy42.size() - 13u); + legacy42[4] = 42u; + const auto replay42 = serializer.Deserialize(EncodeBase64(legacy42)); + Require(replay42.request.context.pixel->sampleOrder == pixel.sampleOrder && !replay42.request.context.vertex->pointList && replay42.request.context.vertex->pointSize == 1.0f); + pixel.sampleOrder = {}; + pointRequest.context.pixel = pixel; + auto legacy34 = DecodeBase64(serializer.Serialize(pointRequest)); + legacy34.resize(legacy34.size() - 20u); + const auto stencil34 = std::search(legacy34.begin(), legacy34.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); + const auto legacyPixelTail = static_cast(1u + sizeof(pixel.sampleOrder)); + Require(legacy34.end() - stencil34 > legacyPixelTail && std::all_of(stencil34, stencil34 + legacyPixelTail, [](std::uint8_t value) { return value == 0u; })); + legacy34.erase(stencil34, stencil34 + legacyPixelTail); + legacy34[4] = 34u; + const auto replay34 = serializer.Deserialize(EncodeBase64(legacy34)); + Require(replay34.request.context.pixel.has_value() && replay34.request.context.pixel->iteratesSamples && replay34.request.context.pixel->positionLocation == pixel.positionLocation && replay34.request.context.pixel->sampleLocations == pixel.sampleLocations); + Require(replay34.request.context.vertex.has_value() && !replay34.request.context.vertex->pointList && replay34.request.context.vertex->pointSize == 1.0f); + return 0; } diff --git a/core/shader/recompiler/tests/SpirvValidatorExit.cpp b/core/shader/recompiler/tests/SpirvValidatorExit.cpp new file mode 100644 index 0000000000..2bab5af5c9 --- /dev/null +++ b/core/shader/recompiler/tests/SpirvValidatorExit.cpp @@ -0,0 +1,62 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::array, 3> modules; + +bool Validate(const std::vector& words) { + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); + return tools.Validate(words); +} + +bool Check(const char* phase) { + const bool function = Validate(modules[0]); + const bool phi = Validate(modules[1]); + const bool invalidRejected = !Validate(modules[2]); + std::fprintf(stderr, "%s: function=%d phi=%d invalidRejected=%d\n", phase, function, phi, invalidRejected); + return function && phi && invalidRejected; +} + +void ValidateAtExit() { + if (!Check("exit validation")) std::_Exit(EXIT_FAILURE); +} + +} + +int main() { + if (std::atexit(ValidateAtExit) != 0) return EXIT_FAILURE; + const std::string prefix = R"(OpCapability Shader +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" +OpExecutionMode %main LocalSize 1 1 1 +%void = OpTypeVoid +%fn = OpTypeFunction %void +%bool = OpTypeBool +%true = OpConstantTrue %bool +%uint = OpTypeInt 32 0 +%one = OpConstant %uint 1 +%two = OpConstant %uint 2 +)"; + const std::string function = "%main = OpFunction %void None %fn\n%entry = OpLabel\n"; + const std::string tail = "OpReturn\nOpFunctionEnd\n"; + const std::string phi = R"(OpSelectionMerge %merge None +OpBranchConditional %true %left %right +%left = OpLabel +OpBranch %merge +%right = OpLabel +OpBranch %merge +%merge = OpLabel +%value = OpPhi %uint %one %left %two %right +)"; + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); + if (!tools.Assemble(prefix + function + tail, &modules[0]) || + !tools.Assemble(prefix + function + phi + tail, &modules[1]) || + !tools.Assemble(prefix + "%main = OpFunction %void None %void\n%entry = OpLabel\n" + tail, &modules[2])) return EXIT_FAILURE; + return Check("warm validation") ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/core/shader/recompiler/tests/test_shader_cache_native_paths.py b/core/shader/recompiler/tests/test_shader_cache_native_paths.py new file mode 100644 index 0000000000..b2f1e8c57f --- /dev/null +++ b/core/shader/recompiler/tests/test_shader_cache_native_paths.py @@ -0,0 +1,78 @@ +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +SCRIPT = Path(sys.argv.pop(1)).resolve() +CMAKE = sys.argv.pop(1) + + +class ShaderCachePathTests(unittest.TestCase): + def fixture(self, root): + source = root / 'recompiler' / 'input.cpp' + source.parent.mkdir(parents=True) + source.write_text('const char* mode = "APS5_TEST_PATH";\n', encoding='utf-8') + listing = root / 'sources.txt' + listing.write_text(f'sources {source.as_posix()}\n', encoding='utf-8') + output = root / 'version.hpp' + return source, listing, output + + def run_script(self, root, listing, output): + return subprocess.run([CMAKE, f'-DROOT={root.as_posix()}', + f'-DRECOMPILER_DIR={(root / "recompiler").as_posix()}', + f'-DLIST_FILE={listing.as_posix()}', f'-DOUTPUT={output.as_posix()}', + '-P', str(SCRIPT)], capture_output=True, encoding='utf-8', errors='replace', timeout=10) + + def check_source_change(self, name): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / name + source, listing, output = self.fixture(root) + result = self.run_script(root, listing, output) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + previous = output.read_bytes() + self.assertIn(b'APS5_TEST_PATH', previous) + source.write_text('const char* mode = "APS5_TEST_CHANGED";\n', encoding='utf-8') + result = self.run_script(root, listing, output) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotEqual(previous, output.read_bytes()) + self.assertIn(b'APS5_TEST_CHANGED', output.read_bytes()) + unchanged = output.stat().st_mtime_ns + result = self.run_script(root, listing, output) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(output.stat().st_mtime_ns, unchanged) + + def test_ascii(self): + self.check_source_change('plain path') + + def test_cyrillic(self): + self.check_source_change('\u0441\u0431\u043e\u0440\u043a\u0430 \u0438\u0433\u0440\u044b') + + def test_cjk(self): + self.check_source_change('\u6e2c\u8a66 path') + + def test_unicode_guard_rejects_unhashed_include(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / '\u0441\u0431\u043e\u0440\u043a\u0430' + source, listing, output = self.fixture(root) + (root / 'shared.hpp').write_text('int example();\n', encoding='utf-8') + source.write_text('#include "../shared.hpp"\n', encoding='utf-8') + result = self.run_script(root, listing, output) + self.assertNotEqual(result.returncode, 0) + self.assertIn('does not hash', result.stdout + result.stderr) + self.assertFalse(output.exists()) + + def test_unicode_header_rejects_unhashed_include(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / 'plain path' + source, listing, output = self.fixture(root) + name = '\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a.hpp' + (root / name).write_text('int example();\n', encoding='utf-8') + source.write_text(f'#include "../{name}"\n', encoding='utf-8') + result = self.run_script(root, listing, output) + self.assertNotEqual(result.returncode, 0) + self.assertIn('does not hash', result.stdout + result.stderr) + self.assertFalse(output.exists()) + + +unittest.main() diff --git a/demo/web/app.css b/demo/web/app.css index c5258a1102..b7c45cab14 100644 --- a/demo/web/app.css +++ b/demo/web/app.css @@ -4,3 +4,7 @@ *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}button,input,select{font:inherit}button,a,select,input{touch-action:manipulation}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.45}a{color:var(--accent);text-underline-offset:3px}button:focus-visible,a:focus-visible,select:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:4px}code,pre,.eyebrow,.fine{font-family:ui-monospace,"Cascadia Mono",Consolas,monospace}code{font-size:.9em}h1,h2,h3,p{margin-top:0}[hidden]{display:none!important}header{border-bottom:1px solid var(--line);background:var(--surface)}.bar{max-width:1120px;margin:auto;min-height:68px;padding:12px 24px;display:flex;align-items:center;justify-content:space-between;gap:24px}.brand{display:flex;align-items:center;gap:10px;text-decoration:none;color:var(--text);font-weight:700;font-size:17px;white-space:nowrap}.brand>span:last-child{font-weight:400;color:var(--muted);font-size:14px}.mark{display:flex;gap:3px;align-items:center;margin-right:3px}.mark i{display:block;width:4px;height:20px;background:var(--text)}.mark i:last-child{opacity:.3}nav{display:flex;align-items:center;gap:24px;font-size:13px}nav a{color:var(--muted);text-decoration:none}nav a:hover{color:var(--text)}main{max-width:1024px;margin:auto;padding:58px 24px 36px}.intro{max-width:660px;margin-bottom:30px}.eyebrow{color:var(--muted);letter-spacing:.12em;font-size:11px;margin-bottom:12px}h1{font-size:clamp(29px,4vw,43px);line-height:1.15;letter-spacing:-.035em;font-weight:650;margin-bottom:18px}.converter{border:1px solid var(--line);border-radius:10px;background:var(--surface);overflow:hidden}.panel-heading{display:flex;justify-content:space-between;align-items:center;gap:16px;border-bottom:1px solid var(--line);padding:19px 26px}.panel-heading h2{font-size:15px;margin:0;font-weight:600}.private{font-size:12px;color:var(--muted)}.private b{color:var(--good);font-size:9px;margin-right:5px}.drop{margin:24px;border:1px dashed var(--line);border-radius:6px;text-align:center;padding:28px 16px 17px;background:var(--bg)}.drop.dragging{border-color:var(--accent);background:var(--sunken)}.archive-icon{display:inline-flex;align-items:center;justify-content:center;width:40px;height:45px;border:1px solid var(--line);border-radius:4px;font-size:25px;color:var(--muted);margin-bottom:15px}.drop h3{font-size:19px;margin-bottom:8px;font-weight:600}.drop>p{color:var(--muted);font-size:13px;margin-bottom:20px}.button{display:inline-flex;align-items:center;justify-content:center;gap:18px;border:1px solid transparent;border-radius:6px;background:var(--accent);color:var(--on-accent);text-decoration:none;font-weight:600;font-size:13px;padding:11px 20px;white-space:nowrap}.button:hover:not(:disabled){filter:brightness(.94)}.drop .fine{margin:15px 0 0;font-size:10px}.fine{font-size:11px;line-height:1.6;color:var(--muted);margin-bottom:0}.plain{border:0;background:none;color:var(--muted);padding:8px;font-size:13px}.plain:hover{color:var(--text)}#theme{font-size:19px;padding:3px}.settings{display:grid;grid-template-columns:1fr 1fr;gap:36px;padding:0 26px 20px}fieldset{border:0;margin:0;padding:0;min-width:0}legend,.save-settings>label{display:block;font-size:12px;font-weight:600;margin-bottom:10px}.choices{display:flex;gap:10px}.choices label{flex:1;display:flex;gap:9px;align-items:center;border:1px solid var(--line);border-radius:6px;padding:10px;font-size:13px;cursor:pointer}.choices label:has(input:checked){border-color:var(--accent)}.choices span{font-size:11px;color:var(--muted);margin-left:auto}input{accent-color:var(--accent)}input[type=radio],input[type=checkbox]{margin:0;width:15px;height:15px;flex-shrink:0}select{width:100%;padding:10px;border:1px solid var(--line);border-radius:6px;background:var(--surface);color:var(--text);font-size:13px}.save-settings .fine{margin-top:8px}.options{padding:0 26px 23px;font-size:12px;color:var(--muted)}.options label{display:flex;gap:9px;align-items:center}.action-row{border-top:1px solid var(--line);padding:17px 26px;display:flex;gap:12px;align-items:center}.action-row>p{margin:0 auto 0 0;font-size:12px}.muted{color:var(--muted)}.selection{display:flex;justify-content:space-between;align-items:center;padding:25px 26px;gap:16px}.selection h3{font-size:18px;margin-bottom:6px;overflow-wrap:anywhere}.selection p{margin-bottom:0;font-size:12px}.selection .eyebrow{font-size:10px;margin-bottom:6px}.warnings,.notice{padding:16px 20px;background:var(--warning-bg);color:var(--warning);font-size:13px;border:1px solid var(--line);border-radius:6px}.warnings{margin:0 26px 24px}.warnings ul{padding-left:18px;margin:0 0 12px}.warnings label{display:flex;gap:8px;align-items:center}.notice{margin-bottom:20px}.status{border-top:1px solid var(--line);padding:22px 26px}.status-line{display:flex;justify-content:space-between;font-size:13px;gap:16px}progress{appearance:none;width:100%;height:5px;margin:14px 0 8px;border:0;border-radius:3px;overflow:hidden;background:var(--line)}progress::-webkit-progress-bar{background:var(--line)}progress::-webkit-progress-value{background:var(--accent)}progress::-moz-progress-bar{background:var(--accent)}#current-file{overflow-wrap:anywhere;min-height:18px}details{margin-top:18px;font-size:11px;color:var(--muted)}summary{cursor:pointer}pre{max-height:180px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:11px}.error{margin:0 26px 24px;padding:15px;background:var(--error-bg);color:var(--error);border-radius:6px;white-space:pre-wrap;overflow-wrap:anywhere}.done{padding:24px 26px;border-top:1px solid var(--line)}.done h3{font-size:20px;color:var(--good);margin-bottom:8px}.done p{font-size:13px;color:var(--muted)}.done .fine{margin:15px 0 0}.compatibility{margin:12px 0 0;color:var(--muted);font-size:11px;line-height:1.8}.limits{border-top:1px solid var(--line);padding-top:22px;margin:30px 0 0;color:var(--muted);font-size:11px;line-height:1.8}footer{max-width:1024px;margin:auto;border-top:1px solid var(--line);padding:22px 24px;display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}footer div{display:flex;gap:20px}footer a{color:var(--muted);text-decoration:none}@media(max-width:680px){.bar{flex-wrap:wrap;gap:12px;padding:14px 20px}nav{gap:17px;font-size:12px}main{padding:34px 16px 26px}.panel-heading{padding:16px}.private{font-size:10px}.drop{margin:16px}.settings{grid-template-columns:1fr;padding:0 16px 18px;gap:22px}.options{padding:0 16px 20px}.action-row{padding:16px;flex-wrap:wrap}.action-row>p{width:100%}.selection{padding:20px 16px}.warnings{margin:0 16px 20px}.status,.done{padding:20px 16px}.error{margin:0 16px 20px}footer{flex-wrap:wrap;gap:12px}footer div{flex-wrap:wrap;gap:14px}} .input-actions{display:flex;justify-content:center;flex-wrap:wrap;gap:12px} +.app-list{display:grid;gap:8px;max-height:220px;overflow:auto;padding-left:22px;margin:14px 0 0;font-size:12px;color:var(--muted)} +.app-list li{padding:8px 0;border-top:1px solid var(--line)} +.app-list strong,.app-list span{display:block;overflow-wrap:anywhere} +.app-list strong{color:var(--text)} diff --git a/demo/web/app.mjs b/demo/web/app.mjs index ca615cf880..4e0b534145 100644 --- a/demo/web/app.mjs +++ b/demo/web/app.mjs @@ -1,11 +1,17 @@ +import { abortAndDiscardOutput, OutputDirectoryTransaction } from "./output-directory.mjs"; +import { batchMemoryEstimate } from "./batch.mjs"; + const $ = id => document.getElementById(id); const stages = ["Converting executable", "Converting PRX modules", "Scanning syscalls", "Scanning instructions", "Converting instructions", "Analysing imports", "Building output image", "Writing output"]; let worker; -let file; -let summary; +let selectedInputs = []; +let summaries = []; +let inspectionComplete = false; let working = false; +let failing = false; let sink; let outputDirectory; +let outputTransaction; let downloadUrl; let logs = []; let release; @@ -22,22 +28,55 @@ function bytes(value) { return `${(value / 1024 ** unit).toFixed(1)} ${["B", "KiB", "MiB", "GiB"][unit]}`; } +function sum(values) { + let total = 0; + for (const value of values) { + if (!Number.isSafeInteger(value) || value < 0 || total > Number.MAX_SAFE_INTEGER - value) return null; + total += value; + } + return total; +} + function update() { const target = document.querySelector('input[name="target"]:checked').value; - const folderBlocked = target === "windows" && $("save").value === "disk"; + const multi = selectedInputs.length > 1; + const inspected = inspectionComplete && summaries.length === selectedInputs.length; + const folderBlocked = $("save").value === "disk" && (target === "windows" || multi); const folderOption = $("save").querySelector('[value="disk"]'); - folderOption.disabled = !canStream || target === "windows"; - folderOption.textContent = target === "windows" ? "Save to folder · unavailable for Windows (DLL restrictions)" : "Save to folder · uncompressed"; + folderOption.disabled = !canStream || target === "windows" || multi; + folderOption.textContent = target === "windows" ? "Save to folder · unavailable for Windows (DLL restrictions)" : multi ? "Save to folder · unavailable for batch output" : "Save to folder · uncompressed"; const libraryBytes = release?.platforms[target]?.expandedBytes; - const estimate = summary && libraryBytes !== undefined ? Math.ceil(summary.totalBytes * 1.02) + summary.selectedBytes * 4 + libraryBytes + summary.files * 1024 + 8 * 1024 * 1024 : null; - const tooLarge = estimate !== null && estimate > 256 * 1024 * 1024; + const selectedBytes = inspected ? sum(summaries.map(summary => target === "windows" ? summary.windowsSelectedBytes : summary.selectedBytes)) : null; + const conversionTooLarge = inspected && summaries.some(summary => (target === "windows" ? summary.windowsSelectedBytes : summary.selectedBytes) > 256 * 1024 * 1024); + const iconTooLarge = target === "windows" && inspected && summaries.some(summary => summary.windowsIconTooLarge); + const conversionBlocked = conversionTooLarge || iconTooLarge; + let estimate = null; + let estimateOverflow = false; + if (inspected && libraryBytes !== undefined) { + try { estimate = batchMemoryEstimate(summaries, target, libraryBytes); } + catch (error) { + if (!(error instanceof Error) || error.message !== "ZIP contains an unsupported file size") throw error; + estimateOverflow = true; + } + } + const tooLarge = estimateOverflow || (estimate !== null && estimate > 256 * 1024 * 1024); $("save").querySelector('[value="memory"]').disabled = Boolean(tooLarge); const memoryBlocked = $("save").value === "memory" && tooLarge; - $("save-note").textContent = $("save").value === "disk" ? "Creates a subfolder with the executable, libs and app0. No ZIP compression. Browsers may block DLL and other restricted files; use ZIP for those apps." : $("save").value === "zip" ? "Writes a compressed ZIP directly to disk without holding the whole archive in memory." : "Up to 256 MiB including expanded files and output overhead."; - $("browser-note").hidden = supported && canStream && canSaveZip && !memoryBlocked && !folderBlocked; - $("browser-note").textContent = !supported ? "This browser or connection does not support the converter. Use a current browser over HTTPS, or use the native relinker." : folderBlocked ? "Browsers block direct DLL creation. Windows output requires ZIP. Choose Save ZIP to disk or Download in memory." : memoryBlocked ? "This archive exceeds the memory-download limit. Choose Save ZIP to disk. If that option is unavailable, open this page in desktop Chrome or Edge." : "Some direct disk saving options are unavailable in this browser or context. Small apps can still be downloaded in memory. For larger apps, use desktop Chrome or Edge over HTTPS."; - $("convert").disabled = !supported || working || !summary || !release || memoryBlocked || folderBlocked || (summary.warnings.length > 0 && !$("acknowledge").checked); - $("ready-note").textContent = summary ? (folderBlocked ? "Choose a ZIP save mode for Windows." : memoryBlocked ? "A direct disk save is required for this archive." : release ? `Libraries: ${release.tag} · ${$("save").value === "disk" ? "Uncompressed folder" : "ZIP compression level 9"}` : "Loading release information…") : "Choose a ZIP or folder to get started."; + if (inspected) { + const totalFiles = sum(summaries.map(summary => summary.files)); + const totalBytes = sum(summaries.map(summary => summary.totalBytes)); + const modules = sum(summaries.map(summary => summary.modules)); + const sizeText = value => value === null ? "beyond the supported size range" : bytes(value); + $("archive-info").textContent = multi + ? `${selectedInputs.length} apps · ${sizeText(totalFiles)} files · ${sizeText(totalBytes)} expanded · ${sizeText(modules)} modules · ${sizeText(selectedBytes)} ${target} conversion input` + : `${summaries[0].files} files · ${bytes(summaries[0].totalBytes)} expanded · ${summaries[0].modules} modules · ${bytes(target === "windows" ? summaries[0].windowsSelectedBytes : summaries[0].selectedBytes)} ${target} conversion input` + (summaries[0].root ? ` · Root: ${summaries[0].root}` : ""); + } + $("save-note").textContent = multi ? $("save").value === "memory" ? "Creates one ZIP with each app in its own folder. The complete bundle must fit in the 256 MiB memory-download budget." : "Writes one compressed ZIP containing each app in its own folder, without holding the whole bundle in memory." : $("save").value === "disk" ? "Creates a subfolder with the executable, libs and app0. No ZIP compression. Incomplete output folders are removed after cancellation or failure. Browsers may block DLL and other restricted files; use ZIP for those apps." : $("save").value === "zip" ? "Writes a compressed ZIP directly to disk without holding the whole archive in memory." : "Up to 256 MiB including expanded files and output overhead."; + $("browser-note").hidden = supported && canStream && canSaveZip && !memoryBlocked && !folderBlocked && !conversionBlocked; + $("browser-note").textContent = !supported ? "This browser or connection does not support the converter. Use a current browser over HTTPS, or use the native relinker." : folderBlocked && multi ? "Batch conversion creates one ZIP bundle. Choose Save ZIP to disk or Download in memory." : folderBlocked ? "Browsers block direct DLL creation. Windows output requires ZIP. Choose Save ZIP to disk or Download in memory." : iconTooLarge ? "A Windows icon exceeds 16 MiB. Choose Linux or use the native relinker." : conversionTooLarge ? `A ${target} conversion input exceeds 256 MiB. Use the native relinker for that app.` : estimateOverflow ? "The combined output exceeds the browser's supported size range. Choose Save ZIP to disk." : memoryBlocked ? "This output exceeds the memory-download limit. Choose Save ZIP to disk. If that option is unavailable, open this page in desktop Chrome or Edge." : "Some direct disk saving options are unavailable in this browser or context. Small apps can still be downloaded in memory. For larger apps, use desktop Chrome or Edge over HTTPS."; + const hasWarnings = inspected && summaries.some(summary => summary.warnings.length > 0); + $("convert").disabled = !supported || working || !inspected || !release || conversionBlocked || memoryBlocked || folderBlocked || (hasWarnings && !$("acknowledge").checked); + $("ready-note").textContent = inspected ? (folderBlocked && multi ? "Choose a ZIP save mode for the app bundle." : folderBlocked ? "Choose a ZIP save mode for Windows." : iconTooLarge ? "A Windows icon exceeds 16 MiB. Choose Linux or use the native relinker." : conversionTooLarge ? `A ${target} conversion input exceeds 256 MiB. Use the native relinker for that app.` : memoryBlocked ? "A direct disk save is required for this output." : release ? `Libraries: ${release.tag} · ${multi ? "ZIP bundle" : $("save").value === "disk" ? "Uncompressed folder" : "ZIP compression level 9"}` : "Loading release information…") : "Choose one folder or one or more ZIP files to get started."; } function lock(value) { @@ -45,22 +84,35 @@ function lock(value) { } async function fail(message) { + if (failing) return; + failing = true; worker?.terminate(); worker = null; - working = false; - summary = null; - if (sink) { - const active = sink; - sink = null; - try { await active.abort(); } catch (error) { message += `\nCould not discard the output file: ${error.message}`; } + working = true; + selectedInputs = []; + summaries = []; + inspectionComplete = false; + const transaction = outputTransaction; + outputTransaction = null; + outputDirectory = null; + if (transaction) { + $("status").hidden = false; + $("phase").textContent = "Removing incomplete output folder"; } - if (outputDirectory) { message += `\nIncomplete files remain in ${outputDirectory.name}/.`; outputDirectory = null; } + const activeSink = sink; + sink = null; + for (const failure of await abortAndDiscardOutput(transaction, activeSink)) message += `\n${failure}`; $("error").textContent = message; $("error").hidden = false; $("status").hidden = true; $("cancel").hidden = true; $("drop").hidden = false; + $("selection").hidden = true; + $("warnings").hidden = true; + $("app-list").replaceChildren(); $("file").value = ""; + working = false; + failing = false; lock(false); update(); } @@ -69,13 +121,14 @@ async function onMessage({ data }) { try { if (data.type === "progress") { if (data.message) { - logs.push(data.message.trim()); + logs.push(`${data.appName ? `${data.appName}: ` : ""}${data.message.trim()}`); logs = logs.slice(-150); $("log").textContent = logs.join("\n"); return; } - $("phase").textContent = data.phase ?? stages[data.stage]; - $("current-file").textContent = data.input ?? ""; + const phase = data.phase ?? stages[data.stage]; + $("phase").textContent = data.appCount > 1 ? `App ${data.appIndex + 1}/${data.appCount} · ${phase}` : phase; + $("current-file").textContent = [data.appName, data.input].filter(Boolean).join(" · "); if (Number.isFinite(data.total) && data.total > 0) { const percent = Math.min(100, Math.floor(data.completed / data.total * 100)); $("progress").value = percent; @@ -85,14 +138,34 @@ async function onMessage({ data }) { $("percent").textContent = ""; } } else if (data.type === "inspected") { - summary = data.summary; + summaries[data.index] = data.summary; + } else if (data.type === "inspection-complete") { + inspectionComplete = true; working = false; $("selection").hidden = false; $("drop").hidden = true; - $("archive-name").textContent = file.name; - $("archive-info").textContent = `${summary.files} files · ${bytes(summary.totalBytes)} expanded · ${summary.modules} modules · ${bytes(summary.selectedBytes)} conversion input` + (summary.root ? ` · Root: ${summary.root}` : ""); - $("warning-list").replaceChildren(...summary.warnings.map(message => { const item = document.createElement("li"); item.textContent = message; return item; })); - $("warnings").hidden = !summary.warnings.length; + $("archive-name").textContent = selectedInputs.length > 1 ? `${selectedInputs.length} app ZIPs` : selectedInputs[0].name; + $("app-list").replaceChildren(...selectedInputs.map((input, index) => { + const item = document.createElement("li"); + const name = document.createElement("strong"); + name.textContent = input.name; + const details = document.createElement("span"); + const summary = summaries[index]; + details.textContent = `${summary.files} files · ${bytes(summary.totalBytes)} expanded · ${summary.modules} modules`; + item.append(name, details); + return item; + })); + $("app-list").hidden = selectedInputs.length < 2; + const warningItems = []; + for (let index = 0; index < summaries.length; index++) { + for (const message of summaries[index].warnings) { + const item = document.createElement("li"); + item.textContent = selectedInputs.length > 1 ? `${selectedInputs[index].name}: ${message}` : message; + warningItems.push(item); + } + } + $("warning-list").replaceChildren(...warningItems); + $("warnings").hidden = warningItems.length === 0; $("status").hidden = true; $("cancel").hidden = true; lock(false); @@ -128,7 +201,7 @@ async function onMessage({ data }) { $("done").hidden = false; $("status").hidden = true; $("cancel").hidden = true; - $("result-info").textContent = `${bytes(data.size)} ${data.mode === "disk" ? "files" : "ZIP"} · ${data.release} system libraries · ${data.outputName}`; + $("result-info").textContent = `${data.apps > 1 ? `${data.apps} apps · ` : ""}${bytes(data.size)} ${data.mode === "disk" ? "files" : "ZIP"} · ${data.release} system libraries · ${data.apps > 1 ? "combined app bundle" : data.outputName}`; if (data.blob) { downloadUrl = URL.createObjectURL(data.blob); $("download").href = downloadUrl; @@ -137,12 +210,17 @@ async function onMessage({ data }) { } else if (data.mode === "zip") { $("result-info").textContent += " · ZIP saved to disk"; } else { - $("result-info").textContent += ` · Saved in ${outputDirectory.name}/`; + $("result-info").textContent += ` · Saved in ${outputTransaction.name}/`; if (document.querySelector('input[name="target"]:checked').value === "linux") $("result-info").textContent += ` · On Linux, run chmod +x "${data.outputName}" before launching.`; } + $("result-note").textContent = data.apps > 1 ? "Extract the ZIP; each app is in its own folder with its executable, libs and app0 files. On Linux, run chmod +x */*.elf before launching." : "Extract the entire ZIP before launching the executable."; $("result-note").hidden = data.mode === "disk"; outputDirectory = null; - summary = null; + outputTransaction?.commit(); + outputTransaction = null; + selectedInputs = []; + summaries = []; + inspectionComplete = false; worker.terminate(); worker = null; lock(false); @@ -151,13 +229,17 @@ async function onMessage({ data }) { } catch (error) { await fail(error.message); } } -async function selectFile(selected, files) { - if (working || !supported || !selected) return; - if (!files && !selected.name.toLowerCase().endsWith(".zip")) { await fail("Choose a ZIP archive."); return; } +async function selectInputs(inputs) { + if (working || !supported || !inputs.length) return; + if (inputs.some(input => !input.files && !input.name.toLowerCase().endsWith(".zip")) || (inputs.length > 1 && inputs.some(input => input.files))) { + await fail("Batch conversion accepts ZIP archives only. Choose one folder or select ZIP files."); + return; + } worker?.terminate(); if (downloadUrl) { URL.revokeObjectURL(downloadUrl); downloadUrl = null; } - summary = null; - file = selected; + selectedInputs = inputs; + summaries = []; + inspectionComplete = false; working = true; logs = []; $("log").textContent = ""; @@ -166,7 +248,7 @@ async function selectFile(selected, files) { $("status").hidden = false; $("cancel").hidden = false; $("phase").textContent = "Inspecting files"; - $("current-file").textContent = file.name; + $("current-file").textContent = inputs.length > 1 ? `Inspecting ${inputs.length} ZIP archives` : inputs[0].name; $("percent").textContent = ""; $("progress").removeAttribute("value"); lock(true); @@ -174,20 +256,40 @@ async function selectFile(selected, files) { worker = new Worker(new URL("./worker.mjs", import.meta.url), { type: "module" }); worker.onmessage = onMessage; worker.onerror = event => fail(event.message || "The converter worker stopped. Try a smaller app or use the native relinker."); - worker.postMessage(files ? { type: "inspect", files } : { type: "inspect", file }); + worker.postMessage({ type: "inspect", inputs }); } -$("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 })); - if (files.length) selectFile({ name: files[0].path.split("/")[0] }, files); + if (files.length) selectInputs([{ name: files[0].path.split("/")[0], files }]); +}; +$("file").onchange = () => { + const inputs = [...$("file").files].map(file => ({ name: file.name, file })); + $("file").value = ""; + selectInputs(inputs); }; -$("file").onchange = () => selectFile($("file").files[0]); for (const type of ["dragenter", "dragover"]) $("drop").addEventListener(type, event => { event.preventDefault(); if (!working) $("drop").classList.add("dragging"); }); $("drop").ondragleave = () => $("drop").classList.remove("dragging"); -$("drop").ondrop = event => { event.preventDefault(); $("drop").classList.remove("dragging"); if (working) return; if (event.dataTransfer.files.length !== 1) { fail("Drop one ZIP archive at a time."); return; } selectFile(event.dataTransfer.files[0]); }; +$("drop").ondrop = event => { + event.preventDefault(); + $("drop").classList.remove("dragging"); + if (working) return; + selectInputs([...event.dataTransfer.files].map(file => ({ name: file.name, file }))); +}; for (const id of ["save", "target", "acknowledge"]) $(id).addEventListener("change", update); $("cancel").onclick = () => fail("Cancelled. Choose the ZIP or folder again to restart."); $("convert").onclick = async () => { @@ -196,22 +298,21 @@ $("convert").onclick = async () => { lock(true); update(); try { + const target = document.querySelector('input[name="target"]:checked').value; if ($("save").value === "zip") { - const handle = await window.showSaveFilePicker({ suggestedName: file.name.replace(/\.zip$/iu, "") + "-" + document.querySelector('input[name="target"]:checked').value + ".zip", types: [{ description: "ZIP archive", accept: { "application/zip": [".zip"] } }] }); + const name = selectedInputs.length > 1 ? `anyps5-${target}-apps.zip` : `${selectedInputs[0].name.replace(/\.zip$/iu, "")}-${target}.zip`; + const handle = await window.showSaveFilePicker({ suggestedName: name, types: [{ description: "ZIP archive", accept: { "application/zip": [".zip"] } }] }); sink = await handle.createWritable(); } else if ($("save").value === "disk") { const parent = await window.showDirectoryPicker({ mode: "readwrite" }); - const name = file.name.replace(/\.zip$/iu, "") + "-" + document.querySelector('input[name="target"]:checked').value; - let exists = false; - try { await parent.getDirectoryHandle(name); exists = true; } - catch (error) { if (error.name !== "NotFoundError") throw error; } - if (exists) throw new Error(`The folder ${name} already exists. Choose another destination or rename the existing folder.`); - outputDirectory = await parent.getDirectoryHandle(name, { create: true }); + const name = selectedInputs[0].name.replace(/\.zip$/iu, "") + "-" + target; + outputTransaction = await OutputDirectoryTransaction.create(parent, name); + outputDirectory = outputTransaction.directory; } $("status").hidden = false; $("cancel").hidden = false; $("error").hidden = true; - worker.postMessage({ type: "convert", target: document.querySelector('input[name="target"]:checked').value, toIntel: $("intel").checked, mode: $("save").value }); + worker.postMessage({ type: "convert", target, toIntel: $("intel").checked, mode: $("save").value }); } catch (error) { if (error.name === "AbortError") { working = false; lock(false); update(); } else await fail(error.message); @@ -223,7 +324,7 @@ $("theme").onclick = () => { }; update(); lock(false); -if (supported) fetch("./release.json", { cache: "no-cache" }).then(response => { if (!response.ok) throw new Error(`Cannot load release information (${response.status})`); return response.json(); }).then(value => { +if (supported) fetch("./release.json", { cache: "no-cache" }).then(response => { if (!response.ok) throw new Error(`Cannot load release information (${response.status}).${response.status === 404 ? " release.json is generated during the site build, so it is missing when serving demo/web directly." : ""}`); return response.json(); }).then(value => { if (!value.tag || !["windows", "linux"].every(target => Number.isSafeInteger(value.platforms?.[target]?.expandedBytes))) throw new Error("Invalid release information"); release = value; update(); diff --git a/demo/web/archive.mjs b/demo/web/archive.mjs index 20c9584823..7021114f0e 100644 --- a/demo/web/archive.mjs +++ b/demo/web/archive.mjs @@ -1,8 +1,9 @@ import { BlobReader, Uint8ArrayReader, ZipReader, ZipWriter, configure } from "https://cdn.jsdelivr.net/npm/@zip.js/zip.js@2.7.57/lib/zip-no-worker.js/+esm"; +import { batchMemoryEstimate, batchOutputDirectory, MEMORY_LIMIT, memoryEstimate } from "./batch.mjs"; configure({ useWebWorkers: false, useCompressionStream: false, chunkSize: 65536 }); -export const MEMORY_LIMIT = 256 * 1024 * 1024; +export { batchMemoryEstimate, batchOutputDirectory, memoryEstimate, MEMORY_LIMIT }; export const CONVERSION_LIMIT = 256 * 1024 * 1024; export const ICON_LIMIT = 16 * 1024 * 1024; const MAX_ENTRIES = 100000; @@ -18,7 +19,7 @@ class BoundedBlobReader extends BlobReader { export function validPath(name) { if (!name || name.includes("\\") || /[\x00-\x1f<>:"|?*]/u.test(name)) throw new Error(`Unsafe ZIP path: ${name}`); const parts = name.replace(/\/$/u, "").split("/"); - if (parts.some(part => !part || part === "." || part === ".." || /[. ]$/u.test(part) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(part))) throw new Error(`Unsafe ZIP path: ${name}`); + if (parts.some(part => !part || part === "." || part === ".." || /[. ]$/u.test(part) || /^(con|prn|aux|nul|com[1-9\u00b9\u00b2\u00b3]|lpt[1-9\u00b9\u00b2\u00b3])(?:\.|$)/iu.test(part))) throw new Error(`Unsafe ZIP path: ${name}`); return name; } @@ -27,24 +28,58 @@ function safeSize(value) { return value; } -async function signature(entry) { - if (entry.uncompressedSize < 4) return false; +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[0] === 0x7f && header[1] === 69 && header[2] === 76 && header[3] === 70; + 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 = [ + [0x4f, 0x15, 0x3d, 0x1d], + [0x54, 0x14, 0xf5, 0xee] +]; + +function isElf(header) { + return header !== null && header[0] === 0x7f && header[1] === 69 && header[2] === 76 && header[3] === 70; +} + +function isSelf(header) { + return header !== null && SELF_MAGICS.some(magic => magic.every((byte, index) => header[index] === byte)); } async function boundedBytes(entry, limit) { @@ -97,6 +132,10 @@ export class Archive { let total = 0; for await (const entry of this.reader.getEntriesGenerator()) { if (this.entries.length >= MAX_ENTRIES) throw new Error("Input contains more than 100,000 entries"); + if (Number.isInteger(entry.versionMadeBy) && entry.versionMadeBy >> 8 === 0 && entry.filename.includes("\\")) { + entry.filename = entry.filename.replaceAll("\\", "/"); + if (entry.filename.endsWith("/")) entry.directory = true; + } validPath(entry.filename); const key = entry.filename.replace(/\/$/u, "").toLowerCase(); if (paths.has(key)) throw new Error(`Duplicate ZIP path: ${entry.filename}`); @@ -109,51 +148,88 @@ 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("/"))) { const first = files[0].filename.slice(prefix.length).split("/")[0]; - if (["sce_sys", "sce_module"].includes(first) || !this.entries.every(entry => entry.filename === prefix || entry.filename.startsWith(prefix + first + "/"))) break; + if (["sce_sys", "sce_module"].includes(first) || !this.entries.every(entry => (entry.directory && prefix.startsWith(entry.filename)) || entry.filename.startsWith(prefix + first + "/"))) break; prefix += first + "/"; } this.prefix = prefix; this.files = this.entries.filter(entry => entry.filename.startsWith(prefix) && entry.filename !== prefix).map(entry => ({ entry, name: entry.filename.slice(prefix.length) })); const executables = []; this.modules = []; + const selfContainers = []; + const selfModules = []; for (let index = 0; index < this.files.length; index++) { const item = this.files[index]; report({ phase: "Inspecting files", input: item.name, completed: index, total: this.files.length }); if (item.entry.directory || item.name.startsWith("sce_sys/")) continue; if (!item.name.startsWith("sce_module/") && item.name.includes("/")) continue; if (item.name.startsWith("sce_module/") && item.name.endsWith(".guest.prx")) continue; - const elf = await signature(item.entry); + const header = await headerBytes(item.entry); if (item.name.startsWith("sce_module/")) { - if (elf) this.modules.push(item); - } else if (elf) executables.push(item); + if (isElf(header)) this.modules.push(item); + else if (isSelf(header)) selfModules.push(item.name); + } else if (isElf(header)) { + executables.push(item); + } else if (isSelf(header)) { + selfContainers.push(item.name); + } + } + if (executables.length !== 1) { + if (selfContainers.length > 0) throw new Error(`${selfContainers.join(", ")} is a SELF container, not an ELF: unwrap the app before converting it`); + throw new Error(`Expected exactly one executable ELF in the app root; found ${executables.length}`); } - if (executables.length !== 1) throw new Error(`Expected exactly one executable ELF in the app root; found ${executables.length}`); this.executable = executables[0]; if (this.executable.name.includes("/")) throw new Error("Place the executable next to sce_sys and sce_module, not inside another folder"); this.icon = this.files.find(item => item.name === "sce_sys/icon0.png" && !item.entry.directory); - if (this.icon && this.icon.entry.uncompressedSize > ICON_LIMIT) throw new Error("sce_sys/icon0.png exceeds 16 MiB"); - this.selectedBytes = [this.executable, ...this.modules, ...(this.icon ? [this.icon] : [])].reduce((sum, item) => sum + item.entry.uncompressedSize, 0); - if (this.selectedBytes > CONVERSION_LIMIT) throw new Error("Executable, modules and icon exceed the 256 MiB conversion limit. Use the native relinker for this app."); + this.selectedBytes = [this.executable, ...this.modules].reduce((sum, item) => sum + item.entry.uncompressedSize, 0); + this.windowsSelectedBytes = this.selectedBytes + (this.icon?.entry.uncompressedSize ?? 0); + if (this.selectedBytes > CONVERSION_LIMIT) throw new Error("Executable and modules exceed the 256 MiB conversion limit. Use the native relinker for this app."); const warnings = []; if (!this.files.some(item => item.name.startsWith("sce_sys/"))) warnings.push("sce_sys is missing. The Windows executable will have no app icon."); else if (!this.icon) warnings.push("sce_sys/icon0.png is missing. The Windows executable will have no app icon."); if (!this.files.some(item => item.name.startsWith("sce_module/"))) warnings.push("sce_module is missing. If the original app included this folder, leaving it out can prevent the app from starting."); + if (selfModules.length > 0) warnings.push(`${selfModules.join(", ")} is a SELF container and was skipped: unwrap it to include that module.`); this.totalBytes = total; - return { name: this.executable.name, files: files.length, modules: this.modules.length, totalBytes: total, selectedBytes: this.selectedBytes, root: prefix, warnings }; + return { name: this.executable.name, files: files.length, modules: this.modules.length, totalBytes: total, selectedBytes: this.selectedBytes, windowsSelectedBytes: this.windowsSelectedBytes, windowsIconTooLarge: (this.icon?.entry.uncompressedSize ?? 0) > ICON_LIMIT, root: prefix, warnings }; + } + + selectedBytesForTarget(target) { + if (target !== "linux" && target !== "windows") throw new Error(`Unsupported conversion target: ${target}`); + if (target === "windows" && (this.icon?.entry.uncompressedSize ?? 0) > ICON_LIMIT) throw new Error("sce_sys/icon0.png exceeds 16 MiB"); + return target === "windows" ? this.windowsSelectedBytes : this.selectedBytes; } async conversionInput(target, toIntel, report) { let remaining = CONVERSION_LIMIT; + const selectedBytes = this.selectedBytesForTarget(target); + if (selectedBytes > CONVERSION_LIMIT) throw new Error(`Selected ${target} conversion input exceeds the 256 MiB conversion limit. Use the native relinker for this app.`); const read = async item => { report({ phase: "Reading conversion input", input: item.name }); const bytes = await boundedBytes(item.entry, remaining); @@ -172,8 +248,8 @@ export class Archive { async close() { await this.reader.close(); } } -export function memoryEstimate(archiveBytes, selectedBytes, libraryBytes, entries) { - return safeSize(Math.ceil(archiveBytes * 1.02) + selectedBytes * 4 + libraryBytes + entries * 1024 + 8 * 1024 * 1024); +export function createPackageWriter(destination) { + return new ZipWriter(destination, { level: 9, zip64: true, bufferedWrite: false, useWebWorkers: false, useCompressionStream: false, preventClose: true }); } async function appendEntry(writer, name, entry, report) { @@ -189,13 +265,13 @@ async function appendEntry(writer, name, entry, report) { await Promise.all([ entry.getData(stream.writable, { checkSignature: true }), writer.add(name, stream.readable.pipeThrough(limit), { - versionMadeBy: 0x31e, externalFileAttributes: entry.externalFileAttributes, + versionMadeBy: 0x31e, externalFileAttributes: entry.externalFileAttributes >>> 16 ? entry.externalFileAttributes : ((0o100644 << 16) | (entry.externalFileAttributes & 0xffff)) >>> 0, onprogress: completed => report({ phase: writer instanceof DirectoryWriter ? "Writing files" : "Compressing ZIP", input: name, completed, total: entry.uncompressedSize }) }) ]); } -class DirectoryWriter { +export class DirectoryWriter { constructor(destination) { this.destination = destination; } @@ -223,8 +299,7 @@ class DirectoryWriter { } await writer.close(); } catch (error) { - await reader.cancel(error); - await writer.abort(error); + await Promise.allSettled([reader.cancel(error), writer.abort(error)]); throw error; } finally { reader.releaseLock(); @@ -239,7 +314,7 @@ class DirectoryWriter { } await writer.close(); } catch (error) { - await writer.abort(error); + await Promise.allSettled([writer.abort(error)]); throw error; } finally { writer.releaseLock(); @@ -250,13 +325,19 @@ class DirectoryWriter { async close() {} } -export async function packageArchive(archive, converted, outputName, libraryFile, destination, report, directory = false) { +export async function packageArchive(archive, converted, outputName, libraryFile, destination, report, directory = false, options = {}) { const libraries = new ZipReader(new BoundedBlobReader(libraryFile)); - const writer = directory ? new DirectoryWriter(destination) : new ZipWriter(destination, { level: 9, zip64: true, bufferedWrite: false, useWebWorkers: false, useCompressionStream: false, preventClose: true }); + const ownsWriter = !options.writer; + const writer = options.writer ?? (directory ? new DirectoryWriter(destination) : createPackageWriter(destination)); + const outputPath = name => { + const path = options.prefix ? `${options.prefix}${name}` : name; + validPath(path); + return path; + }; const names = new Set(); const claim = name => { - validPath(name); - const key = name.replace(/\/$/u, "").toLowerCase(); + const path = outputPath(name); + const key = path.replace(/\/$/u, "").toLowerCase(); if (names.has(key)) throw new Error(`Output path collision: ${name}`); names.add(key); }; @@ -276,19 +357,34 @@ 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))); - for (const module of converted.modules) await writer.add(module.name, new Uint8ArrayReader(module.bytes), binary); + await writer.add(outputPath(outputName), new Uint8ArrayReader(converted.executable), binary); + if (converted.diagnostics) await writer.add(outputPath("windows-diagnostics-imports.txt"), new Uint8ArrayReader(new TextEncoder().encode(converted.diagnostics))); + for (const module of converted.modules) await writer.add(outputPath(module.name), new Uint8ArrayReader(module.bytes), binary); for (const entry of libraryEntries) { - if (entry.directory) await writer.add(entry.filename, undefined, { directory: true }); - else await appendEntry(writer, entry.filename, entry, report); + if (entry.directory) await writer.add(outputPath(entry.filename), undefined, { directory: true }); + else await appendEntry(writer, outputPath(entry.filename), entry, report); } for (const { entry, name } of originalFiles) { - if (entry.directory) await writer.add("app0/" + name, undefined, { directory: true }); - else await appendEntry(writer, "app0/" + name, entry, report); + if (entry.directory) await writer.add(outputPath("app0/" + name), undefined, { directory: true }); + else await appendEntry(writer, outputPath("app0/" + name), entry, report); } - await writer.close(); + if (ownsWriter) await writer.close(); } finally { await libraries.close(); } diff --git a/demo/web/batch.mjs b/demo/web/batch.mjs new file mode 100644 index 0000000000..5058dbea4f --- /dev/null +++ b/demo/web/batch.mjs @@ -0,0 +1,27 @@ +export const MEMORY_LIMIT = 256 * 1024 * 1024; + +function safeSize(value) { + if (!Number.isSafeInteger(value) || value < 0) throw new Error("ZIP contains an unsupported file size"); + return value; +} + +export function memoryEstimate(archiveBytes, selectedBytes, libraryBytes, entries) { + return safeSize(Math.ceil(archiveBytes * 1.02) + selectedBytes * 4 + libraryBytes + entries * 1024 + 8 * 1024 * 1024); +} + +export function batchMemoryEstimate(summaries, target, libraryBytes) { + if (target !== "linux" && target !== "windows") throw new Error(`Unsupported conversion target: ${target}`); + return summaries.reduce((total, summary) => safeSize(total + memoryEstimate( + summary.totalBytes, + target === "windows" ? summary.windowsSelectedBytes : summary.selectedBytes, + libraryBytes, + summary.files + )), 0); +} + +export function batchOutputDirectory(name, index) { + if (!Number.isSafeInteger(index) || index < 0) throw new Error("Invalid app index"); + const stem = String(name).replace(/\.zip$/iu, "").normalize("NFKC") + .replace(/[^A-Za-z0-9._-]+/gu, "-").replace(/^[.-]+/u, "").slice(0, 80).replace(/[. -]+$/u, ""); + return `${String(index + 1).padStart(2, "0")}-${stem || "app"}`; +} diff --git a/demo/web/index.html b/demo/web/index.html index c280516fb8..e0d468febc 100644 --- a/demo/web/index.html +++ b/demo/web/index.html @@ -11,10 +11,10 @@

LOCAL CONVERSION

PS5 apps to Windows/Linux converter

-

Convert an app

Files stay on your device
-

Choose a folder or drop a ZIP

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

ZIP & ZIP64 · Decrypted ELF files only

- - +

Convert apps

Files stay on your device
+

Choose a folder or drop ZIP files

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

ZIP & ZIP64 · Decrypted ELF files only · Multiple ZIPs become one bundle

+ +
Convert for

Choose a ZIP or folder to get started.

@@ -22,7 +22,7 @@
-

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

+

Memory downloads have a 256 MiB budget, checked against expanded files and output overhead. For multiple ZIPs, the limit applies to the combined bundle; larger batches require Save ZIP to disk in desktop Chrome or Edge. Executable and modules are limited to 256 MiB per app; the icon is also limited to 16 MiB and counts toward the Windows conversion limit. Use the native relinker for larger conversion inputs.

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

diff --git a/demo/web/output-directory.mjs b/demo/web/output-directory.mjs new file mode 100644 index 0000000000..ee199e1341 --- /dev/null +++ b/demo/web/output-directory.mjs @@ -0,0 +1,44 @@ +export class OutputDirectoryTransaction { + #state = "open"; + + constructor(parent, name, directory) { + this.parent = parent; + this.name = name; + this.directory = directory; + } + + static async create(parent, name) { + try { + await parent.getDirectoryHandle(name); + } catch (error) { + if (error.name !== "NotFoundError") throw error; + const directory = await parent.getDirectoryHandle(name, { create: true }); + return new OutputDirectoryTransaction(parent, name, directory); + } + throw new Error(`The folder ${name} already exists. Choose another destination or rename the existing folder.`); + } + + commit() { + if (this.#state !== "open") throw new Error("Output directory transaction is no longer active"); + this.#state = "committed"; + } + + async discard() { + if (this.#state !== "open") return; + await this.parent.removeEntry(this.name, { recursive: true }); + this.#state = "discarded"; + } +} + +export async function abortAndDiscardOutput(transaction, sink) { + const failures = []; + if (sink) { + try { await sink.abort(); } + catch (error) { failures.push(`Could not discard the output file: ${error instanceof Error ? error.message : String(error)}`); } + } + if (transaction) { + try { await transaction.discard(); } + catch (error) { failures.push(`Could not remove incomplete output folder ${transaction.name}/: ${error instanceof Error ? error.message : String(error)}`); } + } + return failures; +} diff --git a/demo/web/tests/archive-reserved.test.mjs b/demo/web/tests/archive-reserved.test.mjs new file mode 100644 index 0000000000..b04432d014 --- /dev/null +++ b/demo/web/tests/archive-reserved.test.mjs @@ -0,0 +1,31 @@ +import assert from "node:assert/strict"; +import { register } from "node:module"; +import { pathToFileURL } from "node:url"; +import path from "node:path"; + +register("./cdn-loader.mjs", import.meta.url, { data: { url: process.env.ANYPS5_TEST_ZIP_URL, path: process.env.ANYPS5_TEST_ZIP_PATH } }); +const { Archive, validPath } = await import(pathToFileURL(path.join(process.argv[2], "archive.mjs"))); +const { BlobWriter, Uint8ArrayReader, ZipWriter } = await import(process.env.ANYPS5_TEST_ZIP_URL); +const header = new Uint8Array([127, 69, 76, 70]); + +for (const prefix of ["COM", "lpt"]) { + for (const digit of ["\u00b9", "\u00b2", "\u00b3"]) { + for (const name of [prefix + digit, prefix + digit + ".txt", "content/" + prefix + digit + "/data.bin"]) { + assert.throws(() => validPath(name), /Unsafe ZIP path/); + const folder = new Archive([{ path: "main.elf", file: new Blob([header]) }, { path: name, file: new Blob(["resource"]) }]); + try { await assert.rejects(folder.inspect(() => {}), /Unsafe ZIP path/); } + finally { await folder.close(); } + } + } +} + +const writer = new ZipWriter(new BlobWriter(), { useWebWorkers: false, useCompressionStream: false }); +await writer.add("main.elf", new Uint8ArrayReader(header)); +await writer.add("content/COM\u00b9.txt", new Uint8ArrayReader(new Uint8Array([1]))); +const archive = new Archive(await writer.close()); +try { await assert.rejects(archive.inspect(() => {}), /Unsafe ZIP path/); } +finally { await archive.close(); } + +for (const name of ["COM10", "COM\u2074.txt", "COM\u00b9-extra", "content/ordinary.txt"]) { + assert.equal(validPath(name), name); +} diff --git a/demo/web/tests/archive-root.test.mjs b/demo/web/tests/archive-root.test.mjs new file mode 100644 index 0000000000..e5ee350ac2 --- /dev/null +++ b/demo/web/tests/archive-root.test.mjs @@ -0,0 +1,288 @@ +import assert from "node:assert/strict"; +import { register } from "node:module"; +import { pathToFileURL } from "node:url"; +import path from "node:path"; + +register("./cdn-loader.mjs", import.meta.url, { data: { url: process.env.ANYPS5_TEST_ZIP_URL, path: process.env.ANYPS5_TEST_ZIP_PATH } }); +const { Archive, DirectoryWriter } = await import(pathToFileURL(path.join(process.argv[2], "archive.mjs"))); +const { BlobWriter, Uint8ArrayReader, ZipWriter } = await import(process.env.ANYPS5_TEST_ZIP_URL); +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, 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([ + ...directories.map(name => [name, true]), ["outer/inner/title/main.elf", false] + ])); + try { + const summary = await archive.inspect(() => {}); + assert.equal(summary.root, "outer/inner/title/"); + assert.equal(summary.name, "main.elf"); + assert.equal(summary.files, 1); + assert.deepEqual((await archive.conversionInput("linux", false, () => {})).executable, header); + } 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(); } + +const siblings = new Archive(await zipped([["outer/", true], ["outer/main.elf", false], ["other/resource", false]])); +try { await assert.rejects(siblings.inspect(() => {}), /exactly one executable ELF/); } +finally { await siblings.close(); } + +for (const cleanupRejects of [false, true]) { + const failure = new Error("input read failed"); + let aborted = false; + const output = new WritableStream({ abort(reason) { + aborted = true; + assert.equal(reason, failure); + if (cleanupRejects) throw new Error("abort failed"); + } }); + const source = new ReadableStream({ start(controller) { controller.error(failure); } }); + const writer = new DirectoryWriter({ async file() { return output; } }); + await assert.rejects(writer.add("example.bin", source), error => error === failure); + assert.equal(aborted, true); + assert.equal(output.locked, false); + assert.equal(source.locked, false); +} + +{ + const failure = new Error("progress failed"); + let cancelled = false; + let aborted = false; + const source = new ReadableStream({ + start(controller) { controller.enqueue(new Uint8Array([1])); }, + cancel(reason) { cancelled = true; assert.equal(reason, failure); throw new Error("cancel failed"); } + }); + const output = new WritableStream({ abort(reason) { aborted = true; assert.equal(reason, failure); } }); + const writer = new DirectoryWriter({ async file() { return output; } }); + await assert.rejects(writer.add("example.bin", source, { onprogress() { throw failure; } }), error => error === failure); + assert.equal(cancelled, true); + assert.equal(aborted, true); + assert.equal(output.locked, false); + assert.equal(source.locked, false); +} + +{ + const failure = new Error("blob read failed"); + let aborted = false; + const output = new WritableStream({ abort(reason) { + aborted = true; + assert.equal(reason, failure); + throw new Error("abort failed"); + } }); + const source = { size: 1, async init() {}, async readUint8Array() { throw failure; } }; + const writer = new DirectoryWriter({ async file() { return output; } }); + await assert.rejects(writer.add("example.bin", source), error => error === failure); + assert.equal(aborted, true); + assert.equal(output.locked, false); +} diff --git a/demo/web/tests/archive-windows.test.mjs b/demo/web/tests/archive-windows.test.mjs new file mode 100644 index 0000000000..97626da702 --- /dev/null +++ b/demo/web/tests/archive-windows.test.mjs @@ -0,0 +1,46 @@ +import assert from "node:assert/strict"; +import { register } from "node:module"; +import { pathToFileURL } from "node:url"; +import path from "node:path"; + +register("./cdn-loader.mjs", import.meta.url, { data: { url: process.env.ANYPS5_TEST_ZIP_URL, path: process.env.ANYPS5_TEST_ZIP_PATH } }); +const { Archive, packageArchive } = await import(pathToFileURL(path.join(process.argv[2], "archive.mjs"))); +const { BlobReader, BlobWriter, Uint8ArrayReader, ZipReader, ZipWriter } = await import(process.env.ANYPS5_TEST_ZIP_URL); +const header = new Uint8Array([127, 69, 76, 70]); +const dos = { versionMadeBy: 0x14, externalFileAttributes: 0x20 }; + +async function zipped(entries, options) { + const writer = new ZipWriter(new BlobWriter(), { useWebWorkers: false, useCompressionStream: false }); + for (const [name, directory] of entries) { + if (directory) await writer.add(name, new Uint8ArrayReader(new Uint8Array()), { ...options, externalFileAttributes: 0x10 }); + else await writer.add(name, new Uint8ArrayReader(header), options); + } + return writer.close(); +} + +const windows = new Archive(await zipped([["title\\main.elf", false], ["title\\sce_sys\\param.json", false], ["title\\data\\", true]], dos)); +const output = []; +try { + const summary = await windows.inspect(() => {}); + assert.equal(summary.root, "title/"); + assert.equal(summary.name, "main.elf"); + assert.deepEqual(windows.files.map(item => item.name).sort(), ["data/", "main.elf", "sce_sys/param.json"]); + const libraries = new ZipWriter(new BlobWriter(), { useWebWorkers: false, useCompressionStream: false }); + await libraries.add("libs/libc.prx", new Uint8ArrayReader(header), { versionMadeBy: 0x31e, externalFileAttributes: (0o100644 << 16) >>> 0 }); + await packageArchive(windows, { executable: header, modules: [] }, "main.elf", await libraries.close(), new WritableStream({ write(chunk) { output.push(chunk.slice()); } }), () => {}); +} finally { await windows.close(); } +const packaged = new ZipReader(new BlobReader(new Blob(output))); +try { + const modes = Object.fromEntries((await packaged.getEntries()).filter(entry => !entry.directory).map(entry => [entry.filename, entry.externalFileAttributes >>> 16])); + assert.equal(modes["app0/main.elf"], 0o100644); + assert.equal(modes["app0/sce_sys/param.json"], 0o100644); + assert.equal(modes["main.elf"], 0o100755); +} finally { await packaged.close(); } + +const escape = new Archive(await zipped([["title\\main.elf", false], ["title\\..\\..\\evil", false]], dos)); +try { await assert.rejects(escape.inspect(() => {}), /Unsafe ZIP path: title\/\.\.\/\.\.\/evil/); } +finally { await escape.close(); } + +const unix = new Archive(await zipped([["title\\main.elf", false]], { versionMadeBy: 0x31e, externalFileAttributes: (0o100644 << 16) >>> 0 })); +try { await assert.rejects(unix.inspect(() => {}), /Unsafe ZIP path: title\\main\.elf/); } +finally { await unix.close(); } diff --git a/demo/web/tests/archive.test.mjs b/demo/web/tests/archive.test.mjs index 4470741f74..d0c2ca7c77 100644 --- a/demo/web/tests/archive.test.mjs +++ b/demo/web/tests/archive.test.mjs @@ -7,8 +7,9 @@ import { register } from "node:module"; register("./cdn-loader.mjs", import.meta.url, { data: { url: process.env.ANYPS5_TEST_ZIP_URL, path: process.env.ANYPS5_TEST_ZIP_PATH } }); const site = name => pathToFileURL(path.join(process.argv[2], name)).href; -const { Archive, memoryEstimate, MEMORY_LIMIT, packageArchive } = await import(site("archive.mjs")); +const { Archive, memoryEstimate, MEMORY_LIMIT, ICON_LIMIT, packageArchive } = await import(site("archive.mjs")); const { BlobReader, BlobWriter, Uint8ArrayReader, Uint8ArrayWriter, ZipReader, ZipWriter } = await import(process.env.ANYPS5_TEST_ZIP_URL); +await import("./archive-reserved.test.mjs"); const { default: createRelinker } = await import(site("native/relinker_wasm.mjs")); const native = await createRelinker(); const executable = new Uint8Array(await readFile(path.join(process.argv[3], "main.elf"))); @@ -110,9 +111,34 @@ for (const target of ["windows", "linux"]) { } } +const bundleArchive = new Archive(source); +await bundleArchive.inspect(() => {}); +const bundleInput = await bundleArchive.conversionInput("linux", false, () => {}); +const bundleConversion = native.Convert(bundleInput, () => {}); +assert.equal(bundleConversion.ok, true, bundleConversion.error); +const bundleLibraries = await zip([["libs/system.prx", new Uint8Array([1, 2, 3])]]); +const bundleWriter = new ZipWriter(new BlobWriter("application/zip"), { zip64: true, level: 9, useWebWorkers: false, useCompressionStream: false }); +await packageArchive(bundleArchive, bundleConversion, bundleInput.outputName, bundleLibraries, null, () => {}, false, { prefix: "01-first/", writer: bundleWriter }); +await packageArchive(bundleArchive, bundleConversion, bundleInput.outputName, bundleLibraries, null, () => {}, false, { prefix: "02-second/", writer: bundleWriter }); +const bundle = await bundleWriter.close(); +const bundleReader = new ZipReader(new BlobReader(bundle)); +const bundleEntries = await bundleReader.getEntries(); +const bundleNames = new Set(bundleEntries.map(entry => entry.filename)); +assert(bundleNames.has(`01-first/${bundleInput.outputName}`)); +assert(bundleNames.has("01-first/libs/system.prx")); +assert(bundleNames.has("01-first/app0/content/resource.bin")); +assert(bundleNames.has(`02-second/${bundleInput.outputName}`)); +assert(bundleNames.has("02-second/libs/system.prx")); +assert(bundleNames.has("02-second/app0/content/resource.bin")); +await bundleReader.close(); +await bundleArchive.close(); +console.log("Multiple app packages append under separate roots in a single ZIP"); + for (const [entries, error] of [ [[["one", executable], ["two", executable]], /exactly one/u], [[["readme", new Uint8Array([1])]], /exactly one/u], + [[["eboot.bin", new Uint8Array([79, 21, 61, 29])]], /SELF container/u], + [[["eboot.bin", new Uint8Array([84, 20, 245, 238])]], /SELF container/u], [[["app", executable], ["..\/escape", resource]], /Unsafe ZIP/u], [[["app", executable], ["APP", resource]], /Duplicate ZIP/u], [[["app", executable], ["folder", resource], ["folder/file", resource]], /also used as a folder/u], @@ -125,17 +151,127 @@ for (const [entries, error] of [ const missing = new Archive(await zip([["any-name", executable]])); assert.equal((await missing.inspect(() => {})).warnings.length, 2); await missing.close(); +const selfModule = new Archive(await zip([["program.dat", executable],["sce_module/library.prx", new Uint8Array([84, 20, 245, 238])]])); +const selfModuleSummary = await selfModule.inspect(() => {}); +assert.equal(selfModuleSummary.warnings.length, 2); +assert.match(selfModuleSummary.warnings.join(" "), /SELF container/u); +await selfModule.close(); assert(memoryEstimate(200 * 1024 ** 3, 1024, 1024, 100) > MEMORY_LIMIT); const invalid = native.Convert({ target: "unsupported" }, () => {}); assert.equal(invalid.ok, false); console.log("Archive validation, missing-folder warnings and large-download budget verified"); +const iconBytes = new Uint8Array(1024 * 1024); +const iconArchive = new Archive([ + { path: "title/program.elf", file: new Blob([new Uint8Array([0x7f, 69, 76, 70])]) }, + { path: "title/sce_sys/icon0.png", file: new Blob([iconBytes]) } +]); +const iconSummary = await iconArchive.inspect(() => {}); +assert.equal(iconSummary.selectedBytes, 4); +assert.equal(iconSummary.windowsSelectedBytes, iconBytes.length + 4); +assert.equal((await iconArchive.conversionInput("linux", false, () => {})).icon, null); +assert.equal((await iconArchive.conversionInput("windows", false, () => {})).icon.length, iconBytes.length); +const librariesAtBudgetEdge = MEMORY_LIMIT + - memoryEstimate(iconSummary.totalBytes, iconSummary.selectedBytes, 0, iconSummary.files) + - iconBytes.length * 2; +assert(memoryEstimate(iconSummary.totalBytes, iconSummary.selectedBytes, librariesAtBudgetEdge, iconSummary.files) < MEMORY_LIMIT); +assert(memoryEstimate(iconSummary.totalBytes, iconSummary.windowsSelectedBytes, librariesAtBudgetEdge, iconSummary.files) > MEMORY_LIMIT); +await iconArchive.close(); +console.log("Linux conversion input and memory estimate exclude the Windows-only icon"); + +const oversizedIconArchive = new Archive([ + { path: "title/program.elf", file: new Blob([new Uint8Array([0x7f, 69, 76, 70])]) }, + { path: "title/sce_sys/icon0.png", file: new Blob([new Uint8Array(ICON_LIMIT + 1)]) } +]); +const oversizedIconSummary = await oversizedIconArchive.inspect(() => {}); +assert.equal(oversizedIconSummary.windowsIconTooLarge, true); +assert.equal(oversizedIconArchive.selectedBytesForTarget("linux"), 4); +assert.throws(() => oversizedIconArchive.selectedBytesForTarget("windows"), /16 MiB/u); +assert.equal((await oversizedIconArchive.conversionInput("linux", false, () => {})).icon, null); +await oversizedIconArchive.close(); +console.log("Oversized Windows-only icons do not block Linux conversion"); + const workerMessages = []; globalThis.self = { postMessage: message => workerMessages.push(message) }; -await import(site("worker.mjs")); +const worker = await import(site("worker.mjs")); +const libraryArchive = await zip([["libs/system.prx", new Uint8Array([1, 2, 3])]]); +const libraryBytes = new Uint8Array(await libraryArchive.arrayBuffer()); +const libraryPath = "libraries/windows-acde1234.zip"; +const librarySha256 = [...new Uint8Array(await crypto.subtle.digest("SHA-256", libraryBytes))] + .map(byte => byte.toString(16).padStart(2, "0")) + .join(""); +const release = { + tag: "v1", + platforms: { + windows: { + path: libraryPath, + size: libraryBytes.byteLength, + expandedBytes: 3, + sha256: librarySha256 + } + } +}; +const originalFetch = globalThis.fetch; +let latestResponse = new Response(null, { status: 403, headers: { "X-RateLimit-Remaining": "0" } }); +globalThis.fetch = async url => { + if (url === "./release.json") return new Response(JSON.stringify(release)); + if (url === "https://api.github.com/repos/boykopovar/AnyPS5/releases/latest") return latestResponse; + if (url === libraryPath) return new Response(libraryBytes); + throw new Error(`Unexpected fetch: ${url}`); +}; +try { + const downloaded = await worker.downloadLibraries("windows"); + assert.equal(downloaded.release.tag, release.tag); + assert.deepEqual(new Uint8Array(await downloaded.file.arrayBuffer()), libraryBytes); + assert(workerMessages.some(message => message.type === "progress" && /rate limit reached/u.test(message.message ?? ""))); + + latestResponse = new Response(null, { status: 429 }); + const rateLimitedDownload = await worker.downloadLibraries("windows"); + assert.deepEqual(new Uint8Array(await rateLimitedDownload.file.arrayBuffer()), libraryBytes); + + latestResponse = new Response(null, { status: 403 }); + await assert.rejects(worker.downloadLibraries("windows"), /Cannot verify the latest release \(403\)/u); + latestResponse = new Response(JSON.stringify({ tag_name: "v2" })); + await assert.rejects(worker.downloadLibraries("windows"), /A new release is being published/u); + + latestResponse = new Response(null, { status: 429 }); + await self.onmessage({ data: { type: "inspect", inputs: [ + { name: "First App.zip", file: source }, + { name: "First App.zip", file: source } + ] } }); + assert.equal(workerMessages.filter(message => message.type === "inspected").length, 2); + assert(workerMessages.some(message => message.type === "inspection-complete" && message.count === 2)); + await self.onmessage({ data: { type: "convert", target: "windows", toIntel: false, mode: "memory" } }); + const batchComplete = workerMessages.find(message => message.type === "complete"); + assert(batchComplete, workerMessages.find(message => message.type === "error")?.message); + assert.equal(batchComplete.apps, 2); + const batchReader = new ZipReader(new BlobReader(batchComplete.blob)); + const batchNames = new Set((await batchReader.getEntries()).map(entry => entry.filename)); + assert(batchNames.has("01-First-App/program.exe")); + assert(batchNames.has("01-First-App/libs/system.prx")); + assert(batchNames.has("01-First-App/app0/content/resource.bin")); + assert(batchNames.has("02-First-App/program.exe")); + assert(batchNames.has("02-First-App/libs/system.prx")); + assert(batchNames.has("02-First-App/app0/content/resource.bin")); + await batchReader.close(); + workerMessages.length = 0; +} finally { + globalThis.fetch = originalFetch; + workerMessages.length = 0; +} await self.onmessage({ data: { type: "convert", target: "windows", mode: "disk" } }); assert.equal(workerMessages.length, 1); assert.equal(workerMessages[0].type, "error"); assert.match(workerMessages[0].message, /browsers block DLL creation/u); +workerMessages.length = 0; +await self.onmessage({ data: { type: "inspect", inputs: [ + { name: "valid.zip", file: source }, + { name: "invalid.zip", file: new Blob(["not a ZIP"]) } +] } }); +assert.equal(workerMessages.at(-1).type, "error"); +assert.match(workerMessages.at(-1).message, /invalid\.zip/u); +assert.equal(workerMessages.some(message => message.type === "inspection-complete"), false); delete globalThis.self; +console.log("Rate-limited release checks use the verified site manifest; other release-check failures remain errors"); console.log("Windows folder output rejected before conversion or file writes"); +console.log("Multiple ZIP inputs stream into separate app roots; invalid inputs name the failing archive"); diff --git a/demo/web/tests/batch.test.mjs b/demo/web/tests/batch.test.mjs new file mode 100644 index 0000000000..b19827d72b --- /dev/null +++ b/demo/web/tests/batch.test.mjs @@ -0,0 +1,30 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { batchMemoryEstimate, batchOutputDirectory, memoryEstimate, MEMORY_LIMIT } from "../batch.mjs"; + +test("estimates all ZIP outputs against one shared memory limit", () => { + const summaries = [ + { totalBytes: 1024, selectedBytes: 128, windowsSelectedBytes: 256, files: 2 }, + { totalBytes: 2048, selectedBytes: 256, windowsSelectedBytes: 512, files: 3 } + ]; + + assert.equal(batchMemoryEstimate(summaries, "linux", 1024), summaries.reduce( + (total, summary) => total + memoryEstimate(summary.totalBytes, summary.selectedBytes, 1024, summary.files), + 0 + )); + assert.equal(batchMemoryEstimate(summaries, "windows", 1024), summaries.reduce( + (total, summary) => total + memoryEstimate(summary.totalBytes, summary.windowsSelectedBytes, 1024, summary.files), + 0 + )); + assert.throws(() => batchMemoryEstimate([], "macos", 0), /Unsupported conversion target/u); + assert.throws(() => batchMemoryEstimate([{ totalBytes: Number.MAX_SAFE_INTEGER, selectedBytes: 0, windowsSelectedBytes: 0, files: 0 }], "linux", 0), /unsupported file size/u); + assert.equal(MEMORY_LIMIT, 256 * 1024 * 1024); +}); + +test("creates safe and distinct ZIP folder names for duplicate app filenames", () => { + assert.equal(batchOutputDirectory("First App.zip", 0), "01-First-App"); + assert.equal(batchOutputDirectory("First App.zip", 1), "02-First-App"); + assert.equal(batchOutputDirectory("CON.zip", 0), "01-CON"); + assert.equal(batchOutputDirectory("...zip", 0), "01-app"); + assert.throws(() => batchOutputDirectory("app.zip", -1), /Invalid app index/u); +}); diff --git a/demo/web/tests/output-directory.test.mjs b/demo/web/tests/output-directory.test.mjs new file mode 100644 index 0000000000..873572a738 --- /dev/null +++ b/demo/web/tests/output-directory.test.mjs @@ -0,0 +1,116 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { abortAndDiscardOutput, OutputDirectoryTransaction } from "../output-directory.mjs"; + +function missingDirectory() { + const error = new Error("not found"); + error.name = "NotFoundError"; + return error; +} + +test("creates a new output root only after confirming the destination is absent", async () => { + const directory = {}; + const calls = []; + const parent = { + async getDirectoryHandle(name, options) { + calls.push([name, options]); + if (!options) throw missingDirectory(); + return directory; + }, + async removeEntry() {} + }; + + const transaction = await OutputDirectoryTransaction.create(parent, "app-linux"); + + assert.equal(transaction.directory, directory); + assert.deepEqual(calls, [["app-linux", undefined], ["app-linux", { create: true }]]); +}); + +test("does not claim or delete a destination that already exists", async () => { + const parent = { + async getDirectoryHandle() { return {}; }, + async removeEntry() { assert.fail("existing destination must not be removed"); } + }; + + await assert.rejects( + OutputDirectoryTransaction.create(parent, "app-linux"), + /already exists/u + ); +}); + +test("discards its incomplete output recursively", async () => { + let removed; + const parent = { + async getDirectoryHandle(name, options) { + if (!options) throw missingDirectory(); + return { name }; + }, + async removeEntry(...args) { removed = args; } + }; + const transaction = await OutputDirectoryTransaction.create(parent, "app-linux"); + + await transaction.discard(); + await transaction.discard(); + + assert.deepEqual(removed, ["app-linux", { recursive: true }]); +}); + +test("keeps completed output and reports cleanup failures", async () => { + let removeCount = 0; + const parent = { + async getDirectoryHandle(name, options) { + if (!options) throw missingDirectory(); + return { name }; + }, + async removeEntry() { + removeCount++; + throw new Error("permission denied"); + } + }; + const completed = await OutputDirectoryTransaction.create(parent, "complete-linux"); + completed.commit(); + await completed.discard(); + assert.equal(removeCount, 0); + + const failed = await OutputDirectoryTransaction.create(parent, "partial-linux"); + await assert.rejects(failed.discard(), /permission denied/u); + assert.equal(removeCount, 1); +}); + +test("aborts an open file before removing the incomplete output tree", async () => { + const calls = []; + const parent = { + async getDirectoryHandle(name, options) { + if (!options) throw missingDirectory(); + return { name }; + }, + async removeEntry(name, options) { calls.push(["remove", name, options]); } + }; + const transaction = await OutputDirectoryTransaction.create(parent, "partial-linux"); + let finishAbort; + const sink = { abort() { calls.push(["abort"]); return new Promise(resolve => { finishAbort = resolve; }); } }; + + const cleanup = abortAndDiscardOutput(transaction, sink); + await Promise.resolve(); + assert.deepEqual(calls, [["abort"]]); + finishAbort(); + assert.deepEqual(await cleanup, []); + assert.deepEqual(calls, [["abort"], ["remove", "partial-linux", { recursive: true }]]); +}); + +test("returns sink and folder cleanup failures without hiding either", async () => { + const parent = { + async getDirectoryHandle(name, options) { + if (!options) throw missingDirectory(); + return { name }; + }, + async removeEntry() { throw new Error("directory permission denied"); } + }; + const transaction = await OutputDirectoryTransaction.create(parent, "partial-linux"); + const sink = { async abort() { throw new Error("file is busy"); } }; + + assert.deepEqual(await abortAndDiscardOutput(transaction, sink), [ + "Could not discard the output file: file is busy", + "Could not remove incomplete output folder partial-linux/: directory permission denied" + ]); +}); diff --git a/demo/web/worker.mjs b/demo/web/worker.mjs index 9a0017792f..51d63e03e0 100644 --- a/demo/web/worker.mjs +++ b/demo/web/worker.mjs @@ -1,6 +1,9 @@ -import { Archive, MEMORY_LIMIT, memoryEstimate, packageArchive } from "./archive.mjs"; +import { batchMemoryEstimate, batchOutputDirectory, MEMORY_LIMIT } from "./batch.mjs"; +import { Archive, CONVERSION_LIMIT, createPackageWriter, packageArchive } from "./archive.mjs"; let archive; +let inputs = []; +let summaries = []; let busy = false; let pendingWrite; let lastReport = 0; @@ -15,26 +18,81 @@ function report(event) { self.postMessage({ type: "progress", ...event }); } -async function downloadLibraries(target) { +function reportFor(item, index) { + let started = false; + return event => { + if (!started) { + lastReport = 0; + lastPhase = ""; + started = true; + } + report({ ...event, appIndex: index, appCount: inputs.length, appName: item.name }); + }; +} + +export async function downloadLibraries(target) { report({ phase: "Downloading system libraries" }); const response = await fetch("./release.json", { cache: "no-cache" }); if (!response.ok) throw new Error(`Cannot load release manifest (${response.status})`); const release = await response.json(); const asset = release.platforms[target]; - if (!asset || !/^libraries\/(windows|linux)-[a-f0-9]+\.zip$/u.test(asset.path) || !/^[a-f0-9]{64}$/u.test(asset.sha256)) throw new Error("Invalid release manifest"); + 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) throw new Error(`Cannot verify the latest release (${latestResponse.status}). Try again later.`); - const latest = await latestResponse.json(); - if (latest.tag_name !== release.tag) throw new Error("A new release is being published to this site. Try again after the Pages build finishes."); + if (!latestResponse.ok) { + const rateLimited = latestResponse.status === 429 || (latestResponse.status === 403 && latestResponse.headers.get("x-ratelimit-remaining") === "0"); + if (!rateLimited) throw new Error(`Cannot verify the latest release (${latestResponse.status}). Try again later.`); + report({ phase: "Downloading system libraries", message: "GitHub API rate limit reached; using the libraries published with this site." }); + } else { + const latest = await latestResponse.json(); + if (latest.tag_name !== release.tag) throw new Error("A new release is being published to this site. Try again after the Pages build finishes."); + } const download = await fetch(asset.path); if (!download.ok) throw new Error(`Cannot download system libraries (${download.status})`); - const bytes = await download.arrayBuffer(); - if (bytes.byteLength !== asset.size) throw new Error("System library download is incomplete"); + const 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 }; } +async function convertInput(item, index, native, libraries, data, destination, directory, writer) { + const current = archive ?? new Archive(item.files ?? item.file); + archive = null; + try { + const itemReport = reportFor(item, index); + if (inputs.length > 1) await current.inspect(itemReport); + const input = await current.conversionInput(data.target, data.toIntel, itemReport); + const converted = native.Convert(input, itemReport); + if (!converted.ok) throw new Error(converted.error); + const options = inputs.length > 1 ? { prefix: `${batchOutputDirectory(item.name, index)}/`, writer } : {}; + await packageArchive(current, converted, input.outputName, libraries.file, destination, itemReport, directory, options); + return input.outputName; + } catch (error) { + throw new Error(`${item.name}: ${error instanceof Error ? error.message : String(error)}`); + } finally { + await current.close(); + } +} + self.onmessage = async ({ data }) => { if (data.type === "written") { if (!pendingWrite) return; @@ -48,25 +106,48 @@ self.onmessage = async ({ data }) => { try { if (data.type === "inspect") { if (archive) await archive.close(); - archive = new Archive(data.files ?? data.file); - const summary = await archive.inspect(report); - self.postMessage({ type: "inspected", summary }); + archive = null; + inputs = data.inputs ?? [{ name: data.file?.name ?? "selected app", file: data.file, files: data.files }]; + if (!inputs.length) throw new Error("Choose at least one ZIP archive."); + summaries = []; + for (let index = 0; index < inputs.length; index++) { + const item = inputs[index]; + const current = new Archive(item.files ?? item.file); + let keepOpen = false; + try { + const summary = await current.inspect(reportFor(item, index)); + summaries.push(summary); + if (inputs.length === 1) { + archive = current; + keepOpen = true; + } + self.postMessage({ type: "inspected", index, count: inputs.length, name: item.name, summary }); + } catch (error) { + throw new Error(`${item.name}: ${error instanceof Error ? error.message : String(error)}`); + } finally { + if (!keepOpen) await current.close(); + } + } + self.postMessage({ type: "inspection-complete", count: inputs.length }); } else if (data.type === "convert") { + if (data.target !== "linux" && data.target !== "windows") throw new Error(`Unsupported conversion target: ${data.target}`); + if (!["memory", "zip", "disk"].includes(data.mode)) throw new Error("Unknown save mode"); if (data.target === "windows" && data.mode === "disk") throw new Error("Windows folder output is unavailable because browsers block DLL creation. Choose a ZIP save mode."); - if (!archive) throw new Error("Select a ZIP or folder first"); + if (!inputs.length || summaries.length !== inputs.length) throw new Error("Inspect the selected apps before converting."); + if (inputs.length > 1 && data.mode === "disk") throw new Error("Batch conversion requires a ZIP save mode."); + for (const summary of summaries) { + const selectedBytes = data.target === "windows" ? summary.windowsSelectedBytes : summary.selectedBytes; + if (data.target === "windows" && summary.windowsIconTooLarge) throw new Error("A Windows icon exceeds 16 MiB. Choose Linux or use the native relinker."); + if (selectedBytes > CONVERSION_LIMIT) throw new Error(`Selected ${data.target} conversion input exceeds the 256 MiB conversion limit. Use the native relinker for this app.`); + } const libraries = await downloadLibraries(data.target); - const estimate = memoryEstimate(archive.totalBytes, archive.selectedBytes, libraries.expandedBytes, archive.entries.length); - if (data.mode === "memory" && estimate > MEMORY_LIMIT) throw new Error("This result exceeds the 256 MiB memory-download budget. Use Save ZIP to disk in desktop Chrome or Edge."); + if (data.mode === "memory" && batchMemoryEstimate(summaries, data.target, libraries.expandedBytes) > MEMORY_LIMIT) throw new Error("This result exceeds the 256 MiB memory-download budget. Use Save ZIP to disk in desktop Chrome or Edge."); report({ phase: "Loading converter" }); const { default: createRelinker } = await import("./native/relinker_wasm.mjs"); const native = await createRelinker(); - let input = await archive.conversionInput(data.target, data.toIntel, report); - const outputName = input.outputName; - const converted = native.Convert(input, report); - input = null; - if (!converted.ok) throw new Error(converted.error); const chunks = []; let size = 0; + let outputName = "anyps5-apps"; const send = (event, transfer = []) => new Promise(resolve => { pendingWrite = resolve; self.postMessage(event, transfer); @@ -86,8 +167,8 @@ self.onmessage = async ({ data }) => { }); } }; - await packageArchive(archive, converted, outputName, libraries.file, destination, report, true); - } else if (data.mode === "memory" || data.mode === "zip") { + outputName = await convertInput(inputs[0], 0, native, libraries, data, destination, true); + } else { const destination = new WritableStream({ async write(chunk) { size += chunk.byteLength; @@ -100,12 +181,17 @@ self.onmessage = async ({ data }) => { chunks.push(chunk.slice()); } }); - await packageArchive(archive, converted, outputName, libraries.file, destination, report); + const writer = inputs.length > 1 ? createPackageWriter(destination) : null; + for (let index = 0; index < inputs.length; index++) { + const name = await convertInput(inputs[index], index, native, libraries, data, destination, false, writer); + if (inputs.length === 1) outputName = name; + } + if (writer) await writer.close(); await destination.close(); - } else throw new Error("Unknown save mode"); - self.postMessage({ type: "complete", mode: data.mode, blob: data.mode === "memory" ? new Blob(chunks, { type: "application/zip" }) : null, outputName, size, release: libraries.release.tag }); - await archive.close(); - archive = null; + } + self.postMessage({ type: "complete", mode: data.mode, blob: data.mode === "memory" ? new Blob(chunks, { type: "application/zip" }) : null, outputName, size, release: libraries.release.tag, apps: inputs.length }); + inputs = []; + summaries = []; } else throw new Error("Unknown worker request"); } catch (error) { self.postMessage({ type: "error", message: error instanceof Error ? error.message : String(error) }); diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 4eaebccf35..32e2509783 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -43,7 +43,23 @@ flowchart LR ``` - [`core/relinker/main.cpp`](../../core/relinker/main.cpp) runs the steps in this order. `--to-intel` is optional, see [USAGE.md](../user/USAGE.md). -- Each library in [`core/libs/prx`](../../core/libs/prx) builds as a shared library. After the build, `nid_patcher` ([`core/libs/nid`](../../core/libs/nid)) renames every export to its NID, computed from the function name. `APS5_EXPORT("", func)` sets the NID directly when the name is unknown. +- Each library in [`core/libs/prx`](../../core/libs/prx) builds as a shared library. After the build, `nid_patcher` ([`core/libs/nid`](../../core/libs/nid)) renames every export to its NID, computed from the function name. `APS5_EXPORT("", func)` sets the NID directly when the name is unknown. Every other library keeps the names libc exports; it reads them from `libc.exports`, which `nid_patcher --write-export-list` rewrites only when that list changes. On Windows the link writes each import library with `SOURCE_DATE_EPOCH=0` and `ranlib -D` and replaces it only when its bytes change. A source edit that keeps the exports therefore relinks and re-patches only its own library. + +## Main-module entry registers + +The host stubs that transfer control to the guest `e_entry` are not the same on every platform, and the console's register contract at that point is not measured here (#9048). What the stubs establish today: + +| Host | Source | `rdi` | `rsi` | `rdx` | `rbp` | Return from entry | +|---|---|---|---|---|---|---| +| Linux | [`EntryStubBuilder.cpp`](../../core/relinker/elfpatcher/src/general/EntryStubBuilder.cpp) | process stack: qword `argc`, then argv pointers, then a null | 0 | left as the host dynamic linker left it (often `rtld_fini`) | zero frame `{0, 0}` | `ud2` | +| Windows | [`WindowsEntryStubBuilder.cpp`](../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) | `VirtualAlloc` block: qword `argc`, UTF-8 argv pointers, null | address of an exit trampoline that finalizes guest modules and returns | not set by the stub (last host call is `FreeLibrary`) | zero frame in the stub's stack | `ExitProcess` with the entry's return code | +| macOS | [`MacOsImage.cpp`](../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp) | guest entry address into `_Aps5StartGuest_nid_no_patch` | stack block `{argc, argv…, 0, envp…, 0}` | — | — | `Aps5StartGuest` has no implementation on `main`, so the final guest-entry registers are unset | + +On Linux the argument block is the process stack itself (`mov rdi, rsp`). On Windows it is a separate allocation the stub fills with UTF-8 argument strings. Titles that read only `argc`/`argv` through `rdi` see the same layout shape on both; titles that call through `rsi` on Windows get a host exit path that Linux does not provide (`rsi` is zero there). + +## Game patches + +[`GamePatch.cpp`](../../core/relinker/relinker/src/patching/GamePatch.cpp) parses the patch file, a strict subset of TOML. `Conversion::Convert` applies the entries of its own target to the executable bytes right after reading them, and to each bundled module before `GuestModuleBuilder` reads it, so every later stage sees patched bytes. Entry virtual addresses are translated through the `PT_LOAD` segments, and a range may not touch the ELF header or program header table, which the output writers rewrite. The patched file ranges are then checked against every in-place writer: the `--to-intel` reports and trampoline sites, `RelinkPatch` records and syscall trap ranges. The patched virtual address ranges are checked against the 8-byte targets of the `RelaData` and `RelaPltData` relocations, which the loader or the output writers overwrite. `main.cpp` builds the manifest text before it writes any output. `main.cpp` matches targets after the conversions, aggregates the applied records into `.patches.toml`, and runs `--verify-patches` through `GamePatch::VerifyPatches`, which reads only bytes. ## Windows deferred module initialization @@ -53,6 +69,19 @@ While initialization runs, the count is `UINT32_MAX` and the entry word holds th [GuestDeferredModule.cpp](../../core/libs/tests/GuestDeferredModule.cpp) checks repeated loads at the same base, multiple guest references, an external native reference, concurrent initialization and recursive loading. The Windows fixture uses the same table layout as the relinker and reports its entry and constructor calls separately. +## Windows allocator + +On Windows, a relinked game run with `APS5_MIMALLOC` set (to any value) allocates through [mimalloc](../../3rdparty/mimalloc) instead of the C runtime's heap: its libraries and the C and C++ runtime they use. Without the variable the game keeps the C runtime's heap. + +- The build copies `mimalloc.dll` and `mimalloc-redirect.dll` beside the libraries ([`core/libs/CMakeLists.txt`](../../core/libs/CMakeLists.txt)). [`pe_import_first`](../../core/libs/allocator/PeImportFirst.cpp) moves `mimalloc-redirect.dll` to the front of the copied `mimalloc.dll`'s import directory. The redirection module has to load before mimalloc, and MinGW's linker does not keep that order. +- When `APS5_MIMALLOC` is set, the relinker's Windows entry stub ([`WindowsEntryStubBuilder.cpp`](../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) loads `mimalloc.dll` from the run path with `LoadLibraryExW` before the first library. `mimalloc-redirect.dll` then sends the C runtime's allocation functions in `ucrtbase.dll` (`malloc`, `calloc`, `realloc`, `free` and their relatives) to mimalloc for the whole process. The libraries loaded after it, and the MinGW runtime DLLs they use (`libstdc++`, `libgcc_s`, `libwinpthread`), allocate and free through mimalloc. + - Without `mimalloc.dll` the load fails quietly and the process keeps the C runtime's heap. With a relative run path, a module directory too long for the stub's path buffer skips the load the same way. + - A run path whose `mimalloc.dll` path cannot fit that buffer fails the relink, as a library path does. +- Guest code, the executable and its bundled modules, allocates through libc's guest heap ([`GuestHeap.cpp`](../../core/libs/prx/libc/src/GuestHeap.cpp)). The guest heap takes its blocks from the guest arena, or from `malloc` when there is no arena, and returns them the same way, so a block never moves between the guest heap and the host allocator. +- `mimalloc.dll` links the MinGW runtime statically, and three parts of it allocate through the C runtime: GCC's emulated thread-locals (`emutls`, which hold mimalloc's own thread-locals), MinGW's thread-exit destructor list and winpthreads. + - After the redirection these calls would enter mimalloc again, before the calling thread's mimalloc state exists. + - Linker wraps send them to [`RuntimeHeap.c`](../../core/libs/allocator/RuntimeHeap.c), which allocates from the process heap (`HeapAlloc` on `GetProcessHeap()`). The same code frees these blocks through the same wraps. + ## Thread cancellation [`Thread.cpp`](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) records cancellation requests on the target guest thread. New threads start with cancellation enabled and deferred. A disabled request stays pending; enabling deferred cancellation returns before the next cancellation point acts on it. Repeated requests do not add cleanup invocations. @@ -94,30 +123,80 @@ 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. +- `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). +- 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 indirect draw resolved on the CPU (its records read by the worker) drains only when its records or its count overlap a queued write; the records it draws inline drain first, and its last drawn record is queued like a direct draw (`APS5_NO_PIPELINED_INDIRECT=1` drains at every indirect draw). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. +- A dispatch's resource build runs stage A (binding plan, buffers, descriptor set) without the GPU lock and stage B (image lookups) under it. Between them `VulkanDevice::presync` waits, still without the lock, for recorded work that writes a surface stage B would read on the CPU and for recorded work that writes the DCC keys of an image the dispatch binds (`ShaderResources::PresyncSurfaces`), so the locked `WaitForKeyWriters` finds nothing pending to synchronize with. A key range is waited for only when recorded work writes it, the key-store memo does not give its value (`DccKeyWritersPending`, without the mutex) and the pending stores do not give it either (`DccKeyWriterNeedsSync`, under the mutex, the same test `WaitForKeyWriters` makes before it synchronizes). Stage A's own key scan runs before that wait, and stage B compares it with the keys it reads under the lock as before. `APS5_NO_DCC_PRESYNC=1` leaves the key ranges out; `APS5_NO_PRESYNC=1` disables the whole pre-sync. - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. - The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead. +- `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 stage that `sceAgcDriverResolve*Abi` cannot prepare for the state the title creates its pipeline with (a compute shader that calls user-data pointers, a descriptor the resource plan rejects) is left to its first use like a stage deferred at registration: `ResolveShaderAbi` and `PrepareGraphicsStages` catch the failure inside their own preparation transaction, so the `ShaderPreparationScope` the title's call runs in still commits, log it once per shader, mark the shader deferred and record the ABI key, so a repeated call prepares nothing; `ResolvePreparedGraphics` skips a deferred stage's rectangles until a draw has prepared it. A draw or dispatch that cannot prepare the stage either fails as before. Driver failures, `ProcessShutdown` and a failure stored by the shader's registration preparation still propagate from the ABI call. - A compute shader whose `s_swappc_b64` calls a user-data pointer cannot be linked at registration, because the callee's address is only known at dispatch. `InvocationFor` in [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) reads each callee from guest memory, appends one copy per call after the caller (`LinkUserDataCalls`) and prepares that linked code, so a different callee gets its own prepared entry. - A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, a separate alpha blend into an 8_8 target with the alternate component order (both draw twice), rect lists, geometry shaders or a stage without a variant id are created whole, still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines. +- A storage or sampled view of a thin array whose whole surface has more layers than the device's `maxImageArrayLayers` (2048 on NVIDIA; a PS5 array may hold up to 8192) is served from its viewed slices alone: `ArrayLayerWindow` in [`TextureTiling.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) describes them as an array of their own starting at the first viewed slice, which is exact because a non-XOR swizzle does not depend on the slice index. A 3D volume is not an array (its depth is bounded by `maxImageDimension3D`) and a 2D image is one layer whatever its last array field holds; both pass unchanged. A view whose slices alone exceed the limit, a view of an XOR swizzle that mixes in the slice index, and any other image past the limit throw; a sampled lookup (`cachedTexture`) throws before it reads or snapshots the surface. - A draw whose `PA_SC_AA_CONFIG` selects 2, 4 or 8 samples renders into host-only multisampled images. [`MultisampleTarget.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) keeps one image per device, surface address, format, extent and sample count (`MultisampleTargetView`); the pipeline, render pass and library keys carry `State::samples`, and a multisampled depth target is a multisampled depth surface. The guest sample planes and FMASK are never read or written, so the registry also guards the target's guest range: a CPU read (`GuestMemory::SetReadGuard`), a DMA copy, and a sampled or storage image read of it throw. A `CB_COLOR_CONTROL` resolve mode over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image (`ResolveMultisampleTarget`). A zero fill of the target's CMASK (`NoteColorMetadataFill`, from fill kernels and CP DMA fills) clears the image at its next use. - A texture over a depth surface samples the depth image itself when it views one 2D slice of the plane's own format (`DepthSurfaceTexture` in [`DepthSurface.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp)). A view of several slices (a 2D array or a cube) or an integer view of a D16 plane is served by a `DepthPlaneCopy`, an image the slices are copied into at every use: from the depth surface bound at each slice's address, else from guest memory. Such a texture is never kept in the resource templates (`RefreshedPerUse`). - Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). +- A guest protect or unmap that overlaps a plain host-pointer import retires the whole import before changing the guest range (`WaitForLeasesAndImports` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). Later accesses import surviving registered ranges again; imports never keep the pages across a cut pinned in a trimmed state. `GuestAllocations::Range::backingSerial` distinguishes the original pages from replacements: untouched pieces of a split keep their serial, newly mapped or protected pieces get a fresh one, and every span passed to `RequireUnpinned` (including the other mappings of remapped direct memory and the resident runs touched by a 2 MiB unmap or remap) gets a fresh serial when the mutation ends. +- A draw binds its vertex and index ranges in place from a host import, except with NVIDIA's Windows driver, which reads a host import of guest memory far slower than a buffer it allocated (Astro Bot at Crash Site on an RTX 5070 Ti: 164 against 45 ms of draws per frame), so they are copied there (`DrawInputsInPlaceUsable`, `Context::drawInputsInPlace`). `APS5_DRAW_INPUTS_IN_PLACE=1` binds them in place and `APS5_NO_DRAW_INPUTS_IN_PLACE=1` copies them on any device; the second wins. +- 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), unless a live storage image overlaps them. Such an import stays compared: an image that took its generation before the stamp would take the stamp for a CPU store over the results it produces after it, and drop 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. + +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. When the selected columns would take a shader past `ShaderInfo::MaxImages` or `ShaderInfo::MaxSampledPairs`, `PlanTableColumns` plans the columns of one heap unselected again, the heap with the most selected columns first, until both fit. + +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 column's 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. + +`TableSnapshotter` keeps the last 16 column snapshots per thread (at most 4 Mi words in all, least recently used first out; `APS5_NO_TABLE_SNAPSHOT_REUSE=1` turns them off), keyed by heap base and size, record shape and key count: the words read and the column a snapshot holds, its codes, distinct T#s and read ranges. A narrowed, selected or pointer-indirect column, or one with an unmapped record, is not kept. The column is built once and never changed (`SharedValues`): a later capture of the same column shares it with no per-record work and no copy, each snapshot holding a reference, and the result memo hashes its contents once (`SharedValues::Hash`). A column read again after a write is a new one; the snapshots that share the old one keep it. When the runtime vouches that nothing wrote the column's runs since a stamp taken before the column's first read, the column is not read again: its words are handed to the capture as one shared block (`SrtRuntime::supplyWords`, `SrtGuestWords`): `ShaderMemory` keeps a reference to it and reports its runs in the captured regions the draw cache compares, split at page boundaries as a read reports them, without copying them into its pages, and moves them into pages only when a later read in the same `ShaderMemory` touches them. When the draw or dispatch cache sets a fresh capture against a kept one, words both copied from the same place of one such block are equal without a compare (`SameCapturedWords`); only the words around them are compared. A capture holds its blocks weakly for this, so the identity of a released block never matches a new one. `ShaderMemory` vouches through the write tracker: the range was stamped by `GuestMemory::CollectWrites` before any page of it was fetched, no block of it was stamped since (a collected CPU store, the driver's own stores, `MarkWritten`), no recorded GPU work writes it, and no guest mapping changed. The stamp and the check both call `GuestMemory::FlushGpuWrites` over the range first, as a read through `GuestMemory` does, so storage image results kept on the GPU, unit shadows and queued labels over it are in guest memory before the tracker answers. A CPU store between two collects of one collect epoch is seen at the next epoch, as for every other stamp. Otherwise, outside the write watch for example, the runs are read again and the snapshot is reused only when they are equal word for word; when they differ, the column is walked afresh from the words just read. + +`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. A result memo miss takes the resolution of an earlier one by reference when the variant, the reducing state of the direct samplers paired with table images and every column match, a column's codes and distinct T#s by identity (the same shared values, so equal copies resolve again): `ResolveImageTablesShared` keeps at most 4 resolutions per variant and 256 in all within 8 Mi words, counting the columns they keep alive, least recently used first out, and `APS5_NO_TABLE_RESOLUTION_REUSE=1` turns it off. 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 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 A storage image over guest memory that the write watch does not cover (a host-pointer import on a driver that drops its write protection, or an unwatched range) has no block stamps to tell a CPU edit apart, so `StorageTexture::compareUntracked` in [`Texture.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) compares the guest bytes with the image's snapshot. A whole-surface comparison that found no change is remembered under a stamp of: -- the calling queue worker's collect epoch (`GuestMemory::CollectEpoch`), which advances at every ordering point where a CPU store may have landed (submission start, a satisfied `WAIT_REG_MEM`, a drain); a thread that never bumps it gets epoch 0 and is never memoized; +- the calling queue worker's collect epoch (`GuestMemory::CollectEpoch`), which advances at every ordering point where a CPU store may have landed (submission start, a satisfied `WAIT_REG_MEM`, a drain); a thread that never bumps it gets epoch 0 and is never memoized, so the presenter adopts the newest epoch any worker started (`GuestMemory::AdoptLatestCollectEpoch`, never moving a thread back; `APS5_NO_PRESENT_EPOCH=1` disables it); - `GuestMemory::NewestStamp` of the surface: the newest block stamp of its watched part, and the generation of the newest driver store (`MarkWritten`, `StoreOwnBytes`) into its unwatched part; - `GuestMemory::ForgetSerial`, which changes when guest memory is released or remapped; - the recorder's newest pending GPU write note over the surface. The comparison runs again when any of them changes. A CPU store into unwatched memory is therefore seen at the next ordering point, the same bound the collect memo gives watched memory. Driver stores into unwatched memory are recorded per 64 KiB block in a fixed table of 65,536 slots indexed by block number modulo the table size, each slot holding the newest generation stored to any of its blocks. A collision can only raise a surface's stamp, which costs one comparison and never skips one. `agc_driver_dcc_keys_first_write` (`RunUntrackedReuse`) checks that a CPU edit to an unwatched image is uploaded after the next epoch, and `agc_write_tracking` (`CheckNewestStamp`) checks which stores advance `NewestStamp`. +### Resource plans + +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. 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 `GuestContext::floatMode` carries the initial shader mode through `Recompile` and `TranslateOptions` into each block's `TranslationContext`. `s_getreg_b32` reads confined to MODE bits 0–3 return the selected initial rounding bits in the destination's low bits. Bits 0–1 select f32 rounding; bits 2–3 select f16/f64 rounding. Missing mode metadata retains the legacy round-to-nearest-even assumption. Reading other MODE fields or hardware registers throws. @@ -127,3 +206,7 @@ An immediate MODE write confined to the rounding fields is accepted only when it The register layout and instruction contract follow [AMD RDNA2 ISA sections 5.8 and 6.4, Table 24](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna2-shader-instruction-set-architecture.pdf), cross-checked against LLVM 20.1.8's [hardware-register encoding](https://github.com/llvm/llvm-project/blob/87f0227cb60147a26a1eeb4fb06e3b505e9c7261/llvm/lib/Target/AMDGPU/Utils/AMDGPUBaseInfo.h) and [MODE state analysis](https://github.com/llvm/llvm-project/blob/87f0227cb60147a26a1eeb4fb06e3b505e9c7261/llvm/lib/Target/AMDGPU/SIModeRegister.cpp). This contract does not establish arithmetic support for every initial mode or console-specific behavior. `agc_shader_mode_register` decodes instruction words, builds the control-flow graph and checks production translation without a Vulkan device. It covers rounding subfield reads, masked constant writes, branches, unsupported runtime changes and diagnostics, plus mode propagation through full recompilation. The requirements remain active in Release; CTest bounds execution to 20 seconds. + +### Hardware f32 denormal flush + +A device with `shaderDenormFlushToZeroFloat32` and an f32 denormal behaviour independent of the other widths runs a probe when it is created (`ProbeFloat32FlushToZero`): the three programs of `agc_driver_f32_denormal_flush` (one mixed module with every flushing instruction, and the tiny-product programs), with no float mode, with `IEEE_MODE` off and with it on, each compiled twice, with the emulated flush and with `DenormFlushToZero 32` declared. When every result matches, `SpirvTarget::float32FlushToZero` is set: every module declares the mode, and `v_add_f32`, `v_sub_f32`, `v_subrev_f32`, `v_mul_f32`, the fused `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32` and `v_fmaak_f32`, `v_floor_f32` and `v_ceil_f32` drop their software flush in the default rounding mode. VOP3P forms, the other rounding modes and the other instructions keep the emulation. `APS5_NO_HW_F32_FLUSH` skips the probe. diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index ba5c740873..7778788a16 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 @@ -12,9 +12,9 @@ ctest --test-dir build-relinker --output-on-failure Python must be found during CMake configuration for Python tests to be registered. List registered tests with `ctest --test-dir build-relinker -N` and look for `input_magic`, a Python-backed relinker test. If automatic discovery fails, add `-DPython3_EXECUTABLE="C:/path/to/python.exe"` to the configure command, using the path to your Python interpreter, then list the tests again. -This mode builds the conversion tool and its tests on Linux, Windows and macOS, including Apple Silicon. macOS uses AppleClang from the Xcode command-line tools; Windows uses the MinGW-w64 toolchain below. The executable is `build-relinker/core/relinker/relinker` (`relinker.exe` on Windows with Ninja). +This mode builds the conversion tool and its tests on Linux, Windows and macOS, including Apple Silicon. macOS uses AppleClang from the Xcode command-line tools. Windows uses MinGW-w64 GCC with C++20 support and accepts distributions with static or shared runtimes; the full build also requires the exception support described 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. @@ -30,9 +30,18 @@ git submodule update --init --recursive - Linux display refresh queries require SDL's XRandR support (`libxrandr-dev` on Debian/Ubuntu; `libXrandr-devel` on Fedora). Bundled SDL configuration fails without it. Keep `SDL_X11_XRANDR` enabled. - Linux: GCC, G++, binutils. SDL's X11 backend requires X11 and Xext development headers (`libx11-dev` and `libxext-dev` on Debian/Ubuntu). SDL's Wayland backend requires the Wayland, xkbcommon and EGL development files (`libwayland-dev`, `libxkbcommon-dev` and `libegl-dev` on Debian/Ubuntu; `wayland-devel`, `libxkbcommon-devel` and `mesa-libEGL-devel` on Fedora), and `libdecor-0-dev` (`libdecor-devel`) for window decorations on GNOME. SDL loads both backends at run time and prefers Wayland when `WAYLAND_DISPLAY` is set; `SDL_VIDEODRIVER=x11` selects X11 (XWayland). Titles that bundle their own SDL read `SDL_VIDEODRIVER` too, and some of them fail to start with `x11`. Without the Wayland files configuration still succeeds, and the build uses X11 only. Audio output needs SDL's PipeWire, PulseAudio or ALSA backend, built from their development files (`libpipewire-0.3-dev`, `libpulse-dev` and `libasound2-dev` on Debian/Ubuntu; `pipewire-devel`, `pulseaudio-libs-devel` and `alsa-lib-devel` on Fedora). SDL loads them at run time. Without any of them SDL keeps only its silent dummy and disk drivers, so configuration fails. - Fedora: if CMake reports `Could NOT find Threads`, install `glibc-static` and `libstdc++-static` with `sudo dnf install glibc-static libstdc++-static`, then rerun configuration. The project enables static executable linking, and FFmpeg's thread check needs these libraries. -- Windows: only MinGW-w64 GCC 15.2.0 (WinLibs `x86_64-ucrt-posix-seh`, release `15.2.0posix-14.0.0-ucrt-r7`) is currently supported. Add its `mingw64/bin` directory to `PATH` before configuring. +- Windows: x86-64 MinGW-w64 GCC with POSIX threads. The compiler must provide DWARF exceptions with `-fno-asynchronous-unwind-tables`, or SEH exceptions for System V ABI functions; CMake checks these capabilities. WinLibs GCC 15.2.0 (`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)) uses DWARF. [MSYS2 UCRT64 GCC](https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-gcc) 16.2.0-6 uses SEH. Add the selected toolchain's `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`. -- 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\mingw64\etc\ssl\certs\ca-bundle.crt` from Git for Windows, as in CI. +- Bitdefender Advanced Threat Defense hooks Windows calls that `sceKernelRaiseException` makes while its target thread is suspended. When the target is suspended inside ntdll's exception dispatch while it holds an ntdll lock, the hook waits for that lock and `guest_raise_exception` hangs until its timeout. A scanning exception for the build directory does not prevent it; add `guest_raise_exception_tests.exe` to the Advanced Threat Defense exceptions. +- FFmpeg binaries are downloaded during configuration unless `FFMPEG_PREBUILT_DIR` is set. With the WinLibs CMake, the download fails with status 60 (`SSL peer certificate or SSH remote key was not OK`) unless `SSL_CERT_FILE` names a CA bundle, for example `C:\Program Files\Git\ucrt64\etc\ssl\certs\ca-bundle.crt` from Git for Windows 2.56 and later (`mingw64` instead of `ucrt64` before 2.56), as in CI. + +## Web converter releases + +The Release workflow builds the web converter and the Linux and Windows libraries from the same commit. It tests the converter and publishes its frontend and WASM files as `web-.zip`, with the tag and commit recorded in `web-release.json`. + +Pages downloads the converter and both library archives from one release selected through GitHub's latest-release API. `release.json` records the same tag and commit. Missing, corrupt or mismatched assets fail the Pages job before deployment. Releases made before the web asset was added need a new release; Pages does not combine them with a converter built from `main`. + +Changes to the converter or its frontend reach Pages through a release. Progress reports still come from `main`. For local development, build `relinker_web` with Emscripten and run `python3 tools/test_web.py --build build-wasm`. To stage a complete published release without compiling, run `python3 tools/build_web.py --build build-wasm --release`; `--libraries` is an alias for this mode. ## Commands @@ -51,6 +60,41 @@ therefore tests the previous binaries and can show no effect at all. [Relinker usage and runtime layout](../user/USAGE.md). +## Windows setup and troubleshooting + +The full build also requires Python 3 for glslang and Git for Windows with Bash for libusb's version-header generation. A minimal Git installation without Bash is insufficient for the full build. + +For MSYS2, install `mingw-w64-ucrt-x86_64-gcc` with `pacman -S mingw-w64-ucrt-x86_64-gcc` and use `C:\msys64\ucrt64\bin` in PowerShell. Adjust the prefix for a Scoop installation of MSYS2. Use a separate build directory for each toolchain, and select both `gcc` and `g++` from the same installation. + +The [Windows MSYS2 workflow](../../.github/workflows/windows-msys2.yml) builds and tests the full runtime nightly at 08:23 UTC and can be started manually with `workflow_dispatch`. The regular Windows build uses WinLibs. + +Scoop's standalone GCC 15.2.0 package cannot build the runtime: disabling SEH selects SjLj exceptions, and its SEH backend cannot compile System V ABI catch handlers. Use MSYS2 UCRT64 or a DWARF-capable WinLibs distribution. This restriction does not apply to relinker-only builds. + +In PowerShell, prepend the supported WinLibs toolchain and Git for Windows to the current session's `PATH`. Adjust the paths for the installed locations; a Portable Git installation can be used in place of `C:\Program Files\Git`. + +```powershell +$env:PATH = "C:\tools\winlibs\mingw64\bin;C:\Program Files\Git\cmd;C:\Program Files\Git\usr\bin;$env:PATH" +``` + +If configuration reports `Could NOT find Python3` or selects an unusable interpreter, pass the absolute path to a working Python 3 executable: + +```powershell +cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DPython3_EXECUTABLE="C:/path/to/python.exe" +``` + +If `gen-describe.ps1` fails while generating libusb's `version_describe.h`, check the Bash path printed in the build output. A WindowsApps or System32 `bash.exe` can launch WSL and fail with `WSL_E_DEFAULT_DISTRO_NOT_FOUND` when no distribution is installed. Install Git for Windows with Bash, or use Portable Git, and put its `cmd` and `usr\bin` directories on `PATH` before retrying the build. Use Bash and `cygpath.exe` from the same Git installation. + +After `cmake --build build --target libs --parallel` succeeds, copy the toolchain's runtime DLLs beside the patched libraries: + +```powershell +$toolchainBin = "C:\tools\winlibs\mingw64\bin" +foreach ($runtimeName in @("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll")) { + Copy-Item -LiteralPath (Join-Path $toolchainBin $runtimeName) -Destination "build/core/libs/libs" +} +``` + +When preparing a title's `libs/` directory, copy these DLLs with the patched `.prx` files. The Windows runtime loader does not search `PATH` for these dependencies; missing DLLs can cause error 126 even when the `.prx` is present. See [runtime layout](../user/USAGE.md#runtime-layout). + ## CMake flags Project switches accept `ON` or `OFF`: @@ -59,6 +103,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. | @@ -80,7 +125,17 @@ Build configuration parameters: SDL and FreeType settings forced by the root `CMakeLists.txt` cannot be overridden with `-D`. -With `ANYPS5_USE_SYSTEM_DEPS=ON` those libraries come from the system (on Debian and Ubuntu: `libsdl2-dev`, `libfreetype-dev`, `libvulkan-dev`, `spirv-headers`, `glslang-dev`, `glslang-tools` and `spirv-tools`). A missing package, Vulkan-Headers older than 1.4.310 (the agc driver uses `VK_KHR_maintenance8`), or an SDL2 without the HIDAPI joystick driver (sdl2-compat over SDL3, or a build with HIDAPI disabled, which breaks the pad and disk-audio tests), fails the configure with an error and never falls back to the submodule. The forced SDL and FreeType settings from the root `CMakeLists.txt` do not apply with the flag ON: the system SDL2 comes with whatever its distribution enabled. The prx libraries are shared objects, so SDL2 is linked from the system as a shared library; the default build keeps its own static SDL2. `ffmpeg-core`, `libjpeg-turbo`, `stb`, `VulkanMemoryAllocator` and `LibAtrac9` always stay bundled and still need `git submodule update --init`. +With `ANYPS5_USE_SYSTEM_DEPS=ON` those libraries come from the system (on Debian and Ubuntu: `libsdl2-dev`, `libfreetype-dev`, `libvulkan-dev`, `spirv-headers`, `glslang-dev`, `glslang-tools` and `spirv-tools`). A missing package, Vulkan-Headers older than 1.4.310 (the agc driver uses `VK_KHR_maintenance8`), or an SDL2 without the HIDAPI joystick driver (sdl2-compat over SDL3, or a build with HIDAPI disabled, which breaks the pad and disk-audio tests), fails the configure with an error and never falls back to the submodule. The forced SDL and FreeType settings from the root `CMakeLists.txt` do not apply with the flag ON: the system SDL2 comes with whatever its distribution enabled. The prx libraries are shared objects, so SDL2 is linked from the system as a shared library; the default build keeps its own static SDL2. `ffmpeg-core`, `libjpeg-turbo`, `stb`, `VulkanMemoryAllocator`, `LibAtrac9`, `ogg` and `vorbis` always stay bundled and still need `git submodule update --init`. + +## Quick syntax checks + +After configuring a build directory, use `tools/check_compile.py` to check the given source files. The tool reads their compile commands from `compile_commands.json` or Ninja, preserving compilation options while dropping object and dependency output options. It handles `-MF`, `-MT`, `-MQ` and `-MJ` with separate or attached values so that unused output options do not cause false failures under `-Werror`. + +```sh +python3 tools/check_compile.py --build build-relinker core/relinker/main.cpp +``` + +Real compile errors still fail. Syntax checks do not replace a full build and test run. ## Write watch tests @@ -118,23 +173,44 @@ Each invocation creates one pipeline. With no `--cache-in`, its application pipe `--fixture` compiles a tiny synthetic compute shader with specialization, sparse descriptor binding and push constant metadata; `--write-fixture DIRECTORY` saves it without opening Vulkan. It performs no GPU work and does not reproduce a title's compile workload. With `BUILD_TESTING=ON`, `agc_compute_pipeline_capture` checks serialization and rejected inputs. With SPIRV-Tools also enabled, `agc_compute_pipeline_replay` compiles the fixture with fresh and imported application caches, verifies cache rejection, and checks required subgroup support. `agc_compute_pipeline_capture_run` verifies the existing branch-past-endpgm compute fixture's output with capture disabled and enabled, then replays the captured pipelines and checks their identities and metadata. Each test has a 30-second timeout. Runtime tests skip when Vulkan is absent; set `ANYPS5_REQUIRE_VULKAN=1` to require execution. There is no performance pass threshold. +## PM4 submission replay + +Set `APS5_DUMP_SUBMITS` to a directory before starting a title to save every submission the AGC driver accepts, as it enters the driver. `index.txt` gets one line per submission: ordinal, queue, `graphics` or `compute`, serial, microseconds on the clock of the `APS5_TRACE_GPU` lines, dword count, kind, file name, the dword offsets of the waits whose value passed their compare when the submission entered the driver (`held`, which the driver then does not wait for) and the COND_EXEC and predicated indirect buffer ranges as `start:end` dword offsets (`-` for none). `submission_.bin` holds the dwords the driver copied, with indirect buffers inlined. A REWIND tail is listed as `rewind` with the serial of its submission, a suspend point as `suspend` without a file. The index is truncated on the first submission. A failed write puts the driver in its failed state: the call that made the submission throws after the submission was queued, and every later driver call throws the same error. Unset, the switch costs one test per submission; set, one copy of the commands per submission before the driver lock, the ordinal, time and held waits under it without allocating, and the files written after it. A capture holds command buffers only, no guest memory. An index written before the held and conditional columns (column line `ordinal queue type serial time_us dwords kind file`) is still read, with both unknown. + +`agc_pm4_replay [--full]` decodes the capture offline with the driver's packet decoder, validator and store decode (`Pm4::DecodeStores`) and reports each WAIT_REG_MEM, WAIT_REG_MEM_64 and REWIND as released, unreleased or unknown. It does not model cross-queue timing: a wait is released only when the capture alone proves that the driver releases it, and unknown, with the reason, whenever its outcome depends on when the stores of another queue run, since a store of another queue can run before, between or after the stores of the waiting queue whatever order the submissions entered the driver in. A memory wait is released when its compare always passes; when it was held at submit; when the last store to the awaited bytes before it on its queue (in that submission or an earlier one) covers them with a value the compare accepts, is not conditional, and no other queue stores to any of those bytes anywhere in the capture (current value); or when a RELEASE_MEM or WRITE_DATA label earlier in the waiting submission, one the driver keeps in its label history (4-byte aligned, at most 64 bytes, not conditional), stores a value the compare accepts and fewer than 4 other stores to the awaited bytes can run between it and the wait (label history). The driver keeps the last 4 label stores per dword over all queues (`LabelHistory.cpp`) and finds one there only if it ran after the waiting submission entered the driver, which the capture proves only for a store earlier in that submission. The stores counted against the history are those between the label and the wait on the waiting queue and every store of another queue to the awaited bytes, whatever its submit order; stores later on the waiting queue run after the wait. A REWIND is released when its tail is in the capture. One that was valid at submit without a tail in the capture is unknown, because the driver reads the valid bit again when the queue reaches the REWIND. A memory wait is unreleased when it was not held at submit and no store in the capture that can run before it may write a value the compare accepts (a store of part of the awaited bytes or of a value known only at run time may): the GPU does not release it, but a CPU store could, and so could a shader store, since the capture holds neither. A REWIND without a tail that was not valid at submit is unreleased. Every other wait is unknown: `unknown: depends on cross-queue timing (stores on queue X at #...)` when another queue stores to the awaited bytes; otherwise when the last store before it on its queue is conditional (COND_WRITE, inside a COND_EXEC range or a predicated packet), stores a value known only at run time or only part of the awaited bytes, or overwrites an accepted store that the capture does not prove is still in the label history (one in an earlier submission, one the history does not keep, or one with 4 or more stores after it). When the index has no held column, a wait that would be unreleased is unknown, since its value may have passed the compare at submit; without the conditional column every packet after a COND_EXEC or predicated INDIRECT_BUFFER is conditional to the end of its submission. The report counts the waits per outcome and route and the unknown waits that depend on cross-queue timing, says when the index lacks the columns, lists up to 32 unreleased and then unknown waits, newest first, each with the packets around it and the stores to the awaited bytes that decide it (for another queue, the 4 nearest the waiting submission in submit order), then the last submissions of each queue. `--full` lists every wait and prints every packet. A packet the driver rejects means the capture is corrupt and fails the analysis. The exit code is 1 when a wait is unreleased and 2 on errors. + +```sh +cmake --build build --target agc_pm4_replay +build/core/libs/prx/libSceAgcDriver/agc_pm4_replay submits +``` + +On Windows use `agc_pm4_replay.exe` and prepend `build/core/libs/libs/unpatched` to the process `PATH`. With `BUILD_TESTING=ON`, `agc_driver_pm4_replay` captures submissions made with the libSceAgc command builders through the driver and checks the index, the held waits, the conditional ranges, the dwords, the decode and the outcome of each wait, then checks hand-made captures: each release route; a store of another queue to the awaited bytes that entered before or after the store of the waiting queue (unknown); the label history limit, with the stores after the label on the waiting queue or on another queue; a fill the history does not keep; a store in an earlier submission overwritten later; later stores, rejected values and a missing rewind tail (unreleased); the six captures of the reviews (a two-queue deadlock, a store of 0 behind a released wait on another queue, with and without that wait held at submit, and labels pushed out of the history by stores of another queue), none of whose waits may be released except the one held at submit; five groups of waits in eight submission orders with identical outcomes; an index without the held and conditional columns; corrupt captures; then a failed write. `agc_driver_pm4_replay_tool` runs the tool on the driver capture and checks its exit codes and report. + ## Pipeline statistics Set `APS5_PIPELINE_STATS=1` to capture and print driver statistics for each newly created graphics or compute pipeline. This requires `VK_KHR_pipeline_executable_properties` and `pipelineExecutableInfo`; an unsupported device fails with an error. Statistic names and units are driver-specific. Capturing statistics can increase pipeline compilation cost. The setting is disabled by default. +## Device loss reports + +On a device that supports `VK_EXT_device_fault`, the first `VK_ERROR_DEVICE_LOST` prints the driver's fault description, the faulting addresses and the vendor fault codes as `[device-fault]` lines. Set `APS5_GPU_CHECKPOINTS=1` to also record a `VK_NV_device_diagnostic_checkpoints` marker with the program address before each compute dispatch; the report then lists the last markers the GPU reached. Checkpoints are disabled by default and ignored on devices without the extension. + ## Worker sampler Set `APS5_SAMPLE_WORKER=` to sample queue worker 0 every millisecond (in practice every timer tick). Every 20 s and at exit it writes ``, one `module offset self inclusive` line per sampled address, and `.collapsed.txt`, the worker's whole stacks in the collapsed format that speedscope.app and flamegraph.pl read: one `frame;...;frame count` line per distinct stack, root first, each frame as `module+0xRVA`. A walk that ends before the thread's root starts with `[truncated]`. The `#` lines give the samples, the walk depth (64 frames), the truncated walks, the mean and longest time the worker was suspended, and the size and PE timestamp of each module, to check the binary a file is symbolized against. A frame with `.pdata` is stepped by `RtlVirtualUnwind`; any other frame by its DWARF record in `.ehfram`, the unwind data the libraries are built with. Those steps read only inside the worker's stack, and a module is indexed after the worker resumes, the first time a walk meets it. -`APS5_SAMPLE_THREADS=` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to ``, 12 frames deep and through `.pdata` only. Both samplers exist only on Windows; the addresses are symbolized offline, for example with `nm` on the `.prx`. +`APS5_SAMPLE_COMMITTER=` samples queue 0's draw committer the same way and writes the same two files. The committer records the draws the worker hands over (`APS5_PIPELINED_DRAWS`), so when the worker's stacks wait in `DrawPipeline::Enqueue`, its stacks show what bounds the frame. It starts with the first pipelined draw; with `APS5_PIPELINED_DRAWS=0` nothing is sampled. + +`APS5_SAMPLE_THREADS=` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to ``, 12 frames deep and through `.pdata` only. The samplers exist only on Windows; the addresses are symbolized offline, for example with `nm` on the `.prx`. ## Performance trace Set `APS5_TRACE_FRAMES=:` to record `` presents from present `` (counted from 0), or `APS5_TRACE_SECONDS=:` to record from the first present at least `` seconds after the first present until the first present `` seconds later. Both values are whole numbers and the length is above 0; a malformed value, or both switches together, fails the first present. The window opens and closes at presents. -Inside the window the queue workers and the presenter record every `PerformanceTimer` scope and stage as a complete event named after its frame timing key: `Driver.WorkerWait`, `Driver.Packet`, `Driver.Packet.draw`, `Graphics.Draw`, `Driver.Present`, `Driver.Present.inflight_wait`, `Vulkan.Retire` and so on. A slice shorter than 20 us is only counted: its time is in the thread's counter ` folded` (milliseconds per present) and in the summary. A thread writes a present's counter sample at its first slice after the next present, so the last present of the window has no sample; its time is still in the summary. Each thread allocates a buffer for 262,144 events when it starts; a full buffer stops that thread's recording and sets `trace_dropped`. When the window closes, the presenter formats the trace once and a `ProfileOutput` writer thread writes `aps5-trace--.json.gz` (Chrome trace-event JSON, opened by ui.perfetto.dev or chrome://tracing) and appends one line to `aps5-summary.jsonl`, both in `APS5_TRACE_DIR` or the working directory. A window that is still open when the title exits writes nothing. +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. -The summary line has `commit` and `dirty` of the build, `env` (every `APS5_*` variable; a value containing `/` or `\` is written as ``), `gpu`, `driver`, `os`, `resolution` (the video output's), `window`, `presents`, `frame_ms` (p50, p90, p99 and max of the intervals between presents), `gpu_busy_ms_per_present` (the batch timestamps behind `[present]`), `worker_busy_pct` (share of the window each queue worker spends in `Driver.Packet`, `Driver.Suspend`, `Driver.EndSubmission`, `Driver.Rewind` and `Driver.Completion`), `top_phases_ms_per_frame` (the eight largest scopes and stages, without `Driver.WorkerWait`), `trace_dropped` and `trace`. A trace holds timings, scope names, counts and thread names; no guest memory, paths or strings of the title. +Inside the window the queue workers and the presenter record every `PerformanceTimer` scope and stage as a complete event named after its frame timing key: `Driver.WorkerWait`, `Driver.Packet`, `Driver.Packet.draw`, `Graphics.Draw`, `Driver.Present`, `Driver.Present.inflight_wait`, `Vulkan.Retire` and so on. A slice shorter than 20 us is only counted: its time is in the thread's counter ` folded` (milliseconds per present) and in the summary. A thread writes a present's counter sample at its first slice after the next present, so the last present of the window has no sample; its time is still in the summary. By default, each queue worker and presenter allocates a buffer for 262,144 events when it starts; a full buffer stops that thread's recording, sets `trace_dropped` and puts an instant event `trace buffer full` on the thread's track at the start of the first lost slice (slices are appended when their outer scope ends, so that can precede the last recorded slice by up to one outer-scope length), with the number of lost slices in `args.events_lost`. When the window closes, the presenter formats the trace once and a `ProfileOutput` writer thread writes `aps5-trace--.json.gz` (Chrome trace-event JSON, opened by ui.perfetto.dev or chrome://tracing) and appends one line to `aps5-summary.jsonl`, both in `APS5_TRACE_DIR` or the working directory. A window that is still open when the title exits writes nothing. + +The summary line has `commit` and `dirty` of the build, `env` (every `APS5_*` variable; a value containing `/` or `\` is written as ``), `gpu`, `driver`, `os`, `cpu` (the CPU brand string), `cpu_threads` and `memory_mib` (physical memory) of the host (all three absent on a host that is not x86 or reports no brand string), `title_id`, `title` and `content_version` from `app0/sce_sys/param.json` (only when the title has one; `content_version` is empty when the file has no `contentVersion`, or one that is not a string of at most 15 characters), `resolution` (the video output's), `window`, `presents`, `frame_ms` (p50, p90, p99 and max of the intervals between presents), `gpu_busy_ms_per_present` (the batch timestamps behind `[present]`), `worker_busy_pct` (share of the window each queue worker spends in `Driver.Packet`, `Driver.Suspend`, `Driver.EndSubmission`, `Driver.Rewind` and `Driver.Completion`), `top_phases_ms_per_frame` (the eight largest scopes and stages, without `Driver.WorkerWait`), `trace_dropped`, `trace_dropped_threads` and `trace`. `trace_dropped` is `true` when any thread lost a slice, any thread's totals table (512 scope and stage entries) was full, or the `gpu` track was full. `trace_dropped_threads` is present only then and has one entry per such thread name (threads that share a name, such as the shader prepare threads, share one entry: `events_lost` is summed, `events_full_after_s` is the earliest, `totals_full` is true if any is): `events_full_after_s` (seconds from the first present of the window to the start of the first lost slice; absent for `gpu` and, in every summary line (windowed `APS5_TRACE_FRAMES`/`APS5_TRACE_SECONDS` and `APS5_TRACE_SUMMARY_EVERY`), whenever the first lost slice starts outside the line's span of presents, including a slice whose scope began before the window opened or one lost after the window's last present), `events_lost` (slices not recorded; absent when 0; in `APS5_TRACE_SUMMARY_EVERY` lines it counts the thread buffer's losses since it started, not only those of the line's span) and `totals_full` (`true` when the thread's totals table overflowed, which makes the summary numbers incomplete; a thread with `events_lost` only has complete totals). A trace holds timings, scope names, counts and thread names; no guest memory, paths or strings of the title. The shader preparation threads are traced as well, `shader prepare`, with a buffer for 32,768 events each. A wait for the GPU mutex on a traced thread is a `GpuMutex.wait.` slice (`dispatch`, `draw`, `present`, `flush` and so on, as on the `[lock]` line). @@ -142,16 +218,72 @@ The `gpu` track holds one `batch` slice per recorder batch, from the batch's tim With any of the three switches set, the recorder writes a timestamp pair per batch for the whole run, as with `APS5_PROFILE_DRAW`. Without them, a `PerformanceTimer`, a GPU mutex acquisition and a read of batch timestamps each cost one more predictable branch. +Set `APS5_TRACE_DRAW_PROGRAMS=1` (any value) to print one `[draw-programs]` line on stderr per draw, with the submission serial, the queue, and for each selected program its stage and role, the shader's code address and its entry address (`vertex/main=0x100000(entry=0x100040)`). It is for comparing the shaders a draw selects with shader-preparation failure logs; it reads no GPU state. + ### Summaries for a long session Set `APS5_TRACE_SUMMARY_EVERY=` (a whole number above 0; a malformed value fails the first present) to profile a long session in the background without a trace. The first present opens the first interval. Every `` seconds, at the first present at least that long after the interval opened, the presenter appends one line to `aps5-summary.jsonl` in `APS5_TRACE_DIR` or the working directory and opens the next interval at that same present, so the intervals follow each other without a gap and their `frame_ms` cover every interval between presents. The line has the fields of the window summary line for that interval only, plus `interval_start_s` and `interval_end_s` (seconds since the first present). Its `window` is `{"spec":"summary_every ","first_present":,"ms":}` and its `trace` is the empty string. No event buffer is allocated: each thread keeps only the per-scope totals it already folds (a few kilobytes), and the presenter subtracts the totals of the previous interval. An interval that is still open when the title exits writes nothing. The switch combines with `APS5_TRACE_FRAMES` or `APS5_TRACE_SECONDS`. The intervals run for the whole session and the window records its full trace on top: the event buffers are allocated and filled only while the window is open, the window's own summary line (with `trace` set and no `interval_*` fields) holds only the time inside the window, and the interval lines continue unchanged around it. Without a window switch no buffer is allocated at all. The two switches do not share a clock origin: `interval_*` is relative to the first present, as the window's `APS5_TRACE_SECONDS` start is. +Set `APS5_TRACE_BENCH=1` together with `APS5_TRACE_SUMMARY_EVERY` to also keep every interval's frame times for a benchmark report. At each interval the presenter hands the kept intervals to a writer thread, which formats and compresses the file and rewrites `aps5-bench--.json.gz` in `APS5_TRACE_DIR` or the working directory: one JSON object with `format` (`aps5-bench/1`), the identity fields of the summary line (`commit`, `dirty`, `env`, `gpu`, `driver`, `os`, `cpu`, `cpu_threads`, `memory_mib` and, with `param.json`, `title_id`, `title` and `content_version`), `intervals_dropped` and `intervals`. Each interval has `start_s`, `end_s`, `first_present`, `resolution`, `gpu_busy_ms` (the interval's total) and `frame_us`, the whole microseconds between consecutive presents. The file keeps the last 240 intervals; `intervals_dropped` counts older ones it no longer holds. Any other value, or the switch without `APS5_TRACE_SUMMARY_EVERY`, fails the first present. The file is written to `.tmp` and then moved over the previous one, so a crash or kill during a rewrite leaves the last complete report. Only the newest report waits for the writer: one handed over while an older one is still pending replaces it. The frame times cost 4 bytes per present in memory. + `python tools/trace_digest.py aps5-summary.jsonl` prints the interval lines of such a file as one row per interval (frame p50, p90, p99, max, GPU busy per present, the busiest queue worker and the largest phase) and a text sparkline of the frame p99 and of the GPU busy time; lines of window captures in the file are skipped, and a change of commit, environment, GPU, driver or OS between intervals is reported. `--json` prints the rows as JSON. With `APS5_TRACE_SUMMARY_EVERY` set, the recorder writes a timestamp pair per batch for the whole run and every `PerformanceTimer` scope adds its time to the thread's totals, two clock reads per scope. A thread's totals are read while it runs, so a scope that straddles an interval boundary is counted in one interval or the other. +## 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. + +## Guest clock scale + +Set `APS5_TIME_SCALE=` to scale `sceKernelGetProcessTime`, `sceKernelGetProcessTimeCounter` and `sceKernelReadTsc` from their first observed value. A positive factor below 1 slows guest time; a factor above 1 speeds it up. An unset, zero, negative or non-numeric value uses `1.0` (normal speed). On Windows, `sceKernelClockGettime` monotonic clock ids are scaled too; on Linux they return the native host monotonic clock unchanged. A title's own `rdtsc` instruction and `sceKernelGetTscFrequency` are unscaled, so dividing scaled TSC deltas by the reported real frequency gives guest-time intervals. Host sleeps and waits still take real time, so this is a diagnostic aid for titles whose frame or timeout logic depends on guest time; it does not speed up the emulator or GPU work. The setting is read once when the process starts. + +`APS5_PAD_INPUT_FILE` command durations and reload polling use `sceKernelGetProcessTime`, so they follow the scale. At `0.02`, a 1-second injected press lasts about 50 seconds of real time, and a new command can take up to 5 seconds to be noticed. + +## Driver profile lines + +With `APS5_PROFILE_DRAW` set (any value), the driver prints a profile line on stderr at most every 10 s. A line either counts the last 10 s (its counters are read and cleared when it is printed; the text says `(10 s)` or `over 10 s`) or counts from the start of the process (the text says `(cumulative)`). A rate is the difference of two cumulative lines divided by the time between them. + +| Line | Counts | +|---|---| +| `[rescache] revalidate` | last 10 s | +| `[texture-cache]` | last 10 s | +| `[pipecache]` | last 10 s | +| `[rescache]` (hits, misses, entries) | from the start | +| `[vk]` | from the start | +| `[indirect]` (gpu-side, cpu-side) | from the start | +| `[flip]` | from the start | +| `[fill-sync]` | from the start | +| `[buffers] descriptor elements bound` | from the start | + +`[gputime]` is described above and needs `APS5_PROFILE_GPU`. Other profile lines state their window in their text or are not listed here because their counters were not checked. + ## 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/CONVENTIONS.md b/docs/dev/CONVENTIONS.md index deb2f6a380..87c674dac4 100644 --- a/docs/dev/CONVENTIONS.md +++ b/docs/dev/CONVENTIONS.md @@ -14,4 +14,15 @@ Comments in code can only be added to indicate areas of [technical debt](Technic [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) -There's no need to add image files to the repository. You can add images to the [gist](https://gist.github.com/boykopovar/0e53f2e1426f29ecd41e3b51540b8a90) comments and paste a link into the md file. \ No newline at end of file +There's no need to add image files to the repository. You can add images to the [gist](https://gist.github.com/boykopovar/0e53f2e1426f29ecd41e3b51540b8a90) comments and paste a link into the md file. + +### Host writes into guest memory + +Host code that writes into guest memory through the kernel or a native library must hold a `GuestArena::HostWrite` ([GuestArena.hpp](../../core/libs/prx/libc/include/GuestArena.hpp)) over the destination range for the whole write. This covers file reads (`ReadFile`, `read`, `pread`), socket receives, `DeviceIoControl` and other calls that fill a caller's buffer. + +The write watch can leave guest pages read-only between collections: every watched page on macOS, and stable resident runs of at least 256 KiB on Windows (see the write-watch entries in [debt/libc.md](debt/libc.md)). A kernel-mode write into such a page doesn't fault into the watch. It fails instead, with `EFAULT` on macOS and `ERROR_NOACCESS` on Windows. `HostWrite` opens the pages and makes sure the write is recorded. + +- A write that completes later on another thread (overlapped I/O, completion ports, asynchronous reads) is bracketed where the bytes land, not where the request was issued. +- Ordinary CPU stores from host C++ code need no bracket: the write watch records them (through a fault where the page is protected). +- A GPU import of guest memory uses `HostImportAccess`, not `HostWrite`. +- Third-party code that writes guest memory on its own (driver threads, library worker threads) can't be bracketed. List it in that library's debt file. diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md new file mode 100644 index 0000000000..dd75c7f25f --- /dev/null +++ b/docs/dev/ENVIRONMENT.md @@ -0,0 +1,570 @@ +# Environment variables + +The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetting`, `MaskFromEnvironment`, `EnvInt`, `GetEnvironmentVariableA`, `GetEnvironmentVariableW` with a wide literal, `configured`, `PipelineSwitch`, `environmentCount` and `addWideString` (the wide names the Windows entry stub reads). This table lists every `APS5_*` and `ANYPS5_*` name it reads, its kind, the value used when it is unset and what it does, one table per library. Regenerate the names with `python3 tools/env_reference.py --table`; the Conventions check fails when a name the code reads has no row here, or a row's name is no longer read. + +### core/libs + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `ANYPS5_LOADEXEC_PARENT` | string | `core/libs/tests/GuestLoadExecHost.cpp` | | variable the Windows entry stub reads; its wide name is built in `core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp` | +| `ANYPS5_LOADEXEC_TEST_ROOT` | string | `core/libs/tests/GuestLoadExecHost.cpp` | | directory the load-exec host test reads its guest executable from | +| `ANYPS5_TEST_QUOTED_ARGV0` | presence | `core/libs/tests/GuestLoadExecHost.cpp` | | makes the load-exec test's first argument one with quotes, a backslash and non-ASCII text | +| `APS5_EBOOT` | string | `core/libs/tests/GuestSystemExit.cpp` | | host path of the executable launched for /app0/eboot.bin, defaulting to the running executable | +| `APS5_SKIP_TIMING_TESTS` | presence | `core/libs/tests/PreciseWait.cpp` | | treats the timing tests' runner as preempted, so they skip without probing the clock | +| `APS5_USBD_DEVICE` | string | `core/libs/tests/GuestUsbd.cpp` | | selects the USB device a test drives, as vendor:product in hex | + +### core/relinker + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `ANYPS5_GUEST_MODULE_ARGS` | string | `core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp` | | path of the file the module-argument fixture appends its events to | + +### core/shader + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `ANYPS5_NO_SHADER_CACHE` | presence | `core/shader/recompiler/ShaderDiskCache.cpp` | | disables the on-disk shader cache when set to a non-empty value other than 0 | +| `ANYPS5_SHADER_CACHE_DIR` | string | `core/shader/recompiler/ShaderDiskCache.cpp` | | directory for the on-disk shader cache, overriding the one beside the executable | +| `APS5_BDA_BYTE_READS` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp` | | forces one address lookup and byte load per byte for BDA reads instead of a wide dword load | +| `APS5_BDA_DWORD_STORES` | presence | `core/shader/recompiler/Translation/src/MemoryInstructions/FlatInstructions.cpp` | | reverts BDA writes to one address store per dword instead of a wide multi-dword store | +| `APS5_DUMP_IR` | string | `core/shader/recompiler/Recompiler.cpp` | | hex code address (or all) of the programs whose IR is printed after each front-end pass | +| `APS5_GATHER_CONST` | integer | `core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp` | | integer every non-depth gather returns in all four texels instead of the sampled value | +| `APS5_IMAGE_TABLE_KEY_SCAN` | integer | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | 256 | number of image-table entries the key scan examines | +| `APS5_IMAGE_TABLE_STRICT` | presence | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | | sets image-table materialization to its strict form; 0 leaves it off | +| `APS5_NO_ATOMIC_BARRIER` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvFormatConvert.cpp` | | drops the device-scope acquire/release memory barrier emitted after atomics | +| `APS5_NO_ATOMIC_ZERO_SKIP` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp` | | restores plain buffer atomics instead of skipping identity-operand atomics | +| `APS5_NO_CODE_HASH_KEY` | presence | `core/shader/recompiler/CacheKey.hpp` | | appends every code word to the cache key instead of hashing the code | +| `APS5_NO_FAILURE_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | rebuilds a program whose compilation failed instead of reusing the memoized failure | +| `APS5_NO_IMAGE_TABLES` | presence | `core/shader/recompiler/Optimization/src/ResourceTracker.cpp` | | disables image tables, leaving table-loaded descriptors unplanned | +| `APS5_NO_PERF_DISK_PLANS` | presence | `core/shader/recompiler/Recompiler.cpp` | | disables the disk cache of planning results | +| `APS5_NO_PERF_DOM_INTERSECTION` | presence | `core/shader/recompiler/ControlFlow/src/Structurizer.cpp` | | computes dominator intersections into a fresh vector instead of intersecting in place | +| `APS5_NO_PERF_ITERATION_SELECTION` | presence | `core/shader/recompiler/ControlFlow/src/Structurizer.cpp` | | recomputes the post-dominator sets when finding a loop's iteration-selection merge instead of reusing them | +| `APS5_NO_RESULT_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | materializes a fresh result for every recompile instead of reusing the result memo | +| `APS5_NO_SRT_TAPE` | presence | `core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp` | | runs the runtime-source walk of a plan in the IR evaluator instead of the compiled tape | +| `APS5_NO_TABLE_RESOLUTION_REUSE` | presence | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | | stops a result memo miss from reusing an unchanged image table's resolution, so the table is resolved again | +| `APS5_NO_TABLE_SNAPSHOT_REUSE` | presence | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | | turns off the image table snapshot reuse, so every capture reads and compares each table column | +| `APS5_NO_WINDOW_NARROWING` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp` | | indexes every slot of the shader's vector window instead of only the slots the program uses | +| `APS5_PROBE` | string | `core/shader/recompiler/Translation/src/DispatchInstructions.cpp` | | pc:vgpr[:shift] probe that copies that VGPR into v255 after the instruction so image stores write it as a per-lane trace | +| `APS5_RAYTRACING` | string | `core/shader/recompiler/Translation/src/DispatchInstructions.cpp` | | selects the image_bvh_intersect_ray behaviour: strict throws and miss writes 0xffffffff | +| `APS5_SAVEEXEC_WRITE_FIRST` | presence | `core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp` | | writes the saved exec to the destination before reading the source in s_*_saveexec | +| `APS5_SINGLE_LANE` | string | `core/shader/recompiler/Recompiler.cpp` | | hex code addresses (comma separated, or all) of wave64 programs kept at one guest lane per invocation | +| `APS5_SRT_DEBUG` | presence | `core/shader/recompiler/Optimization/src/RequestMemoryView.cpp` | | prints extra SRT descriptor-walk debug output, such as guest reads outside the request memory | +| `APS5_TRACE_BDA` | presence | `core/shader/recompiler/Optimization/src/ResourceTracker.cpp` | | prints the first few accesses that make a program take the address-based path | +| `APS5_TRACE_BUFFER_WRITTEN` | presence | `core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp` | | prints how many guest buffer elements a program may store to and how many it only loads | +| `APS5_TRACE_IMAGE_TABLE` | presence | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | | traces image-table resolution | +| `APS5_VERIFY_SRT_COMPACT` | presence | `core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp` | | walks every SRT descriptor a second time through the IR evaluator and throws when the two walks differ | +| `APS5_VERIFY_SRT_TAPE` | presence | `core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp` | | walks a plan's runtime sources a second time through the evaluator, adopts its answers where the two differ and prints the first twenty differences | +| `APS5_WAVE_LDS_SUBGROUP` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp` | | restores subgroup-scope LDS barriers for single-wave workgroups instead of workgroup scope | + +### libSceAgc + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_TRACE_LABEL` | integer | `core/libs/prx/libSceAgc/Cb/src/Sync.cpp` | 0 | hex guest address of a RELEASE_MEM label whose packet is logged when recorded | + +### libSceAgcDriver + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `ANYPS5_GPU` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | restricts physical device selection to device names containing this case-insensitive substring | +| `ANYPS5_OVERLAY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp` | 0 | 1 shows the performance overlay; 0 or unset hides it, and any other value is refused | +| `ANYPS5_OVERLAY_POS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp` | tl | the overlay's corner: tl, tr, bl or br | +| `ANYPS5_PRESENT_MODE` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | fifo | selects the swapchain present mode: fifo, fifo_latest_ready, mailbox or immediate | +| `ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS` | presence | `core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp` | | fails the replay test instead of accepting a device that refuses descriptor binding flags | +| `ANYPS5_REQUIRE_ROBUST2` | presence | `core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp` | | fails the LDS lock-budget test when the device does not enable robustBufferAccess2, instead of only printing it | +| `ANYPS5_REQUIRE_VULKAN` | presence | `core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp` | | fails a test instead of skipping when no usable Vulkan device is present | +| `ANYPS5_SCREENSHOTS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp` | | directory the screenshot key saves its PNG files to, defaulting to anyps5-screenshots beside the executable | +| `APS5_ADDRESS_COPY_MAX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 64 | upper bound in MiB on the guest memory one address-based build copies; exceeding it aborts | +| `APS5_ALL_BUFFERS_WRITTEN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | notes every shader buffer element as written, as before bufferWritten existed | +| `APS5_ASSERT_GPU_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | aborts when a guarded path is reached without holding GuestMemory::GpuMutex | +| `APS5_ATOMIC_STAGE_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 1024 | size cap in KiB on an atomic shader element covered by device-local staging | +| `APS5_BARRIER_VALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | counts the barriers a hazard-tracking validator would emit and skip, by class and hazard kind | +| `APS5_BATCH_CAP` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | 64 | recorded work since the last submit at which a label batch, flush or reap is forced (0 disables the cap) | +| `APS5_BDA_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | also flushes pending storage results over address-based shader regions, which are otherwise exempt | +| `APS5_BDA_TABLE_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp` | 8 | number of recently used BDA page tables kept in the cache (1 is the previous behaviour) | +| `APS5_BLOCK_WRITEBACK_EACH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | stores the touched units only on every write-back instead of widening repeated partial stores | +| `APS5_BLOCK_WRITEBACK_FRAMES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | 8 | window of presents within which repeated partial stores trigger the whole-image widening | +| `APS5_BLOCK_WRITEBACK_PIECES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | 4 | number of partial stores within the window that triggers the whole-image widening | +| `APS5_BLOCK_WRITEBACK_WIDEN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | enables the widening that stores a whole image when it keeps being asked for in pieces | +| `APS5_BUFFER_POOL_SHARED` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | uses one buffer tier for every allocation size, as before the tier split | +| `APS5_BUFFER_POOL_SLOTS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | 4096 | bounds the buffers each pool tier retains (0 keeps the default of 4096) | +| `APS5_CAPTURE_GPU_COPIES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | records GPU buffer copies in the capture trace (requires APS5_CAPTURE_TRACE) | +| `APS5_CAPTURE_INPUTS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | records each draw's inputs in the capture trace (requires APS5_CAPTURE_TRACE) | +| `APS5_CAPTURE_TRACE` | presence | `core/libs/prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp` | | writes capture events to capture-trace.log | +| `APS5_CHECK_INDIRECT_ARGS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | reads GPU-side indirect draw records back on the CPU and prints the first ones | +| `APS5_COMPLETION_STORE_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | makes a store made by a completion action wait for later batches over its range | +| `APS5_COPY_DESTINATION_STRICT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | refuses a CPU copy whose destination has any pending writer, not only an unsettled one | +| `APS5_COPY_HLE_CPU_MAX` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | 262144 | largest copy in bytes the CPU side of the copy HLE handles | +| `APS5_COPY_HLE_GPU_MAX` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | largest copy in bytes the GPU side of the copy HLE handles; unset means no limit | +| `APS5_COPY_READ_TRACKING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | notes in-place reads of a range; set to 0 to disable, making the CPU copy path require an idle recorder | +| `APS5_COPY_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | treats any pending writer over a copy's range as needing a wait | +| `APS5_COPY_VERIFY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | verifies a CPU copy's source and destination bytes against the pre-copy values after a sync | +| `APS5_COPY_WAIT_SOURCE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | waits for the source's producer batch when it is the only one in flight, on non-zero queues | +| `APS5_COUNT_ALL_COMPLETION_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | counts a completion label also stored on the GPU as pending from its registration | +| `APS5_CPU_COPIES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | copies misaligned descriptor sub-ranges on the CPU instead of with a GPU buffer copy | +| `APS5_CPU_DCC_KEYS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp` | | always stores DCC keys on the CPU | +| `APS5_CPU_FLIP_IN_ORDER` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp` | | orders every CPU flip behind queued work instead of presenting unqueued flips at once | +| `APS5_CPU_STORES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | writes packet stores on the CPU instead of recording them on the GPU | +| `APS5_DCC_KEYS_EACH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every DCC key store at once instead of batching them | +| `APS5_DEPTH_COPY_MIN_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1024 | minimum surface size in KiB for a depth target to take the depth-copy path | +| `APS5_DEPTH_PLANE_WHOLE_STORE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp` | | stores a stale depth plane whole when memory over it is read, instead of only the 64 KiB units the read overlaps | +| `APS5_DISPATCH_CACHE_BYTES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 268435456 | size of the dispatch cache in bytes (0 keeps the default of 256 MiB) | +| `APS5_DISPATCH_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 8192 | number of dispatch cache entries; 0 picks a default from the variant count | +| `APS5_DISPATCH_STAMP_VALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | validates dispatch cache hits by write stamps, forcing one variant and a 4096-entry cache | +| `APS5_DISPATCH_VARIANTS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 4 | number of dispatch variants kept per cache entry, clamped to the maximum | +| `APS5_DISPMOVE_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 1024 | upper bound in KiB on the read-only buffers a dispatch-cache hit moves into its template, above which the dispatch is prepared again | +| `APS5_DRAIN_ALL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | drains every packet, disabling the batched recording path | +| `APS5_DRAIN_COMPLETION_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | records completion labels immediately instead of deferring them with the batch | +| `APS5_DRAW_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | 4096 | number of entries in the draw cache; 0 keeps the default 4096 | +| `APS5_DRAW_DRAIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | treats draw packets as drains, waiting for them instead of recording ahead | +| `APS5_DRAW_INPUTS_IN_PLACE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | binds draw vertex and index ranges in place from host imports also with NVIDIA's Windows driver, which copies them by default | +| `APS5_DRAW_KEY_ALL_USER_WORDS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp` | | includes all 32 user words in a draw's register key instead of the used count | +| `APS5_DRAW_SNAPSHOT_MAX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 64 | upper bound in MiB on the snapshot bytes a draw's bindings may take | +| `APS5_DRAW_TRANSITIONS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | keeps the per-draw upload and download barriers and layout transitions instead of the lean recorded path | +| `APS5_DRAW_UPLOAD_MAX` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 4096 | largest draw snapshot in bytes copied into the per-recorder upload chunk; 0 turns the chunk off | +| `APS5_DUMP_COMPUTE_MIN_WORDS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | only captures compute pipelines whose SPIR-V has at least this many words | +| `APS5_DUMP_COMPUTE_PIPELINES` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | directory in which captured compute pipelines are written | +| `APS5_DUMP_DRAW_SHADERS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | 0 | hex color-target or slot-0 address whose draw's shaders are dumped | +| `APS5_DUMP_DRAW_SLOT1` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | 0 | hex slot-1 address whose draw's shaders and register state are dumped | +| `APS5_DUMP_FRAMES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 0 | saves the first n presented frames as frame_.bmp | +| `APS5_DUMP_FRAMES_EVERY` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 1 | dumps every n-th presented frame | +| `APS5_DUMP_QUEUE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | dumps the first submissions of the queue with this hex id | +| `APS5_DUMP_REJECTED` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp` | | writes a rejected submission to rejected_submission_.bin | +| `APS5_DUMP_SCALE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 4 | keeps every n-th pixel in each direction of a dumped frame | +| `APS5_DUMP_SHADERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | writes each prepared shader's request to shader_
.req | +| `APS5_DUMP_STORAGE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | selects storage textures to dump by hex address or WxHxL[:format] extent as storage_
_.raw | +| `APS5_DUMP_SUBMITS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp` | | directory in which every submission is captured for replay | +| `APS5_DUMP_TARGETS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | 0 | dumps the first n renders of each non-resident color target as target_
_.raw in the working directory; a non-zero value also runs every draw synchronously, and in practice it needs APS5_NO_RESIDENT_TARGETS=1 | +| `APS5_DUMP_TEXTURE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | hex addresses or WxH extents whose sampled textures are dumped on first upload as texture_
_.raw | +| `APS5_EAGER_WRITEBACK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | writes back pending storage layers eagerly | +| `APS5_EXACT_DRAW_WRITES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp` | | sizes a color or depth target's draw write ranges from its own bytes and slices instead of the slack and unknown-byte approximation; 0 leaves the approximation on | +| `APS5_FILL_CHAIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | seeds and doubles the fill pattern in place in the import | +| `APS5_FILL_CLEAR_EXACT_ONLY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | turns a fill into a clear only when it covers the target exactly and nothing else | +| `APS5_FILL_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | waits for every recorded store over the fill's range | +| `APS5_FLIP_INFLIGHT` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 1 | number of presentations that may trail on the GPU (0, 1 or 2) | +| `APS5_FLIP_READ_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records the in-place reads of completed batches and checks them before the present blit's fence | +| `APS5_FORCE_SDR` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp` | unset | suppresses advertised HDR availability while retaining detected host capabilities; uses the existing SDR presentation path | +| `APS5_FORCE_STRUCTURIZER_FALLBACK` | presence | `core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp` | | makes the control-flow test expect the reach-flag fallback instead of no cloning | +| `APS5_FULL_BARRIERS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every barrier instead of merging redundant ones | +| `APS5_GPU_CHECKPOINTS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | attaches device diagnostic checkpoints through VK_NV_device_diagnostic_checkpoints | +| `APS5_GPU_COPY_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 1024 | size cap in KiB on a descriptor sub-range copied through the GPU path, larger ones staying on the CPU | +| `APS5_GPU_NO_WRITEBACK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | skips writing GPU results back into guest memory | +| `APS5_GPU_WAIT_TIMEOUT_MS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | 1000 | milliseconds a GPU wait may run before a warning is printed | +| `APS5_GROUP_CAPTURE_ALL_QUEUES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp` | | resolves group-ahead capture on queue 0 as well | +| `APS5_HANDOFF_SPIN_US` | integer | `core/libs/prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp` | | microseconds a thread spins for a handed-off draw's work or for the GPU mutex before blocking, clamped to 0..10000; unset spins 150 us when the process has more than 8 usable processors and not at all otherwise | +| `APS5_HEAP_MIRROR_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 24576 | budget in MiB for the heap mirrors held for registered allocations | +| `APS5_HOOK_FLUSH_CPU_BLOCKS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | set to 0 flushes pending images whose every 64 KiB block the CPU wrote, instead of skipping them | +| `APS5_HOOK_FULL_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | makes the flush hook's recorded-store wait a full Sync under the GpuMutex | +| `APS5_HOOK_LOCKED_WAIT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | keeps the flush hook's pending-write wait under the GpuMutex | +| `APS5_HOST_IMPORT_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 0 | budget in MiB for pinned host imports; `0` selects the larger of 6144 MiB and half the physical memory, at most the largest host heap minus the larger of 2 GiB and half that heap (4055 MiB on a 16 GB Windows PC with an RTX 4060) | +| `APS5_IGNORE_DEPTH_TEST` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp` | | renders depth- and stencil-tested draws without a depth target as if the tests always passed | +| `APS5_IMAGE_POOL_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | size in MiB of the released image memory the buffer pool keeps for reuse by memory type and size while the device-local heap is under its reported budget, a quarter of the device tier's budget by default; `0` frees image memory at once | +| `APS5_INDIRECT_VERTEX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp` | 64 | cap in MiB on a vertex attribute extent, above which a GPU-side indirect draw resolves on the CPU | +| `APS5_INSERT_COMPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | compares a fresh capture's regions against the recorder before inserting it into the cache | +| `APS5_KEPT_DESCRIPTOR_SETS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 8 | number of freed dedicated image table descriptor sets kept for a later build with the same layout and sizes; 0 keeps none | +| `APS5_KEYFLIP_DROP` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | drops a pending unit's results on a clear-code to uncompressed key flip, as before | +| `APS5_KEYS_FILL_CLEAR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | clears an image from DCC keys whose fill pattern it matches (only when set to 1) | +| `APS5_LABEL_FLUSH_US` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | 1000 | age in microseconds at which a pending label batch is recorded or submitted | +| `APS5_LABEL_GATE_ALL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | gates a label store on any pending completion, not only one overlapping the range | +| `APS5_LABEL_LOCK_EACH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | records each label under the GpuMutex instead of deferring it | +| `APS5_LABEL_OWN_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketFlush.cpp` | | makes a label flush take the GpuMutex itself instead of relying on a self-locking packet | +| `APS5_LABEL_REAP_ALL_QUEUES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | reaps completion labels on every queue instead of only queue 0 | +| `APS5_LABEL_RUNS_INLINE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records store runs at once and closes each run at the next command | +| `APS5_LABEL_STORE_ALWAYS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | writes back every label store even when a later store already covered its bytes | +| `APS5_LABEL_SUBMIT_NOW` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | submits every label immediately instead of waiting for the flush deadline | +| `APS5_LABEL_TRUST_LATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | trusts cache entries with a stamp at or below the wait's stamp under the late rule (off unless set to a non-zero value) | +| `APS5_LABEL_TRY_EACH_PACKET` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | attempts to record deferred labels at every self-locking packet instead of deferring to its lock | +| `APS5_LAND_WAITS_ALL_QUEUES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | notes pending waits on queue 0 as well | +| `APS5_LAZY_SHADER_PREPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | prepares a registered shader on its first use instead of while registering it | +| `APS5_LDS_SLACK` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp` | 0 | dwords added to every dispatch's LDS allocation | +| `APS5_LOCKED_BUILD` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | builds the whole dispatch preparation under the GpuMutex, skipping the split path | +| `APS5_LOCKED_DRAW_PREPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | | takes the GpuMutex before preparing a draw | +| `APS5_LOG_STATS` | presence | `core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp` | | prints the AGC log's line, byte and coalescing statistics every window; 0 turns them off | +| `APS5_LOG_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp` | | writes AGC log lines on the calling thread instead of through the asynchronous sink thread | +| `APS5_LOOP_GUARD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | enables the fault buffer that guards shader loops running past their bound | +| `APS5_MIMALLOC` | presence | `core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp` | | loads `mimalloc.dll` in the Windows entry stub, so the process allocates through mimalloc instead of the C runtime's heap | +| `APS5_MIRROR_REFRESH_THREADS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 3 | number of helper threads that refresh heap mirrors, clamped to 0 through 16 | +| `APS5_NO_ADDRESS_READ_PASSES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | stops a draw that only reads through its buffer descriptors from counting as read-only, so it breaks the pass instead of continuing the producer's | +| `APS5_NO_ADDRESS_SPACE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | disables the address-space cache so every build rebuilds its space | +| `APS5_NO_ADJACENT_GENERATION` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | leaves adjacent pending images at their generation during a write-back, as before | +| `APS5_NO_ALIAS_BORROW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | disables borrowing a pending aliased storage image | +| `APS5_NO_ATOMIC_STAGING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | binds atomic shader elements in place instead of staging them through a device-local buffer | +| `APS5_NO_BARRIER_BATCH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records the staging, write-back and label-run leading barriers that a covering barrier already orders, instead of leaving them out | +| `APS5_NO_BARRIER_ELISION` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every leading barrier instead of merging it into the following submission | +| `APS5_NO_BDA_TABLE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp` | | builds every BDA descriptor table instead of reusing the recent-table cache | +| `APS5_NO_BLOCK_TRACKING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | tracks array layers, or the whole surface, instead of 64 KiB blocks | +| `APS5_NO_BUFFER_CLASSES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | matches exact sizes in the buffer pool instead of rounding up to size classes | +| `APS5_NO_CACHED_PRECOLLECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | leaves the write-watch walks of a cached object's Revalidate under the GPU mutex | +| `APS5_NO_CLEAR_HAND_OVER` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | writes other images' pending results back at a clear-key storage refresh instead of handing them over to the refreshed image | +| `APS5_NO_CLEARED_VIEW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | keeps fast-cleared surfaces as CPU snapshots instead of viewing the cleared storage image | +| `APS5_NO_COLLECT_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | disables the per-thread memo of write-watch collects | +| `APS5_NO_CONTEXT_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | rebuilds the Graphics::Context on every call instead of returning the cached one | +| `APS5_NO_COPY_ALIAS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | keeps every device copy a transfer instead of aliasing the source storage image | +| `APS5_NO_COPY_HLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | disables the copy-kernel HLE, so matching copies run as compute | +| `APS5_NO_COPY_KNOWN_VALUES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | disables serving a copy's destination from source values the driver already knows | +| `APS5_NO_CPU_COPY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | disables the CPU copy path of the copy HLE | +| `APS5_NO_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | disables dispatch-cache hits that differ only in ShaderData or FlattenedSrt words | +| `APS5_NO_DCC_PRESYNC` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | leaves the DCC key ranges out of the surfaces a build pre-syncs, so only the surface itself waits for its producer | +| `APS5_NO_DCC_WORD_SCAN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp` | | restores the byte-by-byte DCC key scan instead of the word-at-a-time compare | +| `APS5_NO_DEFERRED_EVICTION` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | destroys displaced resource-cache entries under the mutex instead of deferring them to the open batch | +| `APS5_NO_DEFERRED_SETTLES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | settles watched private host imports as they open instead of through the deferred block-settling path (Windows only) | +| `APS5_NO_DISPATCH_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | disables the dispatch cache so every dispatch takes the uncached path | +| `APS5_NO_DISPATCH_KEY_HYGIENE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | mixes every shader register into the dispatch key instead of only the registers that affect the dispatch | +| `APS5_NO_DISPATCH_LRU` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchInsert.cpp` | | clears the whole dispatch cache on overflow instead of evicting the least recently used entry | +| `APS5_NO_DISPATCH_MOVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | leaves a dispatch-cache hit from moving its read-only buffers into the template, so it prepares the dispatch again; also off when APS5_NO_TEMPLATE_DATA_REFRESH is set | +| `APS5_NO_DISPATCH_RECIPE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | builds no dispatch recipes, so dispatch-cache hits repeat the recorded work | +| `APS5_NO_DRAW_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | disables draw-cache hits that differ only in ShaderData words | +| `APS5_NO_DRAW_INPUTS_IN_PLACE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | copies draw vertex and index ranges instead of binding them in place from host imports, on any device; wins over `APS5_DRAW_INPUTS_IN_PLACE` | +| `APS5_NO_DRAW_KEY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | registers no draw key, so every draw decodes its programs instead of hitting the draw cache | +| `APS5_NO_DRAW_KEY_TRIM` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | keys the render target and index ranges into the draw resource key instead of repeating the alias checks on a hit | +| `APS5_NO_DRAW_PRECHECK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp` | | disables the draw precheck that rejects empty or invalid draws early | +| `APS5_NO_DRAW_RECIPE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | builds no draw recipes, so draw-cache hits repeat the recorded stages | +| `APS5_NO_DRAW_RELOCATION` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp` | | disables draw relocation, which rewrites a draw's moved user words from a learned variant instead of preparing the draw again | +| `APS5_NO_DRAW_RESOURCE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | builds every draw's shader resources instead of reusing a cached identical build | +| `APS5_NO_DRAW_SRT_ENTRIES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | disables the draw cache's SRT entries, so every draw decodes its programs | +| `APS5_NO_DRAW_STAGE_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | disables draw-cache hits that differ only in a stage's ShaderData words | +| `APS5_NO_EPOCH_COLLECT_MAP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | drops the per-epoch map of completed collects, leaving only the fixed-size thread memo to answer a repeated collect | +| `APS5_NO_EPOCH_REVALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | runs every Revalidate's per-element and per-region checks instead of the epoch gate | +| `APS5_NO_FAST_REVALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | always repeats the resource lookups instead of using the fast proof | +| `APS5_NO_FILL_CLEAR` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | disables the clear-through-DCC-keys path in a fill | +| `APS5_NO_FILL_HLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | disables the fill-kernel HLE, so matching fills run as compute | +| `APS5_NO_FLUSH_PRETEST` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | lists every pending image on FlushPending instead of testing whether a pending unit lies inside the range | +| `APS5_NO_FOREIGN_WRITERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | records no draw-written ranges as foreign writers for capture validation | +| `APS5_NO_GATE_RETRY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | retries no validation whose entry moved, leaving the dispatch or draw unvalidated | +| `APS5_NO_GPL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | draws without graphics pipeline libraries; the device still enables their extensions, so the driver builds its pipelines as for a default device | +| `APS5_NO_GPU_DUMP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | decodes dump frames from the tiled guest buffer on the CPU | +| `APS5_NO_GPU_INDIRECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp` | | reads indirect dispatch arguments on the CPU instead of letting the GPU count them | +| `APS5_NO_GPU_INDIRECT_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | reads indirect draw arguments through the flush hook on the CPU instead of from the GPU | +| `APS5_NO_GPU_SAMPLE_DUMPS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | dumps occlusion-counter samples on the CPU instead of through the GPU copy path | +| `APS5_NO_GPU_THREAD_DIMENSIONS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp` | | reads the thread counts of an indirect dispatch with USE_THREAD_DIMENSIONS on the CPU instead of turning them into group counts and a thread limit on the GPU | +| `APS5_NO_HOLD_PROFILE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | turns off GpuMutex hold-time profiling, keeping only the wait timing | +| `APS5_NO_HOOKSYNC_PROFILE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | leaves only the plain hook-sync counts under APS5_PROFILE_DRAW | +| `APS5_NO_HOOK_COMPLETION_GUARD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets the flush hook record the accessing worker's queued labels even from inside a completion action | +| `APS5_NO_HOOK_SNAPSHOT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | takes the GpuMutex on every access instead of using the pending-write snapshot | +| `APS5_NO_HOST_IMPORT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables importing guest memory, so address-based shaders copy instead of using it in place | +| `APS5_NO_HW_F32_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | skips the startup f32 denormal flush probe and keeps the emulated flush in every shader | +| `APS5_NO_HW_F32_PRESERVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | stops requesting DenormPreserve for 32-bit floats, so the device keeps its default f32 denormal handling | +| `APS5_NO_IMAGE_PAGE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | leaves the main guest image's page-protection table uncached | +| `APS5_NO_INDIRECT_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp` | | rejects indirect draw packets at submission validation | +| `APS5_NO_JOIN_WAW_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets a contiguous store join the pending one without the write-after-write check | +| `APS5_NO_KEEP_NEW_STORAGE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | does not keep a newly created storage image alive with the open batch; such an image evicted before the batch completes can hand its memory to another image through the buffer pool while the batch still writes it | +| `APS5_NO_KEYS_FILL_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp` | | records no memo of GPU-side DCC key fills | +| `APS5_NO_KEYS_REFILL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | keeps a covered image's uploaded keys as they were instead of marking them uncompressed after a matching key fill | +| `APS5_NO_KEYS_RESCAN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | reuses the DCC keys scanned before the lookup's refresh instead of discarding them for a rescan | +| `APS5_NO_KEY_FAST_PATH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp` | | scans a surface's DCC keys on every call instead of using and storing a proof | +| `APS5_NO_LABEL_BATCHING` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | turns off every label-batching behaviour, the label runs, queued labels and batched submit | +| `APS5_NO_LABEL_BATCH_SUBMIT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | disables batched label submission | +| `APS5_NO_LABEL_PACKET_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketFlush.cpp` | | disables the label flush between packets | +| `APS5_NO_LABEL_RUNS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every store in a run of its own between a barrier pair | +| `APS5_NO_LABEL_SHORTCUT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | disables the pending-label table shortcut in a memory wait | +| `APS5_NO_LAYER_CLEAR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | tracks every surface as one layer instead of per array layer | +| `APS5_NO_LAYER_REFUSAL_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | re-checks a layer whose surface check failed at its current generation on every fill | +| `APS5_NO_LAYOUT_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | builds descriptor set layouts fresh instead of through the descriptor cache | +| `APS5_NO_LEASE_MIRROR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | restores copies instead of image mirrors for address-based builds | +| `APS5_NO_LIBRARY_PREBUILD` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | disables prebuilding registration-form shader libraries (only 1 disables it) | +| `APS5_NO_LOCK_PROFILE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | turns off GpuMutex wait profiling under APS5_PROFILE_DRAW | +| `APS5_NO_MOVED_BUFFER_TEMPLATES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | keys the full descriptor words of read-only guest buffers instead of allowing a hit when they move | +| `APS5_NO_OBSERVATION_GUARD` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | observes every validated range, not only the ranges whose flush hook waited | +| `APS5_NO_OPPORTUNISTIC_REAP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | skips retiring already-finished batches before a submit | +| `APS5_NO_OWN_IMAGE_REFRESH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | lets a Pending failure of the fast proof take the full walk instead of the own-object refresh | +| `APS5_NO_PARALLEL_MIRROR_REFRESH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | disables the mirror-refresh thread pool, so mirror refreshes run on the calling thread | +| `APS5_NO_PATTERN_FILL_HLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | disables the dword-pattern fill HLE | +| `APS5_NO_PAUSE_SPIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | disables the CPU pause-spin in a blocked memory wait | +| `APS5_NO_PENDING_HTILE_FILLS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp` | | reads the HTILE words of a depth target bind even when recorded fills are pending over them, instead of working the words out from the fills | +| `APS5_NO_PERF_FRONTEND_PAIR` | presence | `core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp` | | disables frontend-pair reuse in the shader-memory test | +| `APS5_NO_PERF_LAYOUT_PAIR` | presence | `core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp` | | disables layout-pair reuse in the shader-memory test | +| `APS5_NO_PIPELINED_INDIRECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | | drains the draw pipeline at every CPU-path indirect draw instead of queuing it behind the pipeline | +| `APS5_NO_PIPELINE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp` | | builds a pipeline per draw instead of reusing cached ones | +| `APS5_NO_PIPELINE_JOURNAL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | | keeps the pipeline journal off; 0 leaves it on, and it is also off while the shader disk cache is disabled | +| `APS5_NO_PIPELINE_LTO` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp` | | disables background link-time optimization of graphics pipeline libraries | +| `APS5_NO_POOL_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | allocates descriptor pools fresh instead of through the descriptor cache | +| `APS5_NO_PRECOLLECT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | disables the stage-A precollect pass | +| `APS5_NO_PREPARED_FIND` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | looks the resource-cache key up again instead of using the object stage A found | +| `APS5_NO_PRESENT_EPOCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | keeps the presenter at collect epoch 0 instead of adopting the newest epoch a worker started | +| `APS5_NO_PRESYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables the stage-A pre-sync of pending writes | +| `APS5_NO_PROC_TABLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | resolves the device's Vulkan entry points per call instead of filling the function table once | +| `APS5_NO_QUEUED_DRAW_READ_ORDER` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp` | | stops queued draws and DMA copies from holding back write-backs over the guest memory they read at commit | +| `APS5_NO_QUEUED_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | disables the queued-label table | +| `APS5_NO_RECORDED_CLEAR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | waits for a storage image's fast clear in a batch of its own instead of recording it | +| `APS5_NO_RECORDER_SYNC_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | gives the draws the recorder waits for their own batch instead of recording them with the wait | +| `APS5_NO_RECORD_COPIED_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | waits for copied-write draws at once instead of listing their write-back for later | +| `APS5_NO_RECT_INDIRECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp` | | sends an indirect rectangle-list draw down the CPU path instead of the GPU-side path | +| `APS5_NO_RESIDENT_LINEAR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | drops the residency of a linear color target whose address already has a host import, so it is copied in and out again | +| `APS5_NO_RESIDENT_PRESENT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | always presents through guest memory instead of the resident image | +| `APS5_NO_RESIDENT_TARGETS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | copies every render target in and out again instead of keeping it resident | +| `APS5_NO_RESOURCE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables the shared resource cache, so every dispatch builds its resources fresh | +| `APS5_NO_SAMPLED_FROM_STORAGE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | keeps every sampled texture a CPU snapshot instead of viewing the surface's storage image | +| `APS5_NO_SAMPLER_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | creates a sampler per binding instead of caching them | +| `APS5_NO_SEPARATE_QUEUED_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets a queued label enter the recorded table and replace a recorded entry of its dword | +| `APS5_NO_SINGLE_PASS_COLLECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | collects written pages in one cleared pass instead of a second confirming walk | +| `APS5_NO_SITE_SAMPLING` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | disables per-site sampling of write evidence | +| `APS5_NO_SNAPSHOT_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | skips the live compare that proves a captured snapshot still equals the guest bytes | +| `APS5_NO_SORTED_SNAPSHOT_LOOKUP` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | scans the snapshot's regions in AddSnapshot instead of searching them | +| `APS5_NO_SPACE_STAMP_ONCE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | stamps every direct import of a cached address space on each hit instead of once per space | +| `APS5_NO_SPARSE_HEAP_MIRROR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | disables sparse heap mirrors, mirroring only whole heap ranges | +| `APS5_NO_STAGE_A_IMAGES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | leaves every image element to the stage-B full lookup instead of a stage-A record | +| `APS5_NO_STORAGE_DEDUPE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | looks each identical consecutive storage descriptor up, and refreshes its image, separately | +| `APS5_NO_SYNC_SITES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | leaves only the per-source and per-thread sync counts, with no per-site attribution | +| `APS5_NO_SYNC_THROUGH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | makes every pending-write sync a full Sync instead of the targeted unlocked wait | +| `APS5_NO_TABLE_REPLAY` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | makes a reused bound image table rerun each element's checks instead of replaying its recorded bind outcomes | +| `APS5_NO_TABLE_REUSE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | disables reusing a materialized image table | +| `APS5_NO_TARGET_WRITEBACK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | skips writing render targets back to guest memory after the draw | +| `APS5_NO_TEMPLATE_DATA_REFRESH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | keys the ShaderData and FlattenedSrt words into compute keys instead of refreshing a template's data buffers | +| `APS5_NO_TEXTURE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | builds every texture and storage-image object fresh instead of caching it | +| `APS5_NO_TEXTURE_HASH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | finds texture-cache entries by scanning the list instead of the hash index | +| `APS5_NO_TIMELINE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | leaves timeline semaphores off, so drains wait under the mutex | +| `APS5_NO_UNIT_SHADOW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | | makes every unit shadow primitive inert | +| `APS5_NO_UNLOCKED_DEVICE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | loads the device and runs a dispatch under the GPU mutex instead of using an unlocked device | +| `APS5_NO_UNLOCKED_DRAIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | keeps the GPU mutex held while draining a packet instead of the unlocked drain | +| `APS5_NO_UNLOCKED_VALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchLookup.cpp` | | keeps the dispatch-cache mutex held while validating a cache entry | +| `APS5_NO_UNREGISTERED_DROP` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | stores a storage image's pending results even when its memory is no longer registered, instead of dropping them | +| `APS5_NO_VALIDATE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | validates every draw instead of serving the validation memo | +| `APS5_NO_VALIDATE_SKIP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | syncs every pending write instead of skipping validation on written-element evidence | +| `APS5_NO_VOUCHED_VALIDATION` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/VariantValidation.cpp` | | validates a cached draw or dispatch through its per-run checks instead of its table block's vouch | +| `APS5_NO_WAIT_OVERLAP_SUBMIT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | disables the unlocked wait that submits recorded work while overlapping writes are open | +| `APS5_NO_WORDWISE_CAPTURE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp` | | fetches every captured page whole instead of reading pending pages word by word | +| `APS5_NO_WORKER_AFFINITY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp` | | leaves the queue workers and presenter unpinned (affinity mask 0) | +| `APS5_NO_WRITER_KEYED_EVIDENCE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | records written-element evidence without keying it by writer | +| `APS5_NO_WRITE_EVIDENCE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | disables the written-element evidence that lets a read skip the flush hook | +| `APS5_NO_WRITE_ONLY_PASSES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | counts a buffer a draw only stores to as read and written, so the next draw storing to it begins a new render pass | +| `APS5_NO_WRITTEN_SHADOW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | binds written staging elements in place instead of through the written shadow | +| `APS5_PACKET_EPOCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitStatistics.cpp` | | makes queue workers bump the collect epoch before every packet | +| `APS5_PIPELINED_DRAWS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp` | 8 | depth of the queue-0 draw pipeline: 0 records draws inline, 1 selects the default depth of 8, and any other value is the depth capped at 256 | +| `APS5_PIPELINE_DISPATCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | records an eligible dispatch as an item in the queue-0 pipeline instead of running it inline | +| `APS5_PIPELINE_DMA` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | records a queue-0 DMA data packet in the draw pipeline instead of executing it inline | +| `APS5_PIPELINE_DRAIN_FILLS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | runs queue-0 buffer fills through the draw pipeline to keep their order; any value stores them in place instead | +| `APS5_PIPELINE_DRAIN_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | answers a queue-0 wait from the draw pipeline's own pending labels while it is busy; any value drains the pipeline at the wait | +| `APS5_PIPELINE_DRAIN_SUBMISSIONS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | lets a submission end without submitting its recorded work, so it joins a later submit with the pipeline's queue; any value drains the pipeline and submits at the end of the submission | +| `APS5_PIPELINE_JOURNAL_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | 1024 | number of journal entries loaded at most; a longer journal is truncated to this count | +| `APS5_PIPELINE_JOURNAL_MAX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | 1024 | size cap in MiB on the pipeline journal; later pipelines are not recorded once it is reached | +| `APS5_PIPELINE_JOURNAL_THREADS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | | threads that replay the journal's pipelines, 0 disables the replay; unset uses half the hardware threads, at least one | +| `APS5_PIPELINE_STATS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | captures and prints driver statistics for each new pipeline, requiring `VK_KHR_pipeline_executable_properties`; `1` enables | +| `APS5_PIPELINE_STORE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp` | 1024 | LRU bound on the entries of the pipeline store; an entry is evicted only once no recorded draw holds it | +| `APS5_PM4_RING` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp` | 16 | number of submissions the PM4 ring keeps for dumps, from 0 to 4096; 0 disables the ring | +| `APS5_POLL_REAP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | reaps completions on every poll tick | +| `APS5_POLL_TRY_EACH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | tries the poll service on every tick instead of at the poll interval | +| `APS5_PREBUILD_THREADS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | 1 | number of threads in the one-thread pool that prebuilds registration-form shader libraries | +| `APS5_PRESENT_POLL_FENCE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | polls the present fence every 50 us instead of blocking on it | +| `APS5_PROBE_DISPATCH` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | probes one dispatch at a hex program address (optionally `:index`) with debug output | +| `APS5_PROFILE_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | prints the per-phase driver and draw timing totals | +| `APS5_PROFILE_GPU` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records the GPU time of recorded work by key from batch timestamps | +| `APS5_REAP_EACH_PACKET` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | reaps deferred labels after every packet | +| `APS5_RELEASE_ON_UNLOCK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | destroys deferred batches on the unlocking thread instead of a release thread | +| `APS5_RELEASE_QUEUE_MAX` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | 256 | maximum deferred batches handed to the release thread before a thread destroys its own | +| `APS5_RELEASE_UNDER_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | destroys deferred batches in finish() under the lock instead of on a release thread | +| `APS5_RESOURCE_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1024 | maximum entries in the per-device shader-resource cache | +| `APS5_RESOURCE_CACHE_TABLE_ELEMENTS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 65536 | number of image-table elements the shader-resource cache keeps (0 keeps the default) | +| `APS5_SAMPLE_COMMITTER` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples queue 0's draw committer every millisecond and writes its stacks to a file (Windows only) | +| `APS5_SAMPLE_THREADS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples every thread of the process every 2 ms and writes each busy thread's hottest frames to a file (Windows only) | +| `APS5_SAMPLE_WORKER` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples the calling queue worker every millisecond and writes its stacks to a file (Windows only) | +| `APS5_SHADOW_VERIFY` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | | verifies that the shadow-published bytes equal the guest bytes | +| `APS5_SHARED_COLLECT_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | uses the tracker's shared collect-memo ring under its mutex instead of the per-thread ring | +| `APS5_SHARED_MEMORY_COUNTERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | accumulates the memory counters into one shared set with atomic RMWs | +| `APS5_SKIP_INVALID_INDIRECT_REGISTERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | skips the entries whose offset dword is no register offset in a SET_*_REG_INDIRECT list read again where its packet executes, and writes its valid entries, instead of stopping | +| `APS5_SKIP_REFUSED_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | | skips a draw whose graphics stage the recompiler refuses, reporting each refused shader once, instead of failing the queue | +| `APS5_SKIP_REQUEST_DUMPS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp` | 8 | caps RecompileRequest dumps per shader-skip reason | +| `APS5_SKIP_SHADER` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | 0 | hex program address of a shader to skip preparing | +| `APS5_SNAPSHOT_CHECK_FATAL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | throws on the first snapshot that differs from registered memory instead of only counting them | +| `APS5_SPANNED_WAVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | spans a single-wave wave64 compute program over two full 32-lane subgroups | +| `APS5_SPIRV_OPT` | string | `core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp` | | selects the SPIR-V optimization pipeline: `full` restores the performance pipeline, `none` skips optimization | +| `APS5_SRGB_ATTACHMENT_PROXY` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | forces the sRGB color-attachment proxy format; `1` forces it | +| `APS5_SRGB_SHADER_DECODE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp` | | `1` forces shader sRGB decode for the sRGB 8-bit formats; `copy` does the same, except that 8 and 8_8 sRGB sample an RGBA8 sRGB copy when the device can blit to RGBA8 | +| `APS5_STAGING_POOL_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | size in MiB of the device-local staging buffer pool; `0` disables the device tier | +| `APS5_STALE_EVICT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | also evicts a proved-present dead image's pending layers on an access | +| `APS5_STALL_WATCHDOG_SECS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp` | 0 | seconds a queue may go without progress before the submission rings are dumped; 0 disables the watchdog, at most 86400 | +| `APS5_STORE_AT_FLIP` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | stores every pending storage-image result at each display flip; only `1` is accepted | +| `APS5_STORE_AT_LABEL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | flushes every pending storage-image result when a label is written on the GPU | +| `APS5_SUBMISSION_EPOCH_EACH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | bumps the guest-memory collect epoch on every submission instead of only when the submission is newer than the last bump | +| `APS5_SUBMIT_AT_END` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | submits recorded work at the end of a submission even when a worker is queued | +| `APS5_SUSPEND_DRAIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | drains the device at a suspend point | +| `APS5_SYNC_COMPLETION_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | keeps every draw with completion work synchronous | +| `APS5_SYNC_DISPATCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | waits for every dispatch instead of batching it | +| `APS5_SYNC_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | makes every draw synchronous | +| `APS5_SYNC_FLIP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | makes flips synchronous (no frames in flight) | +| `APS5_SYNC_LEASE_DISPATCH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | waits for leased draws and dispatches at once instead of deferring them | +| `APS5_SYNC_SAMPLE_DUMPS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | takes the CPU path for occlusion-counter sample dumps, as `APS5_NO_GPU_SAMPLE_DUMPS` does | +| `APS5_SYNC_SHADER_PREPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | prepares registered shaders on the registering thread instead of in the background | +| `APS5_SYNC_TEXTURE_UPLOAD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | restores the waiting batch for every texture upload | +| `APS5_TEMPLATE_AUDIT_MS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1000 | milliseconds after which a cached shader-resource object takes the full walk instead of the fast proof; `0` turns the audit off | +| `APS5_TEXTURE_CACHE_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 0 | forced byte budget in MiB for the storage and sampled texture caches; `0` selects the budget automatically | +| `APS5_THREAD_NICE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | -10 | nice value the raised queue workers and presenter ask rtkit for (clamped to -20..19) | +| `APS5_THREAD_PRIORITY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | off | raises the queue workers and presenter above the title's threads: `off`, `high` or `rt` | +| `APS5_THREAD_RT` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | 10 | realtime priority the raised threads ask rtkit for (clamped to 1..99) | +| `APS5_THREAD_RTTIME_US` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | 200000 | hard RLIMIT_RTTIME the realtime plan lowers to, in microseconds | +| `APS5_TRACE_BARRIERS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | prints that many simulated hazard barriers before stopping | +| `APS5_TRACE_BENCH` | string | `core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp` | | `1` with `APS5_TRACE_SUMMARY_EVERY` also rewrites `aps5-bench--.json.gz` with every interval's frame times; any other value fails the first present | +| `APS5_TRACE_BIGBUF` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | lists every copied guest region of at least 1 MiB | +| `APS5_TRACE_CAPSYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | prints every unlocked hook wait | +| `APS5_TRACE_CONTEXT_STATE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp` | | logs context-state operations and the context registers written | +| `APS5_TRACE_COPY_HLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | traces the CPU copy HLE's refusals | +| `APS5_TRACE_DCC_KEYS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp` | | traces stores of uncompressed DCC keys | +| `APS5_TRACE_DIR` | string | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp` | . | directory for the trace and summary files | +| `APS5_TRACE_DISPATCH_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | reports dispatch-cache entries whose key changed | +| `APS5_TRACE_DISPATCH_IO` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | prints every dispatch's resources; `2` also lists the small-buffer words | +| `APS5_TRACE_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | prints one line per draw with its phases and inputs | +| `APS5_TRACE_DRAW_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp` | | reports draw-cache entries whose key changed | +| `APS5_TRACE_DRAW_PROGRAMS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | | prints one line per recorded draw with the stage, role and code address of each program | +| `APS5_TRACE_EVENTS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp` | | number of events a windowed trace keeps in its buffer | +| `APS5_TRACE_FILL_COVER` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | prints the first fills that meet images without covering them | +| `APS5_TRACE_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | names what forces pending storage-image results into guest memory | +| `APS5_TRACE_FRAMES` | string | `core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp` | | records `` presents from present `` (whole numbers, `start:count`) | +| `APS5_TRACE_GPU` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp` | | prints GPU wait, submission and scanout events, the execute lines, and waits longer than 200 ms (read in DisplayBuffer, PacketExecution, QueueWorker, Submission and WaitMemory) | +| `APS5_TRACE_HOST_IMPORT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | traces host-import mapping failures | +| `APS5_TRACE_INDIRECT_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp` | | prints indirect draw packets that reach the precheck | +| `APS5_TRACE_LATE_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | prints how late labels were trusted or refused | +| `APS5_TRACE_PREPARATION_WAIT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | prints where a demanded shader preparation sat in the queue | +| `APS5_TRACE_QUERY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | prints every 16th page query with its cost | +| `APS5_TRACE_RESOURCE_KEY_MISSES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | reports shader-resource cache key misses | +| `APS5_TRACE_SECONDS` | string | `core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp` | | records from the first present `` seconds after the first present for `` seconds (whole numbers, `start:length`) | +| `APS5_TRACE_SHADER_REGS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | prints a shader's registers for one hex program address, or `all` | +| `APS5_TRACE_SHADOW` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | | prints the first n shadow publishes, the first 64 seeds, every evict and retire | +| `APS5_TRACE_SKIPS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp` | | reports each distinct shader skip once | +| `APS5_TRACE_STAGING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | prints every distinct staged range once, with its size and whether an atomic element lies in it | +| `APS5_TRACE_SUMMARY_EVERY` | integer | `core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp` | | seconds between periodic trace summaries, emitted on the next present | +| `APS5_TRACE_TEXTURES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | prints a sampled texture's bytes and how many are nonzero | +| `APS5_TRACE_UNREADABLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | names the code that checked an inaccessible range | +| `APS5_TRACE_UPLOAD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | names why a storage image is uploaded again | +| `APS5_UNIT_SHADOW_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | 1024 | budget in MiB of the unit shadow's slabs | +| `APS5_UNIT_SHADOW_SLAB_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | 8 | size in MiB of one unit-shadow slab | +| `APS5_VALIDATE_LEGACY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | uses the legacy snapshot and full-compare path for pending-write validation | +| `APS5_VALIDATE_SYNC_SKIP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | reads skipped dwords through the hook too and counts a mismatch | +| `APS5_VERIFY_BDA_TABLES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | checks a reused BDA page table against the one just built | +| `APS5_VERIFY_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | recomputes a dispatch data hit and aborts on a disagreement | +| `APS5_VERIFY_DRAW_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | recomputes a draw data hit and aborts on a disagreement | +| `APS5_VERIFY_DRAW_ENTRIES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | re-decodes every draw-cache hit through the full path and checks it | +| `APS5_VERIFY_DRAW_RECIPE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | re-decodes a draw recipe instead of using the cache and checks the result | +| `APS5_VERIFY_DRAW_SNAPSHOTS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | re-reads a reused draw snapshot and counts the ones that differ | +| `APS5_VERIFY_KNOWN_VALUES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | serves known values through the hook too and counts mismatches | +| `APS5_VERIFY_PROOFS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | runs the full walk beside every own-object refresh proof and aborts on a different decision | +| `APS5_VERIFY_RECIPE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | makes the ordinary prepare path beside a recipe hit and compares the answers | +| `APS5_VERIFY_REGISTER_KEYS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp` | | checks the incremental register fingerprint against the full state | +| `APS5_VERIFY_SET_LAYOUTS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | checks the draw's set layouts against those described from the shaders | +| `APS5_WAIT_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | always takes the GPU mutex on a wait (no unlocked label lookup) | +| `APS5_WAIT_LOCK_ENTRY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | takes the GPU mutex blocking on the entry instead of trying it | +| `APS5_WATCH_MEMORY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | watches named guest ranges (`:[,...]`) across dispatches; needs `APS5_SYNC_DISPATCH=1` | +| `APS5_WORKER_AFFINITY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp` | | pins the queue workers and presenter to the performant cores (needed to enable it on non-Linux hosts) | +| `APS5_WORKER_AFFINITY_MASK` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp` | | explicit CPU affinity mask for the queue workers and presenter | +| `APS5_WORKING_SET_MIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | sets the process working-set minimum to this many MiB (Windows only) | +| `APS5_WRITE_EVIDENCE_AFTER` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | 4 | consecutive unchanged observations before a dword's written-element evidence is trusted | +| `APS5_WRITE_EVIDENCE_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | 16 | largest written range in KiB that the evidence may serve | +| `APS5_WRITE_EVIDENCE_SAMPLE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | 16 | reads one in this many pending writes to sample them | +| `APS5_WRITE_WATCH_IMPORTS` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | how host imports are write-tracked: `probe`, `watch` or `unwatch` | +| `APS5_WRITTEN_SHADOW_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 2048 | upper bound in KiB of the written staging window | +| `APS5_WRITTEN_SHADOW_MIN_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 16 | lower bound in KiB of the written staging window | + +### libSceAjm.native + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_TRACE_AJM` | presence | `core/libs/prx/libSceAjm.native/src/Ajm.cpp` | | logs every job the title submits and what the library writes back | + +### libSceAudioOut + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_TRACE_AUDIOOUT` | presence | `core/libs/prx/libSceAudioOut/src/AudioOut.cpp` | | logs each audio-out port opened with its type, sample count, frequency and format | +| `APS5_TRACE_AUDIOOUT2` | presence | `core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp` | | prints the title's AudioOut2 context and port parameters and stream activity | + +### libSceFiber + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_TRACE_FIBER` | presence | `core/libs/prx/libSceFiber/Export.cpp` | | logs fiber initialization, switches and returns to the thread | + +### libSceFont + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `ANYPS5_SYSTEM_FONTS` | string | `core/libs/prx/libSceFont/src/SystemFont.cpp` | | directory holding the system fonts, defaulting to anyps5-fonts beside the executable | + +### libSceNpEntitlementAccess + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `ANYPS5_ENTITLEMENTS` | string | `core/libs/prx/libSceNpEntitlementAccess/Export.cpp` | | path of the entitlements ini listing owned add-ons, defaulting to anyps5-entitlements.ini beside the executable | + +### libSceNpTrophy2 + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_TRACE_UDS` | presence | `core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp` | | prints the UDS events contexts post and the stat changes trophy rules apply, and the trophy files a lookup does not find (read in NpUniversalDataSystem/Event and NpTrophy2/TitleTrophies) | + +### libScePad + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_PAD_INPUT_FILE` | string | `core/libs/prx/libScePad/src/PadState.cpp` | | path of the file polled for injected pad button and stick commands | + +### libScePlayGo + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_PLAYGO_UNLISTED_CHUNKS` | string | `core/libs/prx/libScePlayGo/Export.cpp` | | answer for a chunk ID the playgo manifest does not list: installed or absent; unset throws instead of answering | + +### libSceSaveData.native + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_SAVEDATA_TRACE` | presence | `core/libs/prx/libSceSaveData.native/Export.cpp` | 3 | logs every save-data operation instead of the first three | + +### libSceSystemService + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `ANYPS5_LANGUAGE` | integer | `core/libs/prx/libSceSystemService/Export.cpp` | 1 | console language number returned by sceSystemServiceParamGetInt, defaulting to English US | + +### libSceVideoOut + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `ANYPS5_INPUT_CONFIG` | string | `core/libs/prx/libSceVideoOut/src/InputMapping.cpp` | | path of the input mapping file that binds host keys and mouse to guest pad actions | +| `ANYPS5_REQUIRE_DISPLAY` | presence | `core/libs/prx/libSceVideoOut/tests/Flip.cpp` | | makes video-out tests fail instead of skipping when no display or Vulkan device is available | +| `APS5_NO_PAD_INPUT` | presence | `core/libs/prx/libSceVideoOut/src/PadInput.cpp` | | ignores keyboard and mouse input so a measurement run is not advanced by a stray press | +| `APS5_VIDEOOUT_TELEMETRY_DIR` | string | `core/libs/prx/libSceVideoOut/src/Telemetry.cpp` | | directory where videoout.jsonl is appended with the pad and present telemetry lines; nothing is opened when unset or empty | + +### libSceVideodec2 + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_TRACE_VIDEODEC` | presence | `core/libs/prx/libSceVideodec2/Export.cpp` | | logs decoder memory queries, queue allocation and the first few decodes | + +### libc + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_CRASH_INDIRECT_MEMORY` | presence | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | dumps the words the faulting registers point at in the Windows crash report | +| `APS5_HANG_DUMP_SECS` | integer | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | seconds after which a watchdog dumps all threads and aborts | +| `APS5_HEAP_CACHE_MIB` | integer | `core/libs/prx/libc/src/GuestHeap.cpp` | 4096 | size in MiB of the cache of freed large blocks before the oldest are decommitted | +| `APS5_HELPER_AFFINITY_MASK` | integer | `core/libs/prx/libc/include/CpuTopology.hpp` | | hex core mask that helper threads (deferred release, mirror refresh) are pinned to | +| `APS5_LOG_TIMESTAMPS` | presence | `core/libs/prx/libc/include/general/LogMacros.hpp` | | prefixes each log line with the milliseconds since the first log | +| `APS5_NO_PROCMAP_QUERY` | presence | `core/libs/prx/libc/include/specifics/linux/HostMapping.hpp` | | skips the /proc/self/maps PROCMAP_QUERY ioctl and scans the maps file instead | +| `APS5_NO_SSE4A_EMULATION` | presence | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | disables emulation of AMD-only SSE4a instructions, making such a fault fatal | +| `APS5_NO_WRITE_WATCH` | presence | `core/libs/prx/libc/src/GuestArena.cpp` | | disables write watching so guest buffers are fully re-read on every use | +| `APS5_PIN_WAIT_MS` | integer | `core/libs/prx/libc/src/GuestAllocations.cpp` | 60000 | milliseconds to wait for a pinned allocation lease before failing | +| `APS5_TRACE_AFFINITY` | presence | `core/libs/prx/libc/include/CpuTopology.hpp` | | prints a line naming the role, tid and mask of every thread pinning | +| `APS5_TRACE_CALLS` | string | `core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp` | | comma-separated libraries (or all) whose guest calls are traced | +| `APS5_TRACE_EXIT` | presence | `core/libs/prx/libc/src/Process.cpp` | | prints a backtrace of the caller of exit, _Exit and the process-exit path | +| `APS5_TRACE_SSE4A` | presence | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | prints each emulated SSE4a instruction once per rip | +| `APS5_WATCH_ADDR` | integer | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | absolute hex address whose writes are reported after its page is committed | +| `APS5_WATCH_WRITE` | integer | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | guest ELF vaddr whose 8-byte writes are reported by single-stepping the protected page | + +### libkernel + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `ANYPS5_LOADEXEC_ARGV_SKIP` | string | `core/libs/prx/libkernel/System/src/Process.cpp` | | on Windows, set to `1` to drop the first process argument and clear the variable | +| `APS5_COARSE_TIMED_WAITS` | presence | `core/libs/prx/libkernel/Time/TimedWait.cpp` | | on Windows, rounds timed waits up to the scheduler tick | +| `APS5_JOB_AFFINITY` | presence | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | pins the title's job worker threads to the efficiency cores of a hybrid CPU | +| `APS5_JOB_AFFINITY_MASK` | integer | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | hex core mask that job worker threads are pinned to | +| `APS5_NO_DIRECT_MEMORY_SECTION_POOL` | presence | `core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp` | | on Windows, creates a section for every direct-memory allocation and closes it at release instead of reusing released sections that never held guest data | +| `APS5_NO_JOB_AFFINITY` | presence | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | disables pinning of job worker threads | +| `APS5_NO_SLEEP_WAKE` | presence | `core/libs/prx/libkernel/Time/SleepWake.cpp` | | keeps every sleep of 1 ms or less in full instead of ending it at a GPU batch, batch completion action or flip completion | +| `APS5_NO_TIMER_RESOLUTION` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | keeps the default 15.6 ms scheduler tick instead of raising it to 0.5 ms on Windows | +| `APS5_TIME_SCALE` | integer | `core/libs/prx/libkernel/Time/Time.cpp` | 1 | factor applied to the guest monotonic clocks and TSC; sleeps still take real time | +| `APS5_TRACE_APR` | presence | `core/libs/prx/libkernel/Apr/src/Apr.cpp` | | logs every APR file read with its path, offset, size and destination | +| `APS5_TRACE_MEMORY` | presence | `core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp` | | logs direct-memory map, remap, protect, unmap and physical allocation operations | +| `APS5_TRACE_SYNC` | presence | `core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp` | | logs event-flag create, set and wait operations | +| `APS5_TRACE_TIMERS` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | runs a timed-wait selftest at startup and prints mean, p50 and max latencies | +| `APS5_TRACE_USLEEP` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | reports every 2000 usleep calls the guest call sites that sleep most | +| `APS5_TRACE_WAITS` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | aggregates blocking waits per guest call site and prints the longest-waiting sites every 3 s | + diff --git a/docs/dev/GAME_TESTING.md b/docs/dev/GAME_TESTING.md new file mode 100644 index 0000000000..6e74085f0e --- /dev/null +++ b/docs/dev/GAME_TESTING.md @@ -0,0 +1,118 @@ +# Game testing + +Running a game is a big effort for the community: a legally dumped title, a suitable machine, hours of setup, often a PR build, and time spent watching it run. Many runs end early with a crash or a stall. Every run is still useful, so this page asks for as much information as possible from each one. A failed run with a complete log helps as much as a successful one. + +For user options see [USAGE](../user/USAGE.md); for internals see [ARCHITECTURE](ARCHITECTURE.md). + +## Before a run + +- Build `main` or the PR head in a clean worktree, including the `libs` target (it is not in the default build). If a new test breaks the build, `cmake --build build --target libs relinker` is enough to convert and run a title. +- `tools/check_main_sync.py --fetch` in the worktree reports whether the branch is in step with upstream `main`, so a result isn't taken on an old base by mistake. +- Convert the title from plain ELF files (`tools/unwrap_self.py` unwraps a dump's `eboot.bin` and `.prx`): + - Linux: `relinker --registry src/eboot.elf app.elf` + - Windows: `relinker --windows --registry src/eboot.elf app.exe`. Without `--windows` the output is an ELF file whatever its name, and Windows refuses to start it (exit code 216, "not compatible with the version of Windows"). +- A dump whose `app0` holds `ampr_assets.index` and `ampr_assets-*.pak` was repacked for [ampr_emu](https://github.com/drakmor/ampr_emu): the packed files were removed from `app0`, and only ampr_emu reads the packs. Restore them first with ampr_emu's `tools/ampr_pack.py unpack --index app0/ampr_assets.index --output ` and copy the files missing from `app0`. Without them a Unity title stops at start-up on the missing `Media/Resources/unity default resources`. +- Convert every module the title loads from its own folders (`sce_module/`, `Media/Modules/`, `Media/Plugins/`). Unity titles need `--module-dir Media/Modules --module-dir Media/Plugins`. Put each printed `Guest module:` file next to its original `.prx` in `app0/`, and keep the original there. +- `libs/` holds only AnyPS5's own libraries, from `build/core/libs/libs` (not `unpatched/`). A game module placed there fails to load (error 193 on Windows). A self-built `libs/` on Windows also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the toolchain's `mingw64/bin`, or every library fails with error 126 ([USAGE](../user/USAGE.md#input-and-conversion)). +- Run `tools/import_audit.py` on the registries. Exit code 1 lists imports that will fail or throw. +- Run one title at a time. Titles share the GPU and the controller file. Some titles start a second process (Peace Walker's `mgspw.mgspw.elf`) that can outlive a time cap on the first: check that every process of the previous run is gone. + +## Launch + +Linux: + +```sh +APS5_PROFILE_DRAW=1 APS5_PROFILE_GPU=1 APS5_LOG_TIMESTAMPS=1 \ +APS5_PAD_INPUT_FILE=/tmp/anyps5-pad \ +setarch x86_64 -R timeout 600 ./app.elf > run.log 2>&1; echo "exit=$?" +``` + +Windows PowerShell: + +```powershell +$env:APS5_PROFILE_DRAW = '1' +$env:APS5_PAD_INPUT_FILE = "$PWD\pad.txt" +cmd /c '.\app.exe > run.log 2>&1' +"exit=$LASTEXITCODE" +``` + +- On Windows PowerShell 5.1, `*>` writes the log as UTF-16 and wraps the first stderr line in an error record, so `grep` can't read it; let `cmd` do the redirection, as above. `$LASTEXITCODE` still carries the exit code. +- `APS5_PROFILE_DRAW=1` prints `[present]` lines every 10 s. Without it a working title looks frozen. +- `APS5_NO_PAD_INPUT=1` ignores keyboard and mouse, so a stray press doesn't move a measurement run. +- `setarch x86_64 -R` (Linux only) turns off address randomisation so addresses match between runs. +- On Windows, a `--windows-gui` build started without a console writes its output to `anyps5.log` beside the executable, replacing the previous run's file. +- On Windows the title needs a signed-in desktop. Started from an SSH session (session 0) it has no usable window. Start it inside the user's session instead, for example with a scheduled task that runs interactively. A newly created task does not start on battery power by default and fails silently; allow that in its settings. + +## Check the GPU and the caches + +- **GPU:** copy the GPU from the log's `Selected GPU name=` line. Without presentation support AnyPS5 can select a software renderer: under Xvfb (no DRI3) that is llvmpipe, without an error, and the title still runs. Set `ANYPS5_GPU` to part of the expected device name; the start then fails if that device can't be used. For headless Linux runs on a real GPU, Weston headless with the GL renderer plus Xwayland provides DRI3. +- **Caches:** first and later runs can differ a lot. On a slow GPU a first frame took minutes with empty caches and seconds on the next run. Two caches matter: + - AnyPS5's `shader_cache/` beside the executable, or `ANYPS5_SHADER_CACHE_DIR`. `ANYPS5_NO_SHADER_CACHE=1` disables it. + - The driver's own: Mesa (`MESA_SHADER_CACHE_DIR`, or `MESA_SHADER_CACHE_DISABLE=true`), AMD on Windows (`%LOCALAPPDATA%\AMD\VkCache`). Back a driver cache up instead of deleting it; it isn't AnyPS5's. + + Say in the report whether the caches were empty or warm. A first run that ends in device lost (`VK_ERROR_DEVICE_LOST`) or a GPU wait timeout while large pipelines are still being built, seen on AMD, often is the cache and not a regression: run again with the caches warm and report both runs. +- **Power and display:** on a laptop, run on AC power; a run on battery can make a neutral change look like a regression. Note the display's refresh rate: a title that asks for 120 Hz output can stop at start on a 60 Hz display. Keep the display awake for long or unattended runs: a title started while it sleeps stops with `SDL_GetWindowDisplayIndex failed: Couldn't find any displays`. +- **Memory budgets:** two budgets come from the machine's memory and can change the frame rate several times over, so check them before comparing results from different machines. + - Host imports (`APS5_HOST_IMPORT_MIB`, see [ENVIRONMENT](ENVIRONMENT.md)): the default follows the physical memory and the host heap the driver reports, so two machines with the same RAM can get different budgets. Refused imports print `[gpu] host import budget: X MiB live of Y MiB (...); N imports (M MiB) refused so far`, at most every 10 s; without such a line nothing was refused. A refused range is copied on every later use instead, and those copies can take device memory the texture caches need. + - Texture caches (`APS5_TEXTURE_CACHE_MIB`, see [USAGE](../user/USAGE.md#texture-cache-budget)): `[gpu] sampled texture cache budget N MiB` at start and when it changes, and with `APS5_PROFILE_DRAW=1` a `[texture-cache] (10 s)` line with the evictions and the budget. Thousands of evictions per 10 s with the budget at its 2048 MiB floor mean textures are rebuilt every frame. + + Report these lines. Set either variable only for a comparison, and say so. + +## What to read + +| Check | Where | Meaning | +|---|---|---| +| Exit code | shell | Linux: 124 time cap, 134 uncaught exception, 139 segfault, 137 killed. Windows: the exception code, `0xc0000005` access violation, `0xc0000409` uncaught exception or `abort()` (the log ends with `FATAL: abort() (SIGABRT) on thread N`) (PowerShell shows it signed, -1073740791) | +| Last lines | `run.log` or `anyps5.log` | `terminate called`, `what()`, `presentation failed`, `ASSERT`, `FATAL:` crash report | +| Frames | `[present] N presents over 10 s` | Steady N above 0 means frames are moving | +| Memory | `[texture] N storage images (X MiB)` | Growth before a `vkAllocateMemory ... -2` error | +| Queue waits | `[packets] ... WAIT_REG_MEM_64 ...` | A queue spending the window waiting on the GPU | +| Milestones | `Level has started: ` | How far the title got | +| Kernel (Linux) | `dmesg \| grep app.elf` | Fault address and instruction pointer | +| System log (Windows) | Event Viewer, System | A hard hang with nothing logged shows up afterwards as Kernel-Power 41 | + +## Stall, slowdown, or crash + +- **Stall:** no `[present]` lines for a long time while other periodic lines keep coming. Some titles present only when the view changes, so first send one input (`tools/pad_input.py`) and check whether presents resume. +- **Slowdown:** presents per 10 s far below earlier runs of the same scene. Record the scene (save file or level), since it changes the numbers, and check power and caches first. +- **Crash on Linux:** take the `ip` from `dmesg` and its offset in the image. Repeat the run under `gdb` (launched by gdb, not attached) for registers, the faulting instructions and all thread stacks. +- **Crash on Windows:** the `FATAL: unhandled exception ...` report in the log lists the faulting `rip` (for example `app.exe+0x...`), the registers and a stack scan. `APS5_CRASH_INDIRECT_MEMORY=1` adds up to 64 bytes from each pointer in the register dump. `APS5_HANG_DUMP_SECS=` writes a dump after a hang of n seconds. `APS5_TRACE_EXIT=1` reports the process exit point. +- **Traces (heavy):** `APS5_TRACE_FRAMES=:` or `APS5_TRACE_SECONDS=:` writes a capture for `tools/trace_digest.py`. Traces slow the title and can move the crash; say so when reporting. + +## Measuring performance + +- Measure in gameplay, not in cinematics, menus or loading screens. +- Use the title's default settings, and say so. A changed graphics mode makes a number incomparable with everyone else's. +- Measure the same spot every time: load a save, wait for the scene to settle, then measure a fixed window with no input. +- Compare builds on the same machine, alternating them (A, B, A, B), at least two runs each. Throw away a run that was disturbed (another program, a sleeping display) and say so. +- `APS5_TRACE_SUMMARY_EVERY=10` writes one line per 10 s to `aps5-summary.jsonl` without slowing the title; `tools/trace_digest.py` turns it into a table, and `tools/performance_compare.py` compares two captures ([BUILD](BUILD.md#performance-trace)). Pull requests labelled `perf` need a capture of `main` and one with the change. +- `APS5_DUMP_FRAMES=` with `APS5_DUMP_FRAMES_EVERY=` saves frames as `frame_.bmp` to check what was on screen. A second process of the same title writes to the same names and overwrites the first one's frames. While 64 MiB of frames wait to be written, the presenter waits for the + writer, so a slow disk or `APS5_DUMP_SCALE=1` slows the title down instead of stopping it. + +## Title notes + +- **PlayGo:** dumps without PlayGo metadata stop at start unless `APS5_PLAYGO_UNLISTED_CHUNKS` is set; report the value used. +- **UE4 titles:** keep the startup movies in place. Renaming them to skip the intro has made a level render without its 3D view, which looked like an emulator bug and wasn't. + +## Input + +`tools/pad_input.py` writes presses to `APS5_PAD_INPUT_FILE`; they reach the title without focusing its window. Keys sent to the window instead (for example with `SendInput` on Windows) only arrive while it has focus. Physical controllers are opened at start and when plugged in later; the log names them with `Pad: connected game controller`. + +## Saves + +Save data is kept under the runtime's `_sd/` folder, and save-data memory under `_sd_mem/`. Copy them into a new runtime to continue from the same point. + +## Reporting a result + +Include, in this order: + +1. The build: the `main` commit, each PR applied on top with its head, and any local change. A result from an integration branch should stay distinguishable after one of its PRs merges. +2. Title ID and content version, not only the game name. +3. Host: CPU, OS (and kernel on Linux), the GPU as the `Selected GPU name=` line names it, the driver version, AC or battery, and the display refresh rate. +4. The exact command and every `APS5_*` / `ANYPS5_*` variable, the in-game settings if they differ from the defaults, and whether the caches were empty or warm. +5. How far the title got (menu, a save, a scene) and what stopped it: the exit code and the last 200 log lines, plus the kernel line (Linux) or crash report (Windows) for a crash. +6. Presents per 10 s, and the storage-image and budget lines (`[gpu] host import budget`, `[gpu] sampled texture cache budget`, `[texture-cache]`) if memory is involved. +7. How many runs and how long each lasted. One run is an observation, not a reproduction. +8. The date of the test. Leave unknown fields empty rather than filling them from another report. + +Keep AI-generated analysis separate from measured data, and say which is which. diff --git a/docs/dev/HW_ORACLE.md b/docs/dev/HW_ORACLE.md index e48f86ed1a..c675934529 100644 --- a/docs/dev/HW_ORACLE.md +++ b/docs/dev/HW_ORACLE.md @@ -10,6 +10,8 @@ the tool, but differs from gfx1013 in more places. Say which GPU you measured on ## Requirements +### Linux + - Linux with the `amdgpu` driver and read/write access to `/dev/kfd` (usually the `render` group). Root is not needed. - The ROCm HSA runtime with its headers: `libhsa-runtime-dev` (Debian, Ubuntu), `rocm-runtime-devel` (Fedora), `hsa-rocr` (Arch), or AMD's ROCm packages. A runtime under `ROCM_PATH` or `/opt/rocm` is used first. The rest of ROCm @@ -20,6 +22,17 @@ the tool, but differs from gfx1013 in more places. Say which GPU you measured on The first run builds `oracle.c` into `~/.cache/anyps5-hw-oracle/` (`HW_ORACLE_CACHE` overrides it). The target is read from the GPU (`HW_ORACLE_TARGET` overrides it). +### Windows + +- AMD's Windows driver, which installs the HIP runtime (`amdhip64_6.dll`) and the code object manager (`amd_comgr_2.dll`) + in `System32`. ROCm, a C compiler, `clang` and `ld.lld` are not needed. +- Python 3 from python.org or the Microsoft Store. + +On Windows `oracle_hip.py` replaces `oracle.c`: comgr assembles and links the kernel, and HIP runs it. The CLI, the Python +interface and the results are the same as on Linux. `HW_ORACLE_RUNTIME` picks the runtime (`hip` on Windows, `hsa` +elsewhere). `HW_ORACLE_HIP` and `HW_ORACLE_COMGR` name other DLLs. `ROCR_VISIBLE_DEVICES` selects the GPU, through +`HIP_VISIBLE_DEVICES`, unless that variable is already set. + ## Kernel `template.s` wraps the body under test. Per lane: diff --git a/docs/dev/PROGRESS.md b/docs/dev/PROGRESS.md index 8b532eeb62..a1257f85a9 100644 --- a/docs/dev/PROGRESS.md +++ b/docs/dev/PROGRESS.md @@ -1,6 +1,6 @@ # Progress reporting -[`tools/progress.py`](../../tools/progress.py) generates `progress.json`, HTML, SVG maps and badges. The [Progress workflow](../../.github/workflows/progress.yml) publishes them after pushes to `main`. The [Progress report workflow](../../.github/workflows/progress-report.yml) compares the base and head trees of pull requests. +[`tools/progress.py`](../../tools/progress.py) generates `progress.json`, HTML, SVG maps and badges. The [Pages workflow](../../.github/workflows/pages.yml) publishes them with the web converter after pushes to `main` that change the converter, the web page or these tools. The [Pages progress workflow](../../.github/workflows/pages-progress.yml) runs after pushes that change `core/libs/prx` or the opcode list, compares the generated `progress.json` with the deployed one and dispatches the Pages workflow only when they differ, so the history chart gains new days only when the progress changes. The [Progress report workflow](../../.github/workflows/progress-report.yml) compares the base and head trees of pull requests. ## Library counts @@ -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). 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, so it shows how much is added over time: one panel for library functions and one for shader instructions, each on its own scale with the implemented count as a solid line and the declared total as a dashed one (for shaders the declared total is the instruction list in `tools/rdna_isa.txt`); the current percentage sits next to each panel's implemented label. It takes one point per UTC day, at the last commit of the day, and skips commits whose tree lacks the scanned paths. The chart compacts as days accumulate: up to 14 days it labels every day, up to 150 days one label per Monday, beyond that one per month; points are drawn up to 45 days, and every day keeps a hover tooltip, while the table and `history.json` keep every day. Old trees are measured with today's counting rules and instruction list, so the series is comparable but not what the page showed on that day. The workflow checks out the full history for it. ## Comparisons @@ -24,11 +24,17 @@ Real removals and transitions back to stubs remain visible. Percentages retain t Implemented definitions are also classified as behaviourally empty when their body holds only `(void)name;` discards and at most one constant return, bare `return;` or `throw`, including a return of a local `static` wrapper that is itself behaviourally empty; wrapper calls whose arguments contain calls do not qualify. Comparisons list newly implemented behaviourally empty functions without changing any count. Older JSON snapshots without `inert_names` are accepted. +## One library + +`--module NAME` limits the library totals to those directories under `core/libs/prx`. Repeat the flag to include several. A name that is not a directory is an error. Shader counts stay complete. A shared-source wrapper is included only when its own directory is named. `--json` prints the object written to `progress.json` on stdout, instead of the two summary lines. Either flag makes the output directory optional. Without them the directory is still required, and a run that names only the directory still writes the whole tree. + ## Verification ```sh python3 tools/tests/test_progress.py python3 tools/progress.py /tmp/anyps5-progress +python3 tools/progress.py --module libScePad +python3 tools/progress.py --json python3 tools/tests/test_progress_history.py ``` diff --git a/docs/dev/REVERSE_ENGINEERING.md b/docs/dev/REVERSE_ENGINEERING.md new file mode 100644 index 0000000000..bcbcd1ef3a --- /dev/null +++ b/docs/dev/REVERSE_ENGINEERING.md @@ -0,0 +1,93 @@ +# Reading titles and libraries + +Many exports have no source that gives their signature or behaviour. Two places still hold them: the code of a title +that calls the export (its call sites) and the code of the library itself. This page covers how to read both, what each +one settles, and how to cite it. + +## Sources, strongest first + +1. **A console measurement**: a homebrew payload that calls the function and records what it returns, with the + firmware version (for example `sceVoiceQoSEnd`, #1407). +2. **The library's own code**: a firmware module, or a copy of an SDK module that a title ships in `sce_module/`. It gives + behaviour, error codes, layouts and validation order ([libSceJobManager](debt/libSceJobManager.md), + [libSceRtc](debt/libSceRtc.md), [libSceHttp](debt/libSceHttp.md)). A module a title ships is not reimplemented + ([CONTRIBUTING](../../CONTRIBUTING.md)); it can still be read to learn how a system library behaves. +3. **Other implementations and SDK bindings**: KytyPS5, shadPS4, fpPS4, SharpProspero, Orbital. PS4 sources may differ + from the PS5 ABI. +4. **A title's call sites**: the arguments, their widths, buffer sizes, which pointers are outputs and which results the + title checks. They show what the caller expects, not what the library does. + +When sources disagree, say so in the pull request and in the debt entry. + +## What a call site settles + +The PS5 uses the System V AMD64 calling convention: integer and pointer arguments in `rdi`, `rsi`, `rdx`, `rcx`, `r8`, +`r9`, then the stack; floating-point arguments in `xmm0`-`xmm7`; the result in `rax` or `xmm0`. + +| Seen at the call site | Settles | +| --- | --- | +| Registers written right before the `call` | The argument count. A register set long before the call, or for another use, is not an argument; check several call sites | +| `mov %esi` / `mov %rsi`, `movzwl`, `movsbl` | The argument width and signedness | +| `lea -0x2030(%rbp), %rsi` | A pointer to a caller buffer; the stack layout gives its size | +| A value stored at the pointer before the call and read after it | An in/out argument (capacity in, size out) | +| `test %eax, %eax` / `cmp`, `js` after the call | An `int32_t` result and which values the title treats as failure | +| The result is never read | Nothing: the return type is not established ([libSceAgc](debt/libSceAgc.md)) | +| A log string that names the function (`"sceXxx() failed. Error code %#x"`) | Which import the call is, and that failures are SCE error codes | +| A first call with a null buffer, then a second one with a buffer of the returned size | A size query ([libSceNpTrophy2](debt/libSceNpTrophy2.md)) | + +A call site doesn't settle what the library writes, which error codes it returns, its side effects, or the values a +title happens to accept. "The title continues if it gets 0" is not "the console returns 0". Everything that the call +site and the other sources leave open throws (`NotImplemented_nid_no_patch`), as in +[libSceAudioIn](debt/libSceAudioIn.md): the signature came from a call site, the meaning of the result did not, so the +export still throws. A silent stub is allowed only when it affects the UI ([CONTRIBUTING](../../CONTRIBUTING.md)): a +read that always reports no data, or a write that drops its data, is not UI. + +## Finding the call sites + +1. Unwrap the title's decrypted `eboot.bin` with `tools/unwrap_self.py` + ([GAME_TESTING](GAME_TESTING.md)). +2. Relink it with `--registry` ([USAGE](../user/USAGE.md)). `app.registry.json` lists every import of the executable + with its `nid`, `relocationType`, `targetOffset` (the GOT slot) and `callSites` (virtual addresses). Bundled modules' + registries have no `callSites`. `tools/nid_names.py --name sceXxx` gives the NID of a name, and `--nid` the name of a + NID (without the `#` suffix). + - `R_X86_64_GLOB_DAT`: each call site is the `call *slot(%rip)` itself. + - `R_X86_64_JUMP_SLOT`: the call site is the PLT stub (`jmp *slot(%rip)`); the callers are the `call ` + instructions. +3. Disassemble around the site. The ELF has no section headers, so `objdump -d` prints nothing; take the executable + `LOAD` segment from `readelf -lW`, cut it out at its file offset and disassemble it at its virtual address: + + ```sh + readelf -lW eboot.elf # executable LOAD: offset 0x4000, vaddr 0x0, filesz 0x1f36afc + tail -c +$((0x4000 + 1)) eboot.elf | head -c $((0x1f36afc)) > text.bin + objdump -D -b binary -m i386:x86-64 --adjust-vma=0x0 text.bin > text.dis + grep -nE 'call +0x1f2e480$' text.dis # callers of the JUMP_SLOT stub at 0x1f2e480 + ``` + + A disassembler that loads the program headers (Ghidra, IDA) does the same. +4. Log strings near the call confirm which import it is: `strings -t x eboot.elf`, then the `lea` that loads that address. + +The same steps work on a module: the code of a firmware module or of one in `sce_module/` gives the library's side of +the contract. + +## Citing it + +In the pull request and in the library's [debt](debt/) entry, say: + +- Where it came from: the title ID and the call-site addresses (`PPSA03393, 0x76D132`), or the module and firmware + version (`libSceRtc.sprx, firmware 13.60`). +- What each site showed: "`rsi` points to a 0x2000-byte stack buffer and `rdx` to its capacity, which the title reads + back as the size". +- What was not verified: "read from disassembly, not measured on a console", "the return value is not read". + +Quote only the instructions that show the claim, not whole functions. The code stays generic: a title ID belongs in the +debt entry, never in code or test names. + +## Rules + +- Work only from dumps of titles and firmware that you own. Never add, attach or link game files, firmware, decrypted + binaries or keys in the repository, a pull request or an issue. +- Write the implementation from the contract you read (arguments, sizes, values, order, errors), not by translating the + disassembly instruction by instruction or copying its structure. Rebuild data tables from a public source when one + exists ([libSceIme](debt/libSceIme.md) rebuilds its codepage checks from Unicode's tables). +- The goal is interoperability: what a title needs from a system library to run. Nothing here is for bypassing + licence, entitlement or online checks. diff --git a/docs/dev/TRACING.md b/docs/dev/TRACING.md new file mode 100644 index 0000000000..8cb84bb4a4 --- /dev/null +++ b/docs/dev/TRACING.md @@ -0,0 +1,75 @@ +# 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 and Linux, records calls to the imports selected by this comma-separated library list (or `all`); crash and abort reports (abort only on Linux) include the last 64 calls and most-called imports. The executable must be converted with `--trace-imports`; see [Usage](../user/USAGE.md#tracing-system-library-calls). | +| `APS5_TRACE_EXIT` | Guest `exit` and `_Exit` calls and process-exit backtraces. | +| `APS5_TRACE_FIBER` | Fiber initialization, switches and returns. | +| `APS5_TRACE_MEMORY` | Direct-memory allocation, mapping and protection operations. | +| `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 queue, submission serial, shader and resources; value `2` also prints small user-data buffers. | +| `APS5_TRACE_DRAW_CACHE` | Draw-cache lookups, hits, misses, and key differences, reported every ten seconds. | +| `APS5_TRACE_DRAWS` | Draw inputs and per-draw timing phases. With `APS5_PROFILE_DRAW`, also prints the detailed draw lines. | +| `APS5_TRACE_DRAW_PROGRAMS` | One line per draw listing selected shader stages, roles, code addresses and entry addresses. | +| `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. | +| `APS5_VIDEOOUT_TELEMETRY_DIR=` | Writes the presents and the pad changes of a run to `/videoout.jsonl`; see [Usage](../user/USAGE.md#video-out-and-pad-telemetry). | + +## Shader registers + +`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 9bd067ad5c..ee2a2dfd75 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -1,10 +1,9 @@ # Project technical debt -Entries about one library or component are in [debt](debt/), one file per library or component, under the same sections as this page. Entries that belong to none stay here. +Entries about one library or component are in [debt](debt/), one file per library or component, under the same sections as this page. Entries that belong to none stay here. An entry may end with `Closes when:` and the measurement, title or test that would close it, such as `Closes when: measured on a PS5`, so the entries a contributor can re-check are found by searching for it. ### Build -- The bundled [SPIRV-Tools build](../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy that releases processed traversal maps. Remove this patch when the pinned revision includes [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). System-dependency builds need the fix in their installed SPIRV-Tools package. - Building on Windows requires a specific version of mingw - MinGW-w64 GCC 15.2.0 (`winlibs-gcc15`, `x86_64-ucrt-posix-seh`) - Even compiled prx libraries on Windows require nearby (static linking of these dependencies causes conflicts): - libgcc_s_seh-1.dll @@ -21,9 +20,12 @@ Throughout the project, every function at every stage either **does exactly what - `sceNetCtlGetIfStat` signature and 56-byte layout come from PPSA09030's call site and SharpProspero's `SceNetCtlIfStat`; the device kind is always 0, the byte counters are the host's totals over its up non-loopback hardware interfaces (not "since cleared"), and the not-connected result is assumed to match `sceNetCtlGetState` - Font, Http2, Net, Ssl, SystemService and libkernel exports imported by PPSA12544 and added without an implementation have unknown signatures - The Share, NpTrophy2, NpEntitlementAccess and WebBrowserDialog exports imported by PPSA12544's Unity plugins and added without an implementation have unknown signatures -- Acm, GameLiveStreaming, Net, NpEntitlementAccess, NpManager, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters +- Acm, GameLiveStreaming, Net, NpEntitlementAccess, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters; of the VoiceQoS ones only those still throwing remain so ### Functional +- Controller haptic PCM that no controller sound card plays (a DualSense over Bluetooth, or any other controller) drives motor rumble from its per-channel RMS (large/small motor, scale 2x, below 0.01 ignored, 80 ms hold); the mapping approximates waveform energy and is not measured against console hardware - [Cancellation coverage](ARCHITECTURE.md#thread-cancellation) checks deferred request handling and recovery, not full asynchronous support or console ABI compatibility. Cancellation during file/socket I/O, sleep interruption, cancellation versus exit/detach, condition signal conservation under cancellation, and safe rejection of non-null invalid or reclaimed thread handles remain unverified. No console title or homebrew establishes the cleanup NIDs or buffer layout. Accepted asynchronous mode currently has no diagnostic for its deferred behavior. - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. + +- [Quick syntax checks](../../tools/check_compile.py) reject unexpanded `@response` arguments rather than parse compiler-specific quoting and risk retaining output flags. Expand response files in `compile_commands.json` before checking; the Ninja fallback expands them with `ninja -t compdb -x`. diff --git a/docs/dev/debt/libSceAc3Enc.md b/docs/dev/debt/libSceAc3Enc.md new file mode 100644 index 0000000000..0ce6dc4f58 --- /dev/null +++ b/docs/dev/debt/libSceAc3Enc.md @@ -0,0 +1,5 @@ +# libSceAc3Enc technical debt + +### Unknown function info + +- [sceAc3EncCreateEncoder, sceAc3EncDeleteEncoder, sceAc3EncEncode, sceAc3EncEncodeAsync, sceAc3EncFlush, sceAc3EncIsEncoding and sceAc3EncWaitEncode](../../../core/libs/prx/libSceAc3Enc/Export.cpp) (libSceAc3Enc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAc3Enc.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceAcm.md b/docs/dev/debt/libSceAcm.md index c5c9b90f30..c94c8ce717 100644 --- a/docs/dev/debt/libSceAcm.md +++ b/docs/dev/debt/libSceAcm.md @@ -2,9 +2,13 @@ ### Silent stubs -- [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) batches execute as silent no-ops, so `sceAcm_ConvReverb_SharedInput`, `sceAcm_FFT` and `sceAcm_Panner` return success without encoding audio commands. +- [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) batches execute as silent no-ops, and do not execute encoded panner and FFT commands. `sceAcm_ConvReverb_SharedInput` returns success without encoding audio commands. ### Unknown function info -- PPSA02530 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceAcmBatchJobPriority](../../../core/libs/prx/libSceAcm/Export.cpp) (libSceAcm) and [scePadVrControllerGetTriggerEffectStates](../../../core/libs/prx/libScePad/Export.cpp) (libScePad). +- [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) context creation, batch submission and waiting use host identifiers instead of the native device and ioctl requests. Batch identifiers, timeout handling, DSP scheduling and service-side errors remain unimplemented. Panner source parameter and matrix payloads are forwarded by the native library; their DSP interpretation remains unverified. +- [sceAcm_ConvReverb_SharedIr, sceAcm_IFFT](../../../core/libs/prx/libSceAcm/Export.cpp) (libSceAcm) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAcm.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim +- [`sceAcmBatchJobNotification`](../../../core/libs/prx/libSceAcm/Export.cpp) is a parameterless pending declaration for import resolution. Its signature and behavior are unverified; it throws through `NotImplemented_nid_no_patch`. + +- The shared-input reverb signature and descriptor fields are recovered from native accesses. Input history indices are mutable; block metadata representation, reverb command encoding and DSP interpretation remain unimplemented in [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp). diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 2a41360807..e83a029f5e 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -2,39 +2,44 @@ ### Unknown function info -- PPSA01341 imports declared without parameters, signatures unknown: [sceAgcSetSemaphoreMemory](../../../core/libs/prx/libSceAgc/Unimplemented.cpp). Name from the shadPS4 aerolib NID list -- [sceAgcWaitRegMemPatchMask](../../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - follows `sceAgcWaitRegMemPatchReference`: the mask is taken as a 32-bit value and written to the low mask word of a 32- or 64-bit wait, as [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) does; [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.PacketPatching.cs) writes both words of a 64-bit wait's mask. A mask above 32 bits throws + - [sceAgcDcbSetCfRegisterDirect, sceAgcDcbSetCfRegisterRangeDirect](../../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - signatures follow [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Agc/SceAgc.cs), which agree; the SET_CONFIG_REG (0x68) packet is OpenAGC's. The driver does not execute SET_CONFIG_REG, so a submitted packet throws - [Command buffer cursors](../../../core/libs/prx/libSceAgc/Command/src/Packet.cpp) (libSceAgc) - a null `cursor_down` is taken as a buffer without downward allocations, whose limit is `top`. PPSA16064 builds one-packet buffers of 8 dwords that way; the console's handling is unconfirmed -- [sceAgcGetRegisterDefaultsInternal](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table, as `sceAgcGetRegisterDefaults` returns the version 0 public one; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it -- [sceAgcInit](../../../core/libs/prx/libSceAgc/DcbState/src/Init.cpp) (libSceAgc) - unsupported register-default versions return `0x80890001` following [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c); AnyPS5 keeps version 13 support because its register-default tables include it, while that reference accepts only versions 0–12. The upper version accepted by PS5 firmware is unverified +- [sceAgcGetRegisterDefaultsInternal](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table while `sceAgcGetRegisterDefaults` now returns the version 13 public one; which internal table a console returns is not known. [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it +- [sceAgcGetRegisterDefaults](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 13 public table (137 keyed entries over 84 context blocks) because the console returns the running system software's table: [ps5-vulkan](https://github.com/mpereiraesaa/ps5-vulkan/blob/4bb0c8b1423698a7369efc22b829bf16a99e7128/src/color_runtime_defaults.h) measured 128 entries over 79 blocks on 10.x and 137 over 84 on 12.02 and 13.x, the shapes of the version 10 and version 11/13 tables here, and PPSA99011 refuses the version 0 table. The [type table](../../../core/libs/prx/libSceAgc/Command/src/RegisterDefaults.cpp) is handed out as 8-byte `{key, index << 2 | bank}` pairs, the layout ps5-vulkan and the homebrew sharing its `ps5_agc_registers.h` read; the compact 12-byte entries also pack the block's register count and stride above bit 10, and those bits are dropped. No console dump of the runtime type table was compared, and AnyPS5 does not choose 10.x or 12.02 by the system software a title targets +- [sceAgcInit](../../../core/libs/prx/libSceAgc/DcbState/src/Init.cpp) (libSceAgc) - versions 0–13 select their register-default tables and return 0; pointer-shaped or otherwise out-of-range values (>=14) return 0 without a register-default lookup. Console probe on firmware 13.60 ([#4808](https://github.com/boykopovar/AnyPS5/issues/4808#issuecomment-6149448503)): `sceAgcInit(&buf, 8)` (NID `kW3GLb7QfPg`) returns 0 and writes nothing; scalar versions 2–13 return `0x8A6C0004` there, while AnyPS5 still returns 0 for those scalars. The probe does not show that table 13 is selected for pointer-shaped calls. [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns `0x80890001` above 12 for `sceAgcInit`, and falls back to version 11 in `sceAgcGetRegisterDefaults2`. PPSA99011, PPSA99995, [ps5-vulkan](https://github.com/mpereiraesaa/ps5-vulkan) and [prospero-win](https://github.com/mpereiraesaa/prospero-win) call that NID with a pointer-shaped first argument. `sceAgcInit_0090` (NID `23LRUSvYu1M`) checks the state pointer and then calls `sceAgcInit`, as OpenAGC does - [sceAgcSetSubmitMode](../../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [sceAgcGetGsOversubscription](../../../core/libs/prx/libSceAgc/Shader/src/GsOversubscription.cpp) (libSceAgc) - signature and occupancy formula (base and expanded vertex/export capacities, budget shift, `GE_PC_ALLOC` / `SPI_SHADER_PGM_RSRC4_GS` encoding) follow KytyPS5 only, not confirmed on hardware or a title; a negative or NaN factor that gives a negative target, a missing GS context register or a zero `GE_MAX_OUTPUT_PER_SUBGROUP` throws - [sceAgcGetGsPrimPayload](../../../core/libs/prx/libSceAgc/Shader/src/GsPrimPayload.cpp) (libSceAgc) - the search does not stop at an `SPI_SHADER_IDX_FORMAT` (0x1C2) entry whose format is not `SPI_SHADER_2COMP`, as the analysis of the library declared in the pull request describes (its origin and firmware version are not identified), while [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1285-L1310) stops there; not verified on PS5 hardware. A null payload, a null shader or a register count with a null register array throws instead of faulting on the memory access - [sceAgcCbCondWrite](../../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) (libSceAgc) - argument order and the 9-dword COND_WRITE layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.CommandPackets.cs), which agree on them; SharpProspero lists another order. The write space is accepted only as 1 (memory), the one value both encode the same way, and other values throw. - [sceAgcAcbAtomicGds_0900](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - signature, control/mode bit mapping and the 11-dword ATOMIC_GDS (0x1d) layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, which derives them from the library's disassembly; KytyPS5 and shadPS4 have no body for it. Not confirmed on hardware or a title (PPSA07286 imports it, nothing was run), and the driver has no ATOMIC_GDS handler (opcode 0x1d is not executed as an atomic GDS operation) - [sceAgcGetIsTrinityMode](../../../core/libs/prx/libSceAgc/Misc/src/Platform.cpp) (libSceAgc) - signature from the only PPSA26344 call (a pointer to a one-byte flag, result ignored); a null pointer throws because its error code is unknown -- [sceAgcDcbAtomicGds_0900](../../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - name from its NID (`zARR5aCmkoY`), the DCB counterpart of `sceAgcAcbAtomicGds_0900`, signature unknown +- [sceAgcDcbAtomicGds_0900](../../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - and `sceAgcDcbAtomicGds` (forwards to it) - signature, control/mode bit mapping and the 11-dword ATOMIC_GDS (0x1d) layout are those of `sceAgcAcbAtomicGds_0900`, which follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only; KytyPS5 and shadPS4 have no body for it. Not confirmed on hardware or a title, and the driver has no ATOMIC_GDS handler - [sceAgcDcbContextStateOpGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - sizes from KytyPS5 only (5, 27, 27 and 32 dwords), equal to what `sceAgcDcbContextStateOp_0100` writes; KytyPS5 returns 0 for an operation above 3, here it throws +- [sceAgcDcbContextStateOp](../../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - delegates to `sceAgcDcbContextStateOp_0100` (same packets as KytyPS5's `AgcDcbContextStateOp`); KytyPS5 returns nullptr for an operation above 3, here it throws; the packet words were not compared with a real console - [sceAgcBranchPatchSetThenTarget_0300, sceAgcBranchPatchSetElseTarget_0300](../../../core/libs/prx/libSceAgc/Patch/src/Branch.cpp) (libSceAgc) - names from their NIDs (`7Wa3aeJgeVU`, `rP5xLdOf26k`); the `(packet, cachePolicy, target, sizeInDwords)` signature is inferred from PPSA28416's calls (a branch packet, 0, an aligned target, 4), with the policy stored where `sceAgcCbBranch` puts it - [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. - [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 +- [sceAgcGetDataPacketPayloadAddress](../../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - forward to `sceAgcGetDataPacketPayloadAddress_0090` and the inferred (addr, cmd, type) parameters follow SharpProspero and decrypted firmware 3.20 NID mapping; not verified on console hardware +- [sceAgcCbMemSemaphore](../../../core/libs/prx/libSceAgc/Cb/src/Sync.cpp) (libSceAgc) - signature (cb, semaphoreAddr, operation, modifier, value) follows SharpProspero; writes 4-dword PM4 0x39 packet with control word 0x80100000 following console oracle measurements in prosper (cases agcbuild_CbMemSemaphore_a/b/c); operation, modifier and value are unused - [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 - [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` - [sceAgcAcbWaitUntilSafeForRendering](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - `(buffer, videoOutHandle, displayBufferIndex)`, the declared arguments, which PPSA12544 (01.000.010) passes inside `waitUntilSafeForRendering` and `waitUntilSafeForRendering VR` markers on an asynchronous compute command buffer, ignoring the result. It writes the same rendering-wait packet as `sceAgcDcbWaitUntilSafeForRendering`, which the driver now accepts on compute queues too; the wait blocks only that queue's worker. Whether the console encodes the compute variant differently is not known +- [sceAgcGetDefaultCxStateFlat](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - copies up to `size` bytes (clamped to 2048 bytes, the 512-dword `AgcContextState`) from the default context state initialized from the version 8 public register default groups (CX, SH, UC), indexed by register offset, following OpenAGC `src/context_state.c`; returns `0x80890001` on null destination; version selection without `sceAgcInit` is unverified +- [sceAgcDcbSetIndexIndirectArgs, sceAgcDcbSetIndexIndirectArgsGetSize](../../../core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp) (libSceAgc) - the `(buffer, attributeAddress, attributeIndex)` signature, a 64-bit address and a 32-bit index, is [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1742-L1757)'s. The console oracle in prosper measured the packet, an INDEX_ATTRIBUTES_INDIRECT (opcode 0x91) holding the low address dword, the high address dword and the index ([measured](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L172-L174)), and a size of 16 bytes ([measured](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/agc.golden.tsv#L67)). The console clears the low four address bits and keeps the low 16 bits of the index; here a nonzero address that is not 16-byte aligned, or an index above 0xffff, throws, as `sceAgcDcbSetIndexBuffer` throws for a misaligned address. A null address is written as 0, as the console writes it. No title is known to call them. + +- [sceAgcSuspendPointAndCheckStatus](../../../core/libs/prx/libSceAgc/Export.cpp) - this named callable export is listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAgc.c) and was cross-checked against the same module in PS5 firmware 13.60. Its signature and behaviour are unverified; the declaration uses the no-parameter unknown-signature convention and throws through `NotImplemented_nid_no_patch`. ### Functional - [sceAgcDebugRaiseException](../../../core/libs/prx/libSceAgc/Misc/src/Debug.cpp) (libSceAgc) - logs the exception id and returns 0, as retail hardware does nothing per [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c); the trap (`ud2`) OpenAGC describes for development hardware is not reproduced. Neither is verified on a console. The `uint32` exception id is [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/6f0cddd4404ec5203e71fea77e75fc373299ba3f/src/libs/agc.cpp)'s claim; OpenAGC declares no arguments, so the logged id may be leftover register contents -- [sceAgcSetAmmSemaphoreMemory, sceAgcGetSemaphoreLabel](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) (libSceAgc) - the `(memory, sizeInBytes)` and `(index, label)` signatures, the 16 KiB alignment of the memory and its size, the single registration, the 32-byte labels and the zeroed first 8 bytes of each are what [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L508-L546) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Agc/AgcExports.AmmSemaphore.cs) agree on; KytyPS5 has neither function. OpenAGC clears the whole memory and sharpemu only those 8 bytes, so a registration whose labels hold data past them throws. The two return different errors for every rejected call (OpenAGC `0x8a6c0002`, `0x8a6c000b`, `0x8a6c0048` and `0x8a6c0049`, sharpemu generic invalid-argument and already-exists errors), so every rejected call throws, including a lookup before registration. Not measured on the console; PPSA02801 imports both (OpenAGC's import analysis), no title is shown calling them. +- [sceAgcSetAmmSemaphoreMemory](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) - native registration first invokes a kernel ioctl and driver registration; host registration does not reproduce their memory-access checks or error propagation. Null, empty and address-wrapping ranges throw. The full range is cleared after successful registration; duplicate registration, alignment and label-range errors match the native wrapper. - [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the driver call records only the stream id (1 to 31; it returns 0 for any argument and keeps no stream record), and the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. The error codes the console driver returns for an invalid or repeated registration are unknown, so registration never fails here. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. - [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included), and throw for a registered stream. The console oracle in prosper measured only that null return, with no stream registered, three argument patterns each and nothing written ([ACB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L48-L56), [DCB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L196-L204)); the packet a registered stream gets is not measured. KytyPS5 has no body for these four, and its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/agc_workload_packet.c) derives from the driver, so neither is taken. The `(buf, streamId)`, `(buf, streamId, workloadId)` and `(buf, streamId, workloadIds, workloadCount)` signatures are those of the DCB calls in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384), which says the ACB calls use the same driver builders; no ACB declaration was found. `sceAgcDcbSetWorkloadsActive` checks the stream before it reads the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 ([measured](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L203)) without faulting - [sceAgcCreatePrimState](../../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 111c9e39ff..d795f5d53f 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -1,125 +1,241 @@ # 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 +- [sceAgcDriverSetResourceRegistrationInfo](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - returns 0 without storing the info, as the retail `libSceAgcDriver.sprx` body does; registration is unavailable, so nothing reads it back. + ### Unknown function info -- [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. +- [sceAgcDriverSubmitCommandBuffer, sceAgcDriverSubmitMultiCommandBuffers](../../../core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp) (libSceAgcDriver) - `queue_context` is treated as a pointer to context whose queue identifier is stored 4 bytes in, matching KytyPS5 (`src/libs/agc.cpp` `AgcDriverSubmitCommandBuffer` / `get_driver_queue`). A null context returns `EINVAL` (22). Whether the console validates other context fields before queue dispatch is unverified + +- [sceAgcDriverSubmitAcb, sceAgcDriverSubmitMultiAcbs](../../../core/libs/prx/libSceAgcDriver/Submit/src/Acb.cpp) (libSceAgcDriver) accept compute queues 0x20 to 0x57 and throw for any other. On a PS5 (firmware 13.40, measured 2026-10-19 from inside a retail title) `sceAgcDriverSubmitAcb` returned 0 and ran a label-only buffer on queues 0x20, 0x30, 0x40, 0x50, 0x57 and also 0x58, which throws here; queue 0x1F faulted inside the console's driver (SIGSEGV in libkernel on the calling thread), which throws here too. Queues above 0x58 and `sceAgcDriverSubmitMultiAcbs` were not measured. Closes when: the console's highest accepted compute queue is measured + +- [sceAgcDriverAcquireHDRScopesACQ, sceAgcDriverHp3dLockEnd, sceAgcDriverHp3dLockStart, sceAgcDriverIsEsvmProcess, sceAgcDriverNopDataWritePacket, sceAgcDriverReleaseHDRScopesACQ, sceAgcDriverSetValueToSharedGds, sceAgcDriverSetWorkloadStreamInactive, sceAgcDriverSubmitReprojectionAcb, sceAgcDriverSuspendPointSubmitCdbg, sceAgcDriverSysGetGfxAppGpuResetStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - signatures and behavior are unverified; these parameterless declarations only resolve imports and throw through `NotImplemented_nid_no_patch`. +- [sceAgcDriverGetCoredumpAddress](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) returns the driver's shared coredump-area pointer, which is null when no coredump area is allocated. AnyPS5 reports coredump mode disabled and does not allocate this area; the export returns null. Enabling and populating an AGC coredump area is not implemented. +- [sceAgcDriverSetTargetRingForDiag](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) returns `0x8A6D0001` (`DRIVER_FEATURE_UNAVAILABLE`), matching the retail PS5 export. The retail diagnostic log message is not reproduced. + +- [Initial point context](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) - new queues and CLEAR_STATE seed PA_SU_POINT_SIZE 0x00080008 and PA_SU_POINT_MINMAX 0xffff0000, matching every public context table in libSceAgc/Command/include/RegisterDefaultsData.hpp. Cocoon (PPSA08766) reaches geometry point output without writing these registers in the observed startup. Applying the SDK context defaults to initial/CLEAR_STATE driver context is inferred; GPU power-on reset state is not measured and is not the source of these values. Explicit writes and pushed/restored contexts retain their own values. + + +- [Multisampled R_X and Z_X layouts](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTextureLayout.cpp) implement single-mip 2D arrays with 2, 4 or 8 equal samples and fragments, using the non-RB+ 16-pipe GFX10 equations from [AMD Addrlib](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10SwizzlePattern.h). R_X accepts 1/2/4/8/16-byte elements and Z_X accepts 1/2/4-byte elements. Tile and detile preserve element bytes and leave padding untouched; they do not synchronize resident images or publish compression metadata. Other modes, sample counts, element sizes and pipe/bank XOR bits outside the supported configuration throw. PS5 address-layout equivalence has not been measured. +- [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. Patches and absent routing or rasterization registers still throw; point lists on that routing use it (next entry). 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. +- [Vertex-path point lists](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - throws for a register size of 0, a clamp maximum of 0 with `USE_VTX_POINT_SIZE`, unequal register width and height, a clamp minimum above its maximum, `USE_VTX_POINT_SIZE` without `VS_OUT_MISC_VEC_ENA`, and point sprite coordinates; sizes above one pixel need `largePoints`. Inferred, not measured: that `PA_SU_POINT_MINMAX` clamps the exported size and not the register size ([radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state.c#L1047-1064)), and that an exported size of 0 or less covers nothing (here: the smallest point). A point list with `GS_EN` and `HS_EN` clear draws as Vulkan points, and the vertex program [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `PointSize`: the misc export's X, clamped, under `USE_VTX_POINT_SIZE`, else the `PA_SU_POINT_SIZE` height (12.4 fixed-point half sizes). +- [RELEASE_MEM of the system counter](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - a `RELEASE_MEM` with `DATA_SEL = 4` throws: the frequency and epoch of the console's 64-bit system counter are not known. [amdgpu's GFX10 packet header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/nvd.h) names the value "send 64bit sys counter value", beside 3, the GPU counter, which the driver writes as a 100 MHz count of the host's steady clock. An older KFD header names 4 a CP performance counter instead. A PS4 title through #6977 writes it at the end of each frame. +- [Point and line polygon modes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - front and back polygon types that differ while both faces are drawn throw, as Vulkan has one `polygonMode` per draw. Points are drawn only one pixel wide (`PA_SU_POINT_SIZE = 0x00080008` inside `PA_SU_POINT_MINMAX`, the `VK_KHR_maintenance5` size of a point whose shader does not write `PointSize`); other sizes, a per-vertex size and devices without `maintenance5` throw. Lines must be one pixel wide (`PA_SU_LINE_CNTL`) and use Vulkan's default line rasterization. Depth bias with a point or line polygon mode, and such a mode on a rectangle list, throw. The fields follow the [AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf); the console's rasterization of these points and lines is not measured. +- [Native multisampled depth clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accept the recognized HTILE mask kernel's direct full-range dispatch after its GPU writes complete. Every metadata byte needs tracked driver-store coverage since notification, no intervening CPU or depth write, and HTILE words proving the current clear value of zero or one. Partial, overlapping, indirect and unproven producers remain unsupported. Existing sampled views retain the native image; guest depth publication and metadata-fill transitions remain unimplemented. Windows shared imports retain write tracking through separate aliases; private host imports that lose tracking and copied or mirrored HTILE uploads remain unsupported. Clear-word encoding and console equivalence remain unmeasured on PS5 hardware. +- [Native depth allocation retirement](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) covers sampling-eligible multisampled owners only. Successful release notifications must cumulatively cover both complete depth and HTILE ranges; protection changes, failed releases and partial coverage keep the original guards. Retired owners leave lookup and metadata routing, while held views and recorded work retain their images. Retirement of single-sample or sampling-ineligible depth attachments and console equivalence remain unimplemented or unmeasured. The regression uses real allocation-registry operations with a no-op host-release callback, so it does not validate the host unmap backend. +- [Native MSAA depth attachment feedback](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) is not implemented. A draw sampling a native multisampled depth view while attaching its source image throws before pipeline creation or render-pass recording, including read-only attachment feedback. Sampling that image in a separate draw or dispatch remains supported. The refusal compares the held native image views and their device and attachment identity; single-sample depth behavior is retained. +- [Native multisampled depth sampling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accepts numeric D16 or D32, two/four/eight samples, one Z_X slice and mip, and pipe-aligned HTILE without stencil or VRS, with ITERATE_256 on or off: the initial import reads no compressed Z planes, and Mesa's HTILE clear words (`radv_get_htile_fast_clear_value`) and Addrlib's HTILE footprint do not depend on ITERATE_256, which `ac_descriptors.c` ties to TC reads of compressed MSAA HTILE. Sampling eligibility is recorded before the attachment's first host clear: every HTILE word must prove the same initial clear of zero or one, and the complete sample-aware depth and HTILE ranges must be accessible and write-watched. Unsupported initial state or device sampling support retains attachment rendering but sampling throws; eligibility is never retried. Sampling rejects later guest writes, queued external GPU writes, and unproven HTILE clear/fill transitions. A guest read over the depth or HTILE range of a surface the host has not written since import (draws with depth or stencil writes, depth/stencil copies, storage write-back; not clears after a proven HTILE clear) retires it instead of throwing; that a draw without those writes leaves the console's Z planes and HTILE unchanged is inferred, not measured. Guest publication of a written surface, arbitrary guest depth import, stencil/raw-bit sampling, arrays and storage access remain unimplemented. The block footprint follows main's AMD Addrlib thin-block rule; the accepted HTILE clear words and PS5 equivalence are not measured on console hardware. PPSA99902's historical resident eight-sample D32 reader is documented in [#419](https://github.com/boykopovar/AnyPS5/pull/419#issuecomment-6105597729). - [sceAgcDriverSetSubmitValidationMode, sceAgcDriverGetSubmitValidationMode, sceAgcDriverSetSubmitValidationConfig, sceAgcDriverGetSubmitValidationConfig, sceAgcDriverSetValidationErrorOutputFrequency, sceAgcDriverGetShaderDebuggingStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - the five validation calls return `0x8A6C1000` without reading their arguments or writing caller memory, and `sceAgcDriverGetShaderDebuggingStatus` returns 1, as [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L250-L291) reads them from Sony's libSceAgcDriver.sprx: each validation export is `mov eax, 0x8a6c1000; ret` in the copy Dragon Quest VII Reimagined bundles (0x7bb0-0x7bf0), and the debugging status is 1 in the firmware 5.50 library (0x6740). The parameters (a 32-bit mode or frequency, a mode output, a configuration pointer) follow OpenAGC and are never read. Whether later retail firmware enables submit validation or shader debugging is unverified; no title in this repository is known to call them +- [sceAgcDriver retail constant results](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - the debug, capture, GPU printf, register access, GPU reset, suspend point and user data exports taken from the retail `libSceAgcDriver.sprx`, and `sceAgcDriverRegisterOwnerForSystem`, `sceAgcDriverInitResourceRegistrationInterface` and `sceAgcDriverSetResourceRegistrationInfo` in [Registration.cpp](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp), are declared without parameters: the console bodies return a constant without reading any argument, so their signatures are unknown. `sceAgcDriverAllocateToolMemoryForGpuReset` and `sceAgcDriverGetAllocatedToolMemoryForGpuReset` read only the output pointer (fourth and second argument, a 12-byte range the console zeroes); the other arguments are declared as `uint64_t`. - [Indices far past every vertex fetch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice - [Depth clear draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a draw with `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` is taken to write `DB_DEPTH_CLEAR` to every pixel it covers whatever the depth test, depth bounds and depth bias say: it is drawn with an always-passing depth write and the viewport depth range pinned to the clear value. The draw is rasterized as submitted, so its coverage is the draw's own. Whether the DB still applies the depth test or HiZ to such a draw is not measured - [GS_FAST_LAUNCH geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_SHADER_STAGES_EN.GS_FAST_LAUNCH = 1` with a point list and `GE_CNTL` groups of one vertex and one primitive decodes as one point per subgroup, with the primitive limit taken from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR` and the GS/ES VGPR counts not required. Follows how RADV drives mesh shaders on gfx10.3 (`ac_nir_lower_ngg.c`, the workgroup index comes from the vertex id of the first thread), not measured on the console. Fast launch 2, other group sizes or input primitives and indexed fast-launch draws still throw. PPSA03671's fast-launch draws decode, but the title stops earlier in a wave64 vertex program, so they have not reached the GPU yet. +- [Point geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 0` emits mesh points with `OutputPoints` and scalar `PrimitivePointIndicesEXT` as required by [SPV_EXT_mesh_shader](https://github.khronos.org/SPIRV-Registry/extensions/EXT/SPV_EXT_mesh_shader.html). Each packed primitive export contributes its first 9-bit vertex index and its null bit, using the same NGG primitive packing as line and triangle outputs. The subgroup reserves at least `GE_MAX_OUTPUT_PER_SUBGROUP` point slots, as the line path does; the console budget is not measured. Points use the register size or the misc-X point-size export and clamp interpreted by the vertex-point path; those register, clamp and rasterization inferences remain unmeasured on the console. Point sprites remain unsupported. Point output from a vertex workgroup still throws. Cocoon (PPSA08766) reaches this path with triangle-strip input. +- [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; whether the console does the same is unmeasured. A fast-launch subgroup takes its line count from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR`. Line output counts as lines for polygon offset decode. The lines are drawn 1 pixel wide with Vulkan default rasterization; `PA_SU_LINE_CNTL` is not read, and console line rules are not measured. - [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw. 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 one-sample target under a multisampled rasterizer 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, one-sample color targets and no depth target, or a one-sample depth target with no color target or one-sample color targets, is drawn as a one-sample draw at the pixel center. Without the bit every sample is taken to be at the pixel center (see the entry for a multisampled target rasterized without MSAA_ENABLE below), and a one-sample target is taken to keep the coverage of sample 0, so a one-sample depth target is tested and written once per pixel with the depth at the pixel center. That is the rule [`VK_AMD_mixed_attachment_samples`](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/fragops.adoc#L2510-L2516) gives a color target with fewer samples than the rasterizer, but Vulkan reaches it only beside a depth or stencil target with the rasterizer's sample count ([VUID-VkGraphicsPipelineCreateInfo-subpass-01505](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/pipelines.adoc#L3033-L3041)), so here it is an analogy, and for a one-sample depth target an extension of it. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp#L127-L128) also rasterizes with the largest sample count of the bound depth and color targets ([L416-L417](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L416-L417), [L477-L480](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L477-L480)), and reads `PA_SC_AA_CONFIG` only for a `SAMPLE_COVERAGE` input ([L174-L177](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L174-L177)). This is not measured on hardware. An `ANCILLARY` or `SAMPLE_COVERAGE` pixel input and a sample-mask export throw in a pixel program that runs, and so does `PS_ITER_SAMPLE` with one-sample color targets and no depth target, because what the console gives them under a multisampled configuration is not known; a depth-only draw that skips its pixel program is drawn with the null program, and its `PS_ITER_SAMPLE` and pixel input bits are ignored. With a one-sample depth target, a pixel program with `PS_ITER_SAMPLE` and neither input runs once per pixel; if the console runs it once per sample, its atomics and append or consume counters count differently. Points and alpha-to-coverage throw as for a multisampled target. With a one-sample depth target, line and point lists (`VGT_PRIMITIVE_TYPE` 1-3, even when a geometry program turns them into triangles), `DB_EQAA` Z interpolation (`INTERPOLATE_COMP_Z`, `INTERPOLATE_SRC_Z`) and overrasterization (`OVERRASTERIZATION_AMOUNT`, `ENABLE_POSTZ_OVERRASTERIZATION`) also throw. METAL GEAR SOLID: Peace Walker (PPSA18216) draws a one-sample color target under a 4x configuration this way; the first draw of its opening movie binds a one-sample depth target and no color target under the same configuration. +- [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 or rectangles (`VGT_PRIMITIVE_TYPE` 7 or 17 as well as the triangle types) 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. EA Sports UFC 5 (PPSA03541) reaches a rect-list draw in this state. Other locations of sample 0, absent location registers, points, lines, alpha-to-coverage and conservative rasterization throw +- [A multisampled target rasterized without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - not measured on hardware: `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear puts every color, depth and stencil sample at the pixel center, as in GL without multisampling ([ARB_multisample](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089/extensions/ARB/ARB_multisample.txt#L185), [RADV 061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11)). Lines cover whole pixels by the host's line rule, not the console's aliased one ([D3D11.3](https://github.com/microsoft/DirectX-Specs/blob/f37b0d6033a1/d3d/archive/D3D11_3_FunctionalSpec.htm#L13705)). Depth sample tests and occlusion counts are unknown. Rasterized points, alpha-to-coverage, and Z interpolation, overrasterization or a sample-mask export with depth throw. PPSA06888 draws a 2x target, PPSA18216 a 4x depth and color pair and 4x lines. - [sceAgcDriverGetEqContextId](../../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw - [User data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed - [SET_PREDICATION](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw +- [Barycentrics without mesh shaders](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp) (AGC driver) - a pixel shader that reads barycentrics on the mesh path is emulated only on a device with `VK_EXT_mesh_shader`; a device with neither it nor `VK_KHR_fragment_shader_barycentric` (reported on Linux RADV with a GCN 5 GPU) throws that the device has neither the barycentric extension nor mesh shaders, because a geometry stage cannot follow a mesh stage and no other route is implemented. `VK_AMD_shader_explicit_vertex_parameter`, which a GCN 5 device on RADV exposes, is an unexplored option. - [NGG vertex programs on the mesh path](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - an unregistered vertex program with `PRIMGEN_EN` that uses LDS or `s_barrier` runs on the geometry (mesh) path. Each input primitive is assumed to yield at most one output primitive, the subgroup is bounded by `GE_MAX_OUTPUT_PER_SUBGROUP` as a vertex budget, and the GS/ES VGPR count checks are skipped, because the merged inputs keep their positions. Taken from the code Mesa's ACO emits for NGG culling and from PPSA59322 and PPSA99010 draws, not measured on hardware. Registered programs keep the vertex path -- [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. -- [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. +- [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. The same holds for a read-only strided buffer that starts below the render target and runs less than one stride into it: it is bound only up to the target, as a vertex fetch whose attribute descriptor starts at the attribute's offset reaches that far past the attribute's last bytes. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware. +- [Buffer descriptor CACHE_SWIZZLE hint](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - bit 30 of word 1 in a buffer resource descriptor (`words[1] & 0x40000000u`) is normalized to zero when binding guest buffers in `ShaderResources::addGuestBuffer`. On GFX10 (gfx1013), bit 30 of V# word 1 is `CACHE_SWIZZLE` (Mesa's `gfx10-rsrc.json`: STRIDE [29:16], CACHE_SWIZZLE [30], SWIZZLE_ENABLE [31]), a cache-bank hint with no effect on the address or data, which is dropped here. +- [Buffer descriptors past the range limit](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a buffer V# whose size is over `maxStorageBufferRange` is bound only up to the end of the readable registered allocation holding its base when that allocation ends before the limit, and as the empty placeholder buffer, like a null V#, when no registered allocation holds its base; a V# over an allocation that reaches the limit still throws. Titles leave such words in descriptor slots their shaders do not reach on the path taken (PPSA10528 binds `c8010002 2c6bec20 c0053c00 04000013`, about 33 TB from an address above the user address space). What the console returns for an access past the bound range is not known; here the device's robust buffer access decides. Follows the review of #3202, not measured on hardware. +- [Vertex fetch overhang](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a zero-padded vertex fetch whose valid range (`num_records * stride`) ends less than one stride past the end of the readable memory at its base copies only the readable bytes and reads the rest as zeros, instead of failing the draw; a longer overhang still fails. This extends the strided buffer overhang rule above (the last record is the rounded-up `num_records`) to the staging copy and is not measured on hardware. The observed case is a fetch of 14858 records of 44 bytes that ends 8 bytes past the end of the 0xa0000-byte mapping holding it. +- [Zero-stride vertex fetch range](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a draw whose highest index is at or past a nonzero NUM_RECORDS of a stride-0 `OOB_SELECT` 1 V# throws: the pipeline binding has stride 0, so vertices cannot differ. Only the highest index is checked, so a draw with every index past it throws too, where the hardware reads zero. An indirect draw is not checked. Unmeasured on gfx1013 (the console), with an instruction offset or SOFFSET, with a swizzled V# and in a vertex shader. Rules from #8299, measured on an RX 6700 XT (gfx1031), same results on gfx1010, with raw ISA ([data](https://gist.github.com/thuanquang/3b25d677c1a32d79123dd2df1819dd83)). Stride 0 reads zero for `OOB_SELECT` 0 always; for 1 when index >= NUM_RECORDS (records); for 2 when NUM_RECORDS is 0; for 3 when NUM_RECORDS is under the fetch's bytes. Untyped loads check each dword under 3; only typed fetches are used here. - [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 +- [Shader header copies](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a header bound for a draw or dispatch at another address than the registered one is accepted when its fixed fields match the registered header, and every array it points at still holds the registered bytes; the registered header's metadata is used. `user_data` is not compared (PPSA21567 binds such a copy of a vertex shader header with `user_data` set to null; whether the console reads the copy's `user_data` is unknown). A copy whose registers, context registers, specials or semantics differ, or whose arrays are unreadable, throws `... refers to a replaced shader header`; what the console does with a header a title patches while relocating it is not measured. PPSA01325 (#2914) resolves the ABI with such a copy - [HtileFillCovers](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a fill is taken to cover a depth surface's whole HTILE when it spans 32 KiB per 1024x512 pixel block from DB_HTILE_DATA_BASE, the pipe-aligned SW_64KB_Z_X metadata extent AMD PAL's gfx10 addrlib gives for GB_ADDR_CONFIG 0x00100044 (1-8 samples, D16/D32); other metadata layouts are not decoded. A partial fill is ignored (the surface keeps its contents); not measured on the console. - [Depth fast clear through HTILE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a surface cleared by an HTILE fill is cleared to DB_DEPTH_CLEAR, also when it is next sampled. For a depth-only HTILE the clear word also carries a value (radv writes `minz = maxz = round(depth * 0x3fff)`, `HTILE_Z_CLEAR_REG` in Mesa's `src/amd/common/ac_descriptors.h`, and allows TC-compatible fast clears only to 0.0 or 1.0); which of the two the texture unit returns for a cleared tile was not measured. - [Depth-only and stencil HTILE fill](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - in a whole-HTILE fill, ZMask (bits 0-3) 0 clears the depth and SMem (bits 8-9) 0 clears the stencil, while SMem 3 leaves the stencil alone (`HtileFillClears`); other SMem values are taken to leave it alone too. The bit meanings follow the gfx10 HTILE layout as the code reads it, not a measurement; `agc_driver_depth_fast_clear_submit` pins them for the paths the Driver takes. +- [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 -- [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register. -- [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. Retired Vulkan images remain allocated until device teardown to preserve outstanding references. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. +- [Workgroup count split](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) - a direct dispatch over `maxComputeWorkGroupCount` runs as `vkCmdDispatch` chunks, each bound to a pipeline variant whose workgroup offset specialization constants add the chunk's base to `WorkgroupId` and `GlobalInvocationId`. A variant is built when its chunk base is first recorded, not when the dispatch is prepared. A split of a shader with device-memory LDS throws: its slot base reads the chunk's `NumWorkgroups`, not the dispatch's. Chunks are ordered by a compute barrier, which the console does not have between workgroups of one dispatch. +- [Scan-resistant sampled texture admission](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - while the sampled texture cache is over its budget but within a quarter over it, the first use of a texture it does not hold in a present is refused (used for its draw, then released, never cached) when its least recently used entry was used in the current or previous present, and a later use in the same present is refused when that entry was used in the current present. So a working set larger than the budget keeps a stable subset: a texture used more than once in a present still evicts an entry not yet used in that present, so a working set with interleaved multi-use textures rotates as on main, and the textures that arrive after the cache fills are created again at each use for as long as the least recently used entry is from the current present, including in the transition present after a scene change. Past a quarter over the budget the entries are evicted as before, so after the budget falls the cache can sit at up to 125% of the reduced budget until entries stop being used. Entries unused for two presents are evicted as before. A later use can evict an entry from the previous present and then be refused when the next least recently used entry is from the current present, so that entry is created again in the next present. The "current or previous present" window is an unmeasured threshold: it was chosen, not derived. Measured once, as one A-B-A series on one title (loading phase about 33000 textures per frame against a 5.4 GiB budget) on one RTX 4090, 3600 s each: 932 / 968 / 941 frames, evictions after frame 620 2.72M -> 1.17M, texture creations unchanged (about 5.8M). Nothing is measured on other titles or GPUs, and a title whose per-present working set only slightly exceeds the budget now re-creates the excess textures at every use. +- [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache's budget is what the reported heap budget leaves after the heap's use other than the cached storage images (at least the sampled textures alive) and an eighth of the budget, capped at a quarter of the budget and at `TextureCacheBudget` (a quarter of the heap's size, at least 2 GiB), never below 512 MiB; the sampled cache's budget reserves `TextureCacheBudget` for the storage cache and never falls below 2 GiB. The storage budget keeps no room for the part of that 2 GiB floor the sampled textures alive do not use: it gives room back as they grow, at its next read. As the heap's other use grows the sampled budget reaches its floor first, then the storage budget falls. Each cache trims only at its next insert, a storage image is allocated before its insert reads the budget, and both budgets read the heap at most once a second, so a burst between two reads, sampled textures included, can still exceed the reported budget. The eighth is the sampled formula's value reused and the 512 MiB floor and the caps the earlier ones; none was measured beyond PPSA11386 on one 10 GB GPU and PPSA16064 on one 8 GB GPU, and a title whose storage working set exceeds the shrunk budget re-creates storage images at each use. +- [Polygon primitive type](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_PRIMITIVE_TYPE` 21 (`PrimitiveType::Polygon`) decodes to `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN` and is treated as inactive for vertex point size, like triangle fans and strips. PPSA01317 (Gran Turismo 7) submits polygon draws; whether the console performs polygon-mode decomposition or custom vertex routing is unverified. +- [Native multisampled DCC fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) import uniform 0000/0001/1110/1111 keys into a single-layer, level-zero SW_64KB_R_X color image with 2, 4 or 8 fragments. Color and DCC/CMASK ranges must stay write-watched and unchanged; expanded or mixed initial DCC, register CMASK clears, compressed payloads, color transforms, lossy modes, other metadata changes, guest publication and writes over a live owner throw. A whole-range DCC fill with one of those keys, landed or still recorded, is taken as its fast clear; a partial fill, or a CMASK changed without a whole fill, throws. Driver-mediated reads of the color and metadata stay guarded. The metadata extent follows [AMD Addrlib](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L347-L454) with the [PS5 Mesa model](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/winsys/ps5/radv_ps5_winsys.c): GFX10.1, 16 pipes, 256-byte interleave, two maximum compressed fragments. Console geometry, concurrent guest mutation and transitions outside this subset are unverified. -- [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane from slice 0 is a storage image over all its slices (`LayeredDepthStorage`): a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, or a sampled copy) is a separate image, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. +- [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register. +- [Multisampled DCC comp-to-single clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (AGC driver) - RADV's multisampled comp-to-single clear dispatch ([`radv_clear_dcc_comp_to_single`](https://github.com/mihawk-99/PS5_Mesa/blob/1ba9874f478657ccf6da72ce55f0260164cc0bd0/src/amd/vulkan/meta/radv_meta_clear.c#L954)) is recognized by its code and not run. Its clear color (user data 5 to 8, which the shader stores as the four dwords of sample 0's texel) is recorded for the surface its image descriptor names, and DCC keys of 0x10 on a multisampled target of that surface, pipe-bank XOR, size and sample count import as a clear to it, for texels of 8 to 128 bits. A record is dropped when an unmap or allocation removal covers its surface's base. The guest's color memory doesn't get the color at the blocks' first pixels, and the recorded color is assumed to be the one the keys refer to: RADV writes 0x10 keys only in a fast clear that runs this dispatch for the same image right after, and initializes DCC to 0x00 or 0xff on an image's first use ([`radv_init_color_image_metadata`](https://github.com/mihawk-99/PS5_Mesa/blob/1ba9874f478657ccf6da72ce55f0260164cc0bd0/src/amd/vulkan/radv_cmd_buffer.c#L16894)). A new image on memory reused within one mapping, with the same address, XOR, size and sample count, whose clear runs a comp-to-single shader other than the recognized one, would still import the old color. +- [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault). TYPE 1 comes from a gfx1010 (RDNA1) measurement in #4139 and #5828 (`image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` return zero, no fault) with format 22 only; TYPE 1 is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). Neither type was measured on the console's gfx1013. +- [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. The next target, sampled or storage lookup removes retired cache owners; recorded work and sampled views keep their images alive until their last reference is released. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. +- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` replays the low eight bits of the values proved from constant, select and phi exports, with an export proved on every kept path. An export the prover cannot bound replays all 256 eight-bit values, one pass per value per triangle (logged once per process). It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. + +- [32-bit depth-bits reads of guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a sampled 32_UINT or 32_SINT T# with a depth swizzle mode that no live depth surface matches reads the guest bytes through an R32_SFLOAT image, as the recompiler's depth-bits read expects, and the shader casts the fetched value back to the stored dword. On RX 6700 XT (gfx1031, AMD Windows driver 32.0.21045.5002) the read keeps float denormals, the sign bit alone and quiet and signaling NaN payloads bit for bit. A host that canonicalizes NaNs or flushes denormals on a 32-bit float fetch would change those dwords, and other drivers are not measured. A live depth surface whose extent or base level differs from the T# is not used; the read takes the guest bytes, as the 32-bit float and 16-bit views already do. +- [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A dispatch of the 15-word 8x8x1 image clear kernel ([`MatchStorageImageClear`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp): one colour read from the V# in `user[8..11]` stored to every texel of the T# in `user[0..7]`) that covers the whole plane and stores 0 or 1 through a 16-bit unorm view, 0 or 0xffff through a 16-bit uint view, or 0 or a normal float in (0, 1] through a 32-bit float or integer view leaves out the copy-in and replaces the copy-out by a clear of the depth image to that value; the dispatch still writes the storage image. The colour is read again when the copy-out is replaced, and a changed value keeps the copy-out. Indirect dispatches, partial last groups, dispatches that queue on the draw pipeline, other values and formats, a plane the kernel covers only partly and stencil planes keep both copies. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. -- [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. Planes with mips, a base slice other than 0, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). +- A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target or a written storage image it does not serve overlaps it, or when it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it, except for the depth planes the next entry keeps in guest memory. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. +- [Depth planes in guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): only single-sample `D32_SFLOAT` and `D32_SFLOAT_S8_UINT` planes with the 64 KiB Z swizzle (`SW_MODE` 24) reach guest memory; D16, other swizzles, stencil and multisampled planes keep depth in the host image only. A plane drawn since its last store is stored, uncompressed, in 64 KiB units (128x128 tiles): a buffer upload, a `GuestBuffers` V#, an image lookup or a pending-store flush stores the units it overlaps, a color target or written storage image over the plane all of them. CPU reads and reads through address-based heaps are not caught. Guest writes are ordered when collected (bind, flush, collect epoch), not when made: a collected write reloads the units it touched unless an HTILE clear is pending. A store outside a recorder waits for the GPU. The console keeps HTILE-compressed tiles compressed in memory until a decompress (Mesa `ac_descriptors.h`). +- [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. A 2D-array T# whose `BASE_ARRAY` is past slice 0 reads slices `BASE_ARRAY` to `LAST_ARRAY` of the copy, which holds slices 0 to `LAST_ARRAY`, and a 2D binding of it reads slice `BASE_ARRAY`; that follows the colour 2D-array semantics and the titles that hit it (PPSA01556, PPSA03912), and was not measured on hardware for depth surfaces. Planes with mips, a cube or a 2D T# with a base slice other than 0, a `BASE_ARRAY` past `LAST_ARRAY`, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). +- [Repacked format-43 copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a sampled texture of format 43 (10-bit R, 11-bit G and B unsigned floats from bit 0) is uploaded into a `B10G11R11_UFLOAT` image with its R and B fields swapped (`RepackTextureWord`) and viewed with R and B swapped back. A texture whose results a storage image still holds is copied from it again, whole, whenever the image changed (`RefreshDepthCopy`); a 3D one is flushed to guest memory and uploaded instead. Storage reads and writes of format 43 keep the packed `R32_UINT` path. - [SW_64KB_Z_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the texture equations and 64 KiB block geometry of `SW_64KB_Z_X`, whose addresses are checked against [AMD addrlib GFX10](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) in the texture tiling tests. Any 256-byte pipe/bank XOR below the block size is accepted, as for `SW_64KB_R_X` targets and Z_X textures; which of those bits the console honours, and the render results, are not measured. - [SW_64KB_D_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the existing texture block/mip geometry and equations from [AMD addrlib GFX10 non-RB+](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) for 16 pipes and 256-byte interleave. The address-reference tests use legal pipe/bank XOR bits for that configuration. Raw column bits outside the source configuration's pipe/bank mask 0xcf00 throw; their console meaning is unresolved. Console address equations and render results have not been measured. - [Per-face depth bias](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): Vulkan has one depth bias per draw, so a draw that rasterizes both faces with different front and back bias (PA_SU_SC_MODE_CNTL, PA_SU_POLY_OFFSET_FRONT/BACK_*) is recorded twice, front faces with the back faces culled and then back faces with the front faces culled. The vertex stages run twice, so a vertex shader that writes memory writes twice, and all front faces land before all back faces instead of in primitive order, which differs only for blending or stencil updates that depend on the order. - [Draw recipes with depth targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): direct draw recipes record and replay with active depth targets, matching the bound depth surface view by its target view identity; altered depth surface addresses or incompatible view handles trigger a recipe miss and pipeline rebuild. - [Partial rect-list primitives](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): a rect-list draw whose vertex or index count is not a multiple of 3 draws its complete rectangles and leaves out the 1 or 2 vertices after them, because rect lists are drawn as Vulkan patch lists of 3 control points, which generate [⌊vertexCount/3⌋ patches](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#drawing-patch-lists). What the console draws for the incomplete rectangle is not measured; Palworld (PPSA23126) draws 4 vertices × 9 instances as a rect list when its character creation opens. +- [8_8 UINT color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) decode as `R8G8_UINT` with the same component-swap mapping as 8_8 UNORM/SNORM (`0xe4` / `0xec`). Follows the existing 8 and 8_8_8_8 UINT paths; not measured on the console. RetroArch (PPSA99169) uses it during menu startup. +- [Quad-list primitives](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): primitive type 19 (DI_PT_QUADLIST, 0x13) translates to Vulkan triangle lists by generating 6 indices per 4-vertex quad in a synthetic 32-bit index buffer during draw input preparation (triangles (0, 1, 2) and (0, 2, 3) for first provoking vertex; (0, 1, 3) and (1, 2, 3) for last provoking vertex). Incomplete trailing vertices (< 4) are discarded. Indirect draws, primitive restart and draw recipes are unsupported for quad-list primitives. Console behavior for incomplete quads has not been measured on hardware. - [Separate alpha blend into 8_8 SWAP_ALT targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver): the second channel of a two-channel target with `COMP_SWAP` 1 holds the alpha export, but Vulkan blends it with the color equation of the R8G8 attachment, so a draw whose alpha equation differs from its color equation is recorded twice, the first channel with the color equation and the second with the alpha factors and operation moved into the color equation. The vertex stages run twice, so a vertex shader that writes memory writes twice, and occlusion counts include both passes. Draws that write depth or stencil, pixel shaders that write memory and factors of the destination alpha throw. That the hardware blends the second channel with the alpha equation is taken from Mesa's `si_translate_colorswap` and radeonsi's use of it for luminance-alpha formats, not measured on hardware. - [Unexported pixel inputs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver): a pixel input whose `SPI_PS_INPUT_CNTL` parameter the previous stage does not export is programmed as a default-value input (`OFFSET` bit 5, `DEFAULT_VAL` 0, FP16 high half too) and reads (0, 0, 0, 0), as radeonsi's `SI_PS_INPUT_CNTL_UNUSED` does and as rect-list draws already do. The console's value for an unwritten parameter cache entry is not measured on hardware. +- [Per-sample pixel shading](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): `PA_SC_MODE_CNTL_1.PS_ITER_SAMPLE` (bit 16, [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json)) on a multisampled draw enables Vulkan sample shading with a minimum rate of 1.0, so the pixel program runs once per sample. The draw throws when the device lacks `sampleRateShading`. On a single-sample draw the bit has no effect and is dropped. That the console runs the pixel program once per covered sample, and what it does with the other multisample registers in that mode, is taken from the register name and Vulkan's definition, not measured on hardware. Without the bit the pixel program runs once per pixel: the recompile request carries the mode (`iteratesSamples`), the ancillary's sample number reads 0 instead of `SampleId`, whose use would make Vulkan shade per sample, and the sample barycentrics and positions are evaluated at sample 0, whose locations the request carries (`sampleLocations`, every sample's in an iterated draw). `DB_EQAA.PS_ITER_SAMPLES`, the number of samples an iterated shader covers, is not read: an iterated draw runs once per sample. +- [Unused pixel programs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): a bound pixel program is skipped when color masks have no intersection and DB export and execution enables are clear, regardless of retained SPI_SHADER_Z_FORMAT. This follows [AMD PAL DbShaderControl](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9AbiToPipelineRegisters.h) and its [GFX10 register definitions](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/chip/gfx9_plus_merged_registers.h), not a hardware measurement. Enabled DB_ALPHA_TO_MASK conservatively retains the program even with ALPHA_TO_MASK_DISABLE, because DecodeState currently enables Vulkan alpha-to-coverage without applying that override. Console behavior for these combinations has not been measured. +- [Sample masks](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `PA_SC_AA_MASK_X0Y0_X1Y0` and `PA_SC_AA_MASK_X0Y1_X1Y1` hold one 16-bit sample mask for each pixel of the aligned 2x2 quad ([Mesa gfx10.json](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/registers/gfx10.json#L13512)); only the bits of the `MSAA_NUM_SAMPLES` rasterized samples count, and at one sample bit 0 of a pixel's field covers every sample of the target the draw is raised to. A mask equal for the four pixels is the Vulkan `pSampleMask`, the case [radv](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_cmd_buffer.c#L13633) and [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/gallium/drivers/radeonsi/si_state.c#L4218) program; with early fragment tests it needs `earlyFragmentSampleMaskTestBeforeSampleCounting` ([radv](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_physical_device.c#L2079) reports it). A mask that differs between the pixels is applied in the pixel shader without early fragment tests: a pixel whose mask covers all or none of its samples is demoted to a helper invocation by `gl_FragCoord` parity and killed when the program returns (the Radeon 860M's Windows driver 32.0.13058.8001 writes undefined colour for a demoted pixel whose export leaves its output unwritten), and otherwise the pixel's mask is ANDed into `gl_SampleMask` (with alpha-to-coverage and the mask export) after demoting invocations whose `SampleMask` input it leaves empty. A pixel program with early Z that writes memory under depth, stencil or depth-bounds tests is therefore refused. That XnYm is the pixel at (x mod 2, y mod 2) = (n, m) is read from the field names; that the mask removes samples before the depth and stencil tests and occlusion counting is inferred from radv implementing the Vulkan sample mask test with it; and that it takes no part in the centroid (`AA_MASK_CENTROID_DTMN` 0) is inferred from radv setting that bit only for conservative rasterization. None of it is measured on gfx1013. A mask equal for the four pixels that removes some but not all samples of a pixel is refused with active centroid inputs, since where Vulkan puts the centroid relative to `pSampleMask` is implementation-defined, and any partial mask is refused with conservative rasterization (radv sets `PREZ_AA_MASK_ENABLE` and `POSTZ_AA_MASK_ENABLE` [there](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_cmd_buffer.c#L13653), which 0x6001 leaves clear). - [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. +- [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, rounded up to an even count so that 64-bit atomics stay 8-byte aligned, per workgroup (the dword after the LDS is the atomic lock on a device without `shaderBufferInt64Atomics`), 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. +- [Host placement after a device-memory refusal](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) (libSceAgcDriver): when the driver refuses a device-local allocation with `VK_ERROR_OUT_OF_DEVICE_MEMORY` after the pool's trim and the texture-cache release, device buffers and images (textures, storage textures, block, repacked, depth, depth plane and stencil copies, render, depth and multisampled targets and attachment proxies) are allocated in a host memory type their requirements allow. A host-placed device buffer leaves the pool on release. A host-placed image keeps that placement until it is destroyed and is never moved back to device memory, so every access to it crosses PCIe for its lifetime. The placements are counted in a rate-limited `[bufferpool]` line with the cumulative MiB, not the resident bytes. The 1x1 denormal clear probe still allocates device memory directly and throws on a refusal. +- [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 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). `APS5_STORE_AT_FLIP=1` stores every pending result at each flip; storing them at the title's GPU waits (labels, fences) is unimplemented. -- [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending, and ranges that leave the watch through a failed collect, have no saved bytes; storing such results throws. -- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing remains unimplemented. -- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. -- [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when its offset in the backing file is not a multiple of the buffer's sparse block size (64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory +- [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. +- [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail. +- [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. +- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. +- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until a GPU use of the import completes; a 4-byte read of it on the settle queue ends that even while main-queue work that used the import first is still running, and an earlier import of the same pages whose first use runs after that read marks none of them (`agc_import_settle_first_use`, `agc_import_settle_earlier_import`). Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists is compared instead. An import of under 1 MiB is not settled inside its window, because the settle blocks the importing thread and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). It is compared until its read, submitted in a batch by a later import lookup under `GpuMutex` while the imports match the registry, has completed; a later lookup then returns its whole blocks to the write watch, each stamped as a CPU store, so whatever was derived from them while they were compared reads as changed once. Its blocks stay compared while storage results are pending over them, and those of an import retired before that stay compared. +- [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled +- [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range whose offset in the backing file is not a multiple of 64 KiB starts its sparse buffer at the 64 KiB block below it (`HostImport::lead`). A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when the buffer's sparse block size is larger than 64 KiB (it is 64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. -- An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) throws with unequal minification and magnification filters, anisotropy, `TRUNC_COORD`, `MC_COORD_TRUNC` or X/Y clamping other than last-texel or border. +- An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) maps clamp mode 0 (repeat) on X and Y to clamp-to-edge as required by Vulkan unnormalized coordinates, and assigns the magnification filter when minification and magnification filters differ (since unnormalized coordinates sample at LOD 0). Anisotropy, TRUNC_COORD, MC_COORD_TRUNC and other clamp modes still throw. Secondary LOD bias (LOD_BIAS_SEC, bits [19:14]) is accepted and ignored, matching RDNA2 hardware measurements across explicit and implicit samples. High on Life (PPSA15388, #4736) uses secondary LOD bias and unnormalized coordinates with clamp mode 0 and differing min/mag filters during home base and city rendering. +- [TRUNC_COORD](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) is not applied: the texel a point sample reads and the bilinear weights follow the host sampler's rounding. On RDNA 2 (RX 6700 XT), an S# with TRUNC_COORD 0 rounds the exact scaled coordinate to 8 fractional bits, ties to even, and one with TRUNC_COORD 1 truncates it, for point and bilinear filters. AMD's Windows driver (32.0.21045.5002) truncates for nearest samplers and rounds for linear ones. Under it, a TRUNC_COORD 0 point sample at most 1/512 texel below a texel edge reads the texel below the edge where the console reads the one above, and `k / width` computed in f32 lands there for about half of all k. A TRUNC_COORD 1 bilinear weight can be 1/256 above the console's. Other host drivers are not measured. - [Background link-time-optimized relink of library pipelines](../../../core/libs/prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp) (libSceAgcDriver) is skipped on every GPU under NVIDIA's proprietary Vulkan driver (`VK_DRIVER_ID_NVIDIA_PROPRIETARY`), so those pipelines keep their fast-linked form. The optimized link loses the device when bound on an RTX 3080 (Ampere, #2866) and an RTX 4060 (Ada, #3434); an RTX 5060 and an RTX 5080 (Blackwell) on the same driver do not, so the scope is wider than the evidence. No standard Vulkan query tells the architecture apart, and the driver version that fixes it is unknown. `APS5_NO_PIPELINE_LTO` disables the relink on every driver. -- [8_SRGB and 8_8_SRGB textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) that the device samples as R8_SRGB or R8G8_SRGB use its own decode, which need not match gfx1035: NVIDIA's R8_SRGB is one bf16 step off in 29 of 256 codes. +- [8_SRGB and 8_8_SRGB textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) that the device samples as R8_SRGB or R8G8_SRGB use its own decode, which need not match gfx1035: NVIDIA's R8_SRGB is one bf16 step off in 29 of 256 codes. A device that cannot sample them gets an RGBA8 sRGB copy instead (a blit of the R8 or RG8 texels, refreshed with the storage image's version), whose decode is likewise the device's; 3D textures and storage images that are not single-depth and format-matched are copied from their snapshot instead. +- [PM4 submission rings](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp) (libSceAgcDriver) keep only the first 512 dwords of each submission, so the packets past them are not printed. The [failure report](../../../core/libs/prx/libc/src/FailureReport.cpp) dump does not allocate from the C runtime heap: packet names come from static strings, each line is formatted with integer and string conversions into a 512-byte stack buffer and written with one `WriteFile`. It still runs on whatever state the crash left. It waits about 100 yields for each ring's lock and otherwise prints that queue as busy, so a crash while recording a submission loses that queue's ring. It reads the rings without checking them, so a crash that corrupted them can fault inside the dump; such a fault during the report of an unhandled exception stops in that report's `Sleep(INFINITE)` and the process hangs instead of ending. The reports run at most once per process. Linux has no crash report: there the rings are printed only by `APS5_STALL_WATCHDOG_SECS`. - [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) reports a diagnostic after `APS5_GPU_WAIT_TIMEOUT_MS` without treating the timeout as completion. An unsatisfied wait keeps polling and checking driver failures; a missing producer can therefore leave its queue waiting indefinitely. Existing recorded-label shortcuts remain in use. +- A [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) (libSceAgcDriver) on a queue 0x20 label can time out while the awaited word holds the address of another slot of the same label area instead of 0 or 1, with no store to that label in the PM4 ring window (`APS5_PM4_RING`). What writes the address (guest CPU or the emulator) and whether the console would have satisfied the wait before the slot was reused are unverified; the wait keeps polling and its queue stays blocked. Seen in several titles on a Windows NVIDIA GPU; not measured on hardware. +- [PM4 type-0 packets](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp) (libSceAgcDriver) - a dword whose packet type is 0, a zero header word included, is rejected with `unsupported PM4 packet type`, so the submission throws. Whether the console's command processor skips or executes such dwords, and what a run of them in front of a REWIND packet means, is unverified; shadPS4 (`liverpool.cpp`) and KytyPS5 (`pm4.cpp`) also treat type 0 as unsupported. One such submission begins with a seven-dword NOP and an eight-dword ACQUIRE_MEM, then 169 dwords of zero headers (dwords 15 to 183, zero in one run and stale words in another), and its packets start at dword 184, as sections of NUM_INSTANCES, SET_BASE, REWIND, NOP and ACQUIRE_MEM separated by zero gaps of 26 to 455 dwords (odd lengths included); letting a zero header through only moved the stop to a stale word. - [COND_EXEC](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) reads its condition after the earlier packets of its queue complete. Without PFP_SYNC_ME the console's graphics PFP reads it ahead of the ME, so a title that races the read sees a later value here. - A [flip inside a COND_EXEC range](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp) throws: the flip is reserved with the video output when the buffer is submitted, and how the console withdraws a skipped one is unknown. - A [COND_EXEC range](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) must hold whole packets and end inside its command buffer. A chained INDIRECT_BUFFER or a REWIND inside a range throws, as what the CP does with the skipped dwords then is unknown. - A [COND_INDIRECT_BUFFER](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (`sceAgcCbBranch`) runs only with the always-pass compare function; a comparison or a predicated packet throws. Modes 1 and 2 run the first buffer, assumed to be the if-then and if-then-else modes of AMD's PM4 packet. [sceAgcCbBranch](../../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) does not check the compare address of an always-pass branch or the address of an empty buffer, as PPSA14632 passes an uninitialised compare address and a null else buffer of 0 dwords. - A [label or store](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`) into guest memory the device has no host import of waits for all recorded work and is then written by the CPU, so the queue worker stalls once per such label. The console's CP stores it in stream order without that stall. -- [WRITE_DATA with DST_SEL 3](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5. AMD's GFX10 header names 3 the GDS, but Killzone: Liberation (PPSA11142) writes its frame fence with `sceAgcDcbWriteData(dst 6, cache policy 3, write confirm)`, which the SPRX-confirmed encoding ([OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/cb_builders.c)) turns into DST_SEL 3 with a 40-bit label address in the title's own memory that `RELEASE_MEM` and `WAIT_REG_MEM` then use; the console must write that label or the title would wait on it forever. Whether the console routes it through the GDS or a cache is unknown -- [SET_*_REG_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. -- The [shader float mode](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (`FLOAT_MODE`, `DX10_CLAMP`, `IEEE_MODE` and `FP16_OVFL` of `COMPUTE_PGM_RSRC1` and `SPI_SHADER_PGM_RSRC1_PS/GS/HS`, bit positions from Mesa's `gfx10.json` and `gfx103.json`) is decoded into `GuestContext::floatMode` and is part of the shader cache keys and the serialized request (#981). `IEEE_MODE` is read by `v_min_f32`, `v_max_f32`, `v_min3_f32`, `v_max3_f32` and `v_med3_f32`, their f16 forms, `v_min_f64` and `v_max_f64`, `IEEE_MODE` and the `FLOAT_MODE` output denormal bits by the output modifier, `DX10_CLAMP` by the clamp modifier, `FP16_OVFL` by every instruction with an f16 result (see its entry), and the whole mode by the 16-bit interpolation instructions (see their entries). With IEEE=1 a signaling NaN operand returns that NaN quieted (the first operand checked first, min3/max3 as two nested pairs, med3 as min3 when any operand is NaN), measured on RDNA2 (gfx1035) over every combination of ±0, ±1, ±inf, denormals, quiet and signaling NaNs with f32 denormals kept and flushed. IEEE=0 and unknown modes keep the previous f32 lowering, which matches IEEE=0. The f16 forms choose their NaN results on the bits: a NaN operand returns the other operand unchanged (src0 when both are NaN), and with IEEE=1 a signaling NaN operand returns that NaN quieted, in the same order as f32. `v_min_f64` and `v_max_f64` quiet a signaling NaN operand the same way with IEEE=1; with IEEE=0 they return src0 unchanged when src1 is NaN, otherwise src1 when src0 is NaN. Both were measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f32 denormals kept and flushed and f16/f64 denormals kept; RDNA2 has no f64 min3, max3 or med3. Unknown modes follow IEEE=0. `v_min_f16` and `v_max_f16` flush each denormal input to its signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3 preserve inputs, even when output flushing is enabled. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1, including signed zeros, NaNs and neg/abs modifiers. Unknown modes keep f16 denormals. With f16/f64 denormals flushed the hardware also flushes denormal inputs of f64 min/max, which those lowerings still do not. NaN quieting in f16 arithmetic (the unary instructions and the f32 arithmetic follow it, see below) and `FLOAT_MODE` (f32 denormals flushed before the min/max compare in Astro's Playroom's 0xc0) still follow a single mode, as the entries below record. The mode comes from the registered shader header's `sh_registers`, the same source at registration and at draw/dispatch so the prepared artifact keys match. A title that rewrites RSRC1 at run time, or a raw compute program with no header, compiles with that mode or none. +- [WRITE_DATA to thread-trace user-data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) updates the queue's UCONFIG values for register destinations `0xc340..0xc347`, including repeated writes to one address; these are not guest-memory labels. The range follows AMD's GFX10 `SQ_THREAD_TRACE_USERDATA_0..7` definitions in [the Linux register header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_offset.h) and the packets emitted by PPSA21564's bundled AGC driver before its EOP flip. Other register destinations throw. GPU thread-trace capture and its user-data stream are not emitted; only the final register state is retained. Console trace behavior is unverified. +- [WRITE_DATA with DST_SEL 3](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5, which a PS5 does not: on firmware 13.40 (measured 2026-10-19 with buffers submitted through `sceAgcDriverSubmitDcb` inside a retail title) a DST_SEL 3 `WRITE_DATA` (control 0x00100300, or 0x06100300 with cache policy 3) left its label in direct memory unwritten, also after a `RELEASE_MEM` with GL2 write-back and invalidate (0x00300514), and a GDS `COPY_DATA` (SRC_SEL 3) at that address read 0. The console's own `sceAgcDcbWriteData(dst 6)` builds DST_SEL 3 (0x00110300) as here, and Killzone: Liberation (PPSA11142) fences its frame with it and waits on that label with `WAIT_REG_MEM`, so the write is kept until the path that lands the label is known. Closes when: that path is measured +- [SET_*_REG_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. When the list read at submission holds an offset dword that is not a register offset (bits 16-27 set, or the 0xffffffff sentinel), the worker reads the list again where the packet executes and writes that copy. PPSA23566 (UFC 6) intermittently submits a SET_CONTEXT_REG_INDIRECT whose first entry is 0x038f038f or 0x03900390 in both copies; what the PFP does with such an entry is unknown, so a copy that still holds one throws unless `APS5_SKIP_INVALID_INDIRECT_REGISTERS=1`, which skips its invalid entries and writes its valid ones. +- The [shader float mode](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (`FLOAT_MODE`, `DX10_CLAMP`, `IEEE_MODE` and `FP16_OVFL` of `COMPUTE_PGM_RSRC1` and `SPI_SHADER_PGM_RSRC1_PS/GS/HS`, bit positions from Mesa's `gfx10.json` and `gfx103.json`) is decoded into `GuestContext::floatMode` and is part of the shader cache keys and the serialized request (#981). `IEEE_MODE` is read by `v_min_f32`, `v_max_f32`, `v_min3_f32`, `v_max3_f32` and `v_med3_f32`, their f16 forms, `v_min_f64` and `v_max_f64`, `IEEE_MODE` and the `FLOAT_MODE` output denormal bits by the output modifier, `DX10_CLAMP` by the clamp modifier, `FP16_OVFL` by every instruction with an f16 result (see its entry), and the whole mode by the 16-bit interpolation instructions (see their entries). With IEEE=1 a signaling NaN operand returns that NaN quieted (the first operand checked first, min3/max3 as two nested pairs, med3 as min3 when any operand is NaN), measured on RDNA2 (gfx1035) over every combination of ±0, ±1, ±inf, denormals, quiet and signaling NaNs with f32 denormals kept and flushed. IEEE=0 and unknown modes keep the previous f32 lowering, which matches IEEE=0. The f16 forms choose their NaN results on the bits: a NaN operand returns the other operand unchanged (src0 when both are NaN), and with IEEE=1 a signaling NaN operand returns that NaN quieted, in the same order as f32. `v_min_f64` and `v_max_f64` quiet a signaling NaN operand the same way with IEEE=1; with IEEE=0 they return src0 unchanged when src1 is NaN, otherwise src1 when src0 is NaN. Both were measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f32 denormals kept and flushed and f16/f64 denormals kept; RDNA2 has no f64 min3, max3 or med3. Unknown modes follow IEEE=0. `v_min_f16` and `v_max_f16` flush each denormal input to its signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3 preserve inputs, even when output flushing is enabled. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1, including signed zeros, NaNs and neg/abs modifiers. Unknown modes keep f16 denormals. `v_min_f64` and `v_max_f64` flush subnormal inputs to signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3, and an unknown mode, preserve inputs. The fully flushed and preserved f64 behavior was [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/2036); split-mode selection follows [LLVM's denormal-mode enumeration](https://llvm.org/docs/AMDGPUUsage.html#floating-point-denorm-mode-enumeration-values). Other f64 arithmetic does not model these denormal controls. NaN quieting in f16 arithmetic (the unary instructions and the f32 arithmetic follow it, see below) and `FLOAT_MODE` (f32 denormals flushed before the min/max compare in Astro's Playroom's 0xc0) still follow a single mode, as the entries below record. The mode comes from the registered shader header's `sh_registers`, the same source at registration and at draw/dispatch so the prepared artifact keys match. A title that rewrites RSRC1 at run time, or a raw compute program with no header, compiles with that mode or none. - A [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) (libSceAgcDriver) that a compute queue passes on another queue's recorded label store, before the GPU has executed it, is landed before the queue's next dispatch, draw, COND_EXEC or predicated packet reads guest memory: the worker polls until memory satisfies the wait or no unfinished batch writes the label. The graphics queue (queue 0) is exempt, as landing its waits costs PPSA01341 about a quarter of its frame rate; a title that fills memory a graphics command buffer reads after it submits the buffer behind such a wait reads it early there. `APS5_LAND_WAITS_ALL_QUEUES=1` lands the graphics queue's waits too. +- [CPU reads behind an address-based build](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp) (libSceAgcDriver) - an address-based store (FLAT/GLOBAL, or through a descriptor the recompiler cannot resolve to a static buffer) is not recorded as a pending write: its batch holds a lease on the registered allocations instead, and the flush hook does not wait for it. A DISPATCH_INDIRECT whose arguments are read on the CPU first finishes the batches recorded with a lease (`Graphics::FinishLeaseWork`). These reads do not, so they can see guest memory from before such a build ran: the indirect draw arguments and draw count of a draw resolved on the CPU (`Pm4::ReadDrawArguments`, `Pm4::ReadDrawCount`), the count record of the [copy kernel](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp) HLE (guarded only by its shape check), and the checks in [Submission.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) that decide whether a queue awaits a label only the title writes (`awaitsTitle`, `orderReleased`). No test covers them and no title has been seen to read such memory there. - The [dispatches that follow a compute queue's wait](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp) (libSceAgcDriver), up to the next wait that memory does not satisfy, a packet that writes guest memory other than a label, or a conditional packet, are resolved (memory captured, shader recompiled) when the wait passes, as the CP reads them right after it; the graphics queue is exempt (`APS5_GROUP_CAPTURE_ALL_QUEUES=1` includes it). The device still reads guest memory when the dispatch is issued; when that memory no longer matches the capture, the dispatch is resolved again from current memory. -- [PA_CL_CLIP_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. +- [PA_CL_CLIP_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `CLIP_DISABLE` (bit 16) and `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) are accepted without changing pipeline state: Vulkan clips primitives against viewport guard bands and clips `NoPerspective` outputs linearly; what clearing bit 24 or setting bit 16 changes on the console beyond standard clipping is undocumented. - [10_11_11 unorm color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. -- [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 with per-vertex inputs, whose [ordering depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). 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 standard (COMP_SWAP 0) and reversed (COMP_SWAP 2) component orders are accepted, putting red and blue in the 10-bit low field. That follows from the format's layout and from the standard swap Mesa pairs with COLOR_10_11_11 for R11G11B10_FLOAT (ac_formats.c); it was not measured on the console. +- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) is not measured on the console: bit 19 follows opengnm's `flatshade_first` and Vulkan's vertex selection; rect lists take flat inputs from the third vertex (first mode: first); a geometry program gets odd strip triangle i as (i+1, i, i+2) in both modes (an RX 6700 XT, first mode: (i, i+2, i+1)); in last mode its primitives take flat inputs and layer from the last index, per-vertex ones in first-mode order; a flat input without pass-through read only via `v_interp_mov_f32` P10 (P20) is the provoking vertex's (ps5-opengl, #1413; first (last) mode throws). Barycentric tessellation stages give each corner the last vertex's flat values. Else last mode throws without `VK_EXT_provoking_vertex`, for tessellation, geometry-program fans, explicit per-vertex flat inputs, and strips whose pixel shader reads a vertex slot (`v_interp_mov_f32`; 16-bit interpolation unchecked). +- [Triangle strips through the barycentric tessellation stages](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - without `geometryShader`, a triangle strip without primitive restart is expanded on the CPU into one three-point patch per triangle in Vulkan's strip order: (i, i+1, i+2), and for odd triangles (i, i+2, i+1), or (i+1, i, i+2) with `PROVOKING_VTX_LAST`. That is the order the geometry stage receives, so both routes give the same weights and per-vertex values; which order the console uses for odd triangles is not measured. Indirect strips throw. - [Dual-source blending](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. +- [Blending into reversed-order color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 8_8_8_8 and 16_16_16_16 targets with `COMP_SWAP` 2 or 3 keep the exported alpha in their first channel and red or blue in their fourth, which Vulkan would blend with the color and the alpha equation respectively. The color block is taken to blend in shader export order and apply `COMP_SWAP` afterwards: the alpha equation and every source-alpha factor use the exported alpha, and the destination alpha is the first channel. This follows the `CB_TARGET_MASK` text of the AMD [SI](https://xorg.freedesktop.org/docs/AMD/old/SI_3D_registers.pdf) (p. 229) and [CIK](https://xorg.freedesktop.org/docs/AMD/old/CIK_3D_registers_v2.pdf) (p. 238) register references ("the order of components after blending and before COMP_SWAP is applied"). RADV builds `CB_BLENDn_CONTROL` without regard to the swap ([radv_pipeline_graphics.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_pipeline_graphics.c#L650-L709)) and renders R4G4B4A4, B4G4R4A4, R5G5B5A1 and B5G5R5A1 with `SWAP_STD_REV` and `SWAP_ALT_REV` ([vk_format.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/vulkan/util/vk_format.c#L43-L48), [ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c#L609-L624)). Mesa CI's Navi10 (gfx1010) CTS run ([job 112381553](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381553), [112381554](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381554)) passes all 400 `blend.format..states` cases of each of those formats, including separate alpha and destination-alpha factors. It is not measured on gfx1013 or on 8_8_8_8 and 16_16_16_16 targets. Factor pairs Vulkan applies per channel are used directly. Otherwise the pixel shader only copies values: it writes the exported alpha, each channel's own export or the constants 0 and 1 into a second color at location 0 index 1 (and 1 into a channel of the first color whose source factor is ONE, moving its export into the factor), and the blend forms every factor, `1 -` and product from those copies through `SRC1_COLOR`/`SRC1_ALPHA` factors. The blend unit therefore rounds them as it rounds a plain Vulkan blend of the same export; Vulkan does not require a second-source color to be converted like the first, but on an RTX 5080 and lavapipe every reversed-order case of the execution test is bit-identical to the same draw into a `SWAP_STD` target, which the test checks. Factor combinations that copies cannot express (for example `ONE_MINUS_SRC_ALPHA` in the alpha equation with `SRC_COLOR` in the color equation), sRGB, SNORM and integer targets, constant and second-source factors, destination-alpha and `SRC_ALPHA_SATURATE` factors in the color equation, different color and alpha operations, MIN or MAX with factors other than ONE, other color exports when the blend needs the second color, and devices without `dualSrcBlend` throw. Gran Turismo 7 (PPSA01317) blends `CB_BLEND0_CONTROL` 0x65010504 into such a target (#4782). - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. -- [DB_SHADER_CONTROL.STENCIL_TEST_VAL_EXPORT_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) becomes `FragStencilRefEXT`, taken from the second MRTZ channel of the 32_GR and 32_ABGR formats as Mesa's AMD drivers write it ([`emit_ps_mrtz_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L194)); the console was not measured. The host value is the test value of both faces and what `REPLACE_TEST` writes, so a stencil operation that writes all ones or the operation value throws with it, as do a stencil clear draw, the UINT16_ABGR layout, `STENCIL_OP_VAL_EXPORT_ENABLE` and a device without `VK_EXT_shader_stencil_export` (NVIDIA's driver 610.57 offers none). +- [DB_SHADER_CONTROL.STENCIL_TEST_VAL_EXPORT_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) becomes `FragStencilRefEXT` when available, taken from the second MRTZ channel of the 32_GR and 32_ABGR formats as Mesa's AMD drivers write it ([`emit_ps_mrtz_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L194)); the console was not measured. The host value is the test value of both faces and what `REPLACE_TEST` writes, so a stencil operation that writes all ones or the operation value throws with it, as do a stencil clear draw and the UINT16_ABGR layout. A device without the extension uses the bounded replay described above. On such a device a draw first drops the export when the reference cannot change its result (`StencilReferenceUnused`: no stencil test or attachment, or on both faces a compare that ignores it and no reachable `REPLACE` through a non-zero write mask), and its pixel shader is then compiled at its first draw, not when its ABI is resolved. A device with the extension keeps the export, so each such shader keeps one pixel variant. +- [DB_SHADER_CONTROL.STENCIL_OP_VAL_EXPORT_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - the pixel shader supplies the stencil operation source value per pixel, in place of `DB_STENCILREFMASK.STENCILOPVAL`. Only the operations that take that value read it: `REPLACE_OP` and the clamped and wrapping add and subtract (codes 4, 5, 6, 8 and 9), and the logic operations, which throw already ([AMD_stencil_operation_extended](https://registry.khronos.org/OpenGL/extensions/AMD/AMD_stencil_operation_extended.txt), [AMD_shader_stencil_value_export](https://registry.khronos.org/OpenGL/extensions/AMD/AMD_shader_stencil_value_export.txt)). Vulkan has no per-pixel operation value. Without `STENCIL_TEST_VAL_EXPORT_ENABLE`, a `REPLACE_OP` reachable through a non-zero write mask (`StencilOperationValueReplaces`) becomes `REPLACE` with the exported value as `FragStencilRefEXT`, or the bounded replay without the extension; this requires a compare that ignores the test value and no `ONES` or `REPLACE_TEST` on either face, and early tests throw. The value is taken from bits 8-15 of the second MRTZ channel: the PPSA27836 pixel shader that uses it shifts it left by 8 before `exp mrtz` (`v_lshlrev_b32 v0, 8, v0`), which is the only evidence for the layout. The add and subtract operations, and `REPLACE_OP` with a test value export, throw. With the 32_GR or 32_ABGR Z format and no such operation, or without a stencil test, the export cannot change the result and is ignored. Other Z formats throw. The console was not measured. - [DB_SHADER_CONTROL.DEPTH_BEFORE_SHADER](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `EarlyFragmentTests`, and so does `Z_ORDER` `EARLY_Z_THEN_LATE_Z` or `EARLY_Z_THEN_RE_Z` without kill, Z export, mask export or stencil export; with alpha-to-coverage the tests stay late. The meaning of the bit and of `Z_ORDER` 3 follows Mesa (`src/amd/registers/gfx10.json`; RADV sets the bit for `early_fragment_tests` in `src/amd/vulkan/radv_shader.c`). What the console does with `DEPTH_BEFORE_SHADER` and a Z export, a mask export, a stencil export or alpha-to-coverage together was not measured. -- [PA_SC_CONSERVATIVE_RASTERIZATION_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 0x6001 uses Vulkan overestimation on devices that add at most 1/256 pixel and keep degenerate triangles; the console's uncertainty region and degenerate handling are unmeasured. A value with `OVER_RAST_ENABLE` and `UNDER_RAST_ENABLE` clear is decoded as disabled even when `UNCERTAINTY_REGION_MODE` or the `OUTER_`/`INNER_UNCERTAINTY_EDGERULE_OVERRIDE` bits (19:16) are set, as PPSA01474 writes 0xe6000; that these fields have no effect without over- or underestimation is assumed from their names, not measured. +- [PA_SC_CONSERVATIVE_RASTERIZATION_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 0x6001 uses Vulkan overestimation on devices that add at most 1/256 pixel and keep degenerate triangles; the console's uncertainty region and degenerate handling are unmeasured. A value with `OVER_RAST_ENABLE` and `UNDER_RAST_ENABLE` clear is decoded as disabled even when `UNCERTAINTY_REGION_MODE` or the `OUTER_`/`INNER_UNCERTAINTY_EDGERULE_OVERRIDE` bits (19:16) are set, as PPSA01474 writes 0xe6000; that these fields have no effect without over- or underestimation is assumed from their names, not measured. With `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 the `PERSP_CENTROID` and `LINEAR_CENTROID` inputs get the pixel-centre barycentrics, as without conservative rasterization ([shader debt](shader.md)): the one rasterized sample sits at the pixel centre, so every centroid the rasterizer could pick for an overestimated pixel is that point; that it does not move the centroid towards the primitive when the sample is uncovered is inferred, not measured. With more rasterized samples, and when a single-sample draw is raised to a multisampled color target, centroid inputs throw. - [Descriptor pool chain](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page -- [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, a cleared tile of a DCC target and a fast clear of a 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). +- [CMASK clear pass on AMD without pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp) (libSceAgcDriver) - the compute pass that stores a fast-cleared CMASK into a resident render target (`DispatchCmaskClear`) is unverified on AMD's Windows driver with `APS5_NO_GPL=1`: on a Radeon 860M (driver 32.0.13058.8001, Windows 11) `agc_driver_cmask_fast_clear` keeps the stale 0x5A5A5A5A in its `SW_64KB_R_X` pass instead of the clear value, while it passes with pipeline libraries on and on an NVIDIA RTX 4080 SUPER in both modes. An RX 6400 (driver 32.0.21033.3005) lost the device (read invalid at 0xbeefdeadc000) in an earlier build tree, but clean rebuilds of four main commits of 19 October pass there (three of three runs each), so that failure is not reproduced. The cause is not found (#5150). Closes when: the cause is found, or `ctest -R '^agc_driver_cmask_fast_clear$' --repeat until-fail:5` passes with `ANYPS5_GPU=Radeon` and `APS5_NO_GPL=1` on AMD. - [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. When the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes the dispatch instead (an output descriptor of one 32-bit value per element, a value descriptor that covers the loaded dword, a 16-byte aligned range), the value is read again when the fill is recorded, after the labels queued before it, and the HTILE fill is noted with that value. -- [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import where they fill a whole 8 MiB aligned window; ranges of a partly filled window, and ranges larger than the window, are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound exactly over adjacent ranges of any size (from the first one's start to the last one's end), joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer is bound exactly over all of its pieces. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. -- [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. +- [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words over its whole pipe-aligned metadata (`HtileMetadataBytes`, the extent `HtileFillCovers` takes) are read, outside the surfaces mutex since the read waits for recorded work: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. PPSA25344 (Unreal Engine 5) reads its HTILE after the depth prepass in its Nanite depth export, which takes a tile with ZMask 0 as holding the clear value; without the expanded words the export put Nanite depth over the prepass and the Nanite material pass painted over the meshes in front. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words. A recognized HTILE fill or clear makes the next bind read the words again. A slice other than 0 of a depth array is read at its own HTILE (`DB_HTILE_DATA_BASE` plus the slice times `HtileMetadataBytes`) but the HTILE word layout of slices 1 and up is not modelled (PPSA05684's tile masks need X8 and Y8 to take Z0 and Z1 as well, slices 4 and up unmeasured), which neither the check that every word is cleared nor the store-back of each word in place needs, so such a slice takes the clear like slice 0. A depth-only HTILE whose tiles all have ZMask 0 but different Min/Max Z (per-tile clear values, PPSA05684's cascades) is taken as a whole-surface clear to `DB_DEPTH_CLEAR`, the value the DB's depth test reads for a cleared tile (`HTILE_Z_CLEAR_REG` in Mesa's `src/amd/common/ac_descriptors.h`: the clear word "only clears HiZ", "the DB register contain[s] the full clear values"); the HiZ test before it, which rejects against each tile's own Min/Max Z range (the HTILE definitions in the same file), is not modelled, so a draw that HiZ would reject over a tile cleared with Min/Max Z 0 is tested against `DB_DEPTH_CLEAR` instead. A multisampled depth target, and any target whose HTILE overlaps a resident native depth guard, skips this read and store-back and goes to the native multisample depth owner, which keeps its own refusal for a fast-clear transition. +- [RADV fill-memory kernel](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) (libSceAgcDriver) - the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes RADV's 16-byte `fill_memory` compute kernel (`MatchClampedGlobalFill`: 11 GFX10.3 words, 64x1x1 groups, six user SGPRs with the address in `user[2..3]`, the largest store offset in `user[4]` and the value in `user[5]`; each thread stores 16 bytes at `min(thread * 16, user[4])`) as a fill of `min(threads * 16, user[4] + 16)` bytes. RADV initializes and fast-clears DCC keys and other metadata with it, so the key and metadata tracking sees such a fill where it is in the command stream; run as a dispatch, its global stores name no range, and a key fill was only seen once it had landed, after later writes to the surface. Only a 16-byte aligned address and largest offset are taken. RADV's 4-byte variant (fills below 16 bytes), other Mesa versions' encodings and other compilers still run as dispatches. The kernel words are the ones vkQuake's PS5 RADV build (Mesa 25.2) submitted; they are not compared with the NIR builder's output for other targets. +- [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import: the largest aligned power-of-two block, from twice the import alignment up to the 8 MiB window, that holds the requested address and that they fill exactly, so a block only grows by doubling as its window fills. A block that would exceed the budget, would replace an import a build holds, or fails (a refused Windows alias, `vkAllocateMemory`) steps down to the next smaller block; a failed block is not tried again until one of its ranges changes. Ranges larger than the window are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound exactly over adjacent ranges of any size (from the first one's start to the last one's end), joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer is bound exactly over all of its pieces, or when a sparse descriptor heap over it is bound: a heap a shader binds whole while only the pieces from its base are mapped is bound in place from the import of that run, its views ending at the import's end as the copy's do, once the previous bind of that address saw the same last mapped byte (a heap that grows between every two of its binds stays copied; one bound more often than it grows is joined again after each growth) and at most 64 times per joined run base; past that, with a message, it is not joined again: it is bound in place while an existing import covers its run and copied otherwise, until no registered range starts at that base or the device changes. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. +- [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces still throw: hardware load bounds and filtered-sample addressing differ. A range of six faces or more that does not end on a whole cube keeps every slice up to `DEPTH`, the bound loads use on hardware; sampling the cube it cuts short is out-of-range cube-array sampling. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. +- [Compressed FMASK reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) without an exact resident native owner throw. Native reads require eight fragments, one slice and mip, pipe-aligned DCC/CMASK, Z_64KB_X FMASK and unchanged, disjoint watched planes. The host stores independent samples and exposes their expanded indices (`0x76543210`, AMD PAL `c5e80007`, `Gfx9Fmask::GetPackedExpandedValue`); console equivalence is unmeasured. Other layouts, standalone compressed decoding, guest publication and recipe reuse remain unsupported. Metadata writes invalidate the native view, including same-value stores. - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. - [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. -- [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve is modeled. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. -- [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. -- [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. -- [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid throw. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. +- [Multisampled texture no draw rendered to](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - sampling a multisampled texture whose surface no draw rendered to creates the host target from the T# and clears it to the color a single 32-bit value repeated over the whole footprint encodes, padding up to whole tile blocks included. Inferred, not measured on the console: that the samples of such a surface are the guest color memory, as a title that clears its canvas on the CPU (ps5-opengl `ps5_clear_msaa4_color`) relies on. Memory that is not one repeated value, tile modes other than Standard4KB, Standard64KB and RenderTarget, unmapped memory and values with no exact clear color throw. CPU writes to the surface after its host target exists are not seen by the host. +- [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. +- [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, index buffer read, fixed-function vertex buffer read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Live targets remain cached per device, surface address, format, extent and sample count. Retired cache owners are removed on the next target, sampled or storage lookup; outstanding recorded work and sampled views retain their images. Ordinary sampled and storage bindings check the color-target guard before resource reuse, including precollected sampled lookups. Reused image tables do not invalidate on multisample color-target registration yet; their per-element fault handling and native multisample sampling require a separate reuse proof. +- [DB depth copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `DB_RENDER_CONTROL.DEPTH_COPY` is run as a compute pass that copies one sample of the `Z_32_FLOAT` depth surface into a single-sample R32 float color target over the whole surface. With `COPY_CENTROID` the sample is the first one the draw covers, which is sample 0 under the full `PA_SC_AA_MASK` the driver requires: [radeonsi copies one sample per draw with `COPY_CENTROID` and a one-sample mask](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state.c#L1679-1681); the console is not measured. Stencil copies, depth tests, other depth formats, multisampled or other-format targets and copies over part of the surface throw. +- [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. PA_SC_AA_CONFIG is only compared with CB0 while PA_SC_MODE_CNTL_0.MSAA_ENABLE is set: PPSA08270 resolves a 4x 512x512 target with MSAA_ENABLE off and PA_SC_AA_CONFIG still at 2x (0x00108001) from earlier draws. +- [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2. On other devices every pixel takes the pattern's pixel (0,0) locations (`PA_SC_AA_SAMPLE_LOCS_PIXEL_X0Y0_*`, its first registers and the corner the grid is anchored at), and the first such pipeline of each sample count logs `[gpu] no 2x2 sample location grid`. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each count, per count and for every count; an RTX 3070 Laptop GPU (driver 617.42) reports `maxSampleLocationGridSize` 1x1 for every count (`VkPhysicalDeviceSampleLocationsPropertiesEXT`), but per count (`VkMultisamplePropertiesEXT`, which the device setup reads and logs) a 2x4 grid at 2 samples, 2x2 at 4 and 1x2 at 8. So the 3070's 2x patterns, the 2x checkerboard among them, take the native grid, and the stand-in plus the exact path serve its 8-sample patterns and devices without a grid. A pattern that only reorders one set of positions between pixels, as a 2x checkerboard pattern puts its samples at x = +4/16 and -4/16 and swaps them on odd rows, is drawn exactly through that stand-in: before every render pass that draws with it each pixel's samples are put in the stand-in's order and after it back in the guest's, through a storage view of a multisampled color target and through a copy and a per-sample pass for depth (writing stencil through VK_EXT_shader_stencil_export, or one pass per bit without it), while the pixel shader reads SampleID, applies the sample mask and exports coverage in the guest's order, and the second log line names the reordering. A uniform partial sample mask that the order moves is applied in the pixel shader. With early Z, a depth or stencil test and memory writes in the pixel program, the draw throws, where #6451's stand-in alone ran it with odd rows' sample indices swapped. A fix would choose the stand-in after compilation. That costs two passes over each multisampled attachment per render pass, and on those devices every multisampled color target is created with a mutable format and storage usage for its view, which a driver may answer by not compressing it. It needs shaderStorageImageMultisample with a storage view of the color format at the sample count, sampleRateShading, and a depth format a FragDepth write gives back exactly (16-bit, or 32-bit float; not 24-bit depth, the stand-in for 16-bit depth with stencil where the device has neither); without them, and for patterns whose pixels hold different positions, the stand-in alone is used, and the log line says why. A device without shaderStorageImageMultisample takes the stand-in alone for every pass, depth-only passes included, so a depth target that a depth-only pass and a color pass both draw keeps one order. On a device that has it, a pass is still refused for a color format without a storage view at its sample count, while a depth-only pass on the same depth target is reordered, so that target's samples change order between those passes; a fix would decide the order per target. Then the top-left pixel of every 2x2 quad matches the console and the other three use its positions. A same-set pattern keeps every position and the number of samples a primitive covers in each pixel, so a resolve that averages a pixel's samples, or takes their minimum or maximum, sees the console's values, but sample i of an odd row holds what the console stores in sample 1 - i: a pass that reads the samples by index, as checkerboard reconstruction does, exchanges the two half-pixels of odd rows, which can show as combing along vertical edges, and per-sample shading, sample-mask exports and coverage inputs see the swapped indices too. A pattern whose pixels hold different positions draws pixel (0,0)'s positions in each pixel whose own differ. A device's pipelines and render pass begins take the same stand-in for a given pattern, so a sample-location-compatible depth image is not drawn with one set of locations and transitioned or tested with another. A multisampled rasterizer drawing into one-sample color targets, and a one-sample depth target if any, with MSAA_ENABLE set, no alpha-to-coverage and no coverage pixel input is drawn with one sample at sample 0's location in each pixel of the grid, as the targets keep only sample 0's coverage. Without PS_ITER_SAMPLE such a draw also refuses centroid inputs, and keeps PERSP_SAMPLE, LINEAR_SAMPLE and ANCILLARY: a pixel shaded once is taken to get sample 0's barycentrics and sample index 0. With PS_ITER_SAMPLE only sample 0's invocation reaches the targets, so its sample and centroid barycentrics are taken to be sample 0's and the draw is shaded once. Both are inferred, not measured. The pixel shader runs once per pixel with sample number 0, so ANCILLARY reads sample 0 and POS_X_FLOAT and POS_Y_FLOAT keep the position `SPI_BARYC_CNTL` names. PERSP_CENTER and LINEAR_CENTER inputs on this path are interpolated at sample 0's location on the host, where the console uses the pixel center; one-sample depth images are created with `SAMPLE_LOCATIONS_COMPATIBLE_DEPTH` when the device supports it, as multisampled ones already were; this is how VK_AMD_mixed_attachment_samples defines the case, and it is not measured on PS5 hardware. Other mixed sample counts throw. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. A pass that draws depth at custom sample locations ends with a transition to TRANSFER_SRC_OPTIMAL and back that names them. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. +- [Single-sample scan into a multisampled depth target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 and `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear into a multisampled depth target rasterizes the depth target's sample count with every sample at the pixel center, as a single-sample scan into a multisampled color target already does. radeonsi programs this `MSAA_ENABLE` and `PA_SC_AA_CONFIG` with the depth buffer's own sample count when GL multisampling is disabled on a multisampled framebuffer ([L1066-L1068](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L1066-L1068), [L3485-L3512](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3485-L3512)), although its comment lists Z samples as at most the coverage samples ([L3439](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3439)) and it sets `DB_EQAA.MAX_ANCHOR_SAMPLES` to the Z sample count, where PPSA18216 sets `DB_EQAA` to 0. GL then lets every sample take the single-sample depth ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190)) and the fragment be treated as in multisampling with full coverage ([L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)), which is what this draws: each depth and stencil sample is tested and written with the depth at the pixel center. GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, whether it instead evaluates the depth plane at each sample position, and how its occlusion queries count such a draw, is not measured on hardware. Points, lines, alpha-to-coverage, `DB_EQAA` Z interpolation or overrasterization, a pixel program reading the sample index and a sample-mask export throw +- [One-sample color targets beside a multisampled depth target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a one-sample color target drawn with a depth target of the rasterizer's sample count is taken to keep the coverage of sample 0 after the depth and stencil tests. That is the rule of [`VK_AMD_mixed_attachment_samples`](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/fragops.adoc#L2510-L2516), and it is not measured on hardware. The draw needs `VK_AMD_mixed_attachment_samples`, or `VK_NV_framebuffer_mixed_samples` with truncated coverage reduction, for its sample count and throws without them. With `PS_ITER_SAMPLE` the color is the output of sample 0's invocation. Overrasterization and color targets of different sample counts throw. - [DCC display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. -- [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. +- [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. A range a queued draw commit still has to read is left pending whatever the stamps say ([`DrawPipeline::HeldReads`](../../../core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp)), so a title that recycles one allocation between an index buffer and a storage image sees the draw's own indices; the trade is a title that reads a surface as index data and means the image gets the pre-image bytes instead, since the reservation does not compare the command-stream positions of the draw and of the image's write. `APS5_NO_QUEUED_DRAW_READ_ORDER=1` restores the old behaviour, and the `[storage]` line counts the listings the hold turned away (`held for a queued draw read`) and the `[draw] pipelined` line the stores it held. The hold covers what the commit reads of guest memory: the index range, the vertex input ranges as their descriptor extents ([`QueuedDrawReadRanges`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp), since the bytes a draw fetches depend on the highest index, which only the commit knows) and the source of a queued DMA copy. Every other commit-time read goes through the flush hook, so it sees the bytes the hold protects. Two commit-time reads are outside the hold: the texels a register or cmask clear materialization reads from the draw's own colour target (through the hook, so ordered, but the hold does not cover a render target), and a CPU read whose store the hold turned away, which sees the pre-image bytes. Nothing retries a refused store: the image reaches guest memory with the next flush of that range (the queue worker's idle store, a `WAIT_REG_MEM` poll over it, a later read or write of it). A kept 64 KiB unit that lies outside every (slice, level) window stores nothing: the tracking grid is flat over the guest range while 3D slices taper in depth with the mip level (`HasLayer`), so the higher depth slices' higher-level blocks belong to no window. The console's tracking of such units is not measured. - [16-bit float display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws. -- [BT.2100 PQ display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0x8100070422000000` and `0x8100070400000000` are taken as 10:10:10:2 texels with BT.2020 primaries and the SMPTE ST 2084 transfer, red or blue in the low bits, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgb10A2Bt2100Pq`, `Bgr10A2Bt2100Pq`) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps the first; PPSA01317 registers display buffers of the first when its output reports HDR. The output is SDR, so they are converted to 8-bit sRGB: ST 2084 EOTF, BT.2020 to BT.709 primaries with negative components clipped, a luminance tone map and the sRGB encoding. The tone map is ours: min(203 nits, white level) is SDR white (the BT.2408 HDR reference white), luminance passes unchanged up to 0.75 of it and an extended Reinhard shoulder above reaches 1.0 at the white level, the tone-map luminance sceSystemServiceGetHdrToneMapLuminance reports. How a console shows such a buffer on an SDR display is unknown. Alpha is opaque. +- [BT.2100 PQ display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0x8100070422000000` and `0x8100070400000000` are taken as 10:10:10:2 texels with BT.2020 primaries and the SMPTE ST 2084 transfer, red or blue in the low bits, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgb10A2Bt2100Pq`, `Bgr10A2Bt2100Pq`) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps the first; PPSA01317 registers display buffers of the first when its output reports HDR. With HDR output they are presented unchanged through an HDR10 swapchain (`VK_COLOR_SPACE_HDR10_ST2084_EXT`, 10-bit, with `VK_EXT_hdr_metadata`); otherwise they are converted to 8-bit sRGB: ST 2084 EOTF, BT.2020 to BT.709 primaries with negative components clipped, a luminance tone map and the sRGB encoding. The tone map is ours: min(203 nits, white level) is SDR white (the BT.2408 HDR reference white), luminance passes unchanged up to 0.75 of it and an extended Reinhard shoulder above reaches 1.0 at the white level, the peak sceSystemServiceGetHdrToneMapLuminance last reported to the title (or the current one when it never asked), since titles read it once. How a console shows such a buffer on an SDR display is unknown. Alpha is opaque. +- [HDR output override](../../../core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp) - setting `APS5_FORCE_SDR` suppresses HDR availability reported to titles while retaining the detected display and surface capabilities. It selects the existing SDR presentation path; it does not add support for floating-point BT.2100 PQ buffers (`0xC001070722000000` and `0xC001070700000000`), whose presentation remains refused. The override does not change the host display settings. - [DCC key stores](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) cover the console's DCC extent only for pipe-aligned DCC of single-level, single-layer 4-byte SW_64KB_R_X surfaces; other surfaces, and every texture and color target key read, cover one byte per 256 bytes. - [DCC key fills over other images](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) - a uniform fill at a live image's `dccAddress`, at least one key per 256 bytes long, is a key fill of that image even when other live images overlap it, but only while it is no longer than the image's key extent (`DccKeyCount`, the pipe-aligned count); a longer one is classified by its overlaps as before. The bound is inferred, not measured on a title: a longer fill also writes the bytes past the keys, which the overlapping image may own. Whether the title's bloom-target key fills stay within the extent was not checked (`APS5_TRACE_DCC_KEYS` prints the length of the fills classified as keys, not of the refused ones). - [Comp-to-single DCC keys](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) (libSceAgcDriver) - key 0x10 (`GFX9_DCC_CLEAR_SINGLE` in Mesa `ac_descriptors.h`) uniform over a surface makes every texel the element at the start of its 256-byte block, the top-left texel of the block where radv's `build_clear_dcc_comp_to_single_shader` stores the clear color. Sampled textures and storage images read that expansion (a storage image's first write stores the keys uncompressed, as under a clear code), and a CB DCC decompress pass stores it over the texels. A fast-clear eliminate over such keys throws, as a metadata pass over mixed keys does; color targets that render over such keys use their stored texels, and textures read mixed keys, and comp-to-single keys of a format whose element does not divide 256 bytes, as stored. "Uniform" is judged over the key bytes the driver reads, one per 256 surface bytes (see DCC key stores above), so a pipe-aligned surface whose keys differ past that range is expanded as if all were comp-to-single, as for the other clear codes. Not verified on hardware - [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. -- [INDEX_ATTRIBUTES_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x91) loads INDEX_BASE, INDEX_BUFFER_SIZE (in indices) and INDEX_TYPE from the 16-byte record at `attribute_base + attribute_index * 16`, read when the packet runs. The record layout (address low, address high, size, type) is PAL's [`IndexAttribIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Chip.h#L60-L67) and the packet is the one [`BuildIndexAttributesIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9CmdUtil.cpp#L2586-L2613) writes; neither is measured on a PS5 or seen in a title. `indirect_mode` 1 (`addr_offset`, which only PAL's ExecuteIndirect path emits) throws, and so does a record whose index type is above 3, as INDEX_TYPE does. -- [16-bit RGBA color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support the standard and the alternate (`COMP_SWAP` 1) component order. Alternate swaps red and blue in shader exports, write masks and constant-color blend factors, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c). The reverse orders (`COMP_SWAP` 2 and 3) keep throwing when blending, and simultaneous targets requiring conflicting blend constants throw. Console equivalence has not been measured on hardware. +- [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. On a PS5 (firmware 13.40, measured 2026-10-19 on the GPU through `sceAgcDriverSubmitDcb` from inside a retail title) `SRC_SEL` 9 and the `RELEASE_MEM` DATA_SEL 3 timestamp were the same counter (0x1C and 0x5C ticks apart within one buffer), and it ran at 98.0 and 99.8 MHz over two 1.1 s intervals timed by the CPU, i.e. 100 MHz within the submission jitter, as the 10 ns ticks here; only its starting value is the host's. The `COPY_DATA` cache policy fields are accepted and have no effect. +- [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. +- [EVENT_WRITE stream-out statistics events](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x46) - `SAMPLE_STREAMOUTSTATS1`-`3` and `SAMPLE_STREAMOUTSTATS` (event types 0x01-0x03 and 0x20 in [navi10_enum.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/include/navi10_enum.h#L14155-L14188)) are accepted on the graphics queue as a 2-dword packet with event index 0 and do nothing. [nvd.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/amdgpu/nvd.h#L326-L347) documents these events only with the sampling index (3, or `EVENT_INDEX__SAMPLE_STREAMOUTSTATS*` 8-11 on gfx10); the index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured on hardware. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. The sampling indices throw that sampling to memory is not implemented, and other indices, other sizes and the compute queue throw. [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address builds the index-0 packet: nothing is written and nothing throws. Whether the console's builder emits a sampling packet for an address is not known. +- [INDEX_ATTRIBUTES_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x91) loads INDEX_BASE, INDEX_BUFFER_SIZE (in indices) and INDEX_TYPE from the 16-byte record at `attribute_base + attribute_index * 16`, read when the packet runs. The record layout (address low, address high, size, type) is PAL's [`IndexAttribIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Chip.h#L60-L67) and the packet is the one [`BuildIndexAttributesIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9CmdUtil.cpp#L2586-L2613) writes. The console's `sceAgcDcbSetIndexIndirectArgs` writes this packet, with `indirect_mode` 0, the attribute base in the address dwords and the attribute index in bits 0-15 ([measured](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L172-L174) by the prosper console oracle); the record layout is not measured on a PS5, and neither is seen in a title. `indirect_mode` 1 (`addr_offset`, which only PAL's ExecuteIndirect path emits) throws, and so does a record whose index type is above 3, as INDEX_TYPE does. +- [16-bit RGBA color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support the standard and the alternate (`COMP_SWAP` 1) component order. Alternate swaps red and blue in shader exports, write masks and constant-color blend factors, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c). Blending into the reverse orders (`COMP_SWAP` 2 and 3) is covered by the reversed-order blending entry, and simultaneous targets requiring conflicting blend constants throw. Console equivalence has not been measured on hardware. - [UINT16_ABGR color exports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - export format 7 is accepted only into an unsigned integer target, and each 16-bit value reaches the target unchanged; a value beyond a narrower target's range (`R8_UINT`) is undefined in Vulkan, and whether the console clamps or truncates it is not confirmed. SINT16_ABGR (8) throws -- [Layered color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - only 3D color targets with 64 KiB render-target or 4 KiB standard tiling render several slices: a `CB_COLOR_VIEW` slice range on an array target, a layered draw with a depth target, and color targets of different slice counts throw. The vertex layer export (`PA_CL_VS_OUT_CNTL` USE_VTX_RENDER_TARGET_INDX) reaches the shader only for a layered draw and is otherwise ignored, as before; a layer beyond the view's last slice is undefined in Vulkan, while the console's handling (clamping to `SLICE_MAX`) is not modeled, and a view whose last slice lies past the surface is clamped to its last slice. DCC keys of a 3D color target are taken as one key per 256 bytes over the whole surface, every slice included, and only uniform keys (all uncompressed, or one fast-clear code) are interpreted, as for 2D targets; mixed keys leave the stored texels as they are. The console's DCC key layout for 3D surfaces is not checked (LEGO Horizon Adventures' (PPSA14632) lighting volumes are DCC) +- [Layered color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 3D color targets with 64 KiB render-target or 4 KiB standard tiling render several slices, and so does a `CB_COLOR_VIEW` slice range on a 2D array target through the array's resident image, or on a surface of one slice as an array of the view's last slice + 1 slices, one slice pitch apart: a layered draw into an array without its resident image, a layered draw with a depth target, and color targets of different slice counts throw. The vertex layer export (`PA_CL_VS_OUT_CNTL` USE_VTX_RENDER_TARGET_INDX) reaches the shader only for a layered draw and is otherwise ignored, as before; a layer beyond the view's last slice is undefined in Vulkan, while the console's handling (clamping to `SLICE_MAX`) is not modeled, and a view whose last slice lies past the surface is clamped to its last slice. DCC keys of a 3D color target are taken as one key per 256 bytes over the whole surface, every slice included, and only uniform keys (all uncompressed, or one fast-clear code) are interpreted, as for 2D targets; mixed keys leave the stored texels as they are. The console's DCC key layout for 3D surfaces is not checked (LEGO Horizon Adventures' (PPSA14632) lighting volumes are DCC) - [VGT_TF_PARAM](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - DISTRIBUTION_MODE is taken as scheduling only; the host tessellator decides the order of a patch's triangles. - [Local programs](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) that the hull header leaves at address 0 are prepared at their first draw, one artifact per tessellation configuration, instead of at registration. - [Packed 16-bit textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) - `5_6_5`, `1_5_5_5` and `4_4_4_4` are viewed as R5G6B5, A1R5G5B5 and B4G4R4A4 (the 4-bit format every Vulkan device can sample; R4G4B4A4 is optional and lavapipe lacks it) with a swizzle that reads X from the low bits, and a DST_SEL of the missing W of `5_6_5` reads X. Measured on RDNA2 (gfx1035), not on the console: through `image_load` and `image_sample_lz`, X, Y, Z (and W of `1_5_5_5` and `4_4_4_4`) come from the low, middle and high fields, and W of `5_6_5` returns X (the low 5 bits), not 1. - [Min/max S# reduction](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction (the same `SQ_IMG_FILTER_MODE` values RADV programs). Measured on RDNA2 (gfx1035), not on the console: only texels of non-zero weight are reduced, and the weight is taken after rounding to the 8-bit subtexel grid (a sample within 1/512 texel of a centre reduces that texel alone); a driver with other `subTexelPrecisionBits` reduces a different set there. Gathers ignore the mode. -- [Min/max S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with anisotropy, a linear mip filter, depth or color-texture compare, or bilinear filtering of an image the recompiler point-samples (sint, converted, depth-bits). -- [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). +- [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) - not measured on the console. A sampler that forces sRGB decoding throws for an SNORM `8`, `8_8` or `8_8_8_8` texture, which RDNA2 (gfx1031) reads bit for bit as the sRGB format of the same bytes, and for a format not measured on RDNA2. The rest follows gfx1035 and gfx1031 (point and bilinear): sRGB textures sample as without the bit, as in shadPS4 (PS4); UNORM `8`, `8_8`, `8_8_8_8`, BC1, BC2, BC3 and BC7 sample through their sRGB view (decoded before filtering); `16`, `16_16` and `16_16_16_16` UNORM, SNORM and FLOAT, `32`, `32_32` and `32_32_32_32` FLOAT, `10_11_11`, `2_10_10_10`, `5_9_9_9`, `5_6_5`, `1_5_5_5`, `4_4_4_4`, BC4, BC5 and BC6H sample as without the bit, as gfx1031 does. - [Tessellation factor ring](../../../core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp) (libSceAgcDriver) - `sceAgcDriverSetTFRing` records the ring and `sceAgcDriverGetTFRing` reports it. Before a title sets one, it reports the console's default, 0xFF0000000 and 128 KiB, as measured in [PS5_Vulkan's hardware findings](https://github.com/mihawk-99/PS5_Vulkan/blob/fde9e3798c2ec1d8fccc90d5067cd6060490607f/docs/HARDWARE_FINDINGS.md). Nothing is written to the recorded ring: tessellated draws run as Vulkan tessellation stages, which keep their factors on the host, so a title that reads tessellation factors from the ring finds none. A null base or a zero size throws; [ps5-opengl's AGC runtime](https://github.com/blackbearreloaded/ps5-opengl/blob/67c873f985eaefe0a72f713529e5632eaa01139c/src/platform/ps5_agc_native_runtime.c) calls `sceAgcDriverSetTFRing(0, 0)` in its cleanup, and what that does on the console is unknown. - [VertexBufferReadSize](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) and the [vertex staging copy](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a vertex fetch whose index is at or past the descriptor's `num_records` no longer throws for `OOB_SELECT` 0 and 1. The copy still spans the whole fetch, but the bytes from `num_records * stride` onward read as zeros, so the out-of-range vertex is `(0, 0, 0, 0)`. Measured with `buffer_load_format_xyzw` (`idxen`) through the [hardware oracle](../HW_ORACLE.md) on RDNA2 gfx1036 for PPSA01325's word 3 `0x0004dfac` (`OOB_SELECT` 0) in #2241: indices 3, 4 and 5 over 3 records each return `00000000 00000000 00000000 00000000`, W included, unlike Vulkan `robustBufferAccess`, which returns `(0, 0, 0, 1)`. The zeros come from the record check, not from memory: stored values placed past the records are ignored, and a `num_records` 6 control returns them. `RESOURCE_LEVEL` 1, `OOB_SELECT` 1 and FORMAT 74 `32_32_32_FLOAT` give the same zeros, in wave32 and wave64; gfx1013 (the console) is not measured. `OOB_SELECT` 2 returns the stored values past the records and `OOB_SELECT` 3 compares `num_records` against the byte offset, both measured on the same setup in #2241; the two keep throwing past `num_records` here, since each needs its own rule. Zero-stride descriptors (`stride` 0) keep their separate byte-count check and are not covered here. Each out-of-range fetch is copied alone and outside the draw-snapshot cache, which is neither read nor written for it: a reused snapshot would carry another draw's bytes into the zero region, and a stored zero-padded copy would hand zeros to a later in-range draw at the same address. The valid prefix is still flushed for GPU writes before it is read. - [Draw snapshot buffer descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct. -- [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; in the shaders seen they are metadata, and a caller that reads its program counter, which could address them, 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. +- 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. +- An [image-table T#/S# pair](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) 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. +- [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. A selected column's range is its record and the bytes of the record before the descriptor. +- An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) T# whose surface has no mapped byte faults when selected instead of becoming a zero texture as a direct T# does; what the console returns for it is not measured. +- [User-data callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped from the recompiled code; in the shaders seen they are metadata or constants the caller reads relative to its program counter, and those reads go to the captured guest code, which linking does not change. A caller that jumps through a table among those words, which the control flow analysis reads from the recompiled code, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code. +- [Table callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is two consecutive dwords of an `s_buffer_load_dword*`, or a copy of them made with `s_mov_b32` or `s_mov_b64` in the same block (a conditional branch forward past the call between them is crossed), calls one of the targets that buffer holds when the load's buffer descriptor comes from an `s_load_dwordx4` without SOFFSET from a user-data pointer, or from a pointer pair that two `s_mov_b32` of a literal or inline constant set, and the load's SOFFSET from an `s_lshl_b32` by 2 to 24 bits. The descriptor load may be in an earlier block, such as before a per-lane loop around the call, when it is the only write of the descriptor registers that reaches the buffer load on every path back from it, and no `s_call_b64` or statically resolved call lies on those paths; the paths may run through any `s_swappc_b64` that is not resolved statically, the table call itself included, whose callee is assumed to keep the descriptor registers. The chain compares the target each iteration actually loads, so a callee that changes them can only make a later target miss the chain and fault as an unlinked call. Paths follow fall-through, direct branches and the `s_setpc_b64` jumps whose target the control flow analysis resolves. A path that reaches, before a write of the descriptor registers, the program's first instruction, the entry of an in-program function (an `s_call_b64` target, whose callers are not followed), or an instruction that nothing modelled jumps or falls into fails the search. At dispatch the descriptor and the buffer are read from guest memory, and every non-zero 64-bit value at the loaded offset plus a multiple of the shift's stride, below the descriptor's size (stride times num_records, or num_records bytes with stride 0), is read as a user-data callee and appended once per call. The call becomes a branch to a chain that compares the source pair with each target (`s_cmp_lg_u32` with a literal and `s_cbranch_scc1` per dword) and `s_call_b64`s the copy that matches; no match runs the original `s_swappc_b64` as an unlinked call. There is one chain for each value SCC has at the call, and the chain sets SCC back to that value (`s_cmp_eq_u32 0, 0` or `s_cmp_lg_u32 0, 0`) before the callee or the unlinked call runs. The searched range is the descriptor's whole size, which covers the range the load can reach whether it is checked in bytes or in strides; which one the PS5 uses for `s_buffer_load` was not measured. More than 64 distinct targets, a buffer above 64 MiB or a copy out of `s_call_b64` range throws. As with user-data callees, the buffer is read again only when a dispatch misses the dispatch cache, so a target a title adds to the buffer later is an unlinked call when a cached program runs. +- [AGC FS table](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp) (libSceAgcDriver) - the driver maps 2 MiB at 0xF_E0000000 when it is created and fills the table that libSceAgc keeps at 0xF_E0040000 (six 16-byte V# slots, slot 0 describing eight 16-byte records) the way PS5 12.00 libSceAgcDriver and libSceAgc do, but every record points at one AnyPS5 routine that only returns (`s_setpc_b64` on the link pair). Sony's routines, which order work before a title's `ds_ordered_count`, are not modeled. - [DB_RENDER_CONTROL resummarize draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing. A pure resummarize with pass-through D16/D32 depth, no depth writes or bounds test, no stencil plane, no color writes and no running pixel stage also draws nothing; other resummarize, copy or decompress draws throw. - [Comparison-sampled R32 float and R16 unorm textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. - [Null vertex buffer V#](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. - [Descriptor pool recycling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured. - [16-bit depth with stencil](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is stored as `D24_UNORM_S8_UINT`, or `D32_SFLOAT_S8_UINT`, on a device without `D16_UNORM_S8_UINT` as an attachment. Depth comparisons, clears and the depth image see the wider format's precision, so results at the 16-bit quantization boundary can differ from the console; the constant depth bias is scaled by 256 for D24, and a non-zero constant on the D32 float fallback throws. Not measured on console hardware. -- [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. -- [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. +- [Depth surfaces created over storage writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth surface created without HTILE copies its depth plane from the storage image at the plane's address, a pending one first, when that image has the plane's extent, one level, a 2D view or a 2D array view (its layer 0 is copied) and a 16-bit format for a 16-bit plane or a 32-bit format for a 32-bit plane. A multisampled surface, a surface the device stores in another format, a surface with HTILE and a surface without such an image start from `DB_DEPTH_CLEAR`, and the stencil plane from `DB_STENCIL_CLEAR`; CPU writes to the plane's memory are not read. Not measured on the console, where the plane is that memory. +- [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing while its own release interrupts keep arriving. The latch is cleared when the queue has flipped five times without another interrupt from its own releases: the fifth flip delivers the event and unlatched the queue, so a title that waits on the flip event after latching recovers only if it submits further flips. The bound cannot be by time: the next release interrupt can arrive seconds later than a suppressed flip across loading stalls, and delivering an unexpected flip event on a queue that pairs flips with releases can crash; no fixed window short enough to unblock a waiting title keeps such a title alive. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue. +- [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half that heap (imports, type-3 mirrors and buffers and textures placed in host memory share it; on a 16 GiB machine whose host heap is half the memory, a 2 GiB reserve left too little for the rest). Where the device enables `VK_EXT_memory_budget`, that host heap's reported budget less its other usage and an eighth, in 64 MiB steps, read at most once a second, also bounds the default budget once an import's `vkAllocateMemory` has failed; a set `APS5_HOST_IMPORT_MIB` is never lowered by the report. The report's other usage includes the host-visible copies and heap mirrors that stand in for refused imports, so it grows with the refusals; the first time the report would lower the budget, `[gpu] host import budget: host heap ...` prints the heap's reported budget and usage and the live imports. An import's `vkAllocateMemory` failure within an eighth of the budget, or the third in a row, lowers the budget to the live imports until the device or the configured budget changes; an isolated failure further below only marks its range failed. Live imports are not capped by `maxMemoryAllocationCount`: NVIDIA's Windows driver reports 4096, while Village held 27,167 live imports without a failure. A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A heap mirror, sparse or not, whose range has left the write watch is refreshed in full at each use, every block counted as written; a build plans its range again only once the import path takes the range. A request that may not evict (an address-based build, an import when a range is mapped) remembers a refused range or block, with the live bytes and the budget at the refusal, and is answered from that memory, still counted as refused, until live imports drop below those bytes, the budget changes or one of its ranges changes. A request that may evict remembers its refusal too, answering later requests for the same run from memory with the room the refusal left (a request whose range fits in that room steps down to a smaller block without scanning the imports again, and a refusal that stepped down is remembered for each block it tried), while no import was made or retired, the budget has not grown, the import window and the run are unchanged, and no import can have become evictable since: the use count stays below the first use at which an import that was neither evictable nor held could leave the 256-lookup protection, and either the build floor has not risen or no held import could leave it yet. A join remembers its refusal the same way, apart from other requests for the same run, so a buffer bound across refused ranges does not try the join again at every upload. The shutdown line `[gpu] host imports at shutdown` gives the live, made, refused (and remembered) and failed counts per requester. The 64 MiB window, the 256-lookup protection, the 2 GiB reserve, the eighth, the 64 MiB steps and the three failures are choices, not measured. +- [Periodic pipeline cache saves skip busy intervals](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp) (libSceAgcDriver) - the saver thread snapshots the Vulkan pipeline cache only when no pipeline creation holds it (`TryLockForSnapshot`); otherwise it skips that 10 s interval. A blocking snapshot queued behind a long creation (an NVIDIA compile of a 1.5M-word compute pipeline runs for minutes) makes the writer-preferring `shared_mutex` hold every later pipeline creation on every queue until that compile ends, which froze Yōtei's (PPSA26344) loading at frame 797-799 for the whole compile. While creations overlap without a gap, no periodic save happens, so a crash in that stretch loses the pipelines built since the last save; `SaveNow` and the teardown save still wait for the lock. How often busy stretches skip saves in practice is not measured. +- [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. +- [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (1,024 at start, up to 16,384 after growth, or `APS5_RESOURCE_CACHE_ENTRIES`, each with its textures); that cost is not measured. When `ReleaseTextureCaches` runs on device memory exhaustion, the sampled textures these entries hold are only marked released and are freed at the next insert, so the immediate allocation retry gets no memory back from them. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured on one title on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. + +- [Sampled MSAA surface geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) supplies the fragment-aware linear size and one mip for a tiled 2D surface at two, four or eight fragments. Unsupported arrays, mip chains, extents and block sizes throw. Linear descriptors retain the existing geometry path, including its use when sampling an existing native target; a linear MSAA guest layout is not modeled. The range calculation follows AMD Addrlib's thin-block dimensions. Console layout equivalence and title effects of the additional mip metadata remain unmeasured. This does not add sample upload or readback. +- [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. + +- [Multisample image readback](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp) fetches unsigned R8/R16/R32/R32G32/R32G32B32A32 or D32 depth samples through a 2D array view in GENERAL, in layer/y/x/fragment order. The caller owns producer completion, image transitions and resource lifetimes. It implements Vulkan per-sample fetches, not guest-memory publication, metadata expansion or PS5 layout equivalence; D32 exceptional-value preservation has not been measured. Other formats, sample counts and invalid ranges throw. +- [Multisample image upload](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleUpload.cpp) does not model guest layouts, native ownership or CMASK/FMASK/DCC publication; console equivalence and large-title initialization costs remain unmeasured. It stores 1/2/4/8/16-byte elements from a storage buffer into an unsigned 2D multisample array view in layer/y/x/fragment order. It requires enabled multisample storage images and formatless image writes, 2/4/8 fragments and valid aligned buffer and dispatch ranges. The caller supplies the matching storage view in GENERAL and owns barriers, completion and resource lifetimes. +- [Thick SW_4KB_S_X volumes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) use AMD addrlib's GFX10 non-RB+ 16-pipe `GFX10_SW_4K_S3_X_PATINFO` equations for 1, 2, 4, 8 and 16-byte elements, including packed mip tails. The layout follows [AMD PAL c5e80007](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp); it has not been measured on console hardware. Other thick swizzle modes outside the supported families still throw. Mipmapped 3D color targets remain refused by the color-register decoder. +- [Viewport depth range remap](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - on a device without `VK_EXT_depth_range_unrestricted` a viewport depth range outside [0, 1] (Minecraft PPSA17221 draws its world with `PA_CL_VPORT_ZSCALE` 1, `ZOFFSET` 0 in negative-one-to-one clip space) is folded into the vertex shader: `RemapPositionDepth` scales clip z so z/w is the console's depth, the pipeline's depth clamp applies the console's `PA_SC_VPORT_ZMIN/ZMAX` clipped to [0, 1] (exact for a normalized depth target or none; Minecraft clamps its float depth target to [-1, 1], so a depth below 0 is stored as 0 there, which only a later test or read at exactly 0 can tell, and the first such draw is reported once), and two extra `gl_ClipDistance` elements restore the near and far planes depth clamping removes. The mode is a pipeline specialization constant (`PipelineSpecialization::DepthRemapBase`); scale and offset are read from the draw's push block (two words at byte 104, `DepthRemapPushOffsetBytes`, so a remapped draw's stage push data ends before it), so draws that differ only in them share one vertex module and one pipeline. The remap serves the vertex path only; mesh and tessellation draws with such a range still throw. Whether the console clamps before or after its depth bias and how it treats a clamp range narrower than the transform's are not measured. +- [Benchmark rewrite failure test off Windows](../../../core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp) (libSceAgcDriver) - `ReplaceGzipFile` checks that a write failing at `.tmp` keeps the previous `aps5-bench.json.gz` only on non-Windows builds: the `std::ios_base::failure` thrown inside the MinGW ofstream did not unwind to the test's catch, so Windows CI covers only the failed move. + +- [Sampled array windows](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) (libSceAgcDriver) - a sampled 2D array, 1D array or cube view whose base array slice is past 0 is built from its viewed slices only (`ViewedArrayWindow`), at the guest address of its base slice. The window is taken only when the surface is thin, has no DCC metadata, and, for a 64 KiB XOR swizzle, when the base slice keeps every slice bit of the swizzle equation at zero (R_X 1-byte elements: a base slice that is a multiple of 16). Any other view still builds slices 0 to its last slice. A window and a whole-array view of the same surface are separate images over overlapping memory, kept coherent by the existing overlap rules, at the cost of the memory and uploads of both. PPSA26344 samples a 256x256 cube array at slices 4299..7232, built at 1467 MiB instead of about 3.8 GB. +- [2D reads of 2D arrays](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled image whose shader shape is 2D and whose T# is a 2D array is built from its base slice alone: the T# is narrowed to that slice (`FirstLayerDescriptor`) before its decode, cache key and image, in stage A records, stage B bindings, revalidation and image tables. A whole-array read of the same T# keeps its own cache entry, so both images exist when a title uses both. Storage images read or written through a 2D shape are not narrowed and still build every slice the T# names. PPSA26344 reads a 3328x1872 R_X T# of 1537 slices this way: the whole array was a 12.4 GiB image that did not fit the device-local budget of an RTX 4090. +- [Sampled and storage views of 1 GiB or more](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) that stay whole are only logged (sampled views by `cachedTexture`, storage views by `cachedStorageTexture`), with the registered guest range holding their base and how many of their slices fit in it; nothing is capped to that range. PPSA26344 samples a 1467 MiB cube array window of which only 838 of 2934 slices lie inside its 3380 MiB range: the rest is read from whatever follows the range. Whether such slices are ever sampled is not measured. +- [Color export type against the target format](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - the export-format check refuses a `UINT16_ABGR` export (`SPI_SHADER_COL_FORMAT` 7) into a target whose color-buffer number format is not unsigned integer, but not the reverse. A 32-bit export (`32_R`, `32_GR`, `32_AR` or `32_ABGR`) into an unsigned-integer target leaves `uintExport` clear, so the recompiler declares the fragment output `float4`; `DecodeColorBuffer` maps a number format of 4 to `R32_UINT` from the color buffer alone, independent of the export format, so the float output reaches an integer attachment. Desktop drivers keep the raw bits, but the Khronos validation layer reports `Undefined-Value-ShaderFragmentOutputMismatch` and MoltenVK's Metal compiler refuses the pipeline. Which export format a title pairs with a 32-bit integer target is not measured, so the output type is left following the export format rather than the attachment. +- [Depth mip levels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a depth view of mip `DB_DEPTH_VIEW.MIPID` in a chain of `DB_Z_INFO.MAXMIP` levels renders into that level as a surface of its own, at the level's offset and extent from `ComputeElementMipLayout` for the 64 KiB Z_X swizzle, the layout color target mips and sampled depth already use; slices of a chain step by the size of the whole chain. Taken from LEGO Builder's Journey (PPSA05496), which renders depth mip 1 of 64 KiB Z_X surfaces without HTILE; the placement was not compared with a console capture. Multisampled depth mips, other swizzle modes, levels in the packed mip tail and HTILE on a mip chain throw + +- [Long Windows pipeline-cache paths](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp): `readHeader` and the failed-read existence check still use ordinary paths. A filename beyond MAX_PATH can prevent header reuse or be reported missing; the shared content reader and journal normalization do not cover these calls. +- [DB_RENDER_CONTROL.STENCIL_CLEAR_ENABLE without a stencil plane](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): a stencil clear on a depth target whose `DB_STENCIL_INFO` format is `STENCIL_INVALID` is dropped; the depth clear and color writes proceed. AMD PAL ([`gfx9RsrcProcMgr.cpp`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32/src/core/hw/gfxip/rpm/gfx9/gfx9RsrcProcMgr.cpp)) and radeonsi (`si_clear.c`) never set the bit without a stencil plane, and shadPS4 drops it the same way (`vk_rasterizer.cpp`, `StencilValid`). No source states what the hardware does with the bit then; the console was not measured. A read-only stencil plane still throws diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index d9a2f57845..5fd76482c4 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -2,20 +2,28 @@ ### Unknown function info -- [sceAjmDecWVorbisCreateHeaderPacket, sceAjmDecWVorbisCreateSetupPacket](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - unknown signatures +- [sceAjmBatchGetLog, sceAjmInstanceAvailable](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjm.native) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAjm.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. +- [sceAjmInstanceExtend, sceAjmInstanceSwitch](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjm.native) - their signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ajm/ajm.h#L19-L20) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ajm.cs#L100-L104), not measured on a console; these throwing declarations only resolve imports. +- [sceAjmInstanceCreate](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a full 14-bit instance slot namespace returns `0x80930007` (`OUT_OF_RESOURCES`), as [shadPS4's InstanceCreate](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ajm/ajm_context.cpp) does for its own instance pool (PS4). The 16384 slots per codec prefix are this runtime's existing ID layout, not a measured console capacity; the PS5 capacity and exhaustion result are unverified. +- [ATRAC9 vibration decoding](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): channel configurations 6 (mono) and 7 (dual mono) use the pinned [vgmstream decoder](https://github.com/vgmstream/vgmstream/blob/7c520e8f9b078dfde2f84f1b5bcbad0c6c415806/src/coding/libs/atrac9_dec.c), including its 12/24/48 kHz profile limits. Synthetic fixtures cover interleaving, reset after malformed input, partial superframes and PCM16/PCM32/float output; console PCM rounding and physical haptic output are unverified. Ordinary channel configurations and discrete multichannel streams still use LibAtrac9. Decoder creation and decoding run under the existing AJM instance lock because the dependency rebuilds shared Huffman tables during initialization. - [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 +- [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: Layer I/II headers are accepted when no original-file-length (`parse_ofl`) lookup is requested, which is supported only for Layer III; MPEG-2.5 stops at 64 kbps. The `parse_ofl` layout (Xing/Info + LAME tag after side information, with the standard fixed Xing/Info offset retained for encoders such as [LAME](https://github.com/lameproject/lame/blob/master/libmp3lame/VbrTag.c#L854-L862), VBRI at byte 36 or after side information, 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 (the one call in PPSA04354 passes a 24-byte result buffer, the 8-byte result plus the 16-byte ATRAC9 codec info written here; the call does not show the codec or the fields); 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 -- [sceAjmBatchJobSetResampleParametersEx](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `(info, instance, float ratio_start, float ratio_change_per_sample, flags, result)` signature is inferred from the name and from `sceAjmBatchJobSetResampleParameters`; with a zero change per sample it sets the ratio as `sceAjmBatchJobSetResampleParameters` does (Watch Dogs: Legion, PPSA01500, calls it so). How a non-zero change ramps the ratio is unknown and throws -- [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); other flags throw +- [AJM codec 23](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - taken as interleaved `s16` PCM that only passes through the resampler, from Broken Sword - Shadow of the Templars: Reforged (PPSA21480); no source names a codec 23 ([shadPS4's PS4 header](https://github.com/shadps4-emu/shadPS4/blob/5aa7ffa3f4f21bba827d7865ed8f69f98f993542/src/core/libraries/ajm/ajm.h#L19-L25) ends its codec list with `Max = 23`) and the name `CODEC_PCM` is ours. The title decodes its Ogg Opus and Vorbis audio itself and, for every block, initializes the instance (flags 0x1002) with `0, 2, 44100`, sets the ratio 0.91875 (44100/48000) and decodes 235 stereo frames into a 256-frame output. The 12 initialize bytes are taken as input format (0 is `s16`, other values throw), channel count and sample rate; the rate is only reported in the format sideband; other sizes throw; zero or more than 8 channels or a zero rate return the invalid-parameter result as for Opus, not verified. A run consumes its whole input and emits every output frame whose position (output index × ratio) is below the input frame count, with the 4-point Catmull-Rom of the other codecs' resampler and the first and last input frames repeated past the edges, so each run restarts at position 0; `totalDecodedSamples` counts input frames; `s32` and float outputs are accepted although only `s16` was seen. Throwing because the console's behaviour is unknown: a run after a resampled run without an initialize or context clear in between (whether the console carries the resampler position across runs), a ratio change per sample, gapless counts, an empty input or one that is not a whole number of frames, an output that cannot hold the resampled input, run flags other than `SIDEBAND_STREAM` and initialization through `sceAjmBatchJobControl` +- [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`, and PPSA02225's one call agrees with that layout (`rdi`, `esi`, `xmm0` and `xmm1` as two single floats, `edx`, `rcx`) without showing `flags` or the float roles. 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); the one call in PPSA25936 passes the flags 0x4000 (bit 14 only), an 8-byte input and an 8-byte output, which agrees with INITIALIZE alone and the 8-byte parameters, without showing the codec or the meaning of the words; other flags throw - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET is assumed to clear the context as sceAjmBatchJobClearContext does -- [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - initialize parameters are 8 bytes for ATRAC9, 12 for Opus and none for MP3, as one title passes them; other codecs and sizes, and an output other than the 8-byte result, throw +- [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - initialize parameters are 8 bytes for ATRAC9 and M4AAC, 12 for Opus and none for MP3, as one title passes them; other codecs and sizes, and an output other than the 8-byte result, throw - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - SIDEBAND_GAPLESS_DECODE with RESET overwrites both counts as in [RPCSX](https://github.com/RPCSX/rpcsx); shadPS4 ignores a zero count when the other is nonzero. Without RESET they differ, so it throws - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty ### Functional -- [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) throws when the original file length is requested (`parse_ofl` non-zero) for a layer other than III, for a CRC-protected frame, or for a VBRI header outside MPEG-1 stereo: shadPS4 looks for the tag after the CRC and after the side information, while LAME and FFmpeg place it without the CRC and VBRI always at byte 36. +- [sceAjmDecWVorbisCreateHeaderPacket](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - refuses an output smaller than the whole packet (56 bytes plus the setup packet) with `0x8A6E0003`. The PS5 12.00 bit writer only checks that each write starts inside the buffer, so the console can accept a buffer a few bytes short and write past it +- [AJM batch contexts](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): start, wait and cancel reject context IDs not allocated by initialization; batches belong to their creating context, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0ebea15c24253b60f514fc37a348dabc8d0bcb90/src/core/libraries/ajm/ajm.cpp). This is not verified on PS5. Finalization still does not invalidate contexts or release their instances and outstanding batches; jobs execute synchronously and cancellation does not interrupt them. + - [ATRAC9 gapless segments](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a run job stops after the gapless total of output samples counted from the start of the segment, leaving the rest of its input unconsumed, and the next run starts the next segment with the decoder as it is and without a second skip. FMOD in PPSA29141 sets a total shorter than a sound's frames and reads the frames after it on later runs; with the output held at the total its stream reader waited for them forever and the last buffer repeated. The console's handling of the frames after the total is not documented; a reset (`SetGaplessDecode` with the reset flag, `ClearContext`, initialization) starts a new segment. An ATRAC9 run job without `RUN_MULTIPLE_FRAMES` decodes its whole input as one with it: FMOD in PPSA29141 puts two run jobs in one batch with the second's input right after the whole of the first's. Whether the console stops after one frame there is not verified; MP3 and Opus run jobs still stop after one frame +- [AJM M4AAC decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 2) - Ghost of Yotei (PPSA26344) initializes it through `sceAjmBatchJobControl`. The initialize parameters (`u32` config type, 1 ADTS or 2 raw, then `u32` sampling frequency index), instance flags (channels in bits 3-6, `ENABLE_SBR_DECODE` bit 32, `ENABLE_NONDELAY_OUTPUT` bit 33, surround interleave order bits 34-35), `{heaac, reserved}` codec info and decoder error result (`CODEC_ERROR | FATAL`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/ajm/ajm_aac.cpp) (PS4, fdk-aac) and are unverified on PS5; decoding uses FFmpeg's AAC decoder. Without `ENABLE_NONDELAY_OUTPUT`, the first two frames after initialization or reset are consumed without output. Raw input takes its channel configuration from the instance (0 means stereo) and is decoded as one access unit per run job. The format reports the last frame's bitrate. Invalid config type or raw sampling frequency index above 11 returns invalid-parameter result 4. The implementation throws for ADTS without a frame header, profiles other than AAC LC, multiple raw data blocks per ADTS frame, unsupported channel configurations, more channels than the instance supports, HE-AAC without `ENABLE_SBR_DECODE`, raw input with `RUN_MULTIPLE_FRAMES`, output room smaller than the previous frame length, and unknown codec flags. - [AJM](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. +- [AJM Opus mapping family 255 with several channels](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): the initialization words carry no stream count or channel mapping, so the decoder opens on the first packet. It counts the self-delimited streams of that multistream packet ([RFC 6716 appendix B](https://datatracker.ietf.org/doc/html/rfc6716#appendix-B)) and takes each stream's stereo flag from its TOC byte as coupled, then decodes with an identity channel mapping. A packet whose streams do not add up to the channel count throws. The layout is inferred, not read from the console; unverified on hardware. diff --git a/docs/dev/debt/libSceAjmi.md b/docs/dev/debt/libSceAjmi.md new file mode 100644 index 0000000000..d3ac224c47 --- /dev/null +++ b/docs/dev/debt/libSceAjmi.md @@ -0,0 +1,5 @@ +# libSceAjmi technical debt + +### Unknown function info + +- [libSceAjmi pending exports](../../../core/libs/prx/libSceAjmi/Export.cpp) - 5 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAjmi.c). diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index db01df9a8b..ae062de3f3 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -2,15 +2,16 @@ ### Unknown function info +- [AMPR command buffer null arguments](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the result for a null command buffer is unknown for `sceAmprCommandBufferConstructor`, `sceAmprAprCommandBufferConstructor`, `sceAmprCommandBufferReset` and `sceAmprCommandBufferSetBuffer`; these return `SCE_KERNEL_ERROR_EINVAL` here, but the behavior has not been confirmed on a PS5. - [AMPR WriteAddressFromCounterPairOnCompletion](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command buffer offsets](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - recording with a current offset greater than the bound memory size throws before calculating remaining capacity or writing a command. This validates the emulator's own command-buffer encoding; the console's response to a corrupted opaque buffer object is unknown. -- [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_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 +- [AMPR markers](../../../core/libs/prx/libSceAmpr/Export.cpp): colors are not recorded and command sizes use the host encoding. Native signatures confirm that set takes a color pointer and push/measure take the color value. +- [__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 - [sceAmprAmmGetUsageStatsData](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) fills the caller-sized 24-byte statistics structure with 512 page-table pool entries, no allocated entries, a zero high watermark and ring idle flags 0x7, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libAmpr.cpp); they are not measured on a console, and the real call is a pass-through to `sceKernelMapperGetUsageStatsData` per [drakmor/ampr_emu](https://github.com/drakmor/ampr_emu/blob/main/src/ampr_emu_amm.cpp), which this port has no kernel-side counterpart for. -- [Dinkumware thread exports](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning. -- [_Cnd_signal, _Cnd_init_with_name](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - take a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and act on the same conditions. `_Cnd_signal` wakes one waiter and returns `_Thrd_success` (0), as `_Cnd_broadcast` does; whether the console maps a failing internal signal to `_Thrd_error` is not known. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. Neither is confirmed on a PS5 `libc.prx`. `_Thrd_id` still throws: microsoft/STL returns an `unsigned int` thread id, but whether the PS5 returns the `pthread_t` or a kernel thread id, and at what width, is not known -- [std::_Pad](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follows Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` of Visual C++ 2013 ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. The pad is taken to be that header's class with the console's pointer handles: a vtable pointer whose first slot is the pure virtual `_Go()`, then `_Cnd_t` at 0x8, `_Mtx_t` at 0x10 and `bool _Started` at 0x18, 32 bytes, the derived `_LaunchPad` target following it; the console's `_Pad(const char*)` and `_Launch(const char*, pthread**)`/`_Launch(pthread_attr**, pthread**)` overloads suggest named primitives and could mean another layout or vtable. `_Launch` starts the thread with `pthread_create` and default attributes (the console's `_Thrd_start` name, stack size and priority are unknown), its entry calls `_Go()` and returns the result as the exit code, and it does not run `_Cnd_do_broadcast_at_thread_exit`, which is absent here. A failing primitive throws the guest `std::system_error` that `_Throw_C_error` picks for its code, and a failing thread start the `resource_unavailable_try_again` one of `_Thrd_startX`; any `pthread_create` error counts, since the console's `_Thrd_start` result is unknown. A null pad or thread slot throws +- [Dinkumware thread exports](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. The spin lock (byte exchange loop, no yield) matches that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning. +- [_Cnd_init_with_name](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - takes a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and acts on the same conditions. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. This is not confirmed on a PS5 `libc.prx`. diff --git a/docs/dev/debt/libSceAppChecker.md b/docs/dev/debt/libSceAppChecker.md new file mode 100644 index 0000000000..9fc80d43e0 --- /dev/null +++ b/docs/dev/debt/libSceAppChecker.md @@ -0,0 +1,5 @@ +# libSceAppChecker technical debt + +### Unknown function info + +- [sceAppCheckerAbort, sceAppCheckerExecute and sceAppCheckerExecuteEx](../../../core/libs/prx/libSceAppChecker/Export.cpp) (libSceAppChecker) - names and NIDs are present in the [aerolib symbol database](https://github.com/zecoxao/sce_symbols/blob/master/aerolib.csv) and are attributed to this module by `sceAppChecker` symbol prefix; the library is absent from the PS5 3.20 export inventory, so the pairing is not firmware-verified and no credible prototypes were found. The declarations have no parameters solely to resolve imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 957dc51912..9976365de2 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -3,10 +3,13 @@ ### 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 +- [sceAppContentDownloadDataFormat](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - empties the `/download0` directory, creating it if needed. Imported by PPSA01317. A null mount point returns `PARAMETER` and another mount point returns `NOT_MOUNTED` as in `sceAppContentTemporaryDataFormat` and `sceAppContentDownloadDataGetAvailableSpaceKb`; the console's error codes for both and its behaviour with files still open are unverified + +- [libSceAppContent pending exports](../../../core/libs/prx/libSceAppContent/Export.cpp) - 16 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppContent.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional -- [Additional content](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued; `sceAppContentAddcontDelete` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no additional content is ever installed. The signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs); no reference gives the console's error codes, so both are unverified -- [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw. +- [Initialization](../../../core/libs/prx/libSceAppContent/Export.cpp) - service readiness and boot attributes are not modeled; successful initialization returns a zero boot record. Initialization-state guards, SDK 0x01500000 repeated-initialization errors and reserved-field checks are recovered. SDK-dependent service-error translation remains incomplete. + +- [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) console behaviour and hardware return values are unverified, including behaviour with open files across unmount; host filesystem failures throw. Storage models one `/temp0` mount, preserving files on unmount while rejecting new path resolutions until remount, and leaving existing open host descriptors usable. `sceAppContentTemporaryDataMount` delegates to `sceAppContentTemporaryDataMount2` with option 1 (`TEMPORARY_DATA_OPTION_FORMAT`) to format on mount as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/app_content/app_content.cpp#L550-L553) does. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); pointer ABI follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). diff --git a/docs/dev/debt/libSceAppInstUtil.md b/docs/dev/debt/libSceAppInstUtil.md new file mode 100644 index 0000000000..92d88fb4a6 --- /dev/null +++ b/docs/dev/debt/libSceAppInstUtil.md @@ -0,0 +1,7 @@ +# libSceAppInstUtil technical debt + +### Unknown function info + +- [sceAppInstUtilAppInstallPkg](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) - the package path is followed by an opaque 64-bit value sent to the install service; its meaning and pointer-versus-integer type remain unresolved. +- [sceAppInstUtilGetInstallProgressInfo, sceAppInstUtilGetInstallProgress and sceAppInstUtilGetPrimaryAppSlot](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) - take a title-id string and an output pointer. The IPC replies contain 48, 4 and 4 bytes respectively; callers and reply producers are needed to establish the field types. +- [sceAppInstUtilAppPrepareOverwritePkg, sceAppInstUtilAppRecoverApp and sceAppInstUtilAppUnInstallTheme](../../../core/libs/prx/libSceAppInstUtil/Export.cpp) - absent from the supplied system module and from the supplied decompiled callers; the old export inventory establishes names but not prototypes. diff --git a/docs/dev/debt/libSceAt9Enc.md b/docs/dev/debt/libSceAt9Enc.md new file mode 100644 index 0000000000..0ceb0e614c --- /dev/null +++ b/docs/dev/debt/libSceAt9Enc.md @@ -0,0 +1,6 @@ +# libSceAt9Enc technical debt + +### Unknown function info + +- [libSceAt9Enc](../../../core/libs/prx/libSceAt9Enc/Export.cpp) (libSceAt9Enc, sysmodule id `0x000000d1`) has no encoder, so all five exports throw: `sceAt9EncClearContext`, `sceAt9EncCreateEncoder`, `sceAt9EncEncode`, `sceAt9EncFlush` and `sceAt9EncQueryMemSize`. PPSA03332 imports them and fails to load (`libSceAt9Enc.prx: cannot open shared object file`) when the library is absent. The names are from the aerolib NID list, and the name reads as the ATRAC9 encoder, unconfirmed +- [sceAt9Enc*](../../../core/libs/prx/libSceAt9Enc/Export.cpp) (libSceAt9Enc) - the signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json), which gives `int32_t sceAt9EncClearContext(void *pAt9EncWorkArea, int32_t *pInternalResult)`, `int32_t sceAt9EncCreateEncoder(void *pAt9EncWorkArea, struct *pAt9EncParam, int32_t memSize, int32_t *pInternalResult)`, `int32_t sceAt9EncEncode(void *pAt9EncWorkArea, const void *pInputBuf, void *pOutputBuf, uint32_t inputBufSize, uint32_t outputBufSize, uint32_t *pOutputDataByte, uint32_t *pUsedBufSize, int32_t *pInternalResult)`, `int32_t sceAt9EncFlush(void *pAt9EncWorkArea, void *pOutputBuf, uint32_t outputBufSize, uint32_t *pOutputDataByte, int32_t *pInternalResult)` and `int32_t sceAt9EncQueryMemSize(struct *pAt9EncParam, int32_t *pMemSize, int32_t *pInternalResult)`. Its `struct *pAt9EncParam` is a placeholder, so that parameter's layout is unknown diff --git a/docs/dev/debt/libSceAudio3d.md b/docs/dev/debt/libSceAudio3d.md index 90d7a053d8..9c17a838c6 100644 --- a/docs/dev/debt/libSceAudio3d.md +++ b/docs/dev/debt/libSceAudio3d.md @@ -2,9 +2,27 @@ ### Silent stubs -- [libSceAudio3d](../../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: objects can be reserved and `sceAudio3dObjectSetAttributes` accepts their attributes, but the library has no bed exports or mixing to give them audio, the attributes have no effect and `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them. `sceAudio3dObjectSetAttributes` validates the port, the reserved object and the attribute array (an object id that was never reserved returns the invalid-object error, while shadPS4 returns success for it and the error only for a released object; neither is measured on a console), treating the reset-state attribute as valid with a null value and requiring a value for the others; the attribute identifiers and the value of each are not interpreted. `sceAudio3dPortFlush` drops the frames queued by `sceAudio3dPortAdvance` instead of mixing and submitting them to an output, as there is no output +- [libSceAudio3d](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - `sceAudio3dObjectSetAttributes` validates the port, the reserved object and the attribute array (an object id that was never reserved returns the invalid-object error, while shadPS4 returns success for it; neither is measured on a console), treating the reset-state attribute as valid with a null value and requiring a value for the others, but the identifiers and the value of each are not interpreted, so an object has no samples, no gain and no position: the ids of the `PCM`, `GAIN` and `POSITION` attributes and the `POSITION` layout are unknown (the aerolib NID list gives the names only, the Orbital IDA type database the signatures only and shadPS4's values are PS4). `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them, and `sceAudio3dPortFlush` drops the frames queued by `sceAudio3dPortAdvance` instead of mixing and submitting them to an output + +### Functional + +- [libSceAudio3d bed path](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - bed PCM is mixed and played: `sceAudio3dBedWrite` and `sceAudio3dBedWrite2` queue a bed, bounded by `queue_depth` and `NOT_READY` when full, `sceAudio3dPortAdvance` consumes one and mixes it into a stereo frame with a linear-balance pan and a 5.1/7.1 downmix, and an advance with no bed queued mixes silence ([Mixer.hpp](../../../core/libs/prx/libSceAudio3d/Mixer.hpp)) and queues the frame, and `sceAudio3dPortPush` opens a type-126 AudioOut port on first use and submits the queued frames to it. The explicit `sceAudio3dAudioOutOpen`, `sceAudio3dAudioOutOutput` and `sceAudio3dAudioOutOutputs` exports queue their blocks and the port drains them. The attribute enum values and the panning model are unverified ### Unknown function info - [sceAudio3dPortOpen](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too +- [sceAudio3dPortGetAttributesSupported](../../../core/libs/prx/libSceAudio3d/Export.cpp): the native export validates the port before the count pointer, writes the total count without reading its incoming value, and writes the complete list when an array is supplied. These checks are matched. The list itself remains the host's three port attributes: the native list derives from AudioOut2 device capability masks, SDK version and platform mode, which the host does not yet model. - [sceAudio3dObjectReserve](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF +- [sceAudio3dStrError](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dPortOpenEx](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dPortCreate](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dPortDestroy](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dPortGetStatus](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dReportRegisterHandler](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dReportUnregisterHandler](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dCreateSpeakerArray](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - the speaker-array calls return the AudioOut2 result unchanged: the console translates AudioOut2 speaker-array errors into Audio3d codes, but this project's AudioOut2 speaker-array calls throw instead of returning error codes, so there is nothing to translate yet +- [Audio3d bed downmix coefficients](../../../core/libs/prx/libSceAudio3d/Mixer.hpp) (libSceAudio3d) - the centre and the surrounds fold into stereo with a 0.7071 gain and the LFE channel is dropped, which follows [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4): its `DOWNMIX_CENTER` and `DOWNMIX_SURROUND` are 0.7071 and its `DOWNMIX_LFE` is 0, so the `if constexpr` that adds the LFE is compiled out. [prosper](https://github.com/mattias800/prosper) disagrees on the LFE only: it folds it into both sides with a 0.5 gain, in its AudioOut2 main bed path, which is not an Audio3d path. KytyPS5 has no Audio3d mixing at all. The absolute level is unmeasured: fpPS4 scales the front by 1 + 3/sqrt(2) instead of 1 +- [Audio3d bed channel order](../../../core/libs/prx/libSceAudio3d/Mixer.hpp) (libSceAudio3d) - the buffer is read as L, R, C, LFE, Ls, Rs, Lrs, Rrs, the standard order, which three references agree on: [shadPS4](https://github.com/shadps4-emu/shadPS4) reads the front, centre, LFE and surround samples at those indices, [prosper](https://github.com/mattias800/prosper) documents FL, FR, FC, LFE and then surround pairs, and fpPS4 names them `_FL=0 _FR=1 _FC=2 _LF=3 _SL=4 _SR=5 _BL=6 _BR=7`. fpPS4's `STD` layout swaps the side and back pairs, which this library does not model +- [sceAudio3dAudioOutOutput](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - the return is the number of frames per channel in the queued block, which follows [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [prosper](https://github.com/mattias800/prosper), whose `sceAudioOutOutput` returns the grain, and this project's own `sceAudioOutOutput`, which returns the port's `samplesNum` (the buffer length in frames). [shadPS4](https://github.com/shadps4-emu/shadPS4) returns the total samples instead (`len * channels`) and fpPS4 returns 0; no console measurement settles it +- [sceAudio3dPortAdvance buffer mode](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - a mode other than 2 returns `NOT_SUPPORTED` (0x80EA0008), the value [shadPS4](https://github.com/shadps4-emu/shadPS4) uses for it and the one [prosper](https://github.com/mattias800/prosper) reads from the PS5 module's own error table, where 0x80EA0008 is what the per-port calls return for a buffer mode they do not support. shadPS4 and prosper refuse only mode 0 in `sceAudio3dPortAdvance` and modes 0 and 1 in `sceAudio3dPortPush`, while [KytyPS5](https://github.com/KytyPS5/KytyPS5) requires mode 2 at `sceAudio3dPortOpen` and halts otherwise, so this library refuses every other mode at the open and the check here is unreachable. 0x80EA0005 is `OUT_OF_MEMORY`, not `NOT_SUPPORTED` +- [sceAudio3dPortAdvance on a full queue](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - a full queue returns `NOT_READY` (0x80EA0007) instead of throwing, which follows [shadPS4](https://github.com/shadps4-emu/shadPS4), the only reference with a body: it returns `ORBIS_AUDIO3D_ERROR_NOT_READY` when the mixed queue is at the port's depth. [KytyPS5](https://github.com/KytyPS5/KytyPS5) halts instead and prosper has no queue at all diff --git a/docs/dev/debt/libSceAudioIn.md b/docs/dev/debt/libSceAudioIn.md index 4e15888d7e..abb06131ee 100644 --- a/docs/dev/debt/libSceAudioIn.md +++ b/docs/dev/debt/libSceAudioIn.md @@ -6,8 +6,39 @@ ### Unknown function info -- [sceAudioInIsPadEmbeddedMic](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - name from its NID (`X+4jdIS75P0`); signature `int32_t(int32_t handle)` from the Ghost of Yotei (PPSA26344) call site in #2355; the result's meaning is unconfirmed, so it still throws rather than invent one +- [sceAudioInDeviceIdOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInDeviceIdAsyncOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetUsbGain](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInIsSharedDevice](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInDeviceIdHqOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInDeviceHqOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetConnections](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetPortConnections](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetPortStatuses](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInGetHandleStatusInfo](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInDeviceOpenEx](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInChangeAppModuleState](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetSparkParam](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetSparkSideTone](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetCompressorPreGain](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature; the PS5 12.00 module returns 0 without reading any argument, and so does this one +- [sceAudioInSetNoiseGateThreshold](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature; the PS5 12.00 module returns 0 without reading any argument, and so does this one +- [sceAudioInSetMicLevel](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetDeviceMuteStatus](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetDevConnection](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInCountPorts](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetUserMute](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicCreate](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicDestroy](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicWrite](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicGetStatus](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicSetMute](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetMode](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetMode2](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInExtCtrl](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInInputs](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature ### Functional -- [sceAudioInHqOpen, sceAudioInAsyncOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) open the same port as `sceAudioInOpen`, so `sceAudioInInput`, `sceAudioInGetSilentState` and `sceAudioInClose` accept their handles. The signatures agree across [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/fc5d2cc219c43c8c8f354153e7b3d0c04a5d506e/src/core/libraries/audio/audioin.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp). HqOpen is limited to 128 samples at 48 kHz; AsyncOpen rejects sizes above 384 and throws for currently unverified inputs. Host capture pacing and console behavior remain unverified. +- [sceAudioInIsPadEmbeddedMic](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - returns 0 for every open port: on the console it returns 1 when the port captures from the controller's built-in microphone and 0 for another input device, but ports here capture from the host's default input device +- [sceAudioInHqOpen, sceAudioInAsyncOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) - PS5 wrapper validation requires 48000 Hz followed by exactly 128 samples for both calls; HqOpen then requires format 2. The host uses the same SDL capture path for both, so native asynchronous scheduling and high-quality processing are not reproduced. Common port validation, device selection and host capture pacing remain incomplete. +- [sceAudioInGetGain, sceAudioInGetRerouteCount](../../../core/libs/prx/libSceAudioIn/Export.cpp) - GetGain writes 1.0f before handle validation and returns 0x80260105 for a null output, as confirmed by the PS5 module. Native handle bit checks and uninitialized/inactive port errors differ from the host port table. Native reroute counters follow device reassignment; the host opens each SDL device once and reports zero. diff --git a/docs/dev/debt/libSceAudioLatencyEstimation.md b/docs/dev/debt/libSceAudioLatencyEstimation.md new file mode 100644 index 0000000000..9e199bf5f3 --- /dev/null +++ b/docs/dev/debt/libSceAudioLatencyEstimation.md @@ -0,0 +1,5 @@ +# libSceAudioLatencyEstimation technical debt + +### Unknown function info + +- [sceAudioLatencyEstimationAddData, sceAudioLatencyEstimationGetDelayTime, sceAudioLatencyEstimationInitialize and sceAudioLatencyEstimationRelease](../../../core/libs/prx/libSceAudioLatencyEstimation/Export.cpp) (libSceAudioLatencyEstimation) - names and NIDs are present in the [aerolib symbol database](https://github.com/zecoxao/sce_symbols/blob/master/aerolib.csv) and are attributed to this module by `sceAudioLatencyEstimation` symbol prefix; the library is absent from the PS5 3.20 export inventory, so the pairing is not firmware-verified and no credible prototypes were found. The declarations have no parameters solely to resolve imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 017fe07cf4..050b5a78dd 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -1,25 +1,121 @@ # libSceAudioOut technical debt +### Silent stubs + +- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) - returns 0, as the PS5 12.00 module (0x248d0) does whether or not the low-latency service runs. The module disconnects from the audio server and clears the service tables; here `sceAudioOut2LoInit` starts no service and open contexts and ports are left as they are + ### Unknown function info -- [sceAudioOut2UserGetSupportedAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp) (libSceAudioOut) - the user handle and two uint32 output pointers follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. The masks describe this runtime: no context attributes and port attributes PCM (bit 0) and gain (bit 1). Null outputs and unknown or destroyed user handles throw; console validation order and error values are unknown. Users are tracked for this query, but port ownership and busy-user destruction are not implemented. The other PPSA23566 imports remain declared without parameters, with unknown signatures: [sceConvertKeycodeGetCharacterFromKeyboardData](../../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp), [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list -- [sceAudioOutExClose, sceAudioOutExConfigureOutput, sceAudioOutExOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99505, which fails to load without them; added without an implementation and declared without parameters. PPSA99505 calls them only for its HDMI bitstream output, an option that is off by default, to read the formats the TV or receiver lists, switch the HDMI output mode and open a port for IEC 61937 bursts ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L110-L179)); [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp) also calls `sceAudioOutExOpen`, `sceAudioOutExConfigureOutput` and `sceAudioOutExClose` for HDMI bitstream output. The two declare the arguments of `sceAudioOutExConfigureOutput` differently and neither is confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no encoded HDMI output to switch to -- [sceAudioOutExGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports a sink without audio descriptors: it zeroes the buffer and returns 0, as `sceAudioOutSysGetHdmiMonitorInfo` does, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(0, 0, info, size)` and the descriptor count as a 32-bit value at +0x98, with 8-byte descriptors from +0xA0, follow [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L62-L117) (PPSA99505), which passes a 0x400-byte buffer and read that layout from one TV; with no descriptor listed it decodes the audio itself instead of opening a bitstream port. The meaning of the first two arguments and the structure's size are unknown: nonzero first or second arguments and sizes below 0xA0, which cannot hold the count, throw, and every byte of the declared size is written as 0. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured -- [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - assumed to take `(user_id, latency)`: KytyPS5 reads `(user_id, output, latency_us)`, but the known calls set just edi and esi; only `(0xFF, 2)` (PPSA26344) and `(0xFF, 1)` (PPSA14632) are accepted -- [sceAudioOut2MasteringInit](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - flag values unknown; only 0 is accepted. Whether the console reports an error for a repeated `sceAudioOut2MasteringInit` or for `sceAudioOut2MasteringTerm` without a matching initialization is unknown; both return 0. PPSA01289 creates the audio device of its next world before it destroys the previous one, so it calls Init twice before Term twice, and only logs a non-zero result -- [sceAudioOutSetMixLevelPadSpk](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted +- [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) - the PS5 module confirms a pointer-sized user handle and two optional uint32 outputs. Handles 0/-1 return 0x80268010 before checking outputs; two null outputs return 0x80268001. Host masks still describe only implemented PCM/gain support, unlike native SDK- and device-dependent masks. Unknown or destroyed host handles throw; native arbitrary handles are pointers and cannot be validated by the host registry. Port ownership and busy-user destruction remain unimplemented. +- [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 +- [sceAudioOutExGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) - native validation requires first argument zero, a non-null buffer and exactly 1024 bytes, in that order (errors 0x80260003/04/06); the second argument is ignored. The host still reports no audio descriptors by zeroing the record. Native content comes from monitor/device services and system audio configuration, which the SDL backend does not reproduce. +- [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the two 32-bit arguments and selector validation match the system library: selector 0 accepts outputs 0 through 2; selector 255 accepts outputs 1 and 2. No third latency argument exists. The host has no system 3D rendering stage, so the call still does not apply latency and does not reproduce the system initialization and output-availability checks. +- [sceAudioOut2MasteringInit](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) - native initialization requires AudioOut2 initialization (0x80268006 otherwise), returns success immediately when already initialized, and otherwise rejects nonzero flags with 0x80268201. Term returns 0x80268203 without mastering initialization and clears the mastering state after a successful service call. The protected Init body remains unchanged; host Term still returns success without this lifecycle or native mastering allocations. +- [sceAudioOut2MasteringGetState and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - the state layout, the states ids 1 (default) and 2 (V2), the descriptor ids `0x01010001`, `0x01020001` and `0x01010003` and the errors `0x80268201` and `0x80268204` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. GetState zeroes the states and writes the two descriptors; with no mastering stage the compressor and limiter values stay zero. SetParam accepts any parameter set and records nothing, as KytyPS5 does; a null parameter returns `0x80268201`, the mastering invalid-API-parameter error KytyPS5 returns for a null state, which is an inference rather than a measurement. The meaning of `output` and `user` is unknown and both are ignored +- [sceAudioOut2SetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - records the debug state id in the trace and returns 0, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) does; the host has no system mix to apply it to. Id `0x20` with a 4-byte param throws, and the not-initialized error is not returned. Not measured on a console +- [sceAudioOut2GetSpeakerArrayCoefficients](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - validates the handle and the coefficient count like the ambisonics call, then writes the front pair at full gain and silence for every other speaker, ignoring the position, spread, height awareness and downmix spread radius: [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) and [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/audio/hle_audio.cpp) both return that pair. The console's VBAP gains over the array's directions are not measured - [sceAudioOutSetMixLevelPadSpk](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - no source applies the level (shadPS4 only stores it); it is assumed to be a linear gain on the port's own output, and the -9 dB default (11626) is applied to every pad speaker port, including those of titles that never call the function - [sceAudioOutGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - signature and behaviour taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [fpPS4](https://github.com/red-prig/fpPS4) reimplementations of the PS4 library, not confirmed on a PS5 title: the time is the process time at which `sceAudioOutOutput` or `sceAudioOutOutputs` accepted the port's last block, not the time the block reached the device -- [sceAudioOutSysClose, sceAudioOutSysConfigureOutput, sceAudioOutSysOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99051 and PPSA99997, which fail to load without them; added without an implementation and declared without parameters. [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp#L166-L240), whose audio code both titles share, calls `sceAudioOutSysOpen`, `sceAudioOutSysConfigureOutput` and `sceAudioOutSysClose` only to open a port for HDMI bitstream passthrough. Its declared arguments are not confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no HDMI output to switch +- [sceAudioOutSysOpen, sceAudioOutSysConfigureOutput, sceAudioOutSysClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99051 and PPSA99997, which fail to load without them; the signatures follow [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp#L24-L29), whose audio code both titles share, and are not confirmed on a console. `sceAudioOutSysOpen(user, mode)` opens a port only for mode 5, the TrueHD MAT route: [the same project's carrier table](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/iec61937.cpp#L125-L140), read from sceAudioOutExOpen's own tables in firmware 12.70, describes it as a 768 kHz port of 1024 S16 8-channel frames whose 16-byte frames are consumed at 192 kHz; every other mode throws. Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so like an Ex port a Sys port has no host device: `sceAudioOutOutput` paces it at its own rate and drops the MAT bursts. `sceAudioOutSysConfigureOutput(type, flags, mode, target, option)` accepts only the observed `(1, 0, 5, 1, 0)` HDMI MAT switch and changes nothing; other arguments throw. `sceAudioOutSysClose` closes a Sys port and throws for any other handle. The restore path EVO-PLAYER-PS5 takes after a Sys port calls `sceAudioOutExConfigureOutput(0, 0, 0xFF, 0xFF, 0)`, so no Sys reset combination is known - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - MAIN (type 0) format validation follows PS5 firmware 13.60 measurements: bits 8-11 accept 1, 2, 8 or 12, bits 0-6 accept 0 or 1, bit 7 requires 8 channels, and bits 12-31 do not affect validation. The sample types are still read as float and int16; bit 7's layout, the PCM effect of bits 12-31 and the port `flags` are unverified. Other port types retain unverified format validation, including 6 channels and rejection of bits 12-31 - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - 12 channels are assumed to be L R C LFE Ls Rs Lb Rb Ltf Rtf Ltb Rtb as in KytyPS5's 12-to-8 fold, and 6 and 8 channels to be its first 6 and 8; no title or SDK header confirms the order - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the stereo fold is assumed, not the console's downmix: C at -3 dB into both sides, Ls/Rs, Lb/Rb and Ltf/Rtf at -3 dB and Ltb/Rtb at -6 dB into their own side, LFE dropped - [sceAudioOut2PortSetAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - each PCM buffer set (attribute 0) is taken as one grain: buffers set between two pushes are queued and played one per grain, at most 16 queued. A grain with no new buffer repeats the last one only when the port has always been given that same buffer (rewritten in place); a port whose buffers move on plays silence instead. Inferred from PPSA12544, whose object port (type 6) is given one grain of a 64-grain ring per set, often two or three between pushes and then none for a few; the console behaviour for several sets per push and for a push without a new buffer is not documented -- [sceAudioOutDeviceIdOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA01317, which fails to load without it; declared without parameters, signature unknown. It opens a port on a given audio device; which device ids exist and how they map to host outputs is unknown, so it throws +- [Legacy vibration ports](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp): PPSA99902's [public input wrapper](https://github.com/Nazky/UnleashedRecomp-Prospero/blob/2ce067a1d51bb58670c7ee4a3aed893c504da805/UnleashedRecomp/hid/driver/sdl_hid.cpp#L316) opens type 10 with 256 stereo S16 or F32 frames at 48000 Hz. Mono duplicates its channel as AudioOut2 does; wider vibration layouts throw. Native controller output uses AudioOut2's four-channel layout, or its six-channel PulseAudio mapping. Without a controller sound card the existing RMS motor transform is applied to segments of at most 256 frames, scheduled by the legacy queue's frame count and rate; drain and close remove only that port's segments. Concurrent sources combine by the maximum per motor, and the pad consumer uses this approximation only for controllers other than DualSense. This host timing and motor model is not a console measurement or waveform reproduction; legacy native output is unmeasured on hardware, and user routing retains the single-controller limitation. AudioOut2 retains its separate 80 ms peak hold. + +- [sceAudioOutDeviceIdOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the native wrapper consumes a 64-bit device ID, port type, sample count, frequency and format from argument slots 1, 2, 4, 5 and 6. Slot 3 is discarded, with the common open API index position preserved. Device enumeration and mapping to host outputs remain unimplemented; the throwing body is unchanged. +- [sceAudioOut2PortGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) and [sceAudioOut2GetSpeakerInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - the port state reports the channel count of the port's data format, output main, volume 127 and a zero reroute counter and flags. Taking the channel count from the data format follows [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs), another emulator. A payload process on a PS5 (firmware 13.60), which is not the foreground application, read output 0, volume -1 and reroute counter 5 for its main ports, and 6 channels for an 8-channel port on a 5.1 system, so a title's values are unverified, including the output of a pad port, the volume after `sceAudioOut2PortSetAttributes` and the reroute counter on a device change. The speaker info reports type 0 with available bits `0x3`, the front left speaker at +30 degrees and the front right one at -30, and -360 for the azimuth and elevation of every other slot. On that console the type followed the HDMI device type and the available bits its channels: type 0 with the device set to TV, 1 to AV amplifier and 2 to sound bar, which [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) names television, AV receiver and sound bar (3 is headphones there), and available bits `0x3` for 2 channels and `0x3F` for 5.1 (centre at 0, surround pair at +110 and -110, -360 for the LFE). The front pair and the empty slots read the same in every setting; 7.1 on the AV amplifier reported 2 channels on that console's HDMI chain. The host output is always reported as a stereo television here. The console also returns `0x80268006` before `sceAudioOut2Initialize`, `0x80268001` for flags above 1 and `0x8026800C` for a null pointer, which are not modelled. ASTRO BOT and ASTRO's PLAYROOM reportedly stop on an assertion that depends on these values (#4317), but which field matters has not been isolated +- [sceAudioOut2UserCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutOpenEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetSystemDebugState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetHandleStatusInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetSimulatedHandleStatusInfo2](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutChangeAppModuleState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetSimulatedHandleStatusInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetSimulatedBusUsableStatusByBusType](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutAttachToApplicationByPid](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutDetachFromApplicationByPid](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutPtOpen](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutPtClose](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutPtGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExPtOpen](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExPtClose](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExPtGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetInfoOpenNum](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutMasteringInit](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutMasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutMasteringGetState](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutMasteringTerm](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetPadSpkVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetPadJackVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetUsbVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetCaesarVolume](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetHeadphoneOutMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutGetFocusEnablePid](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetMainOutput](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetVolumeDown](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetChatBalance](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetRecMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetAllMute](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetSparkEQ](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutArbitrationInitialize](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ArbitrationInitialize](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnable3DAudioByArbitration](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutAssignHRTF](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnable3DAudioForUser](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableMonoHpOutput](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableTVVirtual](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableTVCorrection](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableVrControllerLowPowerMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutEnableCinematicVrRotation](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutCustomHrtfIsExist](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExConfigureOutputMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExSystemInfoIsSupportedAudioOutExMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExGetSystemInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutExGetOutputInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysConfigureOutputMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysHdmiMonitorInfoIsSupportedAudioOutMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysGetSystemInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSysGetOutputInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusInit](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetPortConnections](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetConnections](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetPortStatuses](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetHeadphoneOutMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusUserLoginFor3DAudio](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetDeviceConnections](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetMorpheusParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetCaesarParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MbusSetSparkParam](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2AllAudioMainToHeadphoneForDiag](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoPortGetStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ReportRegisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ReportUnregisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetIpcHandle](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetSystemStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) accepts only flag bit 31, following the shared Init/InitEx validation routine in the decrypted PS5 12.00 `libSceAudioOut` analyzed for #9027. The mode is not applied because this implementation has no mastering stage; other firmware versions are unverified. +- [sceAudioOut2SetDebugHeadphoneMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2DebugStateCtrl](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ContextBedWrite](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2PortCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2InitializeEx](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the PS5 12.00 module (0x2a640) stores its 32-bit argument and runs `sceAudioOut2Initialize`; the argument's meaning is unknown and it is ignored +- [sceAudioOut2Terminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2PortRegister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2PortUnregister](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetAmbisonicsEncodeCoefficients](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetHrtfIdForCronos](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature ### Functional -- [sceAudioOut2EnableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) takes no arguments and returns 0, as declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L460-L464) ("Allows chat audio alongside the application's own"). The host mixes no chat audio and AudioOut2 has no chat port, so the call only records that chat audio is allowed. Whether the console fails it without a chat session, when repeated or before `sceAudioOut2Initialize` is unknown. [sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) is the counterpart, imported by PPSA18216: it takes no arguments, returns 0 and records that chat audio is no longer allowed; its signature is not confirmed by a console measurement, and whether the console fails it without a chat session or when repeated is unknown. -- [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit, sceAudioOut2MasteringTerm and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage. -- [AudioOut2 contexts](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device read a grain's port buffers at the next push, at least 100 us after the grain was pushed, and play the mix behind a 40 ms cushion that the queue level leaves out. When the hardware reads the buffers is not known. The timing follows a title whose mixer fills a buffer after pushing it, reuses it two pushes later, and may still be writing it when the next push follows within microseconds. The last grain pushed before a title stops pushing is never read or played. -- [sceAudioOutSysGetHdmiMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports an HDMI sink without a monitor name or audio descriptors: it zeroes the 0x180 bytes and returns 0, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(type, info, size)`, the required size 0x180 and the fields titles read (monitor name at +0x0B, 13 bytes; descriptor count as a 32-bit value at +0x94; 8-byte descriptors `[coding type, channels, rate mask, 0, extra, 0, 0, 0]` from +0x98) come from [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/c3be9c2afcb8f7838784ead0fe1c12a2ceea965a/docs/FINDINGS.md) (firmware 12.70, one monitor). What a console without an HDMI audio sink returns, an error or no descriptors, is unknown. The other bytes, among them the header at +0x90 (`01 09 4f 00` on that monitor), have no known meaning and are written as 0. No LPCM descriptor is reported for the host device, as the rate mask and extra byte of one are not known. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured. Monitor type 0x10, which the console also accepts, other types and sizes other than 0x180 throw: their results and error codes are unknown. +- [sceAudioOut2EnableChat and sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) - both PS5 exports take no arguments, return 0x80268006 before AudioOut2 initialization, and invoke the system chat service with selector 3. A negative service result maps to 0x80268000, otherwise they return zero. The host only records a local enabled flag, without initialization validation or a chat service; repeated-call and no-session behavior therefore still depends on the missing service. +- [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - the two 32-bit arguments and selector validation match the system library: selector 0 accepts outputs 0 through 2; selector 255 accepts outputs 1 and 2. No third latency argument exists. The host has no system 3D rendering stage, so the call still does not apply latency and does not reproduce the system initialization and output-availability checks. +- [AudioOut2 contexts](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device and no `sceAudioOut2ContextAdvance` calls read a grain's port buffers at the next push, at least 100 us after the grain was pushed, and play the mix behind a 40 ms cushion that the queue level leaves out. When the hardware reads the buffers is not known. The timing follows a title whose mixer fills a buffer after pushing it, reuses it two pushes later, and may still be writing it when the next push follows within microseconds. The last grain pushed before a title stops pushing is never read or played. +- [sceAudioOut2ContextGetQueueLevel](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device counts the grains that `sceAudioOut2ContextAdvance` completed and no push has consumed as queued, together with the pushed grains beyond the 40 ms cushion, up to `queue_depth`. Whether the console counts them is unknown. Pushes that fill the cushion leave the level at 0, so a title that renders as many grains as the level leaves room for and pushes them on another thread rendered a full queue again after each of those pushes: PPSA23226's Wwise sink passes the available count to the sound engine as the number of frames to render, advances once per frame and pushes on a separate thread, and exceeded 16 completed grains. Counted, the completed grains keep such a title at most `queue_depth` grains ahead of its pushes. +- [sceAudioOut2LoInit, sceAudioOut2LoContext* and sceAudioOut2LoPort* calls](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) have no low-latency path: each forwards to its `sceAudioOut2` counterpart, so grain and queue depth are the context's `num_grains` and `queue_depth`, and playback keeps the 40 ms cushion. [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L489-L545) only says each call takes the same handles and parameter blocks as its counterpart and drives the low-latency clock with a shallow queue and a small grain ([docs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/docs/audio.md#L662-L668)). Unverified: the console's grain and depth limits, its error for a handle of the other service (accepted here) and what `sceAudioOut2LoInit` returns when repeated (0 here). +- [sceAudioOutSysGetHdmiMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) - native validation requires a non-null buffer and exactly 384 bytes, then accepts types 1 and 16. Mapped pointer/size/type errors are 0x80260004/06/0C. The host still zeroes type 1 output and throws for type 16: native monitor descriptors and system audio settings are unavailable. Driver initialization failures and service errors are not represented. diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 0785892d93..ee209da65f 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -2,9 +2,10 @@ ### Silent stubs -- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested and sources report no audio paths. Ray results, path calculation and audio paths are accepted only when empty and throw otherwise; `sceAudioPropagationSourceRender` writes silence (zeroes) to each source's output buffer for the given size, without a dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) lacks acoustic propagation: attributes have no effect, sources expose no paths or rays, rendering preserves the destination mix without adding path contributions, and allocated CPU work memory is mostly unused. Ray results and audio paths accept empty lists and throw otherwise; calculation returns initial native validation errors and throws for unsupported propagation. Room, portal, source and material handles are tracked, but destruction after their system is destroyed silently succeeds without native lifetime semantics being confirmed. Occlusion, reflections and reverb are absent. ### Unknown function info -- [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation, NID `tKSmk2JsMAA`) - `(uint64_t path, const void* data, float gain)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs) and SAROS's call, which passes a handle from `sceAudioPropagationSourceGetAudioPath`, path data and gain 1.0. Path updates are unsupported and every call throws without reading or writing caller memory. The no-propagation model yields no path handles; path allocation, ownership, rendering and console error codes remain unknown. -- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation) - signatures and struct descriptors recovered from PPSA21567's calls (#481), not from documentation. The system options layout is unknown (only checked for non-null); the fourth argument of `sceAudioPropagationSourceCalculateAudioPaths` is unknown; `sceAudioPropagationSourceGetAudioPath` is assumed to output an 8-byte path handle. The meaning of the `RenderInfo` word at +0x28 is unknown (PPSA21567 and PPSA21564 pass 2); only 2 is accepted. Both titles pass one `RenderInfo` per call: a count of 0 throws, and larger counts are handled element by element without proof. No error code is known, so invalid input throws instead of returning one +- [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): the native path lookup and update confirm a 64-bit path handle, a read-only 224-byte path description and a float gain in XMM0. The description contains four 48-byte points, a point count and flags; each point contains a descriptor, room handle, gain and three position floats. The last eight bytes of each point and of the description are not consumed by these paths and remain opaque. Protected implementation remains unchanged; allocation, ownership and rendering are not implemented. +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): system options use a recovered 56-byte record; the field at byte 24 and final eight bytes remain semantically unresolved. CPU memory requirements and option limits are recovered; GPU memory queries throw because their platform-specific runtime condition is unresolved. Descriptor validation, ray ownership and path generation are unimplemented. Native rendering accepts multiple mutable records; the host supports only format 2 and writes one silent frame without native accumulation. Frame capacity and source-count limits follow the recovered options. Other formats and native validation precedence remain unimplemented. +- [Initialization exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) ending in `Init`, plus `sceAudioPropagationSystem`, are not exported: the SDK declares the helpers `static inline` and none is in the SDK stub library or the supplied native module. Query, locking, enumeration and report callback signatures are recovered; diagnostic layouts are recovered, but source attributes 2/3 and the constant-one fields still lack semantic names. The final system word and the fourth position/direction words have no confirmed semantics. diff --git a/docs/dev/debt/libSceAudioSystem.md b/docs/dev/debt/libSceAudioSystem.md new file mode 100644 index 0000000000..0595a501f9 --- /dev/null +++ b/docs/dev/debt/libSceAudioSystem.md @@ -0,0 +1,5 @@ +# libSceAudioSystem technical debt + +### Unknown function info + +- [sceAudioSystemSystemResume and sceAudioSystemSystemSuspend](../../../core/libs/prx/libSceAudioSystem/Export.cpp) (libSceAudioSystem) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAudioSystem.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceAudiodec.md b/docs/dev/debt/libSceAudiodec.md index 6e1bcb974e..ac9b492ea4 100644 --- a/docs/dev/debt/libSceAudiodec.md +++ b/docs/dev/debt/libSceAudiodec.md @@ -2,8 +2,12 @@ ### Unknown function info +- [M4AAC extended parameters](../../../core/libs/prx/libSceAudiodec/Export.cpp): 28-byte and 32-byte parameter variants add fields at offsets 24 and 28. Their accepted ranges are 0..1 and 0..2; nonzero modes throw until the corresponding decoder configuration is implemented. Zero extensions use the base decoding configuration. + - [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - a raw M4AAC stream carries no channel configuration, so the decoder tries the mono/stereo configuration and then configurations 3 to 7 within `ui_max_channels` until one decodes the first access unit, and keeps that one. No reference documents how the library picks the layout (shadPS4 maps the channel count straight to the configuration; SharpProspero only describes the maximum). With the stereo configuration a mono raw stream decodes as 2 channels, so 2 is reported, not 1 +- [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - the HE-AAC handling follows one console measurement of an HE-AAC v2 ADTS stream: SBR output at 48 kHz in one channel with `ui_enable_heaac` 1, the 24 kHz mono core with 0. Not measured: whether the console, like FFmpeg, decodes a stream without SBR data in its first access unit as AAC-LC with `ui_enable_heaac` 1, and what it reports in `ui_sampling_freq` and `ui_heaac` with `ui_enable_heaac` 0 (here the rate of the returned PCM and 0) ### Functional -- [libSceAudiodec](../../../core/libs/prx/libSceAudiodec/Export.cpp) throws for the 24-bit PCM word size (`iBwPcm` 0): its sample layout is unknown. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. +- [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - an ADTS stream with channel configuration 0 (layout in a program config element) is still parsed by FFmpeg, so `ui_enable_heaac` 0 does not remove its SBR and a mono one with Parametric Stereo comes out as stereo. +- [libSceAudiodec](../../../core/libs/prx/libSceAudiodec/Export.cpp) accepts only 16-bit PCM, and float PCM for ATRAC9 and M4AAC, as `sceAudiodecCreateDecoder` did in a payload process on a PS5 (firmware 13.60), which returned `ERROR_INVALID_WORD_LENGTH` for `iBwPcm` 0, 3 and -1 with each codec and for float with MP3; other word sizes are assumed to fail the same way. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. diff --git a/docs/dev/debt/libSceAudiodec.native.md b/docs/dev/debt/libSceAudiodec.native.md new file mode 100644 index 0000000000..01bc5f3533 --- /dev/null +++ b/docs/dev/debt/libSceAudiodec.native.md @@ -0,0 +1,5 @@ +# libSceAudiodec.native technical debt + +### Unknown function info + +- [libSceAudiodec.native pending exports](../../../core/libs/prx/libSceAudiodec.native/Export.cpp) - 14 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAudiodec.native.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceAudiodecCpu.md b/docs/dev/debt/libSceAudiodecCpu.md new file mode 100644 index 0000000000..d249001e8d --- /dev/null +++ b/docs/dev/debt/libSceAudiodecCpu.md @@ -0,0 +1,5 @@ +# libSceAudiodecCpu technical debt + +### Unknown function info + +- [libSceAudiodecCpu pending exports](../../../core/libs/prx/libSceAudiodecCpu/Export.cpp) - 8 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAudiodecCpu.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceAutoMounterClient.md b/docs/dev/debt/libSceAutoMounterClient.md new file mode 100644 index 0000000000..2996a7b44b --- /dev/null +++ b/docs/dev/debt/libSceAutoMounterClient.md @@ -0,0 +1,6 @@ +# libSceAutoMounterClient technical debt + +### Unknown function info + +- [sceAutoMounterClientRegisterCallback, sceAutoMounterClientTerm and sceAutoMounterClientUnregisterCallback](../../../core/libs/prx/libSceAutoMounterClient/Export.cpp) (libSceAutoMounterClient) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAutoMounterClient.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceAutoMounterClientGetUsbDeviceInfo](../../../core/libs/prx/libSceAutoMounterClient/Export.cpp): the 22 metadata bytes at offset 128 remain opaque. The 944-byte output layout, 128-byte device name and three 256-byte arrays with 64-bit lengths are recovered. diff --git a/docs/dev/debt/libSceAvPlayer.md b/docs/dev/debt/libSceAvPlayer.md index 5b8396edbf..fb24c0d166 100644 --- a/docs/dev/debt/libSceAvPlayer.md +++ b/docs/dev/debt/libSceAvPlayer.md @@ -7,12 +7,16 @@ ### 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 - [sceAvPlayerInitEx](../../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - the PS5 init data layout (`this_size` 0x230: auto start at 0x74, nine 0x30-byte thread parameter blocks from 0x78, output video frame buffer count at 0x228) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/avPlayer.cpp), [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/video/avplayer.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Media/AvPlayer.cs) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs), which agree on it. PPSA25063 fills the same fields, writes auto start 0 and starts playback with `sceAvPlayerStart`; a title that sets auto start has not been seen. A smaller `this_size` is read with the PS4 offsets. The thread parameters are ignored - [AvPlayer auto start](../../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - the auto start that `sceAvPlayerAddSource` queues for a player with auto start or without an event callback starts the player only while it is still ready: a title that starts the player itself before the auto start runs keeps its playback and the frames it holds. Not confirmed on a title. +- [Paused initial video](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) keeps the first picture available when audio has already established the playback epoch before the caller pauses. Further pictures of that epoch stay blocked, as after a paused jump. The console's exact initial preparation ordering has not been measured. + ### Functional +- [sceAvPlayerGetVideoData / sceAvPlayerGetVideoDataEx](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - delivers a video frame up to 0.75 × frame duration ahead of its timestamp in default sync mode to accommodate presentation loops that poll slightly before the refresh boundary; the console's exact presentation tolerance is unverified. - [sceAvPlayerGetVideoData](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) in sync mode `SCE_AVPLAYER_AV_SYNC_MODE_NONE` skips queued frames that the audio clock has already passed while the title pulls audio, so a title that requests video slower than the film's frame rate keeps the picture with the sound. Whether the console hands out every frame in this mode is unknown. PPSA12544 requests 7-19 frames per second for a 30 fps cinematic diff --git a/docs/dev/debt/libSceAvPlayer.native.md b/docs/dev/debt/libSceAvPlayer.native.md new file mode 100644 index 0000000000..85b1b75aba --- /dev/null +++ b/docs/dev/debt/libSceAvPlayer.native.md @@ -0,0 +1,5 @@ +# libSceAvPlayer.native technical debt + +### Unknown function info + +- [libSceAvPlayer.native pending exports](../../../core/libs/prx/libSceAvPlayer.native/Export.cpp) - 2 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvPlayer.native.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceAvPlayerStreaming.md b/docs/dev/debt/libSceAvPlayerStreaming.md new file mode 100644 index 0000000000..4ef5238bac --- /dev/null +++ b/docs/dev/debt/libSceAvPlayerStreaming.md @@ -0,0 +1,7 @@ +# libSceAvPlayerStreaming technical debt + +### Unknown function info + +- [libSceAvPlayerStreaming pending exports](../../../core/libs/prx/libSceAvPlayerStreaming/Export.cpp) - 33 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvPlayerStreaming.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. + +- [libSceAvPlayerStreaming pending exports](../../../core/libs/prx/libSceAvPlayerStreaming/Export.cpp) - 33 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvPlayerStreaming.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceAvSetting.md b/docs/dev/debt/libSceAvSetting.md new file mode 100644 index 0000000000..6bbd62abd0 --- /dev/null +++ b/docs/dev/debt/libSceAvSetting.md @@ -0,0 +1,28 @@ +# libSceAvSetting technical debt + +### Unknown function info + +- [sceAvControlGetPuTypeByMonitorInfo](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlIsVideoOutModeSupportedByMonitorInfoWithOptions](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlSetDitherModeForDiag](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlSetLayoutModeForDiag](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlSetMonitorFakeMode](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlGetCurrentOutputSysMode](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlGetNativeHdmiMonitorInfo](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlGetRawMonitorInfo](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlGetKsvList](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlGetRawEdid](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlRemoveProcessOutputMode](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlUpdateDriverStatus](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlNotifyUmdEvent](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlAddCallbacksForLnc](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlAddCallbacksForResourceArbitrator](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlRemoveCallbacks](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlCheckCallback](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlWaitCallback](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlSetColorMode](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlCheckMonitorInfoCapability](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlCheckVideoOutMode](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlSetCheckoutputMode](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlSetSystemVideoMode](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature +- [sceAvControlDebugAddCallbacks](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature diff --git a/docs/dev/debt/libSceBackupRestoreUtil.md b/docs/dev/debt/libSceBackupRestoreUtil.md new file mode 100644 index 0000000000..925f4c078c --- /dev/null +++ b/docs/dev/debt/libSceBackupRestoreUtil.md @@ -0,0 +1,5 @@ +# libSceBackupRestoreUtil technical debt + +### Unknown function info + +- [sceBackupRestoreUtilBackupData, sceBackupRestoreUtilCancelBackupData, sceBackupRestoreUtilCancelGetRequiredSize, sceBackupRestoreUtilCancelGetTitles, sceBackupRestoreUtilCancelRestoreData, sceBackupRestoreUtilDeleteBackupData, sceBackupRestoreUtilGetBackupDataInfo, sceBackupRestoreUtilGetBackupParam, sceBackupRestoreUtilGetBackupProgress, sceBackupRestoreUtilGetRequiredSize, sceBackupRestoreUtilGetRestoreProgress, sceBackupRestoreUtilGetTitles, sceBackupRestoreUtilInitialize, sceBackupRestoreUtilPrepareBackup, sceBackupRestoreUtilRestoreData and sceBackupRestoreUtilTerminate](../../../core/libs/prx/libSceBackupRestoreUtil/Export.cpp) (libSceBackupRestoreUtil) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBackupRestoreUtil.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceBeisobmf.md b/docs/dev/debt/libSceBeisobmf.md new file mode 100644 index 0000000000..643f09dd5e --- /dev/null +++ b/docs/dev/debt/libSceBeisobmf.md @@ -0,0 +1,7 @@ +# libSceBeisobmf technical debt + +### Unknown function info + +- [libSceBeisobmf pending exports](../../../core/libs/prx/libSceBeisobmf/Export.cpp) - 18 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBeisobmf.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. + +- [libSceBeisobmf pending exports](../../../core/libs/prx/libSceBeisobmf/Export.cpp) - 18 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBeisobmf.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceBemp2sys.md b/docs/dev/debt/libSceBemp2sys.md new file mode 100644 index 0000000000..1ca60eb890 --- /dev/null +++ b/docs/dev/debt/libSceBemp2sys.md @@ -0,0 +1,7 @@ +# libSceBemp2sys technical debt + +### Unknown function info + +- [libSceBemp2sys pending exports](../../../core/libs/prx/libSceBemp2sys/Export.cpp) - 31 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBemp2sys.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. + +- [libSceBemp2sys pending exports](../../../core/libs/prx/libSceBemp2sys/Export.cpp) - 31 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBemp2sys.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceBgft.md b/docs/dev/debt/libSceBgft.md new file mode 100644 index 0000000000..385f701d0f --- /dev/null +++ b/docs/dev/debt/libSceBgft.md @@ -0,0 +1,7 @@ +# libSceBgft technical debt + +### Unknown function info + +- [sceBgftServiceDownloadGetProgress](../../../core/libs/prx/libSceBgft/Export.cpp), `sceBgftServiceIntDownloadGetProgress`, `sceBgftServiceIntDownloadGetGameAndGameAcProgress` and `sceBgftServiceIntDownloadGetPlayGoProgress`: signatures and the shared 88-byte output are confirmed. The library forwards the output through IPC without accessing individual fields; their types remain unresolved. +- [sceBgftServiceIntDownloadRegisterTaskByStorageEx](../../../core/libs/prx/libSceBgft/Export.cpp): confirmed parameter pointer and 32-bit task ID output. The input contains 32-bit scalars at offsets 0, 4, 56 and 104, strings at offsets 8, 16, 32, 40, 48, 64, 72, 80 and 88, and a 64-bit scalar at offset 96. The typed layout preserves the unused eight bytes at offset 24; scalar meanings and several string roles remain unresolved. +- [sceBgftServiceIntDownloadGetPatchProgress](../../../core/libs/prx/libSceBgft/Export.cpp) and `sceBgftServiceIntDownloadGetTaskInfo`: neither export nor a caller was found in the supplied decompiled library corpus. Their signatures remain unresolved. diff --git a/docs/dev/debt/libSceBluetoothHid.md b/docs/dev/debt/libSceBluetoothHid.md index d5e47f67fa..faf41e09da 100644 --- a/docs/dev/debt/libSceBluetoothHid.md +++ b/docs/dev/debt/libSceBluetoothHid.md @@ -2,8 +2,9 @@ ### Silent stubs -- [libSceBluetoothHid](../../../core/libs/prx/libSceBluetoothHid/Export.cpp) implements only the no-device path: initialization and callback and device registration succeed, the callback is never called because no device connects, and calls that need a connected device throw +- [libSceBluetoothHid](../../../core/libs/prx/libSceBluetoothHid/Export.cpp): the host still has no device backend or event worker. Registration retains only lifecycle state and never delivers the recovered 104-byte callback events. Custom worker scheduling throws; device ioctl failures and resource exhaustion are not modeled. ### Unknown function info -- [libSceBluetoothHid](../../../core/libs/prx/libSceBluetoothHid/Export.cpp) (libSceBluetoothHid) - signatures taken from PPSA01317: `sceBluetoothHidInit()` and `sceBluetoothHidUnregisterCallback()` without arguments, `sceBluetoothHidRegisterCallback(callback, 0, param)` with a 16-byte param, and `sceBluetoothHidRegisterDevice` / `sceBluetoothHidUnregisterDevice(vendorId, productId)`; the report and device name exports are declared without parameters +- [sceBluetoothHidGetDeviceName and sceBluetoothHidGetDeviceInfo](../../../core/libs/prx/libSceBluetoothHid/Export.cpp): output pointers are forwarded to device ioctls without accessing the buffers. Their output sizes and layouts remain unresolved. Report APIs use 64-bit device handles; device-info and disconnect APIs use 32-bit device IDs. +- [sceBluetoothHidDebugGetVersion](../../../core/libs/prx/libSceBluetoothHid/Export.cpp): returns a constant without reading arguments, so the supplied implementation does not establish whether the public prototype has unused parameters. diff --git a/docs/dev/debt/libSceCdlgG2P.md b/docs/dev/debt/libSceCdlgG2P.md index 2549e5ceee..ad18657b3d 100644 --- a/docs/dev/debt/libSceCdlgG2P.md +++ b/docs/dev/debt/libSceCdlgG2P.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceCdlgG2P](../../../core/libs/prx/libSceCdlgG2P/Export.cpp) - the G2P dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled. The signatures (`Open` and `GetResult` take a pointer, the others none) and the leading `int32` result are assumed from the other common dialogs, not verified for G2P; PPSA01289 imports it +- [libSceCdlgG2P](../../../core/libs/prx/libSceCdlgG2P/Export.cpp) - the G2P dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled. The 128-byte open parameters, common-header validation and result field at byte 8 are recovered. Native service state transitions remain unimplemented; PPSA01289 imports it diff --git a/docs/dev/debt/libSceCdlgPlayerReview.md b/docs/dev/debt/libSceCdlgPlayerReview.md index bdfe9125fe..0e97a3d72d 100644 --- a/docs/dev/debt/libSceCdlgPlayerReview.md +++ b/docs/dev/debt/libSceCdlgPlayerReview.md @@ -2,7 +2,7 @@ ### Unknown function info -- [scePlayerReviewDialogOpen, scePlayerReviewDialogGetResult](../../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - the PS5-only dialog has no public reference: the parameter is not read and the result is assumed to start with the 32-bit common dialog result, as `sceWebBrowserDialogGetResult` assumes +- [scePlayerReviewDialogOpen](../../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) uses a recovered 168-byte parameter layout; the 40-byte target record at byte 60 and mode semantics remain unresolved. ### Functional diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 99cf6f6734..55ef735ddc 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -2,6 +2,15 @@ ### Silent stubs -- [sceCesMbcsUcsContextInit](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns success without writing to the context, and returns the invalid-parameter error for a null context. The signature `int sceCesMbcsUcsContextInit(union* context, const union* mbcsUcsPrf)` follows the [Orbital IDA type database](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json); the context layout and its size are unknown, so nothing is stored. PPSA15246 calls it and imports no other function that takes the context; no conversion through an initialized context exists. `sceCesRefersUcsProfileCp1251` returns a profile accepted by `sceCesSbcToUtf8` and `sceCesUtf8ToSbc` only, with the Windows-1251 table of Microsoft's [bestfit1251.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1251.txt), where byte 0x98 (unassigned) maps to U+0098 like the unassigned Cp1252 bytes; the console behavior for that byte is not verified -- [sceCesUcsProfileInitSJis1997Cp932](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns the sheet argument without writing profile tables to it, and returns null for a null pointer; Shift-JIS conversion tables are unimplemented. The signature `union* sceCesUcsProfileInitSJis1997Cp932(SceCesUcsProfileSheet* sheet)` follows the [Orbital IDA type database](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) +- [sceCesMbcsUcsContextInit](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) stores the profile pointer in the first 8 bytes of the caller's context; the console's context layout is not known. It returns the invalid-parameter error for a null context. The signature `int sceCesMbcsUcsContextInit(union* context, const union* mbcsUcsPrf)` follows the [Orbital IDA type database](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json). `sceCesRefersUcsProfileCp1251` returns a profile accepted by `sceCesSbcToUtf8` and `sceCesUtf8ToSbc` only, with the Windows-1251 table of Microsoft's [bestfit1251.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1251.txt), where byte 0x98 (unassigned) maps to U+0098 like the unassigned Cp1252 bytes; the console behavior for that byte is not verified +- [EUC-JP (CP51932) conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs): `sceCesUcsProfileInitEucJpCp51932` marks the caller's sheet with an 8-byte tag and returns it as the profile. `sceCesUtf8StrToMbcsStr`, `sceCesUcs2StrToMbcsStr`, `sceCesMbcsStrToUtf8Str`, `sceCesMbcsStrToUcs2Str`, their `Get*Len` forms and `sceCesEucJpToUcs2` convert ASCII, the JIS X 0201 half-width katakana (`0x8E` + `0xA1`-`0xDF`, U+FF61-U+FF9F) and the two-byte rows `0xA1`-`0xFE` for that profile, and every character for the CP1252 and CP1251 profiles. The two-byte table ([JisX0208.hpp](../../../core/libs/prx/libSceCesCs/JisX0208.hpp), 7336 of 8836 cells) is Microsoft code page 932 read through the standard JIS row/cell to Shift-JIS arithmetic, so it includes NEC row 13 and the NEC-selected IBM rows 89-92 and maps the Microsoft variants (U+FF5E at `0xA1C1`, U+2225, U+FF0D, U+FFE0-U+FFE2) instead of JIS X 0208's; that the console's CP51932 table equals it is assumed from the Microsoft code page, not measured. A character with several cells encodes to the lowest cell. A well-formed but unassigned cell answers `UNASSIGNED_CODE`, a trail byte outside `0xA1`-`0xFE` `INVALID_ENCODE`, a cut pair `SRC_BUFFER_END`; the console's errors for these are not known. Three-byte JIS X 0212 (`0x8F`) characters convert through a second table ([JisX0212.hpp](../../../core/libs/prx/libSceCesCs/JisX0212.hpp), 6067 of 8836 cells) read from the Unicode consortium's [JIS0212.TXT](https://unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/JIS0212.TXT); that the console's CP51932 table equals it is assumed from that mapping, not measured. A well-formed but unassigned three-byte cell answers `UNASSIGNED_CODE` and a cut triple `SRC_BUFFER_END`. A character with cells in both JIS X 0208 and JIS X 0212 encodes to the two-byte form, and one only in JIS X 0212 to the three-byte form. Signatures follow [orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json). The string forms stop at the source limit or at a zero code unit, which is neither converted nor counted, and write no terminator; `*Len` outputs count code units consumed and produced. A code point outside the BMP answers `UNASSIGNED_CODE`. Unverified on hardware. +- [Shift-JIS (CP932) conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs): `sceCesUcsProfileInitSJis1997Cp932` marks the caller's sheet with an 8-byte tag and returns it as the profile. `sceCesUtf8StrToMbcsStr`, `sceCesUcs2StrToMbcsStr`, `sceCesMbcsStrToUtf8Str`, `sceCesMbcsStrToUcs2Str`, and their `Get*Len` forms convert ASCII, JIS X 0201 half-width katakana (`0xA1`-`0xDF`, U+FF61-U+FF9F), two-byte Shift-JIS sequences (`0x81`-`0x9F` and `0xE0`-`0xFC` lead, `0x40`-`0x7E` and `0x80`-`0xFC` trail) mapped to JIS X 0208 row/cell indices against the CP932 table ([JisX0208.hpp](../../../core/libs/prx/libSceCesCs/JisX0208.hpp)), and the IBM extensions (`0xFA40`-`0xFC4B`, 388 characters) decoded to their corresponding JIS X 0208 code points. The user-defined area (`0xF040`-`0xF9FC`, which CP932 maps to Private Use Area U+E000-U+E757) and unassigned lead/trail combinations answer `UNASSIGNED_CODE`, an invalid lead or trail byte `INVALID_ENCODE`, a cut sequence `SRC_BUFFER_END`. Characters with multiple cells encode to the lowest cell. Signatures follow [orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json). Unverified on hardware. +### Unknown function info + +- [Unicode scalar conversions](../../../core/libs/prx/libSceCesCs/Unicode.cpp) (libSceCesCs) - console error precedence, optional length pointers and length units are unverified. Signatures follow the [Orbital type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); names are in the aerolib table pinned by `tools/nid_names.py`. Encoding follows [Unicode section 3.9](https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-3/). Each call converts one scalar, including zero and U+FEFF, without a terminator or byte-order detection; lengths count bytes for UTF-8 and 16-bit units for UTF-16. Native forms use host byte order (supported hosts are little-endian). Errors follow the existing CES converters, not console measurements, and failure leaves outputs unchanged. No console or title comparison has been run +- [sceCesEucJpToUtf8](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the scalar conversion's errors and optional length pointers are unverified against the console. Its signature follows the Orbital type database linked above; it uses the existing CP51932 profile and JIS tables, with their mapping and profile-layout assumptions listed above, converts one character (including zero), and writes no terminator. Failure leaves the output and length pointers unchanged +- [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), where the unassigned bytes map to their Latin-1 control characters as the Cp1252 and Cp1251 tables do; the console behavior for those bytes and the whole profile is not verified. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) +- [Single-byte profiles](../../../core/libs/prx/libSceCesCs/SingleByte.hpp) (libSceCesCs) - the console's tables for these 27 profiles are not verified. Each returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with bytes 0x00-0x7F unchanged and the high half of a public table: ISO 8859-1, -2, -4, -5, -9, -10, -13, -14, -15 and -16 from [unicode.org](https://unicode.org/Public/MAPPINGS/ISO8859/), code pages 437, 737, 775, 850, 852, 855, 860-863, 865 and 866 from [Microsoft's PC tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/PC/), KOI8-R and KOI8-U from [unicode.org](https://unicode.org/Public/MAPPINGS/VENDORS/MISC/), and 1254, 1256 and 1258 from Microsoft's [best-fit tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/), whose unassigned bytes map to C1 controls as in Cp1250. Profiles whose public table leaves bytes undefined or maps them to private use still throw +- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 222 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables, errors and Big5 duplicate choices are assumed, not measured. `sceCesUcsProfileInitEucKr`, `sceCesUcsProfileInitUhc` and `sceCesUcsProfileInitBig5Cp950` tag the caller's sheet like the EUC-JP profile. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT) and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`. Bytes below `0x80` are ASCII; the KS X 1003 single-byte profile (`sceCesEucKrUcsProfileSetSbcs`) is not implemented. The user-defined areas, which Windows maps to the Private Use Area, answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as Windows code page 950 does. The `*ToUtf8` calls convert one character and leave their outputs unchanged on failure diff --git a/docs/dev/debt/libSceCommonDialog.md b/docs/dev/debt/libSceCommonDialog.md index 0e7ac021bf..f46aaf0394 100644 --- a/docs/dev/debt/libSceCommonDialog.md +++ b/docs/dev/debt/libSceCommonDialog.md @@ -3,3 +3,7 @@ ### Silent stubs - [libSceCommonDialog](../../../core/libs/prx/libSceCommonDialog/Export.cpp) + +### Functional + +- [sceCommonDialogInitialize](../../../core/libs/prx/libSceCommonDialog/Export.cpp) (libSceCommonDialog) - returning `ALREADY_SYSTEM_INITIALIZED` (`0x80B80002`) from a repeated call is not measured on a console by the project. A third-party PS5 log ([obSCEne](https://github.com/project-oops/obSCEne/blob/67b4c529eb3c271184117e26bb54330531ef2cf6/reports/hardware/report-20261014-004800.log#L260-L261), firmware 12.40) records two calls, `0x0` and then `0x80b80002`; a third call is not covered. The SDK error names, shadPS4 (a PS4 emulator) and SharpProspero give the same value; PPSA09695 compares the result with `0x80b80002` (call site 0x5F7BD0) and PPSA06327 stops on `0x80b80004`. Closes when: measured on a PS5, including a third call. diff --git a/docs/dev/debt/libSceCompanionHttpd.md b/docs/dev/debt/libSceCompanionHttpd.md new file mode 100644 index 0000000000..2738a6bd6d --- /dev/null +++ b/docs/dev/debt/libSceCompanionHttpd.md @@ -0,0 +1,5 @@ +# libSceCompanionHttpd technical debt + +### Unknown function info + +- [sceCompanionHttpdAddHeader, sceCompanionHttpdGet2ndScreenStatus, sceCompanionHttpdGetEvent, sceCompanionHttpdGetUserId, sceCompanionHttpdInitialize, sceCompanionHttpdInitialize2, sceCompanionHttpdOptParamInitialize, sceCompanionHttpdRegisterRequestBodyReceptionCallback, sceCompanionHttpdRegisterRequestCallback, sceCompanionHttpdRegisterRequestCallback2, sceCompanionHttpdSetBody, sceCompanionHttpdSetStatus, sceCompanionHttpdStart, sceCompanionHttpdStop, sceCompanionHttpdTerminate, sceCompanionHttpdUnregisterRequestBodyReceptionCallback and sceCompanionHttpdUnregisterRequestCallback](../../../core/libs/prx/libSceCompanionHttpd/Export.cpp) (libSceCompanionHttpd) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCompanionHttpd.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceCompanionUtil.md b/docs/dev/debt/libSceCompanionUtil.md new file mode 100644 index 0000000000..48d3c1cb47 --- /dev/null +++ b/docs/dev/debt/libSceCompanionUtil.md @@ -0,0 +1,5 @@ +# libSceCompanionUtil technical debt + +### Unknown function info + +- [sceCompanionUtilGetEvent, sceCompanionUtilGetRemoteOskEvent, sceCompanionUtilInitialize, sceCompanionUtilOptParamInitialize and sceCompanionUtilTerminate](../../../core/libs/prx/libSceCompanionUtil/Export.cpp) (libSceCompanionUtil) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCompanionUtil.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceComposite.md b/docs/dev/debt/libSceComposite.md new file mode 100644 index 0000000000..45ba88b721 --- /dev/null +++ b/docs/dev/debt/libSceComposite.md @@ -0,0 +1,5 @@ +# libSceComposite technical debt + +### Unknown function info + +- [sceCompositorAllocateIndex, sceCompositorCheckCrash, sceCompositorCommandGpuPerfBegin, sceCompositorCommandGpuPerfEnd, sceCompositorCreateIndirectRenderTarget, sceCompositorDeleteIndirectRenderTarget, sceCompositorFlush, sceCompositorFlushWithRepeat, sceCompositorGetAnotherProcessSystemAddress, sceCompositorGetAnotherProcessSystemSize, sceCompositorGetAnotherProcessVideoAddress, sceCompositorGetAnotherProcessVideoSize, sceCompositorGetCanvasHandle, sceCompositorGetClientList, sceCompositorGetDmemOffset, sceCompositorGetDummyAddress, sceCompositorGetDummySize, sceCompositorGetRemainedCommandNum, sceCompositorGetRenderTargetResolution, sceCompositorGetSystemAddress, sceCompositorGetSystemSize, sceCompositorGetVideoAddress, sceCompositorGetVideoSize, sceCompositorHandleProcessEvents, sceCompositorInit, sceCompositorInitWithProcessOrder, sceCompositorInsertThreadTraceMarker, sceCompositorIsDebugCaptureEnabled, sceCompositorIsReady4K, sceCompositorLockCommandBuffer, sceCompositorMakeCanvasHandle, sceCompositorMapAnotherProcess, sceCompositorMemoryPoolDecommit, sceCompositorPrivacyMaskPushArea, sceCompositorQuit, sceCompositorReleaseCommandBuffer, sceCompositorReleaseIndex, sceCompositorReleaseLocalStall, sceCompositorReserveCommandBuffer, sceCompositorResetZoomCommand, sceCompositorSetAgcCompositeCanvasCommand, sceCompositorSetAgcContextCommand, sceCompositorSetCompositeCanvasCommandInC, sceCompositorSetCursorImageAddress, sceCompositorSetDebugPositionCommand, sceCompositorSetEndOfFrameCommand, sceCompositorSetEventCommand, sceCompositorSetFilterType, sceCompositorSetFlipCommand2, sceCompositorSetGameSufaceControlCommand, sceCompositorSetGnmContextCommand, sceCompositorSetIndirectCanvasCommand, sceCompositorSetIndirectCompositionCommand, sceCompositorSetIndirectRenderTargetConfigCommand, sceCompositorSetInvisibleCanvasCommand, sceCompositorSetKeepDisplayBufferCommand, sceCompositorSetLocalStallCommand, sceCompositorSetMemoryCommand, sceCompositorSetPatchCommand, sceCompositorSetPostEventCommand, sceCompositorSetRepeatCommand, sceCompositorSetResolutionCommand, sceCompositorSetScalingLayoutInfo, sceCompositorSetSeparateContextCommand, sceCompositorSetSystemConfigWithMask, sceCompositorSetVideoOutMode, sceCompositorSetVirtualCanvasAgcContextCommand, sceCompositorSetVirtualCanvasCommandFooter, sceCompositorSetVirtualCanvasPatchCommand, sceCompositorSetVrMode, sceCompositorSetZoomCommand, sceCompositorSystemConfig, sceCompositorWaitEndOfRendering, sceCompositorWaitEvent, sceCompositorWaitPostEvent, sceCompsoitorGetGpuClock, sceCompsoitorGetProcessRenderingTime and sceCompsoitorGetRenderingTime](../../../core/libs/prx/libSceComposite/Export.cpp) (libSceComposite) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceComposite.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceCompositeExt.md b/docs/dev/debt/libSceCompositeExt.md new file mode 100644 index 0000000000..0f014d4acb --- /dev/null +++ b/docs/dev/debt/libSceCompositeExt.md @@ -0,0 +1,5 @@ +# libSceCompositeExt technical debt + +### Inventory exports declared elsewhere + +The complete PS5 3.20 export table of this module per the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCompositeExt.c) (78 names) is already declared under the sibling libSceComposite in this tree; the duplicate-export rule keeps each name in a single library, so this module keeps its `APS5_DUMMY_FUN` skeleton. Imports resolve through the sibling declarations. diff --git a/docs/dev/debt/libSceContentDelete.md b/docs/dev/debt/libSceContentDelete.md index e771fdeec7..f7365210d5 100644 --- a/docs/dev/debt/libSceContentDelete.md +++ b/docs/dev/debt/libSceContentDelete.md @@ -1,5 +1,10 @@ # libSceContentDelete technical debt +### Functional + +- [sceContentDeleteById](../../../core/libs/prx/libSceContentDelete/Export.cpp) and `sceContentDeleteByPath`: content database access and deletion are not implemented. Calls with valid arguments after initialization throw; the backend result cannot be inferred from an empty host library. Null paths after initialization throw instead of reproducing the original unchecked string access. +- [sceContentDeleteInitialize](../../../core/libs/prx/libSceContentDelete/Export.cpp): validates the confirmed 16 KiB heap requirement and lifecycle errors, but does not create the original allocator, mutex and database client. Initialization-parameter fields other than the 64-bit heap size at offset 8 remain unresolved. + ### Unknown function info -- [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 +- [sceContentDeleteGlobalMstat](../../../core/libs/prx/libSceContentDelete/Export.cpp), `sceContentDeleteMemoryInitialize` and `sceContentDeleteMemoryTerminate`: neither exports nor callers were found in the supplied decompiled library corpus; their signatures remain unresolved. diff --git a/docs/dev/debt/libSceContentExport.md b/docs/dev/debt/libSceContentExport.md index 1c8c0c1285..01f0d64324 100644 --- a/docs/dev/debt/libSceContentExport.md +++ b/docs/dev/debt/libSceContentExport.md @@ -2,4 +2,10 @@ ### Functional -- [Content export sessions](../../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport): `sceContentExportStart` hands out positive session ids, at most 10 live, which `sceContentExportFinish` closes. The signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) and the limit `MaxExecution` [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentExport.cs); the parameter layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/content_export/content_export.h). The host has no media library, so `sceContentExportFromDataWithThumbnail` throws once the session is checked, before calling either data callback, and no export ever runs. `sceContentExportGetProgress` and `sceContentExportCancel` throw for a live session, as their result with no running export is unknown. An eleventh `sceContentExportStart` and `sceContentExportTerm` with live sessions throw: whether the console answers `EXECUTION_MAX` or `BUSY` there, or ends the sessions, is unverified. Session id values and error precedence are unverified +- [Content export sessions](../../../core/libs/prx/libSceContentExport/Export.cpp): the host has no media library, so `sceContentExportFromDataWithThumbnail` throws for a valid session without invoking data callbacks. Session allocation does not reproduce guest allocator calls, worker creation or their failure paths. `sceContentExportGetProgress` throws because an idle session returns either zero or an invalid-state error according to the caller's SDK compatibility mode, which is not available here. Initialization and session lookup still throw for some invalid inputs instead of reproducing the console's error precedence and legacy SDK behavior. Session IDs now use the lowest free slot in 0–9; exhaustion, termination with live sessions and cancellation before an export return the confirmed errors. + +### Unknown function info + +- [sceContentExportDataProvideFunction, sceContentExportFree, sceContentExportFrom and sceContentExportMalloc](../../../core/libs/prx/libSceContentExport/Export.cpp): their signatures remain unresolved; these names have no matching exports in the supplied module's C or assembly listing. + +- [Content export arguments](../../../core/libs/prx/libSceContentExport/Export.cpp): file/data entry points and content-ID operations have restored argument lists. The Share Factory parameter contains a size, metadata string, content-ID list, count and option; metadata format and option values remain unresolved. Protected export bodies are unchanged. diff --git a/docs/dev/debt/libSceContentSearch.md b/docs/dev/debt/libSceContentSearch.md index 2222020f2e..6df6de62ff 100644 --- a/docs/dev/debt/libSceContentSearch.md +++ b/docs/dev/debt/libSceContentSearch.md @@ -1,5 +1,10 @@ # libSceContentSearch technical debt +### Unknown function info + +- [sceContentBinderEnd, sceContentBinderGetNumOfDifference, sceContentBinderInitialize, sceContentBinderIsUpdated, sceContentBinderReset, sceContentBinderSearchDifference, sceContentBinderStart, sceContentBinderTerminate, sceContentSearchGetApplicationLastUpdateId, sceContentSearchGetMyApplicationIndex, sceContentSearchGetNumOfApplication and sceContentSearchSearchApplication](../../../core/libs/prx/libSceContentSearch/Export.cpp) (libSceContentSearch) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceContentSearch.c), but no credible prototypes were found in the cited sources. They are declared without parameters solely to resolve known imports and make no ABI claim. + ### Functional -- [libSceContentSearch](../../../core/libs/prx/libSceContentSearch/Export.cpp) searches a content library that is empty on the host, as nothing is exported to it. The signatures follow [AlexAltea/orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L555-L569) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentSearch.cs#L255-L316), which agree; the filter and sort layouts and enums, the limit of 92 rows, `0x809D100A` for a larger limit and `0x809D1001` before `sceContentSearchInit` are SharpProspero's. `sceContentSearchGetNumOfContent`, `sceContentSearchGetTotalContentSize` and `sceContentSearchSearchContent` report 0 rows and 0 bytes without writing a content row, and the update id they and `sceContentSearchGetContentLastUpdateId` report is always 0; no reference gives the console's update id for a library that never changed. Filters and sort conditions are only checked against SharpProspero's enums, and a null output, a null array with a non-zero length, an empty column set, a null filter value or an unknown enum value throws, as the console's errors for them are unknown; a null column set with length 0 is taken as no filter. `sceContentSearchOpenMetadata` and `sceContentSearchOpenMetadataByContentId` throw, as no reference gives the error for a path or content id that is not in the library, so no metadata id is ever handed out and `sceContentSearchCloseMetadata`, `sceContentSearchGetMetadataFieldInfo` and `sceContentSearchGetMetadataValue` throw. Every title is taken to hold the content-search permission (SharpProspero's `0x809D1013` is never returned). The order of the initialization, argument and limit checks is unverified +- [Content queries](../../../core/libs/prx/libSceContentSearch/Export.cpp) use an empty host library and always report update id 0; the console's initial update id is unverified. No content rows are produced. Every title is assumed to have search permission. Nested filter/sort validation is incomplete: empty nested column sets, null filter values and unknown enum values throw. Guest heap allocation and the platform-dependent minimum heap size are unimplemented. Initialization and outer pointer/count/limit validation follow the supplied module. +- [Metadata access](../../../core/libs/prx/libSceContentSearch/Export.cpp) cannot open metadata by path or content id because missing-content errors remain unresolved. No metadata handles are issued. Queries reject invalid outer pointers but still throw for valid pointers because errors depend on the requested field; closing an unopened handle returns the confirmed invalid-argument error. diff --git a/docs/dev/debt/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md index 1f1b491f21..5c01a3cabc 100644 --- a/docs/dev/debt/libSceConvertKeycode.md +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -1,9 +1,6 @@ # libSceConvertKeycode technical debt -### Unknown function info - -- [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) - the signature `(uint16_t keycode, uint32_t, uint32_t keyboardType, char16_t* character)` is inferred from PPSA12361, which passes a 16-bit keycode, 0, the type from `sceConvertKeycodeGetImeKeyboardType` and a pointer it reads one UTF-16 unit from when the result is 0. The meaning of the second argument is unknown; a value other than 0 throws, and so does a keyboard type other than 0. - ### Functional -- [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) takes the keycode as a USB HID keyboard usage and returns the unshifted character of the US layout for letters, digits, punctuation, space and the keypad. A keycode without a character returns 0 with the character 0; whether the console returns an error instead is not measured. +- [sceConvertKeycodeGetVirtualKeycode](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) and `sceConvertKeycodeGetCharacter`: layouts 31–39 throw until the guest platform and compiled SDK version are exposed. `GetVirtualKeycode` also throws for version-dependent Y/Z mappings in layouts 2, 3 and 12. Mapping tables and modifier behavior are recovered; no SDK version is guessed. +- [sceConvertKeycodeGetImeKeyboardType](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): user keyboard language and Japanese/Chinese keyboard variants are not exposed by the current UserService implementation. Valid-user queries throw instead of returning a fabricated keyboard type. The confirmed language/variant mapping and SDK compatibility rules still need integration with those settings. diff --git a/docs/dev/debt/libSceCustomMusicAudioOut.md b/docs/dev/debt/libSceCustomMusicAudioOut.md new file mode 100644 index 0000000000..15ca7e3fa5 --- /dev/null +++ b/docs/dev/debt/libSceCustomMusicAudioOut.md @@ -0,0 +1,5 @@ +# libSceCustomMusicAudioOut technical debt + +### Unknown function info + +- [sceCustomMusicAudioOutFinalize, sceCustomMusicAudioOutGetAllMusicVolume, sceCustomMusicAudioOutGetMuted, sceCustomMusicAudioOutGetVolume and sceCustomMusicAudioOutSetAllMusicVolume](../../../core/libs/prx/libSceCustomMusicAudioOut/Export.cpp) (libSceCustomMusicAudioOut) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCustomMusicAudioOut.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceCustomMusicCore.md b/docs/dev/debt/libSceCustomMusicCore.md new file mode 100644 index 0000000000..9ffaf08475 --- /dev/null +++ b/docs/dev/debt/libSceCustomMusicCore.md @@ -0,0 +1,5 @@ +# libSceCustomMusicCore technical debt + +### Unknown function info + +- [sceCustomMusicCoreBgmClose, sceCustomMusicCoreBgmOpen, sceCustomMusicCoreBgmOpen2, sceCustomMusicCoreBgmOutput, sceCustomMusicCoreBgmSetAudioVolume, sceCustomMusicCoreBgmStart, sceCustomMusicCoreBgmStop, sceCustomMusicCoreGetBgmAuthorityStatus, sceCustomMusicCoreGetHdcpStatus, sceCustomMusicCoreGetSystemAudioVolume, sceCustomMusicCorePostEvent, sceCustomMusicCoreSendEvent, sceCustomMusicCoreSendMulticastEvent, sceCustomMusicCoreSendSpMessage, sceCustomMusicCoreSetSystemAudioVolume, sceCustomMusicCoreStartOperation, sceCustomMusicCoreStartOperation2 and sceCustomMusicCoreStopOperation](../../../core/libs/prx/libSceCustomMusicCore/Export.cpp) (libSceCustomMusicCore) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCustomMusicCore.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceCustomMusicSysCallWrapper.md b/docs/dev/debt/libSceCustomMusicSysCallWrapper.md new file mode 100644 index 0000000000..f7379ba3fc --- /dev/null +++ b/docs/dev/debt/libSceCustomMusicSysCallWrapper.md @@ -0,0 +1,5 @@ +# libSceCustomMusicSysCallWrapper technical debt + +### Unknown function info + +- [ and sceCustomMusicSelect](../../../core/libs/prx/libSceCustomMusicSysCallWrapper/Export.cpp) (libSceCustomMusicSysCallWrapper) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCustomMusicSysCallWrapper.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceDataTransfer.md b/docs/dev/debt/libSceDataTransfer.md new file mode 100644 index 0000000000..34655c5a6c --- /dev/null +++ b/docs/dev/debt/libSceDataTransfer.md @@ -0,0 +1,5 @@ +# libSceDataTransfer technical debt + +### Unknown function info + +- [sceDataTransferAbortFgTransfer, sceDataTransferAbortRequest, sceDataTransferAbortSearchPS4, sceDataTransferCheckBgTransferRunning, sceDataTransferGetFgTransferProgress, sceDataTransferGetPrepareFgTransferProgress, sceDataTransferInitialize, sceDataTransferRequestGetAppInfoPS4, sceDataTransferRequestGetSavedataInfoPS4, sceDataTransferRequestGetUsersPS4, sceDataTransferRequestLaunchPS4, sceDataTransferRequestPrepareFgTransfer, sceDataTransferRequestRebootAuthPS4, sceDataTransferRequestSearchPS4, sceDataTransferRequestStartFgTransfer, sceDataTransferRequestTransferTimePS4, sceDataTransferTargetAbortBindSavedata, sceDataTransferTargetAbortDeactivate, sceDataTransferTargetAbortEth0, sceDataTransferTargetAbortGetDeviceInfo, sceDataTransferTargetAbortGetDeviceInfoApplication, sceDataTransferTargetAbortGetTitles, sceDataTransferTargetAbortGetUsers, sceDataTransferTargetAbortLaunch, sceDataTransferTargetAbortPrepareTransfer, sceDataTransferTargetAbortPwrReq, sceDataTransferTargetAbortReboot, sceDataTransferTargetAbortSendSsoNew2Old, sceDataTransferTargetAbortSendSsoOld2New, sceDataTransferTargetAbortTransfer, sceDataTransferTargetAbortTransferSpeed, sceDataTransferTargetEventIsAuthReady, sceDataTransferTargetEventIsIPv6Ready, sceDataTransferTargetEventIsPwrReqReady, sceDataTransferTargetGetFailedUsers, sceDataTransferTargetGetRebootData, sceDataTransferTargetGetTransferProgress, sceDataTransferTargetRequestAbortSearch, sceDataTransferTargetRequestActivate, sceDataTransferTargetRequestAuth, sceDataTransferTargetRequestBindSavedata, sceDataTransferTargetRequestComplete, sceDataTransferTargetRequestCreateRebootData, sceDataTransferTargetRequestDeactivate, sceDataTransferTargetRequestEndTransfer, sceDataTransferTargetRequestGetDeviceInfo, sceDataTransferTargetRequestGetDeviceInfoApplication, sceDataTransferTargetRequestGetTitles, sceDataTransferTargetRequestGetUsers, sceDataTransferTargetRequestLaunch, sceDataTransferTargetRequestPrepareTransfer, sceDataTransferTargetRequestPrepareTransferProgress, sceDataTransferTargetRequestPwrReq, sceDataTransferTargetRequestReboot, sceDataTransferTargetRequestSearch, sceDataTransferTargetRequestSendSsoNew2Old, sceDataTransferTargetRequestSendSsoOld2New, sceDataTransferTargetRequestStartTransfer, sceDataTransferTargetRequestTransferSpeed and sceDataTransferTerminate](../../../core/libs/prx/libSceDataTransfer/Export.cpp) (libSceDataTransfer) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceDataTransfer.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceDepth.md b/docs/dev/debt/libSceDepth.md new file mode 100644 index 0000000000..9a67891d10 --- /dev/null +++ b/docs/dev/debt/libSceDepth.md @@ -0,0 +1,7 @@ +# libSceDepth technical debt + +### Unknown function info + +- [_sceDepthCalcHeteroAdjustParam, _sceDepthHeadCandidateTrackerGetInformation, _sceDepthHeadCandidateTrackerSetInformation, _sceDepthSetCameraFactoryCalibrationData, _sceDepthSetHeteroAdjustParam, _sceDepthSetHeteroMode](../../../core/libs/prx/libSceDepth/Export.cpp) (libSceDepth) - signatures and behavior are unverified; these parameterless declarations only resolve imports and throw through `NotImplemented_nid_no_patch`. + +- [libSceDepth pending exports](../../../core/libs/prx/libSceDepth/Export.cpp) - 32 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceDepth.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceDepth2.md b/docs/dev/debt/libSceDepth2.md new file mode 100644 index 0000000000..ee4e1f9b5c --- /dev/null +++ b/docs/dev/debt/libSceDepth2.md @@ -0,0 +1,5 @@ +# libSceDepth2 technical debt + +### Unknown function info + +- [libSceDepth2 pending exports](../../../core/libs/prx/libSceDepth2/Export.cpp) - 10 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceDepth2.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceDipsw.md b/docs/dev/debt/libSceDipsw.md new file mode 100644 index 0000000000..cebac2c8b7 --- /dev/null +++ b/docs/dev/debt/libSceDipsw.md @@ -0,0 +1,5 @@ +# libSceDipsw technical debt + +### Unknown function info + +- [sceKernelCheckDipsw, sceKernelInitializeDipsw, sceKernelIsAssistMode, sceKernelIsDevelopmentMode, sceKernelIsDisableRazor, sceKernelReadDipswData, sceKernelSetDipsw, sceKernelUnsetDipsw and sceKernelWriteDipswData](../../../core/libs/prx/libSceDipsw/Export.cpp) (libSceDipsw) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceDipsw.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceDiscId.md b/docs/dev/debt/libSceDiscId.md new file mode 100644 index 0000000000..17eb02c445 --- /dev/null +++ b/docs/dev/debt/libSceDiscId.md @@ -0,0 +1,5 @@ +# libSceDiscId technical debt + +### Unknown function info + +- [sceDiscIdGet](../../../core/libs/prx/libSceDiscId/Export.cpp) takes one output pointer; `sceDiscIdGet2` takes an output pointer and a 32-bit argument that must be zero. Both pass the pointer to a device ioctl without accessing its contents. Output structure sizes and fields require the device-side implementation or a caller and remain unresolved. diff --git a/docs/dev/debt/libSceDiscMap.md b/docs/dev/debt/libSceDiscMap.md new file mode 100644 index 0000000000..b058be4747 --- /dev/null +++ b/docs/dev/debt/libSceDiscMap.md @@ -0,0 +1,9 @@ +# libSceDiscMap technical debt + +### Functional + +- [libSceDiscMap](../../../core/libs/prx/libSceDiscMap/Export.cpp): loading disc bitmap metadata and resolving file byte ranges to bitmap blocks are not implemented. Both exports implement the confirmed no-bitmap path: return `SCE_DISC_MAP_ERROR_NO_BITMAP_INFO` before validating any argument and leave every output unchanged. The supplied module confirms signed 64-bit byte offset/size, a 32-bit `IsRequestOnHDD` result and three 64-bit outputs for `ioKMruft1ek`. The latter outputs describe the bitmap selector, first block and block count. Its exported source name remains unavailable. + +### Unknown function info + +- [sceDiscMapGetPackageSize](../../../core/libs/prx/libSceDiscMap/Export.cpp) (libSceDiscMap) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceDiscMap.c); the `(int64_t, int*, int*)` prototype follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/disc_map/disc_map.cpp#L12), is not measured on a console, and every call throws diff --git a/docs/dev/debt/libSceDseehx.md b/docs/dev/debt/libSceDseehx.md new file mode 100644 index 0000000000..9bde1c867e --- /dev/null +++ b/docs/dev/debt/libSceDseehx.md @@ -0,0 +1,5 @@ +# libSceDseehx technical debt + +### Unknown function info + +- [sceDseehxConvert, sceDseehxExtInit, sceDseehxFlush and sceDseehxInit](../../../core/libs/prx/libSceDseehx/Export.cpp) (libSceDseehx) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceDseehx.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceDtsEnc.md b/docs/dev/debt/libSceDtsEnc.md new file mode 100644 index 0000000000..c5656e8a64 --- /dev/null +++ b/docs/dev/debt/libSceDtsEnc.md @@ -0,0 +1,5 @@ +# libSceDtsEnc technical debt + +### Unknown function info + +- [sceDtsEncCreateEncoder, sceDtsEncDeleteEncoder, sceDtsEncEncode, sceDtsEncEncodeAsync, sceDtsEncFlush, sceDtsEncIsEncoding and sceDtsEncWaitEncode](../../../core/libs/prx/libSceDtsEnc/Export.cpp) (libSceDtsEnc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceDtsEnc.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceEditMp4.md b/docs/dev/debt/libSceEditMp4.md new file mode 100644 index 0000000000..9619922e39 --- /dev/null +++ b/docs/dev/debt/libSceEditMp4.md @@ -0,0 +1,5 @@ +# libSceEditMp4 technical debt + +### Unknown function info + +- [sceEditMp4Abort, sceEditMp4Edit, sceEditMp4GetStatus, sceEditMp4Initialize, sceEditMp4InitializeParam, sceEditMp4QueryMemSize, sceEditMp4SetFinishCallback, sceEditMp4Terminate and sceEditMp4Unite](../../../core/libs/prx/libSceEditMp4/Export.cpp) (libSceEditMp4) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceEditMp4.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceErrorDialog.md b/docs/dev/debt/libSceErrorDialog.md index 3c596b30b0..1166c74424 100644 --- a/docs/dev/debt/libSceErrorDialog.md +++ b/docs/dev/debt/libSceErrorDialog.md @@ -3,3 +3,7 @@ ### Silent stubs - [libSceErrorDialog](../../../core/libs/prx/libSceErrorDialog/Export.cpp) - the error dialog runs the state machine but shows nothing; the error code passed to `sceErrorDialogOpen` is only logged + +### Unknown function info + +- [sceErrorDialogOpenDetail, sceErrorDialogOpenWithReport](../../../core/libs/prx/libSceErrorDialog/Export.cpp) (libSceErrorDialog) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceErrorDialog.c); the no-parameter prototypes follow [shadPS4 `error_dialog.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/error_dialog.cpp#L167-L172) and are not measured on a console. They are declared without parameters solely to resolve known imports; this is not an ABI claim, and every call throws. diff --git a/docs/dev/debt/libSceEsvm.md b/docs/dev/debt/libSceEsvm.md new file mode 100644 index 0000000000..099c999a95 --- /dev/null +++ b/docs/dev/debt/libSceEsvm.md @@ -0,0 +1,5 @@ +# libSceEsvm technical debt + +### Unknown function info + +- [sceEsvmAppAudioSyncCreateContext, sceEsvmAppAudioSyncFlush and sceEsvmSyncVideo](../../../core/libs/prx/libSceEsvm/Export.cpp) (libSceEsvm) - signatures and behavior are unverified; these parameterless declarations only resolve imports and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceEsvmEngine.md b/docs/dev/debt/libSceEsvmEngine.md new file mode 100644 index 0000000000..c7fd387983 --- /dev/null +++ b/docs/dev/debt/libSceEsvmEngine.md @@ -0,0 +1,5 @@ +# libSceEsvmEngine technical debt + +### Unknown function info + +- [sceEsvmEngineDestroyMediaKeySystemAccess, sceEsvmEngineMediaKeySessionClose, sceEsvmEngineMediaKeySessionGenerateRequest, sceEsvmEngineMediaKeySessionGetExpiration, sceEsvmEngineMediaKeySessionGetKeyStatuses, sceEsvmEngineMediaKeySessionGetSessionId, sceEsvmEngineMediaKeySessionLoad, sceEsvmEngineMediaKeySessionRemove, sceEsvmEngineMediaKeySessionSetEventHandler, sceEsvmEngineMediaKeySessionUpdate, sceEsvmEngineMediaKeySystemAccessCreateMediaKeys, sceEsvmEngineMediaKeySystemAccessDestroyMediaKeys, sceEsvmEngineMediaKeySystemAccessGetConfiguration, sceEsvmEngineMediaKeySystemAccessSetCdmStorageConfig, sceEsvmEngineMediaKeysCreateSession, sceEsvmEngineMediaKeysDestroySession, sceEsvmEngineMediaKeysSetEventHandler, sceEsvmEngineMediaKeysSetServerCertificate and sceEsvmEngineRequestMediaKeySystemAccess](../../../core/libs/prx/libSceEsvmEngine/Export.cpp) (libSceEsvmEngine) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceEsvmEngine.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceFiber.md b/docs/dev/debt/libSceFiber.md index 0a64c52ca5..9ad010851f 100644 --- a/docs/dev/debt/libSceFiber.md +++ b/docs/dev/debt/libSceFiber.md @@ -6,4 +6,7 @@ ### Unknown function info +- [_sceFiberInitializeWithInternalOptionImpl](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - its signature follows shadPS4's [initialize implementations](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L150-L153); its export alias is mapped at [RegisterLib](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L548-L551). Not measured on a console; the throwing declaration only resolves the import. +- [_sceFiberAttachContextAndRun, _sceFiberAttachContextAndSwitch](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - signatures follow shadPS4's [run implementation](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L260-L261) and [switch implementation](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L336-L337); their export aliases are mapped at [RegisterLib](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L555-L563). Not measured on a console; the throwing declarations only resolve imports. - [_sceFiberGetThreadFramePointerAddress](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber, NID `0dy4JtMUcMQ`) forwards to `sceFiberGetThreadFramePointerAddress`; that the argument list matches is inferred from the export without the underscore, as for the underscore Ult exports, not measured +- [Fiber stacks](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - a fiber whose context is smaller than 64 KiB runs on an mmap'd host stack of its context size plus 1 MiB, so host calls made from it (21 KiB deep when PPSA01341 reaches SPIRV-Tools from a 16 KiB context) do not overwrite the contexts packed next to it; a larger context is the stack, as on the console. PPSA05684 needs the second: its job fibers have 64 KiB and 512 KiB contexts, the 512 KiB ones at 1 MiB-aligned addresses, and find their job record by masking the stack pointer (`~0xfffff` for the 512 KiB ones), which a host stack breaks (the game waits forever in GameInit, or faults later in a 64 KiB fiber). The 64 KiB limit is chosen between these two titles, not measured: a title with small contexts that also derives state from the stack pointer, or one whose host calls exceed what is left of a large context, is not covered diff --git a/docs/dev/debt/libSceFios2.md b/docs/dev/debt/libSceFios2.md new file mode 100644 index 0000000000..ac6b0499b6 --- /dev/null +++ b/docs/dev/debt/libSceFios2.md @@ -0,0 +1,5 @@ +# libSceFios2 technical debt + +### Unknown function info + +- [sceFiosArchiveGetDecompressorThreadCount, sceFiosArchiveGetMountBufferSize, sceFiosArchiveGetMountBufferSizeSync, sceFiosArchiveMount, sceFiosArchiveMountSync, sceFiosArchiveMountWithOrder, sceFiosArchiveMountWithOrderSync, sceFiosArchiveSetDecompressorThreadCount, sceFiosArchiveUnmount, sceFiosArchiveUnmountSync, sceFiosCacheContainsFileRangeSync, sceFiosCacheContainsFileSync, sceFiosCacheFlushFileRangeSync, sceFiosCacheFlushFileSync, sceFiosCacheFlushSync, sceFiosCachePrefetchFH, sceFiosCachePrefetchFHRange, sceFiosCachePrefetchFHRangeSync, sceFiosCachePrefetchFHSync, sceFiosCachePrefetchFile, sceFiosCachePrefetchFileRange, sceFiosCachePrefetchFileRangeSync, sceFiosCachePrefetchFileSync, sceFiosCancelAllOps, sceFiosClearTimeStamps, sceFiosCloseAllFiles, sceFiosDHClose, sceFiosDHCloseSync, sceFiosDHGetPath, sceFiosDHOpen, sceFiosDHOpenSync, sceFiosDHRead, sceFiosDHReadSync, sceFiosDLLInitialize, sceFiosDLLTerminate, sceFiosDateFromComponents, sceFiosDateGetCurrent, sceFiosDateToComponents, sceFiosDeallocatePassthruFH, sceFiosDebugDumpDH, sceFiosDebugDumpDate, sceFiosDebugDumpError, sceFiosDebugDumpFH, sceFiosDebugDumpOp, sceFiosDelete, sceFiosDeleteSync, sceFiosDirectoryCreate, sceFiosDirectoryCreateSync, sceFiosDirectoryCreateWithMode, sceFiosDirectoryCreateWithModeSync, sceFiosDirectoryDelete, sceFiosDirectoryDeleteSync, sceFiosDirectoryExists, sceFiosDirectoryExistsSync, sceFiosExists, sceFiosExistsSync, sceFiosFHClose, sceFiosFHCloseSync, sceFiosFHGetOpenParams, sceFiosFHGetPath, sceFiosFHGetSize, sceFiosFHOpen, sceFiosFHOpenSync, sceFiosFHOpenWithMode, sceFiosFHOpenWithModeSync, sceFiosFHPread, sceFiosFHPreadSync, sceFiosFHPreadv, sceFiosFHPreadvSync, sceFiosFHPwrite, sceFiosFHPwriteSync, sceFiosFHPwritev, sceFiosFHPwritevSync, sceFiosFHRead, sceFiosFHReadSync, sceFiosFHReadv, sceFiosFHReadvSync, sceFiosFHSeek, sceFiosFHStat, sceFiosFHStatSync, sceFiosFHSync, sceFiosFHSyncSync, sceFiosFHTell, sceFiosFHToFileno, sceFiosFHTruncate, sceFiosFHTruncateSync, sceFiosFHWrite, sceFiosFHWriteSync, sceFiosFHWritev, sceFiosFHWritevSync, sceFiosFileDelete, sceFiosFileDeleteSync, sceFiosFileExists, sceFiosFileExistsSync, sceFiosFileGetSize, sceFiosFileGetSizeSync, sceFiosFileRead, sceFiosFileReadSync, sceFiosFileTruncate, sceFiosFileTruncateSync, sceFiosFileWrite, sceFiosFileWriteSync, sceFiosFilenoToFH, sceFiosGetAllDHs, sceFiosGetAllFHs, sceFiosGetAllOps, sceFiosGetDefaultOpAttr, sceFiosGetGlobalDefaultOpAttr, sceFiosGetSuspendCount, sceFiosGetThreadDefaultOpAttr, sceFiosIOFilterAdd, sceFiosIOFilterCache, sceFiosIOFilterGetInfo, sceFiosIOFilterPsarcDearchiver, sceFiosIOFilterRemove, sceFiosInitialize, sceFiosIsIdle, sceFiosIsInitialized, sceFiosIsSuspended, sceFiosIsValidHandle, sceFiosOpCancel, sceFiosOpDelete, sceFiosOpGetActualCount, sceFiosOpGetAttr, sceFiosOpGetBuffer, sceFiosOpGetError, sceFiosOpGetOffset, sceFiosOpGetPath, sceFiosOpGetRequestCount, sceFiosOpIsCancelled, sceFiosOpIsDone, sceFiosOpReschedule, sceFiosOpRescheduleWithPriority, sceFiosOpSetBuffer, sceFiosOpSyncWait, sceFiosOpSyncWaitForIO, sceFiosOpWait, sceFiosOpWaitUntil, sceFiosOverlayAdd, sceFiosOverlayGetInfo, sceFiosOverlayGetList, sceFiosOverlayModify, sceFiosOverlayRemove, sceFiosOverlayResolveSync, sceFiosPathNormalize, sceFiosPathcmp, sceFiosPathncmp, sceFiosPrintTimeStamps, sceFiosPrintf, sceFiosRename, sceFiosRenameSync, sceFiosResolve, sceFiosResolveSync, sceFiosResume, sceFiosSaveTimeStamp, sceFiosSetGlobalDefaultOpAttr, sceFiosSetThreadDefaultOpAttr, sceFiosShutdownAndCancelOps, sceFiosStat, sceFiosStatSync, sceFiosSuspend, sceFiosTerminate, sceFiosTimeGetCurrent, sceFiosTimeIntervalFromNanoseconds, sceFiosTimeIntervalToNanoseconds, sceFiosTraceTimestamp, sceFiosUpdateParameters and sceFiosVprintf](../../../core/libs/prx/libSceFios2/Export.cpp) (libSceFios2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFios2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index c2b60afaf4..b1b5e56398 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -1,24 +1,39 @@ # libSceFont technical debt -### Silent stubs - -- [sceFontGlyphDefineAttribute](../../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) checks the glyph and ignores the attribute and its value, whose meaning is unknown. PPSA01325 sets attribute 0x11 to 0 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused, and draws the character with `sceFontRenderCharGlyphImageHorizontal` - ### Unknown function info -- [sceFontSetScriptLanguage, sceFontGetScriptLanguage, sceFontSetTypographicDesign, sceFontGetTypographicDesign](../../../core/libs/prx/libSceFont/src/Style.cpp) (libSceFont) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.cs), not confirmed for the PS5. The handle keeps the language set for each script and the feature set for each typographic design and returns them, with 0 for one never set; the console default and its valid ranges are unknown, so any value is accepted. Neither setting changes shaping or rendering yet +- [Typographic design](../../../core/libs/prx/libSceFont/src/Style.cpp): native selector, feature, font-set eligibility and output-clearing rules are recovered. Driver typography selection and queries, feature-cache updates and glyph substitution remain unimplemented and throw instead of storing arbitrary selector/value pairs. +- [sceFontSetScriptLanguage and sceFontGetScriptLanguage](../../../core/libs/prx/libSceFont/src/Style.cpp): native script/language ranges, handle-error precedence and clearing of failed getter outputs are implemented. Valid driver queries and updates throw; the previous arbitrary-value map and fabricated zero default are no longer used. Driver selection and actual language-dependent shaping remain unimplemented. - [sceFontGetRenderScaledKerning](../../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - arguments, the cleared output and the bound renderer check taken from `cellFontGetRenderScaledKerning` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellFont.cpp) (PS3 predecessor of the library), not confirmed on a title. The kerning is the `sceFontGetKerning` one at the render scale -- [sceFontGetPixelResolution](../../../core/libs/prx/libSceFont/src/Library.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title. The value is the one the library's driver reports (64 for FreeType); the argument checks and the cleared output follow `sceFontGetLibrary` -- [sceFontDettachDeviceCacheBuffer](../../../core/libs/prx/libSceFont/src/Library.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title. It detaches the cache buffer attached by `sceFontAttachDeviceCacheBuffer`, returns the buffer and its size, and reports `SCE_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER` when no cache buffer is attached -- [sceFontGetFontGlyphsCount, sceFontGetCharGlyphCode](../../../core/libs/prx/libSceFont/src/Open.cpp) (libSceFont) - signatures taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and matching the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings, not confirmed on a title. The count is the face's glyph count and the glyph code its glyph index, as the library's driver reports them; a code with no glyph returns `NO_SUPPORT_GLYPH` and code 0 `NO_SUPPORT_CODE`, as `sceFontRenderCharGlyphImage` does. The argument checks and the cleared output follow `sceFontGetPixelResolution` -- [sceFontGetFontResolution](../../../core/libs/prx/libSceFont/src/Open.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title; the outputs are inferred from the names. The resolution is the face's units per EM and the scale pixel the driver's face scale (units per EM / 64 for FreeType), the pixel scale a font has when opened. Either output may be null, as in `sceFontGetResolutionDpi` -- [sceFontCreateWritingLine, sceFontWritingLineWritesOrder](../../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - signatures and the `SceFontWritingLineStep`/`SceFontCreateWritingLineDetail` layouts (detail id `0x0FD5`) taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFontStructs.cs), not confirmed on a title. The 64-bit writing attribute is read as the PS4 11.00 `libSceFont.sprx` reads it: low byte `0xFA`, embedding level in bits 8-15 (0 to 126), bit 30 and bit 31 marking space before and after the run; the PS5 library was not measured, and the names of the two bits are ours. PPSA01325 builds its attribute the same way (`0xFA | level << 8`, bits 30 and 31) and passes a null metrics pointer when its run has no metrics object. Attribute 0 is accepted and treated as `0xFA` with level 0 and no spacing bits, as before this reading; the PS4 library rejects it and what the PS5 library does with it is unknown. Any other attribute, a level above 126 and a null orderer throw: the PS4 library returns `0x80460002` for them, not confirmed for the PS5 +- [sceFontGetFontGlyphsCount, sceFontGetCharGlyphCode](../../../core/libs/prx/libSceFont/src/Open.cpp): signatures, error precedence, failed-output clearing and external-font glyph-code encoding are confirmed by the PS5 library. Multi-face system-font glyph counts, face-selection bits and typographic glyph substitutions are not modeled. +- [sceFontGetFontResolution](../../../core/libs/prx/libSceFont/src/Open.cpp): the 32-bit resolution output, float scale and nullable outputs are confirmed by the PS5 library and FontFt driver. Native system font sets return resolution 1000; the host still uses its selected face resolution because multi-face system font sets are not modeled. +- [Writing-line ABI and allocation](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): PS5 layouts, creation forms, memory validation, allocator callbacks and error handling are recovered. The 2208-byte line and 64-byte orders use caller memory. Host registry bookkeeping still allocates separately; memory lifetime and concurrent destruction require caller synchronization. - [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 +- [sceFontWritingLineGetOrderingSpace](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): PS5 confirms output order, calculations and zeroed outputs on invalid handles. A non-null head output with a null tail output causes an unchecked native write; the host throws instead. With no head output, the tail output remains untouched. - [sceFontCharacterGetSyllableStringState](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signature `(textCharacter, int* syllableStringState)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs). Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): the state is 0 for Latin, CJK, Hangul, Arabic, Hebrew, Devanagari and the other scripts tried, and the library sets 1, 3 or 4 (never 2 on real text) for Thai while it creates the string; a null character or a null result returns `SCE_FONT_ERROR_INVALID_PARAMETER`, and a null character writes 0 to a non-null result. This returns 0 for every character outside the Thai block (U+0E00 to U+0E7F), as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1805-L1815) does, and throws for Thai characters, whose cluster states are not modelled. SharpProspero's `SceFontSyllableString` names 0 NonDetection, 1 Starting, 2 Following, 3 Depending and 4 Connecting. PPSA06699 (Hot Shots Golf 2) calls it. +- [sceFontCharactersRefersTextCodes, sceFontTextCodesStepNext and sceFontTextCodesStepBack](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signatures follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L679) IDA type database and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFont.Engine.cs#L395-L406), and the 64-byte `FontTextCodes` with the order pointer at 0 and the code at 8 follows SharpProspero's [SceFontTextCodes](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFontStructs.cs#L216-L228). The traversal follows the PS4 11.00 `libSceFont.sprx` disassembly posted in [#4050](https://github.com/boykopovar/AnyPS5/pull/4050#issuecomment-6114504202), not a PS5 measurement: the cursor keeps the current, start and term characters at +0x28, +0x30 and +0x38; `Refers` never compares the character with the term and returns null for a clustered or synthetic character; `StepNext` returns null at the term after visiting it; `StepBack` returns null when the current character is the start; both steps skip synthetic characters. Assumed and not covered by the report, which says `StepNext` returns null only at the term: `StepNext` also returns null at the end of the chain (for example with a null term), and `StepBack` at the head of the chain. A null cursor or output returns null as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1696-L1740); a null text character throws, as its console result is unknown + +- [sceFontDefineAttribute, sceFontGetAttribute](../../../core/libs/prx/libSceFont/src/Style.cpp), [sceFontGlyphDefineAttribute, sceFontGlyphGetAttribute](../../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - the signatures `(handle, int attribute, int* oldAttribute)` and `(handle, int attribute, int* nowAttribute)`, and the same shape for a glyph, follow the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings, matching the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1249-L1264), not confirmed on a title. Every handle and every glyph keeps one attribute, an opaque int whose meaning and valid values are unknown: define stores the new value and returns the previous one, and get returns the stored value with its `attribute` argument unused, as in KytyPS5. An attribute never set is 0. The null checks and the invalid-object errors follow the other libSceFont functions and diverge from [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1251-L1265), which returns `OK` for a null handle or glyph after writing 0 to the old-attribute pointer: every other handle and glyph entry point in this library reports the invalid object instead, and a title that passes one is already failing. shadPS4 leaves both exports unimplemented, so there is no second implementation to compare. PPSA01325 calls the glyph define with 0x11 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused and draws with `sceFontRenderCharGlyphImageHorizontal`; the three bindings all read the third argument as an out pointer, not a value, so this call sets the glyph's attribute to 0x11. The attribute changes neither shaping nor rendering. ### Functional -- [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) opens only the primary file of a system font set. Characters the console takes from the set's other files (Hangul, Thai and Arabic in Japanese and Chinese sets, Arabic in European sets) are reported as unsupported glyphs. The console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans CJK) is loaded instead. It is not the system font: its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. +- [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the other script files of a set are chosen from its type, not measured on a console: Vietnamese for low-byte bit 0x10, Thai for script bit 0x10, Japanese for 0x04, simplified Chinese for 0x80 (before Japanese with 0x08), Hangul for 0x20 and Arabic for low-byte bit 0x80, as the SDK set names (`..._JP_AR`, `..._GBCJK_TH`) listed by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/font/font_internal.cpp) suggest; the file order and the Hangul file name (`SCEPS4Yoongd-Medium.otf`, `SCEPS4Yoongd-Bold.otf`) also come from shadPS4. A character the primary file lacks is taken from the first of them that has it; kerning between two files is 0, and `sceFontGetCharGlyphCode` and `sceFontGetFontGlyphsCount` answer for the primary file only +- [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans Arabic, Noto Sans CJK with its Korean face for Hangul) is loaded instead; its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. - [sceFontGetCharGlyphMetrics, sceFontGetRenderCharGlyphMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) report the vertical metrics the way the [FreeType driver](../../../core/libs/prx/libSceFontFt/src/Driver.cpp) reports them for a rendered glyph, not confirmed on a title: FreeType's vertical advance, and for a face without vertical metrics a bearing of half the horizontal advance left of the horizontal bearing and of half the advance left over by the glyph height. -- [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - `sceFontWritingLineWritesOrder`, `sceFontWritingLineRefersRenderStep`, `sceFontWritingLineGetRenderMetrics`, `sceFontWritingLineGetOrderingSpace` and `sceFontWritingLineClear` follow the PS4 11.00 `libSceFont.sprx` (firmware module, read from its disassembly and compared call for call on random sequences): runs are linked in visual order by embedding level, the space at each spaced join is summed for the line, steps walk the visual order from the left (form `0x12`) or from the right (form `0x11`) and the render metrics grow as steps are consumed. The PS5 library was not measured. Not from that module: form `0x10` is accepted and treated as left to right and a detail with id `0x0FD5` is accepted (the PS4 library rejects both); the memory argument of `sceFontCreateWritingLine` and the rest of the detail are not read (the PS4 library validates its library object there); an unknown writing form, another detail id, a null line pointer on creation, unknown and null handles (except in `sceFontWritingLineRefersRenderStep`, which returns null for a null line) and a null output of `sceFontWritingLineGetRenderMetrics` throw instead of returning the PS4 error codes, which are not confirmed for the PS5; `sceFontDestroyWritingLine` was not compared. Lines are allocated on the host, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. +- [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp): visual ordering and spacing originated from PS4 measurements and match the supplied PS5 structure and control flow. Exhaustive floating-point equivalence, including NaN payloads and rounding, has not been verified against a running PS5 module. + +- [Font graphics and metric signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): graphics-device/service creation takes font memory, parameters and a handle output; destruction takes a handle pointer. Attribute selectors and outputs are 32-bit values. Metric, outline-profile, style-information and expansion-buffer outputs remain opaque pending field recovery. Protected implementations are unchanged. + +- [Font graphics fill and transform signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp): region layout and scalar float arguments are confirmed from assembly, including the extra 32-bit corner flags omitted by the roundish-region decompilation. Fill-method, plot and rates layouts, layout selector meanings and mapping parameter meanings remain unresolved. Protected bodies are unchanged. + +Frame lifecycle, drawing completion, device usage, frame policy, clipping and fill setup/update argument signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered. Frame and resource layouts remain opaque; their runtime implementation is still unavailable. + +Glyph image rendering uses six pointer arguments and two scalar coordinates, with existing style, surface, metrics and output layouts. Positioning takes a mask and two floats. Control, word-character search and resource release/exchange/render signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered; control payload types depend on the command and remain unmodeled. + +Canvas, design, surface texture and fill-pattern construction signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered. Pattern mode/count semantics and the graphics surface descriptor fields still require reconstruction; distinct opaque types preserve the confirmed object categories. + +[Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) now defines the 32-byte fill method, 64-byte fill plot/rates and 96-byte device usage layouts, with size and field-offset assertions. Uninterpreted descriptor tails remain reserved; pool statistics expose capacity, current usage, available bytes, peak and signed change. + +The [glyph support registration and texture-image fill signatures](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered from native argument use. The image constructor takes a byte span and four scalar coordinates; the decompiler omits its successful path. `CreateWords` and `CreateGraphicsServiceWithEdition` consume the output pointer in the fourth general-purpose argument slot but do not read the third slot, whose source type is not established. + +- [sceFontCreateGraphicsServiceWithEdition and sceFontCreateWords](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) take their handle output in the fourth integer argument slot. The third slot is not read by the supplied module and is represented as an opaque 64-bit value; its declared source type remains unknown. diff --git a/docs/dev/debt/libSceFontFt.md b/docs/dev/debt/libSceFontFt.md new file mode 100644 index 0000000000..c5c609e43f --- /dev/null +++ b/docs/dev/debt/libSceFontFt.md @@ -0,0 +1,10 @@ +# libSceFontFt technical debt + +### Functional + +- [Font format support](../../../core/libs/prx/libSceFontFt/Export.cpp): format queries now take the font library, validate its magic and lock it while checking its actual FreeType modules. The host registers its default modules at creation rather than enabling formats individually, so format-selection state and guest allocator failures during format registration are not reproduced. Unsupported foreign drivers throw. +- [TrueType GX support](../../../core/libs/prx/libSceFontFt/Export.cpp): the GX query checks the bundled FreeType TrueType module, whose configuration enables GX variation support. The console's response for a FreeType build without GX variation support has not been measured. + +### Unknown function info + +- [Alias and glyph selection exports](../../../core/libs/prx/libSceFontFt/Export.cpp): alias initialization/termination take a handle pointer; alias registration takes a string and font ID or path. Glyph selection takes a 32-bit selector and returns a table pointer whose layout remains unresolved. Their protected implementations are unchanged. diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md new file mode 100644 index 0000000000..b2a706294a --- /dev/null +++ b/docs/dev/debt/libSceFreeType.md @@ -0,0 +1,23 @@ +# libSceFreeType technical debt + +### Unknown function info + +- [FT_Get_Sfnt_Name_Count, FT_Get_Sfnt_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, record layout and errors are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftsnames.c) and assumes the public LP64 `FT_SfntName` layout (24 bytes, string pointer at 8, byte length at 16). Strings retain their original encoding, borrow native face storage and remain valid only while the face is alive + +- [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char, FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp), [FT_Get_Glyph_Name, FT_Get_Name_Index, FT_Get_Postscript_Name](../../../core/libs/prx/libSceFreeType/src/Names.cpp) (libSceFreeType) - console signatures, naming rules and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Iteration uses the low 32 input bits and updates only the selected guest charmap; charmap fields are read-only copies. Naming preserves LP64 invalid-index errors on Windows. PostScript names borrow native face storage and remain valid only while the face is alive + +- [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions + +- [FT_Request_Size](../../../core/libs/prx/libSceFreeType/src/SizeRequest.cpp) (libSceFreeType) - console signature, request layout and errors are unverified; the guest request follows the LP64 public headers of the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/include/freetype/freetype.h). Host overflow behavior remains that of the bundled build. On Windows, native `FT_Long` is 32 bits; valid requests whose width or height exceeds that range throw before changing the size. + +- [FT_New_Memory_Face, FT_Done_Face and FT_Done_FreeType](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures, errors and generic-finalizer ordering are unverified; they follow the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/include/freetype/freetype.h) with LP64 guest arguments. Caller-owned font bytes must remain valid until the face is freed. + +### Functional + +- [FT_Set_Transform](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, transform arithmetic and FreeType-version equivalence are unverified. The name and NID `bcM5xfX2SeA` come from the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the 32-byte LP64 matrix, 16-byte vector, null defaults and glyph behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Input components outside the native `FT_Long` range throw without changing the transform; on Windows this rejects values outside signed 32 bits. Native arithmetic limits remain those of the host library. + +- [libSceFreeType](../../../core/libs/prx/libSceFreeType/src/GuestObjects.cpp) (libSceFreeType) - private fields (`driver`, `memory`, `stream`, `sizes_list`, `extensions`) and `subglyphs` stay null. Face, size and glyph-slot records are copies. Only edits to outline points and flags are written back before `FT_Render_Glyph`; replacing arrays or exceeding the host `FT_Pos` range throws. `FT_New_Face` reads the whole file into memory and throws for files of 2 GiB or more on Windows. On macOS, native FreeType symbols remain exported and could resolve guest imports without adaptation; Windows and Linux hide them with `--exclude-libs`. + +- [FT_New_Memory_Face](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - on Windows, the host FT_Long is 32 bits, so buffer sizes of 2 GiB or more or below -2 GiB throw before the font buffer is read + +- [FT_Set_Char_Size](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, FreeType version and error compatibility are unverified. The name and NID `xuY99Qcb108` are in the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the LP64 signature and behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c), not a console measurement. After applying its defaults and minimum-size clamp, requests whose `dimension * dpi + 36` exceeds the host's signed `FT_Long` range throw before the native call. This is a 32-bit limit on Windows; larger console requests remain unsupported. diff --git a/docs/dev/debt/libSceFsInternalForVsh.md b/docs/dev/debt/libSceFsInternalForVsh.md new file mode 100644 index 0000000000..92591afa7b --- /dev/null +++ b/docs/dev/debt/libSceFsInternalForVsh.md @@ -0,0 +1,5 @@ +# libSceFsInternalForVsh technical debt + +### Unknown function info + +- [sceBdSchedCancelBackgroundCopyRequest, sceBdSchedCancelPrioritizedBackgroundCopyRequest, sceBdSchedConfigure, sceBdSchedConfigure2, sceBdSchedDeconfigure, sceBdSchedGetBackgroundCopyRequest, sceBdSchedGetExtentMap, sceBdSchedGetPrioritizedBackgroundCopyRequest, sceBdSchedGetState, sceBdSchedSetBackgroundCopyRequest, sceBdSchedSetDiscIdentity, sceBdSchedSetPrioritizedBackgroundCopyRequest, sceBdSchedStartGame, sceBdSchedStartRipping, sceBdSchedStopScheduling, sceFsBfsCheckCleanFlag, sceFsBindApplication, sceFsBindPackages, sceFsCheckFormat, sceFsCheckSingleUser, sceFsCreateExfatPartition, sceFsCreatePfsSaveDataImage, sceFsCreatePfsTrophyDataImage, sceFsCreatePprPfsTrophyDataImage, sceFsDeviceAlignedPread, sceFsDeviceAlignedPwrite, sceFsDevpfsA53IocGetBuildMetadataStatus, sceFsDevpfsCancelWaitEvent, sceFsDevpfsCreateForVSH, sceFsDevpfsDeleteForVSH, sceFsDevpfsDeregisterEvent, sceFsDevpfsEventComplete, sceFsDevpfsEventResponseComplete, sceFsDevpfsFetchMetaEntry, sceFsDevpfsFsck, sceFsDevpfsInitFetchMetaEntryCtx, sceFsDevpfsMountForFSMP, sceFsDevpfsRegisterEvent, sceFsDevpfsWaitEvent, sceFsDrfsRegisterSocket, sceFsDrfsResumeSocket, sceFsErriCtl, sceFsErriGetState, sceFsExfatMkfsExternalHdd, sceFsExfatfsMkfs, sceFsExtUSBSchedDestroy, sceFsExtUSBSchedInsert, sceFsExtUSBSchedInsertForHDD, sceFsExternalStorageDeconfigure, sceFsExternalStorageGetRawDevice, sceFsExternalStorageInitialize, sceFsExternalStorageVerifyMetadata, sceFsGetAttribute, sceFsGetClusterStatistics, sceFsGetDeviceSectorsize, sceFsGetFsMetadata, sceFsGetFsMetadataWithProgress, sceFsGetSmrInfo, sceFsGschedCtl, sceFsISSchedConfigCurrentThread, sceFsISSchedConfigDescriptor, sceFsISSchedConfigThread, sceFsISSchedMapStat, sceFsISSchedResultToPriority, sceFsISSchedResultToSlot, sceFsISSchedStatCurrentThread, sceFsISSchedStatDescriptor, sceFsISSchedStatThread, sceFsISSchedUnmapStat, sceFsInitCreatePfsSaveDataOpt, sceFsInitCreatePfsTrophyDataOpt, sceFsInitMountDownloadDataOpt, sceFsInitMountGamePkgOpt, sceFsInitMountLwfsCompatOpt, sceFsInitMountLwfsOpt, sceFsInitMountPprPkgOpt, sceFsInitMountSaveDataOpt, sceFsInitMountTrophyDataOpt, sceFsInitUmountDownloadDataOpt, sceFsInitUmountGamePkgOpt, sceFsInitUmountLwfsCompatOpt, sceFsInitUmountLwfsOpt, sceFsInitUmountPprPkgOpt, sceFsInitUmountSaveDataOpt, sceFsInitUmountTrophyDataOpt, sceFsInitWorkspaceFileMetadataParam, sceFsLvdAttach, sceFsLvdAttach2, sceFsLvdAttachPartiallyCryptedDownloadData, sceFsLvdAttachSingleDefaultImage, sceFsLvdAttachSingleImage, sceFsLvdDetach, sceFsLvdDisableLayer, sceFsLvdEnableLayer, sceFsLvdSetDiscIdentity, sceFsLvdWaitResumed, sceFsMlfsMount, sceFsMlfsMountWithFlags, sceFsMlfsUmount, sceFsMountDownloadData, sceFsMountGamePkg, sceFsMountLwfs, sceFsMountLwfsCompat, sceFsMountPartiallyCryptedDownloadData, sceFsMountPprPkg, sceFsMountSaveData, sceFsMountTrophyData, sceFsNsfsAddOverlayToNamespace, sceFsNsfsMountWithFlags, sceFsNsfsSetGlobalExclude, sceFsNsfsUmount, sceFsPartitionTrim, sceFsReserveCluster, sceFsSetAttribute, sceFsSetClusterCacheSize, sceFsSetFileReservedTotal, sceFsSetSystemReserveSize, sceFsSetWorkspaceFileHash, sceFsStatWorkspace, sceFsSysStorageGetStatus, sceFsTransactionCancel, sceFsTransactionSaveDataInitMountOpt, sceFsTransactionSaveDataMount, sceFsTransactionStart, sceFsTrophyImageError, sceFsUfsAllocateAdditionalContents, sceFsUfsAllocateAligned, sceFsUfsAllocateDownloadData, sceFsUfsAllocateGameImage, sceFsUfsAllocatePatchImage, sceFsUfsAllocateSaveData, sceFsUfsAllocateTrophyData, sceFsUfsCheckCleanFlag, sceFsUfsCheckFixedCylinderGroupSize, sceFsUfsCollectTelemetry, sceFsUfsCollectTelemetry2, sceFsUfsDirectWrite, sceFsUfsEstimateZoneCapacityForUser, sceFsUfsExtendFile, sceFsUfsFsck, sceFsUfsFsckWithMM, sceFsUfsGetExtentMap, sceFsUfsGrowLwfs, sceFsUfsGrowfs, sceFsUfsHideBlocksFromUser, sceFsUfsInitalizeSparseFile, sceFsUfsLargewriteFsck, sceFsUfsLargewriteFsckWithMM, sceFsUfsMkLwfs, sceFsUfsMkfs, sceFsUfsMkfsForExternalHDD, sceFsUfsMkfsForSystem, sceFsUfsMkfsWithFixedCylinderGroupSize, sceFsUfsReallocateSparseFile, sceFsUfsRecryptBlocks, sceFsUfsRecryptGetState, sceFsUfsRecryptIsUnfinished, sceFsUfsRecryptSyncMetadata, sceFsUfsSeekHoleInFile, sceFsUfsStopOperation, sceFsUmountDownloadData, sceFsUmountGamePkg, sceFsUmountLwfs, sceFsUmountLwfsCompat, sceFsUmountSaveData, sceFsUmountTrophyData, sceFsUmountWorkspace, sceFsUnbindApplication, sceFsUnbindPackages, sceFsWsCreate, sceFsWsDelete, sceFsWsFetchFileMetadata, sceFsWsFsck, sceFsWsGetStatus, sceFsWsMount and sceFsWsUmount](../../../core/libs/prx/libSceFsInternalForVsh/Export.cpp) (libSceFsInternalForVsh) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFsInternalForVsh.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceGameLiveStreaming.md b/docs/dev/debt/libSceGameLiveStreaming.md index b30576336c..75299aa071 100644 --- a/docs/dev/debt/libSceGameLiveStreaming.md +++ b/docs/dev/debt/libSceGameLiveStreaming.md @@ -1,5 +1,53 @@ # libSceGameLiveStreaming technical debt +### Functional + +- [sceGameLiveStreamingEnableLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - a call outside `sceGameLiveStreamingInitialize` and `sceGameLiveStreamingTerminate` throws, as the module's error for it is not confirmed (`sceGameLiveStreamingTerminate` and `sceGameLiveStreamingGetCurrentStatus2` return `0x80A00004`; [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ps4_libscegamelivestreaming.pas#L18) does not return its `SCE_GAME_LIVE_STREAMING_ERROR_NOT_INITIALIZED` from this call). Otherwise both values are accepted and change nothing, as no broadcast is ever on air. The signature, `int sceGameLiveStreamingEnableLiveStreaming(const bool)`, follows [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L810) and [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/src/ps4_libscegamelivestreaming.pas#L111) +- [libSceGameLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - native initialization requires exactly 0x4000 heap bytes before checking repeated initialization. Host lifecycle and pointer errors now match the wrapper, but service registration, allocation failures and asynchronous service readiness are not modelled. The host becomes ready immediately after initialization. +- [sceGameLiveStreamingGetCurrentStatus2](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) - the supplied module confirms the 72-byte layout and inactive status (user -1, remaining bytes zero). Live service counters, active-user changes and broadcast-resolution conversion are unimplemented. +- [sceGameLiveStreamingGetProgramInfo](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - the PS5 12.00 module clears the 0x134C-byte program info, asks the system service for its first 0xFD4 bytes and returns `0x80A00002` for a null pointer and `0x80A00004` before initialization. With nothing broadcast here the info stays zero, which assumes the service reports an empty program when nothing is on air; the field layout is unknown + ### Unknown function info -- [sceGameLiveStreamingGetCurrentStatus2](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - the status layout (user id, on-air flag, spectator, text and command message counts, broadcast resolution, 48 reserved bytes) is taken from shadPS4's `OrbisGameLiveStreamingStatus2`; the call reports that no stream is on air, with an invalid user id +- [sceGameLiveStreamingScreenInitialize](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingScreenInitializeSeparateModeParameter](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingScreenTerminate](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingScreenCloseSeparateMode](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingScreenConfigureSeparateMode](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingScreenOpenSeparateMode](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingScreenSetMode](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingPermitLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingSetMaxBitrate](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingGetCurrentStatus](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingEnableSocialFeedback](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingGetSocialFeedbackMessagesCount](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingGetSocialFeedbackMessages](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingPermitServerSideRecording](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingSetMetadata](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingSetSpoilerTag](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingClearSpoilerTag](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingSetPresetSocialFeedbackCommands](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingClearPresetSocialFeedbackCommands](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingSetPresetSocialFeedbackCommandsDescription](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingClearSocialFeedbackMessages](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingPostSocialMessage](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingSetCameraFrameSetting](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingSetServiceProviderPermission](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingSetDefaultServiceProviderPermission](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingSetInvitationSessionId](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingStartGenerateStandbyScreenResource](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingStopGenerateStandbyScreenResource](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingRegisterCallback](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingUnregisterCallback](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingCheckCallback](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingSetStandbyScreenResource](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingGetCurrentBroadcastScreenLayout](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingLaunchLiveViewer](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingLaunchLiveViewerA](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingSetGuardAreas](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingStartSocialFeedbackMessageFiltering](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingStopSocialFeedbackMessageFiltering](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingApplySocialFeedbackMessageFilter](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingSetLinkCommentPreset](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingStartDebugBroadcast](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature +- [sceGameLiveStreamingStopDebugBroadcast](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - unknown signature diff --git a/docs/dev/debt/libSceGameUpdate.md b/docs/dev/debt/libSceGameUpdate.md index 7d7e234519..fd8f0f341a 100644 --- a/docs/dev/debt/libSceGameUpdate.md +++ b/docs/dev/debt/libSceGameUpdate.md @@ -1,5 +1,7 @@ # libSceGameUpdate technical debt -### Unknown function info +### Functional -- [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 +- [libSceGameUpdate](../../../core/libs/prx/libSceGameUpdate/Export.cpp): initialization tracks host state but does not connect the patch service. Request creation and entitlement version lookup throw instead of fabricating an offline error or a successful no-update result. The original maintains eight service-backed requests with IDs starting at `0x20000000`; the service's offline response is still unknown. Check, Abort and Delete implement argument/state checks but have no live requests to operate on. +- [sceGameUpdateCheck](../../../core/libs/prx/libSceGameUpdate/Export.cpp): option zero is supported for argument validation. The original additionally permits option one when a kernel platform predicate succeeds; that platform mode is not modeled. +- [sceGameUpdateGetAddcontLatestVersion](../../../core/libs/prx/libSceGameUpdate/Export.cpp): the supplied module confirms a 20-byte entitlement-label input and a 48-byte service output. Null pointers are rejected before initialization is checked. There is no local output-size validation or blanket clearing of the output; the host leaves it untouched when the missing service causes an exception. diff --git a/docs/dev/debt/libSceGic.md b/docs/dev/debt/libSceGic.md new file mode 100644 index 0000000000..c7b8a71c3e --- /dev/null +++ b/docs/dev/debt/libSceGic.md @@ -0,0 +1,5 @@ +# libSceGic technical debt + +### Unknown function info + +- [ and sceGicGetValue](../../../core/libs/prx/libSceGic/Export.cpp) (libSceGic) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGic.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceGnmDriverForNeoMode.md b/docs/dev/debt/libSceGnmDriverForNeoMode.md new file mode 100644 index 0000000000..46f92c14e9 --- /dev/null +++ b/docs/dev/debt/libSceGnmDriverForNeoMode.md @@ -0,0 +1,9 @@ +# libSceGnmDriverForNeoMode technical debt + +### Unknown function info + +- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 70 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. + +### Functional + +- [retail-only answers](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - the debug resets, the resource and owner registration calls, the SDMA calls and the SQ counter calls return `0x8EEE00FF`, the five `sceGnmDriverInternalRetrieveGnmInterface*` calls return `0x80000000`, `sceGnmGetDbgGcHandle` returns -1, `sceGnmDriverTraceInProgress` and `sceGnmValidateOnSubmitEnabled` return 0 and the other validation calls return `0x80D13FFF`. These are the values [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver.cpp) returns for calls it marks as not available in retail firmware; they are not measured on a console, and the arguments are not read diff --git a/docs/dev/debt/libSceHidControl.md b/docs/dev/debt/libSceHidControl.md new file mode 100644 index 0000000000..fc779520a0 --- /dev/null +++ b/docs/dev/debt/libSceHidControl.md @@ -0,0 +1,44 @@ +# libSceHidControl technical debt + +### Unknown function info + +- [sceHidControlEnableMicBeamForming](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlSetAllVibrationMute](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlSetVibrationTriggerEffectWeakForce](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlIsForceUpdateNeeded](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlSetRemoteMergeProcess](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlSetCollaboInputSettingOverrideProcess](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlSetForceAdvancedVibrationMode](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlPswUpdateIsUpdateNeeded](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlPswUpdateIsUpdateNeededForMultiDevice](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlPswUpdateIsReady](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlPswUpdateStart](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlPswUpdateGetStatus](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlPswUpdateSendData](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlPswUpdateSendAppendixData](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlPswUpdateGetReconnectStatus](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlPswUpdateGetDeviceFirmwareUpdateTelemetryData](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlGetPswState](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlGetPswBatteryState](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlSetPswDeviceName](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlGetPswDeviceName](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlSetPswConsoleMute](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlSetPswSpeakerVolume](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlSetPswSidetoneLevel](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlGetPswSidetoneLevel](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlSetPswEqPresetSelection](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlGetPswEqPresetSelection](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlSetPswEqPresetName](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlGetPswEqPresetName](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlSetPswEqBandValues](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlGetPswEqBandValues](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlGetPswAllEqBandValues](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlSetPswResetSettings](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlSetPswNetworkLocale](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlGetPswNetworkLocale](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlGetPswDeviceNameForNearby](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlConnectPswDeviceForNearby](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlGetPswDeviceInfo](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlSetPswCommand](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlGetPswCommand](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature +- [sceHidControlGetPswDeviceIdFromIdxAdd](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature diff --git a/docs/dev/debt/libSceHmd.md b/docs/dev/debt/libSceHmd.md index 1206760ae4..cf169b2329 100644 --- a/docs/dev/debt/libSceHmd.md +++ b/docs/dev/debt/libSceHmd.md @@ -3,3 +3,24 @@ ### Silent stubs - [libSceHmd](../../../core/libs/prx/libSceHmd/Export.cpp) implements only the disconnected-headset path: initialization succeeds, device queries report `NotDetected`, and opening a device returns `DeviceDisconnected`. Headset support, tracking, rendering and additional HMD exports are not implemented; SDK-level ABI compatibility and in-game behaviour remain unverified. + +### Unknown function info + +- [libSceHmd internal and capture exports](../../../core/libs/prx/libSceHmd/Export.cpp) - input/output directions and argument registers are recovered for shared VR2D data, device binding, optical and serial queries, screen status and capture operations. VR2D records are 128 bytes, optical output 16, serial output 16, PU position output 20, crash-dump output 68, audio/video status 32, and reprojection debug outputs 216 or 1136. Payload field meanings and capture callback contracts remain incomplete; these structures use forward declarations. + +- [libSceHmd queries without a headset](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - the signatures, the distortion work memory (`0x20000`, aligned to `0x400`) and reprojection buffer sizes (garlic `0x100000`, onion `0x810`, both aligned to `0x100`), and the results of `sceHmdGetFieldOfView`, `sceHmdGet2DEyeOffset`, `sceHmdGetInertialSensorData` (`HANDLE_INVALID` once initialized; field of view checks its output for null first) and `sceHmdGetAssyError` (null output, then `DEVICE_DISCONNECTED` once initialized) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/hmd/hmd.cpp) (PS4) with no headset, not a PS5 measurement. Before initialization `sceHmdGetFieldOfView` returns `NOT_INITIALIZED` for a non-null output, where shadPS4 checks the handle first and returns `HANDLE_INVALID`. The third argument of `sceHmdGetInertialSensorData` is unnamed there and taken as a count +- [libSceHmd VR2D queries](../../../core/libs/prx/libSceHmd/Export.cpp) (libSceHmd) - `sceHmdInternalIsGameVr2d`, `sceHmdInternalIsMiniAppVr2d`, `sceHmdInternalIsCommonDlgVr2d` and `sceHmdInternalIsCommonDlgMiniAppVr2d` return false: nothing runs in VR2D without a headset, and the console answers 0 to the same four queries in libSceHmd2 (`Hmd2BeforeInitializeZero` in [libSceHmd2](libSceHmd2.md)). The libSceHmd answers themselves are not measured. +- [sceHmdDistortionGetCompoundEyeCorrectionCommand](../../../core/libs/prx/libSceHmd/Export.cpp) returns a constant error without consuming arguments in the supplied module; unused argument types remain unresolved. +- [sceHmdInternalGetDefaultLedData](../../../core/libs/prx/libSceHmd/Export.cpp) takes an unused handle, a 32-bit mask output and a nine-byte LED output. Output sizes match the paired setter; mask bits and LED payload semantics remain unresolved. The protected body is unchanged. + +[Device, field-of-view, assembly-error, distortion, sensor and DFU query signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered from native register use. Field of view and assembly errors each output four floats; device information occupies 64 bytes and distortion output spans 76 bytes. The remaining device/sensor/report field semantics and DFU status bit meanings are unresolved. Assembly confirms integer error returns where decompilation incorrectly inferred void. Protected bodies are unchanged. + +[Reprojection initialization and display-buffer signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered: initialization takes a parameter pointer, 32-bit mode and null reserved pointer; display-buffer setup takes a VideoOut handle, two buffer indices and reserved pointer, while adding a buffer takes an index and reserved pointer. Parameter fields remain incomplete. Protected bodies are unchanged. + +[Work-memory queries, capture start, separate-mode open and crash-report cancellation](../../../core/libs/prx/libSceHmd/Export.cpp) consume no arguments in the supplied native implementation. The mirroring aspect debug setter takes five 16-bit values followed by a byte mode; the individual scalar meanings remain unresolved. These argument lists are confirmed; protected bodies remain unimplemented. + +[Reprojection start signatures](../../../core/libs/prx/libSceHmd/Export.cpp) are recovered, including the five arguments of `StartMultilayer2` hidden by an incomplete decompilation. Standard and wide-near starts take frame parameters, pose, 64-bit flip argument and reserved pointer; overlay variants add an overlay descriptor. The 2D variant takes parameters, flip argument and reserved pointer. Multilayer takes a layer array/count, submission parameters, pose, flip argument and reserved pointer. Frame, pose, overlay and submission field semantics remain incomplete. Protected bodies are unchanged. + +[Distortion and 2D VR command queries](../../../core/libs/prx/libSceHmd/Export.cpp) take a 32-bit HMD handle, input descriptor and command-buffer output. The output contains a command pointer at offset zero and a 32-bit dword count at offset eight. Descriptor fields and GPU resource types remain incomplete; the standard, wide-near and 2D descriptors are distinct. Without a headset `sceHmdOpen` never issues a handle, so the stored handle stays 0. As firmware 13.42's libSceHmd does when its exports are run under an emulator ([#9577](https://github.com/boykopovar/AnyPS5/pull/9577#issuecomment-6336871370)), the distortion queries return `HANDLE_INVALID` for any other handle before checking initialization, and `NOT_INITIALIZED` for handle 0 before it when the title's compiled SDK version is below 4.50. The 2D VR query first returns `PARAMETER_NULL` for a null descriptor or output or a null pointer at descriptor offset 0, 8 or 0x20, and `PARAMETER_INVALID` for a non-zero field at 0x34 (32-bit) or 0x38 (64-bit). None writes the output. From SDK 4.50 the firmware first checks state the library sets up when it starts, which is not measured ([#9577](https://github.com/boykopovar/AnyPS5/pull/9577#issuecomment-6343495321)), and what handle 0 gets after initialization is not measured either, so both throw. + +The ELF export table aliases the [Distortion correction/2D command and work-memory queries](../../../core/libs/prx/libSceHmd/Export.cpp) to their corresponding HMD queries, confirming identical signatures. Distortion initialization consumes only a work-memory pointer. No shader code or calibration constants are copied; each aliased command query answers as its twin, and distortion initialization keeps its protected body. diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index aab10875a0..25f1bc36a6 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -2,15 +2,85 @@ ### 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) has no PS VR2 runtime: Initialize returns unsupported-feature, so normal calls never reach initialized device behavior. Reprojection and gaze operations retain their throwing bodies. Four size/alignment queries return measured constants; QueryBufferSizeAlign still throws. Device setup, headset tracking and rendering are not implemented. ### Unknown function info -- PPSA29866 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [sceVideoOutSetFlipMaster and sceVideoOutClearFlipMaster](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut) -- [sceHmd2Initialize](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the error the console returns when the PS VR2 runtime is unavailable is unknown; it returns libSceHmd's `UNSUPPORTED_FEATURE` (0x81110016). PPSA01317 passes a pointer to 16 zeroed bytes and only tests the result for non-zero -- PPSA01317 imports declared without parameters, signatures unknown: the remaining [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) and [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list -- PPSA15878 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reprojection and gaze exports, `sceVrTracker2Finalize`, `sceVrTracker2GetPlayAreaOrientedBoundingBox` and `sceVrTracker2LocateCoordinateSystem` in [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp), and the `scePadVrController` trigger effect exports in [libScePad](../../../core/libs/prx/libScePad/Export.cpp). Names from the aerolib NID list +- [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 +- [libSceHmd2 legacy exports](../../../core/libs/prx/libSceHmd2/Export.cpp) - SeeThroughDraw, SeeThroughIC, VideoOutEq, WaitShader, SeeThroughCallbackGetPoseDataFunc, ReprojectionClear, ReprojectionSet and RpeorjectionSetParam have no matching symbols or callers in the supplied system modules. Their signatures remain unresolved. ClearUserEventEnd/Start, GazeStart/StopUserCalibration and GazeObjectSelectionTerminate are confirmed to take no arguments; their behavior after initialization remains unimplemented. +- [sceHmd2InternalSetPortStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalLedDetectorStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetPortConnection](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetCameraSeeThroughMode](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetFakeLensSeparationDistance](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalDfuSend](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalLedDetectorOpenBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageOpenBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalLedDetectorStopBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetDeviceConnection](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalLedDetectorCloseBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetSidetone](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGetDeviceId](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetBrightness](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageStopBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetVibrationMode](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGetVr2dData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageCloseBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature ### Functional -- 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 +- [Reprojection size/alignment queries](../../../core/libs/prx/libSceHmd2/Export.cpp) - all five exports consume no arguments and return two uint64 values in RAX:RDX. Four host queries return values measured without a headset; native display sizes are calculated through the graphics layout service and see-through size follows internal configuration. QueryBufferSizeAlign changes calculation with runtime state; its native pair return type is restored, while its protected body remains unchanged and throws. +- [Hmd2BeforeInitializeZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers 0 to these 45 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `vDgpdS2N9Rw` (sceHmd2InternalRegisterVibrationCallback), `Ri35gKTZpvE` (sceHmd2InternalUnregisterVibrationCallback), `9oyW8puiKeI`, `1HZNE6jhBnQ` (sceHmd2InternalPowerEventInitialize), `cdwAKBZLXjs`, `Yub1MF58jfY`, `JYXAuQFnPyw` (sceHmd2InternalDfuOpen), `VuPKqCu3wzw` (sceHmd2InternalDfuClose), `GSThagVyW5U` (sceHmd2InternalCrashReportOpen), `J4qE5N937MY` (sceHmd2InternalCrashReportClose), `9KnNj-dyASg`, `Qta2bkstbFk`, `5ZcObnDRBXE`, `AvPNn0qx4WQ`, `jp0apqNGgi8` (sceHmd2SetIpdOffset), `-kZT7X7OMg0` (sceHmd2InternalRelocalizerOpen), `1ewLtng-SVk` (sceHmd2InternalRelocalizerClose), `B0b8DeIUGes` (sceHmd2InternalTrackingOpen), `HclLg4Oj5yw` (sceHmd2InternalTrackingClose), `VbEvrQXsZyY` (sceHmd2InternalLedDetectorOpen), `bB6MKZi5UCQ` (sceHmd2InternalLedDetectorClose), `OXQ1EDf02XU` (sceHmd2InternalImageOpen), `IAxfMTvFm0M` (sceHmd2InternalImageClose), `qTJtAtaLeno` (sceHmd2InternalGenDataOpen), `j7Q16UX6l34` (sceHmd2InternalGenDataClose), `pmmNM06rLqU`, `zdQfyDK9Ods` (sceHmd2InternalSlamTraceOpen), `aPRwp9GnljM` (sceHmd2InternalSlamTraceClose), `MjY1gBSaed0` (sceHmd2InternalGazeOpen), `evAMjJYHqW4` (sceHmd2InternalGazeClose), `r93FLEZ3BBk` (sceHmd2InternalGazeStart), `IdYURdEdNFo` (sceHmd2InternalGazeStop), `CVcL2Cu2AEI` (sceHmd2GazeActivateGazeTracking), `NmKIfVxr2s0`, `KeDLkkp62Gg`, `+aadu6-ra+A`, `FgnTiwwGSzk`, `PWAUwOTHbbo` (sceHmd2InternalCreateSharedMemory), `WzaEApHnsP8` (sceHmd2InternalIsGameVr2d), `dXzQI8QVY7Y` (sceHmd2InternalIsMiniAppVr2d), `cCvFKGqRh1k` (sceHmd2InternalIsCommonDlgVr2d), `uj4-2VEAFPU` (sceHmd2InternalIsCommonDlgMiniAppVr2d), `sIaGStI-iLM` (sceHmd2InternalMapSharedMemory), `JzwpZOemDO8`, `IsC4R4bZFVU`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeNotInitialized](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720002` to these 9 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `sXaPEPY-fLA` (sceHmd2InternalOpen), `3F5inHhKzXg` (sceHmd2InternalGetDeviceInformation), `sJAiB-6p55U`, `E8hDydLjsSs`, `xEQkSb2WJ7Y`, `aaP4dnR4q-0`, `lEU0TeUoGwI`, `cFVyjHBpUUU`, `zROtb46jTj4`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeError0004](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720004` to these 15 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `kAMsjYHBbNQ` (sceHmd2InternalSetVolume), `5m73zmLpsQ0` (sceHmd2InternalResetViewPosition), `EPeH4xuQ2XQ` (sceHmd2InternalGetStatus), `TJ8lyRsjFY4` (sceHmd2InternalClearTrackerFts), `-XQVcLnyX68` (sceHmd2InternalSetSlamCommand), `giQwQoHhYYA` (sceHmd2InternalDfuStart), `zNc9IkgawMI`, `-tnG5UJPvAc` (sceHmd2GazeStartUserCalibration), `FPhFmG+jXuQ` (sceHmd2GazeStopUserCalibration), `CKlkvNngofQ`, `yL1CZaciFWE` (sceHmd2GazeSetUserCalibrationPoint), `qlzgyG3s0RY`, `kTIDrDLzMv0`, `9Gee0ef8a+U`, `ecZVjQdwmBI` (sceHmd2GazeSetEnabledEye). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeParameterNull](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` to these 16 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `MNtiJTDOPAU`, `RRy9WmqZfgU` (sceHmd2InternalDecryptData), `uobR4m9lgwc` (sceHmd2InternalGetMirroringMode), `-XPk5KWBYdo` (sceHmd2InternalRelocalizerSetResultData), `J44idh9izZM`, `Ps0wAvYaa1E`, `kLnMR8oQ1MY`, `kMz4rbBxvjo`, `DIU1sFRRaiE` (sceHmd2GazeGetCalibrationData), `ZQvU4vDzZGQ` (sceHmd2GazeSetUserCalibrationData), `TJpyGGP9YEY`, `Dih+Odk+RC0`, `oaqdSJw+HVY`, `m35gtHuanXU`, `CTM7QPLJF9Q` (sceHmd2InternalGetFlipInfo), `+rExHeC9lP0` (sceHmd2GazeObjectSelectionInitialize). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeParameterInvalid](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720009` to these 9 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `bBh5bCp9coo` (sceHmd2InternalSetPositionResetIcon), `G3PNDE2VjfY` (sceHmd2InternalSetPowerOff), `nScCT0OPU0o`, `6Q0Ng7OTN9o`, `-fJKh4SnFR0` (sceHmd2InternalSetBudgetMode), `jUA2s-l0npY` (sceHmd2InternalSetForceIcon), `+9XhyAIJVj4`, `eBGK-o6E4RQ` (sceHmd2InternalBindDeviceWithUserId), `MTuFeL9184E`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeReprojectionNotInitialized](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a72000b` to these 10 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `d3SEIYTP6wc`, `EVECbTvBezo`, `ciusPdJKjSU`, `DIwqTkcZiJo` (sceHmd2ReprojectionSetUserEventStart), `64ERZWVFBTE` (sceHmd2ReprojectionSetUserEventEnd), `z0+0gXUJWs8` (sceHmd2ReprojectionClearUserEventStart), `rjAd4nCY4DM` (sceHmd2ReprojectionClearUserEventEnd), `Acyg4vTdGm0` (sceHmd2ReprojectionGetCurrentOutput), `aGOWaEeg04I`, `9up+EPhw1No`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeError002C](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a72002c` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `V1kLQmP-17U` (sceHmd2GazeObjectSelectionTerminate). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a721002` to these 5 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `2LVD1EC5SYE`, `Bf87AL6zWxU`, `XKt7w1MODFY`, `Y5s22M-qUDA`, `JJKY-BtQJLg`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a721003` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `9kj9vw8UShQ`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeMinusOne](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers -1 to these 12 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `EAkv8Mqt33E` (sceHmd2InternalRelocalizerStart), `Rv7bkmKvmz0` (sceHmd2InternalRelocalizerStop), `1oPZizn0SNA` (sceHmd2InternalTrackingStart), `TwCpE-nNrlo` (sceHmd2InternalTrackingStop), `jkqAeQfdtxw` (sceHmd2InternalLedDetectorStart), `+l0RQph81Z8` (sceHmd2InternalLedDetectorStop), `qsmAtGw1Ncc` (sceHmd2InternalImageStart), `uKhwYoMsIsA` (sceHmd2InternalImageStop), `WNdaigr94dI` (sceHmd2InternalGenDataStart), `P6S5ZVWf-H0` (sceHmd2InternalGenDataStop), `sun6NWE1sdg` (sceHmd2InternalSlamTraceStart), `--TFsJYXo04` (sceHmd2InternalSlamTraceStop). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeValue0400](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x400` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `BWqcwjCozzI`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeValue20000](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x20000` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `PRz7TdBGUI0`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenError0004](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a720004` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 20 exports: `9WeZr4kYfzk` (sceHmd2InternalGetFwVersion), `N+5h1hCOFAg` (sceHmd2InternalGetRequiredPUPVersion), `w2vEs2y5ZmE` (sceHmd2InternalGetOpticalParam), `L5gat1pe0y0` (sceHmd2InternalSetCameraSeeThroughParam), `4GnLHR9NJ1w`, `AHk7nGSzef0` (sceHmd2InternalGetTrackerFtsData), `ncjX9d3mc0M`, `l63QYAUuogc` (sceHmd2InternalGetSlamCommand), `mrcBySCdXSo` (sceHmd2InternalDfuGetStatus), `ieBra+td8sU` (sceHmd2InternalDfuGetUpdateStatus), `s+rzBnewgGs` (sceHmd2InternalDfuCheckAfterPvt), `Nfcm0GBdzZI` (sceHmd2InternalRelocalizerGetVersion), `Tc9rasWfrm4` (sceHmd2InternalSetHomeCalibration), `ckTbKTX4KZg`, `D7576PUbUjk` (sceHmd2InternalGazeReadStatus), `tTJ04SrBT8U`, `sDP7hZ-P6OI` (sceHmd2GazeGetDeviceStatus), `J+eVNbN9ndY` (sceHmd2GazeGetUserCalibrationStatus), `kozvHBNyxf0`, `638Tmg5y6U4`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and 0 when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 8 exports: `8ETtFtp5zqY` (sceHmd2InternalGetVrMode), `CWPkBm3EXQA`, `R62liQQIAaA` (sceHmd2InternalGetFakeLensSeparationDistance), `-bcWLHsdzo0`, `JngJzgNbu8U` (sceHmd2SetFieldOfView), `xNBSz8xrGdk`, `yUVPfN-jRIU`, `BKPR3RZ5aO4`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a721002` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 8 exports: `IG4y+drWAWw` (sceHmd2InternalRelocalizerReadData), `rhYUml9vppE` (sceHmd2InternalRelocalizerReadDataAsync), `6574xHS+jZk` (sceHmd2InternalRelocalizerReadData2), `mXzaY6xVoxg` (sceHmd2InternalTrackingReadData), `6af-HIZQQn4` (sceHmd2InternalTrackingReadData2), `6qWtdTHQsTk`, `pUm8t8xWhFg`, `A0XKI-WjxK8` (sceHmd2InternalImageReadData). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenMinusOne](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and -1 when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 5 exports: `A9s2j8OioPQ`, `YIgtmrL+HLY`, `Ng8zEkGxDNs`, `EtslTnqO1xU` (sceHmd2InternalImageStart2), `hfrnRTryKQQ` (sceHmd2InternalImageSetConfig). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstTwoThenParameterInvalid](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 1 is null and `0x8a720009` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 3 exports: `PPX68fNToFM`, `LxPIfjHaY4c` (sceHmd2GazeObjectSelectionQueryMemory), `w8gBTTdg+po` (sceHmd2GazeObjectSelectionGetSelectedObject). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckSecondThenError002C](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a72002c` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 3 exports: `tWJPX2ycRMM`, `5UMB9PJA70o`, `xQs62qCk1EI`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenParameterInvalid](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a720009` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 2 exports: `MMc+bSlqkzk` (sceHmd2InternalSetVirtualDisplaySize), `NpZHgvSYgEA`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckAllSixThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 1 and argument 2 and argument 3 and argument 4 and argument 5 is null and 0 when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 2 exports: `z-serf9vsb0` (sceHmd2InternalPushVr2dData), `IRMUFu+uG5s`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckSecondThenParameterInvalid](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a720009` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 2 exports: `72U64E+0z6U`, `LCQzi5ILGrY` (sceHmd2GazeGetCalibrationDataMaxSize). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenError800d0203](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x800d0203` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `Im0qL0j9Ubg`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a721003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `ng6eI8uA0h4` (sceHmd2InternalImageGetMemoryLayout). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenError1010](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a721010` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `mPPGgjyJrCI`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstTwoThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 1 is null and `0x8a721002` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `ZupCJmV5BDI` (sceHmd2InternalImageGetFrameData). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstTwoThenError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 1 is null and `0x8a721003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `mAQZwOX1IHU` (sceHmd2InternalImageGetAverageLuminance). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstAndThirdThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 2 is null and 0 when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `oacDkQkinLo`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstAndThirdThenError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 2 is null and `0x8a721003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `zn7PHGzzEyI`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckSecondThenInvalidHandle](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a720003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `wEHILc7TP04` (sceHmd2GazeInsertRawDataWithHandle). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckSecondThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a721002` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `W-ZIlUXcrNA`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2WriteStateTwo](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the five bytes `02 00 00 00 00` at the start of its first argument and returns 0; a null first argument returns `0x8a720008`: `omimgG5itSs`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2WriteSize0x2000Align0x100](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 64-bit values `0x2000` through its first argument and `0x100` through its second and returns 0; a null first or second argument returns `0x8a720008`: `6PeHcAaKYg8`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2WriteFourConstants](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 16 bytes `e1 f0 74 3c d8 f0 74 3c 1f f1 74 3c d8 f0 74 3c` (four floats near 0.01495) at the start of its first argument and returns 0; a null first argument returns `0x8a720008`: `yur+xPdWPAg`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2WriteSixHalvesThenInvalidHandle](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 24 bytes `00 00 00 00 00 00 00 3f 00 00 00 3f 00 00 00 00 00 00 00 3f 00 00 00 3f` (floats 0, 0.5, 0.5, 0, 0.5, 0.5) at the start of its second argument and returns `0x8a720003` for every first argument tried (0, a pointer); a null second argument returns `0x8a720008`: `RnieDDp5ge4`, `jM3NEJA11yw`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2WriteTwelveHalvesThenInvalidHandle](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 48 bytes `00 00 00 00 00 00 00 3f 00 00 00 3f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3f 00 00 00 3f 00 00 00 00 00 00 00 00 00 00 00 00` (floats 0, 0.5, 0.5, 0, 0, 0, 0, 0.5, 0.5, 0, 0, 0) at the start of its second argument and returns `0x8a720003` for every first argument tried; a null second argument returns `0x8a720008`: `b0H-n-tUk0U`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo3CheckSecondThirdThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns 0 for a first argument of 0 to 3 and `0x8a720009` for 4, 5, 8, 16, 255 and -1; a null second or third argument returns `0x8a720008` even with an invalid first argument. Its fourth to sixth arguments answered `0x8a720009` when null with an invalid first argument, which does not separate them from the first-argument check, so they are not looked at: `MXhloJ6Iw3w`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo1ThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a721002` for a first argument of 0 or 1 and `0x8a720009` for 2 to 5, 8, 16, 255 and -1; its other arguments were only nulled with an invalid first argument, which answered `0x8a720009` either way, so they are not looked at: `19wrIIiqFgM`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo1CheckSecondThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a721002` for a first argument of 0 or 1 and `0x8a720009` for 2 to 5, 8, 16, 255 and -1; a null second argument returns `0x8a720008` even with an invalid first argument: `Z389vAm47Ys`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo1ThenMinusOne](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns -1 for a first argument of 0 or 1 and `0x8a720009` for 2 to 5, 8, 16, 255 and -1; the other arguments are not looked at, for the reason given above: `cDcrvEG8j8k`, `tuqJO6O6cb4` (sceHmd2InternalLedDetectorSetIrCameraConfigBySlot), `fd4fu1cziaI` (sceHmd2InternalLedDetectorSetPriorityConfigBySlot). Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo2ThenMinusOne](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns -1 for a first argument of 0 to 2 and `0x8a720009` for 3 to 5, 8, 16, 255 and -1; the other arguments are not looked at: `UxMg7iL4nRs`, `K1GELwlaaGc` (sceHmd2InternalImageSetConfigBySlot). Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo2ThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a721002` for a first argument of 0 to 2 and `0x8a720009` for 3 to 5, 8, 16, 255 and -1; the other arguments are not looked at: `RFn39VCHjGY`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo2ThenError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a721003` for a first argument of 0 to 2 and `0x8a720009` for 3 to 5, 8, 16, 255 and -1; the other arguments are not looked at: `FRoSjceDv10`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo2CheckSecondThenError002C](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a72002c` for a first argument of 0 to 2 and `0x8a720009` for 3 to 5, 8, 16, 255 and -1; a null second argument returns `0x8a720008` even with an invalid first argument: `wQ44gBwBw44`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2IndexZeroCheckSecondThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns 0 for a first argument of 0 and `0x8a72100f` for 1 to 5, 8, 16, 255 and -1; a null second argument returns `0x8a720008` even with a nonzero first argument. Its third to sixth arguments answered `0x8a72100f` when null with a nonzero first argument, which does not separate them from the first-argument check, so they are not looked at: `wiv8+7ogrq0`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [FBUEhH9tQ1A](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - left unimplemented: before initialization it writes the 32-bit value 9 through its second argument and returns 0 when its first argument points at zeroed memory, but returns `0x8a720001` without writing when that memory holds 0xAA bytes, and its fourth to sixth arguments answer `0x8a720001` when null, so the first argument is an in-out structure whose fields are not known + +- [libSceHmd2 gaze exports](../../../core/libs/prx/libSceHmd2/Export.cpp) - gaze argument registers and input/output directions are recovered. Result queries take a 16-byte size/handle parameter. The 24-byte foveated result contains size, status and a three-float direction; its trailing four bytes are untouched. The 56-byte full result still needs its eye-field semantics. Object selection uses a 120-byte parameter and 56-byte result whose fields remain incomplete. Its initialization and memory-query structures are recovered, including the 32-bit object count and memory size, memory pointer and 64-bit alignment. Calibration data descriptors contain a pointer and signed 32-bit size; calibration status contains two 32-bit selectors and three floats. The underlying calibration blob formats remain incomplete. Raw gaze input is mutable. CalibrationPoint is three floats passed by value in two SIMD registers; SetIpdOffset takes a byte and a float. + +- [libSceHmd2 reprojection exports](../../../core/libs/prx/libSceHmd2/Export.cpp) - frame identifiers and predicted display times are 64-bit. Event setters accept a kernel event queue and 32-bit event ID, consumed by sceKernelTriggerUserEvent. Mirroring uses two memory pointers and a separate 32-byte mode/reserved option. Status and current-output queries write 104 and 32 bytes; those field layouts and the reprojection initialization fields remain incomplete. Positional-reprojection mode is a byte with values 0 through 5, not a boolean. + +- [Reprojection parameter submission](../../../core/libs/prx/libSceHmd2/Export.cpp) - SetParam and SetParamWithBuffer take a 32-bit buffer-set ID, 64-bit frame ID, parameter pointer and reserved register slot. The parameter contains a layer pointer, signed count and optional 128-byte flags/reserved structure; plain and buffered layer strides are 328 and 472 bytes. Layer union fields remain incomplete. SetRenderConfig consumes three 64-bit slots; only bits 0 and 32 of the first can be nonzero. These exports remain unimplemented. diff --git a/docs/dev/debt/libSceHmdSetupDialog.md b/docs/dev/debt/libSceHmdSetupDialog.md new file mode 100644 index 0000000000..1566cbc62a --- /dev/null +++ b/docs/dev/debt/libSceHmdSetupDialog.md @@ -0,0 +1,5 @@ +# libSceHmdSetupDialog technical debt + +### Silent stubs + +- [libSceHmdSetupDialog](../../../core/libs/prx/libSceHmdSetupDialog/Export.cpp) - the HMD setup dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no headset is supported (see libSceHmd2). The signatures are from [Orbital's type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); the result layout beyond the leading `int32_t` result code is not known, so nothing else is written. diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index 25cae419d8..7d65b883c1 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -3,22 +3,38 @@ ### Silent stubs - [libSceHttp](../../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit, `sceHttpSetRecvBlockSize` (`(int id, u32 blockSize)` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h)) no response body to read in blocks and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate; `sceHttpSetRedirectCallback` and `sceHttpSetCookieRecvCallback` accept callbacks that are never called, `sceHttpsSetSslVersion` has no TLS connection to configure and `sceHttpsGetSslError` reports error 0 with detail 0; `sceHttpSetAuthInfoCallback` accepts a callback that is never called, as no server asks for authentication; `sceHttpsGetCaList` and `sceHttpsFreeCaList` report an empty CA list (`certs` null, `certsNum` 0) and return 0, as no CA store is kept, and a null list returns `ERROR_INVALID_VALUE`; unlike shadPS4 they do not check that `http_ctx_id` is a live context nor that the library is initialised, and `sceHttpsFreeCaList` also nulls `certs`, which shadPS4 leaves untouched. The signatures and the `{ void* certs; int certsNum; }` layout follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) (PS4), not confirmed on a PS5 title +- [HTTP options without a network](../../../core/libs/prx/libSceHttp/src/Options.cpp) (libSceHttp) - `sceHttpCacheRedirectedConnectionEnabled`, `sceHttpsEnableOptionPrivate` and `sceHttpsDisableOptionPrivate` return 0 for a live template, connection or request and keep nothing, since no request reaches the network; the private options refuse flags outside `0x2dff` with `ERROR_INVALID_VALUE` before they look at the id, and `sceHttpAbortRequestForce` does what `sceHttpAbortRequest` does. Signatures, the mask and the order of the checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/network/http.cpp) (PS4), not measured on a PS5 +- [sceHttpSetProxy](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - returns 0 for a live template, connection or request and keeps nothing, since no request reaches the network. The signature `(id, HTTP proxy conf, WLAN proxy conf, host, port)` and the order of the checks (`ERROR_BEFORE_INIT`, then `ERROR_INVALID_VALUE` for a null host, then `ERROR_INVALID_ID`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/network/http.cpp) (PS4), which stores the proxy; not measured on a PS5 ### Unknown function info -- [sceHttpUriBuild](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) retains `//` for a non-opaque URI independently of the selected fields, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a8a70da595da2a96e0410309472ba4a7eb558144/src/core/libraries/network/http.cpp#L3551-L3558). Black Myth: Wukong builds its `Scheme` header with option 1 and expects `http://` or `https://`. Other partial field combinations follow the PS4 reference and remain unverified on PS5 hardware. +- [HTTP object options](../../../core/libs/prx/libSceHttp/src/Options.cpp) (libSceHttp) - a template starts with redirects followed, blocking calls and no `Accept-Encoding: gzip`, and a connection or request copies its parent's options when it is created. The signatures of the option calls, these defaults, the copying and the order of the checks (`BEFORE_INIT` first; the last `sceHttpTerm` ends every object's options) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/network/http.cpp) (PS4) and are not measured on a PS5. `sceHttpSetDefaultAcceptEncodingGZIPEnabled` sets the gzip value for templates created later in that context, where shadPS4 keeps one value for the library, and `sceHttpUnsetEpoll` takes a template or connection id, where shadPS4 looks only at requests. The options change no request, as none reaches the network +- [sceHttpGetConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - connection and request diagnostic signatures are recovered; their outputs contain four and three 32-bit words, respectively. Field semantics remain incomplete. Epoll setters/getters distinguish a 32-bit epoll ID from a pointer-sized handle, and use a pointer-sized user argument. The supplied firmware getter dispatch does not handle the pointer-handle selector used by GetEpoll, so its intended successful behavior is not recoverable from this implementation. + - [sceHttpSetCookieEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) - the two-argument signature and 0/1 enable values follow [VitaSDK](https://github.com/vitasdk/vita-headers/blob/master/include/psp2/net/http.h) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp); PPSA01521 passes 0 to disable cookies. Disabling succeeds because the offline HTTP backend has no cookie engine; enabling throws. Invalid enable values use the same error as `sceHttpSetInflateGZIPEnabled`. Like the existing offline HTTP setters, the handle is not validated; requests still fail with the network error. Console behavior is unverified -- [sceHttpParseResponseHeader](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); parsing and error codes follow [shadPS4's reverse-engineered PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Folded values retain their line breaks and whitespace; an empty first line starts the value scan after its line feed. A consumed byte count above `INT_MAX` throws instead of wrapping. -- [sceHttpParseStatusLine](../../../core/libs/prx/libSceHttp/src/Parse.cpp) (libSceHttp) - behaviour and error codes follow the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title; unlike it, no byte past `lineLen` is read -- [sceHttpUriMerge](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); validation, size formula, absolute/relative split and path join follow the disassembly-like body in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. The base URL goes through our `sceHttpUriParse`, which removes dot segments from the base path, while the relative part is appended unswept; base query and fragment are dropped. The relative reference is only classified by its scheme and a leading `//`, and only references with `//` are validated; as in shadPS4, `//host/x` is returned unchanged instead of taking the base scheme, a reference with a scheme but no `//` (`mailto:x`) is joined onto the base directory, and `?q` and `#f` are appended to the base directory instead of the base path. -- [sceHttpUriUnescape](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); percent decoding, invalid sequences and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Size includes the final NUL; malformed escapes and `+` remain unchanged, and decoding runs once. -- [sceHttpUriSweepPath](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - behaviour follows the [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) reimplementation of the PS4 library and its tests (including the PS4 SDK example), not confirmed on a PS5 title or hardware. It is not RFC 3986 5.2.4: a path that doesn't start with `/` is copied unchanged and a trailing `.` or `..` without a slash is kept. Unlike shadPS4, nothing at or past `srcSize - 1` is read, and `srcSize == 1` writes only the terminator +- [sceHttpParseResponseHeader](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); the result, value bounds and error codes follow a payload process on a PS5 (firmware 13.60). Unmeasured there: a bare carriage return, and vertical tab or form feed around the value, which are skipped as blanks before it as before. A value end above `INT_MAX` throws instead of wrapping +- [sceHttpParseStatusLine](../../../core/libs/prx/libSceHttp/src/Parse.cpp) (libSceHttp) - follows a payload process on a PS5 (firmware 13.60) for 124 lines and lengths. As on the console, `lineLen` is only checked to be at least 8 and to leave the 3 code digits (`lineLen - index < 3` in unsigned arithmetic): the version digits and the `.` and space after them are read past `lineLen`, and the line feed is searched up to and including the byte at `lineLen`, so a length that stops right before the line feed still parses. When the version ends past `lineLen` both subtractions wrap and the search goes on until a line feed; what the console does when none follows in readable memory was not measured +- [sceHttpUriMerge](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); results, errors and `require` (the base's `sceHttpUriParse` size plus 2 and twice the two string lengths, also for a reference copied unchanged) follow a payload process on a PS5 (firmware 13.60). A reference with an authority (`//` after an optional scheme) is copied unchanged; any other, including `mailto:x`, `http:d`, `?q` and `#f`, is appended unswept to the base path up to its last `/`, and the base query and fragment are dropped. A reference that `sceHttpUriParse` rejects returns that error, which was not measured. +- [sceHttpUriParse](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - follows a payload process on a PS5 (firmware 13.60) for every ASCII byte inside a scheme, userinfo and hostname, and for letters, digits, `+`, `-` and `.` at the start of a scheme, where a digit ends it; other bytes at the start of a scheme are taken as they are inside one. The userinfo ends at the first `@` after bytes from the RFC 3986 userinfo set; a hostname is letters, digits, `-`, `_` and `.` without a leading `.`, and the first byte outside it starts the path instead of failing (`http://bad host/` has the host `bad` and the path ` host/`). A port is up to 5 digits that end at `/` or the end, at most 65536, which reads as 0; an empty port takes 443 for a scheme starting with `https`, 80 for one starting with `http` (case-insensitive) and 0 otherwise. The path is swept as `sceHttpUriSweepPath` does, while the pool keeps room for the unswept path. With a pool too small the console returns `ERROR_OUT_OF_MEMORY`, leaves `require` alone and has already written part of the pool; how much is not known, so nothing is written here. +- [sceHttpUriSweepPath](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - follows a payload process on a PS5 (firmware 13.60), which agrees with the [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) reimplementation of the PS4 library: it is not RFC 3986 5.2.4, a path that doesn't start with `/` is copied unchanged and a trailing `.` or `..` is kept. Like the console, the byte at `srcSize - 1` is read as the `/` that ends a `..` segment, and `srcSize == 1` with a leading `/` writes `/`; whether it also ends a `.` segment was not measured, so a `.` there is still kept as before - [sceHttpSetResponseHeaderMaxSize, sceHttpRedirectCacheFlush, sceHttpsUnloadCert](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4); no PS5 reference has them - [sceHttpSetRequestStatusCallback](../../../core/libs/prx/libSceHttp/Export.cpp) - the three-argument ABI follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp). Registrations and user data are retained and removed on request deletion; the offline backend never dispatches callbacks. Invalid ids return `0x80431100`, an uncited assumption, not a verified console error. Handle hierarchy, inheritance and deleted-handle validation remain incomplete; callback statuses and dispatch timing are unknown. - [sceHttpSetRedirectCallback, sceHttpSetCookieRecvCallback, sceHttpsSetSslVersion, sceHttpsGetSslError](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4), which only logs and returns 0; a null `errNum` or `detail` in `sceHttpsGetSslError` is assumed to return `SCE_HTTP_ERROR_INVALID_VALUE` like `sceHttpGetLastErrno` - [sceHttpSetAuthInfoCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature and callback type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) (PS4); no PS5 reference has them. The `id` is not checked, as in the other libSceHttp setters - [sceHttpWaitRequest](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so a wait returns 0 events when its timeout passes +- [sceHttpUriCopy](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - unverified: whether the strings are written before the size check, which a short pool hides by ending cleared. The rest follows a payload process on a PS5 (firmware 13.60): the arguments are (out, src, pool, prepare, require) as in the disassembly; the whole output and all `prepare` pool bytes are cleared before the source is read, so a source that is the output or lies in the pool copies as empty; the scheme, hostname, path, username and password are copied in that order with terminators, a null one as empty, and the port, while the query, fragment and opaque flag stay 0; a short pool returns 0x80431022 and leaves `require` alone; a null source returns 0x80433060, and no output or pool with a null `require` 0x804311FE +- [sceHttpDbgEnableProfile](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the export is absent from the supplied native module; its signature remains unresolved. +- [sceHttpDbgSetPrintf](../../../core/libs/prx/libSceHttp/Export.cpp) returns constant success without consuming arguments in the supplied module; the callback contract remains unresolved. +- [sceHttpDbgShowStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the native diagnostic consumes no arguments; it prints library state. The throwing implementation remains unchanged. +- [sceHttpDbgShowConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the native diagnostic consumes no arguments; it prints library state. The throwing implementation remains unchanged. +- [sceHttpDbgShowRequestStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the native diagnostic consumes no arguments; it prints library state. The throwing implementation remains unchanged. +- [sceHttpDbgShowMemoryPoolStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the native diagnostic consumes no arguments; it prints library state. The throwing implementation remains unchanged. ### Functional +- [sceHttpUriParse, sceHttpUriBuild, sceHttpUriMerge](../../../core/libs/prx/libSceHttp/src/Uri.cpp) - bytes 0x80 to 0xFF were measured only as 0x80, 0xC3 and 0xFF in a hostname; the others, and any of them in a scheme or userinfo, are taken as the measured ASCII bytes outside those sets are. ASCII classification and scheme case folding are independent of the host locale. A bracketed host such as `[::1]` is parsed, merged and built without its brackets, as a PS5 (firmware 13.60) does; a PS4 (firmware 10.71) takes no bracketed host (`http://[::1]x/` has an empty host and the path `[::1]x/`) and keeps `[::1]:80` in a merge, as measured in #9058, so this follows the PS5 only. + +- [sceHttpUriEscape](../../../core/libs/prx/libSceHttp/src/Uri.cpp) escapes every byte except the ASCII unreserved set in [RFC 3986 section 2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3), independently of the host locale. Console behaviour for non-ASCII input remains unmeasured. + +- [sceHttpAuthCacheFlush, sceHttpCookieFlush](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the context-id signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs), and the NIDs `zzB0StvRab4` and `seCvUt91WHY` match [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp). The offline backend never stores authentication credentials or cookies, so flushing a live context succeeds with empty caches. Calls return `BEFORE_INIT` (0x80431001) when no context is alive and `INVALID_ID` (0x80431100) for an unknown, deleted or non-context handle, following the existing context lifecycle checks. These error codes and their precedence have not been measured on PS5 hardware; shadPS4 only stubs these calls. Network-backed caches remain unimplemented. - [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/libSceHttp2.md b/docs/dev/debt/libSceHttp2.md index 3486ea1fd3..73186b8a90 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -2,16 +2,18 @@ ### Silent stubs -- [libSceHttp2](../../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and its value is ignored, as for `sceHttp2SslEnableOption` and `sceHttp2SslDisableOption`. The three setters validate `id` against the live handle registry and return `SCE_HTTP2_ERROR_INVALID_ID` (`0x817B1100`) for a handle that was never allocated, was deleted, or belonged to a terminated context, following [prosper](https://github.com/mattias800/prosper). Whether the console restricts the object kind these setters accept, and what it does with an id from a previous init/term cycle, is unverified, so any live handle kind is accepted and ids are never reused. `sceHttp2Term`, `sceHttp2DeleteTemplate`, `sceHttp2DeleteRequest`, `sceHttp2CreateTemplate`, `sceHttp2CreateRequestWithURL`, `sceHttp2CreateCookieBox` and `sceHttp2SendRequestAsync` also return the invalid-id error for a handle of the wrong kind or no longer live, following prosper (not measured on a console) +- [libSceHttp2](../../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and its value is ignored, as for `sceHttp2SslEnableOption` and `sceHttp2SslDisableOption`. The three setters validate `id` against the live handle registry and return `SCE_HTTP2_ERROR_INVALID_ID` (`0x817B1100`) for an invalid handle, following [prosper](https://github.com/mattias800/prosper). Whether the console restricts the object kind these setters accept, and what it does with an id from a previous init/term cycle, is unverified, so any live handle kind is accepted and ids are never reused. `sceHttp2Term`, `sceHttp2DeleteTemplate`, `sceHttp2DeleteRequest`, `sceHttp2CreateTemplate`, `sceHttp2CreateRequestWithURL` and `sceHttp2SendRequestAsync` also return the invalid-id error for a handle of the wrong kind or no longer live, following prosper (not measured on a console) ### Unknown function info -- [sceHttp2WebSocketSetPingInterval](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2, NID `YfaXPnc8PJE`) - imported by `libdiscord_partner_sdk.prx` of PPSA23126 (issue 2648). The parameters, the unit of the interval and the console errors are unknown. Every call throws. - [sceHttp2DeleteCookieBox](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - imported by PPSA35943's Unity player. The `(int cookieBoxId)` signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http2.cs#L215-L217). A box `sceHttp2CreateCookieBox` returned is discarded; an unknown or already deleted id returns the library's invalid-id error (0x817B1100), as [prosper](https://github.com/mattias800/prosper/blob/b1bec57186a0/prosper/src/hle/net/hle_http2.cpp#L512-L523) does; not measured on a console - [sceHttp2SetResolveRetry](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - signature `(int id, int32_t retry)` from libSceHttp's `sceHttpSetResolveRetry`; the value is accepted and ignored like the other Http2 settings, since no request reaches the network -- [sceHttp2WebSocketCreateRequest, sceHttp2WebSocketCloseAsync, sceHttp2WebSocketSendTextMessageAsync, sceHttp2WebSocketSendDataMessageAsync](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - imported by PPSA16064 and not called during its start-up; unknown signatures -- [libSceHttp2 cookie boxes](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - `sceHttp2CreateCookieBox` is taken to take the library context id first, as in the [prosper](https://github.com/mattias800/prosper) reimplementation; further arguments are unknown and ignored. `sceHttp2SetRequestNoContentLength` is assumed to take only the request id. Cookies are never stored, so `sceHttp2CookieFlush` has nothing to discard +- [libSceHttp2 cookie boxes](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - `sceHttp2CreateCookieBox` takes a machine-sized pool size, requires at least 64 KiB and allocates independently of HTTP contexts. The host reserves the pool with 16 KiB rounding but does not reproduce native cookie-heap bookkeeping, identifier ranges or retained request references. Its delete signature and invalid-ID error are confirmed. `sceHttp2SetRequestNoContentLength` takes only the request id. Cookies are never stored, so `sceHttp2CookieFlush` has nothing to discard + +- [libSceHttp2 pending exports](../../../core/libs/prx/libSceHttp2/Export.cpp) - 18 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHttp2.c). ### Functional - [libSceHttp2 asynchronous requests](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. PPSA15508 passes null kqueue and extra options at original VA 0xb63931, then calls `sceHttp2WaitAsync` with a 24-byte result and a 2,000,000 timeout, and checks the result at offset 8. A null kqueue option therefore retains the same offline completion for direct waiting without an event notification. The timeout is not used because the offline result is already complete. This path follows the original title call and caller control flow, not a console measurement. A supplied kqueue with a missing user event, an extra option, and a wait with no completed operation still throw + +- [WebSocket ping settings](../../../core/libs/prx/libSceHttp2/Export.cpp): both setters take a 32-bit request ID and a 32-bit interval/timeout in microseconds, confirmed by the timer consumer and its clock conversion. The protected implementations remain unchanged. diff --git a/docs/dev/debt/libSceHttpCache.md b/docs/dev/debt/libSceHttpCache.md new file mode 100644 index 0000000000..a2d7b748a6 --- /dev/null +++ b/docs/dev/debt/libSceHttpCache.md @@ -0,0 +1,6 @@ +# libSceHttpCache technical debt + +### Unknown function info + +- [sceHttpCacheSystemSendStatistics and sceHttpCacheSystemTerm](../../../core/libs/prx/libSceHttpCache/Export.cpp): the supplied implementations return zero without reading arguments. No callers have yet established whether their public prototypes include unused arguments. +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceHttpCacheClearAllUserTags`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheClearCacheInfo`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheDebugSetUrl`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheRegisterCacheInfoUrl`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheRemoveTag`](../../../core/libs/prx/libSceHttpCache/Export.cpp). diff --git a/docs/dev/debt/libSceHubAppUtil.md b/docs/dev/debt/libSceHubAppUtil.md new file mode 100644 index 0000000000..e626d2eb49 --- /dev/null +++ b/docs/dev/debt/libSceHubAppUtil.md @@ -0,0 +1,5 @@ +# libSceHubAppUtil technical debt + +### Unknown function info + +- [sceHubAppUtilDeeplinkToLiveDetailView, sceHubAppUtilDeeplinkToPSSubscriptions and sceHubAppUtilGetInstallationStatus](../../../core/libs/prx/libSceHubAppUtil/Export.cpp) (libSceHubAppUtil) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHubAppUtil.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceIdu.md b/docs/dev/debt/libSceIdu.md new file mode 100644 index 0000000000..4ce48a2927 --- /dev/null +++ b/docs/dev/debt/libSceIdu.md @@ -0,0 +1,5 @@ +# libSceIdu technical debt + +### Unknown function info + +- [sceIduGetEnv, sceIduInit, sceIduInstallPkg, sceIduIsAppInstalled, sceIduIsRifInstalled, sceIduIsStaffMode, sceIduReboot and sceIduTerm](../../../core/libs/prx/libSceIdu/Export.cpp) (libSceIdu) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIdu.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 8511d88e3d..b26437726a 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -2,14 +2,25 @@ ### Silent stubs -- [sceImeOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) shows no panel: the text is typed on the host keyboard, which [ImeTextInput](../../../core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp) forwards from the game window (SDL text input, Backspace, Delete, Left, Right, Home, End, Enter and Escape), and the controller cannot type. While a panel is open, key presses do not reach the keyboard-to-pad bindings of [PadInput](../../../core/libs/prx/libSceVideoOut/src/PadInput.cpp), as the console's panel takes the controller; key releases still do. Minecraft Preview (PPSA19639, #1277) opens it when entering a world +- [sceImeOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) shows no panel: the text is typed on the host keyboard, which [ImeTextInput](../../../core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp) forwards from the game window (SDL text input, Backspace, Delete, Left, Right, Home, End, Enter and Escape), and the controller cannot type. While a panel is open, key presses do not reach the keyboard-to-pad bindings of [PadInput](../../../core/libs/prx/libSceVideoOut/src/PadInput.cpp), as the console's panel takes the controller; key releases still do. Minecraft Preview (PPSA19639, #1277) opens it when entering a world. The external keyboard filter is accepted and never called because no keycode events are produced ### Unknown function info -- [sceImeGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills. While a panel is open it reports the type, position and alignments passed to `sceImeOpen` and the size `sceImeGetPanelSize` gives; the console's values for a panel moved by its own position adjustment are unknown. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title -- [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. A text filter or an external keyboard filter throws, as their contract is unknown. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn +- [sceImeVshOpen](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - VSH context, UTF-16 text and dictionary argument registers are recovered. The VSH parameter is copied as 256 bytes; preedit metadata is eight bytes. Dictionary operations transfer 224-byte records, with a fixed 11200-byte output for GetWords; the 64-bit dictionary selector and record fields still need receiver-side identification. These payload types remain incomplete. UpdateTextInfo and RemoteEventParam use the existing 64-byte ImeEditText and event payload at offset eight; their byte-sized repair flag permits correcting invalid text ranges and caret positions. + +- [sceImeGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills. While a panel is open it reports the type, position and alignments passed to `sceImeOpen` and the size `sceImeGetPanelSize` gives; the console's values for a panel moved by its own position adjustment are unknown. +- [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 and is differential-tested against that function in an emulator, not measured on a console. [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp) holds the two codepage checks (helpers at 0x4850 and 0x48f0), rebuilt from Unicode's public [CP932](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT) and [CP936](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT) tables plus the Latin-1, CJK Extension A and a few other units the firmware also accepts (listed in the PR). The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` derive it when it is 0. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown +- [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn +- [sceImeForTestFunction](../../../core/libs/prx/libSceIme/Export.cpp) returns a constant error without consuming arguments in the supplied module; unused argument types cannot be recovered from that body. +- [sceImeConfigSet and sceImeConfigGet](../../../core/libs/prx/libSceIme/Export.cpp) preserve the native argument slots: an unused first slot, a 64-bit configuration value, a 32-bit key and, for set, a 32-bit value. The service receives a 24-byte request; configuration/key semantics and unused argument types remain unresolved. The getter advertises one output transfer but does not initialize its descriptor in this module. Protected bodies remain unchanged. + +- [libSceIme pending exports](../../../core/libs/prx/libSceIme/Export.cpp) - 2 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIme.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional +- [sceImeKeyboardOpen, sceImeKeyboardClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - opening a keyboard that is already open for the same user returns `0x80bc0001` (busy) and closing one that is not open returns `0x80bc0002`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/1211958b4eb38256cc0c5c7ba92d668820c05389/src/libs/ime.cpp) does; it tracks one keyboard for all users, while the open state is kept per user id here, and the PS5 result is unverified - [sceImeKeyboardGetInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [sceImeKeyboardGetResourceId](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard +- [sceImeOpen text filter](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) runs on the text edits of host input - typing, backspace, forward delete and a multiline Enter - following [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/1211958b4eb38256cc0c5c7ba92d668820c05389/src/libs/ime.cpp), not confirmed on a PS5. The host input thread only queues the edit under the IME mutex; `sceImeUpdate` applies it on the title's thread, where the filter also runs with the mutex released - like the event handler, so a filter that calls back into `sceIme*` cannot deadlock - and the structural edit and the filtered commit are serialized under the mutex, with a commit after the session closed or reopened dropped by a session id. The filter is guest code and `ImeTextFilter` and `ImeExtKeyboardFilter` are declared `APS5_VABI` like `EventHandler`. The filter is called with the edited text, a buffer of `maxTextLength` UTF-16 units and its length; a nonzero return, an output longer than the capacity, an unpaired surrogate or, without the multiline option, a line break keeps the edit unfiltered. An accepted output replaces the text, the caret stays where the edit put it and is clamped to the text and normalized past a surrogate pair, and one UpdateText is sent with the minimal edit delta of the whole old and new text, as KytyPS5 computes it; an output equal to the previous text sends no event. Caret moves, PressEnter without multiline, PressClose, `sceImeSetText` and `sceImeSetCaret` do not run the filter, as in KytyPS5; a `sceImeSetText` or `sceImeSetCaret` that lands while the filter runs bumps a revision beside the session id, and the filtered commit is dropped when either changed, as KytyPS5 drops the commit when its revision changed, so the title's own text or caret setting wins, the edit is discarded and sends no event. An external keyboard filter is accepted and stored on the session: it is called only for keyboard events that carry key data, which the host input path does not produce - it delivers text and actions, as KytyPS5's host API queues external inputs with a zeroed key and skips the filter for them - so it is never called + +- [sceImeVshNotifyCurrentTextInfo and sceImeVshNotifyCurrentTextInfoForReconvertText](../../../core/libs/prx/libSceIme/Export.cpp): assembly confirms seven and eight integer/pointer arguments, including the stack arguments, with no incoming SIMD parameter. Both take a context, UTF-16 string, signed 32-bit caret index and two pairs of 32-bit preedit/selection values. Pair endpoint-versus-length semantics and the reconversion option remain unresolved. Protected bodies are unchanged. diff --git a/docs/dev/debt/libSceImeDialog.md b/docs/dev/debt/libSceImeDialog.md new file mode 100644 index 0000000000..84726f50bc --- /dev/null +++ b/docs/dev/debt/libSceImeDialog.md @@ -0,0 +1,14 @@ +# libSceImeDialog technical debt + +### Silent stubs + +- [sceImeDialogInit](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) shows no dialog: the text is typed on the host keyboard, which [ImeTextInput](../../../core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp) forwards from the game window through libSceIme while no IME panel is open. Text, Backspace, Delete, Left, Right, Home and End edit the text, Enter accepts it (end status 0) and Escape cancels (end status 1). The controller cannot type, the title, placeholder, type and options are not shown or applied, and line breaks cannot be typed. While the dialog runs, key presses do not reach the keyboard-to-pad bindings of [PadInput](../../../core/libs/prx/libSceVideoOut/src/PadInput.cpp) + +### Unknown function info + +- [sceImeDialogForTestFunction](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - declared without parameters to resolve a known import, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/ime_dialog.h#L34-L51); not measured on a console and not an ABI claim. +- [sceImeDialogGetCurrentStarState, sceImeDialogInitInternal, sceImeDialogSetPanelPosition](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - not measured on a console; signatures and checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/ime/ime_dialog.cpp). `sceImeDialogInitInternal` runs `sceImeDialogInit`, as shadPS4's two share one path apart from user flags this library does not keep. There is no movable panel, so `sceImeDialogSetPanelPosition` never succeeds: `DIALOG_NOT_IN_USE` without a dialog and `IME_SUSPENDING` while one is running or finished, the states in which shadPS4 refuses it too. The star state is always 0 +- [sceImeDialogForceClose](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - not measured on a PS5: its `int (void)` signature and behaviour follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/ime_dialog.cpp#L571-L601) (PS4). It closes a running or finished dialog, as `sceImeDialogTerm` does, without writing the typed text to the title's buffer, which shadPS4 updates while typing and this library only on Enter, and returns `0x80bc0107` with no dialog open, the `NotInUse` code of [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Dialog/ImeDialog.cs#L224-L225), while this library's `sceImeDialogTerm` returns `0x80bc0002` in the same state. Closes when: measured on a PS5 +- [sceImeDialogInit, sceImeDialogGetStatus, sceImeDialogGetResult, sceImeDialogAbort, sceImeDialogTerm](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - the statuses (none 0, running 1, finished 2), the end statuses (ok 0, user canceled 1, aborted 2) and the errors of `sceImeDialogInit` for an open dialog (`0x80bc0001`), a null parameter (`0x80bc0031`), a maximum text length outside 1 to 2048 (`0x80bc0016`) and a null text buffer (`0x80bc0017`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b2d7ec7d31f9ad9426db93afe079db8679b3e1e0/src/core/libraries/ime/ime_dialog.cpp); none is measured on a PS5. A result, abort or term on a dialog that is not finished, running or open returns `0x80bc0002`, where shadPS4 returns `0x80bc0105` to `0x80bc0107`, and `sceImeDialogTerm` also closes a running dialog, which shadPS4 refuses. The text reaches the title's buffer only when Enter accepts it + +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceImeDialogInitInternalWithSubInfo`](../../../core/libs/prx/libSceImeDialog/Export.cpp). diff --git a/docs/dev/debt/libSceIpmi.md b/docs/dev/debt/libSceIpmi.md new file mode 100644 index 0000000000..09625b0ec9 --- /dev/null +++ b/docs/dev/debt/libSceIpmi.md @@ -0,0 +1,5 @@ +# libSceIpmi technical debt + +### Unknown function info + +- [libSceIpmi pending exports](../../../core/libs/prx/libSceIpmi/Export.cpp) - 5 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIpmi.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceJemspace.md b/docs/dev/debt/libSceJemspace.md new file mode 100644 index 0000000000..c0baf800a1 --- /dev/null +++ b/docs/dev/debt/libSceJemspace.md @@ -0,0 +1,5 @@ +# libSceJemspace technical debt + +### Unknown function info + +- The thirteen exports in [Export.cpp](../../../core/libs/prx/libSceJemspace/Export.cpp) resolve imports only and throw through `NotImplemented_nid_no_patch`. Each declared name hashes to a defined function NID in the `libSceJemspace` ELF export table. The module firmware version is unverified. The no-parameter declarations do not establish argument layouts or return types. No allocator behavior, memory ownership, allocation failure handling, statistics layout, or console compatibility is implemented or verified. 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/libSceJpegDec.md b/docs/dev/debt/libSceJpegDec.md new file mode 100644 index 0000000000..174718fdd4 --- /dev/null +++ b/docs/dev/debt/libSceJpegDec.md @@ -0,0 +1,10 @@ +# libSceJpegDec technical debt + +### Unknown function info + +- [sceJpegDecDecodeWithInputControl](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJpegDec.c), but no credible prototype was found. It is declared without parameters solely to resolve known imports; this is not an ABI claim + +### Functional + +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp): work-memory size, alignment, creation checks, image-info fields, progressive coefficient-buffer capacity, row-buffer capacity and scaled dimensions are recovered from the native module. The host still allocates its decoder storage separately rather than using the guest work and coefficient regions. Malformed-header diagnostics and output mutation on failure are not fully matched. +- [sceJpegDecDecode](../../../core/libs/prx/libSceJpegDec/Export.cpp): decode mode 1 and pixel formats 9, 10 and 11 remain unimplemented and throw. Exact decoded pixel values and color conversion are not verified against the native decoder. diff --git a/docs/dev/debt/libSceJpegEnc.md b/docs/dev/debt/libSceJpegEnc.md index 2bff53d167..d64816c332 100644 --- a/docs/dev/debt/libSceJpegEnc.md +++ b/docs/dev/debt/libSceJpegEnc.md @@ -1,5 +1,5 @@ # libSceJpegEnc technical debt -### Unknown function info +### Unimplemented functionality -- [sceJpegEncEncode](../../../core/libs/prx/libSceJpegEnc/Export.cpp) (libSceJpegEnc) - `restart_interval` follows SharpProspero's field doc (0 none, -1 every row of blocks, positive a block count); a positive value is taken as the JPEG DRI interval in MCUs and -1 as one restart per MCU row, which no reference confirms. Other negative values throw +- [sceJpegEncEncode](../../../core/libs/prx/libSceJpegEnc/Export.cpp) (libSceJpegEnc) - MJPEG mode throws; its encoding path is not implemented. Restart intervals are confirmed by the native validator, MCU counter and DRI writer: 0 disables restart markers, -1 selects one MCU row, and 1 through 65535 specify an MCU count. Other values return the invalid-parameter error. diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 61011254e4..9c411149b3 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -2,11 +2,13 @@ ### Unknown function info -- [sce::Json::Parser::parse with a read callback](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the callback's contract is taken from PPSA04540's: it returns 0 with the next character, and at the end of its data writes `'\0'` and returns -1. Characters are read until the callback returns non-zero or gives `'\0'`, and the text read is then parsed as by the `(text, size)` overload, so trailing characters after the value are rejected as there. Whether the console stops reading at the end of the top-level value, and what it returns when the callback fails before the end, are unknown -- [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown -- [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layouts inferred, not known. `InitParameter2` is taken as the 40-byte object of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/1811cef94435db7655209fb39f65ddf258b93fd6/src/SharpEmu.Libs/Json/JsonExports.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/eda6ba37ece127ab0396378404d99182e3e8a530/src/libs/libJson2.cpp): the allocator, its user data, the file buffer size, the 32-bit special float format type at 0x18 and 12 bytes both zero, and both constructors zero all of it. `InitParameterRtti2::setAllocatorRtti` writes the allocation parameters and user data where `setAllocator` writes the allocator, as both references do; whether the console also marks the object as using RTTI allocation is unknown. `InitParameterRtti`'s constructor writes the allocation parameters, user data and file buffer size to the first 24 bytes, an order inferred from its mangled arguments. The values of `SpecialFloatFormatType` and what they select are unknown, so `setSpecialFloatFormatType` stores any value and nothing reads it: `Initializer::initialize(InitParameter2)` ignores every field as `initialize(InitParameter)` does, serializing a non-finite real throws whatever the type, and whether the type lets the parser accept NaN or infinity is unknown. The setters return nothing; whether the console's return `this` is unknown - -### Functional - -- PPSA30601 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [six sce::Json::Object::Pair, sce::Json::Object and sce::Json::Array::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) and [sceMouseResetAbsolutePosition](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) -- [sce::Json::InitParameter2 base-object constructor](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - forwards to the complete-object constructor and uses its inferred layout and default field values; constructor equivalence and initialization bytes remain unverified on the console +- [JSON real-number conversion](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) uses locale-independent `to_chars` with 17 significant digits and `from_chars`, following JSON's decimal-point grammar in RFC 8259 section 6. Out-of-range input falls back to a classic-locale stream so underflow retains the prior signed-zero behavior and overflow throws. Console rounding and formatting are unverified. +- [sce::Json::Array::pop_front](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - removes and destroys the first element; on an empty array it throws `std::out_of_range`, as `front` and `back` do here. The console's result for an empty array is unverified. +- [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a null text pointer with nonzero size throws instead of being dereferenced. A number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown. Syntax validation for number tokens and destination state after a failed parse still differ from the system parser +- [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - RTTI allocator adaptation and the `InitParameterRtti` layout remain unverified. `setAllocatorRtti` stores the raw allocation-parameter pointer as the allocator; `initialize` ignores allocator, buffer-size and special-float settings. Special-float enum meanings and parser support for non-finite numbers remain unresolved; serialization of non-finite values throws. Setter return types remain unverified. +- [sce::Json::Object::Pair, sce::Json::Array::iterator and sce::Json::Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names, with `Array::iterator::operator++(int)` and `Object::iterator::operator++(int)` returning their copy through a result pointer before `this` as `begin` does, since the iterators have non-trivial destructors. `Pair(const String&, const Value&)` and default `Pair()` deep-copy or initialize into the layout `Object` iteration already hands out (key at 0, value at 0x10) and zeroes the 8 bytes between them; what the console keeps there is unknown. +- [sce::Json::Value::setTypeMismatchHandler, setElementAccessFailureHandler and setSpecialFloatHandler](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - members of `Value` that return an int, inferred from a caller that registers them on its root `Value` before parsing into it, with the handler and its context, and accepts 0 or 0x80848104 as success; they return 0. The PS Vita library's `Value::setNullAccessCallback` is such a member too ([vitasdk](https://github.com/vitasdk/vita-headers/blob/15605617cbbb23d266f88af28899fdb573e2355e/include/psp2/json.h#L638)). The handler signatures follow the mangled names. A handler applies to the `Value` it is set on and to the values that `Value` holds then, and `Parser::parse` gives the values it creates the handlers of the `Value` it parses into, since the title reads the errors its handlers record after accessing parsed members; copies, `push_back` and `Object::operator[]` do not carry them, and how the console finds the handler of an accessed value is unknown. A typed getter on a non-null value of another type calls the type mismatch handler with the requested type, the value, null and the context, and returns the value the handler returns if it has the requested type, else the getter's default; passing null as the third handler argument (the second `Value` pointer), and the order of the two `Value` pointers, remain unverified against the console. The element access failure handler is not called: a failed `operator[]` or `getValue` on a value carrying it throws, as the error code the console passes, which the title records as its error, is unknown; without one the access still reaches the null-access callback. The special float handler is not called: serializing a non-finite real throws with or without it, as the `FunctionType` values and the `bool*` output are unknown, and the title's handler returns a different replacement for each of its two types +- [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the base class's virtual does nothing. A title's allocator that does not override it calls it through its own vtable; the library never calls the title's allocator, so it never reports an allocation failure. Whether the console's base implementation logs anything is unknown +- [sce::Json::String size, capacity, resize, append(const char*, size_t), operator+=](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - storage uses `std::string`; capacity and allocation growth do not match the system 32-bit capacity and allocator settings. Appending null text with a nonzero length throws instead of dereferencing it. +- [sce::Json::Array::iterator::advance and Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the return type of `advance` remains unverified. Object iterator advancement past the end throws instead of asserting. Dereferencing an empty array iterator throws instead of returning the system null-value pointer or an invalid address. +- [sce::Json::Value::operator bool](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - callback dispatch before conversion is not implemented. The system library invokes the applicable type mismatch handler before converting a non-boolean value, with SDK-dependent global callback handling. diff --git a/docs/dev/debt/libSceJxr.md b/docs/dev/debt/libSceJxr.md new file mode 100644 index 0000000000..0fbc29eb6c --- /dev/null +++ b/docs/dev/debt/libSceJxr.md @@ -0,0 +1,9 @@ +# libSceJxr technical debt + +### Unknown function info + +- [libSceJxr pending exports](../../../core/libs/prx/libSceJxr/Export.cpp) - 4 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJxr.c) and were cross-checked against the same module in PS5 firmware 13.60. `PixelFormatLookup`, `readIS` and `writeIS` have unverified signatures and behaviour; their declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. + +### Functional + +- [PKFree](../../../core/libs/prx/libSceJxr/Export.cpp) (libSceJxr) follows Microsoft's jxrlib [JXRGlue.c](https://github.com/4creators/jxrlib/blob/f7521879862b9085318e814c6157490dd9dbbdb4/jxrgluelib/JXRGlue.c#L53-L62), whose four exported names the module shares: a non-null `ppv` has `*ppv` released with the guest `free` and set to null, and the result is always `WMP_errSuccess` (0) as a 64-bit `ERR` (`long`). That the console's module is built from jxrlib, and that its `PKFree` releases through libc's `free`, is inferred from the export names and not checked against the console's code. Closes when: the console's `PKFree` is disassembled or measured on a PS5 diff --git a/docs/dev/debt/libSceKeyboard.md b/docs/dev/debt/libSceKeyboard.md index 9153b2c529..60edc36b24 100644 --- a/docs/dev/debt/libSceKeyboard.md +++ b/docs/dev/debt/libSceKeyboard.md @@ -2,4 +2,11 @@ ### Functional -- [sceKeyboardGetKey2Char](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) (libSceKeyboard) translates the 101-key (US) arrangement and the alphanumeric layer of the 106-key (Japanese) one, which follows the JIS layout of the HID usages: the Yen (0x89) and Ro (0x87) keys return `\` (0x5C) unshifted, as JIS X 0201 places the yen sign there; whether the console returns U+00A5 instead is unverified. With the Kana LED (bit 4, the HID position) set the 106-key arrangement throws, as the kana the console returns are unknown. Ctrl and Alt do not change the character. +- [sceKeyboardGetKey2Char](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp): the original validates HID usages 4 through 231 before the handle and preserves unconverted HID usages in the output with `processed == 0`. The host instead accepts out-of-range usages and writes zero for unconverted keys. The original ignores the Kana LED, while the host throws for it. This partially implemented function contains `NotImplemented_nid_no_patch(__func__)`; its body is preserved under the requested restriction. + +### Unknown function info + +- [sceDbgKeyboardClose, sceDbgKeyboardGetKey2Char, sceDbgKeyboardInit, sceDbgKeyboardOpen, sceDbgKeyboardRead, sceDbgKeyboardReadState](../../../core/libs/prx/libSceKeyboard/Export.cpp) (libSceKeyboard) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceKeyboard.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. +- [sceKeyboardGetDeviceInfo and sceKeyboardSetProcessFocus](../../../core/libs/prx/libSceKeyboard/Export.cpp): pointer arguments are passed to device ioctls. Device-info storage is 168 bytes in the connection-query caller, but its fields and the process-focus layout remain unresolved. +- [sceKeyboardConnectPort, sceKeyboardDisconnectPort and sceKeyboardDeviceOpen](../../../core/libs/prx/libSceKeyboard/Export.cpp): argument widths are confirmed. The meaning/order of the two 32-bit port arguments and the final 32-bit device-open argument still require the device-service backend. +- [sceKeyboardPadEmulateClose and sceKeyboardPadEmulateOpen](../../../core/libs/prx/libSceKeyboard/Export.cpp): the supplied keyboard library has neither export; their signatures remain unresolved. diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index 7cc67d0d95..ff8c271f1f 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -10,8 +10,10 @@ - [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) follows FreeBSD 11's `lib/libc/amd64/gen/makecontext.c`: at most six arguments, a stack of at least `MINSIGSTKSZ` (2048) bytes, and `exit(0)` when the function returns with no `uc_link`. The console's version is not verified - [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) on Windows pins its stack writable until a later `makecontext` stack overlaps it, since nothing tells the library when the guest frees the stack - [Thread exit destructors](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped -- [fdopendir](../../../core/libs/prx/libSceLibcInternal/Directory.cpp) (libSceLibcInternal) - the directory stream starts at the directory's first entry, while FreeBSD's [`fdopendir`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/opendir.c#L67-L74) reads from the descriptor's current offset, and `FD_CLOEXEC` is not set on the descriptor. As in FreeBSD, a descriptor that is not open fails with `EBADF`, one that is not a directory with `ENOTDIR` (both leave it open), and `closedir` closes it +- [fdopendir](../../../core/libs/prx/libSceLibcInternal/Directory.cpp) (libSceLibcInternal) follows FreeBSD 11's [`fdopendir`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/opendir.c#L67-L74): it sets `FD_CLOEXEC` on the descriptor first, and the stream starts at the descriptor's current position instead of the first entry. On Windows the stream skips the entries the descriptor's cursor (`lseek` or `getdirentries`) has passed, which assumes that the descriptor and the stream list a directory in the same host order; on Linux the stream is opened on a duplicate of the descriptor, which shares its position. The Linux path is not run by the tests here. As in FreeBSD, a descriptor that is not open fails with `EBADF`, one that is not a directory with `ENOTDIR` (both leave it open, and `FD_CLOEXEC` stays set on the latter), and `closedir` closes it - [fstatvfs](../../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) -- [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) 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. +- [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) create no process. FreeBSD 11.4 returns the error the child's exec leaves ([`_posix_spawn_thr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/posix_spawn.c#L227-L231), [`do_posix_spawn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/posix_spawn.c#L295-L303)), and `posix_spawnp` searches `PATH`, or `_PATH_DEFPATH` when it is unset, ending with `ENOENT` when no directory holds the program ([`_execvpe`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/exec.c#L272-L281), [`execvPe`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/exec.c#L257-L260)). So when the program is not in the guest file system both return `ENOENT` and leave the process ID unwritten; when it is there they throw. PPSA99305 (Dusklight) reaches this when SDL looks for `zenity` to show a file dialog. The attributes and file actions are not applied, so one that would fail in the child is not reported, and whether the console's application sandbox lets the child be created at all (FreeBSD returns that error first) is unverified. The attribute and file action calls keep FreeBSD 11.4's objects. Null or uninitialized objects, which FreeBSD dereferences, throw. - [getopt, getopt_long](../../../core/libs/prx/libSceLibcInternal/Getopt.cpp) (libSceLibcInternal) follow FreeBSD 11 [getopt.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt.c) and [getopt_long.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt_long.c) with `GNU_COMPATIBLE`: `getopt` does not permute, `getopt_long` permutes unless `POSIXLY_CORRECT` is set in the guest environment or the option string starts with `+`, and the exported `opterr`, `optind`, `optopt`, `optreset` and `optarg` start at 1, 1, 0, 0 and null. Warnings go to the guest's current `stderr` prefixed with `__progname`, where FreeBSD's `warnx` keeps the `stderr` of its first call. A null `getopt` option string, a null argument vector or argument, a negative `optind` and an `optind` moved inside a pending permutation throw where FreeBSD faults. `getopt_long_only` is not exported. That the console's `libSceLibcInternal` uses this FreeBSD code is unverified +- [posix_spawnattr_getflags, getpgroup, getschedparam, getschedpolicy, getsigdefault, getsigmask, setpgroup, setschedparam, setschedpolicy and setsigmask](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) read and write the 0x30-byte attribute object at the offsets of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8D860 to 0x8D910: flags at 0, process group at 4, scheduling parameter at 8, policy at 0xC, default and masked signal sets at 0x10 and 0x20), which never fail there and return 0; the layout matched on a PS5 (firmware 13.60). Uninitialized attributes and null pointers, which the console dereferences, throw as the existing setters do - [sys_siglist and sys_nsig](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) hold the 32 descriptions and the count of [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/siglist.c#L73-L107) `siglist.c` (`NSIG` 32, signal 0 to `SIGUSR2`); the console's table was not read +- [inet_aton, inet_addr, inet_ntoa and inet_ntoa_r](../../../core/libs/prx/libSceLibcInternal/Inet.cpp) (libSceLibcInternal) follow the FreeBSD code in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8ADE0 to 0x8B3D0) and matched it on a PS5 (firmware 13.60) for 20 `inet_aton` inputs; a null text, which the console dereferences, throws. `inet_ntoa_r` formats through libkernel's `__inet_ntop`, so a null buffer with room for the text throws and a null buffer that is too small sets `ENOSPC`, as `__inet_ntop` does for a null destination; the console's `inet_ntoa_r` was not measured with a null buffer diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md new file mode 100644 index 0000000000..bcaab509a1 --- /dev/null +++ b/docs/dev/debt/libSceLibreSsl.md @@ -0,0 +1,6 @@ +# libSceLibreSsl technical debt + +### Unknown function info + +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 377 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); the module's LibreSSL version is unknown and nothing is checked on the console. Behaviour that differs between LibreSSL versions throws: `SHA256` with a null digest buffer (older versions return a static buffer) and `SHA512_Final` with the SHA-512/224 or SHA-512/256 digest length. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw diff --git a/docs/dev/debt/libSceLibreSsl3.md b/docs/dev/debt/libSceLibreSsl3.md new file mode 100644 index 0000000000..dba672f91f --- /dev/null +++ b/docs/dev/debt/libSceLibreSsl3.md @@ -0,0 +1,10 @@ +# libSceLibreSsl3 technical debt + +### Unknown function info + +- [libSceLibreSsl3 pending exports](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - 84 named callable exports, excluding `CRYPTO_memcmp`, `MD5` and `SHA1`, remain unimplemented from the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl3.c), cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. + +### Functional + +- [CRYPTO_memcmp](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) follows the public [LibreSSL signature and equality contract](https://man.openbsd.org/CRYPTO_memcmp.3), without PS5 verification. The exact nonzero result for unequal buffers is unspecified by that contract. Content-independent execution of its volatile byte reads still depends on the compiler; inspect optimized code when changing toolchains. Closes when: measured on a PS5 and the supported toolchains' generated code is checked. +- [MD5 and SHA1](../../../core/libs/prx/libSceLibreSsl3/src/Digest.cpp) (libSceLibreSsl3) - follow LibreSSL's [md5.c, sha1.c](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto) and hash through libSceLibreSsl's `MD5_*` and `SHA1_*`. A null digest buffer throws: LibreSSL up to 3.8 returns a static buffer, later versions do not check it. Nothing is checked on the console diff --git a/docs/dev/debt/libSceLoginDialog.md b/docs/dev/debt/libSceLoginDialog.md index 5e631eb2be..c9408a3d58 100644 --- a/docs/dev/debt/libSceLoginDialog.md +++ b/docs/dev/debt/libSceLoginDialog.md @@ -3,3 +3,8 @@ ### Silent stubs - [libSceLoginDialog](../../../core/libs/prx/libSceLoginDialog/Export.cpp) - the login dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled +- [sceLoginDialogClose](../../../core/libs/prx/libSceLoginDialog/Export.cpp) - sceLoginDialogClose transitions an initialized dialog directly to COMMON_DIALOG_STATUS_FINISHED even when called before Open, following the Signin / NetCtlAp / PlayerSelection convention; this behavior is unverified against real hardware. + +### Unknown function info + +- [sceLoginDialogOpen, sceLoginDialogGetResult](../../../core/libs/prx/libSceLoginDialog/Export.cpp) - the 64-byte input and 16-byte result layouts are recovered. The roles of the nine user-ID slots and the eight trailing result bytes require service-side analysis. The UI stub returns the native disconnected-service cancellation result and does not populate successful login data. diff --git a/docs/dev/debt/libSceLoginMgrServer.md b/docs/dev/debt/libSceLoginMgrServer.md new file mode 100644 index 0000000000..9c6b1255ce --- /dev/null +++ b/docs/dev/debt/libSceLoginMgrServer.md @@ -0,0 +1,5 @@ +# libSceLoginMgrServer technical debt + +### Unknown function info + +- [sceLoginMgrServerCheckSafetyNoticeShown, sceLoginMgrServerClearMorpheusRequiredUserId, sceLoginMgrServerConvertToUserId, sceLoginMgrServerCreateUser, sceLoginMgrServerDestroyUser, sceLoginMgrServerDialogGetAppId, sceLoginMgrServerDialogGetOpenParam, sceLoginMgrServerDialogNotifyCloseFinished, sceLoginMgrServerDialogNotifyOpenFinished, sceLoginMgrServerDialogSetResult, sceLoginMgrServerGetLogoutInfo, sceLoginMgrServerGetMorpheusRequiredUserId, sceLoginMgrServerGetSharePlayAllowPadOperation, sceLoginMgrServerInitialize, sceLoginMgrServerInitializeSharePlayAllowPadOperation, sceLoginMgrServerIsRequestedCdlgClose, sceLoginMgrServerLoginServiceGetRequestParam, sceLoginMgrServerLoginServiceNotifyRequestFinished, sceLoginMgrServerNotifyEasySignInFinished, sceLoginMgrServerNotifyEasySignInUserCode, sceLoginMgrServerNotifyHmdConnectInfo, sceLoginMgrServerNotifySafetyNoticeShown, sceLoginMgrServerNotifyShellUIState, sceLoginMgrServerSetLoginFlag, sceLoginMgrServerSetMorpheusAssignEnabledFlag, sceLoginMgrServerSetMorpheusRequiredUserId, sceLoginMgrServerSetSharePlayMode, sceLoginMgrServerSetUserStatus, sceLoginMgrServerTerminate and sceLoginMgrServerUpdateUserIdRalatedToPadUniqueId](../../../core/libs/prx/libSceLoginMgrServer/Export.cpp) (libSceLoginMgrServer) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLoginMgrServer.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceLoginService.md b/docs/dev/debt/libSceLoginService.md new file mode 100644 index 0000000000..db043fd9c7 --- /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): the native wrapper confirms one read-only request pointer and transfers exactly 64 bytes to the login service. Null input returns 0x813a0003 before the initialization check; an uninitialized service returns 0x813a0001. The request fields are interpreted by the service backend and remain unresolved. The protected export body is unchanged. diff --git a/docs/dev/debt/libSceM4aacEnc.md b/docs/dev/debt/libSceM4aacEnc.md new file mode 100644 index 0000000000..fbb1f5292f --- /dev/null +++ b/docs/dev/debt/libSceM4aacEnc.md @@ -0,0 +1,6 @@ +# libSceM4aacEnc technical debt + +### Unknown function info + +- [libSceM4aacEnc](../../../core/libs/prx/libSceM4aacEnc/Export.cpp) (libSceM4aacEnc, sysmodule id `0x000000bc`) has no encoder, so all seventeen exports throw. PPSA03332 imports them and fails to load (`libSceM4aacEnc.prx: cannot open shared object file`) when the library is absent. The names are from the aerolib NID list: `sceM4aacEncClearContext`, `sceM4aacEncClearContextEx`, `sceM4aacEncCreateEncoder`, `sceM4aacEncCreateEncoderEx`, `sceM4aacEncDeleteEncoder`, `sceM4aacEncDeleteEncoderEx`, `sceM4aacEncEncode`, `sceM4aacEncEncodeEx`, `sceM4aacEncEncodeWithPriority`, `sceM4aacEncEncodeWithPriorityEx`, `sceM4aacEncFlush`, `sceM4aacEncFlushEx`, `sceM4aacEncFlushWithPriority`, `sceM4aacEncFlushWithPriorityEx`, `sceM4aacEncGetContext`, `sceM4aacEncInitialize` and `sceM4aacEncTerminate` +- [sceM4aacEnc*](../../../core/libs/prx/libSceM4aacEnc/Export.cpp) (libSceM4aacEnc) - five signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json): `int32_t sceM4aacEncClearContext(int32_t m4aacEncHandle)`, `int32_t sceM4aacEncCreateEncoder(int32_t channels, int32_t samplingRate, int32_t bitRate, int32_t inputFormat, int32_t outputFormat, int32_t *pInternalError)`, `int32_t sceM4aacEncDeleteEncoder(int32_t m4aacEncHandle, int32_t *pInternalError)`, `int32_t sceM4aacEncEncode(int32_t m4aacEncHandle, void *pInput, uint32_t inputByte, void *pOutput, uint32_t *pOutputByte, int32_t *pInternalError)` and `int32_t sceM4aacEncFlush(int32_t m4aacEncHandle, void *pOutput, uint32_t *pOutputByte, int32_t *pInternalError)`. Orbital has no entry for the other twelve, which are declared without parameters, so their signatures are unknown diff --git a/docs/dev/debt/libSceMbus.md b/docs/dev/debt/libSceMbus.md index e46de1ffb3..9078d4ebc6 100644 --- a/docs/dev/debt/libSceMbus.md +++ b/docs/dev/debt/libSceMbus.md @@ -2,8 +2,26 @@ ### Silent stubs -- [libSceMbus](../../../core/libs/prx/libSceMbus/Export.cpp) implements only the no-device path: initialization and termination succeed, no device events are reported and device queries return no entries +- [libSceMbus](../../../core/libs/prx/libSceMbus/Export.cpp) implements only the no-device path: initialization and termination succeed, no device events are reported, device queries return no entries and `sceDeviceServiceGetGeneration` always returns 0, as the device set never changes. The generation a console starts at is unmeasured; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Device/DeviceService.cs#L63-L65) documents `int sceDeviceServiceGetGeneration()` as a counter that advances when the device set changes ### Unknown function info -- [libSceMbus](../../../core/libs/prx/libSceMbus/Export.cpp) (`libSceDeviceService`) - signatures taken from PPSA01317, which calls `sceDeviceServiceInitialize(3, 0)`, polls `sceDeviceServiceGetEventState(1)` and queries again when bit 0 or 1 is set, and calls `sceDeviceServiceQueryDeviceInfo_(0x7001, 0, 0, infos, 1, &count, &unknown, 0x70)`, treating a negative result as an error and `count` as the number of entries. The host has no devices: the event state is 0 and a query returns 0 with `count` 0 +- [sceDeviceServiceQueryDeviceInfo_](../../../core/libs/prx/libSceMbus/Export.cpp): the 112-byte device-info layout and the third argument's meaning remain unresolved. The second argument is a six-byte HID usage filter pointer, not an integer. Initialization takes an optional 16-byte parameter structure with an optional thread configuration; custom scheduling throws because the host has no event worker. `sceDeviceServiceGetEventState` takes a clear flag, not a device type. + +- [sceMbus remaining declarations](../../../core/libs/prx/libSceMbus/PendingExports.cpp): SetDeviceFunctionState takes a 64-bit device identifier and a 180-byte aggregate by value on the stack. Its service-side fields and source alignment remain unresolved; the declaration preserves the observed stack ABI with opaque storage. AcquireControlList and Term return constants without reading incoming arguments, so their original parameter lists cannot be established from the callees. All protected stub bodies remain unchanged. + +- [sceMbusAddHandleByDeviceId](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the six-argument ABI is confirmed, including the 64-bit device ID. The meanings of the last three 32-bit fields require the device-service backend. + +- [sceMbus control and handle registration](../../../core/libs/prx/libSceMbus/PendingExports.cpp): argument positions and transmitted widths are recovered for control acquisition, handle registration, process attachment, user binding and device-ID conversion. Several scalar field meanings still depend on the service protocol; the 64-bit control value and unnamed registration fields must not be interpreted as pointers. AcquireControlList returns a constant error without reading its incoming arguments, so its source signature remains unresolved. + +- [sceMbus debug process and device queries](../../../core/libs/prx/libSceMbus/PendingExports.cpp): process information is transmitted as 16 bytes, startup input/output as eight bytes, control status as 24 bytes, internal information as 2832 bytes and priority information as 3088 bytes. Device enumeration writes a 32-bit count followed by 396-byte entries. These pointee fields and the semantics of unnamed process flags remain unresolved; declarations use distinct incomplete types instead of guessing their fields. + +- [sceMbus event subscriptions](../../../core/libs/prx/libSceMbus/PendingExports.cpp): the subscription transports two 32-bit and two 64-bit fields whose service-side meanings remain unresolved. EventCreate uses 80-byte queue records and copies a 16-byte callback table; event fields still require the dispatch path; the overflow callback takes an event handle and user data, and the event callback additionally takes a pointer to the queued event. EventCallbackFuncsInit only clears memory and has no defined return value; its protected stub retains the existing integer return. Eight dump/BGM wrappers have confirmed zero-argument ABIs. EventBusStatusChangeUnsubscribe ignores its first incoming argument and consumes a 32-bit second argument; its first parameter type remains unresolved. + +- [sceMbus device and driver payloads](../../../core/libs/prx/libSceMbus/PendingExports.cpp): device-info records occupy 256 bytes, descriptions 264 bytes, simulated-handle status and Spark state 16 bytes each. Their service-side fields remain unresolved. Driver registration copies four callback pointers; driver-event notification transports two 64-bit values without dereferencing them. Init, IsBgmProhibited, ReenableBgmForShellUi and RemoveCameraAppModuleFocus consume no incoming arguments. + +- [sceMbus settings](../../../core/libs/prx/libSceMbus/PendingExports.cpp): USB power-list entries occupy 12 bytes, with an 8-bit count limited to 100; their fields remain unresolved. Several routing, scratch-data and proxy scalar meanings still require the service handler. Start/StopAudioOutAuxBroadcast and StopAudioOutSharePlay have confirmed zero-argument ABIs. + +- [sceMbusGetDeviceInfoByCondition_](../../../core/libs/prx/libSceMbus/PendingExports.cpp): its first parameter points to a 256-byte query condition; the second is a 32-bit scalar. Two optional 32-bit outputs and the sized 256-byte record array are confirmed, while condition fields and output meanings remain unresolved. + +- [sceMbusGetDeviceInfoByConditionForDeviceService](../../../core/libs/prx/libSceMbus/PendingExports.cpp): assembly and its DeviceService caller confirm five register arguments followed by a 256-byte condition passed by value on the stack. The condition keeps opaque storage because its service-side fields remain unresolved; byte 228 contains the 16-bit device class. Output records occupy 112 bytes. The two optional outputs are 32-bit values whose meanings remain unresolved. diff --git a/docs/dev/debt/libSceMouse.md b/docs/dev/debt/libSceMouse.md new file mode 100644 index 0000000000..afca5f0db7 --- /dev/null +++ b/docs/dev/debt/libSceMouse.md @@ -0,0 +1,9 @@ +# libSceMouse technical debt + +### Unknown function info + +- [sceMouseConnectPort, sceMouseDebugGetDeviceId, sceMouseDeviceOpen, sceMouseDisconnectDevice, sceMouseDisconnectPort, sceMouseGetDeviceInfo, sceMouseMbusInit, sceMouseSetHandType, sceMouseSetPointerSpeed and sceMouseSetProcessPrivilege](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMouse.c); the no-argument prototypes follow [shadPS4's PS4 stubs](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/mouse/mouse.cpp#L34-L146), not measured on a console. These declarations have no parameters solely to resolve known imports and make no ABI claim. + +### Functional + +- [sceMouseResetAbsolutePosition](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - the position is not stored because the host reports relative motion only. Native input widths and validation are confirmed, including the default 1920 by 1080 bounds; other native coordinate modes and absolute reports remain unimplemented diff --git a/docs/dev/debt/libSceMove.md b/docs/dev/debt/libSceMove.md new file mode 100644 index 0000000000..bc12dfcafb --- /dev/null +++ b/docs/dev/debt/libSceMove.md @@ -0,0 +1,7 @@ +# libSceMove technical debt + +### Functional + +- [libSceMove](../../../core/libs/prx/libSceMove/Export.cpp): the host implements only disconnected controllers. The native module confirms initialization errors, disconnected return 1 without output writes, invalid-handle error 0x80EE0004, and initialization then argument then handle validation for queries. Reset-light-sphere still omits the device ioctl and its failure mapping. +- [sceMoveOpen and sceMoveClose](../../../core/libs/prx/libSceMove/Export.cpp): protected bodies remain unchanged. Native opening resolves a user/device through the input service, rejects duplicate device handles with 0x80EE0007, limits storage to eight slots with 0x80EE0005, and obtains handles from the device ioctl. The host generates monotonically increasing handles, accepts arbitrary type/index values and returns 0x80EE0007 for duplicate slots and throws for other users. Closing an unknown handle returns the confirmed 0x80EE0004. +- [sceMoveReadStateRecent](../../../core/libs/prx/libSceMove/Export.cpp): the native module permits a null data array for SDK versions through 0x03ffffff while still requiring a count pointer and nonnegative timestamp. The host requires the data array for every SDK version. Connected state records and device-service failures remain unimplemented. diff --git a/docs/dev/debt/libSceMoveTracker.md b/docs/dev/debt/libSceMoveTracker.md new file mode 100644 index 0000000000..c261f9fcd9 --- /dev/null +++ b/docs/dev/debt/libSceMoveTracker.md @@ -0,0 +1,5 @@ +# libSceMoveTracker technical debt + +### Unknown function info + +- [libSceMoveTracker](../../../core/libs/prx/libSceMoveTracker/Export.cpp) remains unimplemented. Calibration direction, camera/controller input, capture initialization, state query, memory-size query and playback output signatures are recovered. Camera and controller records occupy 24 bytes, state records 160 bytes, and playback metadata 16 bytes; their complete field meanings still require reconstruction. Initialization consumes a work-memory pointer, a required nonzero 64-bit argument and two 32-bit GPU queue selectors below eight. The second argument is only checked for zero; its declared type remains unresolved. Capture/playback helper signatures need further checking against caller register use. diff --git a/docs/dev/debt/libSceMsgDialog.native.md b/docs/dev/debt/libSceMsgDialog.native.md index 8f8116bec7..bcd6bc6be9 100644 --- a/docs/dev/debt/libSceMsgDialog.native.md +++ b/docs/dev/debt/libSceMsgDialog.native.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceMsgDialog.native](../../../core/libs/prx/libSceMsgDialog.native/Export.cpp) - dialogs finish at `sceMsgDialogOpen` without UI and answer with button 1, so `sceMsgDialogClose` returns `NOT_RUNNING`; its result before `sceMsgDialogInitialize` follows libSceMsgDialog +- [libSceMsgDialog.native](../../../core/libs/prx/libSceMsgDialog.native/Export.cpp) - dialogs finish at `sceMsgDialogOpen` without UI and answer with button 1, so `sceMsgDialogClose` returns `NOT_RUNNING`; its result before `sceMsgDialogInitialize` follows libSceMsgDialog. `sceMsgDialogTerminate` resets running or finished dialogs and returns OK, rejecting uninitialized calls with `COMMON_DIALOG_ERROR_NOT_INITIALIZED` following shadPS4; null pointer arguments return `COMMON_DIALOG_ERROR_ARG_NULL`. diff --git a/docs/dev/debt/libSceMusicFrameworkInterface.md b/docs/dev/debt/libSceMusicFrameworkInterface.md new file mode 100644 index 0000000000..1a47762ae9 --- /dev/null +++ b/docs/dev/debt/libSceMusicFrameworkInterface.md @@ -0,0 +1,5 @@ +# libSceMusicFrameworkInterface technical debt + +### Unknown function info + +- [sceMusicFwGetCurrentTime, sceMusicFwGetLoop, sceMusicFwGetMuted, sceMusicFwGetNetworkState, sceMusicFwGetVolume, sceMusicFwIsEnded, sceMusicFwIsPlaying, sceMusicFwIsStopped, sceMusicFwRemoveSrc, sceMusicFwSendMessageAsynchronously, sceMusicFwSendMessageSynchronously, sceMusicFwSetCurrentTime, sceMusicFwSetLoop, sceMusicFwSetShuffle, sceMusicFwSetVolume, sceMusicFwSkipNext and sceMusicFwSkipPrevious](../../../core/libs/prx/libSceMusicFrameworkInterface/Export.cpp) (libSceMusicFrameworkInterface) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMusicFrameworkInterface.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceNKWeb.md b/docs/dev/debt/libSceNKWeb.md new file mode 100644 index 0000000000..5b311c43ed --- /dev/null +++ b/docs/dev/debt/libSceNKWeb.md @@ -0,0 +1,5 @@ +# libSceNKWeb technical debt + +### Functional + +- [sceNKWebInitialize and sceNKWebTerminate](../../../core/libs/prx/libSceNKWeb/Export.cpp) remain throwing stubs. Both native exports take no input arguments and return a 32-bit status; decompiler SIMD parameters are unused register propagation through initialization and cleanup helpers. diff --git a/docs/dev/debt/libSceNKWebKit.md b/docs/dev/debt/libSceNKWebKit.md new file mode 100644 index 0000000000..e515ec10a7 --- /dev/null +++ b/docs/dev/debt/libSceNKWebKit.md @@ -0,0 +1,5 @@ +# libSceNKWebKit technical debt + +### Known export names; ABI prototypes unverified + +- The 150 `sceNKWebKitUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceNKWebKit/Export.cpp) are present in the PS5 3.20 `libSceNKWebKit` export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their NIDs are derived from exported mangled symbols. Prototypes and behavior are unverified; each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`. These declarations make no ABI or behavior claim. diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 86c1b99a3f..f9f697287e 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -1,17 +1,49 @@ # libSceNet technical debt +### Silent stubs + +- [sceNetShowIfconfig, sceNetShowNetstat](../../../core/libs/prx/libSceNet/Export.cpp) return the native not-initialized error and set errno before initialization. Initialized calls throw because native diagnostic collection and formatting are not implemented. +- [sceNetShowRoute, sceNetShowRoute6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - return success without printing anything: the `()` signatures and the success return follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/net.cpp), which stubs each the same way. PPSA01530 fails to load without the `sceNetShowIfconfig` export (#3177); PPSA11386 imports `sceNetShowNetstat` but does not call it during start-up. What the console prints and where is unverified + ### Unknown function info -- PPSA01530 and PPSA13356 imports declared without parameters, signatures unknown, added without an implementation because the titles fail to load without them: [sceNetShowIfconfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet), [sce::Json::Value::setNullAccessCallBack](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), and [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad), whose real name is unknown. [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured. +- [sceNetConfigAddIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - disassembly confirms the same interface/address/broadcast/netmask arguments as SetIfaddr, entering its shared handler with add mode selected. GetIfaddr copies up to two 16-byte interface-address records. GetRouteInfo uses a family, record buffer, signed capacity and byte flags; internal statistics output is 16 bytes. The address-record and internal-statistics fields remain incomplete. + +- [Resolver-connect, thread creation, interface and diagnostic queries, WLAN configuration and DHCP queries](../../../core/libs/prx/libSceNet/Export.cpp) retain unsupported implementations. Their restored argument lists still reference incomplete connection, thread, interface, route, WLAN and statistics types. Kernel ioctl payloads need reconstruction from their handlers or consumers; buffer lengths alone do not establish field types. - [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 +- [sceNetClearDnsCache](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the library has no DNS cache to clear because lookups use the host resolver. The host resolver cache is left unchanged. +- [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - strings shorter than 17 bytes return EINVAL without changing the output, rather than reading past the terminating NUL as in the [firmware 13.60 probe](https://gist.github.com/orezraey/789290577123af3c45b340d6db6f395c/71e0a6215c7ee86f49fdb31de989dd2b50720b7a). Non-ASCII digit bytes throw because their decoding is unmeasured; other firmware versions remain unverified. Fixed character pairs, ignored separators, alphabetic values beyond f, non-alphanumeric ASCII values of 0x7f, and null-pointer EINVAL follow those measurements, not the strict [shadPS4 parser](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1274-L1297) +- [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(info, int flags)` signature, the two IPv4 server slots in network byte order and the server count as the return value follow the PS4 [OpenOrbis musl resolver](https://github.com/OpenOrbis/musl/blob/2b0bb8ce2fa64ac255cb656785729d159a5f074c/src/network/resolvconf.c) and [OpenOrbis Net.h](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/Net.h); unverified on PS5 hardware. It reports the host DNS servers of the interface `sceNetCtlGetInfo` reports, never servers set through `sceNetSetDnsInfo`. Zeroing the unused slots and returning 0 without a host IPv4 interface are unverified. Nonzero flags and a null info throw because their console results are unknown +- [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - generates values with the host random device; the system library maintains its own seeded PRNG state. +- [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); IPv4 conversion and the ignored flags follow PS4 firmware 10.71, not yet confirmed on PS5. There IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at the first space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails. Flags 0, each single bit up to 0x100, 0x10000, 0x80000000 and -1 gave the same results; other values were not measured. LF, FF and CR as terminators, and `NET_EINVAL` for a null `src` or `dst`, follow `sceNetInetPton` and were not measured for this function. IPv6 converts as `sceNetInetPton` does. +- [sceNetMemoryAllocate and sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - allocations use the guest heap instead of the system network pool. Pool limits, its allocation accounting and free-time fill are not reproduced: a reused block allocated without flag 2 holds 0xdd bytes on the console and its previous contents here. Flag 4 bypasses the system pool reserve threshold; with no pool there is no threshold, so it allocates as other flags do. What the console returns once the pool is exhausted is unmeasured; AnyPS5 returns null without setting an errno. Measured on a PS5 (firmware 13.60): a size of 0 or less returns null and leaves `sceNetErrnoLoc` unchanged, flag 2 zero-fills, flags 1, 4, 8, 0x10, 0x100 and 0x80000000 had no visible effect, and every block of 1 to 65536 bytes was 64-byte aligned +- [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) is a zero-argument, non-returning void export in the native module. The protected host stub still declares int and contains `return 0`; changing it to void requires changing that protected body, so the return-type mismatch is retained. +- [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. +- [sceNetDhcpdStart](../../../core/libs/prx/libSceNet/Export.cpp): six arguments and the three-argument address callback are confirmed. The 24-byte parameter contains five network-order addresses and a client limit. Individual address roles and the two unused thread-option meanings remain unresolved. +- [sceNetPppoeStart](../../../core/libs/prx/libSceNet/Export.cpp): seven arguments are confirmed, including the parameter pointer on the stack. The 272-byte parameter contains two DNS addresses, two 128-byte credential arrays, MTU and flags. Callback arguments are user data, event and error; the two unused thread-option meanings remain unresolved. +- [sceNetConfigRoutingStart](../../../core/libs/prx/libSceNet/Export.cpp): five pointer/scalar arguments are confirmed. Input address data is eight bytes; the mutable server parameter is 20 bytes and duplicates its first address when its count is one. Optional rules contain a count and pointer to 20-byte records with protocol, two address/port pairs and flags. Layouts are defined; rule flag meanings and protocol-specific port interpretation remain unresolved. +- [sceNetConfigRoutingShowRoutingConfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. ### Functional +- [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) prints nothing for a negative count, where the console prints `[LIBNET] usleep ` to standard output before failing with `EINVAL`. What an interrupted sleep returns is unmeasured: AnyPS5 returns the result of libkernel's `sceKernelUsleep`, a cancellation point. The signed 32-bit count (the upper half of the register is ignored) and the 0 result were measured on a PS5 (firmware 13.60) +- [sceNetResolverStartAton6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reverse-resolves a 16-byte IPv6 address through the host resolver (`getnameinfo` with `NI_NAMEREQD`) into the caller's buffer of `len` bytes, as `sceNetResolverStartAton` does for IPv4. The `(rid, addr, hostname, len, timeout, retry, flags)` signature comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L992-L997). An address with no name, or a name longer than `len - 1` bytes, fails with `RESOLVER_ENODNS` (`0x804101E1`) and leaves the buffer untouched; whether the console truncates instead is not verified. Abort flag 2 (ATON preservation) fails the next call with `EINTR` as it does `sceNetResolverStartAton`. Timeout and retry are ignored, and any nonzero flags throw because their meaning is unknown +- [sceNetAccept](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) inherits the listener's non-blocking mode through the existing `SO_NBIO` setter, as the libkernel socket path does and FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L571) copies the socket state. The mode is sampled when accept starts; changes between connection creation and accept are not modelled. Not measured on a console. + - [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had -- [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. On Windows a receive or send on a stream socket that times out fails with `ETIMEDOUT`, where FreeBSD returns `EAGAIN` or the partial count: Winsock leaves the connection in an indeterminate state after a timeout and reports no partial count ([SOL_SOCKET options](https://learn.microsoft.com/en-us/windows/win32/winsock/sol-socket-socket-options)), so FreeBSD's behaviour needs the timeout waited for without the host option. A receive on a datagram socket that times out fails with `EAGAIN` +- [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. Windows sockets stay non-blocking and the library waits for readiness until the stored timeout. Blocking stream sends return the queued byte count on timeout, or `EAGAIN` when no bytes were queued. +- [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take `SO_SNDTIMEO` and `SO_RCVTIMEO` (0x1005, 0x1006) as a 16-byte `struct timeval`, as FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/uipc_socket.c#L2657-L2690) do, and Sony's 0x1105 and 0x1106 as an `int` of microseconds. A [console log of ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/issues/49) (firmware 12.20) shows receives timing out every 100 ms after it gave 0x1006 a `{0, 100000}` timeval. The errors for a value shorter than a timeval (`EINVAL`) or out of range (`EDOM`), and `sceNetGetsockopt` cutting the timeval to the caller's length, follow FreeBSD's `sooptcopyin` and `sooptcopyout` and are not measured on a console +- [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) stores `SO_ONESBCAST` (`0x10000`, [shadPS4's `ORBIS_NET_SO_ONESBCAST`](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net_types.h)) per socket and `sceNetGetsockopt` reads back the value that was set, as shadPS4 does; PPSA34501 sets it and crashes later when it fails with `EOPNOTSUPP`. Sends ignore it. FreeBSD 11.4's [ip(4)](https://man.freebsd.org/cgi/man.cgi?manpath=FreeBSD+11.4-RELEASE&query=ip&sektion=4) documents the like-named `IPPROTO_IP` option `IP_ONESBCAST`: a datagram sent to an interface's directed broadcast address, such as 192.168.2.255, leaves through that interface with its destination rewritten to 255.255.255.255, and only on a socket allowed to set `SO_BROADCAST`. Here host routing selects the outgoing interface and the destination is not rewritten. shadPS4 also leaves sends unchanged, and its [TODO](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp#L2467-L2470) reads the option the other way round: a send to 255.255.255.255 would be converted to the interface's broadcast address. Whether the console's `SOL_SOCKET` option behaves like `IP_ONESBCAST`, and what it reads back, is not verified on hardware. FreeBSD 11.4's `sogetopt()` reads several boolean socket options back as their flag bit (`so_options & optname`, `sys/kern/uipc_socket.c`), not as the value set; whether the console does so for `0x10000` is unknown - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. +- [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) set `TCP_NODELAY` (`IPPROTO_TCP`, 1) on the host socket of a stream socket and read it back as `TF_NODELAY` (4) or 0, as FreeBSD 11.4's [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1626-L1650) does. Every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_NOPUSH` and `TCP_KEEPIDLE` among them, which FreeBSD implements) fails with `ENOPROTOOPT`, and the `IPPROTO_TCP` level on a socket that is not a stream socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/ip_output.c#L943-L944) answers it. A set on a connection that FreeBSD's [`tcp_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1498-L1502) refuses with `ECONNRESET` (refused, reset or in `TIME_WAIT`) succeeds here because the host accepts it. A null value with a nonzero length fails with `EINVAL`, where FreeBSD's `kern_setsockopt` returns `EFAULT`, and a get into less than an `int` fails with `EINVAL`, where `sooptcopyout` truncates. None of this was measured on a console. +- [sceNetGetSockInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reports 0 for the name, pid, policy, priority, queue lengths, virtual ports, state, bandwidth, drops and waits, sets no status flag except `SELF` (0x1) and `NONBLOCK` (0x200000), and reports the host's buffer sizes where none was set. An IPv6 socket fails with `EAFNOSUPPORT` and is not counted or listed; shadPS4 lists it with zero addresses. `flags` bits other than 0x40 fail with `EINVAL`, where shadPS4 ignores `flags`; 0x40 adds sockets in TIME_WAIT ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs)), but closed sockets are not kept. Otherwise the `s` below 0 count and list, the return value and `EINVAL` for a null `info` or an `n` below 1 follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/net/net.cpp#L1498-L1522). Not measured on a console. +- [SO_SNDBUF and SO_RCVBUF validation](../../../core/libs/prx/libSceNet/Export.cpp) remains unverified on PS5, and positive sizes remain subject to host buffer limits. Nonpositive sizes fail with `EINVAL` before changing the native socket, following FreeBSD 11.4 [sosetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c#L2494-L2509). - [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 +- [sceNetSocketClose, sceNetEpollControl and sceNetEpollWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - close and concurrent registration ordering are unmeasured on a console. Closed descriptors retire their registrations, and a wait discards snapshots invalidated by close, DEL or MOD; descriptor identity follows [Linux epoll(7)](https://man7.org/linux/man-pages/man7/epoll.7.html). ADD, MOD and DEL reject closed or invalid socket descriptors with `EBADF`, as [Linux epoll_ctl(2)](https://man7.org/linux/man-pages/man2/epoll_ctl.2.html) does; this error ordering is unmeasured on a console. Duplicate-descriptor alias lifetime is not modeled by this epoll backend. +- [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - console ordering between competing readers and writers is unmeasured. The per-call behavior follows FreeBSD 11.4's `soreceive_generic` and `sosend_generic` (`sys/kern/uipc_socket.c`); `0x80` is FreeBSD's flag value (`sys/sys/socket.h`). On Windows native sockets stay non-blocking. `MSG_DONTWAIT` and guest `SO_NBIO` return one host transfer's result. Blocking stream sends queue the whole buffer with one deadline and return progress after a timeout or host error. SSL uses this path through the native owner, retaining the socket, its mode and timeouts. Readiness waits poll at most every 10 ms for local close, so idle blocked operations wake periodically. +- [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does - [sceNetResolverStartNtoa6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) resolves a host name to its first IPv6 address through the host resolver (`getaddrinfo` with `AF_INET6`) and writes the 16-byte address; the `(rid, hostname, addr, timeout, retry, flags)` signature follows `sceNetResolverStartNtoa`. A name with no IPv6 address fails with `RESOLVER_ENODNS` (`0x804101E1`) like a failed lookup; whether the console returns IPv4-mapped addresses instead is not verified. Timeout and retry are ignored as in `sceNetResolverStartNtoa`, and any nonzero flags throw because their meaning for this call is not known. Unverified on hardware. +- [sceNetInetPton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) ends an IPv4 address at the first space, tab, LF, VT, FF or CR and ignores the rest (`"1.2.3.4 x"` converts), leaves the destination and `sceNetErrnoLoc` unchanged when a conversion fails, as PS4 firmware 10.71 does; not yet confirmed on PS5. Leading whitespace, any other trailing byte (`0x01`, `0xa0`) and IPv6 text with whitespace fail. + +- [sceNetDhcpStart, sceNetAddrConfig6Start, sceNetDuplicateIpStart and sceNetInfoDumpStart](../../../core/libs/prx/libSceNet/Export.cpp) - argument widths and callback ABI are recovered. Address callbacks receive user data, a 32-bit event and a 32-bit error. DHCP configuration is mutable; the 512-byte IPv6 configuration is read-only. Their complete field layouts remain unknown. The two 32-bit thread parameters are stored but not consumed by this firmware thread wrapper, so their original names and units are unconfirmed. Information-dump options contain flags, an interval in seconds and a 64-byte path. All four exports remain unimplemented. diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md new file mode 100644 index 0000000000..aaa9cf2f9d --- /dev/null +++ b/docs/dev/debt/libSceNetCtl.md @@ -0,0 +1,22 @@ +# libSceNetCtl technical debt + +### Functional + +- [sceNetCtlGetNatInfo and sceNetCtlGetResult](../../../core/libs/prx/libSceNetCtl/Export.cpp) - NAT discovery and service event results are not implemented and throw after native initialization and argument checks. NAT information is 16 bytes with a 32-bit size prefix; the remaining fields are unresolved. + +### Unknown function info + +- [sceNetCtlScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - argument registers and IPC transfer directions are recovered for the info, connection, scan, NAT, bandwidth and Wi-Fi queries. The connection configuration is 1464 bytes, scan options 88, BSSID output 96, scan info 112, IPv6 info 256, internal NAT info 196, bandwidth info 32, bandwidth-estimation info 28 and Wi-Fi info 48. Their field semantics still require the receiving service; incomplete declarations preserve this gap. Scan queries transfer a six-byte BSSID and use a 64-bit in/out buffer size. + +- [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The DHCP host name (12), the PPPoE user name (13), the HTTP proxy server (20) and the Wi-Fi BSSID and SSID (5, 6) are empty strings, as that configuration sets none of them; the remaining Wi-Fi codes (7-10) and the HTTP proxy port (21) report 0; this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/network.cpp) and is not measured on hardware +- [sceNetCtlGetInfoV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `Jy1EO5GdlcM`) - imported by PPSA08587. `(int code, SceNetCtlInfoV6* info)` with codes 1-5 (IP address, link-local address, default route, primary and secondary DNS) comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero), whose source is unknown; shadPS4 and fpPS4 only stub it. A null `info` answers `INVALID_ADDR`, a code outside 1-5 `INVALID_CODE`, and a host without an IPv6 address `NOT_CONNECTED`, as `sceNetCtlGetStateV6` reports it. The layout of each answer is unverified, so every code throws on a host with an IPv6 address +- [sceNetCtlGetResultV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `H5yARg37U5g`) - `(int eventType, int* errorCode)` comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/NetCtl.cs#L246-L251), whose source is unknown; shadPS4 only stubs it. Callbacks registered with `sceNetCtlRegisterCallbackV6` receive no event, so event types 1-3 write 0 to `errorCode`; what a console writes before the first event is unmeasured. A null `errorCode` answers `INVALID_ADDR`, as `sceNetCtlGetResult` does, and other event types throw, as their error code on a console is unknown +- [ApRp exports](../../../core/libs/prx/libSceNetCtl/Export.cpp) are constant-success functions in the supplied native module and consume no arguments. No callers were found in the supplied system-library decompilations, so unused callback, output and configuration argument types remain unresolved. Protected stub bodies are unchanged. + +### Functional + +- [sceNetCtlCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) - host address changes stand in for the native service event queues. IPv4 callbacks receive connected/disconnected transitions and an initial event; intermediate connection states and errors are not represented. Native CheckCallback drains both IPv4 and IPv6 queues, but the host backend never delivers IPv6 events. Dispatch copies slots before invoking callbacks, so unregistering concurrently may leave one callback in flight; native dispatch holds its registration lock. Slot limits, invalid/deleted IDs and the no-op Term follow the supplied module. +- [libSceNetCtl initialization](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - the library starts initialized, as titles find it: it is the internal sysmodule `0x80000009`, and PPSA11386, PPSA20049, PPSA02530, PPSA14632 and PPSA15878 register and check NetCtl callbacks without importing `sceNetCtlInit`. PPSA11386 executes `int 0x42` at start-up unless `sceNetCtlRegisterCallback` returns 0. So the supplied module's `SCE_NET_CTL_ERROR_NOT_INITIALIZED` answers are unreachable, since Term is a no-op. Whether the console's process start or the module's start sets that state is not measured. +- [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) finds the host's default route in `/proc/net/route` on Linux and, on macOS, in the routing table read with `sysctl(NET_RT_FLAGS, RTF_GATEWAY)`, where an interface-scoped default route ranks after the unscoped one +- [sceNetCtlGetIfStat](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) throws on macOS: an unprivileged process reads the interface byte counters (`getifaddrs` and `sysctl(NET_RT_IFLIST2)` alike) reduced to their low 32 bits, so a total would wrap every 4 GiB +- [Host interface cache](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - on Windows, `sceNetCtlGetState`, `sceNetCtlGetStateV6`, `sceNetCtlCheckCallback`, `sceNetCtlGetIfStat` and `sceNetCtlGetInfo` reuse the last host interface query until `NotifyUnicastIpAddressChange`, `NotifyIpInterfaceChange` or `NotifyRouteChange2` reports a change, instead of calling `GetAdaptersAddresses` on every call. A DNS server change with no address, interface or route change is seen only at the next such change. If the notifications cannot be registered, every call queries the host as before. diff --git a/docs/dev/debt/libSceNetCtlAp.md b/docs/dev/debt/libSceNetCtlAp.md new file mode 100644 index 0000000000..94b54af330 --- /dev/null +++ b/docs/dev/debt/libSceNetCtlAp.md @@ -0,0 +1,11 @@ +# libSceNetCtlAp technical debt + +### Functional + +- [libSceNetCtlAp](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) (libSceNetCtlAp) - the access point never starts: a title starts it through [libSceNetCtlApDialog](libSceNetCtlApDialog.md), which always reports the user as having cancelled. `sceNetCtlApGetState` reports Stopped (0), `sceNetCtlApGetInfo`, `sceNetCtlApGetConnectInfo` and `sceNetCtlApStop` return `NOT_STARTED` (`0x8041298F`) without writing, and no callback ever fires. Signatures, states, structure sizes (152 and 184 bytes), the 8-callback limit and error codes come from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/NetCtlAp.cs) + +### Unknown function info + +- [sceNetCtlApInit, sceNetCtlApTerm](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) (libSceNetCtlAp) - every other call returns `NOT_INITIALIZED` (`0x80412994`) before `sceNetCtlApInit` and after `sceNetCtlApTerm`, which also drops the registered callbacks. A second `sceNetCtlApInit` throws, as no error for it is documented +- [sceNetCtlApGetInfo, sceNetCtlApGetConnectInfo, sceNetCtlApStop](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) - stopped-state results come from a service on the console and remain unverified. Wrapper validation is recovered: initialization precedes arguments, the optional connection string is at most eight bytes, then output pointers and exact 152/184-byte sizes are checked. The service interpretation of the option string remains unresolved. +- [sceNetCtlApGetResult](../../../core/libs/prx/libSceNetCtlAp/Export.cpp) - with no event delivered, the host writes zero; the service result before the first event remains unknown. Wrapper validation checks initialization, output pointer and event type in that order. Event types 1 through 4 and lowest-free callback IDs 0 through 7 are confirmed. diff --git a/docs/dev/debt/libSceNetCtlApDialog.md b/docs/dev/debt/libSceNetCtlApDialog.md index aefc800699..faeb9b3cb6 100644 --- a/docs/dev/debt/libSceNetCtlApDialog.md +++ b/docs/dev/debt/libSceNetCtlApDialog.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceNetCtlApDialog](../../../core/libs/prx/libSceNetCtlApDialog/Export.cpp) - the access-point dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no access-point connection is brought up. `Open` takes the 104-byte parameter block and `GetResult` writes a 40-byte result (int32 result and 36 zeroed reserved bytes), following [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/NetCtlApDialog.cs); the parameter block is not validated and the 0x80B8xxxx error codes follow the other common dialogs, neither verified on hardware. Deathloop imports it +- [libSceNetCtlApDialog](../../../core/libs/prx/libSceNetCtlApDialog/Export.cpp) - the access-point dialog shows no UI and reports user cancellation without bringing up a connection. Parameter size, reserved-byte checks, option-string limit and lifecycle errors are recovered from the supplied module. The meaning of the input word at offset 56 and service-side connection behavior remain unresolved. diff --git a/docs/dev/debt/libSceNgs2.md b/docs/dev/debt/libSceNgs2.md new file mode 100644 index 0000000000..a3bd2a6776 --- /dev/null +++ b/docs/dev/debt/libSceNgs2.md @@ -0,0 +1,6 @@ +# libSceNgs2 technical debt + +### Unknown function info + +- [sceNgs2FftInit, sceNgs2FftProcess, sceNgs2FftQuerySize](../../../core/libs/prx/libSceNgs2/PendingExports.cpp) (libSceNgs2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNgs2.c), but no credible prototypes were found in the cited sources. They are declared without parameters solely to resolve known imports and make no ABI claim. +- [sceNgs2CustomRackGetModuleInfo, sceNgs2ParseWaveformUser, sceNgs2SystemEnumHandles, sceNgs2SystemEnumRackHandles, sceNgs2VoiceGetMatrixInfo](../../../core/libs/prx/libSceNgs2/PendingExports.cpp) (libSceNgs2) - names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNgs2.c); signatures follow [SharpProspero's Ngs2 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs), not measured on a console. Opaque structure pointers use `void*`; these stubs throw for import resolution. diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 1eda352489..3bdb203f0c 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -2,26 +2,47 @@ ### Unknown function info +- [NGS2 voice validation](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) returns `0x804A8203` for an invalid voice in Control, RunCommands, GetState, GetStateFlags, GetPortInfo and QueryInfo, following the type-4 handle error in [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4478). Control checks a null parameter list first (`0x804A8360`); GetState resolves the voice before output validation, and GetState rejects sizes below 8 before a null output. Existing rack-specific exact state sizes, other argument precedence and command layouts are retained; arbitrary-sized state copies and full console behavior remain unverified. These values follow module analysis, not new console execution. + +- [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. +- [NGS2 voice callback after setup](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a sampler or mixer setup clears the voice callback with the rest of the voice state. PPSA18216 registers a block-end callback for a streamed music voice, later sets the same voice up again for one-shot ATRAC9 blocks with user data 0, and registers the callback again before it streams again; its callback dereferences the block's user data, so it must not run for the one-shot blocks. A setup made from inside the voice's own callback clears that callback as well, so a callback that sets its voice up again and queues the next block runs once instead of for every block; PPSA18216 never sets a voice up from inside its callback, so that ordering is unverified. Unverified on console hardware. - [NGS2 ATRAC9 streaming](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) keeps waveform duration and skip counts across compressed input blocks. The shared `APPEND` flag (2), called `ONLY_DATA` in the compressed-data reference, ignores an ATRAC9 continuation block's sample, skip and repeat counts, following [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/bb98f26433fa9e711c219aac1711fe8ecba5bf3a/src/libs/ngs2.cpp); PPSA03747 confirms a 262044-byte initial buffer declaring 11310532 samples, followed by 16384-byte buffers with flags 3. Incomplete superframes are retained until more input arrives. Input-block completion callbacks allow refills; the final callback of a logical waveform waits for its buffered PCM to drain, preserving the existing complete-block repeat behaviour. This buffering, callback timing, decoded-byte accounting and starvation silence are unverified on hardware. A closed ATRAC9 stream ending before its declared samples throws; fragmented ATRAC9 waveforms with repeats are unsupported. PCM streaming retains its existing append validation and early-close behaviour. - [NGS2 mastering voice gain](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the param id 0x30000004 is taken from PPSA22520 and the layout from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2MasteringVoiceGainParam`; no reference gives the units or range. The levels are assumed to be linear factors applied when the mastering voice is mixed into the render buffer, so the voice's own samples stay unscaled; the LFE level applies to channel 3 of 6- and 8-channel voices, assuming the L R C LFE order of the AudioOut2 entries above, and the full-band level to the rest. Negative levels are accepted, and a new setup resets both levels to 1 - [NGS2 stereo mastering voice into a surround buffer](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - PPSA21402 renders a 2-channel mastering voice into a 6- or 8-channel render buffer, which the mixing stage rejected. The console's conversion is unknown: the left and right channels are assumed to go to the first two buffer channels (front left and right) and the remaining channels stay silent, with no centre, LFE or surround content. Any other channel count mismatch still throws +- [NGS2 voices without a format](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a sampler voice whose setup has no format (all zero) accepts an add of no waveform blocks without `APPEND`, which only applies `RESET` and the `CONTINUE` state, and matrix levels with no levels, which clear that matrix. PPSA36591 sets such a voice up, adds no blocks and sets no levels when a button is pressed at its title screen. Whether the console returns success or an error there is not measured; blocks with data, `APPEND` and an empty matrix on a voice with a format are still refused - [NGS2 silent waveform blocks](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native): flag `0x10` (named `SILENCE` here; no public reference names it) lets a waveform block declare samples without data. PPSA04867 adds one block with flags `0x11`, `data_size` 0 and `num_samples` 16385, then another with flags `0x11` and a fully specified 0x800-byte, 1024-sample mono block, which takes the ordinary path. A dataless block under the flag outputs `num_samples` frames of silence, reports them through `decoded_data_size`, and finishes like a complete block; a dataless block without the flag still throws as not fitting its data. The flag's real name, its interaction with ATRAC9 and repeats, and the console's `waveform_data` for silent blocks are unverified on hardware - [NGS2 streaming PCM waveforms](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - PPSA21402 streams its music by adding one block whose `num_samples` is the whole track and whose `data_size` is a 16 KiB first piece, with `CONTINUE`, and then adds each following piece with flag `0x2` (called `APPEND` here) and the same `num_samples`. No reference describes flag `0x2`. A 16-bit PCM block without skipped samples or repeats whose `num_samples` exceed its data is assumed to start such a stream; flag `0x2` is assumed to append the block's data to the last streaming block, ignoring its `num_samples`; the voice is assumed to output silence without advancing while it has played all appended data and `CONTINUE` is set; and the waveform is assumed to end at `num_samples` or, once `CONTINUE` is cleared, at the end of the appended data. `sceNgs2VoiceGetState` reports the bytes played as `decoded_data_size`, which the title uses to release its queued data, and the read position as `waveform_data`. Appending to a block that is not a stream, or other formats, skipped samples, repeats and partial frames, throw -- [NGS2 matrix levels](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - levels are clamped to -4..4 and NaN is kept, as the PS4 firmware 11.00 `libSceNgs2.sprx` stores them; not yet confirmed on the PS5 library. That library rejects a level count of 0 and keeps the matrix; the PS5 error code is unknown, so a count of 0 throws. `sceNgs2VoiceRunCommands` is not in that library, so its matrix levels command is assumed to clamp the same way +- [NGS2 matrix levels](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - levels are clamped to -4..4 and NaN is kept, as the PS4 firmware 11.00 `libSceNgs2.sprx` stores them; not yet confirmed on the PS5 library. That library rejects a level count of 0 and keeps the matrix; the PS5 error code is unknown, so a count of 0 throws, except on a sampler voice without a format, where it clears the matrix (see NGS2 voices without a format). `sceNgs2VoiceRunCommands` is not in that library, so its matrix levels command is assumed to clamp the same way - [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, the voice parameter id `0x40001f00 | 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). Only 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, 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 submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`. UserFx2's id 0x1f, parameter id `0x40001f00 | index`, process flags (1 on the first call after setup, 2 after a param change, else 0) and call order (setup per voice at creation, process after input mixing, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). 0x1d is the id PPSA03061 passes (#5139); its UserFx name and parameter id `0x40001d00 | index` are unsourced. Only these two modules and Gain on one buffer work: a module control handler, unequal input and output channels, setup flags and default rack options throw. Module state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` throws for this rack. As in KytyPS5, UserFx2 runs only on grains with input samples, so an effect tail stops with the input +- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Reverb (0x1b), PitchShift (0x20) and Convolution (0x23) are not processed, nor Gain (0x15) on a custom sampler rack: a rack that passes the creation checks with one throws, as do ids besides these, 0x13, 0x1d and 0x1f. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a non-zero state size other than the module's (0 for Gain and Reverb, 8 for PitchShift), and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: the check order, the errors for a Gain option, a missing Reverb or PitchShift option, other PitchShift and Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). fma-rc maps 0x19 to UserFx, as KytyPS5 does; it still throws, and 0x1d on PS5 is unknown. PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246) +- [NGS2 custom Gain module](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - follows [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943): parameter `0x40001500 | module index` holds 37 linear levels, one per channel, default 0, and the module writes source × level into dest, ramping linearly over the grain from the previous level. Unverified: where the ramp starts and ends (here the grain's first sample is one step past the previous level and its last sample is at the new level), whether stopping or setting up the voice again resets the levels, and what the library does with a level outside -32 to 32 (here it throws). On a custom sampler rack, whose parameter index is one higher per fma-rc, Gain throws. A Gain module with an option throws: fma-rc reads Gain as taking none, and the error the library returns for one is unknown. The extra buffer is ignored, as fma-rc reads it +- [NGS2 custom rack buffers and channels](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, racks 0x4001 and 0x4002) - racks with 2 to 4 buffers, custom submixers with 9 to 37 channels and submixer `max_inputs` other than 1 or 2 throw: how modules and ports route several buffers is unknown, and voices hold at most 8 channels. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A830B` for a buffer count outside 1 to 4 or below the submixer's `max_inputs`, `0x804A8306` for a module source, extra or dest buffer id at or above the count, and `0x804A8052` for more than 37 submixer channels; the check order is not known. `sceNgs2RackQueryBufferSize` throws for these options, as no source says whether it checks them. A port buffer id other than 0 throws. PPSA04540 creates racks with up to 4 buffers and a 16-channel custom submixer (#8039) - [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` +- [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) keep a registry of report handlers. Register rejects a null handler with an invalid argument and returns `INVALID_OUT_ADDRESS` for a null out handle, then stores the report type, handler and user data under a freshly allocated opaque handle; unregister removes and frees the matching record and throws for a handle that is not registered. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`, and the 24-byte `Ngs2ReportDataHeader` passed to a handler is its `OrbisNgs2ReportDataHeader`. No report is produced yet, so no registered handler is ever invoked and PPSA04540, which imports both and registers a CPU load handler (type 5), still receives none. +- [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. `sceNgs2SystemLock` and `sceNgs2SystemUnlock` follow [prosper's notes](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4367-L4384) on the PS5 module: Lock takes a per-system lock and counts nested holds, Unlock releases one hold from any thread and returns 0 without a hold, and `sceNgs2SystemRender` only tries the lock: while another thread holds it, Render clears one grain of each output, leaves the render count and voices unchanged and returns 0. The other calls do not wait for the system lock. Peace Walker (PPSA18216) holds it while it spins on a flag that its audio thread clears only after `sceNgs2SystemRender` and eight `sceNgs2VoiceGetStateFlags` calls, so on the console neither can wait for it. Whether the console's voice and rack calls take the lock at all is not known. `sceNgs2RackLock` still holds the library's lock until `sceNgs2RackUnlock`, as before +- [sceNgs2GetWaveformFrameInfo](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - not measured on a console; shadPS4 only stubs it, and prosper does not cover the call. The outputs are taken to be the frame fields of the parsed waveform info, which [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L224-L231) fills for ATRAC9 with the superframe bytes and samples, the frames per superframe and a one-frame delay, whatever the `fact` chunk says. `PCM_I16L` answers channels * 2 bytes, 1 sample, 1 unit and no delay, as for parsed 16-bit PCM, itself assumed. Other types throw. Null outputs return `INVALID_OUT_ADDRESS` and a bad format `INVALID_WAVEFORM_FORMAT`, as in `sceNgs2CalcWaveformBlock`; the codes are unverified +- [sceNgs2RackSetUserData, sceNgs2RackGetUserData](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - the signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L418-L421) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L86-L111), which only logs them; neither gives an error. A handle that is not a live rack answers `INVALID_RACK_HANDLE` before a null user data pointer is checked, and the null pointer throws, as in `sceNgs2SystemGetUserData`. A new rack starts with 0. Not measured on a console - [sceNgs2RackGetInfo](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero - [sceNgs2VoiceQueryInfo](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw +- [sceNgs2VoiceGetOwner](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the signature `(voiceHandle, outOwnerRack, outVoiceIndex)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L439-L440) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L322-L326), which only logs it; neither gives an error. The errors are unmeasured: a handle that is not a live voice answers `INVALID_VOICE_HANDLE` before the outputs are checked, as in `sceNgs2RackGetVoiceHandle`, and either output pointer null answers `INVALID_OUT_ADDRESS` without writing the other - [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. +- [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. A speaker count of 0 without angles, which the firmware accepts, a non-finite speaker angle and a speaker angle equal to the unit angle, on which that library loops forever, throw. 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` 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. When front and up are parallel, up is replaced by `(0,1,0)` or `(1,0,0)` so the basis stays defined: PPSA15552 (Dead Cells) passes `orient_front={0,0,1}` with `orient_up={0,0,1}` after moving the listener, which PS5PCEM rejects; the console error code is unknown. Zero orientation vectors, nonfinite input and an unrepresentable translation still 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 plus that Dead Cells case. 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 +- [sceNgs2CalcWaveformBlock](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - ATRAC9 keeps AnyPS5's own mapping: it checks the config against the channels and rate (0x804A8431), leaves out the decoder delay from the skip and size, leaves both 0 for zero samples and computes the size in 64 bits, while [prosper reads the PS5 module](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/src/hle/audio/ngs2_waveform.cpp#L348-L396) without the check, with the delay, filling both and modulo 2^32. Linear PCM and float (0x10 to 0x1b), VAG (0x1c: whole 28-sample frames of 16 bytes per channel, the frame-rounded sample count and no skip), the null checks and the 0x804A8432 of other types follow that reading and [its entry point](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/src/hle/audio/ngs2_waveform.cpp#L444-L466). Closes when: ATRAC9 follows the module + +### Functional + +- [NGS2 rack lock and voice callbacks](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - `sceNgs2RackLock` gives the calling thread the rack, nested. Rack locks, renders and rack or system destroys exclude each other across threads and systems, including during a render's voice callbacks; a thread nests them, also from its own callbacks, and a callback that waits for another thread's rack lock or render never returns. Other NGS2 calls from other threads go ahead, and a block-end or repeat callback runs with the internal lock released. The console's behaviour for other threads' calls during a rack lock is not measured. PPSA36591 holds a rack lock on its main thread while it waits for one of its own locks, held by a thread in `sceNgs2VoiceGetStateFlags`; it froze while the rack lock held the internal lock. `sceNgs2RackUnlock` of a rack this thread has not locked returns `0x804A8202`; the console code is unknown. UserFx process handlers keep the internal lock. +- [NGS2 destroy from its own render](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - `sceNgs2RackDestroy` of a rack of the system being rendered, or `sceNgs2SystemDestroy` of that system, called from one of that render's voice callbacks throws, also when the callback renders another system whose own callback makes the call, because the render would go on using the destroyed rack. The console's behaviour is not measured. +- [sceNgs2RackGetVoiceHandle voice index](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - an index at or beyond the rack's voice count returns `0x804A8305` and leaves the output handle untouched, after the null-output and rack-handle checks that keep their order and codes. The value is read from the PS5 `libSceNgs2.native.sprx` export `MwmHz8pAdAo` (firmware 10.01 at `0x11f10`, 12.70 at `0x12130`, and every module from 1.00 to 12.70 checked in [#5916](https://github.com/boykopovar/AnyPS5/pull/5916#issuecomment-6172832590)): an index at or past the rack's voice count returns it through the same error helper that answers `0x804A8010` for a null output, and the output handle is written only on success. The rack is checked before the output pointer, and an invalid rack returns `0x804A8202`. Heretic + Hexen (PPSA26525), DOOM + DOOM II (PPSA21444) and STAR WARS: Dark Forces Remaster (PPSA19728) create a one-voice reverb rack, take voice 0 and ask for voices 1 to 3 at start-up, log the failures and go on diff --git a/docs/dev/debt/libSceNpAuth.md b/docs/dev/debt/libSceNpAuth.md index 8e344dfda7..98b7bde5c9 100644 --- a/docs/dev/debt/libSceNpAuth.md +++ b/docs/dev/debt/libSceNpAuth.md @@ -2,6 +2,12 @@ ### Unknown function info -- [sceNpAuthGetAuthorizedAppCode](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - declared without parameters and returns `SCE_NP_ERROR_SIGNED_OUT` without reading them, as there is no PSN sign-in; the request parameter and code output are not known. Minecraft (PPSA17221) imports it, fails to load without it, and after its sign-in dialog treats the signed-out code as offline play. -- [sceNpAuthAbortRequest, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdTokenV3, sceNpAuthPollAsync, sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - request tracking, `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` (0x80550306) on unknown request IDs, `ORBIS_NP_AUTH_ERROR_INVALID_ID` (0x80550307) on non-async or unprocessed requests in Poll/Wait, `ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT` (0x80550301) on null arguments and repeated calls on completed requests, and `ORBIS_NP_AUTH_ERROR_ABORTED` (0x80550304) after an abort follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_auth.cpp) (PS4); async requests return OK and defer `SCE_NP_ERROR_SIGNED_OUT` to `sceNpAuthPollAsync`/`sceNpAuthWaitAsync` while sync requests fail immediately; PlayStation Network is not contacted. The parameter `size` fields are not checked (`ORBIS_NP_AUTH_ERROR_INVALID_SIZE`, structure layouts unknown), the request table has no limit (`ORBIS_NP_AUTH_ERROR_REQUEST_MAX`), and console behavior on PS5 is unverified -- [sceNpAuthSetTimeout](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - imported by PPSA01317, which fails to load without it. The signature `(reqId, resolveRetry, resolveTimeout, connTimeout, sendTimeout, recvTimeout)` follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_auth.cpp) (PS4), which only stubs it. An unknown request ID returns `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` like the other request calls; otherwise it returns OK and keeps nothing, as no request reaches the network the timeouts would bound. Argument checks of the console are unverified +- [sceNpAuthAbortRequest, sceNpAuthGetAuthorizationCode, sceNpAuthGetAuthorizationCodeA, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdToken, sceNpAuthGetIdTokenA, sceNpAuthGetIdTokenV3, sceNpAuthPollAsync, sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - request tracking, `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` (0x80550306) on unknown IDs, `ORBIS_NP_AUTH_ERROR_INVALID_ID` (0x80550307) on non-async or unprocessed requests in Poll/Wait, `ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT` (0x80550301) on null arguments, and `ORBIS_NP_AUTH_ERROR_ABORTED` (0x80550304) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp) (PS4); the A calls forward to their V3 counterparts, which in shadPS4 ([AuthorizationCode A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L189-L193), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L195-L199); [IdToken A](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L375-L378), [V3](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp#L380-L383)) call the same helper and differ only in a version flag; async requests defer `SCE_NP_ERROR_SIGNED_OUT` to Poll/Wait while sync requests fail immediately; PSN is not contacted. Parameter size validation and PS5 behavior are unverified + +- [sceNpAuthPollAsync and sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - PS5 abort-before-start behavior is unmeasured. Poll/Wait keep returning `ORBIS_NP_AUTH_ERROR_INVALID_ID` without changing output after a pre-start abort, including subsequent rejected getter calls, following [shadPS4 StartRequest and FindAsyncRequest](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_auth/np_auth.cpp). + +### Functional + +- [sceNpAuthSetTimeout](../../../core/libs/prx/libSceNpAuth/Export.cpp): native validation and storage are implemented: retry must be nonnegative, the all-zero tuple is invalid, resolve timeout is zero or at least 1000000, and other timeouts are zero or at least 10000000. Invalid arguments precede request lookup. Stored settings currently have no transport to configure because authentication remains offline. + +- [sceNpAuthGetAuthorizedAppCode](../../../core/libs/prx/libSceNpAuth/Export.cpp): the five-argument ABI and 40-byte request parameter are recovered. Pointer, size, user, option and request-ID validation replaces the unconditional signed-out result. Valid calls throw because authorization and asynchronous result delivery are not implemented. Client-ID storage is copied as 136 bytes by the firmware; its internal fields and the complete output-code format remain unresolved. diff --git a/docs/dev/debt/libSceNpAuthAuthorizedAppDialog.md b/docs/dev/debt/libSceNpAuthAuthorizedAppDialog.md index 7411171423..6f98749d82 100644 --- a/docs/dev/debt/libSceNpAuthAuthorizedAppDialog.md +++ b/docs/dev/debt/libSceNpAuthAuthorizedAppDialog.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceNpAuthAuthorizedAppDialog](../../../core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp) - the authorized-app dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as there is no PSN sign-in to authorize an app against. The signatures follow the other common dialogs (`Open` takes a parameter block, `GetResult` writes a 32-bit result); Minecraft (PPSA17221) imports the seven functions and fails to load without them. +- [libSceNpAuthAuthorizedAppDialog](../../../core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp) - the authorized-app dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as there is no PSN sign-in to authorize an app against. Parameter fields at offsets 64 and 72 feed a 136-byte application record and a text value of at most 1024 bytes; their service-side meaning remains incomplete. diff --git a/docs/dev/debt/libSceNpCommerce.md b/docs/dev/debt/libSceNpCommerce.md index a3a899c134..02559d2c86 100644 --- a/docs/dev/debt/libSceNpCommerce.md +++ b/docs/dev/debt/libSceNpCommerce.md @@ -3,3 +3,10 @@ ### Silent stubs - [libSceNpCommerce](../../../core/libs/prx/libSceNpCommerce/Export.cpp) - the PS Store icon show/hide calls and `sceNpCommerceSetPsStoreIconLayout` do nothing + +### Unknown function info + +- [sceNpCommerceDialogInitializeInternal](../../../core/libs/prx/libSceNpCommerce/Export.cpp) (libSceNpCommerce) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpCommerce.c); the no-parameter prototype follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_commerce/np_commerce.cpp#L61) and is not measured on a console. It is declared without parameters solely to resolve known imports; this is not an ABI claim, and every call throws. +- [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp): extended content combinations outside content mode 0 and content mode 1 with dialog mode 1 throw when otherwise valid. The native restriction reads process sysctl field 68; its relation to host process metadata is unresolved. Mode-specific meanings of the option and 64-bit argument remain unresolved. + +- [sceNpCommerceDialogOpen](../../../core/libs/prx/libSceNpCommerce/Export.cpp): Product copying and service submission remain part of the cancelled UI stub. diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md new file mode 100644 index 0000000000..2a8900021d --- /dev/null +++ b/docs/dev/debt/libSceNpCommon.md @@ -0,0 +1,156 @@ +# libSceNpCommon technical debt + +### Unknown function info + +The 150 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. + +- [sceNpAllocateKernelMemoryNoAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `MWPOkqzYss0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpAllocateKernelMemoryWithAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `gMlY6eewr-c`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpArchInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `jGF+MaB4b-M`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpArchTerm](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `UskWpVWxSvg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpAtomicCas32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `+9+kKMY9YIw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpAtomicDec32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Yohe0MMDfj0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpAtomicInc32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pfJgSA4jO3M`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpBase64Decoder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `l67qBmMmKP4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpBase64Encoder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pu39pU8UgCo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpBase64GetDecodeSize](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `a5IfPlpchXI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpBase64UrlDecoder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `moGcgMNTHvQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpBase64UrlEncoder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `IeNj+OcWgU8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpBase64UrlGetDecodeSize](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `7BjZKcN+oZ4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCalloutInitCtx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9+m5nRdJ-wQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCalloutStartOnCtx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fClnlkZmA6k`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCalloutStartOnCtx64](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `lpr66Gby8dQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCalloutStopOnCtx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `in19gH7G040`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCalloutTermCtx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `AqJ4xkWsV+I`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCancelEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kb2thTuS8t8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpClearEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9pLoHoPMxeg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCloseEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `+nmn+Z0nWDo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCloseSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `8hPzfjZzV88`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCmpNpIdInOrder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `TcwEFnakiSc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCmpOnlineId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `dj+O5aD2a0Q`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCondDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `1a+iY5YUJcI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCondInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `q2tsVO3lM4A`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCondSignal](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `uMJFOA62mVU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCondSignalAll](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `bsjWg59A7aE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCondSignalTo](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `bAHIOyNnx5Y`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCondTimedwait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ss2xO9IJxKQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCondWait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fZShld2PQ7w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCreateEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `6jFWpAfqAcc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCreateSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `LHZtCT2W1Pw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCreateThread](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fhJ5uKzcn0w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpDbgAssignDebugId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `90pmGqDK4BI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpDbgDumpBinary](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Etq15-l9yko`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpDbgDumpText](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ZaKa5x61hGA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpDeleteEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `sjnIeFCuTD0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpDeleteSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xPrF2nGPBXQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpFreeKernelMemory](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vjwlDmsGtME`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `QmDEFikd3VA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `sXVQUIGmk2U`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGetProcessId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Z3mnqcGmf8E`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGetRandom](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pJlGhXEt5CU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Pglk7zFj0DI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGetSdkVersionUInt](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ljqnF0hmLjo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGetSystemClockUsec](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `PVVsRmMkO1g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGlobalHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-gN6uE+zWng`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGlobalHeapGetAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `VUHUasztbUY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGlobalHeapGetAllocatorExPtr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `P4YpPziLBd4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGlobalHeapGetAllocatorPtr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `DI5n4aOdxmk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpHeapDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `wVdn78HKc30`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `lvek8w7yqyE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpHeapGetStat](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `2jdHoPpS+W0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpHeapInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `B+yGIX1+BTI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpHeapShowStat](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `evz0-93ucJc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpHexToInt](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Hvpr+otU4bo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpInt32ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `5y0wMPQkaeU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpInt64ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `HoPC33siDD4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpIntGetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `G6qytFoBJ-w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpIntIsOnlineIdString](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fY4XQoA20i8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpIntIsValidOnlineId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `hkeX9iuCwlI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpIntSetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `X6emt+LbSEI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpIntToHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `TWPY1x1Atys`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpIpc2ClientInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kgDwlmy78k0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpIpc2ClientTerm](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `CI2p6Viee9w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpJoinThread](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `EjMsfO3GCIA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpJsonParse](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vJGDnNh4I0g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpJsonParseBuf](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `RgfCYkjW7As`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpJsonParseBufInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `SnAdybtBK3o`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpJsonParseEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `p5ZkSMRR7AU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpJsonParseExInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `nhgjiwPUIzI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpJsonParseInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `teVnFAL6GNY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwCondDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `zNb6IxegrCE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwCondInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `++eqYdzB8Go`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwCondSignal](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Xkn6VoN-wuQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwCondSignalAll](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FJ4DCt8VzVE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwCondSignalTo](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Bwi+EP8VQ+g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwCondWait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ExeLuE3EQCQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwMutexDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4zxevggtYrQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwMutexInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `1CiXI-MyEKs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwMutexLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `18j+qk6dRwk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwMutexTryLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `hp0kVgu5Fxw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwMutexUnlock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `CQG2oyx1-nM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMemoryHeapDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `dfXSH2Tsjkw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMemoryHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FaMNvjMA6to`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMemoryHeapGetAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xHAiSVEEjSI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMemoryHeapInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kZizwrFvWZY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMutexDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `lQ11BpMM4LU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMutexInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `uEwag-0YZPc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMutexLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `r9Bet+s6fKc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMutexTryLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `DuslmoqQ+nk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMutexUnlock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `oZyb9ktuCpA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpOpenEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `5DkyduAF2rs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpOpenSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-blITIdtUd0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpPanic](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ZoXUrTiwKNw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpPollEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9YmBJ8KF9eI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpPollSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xmF0yIF4iXc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpRtcConvertToPosixTime](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `VMjIo2Z-aW0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpRtcFormatRFC3339](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `W0YWLVDndx0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpRtcParseRFC3339](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `LtkeQwMIEWY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpServerErrorJsonGetErrorCode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `0lZHbA-HRD0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpServerErrorJsonMultiGetErrorCode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `cRabutqUG7c`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpServerErrorJsonParse](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `WSQxnAVLKgw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpServerErrorJsonParseInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `UbStlMKTBeU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpServerErrorJsonParseMultiInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `hbe+DdooIi4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpSetEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `29ftOGIrUCo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpSetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `m9JzZSoDVFY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpSignalSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-W28+9p1CKI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrBuildHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `i5TP5NLmkoQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrParseHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `DUHzVPNlugg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrToInt32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fElyBSn-l24`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrToInt64](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `CwqYdG4TrjA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrToUInt32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `uj86YxCYid0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrToUInt64](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Ted2YU9lv94`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrcpyToBuf](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ivnnssCwjGI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrnParseHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `h1SWCcBdImo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrncpyToBuf](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `PHrpHMSU8Cs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpThreadGetId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `yvaNTRiKXmo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUInt32ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `rRN89jBArEM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUInt64ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `QjNUYQbGoHA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUserGetUserIdList](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Gh74vNl06sg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilBuildTitleId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `N3tAHlBnowE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilCanonicalizeNpIdForPs4](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4mEAk-UKVNw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilCanonicalizeNpIdForPsp2](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `N3FB4r8JoRE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilCmpAccountId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xPRHNaD3kTc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetDateSetAuto](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `owm52JoZ8uc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetDbgCommerce](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `1Gfhi+tZ9IE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetEnv](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kBON3bAtfGs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetFakeDisplayNameMode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `MUj0IV6XFGs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetFakeRateLimit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `O86rgZ2azfg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetIgnoreNpTitleId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FrxliFYAO8Y`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetNpDebug](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `GRvK1ZE+FEQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetNpLanguageCode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `OFiFmfsADas`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetNpLanguageCode2](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `X9CqyP164Hc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetNpLanguageCode2Str](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Fxux7Ob+Ynk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetNpLanguageCodeStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `RfiA17kV+xs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetNpTestPatch](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `OA8f3KF9JsM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetNthChar](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `KCk4OGu8+sc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetShareTitleCheck](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fB5hE65pzbU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetSystemLanguage](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `SXUNKr9Zkv0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetTrcNotify](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `AjzLvR0g5Zs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetWebApi2FakeRateLimit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pmHBFJyju9E`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetWebApi2FakeRateLimitTarget](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ZRxKp9vjcNc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetWebTraceSetting](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4CqfNm3pisU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilHttpUrlEncode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ajoqGz0D9Dw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilJidToNpId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `458yjI+OECI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilJsonEscape](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `EftEB4kmkSg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilJsonGetOneChar](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vj04qzp7uKY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilJsonUnescape](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4YJ5gYtRAAE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceNpCppWebApi.md b/docs/dev/debt/libSceNpCppWebApi.md new file mode 100644 index 0000000000..f18925c392 --- /dev/null +++ b/docs/dev/debt/libSceNpCppWebApi.md @@ -0,0 +1,19 @@ +# libSceNpCppWebApi technical debt + +### Silent stubs + +- [sce::Np::CppWebApi::Common](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the `finish` overrides of `TransactionBase<...>` (13 instantiations), `DownStreamTransaction<...>` (2 instantiations) and `Vector>::pushBack` return success without doing work; a failed request never produces a transaction to finish. `Transaction<...>::setOnFinishedCallback` and `TransactionBase<...>::setResponseInformationOption` (5 instantiations each, the `UserProfile::V1`, `ProfanityFilter::V2` and `CommunicationRestrictionStatus::V3` responses) return success without storing the callback or the option: `start` fails, so neither is ever used. The constructors and destructors of the same five `Transaction<...>` instantiations succeed without keeping state +- [sce::Np::CppWebApi::Leaderboards::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - request-body and parameter bookkeeping succeeds without storing state: `GetRankingRequestBody::set{StartSerialRank,Group,Limit,Users,Offset}`, `RecordScoreRequestBody::set{Comment,SmallData,NeedsTmpRank,ComparedDateTime,PcId}`, `User::{setAccountId,setPcId}`, `BoardsApi::ParameterToGetBoardDefinition::{initialize,terminate}`, `RecordApi::ParameterToRecordLargeData::{initialize,terminate}`, `RecordApi::ParameterToRecordScore::{initialize,setxPsnAtomicOperation,terminate}`, `ViewApi::ParameterToGetLargeDataByObjectId::{initialize,terminate}`, `ViewApi::ParameterToGetRanking::{initialize,setgetRankingRequestBody,terminate}` +- [sce::Np::CppWebApi::SessionManager::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - parameter bookkeeping succeeds without storing state: `PlayerSessionsApi::ParameterToCreatePlayerSessions::{ctor,dtor,initialize}`, `PlayerSessionsApi::ParameterToGetPlayerSessionInvitations::{ctor,dtor,initialize,setfields,setinvitationInvalidFilter}`, `PlayerSessionsApi::ParameterToJoinPlayerSessionAsPlayer::{ctor,dtor,initialize,terminate}`, `PlayerSessionsApi::ParameterToLeavePlayerSession::{ctor,dtor,initialize}`, `RequestPlayerSession::set{MaxSpectators,InvitableUserType,JoinableUserType,ExclusiveLeaderPrivileges}`, `RequestPlayerSessionPlayer::setPushContexts` +- [sce::Np::CppWebApi::TitleCloudStorage::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - `DataApi::ParameterToDownloadData::{initialize,terminate}` succeed without storing state +- [sce::Np::CppWebApi::UserProfile::V1, ProfanityFilter::V2, CommunicationRestrictionStatus::V3](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - parameter bookkeeping succeeds without storing state: `FriendsApi::ParameterToGetFriends`, `PresenceApi::ParameterToGetBasicPresences`, `BlocksApi::ParameterToGetBlockingUsers`, `ProfanityApi::ParameterToFilterProfanity` and `CommunicationRestrictionStatusApi::ParameterToGetCommunicationRestrictionStatus`, each `{ctor,dtor,initialize,terminate}` + +### Unknown function info + +- [sce::Np::CppWebApi](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - every request entry point (API calls, transaction `start`/`getResponse`/`readData`, the `*Factory::create`s) returns `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN` offline. The constant's name and value follow the reference tables in shadPS4 `np_error.h` and fpPS4 `np_error.pas`, where `0x80552907` is `ORBIS_NP_WEBAPI_ERROR_NOT_SIGNED_IN` and `0x80552901` is `OUT_OF_MEMORY` (no `UNAVAILABLE` exists in the `0x80552901`-`0x8055291F` range); it is the code a signed-out console returns from these entry points. The earlier `0x80552901` value came from the module the previous implementation shipped, which used the same incorrect constant +- [sce::Np::CppWebApi::Common::IntrusivePtr](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the 0x18-byte layout (object, deleter, `LibContext*`) and the offset of the 32-bit reference count in the pointee (0x00, 0x30 for `Vector`) come from the module bundled with one title (PPSA21564); other SDK versions are not checked. The destructor throws on the last reference of a pointer without a deleter: destroying the object needs its destructor and the `LibContext` allocator, which are not implemented +- [sce::Np::CppWebApi::Common::String](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the 0x20-byte layout (32-bit reference count, buffer at 0x08, buffer size at 0x10, `LibContext*` at 0x18) comes from the module bundled with one title (PPSA21564); other SDK versions are not checked. `append` grows the buffer with the host allocator and the destructor frees it, instead of going through the `LibContext` allocator. `pushBack` into `Vector` stores a deep copy, so a destroyed temporary cannot invalidate the stored element; a title that byte-copies a `String` with a live buffer would still double-free, as nothing increments the reference count +- [sce::Np::CppWebApi::Common::Vector](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671, whose dump ships no `libSceNpCppWebApi.prx`. A `Vector` handle is an `IntrusivePtr` over a shared body; bodies created by `pushBack` use a host-side layout with the reference count at 0x30, matching the adopted-body convention measured on PPSA21564. The pointer/deleter constructor adopts a title-provided body released through the stored deleter; `pushBack` on such a body throws, since its element layout is unknown. `begin`, `end`, iterators and `operator[]` throw: the only `Vector`s the title reads back come from failed transactions, which never produce one +- [sce::Np::CppWebApi::SessionManager2V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671. Offline, the factories, `PlayerSessionsApi` calls and transaction `start`/`getResponse`/`readData` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter setters, `initialize`, `terminate`, constructors and destructors succeed without keeping state, as no request is ever sent. Response-body accessors (`getSessionId`, `getPlayerSessions`, `getInvitations`, the `IsSet` queries) throw: a failed request never produces a response object +- [sce::Np::CppWebApi::UserProfile::V1, ProfanityFilter::V2, CommunicationRestrictionStatus::V3](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by an Epic Online Services module whose dump ships no `libSceNpCppWebApi.prx`. The reference-count offset 0x00 of `ResponseHeaderBase` and `WebApiFilterRequest` is assumed from the other objects, not measured. Offline, the five API calls, `WebApiFilterRequestFactory::create` and the transactions' `start`/`getResponseHeaders` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`. Response accessors, `ResponseHeaderBase::getHeaderValue` and the `Vector`/`Iterator` members over `IntrusivePtr` and `unsigned long` throw: a failed request never produces a response +- [libSceNpCppWebApi](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - unimplemented stubs have their parameter lists, const qualifiers and return types aligned with the demangled symbols confirmed by NID matching in [#8143](https://github.com/boykopovar/AnyPS5/issues/8143). They cover response-body accessors, iterators, and uninstantiated transactions in Leaderboards::V1, SessionManager::V1, InGameCatalog::V5, TitleCloudStorage::V1, and Common templates: each mangled name gives the method and parameter types, but the runtime layout of response objects and errors are unverified on console, so each call continues to throw NotImplemented_nid_no_patch. No call site is recorded for them diff --git a/docs/dev/debt/libSceNpEntitlementAccess.md b/docs/dev/debt/libSceNpEntitlementAccess.md index b457b050c8..7d54ea4fe1 100644 --- a/docs/dev/debt/libSceNpEntitlementAccess.md +++ b/docs/dev/debt/libSceNpEntitlementAccess.md @@ -2,8 +2,9 @@ ### Unknown function info +- [sceNpEntitlementAccessGetAddcontEntitlementInfoIndividual, sceNpEntitlementAccessPollConsumableEntitlementInfo, sceNpEntitlementAccessRequestConsumableEntitlementInfo, sceNpEntitlementAccessRequestConsumeEntitlement](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) (libSceNpEntitlementAccess) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpEntitlementAccess.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim - [sceNpEntitlementAccessGetEntitlementKey](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - the three-argument signature, 16-byte key and errors `0x817D0002`/`0x817D0007` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp); PPSA01521 passes a service label, entitlement label and output pointer. Only the no-key path is implemented: local entitlement labels carry no keys, and the output is untouched on failure. Console error precedence and key provisioning are unverified -- [sceNpEntitlementAccess{Request,Poll}{Unified,Service}EntitlementInfo{,List}](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - signatures unknown; no reference implements them, so they are declared without parameters. The single-item variants copy the behaviour of the list ones, with no source of their own. Like the consume requests in the same library, they report the signed-out NP state with `SCE_NP_ERROR_SIGNED_OUT` (`0x80550006`) and write nothing. PPSA15657 calls the unified request during start-up and continues past the error. Whether the console fails at the request or at the poll when signed out is unverified +- [Entitlement requests and polls](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp): native argument lists are restored for single-item, list and consume operations, plus abort/delete. Request IDs are 64-bit, labels occupy 20 bytes and transaction IDs occupy 72 bytes. List options occupy 32 bytes and results have a 64-byte stride; their field semantics and three list-result scalars remain unresolved. The existing signed-out responses do not reproduce native initialization, validation or asynchronous request handling; abort/delete throw until request tracking is implemented. ### Functional diff --git a/docs/dev/debt/libSceNpEulaDialog.md b/docs/dev/debt/libSceNpEulaDialog.md new file mode 100644 index 0000000000..60dab48126 --- /dev/null +++ b/docs/dev/debt/libSceNpEulaDialog.md @@ -0,0 +1,5 @@ +# libSceNpEulaDialog technical debt + +### Unknown function info + +- [sceNpEulaDialogCheckVersion, sceNpEulaDialogCheckVersionAbort, sceNpEulaDialogClose, sceNpEulaDialogGetResult, sceNpEulaDialogGetStatus, sceNpEulaDialogInitialize, sceNpEulaDialogOpen, sceNpEulaDialogTerminate and sceNpEulaDialogUpdateStatus](../../../core/libs/prx/libSceNpEulaDialog/Export.cpp) (libSceNpEulaDialog) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpEulaDialog.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceNpFriendListDialog.md b/docs/dev/debt/libSceNpFriendListDialog.md new file mode 100644 index 0000000000..2d5b73e230 --- /dev/null +++ b/docs/dev/debt/libSceNpFriendListDialog.md @@ -0,0 +1,9 @@ +# libSceNpFriendListDialog technical debt + +### Silent stubs + +- [libSceNpFriendListDialog](../../../core/libs/prx/libSceNpFriendListDialog/Export.cpp) - the friend list dialog (`sceNpFriendListDialogOpenA` and `sceNpFriendListDialogGetResultA`) runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as there is no PSN friend list. The signatures are from [Orbital's type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); the result layout beyond the leading `int32_t` result code is not known, so nothing else is written. + +### Unknown function info + +- [sceNpFriendListDialogGetResult and sceNpFriendListDialogOpen](../../../core/libs/prx/libSceNpFriendListDialog/Export.cpp) (libSceNpFriendListDialog) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpFriendListDialog.c), but Orbital has prototypes only for the `A` variants and no credible prototypes were found for these. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceNpGameIntent.md b/docs/dev/debt/libSceNpGameIntent.md new file mode 100644 index 0000000000..622b40a2c2 --- /dev/null +++ b/docs/dev/debt/libSceNpGameIntent.md @@ -0,0 +1,12 @@ +# libSceNpGameIntent technical debt + +### Silent stubs + +- [sceNpGameIntentVshInitialize, sceNpGameIntentVshTerminate](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - VSH initialization and termination return 0 without performing shell operations, matching `sceNpGameIntentInitialize` and `sceNpGameIntentTerminate` +- [sceNpAppLauncherInitialize, sceNpAppLauncherTerminate](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - app-launcher lifecycle returns success without tracking launch state; `sceNpAppLauncherInitialize` accepts its init parameter without validation, following the silent-stub precedent of `sceNpGameIntentInitialize` — a title calls it early in boot, and returning success lets it continue + +### Unknown function info + +- [sceNpAppLauncherLaunchApp, sceNpGameIntentGetUdsNpComId, sceNpGameIntentLaunchApp, sceNpGameIntentLaunchApp2, sceNpGameIntentNotifyAppLaunched, sceNpSessionServiceGetJoinedPartySession, sceNpSessionServiceGetRunningGamePlayerSession and sceNpSessionServiceSendPlayerSessionInvitations](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpGameIntent.c). Call sites in a title's executable give a partial prototype: `sceNpAppLauncherLaunchApp` takes `(user_id, uri struct, context)` and returns int — throws because no launcher-service semantics are derivable. The rest are declared without parameters solely to resolve known imports and make no ABI claim. +- [sceNpGameIntentInitParamInit](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - struct layout matches [fpPS4](https://github.com/red-prig/fpPS4/blob/master/src/np/ps4_libscenpgameintent.pas#L17) `SceNpGameIntentInitParam`; setting `size`, zeroing reserved bytes, and throwing on null pointer follow Sony SDK `*ParamInit` conventions but are unverified on retail hardware +- [sceNpGameIntentInfoInit](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - struct layout matches [fpPS4](https://github.com/red-prig/fpPS4/blob/master/src/np/ps4_libscenpgameintent.pas#L28) `SceNpGameIntentInfo`; setting `size`, `user_id = -1`, zeroing fields, and throwing on null pointer match `sceNpGameIntentReceiveIntent` defaults but are unverified on retail hardware diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index cb0212dd00..a484f93b3a 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -1,11 +1,31 @@ # libSceNpManager technical debt +### Silent stubs + +- [sceNpUnregisterPlusEventCallback](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - returns 0 whether or not a callback was registered, as `sceNpUnregisterPremiumEventCallback` does: `sceNpRegisterPlusEventCallback` keeps no callback, and a signed-out user gets no plus events + ### Unknown function info +- [libSceNpManager pending exports](../../../core/libs/prx/libSceNpManager/Export.cpp) - 143 named callable exports are present in the local `libSceNpManager.sprx` export table and the public PS5 3.20 symbol list. Their signatures and behavior are unverified; each declaration uses the no-parameter unknown-signature convention and throws through `NotImplemented_nid_no_patch`. + - [sceNpGetSandboxInfo](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. +- [sceNpUnknown_8weiQyIdI3A](../../../core/libs/prx/libSceNpManager/Export.cpp): the original name remains unknown. The native wrapper takes a user ID and a read-only 20-byte online-ID record, validates its first byte and sends the record through IPC. The implementation remains a throwing stub. +- [sceNpUnknown_WxLcN76q19o](../../../core/libs/prx/libSceNpManager/Export.cpp): the original name remains unknown. The native wrapper has no input arguments and returns a 32-bit status from an IPC request after initialization checks. The implementation remains a throwing stub. - [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 +- [sceNpNotifyPlusFeature](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - PS4 titles import it; its parameter block and what the console does with it for a signed-out user are unknown ([shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp#L1170) returns 0 without reading it), so every call throws. ### Functional +- [sceNpGetAccountCountry, sceNpGetAccountDateOfBirth, sceNpGetAccountLanguage, sceNpGetParentalControlInfo](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - PS4 forms taking an online id, not measured on a console. Signatures and checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp): a null pointer is `INVALID_ARGUMENT`; no user is signed in, so no online id matches one and the rest return `USER_NOT_FOUND` without writing, like `sceNpCheckNpAvailability`. Request ids are not checked +- [sceNpCheckNpAvailabilityA, sceNpGetAccountDateOfBirthA, sceNpGetAccountLanguageA, sceNpGetGamePresenceStatus, sceNpGetGamePresenceStatusA, sceNpGetParentalControlInfoA](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - answer as if no PSN account is signed in; the signed-in behaviour is not implemented and none of it is measured on a console. Checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c2814041e546bf75c12756dbec9da911c721ce1c/src/core/libraries/np/np_manager.cpp): a null pointer or user id -1 is `INVALID_ARGUMENT`, then request id 0 is `INVALID_ARGUMENT` and an unknown one `REQUEST_NOT_FOUND`. Other users get `SIGNED_OUT` with nothing written, as from `sceNpGetAccountCountryA`; shadPS4's `sceNpGetAccountDateOfBirthA` returns `USER_NOT_FOUND` for a user it does not know. An async request gets `SIGNED_OUT` from the call; shadPS4 returns 0 and leaves it to `sceNpPollAsync`. The presence queries write `OFFLINE` (0) and return 0 +- [sceNpGetUserIdByOnlineId](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the `(online id, user id output)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp#L703): a null online id or output returns `SCE_NP_ERROR_INVALID_ARGUMENT`, and a signed-out NP returns `SCE_NP_ERROR_SIGNED_OUT` without writing the output, as `sceNpGetUserIdByAccountId` does +- [sceNpCheckPlus](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the `(request id, parameter, result)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp#L246): request id 0 or a null parameter or result returns `SCE_NP_ERROR_INVALID_ARGUMENT`, and every other call returns `SCE_NP_ERROR_SIGNED_OUT` without writing the result, as `sceNpCheckPremium` does. The parameter's size and feature fields, which shadPS4 checks, are not read: the errors the console returns for them are not known +- [sceNpGetAccountId, sceNpSetGamePresenceOnline](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - PS4 forms; the `(online id, account id output)` and `(request id, online id, presence)` signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp#L1092-L1096). A null online id or output, or request id 0 for `sceNpSetGamePresenceOnline`, returns `SCE_NP_ERROR_INVALID_ARGUMENT`. `sceNpSetGamePresenceOnline` then returns `SCE_NP_ERROR_REQUEST_NOT_FOUND` for a request that was never created or was deleted, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c1b26558352b863f80142256e4df38585aa7d727/src/core/libraries/np/np_manager.cpp#L1098-L1104) does. Otherwise both return `SCE_NP_ERROR_SIGNED_OUT` without writing, as `sceNpGetAccountIdA` does. The console's result for a signed-out user is not measured - [NP reachability callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) retain one callback and its user data; duplicate registration and unregistering an absent callback return the errors used by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/np/np_manager.cpp). No callback is delivered because the existing NP model remains signed out with reachability unavailable. -- [sceNpCheckNpAvailability](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the `(req_id, online_id)` signature and the errors follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/np/np_manager.cpp) with no user signed in to PSN: a null online id is `SCE_NP_ERROR_INVALID_ARGUMENT`, and any other online id matches no user, `SCE_NP_ERROR_USER_NOT_FOUND`; the request id is not checked, as in the other NpManager requests +- [sceNpCheckNpAvailability](../../../core/libs/prx/libSceNpManager/Export.cpp): native NP initialization and online-ID lookup are not modeled. The offline host returns user-not-found for valid arguments after clearing the optional one-byte availability output; request ID 0 and a null online-ID pointer return invalid-argument before writing the output. +- [NP state callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the registration rules are [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L857-L901)'s: `sceNpRegisterStateCallbackA` keeps up to 8 callbacks and returns the slot plus 1, a null callback returns `INVALID_ARGUMENT`, a callback already registered `CALLBACK_ALREADY_REGISTERED` and a ninth one `CALLBACK_MAX` (0x8055001D, [np_error.h](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_error.h#L38)); `sceNpUnregisterStateCallbackA` returns `INVALID_ARGUMENT` for an id outside 1..8 and `CALLBACK_NOT_REGISTERED` for an empty slot; the legacy [sceNpRegisterStateCallback](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L981-L1005) keeps one callback with the same errors. Each registration gets `SIGNED_OUT` for the initial user (0x10000000) once, from the next `sceNpCheckCallback` on the calling thread. For the A form this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3959-L3974), whose [sceNpRegisterStateCallbackA](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4021-L4034) marks the registration pending. KytyPS5 never calls the legacy callback ([sceNpRegisterStateCallback](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4009-L4019) stores nothing); it is called as `(user, SIGNED_OUT, nullptr, userdata)`, as in shadPS4, which passes an NpId only for `SIGNED_IN` ([L823](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L823), [L938-L940](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L938-L940)). shadPS4 instead queues `SIGNED_OUT` from the user service login event and delivers it from `sceNpCheckCallback` ([DispatchPendingNpStateCallbacks](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L903-L962)); the reachability callback is not given an initial `UNAVAILABLE`, unlike that function ([L922-L961](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L922-L961)). A callback being delivered when another thread unregisters it is still called once. When and how often the console calls the state callbacks, and the errors, are not measured +- [sceNpRegisterGamePresenceCallback](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - accepts any callback, null included, and returns nothing; no game presence event is delivered because the NP model remains signed out. [Kyty](https://github.com/InoriRus/Kyty/blob/4733b7e1c91b10554a52007903d74dc76c39a230/source/emulator/src/Network.cpp#L1549-L1552) ignores the callback the same way, and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_manager.cpp#L1121-L1126) keeps the last one without calling it. Not measured on a PS5 +- [sceNpIsPlusMember and sceNpSetContentRestriction](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the signatures and checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_manager.cpp): a null output or restriction is `INVALID_ARGUMENT`, a restriction size other than 0x18 is `INVALID_SIZE` (0x80550011), and a negative default age, more than 256 age restrictions or a count without an array is `INVALID_ARGUMENT`. Any user is reported as not a Plus member, and a valid restriction is accepted and not stored. Not measured on a PS5 + +- [sceNpCheckCallbackForLib](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - not measured on a PS5: it delivers the pending state callbacks like `sceNpCheckCallback`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_manager.cpp) does with its pending state events; shadPS4's `sceNpCheckCallback` also runs other registered NP callbacks, which this library does not keep 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/libSceNpParty.md b/docs/dev/debt/libSceNpParty.md new file mode 100644 index 0000000000..e88c71d92d --- /dev/null +++ b/docs/dev/debt/libSceNpParty.md @@ -0,0 +1,8 @@ +# libSceNpParty technical debt + +### Unknown function info + +- [sceNpPartyGetState and sceNpPartyGetStateAsUserA](../../../core/libs/prx/libSceNpParty/Export.cpp) (libSceNpParty) - the 16-bit state, its `NOT_IN_PARTY` value 2 and the `INVALID_ARGUMENT` (`0x80552504`) error follow shadPS4's [np_party.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_party.h) and [np_error.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_error.h) (PS4); shadPS4 does not check for a null state, and whether the console needs an initialized party library first is not known. Not measured on a PS5 +- [sceNpPartyGetId, sceNpPartyGetMembersA, sceNpPartyGetMemberInfoA, sceNpPartyGetMemberSessionInfo, sceNpPartyGetMemberVoiceInfo and sceNpPartySendBinaryMessage](../../../core/libs/prx/libSceNpParty/Export.cpp) (libSceNpParty) - the user is never in a party, so these return `NOT_IN_PARTY` (`0x80552506`, shadPS4's np_error.h) and write nothing, or `INVALID_ARGUMENT` for a null pointer; the signatures are from [Orbital's type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json). Like `sceNpPartyGetState` they do not require `sceNpPartyInitialize`, and the order of the null and party checks on the console is not known. Not measured on a PS5 +- [sceNpPartyInitialize, sceNpPartyTerminate, sceNpPartyCheckCallback and sceNpPartyRegisterHandlerA](../../../core/libs/prx/libSceNpParty/Export.cpp) (libSceNpParty) - an initialized flag with `ALREADY_INITIALIZED` (`0x80552502`) and `NOT_INITIALIZED` (`0x80552503`) from shadPS4's np_error.h; the handlers are accepted but never called, since no party event can happen. Whether the console refuses a null initialize parameter, and which check comes first, is not known +- [sceNpPartyCreate, sceNpPartyCreateA, sceNpPartyGetMemberInfo, sceNpPartyGetMembers, sceNpPartyGetStateAsUser, sceNpPartyGetVoiceChatPriority, sceNpPartyJoin, sceNpPartyLeave, sceNpPartyRegisterHandler, sceNpPartyRegisterPrivateHandler, sceNpPartySetVoiceChatPriority, sceNpPartyShowInvitationList, sceNpPartyShowInvitationListA and sceNpPartyUnregisterPrivateHandler](../../../core/libs/prx/libSceNpParty/Export.cpp) (libSceNpParty) - Orbital has no prototypes for these and no credible ones were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceNpProfileDialog.md b/docs/dev/debt/libSceNpProfileDialog.md new file mode 100644 index 0000000000..2755456503 --- /dev/null +++ b/docs/dev/debt/libSceNpProfileDialog.md @@ -0,0 +1,5 @@ +# libSceNpProfileDialog technical debt + +### Unknown function info + +- [sceNpProfileDialogClose, sceNpProfileDialogGetResult, sceNpProfileDialogGetStatus, sceNpProfileDialogInitialize, sceNpProfileDialogOpen, sceNpProfileDialogOpenA, sceNpProfileDialogTerminate and sceNpProfileDialogUpdateStatus](../../../core/libs/prx/libSceNpProfileDialog/Export.cpp) (libSceNpProfileDialog) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpProfileDialog.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceNpRemotePlaySessionSignaling.md b/docs/dev/debt/libSceNpRemotePlaySessionSignaling.md new file mode 100644 index 0000000000..98a7efb65f --- /dev/null +++ b/docs/dev/debt/libSceNpRemotePlaySessionSignaling.md @@ -0,0 +1,5 @@ +# libSceNpRemotePlaySessionSignaling technical debt + +### Unknown function info + +- [sceNpRemotePlaySessionSignalingCreateJoinSession, sceNpRemotePlaySessionSignalingGetConnectionStatus, sceNpRemotePlaySessionSignalingGetCustomProperty, sceNpRemotePlaySessionSignalingGetLocalNetInfo, sceNpRemotePlaySessionSignalingGetMemoryInfo, sceNpRemotePlaySessionSignalingInitialize, sceNpRemotePlaySessionSignalingJoinSession, sceNpRemotePlaySessionSignalingLeaveSession, sceNpRemotePlaySessionSignalingSetCustomProperty and sceNpRemotePlaySessionSignalingTerminate](../../../core/libs/prx/libSceNpRemotePlaySessionSignaling/Export.cpp) (libSceNpRemotePlaySessionSignaling) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpRemotePlaySessionSignaling.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceNpSessionSignaling.md b/docs/dev/debt/libSceNpSessionSignaling.md index 9f9b7ad514..f6a89b9950 100644 --- a/docs/dev/debt/libSceNpSessionSignaling.md +++ b/docs/dev/debt/libSceNpSessionSignaling.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceNpSessionSignalingGetConnectionFromPeerAddress](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - imported by PPSA01289 and added without an implementation; unknown signature -- [sceNpSessionSignalingGetMemoryInfo](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature -- [sceNpSessionSignalingGetConnectionStatistics](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature -- [sceNpSessionSignalingGetConnectionStatus](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - `(int32 context id, int32 connection id, int32* status, SceNetInAddr* peer address, uint16* peer port)` is ASTRO BOT's (PPSA21564 01.007.000) call at 0x30622B: the context id from a 32-bit field, the connection id, three stack outputs, a negative result taken as an error, then the address passed to `sceNetInetNtop(AF_INET, ...)` and the port to `sceNetNtohs`. It matches PS4's `sceNpSignalingGetConnectionStatus`. The title reaches it only for a connection id from the signaling library, which offline never hands one out. No session ever activates offline, so every connection id answers `CONN_NOT_FOUND` (0x8055330C, from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/64d198190fa05d37cbb1fa29c50f967c376fce55/src/core/libraries/np/np_error.h)) without writing the outputs, and a null status answers `INVALID_ARGUMENT`. The context id is not checked, as contexts are not tracked. The console's answer for an unknown context or connection is unverified +- [sceNpSessionSignalingInitialize, sceNpSessionSignalingCreateContext and sceNpSessionSignalingCreateContext2](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) - the initialization layout is 40 bytes; both context layouts are 56 bytes with callback and user-data pairs. The second context variant adds a group identifier to its connection callback. Pool allocation, callback delivery and underlying NP account validation are not implemented; priority and affinity settings do not affect host scheduling. + +- [sceNpSessionSignalingGetConnectionStatus, sceNpSessionSignalingActivateSession and sceNpSessionSignalingActivateUser](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) - initialization, context lookup and output validation follow the system library. Connections never activate in the offline runtime; activation returns unavailable and a connection query on a valid context returns connection-not-found without writing outputs. SDK-dependent peer-address validation and local-network discovery remain incomplete. +- [sceNpSessionSignalingCreateJoinActivate, sceNpSessionSignalingGetConnectionFromNetAddress, sceNpSessionSignalingGetCustomProperty, sceNpSessionSignalingGetGroupFromPeerAddress, sceNpSessionSignalingInitializeWithPort, sceNpSessionSignalingJoinActivate and sceNpSessionSignalingSetCustomProperty](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpSessionSignaling.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. diff --git a/docs/dev/debt/libSceNpSns.md b/docs/dev/debt/libSceNpSns.md new file mode 100644 index 0000000000..9f14bbcc3a --- /dev/null +++ b/docs/dev/debt/libSceNpSns.md @@ -0,0 +1,8 @@ +# libSceNpSns technical debt + +### Unknown function info + +- [sceNpSnsFacebookAbortRequest, sceNpSnsFacebookCreateRequest, sceNpSnsFacebookDeleteRequest, sceNpSnsFacebookGetAccessToken, sceNpSnsIntAbortRequest, sceNpSnsIntCreateRequest, sceNpSnsIntDeleteRequest, sceNpSnsIntFbGetGameAccessToken, sceNpSnsIntFbGetGameAccessTokenAllowed, sceNpSnsIntFbGetSystemAccessToken, sceNpSnsIntFbGetTitleInfo, sceNpSnsIntLinkedStatus, sceNpSnsIntTwGetSystemAccessToken, sceNpSnsIntUnlink, sceNpSnsIntYtGetAccessToken, sceNpSnsIntYtRefreshMasterToken, sceNpSnsTwitchAbortRequest, sceNpSnsTwitchCreateRequest, sceNpSnsTwitchDeleteRequest, sceNpSnsTwitchGetAccessToken, sceNpSnsYouTubeAbortRequest, sceNpSnsYouTubeCreateRequest, sceNpSnsYouTubeDeleteRequest and sceNpSnsYouTubeGetAccessToken](../../../core/libs/prx/libSceNpSns/Export.cpp) (libSceNpSns) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpSns.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified + +### Inventory exports declared elsewhere + sceNpSnsFacebookCreateRequest, sceNpSnsFacebookDeleteRequest and sceNpSnsFacebookGetAccessToken are also 3.20 exports of this module but are currently declared by #6977 in libSceNpSnsFacebookDialog, so they are omitted here to respect the duplicate-export rule; re-add if that PR's module attribution is rejected. diff --git a/docs/dev/debt/libSceNpSnsFacebookDialog.md b/docs/dev/debt/libSceNpSnsFacebookDialog.md new file mode 100644 index 0000000000..3009c1d373 --- /dev/null +++ b/docs/dev/debt/libSceNpSnsFacebookDialog.md @@ -0,0 +1,5 @@ +# libSceNpSnsFacebookDialog technical debt + +### Unknown function info + +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceNpSnsFacebookDialogClose`](../../../core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp), [`sceNpSnsFacebookDialogGetStatus`](../../../core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp). diff --git a/docs/dev/debt/libSceNpSnsYouTubeDialog.md b/docs/dev/debt/libSceNpSnsYouTubeDialog.md new file mode 100644 index 0000000000..cbd7cf8126 --- /dev/null +++ b/docs/dev/debt/libSceNpSnsYouTubeDialog.md @@ -0,0 +1,5 @@ +# libSceNpSnsYouTubeDialog technical debt + +### Unknown function info + +- [sceNpSnsYouTubeDialogClose, sceNpSnsYouTubeDialogGetResult, sceNpSnsYouTubeDialogGetStatus, sceNpSnsYouTubeDialogInitialize, sceNpSnsYouTubeDialogOpen, sceNpSnsYouTubeDialogTerminate and sceNpSnsYouTubeDialogUpdateStatus](../../../core/libs/prx/libSceNpSnsYouTubeDialog/Export.cpp) (libSceNpSnsYouTubeDialog) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpSnsYouTubeDialog.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceNpTcs.md b/docs/dev/debt/libSceNpTcs.md new file mode 100644 index 0000000000..3f4822e6e5 --- /dev/null +++ b/docs/dev/debt/libSceNpTcs.md @@ -0,0 +1,5 @@ +# libSceNpTcs technical debt + +### Unknown function info + +- [sceNpTcsIntDownloadData, sceNpTcsIntUploadDataToNewSlotAbort and sceNpTcsIntUploadDataToNewSlotBegin](../../../core/libs/prx/libSceNpTcs/Export.cpp) (libSceNpTcs) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpTcs.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceNpTrophy.md b/docs/dev/debt/libSceNpTrophy.md new file mode 100644 index 0000000000..c445f4e8d8 --- /dev/null +++ b/docs/dev/debt/libSceNpTrophy.md @@ -0,0 +1,5 @@ +# libSceNpTrophy technical debt + +### Unknown function info + +- [sceNpTrophyRegisterContext, sceNpTrophyGetTrophyUnlockState, sceNpTrophyUnlockTrophy](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy, PS4) - the trophy definitions in `TROPCONF.ESFM` are encrypted, so the trophy count is the number of consecutive `TROPnnn.PNG` icons in the title's `/app0/sce_sys/trophy/trophyNN.trp`, and the calls that return names and details (`sceNpTrophyGetGameInfo`, `sceNpTrophyGetGroupInfo`, `sceNpTrophyGetTrophyInfo`) throw. Unlocks are kept per user and service label in `_trophy/trophyNN.` under the working directory, no platinum trophy is reported (`platinumId` is -1), and a locked trophy's icon fails with `TROPHY_NOT_UNLOCKED`. None of it is checked on a console. diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index 8e1fa7ef7d..e6cb5cd197 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -2,4 +2,20 @@ ### 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 + + +- [libSceNpTrophy2 pending exports](../../../core/libs/prx/libSceNpTrophy2/Export.cpp) - 23 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpTrophy2.c). +### Functional + +- [Trophy queries](../../../core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp) - not confirmed on the console: `sceNpTrophy2GetTrophyInfo` of a trophy id that is not in the title's [trophy data](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp) (`sce_sys/trophy2/trophyNN.ucp`) returns `0x8055390A` (KytyPS5's `NP_TROPHY2_ERROR_INVALID_TROPHY_ID`) and writes nothing; a context not created or destroyed reports placeholders and no unlocks and returns success; hidden trophies report their names and descriptions before the unlock; `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). A group that is not in the data throws. Without the file, or when it is malformed, the queries report the old placeholders and `sceNpTrophy2GetTrophyInfoArray` lists only trophy 0. Icons are not read. Unlocks come from the [trophy store](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp). +- [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/libSceNpTus.md b/docs/dev/debt/libSceNpTus.md new file mode 100644 index 0000000000..cb413bbf34 --- /dev/null +++ b/docs/dev/debt/libSceNpTus.md @@ -0,0 +1,34 @@ +# libSceNpTus technical debt + +### Unknown function info + +- [TUS variable outputs](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the layouts of the variable arrays of the `sceNpTusGetMultiSlotVariable*` calls and of `outVariable` in `sceNpTusAddAndGetVariableAAsync` are not verified, so the output is never written and `variableArraySize` / `outVariableSize` are not checked; shadPS4, and RPCS3 in its PS3 `sceNpTusGetMultiSlotVariable*` and `sceNpTusAddAndGetVariable*` calls, return `INVALID_ALIGNMENT` unless the size is `arrayLen` variables of the call's type, or one variable for `outVariable`. The signatures and the community error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/tree/main/src/core/libraries/np/np_tus), which fpPS4 agrees with for the context and request calls; no console measurement. +- [Multi-slot variable calls](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the argument checks of the multi-slot get, set and delete calls are not measured on a console; they follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c1b26558352b863f80142256e4df38585aa7d727/src/core/libraries/np/np_tus/np_tus.cpp), where they differ between functions: `arrayLen < 1` is `INSUFFICIENT_ARGUMENT` in the NpId and virtual-user delete calls and in `sceNpTusGetMultiSlotVariableVUserAsync`, and `INVALID_ARGUMENT` in the others; a null NpId is accepted except in delete; an empty virtual user ID is `INVALID_ARGUMENT`. RPCS3's [PS3 module](https://github.com/RPCS3/rpcs3/blob/1a9dca3a3a84a18df866e3021918322bd295160a/rpcs3/Emu/Cell/Modules/sceNpTus.cpp) has only the NpId and virtual-user calls; it returns `INVALID_ARGUMENT` for an empty array in all of them, rejects a null target in get and set and does not check for an empty virtual user ID. +- [Variable, data, data status and TSS requests](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the argument checks of the 82 add-and-get, try-and-set, data, data status, multi-user, friends and `sceNpTssGetSmallStorage` calls are not measured on a console; their signatures, checks and order follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_tus/np_tus.cpp), including its differences between forms: the NpId try-and-set calls return `INVALID_ARGUMENT` for an operation type outside 1-6 where the others return `INVALID_TYPE`, and the NpId and virtual-user multi-slot data status calls return `TOO_MANY_NPID` for more than 64 slots. As for the calls above, the output and structure sizes are not checked, and every request finishes with `SCE_NP_ERROR_SIGNED_OUT` +- [sceNpTusSetThreadParam](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusCreateTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetTimeout](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusChangeModeForOtherSaveDataOwners](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetStorage](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetStorageAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteNpTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the PS5 signature, error codes and child-request lifetime have not been measured on a console. Deletion invalidates the context and all its requests, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e7626e3c7de8d0f0fda6a91f620b42f6c762/src/core/libraries/np/np_tus/np_tus.cpp#L158-L180). + +### Functional + +- [sceNpTusCreateNpTitleCtxA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - creates a title context without checking sign-in, up to 32 contexts (`TOO_MANY_OBJECTS` beyond); shadPS4 returns the `sceNpGetNpId` error instead. Requests are limited to 256, and the limits are not measured on a console. +- [sceNpTssCreateNpTitleCtx and sceNpTssCreateNpTitleCtxA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - that TSS title contexts share the TUS context table and its 32-context limit is inferred from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/core/libraries/np/np_tus/np_tus.cpp#L146-L168), which redirects both to the TUS creators, and from TSS requests coming from `sceNpTusCreateRequest`; nothing is measured on a console. The `A` form is `sceNpTusCreateNpTitleCtxA`. The `NpId` form returns `INSUFFICIENT_ARGUMENT` for a null `npId` and `INVALID_ARGUMENT` for service label `0xFFFFFFFF` before the limit, as shadPS4's [sceNpTusCreateNpTitleCtx](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/core/libraries/np/np_tus/np_tus.cpp#L122-L143) does; the signatures are shadPS4's and fpPS4's, and Orbital's for the `A` form. +- [sceNpTusPollAsync, sceNpTusWaitAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - there is no PSN, so every implemented `*Async` call whose arguments pass the checks starts a request that finishes at once with `SCE_NP_ERROR_SIGNED_OUT`, which poll and wait report and the blocking calls return; shadPS4 returns `SIGNED_OUT` from the `*Async` call itself when no shadNet client is signed in, and RPCS3 returns `INVALID_ONLINE_ID` while offline, marked there as probable behaviour. An abort stays set, so every later operation on the request, blocking ones included, finishes with `ABORTED`; in shadPS4 an abort only ends the operation in progress. Until an operation has started, poll returns 1 and wait returns `INVALID_ARGUMENT` as in shadPS4; RPCS3 [blocks until a result is set](https://github.com/RPCS3/rpcs3/blob/1a9dca3a3a84a18df866e3021918322bd295160a/rpcs3/Emu/NP/np_contexts.cpp#L40-L52). diff --git a/docs/dev/debt/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/libSceNpWebApi.md b/docs/dev/debt/libSceNpWebApi.md new file mode 100644 index 0000000000..95fb314c74 --- /dev/null +++ b/docs/dev/debt/libSceNpWebApi.md @@ -0,0 +1,6 @@ +# libSceNpWebApi technical debt + +### Unknown function info + +- [sceNpWebApiInitialize, sceNpWebApiTerminate, sceNpWebApiCreateHandle, sceNpWebApiDeleteHandle, sceNpWebApiCreatePushEventFilter, sceNpWebApiDeletePushEventFilter](../../../core/libs/prx/libSceNpWebApi/Export.cpp) (libSceNpWebApi, PS4) - signatures, id ranges (library contexts 1 to 0x7fff, handles and filters below 0xf0000000) and errors (`LIB_CONTEXT_NOT_FOUND`, `HANDLE_NOT_FOUND`, `PUSH_EVENT_FILTER_NOT_FOUND`, `INVALID_ARGUMENT` for a missing filter parameter) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api/np_web_api_internal.cpp); they are not checked on a console. The libHttp context and pool size are kept but not used, since no request is ever sent. Limbo (CUSA01664) creates a context, a handle and filters when its game starts. +- [sceNpWebApiCreateServicePushEventFilter](../../../core/libs/prx/libSceNpWebApi/Export.cpp) (libSceNpWebApi, PS4) - a filter for a PSN service needs the signed-in user, and no user is signed in to PSN, so a valid call returns `SIGNED_IN_USER_NOT_FOUND` after the context and handle checks, as shadPS4 does with its network disabled. shadPS4 also rejects an invalid service label for titles built with SDK 2.00 or later; AnyPS5 has no compiled-SDK version for libraries yet, so such a call returns `SIGNED_IN_USER_NOT_FOUND` instead of `INVALID_ARGUMENT`. `sceNpWebApiDeleteServicePushEventFilter` therefore never finds a filter. diff --git a/docs/dev/debt/libSceNpWebApi2.md b/docs/dev/debt/libSceNpWebApi2.md index 9e8e3461c6..7c73e05da4 100644 --- a/docs/dev/debt/libSceNpWebApi2.md +++ b/docs/dev/debt/libSceNpWebApi2.md @@ -2,10 +2,15 @@ ### Silent stubs +- [sceNpWebApi2AddWebTraceTag](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - the `(request id, value)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api2/np_web_api2.cpp). A title imports it. It validates that the value pointer is not null (returning `INVALID_ARGUMENT` 0x80553402), accepts any request id, and stores nothing: no request reaches a service, as `sceNpWebApi2SendRequest` fails with `UNAVAILABLE` at once, so trace tags are not transmitted offline - [sceNpWebApi2SetRequestTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - the `(request id, timeout)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api2/np_web_api2.cpp). Pinball FX (PPSA08755) calls it while its main menu loads. It succeeds for any request id and stores nothing: no request reaches a service, as `sceNpWebApi2SendRequest` fails with `UNAVAILABLE` at once, so no timeout can expire. shadPS4 returns `LIB_CONTEXT_NOT_FOUND`, `USER_CONTEXT_NOT_FOUND` or `REQUEST_NOT_FOUND` for an id it does not know; this library keeps no context or request table (`sceNpWebApi2CreateRequest` hands out counter values, and `sceNpWebApi2AbortRequest`, `sceNpWebApi2AddHttpRequestHeader` and `sceNpWebApi2DeleteRequest` also accept any id), so those errors are not reported until the request functions track their ids together ### Unknown function info -- [sceNpWebApi2PushEventAbortHandle](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2, NID `1OLgvahaSco`) is a throwing stub. The two signed 32-bit handle arguments and return type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/np/np_web_api2/np_web_api2.cpp) (PS4); the PS5 signature and abort behavior are unverified. PPSA01736 imports it. -- [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 +- [sceNpWebApi2AddMultipartPart, sceNpWebApi2IntCreateRequest, sceNpWebApi2IntPushEventCreateCtxIndFilter and sceNpWebApi2SetMultipartContentType](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpWebApi2.c); no-argument prototypes follow [shadPS4's PS4 stubs](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_web_api2/np_web_api2.cpp), not measured on a console. These declarations have no parameters solely to resolve known imports and make no ABI claim. +- [sceNpWebApi2InitializeForPresence, sceNpWebApi2IntInitialize and sceNpWebApi2IntInitialize2](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - create a library context like `sceNpWebApi2Initialize`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L174-L222) does on the PS4: the argument structures (32 and 40 bytes) and the `struct_size` check that returns `SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT` follow its [declarations](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_web_api2/np_web_api2.h#L15-L31). The context type shadPS4 records (2 for the internal initializers, 3 for presence), the context name and the push configuration group are not kept, and the 0x8000 context limit is not applied. Not measured on a PS5 +- [sceNpWebApi2CreateMultipartRequest and sceNpWebApi2SendMultipartRequest](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpWebApi2.c). The signatures follow the PS4 implementations in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L52-L221) and [its multipart sender](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L388-L399); opaque struct pointers use `const void*` or `void*` to preserve source constness. These prototypes are not measured on a console. +- [sceNpWebApi2IntPushEventCreateDestPs4CtxIndFilter](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - no credible prototype was found; it is declared without parameters solely to resolve known imports and makes no ABI claim. +- [Push handle lifecycle](../../../core/libs/prx/libSceNpWebApi2/Export.cpp): PS5 create/delete/abort signatures and invalid-context/invalid-handle errors are confirmed. Handles track ownership, timeout and abort state; deletion and context termination remove them. Network push operations and cancellation of active requests remain unimplemented. +- [sceNpWebApi2GetMemoryPoolStats](../../../core/libs/prx/libSceNpWebApi2/Export.cpp): PS5 confirms the context/statistics arguments and 32-byte output. An invalid context returns `0x80553404` without changing output; a valid context with null output succeeds. Allocator accounting remains absent: peak and current usage still report zero instead of native pool allocations. +- [sceNpWebApi2PushEventSetHandleTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp): PS5 confirms three 32-bit arguments, ownership validation and storing the entire timeout value without range checks. The stored timeout is not consumed because asynchronous push operations are unimplemented. diff --git a/docs/dev/debt/libSceOpusCeltDec.md b/docs/dev/debt/libSceOpusCeltDec.md index 0a45940eed..2b3827fa86 100644 --- a/docs/dev/debt/libSceOpusCeltDec.md +++ b/docs/dev/debt/libSceOpusCeltDec.md @@ -2,4 +2,10 @@ ### Unknown function info -- [sceOpusCeltDecCreateEx, sceOpusCeltDecDecode, sceOpusCeltDecDestroy, sceOpusCeltDecGetSize, sceOpusCeltDecInitialize, sceOpusCeltDecTerminate](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - imported by PPSA99001, which fails to load without the module; added without an implementation. Their signatures match libSceOpusDec's in [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). PPSA99001 opens this decoder only as a fallback when libSceOpusDec reports a retryable error for a CELT packet, which [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp) here never returns, so the fallback is not reached +- [sceOpusCeltDecCreate, sceOpusCeltDecCtl, sceOpusCeltDecDecodeFloat, sceOpusCeltDecDecodeFloatWithPriority, sceOpusCeltDecDecodeWithPriority](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusCeltDec.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. +- [sceOpusCeltDecTerminate](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the non-null path forwards the AJM finalization result, but the null path returns without setting the return register. Its source return type cannot be established from this callee alone. The existing integer return is retained. + +### Functional + +- [libSceOpusCeltDec](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the supplied module confirms the six exported argument lists, read-only context inputs, 640-byte state allocation, mono/stereo channels, five sample rates, byte-sized decode capacity and byte-count return. The implementation still keeps decoder state outside guest memory and uses host contexts instead of AJM contexts. AJM error propagation, resource ownership and termination with live decoders are not implemented. Wrapper validation errors and their precedence are recovered. +- [sceOpusCeltDecDecode](../../../core/libs/prx/libSceOpusCeltDec/Export.cpp): the original forwards packets to AJM codec 16 without a local CELT-only check. The backend response to SILK/hybrid packets is unresolved; these inputs still throw. Packet loss concealment, asynchronous AJM execution, exact PCM conversion and resampler buffering remain outside the verified behavior of the FFmpeg implementation in [OpusDecoderLibrary](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp). diff --git a/docs/dev/debt/libSceOpusCeltEnc.md b/docs/dev/debt/libSceOpusCeltEnc.md new file mode 100644 index 0000000000..6ef2fceb48 --- /dev/null +++ b/docs/dev/debt/libSceOpusCeltEnc.md @@ -0,0 +1,5 @@ +# libSceOpusCeltEnc technical debt + +### Unknown function info + +- [sceOpusCeltEncTerminate](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp): the context-ID pointer is confirmed. The null-input path does not define the return register, so an integer return contract is not established. The current integer return declaration is retained because changing it to `void` would require altering the protected stub body, which contains `return 0`. diff --git a/docs/dev/debt/libSceOpusDec.md b/docs/dev/debt/libSceOpusDec.md index 74c417e67e..6dbf108315 100644 --- a/docs/dev/debt/libSceOpusDec.md +++ b/docs/dev/debt/libSceOpusDec.md @@ -1,6 +1,11 @@ # libSceOpusDec technical debt +### Unknown function info + +- [sceOpusDecCancel, sceOpusDecCreate, sceOpusDecCtl, sceOpusDecDecodeFloat, sceOpusDecDecodeFloatWithPriority, sceOpusDecDecodeFloatWithPriorityAndTimeout, sceOpusDecDecodeWithPriority and sceOpusDecDecodeWithPriorityAndTimeout](../../../core/libs/prx/libSceOpusDec/Export.cpp) (libSceOpusDec) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusDec.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. + ### Functional -- [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for 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 supplied module confirms the exported arguments, read-only context inputs, 640-byte state size and creation rates. Validation errors and their precedence are recovered. Console PCM equivalence, AJM failures, context ownership and termination with live decoders remain unimplemented. Packet loss concealment still throws; decoder state is kept in host registries instead of the native guest state layout. - [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/libSceOpusSilkEnc.md b/docs/dev/debt/libSceOpusSilkEnc.md new file mode 100644 index 0000000000..266dfe90fd --- /dev/null +++ b/docs/dev/debt/libSceOpusSilkEnc.md @@ -0,0 +1,5 @@ +# libSceOpusSilkEnc technical debt + +### Unknown function info + +- [sceOpusSilkEncTerminate](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp): the context-ID pointer is confirmed. The null-input path does not define the return register, so an integer return contract is not established. The current integer return declaration is retained because changing it to `void` would require altering the protected stub body, which contains `return 0`. diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 04435c2849..c8fa44cf9d 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -1,11 +1,30 @@ # libScePad technical debt +### Silent stubs + +- [scePadEnableExtensionPort](../../../core/libs/prx/libScePad/Export.cpp) returns success without changing extension-port state. The boolean argument is recovered; connected-device behavior is not modeled. + ### Unknown function info +- [libScePad profile and device-setting exports](../../../core/libs/prx/libScePad/Export.cpp) - profile getters, tuning, saved settings, USB state, shared output and virtual-device setting arguments are recovered. Hunt and Leonardo reset each consume only a handle; physical VR USB status consumes only an output pointer. Remote settings output is 64 bytes and license-controller information is 1224 bytes. Kernel ioctl payload fields remain incomplete. + +- [libScePad report and tracking exports](../../../core/libs/prx/libScePad/Export.cpp) - read, extended-read, tracker, UI, VR-controller and report argument registers are recovered. Feature reports use a byte report ID and 64-bit byte count, capped at 64. VR trigger effects consume a 56-byte record; the dual-controller call uses two handles and a mask with records at offsets 8 and 64. Remaining payload field layouts use incomplete declarations, including extended samples and kernel-owned extension reports. + +- [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - the name is unknown, and the 0 returned after `scePadInit` is inferred, not measured. In the OS 10.01 system module the function takes no arguments, returns `0x80920005` before `scePadInit`, and otherwise sends a per-process value of 1 to ShellCore over Mbus, returning `0x809200ff` if that call fails and 0 if it succeeds, with or without an open handle. The #3177 probe measured `0x809200ff` in a process that is not a game; a game process is assumed to get 0. The ShellCore setting it changes is unknown and not modelled - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per library rather than per handle since one pad handle exists - [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - special ports (type 2) return `DEVICE_NOT_CONNECTED` (0x80920007) because the host has no special controller, and `scePadGetHandle` has no handle for them; whether the console opens a handle and reports the device as disconnected later is unknown. scePadOpenExt handles only special ports. PPSA01317 passes a 16-byte param that names the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw -- [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `iA-DdobUen8`) - the name hashes to the imported NID; the `(int handle, void* state)` signature follows [PS5VR](https://github.com/theghostonline/PS5VR/blob/0e408909ce7a39891a2e06eee3438d07a09e11e3/app/src/app_vr.c#L326-L369) (PPSA99177), which passes a PS VR2 Sense controller handle from `scePadOpen` type 3 or 4 and a 0x68-byte state it reads as buttons at 0, stick bytes at 4 and a connected byte at 0x40. The title calls it only after `sceHmd2Initialize` succeeds, which it does not here, so every call throws; the state layout is otherwise unknown -- [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 0 to 3 are accepted and stored; the meaning of mode 3 is unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect +- [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) - the 104-byte output layout is recovered, including buttons, stick, triggers, touch fields, motion, connection state and timestamp. Touch-value semantics and kernel report production remain incomplete; the export still throws. +- [scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) - the two handle arguments and 128-byte output are recovered; output field semantics remain incomplete. +- [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 +- [scePadIsDS4Connected, scePadIsBlasterConnected and scePadIsMoveConnected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle)` signatures are recovered; the library reports one DualSense to every title (see Touchpad resolution below), so no DualShock 4, Blaster or Move controller is ever behind a handle and each call answers false for any handle. The console's answer for an invalid handle is not measured - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled -- [scePadUnknown_zBsgF0q8DIM](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `zBsgF0q8DIM`) - PPSA03671 imports it and fails to load without it. The aerolib list has no name for the NID, and none of the pad names in SharpProspero, shadPS4 or OpenOrbis, nor the trigger effect, motion sensor, VR controller, HD and haptic names tried, hashes to it. It is declared without parameters and throws. The title's executable has an import stub for it but no call, jump or address reference to that stub, so the argument count and whether the result is used are unknown +- [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown + +- [libScePad internal data and device control](../../../core/libs/prx/libScePad/Export.cpp) - kernel-owned version, telemetry, Lotus-button and update payload fields remain unresolved. Lotus-button parameter units and the three blinking byte meanings are unknown. Port information occupies 152 bytes; history records occupy 72 bytes; Leonardo unique data occupies 24 bytes. Their complete field semantics remain unresolved. + +- [libScePad profile writes and virtual devices](../../../core/libs/prx/libScePad/Export.cpp) - profile writes take a handle, profile ID, profile pointer and a 32-bit result pointer; the profile payload direction and fields remain unresolved. Virtual-device creation takes an input record and an output ID pointer, whose width is not exposed by the wrapper. Component information copies 16 bytes; field meanings remain unknown. Virtual trigger state contains two 32-bit states and three control bytes, but the control semantics remain incomplete. + +### Functional + +- [Touchpad resolution](../../../core/libs/prx/libScePad/include/PadState.hpp) (libScePad) - the library reports a DualSense to every title, so the controller information reports its touchpad, 1920 by 1080 at 44.86 pixels per cm, and the host controller's fingers and the keyboard touch contacts are placed in that range. [duaLib](https://github.com/WujekFoliarz/duaLib/blob/0b39f92261e2ff0e77eb0dac00943be6970e0dbf/src/source/duaLib.cpp#L847-L849) reports these values for a DualSense and 1920 by 943 at 44 for a DualShock 4, and the Linux kernel's [hid-playstation](https://github.com/torvalds/linux/blob/cd4f494ba535e848e2d299707d363de84e61a518/drivers/hid/hid-playstation.c#L177-L178) holds 1920 by 1080 against 1920 by 942 for the DualShock 4. A DualShock 4 on the host reports through the DualSense range too, so its last touchpad rows are never reached. A PS5 on firmware 13.40 reported 1920 by 1080 at 44.86 and stick dead zones of 13 to a retail title diff --git a/docs/dev/debt/libScePadTracker.md b/docs/dev/debt/libScePadTracker.md new file mode 100644 index 0000000000..3111036706 --- /dev/null +++ b/docs/dev/debt/libScePadTracker.md @@ -0,0 +1,5 @@ +# libScePadTracker technical debt + +### Unknown function info + +- [scePadTrackerCalibrate, scePadTrackerGetWorkingMemorySize, scePadTrackerInit, scePadTrackerReadState, scePadTrackerTerm and scePadTrackerUpdate](../../../core/libs/prx/libScePadTracker/Export.cpp) (libScePadTracker) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePadTracker.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libScePatchCheckerClient.md b/docs/dev/debt/libScePatchCheckerClient.md new file mode 100644 index 0000000000..16461c497c --- /dev/null +++ b/docs/dev/debt/libScePatchCheckerClient.md @@ -0,0 +1,5 @@ +# libScePatchCheckerClient technical debt + +### Unknown function info + +- [scePatchCheckerCancel, scePatchCheckerCheckPatch, scePatchCheckerCreateHandler, scePatchCheckerDestroyHandler, scePatchCheckerDisableAutoDownload, scePatchCheckerEnableAutoDownload, scePatchCheckerGetApplicableTick, scePatchCheckerGetPackageInfo, scePatchCheckerInitialize, scePatchCheckerRequestCheckPatch, scePatchCheckerRequestCheckPatchByType and scePatchCheckerTerminate](../../../core/libs/prx/libScePatchCheckerClient/Export.cpp) (libScePatchCheckerClient) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePatchCheckerClient.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libScePlayGo.md b/docs/dev/debt/libScePlayGo.md index ecf89fb032..40a6135747 100644 --- a/docs/dev/debt/libScePlayGo.md +++ b/docs/dev/debt/libScePlayGo.md @@ -2,4 +2,7 @@ ### Unknown function info -- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts every chunk id as installed while `scePlayGoGetChunkId` lists only chunk 0, and a package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts only chunk 0 and returns `SCE_PLAYGO_ERROR_BAD_CHUNK_ID` for any other id; a `sce_sys` that holds other `playgo-*` files (`playgo-ficm.dat`, `playgo-hash-table.dat`) without `playgo-chunk.dat` or `playgo-scenario.json`, or a `playgo-chunk.dat` without a `plgx` header and a nonzero count, throws naming the file, because no such dump is known; the two answers above are unverified on a console, as is the console's answer for an id outside a package's chunk table. +- [scePlayGoRequestNextChunk, scePlayGoSnapshot](../../../core/libs/prx/libScePlayGo/Export.cpp) (libScePlayGo) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePlayGo.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim +- [scePlayGoSetLanguageMask](../../../core/libs/prx/libScePlayGo/Export.cpp) (libScePlayGo) - stores the mask that `scePlayGoGetLanguageMask` then reports, and returns `SCE_PLAYGO_ERROR_BAD_HANDLE` for another handle, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/playgo/playgo.cpp#L346-L359) does on the PS4. shadPS4 also returns `SCE_PLAYGO_ERROR_NOT_INITIALIZED` before `scePlayGoInitialize` and starts from the system language's mask; here the mask starts with every language and initialization is not tracked. Not measured on a PS5 +- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts every chunk id as installed, and `scePlayGoGetChunkId` lists ids 0 to 999. A title that builds its set of installed chunks from that list skips any chunk it does not see. The console's chunk limit is not known here, and a package with more chunks would see ids from 1000 up as not listed. A package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts chunk 0; querying any other id throws with a message naming the missing `playgo-chunk.dat` and `playgo-scenario.json`, since the incomplete dump cannot establish whether that id exists; `APS5_PLAYGO_UNLISTED_CHUNKS=installed` answers every such id as installed (`LOCAL_FAST`, which PPSA15388 needs to load its saves) and `=absent` answers `SCE_PLAYGO_ERROR_BAD_CHUNK_ID` (which ends PPSA03001's id walk), as the user's choice for that dump. A `sce_sys` that holds other `playgo-*` files (`playgo-ficm.dat`, `playgo-hash-table.dat`) without `playgo-chunk.dat` or `playgo-scenario.json`, or a `playgo-chunk.dat` without a `plgx` header and a nonzero count, throws naming the file, because no such dump is known. These answers are unverified on a console, as is the console's answer for an id outside a package's chunk table. +- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - `scePlayGoGetSupportedOptionalChunk` and `scePlayGoGetOptionalChunk` report every bit set for `type` 0, 1 or 2, i.e. every optional chunk (language pack) supported and installed, like `scePlayGoGetLanguageMask`. Native requests confirm types 0 through 2 and an eight-byte result; their meanings and bit layout are still determined by the external service, not this module; PPSA01748 calls both with type 0 and hides every text language listed in `playgo-scenario.json` `chunkSupportedLanguages` when the bitmask is 0. The console's answer for a package that ships no optional chunks is unverified. diff --git a/docs/dev/debt/libScePlayGoDialog.md b/docs/dev/debt/libScePlayGoDialog.md new file mode 100644 index 0000000000..53b4cd2147 --- /dev/null +++ b/docs/dev/debt/libScePlayGoDialog.md @@ -0,0 +1,5 @@ +# libScePlayGoDialog technical debt + +### Unknown function info + +- [scePlayGoDialogOpen](../../../core/libs/prx/libScePlayGoDialog/Export.cpp) does not validate or copy the native nested parameters or query download progress. The host UI stub immediately reports completion; service-driven failures and transitions remain unimplemented. diff --git a/docs/dev/debt/libScePlayReady.md b/docs/dev/debt/libScePlayReady.md new file mode 100644 index 0000000000..f3b7a6a60c --- /dev/null +++ b/docs/dev/debt/libScePlayReady.md @@ -0,0 +1,5 @@ +# libScePlayReady technical debt + +### Unknown function info + +- [sceBufferAlloc, sceBufferDup, sceBufferFree, sceBufferInit, sceBufferRealloc, scePlayReadyApiInitializeInitParams, scePlayReadyApiIsStarted, scePlayReadyApiStart, scePlayReadyApiStop, scePlayReadyApiVersion, scePlayReadyApiVersionString, scePlayReadyBufferFree, scePlayReadyBufferInit, scePlayReadyBufferMalloc, scePlayReadyBufferRealloc, scePlayReadyContentGetProperty, scePlayReadyContentSetProperty, scePlayReadyDomainCertEnumFini, scePlayReadyDomainCertEnumInit, scePlayReadyDomainCertEnumNext, scePlayReadyDomainCertFind, scePlayReadyDomainJoinGenerateChallenge, scePlayReadyDomainJoinProcessResponse, scePlayReadyDomainLeaveGenerateChallenge, scePlayReadyDomainLeaveProcessResponse, scePlayReadyEnvelopeClose, scePlayReadyEnvelopeGetSize, scePlayReadyEnvelopeInitializeRead, scePlayReadyEnvelopeOpen, scePlayReadyEnvelopeRead, scePlayReadyEnvelopeSeek, scePlayReadyFinalize, scePlayReadyFreeDecryptContext, scePlayReadyInitialize, scePlayReadyInitializeInitParams, scePlayReadyLicenseAcqGenerateAck, scePlayReadyLicenseAcqGenerateChallenge, scePlayReadyLicenseAcqGetAdditionalResponseData, scePlayReadyLicenseAcqProcessAckResponse, scePlayReadyLicenseAcqProcessResponse, scePlayReadyMemAlloc, scePlayReadyMemFree, scePlayReadyMemGetStats, scePlayReadyMemRealloc, scePlayReadyNewDecryptContext, scePlayReadyReaderBind, scePlayReadyReaderCommit, scePlayReadyReaderDecrypt, scePlayReadyReset and scePlayReadyStoreCleanup](../../../core/libs/prx/libScePlayReady/Export.cpp) (libScePlayReady) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePlayReady.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libScePlayReady2.md b/docs/dev/debt/libScePlayReady2.md new file mode 100644 index 0000000000..e3a51504a9 --- /dev/null +++ b/docs/dev/debt/libScePlayReady2.md @@ -0,0 +1,8 @@ +# libScePlayReady2 technical debt + +### Unknown function info + +- [scePlayReadyBufferInitWithData, scePlayReadyCloneDecryptContext, scePlayReadyDebugPrintf, scePlayReadyDebugSetLevel, scePlayReadyLicenseDelete, scePlayReadyLicenseDeleteInMemory, scePlayReadyLicenseGetExpirationTime, scePlayReadyLicenseGetProperty, scePlayReadyReaderClose, scePlayReadyReaderDecryptMultiple, scePlayReadyReaderDecryptMultipleOpaque, scePlayReadyReaderDecryptOpaque, scePlayReadyReaderDecrypt_Legacy, scePlayReadyReaderFreeOpaqueContent, scePlayReadyReaderRetrieveOpaqueContent and scePlayReadyReinitialize](../../../core/libs/prx/libScePlayReady2/Export.cpp) (libScePlayReady2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePlayReady2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified + +### Inventory exports declared elsewhere + The PS5 3.20 export table for this module also contains 39 names already declared under libScePlayReady in this tree; the duplicate-export rule keeps each name in a single library, so they are omitted here. They resolve through the sibling declarations. diff --git a/docs/dev/debt/libScePlayReady4.md b/docs/dev/debt/libScePlayReady4.md new file mode 100644 index 0000000000..5ee34a2ccf --- /dev/null +++ b/docs/dev/debt/libScePlayReady4.md @@ -0,0 +1,8 @@ +# libScePlayReady4 technical debt + +### Unknown function info + +- [scePlayReadyApiPlatform, scePlayReadyCdmiClose, scePlayReadyCdmiCreateMediaKeySession, scePlayReadyCdmiCreateMediaKeySystemAccess, scePlayReadyCdmiCreateMediaKeys, scePlayReadyCdmiDecryptMultipleOpaque, scePlayReadyCdmiDecryptOpaque, scePlayReadyCdmiDecryptOpaque2, scePlayReadyCdmiDestroyMediaKeySession, scePlayReadyCdmiDestroyMediaKeySystemAccess, scePlayReadyCdmiDestroyMediaKeys, scePlayReadyCdmiFreeOpaqueDecryptedContent, scePlayReadyCdmiGenerateRequest, scePlayReadyCdmiGetConfiguration, scePlayReadyCdmiGetExpiration, scePlayReadyCdmiGetKeySystem, scePlayReadyCdmiGetMediaKeyCount, scePlayReadyCdmiGetMediaKeyStatus, scePlayReadyCdmiGetSessionAppContext, scePlayReadyCdmiGetSessionId, scePlayReadyCdmiIsLicenseAckRequired, scePlayReadyCdmiLoad, scePlayReadyCdmiRemove, scePlayReadyCdmiRetrieveOpaqueDecryptedContent, scePlayReadyCdmiSetServerCertificate, scePlayReadyCdmiSetSessionCallbacks, scePlayReadyCdmiUpdate, scePlayReadyLicenseCount, scePlayReadyProfileClear, scePlayReadyProfileEnter, scePlayReadyProfileLeave, scePlayReadyProfilePause, scePlayReadyProfileResume, scePlayReadyProfileStart and scePlayReadyProfileStop](../../../core/libs/prx/libScePlayReady4/Export.cpp) (libScePlayReady4) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePlayReady4.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified + +### Inventory exports declared elsewhere + The PS5 3.20 export table for this module also contains 49 names already declared under libScePlayReady, libScePlayReady2 in this tree; the duplicate-export rule keeps each name in a single library, so they are omitted here. They resolve through the sibling declarations. diff --git a/docs/dev/debt/libScePlayerSelectionDialog.md b/docs/dev/debt/libScePlayerSelectionDialog.md new file mode 100644 index 0000000000..96b3ef7efc --- /dev/null +++ b/docs/dev/debt/libScePlayerSelectionDialog.md @@ -0,0 +1,9 @@ +# libScePlayerSelectionDialog technical debt + +### Silent stubs + +- [libScePlayerSelectionDialog](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) shows no selection UI and immediately reports cancellation. Account discovery, selection and successful-result list delivery are not implemented. + +### Unknown function info + +- [scePlayerSelectionDialogParamInitialize](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) (libScePlayerSelectionDialog) - returns `COMMON_DIALOG_ERROR_ARG_NULL` on a null parameter and throws `NotImplemented_nid_no_patch` for non-null pointers because `ScePlayerSelectionDialogParam` struct layout and default fields remain unverified. diff --git a/docs/dev/debt/libScePngDec.md b/docs/dev/debt/libScePngDec.md new file mode 100644 index 0000000000..9449f5a2e9 --- /dev/null +++ b/docs/dev/debt/libScePngDec.md @@ -0,0 +1,10 @@ +# libScePngDec technical debt + +### Functional + +- [libScePngDec](../../../core/libs/prx/libScePngDec/Export.cpp): the supplied module confirms the work-memory size formula, 16-byte handle alignment, the first 24 bytes of its context, and Create/QueryMemorySize validation order. The host now exposes these values but still allocates decoder working storage on the host instead of using the guest work region. +- [scePngDecDecode](../../../core/libs/prx/libScePngDec/Export.cpp): 16-bit output and the original's accepted pixel format 132 remain unsupported and throw. Malformed-image diagnostics and decoder scratch-memory behavior are not fully matched to the supplied module. + +### Unknown function info + +- [scePngDecDecodeWithInputControl](../../../core/libs/prx/libScePngDec/Export.cpp) (libScePngDec) - its name and NID are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePngDec.c); the no-parameter prototype follows [shadPS4's pngdec.h](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/libpng/pngdec.h#L76) and is not measured on a console. It is declared without parameters solely to resolve known imports; this is not an ABI claim. diff --git a/docs/dev/debt/libScePngEnc.md b/docs/dev/debt/libScePngEnc.md index 0ba7edc2bf..88f5089fbc 100644 --- a/docs/dev/debt/libScePngEnc.md +++ b/docs/dev/debt/libScePngEnc.md @@ -1,5 +1,5 @@ # libScePngEnc technical debt -### Unknown function info +### Functional -- [scePngEncEncode](../../../core/libs/prx/libScePngEnc/Export.cpp) (libScePngEnc) - whether rows may stay unfiltered when `filter_type` names a subset of the filters is unknown; as in shadPS4, they may only for 0 and for the all-filters mask +- [scePngEncEncode](../../../core/libs/prx/libScePngEnc/Export.cpp): native filter setup always includes the unfiltered candidate, including for partial masks. The host now permits it too; exact filter-selection ties, compressed bytes and partial-output behavior are not matched to the native encoder. Creation validation, work-memory size, 16-byte handle alignment and the context prefix now match the supplied module. Input-buffer alignment, pitch, dimensions, buffer capacity and selected-filter count now follow the native checks. Host scratch storage remains separately allocated. diff --git a/docs/dev/debt/libScePosixForWebKit.md b/docs/dev/debt/libScePosixForWebKit.md index c57da143e9..39e20c0542 100644 --- a/docs/dev/debt/libScePosixForWebKit.md +++ b/docs/dev/debt/libScePosixForWebKit.md @@ -3,6 +3,8 @@ ### Unknown function info - [gethostbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) - numeric IPv4 forms, trailing whitespace and 64-bit accumulation follow FreeBSD 11.4 [inet_aton](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/inet/inet_addr.c); clearing the resolver error on numeric success follows [fakeaddr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/net/gethostnamadr.c). Exact PS5 behavior is unverified; hostname lookup still uses the host resolver +- [clock_gettime_np, getenv_np, gethostbyname_r (`scePosixForWebKit_gethostbyname_r`), mmap_np, msetname, pthread_setschedparam_np, setenv_np, sysinfo (`scePosixForWebKit_sysinfo`)](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) - names/NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePosixForWebKit.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); credible prototypes are unknown. These exports use renamed helpers; defining `gethostbyname_r` would interpose on the host function this library calls itself. All are parameterless import stubs, not ABI claims; callers must not use them until signatures are verified. +- [__wrap_fchown, __wrap_link, __wrap_readlink, __wrap_statvfs, __wrap_symlink](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) - parameterless throwing stubs. Under the `--wrap` convention assumed for the forwarding wrappers (see Functional) each would take the POSIX signature of the function it wraps, but the targets are in libkernel_sys (`fchown`, `link`, `readlink`, `symlink`) and libSceLibcInternal (`statvfs`), which no prx links; libkernel_sys's `fchown`, `readlink` and `symlink` also still throw for some arguments ### Functional @@ -10,3 +12,4 @@ - [arc4random](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes of host OS entropy (`getentropy`, `rand_s` on Windows), as `arc4random_buf` does, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent - [fnmatch](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) follows FreeBSD 11 [fnmatch.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/fnmatch.c) in the C locale: each byte is one character, `FNM_CASEFOLD` folds ASCII letters only, and bracket ranges compare byte values. `[:class:]` names are not character classes: as in FreeBSD 11 through 14, `[[:digit:]]` is the set of `[`, `:`, `d`, `i`, `g`, `t` followed by a literal `]` (classes were added to `fnmatch.c` after 14). Multibyte characters and locale collation are not modelled. A null pattern or string throws - [getservbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) searches a built-in copy of 37 lines of FreeBSD 11.4's [etc/services](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/etc/services) (ftp-data, ftp, ssh, telnet, smtp, domain, tftp, http, pop3, ntp, imap, https, rtsp, submission, imaps, pop3s with their aliases and protocols, in file order) the way [getservent.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/net/getservent.c) searches the file: case-sensitive name or alias, a null protocol takes the first match, the port in network byte order, the result in per-thread storage. The console's `/etc/services` is unknown, so any other service returns null without setting `errno`, as for a name the file lacks. `getaddrinfo` still resolves service names through the host's service database +- [__wrap_access, __wrap_clock_gettime, __wrap_getcwd, __wrap_getenv, __wrap_lstat and __wrap_mmap](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) assume, from the export names alone, the GNU ld [`--wrap`](https://sourceware.org/binutils/docs/ld/Options.html) convention (a reference to `` resolves to `__wrap_`): each takes its target's POSIX signature and forwards to libc's (`access`, `getcwd`, `getenv`) or libkernel's (`clock_gettime`, `lstat`, `mmap`) implementation, which sets `errno`. No PS5 module or WebKit call site was read to confirm that WebKit imports `__wrap_` in place of `` or that the console wrappers only forward. Any sandbox or path handling the console module adds is not modelled, and how the wrappers relate to `getenv_np`, `setenv_np`, `mmap_np` and `clock_gettime_np` (such as a separate environment that `setenv_np` fills) is unknown 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/libScePs2EmuMenuDialog.md b/docs/dev/debt/libScePs2EmuMenuDialog.md new file mode 100644 index 0000000000..abb7485619 --- /dev/null +++ b/docs/dev/debt/libScePs2EmuMenuDialog.md @@ -0,0 +1,5 @@ +# libScePs2EmuMenuDialog technical debt + +### Unknown function info + +- [scePs2EmuMenuDialogClose, scePs2EmuMenuDialogGetResult, scePs2EmuMenuDialogGetStatus, scePs2EmuMenuDialogInitialize, scePs2EmuMenuDialogOpen, scePs2EmuMenuDialogTerminate and scePs2EmuMenuDialogUpdateStatus](../../../core/libs/prx/libScePs2EmuMenuDialog/Export.cpp) (libScePs2EmuMenuDialog) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePs2EmuMenuDialog.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libScePsmUtil.md b/docs/dev/debt/libScePsmUtil.md new file mode 100644 index 0000000000..569dad80bc --- /dev/null +++ b/docs/dev/debt/libScePsmUtil.md @@ -0,0 +1,5 @@ +# libScePsmUtil technical debt + +### Unknown function info + +- [scePsmUtilFinalize, scePsmUtilGetPsmIniFile, scePsmUtilInitialize and scePsmUtilRegisterCallbacks](../../../core/libs/prx/libScePsmUtil/Export.cpp) (libScePsmUtil) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsmUtil.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md index 9dd2ab63c4..f77c32d0b1 100644 --- a/docs/dev/debt/libScePsml_debug.md +++ b/docs/dev/debt/libScePsml_debug.md @@ -2,8 +2,13 @@ ### Unknown function info -- [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 -- [scePsmlMfsrGetDispatchMfsrPacket1000](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - unknown signature, imported by PPSA25344; declared without parameters. libScePsml and libScePsml_debug are built from the same source -- [scePsmlMfsrCreateContext1300, scePsmlMfsrGetDispatchMfsrPacket1300](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - unknown signatures, imported by PPSA28416 next to `scePsmlMfsrGetContextBufferRequirement1100`; declared without parameters. The names are not in the aerolib NID list; they hash to the imported NIDs `kLF7WtQ1-Dg` and `DDVUyPyYHTA` (`tools/nid_names.py`) -- [NIDs `xNZM9Y5lf-M` and `lcZD9tJraAw`](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_specularAA and libScePsml_quality8K, exported as `scePsmlUnknown01` and `scePsmlUnknown02`) - imported by PPSA01317, which fails to load without them; names and signatures unknown (not in the aerolib NID list), so they throw +- [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - only resource type 0 is supported. The context requirement reports four 2 MiB blocks regardless of its parameters, while the system calculation depends on the context configuration. The descriptor pointer at 0x28 is not read. Validation throws for null pointers, short block lists and foreign handles instead of reproducing the system error and fault behaviour. +- [scePsmlMfsr2GetDispatchPacketsSizeInDwords](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - writes a 32-bit size through the second argument; the unused first argument still needs caller verification. +- [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`) + +### Functional + +- [scePsmlMfsrGetDispatchMfsrPacketSizeInDwords, scePsmlMfsrGetMipmapBias, scePsmlMfsrSelectConfig, scePsmlMfsrRequestCapture, scePsmlMfsrIsCaptureInProgress](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - semantics from the APR-EMU `fakelib/libScePsml.sprx`. Guards: `0x8A810001` uninitialized, `0x8A810005` null/foreign handle (fakelib checks an engine id `0xA9C4`/`0x9231`; our map check is the equivalent), `0x8A810009` null output. `GetDispatchMfsrPacketSizeInDwords` writes `0x600`; `GetMipmapBias(context, bias, dimension)` writes `log2f(dimension / context->inputWidth) - 1.0f`, width recorded at `CreateContext800M3_2` from the parameter pair at 0x18 or `descriptor0 + 0x20` for callers that pass a descriptor pointer there (indirection inferred; zero width returns `0x8A810005`). `SelectConfig`, `RequestCapture`, `IsCaptureInProgress` validate then return 0. The `GetDispatchMfsrPacket*` calls still throw: the 0x158-byte template's packet routine is not understood +- [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 diff --git a/docs/dev/debt/libSceRazorCpu.md b/docs/dev/debt/libSceRazorCpu.md index 0c42c424a8..092aa68738 100644 --- a/docs/dev/debt/libSceRazorCpu.md +++ b/docs/dev/debt/libSceRazorCpu.md @@ -2,11 +2,11 @@ ### Silent stubs -- [sceRazorCpuPushMarker](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature (label, color, flags) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), the same as sceRazorCpuPushMarkerStatic's except that the label is copied; it does nothing without a capture and returns 0 -- [sceRazorCpuSync](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - the `int(void)` signature and its meaning, an unnamed frame-boundary event, come from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L119-L122). Without a capture there is no event to record, so it does nothing and returns 0, like the other `sceRazorCpu*` markers here and the [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/razor_cpu/razor_cpu.cpp#L143-L146) stub. PRAGMATA (PPSA02530) continues on 0; the console's result with no capture attached is unverified +- [libSceRazorCpu](../../../core/libs/prx/libSceRazorCpu/Export.cpp): capture-controller registration, thread-local capture storage, marker serialization and flushing are not implemented. The host implements only the confirmed uninitialized-profiler path: markers return zero without reading labels, and `sceRazorCpuFlushOccurred` returns false without writing its optional output. ### Unknown function info -- [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 +- [sceRazorCpuFiberLogNameChange, sceRazorCpuFiberSwitch, sceRazorCpuGpuMarkerSync, sceRazorCpuInitializeGpuMarkerContext, sceRazorCpuInitializeInternal, sceRazorCpuSetPopMarkerCallback](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - the list is the intersection of the export table of the console's [libSceRazorCpu](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRazorCpu.c) (function symbols only), the pinned [aerolib names](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) and the functions not yet defined anywhere in the repo. No credible prototypes were found; they are declared without parameters solely to resolve known imports, not as an ABI claim, and callers must not call them until signatures are verified. +- [sceRazorCpuSetPushMarkerCallback, sceRazorCpuSetPushMarkerStaticCallback, sceRazorCpuStartCaptureInternal, sceRazorCpuStopCaptureInternal, sceRazorCpuWorkloadRunBegin, sceRazorCpuWorkloadRunEnd, sceRazorCpuWorkloadSubmit](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - the list is the intersection of the export table of the console's [libSceRazorCpu](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRazorCpu.c) (function symbols only), the pinned [aerolib names](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) and the functions not yet defined anywhere in the repo. No credible prototypes were found; they are declared without parameters solely to resolve known imports, not as an ABI claim, and callers must not call them until signatures are verified. +- [sceRazorCpuJobManagerDispatch, sceRazorCpuJobManagerJob and sceRazorCpuJobManagerSequence](../../../core/libs/prx/libSceRazorCpu/Export.cpp): argument positions and serialized widths are recovered from machine instructions. Dispatch takes seven arguments, including two 64-bit payloads whose upper 16 bits are replaced by the count fields. The payloads are not dereferenced, so their original pointer-versus-integer types and the semantic names of the event identifiers remain unresolved. Job and Sequence serialize 16-bit identifiers and an event selector; their source enum names are unavailable. +- [sceRazorCpuGetDataTagStorageSize](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - throws: [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L142-L144) documents it as the byte size a tag table needs for a count of entries, but not the size of an entry diff --git a/docs/dev/debt/libSceRegMgr.md b/docs/dev/debt/libSceRegMgr.md new file mode 100644 index 0000000000..24cd287ce7 --- /dev/null +++ b/docs/dev/debt/libSceRegMgr.md @@ -0,0 +1,62 @@ +# libSceRegMgr technical debt + +### Unknown function info + +- [sceRegMgrCheckError](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrCheckIntVal](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrGetBinInitVal](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrGetIntInitVal](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrGetStrInitVal](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrGetVersion](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrIsChange](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrEvtGetCnt](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrEvtGetRegId](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrResetVal](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrSetInitLevel](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrSrvCnvRegionInt](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrSrvCnvRegionStr](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrSrvGetMachineType](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrSrvGetQAFforReg](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrSrvGetRealMachineType](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrSrvGetRegion](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrSrvGetRegionStr](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrBackupNeedMem](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrBackupPullData](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrBackupPushData](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrBackupPullDataGkp](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrBackupPushDataGkp](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrCntlStart](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrCntlDeleteReg](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrCntlGetFilesCount](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrCntlGetFileName](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrCntSetInitReinst](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrIsInitOK](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrGetInitError](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrRecoverRegNvs](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrNonSysCheckError](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrNonSysGetBin](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrNonSysGetStr](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrNonSysSetBin](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrNonSysSetInt](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrNonSysSetStr](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrGetVersionForPS4](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrBackupNeedMemForPS4](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrBackupPullDataForPS4](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrBackupPushDataForPS4](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrEvtGetCntForPS4](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrEvtGetRegIdForPS4](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrStrncpy](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrMemset](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrSetbin](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrToolGetUpdateCnt](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrToolGetEntryCnt](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrToolDataCheckGet](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrDrvGetUpdateCnt](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrDrvGetEntCnt](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrDrvDataOpen](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrDrvDataCheckGet](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrDrvDataClose](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrToolGetInfo](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrLogPull](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrCntlStopHdd](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature +- [sceRegMgrPrintInfo](../../../core/libs/prx/libSceRegMgr/Export.cpp) (libSceRegMgr) - unknown signature diff --git a/docs/dev/debt/libSceRemoteplay.md b/docs/dev/debt/libSceRemoteplay.md new file mode 100644 index 0000000000..a1bb852834 --- /dev/null +++ b/docs/dev/debt/libSceRemoteplay.md @@ -0,0 +1,43 @@ +# libSceRemoteplay technical debt + +### Unknown function info + +- [sceRemoteplayApprove](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayClearAllRegistData](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayClearConnectHistory](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayConfirmDeviceRegist](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayDisconnect](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGeneratePinCode](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGetApMode](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGetConnectHistory](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGetConnectUserId](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGetMbusDeviceInfo](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGetOperationStatus](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGetPrimaryUserId](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGetRemoteplayStatus](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGetRpMode](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayImeClose](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayImeFilterResult](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayImeGetEvent](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayImeNotify](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayImeNotifyEventResult](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayImeOpen](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayImeSetCaret](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayImeSetText](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayIsRemoteOskReady](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayIsRemotePlaying](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayNotifyMbusDeviceRegistComplete](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayNotifyNpPushWakeup](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayNotifyPinCodeError](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayNotifySubmitCode](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayNotifyUserDelete](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayPrintAllRegistData](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayProhibit](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayProhibitStreaming](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayServerLock](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayServerUnLock](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplaySetApMode](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplaySetLogLevel](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplaySetProhibition](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplaySetProhibitionForVsh](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplaySetRpMode](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) diff --git a/docs/dev/debt/libSceRnpsAppMgr.md b/docs/dev/debt/libSceRnpsAppMgr.md new file mode 100644 index 0000000000..b430381f91 --- /dev/null +++ b/docs/dev/debt/libSceRnpsAppMgr.md @@ -0,0 +1,5 @@ +# libSceRnpsAppMgr technical debt + +### Unknown function info + +- [sceRnpsAppMgrBlockAppInstall, sceRnpsAppMgrCreateUpdateRequest, sceRnpsAppMgrCreateUrlRequest, sceRnpsAppMgrDestroyBundle, sceRnpsAppMgrDestroyUpdateRequest, sceRnpsAppMgrFinishUrlRequest, sceRnpsAppMgrGetAppInfo, sceRnpsAppMgrGetAppInfoDebugString, sceRnpsAppMgrGetAppInfoList, sceRnpsAppMgrGetEventForShellUI, sceRnpsAppMgrGetUpdateProgress, sceRnpsAppMgrHasEventForShellUI, sceRnpsAppMgrInitialize, sceRnpsAppMgrPushEventForShellUI, sceRnpsAppMgrRecoverUfsImage, sceRnpsAppMgrRegisterShellUI, sceRnpsAppMgrRemoveApp, sceRnpsAppMgrRemoveAppOnSystemShutdown, sceRnpsAppMgrRemoveUfsImageOnSystemShutdown, sceRnpsAppMgrRequestUpdateCheck, sceRnpsAppMgrSetCrlUrl, sceRnpsAppMgrSetEventFlagForClients, sceRnpsAppMgrSetUpdateUrl, sceRnpsAppMgrStartService, sceRnpsAppMgrTerminate, sceRnpsAppMgrUnblockAppInstall and sceRnpsAppMgrUnregisterShellUI](../../../core/libs/prx/libSceRnpsAppMgr/Export.cpp) (libSceRnpsAppMgr) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRnpsAppMgr.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index 868932aa03..56985f25a2 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -1,8 +1,20 @@ # libSceRtc technical debt +### Silent stubs + +- [sceRtcInit, sceRtcEnd](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - return 0 and do nothing, as the console functions do: both are a bare `xor eax, eax; ret` in libSceRtc.sprx on firmware 13.60 + ### Unknown function info -- [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console's zone name table is only known from the names measured on PS5 firmware 13.60 and 13.42; any other name is assumed to take the military letter offset of its first character, as every measured name outside the table does (`HST`, `MSK`, `XYZ`), so a name the console's table holds but nobody measured gets the wrong offset -- [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a blank zone after seconds is accepted. With fully allocated input storage on PS5 firmware 13.42, the same NUL-terminated text returned `BAD_PARSE` when the byte after its terminator was zero and succeeded when that byte was uppercase `T`. This dependence on storage after the terminator is not modeled; controlled backing-byte measurements on other firmware and title contexts are unavailable. -- [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick returns `INVALID_POINTER` like the existing `sceRtcFormatRFC3339`, where shadPS4 formats the current time instead; offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset) +- [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a blank zone after seconds is accepted. With fully allocated input storage on PS5 firmware 13.42 and again on 13.60, the same NUL-terminated text returned `BAD_PARSE` when the byte after its terminator was zero and succeeded when that byte was uppercase `T`. This dependence on storage after the terminator is not modeled; controlled backing-byte measurements in a title process are unavailable. - [sceRtcGetDosTime, sceRtcSetDosTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date +- [sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp): the native wrappers obtain the zone from a kernel timezone query, while the host uses the offset at the supplied instant. The kernel timezone configuration and its failure paths are not modeled. Native formatting confirms that null tick pointers select current time, RFC3339 uses two fractional digits, and explicit offsets are limited to -1439 through 1439 minutes. +- [sceRtcGetCurrentRawNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console reads clock 0x13 and returns its time plus the Unix epoch in ticks, or `0x80B50001` when the clock is unsupported (errno 5). The raw network clock is not modelled; this returns the host clock. Read from the disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console +- [sceRtcGetCurrentAdNetworkTick, sceRtcGetCurrentDebugNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console reads clocks 0x12 and 0x11 and falls back to network clock 0x10 when either clock is unsupported. These getters use their process-local clock when it was set, otherwise they follow the process-local network clock. The firmware's unsupported-clock result is not modelled. Read from the disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console +- [sceRtcSetCurrentTick, sceRtcSetCurrentNetworkTick, sceRtcSetCurrentAdNetworkTick, sceRtcSetCurrentDebugNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a valid tick sets a process-local clock that advances from a monotonic host clock; the host system clock is not changed. The console sets clock 0, 0x10, 0x12 or 0x11. Null pointers return `0x80B50002` and ticks before the Unix epoch return `0x80B50003`, from disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console. sceRtcSetConf passes its third and fourth arguments to the same clock call with clock 0 and throws; what the pair selects is unknown +- [sceRtcFormatRFC3339Precise, sceRtcFormatRFC3339PreciseLocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - read from the disassembly of libSceRtc.sprx on firmware 13.60: a null tick means the current tick, a precision above 6 or a zone outside -1439 to 1439 minutes returns `0x80B50003`, and the fraction is the microseconds truncated to the requested number of digits. Not compared with console output. The output buffer size is unknown; at most 33 bytes are written +- [sceRtcGetYear, sceRtcGetMonth, sceRtcGetDay, sceRtcGetHour, sceRtcGetMinute, sceRtcGetSecond, sceRtcGetMicrosecond, sceRtcSetYear, sceRtcSetMonth, sceRtcSetDay, sceRtcSetHour, sceRtcSetMinute, sceRtcSetSecond, sceRtcSetMicrosecond](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - inferred component behavior without confirmed hardware trace. Component setters validate individual boundaries (e.g. day 1..31) independently without cross-field calendar validation; getters return `0x80B50002` (`SCE_RTC_ERROR_INVALID_POINTER`) for a null pointer + +### Functional + +- The process-local clocks set by [sceRtcSetCurrentTick, sceRtcSetCurrentNetworkTick, sceRtcSetCurrentAdNetworkTick and sceRtcSetCurrentDebugNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) are visible only through libSceRtc. The system clock and libkernel clock APIs keep reporting host time; cross-library clock consistency is not modelled. The console's clock interactions are not measured diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index f2e4d7383b..6b667bd48d 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -2,11 +2,14 @@ ### Silent stubs -- [sceRudpProcessEvents and sceRudpNetFlush](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host, and every `sceRudpWrite`, `sceRudpRead` and `sceRudpGetStatus` call here reports a reset or idle context without queueing data, so no context ever holds pending data to process or send; both calls return success after `sceRudpInit` and `RUDP_ERROR_NOT_INITIALIZED` before it, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) returns OK from its stubs. PPSA03490 calls `sceRudpProcessEvents(0)` followed by `sceRudpNetFlush()` from its P2P service thread and ignores the first result; whether the real calls take parameters is unknown -- [sceRudpActivate](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host to activate, so it returns success without doing anything, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) does for its own `sceRudpActivate`. PPSA01325 calls it with no arguments during start-up; what the console activates, and whether it reports an error before `sceRudpInit`, is unverified +- [sceRudpProcessEvents, sceRudpNetFlush and sceRudpNetReceived](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host, and every `sceRudpWrite`, `sceRudpRead` and `sceRudpGetStatus` call here reports a reset or idle context without queueing data, so no context ever holds pending data to process, send or receive; the three calls return success after `sceRudpInit` and `RUDP_ERROR_NOT_INITIALIZED` before it, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) returns OK from its stubs. What the console's `sceRudpNetReceived` returns is unverified. PPSA03490 calls `sceRudpProcessEvents(0)` followed by `sceRudpNetFlush()` from its P2P service thread and ignores the first result. +- [sceRudpEnableInternalIOThread2](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) takes `(uint32_t stack_size, uint32_t priority, uint64_t affinity)`, taken from the managed `SceRudp.Rudp.EnableInternalIOThread(uint stackSize, uint priority, ulong affinity)` in PPSA06888's Unity metadata; the first two match `sceRudpEnableInternalIOThread`. It returns success after `sceRudpInit` and `RUDP_ERROR_NOT_INITIALIZED` before it, as `sceRudpEnableInternalIOThread` does, without starting any thread. Whether the console reports other errors, and whether it applies the affinity, is unverified. +- [sceRudpActivate](../../../core/libs/prx/libSceRudp/Export.cpp) takes a context ID, socket address and 32-bit address length. Initialization and initial argument checks match the native implementation; transport mode checks, address conversion and connection activation remain unimplemented and throw. ### Unknown function info - [sceRudpGetStatus](../../../core/libs/prx/libSceRudp/Export.cpp) - PPSA01521 passes an output pointer and byte count `0xF8`. Measured on a PS5 (firmware 13.60): the status is 0xF8 bytes; before `sceRudpInit` every call returns `RUDP_ERROR_NOT_INITIALIZED`, and after it a null pointer or a byte count of 0 or above 0xF8 returns `RUDP_ERROR_INVALID_ARGUMENT`. The layout matches `SceRudpStatus` in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp) with `currentContexts` at 0x64 and `allocs`, `frees`, `memCurrent` and `memPeak` at 0xC8 to 0xD4. Only `currentContexts` is reported: the traffic counters stay 0 because nothing is sent, and the allocator counters stay 0 because the library does not allocate from the title's pool (the console reports 6 allocations and 0x1260 bytes right after `sceRudpInit`, and 4 more allocations and 0x3F0 bytes per context). The console also writes `currentContexts` and the allocator counters when the byte count is smaller than their offset; only the requested bytes are written here. -- [sceRudpGetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. The library keeps no configurable segment size (there is no `sceRudpSetMaxSegmentSize`), so 1410 is always reported. The `uint16_t*` parameter is inferred: the measurement does not show how many bytes the console writes, PPSA03490 passes a pointer to a 16-bit value and [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.cpp) declares the PS3 `cellRudpSetMaxSegmentSize(u16 mss)`, so only 16 bits are written -- PPSA06888 imports declared without parameters, signatures unknown: [sceRudpEnableInternalIOThread2, sceRudpGetOption, sceRudpGetRemoteInfo](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by its bundled `Media/Plugins/SceRudp.prx` and not called during start-up. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp), which declares them without parameters too +- [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. +- [sceRudpGetOption, sceRudpSetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - full PS5 option contracts are unverified; defaults and validation follow [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp). Options 12 and 13 have unknown meanings; last-error option 14 is not maintained. Their setters store all 32 bits per context and return 0 for valid four-byte buffers, preserving acceptance on `main`; their getters throw. Static inspection of PPSA06888 v01.001.008's bundled Unity transport shows its context-set constructor configuring IDs 2, 3, 5 and 6; runtime usage and other paths remain unverified. The plugin wrappers pass four-byte values. No context can connect, so stored settings cannot affect traffic. Closes when: PS5 option contracts are verified and the remaining behavior is implemented. +- [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - unverified on a PS5; imported by PPSA31327 and not called before the main menu. Signatures and the 8-byte event layout are recovered from the supplied module. The operations (1 add, 2 modify, 3 remove) and the argument checks follow the PS Vita library in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp), and the error codes match [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.h). Vita3K leaves `sceRudpPollDestroy` unimplemented, so `int sceRudpPollDestroy(int pollId)` and its checks are inferred. No context can connect, so no event is ever reported: `sceRudpPollWait` returns 0 when its timeout elapses, also for an empty poll, where Vita3K returns at once +- [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo, sceRudpListen, sceRudpPollCancel and sceRudpSetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRudp.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. diff --git a/docs/dev/debt/libSceS3da.md b/docs/dev/debt/libSceS3da.md new file mode 100644 index 0000000000..cc6345ecaa --- /dev/null +++ b/docs/dev/debt/libSceS3da.md @@ -0,0 +1,5 @@ +# libSceS3da technical debt + +### Unknown function info + +- [sceS3daClose, sceS3daGetInfo, sceS3daOpen and sceS3daSendData](../../../core/libs/prx/libSceS3da/Export.cpp) remain unimplemented. Port arguments and transfer buffer/length are recovered from the native module. `GetInfo` forwards its output pointer to a kernel ioctl without accessing its fields; the information layout requires the kernel handler or a caller that interprets its output. diff --git a/docs/dev/debt/libSceSaveData.md b/docs/dev/debt/libSceSaveData.md new file mode 100644 index 0000000000..936ec9f874 --- /dev/null +++ b/docs/dev/debt/libSceSaveData.md @@ -0,0 +1,5 @@ +# libSceSaveData technical debt + +### Unknown function info + +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceSaveDataAbort`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataBindPsnAccount`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataBindPsnAccountForSystemBackup`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataChangeDatabase`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataChangeInternal`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCheckBackupData`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCheckBackupDataForCdlg`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCheckBackupDataInternal`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCheckBackupDataLight`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCheckCloudData`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCheckSaveDataBroken`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCheckSaveDataVersion`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCheckSaveDataVersionLatest`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataClearProgress`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCreateUploadData`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDebugGetThreadId`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDeleteAllUser`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDeleteBackupData`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDeleteCloudData`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDeleteProspero`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDeleteTitle`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDeleteUser`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDirNameSearchInternal`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDownload`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetAllSize`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetAppLaunchedUser`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetAutoUploadConditions`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetAutoUploadRequestInfo`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetAutoUploadSetting`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetBoundPsnAccountCount`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetClientThreadPriority`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetCloudQuotaInfo`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetEventInfo`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetMountedSaveDataCount`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetProgress`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetSaveDataCount`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetSaveDataRootDir`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetSaveDataRootPath`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetSaveDataRootUsbPath`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetSavePoint`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetUpdatedDataCount`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataInitialize2`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataInitializeForCdlg`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataIsDeletingUsbDb`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataIsMounted`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataManualDelete`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataManualDownload`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataManualUpload`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataMount5`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataMountInternal`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataMountSys`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataRebuildDatabase`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataRegisterEventCallback`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataRestoreBackupData`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataRestoreBackupDataForCdlg`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataRestoreLoadSaveDataMemory`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataSetAutoUploadSetting`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataSetEventInfo`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataSetSaveDataLibraryUser`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataShutdownStart`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataSupportedFakeBrokenStatus`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataSyncCloudList`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataUmountSys`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataUmountWithBackup`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataUnregisterEventCallback`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataUpload`](../../../core/libs/prx/libSceSaveData/Export.cpp). diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 6759fd2a9e..ba886396d2 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -2,17 +2,23 @@ ### Unknown function info -- [sceSaveDataUnknown_RvID8BVeg](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native, NID `RvID8BVeg+g`) - declared without parameters as a throwing stub; Minecraft (PPSA17221) imports it beside `sceSaveDataTransferringMountPs4` and `sceSaveDataDirNameSearchPs4` and fails to load without it. No name in aerolib or among the PS4-migration variants tried with `tools/nid_names.py` hashes to it. +- [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): the native wrapper confirms the same 16-byte mount point and 40-byte preparation parameter transport as `sceSaveDataPrepare`. Transfer preparation still requires the save-data service backend; its protected stub is unchanged. - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits - [sceSaveDataConvert](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the parameter block (user id at `+0x00`, `SceSaveDataDirName` pointer at `+0x08`, 48 opaque bytes after it) is read from PPSA28416's single call, not confirmed by KytyPS5, shadPS4, fpPS4 or SharpProspero, which have no body for it. Host saves have no size limit, so an existing save returns `SCE_OK` without touching its files and no event is queued; what the console returns for a save that is mounted, already converted or needs no conversion is unknown. The directory name is validated as `sceSaveDataDelete` does - [sceSaveDataGetConvertProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA16064; signature and error codes follow the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-get-convert-progress.md) (`float* progress`). Save data conversion on host filesystems completes synchronously, so the progress query returns `SAVE_DATA_OK` with the last conversion's progress (`0.0f` before any conversion or when conversion fails, `1.0f` after successful conversion), `SAVE_DATA_ERROR_PARAMETER` if `progress` is null, or `SAVE_DATA_ERROR_NOT_INITIALIZED` when the library is uninitialized; intermediate asynchronous conversion progress on console is unmeasured -- [sceSaveDataCancel](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA23126, which fails to load without it; added without an implementation because its signature is unknown. Every save data call here finishes before it returns, so nothing is ever in flight to cancel, but what the console returns then is unknown -- [sceSaveDataDirNameSearch](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. A save's size is the `blocks` of the `sceSaveDataMount3` call that created it, kept beside the save in `.blocks`; a save without a kept size takes that of its next mount that names one, and otherwise reports `SAVE_DATA_BLOCKS_MAX`. The search infos (blocks, free blocks and 32 reserved bytes per returned name, following shadPS4's `OrbisSaveDataSearchInfo`), `sceSaveDataGetMountInfo` and the two block keys use that size and, as free, what the save's files leave of it, each file rounded up to 32 KiB blocks; the console's file system overhead is not known and not counted. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it. In the directory name pattern `%` matches any run of characters, none included, and `_` exactly one, as the comments of shadPS4's matcher describe; also not confirmed on hardware +- [sceSaveDataCancel](../../../core/libs/prx/libSceSaveData.native/Export.cpp): the native wrapper forwards one 32-bit argument to the save-data service. Its meaning and cancellation results require the service handler; the protected stub is unchanged. +- [sceSaveDataDirNameSearch](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. The search infos (blocks, free blocks and 32 reserved bytes per returned name) follow shadPS4's `OrbisSaveDataSearchInfo`. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it. In the directory name pattern `%` matches any run of characters, none included, and `_` exactly one, as the comments of shadPS4's matcher describe; also not confirmed on hardware +- [sceSaveDataDirNameSearch, sceSaveDataGetMountInfo, sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the 64 KiB block of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/c6d304cc3328bad62cb671327ace181110cf2ddb/src/SharpEmu.Libs/SaveData/SaveDataExports.cs#L82-L84) and the 48 to 16384-block range of SharpEmu and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L30-L31) are not measured on a console, and file system overhead is not counted. What the console does with a `blocks` outside that range is unknown: KytyPS5 rejects the creating mount and SharpEmu clamps it; here a larger one is kept as 16384 and a smaller non-zero one as given. A save's size is the `blocks` of the mount that created it, kept in `.blocks`, else that of its next mount that names one, else 16384; its free blocks are that size less its files, each rounded up to 64 KiB blocks - [sceSaveDataInitialize3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown +- [sceSaveDataCheckIpmiIfSize, sceSaveDataDebug, sceSaveDataDebugCheckBackupData, sceSaveDataDebugCleanMount, sceSaveDataDebugCompiledSdkVersion, sceSaveDataDebugCreateSaveDataRoot, sceSaveDataDebugProspero, sceSaveDataDebugRemoveSaveDataRoot and sceSaveDataDebugTarget](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSaveData.native.c). The seven no-argument prototypes follow the PS4 stubs in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/save_data/savedata.cpp#L653-L725), not measured on a console; no credible prototypes were found for the other two. All declarations have no parameters solely to resolve known imports and make no ABI claim. ### Functional - [sceSaveDataDirNameSearch, sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - only the running title's saves are kept, so a search with another title's `title_id` reports zero hits, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/ec334904604ba41f5d4753f4a2498ec82540e5d6/src/core/libraries/save_data/savedata.cpp#L791-L802) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L592-L596) do when that title has no save directory; what a console returns for another title's saves is not verified. The running title's ID is the `titleId` in its `param.json` - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2` +- [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated or empty directory name, `.`, `..`, or a name containing `/`, `\` or `:` returns `SAVE_DATA_ERROR_PARAMETER`, which the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-mount3.md#L196-L204) lists for an invalid parameter, and leaves the result as it was, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L652-L658) does. The [SDK](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-dir-name.md#L32-L36) lets an application use only letters, digits, `-`, `.` and `@` in a name, and KytyPS5 also accepts `_` ([`valid_path_component`](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L276-L287)); which other names the console's mount rejects is not checked on hardware +- [sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup +- [sceSaveDataSyncSaveDataMemory](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - validates library initialization and parameters; since in-memory saves flush synchronously on write, sync confirms existence and readiness of the memory blob diff --git a/docs/dev/debt/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/libSceScreenShot.md b/docs/dev/debt/libSceScreenShot.md new file mode 100644 index 0000000000..a109ca5bb9 --- /dev/null +++ b/docs/dev/debt/libSceScreenShot.md @@ -0,0 +1,11 @@ +# libSceScreenShot technical debt + +### Silent stubs + +- [sceScreenShotEnableNotification and sceScreenShotDisableNotification](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - return 0 without effect: on the console they ask the system software to show or hide the capture notification, which this project does not display +- [sceScreenShotSetOverlayImage and sceScreenShotSetOverlayImageWithOrigin](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - check the arguments and the file as the PS5 module does, then return 0 without keeping the image, since this project takes no screenshots to overlay +- [sceScreenShotIsDisabled](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - reports one process-wide flag set by sceScreenShotDisable and cleared by sceScreenShotEnable for any id other than -1; on the console the system software answers, and what the id selects is unknown + +### Unknown function info + +- [sceScreenShotCapture, sceScreenShotGetAppInfo, sceScreenShotGetDrcParam, sceScreenShotIsVshScreenCaptureDisabled, sceScreenShotSetDrcParam and sceScreenShotSetParam](../../../core/libs/prx/libSceScreenShot/Export.cpp) (libSceScreenShot) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceScreenShot.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. diff --git a/docs/dev/debt/libSceSdma.md b/docs/dev/debt/libSceSdma.md new file mode 100644 index 0000000000..78b6855431 --- /dev/null +++ b/docs/dev/debt/libSceSdma.md @@ -0,0 +1,5 @@ +# libSceSdma technical debt + +### Unknown function info + +- [libSceSdma pending exports](../../../core/libs/prx/libSceSdma/Export.cpp) - 26 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSdma.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceShare.md b/docs/dev/debt/libSceShare.md index 8ad04d2333..e9062c9e03 100644 --- a/docs/dev/debt/libSceShare.md +++ b/docs/dev/debt/libSceShare.md @@ -3,9 +3,9 @@ ### Unknown function info - [sceShareGetCurrentStatus](../../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - validation (feature flag non-zero, status non-null) and the all-zero 16-byte status taken from the [KytyPS5](https://github.com/KytyPS5/KytyPS5) reimplementation, not confirmed on a PS5 title; the meaning of the recording status values is unknown -- [sceShareGetRunningStatus](../../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - only the first 32-bit word of the status is known: PPSA01317 treats bits 0x3c of it as a running share feature and gives the call a 40-byte buffer. The host never records or broadcasts, so it stores 0 in that word and leaves the rest untouched; a null status returns `INVALID_PARAM` by analogy with `sceShareGetCurrentStatus` -- [sceShareSetContentParamForApplicationTitle](../../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - imported by PPSA12544's Unity plugins; the signature follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libShare.cpp), not confirmed on a PS5 title. The application title is only checked for null, as KytyPS5 does, since nothing is captured +- [sceShareGetRunningStatus](../../../core/libs/prx/libSceShare/Export.cpp) - the native wrapper writes only the first 32-bit word, populated from a kernel event flag. The host reports zero because capture and broadcast services are absent; native initialization checks and event-flag failures are not modeled. +- [sceShareSetContentParamForApplicationTitle](../../../core/libs/prx/libSceShare/Export.cpp) - the recovered string length limit is 1 through 5119 bytes. The title is validated but not sent to the capture service; native initialization and service failures are not modeled. ### Functional -- [sceShareCaptureVideoClipExtended](../../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - imported by PPSA12544's Share plugin; the signature (extended parameter, request id) and the answer follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/147188b143f67c27d8da396e759c19336f64c105/src/libs/libShare.cpp), not confirmed on a PS5 title. Nothing is captured, so, like `sceShareCaptureScreenshotExtended`, it stores the invalid request id -1 when the pointer is not null and returns `NOT_SUPPORTED` without reading the extended parameter, whose layout is unknown +- [sceShareCaptureVideoClipExtended, sceShareCaptureScreenshotExtended](../../../core/libs/prx/libSceShare/Export.cpp) - the 40-byte video and 24-byte screenshot parameters and request-ID write precedence are recovered. Video values at offsets 8 and 16 and screenshot mode semantics remain incomplete. Valid requests still return NOT_SUPPORTED without capture; screenshot mode 2 platform checks, throttling, initialization and the backend text limit are not modeled. diff --git a/docs/dev/debt/libSceShareFactoryUtil.md b/docs/dev/debt/libSceShareFactoryUtil.md new file mode 100644 index 0000000000..3bb30765d2 --- /dev/null +++ b/docs/dev/debt/libSceShareFactoryUtil.md @@ -0,0 +1,5 @@ +# libSceShareFactoryUtil technical debt + +### Unknown function info + +- [sceShareFactoryUtilBackToGameFromShareFactory, sceShareFactoryUtilGetLaunchStatus, sceShareFactoryUtilGetLaunchStatusOfShareFactory, sceShareFactoryUtilInitialize, sceShareFactoryUtilLaunchShareFactory and sceShareFactoryUtilTerminate](../../../core/libs/prx/libSceShareFactoryUtil/Export.cpp) (libSceShareFactoryUtil) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceShareFactoryUtil.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceSharePlay.md b/docs/dev/debt/libSceSharePlay.md index e6c2086083..cd999b7fab 100644 --- a/docs/dev/debt/libSceSharePlay.md +++ b/docs/dev/debt/libSceSharePlay.md @@ -2,9 +2,11 @@ ### Unknown function info -- [sceSharePlayInitialize](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - the library keeps no state, so the heap is not used. A null heap is accepted: PPSA11386 passes a null heap and 0x1800 bytes and stops on any non-zero result. The error for a zero size is unknown, so it throws +- [sceSharePlayCrashDaemon, sceSharePlayGetCurrentConnectionInfo, sceSharePlayGetCurrentInfo, sceSharePlayGetEvent, sceSharePlayNotifyDialogOpen, sceSharePlayNotifyForceCloseForCdlg, sceSharePlayResumeScreenForCdlg, sceSharePlayServerLock](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - the list is the intersection of the export table of the console's [libSceSharePlay](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSharePlay.c) (function symbols only), the pinned [aerolib names](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) and the functions not yet defined anywhere in the repo. No credible prototypes were found; they are declared without parameters solely to resolve known imports, not as an ABI claim, and callers must not call them until signatures are verified. +- [sceSharePlayServerUnLock, sceSharePlaySetMode, sceSharePlaySetProhibition, sceSharePlaySetProhibitionModeWithAppId, sceSharePlayStartStandby, sceSharePlayStartStreaming, sceSharePlayStopStandby, sceSharePlayStopStreaming](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - the list is the intersection of the export table of the console's [libSceSharePlay](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSharePlay.c) (function symbols only), the pinned [aerolib names](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv) and the functions not yet defined anywhere in the repo. No credible prototypes were found; they are declared without parameters solely to resolve known imports, not as an ABI claim, and callers must not call them until signatures are verified. ### Functional -- [sceSharePlayGetCurrentConnectionInfoA](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. -- [sceSharePlayProhibitController](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - accepts 0 (allow) and 1 (prohibit) and returns 0, following [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Share/SharePlayExports.cs#L9-L31). The setting is not stored, as no Share Play session or visitor controller exists and no export reads it back. sharpemu rejects any other value with its synthetic `0x80020003`, not a Share Play code, so other values throw. Like the other exports here it does not require `sceSharePlayInitialize`. The `int` argument type and the console's error for other values are unverified +- [sceSharePlayInitialize and sceSharePlayTerminate](../../../core/libs/prx/libSceSharePlay/Export.cpp) - native heap validation and initialization state are recovered: a null heap ignores the supplied size, a supplied heap requires at least 0x1800 bytes, repeated initialization returns 0x810E0003 and termination before initialization returns 0x810E0004. The host does not allocate the native internal heap, connect to the Share Play service or reproduce its failures. +- [sceSharePlayGetCurrentConnectionInfoA](../../../core/libs/prx/libSceSharePlay/Export.cpp) - the native module confirms a 72-byte result, initial zero fields and both user IDs -1. Initialization is checked before the output pointer. The host retains this initial disconnected state; native event draining, cached updates and service reconnection are unimplemented. +- [sceSharePlayProhibitController](../../../core/libs/prx/libSceSharePlay/Export.cpp) - the argument is one byte, forwarded unchanged to the service. The host checks initialization and accepts 0/1 without a visitor controller; other byte values still throw because their handling belongs to the unimplemented service. diff --git a/docs/dev/debt/libSceShareUtility.md b/docs/dev/debt/libSceShareUtility.md new file mode 100644 index 0000000000..c953b5f180 --- /dev/null +++ b/docs/dev/debt/libSceShareUtility.md @@ -0,0 +1,5 @@ +# libSceShareUtility technical debt + +### Unknown function info + +- [sceShareUtilityAdvanceFromFileWithContentIdList, sceShareUtilityAdvanceFromFileWithTitleIdList, sceShareUtilityAdvanceGetServiceInfo, sceShareUtilityAdvanceInitialize, sceShareUtilityAdvanceQueryServiceInfo, sceShareUtilityAdvanceQueryServiceInfoA, sceShareUtilityAdvanceResetPlayingContent, sceShareUtilityAdvanceSetPlayingContent, sceShareUtilityAdvanceSetUploadContentData, sceShareUtilityAdvanceTerminate, sceShareUtilityAdvanceValidateContents, sceShareUtilityAdvanceWakeUpShareMenu, sceShareUtilityAdvanceWakeUpShareMenuBroadcast, sceShareUtilityAdvanceWakeUpShareMenuDefault, sceShareUtilityAdvanceWakeUpShareMenuForShareContent, sceShareUtilityInitialize, sceShareUtilityInitializeEx2, sceShareUtilityOpenShareMenu, sceShareUtilityOpenShareMenuBroadcast, sceShareUtilityOpenShareMenuDefault, sceShareUtilityOpenShareMenuForShareContent and sceShareUtilityTerminate](../../../core/libs/prx/libSceShareUtility/Export.cpp) (libSceShareUtility) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceShareUtility.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceShellUIUtil.md b/docs/dev/debt/libSceShellUIUtil.md new file mode 100644 index 0000000000..12414a8ba7 --- /dev/null +++ b/docs/dev/debt/libSceShellUIUtil.md @@ -0,0 +1,5 @@ +# libSceShellUIUtil technical debt + +### Unknown function info + +- [libSceShellUIUtil](../../../core/libs/prx/libSceShellUIUtil/Export.cpp) remains unimplemented. Native wrappers confirm the export signatures, including two input strings, output buffer, 32-bit capacity and optional 32-bit output size for `GetAppUrl`. The precise string meanings and the second word of the 8-byte `LaunchByUri` parameter require the receiving IPC service; the wrapper only checks its leading size. Task completion, IPC errors and lifecycle behavior are not implemented. diff --git a/docs/dev/debt/libSceSigninDialog.md b/docs/dev/debt/libSceSigninDialog.md index 9c05ac5649..5353db25a6 100644 --- a/docs/dev/debt/libSceSigninDialog.md +++ b/docs/dev/debt/libSceSigninDialog.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceSigninDialogClose, sceSigninDialogGetResult](../../../core/libs/prx/libSceSigninDialog/Export.cpp) (libSceSigninDialog) - SceSigninDialogResult layout (int32 result and 3 reserved int32 words, zeroed) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/dialog.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/SigninDialog/SigninDialog.cs); error codes follow libSceLoginDialog's 0x80B8xxxx common-dialog codes rather than KytyPS5's 0x8135xxxx, and offline calls finish as user cancelled without opening a dialog. Neither choice is verified on hardware +- [sceSigninDialogClose, sceSigninDialogGetResult](../../../core/libs/prx/libSceSigninDialog/Export.cpp) (libSceSigninDialog) - SceSigninDialogResult layout (int32 result and 3 reserved int32 words, zeroed) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/dialog.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/SigninDialog/SigninDialog.cs); error codes are the 0x80B8xxxx common-dialog codes rather than KytyPS5's 0x8135xxxx, and offline calls finish as user cancelled without opening a dialog. Neither choice is verified on hardware diff --git a/docs/dev/debt/libSceSocialScreen.md b/docs/dev/debt/libSceSocialScreen.md new file mode 100644 index 0000000000..1978669c08 --- /dev/null +++ b/docs/dev/debt/libSceSocialScreen.md @@ -0,0 +1,5 @@ +# libSceSocialScreen technical debt + +### Unknown function info + +- [sceSocialScreenCloseSeparateMode, sceSocialScreenConfigureSeparateMode, sceSocialScreenCrashDaemon, sceSocialScreenGetIgnoreSeparateMode, sceSocialScreenInitialize, sceSocialScreenInitializeSeparateModeParameter, sceSocialScreenOpenSeparateMode, sceSocialScreenReserveHighFrameRateVideoCaptureRequest, sceSocialScreenSetIgnoreSeparateMode, sceSocialScreenSetMode and sceSocialScreenTerminate](../../../core/libs/prx/libSceSocialScreen/Export.cpp) (libSceSocialScreen) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSocialScreen.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceSocialScreenDialog.md b/docs/dev/debt/libSceSocialScreenDialog.md new file mode 100644 index 0000000000..b1b16a216d --- /dev/null +++ b/docs/dev/debt/libSceSocialScreenDialog.md @@ -0,0 +1,5 @@ +# libSceSocialScreenDialog technical debt + +### Silent stubs + +- [libSceSocialScreenDialog](../../../core/libs/prx/libSceSocialScreenDialog/Export.cpp) - the social screen dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as there is no second-screen viewer. The signatures are from [Orbital's type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); the result layout beyond the leading `int32_t` result code is not known, so nothing else is written. diff --git a/docs/dev/debt/libSceSpSysCallWrapper.md b/docs/dev/debt/libSceSpSysCallWrapper.md new file mode 100644 index 0000000000..ea6d5fbf4e --- /dev/null +++ b/docs/dev/debt/libSceSpSysCallWrapper.md @@ -0,0 +1,5 @@ +# libSceSpSysCallWrapper technical debt + +### Unknown function info + +- [sceSpGetPid, sceSpKernelGettimeofday, sceSpKernelNanosleep, sceSpNetConnect, sceSpNetEpollAbort, sceSpNetEpollControl, sceSpNetEpollCreate, sceSpNetEpollDestroy, sceSpNetEpollWait, sceSpNetErrnoLoc, sceSpNetPoolCreate, sceSpNetPoolDestroy, sceSpNetRecv, sceSpNetResolverAbort, sceSpNetResolverCreate, sceSpNetResolverDestroy, sceSpNetResolverGetError, sceSpNetResolverStartNtoa, sceSpNetSend, sceSpNetSetsockopt, sceSpNetSocket, sceSpNetSocketClose, sceSpPthreadAttrDestroy, sceSpPthreadAttrInit, sceSpPthreadAttrSetinheritsched, sceSpPthreadAttrSetschedparam, sceSpPthreadAttrSetstacksize, sceSpPthreadCondDestroy, sceSpPthreadCondInit, sceSpPthreadCondSignal, sceSpPthreadCondWait, sceSpPthreadCondattrDestroy, sceSpPthreadCondattrInit, sceSpPthreadCreate, sceSpPthreadDetach, sceSpPthreadExit, sceSpPthreadJoin, sceSpPthreadMutexDestroy, sceSpPthreadMutexInit, sceSpPthreadMutexLock, sceSpPthreadMutexUnlock, sceSpPthreadMutexattrDestroy, sceSpPthreadMutexattrInit, sceSpPthreadMutexattrSetprioceiling, sceSpPthreadMutexattrSetprotocol, sceSpPthreadMutexattrSettype, sceSpPthreadSetprio and sceSpSelect](../../../core/libs/prx/libSceSpSysCallWrapper/Export.cpp) (libSceSpSysCallWrapper) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSpSysCallWrapper.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceSrcUtl.md b/docs/dev/debt/libSceSrcUtl.md new file mode 100644 index 0000000000..bb8018ab3a --- /dev/null +++ b/docs/dev/debt/libSceSrcUtl.md @@ -0,0 +1,5 @@ +# libSceSrcUtl technical debt + +### Unknown function info + +- [sceSrcUtilityFinalize, sceSrcUtilityFlush, sceSrcUtilityGetDelaySamples, sceSrcUtilityGetHandleSize, sceSrcUtilityGetPriority, sceSrcUtilityInitialize, sceSrcUtilityInitializeEx, sceSrcUtilityResample and sceSrcUtilitySetPriority](../../../core/libs/prx/libSceSrcUtl/Export.cpp) (libSceSrcUtl) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSrcUtl.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index f5b9ed5354..20e9e19b1d 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -2,12 +2,13 @@ ### 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 +- [CA_MGMT_extractKeyBlobEx, CA_MGMT_extractPublicKeyInfo, CA_MGMT_freeKeyBlob, CRYPTO_initAsymmetricKey, CRYPTO_uninitAsymmetricKey, RSA_verifySignature, VLONG_freeVlongQueue, sceSslCheckRecvPending, sceSslCreateSslConnection and sceSslDeleteSslConnection](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - the list is the intersection of the export table of the console's libSceSsl (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); no credible prototypes were found. They are declared without parameters solely to resolve known imports, not as an ABI claim; callers must not call them until signatures are verified. +- [sceSslDisableOption, sceSslDisableOptionInternal, sceSslDisableOptionInternalInsecure, sceSslEnableOption, sceSslEnableOptionInternal, sceSslGetNanoSSLModuleId, sceSslGetPeerCert, sceSslGetSslError, sceSslLoadRootCACert and sceSslSetSslVersion](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - the list is the intersection of the export table of the console's libSceSsl (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo. Names and NIDs appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); no credible prototypes were found. They are declared without parameters solely to resolve known imports, not as an ABI claim; callers must not call them until signatures are verified. +- [sceSslReuseConnection](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - its name and NID appear in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSsl.c) and [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv); its parameter shape follows [SharpProspero's pinned binding](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs#L2231-L2235). No console observation or reliable implementation source for session reuse was found. -### 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`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`, whose connections return `SSL_ERROR_INVALID_ID` from then on. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with flags 1 or 2 returns what the first record delivers. Native flags 3 peek without consuming data; values outside 0 through 3 return `SSL_ERROR_INVALID_VALUE` before connection lookup. A non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, which then returns `SSL_ERROR_INVALID_ID`; a second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts +- [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). Measured on 13.40 as the connection entry describes: it sends close_notify once if the handshake completed and returns 0, 0 again when repeated and 0 on a connection that never connected, already failed verification or lost its transport to a fatal alert; a close before the handshake changes nothing, so the connection can still connect. After the close `sceSslConnect` returns 0 and `sceSslRead` 0, as on the console; `sceSslWrite` after it throws, as the console's code was not measured. A close_notify that a non-blocking socket cannot take at once returns `SSL_ERROR_WANT_POLLOUT` (0x8095F014) and the close can be repeated; after a reset it returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) every time. The socket is left to the title; an unknown connection returns `SSL_ERROR_INVALID_ID` (0x8095F006). Whether the console closes the socket is unknown +- [Certificate readers](../../../core/libs/prx/libSceSsl/Export.cpp): foreign and expired certificate/name handles throw instead of following native unchecked pointers. Name views track certificate lifetime and preserve identity; the native no-op name release is implemented. View storage remains allocated until context destruction. Numeric names for attributes Mbed TLS cannot identify may differ from native names. Fingerprint stack-space validation and backend digest failures are not reproduced. Signatures, valid-context/null-argument precedence, serial truncation, name copy bounds, validity timestamps and supported hash selectors are confirmed by the supplied module. +- [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 parses DER/PEM chains. CA imports still commit together instead of preserving successful entries before a later failure. Client certificate replacement clears the previous identity; raw private keys load independently and are parsed when configuring TLS. Partial client chains on decode failure, native key parse errors and incomplete identities during TLS setup remain unsupported. 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. Native CA queries include built-in roots; the host still queries loaded authorities and returns `NOT_FOUND` for an empty context. No native root bundle is copied. Null-output and invalid-context precedence, output preservation on invalid contexts and the SDK 7.50 unload error boundary are recovered. Legacy clearing of foreign raw CA lists remains unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate +- [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures; the behaviour was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title). The verify options select which verification failures `sceSslConnect` reports (see the connection entry above), with the bit values SharpProspero's `SceSslVerifyOption` gives: `SERVER_VERIFY` 0x1, `CN_CHECK` 0x4, `NOT_AFTER_CHECK` 0x8, `NOT_BEFORE_CHECK` 0x10, `KNOWN_CA_CHECK` 0x20. The initial options are `SERVER_VERIFY`, `CN_CHECK` and `KNOWN_CA_CHECK` (0x25): the console accepted an expired and a not-yet-valid server certificate until 0x8 or 0x10 was enabled, which then returned `SSL_ERROR_NOT_AFTER_CHECK` (0x8095F00D) and `SSL_ERROR_NOT_BEFORE_CHECK` (0x8095F00E); whether 0x2 (`CLIENT_VERIFY`) starts enabled was not determined and it is kept off. A check that is disabled is not reported, whatever the chain. Bits outside 0x3F return `SSL_ERROR_INVALID_VALUE` (0x8095F007) from both calls. Disabling 0x1, 0x4 or 0x20 from the payload stopped its process through the kernel's debug exception path (`mDBG: Sending signal ... signo: 0xa0020101`, 6 of 6 attempts; disabling 0x2, 0x8 and 0x10 returned 0), so whether a title may clear them is unknown; this build clears them, as titles import the call. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values 2 (SSL 3.0) to 5 (TLS 1.2) and returns `SSL_ERROR_INVALID_VALUE` for 0, 1 and anything above 5; the console has no TLS 1.3 value. Every accepted value gives the same handshake, as Mbed TLS 3 speaks TLS 1.2 and nothing older and TLS 1.3 is compiled out, which matches the console offering TLS 1.2 only. `sceSslSetAlpn` stores the string up to its terminator, split on commas into the protocols offered in the handshake (the console sent `h2,http/1.1` as the two names and a wire-format string as one name), a second call replaces the list, an empty string returns `SSL_ERROR_BAD_LENGTH` (0x80951773), and a null list clears the configured protocols. Native inspection confirms that replacement clears the old list even on failure, that each comma-separated name must contain 1-255 bytes and that the whole string must be shorter than 65535 bytes. `sceSslGetAlpnSelected` returns the protocol the server selected, still after `sceSslClose`, or null before the handshake and when none was selected; the pointer is owned by the connection. An unknown connection and a connection whose context was terminated return `SSL_ERROR_INVALID_ID` (0x8095F006) from the four setters and null from `sceSslGetAlpnSelected`, as confirmed by the supplied native module +- [sceSslGetPem](../../../core/libs/prx/libSceSsl/Export.cpp): native argument validation, CRLF line endings, 64-character Base64 lines, NUL-inclusive length and buffer clearing on insufficient capacity are recovered. Foreign and expired certificate handles throw instead of performing native unchecked pointer access. DER reserialization differences for noncanonical certificates remain unverified. diff --git a/docs/dev/debt/libSceSulphaDrv.md b/docs/dev/debt/libSceSulphaDrv.md new file mode 100644 index 0000000000..88b6f9091f --- /dev/null +++ b/docs/dev/debt/libSceSulphaDrv.md @@ -0,0 +1,5 @@ +# libSceSulphaDrv technical debt + +### Inventory exports declared elsewhere + +sceSulphaGetClientInfo, sceSulphaGetConnectedClientCount, sceSulphaGetDefaultConfig, sceSulphaGetNeededMemory, sceSulphaInit, sceSulphaMessage, sceSulphaSetBookmark and sceSulphaShutdown are the complete PS5 3.20 export table of this module per the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSulphaDrv.c), but all eight are currently declared by #7416 in libSceSulpha, so this module keeps its `APS5_DUMMY_FUN` skeleton to respect the duplicate-export rule. Declare them here (with no parameters, matching the `1b5004d77` unknown-prototype shape) if that PR's module attribution is rejected. diff --git a/docs/dev/debt/libSceSysCore.md b/docs/dev/debt/libSceSysCore.md new file mode 100644 index 0000000000..969d61b1fe --- /dev/null +++ b/docs/dev/debt/libSceSysCore.md @@ -0,0 +1,5 @@ +# libSceSysCore technical debt + +### Unknown function info + +- [sceApplicationGetAppInfoByAppId](../../../core/libs/prx/libSceSysCore/Export.cpp) takes a 32-bit application ID and a writable 152-byte record. The service fills the record through IPC; its complete field layout remains unresolved. The SystemService caller consumes a subrecord beginning at byte 48. diff --git a/docs/dev/debt/libSceSysUtil.md b/docs/dev/debt/libSceSysUtil.md new file mode 100644 index 0000000000..94f5cccef1 --- /dev/null +++ b/docs/dev/debt/libSceSysUtil.md @@ -0,0 +1,8 @@ +# libSceSysUtil technical debt + +### Unknown function info + +- [sceSysUtilSendAddressingSystemNotification, sceSysUtilSendAddressingSystemNotificationWithDeviceId, sceSysUtilSendAddressingSystemNotificationWithUserId, sceSysUtilSendAddressingSystemNotificationWithUserName, sceSysUtilSendNotificationRequest, sceSysUtilSendNpDebugNotificationRequest](../../../core/libs/prx/libSceSysUtil/Export.cpp) (libSceSysUtil) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSysUtil.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceSysUtilSendSystemNotification, sceSysUtilSendSystemNotification2, sceSysUtilSendSystemNotificationParamInit, sceSysUtilSendSystemNotificationWithAppId, sceSysUtilSendSystemNotificationWithAppInfo, sceSysUtilSendSystemNotificationWithAppName](../../../core/libs/prx/libSceSysUtil/Export.cpp) (libSceSysUtil) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSysUtil.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceSysUtilSendSystemNotificationWithAppNameRelatedToUser, sceSysUtilSendSystemNotificationWithDeviceId, sceSysUtilSendSystemNotificationWithDeviceIdRelatedToUser, sceSysUtilSendSystemNotificationWithErrorCode, sceSysUtilSendSystemNotificationWithParams, sceSysUtilSendSystemNotificationWithTextRelatedToUser](../../../core/libs/prx/libSceSysUtil/Export.cpp) (libSceSysUtil) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSysUtil.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceSysUtilSendSystemNotificationWithUserId, sceSysUtilSendSystemNotificationWithUserName, sceSysUtilSendSystemNotificationWithUserNameInfo, sceSysUtilSendTrcCheckNotificationRequest, sceSysUtilSendWebDebugNotificationRequest](../../../core/libs/prx/libSceSysUtil/Export.cpp) (libSceSysUtil) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSysUtil.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. diff --git a/docs/dev/debt/libSceSysmodule.md b/docs/dev/debt/libSceSysmodule.md new file mode 100644 index 0000000000..042c89ea84 --- /dev/null +++ b/docs/dev/debt/libSceSysmodule.md @@ -0,0 +1,17 @@ +# 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`, `sceSysmoduleUnloadModuleInternalWithArg`, `sceSysmoduleGetModuleHandleInternal`) throw for an unknown id. What the console returns for an id it does not know is not verified. + +### Unknown function info + +- [kModuleTable](../../../core/libs/prx/libSceSysmodule/ModuleTable.hpp) (libSceSysmodule) - module id 0x00000132 is named `libSceProprietaryVoiceChatHelperPrx`, the name of the built module, because `sceSysmoduleGetModuleHandleInternal` opens `libs/.prx`. The name the console's table uses for this id is not verified +- [kModuleTable](../../../core/libs/prx/libSceSysmodule/ModuleTable.hpp) (libSceSysmodule) - module IDs registered from decrypted PS5 firmware 1.00–3.20 sysmodule lookup tables: + - User-space IDs (0x00000005, 0x0000010a, 0x0000010e, 0x0000011b, 0x00000125, 0x00000126, 0x00000127, 0x00000128, 0x0000012a, 0x0000012b, 0x00000130) and internal IDs (0x8000007b, 0x8000007e, 0x8000007f, 0x80000080, 0x80000082, 0x80000083, 0x80000084, 0x80000085, 0x80000086, 0x80000087, 0x80000088, 0x80000089, 0x8000008e, 0x80000090, 0x80000091, 0x80000092, 0x80000093, 0x80000095, 0x80000097, 0x80000099, 0x8000009b, 0x8000009c, 0x8000009e, 0x8000009f, 0x800000a0, 0x800000a1, 0x800000a2, 0x800000a3, 0x800000a4, 0x800000a6, 0x800000a7, 0x800000a8, 0x800000a9, 0x800000ad, 0x800000ae, 0x800000af, 0x800000b0, 0x800000b2, 0x800000b3, 0x800000b4, 0x800000b5, 0x800000b6, 0x800000b7, 0x800000b9, 0x800000ba, 0x800000bb, 0x800000bc, 0x800000bd, 0x800000be, 0x800000bf, 0x800000c3); whether all mapped PRX modules exist as separate runtime files or are embedded in system daemons on retail consoles is unverified + - Dual IDs for `libSceNgs2` (0x0000000b user / 0x80000090 internal) and `libSceHdrScopes` (0x00000109 user / 0x800000bd internal) reflect distinct public and internal sysmodule identifiers defined in PS5 firmware SDKs +- [sceSysmoduleIsCalledFromSysModule, sceSysmoduleIsCameraPreloaded, sceSysmoduleLoadModuleByNameInternal, sceSysmoduleMapLibcForLibkernel, sceSysmodulePreloadModuleForLibkernel and sceSysmoduleUnloadModuleByNameInternal](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libSceSysmodule) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSysmodule.c). `sceSysmoduleLoadModuleByNameInternal` takes `(uint8_t* fname, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5)` from [SharpProspero's Sysmodule bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Sysmodule/Sysmodule.cs#L149-L151), not measured on a console. The other exports had no credible prototypes and are declared without parameters solely to resolve known imports; they make no ABI claim. + +### Functional + +- [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 838de9ac99..5d9acec690 100644 --- a/docs/dev/debt/libSceSystemGesture.md +++ b/docs/dev/debt/libSceSystemGesture.md @@ -2,4 +2,9 @@ ### Silent stubs -- [System gesture recognizers](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureGetPrimitiveTouchEvents`, `GetPrimitiveTouchEventsCount`, `GetTouchEvents`, `GetPrimitiveTouchEventByIndex` and the recognizer append, remove, reset and update calls report that no touch input exists and keep no recognizer state, 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 +- [System gesture recognizers](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureGetTouchEvents`, `GetTouchEventsCount`, `GetTouchEventByIndex`, `GetTouchEventByEventID` and the recognizer append, remove, reset and update calls report that no recognizer ever matches, so a title receives primitive touch events but no tap, drag, hold, pinch, rotation or flick event. Handle and pointer arguments are still checked across all query entry points. `sceSystemGestureGetTouchRecognizerInformation` returns the gesture type and rectangle given to `sceSystemGestureCreateTouchRecognizer` or `UpdateTouchRecognizerRectangle`, stored in the title's recognizer buffer in a layout private to this file, and always reports `updated_time` 0 because no recognizer is ever updated with input. It throws for a recognizer that was not created by `sceSystemGestureCreateTouchRecognizer` or has no rectangle, since the console's result in those cases is not known + +### Unknown function info + +- [Primitive touch events](../../../core/libs/prx/libSceSystemGesture/Export.cpp): pad handles are not validated against native controller information. Begin, active and end states are confirmed, but cancellation state 4, motion thresholds and SDK-dependent duplicate-report handling remain unimplemented. Timing, coordinate conversion and update flags still use the host approximation. Recognizer allocation and native descriptor layouts remain incomplete. +- [sceSystemGestureDebugGetVersion](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - its name and NID are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSystemGesture.c), but no credible prototype was found. It is declared without parameters solely to resolve known imports; this is not an ABI claim diff --git a/docs/dev/debt/libSceSystemLogger.md b/docs/dev/debt/libSceSystemLogger.md new file mode 100644 index 0000000000..f4f9de0077 --- /dev/null +++ b/docs/dev/debt/libSceSystemLogger.md @@ -0,0 +1,5 @@ +# libSceSystemLogger technical debt + +### Unknown function info + +- [sceSystemLoggerInitialize, sceSystemLoggerSetLogData and sceSystemLoggerTerminate](../../../core/libs/prx/libSceSystemLogger/Export.cpp) (libSceSystemLogger) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSystemLogger.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceSystemLogger2.md b/docs/dev/debt/libSceSystemLogger2.md new file mode 100644 index 0000000000..3d17ac6069 --- /dev/null +++ b/docs/dev/debt/libSceSystemLogger2.md @@ -0,0 +1,5 @@ +# libSceSystemLogger2 technical debt + +### Unknown function info + +- [sceSystemLogger2GetTimestamp, sceSystemLogger2Initialize, sceSystemLogger2SetEventAsync, sceSystemLogger2SetEventAsyncEx and sceSystemLogger2Terminate](../../../core/libs/prx/libSceSystemLogger2/Export.cpp) (libSceSystemLogger2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSystemLogger2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceSystemLogger2Delivery.md b/docs/dev/debt/libSceSystemLogger2Delivery.md new file mode 100644 index 0000000000..c97c25f582 --- /dev/null +++ b/docs/dev/debt/libSceSystemLogger2Delivery.md @@ -0,0 +1,5 @@ +# libSceSystemLogger2Delivery technical debt + +### Unknown function info + +- [sceSystemLogger2DeliveryGetBatchResult, sceSystemLogger2DeliveryGetResult, sceSystemLogger2DeliverySetBatchDeliveryEvent and sceSystemLogger2DeliveryTerminate](../../../core/libs/prx/libSceSystemLogger2Delivery/Export.cpp) (libSceSystemLogger2Delivery) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSystemLogger2Delivery.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceSystemLogger2NativeQueueClient.md b/docs/dev/debt/libSceSystemLogger2NativeQueueClient.md new file mode 100644 index 0000000000..378c259200 --- /dev/null +++ b/docs/dev/debt/libSceSystemLogger2NativeQueueClient.md @@ -0,0 +1,5 @@ +# libSceSystemLogger2NativeQueueClient technical debt + +### Unknown function info + +- [sceSystemLogger2NativeQueueClientCheckAndMaskData, sceSystemLogger2NativeQueueClientGetEvent, sceSystemLogger2NativeQueueClientSetDeliveryEnable, sceSystemLogger2NativeQueueClientSetDeliveryResult and sceSystemLogger2NativeQueueClientSetNativeQueueConfig](../../../core/libs/prx/libSceSystemLogger2NativeQueueClient/Export.cpp) (libSceSystemLogger2NativeQueueClient) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSystemLogger2NativeQueueClient.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 17773650c4..176b34aa6a 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -2,19 +2,155 @@ ### Silent stubs +- [sceShellCoreUtilActivateAbort, sceShellCoreUtilActivateStart, sceShellCoreUtilActivateStartAsync, sceShellCoreUtilActivateTerm](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) return zero with the recovered handle argument. Activation service state is not modeled. + - [libSceSystemService](../../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick`, `sceSystemServiceReportAbnormalTermination`, `sceSystemServiceDisableMusicPlayer`, `sceSystemServiceReenableMusicPlayer`, `sceSystemServiceDisableMediaPlay` and `sceSystemServiceReenableMediaPlay` return success and do nothing: the host has no auto power-off timer to reset, no system crash reporter to notify, no system music player to pause or resume and no system media playback to disable. PPSA01521 calls `sceSystemServiceDisableMediaPlay` during start-up, and its wrappers for both pass no arguments - [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 launches no web browser, so the call fails and nothing is shown. RommPS PPSA76677 then tells the player to scan its QR code instead - [sceSystemServiceShowControllerSettings](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without showing the controller settings: AnyPS5 has no system UI to open. Cat Quest III (PPSA15065) imports it +- [sceSystemServiceShowDisplaySafeAreaSettings](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without showing the display safe area settings dialog: AnyPS5 has no system UI to open. PPSA03332 imports it, and the no-argument signature is assumed: [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) has `sceSystemServiceShowControllerSettings` but not this call - [sceSystemServiceOpenChallengeActivity](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without opening the challenge activity: AnyPS5 has no system UI to open. Cat Quest III (PPSA15065) imports it +- [sceSystemServiceOpenTournamentOccurrence](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without opening the tournament occurrence: AnyPS5 has no system UI to open ### Unknown function info +- [sceSystemServiceAddLocalProcessForPsmKit, sceSystemServiceKillLocalProcessForPsmKit](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 runs one process and starts or stops no other, so both throw without reading their arguments. The parameters follow Forever Launcher's own firmware 5.50 probes (`ps5/probes/localproc` in [bizkut/ps5-forever](https://github.com/bizkut/ps5-forever)): a path, a null-terminated argv and a 0x90-byte parameter block in, a handle out; the kill takes that handle. Their results on the console are unknown. Forever Launcher (PPSA99030) imports both and stops at load without them. - [sceSystemServiceLaunchApp](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 runs one title and cannot start another, so every call throws without reading its arguments. The (title ID, null-terminated argv, launch parameters) signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L141-L148), whose `AppLauncher` Prospero Explorer (PPSA99109) and Prospero Multi Tools (PPSA99110) import it through and which passes a 0x20-byte launch context or null; the console's argument checks and errors are unknown -- [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the console's error for an unavailable web browser is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D) for any arguments. The (uri, param) signature follows [RommPS](https://github.com/s0liton/RommPS/blob/754d4d60f25938a5136b9e2efcd074313c4386c5/platform/ps5/app/rommps/examples/rommps/kit/credits.cpp#L86-L95), which passes a URL and a null param and only tests the result for zero; the param layout and the console's argument checks are unknown -- [sceSystemServiceParamGetString](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw -- [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 +- [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp): the native export takes one URI argument. Null input, the 6015-byte length limit and supported scheme checks are recovered. The host still returns kernel `EOPNOTSUPP` for valid input; a missing browser service's native error is unresolved. +- [sceSystemServiceParamGetString](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name. As in a payload process on a PS5 (firmware 13.60), the name is written NUL-padded to the whole buffer, and a buffer under 65 bytes, a null buffer or any other parameter returns `SYSTEM_SERVICE_ERROR_PARAMETER` without writing. Parameters 0 to 40, 100 to 110, 1000 to 1010, -1 and `INT_MAX` were tried there; the others are assumed to fail the same way. PPSA26344 at `0xA87935` passes parameter 6 with a 65-byte buffer +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format and time format stay fixed +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - parameters 1 to 5, 100 and 400 answer; 101 to 111, 200 to 213 and 300 throw (see the integer system parameters entry); every other parameter, and a null value pointer, returns 0x80A10003 without writing the value. Inside a retail title on a PS5 (firmware 13.40, measured 2026-10-19 through ptrace remote calls in the game process) parameters 0, 6, 7 (game parental level), 8 to 20, 401, 999, 1000 (enter button assignment) and 1001 to 1008 returned 0x80A10003, as did a null value pointer; 1 to 5 and 400 answered, and 100 answered the value 0, which is returned here too, although what the parameter means is not known. Whether the console writes the value on that error is not measured +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the time zone (parameter 4) and summer time (parameter 5) follow the host's local time zone at the moment of the call. Titles compute the local time as UTC plus the time zone in minutes plus 60 minutes when summer time is 1 ([Apollo](https://github.com/bucanero/apollo-ps4/blob/252a9d5a187c463994263009851a179f4f910c49/source/exec_cmd.c#L70-L92), [GoldHEN's frame logger](https://github.com/GoldHEN/GoldHEN_Plugins_Repository/blob/6c3a06f53d9caebadfb0aeed9bb94119012010a7/plugin_src/frame_logger/source/main.c#L60-L82), and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/413d2e3518fb435f40971d238fe0ecae7ee551da/src/SharpProspero/Platform/SystemParameters.cs#L75-L79) calls them the offset from UTC in minutes and whether summer time is in effect), so the time zone is the host's current offset east of UTC less the summer time shift while summer time is in effect: 60 minutes on Linux, and on Windows the C runtime's own summer time bias (`_get_dstbias`), which is also what shifted its local time. For a host zone whose summer time is not one hour, Linux keeps the sum right and the standard offset wrong, and Windows the opposite; how the console reports such a zone is unknown. libkernel's `sceKernelConvertUtcToLocaltime`, `sceKernelConvertLocaltimeToUtc` and `sceKernelGettimezone` still use UTC +- [sceSystemServiceInitializePlayerDialogParam](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - a null param throws: what the console does with it is not measured. The console run in [prosper](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/tests/data/console_oracle/nethttp.golden.tsv#L46) shows only the 0x40 bytes written (size 0x40, user ID -1 at 0xC, zeros), so the field names and the mode and target account fields follow [SharpProspero's struct](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L637-L655), and the `void` return [its declaration](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L419-L426); the console left 0x800693020 in `rax`, not an SCE error code. Closes when: the null case is measured on a PS5 + +- [libSceSystemService pending exports](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): unreviewed argument lists and service behavior remain unresolved. The C decompilation contains 381 of the 477 pending names; additional exports exist as direct assembly jumps, so absence from that listing does not imply absence from the module. Confirmed argument lists are being restored independently of their protected throwing bodies; remaining empty declarations must not be treated as proof of a zero-argument ABI. +- [sceSystemServiceLaunchSystemDeeplink](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilLaunchByUriParamInit](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceSystemStateMgrTickVideoPlayback](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceSystemServiceTelemetrySetData2WithUserId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilStartUmmDictation](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceSystemServiceGetSpotifyTitleId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceSystemServiceGetAppStatusListContainsDaemon](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceSystemServiceSwitchToVRApp](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceSystemServiceSwitchToNonVRApp](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceLncUtilGetSpmemAllocatorConfigValue](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrInitialize](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrTerminate](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrCreateLocalSandbox](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrDestroyLocalSandbox](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrSwitchLocalSandbox](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrGetCurrentLocalSandboxId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrGetLocalSandboxIdList](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrGetLocalSandboxInfo](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrSetLocalSandboxInfo](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrIssueStreamingSandboxInfo](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceReportSequenceHandlerRequestReport](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceReportSequenceHandlerCancelReport](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceReportSequenceHandlerExistsEvent](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceReportSequenceHandlerUpdateEventCache](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceReportSequenceHandlerGetEventCache](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceLncUtilRegisterCallEntries](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceLncUtilGetSpmemConfigJson](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceLncUtilExportSpmemConfigJson](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceLncUtilImportSpmemConfigJson](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilExitAppWithValue](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilNpDebugSpecialNotification](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilIsDddTemperatureDanger](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilIsM2StorageTemperatureDanger](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetAvailableSizeM2](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilNotifyConnectAccountStatusChanged](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilIsInternalCronosUser](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilChangePbtcMode](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilTelemetryCacheData](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilMicMuteKeyPressed](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilSwitchMicMute](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilNotifyStartEventToUniversalMicMute](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilIsLotusMicActivated](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilResetAllNoticeScreenSkipFlag](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilResetNoticeScreenSkipFlag](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetNoticeScreenSkipFlagByAppId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetPasscodeInputType](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetPasscodeDisplayInfo](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilNotifySystemResumeTelemetryInfo](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilUpdateTelemetryToken](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetVRBaseMatRectangles](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilSetVRBaseMatRectangles](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilIsLaunchBlockDialog](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetConsoleToken](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetDeviceIdByDeviceUniqueId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetDeviceUniqueIdByDeviceId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetStreamingClientInfo](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilRequestCffUpdate](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetCffConfig](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilRemoveCffConfigFile](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilDebugGetNumberOfCffItems](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilDebugGetCffInfo](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilDebugSetCffStatus](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetPeripheralConnectionSessionId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetCdlgEnabledFreezeRendering](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetNpTitleId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilIsCronosInGameCommerceEnabled](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilLemSettingGet](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilLemSettingSet](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilLemSettingGetList](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilLemSettingReset](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilIsEChassisVariant](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceSystemStateMgrGetRebootCause](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCoredumpSequenceHandlerUpdateEventCache](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCoredumpSequenceHandlerGetEventCache](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCoredumpSequenceHandlerGetWorkingEventSummary](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCoredumpSequenceHandlerGetCoredumpStatus](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCoredumpSequenceHandlerGetCoredumpStatusByAppId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCoredumpSequenceHandlerCancelCoredump](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature ### Functional -- [sceSystemServiceLoadExec](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - besides `"exit"`, it starts the title's own `/app0/eboot.bin` or another executable under `/app0` that the relinker converted with `--executable`, which is looked up beside the running one (`/app0/picker/picker.bin` as `picker.picker.bin` with the running executable's extension). The main executable is the one `APS5_EBOOT` names, which the first call sets to its own path. After the guest shutdown the new executable gets the argument list as its command line, after its own path (`CreateProcessW` on Windows, `execv` on Linux), and the old process ends without running host destructors and without waiting, so its direct memory is released before the new one allocates. [VLC for PS5](https://github.com/ZiZc3/VLC/blob/86bb7d474a303f62bb3c2fc12f728db4846085b8/src/platform_ps5.c#L565-L571) (PPSA85300) relaunches itself with no arguments; RetroArch (PPSA99169) starts its frontend picker with `{"", NULL}`, and the picker starts `eboot.bin` with `{"--ps5-mode=retroarch", NULL}` or `/app0/es-de/es-de.bin` ([picker.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/frontends/picker/picker.cpp#L444-L446)), so the list holds only arguments. What the console puts in `argv[0]` is not known; here it is the host path. On Windows the new process is a separate process, so a shell or script that started the title sees it end. Paths outside `/app0`, unconverted executables and macOS hosts throw +- [sceSystemServiceLoadExec](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - besides `"exit"`, it starts the title's own `/app0/eboot.bin` or another executable under `/app0` that the relinker converted with `--executable`, which is looked up beside the running one (`/app0/picker/picker.bin` as `picker.picker.bin` with the running executable's extension). The main executable is the one `APS5_EBOOT` names, which the first call sets to its own path. After the guest shutdown the old process ends without running guest exit handlers or host destructors; as during `exit`, a guest thread that calls a library the shutdown has stopped waits for the end instead of unwinding. `"exit"` ends the process after the host shutdown without running the title's `atexit` and `__cxa_atexit` handlers, since the console's shell closes the title instead; a line the title writes just before through its own thread, such as a forwarded standard error line, may not be printed. [VLC for PS5](https://github.com/ZiZc3/VLC/blob/86bb7d474a303f62bb3c2fc12f728db4846085b8/src/platform_ps5.c#L565-L571) (PPSA85300) relaunches itself with no arguments; RetroArch (PPSA99169) starts its frontend picker with `{"", NULL}`, and the picker starts `eboot.bin` with `{"--ps5-mode=retroarch", NULL}` or `/app0/es-de/es-de.bin` ([picker.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/frontends/picker/picker.cpp#L444-L446)), so the list holds only arguments. A list with a first entry becomes the whole guest `argv`, with no program name in front ([LoadExec.cpp](../../../core/libs/prx/libSceSystemService/LoadExec.cpp)): a [PS5 RetroArch console transcript](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/evidence/loadexec-second-image/relaunch-test.jsonl) records a restart with one argument entering with `argc=1`, `argv[0]="--ps5-relaunch=1"`, and a launch from the home screen with `argc=1`, `argv[0]=""` ([relaunch_ps5.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/src/relaunch_ps5.cpp#L161-L164)). For such a list Linux replaces the process with `fexecve`, and macOS with `posix_spawn` and `POSIX_SPAWN_SETEXEC`: the PID and the standard streams stay, the other descriptors are closed and the working directory becomes the executable's. Windows starts a suspended process that inherits only the standard streams and a handle of the old process, and its entry stub waits for the old process to end before it loads the title's libraries, so a shell or script that started the title sees it end. A null list or one without a first entry keeps the earlier launch: the new executable gets only its own path as its command line (`CreateProcessW` on Windows, `execv` on Linux and macOS), so its `argv[0]` is the host path, and the old process ends without waiting for it. What the console passes for such a list is not known, nor are its argument limits and error codes. Paths outside `/app0` and unconverted executables throw - [Running application ID](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `sceSystemServiceGetAppIdOfRunningBigApp` reports 1 as the running title's app ID. The value is AnyPS5's own: the console's shell assigns app IDs and their values are not known here, so other calls that take an app ID must use the same one. `sceSystemServiceKillApp` with that ID and the arguments -1, 0 and 0, which [SwanStationPS5](https://github.com/darkxex/SwanStationPS5/blob/4cdc981d1a64ffb7f2af73f7a27a9e91a3cb5eb4/src/main.c#L1050-L1058) passes to close itself and [ps5-payload-dev's hbldr](https://github.com/ps5-payload-dev/websrv/blob/46374edee6627885abac717de24da7a1d73af6f1/src/ps5/hbldr.c#L425-L435) passes to close the running game, ends the title as its own `exit(0)` would: the guest's shutdown and `atexit` handlers run and the process exits with status 0. On the console the call returns and the shell then ends the title, and whether those handlers run there is unknown; here there is no shell, so the call does not return. Another app ID or other arguments throw. +- [sceLncUtilKillApp, sceLncUtilKillAppWithReason](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the running title's app ID ends the title as `sceSystemServiceKillApp` does, which [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L219-L227) describes as a wrapper of `sceLncUtilKillApp`; the signatures follow its [launcher utility bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L262-L279). Another app ID throws, and so does a reason other than 0, whose meaning and effect on the console are not documented +- [LoadExec descriptors](../../../core/libs/prx/libSceSystemService/LoadExec.cpp) (libSceSystemService) - on Linux, a call with an argument list replaces the process with `fexecve`, which first closes every descriptor above 2, and on macOS with `posix_spawn`, which closes them through `POSIX_SPAWN_CLOEXEC_DEFAULT`; a call without one uses `execv`, and the guest's descriptors stay open across it. On both paths descriptor 2 first gets back the host standard error a guest redirect kept, so the runtime's output does not follow the title's redirect (RetroArch's trace file, VLC's log) into the next executable. Which descriptors the console passes to the new process is not known + + +- [LncUtil process and status payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): local-process creation transmits a 72-byte parameter block whose fields remain unresolved. Title-ID output occupies 16 bytes; category output is a four-byte string, while category type and launched-user outputs are 32-bit scalars. The argument lists are recovered through the indirect dispatch targets rather than the decompiler's apparent zero-argument wrappers. + +- [LncUtil event and diagnostic payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): Daemon and shell-UI events are distinct output structures. GetDbgExecutablePath writes up to a 1024-byte protocol output and forwards a 64-bit selector whose meaning is unresolved. Boolean outputs and the two direct boolean budget/mode queries are confirmed. + +- [LncUtil launch and lifecycle payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): Flag values and service-side focus/resume/suspend/exception behavior remain unresolved. Launch and load-exec use null-terminated argument vectors; asynchronous launch/kill write a 32-bit request ID. The argument positions and transported widths are confirmed, but the service operations remain protected stubs. + +- [ShellCoreUtil application data](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): GetAppLaunchTypeInfo writes eight bytes whose fields are unresolved. Application-data and launched-parameter keys are null-terminated strings, with 32-bit capacities/outputs. Format/fsck paths and mini-app return values are strings. File-formatting and service implementations remain protected stubs. + +- [ShellCoreUtil queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): launched-parameter strings have signed 32-bit capacities; checker strings use machine-sized capacities and one or two string keys. Cloud status and device-index outputs are 32-bit scalars; the timeout variant consumes one byte. Partition-size output and the direct temporary-content free-size return are 64-bit. The service-side checker protocol remains unresolved. Device-index scene output uses a 32-bit native store despite the managed wrapper using a byte-backed scene enum; the native output width is preserved. + +- [ShellCoreUtil storage and HID queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): partition free sizes and required application-content size are 64-bit outputs; fsck progress takes a title ID and a 32-bit progress output. HID string queries transport a 64-bit capacity and 32-bit selector, while output-vector lengths sign-extend the capacity's low 32 bits. HID selectors are configuration indices; the wrapper declares 32-bit capacities while the native code transports 64 bits, so their declared width remains unresolved. IDU mode byte values remain unresolved. Streaming status and the compositor-profiler return are boolean; the compositor-profiler and current GPU-load queries consume no caller arguments. + +- [ShellCoreUtil account and privacy payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): Pbtc user information occupies 800 bytes; privacy-event data occupies eight bytes after two string keys, each limited to 32 characters. The five account-information outputs occupy 8, 104, 136, 1024 and 1032 bytes; their fields remain unresolved. Category-default list output has a byte capacity and unresolved element format. Remote-play/share-play/social-screen outputs are boolean; application display-state queries take a 32-bit application ID. Only the support-site query signature is restored; no endpoint content is included. + +- [ShellCoreUtil telemetry and user queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): telemetry-token output is 4112 bytes, including an eight-byte service result at offset 4104; its meaning remains unresolved. UI status is a 64-bit event mask; VM statistics and workaround-file strings use machine-sized capacities. Eye-distance adjustment writes one byte. + +- [ShellCoreUtil launch and status arguments](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): Navigation consumes one 32-bit value, not the phantom vector/register arguments shown by decompilation. Workaround checks and mount requests take strings. Logout-state queries transmit two 32-bit values and return a byte output; selector meanings remain unresolved. + +- [ShellCoreUtil notifications](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): IDU notifications use a signed 64-bit identifier, optional text and a 32-bit type. PSM events carry a 32-bit event ID and a string. + +- [ShellCoreUtil state setters](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): UI updates take a 64-bit status and mask; app data takes two strings. Display-state setters mask their low bits, so source enum names and declared widths cannot be inferred from those masks alone. Streaming/impose/profiler setters test all 32 bits. User-focus reads take an application ID and return its user ID; writes apply a user ID to the current application. + +- [ShellCoreUtil dialog parameters](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): the 24-byte error-dialog parameter contains a message pointer at offset zero, a null-terminated vector of string arguments at offset eight and a byte flag at offset 16. Field offsets and size are asserted; the flag meaning remains unresolved. Start/stop streaming-game and unmount requests take strings. Bus-transfer testing writes one byte after a string input. + +- [SystemService activation and process signatures](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): activation wrappers validate a machine-sized handle and expose 32-bit status/result outputs or a byte activation flag. Shell activation initialization also receives a type selector; managed callers confirm the machine-sized handle consumed by constant-return start/abort/terminate exports. SystemService local-process parameters differ from LncUtil parameters and are kept as distinct types; general creation reads through offset 71, JVM/PS2 creation reads two 32-bit fields at offsets four and eight. Install status writes 12 bytes and application status writes 16 bytes; their remaining field meanings are unresolved. + +- [SystemService query payloads](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): daemon events write 12 bytes; local-process entries occupy 36 bytes, consisting of a 32-bit value and 32-byte name. PS-button extraction reads a 16-byte event and rearranges its fields into a distinct 16-byte output; semantic names remain unresolved. Privacy definition/version queries take string keys and distinct output types; their layouts remain unresolved. Workaround information writes four bytes at offset zero and 16 bytes at offset eight. + +- [SystemService direct aliases](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): assembly-only jumps now share the confirmed signatures of their LncUtil/ShellCoreUtil targets for application-ID lists, remote-play/UI status, navigation, controller-focus permissions and boolean budget/status queries. Aliases of unresolved targets still require target or caller analysis. + +- [LncUtil collapsed dispatch stubs](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): assembly table targets confirm application-ID lookup takes a title-ID string, current/focused status outputs occupy 64 bytes, GPU-crash status occupies 1320 bytes, and dialog activation writes one byte. Shared-memory registration takes a string. The protected service implementations remain unchanged; collapsed assembly procedures are not missing exports. + +- [SystemService launch and telemetry arguments](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): store-launch parameters contain only a four-byte size; web-app launch takes one string argument. Process lifecycle wrappers forward the verified LncUtil argument lists. TelemetrySetData takes a string key, binary payload and machine-sized length; TelemetrySetData2 takes two strings. Token storage consumes a 32-bit identifier and string; only its signature is restored, with no token values included. + +- [SystemStateMgr and modal signatures](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): keep-main-on-standby takes a string, reboot timer consumes a 32-bit mode and wake-up consumes a 16-bit trigger. Suspend/resume progress forwards a string to kernel logging. Disc readiness, shell shutdown progress and standby enablement return booleans. Modal open takes a string and close takes a 32-bit dialog ID. Platform privacy setting writes a 32-bit value; the kernel platform query it calls consumes no caller arguments. + +- [USB storage aliases](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) and [USB exports](../../../core/libs/prx/libSceUsbStorage/Export.cpp): direct-jump signatures are matched to the supplied USB module. RequestMap/RequestMapWSB take six arguments; the two extra helper arguments are internal. IsExist takes a device, directory and byte output, with a separate boolean failure result. Callbacks receive an event pointer and user data. Device information writes 824 bytes, device lists hold eight 32-bit IDs, and mount-point outputs occupy 128 bytes. Event/device field meanings and initialization attributes remain unresolved. + +- [Assembly-only shell and clock queries](../../../core/libs/prx/libSceSystemService/PendingExports.cpp): format/fsck progress returns a float in XMM0. Impose-menu, accessibility-zoom and suspend-confirmation queries take an application ID and return a boolean. Suspend-blacklist lookup forwards a 32-bit ID to SysCore. Clock changes take a string and 32-bit value; CPU pstate takes a 32-byte input structure whose fields remain unresolved, and memory pstate takes one 32-bit value. + +- [Integer system parameters](../../../core/libs/prx/libSceSystemService/Export.cpp): registry-backed parameters 101–111, 200–213 and 300 throw because their settings are not modeled; 100 answers the value 0 measured on a PS5. Language results omit the console SDK-version compatibility remapping, and parameter 400 does not reproduce platform-dependent availability. + +- [Tournament launch parameters](../../../core/libs/prx/libSceSystemService/Export.cpp): occurrence parameters occupy 32 bytes and contain two string pointers and a user ID. The final string semantics are unresolved. Event-details and team-profile launches retain an unused first argument slot of unknown declared type. The daemon blocking-event receive argument is passed unchanged as 64 bits to IPC; its meaning remains unresolved. Protected bodies in [PendingExports.cpp](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) remain unchanged. +- [sceSystemServiceIsBgmPlaying, sceShellCoreUtilIsBgmPlaying](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - not measured on a PS5: both return 0. In [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/system/systemservice.cpp) they report whether its libSceMusicPlayerService is playing; there is no libSceMusicPlayerService here, so no background music can be playing diff --git a/docs/dev/debt/libSceSystemTts.md b/docs/dev/debt/libSceSystemTts.md new file mode 100644 index 0000000000..593c01117d --- /dev/null +++ b/docs/dev/debt/libSceSystemTts.md @@ -0,0 +1,5 @@ +# libSceSystemTts technical debt + +### Unknown function info + +- [sceSystemTtsCancel, sceSystemTtsIsAccessibilityAvailable, sceSystemTtsRegisterCallback, sceSystemTtsSpeakRequest and sceSystemTtsUnregisterCallback](../../../core/libs/prx/libSceSystemTts/Export.cpp) (libSceSystemTts) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSystemTts.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceTEEClient.md b/docs/dev/debt/libSceTEEClient.md new file mode 100644 index 0000000000..04823e75e6 --- /dev/null +++ b/docs/dev/debt/libSceTEEClient.md @@ -0,0 +1,5 @@ +# libSceTEEClient technical debt + +### Unknown function info + +- [libSceTEEClient pending exports](../../../core/libs/prx/libSceTEEClient/Export.cpp) - 10 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceTEEClient.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceTextToSpeech.md b/docs/dev/debt/libSceTextToSpeech.md new file mode 100644 index 0000000000..4098de045d --- /dev/null +++ b/docs/dev/debt/libSceTextToSpeech.md @@ -0,0 +1,5 @@ +# libSceTextToSpeech technical debt + +### Unknown function info + +- [sceTextToSpeechCancelImpl, sceTextToSpeechCloseImpl, sceTextToSpeechGetStatusImpl, sceTextToSpeechInitImpl, sceTextToSpeechOpenImpl, sceTextToSpeechPauseImpl, sceTextToSpeechResumeImpl, sceTextToSpeechSpeakContinuousImpl, sceTextToSpeechSpeakImpl and sceTextToSpeechTermImpl](../../../core/libs/prx/libSceTextToSpeech/Export.cpp) (libSceTextToSpeech) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceTextToSpeech.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceTextToSpeech2.md b/docs/dev/debt/libSceTextToSpeech2.md index fb8dc582dc..58ba1298f6 100644 --- a/docs/dev/debt/libSceTextToSpeech2.md +++ b/docs/dev/debt/libSceTextToSpeech2.md @@ -6,5 +6,4 @@ ### Unknown function info -- [sceTextToSpeech2Initialize](../../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - the console's error for an unavailable speech engine is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D). PPSA01317 passes a param that starts with 0x2000000 and only tests the result for non-zero; the other exports it imports are declared without parameters and return the same error -- [sceTextToSpeech2GetSystemStatus](../../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - unknown signature; returns `EOPNOTSUPP` like `sceTextToSpeech2Initialize` +- [sceTextToSpeech2Initialize](../../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - the console's error for an unavailable speech engine is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D). PPSA01317 passes a param that starts with 0x2000000 and only tests the result for non-zero. diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index e1fb5ff21a..d539ce9772 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -2,10 +2,15 @@ ### Unknown function info -- [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - `_sceUltUlthreadRuntimeOptParamInitialize`, `_sceUltUlthreadRuntimeCreate` and `_sceUltUlthreadCreate` are assumed to take the same arguments as the existing exports without the underscore, including the trailing build version. `sceUltUlthreadTryJoin` is assumed to return `ULT_ERROR_BUSY` while the ulthread runs, and `sceUltUlthreadRuntimeDestroy` to return `ULT_ERROR_BUSY` while an ulthread created on the runtime is not joined. Neither error has been confirmed on PS5 hardware -- [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. -- [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 +- [Bound condition variables](../../../core/libs/prx/libSceUlt/Export.cpp) use the creation-time mutex, inferred from PPSA28061 callers of `_sceUltConditionVariableCreate`; the wait caller reloads only the condition argument. Error precedence, busy/state errors, FIFO signal selection and unsupported recursive waits have not been measured on PS5 hardware. Non-null options and waits at recursion depth above one throw. A live condition prevents destroying its bound mutex. Finalization follows the existing host mutex lifecycle: it invalidates conditions and wakes queued or reacquiring waiters with a state error. Scheduling priority and build-version validation are not implemented. +- [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - Runtime option defaults still omit the native header, stack size, worker affinity and priority fields; affinity depends on unresolved process configuration queries. Concurrent join ownership is not modeled: the native runtime distinguishes an ordinary running thread from one with another joiner. Host threads do not use the guest context or worker scheduler. +- [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - Queue-data pool destruction and shared slot limits still require comparison with the native node allocator. +- [`sceUltConditionVariableCreate` and `sceUltConditionVariableOptParamInitialize`](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) are absent from the console export list, where only the underscored variants `_sceUltConditionVariableCreate` and `_sceUltConditionVariableOptParamInitialize` appear; their non-underscored signatures are assumed. Condition variable option parameter initialization only zeroes 128 bytes, and its layout without the common header remains unverified on hardware. +- [Ult factory and creation exports](../../../core/libs/prx/libSceUlt/Export.cpp) without a leading underscore are absent from the supplied module; their signatures remain unresolved. Diagnostic signatures and synchronization argument lists are recovered. + +- [sceUltMutexTryLock, the reader-writer lock and sceUltUlthreadGetSelf, sceUltUlthreadYield and sceUltUlthreadExit](../../../core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp) (libSceUlt) - the signatures follow the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.cpp) (a 256-byte lock and 128-byte option structures), not a PS5 binary. The rules are assumed: readers may take the lock while a writer waits, a thread that holds the write lock gets `ULT_ERROR_STATE` for any further lock instead of waiting on itself, a reader asking for the write lock waits on its own read lock, and the try forms return `ULT_ERROR_BUSY`. Outside an ulthread, `sceUltUlthreadGetSelf`, `sceUltUlthreadYield` and `sceUltUlthreadExit` return `ULT_ERROR_STATE`; inside one they act on the guest thread it runs on, since ulthreads are not scheduled cooperatively here ### Functional -- [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. +- [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; the guest object layouts remain incomplete. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. +- [sceUltWaitingQueueResourcePoolDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) refuses with `ULT_ERROR_STATE` only when a thread is currently waiting on a pool-backed mutex, semaphore or queue, matching the PS5 libSceUlt disassembly (0x473a in or55417wcDk); bound but idle objects do not block pool destruction. Waiting-queue pool guest work area node accounting is not implemented. diff --git a/docs/dev/debt/libSceUpdateService.md b/docs/dev/debt/libSceUpdateService.md new file mode 100644 index 0000000000..73c61c3d32 --- /dev/null +++ b/docs/dev/debt/libSceUpdateService.md @@ -0,0 +1,5 @@ +# libSceUpdateService technical debt + +### Unknown function info + +- [sceUpsrvGetCompletedTaskId, sceUpsrvGetRebootFlag, sceUpsrvGetSystemExUpdateTaskState, sceUpsrvInitialize, sceUpsrvPostErrorLog, sceUpsrvResumePausedTask, sceUpsrvSetFakeVersion, sceUpsrvStartUpdateSession, sceUpsrvSwitchSystemExBank, sceUpsrvSwitchSystemExBankWithoutVerify, sceUpsrvTerminate, sceUpsrvUpdateCancelBDUpdate, sceUpsrvUpdateCheckCancel, sceUpsrvUpdateCheckCreateHandler, sceUpsrvUpdateCheckDestroyHandler, sceUpsrvUpdateCheckDoCheckSystem, sceUpsrvUpdateCheckDoCheckSystemForDevKit, sceUpsrvUpdateCheckDoCheckSystemForSettings, sceUpsrvUpdateCheckDoCheckSystemWithPupInfo, sceUpsrvUpdateCreateUpdateTask, sceUpsrvUpdateDestroyUpdateTask, sceUpsrvUpdateDoCheckBootUpdate, sceUpsrvUpdateDoUpdateSystemEx, sceUpsrvUpdateDoUpdateWithEntitlement, sceUpsrvUpdateDoUpdateWithPupInfo, sceUpsrvUpdateDoUpdateWithPupPath2, sceUpsrvUpdateGetBetaAgreementUrl, sceUpsrvUpdateGetEulaDisplay, sceUpsrvUpdateGetEulaStr, sceUpsrvUpdateGetEulaStrLen, sceUpsrvUpdateGetFeatureListUrl, sceUpsrvUpdateGetFeatureListUrlForSystemExUpdate, sceUpsrvUpdateGetFeatureListUrlForUpdate, sceUpsrvUpdateGetImageWritingProgress, sceUpsrvUpdateGetProgress, sceUpsrvUpdateGetUpdateTaskInfo, sceUpsrvUpdateGetUpdateTaskMgrInfo, sceUpsrvUpdateSetEulaAccept, sceUpsrvUpdateStartUpdateTask and sceUpsrvVerifySystemExBank](../../../core/libs/prx/libSceUpdateService/Export.cpp) (libSceUpdateService) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceUpdateService.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceUrlConfigResolver.md b/docs/dev/debt/libSceUrlConfigResolver.md new file mode 100644 index 0000000000..a770778d1b --- /dev/null +++ b/docs/dev/debt/libSceUrlConfigResolver.md @@ -0,0 +1,6 @@ +# libSceUrlConfigResolver technical debt + +### Unknown function info + +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceUrlConfigResolverGetReleaseVersion`](../../../core/libs/prx/libSceUrlConfigResolver/Export.cpp), [`sceUrlConfigResolverGetSysVer`](../../../core/libs/prx/libSceUrlConfigResolver/Export.cpp), [`sceUrlConfigResolverIsUpdated`](../../../core/libs/prx/libSceUrlConfigResolver/Export.cpp), [`sceUrlConfigResolverUpdate`](../../../core/libs/prx/libSceUrlConfigResolver/Export.cpp). +- [sceUrlConfigResolverGetAppConfig, sceUrlConfigResolverGetDefaultQueryParameter, sceUrlConfigResolverGetDeviceId and sceUrlConfigResolverGetJscHeapSizeSoftLimit](../../../core/libs/prx/libSceUrlConfigResolver/Export.cpp) (libSceUrlConfigResolver) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceUrlConfigResolver.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceUsbStorage.md b/docs/dev/debt/libSceUsbStorage.md new file mode 100644 index 0000000000..e1bc7fc86b --- /dev/null +++ b/docs/dev/debt/libSceUsbStorage.md @@ -0,0 +1,6 @@ +# libSceUsbStorage technical debt + +### Unknown function info + +- [sceUsbStorageClearFakeWriteProtection, sceUsbStorageSetFakeMapLockForDebug, sceUsbStorageSetFakeWriteProtection](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceUsbStorage.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. +- [sceUsbStorageRequestMapWSB](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - its prototype follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L245-L246), not measured on a console; the throwing declaration only resolves imports. diff --git a/docs/dev/debt/libSceUsbStorageDialog.md b/docs/dev/debt/libSceUsbStorageDialog.md new file mode 100644 index 0000000000..58151aaf70 --- /dev/null +++ b/docs/dev/debt/libSceUsbStorageDialog.md @@ -0,0 +1,9 @@ +# libSceUsbStorageDialog technical debt + +### Silent stubs + +- [libSceUsbStorageDialog](../../../core/libs/prx/libSceUsbStorageDialog/Export.cpp) (libSceUsbStorageDialog) - the USB storage dialog runs the common-dialog state machine without showing a UI and reports the dialog as canceled by the user (`COMMON_DIALOG_RESULT_USER_CANCELED = 1`). `sceUsbStorageDialogOpen` transitions to `RUNNING` (2), while `sceUsbStorageDialogUpdateStatus` and `sceUsbStorageDialogClose` transition to `FINISHED` (3) before returning results, matching other common dialogs in the repository. + +### Unknown function info + +- [libSceUsbStorageDialog](../../../core/libs/prx/libSceUsbStorageDialog/Export.cpp) - the service-specific result status at offset 12 is not produced because the UI always cancels. The 80-byte result header and SDK-dependent validation are recovered; the remaining 64 bytes are untouched by the native wrapper. The purpose of the Open parameter size field remains unverified; the native wrapper does not validate it. diff --git a/docs/dev/debt/libSceUsbd.md b/docs/dev/debt/libSceUsbd.md index 85e8ca7ba8..f23c18fd72 100644 --- a/docs/dev/debt/libSceUsbd.md +++ b/docs/dev/debt/libSceUsbd.md @@ -3,3 +3,7 @@ ### Silent stubs - [libSceUsbd](../../../core/libs/prx/libSceUsbd/Export.cpp) passes the calls through to the host with [libusb](https://github.com/libusb/libusb) built from the `3rdparty/libusb-cmake` submodule. Devices, handles and descriptors are libusb's own objects: the device, config, interface and endpoint descriptor layouts of [FreeBSD 11.4 libusb](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb.h) match libusb's field for field, which `static_assert` pins. Each one handed to the title is tracked, and a pointer this library did not return throws. Errors are libusb's codes mapped to `0x80240000 - code`, with `LIBUSB_ERROR_OTHER` at `0x802400FF`, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/usbd/usbd.cpp); the null arguments FreeBSD libusb rejects ([libusb10.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10.c), [libusb10_desc.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_desc.c), [libusb10_io.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_io.c)) return `0x80240002`, and `sceUsbdOpen` and `sceUsbdGetActiveConfigDescriptor` throw on a null device, which it dereferences. A submitted transfer is copied into a host transfer whose completion copies the status, actual length and iso packet results back and then calls the title's callback from `sceUsbdHandleEventsTimeout`; transfer flags other than short-not-ok and add-zero-packet throw. `sceUsbdAttachKernelDriver` returns 0 as FreeBSD's stub does, `sceUsbdCheckConnected` reports a device gone when reading its configuration fails with `LIBUSB_ERROR_NO_DEVICE` (FreeBSD asks the kernel directly), and `sceUsbdEventHandlingOk` returns 1. When the host has no USB access, initialization keeps the empty device list and event handling waits for its timeout. Opening, descriptors, string and control requests, interface claims and an asynchronous control transfer were checked against an Xbox 360 controller on Windows; the transfer layout, the argument lists, the error codes and isochronous transfers are unverified on PS5 + +### Unknown function info + +- [sceUsbdClearHalt, sceUsbdDetachKernelDriver, sceUsbdEventHandlerActive, sceUsbdGetConfigDescriptorByValue, sceUsbdGetConfiguration, sceUsbdGetDescriptor, sceUsbdGetDevice, sceUsbdGetDeviceSpeed, sceUsbdGetMaxIsoPacketSize, sceUsbdGetMaxPacketSize, sceUsbdGetStringDescriptorAscii, sceUsbdHandleEvents, sceUsbdHandleEventsLocked, sceUsbdLockEventWaiters, sceUsbdLockEvents, sceUsbdOpenDeviceWithVidPid, sceUsbdSetInterfaceAltSetting, sceUsbdTryLockEvents, sceUsbdUnlockEventWaiters, sceUsbdUnlockEvents, sceUsbdWaitForEvent](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - signatures follow the console type database in [orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L2268-L2322), but their behavior is unknown and each call throws; not measured on a console diff --git a/docs/dev/debt/libSceUserService.md b/docs/dev/debt/libSceUserService.md index fe81330fce..e320add914 100644 --- a/docs/dev/debt/libSceUserService.md +++ b/docs/dev/debt/libSceUserService.md @@ -3,6 +3,7 @@ ### Unknown function info - [sceUserServiceGetNpAccountId](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the account id for a user with no linked PSN account is assumed to be 0 with success; libSceNpManager's `sceNpGetAccountIdA` answers the same question with `SCE_NP_ERROR_SIGNED_OUT` +- [sceUserServiceInitialize and sceUserServiceInitialize2](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - event delivery has no background service thread, so the confirmed priority and Initialize2 CPU affinity mask do not affect host scheduling. - [sceUserServiceGetUserColor](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the default profile colour is assumed to be blue (0) - [sceUserServiceGetUserName](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - `0x80960105`, which a PS5 (firmware 13.60) returns for a user id it does not know, has no known name; it is `USER_SERVICE_ERROR_UNKNOWN_USER` here. On the console a registered user who is not logged in still gets their name; here only the initial user exists - [sceUserServiceGetAgeLevel](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile diff --git a/docs/dev/debt/libSceVdecCore.md b/docs/dev/debt/libSceVdecCore.md new file mode 100644 index 0000000000..a6803142e3 --- /dev/null +++ b/docs/dev/debt/libSceVdecCore.md @@ -0,0 +1,5 @@ +# libSceVdecCore technical debt + +### Unknown function info + +- [sceVdecCoreAnalyzeStream, sceVdecCoreCreateDecoder, sceVdecCoreDeleteDecoder, sceVdecCoreFinalizeComputeResource, sceVdecCoreFlushDecodeOutput, sceVdecCoreGetDecodeOutput, sceVdecCoreInitializeComputeResource, sceVdecCoreMapMemoryBlock, sceVdecCoreQueryComputeResourceInfo, sceVdecCoreQueryFrameBufferInfo, sceVdecCoreQueryInstanceSize, sceVdecCoreResetDecoder, sceVdecCoreSetDecodeInput, sceVdecCoreSetDecodeOutputSw, sceVdecCoreSyncDecode, sceVdecCoreSyncDecodeOutputSw, sceVdecCoreSyncDecodeWptr, sceVdecCoreTrySyncDecode, sceVdecCoreTrySyncDecodeOutputSw and sceVdecCoreTrySyncDecodeWptr](../../../core/libs/prx/libSceVdecCore/Export.cpp) (libSceVdecCore) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVdecCore.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceVdecCore.native.md b/docs/dev/debt/libSceVdecCore.native.md new file mode 100644 index 0000000000..fd07ad5f05 --- /dev/null +++ b/docs/dev/debt/libSceVdecCore.native.md @@ -0,0 +1,8 @@ +# libSceVdecCore.native technical debt + +### Unknown function info + +- [sceVdecCoreCreateDecoderBid, sceVdecCoreCreateDecoderSvp, sceVdecCoreMapMemory and sceVdecCoreSetDecodeInputSvp](../../../core/libs/prx/libSceVdecCore.native/Export.cpp) (libSceVdecCore.native) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVdecCore.native.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified + +### Inventory exports declared elsewhere + The PS5 3.20 export table for this module also contains 20 names already declared under libSceVdecCore in this tree; the duplicate-export rule keeps each name in a single library, so they are omitted here. They resolve through the sibling declarations. diff --git a/docs/dev/debt/libSceVdecSavc.md b/docs/dev/debt/libSceVdecSavc.md new file mode 100644 index 0000000000..be07a4d7b7 --- /dev/null +++ b/docs/dev/debt/libSceVdecSavc.md @@ -0,0 +1,5 @@ +# libSceVdecSavc technical debt + +### Unknown function info + +- [sceSdecCreateSw, sceSdecDestroySw, sceSdecGetVersionSw and sceSdecQueryMemorySw](../../../core/libs/prx/libSceVdecSavc/Export.cpp) (libSceVdecSavc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVdecSavc.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceVdecSavc2.md b/docs/dev/debt/libSceVdecSavc2.md new file mode 100644 index 0000000000..d72e048aac --- /dev/null +++ b/docs/dev/debt/libSceVdecSavc2.md @@ -0,0 +1,5 @@ +# libSceVdecSavc2 technical debt + +### Unknown function info + +- [sceSdecCreateSw2, sceSdecDestroySw2, sceSdecGetVersionSw2 and sceSdecQueryMemorySw2](../../../core/libs/prx/libSceVdecSavc2/Export.cpp) (libSceVdecSavc2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVdecSavc2.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceVdecSavc2.native.md b/docs/dev/debt/libSceVdecSavc2.native.md new file mode 100644 index 0000000000..ac5be7701f --- /dev/null +++ b/docs/dev/debt/libSceVdecSavc2.native.md @@ -0,0 +1,5 @@ +# libSceVdecSavc2.native technical debt + +### Inventory exports declared elsewhere + +The complete PS5 3.20 export table of this module per the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVdecSavc2.native.c) (4 names) is already declared under the sibling libSceVdecSavc2 in this tree; the duplicate-export rule keeps each name in a single library, so this module keeps its `APS5_DUMMY_FUN` skeleton. Imports resolve through the sibling declarations. diff --git a/docs/dev/debt/libSceVdecShevc.md b/docs/dev/debt/libSceVdecShevc.md new file mode 100644 index 0000000000..0413883bb5 --- /dev/null +++ b/docs/dev/debt/libSceVdecShevc.md @@ -0,0 +1,5 @@ +# libSceVdecShevc technical debt + +### Unknown function info + +- [sceSdecCreateSwHevc, sceSdecDestroySwHevc, sceSdecExecuteSwHevc, sceSdecGetFrameSwHevc, sceSdecGetVersionSwHevc, sceSdecQueryMemorySwHevc and sceSdecResetSwHevc](../../../core/libs/prx/libSceVdecShevc/Export.cpp) (libSceVdecShevc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVdecShevc.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceVdecShevc.native.md b/docs/dev/debt/libSceVdecShevc.native.md new file mode 100644 index 0000000000..1187969c07 --- /dev/null +++ b/docs/dev/debt/libSceVdecShevc.native.md @@ -0,0 +1,5 @@ +# libSceVdecShevc.native technical debt + +### Inventory exports declared elsewhere + +The complete PS5 3.20 export table of this module per the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVdecShevc.native.c) (7 names) is already declared under the sibling libSceVdecShevc in this tree; the duplicate-export rule keeps each name in a single library, so this module keeps its `APS5_DUMMY_FUN` skeleton. Imports resolve through the sibling declarations. diff --git a/docs/dev/debt/libSceVdecsw.md b/docs/dev/debt/libSceVdecsw.md index 09e1b11228..4818e0bfea 100644 --- a/docs/dev/debt/libSceVdecsw.md +++ b/docs/dev/debt/libSceVdecsw.md @@ -2,4 +2,6 @@ ### 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. The 0xb8-byte HEVC picture info (shadPS4's `vdecsw.h`) carries only the timestamps, coded size, bit depth, IDR and IRAP flags, profile, level and conformance window (in chroma samples, repeated in the frame cropping fields, whose console unit is unknown) + +- [sceVdecswGetVp9PictureInfo](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - no console behaviour is recorded; the signature and 0x58-byte layout come from [SharpProspero's Vdecsw.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Vdecsw.cs#L892-L952), and shadPS4 has no VP9 getter. It copies the leading `thisSize - 8` bytes of the picture's info, as [shadPS4's `sceVdecswGetPictureInfo`](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/videodec/vdecsw.cpp#L303-L334) does for AVC and HEVC, from Videodec2's VP9 info (`level` is 0 there). An output with no picture returns 0 and writes nothing. Null pointers, `thisSize` below 8, two pictures and a frame buffer no VP9 decoder returned (an H.264 picture too) throw; console error codes are unknown. VP9 decoding goes through Videodec2 and rejects pictures deeper than 10 bits diff --git a/docs/dev/debt/libSceVdecwrap.md b/docs/dev/debt/libSceVdecwrap.md new file mode 100644 index 0000000000..d76e301e8e --- /dev/null +++ b/docs/dev/debt/libSceVdecwrap.md @@ -0,0 +1,5 @@ +# libSceVdecwrap technical debt + +### Unknown function info + +- [sceVdecwrapCreateDecoder, sceVdecwrapDeleteDecoder, sceVdecwrapFlushDecodeOutput, sceVdecwrapGetDecodeOutput, sceVdecwrapMapDirectMemory, sceVdecwrapMapMemory, sceVdecwrapQueryDecoderMemoryInfo, sceVdecwrapQueryFrameBufferInfo, sceVdecwrapResetDecoder, sceVdecwrapSetDecodeInput and sceVdecwrapSyncDecode](../../../core/libs/prx/libSceVdecwrap/Export.cpp) (libSceVdecwrap) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVdecwrap.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceVenc.md b/docs/dev/debt/libSceVenc.md new file mode 100644 index 0000000000..92c6460425 --- /dev/null +++ b/docs/dev/debt/libSceVenc.md @@ -0,0 +1,5 @@ +# libSceVenc technical debt + +### Unknown function info + +- [sceVencCreateEncoder, sceVencDeleteEncoder, sceVencGetConfig, sceVencMapMemory, sceVencQueryHeader, sceVencQueryMemorySize, sceVencQueryPreset, sceVencSetConfig, sceVencSetDecodedFrame, sceVencSetPictureConfig, sceVencSetRateControlConfig, sceVencSetReferenceFrameInvalidationConfig, sceVencStartEncode and sceVencStopEncode](../../../core/libs/prx/libSceVenc/Export.cpp) (libSceVenc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVenc.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceVideoArbitration.md b/docs/dev/debt/libSceVideoArbitration.md new file mode 100644 index 0000000000..d683418191 --- /dev/null +++ b/docs/dev/debt/libSceVideoArbitration.md @@ -0,0 +1,5 @@ +# libSceVideoArbitration technical debt + +### Unknown function info + +- [sceVideoArbitrationAcceptEvent, sceVideoArbitrationEnable and sceVideoArbitrationInitialize](../../../core/libs/prx/libSceVideoArbitration/Export.cpp) (libSceVideoArbitration) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVideoArbitration.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceVideoCoreInterface.md b/docs/dev/debt/libSceVideoCoreInterface.md new file mode 100644 index 0000000000..347236cfd8 --- /dev/null +++ b/docs/dev/debt/libSceVideoCoreInterface.md @@ -0,0 +1,6 @@ +# libSceVideoCoreInterface technical debt + +### Unknown function info + +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceVideoCoreIfInitializeInterface`](../../../core/libs/prx/libSceVideoCoreInterface/Export.cpp), [`sceVideoCoreIfMainLoop`](../../../core/libs/prx/libSceVideoCoreInterface/Export.cpp), [`sceVideoCoreIfSetFunctionTable`](../../../core/libs/prx/libSceVideoCoreInterface/Export.cpp). +- [sceVideoCoreIfSetFunctionTableUnlimited](../../../core/libs/prx/libSceVideoCoreInterface/Export.cpp) (libSceVideoCoreInterface) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVideoCoreInterface.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceVideoCoreServerInterface.md b/docs/dev/debt/libSceVideoCoreServerInterface.md new file mode 100644 index 0000000000..d5d88b3c3d --- /dev/null +++ b/docs/dev/debt/libSceVideoCoreServerInterface.md @@ -0,0 +1,5 @@ +# libSceVideoCoreServerInterface technical debt + +### Unknown function info + +- [sceVideoCoreAbortRequest, sceVideoCoreAcquireDecoderResource, sceVideoCoreAcquireDecoderResourceForBeWrapper, sceVideoCoreAddStream, sceVideoCoreAsyncAddStream, sceVideoCoreAsyncSendRequest, sceVideoCoreCancelThumbnail, sceVideoCoreChangeActiveStatus, sceVideoCoreChangeAudioStream, sceVideoCoreChangeDisplaySize, sceVideoCoreChangeHdcpSetting, sceVideoCoreChangeSubtitle, sceVideoCoreDeleteStream, sceVideoCoreGenerateThumbnail, sceVideoCoreGenerateThumbnail2, sceVideoCoreGetCanvasHandle, sceVideoCoreGetPlayStatus, sceVideoCoreGetPlayStatus2, sceVideoCoreGetStreamUid, sceVideoCoreGetThumbnail, sceVideoCoreGetThumbnail2, sceVideoCoreLaunchVideoCore, sceVideoCoreMediaSourceAbortSourceBuffer, sceVideoCoreMediaSourceAddSourceBuffer, sceVideoCoreMediaSourceAppendData, sceVideoCoreMediaSourceEndOfStream, sceVideoCoreMediaSourceGetBufferedTimeRanges, sceVideoCoreMediaSourceGetInputBufferEmptySize, sceVideoCoreMediaSourceGetPlaybackQuality, sceVideoCoreMediaSourceGetSourceBufferEvent, sceVideoCoreMediaSourceRemoveSourceBuffer, sceVideoCoreMediaSourceSetDuration, sceVideoCoreMediaSourceSetTimestampOffset, sceVideoCorePlayControl, sceVideoCoreRegistEventCallback, sceVideoCoreReleaseDecoderResource, sceVideoCoreReleaseDecoderResourceForBeWrapper, sceVideoCoreSetAudioMute, sceVideoCoreSetAudioVolume, sceVideoCoreSetFullScreen, sceVideoCoreSetInitialInfo, sceVideoCoreSetInitializeInfo, sceVideoCoreSetPlaybackRange, sceVideoCoreSetThumbnailInfo, sceVideoCoreSyncAddStream, sceVideoCoreSyncSendRequest and sceVideoCoreUnregistEventCallback](../../../core/libs/prx/libSceVideoCoreServerInterface/Export.cpp) (libSceVideoCoreServerInterface) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVideoCoreServerInterface.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceVideoDecoderArbitration.md b/docs/dev/debt/libSceVideoDecoderArbitration.md new file mode 100644 index 0000000000..219e005a17 --- /dev/null +++ b/docs/dev/debt/libSceVideoDecoderArbitration.md @@ -0,0 +1,5 @@ +# libSceVideoDecoderArbitration technical debt + +### Unknown function info + +- [sceVideoDecoderArbitrationAcceptEvent, sceVideoDecoderArbitrationEnable, sceVideoDecoderArbitrationEnableSuspendMode and sceVideoDecoderArbitrationInitialize](../../../core/libs/prx/libSceVideoDecoderArbitration/Export.cpp) (libSceVideoDecoderArbitration) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVideoDecoderArbitration.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceVideoNativeExtEssential.md b/docs/dev/debt/libSceVideoNativeExtEssential.md new file mode 100644 index 0000000000..f332e1ccbd --- /dev/null +++ b/docs/dev/debt/libSceVideoNativeExtEssential.md @@ -0,0 +1,5 @@ +# libSceVideoNativeExtEssential technical debt + +### Unknown function info + +- [sceVideoNativeExtEssential_CreateDecInstance, sceVideoNativeExtEssential_DestroyDecInstance, sceVideoNativeExtEssential_Finalize, sceVideoNativeExtEssential_Initialize and sceVideoNativeExtEssential_IsInit](../../../core/libs/prx/libSceVideoNativeExtEssential/Export.cpp) (libSceVideoNativeExtEssential) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVideoNativeExtEssential.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index ce195aa306..5fdf3bd52f 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -2,26 +2,38 @@ ### Silent stubs +- [sceVideoOutSetFlipRate](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) stores a rate of 0, 1 or 2 and returns `VIDEO_OUT_ERROR_INVALID_VALUE` for any other rate. PPSA13356 calls it with 60; whether the console accepts that value or returns this error is unverified - [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) validates the priority and CPU affinity that the open param requests for the VideoOut service thread but does not apply them: the port's present and vblank threads are host threads, and guest priorities and affinities do not reach host scheduling - [sceVideoOutAddVrrStatusFlagsPrivilege](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) returns 0 and does nothing: the host output has no VRR state to report. The name comes from an assert message in a title that requires `SCE_VIDEO_OUT_OK`; the NID hashes to it, the arguments are unknown (it is called without any) - [sceVideoOutVrrPegToFixedRate, sceVideoOutVrrUnpegFromFixedRate](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA05684 passes the handle to both, two further zero arguments to `sceVideoOutVrrPegToFixedRate`, and ignores the results. The meaning of those two arguments is unknown, so only zeros are accepted. VRR is not modelled and the output always runs at a fixed rate, so both calls only validate the handle and return 0 ### 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. +- [sceVideoOutCursorEnable, sceVideoOutCursorDisable, sceVideoOutCursorSetImageAddress, sceVideoOutCursorSetPosition](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NIDs `UPZWCH8qTM4`, `fEPEwROdCwM`, `u-9bhWQApq8`, `N17AK88NdD0`) - the names hash to the NIDs Prospero Win (PPSA99995) imports, and the parameter lists `(handle, index, address)`, `(handle, index)`, `(handle, index, address)` and `(handle, index, x, y)` follow its [declarations](https://github.com/mpereiraesaa/prospero-win/blob/644e32e400c555e340d507383398c5d21b39b0b0/wine/ps5/pw_vulkan_radv.c#L65-L68). The cursor image's format and size and the console's return values are not known; the functions report that they are not implemented. +- [sceVideoOutGetOutputStatusWithVrr](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - bytes 48-53 are written as zero, where a PS5 (firmware 13.40, measured 2026-10-19 inside a retail title) with a 4K TV that takes 120 Hz returned the 16-bit values 48, 60 and 60 (0x30, 0x3C, 0x3C); their meaning is not established, so the value at byte 52 never adjusts `refreshRate`. `(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 and the rest 0, a null `status` returns `0x80290002` and an unknown handle `0x8029000B`, all as that console answered. 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 -- [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 +- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - mode 1 is supported; mode 15 is supported when the current host display runs at 120 Hz or above. Configuring mode 15 requires attribute3 bit 0x40; this single-bit mask is inferred from the measured full value 0x80040, not isolated by a console measurement. Vblanks run at 119.88 Hz until mode 1 restores 59.94 Hz. Display modes are read through SDL; a move to a slower display falls back the configured output mode to mode 1, fires the output-mode event and continues at 59.94 Hz. The console's exact fallback behaviour when a 120 Hz display is lost has not been measured; this fallback avoids the abort in #7877 and matches the HDMI output mode renegotiation that a display change requires. Selecting another host display mode and VRR are not implemented. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-14, 16-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and two others 0x8029001E, the last a PS5 on 13.40 with a 120 Hz TV (measured 2026-10-19 inside a retail title), whose answers for every other mode match the ones here. 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) - `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; a PS5 (firmware 13.40, measured 2026-10-19 inside a retail title) reported its TV's 60.0f. The 48-byte layout, from the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), is confirmed there: full size and pane 3840 x 2160, `refreshRate` 3, `flags` 0 and the rest 0 on the title's handle, 0x8029000B for handles 0 to 8. `flags` is 0 here too; its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins +- [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. PPSA26344's only call at `0xCCF5EA` passes `(0xFF, 0, 0, nullptr)`. Whether a non-null param continues past offset 24 is also unknown +- [sceVideoOutGetBufferLabelAddress](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) exposes a port-owned contiguous array of 16 aligned 64-bit labels and returns its count. Astro Bot PPSA21564's bundled AGC driver passes a pointer output and addresses each label at `base + index * 8`. The count, zero initialization on registration, and release of the previous displayed buffer when its replacement completes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/videoout/video_out.cpp) and its driver; PS5 console equivalence has not been measured. Labels live until the port closes, and reopening creates a new array. Error precedence follows the reference for null output and the existing open-port policy for handles. Exact scanout release timing, repeated same-buffer flips and concurrent guest label stores remain unverified. EOP-flip registration and interrupt tokens remain unimplemented; exposing these labels does not implement that separate API. - [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 +- [sceVideoOutSubmitFlip](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut, libSceAgcDriver) - in the [AGC driver](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp), a CPU flip waits for the graphics-queue submissions ahead of it when the calling thread has submitted graphics-queue work itself: a title that submits a frame and flips it without waiting relies on that order, since emulated GPU work finishes later than the console's. A flip from a thread that has never submitted graphics-queue work is presented without waiting for queued submissions, unless a command-buffer or EOP flip ahead of it has not been reached. Such a thread can only know its frame is finished by waiting for it, as the PS5 RADV WSI's flip thread waits on the frame's fence. Whether the console ever orders a CPU flip behind the graphics queue is not measured; `APS5_CPU_FLIP_IN_ORDER=1` orders every CPU flip +- [sceVideoOutGetVblankStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the third 64-bit word of the PS5 status (`reserved` in [SceTypes.hpp](../../../core/libs/SceTypes.hpp)) is written 0. On a PS5 (firmware 13.40, measured 2026-10-19) a retail title's own call, caught inside it, got count 0xb16b, `processTime` 0x381a8790 (941 s), third word 0x1c485ffaadc, `processTimeCounter` 0x15dd63c364b, flags 0x04 and phase 0: the third word is a running counter, about 970 s at ~2 GHz, consistent with a TSC, not zero. Its clock and what it counts from are not established. Closes when: the third word's source is measured and written - [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 - [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - opening a bus that is already open returns `SCE_VIDEO_OUT_ERROR_RESOURCE_BUSY` (`0x80290009`), as [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/videoout/driver.cpp#L53-L60) PS4 driver does for its main port; the PS5 result is unverified. PPSA99764 opens the main bus twice and carries on, presenting through the first handle +- [sceVideoOutOpen, bus 0x20](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA05666 opens the PS VR2 output as `sceVideoOutOpen(255, 0x20, 0, NULL)` after `sceHmd2ReprojectionEnableVrMode(0x2000F)`, and its own log calls the bus `SCE_VIDEO_OUT_BUS_TYPE_HMD2_MAIN`. The console answers `0x80290001` from a process that is not a game, with the headset connected and worn or not, for users 255 and 0 and for a null or zeroed open parameter, so that is returned here before the other argument checks; what a game gets with a headset attached is not measured. On the handle the title then calls `sceVideoOutRegisterBuffers2` with set 0, two buffers, a 4000x2040 attribute (tiling 0, pitch 0, pixel format `0x8100000022000000`), category 16 and a null option, where each `VideoOutBuffers.data` points at a 16-byte `{pointer, 2}` whose pointer is the structure it passed to `sceHmd2ReprojectionEnableMirroring`, holding per-eye 2000x2040 texture descriptors rather than frame memory; then `sceVideoOutConfigureOutput` and `sceVideoOutIsOutputSupported` with mode `0x2000F`, and the GPU waits on buffer 0 of that handle. None of that is modelled: the HMD output needs a VR backend. With the error code as its handle the title's next call, `sceVideoOutRegisterBuffers2`, throws here for the unknown handle; the console's answer to it is not measured. The TV bus answers `0x8029001E` to the HMD modes `0x20001` and `0x2000C` to `0x20010` on the console, as here - [Overlay output bus](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - flips on a port opened with `SCE_VIDEO_OUT_BUS_TYPE_OVERLAY` complete and signal their events like main-bus flips, but their buffers are not shown: the overlay is not composited over the main output. Presenting them as whole frames made the window alternate between the two buses +- [sceVideoOutSubmitEopFlip](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) reserves a flip and returns an opaque DWORD token; presentation is driven by a matching graphics-queue `RELEASE_MEM` with `INT_SEL` 2 and that token in `INT_CTXID`. This five-argument ABI and packet use are inferred from PPSA21564's bundled AGC driver, whose original packet-emission routine is checked in an isolated host ABI probe. The console token encoding, output width beyond the observed DWORD, error policy and reservation capacity are unverified. Host tokens occupy `0x04000000..0x07ffffff`, never repeat within a process, and exhaustion throws. Closing cancels unsubmitted reservations; only live reservations match; consumed or cancelled context IDs execute as ordinary label writes without targeting a reopened port. Async-queue EOP flips throw. Buffer registration, flip modes, queue capacity and vsync presentation follow the existing host VideoOut model, not measured console behavior. - [Mouse capture and delta scaling](../../../core/libs/prx/libSceVideoOut/src/MouseInput.cpp) (libSceVideoOut) - the console's mouse reports relative motion only, so a host cursor can never coincide with the title's cursor; while a title has a mouse open and the window is focused the host cursor is hidden and captured (SDL relative mode, released on focus loss or while the pad's mouse-to-stick toggle is on). Motion deltas are scaled from the presented image's size in window units to the title's display resolution with sub-pixel carry; the console's deltas are counts, so a title that applies its own sensitivity moves its cursor that much faster than the host pointer would have. The scale takes SDL's relative motion in logical window units, the units of `SDL_GetWindowSize`; under Windows DPI scaling SDL may report it in physical pixels, which is unverified (no Windows run yet), and a drawable larger than the window does not change the scale. -- [Windows game controllers](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - SDL's DirectInput joystick backend is disabled at default hint priority before `SDL_InitSubSystem`, because its device enumeration blocks forever on some vendor HID devices (#1042). XInput and HIDAPI controllers are detected; controllers reachable only through DirectInput are not. Setting `SDL_DIRECTINPUT_ENABLED=1` restores them on hosts without such a device +- [HDR output](../../../core/libs/prx/libSceVideoOut/src/HostHdrProbe.cpp) (libSceVideoOut, libSceSystemService) - `sceVideoOutGetOutputStatus` reports dynamic range 2 and flag bit 0 (named HDR in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs)) only on Windows, while the window's Vulkan surface offers a 10-bit HDR10 swapchain with `VK_EXT_swapchain_colorspace` and `VK_EXT_hdr_metadata` and DXGI (`IDXGIOutput6::GetDesc1`) reports the window's monitor in `DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020`; `sceSystemServiceGetHdrToneMapLuminance` then reports the monitor's peak, full-frame peak and black level. A monitor that reports no usable peak gets a default of 1000 nits peak and full frame, 0 nits black. On Linux and other hosts the output stays SDR: a surface can offer HDR10 formats while the display is not in HDR (KDE Wayland with RADV lists them with the output's HDR disabled), and SDL2 and Vulkan expose no output HDR state; a compositor or output-state signal is needed first. The surface formats are read again when DXGI's monitor report changes and when the swapchain is recreated; a compositor that changes them without making the swapchain out of date is not seen until then. Every open port's output mode event fires when HDR output becomes available or unavailable and when its luminance changes; the console's events for an HDR change are unknown. Detection starts when VideoOut is first used: `sceSystemServiceGetHdrToneMapLuminance` called before that reports SDR, and no event fires for the availability found at start-up beyond the one every output-mode subscription gets when it is made. Console HDR settings (calibrated peak, the SDR-in-HDR conversion block) are not modelled +- [Windows game controllers](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - SDL's DirectInput joystick backend is disabled at default hint priority before `SDL_InitSubSystem`, because its device enumeration blocks forever on some vendor HID devices (#1042). XInput and HIDAPI controllers are detected; controllers reachable only through DirectInput are not. Setting `SDL_DIRECTINPUT_ENABLED=1` restores them on hosts without such a device. SDL's Windows.Gaming.Input backend stays on by default. Setting `APS5_NO_WGI` turns it off at default hint priority, for hosts where it faults at `Windows.Gaming.Input.dll+0x10a84` a few seconds into every run: seen on Windows 10 Pro 22H2 (19045) with the Nefarius and Oculus virtual gamepad emulation buses installed and no physical controller connected. With it off, devices and features reachable only through Windows.Gaming.Input are lost: racing wheels, arcade sticks, flight sticks, HID pads without an HIDAPI driver, and Xbox One and Series trigger rumble. + +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceDbgVideoOutAddOutputModeVr60Privilege`](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp). diff --git a/docs/dev/debt/libSceVideoRecordingP.md b/docs/dev/debt/libSceVideoRecordingP.md new file mode 100644 index 0000000000..0b82a50648 --- /dev/null +++ b/docs/dev/debt/libSceVideoRecordingP.md @@ -0,0 +1,19 @@ +# libSceVideoRecordingP technical debt + +### Unknown function info + +- [sceVideoRecordingGetInfo, sceVideoRecordingSetInfo](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) support native validation and errors before a recording context exists. Open and initialization remain protected unimplemented exports; live recording queries, control and volume changes are unavailable. + +- [_sceVideoRecordingQueryParam_nid_postfix](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp): the supplied module reads a 64-bit size and writes a 32-bit mode at offset 12 for sizes at least 16, plus a 32-bit option at offset 28 for sizes at least 64. Mode/option meanings remain unresolved; the protected implementation is unchanged. +- [sceVideoRecordingCloseByHandle, sceVideoRecordingGetInfoByHandle](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - native handles are context pointers. Recording state and per-handle queries remain unimplemented. +- [sceVideoRecordingCopyBGRAtoNV12](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - the destination, source, 32-bit width and height arguments are confirmed. Pixel conversion remains unimplemented. +- [sceVideoRecordingGetSupportInfo, sceVideoRecordingGetVideoOutMode](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - GetSupportInfo writes an 18-byte payload whose fields remain unresolved. GetVideoOutMode takes a four-byte output confirmed by the native recording assembly and Avcap2 IPC descriptor; the mode values remain unresolved. Both exports remain unimplemented. +- [sceVideoRecordingInit, sceVideoRecordingTerm](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) - parameterless initialization and termination stubs; unimplemented and throw `NotImplemented_nid_no_patch` +- [sceVideoRecordingInitialize](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp): the native initializer consumes no caller arguments; the former optional parameter is not read by the Avcap2 initializer or recording setup. Its unused parameter slot is retained because the protected body references it. +- [sceVideoRecordingOpen, sceVideoRecordingOpen2](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - the 32-byte and 40-byte parameter variants, four argument registers and 32-bit heap size are recovered. Mode, output mode, option and format semantics remain incomplete. QueryHddSize2 returns a signed 64-bit size or error; recording and sizing exports remain unimplemented. + +- [Handle and support queries](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) - native recording handles are context pointers, and GetSupportInfo writes 18 bytes. Support payload field semantics remain incomplete. RestModeProcessStart, RestModeProcessGetStatus and SendCommand consume no caller arguments in the supplied native module; their existing unused parameter slots remain because the protected bodies reference those parameter names. + +### Functional + +- [sceVideoRecordingGetStatus](../../../core/libs/prx/libSceVideoRecordingP/Export.cpp) (libSceVideoRecordingP) always returns 0, `None` ("The recorder is not open") in [SharpProspero's `VideoRecordingStatus`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/VideoRecording.cs#L18-L29), also listed in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/video_recording/video_recording.h#L32-L36). It is true here because `sceVideoRecordingOpen` throws, so no recorder is ever open. Unverified: whether the console returns 0 or an error before `sceVideoRecordingInit`/`sceVideoRecordingInitialize`, which throw, and when it reports `Running` (1) or `Paused` (2) diff --git a/docs/dev/debt/libSceVideoStreamingEngine.md b/docs/dev/debt/libSceVideoStreamingEngine.md new file mode 100644 index 0000000000..216d7f8beb --- /dev/null +++ b/docs/dev/debt/libSceVideoStreamingEngine.md @@ -0,0 +1,5 @@ +# libSceVideoStreamingEngine technical debt + +### Unknown function info + +- [sceVideoStreamingEngineDestroyMediaKeySystemAccess, sceVideoStreamingEngineMediaKeySessionGenerateRequest, sceVideoStreamingEngineMediaKeySessionGetExpiration, sceVideoStreamingEngineMediaKeySessionGetKeyStatuses, sceVideoStreamingEngineMediaKeySessionGetSessionId, sceVideoStreamingEngineMediaKeySessionLoad, sceVideoStreamingEngineMediaKeySessionSetEventHandler, sceVideoStreamingEngineMediaKeySystemAccessCreateMediaKeys, sceVideoStreamingEngineMediaKeySystemAccessDestroyMediaKeys, sceVideoStreamingEngineMediaKeySystemAccessGetKeySystem, sceVideoStreamingEngineMediaKeySystemAccessSetCdmStorageConfig and sceVideoStreamingEngineMediaKeysCreateSession](../../../core/libs/prx/libSceVideoStreamingEngine/Export.cpp) (libSceVideoStreamingEngine) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVideoStreamingEngine.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceVideoStreamingEngine_sys.md b/docs/dev/debt/libSceVideoStreamingEngine_sys.md new file mode 100644 index 0000000000..d3211d8a90 --- /dev/null +++ b/docs/dev/debt/libSceVideoStreamingEngine_sys.md @@ -0,0 +1,5 @@ +# libSceVideoStreamingEngine_sys technical debt + +### Inventory exports declared elsewhere + +The complete PS5 3.20 export table of this module per the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVideoStreamingEngine_sys.c) (12 names) is already declared under the sibling libSceVideoStreamingEngine in this tree; the duplicate-export rule keeps each name in a single library, so this module keeps its `APS5_DUMMY_FUN` skeleton. Imports resolve through the sibling declarations. diff --git a/docs/dev/debt/libSceVideodec.md b/docs/dev/debt/libSceVideodec.md new file mode 100644 index 0000000000..8abac004cf --- /dev/null +++ b/docs/dev/debt/libSceVideodec.md @@ -0,0 +1,15 @@ +# libSceVideodec technical debt + +### Unknown function info + +- [sceVideodecCreateDecoder, sceVideodecDecode, sceVideodecDeleteDecoder, sceVideodecFlush, sceVideodecQueryResourceInfo and sceVideodecReset](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the structure layouts and the error codes follow [shadPS4's PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/videodec/videodec.h) and its [implementation](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/videodec/videodec.cpp), not measured on a PS5: the console's error for a rejected argument and its own version of the structures are unknown. The decoder runs through libSceVideodec2, which decodes H.264 with FFmpeg, and the picture is copied into the caller's frame buffer with the pitch aligned to 64 and the coded height, as the reference writes it + +### Functional + +- [sceVideodecCreateDecoder](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the codec type is not checked and the decoder always decodes H.264, as the PS4 library does; the console's codec ids are not documented. A configuration without a maximum frame size is refused with the configuration error, and the resource information the caller passes is not used + +- [sceVideodecQueryResourceInfo](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the sizes it reports are libSceVideodec2's, not the console's: the CPU memory, CPU-GPU memory and frame buffer sizes and the frame buffer alignment that `sceVideodec2QueryDecoderMemoryInfo` gives for the configuration mapped as `sceVideodecCreateDecoder` maps it. The GPU memory size that query also gives has no field in this structure and is dropped. The memory pointers and `thisSize` are left as the caller passed them, where [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c2814041e546bf75c12756dbec9da911c721ce1c/src/core/libraries/videodec/videodec.cpp#L137-L187) clears the pointers; what the console writes there is not known. A larger structure is accepted, as by the module's other exports, where shadPS4 requires the exact size, and a configuration without a maximum frame size is refused with the configuration error, where shadPS4 assumes 1920x1080 or 3840x2160 + +- [sceVideodecDecode and sceVideodecFlush](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the picture information's video usability information (the tick rate, the sample aspect ratio and the colour description) stays zero, as the decoder does not report it, and the crop offsets are the decoded picture's crop halved, the unit the H.264 offsets use. The frame buffer holds the coded picture the decoder reports, so the rows the crop covers are in it and the caller crops them + +- [sceVideodecMapMemory](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - its name is listed in the PS5 3.20 export table and cross-checked against PS5 13.60. Its signature and behaviour are unverified; this no-parameter declaration only resolves the import and throws through NotImplemented_nid_no_patch. diff --git a/docs/dev/debt/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md index 76298fe8ae..74a8f81c0f 100644 --- a/docs/dev/debt/libSceVideodec2.md +++ b/docs/dev/debt/libSceVideodec2.md @@ -2,8 +2,18 @@ ### Unknown function info +- [sceVideodec2InitLibrary](../../../core/libs/prx/libSceVideodec2/Unimplemented.cpp) (libSceVideodec2) throws: the FFmpeg decoders this module creates need no library-level initialisation, and neither SharpProspero nor EVO-PLAYER-PS5 declares this call, so its arguments and result are unknown. A title imports it from this module - [sceVideodec2GetAvcPictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - runs the `sceVideodec2GetPictureInfo` body (KytyPS5 binds both NIDs to one function; SharpProspero gives both the same `outputInfo, first, second` signature); the second (bottom field) picture info is never written, since only progressive H.264 output is modelled. prosper calls the AVC layout unestablished (no title observed calling it) +- [sceVideodec2TermLibrary and twelve other exports](../../../core/libs/prx/libSceVideodec2/Unimplemented.cpp) (libSceVideodec2) - report that they are not implemented; what they do on the console is not known. The others are `QueryLibraryInfo`, `QueryDecoderInfo`, `MapMemory`, `MapDirectMemory`, `PresetDecoderConfigInfo` (plain, `Avc`, `Hevc`, `Vp9`), `CreateDecoderBid`, `CreateHevcDecoder`, `QueryHevcDecoderMemoryInfo` and `GetHevcPictureInfo`. They are exported so that a title which imports them with `sceVideodec2InitLibrary` and calls none of them directly loads. Only `sceVideodec2MapDirectMemory` has a known parameter list, `(decoder, memory)`, from [EVO-PLAYER-PS5's `sce_videodec2.h`](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783/projects/evoplayer/media/include/sce/sce_videodec2.h#L173); the others take none here + ### Functional -- [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` and `sceVideodec2GetVp9PictureInfo` are not exported +- [AVC parameter-set units](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - an H.264 access unit with no slice NAL (types 1 to 5), such as the sequence and picture parameter sets Twispero (PPSA99474) submits on their own before a stream's first slices, is kept and put in front of the next unit that has a slice, and `sceVideodec2Decode` returns 0 with no picture for it, since libavcodec refuses a packet without a slice. What the console returns for such a unit is not known. HEVC units of parameter sets alone are still passed to libavcodec as they are + +- [sceVideodec2CreateDecoder](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is NV12 for 8-bit pictures. A 10-bit picture is written as 16-bit little-endian words with the sample in the low 10 bits, Y rows then interleaved UV rows at `framePitchInBytes` times the picture height, with `framePitch` in samples and `framePitchInBytes` in bytes (twice the width, aligned to 256), and `QueryDecoderMemoryInfo` sizes a frame of an HEVC or VP9 profile 2 configuration with two bytes per sample. Evidence is from [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/tree/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783), which decodes and presents 1080p HEVC Main10 on hardware through this path (`docs/evo-pro/on-demand-decoder-handoff.md`): it takes a picture as 10-bit when `pitch_bytes >= pitch * 2`, puts UV at `pitch_bytes` times the height and samples with a low-bit factor (`projects/evoplayer/media/src/evo_vdec_native.c`, `ro_harvest`), and its hardware logs give 6128 KB and 24496 KB frame buffers for 1920x1088 and 3840x2176 Main10 decoders after rounding to 16 KiB. The 1080p size is what this formula gives; at 3840x2176 it gives 16 KiB less, so the console's size there has another term that is not known. Unverified: `frameFormat` (still 0; no consumer found reads it), VP9 profile 2 output (no hardware evidence), and the layout of 12-bit pictures and of decoded formats other than yuv420p10le, which throw. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC pictures (`SceVideodec2HevcPictureInfo` is not filled), and `sceVideodec2GetHevcPictureInfo` reports that it is not implemented + +- [sceVideodec2GetVp9PictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) uses the 88-byte layout and two-pointer signature from [SharpProspero Videodec2.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs). Frame metadata follows the [VP9 specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.7-20170222-draft.pdf), sections 5.26 and 6.2: profile, key/intra-only flags, bit depth, color space/range, coded/render dimensions and superframe count are retained per emitted buffer. Level is 0 because the elementary stream does not signal a level; the console value is unverified. Null pointers, undersized structures and querying another codec throw; console error codes and support for other structure sizes are unknown. Pictures deeper than 10 bits are rejected. Superframes are submitted as individual coded frames to keep each output associated with its input metadata. `sceVideodec2GetPictureInfo` writes this layout for a VP9 picture, as shadPS4's `videodec2.cpp` writes the decoder's own layout; Mortal Shell 2 (PPSA34008) calls it with `thisSize` 88. +- [sceVideodec2Decode after sceVideodec2Flush](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) without `sceVideodec2Reset` restarts FFmpeg's decoder (`avcodec_flush_buffers`) and decodes the unit, where it threw before. TEKKEN DARK RESURRECTION (PPSA31371), which runs in Sony's PSP emulator, decodes after a flush without a reset (#7118), so the console accepts the call. Pictures the flush decoded but has not returned yet are returned before the new ones. A flush with no picture pending (each decoded unit has returned its picture) leaves the decoder and its reference pictures as they are, so a stream that continues without a new IDR picture keeps decoding: Daxter (PPSA09695), also in the PSP emulator, flushes after every access unit of its H.264 movies, and restarting the decoder there lost every P picture until the next keyframe. What the console returns for pending pictures, and for a stream that continues across a flush that had pictures pending, 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 +- [sceVideodec2Decode of an empty access unit](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) returns `0x811D010E` for a null `auData` and `0x811D010D` for an `auSize` of 0, with no picture, where it threw before. The values are `ACCESS_UNIT_POINTER` and `ACCESS_UNIT_SIZE` in the error table of [shadPS4's videodec_error.h](https://github.com/shadps4-emu/shadPS4/blob/f3f0c14c1350a768558dfd1fd058d259ccecaca2/src/core/libraries/videodec/videodec_error.h), whose libSceVdecsw codes this repository already uses; they are not measured on a PS5, and neither is whether the console leaves the decoder's state alone on that call, as this does. Media Player Classic PS5 (PPSA99015) submits empty units: its own converter reads a 4-byte length prefix of `00 00 01 xx` as a start code and drops the rest as a non-base HEVC layer. It goes on after the error. diff --git a/docs/dev/debt/libSceVisionManager.md b/docs/dev/debt/libSceVisionManager.md new file mode 100644 index 0000000000..6e8c187895 --- /dev/null +++ b/docs/dev/debt/libSceVisionManager.md @@ -0,0 +1,5 @@ +# libSceVisionManager technical debt + +### Unknown function info + +- [sceVisionManagerApplyCalibrateHmdResult, sceVisionManagerCancelRequest, sceVisionManagerClose, sceVisionManagerFinalize, sceVisionManagerGenerateRegisterUserData, sceVisionManagerGenerateRegisterUserDataFinalize, sceVisionManagerGenerateRegisterUserDataInitialize, sceVisionManagerGetCalibrateTrackingLedInfo, sceVisionManagerGetCalibrateTrackingLedNecessity, sceVisionManagerGetHitCountOfRegisterUserData, sceVisionManagerGetLibraryVersion, sceVisionManagerGetNumOfRegisterUserDataSlot, sceVisionManagerGetRegisterUserDataSize, sceVisionManagerGetRegisterUserDataVersion, sceVisionManagerGetResultOfCalibrateHmd, sceVisionManagerGetResultOfCalibrateStereoCamera, sceVisionManagerGetResultOfCalibrateTrackingLed, sceVisionManagerGetResultOfFindFace, sceVisionManagerGetResultOfFindPad, sceVisionManagerGetResultOfFindPadCorrespondRegion, sceVisionManagerGetResultOfFindUser, sceVisionManagerGetResultOfFindUserInRegion, sceVisionManagerGetResultOfFindUserWithoutIdentify, sceVisionManagerGetResultOfRegisterUserInRegion, sceVisionManagerGetStoreCameraImgYPtr, sceVisionManagerGetWorkingMemorySize, sceVisionManagerInitialize, sceVisionManagerLoadSettings, sceVisionManagerOpen, sceVisionManagerRegistryClear, sceVisionManagerRequestCalibrateHmd, sceVisionManagerRequestCalibrateStereoCamera, sceVisionManagerRequestCalibrateTrackingLed, sceVisionManagerRequestClose, sceVisionManagerRequestFindFace, sceVisionManagerRequestFindPad, sceVisionManagerRequestFindPadCorrespondRegion, sceVisionManagerRequestFindUser, sceVisionManagerRequestFindUserInRegion, sceVisionManagerRequestFindUserWithoutIdentify, sceVisionManagerRequestHeadDetection, sceVisionManagerRequestInitializeRegisterUserData, sceVisionManagerRequestOpen, sceVisionManagerRequestPadTracking, sceVisionManagerRequestRecogUtility, sceVisionManagerRequestRegisterUserInRegion, sceVisionManagerRequestStoreCameraImgY, sceVisionManagerSetCallbackForUpdateCalibrateHmdResult, sceVisionManagerSetCallbackForUpdateCalibrateStereoCameraResult, sceVisionManagerSetCallbackForUpdateCalibrateTrackingLedResult, sceVisionManagerSetCallbackForUpdateCameraFrame, sceVisionManagerSetCallbackForUpdateCtrlCore, sceVisionManagerSetCallbackForUpdateRecog, sceVisionManagerSetCallbackForUpdateRunningStatus, sceVisionManagerSetCallbackForUpdateStoreFrame and sceVisionManagerWaitResponse](../../../core/libs/prx/libSceVisionManager/Export.cpp) (libSceVisionManager) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVisionManager.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md new file mode 100644 index 0000000000..3dcdeae2a9 --- /dev/null +++ b/docs/dev/debt/libSceVoice.md @@ -0,0 +1,23 @@ +# libSceVoice technical debt + +### Functional + +- [Voice resource and port state](../../../core/libs/prx/libSceVoice/Export.cpp): resource information is eight bytes containing four 16-bit limits selected by initialization mode. Reset, pause and resume use confirmed initialization, range and missing-port errors; repeated pause/resume succeed. GetPortInfo preserves unused output fields and reports native frame sizes, but still reports a constant ready state and does not reproduce stopped-state or snapshot-slot errors. The host has no buffered voice processing or device reset backend, so reset only covers empty software ports and pause flags currently affect no audio. Initialization validates native modes and version 100, but still lacks event callback delivery and backend creation. Port creation enforces recovered format and resource limits, but does not allocate device or codec backends. +- [sceVoiceEnableChat and sceVoiceDisableChat](../../../core/libs/prx/libSceVoice/Export.cpp): zero-argument signatures and the not-initialized error are confirmed. Initialized calls throw because the required audio chat backend is not implemented; they no longer report success for a local flag change. + +### Unknown function info + +- [sceVoiceInit, sceVoiceStart, sceVoiceCreatePort, sceVoiceGetPortInfo](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - a second `sceVoiceInit` returns 0x804E0802, and Start, CreatePort and GetPortInfo return 0x804E0801 before checking any argument while the library is not initialized, as [prosper](https://github.com/mattias800/prosper/blob/ed42e499888d5a86792c3e1b2df3aebd8ca1a681/prosper/src/hle/audio/hle_audio.cpp#L4697-L4713) reads them from the shipped module. The other port and lifecycle calls still throw when the library is not initialized, since prosper gives no module address for them. Whether `sceVoiceInit` checks a null param before its state is unknown, so a null param still throws first, and that `sceVoiceEnd` returns the library to the not-initialized state is assumed. Closes when: the other entry points are read from the module or measured on a PS5 +- [sceVoiceInitHQ, sceVoiceVADSetVersion](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5: the signatures and rules follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/voice/voice.cpp). `sceVoiceInitHQ` runs `sceVoiceInit`, as shadPS4's two share one path. `sceVoiceVADSetVersion` accepts versions 0 and 1 and returns `0x804E0805` for any other; nothing uses the version, as no voice activity detection runs +- [sceVoicePausePortAll, sceVoiceResumePortAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5: both return 0x804E0801 while the library is not initialized and 0 otherwise, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c1b26558352b863f80142256e4df38585aa7d727/src/core/libraries/voice/voice.cpp#L356-L437); prosper reads the same code for `sceVoiceStart`, and the PS3 calls in [RPCS3](https://github.com/RPCS3/rpcs3/blob/be69c054eeac57e2e671697a463473d37fe5a7b2/rpcs3/Emu/Cell/Modules/cellVoice.cpp#L462-L543) make the same check. Like `sceVoicePausePort` they change nothing that is reported: [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L540-L557) documents that pausing every port keeps the buffered data and the reported state, and nothing here processes port data +- [sceVoiceGetMemorySize](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceDeviceInCustomerData](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceQAEnableRecording](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceVADAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceVADPVTAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceSetMuteFlagAll, sceVoiceSetBitRate, sceVoiceUpdatePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5; signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), behaviour the Vita's [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L250-L557). They return the not-initialized, port-range and missing-port codes the other port calls return. The mute-all flag makes `sceVoiceGetMuteFlag` report every port muted without changing port flags. A non-voice port, a bitrate `sceVoiceCreatePort` rejects, a null param or a different port type throw, since the console's codes for them are unknown +- [sceVoiceGetPortAttr](../../../core/libs/prx/libSceVoice/Export.cpp): native initialization, port-range, pointer, attribute-size and port-type validation is implemented. Supported device attributes throw because the host has no device state or voice-activity processing. The former constant-zero voice-activity fallback is removed; failed calls leave output untouched. + +- [sceVoiceSetThreadsParams](../../../core/libs/prx/libSceVoice/Export.cpp): the priorities and affinity masks that mode 1 sets reach no host scheduling, because no voice worker threads run (see sceVoiceStart below). Mode 1 returns 0 for priorities 256-767 with non-zero masks within 0x1fff, the values a PS5 thread takes; the native result for other values is unverified, so they throw. The 32-byte event callback union is recovered, but callback delivery and device-state byte semantics remain unimplemented. + +- [sceVoiceStart, sceVoiceStop and sceVoiceEnd](../../../core/libs/prx/libSceVoice/Export.cpp) validate initialization, container presence and native mode-dependent memory sizes, but do not partition guest working memory or run the audio workers. Start/stop currently change software state only; backend failures and active-stream shutdown are not reproduced. diff --git a/docs/dev/debt/libSceVoiceChat.md b/docs/dev/debt/libSceVoiceChat.md index 17e7eb1875..60ad4dc3bb 100644 --- a/docs/dev/debt/libSceVoiceChat.md +++ b/docs/dev/debt/libSceVoiceChat.md @@ -1,5 +1,10 @@ # libSceVoiceChat technical debt +### Silent stubs + +- [sceVoiceChatAbortRequest, sceVoiceChatRegisterChannelEventHandler, sceVoiceChatRegisterDaemonEventHandler, sceVoiceChatRegisterMemberEventHandler, sceVoiceChatRegisterPartyUserStateChangedHandler, sceVoiceChatRegisterVoiceEventHandler, sceVoiceChatStartAccessibilityEvent, sceVoiceChatStartChannelEvent, sceVoiceChatStartDaemonEvent, sceVoiceChatStartMemberEvent, sceVoiceChatStartRangedMemberEvent, sceVoiceChatStartVoiceEvent, sceVoiceChatStopAccessibilityEvent, sceVoiceChatStopChannelEvent, sceVoiceChatStopDaemonEvent, sceVoiceChatStopMemberEvent, sceVoiceChatStopRangedMemberEvent, sceVoiceChatStopVoiceEvent, sceVoiceChatUnregisterAccessibilityEventHandler, sceVoiceChatUnregisterChannelEventHandler, sceVoiceChatUnregisterDaemonEventHandler, sceVoiceChatUnregisterMemberEventHandler, sceVoiceChatUnregisterMicEventHandler, sceVoiceChatUnregisterPartyUserStateChangedHandler, sceVoiceChatUnregisterRangedMemberEventHandler, sceVoiceChatUnregisterVoiceEventHandler](../../../core/libs/prx/libSceVoiceChat/Export.cpp) (libSceVoiceChat) - offline handler registration and event controls return 0 without connecting to a voice chat daemon, because the host environment does not run a PSN voice chat service; titles that register event listeners or start/stop audio capture events proceed safely without throwing + ### Unknown function info +- [sceVoiceChatAbortRequest, sceVoiceChatGetActiveChannel, sceVoiceChatGetChannelMemberActiveChannelMatchingState, sceVoiceChatGetChannelMemberVoiceConnectionState, sceVoiceChatGetChatChannelMemberInfoList, sceVoiceChatGetChatGroupMemberInfoList, sceVoiceChatGetMicState, sceVoiceChatGetUserVoiceVolumeGain, sceVoiceChatRegisterChannelEventHandler, sceVoiceChatRegisterDaemonEventHandler, sceVoiceChatRegisterMemberEventHandler, sceVoiceChatRegisterPartyUserStateChangedHandler, sceVoiceChatRegisterVoiceEventHandler, sceVoiceChatRequest0, sceVoiceChatRequest1, sceVoiceChatRequestAcsTts, sceVoiceChatRequestAction, sceVoiceChatRequestSetMemberVolume, sceVoiceChatRequestUpdateGameSessionVoiceChatChannelName, sceVoiceChatRequestUpdatePlayerSessionVoiceChatChannelName, sceVoiceChatSetUserVoiceVolumeGain, sceVoiceChatStartAccessibilityEvent, sceVoiceChatStartChannelEvent, sceVoiceChatStartDaemonEvent, sceVoiceChatStartMemberEvent, sceVoiceChatStartRangedMemberEvent, sceVoiceChatStartVoiceEvent, sceVoiceChatStopAccessibilityEvent, sceVoiceChatStopChannelEvent, sceVoiceChatStopDaemonEvent, sceVoiceChatStopMemberEvent, sceVoiceChatStopRangedMemberEvent, sceVoiceChatStopVoiceEvent, sceVoiceChatUnregisterAccessibilityEventHandler, sceVoiceChatUnregisterChannelEventHandler, sceVoiceChatUnregisterDaemonEventHandler, sceVoiceChatUnregisterMemberEventHandler, sceVoiceChatUnregisterMicEventHandler, sceVoiceChatUnregisterPartyUserStateChangedHandler, sceVoiceChatUnregisterRangedMemberEventHandler, sceVoiceChatUnregisterVoiceEventHandler](../../../core/libs/prx/libSceVoiceChat/Export.cpp) (libSceVoiceChat) - parameter layouts are inferred; offline, handlers and event controls return 0, query calls, volume gain controls, and requests return `SIGNED_OUT` (0x80550006) without modifying guest buffers - [sceVoiceChatRequestCreateVoiceChatGroup, sceVoiceChatRequestJoinVoiceChatGroup, sceVoiceChatRequestLeaveVoiceChatGroup, sceVoiceChatRequestDeleteVoiceChatGroup](../../../core/libs/prx/libSceVoiceChat/Export.cpp) (libSceVoiceChat) - assumed to take a request id and two pointers like the other `sceVoiceChatRequest*` calls; offline, every argument is ignored and `SIGNED_OUT` is returned, as for those diff --git a/docs/dev/debt/libSceVoiceQoS.md b/docs/dev/debt/libSceVoiceQoS.md index 7b3937b6a9..9b322456c0 100644 --- a/docs/dev/debt/libSceVoiceQoS.md +++ b/docs/dev/debt/libSceVoiceQoS.md @@ -1,10 +1,17 @@ # libSceVoiceQoS technical debt +### Silent stubs + +- [sceVoiceQoSReadPacket](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - network voice packet streaming is not implemented: it returns 0 and writes size 0 (indicating no packet is available). Aliens: Fireteam Elite (PPSA03393) calls it on its startup path (`0x76D14C`-`0x76D16E`) and stops with `not implemented` if it throws +- [sceVoiceQoSWritePacket](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - network voice packet streaming is not implemented: it returns 0 leaving `*size` untouched (treating all input bytes as accepted). Aliens: Fireteam Elite (PPSA03393) sets `*size` to the packet size before `WritePacket` (`0x76CEB9`-`0x76CEC4`), compares `*size` against that input size upon return (`0x76CF07`-`0x76CF10`) to verify all bytes were accepted, and stops with `not implemented` if it throws + ### Unknown function info -- [sceVoiceQoSSetConnectionAttribute](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`) -- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. The app type and the block are not used, as no endpoint is implemented +- [sceVoiceQoSSetMode and sceVoiceQoSTerminate](../../../core/libs/prx/libSceVoiceQoS/Export.cpp): these exports are absent from the supplied module. Their argument declarations still require another firmware version or callers. ### 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 +- [libSceVoiceQoS](../../../core/libs/prx/libSceVoiceQoS/Export.cpp): initialization arguments, initialization errors, endpoint and connection argument lists, and the no-argument End path are confirmed by the supplied module. The host still tracks endpoints without initializing, starting or stopping libSceVoice or creating its ports. Device-dependent port creation, deletion and connection errors remain unimplemented; connecting an uncreated port throws. End clears host state but does not execute the confirmed voice stop/end sequence or propagate its result. +- [sceVoiceQoSCreateLocalEndpoint, sceVoiceQoSCreateRemoteEndpoint and sceVoiceQoSConnect](../../../core/libs/prx/libSceVoiceQoS/Export.cpp): local ID zero and its single-slot limit, remote/connection slot allocation from zero, seven/fifteen slot capacities, exhaustion outputs, and reuse of released slots are recovered. Remote endpoint deletion clears the first matching connection. Shared encoder ownership, codec parameters and packet accounting are still missing. +- [sceVoiceQoSSetLocalEndpointAttribute and sceVoiceQoSSetRemoteEndpointAttribute](../../../core/libs/prx/libSceVoiceQoS/Export.cpp): local selectors 0/2 accept four-byte values and 1/3 one-byte values; remote selector 7 accepts one byte. Unsupported selectors and sizes return the confirmed invalid-argument error. Valid requests throw until voice-port volume/mute forwarding is implemented; they no longer silently store arbitrary bytes. +- [sceVoiceQoSGetLocalEndpoint and sceVoiceQoSGetRemoteEndpoint](../../../core/libs/prx/libSceVoiceQoS/Export.cpp): all in-range connection slots are queryable, including unused slots returning endpoint ID -1. Disconnecting an unused in-range slot succeeds. These state rules are confirmed; the packet, status and attribute getter bodies containing `NotImplemented_nid_no_patch` remain unchanged. diff --git a/docs/dev/debt/libSceVorbisDec.md b/docs/dev/debt/libSceVorbisDec.md new file mode 100644 index 0000000000..45ad9e2d0e --- /dev/null +++ b/docs/dev/debt/libSceVorbisDec.md @@ -0,0 +1,14 @@ +# libSceVorbisDec technical debt + +### Unknown function info + +- [sceVorbisDecOpenCallbacks, sceVorbisDecRead and the other libvorbisfile exports](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - the 31 exports named after libvorbisfile functions are taken to be the libvorbisfile API with a `sceVorbisDec` prefix (`Read` is `ov_read`, `CrossLap` is `ov_crosslap`, `HalfrateP` is `ov_halfrate_p`). `ReadFilter`, `ReadFloat` and `Halfrate` exist in libvorbis (`ov_read_filter` since 1.2.2, with the same `vorbisfile.h` signatures through 1.3.7 apart from `const`) but not in Tremor, so times are `double` and `ov_read` takes `bigendianp`, `word` and `sgned`. Guest `long` arguments and results are 64-bit. They run the bundled libvorbis 1.3.7 and libogg 1.3.6; the console's libvorbis version, any Sony changes to its behaviour or error codes, and the signatures themselves are unverified on a PS5. +- [sceVorbisDecGetDecodePriority](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database +- [sceVorbisDecGetLastDecodeError](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database +- [sceVorbisDecSetDecodePriority](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - declared without parameters, signature unknown; name from the NID database + +### Functional + +- [libSceVorbisDec](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - the guest `OggVorbis_File` is never written: the decoder state lives on the host, keyed by the struct's address, so fields a title reads directly keep what it stored, and a struct never opened, cleared or failed to open behaves as libvorbisfile's zeroed struct. `sceVorbisDecInfo` returns a copy of each link's `vorbis_info` with 64-bit `long` fields, refreshed after every call on the struct and valid until `sceVorbisDecClear`. Reopening a struct that is still open frees its old state without calling its close callback; libvorbisfile leaks it. +- [sceVorbisDecClear and sceVorbisDecCrossLap](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - a null `vf` throws, as libvorbisfile dereferences it, except that `sceVorbisDecClear` and `sceVorbisDecCrossLap` with the same pointer twice return 0, and `sceVorbisDecCrossLap` with a null second pointer after a first that is not open returns `OV_EINVAL`, as libvorbisfile does. `sceVorbisDecClear` frees the state before it calls the close callback, as `ov_clear` does, so when that callback throws the struct is already cleared, where libvorbisfile leaves it freed but not zeroed. +- [sceVorbisDecOpenCallbacks and sceVorbisDecBitrate](../../../core/libs/prx/libSceVorbisDec/Export.cpp) (libSceVorbisDec) - on Windows the host `long` is 32-bit: a tell result outside it, or an initial byte count outside it with a non-null `initial`, throws; a sample rate of 2^31 Hz or more is rejected as a bad header; and `sceVorbisDecBitrate` rounds halves up instead of to even. diff --git a/docs/dev/debt/libSceVrServiceDialog.md b/docs/dev/debt/libSceVrServiceDialog.md new file mode 100644 index 0000000000..fe48267534 --- /dev/null +++ b/docs/dev/debt/libSceVrServiceDialog.md @@ -0,0 +1,5 @@ +# libSceVrServiceDialog technical debt + +### Silent stubs + +- [libSceVrServiceDialog](../../../core/libs/prx/libSceVrServiceDialog/Export.cpp) - the VR service dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no headset is supported (see libSceHmd2). The signatures are from [Orbital's type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); the result layout beyond the leading `int32_t` result code is not known, so nothing else is written. diff --git a/docs/dev/debt/libSceVrSetupDialog.md b/docs/dev/debt/libSceVrSetupDialog.md index 34bc4cd532..6890166ca4 100644 --- a/docs/dev/debt/libSceVrSetupDialog.md +++ b/docs/dev/debt/libSceVrSetupDialog.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceVrSetupDialog](../../../core/libs/prx/libSceVrSetupDialog/Export.cpp) - the VR setup dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no headset is supported (see libSceHmd2). The signatures are assumed from the other common dialogs; PPSA12544 imports it +- [libSceVrSetupDialog](../../../core/libs/prx/libSceVrSetupDialog/Export.cpp) - the VR setup dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no headset is supported (see libSceHmd2). Initialization does not check the native process eligibility record, and the 46-byte setup payload remains opaque. Open validates the recovered 56-byte parameter header. GetResult writes one 32-bit result after initialization, including when returning the not-finished error; its error codes and validation order are recovered. diff --git a/docs/dev/debt/libSceVrTracker.md b/docs/dev/debt/libSceVrTracker.md new file mode 100644 index 0000000000..138b88a370 --- /dev/null +++ b/docs/dev/debt/libSceVrTracker.md @@ -0,0 +1,9 @@ +# libSceVrTracker technical debt + +### Unknown function info + +- [VrTracker parameters](../../../core/libs/prx/libSceVrTracker/Export.cpp): argument slots are recovered. CPU processing and GPU waiting consume 32-byte records, result queries consume 56-byte records, and time queries write 64 bits. Initialization accepts 128-byte and 144-byte versions; the 144-byte layout includes three address/size/alignment records, but configuration selector semantics and the older field layout remain unresolved. GPU submission accepts 552-byte and 648-byte versions; submission and result records still require field recovery; result storage has 1520-byte and 3640-byte variants. Result-query records have a signed handle, 64-bit time, 32-bit selectors and an extended-result flag; selector meanings and the four-byte value copied unchanged to result offset 448 remain unresolved. CPU mode/argument semantics are unresolved. Protected bodies remain unchanged. +- [sceVrTrackerCpuPopMarker, sceVrTrackerCpuPushMarker, sceVrTrackerGetLiveCaptureId, sceVrTrackerGpuWaitAndCpuProcess, sceVrTrackerResetAll, sceVrTrackerSaveInternalBuffers and sceVrTrackerSetDeviceRejection](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). No credible prototypes were found; these no-parameter declarations only resolve imports, make no ABI claim and must not be called until signatures are verified. +- [sceVrTrackerSetExtendedMode, sceVrTrackerSetLEDBrightness, sceVrTrackerSetRestingMode, sceVrTrackerStartLiveCapture, sceVrTrackerStopLiveCapture and sceVrTrackerTerm](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). No credible prototypes were found; these no-parameter declarations only resolve imports, make no ABI claim and must not be called until signatures are verified. +- [sceVrTrackerGetPlayAreaWarningInfo, sceVrTrackerNotifyEndOfCpuProcess, sceVrTrackerQueryMemory, sceVrTrackerRecalibrate, sceVrTrackerRegisterDevice and sceVrTrackerRegisterDevice2](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). Parameter shapes follow the [pinned shadPS4 declarations](https://github.com/shadps4-emu/shadPS4/blob/f9d07487dd85bb7dc522df6de109b8ad4b9b7175/src/core/libraries/vr_tracker/vr_tracker.h), not measured on a PS5. +- [sceVrTrackerRegisterDeviceInternal, sceVrTrackerResetOrientationRelative, sceVrTrackerSetDurationUntilStatusNotTracking, sceVrTrackerUnregisterDevice and sceVrTrackerUpdateMotionSensorData](../../../core/libs/prx/libSceVrTracker/Export.cpp) (libSceVrTracker) - the list is the intersection of the export table of the console's libSceVrTracker (function symbols only), pinned aerolib names and functions not yet defined anywhere in the repo. [Inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceVrTracker.c); [aerolib](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv). Parameter shapes follow the [pinned shadPS4 declarations](https://github.com/shadps4-emu/shadPS4/blob/f9d07487dd85bb7dc522df6de109b8ad4b9b7175/src/core/libraries/vr_tracker/vr_tracker.h), not measured on a PS5. diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md new file mode 100644 index 0000000000..29b19cc61c --- /dev/null +++ b/docs/dev/debt/libSceVrTracker2.md @@ -0,0 +1,28 @@ +# libSceVrTracker2 technical debt + +### Silent stubs + +- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) never initializes; initialized tracking behavior is absent. Its uninitialized responses were measured on firmware 13.60 without a headset: most exports return 0x8A740001 without writes, while specific output/reset helpers clear buffers or write defaults. These helpers still lack complete signatures. Named export signatures and play-area output layouts are recovered; initialization option semantics and result fields need reconstruction. Anonymous exports remain grouped by their observed response rather than their individual contracts. + +### Unknown function info + +- [sceVrTracker2Initialize](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the 72-byte parameter layout is recovered, including thread priorities, CPU affinity masks, work memory, size/alignment and coordinate system. Meanings of the 32-bit option at 0x30 and byte options at 0x38/0x39 remain unresolved; initialization remains unimplemented. +- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - the seven anonymous throwing exports now have their consumed argument slots restored. Calibration output (`jRmuXjsknsI`) uses a 32-bit capacity with 608/1216-byte variants; its fields remain unresolved. Mesh requirements (`NBhWy+Gl0tY`) contain four 32-bit counts/sizes. The mode/buffer/capacity/size export (`9R6W33Au4B4`) uses 64-bit sizes. The 32/48-byte result parameters are recovered; result output and pose parameters still need full layouts; the telemetry input (`oZDm1bynCaA`) spans 7024 bytes. Their bodies remain unimplemented. + +- [Anonymous tracking queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) still lack initialized behavior. The paired option setter/getter consume 28/40-byte records with three integer options, a reset byte and one/two float vectors; option meanings remain unresolved. The scalar query uses an 8-byte size/float record. Diagnostic output spans 4096 bytes, extended output 0xD02E8 bytes, and tracking configuration input 1024 bytes; their fields remain unresolved. The relative-pose query uses two coordinate-system selectors and a 64-byte output containing a pose and 32 cleared bytes; the counter query uses a 64-bit output and a 32-bit selector whose meaning remains unresolved. Remaining aliases still need individual signatures. + +- [Anonymous diagnostic exports](../../../core/libs/prx/libSceVrTracker2/Export.cpp) have separate pointer contracts for composite poses, configuration, camera images, sensor frames, tracking status and boundary parameters. The camera-image query takes a 32-bit byte count and an in/out frame identifier; diagnostics use six 512-byte text sections, and the mode setter uses a 12-byte size/mode/float record. Composite pose, sensor/status, configuration and boundary fields still require reconstruction; none of these exports supports initialized operation. + +- [Camera-frame exports](../../../core/libs/prx/libSceVrTracker2/Export.cpp) do not acquire or release native buffers. Their descriptor has a selector, two identifiers, dimensions, pitch, pixel pointer and buffer index; native acquisition writes the first 36 bytes, leaving natural tail padding untouched. The optional 48-byte metadata record and identifier meanings remain unresolved. The scalar-option setter/getter use a float value/output, but its units and meaning remain unresolved. + +- [Device and spatial queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) retain uninitialized responses. Position queries use a three-float input or a device handle and a 36-byte size/status/two-vector/scalar result; the vector and scalar meanings remain unresolved. The bounds query uses the recovered 80-byte oriented box. Device-boundary operations consume a handle and selector; their internal parameter layout differs from the public boundary record and remains unresolved, as do the 28-byte device-calibration status and reset-pose input. + +- [Map and flag queries](../../../core/libs/prx/libSceVrTracker2/Export.cpp) retain uninitialized responses. Map initialization takes a device handle and a pointer to one float; map data retrieval takes an output and a 32-bit size constrained to 2048. Map data and status field meanings remain unresolved. Tracking flags use a required boolean output and an optional byte output, rather than an argument-free alias. + +- [Diagnostic records](../../../core/libs/prx/libSceVrTracker2/Export.cpp) have individual output-pointer signatures, but field interpretation remains incomplete. Native runtime status clears 88 bytes; diagnostic state writes 120 bytes, sensor diagnostics reach byte 339, device diagnostics reach byte 6983, timing diagnostics write 80 bytes, and sensor summary writes 32 bytes. The map-configuration getter/setter exchange the same 72-byte record. These exports still return only the uninitialized error. + +- [Extended result and boundary fitting](../../../core/libs/prx/libSceVrTracker2/Export.cpp) retain uninitialized responses. The extended result spans 816 bytes; its coordinate-specific parameter prefixes the 48-byte query with a coordinate selector and four-byte padding. Boundary fitting takes the recovered point list, optionally returns two 80-byte bounds, and writes four byte flags plus four floats; flag and dimension semantics remain unresolved. The device-center output is a 16-byte size/three-float record. + +- [Mesh and image-pose utilities](../../../core/libs/prx/libSceVrTracker2/Export.cpp) retain uninitialized responses. Mesh generation takes vertex and 16-bit index outputs plus a 32-bit format. Timed-pose queries take an in/out result, timestamp and user tag. Image submission consumes a record with format at byte 16 and a data pointer at byte 80; image-pose retrieval uses an in/out record with a buffer pointer and capacity. Record fields and initialized algorithms remain incomplete. Scalar queries use float outputs; the map-scalar query also consumes map data. + +- [Legacy unsupported operations](../../../core/libs/prx/libSceVrTracker2/Export.cpp) have separate input-pointer signatures. Two consume only the size prefix of 12/20-byte records, leaving their other fields unrecoverable from these bodies. The coordinate operation reads a selector at byte 4 but always returns unsupported after initialization; the remaining record layout is unresolved. diff --git a/docs/dev/debt/libSceWebBrowserDialog.md b/docs/dev/debt/libSceWebBrowserDialog.md new file mode 100644 index 0000000000..c057cc3710 --- /dev/null +++ b/docs/dev/debt/libSceWebBrowserDialog.md @@ -0,0 +1,9 @@ +# 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 424-byte allow-list parameter and its twenty optional address pointers are validated, but address filtering is not modelled without an interactive browser. Main open-parameter validation remains incomplete. +- [sceWebBrowserDialogSetCookie, sceWebBrowserDialogResetCookie](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) (libSceWebBrowserDialog) - there is no browser cookie store. Both calls check their parameters in the same order and with the same codes as the PS5 12.00 module, then return 0 without storing or clearing anything. On the console they then forward the request to the browser service. While an opened dialog has not been updated to finished, closed or terminated, they return `0x80B80006` (the console's open-dialog flag). +### Unknown function info + +- [sceWebBrowserDialogGetEvent, sceWebBrowserDialogNavigate, sceWebBrowserDialogSetZoom](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) (libSceWebBrowserDialog) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceWebBrowserDialog.c); the no-parameter prototypes follow [shadPS4 `webbrowserdialog.cpp`](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/web_browser_dialog/webbrowserdialog.cpp#L23-L85) and are not measured on a console. They are declared without parameters solely to resolve known imports; this is not an ABI claim, and every call throws. diff --git a/docs/dev/debt/libSceWkFontConfig.md b/docs/dev/debt/libSceWkFontConfig.md new file mode 100644 index 0000000000..20975e63cd --- /dev/null +++ b/docs/dev/debt/libSceWkFontConfig.md @@ -0,0 +1,5 @@ +# libSceWkFontConfig technical debt + +### Unknown function info + +- [libSceWkFontConfig pending exports](../../../core/libs/prx/libSceWkFontConfig/Export.cpp) - 5 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceWkFontConfig.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceZlib.md b/docs/dev/debt/libSceZlib.md index 5d289d575b..8cbfd2864e 100644 --- a/docs/dev/debt/libSceZlib.md +++ b/docs/dev/debt/libSceZlib.md @@ -2,4 +2,4 @@ ### Functional -- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports one outstanding request, null-timeout waits and one result retrieval. Timed waits, multiple outstanding requests, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. +- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) decompresses synchronously and supports one outstanding request. Timed waits, multiple outstanding requests and repeated retrieval throw. Work-memory registration and driver-side destination alignment validation remain unsupported; non-null work memory and unaligned destinations throw. Native user-space validation confirms the 64-bit initialization length, 2 KiB output-length granularity up to 64 KiB, null-argument errors and initialization precedence. Finalization discards the host's completed result. Driver scheduling, completion consumption, cancellation, scratch-memory requirements and decompression error details remain unverified; host decoding maps malformed streams to 0x811200FF and exhausted output to 0x8112001C. diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index b40ba0c49a..c9cda8fa5a 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -2,75 +2,140 @@ ### Unknown function info +- [strtonum](../../../core/libs/prx/libc/src/StringExtensions.cpp) (libc) is unverified on the console. Its signature, decimal parsing, inclusive bounds, error strings, error precedence and errno preservation follow [FreeBSD 11.4 strtonum.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/strtonum.c). Parsing uses the host `strtoll` with base 10. + +- [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. On Windows, unwinding uses `_Unwind_Backtrace` and stops at a frame without `.ehmeta` or `.ehfram` tables whose PE unwind data has an exception or termination handler, leaving the trace empty or cut short while still returning 0. Implemented only on Linux and Windows; other platforms throw +- [ApplicationGuestHeapApi](../../../core/libs/prx/libc/src/ApplicationProcessParameters.cpp) (libc) - on Linux, HLE application allocation uses a loaded guest SDK's ten allocator exports when the title's replacement table has an allocator entry and the exports belong to one native module. With an empty table it keeps the default heap, since a bundled `libc.prx` may export the allocator NIDs while forwarding them to HLE. Its CRT owns application allocator initialization and finalization; HLE retains the module until process exit. Partial or mixed providers throw. This avoids reinitializing an allocator already used by SDK startup; console behavior and other host platforms remain unverified. - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. +- [scriptingGetMem, scriptingFreeMem](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - runtime allocator hooks for Unity titles. The allocation signature `(alignment, size)`, minimum alignment 16 and null result for a non-power-of-two alignment follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libKernel.cpp); the free hook's one-pointer signature follows [Raeen's recorded contract](https://github.com/Whoisraeen/Raeen/blob/2db56923204b89719916b95e29c2f3c4e1d9f90b/docs/unity-dlsym-blocker.md). Both use libc's own guest heap, not the title's registered allocator. A title that ships its own `libc.prx` (Metal Gear Solid Master Collection, PPSA18194) has already initialized that allocator, and reaching it from here would run its initializer a second time. That title's IL2CPP runtime looks up only `scriptingGetMem`, by name from `Il2cppUserAssemblies.prx`, and asks it for 256 KiB blocks aligned to 4 KiB. Which console library exports the hooks is unknown, and there is no console measurement. Allocation failure and sizes that overflow return null. + - [_Mtx_trylock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `k6pGNMwJB08`) - returns busy for a non-recursive mutex already owned by the caller, assumed to match `_Mtx_lock`'s ownership handling; exact console behavior is unverified. - [nearbyintf](../../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) uses the host rounding mode, not the console's; console rounding mode state remains unverified. -- [_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. +- [__std_find_trivial_unsized_N](../../../core/libs/prx/libc/src/VectorAlgorithms.cpp) (libc) - matches the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs): when nothing matches between `first` and the end of its 32-byte-aligned block, the console rescans that whole block without masking and so can return a match up to 31 bytes before `first` +- [_Thrd_sleep](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep` on POSIX, retrying on `EINTR`, or high-resolution waitable timers on Windows; a null xtime or one whose normalization overflows throws. Guest thread cancellation is not integrated with this wait. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. - [_Mtx_init_with_name](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `tgioGpKtmbE`) forwards to `_Mtx_init` and ignores the mutex name parameter. - [_Cnd_timedwait](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `McaImWKXong`) follows the console's `_Cnd_timedwait` (disassembled from a title's `libc.prx`), where xtime is an absolute deadline converted against the current time: it returns `_Thrd_timedout` (2) on expiration and `_Thrd_success` (0) on notification. The console returns 4 for other internal pthread errors while this implementation throws. +- [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow FreeBSD 11.4 amd64 [fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): rounding modes use x87 control bits 10-11, exception flags include denormals (`0x3f`), and `fexcept_t` is 16 bits. They access the current host thread's x87 and MXCSR state. This console ABI is assumed, as for `fegetenv`, not measured on hardware. Null flag pointers throw instead of faulting. +- [fegetenv](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `psx0YiAKm7k`) follows FreeBSD 11 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/amd64/fenv.c) with the 32-byte `fenv_t` of [x86/fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): `fnstenv` fills the 28-byte x87 part, MXCSR follows at 0x1c, and the x87 control word is reloaded because `fnstenv` masks every x87 exception. That the console's `libSceLibcInternal` has the same code and layout is assumed, not read from its binary. A null pointer throws instead of faulting - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error - [sceLibcInternalHeapErrorReportForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(void* heap, void* block, uint32 error)` follows the same `libc.prx` (call at 0xF634): it passes the heap, the block and the nonzero result of its block check, and then calls a function that never returns, so the report only has to return. The meaning of the error values is unknown; the three values and the guest return address are written to the host standard error +- [sceLibcPafMspaceCreate, Destroy, Malloc, Memalign, PosixMemalign, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) - PAF exports delegate to `sceLibcMspace` without malloc hooks; `Memalign` and `PosixMemalign` forward to their `Mspace` counterparts with owner ignored and are unverified on console. `Create`'s options block (version, type 1 or 2 at +2, field at +0x10) throws when non-null as its meaning is unknown. Block footers are not modelled, so owners are ignored: on PS5 firmware 13.60 mismatched owners in `Reallocalign` or in-place `Realloc` kill the process and `MallocStats` returns 1 if non-heap-owned blocks exist, which is not reproduced. `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` are not implemented. - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` +- [malloc_stats_fast](../../../core/libs/prx/libc/src/GuestHeap.cpp) (libc) - with the default allocator it fills the 0x28-byte managed-size block as `sceLibcMspaceMallocStatsFast` does, from the default heap's own accounting, not Sony's allocator: the system sizes count the 1 MiB spans of its small-block classes and its committed large blocks, cached free ones included, and the in-use sizes count each handed-out block at its size-class or 64 KiB-granule size, header included. A block size below 0x28 or a null pointer returns 22, as in the mspace version; the version field is not checked. Ship of Harkinian (PPSA99320) calls it about 10 s after start-up. - [malloc, calloc, realloc, memalign, aligned_alloc and reallocalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - follow FreeBSD 11.4 `contrib/jemalloc/src/jemalloc.c` for failures, not measured on the console: a size the default heap rejects or the guest arena cannot place (including a `calloc` product that overflows) returns null with `errno` `ENOMEM`, and `memalign` and `aligned_alloc` return null with `EINVAL` for an alignment that is zero or not a power of two. FreeBSD's libc has no `memalign`, so setting `errno` there is assumed. A host commit failure for a block that fits in the arena (Windows `ERROR_NOT_ENOUGH_MEMORY`, `ERROR_OUTOFMEMORY` or `ERROR_COMMITMENT_LIMIT`, Linux `ENOMEM`) resets and releases the reserved range and returns null with `ENOMEM`; any other commit failure releases the range and throws. `reallocalign` returns null with `EINVAL` for an invalid alignment and `ENOMEM` like the others, which is an assumption as it is not in FreeBSD. `realloc(ptr, 0)` frees the block and returns null, where FreeBSD 11.4's jemalloc 4.5 resizes it to one byte and returns a block - [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 +- [Dinkumware atomics](../../../core/libs/prx/libc/src/DinkumwareAtomics.cpp) (libc) - every operation is sequentially consistent whatever memory order it is given, which is never weaker than the console. Matching the `libc.prx` bundled with PPSA08427 (that binary run side by side on every order from 0 to 7), `_Atomic_load_8` returns 0 for a release, acq_rel or invalid order and the 8-byte exchange and fetch operations return 0 without touching memory for an order above `memory_order_seq_cst`; the generic `_Atomic_copy`, `_Atomic_exchange` and `_Atomic_compare_exchange_*` lock the byte flag passed first, as the console does - [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 +- [__atomic_load, __atomic_store, __atomic_exchange and __atomic_compare_exchange](../../../core/libs/prx/libc/src/AtomicBuiltins.cpp) (libc) - the generic, size-parameterised libatomic entry points are a single `ud2` in the `libc.prx` bundled with PPSA08427 (disassembly), so the console traps on any call; these throw instead - [Dinkumware mutexes and conditions](../../../core/libs/prx/libc/src/Threads.cpp) (libc) use pointer-to-handle arguments, matching the console aerolib signatures reported in [#3079](https://github.com/boykopovar/AnyPS5/pull/3079). The title's `_Mtx_lock` maps `0` to `_Thrd_success` (0), `0x8002000b` to `_Thrd_busy` (3), and other failures to `_Thrd_error` (4); its internal mutex init recognizes recursive flag `0x100`, while every other type uses default attributes. The observed relock failure for a plain mutex suggests those defaults are error-checking, but that has not been confirmed on hardware. The title's `_Mtx_unlock` and `_Cnd_wait` return success regardless of their internal result. The host implementation treats invalid ownership preconditions as no-ops, and throws for invalid handles, host wait errors and destroying a locked mutex as debug guards; those cases are not confirmed against the console. +- [Floating-point compiler-rt builtins](../../../core/libs/prx/libc/src/FloatBuiltins.cpp) (libc) follow the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs) where it differs from current compiler-rt: the soft-float `__add`, `__sub`, `__mul` and `__div` (`sf3`, `df3`) round to nearest, raise no exception flags and ignore MXCSR, here done with SSE under a default MXCSR that is restored afterwards; division flushes a quotient below the smallest normal to a signed zero; `__extendsfdf2` and `__truncdfsf2` copy NaN payloads unshifted; float-to-integer conversions do not saturate but shift the significand by a count masked to the shift width. `__mulsc3`, `__muldc3`, `__divsc3` and `__divdc3` fuse the same products as the console with `fma` and scale through the x87 like its `scalbn`, so their values match, but NaN payloads and exception flags can differ, as the host `fma` and `logb` are not the console's VEX FMA code and libm - [random, srandom, initstate, setstate](../../../core/libs/prx/libc/src/Random.cpp) (libc) follow FreeBSD 11 [random.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdlib/random.c): the additive feedback generator on a default 128-byte `TYPE_3` table, `good_rand` seeding, state sizes rounded down to 8, 32, 64, 128 or 256 bytes, and `setstate` returning the previous state or null for a rear position past the degree. `initstate` below 8 bytes returns null without setting errno, as that file and its manual do (glibc sets `EINVAL`). FreeBSD does not lock the generator; here every call is serialised by one lock, so concurrent callers draw the serial sequence between them. A null state throws, where FreeBSD faults. That the console's `libSceLibcInternal` uses this FreeBSD code and not another generator is unverified +- [Integer compiler-rt builtins](../../../core/libs/prx/libc/src/IntegerBuiltins.cpp) (libc) - the console traps in `__divsi3`, `__modsi3` and the 64- and 128-bit divisions on a zero divisor (`div`) and in `__negv*`, `__absv*` and `__mulv*` on overflow (an abort helper, then `ud2`); these throw instead. As in the `libc.prx` bundled with PPSA08427 (disassembly), `__addv*` and `__subv*` wrap without an overflow check, and `__udivsi3`, `__umodsi3` and `__udivmodsi4` divide in software, giving a quotient of 0 and the dividend as remainder for a zero divisor; `__clzdi2` and `__ctzdi2` of 0 return 32 and `__clzti2` and `__ctzti2` of 0 return 64 - [qsort](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) follows FreeBSD 11 [qsort.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdlib/qsort.c): Bentley and McIlroy's quicksort, with insertion sort below 7 elements and after a partition pass that swaps nothing, so elements that compare equal end in FreeBSD's order and the comparator is called in the same sequence. That the console's libc uses this FreeBSD code is unverified -- [memcpy_s and memmove_s](../../../core/libs/prx/libc/src/Strings.cpp) (libc) use the C11 K.3.7.1.1 and K.3.7.1.2 size constraints, with RSIZE_MAX taken as SIZE_MAX >> 1 as in memset_s. An oversized destination size returns ERANGE (34) without touching the destination; the console's RSIZE_MAX and exact error value are unverified. Constraint violations return an error without invoking the registered constraint handler, as before. -- [snwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` +- [memcpy_s, memmove_s, strcpy_s, strncpy_s, strcat_s, strncat_s, wmemcpy_s, wmemmove_s, wcsncpy_s, wcscat_s, wcsncat_s](../../../core/libs/prx/libc/src/Strings.cpp) (libc) use the C11 Annex K destination size constraints, with RSIZE_MAX taken as (SIZE_MAX >> 1) / sizeof(char16_t) for wide operations and SIZE_MAX >> 1 for byte operations. 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: the handler installed with `set_constraint_handler_s` is not invoked, 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. +- [snprintf_s and vsnprintf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - follow C11 K.3.5.3.5 and K.3.5.3.12 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`, as `snwprintf_s` does: a null buffer or format, `size` of 0 or above `RSIZE_MAX`, any `%n` and a null `%s`, `%ls` or `%S` argument return a negative value and set `buffer[0]` to zero when the buffer and size allow it; otherwise the output is truncated to the buffer and the full length is returned. A violation only returns: the constraint handler is not invoked, and the console's error value is unverified. Not confirmed on a PS5 title +- [vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - returns the count of items converted before an undersized `%s`/`%c`/`%[` destination (0 when it is the first), not EOF. Not confirmed on a PS5 title - [localeconv](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, re-exported by libSceAmpr) - the 0x90-byte Dinkumware struct (empty monetary strings, `CHAR_MAX` for the 14 `char` fields, `decimal_point` ".", `_False`/`_True` "false"/"true") is read from the `libc.prx` that Astro's Playroom ships. It is a fixed C-locale table: whether the console changes it with `setlocale` is not known, and `_Frac_grouping`, `_Frac_sep`, `_No`, `_Yes` are empty strings as in that dump -- [set_constraint_handler_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc, re-exported by libSceAmpr) - the default handler is assumed to abort; each module keeps its own handler state, so a handler set through one library is not seen by the other. `ctime_s` and `asctime_s` follow the Dinkumware 26-byte format with `EINVAL` for a buffer under 26 bytes, a null pointer or an out-of-range weekday/month; `ctime_s` uses the guest time zone of `localtime_s`, and the console's exact checks are not verified +- [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. Within a module every `_s` function calls the installed handler, with the guest's calling convention. `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 +- [qsort_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) sorts as `qsort` does and passes its context as the comparator's third argument, as FreeBSD 13's [`qsort_s`](https://github.com/freebsd/freebsd-src/blob/release/13.0.0/lib/libc/stdlib/qsort.c#L97-L127) (C11 Annex K) does. It returns `EINVAL` after calling the constraint handler when `n` or `es` exceeds `RSIZE_MAX`, or when `n` is not 0 and `a` or `cmp` is null; FreeBSD 11, which the rest of libc follows, has no `qsort_s`. An element size of 0 sorts nothing, where FreeBSD loops over the same address. The console's behaviour is unverified; the Quake homebrew (#6698) imports it from its `libOSMesa.so.8` - [__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 + +- [snprintf and vsnprintf](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) reject sizes above `INT_MAX + 1` with `EOVERFLOW` (84), a terminating NUL and return value -1, following FreeBSD 11.4 [snprintf.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/snprintf.c) and [vsnprintf.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/vsnprintf.c). This limit is unverified on the console. A null buffer at an oversized nonzero size returns the error without writing, where FreeBSD dereferences it. ### Functional +- [_Cosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the two-argument Dinkumware CRT helper used by `std::complex`, including a scaled large-finite-input path; the signature, exact result and floating-point exception behavior are not verified against the console. +- [_FSinh and _FCosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) finite results are not verified bit for bit against the console: the scaled product uses the host's binary64 hyperbolic function before rounding to binary32, rather than the float rational and `_FExp` sequence in PRAGMATA's bundled `libc.prx`. NaN preference and payloads, signed zeros, infinite operands and the range-error thresholds follow that binary's instructions; the system `libSceLibcInternal` implementation and nondefault flush-to-zero modes are unverified. + +- [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. +- [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. + +- [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name, changing `%Z` output. +- [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. + - Windows direct-memory backing reserves a paging-file section. A [direct memory page](../../../core/libs/prx/libc/include/WindowsMappings.hpp) mapped at one guest address is private write-watched memory at that address and is not committed in the section; a page mapped at a second address moves its whole resident run into the section (committed there) and every address becomes a view of it, tracked by write protection. An unmapped resident page that is not all zeros is kept in a private reservation until it is mapped again. Committed section contents remain across unmap/remap and are released when the backing and its remaining views are destroyed. A host import alias over part of a resident run is refused. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. -- [getc_unlocked, putc_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. +- Stable Windows resident runs of at least 256 KiB use temporary read-only protection for clean write-watch proofs. The first CPU write restores the logical protection of the whole run; that run remains on native write watching until it is remapped, so repeatedly rewritten runs stay writable. Native kernel writes into guest buffers must hold [GuestArena::HostWrite](../../../core/libs/prx/libc/include/GuestArena.hpp), as shared views already require: an unbracketed WinAPI ReadFile can fail with ERROR_NOACCESS while the resident proof is armed. Guest read, pread and socket receive paths already bracket their destinations. GPU host-import access suppresses resident rearming without inventing shared-page stores. This is a host integration requirement, not a change to guest logical protection. +- [getc_unlocked, putc_unlocked, getchar_unlocked, putchar_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. +- [feof_unlocked and ferror_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) have no console measurement. The firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c) names NIDs `NuydofHcR1w` and `yxbGzBQC5xA`. They follow FreeBSD 11 [feof.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/stdio/feof.c) and [ferror.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/stdio/ferror.c), returning 0 or 1 from the stream status without acquiring the stream lock. They inherit the host CRT limitations of the unlocked byte operations above. +- [fgetws, getwc, getwchar, putwc, putwchar](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - wide stream I/O encodes and decodes wide characters as UTF-8 on the underlying byte stream; `fgetws` reads characters into the destination buffer until newline or EOF and null-terminates, returning null on stream error. Console wide stream buffering and encoding details remain unverified. +- [Closed standard descriptors](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - on Linux and macOS, a descriptor 0, 1 or 2 that is closed when libc loads is opened on `/dev/null` (read-only for 0, write-only for 1 and 2) by a static initializer of libc, which runs before the libraries that depend on libc initialize and before the title starts. Within libc this relies on no static initializer opening a descriptor before it, which `guest_closed_standard_descriptors` checks. glibc does the same for set-user-ID programs ([`check_fds.c`](https://sourceware.org/git/?p=glibc.git;a=blob;f=csu/check_fds.c)). Without it the first descriptor a library opens, the write watch's `userfaultfd` for one, becomes the guest's standard input or output. What the console gives a title as its standard descriptors is not measured, and on Windows a process started without standard handles is not handled. +- [Guest path names](../../../core/libs/prx/libc/src/General.cpp) (libc) are checked against FreeBSD 11.4's `NAME_MAX` (255 bytes per component) and `MAXPATHLEN` (1024 bytes with the terminating NUL) before they are resolved, and a longer name fails with `ENAMETOOLONG`, as `namei` does. The host's own limit still applies below that: on Windows a path whose host form exceeds `MAX_PATH` fails with `ENOENT`. A backslash counts as a path separator for the component length, because the guest path resolver turns it into `/` before the host call, while FreeBSD treats it as an ordinary name character, so a 301-byte name that contains one is accepted here. `sceNgs2ParseWaveformFile` resolves its path without a try block, so a long component ends in an uncaught `filesystem_error` there where a missing file ended in a `runtime_error` (both end the title). A path through a regular file (`file/x`, `file/`) is not always `ENOTDIR` on the calls that resolve it. None of this was measured on a console. - [ftrylockfile](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it +- [fgetln](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) throws: in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5FE80) the export is a `hlt` instruction, which faults in user mode and ends the process - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. -- [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `NAN(n-char-sequence)` converts to the quiet NaN that `nan` and `nanf` return, negated after a minus sign, and ignores the sequence as they ignore their tag. This is inferred from C17 7.12.11.2p2, which makes `nan("n")` equivalent to `strtod("NAN(n)", NULL)`, and not observed on the console. No console `nanl` was observed, so `strtold` returns the x87 default quiet NaN (exponent `0x7fff`, significand `0xc000000000000000`) +- [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there +- [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) +- [_Stod](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libSceLibcInternal, implemented in libc) throws for hexadecimal text with a non-zero power-of-ten scale. The console's result there does not follow a simple rule: `0x3` with a scale of -1 gives 0.29999999999999999 +- [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console +- [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. -- [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 +- [jrand48 and mrand48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) return the high 32 bits of the new 48-bit state zero-extended to 64 bits, as the console's `libSceLibcInternal.sprx` does when called from a homebrew payload process on a PS5 (firmware 12.70): a result with bit 31 set comes back between 0x80000000 and 0xFFFFFFFF, as FreeBSD 11.0's [`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/jrand48.c#L23) and [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/mrand48.c#L25) return it; FreeBSD 11.1 and later cast it to `int32_t` ([`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/jrand48.c#L26), [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/mrand48.c#L28)), which makes it negative. `lrand48` and `nrand48` matched FreeBSD there. Whether a title's own libc does the same is unverified +- [seed48 and lcong48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x6F460 and 0x6F420): the multiplier and addend are shared by the whole `rand48` family, including `erand48`, `nrand48` and `jrand48` on a caller's seed; `lcong48` sets them with the seed, and `srand48` and `seed48` restore 0x5DEECE66D and 0xB. Unlike FreeBSD, `seed48` returns a static copy of the new seed rather than the previous one. A null argument, which the console dereferences, throws +- [lgamma and signgam](../../../core/libs/prx/libc/src/Math.cpp) (libc) - `lgamma` returns the host's `lgamma` and stores the sign of Γ(x) in the exported `signgam` (NID `5gOOC0kzW0c`, starting at 0 as in FreeBSD 11's [`s_signgam.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_signgam.c)) by the rules of [`e_lgamma_r.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma_r.c): -1 for -0 and for a negative non-integer whose floor is odd, 1 otherwise, including poles, infinities and NaN. Taken from the FreeBSD source, not measured on the console. The NID is in the aerolib symbol list; no title is known to read `signgam`. PPSA99004 imports `lgamma` - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed +- [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ (`y0f`, `y1f`, `ynf` follow the console at `+-0`). As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code - [atanhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `atanhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_atanhf.c`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_atanhf.c) calling msun's [`log1pf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_log1pf.c), whose paths for the positive arguments it passes are inlined here; the exported `log1pf` is still the host's. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, and for |x| = 1 it raises `FE_DIVBYZERO` and sets `errno` to `ERANGE` before returning `x / 0`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits, `errno` and floating-point exceptions as this implementation for all 2^32 arguments; it was not run on Linux. That the system `libSceLibcInternal`, which Dusklight (PPSA99305) imports while bundling a zero-return `libc.prx`, has the same code is assumed +- [j0, j1, jn, y0, y1 and yn](../../../core/libs/prx/libc/src/Bessel.cpp) (libc) call the host's `sin`, `cos`, `sincos` and `log`, so the last bits can differ from the console. The rest follows the system `libc.prx` built as `J03093718` (disassembly), which given the console's own `sin`, `cos`, `sincos` and `log` gave the same bits and exception flags for 90 million arguments: msun's [`e_j0.c`, `e_j1.c` and `e_jn.c`](https://github.com/freebsd/freebsd-src/tree/release/10.0.0/lib/msun/src) with each multiply-add the console fuses done as one FMA. As in that release, before FreeBSD commit `186f620727`, `y0`, `y1` and `yn` return `-inf` for ±0 and `0x7ff8000000000000` for a negative argument without raising an exception. The console's `y0f`, `y1f` and `ynf` raise none either, unlike [ours](../../../core/libs/prx/libc/src/Math.cpp), which raise `FE_DIVBYZERO` at ±0. The order wraps at 32 bits as on the console: `jn(1 << 30, x)` never returns for 2^-29 <= |x| < 2^30 +- [getenv, setenv, unsetenv, putenv](../../../core/libs/prx/libc/src/Environment.cpp) (libc) keep one guest environment that starts empty, like the `environ` libkernel exports, and never reads or changes the host's. A title sees only the variables it sets itself; host variables such as `HOME` and `PATH` name host paths a guest cannot use (PS5MC, PPSA99420, exits with `XBMC_Run returned -1` when it gets the host's `HOME`). Whether the console starts a title with any variables set is not measured +- [mergesort](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) checks its arguments like the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x762D0): an element size under 4 sets `errno` to `EINVAL` (22) and returns -1, an empty array returns 0, and a failed `malloc` of the count times the size plus 8 bytes returns -1. The sort is a stable bottom-up merge, not FreeBSD's natural merge with its insertion sort for short runs, so the comparator is called in a different order and number of times; the result is the same for a consistent comparator. A null base or comparator and a size product that overflows, which the console does not check, throw - On macOS the [write watch](../../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: - - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (file reads and socket receives); a host call that writes guest memory without it (`clock_gettime` and the like into a watched page) fails; + - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (file reads and socket receives), as [Host writes into guest memory](../CONVENTIONS.md#host-writes-into-guest-memory) requires; a host call that writes guest memory without it (`clock_gettime` and the like into a watched page) fails; - code that changes the protection of watched pages, or maps over them, has to call `GuestWriteWatchProtectionChanged`, and code that asks the kernel for a page's protection has to ask `GuestWriteWatchProtection` first, since a watched page is read-only between a collection and the next write. Nothing calls either yet: the macOS memory paths are not in the tree; - the handler is installed when libc loads and passes other faults to the handler installed before it. A handler installed later has to do the same. +- On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps the range up to 0x70_0000_0000 unmappable for every process (max protection none). It starts at 0x10_0000_0000 on macOS 26.5 (M1 Pro, arm64 and x86_64 under Rosetta) and at 0xF_C000_0000 on macOS 27 (27.0.1 on an M4 Pro, arm64 and x86_64 under Rosetta; 27.2 on an M2 Max, arm64). With dyld and the Rosetta runtime near 0x2_0200_0000 and 0x3_0400_0000, a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them. - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync` - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way -- [`fopen` streams](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access. On Windows, opening a directory in a read mode fails with `EACCES`, where FreeBSD opens it; directory streams are not emulated +- [`fopen` and `freopen`](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows: `freopen` onto a directory in a read mode fails with `EACCES`, where FreeBSD opens it. Reading a directory stream from `fopen` fails with `EISDIR` and sets the error indicator on every host, as FreeBSD 11 tmpfs does; FreeBSD 11 FFS returns the directory's raw blocks instead, and what the console's filesystems return is unverified +- [readdir and readdir_r](../../../core/libs/prx/libc/src/Directory.cpp) (libc) on Windows report, on a file system whose directory listing carries no file ID (the entry's ID is 0, or the volume refuses `FileIdBothDirectoryInfo`), a nonzero `d_fileno` hashed from the entry name and the directory's file index, which differs from `lstat`'s `st_ino`. Not reproduced: NTFS lists an ID for every entry +- [Writes on an update stream](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows go through the UCRT, which drops a write that directly follows a read without `fflush`, `fseek`, `fsetpos` or `rewind` unless the read reached end of file: `fwrite` returns 0, and `fputc` and `fprintf` report success, without the error indicator. FreeBSD 11.4's [`__swsetup`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/wsetup.c#L59-L74) discards the read buffer and writes at the descriptor's offset, past a read-ahead of `st_blksize` bytes for a regular file, so where the console writes depends on its block size. Reads directly after writes follow FreeBSD's `__srefill` +- [`fopen` of /dev/random and /dev/urandom](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) in a read mode returns a stream over the host null device whose `fread` fills the buffer with host OS entropy, as the libkernel [random device](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) descriptors do. Only the exact paths are recognised, the stream has no descriptor, `setvbuf`, `feof`, `ferror`, `clearerr`, `flockfile`, `ftrylockfile`, `funlockfile` and `fclose` work on it, `fflush` returns 0 as on a FreeBSD read-only stream, and every other stream function throws, as does opening the paths for writing. `freopen` onto these paths still fails with `ENOENT`. ProsperoTV (PPSA99003) reads its phone pairing code this way +- [RemoveTree](../../../core/libs/prx/libc/include/RemoveTree.hpp) (libc) removes a host tree itself on Windows instead of calling `std::filesystem::remove_all`: MinGW libstdc++ up to GCC 16 does not treat a reparse point as a link, so `remove_all` recurses through it and never terminates, and any tree that holds a symlink hangs. It builds the entry list from `GetFileAttributesW`, removes a reparse point as the link it is rather than its target, clears the read-only attribute and retries as `remove_all` does, and reports the first native error. The libstdc++ fix is in GCC 17 (commit 88856ad855ae); other hosts call `remove_all`. - The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked -- The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) steps through frames of modules without the project's `.ehmeta` and `.ehfram` tables (system DLLs, and WinLibs GCC 15.2's `libstdc++-6.dll` and `libwinpthread-1.dll`, whose `.eh_frame` sections are empty) with their PE unwind data through `RtlVirtualUnwind`, but does not run Windows exception or termination handlers: it stops at a frame whose unwind data has one, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), and the exception is then reported as unhandled +- The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) cannot run arbitrary Windows exception or termination handlers. It accepts GCC C and C++ handlers identified by a project's `.sehgcc` section and interprets their LSDA with the guest personalities. A handler in an unmarked module, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), still stops unwinding and the exception is reported as unhandled. SjLj exceptions are unsupported; Scoop GCC 15.2.0 selects them when SEH is disabled and lacks the System V ABI SEH compiler fix carried by MSYS2. - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. -- [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists the host's IPv4 and IPv6 addresses in FreeBSD's `ifaddrs` and sockaddr layouts. FreeBSD's per-interface AF_LINK entries (MAC, index, MTU) and their `if_data` are not produced, so interfaces without an address are omitted. Names are the host's (Linux `lo`, `eth0`; Windows NDIS names such as `ethernet_32769`, or `if` when one does not fit 15 bytes), not the console's. A Linux point-to-point address without a peer loses `IFF_POINTOPOINT`, and an IPv4 broadcast address the host omits is computed from the netmask. On Windows the flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST`, derived from the adapter: PPP and tunnel adapters get neither `IFF_BROADCAST` nor `IFF_POINTOPOINT` because their peer address is not reported, and an address whose prefix length the host reports as unspecified gets a host mask. +- [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. Windows omits per-interface AF_LINK entries and interfaces without an address; its flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST` (PPP/tunnel adapters omit broadcast and point-to-point, and an unspecified prefix length gets a host mask). On Linux, `AF_LINK` `if_data` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1) and leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null; a point-to-point address without a peer loses `IFF_POINTOPOINT`, and an omitted IPv4 broadcast is derived from the netmask. Interface names are the host's (`if` on Windows above 15 bytes). - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. +- [wctomb_s, wcrtomb_s, mbstowcs_s and mbsrtowcs_s](../../../core/libs/prx/libc/src/BoundsChecking.cpp) (libc) follow the console's `libc.prx`, but convert in the single-byte C locale of `wctomb`, `wcrtomb` and `mbrtowc` (`MB_CUR_MAX` 1): a character above 0xFF is `EILSEQ` in `wctomb_s` and `wcrtomb_s`, and each byte is one wide character in `mbstowcs_s` and `mbsrtowcs_s`. The locale of a title's `libc.prx` was not measured. The `wcrtomb_s` path where a character does not fit `smax` and the `EILSEQ` paths of `mbstowcs_s` and `mbsrtowcs_s` are not reached - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached -- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments or `%m`, so a narrow `%p` and a NaN there still follow the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`, and a NaN with its sign, `-nan`, and with the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter -- [collate::_Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. Its result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. -- [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, on Linux throws for the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`): glibc would store a 32-bit `wchar_t` per character into the guest's 16-bit buffer. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits +- [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x7D480 to 0x7D920), which converts to and from UTF-8 whatever the locale, with the original sequences of up to six bytes and no check for surrogates or for values above 0x10FFFF when encoding. The conversion state keeps the value in its first eight bytes and the count of bytes still expected (or 0x10 for a pending low surrogate, 1 for a stored high surrogate) as 16 bits at offset 0xA; a null state, which the console dereferences, throws. As on the console, `mbrtoc16` does not store the complete code point when it returns a high surrogate, so the low surrogate of the next call is built from the value the state last held, which is the partial value of an earlier incomplete call or whatever the caller initialised it to, `c16rtomb` returns -1 without setting `errno` when a high surrogate is not followed by a low one, and `mbrtoc16` and `mbrtoc32` report `EILSEQ` as 86. Measured on a PS5 (firmware 13.60) from a homebrew payload, the system `libSceLibcInternal`'s default locale also converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen` (`wctomb` of U+263A gives `E2 98 BA`, `mblen` of a lone 0xE9 gives -1); this libc keeps its single-byte C locale, so [wctomb and mblen](../../../core/libs/prx/libc/src/Multibyte.cpp), which call `wcrtomb` and `mbtowc` here as they call the same internal routines on the console, share that difference. Titles bundle their own `libc.prx`, whose locale was not measured +- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, `%m` or `%p`, so a NaN there still follows the host (glibc prints its sign, `-nan`, and honors the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter +- [%p and %lp](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) follow the console's `libSceLibcInternal.sprx` measured from a homebrew payload on firmware 12.70 ([#7549](https://github.com/boykopovar/AnyPS5/issues/7549)), not FreeBSD: bare lowercase hex with no `0x` prefix even under `#`, `0` for a null pointer, `+` and the space flag ignored, the width counting only the digits, `-` padding on the right, `0` padding with zeros when there is no precision, the precision as the minimum number of digits, nothing for a null pointer under `%.0p`, and `l` accepted. Only `snprintf` and `swprintf` of that module were measured, from a payload process rather than a title process, so a width beside `%.0p`, the `0` flag beside a precision (which C17 ignores) and `%.0p` of a non-null pointer follow this formatter's own rules and are not measured; `swprintf` was not run with `%lp`, so the wide family still refuses that conversion and returns -1. Linux routes `%p` and `%lp` to this formatter because glibc prints the FreeBSD form and `(nil)` for a null pointer, which means a format that combines `%p` with a conversion this formatter refuses, such as an argument position above 99, throws there instead of reaching glibc +- [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits::compare` does), `transform` copies the characters into the result and records size and capacity in 16-bit elements (inline capacity 7, heap capacity `length | 7`, `(capacity + 1) * 2` bytes including the terminator), following Dinkumware's [VC12 `basic_string`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstring#L500-L531), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. +- [std::__throw_bad_variant_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) throws `std::bad_variant_access` with the `what()` string `bad_variant_access`, the one [LLVM libc++](https://github.com/llvm/llvm-project/blob/main/libcxx/src/variant.cpp) returns, since the export carries libc++'s `__throw_bad_variant_access` name; the console's string is unverified. Onimusha: Way of the Sword (PPSA27836 01.000.001) imports it from `libc`, and its bundled `libc.prx` does not export it. +- [std::bad_optional_access and std::_Throw_bad_optional_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) provide the exception type, constructor, destructor, and `what()` exports alongside `_Throw_bad_optional_access`, returning the `what()` string `bad_optional_access` matching Dinkumware and libc++ conventions; the console libc.prx string is unverified. PPSA26578 (issue #7624) and PPSA34015 import `_ZSt26_Throw_bad_optional_accessv` (`KwzEip7m6TM`) from `libc`, and fail to load on main with unresolved import (0xC0000139). +- [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, decodes the input of the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) on Linux as UTF-8, mirroring the narrow printf family, where FreeBSD uses the locale's `mbrtowc` (one byte per character in the C locale); the console's locale encoding is unverified. Input that is not UTF-8, a character outside the BMP, a scanset member outside ASCII, positional arguments next to a wide conversion and wide conversions in `fscanf` throw. Each character is stored as one 16-bit unit, widths and `_s` sizes count characters, and a `%lc` field cut short by the end of the input keeps what it read, as FreeBSD's `convert_wchar` does. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` -- [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx` +- [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx`. A relinked module request that misses both the exact and the case-insensitive `.guest.prx` takes the sibling whose stem (the name without its last extension, ASCII case ignored) equals the requested stem and that ends with `.guest.prx`, shortest name first: Unity IL2CPP titles load `Media/Modules/.prx` and `Media/Plugins/.prx`, and the relinker names the converted module after the ELF it was built from, `.elf.guest.prx`. The console's module namespace is assumed to ignore case and the extension; several stem matches pick the shortest name instead of throwing, which is not measured on a console +- [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount - [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A zone with no daylight-saving time has `tzname[1]` empty on Windows and equal to `tzname[0]` on Linux, where FreeBSD leaves three spaces. The console's names are not known - [Trailing slash on a mount alias](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows a trailing slash after a file name under a mount alias such as `/savedata0` is still dropped, so `unlink`, `rename`, `open` and the other path calls act on the file where FreeBSD 11.4's `lookup()` fails with `ENOTDIR`; Linux and macOS pass the slash to the host, as for paths outside an alias -- [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. `gmtime` converts every time whose year fits an `int` on both hosts. +- [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. On macOS `mktime` converts through the host's `mktime`, which refuses every year before 1900. A local time before 1900 is instead solved for directly: the offset `localtime_r` gives for a candidate time, which follows the zone's history (local mean time before its first transition), is subtracted from the local fields, and the step is repeated until the offset holds. `tm_isdst` is not consulted there, so a time with `tm_isdst` set where the zone had no daylight offset is taken as standard time. FreeBSD's [`time1`](https://github.com/freebsd/freebsd-src/blob/stable/11/contrib/tzcode/stdtime/localtime.c#L2036) would instead shift it by the difference between one of the zone's daylight types and a standard one. `gmtime` converts every time whose year fits an `int` on both hosts. - [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow FreeBSD 11 [strftime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdtime/strftime.c) and [asctime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/contrib/tzcode/stdtime/asctime.c) with the C locale on every host, not measured on the console; other locales are not implemented. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%Z` is `tm_zone`, or `tzname[tm_isdst != 0]` when it is null and `tm_isdst` is not negative. The two-digit year warning FreeBSD writes to `stderr` when `CHECK_STRFTIME_FORMATS_FOR_TWO_DIGIT_YEARS` is set is not written - [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follows FreeBSD 11.4's [wcsftime.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/wcsftime.c) over this library's `wcsrtombs`, `strftime` and `mbsrtowcs` in the single-byte C locale of [none.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/none.c), with the guest's 16-bit `wchar_t`, without console verification. It has the `strftime` gaps above. A format unit above 255 returns 0 with `EILSEQ`; an empty result, a result that does not fit and a null buffer return 0 with `errno` unchanged, discarding the `ERANGE` or `EINVAL` that the Windows CRT's `strftime` sets. A null format or `tm` throws, where FreeBSD dereferences it (a null `tm` is read there only when the format has a conversion that needs it) - [syslog](../../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER` or the facility set by `openlog`, and errno is preserved. `openlog` applies its identity and `LOG_PID`; a null identity retains the previous one, and a valid nonzero facility replaces the default. `closelog` clears the identity but retains the options and facility, as in FreeBSD 11; when no identity is set, FreeBSD falls back to the program name, which this backend does not emit. `LOG_PERROR` has no extra effect because stderr is the emulated log sink; `LOG_NDELAY`/`LOG_ODELAY`, `LOG_NOWAIT` and `LOG_CONS` have no daemon, child or console backend to control. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified +- [fwscanf, vfwscanf, wscanf, vwscanf and their `_s` forms](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) run the `swscanf` scanner on characters read with `fgetwc`, so everything listed for `swscanf` in this file applies to them. As in the console's `libc.prx`, a field takes characters while they can still begin a valid item, so a failed field such as `0x` for `%x` stays consumed, and only the character that ended the scan goes back to the stream. That character is pushed back as its UTF-8 bytes with `ungetc`, because `ungetwc` takes single bytes only; the Windows C runtime guarantees one byte of push-back, so a non-ASCII lookahead can be lost there. A null stream or format returns `EOF`; the console checks them only in the `_s` forms. - [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981)) -- [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 +- [_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 at end of input returns the number of assignments, including zero, rather than `EOF` as FreeBSD does, as reported from the console scanner in #8479; `%%` at end of input still returns `EOF`. A literal mismatch is a matching failure, where FreeBSD's `vfwscanf` reports an input failure. An incomplete floating exponent such as `1e+` converts its mantissa, as reported from the console scanner in #8479; other incomplete exponent forms are not separately verified. A short `%c` field is currently a matching failure without storing its available prefix, while #9321 reports that the console stores a short `%3lc` field; its return count, partial-output extent and precedence are unverified. An integer field that is only a prefix, such as `0x`, does not convert; FreeBSD's `vfwscanf` pushes back the `x` and converts the `0`. Integers out of range are truncated from `strtoll`/`strtoull`. `%p` follows FreeBSD 11's [vfwscanf.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdio/vfwscanf.c#L610-L615): a hexadecimal integer with an optional `0x`, converted as unsigned and stored as a pointer; a length modifier on it throws, where FreeBSD ignores it. It throws for `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The lock is a host [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) lock on one byte at offset 2^63 - 2, which no file reaches, so it excludes other `flock` calls and leaves reads, writes and mapped views alone, like FreeBSD's advisory locks. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. -- [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, selected by the first matching errno entry in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects::_Generic_object`, and the new `generic_category()` export returns that object; a title's inline category object in a bundled libc can still have a different identity. Its `message` and direct message export construct the existing [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) model: a reserved eight-byte word, 16-byte inline buffer, length and capacity, with guest `operator new` storage above 15 bytes. This 40-byte string layout and virtual return ABI have not been verified against a console library. The deleting destructor still throws. `_Syserror_map` covers the 72 distinct Dinkumware `errc` values present in FreeBSD 11.4 [errno.h](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h), returning null for other values; category messages use `unknown error` for those. First-match aliases are `EAGAIN`/`EWOULDBLOCK` -> `operation would block` and `ENOTSUP`/`EOPNOTSUPP` -> `not supported`. STREAMS-only errno assignments absent from that header (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are not inferred -- [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested +- [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) follows Dinkumware `thread0.cpp` ([VC12](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/thread0.cpp), [STL](https://github.com/microsoft/STL/blob/da52dc0f/stl/src/thread0.cpp)), without console verification. Codes outside 0..6 throw `std::out_of_range`, the deleting destructor throws, and STREAMS errnos absent from FreeBSD 11.4 [`errno.h`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h) (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are omitted. Codes 0..6 throw a 32-byte [VC12 `system_error`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/system_error) with FreeBSD errnos (16, 22, 3, 12, 1, 11, 35), a static `generic_category` (not `_Generic_object`), and unverified 40-byte [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) messages from [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/syserror.cpp) +- [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) is missing on macOS and records only the entry of a call, not its arguments or result. One process-wide ring keeps the last 65536 calls, so a busy thread pushes the others out; the reporting thread's own calls are listed only while they are still in that ring. Calls are counted per traced target; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; wider vector arguments survive only while the recording code avoids AVX, which is untested. Linux has no crash report, so it prints the trace only on `SIGABRT` (guest or host `abort()`), not on `SIGSEGV`; a caller in a module loaded after the last traced import prints without a module name, and a thread aborting while another writes the report waits for it at most 2 seconds - [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2. +- [app0.zar](../../../core/libs/prx/libc/src/AppArchive.cpp) (libc) - a [ZArchive](https://github.com/Exzap/ZArchive) beside `app0/` is a read-only layer under it: a path missing from `app0/` is looked up in the archive, case-insensitively. It is not a console format; titles ship their files in a PFS image instead. Resolving an archive-only path through any call other than the archive-aware libkernel ones throws, so `fopen`, `stat`, `access`, `opendir` and the rest do not read the archive yet, and `opendir` of a directory that exists in both places, `app0/` itself included, and `fdopen` of an archived descriptor throw instead of missing its entries or reading the null device. A damaged compressed block throws instead of returning `EIO` +- [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified +- [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. +- [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes. The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered +- [num_get::id, time_get::id and the num_get vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) are data objects, as the public SDK's libSceLibcInternal table declares them and as the other facet ids and the `num_put` and `codecvt` vtables are kept: the ids start at zero and the vtable is 17 zeroed slots, the 136-byte size of `KfcTPbeaOqg` in the console's libSceLibcInternal, whose relocations fill the offset-to-top and typeinfo entries, the two destructors, `locale::facet::_Incref` and `_Decref`, and the eleven `do_get` of Dinkumware's crt.vc12 `xlocnum`, so a guest resolves the symbols and reads data, and a facet call through the vtable faults. libSceAmpr exported the three as throwing functions, which a data consumer reads as code; its qualified imports fall back to libc. diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index f947d86c51..40d8b39075 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -2,37 +2,47 @@ ### Silent stubs +- [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_getprotocol, pthread_mutexattr_setprioceiling, pthread_mutexattr_getprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - the protocol and the priority ceiling are stored and reported as in [FreeBSD 11 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libthr/thread/thr_mutexattr.c#L192-L245) (the ceiling is set or read only under `PTHREAD_PRIO_PROTECT`, and setting a protocol resets it to the highest `SCHED_RR` priority). A null, uninitialized or destroyed attribute returns `EINVAL` before the other arguments are checked; this error behavior is unverified on the console. Mutexes do not apply the protocol or ceiling: a priority-inherit or priority-protect mutex locks like a normal one and does not change its owner's priority +- [sceCoredumpSetUserDataType](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `Uxqkdta7wEg`) - `(int type)` follows the PS5 12.00 `libkernel.sprx` at 0x138B0: the sign-extended `type` goes to syscall 601 (command 29, third argument 0), and the raw result comes back with no SCE code mapping, so a kernel error is -1 with `errno` set. Which types the kernel accepts is not measured. No dump is produced here, so every type returns 0. Palworld (PPSA23126, #2598) and Hello Neighbor (PPSA35154, discussion #6991) import it; #4144 left it throwing because no signature was known - [sceKernelGetMainSocId and sceKernelGetCpumode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) both return 0, and [sceKernelGetCpuFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) returns the host TSC frequency measured for `sceKernelGetTscFrequency`, not the console CPU clock. The PS5's SoC id and CPU mode are unknown; shadPS4's PS4 values are [0x710f10/0x740f30 and 0, 2 or 5 from the param.sfo CPU-mode flags](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1a/src/core/libraries/kernel/process.cpp). PPSA99203 only logs them (`[console] main SoC id: result=%#x out=%#x`, `CPU: %llu Hz, cpumode %d`), passing a pointer that `sceKernelGetMainSocId` leaves untouched - [sceKernelEnableDmemAliasing](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) returns 0 and changes nothing: direct memory can always be mapped more than once here, as `DoMapDirect` in [DirectMemory.cpp](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) only checks that the physical pages are allocated, so there is no aliasing to turn on. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L39-L43) (PS4) sets a flag there that lifts the `EBUSY` its [`sceKernelMapDirectMemory2`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L299-L301), and [`sceKernelMapDirectMemory` for SDK 2.50 and later](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L237-L242), return for direct memory that is [already mapped](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/memory.cpp#L548-L553); that check is not applied here. The no-argument `int` signature follows shadPS4 and ProsperoEden (PPSA99008), which calls it before mapping the same direct memory again. Whether the PS5 refuses such a mapping without the call is unverified -- [sceKernelInternalMemoryGetAvailableSize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 0 bytes and returns 0: what the console's internal memory pool holds and how large it is are unknown. The `int (size_t*)` signature is from [xUtilty](https://github.com/Backporter/xUtilty/blob/328a856af1ef/source/SystemWrapper.cpp#L85) (PS4). PPSA99203 calls it once beside `sceKernelAvailableFlexibleMemorySize` while starting its GS renderer and never reads the result ([GS.cpp](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34/pcsx2/GS/GS.cpp#L233-L247)) -- [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) (libkernel, NID `LzoM-wVLJDE`) returns 0, as `sceKernelIsAddressSanitizerEnabled` does: a converted title never runs under the thread sanitizer, so a title that checks it skips the [libSceDbgThreadSanitizer](../../../core/libs/prx/libSceDbgThreadSanitizer/Export.cpp) annotation calls, which throw. PPSA23226's bundled `libSceJobManager_nosubmission.prx` makes those calls only when it reports nonzero +- [sceKernelInternalMemoryGetAvailableSize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - internal pool allocation and statistics are not implemented, so a non-null query throws without changing its output. Native null output returns -1; an absent pool returns -2, a failed statistics query returns -3, and success writes total pool bytes minus used bytes. +- [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) - the native zero-argument query returns bit 3 of the process sanitizer flags loaded from sysctl `{1, 14, 41}`. The host always reports disabled because guest sanitizer activation is not modeled. ### Unknown function info +- [sceCoredumpRegisterCoredumpHandler, sceCoredumpUnregisterCoredumpHandler](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) retain registration metadata but do not create the native report thread or deliver callbacks. Native registration-state precedence, stack bounds and SDK capture are recovered; thread-creation failures and active-report synchronization remain unimplemented. + +- [sceKernelError](../../../core/libs/prx/libkernel/System/src/Error.cpp) follows [shadPS4's kernel implementation](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/kernel/kernel.cpp): zero returns zero; other guest POSIX error values receive the `0x80020000` bias without modifying errno. Arithmetic is defined modulo 32 bits to avoid signed overflow for inputs outside the errno domain. Console behavior for those inputs and PS5 equivalence have not been measured. +- [physhm_open, physhm_unlink](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel, NIDs `n371J5cP+uo` and `AUqJNkobQ1c`) - declared as throwing stubs: the PS5 physical shared memory API is not documented, and [shadPS4's module generator](https://github.com/shadps4-emu/moduleGenerator/blob/main/libraries/kernel.cpp) also declares both with no arguments and returns `ORBIS_OK`, so the signature, the handle a success would return and the console's behaviour are unmeasured. The no-argument `int` signature follows shadPS4. Forever Launcher (PPSA99030) imports both and stops at load on `physhm_open`, and the relinker resolves no call site for either in that title, so no handle is fabricated and both throw - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. -- [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` -- [hw.realmem](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_HW`, 12} and `sysctlbyname` report the same 8-byte value as `hw.physmem` (the `_SC_PHYS_PAGES` pages of 16 KiB), read-only with `EPERM` for a write and `ENOMEM` for a short buffer, as FreeBSD's [`sysctl_hw_realmem`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_mib.c#L191-L204) does. FreeBSD reports there the memory the firmware describes, which is at least `hw.physmem`; the console's values are not known. Dolphin reads it as the machine's memory size in [`MemPhysical`](https://github.com/dolphin-emu/dolphin/blob/8d2ad1a662472955497e5bb79d7b9ec6999e1f0d/Source/Core/Common/MemoryUtil.cpp#L253-L273) (RetroArch PPSA99169's `dolphin_libretro` core) -- [vm.overcommit](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the 4-byte value 0, the default FreeBSD gives its [`vm.overcommit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/swap_pager.c#L158-L161) `int`, with `ENOMEM` for a short buffer; the console's value is not known and a write throws. mimalloc reads it in [`unix_detect_overcommit`](https://github.com/microsoft/mimalloc/blob/31d034d94cdb8e22f7d7ed55967f581a2d6e831d/src/prim/unix/prim.c#L129-L153) and takes 0 as no overcommit, as PS5MC (PPSA99420) does at start-up +- [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` draws a new block of 64 bytes from the host's `std::random_device` for each read (no new value) that passes a buffer and a length, and sizes its answers as a payload process on a PS5 (firmware 12.70) sees it: a size query reports 64; a buffer of 64 bytes or more gets 64 bytes and a length of 64 (tried up to 65536); a smaller buffer gets as many bytes as its length, which is left as given, and a null length gets none, both with -1 and `ENOMEM`; a null buffer with a null length returns 0. `errno` is kept when the call succeeds. A write returns -1 with `EPERM`, which was not tried on the console. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator is unknown, and a title process was not measured. On the console `sysctlnametomib` gives MIB {1, 50} for the name, and `sysctl` with it answers reads of 0, 1, 63, 64, 65 and 128 bytes, a size query, a null length and a null buffer with a null length as the name does; here `sysctlnametomib` maps the name to {1, 50} and `sysctl` {1, 50} answers as the name does; a write through the MIB was not measured and throws +- [sysconf](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `_SC_CLK_TCK` returns 128, FreeBSD's `CLK_TCK` as its [`sysconf`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L100-L101) returns it ([`time.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/include/time.h#L48-L54)); the console's value is not checked, and no `times` reports ticks at that rate. CPython's `posix` module stops its import with "cannot read ticks_per_second" when the value is below 1, as the Python in PS5MC (PPSA99420) does. `_SC_ARG_MAX` returns 262144 (FreeBSD's `NCARGS`), `_SC_OPEN_MAX` returns the `RLIMIT_NOFILE` soft limit via `getrlimit` ([`sysconf.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L106-L115)), and `_SC_IOV_MAX` returns 1024 (FreeBSD's `UIO_MAXIOV`). Names other than `_SC_ARG_MAX`, `_SC_CLK_TCK`, `_SC_OPEN_MAX`, `_SC_PAGESIZE`, `_SC_IOV_MAX`, `_SC_NPROCESSORS_CONF`, `_SC_NPROCESSORS_ONLN` and `_SC_PHYS_PAGES` fail with `EINVAL` +- [hw.realmem](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_HW`, 12} and `sysctlbyname` report the same 8-byte value as `hw.physmem` (the `_SC_PHYS_PAGES` pages of 16 KiB), read-only with `EPERM` for a write and `ENOMEM` for a short buffer, as FreeBSD's [`sysctl_hw_realmem`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_mib.c#L191-L204) does. FreeBSD reports there the memory the firmware describes, which is at least `hw.physmem`. A payload process on a PS5 (firmware 12.70) gets -1 with `ENOENT` for `hw.physmem` and `hw.realmem` (a read, a size query and a null length) and for {`CTL_HW`, 5} and {`CTL_HW`, 12} (a read and a size query); what a title process gets was not measured. Dolphin reads it as the machine's memory size in [`MemPhysical`](https://github.com/dolphin-emu/dolphin/blob/8d2ad1a662472955497e5bb79d7b9ec6999e1f0d/Source/Core/Common/MemoryUtil.cpp#L253-L273) (RetroArch PPSA99169's `dolphin_libretro` core) +- [vm.overcommit](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the 4-byte value 0, the default FreeBSD gives its [`vm.overcommit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/swap_pager.c#L158-L161) `int`, with `ENOMEM` for a short buffer, and a write throws. A payload process on a PS5 (firmware 12.70) gets -1 with `ENOENT` for it, for a read, a size query and a null length; what a title process gets was not measured. mimalloc reads it in [`unix_detect_overcommit`](https://github.com/microsoft/mimalloc/blob/31d034d94cdb8e22f7d7ed55967f581a2d6e831d/src/prim/unix/prim.c#L129-L153) and takes 0 as no overcommit, as PS5MC (PPSA99420) does at start-up - [kern.sdk_version](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the system software version as the 4-byte value `0x01000000` (1.00), the value `sceKernelGetSystemSwVersion` reports; a write returns -1 with `EPERM`. The major version is byte 3 and the minor version byte 2, in hex digits that read as decimal (13.60 is `0x1360xxxx`), as [Capy Mobile](https://github.com/khalifa007/CapyMobile/blob/2758c016/src/ps5/platform.c#L64-L71) (PPSA92280), [remote_lua_loader](https://github.com/n0llptr/remote_lua_loader/blob/7c2840e475/savedata/misc.lua#L195-L209) and the [lapse payload](https://github.com/NetflixNHack/Netflix-N-Hack/blob/605eb93ba5/PS4/payloads/lapse.js#L436-L450) read it. Capy Mobile only displays it. The console's firmware version is not modeled and the low two bytes are unknown +- [kern.ostype, kern.osrelease, kern.version, kern.hostname, hw.machine](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` (1.1, 1.2, 1.4, 1.10, 6.1), `sysctlbyname` and `sysctlnametomib` serve the names `uname` reads as read-only strings: "FreeBSD", "11.4-RELEASE" and "FreeBSD 11.4-RELEASE\n", the values FreeBSD 11.4-RELEASE builds without source control information ([newvers.sh](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/conf/newvers.sh#L45-L52), [L293-L294](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/conf/newvers.sh#L293-L294)), the host name `gethostname` reports ("AnyPS5") and "amd64"; a short buffer returns `ENOMEM` and a write `EPERM`. The console's strings are not measured +- [kern.osreldate](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - reports MIB `{CTL_KERN, KERN_OSRELDATE}` as `1104001`, FreeBSD 11.4's `__FreeBSD_version` in `sys/sys/param.h`; this is consistent with `kern.osrelease` and `kern.version` above, but the value reported by a PS5 title has not been measured. - [sceKernelGetProsperoSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it - [sceKernelGetAllowedSdkVersionOnSystem](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports the system software version with the 12 patch bits set, `0x01000fff`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/98fd0689/src/core/libraries/kernel/kernel.cpp) does on the PS4 and as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) describes it, which Prospero Explorer (PPSA99109) reads it through; a null pointer returns `SCE_KERNEL_ERROR_EINVAL`. The console's value and its errors are unverified -- [getuid, geteuid, getgid, getegid and issetugid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) report the root identity (0) and a process that never changed credentials (`issetugid` 0), as a jailbroken process has; the host user and group are not exposed to the guest. The values a title sees on a console are unverified +- [sceKernelConvertUtcToLocaltime, sceKernelConvertLocaltimeToUtc](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) convert with the host time zone at the given instant, as libc `localtime` does, instead of the console's time zone setting. `west_sec` holds the standard-time offset in seconds east of UTC, despite its name, and `dst_sec` the daylight saving seconds in effect. This follows PPSA99203 ([PS5SX2](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34e8a2a1cd9302df4373e5a94d28f4/ps5/frontend/fe_ps5.cpp#L54-L63)), whose author saw a console write 7200 and 3600 into the 16-byte block; which zone that console was set to is not recorded, so whether `west_sec` includes daylight saving is not settled. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333/src/core/libraries/kernel/time.cpp) also stores seconds east. PPSA99203 declares the last argument of `sceKernelConvertUtcToLocaltime` as `uint64_t*`; 32 bits are written. On Windows the standard offset and the daylight saving amount are the C runtime's current `_get_timezone` and `_get_dstbias` for every instant, as libc `localtime_s` uses them, so instants under a zone's earlier rules get today's offsets; a `TZ` variable does not set `_dstbias`, which keeps the value an earlier `_tzset` read from the system (0 when the system zone has no daylight saving), and summer time then adds nothing. In a repeated or skipped local hour, `sceKernelConvertLocaltimeToUtc` takes the offset in effect one standard offset earlier. Its last argument receives the zone's daylight saving amount whether or not it is in effect, found by sampling the host zone over the year around the result, while `dst_sec` is the amount in effect: the libc `mktime` that PPSA99109 bundles (text 0x70ebb-0x70ee5) subtracts that amount when `tm_isdst` is set outside summer time. That `mktime` passes `tm_isdst` as the second argument, which is ignored, so it does not choose the offset in a repeated hour. `sceKernelGettimezone` and the `gettimeofday` time zone still report no offset and no daylight saving. A host time the C runtime cannot convert throws +- [getuid, geteuid, getgid, getegid, getresuid, getresgid and issetugid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) report the root identity (0) and a process that never changed credentials (`issetugid` 0), as a jailbroken process has; the host user and group are not exposed to the guest. `getresuid` and `getresgid` follow FreeBSD 11.4's `sys_getresuid` and `sys_getresgid`, writing 0 to non-null pointers and leaving errno untouched on success. The values a title sees on a console are unverified - [sceKernelGetAppInfo](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - only `app_id` and the title ID (offset 0x10, up to 9 characters plus a terminator) are written; callers declare the structure with different sizes, so the rest is left alone. `app_id` is 1, the running title's id in libSceSystemService, which is a guess, not something measured. The error codes follow shadPS4: `EPERM` for another process, and 0 without writing anything for a null pointer. - [AMPR object layout and compact gathers](../../../core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp) - the 24-byte object and offsets (type 0, cursor 4, count 8, capacity 12, pointer 16) follow [ampr_emu assertions](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_emu_command_buffer_types.h#L138-L143) and its [C declaration](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr.h#L41-L47). AnyPS5 represents the 32-bit type word as low 16-bit type and high 16-bit recording flags. Command encoding is AnyPS5 internal: opcode 29 and three 32-bit words encode the high offset byte, length minus one and low offset word. The 12-byte size reduces HFW's 1 MiB buffer pressure; it is not a claim about console opcode encoding. Existing 40-bit offsets and lengths 1 through 2^32 are retained; console behavior outside those limits is unverified. - [APR resolve ForEach result](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - `sceKernelAprResolveFilepathsToIdsAndFileSizesForEach` returns the number of paths that resolved, with the per-path status in `results` and the failed ids/sizes set to the invalid id and 0. The [ampr_emu](https://github.com/drakmor/ampr_emu) bridge passes the count of its resolver through and does not show what is counted; counting successes is not confirmed on a title. The guest errno of the failing non-`ForEach` resolves is set through `__error_nid_postfix` - [APR reads past the end of a file](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - a read that reaches the current end of the file on disk copies the bytes the file has and fills the rest of the destination with zeros; a read that comes back short before that end still throws. PPSA18605 resolves its settings file once, rewrites it one byte shorter and reads it again with the size that resolve returned. The console's behaviour for a read past the end of a file is unverified: it may zero-fill, leave the destination untouched or fail the command - [AMPR counter access](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and checks of `WaitOnCounter_04_00`, `WriteCounter_04_00`, `ConstructNop` and `ConstructMarker` taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands counter commands to the console and does not show their effect. The counters are taken as 32 bits each: the 8-byte access of counter n spans n (low) and n + 1 (high), the 2- and 1-byte accesses select a half or a byte of the counter by its offset, waits compare at the access width (signed and wrapped compares included), the AND mask applies to both sides and the atomic writes wrap within the access. The `ConstructNop` type is not recorded - [APR gather and scatter reads](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, argument checks and the read cursor taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: each read leaves the file, the next file offset and the next destination; a gather reads that file at a new offset into the next destination, a scatter continues in the file into a new destination, a gather-scatter takes both. Recording one needs a read file since the last `ResetGatherScatterState`; like ampr_emu, `sceAmprCommandBufferReset` keeps that state, and a gather or scatter with no read before it in the submitted buffer throws instead of returning `SCE_AMPR_ERROR_APR_INVALIDGATHERSCATTERSTATE` -- [AMM map, map direct and unmap](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection mask (CPU, GPU, AMPR and ACP read and write) taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands the remaining checks and the mapping to the console kernel; the mapper model taken from [PS5PCEM](https://github.com/iStark/PS5PCEM): `sceAmprAmmGiveDirectMemory` usage 1 gives the allocated direct memory to the pool plain maps take pages from, usage 0 only allocates it. Not confirmed on a title. The AMM virtual address ranges are a 32 GiB range and a 32 GiB multimap range reserved on first use at the top of the guest arena, above the addresses that maps without `MAP_FIXED` are given lowest first and that titles derive their next hints from; the console's addresses are not known (ampr_emu takes them from `sceKernelMapperGetParam`), and a reservation that cannot be made there throws. Maps outside them throw. Addresses, sizes and direct offsets must be 16 KiB aligned. The memory type and the GPU mask are ignored, AMPR and ACP access maps as CPU access, unmapped pages go back to the pool and stay reserved. Submission runs the buffer before returning, the submit result is always 0 and a failing command throws; the AMM measures return errors sign-extended, as ampr_emu does. A map returns its target to the reservation before releasing the old pages, so pool exhaustion leaves that target reserved; mapping failure also restores the reservation before returning newly taken pages to the pool. The console's map failure and rollback behavior is unverified. `sceAmprAmmGetVirtualAddressRanges` throws for a null output +- [AMM map, map direct and unmap](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection mask (CPU, GPU, AMPR and ACP read and write) taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands the remaining checks and the mapping to the console kernel; the mapper model taken from [PS5PCEM](https://github.com/iStark/PS5PCEM): `sceAmprAmmGiveDirectMemory` usage 1 gives the allocated direct memory to the pool plain maps take pages from, usage 0 only allocates it. Not confirmed on a title. The AMM virtual address ranges are a 256 GiB range (PPSA02015 places its heaps 64 GiB above the start of the range, so 32 GiB is too small, and reached 84 GiB above it in its first 159 s; KytyPS5 reports 512 GiB, but every 64 GiB-aligned 544 GiB window in the guest arena covers 448 to 608 GiB, and on Windows one host thread stack there is enough to block it) and a 32 GiB multimap range reserved on first use at the top of the guest arena with the start on a 64 GiB boundary (PPSA02015 tells its pool pointers from its heap pointers by address bits 28 to 35, which needs such a start for its layout), above the addresses that maps without `MAP_FIXED` are given lowest first and that titles derive their next hints from; the console's addresses are not known (ampr_emu takes them from `sceKernelMapperGetParam`), and a reservation that cannot be made there throws. Maps outside them throw. Addresses, sizes and direct offsets must be 16 KiB aligned. The memory type and the GPU mask are ignored, AMPR and ACP access maps as CPU access, unmapped pages go back to the pool and stay reserved. Submission runs the buffer before returning, the submit result is always 0 and a failing command throws; the AMM measures return errors sign-extended, as ampr_emu does. A map returns its target to the reservation before releasing the old pages, so pool exhaustion leaves that target reserved; mapping failure also restores the reservation before returning newly taken pages to the pool. The console's map failure and rollback behavior is unverified. `sceAmprAmmGetVirtualAddressRanges` throws for a null output - [AMM remap, multimap and protection changes](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection and mask checks taken from ampr_emu; the effects are inferred from the names, not confirmed on a title. A remap moves every page of a fully mapped range to the new address with the given protection and leaves the old range reserved; a multimap maps the same pages at a second address, anywhere in the AMM ranges, and a pool page goes back to the pool once its last mapping is unmapped; a multimap onto its own source range throws. A protection change sets the masked bits on every page of a fully mapped range; the memory type is recorded and ignored. A command on a range that is not fully mapped throws - [AMM PRT ranges](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, checks and their order taken from ampr_emu, where `MapAsPrt` is a map with the PRT flag and protection 0 and `AllocatePaForPrt` a memory type and protection change with mask 1019, both checking the buffer memory after its size; the effects are inferred from the names, not confirmed on a title. Unbacked PRT pages are zero-filled pages with GPU read access, so CPU reads see zeros instead of faulting and GPU writes fault instead of being dropped. `AllocatePaForPrt` backs each unbacked page with a pool page and sets the type and protection of backed ones; `RemapIntoPrt` moves a fully mapped range into a PRT range, its opcode argument (1011 when 0, as in ampr_emu) is recorded and ignored; `UnmapToPrt` returns the pages to the pool and zero-fills them again. A plain map, unmap or remap over a PRT range ends it; `AllocatePaForPrt`, `RemapIntoPrt` or `UnmapToPrt` outside a PRT range throws - [AMM kernel write commands](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - signatures of `sceKernelWriteModifyMtypeProtectCommand` and `sceKernelWriteRemapIntoPrtCommand` taken from [ampr_emu's libkernel hook targets](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_libkernel_hook_targets.h), which forward to the real kernel; the measure mode (`dst` null reports the size, `outSize` null skips the report) follows ampr_emu's measure-then-record calls, not confirmed on a title. The range, protection and mask checks mirror the `libSceAmpr` siblings, and the recorded bytes match them exactly so the submitted buffer executes on the existing paths -- [sceCoredumpWriteUserString](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) - 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 @@ -43,35 +53,45 @@ - [sceKernelGetLibkernelTextLocation](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(void** address, std::size_t* size)` signature and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L338-L348); whether the console reports that segment, the text of libkernel itself or the calling module's is unverified. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null argument throws. - [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - unmaps the range like sceKernelMunmap, as fpPS4 and prosper do; whether the console keeps the virtual range reserved after releasing its pages is unverified - [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) +- [sceKernelAvailableDirectMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - how the console handles invalid arguments is not measured. A negative `search_start`, a `search_end` at or below `search_start` and an alignment that overflows when rounded up to 16 KiB return `EAGAIN` (`0x80020023`); bounds that are not multiples of 16 KiB are rounded inward, the alignment is raised to a multiple of 16 KiB and need not be a power of two, and a `search_end` past the pool is cut to it. fpPS4's HLE wrapper on its trunk branch refuses all of these with `EINVAL` ([ps4_map_mm.pas](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_map_mm.pas#L558-L570)) - [Memory protection bits 0x100 and 0x200](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits +- [Protecting a host thread's stack](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - on Windows `sceKernelIsStack` reports a host thread's whole stack reservation, which Windows commits on demand from the top down behind a guard page. A protect over that range changes only the pages above the guard; the guard page and the reserved pages below it are left as they are, so the stack still grows and an overflow still raises a stack overflow. A protect that starts at the bottom of the reservation and stops short of the guard page, as HotSpot makes its stack guard zones, is committed and protected. Windows moves the guard page down by committing the pages below it as read/write guard pages, whether they were reserved or committed, so those pages come in read/write as the stack grows to within one guard region of them (three pages on GitHub's runners). A protection other than read/write on them, `PROT_NONE` included, does not hold once the stack is that close, where on the console every page of the reported range takes the protection. Holding it would mean removing the thread's guard page and keeping the stack limit in its TEB by hand. On Linux the reported range of the main thread is mostly unmapped; the unmapped pages from the bottom of a protect up to the top of the stack are mapped read/write in place, then the protect is applied, so the stack never has to grow down to the protected pages, which Linux refuses within its stack guard gap (1 MiB by default) above a readable mapping. The pages below a protect stay unmapped and out of the stack's reach, where on the console the stack goes on below them - [Foreign guest module image protection on Windows](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMprotect` on Windows allows protection mutations on mapped PE images containing a `.elf0` section (as relinked guest modules produce, such as NVIDIA Real-time Denoisers `libNRD.prx` bundled in Dying Light 2: Stay Human PPSA02262), registering their sections into `GuestAllocations`. Host libraries and system DLLs (such as `libkernel.prx` and `kernel32.dll`) without a `.elf0` section continue to be rejected -- [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE` and `UMTX_OP_NWAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. `UMTX_OP_NWAKE_PRIVATE` reads the value as an `int` count of pointers at the object and wakes every waiter on each one; a count of 0 or fewer reads nothing, and a null list fails with `EFAULT`, where FreeBSD's `copyin` would also fail for any other unreadable list. Waiters share the queue of `sceKernelSyncOnAddressWait`. Operation 0 fails with `EOPNOTSUPP` and operations above 26 with `EINVAL`, as in FreeBSD 11; `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. Operation 1 throws, since PPSA12544's call_once uses it in a way FreeBSD rejects; the other operations throw, and so does a misaligned object. PPSA12544's `libc.prx` 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) - the mutex, condition variable, read-write lock, legacy lock and `SEM2` operations throw, and so does a misaligned object. Implemented as FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE`, `UMTX_OP_NWAKE_PRIVATE`, `UMTX_OP_SEM_WAIT` and `UMTX_OP_SEM_WAKE` (the last two built under `COMPAT_FREEBSD9`/`10`). Operations 0 and 1 fail with `EOPNOTSUPP`, as measured on a PS5 (firmware 12.70). The `SEM2` operations, 23 and 24, were measured on the console and returned `0` for a count of 0 where FreeBSD would time out, so they stay unimplemented. - [sceKernelSyncOnAddressWait](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded -- [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware +- [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console +- [sceKernelSetEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - multi-waiter satisfaction follows fpPS4 in-order evaluation: registered waiters are evaluated in FIFO order and each satisfied waiter's clear mode is applied immediately to the pattern before evaluating subsequent waiters; this ordering is unverified on PS5 console hardware +- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - returns the handle of an undeleted event flag this process created with the same name, and `SCE_KERNEL_ERROR_ENOENT` otherwise. Flags owned by other processes, such as the shell's `SceShellCoreUtilAppFocus`, do not exist here, and which attribute makes a flag openable on the console is not modelled. The error code for a missing name and the choice between flags sharing a name are unverified. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written -- [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs +- [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked +- [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the returned scheduling policy is unmeasured on a console. Both report the recorded guest policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164) +- [pthread_getprio](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) is unmeasured on the console. NID `aO+WS2xWuo4` is named in the firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libkernel.c). Its return and errno contract follows FreeBSD 11 [thr_getprio.c](https://github.com/freebsd/freebsd-src/blob/671fb7df97631020c42ce55527dc49fffe0656b7/lib/libthr/thread/thr_getprio.c): return the recorded guest priority, or -1 with `ESRCH` for an unknown or finished thread. The live-thread registry lock protects the read from a concurrent join. This reports guest scheduler state, not the host scheduling priority. +- [pthread_rwlock_reltimedrdlock_np, pthread_rwlock_reltimedwrlock_np](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) take `(rwlock, usec)`, a relative timeout in microseconds, and are `scePthreadRwlockTimedrdlock` and `scePthreadRwlockTimedwrlock` with POSIX error numbers, so they return what those return (`ETIMEDOUT` when the time runs out, at once for 0 while another thread holds the lock, and `EINVAL` for a destroyed lock); a zero lock is initialized as by `pthread_rwlock_rdlock`. FreeBSD 11.4 libthr has no such functions; the signature comes only from shadPS4, which registers one function for each NID and its `scePthreadRwlockTimed*` twin ([`rwlock.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/rwlock.cpp#L167-L179), 32-bit `usec`). Unverified on the console - [sceKernelGetAvailableCpumask](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed - [sceKernelGetCurrentCpu](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - guest affinities are not applied to host threads, so the host CPU index is mapped into the calling thread's affinity clipped to `sceKernelGetAvailableCpumask` (the available mask when they do not overlap): host CPU n reports the (n modulo the CPU count)-th CPU of that mask, so a thread pinned to one CPU reports that CPU, as a FreeBSD `cpuset` guarantees on the console. Which CPU the console reports for a thread allowed on several is not known; shadPS4 returns 0 - [pthread_attr_setsolosched_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) takes `(attr, int)` and stores the value through `scePthreadAttrSetsolosched`, returning `EINVAL` (22) for a null or uninitialized attribute like the other POSIX attribute setters. Only the name is public (aerolib `2+pVfgiEd7A`); the signature and behaviour follow the SCE twin, which [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/pthread.cpp#L2188-L2199) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs#L891-L913) store as an `int` without a range check, and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/71de7681f06c5db59a1120a3bb554c86780c3f5f/src/hle/libs/kernel_threading.zig#L1396-L1398) implements this alias over it. On a PS5 (firmware 13.60) only 0 and 16 are accepted; every other value returns `ENOTSUP` (45, `0x8002002D` from the SCE call) and keeps the stored one, and the SCE calls return `EINVAL` for a null or uninitialized attribute. PPSA15878 passes 16. Threads created from the attribute are not scheduled differently; what solo scheduling does on the console is unknown. Returnal (PPSA01285 v03.000.005) stopped on it with `what(): pthread_attr_setsolosched_np_nid_postfix not implemented` (#2872, #1155); LEGO Horizon Adventures (PPSA14632, #734) and Palworld (PPSA23126, #2598) import it -- [sceKernelMapFlexibleMemory / sceKernelMapNamedFlexibleMemoryInternal](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 is accepted and ignored; its meaning is not established. KytyPS5 (`GUEST_MAP_UNKNOWN_8000`) and sharpemu accept it without reading it. Every known caller is an SDK 9 `libc.prx` heap, which passes it with read/write protection and uses the mapping as an older `libc.prx` uses `sceKernelMapNamedFlexibleMemory` with flags 0: through the Internal variant in Astro Bot PPSA21564, through `sceKernelMapFlexibleMemory` in Animal Well PPSA22520 01.000.011 +- [sceKernelMapFlexibleMemory / sceKernelMapNamedFlexibleMemoryInternal](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 is accepted and ignored; its meaning is not established. KytyPS5 (`GUEST_MAP_UNKNOWN_8000`) and sharpemu accept it without reading it. Every known caller is an SDK 9 `libc.prx` heap, which passes it with read/write protection and uses the mapping as an older `libc.prx` uses `sceKernelMapNamedFlexibleMemory` with flags 0: through the Internal variant in PPSA21564, through `sceKernelMapFlexibleMemory` in PPSA22520 01.000.011 - [sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - sets the memory type of the direct mappings in the page-rounded range and of the direct memory behind them, as shadPS4 and prosper do (KytyPS5 ignores the type); the type is not validated, flexible pages in the range only get the protection, and a later map of the retyped direct memory reports the type it was allocated with. `sceKernelBatchMap2` operation 4 takes the same path with the entry type - [Map hints inside a reservation](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMapDirectMemory` and the flexible and anonymous maps place a mapping without `MAP_FIXED` at its hint address when the whole range lies in a range still reserved by `sceKernelReserveVirtualRange`, as if `MAP_FIXED` were set. Taken from PPSA01474, which reserves 64 GiB and then maps its heaps inside it with hints and no `MAP_FIXED`, and uses the hint addresses afterwards; not measured on the console. Hints outside a reservation, or over a range that is partly mapped, stay hints +- [Fixed anonymous mmap](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `MAP_PRIVATE | MAP_ANON | MAP_FIXED` replaces the requested range through the existing guest mapping machinery and zeroes the new memory. `MAP_EXCL` is checked under the allocation-registry mutation lock, including virtual reservations, and returns `ENOMEM` without changing an occupied range; without `MAP_FIXED` it is `EINVAL`. Lengths are rounded to 16 KiB and fixed addresses must be aligned. The complete rounded range must lie inside the guest arena or, for POSIX fixed anonymous `mmap` on Windows, one current or registered native thread stack; other mapping APIs do not opt into stack replacement. Native stack replacement keeps the reservation, decommits and recommits fresh pages, and rejects foreign allocations, ranges spanning reservations and host `PAGE_GUARD` pages before mutation. Current-stack ranges must end at least 64 KiB below the active host frame; callers replacing another thread's stack must coordinate with that thread. If recommitting fails, the call returns `ENOMEM` with the range still reserved; thread cleanup attempts to recommit it before restoring write access. This follows FreeBSD 11.4 [argument validation and rounding](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L226-L299), [fixed replacement](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1455-L1464), and [collision errno](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1604-L1613). The console behaviour has not been measured. AnyPS5 starts the guest arena at 0x200000000, so the fixed path rejects the low 0x400000 address requested by PICO-8 PPSA99808. The console address-space layout is unmeasured; executable placement outside the host arena is tracked in #5217. Static catalog disassembly finds `0x1012` in PPSA98001/PPSA99808 and `0x5012` in PPSA99808; these titles were not run. +- [Mapping hints with no room above them](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a mapping without `MAP_FIXED` whose hint lies above the guest address space, or has no free range at or above it, is placed from the bottom of the guest arena, as FreeBSD 11.4's [`vm_mmap_object`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1569) retries through [`vm_map_find_min`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1555-L1584) from its minimum address. FreeBSD's minimum is the end of the largest possible data segment rather than the start of the arena, and the console's address-space layout is not measured. PS5CEMU-HAR (PPSA99360) gives `sceKernelReserveVirtualRange` a hint near its own code, which is a host address here because the relinked executable lies outside the guest arena, so its JIT gets memory more than 2 GiB from its code and does not start - [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match - [tcgetattr, tcsetattr](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) - every file and pipe descriptor is taken as not a terminal (`ENOTTY`), and a socket gets `EADDRNOTAVAIL` for `AF_INET` and `EOPNOTSUPP` otherwise, as FreeBSD's `in_control` and `pru_control_notsupp` answer a termios ioctl; what descriptors 0 to 2 of a title are on the console, and whether any of them is a terminal there, is unknown -- [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 +- [cfsetspeed](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel, NID `C1MQ6OaXRjo`) - console behavior and module/library versions are unverified. The prototype, uint32 speed and 44-byte termios layout (input speed at 36, output speed at 40) follow the public PS5 payload SDK [termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/termios.h) and [sys/_termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/sys/_termios.h). The module association follows its libkernel stub table and the NID rehashes to the aerolib name. Both speed fields accept every uint32 value and no flags or control characters change, as [FreeBSD 11.4 termios.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/gen/termios.c) does. No terminal I/O is performed. Null pointers throw where the reference dereferences them. +- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` follow `randomdev_poll` for a seeded device under [`selscan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1178-L1236), which asks once with the flags of every set the descriptor is in: in the read set they are readable and reported in no other set, and otherwise they are reported in the write and exception sets they were asked in. A regular file, a directory or a file read from app0.zar is always readable and writable and never in the exception set, as `vop_stdpoll` answers, on every host. Guest sockets and libSceNet sockets are polled. A kqueue is readable while one of its events is triggered and is never writable or in the exception set, as FreeBSD 11.4's [`kqueue_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c#L1900-L1924) answers, on every host. Other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket +- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing (on Windows only a pipe, as above); an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. A descriptor that stops being a socket, a kqueue or a pipe during the wait fails the call with `EBADF` when it is closed and is selected again with the rest of the timeout when its number was reused, as FreeBSD 11.4's [`kern_select`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1129-L1140) looks every descriptor up again in each pass. A close that has already removed the descriptor from the guest's kqueue, pipe or file tables but not yet closed its host descriptor is seen by that new look-up as a descriptor of unknown kind, which throws on Windows and is polled through the host on Linux. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles - [__progname](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [environ](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it -- [seteuid, setegid, setuid, setreuid, setregid, setgroups, getgroups](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid`, `sys_setreuid` and `sys_setregid`, id 0 (or -1 for an unchanged `setreuid`/`setregid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified -- [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 +- [seteuid, setegid, setuid, setgid, setreuid, setregid, setresuid, setresgid, setgroups, getgroups](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid`, `sys_setgid`, `sys_setreuid`, `sys_setregid`, `sys_setresuid` and `sys_setresgid`, id 0 (or -1 for an unchanged `setreuid`/`setregid`/`setresuid`/`setresgid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified +- [pthread_getname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a thread that has already been joined returns `ESRCH` (3) and its output buffer is left untouched, as measured on a PS5 (firmware 13.60) by the author of this entry; since [#4481](https://github.com/boykopovar/AnyPS5/pull/4481) the joined thread's object is retained, so the posix wrapper answers `ESRCH` for it instead of reading the retained name. A null name buffer is answered with `EFAULT` before that, an order the console table does not measure. The SCE sibling `scePthreadGetname` has no measured joined-thread behaviour on the console and still reads the retained object's name, so it is unchanged, as is the pre-existing dangling case of a thread that ended detached (its object is released at exit; see the scePthreadJoin entry) - [sceKernelGetOperationMode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call and ignores the result. A homebrew payload on a PS5 (firmware 13.60) reads mode 2 and submode 0, nothing else is written, and the result register holds no status, so the function is taken to return nothing. Whether a title process reads the same values is unverified. Null outputs throw -- [sceCoredumpGetStopInfoCpu](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) takes `(void* info, size_t size)` as in the [Orbital type database](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json); the 16-byte, 8-byte-aligned block follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which places coredump reads inside the handler. The handler is never invoked here, so a well-formed request returns `NOT_IN_COREDUMP_HANDLER` without reading or writing the block. Its value `0x81180003` follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h) and the NHL 27 (PPSA34063) call site reported in [#4275](https://github.com/boykopovar/AnyPS5/discussions/4275), not a PS5 measurement. The console validation order is unverified: null, unaligned or wrapping buffers and sizes other than 16 throw instead of guessing an argument error. Information from an active coredump handler remains unsupported -- [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 +- [sceCoredumpGetStopInfoCpu](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) has no active coredump context. Native null/size validation is recovered, including both 8-byte and 16-byte records; valid requests return `NOT_IN_COREDUMP_HANDLER` without accessing the output. Active-handler data retrieval remains unimplemented. +- [sceCoredumpGetThreadContextInfo](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) validates a nonnull 144-byte output using the native checks. Active-handler register capture remains unimplemented; valid requests return `NOT_IN_COREDUMP_HANDLER` without accessing the output. +- [sceCoredumpAttachMemoryRegion, sceCoredumpAttachUserMemoryFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) cannot attach memory to an active coredump. The native signed 32-bit length is recovered: negative lengths are ignored successfully; other calls return the outside-handler error without accessing memory. Capture and kernel-side record limits remain unimplemented. +- [sceCoredumpWriteUserData, sceCoredumpDebugTextOut](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) have no active coredump report. WriteUserData validates a nonnull buffer and size at most `INT32_MAX`, including zero; accepted requests return the sign-extended outside-handler error. DebugTextOut still prints to stderr instead of a report; its validation and report integration remain unverified. +- [sceCoredumpAttachUserFile, sceCoredumpAttachMemoryRegionAsUserFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) cannot write files to an active coredump. Native null-name checks, the signed 32-bit memory length and SDK 6.50 flag validation are recovered. Nonnegative requests return the outside-handler error; negative memory lengths are ignored after name/flag validation. Kernel-side path handling and capture limits remain unimplemented. - [sceKernelAddTimerEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - `usec` is taken as the period in microseconds; re-adding an existing timer keeps its pending expiration count and restarts the period from the time of the call, where FreeBSD 12+ `filt_timertouch` clears pending expirations on `EV_ADD`; `usec == 0` fires on every wait. None of this is confirmed on a PS5 title - [sceKernelWaitEqueue](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - a level-triggered user event (`sceKernelAddUserEvent`) is reported at most once per call, as in FreeBSD `kqueue_scan` (`sys/kern/kern_event.c`), which re-queues a reported knote without `EV_CLEAR` or `EV_DISPATCH` behind the scan marker, and kevent(2), where several triggers do not produce several kevents. Not measured on a console. Events are scanned in list order and the list is not rotated, so when `num` is smaller than the number of ready level events the later ones can starve, where FreeBSD moves a reported level knote to the tail. Several completions of one AMPR or VideoOut event within one call are returned as separate entries, where FreeBSD would aggregate them; the Orbis behaviour is unconfirmed - [AC1FtjqMCL0](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature @@ -93,94 +113,143 @@ - [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 of explicit scheduling attributes, or its creator's when it inherits. When it inherits, it still takes the priority of its attributes, where the console gives it its creator's priority -- [inet_pton, inet_ntop](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - the text forms are assumed to be those of FreeBSD 11.4 and are unverified on a console: `inet_pton` accepts only four decimal parts without leading zeros for `AF_INET` and the RFC 4291 forms for `AF_INET6` (one `::` standing for at least one group, a dotted-quad tail only as the last 32 bits, no zone id), and `inet_ntop` prints lowercase hex with the first longest run of two or more zero groups compressed, and a dotted tail only for `::a.b.c.d` and `::ffff:a.b.c.d`. They no longer depend on the host resolver library -- [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) (libkernel, NID `LzoM-wVLJDE`) - no source gives the signature; `int (void)` is assumed from `sceKernelIsAddressSanitizerEnabled` and from PPSA23226's bundled `libSceJobManager_nosubmission.prx`, which passes no arguments and tests only the 32-bit result -- [getdtablesize](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - throws: FreeBSD returns the smaller of the `RLIMIT_NOFILE` soft limit and `maxfilesperproc` ([`sys_getdtablesize`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L338-L355)), and neither value is known for the console, where `getrlimit(RLIMIT_NOFILE)` throws too. VLC (PPSA85300) imports it for a `closefrom` loop in the child of its spawn helper ([libc_stubs.c](https://github.com/ZiZc3/VLC/blob/d46be6a68f4244466b041c2dc1b640915ac1a983/ps5/compat/libc_stubs.c#L66-L72)), which never runs because its `fork` fails with `ENOSYS` +- [getdtablesize](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - returns the smaller of the `RLIMIT_NOFILE` soft limit of `getrlimit` and the host's `getdtablesize()`, capped at `INT_MAX`: FreeBSD returns the smaller of the `RLIMIT_NOFILE` soft limit and `maxfilesperproc` ([`sys_getdtablesize`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L338-L355)), and the host call supplies that bound on macOS, where the soft limit alone is not one (`setrlimit` accepts soft limits above `maxfilesperproc`, `RLIM_INFINITY` included); `maxfilesperproc` is not measured on the console. The guest limit is the host's soft limit, raised to the hard limit when libkernel loads only on Linux: `core/libs/prx/libkernel/File/src/OpenFileLimit.cpp` is `__linux__`-only, so on macOS the guest gets whatever soft limit the host process inherited. VLC (PPSA85300) imports it for a `closefrom` loop in the child of its spawn helper ([libc_stubs.c](https://github.com/ZiZc3/VLC/blob/d46be6a68f4244466b041c2dc1b640915ac1a983/ps5/compat/libc_stubs.c#L66-L72)), which never runs because its `fork` fails with `ENOSYS` +- [close, sceKernelClose](../../../core/libs/prx/libkernel/File/src/HostStreams.cpp) (libkernel) - descriptors 0 to 2 that are open when libkernel loads belong to the host process: closing one fails with `EBADF` and leaves it open. The console's result is unknown. PPSA21703 calls `sceKernelClose(0)` repeatedly; closing the host stream let the host C runtime give descriptor 0 to the next file opened, which the next call then closed +- [sceKernelPrintBacktraceWithModuleInfo](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws ### Functional -- [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors is unsupported, as for `shm_open`. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws -- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `EV_DISABLE`, `EV_DISPATCH`, `EV_FORCEONESHOT`, `NOTE_LOWAT`, `EV_CLEAR` on a socket, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro), any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) and a negative `nevents` throw. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop -- [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, 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 +- [scePthreadYield](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) on Windows calls Sleep(1) after 512 consecutive yields that find no other thread ready on the current processor and arrive less than ~10,000 TSC ticks apart (so the span varies with host CPU TSC rate); FreeBSD sched_yield never sleeps. +- [Guest thread scheduling](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - the policies and priorities that `sched_get_priority_max`, `sched_get_priority_min`, `pthread_setschedparam` and `scePthreadSetschedparam` accept, and those a created thread takes, follow a PS5 (firmware 13.60), but they are only recorded and reported: they are not applied to host threads, so a title's priority order has no effect on host scheduling +- [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. +- [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, in the POSIX `mprotect` as well (FreeBSD 11.4 masks the argument, `sys/vm/vm_mmap.c:613`; the console was measured only through `sceKernelMprotect`), as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`), and the direct map calls refuse execute; here both throw for undefined bits, and the direct map calls map execute +- [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors maps the object as a [regular file](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp), so its views and the JIT views see each other's writes. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws +- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end and `EVFILT_WRITE` on the write end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters and a read event on the host stream on descriptor 0, which never triggers, as `select` never reports descriptor 0 readable; PPSA34015 registers descriptor 0 for reading in a new kqueue at start-up and does not check the result; other filters, `EV_CLEAR` on a socket or a pipe off Linux (see the kevent EV_CLEAR entry), a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event fires once the send buffer's free space reaches the mark, as `filt_sowrite` does with `sbspace`; the host reports that space as `SO_SNDBUF` less `SIOCOUTQ` on Linux and as `SO_SNDBUF` alone elsewhere, so off Linux it does not shrink while data is queued. Marks below 1, which make FreeBSD's read filter always fire, throw +- [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) (fix(libkernel): follow kqueue_register for EV_FORCEONESHOT and modify flags) - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here -- [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond +- [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Directory timestamp updates resolve the current path of the opened handle. Filesystems with coarser timestamp resolution cannot retain every guest microsecond +- [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails -- [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 +- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction is reported the same way when the name left after that prefix is a drive path (`X:\...`): the print name when that name is a drive path, otherwise the substitute name. `open` with `O_NOFOLLOW` returns `EMLINK` for that junction and does not open the target +- [lstat of a Windows reparse point that is not a link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a mounted volume (`\??\Volume{GUID}\`) or another name surrogate as the reparse point itself, not followed, with the mode of its attributes, as CPython's `os.lstat` does, and a reparse point without the name surrogate bit (deduplication, cloud files) as `stat` reports 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 -- [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 +- [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed; returns `EFAULT` (`0x8002000e`) when passed a null status buffer pointer. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown +- [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies. Descriptors retain the opened directory's native handle across renames. Enumeration reloads and `*at` operations resolve its current path, so a concurrent rename between resolution and the operation is not atomic. Enumeration of a removed directory and network-share path queries have not been verified +- [Linux directory entry types](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) - `sceKernelGetdirentries` reports a host symlink inside the app0 tree as the type of its target (directory or regular file), since a console app0 has no links and links there come from how the title was unpacked; a link elsewhere, or a dangling one, stays a link. A directory opened through an app0 link resolves to its target's host path, so links nested inside that target keep the link type. +- [ftruncate, truncate, sceKernelFtruncate](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) extend a file on Windows with `SetFileInformationByHandle` (`FileEndOfFileInfo`), which reserves the clusters but writes no zero bytes, and report `EFBIG` for a length the volume cannot hold (`ERROR_INVALID_PARAMETER` on a disk file; seen with a length of `INT64_MAX`, the size from which NTFS refuses it was not measured) and `ENOSPC` when the free space is smaller. FreeBSD 11.4 makes the extension a hole on a file system that has them ([`ffs_truncate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/ufs/ffs/ffs_inode.c) writes only the last block), so a length above the free space succeeds there. A write behind the end of the file still makes NTFS write zero bytes over the gap, as the file is not sparse. None of this was measured on a console. +- [readv, writev, preadv, pwritev and their sceKernel forms](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) - unreadable iovec-table errors follow FreeBSD 11.4's [`copyinuio`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/subr_uio.c#L401-L434), not console measurements. The table is copied before inspecting its entries; payload-buffer validation is unchanged. A Linux `process_vm_readv` failure other than `EFAULT` throws, including when the host denies that API. - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing -- [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. 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 +- [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. On Windows, anonymous objects are delete-on-close temporary files and `mmap` maps them as [regular files](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp); named objects remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. +- [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a read event on a listening socket, here and in `kevent`, reports the number of completed connections waiting to be accepted in `data`, as FreeBSD 11.4's [`filt_solisten`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3384-L3392) reports `so_qlen`: on Linux a TCP listener's accept queue from `TCP_INFO` (`tcpi_unacked`, which Linux fills with the accept backlog for a listening socket), and elsewhere, or for a listener that is not TCP, 1 while a connection is waiting, as no host call reports that count. A write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw +- [sceKernelAddReadEvent on pipes](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) is the one exception to the sockets-only rule above: it also takes the read end of a pipe, as `kevent` does through FreeBSD's `filt_piperead`, and ignores the size argument there. Host streams, other descriptors and a write event on a pipe throw - [close](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) and [setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` polls them; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space -- [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards, 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 +- [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - the answer for a thread that has ended is not measured on the console, and [`pthread_attr_get_np`](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) still returns 0 for such a thread, reading a detached one's object after it is freed. `scePthreadAttrGet` returns `ESRCH` (`0x80020003`) for a thread that has ended, joined or not and detached or not, as FreeBSD 11.4's [`_pthread_attr_get_np`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L139-L172) does through `_thr_find_thread`; a thread counts as ended once its thread-specific data destructors have run, where FreeBSD marks it `PS_DEAD`. On Windows a host thread adopted by `scePthreadSelf` that ends without `scePthreadExit` keeps answering 0. Monster Hunter Wilds (PPSA24191) reuses a slot of its thread table only when `scePthreadAttrGet` returns `0x80020003` +- [scePthreadAttrGet, pthread_attr_get_np and sceKernelIsStack](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) report the stack of a host thread the library did not create, on Windows, from one allocation granule (64 KiB) above the bottom of its reservation when everything above that is committed read-write, as in a relinked executable's main thread and its default-size host threads (0x200000 committed of 0x210000 reserved), so the guest can protect the lowest reported pages, as HotSpot does for its stack guard pages in Java 25 LAN / DNS (PPSA99869). Any other such thread, and every such thread on Linux, gets its whole host stack: on Windows `mprotect` commits its reserved lower pages before protecting them and drops the guard flag of Windows' guard pages, and on Linux it cannot change the unmapped lower pages - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Linux sends the host SIGUSR1, which the library takes over, to the target thread, so the handler runs wherever the thread was interrupted, emulator code included. On the console kernel objects are system calls and libthr defers signals while a thread holds its internal locks; here a handler that needs a host lock the interrupted code holds deadlocks, and a thread that the handler keeps waiting keeps its host locks. Host waits resume after the handler. The host calls Linux does not restart end early: sleeps that do not retry return before their time, and the poll of `sceNetEpollWait` and socket calls with a send or receive timeout return `EINTR` to the guest +- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows suspends the target and redirects it to the handler. A target running host code (any module other than the executable and the `*.guest.prx` modules, so libkernel, libc and the other HLE modules count too) is resumed and sampled again until it is back in guest code, since a handler that waits there would keep the host locks that code holds; a target blocked in an ntdll wait is redirected in place, as are NtContinue and winpthreads after their retry limit. A target that stays in host code without waiting for 50 ms after the raise began, such as a long HLE or driver call, is then redirected in host code anyway, with the same hazard. A target seen right after a `syscall` instruction in ntdll, inside a system call that is not a wait such as the `NtWriteFile` of an `fwrite` holding its stream lock, restarts those 50 ms, so a raise can cost the raising thread up to the 1 s retry limit. While the target is suspended only `VirtualQuery` and a lock-free cache of image bases are consulted; an image not yet in the cache is classified with `GetModuleFileNameW` after the target is resumed. An APC queued at the first host-code sample, which an alertable guest wait would run sooner, is not implemented - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask - [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`, which keeps only the first 32 signals; FreeBSD keeps a mask per thread - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) reaches a thread that waits in an alertable `TimedWait` wait through an APC. A thread blocked anywhere else, including all waits under `APS5_COARSE_TIMED_WAITS` and host waits inside the emulator, is redirected and runs the handler only when that wait returns -- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, and throws if that lasts 1 s, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it +- [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 -- [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 +- [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - on a PS5 (firmware 13.60) joining a thread a second time returns `ESRCH` (`0x80020003`), as it does here, but a joined thread is kept until `scePthreadCreate` publishes over the same handle storage, so one host object and its handle are retained per distinct guest handle storage location for the lifetime of the process; a guest that joins into a fresh storage location every time keeps every one of them. A null entry crashed the payload and still throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console. `scePthreadDetach` on a joined thread answers `EINVAL`, as FreeBSD does on a thread that is not joinable; the console answer for a detach after a join is unverified. Detaching a thread while another thread waits to join it returns `EINVAL`, and a second concurrent join returns `EOPNOTSUPP`, as FreeBSD 11.4's [`thr_detach.c`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_detach.c) and [`thr_join.c`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_join.c) check `joiner`; before this PPSA34015 stopped once with `Joining guest thread: Access is denied` when a thread it was joining was released under the join, and which of the two calls released it was not captured. A detached thread is still released when it ends, so joining it after it ended reads freed memory. A handle value copied to a second storage and joined there retains the object under the original storage key, so a later `scePthreadCreate` over that original storage releases an object the copy still points at, and the copy reads freed memory +- [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; file and socket calls are not cancellation points. An enabled cancellation request wakes POSIX `sleep` and exits the thread. `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check cancellation only before and after the host sleep, so an enabled request during those waits is delivered when the sleep ends. The asynchronous type acts like the deferred one, except at the two set calls. +- [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured +- [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified -- [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. +- [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. On Windows a guest `dup2` onto descriptor 1 or 2 keeps host logging on an `_fdopen` of a duplicate, and the guest `FILE` stays on the descriptor that was replaced; opening that duplicate throws (#7791) - [waitpid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns -1 with `ECHILD` (`EINVAL` for unknown options): nothing here creates a child process; child processes would need a real `waitpid`. - [execv](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. - [execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws like `execvp`: replacing the process image is not supported. The console behavior for invalid arguments and executable path lookups is not modeled. +- [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `AMD64_GET_FSBASE`, `AMD64_GET_GSBASE` and `AMD64_SET_GSBASE` follow FreeBSD 11's amd64 [`sysarch`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c) on Linux x86-64 hosts, through the host's `arch_prctl`. The GS base belongs to the calling thread, a base at or above `0x800000000000` (`VM_MAXUSER_ADDRESS`) returns `EINVAL`, and a null argument returns `EFAULT`. `AMD64_SET_FSBASE` throws, since guest and host code share the thread's FS base for their TLS. In a payload process on a PS5 (firmware 12.70), `I386_GET_FSBASE` (7) and `I386_GET_GSBASE` (9) wrote 4 bytes, the low 32 bits of the FS and GS bases, and failed with `EFAULT` for a null argument; here they do the same on Linux x86-64 hosts. There a GS base set by one thread left the main thread's unchanged, and threads made with `pthread_create` and `scePthreadCreate` started with their creator's GS base; here a guest thread's host thread is created by the creating thread, and Linux's [`copy_thread`](https://github.com/torvalds/linux/blob/v6.6/arch/x86/kernel/process.c#L182-L186) gives a new thread its creator's GS base. The operation numbers FreeBSD 11's amd64 `sysarch` has no case for returned `EINVAL` there for the nine tried, each with a null argument (2, 5, 6, 12, 127, 133, 134, 1000 and -1; 6 is `I386_VM86`, which its header names), as its [default case](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c#L324-L325) does; here every such number returns `EINVAL` on every host; the numbers other than those nine follow that default case and were not measured. `AMD64_GET_XFPUSTATE` (132) returned 0 there for lengths 0, 64, 128, 256 and 512 (512 was also the largest length a search accepted) and failed with `EINVAL` for 1024, 2048, 4096 and 8192 and with `EFAULT` for a null argument or address, and `I386_GET_XFPUSTATE` (11) failed with `EFAULT` for a null argument; both throw here. The other operations FreeBSD 11's amd64 `sysarch` handles, and every operation it handles on other hosts (Windows keeps its TEB in GS), throw. - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. -- [machdep.cacheline_size](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns -1 with `ENOENT`, matching FreeBSD amd64 where this sysctl OID is not defined. +- [kern.proc.vmmap](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_VMMAP`, pid} returns packed FreeBSD 11 `kinfo_vmentry` records (144 bytes each, an empty path) for the used ranges of the guest address-space arena, in address order, as [`kinfo_getvmmap`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libutil/kinfo_getvmmap.c) reads them. A range is one record: the type is always `KVME_TYPE_DEFAULT`, the protection is `sceKernelVirtualQuery`'s for the range's first page (0 where it reports none), and offset, object, resident page counts, flags and path are 0. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included, as FreeBSD's `pget` gives); a write returns `EPERM` and a short buffer `ENOMEM`. Ranges outside the arena, which guest code cannot map, are not reported. The walk takes the arena lock once per range, not for the whole walk as FreeBSD holds the map lock, so an `mmap` or `munmap` on another thread can change the list between two records. Whether a PS5 application may read its own map, and what types and paths the console reports, is unverified +- [sysctl names a PS5 payload process gets ENOENT for](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` returns -1 with `ENOENT` for `hw.byteorder`, `hw.cacheline_size`, `hw.machine_arch`, `hw.usermem`, `kern.clockrate`, `kern.maxfiles`, `kern.maxfilesperproc`, `kern.maxproc`, `kern.ps_strings`, `kern.sched.name`, `kern.smp.maxcpus`, `kern.stackprot`, `kern.timecounter.hardware` and `machdep.cacheline_size`, and `sysctl` does the same for the MIBs {1, 6}, {1, 7}, {1, 12}, {1, 27}, {1, 32}, {6, 4} and {6, 11}. The buffer and the length are left alone. A payload process on a PS5 (firmware 12.70) gets this for a read, a size query, a null length and a null buffer with a null length, by name and by MIB. `sysctlnametomib` gives those seven MIBs, and returns `ENOENT` for `hw.cacheline_size`, `hw.usermem` and `machdep.cacheline_size` without touching the count or the first three words of the MIB, as the console does. A write to these names and MIBs throws, except for `machdep.cacheline_size`, which returns `ENOENT` as FreeBSD amd64 does for an OID it does not define; the console's answer to a write was not tried. The console's `sysctlnametomib` gives `kern.smp.maxcpus`, `kern.stackprot`, `kern.sched.name` and `kern.timecounter.hardware` the MIBs {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}, and its `sysctl` returns `ENOENT` for them; here `sysctlnametomib` throws for them. `hw.physmem`, `hw.realmem` and `vm.overcommit` return `ENOENT` on the console too but report values here, as the `hw.realmem` and `vm.overcommit` entries say. A payload process also gets `ENOENT` for `kern.anyps5_no_such_name`, which its `sysctlnametomib` does not resolve; other names still throw here. A title process was not measured +- [hw.pagesize](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the system page size (16384 bytes, PS5_PAGE_SIZE) as an integer through sysctl MIB {6, 7} (CTL_HW, HW_PAGESIZE) and the "hw.pagesize" sysctlbyname lookup, following FreeBSD's [`hw.pagesize`](https://github.com/freebsd/freebsd-src/blob/main/sys/kern/kern_mib.c#L177), a read-only `SYSCTL_INT` of `PAGE_SIZE`. The value on a PS5 follows the 16 KiB page size the rest of libkernel uses and is unmeasured. A write returns -1 with EPERM. - [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the [host CPU's name](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp), built as FreeBSD 11 builds `hw.model`: the CPUID brand string (leaves 0x80000002 to 0x80000004) without leading spaces ([identcpu.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/identcpu.c#L683-L691)). Trailing padding stays, as it does there, so an AMD host's name ends in spaces. The name a PS5 reports is unknown. A write returns -1 with `EPERM`. A host without an x86 CPUID brand string, such as an Apple Silicon Mac, throws. PPSA99203 only prints it (`[console] hw.model: %s`) -- [Host CPU check](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp) (libkernel) - when libkernel loads on a Linux or Windows x86-64 host, it logs which of the PS5 CPU's user-mode extensions CPUID reports and throws, naming the host CPU, if any extension is missing that LLVM selects from plain C/C++ for the PS5's Zen 2 (`-march=znver2`, [feature list](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/llvm/lib/Target/X86/X86.td#L1459-L1513)): SSE to SSE4.2, SSSE3, POPCNT, LZCNT, BMI1, BMI2, MOVBE, CMPXCHG16B, LAHF/SAHF, AVX, AVX2, FMA, F16C, and the operating system's AVX state in XCR0 (#3719). SSE4A, CLZERO and SHA, which `--to-intel` converts, and AES, PCLMULQDQ, RDRAND, RDSEED, ADX, CLFLUSHOPT, CLWB, RDPID, XSAVEOPT, XSAVEC and PREFETCHW, which compilers emit only for intrinsics or explicit prefetches, are logged but not required: a title that executes one of them on a host without it still stops with an illegal instruction. macOS hosts are not checked, 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_STACK`, `RLIMIT_RSS`, `RLIMIT_MEMLOCK`, `RLIMIT_NPROC` and `RLIMIT_NOFILE` throw because no console values are established. -- [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request succeeds only when its normalized values match `getrlimit`; changing limits throws because guest limit state is not stored. +- [getrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `RLIMIT_CPU`, `RLIMIT_FSIZE`, `RLIMIT_CORE`, `RLIMIT_SBSIZE`, `RLIMIT_VMEM`/`AS`, `RLIMIT_NPTS`, `RLIMIT_SWAP`, `RLIMIT_KQUEUES` and `RLIMIT_UMTXP` report infinity as in FreeBSD's process-0 defaults, and `RLIMIT_DATA` mirrors the host limit; a payload process on a PS5 (firmware 12.70) reads 0x800000000 for both limits. `RLIMIT_NOFILE` reports, as both soft and hard limit, the number of descriptors after which AnyPS5 itself fails with `EMFILE`: on Windows the C runtime's 8192 low-level descriptors, which guest files, pipes and sockets all take, and as many stdio streams, which libkernel raises from 512 at load with [`_setmaxstdio`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio), on Linux the host's `RLIMIT_NOFILE`, whose soft limit libkernel raises to the hard limit at load, and on macOS the smaller of the host's hard limit and `kern.maxfilesperproc`, which libkernel sets as both limits at load, since macOS fails with `EMFILE` at `kern.maxfilesperproc` whatever the soft limit. A payload process on a PS5 (firmware 12.70) reads 13952 for both limits, while `getdtablesize` returns 12555 and the process can open descriptors up to 12554. XPSemu (PPSA97358) reads `RLIMIT_NOFILE` in QEMU's `os_setup_limits` and raises the soft limit to the hard one when they differ. `RLIMIT_STACK` (0x200000), `RLIMIT_RSS` and `RLIMIT_MEMLOCK` (infinity) and `RLIMIT_NPROC` (1604) report the soft and hard limits a payload process reads on a PS5 (firmware 12.70). Guest locking still follows the host: `mlock` and `sceKernelMlock` fail at the host's hard `RLIMIT_MEMLOCK` on Linux, and on Windows when the process working set cannot grow, although `RLIMIT_MEMLOCK` reports infinity. There the main thread's stack is 0x200000 as well (`pthread_attr_get_np`); here the main thread runs on the host thread's stack, whose size can differ. Whether a title process reads the same limits is unverified. +- [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request other than `RLIMIT_NPROC` succeeds when its normalized values match `getrlimit`, or, for `RLIMIT_NOFILE`, when `rlim_max` does not exceed the current hard limit and `rlim_cur` does not exceed `rlim_max`, storing the updated limits for subsequent `getrlimit` queries; `RLIMIT_NPROC` always throws; descriptor allocation enforcement of the lowered soft limit is not implemented, and changing other limits throws because guest limit state is not stored. In a payload process on a PS5 (firmware 12.70), `setrlimit(RLIMIT_NOFILE)` lowers a requested 13952 to 12555, as the hard and as the soft limit; here a request within the current hard limit is stored as given and one above it throws; setting `RLIMIT_STACK`, `RLIMIT_RSS` and `RLIMIT_MEMLOCK` to their current values succeeds and changes nothing, as here; setting `RLIMIT_NPROC` to its current 1604 also succeeds but leaves 1443, as FreeBSD 11 would if the console's `maxproc` were 1604: process 0's limit starts at `maxproc` ([`init_main.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/init_main.c#L550-L551)), `maxprocperuid` starts at nine tenths of it ([`subr_param.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/subr_param.c#L258)), and `setrlimit` lowers an `RLIMIT_NPROC` limit above `maxprocperuid` to it ([`kern_resource.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_resource.c#L714-L718)); 1604 × 9 / 10 gives 1443. Neither value is measured: `kern.maxproc` fails with `ENOENT` in the payload process. Here that call throws, because the limit is not stored. Java 25 LAN / DNS (PPSA99869) sets `rlim_cur = MIN(0x100000, rlim_max)` during VM initialization - [execvp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. -- [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` +- [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 macOS and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows and macOS, which has no such `AF_UNIX` pair. 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 the first stream `send` after the peer closed succeeds on the loopback connection, where the console returns `EPIPE`; the sends after it report `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 `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, as the console does after a `shutdown(SHUT_WR)`; after the peer's `close` the console reports `POLLIN` with `POLLHUP`, because `unp_drop` disconnects the other end, and the loopback connection cannot tell that from a shutdown, so no `POLLHUP` is reported +- Guest sockets on macOS ([the equeue wait, kevent, fstat and ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp), [gethostbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp)): macOS has no `ppoll`, so the wait uses `poll` with the timeout rounded up to a millisecond; it has no `POLLRDHUP`, so a stream that the peer closed is found by peeking one byte when it is readable; `FIONREAD` on a datagram socket, and the byte count that `fstat` and `kevent` report for it, is `SO_NREAD`, the size of the next datagram, because macOS adds the source address to `FIONREAD`; and `gethostbyname` replaces `gethostbyname_r`, which macOS lacks. - [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. +- [recvmsg](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) loses received stream data on an output copy fault: outputs are checked after the host receive. FreeBSD 11.4 leaves unread stream data queued, though it also drops a datagram on a copy fault ([`soreceive_dgram`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L2308-L2312), [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1758-L1774)). A Linux `process_vm_readv` or `process_vm_writev` failure other than `EFAULT` throws +- [MSG_DONTWAIT](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `send`, `sendto`, `sendmsg`, `recv`, `recvfrom` and `recvmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`). Winsock has no per-call non-blocking flag, so on Windows the call first asks `WSAPoll` with a zero timeout whether the socket is ready and fails with `EAGAIN` when it is not. The check and the transfer are two host calls: a second thread that takes the data in between can leave a `recv` blocked, and a `send` of more bytes than the socket buffer holds can still block until it has sent them, where FreeBSD's `sosend_generic` returns a partial count +- [MSG_WAITALL](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `recv`, `recvfrom` and `recvmsg` on a stream socket is passed to the host, which returns when the buffer is full, the peer closes or an error occurs, as FreeBSD 11.4's [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1855-L1860) does; a datagram socket ignores it. It is not measured on a console, a nonblocking socket and a call that also passes `MSG_PEEK` follow the host's rules, and `SO_RCVTIMEO` bounds only the wait for the first data. Any other flag still fails with `EOPNOTSUPP`. +- [recv and recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answer without the host a zero-length receive on a stream socket (0, or `ENOTCONN` when it is not connected, [L1566-L1578](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1566-L1578)) and, on Windows only, a receive on a socket whose read side `shutdown` closed (0, as [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1556-L1562) returns at `SBS_CANTRCVMORE`); Winsock fails the second with `EPIPE` and blocks on the first. Linux and macOS still return data buffered before the `shutdown`, where FreeBSD discards it. `recvmsg` still asks the host in both cases. Not measured on a console. - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). -- [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux, which the guest can neither ignore nor suppress: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; `SO_NOSIGPIPE` is not an option here, and raises nothing on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor +- [bind, connect, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) check a socket address length as FreeBSD 11.4's [`getsockaddr`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_syscalls.c) and the `sa_len` test of `tcp_usr_connect` and `in_pcbconnect` do: below 2 or an `AF_INET` address below 16 or an `AF_INET6` address below 28 bytes fails with `EINVAL`, above 255 with `ENAMETOOLONG`. An address longer than `sockaddr_in` or `sockaddr_in6` is still accepted where FreeBSD answers `EINVAL`, and an address of another family still fails with `EAFNOSUPPORT`, as the console's behaviour for these is not known. None of this was measured on a console. +- [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number -- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket nor a pipe (files, stdio, libSceNet sockets) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked +- [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 +- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) on Windows reports a guest stream socket whose peer closed as readable and adds `POLLHUP` only once the socket has shut its own sending side, as FreeBSD 11.4's [`sopoll_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3080-L3086) does; `WSAPoll` reports `POLLHUP` alone in both cases. A reset or refused connection (`POLLHUP` with `POLLERR`) keeps `WSAPoll`'s flags, a libSceNet socket keeps them too, and a call that asks for no event still returns early on the peer's close with nothing reported. Not measured on a console. +- [kevent EV_CLEAR on a socket or a pipe](../../../core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp) (libkernel) - FreeBSD 11 activates an `EV_CLEAR` knote when `kqueue_register` finds it ready, which every `EV_ADD` or `EV_ENABLE` of an enabled knote does again, and on each `KNOTE` the socket or pipe raises (`sorwakeup`, `sowwakeup`, `pipeselwakeup`); [`kqueue_scan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c) checks the filter again before reporting, drops a knote that is no longer ready and deactivates a reported one until the next activation. On Linux each such knote registers the host socket or pipe with its own epoll instance in edge-triggered mode (`EPOLLIN | EPOLLRDHUP` for `EVFILT_READ`, `EPOLLOUT` for `EVFILT_WRITE`), and a wakeup of the host descriptor with that mask stands for a `KNOTE`: the knote is then checked with the same readiness test as a level-triggered one, `NOTE_LOWAT` mark included, and a wait blocks on the epoll descriptor instead of the socket, so a socket that stays readable does not wake it. The host's wakeups are not FreeBSD's: FreeBSD raises `sowwakeup` whenever send buffer space is freed, while Linux TCP wakes `EPOLLOUT` waiters only once the socket is writable again after a write found it full (`SOCK_NOSPACE`, [`tcp_check_space`](https://github.com/torvalds/linux/blob/v6.8/net/ipv4/tcp_input.c#L5639)), so a TCP writer that never fills its buffer sees fewer `EVFILT_WRITE` events than on FreeBSD; a host stack may also coalesce data wakeups, so the number of times a busy socket fires can differ. Windows and macOS throw, as the host there has no edge-triggered wait over these descriptors that is wired up; `EV_CLEAR` on the timer, user and vnode filters does not use this and works everywhere. +- [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - host pipes are one-way, where FreeBSD's [`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422) opens both ends for reading and writing, so `EVFILT_READ` on the write end and `EVFILT_WRITE` on the read end throw; on macOS `EVFILT_WRITE` on a pipe throws, as the host has no call for a pipe's capacity. `EVFILT_WRITE` reports the free space of the host buffer, not of FreeBSD's 16 KiB one, and on Linux fires when the host says the pipe is writable rather than at `PIPE_BUF` (512) free bytes. Windows pipes cannot be waited on, so a kqueue waiting on one checks it every 1 ms. Otherwise [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1828) and `filt_pipewrite` are followed: `EV_EOF` once the other end closes, `EPIPE` for a write event added after the read end closed, and `NOTE_LOWAT` ignored. On Windows `fcntl` cannot make a pipe write end non-blocking, so the full-pipe and drain case of `EVFILT_WRITE` is tested on Linux only and the `NtQueryInformationFile` write quota is checked there only for the free space, `EV_EOF` and `EPIPE`. +- [kevent on a kqueue](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on a kqueue counts the watched queue's enabled notes that are ready when it is evaluated, where [`filt_kqueue`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/sys/kern/kern_event.c#L363-L370) reports `kq_count`, the queued notes: a queued note counts until that kqueue is scanned, even once disabled or no longer ready (a socket or pipe read empty, a user note cleared, a watched kqueue drained), so FreeBSD can report it readable where this reports nothing. A queued `EV_ONESHOT` note is reported there with its last count and deleted; here it is evaluated again and, if the watched queue is empty by then, neither reported nor deleted. `EV_CLEAR` throws, as FreeBSD re-arms it only when the watched kqueue queues a note. A cycle of watches throws; a note on the queue itself is `EINVAL`. Not measured on a console - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 -- [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level only, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT`, `SO_TIMESTAMP` and `SO_NOSIGPIPE`, which FreeBSD stores and no title has been seen to set, the send and receive timeouts and low-water marks among them) and every other level (`IPPROTO_TCP`, `IPPROTO_IP`, `IPPROTO_IPV6`) fails with `ENOPROTOOPT`. None of this was measured on a console. +- [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level, `TCP_NODELAY` and `IPV6_V6ONLY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. `IPV6_V6ONLY` (`IPPROTO_IPV6`, 27) on an `AF_INET6` socket is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL` ([L1572](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1572)), as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as FreeBSD's default `net.inet6.ip6.v6only` does, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The `IPPROTO_IPV6` level on an `AF_INET` or `AF_UNIX` socket fails with `EINVAL`, as `ip_ctloutput` and `uipc_ctloutput` answer it. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores, the low-water marks among them; PPSA99871 sets `SO_REUSEPORT` and carries on when it fails), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them), every other `IPPROTO_IPV6` option ([L1928-L1929](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1928-L1929), [L2158-L2159](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L2158-L2159)) and the `IPPROTO_IP` level fail with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had +- [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an unconnected `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), as FreeBSD 11.4's `in_pcbconnect_setup()` and `in6_pcbconnect()` do. A connected stream socket keeps its host `EISCONN` answer. Known gaps: on an already connected datagram socket `connect` to port 0 keeps the old peer, where FreeBSD's `soconnect()` disconnects first and then fails with 49; `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD sends to port 0. There nothing can receive on port 0, so the call returns the length without the host, because Linux's `udp_sendmsg` refuses port 0 with `EINVAL`; an `AF_INET6` socket still goes to the host. The answers are taken from the FreeBSD source and are not measured on a console. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified -- [mlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. On main, XPSemu (PPSA97358) fails to load at this import; its `MCL_CURRENT | MCL_FUTURE` calls execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. +- [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, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). None of the four waits is a cancellation point +- [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 +- [signal](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records its handler with `SA_RESTART` unless the signal is in the exported `_sigintr` set, as FreeBSD's libc does (`lib/libc/gen/signal.c`). `siginterrupt`, which fills that set, is a libSceLibcInternal export and is not provided: libc does not link libkernel and cannot reach the dispositions, so only a guest that writes the set itself clears `SA_RESTART` this way. - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console -- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `EINVAL` for every other command, `F_GETFD` and `F_SETFD` included). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux, where `F_GETLK` with `F_UNLCK` throws. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console -- [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other request throws. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests +- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console +- [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2`, `F_DUPFD`, `F_DUP2FD` onto another descriptor and an accepted socket start without it, `F_DUP2FD` onto the same descriptor keeps it, and `SOCK_CLOEXEC`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. +- [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests +- [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`. On Windows a shared mapping is a view of a file section (`CreateFileMappingW` on the descriptor's handle, read-write when the descriptor is, otherwise read-only), so writes through the descriptor and the mapping see each other. This also maps anonymous `shm_open` objects and JIT shared memory descriptors, which are temporary files on Windows. A shared mapping may end inside the 16 KiB page that holds the end of the file, as POSIX [`mmap`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/mmap.html) allows (llama.cpp in PS5LM Plus PPSA99581 maps a whole model file this way): the section keeps the file's size, so the file never grows, the view ends with the 4 KiB host page that holds the end of the file, which Windows fills with zeros past it, and the rest of the guest page is private zero pages. As POSIX requires, the bytes past the end read zero and are never written to the file; bytes written to those private pages are seen only through that mapping, not through other mappings of the file, and a file that grows later does not show its new bytes there. A shared mapping that reaches a whole page past the end of the file fails with `EOPNOTSUPP`, where POSIX maps it and delivers `SIGBUS` on access, as do `PROT_EXEC` and a write-only descriptor. A read-write alias (`GuestArenaMapAlias`) of a mapped page that holds the end of the file throws. `mprotect` above the view's protection (writing through a read-only descriptor, or adding `PROT_EXEC`) fails with `EACCES` before anything changes; FreeBSD allows `PROT_EXEC` on a readable descriptor. Shrinking the file with `ftruncate` fails with `EINVAL` while any shared mapping of it exists, even one that does not cover the removed part, because the section keeps the file's size at `mmap`; FreeBSD truncates. A read-write alias of a view (`GuestArenaMapAlias`, used when the GPU imports guest memory) of a read-only file section fails, as the alias is mapped `PAGE_READWRITE`. Views of the same file made by separate `mmap` calls are separate sections, so write tracking does not treat them as aliases of each other. A private mapping on Windows is a copy of the file read at `mmap`, which POSIX allows (later changes to the file need not show); pages wholly past the end of the file fail with `EOPNOTSUPP` instead of faulting on access, and a write-only descriptor fails with `EOPNOTSUPP`. +- [File mappings on macOS](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mmap` of a regular file, of an anonymous `shm_open` object (an unlinked temporary file on macOS) or of a JIT shared memory descriptor maps the descriptor shared or private as on Linux, with the same access checks. macOS refuses to create an executable file mapping (`EPERM`), so an executable mapping is made without execute and then given it with `mprotect`. Under Rosetta 2 (Apple M2, macOS 26), code rewritten through one view of a file after it ran from another view keeps its old translation until the executable view's protection changes. Named `shm_open` objects still throw on macOS. - [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it -- [pthread_kill](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread is left on the pending set for whichever thread waits for or unblocks it, since it is not delivered to the chosen thread, and a handle that is not a live guest thread returns `ESRCH` without touching it +- [pthread_kill](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread is left on the pending set for whichever thread waits for or unblocks it, since it is not delivered to the chosen thread, and a handle that is not a live guest thread returns `ESRCH` without touching it - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. -- [_fstatfs](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields -- [pthread_spin_init, pthread_spin_destroy, pthread_spin_lock, pthread_spin_trylock, pthread_spin_unlock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) follow FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) and the owner checks of its [`thr_umtx.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_umtx.h#L80-L175), unverified on the console: `EINVAL` for a null or destroyed lock and a `pshared` other than 0 or 1, `EBUSY` from `trylock` while any thread holds it, `EPERM` from `unlock` by a thread that does not hold it. `PTHREAD_PROCESS_SHARED` allocates a private lock, as guest code runs in one process. libthr spins forever when the holder locks again; here that call throws -- [sceKernelGetEventError](../../../core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp) (libkernel) returns the event's `data` as an `int` when `EV_ERROR` (0x4000) is set in `flags` and 0 otherwise, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/eventQueue.cpp#L663) does and as FreeBSD 11's [kevent](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1005) reports a failed change (`flags = EV_ERROR`, errno in `data`). [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_queue.pas#L643) instead returns `fflags` plus `0x80020000` when `fflags` is nonzero. Whether the console reads `data` or `fflags` and returns the raw errno or an SCE error code is unverified. No event delivered by the equeue here carries `EV_ERROR`, so the call returns 0 for every event `sceKernelWaitEqueue` reports +- [inet_ntop, inet_pton](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - an `AF_INET6` result of `inet_ntop` is followed by zero bytes up to `size`. In a payload process on a PS5 (firmware 12.70), the text forms, the order of the checks and this padding were measured through both exports. The padding was sampled at sizes from 3 to 65536, above 64 only at 65, 128, 256, 1024, 4096 and 65536; the sizes that were not sampled, sizes above 65536 and a title process are assumed to behave the same +- [_fstatfs](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports the host volume of a file, not a console mount: block sizes, block counts, name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0). The type, mount names, owner, filesystem id and I/O counters are 0 or empty. Sockets, pipes and kqueues return `EINVAL`, as FreeBSD's `getvnode` does. A character device, including `/dev/random` and `/dev/urandom`, reports FreeBSD 11.4's [`devfs_statfs`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/fs/devfs/devfs_vfsops.c#L225-L237) values on every host: no flags, 512-byte blocks, 2 blocks, nothing free and a 255-byte name limit. A Windows descriptor that is none of these throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields +- [pthread_spin_lock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) - a lock taken again by the thread that holds it throws here. On a PS5 (firmware 12.70) that call had not returned after 2 s, as FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) spins forever +- [pthread_mutex_getspinloops_np, pthread_mutex_setspinloops_np, pthread_mutex_getyieldloops_np, pthread_mutex_setyieldloops_np and their scePthreadMutex counterparts](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) store and report the counts, but host mutexes do not spin or yield by them. The defaults follow FreeBSD 11.4's [`mutex_init_body`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L236-L272) without `LIBPTHREAD_SPINLOOPS` and `LIBPTHREAD_YIELDLOOPS`: 2000 spins for an adaptive mutex, 0 otherwise; the console's defaults are unverified. Setting the yield count of a destroyed mutex returns 0 as FreeBSD's [`__pthread_mutex_setyieldloops_np`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L1125-L1135) does; the console's result is unverified +- [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) throws for `fflags` above 0xFFFF, where OR and addition of 0x80020000 differ and the console was not measured. Otherwise it returns 0 for `fflags` 0 and `0x80020000 | fflags`, whatever `flags` and `data` hold, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_queue.pas#L643) does and a PS5 (firmware 13.40, measured 2026-10-19 in a payload process) answered: `{flags 0x4000, fflags 0, data 22}`, `{0x4000, 0, -1}`, `{0x4000, 0, 0x80020016}` and `{0, 0, 0}` gave 0, `{0x4000, 5, 0}` and `{0x4000, 5, 22}` 0x80020005, `{0, 7, 22}` 0x80020007, `{0x8000, 3, 9}` 0x80020003. Video-out events carry their count in `fflags`, so they get 0x80020001 and up; only events built by the caller were measured. Closes when: `fflags` above 0xFFFF is measured - [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 +- [pthread_mutex_reltimedlock_np](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) takes `(mutex, usec)`, a relative timeout in microseconds, and is `scePthreadMutexTimedlock` 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 `scePthreadMutexTimedlock` ([`mutex.cpp`](https://github.com/shadps4-emu/shadPS4/blob/7c960eae136b956cf4c1d72aacd2426b650330df/src/core/libraries/kernel/threads/mutex.cpp#L284-L287)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `pthread_cond_reltimedwait_np`, so a count of 2^32 microseconds or more would be truncated if the console reads 64 bits. Unverified on the console - [sysctl kern.arandom](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. - [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified; XPSemu currently stops first at the unresolved `mlockall` import (#4386) -- [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports page-table entries over the recorded protection ranges (committed mappings; reservations are not recorded): each range charges one entry per 2 MiB granule it spans to the cpu pool when it has CPU access bits (0x7) and to the gpu pool when it has GPU access bits (0x30), so a range with both counts in both and two ranges sharing one granule charge it once each, as KytyPS5 sums per committed range. Mappings with no access bits, such as PROT_NONE or ACP-only protections, charge neither pool. The 2048-entry budget per side, its constants, and whether the console counts reservations or guest image segments are unverified. Resonance: A Plague Tale Legacy (PPSA30601) reaches this call after the other missing exports of its start-up are provided +- [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 8192 entries on each side and counts them as a PS5 (firmware 13.60) counted them for a payload process: one cpu entry for each 2 MiB granule that holds a committed mapping, whatever its protection (none included), and one gpu entry for each 1 GiB region that holds a GPU-accessible mapping, plus one for each 2 MiB granule of such a mapping that is not a whole granule of direct memory at a 2 MiB-aligned physical address. A granule or region shared by several mappings counts once, and reservations do not count. On the console the first GPU mapping of a process also takes three gpu entries and one cpu entry that stay after it is unmapped, which is not modelled, and only the ranges whose protection is recorded here are counted. A null pointer returns `SCE_KERNEL_ERROR_EFAULT`, where the console faults. Resonance: A Plague Tale Legacy (PPSA30601) calls it at start-up (#4067) - [clock_settime](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) validates like FreeBSD 11.4's [`kern_clock_settime`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_time.c#L396-L412) for a root caller: `EFAULT` for a null time, `EINVAL` for any clock but `CLOCK_REALTIME`, a negative second count or a nanosecond count outside 0 to 999999999. A valid `CLOCK_REALTIME` request throws, naming the requested time, since the host clock is never set and no guest clock offset is kept. FreeBSD checks `PRIV_CLOCK_SETTIME` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is unverified. PS5MC (PPSA99420) imports it; no call site is known +- [Short sleeps ended by GPU and flip completions](../../../core/libs/prx/libkernel/Time/SleepWake.cpp) (libkernel) - a `sceKernelUsleep`, `usleep`, `nanosleep` or `sceKernelNanosleep` of 1 ms or less returns before the requested time, which FreeBSD and the console do not do: it ends once the AGC driver reports a completed GPU batch or batch completion action, or VideoOut completes a flip, so a title that polls a GPU label or the flip status between sleeps does not sleep the rest. A title that paces frames with such a sleep can wake up to 1 ms early. A thread returns early at most 8 times in a run of sleeps; the run restarts when the thread submits AGC work or did not sleep the requested time since its last sleep, so a loop that counts sleeps to time out still sleeps all but 8 in full. `APS5_NO_SLEEP_WAKE=1` keeps every sleep in full - [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's [`kern_chflagsat`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2366-L2383) and UFS's [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556) for a root caller: the path lookup reports `EFAULT`, `ENOENT` and `ENOTDIR`, a flag outside the 17 UFS stores (`VNOVAL` included) `EOPNOTSUPP`, and `SF_SNAPSHOT` `EPERM`. Neither host keeps BSD file flags, so every file is taken to have none (`stat` reports `st_flags` 0): flags 0 succeed without touching the file, and any other flag throws, naming the flags. `UF_READONLY`, `UF_HIDDEN`, `UF_SYSTEM` and `UF_ARCHIVE` are FreeBSD's names for the Windows attributes but are not mapped to them: UFS stores them without enforcing them, `FILE_ATTRIBUTE_READONLY` already carries the owner write bit of `chmod` on Windows, and Linux has no equivalent a process can set without `CAP_LINUX_IMMUTABLE`. The console's filesystem types, `EROFS` on read-only mounts and the change-time update are not modelled -- [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 +- [fchflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) has the gaps of `chflags`: a valid non-zero flag throws, and the console's filesystem types, `EROFS` and the change-time update are not modelled. Character devices, the random device, other host descriptors and files read from app0.zar throw, since their console filesystems and the privilege check for devices are not modelled. On Windows, anonymous shared memory objects are temporary files that are not told apart from regular files, so they are treated as files instead of reporting `EINVAL` as FreeBSD 11.4's [`getvnode`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L3850-L3877) does. +- [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process, which is taken to run at nice 0: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`. As in FreeBSD's libc, where `getpriority` is the bare system call, errno is left unchanged on success. The nice value of a title process on the console is unverified, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises is not reported. `setpriority` accepts the same target ids (any other id fails with `ESRCH`, any other `which` with `EINVAL`), clamps the value to FreeBSD's `PRIO_MIN`-`PRIO_MAX` (-20 to 20) and stores it as the guest process's nice value, which `getpriority` returns; the host scheduling class is not changed, and whether a title process may lower the value without privilege is unverified +- [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `rtprio_thread` follows FreeBSD 11.4's `rtprio_thread(2)`. Every guest thread here runs under `SCHED_FIFO` or `SCHED_RR` (priority 256-767), whose rtprio class is real-time; the console's mapping from that priority to the rtprio `prio` range (0-31) is unmeasured, so `RTP_LOOKUP` and `RTP_SET` throw for such a thread rather than report an impossible priority. A thread that is not real-time reports `RTP_PRIO_NORMAL` (3) with 0, and `RTP_SET` accepts only that value. Another function fails with `EINVAL`, a null `rtprio` with `EFAULT`, a thread other than the calling one throws, and an id that is not a live guest thread with `ESRCH`. `pthread_setconcurrency` stores 0 or more and returns `EINVAL` for a negative level (the error itself, as FreeBSD's libthr does, with errno untouched); `pthread_getconcurrency` returns the level, which is a hint and changes nothing about the host's concurrency. Morrowind (`mshivam019/morrowind-ps5`) imports all three at start-up +- [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. +- [app0.zar files](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - `sceKernelOpen`, `open`, `sceKernelStat` and `sceKernelCheckReachability` resolve a path missing from `app0/` in `app0.zar`. An archived file is a read-only host null device whose reads, positioned and vectored reads, AIO reads, seeks, `fstat` and `FIONREAD` are served from the archive, with the position shared by `dup`, `dup2` and `F_DUPFD`; writes fail with `EBADF` as on a read-only descriptor, opening one for writing or truncation fails with `EROFS` and `O_CREAT | O_EXCL` with `EEXIST`. `fstat` and `sceKernelStat` report mode `0444` (`0555` for a directory), the node index as inode and the archive file's device and times. Opening an archived directory or a directory present both on disk and in the archive throws, as `getdents` would miss entries. On an archived descriptor `mmap`, `flock`, record locks, `fstatfs`, `ftruncate`, `fsync`, `fdatasync`, `getdirentries`, `fchmod` and `futimes` throw instead of acting on the null device; `mmap` is `noexcept`, so its refusal terminates the process. `lstat` and the other path calls on archived files are not implemented +- [sceKernelLoadStartModule](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) loads a system library named by its console path, `/system/common/lib/.sprx` or the sandboxed `//common/lib/.sprx` that `sceKernelGetFsSandboxRandomWord` names, from the host `libs/.prx`, as libSceSysmodule loads modules by ID; a host library has no `module_start`, so `res` is 0. Other system directories, such as `/system/priv/lib`, are not mapped. The OpenOrbis piglet sample loads `libScePigletv2VSH.sprx` and `libScePrecompiledShaders.sprx` this way +- [param.sfo](../../../core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp) (libkernel) is read when `param.json` is absent, for `TITLE`, `TITLE_ID`, `DOWNLOAD_DATA_SIZE` and `USER_DEFINED_PARAM_1` to `_4`, and mounts `/data` and `/download0` like `param.json`; the localized `TITLE_00` to `TITLE_30` are not selected by the console language. +- [sysctl MIBs for kern.smp.maxcpus, kern.stackprot, kern.sched.name and kern.timecounter.hardware](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - on a PS5 (firmware 12.70), a payload process resolves these names to {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}; `sysctl` returns -1 with `ENOENT` for reads and size queries by those exact MIBs, leaving the buffer and length unchanged, including when either or both output pointers are null. Writes were not measured and throw. `sysctlnametomib` still throws for these four names; #7541 implements the mappings for nine of the other names. +- [sceKernelGetProcessAbiType](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) throws: no measured value, prototype or documented consumer of the process ABI type was found, and PS5_MihawkOS (`mihawk-99/PS5_MihawkOS`, PPSA99170) is the only title known to import it. Returning a guessed constant would let a title branch on a value the console never answered with, so the declaration only resolves the import until the console value is measured +- [flock on a directory descriptor](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) returns 0 on Windows without taking a lock, as FreeBSD 11.4's `sys_flock` accepts any vnode descriptor and so succeeds on a directory; Windows `LockFileEx` rejects a directory handle, which used to abort the process. Two descriptors of the same directory therefore never conflict: a second `LOCK_EX | LOCK_NB` succeeds where FreeBSD fails with `EWOULDBLOCK`. Linux and macOS use the host `flock`. Not measured on a console diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 8fb1dba028..cb9cee2b32 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -1,15 +1,27 @@ # 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) + +### Unknown function info + +- [sceKernelGetCompiledSdkVersion](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - which SDK version the console returns is unmeasured. The [PS5 payload SDK](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/crt/kernel.c#L257-L275) declares the process parameters with a 32-bit PS4 SDK version at offset `0x10` and a 32-bit PS5 SDK version at `0x14`; this export returns the field at `0x10`, the one [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reads and the one [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/emulator.cpp#L110-L138) (PS4, a single 64-bit `sdk_version` there) reads from the eboot. The `int (int32_t*)` signature and `EINVAL` (`0x80020016`) for a null output follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/core/libraries/kernel/process.cpp#L56-L62). Closes when: measured on a PS5 + ### 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 +- [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, `EINVAL` for a pipe or socket, as FreeBSD `getvnode` does for a descriptor without a vnode, and `ENOTDIR` for any other descriptor that is not a directory. Outside Windows it checks search permission on the directory with `faccessat`, which fails with `EACCES` as FreeBSD `change_dir` does; on Windows that check is not modelled. 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 +- [lchmod](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws for a symbolic link on Windows and Linux, whose hosts cannot change a link's own mode; on macOS the link's mode is changed. Anything else goes to `fchmodat(AT_FDCWD, path, mode, AT_SYMLINK_NOFOLLOW)`, as FreeBSD 11.4's [`sys_lchmod`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2490-L2496) does, so a file or directory is changed as by `chmod`, with `fchmodat`'s path lookup and Windows mode mapping. On Windows any reparse point counts as a link. 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 +- [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume (`\??\Volume{GUID}\`), any other name surrogate and every reparse point without the name surrogate bit (WOF compression, deduplication, cloud files) return `EINVAL`, as FreeBSD does for a file that is not a link and as CPython's `os.readlink`, which reads only symbolic links and junctions, does - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified +- [link](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows resolves a symbolic link source through the host, which follows longer chains than FreeBSD: a chain of 40 links is linked, where FreeBSD 11.4 returns `ELOOP` after 32 (`MAXSYMLINKS`). A loop returns `ELOOP` as on FreeBSD +- [fstatat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `stat`, or to `lstat` with `AT_SYMLINK_NOFOLLOW` (0x200). Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory, and flags other than `AT_SYMLINK_NOFOLLOW` return `EINVAL`. PPSA74126 imports it. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) +- [fchown](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled: the guest's own owner and group (0) and the unchanged value (-1) are accepted, a descriptor that is not open fails with `EBADF`, and any other owner or group throws, naming it. The check reads the descriptor with `fstat`, ignoring the CRT's invalid-parameter handler on Windows as `DescriptorControl` does. The console's ownership rules are unverified. Morrowind (`mshivam019/morrowind-ps5`) imports it +- [pthread_set_defaultstacksize_np and scePthreadSetDefaultstacksize](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) set the stack size that `scePthreadAttrInit` and a thread created without attributes use, as [fpPS4's `pthread_set_defaultstacksize_np`](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_pthread_attr.pas#L89-L102) does: a size of 16384 bytes or less fails with `EINVAL` (`0x80020016` from the sce form) and there is no upper limit, so a default the host cannot allocate fails only when a thread is created. Attributes initialised earlier keep their size. Not checked on a PS5 diff --git a/docs/dev/debt/libmdbg_syscore.md b/docs/dev/debt/libmdbg_syscore.md new file mode 100644 index 0000000000..cb843c4f05 --- /dev/null +++ b/docs/dev/debt/libmdbg_syscore.md @@ -0,0 +1,5 @@ +# libmdbg_syscore technical debt + +### Unknown function info + +- [sceDebugAcquireAndUpdateDebugRegister, sceDebugAttachProcess, sceDebugCancelCoredump, sceDebugClearStepThread, sceDebugCreatePerfScratchDataArea, sceDebugCreatePerfScratchExecutableArea, sceDebugCreateScratchDataArea, sceDebugCreateScratchDataAreaForPrx, sceDebugCreateScratchExecutableArea, sceDebugCreateScratchExecutableAreaForPrx, sceDebugDestroyPerfScratchDataArea, sceDebugDestroyPerfScratchExecutableArea, sceDebugDestroyScratchDataArea, sceDebugDestroyScratchExecutableArea, sceDebugDetachProcess, sceDebugGetApplicationIdByTitleId, sceDebugGetApplicationInfo, sceDebugGetApplicationList, sceDebugGetCrashInfoDetailForCoredump, sceDebugGetCrashInfoForCoredump, sceDebugGetDLLoadFlag, sceDebugGetDataListOfUltQueue, sceDebugGetDebugRegisterStatusMap, sceDebugGetEventList, sceDebugGetEventListForEQueueFd, sceDebugGetEventSubscriptionList, sceDebugGetEventSubscriptionListForEQueueFd, sceDebugGetFiberInfo, sceDebugGetFileInfo, sceDebugGetFileInfoForCoredump, sceDebugGetFileList, sceDebugGetFileListForCoredump, sceDebugGetGpuInfoArea, sceDebugGetJobManagerInfo, sceDebugGetJobManagerSequenceInfo, sceDebugGetJobManagerSequenceList, sceDebugGetModuleInfo, sceDebugGetModuleList, sceDebugGetModuleMetaData, sceDebugGetMonoVMInfo, sceDebugGetMonoVMList, sceDebugGetProcessCoredumpHandlerEventBlob, sceDebugGetProcessCoredumpHandlerEventInfo, sceDebugGetProcessCoredumpHandlerResult, sceDebugGetProcessEventCntlFlag, sceDebugGetProcessInfo, sceDebugGetProcessList, sceDebugGetProcessPropertyForCoredump, sceDebugGetProcessResourceStatCount, sceDebugGetProcessResourceStatData, sceDebugGetProcessTime, sceDebugGetProcessTimeCounter, sceDebugGetSyncExclusiveWaiterList, sceDebugGetSyncObjectData, sceDebugGetSyncObjectList, sceDebugGetSyncWaiterList, sceDebugGetSyncWaiterListForEQueueFd, sceDebugGetSystemStatusBlob, sceDebugGetSystemStatusCount, sceDebugGetThreadInfo, sceDebugGetThreadInfoByIdent, sceDebugGetThreadList, sceDebugGetThreadListAsIdent, sceDebugGetUlObjectList, sceDebugGetUltCondvarInfo, sceDebugGetUltInfo, sceDebugGetUltListOfUltRuntime, sceDebugGetUltMutexInfo, sceDebugGetUltQueueDataResourcePoolInfo, sceDebugGetUltQueueInfo, sceDebugGetUltRuntimeInfo, sceDebugGetUltRwlockInfo, sceDebugGetUltSemaphoreInfo, sceDebugGetUltWaitingQueueResourcePoolInfo, sceDebugGetVirtualMemoryDetailInfoForCoredump, sceDebugGetVirtualMemoryInfo, sceDebugGetVirtualMemoryInfoForCoredump, sceDebugGetWaitingListOfUltCondvar, sceDebugGetWaitingListOfUltMutex, sceDebugGetWaitingListOfUltQueue, sceDebugGetWaitingListOfUltRwlock, sceDebugGetWaitingListOfUltSemaphore, sceDebugGetWorkerThreadListOfUltRuntime, sceDebugInit, sceDebugInitForCoredump, sceDebugInitForTest, sceDebugIpmiGetBlockTimeInfoList, sceDebugIpmiGetBlockedIpcInfo, sceDebugIpmiGetChannelInfo, sceDebugIpmiGetChannelKidList, sceDebugIpmiGetChannelWaitingThreadList, sceDebugIpmiGetClientInfo, sceDebugIpmiGetClientKidList, sceDebugIpmiGetClientKidListByDump, sceDebugIpmiGetClientKidListByServerKid, sceDebugIpmiGetConnectRequestInfoList, sceDebugIpmiGetConnectionInfoList, sceDebugIpmiGetConnectionWaitingThreadListByClientKid, sceDebugIpmiGetConnectionWaitingThreadListBySessionKid, sceDebugIpmiGetDump, sceDebugIpmiGetEncryptedInfoAllForCoredump, sceDebugIpmiGetKidInfoListForCoredump, sceDebugIpmiGetServerDispatchInfo, sceDebugIpmiGetServerInfo, sceDebugIpmiGetServerKidList, sceDebugIpmiGetServerKidListByDump, sceDebugIpmiGetServerWaitingThreadList, sceDebugIpmiGetSessionInfo, sceDebugIpmiGetSessionKidList, sceDebugIpmiGetSessionKidListByServerKid, sceDebugIpmiGetTidListByDump, sceDebugKillApplication, sceDebugKillProcess, sceDebugNoStopChildProcesses, sceDebugNoStopOnDLLoad, sceDebugProcessSpawn, sceDebugReadEvent, sceDebugReadEventForTest, sceDebugReadProcessMemory, sceDebugReadProcessMemoryForSDBGP, sceDebugReadProcessRegister, sceDebugReadProcessRegisterForSDBGP, sceDebugReadProcessResourceStatData, sceDebugReadThreadRegister, sceDebugReadThreadRegisterForSDBGP, sceDebugReleaseDebugRegister, sceDebugResumeApplication, sceDebugResumeProcess, sceDebugResumeThread, sceDebugSetProcessEventCntlFlag, sceDebugSetStepThread, sceDebugSpawnApplication, sceDebugStopChildProcesses, sceDebugStopOnDLLoad, sceDebugSuspendApplication, sceDebugSuspendProcess, sceDebugSuspendThread, sceDebugTriggerCoredump, sceDebugTriggerCoredumpForSystem, sceDebugTriggerVrCaptureDump, sceDebugWriteProcessMemory, sceDebugWriteProcessRegister and sceDebugWriteThreadRegister](../../../core/libs/prx/libmdbg_syscore/Export.cpp) (libmdbg_syscore) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libmdbg_syscore.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified diff --git a/docs/dev/debt/libs.md b/docs/dev/debt/libs.md index 393d228df5..1e07f7ef14 100644 --- a/docs/dev/debt/libs.md +++ b/docs/dev/debt/libs.md @@ -3,3 +3,7 @@ ### Unknown function info - [SceAvPlayerVideoEx](../../../core/libs/SceTypes.hpp) (libSceAvPlayer) - frame rate field and encoding unknown; it is left zero in frame and stream info + +### Functional + +- [GuestKernelSys.cpp](../../../core/libs/tests/GuestKernelSys.cpp) (libs) - Windows removes the fixture's symbolic links and the fixture without std::filesystem: the MinGW libstdc++ (GCC 15.2.0) reports a dangling symbolic link as missing and clears the error, so `remove` neither deletes it nor fails, and `remove_all` loops forever over a tree that holds one (`recursive_directory_iterator::__erase` retries the entry while the error stays clear). The tests delete reparse points with `DeleteFileW` and `RemoveDirectoryW` and the fixture through the shell diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 7922600c43..2d22df8a66 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -2,18 +2,35 @@ ### Functional +- [Executable guest imports](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - on Linux and macOS, a relocated executable import resolved to one guest provider requires that module and its dependencies at startup, even when it was discovered through `--module-dir` without a DT_NEEDED entry. Unreferenced symbols and no-op relocations do not require an otherwise deferred module. Console module-start ordering is unmeasured. + - [Runtime-loaded modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) that another module needs are initialized at startup with no start arguments, before the module that needs them is loaded; the console is assumed to start them when that module is loaded. -- The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens a console when launched, unless it is relinked with `--windows-gui`, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle, so everything it prints is lost, including the entry stub's errors and crash reports (#3282). +- [Runtime-loaded modules the executable needs](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) are mapped at startup on Windows and started when the title loads them with `sceKernelLoadStartModule`, with their start arguments, as a Unity player loads `mono-ps4.sprx`. A title that calls such a module before loading it finds its initializers not run; whether the console starts it earlier is not measured. A module the executable only imports from and the title never loads itself gets no `module_start` and no initializers at all under this deferral, and nothing reports it. Linux and macOS still start it at startup. +- The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens a console when launched, unless it is relinked with `--windows-gui`, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle; the [entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) gives each missing one `anyps5.log` beside the executable, replacing the previous run's file (#3282). Errors about the executable path or about waiting for the previous LoadExec process are written before the log is opened and are lost when the standard error handle is missing. If the log cannot be created, output to the missing handles is lost. +- [Guest import registries](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - declared host imports of bundled modules are recorded with their module name and relocation target, even when another guest module exports the same stripped NID. No-op relocations are omitted. Registries describe import bindings, not implementation correctness or runtime reachability. - Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Relocations are kept, so if `0x140000000` is taken Windows relocates the image, possibly above that window. Verified only on Windows 10. - [SDK memory-management syscall traps](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) under `--trap-sdk-syscall-stubs` recognize three exact instruction sequences from [ps5-payload-dev SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), observed in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument preparation and carry-flag error handling. Matching requires a decoded instruction boundary and the complete sequence inside the scanned code range. Other SDK/compiler output remains rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults. -- `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. +- 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. +- Converted Linux main executables are [placed at `0x400000`](../../../core/relinker/relinker/src/pipeline/Conversion.cpp) by default, as `ET_EXEC` without ASLR, not only with `--linux-fixed-base`: the console executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Where the console loads an executable that does not require an address is not measured; `0x400000` is the base that executables reading their GOT through `app0/imports.txt` assume (#4013). The relinker refuses an image that spans 2 GiB or more, so the executable ends below the guest arena at `0x200000000`. With `--lazy-binding` the executable stays position independent. +- `--to-intel` lowers MCOMMIT to STC plus NOP padding and RDPRU through a runtime-checked stub in the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp): AMD APM Vol. 3 defines RDPRU ECX 0 as MPERF and ECX 1 as APERF, neither readable on Intel hosts, so both selectors return the host TSC as a disclosed monotonic-counter approximation (their ratio always reads 1.0) while other selectors yield zero EDX:EAX. STC asserts CF=1 because under x86-TSO prior stores are already ordered and guest memory is host-DRAM-backed, leaving the commit-success case as the only observable one. Non-zero RDPRU selectors, the TSC-for-MPERF/APERF substitution, and any console path reporting MCOMMIT CF=0 are unverified. +- [Unscoped imports on Windows](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) - an import without a module that no searched library exports under its name is searched again under the NID of that name, because homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) imports by symbol name (Quake, from the websrv homebrew launcher; Mari0, #6239) while the libraries in `libs/` export NIDs. A dependency named with `.sprx`, `.suprx` or `.debug_prx` loads the `.prx` library of the same stem from `libs/`, and `libkernel_web` also loads `libkernel.prx`, as `libSceLibcInternal` already loads `libc.prx`: AnyPS5's `libkernel_web.prx` exports only two functions, where the console's `libkernel_web.sprx` is a libkernel variant. A name that matches a library's export as written still binds to that export first. macOS resolves these imports through the host loader by name and is unchanged. +- [Unscoped imports on Linux](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) - in an executable that names a dependency with `.sprx`, `.suprx` or `.debug_prx`, as homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) does, an import without a module that is named by symbol is bound to the NID of that name only. Unlike Windows, the name as written is not tried first, because the host loader also searches the host C library, which exports `malloc` or `pthread_create` under their names. A name that a bundled module exports still binds to that module, and such a dependency loads the `.prx` library of the same stem. Unqualified imports of other executables, and imports of bundled modules, are still bound by name. - `--frame-red-zone` frames only leaf functions that start with a chain of pushes of at least five bytes (`push rbp; mov rbp, rsp; push ...`, or pushes alone) and leave through `pop ...; ret` exits of at least five bytes ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). In a leaf without a frame pointer, every RSP operand is moved by the 128 bytes RSP is lowered by, so the leaf is skipped when one cannot move in place: an operand at or above RSP with an 8-bit displacement, or one without a displacement. Leaves without pushes, shorter prologues and exits, and functions with indirect jumps or other uses of RSP keep their red zone. Inside a moved frame the CFA rules of the guest's unwind tables are 128 bytes off. Function starts are direct call targets and 16-byte-aligned code after int3 padding, so a leaf reached only through a pointer without padding before it is not seen. A body ends at int3 padding, at 16-byte-aligned code after a ret or ud2, and at the target of a jmp with only padding between them (a tail call to the next function), so such a leaf is not taken into the body before it there; one that follows a jmp to elsewhere, such as a loop's back edge, without int3 padding still is. - The length-changing path of the [instruction rewriter](../../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. +- The [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp) takes the branch targets that keep an instruction in place from a linear scan of the code segment, so data in that segment decodes as branches too: OpenSSL's x86-64 assembly keeps strings such as "SHA1 block transform for x86_64, CRYPTOGAMS by " in its code, and in SuperTuxKart (PPSA69421) and Mortalfin (PPSA99141) the `jae` read from "sf" points into the VEX prefix of a 256-bit store. A direct branch target inside a scanned instruction counts only where every byte before it is a legacy prefix or REX, which a real branch can skip (a `lock` prefix, for example); a target in a VEX prefix, opcode or operand is ignored, as a jump-table entry that starts no instruction already is. A real branch to such a target, overlapping instructions as hand-written code can, is not seen, and the instruction it enters can be moved. - A `PT_SCE_RELRO` segment that no `PT_LOAD` covers, the PS4 layout, is [loaded](../../../core/relinker/relinker/src/parsing/ElfReader.cpp) as a read-write `PT_LOAD` and stays writable after relocation; FreeBSD rtld makes `PT_GNU_RELRO` read-only once relocation is done, and what the console does with `PT_SCE_RELRO` is not checked. +- In Windows output, a guest instruction that addresses memory through GS becomes `UD2` and NOP padding of its own length ([TLS builder](../../../core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp)): the host owns GS, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`. - In executables with a `PT_SCE_DYNLIBDATA` segment, the PS4 layout, the executable segment also holds read-only data after the code (CUSA00900: 37 MiB after its last function). The [relinker pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp) therefore scans for syscalls and import call sites only up to the end of the PLT that follows the last function of the unwind table; code after the PLT without an unwind entry is not scanned. `--to-intel` and the `unused-filter=1` and `unused-filter=2` analyses are not limited this way. -- 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. +- The [syscall scan of `sce_module` modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) with a `PT_SCE_DYNLIBDATA` segment stops at the same end, or, when the PLT comes before the code, at the end of the last function of the unwind table; code after it is not scanned. +- In the same executables and modules, the bytes between unwind functions are 16-byte slots of function tails and padding, and a tail can begin inside an instruction. The [instruction scanner](../../../core/relinker/codegen/src/InstructionScanner.cpp) of the syscall and call-site scans therefore resumes at the next byte when bytes outside every unwind function do not decode, and ends a decoding outside them at the start of the next unwind function. Code without an unwind entry that does not decode from its first byte is skipped by these scans instead of failing the relink; inside an unwind function a decode failure still fails it. +- Read-only data can also sit between the unwind functions of such images. There, a `syscall`, `sysenter` or `int 0x80` pattern fails the [syscall scan](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) only when a constant syscall number is set before it; the others, and every `sysret`, are skipped and counted in one message. The number is looked for in the decoded instructions and, whatever alignment the decoding took, in the bytes before the pattern: a `mov eax, imm32`, `mov rax, imm32`, `mov rax, imm64` or `xor eax, eax`, optionally followed by `mov al, imm8` or `mov ax, imm16`, then by any number of register moves or zeroings that leave `rax` alone. A real syscall without an unwind entry whose number is set otherwise is not refused and runs on the host: from memory or another register (`mov eax, ecx`), by `push`/`pop` or arithmetic (`or eax, imm32`), or with any other instruction between the load and the pattern. +- In the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs for such images, a relocation or `lea` target outside every unwind function is followed as code only when its linear decoding, up to the first jump, return or trap, the next unwind function or 256 instructions, holds an `fs:[disp32]` access, or when it is 16-byte aligned, starts less than 256 bytes after an unwind function and ends in a jump, return or trap. Every other such target is taken as data and counted in one message, and the Windows TLS rewrite fails instead of overwriting one. A TLS access of another form, such as `mov rax, fs:[rax]`, or a `gs` instruction in code reached only through such a target is therefore not rewritten or trapped. Other fs and gs instructions do not tell code from data there: string bytes decode as these prefixes, and in two measured titles about 2,500 of about 5,500 such targets decode to one before their first jump. +- In executables and guest modules with a `PT_SCE_DYNLIBDATA` segment, the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs follows code only from the end of `PT_INTERP`, or of the `/libexec/ld-elf.so.1` string that begins the segment when no `PT_INTERP` covers it, as in OpenOrbis modules, to the end of the PLT after the last function of the unwind table, or to the end of that function when the PLT comes before the code, as in the `libSceFios2.prx` and `libc.prx` that CUSA02636 bundles (PLT at 0xc8); the pipeline's scans of executables above stop at this same end, also when the PLT comes before the code. A guest module with PLT relocations and no PLT before or after its unwind functions, or with a PLT that overlaps one, fails as the executable does. Code after that end is not analyzed, such as the `DT_FINI` routine without an unwind entry that follows the last unwind function in both modules and that the Windows and macOS outputs call when the program exits: a TLS access in it is not rewritten, the ELF entry point, `DT_INIT`, `DT_FINI`, a branch or a pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion. - A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). +- In macOS output, a guest instruction that addresses memory through GS becomes `UD2` and NOP padding of its own length ([macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)): the host owns GS, which holds the pthread TSD base the guest TLS stubs read, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. -- `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). 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, and RDTSCP runs unchanged with ECX set to 0 after it, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB (also measured on an M5, macOS 27.0.1). An RDPID or RDTSCP site with no movable instruction after it for the jump is kept; a kept RDTSCP reports 0xAAAAAAAB. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. - `--to-intel` replaces the approximate legacy SSE and VEX.128 register forms of RSQRTPS/RCPPS and VRSQRTPS/VRCPPS with [correctly rounded](../../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). Legacy SSE forms preserve the destination's upper YMM bits; VEX forms clear them. The 256-bit, scalar and memory forms keep the native instruction, as does a site shorter than 5 bytes with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). -- `--trace-imports` ([entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) exists for Windows output only. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with `sceKernelDlsym`, is not, and neither are weak imports or the imports of modules loaded at run time +- `--trace-imports` ([Windows entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp), [Linux entry stub](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp)) exists for Windows and Linux output, not macOS. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with `sceKernelDlsym`, is not, and neither are weak imports or the imports of modules loaded at run time. On Linux the entry stub makes the page of every traced slot readable, writable and executable with `mprotect` before it stores the thunk, so a slot inside `PT_GNU_RELRO` can be replaced; those pages stay writable, and data pages become executable, while tracing is on + +- [ANDN lowering](../../../core/relinker/codegen/src/x86/AndnLowering.cpp) uses scalar NOT and AND for 32- and 64-bit register and memory operands under `--to-rosetta`, following the ANDN entry in [Intel SDM volume 2A](https://cdrdv2-public.intel.com/812383/253666-sdm-vol-2a.pdf). SF and ZF follow the result, CF and OF clear, and AF and PF are undefined. The scratch register is saved below the 128-byte red zone. A 64-bit stack-relative displacement that exceeds signed disp32 after reserving the spill frame throws; other BMI instructions are not lowered. The M1 test covers register overlap, RSP operands, memory addressing, RIP relocation, flags and red-zone preservation; precise fault delivery inside a spill sequence has not been verified. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 036f899098..2b6ace99db 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -4,26 +4,57 @@ - winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. The host `std::shared_mutex` objects in [Recompiler.cpp](../../../core/shader/recompiler/Recompiler.cpp) are locked once when created. A new host `std::shared_mutex` used on Windows needs the same. +- The bundled [SPIRV-Tools build](../../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy. A depth-first traversal keeps one value map and removes each subtree's added values on return, avoiding growing map copies and preventing sibling values from leaking into one another. This extends the processed-map release fix in [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). Remove the patch when the pinned dependency has equivalent copy-free traversal; #6962 alone still copies maps for sibling branches. System-dependency builds need an equivalent fix in their installed package. Optimization passes and validation remain unchanged; wider shader and game performance measurement is needed. + ### Unknown function info -- [Packed pixel ancillary word](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295. The sample index is the SampleId builtin, an equality inferred, not measured. The render target index is the Layer builtin when the draw exports it (a layered draw, `PA_CL_VS_OUT_CNTL` bit 18; the pipeline specialization constant `LayerExported`) and 0 otherwise, the first layer Vulkan renders when no pre-rasterization stage writes Layer. Reading the unwritten Layer builtin instead makes AMD's Windows driver drop the fragment's writes (#4800); with the constant at 0 the load is folded away. Mesa reads both fields from those bits, with the layer 14 bits wide only from GFX12 on ([ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c#L342-L368)). A value passed through a select, move or phi whose every read is an extract inside one of those fields is rebuilt from the two fields alone. Any other read the masked-select pass cannot drop gets the whole word: the primitive class at bits 0-1 (0 point, 1 line, 2 triangle, 3 rectangle), which the draw sets as a pipeline specialization constant ([RasterPrimitiveOf](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp)) in the shaders that read it, plus the sample field, with every other bit zero. That layout comes from the RDNA 3 and 3.5 ISA guides (prim type 1:0, VRS rates 3:2 and 5:4, eye id 7, sample 11:8, render target index 28:16). The GCN register guides give the same prim type and sample fields and put the render target index at 26:16, so the two sources agree for layers below 2048. AMD's LLPC reads the primitive class from bits 1:0 with the same encoding ([LowerGlCompatibility.cpp](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/llpc/lowering/LowerGlCompatibility.cpp#L830-L836)) and without a GFX-level check ([getPrimType](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/LowerInOut.cpp#L5307-L5315)). Nobody has checked it on gfx1013. In the whole word only bits 0-1 and 8-11 have a source. Bits 2-7, 12-15 and 29-31 are assumed zero, not measured: the VRS rates (2-5) and the eye id (7) because the driver supports neither variable-rate shading nor multiview, so a title that enables either would read other bits on the console, and bits 6, 12-15 and 29-31 because no source gives them a meaning. The sample field reads SampleId, which is always 0 here because the driver rejects MSAA and per-sample shading (`PA_SC_AA_CONFIG`, `PS_ITER_SAMPLE`). The whole word has the same 0 at bits 16-28. A change that makes draws export a layer must restore the Layer read in both places -- [Color comparison descriptor forms](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): emulated color comparison supports UNORM, SNORM and float formats for 2D, 2D-array and cube views. 8-bit sRGB and UNORM (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`, `Format8UNorm`, `Format8_8UNorm`, `Format8_8_8_8UNorm`) return 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. Anisotropic filters are refused, except on those 8-bit formats, where every texel comparison is 0 (not measured with an anisotropic S#). Bilinear cube comparisons of float, snorm and the other unorm formats without `DISABLE_CUBE_WRAP` are refused; on that hardware a 4x4 R32 float cube blends across faces (46 of 90 edge rows differ from a per-face clamp; face 0, ref 0.1, u=3.9 v=2.5 is 0.6016 and u=1.5 v=0.1 is 0.3984), and with `DISABLE_CUBE_WRAP` the per-face clamp matches. An 8-bit sRGB or UNORM bilinear cube without that bit returns 0, since every texel comparison is already 0 for every compare function and the selected face cannot change the result. Cube-edge sampling behavior and console coverage on PS5's gfx1013 remain unverified. +- [VCC-backed MIMG samplers](../../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp): the s104/s105/vcc_lo/vcc_hi tuple follows LLVM's [SGPR register classes](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/SIRegisterInfo.td#L452) and [gfx10 sampler operand](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/MIMGInstructions.td#L1379), using the existing VCC word reads. Recompiled wave32/wave64 reads are checked by [bilinear and white-border cases](../../../core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleVccSampler.cpp); raw RDNA execution of this tuple on gfx1013 or gfx1036 is unmeasured. +- [MSAA-address image loads on 2D textures](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) - 32-bit `image_load` with DIM 2D_MSAA reads a non-multisampled 2D T# as the same DIM 2D load, ignoring the sample index. Measured on RX 6650 XT (gfx1032), wave32 and wave64, for R32_UINT, R32_FLOAT, RGBA8_UNORM and RGBA8_SRGB, sample indices 0/1/2/7/31/0xffffffff and in-range/out-of-range coordinates; IEEE 0, DX10 clamp 1, denorm32 0, denorm16 3, round32/16 0, fp16 overflow 0. Pinned in ImageMsaaAddressOnPlane.cpp. Not measured on gfx1013. Stores, atomics, mip-bearing or A16 addresses, packed accesses, converted formats, depth-bit views, array-MSAA addresses and image-table accesses are not enabled by this compatibility mode. + +- [MUBUF/MTBUF descriptors ending in VCC](../../../core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp): the four dword descriptor at s104 is s104, s105, vcc_lo and vcc_hi, as LLVM's [VCCTuple/SReg_128](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/SIRegisterInfo.td#L422) and [buffer resource operands](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/BUFInstructions.td) describe. Existing translation maps scalar codes106/107 to the raw VCC words. Recompiled loads/stores, both words, bounds, formats and wave32/wave64 execute on NVIDIA and AMD Vulkan; raw gfx10/gfx1013 hardware is not measured. Higher descriptor bases remain refused; the normal SGPR bound is unchanged. +- [Packed pixel ancillary word](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295. The sample index is the SampleId builtin in a pixel shader the guest iterates per sample (`PS_ITER_SAMPLE` on a multisampled draw), an equality inferred, not measured. In any other pixel shader it is 0, the iterated sample number of a pixel the hardware does not iterate (also inferred), so reading it does not make the host shade per sample. The render target index is the Layer builtin when the draw exports it (a layered draw, `PA_CL_VS_OUT_CNTL` bit 18; the pipeline specialization constant `LayerExported`) and 0 otherwise, the first layer Vulkan renders when no pre-rasterization stage writes Layer. Reading the unwritten Layer builtin instead makes AMD's Windows driver drop the fragment's writes (#4800); with the constant at 0 the load is folded away. Mesa reads both fields from those bits, with the layer 14 bits wide only from GFX12 on ([ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c#L342-L368)). A value passed through a select, move or phi whose every read is an extract inside one of those fields is rebuilt from the two fields alone. Any other read the masked-select pass cannot drop gets the whole word: the primitive class at bits 0-1 (0 point, 1 line, 2 triangle, 3 rectangle), which the draw sets as a pipeline specialization constant ([RasterPrimitiveOf](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp)) in the shaders that read it, plus the sample field, with every other bit zero. That layout comes from the RDNA 3 and 3.5 ISA guides (prim type 1:0, VRS rates 3:2 and 5:4, eye id 7, sample 11:8, render target index 28:16). The GCN register guides give the same prim type and sample fields and put the render target index at 26:16, so the two sources agree for layers below 2048. AMD's LLPC reads the primitive class from bits 1:0 with the same encoding ([LowerGlCompatibility.cpp](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/llpc/lowering/LowerGlCompatibility.cpp#L830-L836)) and without a GFX-level check ([getPrimType](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/LowerInOut.cpp#L5307-L5315)). Nobody has checked it on gfx1013. In the whole word only bits 0-1 and 8-11 have a source. Bits 2-7, 12-15 and 29-31 are assumed zero, not measured: the VRS rates (2-5) and the eye id (7) because the driver supports neither variable-rate shading nor multiview, so a title that enables either would read other bits on the console, and bits 6, 12-15 and 29-31 because no source gives them a meaning. The sample field reads SampleId only in iterated shaders. The whole word has the same 0 at bits 16-28. A change that makes draws export a layer must restore the Layer read in both places +- [Front-face input with `FRONT_FACE_ALL_BITS`](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (shader recompiler) - with `SPI_BARYC_CNTL.FRONT_FACE_ALL_BITS` set, a back face reads 0 and a front face 0xffffffff. Mesa 22.0 RADV sets the bit ([radv_pipeline.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/vulkan/radv_pipeline.c#L5953)) and ACO tests the input with `v_cmp_lg_u32 0` ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/compiler/aco_instruction_selection.cpp#L8123-L8128)), so only back = 0 and front ≠ 0 are sourced; PPSA05684's two-sided cloth shaders test it with `v_cmp_lt_u32 0`. The exact front value is not measured. Without the bit the input is ±1.0, as current Mesa reads it (`fgt 0`, `fsign`; [ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c)) +- [Color comparison descriptor forms](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): for formats AnyPS5 can sample as floating-point from GFX10 codes 1–77, `image_sample_c*` returns 0 for every comparison function except codes 7 (`16_UNORM`) and 22 (`32_FLOAT`), which use the native comparison path. The split was measured with point sampling on RDNA2 (gfx1031 and gfx1036) and with point and bilinear sampling on RDNA3 (gfx1100); the PS5's gfx1013 and 2D-array/cube comparisons across these formats remain unverified. Codes 128–130 (8-bit sRGB) also return 0, measured on gfx1035 over references -0.5..1.5 and texel codes 0..255. Scaled, integer and formats requiring converted sampling remain unsupported, although the tested hardware formats also return 0 for them. Anisotropic comparison of the remaining formats has not been measured. - [s_setkill](../../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified - [s_sethalt](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) (core/shader/recompiler) - with SIMM16[0] clear it is a no-op: the RDNA 2 ISA guide describes that value as resuming a halted wave, and a wave that executes it is not halted; measured on a gfx1035 (RDNA 2) wave: `s_sethalt 0` followed by a VALU op completes and gives the normal result. Only SIMM16[0] is read; with it set the instruction throws -- [s_subvector_loop_begin and s_subvector_loop_end](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled +- [s_subvector_loop_begin and s_subvector_loop_end](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. The begin instruction tests for an empty mask as `(exec_lo | exec_hi) == 0`: AMD's Windows driver (32.0.21045.5002 and 32.0.21042.62) compiled `exec_lo == 0 && exec_hi == 0` as a test of the whole 64-bit mask when guest loads use selects, as they do with `robustBufferAccess2` (#7376, #7623). Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled - [Vertex entry merged wave info](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - `s3` of every vertex-path shader is set to `waveSize | waveSize << 8` (vertex and primitive count of a full wave, wave index 0), or to `32 | 32 << 8` for a wave64 program on a 32-lane host subgroup, which runs it at one lane per invocation and holds no lanes 32-63. On a device whose `subgroupSupportedStages` lacks the vertex stage (lavapipe, MoltenVK), vertex and local programs run as one-lane waves: `s3` is `1 | 1 << 8`, the lane id is 0, a ballot is the invocation's own bit, a shuffle or broadcast returns the invocation's value, an elect is true and a subgroup reduction is the value itself, so no `GroupNonUniform` capability is declared. Layout taken from the prologue Mesa's ACO emits for NGG vertex shaders (`s_bfe_u64 exec` from `s3`), not measured on hardware. The driver gives every graphics program user data from `s8`, so the condition does not tell NGG from legacy vertex shaders: they just never read `s3`. A draw whose last wave is partial still reports a full wave, and so does a host wave with idle invocations (an RTX 5070 ran every vertex warp of a non-indexed triangle list with 30 of its 32 invocations). -- [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Programs with LDS, GDS or memory writes throw. Not measured on hardware -- [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them, so a shader with eight guest clip and cull distances exceeds the eight the device holds and fails to compile. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan +- [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another, or when the barrier runs before every memory access of the program (it is on the entry's straight-line path with no load, store, atomic, image or LDS access ahead of it), so it orders nothing; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Other programs with LDS, GDS or memory writes throw. Not measured on hardware +- [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan - [32_AR color exports](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - with `SPI_SHADER_COL_FORMAT` 3 the red value is export channel 0 and the alpha is export channel 1, the GFX10 layout of Mesa's [`emit_ps_color_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L400-L411), ACO and radeonsi in Mesa 22.0 ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/amd/compiler/aco_instruction_selection.cpp#L11087-L11096), [si_shader_llvm_ps.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/gallium/drivers/radeonsi/si_shader_llvm_ps.c#L323-L333)) and AMD's [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/FragmentColorExport.cpp#L157-L168); before GFX10 the alpha was channel 3. Channels 2 and 3, which none of these compilers enables, are ignored; green and blue are 0 and the alpha of an export without channel 1 is 1.0, the defaults of the other formats, which no source gives. Not measured on hardware, neither on gfx1013 nor on a PC RDNA 2 GPU; pinned in [Pixel32ArExport.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp) +- [GCN instructions](../../../core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp) (shader, PS4) - PS4 (Sea Islands) instructions are re-encoded to their RDNA encodings with opcode tables built from shadPS4's `opcodes.h` and decoded by the RDNA decoder; an encoding without an RDNA counterpart throws. MIMG has no DIM field on GCN: the dimension is taken as 2D, or 2D array when DA is set, and the [materializer](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) rejects a T# whose type needs another address count. MIMG word 1 bits [31:26] are reserved on GCN (A16 and D16 on RDNA). +- [GCN buffer and image descriptors](../../../core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp) (shader, PS4) - a GCN V# word 3 (DATA_FORMAT [18:15], NUM_FORMAT [14:12], names read from the most significant field) is rewritten to the unified format of the PS5 layout with OOB_SELECT 1 for a strided buffer and 3 otherwise, following the GFX6-7 NUM_RECORDS rules in Mesa's `ac_descriptors`; ATC, HASH, HEAP and MTYPE are dropped. The V# base is 44 bits, as in shadPS4's `AmdGpu::Buffer`: Limbo leaves 0x7 in word 1 bits [15:12]. A GCN T# is rewritten to the PS5 layout with SW_MODE linear, keeping TILING_INDEX [4:0], POW2_PAD [5] and PITCH - 1 [19:6] in word 7. None of it is checked on a console. +- [PS4 legacy vertex shaders](../../../core/shader/recompiler/Recompiler.cpp) (shader, PS4) - a PS4 vertex program at the VS slot takes its user data from s0, its vertex index in v0 and its instance index in v1-v3, and a fetch shader it calls through the user pair (`s_swappc_b64 s[0:1], s[0:1]`) returns through the same pair. These rules apply only while GCN shaders are decoded. ### Functional +- [Pixel interpolators past `SPI_PS_IN_CONTROL.NUM_INTERP`](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp) (shader recompiler) - when the IR reads an attribute at or past the guest interpolator count, the missing `SPI_PS_INPUT_CNTL` slots are filled as DEFAULT (`OFFSET` 0x20, constant 0/1) before info collection, and SPIR-V emission of `GetInterpolationParameter` for a missing binding returns the same defaults instead of throwing, matching Mesa's treatment of a PS input with no VS output. Component and mode bounds still throw. A retail title prepares a pixel shader with `NUM_INTERP` 0 while it still issues `v_interp` on attribute 0 (`undefined attribute` after the collector fill alone). Console behaviour for that pairing is not measured. +- [Windows cache housekeeping](../../../core/shader/recompiler/ShaderDiskCache.cpp): eviction of old versions and temporary files is skipped for `\\?\` and `\\.\` cache roots because the supported WinLibs directory iterator can enumerate the process working directory for long prefixed paths. The active version's `last-used` stamp update still runs; it fails for long `\\.\` aliases with the supported toolchain. Ordinary Windows directory enumeration remains subject to MAX_PATH. + +- [f64 compares](../../../core/shader/recompiler/Translation/src/CompareInstructions.cpp) flush denormal inputs to signed zero when bit 0 of the `FLOAT_MODE` f16/f64 denormal field is clear (modes 0 and 2); modes 1 and 3 and an unknown mode keep them. This follows the RDNA2 ISA section 6.4 and LLVM's sign-preserving AMDGPU denormal modes. The gfx1036 audit in [#981](https://github.com/boykopovar/AnyPS5/issues/981#issuecomment-6070046381) reports mode-sensitive `VopcCompareF64` results. Class compares still inspect the original bits. The full four-mode matrix has not been newly measured on AMD hardware or the PS5's gfx1013. +- [Unlinked calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) - the `s_swappc_b64` a table call's compare chain falls back to, when its target matches no linked copy, records the `UnlinkedCall` fault with the target and the program counter and stops the invocation, or continues after the call in programs whose faults do not stop invocations (see `bdaStopsInvocations`). The driver lists those calls in the request (`linkedCallFallbacks`), and it reports the fault like a BDA fault, naming the target: a dispatch that checks its faults when it completes throws, one whose check runs in a deferred write-back logs it and continues. The hardware would jump to the target, which is not modelled. Any other `s_swappc_b64` whose target is not resolved still throws when the shader is compiled. - [Cached source IR](../../../core/shader/recompiler/Recompiler.cpp) releases lazily allocated SSA construction tables after resource preparation and reclaims values unreachable from instructions, metadata and resource plans when retaining a pending source program. Immediate shader preparation skips reclamation because compilation consumes the program. Reclamation must account for every new IR pointer owner; a remaining external use throws. Live IDs are retained. Source and variant entries remain cached without an eviction bound. A pending source program is released after its first variant is obtained; memory beyond the measured title checkpoint and Linux performance remain unmeasured. -- [Vertex user-data tail jumps](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) link a linear raw vertex prolog's terminal `s_setpc_b64` through an unchanged user-data pair to its continuation. The instruction semantics follow AMD RDNA2 ISA section 12.3; the prolog convention follows Mesa `aco_select_vs_prolog.cpp` at `0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8`. Console hardware has not been measured. Computed pointers, branching prologs, fragment epilogs and continuations reading `s_getpc_b64` remain unsupported +- [Vertex user-data tail jumps](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) link a linear raw vertex prolog's terminal `s_setpc_b64` through an unchanged user-data pair to its continuation. The instruction semantics follow AMD RDNA2 ISA section 12.3; the prolog convention follows Mesa `aco_select_vs_prolog.cpp` at `0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8`. Console hardware has not been measured. Branching prologs, computed low pointers and continuations reading `s_getpc_b64` remain unsupported +- [Raw fragment user-data tails](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) also link a terminal `s_setpc_b64` when the low half is unchanged user data and the last high-half writer is an unconditional immediate `s_mov_b32`. Other computed high halves remain unsupported. This follows AMD RDNA2 ISA section 12.3 and Mesa `aco_select_nir.cpp` (`create_fs_jump_to_epilog`), `aco_isel_helpers.cpp` (`convert_pointer_to_64_bit`) and `aco_lower_to_hw_instr.cpp` (`p_jump_to_epilog`) at `0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8`; console hardware has not been measured - The [resource tracker](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) plans a [descriptor source](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) used only behind a conditional branch as inactive when the branch condition reads only clean (specialization) memory and so is evaluable at capture time; an inactive source is bound as zero words and not validated. Other conditions (EXEC, VCC, SCC set by vector or non-clean data, loop headers) leave every source active, as before, and a branch on a value that changes between dispatches is a specialization key like any other clean slot. PPSA15246 has a copy kernel that selects a buffer or an image path from an SRT word and reads the same address as a V# or a T#; before, the untaken path's descriptor was validated and threw. Whether the console would also accept an invalid descriptor in a path it does not take is assumed, not measured +- [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. A selected column (a T# the SRT walker evaluates) holds its one record wherever it lies. +- [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read by `s_load` through a 64-bit pointer hold the keys the key's known bits allow, at most 65536; a key past them, or an entry at a wrapping, unaligned or unmapped address, faults when selected. +- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. A shader whose selected records would pass 64 images or 64 sampled image/sampler pairs binds, for the heaps with the most such reads, the union of their keys in one table instead. +- [Image table pointer indexing](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) propagates 32-bit value bounds through `ReadFirstLane` and `SelectU32` so waterfall key masks keep their bit domain. The SRT walker validates runtime select conditions whose active lane mask implies the condition through `LogicalAnd` conjunctions, assuming unselected inactive lanes need not be uniform across the full wave. The same assumption lets the two `s_load`s of one T# record match when one offset is added before `v_readfirstlane` and the other after (`ReadFirstLane(Select(mask, (key << 5) + 16, old), mask)` against `ReadFirstLane(key, mask) << 5`): with no active lane the hardware reads lane 0 of the inactive arm, and the table still binds the whole record. +- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. +- Direct [FMASK image loads](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read uncompressed `Fmask32_S8_F8` through a single-mip 2D or 2D-array `R32_UINT` snapshot with `Z_64KB_X` addressing. Destination selectors may name X, zero or one. Other FMASK formats, CMASK compression, packed accesses and other layouts throw. The layout follows PAL's [FMASK-to-surface conversion](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L877) and [32-bit view selectors](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Device.cpp#L4973). Native RDNA/PS5 image-load results and title rendering are unmeasured; compressed color sampling remains unsupported. +- `image_store`, image atomics, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. A T# whose words the SRT walker evaluates is bound directly instead, for every operation of the shader on it (`DirectImageHandles`), so only T#s keyed per lane or by a value the walker cannot evaluate reach a table with these operations; such a direct T# is read when the work is recorded, so a key its own dispatch writes is read before that write. +- 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. +- 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. -- [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. +- [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 pixel shaders of a retail title compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding, the format word of a buffer accessed with the descriptor's format (`buffer_load_format_*`; as runtime values every format's decoder is compiled, which made CI's lavapipe JIT ten times slower) and the vertex attribute numeric class stay specialized, so shaders with the first two, and pixel shaders whose image needs a multisampled descriptor, get no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. Compute and the other stages stay specialized: in registration form the `agc_driver_dword_pattern_fill` compute shader hung CI's lavapipe (Mesa 25.2.8) when built with `ANYPS5_ENABLE_SPIRV_TOOLS=ON`, not on RADV, and the cause was not found. - [SPIRV-Tools optimization](../../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. - [SRT signed comparison](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) evaluates `SLessThan32` as signed 32-bit less-than, matching the existing SPIR-V backend and [SPIR-V `OpSLessThan`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSLessThan). Signed boundary descriptor expressions are tested on the host; native RDNA2/PS5 execution and title behavior remain unverified. - [MIMG `tfe` and `lwe`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, the LOD warning (texture-streaming feedback, like the T# `MIN_LOD_WARN` itself) is never reported, and `lwe` on `image_get_lod` throws. @@ -32,92 +63,119 @@ - [2D stores to 1D storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp): an `image_store` with DIM 2D through a 1D T# writes texel x and ignores y, as measured on RDNA2 (gfx1035) for y from 0 to 4 and 0xffffffff, single- and four-component formats and a 1x1 8_8_8_8_UNORM T#: the T# type decides the addressing, DIM only how many address VGPRs are read. Storage loads, atomics and `image_store_mip` through such a pair still throw - [MIMG PCK2/PCK4 loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_pck2/pck4`, opcodes 0x70, 0x71, 0x73 and 0x74) were measured on RDNA2 (gfx1035) with the [hardware oracle](../HW_ORACLE.md) for 2D images with identity swizzle. With data mask 0x1, they write one dword: the raw bits of the N texels (PCK2: 2, PCK4: 4) from `x & ~(N - 1)` on row y, concatenated (element k at bit `k * texel bits`) and zero-extended. The dword is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when N texels fit in 32 bits: R8, R16 or RG8 for PCK2, R8 for PCK4. Wider texels (R32, RG16, RGBA8) and masks other than 0x1 write zeros on hardware. Those masks throw at decode, and formats outside UNORM/UINT/SINT R8, R16 and RG8 throw. #1314's earlier measurement saw only zeros because it used masks 0x3 and 0xf. The stores (0x76, 0x77, 0x79 and 0x7a) decode but throw at translation. They are not measured, and opcode 0x76 on 32_32_32_32_FLOAT hung the GPU (#1314). - [3D image accesses bound to a 2D or 2D-array T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) follow CouchRiv's [hardware oracle](../HW_ORACLE.md) measurement on RDNA2 (gfx1036, #2023) and adriadam10's (gfx1035, #2140), not the PS5's gfx1013. On a 2D T#, `image_sample_lz` ignores r and reads the (u,v) texel, `image_load` and `image_load_mip` read the (x,y) texel when z is 0 and return zero otherwise, and `image_get_resinfo` returns (width, height, 1, levels). On a 2D-array T#, these accesses behave like dim 2D-array: r or z is the layer, and `image_get_resinfo` returns the layer count as its third component. Every other access of such an image, including samples with an LOD, bias, derivatives, offset or comparison, gathers, `image_get_lod` and stores, keeps throwing because it is not measured. +- [2D-array samples through a 3D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) (`image_sample`, `image_sample_l` and `image_sample_lz` with DIM 2D_ARRAY) sample the volume as DIM 3D does: the third address VGPR is the normalized r coordinate, addressed by the sampler's Z mode (only wrap was measured), not a layer index, and a plain sample in a pixel shader takes its LOD from the derivatives of all three coordinates. Measured on RDNA2 (RX 6700 XT, gfx1031, driver 32.0.21045.5002, Windows 11) with the [hardware oracle](../HW_ORACLE.md) template run through the driver's comgr and HIP runtime, not on the PS5's gfx1013: on 16x16x4 R32_FLOAT 3D T#s with one and three levels, the three instructions with DIM 2D_ARRAY returned the same value as with DIM 3D in all 256 lanes of 66 cases (point, bilinear and z-linear filters, wrap addressing, r from -0.125 to 3, quad steps of 1 to 4 texels in u and v and of 0 to 4 slices in r, LOD 1). Other samples of such a pair (bias, LOD clamp, derivatives, offsets, comparison, 16-bit addresses), gathers, `image_get_lod` and a sample mixed with a load of the same image keep throwing because they are not measured. The host sampler ignores `Z_FILTER`, as for every 3D sample, so r is filtered with the XY filter. +- [2D accesses through a cube T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) (#4139) read the T#'s `BASE_ARRAY` face as a cube access with face coordinate 0 does, measured on RDNA1 (gfx1010, RX 5700 XT) with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime on 8x8 and 32x32 32_FLOAT cubes (`DEPTH` 5 and 11, `BASE_ARRAY` 0 and 6): `image_sample_lz`, plain `image_sample` (one level, and six levels in 2x2 compute quads) and `image_sample_l` (the third address VGPR is the LOD, integer and fractional) with DIM 2D equal DIM CUBE with face 0 on every row, with point and bilinear filters, wrap, clamp and mirror addressing and `DISABLE_CUBE_WRAP` 0 and 1, and the VGPR after the two coordinates is not read; `image_load` with DIM 2D reads texel (x, y) of the `BASE_ARRAY` face. So the face coordinates are cube coordinates in [1, 2]: the texel is `floor((u - 1) * width)`. They are translated through the cube mode DIM CUBE accesses use, which samples the face of the 2D-array view at (u - 1, v - 1) with the S# addressing. That matches the hardware inside the face, except within half a texel of its edges with a bilinear filter and `DISABLE_CUBE_WRAP` 0, where the hardware blends texels of the neighbouring faces (also measured on RDNA2, gfx1035, 8x8 cubes with `BASE_ARRAY` 0 and 6). Outside it the hardware clamps a coordinate below 1 to the first texel under every filter, wrap mode and `DISABLE_CUBE_WRAP` setting measured, clamps a coordinate of 2 or more to the last texel when `DISABLE_CUBE_WRAP` is 0 (point filter) and applies the S# addressing to it when it is 1; the translation does none of this, as for DIM CUBE. Only `image_sample_lz`, plain `image_sample`, `image_sample_l`, mip-less `image_load`, and mip-less `image_store` to an image the program only stores to take this mode. Such a store writes texel (x, y) of the `BASE_ARRAY` face, measured on RDNA2 (gfx1036, [by simonbbby](https://github.com/boykopovar/AnyPS5/pull/6799#issuecomment-6217609589), not the PS5's gfx1013) with the [hardware oracle](../HW_ORACLE.md) on 8x8 mip-less cube arrays (`DEPTH` 11) of 32_UINT, 32_FLOAT and 16_16_16_16_FLOAT ([the last also by simonbbby](https://github.com/boykopovar/AnyPS5/pull/6799#issuecomment-6224023050), with exactly representable half values) with `BASE_ARRAY` 0 and 6, wave32 and wave64: the 64 stored texels land on that face and every other byte of the 64 KiB backing is unchanged. Other formats, and Crimson Desert's own 256x256 tiled descriptor, are not measured. Other DIM 2D accesses through a cube T# (gathers, bias, comparison, offsets, derivatives, atomics, stores to an image the program also reads, stores of formats written through a conversion such as 32_SINT, mip loads and stores) still throw. - [Unnormalized S#s](../../../core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp) are accepted only when direct and used only by explicit-LOD `image_sample` without comparison, derivatives or `_a`, on a single-level, single-layer 1D or 2D view of mip 0, even when the image is also sampled through a normalized S#. - [Unnormalized texel offsets](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) are added to the f32 coordinates: exact on RDNA2 (gfx1035), which applies the offset before the clamp ([measured by adriadam10](https://github.com/boykopovar/AnyPS5/pull/822#issuecomment-6033192725)). - [Cube image gradients](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (shader recompiler) - `image_sample_d` and `image_sample_cd` on a cube pass their gradients to the host sample without the -1.0 that moves the face coordinates from [1, 2] to [0, 1], as Mesa's AMD lowering builds them (`src/amd/common/nir/ac_nir_lower_tex_coords.c`, `prepare_cube_coords`: face-space derivatives with no offset, coordinates offset by 1.5). Measured on RDNA2 (gfx1035) with the [hardware oracle](../HW_ORACLE.md): on a 16x16 32_FLOAT cube with five levels, point mip filter, `image_sample_d` reads the level of `round(log2(16 * max(|ddx|, |ddy|)))` for gradients of 2^(level ± 0.3) texels on each of the four components, both signs, mixed components, every face and two coordinates (1032 rows). `image_sample_cd` takes one level per quad of lanes, from the gradients of its first lane; the host sample computes it per lane. -- [Image atomics](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) on 32_SINT and 32_FLOAT surfaces act on the raw dword through an R32_UINT storage view; their float results are assumed to match those measured on 32_UINT surfaces. Other formats throw. -- [`s_memtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on RADV with a GFX10.3 to GFX11.7 GPU (RDNA 2, RDNA 3, RDNA 3.5) reads the Vulkan device clock instead of the subgroup clock, which [ACO](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/compiler/instruction_selection/aco_select_nir_intrinsics.cpp) reads from the 20-bit `SHADER_CYCLES` register there and which wraps every 2^20 cycles. The device clock is a monotonic 64-bit counter, but it counts the 100 MHz reference clock (the same one as `s_memrealtime`), not shader cycles, so `s_memtime` deltas are about 20 times smaller than on hardware. The families are matched by the name RADV reports ([`NarrowSubgroupClock`](../../../core/libs/prx/libSceAgcDriver/Execution/src/SubgroupClock.cpp)); a family added to Mesa later reads the subgroup clock until it is listed. AMD's proprietary Windows driver also exposes a 20-bit subgroup clock on device 0x1114 (Radeon 860M, measured in #1615), device 0x744c (Radeon RX 7900 XT, measured in #1632), and device 0x747e (Radeon RX 7800 XT, measured in #1632); other AMD proprietary devices remain unverified. +- [Image atomics](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) on 32_SINT and 32_FLOAT surfaces act on the raw dword through an R32_UINT storage view. On gfx1100 (RDNA3, [hardware oracle](../HW_ORACLE.md), wave32 and wave64), all 13 integer image atomics, `swap` to `dec`, return the old dword and store the raw-dword result identically on 32_UINT, 32_SINT and 32_FLOAT T#s, over 121 old/data pairs per op, among them 0x80000000, 0xffffffff, 1.0, -1.0 and NaN: `add` adds the bits and `umin` is unsigned on every format. A 16_UINT control differs, so the format is read. gfx1031 (RDNA2) gives the same rows, and its `fmin`, `fmax` and `fcmpswap`, which gfx1100 lacks, also match on the three formats ([measured by thuanquang](https://github.com/boykopovar/AnyPS5/pull/7939#issuecomment-6275099482)). The console's gfx1013 is unmeasured. Other formats throw. +- [`s_memtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on RADV with a GFX10.3 to GFX11.7 GPU (RDNA 2, RDNA 3, RDNA 3.5) reads the Vulkan device clock instead of the subgroup clock, which [ACO](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/compiler/instruction_selection/aco_select_nir_intrinsics.cpp) reads from the 20-bit `SHADER_CYCLES` register there and which wraps every 2^20 cycles. The device clock is a monotonic 64-bit counter, but it counts the 100 MHz reference clock (the same one as `s_memrealtime`), not shader cycles, so `s_memtime` deltas are 22.35 times smaller than on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title), where it counts the ~2.23 GHz core clock (44,041,632 against 1,970,792 `s_memrealtime` ticks). The families are matched by the name RADV reports ([`NarrowSubgroupClock`](../../../core/libs/prx/libSceAgcDriver/Execution/src/SubgroupClock.cpp)); a family added to Mesa later reads the subgroup clock until it is listed. AMD's proprietary Windows driver also exposes a 20-bit subgroup clock on device 0x1114 (Radeon 860M, measured in #1615), device 0x744c (Radeon RX 7900 XT, measured in #1632), and device 0x747e (Radeon RX 7800 XT, measured in #1632); other AMD proprietary devices remain unverified. - [`s_memtime` and `s_memrealtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on llvmpipe read only the low 32 bits of the host's nanosecond clock at either scope: Mesa's [`emit_clock`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/gallium/auxiliary/gallivm/lp_bld_nir_soa.c) builds the high dword with a left shift by 32, so it is always 0 and both counters wrap every 2^32 ns (about 4.3 s). The [shader clock test](../../../core/libs/prx/libSceAgcDriver/tests/execution/ShaderClock.cpp) compares them modulo 2^32 there - [`ds_wrxchg2_rtn_b32`, `ds_wrxchg2st64_rtn_b32`, `ds_wrxchg2_rtn_b64` and `ds_wrxchg2st64_rtn_b64`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) are translated as two separate 32-bit or 64-bit atomic swaps per invocation. RDNA2 hardware (measured on gfx1035) performs every lane's first exchange before any lane's second one, so when lanes of the same wave or of different waves overlap addresses, the result can differ from hardware. `offset0 == offset1` throws, although on hardware it is two exchanges in order. - [`ds_condxchg32_rtn_b64`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) uses two 32-bit atomic conditional exchanges. RDNA2 (gfx1035) measurements confirmed aligned return values, conditional bit clearing and 16-bit offsets; out-of-range accesses return zero without modifying LDS. Misaligned accesses fault the wave on gfx1035; the recompiler does not model this fault. Radeon 780M (gfx1103, RDNA3) measurements confirmed aligned return values, conditional bit clearing, EXEC masking and operand overlap in wave32 and wave64; contended cases passed per-dword conservation checks. Contended lane ordering and gfx1013 remain unmeasured; pair-wide indivisibility is not assumed, and concurrent 64-bit LDS atomics use a separate lock. GDS throws. VGPR pairs extending beyond v255 throw; allocated-VGPR EXEC nullification is not emulated. - [64-bit image atomics](../../../core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp) (`image_atomic_*` with a two-dword data mask) need `VK_EXT_shader_image_atomic_int64` and act on 32_32 UINT, SINT and FLOAT surfaces through an R64_UINT storage view. 64-bit `image_atomic_inc`, `image_atomic_dec` and the float atomics throw, as does any other access in the same shader to an image it updates with 64-bit atomics. -- [8_SRGB and 8_8_SRGB loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) on a device that cannot sample R8_SRGB or R8G8_SRGB return the sRGB EOTF rounded to nearest even at bf16 precision, as adriadam10 measured on RDNA2 (gfx1035) in #866; the PS5's gfx1013 is not measured. -- [Samples and gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures throw on a device that cannot sample R8_SRGB or R8G8_SRGB. +- [8_SRGB and 8_8_SRGB loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) on a device that cannot sample R8_SRGB or R8G8_SRGB return the sRGB EOTF rounded to nearest even at bf16 precision, as adriadam10 measured on RDNA2 (gfx1035) in #866; the PS5's gfx1013 is not measured. This is the path on a device that cannot blit R8 or RG8 to RGBA8, and with `APS5_SRGB_SHADER_DECODE=1`; otherwise a load of a sampled texture reads the RGBA8 sRGB copy and gets the device's own decode. +- [Samples and gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures read an RGBA8 sRGB copy of the texture (libSceAgcDriver, [Texture.cpp](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp)) on a device that cannot sample R8_SRGB or R8G8_SRGB but can blit R8 or RG8 to RGBA8, so they use that device's own decode. They still throw on a device that cannot blit, and with `APS5_SRGB_SHADER_DECODE=1`. `APS5_SRGB_SHADER_DECODE=copy` also takes the copy on a device that samples these formats natively, so the copy can be tested on any device that can blit R8 or RG8 to RGBA8; on a device that cannot blit, it behaves like `APS5_SRGB_SHADER_DECODE=1`. A multisampled or depth view of these formats gets no copy. - [Bindless image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) entries of 8_SRGB and 8_8_SRGB textures read zeros, like converted formats, on a device that cannot sample R8_SRGB or R8G8_SRGB. -- [Shader MODE register access](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) reads the initial rounding fields from `GuestContext::floatMode`; missing metadata retains the legacy round-to-nearest-even assumption. Immediate rounding writes are accepted only when they preserve that state. State-changing or SGPR-sourced MODE writes throw until runtime mode tracking and affected arithmetic are implemented. Non-rounding register reads still throw; `s_setreg_b32` writes to other hardware registers are still ignored. Tests follow the public RDNA2/LLVM contract, not console measurements. +- [Shader MODE register access](../../../core/shader/recompiler/Translation/src/RoundingMode.cpp) starts from the rounding fields of `GuestContext::floatMode` and follows immediate rounding writes (`s_round_mode`, `s_setreg_imm32_b32` on MODE bits 0-3) through the control flow graph; `s_getreg_b32` of those bits returns the tracked value. Only `v_cvt_f32_i32`, `v_cvt_f32_u32`, `v_ldexp_f32`, the f16 conversions and `v_ldexp_f16` (with f16 denormals kept) and the instructions that already read `FLOAT_MODE` follow a rounding mode set at run time; any other float instruction that runs while a directed rounding mode differs from the initial one throws at that instruction. Rounding bits written from an SGPR (`s_setreg_b32`) or that differ between the paths reaching an instruction throw where they are read or used. A shader without `FLOAT_MODE` metadata throws on any rounding change. Non-rounding register reads still throw; `s_setreg_b32` writes to other hardware registers are still ignored. Tests follow the public RDNA2/LLVM contract, not console measurements. +- [Flat scalar-read reachability](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) keeps both successors active for non-clean predicates and loop headers, so an inaccessible direct root on such a path still refuses capture. Skipping a read under a clean, evaluable scalar branch follows RDNA2 conditional PC semantics and synthetic fixtures; console execution and title behavior are unverified. - A [flat SRT read](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) through a null or unmapped pointer loaded from memory raises a [BDA fault](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp) only if the program executes it, then stops or reads zero like a BDA read. - Repeated `s_load`s of one [flat SRT](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) dword through a null or unmapped pointer report one of their pcs, and an `s_load` whose inaccessible dwords are all unused, or only address repeated loads, reports no fault of its own. - A value read through a null or unmapped pointer still throws at record time if a [uniform fill or table base](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) needs it, or if the pointer comes from user data and is not null, even if the console skips the read. - A [V# or flat SRT word](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) read through a null pointer taken from user data is zero and counts as a [poisoned SRT read](../../../core/shader/recompiler/Recompiler.cpp), so a dispatch that reads it is not cached. The console faults if the program executes the read; PPSA28416 branches around it with `s_cbranch_execz`. The fault is not modeled. - A [V#, T# or S#](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) loaded through a null or unmapped pointer read from memory is zero when the work is recorded, and the capture throws if no flat SRT slot records that read. -- [2D accesses through a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; the PS5's gfx1013 is not measured. A plain `image_sample` (implicit LOD) through such a pair takes its LOD from the derivatives of the first coordinate alone, measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) on the quads of a compute kernel (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): on a 16-texel, 5-mip 32_UINT T# with point filters it picks the same mip and texel as DIM 1D for first-coordinate steps of 0, 1, 2, 4 and 8 texels per lane along x or along y, whatever the second coordinate does (normalised steps up to 16 per lane), while the same rows on a 16x16 2D T# move the mip with the second coordinate. Outside the pixel stage a plain `image_sample` is emitted with LOD 0 for every image type, so this pair reads mip 0 there, where the kernel measurement shows the quad's derivatives being used. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips, explicit LOD, bias, offsets, derivatives, comparison) still throw. +- [2D accesses through a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; `image_sample_l` was measured the same way on gfx1036 against `image_sample_l` with DIM 1D through the same T#, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, LOD -1 to 6 on a 5-level R32_FLOAT texture, point, linear and linear-mip filtering, wrap, clamp and border addressing and HEIGHT or DEPTH 3: all 4725 rows matched. The PS5's gfx1013 is not measured. A plain `image_sample` (implicit LOD) through such a pair takes its LOD from the derivatives of the first coordinate alone, measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) on the quads of a compute kernel (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): on a 16-texel, 5-mip 32_UINT T# with point filters it picks the same mip and texel as DIM 1D for first-coordinate steps of 0, 1, 2, 4 and 8 texels per lane along x or along y, whatever the second coordinate does (normalised steps up to 16 per lane), while the same rows on a 16x16 2D T# move the mip with the second coordinate. Outside the pixel stage a plain `image_sample` is emitted with LOD 0 for every image type, so this pair reads mip 0 there, where the kernel measurement shows the quad's derivatives being used. `image_sample_c_lz` and plain `image_sample_c` through such a pair compare against the texel the DIM 1D comparison with the same reference and first coordinate reads, measured on RDNA1 (gfx1010, RX 5700 XT) with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): 8-texel 32_FLOAT and 16_UNORM T#s, also with HEIGHT 3, point and bilinear filters, wrap, clamp and opaque-white border addressing, less and gequal functions, second coordinates 0, 0.5, 1, 1.5, -0.25 and 3; on a 64-texel, 7-mip 32_FLOAT T# `image_sample_c` picks its LOD from the first coordinate's steps alone (1, 4 and 16 texels per lane give mips 0, 2 and 4; second-coordinate steps up to 64 texels leave it unchanged). Only comparisons the runtime does natively (32_FLOAT, 16_UNORM) reach the 1D view; comparisons the runtime emulates for other color formats still throw through such a pair. 2D-array `image_sample_l` and `image_sample_lz` through a 1D T# read as DIM 1D too, ignoring v and the slice: measured on gfx1036 with the hardware oracle (template assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll`) against the same sample with DIM 1D on a 16-texel 8_8_8_8 UNORM T# with 1 or 5 levels, HEIGHT or DEPTH 3, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, slices 0, 1, 2.7, -1 and 5, LOD -1 to 6, and the five filter and address cases above: all 48000 rows matched. A 2D-array `image_load` through a 1D T# is not the same: on gfx1036 it returns the DIM 1D texel only at slice 0, and zeros at slices 1, 2, 5 and -1 even with DEPTH 3, so it, the plain `image_sample` and the other 2D-array accesses still throw. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips other than explicit LOD, explicit LOD with comparison, bias, offsets, derivatives) still throw. +- [3D image accesses bound to a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) follow a [hardware oracle](../HW_ORACLE.md) measurement on RDNA1 (RX 5700 XT, gfx1010; wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even), not the PS5's gfx1013, on 8x1 R8G8B8A8_UNORM linear 1D T#s with HEIGHT and DEPTH 0 or 3 and point filters. `image_sample_lz` reads the texel u selects for v and r in {0, 0.5, 1, 1.5, -0.25, 3}, with wrap, clamp and border addressing, and v and r never produce the border; u wraps, clamps and borders as with DIM 1D. Mip-less `image_load` reads the x texel for any y in {0, 1, 2, 3, -1} when z is 0, returns zero for z in {1, 2, 3, -1}, whatever DEPTH says, and returns zero for x past the width. A plain `image_sample` (implicit LOD) on the quads of a compute kernel takes its texel and LOD from u alone: on a 64-texel, 7-mip 1D T# steps of 1, 4 and 16 texels per lane pick mips 0, 2 and 4, as DIM 1D `image_sample_l` does, and steps in v or r alone stay at mip 0. Outside the pixel stage a plain `image_sample` is emitted with LOD 0, as for 2D accesses through a 1D T#. Every other 3D access of such an image (`image_load` with mips, storage, explicit LOD, bias, derivatives, offsets, comparison, gathers, queries) keeps throwing. SONIC SUPERSTARS (PPSA06888) samples a 1x1 1D T# this way with plain `image_sample` and `image_load`. - [Emulated comparison of a 2D-array access through a 2D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) samples the 2D image and ignores the slice, as the native modes of a 2D-array access through a 2D T# already do. This follows the T# type deciding the addressing; it is not measured on hardware for comparison samples -- [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035: `src_shared_base` 0x00010000_00000000, `src_shared_limit` 0x00010000_ffffffff). The PS5's base values, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), are inferred from GTA V's traversal addresses, not measured on a console or read from SH_MEM_BASES. The scalar aperture sources read the same two values +- [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035). How the console routes a flat address was not measured. Its bases, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), first inferred from PPSA04263's traversal addresses, are measured: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) `src_shared_base` / `src_shared_limit` read 0x80000000_00000000 / 0x80000000_FFFFFFFF and `src_private_base` / `src_private_limit` 0x70000000_00000000 / 0x70000000_FFFFFFFF. The scalar aperture sources read the same two values - [Flat accesses outside the aperture routing](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - only compute stages route flat accesses, so graphics-stage flat accesses to the shared or private aperture still go to BDA. In compute, a 64-bit flat atomic whose address is in the shared aperture takes the workgroup lock (`ldsLock`) for its two-dword update, and every such atomic requests that lock, so a shader with one pays for it even on global addresses (the path is exercised by `agc_driver_global_atomics_float`). Inferred, not measured on LDS or scratch: an aperture access that is not inside one dword is done byte by byte, an unaligned aperture atomic records the BDA unaligned fault, and LDS or scratch accesses past their end are dropped or read zero. - On a host subgroup narrower than 32 lanes, [`threadBit`](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. -- [EXEC read as a value](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) (`s_mov_b64 sN, exec`, the `s_*_saveexec` copy, ALU sources) holds only lanes with a host invocation; the console's also holds the wave's other lanes, e.g. 32-63 of a wave64 on 32 host lanes. +- [EXEC read as a value](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) (`s_mov_b64 sN, exec`, the `s_*_saveexec` copy, ALU sources) is limited to host lanes. Pixel reads retain helper bits in quads with an active fragment, preserving whole-quad masks across copies. Quad expansion follows the [RDNA 2 ISA, section 12.3, S_WQM_B32/B64, page 111](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf#page=119); save/restore followed by implicit-LOD sampling is checked on an RX 6700 XT, not on a console. The console also holds lanes the host lacks, e.g. 32-63 of a wave64 on 32 host lanes. - [`v_readlane_b32`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) and fetch-inactive `v_permlane16_b32`/`v_permlanex16_b32` read undefined data from a lane that the host subgroup does not hold or whose host invocation is inactive; the console reads the lane's VGPR. - Without fetch-inactive, [`v_permlane16_b32` and `v_permlanex16_b32`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take a source lane that the host subgroup does not hold as inactive and keep the old value (0 with bound_ctrl); the console reads that lane's VGPR. - On a host subgroup narrower than the wave, [`v_readlane_b32`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) of a constant lane outside it builds only as lane 63 of a half-wave scan (DPP row_shr 1/2/4/8, `v_permlanex16_b32`, EXEC -1) or, in a program whose entry EXEC is every lane (vertex, local and tessellation stages), as 0 when the value is 0 in lanes 32-63; other such reads throw. A value is 0 there when it is built from 0 through selects under a lane index (lane id or `v_mbcnt` of -1) below at most 32, `v_cmp_ne_u32` against 0, OR, DPP and permlanes; the counts in `s3` give such a bound on a 32-lane host. On a 32-lane host a half-wave scan may also run under `s_mov_b64 exec, -1`, use bound_ctrl:1 steps for an OR, add or unsigned max, and take keys that are 0 in lanes 32-63; its lane 31 is then the subgroup reduction of the host's invocations (a shuffle of the scan on a device without subgroup arithmetic): idle invocations add the identity, where the console would add the keys of the lanes `s3` claims. - [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 +- [Mesh layer output](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a mesh shader writes each primitive's layer through a one-member `Block` struct array whose member is decorated `BuiltIn Layer` and `PerPrimitiveEXT`, as glslang declares `gl_Layer` inside `gl_MeshPerPrimitiveEXT`, not through a standalone `PerPrimitiveEXT` `Layer` array. Both forms pass `spirv-val`, but AMD's Windows driver (32.0.21045.5002, RX 6700 XT) crashes in `amdvlk64.dll` creating a pipeline whose mesh shader writes the standalone form, whether the layer is signed or unsigned and whatever the target, while the block form draws, also with a pixel shader that reads the layer. - [Shader recompilation](../../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../../core/shader/recompiler) was written completely independently from [libs/prx](../../../core/libs/prx). -- [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric` 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 or, on a device without the Float64 capability, in 64-bit integers: the exact product is added with a sticky bit and rounded once. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the integer arithmetic on every device, and the f64 arithmetic on devices with Float64. It checks both against the hardware rows in every mode, and against a CPU reference on random rows in every mode and f16 rounding. +- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the provoking vertex's value there, the first or with `PROVOKING_VTX_LAST` the last, and every corner of the patch writes it, so the host's own provoking vertex does not matter and `VK_EXT_provoking_vertex` is not needed; a flat per-vertex parameter with the last provoking vertex still throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. Tessellation, mesh and rect-list pipelines, primitives other than triangles, triangle fans and primitive restart 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. +- [v_floor_f64, v_ceil_f64, v_fract_f64, v_frexp_mant_f64 and v_frexp_exp_i32_f64](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field: a denormal input is flushed to a zero of its sign when bit 0 is clear (0 and 2), and `v_fract_f64` flushes a denormal result, the fraction of a positive denormal input, to +0 when bit 1 is clear (0 and 1). `v_trunc_f64` and `v_rndne_f64` give the same results either way and are unchanged. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 over 23 f64 values (zeros, denormals, minimum normals, ±0.5, ±1, values next to 1, the largest finite value, infinities and quiet and signaling NaNs) and 16 of them with neg and abs modifiers. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. +- [v_add_f64 and v_mul_f64 denormals](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field; the PS5's gfx1013 is not measured. Inputs flush to a zero of their sign when bit 0 is clear (0 and 2). When bit 1 is clear (0 and 1), a sum below 2^-1022, or a product whose exact value rounded to 53 significant bits with an unbounded exponent is below 2^-1022, flushes to a zero of its sign, so `0x2000000000000001 * 0x1ffffffffffffffe` (exactly 2^-1022 - 2^-1126) rounds to 2^-1022 and is kept. Measured on RDNA2 (gfx1031, RX 6700 XT, AMD Windows driver 32.0.21045.5002, HIP) at IEEE=0 and 1, denorm16 0..3, over 576 pairs of 24 f64 values (zeros, denormals, minimum normals, values next to 1 and to 2^-1022, products just below 2^-1022, the largest finite value, infinities), with neg, abs and clamp. `v_fma_f64`, `v_div_fmas_f64` and shaders without a known float mode keep the previous lowering. +- [SINT storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. A D16 store sign-extends each 16-bit half before that saturation, as measured on RDNA2 (gfx1035) for `image_store d16` to all eight SINT formats with DMASK 0xf, 0x7, 0x5, 0x3, 0x2, 0xa and 0x1 (1792 rows match). - [`StoreTexel`](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) writes channel 1 of a two-channel format from the data slot whose DST_SEL is Y, for SINT and UINT alike. With DST_SEL X,Y,Z,W, gfx1035 writes it from the W slot, as adriadam10 measured in #944. - [v_ldexp_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f16 denormal field: bit 0 preserves the half input and bit 1 the half result. Otherwise a denormal input is flushed to its signed zero, and a result whose exact scaled value is below the minimum normal (2^-14) is flushed to its signed zero before rounding, so `v_ldexp_f16 0x07ff, -1` gives +0 instead of 0x0400. Measured on RDNA2 (gfx1036) with round to nearest, IEEE 0 and 1 and denorm16 0..3, over every half magnitude 0x0000..0x0fff of both signs scaled by 2^-1 and 2^0 (16384 rows per mode): no mismatch. NaN and infinity handling and destination high-half preservation are unchanged. Shaders without a known float mode keep the previous lowering. +- [v_div_fixup_f64](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) flushes its src1 and src2 to a zero of the same sign when bit 0 of the `FLOAT_MODE` f16/f64 denormal field is clear (0 and 2) and keeps a denormal src0 and result in every mode, as `v_div_fixup_f32` does with the f32 field. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 1728 triples of 12 f64 values (zeros, denormals, the minimum normal, ±1, the largest finite value, infinity and quiet and signaling NaNs) at IEEE=0 and IEEE=1 with denorm16 0..3. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [v_mullit_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted only with IEEE=1, as measured at IEEE=0 too; a positive denormal src2 counts as positive); with f32 denormals flushed it follows the f32 denormals entry below, and denormal modes 1 and 2 throw. - [v_mad_legacy_f32 and v_mac_legacy_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140, VOP2 0x06) round the product to f32 before the add, give a +0 product for a zero factor, and flush their f32 denormal sources, their product and their result to a zero of the same sign in every f32 denormal mode, an exception to the `FLOAT_MODE` model in [f32DenormalFlushFor](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp). Measured on gfx1010 (RX 5700 XT) with `tools/hw-oracle/template.s`, assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll` instead of `oracle.c` and ROCr, over the 32 rows of the five `FmaLegacy.cpp` and the four `MacLegacy.cpp` encodings: identical in wave32 and wave64 at IEEE 0 and 1 with f32 denormal modes 0 to 3, except that the two signaling NaN rows are quieted only with IEEE=1 (#1281, #1290). A sum of two zeros takes its sign on the bits (+0 unless both are -0), as lavapipe returned -0 for +0 + -0. The PS5's gfx1013 is not measured; LLVM decodes the encoding as `v_mad_legacy_f32` there, as on gfx1010. RDNA2 differs: on gfx1031 (RX 6700 XT, measured by pandather in #1281) the encoding is `v_fma_legacy_f32` and fused, with the same zero rule and denormal-source flush, so an RDNA2 measurement is not evidence for the console. -- [v_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. -- [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. +- [v_mad_f32, v_mac_f32, v_madak_f32 and v_madmk_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (gfx10.1 only) leave f32 denormals and NaN bits to the driver, unlike the console. Measured on gfx1010 (RX 5700 XT, #5782): the product is rounded before the add; f32 sources, product and result are flushed to a zero of the same sign in every f32 denormal mode; the product is flushed when tiny after rounding to 24 bits with an unbounded exponent; NaN results follow the fma rule, an invalid product beating a NaN src2; in round mode 2 a sum of zeros of opposite signs is -0. Translating this cost 15× `main` on a `v_mac_f32` chain on the RX 5700 XT and 23× on an RTX 3070, and a never-taken branch around it 6×, so it waits for the SPIR-V float controls of #981. +- [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the arithmetic of v_interp_p2_f16, in f64 or, without the Float64 capability, in 64-bit integers. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. With f16 denormals kept, every f16 rounding mode is translated: the exact product-sum is rounded once in the mode's direction. All four modes match RDNA2 (gfx1036) on the 2,233 rows of the #2631 sweep, including `FLOAT_MODE` 0xcc (toward zero, used by ps5-opengl) and the exact cancellations toward -infinity; the PS5's gfx1013 is not measured. A result beyond 65504 becomes infinity or 65504 as IEEE 754 gives for the mode. Flushed f16 denormals still throw, and packed f16 throws with any f16 rounding but nearest even. `FP16_OVFL` set is not measured. +- [Signaling NaN inputs](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) of the f32 and f16 unary instructions (`v_sqrt/rcp/rsq/log/exp/sin/cos/fract/floor/ceil/trunc/rndne/rcp_iflag/frexp_mant`), `v_ldexp_f32/f16`, `v_frexp_mant_f16`, `v_div_fixup_f32/f16` and `v_mullit_f32` are returned quieted only with IEEE=1 and unchanged with IEEE=0, measured on RDNA2 (gfx1035) in both modes, and so are those of the f64 forms (`v_sqrt/rcp/rsq/fract/floor/ceil/trunc/rndne/frexp_mant/ldexp/div_fixup_f64`) and `v_cvt_f64_f32`, measured on RDNA2 (gfx1036) in both modes; shaders without a known float mode follow IEEE=0. `v_pack_b32_f16` quiets each signaling NaN half by the same rule, measured on RDNA2 (gfx1036) in both modes. A signaling NaN result of `v_cubesc/cubetc/cubema_f32` is quieted by the same rule and keeps the sign the instruction gives it, measured on RDNA2 (gfx1036) in both modes. `v_add/sub/subrev/mul/fma/fmac/fmamk/fmaak/mul_legacy_f32` and `v_fma_mix_f32` with f32 sources choose their NaN result on the bits, measured over 1000 operand triples in both modes: the first NaN source in operand order after its modifiers (the subtrahend negated, as the hardware adds its negation), quieted only with IEEE=1, or the default NaN 0xffc00000 for an invalid operation; in the fma forms an invalid product (inf * 0) gives the default NaN even when src2 is a NaN, and in the legacy forms a zero factor makes the product 0 even when the other factor is a NaN (a NaN src2 still wins). `v_mad_legacy_f32` and `v_mac_legacy_f32` follow the same rule with their separately rounded product, as measured on gfx1010 (#1281, #1290): a NaN source quieted only with IEEE=1, inf - inf giving 0xffc00000, the zero rule hiding a NaN factor and a NaN src2 winning. The f16 and f64 arithmetic, `v_fma_mix_f32` with f16 sources and the separately rounded mad/mac forms (not measurable on RDNA2) follow the same rule on hardware but their NaN bits are left to the driver, except that an invalid operation of `v_fma_f16`, `v_fmac_f16`, `v_fmamk_f16`, `v_fmaak_f16` and `v_fma_mix_f32` with f16 sources (an inf * 0 product, also with a NaN src2, or a NaN from sources that are not NaN) gives the default NaN 0xfe00 or 0xffc00000, which NVIDIA does not return on its own. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f16 and f32 denormals kept and flushed, over six inf * 0 products with eight NaN src2 values in every f16 fma form (#7074); the AMD Windows driver 32.0.21042.62 on an RDNA2 iGPU (Ryzen 5 7600) instead returns src2 quieted through Vulkan in the f16 fma forms. `v_cvt_f16_f32`, `v_cvt_f32_f64`, `v_dot2_f32_f16`, `v_fma_mixlo_f16` and `v_fma_mixhi_f16` always quiet and `v_div_scale_f32` never does: the f32 signaling NaN 0x7f812345 as src0 or src2 gives 0x7e09 from mixlo and mixhi at IEEE=0 and IEEE=1 on RDNA2 (gfx1036), where `v_fma_mix_f32` gives 0x7f812345 at IEEE=0 and 0x7fc12345 at IEEE=1. The payload of an f16 NaN source of mixlo and mixhi is left to the driver's f16 to f32 conversion, which NVIDIA replaces. +- [Scalar f16 add, sub, subrev and mul denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. `v_fma_f16`, `v_fmac_f16`, `v_fmamk_f16` and `v_fmaak_f16` follow the same field at nearest-even: inputs are flushed when bit 0 is clear, and the result is flushed when bit 1 is clear if the exact product-sum rounded to 11 significant bits with an unbounded exponent is below 2^-14, so `0x0400 * 0x0400 + 0x8400` rounds to `0x8400` and is kept. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 over 5075 operand triples of `v_fma_f16` and `v_fmac_f16` (4000 from 20 halves around the denormal range, 75 whose product-sum lies just below 2^-14 and 1000 random), and over the first 4075 of them for `v_fmamk_f16` and `v_fmaak_f16`, FP16_OVFL=1, the clamp, neg and abs modifiers and op_sel. Directed rounding and `v_fma_mixlo/mixhi_f16` keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. +- [f16 unary, frexp and conversion denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_ceil_f16`, `v_floor_f16`, `v_fract_f16`, `v_rcp_f16`, `v_rsq_f16`, `v_sqrt_f16`, `v_log_f16`, `v_exp_f16`, `v_sin_f16`, `v_frexp_mant_f16`, `v_frexp_exp_i16_f16`, `v_cvt_norm_i16_f16`, `v_cvt_norm_u16_f16` and `v_cvt_f32_f16` flush a denormal half input to its signed zero when bit 0 is clear (0 and 2), and the f16-result ones flush a denormal half result to its signed zero when bit 1 is clear (0 and 1), after the host rounds it to half. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with denorm16 0..3 over every non-NaN f16 input (63,490 rows per mode and instruction): the only mismatches are the 1-ULP driver differences of `v_exp_f16` (2 inputs) and `v_sin_f16` (14), which are the same with denormals kept. NaN inputs are not compared. `v_ldexp_f16` shares the input rule. `v_cvt_pknorm_i16_f16` and `v_cvt_pknorm_u16_f16` flush both half inputs by the same bit, measured on RDNA2 (gfx1030) over every f16 input (each half also negated in the other source) at IEEE=0 and IEEE=1 with denorm16 0..3 and at f16 rounding 1..3: every row matches. Shaders without a known float mode keep the previous lowering. +- [v_div_fixup_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) flushes its src1 and src2 to a zero of the same sign when bit 0 of the `FLOAT_MODE` f16 denormal field is clear (0 and 2) and keeps a denormal src0 and result in every mode, as `v_div_fixup_f32` does with the f32 field. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 6859 triples of 19 halves (zeros, denormals, minimum normals, ±0.5, ±1, the largest finite value, infinities and quiet and signaling NaNs) at IEEE=0 and IEEE=1 with denorm16 0..3 and FP16_OVFL 0 and 1, also with neg and abs source modifiers and with op_sel high halves. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. +- [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. The [constant folder](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) keeps a field read that a multiply uses, even when the program already masked the factor to 16 bits, since folding it away brings the full product back (#8271). Measured against the same instructions run through the [hardware oracle](../HW_ORACLE.md) on that GPU; other drivers are not checked +- [f32 compares in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cmp_X_f32` and `v_cmpx_X_f32` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 484 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3, f32 round to nearest even, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0. The PS5's gfx1013 is not measured. +- [f16 compares](../../../core/shader/recompiler/Translation/src/CompareInstructions.cpp) (`v_cmp_X_f16` and `v_cmpx_X_f16` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f16 denormal inputs to a zero of the same sign by bit 0 of the `FLOAT_MODE` f16 denormal field: modes 0 and 2 flush them, 1 and 3 keep them. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 256 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f16 denormal mode 0 to 3, f16 round to nearest even, DX10_CLAMP=1, f32 denormals kept and FP16_OVFL=0. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE. - [POPS collision SGPRs](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once. - [Image loads](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) (shader recompiler) - an image_load reads a sampled binding, also of an image the shader stores to, so in a primitive-ordered pixel shader it misses an earlier overlapping primitive's image_store in the same draw. - [Tessellation rings](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) of local, hull and domain programs become Vulkan tessellation interfaces and are never written to guest memory; a title that reads the off-chip or factor ring back sees no data. -- [Hull input control points](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a hull LDS read at a control-point index computed from data reads `gl_in` at that index clamped to the patch's last input control point. On the console an index past the patch reads the LDS slot of a neighbouring patch or of no patch; the host has no such memory, and Vulkan leaves an out-of-range `gl_in` index undefined. The lowering refuses a computed offset that is not a whole number of control points, but the index itself is only known when the program runs. -- [Inner tessellation factors](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) of integer or fractional-odd triangle patches that are NaN tessellate as 1, as in the D3D11 reference tessellator; the console is not measured. +- [Hull input control points](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a hull LDS read at a control-point index computed from data reads `gl_in` at that index clamped to the patch's last input control point, and a domain off-chip read at a computed index reads `gl_in` clamped to the patch's last output control point. On the console an index past the patch reads the LDS slot or off-chip record of a neighbouring patch or of no patch; the host has no such memory, and Vulkan leaves an out-of-range `gl_in` index undefined. The lowering refuses a computed offset that is not a whole number of control points, but the index itself is only known when the program runs. +- [Inner tessellation factors](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) of integer, fractional-even or fractional-odd triangle patches that are NaN tessellate as 1 (fractional-even then raises them to 2, as Vulkan does), as in the D3D11 reference tessellator; the console is not measured. - [Hull programs](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) start from the hull launch registers (user data, ring bases, v0, v1); on the console they start with what the local program leaves, and that it leaves them unchanged is not checked. - [Quad tessellation factors](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) are taken as outer[0..3] then inner[0..1], 24 bytes per patch, as [Mesa stores them](https://github.com/mihawk-99/PS5_Mesa/blob/b3588f78dd7fd46fbadc7ecd487e5f14d4213e4b/src/amd/common/nir/ac_nir_lower_tess_io_to_mem.c); unmeasured on the console. - [VGT_HOS_MIN/MAX_TESS_LEVEL](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) clamp positive tessellation factors (inner ones to at least 1); outer factors that are zero, negative or NaN still cull the patch and such inner factors become max(HOS_MIN, 1). The console's clamp and cull order is not measured. - [Patch constants](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull programs write and domain programs read are placed by their off-chip address modulo the record stride, as the two compile apart; a domain read of a field the hull never writes, or of only a neighbouring patch's record, is not detected. -- [Integer partitioning](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (`VGT_TF_PARAM.PARTITIONING` 0) is drawn with Vulkan's equal spacing, which [radeonsi programs as `PART_INTEGER`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L539-540), with factors bounded at 64 instead of fractional-odd's 63; how the console rounds a clamped factor up to a whole number of segments is not measured. Power-of-two (1) and fractional-even (3) partitioning throw. -- The [relative patch index](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull and domain programs use as data is (PrimitiveID mod PRIM_GRP_SIZE) mod NUM_PATCHES, as if each primitive group and each instance started new threadgroups; it throws when GE_CNTL.BREAK_WAVE_AT_EOI is clear. Unmeasured; PPSA26344 programs 64 primitives for 63 patches. -- The [output modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the shader's float mode as measured on RDNA2 (gfx1035): the hardware applies it only with IEEE mode off and output denormals of the result's precision flushed (bit 1 of the `FLOAT_MODE` denormal field clear), and ignores it otherwise. On f32 results it is applied as the hardware does with f32 denormals flushed; a shader without a known float mode is translated with IEEE mode off and f32 denormals flushed, the mode titles use. On f16 and f64 results it is ignored, and it throws in the modes where the hardware applies it. +- [Integer partitioning](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (`VGT_TF_PARAM.PARTITIONING` 0) is drawn with Vulkan's equal spacing, which [radeonsi programs as `PART_INTEGER`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L539-540), with factors bounded at 64 instead of fractional-odd's 63; how the console rounds a clamped factor up to a whole number of segments is not measured. Power-of-two (1) partitioning throws. +- [Fractional-even partitioning](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (`VGT_TF_PARAM.PARTITIONING` 3) is drawn with Vulkan's fractional-even spacing, which [radeonsi programs as `PART_FRAC_EVEN`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L536-537), with factors bounded at 64. Vulkan raises every factor that is not culled to at least 2 after `VGT_HOS_MIN/MAX_TESS_LEVEL`, so a `VGT_HOS_MAX_TESS_LEVEL` below 2 still tessellates at 2; the D3D11 range for this mode is also [2, 64], but the console's order of the HOS clamp and that minimum is not measured. +- The [relative patch index](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull and domain programs use as data is (PrimitiveID mod PRIM_GRP_SIZE) mod NUM_PATCHES, with the draw-relative instance times the patches per instance added when GE_CNTL.BREAK_WAVE_AT_EOI is clear and the instances share hull threadgroups; a domain that uses it in such a draw, and a hull without shaderDrawParameters, throw. Unmeasured on hardware; PPSA26344 programs 64 primitives for 63 patches. +- The [output modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the shader's float mode as measured on RDNA2 (gfx1035): the hardware applies it only with IEEE mode off and output denormals of the result's precision flushed (bit 1 of the `FLOAT_MODE` denormal field clear), and ignores it otherwise. On f32 results it is applied as the hardware does with f32 denormals flushed; a shader without a known float mode is translated with IEEE mode off and f32 denormals flushed, the mode titles use. On f16 and f64 results it is ignored, and it throws in the modes where the hardware applies it. `v_cubeid_f32`, `v_cubesc_f32`, `v_cubetc_f32` and `v_cubema_f32`, measured on RDNA2 (gfx1036) with `DX10_CLAMP`=1, apply it whenever IEEE mode is off, in all four f32 denormal modes (`v_mul_f32_e64` in the same measurement follows the rule above), with the same bit-level result as the f32 path, and ignore it with IEEE mode on; clamp is applied after it. Only round to nearest even is modelled: in the other round modes (measured with IEEE mode off) gfx1036 rounds an overflow as the mode says (to the largest finite value when rounding toward zero), in `v_cubema_f32`'s own result and in the `mul:2` and `mul:4` scaling, where the translation gives infinity, and when rounding toward negative infinity a zero `v_cubema_f32` result with a NaN input is -0, where the translation gives +0. - The [clamp modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows `DX10_CLAMP`, measured on RDNA2 (gfx1035) for f32, f16, packed f16 and f64 results at IEEE=0 and IEEE=1: with `DX10_CLAMP`=1 (and in shaders without a known float mode) a NaN result clamps to +0, including the quieted NaN that `v_pk_min_f16` and `v_pk_max_f16` return for a signaling NaN operand with IEEE=1 (measured on RDNA2 gfx1036 at both `IEEE_MODE` and both `DX10_CLAMP` settings); with `DX10_CLAMP`=0 a NaN result is written unchanged and every other value clamps to [0, 1] as before. f64 results follow the same rule on hardware, but a clamped f64 result with `DX10_CLAMP`=0 throws until it has an execution test. With `DX10_CLAMP`=0 the NaN's bits are those the translation already produces for the instruction without clamp, so where those are left to the driver (f32 and scalar f16 arithmetic) the payload is the driver's, not the hardware's input propagation. `v_dot2_f32_f16` and the integer clamps are unchanged. - [FP16_OVFL](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows the shader's float mode as measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f16 denormals kept and flushed: with `FP16_OVFL`=1 an f16 result that rounds to ±infinity is written as ±0x7bff unless a source operand is infinite, so `v_rcp_f16`, `v_rsq_f16` and `v_log_f16` of ±0 and `v_exp_f16` of 16 or more are clamped while an infinite input passes through, and `v_div_fixup_f16` keeps the infinity it writes for a zero denominator or an infinite numerator and clamps any other infinite quotient, a NaN one included. `v_cvt_pkrtz_f16_f32` rounds toward zero and never overflows. The sources are checked as the f32 values the translation computes with; the rule is applied to `v_fma_mix*_f16` with f32 sources and to `v_interp_p2_f16` without a measurement of infinite f32 sources or parameters. Round modes other than nearest even are not modelled, so the overflow threshold is the nearest-even one (65520). -- [Directed f16 rounding](../../../core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp) for `v_cvt_f16_f32`, `v_cvt_f16_i16`, `v_cvt_f16_u16` and `v_ldexp_f16` follows `FLOAT_MODE` bits 3:2 only when f16 denormals are preserved (bits 7:6 = 3), measured on RDNA2 (gfx1036). With f16 denormals flushed (bits 7:6 = 0), or flushed on one side (modes 1 and 2, not measured), the hardware also flushes the f16 result and the directed path does not model that, so those shaders keep round-to-nearest-even. The `clamp` modifier with directed rounding is not measured. -- [Comparison sampling of a color texture](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D, 2D array and cube views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented, with texel offsets added to the texel indices; LOD biases and clamps are accepted, as they cannot select another level of a single-level sample. Half-border addressing, the border color table, gradients, gathers, other dimensions, anisotropy (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 +- [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) returns 0 for every comparison function on formats AnyPS5 can sample as floating-point measured on GFX10 (codes 1–77 except native comparison codes 7 and 22) and on the corresponding GFX11 formats measured on gfx1100. The current Vulkan path still lowers these formats through the shader; the compare result is constant zero. Scaled, integer and formats requiring converted sampling remain refused. Only 2D, 2D-array and cube views are accepted; image tables, min/max reduction, unnormalized coordinates and unsupported addressing remain refused. Point and bilinear results match on gfx1100. The PS5's gfx1013, array/cube behavior for the measured formats, and anisotropic comparisons remain unverified. Bilinear weights are full float, not the hardware's fixed-point subtexel weights - [Negative color-texture comparison wrap indices](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap has not been measured on PS5 hardware. -- [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw. +- [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view) are sampled in the shader, the way color comparisons are emulated: `image_sample` of a 2D or 2D array view fetches the one or four texels at LOD 0, decodes them and blends them, for point or bilinear filtering with equal minification and magnification filters and wrap or clamp-to-edge addressing. A shader gets these sample modes only once a converted descriptor reaches one of its sampled images: that capture throws `ConvertedSampleRequired`, the driver records the program and prepares it again with `GuestContext::convertedSamples`, so such a shader is prepared twice per run and its first capture is repeated. The record lives with the registered program and is not kept across runs. Bilinear weights are rounded to the hardware's 8-bit grid (`roundEven(fraction * 256) / 256`, ties to even), as measured with `image_sample_lz` on RDNA2 (gfx1035) for 10_11_11 UNORM texels by adriadam10 in #5958; point sampling, decoding, swizzles and wrap and clamp-to-edge addressing matched the same measurement. A sample that is not `_lz` needs an image with one mip level; gathers, LOD queries, clamp-to-border and mirrored addressing, other filters, min/max reduction, unnormalized coordinates, images selected at run time, 16-bit data and a DST_SEL of the missing W channel throw. - [Converted unorm image loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) divide by 2^n - 1 with the buffer format path's `OpFDiv`, which Vulkan allows to be up to 2.5 ULP off, so some drivers are not bit-exact. -- [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. -- [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. The same bit rule serves the buffer float atomics (`AtomicFloatBits`). Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given), and `ds_add_f32`, which keeps the host's `OpFAdd`. +- [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. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. +- [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, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. 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, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console +- [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). - [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`. - [Buffer loads without GLC or DLC](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) are not coherent in a program that has GLC or DLC buffer accesses and no `buffer_gl0_inv`, `buffer_gl1_inv`, `s_gl1_inv` or `s_dcache_inv`: only its GLC and DLC loads, stores and atomics use the `Coherent` buffer view, so a plain load can return a value cached before another work-group's write. This follows the [LLVM AMDGPU memory model for GFX10](https://llvm.org/docs/AMDGPUUsage.html#memory-model-gfx10-gfx11), in which a load-acquire is a GLC DLC load followed by `buffer_gl1_inv` and `buffer_gl0_inv`; it is not measured on the console or on RDNA hardware. - [DS src2 operations](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) (`ds_*_src2_*`, GFX10.1 only) read `DS[B]`, with `B = A + 4 * sext(offset1[6:0]:offset0)`, and then update `DS[A]` with the matching 32-bit atomic, so the read of `B` is not atomic with the update of `A`. The form that takes the offset from the address (`offset1[7]` set), the 64-bit forms and GDS throw: whether the LDS address keeps `A[31:17]` in that form, and whether the 64-bit forms scale the offset by 4 or 8, is not documented. `ds_min_src2_f32`, `ds_max_src2_f32` and `ds_add_src2_f32` throw: on RDNA2 (gfx1035, the oracle's hardware, which has no src2 opcodes) `ds_min_f32`, `ds_max_f32` and `ds_add_f32` ignore `IEEE_MODE` but depend on whether f32 denormals are flushed (28, 28 and 12 of 196 special rows differ), and that is not modelled for LDS float atomics yet (#981). -- [s_swappc_b64](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) writes the link pair with the same model as `s_getpc_b64`: the guest return address `pc + 4` as a byte offset over `GetShaderBase`. Any other use of the link pair (arithmetic, compare, hash, store) throws, apart from the overwrites and shared links that [static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) allow; constant-offset folding over the link is not modeled. The special encoding `s_swappc_b64` with SDST 125 still decodes as `s_setpc_b64`, which matches the hardware: measured on RDNA2 (gfx1036) in wave32 and wave64, it branches to the source pair and leaves a marker SGPR pair untouched, and with an SGPR destination the link is `PC + 4`. +- [`ds_ordered_count`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) (GDS only) reads its operand fields as LLVM encodes them for `llvm.amdgcn.ds.ordered.add` ([SIISelLowering.cpp](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.0/llvm/lib/Target/AMDGPU/SIISelLowering.cpp#L8844-L8894)): `offset0` is the counter's byte offset (index times 4) and the whole GDS address, `offset1` bit 0 wave release, bit 1 wave done, bits 2-3 the shader type, bit 4 swap instead of add and bits 6-7 the dword count less one. `M0` holds the ordered wave ID, not a GDS address as for `ds_append` (radeonsi passes `TG_SIZE` bits 6-17 as the intrinsic's pointer in [si_compute_prim_discard.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-20.3.0/src/gallium/drivers/radeonsi/si_compute_prim_discard.c) and `gs_tg_info` bits 0-11 in [gfx10_shader_ngg.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-21.3.0/src/gallium/drivers/radeonsi/gfx10_shader_ngg.c), with "The GDS address is always 0 with ordered append"); it only orders the waves, which is not modeled, so it is ignored. Each wave adds the value of its first active lane to the counter with one device-scope atomic and every active lane gets the counter's previous value (the `ORDERED_APPEND_ENBL` dispatch modifier is accepted for it). The hardware also serializes the waves of a dispatch in launch order, which is not modeled: the waves add in any order, so counters a title reads back as ordered offsets give the same totals but may hand out ranges in another order. Which lane's value is added is not documented; the first active lane is assumed and not measured on hardware. A swap (`offset1` bit 4) stores the first active lane's value in place of the counter with one device-scope atomic exchange and returns the counter's previous value to every active lane; with the waves unordered, which wave's value the counter keeps and which previous value each wave gets need not follow the launch order the hardware gives. Counts of more than one dword and LDS forms throw. Covered by [GdsAppendConsume.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/GdsAppendConsume.cpp). +- [s_swappc_b64](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) writes the link pair with the same model as `s_getpc_b64`: the guest return address `pc + 4` as a byte offset over `GetShaderBase`. Any other use of the link pair (arithmetic, hash, store) throws, apart from the overwrites, SOPC compares and shared links that [static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) allow; constant-offset folding over the link is not modeled. The special encoding `s_swappc_b64` with SDST 125 still decodes as `s_setpc_b64`, which matches the hardware: measured on RDNA2 (gfx1036) in wave32 and wave64, it branches to the source pair and leaves a marker SGPR pair untouched, and with an SGPR destination the link is `PC + 4`. +- [2D biased sampling through a one-slice, single-level 3D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) supplies a zero third coordinate and retains the bias operation, with normalized point minification and magnification and no linear mip filtering. Measured on RDNA2 (AMD integrated gfx1036, driver 32.0.21036.18, Windows 11) by running an OpenCL-loaded native kernel with `image_sample_b` DIM 2D against 16x16x1 R32_UINT and RGBA8_UNORM 3D images: biases -16, -0.5, 0, 0.5 and 16 read the (u,v) texel in slice zero, including wrapped coordinates, point min/max reduction and the title's sampler with Z border addressing. Wave32, IEEE off, DX10 clamp on, denormals preserved, round to nearest even, FP16 overflow off; wave64 and the PS5's gfx1013 are not measured. Pixel-stage, direct, non-depth, unpacked samples whose only use is the plain 32-bit bias form are accepted; extra slices, any nonzero mip range, linear or anisotropic filtering and unnormalized coordinates throw. Vulkan readback is checked on NVIDIA RTX 5070 and AMD integrated graphics for positive and negative bias and point weighted-average/min/max reduction. +- [FORCE_DEGAMMA on RGBA8_UINT](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) materializes a direct image sampled only by plain biased pixel accesses as RGBA8_SRGB when every paired sampler forces degamma. Measured on AMD integrated RDNA2 gfx1036 (driver 32.0.21036.18, Windows 11, wave32, IEEE off, DX10 clamp on, denormals preserved, round to nearest even, FP16 overflow off): a native `image_sample_b` DIM 2D on a 16x16x1 RGBA8_UINT 3D T# returns integer channels without the bit, but the same floating-point decoded values as RGBA8_UNORM with it, including point min/max reduction and the title's exact sampler. The metadata and image view must both become floating-point sRGB; ignoring the sampler bit or retaining a UINT heap is incorrect. The accepted case requires one slice, mip zero, normalized point sampling and uniform FORCE_DEGAMMA across its samplers. Other image accesses, mixed sampler states, indirect descriptors and other integer formats remain refused. Wave64, other filtering modes and the PS5's gfx1013 are not measured. - 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. -- [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. +- 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, as a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) read them (0x80000000_00000000 / 0x80000000_FFFFFFFF shared, 0x70000000_00000000 / 0x70000000_FFFFFFFF private). 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 the low dword, after LLVM's `SIRegisterInfo.td` and `getSegmentAperture` and the gfx1030 and gfx1035 readouts on [#1190](https://github.com/boykopovar/AnyPS5/pull/1190) and [#3803](https://github.com/boykopovar/AnyPS5/issues/3803). The 32-bit read and the wave's address mode (taken to be 64-bit, PTR32 clear) were not measured on the console +- [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, and a vertex or tessellation evaluation shader whose guest clip and cull distances already take all eight does not write it either, so NVIDIA 615.78.08 draws such a vertex at the viewport centre; a device without `shaderClipDistance` cannot compile vertex shaders that write it (the compile throws). The PS5 rasteriser was not measured. +- [PERSP_SAMPLE and LINEAR_SAMPLE](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) are the barycentrics at the iterated sample, which in a pixel shader the guest does not iterate is taken to be sample 0, inferred from the ancillary's iterated sample number and not measured. They are interpolated with `InterpolateAtOffset`, which [GLSL_EXT_fragment_shader_barycentric](https://github.com/KhronosGroup/GLSL/blob/0099bf83b0288469917fc2e2972cb0a2b74d6711/extensions/ext/GLSL_EXT_fragment_shader_barycentric.txt#L286-L299) names for barycentrics away from the default position, at the sample's offset from the pixel centre, read per pixel of the 2x2 quad from `PA_SC_AA_SAMPLE_LOCS` (the standard pattern unless the draw sets its own): sample 0's in a shader the guest does not iterate, and in an iterated shader the offset of the host's `SampleId` from a table of the guest's locations, because Vulkan leaves the position of plain barycentrics under sample shading implementation-defined (lavapipe uses the pixel centre). That needs the device's `sampleRateShading`; without it, and when every location is the pixel centre (a single-sample draw), they keep the host's default position, the pixel centre once per pixel and an implementation-defined position under sample shading. PERSP_CENTER and LINEAR_CENTER are interpolated at offset 0 in an iterated shader, so they stay at the pixel centre as on the console; the centroid pairs keep the host's default position. With barycentric emulation the offsets apply to the emulated barycentrics. Devices that interpolate attributes on the host instead (no usable fragment-shader barycentrics, as on MoltenVK) keep the default position for all four pairs. +- [Pixel positions](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - POS_X_FLOAT and POS_Y_FLOAT follow `SPI_BARYC_CNTL.POS_FLOAT_LOCATION` (bits 17:16) and `POS_FLOAT_ULC` (bit 20), whose positions come from [PAL's GFX9+ register header](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/chip/gfx9_plus_merged_registers.h) and whose meanings from [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/RegisterMetadataBuilder.cpp#L767-L779): the pixel centre (0), the iterated sample (2), or the upper-left corner (ULC). Under host sample shading the centre is `floor(FragCoord) + 0.5`, and a non-iterated shader's sample is the centre plus sample 0's offset. The centroid (1) keeps `FragCoord`, the pixel centre or under sample shading the sample, not the centroid of the covered samples. A missing `SPI_BARYC_CNTL` reads as the pixel centre and the reserved location 3 throws. POS_Z_FLOAT and POS_W_FLOAT keep `FragCoord`'s depth and 1/w at the host's position. `PERSP_CENTER_CNTL`, `PERSP_CENTROID_CNTL`, `LINEAR_CENTER_CNTL` and `LINEAR_CENTROID_CNTL` (bits 0, 4, 8 and 12) are not read: PAL and LLPC never set them. - [LINE_STIPPLE_TEX](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) reads as 0: line stipple is not emulated. RADV enables the input only as the required pixel input of a shader that reads no interpolants, because it initializes the fewest VGPRs ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_shader.c) `radv_compute_spi_ps_input`), and never reads it -- [Static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair may only be overwritten whole, by `s_mov_b32`, `s_mov_b64`, an SOP2 result or a scalar load, as the caller of a user-data callee does before each call. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. -- [Relative vector addressing](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in a zero-initialised function-scope array per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window; the host compiler decides whether that array stays in registers or spills to scratch, which is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. Pixel programs whose relative span contains the packed ancillary input instead keep that span in SSA and implement indexed operations with compare/select chains. This keeps ancillary definitions visible to field lowering and masked-write proofs; storing the unmodelled whole word in a private array would require composing unknown bits. A fold after masked-select elimination lowers fields whose consumers become visible only after redundant copies are removed. A whole-word use that the masked-select pass keeps gets the composed word described under the packed pixel ancillary word. Both representations include implicit entry inputs in the inferred register limit and retain unsigned M0, inactive-write and DPP update rules; wave32/wave64 native Vulkan readbacks cover relative reads, writes, split-offset swaps, inactive writes, DPP masks and out-of-range accesses, but console comparison and the performance cost of selecting over this span are unverified. +- [Static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. Outside every region of its calls the pair is free to use wherever no return address can reach: a dataflow over the decoded instructions follows each dword of the pair from the calls that write it until a scalar load, SALU or VALU result is known to overwrite it, and an instruction there that may read a dword still holding a return address throws; SOPC compares are exempt, as they only set SCC. A VALU mask covers one SGPR in wave32 and a pair in wave64. When the graph is built without the wave size, the dataflow runs for both: a program that escapes under both throws there, and one that escapes under only one throws when it is translated at that size. +- [Static scalar call link pairs](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) each serve one call, or several calls whose regions are disjoint, each call closed by the first return reading the pair after its target. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. +- [Relative vector addressing](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in zero-initialised function-scope storage per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window. [`IndexedWindowSlots`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) bounds each relative index through the IR (bit-field extracts, masks, shifts, adds, selects, phis, `ReadFirstLane` and the window stores that reach a load); the storage is an array of the slots below the largest bound that is under half the window, and one function variable per slot after it. A constant index uses the array element or the slot's variable; a relative access whose index has no such bound, or lands past the array at run time, indexes the array below its size and otherwise branches through an `OpSwitch` over the slot variables, so the bound only decides speed. When no index has a bound under half the window, the whole window is the array, as with `APS5_NO_WINDOW_NARROWING=1` (a recompiler switch in the shader cache key). Measured on an NVIDIA GeForce RTX 3060 (driver 615.78.08, Linux) with a compute program whose 167-register window an ALU loop reads and writes in full and whose relative accesses take `m0 = s8 & 3`: a 2048-group dispatch took 70.2 ms in wave32 and 123.3 ms in wave64 with the whole window in the array (as before narrowing) and 1.9-2.1 ms and 7.8 ms narrowed; with a second relative access whose index has no bound and lands past the array at run time (index 50 or 166), 113 ms and 5.8-6.0 ms in wave32. In the PPSA08427 (Killing Floor 3) hub it made no measurable difference to presents or to the GPU time of the eight most expensive programs. Whether the host compiler keeps the array and the slot variables in registers or spills them to scratch is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. Pixel programs whose relative span contains the packed ancillary input instead keep that span in SSA and implement indexed operations with compare/select chains. This keeps ancillary definitions visible to field lowering and masked-write proofs; storing the unmodelled whole word in a private array would require composing unknown bits. A fold after masked-select elimination lowers fields whose consumers become visible only after redundant copies are removed. A whole-word use that the masked-select pass keeps gets the composed word described under the packed pixel ancillary word. Both representations include implicit entry inputs in the inferred register limit and retain unsigned M0, inactive-write and DPP update rules; wave32/wave64 native Vulkan readbacks cover relative reads, writes, split-offset swaps, inactive writes, DPP masks and out-of-range accesses, but console comparison and the performance cost of selecting over this span are unverified. - [s_call_b64](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Measured on RDNA2 (gfx1036) in wave32 and wave64 with SIMM16 = 3: the link pair receives `PC + 4`, the branch goes to `PC + 4 + signext(SIMM16) * 4`, and `s_setpc_b64` of the link returns to the instruction after the call. Execution on the PS5 (gfx1013) and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. +- [BDA anchor halfword reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) retain volatile byte loads even for noncoherent memory. On RX 6650 XT / RADV Mesa 26.2.4, the valid SPIR-V for two adjacent nonvolatile byte loads returned `0x80a0` for bytes `0xa0, 0xa1`, where lookup-based reads returned `0xa1a0`. Adding the volatile operand makes the same GPU exactness fixture pass, including mixed direct/fallback lanes. The driver transformation responsible has not been isolated; removing this restriction needs equivalent readback coverage on the affected driver. Single-byte reads keep their original access mask. - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests - [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. +- [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - `APS5_SPANNED_WAVE=1`, off by default, runs a single-wave wave64 compute program at one lane per invocation on a 32-lane host subgroup and exchanges ballots, shuffles and reductions between the two host subgroups through workgroup memory and barriers. The driver turns it on only where compute subgroups can be pinned at 32 full lanes (`subgroupSizeControl` and `computeFullSubgroups`), passes it as `SpirvTarget::spannedWaves` (part of the cache key and the serialized request), and creates the pipelines of spanned programs with `REQUIRE_FULL_SUBGROUPS`, since the lane number `SubgroupId * 32 + SubgroupLocalInvocationId` needs two full subgroups. Those barriers are correct only where every invocation of the workgroup reaches them, which holds for the guest's wave-uniform control flow. The emulated 64-bit LDS atomics ([LockedLdsUpdate](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp)) run only on the lanes the exec mask keeps, so their lock loop ballots within its own host subgroup, as its elect already does. Any other cross-lane exchange emitted inside per-lane control flow would hang a spanned wave in the same way, and the emitter does not reject one: there is no uniformity check. The only check so far is manual: disassemble the module (`spirv-dis`), list the selection and loop constructs that enclose each `OpControlBarrier`, and read the condition of each enclosing selection. In Ghost of Yōtei's program 0x80003af600 (PPSA26344), 108 of 111 barriers sat only in loops on wave-uniform conditions and 3 inside per-invocation conditionals, all in the lock loop. Other programs and titles have not been scanned. An earlier, cheaper ballot (one barrier instead of three) also lost the device for a reason that is not understood, which may be another barrier placement problem. Measured on an RTX 4090 (Windows): an offline replay of that program and two Yōtei runs that previously lost the device (NVIDIA TDR) now run past it, and [LdsMaskWaterfall.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp) runs both modes and covers a half-inactive wave. Stray (PPSA02101) on Intel ANV and a Demon's Souls (PPSA01342) request replay showed no fault, before this fix +- [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - a guest load whose lane is inactive or whose address is out of bounds is still performed and its result replaced by 0 with `OpSelect`, instead of a selection per guard, when the load can be pointed at a location nothing writes: scratch reads its own element 0; storage buffers (direct buffers, GDS) read element 0xfffffffe, which `robustBufferAccess2` turns into a zero load with no memory access, so the extra read cannot race with a write the guarded program would not have raced with ([Vulkan memory model](https://docs.vulkan.org/spec/latest/appendices/memorymodel.html#data-race)). Without `robustBufferAccess2` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it) those loads keep their selections; LDS loads, stores, atomics, formatted and physical-address (BDA) accesses always do. LDS keeps the branch because NVIDIA's compiler predicates a branched LDS load at no cost, while the select form costs about 2.5 instructions per dword and splits a `ds_read_b128` into four loads (RTX 4060 measurements by @karanvora2599 in #4048); Intel's static counts favour the select only for wide reads, and run time was not measured on either. The aligned high word of a misaligned-capable buffer read re-reads the low word's own element. A selection guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. `agc_guarded_memory_loads` traces the emitted loads in both device modes. +- [Merged hull programs](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) (registered binary type 3) run whole in the hull stage, one invocation per output control point. On the console hull lane i keeps local lane i's registers, which with fewer input than output control points stop matching the patch's control points from i = inputs on; here invocation i holds control point min(i, inputs - 1) of its own patch. A wave-mask branch (`s_cbranch_exec[n]z`, `s_cbranch_vcc[n]z`) tests the invocation's own lane, so it can skip a block, scalar instructions included, that the console runs for the wave; `v_readfirstlane_b32` returns the invocation's own value, not the first active lane's, and another lane's register throws. The register layout (`s3`, `v0`-`v5`, as for the separate halves) is not measured on hardware. s[0:1] hold the hull user-data address (SH 0x102/0x103) when a draw sets it and zero otherwise, so a program reading it unset faults at the null address. +- [Wave LDS ordering on AMD's proprietary driver](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (shader recompiler) - there ([SubgroupBarrier.hpp](../../../core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp)) one guest wave's LDS accesses are ordered by a subgroup-scope `OpMemoryBarrier` with no control barrier, relying on a 32-lane subgroup being one hardware wave whose lanes run in lockstep and issue LDS instructions in order, which Vulkan does not guarantee; for LDS kept in device memory the same is assumed of global loads and stores, unmeasured. Driver 32.0.21042.62 (Radeon 0x13c0, RDNA 2) compiled the subgroup-scope `OpControlBarrier` to `s_barrier`, which waves in a branch paired with other waves' barriers (PS5LM Plus). Measured on that driver only ([LdsTreeReduction.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsTreeReduction.cpp)); a Radeon 860M on 32.0.13058.8001 reportedly did not show the fault. diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index d61e36d70a..7fd7a8d16d 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -1,8 +1,8 @@ # Controller, keyboard and mouse input -AnyPS5 accepts SDL-mapped game controllers as well as its built-in keyboard and mouse bindings. Xbox-style controllers and PlayStation controllers recognized by SDL use the standard PS button layout; sticks and analog triggers are passed through, and controllers can be connected or disconnected while the game is running. The first recognized controller is used. AnyPS5 uses its built-in keyboard and mouse bindings when no configuration file is present. To change selected keyboard and mouse bindings, create `anyps5-input.ini` beside the generated game executable. Set `ANYPS5_INPUT_CONFIG` to use a file at another path. +AnyPS5 accepts SDL-mapped game controllers as well as its built-in keyboard and mouse bindings. Xbox-style controllers and PlayStation controllers recognized by SDL use the standard PS button layout; sticks and analog triggers are passed through, and controllers can be connected or disconnected while the game is running. The first recognized controller is used. AnyPS5 uses its built-in keyboard and mouse bindings when no configuration file is present. To change selected keyboard and mouse bindings, create `anyps5-input.ini` beside the generated game executable. Set `ANYPS5_INPUT_CONFIG` to use a file at another path. Both the game directory and that path may contain any characters, such as `ü` or `日本`. -For an Xbox-layout controller, A/B/X/Y map to Cross/Circle/Square/Triangle, LB/RB map to L1/R1, and LT/RT map to analog L2/R2. Start maps to Options. D-pad and stick-click buttons are supported. On a PlayStation controller, the touchpad click button is forwarded; finger touch coordinates are not implemented. +For an Xbox-layout controller, A/B/X/Y map to Cross/Circle/Square/Triangle, LB/RB map to L1/R1, and LT/RT map to analog L2/R2. Start maps to Options. D-pad and stick-click buttons are supported. On a PlayStation controller, touchpad clicks and the positions of up to two fingers on the touchpad are forwarded. On other controllers, View (Back) presses the touchpad at its center. Each non-empty line has the form `Action = Type:Value`. Action names are case-insensitive. A `#` or `;` starts a comment. The first line for an action replaces its built-in bindings; later lines for the same action add alternate inputs. Duplicate entries for the same action are ignored. Actions omitted from the file keep their built-in bindings. UTF-8 files with or without a byte-order mark are supported. @@ -16,7 +16,34 @@ While a title has a mouse open (`sceMouseOpen`) and the window is focused, AnyPS Game-controller bindings are enabled automatically and are not affected by keyboard or mouse overrides in this file. -Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `ToggleMouse`, and `ToggleFullscreen`. +Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `StickModifier`, `Shake`, `ToggleMouse`, `ToggleFullscreen` (F11), `TogglePerformanceOverlay` (F10, the [performance overlay](USAGE.md#performance-overlay)), and `Screenshot` (F12, saves the next displayed frame as described in [screenshots](USAGE.md#screenshots)). `ToggleFullscreen`, `TogglePerformanceOverlay` and `Screenshot` accept only keyboard keys. + +### Default bindings + +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 | +| `L2` | R, Mouse button 4 (X1) | +| `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 | +| `Shake` | X (reports a swinging acceleration and angular velocity while held) | +| `ToggleMouse` | Middle mouse button, F7 | +| `ToggleFullscreen` | F11 | +| `TogglePerformanceOverlay` | F10 | +| `Screenshot` | F12 | For example, this changes Cross to F or Space, moves the left stick to IJKL, uses the mouse buttons for Square and R2, and keeps all other built-in bindings: @@ -42,8 +69,11 @@ The same file also accepts these settings, each at most once, in the form `Setti | `MouseCurve` | Number from 0.2 to 5, the response exponent. Above 1 gives finer control at low speed; below 1 reaches large tilts sooner. | `1` | | `MouseDeadzoneOffset` | Number from 0 to 0.9, the smallest tilt any movement produces as a fraction of full tilt. Set it just above the game's own deadzone: raise it if slow movement doesn't move the camera, lower it if small movements jump. | `0.2` | | `MouseMaxSpeed` | Number from 0.1 to 10, the highest tracked speed as a multiple of the full-tilt speed. Above 1, a fast flick keeps the stick at full tilt a little longer after the mouse stops. | `1` | -| `MouseSmoothingMs` | Number from 0 to 500, the smoothing time constant in milliseconds. 0 follows every mouse report directly. | `10` | +| `MouseSmoothingMs` | Number from 0 to 500, the smoothing time constant in milliseconds. A mouse that reports fewer than 300 times per second, such as a 125 or 250 Hz mouse, is smoothed over at least three of its report intervals, up to 30 ms; the report rate is measured from the movement itself. 0 follows every mouse report directly and turns this off too. | `10` | | `MouseDecayMs` | Number from 1 to 1000, the milliseconds without movement after which the stick is centered. | `30` | +| `StickModifierScale` | Number from 0.05 to 1, the tilt of keyboard stick bindings while a `StickModifier` binding is held. | `0.5` | +| `KeyboardRampMs` | Number from 0 to 2000, the milliseconds a keyboard stick binding takes to move the stick from the center to full tilt. 0 tilts at once. | `0` | +| `TriggerRampMs` | Number from 0 to 2000, the milliseconds a key or mouse button bound to `L2` or `R2` takes to press the trigger from 0 to full. 0 presses it fully at once. | `0` | ```ini LeftStickDeadzone = 12 @@ -51,9 +81,42 @@ RightStickDeadzone = 8 MouseSensitivity = 2.5 ``` +## Controller timelines + +Set `APS5_PAD_INPUT_FILE` as described in [Controller input for test runs](USAGE.md#controller-input-for-test-runs), then queue an entire button sequence after the application begins reading its controller. Save a JSON file such as `menu.json`: + +```json +{"version":1,"events":[{"at_ms":200,"hold_ms":300,"buttons":["cross","r1"]},{"at_ms":1000,"hold_ms":300,"buttons":["down"]}]} +``` + +```sh +python3 tools/pad_timeline.py menu.json /tmp/anyps5-pad --repeat 3 --gap-ms 500 +``` + +Events start relative to when the application first observes the new command. Button lists support every name in `pad_input.py`; simultaneous names are combined. An empty list or `["release"]` injects no buttons. Events must be ordered and non-overlapping, hold for 1–5000 ms, and finish within one hour. Repetition adds the specified gap after the last event; at most 4096 events are accepted after repetition. The helper validates the entire sequence before atomically replacing the command file. + +The runtime evaluates scheduled events at each controller read, including their automatic releases. A new pulse or timeline replaces the remaining sequence. Physical controller buttons still add to injected buttons. Existing commands at the application's first controller read are ignored. This schedules wall-time input: events can be missed while the application is not reading its controller, and it does not reproduce frame timing or game state. Use gaps and holds long enough for the title's input loop. + +## Analog keyboard input + +`StickModifier` has no built-in binding. While a key or mouse button bound to it is held, the keyboard stick bindings tilt the stick by `StickModifierScale` of its range instead of fully, for walking or fine aim. Controller sticks and the mouse stick are not affected, and opposite keys still cancel each other. + +```ini +StickModifier = KEY:Z +StickModifierScale = 0.4 +``` + +With `KeyboardRampMs` above 0, keyboard stick bindings move the stick toward their tilt at a steady speed instead of jumping, and back to the center the same way when the keys are let go. Opposite keys held together move the stick to the center at the same speed. + +With `TriggerRampMs` above 0, a key or mouse button bound to `L2` or `R2` presses the analog trigger gradually and lets it go the same way, for throttle and brake in racing titles. A pressed controller trigger is not slowed, and the larger of the two values is passed on. + +## Shake + +While the `Shake` input is held (X by default), the controller reports its acceleration and angular velocity swinging back and forth every 50 ms, for titles that ask the player to shake the controller. A controller's own motion sensors are used otherwise. + ## Mouse as the right stick -`ToggleMouse` (the middle mouse button by default) captures the mouse and drives the right stick from how fast the mouse moves. Capture uses raw relative input, so the pointer speed and acceleration set in the operating system don't apply. Press it again to release the mouse. Capture also ends, and every held key is let go, when the window loses focus. +`ToggleMouse` (the middle mouse button or F7 by default; a mouse without a middle button can use F7) captures the mouse and drives the right stick from how fast the mouse moves. Capture uses raw relative input, so the pointer speed and acceleration set in the operating system don't apply. Press it again to release the mouse. Capture also ends, and every held key is let go, when the window loses focus. The stick tilts in the direction the mouse moves, and further the faster it moves. Speed is measured in counts per second over the time between updates, so the same hand speed gives the same tilt at any frame rate. Any movement tilts the stick at least `MouseDeadzoneOffset`, so slow movement isn't lost inside the game's own deadzone. When the mouse stops, the stick eases back and is centered `MouseDecayMs` after the last movement. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 2aa5efb27a..aa4abd1ada 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -2,9 +2,19 @@ [**Online relinker**](https://boykopovar.github.io/AnyPS5/) - files stay on your device. +### Browser folder output + +The online converter can save Linux output as an uncompressed folder in supported browsers. It does not overwrite an existing destination folder. If conversion fails or is cancelled, it aborts the active file and attempts to remove the newly created output folder. If the browser prevents cleanup, the error names the folder that may contain incomplete files. + +### Browser batch conversion + +Select or drop multiple app ZIP archives to convert them into one ZIP bundle. The converter inspects each app separately and puts its executable, libraries and `app0/` files in a numbered folder, so archives with the same filename remain separate. Batch output uses ZIP mode; saving to an uncompressed folder remains available for one Linux app at a time. The 256 MiB memory-download limit applies to the whole bundle, so use **Save ZIP to disk** for larger batches. If a batch is cancelled or any app fails, the converter aborts the incomplete bundle; any cleanup failure is reported instead of offering it as a completed result. + ## 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. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present, or all three absent, is an error. +The SELF unwrapper refuses containers requiring more than 64 MiB of total alignment padding before allocating the output buffer. This limit counts padding across file-backed and empty segments, not segment data; a refusal preserves the input and any existing output file. + +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/ @@ -17,6 +27,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 @@ -53,10 +65,11 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--windows-diagnostics` | Include startup dependency diagnostics. Requires `--windows`. | | `--windows-gui` | Select the Windows GUI subsystem instead of the console subsystem. Requires `--windows`. | | `--windows-fixed-base
` | Map the lowest ELF load page at this fixed Windows address, given in decimal or hexadecimal (`0x`). Requires `--windows`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and a complete image fitting in the user address space. The main executable has no ASLR or base relocations; an occupied address fails to load. Bundled modules keep their normal placement. | -| `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Requires `--windows`; conflicts with `--lazy-binding`. | -| `--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`. | +| `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Windows and Linux output only; conflicts with `--lazy-binding`. | +| `--linux-fixed-base
` | Load the Linux executable at this fixed address instead of the default `0x400000`, 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, as it is at the default address. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. | +| `--macos-fixed-base
` | Map the lowest ELF load page of the macOS executable at this fixed address, given in decimal or hexadecimal (`0x`). Requires `--macos`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and an image that ends in the macOS user address space. `__PAGEZERO` ends 64 KiB below the address and the Mach-O header occupies those 64 KiB. The executable has no ASLR; an address the process cannot map fails to load. Bundled modules keep their normal placement. | | `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. | -| `--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 and the ECX of RDTSCP return 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. | @@ -64,14 +77,18 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--rpath ` | Set the system library search path. Quote `$ORIGIN` to prevent shell expansion, for example `--rpath '$ORIGIN/libs'` in Bash or PowerShell. Linux guest modules require an absolute path or a path beginning with `$ORIGIN`. Windows requires a nonempty ASCII path and supports `$ORIGIN` as the executable directory. | | `--autorun` | Run the output after conversion, print its exit code, and wait for Enter. Adds executable permissions for Linux output. Requires the target OS and prepared runtime layout. | | `--sce-module-path ` | Parent of the module directories, not the module directory itself. Default: input ELF parent; relative input paths resolve from the process working directory. Explicit absolute path: used directly. Explicit relative path (including `.` and `..`): relative to the process working directory. Output path has no effect. Requires one nonempty value; repeats, nonexistent or nondirectory paths, access failures, and combination with `--skip-sce-module` are errors. | -| `--module-dir ` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. A module the executable or another module needs is initialized at startup, any other when the title loads it with `sceKernelLoadStartModule`, with its start arguments (for example Unity plugins in `Media/Plugins`). Repeatable; `.` is the parent itself, without the input ELF. Conflicts with `--skip-sce-module`. | +| `--module-dir ` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. A module another module needs is initialized at startup, and on Linux and macOS also one the executable needs; any other when the title loads it with `sceKernelLoadStartModule`, with its start arguments (for example Unity plugins in `Media/Plugins`, or `mono-ps4.sprx` on Windows). Repeatable; `.` is the parent itself, without the input ELF. Conflicts with `--skip-sce-module`. | | `--executable ` | Also convert another executable of the title, which it starts with `sceSystemServiceLoadExec`; the path is relative to the module parent directory, for example `picker/picker.bin`. It is written beside the output, named after the path with `/` replaced by `.` and given the output's extension (`picker.picker.bin.exe`), and uses the same `libs/`, `app0/` and guest modules. It is an error if a guest module converts differently for it. Repeatable. Conflicts with `--skip-sce-module`. | +| `--patches ` | Apply the byte patches of this file during conversion, see [game patches](#game-patches). Overrides `relinker-patches.toml` discovery. Conflicts with `--no-patches`. | +| `--no-patches` | Do not apply patches, and do not load `relinker-patches.toml` from the input directory. | +| `--verify-patches ` | Check a patch file against `` without writing anything, see [game patches](#game-patches). Takes one path and no output path. | +| `--verify-output` | With `--verify-patches`, treat `` as a converted Linux output instead of an input executable. | | `--skip-sce-module` | Deprecated. Disable bundled module processing and module directory checks. Conflicts with explicit `--sce-module-path`. | | `--exclude-sce-module ` | Deprecated. Exclude a bundled module by exact filename, not path. Repeat for multiple files; a missing filename is an error. Conflicts with `--skip-sce-module`. | | `--skip-syscall-check` | Deprecated. Disable syscall scanning in the executable and bundled modules. | | `--trap-sdk-syscall-stubs` | Replace the exact ps5-payload-dev generic and numbered syscall wrapper stubs and the recognized inline mmap/mprotect sequences from SDK libc/mman.c with `UD2`, so unused wrappers do not block relinking. Other syscall instructions still cause an error. If a trapped wrapper runs, it raises an illegal-instruction exception. For debugging only. | | `--frame-red-zone` | Windows only: give leaf functions that keep values below RSP (the System V red zone) a frame, so an exception Windows delivers on a guest thread cannot overwrite them. Covers leaves with a `push rbp; mov rbp, rsp; push ...` prologue and leaves that only push registers (their RSP operands are moved with RSP); the relinker prints how many it framed and why it skipped the others. | -| `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. | +| `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. The Linux executable stays position independent (`ET_DYN`) instead of being placed at `0x400000`. | Specify `unused-filter=0|1|2` without `--`, at most once. Unknown options and extra positional arguments are errors. Use `relinker --help` to print the usage syntax to stdout and exit with code `0` without converting files. Invoking `relinker` without arguments prints the usage syntax to stderr and exits with code `1`. @@ -79,11 +96,63 @@ The `--skip-sce-module`, `--exclude-sce-module `, `--skip-syscall-check`, `--trap-sdk-syscall-stubs` is also for debugging. It preserves a runtime failure if a syscall wrapper is called; it does not implement FreeBSD syscalls or make these applications generally safe to run. +## Game patches + +A patch file changes bytes of the input executable or of a bundled module before conversion, for a title that hard-codes a value the host cannot meet. The relinker checks the bytes it replaces, so a patch for another version of the title fails instead of corrupting it. With no patch file the output is unchanged. + +`relinker-patches.toml` in the directory of the input executable is loaded automatically and announced as `Patches: `. `--patches ` names another file, and `--no-patches` loads none. Each applied patch is reported: + +``` +Patch: [] va=0x
off=0x -> () +``` + +The file is a subset of TOML; any other TOML syntax is an error that names the line. It is UTF-8, with blank lines, `#` comments outside strings, `[[patch]]` entries and `key = value` lines. Integers are decimal or `0x` hexadecimal, strings are double-quoted without escapes, and keys match `[A-Za-z0-9_]+`. + +```toml +format = 1 + +[[patch]] +name = "pool_size" +target = "eboot.elf" +va = 0x1234 +type = "i32" +expected = 0x10000000 +value = 0x18000000 +max = 0x7f000000 +align = 0x1000 +description = "pool size" +``` + +`format = 1` is required. The top-level keys `relinker_version`, `relinker_commit`, `config`, `config_sha256`, `main_target`, `linux_base`, `settings` and `settings_sha256` are accepted in any file; only `main_target` and `linux_base` are used, by `--verify-output`. Any other top-level key is an error. + +| Entry key | Meaning | +|-----------|---------| +| `name` | Optional. A setting name of `[a-z0-9_]`, unique within the file and not one of the reserved top-level key names (`format` and the eight keys above), used by a later `--set` option. Shown in the progress line, the manifest and the verify report. | +| `target` | Required. The main executable's file name, or for `--executable` files and bundled modules the path relative to the module parent directory, for example `sce_module/libc.prx`. | +| `va` | Required. The virtual address of the ELF as a disassembler shows it. | +| `type` | Optional: `u8 u16 u32 u64 i8 i16 i32 i64`. Then `expected`, `value`, `min`, `max` and `align` are integers, range-checked and written little-endian. Without it, `expected` and `value` are hexadecimal byte strings of the same length. | +| `expected` | Required. The bytes the input must hold. | +| `value` | Required. The bytes to write. | +| `min`, `max`, `align` | Optional, typed entries only; checked against `value`. | +| `description` | Optional. Reported and copied to the manifest. | + +x86 `mov r/m64, imm32` and `add`/`sub`/`cmp r/m64, imm32` sign-extend their immediate. To patch a 64-bit field through an imm32 use `type = "i32"`, and add `min` to keep negative values out. + +Entries apply in file order. A patch range must lie in the file bytes of one `PT_LOAD` and outside the ELF header and program header table. The conversion fails before writing any output if bytes differ from `expected`, a range is not valid, two entries overlap, a target matches no file or more than one file, or a range overlaps bytes the relinker itself rewrites (`--to-intel` replacements and trampolines, relinker patches, syscall traps) or the 8-byte target of a relocation of the file. + +When at least one patch is applied, `.patches.toml` is written beside the output. It lists every applied entry for every converted file, with `main_target`, `linux_base` (the address a Linux `ET_EXEC` output is placed at) and `config` (the patch file path in forward-slash form, reduced to its file name when the path holds a `"`, backslash or control character; the conversion fails before writing any output when even that cannot be held); typed entries keep their `type`, integers and `min`, `max` and `align`, and `--patches` accepts it to reproduce the conversion. The manifest must not replace the patch file it was read from. + +`--verify-patches ` writes nothing and exits `0` when no entry is a mismatch. Without `--verify-output`, `` is the input executable of any type, modules are found as in a conversion (module options and `--executable` apply), and each entry is `applicable` (the input holds `expected`) or `mismatch`. A target that matches nothing is an error that suggests `--verify-output`. + +With `--verify-output`, `` is a converted Linux ELF; PE and Mach-O outputs are refused. The file must have `main_target`, so verify the generated manifest, not a hand-written file. Entries are checked at their `va` plus the manifest's `linux_base` in an `ET_EXEC` output, which requires `linux_base`; an `ET_DYN` output, from `--lazy-binding`, requires a manifest without it. The entry whose target is `main_target` is checked against ``, and entries for the `--executable` strings given are checked against the output file derived as in a conversion; an entry holding `value` is `applied`. Every other entry is `not-checked (module, or executable not named with --executable)`, including a mistyped module target. Two targets with the same name, such as an `--executable` equal to `main_target`, are an `ambiguous patch target` error. Module discovery options are rejected with `--verify-output`. + +Verify mode rejects the conversion options (`--windows`, `--macos`, `--to-intel`, `--rpath`, `--patches`, `--no-patches` and the like) and a second path. + ## Runtime layout For an executable that assumes its image starts at `0x400000`, use `relinker --windows --windows-fixed-base 0x400000 source/input.elf app.exe`. This maps its lowest ELF load page there; the PE headers occupy the preceding 64 KiB. Use this option only when the executable requires an exact address. Without it, the Windows executable has no `DYNAMIC_BASE` and loads at its preferred base `0x140000000`; relocations are kept, so Windows relocates it if that range is taken. Modules keep `DYNAMIC_BASE`. This option does not change macOS output. -For an executable that assumes its image starts at `0x400000`, as Ruffle (PPSA68091), VLC (PPSA85300) and XPSemu (PPSA97358) do when they read their `app0/imports.txt`, use `relinker --linux-fixed-base 0x400000 source/input.elf app.elf` on Linux. Use this option only when the executable requires a fixed address. Without it, Linux output is a position-independent executable as before. +For an executable that assumes its image starts at `0x400000`, as Ruffle (PPSA68091), VLC (PPSA85300) and XPSemu (PPSA97358) do when they read their `app0/imports.txt`, use `relinker --macos --macos-fixed-base 0x400000 source/input.elf app` on macOS. Use this option only when the executable requires a fixed address; without it, macOS output places the lowest ELF load page at `0x140010000`. Linux output places virtual address 0 at `0x400000` without an option, inside the 40-bit window that a title's own `sceLibcMspaceCreate` checks; `--linux-fixed-base` selects another address, and with `--lazy-binding` the executable stays position independent. The same base is applied to executables selected with `--executable`; bundled PRX modules are not rebased. @@ -95,11 +164,16 @@ libs/ *.prx app0/ + eboot.bin sce_module/ ``` -Use `sce_modules/` or `prx/` instead of `sce_module/` if that is the input directory name. Relinker preserves each module's directory under `app0/` and prints its exact path. Place app resources in `app0/` separately. Copy the built system libraries from `build/core/libs/libs/*.prx` into `libs/`; use libraries built for the target OS. A custom `--rpath` changes the system library location. +Use `sce_modules/` or `prx/` instead of `sce_module/` if that is the input directory name. Relinker preserves each module's directory under `app0/` and prints its exact path. Place app resources in `app0/` separately, including the title's original `eboot.bin`: titles test `/app0/eboot.bin` to find their own directory, as it is always present on the console (ProsperoTV, ProsperoRadio, ProsperoLight and ProsperoLichess fall back to an empty `/data/homebrew/` and fail to load their fonts without it). Only the converted executable runs. On Linux, a symbolic link inside the `app0/` tree is listed with the type of the file or directory it points to, so a linked directory is walked like a real one. Links inside a linked directory whose target is outside `app0/` keep the link type; for nested trees, link only the top-level entries or use copies or hard links. Copy the built system libraries from `build/core/libs/libs/*.prx` into `libs/`; use libraries built for the target OS. A custom `--rpath` changes the system library location. + +Some applications also need their original `app0/sce_sys/param.json` at runtime. AnyPS5 reads this file for the title ID and display name; it must contain a non-empty `titleId` string and a non-empty `localizedParameters` object with a language entry that has a non-empty `titleName`. It selects the entry named by `defaultLanguage` (inside `localizedParameters`, or at the top level); if that entry is absent, it tries `en-US`, then the first object-valued language entry. Preserve the values from the title's own metadata and place the file under the same `app0/` directory as the other app resources. At startup, its presence in the working directory creates `data/`; a non-zero `downloadDataSize` also creates `download0/`. Without the file, neither directory is created. If it is missing or invalid, metadata calls such as title/window setup can fail with a `param.json` error. + +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). @@ -107,15 +181,16 @@ On Windows, a self-built `libs/` also needs `libgcc_s_seh-1.dll`, `libstdc++-6.d 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: ```sh -chmod +x app.elf ./app.elf ``` +The relinker writes `app.elf` and each `Executable file:` it lists with execute permission. A copy that lost its file mode, such as a browser download, needs `chmod +x` on each of them first; without it a title that starts another executable stops with `sceSystemServiceLoadExec: execv failed: Permission denied`. + Windows PowerShell: ```powershell @@ -124,6 +199,14 @@ Windows PowerShell: A Windows executable started without a standard output or error handle, such as a `--windows-gui` build started from Explorer, writes that output to `anyps5.log` beside it, replacing the previous run's file. Without write access to that directory it runs without the log. +### Display resolution and window size + +The game chooses its video-output resolution; AnyPS5 uses that mode for rendering. The display window can be resized or switched to fullscreen, which scales the presented image to fit the window. Changing the window size does not change the game's render resolution. AnyPS5 does not currently provide a global render-resolution override; use a game's own display setting when it offers one. + +### VR compatibility + +AnyPS5 does not currently support PS VR2 headsets or PC VR headsets connected through an OpenXR runtime. It does not provide headset tracking, stereoscopic output or reprojection. A VR title may skip its headset setup when no device is detected, but that does not mean its VR gameplay is supported. + ### Controller input for test runs Set `APS5_PAD_INPUT_FILE` to an absolute path before launching the application to enable controller pulses from a local command file. Once the application is reading controller input, send a button with the helper from the repository root: @@ -135,18 +218,53 @@ 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 +python3 tools/pad_input.py /tmp/anyps5-pad l1+r1 ``` -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. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. +For timed button sequences, see [Controller timelines](INPUT_MAPPING.md#controller-timelines). + +Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads, and their buttons add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. `--left-stick X Y` and `--right-stick X Y` hold the sticks for the same time: each axis is 0-255, 128 is the centre, X grows to the right and Y grows downward. While such a command is active both sticks come from it instead of the physical controller, and a stick that was not named stays centred. `touch-left` and `touch-right` press the touchpad with a finger on its left or right half, as the `TouchLeft` and `TouchRight` keys do; in the command file they are bits `0x200000` and `0x400000` of the button field. Button names joined by `+`, such as `l1+r1`, are pressed together in one command; `release` cannot be joined. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. On POSIX, replacing an existing command file retains its permission mode; a privileged helper also retains its owner and group so the game user can still read it. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. + +### Video-out and pad telemetry + +Set `APS5_VIDEOOUT_TELEMETRY_DIR` to a directory before launching the application to write the run's presents and pad changes to `<directory>/videoout.jsonl`, one JSON object per line, appended when the file already exists. Every line starts with `"v":1`, the host's steady-clock nanoseconds in `t_ns` and the event name in `ev`. A run starts with a `contract` line, `{"v":1,"t_ns":<ns>,"ev":"contract","contract":"v1-pad","commit":"<12 hex>","pid":<pid>,"prx":"libSceVideoOut"}`: `commit` is the source commit the libraries were built from (`unknown` when Git was not found at configuration) and `pid` tells apart the runs that appended to the same file. A `pad` line is written when the pad state the title reads changes in its buttons, sticks or triggers, `{"v":1,"t_ns":<ns>,"ev":"pad","n":<present>,"buttons":<bits>,"sticks":[<left x>,<left y>,<right x>,<right y>],"triggers":[<L2>,<R2>]}`, where `n` is the number of presents completed when the change was published, the count the window title shows. `buttons` is the button bit field in decimal, stick axes run 0-255 with 128 at the centre, and both triggers run 0-255. Motion, touch and the device kind are not compared, so a run that only moves the controller writes no `pad` line. The state a `pad` line reports is the controller and keyboard/mouse input read through SDL; input fed by `APS5_PAD_INPUT_FILE` is applied later, in `libScePad`, so a scripted run writes no `pad` line. A `present` line is written after the first present and every 30 presents after that, `{"v":1,"t_ns":<ns>,"ev":"present","n":<present>,"fps":<rate>}`, with the count and rate the window title shows. A background thread writes the lines and the title is not waited for: when it falls behind, the line is dropped and a `drops` line with the number dropped so far, `{"v":1,"t_ns":<ns>,"ev":"drops","dropped":<count>}`, is written every ten seconds while the count grows, and once at a normal exit, only when lines were dropped; an exit that skips static destructors writes no last line. A pad line the full queue refused counts as dropped even though a later publish writes the change. Without the variable no file is opened and no thread is started; an empty value is the same as an unset one, and a directory that cannot be written stops the title when video output starts, with an error naming the file. ### System fonts -Games that open the console's system font sets (`sceFontOpenFontSet`) need font files in an `anyps5-fonts/` directory beside the output executable; set `ANYPS5_SYSTEM_FONTS` to use another directory. Files dumped from the console are used under their own names (`SST-Roman.otf`, `SST-Bold.otf`, `SSTJpPro-Regular.otf`, ...). Without them, these openly licensed substitutes are used when present: `NotoSans-{Light,Regular,Medium,Bold}.ttf` and `NotoSans-{LightItalic,Italic,MediumItalic,BoldItalic}.ttf` (Latin and Vietnamese), `NotoSansMono-{Light,Regular,Medium,Bold}.ttf` (typewriter), `NotoSansThai-{Light,Regular,Medium,Bold}.ttf` (Thai) and `NotoSansCJK-{Light,Regular,Medium,Bold}.ttc` (Japanese and Chinese). Without either, opening a system font set fails and the game shows no text in those fonts. +Games that open the console's system font sets (`sceFontOpenFontSet`) need font files in an `anyps5-fonts/` directory beside the output executable; set `ANYPS5_SYSTEM_FONTS` to use another directory. Files dumped from the console are used under their own names (`SST-Roman.otf`, `SST-Bold.otf`, `SSTJpPro-Regular.otf`, ...). Without them, these openly licensed substitutes are used when present: `NotoSans-{Light,Regular,Medium,Bold}.ttf` and `NotoSans-{LightItalic,Italic,MediumItalic,BoldItalic}.ttf` (Latin and Vietnamese), `NotoSansMono-{Light,Regular,Medium,Bold}.ttf` (typewriter), `NotoSansThai-{Light,Regular,Medium,Bold}.ttf` (Thai), `NotoSansArabic-{Light,Regular,Medium,Bold}.ttf` (Arabic) and `NotoSansCJK-{Light,Regular,Medium,Bold}.ttc` (Japanese, Chinese and Korean). A set that covers several scripts also opens the files of its other scripts (`SSTThai-*.otf`, `SSTArabic-*.otf`, `SSTJpPro-*.otf`, `DFHEI5-SONY.ttf`, `SCEPS4Yoongd-*.otf` or their substitutes) for the characters its main file lacks; a missing one leaves those characters without a glyph. Without either, opening a system font set fails (the log shows `sceFontOpenFontSet` with `file=(none)`), and what happens next depends on the game: some show no text in those fonts, and others stop at start-up because they assert that the open succeeded. The substitutes come from the [Noto project](https://github.com/notofonts/notofonts.github.io) under the SIL Open Font License (the Noto Sans files are in `fonts/NotoSans/full/ttf`, the Arabic ones in `fonts/NotoSansArabic/full/ttf`); copy the files named above into `anyps5-fonts/`. ### Console language 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/<user id>_<service label>.txt` in the directory the game runs in (the one that holds `app0/`), one `<trophy id> <unlock time>` 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/<user id>_<service label>.stats.txt`, one `<stat id> <value>` 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 `<id> <value>` 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. + +### Screenshots + +Press F12 in the game window to save the next displayed frame as a PNG at the resolution of the game's display buffer, without the window's scaling, letterbox or [performance overlay](#performance-overlay). A BT.2100 PQ display buffer is saved converted to SDR, also while it is shown through [HDR output](#hdr-output). Files are named `<title ID>-<YYYYMMDD-HHMMSS>.png` and written to an `anyps5-screenshots/` directory beside the output executable, which is created when needed; set `ANYPS5_SCREENSHOTS` to use another directory. The saved path is printed to stderr. Presses while the previous screenshot is still being written are merged into one. The key is the `Screenshot` action of the [input mapping](INPUT_MAPPING.md). A screenshot that cannot be written stops the game with the error. + ### GPU selection The game runs on the first Vulkan 1.1 device with graphics and compute queues and swapchain presentation, preferring a discrete GPU over an integrated one. Set `ANYPS5_GPU` to a part of a device name, compared without regard to case, to run on another device; the names are printed at start-up in the `Physical device candidate` lines. When no usable device contains the text, the start fails and the error lists the device names. @@ -162,30 +280,91 @@ $env:VK_LOADER_DRIVERS_DISABLE = '*dzn*' Do not disable Dozen if it is the only Vulkan driver available; the application would have no device to use. `VK_LOADER_DRIVERS_DISABLE` matches Vulkan driver manifest filenames and requires a Vulkan loader built with headers 1.3.234 or later ([loader documentation](https://github.com/KhronosGroup/Vulkan-Loader/blob/main/docs/LoaderInterfaceArchitecture.md#filter-environment-variables)). +On Windows, NVIDIA driver 610.47 can crash in its shader compiler while pipelines are created: the log ends in `FATAL: unhandled exception 0xc0000005` with `nvgpucomp64.dll` in `rip`, on a driver thread. PPSA25344 on an RTX 3060 stopped this way in 3 of 3 New Game loads, also with `APS5_NO_PIPELINE_LTO=1`; with driver 617.42 the same build and scene ran for 40 minutes. Update the driver when the crash is in `nvgpucomp64.dll`. + +### HDR output + +HDR output is available on Windows only. Titles see it when the monitor showing the window runs in HDR mode (Settings > Display > Use HDR), the window's Vulkan surface offers an HDR10 (`VK_COLOR_SPACE_HDR10_ST2084_EXT`) swapchain in a 10-bit format, the instance has `VK_EXT_swapchain_colorspace` and the device `VK_EXT_hdr_metadata`. The monitor's luminance comes from DXGI; a monitor that reports no peak luminance is taken as 1000 nits peak and full-frame, 0 nits black. On Linux and other hosts the output is always SDR, because the surface's formats do not show whether the display is in HDR. A title's BT.2100 PQ display buffers then reach the display unchanged; without HDR output they are converted to SDR. + +### 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 in the same subdirectory, so it goes with the shaders it was built from. Entries of the current version are never removed and the directory has no size limit, so a title that compiles many shaders can add several GB within minutes. Deleting the directory is safe: it is rebuilt on the next run. + +### Pipeline journal + +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 and an eighth of the budget; it is capped at a quarter of that budget, at most a quarter of the heap's size or 2 GiB, whichever is larger, and never below 512 MiB. The sampled texture budget is what the reported budget leaves after the heap's use other than cached textures, a quarter of the heap's size (at least 2 GiB) and an eighth of the budget, and never below 2 GiB. As the heap's other use grows the sampled budget falls first, to that floor, then the storage budget; the storage budget also gives back what the sampled textures alive take as they grow. Each cache trims at its next insert. Both budgets are read again every second; `[gpu] sampled texture cache budget` lines report them at start-up and when either moves by more than a tenth, at most every 10 s. `APS5_TEXTURE_CACHE_MIB=<n>` fixes both budgets at `n` MiB, and the lines are then not printed. + +A fixed budget above the automatic one can exceed what the card holds next to the rest of the frame. Allocations then go to host memory (`[gpu] ... placed in host memory` lines), where the GPU reads them over PCIe. PPSA16064 on an RTX 4060 8 GB, whose automatic storage image budget was 1797 MiB under the earlier rule, placed 868 and 878 MiB in host memory with `APS5_TEXTURE_CACHE_MIB=3072` and walked through its first level at 4.46 and 4.83 FPS; with the variable unset it placed none and ran the same walk at 6.48 and 6.07 FPS. Leave it unset unless a measurement on the card shows otherwise. + ### Storing GPU results at each flip GPU results in storage images and render targets stay on the GPU until something reads their memory. A title that frees such an image after waiting for its GPU work and reuses the memory can have its new data overwritten when the results are stored later ([TechnicalDebt](../dev/TechnicalDebt.md)). `APS5_STORE_AT_FLIP=1` stores every pending result when a display buffer is flipped, at the cost of one write-back per pending image and frame. Any other value stops the title at its first flip. +A title's own CPU reads of that memory are not seen, so a title that waits for a GPU label and then reads the results reads stale memory. `APS5_STORE_AT_LABEL=1` stores every pending result before each label the GPU writes (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`), at the cost of a write-back of every pending image per label. Any other value stops the title at its first label. + +### Benchmark reports + +To measure how a title runs on a machine, start it with `APS5_TRACE_SUMMARY_EVERY=30 APS5_TRACE_BENCH=1`, play the scene for a few minutes and quit. Every 30 seconds the runtime rewrites `aps5-bench-<commit>-<pid>.json.gz` in `APS5_TRACE_DIR` or the working directory and appends a line to `aps5-summary.jsonl`. The file holds the time between every two presents, the commit, GPU, driver, OS, CPU name, thread count and physical memory, and, when `app0/sce_sys/param.json` is present, the title ID, name and content version. It holds no game data. The first interval includes shader compilation; compare later intervals. The interval still open when the title exits is not written. The fields are described under [summaries for a long session](../dev/BUILD.md#summaries-for-a-long-session). + ### Tracing system library calls -Windows only. An executable converted with `--trace-imports` records the calls it makes to the libraries named in `APS5_TRACE_CALLS`, a comma-separated list such as `libkernel,libSceSaveData`, or `all`. Without the variable nothing is recorded. The crash and abort reports then end with the last 64 calls: sequence number, thread, library, import name as the executable imports it (a NID) and the caller's address. They are followed by the 32 most called imports since start-up, with their number of calls. +Windows and Linux. An executable converted with `--trace-imports` records the calls it makes to the libraries named in `APS5_TRACE_CALLS`, a comma-separated list such as `libkernel,libSceSaveData`, or `all`. Without the variable nothing is recorded. The crash and abort reports then end with the last 64 calls (on Linux, which has no crash report, only an abort prints them): sequence number, thread, library, import name as the executable imports it (a NID) and the caller's address. The last 32 of these calls made by the reporting thread follow, taken from the 65536 most recent calls, so a thread whose calls were pushed out of the last 64 by a busier one still shows them. Then come the 32 most called imports since start-up, with their number of calls. ``` last traced system library calls (APS5_TRACE_CALLS): #1 thread 9384 libkernel.prx k+AXqu2-eBc from 0x0000000140010006 app.exe+0x10006 +last traced calls of thread 9384: + #1 libkernel.prx k+AXqu2-eBc from 0x0000000140010006 app.exe+0x10006 most called traced imports (1 calls in total): 1 libkernel.prx k+AXqu2-eBc ``` Limits are listed in the [libc](../dev/debt/libc.md) and [relinker](../dev/debt/relinker.md) technical debt. +### Performance overlay + +F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERLAY=1` shows it from the first frame. `ANYPS5_OVERLAY_POS` puts it in a corner of the window: `tl` (top left, the default), `tr`, `bl` or `br`. For either variable an empty value is the same as an unset one, and any other value stops the game at its first frame with an error that names the variable. The key can be moved with `TogglePerformanceOverlay` in the [input mapping](INPUT_MAPPING.md). The text scales with the window height, at a whole multiple of its 5x7 pixel font; on an HDR10 swapchain the overlay is drawn at 203 nits. + +| Line | Meaning | +|------|---------| +| `PROFILED` | The first line while `APS5_PROFILE_GPU=1` or `APS5_PROFILE_DRAW=1` is set. Those switches add timestamp queries to every batch and timers to every packet, so the numbers of such a run aren't comparable with an unprofiled one. | +| `FPS` | Frames presented per second, over the last second. When the last second holds fewer than two presents (below 1 fps), the rate of the last frame instead, 1000 divided by its time in ms: a frame every 2 s shows `0.50`. Every present counts, including frames a title presents cleared, for example while it loads. | +| `FRAME` | Time between the last two presented frames, in ms. | +| graph | The last 120 frame times, the newest on the right. The full height is the longest of them or twice their average, whichever is larger. | +| `MIN/AVG/MAX` | The shortest, average and longest of the 120 frame times, in ms. | +| `GPU` | GPU time per frame over the last second, or of the last frame below 1 fps, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second, or by a frame below 1 fps. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | +| `DRIVER` | CPU time per frame over the last second, or of the last frame below 1 fps, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. Every worker's time counts in full, so with several workers busy at once the line can exceed `FRAME`. | +| `DRAWS` | Draw packets per frame over the last second, or of the last frame below 1 fps, then dispatch packets per frame (`DISPATCH`), as the packet workers execute them; packets that predication skips are not counted. Shown once the title has submitted either. | +| `SHADERS` | Pipelines created in the last second, or during the last frame below 1 fps, then since the start (`TOTAL`): a graphics pipeline for each draw state the pipeline cache had not seen and a compute pipeline for each new dispatch shader. A rise while the game stutters points at pipeline compilation. Shown once a pipeline was created. | +| `VRAM` | Usage and budget of the GPU's largest device-local memory heap from `VK_EXT_memory_budget`, read once a second. Not shown when the driver lacks the extension. | +| `RAM` | The process's private memory, read once a second: `PrivateUsage` of `GetProcessMemoryInfo` on Windows, `RssAnon` plus `VmSwap` of `/proc/self/status` on Linux. The title's direct memory is shared memory on both systems and is not included. | +| `RES` | The display buffer's size, then the window's swapchain size, for example `RES: 1920X1080 / 2560X1440`. Not shown for frames without a display buffer. | + +Times are shown with one decimal below 100 and as whole numbers above; anything above 9999 shows as 9999, so `FRAME` and `MIN/AVG/MAX` read 9999 for frames of 10 s or more. `FPS` has the same format, with two decimals below 1. Counts are whole numbers, in thousands (`12K`) from 10000 and in millions (`3M`) from a million. Memory is in GB of 2^30 bytes. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. Each draw and dispatch packet and each pipeline creation is counted also while the overlay is hidden, with one increment of a counter that belongs to the packet worker or, for pipelines, a shared one; the memory and the counters are read only while the overlay is shown. + ### 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. +Set `APS5_CRASH_STACK_BYTES=<n>` to print, after the stack return address candidates, the faulting thread's stack words from `rsp` for `n` bytes (at most 1048576, decimal or `0x` hexadecimal), four words per line with their offset from `rsp`. Below them the report prints eight words at every distinct stack word that points at readable, non-executable memory outside the thread's stack, labelled with the word's offset and the address it holds, up to 256 addresses. This shows the objects the caller frames kept, such as the `this` of a destructor or the buffer of a container. The dump stops at the first unreadable stack page, and nothing is printed when the variable is unset, empty or 0. + +The report lists every other thread of the process with its `rip`, `rsp`, `rbp` and the return addresses of its `rbp` frame chain. A thread whose chain yields fewer than two frames, such as one waiting inside `ntdll.dll` or in code built without frame pointers, also gets up to 24 stack return address candidates: the words in the 32 KiB above its `rsp` that point at executable memory, read while the thread is suspended. Like the candidates of the faulting thread they are a scan, not an unwind, so stale return addresses of finished calls can appear among them. + +### GPU submission rings and stall dumps + +Each GPU queue keeps its last 16 command buffer submissions: when each was submitted, its serial, size and guest address, and a copy of its first 512 dwords after indirect buffers are inlined. `APS5_PM4_RING=<n>` keeps the last `n` instead, from 0 to 4096; 0 keeps none, and any other value stops the title at its first submission. The rings are printed to stderr, one line per submission and one per decoded packet, when the Windows crash report runs: an unhandled exception, `abort()`, an invalid CRT parameter or the timed dump of `APS5_HANG_DUMP_SECS`. + +With `APS5_STALL_WATCHDOG_SECS=<n>`, 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. +`0`: conversion succeeded or help was displayed. `1`: invalid arguments, or a `--verify-patches` entry that is a mismatch. `2`: conversion failed; the error is printed to stderr. With `--autorun`, successful conversion returns the launched application's exit code. ## Import audit @@ -205,9 +384,15 @@ python3 tools/import_audit.py app.registry.json app.libc.prx.guest.prx.registry. | `absent` | No built library exports the NID: the loader fails. | | `module` | The import names a file found in `--modules` (the title's own library), which is trusted to provide it. | -Each `--modules` file is also audited itself: its undefined symbols are classified against the built libraries and the other modules' exports, with the module file named in the report, and the libraries it needs must have a file in `--libs` or `--modules`. The audit reads dynamic tables from ELF section headers or, for sectionless files, from `PT_DYNAMIC` and a SysV or GNU hash table. +Module registries mark ELF weak imports with `"weak": true`, allowing the audit's current-stop selection to skip them. Strong imports omit the field. Older module registries do not preserve weak bindings; regenerate them to distinguish optional imports. -The report also lists needed libraries that have no file in `--libs` or `--modules`, and imports exported only by a library other than the one they name; those resolve on Linux and can fail on Windows. +ELF modules in `--modules` are audited regardless of filename: their undefined symbols are classified against the built libraries and the other modules' exports, with the module file named in the report, and the libraries they need must have a file in `--libs` or `--modules`. Ordinary non-module sidecars are ignored; malformed ELF, PE/SELF containers and non-ELF files with module extensions remain errors. The audit reads dynamic tables from ELF section headers or, for sectionless files, from `PT_DYNAMIC` and a SysV or GNU hash table. + +Each registry has a companion with the same prefix and the suffix `.needed.json`, listing its host library dependencies, including declared `DT_NEEDED` entries with no imports. Dependencies supplied by converted bundled modules, including their aliases, are omitted. The executable's companion includes host dependencies of its bundled modules. The audit reads these companions automatically; keep them beside the registries. Older registries without companions still work, but cannot report dependencies that have no imports; regenerate them with `--registry` to include those dependencies. + +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: <root> Chain: <root> -> ... -> <missing>` lines, since the loader fails on them even when every game import resolves. | Option | Effect | |--------|--------| @@ -217,6 +402,8 @@ The report also lists needed libraries that have no file in `--libs` or `--modul | `--names <file>` | 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 <file>` | 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 849042afd4..ed871bb1d4 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -1,14 +1,20 @@ import argparse import hashlib +import io import json import os from pathlib import Path +import sys import re import shutil +import tempfile import urllib.request import zipfile +from package_release import WEB_FILES + ROOT = Path(__file__).resolve().parent.parent +MAX_EXPANDED_BYTES = 512 * 1024 * 1024 def copy_sources(output): @@ -27,9 +33,10 @@ def request(url): return urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=60) -def publish_libraries(output): - with request("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest") as response: - release = json.load(response) +def publish_libraries(output, release=None): + if release is None: + with request("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest") as response: + release = json.load(response) tag = release["tag_name"] if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", tag): raise ValueError("Invalid release tag") @@ -62,13 +69,24 @@ def publish_libraries(output): files = archive.infolist() if not files or len(files) > 4096: raise ValueError("Invalid library archive") + 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[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} @@ -78,20 +96,98 @@ def publish_libraries(output): print(f"Published library assets from {tag}") -def main(): +def publish_release(output): + with request("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest") as response: + release = json.load(response) + tag = release["tag_name"] + if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", tag) or tag.endswith("."): + raise ValueError("Invalid release tag") + name = f"web-{tag}.zip" + matches = [asset for asset in release["assets"] if asset["name"] == name] + if len(matches) != 1: + raise ValueError(f"Missing or ambiguous release asset: {name}; publish a release with the web converter") + asset = matches[0] + if not 0 < asset["size"] <= 128 * 1024 * 1024: + raise ValueError("Invalid web release archive size") + with request(asset["browser_download_url"]) as response: + data = response.read(asset["size"] + 1) + if len(data) != asset["size"]: + raise ValueError("Incomplete or oversized web release archive") + checksum = hashlib.sha256(data).hexdigest() + if asset.get("digest") and asset["digest"] != "sha256:" + checksum: + raise ValueError("Web release archive checksum mismatch") + if output.is_symlink() or (output.exists() and not output.is_dir()): + raise ValueError(f"Release output is not a directory: {output}") + output.parent.mkdir(parents=True, exist_ok=True) + temporary = Path(tempfile.mkdtemp(prefix=".web-release-", dir=output.parent)) + staging = temporary / "site" + previous = temporary / "previous" + published = False + try: + staging.mkdir() + with zipfile.ZipFile(io.BytesIO(data)) as archive: + files = archive.infolist() + expected = {*WEB_FILES, "web-release.json"} + if len(files) != len(expected) or {file.filename for file in files} != expected: + raise ValueError("Invalid web release archive layout") + if any(file.file_size == 0 for file in files) or sum(file.file_size for file in files) > 128 * 1024 * 1024: + raise ValueError("Invalid expanded web release archive size") + metadata = json.loads(archive.read("web-release.json")) + if (not isinstance(metadata, dict) or metadata.get("tag") != tag + or not isinstance(metadata.get("revision"), str) + or not re.fullmatch(r"[0-9a-f]{40}", metadata["revision"])): + raise ValueError("Web converter does not match the selected release") + for file in files: + target = staging / file.filename + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(archive.read(file)) + publish_libraries(staging, release) + manifest = json.loads((staging / "release.json").read_text(encoding="utf-8-sig")) + manifest["revision"] = metadata["revision"] + (staging / "release.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + (staging / ".nojekyll").touch() + if output.exists(): + output.rename(previous) + try: + staging.rename(output) + except OSError: + if previous.exists(): + try: + previous.rename(output) + except OSError as error: + raise RuntimeError(f"Could not restore release output. Previous site is preserved at {previous}") from error + raise + published = True + finally: + if published or not previous.exists(): + shutil.rmtree(temporary) + print(f"Published web converter and libraries from {tag} ({metadata['revision']})") + + +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser() parser.add_argument("--build", type=Path, required=True) - parser.add_argument("--libraries", action="store_true") - args = parser.parse_args() + parser.add_argument("--release", "--libraries", dest="release", action="store_true", + help="Download the matching converter and libraries from the latest release") + args = parser.parse_args(argv) output = args.build.resolve() / "web" - for name in ("relinker_wasm.mjs", "relinker_wasm.wasm"): - if not (output / "native" / name).is_file(): - raise ValueError(f"Build relinker_wasm first: missing {name}") - copy_sources(output) - if args.libraries: - publish_libraries(output) - print(output) + try: + if args.release: + publish_release(output) + print(output) + return 0 + for name in ("relinker_wasm.mjs", "relinker_wasm.wasm"): + if not (output / "native" / name).is_file(): + parser.error(f"Build relinker_wasm first: missing {name}") + copy_sources(output) + print(output) + return 0 + except (OSError, ValueError) as error: + parser.error(str(error)) if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/tools/check_compile.py b/tools/check_compile.py index 1ec01a433c..9b66a1cd16 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -6,18 +6,33 @@ import shlex import shutil import subprocess import sys +from concurrent.futures import ThreadPoolExecutor +from contextlib import nullcontext from pathlib import Path CPP = (".cpp", ".cc", ".cxx", ".c") DROP = ("-c", "-MD", "-MMD", "-MP") -DROP_WITH_VALUE = ("-o", "-MT", "-MF") +DROP_WITH_VALUE = ("-o", "-MT", "-MF", "-MQ", "-MJ") -def capture(command, cwd=None): - return subprocess.run(command, capture_output=True, text=True, encoding="utf-8", +def capture(command, cwd=None, encoding="utf-8"): + return subprocess.run(command, capture_output=True, text=True, encoding=encoding, errors="replace", cwd=cwd) +def ninja_encoding(ninja): + if sys.platform != "win32": + return "utf-8" + result = capture([ninja, "-t", "wincodepage"]) + if result.returncode != 0: + raise ValueError(f"ninja -t wincodepage: {result.stderr.strip() or result.stdout.strip()}") + declared = result.stdout.strip() + encodings = {"Build file encoding: UTF-8": "utf-8", "Build file encoding: ANSI": "mbcs"} + if declared not in encodings: + raise ValueError(f"unsupported Ninja build-file encoding: {declared!r}") + return encodings[declared] + + def repository(): result = capture(["git", "rev-parse", "--show-toplevel"]) if result.returncode != 0: @@ -30,35 +45,40 @@ def entries(build): raise ValueError(f"{build} is not a directory; configure the build first") manifest = build / "compile_commands.json" if manifest.is_file(): - return json.loads(manifest.read_text(encoding="utf-8")) + return json.loads(manifest.read_text(encoding="utf-8-sig")) 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"], encoding=ninja_encoding(ninja)) 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 +88,39 @@ 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") + for argument in arguments[1:]: + if argument.startswith("@"): + raise ValueError(f"unexpanded response file {argument[1:]}; provide expanded compile arguments before checking syntax") wanted = source.resolve() trimmed = [] index = 0 @@ -79,7 +129,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(("-MT", "-MF", "-MQ", "-MJ")): index += 1 continue if not argument.startswith("-"): @@ -95,33 +145,62 @@ def syntax_only(entry, source): return trimmed -def check(root, build, files): - commands = index(build) - checked = 0 - failed = 0 - for file in files: - source = file.resolve() - name = os.path.relpath(source, root).replace(os.sep, "/") - entry = commands.get(name) - if entry is None: - print(f"{name}: no compile command in {build}", file=sys.stderr) - continue - checked += 1 - result = subprocess.run(syntax_only(entry, source), cwd=entry["directory"], +def check_source(task): + source, name, build, configurations = task + if configurations is None: + return 0, 0, f"{name}: no compile command in {build}\n" + failed = False + diagnostics = [] + for entry in configurations: + command = (syntax_arguments(entry["arguments"], Path(entry["directory"]), source) + if "arguments" in entry else syntax_only(entry, source)) + result = subprocess.run(command, cwd=entry["directory"], capture_output=True, text=True, encoding="utf-8", errors="replace") if result.returncode == 0: continue - failed += 1 + failed = True output = (result.stdout or "") + (result.stderr or "") - sys.stderr.write(output if output.endswith("\n") else output + "\n") + diagnostics.append(output if output.endswith("\n") else output + "\n") + return 1, int(failed), "".join(diagnostics) + + +def check(root, build, files, jobs=1): + commands = index(build, root) + tasks = [] + for file in files: + source = file.resolve() + try: + name = source.relative_to(root.resolve()).as_posix() + except ValueError: + name = source.as_posix() + tasks.append((source, name, build, commands.get(name))) + checked = failed = 0 + with (ThreadPoolExecutor(max_workers=jobs) if jobs > 1 else nullcontext()) as executor: + results = executor.map(check_source, tasks) if executor is not None else map(check_source, tasks) + for count, failures, diagnostics in results: + checked += count + failed += failures + sys.stderr.write(diagnostics) return checked, failed +def positive_integer(value): + number = int(value) + if number <= 0: + raise argparse.ArgumentTypeError("must be a positive integer") + return number + + def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + 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"), help="a configured Ninja build directory of this checkout (default: build)") + parser.add_argument("--jobs", type=positive_integer, default=1, metavar="N", + help="check up to N translation units concurrently; diagnostics retain input order (default 1)") parser.add_argument("--base", default="origin/main", help="branch the change is against (default: origin/main)") parser.add_argument("paths", nargs="*", help="translation units to check instead of the changed ones") @@ -132,7 +211,7 @@ def main(argv=None): if not files: print(f"no changed translation unit against {args.base}") return 0 - checked, failed = check(root, args.build, files) + checked, failed = check(root, args.build, files, args.jobs) except (json.JSONDecodeError, OSError, ValueError) as error: parser.error(str(error)) if failed: diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 61b187c984..675981e57b 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -1,84 +1,128 @@ 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 sys.path.insert(0, str(Path(__file__).resolve().parent)) -from nid_names import CPP_TOKEN +from nid_names import CPP_TOKEN, NID_POSTFIX, compute_nid, source_aliases +import env_reference RULES = { "comment": ("error", "docs/dev/CONVENTIONS.md", "Comments are only for technical debt, so they need a change to docs/dev/TechnicalDebt.md or docs/dev/debt/ in the same pull request; #endif and namespace ends are always allowed"), "silent-stub": ("error", "CONTRIBUTING.md#code", "Unimplemented exports call NotImplemented_nid_no_patch(__func__); silent stubs are listed under Silent stubs in docs/dev/debt/<library>.md, by their file or by name with a path in their library"), - "debt-file": ("error", "CONTRIBUTING.md#documentation", "Technical debt about one library or component goes in docs/dev/debt/<name>.md, named after the first core/ path it links: <library> for core/libs/prx/<library>, otherwise <component> for core/<component>"), + "debt-file": ("error", "CONTRIBUTING.md#documentation", "Technical debt about one library or component goes in docs/dev/debt/<name>.md, named after the first core/ path it links: <library> for core/libs/prx/<library>, otherwise <component> for core/<component>; entries that link neither stay in docs/dev/TechnicalDebt.md"), "duplicate-export": ("error", "CONTRIBUTING.md#code", "Already exported by another library: remove the copy, or use APS5_DUMMY_FUN if nothing is left"), + "unknown-nid": ("error", "CONTRIBUTING.md#code", "An Unknown export has the NID of a symbol in core/libs/prx, so the NID is not unknown: name the export after that symbol, or give an AnyPS5 helper the _nid_no_patch suffix"), "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"), "commit-subject": ("error", "docs/dev/CONVENTIONS.md", "Commit subject is not Conventional Commits"), "pr-title": ("error", "docs/dev/CONVENTIONS.md", "Pull request title is not Conventional Commits"), "pr-template": ("error", ".github/pull_request_template.md", "Fill in the pull request template"), + "env-reference": ("error", "docs/dev/ENVIRONMENT.md", "Every APS5_* or ANYPS5_* name a pull request adds to the code has a row in docs/dev/ENVIRONMENT.md, a row whose name the pull request stops reading is removed, and a row the pull request removes is no longer read"), + "debt-length": ("warning", "CONTRIBUTING.md#documentation", "Technical debt entry over 1000 characters: state the gap first (what differs from the console, what is unverified, what throws) and keep only the behaviour it needs"), } CPP = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl", ".ipp"} IMAGES = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".webp", ".ico", ".tga", ".dds"} NOTES = {".md", ".markdown", ".rst", ".log", ".patch", ".diff"} -AGENT_FILES = {"agents.md", "claude.md", "gemini.md", ".cursorrules", "copilot-instructions.md"} +AGENT_FILES = { + "agents.md", "claude.md", "gemini.md", ".cursorrules", "copilot-instructions.md", + ".windsurfrules", "windsurf.md", "chatgpt.md", ".chatgptrules", ".aider.conf.yml", +} DEBT = "docs/dev/TechnicalDebt.md" DEBT_DIR = "docs/dev/debt/" DEBT_TARGET = re.compile(r"\]\((?:\.\./)+(core/[^)#\s]+)") +DEBT_ENTRY_LIMIT = 1000 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<comment>#\[(?P<level>=*)\[.*?\](?P=level)\]|#[^\n]*)|\\.', re.S) EXPORT = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\(") +IDENTIFIER = re.compile(r"[A-Za-z_][A-Za-z0-9_]*") +NID_SOURCES = r"APS5_EXPORT|APS5_VABI|_nid_postfix|ASM_FUNCTION" STUB_BODY = re.compile(r"^(?:\(void\)\s*\w+\s*;|static_cast<void>\(\s*\w+\s*\)\s*;)*" r"(?:return\s*\(?\s*(?:-?(?:0x[0-9a-fA-F]+|\d+)[uUlL]*|nullptr|true|false|\w*_OK)?\s*\)?\s*;)?$") SYSTEM_DEPENDENCY = re.compile(r"\b(find_package\s*\((?!\s*(Python3|Threads|Git)\b)|pkg_check_modules\b|pkg_search_module\b|find_(library|path|program|file)\b)", re.I) 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]+)") +LINK = re.compile(r"\[[^\]]+\]\(([^)\s]+)") +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*$") + def git(*args, text=True): - output = subprocess.run(["git", "-c", "core.quotePath=false", *args], capture_output=True, check=True).stdout + try: + result = subprocess.run( + ["git", "-c", "core.quotePath=false", *args], + capture_output=True, + check=True, + ) + except subprocess.CalledProcessError as error: + message = error.stderr.decode("utf-8", errors="replace").strip() + print( + f"Git command failed: {message or 'unknown Git error'}", + file=sys.stderr, + ) + raise SystemExit(2) from None + except FileNotFoundError: + print("Git executable not found. Please install Git.", file=sys.stderr) + raise SystemExit(2) from None + + output = result.stdout return output.decode("utf-8", errors="replace") if text else output + class Check: def __init__(self, base, head): self.head = head + self.target = base self.base = git("merge-base", base, head).strip() self.findings = [] self.added = defaultdict(list) self.status = {} self.numstat = {} - entries = [entry.split("\t", 1) for entry in git("ls-tree", "-rz", "--full-tree", head).split("\0") if entry] - self.modes = {path: metadata.split()[0] for metadata, path in entries} + entries = [(metadata.split(), path) for metadata, path in + (entry.split("\t", 1) for entry in git("ls-tree", "-rz", "--full-tree", head).split("\0") if entry)] + self.modes = {path: fields[0] for fields, path in entries} + self.objects = {path: fields[2] for fields, path in entries if fields[1] == "blob"} + self.blobs = {} self.tree = set(self.modes) self.dirs = {str(parent) for path in self.tree for parent in PurePosixPath(path).parents} - statuses = git("diff", "--name-status", "--no-renames", "-z", self.base, head).split("\0") - for status, path in zip(statuses[::2], statuses[1::2]): - self.status[path] = status - for line in git("diff", "--numstat", "--no-renames", "-z", self.base, head).split("\0"): - if not line: - continue + diff = ("--find-renames", "-l0", self.base, head) + statuses = iter(git("diff", "--name-status", "-z", *diff).split("\0")[:-1]) + for status in statuses: + path = next(statuses) + if status.startswith("R"): + path = next(statuses) + self.status[path] = status[0] + statistics = iter(git("diff", "--numstat", "-z", *diff).split("\0")[:-1]) + for line in statistics: added, deleted, path = line.split("\t", 2) + if not path: + next(statistics) + path = next(statistics) self.numstat[path] = (added, deleted) - paths = iter(self.status) + paths = iter([path for path, status in self.status.items() for _ in range(2 if status == "T" else 1)]) path, number = None, None - for line in git("diff", "-U0", "--no-color", "--no-renames", self.base, head).split("\n"): + for line in git("diff", "-U0", "--no-color", *diff).split("\n"): if line.startswith("diff --git "): path, number = next(paths), None elif line.startswith("@@"): @@ -86,6 +130,9 @@ class Check: elif path and number is not None and line.startswith("+"): self.added[path].append((number, line[1:])) number += 1 + self.read([path for path in sorted(self.tree) if is_debt(path)] + + [path for path, (added, _) in self.numstat.items() if self.status.get(path) != "D" and added != "-" and + not path.startswith("3rdparty/") and PurePosixPath(path).suffix.lower() not in IMAGES]) def report(self, rule, path, line, detail=""): self.findings.append((rule, path, line, detail)) @@ -93,16 +140,38 @@ class Check: def files(self, predicate): return [(path, lines) for path, lines in self.added.items() if not path.startswith("3rdparty/") and predicate(PurePosixPath(path))] + def read(self, paths): + pending = [path for path in dict.fromkeys(paths) if path in self.objects and path not in self.blobs] + if not pending: + return + data = subprocess.run(["git", "cat-file", "--batch"], input="".join(f"{self.objects[path]}\n" for path in pending).encode(), + capture_output=True, check=True).stdout + offset = 0 + for path in pending: + end = data.index(b"\n", offset) + header = data[offset:end].split() + offset = end + 1 + if len(header) == 3 and header[1] == b"blob": + size = int(header[2]) + self.blobs[path] = data[offset:offset + size] + offset += size + 1 + + def blob(self, path): + data = self.blobs.get(path) + return git("show", f"{self.head}:{path}", text=False) if data is None else data + def show(self, path): - return git("show", f"{self.head}:{path}") + return self.blob(path).decode("utf-8", errors="replace") def code(self): debt = [entry for path in sorted(self.tree) if is_debt(path) for entry in silent_stub_debt(self.show(path), path)] exports = {} + unknowns, symbols = [], [] for path, lines in self.files(lambda p: p.suffix in CPP): core = path.startswith("core/") and "tests" not in PurePosixPath(path).parts source = self.show(path).split("\n") masked = cpp_text("\n".join(source), preserve_comments=True).split("\n") + assembly = asm_exports("\n".join(source)) if core and path.startswith("core/libs/prx/") else {} for number, text in lines: bare = masked[number - 1] if ("//" in bare or "/*" in bare) and not ALLOWED_COMMENT.match(bare): @@ -111,6 +180,10 @@ class Check: self.report("extension", path, number, EXTENSION.search(bare).group(0)) if core and TITLE.search(text): self.report("title-specific", path, number, TITLE.search(text).group(0)) + if core and path.startswith("core/libs/prx/"): + unknowns.extend((nid, name, path, number) for nid, name in source_aliases(text) if "unknown" in name.lower()) + symbols.extend((symbol, path, number) for symbol in nid_symbols(text)) + exports.update((name, (path, number)) for name in assembly.get(number, ()) if not name.endswith(("_nid_no_patch", "_nid_no_patch_cut"))) match = EXPORT.search(bare) if match and core and path.startswith("core/libs/prx/") and not match.group(1).endswith(("_nid_no_patch", "_nid_no_patch_cut")): source = source or self.show(path).splitlines() @@ -121,9 +194,14 @@ class Check: if match.group(1).startswith("sce") and STUB_BODY.match(body) and not listed(debt, match.group(1), path): self.report("silent-stub", path, number, match.group(1)) self.duplicates(exports) + self.unknown_nids(unknowns, symbols) 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: - if "#" in STRINGS.sub('""', text) and not text.startswith("#!"): + shebang = text.startswith("#!") or (number == 1 and text.startswith("\ufeff#!")) + 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"] @@ -131,14 +209,44 @@ class Check: def duplicates(self, exports): 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") - for line in filter(None, result.stdout.split("\n")): - _, path, number, text = line.split(":", 3) - name = EXPORT.search(text).group(1) - own, own_number = exports[name] - if library(path) != library(own) and "tests" not in PurePosixPath(path).parts and function_body(self.show(path).splitlines(), int(number) - 1) is not None: - self.report("duplicate-export", own, own_number, f"{name} is also in {library(path)}") + paths = sorted(path for path in self.objects if path.startswith("core/libs/prx/") and + "tests" not in PurePosixPath(path).parts) + self.read(paths) + for path in paths: + text = self.show(path) + if "APS5_VABI" not in text and "ASM_FUNCTION" not in text: + continue + source = cpp_text(text).split("\n") + assembly = asm_exports(text) + for number, line in enumerate(source): + names = [match.group(1) for match in EXPORT.finditer(line) + if match.group(1) in exports and library(path) != library(exports[match.group(1)][0]) + and function_body(source, number) is not None] + names += [name for name in assembly.get(number + 1, ()) if name in exports and library(path) != library(exports[name][0])] + for name in names: + own, own_number = exports[name] + self.report("duplicate-export", own, own_number, f"{name} is also in {library(path)}") + + def unknown_nids(self, unknowns, symbols): + if not unknowns and not symbols: + return + result = subprocess.run(["git", "grep", "-nE", "--full-name", NID_SOURCES, self.head, "--", ":(top)core/libs/prx"], capture_output=True, encoding="utf-8", errors="replace") + tree_unknowns, tree_symbols = defaultdict(list), defaultdict(list) + for path, number, text in (line.split(":", 3)[1:] for line in filter(None, result.stdout.split("\n"))): + if "tests" in PurePosixPath(path).parts or PurePosixPath(path).suffix not in CPP: + continue + for nid, name in source_aliases(text): + if "unknown" in name.lower(): + tree_unknowns[nid].append(f"{name} at {path}:{number}") + for symbol in nid_symbols(text): + tree_symbols[symbol_nid(symbol)].append(f"{symbol} at {path}:{number}") + for nid, name, path, number in unknowns: + if tree_symbols[nid]: + self.report("unknown-nid", path, number, f"{nid} ({name}) is {tree_symbols[nid][0]}") + for symbol, path, number in symbols: + nid = symbol_nid(symbol) + if tree_unknowns[nid]: + self.report("unknown-nid", path, number, f"{symbol} has the NID {nid} of {tree_unknowns[nid][0]}") def build(self): for path, lines in self.files(lambda p: p.name == "CMakeLists.txt" or p.suffix == ".cmake"): @@ -152,6 +260,16 @@ class Check: if match and not match.group(1).startswith("3rdparty/"): self.report("system-dependency", ".gitmodules", number, match.group(1)) + def shaders(self): + if any(is_debt(path) for path in self.status): + return + for path, lines in self.files(lambda p: p.suffix in (".comp", ".frag", ".vert", ".glsl")): + masked = cpp_text(self.show(path), preserve_comments=True).split("\n") + for number, _ in lines: + bare = masked[number - 1] + if ("//" in bare or "/*" in bare) and not ALLOWED_COMMENT.match(bare): + self.report("comment", path, number) + def submodules(self): result = subprocess.run(["git", "config", "--no-includes", "-z", "--blob", f"{self.head}:.gitmodules", "--list"], capture_output=True, encoding="utf-8", errors="replace") @@ -171,21 +289,36 @@ class Check: self.report("system-dependency", path, 0, "submodule outside 3rdparty/") continue suffix = PurePosixPath(path).suffix.lower() - if self.status.get(path) != "D" and (added == "-" or suffix in IMAGES or not utf8(git("show", f"{self.head}:{path}", text=False))): + if self.status.get(path) != "D" and (added == "-" or suffix in IMAGES or not utf8(self.blob(path))): self.report("binary", path, 0) - if self.status.get(path) == "A" and allowed_notes(path) is False: + if self.status.get(path) in ("A", "R") and allowed_notes(path) is False: self.report("notes-file", path, 0) def docs(self): - for path, lines in self.files(lambda p: p.suffix == ".md"): + files = dict(self.files(lambda p: p.suffix == ".md")) + for path, status in self.status.items(): + if status == "R" and not path.startswith("3rdparty/") and PurePosixPath(path).suffix == ".md": + files[path] = list(enumerate(self.show(path).split("\n"), 1)) + for path, lines in files.items(): + fenced = fenced_lines(self.show(path)) for number, text in lines: - name = debt_name(text) if is_debt(path) and text.startswith("- ") else None - if name and path != f"{DEBT_DIR}{name}.md": - self.report("debt-file", path, number, f"{DEBT_DIR}{name}.md") + if is_debt(path) and text.startswith("- "): + name = debt_name(text) + expected = f"{DEBT_DIR}{name}.md" if name else DEBT + if path != expected: + self.report("debt-file", path, number, expected) + if number in fenced: + continue for target, resolved in links(path, text): if resolved not in self.tree and resolved not in self.dirs: self.report("doc-link", path, number, target) + def debt_entries(self): + for path, lines in self.files(lambda p: is_debt(p.as_posix())): + for number, text in lines: + if long_debt_entry(text): + self.report("debt-length", path, number, f"{len(text)} characters") + def workflows(self): for path in sorted(self.status): if self.status[path] == "D" or not re.fullmatch(r"\.github/workflows/[^/]+\.ya?ml", path): @@ -206,6 +339,46 @@ class Check: if not CONVENTIONAL.match(subject): self.report("commit-subject", "", 0, f"{sha} {subject}") + def env_reference(self): + doc = "docs/dev/ENVIRONMENT.md" + added, removed, dropped = {}, set(), set() + path, number, in_hunk = "", 0, False + for line in git("diff", "-U0", self.base, self.head, "--", "core", doc).splitlines(): + if line.startswith("diff --git"): + path, in_hunk = "", False + elif line.startswith("@@"): + number = int(re.search(r"\+(\d+)", line).group(1)) - 1 + in_hunk = True + elif in_hunk: + if path == doc: + if line.startswith("-"): + dropped.update(env_reference.documented_pattern(env_reference.PREFIXES).findall(line[1:])) + elif line.startswith("+"): + number += 1 + for _, match in env_reference.line_reads(line[1:]): + added.setdefault(match.group("name"), (path, number)) + elif line.startswith("-"): + removed.update(match.group("name") for _, match in env_reference.line_reads(line[1:])) + elif line.startswith("+++ b/"): + path = line[6:] + result = subprocess.run(["git", "grep", "-nE", env_reference.GREP, self.head, "--", "core"], + capture_output=True, encoding="utf-8", errors="replace") + reads = set() + for line in result.stdout.splitlines(): + reads.update(match.group("name") for _, match in env_reference.line_reads(line)) + pattern = env_reference.documented_pattern(env_reference.PREFIXES) + documented = set(pattern.findall(self.show(doc))) if doc in self.tree else set() + if git("ls-tree", "--name-only", self.target, "--", doc).strip() == doc: + documented |= set(pattern.findall(git("show", f"{self.target}:{doc}"))) - dropped + for name, (source, line) in sorted(added.items()): + if name not in documented: + self.report("env-reference", source, line, f"{name} is read here but has no row in {doc}") + for name in sorted(removed - reads): + if name in documented: + self.report("env-reference", doc, 0, f"{name} has a row but the code no longer reads it") + for name in sorted(dropped & reads - documented): + self.report("env-reference", doc, 0, f"{name} is still read by the code but this pull request removes its row") + def pull_request(self, title, body): if not CONVENTIONAL.match(title): self.report("pr-title", "", 0, title) @@ -219,7 +392,7 @@ class Check: self.report("pr-template", "", 0, f"unchecked: {item.strip()}") if checklist and not re.search(r"AI-assisted:\s*(yes|no)\b", checklist, re.I): self.report("pr-template", "", 0, "answer AI-assisted with yes or no") - if checklist and not re.search(r"Depends on:\s*(#\d+|none)\b", checklist, re.I): + if checklist and not re.search(r"Depends on:\s*(?:[-*+]\s+)?(#\d+|none)\b", checklist, re.I): self.report("pr-template", "", 0, "fill Depends on with #N or none") @@ -231,8 +404,17 @@ def utf8(data): return True +def fenced_lines(text): + inside = set() + for match in FENCE.finditer(text): + first = text.count("\n", 0, match.start()) + 1 + inside.update(range(first, first + match.group(0).count("\n") + 1)) + return inside + + def links(path, text): result = [] + text = INLINE.sub("code", FENCE.sub("", text)) for target in LINK.findall(text): if re.match(r"[A-Za-z][A-Za-z0-9+.-]*:|//|#", target): continue @@ -246,6 +428,10 @@ def is_debt(path): return path == DEBT or (path.startswith(DEBT_DIR) and path.endswith(".md") and "/" not in path[len(DEBT_DIR):]) +def long_debt_entry(text): + return text.startswith("- ") and len(text) > DEBT_ENTRY_LIMIT + + def debt_name(text): match = DEBT_TARGET.search(text) if not match: @@ -307,7 +493,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 @@ -325,15 +513,63 @@ def library(path): return PurePosixPath(path).parts[3] +def symbol_nid(name): + if name.endswith(("_nid_no_patch", "_nid_no_patch_cut")) or name.startswith("SDL_"): + return None + base = re.sub(r"_nid_disambig\d+$", "", name.removesuffix(NID_POSTFIX)) + return compute_nid(base) if base else None + + +def asm_exports(text): + tokens = [match for match in CPP_TOKEN.finditer(text) if match.group("comment") is None] + exports = defaultdict(list) + for index, match in enumerate(tokens[:-2]): + name = tokens[index + 2].group()[1:-1] + if match.group() in ("APS5_ASM_FUNCTION", "APS5_SIGNAL_ASM_FUNCTION") and tokens[index + 1].group() == "(" and tokens[index + 2].group("string") is not None and IDENTIFIER.fullmatch(name): + exports[text.count("\n", 0, tokens[index + 2].start()) + 1].append(name) + return exports + + +def nid_symbols(text): + tokens = [match for match in CPP_TOKEN.finditer(text) if match.group("comment") is None] + names = [] + for index, match in enumerate(tokens): + token, following = match.group(), tokens[index + 1:index + 3] + if token == "APS5_VABI" and following and IDENTIFIER.fullmatch(following[0].group()): + names.append(following[0].group()) + elif match.group("code") is not None and token.endswith(NID_POSTFIX): + names.append(token) + elif token in ("APS5_ASM_FUNCTION", "APS5_SIGNAL_ASM_FUNCTION") and len(following) == 2 and following[0].group() == "(" and following[1].group("string") is not None: + names.append(following[1].group()[1:-1]) + return [name for name in dict.fromkeys(names) if symbol_nid(name)] + + 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: @@ -401,10 +637,13 @@ if __name__ == "__main__": check = Check(args.base, args.head) check.code() check.build() + check.shaders() check.repository() check.docs() + check.debt_entries() check.workflows() check.commits() + check.env_reference() pr = json.load(open(args.event, encoding="utf-8")).get("pull_request") if args.event else None if pr: check.pull_request(pr["title"], pr["body"]) diff --git a/tools/check_main_sync.py b/tools/check_main_sync.py new file mode 100644 index 0000000000..63d952ee12 --- /dev/null +++ b/tools/check_main_sync.py @@ -0,0 +1,130 @@ +import argparse +import re +import subprocess +import sys +from pathlib import Path + +PULL_REQUEST = re.compile(r"Merge pull request #(\d+)") +BASES = ("upstream/main", "origin/main", "main") + + +def git(repo, *args, check=True): + result = subprocess.run(["git", "-C", str(repo), *args], capture_output=True, text=True, encoding="utf-8", errors="replace") + if check and result.returncode: + raise RuntimeError(f"git {' '.join(args[:2])} failed: {result.stderr.strip()}") + return result + + +def lines(repo, *args): + return [line for line in git(repo, *args).stdout.splitlines() if line] + + +def names(repo, *args): + return [name for name in git(repo, *args).stdout.split("\0") if name] + + +def resolve_base(repo, name): + for candidate in (name,) if name else BASES: + if git(repo, "rev-parse", "--verify", "--quiet", f"{candidate}^{{commit}}", check=False).returncode == 0: + return candidate + raise RuntimeError(f"no base ref found ({name or ', '.join(BASES)}); fetch the upstream remote first") + + +def conflicts(repo, base, head): + result = git(repo, "merge-tree", "-z", "--write-tree", "--name-only", "--no-messages", base, head, check=False) + if result.returncode not in (0, 1): + raise RuntimeError(f"git merge-tree failed: {result.stderr.strip()}") + if result.returncode == 0: + return [] + fields = result.stdout.split("\0")[1:] + return fields[:fields.index("")] if "" in fields else fields + + +def merged_pull_requests(repo, mergebase, base, paths): + found = {} + if not paths: + return found + for line in lines(repo, "-c", "core.quotepath=off", "log", "--first-parent", "--format=%h %s", f"{mergebase}..{base}", "--", *paths): + match = PULL_REQUEST.search(line) + if match: + found[int(match.group(1))] = line.split(" ", 1)[0] + return found + + +def report(repo, base, head): + found = git(repo, "merge-base", base, head, check=False) + if found.returncode: + raise RuntimeError(f"{head} and {base} have no common ancestor") + mergebase = found.stdout.strip() + behind = int(git(repo, "rev-list", "--count", f"{head}..{base}").stdout) + ahead = int(git(repo, "rev-list", "--count", f"{base}..{head}").stdout) + mine = set(names(repo, "diff", "-z", "--name-only", mergebase, head)) + theirs = set(names(repo, "diff", "-z", "--name-only", mergebase, base)) + shared = sorted(mine & theirs) + return { + "base": base, + "head": head, + "mergebase": mergebase, + "behind": behind, + "ahead": ahead, + "conflicts": conflicts(repo, base, head), + "shared": shared, + "pulls": merged_pull_requests(repo, mergebase, base, shared), + } + + +def render(data): + out = [f"{data['head']} is {data['ahead']} ahead and {data['behind']} behind {data['base']} (merge base {data['mergebase'][:9]})"] + if data["conflicts"]: + out.append(f"merge: CONFLICT in {len(data['conflicts'])} file(s)") + out += [f" {name}" for name in data["conflicts"]] + else: + out.append("merge: clean") + if data["shared"]: + out.append(f"{len(data['shared'])} file(s) changed on both sides since the merge base:") + out += [f" {name}" for name in data["shared"]] + if data["pulls"]: + out.append("merged pull requests that touched them: " + ", ".join(f"#{number}" for number in sorted(data["pulls"]))) + return "\n".join(out) + + +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(errors="backslashreplace") + parser = argparse.ArgumentParser(description="report whether a branch is in step with the upstream main before you push or test") + parser.add_argument("--base", help="the main ref to compare against (default: upstream/main, then origin/main, then main)") + parser.add_argument("--head", default="HEAD", help="the branch or commit to check (default: HEAD)") + parser.add_argument("--repo", type=Path, default=Path("."), help="the repository to check (default: the current directory)") + parser.add_argument("--fetch", action="store_true", help="fetch the base's remote first") + parser.add_argument("--strict", action="store_true", help="also fail when the branch is behind the base") + args = parser.parse_args(argv) + try: + 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: + if args.base: + fetch(args.base) + else: + remotes = set(lines(args.repo, "remote")) + for candidate in BASES: + remote, _, branch = candidate.partition("/") + if branch and remote in remotes: + fetch(candidate) + break + base = resolve_base(args.repo, args.base) + data = report(args.repo, base, args.head) + except RuntimeError as error: + parser.error(str(error)) + print(render(data)) + if data["conflicts"] or (args.strict and data["behind"]): + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/check_skips.py b/tools/check_skips.py index 0cc51cacb3..c837fb1096 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -1,3 +1,4 @@ +import sys import argparse import re import xml.etree.ElementTree as ET @@ -7,6 +8,8 @@ DEVICE_LIMITATION = re.compile(r"skipped,.*(subgroup|the device (has no|reports| NO_DISPLAY = re.compile(r"skipped,.*no display", re.IGNORECASE) +NO_VULKAN_DEVICE = re.compile(r"skipped,.*no usable Vulkan device", re.IGNORECASE) + HIGH_REFRESH_LIMITATION = re.compile(r"current display does not provide 120 Hz", re.IGNORECASE) WINDOWS_PREFIX = "agc" @@ -33,28 +36,31 @@ def skips(path): name = case.get("name") if not name: raise ValueError(f"{path}: skipped testcase has no name") - output = ["skipped, " + reason for reason in (skipped.get("message", ""), skipped.text or "") if reason] + output = ["skipped, " + reason.strip() for reason in (skipped.get("message", ""), skipped.text or "") if reason.strip()] output += [case.findtext(channel) or "" for channel in ("system-out", "system-err")] yield name, "\n".join(output) -if __name__ == "__main__": +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="fail when a test is skipped that the job's device is not expected to skip") parser.add_argument("junit", type=Path, help="the file written by ctest --output-junit") parser.add_argument("--job", choices=("linux", "windows"), default="linux") parser.add_argument("--list", action="store_true", help="print the skipped tests and why the file says they skipped") - args = parser.parse_args() + args = parser.parse_args(argv) try: found = sorted(skips(args.junit)) except (OSError, ET.ParseError, ValueError) as error: 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 + return 0 if args.job == "windows": - unexpected = [name for name, output in found if not name.startswith(WINDOWS_PREFIX) and not NO_DISPLAY.search(output) and not high_refresh_limitation(output)] + unexpected = [name for name, output in found if not name.startswith(WINDOWS_PREFIX) and not NO_DISPLAY.search(output) and not NO_VULKAN_DEVICE.search(output) and not high_refresh_limitation(output)] expected = f"any {WINDOWS_PREFIX}* test, which the runner has no Vulkan driver for, or a test that reports no display or Vulkan device or a display without a 120 Hz mode" else: unexpected = [name for name, output in found if not DEVICE_LIMITATION.search(output) and not high_refresh_limitation(output)] @@ -63,5 +69,10 @@ if __name__ == "__main__": print(f"error: {name} skipped") if unexpected: print(f"{len(unexpected)} test(s) skipped for a reason that is not expected: the job lost that coverage and ctest still reports every test as passed") - raise SystemExit(1) + return 1 print(f"{len(found)} test(s) skipped, all expected ({expected})") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 7b00d94d3e..bbc80508d8 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -1,18 +1,22 @@ import argparse import re import subprocess +import sys from pathlib import Path TEST_OUTPUT = r"tests[\\/]" COMMAND = re.compile(r"^\s*(add_test|set_tests_properties|subdirs)\s*\(", re.M | re.I) ARGUMENT = re.compile(r'"(?:\\.|[^"\\])*"|\[(?P<equals>=*)\[.*?\](?P=equals)\]|[^\s()]+', re.S) +LEXICAL = re.compile(r'\\.|"(?:\\.|[^"\\])*"|\[(?P<literal_equals>=*)\[.*?\](?P=literal_equals)\]' + r'|(?P<comment>\#\[(?P<comment_equals>=*)\[.*?\](?P=comment_equals)\]|\#[^\n]*)', re.S) KNOWN_UNRUN = set() +SCRIPT = re.compile(r"(?:[\w.-]+/)*test_\w+\.py") def built(build): program, generator = None, None - for line in (Path(build) / "CMakeCache.txt").read_text(encoding="utf-8").splitlines(): + for line in (Path(build) / "CMakeCache.txt").read_text(encoding="utf-8-sig").splitlines(): if line.startswith("CMAKE_MAKE_PROGRAM:"): program = line.partition("=")[2] elif line.startswith("CMAKE_GENERATOR:"): @@ -21,8 +25,16 @@ def built(build): raise ValueError(f"test registration audit requires a Ninja generator, got {generator!r}") if not program or program.endswith("-NOTFOUND"): raise FileNotFoundError(f"CMAKE_MAKE_PROGRAM is not set in {Path(build) / 'CMakeCache.txt'}") + encoding = "utf-8" + if sys.platform == "win32": + codepage = subprocess.run([program, "-t", "wincodepage"], capture_output=True, text=True, + encoding="ascii", check=True).stdout.strip() + encodings = {"Build file encoding: UTF-8": "utf-8", "Build file encoding: ANSI": "mbcs"} + if codepage not in encodings: + raise ValueError(f"unsupported Ninja build-file encoding: {codepage!r}") + encoding = encodings[codepage] listed = subprocess.run([program, "-C", str(build), "-t", "targets", "all"], - capture_output=True, text=True, check=True).stdout + capture_output=True, text=True, encoding=encoding, check=True).stdout names = set() for line in listed.splitlines(): path, _, rule = line.partition(": ") @@ -40,8 +52,11 @@ def built(build): def commands(content): + content = LEXICAL.sub(lambda match: re.sub(r"[^\n]", " ", match.group()) + if match.group("comment") is not None else match.group(), content) + code = LEXICAL.sub(lambda match: re.sub(r"[^\n]", "_", match.group()), content) parsed_until = 0 - for command in COMMAND.finditer(content): + for command in COMMAND.finditer(code): if command.start() < parsed_until: continue position, arguments = command.end(), [] @@ -81,21 +96,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[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.items(): - 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)): @@ -108,21 +126,68 @@ def registered(build): return names -if __name__ == "__main__": +def listed(root): + output = subprocess.run(["git", "-c", "core.quotePath=false", "-C", str(root), "ls-files", "-z", "--cached", "--others", + "--exclude-standard"], capture_output=True, check=True).stdout.decode("utf-8", errors="replace") + return sorted(set(filter(None, output.split("\0")))) + + +def run_scripts(root, files): + run = set() + for name in files: + if name.endswith("CMakeLists.txt") or name.endswith(".cmake"): + content = (Path(root) / name).read_text(encoding="utf-8", errors="replace") + for command, arguments in commands(content): + if command == "add_test": + run.update(argument.rpartition("}")[2].lstrip("/") for argument in arguments) + elif name.startswith(".github/workflows/") and name.endswith((".yml", ".yaml")): + for line in (Path(root) / name).read_text(encoding="utf-8", errors="replace").splitlines(): + if line.lstrip().startswith("#"): + continue + line = re.split(r"\s#", line)[0] + if "python" in line: + run.update(token[2:] if token.startswith("./") else token for token in SCRIPT.findall(line)) + return {name for name in run if SCRIPT.fullmatch(name)} + + +def unrun_scripts(root): + files = listed(root) + run = run_scripts(root, files) + scripts = [name for name in files if re.match(r"^test_\w+\.py$", name.rpartition("/")[2])] + return scripts, [name for name in scripts if not any(name == path or name.endswith("/" + path) for path in run)] + + +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="fail when a test executable is built that no ctest test runs") parser.add_argument("build", type=Path, nargs="?", default=Path("build"), help="configured build directory") parser.add_argument("--list", action="store_true", help="print every test executable and whether ctest runs it") - args = parser.parse_args() - every = sorted(built(args.build)) - run = registered(args.build) + parser.add_argument("--scripts", action="store_true", help="instead check that an add_test or a workflow runs every test_*.py of the source tree") + args = parser.parse_args(argv) + if args.scripts: + scripts, unrun = unrun_scripts(Path(__file__).resolve().parents[1]) + for name in unrun: + print(f"error: {name} is a test script that no add_test or workflow runs") + if unrun: + print(f"{len(unrun)} test script(s) would never run: register each with add_test, or run it from a workflow") + return 1 + print(f"{len(scripts)} test script(s), each run by an add_test or a workflow") + return 0 + try: + every = sorted(built(args.build)) + run = registered(args.build) + except (OSError, ValueError, subprocess.CalledProcessError) as error: + parser.error(str(error)) if args.list: for name in every: print(f"{'run' if name in run else 'NOT RUN'} {name}") - raise SystemExit + return 0 unrun = sorted(set(every) - run) if not every: print(f"no test executables found under {args.build}; is it configured with -DBUILD_TESTING=ON?") - raise SystemExit(1) + return 1 for name in sorted(set(unrun) & KNOWN_UNRUN): print(f"note: {name} is built and not run, as recorded") stale = sorted(KNOWN_UNRUN & run) @@ -134,5 +199,10 @@ if __name__ == "__main__": if fresh: print(f"{len(fresh)} test executable(s) would never run: ctest reports a full pass over what it knows, so a test nobody registered is invisible in CI") if stale or fresh: - raise SystemExit(1) + return 1 print(f"{len(every)} test executable(s), {len(unrun)} built and not run as recorded, none new") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) 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..8e042f9722 --- /dev/null +++ b/tools/configure_seed.py @@ -0,0 +1,61 @@ +import argparse +import fnmatch +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-sig").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(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") + parser = argparse.ArgumentParser(description="Write the configure check results of a CMake cache as an initial cache file") + parser.add_argument("cache", help="CMakeCache.txt of a configured tree") + parser.add_argument("seed", help="file to pass to cmake -C") + parser.add_argument("--include", action="append", default=[], metavar="GLOB", + help="seed matching check names only; repeat to include several patterns") + parser.add_argument("--exclude", action="append", default=[], metavar="GLOB", + help="omit matching check names after inclusion; patterns are case-sensitive") + args = parser.parse_args(argv) + cache, seed = Path(args.cache), Path(args.seed) + if cache.resolve() == seed.resolve() or (cache.exists() and seed.exists() and cache.samefile(seed)): + parser.error("seed output would overwrite the input cache") + try: + results = checks(cache) + except (OSError, UnicodeDecodeError) as error: + parser.error(f"{cache}: {error}") + found = {name: entry for name, entry in results.items() + if (not args.include or any(fnmatch.fnmatchcase(name, pattern) for pattern in args.include)) + and not any(fnmatch.fnmatchcase(name, pattern) for pattern in args.exclude)} + try: + seed.parent.mkdir(parents=True, exist_ok=True) + seed.write_text("".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" + for name, (value, doc) in sorted(found.items())), encoding="utf-8") + except OSError as error: + parser.error(f"{seed}: {error}") + 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 e7341fefe1..1460f46bca 100644 --- a/tools/debt_split.py +++ b/tools/debt_split.py @@ -17,6 +17,7 @@ def parse(text): items = [] section = None current = None + blanks = [] for line in text.splitlines(): heading = HEADING.match(line) if heading: @@ -26,13 +27,15 @@ def parse(text): elif line.startswith("- "): current = ["entry", section, [line]] items.append(current) + blanks = [] elif current and (line.startswith(" ") or line.startswith("\t")): - current[2].append(line) + current[2] += blanks + [line] + blanks = [] elif line.strip(): current = None items.append(["text", section, [line]]) else: - current = None + blanks.append(line) return [(kind, section, "\n".join(lines)) for kind, section, lines in items] @@ -43,7 +46,7 @@ def key(kind, text): def relink(text, source, destination): def replace(match): target = match.group(1) - if re.match(r"^[a-z][a-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 @@ -68,7 +71,7 @@ def read_debt(root, name): sections = {} if path.exists(): section = None - for kind, item_section, text in parse(path.read_text(encoding="utf-8")): + for kind, item_section, text in parse(path.read_text(encoding="utf-8-sig")): if kind == "heading" and item_section and text.startswith("### "): section = item_section sections.setdefault(section, []) @@ -77,6 +80,11 @@ def read_debt(root, name): return sections +def write_lf(path, text): + with open(path, "w", encoding="utf-8", newline="\n") as file: + file.write(text) + + def write_debt(root, name, sections, order): path = root / debt_file(name) sections = {section: entries for section, entries in sections.items() if entries} @@ -85,7 +93,7 @@ def write_debt(root, name, sections, order): path.unlink() return path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(render(f"# {name} technical debt", sections, order), encoding="utf-8") + write_lf(path, render(f"# {name} technical debt", sections, order)) def sections_of(text): @@ -107,7 +115,7 @@ def split(text): def render_index(index): out = [] for kind, item in index: - if kind == "heading" and out and out[-1] != "": + if kind in ("heading", "text") and out and out[-1] != "": out.append("") out.append(item) if kind == "heading" or kind == "text": @@ -121,7 +129,7 @@ def index_note(): def command_split(root): - text = (root / INDEX).read_text(encoding="utf-8") + text = (root / INDEX).read_text(encoding="utf-8-sig") index, files, order = split(text) if not files: sys.exit(f"{INDEX} has no entries to move") @@ -132,12 +140,12 @@ def command_split(root): write_debt(root, name, existing, order) if index_note() not in index: index.insert(1, index_note()) - (root / INDEX).write_text(render_index(index), encoding="utf-8") + write_lf(root / INDEX, render_index(index)) print("\n".join(sorted(debt_file(name) for name in files))) def git(root, *args, check=True): - return subprocess.run(["git", *args], cwd=root, capture_output=True, text=True, encoding="utf-8", check=check).stdout + return subprocess.run(["git", *args], cwd=root, capture_output=True, text=True, encoding="utf-8", errors="replace", check=check).stdout def apply_change(root, base, new, index_text): @@ -189,7 +197,7 @@ def apply_change(root, base, new, index_text): for offset, fresh in enumerate(added): if offset in kept_new: continue - anchor = after[j1 + offset - 1] if j1 + offset > 0 else None + anchor = next((entry for entry in reversed(after[:j1 + offset]) if entry[1] == fresh[1] and entry[3] == fresh[3]), None) insert(fresh, anchor) for name, sections in files.items(): write_debt(root, name, sections, order) @@ -215,16 +223,19 @@ def command_resolve(root): touched = apply_change(root, base, theirs, ours) except LookupError as error: sys.exit(str(error)) - (root / INDEX).write_text(ours, encoding="utf-8") + write_lf(root / INDEX, ours) git(root, "add", "--", INDEX, *touched) print("\n".join(touched)) -def main(): +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description=f"Split {INDEX} into one file per library or component, and move entries of a change made before the split") parser.add_argument("command", choices=("split", "resolve"), help=f"split: move the entries of {INDEX} into {DEBT_DIR}/; resolve: during a rebase stopped on {INDEX}, apply the stopped commit's entry changes to {DEBT_DIR}/ and stage them") - args = parser.parse_args() + args = parser.parse_args(argv) root = Path(git(Path.cwd(), "rev-parse", "--show-toplevel").strip()) command_split(root) if args.command == "split" else command_resolve(root) diff --git a/tools/env_reference.py b/tools/env_reference.py new file mode 100644 index 0000000000..683b1e9168 --- /dev/null +++ b/tools/env_reference.py @@ -0,0 +1,190 @@ +import argparse +import functools +import re +import sys +from pathlib import Path + +PREFIXES = ("APS5_", "ANYPS5_") +DOC = "docs/dev/ENVIRONMENT.md" +ROOTS = ("core",) +EXTENSIONS = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl", ".ipp", ".py"} +SKIP = {"build", "build-linux", "build-relinker", "3rdparty", ".git"} + +INTEGER_READERS = {"kibSetting", "MaskFromEnvironment", "EnvInt", "configured", "environmentCount"} +STRING_READERS = {"GetEnvironmentVariableA", "GetEnvironmentVariableW", "addWideString"} +NULL_TEST = re.compile(r"[!=]=\s*(nullptr|NULL|0)\b|!\s*(?:std::)?_?w?getenv\(") +INTEGER_TEST = re.compile(r"\b(atoi|strtol|strtoul|strtoll|strtoull|stoi|stol|stoul|from_chars|atoi64|_wtoi|_wtol|_wtoi64)\s*\(") +GREP = (r'(getenv|_wgetenv|wgetenv|IsEnvironmentSet|kibSetting|MaskFromEnvironment|EnvInt|GetEnvironmentVariable[AW]' + r'|configured|PipelineSwitch|environmentCount|addWideString)' + r'\([[:space:]]*(u|L)?"(APS5|ANYPS5)_[A-Za-z0-9_]+"') + + +def _name(prefixes): + return "(?:" + "|".join(re.escape(prefix) for prefix in prefixes) + ")[A-Za-z0-9_]+" + + +@functools.lru_cache(maxsize=None) +def forms(prefixes=PREFIXES): + name = _name(prefixes) + return ( + ("getenv", re.compile(r'(?:std::)?getenv\(\s*"(?P<name>' + name + r')"\s*\)')), + ("_wgetenv", re.compile(r'(?:std::)?_?wgetenv\(\s*L"(?P<name>' + name + r')"\s*\)')), + ("IsEnvironmentSet", re.compile(r'IsEnvironmentSet\(\s*"(?P<name>' + name + r')"\s*\)')), + ("MaskFromEnvironment", re.compile(r'MaskFromEnvironment\(\s*"(?P<name>' + name + r')"\s*\)')), + ("kibSetting", re.compile(r'kibSetting\(\s*"(?P<name>' + name + r')"\s*,\s*(?P<default>[0-9]+)')), + ("EnvInt", re.compile(r'EnvInt\(\s*"(?P<name>' + name + r')"\s*,\s*(?P<default>-?[0-9]+)')), + ("configured", re.compile(r'\b(?:configured|PipelineSwitch)\(\s*"(?P<name>' + name + r')"' + r'(?:\s*,\s*(?P<default>[0-9]+)[uUlL]*)?')), + ("environmentCount", re.compile(r'\benvironmentCount\(\s*"(?P<name>' + name + r')"\s*,\s*(?P<default>[0-9]+)')), + ("GetEnvironmentVariableA", re.compile(r'GetEnvironmentVariableA\(\s*"(?P<name>' + name + r')"')), + ("GetEnvironmentVariableW", re.compile(r'GetEnvironmentVariableW\(\s*L"(?P<name>' + name + r')"')), + ("addWideString", re.compile(r'\baddWideString\(\s*u"(?P<name>' + name + r')"\s*\)')), + ) + + +def line_reads(line, prefixes=PREFIXES): + return [(reader, match) for reader, form in forms(tuple(prefixes)) for match in form.finditer(line)] + + +def documented_pattern(prefixes): + return re.compile(r"`(" + _name(prefixes) + r")`") + + +def kind_of(reader, window): + if reader == "IsEnvironmentSet": + return "presence" + if reader in INTEGER_READERS: + return "integer" + if reader in STRING_READERS: + return "string" + if NULL_TEST.search(window): + return "presence" + if INTEGER_TEST.search(window): + return "integer" + return "string" + + +def default_of(match, window): + default = match.groupdict().get("default") + if default: + return default + found = re.search(r"\?[\s\S]*?:\s*([0-9]+)", window) + return found.group(1) if found else "" + + +def source_files(root): + root = Path(root) + if not root.is_dir(): + return + for path in sorted(root.rglob("*")): + if path.suffix not in EXTENSIONS or any(part in SKIP for part in path.relative_to(root).parts): + continue + yield path + + +def reads(roots, prefixes=PREFIXES): + found = {} + for root in roots: + for path in source_files(root): + try: + text = path.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError): + continue + lines = text.splitlines() + for number, line in enumerate(lines, 1): + if not any(prefix in line for prefix in prefixes): + continue + window = "\n".join(lines[number - 1:number + 2]) + for reader, match in line_reads(line, prefixes): + found.setdefault(match.group("name"), []).append( + (path.as_posix(), number, kind_of(reader, window), default_of(match, window))) + for sites in found.values(): + sites.sort() + return found + + +def documented(path, prefixes=PREFIXES): + try: + return set(documented_pattern(prefixes).findall(Path(path).read_text(encoding="utf-8"))) + except (OSError, UnicodeDecodeError): + return None + + +def library_of(source): + parts = Path(source).parts + if len(parts) >= 4 and parts[0] == "core" and parts[1] == "libs" and parts[2] == "prx": + return parts[3] + if len(parts) >= 2 and parts[0] == "core": + return "core/" + parts[1] + return parts[0] if parts else source + + +def list_names(found, out): + for name in sorted(found): + for source, number, kind, default in found[name]: + out.write(f"{name}\t{kind}\t{source}:{number}\t{default}\n") + + +def table(found, out): + libraries = {} + for name, sites in found.items(): + libraries.setdefault(library_of(sites[0][0]), []).append((name, sites[0])) + for library in sorted(libraries): + out.write(f"### {library}\n\n") + out.write("| Name | Kind | Source | Default | Description |\n") + out.write("|------|------|--------|---------|-------------|\n") + for name, (source, number, kind, default) in sorted(libraries[library]): + out.write(f"| `{name}` | {kind} | `{source}` | {default} | |\n") + out.write("\n") + + +def check(found, doc, prefixes, out): + names = documented(doc, prefixes) + if names is None: + out.write(f"error: cannot read {doc}\n") + return 1 + missing = sorted(set(found) - names) + stale = sorted(names - set(found)) + for name in missing: + source, number, _, _ = found[name][0] + out.write(f"error: {name} is read at {source}:{number} but has no row in {doc}\n") + for name in stale: + out.write(f"error: {name} has a row in {doc} but the code no longer reads it\n") + if missing or stale: + out.write(f"{len(missing)} name(s) undocumented, {len(stale)} stale\n") + return 1 + out.write(f"{len(found)} name(s), all documented\n") + return 0 + + +def main(argv=None): + parser = argparse.ArgumentParser( + description="list the environment names the code reads, or check them against a reference document") + parser.add_argument("--check", action="store_true", + help="compare the names the code reads with the document and fail on a mismatch") + parser.add_argument("--table", action="store_true", + help="emit the reference table rows, grouped by library") + parser.add_argument("--doc", default=DOC, help="reference document to check (default: %(default)s)") + parser.add_argument("--root", action="append", default=None, + help="directory to scan (default: %s); repeatable" % ", ".join(ROOTS)) + parser.add_argument("--prefix", action="append", default=None, + help="name prefix to scan (default: %s); repeatable" % ", ".join(PREFIXES)) + args = parser.parse_args(argv) + roots = args.root if args.root else ROOTS + prefixes = tuple(args.prefix) if args.prefix else PREFIXES + for root in roots: + if not Path(root).is_dir(): + print(f"error: scan root {root} is not a directory", file=sys.stderr) + return 1 + found = reads(roots, prefixes) + if args.check: + return check(found, args.doc, prefixes, sys.stdout) + if args.table: + table(found, sys.stdout) + return 0 + list_names(found, sys.stdout) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/hw-oracle/compare_results.py b/tools/hw-oracle/compare_results.py new file mode 100644 index 0000000000..dbc855f623 --- /dev/null +++ b/tools/hw-oracle/compare_results.py @@ -0,0 +1,113 @@ +import argparse +import json +import re +from pathlib import Path + + +SETTINGS = ("body", "wave_size", "floating_point_modes", "lds_bytes", "memory", + "extra_bytes", "extra_sha256", "output_registers") +MODES = dict(ieee=2, denorm32=4, denorm16=4, dx10_clamp=2, round32=4, round16=4, fp16_overflow=2) + + +def integer(value, minimum, maximum): + return type(value) is int and minimum <= value <= maximum + + +def read_report(path): + report = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(report, dict): + raise ValueError(f"{path}: expected a JSON object") + required = {"schema_version", "assembly_target", "rows", *SETTINGS} + missing = required - report.keys() + if missing: + raise ValueError(f"{path}: missing fields: {', '.join(sorted(missing))}") + if not integer(report["schema_version"], 1, 1): + raise ValueError(f"{path}: unsupported schema_version") + modes = report["floating_point_modes"] + if not isinstance(modes, dict) or modes.keys() != MODES.keys() or any( + not integer(modes[name], 0, limit - 1) for name, limit in MODES.items()): + raise ValueError(f"{path}: invalid floating_point_modes") + if not isinstance(report["body"], str): + raise ValueError(f"{path}: body must be text") + if not isinstance(report["assembly_target"], str) or not report["assembly_target"].strip(): + raise ValueError(f"{path}: assembly_target must be nonempty text") + if type(report["wave_size"]) is not int or report["wave_size"] not in (32, 64): + raise ValueError(f"{path}: wave_size must be 32 or 64") + if not integer(report["lds_bytes"], 0, 65536): + raise ValueError(f"{path}: invalid lds_bytes") + if report["memory"] not in ("fine", "coarse"): + raise ValueError(f"{path}: memory must be fine or coarse") + if type(report["extra_bytes"]) is not int or report["extra_bytes"] < 0: + raise ValueError(f"{path}: invalid extra_bytes") + digest = report["extra_sha256"] + if not isinstance(digest, str) or re.fullmatch(r"[0-9a-f]{64}", digest) is None: + raise ValueError(f"{path}: invalid extra_sha256") + registers = report["output_registers"] + if not isinstance(registers, list) or not 1 <= len(registers) <= 16 or registers != [ + f"v{10 + index}" for index in range(len(registers))]: + raise ValueError(f"{path}: output_registers must be consecutive registers starting at v10, up to v25") + rows = report["rows"] + if not isinstance(rows, list) or not rows: + raise ValueError(f"{path}: rows must be a nonempty array") + for index, row in enumerate(rows, 1): + for name, count in (("input", 4), ("output", len(registers))): + values = row.get(name) if isinstance(row, dict) else None + if not isinstance(values, list) or len(values) != count or any( + not integer(value, 0, 0xffffffff) for value in values): + raise ValueError(f"{path}: row {index} {name} needs {count} unsigned 32-bit integers") + return report + + +def compare(before, after): + for name in SETTINGS: + if before[name] != after[name]: + raise ValueError(f"incompatible reports: {name} differs") + if len(before["rows"]) != len(after["rows"]): + raise ValueError("incompatible reports: row count differs") + for index, (left, right) in enumerate(zip(before["rows"], after["rows"]), 1): + if left["input"] != right["input"]: + raise ValueError(f"incompatible reports: row {index} input differs") + differences = [] + for index, (left, right) in enumerate(zip(before["rows"], after["rows"]), 1): + for register, old, new in zip(before["output_registers"], left["output"], right["output"]): + if old != new: + differences.append((index, register, old, new)) + return differences + + +def nonnegative_integer(value): + number = int(value) + if number < 0: + raise argparse.ArgumentTypeError("must be a nonnegative integer") + return number + + +def main(): + parser = argparse.ArgumentParser( + description="Compare two hw_oracle.py --json reports bit for bit without a GPU. " + "Inputs and settings must match; assembly targets may differ.", + epilog="Exit codes: 0 = identical outputs, 1 = different outputs, 2 = invalid or incompatible reports. " + "Assembly targets do not identify the physical GPU or driver.") + parser.add_argument("before", type=Path) + parser.add_argument("after", type=Path) + parser.add_argument("--max-differences", type=nonnegative_integer, metavar="N", + help="print at most N differing words (0 prints only counts); all words are still compared") + args = parser.parse_args() + try: + before, after = read_report(args.before), read_report(args.after) + differences = compare(before, after) + except (OSError, UnicodeError, ValueError) as error: + parser.error(str(error)) + print(f"Assembly targets: {before['assembly_target']} -> {after['assembly_target']}") + print(f"Compared {len(before['rows'])} rows x {len(before['output_registers'])} registers: " + f"{len(differences)} differing words") + visible = differences if args.max_differences is None else differences[:args.max_differences] + for row, register, old, new in visible: + print(f"row {row}, {register}: 0x{old:08x} -> 0x{new:08x}") + if len(visible) < len(differences): + print(f"{len(differences) - len(visible)} additional differing words omitted") + return int(bool(differences)) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py index df15c544a7..f1dc2da856 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -1,5 +1,7 @@ import argparse import hashlib +import importlib.util +import json import os import operator import shlex @@ -16,6 +18,22 @@ CACHE = Path(os.environ.get("HW_ORACLE_CACHE") or Path(os.environ.get("XDG_CACHE ROWS_PER_DISPATCH = 1024 DEFAULT_LDS = 4096 MAX_LDS = 65536 +RUNTIMES = ("hsa", "hip") + + +def runtime(): + selected = os.environ.get("HW_ORACLE_RUNTIME") or ("hip" if os.name == "nt" else "hsa") + if selected not in RUNTIMES: + raise ValueError(f"HW_ORACLE_RUNTIME must be one of {', '.join(RUNTIMES)}") + return selected + + +@cache +def hip_runtime(): + spec = importlib.util.spec_from_file_location("oracle_hip", HERE / "oracle_hip.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module def rocm_root(): @@ -43,7 +61,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" @@ -56,7 +76,7 @@ def oracle(): if root: lib = root / "lib" command += [f"-I{root / 'include'}", f"-L{lib}", f"-Wl,-rpath,{lib}"] - subprocess.run(command + ["-lhsa-runtime64"], check=True) + subprocess.run(command + ["-lhsa-runtime64"], check=True, stdout=sys.stderr) output.replace(binary) return binary @@ -64,12 +84,22 @@ 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()) + if os.environ.get("HW_ORACLE_TARGET"): + return os.environ["HW_ORACLE_TARGET"] + if runtime() == "hip": + return hip_runtime().target() + return 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): @@ -88,12 +118,15 @@ def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round3 ("@FP16_OVERFLOW@", str(int(fp16_overflow))), ("@LDS@", str(lds)), ("@BODY@", body)): text = text.replace(key, value) (work / "k.s").write_text(text) - subprocess.run([tool("clang"), "-x", "assembler", "-target", "amdgcn-amd-amdhsa", f"-mcpu={target()}", *(["-mwavefrontsize64"] if wave64 else []), "-c", str(work / "k.s"), "-o", str(work / "k.o")], check=True) - subprocess.run([tool("ld.lld"), "-m", "elf64_amdgpu", "-shared", str(work / "k.o"), "-o", str(work / "k.co")], check=True) + if runtime() == "hip": + (work / "k.co").write_bytes(hip_runtime().assemble(text.encode("utf-8"), target(), wave64)) + return work / "k.co" + subprocess.run([tool("clang"), "-x", "assembler", "-target", "amdgcn-amd-amdhsa", f"-mcpu={target()}", *(["-mwavefrontsize64"] if wave64 else []), "-c", str(work / "k.s"), "-o", str(work / "k.o")], check=True, stdout=sys.stderr) + subprocess.run([tool("ld.lld"), "-m", "elf64_amdgpu", "-shared", str(work / "k.o"), "-o", str(work / "k.co")], check=True, stdout=sys.stderr) 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: @@ -103,7 +136,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) @@ -118,9 +155,17 @@ def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, de rows_per_dispatch = ROWS_PER_DISPATCH // 2 if wave64 else ROWS_PER_DISPATCH 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) - subprocess.run([oracle(), str(code), str(len(chunk)), str(work / "in.bin"), str(work / "out.bin"), "16"], check=True, env=env) - data = (work / "out.bin").read_bytes() + payload = b"".join(struct.pack("<4I", *r) for r in chunk) + extra + if runtime() == "hip": + data = hip_runtime().dispatch(Path(code).read_bytes(), len(chunk), payload, 16, coarse) + else: + (work / "in.bin").write_bytes(payload) + (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)] @@ -148,16 +193,53 @@ def main(): parser.add_argument("--lds", type=int, default=DEFAULT_LDS, metavar="BYTES", help=f"group segment size, 0 to {MAX_LDS} (default {DEFAULT_LDS})") parser.add_argument("--extra", type=Path, help="bytes appended after the rows") parser.add_argument("--outs", type=int, choices=range(1, 17), default=16, metavar="N", help="print v10..v(10+N-1)") + parser.add_argument("--json", action="store_true", help="emit inputs, results and run settings as JSON; target is the assembly target, not a GPU model or driver") + parser.add_argument("--validate-only", action="store_true", + help="check input rows and report dispatch dimensions without assembling or executing the kernel") args = parser.parse_args() - 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"" - for result in run(args.body.read_text(), rows, extra, args.wave64, args.coarse, - ieee=args.ieee, denorm32=args.denorm32, denorm16=args.denorm16, - dx10_clamp=args.dx10_clamp, round32=args.round32, round16=args.round16, - fp16_overflow=args.fp16_overflow, lds=args.lds): - print(" ".join(f"{v:08x}" for v in result[:args.outs])) + if args.lds not in range(MAX_LDS + 1): + parser.error(f"lds must be an integer from 0 to {MAX_LDS}") + try: + rows = validated_rows(tuple(parse_word(v) for v in line.split()) + for line in args.rows.read_text(encoding="utf-8").splitlines() if line.strip()) + 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")} + if args.validate_only: + wave = 64 if args.wave64 else 32 + padded = (len(rows) + wave - 1) // wave * wave + dispatch = ROWS_PER_DISPATCH // 2 if args.wave64 else ROWS_PER_DISPATCH + report = {"validation_only": True, "rows": len(rows), "padded_rows": padded, + "dispatches": (padded + dispatch - 1) // dispatch, "wave_size": wave, + "floating_point_modes": modes, "lds_bytes": args.lds, + "memory": "coarse" if args.coarse else "fine", "extra_bytes": len(extra)} + print(json.dumps(report, indent=2) if args.json else + f"Validated {len(rows)} input rows, {padded} padded rows, {report['dispatches']} dispatches. " + "Assembly and GPU execution were not checked.") + return 0 + results = run(body, rows, extra, args.wave64, args.coarse, **modes, lds=args.lds) + if args.json: + report = { + "schema_version": 1, + "assembly_target": target(), + "wave_size": 64 if args.wave64 else 32, + "floating_point_modes": modes, + "lds_bytes": args.lds, + "memory": "coarse" if args.coarse else "fine", + "body": body, + "extra_bytes": len(extra), + "extra_sha256": hashlib.sha256(extra).hexdigest(), + "output_registers": [f"v{10 + index}" for index in range(args.outs)], + "rows": [{"input": row, "output": result[:args.outs]} for row, result in zip(rows, results)], + } + print(json.dumps(report, indent=2)) + else: + for result in results: + print(" ".join(f"{v:08x}" for v in result[:args.outs])) if __name__ == "__main__": diff --git a/tools/hw-oracle/oracle_hip.py b/tools/hw-oracle/oracle_hip.py new file mode 100644 index 0000000000..596b82a8f7 --- /dev/null +++ b/tools/hw-oracle/oracle_hip.py @@ -0,0 +1,235 @@ +import argparse +import ctypes +import os +import sys +from functools import cache +from pathlib import Path + +HIP_LIBRARIES = ("amdhip64_7.dll", "amdhip64_6.dll", "amdhip64.dll") +COMGR_LIBRARIES = ("amd_comgr_3.dll", "amd_comgr_2.dll", "amd_comgr.dll") +DATA_SOURCE = 0x1 +DATA_LOG = 0x5 +DATA_RELOCATABLE = 0x7 +DATA_EXECUTABLE = 0x8 +ASSEMBLE_ACTIONS = (0x8, 0xA) +LINK_ACTIONS = (0x7, 0x9) +HIP_MEMCPY_HOST_TO_DEVICE = 1 +HIP_MEMCPY_DEVICE_TO_HOST = 2 +DEVICE_PROPERTIES_SIZE = 8192 +DEVICE_NAME_SIZE = 256 + + +class OracleError(Exception): + pass + + +class Handle(ctypes.Structure): + _fields_ = [("handle", ctypes.c_uint64)] + + +def check(status, what): + if status != 0: + raise OracleError(f"{what} failed with status {status}") + + +def load(variable, names): + candidates = (os.environ[variable],) if os.environ.get(variable) else names + errors = [] + for name in candidates: + try: + return ctypes.WinDLL(name) + except OSError as error: + errors.append(f"{name}: {error}") + raise OracleError(f"no library found ({variable} overrides it): " + "; ".join(errors)) + + +@cache +def hip(): + if os.environ.get("ROCR_VISIBLE_DEVICES") is not None and "HIP_VISIBLE_DEVICES" not in os.environ: + os.environ["HIP_VISIBLE_DEVICES"] = os.environ["ROCR_VISIBLE_DEVICES"] + library = load("HW_ORACLE_HIP", HIP_LIBRARIES) + library.hipModuleLoadData.argtypes = [ctypes.POINTER(ctypes.c_void_p), ctypes.c_char_p] + library.hipModuleGetFunction.argtypes = [ctypes.POINTER(ctypes.c_void_p), ctypes.c_void_p, ctypes.c_char_p] + library.hipMemcpy.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int] + library.hipMemset.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_size_t] + library.hipModuleLaunchKernel.argtypes = [ctypes.c_void_p, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, + ctypes.c_uint, ctypes.c_uint, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + library.hipModuleUnload.argtypes = [ctypes.c_void_p] + library.hipFree.argtypes = [ctypes.c_void_p] + library.hipHostFree.argtypes = [ctypes.c_void_p] + check(library.hipInit(0), "hipInit") + count = ctypes.c_int() + check(library.hipGetDeviceCount(ctypes.byref(count)), "hipGetDeviceCount") + if count.value == 0: + raise OracleError("no HIP device") + return library + + +@cache +def comgr(): + library = load("HW_ORACLE_COMGR", COMGR_LIBRARIES) + library.amd_comgr_create_data.argtypes = [ctypes.c_int, ctypes.POINTER(Handle)] + library.amd_comgr_create_data_set.argtypes = [ctypes.POINTER(Handle)] + library.amd_comgr_create_action_info.argtypes = [ctypes.POINTER(Handle)] + library.amd_comgr_set_data.argtypes = [Handle, ctypes.c_size_t, ctypes.c_char_p] + library.amd_comgr_set_data_name.argtypes = [Handle, ctypes.c_char_p] + library.amd_comgr_data_set_add.argtypes = [Handle, Handle] + library.amd_comgr_action_info_set_isa_name.argtypes = [Handle, ctypes.c_char_p] + library.amd_comgr_action_info_set_option_list.argtypes = [Handle, ctypes.POINTER(ctypes.c_char_p), ctypes.c_size_t] + library.amd_comgr_action_info_set_logging.argtypes = [Handle, ctypes.c_bool] + library.amd_comgr_do_action.argtypes = [ctypes.c_int, Handle, Handle, Handle] + library.amd_comgr_action_data_count.argtypes = [Handle, ctypes.c_int, ctypes.POINTER(ctypes.c_size_t)] + library.amd_comgr_action_data_get_data.argtypes = [Handle, ctypes.c_int, ctypes.c_size_t, ctypes.POINTER(Handle)] + library.amd_comgr_get_data.argtypes = [Handle, ctypes.POINTER(ctypes.c_size_t), ctypes.c_char_p] + library.amd_comgr_release_data.argtypes = [Handle] + library.amd_comgr_destroy_data_set.argtypes = [Handle] + library.amd_comgr_destroy_action_info.argtypes = [Handle] + return library + + +@cache +def target(): + properties = ctypes.create_string_buffer(DEVICE_PROPERTIES_SIZE) + library = hip() + getter = getattr(library, "hipGetDevicePropertiesR0600", None) or library.hipGetDeviceProperties + check(getter(properties, 0), "hipGetDeviceProperties") + start = properties.raw.find(b"gfx", DEVICE_NAME_SIZE) + if start < 0: + raise OracleError("the HIP device properties name no gfx target") + return properties.raw[start:properties.raw.index(b"\0", start)].decode("ascii").split(":")[0] + + +def data_bytes(data_set, kind): + library = comgr() + count = ctypes.c_size_t() + check(library.amd_comgr_action_data_count(data_set, kind, ctypes.byref(count)), "amd_comgr_action_data_count") + chunks = [] + for index in range(count.value): + data = Handle() + check(library.amd_comgr_action_data_get_data(data_set, kind, index, ctypes.byref(data)), "amd_comgr_action_data_get_data") + size = ctypes.c_size_t() + check(library.amd_comgr_get_data(data, ctypes.byref(size), None), "amd_comgr_get_data") + buffer = ctypes.create_string_buffer(size.value) + check(library.amd_comgr_get_data(data, ctypes.byref(size), buffer), "amd_comgr_get_data") + chunks.append(buffer.raw[:size.value]) + library.amd_comgr_release_data(data) + return chunks + + +def data_set(kind, name, payload): + library = comgr() + data = Handle() + check(library.amd_comgr_create_data(kind, ctypes.byref(data)), "amd_comgr_create_data") + check(library.amd_comgr_set_data(data, len(payload), payload), "amd_comgr_set_data") + check(library.amd_comgr_set_data_name(data, name), "amd_comgr_set_data_name") + result = Handle() + check(library.amd_comgr_create_data_set(ctypes.byref(result)), "amd_comgr_create_data_set") + check(library.amd_comgr_data_set_add(result, data), "amd_comgr_data_set_add") + library.amd_comgr_release_data(data) + return result + + +def comgr_step(actions, isa, options, inputs, kind): + library = comgr() + logs = [] + for action in actions: + info = Handle() + check(library.amd_comgr_create_action_info(ctypes.byref(info)), "amd_comgr_create_action_info") + outputs = Handle() + try: + check(library.amd_comgr_action_info_set_isa_name(info, isa.encode("ascii")), "amd_comgr_action_info_set_isa_name") + check(library.amd_comgr_action_info_set_logging(info, True), "amd_comgr_action_info_set_logging") + if options: + check(library.amd_comgr_action_info_set_option_list(info, (ctypes.c_char_p * len(options))(*options), len(options)), + "amd_comgr_action_info_set_option_list") + check(library.amd_comgr_create_data_set(ctypes.byref(outputs)), "amd_comgr_create_data_set") + status = library.amd_comgr_do_action(action, info, inputs, outputs) + result = data_bytes(outputs, kind) + log = b"".join(data_bytes(outputs, DATA_LOG)).decode("utf-8", errors="replace").strip() + library.amd_comgr_destroy_data_set(outputs) + finally: + library.amd_comgr_destroy_action_info(info) + if status == 0 and len(result) == 1: + return result[0] + logs.append(f"action {action:#x} status {status}" + (f": {log}" if log else "")) + raise OracleError("comgr produced no output\n" + "\n".join(logs)) + + +def assemble(source, isa_target, wave64): + isa = f"amdgcn-amd-amdhsa--{isa_target}" + inputs = data_set(DATA_SOURCE, b"k.s", source) + try: + relocatable = comgr_step(ASSEMBLE_ACTIONS, isa, [b"-mwavefrontsize64"] if wave64 else [], inputs, DATA_RELOCATABLE) + finally: + comgr().amd_comgr_destroy_data_set(inputs) + objects = data_set(DATA_RELOCATABLE, b"k.o", relocatable) + try: + return comgr_step(LINK_ACTIONS, isa, [], objects, DATA_EXECUTABLE) + finally: + comgr().amd_comgr_destroy_data_set(objects) + + +def dispatch(code, threads, payload, out_dwords, coarse): + library = hip() + module = ctypes.c_void_p() + check(library.hipModuleLoadData(ctypes.byref(module), code), "hipModuleLoadData") + try: + function = ctypes.c_void_p() + check(library.hipModuleGetFunction(ctypes.byref(function), module, b"probe"), "hipModuleGetFunction") + out_size = threads * out_dwords * 4 + buffers = [] + try: + for size in (len(payload), out_size): + pointer = ctypes.c_void_p() + if coarse: + check(library.hipMalloc(ctypes.byref(pointer), ctypes.c_size_t(size)), "hipMalloc") + else: + check(library.hipHostMalloc(ctypes.byref(pointer), ctypes.c_size_t(size), 0), "hipHostMalloc") + buffers.append(pointer) + device_in, device_out = buffers + check(library.hipMemcpy(device_in, payload, len(payload), HIP_MEMCPY_HOST_TO_DEVICE), "hipMemcpy") + check(library.hipMemset(device_out, 0xee, out_size), "hipMemset") + arguments = (ctypes.c_void_p * 2)(ctypes.addressof(device_in), ctypes.addressof(device_out)) + check(library.hipModuleLaunchKernel(function, 1, 1, 1, min(threads, 1024), 1, 1, 0, None, arguments, None), "hipModuleLaunchKernel") + check(library.hipDeviceSynchronize(), "hipDeviceSynchronize") + output = ctypes.create_string_buffer(out_size) + check(library.hipMemcpy(output, device_out, out_size, HIP_MEMCPY_DEVICE_TO_HOST), "hipMemcpy") + return output.raw + finally: + for pointer in buffers: + (library.hipFree if coarse else library.hipHostFree)(pointer) + finally: + library.hipModuleUnload(module) + + +def main(argv=None): + parser = argparse.ArgumentParser(description="Assemble and run hw-oracle kernels through the HIP runtime and comgr of AMD's Windows driver.") + parser.add_argument("--target", action="store_true", help="print the gfx target of the HIP device") + parser.add_argument("--assemble", nargs=3, metavar=("SOURCE", "OUTPUT", "TARGET"), help="assemble and link SOURCE for TARGET into OUTPUT") + parser.add_argument("--wave64", action="store_true", help="assemble for wave64") + parser.add_argument("dispatch", nargs="*", metavar="CODE THREADS IN OUT OUT_DWORDS", help="run CODE as oracle does") + args = parser.parse_args(argv) + try: + if args.target and not args.assemble and not args.dispatch: + print(target()) + return 0 + if args.assemble and not args.target and not args.dispatch: + source, output, isa_target = args.assemble + Path(output).write_bytes(assemble(Path(source).read_bytes(), isa_target, args.wave64)) + return 0 + if len(args.dispatch) != 5 or args.target or args.assemble or args.wave64: + parser.error("give --target, --assemble SOURCE OUTPUT TARGET, or CODE THREADS IN OUT OUT_DWORDS") + code, threads, inputs, output, out_dwords = args.dispatch + threads, out_dwords = int(threads), int(out_dwords) + if threads not in range(1, 1025) or out_dwords not in range(1, 17): + parser.error("THREADS must be 1 to 1024 and OUT_DWORDS 1 to 16") + result = dispatch(Path(code).read_bytes(), threads, Path(inputs).read_bytes(), out_dwords, os.environ.get("COARSE") is not None) + Path(output).write_bytes(result) + return 0 + except (OSError, OracleError) as error: + print(error, file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/hw-oracle/test_compare_results.py b/tools/hw-oracle/test_compare_results.py new file mode 100644 index 0000000000..453ca40f9f --- /dev/null +++ b/tools/hw-oracle/test_compare_results.py @@ -0,0 +1,215 @@ +import copy +import contextlib +import hashlib +import io +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +import compare_results +import hw_oracle + + +SCRIPT = Path(__file__).with_name("compare_results.py") +MODES = dict(ieee=0, denorm32=0, denorm16=3, dx10_clamp=1, round32=0, round16=0, fp16_overflow=0) + + +class CompareResultsTests(unittest.TestCase): + def setUp(self): + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + self.root = Path(directory.name) + self.before = self.root / "before.json" + self.after = self.root / "after.json" + self.report = { + "schema_version": 1, + "assembly_target": "gfx1030", + "wave_size": 32, + "floating_point_modes": MODES.copy(), + "lds_bytes": 4096, + "memory": "fine", + "body": "v_mov_b32 v10, v4\n", + "extra_bytes": 0, + "extra_sha256": hashlib.sha256(b"").hexdigest(), + "output_registers": ["v10", "v11"], + "rows": [ + {"input": [0, 0x80000000, 0xffffffff, 0], "output": [0, 0x80000000]}, + {"input": [0x7fc00001, 0x7fc00002, 0, 0], "output": [0x7fc00001, 0xffffffff]}, + ], + } + + def cli(self, before=None, after=None, process=False): + self.before.write_text(json.dumps(self.report if before is None else before), encoding="utf-8") + self.after.write_text(json.dumps(self.report if after is None else after), encoding="utf-8") + if process: + return subprocess.run([sys.executable, str(SCRIPT), str(self.before), str(self.after)], + capture_output=True, text=True, timeout=10) + return self.compare() + + def compare(self): + argv = [str(SCRIPT), str(self.before), str(self.after)] + stdout, stderr = io.StringIO(), io.StringIO() + with patch("sys.argv", argv), contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): + try: + returncode = compare_results.main() + except SystemExit as exit: + returncode = exit.code + return subprocess.CompletedProcess(argv, returncode, stdout.getvalue(), stderr.getvalue()) + + def test_identical_outputs_across_assembly_targets(self): + after = copy.deepcopy(self.report) + after["assembly_target"] = "gfx1100" + result = self.cli(after=after, process=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("gfx1030 -> gfx1100", result.stdout) + self.assertIn("2 rows x 2 registers: 0 differing words", result.stdout) + self.assertEqual(result.stderr, "") + + def test_bit_differences_include_signed_zero_nan_payload_and_unsigned_words(self): + after = copy.deepcopy(self.report) + after["rows"][0]["output"][0] = 0x80000000 + after["rows"][1]["output"] = [0x7fc00002, 0] + result = self.cli(after=after, process=True) + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn("3 differing words", result.stdout) + self.assertIn("row 1, v10: 0x00000000 -> 0x80000000", result.stdout) + self.assertIn("row 2, v10: 0x7fc00001 -> 0x7fc00002", result.stdout) + self.assertIn("row 2, v11: 0xffffffff -> 0x00000000", result.stdout) + self.assertEqual(result.stderr, "") + + def test_incompatible_settings_fail_before_comparing_outputs(self): + changes = dict(body="v_mov_b32 v10, v5\n", wave_size=64, lds_bytes=8192, + memory="coarse", extra_bytes=1, extra_sha256=hashlib.sha256(b"x").hexdigest(), + output_registers=["v10"]) + for field, value in changes.items(): + with self.subTest(field=field): + after = copy.deepcopy(self.report) + after[field] = value + if field == "output_registers": + for row in after["rows"]: + row["output"] = row["output"][:1] + result = self.cli(after=after) + self.assertEqual(result.returncode, 2) + self.assertIn(f"{field} differs", result.stderr) + self.assertEqual(result.stdout, "") + for mode in MODES: + with self.subTest(mode=mode): + after = copy.deepcopy(self.report) + after["floating_point_modes"][mode] ^= 1 + result = self.cli(after=after) + self.assertEqual(result.returncode, 2) + self.assertIn("floating_point_modes differs", result.stderr) + self.assertEqual(result.stdout, "") + + def test_input_order_and_count_must_match(self): + for rows, error in ((list(reversed(self.report["rows"])), "row 1 input"), + (self.report["rows"][:1], "row count")): + with self.subTest(error=error): + after = copy.deepcopy(self.report) + after["rows"] = rows + result = self.cli(after=after) + self.assertEqual(result.returncode, 2) + self.assertIn(error, result.stderr) + self.assertEqual(result.stdout, "") + + def test_invalid_reports_do_not_report_a_match(self): + changes = [("schema_version", 2), ("schema_version", True), ("rows", []), + ("rows", [None]), ("rows", {}), ("body", 123), ("assembly_target", 123), + ("wave_size", 16), ("wave_size", 32.0), ("lds_bytes", -1), ("lds_bytes", 65537), + ("memory", "other"), ("extra_bytes", True), ("extra_bytes", -1), + ("extra_sha256", "x" * 64), ("output_registers", ["v11", "v10"]), + ("output_registers", []), ("floating_point_modes", {})] + for field, value in changes: + with self.subTest(field=field, value=value): + invalid = copy.deepcopy(self.report) + invalid[field] = value + result = self.cli(after=invalid) + self.assertEqual(result.returncode, 2) + self.assertIn("row" if field == "rows" else field, result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(result.stdout, "") + invalid = copy.deepcopy(self.report) + del invalid["wave_size"] + result = self.cli(before=invalid) + self.assertEqual(result.returncode, 2) + self.assertIn("missing fields: wave_size", result.stderr) + self.assertEqual(result.stdout, "") + + def test_invalid_words_and_widths_fail_on_either_side(self): + for field in ("input", "output"): + for values in ([], [0], [0] * 17, [True, 0, 0, 0], [-1, 0, 0, 0], + [0x100000000, 0, 0, 0], [1.5, 0, 0, 0], ["1", 0, 0, 0]): + if len(values) == 4 and field == "output": + values = values[:2] + for side in ("before", "after"): + with self.subTest(field=field, values=values, side=side): + invalid = copy.deepcopy(self.report) + invalid["rows"][0][field] = values + result = self.cli(**{side: invalid}) + self.assertEqual(result.returncode, 2) + self.assertIn(f"row 1 {field}", result.stderr) + self.assertEqual(result.stdout, "") + + def test_bad_json_missing_files_and_encoding_fail_cleanly(self): + self.before.write_text(json.dumps(self.report), encoding="utf-8") + for content in (None, b"{", b"[]", b"\xff"): + with self.subTest(content=content): + if content is not None: + self.after.write_bytes(content) + result = self.compare() + self.assertEqual(result.returncode, 2) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(result.stdout, "") + + def test_reports_from_oracle_writer_and_rows_past_dispatch_boundaries(self): + body = self.root / "body.s" + rows = self.root / "rows.txt" + body.write_text(self.report["body"], encoding="utf-8") + rows.write_text("1 2 3 4\n" * 1025, encoding="utf-8") + for wave64 in (False, True): + for outs in (1, 16): + with self.subTest(wave64=wave64, outs=outs): + argv = ["hw_oracle.py", str(body), str(rows), "--json", "--outs", str(outs)] + argv += [part for key, value in MODES.items() for part in ["--" + key.replace("_", "-"), str(value)]] + if wave64: + argv.append("--wave64") + output = io.StringIO() + with patch("sys.argv", argv), patch.object(hw_oracle, "target", return_value="gfx1030"), \ + patch.object(hw_oracle, "run", return_value=[tuple(range(16))] * 1025), \ + contextlib.redirect_stdout(output): + hw_oracle.main() + before = json.loads(output.getvalue()) + after = copy.deepcopy(before) + after["rows"][-1]["output"][-1] = 0xffffffff + result = self.cli(before=before, after=after) + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn("1 differing words", result.stdout) + self.assertIn(f"row 1025, v{9 + outs}", result.stdout) + + def test_diagnostic_limit_does_not_limit_comparison_or_hide_failure(self): + after = copy.deepcopy(self.report) + after["rows"][0]["output"][0] = 1 + after["rows"][1]["output"] = [2, 3] + self.cli(after=after) + for limit, shown in ((0, 0), (1, 1), (3, 3), (10, 3)): + with self.subTest(limit=limit): + result = subprocess.run([sys.executable, str(SCRIPT), str(self.before), str(self.after), + "--max-differences", str(limit)], capture_output=True, text=True) + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn("3 differing words", result.stdout) + self.assertEqual(sum(line.startswith("row ") for line in result.stdout.splitlines()), shown) + if shown < 3: + self.assertIn(f"{3 - shown} additional differing words omitted", result.stdout) + result = subprocess.run([sys.executable, str(SCRIPT), str(self.before), str(self.after), + "--max-differences", "-1"], capture_output=True, text=True) + self.assertEqual(result.returncode, 2) + self.assertEqual(result.stdout, "") + + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index bdf6940a72..e234145299 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -1,5 +1,7 @@ import contextlib +import hashlib import io +import json import os import shlex import shutil @@ -8,6 +10,7 @@ import sys import tempfile import unittest from pathlib import Path +import unittest.mock from unittest.mock import patch import hw_oracle @@ -16,6 +19,10 @@ import hw_oracle MODES = dict(ieee=0, denorm32=0, denorm16=3, dx10_clamp=1, round32=0, round16=0, fp16_overflow=0) +def setUpModule(): + os.environ["HW_ORACLE_RUNTIME"] = "hsa" + + class OracleCacheTests(unittest.TestCase): def test_failed_build_is_not_cached_and_can_be_retried(self): for previous in (None, b"old executable"): @@ -27,12 +34,12 @@ class OracleCacheTests(unittest.TestCase): binary.write_bytes(previous) os.utime(binary, (0, 0)) - def fail(command, *, check): + def fail(command, *, check, stdout): self.assertTrue(check) Path(command[command.index("-o") + 1]).write_bytes(b"partial executable") raise subprocess.CalledProcessError(1, command) - def succeed(command, *, check): + def succeed(command, *, check, stdout): self.assertTrue(check) Path(command[command.index("-o") + 1]).write_bytes(b"complete executable") @@ -56,7 +63,7 @@ class OracleCacheTests(unittest.TestCase): cache = Path(tmp) / "cache" outputs = [] - def compile(command, *, check): + def compile(command, *, check, stdout): self.assertTrue(check) output = Path(command[command.index("-o") + 1]) outputs.append(output) @@ -77,6 +84,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)): @@ -85,6 +145,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): @@ -114,6 +196,136 @@ class InputRowsTests(unittest.TestCase): hw_oracle.main() self.assertEqual(run.call_args.args[1], [(1, 8, 0xffffffff, 2)]) + def test_validation_only_reports_dispatch_shape_without_running_tools(self): + with tempfile.TemporaryDirectory() as directory: + body, rows, extra = (Path(directory) / name for name in ("body.s", "rows.txt", "extra.bin")) + body.write_text("v_mov_b32 v10, v4", encoding="utf-8") + extra.write_bytes(b"abc") + options = [part for name, value in MODES.items() for part in ["--" + name.replace("_", "-"), str(value)]] + for count in (1, 33, 513, 1025): + rows.write_text("1 2 3 4\n" * count, encoding="utf-8") + for wave64 in (False, True): + argv = ["hw_oracle.py", str(body), str(rows), *options, "--validate-only", "--json", "--extra", str(extra)] + if wave64: + argv.append("--wave64") + with self.subTest(count=count, wave64=wave64), patch("sys.argv", argv), \ + patch.object(hw_oracle, "run", side_effect=AssertionError("GPU run")), \ + patch.object(hw_oracle, "target", side_effect=AssertionError("target lookup")), \ + contextlib.redirect_stdout(io.StringIO()) as output: + self.assertEqual(hw_oracle.main(), 0) + report = json.loads(output.getvalue()) + wave = 64 if wave64 else 32 + padded = (count + wave - 1) // wave * wave + dispatch = 512 if wave64 else 1024 + self.assertTrue(report["validation_only"]) + self.assertEqual((report["rows"], report["padded_rows"], report["dispatches"]), + (count, padded, (padded + dispatch - 1) // dispatch)) + self.assertEqual(report["extra_bytes"], 3) + self.assertEqual(report["floating_point_modes"], MODES) + self.assertNotIn("assembly_target", report) + for lds in (-1, 65537): + with self.subTest(lds=lds), patch("sys.argv", [*argv, "--lds", str(lds)]), \ + contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as exited: + hw_oracle.main() + self.assertEqual(exited.exception.code, 2) + + + +class JsonReportTests(unittest.TestCase): + def setUp(self): + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + self.root = Path(directory.name) + self.body = self.root / "body.s" + self.body.write_text("v_mov_b32 v10, v4\n", encoding="utf-8") + self.rows = self.root / "rows.txt" + self.rows.write_text("0xffffffff 0x80000000 08 0\n1 2 3 4\n", encoding="utf-8") + self.extra = self.root / "extra.bin" + self.extra.write_bytes(b"\x00\xff\x01") + self.runner = self.root / "runner.py" + self.runner.write_text( + "import sys\nfrom pathlib import Path\n" + "count = int(sys.argv[1])\n" + "data = Path(sys.argv[2]).read_bytes()\n" + "Path(sys.argv[3]).write_bytes(b''.join(data[i*16:i*16+16] * 4 for i in range(count)))\n" + "print('fixture tool diagnostic')\n", encoding="utf-8") + + def cli(self, *options, output=None): + argv = ["hw_oracle.py", str(self.body), str(self.rows), *options] + argv += [item for name, value in MODES.items() for item in ["--" + name.replace("_", "-"), str(value)]] + if output is None: + output = io.StringIO() + with patch("sys.argv", argv), patch.object(hw_oracle, "assemble", return_value=self.runner), \ + patch.object(hw_oracle, "oracle", return_value=sys.executable), \ + patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1030"}), \ + contextlib.redirect_stdout(output), tempfile.TemporaryFile(mode="w+", encoding="utf-8") as errors: + with contextlib.redirect_stderr(errors): + hw_oracle.main() + errors.seek(0) + return output.getvalue(), errors.read() + + def test_json_preserves_unsigned_words_and_explicit_run_settings(self): + output, errors = self.cli("--json", "--wave64", "--coarse", "--lds", "8192", "--outs", "3", "--extra", str(self.extra)) + report = json.loads(output) + self.assertEqual(report["schema_version"], 1) + self.assertEqual(report["assembly_target"], "gfx1030") + self.assertEqual(report["wave_size"], 64) + self.assertEqual(report["floating_point_modes"], MODES) + self.assertEqual(report["lds_bytes"], 8192) + self.assertEqual(report["memory"], "coarse") + self.assertEqual(report["output_registers"], ["v10", "v11", "v12"]) + self.assertEqual(report["body"], "v_mov_b32 v10, v4\n") + self.assertEqual(report["extra_bytes"], 3) + self.assertEqual(report["extra_sha256"], hashlib.sha256(b"\x00\xff\x01").hexdigest()) + self.assertEqual(report["rows"], [ + {"input": [0xffffffff, 0x80000000, 8, 0], "output": [0xffffffff, 0x80000000, 8]}, + {"input": [1, 2, 3, 4], "output": [1, 2, 3]}, + ]) + self.assertIn("fixture tool diagnostic", errors) + + def test_defaults_include_all_registers_and_empty_extra_identity(self): + output, _ = self.cli("--json") + report = json.loads(output) + self.assertEqual(report["wave_size"], 32) + self.assertEqual(report["memory"], "fine") + self.assertEqual(report["lds_bytes"], 4096) + self.assertEqual(report["extra_bytes"], 0) + self.assertEqual(report["extra_sha256"], hashlib.sha256(b"").hexdigest()) + self.assertEqual(report["output_registers"], [f"v{index}" for index in range(10, 26)]) + self.assertEqual(report["rows"][1]["output"], [1, 2, 3, 4] * 4) + + def test_json_keeps_input_order_across_dispatches_and_omits_padding(self): + self.rows.write_text("\n".join(f"{index} 0 0 0" for index in range(1025)), encoding="utf-8") + for options, dispatches in (([], 2), (["--wave64"], 3)): + with self.subTest(options=options): + output, errors = self.cli("--json", "--outs", "1", *options) + report = json.loads(output) + self.assertEqual(report["rows"], [{"input": [index, 0, 0, 0], "output": [index]} for index in range(1025)]) + self.assertEqual(errors.count("fixture tool diagnostic"), dispatches) + + def test_plain_hex_output_remains_unchanged(self): + output, errors = self.cli("--outs", "2") + self.assertEqual(output, "ffffffff 80000000\n00000001 00000002\n") + self.assertIn("fixture tool diagnostic", errors) + + def test_failed_dispatch_emits_no_json_report(self): + self.runner.write_text("import sys\nprint('fixture dispatch failed')\nsys.exit(7)\n", encoding="utf-8") + output = io.StringIO() + with self.assertRaises(subprocess.CalledProcessError) as error: + self.cli("--json", output=output) + self.assertEqual(error.exception.returncode, 7) + self.assertEqual(output.getvalue(), "") + + def test_invalid_json_input_fails_before_tool_lookup(self): + 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.assertRaises(SystemExit) as exited: + self.cli("--json", output=output) + self.assertEqual(exited.exception.code, 2) + target.assert_not_called() + self.assertEqual(output.getvalue(), "") + class FloatModeTests(unittest.TestCase): def test_python_requires_each_mode(self): @@ -182,6 +394,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('@"%HW_ORACLE_TEST_PYTHON%" "%~dp0probe.py" %*\n', encoding="ascii") + else: + binary.write_text(f'#!/bin/sh\nexec {shlex.quote(sys.executable)} {shlex.quote(str(runner))} "$@"\n', + encoding="utf-8") + binary.chmod(0o755) + with patch.object(hw_oracle, "oracle", return_value=binary), \ + patch.dict(os.environ, {"HW_ORACLE_TARGET": "", "HW_ORACLE_TEST_PYTHON": sys.executable}): + os.environ["ROCR_VISIBLE_DEVICES"] = "0" + self.assertEqual(hw_oracle.detected_target(binary, "1"), "gfx1100") + for visible, expected in (("0", "gfx1030"), ("0", "gfx1030"), + ("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")), \ @@ -202,6 +447,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") @@ -240,7 +509,7 @@ class OracleConfigurationTests(unittest.TestCase): def test_compiler_command_supports_wrapper_and_flags(self): with tempfile.TemporaryDirectory() as tmp: - def compile(command, *, check): + def compile(command, *, check, stdout): self.assertEqual(command[:3], ["ccache", "cc", "-fno-omit-frame-pointer"]) Path(command[command.index("-o") + 1]).write_bytes(b"executable") @@ -258,7 +527,7 @@ class OracleConfigurationTests(unittest.TestCase): source_file.write_text("first", encoding="utf-8") os.utime(source_file, (0, 0)) - def compile(command, *, check): + def compile(command, *, check, stdout): Path(command[command.index("-o") + 1]).write_bytes(source_file.read_bytes()) with patch.object(hw_oracle, "HERE", source), patch.object(hw_oracle, "CACHE", root / "cache"), \ @@ -272,7 +541,7 @@ class OracleConfigurationTests(unittest.TestCase): def test_compiler_change_invalidates_cache(self): with tempfile.TemporaryDirectory() as tmp: - def compile(command, *, check): + def compile(command, *, check, stdout): Path(command[command.index("-o") + 1]).write_bytes(command[0].encode()) with patch.object(hw_oracle, "CACHE", Path(tmp)), patch.object(hw_oracle, "rocm_root", return_value=None), \ @@ -287,7 +556,7 @@ class OracleConfigurationTests(unittest.TestCase): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) - def compile(command, *, check): + def compile(command, *, check, stdout): library_path = next(option for option in command if option.startswith("-L")) Path(command[command.index("-o") + 1]).write_bytes(library_path.encode()) @@ -310,7 +579,7 @@ class GroupSegmentTests(unittest.TestCase): with self.subTest(wave64=wave64), tempfile.TemporaryDirectory() as tmp: dispatch_sizes = [] - def dispatch(command, *, check, env): + def dispatch(command, *, check, env, stdout): self.assertTrue(check) dispatch_sizes.append(int(command[2])) Path(command[4]).write_bytes(bytes(int(command[2]) * 64)) @@ -378,5 +647,136 @@ class AssemblyTests(unittest.TestCase): self.assertTrue(code.is_file()) +class HipRuntimeTests(unittest.TestCase): + def test_runtime_defaults_to_hip_on_windows_only(self): + environment = {key: value for key, value in os.environ.items() if key != "HW_ORACLE_RUNTIME"} + with patch.dict(os.environ, environment, clear=True): + self.assertEqual(hw_oracle.runtime(), "hip" if os.name == "nt" else "hsa") + for selected in hw_oracle.RUNTIMES: + with patch.dict(os.environ, {"HW_ORACLE_RUNTIME": selected}): + self.assertEqual(hw_oracle.runtime(), selected) + with patch.dict(os.environ, {"HW_ORACLE_RUNTIME": "rocm"}), self.assertRaises(ValueError): + hw_oracle.runtime() + + def test_hip_run_assembles_and_dispatches_in_process(self): + calls = [] + + class Runtime: + @staticmethod + def target(): + calls.append(("target",)) + return "gfx1010" + + @staticmethod + def assemble(source, isa_target, wave64): + calls.append(("assemble", isa_target, wave64, b"v_mov_b32 v10, v4" in source)) + return b"code" + + @staticmethod + def dispatch(code, threads, payload, out_dwords, coarse): + calls.append(("dispatch", code, threads, len(payload), out_dwords, coarse)) + return bytes(threads * out_dwords * 4) + + environment = {key: value for key, value in os.environ.items() if key != "HW_ORACLE_TARGET"} + for wave64, coarse in ((False, False), (True, True)): + calls.clear() + with self.subTest(wave64=wave64), patch.dict(os.environ, {**environment, "HW_ORACLE_RUNTIME": "hip"}, clear=True), patch.object(hw_oracle, "hip_runtime", return_value=Runtime), patch.object(hw_oracle.subprocess, "run", side_effect=AssertionError("subprocess called")), patch.object(hw_oracle, "oracle", side_effect=AssertionError("oracle built")): + results = hw_oracle.run(" v_mov_b32 v10, v4", [(1, 2, 3, 4)], b"xy", wave64, coarse, **MODES) + wave = 64 if wave64 else 32 + self.assertEqual(results, [(0,) * 16]) + self.assertIn(("target",), calls) + self.assertEqual([call for call in calls if call != ("target",)], + [("assemble", "gfx1010", wave64, True), ("dispatch", b"code", wave, wave * 16 + 2, 16, coarse)]) + + def test_hip_output_must_match_the_padded_dispatch_size(self): + class Runtime: + @staticmethod + def assemble(source, isa_target, wave64): + return b"code" + + @staticmethod + def dispatch(code, threads, payload, out_dwords, coarse): + return bytes(threads * out_dwords * 4 - 4) + + with patch.dict(os.environ, {"HW_ORACLE_RUNTIME": "hip", "HW_ORACLE_TARGET": "gfx1010"}), patch.object(hw_oracle, "hip_runtime", return_value=Runtime), self.assertRaises(ValueError): + hw_oracle.run(" s_nop 0", [(0, 0, 0, 0)], **MODES) + + def test_hip_runtime_loads_the_script_next_to_the_tool(self): + self.assertEqual(Path(hw_oracle.hip_runtime().__file__), hw_oracle.HERE / "oracle_hip.py") + + +class HipOracleTests(unittest.TestCase): + def setUp(self): + import oracle_hip + self.oracle_hip = oracle_hip + + def test_cli_rejects_mixed_or_incomplete_requests(self): + for argv in ([], ["--target", "code", "32", "in", "out", "16"], ["--wave64", "code", "32", "in", "out", "16"], + ["code", "32", "in", "out"], ["code", "0", "in", "out", "16"], ["code", "1025", "in", "out", "16"], + ["code", "32", "in", "out", "17"]): + with self.subTest(argv=argv), contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as exit: + self.oracle_hip.main(argv) + self.assertEqual(exit.exception.code, 2) + + def test_comgr_step_tries_each_action_until_one_produces_output(self): + library = unittest.mock.MagicMock() + for name in ("amd_comgr_create_action_info", "amd_comgr_create_data_set", "amd_comgr_action_info_set_isa_name", + "amd_comgr_action_info_set_logging", "amd_comgr_action_info_set_option_list"): + getattr(library, name).return_value = 0 + outputs = {0x8: [], 0xA: [b"object"]} + actions = [] + library.amd_comgr_do_action.side_effect = lambda action, *args: actions.append(action) or (0 if action == 0xA else 1) + + def data_bytes(data_set, kind): + return [b"log"] if kind == self.oracle_hip.DATA_LOG else outputs[actions[-1]] + + isa = "amdgcn-amd-amdhsa--gfx1010" + with patch.object(self.oracle_hip, "comgr", return_value=library), patch.object(self.oracle_hip, "data_bytes", side_effect=data_bytes): + self.assertEqual(self.oracle_hip.comgr_step((0x8, 0xA), isa, [b"-mwavefrontsize64"], self.oracle_hip.Handle(), + self.oracle_hip.DATA_RELOCATABLE), b"object") + self.assertEqual(actions, [0x8, 0xA]) + outputs[0xA] = [] + with self.assertRaises(self.oracle_hip.OracleError) as error: + self.oracle_hip.comgr_step((0x8, 0xA), isa, [], self.oracle_hip.Handle(), self.oracle_hip.DATA_RELOCATABLE) + self.assertIn("action 0x8 status 1: log", str(error.exception)) + self.assertIn("action 0xa status 0: log", str(error.exception)) + + def test_device_selection_follows_rocr_visible_devices(self): + library = unittest.mock.MagicMock() + library.hipInit.return_value = 0 + + def count(pointer): + pointer._obj.value = 1 + return 0 + + library.hipGetDeviceCount.side_effect = count + base = {key: value for key, value in os.environ.items() if key not in ("ROCR_VISIBLE_DEVICES", "HIP_VISIBLE_DEVICES")} + for environment, expected in (({"ROCR_VISIBLE_DEVICES": "1"}, "1"), ({"ROCR_VISIBLE_DEVICES": "1", "HIP_VISIBLE_DEVICES": "0"}, "0")): + self.oracle_hip.hip.cache_clear() + with self.subTest(environment=environment), patch.dict(os.environ, {**base, **environment}, clear=True), \ + patch.object(self.oracle_hip, "load", return_value=library): + self.oracle_hip.hip() + self.assertEqual(os.environ["HIP_VISIBLE_DEVICES"], expected) + self.oracle_hip.hip.cache_clear() + + def test_assembles_both_wave_sizes_with_the_driver_comgr(self): + if os.name != "nt": + self.skipTest("comgr from AMD's Windows driver") + try: + self.oracle_hip.comgr() + except self.oracle_hip.OracleError as error: + self.skipTest(str(error)) + template = (hw_oracle.HERE / "template.s").read_text() + for wave64, mask in ((False, "s8"), (True, "s[8:9]")): + text = template + for key, value in (("@TARGET@", "gfx1010"), ("@WAVE32@", "0" if wave64 else "1"), ("@WAVESIZE@", "64" if wave64 else "32"), + ("@DENORM@", "0"), ("@DENORM16@", "3"), ("@IEEE@", "0"), ("@DX10_CLAMP@", "1"), ("@ROUND32@", "0"), + ("@ROUND16@", "0"), ("@FP16_OVERFLOW@", "0"), ("@LDS@", "4096"), + ("@BODY@", f" v_cmp_lt_f32 {mask}, v4, v5\n v_cndmask_b32 v10, v6, v7, {mask}\n")): + text = text.replace(key, value) + with self.subTest(wave64=wave64): + self.assertEqual(self.oracle_hip.assemble(text.encode(), "gfx1010", wave64)[:4], b"\x7fELF") + + if __name__ == "__main__": unittest.main() diff --git a/tools/import_audit.py b/tools/import_audit.py index 0eb4e52391..6ee58ad673 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -1,7 +1,7 @@ import argparse import hashlib import json -import re +import os import struct import subprocess import sys @@ -14,8 +14,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") PE_DIRECTORY_OFFSET = {0x20b: 112, 0x10b: 96} @@ -50,6 +48,8 @@ def elf_sections(path, data): ident, section_offset, section_size, section_count = header[0], header[6], header[11], header[12] if ident[4] != 2 or ident[5] != 1 or section_size != 64: raise AuditError(f"{path}: only little-endian ELF64 with 64-byte section headers is supported (class {ident[4]}, data {ident[5]}, shentsize {section_size})") + if ident[6] != 1 or header[3] != 1: + raise AuditError(f"{path}: unsupported ELF version (identification {ident[6]}, object {header[3]})") if not section_offset: raise AuditError(f"{path}: ELF file without a section header table is not supported") if not section_count: @@ -61,6 +61,8 @@ def elf_sections(path, data): def elf_program_dynamic_symbols(path, data): header = unpack("<16sHHIQQQIHHHHHH", data, 0, path, "ELF header") ident, program_offset, program_size, program_count = header[0], header[5], header[9], header[10] + if ident[6] != 1 or header[3] != 1: + raise AuditError(f"{path}: unsupported ELF version (identification {ident[6]}, object {header[3]})") if ident[4] != 2 or ident[5] != 1 or program_size != 56 or not program_offset or not program_count: raise AuditError(f"{path}: sectionless ELF needs little-endian ELF64 program headers") take(data, program_offset, program_count * program_size, path, "program header table") @@ -104,11 +106,12 @@ def elf_program_dynamic_symbols(path, data): if 4 in dynamic: hash_offset = file_offset(dynamic[4], 8, "SysV hash table") buckets, chains = unpack("<II", data, hash_offset, path, "SysV hash header") - take(data, hash_offset, 8 + 4 * (buckets + chains), path, "SysV hash table") + file_offset(dynamic[4], 8 + 4 * (buckets + chains), "SysV hash table") symbol_count = chains elif 0x6ffffef5 in dynamic: hash_offset = file_offset(dynamic[0x6ffffef5], 16, "GNU hash header") buckets, first_symbol, bloom_count, _ = unpack("<IIII", data, hash_offset, path, "GNU hash header") + file_offset(dynamic[0x6ffffef5], 16 + bloom_count * 8 + buckets * 4, "GNU hash prefix") bucket_offset = hash_offset + 16 + bloom_count * 8 bucket_data = take(data, bucket_offset, buckets * 4, path, "GNU hash buckets") chain_offset = bucket_offset + buckets * 4 @@ -148,7 +151,7 @@ def elf_program_dynamic_symbols(path, data): def elf_exports(path, data): header = unpack("<16sHHIQQQIHHHHHH", data, 0, path, "ELF header") - if not header[6]: + if not header[6] or header[6] >= len(data): return elf_program_dynamic_symbols(path, data)[1] sections = elf_sections(path, data) names = set() @@ -177,13 +180,13 @@ 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 def elf_imports(path, data): header = unpack("<16sHHIQQQIHHHHHH", data, 0, path, "ELF header") - if not header[6]: + if not header[6] or header[6] >= len(data): imports, _, needed = elf_program_dynamic_symbols(path, data) return imports, needed sections = elf_sections(path, data) @@ -208,12 +211,14 @@ def elf_imports(path, data): take(data, strings_offset, strings_size, path, "dynamic string table") for position in range(0, size, 16): tag, value = unpack("<qQ", data, offset + position, path, "dynamic entry") + if tag == 0: + break if tag == 1: needed.add(cstring(data, strings_offset + value, strings_offset + strings_size, path, "needed library name")) return symbols, needed -def pe_exports(path, data): +def pe_headers(path, data): header = unpack("<I", data, 0x3c, path, "PE header offset")[0] if unpack("<I", data, header, path, "PE signature")[0] != 0x4550: raise AuditError(f"{path}: invalid PE signature at 0x{header:x}") @@ -226,12 +231,10 @@ def pe_exports(path, data): if optional_size < directory + 8: raise AuditError(f"{path}: PE optional header of {optional_size} bytes has no export directory") take(data, optional, optional_size, path, "PE optional header") - if unpack("<I", data, optional + directory - 4, path, "data directory count")[0] == 0: + count = unpack("<I", data, optional + directory - 4, path, "data directory count")[0] + if count == 0: raise AuditError(f"{path}: PE file declares no data directories") headers_size = unpack("<I", data, optional + 60, path, "header size")[0] - rva, size = unpack("<II", data, optional + directory, path, "export data directory") - if not rva or size < 40: - raise AuditError(f"{path}: PE file has no export table") sections = [unpack("<8sIIIIIIHHI", data, optional + optional_size + index * 40, path, f"PE section {index}") for index in range(section_count)] def to_offset(address, length): @@ -249,6 +252,15 @@ def pe_exports(path, data): return raw_offset + relative, min(raw_offset + raw_size, len(data)) raise AuditError(f"{path}: RVA 0x{address:x} is not inside any section") + return to_offset, optional + directory, count + + +def pe_exports(path, data): + to_offset, directory, _ = pe_headers(path, data) + rva, size = unpack("<II", data, directory, path, "export data directory") + if not rva or size < 40: + raise AuditError(f"{path}: PE file has no export table") + table = unpack("<IIHHIIIIIII", data, to_offset(rva, 40)[0], path, "export directory") function_count, name_count, functions_rva, names_rva, ordinals_rva = table[6:11] names = set() @@ -280,6 +292,37 @@ def pe_exports(path, data): return names +def pe_imports(path, data): + to_offset, directory, count = pe_headers(path, data) + if count < 2: + return set() + rva, size = unpack("<II", data, directory + 8, path, "import data directory") + if not rva: + return set() + if size < 20: + raise AuditError(f"{path}: PE import table of {size} bytes holds no import entry") + names = set() + for index in range(size // 20): + entry = unpack("<IIIII", data, to_offset(rva + index * 20, 20)[0], path, "import directory entry") + if entry == (0, 0, 0, 0, 0): + return names + if not entry[3]: + raise AuditError(f"{path}: PE import entry {index} names no library") + offset, limit = to_offset(entry[3], 1) + names.add(cstring(data, offset, limit, path, "import library name", "ASCII", "strict")) + raise AuditError(f"{path}: PE import table is not NUL-terminated") + + +def read_host_imports(path, data=None): + if data is None: + data = path.read_bytes() + if data[:2] == b"MZ": + return pe_imports(path, data) + if data[:4] == b"\x7fELF": + return elf_imports(path, data)[1] + raise AuditError(f"{path}: neither ELF nor PE (first bytes {data[:4]!r})") + + def read_exports(path, data=None): if data is None: data = path.read_bytes() @@ -320,24 +363,103 @@ def built_libraries(directories): return {name: sorted(files) for name, files in exports.items()}, libraries +def built_host_dependencies(directories): + """Map each built .prx to its imported .prx names and identify PE importers. + + The loader resolves these host-to-host dependencies with LoadLibrary, so a + name with no file in --libs or --modules fails even when every game import + is implemented. System DLLs resolve through the OS and are not reported. + """ + direct = {} + windows = set() + images = {} + for directory in directories: + for file in directory_files(directory, "*.prx"): + data = file.read_bytes() + digest = hashlib.sha256(data).digest() + if file.name in images: + previous, fingerprint = images[file.name] + if digest != fingerprint: + raise AuditError(f"ambiguous built library {file.name}: {previous} and {file} have different contents") + continue + images[file.name] = (file, digest) + direct[file.name] = sorted({name for name in read_host_imports(file, data) if name.lower().endswith(".prx")}) + if data[:2] == b"MZ": + windows.add(file.name) + return direct, windows + + +def audit_host_dependencies(direct, libraries, modules, windows=()): + """Walk host imports transitively; list each missing library with its chain.""" + available = set(libraries) | set(modules) + folded = defaultdict(set) + for name in available: + folded[name.lower()].add(name) + missing = [] + for root in sorted(direct): + expanded = set() + stack = [(name, [root, name]) for name in reversed(direct[root])] + while stack: + name, chain = stack.pop() + if chain[-2] in windows: + matches = folded.get(name.lower(), ()) + if len(matches) > 1: + raise AuditError(f"ambiguous Windows host library {name}: {', '.join(sorted(matches))}") + name = next(iter(matches), name) + chain[-1] = name + if name not in available: + missing.append({"importer": root, "chain": chain}) + elif name in direct and name not in expanded and name not in chain[1:-1]: + expanded.add(name) + for dependency in reversed(direct[name]): + if dependency in chain: + continue + stack.append((dependency, chain + [dependency])) + return sorted(missing, key=lambda entry: (entry["importer"], entry["chain"])) + + +def module_files(directory): + for file in directory_files(directory, "*"): + with file.open("rb") as handle: + magic = handle.read(4) + if magic == b"\x7fELF" or (magic.startswith(b"MZ") and file.name.endswith(".guest.prx")): + yield file + elif (file.suffix.lower() in (".prx", ".sprx", ".suprx", ".debug_prx", ".elf") + or magic.startswith(b"MZ") + or magic in (b"\x4f\x15\x3d\x1d", b"\x54\x14\xf5\xee")): + raise AuditError(f"{file}: expected ELF module (first bytes {magic!r})") + + def read_modules(directories): imports = {} exports = defaultdict(set) + images = {} for directory in directories: - for file in directory_files(directory, "*"): + for file in module_files(directory): data = file.read_bytes() - own = elf_imports(file, data) - symbols, needed = own - imports[file.name] = (sorted(symbols), sorted(needed)) - for name in elf_exports(file, data): - exports[name].add(file.name) + digest = hashlib.sha256(data).digest() + if file.name in images: + previous, fingerprint = images[file.name] + if digest != fingerprint: + raise AuditError(f"ambiguous guest module {file.name}: {previous} and {file} have different contents") + continue + images[file.name] = (file, digest) + if data.startswith(b"MZ"): + imports[file.name] = ([], []) + names = pe_exports(file, data) + else: + symbols, needed = elf_imports(file, data) + imports[file.name] = (sorted(symbols), sorted(needed)) + names = elf_exports(file, data) + for name in names: + exports[name.split(MODULE_SUFFIX, 1)[0]].add(file.name) return imports, {name: sorted(files) for name, files in exports.items()} -def nid_of(name, aliases): +def nids_of(name, aliases): if name in aliases: return aliases[name] - return nid_names.compute_nid(name.removesuffix(NID_POSTFIX)) + return {nid_names.compute_nid(nid_names.strip_nid_postfix(name))} def source_stubs(source): @@ -347,16 +469,15 @@ def source_stubs(source): shared = {} for library in sorted(item for item in Path(source).iterdir() if item.is_dir()): aliases = {} - for file in library.rglob("*.cpp"): + for file in nid_names.source_files(library): for nid, name in nid_names.source_aliases(file.read_text(errors="ignore")): - aliases[name] = nid + aliases.setdefault(name, set()).add(nid) group = progress.scan_library(library) if "shared_sources" in group: shared[library.name] = group["shared_sources"] - done = {nid_of(name, aliases) for name in group["done_names"]} + done = {nid for name in group["done_names"] for nid in nids_of(name, aliases)} for name in group["todo_names"]: - nid = nid_of(name, aliases) - if nid not in done: + for nid in nids_of(name, aliases) - done: stubs.setdefault((library.name + ".prx", nid), name) for library, owner in shared.items(): chain = [library] @@ -383,12 +504,29 @@ def read_registry(path): if not isinstance(entry, dict) or not isinstance(entry.get("nid"), str) or not isinstance(entry.get("library"), str): raise AuditError(f"{path}: entry {index} needs string fields 'nid' and 'library': {entry!r}") nid = entry["nid"].split("#", 1)[0] - if not NID.fullmatch(nid): - raise AuditError(f"{path}: entry {index} has an invalid NID {entry['nid']!r}") - imports.append((nid, entry["library"])) + if not nid or any(character.isspace() or ord(character) < 32 or ord(character) == 127 for character in nid): + raise AuditError(f"{path}: entry {index} has an invalid import symbol {entry['nid']!r}") + weak = entry.get("weak", False) + if not isinstance(weak, bool): + raise AuditError(f"{path}: entry {index} needs a boolean 'weak' field") + imports.append((nid, entry["library"], weak)) return imports +def needed_registry_path(path): + return path.with_name(path.name.removesuffix(".registry.json") + ".needed.json") + + +def read_needed_registry(path): + try: + libraries = json.loads(path.read_text(encoding="utf-8-sig")) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error: + raise AuditError(f"{path}: cannot read needed libraries: {error}") from error + if not isinstance(libraries, list) or any(not isinstance(library, str) or not library for library in libraries): + raise AuditError(f"{path}: needed libraries must be a JSON list of nonempty strings") + return libraries + + def check_conservation(references, records): classified = sum(record["references"] for record in records) unknown = sorted({record["class"] for record in records} - set(CLASSES)) @@ -396,6 +534,30 @@ def check_conservation(references, records): raise AuditError(f"conservation failed: {references} references in, {classified} classified, unknown classes {unknown}") +MODULE_FILE_SUFFIXES = (".debug_prx", ".suprx", ".sprx", ".prx") + + +def module_stem(name, fold): + for suffix in MODULE_FILE_SUFFIXES: + if len(name) > len(suffix) and name.endswith(suffix): + name = name[:-len(suffix)] + break + return name.lower() if fold else name + + +class ModuleNames(set): + """Title module names, matched like the relinker: by the name without its .prx, .sprx, .suprx or .debug_prx suffix, and + ignoring case for Windows output.""" + + def __init__(self, names=(), fold=False): + super().__init__(names) + self.fold = fold + self.stems = {module_stem(name, fold) for name in self} + + def __contains__(self, library): + return isinstance(library, str) and module_stem(library, self.fold) in self.stems + + def classify(nid, library, providers, stubs, modules): if library and library in modules: return "module" @@ -406,28 +568,38 @@ def classify(nid, library, providers, stubs, modules): return "implemented" -def audit(imports, exports, stubs, libraries, modules, module_imports=None, module_exports=None): +def audit(imports, exports, stubs, libraries, modules, module_imports=None, module_exports=None, *, needed_libraries=()): records = {} - for nid, library in imports: - record = records.setdefault((nid, library), {"nid": nid, "library": library, "references": 0}) + for nid, library, weak in imports: + record = records.setdefault((nid, library), {"nid": nid, "library": library, "references": 0, "weak": weak}) record["references"] += 1 + record["weak"] = record["weak"] and weak for module, (symbols, needed) in (module_imports or {}).items(): for nid in symbols: - record = records.setdefault((nid, None, module), {"nid": nid, "library": None, "module": module, "references": 0}) + record = records.setdefault((nid, None, module), {"nid": nid, "library": None, "module": module, "references": 0, "weak": False}) record["references"] += 1 for record in records.values(): nid, library = record["nid"], record["library"] providers = exports.get(nid, []) - if library is None: + if not library: providers = sorted(set(providers) | set((module_exports or {}).get(nid, []))) - kind = classify(nid, library, providers, stubs, modules) + resolved_nid = nid + if not providers: + resolved_nid = nid_names.compute_nid(nid) + providers = exports.get(resolved_nid, []) + if not library: + providers = sorted(set(providers) | set((module_exports or {}).get(resolved_nid, []))) + kind = classify(resolved_nid, library, providers, stubs, modules) record["class"] = kind record["providers"] = providers record["library_mismatch"] = kind in ("implemented", "stub") and bool(library) and library not in providers and "libc.prx" not in providers - record["name"] = next((stubs[(provider, nid)] for provider in providers if (provider, nid) in stubs), None) if kind == "stub" else None + record["name"] = next((stubs[(provider, resolved_nid)] for provider in providers if (provider, resolved_nid) in stubs), None) if kind == "stub" else None ordered = sorted(records.values(), key=lambda record: (record["class"], record["library"] or "", record.get("module") or "", record["nid"])) check_conservation(len(imports) + sum(len(symbols) for symbols, _ in (module_imports or {}).values()), ordered) missing = defaultdict(int) + for library in needed_libraries: + if library not in libraries and library not in modules: + missing[library] = 0 for record in ordered: library = record["library"] if library and library not in libraries and library not in modules: @@ -440,9 +612,11 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu def current_stop(imports, records): - """The first import the loader cannot resolve, in registry order.""" + """The first import the loader cannot resolve, in registry order; a weak import resolves to null instead.""" classes = {(record["nid"], record["library"]): record for record in records} - for nid, library in imports: + for nid, library, weak in imports: + if weak: + continue record = classes.get((nid, library)) if record is not None and record["class"] == "absent": return record @@ -483,13 +657,16 @@ def write_json(path, result): def git_stamp(source=ROOT): try: commit = subprocess.run(["git", "-C", str(source), "rev-parse", "HEAD"], capture_output=True, text=True, check=True).stdout.strip() - dirty = bool(subprocess.run(["git", "-C", str(source), "status", "--porcelain"], capture_output=True, text=True, check=True).stdout.strip()) except (OSError, subprocess.CalledProcessError): return None, None + try: + dirty = bool(subprocess.run(["git", "-C", str(source), "status", "--porcelain"], capture_output=True, text=True, check=True).stdout.strip()) + except (OSError, subprocess.CalledProcessError): + dirty = None return commit, dirty -def summarize(records, references, missing): +def summarize(records, references, missing, missing_hosts=()): counts = {kind: sum(1 for record in records if record["class"] == kind) for kind in CLASSES} by_references = {kind: sum(record["references"] for record in records if record["class"] == kind) for kind in CLASSES} return { @@ -498,6 +675,7 @@ def summarize(records, references, missing): "unique_by_class": counts, "references_by_class": by_references, "missing_libraries": missing, + "missing_host_dependencies": [{"importer": entry["importer"], "chain": list(entry["chain"])} for entry in missing_hosts], "library_mismatch": sum(1 for record in records if record["library_mismatch"]), } @@ -511,7 +689,25 @@ def label(record): return f"{text} (imported by {record['module']})" if record.get("module") else text -def render(summary, records, missing, stops): +def locate_missing(names, roots, depth=4): + wanted = {name.lower(): name for name in names} + found = {} + for root in roots: + root = Path(root) + if not root.is_dir(): + continue + base = len(root.parts) + for directory, directories, files in os.walk(root): + if len(Path(directory).parts) - base >= depth: + directories[:] = [] + for file in files: + name = wanted.get(file.lower()) + if name is not None and name not in found: + found[name] = Path(directory) + return found + + +def render(summary, records, missing, stops, missing_hosts=(), located=None): lines = [f"{summary['references']} references, {summary['unique_imports']} unique imports"] for kind in CLASSES: lines.append(f" {kind:<12}{summary['unique_by_class'][kind]:>6} imports {summary['references_by_class'][kind]:>6} references") @@ -536,6 +732,14 @@ def render(summary, records, missing, stops): lines.append("") lines.append(f"Needed libraries with no file in --libs or --modules ({len(missing)})") lines.extend(f" {library}: {count} imports" for library, count in missing.items()) + if any(count == 0 for count in missing.values()): + lines.append(" Libraries with 0 imports are declared dependencies; Windows can skip them if no import needs them") + for library, directory in sorted((located or {}).items()): + lines.append(f" {library} is in {directory}: pass that directory with --modules") + if missing_hosts: + lines.append("") + lines.append(f"Host dependencies with no file in --libs or --modules ({len(missing_hosts)})") + 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") @@ -550,10 +754,15 @@ def main(argv=None): parser.add_argument("--source", default=str(PRX), help="core/libs/prx tree used to find throwing stubs (default: this repository's)") parser.add_argument("--names", help="aerolib.csv style file (NID, name per line) used to name absent imports") parser.add_argument("--json", type=Path, help="write the full result to this file") + parser.add_argument("--strict-stubs", action="store_true", + help="also fail when a referenced import resolves to a throwing stub") + parser.add_argument("--strict-providers", action="store_true", help="fail when a named import is provided only by another library; retains the existing libc and title-module rules") args = parser.parse_args(argv) try: + dependency_registries = [needed_registry_path(path) for path in args.registries if path.name.endswith(".registry.json")] + dependency_registries = [path for path in dependency_registries if path.exists()] if args.json: - inputs = args.registries + ([Path(args.names)] if args.names else []) + inputs = args.registries + dependency_registries + ([Path(args.names)] if args.names else []) inputs += [file for directory in args.libs for file in directory_files(directory, "*.prx")] inputs += [file for directory in args.modules for file in directory_files(directory, "*")] inputs += list(Path(args.source).rglob("*.cpp")) @@ -562,12 +771,18 @@ def main(argv=None): raise AuditError(f"{args.json}: JSON output would overwrite an input file") registries = [(registry, read_registry(registry)) for registry in args.registries] 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, windows = 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) + modules = set(module_imports) + modules |= {name.removesuffix(".guest.prx") for name in modules if name.endswith(".guest.prx")} + modules = ModuleNames(modules, fold=bool(windows)) + records, missing = audit(imports, exports, source_stubs(args.source), libraries, modules, module_imports, module_exports, needed_libraries=needed) + missing_hosts = audit_host_dependencies(host_dependencies, libraries, modules, windows) 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)) @@ -575,7 +790,7 @@ def main(argv=None): "nid": entry["stop"]["nid"] if entry["stop"] else None, "library": entry["stop"]["library"] if entry["stop"] else None, "name": entry["stop"]["name"] if entry["stop"] else None} for entry in stops] - result = {"source_commit": commit, "source_dirty": dirty, "registries": [str(path) for path in args.registries], **summary, "stops": json_stops, "imports": records} + result = {"source_commit": commit, "source_dirty": dirty, "registries": [str(path) for path in args.registries], "dependency_registries": [str(path) for path in dependency_registries], "needed_libraries": needed, **summary, "stops": json_stops, "imports": records} write_json(args.json, result) except AuditError as error: print(f"FAIL: {error}", file=sys.stderr) @@ -583,11 +798,13 @@ 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 else 0 + located = locate_missing(missing, {path.resolve().parent for path in args.registries}) if missing else None + print(render(summary, records, missing, stops, missing_hosts, located)) + return 1 if summary["unique_by_class"]["absent"] or missing or missing_hosts or (args.strict_providers and summary["library_mismatch"]) or (args.strict_stubs and summary["unique_by_class"]["stub"]) else 0 if __name__ == "__main__": - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8", errors="replace") + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="replace") raise SystemExit(main()) diff --git a/tools/nid_names.py b/tools/nid_names.py index 2cb0d756e4..86610f409c 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -26,6 +26,7 @@ CPP_TOKEN = re.compile( r"|(?P<code>(?:[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): @@ -42,6 +43,10 @@ def compute_nid(symbol): return "".join(out) +def strip_nid_postfix(name): + return re.sub(r"_nid_disambig[0-9]+$", "", name.removesuffix(NID_POSTFIX)) + + def load_db(path): if not path.exists(): path.parent.mkdir(parents=True, exist_ok=True) @@ -92,6 +97,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): @@ -109,8 +130,10 @@ def collect_real_names(): for match in source_definitions(text): name = match.group(1) names.add(name) - if name.endswith(NID_POSTFIX): - names.add(name[:-len(NID_POSTFIX)]) + names.add(strip_nid_postfix(name)) + for name in source_assembly_definitions(text): + names.add(name) + names.add(strip_nid_postfix(name)) return names @@ -131,36 +154,69 @@ def option_name(argument, options): def nid_arguments(arguments, options): normalized = [] collecting = False + collecting_option = "--nid" for index, argument in enumerate(arguments): if argument == "--": normalized.extend(arguments[index:]) break name = option_name(argument, options) - if name == "--nid" and "=" not in argument: + if name in ("--nid", "--name", "--symbol") and "=" not in argument: collecting = True + collecting_option = "--name" if name in ("--name", "--symbol") else "--nid" continue if collecting and name is None: - normalized.append("--nid=" + argument) + normalized.append(collecting_option + "=" + argument) continue collecting = False normalized.append(argument) return normalized +def symbol_file(path): + if path == "-": + text = sys.stdin.buffer.read().decode("utf-8-sig") if hasattr(sys.stdin, "buffer") else sys.stdin.read().removeprefix("\ufeff") + else: + text = Path(path).read_text(encoding="utf-8-sig") + return [line for raw in text.split("\n") if (line := raw.removesuffix("\r"))] + + def main(argv=None): parser = argparse.ArgumentParser( description="Suggest real names for Unknown NID stubs") parser.add_argument("--db", default=os.environ.get("ANYPS5_NID_DB"), - help="NID database CSV (downloaded to a cache dir by default)") + help="NID database CSV; defaults to $ANYPS5_NID_DB, then to a copy downloaded to a cache dir") parser.add_argument("--nid", action="append", default=None, help="resolve only these NIDs instead of scanning the tree") + parser.add_argument("--name", "--symbol", dest="name", action="append", default=None, + help="compute NIDs for these symbol names") + parser.add_argument("--from-file", action="append", default=[], metavar="PATH", + help="append symbol names from a UTF-8 file, one nonempty line per name; repeat or use - for stdin") parser.add_argument("--json", action="store_true", help="emit machine-readable JSON") options = [option for action in parser._actions for option in action.option_strings] arguments = sys.argv[1:] if argv is None else argv args = parser.parse_args(nid_arguments(arguments, options)) - requested_nids = any(option_name(argument, options) == "--nid" for argument in arguments) - for nid in args.nid or []: + requested_options = {option_name(argument, options) for argument in arguments} + requested_nids = "--nid" in requested_options + if requested_nids and requested_options & {"--name", "--symbol"}: + parser.error("--name/--symbol cannot be combined with --nid") + if requested_nids and "--from-file" in requested_options: + parser.error("--from-file cannot be combined with --nid") + if requested_options & {"--name", "--symbol", "--from-file"}: + try: + names = [*(args.name or []), *(name for path in args.from_file for name in symbol_file(path))] + except (OSError, UnicodeError) as error: + parser.error(str(error)) + rows = [{"name": name, "nid": compute_nid(name)} for name in names] + if args.json: + print(json.dumps({"symbols": rows}, indent=1)) + else: + print("%-40s %s" % ("NAME", "NID")) + for row in rows: + print("%-40s %s" % (row["name"], row["nid"])) + return 0 + args.nid = [nid.split("#", 1)[0] for nid in args.nid or []] + for nid in args.nid: if re.fullmatch(r"[A-Za-z0-9+\-]{11}", nid) is None: parser.error(f"invalid NID {nid!r}: expected 11 characters from A-Z, a-z, 0-9, + or -") @@ -205,4 +261,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 c825539e1d..effd4c1cac 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -1,21 +1,46 @@ import argparse +import json +import os import re import shutil +import stat +import sys import tarfile import tempfile import zipfile from pathlib import Path ROOT = Path(__file__).resolve().parent.parent +WEB_FILES = ("index.html", "app.css", "app.mjs", "archive.mjs", "batch.mjs", "output-directory.mjs", "worker.mjs", + "native/relinker_wasm.mjs", "native/relinker_wasm.wasm") def windows_runtime(build): - for line in (build / "CMakeCache.txt").read_text(encoding="utf-8").splitlines(): + cache = build / "CMakeCache.txt" + if not cache.is_file(): + raise RuntimeError(f"Missing CMakeCache.txt in build directory: {build}") + for line in cache.read_text(encoding="utf-8-sig").splitlines(): if line.startswith("CMAKE_CXX_COMPILER:"): compiler = line.partition("=")[2] if compiler and not compiler.endswith("-NOTFOUND"): return Path(compiler).parent - raise RuntimeError(f"CMAKE_CXX_COMPILER is not configured in {build / 'CMakeCache.txt'}") + raise RuntimeError(f"CMAKE_CXX_COMPILER is not configured in {cache}") + + +def tracy_runtime(platform, build): + cache = build / "CMakeCache.txt" + if not cache.is_file(): + return [] + lines = cache.read_text(encoding="utf-8-sig").splitlines() + enabled = next((line.partition("=")[2].upper() for line in lines if line.startswith("APS5_ENABLE_TRACY:")), "OFF") + if enabled in ("", "0", "OFF", "NO", "FALSE", "N", "IGNORE", "NOTFOUND") or enabled.endswith("-NOTFOUND"): + return [] + pattern = "libTracyClient.dll" if platform == "windows" else "libTracyClient.so.*" + directory = build / "core/libs/libs" + clients = sorted(directory.glob(pattern)) + if len(clients) != 1: + raise RuntimeError(f"Missing or ambiguous Tracy runtime for APS5_ENABLE_TRACY: {directory / pattern}") + return clients def package(platform, build, output, version): @@ -28,11 +53,19 @@ def package(platform, build, output, version): raise RuntimeError(f"Missing patched libraries: {', '.join(sorted(missing))}") executable = "relinker.exe" if platform == "windows" else "relinker" files = [library for library in libraries if library.name in expected] + licenses = [] + if "libSceAjm.native.prx" in expected: + licenses.append((ROOT / "3rdparty/vgmstream/COPYING", "libs/licenses/vgmstream.txt")) + if "libSceVorbisDec.prx" in expected: + licenses.append((ROOT / "3rdparty/ogg/COPYING", "libs/licenses/ogg.txt")) + licenses.append((ROOT / "3rdparty/vorbis/COPYING", "libs/licenses/vorbis.txt")) if platform == "windows": runtime = windows_runtime(build) files.extend(runtime / name for name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll")) + files.extend(build / "core/libs/libs" / name for name in ("mimalloc.dll", "mimalloc-redirect.dll")) + files.extend(tracy_runtime(platform, build)) binary = build / "core/relinker" / executable - for file in [*files, binary]: + for file in [*files, binary, *(source for source, _ in licenses)]: if not file.is_file() or file.stat().st_size == 0: raise RuntimeError(f"Missing or empty release file: {file}") asset = f"relinker-{version}.exe" if platform == "windows" else f"relinker-{version}" @@ -41,21 +74,33 @@ 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): + 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) with zipfile.ZipFile(staging / f"prx-{platform}-{version}.zip", "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: for file in files: archive.write(file, arcname=f"libs/{file.name}") + for source, destination in licenses: + archive.write(source, arcname=destination) with tarfile.open(staging / f"prx-{platform}-{version}.tar.gz", "w:gz", compresslevel=9, dereference=True) as archive: for file in files: archive.add(file, arcname=f"libs/{file.name}") + for source, destination in licenses: + archive.add(source, arcname=destination) shutil.copy2(binary, staging / asset) for file in sorted(staging.iterdir()): file.replace(output / file.name) def collect_docs(source, output): + if not source.is_dir(): + raise RuntimeError(f"Documentation source is not a directory: {source}") documents = sorted(document for document in source.rglob("*.md") if document.is_file()) if not documents: raise RuntimeError(f"No Markdown documents found in {source}") @@ -69,23 +114,63 @@ def collect_docs(source, output): shutil.copy2(document, output / document.name) -def main(): +def package_web(build, output, version, revision): + if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", version) or version.endswith("."): + raise ValueError(f"Invalid release tag for asset filenames: {version}") + if not re.fullmatch(r"[0-9a-f]{40}", revision): + raise ValueError("Web release revision must be a full commit SHA") + source = build / "web" + for name in WEB_FILES: + file = source / name + if not file.is_file() or file.stat().st_size == 0: + raise RuntimeError(f"Missing or empty web release file: {file}") + name = f"web-{version}.zip" + destination = output / name + if destination.exists() and not destination.is_file(): + raise RuntimeError(f"Release destination is not a file: {destination}") + output.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix=".release-", dir=output) as temporary: + asset = Path(temporary) / name + with zipfile.ZipFile(asset, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: + for file in WEB_FILES: + archive.write(source / file, arcname=file) + archive.writestr("web-release.json", json.dumps({"tag": version, "revision": revision}) + "\n") + asset.replace(destination) + + +def main(argv=None): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser() - parser.add_argument("--platform", choices=("linux", "windows")) + parser.add_argument("--platform", choices=("linux", "windows", "web")) parser.add_argument("--build", type=Path) parser.add_argument("--version") + parser.add_argument("--revision") parser.add_argument("--docs", type=Path) parser.add_argument("--output", required=True, type=Path) - args = parser.parse_args() - if args.docs is not None: - if args.platform is not None or args.build is not None or args.version is not None: - parser.error("--docs cannot be combined with --platform, --build or --version") - collect_docs(args.docs, args.output) - else: - if args.platform is None or args.build is None or args.version is None: - parser.error("--platform, --build and --version are required when --docs is not specified") - package(args.platform, args.build, args.output, args.version) + args = parser.parse_args(argv) + try: + if args.docs is not None: + if args.platform is not None or args.build is not None or args.version is not None or args.revision is not None: + parser.error("--docs cannot be combined with --platform, --build, --version or --revision") + collect_docs(args.docs, args.output) + else: + if args.platform is None or args.build is None or args.version is None: + parser.error("--platform, --build and --version are required when --docs is not specified") + if args.platform == "web": + if args.revision is None: + parser.error("--revision is required for --platform web") + package_web(args.build, args.output, args.version, args.revision) + else: + if args.revision is not None: + parser.error("--revision requires --platform web") + package(args.platform, args.build, args.output, args.version) + except (RuntimeError, ValueError, OSError) as error: + parser.error(str(error)) + return 0 if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/tools/pad_input.py b/tools/pad_input.py index af7238e897..69b1d64579 100644 --- a/tools/pad_input.py +++ b/tools/pad_input.py @@ -2,6 +2,7 @@ import argparse import os from pathlib import Path import re +import stat import tempfile import time @@ -11,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, } @@ -24,14 +26,19 @@ def previous_sequence(path): if os.name == "nt" and line.endswith(b"\r\n"): line = line[:-2] + b"\n" tokens = line.split() - if len(line.rstrip(b"\n")) > 127 or len(tokens) not in (3, 7): + timeline = tokens[:1] == [b"APS5_PAD_TIMELINE"] + if timeline: + tokens = tokens[1:] + if len(line.rstrip(b"\n")) > 127 or len(tokens) not in ((3,) if timeline else (3, 7)): return 0 if any(re.fullmatch(rb"\+?[0-9]+", token) is None for token in tokens): return 0 values = [int(token) for token in tokens] if any(value > 2**64 - 1 for value in values): return 0 - if values[0] == 0 or values[1] & ~0x10fffe or not 1 <= values[2] <= 5000: + if timeline: + return values[1] if values[0] == 1 and 1 <= values[2] <= 4096 else 0 + if values[0] == 0 or values[1] & ~0x70fffe or not 1 <= values[2] <= 5000: return 0 if any(axis > 255 for axis in values[3:]): return 0 @@ -45,14 +52,28 @@ def next_sequence(path): return sequence +def button_mask(value): + names = value.split("+") + if any(name not in BUTTONS for name in names) or ("release" in names and len(names) > 1) or len(set(names)) != len(names): + raise argparse.ArgumentTypeError(f"invalid button {value!r}: use one of {', '.join(BUTTONS)}, or several joined by '+'") + mask = 0 + for name in names: + mask |= BUTTONS[name] + return mask + + def main(): parser = argparse.ArgumentParser(description="Queue a controller pulse for APS5_PAD_INPUT_FILE.") parser.add_argument("path", type=Path) - parser.add_argument("button", choices=BUTTONS) + parser.add_argument("button", help=f"a button name ({', '.join(BUTTONS)}), or several joined by '+' to press them together (for example l1+r1); release must be used alone") parser.add_argument("--hold-ms", type=int, default=300) parser.add_argument("--left-stick", type=int, nargs=2, metavar=("X", "Y")) parser.add_argument("--right-stick", type=int, nargs=2, metavar=("X", "Y")) args = parser.parse_args() + try: + mask = button_mask(args.button) + except argparse.ArgumentTypeError as error: + parser.error(str(error)) if not 1 <= args.hold_ms <= 5000: parser.error("--hold-ms must be between 1 and 5000") sticks = "" @@ -65,11 +86,21 @@ def main(): sequence = next_sequence(args.path) except ValueError as error: parser.error(str(error)) + metadata = None + if os.name == "posix": + try: + metadata = args.path.stat() + except FileNotFoundError: + pass temporary = None try: with tempfile.NamedTemporaryFile(mode="w", dir=args.path.parent, delete=False) as output: temporary = Path(output.name) - output.write(f"{sequence} {BUTTONS[args.button]} {args.hold_ms}{sticks}\n") + if metadata is not None: + if os.geteuid() == 0: + os.fchown(output.fileno(), metadata.st_uid, metadata.st_gid) + os.fchmod(output.fileno(), stat.S_IMODE(metadata.st_mode)) + output.write(f"{sequence} {mask} {args.hold_ms}{sticks}\n") os.replace(temporary, args.path) finally: if temporary is not None: diff --git a/tools/pad_timeline.py b/tools/pad_timeline.py new file mode 100644 index 0000000000..16672d228e --- /dev/null +++ b/tools/pad_timeline.py @@ -0,0 +1,98 @@ +import argparse +import json +import os +import stat +from pathlib import Path +import tempfile + +from pad_input import BUTTONS, next_sequence + + +def integer(value, field, minimum, maximum): + if type(value) is not int or not minimum <= value <= maximum: + raise ValueError(f"{field} must be an integer between {minimum} and {maximum}") + return value + + +def compile_timeline(document, sequence, repeat=1, gap_ms=200): + integer(sequence, "sequence", 1, 2**64 - 1) + integer(repeat, "repeat", 1, 4096) + integer(gap_ms, "gap_ms", 0, 3600000) + if not isinstance(document, dict) or set(document) != {"version", "events"}: + raise ValueError("timeline must contain version and events") + if type(document["version"]) is not int or document["version"] != 1: + raise ValueError("unsupported timeline version") + source = document["events"] + if not isinstance(source, list) or not 1 <= len(source) * repeat <= 4096: + raise ValueError("timeline must contain 1 to 4096 events after repetition") + events = [] + previous_end = 0 + for index, event in enumerate(source): + prefix = f"events[{index}]" + if not isinstance(event, dict) or set(event) != {"at_ms", "hold_ms", "buttons"}: + raise ValueError(f"{prefix} must contain at_ms, hold_ms and buttons") + start = integer(event["at_ms"], f"{prefix}.at_ms", 0, 3600000) + duration = integer(event["hold_ms"], f"{prefix}.hold_ms", 1, 5000) + if start < previous_end or start + duration > 3600000: + raise ValueError(f"{prefix} overlaps an earlier event or exceeds one hour") + names = event["buttons"] + if not isinstance(names, list) or not all(isinstance(name, str) and name in BUTTONS for name in names): + raise ValueError(f"{prefix}.buttons must be a list of known button names") + if "release" in names and len(names) != 1: + raise ValueError(f"{prefix}: release cannot be combined with another button") + buttons = 0 + for name in names: + buttons |= BUTTONS[name] + events.append((start, duration, buttons)) + previous_end = start + duration + period = previous_end + gap_ms + if (repeat - 1) * period + previous_end > 3600000: + raise ValueError("repeated timeline exceeds one hour") + rows = [f"APS5_PAD_TIMELINE 1 {sequence} {len(events) * repeat}"] + for iteration in range(repeat): + rows.extend(f"{start + iteration * period} {duration} {buttons}" for start, duration, buttons in events) + return "\n".join(rows) + "\n" + + +def queue_timeline(path, text): + metadata = None + if os.name == "posix": + try: + metadata = path.stat() + except FileNotFoundError: + pass + temporary = None + try: + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=path.parent, delete=False) as output: + temporary = Path(output.name) + if metadata is not None: + if os.geteuid() == 0: + os.fchown(output.fileno(), metadata.st_uid, metadata.st_gid) + os.fchmod(output.fileno(), stat.S_IMODE(metadata.st_mode)) + output.write(text) + os.replace(temporary, path) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + + +def main(): + parser = argparse.ArgumentParser(description="Queue a timed controller button sequence for APS5_PAD_INPUT_FILE.") + parser.add_argument("timeline", type=Path) + parser.add_argument("path", type=Path) + parser.add_argument("--repeat", type=int, default=1) + parser.add_argument("--gap-ms", type=int, default=200) + args = parser.parse_args() + try: + if args.timeline.resolve() == args.path.resolve() or (args.path.exists() and os.path.samefile(args.timeline, args.path)): + raise ValueError("the command file must be separate from the timeline JSON") + document = json.loads(args.timeline.read_text(encoding="utf-8")) + text = compile_timeline(document, next_sequence(args.path), args.repeat, args.gap_ms) + queue_timeline(args.path, text) + except (OSError, ValueError) as error: + parser.exit(2, f"pad_timeline: {error}\n") + print(f"Queued {len(text.splitlines()) - 1} timed events in {args.path}") + + +if __name__ == "__main__": + main() diff --git a/tools/performance_compare.py b/tools/performance_compare.py new file mode 100644 index 0000000000..6a6c599351 --- /dev/null +++ b/tools/performance_compare.py @@ -0,0 +1,191 @@ +import argparse +import json +import math +import sys +from pathlib import Path + + +CONTEXT = ("gpu", "driver", "os", "resolution") +OPTIONAL_STRINGS = ("cpu", "title_id", "title", "content_version") +OPTIONAL_INTEGERS = ("cpu_threads", "memory_mib") +PERCENTILES = ("p50", "p90", "p99", "max") + + +def number(value, name): + try: + valid = type(value) in (int, float) and math.isfinite(value) and value >= 0 + except OverflowError: + valid = False + if not valid: + raise ValueError(f"{name} must be a finite, non-negative number") + return value + + +def validate(summary): + for key in ("commit", "trace", *CONTEXT): + if not isinstance(summary.get(key), str): + raise ValueError(f"{key} must be a string") + for key in OPTIONAL_STRINGS: + if key in summary and not isinstance(summary[key], str): + raise ValueError(f"{key} must be a string") + for key in OPTIONAL_INTEGERS: + if key in summary and (type(summary[key]) is not int or summary[key] < 0): + raise ValueError(f"{key} must be a non-negative integer") + if type(summary.get("trace_dropped")) is not bool: + raise ValueError("trace_dropped must be a boolean") + if "dirty" not in summary or (summary["dirty"] is not None and type(summary["dirty"]) is not bool): + raise ValueError("dirty must be a boolean or null") + env = summary.get("env") + if not isinstance(env, dict) or any(not isinstance(v, str) for v in env.values()): + raise ValueError("env must map names to strings") + window = summary.get("window") + if not isinstance(window, dict) or not isinstance(window.get("spec"), str): + raise ValueError("window must contain a string spec") + if number(window.get("ms"), "window.ms") == 0: + raise ValueError("window.ms must be positive") + if type(window.get("first_present")) is not int or window["first_present"] < 0: + raise ValueError("window.first_present must be a non-negative integer") + if type(summary.get("presents")) is not int or summary["presents"] <= 0: + raise ValueError("presents must be a positive integer") + number(summary["presents"], "presents") + frames = summary.get("frame_ms") + if not isinstance(frames, dict): + raise ValueError("frame_ms must be an object") + values = [number(frames.get(key), f"frame_ms.{key}") for key in PERCENTILES] + if values != sorted(values): + raise ValueError("frame_ms must satisfy p50 <= p90 <= p99 <= max") + number(summary.get("gpu_busy_ms_per_present"), "gpu_busy_ms_per_present") + phases = summary.get("top_phases_ms_per_frame") + if not isinstance(phases, dict): + raise ValueError("top_phases_ms_per_frame must be an object") + for name, value in phases.items(): + number(value, f"top_phases_ms_per_frame.{name}") + return summary + + +def load_summary(path, trace=None): + records = [] + with Path(path).open(encoding="utf-8-sig") as source: + for line_number, line in enumerate(source, 1): + if not line.strip(): + continue + try: + record = json.loads(line) + if not isinstance(record, dict): + raise ValueError("summary must be an object") + except ValueError as error: + raise ValueError(f"{path}:{line_number}: {error}") from error + if trace is None or record.get("trace") == trace: + records.append((line_number, record)) + if len(records) != 1: + selection = f" matching trace {trace!r}" if trace is not None else "" + raise ValueError(f"{path}: expected one summary{selection}, found {len(records)}; " + "select a unique trace with --baseline-trace/--candidate-trace " + "or provide a file containing one summary line") + line_number, record = records[0] + try: + return validate(record) + except ValueError as error: + raise ValueError(f"{path}:{line_number}: {error}") from error + + +def delta(baseline, candidate): + change = candidate - baseline if baseline is not None and candidate is not None else None + percent = change / baseline * 100 if change is not None and baseline else None + if change is not None and not math.isfinite(change): + raise ValueError("metric difference is outside the finite numeric range") + if percent is not None and not math.isfinite(percent): + raise ValueError("metric percentage change is outside the finite numeric range") + return {"baseline": baseline, "candidate": candidate, "change": change, "change_pct": percent} + + +def compare(baseline, candidate): + metrics = {f"frame_ms.{key}": delta(baseline["frame_ms"][key], candidate["frame_ms"][key]) + for key in PERCENTILES} + metrics["mean_frame_ms"] = delta(baseline["window"]["ms"] / baseline["presents"], + candidate["window"]["ms"] / candidate["presents"]) + metrics["gpu_busy_ms_per_present"] = delta(baseline["gpu_busy_ms_per_present"], candidate["gpu_busy_ms_per_present"]) + before_phases = baseline["top_phases_ms_per_frame"] + after_phases = candidate["top_phases_ms_per_frame"] + phases = {name: delta(before_phases.get(name), after_phases.get(name)) + for name in sorted(before_phases.keys() | after_phases.keys())} + contexts = [] + for summary in (baseline, candidate): + context = {key: summary[key] for key in CONTEXT} + context.update({key: summary[key] for key in (*OPTIONAL_STRINGS, *OPTIONAL_INTEGERS) if key in summary}) + context.update({f"window.{key}": summary["window"][key] for key in ("spec", "first_present")}) + context.update({f"env.{key}": value for key, value in summary["env"].items()}) + contexts.append(context) + differences = {key: {"baseline": contexts[0].get(key), "candidate": contexts[1].get(key)} + for key in sorted(contexts[0].keys() | contexts[1].keys()) + if contexts[0].get(key) != contexts[1].get(key)} + notes = ["Compare captures from the same machine, title, content version and scene; matching recorded metadata cannot verify the physical machine or scene.", + "Changes are candidate minus baseline. A negative frame-time change means a shorter frame interval.", + "A missing top phase is unreported, not zero; its change is unavailable."] + if differences: + notes.append("Recorded configuration differs; these measurements do not isolate the code change.") + for label, summary in (("baseline", baseline), ("candidate", candidate)): + if summary["trace_dropped"]: + notes.append(f"{label}: trace events were dropped; the event trace omits some of the work included in phase totals.") + if summary["dirty"] is not False: + notes.append(f"{label}: source tree is dirty or its state is unknown; the commit alone does not identify the build.") + missing = [key for key in (*CONTEXT, *OPTIONAL_STRINGS, *OPTIONAL_INTEGERS) if not summary.get(key)] + if missing: + notes.append(f"{label}: missing configuration: {', '.join(missing)}.") + return {"baseline": baseline, "candidate": candidate, "configuration_differences": differences, + "metrics_ms": metrics, "top_phases_ms_per_frame": phases, "notes": notes} + + +def cell(value): + text = json.dumps(value, ensure_ascii=False) if not isinstance(value, str) else value + return text.replace("&", "&").replace("<", "<").replace(">", ">").replace("|", "|").replace("\r", " ").replace("\n", " ") + + +def markdown(report): + lines = ["# Performance comparison", "", "| Capture | Commit | Trace | Presents | Window (ms) |", + "| --- | --- | --- | ---: | ---: |"] + for label in ("baseline", "candidate"): + summary = report[label] + lines.append(f"| {label} | {cell(summary['commit'])} | {cell(summary['trace'])} | " + f"{summary['presents']} | {summary['window']['ms']:.3f} |") + lines.extend(["", *[f"- {note}" for note in report["notes"]]]) + if report["configuration_differences"]: + lines.extend(["", "## Configuration differences", "", "| Setting | Baseline | Candidate |", "| --- | --- | --- |"]) + for key, values in report["configuration_differences"].items(): + lines.append(f"| {cell(key)} | {cell(values['baseline'])} | {cell(values['candidate'])} |") + for title, key in (("Frame and GPU times (ms)", "metrics_ms"), ("Top phases (ms per frame)", "top_phases_ms_per_frame")): + lines.extend(["", f"## {title}", "", "| Metric | Baseline | Candidate | Change | Change (%) |", + "| --- | ---: | ---: | ---: | ---: |"]) + for name, values in report[key].items(): + columns = ["n/a" if values[field] is None else format(values[field], "+.3f" if field.startswith("change") else ".3f") + for field in ("baseline", "candidate", "change", "change_pct")] + lines.append(f"| {cell(name)} | {' | '.join(columns)} |") + return "\n".join(lines) + "\n" + + +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="backslashreplace") + parser = argparse.ArgumentParser(description="Compare two aps5-summary.jsonl captures. " + "Reports numerical differences, not statistical significance or an automatic pass/fail verdict.") + parser.add_argument("baseline", type=Path, help="Baseline summary JSONL file") + parser.add_argument("candidate", type=Path, help="Candidate summary JSONL file") + parser.add_argument("--require-matching-context", action="store_true", + help="reject differences in recorded configuration, including optional machine/title metadata; scene identity is not checked") + parser.add_argument("--baseline-trace", help="Exact trace field to select in the baseline file") + parser.add_argument("--candidate-trace", help="Exact trace field to select in the candidate file") + parser.add_argument("--json", action="store_true", help="Write JSON instead of Markdown") + args = parser.parse_args(argv) + try: + report = compare(load_summary(args.baseline, args.baseline_trace), load_summary(args.candidate, args.candidate_trace)) + if args.require_matching_context and report["configuration_differences"]: + raise ValueError("recorded configuration differs: " + ", ".join(report["configuration_differences"])) + output = json.dumps(report, indent=2, ensure_ascii=False, allow_nan=False) + "\n" if args.json else markdown(report) + except (OSError, ValueError, OverflowError) as error: + parser.exit(2, f"{parser.prog}: {error}\n") + sys.stdout.write(output) + + +if __name__ == "__main__": + main() diff --git a/tools/pr_labels.py b/tools/pr_labels.py new file mode 100644 index 0000000000..f6f0039090 --- /dev/null +++ b/tools/pr_labels.py @@ -0,0 +1,162 @@ +import argparse +import json +import os +import re +import subprocess +import sys +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import performance_compare + +UNBLOCKS = re.compile(r'Unblocks:[ \t]*([^\r\n]*(?:\r?\n[ \t]*[-*][ \t]+(?!\[[ xX]\])[^\r\n]*)*)') +TITLE_ID = re.compile(r"(?<![A-Za-z0-9])[A-Z]{4}\d{5}(?!\d)") +NONE = re.compile(r"[-*_ \t\r\n]*none\b", re.I) +PERF = re.compile(r"perf(\(|!?:)") +FENCE = re.compile(r"(?:```|~~~)[^\r\n]*\r?\n(.*?)(?:```|~~~)", re.S) +MARKER = "<!-- pr-labels-perf -->" +UNBLOCKS_LABEL = "unblocks title" +PERF_LABEL = "perf" +MISSING_LABEL = "perf numbers missing" +MANAGED = {UNBLOCKS_LABEL, PERF_LABEL, MISSING_LABEL} +WRITE_PAUSE = 1.0 +COMMENT = (f"{MARKER}\nThis pull request is labeled `perf`, so it needs the summary line of a capture on `main` and one with the change, " + "from the same title, scene and machine, as the Performance report form asks. " + "Please paste both `aps5-summary.jsonl` lines, attach the two traces and add the output of `tools/trace_digest.py`.") + + +def gh(*args): + return subprocess.run(["gh", *args], capture_output=True, text=True, encoding="utf-8", check=True).stdout + + +def repository(): + return os.environ.get("GITHUB_REPOSITORY") or gh("repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner").strip() + + +def strip_comments(body): + return re.sub(r"<!--.*?-->", "", body or "", flags=re.S) + + +def title_ids(body): + match = UNBLOCKS.search(strip_comments(body)) + return set(TITLE_ID.findall(match.group(1))) if match and not NONE.match(match.group(1)) else set() + + +def is_perf(title): + return bool(PERF.match(title or "")) + + +def has_numbers(body): + body = strip_comments(body) + candidates = [line.strip() for line in body.splitlines()] + [block.strip() for block in FENCE.findall(body)] + commits = set() + for text in candidates: + if not text.startswith("{"): + continue + try: + summary = json.loads(text) + if isinstance(summary, dict): + commits.add(performance_compare.validate(summary)["commit"]) + except (ValueError, RecursionError): + continue + return len(commits) >= 2 + + +def labels_for(pr): + wanted = set() + if title_ids(pr["body"]): + wanted.add(UNBLOCKS_LABEL) + if is_perf(pr["title"]): + wanted.add(PERF_LABEL) + if not has_numbers(pr["body"]): + wanted.add(MISSING_LABEL) + current = set(pr["labels"]) + return wanted - current, (MANAGED & current) - wanted + + +def decode_pages(out): + decoder = json.JSONDecoder() + offset = 0 + items = [] + while offset < len(out): + while offset < len(out) and out[offset].isspace(): + offset += 1 + if offset == len(out): + break + page, offset = decoder.raw_decode(out, offset) + if not isinstance(page, list): + raise ValueError("gh api --paginate returned a non-array page") + items.extend(page) + return items + + +def pull(data): + return {"number": data["number"], "title": data["title"], "body": data["body"] or "", + "labels": [label["name"] for label in data["labels"]], "state": data["state"]} + + +def commented(repo, number): + out = gh("api", "--paginate", f"repos/{repo}/issues/{number}/comments", + "--jq", f'[.[] | select(.body | contains("{MARKER}"))] | length') + return sum(int(count) for count in out.split()) + + +def process(repo, pr, dry_run, comment=True): + if pr["state"] != "open": + return set(), set(), False + add, remove = labels_for(pr) + wants_comment = comment and (MISSING_LABEL in add or (MISSING_LABEL in pr["labels"] and MISSING_LABEL not in remove)) + post = wants_comment and not commented(repo, pr["number"]) + if not dry_run: + if add or remove: + args = ["pr", "edit", str(pr["number"]), "-R", repo] + if add: + args += ["--add-label", ",".join(sorted(add))] + if remove: + args += ["--remove-label", ",".join(sorted(remove))] + gh(*args) + if post: + gh("api", "-X", "POST", f"repos/{repo}/issues/{pr['number']}/comments", "-f", f"body={COMMENT}") + return add, remove, post + + +def main(argv=None): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") + parser = argparse.ArgumentParser(description="Label pull requests from their title and template fields.") + which = parser.add_mutually_exclusive_group(required=True) + which.add_argument("--pr", type=int, help="label one pull request") + which.add_argument("--all", action="store_true", help="label every open pull request") + parser.add_argument("--dry-run", action="store_true", help="print the planned changes and write nothing") + args = parser.parse_args(argv) + try: + repo = repository() + if args.pr is not None: + prs = [pull(json.loads(gh("api", f"repos/{repo}/pulls/{args.pr}")))] + else: + prs = [pull(data) for data in decode_pages(gh("api", "--paginate", f"repos/{repo}/pulls?state=open&base=main&per_page=100"))] + except subprocess.CalledProcessError as error: + parser.error((error.stderr or str(error)).strip()) + failed = [] + for pr in prs: + try: + add, remove, post = process(repo, pr, args.dry_run, comment=args.pr is not None) + except subprocess.CalledProcessError as error: + failed.append(pr["number"]) + print(f"cannot label #{pr['number']}: {(error.stderr or '').strip()}", file=sys.stderr) + continue + if add or remove or post: + plan = [f"+{label}" for label in sorted(add)] + [f"-{label}" for label in sorted(remove)] + (["comment"] if post else []) + print(f"{'would change' if args.dry_run else 'changed'} #{pr['number']}: {' '.join(plan)}") + if args.all and not args.dry_run: + time.sleep(WRITE_PAUSE) + if failed: + sys.exit(f"{len(failed)} pull requests not labeled: {', '.join(f'#{number}' for number in failed)}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 4a1a4b4720..29008a4d9d 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -3,12 +3,17 @@ import json import os import re import subprocess +import sys +from concurrent.futures import ThreadPoolExecutor from itertools import combinations from pathlib import Path -from urllib.parse import urlencode +from urllib.parse import quote, urlencode + +from nid_names import compute_nid, source_aliases PARENT = 'if .parent then "\\(.parent.owner.login)/\\(.parent.name)" else .nameWithOwner end' EXPORT = re.compile(r"^\+.*\bAPS5_VABI\s+(\w+)\s*\(") +NID_DISAMBIG = re.compile(r"_nid_disambig[0-9]+$") DEPENDS = re.compile(r'Depends on:[ \t]*((?:#\d+\b(?:(?:[ \t]*,[ \t]*(?:and[ \t]+)?|[ \t]+and[ \t]+)#\d+\b)*|none\b)?(?:\r?\n[ \t]*[-*][ \t]*#\d+\b[^\r\n]*)*)') PLAIN_DEPENDS = re.compile(r'Depends on[ \t]+(#\d+\b(?:(?:[ \t]*,[ \t]*(?:and[ \t]+)?|[ \t]+and[ \t]+)#\d+\b)*)') HUNK = re.compile(r"^@@ -(\d+)(?:,(\d+))?", re.M) @@ -17,12 +22,16 @@ FETCH_REFS = 100 def git(*args): - return subprocess.run(["git", *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): @@ -35,12 +44,15 @@ def upstream(): def depends_on(body): body = re.sub(r"<!--.*?-->", "", body or "", flags=re.S) - match = DEPENDS.search(body) - if not match or not match.group(1).strip(): - match = PLAIN_DEPENDS.search(body) + repo = re.escape(globals().get("REPO", "boykopovar/AnyPS5")) + repo_pattern = rf"(?:{repo}|boykopovar/AnyPS5)" + body = re.sub(rf"\[([^\]]*)\]\(https?://github\.com/{repo_pattern}/pull/(\d+)(?:[#/?][^)\s]*)?\)", r"#\2", body, flags=re.IGNORECASE) + body = re.sub(rf"\[([^\]]*)\]\(https?://github\.com/(?!{repo_pattern}/)[^/\s]+/[^/\s]+/pull/\d+[^)\s]*\)", "", body, flags=re.IGNORECASE) + body = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", body) + body = re.sub(rf"https?://github\.com/{repo_pattern}/pull/(\d+)\b(?:/[^\s]*)?", r"#\1", body, flags=re.IGNORECASE) + match = next((match for match in DEPENDS.finditer(body) if match.group(1).strip()), None) or PLAIN_DEPENDS.search(body) return {int(n) for n in re.findall(r"#(\d+)", match.group(1))} if match else set() - def open_prs(base): query = urlencode({"state": "open", "base": base, "per_page": 100}) out = gh("api", "--paginate", f"repos/{REPO}/pulls?{query}") @@ -79,11 +91,12 @@ def fetch(base, prs): def merge(*args): - result = subprocess.run(["git", "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() @@ -106,27 +119,53 @@ def exports(diff): text += nxt[1:] semi, brace = text.find(";"), text.find("{") if semi < 0 or 0 <= brace < semi: - found.add(m.group(1)) + name = m.group(1).removesuffix("_nid_postfix") + found.add(NID_DISAMBIG.sub("", name)) return found +def aliases(diff): + added = "\n".join(line[1:] for line in diff.splitlines() if line.startswith("+") and not line.startswith("+++")) + return {nid for nid, _ in source_aliases(added)} + + +def export_ids(pr): + return {compute_nid(name) for name in pr["exports"]} | pr.get("aliases", set()) + + +def same_exports(pa, pb, shared): + common = (export_ids(pa) & export_ids(pb)).difference(*(export_ids(pr) for pr in shared)) + named = {name for name in pa["exports"] | pb["exports"] if compute_nid(name) in common} + return named | common.difference(compute_nid(name) for name in named) + + def scan(pr): tree, conflicted = merge("refs/pr/base", pr["ref"]) 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) diff = git("diff", "-U0", "--no-color", old, new, "--", "core/libs/prx/*.cpp") pr["exports"] = exports(diff) + pr["aliases"] = aliases(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) - return [(int(start), int(start) + max(int(count or 1), 1) - 1) for start, count in HUNK.findall(diff)] + found = [] + for start, count in HUNK.findall(diff): + start = max(int(start), 1) + found.append((start, start + max(int(count or 1), 1) - 1)) + return found def clashes(a, b, path): @@ -167,7 +206,11 @@ def overlaps(prs, a, b): found = {"added": (pa["added"] & pb["added"]).difference(*(pr["added"] for pr in shared))} if pa["author"] != pb["author"]: found["conflicts"] = conflicts(pa, pb) - found["exports"] = (pa["exports"] & pb["exports"]).difference(*(pr["exports"] for pr in shared)) + found["exports"] = same_exports(pa, pb, shared) + if not (pa["tree"] and pb["tree"]): + uncompared = pa.get("files", set()) & pb.get("files", set()) + if uncompared: + found["uncompared"] = uncompared return found if any(found.values()) else None @@ -176,21 +219,43 @@ def cell(items): def spans(conflicts, blob): - return "<br>".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 "<br>".join(f"`{path}` " + ", ".join(f"[L{s}-{e}]({blob}/{quote(path)}#L{s}-L{e})" if s != e else f"[L{s}]({blob}/{quote(path)}#L{s})" for s, e in ranges) for path, ranges in sorted((conflicts or {}).items())) +def pull(blob, number): + return f'[PR {number}]({blob.split("/blob/", 1)[0].replace("https://github.com/", "https://www.github.com/", 1)}/pull/{number})' + + def report(rows, blob): if not rows: return "" - lines = [MARKER, - "Other open pull requests touch the same code. Merging one will break the other, " - "or both implement the same thing. If this one builds on them, list them in **Depends on**.", - "", - "| Pull request | Conflicting files (lines on `main`) | Same exports | Same new files |", - "|---|---|---|---|"] - for number, found in sorted(rows.items()): - lines.append(f'| #{number} | {spans(found.get("conflicts"), blob)} | {cell(found.get("exports"))} | {cell(found["added"])} |') + confirmed = {number: found for number, found in rows.items() + if found.get("conflicts") or found.get("exports") or found["added"]} + uncompared = {number: found["uncompared"] for number, found in rows.items() if found.get("uncompared")} + lines = [MARKER] + if confirmed: + lines.extend([ + "Other open pull requests touch the same code. Merging one will break the other, " + "or both implement the same thing. If this one builds on them, list them in **Depends on**.", + "", + "| Pull request | Conflicting files (lines on `main`) | Same exports | Same new files |", + "|---|---|---|---|", + ]) + for number, found in sorted(confirmed.items()): + lines.append(f'| {pull(blob, number)} | {spans(found.get("conflicts"), blob)} | {cell(found.get("exports"))} | {cell(found["added"])} |') + if uncompared: + if confirmed: + lines.append("") + lines.extend([ + "Some comparisons could not be completed because a pull request conflicts with the base branch. " + "Review these shared files manually before concluding that there is no overlap.", + "", + "| Pull request | Shared files not compared |", + "|---|---|", + ]) + for number, files in sorted(uncompared.items()): + lines.append(f"| {pull(blob, number)} | {cell(files)} |") return "\n".join(lines) + "\n" @@ -200,31 +265,55 @@ def write_reports(out, rows, blob): (out / f"{number}.md").write_text(report(found, blob), encoding="utf-8") -if __name__ == "__main__": +def describe_failure(error): + command = " ".join(str(part) for part in error.cmd) + message = (error.stderr or "").strip() or "no error output" + return f"pr_overlap: {command} failed with exit code {error.returncode}: {message}" + + +def main(argv=None): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="report open pull requests that overlap each other") parser.add_argument("--base", default="main") parser.add_argument("--out", type=Path, help="write <number>.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") - args = parser.parse_args() + parser.add_argument("--jobs", type=int, default=4, help="number of pull request comparisons to run at once") + args = parser.parse_args(argv) if bool(args.out) == bool(args.branch): parser.error("pass exactly one of --out or --branch") - REPO = args.repo or upstream() - prs = open_prs(args.base) - fetch(args.base, prs) - if args.branch: - sha = git("rev-parse", args.branch).strip() - prs = own(prs, sha) - prs[0] = {"ref": sha, "author": None, "depends": set()} - for pr in prs.values(): - scan(pr) - rows = {n: {} for n in prs} - pairs = [(0, n) for n in prs if n] if args.branch else combinations(prs, 2) - for a, b in pairs: - if found := overlaps(prs, a, b): - rows[a][b] = rows[b][a] = found - blob = f'https://github.com/{REPO}/blob/{git("rev-parse", "refs/pr/base").strip()}' + if args.jobs < 1: + parser.error("--jobs must be at least 1") + global REPO + if args.out: + args.out = args.out.resolve() + try: + os.chdir(git("rev-parse", "--show-toplevel").strip()) + REPO = args.repo or upstream() + prs = open_prs(args.base) + fetch(args.base, prs) + if args.branch: + sha = git("rev-parse", args.branch).strip() + prs = own(prs, sha) + prs[0] = {"ref": sha, "author": None, "depends": set()} + scan_all(prs, args.jobs) + rows = {n: {} for n in prs} + pairs = [(0, n) for n in prs if n] if args.branch else combinations(prs, 2) + for a, b in pairs: + if found := overlaps(prs, a, b): + rows[a][b] = rows[b][a] = found + blob = f'https://github.com/{REPO}/blob/{git("rev-parse", "refs/pr/base").strip()}' + except subprocess.CalledProcessError as error: + raise SystemExit(describe_failure(error)) if args.branch: print(report(rows[0], blob) or "No overlap with open pull requests.", end="") - raise SystemExit + return 0 write_reports(args.out, rows, blob) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/prepare_release_notes.py b/tools/prepare_release_notes.py index ed4c2e5562..0163999059 100644 --- a/tools/prepare_release_notes.py +++ b/tools/prepare_release_notes.py @@ -1,6 +1,8 @@ import argparse import json +import os import re +import stat from pathlib import Path BODY_LIMIT = 125000 @@ -19,12 +21,23 @@ 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") + for destination in (body, full): + if destination.is_dir(): + raise IsADirectoryError(str(destination)) + if os.name == "nt" and destination.exists() and destination.stat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY: + raise PermissionError(f"Release note destination is read-only: {destination}") + body.parent.mkdir(parents=True, exist_ok=True) full.parent.mkdir(parents=True, exist_ok=True) - full.write_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 cc18c35023..944ba4170b 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -20,6 +20,8 @@ SOURCE = f'https://github.com/{os.environ.get("GITHUB_REPOSITORY", "boykopovar/A DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?(?:try\s*)?\{") STUB = "NotImplemented_nid_no_patch" CALL = re.compile(r"\b(\w+)\s*\(") +KEYWORDS = r"return|else|do|throw|co_return|co_await|co_yield|and|or|not|xor|bitand|bitor|compl" +QUALIFIED = re.compile(rf"(?:\.|->|(?:\b(?!(?:{KEYWORDS})\b)\w+|>)\s*::)\s*$") CMAKE_SHARED = re.compile(r'^\s*include\(\s*"?\$\{CMAKE_CURRENT_SOURCE_DIR\}/\.\./([\w.-]+)/[\w.-]+\.cmake"?\s*\)', re.M) SOURCE_SHARED = re.compile(r'^\s*#\s*include\s*"prx/([\w.-]+)/[\w./-]+\.cpp"', re.M) STUB_WRAPPER = re.compile( @@ -101,12 +103,16 @@ def body_end(text, start): def stub_calls(text): - calls = {STUB} + helpers = {} for match in STUB_WRAPPER.finditer(text): body = text[match.end() - 1:body_end(text, match.end() - 1)] - if any(call.group(1) == STUB for call in CALL.finditer(body)): - calls.add(match.group(1)) - return calls + helpers.setdefault(match.group(1), set()).update(call.group(1) for call in CALL.finditer(body)) + calls = {STUB} + while True: + found = {name for name, targets in helpers.items() if name not in calls and targets & calls} + if not found: + return calls + calls |= found def statements(body): @@ -159,7 +165,7 @@ def scan_library(path): for source in path.rglob("*.cpp"): if "tests" in source.relative_to(path).parts: continue - text = code(source.read_text(errors="ignore")) + text = code(source.read_text(encoding="utf-8", errors="ignore")) calls = stub_calls(text) wrappers = inert_wrappers(text) for match in DEFINITION.finditer(text): @@ -167,7 +173,8 @@ def scan_library(path): if name.endswith(("_nid_no_patch", "_nid_no_patch_cut")): continue body = text[match.end() - 1:body_end(text, match.end() - 1)] - if any(call.group(1) in calls for call in CALL.finditer(body)): + if any(call.group(1) in calls and not QUALIFIED.search(body, 0, call.start()) + for call in CALL.finditer(body)): todo.add(name) continue done.add(name) @@ -179,7 +186,7 @@ def scan_library(path): if not (done or todo): cmake = path / "CMakeLists.txt" texts = [(CMAKE_SHARED, cmake.read_text(encoding="utf-8", errors="ignore"))] if cmake.is_file() else [] - texts += [(SOURCE_SHARED, source.read_text(errors="ignore")) for source in sorted(path.glob("*.cpp"))] + texts += [(SOURCE_SHARED, source.read_text(encoding="utf-8", errors="ignore")) for source in sorted(path.glob("*.cpp"))] owners = [m[1] for pattern, content in texts for m in pattern.finditer(content)] owners = [o for o in owners if o != path.name and (path.parent / o).is_dir()] if owners: @@ -197,6 +204,21 @@ def collect_libraries(): return summarize([scan_library(p) for p in sorted(PRX.iterdir()) if p.is_dir()]) +def select_libraries(libraries, modules): + if not modules: + return libraries + known = {group["name"] for group in libraries["groups"]} + missing = [] + for name in modules: + if name not in known and name not in missing: + missing.append(name) + if missing: + label = "library" if len(missing) == 1 else "libraries" + raise ValueError(f"unknown {label}: {', '.join(missing)}") + chosen = set(modules) + return summarize([group for group in libraries["groups"] if group["name"] in chosen]) + + def camel(name): return "".join(part.capitalize() for part in name.split("_")) @@ -299,7 +321,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'<rect x="{x:.2f}" y="{y:.2f}" width="{w:.2f}" height="{h:.2f}" fill="none" stroke="{BORDER}" stroke-width="2"/>') - 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("</g>") @@ -424,31 +447,97 @@ def report(base, head): return "\n".join(lines) -if __name__ == "__main__": - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8") +def write_site(output, libraries, shaders): + output.mkdir(parents=True, exist_ok=True) + (output / "progress.json").write_text(json.dumps({"libraries": libraries, "shaders": shaders}, indent=2), encoding="utf-8") + (output / "badge-libraries.svg").write_text(badge("libraries*", libraries), encoding="utf-8") + (output / "badge-shaders.svg").write_text(badge("shaders", shaders), encoding="utf-8") + (output / "progress.svg").write_text(render(libraries, shaders), encoding="utf-8") + (output / "progress.html").write_text(summary(libraries, shaders), encoding="utf-8") + + +def validate_snapshot(snapshot): + if not isinstance(snapshot, dict): + raise ValueError("snapshot must be an object") + for section in ("libraries", "shaders"): + data = snapshot.get(section) + if not isinstance(data, dict): + raise ValueError(f"{section} must be an object") + groups = data.get("groups") + if not isinstance(groups, list): + raise ValueError(f"{section}.groups must be a list") + percent = data.get("percent") + if isinstance(percent, bool) or not isinstance(percent, (int, float)) or not 0 <= percent <= 100: + raise ValueError(f"{section}.percent must be a finite number between 0 and 100") + for index, group in enumerate(groups): + location = f"{section}.groups[{index}]" + if not isinstance(group, dict) or not isinstance(group.get("name"), str): + raise ValueError(f"{location} must be an object with a string name") + if "shared_sources" in group and not isinstance(group["shared_sources"], str): + raise ValueError(f"{location}.shared_sources must be a string") + for field in ("done_names", "todo_names", "inert_names"): + values = group.get(field, []) + if not isinstance(values, list) or any(not isinstance(value, str) for value in values): + raise ValueError(f"{location}.{field} must be a list of strings") + + +def main(argv=None): + global PRX, OPCODES + previous = PRX, OPCODES parser = argparse.ArgumentParser() parser.add_argument("output", type=Path, nargs="?") parser.add_argument("--root", type=Path, help="source tree to measure instead of the one containing this script") parser.add_argument("--compare", type=Path, nargs=2, metavar=("BASE", "HEAD"), help="print a markdown report of the changes between two progress.json files") - args = parser.parse_args() - if args.compare: - base, head = (json.loads(path.read_text(encoding="utf-8")) for path in args.compare) - print(report(base, head), end="") - raise SystemExit - if not args.output: - parser.error("the output directory is required") - if args.root: - PRX = args.root / "core" / "libs" / "prx" - OPCODES = args.root / OPCODES.relative_to(ROOT) - output = args.output - output.mkdir(parents=True, exist_ok=True) - libraries, shaders = collect_libraries(), collect_shaders() - (output / "progress.json").write_text(json.dumps({"libraries": libraries, "shaders": shaders}, indent=2)) - (output / "badge-libraries.svg").write_text(badge("libraries*", libraries)) - (output / "badge-shaders.svg").write_text(badge("shaders", shaders)) - (output / "progress.svg").write_text(render(libraries, shaders)) - (output / "progress.html").write_text(summary(libraries, shaders)) - print(f'libraries {libraries["done"]}/{libraries["total"]} ({libraries["percent"]}%)') - print(f'shaders {shaders["done"]}/{shaders["total"]} ({shaders["percent"]}%)') + parser.add_argument("--module", action="append", metavar="NAME", + help="limit library counts to these core/libs/prx directories; repeat to include several. Shader counts stay complete") + parser.add_argument("--json", action="store_true", + help="print the progress.json object to stdout instead of the summary lines") + try: + args = parser.parse_args(argv) + if args.compare: + if args.module or args.json: + parser.error("--compare does not combine with --module or --json") + snapshots = [] + for path in args.compare: + try: + snapshots.append(json.loads(path.read_text(encoding="utf-8"))) + except (OSError, ValueError) as error: + parser.error(f"{path}: {error}") + for path, snapshot in zip(args.compare, snapshots): + try: + validate_snapshot(snapshot) + except ValueError as error: + parser.error(f"{path}: {error}") + base, head = snapshots + print(report(base, head), end="") + return 0 + if not args.output and not (args.module or args.json): + parser.error("the output directory is required") + if args.root: + PRX = args.root / "core" / "libs" / "prx" + OPCODES = args.root / previous[1].relative_to(ROOT) + try: + libraries, shaders = collect_libraries(), collect_shaders() + except FileNotFoundError as error: + parser.error(f"{args.root or ROOT} is not a source tree: {error.filename} not found") + try: + libraries = select_libraries(libraries, args.module or []) + except ValueError as error: + parser.error(str(error)) + if args.output: + write_site(args.output, libraries, shaders) + if args.json: + print(json.dumps({"libraries": libraries, "shaders": shaders}, indent=2)) + else: + print(f'libraries {libraries["done"]}/{libraries["total"]} ({libraries["percent"]}%)') + print(f'shaders {shaders["done"]}/{shaders["total"]} ({shaders["percent"]}%)') + return 0 + finally: + PRX, OPCODES = previous + + +if __name__ == "__main__": + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") + raise SystemExit(main()) diff --git a/tools/progress_history.py b/tools/progress_history.py index 63159c40a5..b223ab7c58 100644 --- a/tools/progress_history.py +++ b/tools/progress_history.py @@ -1,20 +1,23 @@ import argparse +import hashlib import io import json +import os +import stat +import sys import subprocess import tarfile import tempfile -from datetime import date +from datetime import date, datetime, timezone from html import escape from pathlib import Path import progress -SERIES = (("libraries", "System libraries", "#2f81f7"), ("shaders", "GPU shader instructions", "#2ea043")) -SHORT = {"libraries": "Libraries", "shaders": "Shaders"} -WIDTH, HEIGHT, LEFT, RIGHT, TOP, BOTTOM = 900, 380, 50, 130, 20, 60 +SERIES = (("libraries", "System library functions", "#2f81f7"), ("shaders", "GPU shader instructions", "#2ea043")) +WIDTH, LEFT, RIGHT, TITLE, PANEL, GAP, AXIS = 900, 56, 190, 34, 150, 16, 32 MAX_DOTS = 45 -BACKGROUND, GRID, TEXT = "#0d1117", "#30363d", "#c9d1d9" +BACKGROUND, GRID, TEXT, MUTED = "#0d1117", "#30363d", "#c9d1d9", "#8b949e" def git(*args): @@ -23,9 +26,9 @@ def git(*args): def daily_commits(ref): seen = {} - for line in git("log", "--first-parent", "--format=%H %cs", ref).decode().splitlines(): - sha, day = line.split() - seen.setdefault(day, sha) + for line in git("log", "--first-parent", "--format=%H %ct", ref).decode("utf-8").splitlines(): + sha, timestamp = line.split() + seen.setdefault(datetime.fromtimestamp(int(timestamp), timezone.utc).date().isoformat(), sha) return sorted(seen.items()) @@ -34,7 +37,11 @@ def measure(sha): with tempfile.TemporaryDirectory() as directory: root = Path(directory) try: - tarfile.open(fileobj=io.BytesIO(git("archive", sha, "--", *paths))).extractall(root, filter="data") + archive = tarfile.open(fileobj=io.BytesIO(git("archive", sha, "--", *paths))) + if hasattr(tarfile, "data_filter"): + archive.extractall(root, filter="data") + else: + archive.extractall(root) except subprocess.CalledProcessError: return None saved = progress.PRX, progress.OPCODES @@ -49,53 +56,140 @@ def measure(sha): for name, data in (("libraries", libraries), ("shaders", shaders))} -def collect(ref): - points = [] +def cache_signature(): + digest = hashlib.sha256(repr(sys.version_info[:3]).encode("ascii")) + for path in (Path(__file__), Path(progress.__file__), Path(progress.tokenizer_spec.origin), progress.ISA): + content = path.read_bytes() + digest.update(len(content).to_bytes(8, "little")) + digest.update(content) + digest.update(repr((progress.PRX.relative_to(progress.ROOT).as_posix(), + progress.OPCODES.relative_to(progress.ROOT).as_posix())).encode("utf-8")) + return digest.hexdigest() + + +def load_cache(path, signature): + if not path.exists(): + return {} + data = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(data, dict) or type(data.get("schema_version")) is not int or data["schema_version"] != 1 or not isinstance(data.get("rules"), str) or not isinstance(data.get("commits"), dict): + raise ValueError(f"{path}: invalid history cache") + if data["rules"] != signature: + return {} + for sha, counts in data["commits"].items(): + if len(sha) != 40 or any(character not in "0123456789abcdef" for character in sha): + raise ValueError(f"{path}: invalid cached commit") + if counts is None: + continue + if not isinstance(counts, dict) or counts.keys() != {"libraries", "shaders"}: + raise ValueError(f"{path}: invalid cached counts for {sha}") + for metric in counts.values(): + if not isinstance(metric, dict) or metric.keys() != {"done", "total", "percent"}: + raise ValueError(f"{path}: invalid cached metric for {sha}") + done, total, percent = (metric[key] for key in ("done", "total", "percent")) + if type(done) is not int or type(total) is not int or not 0 <= done <= total or type(percent) not in (int, float) or percent != (round(100 * done / total, 2) if total else 0): + raise ValueError(f"{path}: inconsistent cached metric for {sha}") + return data["commits"] + + +def save_cache(path, signature, commits): + path.parent.mkdir(parents=True, exist_ok=True) + temporary = None + try: + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=path.parent, + prefix=".history-", delete=False) as output: + temporary = Path(output.name) + json.dump({"schema_version": 1, "rules": signature, "commits": commits}, output, sort_keys=True) + output.write("\n") + if path.exists(): + temporary.chmod(stat.S_IMODE(path.stat().st_mode)) + os.replace(temporary, path) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + + +def collect(ref, cache=None): + signature = cache_signature() if cache is not None else None + records = load_cache(cache, signature) if cache is not None else {} + points, changed = [], False for day, sha in daily_commits(ref): - counts = measure(sha) + if sha not in records: + records[sha] = measure(sha) + changed = True + counts = records[sha] if counts: points.append({"date": day, "commit": sha, **counts}) + if cache is not None and changed: + save_cache(cache, signature, records) return points def labelled(day, span): - if span <= 31: - return day.strftime("%m-%d") + if span <= 14: + return f"{day:%b} {day.day}" if span <= 150: - return day.strftime("%m-%d") if day.weekday() == 0 else "" - return day.strftime("%Y-%m") if day.day <= 7 and day.weekday() == 0 else "" + return f"{day:%b} {day.day}" if day.weekday() == 0 else "" + return f"{day:%b %Y}" if day.day <= 7 and day.weekday() == 0 else "" + + +def scale(peak): + step = next(s for s in (5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000) if peak <= s * 4) + return step, max(step, -(-peak // step) * step) + + +def spread(ys, gap=15): + out = list(ys) + order = sorted(range(len(ys)), key=ys.__getitem__) + for above, below in zip(order, order[1:]): + out[below] = max(out[below], out[above] + gap) + return out def chart(points): days = [date.fromisoformat(p["date"]).toordinal() for p in points] first, span = days[0], max(days[-1] - days[0], 1) - top = max(10, int(-(-max(p[name]["percent"] for p in points for name, _, _ in SERIES) // 10) * 10)) px = lambda d: LEFT + (d - first) / span * (WIDTH - LEFT - RIGHT) - py = lambda v: TOP + (1 - v / top) * (HEIGHT - TOP - BOTTOM) - base = py(0) - out = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {WIDTH} {HEIGHT}" role="img" ' - f'aria-label="Progress over time" font-family="sans-serif" font-size="12" fill="{TEXT}">', - f'<rect width="{WIDTH}" height="{HEIGHT}" rx="6" fill="{BACKGROUND}"/>'] - for tick in range(0, top + 1, 10): - out.append(f'<line x1="{LEFT}" x2="{WIDTH - RIGHT}" y1="{py(tick):.1f}" y2="{py(tick):.1f}" stroke="{GRID}"/>' - f'<text x="{LEFT - 6}" y="{py(tick) + 4:.1f}" text-anchor="end">{tick}%</text>') - for d, p in zip(days, points): - x = px(d) - label = labelled(date.fromordinal(d), span) - if label: - out.append(f'<line x1="{x:.1f}" x2="{x:.1f}" y1="{TOP}" y2="{base:.1f}" stroke="{GRID}" stroke-width="0.5"/>' - f'<text transform="translate({x + 4:.1f} {base + 8:.1f}) rotate(60)" font-size="10">{label}</text>') - for name, label, color in SERIES: - coords = [(px(d), py(p[name]["percent"])) for d, p in zip(days, points)] - line = " ".join(f"{x:.1f},{y:.1f}" for x, y in coords) - out.append(f'<polygon points="{coords[0][0]:.1f},{base:.1f} {line} {coords[-1][0]:.1f},{base:.1f}" fill="{color}" opacity="0.15"/>' - f'<polyline points="{line}" fill="none" stroke="{color}" stroke-width="2"/>') - for (x, y), p in zip(coords, points) if len(points) <= MAX_DOTS else (): - out.append(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="3" fill="{color}"><title>{p["date"]}: {label} ' - f'{p[name]["percent"]}% ({p[name]["done"]}/{p[name]["total"]})') + xs = [px(d) for d in days] + edges = [LEFT] + [(a + b) / 2 for a, b in zip(xs, xs[1:])] + [WIDTH - RIGHT] + labels = [(px(d), labelled(date.fromordinal(d), span)) for d in range(first, days[-1] + 1)] + height = len(SERIES) * (TITLE + PANEL + GAP) - GAP + AXIS + out = [f'', + '', + f''] + for index, (name, title, color) in enumerate(SERIES): + top = index * (TITLE + PANEL + GAP) + TITLE + base = top + PANEL + step, peak = scale(max(p[name][key] for p in points for key in ("done", "total"))) + py = lambda v: base - v / peak * PANEL + out.append(f'{title}') + for tick in range(0, peak + 1, step): + out.append(f'' + f'{tick:,}') + for x, label in labels: + if label: + out.append(f'') + done = [(x, py(p[name]["done"])) for x, p in zip(xs, points)] + total = [(x, py(p[name]["total"])) for x, p in zip(xs, points)] + line = lambda coords: " ".join(f"{x:.1f},{y:.1f}" for x, y in coords) + out.append(f'') + out.append(f'') + out.append(f'') + if len(points) <= MAX_DOTS: + out += [f'' for x, y in done] last = points[-1][name] - out.append(f'' - f'{SHORT[name]} {last["percent"]}%') + declared_y, implemented_y = spread([total[-1][1], done[-1][1]]) + out.append(f'Declared {last["total"]:,}' + f'' + f'\u25cf Implemented {last["done"]:,} ({last["percent"]:.0f}%)') + for left, right, p in zip(edges, edges[1:], points): + out.append(f'' + f'{p["date"]} · {title}\nImplemented {p[name]["done"]:,} of {p[name]["total"]:,} declared ' + f'({p[name]["percent"]}%)') + axis = height - AXIS + 18 + out += [f'{label}' + for x, label in labels if label] out.append("") return "\n".join(out) @@ -113,9 +207,12 @@ def page(points): '', '

Progress history

', chart(points), - f'

One point per day with commits on main, taken at the last commit of the day. Every point is measured ' + f'

Each panel has its own scale: the solid line is what is implemented, the dashed line what is declared. Library ' + 'functions are declared in the prx sources; the shader total is the AMD RDNA 1 + RDNA 2 instruction list ' + f'(tools/rdna_isa.txt). ' + 'One point per UTC day with commits on main, taken at the last commit of the day. Every point is measured ' f'with the current tools/progress.py rules and instruction list, ' - 'so the series is comparable across days. Hover a point for the exact numbers. Raw numbers: history.json. ' + 'so the series is comparable across days. Hover a day for the exact numbers. Raw numbers: history.json. ' 'Back to the current progress.

', '
' + "".join(f'' for _, label, _ in SERIES) + '', rows, @@ -124,16 +221,29 @@ def page(points): ]) + "\n" -if __name__ == "__main__": +def main(argv=None): parser = argparse.ArgumentParser() parser.add_argument("output", type=Path) parser.add_argument("--ref", default="HEAD", help="branch or commit whose first-parent history is measured") - args = parser.parse_args() - points = collect(args.ref) + parser.add_argument("--cache", type=Path, help="reuse per-commit counts while the scanner, ISA list and Python version match") + args = parser.parse_args(argv) + try: + if args.cache: + outputs = [args.output / name for name in ("history.json", "history.svg", "history.html")] + if any(args.cache.resolve() == path.resolve() or + (args.cache.exists() and path.exists() and args.cache.samefile(path)) for path in outputs): + raise ValueError("history cache must differ from generated reports") + points = collect(args.ref, args.cache) + except (OSError, ValueError) as error: + parser.error(str(error)) if not points: parser.error("no commit with a measurable tree") args.output.mkdir(parents=True, exist_ok=True) - (args.output / "history.json").write_text(json.dumps(points, indent=2)) - (args.output / "history.svg").write_text(chart(points) + "\n") - (args.output / "history.html").write_text(page(points)) + (args.output / "history.json").write_text(json.dumps(points, indent=2), encoding="utf-8") + (args.output / "history.svg").write_text(chart(points) + "\n", encoding="utf-8") + (args.output / "history.html").write_text(page(points), encoding="utf-8") print(f"{len(points)} days, {points[0]['date']} .. {points[-1]['date']}") + + +if __name__ == "__main__": + main() diff --git a/tools/select_tests.py b/tools/select_tests.py new file mode 100644 index 0000000000..a69c482e43 --- /dev/null +++ b/tools/select_tests.py @@ -0,0 +1,291 @@ +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-sig").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, *arguments): + listing = json.loads(subprocess.run(["ctest", "--test-dir", self.path, "--show-only=json-v1", *arguments], + capture_output=True, text=True, check=True).stdout) + return listing, listing["tests"] + + 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, reasons=None): + 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) + if reasons is not None: + reasons[name] = (["no build executable"] if test["target"] is None else []) + reasons[name] += ["input: " + path for path in sorted(files & scope)] + reasons[name] += ["registration: " + path for path in sorted(files & set(test["registered"]))] + reasons[name] += ["directory: " + directory for directory in dirs + if any(path.startswith(directory) for path in scope | set(test["registered"]))] + setups = collections.defaultdict(set) + for name, test in tests.items(): + for fixture in test["sets"]: + setups[fixture].add(name) + todo = list(picked) + while todo: + name = todo.pop() + test = tests[name] + for extra in set(test["depends"]).union(*(setups[f] for f in test["requires"])): + if extra not in tests: + continue + if reasons is not None: + fixture = set(test["requires"]) & set(tests[extra]["sets"]) + kind = "fixture setup for: " if fixture else "dependency of: " + reasons.setdefault(extra, []).append(kind + name) + if extra not in picked: + picked.add(extra) + todo.append(extra) + return picked, [] + + +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"]] + if not run: + skip = {build.target(t) for t in listed} - {None} + return [], [t for t in build.leaves() if t not in skip], len(listed) + _, selected = build.tests("-I", "0,0,0," + ",".join(map(str, run))) + names = {t["name"] for t in selected} + run = [i for i, t in enumerate(listed, 1) if t["name"] in names] + keep = {build.output(a) for t in listed if t["name"] in names for a in t.get("command") or [] if os.path.isabs(a)} + skip = {build.target(t) for t in listed if t["name"] not in names} - keep - {None} + return run, [t for t in build.leaves() if t not in skip], len(listed) + + +def escape(name): + return name.replace("$", "$$").replace(":", "$:").replace(" ", "$ ") + + +def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") + 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") + e = sub.add_parser("explain", help="explain saved-graph selections without a configured build; CTest may add fixtures") + e.add_argument("graph", type=Path) + e.add_argument("changed", type=Path, help="file with one changed path per line") + c = sub.add_parser("ctest", help="run ctest on a tree, limited to the selected tests if select wrote any") + c.add_argument("build") + c.add_argument("arguments", nargs=argparse.REMAINDER, help="ctest arguments") + args = parser.parse_args(argv) + if args.command == "ctest": + selected = Path(args.build, "select.tests") + extra = ["-I", selected.read_text(encoding="utf-8-sig").strip()] if selected.exists() else [] + return subprocess.run(["ctest", "--test-dir", args.build, *args.arguments, *extra]).returncode + if args.command == "explain": + try: + graph = json.loads(args.graph.read_text(encoding="utf-8-sig")) + changed = [line.strip() for line in args.changed.read_text(encoding="utf-8-sig").splitlines() if line.strip()] + reasons = {} + picked, full = select(graph, changed, reasons) + except (OSError, ValueError) as error: + parser.error(str(error)) + print(json.dumps({"tests": sorted(picked) if picked is not None else None, + "full_run_paths": full, "reasons": {name: sorted(set(why)) for name, why in reasons.items()}}, + indent=2, sort_keys=True)) + return 0 + try: + build = Build(args.build) + except (OSError, KeyError) as error: + parser.error(f"{args.build}: not a configured CMake build directory ({error})") + if args.command == "dump": + dump(build, args.graph) + return 0 + 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-sig")) + changed = [line.strip() for line in Path(args.changed).read_text(encoding="utf-8-sig").splitlines() if line.strip()] + picked, full = select(graph, changed) + if picked is None: + print(f"full run: {', '.join(full[:5])}") + 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 2c296b9298..7426cd8aa2 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 @@ -115,6 +116,7 @@ def td_target(module, td_path, td_lib): def generate(module, nids, db, td_path=None, td_lib=None): + module = re.sub(r"\.prx$", "", module, flags=re.IGNORECASE) prefix = module_prefix(module) path, lib = td_target(module, td_path, td_lib) used = set() @@ -146,7 +148,7 @@ def generate(module, nids, db, td_path=None, td_lib=None): if display is not None: chunks.append("") chunks.append("int APS5_VABI %s(void) {" % func) - chunks.append(' NotImplemented_nid_no_patch("%s");' % nid) + chunks.append(" NotImplemented_nid_no_patch(__func__);") chunks.append(" return 0;") chunks.append("}") td.append("- [%s](%s) (%s) - unknown signature" % (display, path, lib)) @@ -193,14 +195,25 @@ 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: for temporary, path in staged: - temporary.unlink(missing_ok=True) + try: + temporary.unlink(missing_ok=True) + except PermissionError: + temporary.chmod(0o600) + temporary.unlink(missing_ok=True) def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="backslashreplace", newline="\n") ap = argparse.ArgumentParser(description="Generate AnyPS5 Export.cpp skeletons and TechnicalDebt lines") ap.add_argument("--module", required=True, help="target module name, e.g. libSceAmpr, used for UnknownNN prefix and default TechnicalDebt path core/libs/prx/MODULE/Export.cpp") ap.add_argument("--nids", required=True, help="path to missing-NIDs file, one 11-char NID per line") @@ -211,8 +224,6 @@ def main(argv=None): ap.add_argument("--td-out", required=False, default=None, help="output file for TechnicalDebt lines, which go in docs/dev/debt/MODULE.md, default stderr when code uses stdout, otherwise stdout") ap.add_argument("--td-only", action="store_true", help="print only TechnicalDebt lines, no code") args = ap.parse_args(argv) - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8", newline="\n") try: if not args.td_only and args.out and args.td_out and same_file(args.out, args.td_out): ap.error("--out and --td-out must name different files") diff --git a/tools/test_web.py b/tools/test_web.py index c6c66fe237..9da700335f 100644 --- a/tools/test_web.py +++ b/tools/test_web.py @@ -5,6 +5,7 @@ import re import subprocess import sys import tempfile +import urllib.error import urllib.request ROOT = Path(__file__).resolve().parent.parent @@ -13,22 +14,39 @@ from test_guest_intel_trampolines import main_fixture from test_guest_module_directories import module_with_symbol -def main(): +def main(argv=None): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser() parser.add_argument("--build", type=Path, required=True) - args = parser.parse_args() - with tempfile.TemporaryDirectory(prefix="anyps5-web-") as directory: - fixture = Path(directory) - (fixture / "main.elf").write_bytes(main_fixture()) - (fixture / "module.elf").write_bytes(module_with_symbol(True)) - source = (args.build / "web/archive.mjs").read_text(encoding="utf-8") - url = re.search(r'from "(https://cdn.jsdelivr.net/[^"\n]+)"', source).group(1) - library = fixture / "zip.mjs" - with urllib.request.urlopen(url, timeout=30) as response: - library.write_bytes(response.read()) - environment = dict(os.environ, ANYPS5_TEST_ZIP_URL=url, ANYPS5_TEST_ZIP_PATH=library.as_uri()) - subprocess.run(["node", str(ROOT / "demo/web/tests/archive.test.mjs"), str(args.build.resolve() / "web"), str(fixture)], env=environment, check=True) + args = parser.parse_args(argv) + archive = args.build / "web/archive.mjs" + if not archive.is_file(): + parser.error(f"archive module does not exist: {archive}") + try: + subprocess.run(["node", "--test", str(ROOT / "demo/web/tests/output-directory.test.mjs")], check=True) + with tempfile.TemporaryDirectory(prefix="anyps5-web-") as directory: + fixture = Path(directory) + (fixture / "main.elf").write_bytes(main_fixture()) + (fixture / "module.elf").write_bytes(module_with_symbol(True)) + source = archive.read_text(encoding="utf-8-sig") + match = re.search(r'from "(https://cdn.jsdelivr.net/[^"\n]+)"', source) + if not match: + parser.error(f"zip library URL not found in {archive}") + url = match.group(1) + library = fixture / "zip.mjs" + with urllib.request.urlopen(url, timeout=30) as response: + library.write_bytes(response.read()) + environment = dict(os.environ, ANYPS5_TEST_ZIP_URL=url, ANYPS5_TEST_ZIP_PATH=library.as_uri()) + subprocess.run(["node", str(ROOT / "demo/web/tests/archive-root.test.mjs"), str(args.build.resolve() / "web")], env=environment, check=True) + subprocess.run(["node", str(ROOT / "demo/web/tests/archive-windows.test.mjs"), str(args.build.resolve() / "web")], env=environment, check=True) + subprocess.run(["node", str(ROOT / "demo/web/tests/archive.test.mjs"), str(args.build.resolve() / "web"), str(fixture)], env=environment, check=True) + except (subprocess.CalledProcessError, urllib.error.URLError, OSError) as error: + parser.error(str(error)) + return 0 if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/tools/tests/test_build_eligibility.py b/tools/tests/test_build_eligibility.py new file mode 100644 index 0000000000..9bc78b0bcb --- /dev/null +++ b/tools/tests/test_build_eligibility.py @@ -0,0 +1,185 @@ +import json +import os +import queue +import shutil +import subprocess +import threading +import tempfile +import unittest +from pathlib import Path + +WORKFLOW = Path(__file__).resolve().parents[2] / ".github/workflows/build.yml" + +HARNESS = r""" +console.error("eligibility: harness-start", process.pid, process.version); +const fs = require('fs'); +const readline = require('readline'); +const script = fs.readFileSync(process.argv[2], 'utf8'); +const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor; +const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity }); + +async function run(scenario) { + const calls = []; + const outputs = {}; + const warnings = []; + let summary = ''; + const github = { + rest: { + pulls: { + get: async () => ({ data: { state: 'open', base: { ref: 'main' }, head: { sha: 'head' }, mergeable: true, user: { login: 'author' } } }), + listFiles: async () => ({ data: [] }) + }, + repos: { + listCommits: async (params) => { + calls.push(['listCommits', params.sha, params.author]); + return { data: scenario.commits }; + }, + getBranch: async () => ({ data: { commit: { sha: 'main' } } }), + getCollaboratorPermissionLevel: async () => ({ data: { permission: 'write' } }) + }, + search: { + issuesAndPullRequests: async (params) => { + calls.push(['search', params.q]); + if (scenario.search === null) { + throw Object.assign(new Error('API rate limit exceeded'), { status: 403 }); + } + return { data: scenario.search }; + } + } + }, + paginate: async () => scenario.files.map((filename) => ({ filename })) + }; + const context = { + eventName: scenario.manual ? 'workflow_dispatch' : 'pull_request', + actor: 'maintainer', + repo: { owner: 'owner', repo: 'project' }, + payload: { pull_request: { number: 7, head: { sha: 'head' }, user: { login: 'author' } } } + }; + const core = { + setOutput: (name, value) => { outputs[name] = value; }, + info: () => {}, + warning: (message) => { warnings.push(String(message)); }, + summary: { addRaw: (text) => { summary += text; return { write: async () => {} }; } } + }; + try { + await new AsyncFunction('github', 'context', 'core', script)(github, context, core); + return { outputs, warnings, calls, summary }; + } catch (error) { + return { error: String(error), outputs, warnings, calls, summary }; + } +} + +input.on('line', (line) => { + run(JSON.parse(line)).then((result) => process.stdout.write(JSON.stringify(result) + '\n')); +}); +""" + + +def gate_script(): + lines = WORKFLOW.read_text(encoding="utf-8").splitlines() + start = next(index for index, line in enumerate(lines) if line.strip() == "id: gate") + start = next(index for index in range(start, len(lines)) if lines[index].strip() == "script: |") + 1 + indent = len(lines[start]) - len(lines[start].lstrip()) + script = [] + for line in lines[start:]: + if line.strip() and len(line) - len(line.lstrip()) < indent: + break + script.append(line[indent:]) + return "\n".join(script) + + +class RelinkerEligibilityTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.node = shutil.which("node") + if not cls.node: + raise unittest.SkipTest("the eligibility script runs in node") + cls.temp = tempfile.TemporaryDirectory() + cls.root = Path(cls.temp.name) + (cls.root / "harness.js").write_text(HARNESS, encoding="utf-8") + (cls.root / "gate.js").write_text(gate_script(), encoding="utf-8") + environment = dict(os.environ, PR_NUMBER="7", GITHUB_TRIGGERING_ACTOR="maintainer") + cls.process = subprocess.Popen( + [cls.node, "harness.js", "gate.js"], cwd=cls.root, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8", bufsize=1, + env=environment, + ) + + @classmethod + def tearDownClass(cls): + if cls.process.poll() is None: + cls.process.stdin.close() + try: + cls.process.wait(timeout=5) + except subprocess.TimeoutExpired: + cls.process.kill() + cls.process.wait() + cls.temp.cleanup() + + def run_gate(self, commits, search, files=("core/relinker/main.cpp",), manual=False): + self.process.stdin.write(json.dumps({ + "commits": commits, "search": search, "files": list(files), "manual": manual, + }) + "\n") + self.process.stdin.flush() + response = queue.Queue(maxsize=1) + threading.Thread(target=lambda: response.put(self.process.stdout.readline()), daemon=True).start() + try: + line = response.get(timeout=90) + except queue.Empty: + self.process.kill() + self.process.wait() + stderr = self.process.stderr.read() + self.fail(f"{self.node}: stderr:\n{stderr}\n" + "eligibility harness did not answer within 90 seconds") + if not line: + stderr = self.process.stderr.read() if self.process.poll() is not None else "" + self.fail(f"eligibility harness exited with status {self.process.poll()}: {stderr}") + result = json.loads(line) + self.assertNotIn("error", result, result.get("error")) + return result + + def test_author_with_a_commit_in_main_runs_the_relinker(self): + result = self.run_gate([{"sha": "a"}], None) + self.assertIs(result["outputs"]["relinker"], True) + self.assertEqual(result["calls"], [["listCommits", "main", "author"]]) + + def test_author_with_only_merged_pull_requests_runs_the_relinker(self): + result = self.run_gate([], {"total_count": 12, "incomplete_results": False, "items": []}) + self.assertIs(result["outputs"]["relinker"], True) + self.assertEqual(result["calls"], [["listCommits", "main", "author"], + ["search", "repo:owner/project is:pr is:merged base:main author:author"]]) + self.assertEqual(result["warnings"], []) + + def test_author_without_merged_pull_requests_skips_the_relinker(self): + result = self.run_gate([], {"total_count": 0, "incomplete_results": False, "items": []}) + self.assertIs(result["outputs"]["relinker"], False) + self.assertEqual(result["warnings"], []) + self.assertIn("no commit or merged pull request in main was found for the author", result["summary"]) + + def test_failed_search_skips_the_relinker_with_a_warning(self): + result = self.run_gate([], None) + self.assertIs(result["outputs"]["relinker"], False) + self.assertEqual(result["outputs"]["sha"], "main") + self.assertEqual(len(result["warnings"]), 1) + self.assertIn("API rate limit exceeded", result["warnings"][0]) + + def test_incomplete_search_skips_the_relinker_with_a_warning(self): + result = self.run_gate([], {"total_count": 3, "incomplete_results": True, "items": []}) + self.assertIs(result["outputs"]["relinker"], False) + self.assertEqual(len(result["warnings"]), 1) + self.assertIn("incomplete", result["warnings"][0]) + + def test_pull_request_without_relinker_files_checks_no_author(self): + result = self.run_gate([], None, files=("core/libs/tests/write_watch/a.cpp",)) + self.assertIs(result["outputs"]["relinker"], False) + self.assertIs(result["outputs"]["write-watch"], True) + self.assertEqual(result["calls"], []) + + def test_manual_run_checks_no_author(self): + result = self.run_gate([], None, manual=True) + self.assertIs(result["outputs"]["relinker"], True) + self.assertEqual(result["calls"], []) + self.assertIs(self.run_gate([], None, files=("README.md",), manual=True)["outputs"]["relinker"], False) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_build_web.py b/tools/tests/test_build_web.py new file mode 100644 index 0000000000..91e093b70d --- /dev/null +++ b/tools/tests/test_build_web.py @@ -0,0 +1,154 @@ +import os +import contextlib +import hashlib +import io +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch +import warnings +import zipfile + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import build_web + + +def library_zip(entries): + buffer = io.BytesIO() + with warnings.catch_warnings(), zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as archive: + warnings.simplefilter("ignore", UserWarning) + for name, data in entries: + archive.writestr(name, data) + return buffer.getvalue() + + +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) + self.output = Path(temporary.name) + self.libraries = self.output / "libraries" + self.libraries.mkdir() + self.manifest = self.output / "release.json" + self.previous = b'{"tag":"previous complete release"}\n' + self.manifest.write_bytes(self.previous) + self.existing = self.libraries / "previous.zip" + self.existing.write_bytes(b"previous archive") + + def publish(self, windows, linux): + assets = [] + responses = {} + for platform, data in (("windows", windows), ("linux", linux)): + url = f"https://example.invalid/{platform}.zip" + assets.append({"name": f"prx-{platform}-v1.zip", "size": len(data), + "digest": "sha256:" + hashlib.sha256(data).hexdigest(), "browser_download_url": url}) + responses[url] = data + release = {"tag_name": "v1", "html_url": "https://example.invalid/v1", "assets": assets} + responses["https://api.github.com/repos/boykopovar/AnyPS5/releases/latest"] = json.dumps(release).encode() + with patch.object(build_web, "request", side_effect=lambda url: io.BytesIO(responses[url])), \ + contextlib.redirect_stdout(io.StringIO()): + build_web.publish_libraries(self.output) + + def test_colliding_paths_do_not_publish_a_release(self): + valid = library_zip([("libs/libc.prx", b"valid archive")]) + for first, second in (("libs/libc.prx", "libs/libc.prx"), + ("libs/libc.prx", "libs/LIBC.prx"), + ("libs/extra/", "libs/EXTRA")): + with self.subTest(first=first, second=second): + invalid = library_zip([(first, b"" if first.endswith("/") else b"first"), (second, b"second")]) + with self.assertRaisesRegex(ValueError, "Duplicate library archive path"): + self.publish(invalid, valid) + self.assertEqual(self.manifest.read_bytes(), self.previous) + self.assertEqual(self.existing.read_bytes(), b"previous archive") + self.assertEqual(list(self.libraries.iterdir()), [self.existing]) + + def test_collision_in_second_platform_preserves_previous_manifest(self): + valid = library_zip([("libs/libc.prx", b"valid archive")]) + invalid = library_zip([("libs/libc.prx", b"first"), ("libs/LIBC.prx", b"second")]) + with self.assertRaisesRegex(ValueError, "Duplicate library archive path"): + self.publish(valid, invalid) + self.assertEqual(self.manifest.read_bytes(), self.previous) + self.assertEqual(self.existing.read_bytes(), b"previous archive") + self.assertFalse(any(file.suffix == ".download" for file in self.libraries.iterdir())) + + def test_distinct_full_paths_publish_both_platforms(self): + payload = library_zip([("libs/", b""), ("libs/libc.prx", b"first"), ("libs/extra/libc.prx", b"second")]) + self.publish(payload, payload) + manifest = json.loads(self.manifest.read_text(encoding="utf-8")) + self.assertEqual(manifest["tag"], "v1") + self.assertEqual(set(manifest["platforms"]), {"windows", "linux"}) + for platform, asset in manifest["platforms"].items(): + digest = hashlib.sha256(payload).hexdigest() + self.assertEqual(asset, {"path": f"libraries/{platform}-{digest}.zip", "size": len(payload), + "expandedBytes": 11, "sha256": digest}) + self.assertEqual((self.output / asset["path"]).read_bytes(), payload) + self.assertEqual(self.existing.read_bytes(), b"previous archive") + self.assertFalse(any(file.suffix == ".download" for file in self.libraries.iterdir())) + + +class BuildWebCliTests(unittest.TestCase): + def test_missing_relinker_wasm_reports_error_without_traceback(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + result = subprocess.run([sys.executable, str(Path(build_web.__file__)), "--build", str(build)], + capture_output=True, text=True, + env={**os.environ, "PYTHONPATH": str(Path(__file__).resolve().parents[1])}) + self.assertEqual(result.returncode, 2) + self.assertIn("Build relinker_wasm first: missing", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_main_with_explicit_argv(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + native = build / "web" / "native" + native.mkdir(parents=True) + (native / "relinker_wasm.mjs").touch() + (native / "relinker_wasm.wasm").touch() + with contextlib.redirect_stdout(io.StringIO()): + code = build_web.main(["--build", str(build)]) + self.assertEqual(code, 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index b691d60d45..ffd20390ab 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -1,10 +1,13 @@ import json import os +import shlex +import shutil import subprocess import sys import tempfile import unittest from pathlib import Path +from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import check_compile @@ -13,6 +16,139 @@ TOOL = Path(__file__).resolve().parents[1] / "check_compile.py" class CheckCompileTests(unittest.TestCase): + def test_unexpanded_response_files_cannot_rewrite_build_outputs(self): + compiler = shutil.which("clang") + if compiler is None: + self.skipTest("Clang is required to check compilation-fragment side effects") + source = self.source("unit.c") + outputs = [self.build / name for name in ("unit.o", "unit.d", "fragment.json")] + response = self.build / "unit flags.rsp" + response.write_text(" ".join('"' + argument + '"' for argument in + ("-c", source.as_posix(), "-MMD", "-MF", outputs[1].as_posix(), + "-MJ", outputs[2].as_posix(), "-o", outputs[0].as_posix())), encoding="utf-8") + arguments = [compiler, "@" + str(response)] + original = subprocess.run(arguments, cwd=self.build, capture_output=True, text=True) + self.assertEqual(original.returncode, 0, original.stderr) + preserved = {path: path.read_bytes() for path in [source, response, *outputs]} + for field in ("arguments", "command"): + for jobs in ("1", "2"): + with self.subTest(field=field, jobs=jobs): + command = (subprocess.list2cmdline(arguments) if os.name == "nt" else + " ".join(shlex.quote(argument) for argument in arguments)) + self.manifest([{"directory": str(self.build), "file": str(source), + field: arguments if field == "arguments" else command}]) + result = self.tool("--jobs", jobs, "unit.c") + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("unexpanded response file", result.stderr) + self.assertIn(str(response), result.stderr) + self.assertNotIn("Traceback", result.stderr) + for path, content in preserved.items(): + self.assertEqual(path.read_bytes(), content) + + def test_dependency_options_do_not_make_syntax_checks_fail_or_rewrite_outputs(self): + compiler = shutil.which("clang") + if compiler is None: + self.skipTest("Clang is required to check unused dependency options with -Werror") + source = self.source("unit.c") + object_file = self.build / "unit.o" + dependency_file = self.build / "unit.d" + for option in ("-MF", "-MT", "-MQ"): + for joined in (False, True): + with self.subTest(option=option, joined=joined): + source.write_text("int x;\n", encoding="utf-8") + value = str(dependency_file) if option == "-MF" else "target with spaces.o" + flags = [option + value] if joined else [option, value] + if option != "-MF": + flags += ["-MF", str(dependency_file)] + arguments = [compiler, "-Werror", "-MMD", *flags, + "-c", str(source), "-o", str(object_file)] + original = subprocess.run(arguments, cwd=self.build, capture_output=True, text=True) + self.assertEqual(original.returncode, 0, original.stderr) + outputs = {path: path.read_bytes() for path in (object_file, dependency_file)} + self.manifest([{"directory": str(self.build), "file": str(source), + "arguments": arguments}]) + result = self.tool("unit.c") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("1 translation units compile", result.stdout) + for path, content in outputs.items(): + self.assertEqual(path.read_bytes(), content) + source.write_text("#error syntax_check_must_fail\n", encoding="utf-8") + result = self.tool("unit.c") + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn("syntax_check_must_fail", result.stderr) + for path, content in outputs.items(): + self.assertEqual(path.read_bytes(), content) + + def test_ninja_response_file_is_expanded_before_syntax_rewriting(self): + compiler = shutil.which("clang") or shutil.which("gcc") + if compiler is None or shutil.which("ninja") is None: + 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_ninja_commands_in_a_non_ascii_build_directory_are_found(self): + compiler = shutil.which("clang") or shutil.which("gcc") + if compiler is None or shutil.which("ninja") is None: + self.skipTest("needs Ninja and a C compiler") + name = "ação" + build = self.root / name + if os.name == "nt": + try: + str(build).encode("mbcs", "strict") + except UnicodeEncodeError: + self.skipTest("the ANSI code page cannot represent the build directory") + build.mkdir() + self.source("unit.c") + command = subprocess.list2cmdline([compiler]) if os.name == "nt" else '"' + compiler + '"' + (build / "build.ninja").write_text( + "rule cc\n command = " + command + " -c $in -o $out\nbuild unit.o: cc ../unit.c\n", encoding="utf-8") + result = self.tool("--build", name, "unit.c") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("1 translation units compile", result.stdout) + + def test_windows_ninja_output_is_decoded_in_its_declared_build_file_encoding(self): + cases = (("Build file encoding: UTF-8\n", 0, "utf-8"), ("Build file encoding: ANSI\n", 0, "mbcs"), + ("Build file encoding: OEM\n", 0, None), ("", 1, None)) + for declared, status, expected in cases: + with self.subTest(declared=declared, status=status): + result = subprocess.CompletedProcess([], status, declared, "ninja: error: unknown tool" if status else "") + with patch.object(check_compile.sys, "platform", "win32"), \ + patch.object(check_compile, "capture", return_value=result): + if expected is None: + with self.assertRaises(ValueError): + check_compile.ninja_encoding("ninja") + else: + self.assertEqual(check_compile.ninja_encoding("ninja"), expected) + with patch.object(check_compile.sys, "platform", "linux"), \ + patch.object(check_compile, "capture", side_effect=AssertionError("queried Ninja")): + self.assertEqual(check_compile.ninja_encoding("ninja"), "utf-8") + + def test_syntax_check_preserves_existing_clang_compilation_fragment(self): + compiler = shutil.which("clang") + if compiler is None: + 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 +163,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 +216,82 @@ 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_compile_commands_accepts_utf8_bom(self): + source = self.source("unit.cpp") + script = self.compiler("import sys\nsys.exit(0)\n") + entry = self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{source}"') + (self.build / "compile_commands.json").write_text("\ufeff" + json.dumps([entry]), encoding="utf-8") + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 0) + self.assertIn("1 translation units compile", result.stdout) + + 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 +336,55 @@ 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_generated_translation_unit_outside_the_checkout_is_checked(self): + with tempfile.TemporaryDirectory() as directory: + source = Path(directory).resolve() / "generated unit.cpp" + source.write_text("int x;\n", encoding="utf-8") + seen = self.root / "arguments.json" + script = self.compiler( + "import json\nimport sys\nfrom pathlib import Path\n" + "Path(sys.argv[1]).write_text(json.dumps(sys.argv[3:]))\n" + "if sys.argv[2] == '1': sys.stderr.write('external source: error: no\\n')\n" + "sys.exit(int(sys.argv[2]))\n") + for relative in (False, True): + for status in (0, 1): + with self.subTest(relative=relative, status=status): + self.manifest([{ + "directory": str(self.build), + "file": os.path.relpath(source, self.build) if relative else str(source), + "arguments": [sys.executable, str(script), str(seen), str(status), + "-c", "-o", "generated.o", str(source)], + }]) + path = os.path.relpath(source, self.root) if relative else str(source) + result = self.tool(path) + self.assertNotIn("no compile command", result.stderr) + self.assertEqual(result.returncode, status, result.stdout + result.stderr) + self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), + ["-fsyntax-only", str(source)]) + if status: + self.assertIn("external source: error: no", result.stderr) + self.assertIn("1 of 1 translation units did not compile", result.stderr) + else: + self.assertIn("1 translation units compile", result.stdout) + def test_a_changed_file_with_a_non_ascii_name_is_checked(self): self.failing("caf\u00e9.cpp") subprocess.run(["git", "-C", str(self.root), "add", "caf\u00e9.cpp"], check=True) @@ -170,6 +431,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) @@ -186,17 +469,85 @@ class CheckCompileTests(unittest.TestCase): bin = self.root / "bin" bin.mkdir() stub = bin / "ninja_stub.py" - stub.write_text("import json\nprint(json.dumps(%r))\n" % [entry], encoding="utf-8") + stub.write_text("import json\nimport sys\n" + "if sys.argv[1:] == ['-t', 'wincodepage']:\n print('Build file encoding: UTF-8')\n" + "else:\n print(json.dumps(%r))\n" % [entry], encoding="utf-8") if os.name == "nt": - (bin / "ninja.cmd").write_text(f'@"{sys.executable}" "{stub}"\n', encoding="utf-8") + (bin / "ninja.cmd").write_text('@"%NINJA_STUB_PYTHON%" "%~dp0ninja_stub.py" %*\n', encoding="ascii") else: ninja = bin / "ninja" ninja.write_text(f'#!/bin/sh\nexec "{sys.executable}" "{stub}"\n', encoding="utf-8") ninja.chmod(0o755) - result = self.tool("unit.cpp", env={"PATH": f"{bin}{os.pathsep}{os.environ['PATH']}"}) + result = self.tool("unit.cpp", env={"PATH": f"{bin}{os.pathsep}{os.environ['PATH']}", + "NINJA_STUB_PYTHON": sys.executable}) self.assertEqual(result.returncode, 0) self.assertIn("1 translation units compile", result.stdout) + 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) + + def test_parallel_checks_overlap_but_diagnostics_keep_source_order(self): + barrier = self.root / "barrier" + barrier.mkdir() + compiler = self.compiler( + "import sys, time\nfrom pathlib import Path\n" + "root, name = Path(sys.argv[1]), sys.argv[2]\n" + "(root / name).touch()\n" + "deadline = time.monotonic() + 5\n" + "while not all((root / value).exists() for value in ('first', 'second')):\n" + " if time.monotonic() > deadline: sys.exit('checks did not overlap')\n" + " time.sleep(0.01)\n" + "if name == 'second': (root / 'second.done').touch()\n" + "while name == 'first' and not (root / 'second.done').exists():\n" + " if time.monotonic() > deadline: sys.exit('second did not finish')\n" + " time.sleep(0.01)\n" + "sys.exit(name + ': diagnostic')\n") + entries = [] + for name in ("first", "second"): + source = self.source(name + ".c") + entries.append(self.entry(name + ".c", f'"{sys.executable}" "{compiler}" "{barrier}" {name} "{source}"')) + self.manifest(entries) + result = self.tool("--jobs", "2", "first.c", "second.c") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("2 of 2 translation units did not compile", result.stderr) + self.assertLess(result.stderr.index("first: diagnostic"), result.stderr.index("second: diagnostic")) + self.assertNotIn("did not overlap", result.stderr) + for jobs in ("0", "-1"): + with self.subTest(jobs=jobs): + result = self.tool("--jobs", jobs, "first.c") + self.assertEqual(result.returncode, 2) + self.assertIn("positive integer", result.stderr) + + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_check_conventions_agent_files.py b/tools/tests/test_check_conventions_agent_files.py index 48a975aa1b..fcef15671a 100644 --- a/tools/tests/test_check_conventions_agent_files.py +++ b/tools/tests/test_check_conventions_agent_files.py @@ -9,11 +9,21 @@ from check_conventions import allowed_notes class AgentFileTests(unittest.TestCase): def test_agent_files_are_rejected_in_any_case(self): - for name in ("CLAUDE.md", "claude.md", "Claude.md", "AGENTS.md", "Agents.md", "GEMINI.md", "Copilot-Instructions.md", ".CursorRules"): + for name in ( + "CLAUDE.md", "claude.md", "Claude.md", "GEMINI.md", + "Copilot-Instructions.md", ".CursorRules", ".WindsurfRules", "windsurf.md", + "ChatGPT.md", ".ChatGPTRules", ".aider.conf.yml", + ): with self.subTest(name=name): 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_blob_reads.py b/tools/tests/test_check_conventions_blob_reads.py new file mode 100644 index 0000000000..17b5795272 --- /dev/null +++ b/tools/tests/test_check_conventions_blob_reads.py @@ -0,0 +1,131 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from collections import Counter +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import check_conventions +from check_conventions import Check + +EXPORT = "core/libs/prx/libSceExample/Export.cpp" +UNLISTED = "core/libs/prx/libSceExample/Unlisted.cpp" +EXPECTED = [ + ("binary", "data.txt", 0, ""), + ("comment", EXPORT, 3, ""), + ("duplicate-export", EXPORT, 2, "sceShared is also in libSceOther"), + ("silent-stub", UNLISTED, 1, "sceUnlisted"), +] + + +def repository(root, fillers, exports=0): + def git(*args): + subprocess.run(["git", "-c", f"safe.directory={root}", "-c", "user.name=Conventions Test", + "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + cwd=root, capture_output=True, check=True) + + def write(path, data): + target = root / path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data if isinstance(data, bytes) else data.encode("utf-8")) + + git("init", "-q") + write("docs/dev/TechnicalDebt.md", "# Project technical debt\n") + for index in range(fillers): + write(f"docs/dev/debt/libSceFiller{index}.md", f"# libSceFiller{index} technical debt\n") + write("docs/dev/debt/libSceExample.md", + f"# libSceExample technical debt\n\n### Silent stubs\n\n- [Known](../../../{EXPORT}) - layout unknown.\n") + write("core/libs/prx/libSceOther/src/Other.cpp", "int APS5_VABI sceShared() { return now(); }\n") + if exports: + names = [f"sceMany{index:04d}" + "LongExportName" * 6 for index in range(exports)] + write("core/libs/prx/libSceOther/src/Many.cpp", + f"int APS5_VABI {names[-1]}Extra() {{ return now(); }}\nint APS5_VABI {names[-1]}() {{ return now(); }}\n") + write("removed.txt", "removed\n") + git("add", ".") + git("commit", "-qm", "docs: add base") + git("tag", "base") + write(EXPORT, "int APS5_VABI sceExample() { return 0; }\nint APS5_VABI sceShared() { return now(); }\nint value; // note\n") + write(UNLISTED, "int APS5_VABI sceUnlisted() { return 0; }\n") + if exports: + write("core/libs/prx/libSceMany/Export.cpp", "".join(f"int APS5_VABI {name}() {{ return now(); }}\n" for name in names)) + write("data.txt", b"\xff\xfe not utf-8\n") + write("docs/user/Café.md", "# Café\n") + (root / "removed.txt").unlink() + git("add", "-A") + git("commit", "-qm", "feat: add example") + + +class BlobReadTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.directory = tempfile.TemporaryDirectory() + cls.roots = {} + for fillers in (2, 40): + root = Path(cls.directory.name) / str(fillers) + root.mkdir() + repository(root, fillers) + cls.roots[fillers] = root + + @classmethod + def tearDownClass(cls): + cls.directory.cleanup() + + def run_checks(self, root): + calls = Counter() + run = subprocess.run + + def counting(args, *rest, **options): + command = [str(arg) for arg in args] + if command[0] == "git": + index = 1 + while command[index] == "-c": + index += 2 + calls[command[index]] += 1 + return run(args, *rest, **options) + + previous = os.getcwd() + os.chdir(root) + try: + with patch.object(check_conventions.subprocess, "run", counting): + check = Check("base", "HEAD") + check.code() + check.build() + check.repository() + check.docs() + check.workflows() + check.commits() + finally: + os.chdir(previous) + return sorted(check.findings), calls + + def test_findings_come_from_the_batched_contents(self): + findings, _ = self.run_checks(self.roots[2]) + self.assertEqual(findings, EXPECTED) + + def test_contents_are_read_without_one_process_per_file(self): + _, calls = self.run_checks(self.roots[2]) + self.assertEqual(calls["show"], 0, calls) + self.assertLessEqual(calls["cat-file"], 2, calls) + + def test_process_count_does_not_grow_with_the_debt_files(self): + few_findings, few = self.run_checks(self.roots[2]) + many_findings, many = self.run_checks(self.roots[40]) + self.assertEqual(many_findings, few_findings) + self.assertEqual(many, few) + + def test_many_exports_keep_duplicate_detection(self): + root = Path(self.directory.name) / "many-exports" + root.mkdir() + repository(root, 2, exports=400) + findings, _ = self.run_checks(root) + self.assertEqual(findings, sorted(EXPECTED + [ + ("duplicate-export", "core/libs/prx/libSceMany/Export.cpp", 400, + "sceMany0399" + "LongExportName" * 6 + " is also in libSceOther") + ])) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_conventions_bom_shebang.py b/tools/tests/test_check_conventions_bom_shebang.py new file mode 100644 index 0000000000..6add18b3cf --- /dev/null +++ b/tools/tests/test_check_conventions_bom_shebang.py @@ -0,0 +1,48 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +class BomShebangTests(unittest.TestCase): + def check(self, header): + checker = Path(__file__).resolve().parents[1] / "check_conventions.py" + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + + def git(*args): + return subprocess.run( + ["git", "-c", "user.name=Conventions Test", "-c", "user.email=test@example.com", + "-c", "commit.gpgsign=false", *args], cwd=root, + capture_output=True, text=True, check=True) + + git("init", "-q") + (root / "README.md").write_text("Example\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "docs: add readme") + script = root / "tools/example.py" + script.parent.mkdir() + script.write_text("\ufeff" + header + '\nprint("ok")\n', encoding="utf-8") + executed = subprocess.run([sys.executable, str(script)], capture_output=True, text=True) + self.assertEqual((executed.returncode, executed.stdout.strip()), (0, "ok"), executed.stderr) + git("add", ".") + git("commit", "-qm", "fix: add Python tool") + environment = {key: value for key, value in os.environ.items() + if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")} + return subprocess.run([sys.executable, str(checker), "--base", "HEAD~1"], + cwd=root, capture_output=True, text=True, env=environment) + + def test_initial_bom_shebang_is_accepted(self): + result = self.check("#!/usr/bin/env python3") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_initial_bom_does_not_exempt_an_ordinary_comment(self): + result = self.check("# ordinary comment") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("[comment]", result.stdout) + + +if __name__ == "__main__": + unittest.main() 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 25e0b1f871..6bc843e5d2 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -11,6 +11,39 @@ 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 = 12 + + +def iter_test_cases(suite): + for test in suite: + if isinstance(test, unittest.TestSuite): + yield from iter_test_cases(test) + else: + yield test + + +def discovered_test_cases(): + suite = unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__]) + return sorted(iter_test_cases(suite), key=lambda test: test.id()) + + +def conventions_test_shard(shard, shard_count=CONVENTIONS_TEST_SHARDS): + if shard_count <= 0 or shard < 0 or shard >= shard_count: + raise ValueError("shard index must be within the shard count") + tests = discovered_test_cases() + return unittest.TestSuite(test for index, test in enumerate(tests) if index % shard_count == shard) + + +class TestShardTests(unittest.TestCase): + def test_shards_discover_each_case_once(self): + discovered = [test.id() for test in discovered_test_cases()] + sharded = [test.id() for shard in range(CONVENTIONS_TEST_SHARDS) + for test in iter_test_cases(conventions_test_shard(shard))] + self.assertEqual(sorted(sharded), sorted(discovered)) + self.assertEqual(len(sharded), len(set(sharded))) + self.assertTrue(all(len(list(iter_test_cases(conventions_test_shard(shard)))) > 0 + for shard in range(CONVENTIONS_TEST_SHARDS))) + class FunctionBodyTests(unittest.TestCase): def test_line_comments_preserve_following_statements(self): @@ -124,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" @@ -310,9 +391,16 @@ class SilentStubSectionTests(unittest.TestCase): result = self.check_stub("# Project technical debt\n\n### Build\n\n- A toolchain limitation.\n", "docs/dev/TechnicalDebt.md") self.assertNotIn("[debt-file]", result.stdout + result.stderr) + def test_entry_without_a_core_link_in_a_library_file_is_rejected(self): + result = self.check_stub("# libSceExample technical debt\n\n### Functional\n\n- A driver limitation.\n") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("docs/dev/debt/libSceExample.md:5: error: [debt-file]", result.stdout) + self.assertIn("docs/dev/TechnicalDebt.md", result.stdout) + class TestFixtureExportTests(unittest.TestCase): - def check(self, path, text): + def check(self, path, text=None, base=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) @@ -332,9 +420,12 @@ class TestFixtureExportTests(unittest.TestCase): git("init", "-q") write("core/libs/prx/libkernel/src/Time.cpp", "std::uint64_t APS5_VABI sceKernelGetProcessTime() { return now(); }\n") write("core/libs/prx/libSceNgs2/tests/Render.cpp", "static int APS5_VABI Process(int value) { return value; }\n") + for relative, content in (base or {}).items(): + write(relative, content) 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() @@ -362,6 +453,81 @@ class TestFixtureExportTests(unittest.TestCase): self.assertIn("core/libs/prx/libSceAudioOut/src/Clock.cpp:1: error: [duplicate-export]", result.stdout) self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) + def test_large_export_set_uses_a_file_backed_pattern(self): + names = [f"ConventionExport{index:08d}LongName" for index in range(1100)] + self.assertGreater(len("|".join(names)), 32767) + source = "\n".join(f"int APS5_VABI {name}(void) {{ return 0; }}" for name in names) + source += "\nstd::uint64_t APS5_VABI sceKernelGetProcessTime() { return now(); }\n" + result = self.check("core/libs/prx/libSceAudioOut/src/Exports.cpp", source) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceAudioOut/src/Exports.cpp:1101: error: [duplicate-export]", result.stdout) + self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) + + def test_export_shadowing_an_assembly_export_is_a_duplicate(self): + result = self.check("core/libs/prx/libSceLibcInternal/Export.cpp", + "int APS5_VABI setjmp_nid_postfix(void) {\n NotImplemented_nid_no_patch(__func__);\n return 0;\n}\n", + base={"core/libs/prx/libc/src/JumpBuffer.cpp": 'asm(".text\\n"\n APS5_ASM_FUNCTION("setjmp_nid_postfix")\n " ret\\n");\n'}) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceLibcInternal/Export.cpp:1: error: [duplicate-export]", result.stdout) + self.assertIn("setjmp_nid_postfix is also in libc", result.stdout) + + def test_export_shadowing_a_spaced_or_split_assembly_export_is_a_duplicate(self): + for macro, call in (("APS5_ASM_FUNCTION", ' ("setjmp_nid_postfix")'), + ("APS5_ASM_FUNCTION", '(\n "setjmp_nid_postfix")'), + ("APS5_SIGNAL_ASM_FUNCTION", ' (\n "setjmp_nid_postfix")')): + with self.subTest(macro=macro, call=call): + result = self.check("core/libs/prx/libSceLibcInternal/Export.cpp", + "int APS5_VABI setjmp_nid_postfix(void) {\n NotImplemented_nid_no_patch(__func__);\n return 0;\n}\n", + base={"core/libs/prx/libc/src/JumpBuffer.cpp": 'asm(".text\\n"\n ' + macro + call + '\n " ret\\n");\n'}) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceLibcInternal/Export.cpp:1: error: [duplicate-export]", result.stdout) + self.assertIn("setjmp_nid_postfix is also in libc", result.stdout) + + def test_assembly_export_in_another_library_is_a_duplicate(self): + result = self.check("core/libs/prx/libSceAudioOut/src/Clock.cpp", + 'asm(".text\\n"\n APS5_SIGNAL_ASM_FUNCTION("sceKernelGetProcessTime")\n " ret\\n");\n') + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceAudioOut/src/Clock.cpp:2: error: [duplicate-export]", result.stdout) + self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) + + def test_assembly_export_after_a_raw_string_is_a_duplicate(self): + result = self.check("core/libs/prx/libSceAudioOut/src/Jump.cpp", + 'asm(R"(\n .text\n)" APS5_ASM_FUNCTION("setjmp_nid_postfix") R"(\n ret\n)");\n', + base={"core/libs/prx/libc/src/JumpBuffer.cpp": 'asm(".text\\n"\n APS5_ASM_FUNCTION("setjmp_nid_postfix")\n " ret\\n");\n'}) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceAudioOut/src/Jump.cpp:3: error: [duplicate-export]", result.stdout) + self.assertIn("setjmp_nid_postfix is also in libc", result.stdout) + + def test_assembly_export_in_a_block_comment_is_not_an_export(self): + result = self.check("core/libs/prx/libSceLibcInternal/Export.cpp", + "int APS5_VABI setjmp_nid_postfix(void) {\n NotImplemented_nid_no_patch(__func__);\n return 0;\n}\n", + base={"core/libs/prx/libc/src/JumpBuffer.cpp": '/*\n APS5_ASM_FUNCTION("setjmp_nid_postfix")\n*/\n'}) + self.assertNotIn("duplicate-export", result.stdout, result.stdout + result.stderr) + + def test_assembly_name_that_is_not_an_identifier_keeps_the_check(self): + result = self.check("core/libs/prx/libSceAudioOut/src/Clock.cpp", + 'asm(".text\\n"\n APS5_ASM_FUNCTION("x(y")\n " ret\\n");\nstd::uint64_t APS5_VABI sceKernelGetProcessTime() {\n return 1;\n}\n') + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + 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 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", + '#version 450\n#define TEXT "a//b"\n#ifdef X\n#endif // X\nvoid main() {}\n') + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("All conventions checks passed", result.stdout) + class PullRequestTemplateTests(unittest.TestCase): def body_findings(self, body): @@ -381,6 +547,16 @@ class PullRequestTemplateTests(unittest.TestCase): def test_checked_item_is_accepted(self): self.assertEqual(self.findings("- [x] all"), []) + def test_depends_on_answered_as_a_list_is_accepted(self): + for answer in ("\n - #3\n - #4", "\r\n - #3\r\n - #4", "\n- #3", "\n * none"): + with self.subTest(answer=answer): + body = f"### What\nx\n### Tested\nLinux\n### Checklist\n- [x] Depends on:{answer}\n- [x] AI-assisted: no\n" + self.assertEqual(self.body_findings(body), []) + + def test_depends_on_left_empty_before_the_next_item_is_reported(self): + body = "### What\nx\n### Tested\nLinux\n### Checklist\n- [x] Depends on:\n- [x] AI-assisted: no\n" + self.assertEqual(self.body_findings(body), [("pr-template", "", 0, "fill Depends on with #N or none")]) + def test_subsections_stay_inside_their_section(self): body = "### What\nx\n### Tested\n#### Linux\nctest\n#### Windows\nctest\n### Checklist\n- [x] Depends on: none\n- [x] AI-assisted: no\n" self.assertEqual(self.body_findings(body), []) @@ -498,6 +674,80 @@ class RepositoryChecksTests(unittest.TestCase): check.code() self.assertIn(("comment", "core/example.cpp", 3, ""), check.findings) + def test_unchanged_move_preserves_existing_comments_and_extensions(self): + source = self.root / "core/tests/Existing.cpp" + source.parent.mkdir(parents=True) + source.write_text("// existing explanation\n__attribute__((noinline)) int Existing() { return 1; }\n", encoding="utf-8") + self.commit() + destination = self.root / "core/tests/group/Café example.cpp" + destination.parent.mkdir() + source.rename(destination) + (self.root / "a.bin").write_bytes(b"binary\x00data") + (self.root / "README.md").unlink() + self.commit() + check = Check("HEAD~1", "HEAD") + check.code() + check.repository() + name = destination.relative_to(self.root).as_posix() + self.assertEqual(check.status[name], "R") + self.assertEqual(check.numstat[name], ("0", "0")) + self.assertNotIn(name, check.added) + self.assertEqual(check.findings, [("binary", "a.bin", 0, "")]) + + def test_edited_move_checks_new_lines_at_the_destination(self): + source = self.root / "core/tests/Existing.cpp" + source.parent.mkdir(parents=True) + original = "// existing explanation\n__attribute__((noinline)) int Existing() { return 1; }\n" + original += "".join(f"int Existing{index}() {{ return {index}; }}\n" for index in range(12)) + source.write_text(original, encoding="utf-8") + self.commit() + destination = self.root / "core/tests/group/Edited.cpp" + destination.parent.mkdir() + source.rename(destination) + destination.write_text(original + "// new explanation\n__declspec(noinline) int Added() { return 2; }\n", encoding="utf-8") + self.commit() + check = Check("HEAD~1", "HEAD") + check.code() + name = destination.relative_to(self.root).as_posix() + self.assertEqual(check.status[name], "R") + self.assertEqual(check.numstat[name], ("2", "0")) + self.assertEqual(check.findings, [("comment", name, 15, ""), ("extension", name, 16, "__declspec")]) + + def test_copied_code_is_still_checked_as_an_addition(self): + source = self.root / "core/tests/Existing.cpp" + source.parent.mkdir(parents=True) + source.write_text("// existing explanation\n__attribute__((noinline)) int Existing() { return 1; }\n", encoding="utf-8") + self.commit() + destination = self.root / "core/tests/Copy.cpp" + destination.write_bytes(source.read_bytes()) + self.commit() + check = Check("HEAD~1", "HEAD") + check.code() + name = destination.relative_to(self.root).as_posix() + self.assertEqual(check.status[name], "A") + self.assertEqual(check.findings, [("comment", name, 1, ""), ("extension", name, 2, "__attribute__")]) + + def test_moving_a_document_to_a_forbidden_path_is_rejected(self): + (self.root / "README.md").rename(self.root / "investigation.md") + self.commit() + check = Check("HEAD~1", "HEAD") + check.repository() + self.assertEqual(check.findings, [("notes-file", "investigation.md", 0, "")]) + + def test_moving_a_document_rechecks_relative_links(self): + source = self.root / "docs/dev/guide.md" + source.parent.mkdir(parents=True) + source.write_text("[reference](reference.md)\n", encoding="utf-8") + (source.parent / "reference.md").write_text("Reference\n", encoding="utf-8") + self.commit() + destination = self.root / "docs/dev/group/guide.md" + destination.parent.mkdir() + source.rename(destination) + self.commit() + check = Check("HEAD~1", "HEAD") + check.docs() + self.assertEqual(check.findings, [("doc-link", "docs/dev/group/guide.md", 1, "reference.md")]) + def test_unicode_raw_literals_keep_git_patch_and_grep_records_intact(self): literals = 'R"text(foo\u2028+tail\u2029+tail)text"' fixtures = ( @@ -515,6 +765,20 @@ class RepositoryChecksTests(unittest.TestCase): check.code() self.assertEqual(check.findings, [("comment", name, 2, "")]) + def test_large_export_set_checks_duplicates_without_command_line_limit(self): + first = self.root / "core/libs/prx/libSceA/Export.cpp" + first.parent.mkdir(parents=True) + first.write_text("int APS5_VABI sceLargeExportSet1999(int value) { return value; }\n", encoding="utf-8") + self.commit() + second = self.root / "core/libs/prx/libSceB/Export.cpp" + second.parent.mkdir(parents=True) + second.write_text("".join(f"int APS5_VABI sceLargeExportSet{index}(int value) {{ return value; }}\n" for index in range(2000)), encoding="utf-8") + self.commit() + first.write_text("", encoding="utf-8") + check = Check("HEAD~1", "HEAD") + check.code() + self.assertEqual(check.findings, [("duplicate-export", "core/libs/prx/libSceB/Export.cpp", 2000, "sceLargeExportSet1999 is also in libSceA")]) + def test_checks_from_a_subdirectory_see_the_whole_repository(self): definition = "int APS5_VABI sceFooBar(int a, int b) { return a + b; }\n" first = self.root / "core/libs/prx/libSceA/Export.cpp" @@ -536,6 +800,21 @@ class RepositoryChecksTests(unittest.TestCase): self.assertEqual(check.findings, [("duplicate-export", "core/libs/prx/libSceB/Export.cpp", 1, "sceFooBar is also in libSceA")]) + def test_type_change_does_not_shift_later_files(self): + (self.root / "a.txt").write_text("text\n", encoding="utf-8") + self.commit() + target = self.root / "target" + target.write_text("README.md", encoding="utf-8") + blob = self.git("hash-object", "-w", "target") + target.unlink() + self.git("update-index", "--cacheinfo", "120000", blob, "a.txt") + (self.root / "z.cpp").write_text("int z;\n// debt\n", encoding="utf-8") + self.git("add", "z.cpp") + self.git("commit", "-qm", "test: replace a file with a link") + check = Check("HEAD~1", "HEAD") + check.code() + self.assertIn(("comment", "z.cpp", 2, ""), check.findings) + def test_submodule_outside_thirdparty_reports_without_crashing(self): head = self.git("rev-parse", "HEAD") self.git("update-index", "--add", "--cacheinfo", "160000", head, "vendor") @@ -596,4 +875,16 @@ class RepositoryChecksTests(unittest.TestCase): if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "--shard": + if len(sys.argv) != 3 or "/" not in sys.argv[2]: + print("usage: test_check_conventions_comments.py --shard INDEX/COUNT", file=sys.stderr) + raise SystemExit(2) + try: + shard_index, shard_count = (int(part) for part in sys.argv[2].split("/", 1)) + suite = conventions_test_shard(shard_index, shard_count) + except ValueError as error: + print(f"invalid shard: {error}", file=sys.stderr) + raise SystemExit(2) + result = unittest.TextTestRunner().run(suite) + raise SystemExit(0 if result.wasSuccessful() else 1) unittest.main() diff --git a/tools/tests/test_check_conventions_debt_length.py b/tools/tests/test_check_conventions_debt_length.py new file mode 100644 index 0000000000..50dad53289 --- /dev/null +++ b/tools/tests/test_check_conventions_debt_length.py @@ -0,0 +1,64 @@ +import io +import os +import subprocess +import sys +import tempfile +import unittest +from contextlib import redirect_stdout +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import check_conventions +from check_conventions import Check + +DEBT = "docs/dev/debt/libSceExample.md" +SHORT = "- [sceShort](../../../core/libs/prx/libSceExample/Export.cpp) (libSceExample) - layout unknown" +LONG = "- [sceLong](../../../core/libs/prx/libSceExample/Export.cpp) (libSceExample) - " + "x" * 1000 +PROSE = "Not an entry. " + "y" * 1000 + + +class DebtLengthTests(unittest.TestCase): + def test_long_debt_entry(self): + self.assertFalse(check_conventions.long_debt_entry(SHORT)) + self.assertFalse(check_conventions.long_debt_entry("- " + "z" * (check_conventions.DEBT_ENTRY_LIMIT - 2))) + self.assertTrue(check_conventions.long_debt_entry("- " + "z" * (check_conventions.DEBT_ENTRY_LIMIT - 1))) + self.assertFalse(check_conventions.long_debt_entry(PROSE)) + + def test_added_long_entry_warns_without_failing(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + + def git(*args): + return subprocess.run(["git", "-c", f"safe.directory={root}", "-c", "user.name=Conventions Test", + "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + cwd=root, capture_output=True, check=True, encoding="utf-8").stdout + + def write(text): + (root / DEBT).parent.mkdir(parents=True, exist_ok=True) + (root / DEBT).write_text(text, encoding="utf-8") + + git("init", "-q") + write(f"# libSceExample technical debt\n\n### Functional\n\n{SHORT}\n") + git("add", ".") + git("commit", "-qm", "docs: add base") + write(f"# libSceExample technical debt\n\n{PROSE}\n\n### Functional\n\n{SHORT}\n{LONG}\n") + git("commit", "-qam", "docs(debt): add a long entry") + head = git("rev-parse", "HEAD").strip() + base = git("rev-parse", "HEAD~1").strip() + + previous = Path.cwd() + try: + os.chdir(root) + check = Check(base, head) + check.debt_entries() + with redirect_stdout(io.StringIO()): + status = check_conventions.output(check, "example/repo") + finally: + os.chdir(previous) + + self.assertEqual(check.findings, [("debt-length", DEBT, 8, f"{len(LONG)} characters")]) + self.assertEqual(status, 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_conventions_doc_links.py b/tools/tests/test_check_conventions_doc_links.py new file mode 100644 index 0000000000..ec02294932 --- /dev/null +++ b/tools/tests/test_check_conventions_doc_links.py @@ -0,0 +1,36 @@ +from pathlib import Path +import sys +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import check_conventions + +FENCE = "`" * 3 + + +class DocLinkTests(unittest.TestCase): + def test_code_blocks_and_inline_code_have_no_links(self): + text = f"{FENCE}cpp\nauto f = [](int a) {{ return a; }};\n{FENCE}\n~~~\nauto g = [](int b) {{}};\n~~~\nuse `[](auto)` here\n" + self.assertEqual(check_conventions.links("docs/x.md", text), []) + + def test_real_link_outside_block_is_kept(self): + 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_link_text_in_inline_code_is_kept(self): + self.assertEqual(check_conventions.links("docs/x.md", "see [`Driver::Run`](../README.md)"), [("../README.md", "README.md")]) + + def test_empty_link_text_is_ignored(self): + self.assertEqual(check_conventions.links("docs/x.md", "![](img.png) and [](empty.md)"), []) + + def test_fenced_lines(self): + text = f"a\n{FENCE}\n[](int a)\n{FENCE}\nb\n" + self.assertEqual(check_conventions.fenced_lines(text), {2, 3, 4}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_conventions_env_reference.py b/tools/tests/test_check_conventions_env_reference.py new file mode 100644 index 0000000000..b217e80247 --- /dev/null +++ b/tools/tests/test_check_conventions_env_reference.py @@ -0,0 +1,178 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import check_conventions +from check_conventions import Check + +DOC = "docs/dev/ENVIRONMENT.md" +SAMPLE = "core/libs/prx/libSceExample/Sample.cpp" +DOC_TEXT = "| `APS5_DOCUMENTED` | presence |\n| `APS5_GONE` | presence |\n" + + +def repository(root, base, head, target=None): + def git(*args): + subprocess.run(["git", "-c", f"safe.directory={root}", "-c", "user.name=Conventions Test", + "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + cwd=root, capture_output=True, check=True) + + def apply(files): + for path, text in files.items(): + target = root / path + if text is None: + target.unlink() + continue + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(text, encoding="utf-8") + + git("init", "-q") + apply(base) + git("add", ".") + git("commit", "-qm", "docs: add base") + git("tag", "base") + if target is not None: + git("branch", "pull") + apply(target) + git("add", "-A") + git("commit", "-qm", "docs: advance the target") + git("tag", "target") + git("checkout", "-q", "pull") + apply(head) + git("add", "-A") + git("commit", "-qm", "feat: read a new name") + + +def findings_of(root, base, head, ref="base"): + previous = os.getcwd() + os.chdir(root) + try: + check = Check(ref, "HEAD") + check.env_reference() + return check.findings + finally: + os.chdir(previous) + + +class EnvReferenceDiffTests(unittest.TestCase): + def run_case(self, base, head, target=None): + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = Path(directory.name) + repository(root, base, head, target) + return findings_of(root, base, head, "base" if target is None else "target") + + def test_only_a_name_the_pull_request_adds_is_reported_at_its_line(self): + base = { + DOC: DOC_TEXT, + SAMPLE: 'static bool a = std::getenv("APS5_DOCUMENTED") != nullptr;\n' + 'static bool b = std::getenv("APS5_OLD_UNDOCUMENTED") != nullptr;\n', + } + added = 'static bool c = std::getenv("APS5_NEW_READ") != nullptr;\n' + head = {SAMPLE: base[SAMPLE] + added} + findings = self.run_case(base, head) + self.assertEqual(findings, [("env-reference", SAMPLE, 3, + f"APS5_NEW_READ is read here but has no row in {DOC}")]) + + def test_a_branch_without_the_document_still_reports_its_new_reads(self): + base = {SAMPLE: "int unrelated;\n"} + head = {SAMPLE: 'static bool a = std::getenv("APS5_NEW_READ") != nullptr;\n'} + findings = self.run_case(base, head) + self.assertEqual([(rule, detail) for rule, _, _, detail in findings], + [("env-reference", f"APS5_NEW_READ is read here but has no row in {DOC}")]) + + def test_a_row_is_reported_when_the_pull_request_removes_its_last_read(self): + base = { + DOC: DOC_TEXT, + SAMPLE: 'static bool a = std::getenv("APS5_DOCUMENTED") != nullptr;\n' + 'static bool b = std::getenv("APS5_GONE") != nullptr;\n', + } + head = {SAMPLE: 'static bool a = std::getenv("APS5_DOCUMENTED") != nullptr;\n'} + findings = self.run_case(base, head) + self.assertEqual(findings, [("env-reference", DOC, 0, + "APS5_GONE has a row but the code no longer reads it")]) + + def test_removing_one_of_two_reads_keeps_the_row(self): + other = "core/libs/prx/libSceExample/Other.cpp" + base = { + DOC: DOC_TEXT, + SAMPLE: 'static bool a = std::getenv("APS5_DOCUMENTED") != nullptr;\n', + other: 'static bool b = std::getenv("APS5_DOCUMENTED") != nullptr;\n', + } + head = {other: None} + self.assertEqual(self.run_case(base, head), []) + + def test_a_name_the_pull_request_does_not_touch_is_not_reported(self): + base = { + DOC: DOC_TEXT, + SAMPLE: 'static bool b = std::getenv("APS5_OLD_UNDOCUMENTED") != nullptr;\n', + } + head = {SAMPLE: base[SAMPLE] + "int unrelated;\n"} + self.assertEqual(self.run_case(base, head), []) + + def test_a_row_the_pull_request_removes_is_reported_while_the_code_still_reads_it(self): + base = { + DOC: DOC_TEXT, + SAMPLE: 'static bool a = std::getenv("APS5_DOCUMENTED") != nullptr;\n', + } + head = {DOC: "| `APS5_GONE` | presence |\n"} + self.assertEqual(self.run_case(base, head), [("env-reference", DOC, 0, + "APS5_DOCUMENTED is still read by the code but this pull request removes its row")]) + + def test_a_removed_row_is_kept_quiet_when_the_code_stops_reading_it_too(self): + base = { + DOC: DOC_TEXT, + SAMPLE: 'static bool a = std::getenv("APS5_DOCUMENTED") != nullptr;\n' + 'static bool b = std::getenv("APS5_GONE") != nullptr;\n', + } + head = { + DOC: "| `APS5_DOCUMENTED` | presence |\n", + SAMPLE: 'static bool a = std::getenv("APS5_DOCUMENTED") != nullptr;\n', + } + self.assertEqual(self.run_case(base, head), []) + + def test_an_added_line_starting_with_plus_does_not_shift_the_reported_line(self): + base = {DOC: DOC_TEXT, SAMPLE: "int a;\n"} + head = {SAMPLE: "int a;\n+++x;\n" 'static bool b = std::getenv("APS5_NEW_READ") != nullptr;\n'} + findings = self.run_case(base, head) + self.assertEqual(findings, [("env-reference", SAMPLE, 3, + f"APS5_NEW_READ is read here but has no row in {DOC}")]) + + def test_a_removed_line_starting_with_minus_still_counts_as_a_removed_read(self): + base = { + DOC: DOC_TEXT, + SAMPLE: '--x; std::getenv("APS5_GONE") != nullptr;\n', + } + head = {SAMPLE: "int unrelated;\n"} + self.assertEqual(self.run_case(base, head), [("env-reference", DOC, 0, + "APS5_GONE has a row but the code no longer reads it")]) + + def test_a_row_the_target_added_after_the_branch_point_documents_the_read(self): + base = {SAMPLE: "int unrelated;\n"} + target = {DOC: "| `APS5_NEW_READ` | presence |\n"} + head = {SAMPLE: 'static bool a = std::getenv("APS5_NEW_READ") != nullptr;\n'} + self.assertEqual(self.run_case(base, head, target), []) + + def test_a_read_the_target_does_not_document_is_still_reported(self): + base = {SAMPLE: "int unrelated;\n"} + target = {DOC: DOC_TEXT} + head = {SAMPLE: 'static bool a = std::getenv("APS5_NEW_READ") != nullptr;\n'} + self.assertEqual(self.run_case(base, head, target), [("env-reference", SAMPLE, 1, + f"APS5_NEW_READ is read here but has no row in {DOC}")]) + + def test_a_row_the_pull_request_removes_is_reported_although_the_target_keeps_it(self): + base = { + DOC: DOC_TEXT, + SAMPLE: 'static bool a = std::getenv("APS5_DOCUMENTED") != nullptr;\n', + } + target = {"core/libs/prx/libSceExample/Other.cpp": "int other;\n"} + head = {DOC: "| `APS5_GONE` | presence |\n"} + self.assertEqual(self.run_case(base, head, target), [("env-reference", DOC, 0, + "APS5_DOCUMENTED is still read by the code but this pull request removes its row")]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_conventions_git.py b/tools/tests/test_check_conventions_git.py new file mode 100644 index 0000000000..5502d752d0 --- /dev/null +++ b/tools/tests/test_check_conventions_git.py @@ -0,0 +1,41 @@ + +import subprocess +import sys +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +CHECKER = REPO_ROOT / "tools" / "check_conventions.py" +INVALID_REVISION = "this-branch-does-not-exist" + + +class GitErrorHandlingTests(unittest.TestCase): + def check_invalid_revision(self, option): + valid = "--head" if option == "--base" else "--base" + arguments = [valid, "HEAD", option, INVALID_REVISION] + result = subprocess.run( + [ + sys.executable, + str(CHECKER), + *arguments, + ], + cwd=REPO_ROOT, + capture_output=True, + text=True, + ) + + self.assertEqual(result.returncode, 2) + self.assertIn("Git command failed:", result.stderr) + self.assertIn(INVALID_REVISION, result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_invalid_base_branch(self): + self.check_invalid_revision("--base") + + def test_invalid_head_branch(self): + self.check_invalid_revision("--head") + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_conventions_pr_template.py b/tools/tests/test_check_conventions_pr_template.py new file mode 100644 index 0000000000..51412f21dd --- /dev/null +++ b/tools/tests/test_check_conventions_pr_template.py @@ -0,0 +1,129 @@ +import os +import re +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +CHECKER = ROOT / "tools" / "check_conventions.py" +TEMPLATE = ROOT / ".github" / "pull_request_template.md" +PREFIX = "(pull request): error: [pr-template] Fill in the pull request template: " + + +def body(checklist, what="Fix x.", tested="Not tested."): + return f"### What\n{what}\n\n### Tested\n{tested}\n\n### Checklist\n{checklist}" + + +def filled_template(): + answers = {"Depends on": "none", "Unblocks": "none", "GPUs tested": "n/a", "AI-assisted": "yes"} + text = TEMPLATE.read_text(encoding="utf-8").replace("- [ ]", "- [x]") + for label, answer in answers.items(): + text, count = re.subn(rf"({re.escape(label)}:) ", rf"\1 {answer}", text) + if count != 1: + raise AssertionError(f"the template has no single '{label}:' answer placeholder") + text = text.replace("### What\n", "### What\nFix x.\n", 1) + return re.sub(r"(### Tested\n)", r"\1\nNot tested.", text, count=1, flags=re.S) + + +class PullRequestTemplateTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.directory = tempfile.TemporaryDirectory() + cls.root = Path(cls.directory.name) + for args in (["init", "-q"], ["add", "."], ["commit", "-qm", "docs: add readme"]): + if args[0] == "add": + (cls.root / "README.md").write_text("# Example\n", encoding="utf-8") + subprocess.run(["git", "-c", "user.name=Conventions Test", "-c", "user.email=test@example.com", + "-c", "commit.gpgsign=false", *args], cwd=cls.root, capture_output=True, check=True) + + @classmethod + def tearDownClass(cls): + cls.directory.cleanup() + + def run_checker(self, text, title="fix: x"): + (self.root / "body.md").write_text(text, encoding="utf-8") + result = subprocess.run( + [sys.executable, "-I", str(CHECKER), "--base", "HEAD", "--head", "HEAD", "--title", title, "--body", "body.md"], + cwd=self.root, capture_output=True, text=True, encoding="utf-8", + env={key: value for key, value in os.environ.items() + if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")}, + ) + findings = [line[len(PREFIX):] for line in result.stdout.splitlines() if line.startswith(PREFIX)] + return result, findings + + def assert_passes(self, text, title="fix: x"): + result, _ = self.run_checker(text, title) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def assert_findings(self, text, expected): + result, findings = self.run_checker(text) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertEqual(findings, expected, result.stdout) + + def test_filled_template_passes(self): + self.assert_passes(filled_template()) + + def test_untouched_template_reports_every_gap(self): + template = TEMPLATE.read_text(encoding="utf-8") + items = re.findall(r"^- \[ \] (.+)$", re.sub(r"", "", template, flags=re.S), re.M) + self.assertTrue(items) + result, findings = self.run_checker(template) + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn("section 'What' is missing or empty", findings) + self.assertIn("section 'Tested' is missing or empty", findings) + for item in items: + self.assertIn(f"unchecked: {item.strip()}", findings) + self.assertIn("answer AI-assisted with yes or no", findings) + self.assertIn("fill Depends on with #N or none", findings) + + def test_one_unchecked_item_is_named(self): + self.assert_findings(body("- [x] Depends on: none\n- [ ] One topic per PR\n- [x] AI-assisted: yes\n"), + ["unchecked: One topic per PR"]) + + def test_unchecked_items_with_other_list_markers(self): + self.assert_findings(body("* [ ] First\n+ [ ] Second\n- [x] Depends on: none\n- [x] AI-assisted: no\n"), + ["unchecked: First", "unchecked: Second"]) + + def test_unchecked_box_outside_the_checklist_is_ignored(self): + self.assert_passes(body("- [x] Depends on: none\n- [x] AI-assisted: yes\n", tested="- [ ] Windows\n")) + + def test_ai_assisted_answers(self): + for answer in ("yes", "no", "Yes", "NO"): + with self.subTest(answer=answer): + self.assert_passes(body(f"- [x] Depends on: none\n- [x] AI-assisted: {answer}\n")) + for answer in ("", "maybe", "", "yesterday"): + with self.subTest(answer=answer): + self.assert_findings(body(f"- [x] Depends on: none\n- [x] AI-assisted: {answer}\n"), + ["answer AI-assisted with yes or no"]) + + def test_depends_on_answers(self): + for answer in ("none", "#123", "None"): + with self.subTest(answer=answer): + self.assert_passes(body(f"- [x] Depends on: {answer}\n- [x] AI-assisted: yes\n")) + for answer in ("", "later", "123", ""): + with self.subTest(answer=answer): + self.assert_findings(body(f"- [x] Depends on: {answer}\n- [x] AI-assisted: yes\n"), + ["fill Depends on with #N or none"]) + + def test_sections_must_have_content(self): + checklist = "- [x] Depends on: none\n- [x] AI-assisted: yes\n" + self.assert_findings(body(checklist, what=""), ["section 'What' is missing or empty"]) + self.assert_findings(body(checklist, tested=" \n"), ["section 'Tested' is missing or empty"]) + self.assert_findings(body(checklist).replace("### What", "#### What"), ["section 'What' is missing or empty"]) + + def test_title_must_be_conventional(self): + text = body("- [x] Depends on: none\n- [x] AI-assisted: yes\n") + for title in ("fix: x", "test(cli): cover options", "feat(relinker)!: drop flag"): + with self.subTest(title=title): + self.assert_passes(text, title) + for title in ("Fix x", "fix:x", "update(cli): x", ""): + with self.subTest(title=title): + result, _ = self.run_checker(text, title) + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn("[pr-title] Pull request title is not Conventional Commits", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_conventions_unknown_nids.py b/tools/tests/test_check_conventions_unknown_nids.py new file mode 100644 index 0000000000..f068a4edfc --- /dev/null +++ b/tools/tests/test_check_conventions_unknown_nids.py @@ -0,0 +1,129 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from nid_names import compute_nid + +CHECKER = Path(__file__).resolve().parents[1] / "check_conventions.py" +HELPER_NID = compute_nid("Helper") + + +def unknown_export(nid): + return (f'APS5_EXPORT("{nid}", sceBUnknown_{nid});\n' + f"int APS5_VABI sceBUnknown_{nid}() {{\n NotImplemented_nid_no_patch(__func__);\n return 0;\n}}\n") + + +class UnknownNidTests(unittest.TestCase): + def check(self, base_files, head_files): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + + def git(*args): + return subprocess.run( + ["git", "-c", f"safe.directory={root}", "-c", "user.name=Conventions Test", + "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + cwd=root, capture_output=True, text=True, check=True, + ) + + def commit(files, message): + for name, text in files.items(): + path = root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + git("add", ".") + git("commit", "-qm", message) + + git("init", "-q") + commit(base_files, "feat: add the base libraries") + commit(head_files, "feat: change a library") + env = {key: value for key, value in os.environ.items() + if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")} + env.update(GIT_CONFIG_COUNT="1", GIT_CONFIG_KEY_0="safe.directory", GIT_CONFIG_VALUE_0=str(root)) + return subprocess.run( + [sys.executable, "-I", str(CHECKER), "--base", "HEAD~1", "--head", "HEAD"], + cwd=root, capture_output=True, text=True, env=env, timeout=20, + ) + + def assertUnknownNid(self, result, symbol): + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("[unknown-nid]", result.stdout) + self.assertIn(symbol, result.stdout) + + def assertPasses(self, result): + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotIn("[unknown-nid]", result.stdout) + + def test_unknown_export_with_the_nid_of_a_helper_in_another_library_fails(self): + result = self.check( + {"core/libs/prx/libSceA/Export.cpp": 'extern "C" bool Helper_nid_postfix() {\n return true;\n}\n'}, + {"core/libs/prx/libSceB/Unimplemented.cpp": unknown_export(HELPER_NID)}, + ) + self.assertUnknownNid(result, f"{HELPER_NID} (sceBUnknown_{HELPER_NID}) is Helper_nid_postfix at core/libs/prx/libSceA/Export.cpp:1") + + def test_unknown_export_with_the_nid_of_an_import_declaration_fails(self): + result = self.check( + {"core/libs/prx/libSceB/include/Helper.hpp": 'extern "C" bool Helper_nid_postfix();\n'}, + {"core/libs/prx/libSceB/Unimplemented.cpp": unknown_export(HELPER_NID)}, + ) + self.assertUnknownNid(result, "Helper_nid_postfix at core/libs/prx/libSceB/include/Helper.hpp:1") + + def test_unknown_export_with_the_nid_of_a_named_export_fails(self): + result = self.check( + {"core/libs/prx/libSceA/Export.cpp": "int APS5_VABI sceAFoo() {\n return 0;\n}\n"}, + {"core/libs/prx/libSceB/Unimplemented.cpp": unknown_export(compute_nid("sceAFoo"))}, + ) + self.assertUnknownNid(result, "sceAFoo at core/libs/prx/libSceA/Export.cpp:1") + + def test_unknown_export_with_the_nid_of_an_assembly_function_fails(self): + result = self.check( + {"core/libs/prx/libSceA/Asm.cpp": 'asm(\n APS5_ASM_FUNCTION("Helper_nid_postfix")\n "ret\\n");\n'}, + {"core/libs/prx/libSceB/Unimplemented.cpp": unknown_export(HELPER_NID)}, + ) + self.assertUnknownNid(result, "Helper_nid_postfix at core/libs/prx/libSceA/Asm.cpp:2") + + def test_unknown_export_with_the_nid_of_a_disambiguated_helper_fails(self): + result = self.check( + {"core/libs/prx/libSceA/Export.cpp": 'extern "C" bool Helper_nid_disambig1_nid_postfix() {\n return true;\n}\n'}, + {"core/libs/prx/libSceB/Unimplemented.cpp": unknown_export(HELPER_NID)}, + ) + self.assertUnknownNid(result, "Helper_nid_disambig1_nid_postfix") + + def test_helper_with_the_nid_of_an_unknown_export_fails(self): + result = self.check( + {"core/libs/prx/libSceB/Unimplemented.cpp": unknown_export(HELPER_NID)}, + {"core/libs/prx/libSceA/Export.cpp": 'extern "C" bool Helper_nid_postfix() {\n return true;\n}\n'}, + ) + self.assertUnknownNid(result, f"Helper_nid_postfix has the NID {HELPER_NID} of sceBUnknown_{HELPER_NID} at core/libs/prx/libSceB/Unimplemented.cpp:1") + + def test_unknown_export_with_an_unused_nid_passes(self): + self.assertPasses(self.check( + {"core/libs/prx/libSceA/Export.cpp": 'extern "C" bool Helper_nid_postfix() {\n return true;\n}\n'}, + {"core/libs/prx/libSceB/Unimplemented.cpp": unknown_export(compute_nid("sceBStillUnknown"))}, + )) + + def test_no_patch_helper_does_not_take_the_nid(self): + self.assertPasses(self.check( + {"core/libs/prx/libSceB/Unimplemented.cpp": unknown_export(HELPER_NID)}, + {"core/libs/prx/libSceA/Export.cpp": 'extern "C" bool Helper_nid_no_patch() {\n return true;\n}\n'}, + )) + + def test_named_alias_of_another_library_export_passes(self): + nid = compute_nid("sceAFoo") + self.assertPasses(self.check( + {"core/libs/prx/libSceA/Export.cpp": "int APS5_VABI sceAFoo() {\n return 0;\n}\n"}, + {"core/libs/prx/libSceB/Export.cpp": f'APS5_EXPORT("{nid}", libSceB_sceAFoo);\nint APS5_VABI libSceB_sceAFoo() {{\n return 0;\n}}\n'}, + )) + + def test_symbols_in_library_tests_are_ignored(self): + self.assertPasses(self.check( + {"core/libs/prx/libSceA/tests/Helper.cpp": 'extern "C" bool Helper_nid_postfix() {\n return true;\n}\n'}, + {"core/libs/prx/libSceB/Unimplemented.cpp": unknown_export(HELPER_NID)}, + )) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_conventions_wrapped_declarations.py b/tools/tests/test_check_conventions_wrapped_declarations.py index 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 new file mode 100644 index 0000000000..22b06de59d --- /dev/null +++ b/tools/tests/test_check_main_sync.py @@ -0,0 +1,208 @@ +from pathlib import Path +import contextlib +import io +import os +import subprocess +import sys +import tempfile +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import check_main_sync + +TOOL = Path(__file__).resolve().parents[1] / "check_main_sync.py" +IDENTITY = { + "GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@example.com", + "GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@example.com", +} + + +class Repo: + def __init__(self, path): + self.path = path + self.git("init", "-q", "-b", "main") + self.write("shared.txt", "one\ntwo\nthree\n") + self.write("other.txt", "x\n") + self.commit("initial") + + def git(self, *args): + subprocess.run(["git", "-C", str(self.path), *args], check=True, capture_output=True, env={**os.environ, **IDENTITY}) + + def write(self, name, text): + (self.path / name).write_text(text, encoding="utf-8") + + def commit(self, message): + self.git("add", "-A") + self.git("commit", "-q", "-m", message) + + def merge_pull(self, number, branch): + self.git("merge", "--no-ff", "-q", "-m", f"Merge pull request #{number} from someone/{branch}", branch) + + +def run_tool(path, *args): + return subprocess.run( + [sys.executable, str(TOOL), "--repo", str(path), "--base", "main", *args], + capture_output=True, + encoding="utf-8", + env={**os.environ, "PYTHONIOENCODING": "utf-8"}, + ) + + +class CheckMainSyncTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.repo = Repo(Path(self.directory.name)) + + def tearDown(self): + self.directory.cleanup() + + def branch(self, name): + self.repo.git("checkout", "-q", "-b", name) + + def test_branch_level_with_main_is_clean_and_not_behind(self): + self.branch("feature") + self.repo.write("other.txt", "y\n") + self.repo.commit("feature change") + data = check_main_sync.report(self.repo.path, "main", "feature") + self.assertEqual((data["behind"], data["ahead"], data["conflicts"], data["shared"]), (0, 1, [], [])) + self.assertEqual(run_tool(self.repo.path, "--head", "feature", "--strict").returncode, 0) + + def test_main_with_explicit_argv(self): + self.branch("feature") + self.repo.write("other.txt", "y\n") + self.repo.commit("feature change") + with contextlib.redirect_stdout(io.StringIO()): + code = check_main_sync.main(["--repo", str(self.repo.path), "--base", "main", "--head", "feature"]) + self.assertEqual(code, 0) + + def test_behind_main_is_reported_and_only_fails_when_strict(self): + self.branch("feature") + self.repo.write("other.txt", "y\n") + self.repo.commit("feature change") + self.repo.git("checkout", "-q", "main") + self.repo.write("extra.txt", "new\n") + self.repo.commit("main moves") + relaxed = run_tool(self.repo.path, "--head", "feature") + strict = run_tool(self.repo.path, "--head", "feature", "--strict") + self.assertEqual(relaxed.returncode, 0, relaxed.stderr) + self.assertIn("1 ahead and 1 behind main", relaxed.stdout) + self.assertIn("merge: clean", relaxed.stdout) + self.assertEqual(strict.returncode, 1) + + def test_conflict_names_the_file_and_the_merged_pull_request(self): + self.branch("feature") + self.repo.write("shared.txt", "one\nmine\nthree\n") + self.repo.commit("feature edit") + self.repo.git("checkout", "-q", "main") + self.repo.git("checkout", "-q", "-b", "theirs") + self.repo.write("shared.txt", "one\ntheirs\nthree\n") + self.repo.commit("their edit") + self.repo.git("checkout", "-q", "main") + self.repo.merge_pull(4242, "theirs") + result = run_tool(self.repo.path, "--head", "feature") + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn("merge: CONFLICT in 1 file(s)", result.stdout) + self.assertIn(" shared.txt", result.stdout) + self.assertIn("merged pull requests that touched them: #4242", result.stdout) + + def test_files_changed_on_both_sides_are_listed_when_the_merge_is_clean(self): + self.branch("feature") + self.repo.write("shared.txt", "zero\none\ntwo\nthree\n") + self.repo.commit("feature edit") + self.repo.git("checkout", "-q", "main") + self.repo.git("checkout", "-q", "-b", "theirs") + self.repo.write("shared.txt", "one\ntwo\nthree\nfour\n") + self.repo.commit("their edit") + self.repo.git("checkout", "-q", "main") + self.repo.merge_pull(77, "theirs") + result = run_tool(self.repo.path, "--head", "feature") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("merge: clean", result.stdout) + self.assertIn("1 file(s) changed on both sides", result.stdout) + self.assertIn("#77", result.stdout) + + def test_paths_with_spaces_and_unicode_are_reported_verbatim(self): + name = "sp ace \u00e9.txt" + self.repo.write(name, "one\ntwo\n") + self.repo.commit("add") + self.branch("feature") + self.repo.write(name, "one\nmine\n") + self.repo.commit("feature edit") + self.repo.git("checkout", "-q", "main") + self.repo.git("checkout", "-q", "-b", "theirs") + self.repo.write(name, "one\ntheirs\n") + self.repo.commit("their edit") + self.repo.git("checkout", "-q", "main") + self.repo.merge_pull(9, "theirs") + result = run_tool(self.repo.path, "--head", "feature") + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn(f" {name}\n", result.stdout.replace("\r", "")) + self.assertIn("#9", result.stdout) + + def test_unrelated_histories_are_an_error(self): + self.repo.git("checkout", "-q", "--orphan", "island") + self.repo.write("lone.txt", "x\n") + self.repo.commit("island") + result = run_tool(self.repo.path, "--head", "island") + self.assertEqual(result.returncode, 2) + self.assertIn("no common ancestor", result.stderr) + + def check_unicode_output(self, conflict): + name = "sp ace \u00e9 \u6e2c \U0001f680.txt" + self.repo.write(name, "one\ntwo\nthree\n") + self.repo.commit("add unicode path") + self.branch("feature") + self.repo.write(name, "one\nmine\nthree\n" if conflict else "zero\none\ntwo\nthree\n") + self.repo.commit("feature edit") + self.repo.git("checkout", "-q", "main") + self.repo.git("checkout", "-q", "-b", "theirs") + self.repo.write(name, "one\ntheirs\nthree\n" if conflict else "one\ntwo\nthree\nfour\n") + self.repo.commit("their edit") + self.repo.git("checkout", "-q", "main") + self.repo.merge_pull(9, "theirs") + expected = {"ascii": r"sp ace \xe9 \u6e2c \U0001f680.txt", + "cp1252": "sp ace \u00e9 " + r"\u6e2c \U0001f680.txt", + "utf-8": name} + for encoding, shown in expected.items(): + for strict in ((False,) if conflict else (False, True)): + with self.subTest(encoding=encoding, strict=strict): + result = subprocess.run([sys.executable, str(TOOL), "--repo", str(self.repo.path), + "--base", "main", "--head", "feature", *(["--strict"] if strict else [])], + capture_output=True, text=True, encoding=encoding, + env={**os.environ, "PYTHONIOENCODING": encoding + ":strict"}) + self.assertEqual(result.returncode, 1 if conflict or strict else 0, result.stderr) + self.assertIn("merge: CONFLICT" if conflict else "merge: clean", result.stdout) + self.assertIn(f" {shown}\n", result.stdout.replace("\r", "")) + self.assertIn("merged pull requests that touched them: #9", result.stdout) + self.assertNotIn("Traceback", result.stderr) + + def test_clean_unicode_report_preserves_exit_status_and_output_encoding(self): + self.check_unicode_output(False) + + def test_conflicting_unicode_report_preserves_exit_status_and_output_encoding(self): + self.check_unicode_output(True) + + def test_missing_base_is_an_error_not_a_pass(self): + result = subprocess.run([sys.executable, str(TOOL), "--repo", self.directory.name, "--base", "upstream/main"], capture_output=True, text=True) + 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) + + def test_default_base_is_fetched_before_it_is_resolved(self): + with tempfile.TemporaryDirectory() as other: + subprocess.run(["git", "clone", "-q", self.directory.name, other], check=True, capture_output=True) + self.repo.git("remote", "add", "upstream", other) + result = subprocess.run([sys.executable, str(TOOL), "--repo", self.directory.name, "--fetch"], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("merge: clean", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index c2a4c33b79..ea82a476d1 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -1,4 +1,6 @@ from pathlib import Path +import contextlib +import io import subprocess import sys import tempfile @@ -143,6 +145,64 @@ class CheckSkipsTests(unittest.TestCase): self.assertIn("error: odd skipped", result.stdout) self.assertNotIn("error: geometry skipped", result.stdout) + def test_reason_on_its_own_line_inside_skipped_is_expected(self): + for reason, job in (("the device lacks geometryShader", "linux"), ("no display", "windows")): + with self.subTest(job=job), tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text(f'\n{reason}\n', encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--job", job], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("all expected", result.stdout) + + def test_no_usable_vulkan_device_is_expected_in_both_jobs(self): + reason = "no usable Vulkan device: ErrorInitializationFailed" + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text(f'skipped, {reason}', encoding="utf-8") + for job in ("linux", "windows"): + with self.subTest(job=job): + result = subprocess.run( + [sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("all expected", result.stdout) + + def test_device_feature_limitation_stays_unexpected_on_windows(self): + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text('skipped, the device lacks shaderSubgroupClock', encoding="utf-8") + result = subprocess.run( + [sys.executable, "-I", str(TOOL), str(junit), "--job", "windows"], + capture_output=True, text=True) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("error: shader_subgroup skipped", result.stdout) + + def test_list_reports_skipped_reasons_including_indented_lines(self): + xml = ( + '' + '' + '\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) + + def test_main_with_explicit_argv(self): + xml = '' + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text(xml, encoding="utf-8") + with contextlib.redirect_stdout(io.StringIO()): + code = check_skips.main([str(junit), "--job", "linux"]) + self.assertEqual(code, 0) + diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py new file mode 100644 index 0000000000..785b724136 --- /dev/null +++ b/tools/tests/test_configure_seed.py @@ -0,0 +1,234 @@ +import os +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") +""" + + +class ConfigureSeedPathTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name) + self.cache = self.root / "CMakeCache.txt" + self.original = b"//Path to a program.\nCMAKE_AR:FILEPATH=/usr/bin/ar\n//Test HAVE_FEATURE\nHAVE_FEATURE:INTERNAL=1\n" + self.cache.write_bytes(self.original) + + def run_seed(self, seed, *options): + return subprocess.run([sys.executable, str(TOOL), str(self.cache), str(seed), *options], + capture_output=True, text=True) + + def assert_rejected(self, seed, *options): + result = self.run_seed(seed, *options) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertEqual(result.stdout, "") + self.assertIn("seed output would overwrite the input cache", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(self.cache.read_bytes(), self.original) + self.assertEqual(Path(seed).read_bytes(), self.original) + + def test_unreadable_cache_is_reported_without_a_traceback(self): + missing = self.root / "no cache dir" / "CMakeCache.txt" + result = subprocess.run([sys.executable, str(TOOL), str(missing), str(self.root / "seed.cmake")], + capture_output=True, text=True) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn(str(missing), result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertFalse((self.root / "seed.cmake").exists()) + + def test_missing_seed_directory_is_created(self): + seed = self.root / "new dir" / "nested" / "seed.cmake" + result = self.run_seed(seed) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("HAVE_FEATURE", seed.read_text(encoding="utf-8")) + + def test_unwritable_seed_is_reported_without_a_traceback(self): + seed_dir = self.root / "seed_dir" + seed_dir.mkdir() + result = self.run_seed(seed_dir) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn(str(seed_dir), result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_same_path_preserves_cache(self): + self.assert_rejected(self.cache) + + def test_normalized_path_preserves_cache(self): + (self.root / "nested").mkdir() + self.assert_rejected(self.root / "nested" / ".." / self.cache.name) + + def test_hard_link_preserves_cache(self): + seed = self.root / "seed.cmake" + os.link(self.cache, seed) + self.assert_rejected(seed) + + def test_symbolic_link_preserves_cache(self): + seed = self.root / "seed.cmake" + try: + seed.symlink_to(self.cache) + except OSError as error: + self.skipTest(f"symbolic links unavailable: {error}") + self.assert_rejected(seed) + + def test_distinct_output_preserves_cache(self): + seed = self.root / "seed.cmake" + for existing in (False, True): + with self.subTest(existing=existing): + if existing: + seed.write_text("old seed", encoding="utf-8") + result = self.run_seed(seed) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "1 check results\n") + self.assertEqual(seed.read_text(encoding="utf-8"), + 'set("HAVE_FEATURE" "1" CACHE INTERNAL "Test HAVE_FEATURE")\n') + self.assertEqual(self.cache.read_bytes(), self.original) + + def test_filters_do_not_allow_overwriting_cache(self): + for options in (("--include", "HAVE_*"), ("--include", "NO_MATCH_*"), ("--exclude", "HAVE_*")): + with self.subTest(options=options): + self.assert_rejected(self.cache, *options) + + def test_filters_preserve_cache_with_distinct_output(self): + self.original += b"//Test OTHER_CHECK\nOTHER_CHECK:INTERNAL=0\n" + self.cache.write_bytes(self.original) + seed = self.root / "seed.cmake" + feature = 'set("HAVE_FEATURE" "1" CACHE INTERNAL "Test HAVE_FEATURE")\n' + other = 'set("OTHER_CHECK" "0" CACHE INTERNAL "Test OTHER_CHECK")\n' + cases = ( + (("--include", "HAVE_*", "--include", "OTHER_*"), feature + other, 2), + (("--include", "*", "--exclude", "OTHER_*"), feature, 1), + (("--include", "have_*"), "", 0), + ) + for options, expected, count in cases: + with self.subTest(options=options): + result = self.run_seed(seed, *options) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, f"{count} check results\n") + self.assertEqual(seed.read_text(encoding="utf-8"), expected) + self.assertEqual(self.cache.read_bytes(), self.original) + + +@unittest.skipUnless(shutil.which("ninja") and (shutil.which("gcc") or shutil.which("cc")), "needs ninja and a C compiler") +class ConfigureSeedProject(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.directory = tempfile.TemporaryDirectory() + 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 + + +class ConfigureSeedTests(ConfigureSeedProject): + def test_only_check_results_are_seeded(self): + found = configure_seed.checks(self.root / "fresh" / "CMakeCache.txt") + self.assertEqual({n for n in found if n.startswith("MINI_")}, + {"MINI_HAVE_STDIO_H", "MINI_HAVE_MISSING_H", "MINI_HAVE_PRINTF", "MINI_COMPILES", "MINI_FAILS"}) + self.assertNotIn("CMAKE_C_COMPILER", found) + + def test_cache_without_checks_writes_an_empty_seed(self): + cache = self.root / "empty" / "CMakeCache.txt" + cache.parent.mkdir() + cache.write_text("//Path to a program.\nCMAKE_AR:FILEPATH=/usr/bin/ar\n", encoding="utf-8") + self.assertEqual(self.seed(cache, "empty.cmake").read_text(encoding="utf-8"), "") + + def test_seed_filters_leave_excluded_checks_to_the_new_configure(self): + seed = self.root / "filtered.cmake" + result = subprocess.run([sys.executable, str(TOOL), str(self.root / "fresh" / "CMakeCache.txt"), str(seed), + "--include", "MINI_HAVE_*", "--include", "MINI_COMPILES", "--exclude", "*MISSING*"], + check=True, capture_output=True, text=True) + self.assertIn("3 check results", result.stdout) + seeded = seed.read_text(encoding="utf-8") + self.assertIn("MINI_HAVE_STDIO_H", seeded) + self.assertIn("MINI_HAVE_PRINTF", seeded) + self.assertIn("MINI_COMPILES", seeded) + self.assertNotIn("MINI_HAVE_MISSING_H", seeded) + self.assertNotIn("MINI_FAILS", seeded) + output = self.configure("filter-seeded", "-C", str(seed)) + self.assertIn("Looking for mini_missing_header.h", output) + self.assertNotIn("Looking for stdio.h", output) + self.assertNotIn("Performing Test MINI_COMPILES", output) + self.assertEqual(configure_seed.checks(self.root / "filter-seeded" / "CMakeCache.txt"), + configure_seed.checks(self.root / "fresh" / "CMakeCache.txt")) + subprocess.run([sys.executable, str(TOOL), str(self.root / "fresh" / "CMakeCache.txt"), str(seed), + "--include", "no_match_*"], check=True, capture_output=True) + self.assertEqual(seed.read_text(encoding="utf-8"), "") + + +class ConfigureSeedRoundTripTests(ConfigureSeedProject): + def test_check_names_with_spaces_round_trip_through_cmake(self): + cache = self.root / "named-check.txt" + cache.write_text('//Test HAVE FEATURE\nHAVE FEATURE:INTERNAL=1\n', encoding="utf-8") + output = self.configure("named-check", "-C", str(self.seed(cache, "named-check.cmake"))) + self.assertNotRegex(output, r"CMake Error") + seeded = self.root / "named-check" / "CMakeCache.txt" + self.assertEqual(configure_seed.checks(seeded)["HAVE FEATURE"][0], "1") + self.assertNotIn("HAVE:INTERNAL=", seeded.read_text(encoding="utf-8")) + + def test_seeded_configure_runs_no_check_and_gets_the_same_results(self): + self.assertRegex(self.fresh, r"Looking for|Performing Test") + output = self.configure("seeded", "-C", str(self.seed(self.root / "fresh" / "CMakeCache.txt", "seed.cmake"))) + 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_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) + + + +class ChecksParsingTests(unittest.TestCase): + def test_checks_reads_cache_with_utf8_bom(self): + with tempfile.TemporaryDirectory() as directory: + cache = Path(directory) / "CMakeCache.txt" + cache.write_bytes(b"\xef\xbb\xbf//Test FIRST_CHECK\nFIRST_CHECK:INTERNAL=1\n") + found = configure_seed.checks(cache) + self.assertEqual(found, {"FIRST_CHECK": ("1", "Test FIRST_CHECK")}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_debt_split.py b/tools/tests/test_debt_split.py index 9bdceffa98..1f2cf31775 100644 --- a/tools/tests/test_debt_split.py +++ b/tools/tests/test_debt_split.py @@ -5,6 +5,9 @@ import tempfile import unittest from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import debt_split + TOOL = Path(__file__).resolve().parents[1] / "debt_split.py" INDEX = "docs/dev/TechnicalDebt.md" @@ -25,6 +28,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) @@ -59,6 +73,20 @@ class DebtSplitTests(unittest.TestCase): self.git("add", "-A") self.git("commit", "-qm", "docs: split technical debt") + def test_split_preserves_external_urls_and_rewrites_local_links(self): + links = ["HTTPS://example.test/guide#intro", "//example.test/guide#intro", + "https://example.test/guide", "MAILTO:author@example.test"] + references = " ".join(f"[reference]({target})" for target in links) + self.write(INDEX, ORIGINAL.replace(A_STUB, A_STUB + " " + references + + " [local](ARCHITECTURE.md#layout)")) + self.split() + moved = self.read("docs/dev/debt/libSceA.md") + for target in links: + with self.subTest(target=target): + self.assertIn(f"[reference]({target})", moved) + self.assertIn("[local](../ARCHITECTURE.md#layout)", moved) + self.assertIn("../../../core/libs/prx/libSceA/Export.cpp", moved) + def test_split_moves_entries_by_their_first_core_link(self): self.split() self.assertEqual(self.read("docs/dev/debt/libSceA.md"), @@ -70,6 +98,8 @@ class DebtSplitTests(unittest.TestCase): self.assertNotIn(entry.split(" - ")[0], text) for kept in ("- A toolchain limitation", "Everything throws, except:", SHARED, "### Silent stubs", "[debt](debt/)"): self.assertIn(kept, text) + for path in (INDEX, "docs/dev/debt/libSceA.md"): + self.assertNotIn(b"\r", (self.root / path).read_bytes()) def test_split_again_appends_to_existing_files(self): self.split() @@ -102,6 +132,8 @@ class DebtSplitTests(unittest.TestCase): self.assertEqual(sorted(result.stdout.split()), ["docs/dev/debt/libSceA.md", "docs/dev/debt/libSceB.md", "docs/dev/debt/libSceC.md"]) self.assertEqual(self.git("diff", "--name-only", "--diff-filter=U").stdout, "") + for path in (INDEX, "docs/dev/debt/libSceA.md"): + self.assertNotIn(b"\r", (self.root / path).read_bytes()) self.git("rebase", "--continue") a = self.read("docs/dev/debt/libSceA.md") self.assertLess(a.index("`sceAOne`"), a.index("`sceAFour`")) @@ -169,6 +201,46 @@ class DebtSplitTests(unittest.TestCase): "\n\n### Functional\n\n" + A_FUNCTIONAL.replace("../../", "../../../") + "\n") self.assertEqual(self.read(INDEX), self.git("show", "main:" + INDEX).stdout) + def test_resolve_places_an_entry_inserted_at_the_top_of_a_later_section(self): + added = "- [libSceA](../../core/libs/prx/libSceA/Added.cpp) - `sceAAdded` returns 0" + result = self.rebase([A_STUB, B_STUB, C_STUB], [added, A_FUNCTIONAL, SHADER, SHARED]) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.git("rebase", "--continue") + self.assertEqual(self.read("docs/dev/debt/libSceA.md"), + "# libSceA technical debt\n\n### Silent stubs\n\n" + A_STUB.replace("../../", "../../../") + + "\n\n### Functional\n\n" + + "\n".join(entry.replace("../../", "../../../") for entry in (added, A_FUNCTIONAL)) + "\n") + + def test_indented_continuation_after_a_blank_line_moves_with_its_entry(self): + note = " - sub note about sceAOne" + self.write(INDEX, "# Project technical debt\n\n### Silent stubs\n\n" + A_STUB + "\n\n" + note + "\n") + self.split() + self.assertIn(note, self.read("docs/dev/debt/libSceA.md")) + self.assertNotIn(note, self.read(INDEX)) + + def test_unindented_paragraph_after_a_blank_line_stays_in_the_index(self): + paragraph = "A plain paragraph after the stubs" + self.write(INDEX, "# Project technical debt\n\n### Silent stubs\n\n" + A_STUB + "\n\n" + paragraph + "\n") + self.split() + self.assertIn(paragraph, self.read(INDEX)) + self.assertNotIn(paragraph, self.read("docs/dev/debt/libSceA.md")) + + def test_read_debt_and_split_accept_utf8_bom(self): + self.write("docs/dev/debt/libSceA.md", "\ufeff### Silent stubs\n\n- entry 1\n") + sections = debt_split.read_debt(self.root, "libSceA") + self.assertEqual(sections, {"Silent stubs": ["- entry 1"]}) + self.write(INDEX, "\ufeff" + ORIGINAL) + self.split() + self.assertTrue((self.root / "docs/dev/debt/libSceA.md").is_file()) + self.assertIn("libSceA technical debt", self.read("docs/dev/debt/libSceA.md")) + + def test_split_keeps_blank_line_between_staying_entry_and_paragraph(self): + paragraph = "A paragraph that belongs to no library." + self.write(INDEX, "# Project technical debt\n\n### Silent stubs\n\n" + A_STUB + + "\n- A toolchain limitation\n\n" + paragraph + "\n") + self.split() + self.assertIn("- A toolchain limitation\n\n" + paragraph + "\n", self.read(INDEX)) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py new file mode 100644 index 0000000000..c414e314c0 --- /dev/null +++ b/tools/tests/test_env_reference.py @@ -0,0 +1,135 @@ +import contextlib +import io +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import env_reference + +BOTH = ("APS5_", "ANYPS5_") + +CODE = """static bool a = std::getenv("APS5_PROFILE_DRAW") != nullptr; +static bool b = IsEnvironmentSet("APS5_TRACE_GPU"); +static unsigned long c = kibSetting("APS5_HOST_IMPORT_MIB", 6144); +static unsigned long d = MaskFromEnvironment("APS5_WORKER_AFFINITY_MASK"); +static const char* e = std::getenv("ANYPS5_GPU"); +static const char* f = std::getenv("PATH"); +static const char* g = std::getenv("TZ"); +static int h = EnvInt("APS5_THREAD_NICE", -10); +static int i = GetEnvironmentVariableA("APS5_CRASH_INDIRECT_MEMORY", nullptr, 0); +static unsigned long j = configured("APS5_PIPELINE_JOURNAL_MAX_MIB", 1024u) << 20u; +static unsigned long k = PipelineSwitch("APS5_PIPELINE_STORE_ENTRIES", 1024u); +static unsigned long l = configured("APS5_PIPELINE_JOURNAL_THREADS", std::max(1u, hardware / 2u)); +static std::size_t m = environmentCount("APS5_PM4_RING", 16, MaximumCapacity, "APS5_PM4_RING must be a number"); +static int n = GetEnvironmentVariableW(L"ANYPS5_LOADEXEC_ARGV_SKIP", skip, 2); +static const auto* o = addWideString(u"ANYPS5_LOADEXEC_PARENT"); +static unsigned long p = reconfigured("APS5_NOT_A_READ", 1); +static bool q = _wgetenv(L"APS5_WIDE_PRESENT") != nullptr; +static int r = _wtoi(_wgetenv(L"APS5_WIDE_INT")); +static const wchar_t* s = _wgetenv(L"ANYPS5_WIDE_CONFIG"); +static const char* t = std::getenv("NOT_AN_ENV"); +""" + + +class EnvReferenceTests(unittest.TestCase): + def fixture(self): + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = Path(directory.name) + (root / "core").mkdir() + (root / "core" / "Sample.cpp").write_text(CODE, encoding="utf-8") + return root + + def test_reads_find_every_reader_form_and_ignore_other_names(self): + root = self.fixture() + found = env_reference.reads([root / "core"], BOTH) + self.assertEqual( + set(found), + {"APS5_PROFILE_DRAW", "APS5_TRACE_GPU", "APS5_HOST_IMPORT_MIB", + "APS5_WORKER_AFFINITY_MASK", "ANYPS5_GPU", "APS5_THREAD_NICE", + "APS5_CRASH_INDIRECT_MEMORY", "APS5_PIPELINE_JOURNAL_MAX_MIB", + "APS5_PIPELINE_STORE_ENTRIES", "APS5_PIPELINE_JOURNAL_THREADS", "APS5_PM4_RING", + "ANYPS5_LOADEXEC_ARGV_SKIP", "ANYPS5_LOADEXEC_PARENT", + "ANYPS5_WIDE_CONFIG", "APS5_WIDE_PRESENT", "APS5_WIDE_INT"}) + self.assertEqual(found["APS5_PROFILE_DRAW"][0][1], 1) + self.assertEqual(found["APS5_THREAD_NICE"][0][2:], ("integer", "-10")) + self.assertEqual(found["APS5_HOST_IMPORT_MIB"][0][2:], ("integer", "6144")) + self.assertEqual(found["APS5_PIPELINE_JOURNAL_MAX_MIB"][0][2:], ("integer", "1024")) + self.assertEqual(found["APS5_PIPELINE_STORE_ENTRIES"][0][2:], ("integer", "1024")) + self.assertEqual(found["APS5_PIPELINE_JOURNAL_THREADS"][0][2:], ("integer", "")) + self.assertEqual(found["APS5_PM4_RING"][0][2:], ("integer", "16")) + self.assertEqual(found["ANYPS5_LOADEXEC_ARGV_SKIP"][0][2:], ("string", "")) + self.assertEqual(found["ANYPS5_LOADEXEC_PARENT"][0][2:], ("string", "")) + self.assertEqual(found["ANYPS5_WIDE_CONFIG"][0][2:], ("string", "")) + self.assertEqual(found["APS5_WIDE_PRESENT"][0][2:], ("presence", "")) + self.assertEqual(found["APS5_WIDE_INT"][0][2:], ("integer", "")) + + def test_the_default_prefixes_cover_both(self): + root = self.fixture() + found = env_reference.reads([root / "core"]) + self.assertTrue(found) + self.assertTrue([name for name in found if name.startswith("APS5_")]) + self.assertTrue([name for name in found if name.startswith("ANYPS5_")]) + + def test_check_passes_when_every_read_name_has_a_row(self): + root = self.fixture() + found = env_reference.reads([root / "core"]) + doc = root / "ENVIRONMENT.md" + doc.write_text("\n".join("| `%s` |" % name for name in found), encoding="utf-8") + out = io.StringIO() + self.assertEqual(env_reference.check(found, doc, env_reference.PREFIXES, out), 0) + + def test_check_reports_a_read_name_without_a_row(self): + root = self.fixture() + found = env_reference.reads([root / "core"]) + doc = root / "ENVIRONMENT.md" + doc.write_text("| `APS5_PROFILE_DRAW` |", encoding="utf-8") + out = io.StringIO() + self.assertEqual(env_reference.check(found, doc, env_reference.PREFIXES, out), 1) + self.assertIn("APS5_TRACE_GPU", out.getvalue()) + self.assertIn("has no row", out.getvalue()) + + def test_check_reports_a_row_the_code_no_longer_reads(self): + root = self.fixture() + found = env_reference.reads([root / "core"]) + doc = root / "ENVIRONMENT.md" + names = ["| `%s` |" % name for name in found] + ["| `APS5_REMOVED` |"] + doc.write_text("\n".join(names), encoding="utf-8") + out = io.StringIO() + self.assertEqual(env_reference.check(found, doc, env_reference.PREFIXES, out), 1) + self.assertIn("APS5_REMOVED", out.getvalue()) + self.assertIn("no longer reads", out.getvalue()) + + def test_check_fails_when_the_document_is_missing(self): + root = self.fixture() + found = env_reference.reads([root / "core"]) + out = io.StringIO() + self.assertEqual( + env_reference.check(found, root / "missing.md", env_reference.PREFIXES, out), 1) + self.assertIn("cannot read", out.getvalue()) + + def test_ancestor_directory_named_in_skip_does_not_suppress_root_scan(self): + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + core = Path(directory.name) / "build" / "checkout" / "core" + (core / "3rdparty").mkdir(parents=True) + (core / "Sample.cpp").write_text('static bool a = IsEnvironmentSet("APS5_TRACE_GPU");\n', encoding="utf-8") + (core / "3rdparty" / "Ignored.cpp").write_text('static bool b = IsEnvironmentSet("APS5_IGNORED");\n', encoding="utf-8") + self.assertEqual(set(env_reference.reads([core], BOTH)), {"APS5_TRACE_GPU"}) + + def test_main_rejects_a_scan_root_that_is_not_a_directory(self): + root = self.fixture() + for name in ("missing", "core/Sample.cpp"): + for flags in (["--check"], ["--table"], []): + err = io.StringIO() + with contextlib.redirect_stdout(io.StringIO()) as out, contextlib.redirect_stderr(err): + code = env_reference.main(flags + ["--doc", str(root / "ENVIRONMENT.md"), "--root", str(root / name)]) + self.assertEqual(code, 1, (name, flags)) + self.assertIn("is not a directory", err.getvalue()) + self.assertNotIn("no longer reads", out.getvalue()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 5fc16352c7..a29775c13b 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(" 2: + record["weak"] = entry[2] + records.append(record) + path.write_text(json.dumps(records), encoding=encoding) class Workspace: @@ -159,19 +198,135 @@ class Workspace: def close(self): self._directory.cleanup() - def run(self, registry, *extra): + def run(self, registry, *extra, registries=()): out, err = io.StringIO(), io.StringIO() - arguments = [str(registry), "--libs", str(self.libs), "--modules", str(self.modules), "--source", str(self.source), *extra] + arguments = [str(registry), *map(str, registries), "--libs", str(self.libs), "--modules", str(self.modules), "--source", str(self.source), *extra] with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err): code = import_audit.main(arguments) return code, out.getvalue(), err.getvalue() class ImportAuditTests(unittest.TestCase): + def test_cli_error_paths_are_utf8_under_an_ascii_environment(self): + registry = self.work.root / "\u65e5\u672c\u8a9e.json" + result = subprocess.run( + [sys.executable, str(Path(import_audit.__file__)), str(registry), + "--libs", str(self.work.libs), "--source", str(self.work.source)], + env=dict(os.environ, PYTHONIOENCODING="ascii"), capture_output=True, timeout=20) + self.assertEqual(result.returncode, 2) + self.assertIn(str(registry), result.stderr.decode("utf-8")) + self.assertNotIn(b"Traceback", result.stderr) + self.assertEqual(result.stdout, b"") + + + def test_registry_weak_flags_are_booleans(self): + registry = self.work.root / "game.json" + output = self.work.root / "audit.json" + output.write_bytes(b"previous report") + for weak in ("false", "true", 0, 1, None, [], {}): + with self.subTest(weak=weak): + registry.write_text(json.dumps([{"nid": "AAAAAAAAAAA", "library": "libSceA.prx", "weak": weak}]), encoding="utf-8") + with self.assertRaisesRegex(import_audit.AuditError, "boolean 'weak'"): + import_audit.read_registry(registry) + code, out, err = self.work.run(registry, "--json", str(output)) + self.assertEqual((code, out), (2, "")) + self.assertIn("boolean 'weak'", err) + self.assertEqual(output.read_bytes(), b"previous report") + for fields, expected in (({}, False), ({"weak": False}, False), ({"weak": True}, True)): + registry.write_text(json.dumps([dict(nid="AAAAAAAAAAA", library="libSceA.prx", **fields)]), encoding="utf-8") + self.assertEqual(import_audit.read_registry(registry), [("AAAAAAAAAAA", "libSceA.prx", expected)]) + + def test_test_file_aliases_cannot_override_production_stubs(self): + source = self.work.source + text = 'APS5_EXPORT("AAAAAAAAAAA", sceExample);\n' + STUB_SOURCE.format(name="sceExample") + write_source(source, "libSceExample", text) + test = source / "libSceExample" / "tests" / "Alias.cpp" + test.parent.mkdir() + test.write_text('APS5_EXPORT("BBBBBBBBBBB", sceExample);\n', encoding="utf-8") + stubs = import_audit.source_stubs(source) + self.assertEqual(stubs, {("libSceExample.prx", "AAAAAAAAAAA"): "sceExample"}) + records, _ = import_audit.audit([("AAAAAAAAAAA", "libSceExample.prx", False)], + {"AAAAAAAAAAA": ["libSceExample.prx"]}, stubs, + {"libSceExample.prx"}, set()) + self.assertEqual(records[0]["class"], "stub") + + def setUp(self): self.work = Workspace() self.addCleanup(self.work.close) + def test_named_imports_use_literal_exports_and_keep_unresolved_names(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting("strcmp", "_Z3foov", "AAAAAAAAAAA")) + registry, report = work.root / "homebrew.registry.json", work.root / "audit.json" + write_registry(registry, [("strcmp", ""), ("strcmp", ""), ("_Z3foov", "libc.prx"), + ("AAAAAAAAAAA#A#B", "libc.prx"), ("missing_function", "", True)]) + code, _, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (1, "")) + data = json.loads(report.read_text(encoding="utf-8")) + records = {record["nid"]: record for record in data["imports"]} + self.assertEqual(data["references"], 5) + self.assertEqual(data["unique_by_class"], {"implemented": 3, "stub": 0, "absent": 1, "module": 0}) + self.assertEqual(records["strcmp"]["references"], 2) + self.assertEqual(records["strcmp"]["providers"], ["libc.prx"]) + self.assertTrue(records["missing_function"]["weak"]) + self.assertIsNone(data["stops"][0]["nid"]) + + def test_named_imports_fall_back_to_nid_exports(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting("j4ViWNHEgww")) + registry, report = work.root / "homebrew.registry.json", work.root / "audit.json" + write_registry(registry, [("strlen", ""), ("strlen", ""), + ("missing_function", "", True)]) + code, _, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (1, "")) + data = json.loads(report.read_text(encoding="utf-8")) + records = {record["nid"]: record for record in data["imports"]} + self.assertEqual(data["references"], 3) + self.assertEqual(records["strlen"]["references"], 2) + self.assertEqual(records["strlen"]["class"], "implemented") + self.assertEqual(records["strlen"]["providers"], ["libc.prx"]) + self.assertEqual(records["missing_function"]["class"], "absent") + self.assertTrue(records["missing_function"]["weak"]) + self.assertIsNone(data["stops"][0]["nid"]) + + def test_named_nid_fallback_preserves_stub_and_library_classification(self): + nid = nid_names.compute_nid(MESSAGE_NAME) + imports = [(MESSAGE_NAME, "libSceOther.prx", False)] + records, missing = import_audit.audit(imports, {nid: ["libSceMsgDialog.prx"]}, + {("libSceMsgDialog.prx", nid): MESSAGE_NAME}, + {"libSceMsgDialog.prx", "libSceOther.prx"}, set()) + self.assertEqual(missing, {}) + self.assertEqual(records[0]["class"], "stub") + self.assertEqual(records[0]["nid"], MESSAGE_NAME) + self.assertEqual(records[0]["name"], MESSAGE_NAME) + self.assertTrue(records[0]["library_mismatch"]) + + def test_literal_exports_take_precedence_over_nid_fallback(self): + imports = [("strlen", "", False)] + stubs = {("literal.prx", "strlen"): "strlen"} + exports = {"strlen": ["literal.prx"], "j4ViWNHEgww": ["libc.prx"]} + records, _ = import_audit.audit(imports, exports, stubs, set(exports), set()) + self.assertEqual(records[0]["class"], "stub") + self.assertEqual(records[0]["providers"], ["literal.prx"]) + module_imports = {"guest.so": (["strlen"], [])} + records, _ = import_audit.audit([], {"j4ViWNHEgww": ["libc.prx"]}, {}, + {"libc.prx"}, set(module_imports), module_imports, + {"strlen": ["other.so"]}) + self.assertEqual(records[0]["providers"], ["other.so"]) + records, _ = import_audit.audit([], {"j4ViWNHEgww": ["libc.prx"]}, {}, + {"libc.prx"}, set(module_imports), module_imports) + self.assertEqual(records[0]["class"], "implemented") + self.assertEqual(records[0]["providers"], ["libc.prx"]) + + def test_empty_and_control_character_imports_remain_invalid(self): + registry = self.work.root / "homebrew.registry.json" + for name in ("", "#A#B", "bad\x00name", "bad\nname", "bad\x7fname", "bad name"): + with self.subTest(name=name): + write_registry(registry, [(name, "")]) + with self.assertRaisesRegex(import_audit.AuditError, "invalid import symbol"): + import_audit.read_registry(registry) + def test_failed_json_write_preserves_existing_report_and_leaves_no_partial_report(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) @@ -383,6 +538,136 @@ class ImportAuditTests(unittest.TestCase): self.assertIn("[library not named]", out) self.assertIn("EEEEEEEEEEE (imported by main.prx)", out) + def test_module_sidecars_do_not_change_the_audit(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + (work.modules / "provider.prx").write_bytes(exporting("BBBBBBBBBBB")) + (work.modules / "consumer.prx").write_bytes(build_elf([ + ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 0, GLOBAL_FUNCTION)], + ["libSceA.prx", "provider.prx"])) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, [("BBBBBBBBBBB", "provider.prx")]) + code, before_out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + before = json.loads(result.read_text()) + for name, data in (("README", b"module notes\n"), ("notes.txt", b"EEEEEEEEEEE\n"), + ("provider.registry.json", b'[{"nid":"EEEEEEEEEEE","library":"missing.prx"}]'), + (".DS_Store", b"\x00\xffmetadata"), ("empty.txt", b"")): + (work.modules / name).write_bytes(data) + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + self.assertEqual(out, before_out) + self.assertEqual(json.loads(result.read_text()), before) + write_registry(registry, [("EEEEEEEEEEE", "notes.txt")]) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + record = next(record for record in data["imports"] if record["nid"] == "EEEEEEEEEEE") + self.assertEqual((record["class"], record["providers"]), ("absent", [])) + self.assertEqual(data["missing_libraries"], {"notes.txt": 1}) + + def test_elf_modules_are_audited_regardless_of_filename(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + (work.modules / "provider.data").write_bytes(build_elf([ + ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)], ["libSceA.prx"])) + (work.modules / "consumer.prx").write_bytes(build_elf([ + ("BBBBBBBBBBB", 0, GLOBAL_FUNCTION)], ["provider.data"])) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, [("BBBBBBBBBBB", "provider.data")]) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["references"], 3) + self.assertEqual(data["unique_by_class"], {"implemented": 2, "stub": 0, "absent": 0, "module": 1}) + self.assertEqual(data["missing_libraries"], {}) + records = {(record["nid"], record.get("module")): record for record in data["imports"]} + self.assertEqual(records[("AAAAAAAAAAA", "provider.data")]["providers"], ["libSceA.prx"]) + self.assertEqual(records[("BBBBBBBBBBB", "consumer.prx")]["providers"], ["provider.data"]) + + def test_module_matches_imports_that_name_another_module_suffix(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting()) + (work.modules / "shipping.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, [("AAAAAAAAAAA", "shipping.debug_prx")]) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["missing_libraries"], {}) + record = next(record for record in data["imports"] if record["library"] == "shipping.debug_prx") + self.assertEqual(record["class"], "module") + + def test_module_names_ignore_case_for_windows_output_only(self): + imports = [("AAAAAAAAAAA", "SDK-Shipping.debug_prx", False)] + records, missing = import_audit.audit(imports, {}, {}, set(), import_audit.ModuleNames({"sdk-shipping.prx"}, fold=True)) + self.assertEqual((records[0]["class"], missing), ("module", {})) + records, missing = import_audit.audit(imports, {}, {}, set(), import_audit.ModuleNames({"sdk-shipping.prx"})) + self.assertEqual((records[0]["class"], missing), ("absent", {"SDK-Shipping.debug_prx": 1})) + + def test_converted_module_matches_imports_of_its_base_name(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting()) + (work.modules / "libSceFace.prx.guest.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceFace.prx")]) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["missing_libraries"], {}) + record = next(record for record in data["imports"] if record["library"] == "libSceFace.prx") + self.assertEqual(record["class"], "module") + + def test_bad_named_modules_and_malformed_elf_are_not_ignored(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting()) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + previous = b'{"previous":"report"}\n' + invalid_class = bytearray(exporting()) + invalid_class[4] = 1 + candidates = [("guest" + suffix, b"module placeholder\n") + for suffix in (".prx", ".sprx", ".suprx", ".debug_prx", ".elf", ".PRX")] + candidates += [("truncated.data", b"\x7fELF"), ("unsupported.data", invalid_class)] + for name, image in candidates: + module = work.modules / name + module.write_bytes(image) + for exists in (False, True): + with self.subTest(name=name, existing_report=exists): + if exists: + result.write_bytes(previous) + else: + result.unlink(missing_ok=True) + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, out), (2, "")) + self.assertIn(name, err) + self.assertIn("ELF", err) + if exists: + self.assertEqual(result.read_bytes(), previous) + else: + self.assertFalse(result.exists()) + module.unlink() + + def test_pe_and_self_modules_are_not_ignored_as_sidecars(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting()) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + previous = b'{"previous":"report"}\n' + for image in (build_pe(["alpha"]), b"\x4f\x15\x3d\x1d" + bytes(64), + b"\x54\x14\xf5\xee" + bytes(64)): + for name in ("guest.prx", "metadata.txt"): + module = work.modules / name + module.write_bytes(image) + with self.subTest(name=name, magic=bytes(image[:4])): + result.write_bytes(previous) + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, out), (2, "")) + self.assertIn(name, err) + self.assertIn("ELF", err) + self.assertEqual(result.read_bytes(), previous) + module.unlink() + def test_module_needed_library_without_a_file_is_missing(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) @@ -397,6 +682,82 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(data["missing_libraries"], {"libSceMissing.prx": 1}) self.assertIn("libSceMissing.prx", out) + def test_missing_library_is_located_next_to_the_registry(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting()) + (work.modules / "main.prx").write_bytes(build_elf([], ["libSceMissing.prx", "libSceGone.prx"])) + registry = work.root / "game.json" + write_registry(registry, []) + elsewhere = work.root / "in" / "Media Modules" + elsewhere.mkdir(parents=True) + (elsewhere / "libSceMissing.prx").write_bytes(exporting("DDDDDDDDDDD")) + code, out, err = work.run(registry) + self.assertEqual((code, err), (1, "")) + self.assertIn(f"libSceMissing.prx is in {elsewhere.resolve()}: pass that directory with --modules", out) + self.assertNotIn("libSceGone.prx is in", out) + code, out, err = work.run(registry, "--modules", str(elsewhere)) + self.assertNotIn("libSceMissing.prx is in", out) + + def test_conflicting_module_files_preserve_or_leave_no_json_report(self): + work = self.work + first, second = work.modules, work.root / "other-modules" + second.mkdir() + (work.libs / "libc.prx").write_bytes(exporting()) + (first / "main.prx").write_bytes(build_elf([ + ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)], ["libSceMissing.prx"])) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["unique_by_class"]["absent"], 1) + self.assertEqual(data["missing_libraries"], {"libSceMissing.prx": 1}) + (second / "main.prx").write_bytes(exporting("CCCCCCCCCCC")) + previous = b'{"previous": "complete report"}\n' + for directories in ((first, second), (second, first)): + for exists in (False, True): + with self.subTest(directories=directories, existing_report=exists): + if exists: + result.write_bytes(previous) + else: + result.unlink(missing_ok=True) + with patch.object(work, "modules", directories[0]): + code, out, err = work.run(registry, "--modules", str(directories[1]), "--json", str(result)) + self.assertEqual((code, out), (2, "")) + self.assertIn("ambiguous guest module main.prx", err) + self.assertIn(str(first / "main.prx"), err) + self.assertIn(str(second / "main.prx"), err) + if exists: + self.assertEqual(result.read_bytes(), previous) + else: + self.assertFalse(result.exists()) + + def test_identical_module_files_are_audited_once(self): + work = self.work + first, second = work.modules, work.root / "other-modules" + second.mkdir() + (work.libs / "libc.prx").write_bytes(exporting()) + image = build_elf([ + ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)], ["libSceMissing.prx"]) + for directory in (first, second): + (directory / "main.prx").write_bytes(image) + (first / "consumer.prx").write_bytes(build_elf([("BBBBBBBBBBB", 0, GLOBAL_FUNCTION)])) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + for directories in ((first, second, first), (second, first, second)): + with self.subTest(directories=directories), patch.object(work, "modules", directories[0]): + extra = [argument for directory in directories[1:] for argument in ("--modules", str(directory))] + code, _, err = work.run(registry, *extra, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + self.assertEqual((data["references"], data["unique_imports"]), (2, 2)) + self.assertEqual(data["unique_by_class"], {"implemented": 1, "stub": 0, "absent": 1, "module": 0}) + self.assertEqual(data["references_by_class"], data["unique_by_class"]) + self.assertEqual(data["missing_libraries"], {"libSceMissing.prx": 1}) + records = {record["nid"]: record for record in data["imports"]} + self.assertEqual((records["AAAAAAAAAAA"]["class"], records["AAAAAAAAAAA"]["references"]), ("absent", 1)) + self.assertEqual((records["BBBBBBBBBBB"]["class"], records["BBBBBBBBBBB"]["providers"]), ("implemented", ["main.prx"])) + def test_module_files_must_be_elf(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) @@ -425,6 +786,72 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(records[("AAAAAAAAAAA", "stripped.prx")]["class"], "implemented") self.assertEqual(records[("BBBBBBBBBBB", "main.prx")]["class"], "implemented") + def test_elf_with_out_of_file_section_table_uses_program_headers(self): + path = Path("module.prx") + image = build_sectionless_elf([ + ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)], ["libSceA.prx"]) + expected_imports = import_audit.elf_imports(path, image) + expected_exports = import_audit.elf_exports(path, image) + self.assertEqual(expected_imports, ({"AAAAAAAAAAA"}, {"libSceA.prx"})) + self.assertEqual(expected_exports, {"BBBBBBBBBBB"}) + for section_offset in (len(image), len(image) + 64): + for section_count in (1, 0): + modified = bytearray(image) + struct.pack_into(" 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_pe_host_dependencies_ignore_library_name_case(self): + work = self.work + (work.libs / "libSceB.prx").write_bytes(build_pe_with_imports([], ["BBBBBBBBBBB"])) + registry, report = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + for dependency in ("LIBSCEB.PRX", "libsceb.prx", "LiBsCeB.PrX"): + with self.subTest(dependency=dependency): + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports([dependency], ["AAAAAAAAAAA"])) + code, out, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (0, ""), out) + self.assertEqual(json.loads(report.read_text())["missing_host_dependencies"], []) + + def test_pe_host_dependency_case_does_not_hide_a_transitive_gap(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEB.PRX"])) + (work.libs / "libSceB.prx").write_bytes(build_pe_with_imports(["LIBSCEMISSING.PRX"])) + registry, report = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + code, _, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (1, "")) + self.assertEqual(json.loads(report.read_text())["missing_host_dependencies"], [ + {"importer": "libSceA.prx", "chain": ["libSceA.prx", "libSceB.prx", "LIBSCEMISSING.PRX"]}, + {"importer": "libSceB.prx", "chain": ["libSceB.prx", "LIBSCEMISSING.PRX"]}, + ]) + + def test_pe_host_dependency_can_find_a_module_with_different_case(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEIME.PRX"])) + (work.modules / "libSceIme.prx").write_bytes(exporting()) + registry = work.root / "game.json" + write_registry(registry, []) + code, out, err = work.run(registry) + self.assertEqual((code, err), (0, ""), out) + + def test_pe_host_dependency_can_find_a_converted_pe_module_with_different_case(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEIME.PRX"])) + (work.modules / "libSceIme.prx.guest.prx").write_bytes(build_pe_with_imports([])) + registry, report = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + code, out, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (0, ""), out) + self.assertEqual(json.loads(report.read_text())["missing_host_dependencies"], []) + + def test_elf_host_dependency_case_stays_significant_in_a_mixed_graph(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEB.PRX"])) + (work.libs / "libSceB.prx").write_bytes(build_elf([], ["LIBSCEC.PRX"])) + (work.libs / "libSceC.prx").write_bytes(exporting()) + registry, report = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + code, _, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (1, "")) + self.assertEqual(json.loads(report.read_text())["missing_host_dependencies"], [ + {"importer": "libSceA.prx", "chain": ["libSceA.prx", "libSceB.prx", "LIBSCEC.PRX"]}, + {"importer": "libSceB.prx", "chain": ["libSceB.prx", "LIBSCEC.PRX"]}, + ]) + + def test_pe_host_dependency_rejects_ambiguous_case_variants(self): + work = self.work + other = work.root / "other-libs" + other.mkdir() + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEB.PRX"])) + (work.libs / "libSceB.prx").write_bytes(build_pe_with_imports([], ["AAAAAAAAAAA"])) + (other / "LIBSCEB.prx").write_bytes(build_pe_with_imports([], ["BBBBBBBBBBB"])) + registry, report = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + report.write_bytes(b"previous complete report") + code, out, err = work.run(registry, "--libs", str(other), "--json", str(report)) + self.assertEqual((code, out), (2, "")) + self.assertIn("ambiguous Windows host library", err) + self.assertEqual(report.read_bytes(), b"previous complete report") + + def test_pe_host_dependency_cycle_with_different_case_terminates(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEB.PRX"])) + (work.libs / "libSceB.prx").write_bytes(build_pe_with_imports(["LIBSCEA.PRX"])) + registry = work.root / "game.json" + write_registry(registry, []) + code, out, err = work.run(registry) + self.assertEqual((code, err), (0, ""), out) + + def test_elf_host_dependencies_are_walked(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_elf( + [("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")) @@ -856,6 +1749,39 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(import_audit.git_stamp(self.work.source), ("abcdef", True)) self.assertEqual([call.args[0][2] for call in run.call_args_list], [str(self.work.source), str(self.work.source)]) + def test_json_keeps_source_commit_when_a_broken_submodule_prevents_status(self): + work = self.work + command = ["git", "-C", str(work.source)] + identity = ["-c", "user.name=Audit test", "-c", "user.email=audit@example.invalid"] + subprocess.run(command + ["init", "-q"], check=True, capture_output=True) + subprocess.run(command + identity + ["commit", "--allow-empty", "-qm", "Source fixture"], check=True, capture_output=True) + commit = subprocess.check_output(command + ["rev-parse", "HEAD"], text=True).strip() + (work.source / ".gitmodules").write_text( + '[submodule "damaged"]\n\tpath = damaged\n\turl = https://example.invalid/damaged.git\n', encoding="utf-8") + subprocess.run(command + ["add", ".gitmodules"], check=True, capture_output=True) + subprocess.run(command + ["update-index", "--add", "--cacheinfo", "160000", commit, "damaged"], check=True, capture_output=True) + subprocess.run(command + identity + ["commit", "-qm", "Submodule fixture"], check=True, capture_output=True) + damaged = work.source / "damaged" + damaged.mkdir() + (damaged / ".git").write_text("gitdir: ../missing-submodule-git\n", encoding="utf-8") + commit = subprocess.check_output(command + ["rev-parse", "HEAD"], text=True).strip() + status = subprocess.run(command + ["status", "--porcelain"], capture_output=True) + self.assertNotEqual(status.returncode, 0) + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry, report = work.root / "game.json", work.root / "audit.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) + code, _, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (0, "")) + data = json.loads(report.read_text(encoding="utf-8")) + self.assertEqual(data["source_commit"], commit) + self.assertIsNone(data["source_dirty"]) + self.assertEqual(data["unique_by_class"], {"implemented": 1, "stub": 0, "absent": 0, "module": 0}) + + def test_git_stamp_keeps_commit_when_status_cannot_start(self): + completed = subprocess.CompletedProcess([], 0, stdout="abcdef\n") + with patch.object(import_audit.subprocess, "run", side_effect=[completed, OSError("status unavailable")]): + self.assertEqual(import_audit.git_stamp(self.work.source), ("abcdef", None)) + def test_library_directories(self): work = self.work (work.libs / "libSceA.prx").write_bytes(build_pe(["AAAAAAAAAAA"])) @@ -886,10 +1812,10 @@ 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")]) + self.assertEqual(import_audit.read_registry(path), [("AAAAAAAAAAA", "libSceA.prx", False)]) def test_names_never_download(self): work = self.work @@ -920,10 +1846,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 @@ -941,6 +1867,69 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual((code, out), (2, "")) self.assertIn(f"FAIL: Permission denied: {library}", err) + def test_strict_stubs_fails_only_for_referenced_throwing_providers(self): + name = "sceExamplePending" + nid = nid_names.compute_nid(name) + registry = self.work.root / "game.json" + output = self.work.root / "audit.json" + (self.work.libs / "libSceExample.prx").write_bytes(exporting(nid)) + write_source(self.work.source, "libSceExample", STUB_SOURCE.format(name=name)) + for weak in (False, True): + with self.subTest(weak=weak): + write_registry(registry, [(nid, "libSceExample.prx", weak)]) + self.assertEqual(self.work.run(registry)[0], 0) + code, out, err = self.work.run(registry, "--strict-stubs", "--json", str(output)) + self.assertEqual((code, err), (1, "")) + self.assertIn("stub", out) + self.assertEqual(json.loads(output.read_text(encoding="utf-8"))["unique_by_class"]["stub"], 1) + write_source(self.work.source, "libSceExample", DONE_SOURCE.format(name=name) + + STUB_SOURCE.format(name="sceUnreferencedPending")) + self.assertEqual(self.work.run(registry, "--strict-stubs")[0], 0) + + + +class StrictProviderTests(unittest.TestCase): + def test_named_provider_mismatch_is_optional_failure(self): + work = Workspace() + self.addCleanup(work.close) + (work.libs / "libSceExpected.prx").write_bytes(build_pe(["BBBBBBBBBBB"])) + (work.libs / "libSceOther.prx").write_bytes(build_pe(["AAAAAAAAAAA"])) + registry = work.root / "registry.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceExpected.prx")]) + result = work.root / "audit.json" + self.assertEqual(work.run(registry)[0], 0) + code, out, err = work.run(registry, "--strict-providers", "--json", str(result)) + self.assertEqual((code, err), (1, "")) + report = json.loads(result.read_text()) + self.assertEqual(report["library_mismatch"], 1) + self.assertEqual(report["imports"][0]["providers"], ["libSceOther.prx"]) + self.assertIn("only by a library other than", out) + + def test_matching_provider_passes(self): + work = Workspace() + self.addCleanup(work.close) + (work.libs / "libSceExpected.prx").write_bytes(build_pe(["AAAAAAAAAAA"])) + registry = work.root / "registry.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceExpected.prx")]) + self.assertEqual(work.run(registry, "--strict-providers")[0], 0) + + def test_shared_libc_provider_remains_valid(self): + work = Workspace() + self.addCleanup(work.close) + (work.libs / "libSceExpected.prx").write_bytes(build_pe([])) + (work.libs / "libc.prx").write_bytes(build_pe(["AAAAAAAAAAA"])) + registry = work.root / "registry.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceExpected.prx")]) + self.assertEqual(work.run(registry, "--strict-providers")[0], 0) + + def test_missing_provider_file_still_fails(self): + work = Workspace() + self.addCleanup(work.close) + (work.libs / "libc.prx").write_bytes(build_pe(["AAAAAAAAAAA"])) + registry = work.root / "registry.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceMissing.prx")]) + self.assertEqual(work.run(registry, "--strict-providers")[0], 1) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index 41a8f5118d..e317357de5 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,40 @@ 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_environment_variable_selects_the_database_and_help_names_it(self): + script = str(Path(nid_names.__file__)) + helped = subprocess.run([sys.executable, script, "--help"], capture_output=True, text=True, timeout=60) + self.assertEqual(helped.returncode, 0, helped.stderr) + self.assertIn("ANYPS5_NID_DB", " ".join(helped.stdout.split())) + with tempfile.TemporaryDirectory() as directory: + database = Path(directory) / "nid database.csv" + nid = nid_names.compute_nid("sceExample") + database.write_text(f"{nid} sceExample\n", encoding="utf-8") + scratch = str(Path(directory) / "scratch") + Path(scratch).mkdir() + environment = {**os.environ, "ANYPS5_NID_DB": str(database), "TEMP": scratch, "TMP": scratch, "TMPDIR": scratch} + result = subprocess.run([sys.executable, script, "--nid", nid, "--json"], capture_output=True, text=True, + env=environment, timeout=20) + self.assertEqual(list(Path(scratch).iterdir()), []) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertNotIn("downloading", result.stderr) + self.assertIn("sceExample", result.stdout) + def test_interrupted_download_does_not_publish_a_partial_cache(self): with tempfile.TemporaryDirectory() as directory: cache = Path(directory) / "cache" / "aerolib.csv" @@ -94,6 +130,31 @@ class DatabaseCacheTests(unittest.TestCase): class NidArgumentTests(unittest.TestCase): options = ["-h", "--help", "--db", "--nid", "--json"] + def test_name_and_nid_modes_conflict_even_with_empty_filters(self): + for name_option in ("--name", "--symbol", "--na", "--sym"): + for nid_option in ("--nid", "--ni"): + for names, nids in ( + ([name_option], [nid_option]), + ([name_option, "ceilf"], [nid_option]), + ([name_option + "=ceilf"], [nid_option]), + ([name_option], [nid_option, "AAAAAAAAAAA"]), + ([name_option], [nid_option + "=AAAAAAAAAAA"])): + for arguments in (names + nids, nids + names): + with self.subTest(arguments=arguments), \ + contextlib.redirect_stdout(io.StringIO()) as output, \ + contextlib.redirect_stderr(io.StringIO()) as error, \ + patch.object(nid_names, "load_db", return_value={}) as database, \ + patch.object(nid_names, "collect_unknowns", return_value={}) as sources, \ + patch.object(nid_names, "collect_real_names", return_value=set()) as definitions: + with self.assertRaises(SystemExit) as exited: + nid_names.main([*arguments, "--json"]) + self.assertEqual(exited.exception.code, 2) + self.assertIn("--name/--symbol cannot be combined with --nid", error.getvalue()) + self.assertEqual(output.getvalue(), "") + database.assert_not_called() + sources.assert_not_called() + definitions.assert_not_called() + def test_invalid_nids_fail_before_database_or_source_access(self): for nid in ("short", "ABCDEFGHIJKL", "ABCDEFGHIJ/", "ABCDEFGHIJ_", "ABCDEFGHIJ\u00e9", "", "--jsooon"): with self.subTest(nid=nid): @@ -126,6 +187,18 @@ class NidArgumentTests(unittest.TestCase): row, = json.loads(output.getvalue())["rows"] self.assertEqual((row["nid"], row["status"]), ("AAAAAAAAAA+", "unknown")) + def test_nid_with_library_module_suffix_matches_the_bare_nid(self): + results = [] + for nid in ("ZjtRqSMJwdw", "ZjtRqSMJwdw#C#D"): + output = io.StringIO() + with patch.object(nid_names, "load_db", return_value={"ZjtRqSMJwdw": "sinh"}), \ + patch.object(nid_names, "collect_real_names", return_value=set()), \ + contextlib.redirect_stdout(output): + self.assertEqual(nid_names.main(["--nid=" + nid, "--json"]), 0) + results.append(json.loads(output.getvalue())["rows"]) + self.assertEqual(results[0], results[1]) + self.assertEqual(results[1][0]["nid"], "ZjtRqSMJwdw") + def normalize(self, arguments): return nid_names.nid_arguments(arguments, self.options) @@ -156,6 +229,29 @@ class NidArgumentTests(unittest.TestCase): rows = json.loads(printed.call_args.args[0])["rows"] self.assertEqual({row["nid"]: row["suggestion"] for row in rows}, database) + def test_symbol_files_and_stdin_compute_nids_without_a_database(self): + with tempfile.TemporaryDirectory() as directory: + names = Path(directory) / "symbols.txt" + symbols = ["sceExample", "-leading-dash", " literal spaces ", "unicode_Ω", "sceExample"] + names.write_bytes(("\ufeff" + "\r\n".join(symbols) + "\r\n\r\n").encode("utf-8")) + for source in (str(names), "-"): + with self.subTest(source=source), patch("sys.stdin", io.StringIO(names.read_text(encoding="utf-8-sig"))), \ + patch.object(nid_names, "load_db", side_effect=AssertionError("database used")), \ + contextlib.redirect_stdout(io.StringIO()) as output: + self.assertEqual(nid_names.main(["--name", "first", "--from-file", source, "--json"]), 0) + rows = json.loads(output.getvalue())["symbols"] + self.assertEqual(rows, [{"name": name, "nid": nid_names.compute_nid(name)} for name in ["first", *symbols]]) + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as exited: + nid_names.main(["--from-file", str(names), "--nid"]) + self.assertEqual(exited.exception.code, 2) + with patch.object(nid_names, "load_db", side_effect=AssertionError("database used")), \ + contextlib.redirect_stdout(io.StringIO()) as output, \ + contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as exited: + nid_names.main(["--from-file", str(names.parent / "missing"), "--json"]) + self.assertEqual(exited.exception.code, 2) + self.assertEqual(output.getvalue(), "") + + class SourceCollectionTests(unittest.TestCase): def setUp(self): @@ -178,6 +274,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; } */ @@ -189,6 +310,30 @@ 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_disambiguated_definitions_are_real_names_of_their_symbol(self): + self.write_source('int APS5_VABI fstat_nid_disambig1_nid_postfix(int fd) { return 0; }\n' + 'int APS5_VABI pwrite_nid_disambig12(int fd) { return 0; }\n' + 'int APS5_VABI sceKeep_nid_disambigA() { return 0; }\n') + self.assertEqual(nid_names.collect_real_names(), { + "fstat", "fstat_nid_disambig1_nid_postfix", + "pwrite", "pwrite_nid_disambig12", + "sceKeep_nid_disambigA", + }) + def test_formatted_alias_macro_is_detected(self): self.write_source('APS5_EXPORT (\n "AAAAAAAAAAA" ,\n exampleUnknown00\n );\n') self.assertEqual(nid_names.collect_unknowns(), {"AAAAAAAAAAA": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown00"}) @@ -240,10 +385,47 @@ int APS5_VABI sceReal() { return 0; } ''') self.assertEqual(nid_names.collect_real_names(), {"sceReal"}) + def test_name_option_computes_symbol_nid(self): + output = io.StringIO() + with patch.object(sys, "argv", ["nid_names.py", "--name", "ceilf", "floorf"]), contextlib.redirect_stdout(output): + self.assertEqual(nid_names.main(), 0) + text = output.getvalue() + self.assertIn("ceilf", text) + self.assertIn("GAUuLKGhsCw", text) + self.assertIn("floorf", text) + self.assertIn("mKhVDmYciWA", text) + + def test_name_option_with_json_emits_symbols_list(self): + output = io.StringIO() + with patch.object(sys, "argv", ["nid_names.py", "--name", "ceilf", "--json"]), contextlib.redirect_stdout(output): + self.assertEqual(nid_names.main(), 0) + data = json.loads(output.getvalue()) + self.assertEqual(data, {"symbols": [{"name": "ceilf", "nid": "GAUuLKGhsCw"}]}) + + def test_symbol_alias_does_not_load_database(self): + output = io.StringIO() + with patch.object(sys, "argv", ["nid_names.py", "--symbol", "truncf"]), \ + patch.object(nid_names, "load_db", side_effect=AssertionError("unexpected db load")), \ + contextlib.redirect_stdout(output): + self.assertEqual(nid_names.main(), 0) + self.assertIn("Vo8rvWtZw3g", output.getvalue()) + + def test_name_option_rejects_nid_and_keeps_long_names(self): + with patch.object(sys, "argv", ["nid_names.py", "--name", "ceilf", "--nid", "AAAAAAAAAAA"]), \ + contextlib.redirect_stderr(io.StringIO()): + with self.assertRaises(SystemExit): + nid_names.main() + long_name = "a" * 50 + output = io.StringIO() + with patch.object(sys, "argv", ["nid_names.py", "--name", long_name]), contextlib.redirect_stdout(output): + self.assertEqual(nid_names.main(), 0) + self.assertIn(long_name, output.getvalue()) + def test_spliced_line_comment_and_macro_tokens(self): self.write_source('// continued \\\nAPS5_EXPORT("AAAAAAAAAAA", exampleUnknown00);\nAPS5_EX\\\nPORT("BBBBBBBBBBB", exampleUnknown01);\n') self.assertEqual(nid_names.collect_unknowns(), {"BBBBBBBBBBB": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown01"}) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index ce95a7bcd0..cc485db136 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -1,5 +1,10 @@ -import os +import contextlib +import io import json +import os +import ctypes +import stat +import subprocess import sys import tarfile import tempfile @@ -44,22 +49,52 @@ class ReleasePackageTests(unittest.TestCase): self.addCleanup(patch.stopall) patch.object(package_release, "ROOT", self.source, create=True).start() + def test_ajm_vibration_decoder_license_in_archives(self): + (self.source / "core/libs/prx/libSceAjm.native").mkdir() + (self.build / "core/libs/libs/libSceAjm.native.prx").write_bytes(b"ajm") + license_file = self.source / "3rdparty/vgmstream/COPYING" + license_file.parent.mkdir(parents=True) + license_file.write_bytes(b"decoder license") + package_release.package("linux", self.build, self.output, "v1") + with zipfile.ZipFile(self.output / "prx-linux-v1.zip") as archive: + self.assertEqual(archive.read("libs/licenses/vgmstream.txt"), b"decoder license") + with tarfile.open(self.output / "prx-linux-v1.tar.gz") as archive: + self.assertEqual(archive.extractfile("libs/licenses/vgmstream.txt").read(), b"decoder license") + + def test_vorbis_decoder_licenses_in_archives(self): + (self.source / "core/libs/prx/libSceVorbisDec").mkdir() + (self.build / "core/libs/libs/libSceVorbisDec.prx").write_bytes(b"vorbis") + for name in ("ogg", "vorbis"): + license_file = self.source / f"3rdparty/{name}/COPYING" + license_file.parent.mkdir(parents=True) + license_file.write_bytes(f"{name} license".encode()) + package_release.package("linux", self.build, self.output, "v1") + with zipfile.ZipFile(self.output / "prx-linux-v1.zip") as archive: + self.assertEqual(archive.read("libs/licenses/ogg.txt"), b"ogg license") + self.assertEqual(archive.read("libs/licenses/vorbis.txt"), b"vorbis license") + with tarfile.open(self.output / "prx-linux-v1.tar.gz") as archive: + self.assertEqual(archive.extractfile("libs/licenses/ogg.txt").read(), b"ogg license") + self.assertEqual(archive.extractfile("libs/licenses/vorbis.txt").read(), b"vorbis license") + def test_windows_runtime_follows_the_configured_compiler_directory(self): runtime = self.root / "custom toolchain" / "bin" runtime.mkdir(parents=True) names = ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll") for name in names: (runtime / name).write_bytes(name.encode()) + allocator = ("mimalloc.dll", "mimalloc-redirect.dll") + for name in allocator: + (self.library.parent / name).write_bytes(name.encode()) compiler = runtime / "g++.exe" (self.build / "CMakeCache.txt").write_text("CMAKE_CXX_COMPILER:STRING=" + compiler.as_posix() + "\n", encoding="utf-8") (self.build / "core/relinker/relinker.exe").write_bytes(b"Windows executable") package_release.package("windows", self.build, self.output, "v1") with zipfile.ZipFile(self.output / "prx-windows-v1.zip") as archive: - self.assertEqual(set(archive.namelist()), {"libs/libc.prx", *("libs/" + name for name in names)}) - for name in names: + self.assertEqual(set(archive.namelist()), {"libs/libc.prx", *("libs/" + name for name in names + allocator)}) + for name in names + allocator: self.assertEqual(archive.read("libs/" + name), name.encode()) with tarfile.open(self.output / "prx-windows-v1.tar.gz") as archive: - for name in names: + for name in names + allocator: self.assertEqual(archive.extractfile("libs/" + name).read(), name.encode()) self.assertEqual((self.output / "relinker-v1.exe").read_bytes(), b"Windows executable") @@ -74,6 +109,70 @@ class ReleasePackageTests(unittest.TestCase): self.assertEqual(list(self.output.iterdir()), [previous]) self.assertEqual(previous.read_bytes(), b"existing output") + def test_windows_runtime_decodes_cmake_cache_with_utf8_bom(self): + compiler = self.root / "custom toolchain" / "bin" / "g++.exe" + (self.build / "CMakeCache.txt").write_bytes(b"\xef\xbb\xbfCMAKE_CXX_COMPILER:STRING=" + compiler.as_posix().encode("utf-8") + b"\n") + self.assertEqual(package_release.windows_runtime(self.build), compiler.parent) + + def test_tracy_runtime_decodes_cmake_cache_with_utf8_bom(self): + client = self.build / "core/libs/libs/libTracyClient.dll" + client.write_bytes(b"client") + (self.build / "CMakeCache.txt").write_bytes(b"\xef\xbb\xbfAPS5_ENABLE_TRACY:BOOL=ON\n") + self.assertEqual(package_release.tracy_runtime("windows", self.build), [client]) + + def test_tracy_packages_include_only_the_target_platform_client(self): + directory = self.build / "core/libs/libs" + for platform, name in (("linux", "libTracyClient.so.0.14.1"), ("windows", "libTracyClient.dll")): + with self.subTest(platform=platform): + client = directory / name + client.write_bytes(platform.encode() + b" Tracy client") + cache = "APS5_ENABLE_TRACY:BOOL=ON\n" + expected = {"libs/libc.prx", "libs/" + name} + if platform == "windows": + runtime = self.root / "toolchain" + runtime.mkdir() + for runtime_name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll"): + (runtime / runtime_name).write_bytes(runtime_name.encode()) + expected.add("libs/" + runtime_name) + for allocator_name in ("mimalloc.dll", "mimalloc-redirect.dll"): + (directory / allocator_name).write_bytes(allocator_name.encode()) + expected.add("libs/" + allocator_name) + cache += "CMAKE_CXX_COMPILER:FILEPATH=" + (runtime / "g++.exe").as_posix() + "\n" + (self.build / "core/relinker/relinker.exe").write_bytes(b"Windows executable") + (self.build / "CMakeCache.txt").write_text(cache, encoding="utf-8") + package_release.package(platform, self.build, self.output, "v1") + with zipfile.ZipFile(self.output / f"prx-{platform}-v1.zip") as archive: + self.assertEqual(set(archive.namelist()), expected) + self.assertEqual(archive.read("libs/" + name), client.read_bytes()) + with tarfile.open(self.output / f"prx-{platform}-v1.tar.gz") as archive: + self.assertEqual(set(archive.getnames()), expected) + self.assertEqual(archive.extractfile("libs/" + name).read(), client.read_bytes()) + + def test_disabled_tracy_does_not_publish_stale_clients(self): + for name in ("libTracyClient.dll", "libTracyClient.so.0.14.1"): + (self.library.parent / name).write_bytes(b"old client") + for index, value in enumerate(("OFF", "0", "NO", "FALSE", "N", "IGNORE", "NOTFOUND", "TRACY-NOTFOUND", "")): + with self.subTest(value=value): + (self.build / "CMakeCache.txt").write_text(f"APS5_ENABLE_TRACY:BOOL={value}\n", encoding="utf-8") + output = self.output / str(index) + package_release.package("linux", self.build, output, "v1") + with zipfile.ZipFile(output / "prx-linux-v1.zip") as archive: + self.assertEqual(archive.namelist(), ["libs/libc.prx"]) + + def test_missing_or_ambiguous_tracy_client_preserves_existing_assets(self): + (self.build / "CMakeCache.txt").write_text("APS5_ENABLE_TRACY:BOOL=ON\n", encoding="utf-8") + previous = {"prx-linux-v1.zip": b"old zip", "prx-linux-v1.tar.gz": b"old tar", "relinker-v1": b"old binary"} + self.output.mkdir() + for name, data in previous.items(): + (self.output / name).write_bytes(data) + for clients in ((), ("libTracyClient.so.0.13.1", "libTracyClient.so.0.14.1")): + with self.subTest(clients=clients): + for name in clients: + (self.library.parent / name).write_bytes(b"client bytes") + with self.assertRaisesRegex(RuntimeError, "Tracy"): + package_release.package("linux", self.build, self.output, "v1") + self.assertEqual({file.name: file.read_bytes() for file in self.output.iterdir()}, previous) + def test_source_discovery_does_not_depend_on_working_directory(self): with working_directory(self.root): package_release.package("linux", self.build, self.output, "v1.0") @@ -145,14 +244,140 @@ class ReleasePackageTests(unittest.TestCase): self.assertEqual(destination.read_bytes(), b"old " + name.encode()) self.assertEqual((output / "unrelated.txt").read_bytes(), b"unrelated") + @unittest.skipUnless(os.name == "nt", "Windows read-only replacement semantics") + def test_readonly_destinations_preserve_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} + previous["unrelated.txt"] = b"unrelated" + for name, content in previous.items(): + (output / name).write_bytes(content) + destination = output / blocked + destination.chmod(stat.S_IREAD) + try: + 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()}, previous) + self.assertIn("read-only", str(error.exception)) + self.assertFalse(destination.stat().st_mode & stat.S_IWRITE) + finally: + destination.chmod(stat.S_IREAD | stat.S_IWRITE) + + @unittest.skipIf(os.name == "nt", "POSIX allows replacement in a writable directory") + def test_posix_readonly_destination_can_be_replaced(self): + self.output.mkdir() + destination = self.output / "prx-linux-v1.zip" + destination.write_bytes(b"old archive") + destination.chmod(stat.S_IREAD) + try: + package_release.package("linux", self.build, self.output, "v1") + with zipfile.ZipFile(destination) as archive: + self.assertEqual(archive.read("libs/libc.prx"), b"library bytes") + 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) + except OSError as error: + self.skipTest(f"File symlinks unavailable: {error}") + + def link_attributes(self, link, attributes): + setter = ctypes.WinDLL("kernel32", use_last_error=True).SetFileAttributesW + setter.argtypes = [ctypes.c_wchar_p, ctypes.c_uint32] + setter.restype = ctypes.c_int + if not setter(str(link), attributes): + raise ctypes.WinError(ctypes.get_last_error()) + + @unittest.skipUnless(os.name == "nt", "Windows read-only symlink semantics") + def test_readonly_dangling_link_preserves_existing_assets(self): + self.output.mkdir() + tar = self.output / "prx-linux-v1.tar.gz" + binary = self.output / "relinker-v1" + tar.write_bytes(b"old tar") + binary.write_bytes(b"old executable") + link = self.output / "prx-linux-v1.zip" + self.file_link(link, "missing.zip") + self.link_attributes(link, stat.FILE_ATTRIBUTE_READONLY) + try: + self.assertFalse(link.exists()) + with self.assertRaisesRegex(RuntimeError, "read-only"): + package_release.package("linux", self.build, self.output, "v1") + self.assertEqual(tar.read_bytes(), b"old tar") + self.assertEqual(binary.read_bytes(), b"old executable") + self.assertEqual(link.readlink(), Path("missing.zip")) + self.assertTrue(link.lstat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY) + self.assertEqual({path.name for path in self.output.iterdir()}, {tar.name, binary.name, link.name}) + finally: + self.link_attributes(link, stat.FILE_ATTRIBUTE_NORMAL) + + @unittest.skipUnless(os.name == "nt", "Windows link and target attributes are distinct") + def test_writable_link_to_readonly_target_can_be_replaced(self): + self.output.mkdir() + target = self.root / "original.zip" + target.write_bytes(b"original target") + link = self.output / "prx-linux-v1.zip" + self.file_link(link, target) + target.chmod(stat.S_IREAD) + try: + self.assertFalse(link.lstat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY) + package_release.package("linux", self.build, self.output, "v1") + self.assertFalse(link.is_symlink()) + self.assertEqual(target.read_bytes(), b"original target") + self.assertFalse(target.stat().st_mode & stat.S_IWRITE) + with zipfile.ZipFile(link) as archive: + self.assertEqual(archive.read("libs/libc.prx"), b"library bytes") + finally: + target.chmod(stat.S_IREAD | stat.S_IWRITE) + def test_invalid_tag_creates_no_assets(self): with self.assertRaises(ValueError): package_release.package("linux", self.build, self.output, "../v1.0") self.assertFalse(self.output.exists()) + def test_windows_runtime_missing_cmake_cache_raises_runtime_error(self): + with tempfile.TemporaryDirectory() as tmp: + with self.assertRaisesRegex(RuntimeError, "Missing CMakeCache.txt"): + package_release.windows_runtime(Path(tmp)) + class DocumentationAssetsTests(unittest.TestCase): + def test_source_not_directory_raises_runtime_error(self): + with tempfile.TemporaryDirectory() as tmp: + with self.assertRaisesRegex(RuntimeError, "not a directory"): + package_release.collect_docs(Path(tmp) / "nonexistent", Path(tmp) / "assets") + def test_unique_nested_documents_are_flattened(self): with tempfile.TemporaryDirectory() as tmp: source = Path(tmp) / "source" @@ -242,7 +467,65 @@ class ReleaseAssetsTests(unittest.TestCase): self.assertEqual((output / name).read_bytes(), b"old " + name.encode()) +class ReleaseNoteDestinationsTests(unittest.TestCase): + def test_directory_body_preserves_existing_full_notes(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source, body, full = root / "notes.json", root / "body", root / "full.md" + source.write_text(json.dumps({"body": "new release notes"}), encoding="utf-8") + body.mkdir() + full.write_bytes(b"previous full notes") + with self.assertRaises(OSError): + prepare_release_notes.prepare_notes(source, body, full) + self.assertEqual(full.read_bytes(), b"previous full notes") + self.assertEqual(list(body.iterdir()), []) + + @unittest.skipUnless(os.name == "nt", "Windows read-only output semantics") + def test_readonly_output_preserves_both_note_files(self): + for target in ("body.md", "full.md"): + with self.subTest(target=target), tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source, body, full = root / "notes.json", root / "body.md", root / "full.md" + source.write_text(json.dumps({"body": "new release notes"}), encoding="utf-8") + body.write_bytes(b"previous body") + full.write_bytes(b"previous full notes") + readonly = root / target + readonly.chmod(stat.S_IREAD) + try: + with self.assertRaises(OSError): + prepare_release_notes.prepare_notes(source, body, full) + self.assertEqual(body.read_bytes(), b"previous body") + self.assertEqual(full.read_bytes(), b"previous full notes") + finally: + readonly.chmod(stat.S_IREAD | stat.S_IWRITE) + + 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) @@ -277,5 +560,114 @@ class ReleaseNotesTests(unittest.TestCase): self.assertEqual(body.read_bytes(), prepare_release_notes.release_body(notes).encode("utf-8")) +class ReleaseNotesEdgeCaseTests(unittest.TestCase): + def test_non_string_body_raises_value_error(self): + source = json.dumps({"body": 123}) + with tempfile.TemporaryDirectory() as directory: + directory = Path(directory) + source_path = directory / "source.json" + source_path.write_text(source, encoding="utf-8") + with self.assertRaisesRegex(ValueError, "must be a string"): + prepare_release_notes.prepare_notes(source_path, directory / "body.md", directory / "full.md") + + def test_main_with_valid_arguments(self): + notes = "# Release v1.0.0\n\n## What's Changed\n\n* Fix a by @user in #1\n" + source = json.dumps({"body": notes}) + with tempfile.TemporaryDirectory() as directory: + directory = Path(directory) + source_path = directory / "source.json" + body_path = directory / "body.md" + full_path = directory / "full.md" + source_path.write_text(source, encoding="utf-8") + arguments = [ + "prepare_release_notes.py", + "--input", str(source_path), + "--body", str(body_path), + "--full", str(full_path), + ] + with contextlib.redirect_stdout(io.StringIO()): + with patch.object(sys, "argv", arguments): + prepare_release_notes.main() + self.assertEqual(full_path.read_text(encoding="utf-8"), notes) + self.assertEqual(body_path.read_text(encoding="utf-8"), notes) + + def test_main_with_missing_arguments_exits_with_error(self): + with contextlib.redirect_stderr(io.StringIO()): + with self.assertRaises(SystemExit): + with patch.object(sys, "argv", ["prepare_release_notes.py"]): + prepare_release_notes.main() + + def test_creates_parent_directories(self): + notes = "# Release v1.0.0\n" + source = json.dumps({"body": notes}) + with tempfile.TemporaryDirectory() as directory: + directory = Path(directory) + source_path = directory / "source.json" + source_path.write_text(source, encoding="utf-8") + body_path = directory / "nested_body" / "body.md" + full_path = directory / "nested_full" / "full.md" + prepare_release_notes.prepare_notes(source_path, body_path, full_path) + self.assertEqual(body_path.read_text(encoding="utf-8"), notes) + self.assertEqual(full_path.read_text(encoding="utf-8"), notes) + + def test_body_over_limit_without_whats_changed_is_returned_unchanged(self): + notes = "# Release\n\n## Details\n\n" + "x" * (prepare_release_notes.BODY_LIMIT + 1) + result = prepare_release_notes.release_body(notes) + self.assertEqual(result, notes) + + def test_body_over_limit_preserves_full_changelog_link(self): + notes = ( + "## What's Changed\n" + "* Fix a by @user in #1\n\n" + "**Full Changelog**: https://github.com/example/compare/v0.9.0...v1.0.0\n" + ) + result = prepare_release_notes.release_body(notes + "x" * (prepare_release_notes.BODY_LIMIT + 1)) + self.assertNotIn("What's Changed", result) + self.assertIn("Full Changelog", result) + + +class ReleaseMainTests(unittest.TestCase): + def test_main_with_docs_arguments(self): + with tempfile.TemporaryDirectory() as tmp: + docs = Path(tmp) / "docs" + docs.mkdir() + (docs / "guide.md").write_text("# Guide", encoding="utf-8") + output = Path(tmp) / "out" + result = package_release.main(["--docs", str(docs), "--output", str(output)]) + self.assertEqual(result, 0) + self.assertEqual((output / "guide.md").read_text(encoding="utf-8"), "# Guide") + + def test_main_reports_packaging_error_via_parser_error(self): + with tempfile.TemporaryDirectory() as tmp: + output = Path(tmp) / "out" + stderr = io.StringIO() + with contextlib.redirect_stderr(stderr), self.assertRaises(SystemExit) as error: + package_release.main(["--docs", str(Path(tmp) / "nonexistent"), "--output", str(output)]) + self.assertEqual(error.exception.code, 2) + self.assertIn("not a directory", stderr.getvalue()) + + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + out, err = Stream(), Stream() + with patch.object(package_release.sys, "stdout", out), patch.object(package_release.sys, "stderr", err): + with tempfile.TemporaryDirectory() as tmp: + docs = Path(tmp) / "docs" + docs.mkdir() + (docs / "doc.md").write_text("text", encoding="utf-8") + package_release.main(["--docs", str(docs), "--output", str(Path(tmp) / "out")]) + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_pad_input.py b/tools/tests/test_pad_input.py index 9771ead3d2..bb557866d4 100644 --- a/tools/tests/test_pad_input.py +++ b/tools/tests/test_pad_input.py @@ -3,6 +3,8 @@ import io import os from pathlib import Path import sys +import stat +import subprocess import tempfile import unittest from unittest import mock @@ -25,6 +27,32 @@ class PadInputTests(unittest.TestCase): pad_input.main() return [int(token) for token in self.path.read_text(encoding="utf-8").split()] + @unittest.skipUnless(os.name == "posix", "POSIX file permissions") + def test_replacement_keeps_shared_command_mode(self): + self.path.write_text("1 0 1\n", encoding="utf-8") + self.path.chmod(0o640) + self.assertEqual(self.queue(1000, "cross"), [1000, 16384, 300]) + self.assertEqual(stat.S_IMODE(self.path.stat().st_mode), 0o640) + + @unittest.skipUnless(os.name == "posix" and os.geteuid() == 0, "requires a privileged POSIX writer") + def test_privileged_replacement_remains_readable_by_original_owner(self): + import pwd + try: + reader = pwd.getpwnam("nobody") + except KeyError: + self.skipTest("unprivileged test reader unavailable") + self.root.chmod(0o755) + self.path.write_text("1 0 1\n", encoding="utf-8") + self.path.chmod(0o600) + os.chown(self.path, reader.pw_uid, reader.pw_gid) + self.queue(1000, "cross") + result = subprocess.run([sys.executable, "-c", + "import sys; from pathlib import Path; print(Path(sys.argv[1]).read_text(), end='')", + str(self.path)], user=reader.pw_uid, group=reader.pw_gid, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "1000 16384 300\n") + self.assertEqual((self.path.stat().st_uid, self.path.stat().st_gid), (reader.pw_uid, reader.pw_gid)) + def test_persistent_command_is_advanced_after_clock_reset(self): self.path.write_text("2000 16384 300\n", encoding="utf-8") initialized_sequence = 2000 @@ -33,6 +61,10 @@ class PadInputTests(unittest.TestCase): self.assertEqual(command[1:], [64, 300]) self.assertEqual(self.queue(1001, "cross"), [command[0] + 1, 16384, 300]) + def test_timeline_command_is_advanced_after_clock_reset(self): + self.path.write_text("APS5_PAD_TIMELINE 1 2000 1\n0 100 16384\n", encoding="utf-8") + self.assertEqual(self.queue(1000, "down"), [2001, 64, 300]) + def test_equal_clock_still_produces_new_pulses(self): first = self.queue(1000, "cross") second = self.queue(1000, "down") @@ -49,6 +81,26 @@ 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_joined_buttons_are_pressed_together(self): + self.assertEqual(self.queue(1000, "l1+r1"), [1000, 0x400 | 0x800, 300]) + self.assertEqual(self.queue(1000, "cross+square+up", "--hold-ms", "40"), [1001, 0x4000 | 0x8000 | 0x10, 40]) + + def test_invalid_joined_buttons_report_error_and_preserve_command(self): + original = "2000 16384 300\n" + for button in ["l1+nothing", "l1+release", "l1+l1", "l1+", "+r1"]: + with self.subTest(button=button): + self.path.write_text(original, encoding="utf-8") + errors = io.StringIO() + with contextlib.redirect_stderr(errors), self.assertRaises(SystemExit) as error: + self.queue(1000, button) + self.assertEqual(error.exception.code, 2) + self.assertIn("invalid button", errors.getvalue()) + self.assertEqual(self.path.read_text(encoding="utf-8"), original) + 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]) @@ -63,7 +115,9 @@ class PadInputTests(unittest.TestCase): "18446744073709551616 16384 300\n", "2000 1 300\n", "2000 16384 0\n", "2000 16384 5001\n", "2000 16384 300 0 256 128 128\n", "2000 16384 300 extra\n", "2_000 16384 300\n", "-2000 16384 300\n", - "0 16384 300\n", " " * 128 + "2000 16384 300\n"]: + "0 16384 300\n", " " * 128 + "2000 16384 300\n", + "APS5_PAD_TIMELINE 2 2000 1\n", "APS5_PAD_TIMELINE 1 0 1\n", "APS5_PAD_TIMELINE 1 2000 0\n", + "APS5_PAD_TIMELINE 1 2000 4097\n", "APS5_PAD_TIMELINE 1 2000\n"]: with self.subTest(text=text): self.path.write_text(text, encoding="utf-8") self.assertEqual(self.queue(1000, "down"), [1000, 64, 300]) diff --git a/tools/tests/test_pad_timeline.py b/tools/tests/test_pad_timeline.py new file mode 100644 index 0000000000..b7a91f3d96 --- /dev/null +++ b/tools/tests/test_pad_timeline.py @@ -0,0 +1,180 @@ +import contextlib +import copy +import io +import json +import os +import stat +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pad_input +import pad_timeline + + +class TimelineTests(unittest.TestCase): + def setUp(self): + self.document = {"version": 1, "events": [ + {"at_ms": 200, "hold_ms": 100, "buttons": ["cross", "r1"]}, + {"at_ms": 500, "hold_ms": 200, "buttons": ["down"]}, + ]} + + def test_compilation_and_repetition(self): + text = pad_timeline.compile_timeline(self.document, 42, 2, 300) + self.assertEqual(text, "APS5_PAD_TIMELINE 1 42 4\n200 100 18432\n500 200 64\n1200 100 18432\n1500 200 64\n") + + def test_touchpad_half_names_keep_their_command_bits(self): + self.document["events"][0]["buttons"] = ["cross", "touch-left", "touch-right"] + self.assertIn("200 100 6307840", pad_timeline.compile_timeline(self.document, 1)) + + def test_empty_buttons_release(self): + self.document["events"][0]["buttons"] = [] + self.assertIn("200 100 0", pad_timeline.compile_timeline(self.document, 1)) + + def test_release_cannot_be_combined(self): + self.document["events"][0]["buttons"] = ["release", "cross"] + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(self.document, 1) + + def test_unknown_buttons(self): + for buttons in [["typo"], "cross", [True]]: + with self.subTest(buttons=buttons): + self.document["events"][0]["buttons"] = buttons + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(self.document, 1) + + def test_overlaps(self): + self.document["events"][1]["at_ms"] = 250 + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(self.document, 1) + + def test_integer_bounds(self): + for field, values in {"at_ms": [-1, True, 1.5, 3600001], "hold_ms": [0, True, 5001]}.items(): + for value in values: + with self.subTest(field=field, value=value): + document = copy.deepcopy(self.document) + document["events"][0][field] = value + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(document, 1) + + def test_schema_validation(self): + for document in [None, [], {}, {"version": True, "events": []}, {"version": 2, "events": []}, + {"version": 1, "events": []}, {"version": 1, "events": [{}]}, + dict(self.document, unknown=1)]: + with self.subTest(document=document): + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(document, 1) + + def test_repetition_bounds(self): + for repeat, gap in [(0, 0), (4097, 0), (True, 0), (1, -1), (4096, 3600000)]: + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(self.document, 1, repeat, gap) + + def test_repeated_event_count_at_capacity(self): + rows = pad_timeline.compile_timeline(self.document, 42, 2048, 0).splitlines() + self.assertEqual(rows[0], "APS5_PAD_TIMELINE 1 42 4096") + self.assertEqual(len(rows) - 1, 4096) + self.assertEqual(rows[1:3], ["200 100 18432", "500 200 64"]) + self.assertEqual(rows[2047:2051], [ + "716300 100 18432", "716600 200 64", "717000 100 18432", "717300 200 64", + ]) + self.assertEqual(rows[-2:], ["1433100 100 18432", "1433400 200 64"]) + with self.assertRaisesRegex(ValueError, "1 to 4096 events after repetition"): + pad_timeline.compile_timeline(self.document, 42, 2049, 0) + + def test_repeated_duration_includes_inter_iteration_gaps_only(self): + document = {"version": 1, "events": [{"at_ms": 0, "hold_ms": 5000, "buttons": ["cross"]}]} + self.assertEqual( + pad_timeline.compile_timeline(document, 42, 2, 3590000), + "APS5_PAD_TIMELINE 1 42 2\n0 5000 16384\n3595000 5000 16384\n") + with self.assertRaisesRegex(ValueError, "repeated timeline exceeds one hour"): + pad_timeline.compile_timeline(document, 42, 2, 3590001) + + def test_sequence_bounds(self): + for sequence in [0, -1, True, 2**64]: + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(self.document, sequence) + + def test_maximum_duration(self): + document = {"version": 1, "events": [{"at_ms": 3599999, "hold_ms": 1, "buttons": ["cross"]}]} + self.assertIn("3599999 1", pad_timeline.compile_timeline(document, 2**64 - 1)) + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(document, 1, 2) + + @unittest.skipUnless(os.name == "posix", "POSIX file permissions") + def test_replacement_preserves_shared_command_permissions(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "command" + path.write_text("previous\n", encoding="utf-8") + path.chmod(0o640) + metadata = path.stat() + pad_timeline.queue_timeline(path, "new\n") + self.assertEqual(path.read_bytes(), b"new\n") + self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o640) + self.assertEqual((path.stat().st_uid, path.stat().st_gid), (metadata.st_uid, metadata.st_gid)) + + def test_atomic_replace_and_cleanup(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "command" + pad_timeline.queue_timeline(path, "new\n") + self.assertEqual(path.read_bytes(), b"new\n") + with mock.patch("pad_timeline.os.replace", side_effect=OSError("denied")): + with self.assertRaises(OSError): + pad_timeline.queue_timeline(path, "wrong\n") + self.assertEqual(path.read_bytes(), b"new\n") + self.assertEqual(list(Path(directory).iterdir()), [path]) + + def run_cli(self, timeline, output): + return subprocess.run([sys.executable, str(Path(pad_timeline.__file__)), str(timeline), str(output)], + capture_output=True, text=True, timeout=5) + + def test_cli_roundtrip_and_invalid_input_preservation(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + timeline, output = root / "sequence.json", root / "command" + timeline.write_text(json.dumps(self.document), encoding="utf-8") + self.assertEqual(self.run_cli(timeline, output).returncode, 0) + original = output.read_bytes() + timeline.write_text('{"version":2}', encoding="utf-8") + self.assertEqual(self.run_cli(timeline, output).returncode, 2) + self.assertEqual(output.read_bytes(), original) + + def test_input_alias_protection(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + timeline = root / "sequence.json" + original = json.dumps(self.document) + timeline.write_text(original, encoding="utf-8") + self.assertEqual(self.run_cli(timeline, timeline).returncode, 2) + linked = root / "hardlink" + os.link(timeline, linked) + self.assertEqual(self.run_cli(timeline, linked).returncode, 2) + self.assertEqual(timeline.read_text(encoding="utf-8"), original) + + def test_sequence_advances_past_existing_command_after_clock_reset(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + timeline, output = root / "sequence.json", root / "command" + timeline.write_text(json.dumps(self.document), encoding="utf-8") + with mock.patch.object(sys, "argv", ["pad_timeline.py", str(timeline), str(output)]), \ + mock.patch.object(pad_input.time, "monotonic_ns", return_value=1000), \ + contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): + output.write_text("2000 16384 300\n", encoding="utf-8") + pad_timeline.main() + self.assertTrue(output.read_text(encoding="utf-8").startswith("APS5_PAD_TIMELINE 1 2001 2\n")) + pad_timeline.main() + self.assertTrue(output.read_text(encoding="utf-8").startswith("APS5_PAD_TIMELINE 1 2002 2\n")) + exhausted = f"{2**64 - 1} 16384 300\n" + output.write_text(exhausted, encoding="utf-8") + with self.assertRaises(SystemExit) as error: + pad_timeline.main() + self.assertEqual(error.exception.code, 2) + self.assertEqual(output.read_text(encoding="utf-8"), exhausted) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_performance_compare.py b/tools/tests/test_performance_compare.py new file mode 100644 index 0000000000..4016f14439 --- /dev/null +++ b/tools/tests/test_performance_compare.py @@ -0,0 +1,350 @@ +import copy +import json +import os +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])) +import performance_compare + + +SUMMARY = { + "commit": "abc123", "dirty": False, "env": {"APS5_TRACE_FRAMES": "600:4"}, + "gpu": "GPU", "driver": "Driver 1", "os": "OS 1", "resolution": "1920x1080", + "window": {"spec": "frames 600:4", "first_present": 600, "ms": 80.0}, "presents": 4, + "frame_ms": {"p50": 20.0, "p90": 40.0, "p99": 40.0, "max": 40.0}, + "gpu_busy_ms_per_present": 5.0, "worker_busy_pct": {"queue worker 0x0": 50.0}, + "top_phases_ms_per_frame": {"Driver.Packet": 12.0, "Driver.Present": 2.0}, + "trace_dropped": False, "trace": "aps5-trace-abc123-7.json.gz", +} + + +IDENTITY = {"cpu": 'CPU "X"', "cpu_threads": 16, "memory_mib": 32000, + "title_id": "PPSA00000", "title": "Example", "content_version": "01.002.000"} + + +class PerformanceCompareTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.baseline = copy.deepcopy(SUMMARY) + self.candidate = copy.deepcopy(SUMMARY) + self.candidate.update(commit="def456", trace="aps5-trace-def456-8.json.gz") + self.before = Path(self.directory.name) / "before.jsonl" + self.after = Path(self.directory.name) / "after.jsonl" + + def write(self, path, *records): + path.write_text("\n" + "\n\n".join(json.dumps(record) for record in records) + "\n", encoding="utf-8") + + def run_cli(self, *args): + return subprocess.run([sys.executable, str(Path(performance_compare.__file__)), str(self.before), str(self.after), *args], + capture_output=True, text=True, encoding="utf-8", timeout=5, + env=dict(os.environ, PYTHONIOENCODING="ascii")) + + def test_timings_and_mean_are_compared_without_inverting_percentiles(self): + self.candidate["window"]["ms"] = 40.0 + self.candidate["frame_ms"] = {"p50": 10.0, "p90": 20.0, "p99": 20.0, "max": 20.0} + self.candidate["gpu_busy_ms_per_present"] = 3.0 + self.write(self.before, self.baseline) + self.write(self.after, self.candidate) + result = self.run_cli("--json") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stderr, "") + report = json.loads(result.stdout) + self.assertEqual(report["metrics_ms"]["frame_ms.p99"], + {"baseline": 40.0, "candidate": 20.0, "change": -20.0, "change_pct": -50.0}) + self.assertEqual(report["metrics_ms"]["mean_frame_ms"]["candidate"], 10.0) + self.assertEqual(report["metrics_ms"]["gpu_busy_ms_per_present"]["change_pct"], -40.0) + self.assertEqual(report["configuration_differences"], {}) + self.assertEqual(report["baseline"], self.baseline) + self.assertEqual(report["candidate"], self.candidate) + + def test_mean_uses_each_captures_own_present_count(self): + self.candidate["presents"] = 8 + report = performance_compare.compare(self.baseline, self.candidate) + self.assertEqual(report["metrics_ms"]["mean_frame_ms"]["change_pct"], -50.0) + + def test_zero_baseline_and_missing_top_phases_are_not_fake_improvements(self): + self.baseline["gpu_busy_ms_per_present"] = 0.0 + self.candidate["top_phases_ms_per_frame"] = {"Driver.Packet": 18.0, "Driver.NewPhase": 1.0} + report = performance_compare.compare(self.baseline, self.candidate) + self.assertIsNone(report["metrics_ms"]["gpu_busy_ms_per_present"]["change_pct"]) + self.assertEqual(report["metrics_ms"]["gpu_busy_ms_per_present"]["change"], 5.0) + self.assertEqual(report["top_phases_ms_per_frame"]["Driver.Packet"]["change_pct"], 50.0) + for phase in ("Driver.Present", "Driver.NewPhase"): + self.assertIsNone(report["top_phases_ms_per_frame"][phase]["change"]) + self.assertIsNone(report["top_phases_ms_per_frame"][phase]["change_pct"]) + self.assertIn("n/a", performance_compare.markdown(report)) + + def test_exact_trace_selection_does_not_pick_the_last_session(self): + self.write(self.before, self.baseline, self.candidate) + self.write(self.after, self.candidate, self.baseline) + original = (self.before.read_bytes(), self.after.read_bytes()) + result = self.run_cli("--baseline-trace", self.baseline["trace"], "--candidate-trace", self.candidate["trace"], "--json") + self.assertEqual(result.returncode, 0, result.stderr) + report = json.loads(result.stdout) + self.assertEqual(report["baseline"]["commit"], "abc123") + self.assertEqual(report["candidate"]["commit"], "def456") + self.assertEqual((self.before.read_bytes(), self.after.read_bytes()), original) + + def test_empty_ambiguous_missing_and_duplicate_selections_fail(self): + self.write(self.after, self.candidate) + for records, selector, count in (([], [], 0), ([self.baseline, self.candidate], [], 2), + ([self.baseline], ["--baseline-trace", "absent"], 0), + ([self.baseline, self.baseline], ["--baseline-trace", self.baseline["trace"]], 2)): + with self.subTest(records=len(records), selector=selector): + self.write(self.before, *records) + result = self.run_cli(*selector) + self.assertEqual(result.returncode, 2) + self.assertEqual(result.stdout, "") + self.assertIn(f"found {count}", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_configuration_differences_and_incomplete_evidence_are_visible(self): + self.candidate.update(gpu="Other GPU", driver="Driver 2", os="OS 2", resolution="1280x720", + dirty=True, trace_dropped=True) + self.candidate["window"].update(spec="frames 800:4", first_present=800) + self.candidate["env"] = {"APS5_TRACE_FRAMES": "800:4", "APS5_WORKER_AFFINITY": "1"} + report = performance_compare.compare(self.baseline, self.candidate) + self.assertEqual(set(report["configuration_differences"]), + {"gpu", "driver", "os", "resolution", "window.spec", "window.first_present", + "env.APS5_TRACE_FRAMES", "env.APS5_WORKER_AFFINITY"}) + self.assertIsNone(report["configuration_differences"]["env.APS5_WORKER_AFFINITY"]["baseline"]) + rendered = performance_compare.markdown(report) + for text in ("Other GPU", "Driver 2", "source tree is dirty", "trace events were dropped", "do not isolate"): + self.assertIn(text, rendered) + + def test_unknown_metadata_is_not_a_claim_of_equivalent_machines(self): + self.baseline.update(dirty=None, gpu="", driver="") + report = performance_compare.compare(self.baseline, self.candidate) + notes = "\n".join(report["notes"]) + self.assertIn("state is unknown", notes) + self.assertIn("missing configuration: gpu, driver", notes) + self.assertIn("cannot verify the physical machine or scene", notes) + self.assertIn("cpu", notes) + self.assertIn("title_id", notes) + + def test_invalid_metric_and_metadata_shapes_are_rejected_with_location(self): + cases = [(("presents",), value) for value in (0, -1, True, 1.5, 10 ** 400)] + cases += [(("window", "ms"), value) for value in (0, -1, True, "10", None, float("nan"), float("inf"))] + cases += [(("frame_ms", "p50"), -1), (("frame_ms", "p50"), 50), (("frame_ms",), []), + (("gpu_busy_ms_per_present",), float("-inf")), (("window",), None), + (("window", "first_present"), True), (("env",), []), (("env",), {"APS5_TRACE_FRAMES": 4}), + (("trace_dropped",), "false"), (("dirty",), 1), (("trace",), None), + (("top_phases_ms_per_frame",), []), (("top_phases_ms_per_frame",), {"phase": -1})] + cases += [((field,), value) for field in ("cpu", "title_id", "title", "content_version") + for value in (None, True, 12, [])] + cases += [((field,), value) for field in ("cpu_threads", "memory_mib") + for value in (None, True, -1, 1.5, "16")] + for path, value in cases: + with self.subTest(path=path, value=value): + invalid = copy.deepcopy(self.baseline) + target = invalid + for key in path[:-1]: + target = target[key] + target[path[-1]] = value + self.write(self.before, invalid) + with self.assertRaisesRegex(ValueError, r"before.jsonl:2:"): + performance_compare.load_summary(self.before) + + def test_bad_json_and_missing_file_produce_only_a_cli_error(self): + self.write(self.after, self.candidate) + for content in ("{bad json", "[]", "{}", '{"frame_ms": Infinity}'): + with self.subTest(content=content): + self.before.write_text(content, encoding="utf-8") + result = self.run_cli("--json") + self.assertEqual(result.returncode, 2) + self.assertEqual(result.stdout, "") + self.assertIn("before.jsonl:1:", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.before.unlink() + result = self.run_cli() + self.assertEqual(result.returncode, 2) + self.assertEqual(result.stdout, "") + self.assertNotIn("Traceback", result.stderr) + + def test_percentage_overflow_is_an_error(self): + self.baseline["gpu_busy_ms_per_present"] = 1e-308 + self.candidate["gpu_busy_ms_per_present"] = 1e308 + self.write(self.before, self.baseline) + self.write(self.after, self.candidate) + result = self.run_cli("--json") + self.assertEqual(result.returncode, 2) + self.assertEqual(result.stdout, "") + self.assertIn("finite numeric range", result.stderr) + + def test_markdown_reports_deltas_and_escapes_table_values(self): + self.candidate["frame_ms"]["max"] = 60.0 + self.candidate["gpu"] = "GPU | \nline" + self.write(self.before, self.baseline) + self.write(self.after, self.candidate) + result = self.run_cli() + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("| frame_ms.max | 40.000 | 60.000 | +20.000 | +50.000 |", result.stdout) + self.assertIn("GPU | <vendor> line", result.stdout) + self.assertIn("abc123", result.stdout) + self.assertIn("def456", result.stdout) + + def test_utf8_paths_and_metadata_work_with_a_non_utf8_output_default(self): + self.before = Path(self.directory.name) / "avant-\u00e9.jsonl" + self.candidate["gpu"] = "GPU \u03a9" + self.write(self.before, self.baseline) + self.write(self.after, self.candidate) + for args in ((), ("--json",)): + with self.subTest(args=args): + result = self.run_cli(*args) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("GPU \u03a9", result.stdout) + self.before.write_text("bad", encoding="utf-8") + result = self.run_cli() + 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) + + def test_matching_recorded_machine_and_title_context_is_accepted(self): + self.baseline.update(IDENTITY) + self.candidate.update(IDENTITY) + self.write(self.before, self.baseline) + self.write(self.after, self.candidate) + result = self.run_cli("--require-matching-context", "--json") + self.assertEqual(result.returncode, 0, result.stderr) + report = json.loads(result.stdout) + self.assertEqual(report["configuration_differences"], {}) + self.assertEqual(report["baseline"], self.baseline) + self.assertEqual(report["candidate"], self.candidate) + self.assertNotIn("missing configuration", "\n".join(report["notes"])) + for key in ("cpu_threads", "memory_mib"): + with self.subTest(unknown=key): + self.baseline[key] = self.candidate[key] = 0 + performance_compare.validate(self.baseline) + report = performance_compare.compare(self.baseline, self.candidate) + self.assertEqual(report["configuration_differences"], {}) + self.assertIn(key, "\n".join(report["notes"])) + + def test_recorded_identity_differences_are_reported_and_rejected_in_strict_mode(self): + self.baseline.update(IDENTITY) + self.write(self.before, self.baseline) + changes = {"cpu": "Other CPU", "cpu_threads": 8, "memory_mib": 16000, + "title_id": "PPSA00001", "title": "Other title", "content_version": "01.003.000"} + for key, value in changes.items(): + with self.subTest(key=key): + candidate = {**self.candidate, **IDENTITY, key: value} + expected = {key: {"baseline": IDENTITY[key], "candidate": value}} + self.assertEqual(performance_compare.compare(self.baseline, candidate)["configuration_differences"], expected) + self.write(self.after, candidate) + result = self.run_cli("--json") + self.assertEqual(result.returncode, 0, result.stderr) + report = json.loads(result.stdout) + self.assertEqual(report["configuration_differences"], expected) + self.assertEqual(report["metrics_ms"]["frame_ms.p50"]["change"], 0) + result = self.run_cli("--require-matching-context", "--json") + self.assertEqual(result.returncode, 2, result.stderr) + self.assertEqual(result.stdout, "") + self.assertIn(f"recorded configuration differs: {key}", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_missing_identity_is_distinct_from_known_identity_but_legacy_summaries_still_work(self): + for key in IDENTITY: + for label in ("baseline", "candidate"): + with self.subTest(key=key, missing=label): + baseline = {**self.baseline, **IDENTITY} + candidate = {**self.candidate, **IDENTITY} + (baseline if label == "baseline" else candidate).pop(key) + report = performance_compare.compare(baseline, candidate) + self.assertEqual(report["configuration_differences"], + {key: {"baseline": baseline.get(key), "candidate": candidate.get(key)}}) + self.assertIn(f"{label}: missing configuration: {key}.", report["notes"]) + self.write(self.before, self.baseline) + self.write(self.after, self.candidate) + result = self.run_cli("--require-matching-context", "--json") + self.assertEqual(result.returncode, 0, result.stderr) + report = json.loads(result.stdout) + self.assertEqual(report["configuration_differences"], {}) + self.assertIn("cpu", "\n".join(report["notes"])) + self.assertIn("title_id", "\n".join(report["notes"])) + self.write(self.after, {**self.candidate, **IDENTITY}) + result = self.run_cli("--require-matching-context") + self.assertEqual(result.returncode, 2, result.stderr) + self.assertEqual(result.stdout, "") + for key in IDENTITY: + self.assertIn(key, result.stderr) + + def test_recorded_identity_differences_are_escaped_in_markdown(self): + self.baseline.update(IDENTITY) + self.candidate.update(IDENTITY, cpu="CPU | <\u03a9>\nline", title="Other title", content_version="02.000.000") + self.write(self.before, self.baseline) + self.write(self.after, self.candidate) + result = self.run_cli() + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("CPU | <\u03a9> line", result.stdout) + self.assertIn("| title | Example | Other title |", result.stdout) + self.assertIn("| content_version | 01.002.000 | 02.000.000 |", result.stdout) + + def test_matching_context_can_be_required_without_a_timing_verdict(self): + self.write(self.before, self.baseline) + self.candidate["frame_ms"]["max"] = 100.0 + self.write(self.after, self.candidate) + result = self.run_cli("--require-matching-context", "--json") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(result.stdout)["metrics_ms"]["frame_ms.max"]["change"], 60.0) + for field in ("gpu", "driver", "os", "resolution"): + with self.subTest(field=field): + candidate = copy.deepcopy(self.candidate) + candidate[field] += " changed" + self.write(self.after, candidate) + result = self.run_cli("--require-matching-context") + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn(field, result.stderr) + self.assertEqual(result.stdout, "") + self.assertEqual(self.run_cli().returncode, 0) + for key, value in (("window", dict(self.candidate["window"], first_present=900)), + ("env", {"APS5_TRACE_FRAMES": "900:4"})): + with self.subTest(key=key): + candidate = copy.deepcopy(self.candidate) + candidate[key] = value + self.write(self.after, candidate) + self.assertEqual(self.run_cli("--require-matching-context").returncode, 2) + + + + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(performance_compare.sys, "stdout", out), patch.object(performance_compare.sys, "stderr", err): + try: + performance_compare.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_pr_labels.py b/tools/tests/test_pr_labels.py new file mode 100644 index 0000000000..76298e7657 --- /dev/null +++ b/tools/tests/test_pr_labels.py @@ -0,0 +1,225 @@ +import contextlib +import copy +import io +import json +import sys +import unittest +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pr_labels + +SUMMARY = { + "commit": "abc123", "dirty": False, "env": {"APS5_TRACE_FRAMES": "600:4"}, + "gpu": "GPU", "driver": "Driver 1", "os": "OS 1", "resolution": "1920x1080", + "window": {"spec": "frames 600:4", "first_present": 600, "ms": 80.0}, "presents": 4, + "frame_ms": {"p50": 20.0, "p90": 40.0, "p99": 40.0, "max": 40.0}, + "gpu_busy_ms_per_present": 5.0, "worker_busy_pct": {"queue worker 0x0": 50.0}, + "top_phases_ms_per_frame": {"Driver.Packet": 12.0, "Driver.Present": 2.0}, + "trace_dropped": False, "trace": "aps5-trace-abc123-7.json.gz", +} +TEMPLATE = ("- [ ] Unblocks: \n" + "- [ ] Depends on: none\n") + + +def summary(commit): + record = copy.deepcopy(SUMMARY) + record["commit"] = commit + return json.dumps(record) + + +def pr(title="fix: x", body="", labels=()): + return {"number": 7, "title": title, "body": body, "labels": list(labels), "state": "open"} + + +class TitleIdTests(unittest.TestCase): + def test_forms(self): + cases = [ + (TEMPLATE, set()), + ("- [x] Unblocks: PPSA01325 #3232\n", {"PPSA01325"}), + ("- [ ] Unblocks: **PPSA01325 #3232, CUSA12345 #1**", {"PPSA01325", "CUSA12345"}), + ("- [ ] Unblocks: none\nSeen on PPSA01325.\n", set()), + ("- [x] Unblocks: none by itself; foundation for PPSA27836 in #6570\n", set()), + ("- [x] Unblocks: None (PPSA03912 #5146 is already in game; this is a speedup)\n", set()), + ("- [x] Unblocks: PPSA01325 #3232 (none of the others)\n", {"PPSA01325"}), + ("- [ ] Unblocks:\n - PPSA01325 #3232\n - PPSA00001\n- [ ] Depends on: none\n", {"PPSA01325", "PPSA00001"}), + ("- [ ] Unblocks:\n- [ ] Depends on: PPSA01325\n", set()), + ("- [ ] Unblocks: \n", set()), + ("- [x] Unblocks: PPSA01325_00 #3232\n", {"PPSA01325"}), + ("- [x] Unblocks: PPSA013250, XPPSA01325\n", set()), + ("", set()), + ] + for body, expected in cases: + with self.subTest(body=body): + self.assertEqual(pr_labels.title_ids(body), expected) + + def test_none_body(self): + self.assertEqual(pr_labels.title_ids(None), set()) + + +class PerfTests(unittest.TestCase): + def test_titles(self): + for title, expected in [("perf(agc): x", True), ("perf: x", True), ("perf!: x", True), ("perf(agc)!: x", True), + ("fix(perf): x", False), ("feat: perf", False), ("performance: x", False)]: + with self.subTest(title=title): + self.assertEqual(pr_labels.is_perf(title), expected) + + +class NumbersTests(unittest.TestCase): + def test_counts(self): + cases = [ + ("", False), + (summary("a"), False), + (summary("a") + "\n" + summary("b"), True), + ("before\n" + summary("a") + "\nafter\n" + summary("b") + "\n", True), + (summary("a") + "\n" + summary("a"), False), + (summary("a") + "\n{not json", False), + ("{}\n{}", False), + ("```json\n" + json.dumps(json.loads(summary("a")), indent=2) + "\n```\n```json\n" + summary("b") + "\n```", True), + ("~~~json\n" + json.dumps(json.loads(summary("a")), indent=2) + "\n~~~\n~~~json\n" + summary("b") + "\n~~~", True), + ("\n" + summary("b"), False), + (summary("a") + "\r\n" + summary("b") + "\r\n", True), + (summary("a") + "\n{" + "[" * 100000 + "\n" + summary("b"), True), + ] + for body, expected in cases: + with self.subTest(body=body[:40]): + self.assertEqual(pr_labels.has_numbers(body), expected) + + def test_invalid_summary_not_counted(self): + broken = json.loads(summary("b")) + broken["presents"] = 0 + self.assertFalse(pr_labels.has_numbers(summary("a") + "\n" + json.dumps(broken))) + + +class LabelTests(unittest.TestCase): + def test_labels(self): + self.assertEqual(pr_labels.labels_for(pr()), (set(), set())) + self.assertEqual(pr_labels.labels_for(pr("perf: x", TEMPLATE)), ({"perf", "perf numbers missing"}, set())) + both = TEMPLATE + summary("a") + "\n" + summary("b") + self.assertEqual(pr_labels.labels_for(pr("perf: x", both, ["perf", "perf numbers missing"])), (set(), {"perf numbers missing"})) + self.assertEqual(pr_labels.labels_for(pr("fix: x", "Unblocks: PPSA01325", ["perf", "other"])), ({"unblocks title"}, {"perf"})) + self.assertEqual(pr_labels.labels_for(pr("fix: x", "Unblocks: none", ["unblocks title", "other"])), (set(), {"unblocks title"})) + + +class ProcessTests(unittest.TestCase): + def run_process(self, pull, comments=0, dry_run=False, comment=True): + calls = [] + + def fake(*args): + calls.append(args) + return "" + + with patch.object(pr_labels, "gh", side_effect=fake), patch.object(pr_labels, "commented", return_value=comments) as counted: + result = pr_labels.process("o/r", pull, dry_run, comment) + self.comment_lookups = counted.call_count + return result, calls + + def test_backfill_labels_without_commenting(self): + result, calls = self.run_process(pr("perf: x", TEMPLATE), comment=False) + self.assertEqual(result, ({"perf", "perf numbers missing"}, set(), False)) + self.assertEqual(calls, [("pr", "edit", "7", "-R", "o/r", "--add-label", "perf,perf numbers missing")]) + self.assertEqual(self.comment_lookups, 0) + + def test_no_edit_when_labels_match(self): + result, calls = self.run_process(pr("perf: x", TEMPLATE, ["perf", "perf numbers missing"]), comments=1) + self.assertEqual(result, (set(), set(), False)) + self.assertEqual(calls, []) + + def test_edit_and_single_comment(self): + result, calls = self.run_process(pr("perf: x", TEMPLATE)) + self.assertEqual(result, ({"perf", "perf numbers missing"}, set(), True)) + self.assertEqual(calls[0], ("pr", "edit", "7", "-R", "o/r", "--add-label", "perf,perf numbers missing")) + self.assertIn("-f", calls[1]) + self.assertIn(pr_labels.MARKER, calls[1][-1]) + self.assertEqual(len(calls), 2) + + def test_comment_not_posted_twice(self): + result, calls = self.run_process(pr("perf: x", TEMPLATE, ["perf", "perf numbers missing"]), comments=1) + self.assertFalse(result[2]) + result, calls = self.run_process(pr("perf: x", TEMPLATE), comments=1) + self.assertEqual(len(calls), 1) + + def test_comment_restored_for_labeled_pr_without_one(self): + result, calls = self.run_process(pr("perf: x", TEMPLATE, ["perf", "perf numbers missing"])) + self.assertTrue(result[2]) + self.assertEqual(len(calls), 1) + + def test_numbers_present_removes_label_keeps_comment(self): + body = summary("a") + "\n" + summary("b") + result, calls = self.run_process(pr("perf: x", body, ["perf", "perf numbers missing"]), comments=1) + self.assertEqual(result, (set(), {"perf numbers missing"}, False)) + self.assertEqual(calls, [("pr", "edit", "7", "-R", "o/r", "--remove-label", "perf numbers missing")]) + + def test_dry_run_writes_nothing(self): + result, calls = self.run_process(pr("perf: x", TEMPLATE), dry_run=True) + self.assertEqual(result[2], True) + self.assertEqual(calls, []) + + def test_closed_pull_request_ignored(self): + pull = pr("perf: x", TEMPLATE) + pull["state"] = "closed" + self.assertEqual(self.run_process(pull)[1], []) + + +class MainTests(unittest.TestCase): + def test_failed_pull_request_does_not_stop_the_others(self): + listed = [{"number": number, "title": "perf: x", "body": "", "labels": [], "state": "open"} for number in (1, 2)] + edited = [] + + def fake(*args): + if args[0] == "api": + return json.dumps(listed) + edited.append(args[2]) + if args[2] == "1": + raise pr_labels.subprocess.CalledProcessError(1, "gh", stderr="HTTP 403") + return "" + + with patch.object(pr_labels, "gh", side_effect=fake), patch.object(pr_labels, "WRITE_PAUSE", 0), \ + patch.dict(pr_labels.os.environ, {"GITHUB_REPOSITORY": "o/r"}): + with self.assertRaises(SystemExit) as raised: + pr_labels.main(["--all"]) + self.assertEqual(edited, ["1", "2"]) + self.assertIn("#1", str(raised.exception.code)) + + def test_main_handles_called_process_error_when_fetching_pr(self): + def fake(*args): + raise pr_labels.subprocess.CalledProcessError(1, "gh", stderr="PR not found") + + with patch.object(pr_labels, "gh", side_effect=fake), \ + patch.dict(pr_labels.os.environ, {"GITHUB_REPOSITORY": "o/r"}), \ + contextlib.redirect_stderr(io.StringIO()): + with self.assertRaises(SystemExit) as raised: + pr_labels.main(["--pr", "999"]) + self.assertEqual(raised.exception.code, 2) + + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(pr_labels.sys, "stdout", out), patch.object(pr_labels.sys, "stderr", err): + try: + pr_labels.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_pr_overlap.py b/tools/tests/test_pr_overlap.py index ed4f4e4b25..4017ab3718 100644 --- a/tools/tests/test_pr_overlap.py +++ b/tools/tests/test_pr_overlap.py @@ -1,8 +1,14 @@ import json +import os +import re +import shutil +import subprocess import sys +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 @@ -77,6 +83,23 @@ class OpenPullRequestsTests(unittest.TestCase): pr_overlap.open_prs("main") +class TopInsertTests(unittest.TestCase): + def test_top_of_file_insertions_clash_on_line_one(self): + pr = {"tree": "t", "files": {"f.cpp"}, "added": set()} + with patch.object(pr_overlap, "git", return_value="@@ -0,0 +1 @@\n+x\n"): + self.assertEqual(pr_overlap.hunks(pr, "f.cpp"), [(1, 1)]) + self.assertEqual(pr_overlap.clashes(pr, pr, "f.cpp"), [(1, 1)]) + + def test_top_of_file_span_links_to_line_one(self): + self.assertEqual(pr_overlap.spans({"f.cpp": [(1, 1)]}, "https://x/blob/sha"), + "`f.cpp` [L1](https://x/blob/sha/f.cpp#L1)") + + def test_ordinary_hunk_is_unchanged(self): + pr = {"tree": "t"} + with patch.object(pr_overlap, "git", return_value="@@ -3,2 +3,2 @@\n-a\n-b\n+c\n+d\n"): + self.assertEqual(pr_overlap.hunks(pr, "f.cpp"), [(3, 4)]) + + class FetchTests(unittest.TestCase): def test_fetches_each_head_commit_the_api_reported(self): prs = { @@ -116,6 +139,291 @@ class ConflictTests(unittest.TestCase): self.assertEqual(pr_overlap.conflicts(first, second), {}) merge.assert_called_once_with("first-tree", "second-tree") + def real_pull_requests(self, main_changes, older_changes, current_changes): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + root = Path(temporary.name).resolve() + self.assertTrue(root.is_relative_to(Path(tempfile.gettempdir()).resolve())) + self.addCleanup(os.chdir, os.getcwd()) + os.chdir(root) + + def git(*args): + return subprocess.run( + ["git", "-c", "user.name=Overlap Test", "-c", "user.email=test@example.com", *args], + capture_output=True, text=True, encoding="utf-8", check=True).stdout.strip() + + git("init", "-q") + git("config", "commit.gpgsign", "false") + for name in ("shared.cpp", "other.cpp", "unrelated.cpp"): + (root / name).write_text("int value = 0;\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "test: create base") + base = git("rev-parse", "HEAD") + + def branch(name, parent, changes): + git("checkout", "-qb", name, parent) + for path, value in changes.items(): + (root / path).write_text(f"int value = {value};\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "test: change values", "--allow-empty") + return git("rev-parse", "HEAD") + + older = branch("older", base, older_changes) + main = branch("updated-base", base, main_changes) + current = branch("current", main, current_changes) + git("update-ref", "refs/pr/base", main) + prs = {1: {"ref": current, "author": "current-author", "depends": set()}, + 2: {"ref": older, "author": "older-author", "depends": set()}} + for pr in prs.values(): + pr_overlap.scan(pr) + return prs + + def test_reports_shared_files_when_older_pr_conflicts_with_main(self): + prs = self.real_pull_requests({"shared.cpp": 2}, {"shared.cpp": 1}, {"shared.cpp": 3}) + self.assertIsNone(prs[2]["tree"]) + self.assertIsNotNone(prs[1]["tree"]) + found = pr_overlap.overlaps(prs, 1, 2) + self.assertIsNotNone(found) + self.assertEqual(found["uncompared"], {"shared.cpp"}) + report = pr_overlap.report({2: found}, "https://example.invalid/blob/base") + self.assertIn("could not be completed", report) + self.assertIn("`shared.cpp`", report) + self.assertNotIn("Merging one will break the other", report) + + def test_conflict_in_another_file_does_not_hide_shared_changes(self): + prs = self.real_pull_requests({"other.cpp": 2}, {"other.cpp": 1, "shared.cpp": 1}, {"shared.cpp": 3}) + self.assertIsNone(prs[2]["tree"]) + found = pr_overlap.overlaps(prs, 1, 2) + self.assertIsNotNone(found) + self.assertEqual(found["uncompared"], {"shared.cpp"}) + + def test_unmergeable_pr_with_no_shared_files_is_not_reported(self): + prs = self.real_pull_requests({"other.cpp": 2}, {"other.cpp": 1}, {"unrelated.cpp": 3}) + self.assertIsNone(prs[2]["tree"]) + self.assertIsNone(pr_overlap.overlaps(prs, 1, 2)) + + def test_mergeable_branches_still_report_confirmed_line_conflicts(self): + prs = self.real_pull_requests({}, {"shared.cpp": 1}, {"shared.cpp": 2}) + self.assertTrue(all(pr["tree"] for pr in prs.values())) + found = pr_overlap.overlaps(prs, 1, 2) + self.assertEqual(found["conflicts"], {"shared.cpp": [(1, 1)]}) + self.assertNotIn("uncompared", found) + + def test_mixed_report_keeps_confirmed_and_uncompared_sections(self): + rows = {2: {"added": set(), "conflicts": {"other.cpp": [(1, 1)]}}, + 3: {"added": set(), "uncompared": {"shared.cpp"}}} + report = pr_overlap.report(rows, "https://example.invalid/blob/base") + self.assertIn("[L1]", report) + self.assertIn("could not be completed", report) + self.assertIn("`shared.cpp`", report) + self.assertTrue(report.startswith(pr_overlap.MARKER)) + + def test_report_links_pull_requests_without_cross_references(self): + rows = {2: {"added": set(), "conflicts": {"other.cpp": [(1, 1)]}}, + 3: {"added": set(), "uncompared": {"shared.cpp"}}} + report = pr_overlap.report(rows, "https://github.com/owner/project/blob/base") + self.assertIsNone(re.search(r"#\d|https://github\.com/owner/project/(pull|issues)/", report)) + self.assertIn("[PR 2](https://www.github.com/owner/project/pull/2)", report) + self.assertIn("[PR 3](https://www.github.com/owner/project/pull/3)", report) + + +class UnicodePathTests(unittest.TestCase): + @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: + 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() + + 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") + git("init", "-q") + git("config", "core.quotePath", "true") + 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) + source.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"], {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) + + +class SubdirectoryTests(unittest.TestCase): + @unittest.skipIf(os.name == "nt", "fake gh is a shell script") + def test_branch_report_from_a_subdirectory_matches_the_root(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory).resolve() / "repo" + fake = Path(directory) / "bin" + fake.mkdir() + + 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() + + filename = "core/libs/prx/libSceExample/Export.cpp" + source = root / filename + source.parent.mkdir(parents=True) + source.write_text("int value = 0;\n", encoding="utf-8") + (root / "tools").mkdir() + (root / "tools" / "tool.py").write_text("print()\n", encoding="utf-8") + git("init", "-q", "-b", "main") + git("add", ".") + git("commit", "-qm", "test: create base") + heads = {} + for branch, value in (("theirs", 1), ("mine", 2)): + git("checkout", "-qb", branch, "main") + source.write_text(f"int value = {value};\nint APS5_VABI sceExample() {{ return {value}; }}\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "fix: change value") + heads[branch] = git("rev-parse", "HEAD") + pulls = [{"number": 7, "user": {"login": "someone"}, "head": {"sha": heads["theirs"]}, "body": "Depends on: none"}] + (fake / "pulls.json").write_text(json.dumps(pulls), encoding="utf-8") + (fake / "gh").write_text(f"#!/bin/sh\ncat '{fake / 'pulls.json'}'\n", encoding="utf-8") + (fake / "gh").chmod(0o755) + environment = {**os.environ, "PATH": f"{fake}{os.pathsep}{os.environ['PATH']}", "GIT_CONFIG_COUNT": "1", + "GIT_CONFIG_KEY_0": f"url.{root.as_uri()}.insteadOf", "GIT_CONFIG_VALUE_0": "https://github.com/owner/project.git"} + environment.pop("GITHUB_REPOSITORY", None) + script = Path(pr_overlap.__file__).resolve() + outputs = {} + for subdirectory in (".", "tools", "core/libs/prx"): + result = subprocess.run([sys.executable, str(script), "--repo", "owner/project", "--branch", "mine"], + cwd=root / subdirectory, env=environment, capture_output=True, text=True, + encoding="utf-8", timeout=60) + self.assertEqual(result.returncode, 0, result.stderr) + outputs[subdirectory] = result.stdout + self.assertIn(f"`{filename}` [L1](", outputs["."]) + self.assertIn("| `sceExample` |", outputs["."]) + self.assertEqual(outputs["tools"], outputs["."]) + self.assertEqual(outputs["core/libs/prx"], outputs["."]) + result = subprocess.run([sys.executable, str(script), "--repo", "owner/project", "--out", "reports"], + cwd=root / "tools", env=environment, capture_output=True, text=True, + encoding="utf-8", timeout=60) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue((root / "tools" / "reports" / "7.md").is_file()) + + +class CommentStateTests(unittest.TestCase): + def setUp(self): + if os.name == "nt" or not all(shutil.which(tool) for tool in ("bash", "jq", "sed", "sha256sum")): + self.skipTest("the workflow step runs in bash with jq, sed and sha256sum on Linux") + lines = (Path(__file__).resolve().parents[2] / ".github/workflows/pr-overlap.yml").read_text(encoding="utf-8").splitlines() + start = next(index for index, line in enumerate(lines) if "tools/pr_overlap.py" in line) + indent = len(lines[start]) - len(lines[start].lstrip()) + while start > 0 and len(lines[start - 1]) - len(lines[start - 1].lstrip()) == indent: + start -= 1 + step = [] + for line in lines[start:]: + if line.strip() and len(line) - len(line.lstrip()) < indent: + break + step.append(line[indent:]) + self.step = "\n".join(step).replace("${{ github.event.repository.default_branch }}", "main") + self.temp = tempfile.TemporaryDirectory() + self.root = Path(self.temp.name) + (self.root / "overlap").mkdir() + + def tearDown(self): + self.temp.cleanup() + + def report(self, commit, lines): + found = {7: {"conflicts": {"core/a.cpp": [lines]}, "added": set()}} + 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_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([ + "set -eo pipefail", + "python3() { :; }", + 'gh() { printf \'%s\\n\' "$*" >> "$RUNNER_TEMP/calls"; case "$*" in "api repos/"*) cat "$RUNNER_TEMP/comment";; esac; }', + self.step, + ]) + (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) + return [call.split(" ")[:3] for call in calls.read_text(encoding="utf-8").splitlines()] + + def test_new_main_commit_alone_makes_no_requests(self): + first = self.report("a" * 40, (3, 4)) + self.assertEqual(self.run_step(first, None), [["api", "repos/owner/project/issues/42/comments", "--paginate"], ["pr", "comment", "42"]]) + self.assertEqual(self.run_step(self.report("b" * 40, (3, 4)), first), []) + + def test_lost_state_reads_the_comment_without_rewriting_it(self): + (self.root / "overlap-state.json").write_text("{}", encoding="utf-8") + self.assertEqual(self.run_step(self.report("b" * 40, (3, 4)), self.report("a" * 40, (3, 4))), [["api", "repos/owner/project/issues/42/comments", "--paginate"]]) + + def test_changed_lines_still_update_the_comment(self): + first = self.report("a" * 40, (3, 4)) + self.run_step(first, None) + self.assertEqual(self.run_step(self.report("b" * 40, (5, 6)), first), [["api", "repos/owner/project/issues/42/comments", "--paginate"], ["api", "-X", "PATCH"]]) if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_pr_overlap_dependencies.py b/tools/tests/test_pr_overlap_dependencies.py index bcff60153c..9af379f868 100644 --- a/tools/tests/test_pr_overlap_dependencies.py +++ b/tools/tests/test_pr_overlap_dependencies.py @@ -47,6 +47,24 @@ class DependencyTests(unittest.TestCase): with self.subTest(body=body): self.assertEqual(pull_requests(body)[1]["depends"], {2, 3}) + def test_markdown_links_and_full_urls(self): + for body in ( + "- [x] Depends on: [#2](https://github.com/owner/repo/pull/2)", + "- [x] Depends on: https://github.com/owner/repo/pull/2", + "- [x] Depends on: [PR #2](https://github.com/owner/repo/pull/2)", + "- [x] Depends on: https://github.com/owner/repo/pull/2/files", + "- [x] Depends on: https://github.com/owner/repo/pull/2#issuecomment-1", + "- [x] Depends on: https://github.com/BOYKOPOVAR/AnyPS5/pull/2", + "Depends on: [#2](https://github.com/owner/repo/pull/2), [#3](https://github.com/owner/repo/pull/3)", + "Depends on:\n- [#2](https://github.com/owner/repo/pull/2)\n- [#3](https://github.com/owner/repo/pull/3)", + "- [x] Depends on: [none](https://example.com)", + "- [x] Depends on: https://github.com/shadps4-emu/shadPS4/pull/1234", + "- [x] Depends on: [#1234](https://github.com/shadps4-emu/shadPS4/pull/1234)", + ): + with self.subTest(body=body): + expected = set() if "none" in body or "shadps4" in body else ({2, 3} if "3" in body else {2}) + self.assertEqual(pull_requests(body)[1]["depends"], expected) + def test_later_references_are_not_dependencies(self): for newline in ("\n", "\r\n"): for declaration in ("Depends on #2.", "Depends on: #2"): @@ -94,6 +112,12 @@ class DependencyTests(unittest.TestCase): with self.subTest(body=body): self.assertEqual(pull_requests(body)[1]["depends"], expected) + def test_empty_colon_declaration_does_not_hide_a_later_one(self): + for body in ("- [ ] Depends on: \n\nDepends on: #2, #3", + "Depends on:\n\nDepends on: #2 and #3"): + with self.subTest(body=body): + self.assertEqual(pull_requests(body)[1]["depends"], {2, 3}) + def test_checklist_none_wins_over_prose_with_placeholder_comment(self): body = "Depends on #2.\n- [x] Depends on: none " self.assertEqual(pull_requests(body)[1]["depends"], set()) diff --git a/tools/tests/test_pr_overlap_encoding.py b/tools/tests/test_pr_overlap_encoding.py index 73082080d6..1dad26c746 100644 --- a/tools/tests/test_pr_overlap_encoding.py +++ b/tools/tests/test_pr_overlap_encoding.py @@ -26,7 +26,7 @@ def simulated_cp1252(): wrapper = io.TextIOWrapper def fake_wrapper(buffer, encoding=None, *args, **kwargs): - if encoding == "locale": + if encoding in (None, "locale"): encoding = "cp1252" return wrapper(buffer, encoding=encoding, *args, **kwargs) @@ -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_errors.py b/tools/tests/test_pr_overlap_errors.py new file mode 100644 index 0000000000..f85c7c3267 --- /dev/null +++ b/tools/tests/test_pr_overlap_errors.py @@ -0,0 +1,69 @@ +import runpy +import subprocess +import sys +import unittest +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pr_overlap + +SCRIPT = Path(pr_overlap.__file__) +MISSING_REF = "fatal: couldn't find remote ref refs/heads/upstream/main" + + +def fake_run(command, **kwargs): + if command[0] == "gh": + return subprocess.CompletedProcess(command, 0, stdout="[]", stderr="") + return subprocess.CompletedProcess(command, 128, stdout=b"", stderr=MISSING_REF.encode()) + + +class ErrorReportTests(unittest.TestCase): + def test_failure_names_the_command_the_exit_code_and_the_git_message(self): + error = subprocess.CalledProcessError(128, ["git", "fetch", "-q"], output="", stderr=MISSING_REF + "\n") + message = pr_overlap.describe_failure(error) + self.assertEqual(message, "pr_overlap: git fetch -q failed with exit code 128: " + MISSING_REF) + + def test_failure_without_output_says_so(self): + error = subprocess.CalledProcessError(1, ["gh", "api"], output="", stderr="") + self.assertEqual(pr_overlap.describe_failure(error), "pr_overlap: gh api failed with exit code 1: no error output") + + def test_unknown_base_branch_exits_with_the_git_message_instead_of_a_traceback(self): + argv = ["pr_overlap.py", "--repo", "owner/project", "--base", "upstream/main", "--branch", "HEAD"] + with patch.object(subprocess, "run", side_effect=fake_run), patch.object(sys, "argv", argv): + with self.assertRaises(SystemExit) as raised: + runpy.run_path(str(SCRIPT), run_name="__main__") + self.assertIn("exit code 128", str(raised.exception.code)) + self.assertIn(MISSING_REF, str(raised.exception.code)) + + + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(pr_overlap.sys, "stdout", out), patch.object(pr_overlap.sys, "stderr", err): + try: + pr_overlap.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_pr_overlap_exports.py b/tools/tests/test_pr_overlap_exports.py index eef9292aa7..d109c06966 100644 --- a/tools/tests/test_pr_overlap_exports.py +++ b/tools/tests/test_pr_overlap_exports.py @@ -25,7 +25,16 @@ diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkerne class ExportTests(unittest.TestCase): def test_added_definitions_are_exports(self): - self.assertEqual(pr_overlap.exports(DIFF), {"mlock_nid_postfix", "sceKernelRead", "getpid_nid_postfix", "sceNpUniversalDataSystemCreateEvent"}) + self.assertEqual(pr_overlap.exports(DIFF), {"mlock", "sceKernelRead", "getpid", "sceNpUniversalDataSystemCreateEvent"}) + + def test_nid_postfix_aliases_match_plain_export_names(self): + suffixed = "+int APS5_VABI sceExample_nid_postfix() { return 0; }\n" + plain = "+int APS5_VABI sceExample() { return 0; }\n" + self.assertEqual(pr_overlap.exports(suffixed), pr_overlap.exports(plain)) + + def test_nid_disambiguation_suffix_is_normalized(self): + diff = "+int APS5_VABI sceExample_nid_disambig12_nid_postfix() { return 0; }\n" + self.assertEqual(pr_overlap.exports(diff), {"sceExample"}) def test_cut_helpers_are_not_export_overlaps(self): diff = DIFF + '+void APS5_VABI Helper_nid_no_patch_cut() {\n' @@ -44,5 +53,23 @@ class ExportTests(unittest.TestCase): self.assertNotIn("Helper_nid_no_patch", found) + def test_explicit_export_aliases_are_collected(self): + diff = ('+int APS5_VABI placeholder_nid_no_patch() {\n' + '+ NotImplemented_nid_no_patch(__func__);\n' + '+}\n' + '+APS5_EXPORT("Pz262plj20w", placeholder_nid_no_patch);\n' + '-APS5_EXPORT("sKneJjUXWkQ", removed_nid_no_patch);\n') + self.assertEqual(pr_overlap.exports(diff), set()) + self.assertEqual(pr_overlap.aliases(diff), {"Pz262plj20w"}) + self.assertEqual(pr_overlap.aliases(diff.replace('+APS5_EXPORT("Pz262plj20w", placeholder_nid_no_patch);\n', "")), set()) + + def test_explicit_alias_overlaps_named_export_with_the_same_nid(self): + named = {"author": "a", "exports": {"FT_Outline_GetInsideBorder"}, "aliases": set()} + explicit = {"author": "b", "exports": set(), "aliases": {"Pz262plj20w"}} + self.assertEqual(pr_overlap.same_exports(named, explicit, []), {"FT_Outline_GetInsideBorder"}) + self.assertEqual(pr_overlap.same_exports(explicit, dict(explicit, author="c"), []), {"Pz262plj20w"}) + self.assertEqual(pr_overlap.same_exports(named, explicit, [explicit]), set()) + + if __name__ == "__main__": unittest.main() 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.py b/tools/tests/test_progress.py index bf1ea28953..1db231c27f 100644 --- a/tools/tests/test_progress.py +++ b/tools/tests/test_progress.py @@ -1,3 +1,5 @@ +import io +import json import sys import tempfile import unittest @@ -8,6 +10,102 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import progress +class ProgressSnapshotTests(unittest.TestCase): + def snapshot(self): + return {"libraries": {"percent": 0, "groups": []}, "shaders": {"percent": 0, "groups": []}} + + def assert_rejected(self, snapshot, message): + for position in (0, 1): + with self.subTest(position=position), tempfile.TemporaryDirectory() as directory: + paths = [Path(directory) / "base snapshot.json", Path(directory) / "head snapshot.json"] + snapshots = [self.snapshot(), self.snapshot()] + snapshots[position] = snapshot + for path, data in zip(paths, snapshots): + path.write_text(json.dumps(data), encoding="utf-8") + stdout, stderr = io.StringIO(), io.StringIO() + with patch("sys.stdout", stdout), patch("sys.stderr", stderr): + with self.assertRaises(SystemExit) as raised: + progress.main(["--compare", *map(str, paths)]) + self.assertEqual(raised.exception.code, 2) + self.assertEqual(stdout.getvalue(), "") + self.assertIn(str(paths[position]), stderr.getvalue()) + self.assertIn(message, stderr.getvalue()) + self.assertNotIn("Traceback", stderr.getvalue()) + + def test_non_object_snapshots_are_usage_errors(self): + for snapshot in (None, [], "progress", 7, True): + with self.subTest(snapshot=snapshot): + self.assert_rejected(snapshot, "snapshot must be an object") + + def test_sections_must_be_present_objects(self): + for section in ("libraries", "shaders"): + for value in (None, [], "progress", 7): + with self.subTest(section=section, value=value): + snapshot = self.snapshot() + snapshot[section] = value + self.assert_rejected(snapshot, section + " must be an object") + snapshot = self.snapshot() + del snapshot[section] + self.assert_rejected(snapshot, section + " must be an object") + + def test_groups_must_be_lists_of_named_objects(self): + for section in ("libraries", "shaders"): + for groups in (None, {}, "groups", [None], ["group"], [{}], [{"name": []}]): + with self.subTest(section=section, groups=groups): + snapshot = self.snapshot() + snapshot[section]["groups"] = groups + self.assert_rejected(snapshot, section + ".groups") + snapshot = self.snapshot() + del snapshot[section]["groups"] + self.assert_rejected(snapshot, section + ".groups") + + def test_symbol_lists_cannot_be_strings_or_unhashable_values(self): + for field in ("done_names", "todo_names", "inert_names"): + for value in ("Ready", None, {}, [7], [["Ready"]]): + with self.subTest(field=field, value=value): + snapshot = self.snapshot() + snapshot["libraries"]["groups"] = [{"name": "libSceExample", field: value}] + self.assert_rejected(snapshot, "." + field + " must be a list of strings") + + def test_shared_source_owners_must_be_strings(self): + for value in (None, [], {}, 7, True): + with self.subTest(value=value): + snapshot = self.snapshot() + snapshot["libraries"]["groups"] = [{"name": "libSceExample.native", "shared_sources": value}] + self.assert_rejected(snapshot, ".shared_sources must be a string") + + def test_percentages_must_be_finite_numbers_in_range(self): + for section in ("libraries", "shaders"): + for value in (None, "0", True, [], {}, -1, 101, float("nan"), float("inf"), -float("inf")): + with self.subTest(section=section, value=value): + snapshot = self.snapshot() + snapshot[section]["percent"] = value + self.assert_rejected(snapshot, section + ".percent") + snapshot = self.snapshot() + del snapshot[section]["percent"] + self.assert_rejected(snapshot, section + ".percent") + + def test_valid_snapshots_keep_reports_and_optional_fields_compatible(self): + base, head = self.snapshot(), self.snapshot() + base["libraries"]["groups"] = [{"name": "libSceExample", "todo_names": ["Ready"]}, {"name": "libEmpty"}] + head["libraries"]["groups"] = [{"name": "libSceExample", "done_names": ["Ready"], "future_field": True}, + {"name": "libEmpty"}, {"name": "libSceExample.native", "shared_sources": "libSceExample"}] + head["libraries"]["percent"] = 100 + expected = progress.report(base, head) + self.assertIn("Ready", expected) + with tempfile.TemporaryDirectory() as directory: + paths = [Path(directory) / "base.json", Path(directory) / "head.json"] + for path, snapshot in zip(paths, (base, head)): + path.write_text(json.dumps(snapshot), encoding="utf-8") + stdout = io.StringIO() + with patch("sys.stdout", stdout): + self.assertEqual(progress.main(["--compare", *map(str, paths)]), 0) + self.assertEqual(stdout.getvalue(), expected) + with patch("sys.stdout", stdout): + self.assertEqual(progress.main(["--compare", str(paths[0]), str(paths[0])]), 0) + self.assertEqual(stdout.getvalue(), expected) + + class ProgressTests(unittest.TestCase): def test_raw_string_example_does_not_declare_an_export(self): source = 'const char* example = R"help(\nint APS5_VABI Fake() { return 0; }\n)help";\n' @@ -280,5 +378,139 @@ class ProgressTests(unittest.TestCase): self.assertNotIn("", html) + def test_qualified_calls_match_only_the_global_wrapper(self): + wrapper = "static void Fail() { NotImplemented_nid_no_patch(__func__); }\n" + cases = {"obj->Fail();": "done", "obj.Fail();": "done", "X::Fail();": "done", + "Foo::Fail();": "done", "::Fail();": "todo", ":: Fail ();": "todo", "Fail();": "todo", + "return ::Fail();": "todo", "else ::Fail();": "todo", "if (obj) ::Fail();": "todo"} + for call, expected in cases.items(): + with self.subTest(call=call), tempfile.TemporaryDirectory() as directory: + library = Path(directory) / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_text(wrapper + "int APS5_VABI Run(Obj* obj) { " + call + " return 0; }\n") + group = progress.scan_library(library) + self.assertEqual(group[expected + "_names"], ["Run"]) + + def library_snapshot(self): + libraries = progress.summarize([ + {"name": "libScePad", "label": "Pad", "done": 1, "todo": 1, + "done_names": ["Ready"], "todo_names": ["Pending"], "inert_names": []}, + {"name": "libSceVideoOut", "label": "VideoOut", "done": 3, "todo": 1, + "done_names": ["A", "B", "C"], "todo_names": ["D"], "inert_names": []}, + {"name": "libScePad.native", "label": "Pad.native", "done": 0, "todo": 0, + "done_names": [], "todo_names": [], "inert_names": [], "shared_sources": "libScePad"}, + ]) + shaders = {"done": 2, "total": 4, "percent": 50.0, "groups": [], "extra": []} + return libraries, shaders + + def run_main(self, argv, libraries, shaders): + stdout = io.StringIO() + with patch.object(progress, "collect_libraries", return_value=libraries), \ + patch.object(progress, "collect_shaders", return_value=shaders), \ + patch("sys.stdout", stdout): + code = progress.main(argv) + return code, stdout.getvalue() + + def test_module_filter_recomputes_library_totals(self): + libraries, shaders = self.library_snapshot() + selected = progress.select_libraries(libraries, ["libSceVideoOut", "libScePad", "libScePad"]) + self.assertEqual([group["name"] for group in selected["groups"]], ["libScePad", "libSceVideoOut"]) + self.assertEqual((selected["done"], selected["total"], selected["percent"]), (4, 6, 66.67)) + self.assertEqual(shaders["total"], 4) + wrapper = progress.select_libraries(libraries, ["libScePad.native"]) + self.assertEqual(wrapper["groups"][0]["shared_sources"], "libScePad") + self.assertEqual((wrapper["done"], wrapper["total"]), (0, 0)) + with self.assertRaisesRegex(ValueError, "unknown libraries: libSceNope, libSceMissing"): + progress.select_libraries(libraries, ["libScePad", "libSceNope", "libSceMissing", "libSceNope"]) + + def test_json_and_module_do_not_require_an_output_directory(self): + libraries, shaders = self.library_snapshot() + code, output = self.run_main(["--json", "--module", "libScePad"], libraries, shaders) + self.assertEqual(code, 0) + payload = json.loads(output) + self.assertEqual(payload["libraries"]["groups"][0]["name"], "libScePad") + self.assertEqual((payload["libraries"]["done"], payload["libraries"]["total"]), (1, 2)) + self.assertEqual(payload["shaders"], shaders) + self.assertNotIn("libraries 1/2", output) + code, output = self.run_main(["--module", "libScePad"], libraries, shaders) + self.assertEqual(output, "libraries 1/2 (50.0%)\nshaders 2/4 (50.0%)\n") + stderr = io.StringIO() + with patch("sys.stderr", stderr), self.assertRaises(SystemExit) as raised: + progress.main([]) + self.assertEqual(raised.exception.code, 2) + self.assertIn("the output directory is required", stderr.getvalue()) + + def test_module_filter_is_what_the_site_writes(self): + libraries, shaders = self.library_snapshot() + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) + code, text = self.run_main(["--json", "--module", "libScePad", str(output)], libraries, shaders) + self.assertEqual(code, 0) + written = json.loads((output / "progress.json").read_text(encoding="utf-8")) + self.assertEqual(written, json.loads(text)) + self.assertEqual(written["libraries"]["groups"][0]["name"], "libScePad") + self.assertEqual(written["shaders"]["done"], 2) + page = (output / "progress.html").read_text(encoding="utf-8") + self.assertIn("libScePad", page) + self.assertNotIn("libSceVideoOut", page) + for name in ("badge-libraries.svg", "badge-shaders.svg", "progress.svg", "progress.html"): + self.assertTrue((output / name).is_file()) + original = progress.PRX + with tempfile.TemporaryDirectory() as directory: + self.run_main(["--root", directory, "--json"], libraries, shaders) + self.assertEqual(progress.PRX, original) + + def test_compare_stays_a_report_of_two_snapshots(self): + def snapshot(names): + return {"libraries": progress.summarize([ + {"name": "libScePad", "label": "Pad", "done": len(names), "todo": 0, + "done_names": names, "todo_names": [], "inert_names": []}]), + "shaders": {"done": 0, "total": 0, "percent": 0, "groups": [], "extra": []}} + with tempfile.TemporaryDirectory() as directory: + base = Path(directory) / "base.json" + head = Path(directory) / "head.json" + base.write_text(json.dumps(snapshot([])), encoding="utf-8") + head.write_text(json.dumps(snapshot(["Ready"])), encoding="utf-8") + stdout = io.StringIO() + with patch("sys.stdout", stdout): + code = progress.main(["--compare", str(base), str(head)]) + self.assertEqual(code, 0) + self.assertIn("implemented", stdout.getvalue()) + stderr = io.StringIO() + with patch("sys.stderr", stderr), self.assertRaises(SystemExit) as raised: + progress.main(["--compare", str(base), str(head), "--json"]) + self.assertEqual(raised.exception.code, 2) + self.assertIn("--compare does not combine with --module or --json", stderr.getvalue()) + + + def test_compare_names_a_snapshot_it_cannot_read(self): + with tempfile.TemporaryDirectory() as directory: + good = Path(directory) / "good.json" + good.write_text("{}", encoding="utf-8") + broken = Path(directory) / "broken snapshot.json" + broken.write_text("not json", encoding="utf-8") + for name, path in (("invalid", broken), ("missing", Path(directory) / "missing snapshot.json")): + with self.subTest(name): + stderr = io.StringIO() + with patch("sys.stderr", stderr), self.assertRaises(SystemExit) as raised: + progress.main(["--compare", str(good), str(path)]) + self.assertEqual(raised.exception.code, 2) + self.assertIn(str(path), stderr.getvalue()) + + def test_root_must_be_a_source_tree(self): + with tempfile.TemporaryDirectory() as directory: + empty = Path(directory) / "empty tree" + empty.mkdir() + half = Path(directory) / "half tree" + (half / "core" / "libs" / "prx").mkdir(parents=True) + for name, path in (("missing", Path(directory) / "missing tree"), ("empty", empty), ("no opcodes", half)): + with self.subTest(name): + stderr = io.StringIO() + with patch("sys.stderr", stderr), self.assertRaises(SystemExit) as raised: + progress.main(["--json", "--root", str(path)]) + self.assertEqual(raised.exception.code, 2) + self.assertIn("is not a source tree", stderr.getvalue()) + self.assertIn(str(path), stderr.getvalue()) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_progress_encoding.py b/tools/tests/test_progress_encoding.py index 289389fc50..1b6628f285 100644 --- a/tools/tests/test_progress_encoding.py +++ b/tools/tests/test_progress_encoding.py @@ -29,7 +29,7 @@ class ProgressEncodingTests(unittest.TestCase): path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") env = {**os.environ, "PYTHONIOENCODING": encoding, "PYTHONUTF8": "0"} result = subprocess.run([sys.executable, "-X", "utf8=0", str(PROGRESS), "--compare", *map(str, paths)], - env=env, capture_output=True, timeout=10) + env=env, capture_output=True, timeout=60) self.assertEqual(result.returncode, 0, result.stderr.decode("utf-8", errors="replace")) return result.stdout.decode("utf-8").replace("\r\n", "\n") @@ -57,6 +57,31 @@ class ProgressEncodingTests(unittest.TestCase): base, head = snapshot(False), snapshot(True) self.assertEqual(self.compare(base, head, "utf-8"), self.compare(base, head)) + def test_site_generation_preserves_unicode_with_cp1252_stdout(self): + library = "libSce\u6e2c\u8a66\U0001f3ae" + with tempfile.TemporaryDirectory(prefix="progress site encoding ") as directory: + root = Path(directory) / "source" + module = root / "core" / "libs" / "prx" / library + module.mkdir(parents=True) + (module / "Export.cpp").write_text("int APS5_VABI Ready() { return 0; }\n", encoding="utf-8") + opcodes = root / "core" / "shader" / "recompiler" / "RdnaDecoder" / "include" / "RdnaDecoder" / "RdnaOpcode.hpp" + opcodes.parent.mkdir(parents=True) + opcodes.write_text("enum class RdnaOpcode {\n SEndpgm,\n};\n", encoding="utf-8") + output = Path(directory) / "site" + env = {**os.environ, "PYTHONIOENCODING": "cp1252", "PYTHONUTF8": "0"} + result = subprocess.run( + [sys.executable, "-X", "utf8=0", str(PROGRESS), str(output), "--root", str(root), + "--module", library, "--json"], env=env, capture_output=True, timeout=30) + self.assertEqual(result.returncode, 0, result.stderr.decode("utf-8", errors="replace")) + generated = {name: (output / name).read_text(encoding="utf-8") for name in ( + "progress.json", "badge-libraries.svg", "badge-shaders.svg", "progress.svg", "progress.html")} + payload = json.loads(generated["progress.json"]) + self.assertEqual(payload, json.loads(result.stdout.decode("utf-8"))) + self.assertEqual((payload["libraries"]["done"], payload["libraries"]["total"]), (1, 1)) + self.assertEqual(payload["libraries"]["groups"][0]["name"], library) + for name in ("progress.svg", "progress.html"): + self.assertIn(library.removeprefix("libSce"), generated[name]) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_progress_history.py b/tools/tests/test_progress_history.py index c8da6a22c0..38b9f5de86 100644 --- a/tools/tests/test_progress_history.py +++ b/tools/tests/test_progress_history.py @@ -1,3 +1,4 @@ +import json import os import subprocess import sys @@ -14,19 +15,74 @@ import progress_history OPCODES = Path("core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp") -def commit(root, day, message, body): +def commit(root, day, message, body, time="12:00:00Z"): library = root / "core/libs/prx/libSceExample" library.mkdir(parents=True, exist_ok=True) - (library / "Export.cpp").write_text(body) + (library / "Export.cpp").write_text(body, encoding="utf-8") (root / OPCODES).parent.mkdir(parents=True, exist_ok=True) - (root / OPCODES).write_text("enum class RdnaOpcode : int {\n SEndpgm,\n};\n") - env = {**os.environ, "GIT_AUTHOR_DATE": f"{day}T12:00:00Z", "GIT_COMMITTER_DATE": f"{day}T12:00:00Z"} + (root / OPCODES).write_text("enum class RdnaOpcode : int {\n SEndpgm,\n};\n", encoding="utf-8") + env = {**os.environ, "GIT_AUTHOR_DATE": f"{day}T{time}", "GIT_COMMITTER_DATE": f"{day}T{time}"} subprocess.run(["git", "-C", str(root), "add", "-A"], check=True) subprocess.run(["git", "-C", str(root), "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "-m", message], check=True, env=env) class ProgressHistoryTests(unittest.TestCase): + def test_cache_reuses_immutable_commits_and_invalidates_changed_rules(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + subprocess.run(["git", "-C", directory, "init", "-q"], check=True) + commit(root, "2026-01-01", "first", "int APS5_VABI A() { return 0; }\n") + cache = root / "cache" / "history.json" + isa = root / "isa.txt" + isa.write_bytes(progress.ISA.read_bytes()) + with patch.multiple(progress, ROOT=root, PRX=root / "core/libs/prx", OPCODES=root / OPCODES, ISA=isa): + with patch.object(progress_history, "measure", wraps=progress_history.measure) as measure: + first = progress_history.collect("HEAD", cache) + self.assertEqual(measure.call_count, 1) + before = cache.read_bytes() + measure.reset_mock() + self.assertEqual(progress_history.collect("HEAD", cache), first) + measure.assert_not_called() + self.assertEqual(cache.read_bytes(), before) + commit(root, "2026-01-02", "second", "int APS5_VABI A() { return 0; }\nint APS5_VABI B() { return 0; }\n") + measure.reset_mock() + second = progress_history.collect("HEAD", cache) + self.assertEqual(measure.call_count, 1) + self.assertEqual(second[0], first[0]) + isa.write_bytes(isa.read_bytes() + b"\n# changed rules input\n") + measure.reset_mock() + self.assertEqual(progress_history.collect("HEAD", cache), second) + self.assertEqual(measure.call_count, 2) + uncached = progress_history.collect("HEAD") + self.assertEqual(uncached, second) + self.assertEqual(progress_history.page(uncached), progress_history.page(second)) + self.assertFalse(any(path.name.startswith(".history-") for path in cache.parent.iterdir())) + + def test_invalid_cache_is_not_overwritten(self): + import json + with tempfile.TemporaryDirectory() as directory: + cache = Path(directory) / "history.json" + for contents in (b"{", b"[]", json.dumps({"schema_version": 1, "rules": progress_history.cache_signature(), + "commits": {"a" * 40: {"libraries": {"done": True, "total": 1, "percent": 100}, + "shaders": {"done": 0, "total": 0, "percent": 0}}}}).encode()): + with self.subTest(contents=contents): + cache.write_bytes(contents) + with patch.object(progress_history, "measure", side_effect=AssertionError("measurement started")), \ + self.assertRaises(ValueError): + progress_history.collect("HEAD", cache) + self.assertEqual(cache.read_bytes(), contents) + + def test_failed_cache_publication_preserves_the_old_file(self): + with tempfile.TemporaryDirectory() as directory: + cache = Path(directory) / "history.json" + cache.write_bytes(b"previous cache") + with patch.object(progress_history.os, "replace", side_effect=OSError("cannot publish")), \ + self.assertRaisesRegex(OSError, "cannot publish"): + progress_history.save_cache(cache, "rules", {}) + self.assertEqual(cache.read_bytes(), b"previous cache") + self.assertEqual(list(cache.parent.iterdir()), [cache]) + def test_one_point_per_day_at_the_last_commit(self): stub = "static void Stub() { NotImplemented_nid_no_patch(__func__); }\n" with tempfile.TemporaryDirectory() as directory: @@ -43,6 +99,17 @@ class ProgressHistoryTests(unittest.TestCase): self.assertIn("{label}<", svg) + + def test_main_writes_utf8_output_files(self): + point = {"date": "2026-01-01", "commit": "abc1234", + "libraries": {"done": 1, "total": 2, "percent": 50.0}, + "shaders": {"done": 3, "total": 4, "percent": 75.0}} + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "site" + with patch.object(progress_history, "collect", return_value=[point]): + progress_history.main([str(output)]) + svg = (output / "history.svg").read_text(encoding="utf-8") + self.assertIn("\u25cf", svg) + html = (output / "history.html").read_text(encoding="utf-8") + self.assertIn("", html) + data = json.loads((output / "history.json").read_text(encoding="utf-8")) + self.assertEqual(data, [point]) + if __name__ == "__main__": diff --git a/tools/tests/test_progress_read_encoding.py b/tools/tests/test_progress_read_encoding.py index 1c01153b58..40f3d7e02a 100644 --- a/tools/tests/test_progress_read_encoding.py +++ b/tools/tests/test_progress_read_encoding.py @@ -36,6 +36,59 @@ class ProgressReadEncodingTests(unittest.TestCase): self.assertEqual(result.returncode, 0, stderr) self.assertEqual(result.stdout.decode("ascii").strip(), "libSceOther") + def test_utf8_cpp_source_with_ascii_default_codec(self): + source_code = """ +import sys +from pathlib import Path +sys.path.insert(0, sys.argv[1]) +import progress +group = progress.scan_library(Path(sys.argv[2]) / "libSceExample") +print(group.get("done_names")) +""" + with tempfile.TemporaryDirectory(prefix="progress read cpp ") as directory: + root = Path(directory) + library = root / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_bytes( + ("// \u65e5\u672c\u8a9e \U0001f3ae\n" + "int APS5_VABI sceExampleReady() { return 0; }\n").encode("utf-8") + ) + env = {key: value for key, value in os.environ.items() if key != "PYTHONIOENCODING"} + env.update({"PYTHONUTF8": "0", "LC_ALL": "C", "LANG": "C"}) + result = subprocess.run([sys.executable, "-I", "-X", "utf8=0", "-c", source_code, str(TOOLS), str(root)], + env=env, capture_output=True, timeout=20) + stderr = result.stderr.decode("utf-8", errors="replace") + self.assertNotIn("UnicodeDecodeError", stderr) + self.assertEqual(result.returncode, 0, stderr) + self.assertEqual(result.stdout.decode("ascii").strip(), "['sceExampleReady']") + + def test_utf8_shared_cpp_with_ascii_default_codec(self): + source_code = """ +import sys +from pathlib import Path +sys.path.insert(0, sys.argv[1]) +import progress +group = progress.scan_library(Path(sys.argv[2]) / "libSceExample") +print(group.get("shared_sources")) +""" + with tempfile.TemporaryDirectory(prefix="progress shared cpp ") as directory: + root = Path(directory) + (root / "libSceOther").mkdir() + library = root / "libSceExample" + library.mkdir() + (library / "Forward.cpp").write_bytes( + ("// \u2010 \u2192 \u65e5\u672c\u8a9e\n" + '#include "prx/libSceOther/Shared.cpp"\n').encode("utf-8") + ) + env = {key: value for key, value in os.environ.items() if key != "PYTHONIOENCODING"} + env.update({"PYTHONUTF8": "0", "LC_ALL": "C", "LANG": "C"}) + result = subprocess.run([sys.executable, "-I", "-X", "utf8=0", "-c", source_code, str(TOOLS), str(root)], + env=env, capture_output=True, timeout=20) + stderr = result.stderr.decode("utf-8", errors="replace") + self.assertNotIn("UnicodeDecodeError", stderr) + self.assertEqual(result.returncode, 0, stderr) + self.assertEqual(result.stdout.decode("ascii").strip(), "libSceOther") + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_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_progress_wrappers.py b/tools/tests/test_progress_wrappers.py index 356e46aafb..3897bd125c 100644 --- a/tools/tests/test_progress_wrappers.py +++ b/tools/tests/test_progress_wrappers.py @@ -40,6 +40,37 @@ class WrapperTests(unittest.TestCase): def test_static_helper_still_works(self): self.assertTodo("static int Helper() " + STUB_BODY + CALLER) + def test_nested_helpers_are_pending_in_either_source_order(self): + for helpers in ( + "int Last() { NotImplemented_nid_no_patch(__func__); return 0; }\n" + "int Middle() { return Last(); }\nint Helper() { return Middle(); }\n", + "int Helper() { return Middle(); }\nint Middle() { return Last(); }\n" + "int Last() { NotImplemented_nid_no_patch(__func__); return 0; }\n", + ): + with self.subTest(helpers=helpers): + self.assertTodo(helpers + CALLER) + + def test_recursive_helper_chain_reaching_stub_is_pending(self): + self.assertTodo("int Helper() { return Middle(); }\n" + "int Middle() { if (false) return Helper(); " + "NotImplemented_nid_no_patch(__func__); return 0; }\n" + CALLER) + + def test_recursive_helpers_without_stub_stay_done(self): + group = scan("int Helper() { return Middle(); }\n" + "int Middle() { return Helper(); }\n" + CALLER) + self.assertEqual(group["done_names"], ["sceExampleCall"]) + self.assertEqual(group["todo_names"], []) + + def test_helper_chain_uses_whole_call_identifiers(self): + group = scan("int Last() { NotImplemented_nid_no_patch(__func__); return 0; }\n" + "int Helper() { return NotLast(); }\n" + CALLER) + self.assertEqual(group["done_names"], ["sceExampleCall"]) + self.assertEqual(group["todo_names"], []) + + def test_overloaded_helper_keeps_each_stub_path(self): + self.assertTodo("int Helper() { NotImplemented_nid_no_patch(__func__); return 0; }\n" + "int Helper(int value) { return value; }\n" + CALLER) + def test_unrelated_helper_stays_done(self): group = scan("namespace {\nint Other() { return 1; }\nint Helper() " + STUB_BODY + "}\n" "int APS5_VABI sceExampleCall() { return Other(); }\n") diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py new file mode 100644 index 0000000000..7dbeb88bee --- /dev/null +++ b/tools/tests/test_select_tests.py @@ -0,0 +1,309 @@ +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_changed_list_with_a_byte_order_mark_and_crlf_keeps_its_first_path(self): + listed = self.build / "changed bom.txt" + listed.write_bytes(b"\xef\xbb\xbfCMakeLists.txt\r\nalpha/alpha.h\r\n") + output = self.tool("select", self.build, self.graph, listed) + self.assertIn("CMakeLists.txt", output) + self.assertFalse((self.build / "select.tests").exists()) + + def test_missing_graph_runs_everything(self): + self.assertIsNone(self.select("alpha/alpha.h", graph=self.build / "missing.json")[1]) + + 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"}) + + def test_explain_reports_input_and_fixture_reasons_without_a_build_cache(self): + changed = self.build / "explain-changed.txt" + cache = self.build / "CMakeCache.txt" + saved = self.build / "CMakeCache.saved" + cache.rename(saved) + try: + for path in ("alpha/alpha.h", "beta/main.c", "CMakeLists.txt"): + with self.subTest(path=path): + changed.write_text(path + "\n", encoding="utf-8") + report = json.loads(self.tool("explain", self.graph, changed)) + picked, full = select_tests.select(json.loads(self.graph.read_text(encoding="utf-8")), [path]) + self.assertEqual(report["tests"], sorted(picked) if picked is not None else None) + self.assertEqual(report["full_run_paths"], full) + if path == "alpha/alpha.h": + self.assertIn("input: alpha/alpha.h", report["reasons"]["alpha"]) + self.assertIn("no build executable", report["reasons"]["script"]) + elif path == "beta/main.c": + self.assertIn("fixture setup for: beta", report["reasons"]["beta_setup"]) + finally: + saved.rename(cache) + + + +@unittest.skipUnless(shutil.which("ninja") and (shutil.which("gcc") or shutil.which("cc")), "needs ninja and a C compiler") +class FixtureBuildTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.directory = tempfile.TemporaryDirectory() + root = Path(cls.directory.name) + cls.source, cls.build, cls.graph = root / "source", root / "build", root / "graph.json" + project = dict(PROJECT) + project["CMakeLists.txt"] += "add_subdirectory(cleanup)\n" + project["cleanup/CMakeLists.txt"] = ( + "add_executable(cleanup_tests main.c)\nadd_test(NAME beta_cleanup COMMAND cleanup_tests)\n" + "set_tests_properties(beta_cleanup PROPERTIES FIXTURES_CLEANUP beta_fixture)\n") + project["cleanup/main.c"] = ('#include \nint main(void) { FILE *f = fopen("cleanup.ran", "w"); ' + 'if (!f) return 1; return fclose(f) != 0; }\n') + for name, content in project.items(): + (cls.source / name).parent.mkdir(parents=True, exist_ok=True) + (cls.source / name).write_text(content, encoding="utf-8") + compiler = shutil.which("gcc") or shutil.which("cc") + subprocess.run(["cmake", "-S", str(cls.source), "-B", str(cls.build), "-G", "Ninja", f"-DCMAKE_C_COMPILER={compiler}"], + check=True, capture_output=True) + subprocess.run(["cmake", "--build", str(cls.build)], check=True, capture_output=True) + SelectTests.tool("dump", cls.build, cls.graph) + + @classmethod + def tearDownClass(cls): + cls.directory.cleanup() + + def run_selected(self, changed, graph): + subprocess.run(["cmake", "--build", str(self.build)], check=True, capture_output=True) + marker = self.build / "cleanup/cleanup.ran" + marker.unlink(missing_ok=True) + listed = self.build / "changed.txt" + listed.write_text(changed + "\n", encoding="utf-8") + SelectTests.tool("select", self.build, graph, listed) + subprocess.run(["ninja", "-C", str(self.build), "-t", "clean"], check=True, capture_output=True) + subprocess.run(["ninja", "-C", str(self.build), "-f", "select.ninja", "selected"], check=True, capture_output=True) + result = subprocess.run([sys.executable, str(TOOL), "ctest", str(self.build), "--output-on-failure"], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertTrue(marker.is_file(), "CTest did not execute the fixture cleanup") + return result.stdout + + def test_cleanup_executable_is_built_when_ctest_adds_it(self): + output = self.run_selected("beta/main.c", self.graph) + self.assertIn("beta_cleanup", output) + self.assertNotIn("alpha", output) + self.assertFalse(any(path.is_file() for path in (self.build / "alpha").glob("alpha_tests*"))) + + def test_new_test_gets_its_current_setup_and_cleanup(self): + graph = json.loads(self.graph.read_text(encoding="utf-8")) + del graph["tests"]["beta"] + older = self.build / "older.json" + older.write_text(json.dumps(graph), encoding="utf-8") + output = self.run_selected("alpha/main.c", older) + for name in ("alpha", "alpha_data", "beta", "beta_setup", "beta_cleanup"): + self.assertIn(name, output) + + +class BuildErrorTests(unittest.TestCase): + def run_tool(self, *args): + return subprocess.run([sys.executable, "-I", str(TOOL), *map(str, args)], capture_output=True, text=True, timeout=60) + + def test_missing_build_directory_is_reported_without_a_traceback(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for command in (("dump", root / "no build", root / "graph.json"), + ("select", root / "no build", root / "graph.json", root / "changed.txt")): + with self.subTest(command[0]): + result = self.run_tool(*command) + self.assertEqual(result.returncode, 2) + self.assertIn("not a configured CMake build directory", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_cache_without_the_build_locations_is_reported_without_a_traceback(self): + with tempfile.TemporaryDirectory() as directory: + (Path(directory) / "CMakeCache.txt").write_text("# empty\n", encoding="utf-8") + result = self.run_tool("dump", directory, Path(directory) / "graph.json") + self.assertEqual(result.returncode, 2) + self.assertIn("CMAKE_CACHEFILE_DIR", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_cache_with_utf8_bom_is_read(self): + with tempfile.TemporaryDirectory() as directory: + cache = Path(directory) / "CMakeCache.txt" + cache.write_text("\ufeffCMAKE_CACHEFILE_DIR:INTERNAL=C:/work/build\nCMAKE_HOME_DIRECTORY:INTERNAL=C:/work/source\n", encoding="utf-8") + with mock.patch.object(select_tests.Build, "run", return_value="Build file encoding: UTF-8"): + build = select_tests.Build(directory) + self.assertEqual(build.path, os.path.normpath("C:/work/build")) + self.assertEqual(build.source, os.path.normpath("C:/work/source")) + + def test_explain_accepts_utf8_bom_in_graph_and_changed(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + graph_path = root / "graph.json" + graph = { + "tests": { + "unit": { + "target": "tests/unit", + "arguments": [], + "registered": [], + "sets": [], + "requires": [], + "depends": [], + } + }, + "closures": {"tests/unit": ["src/main.c"]}, + } + graph_path.write_text("\ufeff" + json.dumps(graph), encoding="utf-8") + changed_path = root / "changed.txt" + changed_path.write_text("\ufeffsrc/main.c\n", encoding="utf-8") + result = self.run_tool("explain", graph_path, changed_path) + self.assertEqual(result.returncode, 0, result.stderr) + data = json.loads(result.stdout) + self.assertEqual(data["tests"], ["unit"]) + + +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"))) + + +class PlanTests(unittest.TestCase): + def test_plan_empty_run_avoids_invoking_ctest(self): + build = mock.Mock() + build.tests.return_value = ({}, [{"name": "alpha", "command": ["/build/alpha_tests"]}]) + build.leaves.return_value = ["tests/alpha_tests"] + build.target.side_effect = lambda t: "tests/alpha_tests" + graph = {"tests": {"alpha": {}}} + run, leaves, count = select_tests.plan(build, graph, set()) + self.assertEqual(run, []) + self.assertEqual(leaves, []) + self.assertEqual(count, 1) + build.tests.assert_called_once_with() + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_shader_cache_runtime_sources.py b/tools/tests/test_shader_cache_runtime_sources.py new file mode 100644 index 0000000000..d4d43946e7 --- /dev/null +++ b/tools/tests/test_shader_cache_runtime_sources.py @@ -0,0 +1,223 @@ +from pathlib import Path +import os +import re +import shutil +import subprocess +import sys +import tempfile +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import select_tests + + +EXPLICIT_ARGS = len(sys.argv) >= 3 and Path(sys.argv[1]).is_dir() +if EXPLICIT_ARGS: + SOURCE = Path(sys.argv.pop(1)) + CMAKE = sys.argv.pop(1) +else: + SOURCE = Path(__file__).resolve().parents[2] / 'core/shader/recompiler' + CMAKE = shutil.which('cmake') or 'cmake' + + +class Fixture: + def __init__(self, root, spirv_tools=False): + self.root = root + self.recompiler = root / 'core/shader/recompiler' + self.build = root / 'build' + self.calls = 0 + for directory, directories, files in os.walk(SOURCE, followlinks=False): + for path in (Path(directory), *(Path(directory) / name for name in directories + files)): + if path.is_symlink() or getattr(path.lstat(), 'st_file_attributes', 0) & 0x400: + raise AssertionError('The source fixture contains a reparse point') + shutil.copytree(SOURCE, self.recompiler) + header = root / 'core/libs/prx/libc/include/HostThreadLocal.hpp' + header.parent.mkdir(parents=True) + header.write_text('struct FixtureThreadLocal {};\n', encoding='utf-8') + self.guard = root / 'driver.cpp' + self.guard.write_text('int fixture_driver;\n', encoding='utf-8') + compiler = root / ('compiler.cmd' if os.name == 'nt' else 'compiler') + self.compiler = compiler + if os.name == 'nt': + compiler.write_text('@echo off\nif "%*"=="-print-sysroot" exit /b 0\nif "%*"=="--version" exit /b 0\nif "%*"=="-v --help" exit /b 0\nif "%*"=="-Wl,--help" exit /b 0\nif "%*"=="-Wl,--version" exit /b 0\n>>"%~dp0compiler-invoked" echo %*\nexit /b 99\n', encoding='utf-8') + else: + compiler.write_text('#!/bin/sh\nif [ "$#" -eq 1 ]; then case "$1" in -print-sysroot|--version|-Wl,--help|-Wl,--version) exit 0;; esac; fi\nif [ "$#" -eq 2 ] && [ "$1" = "-v" ] && [ "$2" = "--help" ]; then exit 0; fi\nprintf "%s\\n" "$*" >> "$(dirname "$0")/compiler-invoked"\nexit 99\n', encoding='utf-8') + compiler.chmod(0o755) + mode = 'ON' if spirv_tools else 'OFF' + (root / 'CMakeLists.txt').write_text(f'''cmake_minimum_required(VERSION 3.20) +set(CMAKE_CXX_COMPILER "{compiler.as_posix()}") +set(CMAKE_CXX_COMPILER_ID GNU) +set(CMAKE_CXX_COMPILER_VERSION 15.2.0) +set(CMAKE_CXX_COMPILER_ID_RUN TRUE) +set(CMAKE_CXX_COMPILER_FORCED TRUE) +set(CMAKE_CXX_COMPILER_WORKS TRUE) +set(CMAKE_CXX_ABI_COMPILED TRUE) +set(CMAKE_CXX_STANDARD_COMPUTED_DEFAULT 17) +set(CMAKE_CXX_EXTENSIONS_COMPUTED_DEFAULT ON) +set(CMAKE_CXX_COMPILE_FEATURES cxx_std_20) +project(ShaderCacheSourceFixture LANGUAGES CXX) +set(CMAKE_BUILD_TYPE Release) +set(ANYPS5_ENABLE_SPIRV_TOOLS {mode}) +set(ANYPS5_USE_SYSTEM_DEPS ON) +set(ANYPS5_SYSTEM_DEPS_STAMP fixture) +add_library(fixture_runtime INTERFACE) +add_library(glslang INTERFACE) +add_library(SPIRV INTERFACE) +add_library(SPIRV-Tools-opt INTERFACE) +add_library(SPIRV-Tools-static INTERFACE) +include("${{CMAKE_CURRENT_SOURCE_DIR}}/core/shader/recompiler/CMakeLists.txt") +add_shader_recompiler(shader_recompiler_probe fixture_runtime GUARD_SOURCES "${{CMAKE_CURRENT_SOURCE_DIR}}/driver.cpp") +get_target_property(runtimeSources shader_recompiler_probe SOURCES) +list(JOIN runtimeSources "\\n" runtimeSources) +file(WRITE "${{CMAKE_CURRENT_BINARY_DIR}}/runtime-sources.txt" "${{runtimeSources}}\\n") +''', encoding='utf-8') + self.configure() + + def configure(self, flags=''): + result = subprocess.run([CMAKE, '-S', str(self.root), '-B', str(self.build), '-G', 'Ninja', f'-DCMAKE_CXX_FLAGS={flags}'], + capture_output=True, encoding='utf-8', errors='replace', timeout=15) + if result.returncode: + raise AssertionError(result.stdout + result.stderr) + if (self.root / 'compiler-invoked').exists(): + raise AssertionError('The generator fixture invoked a compiler: ' + (self.root / 'compiler-invoked').read_text(encoding='utf-8')) + self.generated = self.build / 'shader_recompiler_probe_generated' + self.listing = self.generated / 'sources.txt' + ninja = (self.build / 'build.ninja').read_text(encoding=select_tests.Build(self.build).encoding) + command = next(line for line in ninja.splitlines() if line.startswith(' COMMAND = ') and 'ShaderCacheVersion.cmake' in line) + self.extra = re.search(r'"-DEXTRA=([^"]*)"', command).group(1) + self.compiled = [] + for line in ninja.splitlines(): + match = re.match(r'build .*: CXX_COMPILER__shader_recompiler_probe_[^ ]+ (.*?) \|\|', line) + if match: + self.compiled.append(Path(match.group(1).replace('$:', ':').replace('$ ', ' '))) + + def version(self, success=True): + self.calls += 1 + output = self.build / f'probe-version-{self.calls}.cpp' + result = subprocess.run([CMAKE, f'-DROOT={self.root.as_posix()}', + f'-DRECOMPILER_DIR={self.recompiler.as_posix()}', + f'-DLIST_FILE={self.listing.as_posix()}', f'-DOUTPUT={output.as_posix()}', + f'-DEXTRA={self.extra}', '-P', str(self.recompiler / 'ShaderCacheVersion.cmake')], + capture_output=True, encoding='utf-8', errors='replace', timeout=10) + if not success: + if result.returncode == 0 or output.exists(): + raise AssertionError('The generator accepted an unkeyed dependency') + return result.stdout + result.stderr + if result.returncode: + raise AssertionError(result.stdout + result.stderr) + return re.search(r'SourceVersion = 0x([0-9a-f]+)ull', output.read_text(encoding='utf-8')).group(1) + + def sources(self): + return {Path(line.split(' ', 1)[1]) for line in self.listing.read_text(encoding='utf-8').splitlines() if line.startswith('sources ')} + + +@unittest.skipUnless(EXPLICIT_ARGS or (shutil.which('ninja') and shutil.which(CMAKE) and SOURCE.is_dir()), 'ninja, cmake, and shader recompiler source required') +class ShaderCacheRuntimeSourceTests(unittest.TestCase): + def fixture(self, spirv_tools=False): + temporary = tempfile.TemporaryDirectory(prefix='shader_cache_sources_') + self.addCleanup(temporary.cleanup) + return Fixture(Path(temporary.name), spirv_tools) + + def test_compiler_metadata_queries_and_compilation_guard(self): + fixture = self.fixture() + allowed = [('-print-sysroot',), ('--version',), ('-v', '--help'), ('-Wl,--help',), ('-Wl,--version',)] + rejected = [('-v', '--help', 'fixture.cpp'), ('-v', '--help', '-c', 'fixture.cpp'), + ('--help', '-v'), ('-v',), ('--help',), ('-c', 'fixture.cpp'), + ('fixture.cpp', '-o', 'fixture.exe'), ('--version', 'fixture.cpp'), + ('-print-sysroot', 'fixture.cpp'), ('-Wl,--help', 'fixture.cpp'), ('-Wl,--version', '-c', 'fixture.cpp')] + script = fixture.root / 'metadata.cmake' + lines = ['cmake_minimum_required(VERSION 3.20)', + f'set(CMAKE_CXX_COMPILER "{fixture.compiler.as_posix()}")', + 'include(Platform/Apple-GNU)', + 'cmake_gnu_set_sysroot_flag(CXX)', + 'cmake_gnu_set_osx_deployment_target_flag(CXX)'] + for args in allowed: + arguments = ' '.join(f'"{arg}"' for arg in args) + lines += [f'execute_process(COMMAND "${{CMAKE_CXX_COMPILER}}" {arguments} RESULT_VARIABLE result)', + 'if(NOT result EQUAL 0)', 'message(FATAL_ERROR "Metadata query was rejected")', 'endif()'] + marker = fixture.root / 'compiler-invoked' + lines += [f'if(EXISTS "{marker.as_posix()}")', + 'message(FATAL_ERROR "Compiler metadata reached the compilation guard")', 'endif()'] + for args in rejected: + arguments = ' '.join(f'"{arg}"' for arg in args) + lines += [f'execute_process(COMMAND "${{CMAKE_CXX_COMPILER}}" {arguments} RESULT_VARIABLE result)', + 'if(NOT result EQUAL 99)', 'message(FATAL_ERROR "Unexpected compiler arguments were accepted")', 'endif()'] + script.write_text('\n'.join(lines) + '\n', encoding='utf-8') + result = subprocess.run([CMAKE, '-P', str(script)], capture_output=True, + encoding='utf-8', errors='replace', timeout=10) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(len(marker.read_text(encoding='utf-8').splitlines()), len(rejected)) + + def test_runtime_source_graph_off_and_on(self): + for enabled in (False, True): + with self.subTest(spirv_tools=enabled): + fixture = self.fixture(enabled) + generated = fixture.generated / 'ShaderCacheVersion.cpp' + selected = {Path(line) for line in (fixture.build / 'runtime-sources.txt').read_text(encoding='utf-8').splitlines()} + self.assertEqual(set(fixture.compiled), selected) + self.assertNotIn(generated, fixture.sources()) + self.assertEqual({path for path in fixture.sources() if path.suffix in ('.cpp', '.cc', '.cxx')}, selected - {generated}) + self.assertIn(fixture.recompiler / 'tests/DummyShaders.cpp', fixture.sources()) + self.assertEqual(fixture.recompiler / 'SpirvBackend/src/SpirvOptimizer.cpp' in fixture.sources(), enabled) + fixture.version() + + def test_uncompiled_test_sources_do_not_change_version(self): + fixture = self.fixture() + previous = fixture.version() + for name in ('CacheFileReadTests.cpp', 'RequestSerializerTests.cpp'): + path = fixture.recompiler / 'tests' / name + self.assertNotIn(path, fixture.sources()) + path.write_bytes(path.read_bytes() + b'\n') + self.assertEqual(fixture.version(), previous, name) + (fixture.recompiler / 'tests/NewFixture.cpp').write_text('int new_fixture;\n', encoding='utf-8') + fixture.configure() + self.assertEqual(fixture.version(), previous) + + def test_runtime_sources_headers_and_cmake_change_version(self): + fixture = self.fixture() + previous = fixture.version() + for name in ('Recompiler.cpp', 'tests/DummyShaders.cpp', 'tests/DummyShaders.hpp', 'BdaAbi.hpp', 'CMakeLists.txt', 'ShaderCacheVersion.cmake'): + with self.subTest(source=name): + path = fixture.recompiler / name + before = path.read_bytes() + path.write_bytes(before + b'\n') + self.assertNotEqual(fixture.version(), previous) + path.write_bytes(before) + self.assertEqual(fixture.version(), previous) + + def test_flags_change_version(self): + fixture = self.fixture() + previous = fixture.version() + fixture.configure('-DSHADER_CACHE_FIXTURE=1') + self.assertNotEqual(fixture.version(), previous) + + def test_optimizer_content_only_changes_enabled_version(self): + for enabled in (False, True): + with self.subTest(spirv_tools=enabled): + fixture = self.fixture(enabled) + previous = fixture.version() + optimizer = fixture.recompiler / 'SpirvBackend/src/SpirvOptimizer.cpp' + optimizer.write_bytes(optimizer.read_bytes() + b'\n') + self.assertEqual(fixture.version() == previous, not enabled) + + def test_uncompiled_internal_include_is_rejected(self): + fixture = self.fixture() + source = fixture.recompiler / 'Recompiler.cpp' + source.write_text('#include "tests/CacheFileReadTests.cpp"\n' + source.read_text(encoding='utf-8'), encoding='utf-8') + self.assertIn('does not hash', fixture.version(False)) + + def test_unhashed_external_include_is_rejected(self): + fixture = self.fixture() + (fixture.root / 'external.hpp').write_text('int external_fixture;\n', encoding='utf-8') + source = fixture.recompiler / 'Recompiler.cpp' + source.write_text('#include "../../../external.hpp"\n' + source.read_text(encoding='utf-8'), encoding='utf-8') + self.assertIn('does not hash', fixture.version(False)) + + def test_unreviewed_driver_call_is_rejected(self): + fixture = self.fixture() + fixture.guard.write_text('ShaderRecompiler::UnreviewedFixtureCall();\n', encoding='utf-8') + self.assertIn('has not been reviewed', fixture.version(False)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/tests/test_stubgen.py b/tools/tests/test_stubgen.py index 66ab9bd1b3..eedd1e3a09 100644 --- a/tools/tests/test_stubgen.py +++ b/tools/tests/test_stubgen.py @@ -1,10 +1,12 @@ import contextlib import io import os +import stat import tempfile from pathlib import Path import sys import unittest +from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import check_conventions @@ -29,8 +31,42 @@ class StubgenTests(unittest.TestCase): self.assertIn("APS5_VABI", code) self.assertIn("NotImplemented_nid_no_patch", code) + def test_prx_suffix_is_stripped_from_module_name(self): + nids = ["AAAAAAAAAAA", "BBBBBBBBBBB"] + expected = stubgen.generate("libSceAmpr", nids, {}) + self.assertIn("sceAmprUnknown00", expected[0]) + self.assertIn("core/libs/prx/libSceAmpr/", expected[1]) + for spelling in ("libSceAmpr.prx", "libSceAmpr.PRX"): + with self.subTest(spelling=spelling): + self.assertEqual(stubgen.generate(spelling, nids, {}), expected) + 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" @@ -105,6 +141,12 @@ class StubGeneratorTests(unittest.TestCase): path.write_text("# imports\nAAAAAAAAAAA#A#B\nAAAAAAAAAAA\nBBBBBBBBBBB\n", encoding="utf-8-sig") self.assertEqual(stubgen.parse_nids(path), ["AAAAAAAAAAA", "BBBBBBBBBBB"]) + def test_named_stub_reports_its_function_name(self): + code = stubgen.generate("libSceAmpr", ["tNn5WBkta60", "AAAAAAAAAAA"], {"tNn5WBkta60": "sceAmprCommandBufferNop"})[0] + named = code.split("sceAmprCommandBufferNop(void) {\n", 1)[1].split("\n", 1)[0] + self.assertEqual(named, " NotImplemented_nid_no_patch(__func__);") + self.assertIn('NotImplemented_nid_no_patch("AAAAAAAAAAA");', code) + def test_valid_generation_keeps_the_code_and_debt(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) @@ -121,5 +163,75 @@ class StubGeneratorTests(unittest.TestCase): self.assertEqual(code.stat().st_mode & 0o777, 0o666 & ~mask) +class ReadOnlyOutputTests(unittest.TestCase): + def test_later_staging_failure_preserves_original_error_and_cleans_readonly_temporary(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + nids, output = root / "nids.txt", root / "Export.cpp" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + original = "existing source\n" + output.write_text(original, encoding="utf-8") + output.chmod(0o444) + errors = io.StringIO() + arguments = ["--module", "libSceExample", "--nids", str(nids), "--out", str(output), + "--td-out", str(root / "absent" / "debt.txt")] + with contextlib.redirect_stderr(errors), self.assertRaises(SystemExit) as error: + stubgen.main(arguments) + self.assertEqual(error.exception.code, 2) + self.assertIn("absent", errors.getvalue()) + self.assertNotIn("Export.cpp.", errors.getvalue()) + self.assertEqual(output.read_text(encoding="utf-8"), original) + self.assertFalse(output.stat().st_mode & stat.S_IWRITE) + self.assertEqual(sorted(path.name for path in root.iterdir()), ["Export.cpp", "nids.txt"]) + + @unittest.skipUnless(os.name == "nt", "Windows denies replacing read-only output files") + def test_denied_replace_preserves_readonly_output_without_leaking_temporary(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + nids, output = root / "nids.txt", root / "Export.cpp" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + original = "existing source\n" + output.write_text(original, encoding="utf-8") + output.chmod(0o444) + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error: + stubgen.main(["--module", "libSceExample", "--nids", str(nids), "--out", str(output)]) + self.assertEqual(error.exception.code, 2) + self.assertEqual(output.read_text(encoding="utf-8"), original) + self.assertFalse(output.stat().st_mode & stat.S_IWRITE) + self.assertEqual(sorted(path.name for path in root.iterdir()), ["Export.cpp", "nids.txt"]) + + + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + self.newline = None + + def reconfigure(self, encoding=None, errors=None, newline=None): + self.encoding = encoding + self.errors = errors + self.newline = newline + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(stubgen.sys, "stdout", out), patch.object(stubgen.sys, "stderr", err): + try: + stubgen.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(out.newline, "\n") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + self.assertEqual(err.newline, "\n") + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_stubgen_stdout_newlines.py b/tools/tests/test_stubgen_stdout_newlines.py index 93b49efe1c..427d45a14c 100644 --- a/tools/tests/test_stubgen_stdout_newlines.py +++ b/tools/tests/test_stubgen_stdout_newlines.py @@ -11,7 +11,7 @@ class StubgenStdoutNewlineTests(unittest.TestCase): def run_stubgen(self, *extra): with tempfile.TemporaryDirectory() as d: nids = Path(d) / "nids.txt" - nids.write_text("AC1FtjqMCL0\n", encoding="utf-8", newline="\n") + nids.write_bytes(b"AC1FtjqMCL0\n") return subprocess.run( [sys.executable, str(tools / "stubgen.py"), "--module", "libkernel", "--nids", str(nids)] + list(extra), capture_output=True, @@ -28,6 +28,11 @@ class StubgenStdoutNewlineTests(unittest.TestCase): self.assertIn(b"\n", result.stdout) self.assertNotIn(b"\r\n", result.stdout) + def test_td_on_stderr_uses_lf(self): + result = self.run_stubgen() + self.assertIn(b"\n", result.stderr) + self.assertNotIn(b"\r\n", result.stderr) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index e9862c2bc3..de64bbec73 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -1,6 +1,9 @@ from pathlib import Path +import contextlib +import io import os import shutil +import subprocess import sys import tempfile import unittest @@ -12,12 +15,85 @@ import check_test_registration class RegistrationTests(unittest.TestCase): + def test_literal_prefixes_cannot_expose_commands_inside_arguments(self): + prefixes = (r"\#", r"\(", '"prefix" ', '"" ', '"line\nprefix" ', "[[prefix]] ", + "[=[prefix]=] ", "[===[line\nprefix]===] ") + for prefix in prefixes: + with self.subTest(prefix=prefix): + content = (f"set(example\n{prefix}add_test(ghost /build/tests/missing_tests)\n)\n" + 'add_test("real" "/build/tests/real_tests")\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_commands_inside_literals_do_not_register_tests(self): + for opening, closing in (('"', '"'), ("[[", "]]"), ("[=[", "]=]"), ("[===[", "]===]")): + with self.subTest(opening=opening): + content = (f"set(example {opening}\nadd_test(ghost /build/tests/missing_tests)\n{closing})\n" + 'add_test("real" "/build/tests/real_tests")\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_properties_inside_literals_do_not_disable_a_real_test(self): + for opening, closing in (('"', '"'), ("[[", "]]"), ("[=[", "]=]"), ("[===[", "]===]")): + with self.subTest(opening=opening): + content = ('add_test("real" "/build/tests/real_tests")\n' + f"set(example {opening}\nset_tests_properties(real PROPERTIES DISABLED TRUE)\n{closing})\n") + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_subdirectories_inside_literals_are_not_followed(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + ignored = build / "ignored" + ignored.mkdir() + (ignored / "CTestTestfile.cmake").write_text( + 'add_test("ghost" "/build/tests/missing_tests")\n', encoding="utf-8") + for opening, closing in (('"', '"'), ("[[", "]]"), ("[=[", "]=]"), ("[===[", "]===]")): + with self.subTest(opening=opening): + content = (f"set(example {opening}\nsubdirs(ignored)\n{closing})\n" + 'add_test("real" "/build/tests/real_tests")\n') + (build / "CTestTestfile.cmake").write_text(content, encoding="utf-8") + self.assertEqual(check_test_registration.registered(build), {"real_tests"}) + + def test_disabled_property_is_local_to_each_configuration(self): + for configurations in (("OFF", "TRUE"), ("TRUE", "OFF"), ("TRUE", "TRUE")): + with self.subTest(configurations=configurations): + 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) (build / "CTestTestfile.cmake").write_text(content, encoding="utf-8") return check_test_registration.registered(build) + def test_bracket_comments_do_not_register_tests(self): + for equals in ("", "=", "==="): + with self.subTest(equals=equals): + content = (f"#[{equals}[\nadd_test(ghost /build/tests/missing_tests)\n]{equals}]\n" + 'add_test("real" "/build/tests/real_tests")\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_bracket_comment_does_not_disable_a_registered_test(self): + content = ('add_test("real" "/build/tests/real_tests")\n' + '#[=[\nset_tests_properties("real" PROPERTIES DISABLED TRUE)\n]=]\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_bracket_comment_inside_arguments_does_not_end_the_command(self): + content = ('add_test("real" #[=[ )\nadd_test(ghost /build/tests/missing_tests)\n]=]\n' + '"/build/tests/real_tests")\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_comment_markers_inside_literals_remain_arguments(self): + content = ('add_test("real" "/build/tests/real_tests" "#[[" [=[# comment]=] "\\\\#")\n') + self.assertEqual(list(check_test_registration.commands(content)), + [("add_test", ["real", "/build/tests/real_tests", "#[[", "# comment", "\\#"])]) + def test_test_name_does_not_register_executable(self): names = self.registered('add_test("missing_tests" "/usr/bin/python3" "unrelated.py")\n') self.assertNotIn("missing_tests", names) @@ -41,7 +117,8 @@ class RegistrationTests(unittest.TestCase): with tempfile.TemporaryDirectory() as directory: build = Path(directory) (build / "CMakeCache.txt").write_text("CMAKE_MAKE_PROGRAM:FILEPATH=/configured/ninja\nCMAKE_GENERATOR:INTERNAL=Ninja\n", encoding="utf-8") - with patch.object(check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout=output)) as run: + with patch.object(check_test_registration.subprocess, "run", side_effect=lambda command, **kwargs: SimpleNamespace( + stdout="Build file encoding: UTF-8\n" if command[-1] == "wincodepage" else output)) as run: self.assertEqual(check_test_registration.built(build), {"buffer-bounds_tests"}) self.assertEqual(run.call_args[0][0][0], "/configured/ninja") @@ -50,7 +127,8 @@ class RegistrationTests(unittest.TestCase): with tempfile.TemporaryDirectory() as directory: build = Path(directory) (build / "CMakeCache.txt").write_text("CMAKE_MAKE_PROGRAM:FILEPATH=/configured/ninja\nCMAKE_GENERATOR:INTERNAL=Ninja Multi-Config\n", encoding="utf-8") - with patch.object(check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout=output)) as run: + with patch.object(check_test_registration.subprocess, "run", side_effect=lambda command, **kwargs: SimpleNamespace( + stdout="Build file encoding: UTF-8\n" if command[-1] == "wincodepage" else output)) as run: self.assertEqual(check_test_registration.built(build), {"buffer_bounds_tests"}) self.assertEqual(run.call_args[0][0][0], "/configured/ninja") @@ -66,6 +144,19 @@ class RegistrationTests(unittest.TestCase): names = self.registered('add_test("bounds"\n "/build/tests/buffer_bounds_tests"\n)\n') self.assertIn("buffer_bounds_tests", names) + def test_multi_config_branches_register_the_executable(self): + names = self.registered( + 'if(CTEST_CONFIGURATION_TYPE MATCHES "^([Dd][Ee][Bb][Uu][Gg])$")\n' + ' add_test([=[bounds]=] "/build/tests/Debug/buffer_bounds_tests")\n' + ' set_tests_properties([=[bounds]=] PROPERTIES TIMEOUT "20")\n' + 'elseif(CTEST_CONFIGURATION_TYPE MATCHES "^([Rr][Ee][Ll][Ee][Aa][Ss][Ee])$")\n' + ' add_test([=[bounds]=] "/build/tests/Release/buffer_bounds_tests")\n' + ' set_tests_properties([=[bounds]=] PROPERTIES TIMEOUT "20")\n' + 'else()\n' + ' add_test([=[bounds]=] NOT_AVAILABLE)\n' + 'endif()\n') + self.assertIn("buffer_bounds_tests", names) + def test_disabled_test_is_not_counted_as_run(self): names = self.registered('add_test("bounds" "/build/tests/buffer_bounds_tests")\nset_tests_properties("bounds" PROPERTIES DISABLED TRUE)\n') self.assertNotIn("buffer_bounds_tests", names) @@ -151,5 +242,127 @@ class RegistrationTests(unittest.TestCase): check_test_registration.built(build) run.assert_not_called() + @unittest.skipIf(shutil.which("ninja") is None, "requires a local Ninja to exercise its real output encoding") + def test_ninja_output_encoding_with_a_cp1252_default(self): + program = shutil.which("ninja") + actual_run = check_test_registration.subprocess.run + for encoding in (("utf-8", "mbcs") if sys.platform == "win32" else ("utf-8",)): + with self.subTest(encoding=encoding), tempfile.TemporaryDirectory() as directory: + build = Path(directory) + name = "caf\u00e9_tests".encode(encoding, errors="ignore").decode(encoding) + (build / "CMakeCache.txt").write_text( + f"CMAKE_MAKE_PROGRAM:FILEPATH={program}\nCMAKE_GENERATOR:INTERNAL=Ninja\n", encoding="utf-8") + (build / "build.ninja").write_text( + "rule CXX_EXECUTABLE_LINKER\n command = true\n" + f"build tests/{name}: CXX_EXECUTABLE_LINKER\n", encoding=encoding) + + def run(command, **kwargs): + if command[-1] == "wincodepage": + return SimpleNamespace(stdout="Build file encoding: " + ("ANSI" if encoding == "mbcs" else "UTF-8")) + kwargs.setdefault("encoding", "cp1252") + return actual_run(command, **kwargs) + + with patch.object(check_test_registration.subprocess, "run", side_effect=run): + self.assertEqual(check_test_registration.built(build), {name}) + + def test_unknown_windows_ninja_encoding_is_rejected(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + (build / "CMakeCache.txt").write_text( + "CMAKE_MAKE_PROGRAM:FILEPATH=/configured/ninja\nCMAKE_GENERATOR:INTERNAL=Ninja\n", encoding="utf-8") + with patch.object(check_test_registration.sys, "platform", "win32"), patch.object( + check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout="unknown")) as run: + with self.assertRaisesRegex(ValueError, "unsupported Ninja build-file encoding"): + check_test_registration.built(build) + run.assert_called_once() + + def run_tool(self, build): + return subprocess.run([sys.executable, "-I", str(Path(check_test_registration.__file__)), str(build)], + capture_output=True, text=True, timeout=30) + + def test_missing_build_directory_is_reported_without_a_traceback(self): + with tempfile.TemporaryDirectory() as directory: + result = self.run_tool(Path(directory) / "no build") + self.assertEqual(result.returncode, 2) + self.assertIn("error:", result.stderr) + self.assertIn("CMakeCache.txt", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_unsupported_generator_is_reported_without_a_traceback(self): + with tempfile.TemporaryDirectory() as directory: + (Path(directory) / "CMakeCache.txt").write_text("CMAKE_GENERATOR:INTERNAL=Unix Makefiles\n", encoding="utf-8") + result = self.run_tool(directory) + self.assertEqual(result.returncode, 2) + self.assertIn("requires a Ninja generator", result.stderr) + self.assertNotIn("Traceback", result.stderr) + +class ScriptRegistrationTests(unittest.TestCase): + def unrun(self, files): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + check_test_registration.subprocess.run(["git", "init", "-q", str(root)], check=True) + for name, content in files.items(): + (root / name).parent.mkdir(parents=True, exist_ok=True) + (root / name).write_text(content, encoding="utf-8") + return check_test_registration.unrun_scripts(root)[1] + + def test_script_registered_by_a_nested_add_test_runs(self): + self.assertEqual(self.unrun({ + "core/relinker/CMakeLists.txt": "if(WIN32)\n add_test(NAME a COMMAND ${Python3_EXECUTABLE}\n ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_a.py)\nendif()\n", + "core/relinker/relinker/tests/test_a.py": ""}), []) + + def test_unregistered_script_is_reported(self): + self.assertEqual(self.unrun({ + "CMakeLists.txt": "add_test(NAME a COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_a.py)\n", + "tools/tests/test_a.py": "", "tools/tests/test_b.py": "", "tools/tests/helper.py": ""}), ["tools/tests/test_b.py"]) + + def test_commented_add_test_does_not_run_a_script(self): + self.assertEqual(self.unrun({ + "CMakeLists.txt": "#add_test(NAME a COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/test_a.py)\n#[[\nadd_test(NAME a COMMAND python test_a.py)\n]]\n", + "test_a.py": ""}), ["test_a.py"]) + + def test_registration_names_its_directory(self): + self.assertEqual(self.unrun({ + "CMakeLists.txt": "add_test(NAME a COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/a/tests/test_a.py)\n", + "a/tests/test_a.py": "", "b/tests/test_a.py": ""}), ["b/tests/test_a.py"]) + + def test_workflow_run_counts_and_a_paths_filter_does_not(self): + self.assertEqual(self.unrun({ + ".github/workflows/pages.yml": "jobs:\n web:\n steps:\n - run: python3 tools/test_web.py --build build-wasm\n", + ".github/workflows/web.yml": "on:\n pull_request:\n paths:\n - tools/test_paths.py\n", + "tools/test_web.py": "", "tools/test_paths.py": ""}), ["tools/test_paths.py"]) + + def test_commented_workflow_line_does_not_run_a_script(self): + self.assertEqual(self.unrun({ + ".github/workflows/ci.yml": "# python3 tools/tests/test_f.py\njobs:\n t:\n steps:\n # python3 tools/tests/test_a.py\n" + " - run: |\n #python3 tools/tests/test_b.py\n" + " \t# python3 tools/tests/test_c.py\n" + " echo done\t# python3 tools/tests/test_d.py\n" + " python3 tools/tests/test_e.py\n", + "tools/tests/test_a.py": "", "tools/tests/test_b.py": "", "tools/tests/test_c.py": "", + "tools/tests/test_d.py": "", "tools/tests/test_e.py": "", "tools/tests/test_f.py": ""}), + ["tools/tests/test_a.py", "tools/tests/test_b.py", "tools/tests/test_c.py", "tools/tests/test_d.py", + "tools/tests/test_f.py"]) + + +class CheckRegistrationCliTests(unittest.TestCase): + def test_cache_with_utf8_bom_is_read(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + cache = build / "CMakeCache.txt" + cache.write_text("\ufeffCMAKE_MAKE_PROGRAM:FILEPATH=ninja\nCMAKE_GENERATOR:INTERNAL=Ninja\n", encoding="utf-8") + output = "tests/dummy_tests: EXECUTABLE\n" + with patch.object(check_test_registration.subprocess, "run", side_effect=lambda command, **kwargs: SimpleNamespace( + stdout="Build file encoding: UTF-8\n" if command[-1] == "wincodepage" else output)): + names = check_test_registration.built(build) + self.assertEqual(names, {"dummy_tests"}) + + def test_main_scripts_with_explicit_argv(self): + with patch.object(check_test_registration, "unrun_scripts", return_value=(["test_a.py"], [])): + with contextlib.redirect_stdout(io.StringIO()): + code = check_test_registration.main(["--scripts"]) + self.assertEqual(code, 0) + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index 4e18d2d2cc..a7fa3619db 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -2,6 +2,8 @@ import contextlib import gzip import io import json +import os +import subprocess import sys import tempfile import unittest @@ -44,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) @@ -57,13 +78,129 @@ class TraceDigestTests(unittest.TestCase): def tearDown(self): self.directory.cleanup() + def test_folded_only_threads_are_reported_in_json_and_markdown(self): + baseline = {thread["thread"]: thread for thread in self.result()["threads"]} + for name, total in (("shader compiler", 6.0), ("tid 3", 6.0), ("shader compiler", 0.0)): + with self.subTest(name=name, total=total): + events = EVENTS + [ + {"name": "thread_name", "ph": "M", "ts": 0, "pid": 7, "tid": 3, "args": {"name": name}}, + {"name": "thread_name", "ph": "M", "ts": 0, "pid": 7, "tid": 4, "args": {"name": "idle"}}, + {"name": f"{name} folded", "ph": "C", "ts": 0, "pid": 7, "tid": 3, "args": {"ms": total / 2}}, + {"name": f"{name} folded", "ph": "C", "ts": 20000, "pid": 7, "tid": 3, "args": {"ms": total / 2}}, + ] + with gzip.open(self.trace, "wt", encoding="utf-8") as handle: + json.dump({"traceEvents": events}, handle) + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(trace_digest.main([str(self.trace), "--json"]), 0) + result = json.loads(output.getvalue()) + threads = {thread["thread"]: thread for thread in result["threads"]} + self.assertEqual(set(threads), set(baseline) | {name}) + self.assertEqual(threads[name]["folded_ms_per_frame"], total / 3) + self.assertEqual(threads[name]["busy_ms_per_frame"], 0) + self.assertEqual(threads[name]["wait_ms_per_frame"], 0) + self.assertEqual(threads[name]["top"], []) + for thread_name, thread in baseline.items(): + self.assertEqual(threads[thread_name], thread) + output = io.StringIO() + with contextlib.redirect_stdout(output): + trace_digest.main([str(self.trace)]) + self.assertIn(f"| {name} | 0.0 (0 %) | 0.0 (0 %) |", output.getvalue()) + + def test_folded_only_thread_without_frame_boundaries(self): + trace = trace_digest.Trace({"traceEvents": [ + {"name": "compiler folded", "ph": "C", "ts": 0, "tid": 3, "args": {"ms": 1.5}}, + ]}) + thread = trace_digest.digest(trace)["threads"][0] + self.assertEqual(thread["thread"], "tid 3") + 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) + def json_output(self, trace, *arguments): + result = subprocess.run([sys.executable, trace_digest.__file__, str(trace), "--json", *arguments], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 0, result.stderr) + return json.loads(result.stdout) + + def test_utf8_bom_plain_and_compressed_traces_preserve_the_digest(self): + events = [dict(event, args={"name": "worker \u03a9"}) if event["name"] == "thread_name" and event["tid"] == 1 + else event for event in EVENTS] + text = json.dumps({"traceEvents": events}, ensure_ascii=False) + self.trace.write_bytes(gzip.compress(text.encode("utf-8"))) + expected = self.json_output(self.trace) + for trace in (self.trace, self.trace.with_suffix("")): + for encoding in ("utf-8", "utf-8-sig"): + with self.subTest(trace=trace.name, encoding=encoding): + data = text.encode(encoding) + trace.write_bytes(gzip.compress(data) if trace.suffix == ".gz" else data) + self.assertEqual(self.json_output(trace), expected) + + def test_utf8_bom_summary_preserves_the_digest(self): + expected = self.json_output(self.trace) + summary = self.trace.with_name("aps5-summary.jsonl") + summary.write_text(summary.read_text(encoding="utf-8"), encoding="utf-8-sig") + self.assertEqual(self.json_output(self.trace), expected) + + def test_utf8_bom_stacks_preserve_comments_and_frame_names(self): + for text in (STACKS, "root;module\u03a9+0x1 2\n"): + with self.subTest(comments=text.startswith("#")): + self.stacks.write_text(text, encoding="utf-8") + expected = self.json_output(self.trace, "--stacks", str(self.stacks)) + expected_stacks = trace_digest.load_stacks(self.stacks) + self.stacks.write_text(text, encoding="utf-8-sig") + self.assertEqual(self.json_output(self.trace, "--stacks", str(self.stacks)), expected) + self.assertEqual(trace_digest.load_stacks(self.stacks), expected_stacks) + def test_summary_is_the_line_of_this_trace(self): self.assertEqual(self.result()["summary"]["trace"], "aps5-trace-abc-7.json.gz") + def test_decompressing_trace_preserves_the_complete_command_line_digest(self): + uncompressed = self.trace.with_suffix("") + uncompressed.write_bytes(gzip.decompress(self.trace.read_bytes())) + results = [] + for trace in (self.trace, uncompressed): + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(trace_digest.main([str(trace), "--json"]), 0) + results.append(json.loads(output.getvalue())) + self.assertEqual((results[0]["frames"], results[0]["window_ms"]), (3, 40.0)) + self.assertEqual(results[1], results[0]) + + def test_compressed_trace_can_use_a_summary_named_for_its_plain_copy(self): + summary = dict(SUMMARY, trace=self.trace.with_suffix("").name) + path = self.trace.with_name("named-summary.jsonl") + path.write_text(json.dumps(summary) + "\n", encoding="utf-8") + self.assertEqual(trace_digest.load_summary(path, self.trace), summary) + + def test_exact_trace_filename_wins_over_a_later_compression_alias(self): + uncompressed = self.trace.with_suffix("") + exact = dict(SUMMARY, trace=uncompressed.name, commit="exact") + alias = dict(SUMMARY, commit="alias") + path = self.trace.with_name("aps5-summary.jsonl") + path.write_text(json.dumps(exact) + "\n" + json.dumps(alias) + "\n", encoding="utf-8") + self.assertEqual(trace_digest.load_summary(None, uncompressed), exact) + self.assertEqual(trace_digest.load_summary(None, self.trace), alias) + + def test_decompressed_trace_uses_the_latest_matching_alias_and_skips_other_traces(self): + uncompressed = self.trace.with_suffix("") + latest = dict(SUMMARY, commit="latest") + unrelated = dict(SUMMARY, trace="other.json.gz", commit="other") + path = self.trace.with_name("aps5-summary.jsonl") + path.write_text("\n".join(json.dumps(line) for line in (SUMMARY, latest, unrelated)) + "\n", encoding="utf-8") + self.assertEqual(trace_digest.load_summary(None, uncompressed), latest) + def test_explicit_summary_selects_the_matching_trace(self): summary = Path(self.directory.name) / "relocated-summary.jsonl" other = dict(SUMMARY, trace="other.json.gz", presents=99) @@ -82,6 +219,16 @@ class TraceDigestTests(unittest.TestCase): self.assertEqual(result["frames"], 3) self.assertAlmostEqual(result["window_ms"], 40.0) + def test_command_line_prints_non_ascii_names_on_a_legacy_console(self): + events = [dict(event, args={"name": "メイン"}) if event["name"] == "thread_name" and event["tid"] == 1 else event for event in EVENTS] + with gzip.open(self.trace, "wt", encoding="utf-8") as handle: + json.dump({"traceEvents": events}, handle) + tool = Path(trace_digest.__file__).resolve() + result = subprocess.run([sys.executable, str(tool), str(self.trace)], capture_output=True, + env=dict(os.environ, PYTHONIOENCODING="cp1252"), timeout=60) + self.assertEqual(result.returncode, 0, result.stderr.decode("utf-8", "replace")) + self.assertIn("メイン", result.stdout.decode("utf-8")) + def test_slowest_frame_lists_its_slices(self): slowest = self.result()["slowest_frames"] self.assertEqual([frame["present"] for frame in slowest], [601, 600]) @@ -91,6 +238,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"] @@ -103,6 +267,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") @@ -114,6 +315,132 @@ class TraceDigestTests(unittest.TestCase): self.assertAlmostEqual(waits[0]["ms"], 14.5) self.assertEqual(waits[1]["name"], "Driver.WorkerWait") + def test_longest_waits_omit_slices_outside_capture_in_cli(self): + events = [EVENTS[1], *EVENTS[-3:], + complete(1, "Inside.wait", 1000, 2000), + complete(3, "Before.wait", -100000, 90000), + complete(4, "After.wait", 40000, 90000), + complete(5, "EndsAtStart.wait", -1000, 1000), + complete(6, "StartsAtEnd.wait", 40000, 1000)] + plain = self.trace.with_suffix("") + plain.write_text(json.dumps({"traceEvents": events}), encoding="utf-8") + self.trace.write_bytes(gzip.compress(plain.read_bytes())) + for path in (self.trace, plain): + for output in ([], ["--json"]): + with self.subTest(path=path.name, output=output): + result = subprocess.run([sys.executable, trace_digest.__file__, str(path), "--top", "1", *output], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual((result.returncode, result.stderr), (0, "")) + if output: + report = json.loads(result.stdout) + self.assertEqual(report["longest_waits"], [ + {"name": "Inside.wait", "thread": "queue worker 0x0", "ms": 2.0, "at_ms": 1.0, "present": 600}]) + else: + self.assertIn("- Inside.wait (queue worker 0x0) 2.0 ms at 1 ms, present 600", result.stdout) + for name in ("Before.wait", "After.wait", "EndsAtStart.wait", "StartsAtEnd.wait"): + self.assertNotIn(name, result.stdout) + + def test_longest_waits_rank_only_the_overlap_with_capture(self): + events = [dict(EVENTS[-3], ts=10000), dict(EVENTS[-2], ts=30000), + complete(1, "Leading.wait", -100000, 111000), + complete(2, "Inside.wait", 12000, 4000), + complete(3, "Trailing.wait", 28000, 90000), + complete(4, "Spanning.wait", -100000, 230000), + complete(4, "Nested.wait", 11000, 1000)] + report = trace_digest.digest(trace_digest.Trace(events), top=4) + self.assertEqual(report["longest_waits"], [ + {"name": "Spanning.wait", "thread": "tid 4", "ms": 20.0, "at_ms": 10.0, "present": 600}, + {"name": "Inside.wait", "thread": "tid 2", "ms": 4.0, "at_ms": 12.0, "present": 600}, + {"name": "Trailing.wait", "thread": "tid 3", "ms": 2.0, "at_ms": 28.0, "present": 600}, + {"name": "Leading.wait", "thread": "tid 1", "ms": 1.0, "at_ms": 10.0, "present": 600}]) + threads = {thread["thread"]: thread for thread in report["threads"]} + for wait in report["longest_waits"]: + self.assertEqual(wait["ms"], threads[wait["thread"]]["wait_ms_per_frame"]) + + def test_longest_waits_clip_to_summary_end(self): + trace = trace_digest.Trace([EVENTS[-3], + complete(1, "Trailing.wait", 8000, 20000), + complete(2, "Inside.wait", 1000, 3000)]) + report = trace_digest.digest(trace, {"window": {"ms": 10.0}}, top=1) + self.assertEqual(report["longest_waits"], [ + {"name": "Inside.wait", "thread": "tid 2", "ms": 3.0, "at_ms": 1.0, "present": 600}]) + + def test_longest_waits_without_frames_keep_the_full_slices(self): + for duration in (0, 3000): + with self.subTest(duration=duration): + trace = trace_digest.Trace([complete(1, "Worker.wait", -1000, duration)]) + self.assertEqual(trace_digest.digest(trace)["longest_waits"], [ + {"name": "Worker.wait", "thread": "tid 1", "ms": duration / 1000.0, "at_ms": -1.0, "present": None}]) + + def test_wait_subscope_without_parent_is_not_busy(self): + events = [EVENTS[1], complete(1, "Driver.WorkerWait.dequeue_order", 0, 4000), + complete(1, "Driver.Packet", 4000, 5000), EVENTS[-3], EVENTS[-2]] + result = trace_digest.digest(trace_digest.Trace({"traceEvents": events})) + worker = result["threads"][0] + self.assertAlmostEqual(worker["wait_pct"], 40.0) + self.assertAlmostEqual(worker["busy_pct"], 50.0) + self.assertEqual(result["longest_waits"][0]["name"], "Driver.WorkerWait.dequeue_order") + self.assertEqual(worker["top"][0]["name"], "Driver.Packet") + + def test_nested_wait_subscope_does_not_double_count(self): + events = [EVENTS[1], complete(1, "Driver.WorkerWait", 0, 4000), + complete(1, "Driver.WorkerWait.dequeue_order", 1000, 2000), + complete(1, "Driver.Packet", 4000, 5000), EVENTS[-3], EVENTS[-2]] + result = trace_digest.digest(trace_digest.Trace({"traceEvents": events})) + worker = result["threads"][0] + self.assertAlmostEqual(worker["wait_pct"], 40.0) + self.assertAlmostEqual(worker["busy_pct"], 50.0) + self.assertEqual(len(result["longest_waits"]), 1) + + def test_gpu_mutex_waits_with_site_names(self): + for site in ("other", "dispatch", "indirect", "draw", "hook", "wait", "label", "flush", "present", "fill", "copy", "end", "try"): + with self.subTest(site=site): + name = f"GpuMutex.wait.{site}" + events = [ + complete(1, "Driver.Packet", 0, 10000), + complete(1, name, 1000, 4000), + complete(1, "Driver.Packet.memory_wait", 2000, 1000), + {"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({"traceEvents": events})) + thread = result["threads"][0] + self.assertAlmostEqual(thread["wait_ms_per_frame"], 4.0) + self.assertAlmostEqual(thread["busy_ms_per_frame"], 6.0) + self.assertAlmostEqual(thread["wait_pct"], 40.0) + self.assertAlmostEqual(thread["busy_pct"], 60.0) + self.assertEqual(result["longest_waits"], [ + {"name": name, "thread": "tid 1", "ms": 4.0, "at_ms": 1.0, "present": 600}, + ]) + self.assertIn(f"- {name} (tid 1) 4.0 ms", trace_digest.markdown(result)) + + def test_top_level_gpu_mutex_wait_is_not_busy_work(self): + events = [ + complete(1, "GpuMutex.wait.draw", 0, 4000), + complete(1, "GpuMutex.hold.draw", 4000, 6000), + {"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({"traceEvents": events})) + thread = result["threads"][0] + self.assertAlmostEqual(thread["busy_ms_per_frame"], 6.0) + 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) @@ -124,6 +451,54 @@ class TraceDigestTests(unittest.TestCase): self.assertNotIn("a.prx+0x10", total) self.assertEqual(total, {"a.prx+0x20": 60.0, "a.prx+0x30": 40.0, "a.prx+0x40": 20.0}) + def test_largest_gpu_batches_with_their_recording(self): + def tagged(tid, name, ts, dur, batch): + return dict(complete(tid, name, ts, dur), args={"batch": batch}) + events = EVENTS + [ + {"name": "thread_name", "ph": "M", "ts": 0, "pid": 7, "tid": 3, "args": {"name": "gpu"}}, + tagged(1, "Driver.Packet.dispatch", 4500, 2000, 11), + tagged(1, "Driver.Packet.dispatch", 13000, 3000, 12), + tagged(3, "batch", 6000, 9000, 11), + tagged(3, "draw", 7000, 4000, 11), + tagged(3, "batch", 16000, 3000, 12), + ] + with gzip.open(self.trace, "wt", encoding="utf-8") as handle: + json.dump({"traceEvents": events}, handle) + batches = self.result()["largest_gpu_batches"] + self.assertEqual([batch["batch"] for batch in batches], [11, 12]) + self.assertAlmostEqual(batches[0]["ms"], 9.0) + self.assertEqual(batches[0]["present"], 600) + self.assertEqual(batches[0]["gpu_ranges"], [{"name": "draw", "ms": 4.0}]) + self.assertEqual(batches[0]["recorded_by"], [{"name": "Driver.Packet.dispatch", "thread": "queue worker 0x0", "ms": 2.0}]) + self.assertEqual(batches[1]["present"], 601) + output = io.StringIO() + with contextlib.redirect_stdout(output): + trace_digest.main([str(self.trace)]) + self.assertIn("**Largest GPU batches**", output.getvalue()) + + 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): @@ -137,6 +512,78 @@ class TraceDigestTests(unittest.TestCase): trace_digest.main([str(self.trace), "--json"]) self.assertEqual(json.loads(output.getvalue())["frames"], 3) + def test_fractional_adjacent_slices_remain_separate_in_command_output(self): + for first, second in (("Driver.Packet", "Driver.Completion"), + ("Driver.WorkerWait", "Driver.CompletionWait")): + with self.subTest(first=first): + events = [EVENTS[1], EVENTS[-3], + complete(1, first, 10000.1, 20000.2), + complete(1, second, 30000.3, 20000.0)] + with gzip.open(self.trace, "wt", encoding="utf-8") as handle: + json.dump({"traceEvents": events}, handle) + summary = dict(SUMMARY, window=dict(SUMMARY["window"], ms=50.0003)) + self.trace.with_name("aps5-summary.jsonl").write_text(json.dumps(summary), encoding="utf-8") + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(trace_digest.main([str(self.trace), "--json"]), 0) + result = json.loads(output.getvalue()) + thread = next(thread for thread in result["threads"] if thread["thread"] == "queue worker 0x0") + field = "wait" if trace_digest.is_wait(first) else "busy" + self.assertAlmostEqual(thread[field + "_ms_per_frame"], 40.0002) + self.assertAlmostEqual(thread[field + "_pct"], 100 * 40.0002 / 50.0003) + self.assertAlmostEqual(thread["untraced_ms_per_frame"], 10.0001) + if field == "busy": + self.assertEqual({entry["name"] for entry in thread["top"]}, {first, second}) + else: + self.assertEqual({entry["name"] for entry in result["longest_waits"]}, {first, second}) + + def test_one_nanosecond_nested_wait_remains_nested(self): + trace = trace_digest.Trace({"traceEvents": [ + complete(1, "Driver.WorkerWait", 10000.1, 20000.2), + complete(1, "Driver.InnerWait", 30000.299, 0.001), + complete(1, "Driver.Completion", 30000.3, 20000.0)]}) + 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 test_thread_filter_keeps_frame_bounds_and_capture_summary(self): + command = [sys.executable, trace_digest.__file__, str(self.trace)] + result = subprocess.run([*command, "--thread", "queue worker 0x0", "--json"], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + report = json.loads(result.stdout) + self.assertEqual(report["frames"], 3) + self.assertEqual(report["summary"], SUMMARY) + self.assertEqual(report["thread_filter"], ["queue worker 0x0"]) + self.assertEqual([thread["thread"] for thread in report["threads"]], ["queue worker 0x0"]) + self.assertTrue(all("Present" not in entry["name"] for entry in report["top_slices_ms_per_frame"])) + self.assertTrue(all(wait["thread"] == "queue worker 0x0" for wait in report["longest_waits"])) + result = subprocess.run([*command, "--thread", "presenter", "--thread", "queue worker 0x0", "--json"], + capture_output=True, text=True) + unfiltered = trace_digest.digest(trace_digest.load_trace(self.trace), SUMMARY) + report = json.loads(result.stdout) + del report["thread_filter"] + self.assertEqual(report, unfiltered) + result = subprocess.run([*command, "--thread", "presenter"], capture_output=True, text=True) + self.assertIn("Thread filter:", result.stdout) + self.assertIn("capture-wide", result.stdout) + result = subprocess.run([*command, "--thread", "misspelled"], capture_output=True, text=True) + self.assertEqual(result.returncode, 2) + self.assertIn("unknown thread", result.stderr) + self.assertEqual(result.stdout, "") + result = subprocess.run([sys.executable, trace_digest.__file__, str(self.trace.with_suffix(".jsonl")), + "--thread", "presenter"], capture_output=True, text=True) + self.assertEqual(result.returncode, 2) + self.assertIn("event trace", result.stderr) + + def interval_line(start, end, p99, gpu_ms, **changes): line = dict(SUMMARY, window={"spec": "summary_every 30", "first_present": 0, "ms": (end - start) * 1000.0}, interval_start_s=start, interval_end_s=end, @@ -159,6 +606,14 @@ class IntervalDigestTests(unittest.TestCase): def test_window_lines_are_skipped(self): self.assertEqual(len(trace_digest.load_intervals(self.path)), 4) + def test_utf8_bom_intervals_preserve_the_command_line_digest(self): + expected = trace_digest.digest_intervals(trace_digest.load_intervals(self.path)) + self.path.write_text(self.path.read_text(encoding="utf-8"), encoding="utf-8-sig") + result = subprocess.run([sys.executable, trace_digest.__file__, str(self.path), "--json"], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(result.stdout), expected) + def test_rows(self): result = trace_digest.digest_intervals(trace_digest.load_intervals(self.path)) first, second, third, _ = result["intervals"] @@ -170,11 +625,37 @@ class IntervalDigestTests(unittest.TestCase): self.assertIsNone(third["top_phase"]) self.assertEqual(result["identity_changed_at"], [3]) + def test_identity_changes_are_reported_where_they_happen(self): + lines = [interval_line(30.0 * index, 30.0 * (index + 1), 20.0, 5.0, commit=commit) + for index, commit in enumerate(("a", "a", "b", "b", "a"))] + result = trace_digest.digest_intervals(lines) + self.assertEqual(result["identity_changed_at"], [2, 4]) + self.assertIn("commit, environment, GPU, driver or OS changed at interval 3, 5", trace_digest.markdown_intervals(result).splitlines()) + def test_sparkline_scales_to_the_largest_value(self): self.assertEqual(trace_digest.sparkline([0.0, 5.0, 10.0]), ".+@") self.assertEqual(trace_digest.sparkline([0.0, 0.0]), "..") self.assertEqual(trace_digest.sparkline([]), "") + def test_largest_phase_does_not_depend_on_json_key_order(self): + phases = [("Driver.Completion", 1.0), ("Driver.Packet", 7.0), ("Driver.Present", 2.0)] + for offset in range(len(phases)): + for sort_keys in (False, True): + with self.subTest(offset=offset, sort_keys=sort_keys): + line = interval_line(0.0, 30.0, 20.0, 5.0, + top_phases_ms_per_frame=dict(phases[offset:] + phases[:offset])) + self.path.write_text(json.dumps(line, sort_keys=sort_keys) + "\n", encoding="utf-8") + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(trace_digest.main([str(self.path), "--json"]), 0) + result = json.loads(output.getvalue()) + self.assertEqual(result["intervals"][0]["top_phase"], + {"name": "Driver.Packet", "ms_per_frame": 7.0}) + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(trace_digest.main([str(self.path)]), 0) + self.assertIn("| Driver.Packet 7.0 |", output.getvalue()) + def test_command_line_table_and_json(self): output = io.StringIO() with contextlib.redirect_stdout(output): @@ -196,5 +677,52 @@ 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"]) + + def test_bom_encoded_stacks_load(self): + with tempfile.TemporaryDirectory() as directory: + stacks_file = Path(directory) / "stacks.txt" + stacks_file.write_bytes(b"\xef\xbb\xbf# samples=1 depth=1 truncated=0\nmodule.prx+0x10 1\n") + loaded = trace_digest.load_stacks(stacks_file) + self.assertEqual(loaded, [(["module.prx+0x10"], 1)]) + + def test_bom_encoded_intervals_load(self): + with tempfile.TemporaryDirectory() as directory: + intervals_file = Path(directory) / "intervals.jsonl" + intervals_file.write_bytes(b"\xef\xbb\xbf" + json.dumps({"interval_start_s": 0.0, "interval_end_s": 10.0, "presents": 600}).encode("utf-8") + b"\n") + loaded = trace_digest.load_intervals(intervals_file) + self.assertEqual(len(loaded), 1) + self.assertEqual(loaded[0]["interval_start_s"], 0.0) + + def test_bom_encoded_summary_loads(self): + with tempfile.TemporaryDirectory() as directory: + summary_file = Path(directory) / "aps5-summary.jsonl" + summary_file.write_bytes(b"\xef\xbb\xbf" + json.dumps(SUMMARY).encode("utf-8") + b"\n") + loaded = trace_digest.load_summary(str(summary_file), SUMMARY["trace"]) + self.assertEqual(loaded["commit"], SUMMARY["commit"]) + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py new file mode 100644 index 0000000000..f30eb5a74f --- /dev/null +++ b/tools/tests/test_unwrap_self.py @@ -0,0 +1,783 @@ +import contextlib +import io +import os +import struct +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import unwrap_self + +SELF_MAGIC = 0x1D3D154F +ELF_HEADER_OFFSET = 0x1A0 +PROGRAM_TABLE_OFFSET = 0x40 +PT_LOAD = 1 +PT_DYNAMIC = 2 +PT_TLS = 7 +PT_NOTE = 4 +SCE_COMMENT = 0x6FFFFF00 +SCE_VERSION = 0x6FFFFF01 +FIRST_REGION = 0x400 + +HEADER = b"\x7fELF\x02\x01\x01\x09" + b"\x00" * 56 +PAGE = 0x1000 + + +def pattern(seed, size): + return bytes((seed + index * 7) % 251 for index in range(size)) + + +def build_elf(segments, program_offset=PROGRAM_TABLE_OFFSET): + header = bytearray(HEADER) + struct.pack_into(" 1: + self.assertEqual(offset % align, vaddr % align, hex(kind)) + self.assertEqual([data[header[2]:header[2] + header[5]] for header in headers], + [pattern(1, 0x100), pattern(9, 0x88)] + blocks) + + def test_file_backed_load_uses_the_first_congruent_offset_after_the_headers(self): + cases = ((PROGRAM_TABLE_OFFSET, 0, 0x1400, 0x400), + (PROGRAM_TABLE_OFFSET, 1, 0x1400, 0x400), + (PROGRAM_TABLE_OFFSET, 8, 0x1003, 0x403), + (PROGRAM_TABLE_OFFSET, PAGE, 0x1400, 0x400), + (PROGRAM_TABLE_OFFSET, PAGE, 0x1480, 0x480), + (PROGRAM_TABLE_OFFSET, PAGE, 0x1200, 0x1200), + (PROGRAM_TABLE_OFFSET, PAGE, 0x1000, 0x1000), + (PROGRAM_TABLE_OFFSET, PAGE, 0x1FFF, 0xFFF), + (0xFF0, PAGE, 0x2100, 0x1100), + (PROGRAM_TABLE_OFFSET, 1 << 63, 0x480, 0x480)) + payload = pattern(3, 0x20) * 8 + for program_offset, alignment, address, expected in cases: + with self.subTest(program_offset=program_offset, alignment=alignment, address=address): + segments = [(PT_LOAD, 5, 0x1234, address, len(payload), alignment)] + container = build_container(segments, [(0x2804, 0x2000, len(payload))], + {0x2000: payload}, program_offset=program_offset) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + data, headers = read_program_headers(output) + load = headers[0] + self.assertEqual(load[2], expected) + self.assertGreaterEqual(load[2], max(FIRST_REGION, program_offset + 56)) + self.assertEqual(load[2] % max(alignment, 1), address % max(alignment, 1)) + self.assertEqual(load[3:], (address, address, len(payload), len(payload), alignment)) + self.assertEqual(data[load[2]:load[2] + load[5]], payload) + self.assertEqual(len(data), expected + len(payload)) + self.assertEqual((Path(self.directory) / "input.self").read_bytes(), container) + + def test_recorded_non_load_region_can_start_at_an_already_congruent_cursor(self): + load_payload = pattern(3, 0x20) * 8 + b"tail" + separate_payload = pattern(9, 0x10) + for kind in (PT_DYNAMIC, PT_TLS): + for alignment in (8, 0x10): + with self.subTest(kind=kind, alignment=alignment): + segments = [(PT_LOAD, 5, 0x1234, 0x1000, len(load_payload), PAGE), + (kind, 4, 0x2000, 0x1004, len(separate_payload), alignment)] + records = [(0x2804, 0x800, len(load_payload)), (0x102804, 0xA00, len(separate_payload))] + container = build_container(segments, records, {0x800: load_payload, 0xA00: separate_payload}) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + data, headers = read_program_headers(output) + load, separate = headers + self.assertEqual(load[2], PAGE) + self.assertEqual(separate[2], load[2] + len(load_payload)) + self.assertEqual(separate[2] % alignment, separate[3] % alignment) + self.assertEqual(data[load[2]:load[2] + load[5]], load_payload) + self.assertEqual(data[separate[2]:separate[2] + separate[5]], separate_payload) + self.assertEqual(len(data), separate[2] + len(separate_payload)) + + def test_declared_offsets_are_used_when_the_container_has_no_records(self): + segments = [(PT_LOAD, 1, 0x800, 0x0, 0x40, 0x1000), (PT_LOAD, 4, 0x900, 0x1000, 0x40, 0x1000)] + payloads = {0x800: pattern(3, 0x40), 0x900: pattern(5, 0x40)} + container = build_container(segments, [], payloads) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + data, headers = read_program_headers(output) + loads = [header for header in headers if header[0] == PT_LOAD] + self.assertEqual(data[loads[0][2]:loads[0][2] + 0x40], pattern(3, 0x40)) + self.assertEqual(data[loads[1][2]:loads[1][2] + 0x40], pattern(5, 0x40)) + for kind, flags, offset, vaddr, _, size, _, align in loads: + self.assertEqual(offset % align, vaddr % align) + + def test_program_headers_do_not_overwrite_the_first_load_payload(self): + for count, program_offset in ((74, PROGRAM_TABLE_OFFSET), (3, 0xFF0)): + with self.subTest(count=count, program_offset=program_offset): + payload = pattern(3, 0x20) * 8 + segments = [(PT_LOAD, 5, 0x1234, 0, len(payload), PAGE)] + segments.extend([(0, 0, 0, 0, 0, 0)] * (count - 1)) + container = build_container(segments, [(0x2804, 0x2000, len(payload))], + {0x2000: payload}, program_offset=program_offset) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + data, headers = read_program_headers(output) + load = headers[0] + self.assertEqual(data[load[2]:load[2] + load[5]], payload) + self.assertGreaterEqual(load[2], program_offset + count * 56) + self.assertEqual(load[2] % PAGE, load[3] % PAGE) + self.assertEqual(len(headers), count) + self.assertTrue(all(header[0] == 0 for header in headers[1:])) + + def test_the_verbose_report_names_every_program_header(self): + container = build_container(default_segments(), default_records(), default_payloads()) + directory = Path(self.directory) + source = directory / "input.self" + output = directory / "output.elf" + source.write_bytes(container) + result = subprocess.run([sys.executable, str(Path(unwrap_self.__file__)), "--verbose", + str(source), str(output)], capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("PT_LOAD", result.stdout) + self.assertIn("PT_DYNAMIC", result.stdout) + + def test_a_file_that_is_not_a_container_is_refused(self): + result, output = self.convert(b"\x7fCNT" + bytes(FIRST_REGION), "input.pkg") + self.assertEqual(result.returncode, 2) + self.assertIn("not a SELF container", result.stderr) + self.assertFalse(output.exists()) + + def test_a_container_without_an_inner_elf_is_refused(self): + container = build_container(default_segments(), default_records(), default_payloads(), inner_elf=False) + result, output = self.convert(container) + self.assertEqual(result.returncode, 2) + self.assertIn("no inner ELF header", result.stderr) + self.assertFalse(output.exists()) + + def test_incomplete_elf_metadata_is_refused_without_replacing_output(self): + truncated_header = bytearray(FIRST_REGION) + struct.pack_into(" 1: + raise AssertionError("Release selection changed during publication") + return io.BytesIO(json.dumps(self.release).encode()) + return io.BytesIO(self.responses[url]) + + def publish(self): + with patch.object(build_web, "request", self.request): + build_web.publish_release(self.output) + + def snapshot(self): + return {path.relative_to(self.output).as_posix(): path.read_bytes() + for path in self.output.rglob("*") if path.is_file()} + + def seed_previous(self): + for name, content in {"index.html": b"old page", "native/relinker_wasm.wasm": b"newer main wasm", + "release.json": b"old manifest", "libraries/old.zip": b"old libraries"}.items(): + path = self.output / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(content) + + def assert_rejected(self, message): + self.seed_previous() + previous = self.snapshot() + with self.assertRaisesRegex(ValueError, message): + self.publish() + self.assertEqual(self.snapshot(), previous) + self.assertEqual(list(self.build.glob(".web-release-*")), []) + + def test_release_replaces_main_converter_with_matching_frontend_and_libraries(self): + self.seed_previous() + self.publish() + for name, content in self.contents.items(): + self.assertEqual((self.output / name).read_bytes(), content) + self.assertEqual(json.loads((self.output / "web-release.json").read_text()), self.metadata) + manifest = json.loads((self.output / "release.json").read_text()) + self.assertEqual((manifest["tag"], manifest["revision"], manifest["url"]), + ("v1", REVISION, self.release["html_url"])) + for platform in ("windows", "linux"): + asset = manifest["platforms"][platform] + data = (self.output / asset["path"]).read_bytes() + self.assertEqual(len(data), asset["size"]) + self.assertEqual(hashlib.sha256(data).hexdigest(), asset["sha256"]) + with zipfile.ZipFile(io.BytesIO(data)) as archive: + self.assertEqual(archive.read("libs/libkernel.prx"), platform.encode()) + self.assertTrue((self.output / ".nojekyll").is_file()) + self.assertEqual(self.requests.count(LATEST), 1) + + def test_libraries_flag_also_replaces_an_incompatible_local_converter(self): + self.seed_previous() + source = self.root / "source/demo/web" + source.mkdir(parents=True) + (source / "index.html").write_bytes(b"main frontend") + (self.output / "native/relinker_wasm.mjs").write_bytes(b"main converter") + with patch.object(build_web, "ROOT", self.root / "source"), \ + patch.object(build_web, "request", self.request), \ + patch.object(sys, "argv", ["build_web.py", "--build", str(self.build), "--libraries"]): + build_web.main() + self.assertEqual((self.output / "native/relinker_wasm.wasm").read_bytes(), self.contents["native/relinker_wasm.wasm"]) + self.assertEqual((self.output / "index.html").read_bytes(), self.contents["index.html"]) + + def test_publication_replaces_obsolete_files_and_directory_conflicts(self): + self.seed_previous() + conflict = self.output / "native/relinker_wasm.mjs" + conflict.mkdir() + (conflict / "obsolete.txt").write_bytes(b"previous layout") + self.publish() + expected = {*FILES, "web-release.json", "release.json", ".nojekyll"} + manifest = json.loads((self.output / "release.json").read_text()) + expected.update(asset["path"] for asset in manifest["platforms"].values()) + self.assertEqual(set(self.snapshot()), expected) + for name, content in self.contents.items(): + self.assertEqual((self.output / name).read_bytes(), content) + + def test_failed_publication_restores_the_previous_site(self): + self.seed_previous() + previous = self.snapshot() + rename = Path.rename + + def failed_rename(path, destination): + if path != self.output and Path(destination) == self.output and path.name == "site": + raise PermissionError("publication blocked") + return rename(path, destination) + + with patch.object(Path, "rename", failed_rename): + with self.assertRaisesRegex(PermissionError, "publication blocked"): + self.publish() + self.assertEqual(self.snapshot(), previous) + self.assertEqual(list(self.build.glob(".web-release-*")), []) + + def test_failed_first_publication_leaves_no_output(self): + self.output.rmdir() + rename = Path.rename + + def failed_rename(path, destination): + if Path(destination) == self.output: + raise PermissionError("publication blocked") + return rename(path, destination) + + with patch.object(Path, "rename", failed_rename): + with self.assertRaisesRegex(PermissionError, "publication blocked"): + self.publish() + self.assertFalse(self.output.exists()) + self.assertEqual(list(self.build.glob(".web-release-*")), []) + + def test_output_file_is_rejected_without_replacing_it(self): + self.output.rmdir() + self.output.write_bytes(b"unrelated file") + with self.assertRaisesRegex(ValueError, "Release output is not a directory"): + self.publish() + self.assertEqual(self.output.read_bytes(), b"unrelated file") + self.assertEqual(list(self.build.glob(".web-release-*")), []) + + def test_failed_backup_keeps_the_previous_site(self): + self.seed_previous() + previous = self.snapshot() + rename = Path.rename + + def failed_rename(path, destination): + if path == self.output: + raise PermissionError("previous site is in use") + return rename(path, destination) + + with patch.object(Path, "rename", failed_rename): + with self.assertRaisesRegex(PermissionError, "previous site is in use"): + self.publish() + self.assertEqual(self.snapshot(), previous) + self.assertEqual(list(self.build.glob(".web-release-*")), []) + + def test_failed_restoration_retains_the_backup_and_reports_its_path(self): + self.seed_previous() + previous = self.snapshot() + rename = Path.rename + + def failed_rename(path, destination): + if path != self.output and Path(destination) == self.output: + raise PermissionError("destination is unavailable") + return rename(path, destination) + + with patch.object(Path, "rename", failed_rename): + with self.assertRaisesRegex(RuntimeError, "Previous site is preserved at") as error: + self.publish() + temporary, = self.build.glob(".web-release-*") + backup = temporary / "previous" + self.assertIn(str(backup), str(error.exception)) + self.assertEqual({path.relative_to(backup).as_posix(): path.read_bytes() + for path in backup.rglob("*") if path.is_file()}, previous) + self.assertFalse(self.output.exists()) + + def test_release_mode_does_not_require_a_local_wasm_build(self): + with patch.object(build_web, "request", self.request), \ + patch.object(sys, "argv", ["build_web.py", "--build", str(self.build), "--release"]): + build_web.main() + self.assertEqual((self.output / "native/relinker_wasm.mjs").read_bytes(), self.contents["native/relinker_wasm.mjs"]) + + def test_local_build_still_copies_sources_without_network(self): + source = self.root / "source/demo/web" + source.mkdir(parents=True) + (source / "index.html").write_bytes(b"local frontend") + (self.output / "native").mkdir() + for name in ("relinker_wasm.mjs", "relinker_wasm.wasm"): + (self.output / "native" / name).write_bytes(b"local wasm") + with patch.object(build_web, "ROOT", self.root / "source"), \ + patch.object(build_web, "request", side_effect=AssertionError("Unexpected network request")), \ + patch.object(sys, "argv", ["build_web.py", "--build", str(self.build)]): + build_web.main() + self.assertEqual((self.output / "index.html").read_bytes(), b"local frontend") + self.assertEqual((self.output / "native/relinker_wasm.wasm").read_bytes(), b"local wasm") + + def test_old_release_without_web_asset_preserves_the_previous_site(self): + self.release["assets"] = [asset for asset in self.release["assets"] if asset["name"] != "web-v1.zip"] + self.assert_rejected("publish a release with the web converter") + + def test_ambiguous_web_asset_is_rejected(self): + self.release["assets"].append(dict(self.release["assets"][0])) + self.assert_rejected("ambiguous release asset") + + def test_wrong_tag_or_invalid_revision_is_rejected(self): + for metadata in ({"tag": "v2", "revision": REVISION}, {"tag": "v1", "revision": "main"}, + {"tag": "v1", "revision": None}, []): + with self.subTest(metadata=metadata): + self.requests.clear() + self.set_asset("web-v1.zip", zipped([*self.contents.items(), ("web-release.json", json.dumps(metadata))])) + self.assert_rejected("does not match") + + def test_unsafe_duplicate_missing_or_empty_files_are_rejected(self): + for entries in (self.entries + [("../escape", b"bad")], self.entries + [self.entries[0]], + self.entries[1:], [(name, b"" if name == "app.mjs" else data) for name, data in self.entries]): + with self.subTest(names=[name for name, data in entries]): + self.requests.clear() + self.set_asset("web-v1.zip", zipped(entries)) + self.assert_rejected("Invalid.*web release archive") + self.assertFalse((self.build / "escape").exists()) + + def test_web_size_and_checksum_failures_preserve_previous_site(self): + for failure in ("short", "long", "checksum", "limit"): + with self.subTest(failure=failure): + self.requests.clear() + asset = self.set_asset("web-v1.zip", zipped(self.entries)) + if failure == "short": + self.responses[asset["browser_download_url"]] = b"short" + elif failure == "long": + self.responses[asset["browser_download_url"]] += b"x" + elif failure == "checksum": + asset["digest"] = "sha256:" + "0" * 64 + else: + asset["size"] = 128 * 1024 * 1024 + 1 + self.assert_rejected("(archive|checksum)") + + def test_missing_or_corrupt_library_never_publishes_the_new_converter(self): + for failure in ("missing", "corrupt"): + with self.subTest(failure=failure): + self.requests.clear() + self.set_asset("prx-linux-v1.zip", zipped([("libs/libkernel.prx", b"linux")])) + if failure == "missing": + self.release["assets"] = [asset for asset in self.release["assets"] if asset["name"] != "prx-linux-v1.zip"] + else: + self.set_asset("prx-linux-v1.zip", zipped([("../escape", b"bad")])) + self.assert_rejected("(release asset|archive layout)") + + def test_library_download_failure_preserves_previous_site_and_removes_staging(self): + self.seed_previous() + previous = self.snapshot() + + def failed_request(url): + if url.endswith("prx-linux-v1.zip"): + raise OSError("connection lost") + return self.request(url) + + with patch.object(build_web, "request", failed_request), self.assertRaisesRegex(OSError, "connection lost"): + build_web.publish_release(self.output) + self.assertEqual(self.snapshot(), previous) + self.assertEqual(list(self.build.glob(".web-release-*")), []) + + def test_invalid_release_tag_is_rejected_before_downloading_assets(self): + for tag in ("../v1", "v1."): + with self.subTest(tag=tag): + self.requests.clear() + self.release["tag_name"] = tag + self.assert_rejected("Invalid release tag") + self.assertEqual(self.requests, [LATEST]) + + def test_packaged_web_archive_is_accepted_by_the_publisher(self): + local = self.root / "local/web" + for name, content in self.contents.items(): + path = local / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(content) + dist = self.root / "dist" + result = subprocess.run([sys.executable, str(Path(package_release.__file__)), "--platform", "web", + "--build", str(local.parent), "--output", str(dist), "--version", "v1", + "--revision", REVISION], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.set_asset("web-v1.zip", (dist / "web-v1.zip").read_bytes()) + self.publish() + for name, content in self.contents.items(): + self.assertEqual((self.output / name).read_bytes(), content) + + +class WebPackageTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name) + self.build = self.root / "build" + self.output = self.root / "dist" + self.output.mkdir() + self.asset = self.output / "web-v1.zip" + self.asset.write_bytes(b"previous asset") + for name in FILES: + path = self.build / "web" / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(name.encode()) + + def test_missing_or_empty_web_file_preserves_previous_archive(self): + file = self.build / "web/native/relinker_wasm.wasm" + for empty in (False, True): + with self.subTest(empty=empty): + if empty: + file.write_bytes(b"") + else: + file.unlink() + with self.assertRaisesRegex(RuntimeError, "Missing or empty web release file"): + package_release.package_web(self.build, self.output, "v1", REVISION) + self.assertEqual(self.asset.read_bytes(), b"previous asset") + + def test_package_includes_every_frontend_source(self): + frontend = {file.name: file.read_bytes() for file in (package_release.ROOT / "demo/web").iterdir() if file.is_file()} + for name, content in frontend.items(): + (self.build / "web" / name).write_bytes(content) + package_release.package_web(self.build, self.output, "v1", REVISION) + with zipfile.ZipFile(self.asset) as archive: + self.assertEqual(set(archive.namelist()), {*frontend, "native/relinker_wasm.mjs", "native/relinker_wasm.wasm", "web-release.json"}) + for name, content in frontend.items(): + self.assertEqual(archive.read(name), content) + + def test_invalid_tag_or_revision_does_not_replace_existing_archive(self): + for tag, revision in (("../v1", REVISION), ("v1.", REVISION), ("v1", "HEAD")): + with self.subTest(tag=tag, revision=revision), self.assertRaises(ValueError): + package_release.package_web(self.build, self.output, tag, revision) + self.assertEqual(self.asset.read_bytes(), b"previous asset") + + def test_archive_write_failure_preserves_previous_archive(self): + with patch.object(package_release.zipfile.ZipFile, "write", side_effect=OSError("disk full")), \ + self.assertRaisesRegex(OSError, "disk full"): + package_release.package_web(self.build, self.output, "v1", REVISION) + self.assertEqual(self.asset.read_bytes(), b"previous asset") + self.assertEqual(list(self.output.glob(".release-*")), []) + + def test_cli_rejects_missing_or_misplaced_revision_without_writing_assets(self): + base = [sys.executable, str(Path(package_release.__file__)), "--build", str(self.build), + "--output", str(self.output), "--version", "v1"] + for arguments in (["--platform", "web"], ["--platform", "linux", "--revision", REVISION]): + with self.subTest(arguments=arguments): + result = subprocess.run([*base, *arguments], capture_output=True, text=True) + self.assertEqual(result.returncode, 2) + self.assertIn("--revision", result.stderr) + self.assertEqual(self.asset.read_bytes(), b"previous asset") + + +class TestWebRunnerTests(unittest.TestCase): + def test_main_rejects_missing_archive_file_without_traceback(self): + with tempfile.TemporaryDirectory() as tmp: + build = Path(tmp) / "build" + build.mkdir() + with self.assertRaises(SystemExit) as error, contextlib.redirect_stderr(io.StringIO()) as err: + test_web.main(["--build", str(build)]) + self.assertEqual(error.exception.code, 2) + self.assertIn("archive module does not exist", err.getvalue()) + + def test_main_rejects_archive_missing_zip_url(self): + with tempfile.TemporaryDirectory() as tmp: + build = Path(tmp) / "build" + (build / "web").mkdir(parents=True) + (build / "web/archive.mjs").write_text("const x = 1;\n", encoding="utf-8") + with patch("subprocess.run"): + with self.assertRaises(SystemExit) as error, contextlib.redirect_stderr(io.StringIO()) as err: + test_web.main(["--build", str(build)]) + self.assertEqual(error.exception.code, 2) + self.assertIn("zip library URL not found", err.getvalue()) + + def test_main_decodes_archive_with_utf8_bom(self): + with tempfile.TemporaryDirectory() as tmp: + build = Path(tmp) / "build" + (build / "web").mkdir(parents=True) + (build / "web/archive.mjs").write_text('\ufefffrom "https://cdn.jsdelivr.net/example/zip.mjs";\n', encoding="utf-8") + with patch("subprocess.run"), \ + patch("urllib.request.urlopen") as mock_url: + mock_resp = mock_url.return_value.__enter__.return_value + mock_resp.read.return_value = b"zip-content" + result = test_web.main(["--build", str(build)]) + self.assertEqual(result, 0) + + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + out, err = Stream(), Stream() + with patch.object(test_web.sys, "stdout", out), patch.object(test_web.sys, "stderr", err): + with self.assertRaises(SystemExit): + test_web.main([]) + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/trace_digest.py b/tools/trace_digest.py index a3afe61b5e..6d03d2200c 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -2,13 +2,16 @@ import argparse import bisect import collections import gzip +import html import json import sys +import zlib from pathlib import Path DESCRIPTION = """Digest of an APS5_TRACE_FRAMES / APS5_TRACE_SECONDS capture (aps5-trace-*.json.gz) for an issue or a pull request: the summary line, the slowest frames with their largest slices, the busy and wait time of each -thread, the largest slices per frame, the longest waits and, with --stacks, the hottest functions of an +thread, the largest slices per frame, the longest waits, the largest GPU batches with the CPU slices that +recorded them (when the trace carries batch ids) and, with --stacks, the hottest functions of an APS5_SAMPLE_WORKER collapsed-stack file. The summary line is read from aps5-summary.jsonl next to the trace unless --summary names another file. @@ -20,7 +23,7 @@ RAMP = "._:-=+*#%@" def is_wait(name): - return "wait" in name.rsplit(".", 1)[-1].lower() + return "wait" in name.lower() class Trace: @@ -29,12 +32,17 @@ class Trace: self.slices = collections.defaultdict(list) self.presents = [] self.folded = collections.defaultdict(float) - for event in document["traceEvents"]: + self.batched = [] + events = document if isinstance(document, list) else (document.get("traceEvents") or []) + for event in events: phase = event.get("ph") if phase == "M" and event.get("name") == "thread_name": self.threads[event["tid"]] = event["args"]["name"] elif phase == "X": self.slices[event["tid"]].append([float(event["ts"]), float(event["dur"]), event["name"], 0, False]) + batch = event.get("args", {}).get("batch") + if batch: + self.batched.append((event["tid"], float(event["ts"]), float(event["dur"]), event["name"], batch)) elif phase == "i" and event.get("name") == "present": self.presents.append((float(event["ts"]), event.get("args", {}).get("present"))) elif phase == "C": @@ -44,12 +52,24 @@ class Trace: slices.sort(key=lambda entry: (entry[0], -entry[1])) stack = [] for entry in slices: - while stack and entry[0] >= stack[-1][0] + stack[-1][1]: + start = round(entry[0] * 1000) + while stack and start >= round(stack[-1][0] * 1000) + round(stack[-1][1] * 1000): stack.pop() entry[3] = len(stack) entry[4] = any(is_wait(parent[2]) for parent in stack) stack.append(entry) + def select_threads(self, names): + available = self.threads.keys() | self.slices.keys() | self.folded.keys() + unknown = set(names) - {self.name(tid) for tid in available} + if unknown: + raise ValueError("unknown thread: " + ", ".join(sorted(unknown))) + selected = {tid for tid in available if self.name(tid) in names} + self.threads = {tid: name for tid, name in self.threads.items() if tid in selected} + self.slices = {tid: slices for tid, slices in self.slices.items() if tid in selected} + self.folded = {tid: value for tid, value in self.folded.items() if tid in selected} + self.batched = [entry for entry in self.batched if entry[0] in selected] + def name(self, tid): return self.threads.get(tid, f"tid {tid}") @@ -63,7 +83,7 @@ class Trace: def load_trace(path): opener = gzip.open if str(path).endswith(".gz") else open - with opener(path, "rt", encoding="utf-8") as handle: + with opener(path, "rt", encoding="utf-8-sig") as handle: return Trace(json.load(handle)) @@ -71,20 +91,29 @@ def load_summary(path, trace_path): candidate = Path(path) if path else Path(trace_path).with_name("aps5-summary.jsonl") if not candidate.exists(): return None - lines = [json.loads(line) for line in candidate.read_text(encoding="utf-8").splitlines() if line.strip()] - for line in reversed(lines): - if line.get("trace") == Path(trace_path).name: - return line + lines = [json.loads(line) for line in candidate.read_text(encoding="utf-8-sig").splitlines() if line.strip()] + name = Path(trace_path).name + alternate = name[:-3] if name.endswith(".gz") else name + ".gz" + for filename in (name, alternate): + for line in reversed(lines): + if line.get("trace") == filename: + return line return None def load_stacks(path): stacks = [] - for line in Path(path).read_text(encoding="utf-8").splitlines(): + for number, line in enumerate(Path(path).read_text(encoding="utf-8-sig").splitlines(), 1): if line.startswith("#") or not line.strip(): continue frames, _, count = line.rpartition(" ") - stacks.append((frames.split(";"), int(count))) + try: + weight = int(count) + except ValueError as error: + raise ValueError(f"{path}:{number}: sample count must be a positive integer") from error + if weight <= 0 or any(not frame.strip() for frame in frames.split(";")): + raise ValueError(f"{path}:{number}: expected nonempty frames and a positive sample count") + stacks.append((frames.split(";"), weight)) return stacks @@ -110,16 +139,23 @@ def digest(trace, summary=None, stacks=None, worst=5, top=10): threads = [] totals = collections.Counter() waits = [] - for tid, slices in sorted(trace.slices.items()): - traced = sum(dur for _, dur, _, depth, _ in slices if depth == 0) - waited = sum(dur for _, dur, name, _, nested in slices if is_wait(name) and not nested) + for tid in sorted(trace.slices.keys() | trace.folded.keys()): + slices = trace.slices.get(tid, []) + traced = 0.0 + waited = 0.0 leaders = collections.Counter() for ts, dur, name, depth, nested in slices: - totals[name] += dur - if depth == 0 and not is_wait(name): - leaders[name] += dur - if is_wait(name) and not nested: - waits.append((dur, name, trace.name(tid), ts)) + overlap = max(min(ts + dur, frames[-1][2]) - max(ts, frames[0][1]), 0.0) if frames else dur + if overlap > 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 and (overlap > 0 or not frames): + waits.append((overlap, name, trace.name(tid), max(ts, frames[0][1]) if frames else ts)) threads.append({"thread": trace.name(tid), "busy_ms_per_frame": (traced - waited) / 1000.0 / count, "wait_ms_per_frame": waited / 1000.0 / count, "untraced_ms_per_frame": max(span_us - traced, 0.0) / 1000.0 / count, @@ -134,6 +170,23 @@ def digest(trace, summary=None, stacks=None, worst=5, top=10): "present": trace.presents[bisect.bisect_right(starts, ts) - 1][1] if starts and ts >= starts[0] else None} for dur, name, thread, ts in sorted(waits, reverse=True)[:top]] + gpu = {tid for tid, name in trace.threads.items() if name == "gpu"} + batches = [entry for entry in trace.batched if entry[0] in gpu and entry[3] == "batch"] + if batches: + ranges = collections.defaultdict(collections.Counter) + recorded = collections.defaultdict(collections.Counter) + for tid, ts, dur, name, batch in trace.batched: + if tid in gpu: + if name != "batch": + ranges[batch][name] += dur + else: + recorded[batch][(name, trace.name(tid))] += dur + result["largest_gpu_batches"] = [{"batch": batch, "ms": dur / 1000.0, "at_ms": ts / 1000.0, + "present": trace.presents[bisect.bisect_right(starts, ts) - 1][1] if starts and ts >= starts[0] else None, + "gpu_ranges": [{"name": name, "ms": us / 1000.0} for name, us in ranges[batch].most_common(3)], + "recorded_by": [{"name": name, "thread": thread, "ms": us / 1000.0} for (name, thread), us in recorded[batch].most_common(4)]} + for _, ts, dur, _, batch in sorted(batches, key=lambda entry: -entry[2])[:worst]] + if stacks is not None: samples = sum(count for _, count in stacks) own = collections.Counter() @@ -161,8 +214,15 @@ 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 = [] + if result.get("thread_filter"): + lines += ["Thread filter: " + ", ".join(table_cell(name) for name in result["thread_filter"]) + + ". Frame bounds, summary and stack samples remain capture-wide.", ""] summary = result["summary"] if summary: frame = summary["frame_ms"] @@ -190,8 +250,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("") @@ -199,6 +259,14 @@ def markdown(result): lines.append("") for wait in result["longest_waits"]: lines.append(f"- {wait['name']} ({wait['thread']}) {wait['ms']:.1f} ms at {wait['at_ms']:.0f} ms, present {wait['present']}") + if "largest_gpu_batches" in result: + lines.append("") + lines.append("**Largest GPU batches** (GPU ranges inside, and the CPU slices that recorded each, inclusive)") + lines.append("") + for batch in result["largest_gpu_batches"]: + ranges = ", ".join(f"{entry['name']} {entry['ms']:.1f}" for entry in batch["gpu_ranges"]) or "no ranges" + recorded = ", ".join(f"{entry['name']} ({entry['thread']}) {entry['ms']:.1f}" for entry in batch["recorded_by"]) or "no CPU slices" + lines.append(f"- batch {batch['batch']}: {batch['ms']:.1f} ms at {batch['at_ms']:.0f} ms, present {batch['present']}: GPU {ranges}; recorded by {recorded}") if "stacks" in result: stacks = result["stacks"] lines.append("") @@ -210,7 +278,7 @@ def markdown(result): def load_intervals(path): - lines = [json.loads(line) for line in Path(path).read_text(encoding="utf-8").splitlines() if line.strip()] + lines = [json.loads(line) for line in Path(path).read_text(encoding="utf-8-sig").splitlines() if line.strip()] return [line for line in lines if "interval_start_s" in line] @@ -226,15 +294,16 @@ def digest_intervals(lines): for line in lines: workers = line.get("worker_busy_pct", {}) busiest = max(workers.items(), key=lambda entry: entry[1]) if workers else None - phases = list(line.get("top_phases_ms_per_frame", {}).items()) + phases = line.get("top_phases_ms_per_frame", {}) + largest = max(phases.items(), key=lambda entry: entry[1]) if phases else None rows.append({"start_s": line["interval_start_s"], "end_s": line["interval_end_s"], "presents": line["presents"], "frame_ms": line["frame_ms"], "gpu_busy_ms_per_present": line["gpu_busy_ms_per_present"], "busiest_worker": {"thread": busiest[0], "pct": busiest[1]} if busiest else None, - "top_phase": {"name": phases[0][0], "ms_per_frame": phases[0][1]} if phases else None, + "top_phase": {"name": largest[0], "ms_per_frame": largest[1]} if largest else None, "trace_dropped": line.get("trace_dropped", False)}) first = lines[0] if lines else {} keys = ("commit", "dirty", "env", "gpu", "driver", "os", "resolution") - changed = [index for index, line in enumerate(lines) if any(line.get(key) != first.get(key) for key in keys)] + changed = [index for index in range(1, len(lines)) if any(lines[index].get(key) != lines[index - 1].get(key) for key in keys)] return {"intervals": rows, "identity": {key: first.get(key) for key in keys}, "identity_changed_at": changed} @@ -254,31 +323,60 @@ 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("--thread", action="append", default=[], metavar="NAME", + help="include event tracks with this exact thread name; repeat to select several tracks") + parser.add_argument("--frames", type=nonnegative_integer, default=5, help="slowest frames to list (default 5)") + parser.add_argument("--top", type=nonnegative_integer, default=10, help="entries per list (default 10)") parser.add_argument("--json", action="store_true", help="print the digest as JSON") options = parser.parse_args(arguments) + if options.summary and not Path(options.summary).is_file(): + parser.error(f"summary file not found: {options.summary}") + if options.thread and options.trace.endswith(".jsonl"): + parser.error("--thread requires an event trace, not interval summaries") if options.trace.endswith(".jsonl"): result = digest_intervals(load_intervals(options.trace)) sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else markdown_intervals(result)) return 0 - 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) + try: + stacks = load_stacks(options.stacks) if options.stacks else None + except (OSError, ValueError) as error: + parser.error(str(error)) + trace = load_trace(options.trace) + if options.thread: + trace.select_threads(options.thread) + result = digest(trace, load_summary(options.summary, options.trace), + stacks, options.frames, options.top) + if options.thread: + result["thread_filter"] = sorted(set(options.thread)) sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else markdown(result)) return 0 if __name__ == "__main__": - sys.exit(main()) + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="replace") + try: + sys.exit(main()) + except (EOFError, OSError, ValueError, zlib.error) as error: + print(f"FAIL: {error}", file=sys.stderr) + sys.exit(2) diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py new file mode 100644 index 0000000000..659f4eb07a --- /dev/null +++ b/tools/unwrap_self.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python3 +"""unwrap_self -- turn a decrypted SELF or fSELF container into a clean ELF. + +The relinker reads a plain ELF executable and skips SELF containers with a warning, +so an eboot.bin or a *.prx taken from a title has to be unwrapped first. Two things +differ inside a container: + + - The inner ELF program headers carry stale offsets. A SELF inserts its own + metadata blocks between the segments, so the segment data sits at the offsets + listed in the SELF segment-record table at file offset 0x40, records of 0x20 + bytes holding flags, data offset, size and the size again. A record with flag + 0x800 holds the data of the program header whose index is in flags bits 20 + and up. Segments the table does not cover are placed by the vaddr to file + mapping of the PT_LOAD that contains them. Segments without a vaddr that + follow the last recorded one in the ELF are stored right after it, back to + back in ELF order, except a PT_NOTE, which the container leaves out and which + is emitted without file bytes. Some dumps end right after the recorded ones + and leave out the version segment as well, so one that would run past the + end of the file is emitted without file bytes too. Other segments without a + vaddr carry the shift of the last placed one. + - The segments are packed without honouring p_align, while ELF requires + p_offset % p_align == p_vaddr % p_align. Every region is copied to an offset + that satisfies it, which the relinker enforces on guest modules. + +Only decrypted containers are handled. A container whose load segments all look +encrypted is refused instead of copied: the entropy of each load segment is +measured and a run of segments near eight bits per byte is reported. + +usage: unwrap_self.py [--verbose] +""" + +import argparse +import math +import struct +import sys +from collections import Counter +from pathlib import Path + +SELF_MAGIC = 0x1D3D154F +SELF_MAGIC_KERNEL = 0xEEF51454 +RECORD_OFFSET = 0x40 +RECORD_SIZE = 0x20 +RECORD_HAS_SEGMENT = 0x800 +RECORD_SEGMENT_SHIFT = 20 +FIRST_REGION = 0x400 +MAX_ALIGNMENT_PADDING = 64 * 1024 * 1024 +PT_LOAD = 1 +PT_NOTE = 4 +ENTROPY_LIMIT = 7.5 +ENTROPY_SAMPLE = 0x10000 + +PHDR_NAMES = { + PT_LOAD: "PT_LOAD", + 2: "PT_DYNAMIC", + 3: "PT_INTERP", + 4: "PT_NOTE", + 6: "PT_PHDR", + 7: "PT_TLS", + 0x6474E550: "PT_GNU_EH_FRAME", + 0x6474E551: "PT_GNU_STACK", + 0x6474E552: "PT_GNU_RELRO", + 0x61000000: "PT_SCE_DYNLIBDATA", + 0x61000001: "PT_OS_PROCPARAM", + 0x61000002: "PT_OS_0x61000002", + 0x6FFFFF00: "PT_OS_0x6fffff00", + 0x6FFFFF01: "PT_OS_0x6fffff01", +} + + +class Refused(Exception): + pass + + +def entropy(data): + if not data: + return 0.0 + counts = Counter(data) + length = len(data) + return -sum((count / length) * math.log2(count / length) for count in counts.values()) + + +def read_records(data): + records = [] + offset = RECORD_OFFSET + while offset + RECORD_SIZE <= FIRST_REGION: + flags, segment, size, size_again = struct.unpack_from("= len(data) or segment < 0x350 or segment % 0x10 != 0: + break + if size > len(data): + break + records.append((flags, segment, size)) + offset += RECORD_SIZE + return records + + +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]) + if header[4] != 2 or header[5] != 1: + raise Refused("inner ELF is not 64-bit little-endian") + 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, + "align": alignment, "source": file_offset, "host": None, + }) + return inner, header, headers + + +def place_segments(data, headers, records): + loads = [header for header in headers if header["type"] == PT_LOAD] + used = set() + placed = set() + for index, (flags, segment, size) in enumerate(records): + target = flags >> RECORD_SEGMENT_SHIFT + if flags & RECORD_HAS_SEGMENT and target < len(headers) and target not in placed and headers[target]["size"] == size: + headers[target]["source"] = segment + placed.add(target) + used.add(index) + recorded = [header for header in headers if header["index"] in placed and header["vaddr"] == 0 and header["type"] != PT_LOAD] + if recorded: + last = max(recorded, key=lambda header: header["offset"]) + cursor = last["source"] + last["size"] + trailing = [header for header in headers if header["index"] not in placed and header["vaddr"] == 0 + and header["type"] != PT_LOAD and header["size"] and header["offset"] > last["offset"]] + for header in sorted(trailing, key=lambda header: header["offset"]): + placed.add(header["index"]) + if header["type"] == PT_NOTE: + header["source"] = 0 + header["size"] = 0 + continue + header["source"] = cursor + cursor += header["size"] + if cursor > len(data): + header["source"] = 0 + header["size"] = 0 + for header in loads: + if header["size"] == 0 or header["index"] in placed: + continue + for index, (_, segment, size) in enumerate(records): + if index in used: + continue + if size == header["size"]: + used.add(index) + header["source"] = segment + break + + def load_for(vaddr, size): + found = None + for header in loads: + 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 + + tail = None + for header in headers: + if header["type"] == PT_LOAD: + continue + if header["index"] in placed: + if header["size"] and (tail is None or header["offset"] > tail[0]): + tail = (header["offset"], header["source"] - header["offset"]) + continue + if header["vaddr"] != 0: + 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"])) + offset = header["vaddr"] - host["vaddr"] + if header["size"] and (offset > host["size"] or header["size"] > host["size"] - offset): + raise Refused("segment %d does not fit the file-backed range of PT_LOAD %d" + % (header["index"], host["index"])) + if host["size"] == 0 or offset > host["size"]: + header["source"] = 0 + continue + header["host"] = host + header["source"] = host["source"] + offset + if tail is None or header["offset"] > tail[0]: + tail = (header["offset"], header["source"] - header["offset"]) + elif header["size"]: + header["source"] = header["offset"] + (tail[1] if tail else 0) + if header["source"] > len(data): + header["source"] = len(data) + keep = max(0, len(data) - header["source"]) + header["size"] = min(header["size"], keep) + 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"]) + + +def check_decrypted(data, headers): + encrypted = [] + for header in headers: + if header["type"] != PT_LOAD or header["size"] == 0: + continue + sample = data[header["source"]:header["source"] + min(header["size"], ENTROPY_SAMPLE)] + value = entropy(sample) + if value >= ENTROPY_LIMIT: + encrypted.append((header["index"], header["source"], value)) + if encrypted and len(encrypted) == len([h for h in headers if h["type"] == PT_LOAD and h["size"]]): + listed = ", ".join("segment %d at %#x (%.2f bits per byte)" % item for item in encrypted) + raise Refused("every load segment looks encrypted: " + listed) + + +def unwrap(path, output, verbose=False): + 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(" MAX_ALIGNMENT_PADDING: + raise Refused("segment alignment requires more than %d bytes of output padding" % MAX_ALIGNMENT_PADDING) + cursor = empty_end + + result = bytearray(cursor) + for region in regions: + result[region["new"]:region["new"] + region["size"]] = data[region["old"]:region["old"] + region["size"]] + + for header in headers: + if header["size"] == 0 and (header["type"] == PT_LOAD or header["source"] == 0): + header["new"] = header["vaddr"] % max(header["align"], 1) if header["type"] == PT_LOAD else 0 + continue + host = header if header["type"] == PT_LOAD else header["host"] + region = load_regions.get(host["index"]) if host is not None else None + if region is None: + region = region_for(header["source"], header["size"]) + header["new"] = header["source"] if region is None else region["new"] + (header["source"] - region["old"]) + if header["new"] + header["size"] > len(result): + raise Refused("segment %d escaped the output" % header["index"]) + + result[0:0x40] = elf_header + struct.pack_into("
Date{escape(label)}%
0